commit e083d8f5d905b123e807d66bc3afc3436e1f74aa Author: wehub-resource-sync Date: Mon Jul 13 12:10:44 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.cspell/frigate-dictionary.txt b/.cspell/frigate-dictionary.txt new file mode 100644 index 0000000..23ad525 --- /dev/null +++ b/.cspell/frigate-dictionary.txt @@ -0,0 +1,330 @@ +aarch +absdiff +airockchip +Alloc +alpr +Amcrest +amdgpu +analyzeduration +Annke +apexcharts +arange +argmax +argmin +argpartition +ascontiguousarray +astype +authelia +authentik +autodetected +automations +autotrack +autotracked +autotracker +autotracking +backchannel +balena +Beelink +BGRA +BHWC +blackshear +blakeblackshear +bottombar +buildx +castable +cdist +Celeron +cgroups +chipset +chromadb +Chromecast +cmdline +codeowner +CODEOWNERS +codeproject +colormap +colorspace +comms +cooldown +coro +ctypeslib +CUDA +Cuvid +Dahua +datasheet +debconf +deci +deepstack +defragment +devcontainer +DEVICEMAP +discardcorrupt +dpkg +dsize +dtype +ECONNRESET +edgetpu +facenet +fastapi +faststart +fflags +ffprobe +fillna +flac +foscam +fourcc +framebuffer +fregate +frégate +fromarray +frombuffer +frontdoor +fstype +fullchain +fullscreen +genai +generativeai +genpts +getpid +gpuload +HACS +Hailo +hass +hconcat +healthcheck +hideable +Hikvision +homeassistant +homekit +homography +hsize +hstack +httpx +hwaccel +hwdownload +hwmap +hwupload +iloc +imagestream +imdecode +imencode +imread +imwrite +inpoint +interp +iostat +iotop +itemsize +Jellyfin +jetson +jetsons +jina +jinaai +joserfc +jsmpeg +jsonify +Kalman +keepalive +keepdims +labelmap +letsencrypt +levelname +LIBAVFORMAT +libedgetpu +libnvinfer +libva +libwebp +libx +libyolo +linalg +localzone +logpipe +Loryta +lstsq +lsusb +markupsafe +maxsplit +MEMHOSTALLOC +memlimit +meshgrid +metadatas +migraphx +minilm +mjpeg +mkfifo +mobiledet +mobilenet +modelpath +mosquitto +mountpoint +movflags +mpegts +mqtt +mse +msenc +muxing +namedtuples +nbytes +nchw +ndarray +ndimage +nethogs +newaxis +nhwc +NOBLOCK +nobuffer +nokey +NONBLOCK +noninteractive +noprint +Norfair +nptype +NTSC +numpy +nvenc +nvhost +nvml +nvmpi +ollama +onnx +onnxruntime +onvif +ONVIF +openai +opencv +openvino +overfitting +OWASP +paddleocr +paho +passwordless +PCMA +PCMU +popleft +posthog +postprocess +poweroff +preexec +probesize +protobuf +pstate +psutil +pubkey +putenv +pycache +pydantic +pyobj +pysqlite +pytz +pywebpush +qnap +quantisation +Radeon +radeonsi +radeontop +rawvideo +rcond +RDONLY +realmonitor +rebranded +recvonly +referer +reindex +Reolink +restream +restreamed +restreaming +RJSF +rkmpp +rknn +rkrga +rockchip +rocm +rocminfo +rootfs +rtmp +RTSP +rtsps +rtspx +ruamel +scroller +sendonly +setproctitle +setpts +shms +SIGUSR +skylake +sleeptime +SNDMORE +socs +sqliteq +sqlitevecq +Srtp +ssdlite +statm +stimeout +stylelint +subclassing +substream +superfast +surveillance +svscan +Swipeable +sysconf +tailscale +Tapo +tensorrt +tflite +thresholded +timelapse +titlecase +tmpfs +tobytes +toggleable +traefik +tzlocal +Ubiquiti +udev +udevadm +ultrafast +unichip +unidecode +Unifi +unixepoch +unraid +unreviewed +userdata +usermod +uvicorn +vaapi +vainfo +variations +vbios +vconcat +vitb +vstream +vsync +wallclock +webp +webpush +webrtc +websockets +webui +werkzeug +workdir +WRONLY +wsgirefserver +wsgiutils +wsize +xaddr +xmaxs +xmins +XPUB +XSUB +ymaxs +ymins +yolo +yolonas +yolox +zeep +zerolatency diff --git a/.cursor/rules/frontend-always-use-translation-files.mdc b/.cursor/rules/frontend-always-use-translation-files.mdc new file mode 100644 index 0000000..3503406 --- /dev/null +++ b/.cursor/rules/frontend-always-use-translation-files.mdc @@ -0,0 +1,6 @@ +--- +globs: ["**/*.ts", "**/*.tsx"] +alwaysApply: false +--- + +Never write strings in the frontend directly, always write to and reference the relevant translations file. \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c782fb3 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,125 @@ +{ + "name": "Frigate Devcontainer", + "dockerComposeFile": "../docker-compose.yml", + "service": "devcontainer", + "workspaceFolder": "/workspace/frigate", + "initializeCommand": ".devcontainer/initialize.sh", + "postCreateCommand": ".devcontainer/post_create.sh", + "overrideCommand": false, + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {} + // Uncomment the following lines to use ONNX Runtime with CUDA support + // "ghcr.io/devcontainers/features/nvidia-cuda:1": { + // "installCudnn": true, + // "installNvtx": true, + // "installToolkit": true, + // "cudaVersion": "12.5", + // "cudnnVersion": "9.4.0.58" + // }, + // "./features/onnxruntime-gpu": {} + }, + "forwardPorts": [ + 8971, + 5000, + 5001, + 5173, + 8554, + 8555 + ], + "portsAttributes": { + "8971": { + "label": "External NGINX", + "onAutoForward": "silent" + }, + "5000": { + "label": "Internal NGINX", + "onAutoForward": "silent" + }, + "5001": { + "label": "Frigate API", + "onAutoForward": "silent" + }, + "5173": { + "label": "Vite Server", + "onAutoForward": "silent" + }, + "8554": { + "label": "gortc RTSP", + "onAutoForward": "silent" + }, + "8555": { + "label": "go2rtc WebRTC", + "onAutoForward": "silent" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "visualstudioexptteam.vscodeintellicode", + "mhutchie.git-graph", + "ms-azuretools.vscode-docker", + "streetsidesoftware.code-spell-checker", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "mikestead.dotenv", + "csstools.postcss", + "blanu.vscode-styled-jsx", + "bradlc.vscode-tailwindcss", + "charliermarsh.ruff", + "eamodio.gitlens" + ], + "settings": { + "remote.autoForwardPorts": false, + "python.formatting.provider": "none", + "python.languageServer": "Pylance", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true, + "python.testing.unittestArgs": [ + "-v", + "-s", + "./frigate/test" + ], + "files.trimTrailingWhitespace": true, + "eslint.workingDirectories": [ + "./web" + ], + "isort.args": [ + "--settings-path=./pyproject.toml" + ], + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + } + }, + "[json][jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsx][js][tsx][ts]": { + "editor.codeActionsOnSave": [ + "source.addMissingImports", + "source.fixAll" + ], + "editor.tabSize": 2 + }, + "cSpell.ignoreWords": [ + "rtmp" + ], + "cSpell.words": [ + "preact", + "astype", + "hwaccel", + "mqtt" + ] + } + } + } +} \ No newline at end of file diff --git a/.devcontainer/features/onnxruntime-gpu/devcontainer-feature.json b/.devcontainer/features/onnxruntime-gpu/devcontainer-feature.json new file mode 100644 index 0000000..3051444 --- /dev/null +++ b/.devcontainer/features/onnxruntime-gpu/devcontainer-feature.json @@ -0,0 +1,22 @@ +{ + "id": "onnxruntime-gpu", + "version": "0.0.1", + "name": "ONNX Runtime GPU (Nvidia)", + "description": "Installs ONNX Runtime for Nvidia GPUs.", + "documentationURL": "", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest", + "1.20.1", + "1.20.0" + ], + "default": "latest", + "description": "Version of ONNX Runtime to install" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/nvidia-cuda" + ] +} \ No newline at end of file diff --git a/.devcontainer/features/onnxruntime-gpu/install.sh b/.devcontainer/features/onnxruntime-gpu/install.sh new file mode 100644 index 0000000..0c090be --- /dev/null +++ b/.devcontainer/features/onnxruntime-gpu/install.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -e + +VERSION=${VERSION} + +python3 -m pip config set global.break-system-packages true +# if VERSION == "latest" or VERSION is empty, install the latest version +if [ "$VERSION" == "latest" ] || [ -z "$VERSION" ]; then + python3 -m pip install onnxruntime-gpu +else + python3 -m pip install onnxruntime-gpu==$VERSION +fi + +echo "Done!" \ No newline at end of file diff --git a/.devcontainer/initialize.sh b/.devcontainer/initialize.sh new file mode 100755 index 0000000..2300df1 --- /dev/null +++ b/.devcontainer/initialize.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +# These folders needs to be created and owned by the host user +mkdir -p debug web/dist + +if [[ -f "config/config.yml" ]]; then + echo "config/config.yml already exists, skipping initialization" >&2 +else + echo "initializing config/config.yml" >&2 + cp -fv config/config.yml.example config/config.yml +fi diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh new file mode 100755 index 0000000..fcf7ca6 --- /dev/null +++ b/.devcontainer/post_create.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -euxo pipefail + +# Cleanup the old github host key +if [[ -f ~/.ssh/known_hosts ]]; then + # Add new github host key + sed -i -e '/AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31\/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi\/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==/d' ~/.ssh/known_hosts + curl -L https://api.github.com/meta | jq -r '.ssh_keys | .[]' | \ + sed -e 's/^/github.com /' >> ~/.ssh/known_hosts +fi + +# Frigate normal container runs as root, so it have permission to create +# the folders. But the devcontainer runs as the host user, so we need to +# create the folders and give the host user permission to write to them. +sudo mkdir -p /media/frigate +sudo chown -R "$(id -u):$(id -g)" /media/frigate + +# When started as a service, LIBAVFORMAT_VERSION_MAJOR is defined in the +# s6 service file. For dev, where frigate is started from an interactive +# shell, we define it in .bashrc instead. +echo 'export LIBAVFORMAT_VERSION_MAJOR=$("$(python3 /usr/local/ffmpeg/get_ffmpeg_path.py)" -version | grep -Po "libavformat\W+\K\d+")' >> "$HOME/.bashrc" + +make version + +cd web + +npm install + +npm run build diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b22b1b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +README.md +docs/ +.gitignore +debug +config/ +*.pyc +.git +core +*.mp4 +*.jpg +*.db +*.ts + +web/dist/ +web/node_modules/ +web/.npm diff --git a/.github/DISCUSSION_TEMPLATE/beta-support.yml b/.github/DISCUSSION_TEMPLATE/beta-support.yml new file mode 100644 index 0000000..bad899f --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/beta-support.yml @@ -0,0 +1,131 @@ +title: "[Beta Support]: " +labels: ["support", "triage", "beta"] +body: + - type: markdown + attributes: + value: | + Thank you for testing Frigate beta versions! Use this form for support with beta releases. + + **Note:** Beta versions may have incomplete features, known issues, or unexpected behavior. Please check the [release notes](https://github.com/blakeblackshear/frigate/releases) and [recent discussions][discussions] for known beta issues before submitting. + + Before submitting, read the [beta documentation][docs]. + + [docs]: https://docs-dev.frigate.video/ + [discussions]: https://github.com/blakeblackshear/frigate/discussions + - type: textarea + id: description + attributes: + label: Describe the problem you are having + description: Please be as detailed as possible. Include what you expected to happen vs what actually happened. + validations: + required: true + - type: input + id: version + attributes: + label: Beta Version + description: Visible on the System Metrics page in the Web UI. Please include the full version including the build identifier (eg. 0.18.0-beta1, 0.18.0-8b72c7a, etc.) + placeholder: "0.18.0-beta1" + validations: + required: true + - type: dropdown + id: issue-category + attributes: + label: Issue Category + description: What area is your issue related to? This helps us understand the context. + options: + - Object Detection / Detectors + - Hardware Acceleration + - Configuration / Setup + - WebUI / Frontend + - Recordings / Storage + - Notifications / Events + - Integration (Home Assistant, etc) + - Performance / Stability + - Installation / Updates + - Other + validations: + required: true + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. Remove any sensitive information like passwords or URLs. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output (if applicable) + description: If your issue involves cameras, streams, or playback, please include go2rtc logs. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant App + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via installation script + - Proxomox via VM + - Windows WSL2 + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. Include relevant environment variables and device mappings. + render: yaml + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Home Assistant OS + - Debian + - Ubuntu + - Other Linux + - Proxmox + - UNRAID + - Windows + - Other + validations: + required: true + - type: input + id: hardware + attributes: + label: CPU / GPU / Hardware + description: Provide details about your hardware (e.g., Intel i5-9400, NVIDIA RTX 3060, Raspberry Pi 4, etc) + placeholder: "Intel i7-10700, NVIDIA GTX 1660" + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Screenshots of the issue, System metrics pages, or any relevant UI. Drag and drop or paste images directly. + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce + description: If applicable, provide detailed steps to reproduce the issue + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. See error + - type: textarea + id: other + attributes: + label: Any other information that may be helpful + description: Additional context, related issues, when the problem started appearing, etc. diff --git a/.github/DISCUSSION_TEMPLATE/camera-support.yml b/.github/DISCUSSION_TEMPLATE/camera-support.yml new file mode 100644 index 0000000..2c06492 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/camera-support.yml @@ -0,0 +1,139 @@ +title: "[Camera Support]: " +labels: ["support", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form for support or questions for an issue with your cameras. + + Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: Describe the problem you are having + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: input + attributes: + label: What browser(s) are you using? + placeholder: Google Chrome 88.0.4324.150 + description: > + Provide the full name and don't forget to add the version! + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output + description: Please copy and paste any relevant go2rtc log output. Include logs before and after your exact error when possible. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: ffprobe + attributes: + label: FFprobe output from your camera + description: Run `ffprobe ` from within the Frigate container if possible, and provide output below + render: shell + validations: + required: true + - type: textarea + id: stats + attributes: + label: Frigate stats + description: Output from frigate's /api/stats endpoint + render: json + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Home Assistant OS + - Debian + - Other Linux + - Proxmox + - UNRAID + - Windows + - Other + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant App + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via installation script + - Proxomox via VM + - Windows WSL2 + validations: + required: true + - type: dropdown + id: object-detector + attributes: + label: Object Detector + options: + - Coral + - OpenVino + - TensorRT + - RKNN + - Other + - CPU (no coral) + validations: + required: true + - type: dropdown + id: network + attributes: + label: Network connection + options: + - Wired + - Wireless + - Mixed + validations: + required: true + - type: input + id: camera + attributes: + label: Camera make and model + description: Dahua, hikvision, amcrest, reolink, etc and model number + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop for images is possible in this field. Please post screenshots of at least General and Cameras tabs. + validations: + required: true + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/.github/DISCUSSION_TEMPLATE/config-support.yml b/.github/DISCUSSION_TEMPLATE/config-support.yml new file mode 100644 index 0000000..564c908 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/config-support.yml @@ -0,0 +1,114 @@ +title: "[Config Support]: " +labels: ["support", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form for support or questions related to Frigate's configuration and config file. + + Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: Describe the problem you are having + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output + description: Please copy and paste any relevant go2rtc log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: stats + attributes: + label: Frigate stats + description: Output from frigate's /api/stats endpoint + render: json + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Home Assistant OS + - Debian + - Other Linux + - Proxmox + - UNRAID + - Windows + - Other + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant App + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via installation script + - Proxomox via VM + - Windows WSL2 + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: dropdown + id: object-detector + attributes: + label: Object Detector + options: + - Coral + - OpenVino + - TensorRT + - RKNN + - Other + - CPU (no coral) + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop or simple cut/paste is possible in this field + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/.github/DISCUSSION_TEMPLATE/detector-support.yml b/.github/DISCUSSION_TEMPLATE/detector-support.yml new file mode 100644 index 0000000..12c806b --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/detector-support.yml @@ -0,0 +1,88 @@ +title: "[Detector Support]: " +labels: ["support", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form for support or questions related to Frigate's object detectors. + + Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: Describe the problem you are having + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant App + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via installation script + - Proxomox via VM + - Windows WSL2 + validations: + required: true + - type: dropdown + id: object-detector + attributes: + label: Object Detector + options: + - Coral + - OpenVino + - TensorRT + - RKNN + - Other + - CPU (no coral) + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop for images is possible in this field. Please post screenshots of at least General and Cameras tabs. + validations: + required: true + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/.github/DISCUSSION_TEMPLATE/general-support.yml b/.github/DISCUSSION_TEMPLATE/general-support.yml new file mode 100644 index 0000000..78d1f8b --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/general-support.yml @@ -0,0 +1,131 @@ +title: "[Support]: " +labels: ["support", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form for support for issues that don't fall into any specific category. + + Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: Describe the problem you are having + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: input + attributes: + label: What browser(s) are you using? + placeholder: Google Chrome 88.0.4324.150 + description: > + Provide the full name and don't forget to add the version! + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output + description: Please copy and paste any relevant go2rtc log output. Include logs before and after your exact error when possible. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: ffprobe + attributes: + label: FFprobe output from your camera + description: Run `ffprobe ` from within the Frigate container if possible, and provide output below + render: shell + validations: + required: true + - type: textarea + id: stats + attributes: + label: Frigate stats + description: Output from frigate's /api/stats endpoint + render: json + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant App + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via installation script + - Proxmox via VM + - Windows WSL2 + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: dropdown + id: object-detector + attributes: + label: Object Detector + options: + - Coral + - OpenVino + - TensorRT + - RKNN + - Other + - CPU (no coral) + validations: + required: true + - type: dropdown + id: network + attributes: + label: Network connection + options: + - Wired + - Wireless + - Mixed + validations: + required: true + - type: input + id: camera + attributes: + label: Camera make and model + description: Dahua, hikvision, amcrest, reolink, etc and model number + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop for images is possible in this field + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml b/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml new file mode 100644 index 0000000..de8d216 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml @@ -0,0 +1,121 @@ +title: "[HW Accel Support]: " +labels: ["support", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form to submit a support request for hardware acceleration issues. + + Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: Describe the problem you are having + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output + description: Please copy and paste any relevant go2rtc log output. Include logs before and after your exact error when possible. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: ffprobe + attributes: + label: FFprobe output from your camera + description: Run `ffprobe ` from within the Frigate container if possible, and provide output below + render: shell + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant App + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via installation script + - Proxomox via VM + - Windows WSL2 + validations: + required: true + - type: dropdown + id: object-detector + attributes: + label: Object Detector + options: + - Coral + - OpenVino + - TensorRT + - RKNN + - Other + - CPU (no coral) + validations: + required: true + - type: dropdown + id: network + attributes: + label: Network connection + options: + - Wired + - Wireless + - Mixed + validations: + required: true + - type: input + id: camera + attributes: + label: Camera make and model + description: Dahua, hikvision, amcrest, reolink, etc and model number + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop for images is possible in this field. Please post screenshots of at least General and Cameras tabs. + validations: + required: true + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/.github/DISCUSSION_TEMPLATE/question.yml b/.github/DISCUSSION_TEMPLATE/question.yml new file mode 100644 index 0000000..6a4789c --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/question.yml @@ -0,0 +1,21 @@ +title: "[Question]: " +labels: ["question"] +body: + - type: markdown + attributes: + value: | + Use this form for questions you have about Frigate. + + Before submitting your question, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + **If you are looking for support, start a new discussion and use a support category.** + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: "What is your question?" + validations: + required: true diff --git a/.github/DISCUSSION_TEMPLATE/report-a-bug.yml b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml new file mode 100644 index 0000000..012d4ea --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml @@ -0,0 +1,155 @@ +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form to submit a reproducible bug in Frigate or Frigate's UI. + + **⚠️ If you are running a beta version (0.18.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.** + + Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community. + + **If you are unsure if your issue is actually a bug or not, please submit a support request first.** + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [prs]: https://www.github.com/blakeblackshear/frigate/pulls + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + [ai]: https://docs.frigate.video + - type: checkboxes + attributes: + label: Checklist + description: Please verify that you've followed these steps + options: + - label: I have updated to the latest available Frigate version. + required: true + - label: I have cleared the cache of my browser. + required: true + - label: I have tried a different browser to see if it is related to my browser. + required: true + - label: I have tried reproducing the issue in [incognito mode](https://www.computerworld.com/article/1719851/how-to-go-incognito-in-chrome-firefox-safari-and-edge.html) to rule out problems with any third party extensions or plugins I have installed. + - label: I have asked the AI at https://docs.frigate.video about my issue. + required: true + - type: textarea + id: description + attributes: + label: Describe the problem you are having + description: Provide a clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: | + Please tell us exactly how to reproduce your issue. + Provide clear and concise step by step instructions and add code snippets if needed. + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: input + attributes: + label: In which browser(s) are you experiencing the issue with? + placeholder: Google Chrome 88.0.4324.150 + description: > + Provide the full name and don't forget to add the version! + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output + description: Please copy and paste any relevant go2rtc log output. Include logs before and after your exact error when possible. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Home Assistant OS + - Debian + - Other Linux + - Proxmox + - UNRAID + - Windows + - Other + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant App + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via installation script + - Proxomox via VM + - Windows WSL2 + validations: + required: true + - type: dropdown + id: network + attributes: + label: Network connection + options: + - Wired + - Wireless + - Mixed + validations: + required: true + - type: input + id: camera + attributes: + label: Camera make and model + description: Dahua, hikvision, amcrest, reolink, etc and model number + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop for images is possible in this field. Please post screenshots of all tabs. + validations: + required: true + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b8892ce --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +github: + - blakeblackshear + - NickM-27 + - hawkeye217 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a7474bc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Frigate Support + url: https://github.com/blakeblackshear/frigate/discussions/new/choose + about: Get support for setting up or troubleshooting Frigate. + - name: Frigate Bug Report + url: https://github.com/blakeblackshear/frigate/discussions/new/choose + about: Report a specific UI or backend bug. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..57f76d3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Describe what you are trying to accomplish and why in non technical terms** +I want to be able to ... so that I can ... + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..724af45 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,50 @@ +name: 'Setup' +description: 'Set up QEMU and Buildx' +inputs: + GITHUB_TOKEN: + required: true +outputs: + image-name: + value: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ steps.create-short-sha.outputs.SHORT_SHA }} + cache-name: + value: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:cache +runs: + using: "composite" + steps: + # Stop docker so we can mount more space at /var/lib/docker + - name: Stop docker + run: sudo systemctl stop docker + shell: bash + # This creates a virtual volume at /var/lib/docker to maximize the size + # As of 2/14/2024, this results in 97G for docker images + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + build-mount-path: '/var/lib/docker' + - name: Start docker + run: sudo systemctl start docker + shell: bash + - id: lowercaseRepo + uses: ASzc/change-string-case-action@v5 + with: + string: ${{ github.repository }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to the Container registry + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ inputs.GITHUB_TOKEN }} + - name: Create version file + run: make version + shell: bash + - id: create-short-sha + run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + shell: bash diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..db67aa2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,40 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: dev + - package-ecosystem: "docker" + directory: "/docker" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: dev + - package-ecosystem: "pip" + directory: "/docker/main" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: dev + - package-ecosystem: "pip" + directory: "/docker/tensorrt" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: dev + - package-ecosystem: "npm" + directory: "/web" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: dev + - package-ecosystem: "npm" + directory: "/docs" + schedule: + interval: daily + allow: + - dependency-name: "@docusaurus/*" + open-pull-requests-limit: 10 + target-branch: dev diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..482f993 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,80 @@ +_Please read the [contributing guidelines](https://github.com/blakeblackshear/frigate/blob/dev/CONTRIBUTING.md) before submitting a PR._ + +## Proposed change + + + +## Type of change + +- [ ] Dependency upgrade +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New feature +- [ ] Breaking change (fix/feature causing existing functionality to break) +- [ ] Code quality improvements to existing code +- [ ] Documentation Update + +## Additional information + +- This PR fixes or closes issue: fixes # +- This PR is related to issue: +- Link to discussion with maintainers (**required** for any large or "planned" features): + +## For new features + + + +- [ ] There is an existing feature request or discussion with community interest for this change. + - Link: + +## AI disclosure + + + +- [ ] No AI tools were used in this PR. +- [ ] AI tools were used in this PR. Details below: + +**AI tool(s) used** (e.g., Claude, Copilot, ChatGPT, Cursor): + +**How AI was used** (e.g., code generation, code review, debugging, documentation): + +**Extent of AI involvement** (e.g., generated entire implementation, assisted with specific functions, suggested fixes): + +**Human oversight**: Describe what manual review, testing, and validation you performed on the AI-generated portions. + +## Checklist + + + +- [ ] The code change is tested and works locally. +- [ ] Local tests pass. **Your PR cannot be merged unless tests pass** +- [ ] There is no commented out code in this PR. +- [ ] I can explain every line of code in this PR if asked. +- [ ] UI changes including text have used i18n keys and have been added to the `en` locale. +- [ ] The code has been formatted using Ruff (`ruff format frigate`) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..41080be --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,226 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: + - dev + - master + paths-ignore: + - "docs/**" + +# only run the latest commit to avoid cache overwrites +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: 3.11 + +jobs: + amd64_build: + runs-on: ubuntu-22.04 + name: AMD64 Build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push amd64 standard build + uses: docker/build-push-action@v7 + with: + context: . + file: docker/main/Dockerfile + push: true + platforms: linux/amd64 + target: frigate + tags: ${{ steps.setup.outputs.image-name }}-amd64 + cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64 + cache-to: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64,mode=max + arm64_build: + runs-on: ubuntu-22.04-arm + name: ARM Build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push arm64 standard build + uses: docker/build-push-action@v7 + with: + context: . + file: docker/main/Dockerfile + push: true + platforms: linux/arm64 + target: frigate + tags: | + ${{ steps.setup.outputs.image-name }}-standard-arm64 + cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64 + - name: Build and push RPi build + uses: docker/bake-action@v7 + with: + source: . + push: true + targets: rpi + files: docker/rpi/rpi.hcl + set: | + rpi.tags=${{ steps.setup.outputs.image-name }}-rpi + *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64 + *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max + jetson_jp6_build: + runs-on: ubuntu-22.04-arm + name: Jetson Jetpack 6 + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push TensorRT (Jetson, Jetpack 6) + env: + ARCH: arm64 + BASE_IMAGE: 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 + uses: docker/bake-action@v7 + with: + source: . + push: true + targets: tensorrt + files: docker/tensorrt/trt.hcl + set: | + tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt-jp6 + *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp6 + *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp6,mode=max + amd64_extra_builds: + runs-on: ubuntu-22.04 + name: AMD64 Extra Build + needs: + - amd64_build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push TensorRT (x86 GPU) + env: + COMPUTE_LEVEL: "50 60 70 80 90" + uses: docker/bake-action@v7 + with: + source: . + push: true + targets: tensorrt + files: docker/tensorrt/trt.hcl + set: | + tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt + *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt + *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt,mode=max + - name: AMD/ROCm general build + env: + HSA_OVERRIDE: 0 + uses: docker/bake-action@v7 + with: + source: . + push: true + targets: rocm + files: docker/rocm/rocm.hcl + set: | + rocm.tags=${{ steps.setup.outputs.image-name }}-rocm + *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-rocm,mode=max + *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-rocm + arm64_extra_builds: + runs-on: ubuntu-22.04-arm + name: ARM Extra Build + needs: + - arm64_build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push Rockchip build + uses: docker/bake-action@v7 + with: + source: . + push: true + targets: rk + files: docker/rockchip/rk.hcl + set: | + rk.tags=${{ steps.setup.outputs.image-name }}-rk + *.cache-from=type=gha + synaptics_build: + runs-on: ubuntu-22.04-arm + name: Synaptics Build + needs: + - arm64_build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push Synaptics build + uses: docker/bake-action@v7 + with: + source: . + push: true + targets: synaptics + files: docker/synaptics/synaptics.hcl + set: | + synaptics.tags=${{ steps.setup.outputs.image-name }}-synaptics + *.cache-from=type=gha + # The majority of users running arm64 are rpi users, so the rpi + # build should be the primary arm64 image + assemble_default_build: + runs-on: ubuntu-22.04 + name: Assemble and push default build + needs: + - amd64_build + - arm64_build + steps: + - id: lowercaseRepo + uses: ASzc/change-string-case-action@v6 + with: + string: ${{ github.repository }} + - name: Log in to the Container registry + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create short sha + run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV + - uses: int128/docker-manifest-create-action@v2 + with: + tags: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }} + sources: | + ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-amd64 + ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-rpi diff --git a/.github/workflows/pr_template_check.yml b/.github/workflows/pr_template_check.yml new file mode 100644 index 0000000..c82b202 --- /dev/null +++ b/.github/workflows/pr_template_check.yml @@ -0,0 +1,120 @@ +name: PR template check + +on: + pull_request_target: + types: [opened, edited] + +permissions: + pull-requests: write + +jobs: + check_template: + name: Validate PR description + runs-on: ubuntu-latest + steps: + - name: Check PR description against template + uses: actions/github-script@v9 + with: + script: | + const maintainers = ['blakeblackshear', 'NickM-27', 'hawkeye217', 'dependabot[bot]', 'weblate']; + const author = context.payload.pull_request.user.login; + + if (maintainers.includes(author)) { + console.log(`Skipping template check for maintainer: ${author}`); + return; + } + + const body = context.payload.pull_request.body || ''; + const errors = []; + + // Check that key template sections exist + const requiredSections = [ + '## Proposed change', + '## Type of change', + '## AI disclosure', + '## Checklist', + ]; + + for (const section of requiredSections) { + if (!body.includes(section)) { + errors.push(`Missing section: **${section}**`); + } + } + + // Check that "Proposed change" has content beyond the default HTML comment + const proposedChangeMatch = body.match( + /## Proposed change\s*(?:\s*)?([\s\S]*?)(?=\n## )/ + ); + const proposedContent = proposedChangeMatch + ? proposedChangeMatch[1].trim() + : ''; + if (!proposedContent) { + errors.push( + 'The **Proposed change** section is empty. Please describe what this PR does.' + ); + } + + // Check that at least one "Type of change" checkbox is checked + const typeSection = body.match( + /## Type of change\s*([\s\S]*?)(?=\n## )/ + ); + if (typeSection && !/- \[x\]/i.test(typeSection[1])) { + errors.push( + 'No **Type of change** selected. Please check at least one option.' + ); + } + + // Check that at least one AI disclosure checkbox is checked + const aiSection = body.match( + /## AI disclosure\s*([\s\S]*?)(?=\n## )/ + ); + if (aiSection && !/- \[x\]/i.test(aiSection[1])) { + errors.push( + 'No **AI disclosure** option selected. Please indicate whether AI tools were used.' + ); + } + + // Check that at least one checklist item is checked + const checklistSection = body.match( + /## Checklist\s*([\s\S]*?)$/ + ); + if (checklistSection && !/- \[x\]/i.test(checklistSection[1])) { + errors.push( + 'No **Checklist** items checked. Please review and check the items that apply.' + ); + } + + if (errors.length === 0) { + console.log('PR description passes template validation.'); + return; + } + + const prNumber = context.payload.pull_request.number; + const message = [ + '## PR template validation failed', + '', + 'This PR was automatically closed because the description does not follow the [pull request template](https://github.com/blakeblackshear/frigate/blob/dev/.github/pull_request_template.md).', + '', + '**Issues found:**', + ...errors.map((e) => `- ${e}`), + '', + 'Please update your PR description to include all required sections from the template, then reopen this PR.', + '', + '> If you used an AI tool to generate this PR, please see our [contributing guidelines](https://github.com/blakeblackshear/frigate/blob/dev/CONTRIBUTING.md) for details.', + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: message, + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + state: 'closed', + }); + + core.setFailed('PR description does not follow the template.'); diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..d2e2799 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,131 @@ +name: On pull request + +on: + pull_request: + paths-ignore: + - "docs/**" + - ".github/*.yml" + - ".github/DISCUSSION_TEMPLATE/**" + - ".github/ISSUE_TEMPLATE/**" + +env: + DEFAULT_PYTHON: 3.11 + +jobs: + web_lint: + name: Web - Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - run: npm install + working-directory: ./web + - name: Lint + run: npm run lint + working-directory: ./web + - name: Check i18n keys + run: npm run i18n:extract:ci + working-directory: ./web + + web_test: + name: Web - Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - run: npm install + working-directory: ./web + - name: Build web + run: npm run build + working-directory: ./web + # - name: Test + # run: npm run test + # working-directory: ./web + + web_e2e: + name: Web - E2E Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - run: npm install + working-directory: ./web + - name: Install Playwright Chromium + run: npx playwright install chromium --with-deps + working-directory: ./web + - name: Build web for E2E + run: npm run e2e:build + working-directory: ./web + - name: Run E2E tests + run: npm run e2e + working-directory: ./web + - name: Upload test artifacts + uses: actions/upload-artifact@v7 + if: failure() + with: + name: playwright-report + path: | + web/test-results/ + web/playwright-report/ + retention-days: 7 + + python_checks: + runs-on: ubuntu-latest + name: Python Checks + steps: + - name: Check out the repository + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v5.4.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Install requirements + run: | + python3 -m pip install -U pip + python3 -m pip install -r docker/main/requirements-dev.txt + - name: Check formatting + run: | + ruff format --check --diff frigate migrations docker *.py + - name: Check lint + run: | + ruff check frigate migrations docker *.py + + python_tests: + runs-on: ubuntu-latest + name: Python Tests + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - name: Install devcontainer cli + run: npm install --global @devcontainers/cli + - name: Build devcontainer + env: + DOCKER_BUILDKIT: "1" + run: devcontainer build --workspace-folder . + - name: Start devcontainer + run: devcontainer up --workspace-folder . + - name: Run mypy in devcontainer + run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m mypy --config-file frigate/mypy.ini frigate" + - name: Check API spec is up to date + run: devcontainer exec --workspace-folder . bash -lc "python3 generate_api_auth_spec.py --check" + - name: Run unit tests in devcontainer + run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m unittest" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1fbf58f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: On release + +on: + workflow_dispatch: + release: + types: [published] + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - id: lowercaseRepo + uses: ASzc/change-string-case-action@v6 + with: + string: ${{ github.repository }} + - name: Log in to the Container registry + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create tag variables + env: + TAG: ${{ github.ref_name }} + LOWERCASE_REPO: ${{ steps.lowercaseRepo.outputs.lowercase }} + run: | + BUILD_TYPE=$([[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "stable" || echo "beta") + echo "BUILD_TYPE=${BUILD_TYPE}" >> $GITHUB_ENV + echo "BASE=ghcr.io/${LOWERCASE_REPO}" >> $GITHUB_ENV + echo "BUILD_TAG=${GITHUB_SHA::7}" >> $GITHUB_ENV + echo "CLEAN_VERSION=$(echo ${GITHUB_REF##*/} | tr '[:upper:]' '[:lower:]' | sed 's/^[v]//')" >> $GITHUB_ENV + - name: Tag and push the main image + run: | + VERSION_TAG=${BASE}:${CLEAN_VERSION} + STABLE_TAG=${BASE}:stable + PULL_TAG=${BASE}:${BUILD_TAG} + docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${VERSION_TAG} + for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm synaptics; do + docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${VERSION_TAG}-${variant} + done + + # stable tag + if [[ "${BUILD_TYPE}" == "stable" ]]; then + docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${STABLE_TAG} + for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm synaptics; do + docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${STABLE_TAG}-${variant} + done + fi diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..39512f2 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,42 @@ +# Close Stale Issues +# Warns and then closes issues and PRs that have had no activity for a specified amount of time. +# https://github.com/actions/stale + +name: "Stalebot" +on: + schedule: + - cron: "0 0 * * *" # run stalebot once a day + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@main + id: stale + with: + stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions." + close-issue-message: "" + days-before-stale: 30 + days-before-close: 3 + exempt-draft-pr: false + exempt-issue-labels: "planned,security" + exempt-pr-labels: "planned,security,dependencies" + operations-per-run: 120 + - name: Print outputs + env: + STALE_OUTPUT: ${{ join(steps.stale.outputs.*, ',') }} + run: echo "$STALE_OUTPUT" + + # clean_ghcr: + # name: Delete outdated dev container images + # runs-on: ubuntu-latest + # steps: + # - name: Delete old images + # uses: snok/container-retention-policy@v2 + # with: + # image-names: dev-* + # cut-off: 60 days ago UTC + # keep-at-least: 5 + # account-type: personal + # token: ${{ secrets.GITHUB_TOKEN }} + # token-type: github-token diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c97a23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +.DS_Store +__pycache__ +.mypy_cache +*.swp +debug +.claude/* +.mcp.json +.vscode/* +!.vscode/launch.json +config/* +!config/*.example +models +*.mp4 +*.db +*.csv +frigate/version.py +web/build +web/node_modules +web/coverage +web/.env +core +!/web/**/*.ts +.idea/* +.ipynb_checkpoints + +# Auto-generated Docker Compose Generator config files +docs/src/components/DockerComposeGenerator/config/devices.ts +docs/src/components/DockerComposeGenerator/config/hardware.ts +docs/src/components/DockerComposeGenerator/config/ports.ts diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..bf205da --- /dev/null +++ b/.pylintrc @@ -0,0 +1,588 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=fstr + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2d7b6c8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Launch Frigate", + "type": "debugpy", + "request": "launch", + "module": "frigate" + }, + { + "type": "editor-browser", + "request": "launch", + "name": "Vite: Launch in integrated browser", + "url": "http://localhost:5173" + }, + { + "type": "editor-browser", + "request": "launch", + "name": "Nginx: Launch in integrated browser", + "url": "http://localhost:5000" + }, + { + "type": "editor-browser", + "request": "attach", + "name": "Attach to integrated browser" + } + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..41d4b64 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,450 @@ +# Agent Instructions for Frigate NVR + +This document provides coding guidelines and best practices for contributing to Frigate NVR, a complete and local NVR designed for Home Assistant with AI object detection. + +## Project Overview + +Frigate NVR is a realtime object detection system for IP cameras that uses: + +- **Backend**: Python 3.13+ with FastAPI, OpenCV, TensorFlow/ONNX +- **Frontend**: React with TypeScript, Vite, TailwindCSS +- **Architecture**: Multiprocessing design with ZMQ and MQTT communication +- **Focus**: Minimal resource usage with maximum performance + +## Code Review Guidelines + +When reviewing code, do NOT comment on: + +- Missing imports - Static analysis tooling catches these +- Code formatting - Ruff (Python) and Prettier (TypeScript/React) handle formatting +- Minor style inconsistencies already enforced by linters + +## Python Backend Standards + +### Python Requirements + +- **Compatibility**: Python 3.13+ +- **Language Features**: Use modern Python features: + - Pattern matching + - Type hints (comprehensive typing preferred) + - f-strings (preferred over `%` or `.format()`) + - Dataclasses + - Async/await patterns + +### Code Quality Standards + +- **Formatting**: Ruff (configured in `pyproject.toml`) +- **Linting**: Ruff with rules defined in project config +- **Type Checking**: Use type hints consistently +- **Testing**: unittest framework - use `python3 -u -m unittest` to run tests +- **Language**: American English for all code, comments, and documentation +- **Punctuation**: Do not use em dashes in documentation, comments, or strings; reword with standard punctuation (commas, colons, parentheses, or separate sentences) + +### Logging Standards + +- **Logger Pattern**: Use module-level logger + + ```python + import logging + + logger = logging.getLogger(__name__) + ``` + +- **Format Guidelines**: + - No periods at end of log messages + - No sensitive data (keys, tokens, passwords) + - Use lazy logging: `logger.debug("Message with %s", variable)` +- **Log Levels**: + - `debug`: Development and troubleshooting information + - `info`: Important runtime events (startup, shutdown, state changes) + - `warning`: Recoverable issues that should be addressed + - `error`: Errors that affect functionality but don't crash the app + - `exception`: Use in except blocks to include traceback + +### Error Handling + +- **Exception Types**: Choose most specific exception available +- **Try/Catch Best Practices**: + - Only wrap code that can throw exceptions + - Keep try blocks minimal - process data after the try/except + - Avoid bare exceptions except in background tasks + + Bad pattern: + + ```python + try: + data = await device.get_data() # Can throw + # ❌ Don't process data inside try block + processed = data.get("value", 0) * 100 + result = processed + except DeviceError: + logger.error("Failed to get data") + ``` + + Good pattern: + + ```python + try: + data = await device.get_data() # Can throw + except DeviceError: + logger.error("Failed to get data") + return + + # ✅ Process data outside try block + processed = data.get("value", 0) * 100 + result = processed + ``` + +### Async Programming + +- **External I/O**: All external I/O operations must be async +- **Best Practices**: + - Avoid sleeping in loops - use `asyncio.sleep()` not `time.sleep()` + - Avoid awaiting in loops - use `asyncio.gather()` instead + - No blocking calls in async functions + - Use `asyncio.create_task()` for background operations +- **Thread Safety**: Use proper synchronization for shared state + +### Documentation Standards + +- **Module Docstrings**: Concise descriptions at top of files + ```python + """Utilities for motion detection and analysis.""" + ``` +- **Function Docstrings**: Required for public functions and methods + + ```python + async def process_frame(frame: ndarray, config: Config) -> Detection: + """Process a video frame for object detection. + + Args: + frame: The video frame as numpy array + config: Detection configuration + + Returns: + Detection results with bounding boxes + """ + ``` + +- **Comment Style**: + - Explain the "why" not just the "what" + - Keep lines under 88 characters when possible + - Use clear, descriptive comments + +### File Organization + +- **API Endpoints**: `frigate/api/` - FastAPI route handlers +- **Configuration**: `frigate/config/` - Configuration parsing and validation +- **Detectors**: `frigate/detectors/` - Object detection backends +- **Events**: `frigate/events/` - Event management and storage +- **Utilities**: `frigate/util/` - Shared utility functions + +## Frontend (React/TypeScript) Standards + +### Internationalization (i18n) + +- **CRITICAL**: Never write user-facing strings directly in components +- **Always use react-i18next**: Import and use the `t()` function + + ```tsx + import { useTranslation } from "react-i18next"; + + function MyComponent() { + const { t } = useTranslation(["views/live"]); + return
{t("camera_not_found")}
; + } + ``` + +- **Translation Files**: Add English strings to the appropriate json files in `web/public/locales/en` +- **Namespaces**: Organize translations by feature/view (e.g., `views/live`, `common`, `views/system`) + +### Code Quality + +- **Linting**: ESLint (see `web/.eslintrc.cjs`) +- **Formatting**: Prettier with Tailwind CSS plugin +- **Type Safety**: TypeScript strict mode enabled + +### Component Patterns + +- **UI Components**: Use Radix UI primitives (in `web/src/components/ui/`) +- **Styling**: TailwindCSS with `cn()` utility for class merging +- **State Management**: React hooks (useState, useEffect, useCallback, useMemo) +- **Data Fetching**: Custom hooks with proper loading and error states + +### ESLint Rules + +Key rules enforced: + +- `react-hooks/rules-of-hooks`: error +- `react-hooks/exhaustive-deps`: error +- `no-console`: error (use proper logging or remove) +- `@typescript-eslint/no-explicit-any`: warn (always use proper types instead of `any`) +- Unused variables must be prefixed with `_` +- Comma dangles required for multiline objects/arrays + +### File Organization + +- **Pages**: `web/src/pages/` - Route components +- **Views**: `web/src/views/` - Complex view components +- **Components**: `web/src/components/` - Reusable components +- **Hooks**: `web/src/hooks/` - Custom React hooks +- **API**: `web/src/api/` - API client functions +- **Types**: `web/src/types/` - TypeScript type definitions + +## Testing Requirements + +### Backend Testing + +- **Framework**: Python unittest +- **Run Command**: `python3 -u -m unittest` +- **Location**: `frigate/test/` +- **Coverage**: Aim for comprehensive test coverage of core functionality +- **Pattern**: Use `TestCase` classes with descriptive test method names + ```python + class TestMotionDetection(unittest.TestCase): + def test_detects_motion_above_threshold(self): + # Test implementation + ``` + +### Test Best Practices + +- Always have a way to test your work and confirm your changes +- Write tests for bug fixes to prevent regressions +- Test edge cases and error conditions +- Mock external dependencies (cameras, APIs, hardware) +- Use fixtures for test data + +## Development Commands + +### Python Backend + +```bash +# Run all tests +python3 -u -m unittest + +# Run specific test file +python3 -u -m unittest frigate.test.test_ffmpeg_presets + +# Check formatting (Ruff) +ruff format --check frigate/ + +# Apply formatting +ruff format frigate/ + +# Run linter +ruff check frigate/ + +# Type check +python3 -u -m mypy --config-file frigate/mypy.ini frigate + +# Regenerate the OpenAPI spec after adding, changing, or removing an API +# endpoint or its auth dependency — outputs docs/static/frigate-api.yaml, +# annotated with each endpoint's auth requirement (admin / any / camera / +# public). NEVER edit that file by hand. CI runs the --check variant and fails +# if it is out of date. (from repo root) +python3 generate_api_auth_spec.py +python3 generate_api_auth_spec.py --check +``` + +### Frontend (from web/ directory) + +```bash +# Start dev server (AI agents should never run this directly unless asked) +npm run dev + +# Build for production +npm run build + +# Run linter +npm run lint + +# Fix linting issues +npm run lint:fix + +# Format code +npm run prettier:write + +# E2E: first-time setup +npm install +npx playwright install chromium + +# E2E: build the app and run all tests +npm run e2e:build && npm run e2e + +# E2E: interactive UI for debugging +npm run e2e:ui + +# E2E: run a specific spec +npx playwright test --config e2e/playwright.config.ts e2e/specs/live.spec.ts + +# E2E: filter by name, or run only desktop/mobile +npx playwright test --config e2e/playwright.config.ts --grep="severity tab" +npx playwright test --config e2e/playwright.config.ts --project=desktop + +# E2E: regenerate mock data after backend model changes (from repo root) +PYTHONPATH=. python3 web/e2e/fixtures/mock-data/generate-mock-data.py + +# Regenerate config translations from Pydantic models — outputs to +# web/public/locales/en/config/{global,cameras}.json. NEVER edit those +# JSON files by hand; change the Pydantic field title/description and +# re-run this script. (from repo root) +python3 generate_config_translations.py + +# Extract i18n keys from source into the locale files after adding +# new t() calls. Use the :ci variant to verify the locale files are +# in sync with source (fails if extraction would change anything). +npm run i18n:extract +npm run i18n:extract:ci +``` + +### Docker Development + +AI agents should never run these commands directly unless instructed. + +```bash +# Build local image +make local + +# Build debug image +make debug +``` + +## Common Patterns + +### API Endpoint Pattern + +```python +from fastapi import APIRouter, Request +from frigate.api.defs.tags import Tags + +router = APIRouter(tags=[Tags.Events]) + +@router.get("/events") +async def get_events(request: Request, limit: int = 100): + """Retrieve events from the database.""" + # Implementation +``` + +After adding, changing, or removing an endpoint (or its auth dependency), regenerate the OpenAPI spec with `python3 generate_api_auth_spec.py` so `docs/static/frigate-api.yaml` stays in sync and the endpoint's auth requirement is documented. CI enforces this via the `--check` variant; never edit that file by hand. + +### Configuration Access + +```python +# Access Frigate configuration +config: FrigateConfig = request.app.frigate_config +camera_config = config.cameras["front_door"] +``` + +### Database Queries + +```python +from frigate.models import Event + +# Use Peewee ORM for database access +events = ( + Event.select() + .where(Event.camera == camera_name) + .order_by(Event.start_time.desc()) + .limit(limit) +) +``` + +## Common Anti-Patterns to Avoid + +### ❌ Avoid These + +```python +# Blocking operations in async functions +data = requests.get(url) # ❌ Use async HTTP client +time.sleep(5) # ❌ Use asyncio.sleep() + +# Hardcoded strings in React components +
Camera not found
# ❌ Use t("camera_not_found") + +# Missing error handling +data = await api.get_data() # ❌ No exception handling + +# Bare exceptions in regular code +try: + value = await sensor.read() +except Exception: # ❌ Too broad + logger.error("Failed") + +# Returning exceptions in JSON responses +except ValueError as e: + return JSONResponse( + content={"success": False, "message": str(e)}, + ) +``` + +### ✅ Use These Instead + +```python +# Async operations +import aiohttp +async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + data = await response.json() + +await asyncio.sleep(5) # ✅ Non-blocking + +# Translatable strings in React +const { t } = useTranslation(); +
{t("camera_not_found")}
# ✅ Translatable + +# Proper error handling +try: + data = await api.get_data() +except ApiException as err: + logger.error("API error: %s", err) + raise + +# Specific exceptions +try: + value = await sensor.read() +except SensorException as err: # ✅ Specific + logger.exception("Failed to read sensor") + +# Safe error responses +except ValueError: + logger.exception("Invalid parameters for API request") + return JSONResponse( + content={ + "success": False, + "message": "Invalid request parameters", + }, + ) +``` + +## WebSocket Broadcasts + +Outbound WebSocket broadcasts go through a per-recipient classifier in `frigate/comms/ws.py` that enforces camera-level access. **The classifier is fail-closed: any topic it doesn't recognize is dropped for every client.** New outbound topics must be classified there or they'll silently disappear. + +## Project-Specific Conventions + +### Configuration Files + +- Main config: `config/config.yml` + +### Directory Structure + +- Backend code: `frigate/` +- Frontend code: `web/` +- Docker files: `docker/` +- Documentation: `docs/` +- Database migrations: `migrations/` + +### Code Style Conformance + +Always conform new and refactored code to the existing coding style in the project: + +- Follow established patterns in similar files +- Match indentation and formatting of surrounding code +- Use consistent naming conventions (snake_case for Python, camelCase for TypeScript) +- Maintain the same level of verbosity in comments and docstrings + +## Additional Resources + +- Documentation: https://docs.frigate.video +- Main Repository: https://github.com/blakeblackshear/frigate +- Home Assistant Integration: https://github.com/blakeblackshear/frigate-hass-integration diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..c37041c --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,7 @@ +# Community-supported boards +/docker/tensorrt/ @madsciencetist @NateMeyer +/docker/tensorrt/*arm64* @madsciencetist +/docker/tensorrt/*jetson* @madsciencetist +/docker/rockchip/ @MarcA711 +/docker/rocm/ @harakas +/docker/hailo8l/ @spanner3003 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e25c208 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,145 @@ +# Contributing to Frigate + +Thank you for your interest in contributing to Frigate. This document covers the expectations and guidelines for contributions. Please read it before submitting a pull request. + +## Before you start + +### Bugfixes + +If you've found a bug and want to fix it, go for it. Link to the relevant issue in your PR if one exists, or describe the bug in the PR description. + +### New features + +A pull request is more than just code — it's a request for the maintainers to review, integrate, and support the change long-term. We're selective about what we take on, and prioritize changes that align with the project's direction and can be responsibly maintained in the long term. + +**Large or highly-requested features** raise the bar even higher. Popularity signals demand, but it doesn't pre-approve any particular implementation. The bigger the change, the higher the long-term cost, and the more important it is that we're aligned on scope and approach before any code is written. A large PR that lands without prior discussion is unlikely to be merged as-is, no matter how well it's implemented. + +Before writing code for a new feature: + +1. **Check for existing discussion.** Search [feature requests](https://github.com/blakeblackshear/frigate/issues) and [discussions](https://github.com/blakeblackshear/frigate/discussions) to see if it's been proposed or discussed. Feature requests tagged with "planned" are on our radar — we plan to get to them, but we don't maintain a public roadmap or timeline. Check in with us first if you have interest in contributing to one. +2. **Start a discussion or feature request first.** This helps ensure your idea aligns with Frigate's direction before you invest time building it. Community interest in a feature request helps us gauge demand, though a great idea is a great idea even without a crowd behind it. + +## AI usage policy + +AI tools are a reality of modern development and we're not opposed to their use. But we need to understand your relationship with the code you're submitting. The more AI was involved, the more important it is that you've genuinely reviewed, tested, and understood what it produced. + +### Requirements when AI is used + +If AI is used to generate any portion of the code, contributors must adhere to the following requirements: + +1. **Explicitly disclose the manner in which AI was employed.** The PR template asks for this. Be honest — this won't automatically disqualify your PR. We'd rather have an honest disclosure than find out later. Trust matters more than method. +2. **Perform a comprehensive manual review prior to submitting the pull request.** Don't submit code you haven't read carefully and tested locally. +3. **Be prepared to explain every line of code they submitted when asked about it by a maintainer.** If you can't explain why something works the way it does, you're not ready to submit it. +4. **It is strictly prohibited to use AI to write your posts for you** (bug reports, feature requests, pull request descriptions, GitHub discussions, responding to humans, etc.). We need to hear from _you_, not your AI assistant. These are the spaces where we build trust and understanding with contributors, and that only works if we're talking to each other. + +### Established contributors + +Contributors with a long history of thoughtful, quality contributions to Frigate have earned trust through that track record. The level of scrutiny we apply to AI usage naturally reflects that trust. This isn't a formal exemption — it's just how trust works. If you've been around, we know how you think and how you work. If you're new, we're still getting to know you, and clear disclosure helps build that relationship. + +### What this means in practice + +We're not trying to gatekeep how you write code. Use whatever tools make you productive. But there's a difference between using AI as a tool to implement something you understand and handing a feature request to an AI and submitting whatever comes back. The former is fine. The latter creates maintenance risk for the project. + +Some honest context: when we review a PR, we're not just evaluating whether the code works today. We're evaluating whether we can maintain it, debug it, and extend it long-term — often without the original author's involvement. Code that the author doesn't deeply understand is code that nobody understands, and that's a liability. + +One more thing worth saying directly: most maintainers already have access to the same AI tools you do. A PR that's entirely AI-generated — where the author can't explain the design, debug issues independently, or engage substantively in design discussions — doesn't offer something we couldn't produce ourselves. What makes a contribution genuinely valuable is the human judgment and domain understanding behind it, as well as the engagement during review that shapes it into something we can confidently take on long-term. + +## Pull request guidelines + +### Before submitting + +- **Search for existing PRs** to avoid duplicating effort. +- **Test your changes locally.** Your PR cannot be merged unless tests pass. +- **Format your code.** Run `ruff format frigate` for Python and `npm run prettier:write` from the `web/` directory for frontend changes. +- **Run the linter.** Run `ruff check frigate` for Python and `npm run lint` from `web/` for frontend. +- **One concern per PR.** Don't combine unrelated changes. A bugfix and a new feature should be separate PRs. + +### What we look for in review + +- **Does it work?** Tested locally, tests pass, no regressions. +- **Is it maintainable?** Clear code, appropriate complexity, good separation of concerns. +- **Does it fit?** Consistent with Frigate's architecture and design philosophy. +- **Is it scoped well?** Solves the stated problem without unnecessary additions. + +### After submitting + +- Be responsive to review feedback. We may ask for changes. +- Expect honest, direct feedback. We try to be respectful but we also try to be efficient. +- If your PR goes stale, rebase it on the latest `dev` branch. + +## Coding standards + +### Python (backend) + +- **Python** — use modern language features (type hints, pattern matching, f-strings, dataclasses) +- **Formatting**: Ruff (configured in `pyproject.toml`) +- **Linting**: Ruff +- **Testing**: `python3 -u -m unittest` +- **Logging**: Use module-level `logger = logging.getLogger(__name__)` with lazy formatting +- **Async**: All external I/O must be async. No blocking calls in async functions. +- **Error handling**: Use specific exception types. Keep try blocks minimal. +- **Language**: American English for all code, comments, and documentation + +### TypeScript/React (frontend) + +- **Linting**: ESLint (`npm run lint` from `web/`) +- **Formatting**: Prettier (`npm run prettier:write` from `web/`) +- **Type safety**: TypeScript strict mode. Avoid `any`. +- **i18n**: All user-facing strings must use `react-i18next`. Never hardcode display text in components. Add English strings to the appropriate files in `web/public/locales/en/`. +- **Components**: Use Radix UI/shadcn primitives and TailwindCSS with the `cn()` utility. + +### Development commands + +```bash +# Python +python3 -u -m unittest # Run all tests +python3 -u -m unittest frigate.test.test_ffmpeg_presets # Run specific test +ruff format frigate # Format +ruff check frigate # Lint + +# Frontend (from web/ directory) +npm run build # Build +npm run lint # Lint +npm run lint:fix # Lint + fix +npm run prettier:write # Format +``` + +## Project structure + +``` +frigate/ # Python backend + api/ # FastAPI route handlers + config/ # Configuration parsing and validation + detectors/ # Object detection backends + events/ # Event management and storage + test/ # Backend tests + util/ # Shared utilities +web/ # React/TypeScript frontend + src/ + api/ # API client functions + components/ # Reusable components + hooks/ # Custom React hooks + pages/ # Route components + types/ # TypeScript type definitions + views/ # Complex view components +docker/ # Docker build files +docs/ # Documentation site +migrations/ # Database migrations +``` + +## Translations + +Frigate uses [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) for managing language translations. If you'd like to help translate Frigate into your language: + +1. Visit the [Frigate project on Weblate](https://hosted.weblate.org/projects/frigate-nvr/). +2. Create an account or log in. +3. Browse the available languages and select the one you'd like to contribute to, or request a new language. +4. Translate strings directly in the Weblate interface — no code changes or pull requests needed. + +Translation contributions through Weblate are automatically synced to the repository. Please do not submit pull requests for translation changes — use Weblate instead so that translations are properly tracked and coordinated. + +## Resources + +- [Documentation](https://docs.frigate.video) +- [Discussions, Support, and Bug Reports](https://github.com/blakeblackshear/frigate/discussions) +- [Feature Requests](https://github.com/blakeblackshear/frigate/issues) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..924cb41 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2026 Frigate, Inc. (Frigate™) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3800399 --- /dev/null +++ b/Makefile @@ -0,0 +1,61 @@ +default_target: local + +COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1) +VERSION = 0.18.0 +IMAGE_REPO ?= ghcr.io/blakeblackshear/frigate +GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) +BOARDS= #Initialized empty + +include docker/*/*.mk + +build-boards: $(BOARDS:%=build-%) + +push-boards: $(BOARDS:%=push-%) + +version: + echo 'VERSION = "$(VERSION)-$(COMMIT_HASH)"' > frigate/version.py + echo 'VITE_GIT_COMMIT_HASH=$(COMMIT_HASH)' > web/.env + +local: version + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --tag frigate:latest \ + --load + +debug: version + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --build-arg DEBUG=true \ + --tag frigate:latest \ + --load + +amd64: + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --tag $(IMAGE_REPO):$(VERSION)-$(COMMIT_HASH) \ + --platform linux/amd64 + +arm64: + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --tag $(IMAGE_REPO):$(VERSION)-$(COMMIT_HASH) \ + --platform linux/arm64 + +build: version amd64 arm64 + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --tag $(IMAGE_REPO):$(VERSION)-$(COMMIT_HASH) \ + --platform linux/arm64/v8,linux/amd64 + +push: push-boards + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --tag $(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH) \ + --platform linux/arm64/v8,linux/amd64 \ + --push + +run: local + docker run --rm --publish=5000:5000 --publish=8971:8971 \ + --volume=${PWD}/config:/config frigate:latest + +run_tests: local + docker run --rm --workdir=/opt/frigate --entrypoint= frigate:latest \ + python3 -u -m unittest + docker run --rm --workdir=/opt/frigate --entrypoint= frigate:latest \ + python3 -u -m mypy --config-file frigate/mypy.ini frigate + +.PHONY: run_tests diff --git a/README.md b/README.md new file mode 100644 index 0000000..1fb158b --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +

+ logo +

+ +# Frigate NVR™ - Realtime Object Detection for IP Cameras + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + + +Translation status + + +\[English\] | [简体中文](https://github.com/blakeblackshear/frigate/blob/dev/README_CN.md) + +A complete and local NVR designed for [Home Assistant](https://www.home-assistant.io) with AI object detection. Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras. + +Use of a GPU or AI accelerator is highly recommended. AI accelerators will outperform even the best CPUs with very little overhead. See Frigate's supported [object detectors](https://docs.frigate.video/configuration/object_detectors/). + +- Tight integration with Home Assistant via a [custom component](https://github.com/blakeblackshear/frigate-hass-integration) +- Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary +- Leverages multiprocessing heavily with an emphasis on realtime over processing every frame +- Uses a very low overhead motion detection to determine where to run object detection +- Object detection with TensorFlow runs in separate processes for maximum FPS +- Communicates over MQTT for easy integration into other systems +- Records video with retention settings based on detected objects +- 24/7 recording +- Re-streaming via RTSP to reduce the number of connections to your camera +- WebRTC & MSE support for low-latency live view + +## Documentation + +View the documentation at https://docs.frigate.video + +## Donations + +If you would like to make a donation to support development, please use [Github Sponsors](https://github.com/sponsors/blakeblackshear). + +## License + +This project is licensed under the **MIT License**. + +- **Code:** The source code, configuration files, and documentation in this repository are available under the [MIT License](LICENSE). You are free to use, modify, and distribute the code as long as you include the original copyright notice. +- **Trademarks:** The "Frigate" name, the "Frigate NVR" brand, and the Frigate logo are **trademarks of Frigate, Inc.** and are **not** covered by the MIT License. + +Please see our [Trademark Policy](TRADEMARK.md) for details on acceptable use of our brand assets. + +## Screenshots + +### Live dashboard + +
+Live dashboard +
+ +### Streamlined review workflow + +
+Streamlined review workflow +
+ +### Multi-camera scrubbing + +
+Multi-camera scrubbing +
+ +### Built-in mask and zone editor + +
+Built-in mask and zone editor +
+ +## Translations + +We use [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) to support language translations. Contributions are always welcome. + + +Translation status + + +--- + +**Copyright © 2026 Frigate, Inc.** diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..a31eb31 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`blakeblackshear/frigate` +- 原始仓库:https://github.com/blakeblackshear/frigate +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..62df77b --- /dev/null +++ b/README_CN.md @@ -0,0 +1,90 @@ +

+ logo +

+ +# Frigate NVR™ - 一个具有实时目标检测的本地 NVR + + +翻译状态 + + +[English](https://github.com/blakeblackshear/frigate) | \[简体中文\] + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +一个完整的本地网络视频录像机(NVR),专为[Home Assistant](https://www.home-assistant.io)设计,具备 AI 目标/物体检测功能。使用 OpenCV 和 TensorFlow 在本地为 IP 摄像头执行实时物体检测。 + +强烈推荐使用 GPU 或者 AI 加速器(例如[Google Coral 加速器](https://coral.ai/products/) 或者 [Hailo](https://hailo.ai/)等)。它们的运行效率远远高于现在的顶级 CPU,并且功耗也极低。 + +- 通过[自定义组件](https://github.com/blakeblackshear/frigate-hass-integration)与 Home Assistant 紧密集成 +- 设计上通过仅在必要时和必要地点寻找目标,最大限度地减少资源使用并最大化性能 +- 大量利用多进程处理,强调实时性而非处理每一帧 +- 使用非常低开销的画面变动检测(也叫运动检测)来确定运行目标检测的位置 +- 使用 TensorFlow 进行目标检测,并运行在单独的进程中以达到最大 FPS +- 通过 MQTT 进行通信,便于集成到其他系统中 +- 根据检测到的物体设置保留时间进行视频录制 +- 24/7 全天候录制 +- 通过 RTSP 重新流传输以减少摄像头的连接数 +- 支持 WebRTC 和 MSE,实现低延迟的实时观看 + +## 社区中文翻译文档 + +你可以在这里查看文档 https://docs.frigate-cn.video + +## 赞助 + +如果您想通过捐赠支持开发,请使用 [Github Sponsors](https://github.com/sponsors/blakeblackshear)。 + +## 协议 + +本项目采用 **MIT 许可证**授权。 + +**代码部分**:本代码库中的源代码、配置文件和文档均遵循 [MIT 许可证](LICENSE)。您可以自由使用、修改和分发这些代码,但必须保留原始版权声明。 + +**商标部分**:“Frigate”名称、“Frigate NVR”品牌以及 Frigate 的 Logo 为 **Frigate, Inc. 的商标**,**不在** MIT 许可证覆盖范围内。 +有关品牌资产的规范使用详情,请参阅我们的[《商标政策》](TRADEMARK.md)。 + +## 截图 + +### 实时监控面板 + +
+实时监控面板 +
+ +### 简单的核查工作流程 + +
+简单的审查工作流程 +
+ +### 多摄像头可按时间轴查看 + +
+多摄像头可按时间轴查看 +
+ +### 内置遮罩和区域编辑器 + +
+内置遮罩和区域编辑器 +
+ +## 翻译 + +我们使用 [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) 平台提供翻译支持,欢迎参与进来一起完善。 + +## 非官方中文讨论社区 + +欢迎加入中文讨论 QQ 群:[1043861059](https://qm.qq.com/q/7vQKsTmSz) + +Bilibili:https://space.bilibili.com/3546894915602564 + +## 中文社区赞助商 + +[![EdgeOne](https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png)](https://edgeone.ai/zh?from=github) +本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助 + +--- + +**Copyright © 2026 Frigate, Inc.** diff --git a/TRADEMARK.md b/TRADEMARK.md new file mode 100644 index 0000000..fdbdc14 --- /dev/null +++ b/TRADEMARK.md @@ -0,0 +1,58 @@ +# Trademark Policy + +**Last Updated:** November 2025 + +This document outlines the policy regarding the use of the trademarks associated with the Frigate NVR project. + +## 1. Our Trademarks + +The following terms and visual assets are trademarks (the "Marks") of **Frigate, Inc.**: + +- **Frigate™** +- **Frigate NVR™** +- **Frigate+™** +- **The Frigate Logo** + +**Note on Common Law Rights:** +Frigate, Inc. asserts all common law rights in these Marks. The absence of a federal registration symbol (®) does not constitute a waiver of our intellectual property rights. + +## 2. Interaction with the MIT License + +The software in this repository is licensed under the [MIT License](LICENSE). + +**Crucial Distinction:** + +- The **Code** is free to use, modify, and distribute under the MIT terms. +- The **Brand (Trademarks)** is **NOT** licensed under MIT. + +You may not use the Marks in any way that is not explicitly permitted by this policy or by written agreement with Frigate, Inc. + +## 3. Acceptable Use + +You may use the Marks without prior written permission in the following specific contexts: + +- **Referential Use:** To truthfully refer to the software (e.g., _"I use Frigate NVR for my home security"_). +- **Compatibility:** To indicate that your product or project works with the software (e.g., _"MyPlugin for Frigate NVR"_ or _"Compatible with Frigate"_). +- **Commentary:** In news articles, blog posts, or tutorials discussing the software. + +## 4. Prohibited Use + +You may **NOT** use the Marks in the following ways: + +- **Commercial Products:** You may not use "Frigate" in the name of a commercial product, service, or app (e.g., selling an app named _"Frigate Viewer"_ is prohibited). +- **Implying Affiliation:** You may not use the Marks in a way that suggests your project is official, sponsored by, or endorsed by Frigate, Inc. +- **Confusing Forks:** If you fork this repository to create a derivative work, you **must** remove the Frigate logo and rename your project to avoid user confusion. You cannot distribute a modified version of the software under the name "Frigate". +- **Domain Names:** You may not register domain names containing "Frigate" that are likely to confuse users (e.g., `frigate-official-support.com`). + +## 5. The Logo + +The Frigate logo (the bird icon) is a visual trademark. + +- You generally **cannot** use the logo on your own website or product packaging without permission. +- If you are building a dashboard or integration that interfaces with Frigate, you may use the logo only to represent the Frigate node/service, provided it does not imply you _are_ Frigate. + +## 6. Questions & Permissions + +If you are unsure if your intended use violates this policy, or if you wish to request a specific license to use the Marks (e.g., for a partnership), please contact us at: + +**help@frigate.video** diff --git a/audio-labelmap.txt b/audio-labelmap.txt new file mode 100644 index 0000000..4a38b5f --- /dev/null +++ b/audio-labelmap.txt @@ -0,0 +1,521 @@ +speech +speech +speech +speech +babbling +speech +yell +bellow +whoop +yell +yell +yell +whispering +laughter +laughter +laughter +snicker +laughter +laughter +crying +crying +crying +yell +sigh +singing +choir +sodeling +chant +mantra +child_singing +synthetic_singing +rapping +humming +groan +grunt +whistling +breathing +wheeze +snoring +gasp +pant +snort +cough +throat_clearing +sneeze +sniff +run +shuffle +footsteps +chewing +biting +gargling +stomach_rumble +burping +hiccup +fart +hands +finger_snapping +clapping +heartbeat +heart_murmur +cheering +applause +chatter +crowd +speech +children_playing +animal +pets +dog +bark +yip +howl +bow-wow +growling +whimper_dog +cat +purr +meow +hiss +caterwaul +livestock +horse +clip-clop +neigh +cattle +moo +cowbell +pig +oink +goat +bleat +sheep +fowl +chicken +cluck +cock-a-doodle-doo +turkey +gobble +duck +quack +goose +honk +wild_animals +roaring_cats +roar +bird +chird +chirp +squawk +pigeon +coo +crow +caw +owl +hoot +flapping_wings +dogs +rats +mouse +patter +insect +cricket +mosquito +fly +buzz +buzz +frog +croak +snake +rattle +whale_vocalization +music +musical_instrument +plucked_string_instrument +guitar +electric_guitar +bass_guitar +acoustic_guitar +steel_guitar +tapping +strum +banjo +sitar +mandolin +zither +ukulele +keyboard +piano +electric_piano +organ +electronic_organ +hammond_organ +synthesizer +sampler +harpsichord +percussion +drum_kit +drum_machine +drum +snare_drum +rimshot +drum_roll +bass_drum +timpani +tabla +cymbal +hi-hat +wood_block +tambourine +rattle +maraca +gong +tubular_bells +mallet_percussion +marimba +glockenspiel +vibraphone +steelpan +orchestra +brass_instrument +french_horn +trumpet +trombone +bowed_string_instrument +string_section +violin +pizzicato +cello +double_bass +wind_instrument +flute +saxophone +clarinet +harp +bell +church_bell +jingle_bell +bicycle_bell +tuning_fork +chime +wind_chime +change_ringing +harmonica +accordion +bagpipes +didgeridoo +shofar +theremin +singing_bowl +scratching +pop_music +hip_hop_music +beatboxing +rock_music +heavy_metal +punk_rock +grunge +progressive_rock +rock_and_roll +psychedelic_rock +rhythm_and_blues +soul_music +reggae +country +swing_music +bluegrass +funk +folk_music +middle_eastern_music +jazz +disco +classical_music +opera +electronic_music +house_music +techno +dubstep +drum_and_bass +electronica +electronic_dance_music +ambient_music +trance_music +music_of_latin_america +salsa_music +flamenco +blues +music_for_children +new-age_music +vocal_music +a_capella +music_of_africa +afrobeat +christian_music +gospel_music +music_of_asia +carnatic_music +music_of_bollywood +ska +traditional_music +independent_music +song +background_music +theme_music +jingle +soundtrack_music +lullaby +video_game_music +christmas_music +dance_music +wedding_music +happy_music +sad_music +tender_music +exciting_music +angry_music +scary_music +wind +rustling_leaves +wind_noise +thunderstorm +thunder +water +rain +raindrop +rain_on_surface +stream +waterfall +ocean +waves +steam +gurgling +fire +crackle +vehicle +boat +sailboat +rowboat +motorboat +ship +motor_vehicle +car +honk +toot +car_alarm +power_windows +skidding +tire_squeal +car_passing_by +race_car +truck +air_brake +air_horn +reversing_beeps +ice_cream_truck +bus +emergency_vehicle +police_car +ambulance +fire_engine +motorcycle +traffic_noise +rail_transport +train +train_whistle +train_horn +railroad_car +train_wheels_squealing +subway +aircraft +aircraft_engine +jet_engine +propeller +helicopter +fixed-wing_aircraft +bicycle +skateboard +engine +light_engine +dental_drill's_drill +lawn_mower +chainsaw +medium_engine +heavy_engine +engine_knocking +engine_starting +idling +accelerating +door +doorbell +ding-dong +sliding_door +slam +knock +tap +squeak +cupboard_open_or_close +drawer_open_or_close +dishes +cutlery +chopping +frying +microwave_oven +blender +water_tap +sink +bathtub +hair_dryer +toilet_flush +toothbrush +electric_toothbrush +vacuum_cleaner +zipper +keys_jangling +coin +scissors +electric_shaver +shuffling_cards +typing +typewriter +computer_keyboard +writing +alarm +telephone +telephone_bell_ringing +ringtone +telephone_dialing +dial_tone +busy_signal +alarm_clock +siren +civil_defense_siren +buzzer +smoke_detector +fire_alarm +foghorn +whistle +steam_whistle +mechanisms +ratchet +clock +tick +tick-tock +gears +pulleys +sewing_machine +mechanical_fan +air_conditioning +cash_register +printer +camera +single-lens_reflex_camera +tools +hammer +jackhammer +sawing +filing +sanding +power_tool +drill +explosion +gunshot +machine_gun +fusillade +artillery_fire +cap_gun +fireworks +firecracker +burst +eruption +boom +wood +chop +splinter +crack +glass +chink +shatter +liquid +splash +slosh +squish +drip +pour +trickle +gush +fill +spray +pump +stir +boiling +sonar +arrow +whoosh +thump +thunk +electronic_tuner +effects_unit +chorus_effect +basketball_bounce +bang +slap +whack +smash +breaking +bouncing +whip +flap +scratch +scrape +rub +roll +crushing +crumpling +tearing +beep +ping +ding +clang +squeal +creak +rustle +whir +clatter +sizzle +clicking +clickety-clack +rumble +plop +jingle +hum +zing +boing +crunch +silence +sine_wave +harmonic +chirp_tone +sound_effect +pulse +inside +inside +inside +outside +outside +reverberation +echo +noise +environmental_noise +static +mains_hum +distortion +sidetone +cacophony +white_noise +pink_noise +throbbing +vibration +television +radio +field_recording diff --git a/config/config.yml.example b/config/config.yml.example new file mode 100644 index 0000000..87deab1 --- /dev/null +++ b/config/config.yml.example @@ -0,0 +1,16 @@ +mqtt: + host: mqtt + +cameras: + test: + ffmpeg: + inputs: + - path: /media/frigate/car-stopping.mp4 + input_args: -re -stream_loop -1 -fflags +genpts + roles: + - detect + - rtmp + detect: + height: 1080 + width: 1920 + fps: 5 diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..132e515 --- /dev/null +++ b/cspell.json @@ -0,0 +1,22 @@ +{ + "version": "0.2", + "ignorePaths": [ + "Dockerfile", + "Dockerfile.*", + "CMakeLists.txt", + "*.db", + "node_modules", + "__pycache__", + "dist", + "/audio-labelmap.txt" + ], + "language": "en", + "dictionaryDefinitions": [ + { + "name": "frigate-dictionary", + "path": "./.cspell/frigate-dictionary.txt", + "addWords": true + } + ], + "dictionaries": ["frigate-dictionary"] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1563057 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +services: + devcontainer: + container_name: frigate-devcontainer + # Check host system's actual render/video/plugdev group IDs with 'getent group render', 'getent group video', and 'getent group plugdev' + # Must add these exact IDs in container's group_add section or OpenVINO GPU acceleration will fail + group_add: + - "109" # render + - "110" # render + - "44" # video + - "46" # plugdev + shm_size: "256mb" + build: + context: . + dockerfile: docker/main/Dockerfile + # Use target devcontainer-trt for TensorRT dev + target: devcontainer + cache_from: + - ghcr.io/blakeblackshear/frigate:cache-amd64 + ## Uncomment this block for nvidia gpu support + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] + environment: + YOLO_MODELS: "" + # devices: + # - /dev/bus/usb:/dev/bus/usb # Uncomment for Google Coral USB + # - /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware + volumes: + - .:/workspace/frigate:cached + - ./web/dist:/opt/frigate/web:cached + - /etc/localtime:/etc/localtime:ro + - ./config:/config + - ./debug:/media/frigate + # - /dev/bus/usb:/dev/bus/usb # Uncomment for Google Coral USB + mqtt: + container_name: mqtt + image: eclipse-mosquitto:2.0 + command: mosquitto -c /mosquitto-no-auth.conf # enable no-auth mode + ports: + - "1883:1883" diff --git a/docker/hailo8l/user_installation.sh b/docker/hailo8l/user_installation.sh new file mode 100644 index 0000000..cfe2782 --- /dev/null +++ b/docker/hailo8l/user_installation.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# Update package list and install dependencies +sudo apt-get update +sudo apt-get install -y build-essential cmake git wget linux-headers-$(uname -r) + +hailo_version="4.21.0" +arch=$(uname -m) + +if [[ $arch == "aarch64" ]]; then + source /etc/os-release + os_codename=$VERSION_CODENAME + echo "Detected OS codename: $os_codename" +fi + +if [ "$os_codename" = "trixie" ]; then + sudo apt install -y dkms +fi + +# Clone the HailoRT driver repository +git clone --depth 1 --branch v${hailo_version} https://github.com/hailo-ai/hailort-drivers.git + +# Build and install the HailoRT driver +cd hailort-drivers/linux/pcie +sudo make all +sudo make install + +# Load the Hailo PCI driver +sudo modprobe hailo_pci + +if [ $? -ne 0 ]; then + echo "Unable to load hailo_pci module, common reasons for this are:" + echo "- Key was rejected by service: Secure Boot is enabling disallowing install." + echo "- Permissions are not setup correctly." + exit 1 +fi + +# Download and install the firmware +cd ../../ +./download_firmware.sh + +# verify the firmware folder is present +if [ ! -d /lib/firmware/hailo ]; then + sudo mkdir /lib/firmware/hailo +fi +sudo mv hailo8_fw.*.bin /lib/firmware/hailo/hailo8_fw.bin + +# Install udev rules +sudo cp ./linux/pcie/51-hailo-udev.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules && sudo udevadm trigger + +echo "HailoRT driver installation complete." +echo "reboot your system to load the firmware!" +echo "Driver version: $(modinfo -F version hailo_pci)" diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile new file mode 100644 index 0000000..3ba1772 --- /dev/null +++ b/docker/main/Dockerfile @@ -0,0 +1,365 @@ +# syntax=docker/dockerfile:1.6 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +# Globally set pip break-system-packages option to avoid having to specify it every time +ARG PIP_BREAK_SYSTEM_PACKAGES=1 + +ARG BASE_IMAGE=debian:12 +ARG SLIM_BASE=debian:12-slim + +# A hook that allows us to inject commands right after the base images +ARG BASE_HOOK= + +FROM ${BASE_IMAGE} AS base +ARG PIP_BREAK_SYSTEM_PACKAGES +ARG BASE_HOOK + +RUN sh -c "$BASE_HOOK" + +FROM --platform=${BUILDPLATFORM} debian:12 AS base_host +ARG PIP_BREAK_SYSTEM_PACKAGES + +FROM ${SLIM_BASE} AS slim-base +ARG PIP_BREAK_SYSTEM_PACKAGES +ARG BASE_HOOK + +RUN sh -c "$BASE_HOOK" + +FROM slim-base AS wget +ARG DEBIAN_FRONTEND +RUN apt-get update \ + && apt-get install -y wget xz-utils \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /rootfs + +FROM base AS nginx +ARG DEBIAN_FRONTEND +ENV CCACHE_DIR /root/.ccache +ENV CCACHE_MAXSIZE 2G + +RUN --mount=type=bind,source=docker/main/build_nginx.sh,target=/deps/build_nginx.sh \ + /deps/build_nginx.sh + +FROM wget AS sqlite-vec +ARG DEBIAN_FRONTEND + +# Build sqlite_vec from source +COPY docker/main/build_sqlite_vec.sh /deps/build_sqlite_vec.sh +RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \ + --mount=type=bind,source=docker/main/build_sqlite_vec.sh,target=/deps/build_sqlite_vec.sh \ + --mount=type=cache,target=/root/.ccache \ + /deps/build_sqlite_vec.sh + +# Build intel-media-driver from source against bookworm's system libva so it +# works with Debian 12's glibc/libstdc++ (pre-built noble/trixie packages +# require glibc 2.38 which is not available on bookworm). +FROM base AS intel-media-driver +ARG DEBIAN_FRONTEND +RUN --mount=type=bind,source=docker/main/build_intel_media_driver.sh,target=/deps/build_intel_media_driver.sh \ + /deps/build_intel_media_driver.sh + +FROM scratch AS go2rtc +ARG TARGETARCH +WORKDIR /rootfs/usr/local/go2rtc/bin +ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.14/go2rtc_linux_${TARGETARCH}" go2rtc + +FROM wget AS tempio +ARG TARGETARCH +RUN --mount=type=bind,source=docker/main/install_tempio.sh,target=/deps/install_tempio.sh \ + /deps/install_tempio.sh + +#### +# +# OpenVino Support +# +# 1. Download and convert a model from Intel's Public Open Model Zoo +# +#### +# Download and Convert OpenVino model +FROM base_host AS ov-converter +ARG DEBIAN_FRONTEND + +# Install OpenVino Runtime and Dev library +COPY docker/main/requirements-ov.txt /requirements-ov.txt +RUN apt-get -qq update \ + && apt-get -qq install -y wget python3 python3-dev python3-distutils gcc pkg-config libhdf5-dev \ + && wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ + && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ + && python3 get-pip.py "pip" \ + && pip3 install -r /requirements-ov.txt + +# Get OpenVino Model +RUN --mount=type=bind,source=docker/main/build_ov_model.py,target=/build_ov_model.py \ + mkdir /models && cd /models \ + && wget http://download.tensorflow.org/models/object_detection/ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz \ + && tar -xvf ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz \ + && python3 /build_ov_model.py + +#### +# +# Coral Compatibility +# +# Builds libusb without udev. Needed for synology and other devices with USB coral +#### +# libUSB - No Udev +FROM wget as libusb-build +ARG TARGETARCH +ARG DEBIAN_FRONTEND +ENV CCACHE_DIR /root/.ccache +ENV CCACHE_MAXSIZE 2G + +# Build libUSB without udev. Needed for Openvino NCS2 support +WORKDIR /opt +RUN apt-get update && apt-get install -y unzip build-essential automake libtool ccache pkg-config +RUN --mount=type=cache,target=/root/.ccache wget -q https://github.com/libusb/libusb/archive/v1.0.26.zip -O v1.0.26.zip && \ + unzip v1.0.26.zip && cd libusb-1.0.26 && \ + ./bootstrap.sh && \ + ./configure CC='ccache gcc' CCX='ccache g++' --disable-udev --enable-shared && \ + make -j $(nproc --all) +RUN apt-get update && \ + apt-get install -y --no-install-recommends libusb-1.0-0-dev && \ + rm -rf /var/lib/apt/lists/* +WORKDIR /opt/libusb-1.0.26/libusb +RUN /bin/mkdir -p '/usr/local/lib' && \ + /bin/bash ../libtool --mode=install /usr/bin/install -c libusb-1.0.la '/usr/local/lib' && \ + /bin/mkdir -p '/usr/local/include/libusb-1.0' && \ + /usr/bin/install -c -m 644 libusb.h '/usr/local/include/libusb-1.0' && \ + /bin/mkdir -p '/usr/local/lib/pkgconfig' && \ + cd /opt/libusb-1.0.26/ && \ + /usr/bin/install -c -m 644 libusb-1.0.pc '/usr/local/lib/pkgconfig' && \ + ldconfig + +FROM wget AS models + +# Get model and labels +RUN wget -qO edgetpu_model.tflite https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite +RUN wget -qO cpu_model.tflite https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite +COPY labelmap.txt . +# Copy OpenVino model +COPY --from=ov-converter /models/ssdlite_mobilenet_v2.xml openvino-model/ +COPY --from=ov-converter /models/ssdlite_mobilenet_v2.bin openvino-model/ +RUN wget -q https://github.com/openvinotoolkit/open_model_zoo/raw/master/data/dataset_classes/coco_91cl_bkgr.txt -O openvino-model/coco_91cl_bkgr.txt && \ + sed -i 's/truck/car/g' openvino-model/coco_91cl_bkgr.txt +# Get Audio Model and labels +RUN wget -qO - https://www.kaggle.com/api/v1/models/google/yamnet/tfLite/classification-tflite/1/download | tar xvz && mv 1.tflite cpu_audio_model.tflite +COPY audio-labelmap.txt . + + +FROM wget AS s6-overlay +ARG TARGETARCH +RUN --mount=type=bind,source=docker/main/install_s6_overlay.sh,target=/deps/install_s6_overlay.sh \ + /deps/install_s6_overlay.sh + + +FROM base AS wheels +ARG DEBIAN_FRONTEND +ARG TARGETARCH +ARG DEBUG=false + +# Use a separate container to build wheels to prevent build dependencies in final image +RUN apt-get -qq update \ + && apt-get -qq install -y \ + apt-transport-https wget unzip \ + && apt-get -qq update \ + && apt-get -qq install -y \ + python3.11 \ + python3.11-dev \ + # opencv dependencies + build-essential cmake git pkg-config libgtk-3-dev \ + libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \ + libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \ + gfortran openexr libatlas-base-dev libssl-dev\ + libtbbmalloc2 libtbb-dev libdc1394-dev libopenexr-dev \ + libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \ + # sqlite3 dependencies + tclsh \ + # scipy dependencies + gcc gfortran libopenblas-dev liblapack-dev && \ + rm -rf /var/lib/apt/lists/* + +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 + +RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ + && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ + && python3 get-pip.py "pip" + +COPY docker/main/requirements.txt /requirements.txt +COPY docker/main/requirements-dev.txt /requirements-dev.txt + +RUN pip3 install -r /requirements.txt + +# Build pysqlite3 from source +COPY docker/main/build_pysqlite3.sh /build_pysqlite3.sh +RUN /build_pysqlite3.sh + +COPY docker/main/requirements-wheels.txt /requirements-wheels.txt +RUN pip3 wheel --wheel-dir=/wheels -r /requirements-wheels.txt && \ + if [ "$DEBUG" = "true" ]; then \ + pip3 wheel --wheel-dir=/wheels -r /requirements-dev.txt; \ + fi + +# Install HailoRT & Wheels +RUN --mount=type=bind,source=docker/main/install_hailort.sh,target=/deps/install_hailort.sh \ + /deps/install_hailort.sh + +# Collect deps in a single layer +FROM scratch AS deps-rootfs +COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/ +COPY --from=sqlite-vec /usr/local/lib/ /usr/local/lib/ +COPY --from=intel-media-driver /rootfs/ / +COPY --from=go2rtc /rootfs/ / +COPY --from=libusb-build /usr/local/lib /usr/local/lib +COPY --from=tempio /rootfs/ / +COPY --from=s6-overlay /rootfs/ / +COPY --from=models /rootfs/ / +COPY --from=wheels /rootfs/ / +COPY docker/main/rootfs/ / + + +# Frigate deps (ffmpeg, python, nginx, go2rtc, s6-overlay, etc) +FROM slim-base AS deps +ARG TARGETARCH +ARG BASE_IMAGE + +ARG DEBIAN_FRONTEND +# http://stackoverflow.com/questions/48162574/ddg#49462622 +ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn + +# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) +ENV NVIDIA_VISIBLE_DEVICES=all +ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" + +# Disable tokenizer parallelism warning +# https://stackoverflow.com/questions/62691279/how-to-disable-tokenizers-parallelism-true-false-warning/72926996#72926996 +ENV TOKENIZERS_PARALLELISM=true +# https://github.com/huggingface/transformers/issues/27214 +ENV TRANSFORMERS_NO_ADVISORY_WARNINGS=1 + +# Set OpenCV ffmpeg loglevel to fatal: https://ffmpeg.org/doxygen/trunk/log_8h.html +ENV OPENCV_FFMPEG_LOGLEVEL=8 + +# Set NumPy to ignore getlimits warning +ENV PYTHONWARNINGS="ignore:::numpy.core.getlimits" + +# Set HailoRT to disable logging +ENV HAILORT_LOGGER_PATH=NONE + +# TensorFlow C++ logging suppression (must be set before import) +# TF_CPP_MIN_LOG_LEVEL: 0=all, 1=INFO+, 2=WARNING+, 3=ERROR+ (we use 3 for errors only) +ENV TF_CPP_MIN_LOG_LEVEL=3 +# Suppress verbose logging from TensorFlow C++ code +ENV TF_CPP_MIN_VLOG_LEVEL=3 +# Disable oneDNN optimization messages ("optimized with oneDNN...") +ENV TF_ENABLE_ONEDNN_OPTS=0 +# Suppress AutoGraph verbosity during conversion +ENV AUTOGRAPH_VERBOSITY=0 +# Google Logging (GLOG) suppression for TensorFlow components +ENV GLOG_minloglevel=3 +ENV GLOG_logtostderr=0 + +ENV PATH="/usr/local/go2rtc/bin:/usr/local/tempio/bin:/usr/local/nginx/sbin:${PATH}" + +# Install dependencies +RUN --mount=type=bind,source=docker/main/install_deps.sh,target=/deps/install_deps.sh \ + /deps/install_deps.sh + +ENV DEFAULT_FFMPEG_VERSION="8.0" +ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:7.0:5.0" + +RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ + && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ + && python3 get-pip.py "pip" + +RUN --mount=type=bind,from=wheels,source=/wheels,target=/deps/wheels \ + 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) +RUN --mount=type=bind,source=docker/main/install_memryx.sh,target=/deps/install_memryx.sh \ + bash -c "bash /deps/install_memryx.sh" + +COPY --from=deps-rootfs / / + +RUN ldconfig + +EXPOSE 5000 +EXPOSE 8554 +EXPOSE 8555/tcp 8555/udp + +# Configure logging to prepend timestamps, log to stdout, keep 0 archives and rotate on 10MB +ENV S6_LOGGING_SCRIPT="T 1 n0 s10000000 T" +# Do not fail on long-running download scripts +ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 + +ENTRYPOINT ["/init"] +CMD [] + +HEALTHCHECK --start-period=300s --start-interval=5s --interval=15s --timeout=5s --retries=3 \ + CMD test -f /dev/shm/.frigate-is-stopping && exit 0; curl --fail --silent --show-error http://127.0.0.1:5000/api/version || exit 1 + +# Frigate deps with Node.js and NPM for devcontainer +FROM deps AS devcontainer + +# Do not start the actual Frigate service on devcontainer as it will be started by VS Code +# But start a fake service for simulating the logs +COPY docker/main/fake_frigate_run /etc/s6-overlay/s6-rc.d/frigate/run + +# Create symbolic link to the frigate source code, as go2rtc's create_config.sh uses it +RUN mkdir -p /opt/frigate \ + && ln -svf /workspace/frigate/frigate /opt/frigate/frigate + +# Install Node 20 +RUN curl -SLO https://deb.nodesource.com/nsolid_setup_deb.sh && \ + chmod 500 nsolid_setup_deb.sh && \ + ./nsolid_setup_deb.sh 20 && \ + apt-get install nodejs -y \ + && rm -rf /var/lib/apt/lists/* \ + && npm install -g npm@10 + +WORKDIR /workspace/frigate + +RUN apt-get update \ + && apt-get install make -y \ + && rm -rf /var/lib/apt/lists/* + +RUN --mount=type=bind,source=./docker/main/requirements-dev.txt,target=/workspace/frigate/requirements-dev.txt \ + pip3 install -r requirements-dev.txt + +HEALTHCHECK NONE + +CMD ["sleep", "infinity"] + + +# Frigate web build +# This should be architecture agnostic, so speed up the build on multiarch by not using QEMU. +FROM --platform=$BUILDPLATFORM node:20 AS web-build + +WORKDIR /work +COPY web/package.json web/package-lock.json ./ +RUN npm install + +COPY web/ ./ +RUN npm run build \ + && mv dist/BASE_PATH/monacoeditorwork/* dist/assets/ \ + && rm -rf dist/BASE_PATH + +# Collect final files in a single layer +FROM scratch AS rootfs + +WORKDIR /opt/frigate/ +COPY frigate frigate/ +COPY migrations migrations/ +COPY --from=web-build /work/dist/ web/ + +# Frigate final container +FROM deps AS frigate + +WORKDIR /opt/frigate/ +COPY --from=rootfs / / diff --git a/docker/main/build_intel_media_driver.sh b/docker/main/build_intel_media_driver.sh new file mode 100755 index 0000000..acc9caf --- /dev/null +++ b/docker/main/build_intel_media_driver.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -euxo pipefail + +# Intel media driver is x86_64-only. Create empty rootfs on other arches so +# the downstream COPY --from has a valid source. +if [ "$(uname -m)" != "x86_64" ]; then + mkdir -p /rootfs + exit 0 +fi + +MEDIA_DRIVER_VERSION="intel-media-25.2.6" +GMMLIB_VERSION="intel-gmmlib-22.7.2" + +apt-get -qq update +apt-get -qq install -y wget gnupg ca-certificates cmake g++ make pkg-config + +# Use Intel's jammy repo for newer libva-dev (2.22) which provides the +# VVC/VVC-decode headers required by media-driver 25.x +wget -qO - https://repositories.intel.com/gpu/intel-graphics.key | gpg --yes --dearmor --output /usr/share/keyrings/intel-graphics.gpg +echo "deb [arch=amd64 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/gpu/ubuntu jammy client" > /etc/apt/sources.list.d/intel-gpu-jammy.list +apt-get -qq update +apt-get -qq install -y libva-dev + +# Build gmmlib (required by media-driver) +wget -qO gmmlib.tar.gz "https://github.com/intel/gmmlib/archive/refs/tags/${GMMLIB_VERSION}.tar.gz" +mkdir /tmp/gmmlib +tar -xf gmmlib.tar.gz -C /tmp/gmmlib --strip-components 1 +cmake -S /tmp/gmmlib -B /tmp/gmmlib/build -DCMAKE_BUILD_TYPE=Release +make -C /tmp/gmmlib/build -j"$(nproc)" +make -C /tmp/gmmlib/build install + +# Build intel-media-driver +wget -qO media-driver.tar.gz "https://github.com/intel/media-driver/archive/refs/tags/${MEDIA_DRIVER_VERSION}.tar.gz" +mkdir /tmp/media-driver +tar -xf media-driver.tar.gz -C /tmp/media-driver --strip-components 1 +cmake -S /tmp/media-driver -B /tmp/media-driver/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DENABLE_KERNELS=ON \ + -DENABLE_NONFREE_KERNELS=ON \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_INSTALL_LIBDIR=/usr/lib/x86_64-linux-gnu \ + -DCMAKE_C_FLAGS="-Wno-error" \ + -DCMAKE_CXX_FLAGS="-Wno-error" +make -C /tmp/media-driver/build -j"$(nproc)" + +# Install driver to rootfs for COPY --from +make -C /tmp/media-driver/build install DESTDIR=/rootfs diff --git a/docker/main/build_nginx.sh b/docker/main/build_nginx.sh new file mode 100755 index 0000000..708a4cb --- /dev/null +++ b/docker/main/build_nginx.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +set -euxo pipefail + +NGINX_VERSION="1.27.4" +VOD_MODULE_VERSION="1.31" +SECURE_TOKEN_MODULE_VERSION="1.5" +SET_MISC_MODULE_VERSION="v0.33" +NGX_DEVEL_KIT_VERSION="v0.3.3" + +source /etc/os-release + +if [[ "$VERSION_ID" == "12" ]]; then + sed -i '/^Types:/s/deb/& deb-src/' /etc/apt/sources.list.d/debian.sources +else + cp /etc/apt/sources.list /etc/apt/sources.list.d/sources-src.list + sed -i 's|deb http|deb-src http|g' /etc/apt/sources.list.d/sources-src.list +fi + +apt-get update +apt-get -yqq build-dep nginx + +apt-get -yqq install --no-install-recommends ca-certificates wget +update-ca-certificates -f +apt install -y ccache + +export PATH="/usr/lib/ccache:$PATH" + +mkdir /tmp/nginx +wget -nv https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz +tar -zxf nginx-${NGINX_VERSION}.tar.gz -C /tmp/nginx --strip-components=1 +rm nginx-${NGINX_VERSION}.tar.gz +mkdir /tmp/nginx-vod-module +wget -nv https://github.com/kaltura/nginx-vod-module/archive/refs/tags/${VOD_MODULE_VERSION}.tar.gz +tar -zxf ${VOD_MODULE_VERSION}.tar.gz -C /tmp/nginx-vod-module --strip-components=1 +rm ${VOD_MODULE_VERSION}.tar.gz + # Patch MAX_CLIPS to allow more clips to be added than the default 128 +sed -i 's/MAX_CLIPS (128)/MAX_CLIPS (1080)/g' /tmp/nginx-vod-module/vod/media_set.h +patch -d /tmp/nginx-vod-module/ -p1 << 'EOF' +--- a/vod/avc_hevc_parser.c 2022-06-27 11:38:10.000000000 +0000 ++++ b/vod/avc_hevc_parser.c 2023-01-16 11:25:10.900521298 +0000 +@@ -3,6 +3,9 @@ + bool_t + avc_hevc_parser_rbsp_trailing_bits(bit_reader_state_t* reader) + { ++ // https://github.com/blakeblackshear/frigate/issues/4572 ++ return TRUE; ++ + uint32_t one_bit; + + if (reader->stream.eof_reached) +EOF + + +mkdir /tmp/nginx-secure-token-module +wget https://github.com/kaltura/nginx-secure-token-module/archive/refs/tags/${SECURE_TOKEN_MODULE_VERSION}.tar.gz +tar -zxf ${SECURE_TOKEN_MODULE_VERSION}.tar.gz -C /tmp/nginx-secure-token-module --strip-components=1 +rm ${SECURE_TOKEN_MODULE_VERSION}.tar.gz + +mkdir /tmp/ngx_devel_kit +wget https://github.com/vision5/ngx_devel_kit/archive/refs/tags/${NGX_DEVEL_KIT_VERSION}.tar.gz +tar -zxf ${NGX_DEVEL_KIT_VERSION}.tar.gz -C /tmp/ngx_devel_kit --strip-components=1 +rm ${NGX_DEVEL_KIT_VERSION}.tar.gz + +mkdir /tmp/nginx-set-misc-module +wget https://github.com/openresty/set-misc-nginx-module/archive/refs/tags/${SET_MISC_MODULE_VERSION}.tar.gz +tar -zxf ${SET_MISC_MODULE_VERSION}.tar.gz -C /tmp/nginx-set-misc-module --strip-components=1 +rm ${SET_MISC_MODULE_VERSION}.tar.gz + +cd /tmp/nginx + +./configure --prefix=/usr/local/nginx \ + --with-file-aio \ + --with-http_sub_module \ + --with-http_ssl_module \ + --with-http_v2_module \ + --with-http_auth_request_module \ + --with-http_realip_module \ + --with-threads \ + --add-module=../ngx_devel_kit \ + --add-module=../nginx-set-misc-module \ + --add-module=../nginx-vod-module \ + --add-module=../nginx-secure-token-module \ + --with-cc-opt="-O3 -Wno-error=implicit-fallthrough" + +make CC="ccache gcc" -j$(nproc) && make install +rm -rf /usr/local/nginx/html /usr/local/nginx/conf/*.default diff --git a/docker/main/build_ov_model.py b/docker/main/build_ov_model.py new file mode 100644 index 0000000..2888d87 --- /dev/null +++ b/docker/main/build_ov_model.py @@ -0,0 +1,11 @@ +import openvino as ov +from openvino.tools import mo + +ov_model = mo.convert_model( + "/models/ssdlite_mobilenet_v2_coco_2018_05_09/frozen_inference_graph.pb", + compress_to_fp16=True, + transformations_config="/usr/local/lib/python3.11/dist-packages/openvino/tools/mo/front/tf/ssd_v2_support.json", + tensorflow_object_detection_api_pipeline_config="/models/ssdlite_mobilenet_v2_coco_2018_05_09/pipeline.config", + reverse_input_channels=True, +) +ov.save_model(ov_model, "/models/ssdlite_mobilenet_v2.xml") diff --git a/docker/main/build_pysqlite3.sh b/docker/main/build_pysqlite3.sh new file mode 100755 index 0000000..14d0cde --- /dev/null +++ b/docker/main/build_pysqlite3.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -euxo pipefail + +SQLITE3_VERSION="3.46.1" +PYSQLITE3_VERSION="0.5.3" + +# Install libsqlite3-dev if not present (needed for some base images like NVIDIA TensorRT) +if ! dpkg -l | grep -q libsqlite3-dev; then + echo "Installing libsqlite3-dev for compilation..." + apt-get update && apt-get install -y libsqlite3-dev && rm -rf /var/lib/apt/lists/* +fi + +# Fetch the pre-built sqlite amalgamation instead of building from source +if [[ ! -d "sqlite" ]]; then + mkdir sqlite + cd sqlite + + # Download the pre-built amalgamation from sqlite.org + # For SQLite 3.46.1, the amalgamation version is 3460100 + SQLITE_AMALGAMATION_VERSION="3460100" + + wget https://www.sqlite.org/2024/sqlite-amalgamation-${SQLITE_AMALGAMATION_VERSION}.zip -O sqlite-amalgamation.zip + unzip sqlite-amalgamation.zip + mv sqlite-amalgamation-${SQLITE_AMALGAMATION_VERSION}/* . + rmdir sqlite-amalgamation-${SQLITE_AMALGAMATION_VERSION} + rm sqlite-amalgamation.zip + + cd ../ +fi + +# Grab the pysqlite3 source code. +if [[ ! -d "./pysqlite3" ]]; then + git clone https://github.com/coleifer/pysqlite3.git +fi + +cd pysqlite3/ +git checkout ${PYSQLITE3_VERSION} + +# Copy the sqlite3 source amalgamation into the pysqlite3 directory so we can +# create a self-contained extension module. +cp "../sqlite/sqlite3.c" ./ +cp "../sqlite/sqlite3.h" ./ + +# Create the wheel and put it in the /wheels dir. +sed -i "s|name='pysqlite3-binary'|name=PACKAGE_NAME|g" setup.py +python3 setup.py build_static +pip3 wheel . -w /wheels diff --git a/docker/main/build_sqlite_vec.sh b/docker/main/build_sqlite_vec.sh new file mode 100755 index 0000000..b41f338 --- /dev/null +++ b/docker/main/build_sqlite_vec.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +set -euxo pipefail + +SQLITE_VEC_VERSION="0.1.3" + +source /etc/os-release + +if [[ "$VERSION_ID" == "12" ]]; then + sed -i '/^Types:/s/deb/& deb-src/' /etc/apt/sources.list.d/debian.sources +else + cp /etc/apt/sources.list /etc/apt/sources.list.d/sources-src.list + sed -i 's|deb http|deb-src http|g' /etc/apt/sources.list.d/sources-src.list +fi + +apt-get update +apt-get -yqq build-dep sqlite3 gettext git + +mkdir /tmp/sqlite_vec +# Grab the sqlite_vec source code. +wget -nv https://github.com/asg017/sqlite-vec/archive/refs/tags/v${SQLITE_VEC_VERSION}.tar.gz +tar -zxf v${SQLITE_VEC_VERSION}.tar.gz -C /tmp/sqlite_vec + +cd /tmp/sqlite_vec/sqlite-vec-${SQLITE_VEC_VERSION} + +mkdir -p vendor +wget -O sqlite-amalgamation.zip https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip +unzip sqlite-amalgamation.zip +mv sqlite-amalgamation-3450300/* vendor/ +rmdir sqlite-amalgamation-3450300 +rm sqlite-amalgamation.zip + +# build loadable module +make loadable + +# install it +cp dist/vec0.* /usr/local/lib + diff --git a/docker/main/fake_frigate_run b/docker/main/fake_frigate_run new file mode 100755 index 0000000..7344f62 --- /dev/null +++ b/docker/main/fake_frigate_run @@ -0,0 +1,13 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the fake Frigate service + +set -o errexit -o nounset -o pipefail + +# Tell S6-Overlay not to restart this service +s6-svc -O . + +while true; do + echo "[INFO] The fake Frigate service is running..." + sleep 5s +done diff --git a/docker/main/install_deps.sh b/docker/main/install_deps.sh new file mode 100755 index 0000000..e197ce1 --- /dev/null +++ b/docker/main/install_deps.sh @@ -0,0 +1,172 @@ +#!/bin/bash + +set -euxo pipefail + +apt-get -qq update + +apt-get -qq install --no-install-recommends -y \ + apt-transport-https \ + ca-certificates \ + gnupg \ + wget \ + lbzip2 \ + procps vainfo \ + unzip locales tzdata libxml2 xz-utils \ + python3.11 \ + curl \ + lsof \ + jq \ + nethogs \ + libgl1 \ + libglib2.0-0 \ + libusb-1.0.0 \ + python3-h2 \ + libgomp1 # memryx detector + +update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 + +mkdir -p -m 600 /root/.gnupg + +# install coral runtime +wget -q -O /tmp/libedgetpu1-max.deb "https://github.com/feranick/libedgetpu/releases/download/16.0TF2.17.1-1/libedgetpu1-max_16.0tf2.17.1-1.bookworm_${TARGETARCH}.deb" +unset DEBIAN_FRONTEND +yes | dpkg -i /tmp/libedgetpu1-max.deb && export DEBIAN_FRONTEND=noninteractive +rm /tmp/libedgetpu1-max.deb + +# install mesa-teflon-delegate from bookworm-backports +# Only available for arm64 at the moment +if [[ "${TARGETARCH}" == "arm64" ]]; then + if [[ "${BASE_IMAGE}" == *"nvcr.io/nvidia/tensorrt"* ]]; then + echo "Info: Skipping apt-get commands because BASE_IMAGE includes 'nvcr.io/nvidia/tensorrt' for arm64." + else + echo "deb http://deb.debian.org/debian bookworm-backports main" | tee /etc/apt/sources.list.d/bookworm-backbacks.list + apt-get -qq update + apt-get -qq install --no-install-recommends --no-install-suggests -y mesa-teflon-delegate/bookworm-backports + fi +fi + +# ffmpeg -> amd64 +if [[ "${TARGETARCH}" == "amd64" ]]; then + mkdir -p /usr/lib/ffmpeg/5.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linux64-gpl-5.1.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe + rm -rf ffmpeg.tar.xz + mkdir -p /usr/lib/ffmpeg/7.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linux64-gpl-7.0.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe + rm -rf ffmpeg.tar.xz + mkdir -p /usr/lib/ffmpeg/8.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linux64-gpl-8.1.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/8.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe + rm -rf ffmpeg.tar.xz +fi + +# ffmpeg -> arm64 +if [[ "${TARGETARCH}" == "arm64" ]]; then + mkdir -p /usr/lib/ffmpeg/5.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linuxarm64-gpl-5.1.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe + rm -f ffmpeg.tar.xz + mkdir -p /usr/lib/ffmpeg/7.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linuxarm64-gpl-7.0.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe + rm -f ffmpeg.tar.xz + mkdir -p /usr/lib/ffmpeg/8.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linuxarm64-gpl-8.1.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/8.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe + rm -f ffmpeg.tar.xz +fi + +# arch specific packages +if [[ "${TARGETARCH}" == "amd64" ]]; then + # Install non-free version of i965 driver + sed -i -E "/^Components: main$/s/main/main contrib non-free non-free-firmware/" "/etc/apt/sources.list.d/debian.sources" \ + && apt-get -qq update \ + && apt-get install --no-install-recommends --no-install-suggests -y i965-va-driver-shaders \ + && sed -i -E "/^Components: main contrib non-free non-free-firmware$/s/main contrib non-free non-free-firmware/main/" "/etc/apt/sources.list.d/debian.sources" \ + && apt-get update + + # install amd / intel-i965 driver packages + apt-get -qq install --no-install-recommends --no-install-suggests -y \ + intel-gpu-tools onevpl-tools \ + libva-drm2 \ + mesa-va-drivers radeontop + + # intel packages use zst compression so we need to update dpkg + apt-get install -y dpkg + + # use intel apt repo for libmfx1 (legacy QSV, pre-Gen12) + wget -qO - https://repositories.intel.com/gpu/intel-graphics.key | gpg --yes --dearmor --output /usr/share/keyrings/intel-graphics.gpg + echo "deb [arch=amd64 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/gpu/ubuntu jammy client" | tee /etc/apt/sources.list.d/intel-gpu-jammy.list + apt-get -qq update + + # intel-media-va-driver-non-free is built from source in the + # intel-media-driver Dockerfile stage for Battlemage (Xe2) support + apt-get -qq install --no-install-recommends --no-install-suggests -y \ + libmfx1 + rm -f /usr/share/keyrings/intel-graphics.gpg + rm -f /etc/apt/sources.list.d/intel-gpu-jammy.list + + # upgrade libva2, oneVPL runtime, and libvpl2 from trixie for Battlemage support + echo "deb http://deb.debian.org/debian trixie main" > /etc/apt/sources.list.d/trixie.list + apt-get -qq update + apt-get -qq install -y -t trixie libva2 libva-drm2 libzstd1 + apt-get -qq install -y -t trixie libmfx-gen1.2 libvpl2 + rm -f /etc/apt/sources.list.d/trixie.list + apt-get -qq update + apt-get -qq install -y ocl-icd-libopencl1 + + # install libtbb12 for NPU support + apt-get -qq install -y libtbb12 + + # install legacy and standard intel compute packages + # see https://github.com/intel/compute-runtime/blob/master/LEGACY_PLATFORMS.md for more info + # needed core package + wget https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libigdgmm12_22.9.0_amd64.deb + dpkg -i libigdgmm12_22.9.0_amd64.deb + rm libigdgmm12_22.9.0_amd64.deb + + # legacy compute-runtime 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-level-zero-gpu-legacy1_1.5.30872.36_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 + # standard compute-runtime packages + wget https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/intel-opencl-icd_26.14.37833.4-0_amd64.deb + wget https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libze-intel-gpu1_26.14.37833.4-0_amd64.deb + wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-opencl-2_2.32.7+21184_amd64.deb + wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-core-2_2.32.7+21184_amd64.deb + # npu packages + 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.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.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.19.0/intel-level-zero-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb + + dpkg -i *.deb + rm *.deb + apt-get -qq install -f -y + + # Battlemage uses the xe kernel driver, but the VA-API driver is still iHD. + # The oneVPL runtime may look for a driver named after the kernel module. + ln -sf /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so /usr/lib/x86_64-linux-gnu/dri/xe_drv_video.so +fi + +if [[ "${TARGETARCH}" == "arm64" ]]; then + apt-get -qq install --no-install-recommends --no-install-suggests -y \ + libva-drm2 mesa-va-drivers radeontop +fi + +# install vulkan +apt-get -qq install --no-install-recommends --no-install-suggests -y \ + libvulkan1 mesa-vulkan-drivers + +apt-get purge gnupg apt-transport-https xz-utils -y +apt-get clean autoclean -y +apt-get autoremove --purge -y +rm -rf /var/lib/apt/lists/* + +# Install yq, for frigate-prepare and go2rtc echo source +curl -fsSL \ + "https://github.com/mikefarah/yq/releases/download/v4.48.2/yq_linux_$(dpkg --print-architecture)" \ + --output /usr/local/bin/yq +chmod +x /usr/local/bin/yq diff --git a/docker/main/install_hailort.sh b/docker/main/install_hailort.sh new file mode 100755 index 0000000..2e568a1 --- /dev/null +++ b/docker/main/install_hailort.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -euxo pipefail + +hailo_version="4.21.0" + +if [[ "${TARGETARCH}" == "amd64" ]]; then + arch="x86_64" +elif [[ "${TARGETARCH}" == "arm64" ]]; then + arch="aarch64" +fi + +wget -qO- "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-debian12-${TARGETARCH}.tar.gz" | tar -C / -xzf - +wget -P /wheels/ "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-${hailo_version}-cp311-cp311-linux_${arch}.whl" diff --git a/docker/main/install_memryx.sh b/docker/main/install_memryx.sh new file mode 100644 index 0000000..676e06d --- /dev/null +++ b/docker/main/install_memryx.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +# Download the MxAccl for Frigate github release +wget https://github.com/memryx/mx_accl_frigate/archive/refs/tags/v2.1.0.zip -O /tmp/mxaccl.zip +unzip /tmp/mxaccl.zip -d /tmp +mv /tmp/mx_accl_frigate-2.1.0 /opt/mx_accl_frigate +rm /tmp/mxaccl.zip + +# Install Python dependencies +pip3 install -r /opt/mx_accl_frigate/freeze + +# Link the Python package dynamically +SITE_PACKAGES=$(python3 -c "import site; print(site.getsitepackages()[0])") +ln -s /opt/mx_accl_frigate/memryx "$SITE_PACKAGES/memryx" + +# Copy architecture-specific shared libraries +ARCH=$(uname -m) +if [[ "$ARCH" == "x86_64" ]]; then + cp /opt/mx_accl_frigate/memryx/x86/libmemx.so* /usr/lib/x86_64-linux-gnu/ + cp /opt/mx_accl_frigate/memryx/x86/libmx_accl.so* /usr/lib/x86_64-linux-gnu/ +elif [[ "$ARCH" == "aarch64" ]]; then + cp /opt/mx_accl_frigate/memryx/arm/libmemx.so* /usr/lib/aarch64-linux-gnu/ + cp /opt/mx_accl_frigate/memryx/arm/libmx_accl.so* /usr/lib/aarch64-linux-gnu/ +else + echo "Unsupported architecture: $ARCH" + exit 1 +fi + +# Refresh linker cache +ldconfig diff --git a/docker/main/install_s6_overlay.sh b/docker/main/install_s6_overlay.sh new file mode 100755 index 0000000..3ea387c --- /dev/null +++ b/docker/main/install_s6_overlay.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euxo pipefail + +s6_version="3.2.1.0" + +if [[ "${TARGETARCH}" == "amd64" ]]; then + s6_arch="x86_64" +elif [[ "${TARGETARCH}" == "arm64" ]]; then + s6_arch="aarch64" +fi + +mkdir -p /rootfs/ + +wget -qO- "https://github.com/just-containers/s6-overlay/releases/download/v${s6_version}/s6-overlay-noarch.tar.xz" | + tar -C /rootfs/ -Jxpf - + +wget -qO- "https://github.com/just-containers/s6-overlay/releases/download/v${s6_version}/s6-overlay-${s6_arch}.tar.xz" | + tar -C /rootfs/ -Jxpf - diff --git a/docker/main/install_tempio.sh b/docker/main/install_tempio.sh new file mode 100755 index 0000000..743a122 --- /dev/null +++ b/docker/main/install_tempio.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euxo pipefail + +tempio_version="2021.09.0" + +if [[ "${TARGETARCH}" == "amd64" ]]; then + arch="amd64" +elif [[ "${TARGETARCH}" == "arm64" ]]; then + arch="aarch64" +fi + +mkdir -p /rootfs/usr/local/tempio/bin + +wget -q -O /rootfs/usr/local/tempio/bin/tempio "https://github.com/home-assistant/tempio/releases/download/${tempio_version}/tempio_${arch}" +chmod 755 /rootfs/usr/local/tempio/bin/tempio diff --git a/docker/main/requirements-dev.txt b/docker/main/requirements-dev.txt new file mode 100644 index 0000000..df5818f --- /dev/null +++ b/docker/main/requirements-dev.txt @@ -0,0 +1,4 @@ +ruff == 0.15.20 + +# types +types-peewee == 3.17.* diff --git a/docker/main/requirements-ov.txt b/docker/main/requirements-ov.txt new file mode 100644 index 0000000..6fd1ca5 --- /dev/null +++ b/docker/main/requirements-ov.txt @@ -0,0 +1,3 @@ +numpy +tensorflow +openvino-dev>=2024.0.0 \ No newline at end of file diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt new file mode 100644 index 0000000..7bd0984 --- /dev/null +++ b/docker/main/requirements-wheels.txt @@ -0,0 +1,85 @@ +aiofiles == 24.1.* +click == 8.1.* +# FastAPI +aiohttp == 3.12.* +starlette == 0.47.* +starlette-context == 0.4.* +fastapi[standard-no-fastapi-cloud-cli] == 0.116.* +uvicorn == 0.35.* +slowapi == 0.1.* +joserfc == 1.2.* +cryptography == 44.0.* +pathvalidate == 3.3.* +markupsafe == 3.0.* +python-multipart == 0.0.26 +# Classification Model Training +tensorflow == 2.19.* ; platform_machine == 'aarch64' +tensorflow-cpu == 2.19.* ; platform_machine == 'x86_64' +# General +mypy == 1.6.1 +onvif-zeep-async == 4.0.* +paho-mqtt == 2.1.* +pandas == 2.2.* +peewee == 3.17.* +peewee_migrate == 1.14.* +psutil == 7.1.* +pydantic == 2.10.* +git+https://github.com/fbcotter/py3nvml#egg=py3nvml +pytz == 2025.* +pyzmq == 26.2.* +ruamel.yaml == 0.18.* +tzlocal == 5.2 +requests == 2.32.* +types-requests == 2.32.* +norfair == 2.3.* +setproctitle == 1.3.* +ws4py == 0.5.* +unidecode == 1.3.* +titlecase == 2.4.* +# Image Manipulation +numpy == 1.26.* +opencv-python-headless == 4.11.0.* +opencv-contrib-python == 4.11.0.* +scipy == 1.16.* +# OpenVino & ONNX +openvino == 2025.4.* +onnxruntime == 1.22.* +# Embeddings +transformers == 4.45.* +# Generative AI +google-genai == 1.58.* +ollama == 0.6.* +openai == 1.65.* +# push notifications +py-vapid == 1.9.* +pywebpush == 2.0.* +# alpr +pyclipper == 1.3.* +shapely == 2.0.* +rapidfuzz==3.12.* +# HailoRT Wheels +appdirs==1.4.* +argcomplete==2.0.* +contextlib2==0.6.* +distlib==0.3.* +filelock==3.8.* +future==0.18.* +importlib-metadata==5.1.* +importlib-resources==5.1.* +netaddr==0.8.* +netifaces==0.10.* +verboselogs==1.7.* +virtualenv==20.17.* +prometheus-client == 0.21.* +# TFLite +tflite_runtime @ https://github.com/frigate-nvr/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_x86_64.whl; platform_machine == 'x86_64' +tflite_runtime @ https://github.com/feranick/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_aarch64.whl; platform_machine == 'aarch64' +# audio transcription +sherpa-onnx==1.12.* +faster-whisper==1.1.* +librosa==0.11.* +soundfile==0.13.* +# DeGirum detector +degirum == 0.16.* +# Memory profiling +memray == 1.15.* diff --git a/docker/main/requirements.txt b/docker/main/requirements.txt new file mode 100644 index 0000000..f1ba7d9 --- /dev/null +++ b/docker/main/requirements.txt @@ -0,0 +1 @@ +scikit-build == 0.18.* diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/consumer-for b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/consumer-for new file mode 100644 index 0000000..09a147a --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/consumer-for @@ -0,0 +1 @@ +certsync diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/dependencies.d/log-prepare b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/dependencies.d/log-prepare new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/pipeline-name b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/pipeline-name new file mode 100644 index 0000000..204da27 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/pipeline-name @@ -0,0 +1 @@ +certsync-pipeline diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/run new file mode 100755 index 0000000..7d66e2c --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/run @@ -0,0 +1,4 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +exec logutil-service /dev/shm/logs/certsync diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/type @@ -0,0 +1 @@ +longrun diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/dependencies.d/nginx b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/dependencies.d/nginx new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/finish b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/finish new file mode 100755 index 0000000..3450034 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/finish @@ -0,0 +1,30 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Take down the S6 supervision tree when the service fails + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +declare exit_code_container +exit_code_container=$(cat /run/s6-linux-init-container-results/exitcode) +readonly exit_code_container +readonly exit_code_service="${1}" +readonly exit_code_signal="${2}" +readonly service="CERTSYNC" + +echo "[INFO] Service ${service} exited with code ${exit_code_service} (by signal ${exit_code_signal})" + +if [[ "${exit_code_service}" -eq 256 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo $((128 + exit_code_signal)) >/run/s6-linux-init-container-results/exitcode + fi + if [[ "${exit_code_signal}" -eq 15 ]]; then + exec /run/s6/basedir/bin/halt + fi +elif [[ "${exit_code_service}" -ne 0 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo "${exit_code_service}" >/run/s6-linux-init-container-results/exitcode + fi + exec /run/s6/basedir/bin/halt +fi diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/producer-for b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/producer-for new file mode 100644 index 0000000..886683f --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/producer-for @@ -0,0 +1 @@ +certsync-log diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run new file mode 100755 index 0000000..b834c09 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run @@ -0,0 +1,59 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the CERTSYNC service + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +echo "[INFO] Starting certsync..." + +lefile="/etc/letsencrypt/live/frigate/fullchain.pem" + +tls_enabled=`python3 /usr/local/nginx/get_nginx_settings.py | jq -r .tls.enabled` +listen_external_port=`python3 /usr/local/nginx/get_nginx_settings.py | jq -r .listen.external_port` + +while true +do + if [[ "$tls_enabled" == 'false' ]]; then + sleep 9999 + continue + fi + + if [ ! -e $lefile ] + then + echo "[ERROR] TLS certificate does not exist: $lefile" + fi + + leprint=`openssl x509 -in $lefile -fingerprint -noout 2>&1 || echo 'failed'` + + case "$leprint" in + *Fingerprint*) + ;; + *) + echo "[ERROR] Missing fingerprint from $lefile" + ;; + esac + + liveprint=`echo | openssl s_client -showcerts -connect 127.0.0.1:$listen_external_port 2>&1 | openssl x509 -fingerprint 2>&1 | grep -i fingerprint || echo 'failed'` + + case "$liveprint" in + *Fingerprint*) + ;; + *) + echo "[ERROR] Missing fingerprint from current nginx TLS cert" + ;; + esac + + if [[ "$leprint" != "failed" && "$liveprint" != "failed" && "$leprint" != "$liveprint" ]] + then + echo "[INFO] Reloading nginx to refresh TLS certificate" + echo "$lefile: $leprint" + /usr/local/nginx/sbin/nginx -s reload + fi + + sleep 60 + +done + +exit 0 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/timeout-kill b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/timeout-kill new file mode 100644 index 0000000..3a05c8b --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/timeout-kill @@ -0,0 +1 @@ +30000 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/type @@ -0,0 +1 @@ +longrun diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/consumer-for b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/consumer-for new file mode 100644 index 0000000..5e93017 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/consumer-for @@ -0,0 +1 @@ +frigate diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/dependencies.d/log-prepare b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/dependencies.d/log-prepare new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/pipeline-name b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/pipeline-name new file mode 100644 index 0000000..01f465e --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/pipeline-name @@ -0,0 +1 @@ +frigate-pipeline diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/run new file mode 100755 index 0000000..c102848 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/run @@ -0,0 +1,4 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +exec logutil-service /dev/shm/logs/frigate diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/type @@ -0,0 +1 @@ +longrun diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/dependencies.d/go2rtc b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/dependencies.d/go2rtc new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/finish b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/finish new file mode 100755 index 0000000..75869b5 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/finish @@ -0,0 +1,28 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Take down the S6 supervision tree when the service exits + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +declare exit_code_container +exit_code_container=$(cat /run/s6-linux-init-container-results/exitcode) +readonly exit_code_container +readonly exit_code_service="${1}" +readonly exit_code_signal="${2}" +readonly service="Frigate" + +echo "[INFO] Service ${service} exited with code ${exit_code_service} (by signal ${exit_code_signal})" + +if [[ "${exit_code_service}" -eq 256 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo $((128 + exit_code_signal)) >/run/s6-linux-init-container-results/exitcode + fi +elif [[ "${exit_code_service}" -ne 0 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo "${exit_code_service}" >/run/s6-linux-init-container-results/exitcode + fi +fi + +exec /run/s6/basedir/bin/halt diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/producer-for b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/producer-for new file mode 100644 index 0000000..65f1316 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/producer-for @@ -0,0 +1 @@ +frigate-log diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run new file mode 100755 index 0000000..9c84c20 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run @@ -0,0 +1,33 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the Frigate service + +set -o errexit -o nounset -o pipefail + +# opt out of openvino telemetry +if [ -e /usr/local/bin/opt_in_out ]; then + /usr/local/bin/opt_in_out --opt_out > /dev/null 2>&1 +fi + +# Logs should be sent to stdout so that s6 can collect them + +# Tell S6-Overlay not to restart this service +s6-svc -O . + +function set_libva_version() { + local ffmpeg_path + ffmpeg_path=$(python3 /usr/local/ffmpeg/get_ffmpeg_path.py) + LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+") + export LIBAVFORMAT_VERSION_MAJOR +} + +echo "[INFO] Preparing Frigate..." +set_libva_version + +echo "[INFO] Starting Frigate..." + +cd /opt/frigate || echo "[ERROR] Failed to change working directory to /opt/frigate" + +# Replace the bash process with the Frigate process, redirecting stderr to stdout +exec 2>&1 +exec python3 -u -m frigate diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/timeout-kill b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/timeout-kill new file mode 100644 index 0000000..6f4f418 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/timeout-kill @@ -0,0 +1 @@ +120000 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/type @@ -0,0 +1 @@ +longrun diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/dependencies.d/go2rtc b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/dependencies.d/go2rtc new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/finish b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/finish new file mode 100755 index 0000000..f834216 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/finish @@ -0,0 +1,12 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +readonly exit_code_service="${1}" +readonly exit_code_signal="${2}" +readonly service="go2rtc-healthcheck" + +echo "[INFO] The ${service} service exited with code ${exit_code_service} (by signal ${exit_code_signal})" diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/producer-for b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/producer-for new file mode 100644 index 0000000..20fbc45 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/producer-for @@ -0,0 +1 @@ +go2rtc-log diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/run new file mode 100755 index 0000000..3a6e423 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/run @@ -0,0 +1,22 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the go2rtc-healthcheck service + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +# Give some additional time for go2rtc to start before start pinging +sleep 10s +echo "[INFO] Starting go2rtc healthcheck service..." + +while sleep 30s; do + # Check if the service is running + if ! curl --connect-timeout 10 --fail --silent --show-error --output /dev/null http://127.0.0.1:1984/api/streams 2>&1; then + echo "[ERROR] The go2rtc service is not responding to ping, restarting..." + # We can also use -r instead of -t to send kill signal rather than term + s6-svc -t /var/run/service/go2rtc 2>&1 + # Give some additional time to go2rtc to restart before start pinging again + sleep 10s + fi +done diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/timeout-kill b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/timeout-kill new file mode 100644 index 0000000..e9c02da --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/timeout-kill @@ -0,0 +1 @@ +5000 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/type @@ -0,0 +1 @@ +longrun diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/consumer-for b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/consumer-for new file mode 100644 index 0000000..bdd482e --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/consumer-for @@ -0,0 +1,2 @@ +go2rtc +go2rtc-healthcheck diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/dependencies.d/log-prepare b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/dependencies.d/log-prepare new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/pipeline-name b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/pipeline-name new file mode 100644 index 0000000..1fe5452 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/pipeline-name @@ -0,0 +1 @@ +go2rtc-pipeline diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/run new file mode 100755 index 0000000..96a204b --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/run @@ -0,0 +1,4 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +exec logutil-service /dev/shm/logs/go2rtc diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/type @@ -0,0 +1 @@ +longrun diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/dependencies.d/prepare b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/dependencies.d/prepare new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/finish b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/finish new file mode 100755 index 0000000..e95ba75 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/finish @@ -0,0 +1,12 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +readonly exit_code_service="${1}" +readonly exit_code_signal="${2}" +readonly service="go2rtc" + +echo "[INFO] The ${service} service exited with code ${exit_code_service} (by signal ${exit_code_signal})" diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/producer-for b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/producer-for new file mode 100644 index 0000000..20fbc45 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/producer-for @@ -0,0 +1 @@ +go2rtc-log diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run new file mode 100755 index 0000000..599ab88 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run @@ -0,0 +1,128 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the go2rtc service + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +function get_ip_and_port_from_supervisor() { + local ip_address + # Example: 192.168.1.10/24 + local ip_regex='^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/[0-9]{1,2}$' + if ip_address=$( + curl -fsSL \ + -H "Authorization: Bearer ${SUPERVISOR_TOKEN}" \ + -H "Content-Type: application/json" \ + http://supervisor/network/interface/default/info | + jq --exit-status --raw-output '.data.ipv4.address[0]' + ) && [[ "${ip_address}" =~ ${ip_regex} ]]; then + ip_address="${BASH_REMATCH[1]}" + echo "[INFO] Got IP address from supervisor: ${ip_address}" + else + echo "[WARN] Failed to get IP address from supervisor" + return 0 + fi + + local webrtc_port + local port_regex='^([0-9]{1,5})$' + if webrtc_port=$( + curl -fsSL \ + -H "Authorization: Bearer ${SUPERVISOR_TOKEN}" \ + -H "Content-Type: application/json" \ + http://supervisor/addons/self/info | + jq --exit-status --raw-output '.data.network["8555/tcp"]' + ) && [[ "${webrtc_port}" =~ ${port_regex} ]]; then + webrtc_port="${BASH_REMATCH[1]}" + echo "[INFO] Got WebRTC port from supervisor: ${webrtc_port}" + else + echo "[WARN] Failed to get WebRTC port from supervisor" + return 0 + fi + + export FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL="${ip_address}:${webrtc_port}" +} + +function set_libva_version() { + local ffmpeg_path + ffmpeg_path=$(python3 /usr/local/ffmpeg/get_ffmpeg_path.py) + LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+") + export LIBAVFORMAT_VERSION_MAJOR +} + +function setup_homekit_config() { + local config_path="$1" + + if [[ ! -f "${config_path}" ]]; then + echo "[INFO] Creating empty config file for HomeKit..." + : > "${config_path}" + fi + + # Convert YAML to JSON for jq processing + local temp_json="/tmp/cache/homekit_config.json" + yq eval -o=json "${config_path}" > "${temp_json}" 2>/dev/null || { + echo "[WARNING] Failed to convert HomeKit config to JSON, skipping cleanup" + return 0 + } + + # Use jq to extract the homekit section, if it exists + local homekit_json + homekit_json=$(jq ' + if has("homekit") then {homekit: .homekit} else null end + ' "${temp_json}" 2>/dev/null) || homekit_json="null" + + # If no homekit section, write an empty config file + if [[ "${homekit_json}" == "null" ]]; then + : > "${config_path}" + else + # Convert homekit JSON back to YAML and write to the config file + echo "${homekit_json}" | yq eval -P - > "${config_path}" 2>/dev/null || { + echo "[WARNING] Failed to convert cleaned config to YAML, creating minimal config" + : > "${config_path}" + } + fi + + # Clean up temp files + rm -f "${temp_json}" +} + +set_libva_version + +if [[ -f "/dev/shm/go2rtc.yaml" ]]; then + echo "[INFO] Removing stale config from last run..." + rm /dev/shm/go2rtc.yaml +fi + +if [[ ! -f "/dev/shm/go2rtc.yaml" ]]; then + echo "[INFO] Preparing new go2rtc config..." + + if [[ -n "${SUPERVISOR_TOKEN:-}" ]]; then + # Running as a Home Assistant Add-on, infer the IP address and port + get_ip_and_port_from_supervisor + fi + + python3 /usr/local/go2rtc/create_config.py +else + echo "[WARNING] Unable to remove existing go2rtc config. Changes made to your frigate config file may not be recognized. Please remove the /dev/shm/go2rtc.yaml from your docker host manually." +fi + +# HomeKit configuration persistence setup +readonly homekit_config_path="/config/go2rtc_homekit.yml" +setup_homekit_config "${homekit_config_path}" + +readonly config_path="/config" + +if [[ -x "${config_path}/go2rtc" ]]; then + readonly binary_path="${config_path}/go2rtc" + echo "[WARN] Using go2rtc binary from '${binary_path}' instead of the embedded one" +else + readonly binary_path="/usr/local/go2rtc/bin/go2rtc" +fi + +echo "[INFO] Starting go2rtc..." + +# Replace the bash process with the go2rtc process, redirecting stderr to stdout +# Use HomeKit config as the primary config so writebacks go there +# The main config from Frigate will be loaded as a secondary config +exec 2>&1 +exec "${binary_path}" -config="${homekit_config_path}" -config=/dev/shm/go2rtc.yaml diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/timeout-kill b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/timeout-kill new file mode 100644 index 0000000..3a05c8b --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/timeout-kill @@ -0,0 +1 @@ +30000 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/type @@ -0,0 +1 @@ +longrun diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/dependencies.d/base b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/dependencies.d/base new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/run new file mode 100755 index 0000000..c493e32 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/run @@ -0,0 +1,11 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Prepare the logs folder for s6-log + +set -o errexit -o nounset -o pipefail + +dirs=(/dev/shm/logs/frigate /dev/shm/logs/go2rtc /dev/shm/logs/nginx /dev/shm/logs/certsync) + +mkdir -p "${dirs[@]}" +chown nobody:nogroup "${dirs[@]}" +chmod 02755 "${dirs[@]}" diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/type new file mode 100644 index 0000000..bdd22a1 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/up b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/up new file mode 100644 index 0000000..f90be02 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/log-prepare/run diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/consumer-for b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/consumer-for new file mode 100644 index 0000000..68b7d12 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/consumer-for @@ -0,0 +1 @@ +nginx diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/dependencies.d/log-prepare b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/dependencies.d/log-prepare new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/pipeline-name b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/pipeline-name new file mode 100644 index 0000000..e22259a --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/pipeline-name @@ -0,0 +1 @@ +nginx-pipeline diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/run new file mode 100755 index 0000000..50057d1 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/run @@ -0,0 +1,4 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +exec logutil-service /dev/shm/logs/nginx diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/type @@ -0,0 +1 @@ +longrun diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/data/check b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/data/check new file mode 100755 index 0000000..8307a79 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/data/check @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -e + +# Wait for PID file to exist. +while ! test -f /run/nginx.pid; do sleep 1; done \ No newline at end of file diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/frigate b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/frigate new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/finish b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/finish new file mode 100755 index 0000000..d147d74 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/finish @@ -0,0 +1,30 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Take down the S6 supervision tree when the service fails + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +declare exit_code_container +exit_code_container=$(cat /run/s6-linux-init-container-results/exitcode) +readonly exit_code_container +readonly exit_code_service="${1}" +readonly exit_code_signal="${2}" +readonly service="NGINX" + +echo "[INFO] Service ${service} exited with code ${exit_code_service} (by signal ${exit_code_signal})" + +if [[ "${exit_code_service}" -eq 256 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo $((128 + exit_code_signal)) >/run/s6-linux-init-container-results/exitcode + fi + if [[ "${exit_code_signal}" -eq 15 ]]; then + exec /run/s6/basedir/bin/halt + fi +elif [[ "${exit_code_service}" -ne 0 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo "${exit_code_service}" >/run/s6-linux-init-container-results/exitcode + fi + exec /run/s6/basedir/bin/halt +fi diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/notification-fd b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/notification-fd new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/notification-fd @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/producer-for b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/producer-for new file mode 100644 index 0000000..307d740 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/producer-for @@ -0,0 +1 @@ +nginx-log diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run new file mode 100755 index 0000000..a3c7b32 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -0,0 +1,96 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the NGINX service + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +echo "[INFO] Starting NGINX..." + +# Taken from https://github.com/felipecrs/cgroup-scripts/commits/master/get_cpus.sh +function get_cpus() { + local quota="" + local period="" + + if [ -f /sys/fs/cgroup/cgroup.controllers ]; then + if [ -f /sys/fs/cgroup/cpu.max ]; then + read -r quota period &2 + fi + else + if [ -f /sys/fs/cgroup/cpu/cpu.cfs_quota_us ] && [ -f /sys/fs/cgroup/cpu/cpu.cfs_period_us ]; then + quota=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us) + period=$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us) + + if [ "$quota" = "-1" ]; then + quota="" + period="" + fi + else + echo "[WARN] /sys/fs/cgroup/cpu/cpu.cfs_quota_us or /sys/fs/cgroup/cpu/cpu.cfs_period_us not found. Falling back to /proc/cpuinfo." >&2 + fi + fi + + local cpus + if [ "${period}" != "0" ] && [ -n "${quota}" ] && [ -n "${period}" ]; then + cpus=$((quota / period)) + if [ "$cpus" -eq 0 ]; then + cpus=1 + fi + else + cpus=$(grep -c ^processor /proc/cpuinfo) + fi + + printf '%s' "$cpus" +} + +function set_worker_processes() { + # Capture number of assigned CPUs to calculate worker processes + local cpus + + cpus=$(get_cpus) + if [[ "${cpus}" -gt 4 ]]; then + cpus=4 + fi + + # we need to catch any errors because sed will fail if user has bind mounted a custom nginx file + sed -i "s/worker_processes auto;/worker_processes ${cpus};/" /usr/local/nginx/conf/nginx.conf || true +} + +set_worker_processes + +# ensure the directory for ACME challenges exists +mkdir -p /etc/letsencrypt/www + +# Create self signed certs if needed +letsencrypt_path=/etc/letsencrypt/live/frigate +mkdir -p $letsencrypt_path + +if [ ! \( -f "$letsencrypt_path/privkey.pem" -a -f "$letsencrypt_path/fullchain.pem" \) ]; then + echo "[INFO] No TLS certificate found. Generating a self signed certificate..." + openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 \ + -subj "/O=FRIGATE DEFAULT CERT/CN=*" \ + -keyout "$letsencrypt_path/privkey.pem" -out "$letsencrypt_path/fullchain.pem" 2>/dev/null +fi + +# build templates for optional FRIGATE_BASE_PATH environment variable +python3 /usr/local/nginx/get_nginx_settings.py | \ + tempio -template /usr/local/nginx/templates/base_path.gotmpl \ + -out /usr/local/nginx/conf/base_path.conf + +# build templates for additional network settings +python3 /usr/local/nginx/get_nginx_settings.py | \ + tempio -template /usr/local/nginx/templates/listen.gotmpl \ + -out /usr/local/nginx/conf/listen.conf + +# Replace the bash process with the NGINX process, redirecting stderr to stdout +exec 2>&1 +exec \ + s6-notifyoncheck -t 30000 -n 1 \ + nginx diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/timeout-kill b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/timeout-kill new file mode 100644 index 0000000..3a05c8b --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/timeout-kill @@ -0,0 +1 @@ +30000 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/type @@ -0,0 +1 @@ +longrun diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/dependencies.d/base b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/dependencies.d/base new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/run new file mode 100755 index 0000000..27b1d63 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/run @@ -0,0 +1,146 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Do preparation tasks before starting the main services + +set -o errexit -o nounset -o pipefail + +function migrate_addon_config_dir() { + local home_assistant_config_dir="/homeassistant" + + if ! mountpoint --quiet "${home_assistant_config_dir}"; then + # Not running as a Home Assistant Add-on + return 0 + fi + + local config_dir="/config" + local new_config_file="${config_dir}/config.yml" + local new_config_file_yaml="${new_config_file//.yml/.yaml}" + if [[ -f "${new_config_file_yaml}" || -f "${new_config_file}" ]]; then + # Already migrated + return 0 + fi + + local old_config_file="${home_assistant_config_dir}/frigate.yml" + local old_config_file_yaml="${old_config_file//.yml/.yaml}" + if [[ -f "${old_config_file}" ]]; then + : + elif [[ -f "${old_config_file_yaml}" ]]; then + old_config_file="${old_config_file_yaml}" + new_config_file="${new_config_file_yaml}" + else + # Nothing to migrate + return 0 + fi + unset old_config_file_yaml new_config_file_yaml + + echo "[INFO] Starting migration from Home Assistant config dir to Add-on config dir..." >&2 + + local db_path + db_path=$(yq -r '.database.path' "${old_config_file}") + if [[ "${db_path}" == "null" ]]; then + db_path="${config_dir}/frigate.db" + fi + if [[ "${db_path}" == "${config_dir}/"* ]]; then + # replace /config/ prefix with /homeassistant/ + local old_db_path="${home_assistant_config_dir}/${db_path:8}" + + if [[ -f "${old_db_path}" ]]; then + local new_db_dir + new_db_dir="$(dirname "${db_path}")" + echo "[INFO] Migrating database from '${old_db_path}' to '${new_db_dir}' dir..." >&2 + mkdir -vp "${new_db_dir}" + mv -vf "${old_db_path}" "${new_db_dir}" + local db_file + for db_file in "${old_db_path}"-shm "${old_db_path}"-wal; do + if [[ -f "${db_file}" ]]; then + mv -vf "${db_file}" "${new_db_dir}" + fi + done + unset db_file + fi + fi + + local config_entry + for config_entry in .model.path .model.labelmap_path .ffmpeg.path .mqtt.tls_ca_certs .mqtt.tls_client_cert .mqtt.tls_client_key; do + local config_entry_path + config_entry_path=$(yq -r "${config_entry}" "${old_config_file}") + if [[ "${config_entry_path}" == "${config_dir}/"* ]]; then + # replace /config/ prefix with /homeassistant/ + local old_config_entry_path="${home_assistant_config_dir}/${config_entry_path:8}" + + if [[ -f "${old_config_entry_path}" ]]; then + local new_config_entry_entry + new_config_entry_entry="$(dirname "${config_entry_path}")" + echo "[INFO] Migrating ${config_entry} from '${old_config_entry_path}' to '${config_entry_path}'..." >&2 + mkdir -vp "${new_config_entry_entry}" + mv -vf "${old_config_entry_path}" "${config_entry_path}" + fi + fi + done + + local old_model_cache_path="${home_assistant_config_dir}/model_cache" + if [[ -d "${old_model_cache_path}" ]]; then + echo "[INFO] Migrating '${old_model_cache_path}' to '${config_dir}'..." >&2 + mv -f "${old_model_cache_path}" "${config_dir}" + fi + + echo "[INFO] Migrating other files from '${home_assistant_config_dir}' to '${config_dir}'..." >&2 + local file + for file in .exports .jwt_secret .timeline .vacuum go2rtc; do + file="${home_assistant_config_dir}/${file}" + if [[ -f "${file}" ]]; then + mv -vf "${file}" "${config_dir}" + fi + done + + echo "[INFO] Migrating config file from '${old_config_file}' to '${new_config_file}'..." >&2 + mv -vf "${old_config_file}" "${new_config_file}" + + echo "[INFO] Migration from Home Assistant config dir to Add-on config dir completed." >&2 +} + +function migrate_db_from_media_to_config() { + # Find config file in yml or yaml, but prefer yml + local config_file="${CONFIG_FILE:-"/config/config.yml"}" + local config_file_yaml="${config_file//.yml/.yaml}" + if [[ -f "${config_file}" ]]; then + : + elif [[ -f "${config_file_yaml}" ]]; then + config_file="${config_file_yaml}" + else + # Frigate will create the config file on startup + return 0 + fi + unset config_file_yaml + + local user_db_path + user_db_path=$(yq -r '.database.path' "${config_file}") + if [[ "${user_db_path}" == "null" ]]; then + local old_db_path="/media/frigate/frigate.db" + local new_db_dir="/config" + if [[ -f "${old_db_path}" ]]; then + echo "[INFO] Migrating database from '${old_db_path}' to '${new_db_dir}' dir..." >&2 + if mountpoint --quiet "${new_db_dir}"; then + # /config is a mount point, move the db + mv -vf "${old_db_path}" "${new_db_dir}" + local db_file + for db_file in "${old_db_path}"-shm "${old_db_path}"-wal; do + if [[ -f "${db_file}" ]]; then + mv -vf "${db_file}" "${new_db_dir}" + fi + done + unset db_file + else + echo "[ERROR] Trying to migrate the database path from '${old_db_path}' to '${new_db_dir}' dir, but '${new_db_dir}' is not a mountpoint, please mount the '${new_db_dir}' dir" >&2 + return 1 + fi + fi + fi +} + +# remove leftover from last run, not normally needed, but just in case +# used by the docker healthcheck +rm -f /dev/shm/.frigate-is-stopping + +migrate_addon_config_dir +migrate_db_from_media_to_config diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/type b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/type new file mode 100644 index 0000000..bdd22a1 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/up b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/up new file mode 100644 index 0000000..ea17af5 --- /dev/null +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/prepare/run diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/certsync-pipeline b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/certsync-pipeline new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/frigate-pipeline b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/frigate-pipeline new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/go2rtc-pipeline b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/go2rtc-pipeline new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx-pipeline b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx-pipeline new file mode 100644 index 0000000..e69de29 diff --git a/docker/main/rootfs/labelmap/coco-80.txt b/docker/main/rootfs/labelmap/coco-80.txt new file mode 100644 index 0000000..79e0171 --- /dev/null +++ b/docker/main/rootfs/labelmap/coco-80.txt @@ -0,0 +1,80 @@ +0 person +1 bicycle +2 car +3 motorcycle +4 airplane +5 car +6 train +7 car +8 boat +9 traffic light +10 fire hydrant +11 stop sign +12 parking meter +13 bench +14 bird +15 cat +16 dog +17 horse +18 sheep +19 cow +20 elephant +21 bear +22 zebra +23 giraffe +24 backpack +25 umbrella +26 handbag +27 tie +28 suitcase +29 frisbee +30 skis +31 snowboard +32 sports ball +33 kite +34 baseball bat +35 baseball glove +36 skateboard +37 surfboard +38 tennis racket +39 bottle +40 wine glass +41 cup +42 fork +43 knife +44 spoon +45 bowl +46 banana +47 apple +48 sandwich +49 orange +50 broccoli +51 carrot +52 hot dog +53 pizza +54 donut +55 cake +56 chair +57 couch +58 potted plant +59 bed +60 dining table +61 toilet +62 tv +63 laptop +64 mouse +65 remote +66 keyboard +67 cell phone +68 microwave +69 oven +70 toaster +71 sink +72 refrigerator +73 book +74 clock +75 vase +76 scissors +77 teddy bear +78 hair drier +79 toothbrush \ No newline at end of file diff --git a/docker/main/rootfs/labelmap/coco.txt b/docker/main/rootfs/labelmap/coco.txt new file mode 100644 index 0000000..79fff17 --- /dev/null +++ b/docker/main/rootfs/labelmap/coco.txt @@ -0,0 +1,91 @@ +0 person +1 bicycle +2 car +3 motorcycle +4 airplane +5 bus +6 train +7 car +8 boat +9 traffic light +10 fire hydrant +11 street sign +12 stop sign +13 parking meter +14 bench +15 bird +16 cat +17 dog +18 horse +19 sheep +20 cow +21 elephant +22 bear +23 zebra +24 giraffe +25 hat +26 backpack +27 umbrella +28 shoe +29 eye glasses +30 handbag +31 tie +32 suitcase +33 frisbee +34 skis +35 snowboard +36 sports ball +37 kite +38 baseball bat +39 baseball glove +40 skateboard +41 surfboard +42 tennis racket +43 bottle +44 plate +45 wine glass +46 cup +47 fork +48 knife +49 spoon +50 bowl +51 banana +52 apple +53 sandwich +54 orange +55 broccoli +56 carrot +57 hot dog +58 pizza +59 donut +60 cake +61 chair +62 couch +63 potted plant +64 bed +65 mirror +66 dining table +67 window +68 desk +69 toilet +70 door +71 tv +72 laptop +73 mouse +74 remote +75 keyboard +76 cell phone +77 microwave +78 oven +79 toaster +80 sink +81 refrigerator +82 blender +83 book +84 clock +85 vase +86 scissors +87 teddy bear +88 hair drier +89 toothbrush +90 hair brush \ No newline at end of file diff --git a/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py b/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py new file mode 100644 index 0000000..9f4d08f --- /dev/null +++ b/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py @@ -0,0 +1,28 @@ +import json +import sys +from typing import Any + +from ruamel.yaml import YAML + +sys.path.insert(0, "/opt/frigate") +from frigate.util.config import find_config_file, resolve_ffmpeg_path + +sys.path.remove("/opt/frigate") + +yaml = YAML() + +config_file = find_config_file() + +try: + with open(config_file) as f: + raw_config = f.read() + + if config_file.endswith((".yaml", ".yml")): + config: dict[str, Any] = yaml.load(raw_config) + elif config_file.endswith(".json"): + config: dict[str, Any] = json.loads(raw_config) +except FileNotFoundError: + config: dict[str, Any] = {} + +path = config.get("ffmpeg", {}).get("path", "default") +print(resolve_ffmpeg_path(path, "ffmpeg")) diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py new file mode 100644 index 0000000..70cb744 --- /dev/null +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -0,0 +1,187 @@ +"""Creates a go2rtc config file.""" + +import json +import os +import sys +from pathlib import Path +from typing import Any + +from ruamel.yaml import YAML + +sys.path.insert(0, "/opt/frigate") +from frigate.config.env import substitute_frigate_vars +from frigate.const import ( + BIRDSEYE_PIPE, + LIBAVFORMAT_VERSION_MAJOR, +) +from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode +from frigate.util.config import find_config_file, resolve_ffmpeg_path +from frigate.util.services import ( + is_go2rtc_arbitrary_exec_allowed, + is_restricted_go2rtc_source, +) + +sys.path.remove("/opt/frigate") + +yaml = YAML() + +FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")} +# read docker secret files as env vars too +if os.path.isdir("/run/secrets"): + for secret_file in os.listdir("/run/secrets"): + if secret_file.startswith("FRIGATE_"): + FRIGATE_ENV_VARS[secret_file] = ( + Path(os.path.join("/run/secrets", secret_file)).read_text().strip() + ) + +config_file = find_config_file() + +try: + with open(config_file) as f: + raw_config = f.read() + + if config_file.endswith((".yaml", ".yml")): + config: dict[str, Any] = yaml.load(raw_config) + elif config_file.endswith(".json"): + config: dict[str, Any] = json.loads(raw_config) +except FileNotFoundError: + config: dict[str, Any] = {} + +go2rtc_config: dict[str, Any] = config.get("go2rtc", {}) + +# Need to enable CORS for go2rtc so the frigate integration / card work automatically +if go2rtc_config.get("api") is None: + go2rtc_config["api"] = {"origin": "*"} +elif go2rtc_config["api"].get("origin") is None: + go2rtc_config["api"]["origin"] = "*" + +# Need to set default location for HA config +if go2rtc_config.get("hass") is None: + go2rtc_config["hass"] = {"config": "/homeassistant"} + +# we want to ensure that logs are easy to read +if go2rtc_config.get("log") is None: + go2rtc_config["log"] = {"format": "text"} +elif go2rtc_config["log"].get("format") is None: + go2rtc_config["log"]["format"] = "text" + +# ensure there is a default webrtc config +if go2rtc_config.get("webrtc") is None: + go2rtc_config["webrtc"] = {} + +if go2rtc_config["webrtc"].get("candidates") is None: + default_candidates = [] + # use internal candidate if it was discovered when running through the add-on + internal_candidate = os.environ.get("FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL") + if internal_candidate is not None: + default_candidates.append(internal_candidate) + # should set default stun server so webrtc can work + default_candidates.append("stun:8555") + + go2rtc_config["webrtc"]["candidates"] = default_candidates + +if go2rtc_config.get("rtsp", {}).get("username") is not None: + go2rtc_config["rtsp"]["username"] = substitute_frigate_vars( + go2rtc_config["rtsp"]["username"] + ) + +if go2rtc_config.get("rtsp", {}).get("password") is not None: + go2rtc_config["rtsp"]["password"] = substitute_frigate_vars( + go2rtc_config["rtsp"]["password"] + ) + +# ensure ffmpeg path is set correctly +path = config.get("ffmpeg", {}).get("path", "default") +ffmpeg_path = resolve_ffmpeg_path(path, "ffmpeg") + +if go2rtc_config.get("ffmpeg") is None: + go2rtc_config["ffmpeg"] = {"bin": ffmpeg_path} +elif go2rtc_config["ffmpeg"].get("bin") is None: + go2rtc_config["ffmpeg"]["bin"] = ffmpeg_path + +# need to replace ffmpeg command when using ffmpeg4 +if LIBAVFORMAT_VERSION_MAJOR < 59: + rtsp_args = "-fflags nobuffer -flags low_delay -stimeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}" + if go2rtc_config.get("ffmpeg") is None: + go2rtc_config["ffmpeg"] = {"rtsp": rtsp_args} + elif go2rtc_config["ffmpeg"].get("rtsp") is None: + go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args + + +for name in list(go2rtc_config.get("streams", {})): + stream = go2rtc_config["streams"][name] + + if isinstance(stream, str): + try: + formatted_stream = stream.format(**FRIGATE_ENV_VARS) + if is_restricted_go2rtc_source(formatted_stream): + print( + f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. " + f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources." + ) + del go2rtc_config["streams"][name] + continue + go2rtc_config["streams"][name] = formatted_stream + except KeyError as e: + print( + "[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info." + ) + sys.exit(e) + + elif isinstance(stream, list): + filtered_streams = [] + for i, stream_item in enumerate(stream): + try: + formatted_stream = stream_item.format(**FRIGATE_ENV_VARS) + if is_restricted_go2rtc_source(formatted_stream): + print( + f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. " + f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources." + ) + continue + + filtered_streams.append(formatted_stream) + except KeyError as e: + print( + "[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info." + ) + sys.exit(e) + + if filtered_streams: + go2rtc_config["streams"][name] = filtered_streams + else: + print( + f"[ERROR] Stream '{name}' was removed because all sources were restricted (echo/expr/exec). " + f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources." + ) + del go2rtc_config["streams"][name] + + elif isinstance(stream, dict): + # The map form ({"url": ...}) lets go2rtc resolve the source + # recursively, so it is effectively a dynamic way to generate the URL + # for a stream. That can only be backed by an exec source, so it cannot + # be allowed unless arbitrary exec is explicitly enabled. When it is + # enabled, leave the map untouched for go2rtc to resolve. + if not is_go2rtc_arbitrary_exec_allowed(): + print( + f"[ERROR] Stream '{name}' uses a dynamic source format which is disabled by default for security. " + f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources." + ) + del go2rtc_config["streams"][name] + continue + +# add birdseye restream stream if enabled +if config.get("birdseye", {}).get("restream", False): + birdseye: dict[str, Any] = config.get("birdseye") + + input = f"-f rawvideo -pix_fmt yuv420p -video_size {birdseye.get('width', 1280)}x{birdseye.get('height', 720)} -r 10 -i {BIRDSEYE_PIPE}" + ffmpeg_cmd = f"exec:{parse_preset_hardware_acceleration_encode(ffmpeg_path, config.get('ffmpeg', {}).get('hwaccel_args', ''), input, '-rtsp_transport tcp -f rtsp {output}')}" + + if go2rtc_config.get("streams"): + go2rtc_config["streams"]["birdseye"] = ffmpeg_cmd + else: + go2rtc_config["streams"] = {"birdseye": ffmpeg_cmd} + +# Write go2rtc_config to /dev/shm/go2rtc.yaml +with open("/dev/shm/go2rtc.yaml", "w") as f: + yaml.dump(go2rtc_config, f) diff --git a/docker/main/rootfs/usr/local/nginx/conf/auth_location.conf b/docker/main/rootfs/usr/local/nginx/conf/auth_location.conf new file mode 100644 index 0000000..285a3d8 --- /dev/null +++ b/docker/main/rootfs/usr/local/nginx/conf/auth_location.conf @@ -0,0 +1,43 @@ +set $upstream_auth http://127.0.0.1:5001/auth; + +## Virtual endpoint created by nginx to forward auth requests. +location /auth { + ## Essential Proxy Configuration + internal; + proxy_pass $upstream_auth; + + ## Headers + + # First strip out all the request headers + # Note: This is important to ensure that upgrade requests for secure + # websockets dont cause the backend to fail + proxy_pass_request_headers off; + # Pass info about the request + proxy_set_header X-Original-Method $request_method; + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header X-Server-Port $server_port; + proxy_set_header Content-Length ""; + # Pass along auth related info + proxy_set_header Authorization $http_authorization; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-CSRF-TOKEN "1"; + + # include headers from common auth proxies + include proxy_trusted_headers.conf; + + ## Basic Proxy Configuration + proxy_pass_request_body off; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; # Timeout if the real server is dead + proxy_redirect http:// $scheme://; + proxy_http_version 1.1; + proxy_cache_bypass $cookie_session; + proxy_no_cache $cookie_session; + proxy_buffers 4 32k; + client_body_buffer_size 128k; + + ## Advanced Proxy Configuration + send_timeout 5m; + proxy_read_timeout 240; + proxy_send_timeout 240; + proxy_connect_timeout 240; +} \ No newline at end of file diff --git a/docker/main/rootfs/usr/local/nginx/conf/auth_request.conf b/docker/main/rootfs/usr/local/nginx/conf/auth_request.conf new file mode 100644 index 0000000..9e745b6 --- /dev/null +++ b/docker/main/rootfs/usr/local/nginx/conf/auth_request.conf @@ -0,0 +1,24 @@ +## Send a subrequest to verify if the user is authenticated and has permission to access the resource. +auth_request /auth; + +## Save the upstream metadata response headers from the auth request to variables +auth_request_set $user $upstream_http_remote_user; +auth_request_set $role $upstream_http_remote_role; +auth_request_set $groups $upstream_http_remote_groups; +auth_request_set $name $upstream_http_remote_name; +auth_request_set $email $upstream_http_remote_email; + +## Inject the metadata response headers from the variables into the request made to the backend. +proxy_set_header Remote-User $user; +proxy_set_header Remote-Role $role; +proxy_set_header Remote-Groups $groups; +proxy_set_header Remote-Email $email; +proxy_set_header Remote-Name $name; + +## Refresh the cookie as needed +auth_request_set $auth_cookie $upstream_http_set_cookie; +add_header Set-Cookie $auth_cookie; + +## Pass the location header back up if it exists +auth_request_set $redirection_url $upstream_http_location; +add_header Location $redirection_url; diff --git a/docker/main/rootfs/usr/local/nginx/conf/go2rtc_upstream.conf b/docker/main/rootfs/usr/local/nginx/conf/go2rtc_upstream.conf new file mode 100644 index 0000000..811bb94 --- /dev/null +++ b/docker/main/rootfs/usr/local/nginx/conf/go2rtc_upstream.conf @@ -0,0 +1,4 @@ +upstream go2rtc { + server 127.0.0.1:1984; + keepalive 1024; +} \ No newline at end of file diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf new file mode 100644 index 0000000..d0b18ff --- /dev/null +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -0,0 +1,356 @@ +daemon off; +user root; +worker_processes auto; + +error_log /dev/stdout warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + map_hash_bucket_size 256; + + include mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'request_time="$request_time" upstream_response_time="$upstream_response_time"'; + + + access_log /dev/stdout main; + + # send headers in one piece, it is better than sending them one by one + tcp_nopush on; + + sendfile on; + + keepalive_timeout 65; + + gzip on; + gzip_comp_level 6; + gzip_types text/plain text/css application/json application/x-javascript application/javascript text/javascript image/svg+xml image/x-icon image/bmp; + gzip_proxied no-cache no-store private expired auth; + gzip_vary on; + + proxy_cache_path /dev/shm/nginx_cache levels=1:2 keys_zone=api_cache:10m max_size=10m inactive=1m use_temp_path=off; + + map $sent_http_content_type $should_not_cache { + 'application/json' 0; + default 1; + } + + upstream frigate_api { + server 127.0.0.1:5001; + keepalive 1024; + } + + upstream mqtt_ws { + server 127.0.0.1:5002; + keepalive 1024; + } + + upstream jsmpeg { + server 127.0.0.1:8082; + keepalive 1024; + } + + include go2rtc_upstream.conf; + + server { + include listen.conf; + + # enable HTTP/2 for TLS connections to eliminate browser 6-connection limit + http2 on; + + # vod settings + vod_base_url ''; + vod_segments_base_url ''; + vod_mode mapped; + vod_max_mapping_response_size 1m; + vod_upstream_location /api; + vod_align_segments_to_key_frames on; + vod_manifest_segment_durations_mode accurate; + vod_ignore_edit_list on; + vod_segment_duration 10000; + + # MPEG-TS settings (not used when fMP4 is enabled, kept for reference) + vod_hls_mpegts_align_frames off; + vod_hls_mpegts_interleave_frames on; + + # file handle caching / aio + open_file_cache max=1000 inactive=5m; + open_file_cache_valid 2m; + open_file_cache_min_uses 1; + open_file_cache_errors on; + aio on; + + # file upload size + client_max_body_size 20M; + + # https://github.com/kaltura/nginx-vod-module#vod_open_file_thread_pool + vod_open_file_thread_pool default; + + # vod caches + vod_metadata_cache metadata_cache 512m; + vod_mapping_cache mapping_cache 5m 10m; + + # gzip manifests + gzip on; + gzip_types application/vnd.apple.mpegurl; + + include auth_location.conf; + include base_path.conf; + + location /vod/ { + include auth_request.conf; + aio threads; + vod hls; + + # Use fMP4 (fragmented MP4) instead of MPEG-TS for better performance + # Smaller segments, faster generation, better browser compatibility + vod_hls_container_format fmp4; + + secure_token $args; + secure_token_types application/vnd.apple.mpegurl; + + add_header Cache-Control "no-store"; + expires off; + + keepalive_disable safari; + + # vod module returns 502 for non-existent media + # https://github.com/kaltura/nginx-vod-module/issues/468 + error_page 502 =404 /vod-not-found; + } + + location = /vod-not-found { + return 404; + } + + location /stream/ { + include auth_request.conf; + add_header Cache-Control "no-store"; + expires off; + + types { + application/dash+xml mpd; + application/vnd.apple.mpegurl m3u8; + video/mp2t ts; + image/jpeg jpg; + } + + root /tmp; + } + + location /clips/ { + include auth_request.conf; + types { + video/mp4 mp4; + image/jpeg jpg; + } + + expires 7d; + add_header Cache-Control "public"; + autoindex on; + root /media/frigate; + } + + location /cache/ { + internal; # This tells nginx it's not accessible from the outside + alias /tmp/cache/; + } + + location /recordings/ { + include auth_request.conf; + types { + video/mp4 mp4; + } + + autoindex on; + autoindex_format json; + root /media/frigate; + } + + location /exports/ { + include auth_request.conf; + types { + video/mp4 mp4; + } + + autoindex on; + autoindex_format json; + root /media/frigate; + } + + location /ws { + include auth_request.conf; + proxy_pass http://mqtt_ws/; + include proxy.conf; + } + + location /live/jsmpeg/ { + include auth_request.conf; + proxy_pass http://jsmpeg/; + include proxy.conf; + } + + # frigate lovelace card uses this path + location /live/mse/api/ws { + include auth_request.conf; + limit_except GET { + deny all; + } + proxy_pass http://go2rtc/api/ws; + include proxy.conf; + } + + location /live/webrtc/api/ws { + include auth_request.conf; + limit_except GET { + deny all; + } + proxy_pass http://go2rtc/api/ws; + include proxy.conf; + } + + # pass through go2rtc player + location /live/webrtc/webrtc.html { + include auth_request.conf; + limit_except GET { + deny all; + } + proxy_pass http://go2rtc/webrtc.html; + include proxy.conf; + } + + # integration uses this to add webrtc candidate + location /api/go2rtc/webrtc { + include auth_request.conf; + limit_except POST { + deny all; + } + proxy_pass http://go2rtc/api/webrtc; + include proxy.conf; + } + + location ~* /api/.*\.(jpg|jpeg|png|webp|gif)$ { + include auth_request.conf; + rewrite ^/api/(.*)$ /$1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + + location /api/ { + include auth_request.conf; + add_header Cache-Control "no-store"; + expires off; + proxy_pass http://frigate_api/; + include proxy.conf; + + proxy_cache api_cache; + proxy_cache_key "$scheme$proxy_host$request_uri|$role|$groups|$user"; + proxy_cache_lock on; + proxy_cache_use_stale updating; + proxy_cache_valid 200 5s; + proxy_cache_bypass $http_x_cache_bypass; + proxy_no_cache $should_not_cache; + add_header X-Cache-Status $upstream_cache_status; + + location /api/vod/ { + include auth_request.conf; + proxy_pass http://frigate_api/vod/; + include proxy.conf; + proxy_cache off; + } + + location /api/login { + auth_request off; + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + + # Allow unauthenticated access to the first_time_login endpoint + # so the login page can load help text before authentication. + location /api/auth/first_time_login { + auth_request off; + limit_except GET { + deny all; + } + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + + location /api/stats { + include auth_request.conf; + access_log off; + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + + location /api/version { + include auth_request.conf; + access_log off; + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + } + + location / { + # do not require auth for static assets + add_header Cache-Control "no-store"; + expires off; + + location /assets/ { + access_log off; + expires 1y; + add_header Cache-Control "public"; + } + + location /fonts/ { + access_log off; + expires 1y; + add_header Cache-Control "public"; + } + + location /locales/ { + access_log off; + add_header Cache-Control "public"; + } + + location ~ ^/.*-([A-Za-z0-9]+)\.webmanifest$ { + access_log off; + expires 1y; + add_header Cache-Control "public"; + default_type application/json; + proxy_set_header Accept-Encoding ""; + sub_filter_once off; + sub_filter_types application/json; + sub_filter '"start_url": "/BASE_PATH/"' '"start_url" : "$http_x_ingress_path/"'; + sub_filter '"src": "/BASE_PATH/' '"src": "$http_x_ingress_path/'; + } + + sub_filter 'href="/BASE_PATH/' 'href="$http_x_ingress_path/'; + sub_filter 'url(/BASE_PATH/' 'url($http_x_ingress_path/'; + sub_filter '"/BASE_PATH/dist/' '"$http_x_ingress_path/dist/'; + sub_filter '"/BASE_PATH/js/' '"$http_x_ingress_path/js/'; + sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/'; + sub_filter '"/BASE_PATH/locales/' '"$http_x_ingress_path/locales/'; + sub_filter '"/BASE_PATH/monacoeditorwork/' '"$http_x_ingress_path/assets/'; + sub_filter 'return"/BASE_PATH/"' 'return window.baseUrl'; + sub_filter '' ''; + sub_filter_types text/css application/javascript; + sub_filter_once off; + + root /opt/frigate/web; + try_files $uri $uri.html $uri/ /index.html; + } + } +} diff --git a/docker/main/rootfs/usr/local/nginx/conf/proxy.conf b/docker/main/rootfs/usr/local/nginx/conf/proxy.conf new file mode 100644 index 0000000..a3aacc3 --- /dev/null +++ b/docker/main/rootfs/usr/local/nginx/conf/proxy.conf @@ -0,0 +1,26 @@ +## Headers +proxy_set_header Host $host; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "Upgrade"; +proxy_set_header X-Original-URL $scheme://$http_host$request_uri; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $http_host; +proxy_set_header X-Forwarded-URI $request_uri; +proxy_set_header X-Forwarded-Ssl on; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Real-IP $remote_addr; + +## Basic Proxy Configuration +client_body_buffer_size 128k; +proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; ## Timeout if the real server is dead. +proxy_redirect http:// $scheme://; +proxy_http_version 1.1; +proxy_cache_bypass $cookie_session; +proxy_no_cache $cookie_session; +proxy_buffers 64 256k; + +## Advanced Proxy Configuration +send_timeout 5m; +proxy_read_timeout 360; +proxy_send_timeout 360; +proxy_connect_timeout 360; \ No newline at end of file diff --git a/docker/main/rootfs/usr/local/nginx/conf/proxy_trusted_headers.conf b/docker/main/rootfs/usr/local/nginx/conf/proxy_trusted_headers.conf new file mode 100644 index 0000000..c945810 --- /dev/null +++ b/docker/main/rootfs/usr/local/nginx/conf/proxy_trusted_headers.conf @@ -0,0 +1,29 @@ +# Header used to validate reverse proxy trust +proxy_set_header X-Proxy-Secret $http_x_proxy_secret; + +# these headers will be copied to the /auth request and are available +# to be mapped in the config to Frigate's remote-user header + +# List of headers sent by common authentication proxies: +# - Authelia +# - Traefik forward auth +# - oauth2_proxy +# - Authentik + +proxy_set_header Remote-User $http_remote_user; +proxy_set_header Remote-Groups $http_remote_groups; +proxy_set_header Remote-Email $http_remote_email; +proxy_set_header Remote-Name $http_remote_name; +proxy_set_header X-Forwarded-User $http_x_forwarded_user; +proxy_set_header X-Forwarded-Groups $http_x_forwarded_groups; +proxy_set_header X-Forwarded-Email $http_x_forwarded_email; +proxy_set_header X-Forwarded-Preferred-Username $http_x_forwarded_preferred_username; +proxy_set_header X-Auth-Request-User $http_x_auth_request_user; +proxy_set_header X-Auth-Request-Groups $http_x_auth_request_groups; +proxy_set_header X-Auth-Request-Email $http_x_auth_request_email; +proxy_set_header X-Auth-Request-Preferred-Username $http_x_auth_request_preferred_username; +proxy_set_header X-authentik-username $http_x_authentik_username; +proxy_set_header X-authentik-groups $http_x_authentik_groups; +proxy_set_header X-authentik-email $http_x_authentik_email; +proxy_set_header X-authentik-name $http_x_authentik_name; +proxy_set_header X-authentik-uid $http_x_authentik_uid; diff --git a/docker/main/rootfs/usr/local/nginx/get_nginx_settings.py b/docker/main/rootfs/usr/local/nginx/get_nginx_settings.py new file mode 100644 index 0000000..79cda36 --- /dev/null +++ b/docker/main/rootfs/usr/local/nginx/get_nginx_settings.py @@ -0,0 +1,62 @@ +"""Prints the nginx settings as json to stdout.""" + +import json +import os +import sys +from typing import Any + +from ruamel.yaml import YAML + +sys.path.insert(0, "/opt/frigate") +from frigate.util.config import find_config_file + +sys.path.remove("/opt/frigate") + +yaml = YAML() + +config_file = find_config_file() + +try: + with open(config_file) as f: + raw_config = f.read() + + if config_file.endswith((".yaml", ".yml")): + config: dict[str, Any] = yaml.load(raw_config) + elif config_file.endswith(".json"): + config: dict[str, Any] = json.loads(raw_config) +except FileNotFoundError: + config: dict[str, Any] = {} + +tls_config: dict[str, Any] = config.get("tls", {}) +tls_config.setdefault("enabled", True) + +networking_config: dict[str, Any] = config.get("networking", {}) +ipv6_config: dict[str, Any] = networking_config.get("ipv6", {}) +ipv6_config.setdefault("enabled", False) + +listen_config: dict[str, Any] = networking_config.get("listen", {}) +listen_config.setdefault("internal", 5000) +listen_config.setdefault("external", 8971) + +# handle case where internal port is a string with ip:port +internal_port = listen_config["internal"] +if type(internal_port) is str: + internal_port = int(internal_port.split(":")[-1]) +listen_config["internal_port"] = internal_port + +# handle case where external port is a string with ip:port +external_port = listen_config["external"] +if type(external_port) is str: + external_port = int(external_port.split(":")[-1]) +listen_config["external_port"] = external_port + +base_path = os.environ.get("FRIGATE_BASE_PATH", "") + +result: dict[str, Any] = { + "tls": tls_config, + "ipv6": ipv6_config, + "listen": listen_config, + "base_path": base_path, +} + +print(json.dumps(result)) diff --git a/docker/main/rootfs/usr/local/nginx/templates/base_path.gotmpl b/docker/main/rootfs/usr/local/nginx/templates/base_path.gotmpl new file mode 100644 index 0000000..ca945ba --- /dev/null +++ b/docker/main/rootfs/usr/local/nginx/templates/base_path.gotmpl @@ -0,0 +1,19 @@ +{{ if .base_path }} +location = {{ .base_path }} { + return 302 {{ .base_path }}/; +} + +location ^~ {{ .base_path }}/ { + # remove base_url from the path before passing upstream + rewrite ^{{ .base_path }}/(.*) /$1 break; + + proxy_pass $scheme://127.0.0.1:{{ .listen.external_port }}; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Ingress-Path {{ .base_path }}; + + access_log off; +} +{{ end }} diff --git a/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl b/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl new file mode 100644 index 0000000..628784b --- /dev/null +++ b/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl @@ -0,0 +1,36 @@ +# Internal (IPv4 always; IPv6 optional) +listen {{ .listen.internal }}; +{{ if .ipv6.enabled }}listen [::]:{{ .listen.internal_port }};{{ end }} + +# intended for external traffic, protected by auth +{{ if .tls.enabled }} + # external HTTPS (IPv4 always; IPv6 optional) + listen {{ .listen.external }} ssl; + {{ if .ipv6.enabled }}listen [::]:{{ .listen.external_port }} ssl;{{ end }} + + ssl_certificate /etc/letsencrypt/live/frigate/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/frigate/privkey.pem; + + # generated 2024-06-01, Mozilla Guideline v5.7, nginx 1.25.3, OpenSSL 1.1.1w, modern configuration, no OCSP + # https://ssl-config.mozilla.org/#server=nginx&version=1.25.3&config=modern&openssl=1.1.1w&ocsp=false&guideline=5.7 + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + + # modern configuration + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + # HSTS (ngx_http_headers_module is required) (63072000 seconds) + add_header Strict-Transport-Security "max-age=63072000" always; + + # ACME challenge location + location /.well-known/acme-challenge/ { + default_type "text/plain"; + root /etc/letsencrypt/www; + } +{{ else }} + # (No tls) default to HTTP (IPv4 always; IPv6 optional) + listen {{ .listen.external }}; + {{ if .ipv6.enabled }}listen [::]:{{ .listen.external_port }};{{ end }} +{{ end }} diff --git a/docker/memryx/user_installation.sh b/docker/memryx/user_installation.sh new file mode 100644 index 0000000..b92b7e3 --- /dev/null +++ b/docker/memryx/user_installation.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -e # Exit immediately if any command fails +set -o pipefail + +echo "Starting MemryX driver and runtime installation..." + +# Detect architecture +arch=$(uname -m) + +# Purge existing packages and repo +echo "Removing old MemryX installations..." +# Remove any holds on MemryX packages (if they exist) +sudo apt-mark unhold memx-* mxa-manager || true +sudo apt purge -y memx-* mxa-manager || true +sudo rm -f /etc/apt/sources.list.d/memryx.list /etc/apt/trusted.gpg.d/memryx.asc + +# Install kernel headers +echo "Installing kernel headers for: $(uname -r)" +sudo apt update +sudo apt install -y dkms linux-headers-$(uname -r) + +# Add MemryX key and repo +echo "Adding MemryX GPG key and repository..." +wget -qO- https://developer.memryx.com/deb/memryx.asc | sudo tee /etc/apt/trusted.gpg.d/memryx.asc >/dev/null +echo 'deb https://developer.memryx.com/deb stable main' | sudo tee /etc/apt/sources.list.d/memryx.list >/dev/null + +# Update and install specific SDK 2.1 packages +echo "Installing MemryX SDK 2.1 packages..." +sudo apt update +sudo apt install -y memx-drivers=2.1.* memx-accl=2.1.* mxa-manager=2.1.* + +# Hold packages to prevent automatic upgrades +sudo apt-mark hold memx-drivers memx-accl mxa-manager + +# ARM-specific board setup +if [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then + echo "Running ARM board setup..." + sudo mx_arm_setup +fi + +echo -e "\n\n\033[1;31mYOU MUST RESTART YOUR COMPUTER NOW\033[0m\n\n" + +echo "MemryX SDK 2.1 installation complete!" + diff --git a/docker/rockchip/COCO/coco_subset_20.txt b/docker/rockchip/COCO/coco_subset_20.txt new file mode 100644 index 0000000..aa372fe --- /dev/null +++ b/docker/rockchip/COCO/coco_subset_20.txt @@ -0,0 +1,20 @@ +./subset/000000005001.jpg +./subset/000000038829.jpg +./subset/000000052891.jpg +./subset/000000075612.jpg +./subset/000000098261.jpg +./subset/000000181542.jpg +./subset/000000215245.jpg +./subset/000000277005.jpg +./subset/000000288685.jpg +./subset/000000301421.jpg +./subset/000000334371.jpg +./subset/000000348481.jpg +./subset/000000373353.jpg +./subset/000000397681.jpg +./subset/000000414673.jpg +./subset/000000419312.jpg +./subset/000000465822.jpg +./subset/000000475732.jpg +./subset/000000559707.jpg +./subset/000000574315.jpg \ No newline at end of file diff --git a/docker/rockchip/COCO/subset/000000005001.jpg b/docker/rockchip/COCO/subset/000000005001.jpg new file mode 100644 index 0000000..a7d4437 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000005001.jpg differ diff --git a/docker/rockchip/COCO/subset/000000038829.jpg b/docker/rockchip/COCO/subset/000000038829.jpg new file mode 100644 index 0000000..f275500 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000038829.jpg differ diff --git a/docker/rockchip/COCO/subset/000000052891.jpg b/docker/rockchip/COCO/subset/000000052891.jpg new file mode 100644 index 0000000..57344ef Binary files /dev/null and b/docker/rockchip/COCO/subset/000000052891.jpg differ diff --git a/docker/rockchip/COCO/subset/000000075612.jpg b/docker/rockchip/COCO/subset/000000075612.jpg new file mode 100644 index 0000000..16555e4 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000075612.jpg differ diff --git a/docker/rockchip/COCO/subset/000000098261.jpg b/docker/rockchip/COCO/subset/000000098261.jpg new file mode 100644 index 0000000..57412f7 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000098261.jpg differ diff --git a/docker/rockchip/COCO/subset/000000181542.jpg b/docker/rockchip/COCO/subset/000000181542.jpg new file mode 100644 index 0000000..e3676d3 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000181542.jpg differ diff --git a/docker/rockchip/COCO/subset/000000215245.jpg b/docker/rockchip/COCO/subset/000000215245.jpg new file mode 100644 index 0000000..624e4f1 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000215245.jpg differ diff --git a/docker/rockchip/COCO/subset/000000277005.jpg b/docker/rockchip/COCO/subset/000000277005.jpg new file mode 100644 index 0000000..629cb6e Binary files /dev/null and b/docker/rockchip/COCO/subset/000000277005.jpg differ diff --git a/docker/rockchip/COCO/subset/000000288685.jpg b/docker/rockchip/COCO/subset/000000288685.jpg new file mode 100644 index 0000000..4dc759d Binary files /dev/null and b/docker/rockchip/COCO/subset/000000288685.jpg differ diff --git a/docker/rockchip/COCO/subset/000000301421.jpg b/docker/rockchip/COCO/subset/000000301421.jpg new file mode 100644 index 0000000..2cbfa4e Binary files /dev/null and b/docker/rockchip/COCO/subset/000000301421.jpg differ diff --git a/docker/rockchip/COCO/subset/000000334371.jpg b/docker/rockchip/COCO/subset/000000334371.jpg new file mode 100644 index 0000000..b47ac6d Binary files /dev/null and b/docker/rockchip/COCO/subset/000000334371.jpg differ diff --git a/docker/rockchip/COCO/subset/000000348481.jpg b/docker/rockchip/COCO/subset/000000348481.jpg new file mode 100644 index 0000000..a2cb75c Binary files /dev/null and b/docker/rockchip/COCO/subset/000000348481.jpg differ diff --git a/docker/rockchip/COCO/subset/000000373353.jpg b/docker/rockchip/COCO/subset/000000373353.jpg new file mode 100644 index 0000000..c092511 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000373353.jpg differ diff --git a/docker/rockchip/COCO/subset/000000397681.jpg b/docker/rockchip/COCO/subset/000000397681.jpg new file mode 100644 index 0000000..5b94259 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000397681.jpg differ diff --git a/docker/rockchip/COCO/subset/000000414673.jpg b/docker/rockchip/COCO/subset/000000414673.jpg new file mode 100644 index 0000000..587c370 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000414673.jpg differ diff --git a/docker/rockchip/COCO/subset/000000419312.jpg b/docker/rockchip/COCO/subset/000000419312.jpg new file mode 100644 index 0000000..274ea87 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000419312.jpg differ diff --git a/docker/rockchip/COCO/subset/000000465822.jpg b/docker/rockchip/COCO/subset/000000465822.jpg new file mode 100644 index 0000000..3510d11 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000465822.jpg differ diff --git a/docker/rockchip/COCO/subset/000000475732.jpg b/docker/rockchip/COCO/subset/000000475732.jpg new file mode 100644 index 0000000..51d9685 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000475732.jpg differ diff --git a/docker/rockchip/COCO/subset/000000559707.jpg b/docker/rockchip/COCO/subset/000000559707.jpg new file mode 100644 index 0000000..4811ef1 Binary files /dev/null and b/docker/rockchip/COCO/subset/000000559707.jpg differ diff --git a/docker/rockchip/COCO/subset/000000574315.jpg b/docker/rockchip/COCO/subset/000000574315.jpg new file mode 100644 index 0000000..ad06b6d Binary files /dev/null and b/docker/rockchip/COCO/subset/000000574315.jpg differ diff --git a/docker/rockchip/Dockerfile b/docker/rockchip/Dockerfile new file mode 100644 index 0000000..70309f0 --- /dev/null +++ b/docker/rockchip/Dockerfile @@ -0,0 +1,38 @@ +# syntax=docker/dockerfile:1.6 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +# Globally set pip break-system-packages option to avoid having to specify it every time +ARG PIP_BREAK_SYSTEM_PACKAGES=1 + +FROM wheels as rk-wheels +COPY docker/main/requirements-wheels.txt /requirements-wheels.txt +COPY docker/rockchip/requirements-wheels-rk.txt /requirements-wheels-rk.txt +RUN sed -i "/https:\/\//d" /requirements-wheels.txt +RUN sed -i "/onnxruntime/d" /requirements-wheels.txt +RUN sed -i '/\[.*\]/d' /requirements-wheels.txt \ + && pip3 wheel --wheel-dir=/rk-wheels -c /requirements-wheels.txt -r /requirements-wheels-rk.txt +RUN rm -rf /rk-wheels/opencv_python-* +RUN rm -rf /rk-wheels/torch-* + +FROM deps AS rk-frigate +ARG TARGETARCH +ARG PIP_BREAK_SYSTEM_PACKAGES + +RUN --mount=type=bind,from=rk-wheels,source=/rk-wheels,target=/deps/rk-wheels \ + pip3 install --no-deps -U /deps/rk-wheels/*.whl + +WORKDIR /opt/frigate/ +COPY --from=rootfs / / +COPY docker/rockchip/COCO /COCO +COPY docker/rockchip/conv2rknn.py /opt/conv2rknn.py + +ADD https://github.com/MarcA711/rknn-toolkit2/releases/download/v2.3.2/librknnrt.so /usr/lib/ + +ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-11/ffmpeg /usr/lib/ffmpeg/6.0/bin/ +ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-11/ffprobe /usr/lib/ffmpeg/6.0/bin/ +ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/7.1-1/ffmpeg /usr/lib/ffmpeg/7.0/bin/ +ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/7.1-1/ffprobe /usr/lib/ffmpeg/7.0/bin/ +ENV DEFAULT_FFMPEG_VERSION="6.0" +ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:${INCLUDED_FFMPEG_VERSIONS}" diff --git a/docker/rockchip/conv2rknn.py b/docker/rockchip/conv2rknn.py new file mode 100644 index 0000000..700f356 --- /dev/null +++ b/docker/rockchip/conv2rknn.py @@ -0,0 +1,82 @@ +import os + +import rknn +import yaml +from rknn.api import RKNN + +try: + with open(rknn.__path__[0] + "/VERSION") as file: + tk_version = file.read().strip() +except FileNotFoundError: + pass + +try: + with open("/config/conv2rknn.yaml") as config_file: + configuration = yaml.safe_load(config_file) +except FileNotFoundError: + raise Exception("Please place a config file at /config/conv2rknn.yaml") from None + +if configuration["config"] != None: + rknn_config = configuration["config"] +else: + rknn_config = {} + +if not os.path.isdir("/config/model_cache/rknn_cache/onnx"): + raise Exception( + "Place the onnx models you want to convert to rknn format in /config/model_cache/rknn_cache/onnx" + ) + +if "soc" not in configuration: + try: + with open("/proc/device-tree/compatible") as file: + soc = file.read().split(",")[-1].strip("\x00") + except FileNotFoundError: + raise Exception("Make sure to run docker in privileged mode.") from None + + configuration["soc"] = [ + soc, + ] + +if "quantization" not in configuration: + configuration["quantization"] = False + +if "output_name" not in configuration: + configuration["output_name"] = "{{input_basename}}" + +for input_filename in os.listdir("/config/model_cache/rknn_cache/onnx"): + for soc in configuration["soc"]: + quant = "i8" if configuration["quantization"] else "fp16" + + input_path = "/config/model_cache/rknn_cache/onnx/" + input_filename + input_basename = input_filename[: input_filename.rfind(".")] + + output_filename = ( + configuration["output_name"].format( + quant=quant, + input_basename=input_basename, + soc=soc, + tk_version=tk_version, + ) + + ".rknn" + ) + output_path = "/config/model_cache/rknn_cache/" + output_filename + + rknn_config["target_platform"] = soc + + rknn = RKNN(verbose=True) + rknn.config(**rknn_config) + + if rknn.load_onnx(model=input_path) != 0: + raise Exception("Error loading model.") + + if ( + rknn.build( + do_quantization=configuration["quantization"], + dataset="/COCO/coco_subset_20.txt", + ) + != 0 + ): + raise Exception("Error building model.") + + if rknn.export_rknn(output_path) != 0: + raise Exception("Error exporting rknn model.") diff --git a/docker/rockchip/requirements-wheels-rk.txt b/docker/rockchip/requirements-wheels-rk.txt new file mode 100644 index 0000000..f841f26 --- /dev/null +++ b/docker/rockchip/requirements-wheels-rk.txt @@ -0,0 +1,2 @@ +rknn-toolkit2 == 2.3.2 +rknn-toolkit-lite2 == 2.3.2 \ No newline at end of file diff --git a/docker/rockchip/rk.hcl b/docker/rockchip/rk.hcl new file mode 100644 index 0000000..9424b46 --- /dev/null +++ b/docker/rockchip/rk.hcl @@ -0,0 +1,27 @@ +target wheels { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "wheels" +} + +target deps { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "deps" +} + +target rootfs { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "rootfs" +} + +target rk { + dockerfile = "docker/rockchip/Dockerfile" + contexts = { + wheels = "target:wheels", + deps = "target:deps", + rootfs = "target:rootfs" + } + platforms = ["linux/arm64"] +} \ No newline at end of file diff --git a/docker/rockchip/rk.mk b/docker/rockchip/rk.mk new file mode 100644 index 0000000..c8278f6 --- /dev/null +++ b/docker/rockchip/rk.mk @@ -0,0 +1,15 @@ +BOARDS += rk + +local-rk: version + docker buildx bake --file=docker/rockchip/rk.hcl rk \ + --set rk.tags=frigate:latest-rk \ + --load + +build-rk: version + docker buildx bake --file=docker/rockchip/rk.hcl rk \ + --set rk.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rk + +push-rk: build-rk + docker buildx bake --file=docker/rockchip/rk.hcl rk \ + --set rk.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rk \ + --push \ No newline at end of file diff --git a/docker/rocm/Dockerfile b/docker/rocm/Dockerfile new file mode 100644 index 0000000..653ed1e --- /dev/null +++ b/docker/rocm/Dockerfile @@ -0,0 +1,98 @@ +# syntax=docker/dockerfile:1.4 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive +ARG ROCM=1 +ARG HSA_OVERRIDE_GFX_VERSION +ARG HSA_OVERRIDE + +####################################################################### +FROM wget AS rocm + +ARG ROCM + +RUN apt update -qq && \ + apt install -y wget gpg && \ + wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.2.3/ubuntu/jammy/amdgpu-install_7.2.3.70203-1_all.deb && \ + apt install -y ./rocm.deb && \ + apt update && \ + apt install -qq -y rocm + +RUN mkdir -p /opt/rocm-dist/opt/rocm-$ROCM/lib +RUN cd /opt/rocm-$ROCM/lib && \ + cp -dpr libMIOpen*.so* libamd*.so* libhip*.so* libhsa*.so* libmigraphx*.so* librocm*.so* librocblas*.so* libroctracer*.so* librocsolver*.so* librocfft*.so* librocprofiler*.so* libroctx*.so* librocroller.so* /opt/rocm-dist/opt/rocm-$ROCM/lib/ && \ + mkdir -p /opt/rocm-dist/opt/rocm-$ROCM/lib/migraphx/lib && \ + cp -dpr migraphx/lib/* /opt/rocm-dist/opt/rocm-$ROCM/lib/migraphx/lib +RUN cd /opt/rocm-dist/opt/ && ln -s rocm-$ROCM rocm + +RUN mkdir -p /opt/rocm-dist/etc/ld.so.conf.d/ +RUN echo /opt/rocm/lib|tee /opt/rocm-dist/etc/ld.so.conf.d/rocm.conf + +####################################################################### +FROM deps AS deps-prelim + +COPY docker/rocm/debian-backports.sources /etc/apt/sources.list.d/debian-backports.sources +# install_deps.sh upgraded libstdc++6 from trixie for Battlemage; the matching +# -dev package must also come from trixie or apt refuses to satisfy it. +RUN echo "deb http://deb.debian.org/debian trixie main" > /etc/apt/sources.list.d/trixie.list && \ + apt-get update && \ + apt-get install -y libnuma1 && \ + apt-get install -qq -y -t bookworm-backports mesa-va-drivers mesa-vulkan-drivers && \ + apt-get install -qq -y -t trixie libstdc++-14-dev && \ + rm -f /etc/apt/sources.list.d/trixie.list && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/frigate +COPY --from=rootfs / / + +RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ + && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ + && python3 get-pip.py "pip" --break-system-packages +RUN python3 -m pip config set global.break-system-packages true + +COPY docker/rocm/requirements-wheels-rocm.txt /requirements.txt +RUN pip3 uninstall -y onnxruntime \ + && pip3 install -r /requirements.txt + +####################################################################### +FROM scratch AS rocm-dist + +ARG ROCM + +# Copy HIP headers required for MIOpen JIT (BuildHip) / HIPRTC at runtime +COPY --from=rocm /opt/rocm-${ROCM}/include/ /opt/rocm-${ROCM}/include/ +COPY --from=rocm /opt/rocm-$ROCM/bin/rocminfo /opt/rocm-$ROCM/bin/migraphx-driver /opt/rocm-$ROCM/bin/ +# Copy MIOpen database files for gfx10xx, gfx11xx, and gfx12xx only (RDNA2/RDNA3/RDNA4) +COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx10* /opt/rocm-$ROCM/share/miopen/db/ +COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx11* /opt/rocm-$ROCM/share/miopen/db/ +COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx12* /opt/rocm-$ROCM/share/miopen/db/ +# Copy rocBLAS library files for gfx10xx, gfx11xx, and gfx12xx only +COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx10* /opt/rocm-$ROCM/lib/rocblas/library/ +COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx11* /opt/rocm-$ROCM/lib/rocblas/library/ +COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx12* /opt/rocm-$ROCM/lib/rocblas/library/ +COPY --from=rocm /opt/rocm-dist/ / + +####################################################################### +FROM deps-prelim AS rocm-prelim-hsa-override0 +ENV MIGRAPHX_DISABLE_MIOPEN_FUSION=1 +ENV MIGRAPHX_DISABLE_SCHEDULE_PASS=1 +ENV MIGRAPHX_DISABLE_REDUCE_FUSION=1 +ENV MIGRAPHX_ENABLE_HIPRTC_WORKAROUNDS=1 +ENV MIOPEN_CUSTOM_CACHE_DIR=/config/model_cache/migraphx +ENV MIOPEN_USER_DB_PATH=/config/model_cache/migraphx +ENV AMD_COMGR_CACHE=1 +ENV AMD_COMGR_CACHE_DIR=/config/model_cache/migraphx + +COPY --from=rocm-dist / / + +RUN ldconfig + +####################################################################### +FROM rocm-prelim-hsa-override0 as rocm-prelim-hsa-override1 + +ARG HSA_OVERRIDE_GFX_VERSION +ENV HSA_OVERRIDE_GFX_VERSION=$HSA_OVERRIDE_GFX_VERSION + +####################################################################### +FROM rocm-prelim-hsa-override$HSA_OVERRIDE as rocm-deps + diff --git a/docker/rocm/debian-backports.sources b/docker/rocm/debian-backports.sources new file mode 100644 index 0000000..fc51f4e --- /dev/null +++ b/docker/rocm/debian-backports.sources @@ -0,0 +1,6 @@ +Types: deb +URIs: http://deb.debian.org/debian +Suites: bookworm-backports +Components: main +Enabled: yes +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg diff --git a/docker/rocm/requirements-wheels-rocm.txt b/docker/rocm/requirements-wheels-rocm.txt new file mode 100644 index 0000000..f60b550 --- /dev/null +++ b/docker/rocm/requirements-wheels-rocm.txt @@ -0,0 +1 @@ +onnxruntime-migraphx @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v7.2.3-1/onnxruntime_migraphx-1.24.4-cp311-cp311-linux_x86_64.whl \ No newline at end of file diff --git a/docker/rocm/rocm.hcl b/docker/rocm/rocm.hcl new file mode 100644 index 0000000..2241188 --- /dev/null +++ b/docker/rocm/rocm.hcl @@ -0,0 +1,42 @@ +variable "ROCM" { + default = "7.2.3" +} +variable "HSA_OVERRIDE_GFX_VERSION" { + default = "" +} +variable "HSA_OVERRIDE" { + default = "1" +} + +target wget { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/amd64"] + target = "wget" +} + +target deps { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/amd64"] + target = "deps" +} + +target rootfs { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/amd64"] + target = "rootfs" +} + +target rocm { + dockerfile = "docker/rocm/Dockerfile" + contexts = { + deps = "target:deps", + wget = "target:wget", + rootfs = "target:rootfs" + } + platforms = ["linux/amd64"] + args = { + ROCM = ROCM, + HSA_OVERRIDE_GFX_VERSION = HSA_OVERRIDE_GFX_VERSION, + HSA_OVERRIDE = HSA_OVERRIDE + } +} diff --git a/docker/rocm/rocm.mk b/docker/rocm/rocm.mk new file mode 100644 index 0000000..f98d387 --- /dev/null +++ b/docker/rocm/rocm.mk @@ -0,0 +1,15 @@ +BOARDS += rocm + +local-rocm: version + docker buildx bake --file=docker/rocm/rocm.hcl rocm \ + --set rocm.tags=frigate:latest-rocm \ + --load + +build-rocm: version + docker buildx bake --file=docker/rocm/rocm.hcl rocm \ + --set rocm.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rocm + +push-rocm: build-rocm + docker buildx bake --file=docker/rocm/rocm.hcl rocm \ + --set rocm.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rocm \ + --push diff --git a/docker/rpi/Dockerfile b/docker/rpi/Dockerfile new file mode 100644 index 0000000..35a2252 --- /dev/null +++ b/docker/rpi/Dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:1.4 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +FROM deps AS rpi-deps +ARG TARGETARCH + +# Install dependencies +RUN --mount=type=bind,source=docker/rpi/install_deps.sh,target=/deps/install_deps.sh \ + /deps/install_deps.sh + +ENV DEFAULT_FFMPEG_VERSION="rpi" +ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:${INCLUDED_FFMPEG_VERSIONS}" + +WORKDIR /opt/frigate/ +COPY --from=rootfs / / diff --git a/docker/rpi/install_deps.sh b/docker/rpi/install_deps.sh new file mode 100755 index 0000000..bf537d5 --- /dev/null +++ b/docker/rpi/install_deps.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euxo pipefail + +apt-get -qq update + +apt-get -qq install --no-install-recommends -y \ + apt-transport-https \ + gnupg \ + wget \ + procps vainfo \ + unzip locales tzdata libxml2 xz-utils \ + python3-pip \ + curl \ + jq \ + nethogs + +mkdir -p -m 600 /root/.gnupg + +# enable non-free repo +echo "deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware" | tee -a /etc/apt/sources.list +apt update + +# ffmpeg -> arm64 +if [[ "${TARGETARCH}" == "arm64" ]]; then + # add raspberry pi repo + gpg --no-default-keyring --keyring /usr/share/keyrings/raspbian.gpg --keyserver keyserver.ubuntu.com --recv-keys 82B129927FA3303E + echo "deb [signed-by=/usr/share/keyrings/raspbian.gpg] https://archive.raspberrypi.org/debian/ bookworm main" | tee /etc/apt/sources.list.d/raspi.list + apt-get -qq update + apt-get -qq install --no-install-recommends --no-install-suggests -y ffmpeg + mkdir -p /usr/lib/ffmpeg/rpi/bin + ln -svf /usr/bin/ffmpeg /usr/lib/ffmpeg/rpi/bin/ffmpeg + ln -svf /usr/bin/ffprobe /usr/lib/ffmpeg/rpi/bin/ffprobe +fi diff --git a/docker/rpi/rpi.hcl b/docker/rpi/rpi.hcl new file mode 100644 index 0000000..66f97c1 --- /dev/null +++ b/docker/rpi/rpi.hcl @@ -0,0 +1,20 @@ +target deps { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "deps" +} + +target rootfs { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "rootfs" +} + +target rpi { + dockerfile = "docker/rpi/Dockerfile" + contexts = { + deps = "target:deps", + rootfs = "target:rootfs" + } + platforms = ["linux/arm64"] +} \ No newline at end of file diff --git a/docker/rpi/rpi.mk b/docker/rpi/rpi.mk new file mode 100644 index 0000000..290b30c --- /dev/null +++ b/docker/rpi/rpi.mk @@ -0,0 +1,15 @@ +BOARDS += rpi + +local-rpi: version + docker buildx bake --file=docker/rpi/rpi.hcl rpi \ + --set rpi.tags=frigate:latest-rpi \ + --load + +build-rpi: version + docker buildx bake --file=docker/rpi/rpi.hcl rpi \ + --set rpi.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rpi + +push-rpi: build-rpi + docker buildx bake --file=docker/rpi/rpi.hcl rpi \ + --set rpi.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rpi \ + --push diff --git a/docker/synaptics/Dockerfile b/docker/synaptics/Dockerfile new file mode 100644 index 0000000..6a60fe4 --- /dev/null +++ b/docker/synaptics/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1.6 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +# Globally set pip break-system-packages option to avoid having to specify it every time +ARG PIP_BREAK_SYSTEM_PACKAGES=1 + +FROM wheels AS synap1680-wheels +ARG TARGETARCH + +# Install dependencies +RUN wget -qO- "https://github.com/GaryHuang-ASUS/synaptics_astra_sdk/releases/download/v1.5.0/Synaptics-SL1680-v1.5.0-rt.tar" | tar -C / -xzf - +RUN wget -P /wheels/ "https://github.com/synaptics-synap/synap-python/releases/download/v0.0.4-preview/synap_python-0.0.4-cp311-cp311-manylinux_2_35_aarch64.whl" + +FROM deps AS synap1680-deps +ARG TARGETARCH +ARG PIP_BREAK_SYSTEM_PACKAGES + +RUN --mount=type=bind,from=synap1680-wheels,source=/wheels,target=/deps/synap-wheels \ +pip3 install --no-deps -U /deps/synap-wheels/*.whl + +WORKDIR /opt/frigate/ +COPY --from=rootfs / / + +COPY --from=synap1680-wheels /rootfs/usr/local/lib/*.so /usr/lib + +ADD https://raw.githubusercontent.com/synaptics-astra/synap-release/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80/model.synap /synaptics/mobilenet.synap diff --git a/docker/synaptics/synaptics.hcl b/docker/synaptics/synaptics.hcl new file mode 100644 index 0000000..a22fb44 --- /dev/null +++ b/docker/synaptics/synaptics.hcl @@ -0,0 +1,27 @@ +target wheels { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "wheels" +} + +target deps { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "deps" +} + +target rootfs { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "rootfs" +} + +target synaptics { + dockerfile = "docker/synaptics/Dockerfile" + contexts = { + wheels = "target:wheels", + deps = "target:deps", + rootfs = "target:rootfs" + } + platforms = ["linux/arm64"] +} diff --git a/docker/synaptics/synaptics.mk b/docker/synaptics/synaptics.mk new file mode 100644 index 0000000..64cb858 --- /dev/null +++ b/docker/synaptics/synaptics.mk @@ -0,0 +1,15 @@ +BOARDS += synaptics + +local-synaptics: version + docker buildx bake --file=docker/synaptics/synaptics.hcl synaptics \ + --set synaptics.tags=frigate:latest-synaptics \ + --load + +build-synaptics: version + docker buildx bake --file=docker/synaptics/synaptics.hcl synaptics \ + --set synaptics.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-synaptics + +push-synaptics: build-synaptics + docker buildx bake --file=docker/synaptics/synaptics.hcl synaptics \ + --set synaptics.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-synaptics \ + --push diff --git a/docker/tensorrt/Dockerfile.amd64 b/docker/tensorrt/Dockerfile.amd64 new file mode 100644 index 0000000..cdf5df9 --- /dev/null +++ b/docker/tensorrt/Dockerfile.amd64 @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1.4 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +# Globally set pip break-system-packages option to avoid having to specify it every time +ARG PIP_BREAK_SYSTEM_PACKAGES=1 + +FROM wheels AS trt-wheels +ARG PIP_BREAK_SYSTEM_PACKAGES + +# Install TensorRT wheels +COPY docker/tensorrt/requirements-amd64.txt /requirements-tensorrt.txt +COPY docker/main/requirements-wheels.txt /requirements-wheels.txt + +# remove dependencies from the requirements that have type constraints +RUN sed -i '/\[.*\]/d' /requirements-wheels.txt \ + && pip3 wheel --wheel-dir=/trt-wheels -c /requirements-wheels.txt -r /requirements-tensorrt.txt + +FROM deps AS frigate-tensorrt +ARG PIP_BREAK_SYSTEM_PACKAGES + +RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \ + pip3 uninstall -y onnxruntime \ + && pip3 install -U /deps/trt-wheels/*.whl + +COPY --from=rootfs / / +COPY docker/tensorrt/detector/rootfs/etc/ld.so.conf.d /etc/ld.so.conf.d +RUN ldconfig + +WORKDIR /opt/frigate/ + +# Dev Container w/ TRT +FROM devcontainer AS devcontainer-trt + +RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \ + pip3 install -U /deps/trt-wheels/*.whl diff --git a/docker/tensorrt/Dockerfile.arm64 b/docker/tensorrt/Dockerfile.arm64 new file mode 100644 index 0000000..dd3c5de --- /dev/null +++ b/docker/tensorrt/Dockerfile.arm64 @@ -0,0 +1,156 @@ +# syntax=docker/dockerfile:1.6 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive +ARG BASE_IMAGE +ARG TRT_BASE=nvcr.io/nvidia/tensorrt:23.12-py3 + +# Build TensorRT-specific library +FROM ${TRT_BASE} AS trt-deps + +ARG TARGETARCH +ARG COMPUTE_LEVEL + +RUN apt-get update \ + && apt-get install -y git build-essential cuda-nvcc-* cuda-nvtx-* libnvinfer-dev libnvinfer-plugin-dev libnvparsers-dev libnvonnxparsers-dev \ + && rm -rf /var/lib/apt/lists/* +RUN --mount=type=bind,source=docker/tensorrt/detector/tensorrt_libyolo.sh,target=/tensorrt_libyolo.sh \ + /tensorrt_libyolo.sh + +# COPY required individual CUDA deps +RUN mkdir -p /usr/local/cuda-deps +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + cp /usr/local/cuda-12.3/targets/x86_64-linux/lib/libcurand.so.* /usr/local/cuda-deps/ && \ + cp /usr/local/cuda-12.3/targets/x86_64-linux/lib/libnvrtc.so.* /usr/local/cuda-deps/ && \ + cd /usr/local/cuda-deps/ && \ + for lib in libnvrtc.so.*; do \ + if [[ "$lib" =~ libnvrtc.so\.([0-9]+\.[0-9]+\.[0-9]+) ]]; then \ + version="${BASH_REMATCH[1]}"; \ + ln -sf "libnvrtc.so.$version" libnvrtc.so; \ + fi; \ + done && \ + for lib in libcurand.so.*; do \ + if [[ "$lib" =~ libcurand.so\.([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]]; then \ + version="${BASH_REMATCH[1]}"; \ + ln -sf "libcurand.so.$version" libcurand.so; \ + fi; \ + done; \ + fi + +# Frigate w/ TensorRT Support as separate image +FROM deps AS tensorrt-base + +#Disable S6 Global timeout +ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 + +# COPY TensorRT Model Generation Deps +COPY --from=trt-deps /usr/local/lib/libyolo_layer.so /usr/local/lib/libyolo_layer.so +COPY --from=trt-deps /usr/local/src/tensorrt_demos /usr/local/src/tensorrt_demos + +# COPY Individual CUDA deps folder +COPY --from=trt-deps /usr/local/cuda-deps /usr/local/cuda + +COPY docker/tensorrt/detector/rootfs/ / +ENV YOLO_MODELS="" + +HEALTHCHECK --start-period=600s --start-interval=5s --interval=15s --timeout=5s --retries=3 \ + CMD curl --fail --silent --show-error http://127.0.0.1:5000/api/version || exit 1 + +FROM ${BASE_IMAGE} AS build-wheels +ARG DEBIAN_FRONTEND + +# Add deadsnakes PPA for python3.11 +RUN apt-get -qq update && \ + apt-get -qq install -y --no-install-recommends \ + software-properties-common \ + && add-apt-repository ppa:deadsnakes/ppa + +# Use a separate container to build wheels to prevent build dependencies in final image +RUN apt-get -qq update \ + && apt-get -qq install -y --no-install-recommends \ + python3.11 python3.11-dev \ + wget build-essential cmake git \ + && rm -rf /var/lib/apt/lists/* + +# Ensure python3 defaults to python3.11 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 + +RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ + && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ + && python3 get-pip.py "pip" + +FROM build-wheels AS trt-wheels +ARG DEBIAN_FRONTEND +ARG TARGETARCH + +# python-tensorrt build deps are 3.4 GB! +RUN apt-get update \ + && apt-get install -y ccache cuda-cudart-dev-* cuda-nvcc-* libnvonnxparsers-dev libnvparsers-dev libnvinfer-plugin-dev \ + && ([ -e /usr/local/cuda ] || ln -s /usr/local/cuda-* /usr/local/cuda) \ + && rm -rf /var/lib/apt/lists/*; + +# Determine version of tensorrt already installed in base image, e.g. "Version: 8.4.1-1+cuda11.4" +RUN NVINFER_VER=$(dpkg -s libnvinfer8 | grep -Po "Version: \K.*") \ + && echo $NVINFER_VER | grep -Po "^\d+\.\d+\.\d+" > /etc/TENSORRT_VER + +RUN --mount=type=bind,source=docker/tensorrt/detector/build_python_tensorrt.sh,target=/deps/build_python_tensorrt.sh \ + --mount=type=cache,target=/root/.ccache \ + export PATH="/usr/lib/ccache:$PATH" CCACHE_DIR=/root/.ccache CCACHE_MAXSIZE=2G \ + && TENSORRT_VER=$(cat /etc/TENSORRT_VER) /deps/build_python_tensorrt.sh + +COPY docker/tensorrt/requirements-arm64.txt /requirements-tensorrt.txt + +RUN pip3 wheel --wheel-dir=/trt-wheels -r /requirements-tensorrt.txt + +# See https://elinux.org/Jetson_Zoo#ONNX_Runtime +ADD https://nvidia.box.com/shared/static/9yvw05k6u343qfnkhdv2x6xhygze0aq1.whl /trt-wheels/onnxruntime_gpu-1.19.0-cp311-cp311-linux_aarch64.whl + +FROM build-wheels AS trt-model-wheels +ARG DEBIAN_FRONTEND + +RUN apt-get update \ + && apt-get install -y protobuf-compiler libprotobuf-dev \ + && rm -rf /var/lib/apt/lists/* +RUN --mount=type=bind,source=docker/tensorrt/requirements-models-arm64.txt,target=/requirements-tensorrt-models.txt \ + pip3 wheel --wheel-dir=/trt-model-wheels --no-deps -r /requirements-tensorrt-models.txt + +FROM wget AS jetson-ffmpeg +ARG DEBIAN_FRONTEND +ENV CCACHE_DIR /root/.ccache +ENV CCACHE_MAXSIZE 2G +RUN --mount=type=bind,source=docker/tensorrt/build_jetson_ffmpeg.sh,target=/deps/build_jetson_ffmpeg.sh \ + --mount=type=cache,target=/root/.ccache \ + /deps/build_jetson_ffmpeg.sh + +# Frigate w/ TensorRT for NVIDIA Jetson platforms +FROM tensorrt-base AS frigate-tensorrt +RUN apt-get update \ + && apt-get install -y python-is-python3 libprotobuf23 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=jetson-ffmpeg /rootfs / +ENV DEFAULT_FFMPEG_VERSION="jetson" +ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:${INCLUDED_FFMPEG_VERSIONS}" + +# ffmpeg runtime dependencies +RUN apt-get -qq update \ + && apt-get -qq install -y --no-install-recommends \ + libx264-163 libx265-199 libegl1 \ + && rm -rf /var/lib/apt/lists/* + +# Fixes "Error loading shared libs" +RUN mkdir -p /etc/ld.so.conf.d && echo /usr/lib/ffmpeg/jetson/lib/ > /etc/ld.so.conf.d/ffmpeg.conf + +COPY --from=trt-wheels /etc/TENSORRT_VER /etc/TENSORRT_VER +RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \ + --mount=type=bind,from=trt-model-wheels,source=/trt-model-wheels,target=/deps/trt-model-wheels \ + pip3 uninstall -y onnxruntime \ + && pip3 install -U /deps/trt-wheels/*.whl \ + && pip3 install -U /deps/trt-model-wheels/*.whl \ + && ldconfig + +WORKDIR /opt/frigate/ +COPY --from=rootfs / / + +# Fixes "Error importing detector runtime: /usr/lib/aarch64-linux-gnu/libstdc++.so.6: cannot allocate memory in static TLS block" +ENV LD_PRELOAD /usr/lib/aarch64-linux-gnu/libstdc++.so.6 \ No newline at end of file diff --git a/docker/tensorrt/build_jetson_ffmpeg.sh b/docker/tensorrt/build_jetson_ffmpeg.sh new file mode 100755 index 0000000..fb29eb2 --- /dev/null +++ b/docker/tensorrt/build_jetson_ffmpeg.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# For jetson platforms, build ffmpeg with custom patches. NVIDIA supplies a deb +# with accelerated decoding, but it doesn't have accelerated scaling or encoding + +set -euxo pipefail + +INSTALL_PREFIX=/rootfs/usr/lib/ffmpeg/jetson + +apt-get -qq update +apt-get -qq install -y --no-install-recommends build-essential ccache clang cmake pkg-config +apt-get -qq install -y --no-install-recommends libx264-dev libx265-dev + +pushd /tmp + +# Install libnvmpi to enable nvmpi decoders (h264_nvmpi, hevc_nvmpi) +if [ -e /usr/local/cuda-12 ]; then + # assume Jetpack 6.2 + apt-key adv --fetch-key https://repo.download.nvidia.com/jetson/jetson-ota-public.asc + echo "deb https://repo.download.nvidia.com/jetson/common r36.4 main" >> /etc/apt/sources.list.d/nvidia-l4t-apt-source.list + echo "deb https://repo.download.nvidia.com/jetson/t234 r36.4 main" >> /etc/apt/sources.list.d/nvidia-l4t-apt-source.list + echo "deb https://repo.download.nvidia.com/jetson/ffmpeg r36.4 main" >> /etc/apt/sources.list.d/nvidia-l4t-apt-source.list + + mkdir -p /opt/nvidia/l4t-packages/ + touch /opt/nvidia/l4t-packages/.nv-l4t-disable-boot-fw-update-in-preinstall + + apt-get update + apt-get -qq install -y --no-install-recommends -o Dpkg::Options::="--force-confold" nvidia-l4t-jetson-multimedia-api +elif [ -e /usr/local/cuda-10.2 ]; then + # assume Jetpack 4.X + wget -q https://developer.nvidia.com/embedded/L4T/r32_Release_v5.0/T186/Jetson_Multimedia_API_R32.5.0_aarch64.tbz2 -O jetson_multimedia_api.tbz2 + tar xaf jetson_multimedia_api.tbz2 -C / && rm jetson_multimedia_api.tbz2 +else + # assume Jetpack 5.X + wget -q https://developer.nvidia.com/downloads/embedded/l4t/r35_release_v3.1/release/jetson_multimedia_api_r35.3.1_aarch64.tbz2 -O jetson_multimedia_api.tbz2 + tar xaf jetson_multimedia_api.tbz2 -C / && rm jetson_multimedia_api.tbz2 +fi + +wget -q https://github.com/AndBobsYourUncle/jetson-ffmpeg/archive/9c17b09.zip -O jetson-ffmpeg.zip +unzip jetson-ffmpeg.zip && rm jetson-ffmpeg.zip && mv jetson-ffmpeg-* jetson-ffmpeg && cd jetson-ffmpeg +LD_LIBRARY_PATH=$(pwd)/stubs:$LD_LIBRARY_PATH # tegra multimedia libs aren't available in image, so use stubs for ffmpeg build +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$INSTALL_PREFIX +make -j$(nproc) +make install +cd ../../ + +# Install nv-codec-headers to enable ffnvcodec filters (scale_cuda) +wget -q https://github.com/FFmpeg/nv-codec-headers/archive/refs/heads/master.zip +unzip master.zip && rm master.zip && cd nv-codec-headers-master +make PREFIX=$INSTALL_PREFIX install +cd ../ && rm -rf nv-codec-headers-master + +# Build ffmpeg with nvmpi patch +wget -q https://ffmpeg.org/releases/ffmpeg-6.0.tar.xz +tar xaf ffmpeg-*.tar.xz && rm ffmpeg-*.tar.xz && cd ffmpeg-* +patch -p1 < ../jetson-ffmpeg/ffmpeg_patches/ffmpeg6.0_nvmpi.patch +export PKG_CONFIG_PATH=$INSTALL_PREFIX/lib/pkgconfig +# enable Jetson codecs but disable dGPU codecs +./configure --cc='ccache gcc' --cxx='ccache g++' \ + --enable-shared --disable-static --prefix=$INSTALL_PREFIX \ + --enable-gpl --enable-libx264 --enable-libx265 \ + --enable-nvmpi --enable-ffnvcodec --enable-cuda-llvm \ + --disable-cuvid --disable-nvenc --disable-nvdec \ + || { cat ffbuild/config.log && false; } +make -j$(nproc) +make install +cd ../ + +rm -rf /var/lib/apt/lists/* +popd diff --git a/docker/tensorrt/detector/build_python_tensorrt.sh b/docker/tensorrt/detector/build_python_tensorrt.sh new file mode 100755 index 0000000..3251034 --- /dev/null +++ b/docker/tensorrt/detector/build_python_tensorrt.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -euxo pipefail + +mkdir -p /trt-wheels + +if [[ "${TARGETARCH}" == "arm64" ]]; then + + # NVIDIA supplies python-tensorrt for python3.10, but frigate uses python3.11, + # so we must build python-tensorrt ourselves. + + # Get python-tensorrt source + mkdir -p /workspace + cd /workspace + git clone -b release/8.6 https://github.com/NVIDIA/TensorRT.git --depth=1 + + # Collect dependencies + EXT_PATH=/workspace/external && mkdir -p $EXT_PATH + pip3 install pybind11 && ln -s /usr/local/lib/python3.11/dist-packages/pybind11 $EXT_PATH/pybind11 + ln -s /usr/include/python3.11 $EXT_PATH/python3.11 + ln -s /usr/include/aarch64-linux-gnu/NvOnnxParser.h /workspace/TensorRT/parsers/onnx/ + + # Build wheel + cd /workspace/TensorRT/python + EXT_PATH=$EXT_PATH PYTHON_MAJOR_VERSION=3 PYTHON_MINOR_VERSION=11 TARGET_ARCHITECTURE=aarch64 TENSORRT_MODULE=tensorrt /bin/bash ./build.sh + mv build/bindings_wheel/dist/*.whl /trt-wheels/ + +fi diff --git a/docker/tensorrt/detector/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf b/docker/tensorrt/detector/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf new file mode 100644 index 0000000..b00d4b4 --- /dev/null +++ b/docker/tensorrt/detector/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf @@ -0,0 +1,6 @@ +/usr/local/lib/python3.11/dist-packages/nvidia/cudnn/lib +/usr/local/lib/python3.11/dist-packages/nvidia/cuda_runtime/lib +/usr/local/lib/python3.11/dist-packages/nvidia/cublas/lib +/usr/local/lib/python3.11/dist-packages/nvidia/cufft/lib +/usr/local/lib/python3.11/dist-packages/nvidia/curand/lib/ +/usr/local/lib/python3.11/dist-packages/nvidia/cuda_nvrtc/lib/ \ No newline at end of file diff --git a/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/frigate/dependencies.d/trt-model-prepare b/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/frigate/dependencies.d/trt-model-prepare new file mode 100644 index 0000000..e69de29 diff --git a/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/dependencies.d/base b/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/dependencies.d/base new file mode 100644 index 0000000..e69de29 diff --git a/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/run b/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/run new file mode 100755 index 0000000..e3440e7 --- /dev/null +++ b/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/run @@ -0,0 +1,115 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Generate models for the TensorRT detector + +# One or more comma-separated models may be specified via the YOLO_MODELS env. +# Append "-dla" to the model name to generate a DLA model with GPU fallback; +# otherwise a GPU-only model will be generated. + +set -o errexit -o nounset -o pipefail + +MODEL_CACHE_DIR=${MODEL_CACHE_DIR:-"/config/model_cache/tensorrt"} +TRT_VER=${TRT_VER:-$(cat /etc/TENSORRT_VER)} +OUTPUT_FOLDER="${MODEL_CACHE_DIR}/${TRT_VER}" +YOLO_MODELS=${YOLO_MODELS:-""} + +# Create output folder +mkdir -p ${OUTPUT_FOLDER} + +FIRST_MODEL=true +MODEL_DOWNLOAD="" +MODEL_CONVERT="" + +if [ -z "$YOLO_MODELS" ]; then + echo "tensorrt model preparation disabled" + exit 0 +fi + +for model in ${YOLO_MODELS//,/ } +do + # Remove old link in case path/version changed + rm -f ${MODEL_CACHE_DIR}/${model}.trt + + if [[ ! -f ${OUTPUT_FOLDER}/${model}.trt ]]; then + if [[ ${FIRST_MODEL} = true ]]; then + MODEL_DOWNLOAD="${model%-dla}"; + MODEL_CONVERT="${model}" + FIRST_MODEL=false; + else + MODEL_DOWNLOAD+=",${model%-dla}"; + MODEL_CONVERT+=",${model}"; + fi + else + ln -s ${OUTPUT_FOLDER}/${model}.trt ${MODEL_CACHE_DIR}/${model}.trt + fi +done + +if [[ -z ${MODEL_CONVERT} ]]; then + echo "No models to convert." + exit 0 +fi + +# Setup ENV to select GPU for conversion +if [ ! -z ${TRT_MODEL_PREP_DEVICE+x} ]; then + if [ ! -z ${CUDA_VISIBLE_DEVICES+x} ]; then + PREVIOUS_CVD="$CUDA_VISIBLE_DEVICES" + unset CUDA_VISIBLE_DEVICES + fi + export CUDA_VISIBLE_DEVICES="$TRT_MODEL_PREP_DEVICE" +fi + +# On Jetpack 4.6, the nvidia container runtime will mount several host nvidia libraries into the +# container which should not be present in the image - if they are, TRT model generation will +# fail or produce invalid models. Thus we must request the user to install them on the host in +# order to run libyolo here. +# On Jetpack 5.0, these libraries are not mounted by the runtime and are supplied by the image. +if [[ "$(arch)" == "aarch64" ]]; then + if [[ ! -e /usr/lib/aarch64-linux-gnu/tegra && ! -e /usr/lib/aarch64-linux-gnu/tegra-egl ]]; then + echo "ERROR: Container must be launched with nvidia runtime" + exit 1 + elif [[ ! -e /usr/lib/aarch64-linux-gnu/libnvinfer.so.8 || + ! -e /usr/lib/aarch64-linux-gnu/libnvinfer_plugin.so.8 || + ! -e /usr/lib/aarch64-linux-gnu/libnvparsers.so.8 || + ! -e /usr/lib/aarch64-linux-gnu/libnvonnxparser.so.8 ]]; then + echo "ERROR: Please run the following on the HOST:" + echo " sudo apt install libnvinfer8 libnvinfer-plugin8 libnvparsers8 libnvonnxparsers8 nvidia-container" + exit 1 + fi +fi + +echo "Generating the following TRT Models: ${MODEL_CONVERT}" + +# Build trt engine +cd /usr/local/src/tensorrt_demos/yolo + +echo "Downloading yolo weights" +./download_yolo.sh $MODEL_DOWNLOAD 2> /dev/null + +for model in ${MODEL_CONVERT//,/ } +do + python3 yolo_to_onnx.py -m ${model%-dla} > /dev/null + + echo -e "\nGenerating ${model}.trt. This may take a few minutes.\n"; start=$(date +%s) + if [[ $model == *-dla ]]; then + cmd="python3 onnx_to_tensorrt.py -m ${model%-dla} --dla_core 0" + else + cmd="python3 onnx_to_tensorrt.py -m ${model}" + fi + $cmd > /tmp/onnx_to_tensorrt.log || { cat /tmp/onnx_to_tensorrt.log && continue; } + + mv ${model%-dla}.trt ${OUTPUT_FOLDER}/${model}.trt; + ln -s ${OUTPUT_FOLDER}/${model}.trt ${MODEL_CACHE_DIR}/${model}.trt + echo "Generated ${model}.trt in $(($(date +%s)-start)) seconds" +done + +# Restore ENV after conversion +if [ ! -z ${TRT_MODEL_PREP_DEVICE+x} ]; then + unset CUDA_VISIBLE_DEVICES + if [ ! -z ${PREVIOUS_CVD+x} ]; then + export CUDA_VISIBLE_DEVICES="$PREVIOUS_CVD" + fi +fi + +# Print which models exist in output folder +echo "Available tensorrt models:" +cd ${OUTPUT_FOLDER} && ls *.trt; diff --git a/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/type b/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/type new file mode 100644 index 0000000..bdd22a1 --- /dev/null +++ b/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/up b/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/up new file mode 100644 index 0000000..b9de40a --- /dev/null +++ b/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/trt-model-prepare/run diff --git a/docker/tensorrt/detector/tensorrt_libyolo.sh b/docker/tensorrt/detector/tensorrt_libyolo.sh new file mode 100755 index 0000000..46e4077 --- /dev/null +++ b/docker/tensorrt/detector/tensorrt_libyolo.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euxo pipefail + +SCRIPT_DIR="/usr/local/src/tensorrt_demos" + +# Clone tensorrt_demos repo +git clone --depth 1 https://github.com/NateMeyer/tensorrt_demos.git -b conditional_download + +# Build libyolo +if [ ! -e /usr/local/cuda ]; then + ln -s /usr/local/cuda-* /usr/local/cuda +fi +cd ./tensorrt_demos/plugins && make all -j$(nproc) computes="${COMPUTE_LEVEL:-}" +cp libyolo_layer.so /usr/local/lib/libyolo_layer.so + +# Store yolo scripts for later conversion +cd ../ +mkdir -p ${SCRIPT_DIR}/plugins +cp plugins/libyolo_layer.so ${SCRIPT_DIR}/plugins/libyolo_layer.so +cp -a yolo ${SCRIPT_DIR}/ diff --git a/docker/tensorrt/requirements-amd64.txt b/docker/tensorrt/requirements-amd64.txt new file mode 100644 index 0000000..597680c --- /dev/null +++ b/docker/tensorrt/requirements-amd64.txt @@ -0,0 +1,18 @@ +# Nvidia ONNX Runtime GPU Support +--extra-index-url 'https://pypi.nvidia.com' +cython==3.0.*; platform_machine == 'x86_64' +nvidia-cuda-cupti-cu12==12.8.90; platform_machine == 'x86_64' +nvidia-cublas-cu12==12.8.4.1; platform_machine == 'x86_64' +nvidia-cudnn-cu12==9.8.0.87; platform_machine == 'x86_64' +nvidia-cufft-cu12==11.3.3.83; platform_machine == 'x86_64' +nvidia-curand-cu12==10.3.9.90; platform_machine == 'x86_64' +nvidia-cuda-nvcc-cu12==12.8.93; platform_machine == 'x86_64' +nvidia-cuda-nvrtc-cu12==12.8.93; platform_machine == 'x86_64' +nvidia-cuda-runtime-cu12==12.8.90; platform_machine == 'x86_64' +nvidia-cusolver-cu12==11.7.3.90; platform_machine == 'x86_64' +nvidia-cusparse-cu12==12.5.8.93; platform_machine == 'x86_64' +nvidia-nccl-cu12==2.26.2.post1; platform_machine == 'x86_64' +nvidia-nvjitlink-cu12==12.8.93; platform_machine == 'x86_64' +onnx==1.16.*; platform_machine == 'x86_64' +onnxruntime-gpu==1.24.*; platform_machine == 'x86_64' +protobuf==3.20.3; platform_machine == 'x86_64' diff --git a/docker/tensorrt/requirements-arm64.txt b/docker/tensorrt/requirements-arm64.txt new file mode 100644 index 0000000..78d6597 --- /dev/null +++ b/docker/tensorrt/requirements-arm64.txt @@ -0,0 +1,2 @@ +cuda-python == 12.6.*; platform_machine == 'aarch64' +numpy == 1.26.*; platform_machine == 'aarch64' diff --git a/docker/tensorrt/requirements-models-arm64.txt b/docker/tensorrt/requirements-models-arm64.txt new file mode 100644 index 0000000..fe89b47 --- /dev/null +++ b/docker/tensorrt/requirements-models-arm64.txt @@ -0,0 +1,2 @@ +onnx == 1.14.0; platform_machine == 'aarch64' +protobuf == 3.20.3; platform_machine == 'aarch64' diff --git a/docker/tensorrt/trt.hcl b/docker/tensorrt/trt.hcl new file mode 100644 index 0000000..501e871 --- /dev/null +++ b/docker/tensorrt/trt.hcl @@ -0,0 +1,105 @@ +variable "ARCH" { + default = "amd64" +} +variable "BASE_IMAGE" { + default = null +} +variable "SLIM_BASE" { + default = null +} +variable "TRT_BASE" { + default = null +} +variable "COMPUTE_LEVEL" { + default = "" +} +variable "BASE_HOOK" { + # Ensure an up-to-date python 3.11 is available in jetson images + default = <> /etc/apt/sources.list.d/deadsnakes.list + echo "deb-src https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu $VERSION_CODENAME main" >> /etc/apt/sources.list.d/deadsnakes.list + + # Add deadsnakes signing key + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F23C5A6CF475977595C89F51BA6932366A755776 +fi +EOT +} + +target "_build_args" { + args = { + BASE_IMAGE = BASE_IMAGE, + SLIM_BASE = SLIM_BASE, + TRT_BASE = TRT_BASE, + COMPUTE_LEVEL = COMPUTE_LEVEL, + BASE_HOOK = BASE_HOOK + } + platforms = ["linux/${ARCH}"] +} + +target wget { + dockerfile = "docker/main/Dockerfile" + target = "wget" + inherits = ["_build_args"] +} + +target deps { + dockerfile = "docker/main/Dockerfile" + target = "deps" + inherits = ["_build_args"] +} + +target rootfs { + dockerfile = "docker/main/Dockerfile" + target = "rootfs" + inherits = ["_build_args"] +} + +target wheels { + dockerfile = "docker/main/Dockerfile" + target = "wheels" + inherits = ["_build_args"] +} + +target devcontainer { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/amd64"] + target = "devcontainer" +} + +target "trt-deps" { + dockerfile = "docker/tensorrt/Dockerfile.base" + context = "." + contexts = { + deps = "target:deps", + } + inherits = ["_build_args"] +} + +target "tensorrt" { + dockerfile = "docker/tensorrt/Dockerfile.${ARCH}" + context = "." + contexts = { + wget = "target:wget", + wheels = "target:wheels", + deps = "target:deps", + rootfs = "target:rootfs" + } + target = "frigate-tensorrt" + inherits = ["_build_args"] +} + +target "devcontainer-trt" { + dockerfile = "docker/tensorrt/Dockerfile.amd64" + context = "." + contexts = { + wheels = "target:wheels", + trt-deps = "target:trt-deps", + devcontainer = "target:devcontainer" + } + platforms = ["linux/amd64"] + target = "devcontainer-trt" +} diff --git a/docker/tensorrt/trt.mk b/docker/tensorrt/trt.mk new file mode 100644 index 0000000..904a2c3 --- /dev/null +++ b/docker/tensorrt/trt.mk @@ -0,0 +1,41 @@ +BOARDS += trt + +JETPACK5_BASE ?= nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime # L4T 35.3.1 JetPack 5.1.1 +JETPACK6_BASE ?= nvcr.io/nvidia/tensorrt:23.12-py3-igpu +X86_DGPU_ARGS := ARCH=amd64 COMPUTE_LEVEL="50 60 70 80 90" +JETPACK5_ARGS := ARCH=arm64 BASE_IMAGE=$(JETPACK5_BASE) SLIM_BASE=$(JETPACK5_BASE) TRT_BASE=$(JETPACK5_BASE) +JETPACK6_ARGS := ARCH=arm64 BASE_IMAGE=$(JETPACK6_BASE) SLIM_BASE=$(JETPACK6_BASE) TRT_BASE=$(JETPACK6_BASE) + +local-trt: version + $(X86_DGPU_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=frigate:latest-tensorrt \ + --load + +local-trt-jp5: version + $(JETPACK5_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=frigate:latest-tensorrt-jp5 \ + --load + +local-trt-jp6: version + $(JETPACK6_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=frigate:latest-tensorrt-jp6 \ + --load + +build-trt: + $(X86_DGPU_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt + $(JETPACK5_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt-jp5 + $(JETPACK6_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt-jp6 + +push-trt: build-trt + $(X86_DGPU_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt \ + --push + $(JETPACK5_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt-jp5 \ + --push + $(JETPACK6_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt-jp6 \ + --push diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..6e46baf --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,21 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader +docs/integrations/api/ + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..68b27e1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,10 @@ +# Website + +This website is built using [Docusaurus 3.5](https://docusaurus.io/docs), a modern static website generator. + +For installation and contributing instructions, please follow the [Contributing Docs](https://docs.frigate.video/development/contributing). + +# Development + +1. Run `npm i` to install dependencies +2. Run `npm run start` to start the website diff --git a/docs/babel.config.js b/docs/babel.config.js new file mode 100644 index 0000000..e00595d --- /dev/null +++ b/docs/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/docs/data/object_detectors_models.yaml b/docs/data/object_detectors_models.yaml new file mode 100644 index 0000000..a4458c3 --- /dev/null +++ b/docs/data/object_detectors_models.yaml @@ -0,0 +1,1346 @@ +edgeTPU: + title: EdgeTPU + models: + - key: mobiledet + label: Mobiledet + recommended: true + download: A TensorFlow Lite model is provided in the container at `/edgetpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`. + ui: Navigate to **Settings > System > Detectors and model** and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. + yaml: |- + detectors: + coral: + type: edgetpu + device: usb + - key: yolov9 + label: YOLOv9 + recommended: false + download: "[Download the model](https://github.com/dbro/frigate-detector-edgetpu-yolo9/releases/download/v1.0/yolov9-s-relu6-best_320_int8_edgetpu.tflite), bind mount the file into the container, and provide the path with `model.path`. Note that the linked model requires a 17-label [labelmap file](https://raw.githubusercontent.com/dbro/frigate-detector-edgetpu-yolo9/refs/heads/main/labels-coco17.txt) that includes only 17 COCO classes." + ui: |- + Navigate to **Settings > System > Detectors and model** and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. Then on the same page, in the **Custom Model** tab, configure the model settings: + + | Field | Value | + | ---------------------------------------- | ----------------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite` | + | **Label map for custom object detector** | `/config/labels-coco17.txt` | + | **Object detection model input width** | `320` (should match the imgsize of the model) | + | **Object detection model input height** | `320` (should match the imgsize of the model) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nhwc` (Frigate's default value) | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + coral: + type: edgetpu + device: usb + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize of the model, typically 320 + height: 320 # <--- should match the imgsize of the model, typically 320 + path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite + labelmap_path: /config/labels-coco17.txt +hailo8l: + title: Hailo-8/Hailo-8L + models: + - key: yolo + label: YOLO + recommended: true + download: If no custom model path or URL is provided, the Hailo detector automatically downloads the default model (YOLOv6n) from the Hailo Model Zoo on first startup based on the detected hardware. Once cached under `/config/model_cache/hailo`, the model works fully offline. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings: + + | Field | Value | + | ---------------------------------------- | ----------------------- | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `rgb` | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` | + | **Object Detection Model Type** | `yolo-generic` | + + The detector automatically selects the default model based on your hardware. Optionally, specify a local model path or URL to override. + yaml: |- + detectors: + hailo: + type: hailo8l + device: PCIe + + model: + width: 320 + height: 320 + input_tensor: nhwc + input_pixel_format: rgb + input_dtype: int + model_type: yolo-generic + labelmap_path: /labelmap/coco-80.txt + + # The detector automatically selects the default model based on your hardware: + # - For Hailo-8 hardware: YOLOv6n (default: yolov6n.hef) + # - For Hailo-8L hardware: YOLOv6n (default: yolov6n.hef) + # + # Optionally, you can specify a local model path to override the default. + # If a local path is provided and the file exists, it will be used instead of downloading. + # Example: + # path: /config/model_cache/hailo/yolov6n.hef + # + # You can also override using a custom URL: + # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef + # just make sure to give it the write configuration based on the model + - key: ssd + label: SSD MobileNet v1 + recommended: false + download: For SSD-based models, provide either a model path or URL to your compiled SSD model. The integration will first check the local path before downloading if necessary. The model file is cached under `/config/model_cache/hailo`. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings: + + | Field | Value | + | --------------------------------------- | ------ | + | **Object detection model input width** | `300` | + | **Object detection model input height** | `300` | + | **Model Input Pixel Color Format** | `rgb` | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `ssd` | + + Specify the local model path or URL for SSD MobileNet v1. + yaml: |- + detectors: + hailo: + type: hailo8l + device: PCIe + + model: + width: 300 + height: 300 + input_tensor: nhwc + input_pixel_format: rgb + model_type: ssd + # Specify the local model path (if available) or URL for SSD MobileNet v1. + # Example with a local path: + # path: /config/model_cache/h8l_cache/ssd_mobilenet_v1.hef + # + # Or override using a custom URL: + # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/ssd_mobilenet_v1.hef +openvino: + title: OpenVINO + models: + - key: yolov9 + label: YOLOv9 + recommended: true + download: |- + YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`). + + ```sh + docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF' + FROM python:3.11 AS build + RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ + WORKDIR /yolov9 + ADD https://github.com/WongKinYiu/yolov9.git . + RUN uv pip install --system -r requirements.txt + RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript + ARG MODEL_SIZE + ARG IMG_SIZE + ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt + RUN sed -i "s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g" models/experimental.py + RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx + FROM scratch + ARG MODEL_SIZE + ARG IMG_SIZE + COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx + EOF + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + ov: + type: openvino + device: GPU # or NPU + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: ssd + label: SSDLite MobileNet v2 + recommended: false + download: An OpenVINO model is provided in the container at `/openvino-model/ssdlite_mobilenet_v2.xml` and is used by this detector type by default. The model comes from Intel's Open Model Zoo [SSDLite MobileNet V2](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/ssdlite_mobilenet_v2) and is converted to an FP16 precision IR model. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------ | + | **Custom object detector model path** | `/openvino-model/ssdlite_mobilenet_v2.xml` | + | **Label map for custom object detector** | `/openvino-model/coco_91cl_bkgr.txt` | + | **Object detection model input width** | `300` | + | **Object detection model input height** | `300` | + | **Model Input Pixel Color Format** | `bgr` | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `ssd` (Frigate's default value) | + yaml: |- + detectors: + ov: + type: openvino + device: GPU # Or NPU + + model: + width: 300 + height: 300 + input_tensor: nhwc + input_pixel_format: bgr + path: /openvino-model/ssdlite_mobilenet_v2.xml + labelmap_path: /openvino-model/coco_91cl_bkgr.txt + - key: yolo-legacy + label: YOLO (v3, v4, v7) + recommended: false + download: |- + To export as ONNX: + + ```sh + git clone https://github.com/NateMeyer/tensorrt_demos + cd tensorrt_demos/yolo + ./download_yolo.sh + python3 yolo_to_onnx.py -m yolov7-320 + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + ov: + type: openvino + device: GPU # or NPU + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: yolonas + label: YOLO-NAS + recommended: false + download: |- + You can build and download a compatible model with pre-trained weights using [this notebook](https://github.com/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) which can be run directly in [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb). + + :::warning + + The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html + + ::: + + The input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------------- | + | **Custom object detector model path** | `/config/yolo_nas_s.onnx` | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match whatever was set in notebook) | + | **Object detection model input height** | `320` (should match whatever was set in notebook) | + | **Model Input Pixel Color Format** | `bgr` | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolonas` | + yaml: |- + detectors: + ov: + type: openvino + device: GPU + + model: + model_type: yolonas + width: 320 # <--- should match whatever was set in notebook + height: 320 # <--- should match whatever was set in notebook + input_tensor: nchw + input_pixel_format: bgr + path: /config/yolo_nas_s.onnx + labelmap_path: /labelmap/coco-80.txt + - key: yolox + label: YOLOX + recommended: false + download: YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime). + ui: |- + Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ------------------------------------- | -------------------------------- | + | **Custom object detector model path** | `/config/yolox.onnx` (use the filename you generated above) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nhwc` (Frigate's default value) | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolox` | + yaml: |- + detectors: + ov: + type: openvino + device: GPU + + model: + model_type: yolox + path: /config/model_cache/yolox.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: rfdetr + label: RF-DETR + recommended: false + download: |- + RF-DETR can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=Nano` in the first line to `Nano`, `Small`, or `Medium` size. + + ```sh + docker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF' + FROM python:3.12 AS build + RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ + WORKDIR /rfdetr + RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript + ARG MODEL_SIZE + RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)" + FROM scratch + ARG MODEL_SIZE + COPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx + EOF + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | --------------------------------------- | --------------------------------- | + | **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` (use the filename you generated above) | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `rfdetr` | + yaml: |- + detectors: + ov: + type: openvino + device: GPU + + model: + model_type: rfdetr + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/rfdetr.onnx # use the filename you generated above + - key: dfine + label: D-FINE / DEIMv2 + recommended: false + download: |- + #### D-FINE + + D-FINE can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=s` in the first line to `s`, `m`, or `l` size. + + ```sh + docker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF' + FROM python:3.11 AS build + RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ + WORKDIR /dfine + RUN git clone https://github.com/Peterande/D-FINE.git . + RUN uv pip install --system -r requirements.txt + RUN uv pip install --system onnx onnxruntime onnxsim onnxscript + # Create output directory and download checkpoint + RUN mkdir -p output + ARG MODEL_SIZE + RUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth + # Modify line 58 of export_onnx.py to change batch size to 1 + RUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py + RUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth + FROM scratch + ARG MODEL_SIZE + COPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx + EOF + ``` + + #### DEIMv2 + + [DEIMv2](https://github.com/Intellindust-AI-Lab/DEIMv2) can be exported as ONNX by running the command below. Pretrained weights are available on Hugging Face for two backbone families: + + - **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n` + - **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x` + + Set `BACKBONE` and `MODEL_SIZE` in the first line to match your desired variant. Hugging Face model names use uppercase (e.g. `HGNetv2_N`, `DINOv3_S`), while config files use lowercase (e.g. `hgnetv2_n`, `dinov3_s`). + + ```sh + docker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF' + FROM python:3.11-slim AS build + RUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ + WORKDIR /deimv2 + RUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git . + # Install CPU-only PyTorch first to avoid pulling CUDA variant + RUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu + RUN uv pip install --no-cache --system -r requirements.txt + RUN uv pip install --no-cache --system onnx safetensors huggingface_hub + RUN mkdir -p output + ARG BACKBONE + ARG MODEL_SIZE + # Download from Hugging Face and convert safetensors to pth + RUN python3 -c "\ + from huggingface_hub import hf_hub_download; \ + from safetensors.torch import load_file; \ + import torch; \ + backbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \ + size = '${MODEL_SIZE}'.upper(); \ + st = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \ + torch.save({'model': st}, 'output/deimv2.pth')" + RUN sed -i "s/data = torch.rand(2/data = torch.rand(1/" tools/deployment/export_onnx.py + # HuggingFace safetensors omits frozen constants that the model constructor initializes + RUN sed -i "s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/" tools/deployment/export_onnx.py + RUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth + FROM scratch + ARG BACKBONE + ARG MODEL_SIZE + COPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx + EOF + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `CPU`. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ---------------------------------- | + | **Custom object detector model path** | `/config/model_cache/dfine-s.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `640` | + | **Object detection model input height** | `640` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `dfine` | + yaml: |- + detectors: + ov: + type: openvino + device: CPU + + model: + model_type: dfine + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/dfine-s.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt +appleSilicon: + title: Apple Silicon + models: + - key: yolov9 + label: YOLOv9 + recommended: true + download: |- + YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`). + + ```sh + docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF' + FROM python:3.11 AS build + RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ + WORKDIR /yolov9 + ADD https://github.com/WongKinYiu/yolov9.git . + RUN uv pip install --system -r requirements.txt + RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript + ARG MODEL_SIZE + ARG IMG_SIZE + ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt + RUN sed -i "s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g" models/experimental.py + RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx + FROM scratch + ARG MODEL_SIZE + ARG IMG_SIZE + COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx + EOF + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + apple-silicon: + type: zmq + endpoint: tcp://host.docker.internal:5555 + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: yolo-legacy + label: YOLO (v3, v4, v7) + recommended: false + download: |- + To export as ONNX: + + ```sh + git clone https://github.com/NateMeyer/tensorrt_demos + cd tensorrt_demos/yolo + ./download_yolo.sh + python3 yolo_to_onnx.py -m yolov7-320 + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + apple-silicon: + type: zmq + endpoint: tcp://host.docker.internal:5555 + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt +onnx: + title: ONNX + models: + - key: yolov9 + label: YOLOv9 + recommended: true + download: |- + YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`). + + ```sh + docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF' + FROM python:3.11 AS build + RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ + WORKDIR /yolov9 + ADD https://github.com/WongKinYiu/yolov9.git . + RUN uv pip install --system -r requirements.txt + RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript + ARG MODEL_SIZE + ARG IMG_SIZE + ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt + RUN sed -i "s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g" models/experimental.py + RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx + FROM scratch + ARG MODEL_SIZE + ARG IMG_SIZE + COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx + EOF + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: rfdetr + label: RF-DETR + recommended: false + download: |- + RF-DETR can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=Nano` in the first line to `Nano`, `Small`, or `Medium` size. + + ```sh + docker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF' + FROM python:3.12 AS build + RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ + WORKDIR /rfdetr + RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript + ARG MODEL_SIZE + RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)" + FROM scratch + ARG MODEL_SIZE + COPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx + EOF + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | --------------------------------------- | --------------------------------- | + | **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` (use the filename you generated above) | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `rfdetr` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: rfdetr + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/rfdetr.onnx # use the filename you generated above + - key: yolonas + label: YOLO-NAS + recommended: false + download: |- + You can build and download a compatible model with pre-trained weights using [this notebook](https://github.com/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) which can be run directly in [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb). + + :::warning + + The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html + + ::: + + The input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------------- | + | **Custom object detector model path** | `/config/yolo_nas_s.onnx` | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match whatever was set in notebook) | + | **Object detection model input height** | `320` (should match whatever was set in notebook) | + | **Model Input Pixel Color Format** | `bgr` | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolonas` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: yolonas + width: 320 # <--- should match whatever was set in notebook + height: 320 # <--- should match whatever was set in notebook + input_pixel_format: bgr + input_tensor: nchw + path: /config/yolo_nas_s.onnx + labelmap_path: /labelmap/coco-80.txt + - key: yolox + label: YOLOX + recommended: false + download: YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime). + ui: |- + Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolox_tiny.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `416` (should match the imgsize set during model export) | + | **Object detection model input height** | `416` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float_denorm` | + | **Object Detection Model Type** | `yolox` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: yolox + width: 416 # <--- should match the imgsize set during model export + height: 416 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float_denorm + path: /config/model_cache/yolox_tiny.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: dfine + label: D-FINE / DEIMv2 + recommended: false + download: |- + #### Downloading D-FINE Model + + D-FINE can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=s` in the first line to `s`, `m`, or `l` size. + + ```sh + docker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF' + FROM python:3.11 AS build + RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ + WORKDIR /dfine + RUN git clone https://github.com/Peterande/D-FINE.git . + RUN uv pip install --system -r requirements.txt + RUN uv pip install --system onnx onnxruntime onnxsim onnxscript + # Create output directory and download checkpoint + RUN mkdir -p output + ARG MODEL_SIZE + RUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth + # Modify line 58 of export_onnx.py to change batch size to 1 + RUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py + RUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth + FROM scratch + ARG MODEL_SIZE + COPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx + EOF + ``` + + #### Downloading DEIMv2 Model + + [DEIMv2](https://github.com/Intellindust-AI-Lab/DEIMv2) can be exported as ONNX by running the command below. Pretrained weights are available on Hugging Face for two backbone families: + + - **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n` + - **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x` + + Set `BACKBONE` and `MODEL_SIZE` in the first line to match your desired variant. Hugging Face model names use uppercase (e.g. `HGNetv2_N`, `DINOv3_S`), while config files use lowercase (e.g. `hgnetv2_n`, `dinov3_s`). + + ```sh + docker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF' + FROM python:3.11-slim AS build + RUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ + WORKDIR /deimv2 + RUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git . + # Install CPU-only PyTorch first to avoid pulling CUDA variant + RUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu + RUN uv pip install --no-cache --system -r requirements.txt + RUN uv pip install --no-cache --system onnx safetensors huggingface_hub + RUN mkdir -p output + ARG BACKBONE + ARG MODEL_SIZE + # Download from Hugging Face and convert safetensors to pth + RUN python3 -c "\ + from huggingface_hub import hf_hub_download; \ + from safetensors.torch import load_file; \ + import torch; \ + backbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \ + size = '${MODEL_SIZE}'.upper(); \ + st = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \ + torch.save({'model': st}, 'output/deimv2.pth')" + RUN sed -i "s/data = torch.rand(2/data = torch.rand(1/" tools/deployment/export_onnx.py + # HuggingFace safetensors omits frozen constants that the model constructor initializes + RUN sed -i "s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/" tools/deployment/export_onnx.py + RUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth + FROM scratch + ARG BACKBONE + ARG MODEL_SIZE + COPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx + EOF + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/dfine_m_obj2coco.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `640` | + | **Object detection model input height** | `640` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `dfine` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: dfine + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/dfine_m_obj2coco.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: yolo-legacy + label: YOLO (v3, v4, v7) + recommended: false + download: |- + To export as ONNX: + + ```sh + git clone https://github.com/NateMeyer/tensorrt_demos + cd tensorrt_demos/yolo + ./download_yolo.sh + python3 yolo_to_onnx.py -m yolov7-320 + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt +cpu: + title: CPU + models: + - key: ssd + label: MobileNet v2 + recommended: true + download: A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **CPU** from the detector type dropdown and click **Add**. Configure the number of threads and click **Add** again to add additional CPU detectors as needed (one per camera is recommended). + + | Field | Value | + | ----------------- | ----- | + | **Detector type** | `cpu` | + | **Num threads** | `3` | + yaml: |- + detectors: + cpu1: + type: cpu + num_threads: 3 +deepstack: + title: DeepStack / CodeProject.AI + models: + - key: yolo + label: YOLO + recommended: true + download: This detector runs object detection over the network against a CodeProject.AI or DeepStack server, so no model is downloaded into Frigate itself. Visit the [CodeProject.AI official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to download and install the AI server on your preferred device (e.g. Raspberry Pi, Nvidia Jetson, or other compatible hardware) before configuring the detector. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **DeepStack** from the detector type dropdown and click **Add**. Set the API URL to point to your CodeProject.AI server (e.g., `http://:/v1/vision/detection`). + + | Field | Value | + | ------------- | ---------------------------------------------------------------------- | + | **API URL** | `http://:/v1/vision/detection` | + | **API Timeout** | `0.1` (seconds) | + yaml: |- + detectors: + deepstack: + api_url: http://:/v1/vision/detection + type: deepstack + api_timeout: 0.1 # seconds +memryx: + title: MemryX + models: + - key: yolonas + label: YOLO-NAS + recommended: true + download: |- + The [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model included in this detector is downloaded automatically and compiled to DFP with [mx_nc](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage). + + **Note:** The default model for the MemryX detector is YOLO-NAS 320x320. + + The input size for **YOLO-NAS** can be set to either **320x320** (default) or **640x640**. + + - The default size of **320x320** is optimized for lower CPU usage and faster inference times. + + MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------------- | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (can be set to `640` for higher resolution) | + | **Object detection model input height** | `320` (can be set to `640` for higher resolution) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolonas` | + yaml: |- + detectors: + memx0: + type: memryx + device: PCIe:0 + + model: + model_type: yolonas + width: 320 # (Can be set to 640 for higher resolution) + height: 320 # (Can be set to 640 for higher resolution) + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolonas.zip + # The .zip file must contain: + # ├── yolonas.dfp (a file ending with .dfp) + # └── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network) + - key: yolov9 + label: YOLOv9 + recommended: false + download: |- + The YOLOv9s model included in this detector is downloaded from [the original GitHub](https://github.com/WongKinYiu/yolov9) and compiled to DFP with [mx_nc](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage). + + MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------------- | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (can be set to `640` for higher resolution) | + | **Object detection model input height** | `320` (can be set to `640` for higher resolution) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + memx0: + type: memryx + device: PCIe:0 + + model: + model_type: yolo-generic + width: 320 # (Can be set to 640 for higher resolution) + height: 320 # (Can be set to 640 for higher resolution) + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolov9.zip + # The .zip file must contain: + # ├── yolov9.dfp (a file ending with .dfp) + - key: yolox + label: YOLOX + recommended: false + download: |- + The model is sourced from the [OpenCV Model Zoo](https://github.com/opencv/opencv_zoo) and precompiled to DFP. + + MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ----------------------- | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `640` | + | **Object detection model input height** | `640` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float_denorm` | + | **Object Detection Model Type** | `yolox` | + yaml: |- + detectors: + memx0: + type: memryx + device: PCIe:0 + + model: + model_type: yolox + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float_denorm + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolox.zip + # The .zip file must contain: + # ├── yolox.dfp (a file ending with .dfp) + - key: ssd + label: SSDLite MobileNet v2 + recommended: false + download: |- + The model is sourced from the [OpenMMLab Model Zoo](https://mmdeploy-oss.openmmlab.com/model/mmdet-det/ssdlite-e8679f.onnx) and has been converted to DFP. + + MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ----------------------- | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `ssd` | + yaml: |- + detectors: + memx0: + type: memryx + device: PCIe:0 + + model: + model_type: ssd + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/ssdlite_mobilenet.zip + # The .zip file must contain: + # ├── ssdlite_mobilenet.dfp (a file ending with .dfp) + # └── ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network) +tensorrt: + title: TensorRT + models: + - key: yolo-legacy + label: YOLO (v3, v4, v7) + recommended: true + download: |- + The model used for TensorRT must be preprocessed on the same hardware platform that it will run on, so Frigate generates the `.trt` model file on-device at startup. Processed models are stored in the `/config/model_cache` folder. + + By default no models are generated. Set the `YOLO_MODELS` environment variable in Docker to one or more comma-separated model names (from the available `yolov3`/`yolov4`/`yolov7` models) and each one will be generated on startup if the corresponding `{model}.trt` file is not already present in `model_cache` (delete it to force regeneration). On Jetson devices with DLAs (Xavier or Orin), append `-dla` to a model name to generate a DLA model. If your GPU does not support FP16 operations, pass `USE_FP16=False` to disable it. + + An example `docker-compose.yml` fragment that converts the `yolov7-320` and `yolov7x-640` models: + + ```yml + frigate: + environment: + - YOLO_MODELS=yolov7-320,yolov7x-640 + - USE_FP16=false + ``` + ui: |- + Navigate to **Settings > System > Detectors and model** and select **TensorRT** from the detector type dropdown and click **Add**, then set the device to `0` (the default GPU index). Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------------------------ | + | **Custom object detector model path** | `/config/model_cache/tensorrt/yolov7-320.trt` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) | + | **Object detection model input height** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) | + | **Model Input Pixel Color Format** | `rgb` | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `ssd` (Frigate's default value) | + yaml: |- + detectors: + tensorrt: + type: tensorrt + device: 0 #This is the default, select the first GPU + + model: + path: /config/model_cache/tensorrt/yolov7-320.trt # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + input_tensor: nchw + input_pixel_format: rgb + width: 320 # MUST match the chosen model i.e yolov7-320 -> 320, yolov4-416 -> 416 + height: 320 # MUST match the chosen model i.e yolov7-320 -> 320 yolov4-416 -> 416 +synaptics: + title: Synaptics + models: + - key: ssd + label: SSD MobileNet + recommended: true + download: A synap model is provided in the container at `/mobilenet.synap` and is used by this detector type by default. The model comes from the [Synap-release Github](https://github.com/synaptics-astra/synap-release/tree/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80). + ui: |- + Navigate to **Settings > System > Detectors and model** and select **Synaptics** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ---------------------------- | + | **Custom object detector model path** | `/synaptics/mobilenet.synap` | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `224` | + | **Object detection model input height** | `224` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `ssd` (Frigate's default value) | + yaml: |- + detectors: # required + synap_npu: # required + type: synaptics # required + + model: # required + path: /synaptics/mobilenet.synap # required + width: 224 # required + height: 224 # required + input_tensor: nhwc # default value (optional. If you change the model, it is required) + labelmap_path: /labelmap/coco-80.txt # required +rknn: + title: RKNN + models: + - key: yolov9 + label: YOLOv9 + recommended: true + download: |- + If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space. + + You can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models. + ui: |- + Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------- | + | **Custom object detector model path** | `frigate-fp16-yolov9-t` (or other yolov9 variants) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + model: # required + # name of model (will be automatically downloaded) or path to your own .rknn model file + # possible values are: + # - frigate-fp16-yolov9-t + # - frigate-fp16-yolov9-s + # - frigate-fp16-yolov9-m + # - frigate-fp16-yolov9-c + # - frigate-fp16-yolov9-e + # your yolo_model.rknn + path: frigate-fp16-yolov9-t + model_type: yolo-generic + width: 320 + height: 320 + input_tensor: nhwc + labelmap_path: /labelmap/coco-80.txt + - key: yolonas + label: YOLO-NAS + recommended: false + download: |- + If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space. + + You can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models. + + **Note:** The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html + ui: |- + Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ----------------------------------------------------------------------- | + | **Custom object detector model path** | `deci-fp16-yolonas_s` (or `deci-fp16-yolonas_m`, `deci-fp16-yolonas_l`) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `bgr` | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolonas` | + yaml: |- + model: # required + # name of model (will be automatically downloaded) or path to your own .rknn model file + # possible values are: + # - deci-fp16-yolonas_s + # - deci-fp16-yolonas_m + # - deci-fp16-yolonas_l + # your yolonas_model.rknn + path: deci-fp16-yolonas_s + model_type: yolonas + width: 320 + height: 320 + input_pixel_format: bgr + input_tensor: nhwc + labelmap_path: /labelmap/coco-80.txt + - key: yolox + label: YOLOx + recommended: false + download: |- + If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space. + + You can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models. + ui: |- + Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ---------------------------------------------- | + | **Custom object detector model path** | `rock-i8-yolox_nano` (or other yolox variants) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `416` | + | **Object detection model input height** | `416` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolox` | + yaml: |- + model: # required + # name of model (will be automatically downloaded) or path to your own .rknn model file + # possible values are: + # - rock-i8-yolox_nano + # - rock-i8-yolox_tiny + # - rock-fp16-yolox_nano + # - rock-fp16-yolox_tiny + # your yolox_model.rknn + path: rock-i8-yolox_nano + model_type: yolox + width: 416 + height: 416 + input_tensor: nhwc + labelmap_path: /labelmap/coco-80.txt +axengine: + title: AXEngine + models: + - key: yolov9 + label: YOLOv9 + recommended: true + download: A yolov9 axmodel is provided in the container at `/axmodels` and is used by this detector type by default. The AXEngine detector downloads its default model from HuggingFace on first startup; once cached, the model works fully offline. + ui: |- + Navigate to **Settings > System > Detectors and model** and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: + + | Field | Value | + | ---------------------------------------- | ----------------------- | + | **Custom object detector model path** | `frigate-yolov9-tiny` | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `bgr` | + | **Model Input Tensor Shape** | `nhwc` (Frigate's default value) | + | **Model Input D Type** | `int` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + axengine: + type: axengine + + model: + path: frigate-yolov9-tiny + model_type: yolo-generic + width: 320 + height: 320 + input_dtype: int + input_pixel_format: bgr + labelmap_path: /labelmap/coco-80.txt +degirumAiServer: + title: DeGirum AI Server + models: + - key: ai-server-inference + label: AI Server Inference + recommended: true + download: |- + Launch a DeGirum AI server as a Docker container, then point the detector at it. Add this to your `docker-compose.yml`: + + ```yaml + degirum_detector: + container_name: degirum + image: degirum/aiserver:latest + privileged: true + ports: + - "8778:8778" + ``` + + Set `location` to the server's service name, container name, or `host:port`. + ui: | + Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**. + + | Field | Value | + | --- | --- | + | **Location** | `degirum` | + | **Zoo** | `degirum/public` | + | **Token** | your AI Hub token (optional for the public zoo) | + yaml: | + degirum_detector: + type: degirum + location: degirum + zoo: degirum/public + token: dg_example_token +degirumLocal: + title: DeGirum Local + models: + - key: local-inference + label: Local Inference + recommended: true + download: Run hardware directly inside the Frigate container with `@local`, removing the AI server hop. The matching device runtime (e.g. the Hailo runtime) must be installed in the container; confirm it with `degirum sys-info`. + ui: | + Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**. + + | Field | Value | + | --- | --- | + | **Location** | `@local` | + | **Zoo** | `degirum/public` | + | **Token** | your AI Hub token (optional for the public zoo) | + yaml: | + degirum_detector: + type: degirum + location: @local + zoo: degirum/public + token: dg_example_token +degirumCloud: + title: DeGirum AI Hub Cloud + models: + - key: ai-hub-cloud-inference + label: AI Hub Cloud Inference + recommended: true + download: Run inferences on DeGirum's [AI Hub](https://hub.degirum.com) cloud with `@cloud`. Sign up, create an access token, and set it as `token`. Network latency may require lowering your detection fps. + ui: | + Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**. + + | Field | Value | + | --- | --- | + | **Location** | `@cloud` | + | **Zoo** | `degirum/public` | + | **Token** | your AI Hub token (optional for the public zoo) | + yaml: | + degirum_detector: + type: degirum + location: @cloud + zoo: degirum/public + token: dg_example_token diff --git a/docs/docs/configuration/advanced/reference.md b/docs/docs/configuration/advanced/reference.md new file mode 100644 index 0000000..896ab26 --- /dev/null +++ b/docs/docs/configuration/advanced/reference.md @@ -0,0 +1,1176 @@ +--- +id: reference +title: Full Reference Config +--- + +### Full configuration reference: + +:::warning + +It is not recommended to copy this full configuration file. Only specify values that are different from the defaults. Configuration options and default values may change in future versions. + +::: + +```yaml +mqtt: + # Optional: Enable mqtt server (default: shown below) + enabled: True + # Required: host name + # NOTE: MQTT host can be specified with an environment variable or docker secrets that must begin with 'FRIGATE_'. + # e.g. host: '{FRIGATE_MQTT_HOST}' + host: mqtt.server.com + # Optional: port (default: shown below) + port: 1883 + # Optional: topic prefix (default: shown below) + # NOTE: must be unique if you are running multiple instances + topic_prefix: frigate + # Optional: client id (default: shown below) + # NOTE: must be unique if you are running multiple instances + client_id: frigate + # Optional: user + # NOTE: MQTT user can be specified with an environment variable or docker secrets that must begin with 'FRIGATE_'. + # e.g. user: '{FRIGATE_MQTT_USER}' + user: mqtt_user + # Optional: password + # NOTE: MQTT password can be specified with an environment variable or docker secrets that must begin with 'FRIGATE_'. + # e.g. password: '{FRIGATE_MQTT_PASSWORD}' + password: password + # Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None) + tls_ca_certs: /path/to/ca.crt + # Optional: tls_client_cert and tls_client key in order to use self-signed client + # certificates (default: None) + # NOTE: certificate must not be password-protected + # do not set user and password when using a client certificate + tls_client_cert: /path/to/client.crt + tls_client_key: /path/to/client.key + # Optional: tls_insecure (true/false) for enabling TLS verification of + # the server hostname in the server certificate (default: None) + tls_insecure: false + # Optional: interval in seconds for publishing stats (default: shown below) + stats_interval: 60 + # Optional: QoS level for subscriptions and publishing (default: shown below) + # 0 = at most once + # 1 = at least once + # 2 = exactly once + qos: 0 + +# Optional: Detectors configuration. Defaults to a single CPU detector +detectors: + # Required: name of the detector + detector_name: + # Required: type of the detector + # Frigate provides many types, see https://docs.frigate.video/configuration/object_detectors for more details (default: shown below) + # Additional detector types can also be plugged in. + # Detectors may require additional configuration. + # Refer to the Detectors configuration page for more information. + type: cpu + +# Optional: Database configuration +database: + # The path to store the SQLite DB (default: shown below) + path: /config/frigate.db + +# Optional: TLS configuration +tls: + # Optional: Enable TLS for port 8971 (default: shown below) + enabled: True + +# Optional: Networking configuration +networking: + # Optional: Enable IPv6 on 5000, and 8971 if tls is configured (default: shown below) + ipv6: + enabled: False + # Optional: Override ports Frigate uses for listening (defaults: shown below) + # An IP address may also be provided to bind to a specific interface, e.g. ip:port + # NOTE: This setting is for advanced users and may break some integrations. The majority + # of users should change ports in the docker compose file + # or use the docker run `--publish` option to select a different port. + listen: + internal: 5000 + external: 8971 + +# Optional: Proxy configuration +proxy: + # Optional: Mapping for headers from upstream proxies. Only used if Frigate's auth + # is disabled. + # NOTE: Many authentication proxies pass a header downstream with the authenticated + # user name and role. Not all values are supported. It must be a whitelisted header. + # See the docs for more info. + header_map: + user: x-forwarded-user + role: x-forwarded-groups + role_map: + admin: + - sysadmins + - access-level-security + viewer: + - camera-viewer + # Optional: Url for logging out a user. This sets the location of the logout url in + # the UI. + logout_url: /api/logout + # Optional: Auth secret that is checked against the X-Proxy-Secret header sent from + # the proxy. If not set, all requests are trusted regardless of origin. + auth_secret: None + # Optional: The default role to use for proxy auth. Must be "admin" or "viewer" + default_role: viewer + # Optional: The character used to separate multiple values in the proxy headers. (default: shown below) + separator: "," + +# Optional: Authentication configuration +auth: + # Optional: Enable authentication + enabled: True + # Optional: Reset the admin user password on startup (default: shown below) + # New password is printed in the logs + reset_admin_password: False + # Optional: Cookie to store the JWT token for native auth (default: shown below) + cookie_name: frigate_token + # Optional: Set secure flag on cookie. (default: shown below) + # NOTE: This should be set to True if you are using TLS + cookie_secure: False + # Optional: Session length in seconds (default: shown below) + session_length: 86400 # 24 hours + # Optional: Refresh time in seconds (default: shown below) + # When the session is going to expire in less time than this setting, + # it will be refreshed back to the session_length. + refresh_time: 1800 # 30 minutes + # Optional: Rate limiting for login failures to help prevent brute force + # login attacks (default: shown below) + # See the docs for more information on valid values + failed_login_rate_limit: None + # Optional: Trusted proxies for determining IP address to rate limit + # NOTE: This is only used for rate limiting login attempts and does not bypass + # authentication. See the authentication docs for more details. + trusted_proxies: [] + # Optional: Number of hashing iterations for user passwords + # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256 + # NOTE: changing this value will not automatically update password hashes, you + # will need to change each user password for it to apply + hash_iterations: 600000 + # Optional: Map roles to the list of cameras each role can access (default: none) + # NOTE: An empty list grants the role access to all cameras. Roles defined here can be + # referenced by proxy header role mapping or assigned to native users. + roles: + my_custom_role: + - front_door + - back_yard + +# Optional: model modifications +# NOTE: The default values are for the EdgeTPU detector. +# Other detectors will require the model config to be set. +model: + # Required: path to the model. Frigate+ models use plus:// (default: automatic based on detector) + path: /edgetpu_model.tflite + # Required: path to the labelmap (default: shown below) + labelmap_path: /labelmap.txt + # Required: Object detection model input width (default: shown below) + width: 320 + # Required: Object detection model input height (default: shown below) + height: 320 + # Required: Object detection model input colorspace + # Valid values are rgb, bgr, or yuv. (default: shown below) + input_pixel_format: rgb + # Required: Object detection model input tensor format + # Valid values are nhwc or nchw (default: shown below) + input_tensor: nhwc + # Optional: Data type of the model input tensor + # Valid values are float, float_denorm, or int (default: shown below) + input_dtype: int + # Required: Object detection model type, currently only used with the OpenVINO detector + # Valid values are ssd, yolox, yolonas (default: shown below) + model_type: ssd + # Required: Label name modifications. These are merged into the standard labelmap. + labelmap: + 2: vehicle + # Optional: Map of object labels to their attribute labels (default: depends on model) + attributes_map: + person: + - amazon + - face + car: + - amazon + - fedex + - license_plate + - ups + +# Optional: Audio Events Configuration +# NOTE: Can be overridden at the camera level +audio: + # Optional: Enable audio events (default: shown below) + enabled: False + # Optional: Configure the amount of seconds without detected audio to end the event (default: shown below) + max_not_heard: 30 + # Optional: Configure the min rms volume required to run audio detection (default: shown below) + # As a rule of thumb: + # - 200 - high sensitivity + # - 500 - medium sensitivity + # - 1000 - low sensitivity + min_volume: 500 + # Optional: Number of threads to use for audio detection (default: shown below) + num_threads: 2 + # Optional: Types of audio to listen for (default: shown below) + listen: + - bark + - fire_alarm + - speech + - yell + # Optional: Filters to configure detection. + filters: + # Label that matches label in listen config. + speech: + # Minimum score that triggers an audio event (default: shown below) + threshold: 0.8 + +# Optional: logger verbosity settings +logger: + # Optional: Default log verbosity (default: shown below) + default: info + # Optional: Component specific logger overrides + logs: + frigate.event: debug + +# Optional: set environment variables +environment_vars: + EXAMPLE_VAR: value + +# Optional: birdseye configuration +# NOTE: Can (enabled, mode) be overridden at the camera level +birdseye: + # Optional: Enable birdseye view (default: shown below) + enabled: True + # Optional: Restream birdseye via RTSP (default: shown below) + # NOTE: Enabling this will set birdseye to run 24/7 which may increase CPU usage somewhat. + restream: False + # Optional: Width of the output resolution (default: shown below) + width: 1280 + # Optional: Height of the output resolution (default: shown below) + height: 720 + # Optional: Encoding quality of the mpeg1 feed (default: shown below) + # 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources. + quality: 8 + # Optional: Mode of the view. Available options are: objects, motion, and continuous + # objects - cameras are included if they have had a tracked object within the last 30 seconds + # motion - cameras are included if motion was detected in the last 30 seconds + # continuous - all cameras are included always + mode: objects + # Optional: Threshold for camera activity to stop showing camera (default: shown below) + inactivity_threshold: 30 + # Optional: Configure the birdseye layout + layout: + # Optional: Scaling factor for the layout calculator, range 1.0-5.0 (default: shown below) + scaling_factor: 2.0 + # Optional: Maximum number of cameras to show at one time, showing the most recent (default: show all cameras) + max_cameras: 1 + # Optional: Frames-per-second to re-send the last composed Birdseye frame when idle (no motion or active updates). (default: shown below) + idle_heartbeat_fps: 0.0 + +# Optional: ffmpeg configuration +# More information about presets at https://docs.frigate.video/configuration/ffmpeg_presets +ffmpeg: + # Optional: ffmpeg binary path (default: shown below) + # can also be set to `8.0` or `5.0` to specify one of the included versions + # or can be set to any path that holds `bin/ffmpeg` & `bin/ffprobe` + path: "default" + # Optional: global ffmpeg args (default: shown below) + global_args: -hide_banner -loglevel warning -threads 2 + # Optional: global hwaccel args (default: auto detect) + # NOTE: See hardware acceleration docs for your specific device + hwaccel_args: "auto" + # Optional: global input args (default: shown below) + input_args: preset-rtsp-generic + # Optional: global output args + output_args: + # Optional: output args for detect streams (default: shown below) + detect: -threads 2 -f rawvideo -pix_fmt yuv420p + # Optional: output args for record streams (default: shown below) + record: preset-record-generic + # Optional: Time in seconds to wait before ffmpeg retries connecting to the camera. (default: shown below) + # If set too low, frigate will retry a connection to the camera's stream too frequently, using up the limited streams some cameras can allow at once + # If set too high, then if a ffmpeg crash or camera stream timeout occurs, you could potentially lose up to a maximum of retry_interval second(s) of footage + # NOTE: this can be a useful setting for Wireless / Battery cameras to reduce how much footage is potentially lost during a connection timeout. + retry_interval: 10 + # Optional: Set tag on HEVC (H.265) recording stream to improve compatibility with Apple players. (default: shown below) + apple_compatibility: false + # Optional: Set the index of the GPU to use for hardware acceleration. (default: shown below) + gpu: 0 + +# Optional: Detect configuration +# NOTE: Can be overridden at the camera level +detect: + # Optional: enables detection for the camera (default: shown below) + enabled: False + # Optional: width of the frame for the input with the detect role (default: use native stream resolution) + width: 1280 + # Optional: height of the frame for the input with the detect role (default: use native stream resolution) + height: 720 + # Optional: desired fps for your camera for the input with the detect role (default: shown below) + # NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera. + fps: 5 + # Optional: Number of consecutive detection hits required for an object to be initialized in the tracker. (default: 1/2 the frame rate) + min_initialized: 2 + # Optional: Number of frames without a detection before Frigate considers an object to be gone. (default: 5x the frame rate) + max_disappeared: 25 + # Optional: Configuration for stationary object tracking + stationary: + # Optional: Stationary classifier that uses visual characteristics to determine if an object + # is stationary even if the box changes enough to be considered motion (default: shown below). + classifier: True + # Optional: Frequency for confirming stationary objects (default: same as threshold) + # When set to 1, object detection will run to confirm the object still exists on every frame. + # If set to 10, object detection will run to confirm the object still exists on every 10th frame. + interval: 50 + # Optional: Number of frames without a position change for an object to be considered stationary (default: 10x the frame rate or 10s) + threshold: 50 + # Optional: Define a maximum number of frames for tracking a stationary object (default: not set, track forever) + # This can help with false positives for objects that should only be stationary for a limited amount of time. + # It can also be used to disable stationary object tracking. For example, you may want to set a value for person, but leave + # car at the default. + # WARNING: Setting these values overrides default behavior and disables stationary object tracking. + # There are very few situations where you would want it disabled. It is NOT recommended to + # copy these values from the example config into your config unless you know they are needed. + max_frames: + # Optional: Default for all object types (default: not set, track forever) + default: 3000 + # Optional: Object specific values + objects: + person: 1000 + # Optional: Milliseconds to offset detect annotations by (default: shown below). + # There can often be latency between a recording and the detect process, + # especially when using separate streams for detect and record. + # Use this setting to make the timeline bounding boxes more closely align + # with the recording. The value can be positive or negative. + # TIP: Imagine there is a tracked object clip with a person walking from left to right. + # If the tracked object lifecycle bounding box is consistently to the left of the person + # then the value should be decreased. Similarly, if a person is walking from + # left to right and the bounding box is consistently ahead of the person + # then the value should be increased. + # TIP: This offset is dynamic so you can change the value and it will update existing + # tracked objects, this makes it easy to tune. + # WARNING: Fast moving objects will likely not have the bounding box align. + annotation_offset: 0 + +# Optional: Object configuration +# NOTE: Can be overridden at the camera level +objects: + # Optional: list of objects to track from labelmap.txt (default: shown below) + track: + - person + # Optional: mask to prevent all object types from being detected in certain areas (default: no mask) + # Checks based on the bottom center of the bounding box of the object. + # NOTE: This mask is COMBINED with the object type specific mask below + mask: + # Object filter mask name (required) + mask1: + # Optional: A friendly name for the mask + friendly_name: "Object filter mask area" + # Optional: Whether this mask is active (default: true) + enabled: true + # Required: Coordinates polygon for the mask + coordinates: "0.000,0.000,0.781,0.000,0.781,0.278,0.000,0.278" + # Optional: filters to reduce false positives for specific object types + filters: + person: + # Optional: minimum size of the bounding box for the detected object (default: 0). + # Can be specified as an integer for width*height in pixels or as a decimal representing the percentage of the frame (0.000001 to 0.99). + min_area: 5000 + # Optional: maximum size of the bounding box for the detected object (default: 24000000). + # Can be specified as an integer for width*height in pixels or as a decimal representing the percentage of the frame (0.000001 to 0.99). + max_area: 100000 + # Optional: minimum width/height of the bounding box for the detected object (default: 0) + min_ratio: 0.5 + # Optional: maximum width/height of the bounding box for the detected object (default: 24000000) + max_ratio: 2.0 + # Optional: minimum score for the object to initiate tracking (default: shown below) + min_score: 0.5 + # Optional: minimum decimal percentage for tracked object's computed score to be considered a true positive (default: shown below) + threshold: 0.7 + # Optional: mask to prevent this object type from being detected in certain areas (default: no mask) + # Checks based on the bottom center of the bounding box of the object + mask: + # Object filter mask name (required) + mask1: + # Optional: A friendly name for the mask + friendly_name: "Object filter mask area" + # Optional: Whether this mask is active (default: true) + enabled: true + # Required: Coordinates polygon for the mask + coordinates: "0.000,0.000,0.781,0.000,0.781,0.278,0.000,0.278" + # Optional: Configuration for AI generated tracked object descriptions + genai: + # Optional: Enable AI object description generation (default: shown below) + enabled: False + # Optional: Use the object snapshot instead of thumbnails for description generation (default: shown below) + use_snapshot: False + # Optional: The default prompt for generating descriptions. Can use replacement + # variables like "label", "sub_label", "camera" to make more dynamic. (default: shown below) + prompt: "Describe the {label} in the sequence of images with as much detail as possible. Do not describe the background." + # Optional: Object specific prompts to customize description results + # Format: {label}: {prompt} + object_prompts: + person: "My special person prompt." + # Optional: objects to generate descriptions for (default: all objects that are tracked) + objects: + - person + - cat + # Optional: Restrict generation to objects that entered any of the listed zones (default: none, all zones qualify) + required_zones: [] + # Optional: What triggers to use to send frames for a tracked object to generative AI (default: shown below) + send_triggers: + # Once the object is no longer tracked + tracked_object_end: True + # Optional: After X many significant updates are received (default: shown below) + after_significant_updates: None + # Optional: Save thumbnails sent to generative AI for review/debugging purposes (default: shown below) + debug_save_thumbnails: False + +# Optional: Review configuration +# NOTE: Can be overridden at the camera level +review: + # Optional: alerts configuration + alerts: + # Optional: enables alerts for the camera (default: shown below) + enabled: True + # Optional: labels that qualify as an alert (default: shown below) + labels: + - car + - person + # Time to cutoff alerts after no alert-causing activity has occurred (default: shown below) + cutoff_time: 40 + # Optional: required zones for an object to be marked as an alert (default: none) + # NOTE: when settings required zones globally, this zone must exist on all cameras + # or the config will be considered invalid. In that case the required_zones + # should be configured at the camera level. + required_zones: + - driveway + # Optional: detections configuration + detections: + # Optional: enables detections for the camera (default: shown below) + enabled: True + # Optional: labels that qualify as a detection (default: all labels that are tracked / listened to) + labels: + - car + - person + # Time to cutoff detections after no detection-causing activity has occurred (default: shown below) + cutoff_time: 30 + # Optional: required zones for an object to be marked as a detection (default: none) + # NOTE: when settings required zones globally, this zone must exist on all cameras + # or the config will be considered invalid. In that case the required_zones + # should be configured at the camera level. + required_zones: + - driveway + # Optional: GenAI Review Summary Configuration + genai: + # Optional: Enable the GenAI review summary feature (default: shown below) + enabled: False + # Optional: Enable GenAI review summaries for alerts (default: shown below) + alerts: True + # Optional: Enable GenAI review summaries for detections (default: shown below) + detections: False + # Optional: Activity Context Prompt to give context to the GenAI what activity is and is not suspicious. + # It is important to be direct and detailed. See documentation for the default prompt structure. + activity_context_prompt: """Define what is and is not suspicious +""" + # Optional: Image source for GenAI (default: preview) + # Options: "preview" (uses cached preview frames at ~180p) or "recordings" (extracts frames from recordings at 480p) + # Using "recordings" provides better image quality but uses more tokens per image. + # Frame count is automatically calculated based on context window size, aspect ratio, and image source (capped at 20 frames). + image_source: preview + # Optional: Additional concerns that the GenAI should make note of (default: None) + additional_concerns: + - Animals in the garden + # Optional: Preferred response language (default: English) + preferred_language: English + # Optional: Save thumbnails sent to the GenAI provider for review/debugging purposes (default: shown below) + debug_save_thumbnails: False + +# Optional: Motion configuration +# NOTE: Can be overridden at the camera level +motion: + # Optional: enables detection for the camera (default: True) + # NOTE: Motion detection is required for object detection, + # setting this to False and leaving detect enabled + # will result in an error on startup. + enabled: False + # Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below) + # Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. + # The value should be between 1 and 255. + threshold: 30 + # Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection needs + # to recalibrate and motion checks stop for that frame. Recordings are unaffected. (default: shown below) + # Increasing this value will make motion detection more likely to consider lightning or ir mode changes as valid motion. + # Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching a doorbell camera. + lightning_threshold: 0.8 + # Optional: Fraction of the frame that must change in a single update before motion boxes are completely + # ignored. Values range between 0.0 and 1.0. When exceeded, no motion boxes are reported and **no motion + # recording** is created for that frame. Leave unset (null) to disable this feature. Use with care on PTZ + # cameras or other situations where you require guaranteed frame capture. + skip_motion_threshold: None + # Optional: Minimum size in pixels in the resized motion image that counts as motion (default: shown below) + # Increasing this value will prevent smaller areas of motion from being detected. Decreasing will + # make motion detection more sensitive to smaller moving objects. + # As a rule of thumb: + # - 10 - high sensitivity + # - 30 - medium sensitivity + # - 50 - low sensitivity + contour_area: 10 + # Optional: Alpha blending factor used in frame differencing for motion calculation (default: shown below) + delta_alpha: 0.2 + # Optional: Alpha value passed to cv2.accumulateWeighted when averaging frames to determine the background (default: shown below) + # Higher values mean the current frame impacts the average a lot, and a new object will be averaged into the background faster. + # Low values will cause things like moving shadows to be detected as motion for longer. + # https://www.geeksforgeeks.org/background-subtraction-in-an-image-using-concept-of-running-average/ + frame_alpha: 0.01 + # Optional: Height of the resized motion frame (default: 100) + # Higher values will result in more granular motion detection at the expense of higher CPU usage. + # Lower values result in less CPU, but small changes may not register as motion. + frame_height: 100 + # Optional: motion mask + # NOTE: see docs for more detailed info on creating masks + mask: + # Motion mask name (required) + mask1: + # Optional: A friendly name for the mask + friendly_name: "Motion mask area" + # Optional: Whether this mask is active (default: true) + enabled: true + # Required: Coordinates polygon for the mask + coordinates: "0.000,0.469,1.000,0.469,1.000,1.000,0.000,1.000" + # Optional: improve contrast (default: shown below) + # Enables dynamic contrast improvement. This should help improve night detections at the cost of making motion detection more sensitive + # for daytime. + improve_contrast: True + # Optional: Delay when updating camera motion through MQTT from ON -> OFF (default: shown below). + mqtt_off_delay: 30 + +# Optional: Notification Configuration +# NOTE: Can be overridden at the camera level (except email) +notifications: + # Optional: Enable notification service (default: shown below) + enabled: False + # Optional: Email for push service to reach out to + # NOTE: This is required to use notifications + email: "admin@example.com" + # Optional: Cooldown time for notifications in seconds (default: shown below) + cooldown: 0 + +# Optional: Record configuration +# NOTE: Can be overridden at the camera level +record: + # Optional: Enable recording (default: shown below) + # WARNING: If recording is disabled in the config, turning it on via + # the UI or MQTT later will have no effect. + enabled: False + # Optional: Number of minutes to wait between cleanup runs (default: shown below) + # This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o + expire_interval: 60 + # Optional: Continuous retention settings + continuous: + # Optional: Number of days to retain recordings regardless of tracked objects or motion (default: shown below) + # NOTE: This should be set to 0 and retention should be defined in alerts and detections section below + # if you only want to retain recordings of alerts and detections. + days: 0 + # Optional: Motion retention settings + motion: + # Optional: Number of days to retain recordings regardless of tracked objects (default: shown below) + # NOTE: This should be set to 0 and retention should be defined in alerts and detections section below + # if you only want to retain recordings of alerts and detections. + days: 0 + # Optional: Recording Export Settings + export: + # Optional: Timelapse Output Args (default: shown below). + # NOTE: The default args are set to fit 24 hours of recording into 1 hour playback. + # See https://stackoverflow.com/a/58268695 for more info on how these args work. + # As an example: if you wanted to go from 24 hours to 30 minutes that would be going + # from 86400 seconds to 1800 seconds which would be 1800 / 86400 = 0.02. + # The -r (framerate) dictates how smooth the output video is. + # So the args would be -vf setpts=0.02*PTS -r 30 in that case. + timelapse_args: "-vf setpts=0.04*PTS -r 30" + # Optional: Global hardware acceleration settings for timelapse exports. (default: inherit) + hwaccel_args: auto + # Optional: Maximum number of export jobs to process at the same time (default: shown below) + max_concurrent: 3 + # Optional: Recording Preview Settings + preview: + # Optional: Quality of recording preview (default: shown below). + # Options are: very_low, low, medium, high, very_high + quality: medium + # Optional: alert recording settings + alerts: + # Optional: Number of seconds before the alert to include (default: shown below) + pre_capture: 5 + # Optional: Number of seconds after the alert to include (default: shown below) + post_capture: 5 + # Optional: Retention settings for recordings of alerts + retain: + # Required: Retention days (default: shown below) + days: 10 + # Optional: Mode for retention. (default: shown below) + # all - save all recording segments for alerts regardless of activity + # motion - save all recordings segments for alerts with any detected motion + # active_objects - save all recording segments for alerts with active/moving objects + # + # NOTE: If the retain mode for the camera is more restrictive than the mode configured + # here, the segments will already be gone by the time this mode is applied. + # For example, if the camera retain mode is "motion", the segments without motion are + # never stored, so setting the mode to "all" here won't bring them back. + mode: motion + # Optional: detection recording settings + detections: + # Optional: Number of seconds before the detection to include (default: shown below) + pre_capture: 5 + # Optional: Number of seconds after the detection to include (default: shown below) + post_capture: 5 + # Optional: Retention settings for recordings of detections + retain: + # Required: Retention days (default: shown below) + days: 10 + # Optional: Mode for retention. (default: shown below) + # all - save all recording segments for detections regardless of activity + # motion - save all recordings segments for detections with any detected motion + # active_objects - save all recording segments for detections with active/moving objects + # + # NOTE: If the retain mode for the camera is more restrictive than the mode configured + # here, the segments will already be gone by the time this mode is applied. + # For example, if the camera retain mode is "motion", the segments without motion are + # never stored, so setting the mode to "all" here won't bring them back. + mode: motion + +# Optional: Configuration for the snapshots written to the clips directory for each tracked object +# Timestamp, bounding_box, crop and height settings are applied by default to API requests for snapshots. +# NOTE: Can be overridden at the camera level +snapshots: + # Optional: Enable writing snapshot images to /media/frigate/clips (default: shown below) + enabled: False + # Optional: print a timestamp on the snapshots (default: shown below) + timestamp: False + # Optional: draw bounding box on the snapshots (default: shown below) + bounding_box: True + # Optional: crop the snapshot (default: shown below) + crop: False + # Optional: height to resize the snapshot to (default: original size) + height: 175 + # Optional: Restrict snapshots to objects that entered any of the listed zones (default: no required zones) + required_zones: [] + # Optional: Camera override for retention settings (default: global values) + retain: + # Required: Default retention days (default: shown below) + default: 10 + # Optional: Per object retention days + objects: + person: 15 + # Optional: quality of the encoded snapshot image, 0-100 (default: shown below) + quality: 60 + +# Optional: Configuration for semantic search capability +semantic_search: + # Optional: Enable semantic search (default: shown below) + enabled: False + # Optional: Re-index embeddings database from historical tracked objects (default: shown below) + reindex: False + # Optional: Set the model used for embeddings. (default: shown below) + model: "jinav1" + # Optional: Set the model size used for embeddings. (default: shown below) + # NOTE: small model runs on CPU and large model runs on GPU + model_size: "small" + # Optional: Target a specific device to run the model (default: shown below) + # NOTE: See https://onnxruntime.ai/docs/execution-providers/ for more information + device: None + +# Optional: Configuration for face recognition capability +# NOTE: enabled, min_area can be overridden at the camera level +face_recognition: + # Optional: Enable face recognition (default: shown below) + enabled: False + # Optional: Minimum face distance score required to mark as a potential match (default: shown below) + unknown_score: 0.8 + # Optional: Minimum face detection score required to detect a face (default: shown below) + # NOTE: This only applies when not running a Frigate+ model + detection_threshold: 0.7 + # Optional: Minimum face distance score required to be considered a match (default: shown below) + recognition_threshold: 0.9 + # Optional: Min area of detected face box to consider running face recognition (default: shown below) + min_area: 500 + # Optional: Min face recognitions for the sub label to be applied to the person object (default: shown below) + min_faces: 1 + # Optional: Number of images of recognized faces to save for training (default: shown below) + save_attempts: 200 + # Optional: Apply a blur quality filter to adjust confidence based on the blur level of the image (default: shown below) + blur_confidence_filter: True + # Optional: Set the model size used face recognition. (default: shown below) + model_size: small + # Optional: Target a specific device to run the model (default: shown below) + # NOTE: See https://onnxruntime.ai/docs/execution-providers/ for more information + device: None + +# Optional: Configuration for license plate recognition capability +# NOTE: enabled, min_area, and enhancement can be overridden at the camera level +lpr: + # Optional: Enable license plate recognition (default: shown below) + enabled: False + # Optional: The device to run the models on (default: shown below) + # NOTE: See https://onnxruntime.ai/docs/execution-providers/ for more information + device: CPU + # Optional: Set the model size used for text detection. (default: shown below) + model_size: small + # Optional: License plate object confidence score required to begin running recognition (default: shown below) + detection_threshold: 0.7 + # Optional: Minimum area of license plate to begin running recognition (default: shown below) + min_area: 1000 + # Optional: Recognition confidence score required to add the plate to the object as a sub label (default: shown below) + recognition_threshold: 0.9 + # Optional: Minimum number of characters a license plate must have to be added to the object as a sub label (default: shown below) + min_plate_length: 4 + # Optional: Regular expression for the expected format of a license plate (default: shown below) + format: None + # Optional: Allow this number of missing/incorrect characters to still cause a detected plate to match a known plate + match_distance: 1 + # Optional: Known plates to track (strings or regular expressions) (default: shown below) + known_plates: {} + # Optional: Enhance the detected plate image with contrast adjustment and denoising (default: shown below) + # A value between 0 and 10. Higher values are not always better and may perform worse than lower values. + enhancement: 0 + # Optional: Save plate images to /media/frigate/clips/lpr for debugging purposes (default: shown below) + debug_save_plates: False + # Optional: List of regex replacement rules to normalize detected plates before matching (default: none) + replace_rules: + # Required: regex pattern to match in the detected plate + - pattern: "O" + # Required: string to replace the matched pattern with + replacement: "0" + +# Optional: Configuration for AI / LLM providers +# WARNING: Depending on the provider, this will send thumbnails over the internet +# to Google or OpenAI's LLMs to generate descriptions. GenAI features can be configured at +# the camera level to enhance privacy for indoor cameras. +# NOTE: genai is a map of named providers. Each key is a name you choose for the provider, +# and each role (chat, descriptions, embeddings) may be assigned to exactly one provider. +genai: + # Required: name of the provider (chosen by you, used to reference it elsewhere) + my_provider: + # Required: Provider must be one of ollama, openai, azure_openai, gemini, or llamacpp + provider: ollama + # Required if provider is ollama. May also be used for an OpenAI API compatible backend with the openai provider. + base_url: http://localhost::11434 + # Required if gemini or openai + api_key: "{FRIGATE_GENAI_API_KEY}" + # Required: The model to use with the provider. + model: gemini-1.5-flash + # Optional: Roles this provider handles (default: shown below) + # Each role (chat, descriptions, embeddings) must be assigned to exactly one provider. + roles: + - chat + - descriptions + - embeddings + # Optional additional args to pass to the GenAI Provider (default: None) + provider_options: + keep_alive: -1 + # Optional: Options to pass during inference calls (default: {}) + runtime_options: + temperature: 0.7 + +# Optional: Configuration for audio transcription +# NOTE: only the enabled option can be overridden at the camera level +audio_transcription: + # Optional: Enable live and speech event audio transcription (default: shown below) + enabled: False + # Optional: The device to run the models on for live transcription. (default: shown below) + device: CPU + # Optional: Set the model size used for live transcription. (default: shown below) + model_size: small + # Optional: Set the language used for transcription translation. (default: shown below) + # List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10 + language: en + +# Optional: Configuration for classification models +classification: + # Optional: Configuration for bird classification + bird: + # Optional: Enable bird classification (default: shown below) + enabled: False + # Optional: Minimum classification score required to be considered a match (default: shown below) + threshold: 0.9 + custom: + # Required: name of the classification model + model_name: + # Optional: Enable running the model (default: shown below) + enabled: True + # Optional: Name of classification model (default: shown below) + name: None + # Optional: Classification score threshold to change the state (default: shown below) + threshold: 0.8 + # Optional: Number of classification attempts to save in the recent classifications tab (default: shown below) + # NOTE: Defaults to 200 for object classification and 100 for state classification if not specified + save_attempts: None + # Optional: Object classification configuration + object_config: + # Required: Object types to classify + objects: [dog] + # Optional: Type of classification that is applied (default: shown below) + classification_type: sub_label + # Optional: State classification configuration + state_config: + # Required: Cameras to run classification on + cameras: + camera_name: + # Required: Crop of image frame on this camera to run classification on + crop: [0, 180, 220, 400] + # Optional: If classification should be run when motion is detected in the crop (default: shown below) + motion: False + # Optional: Interval to run classification on in seconds (default: shown below) + interval: None + +# Optional: Restream configuration +# Uses https://github.com/AlexxIT/go2rtc (v1.9.14) +# NOTE: The default go2rtc API port (1984) must be used, +# changing this port for the integrated go2rtc instance is not supported. +go2rtc: + +# Optional: Live stream configuration for WebUI. +# NOTE: Can be overridden at the camera level +live: + # Optional: Set the streams configured in go2rtc + # that should be used for live view in frigate WebUI. (default: name of camera) + # NOTE: In most cases this should be set at the camera level only. + streams: + main_stream: main_stream_name + sub_stream: sub_stream_name + # Optional: Set the height of the jsmpeg stream. (default: 720) + # This must be less than or equal to the height of the detect stream. Lower resolutions + # reduce bandwidth required for viewing the jsmpeg stream. Width is computed to match known aspect ratio. + height: 720 + # Optional: Set the encode quality of the jsmpeg stream (default: shown below) + # 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources. + quality: 8 + +# Optional: in-feed timestamp style configuration +# NOTE: Can be overridden at the camera level +timestamp_style: + # Optional: Position of the timestamp (default: shown below) + # "tl" (top left), "tr" (top right), "bl" (bottom left), "br" (bottom right) + position: "tl" + # Optional: Format specifier conform to the Python package "datetime" (default: shown below) + # Additional Examples: + # german: "%d.%m.%Y %H:%M:%S" + format: "%m/%d/%Y %H:%M:%S" + # Optional: Color of font + color: + # All Required when color is specified (default: shown below) + red: 255 + green: 255 + blue: 255 + # Optional: Line thickness of font (default: shown below) + thickness: 2 + # Optional: Effect of lettering (default: shown below) + # None (No effect), + # "solid" (solid background in inverse color of font) + # "shadow" (shadow for font) + effect: None + +# Required +cameras: + # Required: name of the camera + back: + # Optional: Enable/Disable the camera (default: shown below). + # When False, ffmpeg is not started and the camera is hidden from the UI + # (except Camera Management). Re-enabling requires a Frigate restart. + enabled: True + # Optional: camera type used for some Frigate features (default: shown below) + # Options are "generic" and "lpr" + type: "generic" + # Required: ffmpeg settings for the camera + ffmpeg: + # Required: A list of input streams for the camera. See documentation for more information. + inputs: + # Required: the path to the stream + # NOTE: path may include environment variables or docker secrets, which must begin with 'FRIGATE_' and be referenced in {} + - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + # Required: list of roles for this stream. valid values are: audio,detect,record + # NOTICE: In addition to assigning the audio, detect, and record roles + # they must also be enabled in the camera config. + roles: + - audio + - detect + - record + # Optional: stream specific global args (default: inherit) + # global_args: + # Optional: stream specific hwaccel args (default: inherit) + # hwaccel_args: + # Optional: stream specific input args (default: inherit) + # input_args: + # Optional: camera specific global args (default: inherit) + # global_args: + # Optional: camera specific hwaccel args (default: inherit) + # hwaccel_args: + # Optional: camera specific input args (default: inherit) + # input_args: + # Optional: camera specific output args (default: inherit) + # output_args: + + # Optional: camera specific hwaccel args for timelapse export (default: inherit) + # record: + # export: + # hwaccel_args: + + # Optional: timeout for highest scoring image before allowing it + # to be replaced by a newer image. (default: shown below) + best_image_timeout: 60 + + # Optional: URL to visit the camera web UI directly from the system page. Might not be available on every camera. + webui_url: "" + + # Optional: zones for this camera + zones: + # Required: name of the zone + # NOTE: This must be different than any camera names, but can match with another zone on another + # camera. + front_steps: + # Optional: A friendly name or descriptive text for the zones + friendly_name: "" + # Optional: Whether this zone is active (default: shown below) + # Disabled zones are completely ignored at runtime - no object tracking or debug drawing + enabled: True + # Required: List of x,y coordinates to define the polygon of the zone. + # NOTE: Presence in a zone is evaluated only based on the bottom center of the objects bounding box. + coordinates: 0.033,0.306,0.324,0.138,0.439,0.185,0.042,0.428 + # Optional: The real-world distances of a 4-sided zone used for zones with speed estimation enabled (default: none) + # List distances in order of the zone points coordinates and use the unit system defined in the ui config + distances: 10,15,12,11 + # Optional: Number of consecutive frames required for object to be considered present in the zone (default: shown below). + inertia: 3 + # Optional: Number of seconds that an object must loiter to be considered in the zone (default: shown below) + loitering_time: 0 + # Optional: Minimum speed required for an object to be considered present in the zone (default: none) + # In real-world units if distances are set. Used for speed-based zone triggers. + speed_threshold: 2.5 + # Optional: List of objects that can trigger this zone (default: all tracked objects) + objects: + - person + # Optional: Zone level object filters. + # NOTE: The global and camera filters are applied upstream. + filters: + person: + min_area: 5000 + max_area: 100000 + threshold: 0.7 + + # Optional: Configuration for the jpg snapshots published via MQTT + mqtt: + # Optional: Enable publishing snapshot via mqtt for camera (default: shown below) + # NOTE: Only applies to publishing image data to MQTT via 'frigate///snapshot'. + # All other messages will still be published. + enabled: True + # Optional: print a timestamp on the snapshots (default: shown below) + timestamp: True + # Optional: draw bounding box on the snapshots (default: shown below) + bounding_box: True + # Optional: crop the snapshot (default: shown below) + crop: True + # Optional: height to resize the snapshot to (default: shown below) + height: 270 + # Optional: jpeg encode quality (default: shown below) + quality: 70 + # Optional: Restrict mqtt messages to objects that entered any of the listed zones (default: no required zones) + required_zones: [] + + # Optional: Configuration for how camera is handled in the GUI. + ui: + # Optional: Adjust sort order of cameras in the UI. Larger numbers come later (default: shown below) + # By default the cameras are sorted alphabetically. + order: 0 + # Optional: Whether or not to show the camera in the Frigate UI (default: shown below) + dashboard: True + # Optional: Whether this camera is visible in review (the review page and its camera + # filter, motion review, and the history view) (default: shown below) + review: True + + # Optional: connect to ONVIF camera + # to enable PTZ controls. + onvif: + # Required: host of the camera being connected to. + # NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0". + # NOTE: ONVIF host, user, and password can be specified with environment variables or docker secrets + # that must begin with 'FRIGATE_'. e.g. host: '{FRIGATE_ONVIF_USERNAME}' + host: 0.0.0.0 + # Optional: ONVIF port for device (default: shown below). + port: 8000 + # Optional: username for login. + # NOTE: Some devices require admin to access ONVIF. + user: admin + # Optional: password for login. + password: admin + # Optional: Skip TLS verification and disable digest authentication for the ONVIF server (default: shown below) + tls_insecure: False + # Optional: Ignores time synchronization mismatches between the camera and the server during authentication. + # Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents. + ignore_time_mismatch: False + # Optional: ONVIF media profile to use for PTZ control, matched by token or name. (default: shown below) + # If not set, the first profile with valid PTZ configuration is selected automatically. + # Use this when your camera has multiple ONVIF profiles and you need to select a specific one. + profile: None + # Optional: PTZ camera object autotracking. Keeps a moving object in + # the center of the frame by automatically moving the PTZ camera. + autotracking: + # Optional: enable/disable object autotracking. (default: shown below) + enabled: False + # Optional: calibrate the camera on startup (default: shown below) + # A calibration will move the PTZ in increments and measure the time it takes to move. + # The results are used to help estimate the position of tracked objects after a camera move. + # Frigate will update your config file automatically after a calibration with + # a "movement_weights" entry for the camera. You should then set calibrate_on_startup to False. + calibrate_on_startup: False + # Optional: the mode to use for zooming in/out on objects during autotracking. (default: shown below) + # Available options are: disabled, absolute, and relative + # disabled - don't zoom in/out on autotracked objects, use pan/tilt only + # absolute - use absolute zooming (supported by most PTZ capable cameras) + # relative - use relative zooming (not supported on all PTZs, but makes concurrent pan/tilt/zoom movements) + zooming: disabled + # Optional: A value to change the behavior of zooming on autotracked objects. (default: shown below) + # A lower value will keep more of the scene in view around a tracked object. + # A higher value will zoom in more on a tracked object, but Frigate may lose tracking more quickly. + # The value should be between 0.1 and 0.75 + zoom_factor: 0.3 + # Optional: list of objects to track from labelmap.txt (default: shown below) + track: + - person + # Required: Begin automatically tracking an object when it enters any of the listed zones. + required_zones: + - zone_name + # Required: Name of ONVIF preset in camera's firmware to return to when tracking is over. (default: shown below) + return_preset: home + # Optional: Seconds to delay before returning to preset. (default: shown below) + timeout: 10 + # Optional: Values generated automatically by a camera calibration. Do not modify these manually. (default: shown below) + movement_weights: [] + + # Optional: Configuration for how to sort the cameras in the Birdseye view. + birdseye: + # Optional: Adjust sort order of cameras in the Birdseye view. Larger numbers come later (default: shown below) + # By default the cameras are sorted alphabetically. + order: 0 + + # Optional: Configuration for triggers to automate actions based on semantic search results. + triggers: + # Required: Unique identifier for the trigger (generated automatically from friendly_name if not specified). + trigger_name: + # Required: Enable or disable the trigger. (default: shown below) + enabled: true + # Optional: A friendly name or descriptive text for the trigger + friendly_name: Unique name or descriptive text + # Type of trigger, either `thumbnail` for image-based matching or `description` for text-based matching. (default: none) + type: thumbnail + # Reference data for matching, either an event ID for `thumbnail` or a text string for `description`. (default: none) + data: 1751565549.853251-b69j73 + # Similarity threshold for triggering. (default: shown below) + threshold: 0.8 + # List of actions to perform when the trigger fires. (default: none) + # Available options: + # - `notification` (send a webpush notification) + # - `sub_label` (add trigger friendly name as a sub label to the triggering tracked object) + # - `attribute` (add trigger's name and similarity score as a data attribute to the triggering tracked object) + actions: + - notification + + # Optional: Named config profiles with partial overrides that can be activated at runtime. + # NOTE: Profile names must be defined in the top-level 'profiles' section. + profiles: + # Required: name of the profile (must match a top-level profile definition) + away: + # Optional: Enable or disable the camera when this profile is active (default: not set, inherits base) + enabled: true + # Optional: Override audio settings + audio: + enabled: true + # Optional: Override birdseye settings + # birdseye: + # Optional: Override detect settings + detect: + enabled: true + # Optional: Override face_recognition settings + # face_recognition: + # Optional: Override lpr settings + # lpr: + # Optional: Override motion settings + # motion: + # Optional: Override notification settings + notifications: + enabled: true + # Optional: Override objects settings + objects: + track: + - person + - car + # Optional: Override record settings + record: + enabled: true + # Optional: Override review settings + review: + alerts: + labels: + - person + - car + # Optional: Override snapshot settings + # snapshots: + # Optional: Override or add zones (merged with base zones) + # zones: + +# Optional +ui: + # Optional: Set a timezone to use in the UI (default: use browser local time) + # timezone: America/Denver + # Optional: Set the time format used. + # Options are browser, 12hour, or 24hour (default: shown below) + time_format: browser + # Optional: Set the unit system to either "imperial" or "metric" (default: metric) + # Used in the UI and in MQTT topics + unit_system: metric + +# Optional: Telemetry configuration +telemetry: + # Optional: Enabled network interfaces for bandwidth stats monitoring (default: empty list, let nethogs search all) + network_interfaces: + - eth + - enp + - eno + - ens + - wl + - lo + # Optional: Configure system stats + stats: + # Optional: Enable AMD GPU stats (default: shown below) + amd_gpu_stats: True + # Optional: Enable Intel GPU stats (default: shown below) + intel_gpu_stats: True + # Optional: Treat GPU as SR-IOV to fix GPU stats (default: shown below) + intel_gpu_device: None + # Optional: Enable network bandwidth stats monitoring for camera ffmpeg processes, go2rtc, and object detectors. (default: shown below) + # NOTE: The container must either be privileged or have cap_net_admin, cap_net_raw capabilities enabled. + network_bandwidth: False + # Optional: Enable the latest version outbound check (default: shown below) + # NOTE: If you use the Home Assistant integration, disabling this will prevent it from reporting new versions + version_check: True + +# Optional: Camera groups (default: no groups are setup) +# NOTE: It is recommended to use the UI to setup camera groups +camera_groups: + # Required: Name of camera group + front: + # Required: list of cameras in the group + cameras: + - front_cam + - side_cam + - front_doorbell_cam + # Required: icon used for group + icon: LuCar + # Required: index of this group + order: 0 + +# Optional: Profile definitions for named config overrides +# NOTE: Profile names defined here can be referenced in camera profiles sections +profiles: + # Required: name of the profile (machine name used internally) + home: + # Required: display name shown in the UI + friendly_name: Home + away: + friendly_name: Away +``` diff --git a/docs/docs/configuration/advanced/system.md b/docs/docs/configuration/advanced/system.md new file mode 100644 index 0000000..1497969 --- /dev/null +++ b/docs/docs/configuration/advanced/system.md @@ -0,0 +1,395 @@ +--- +id: system +title: System +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +### Logging + +#### Frigate `logger` + +Change the default log level for troubleshooting purposes. + + + + +Navigate to . + +| Field | Description | +| ------------------------- | ------------------------------------------------------- | +| **Logging level** | The default log level for all modules (default: `info`) | +| **Per-process log level** | Override the log level for specific modules | + + + + +```yaml +logger: + # Optional: default log level (default: shown below) + default: info + # Optional: module by module log level configuration + logs: + frigate.mqtt: error +``` + + + + +Available log levels are: `debug`, `info`, `warning`, `error`, `critical` + +Examples of available modules are: + +- `frigate.app` +- `frigate.mqtt` +- `frigate.object_detection.base` +- `detector.` +- `watchdog.` +- `ffmpeg..` NOTE: All FFmpeg logs are sent as `error` level. + +#### Go2RTC Logging + +See [the go2rtc docs](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#module-log) for logging configuration + +```yaml +go2rtc: + streams: + # ... + log: + exec: trace +``` + +### `environment_vars` + +This section can be used to set environment variables for those unable to modify the environment of the container, like within Home Assistant OS. Docker users should set environment variables in their `docker run` command (`-e FRIGATE_MQTT_PASSWORD=secret`) or `docker-compose.yml` file (`environment:` section) instead. Note that values set here are stored in plain text in your config file, so if the goal is to keep credentials out of your configuration, use Docker environment variables or Docker secrets instead. + +Variables prefixed with `FRIGATE_` can be referenced in config fields that support environment variable substitution (such as MQTT host and credentials, camera stream URLs, and ONVIF host and credentials) using the `{FRIGATE_VARIABLE_NAME}` syntax. + + + + +Navigate to to add or edit environment variables. + +| Field | Description | +| ----------------- | --------------------------------------------------------- | +| **Variable name** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) | +| **Value** | The value for the variable | + +Variables defined here can be referenced elsewhere in your configuration using the `{FRIGATE_VARIABLE_NAME}` syntax. + + + + +```yaml +environment_vars: + FRIGATE_MQTT_USER: my_mqtt_user + FRIGATE_MQTT_PASSWORD: my_mqtt_password + +mqtt: + host: "{FRIGATE_MQTT_HOST}" + user: "{FRIGATE_MQTT_USER}" + password: "{FRIGATE_MQTT_PASSWORD}" +``` + + + + +#### TensorFlow Thread Configuration + +If you encounter thread creation errors during classification model training, you can limit TensorFlow's thread usage: + + + + +Navigate to and add the following variables: + +| Variable | Description | +| --------------------------------- | ---------------------------------------------- | +| `TF_INTRA_OP_PARALLELISM_THREADS` | Threads within operations (`0` = use default) | +| `TF_INTER_OP_PARALLELISM_THREADS` | Threads between operations (`0` = use default) | +| `TF_DATASET_THREAD_POOL_SIZE` | Data pipeline threads (`0` = use default) | + + + + +```yaml +environment_vars: + TF_INTRA_OP_PARALLELISM_THREADS: "2" # Threads within operations (0 = use default) + TF_INTER_OP_PARALLELISM_THREADS: "2" # Threads between operations (0 = use default) + TF_DATASET_THREAD_POOL_SIZE: "2" # Data pipeline threads (0 = use default) +``` + + + + +### `database` + +Tracked object and recording information is managed in a sqlite database at `/config/frigate.db`. If that database is deleted, recordings will be orphaned and will need to be cleaned up manually. They also won't show up in the Media Browser within Home Assistant. + +If you are storing your database on a network share (SMB, NFS, etc), you may get a `database is locked` error message on startup. You can customize the location of the database if necessary. + +This may need to be in a custom location if network storage is used for the media folder. + + + + +Navigate to . + +- Set **Database path** to the custom path for the Frigate database file (default: `/config/frigate.db`) + + + + +```yaml +database: + path: /path/to/frigate.db +``` + + + + +### `model` + +If using a custom model, the width and height will need to be specified. + +Custom models may also require different input tensor formats. The colorspace conversion supports RGB, BGR, or YUV frames to be sent to the object detector. The input tensor shape parameter is an enumeration to match what specified by the model. + +| Tensor Dimension | Description | +| :--------------: | -------------- | +| N | Batch Size | +| H | Model Height | +| W | Model Width | +| C | Color Channels | + +| Available Input Tensor Shapes | +| :---------------------------: | +| "nhwc" | +| "nchw" | + + + + +Navigate to and open the **Custom Model** tab to configure the model path, dimensions, and input format. + +| Field | Description | +| --------------------------------------------- | ------------------------------------ | +| **Custom object detector model path** | Path to the custom model file | +| **Object detection model input width** | Model input width (default: 320) | +| **Object detection model input height** | Model input height (default: 320) | +| **Advanced > Model Input Tensor Shape** | Input tensor shape: `nhwc` or `nchw` | +| **Advanced > Model Input Pixel Color Format** | Pixel format: `rgb`, `bgr`, or `yuv` | + + + + +```yaml +# Optional: model config +model: + path: /path/to/model + width: 320 + height: 320 + input_tensor: "nhwc" + input_pixel_format: "bgr" +``` + + + + +#### `labelmap` + +:::warning + +If the labelmap is customized then the labels used for alerts will need to be adjusted as well. See [alert labels](../review.md#restricting-alerts-to-specific-labels) for more info. + +::: + +The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. By default, truck is renamed to car because they are often confused. You cannot add new object types, but you can change the names of existing objects in the model. + +```yaml +model: + labelmap: + 2: vehicle + 3: vehicle + 5: vehicle + 7: vehicle + 15: animal + 16: animal + 17: animal +``` + +Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well. + +:::warning + +Some labels have special handling and modifications can disable functionality. + +`person` objects are associated with `face` and `amazon` + +`car` objects are associated with `license_plate`, `ups`, `fedex`, `amazon` + +::: + +## Network Configuration + +Frigate exposes a few networking options. IPv6 and the listen ports are set in the `networking` configuration (or from the Settings UI); more advanced changes require [customizing the bundled Nginx configuration](#customizing-the-nginx-configuration). + +### Enabling IPv6 + +By default Frigate listens on IPv4 only. To also listen on IPv6 (on port `5000`, and on `8971` when TLS is configured), enable it in the `networking` configuration. + + + + +Navigate to and enable **IPv6**. + + + + +```yaml +networking: + ipv6: + enabled: true +``` + + + + +### Listen on different ports + +You can change the ports Nginx uses for listening. The internal port (unauthenticated) and external port (authenticated) can be changed independently. You can also specify an IP address using the format `ip:port` if you wish to bind the port to a specific interface. This may be useful for example to prevent exposing the internal port outside the container. + + + + +Navigate to to configure the listen ports. + +| Field | Description | +| ----------------- | --------------------------------------------------------- | +| **Internal port** | The unauthenticated listen address/port (default: `5000`) | +| **External port** | The authenticated listen address/port (default: `8971`) | + + + + +```yaml +networking: + listen: + internal: 127.0.0.1:5000 + external: 8971 +``` + + + + +:::warning + +This setting is for advanced users. For the majority of use cases it's recommended to change the `ports` section of your Docker compose file or use the Docker `run` `--publish` option instead, e.g. `-p 443:8971`. Changing Frigate's ports may break some integrations. + +::: + +### Customizing the Nginx configuration + +More advanced changes to Frigate's internal network configuration can be made by bind mounting your own `nginx.conf` into the container. For example: + +```yaml +services: + frigate: + container_name: frigate + ... + volumes: + ... + - /path/to/your/nginx.conf:/usr/local/nginx/conf/nginx.conf +``` + +## Base path + +By default, Frigate runs at the root path (`/`). However some setups require to run Frigate under a custom path prefix (e.g. `/frigate`), especially when Frigate is located behind a reverse proxy that requires path-based routing. + +### Set Base Path via HTTP Header + +The preferred way to configure the base path is through the `X-Ingress-Path` HTTP header, which needs to be set to the desired base path in an upstream reverse proxy. + +For example, in Nginx: + +``` +location /frigate { + proxy_set_header X-Ingress-Path /frigate; + proxy_pass http://frigate_backend; +} +``` + +### Set Base Path via Environment Variable + +When it is not feasible to set the base path via a HTTP header, it can also be set via the `FRIGATE_BASE_PATH` environment variable in the Docker Compose file. + +For example: + +``` +services: + frigate: + image: blakeblackshear/frigate:latest + environment: + - FRIGATE_BASE_PATH=/frigate +``` + +This can be used for example to access Frigate via a Tailscale agent (https), by simply forwarding all requests to the base path (http): + +``` +tailscale serve --https=443 --bg --set-path /frigate http://localhost:5000/frigate +``` + +## Custom Dependencies + +### Custom ffmpeg build + +Included with Frigate is a build of ffmpeg that works for the vast majority of users. However, there exists some hardware setups which have incompatibilities with the included build. In this case, statically built `ffmpeg` and `ffprobe` binaries can be placed in `/config/custom-ffmpeg/bin` for Frigate to use. + +To do this: + +1. Download your ffmpeg build and uncompress it to the `/config/custom-ffmpeg` folder. Verify that both the `ffmpeg` and `ffprobe` binaries are located in `/config/custom-ffmpeg/bin`. +2. Update the `ffmpeg.path` in your Frigate config to `/config/custom-ffmpeg`. +3. Restart Frigate and the custom version will be used if the steps above were done correctly. + +### Custom go2rtc version + +Frigate currently includes go2rtc v1.9.14, there may be certain cases where you want to run a different version of go2rtc. + +To do this: + +1. Download the go2rtc build to the `/config` folder. +2. Rename the build to `go2rtc`. +3. Give `go2rtc` execute permission. +4. Restart Frigate and the custom version will be used, you can verify by checking go2rtc logs. + +## Validating your config.yml file updates + +When frigate starts up, it checks whether your config file is valid, and if it is not, the process exits. To minimize interruptions when updating your config, you have three options -- you can edit the config via the WebUI which has built in validation, use the config API, or you can validate on the command line using the frigate docker container. + +### Via API + +Frigate can accept a new configuration file as JSON at the `/api/config/save` endpoint. When updating the config this way, Frigate will validate the config before saving it, and return a `400` if the config is not valid. + +```bash +curl -X POST http://frigate_host:5000/api/config/save -d @config.json +``` + +if you'd like you can use your yaml config directly by using [`yq`](https://github.com/mikefarah/yq) to convert it to json: + +```bash +yq -o=json '.' config.yaml | curl -X POST 'http://frigate_host:5000/api/config/save?save_option=saveonly' --data-binary @- +``` + +### Via Command Line + +You can also validate your config at the command line by using the docker container itself. In CI/CD, you leverage the return code to determine if your config is valid, Frigate will return `1` if the config is invalid, or `0` if it's valid. + +```bash +docker run \ + -v $(pwd)/config.yml:/config/config.yml \ + --entrypoint python3 \ + ghcr.io/blakeblackshear/frigate:stable \ + -u -m frigate \ + --validate-config +``` diff --git a/docs/docs/configuration/audio_detectors.md b/docs/docs/configuration/audio_detectors.md new file mode 100644 index 0000000..03c6889 --- /dev/null +++ b/docs/docs/configuration/audio_detectors.md @@ -0,0 +1,305 @@ +--- +id: audio_detectors +title: Audio Detectors +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Frigate provides a builtin audio detector which runs on the CPU. Compared to object detection in images, audio detection is a relatively lightweight operation so the only option is to run the detection on a CPU. + +## Configuration + +Audio events work by detecting a type of audio and creating an event, the event will end once the type of audio has not been heard for the configured amount of time. Audio events save a snapshot at the beginning of the event as well as recordings throughout the event. The recordings are retained using the configured recording retention. + +### Enabling Audio Events + +Audio events can be enabled globally or for specific cameras. + + + + +**Global:** Navigate to and set **Enable audio detection** to on. + +**Per-camera:** Navigate to and set **Enable audio detection** to on for the desired camera. + + + + +```yaml + +audio: # <- enable audio events for all camera + enabled: True + +cameras: + front_camera: + ffmpeg: + ... + audio: + enabled: True # <- enable audio events for the front_camera +``` + + + + +If you are using multiple streams then you must set the `audio` role on the stream that is going to be used for audio detection, this can be any stream but the stream must have audio included. + +:::note + +The ffmpeg process for capturing audio will be a separate connection to the camera along with the other roles assigned to the camera, for this reason it is recommended that the go2rtc restream is used for this purpose. See [the restream docs](/configuration/restream.md) for more information. + +::: + + + + +Navigate to and add an input with the `audio` role pointing to a stream that includes audio. + + + + +```yaml +cameras: + front_camera: + ffmpeg: + inputs: + - path: rtsp://.../main_stream + roles: + - record + - path: rtsp://.../sub_stream # <- this stream must have audio enabled + roles: + - audio + - detect +``` + + + + +### Configuring Minimum Volume + +The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that Frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. The [Debug view](/usage/live#the-single-camera-view) in the Frigate UI has an Audio tab for cameras that have the `audio` role assigned where a graph and the current levels are displayed. The `min_volume` parameter should be set to the minimum the `RMS` level required to run audio detection. + +:::tip + +Volume is considered motion for recordings, this means when the `record -> retain -> mode` is set to `motion` any time audio volume is > min_volume that recording segment for that camera will be kept. + +::: + +### Configuring Audio Events + +The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `fire_alarm`, `speech`, and `yell` are enabled but these can be customized. + + + + +Navigate to . + +- Set **Enable audio detection** to on +- Set **Listen types** to include the audio types you want to detect + + + + +```yaml +audio: + enabled: True + listen: + - bark + - fire_alarm + - speech + - yell +``` + + + + +### Common Audio Labels + +The labelmap includes hundreds of sound types. The labels below are the ones most users may find practical, grouped by what they're typically used for. Use the exact label string from the left column in your `listen` config, or search for the label in the Frigate UI directly. + +Some labels cover several related sounds: `yell` is triggered by shouting, yelling, children shouting, and screaming; `crying` covers baby cries, sobbing, and whimpering; and `speech` covers ordinary talking and conversation. + +**Safety and security** + +| Label | Detects | +| ---------------- | ---------------------------------- | +| `yell` | Shouting, yelling, screaming | +| `fire_alarm` | Fire and smoke alarm sirens | +| `smoke_detector` | Smoke detector beeps | +| `alarm` | General alarm sounds | +| `car_alarm` | Car alarms | +| `siren` | Emergency vehicle and civil sirens | +| `glass` | Glass clinking | +| `shatter` | Breaking glass | +| `breaking` | Something breaking | +| `gunshot` | Gunshots | +| `explosion` | Explosions | + +**People and activity** + +| Label | Detects | +| ----------- | ------------------------ | +| `speech` | Talking and conversation | +| `laughter` | Laughing | +| `crying` | Baby crying and sobbing | +| `cough` | Coughing | +| `footsteps` | Footsteps and walking | +| `knock` | Knocking on a door | +| `doorbell` | Doorbell | +| `ding-dong` | Doorbell chime | + +**Pets and animals** + +| Label | Detects | +| ---------- | ---------------- | +| `bark` | Dog barking | +| `dog` | Other dog sounds | +| `howl` | Howling | +| `growling` | Growling | +| `meow` | Cat meowing | +| `cat` | Other cat sounds | +| `hiss` | Hissing | + +**Vehicles and driveway** + +| Label | Detects | +| ----------------- | -------------------- | +| `car` | Passing cars | +| `honk` | Car horns | +| `truck` | Trucks | +| `reversing_beeps` | Vehicle backup beeps | +| `motorcycle` | Motorcycles | +| `engine_starting` | Engines starting | + +:::tip + +Frequently-heard labels like `speech` can generate a lot of events, and each event could save a snapshot and recording based on your configuration, so start with a focused set and expand from there. The defaults (`bark`, `fire_alarm`, `speech`, `yell`) plus a few of the safety labels above cover most needs. See the [full audio labelmap](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) or the Frigate UI for every available type. + +::: + +### Audio Transcription + +Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI's open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service. Automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background. + +:::info + +Audio transcription requires a one-time internet connection to download the Whisper or Sherpa-ONNX model on first use. Once cached, transcription runs fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details. + +::: + +Transcription accuracy also depends heavily on the quality of your camera's microphone and recording conditions. Many cameras use inexpensive microphones, and distance to the speaker, low audio bitrate, or background noise can significantly reduce transcription quality. If you need higher accuracy, more robust long-running queues, or large-scale automatic transcription, consider using the HTTP API in combination with an automation platform and a cloud transcription service. + +#### Configuration + +To enable transcription, configure it globally and optionally disable for specific cameras. Audio detection must also be enabled as described above. + + + + +**Global:** Navigate to . + +- Set **Enable audio transcription** to on +- Set **Transcription device** to the desired device +- Set **Model size** to the desired size + +**Per-camera:** Navigate to to enable or disable transcription for a specific camera. + + + + +```yaml +audio_transcription: + enabled: True + device: ... + model_size: ... +``` + +Disable audio transcription for select cameras at the camera level: + +```yaml +cameras: + back_yard: + ... + audio_transcription: + enabled: False +``` + + + + +:::note + +Audio detection must be enabled and configured as described above in order to use audio transcription features. + +::: + +The optional config parameters that can be set at the global level include: + +- **`enabled`**: Enable or disable the audio transcription feature. + - Default: `False` + - It is recommended to only configure the features at the global level, and enable it at the individual camera level. +- **`device`**: Device to use to run transcription and translation models. + - Default: `CPU` + - This can be `CPU` or `GPU`. The `sherpa-onnx` models are lightweight and run on the CPU only. The `whisper` models can run on GPU but are only supported on CUDA hardware. +- **`model_size`**: The size of the model used for live transcription. + - Default: `small` + - This can be `small` or `large`. The `small` setting uses `sherpa-onnx` models that are fast, lightweight, and always run on the CPU but are not as accurate as the `whisper` model. + - This config option applies to **live transcription only**. Recorded `speech` events will always use a different `whisper` model (and can be accelerated for CUDA hardware if available with `device: GPU`). +- **`language`**: Defines the language used by `whisper` to translate `speech` audio events (and live audio only if using the `large` model). + - Default: `en` + - You must use a valid [language code](https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10). + - Transcriptions for `speech` events are translated. + - Live audio is translated only if you are using the `large` model. The `small` `sherpa-onnx` model is English-only. + +The only field that is valid at the camera level is `enabled`. + +#### Live transcription + +The single camera Live view in the Frigate UI supports live transcription of audio for streams defined with the `audio` role. Use the Enable/Disable Live Audio Transcription button/switch to toggle transcription processing. When speech is heard, the UI will display a black box over the top of the camera stream with text. The MQTT topic `frigate//audio/transcription` will also be updated in real-time with transcribed text. + +Results can be error-prone due to a number of factors, including: + +- Poor quality camera microphone +- Distance of the audio source to the camera microphone +- Low audio bitrate setting in the camera +- Background noise +- Using the `small` model - it's fast, but not accurate for poor quality audio + +For speech sources close to the camera with minimal background noise, use the `small` model. + +If you have CUDA hardware, you can experiment with the `large` `whisper` model on GPU. Performance is not quite as fast as the `sherpa-onnx` `small` model, but live transcription is far more accurate. Using the `large` model with CPU will likely be too slow for real-time transcription. + +#### Transcription and translation of `speech` audio events + +Any `speech` events in Explore can be transcribed and/or translated through the Transcribe button in the Tracked Object Details pane. + +In order to use transcription and translation for past events, you must enable audio detection and define `speech` as an audio type to listen for. To have `speech` events translated into the language of your choice, set the `language` config parameter with the correct [language code](https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10). + +The transcribed/translated speech will appear in the description box in the Tracked Object Details pane. If Semantic Search is enabled, embeddings are generated for the transcription text and are fully searchable using the description search type. + +:::note + +Only one `speech` event may be transcribed at a time. Frigate does not automatically transcribe `speech` events or implement a queue for long-running transcription model inference. + +::: + +Recorded `speech` events will always use a `whisper` model, regardless of the `model_size` config setting. Without a supported Nvidia GPU, generating transcriptions for longer `speech` events may take a fair amount of time, so be patient. + +#### FAQ + +1. Why doesn't Frigate automatically transcribe all `speech` events? + + Frigate does not implement a queue mechanism for speech transcription, and adding one is not trivial. A proper queue would need backpressure, prioritization, memory/disk buffering, retry logic, crash recovery, and safeguards to prevent unbounded growth when events outpace processing. That's a significant amount of complexity for a feature that, in most real-world environments, would mostly just churn through low-value noise. + + Because transcription is **serialized (one event at a time)** and speech events can be generated far faster than they can be processed, an auto-transcribe toggle would very quickly create an ever-growing backlog and degrade core functionality. For the amount of engineering and risk involved, it adds **very little practical value** for the majority of deployments, which are often on low-powered, edge hardware. + + If you hear speech that's actually important and worth saving/indexing for the future, **just press the transcribe button in Explore** on that specific `speech` event - that keeps things explicit, reliable, and under your control. + + Other options are being considered for future versions of Frigate to add transcription options that support external `whisper` Docker containers. A single transcription service could then be shared by Frigate and other applications (for example, Home Assistant Voice), and run on more powerful machines when available. + +2. Why don't you save live transcription text and use that for `speech` events? + + There's no guarantee that a `speech` event is even created from the exact audio that went through the transcription model. Live transcription and `speech` event creation are **separate, asynchronous processes**. Even when both are correctly configured, trying to align the **precise start and end time of a speech event** with whatever audio the model happened to be processing at that moment is unreliable. + + Automatically persisting that data would often result in **misaligned, partial, or irrelevant transcripts**, while still incurring all of the CPU, storage, and privacy costs of transcription. That's why Frigate treats transcription as an **explicit, user-initiated action** rather than an automatic side-effect of every `speech` event. diff --git a/docs/docs/configuration/authentication.md b/docs/docs/configuration/authentication.md new file mode 100644 index 0000000..a565398 --- /dev/null +++ b/docs/docs/configuration/authentication.md @@ -0,0 +1,422 @@ +--- +id: authentication +title: Authentication +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +# Authentication + +Frigate stores user information in its database. Password hashes are generated using industry standard PBKDF2-SHA256 with 600,000 iterations. Upon successful login, a JWT token is issued with an expiration date and set as a cookie. The cookie is refreshed as needed automatically. This JWT token can also be passed in the Authorization header as a bearer token. + +Users are managed in the UI under Settings > Users. + +The following ports are available to access the Frigate web UI. + +| Port | Description | +| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `8971` | Authenticated UI and API. Reverse proxies should use this port. | +| `5000` | Internal unauthenticated UI and API access. Access to this port should be limited. Intended to be used within the docker network for services that integrate with Frigate and do not support authentication. | + +## Onboarding + +On startup, an admin user and password are generated and printed in the logs. It is recommended to set a new password for the admin account after logging in for the first time under Settings > Users. + +## Resetting admin password + +In the event that you are locked out of your instance, you can tell Frigate to reset the admin password and print it in the logs on next startup. + + + + +Navigate to . + +- Set **Reset admin password** to on to reset the admin password and print it in the logs on next startup + + + + +```yaml +auth: + reset_admin_password: true +``` + + + + +## Password guidance + +Constructing secure passwords and managing them properly is important. Frigate requires a minimum length of 12 characters. For guidance on password standards see [NIST SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html). To learn what makes a password truly secure, read this [article](https://medium.com/peerio/how-to-build-a-billion-dollar-password-3d92568d9277). + +## Login failure rate limiting + +In order to limit the risk of brute force attacks, rate limiting is available for login failures. This is implemented with SlowApi, and the string notation for valid values is available in [the documentation](https://limits.readthedocs.io/en/stable/quickstart.html#examples). + +For example, `1/second;5/minute;20/hour` will rate limit the login endpoint when failures occur more than: + +- 1 time per second +- 5 times per minute +- 20 times per hour + +Restarting Frigate will reset the rate limits. + +If you are running Frigate behind a proxy, you will want to set `trusted_proxies` or these rate limits will apply to the upstream proxy IP address. This means that a brute force attack will rate limit login attempts from other devices and could temporarily lock you out of your instance. In order to ensure rate limits only apply to the actual IP address where the requests are coming from, you will need to list the upstream networks that you want to trust. These trusted proxies are checked against the `X-Forwarded-For` header when looking for the IP address where the request originated. + +If you are running a reverse proxy in the same Docker Compose file as Frigate, configure rate limiting and trusted proxies as follows: + + + + +Navigate to . + +| Field | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| **Failed login limits** | Rate limit string for login failures (e.g., `1/second;5/minute;20/hour`) | +| **Trusted proxies** | List of upstream network CIDRs to trust for `X-Forwarded-For` (e.g., `172.18.0.0/16` for internal Docker Compose network) | + + + + +```yaml +auth: + failed_login_rate_limit: "1/second;5/minute;20/hour" + trusted_proxies: + - 172.18.0.0/16 # <---- this is the subnet for the internal Docker Compose network +``` + + + + +## Session Length + +The default session length for user authentication in Frigate is 24 hours. This setting determines how long a user's authenticated session remains active before a token refresh is required. Otherwise, the user will need to log in again. + +While the default provides a balance of security and convenience, you can customize this duration to suit your specific security requirements and user experience preferences. The session length is configured in seconds. + +The default value of `86400` will expire the authentication session after 24 hours. Some other examples: + +- `0`: Setting the session length to 0 will require a user to log in every time they access the application or after a very short, immediate timeout. +- `604800`: Setting the session length to 604800 will require a user to log in if the token is not refreshed for 7 days. + + + + +Navigate to . + +- Set **Session length** to the duration in seconds before the authentication session expires (default: 86400 / 24 hours) + + + + +```yaml +auth: + session_length: 86400 +``` + + + + +## JWT Token Secret + +The JWT token secret needs to be kept secure. Anyone with this secret can generate valid JWT tokens to authenticate with Frigate. This should be a cryptographically random string of at least 64 characters. + +You can generate a token using the Python secret library with the following command: + +```shell +python3 -c 'import secrets; print(secrets.token_hex(64))' +``` + +Frigate looks for a JWT token secret in the following order: + +1. An environment variable named `FRIGATE_JWT_SECRET` +2. A file named `FRIGATE_JWT_SECRET` in the directory specified by the `CREDENTIALS_DIRECTORY` environment variable (defaults to the Docker Secrets directory: `/run/secrets/`) +3. A `jwt_secret` option from the Home Assistant App options +4. A `.jwt_secret` file in the config directory + +If no secret is found on startup, Frigate generates one and stores it in a `.jwt_secret` file in the config directory. + +Changing the secret will invalidate current tokens. + +## Proxy configuration + +Frigate can be configured to leverage features of common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth. Frigate does not implement OIDC, SAML, or LDAP natively; as an NVR focused on recording and object detection, it relies on robust, battle-tested proxies to handle those protocols and passes the authenticated user and role through via headers (see below). + +If you are leveraging the authentication of an upstream proxy, you likely want to disable Frigate's authentication as there is no correspondence between users in Frigate's database and users authenticated via the proxy. Optionally, if communication between the reverse proxy and Frigate is over an untrusted network, you should set an `auth_secret` in the `proxy` config and configure the proxy to send the secret value as a header named `X-Proxy-Secret`. Assuming this is an untrusted network, you will also want to [configure a real TLS certificate](tls.md) to ensure the traffic can't simply be sniffed to steal the secret. + +To disable Frigate's authentication and ensure requests come only from your known proxy: + + + + +1. Navigate to . + - Set **Enable authentication** to off +2. Navigate to . + - Set **Proxy secret** to `` + + + + +```yaml +auth: + enabled: False + +proxy: + auth_secret: +``` + + + + +You can use the following code to generate a random secret. + +```shell +python3 -c 'import secrets; print(secrets.token_hex(64))' +``` + +### Header mapping + +If you have disabled Frigate's authentication and your proxy supports passing a header with authenticated usernames and/or roles, you can use the `header_map` config to specify the header name so it is passed to Frigate. For example, the following will map the `X-Forwarded-User` and `X-Forwarded-Groups` values. Header names are not case sensitive. Multiple values can be included in the role header. Frigate expects that the character separating the roles is a comma, but this can be specified using the `separator` config entry. + + + + +Navigate to and configure the header mapping and separator settings. + +| Field | Description | +| -------------------------------- | ---------------------------------------------------------------------------------------------------- | +| **Separator character** | Character separating multiple roles in the role header (default: comma). Authentik uses a pipe `\|`. | +| **Header mapping > User header** | Header name for the authenticated username (e.g., `x-forwarded-user`) | +| **Header mapping > Role header** | Header name for the authenticated role/groups (e.g., `x-forwarded-groups`) | + + + + +```yaml +proxy: + ... + separator: "|" # This value defaults to a comma, but Authentik uses a pipe, for example. + header_map: + user: x-forwarded-user + role: x-forwarded-groups +``` + + + + +Frigate supports `admin`, `viewer`, and custom roles (see below). When using port `8971`, Frigate validates these headers and subsequent requests use the headers `remote-user` and `remote-role` for authorization. + +A default role can be provided. Any value in the mapped `role` header will override the default. + + + + +Navigate to and set the default role. + +| Field | Description | +| ---------------- | ------------------------------------------------------------- | +| **Default role** | Fallback role when no role header is present (e.g., `viewer`) | + + + + +```yaml +proxy: + ... + default_role: viewer +``` + + + + +## Role mapping + +In some environments, upstream identity providers (OIDC, SAML, LDAP, etc.) do not pass a Frigate-compatible role directly, but instead pass one or more group claims. To handle this, Frigate supports a `role_map` that translates upstream group names into Frigate's internal roles (`admin`, `viewer`, or custom). This is configurable via YAML in the configuration file: + +```yaml +proxy: + ... + header_map: + user: x-forwarded-user + role: x-forwarded-groups + role_map: + admin: + - sysadmins + - access-level-security + viewer: + - camera-viewer + operator: # Custom role mapping + - operators +``` + +In this example: + +- If the proxy passes a role header containing `sysadmins` or `access-level-security`, the user is assigned the `admin` role. +- If the proxy passes a role header containing `camera-viewer`, the user is assigned the `viewer` role. +- If the proxy passes a role header containing `operators`, the user is assigned the `operator` custom role. +- If no mapping matches, Frigate falls back to `default_role` if configured. +- If `role_map` is not defined, Frigate assumes the role header directly contains `admin`, `viewer`, or a custom role name. + +**Note on matching semantics:** + +- Admin precedence: if the `admin` mapping matches, Frigate resolves the session to `admin` to avoid accidental downgrade when a user belongs to multiple groups (for example both `admin` and `viewer` groups). + +#### Port Considerations + +**Authenticated Port (8971)** + +- Header mapping is **fully supported**. +- The `remote-role` header determines the user's privileges: + - **admin** → Full access (user management, configuration changes). + - **viewer** → Read-only access. + - **Custom roles** → Read-only access limited to the cameras defined in `auth.roles[role]`. +- Ensure your **proxy sends both user and role headers** for proper role enforcement. + +**Unauthenticated Port (5000)** + +- Headers are **ignored** for role enforcement. +- All requests are treated as **anonymous**. +- The `remote-role` value is **overridden** to **admin-level access**. +- This design ensures **unauthenticated internal use** within a trusted network. + +Note that only the following list of headers are permitted by default: + +``` +Remote-User +Remote-Groups +Remote-Email +Remote-Name +X-Forwarded-User +X-Forwarded-Groups +X-Forwarded-Email +X-Forwarded-Preferred-Username +X-authentik-username +X-authentik-groups +X-authentik-email +X-authentik-name +X-authentik-uid +``` + +If you would like to add more options, you can overwrite the default file with a docker bind mount at `/usr/local/nginx/conf/proxy_trusted_headers.conf`. Reference the source code for the default file formatting. + +### Login page redirection + +Frigate gracefully performs login page redirection that should work with most authentication proxies. If your reverse proxy returns a `Location` header on `401`, `302`, or `307` unauthorized responses, Frigate's frontend will automatically detect it and redirect to that URL. + +### Custom logout url + +If your reverse proxy has a dedicated logout url, you can specify using the `logout_url` config option. This will update the link for the `Logout` link in the UI. + +## User Roles + +Frigate supports user roles to control access to certain features in the UI and API, such as managing users or modifying configuration settings. Roles are assigned to users in the database or through proxy headers and are enforced when accessing the UI or API through the authenticated port (`8971`). + +### Supported Roles + +- **admin**: Full access to all features, including user management and configuration. +- **viewer**: Read-only access to the UI and API, including viewing cameras, review items, and historical footage. Configuration editor and settings in the UI are inaccessible. +- **Custom Roles**: Arbitrary role names (alphanumeric, dots/underscores) with specific camera permissions. These extend the system for granular access (e.g., "operator" for select cameras). + +### Custom Roles and Camera Access + +The viewer role provides read-only access to all cameras in the UI and API. Custom roles allow admins to limit read-only access to specific cameras. Each role specifies an array of allowed camera names. If a user is assigned a custom role, their account is like the **viewer** role - they can only view Live, Review/History, Explore, and Export for the designated cameras. Backend API endpoints enforce this server-side (e.g., returning 403 for unauthorized cameras), and the frontend UI filters content accordingly (e.g., camera dropdowns show only permitted options). + +### Role Configuration Example + + + + +Navigate to to define custom roles and assign which cameras each role can access. + + + + +```yaml {11-16} +cameras: + front_door: + # ... camera config + side_yard: + # ... camera config + garage: + # ... camera config + +auth: + enabled: true + roles: + operator: # Custom role + - front_door + - garage # Operator can access front and garage + neighbor: + - side_yard +``` + + + + +If you want to provide access to all cameras to a specific user, just use the **viewer** role. + +### Managing User Roles + +1. Log in as an **admin** user via port `8971` (preferred), or unauthenticated via port `5000`. +2. Navigate to **Settings**. +3. In the **Users** section, edit a user's role by selecting from available roles (admin, viewer, or custom). +4. In the **Roles** section, add/edit/delete custom roles (select cameras via switches). Deleting a role auto-reassigns users to "viewer". + +### Role Enforcement + +When using the authenticated port (`8971`), roles are validated via the JWT token or proxy headers (e.g., `remote-role`). + +On the internal **unauthenticated** port (`5000`), roles are **not enforced**. All requests are treated as **anonymous**, granting access equivalent to the **admin** role without restrictions. + +To use role-based access control, you must connect to Frigate via the **authenticated port (`8971`)** directly or through a reverse proxy. + +### Role Visibility in the UI + +- When logged in via port `8971`, your **username and role** are displayed in the **account menu** (bottom corner). +- When using port `5000`, the UI will always display "anonymous" for the username and "admin" for the role. + +### Managing User Roles + +1. Log in as an **admin** user via port `8971`. +2. Navigate to **Settings > Users**. +3. Edit a user's role by selecting **admin** or **viewer**. + +## API Authentication Guide + +### Getting a Bearer Token + +To use the Frigate API, you need to authenticate first. Follow these steps to obtain a Bearer token: + +#### 1. Login + +Make a POST request to `/login` with your credentials: + +```bash +curl -i -X POST https://frigate_ip:8971/api/login \ + -H "Content-Type: application/json" \ + -d '{"user": "admin", "password": "your_password"}' +``` + +:::note + +You may need to include `-k` in the argument list in these steps (eg: `curl -k -i -X POST ...`) if your Frigate instance is using a self-signed certificate. + +::: + +The response will contain a cookie with the JWT token. + +#### 2. Using the Bearer Token + +Once you have the token, include it in the Authorization header for subsequent requests: + +```bash +curl -H "Authorization: Bearer " https://frigate_ip:8971/api/profile +``` + +#### 3. Token Lifecycle + +- Tokens are valid for the configured session length +- Tokens are automatically refreshed when you visit the `/auth` endpoint +- Tokens are invalidated when the user's password is changed +- Use `/logout` to clear your session cookie diff --git a/docs/docs/configuration/autotracking.md b/docs/docs/configuration/autotracking.md new file mode 100644 index 0000000..f49026e --- /dev/null +++ b/docs/docs/configuration/autotracking.md @@ -0,0 +1,283 @@ +--- +id: autotracking +title: Camera Autotracking +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; +import FaqItem from "@site/src/components/FaqItem"; + +An ONVIF-capable, PTZ (pan-tilt-zoom) camera that supports relative movement within the field of view (FOV) can be configured to automatically track moving objects and keep them in the center of the frame. + +![Autotracking example with zooming](/img/frigate-autotracking-example.gif) + +## Autotracking behavior + +Once Frigate determines that an object is not a false positive and has entered one of the required zones, the autotracker will move the PTZ camera to keep the object centered in the frame until the object either moves out of the frame, the PTZ is not capable of any more movement, or Frigate loses track of it. + +Upon loss of tracking, Frigate will scan the region of the lost object for `timeout` seconds. If an object of the same type is found in that region, Frigate will autotrack that new object. + +When tracking has ended, Frigate will return to the camera firmware's PTZ preset specified by the `return_preset` configuration entry. + +## Checking ONVIF camera support + +Frigate autotracking functions with PTZ cameras capable of relative movement within the field of view (as specified in the [ONVIF spec](https://www.onvif.org/specs/srv/ptz/ONVIF-PTZ-Service-Spec-v1712.pdf) as `RelativePanTiltTranslationSpace` having a `TranslationSpaceFov` entry). + +Many cheaper or older PTZs may not support this standard. Frigate will report an error message in the log and disable autotracking if your PTZ is unsupported. + +The FeatureList on the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/) can provide a starting point to determine a camera's compatibility with Frigate's autotracking. Look to see if a camera lists `PTZRelative`, `PTZRelativePanTilt` and/or `PTZRelativeZoom`. These features are required for autotracking, but some cameras still fail to respond even if they claim support. + +A growing list of cameras and brands that have been reported by users to work with Frigate's autotracking can be found [here](cameras.md). + +## Configuration + +First, set up a PTZ preset in your camera's firmware and give it a name. If you're unsure how to do this, consult the documentation for your camera manufacturer's firmware. Some tutorials for common brands: [Amcrest](https://www.youtube.com/watch?v=lJlE9-krmrM), [Reolink](https://www.youtube.com/watch?v=VAnxHUY5i5w), [Dahua](https://www.youtube.com/watch?v=7sNbc5U-k54). + +Configure the ONVIF connection and autotracking parameters for your camera. Specify the object types to track, a required zone the object must enter to begin autotracking, and the camera preset name you configured in your camera's firmware to return to when tracking has ended. Optionally, specify a delay in seconds before Frigate returns the camera to the preset. + +An [ONVIF connection](cameras.md) is required for autotracking to function. Also, a [motion mask](masks.md) over your camera's timestamp and any overlay text is recommended to ensure they are completely excluded from scene change calculations when the camera is moving. + +Note that `autotracking` is disabled by default but can be enabled in the configuration or by MQTT. + + + + +Navigate to for the desired camera. + +**ONVIF Connection** + +| Field | Description | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **ONVIF host** | Host of the camera being connected to. HTTP is assumed by default; prefix with `https://` for HTTPS. | +| **ONVIF port** | ONVIF port for device (default: 8000) | +| **ONVIF username** | Username for login. Some devices require admin to access ONVIF. | +| **ONVIF password** | Password for login | +| **Disable TLS verify** | Skip TLS verification and disable digest auth for ONVIF (default: false) | +| **ONVIF profile** | ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically. | + +**Autotracking** + +| Field | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| **Enable Autotracking** | Enable or disable object autotracking (default: false) | +| **Calibrate on start** | Calibrate the camera on startup by measuring PTZ motor speed (default: false) | +| **Zoom mode** | Zoom mode during autotracking: `disabled`, `absolute`, or `relative` (default: disabled) | +| **Zoom Factor** | Controls zoom behavior on tracked objects, between 0.1 and 0.75. Lower keeps more scene visible; higher zooms in more (default: 0.3) | +| **Tracked objects** | List of object types to track (default: person) | +| **Required Zones** | Zones an object must enter to begin autotracking | +| **Return Preset** | Name of ONVIF preset in camera firmware to return to when tracking ends (default: home) | +| **Return timeout** | Seconds to delay before returning to preset (default: 10) | + + + + +```yaml +cameras: + ptzcamera: + ... + onvif: + # Required: host of the camera being connected to. + # NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0". + host: 0.0.0.0 + # Optional: ONVIF port for device (default: shown below). + port: 8000 + # Optional: username for login. + # NOTE: Some devices require admin to access ONVIF. + user: admin + # Optional: password for login. + password: admin + # Optional: Skip TLS verification from the ONVIF server (default: shown below) + tls_insecure: False + # Optional: ONVIF media profile to use for PTZ control, matched by token or name. (default: shown below) + # If not set, the first profile with valid PTZ configuration is selected automatically. + # Use this when your camera has multiple ONVIF profiles and you need to select a specific one. + profile: None + # Optional: PTZ camera object autotracking. Keeps a moving object in + # the center of the frame by automatically moving the PTZ camera. + autotracking: + # Optional: enable/disable object autotracking. (default: shown below) + enabled: False + # Optional: calibrate the camera on startup (default: shown below) + # A calibration will move the PTZ in increments and measure the time it takes to move. + # The results are used to help estimate the position of tracked objects after a camera move. + # Frigate will update your config file automatically after a calibration with + # a "movement_weights" entry for the camera. You should then set calibrate_on_startup to False. + calibrate_on_startup: False + # Optional: the mode to use for zooming in/out on objects during autotracking. (default: shown below) + # Available options are: disabled, absolute, and relative + # disabled - don't zoom in/out on autotracked objects, use pan/tilt only + # absolute - use absolute zooming (supported by most PTZ capable cameras) + # relative - use relative zooming (not supported on all PTZs, but makes concurrent pan/tilt/zoom movements) + zooming: disabled + # Optional: A value to change the behavior of zooming on autotracked objects. (default: shown below) + # A lower value will keep more of the scene in view around a tracked object. + # A higher value will zoom in more on a tracked object, but Frigate may lose tracking more quickly. + # The value should be between 0.1 and 0.75 + zoom_factor: 0.3 + # Optional: list of objects to track from labelmap.txt (default: shown below) + track: + - person + # Required: Begin automatically tracking an object when it enters any of the listed zones. + required_zones: + - zone_name + # Required: Name of ONVIF preset in camera's firmware to return to when tracking is over. (default: shown below) + return_preset: home + # Optional: Seconds to delay before returning to preset. (default: shown below) + timeout: 10 + # Optional: Values generated automatically by a camera calibration. Do not modify these manually. (default: shown below) + movement_weights: [] +``` + + + + +## Calibration + +PTZ motors operate at different speeds. Performing a calibration will direct Frigate to measure this speed over a variety of movements and use those measurements to better predict the amount of movement necessary to keep autotracked objects in the center of the frame. + +Calibration is optional, but will greatly assist Frigate in autotracking objects that move across the camera's field of view more quickly. + +To begin calibration, set `calibrate_on_startup` for your camera to `True` and restart Frigate. Frigate will then make a series of small and large movements with your camera. Don't move the PTZ manually while calibration is in progress. Once complete, camera motion will stop and your config file will be automatically updated with a `movement_weights` parameter to be used in movement calculations. You should not modify this parameter manually. + +After calibration has ended, your PTZ will be moved to the preset specified by `return_preset`. + +:::note + +Frigate's web UI and all other cameras will be unresponsive while calibration is in progress. This is expected and normal to avoid excessive network traffic or CPU usage during calibration. Calibration for most PTZs will take about two minutes. The Frigate log will show calibration progress and any errors. + +::: + +At this point, Frigate will be running and will continue to refine and update the `movement_weights` parameter in your config automatically as the PTZ moves during autotracking and more measurements are obtained. + +Before restarting Frigate, you should set `calibrate_on_startup` in your config file to `False`, otherwise your refined `movement_weights` will be overwritten and calibration will occur when starting again. + +You can recalibrate at any time by removing the `movement_weights` parameter, setting `calibrate_on_startup` to `True`, and then restarting Frigate. You may need to recalibrate or remove `movement_weights` from your config altogether if autotracking is erratic. If you change your `return_preset` in any way or if you change your camera's detect `fps` value, a recalibration is also recommended. + +If you initially calibrate with zooming disabled and then enable zooming at a later point, you should also recalibrate. + +## Best practices and considerations + +Every PTZ camera is different, so autotracking may not perform ideally in every situation. This experimental feature was initially developed using an EmpireTech/Dahua SD1A404XB-GNR. + +The object tracker in Frigate estimates the motion of the PTZ so that tracked objects are preserved when the camera moves. In most cases 5 fps is sufficient, but if you plan to track faster moving objects, you may want to increase this slightly. Higher frame rates (> 10fps) will only slow down Frigate and the motion estimator and may lead to dropped frames, especially if you are using experimental zooming. + +A fast [detector](object_detectors.md) is recommended. CPU detectors will not perform well or won't work at all. You can watch Frigate's [debug viewer](/usage/live#the-single-camera-view) for your camera to see a thicker colored box around the object currently being autotracked. + +![Autotracking Debug View](/img/autotracking-debug.gif) + +A full-frame zone in `required_zones` is not recommended, especially if you've calibrated your camera and there are `movement_weights` defined in the configuration file. Frigate will continue to autotrack an object that has entered one of the `required_zones`, even if it moves outside of that zone. + +Some users have found it helpful to adjust the zone `inertia` value. See the [configuration reference](advanced/reference.md). + +## Zooming + +Zooming is a very experimental feature and may use significantly more CPU when tracking objects than panning/tilting only. + +Absolute zooming makes zoom movements separate from pan/tilt movements. Most PTZ cameras will support absolute zooming. Absolute zooming was developed to be very conservative to work best with a variety of cameras and scenes. Absolute zooming usually will not occur until an object has stopped moving or is moving very slowly. + +Relative zooming attempts to make a zoom movement concurrently with any pan/tilt movements. It was tested to work with some Dahua and Amcrest PTZs. But the ONVIF specification indicates that there no assumption about how the generic zoom range is mapped to magnification, field of view or other physical zoom dimension when using relative zooming. So if relative zooming behavior is erratic or just doesn't work, try absolute zooming. + +You can optionally adjust the `zoom_factor` for your camera in your configuration file. Lower values will leave more space from the scene around the tracked object while higher values will cause your camera to zoom in more on the object. However, keep in mind that Frigate needs a fair amount of pixels and scene details outside of the bounding box of the tracked object to estimate the motion of your camera. If the object is taking up too much of the frame, Frigate will not be able to track the motion of the camera and your object will be lost. + +The range of this option is from 0.1 to 0.75. The default value of 0.3 is conservative and should be sufficient for most users. Because every PTZ and scene is different, you should experiment to determine what works best for you. + +## Usage applications + +In security and surveillance, it's common to use "spotter" cameras in combination with your PTZ. When your fixed spotter camera detects an object, you could use an automation platform like Home Assistant to move the PTZ to a specific preset so that Frigate can begin automatically tracking the object. For example: a residence may have fixed cameras on the east and west side of the property, capturing views up and down a street. When the spotter camera on the west side detects a person, a Home Assistant automation could move the PTZ to a camera preset aimed toward the west. When the object enters the specified zone, Frigate's autotracker could then continue to track the person as it moves out of view of any of the fixed cameras. + +## Troubleshooting and FAQ + +### Camera Compatibility + + + +See the community-maintained list of [ONVIF PTZ camera recommendations](cameras.md#onvif-ptz-camera-recommendations) for cameras and brands reported to work (and not work) with autotracking. This is not an exhaustive list that is frequently updated, so other cameras not listed may also work well. Frigate's autotracking was developed with a Dahua SD1A404XB-GNR (now sold as the EmpireTech PTZ1A4M-4X-S2), and Dahua / EmpireTech PTZs are the most consistently reported as working well. + +When comparing models: + +- Verify ONVIF support first. See [Checking ONVIF camera support](#checking-onvif-camera-support) above. +- Favor a camera with a fast PTZ motor. Cameras with slow motors may fail [calibration](#calibration) and will struggle to keep up with objects that move across the field of view quickly. + + + + + +No. Reolink cameras (including the TrackMix series) lack the ONVIF FOV RelativeMove firmware support that Frigate's autotracker requires, so autotracking will not work with any current Reolink PTZ. Their video streams and basic PTZ controls still work in Frigate. If you want object tracking on a Reolink PTZ, you will need to use the tracking feature built into the camera's firmware, which is proprietary and operates independently of Frigate. + + + + + +There are two possible known reasons for this (and perhaps others yet unknown): a slow PTZ motor or buggy camera firmware. Frigate uses an ONVIF parameter provided by the camera, `MoveStatus`, to determine when the PTZ's motor is moving or idle. According to some users, Hikvision PTZs (even with the latest firmware), are not updating this value after PTZ movement. Unfortunately there is no workaround to this bug in Hikvision firmware, so autotracking will not function correctly and should be disabled in your config. This may also be the case with other non-Hikvision cameras utilizing Hikvision firmware, such as some Annke models. In rare cases the vendor may provide fixed firmware on request; for example, Annke has supplied firmware that resolves this for the CZ504 (see the [camera recommendations list](cameras.md#onvif-ptz-camera-recommendations)). + + + + + +Some cameras have firmware that reports that FOV RelativeMove, the ONVIF command that Frigate uses for autotracking, is supported. However, if the camera does not pan or tilt when an object comes into the required zone, your camera's firmware does not actually support FOV RelativeMove. One such camera is the Uniview IPC672LR-AX4DUPK. It actually moves its zoom motor instead of panning and tilting and does not follow the ONVIF standard whatsoever. + + + +### Calibration Issues + + + +This is often caused by the same reason as the "MOVING" status error above - the `MoveStatus` ONVIF parameter is not changing due to a bug in your camera's firmware. Also, see the note above: Frigate's web UI and all other cameras will be unresponsive while calibration is in progress. This is expected and normal. But if you don't see log entries every few seconds for calibration progress, your camera is not compatible with autotracking. + + + + + +Calibration measures the amount of time it takes for Frigate to make a series of movements with your PTZ. This error message is recorded in the log if these values are too high for Frigate to support calibrated autotracking. This is often the case when your camera's motor or network connection is too slow or your camera's firmware doesn't report the motor status in a timely manner. + +Some things to try: + +- If your camera's firmware has a PTZ or motor speed setting, set it to the fastest available speed and calibrate again. +- Run without calibration: remove the `movement_weights` line from your config, set `calibrate_on_startup` to `False`, and restart. + +If calibration consistently fails, this often means your camera's motor is too slow and autotracking will behave unpredictably or won't be able to keep up with moving objects. + + + + + +Frigate uses the `movement_weights` measured during calibration to predict how far the camera needs to move to keep an object centered, so inaccurate values produce movements that don't seem to make sense: overshooting, moving the opposite direction, or zooming in on an object's last known position and losing it entirely. This is almost always a calibration issue. + +- Remove the `movement_weights` entry from your config and restart Frigate to run without calibration. If tracking improves, try recalibrating. +- Recalibrate several times. The `movement_weights` values should be close to each other after each run. If they vary significantly between runs, your camera may not be reporting its motor status reliably, and you may get better results without calibration. +- If you are using zooming, a high `zoom_factor` can cause the camera to zoom in too far and lose the object. Try a lower value. + +Remember to recalibrate whenever you change your `return_preset`, change your camera's detect `fps`, or enable zooming after calibrating with it disabled. + + + +### Tracking Behavior + + + +There are many reasons this could be the case. If you are using experimental zooming, your `zoom_factor` value might be too high, the object might be traveling too quickly, the scene might be too dark, there are not enough details in the scene (for example, a PTZ looking down on a driveway or other monotone background without a sufficient number of hard edges or corners), or the scene is otherwise less than optimal for Frigate to maintain tracking. + +Your camera's shutter speed may also be set too low so that blurring occurs with motion. Check your camera's firmware to see if you can increase the shutter speed. + +Watching Frigate's debug view can help to determine a possible cause. The autotracked object will have a thicker colored box around it. If the camera consistently zooms in on the object and then loses it, see [Autotracking is erratic, moves the camera in the wrong direction, or zooms past my object. Why?](#autotracking-is-erratic-or-moves-the-camera-in-the-wrong-direction) above. + + + + + +To maintain object tracking during PTZ moves, Frigate tracks the motion of your camera based on the details of the frame. If you are seeing this message, it could mean that your `zoom_factor` may be set too high, the scene around your detected object does not have enough details (like hard edges or color variations), or your camera's shutter speed is too slow and motion blur is occurring. Try reducing `zoom_factor`, finding a way to alter the scene around your object, or changing your camera's shutter speed. + + + + + +When the PTZ moves, the entire frame changes at once. Frigate's motion detection treats sudden scene-wide changes (like a lightning flash, an infrared mode switch, or a camera move) specially and pauses detection momentarily until the scene stabilizes. This is expected and normal, and detection resumes shortly after the camera stops moving. If detection does not resume once the camera is stationary, use the [debug view](/usage/live#the-single-camera-view) to see what is happening. + + + + + +Yes. Autotracking can be toggled per camera at runtime over MQTT with the [`frigate//ptz_autotracker/set`](../integrations/mqtt.md#frigatecamera_nameptz_autotrackerset) topic, and the [Home Assistant integration](../integrations/home-assistant.md) exposes a switch for it. This pairs well with the "spotter" camera automations described in [Usage applications](#usage-applications) above, for example only enabling autotracking at night or when nobody is home. + + diff --git a/docs/docs/configuration/bird_classification.md b/docs/docs/configuration/bird_classification.md new file mode 100644 index 0000000..1c52131 --- /dev/null +++ b/docs/docs/configuration/bird_classification.md @@ -0,0 +1,55 @@ +--- +id: bird_classification +title: Bird Classification +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Bird classification identifies known birds using a quantized Tensorflow model. When a known bird is recognized, its common name will be added as a `sub_label`. This information is included in the UI, filters, as well as in notifications. + +:::info + +Bird classification requires a one-time internet connection to download the classification model and label map from GitHub. Once cached, models work fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details. + +::: + +## Minimum System Requirements + +Bird classification runs a lightweight tflite model on the CPU, there are no significantly different system requirements than running Frigate itself. + +## Model + +The classification model used is the MobileNet INat Bird Classification, [available identifiers can be found here.](https://raw.githubusercontent.com/google-coral/test_data/master/inat_bird_labels.txt) + +## Configuration + +Bird classification is disabled by default and must be enabled before it can be used. Bird classification is a global configuration setting. + + + + +Navigate to . + +- Set **Bird classification config > Bird classification** to on +- Set **Bird classification config > Minimum score** to the desired confidence score (default: 0.9) + + + + +```yaml +classification: + bird: + enabled: true +``` + + + + +## Advanced Configuration + +Fine-tune bird classification with these optional parameters: + +- `threshold`: Classification confidence score required to set the sub label on the object. + - Default: `0.9`. diff --git a/docs/docs/configuration/birdseye.md b/docs/docs/configuration/birdseye.md new file mode 100644 index 0000000..26a2386 --- /dev/null +++ b/docs/docs/configuration/birdseye.md @@ -0,0 +1,211 @@ +# Birdseye + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +In addition to Frigate's Live camera dashboard, Birdseye allows a portable heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about. + +Birdseye can be viewed by adding the "Birdseye" camera to a Camera Group in the Web UI. Add a Camera Group by pressing the pencil icon in the sidebar on the Live page, and choose "Birdseye" as one of the cameras. + +Birdseye can also be used in Home Assistant dashboards, cast to media devices, etc. + +:::note + +Each camera tile in Birdseye is composed from the frames of the stream assigned the `detect` role, so a camera's image quality in Birdseye matches its detect stream resolution rather than a higher-resolution recording stream. If a camera looks low quality in Birdseye, increasing the detect width and height (or assigning the `detect` role to a higher-resolution stream) is what affects it. See [setting up camera inputs](./cameras.md#setting-up-camera-inputs) for how roles are assigned. + +::: + +## Birdseye Behavior + +### Birdseye Modes + +Birdseye offers different modes to customize which cameras show under which circumstances. + +- **continuous:** All cameras are always included +- **motion:** Cameras that have detected motion within the last 30 seconds are included +- **objects:** Cameras that have tracked an active object within the last 30 seconds are included + +### Custom Birdseye Icon + +A custom icon can be added to the birdseye background by providing a 180x180 image named `custom.png` inside of the Frigate `media` folder. The file must be a png with the icon as transparent, any non-transparent pixels will be white when displayed in the birdseye view. + +### Birdseye view override at camera level + +To include a camera in Birdseye view only for specific circumstances, or exclude it entirely, configure Birdseye at the camera level. + + + + +**Global settings:** Navigate to to configure the default Birdseye behavior for all cameras. + +**Per-camera overrides:** Navigate to to override the mode or disable Birdseye for a specific camera. + +| Field | Description | +| ------------------- | ------------------------------------------------------------- | +| **Enable Birdseye** | Whether this camera appears in Birdseye view | +| **Tracking mode** | When to show the camera: `continuous`, `motion`, or `objects` | + + + + +```yaml {8-10,12-14} +# Include all cameras by default in Birdseye view +birdseye: + enabled: True + mode: continuous + +cameras: + front: + # Only include the "front" camera in Birdseye view when objects are detected + birdseye: + mode: objects + back: + # Exclude the "back" camera from Birdseye view + birdseye: + enabled: False +``` + + + + +### Birdseye Inactivity + +By default birdseye shows all cameras that have had the configured activity in the last 30 seconds. This threshold can be configured. + + + + +Navigate to . + +| Field | Description | +| ------------------------ | --------------------------------------------------------------------------- | +| **Inactivity threshold** | Seconds of inactivity before a camera is hidden from Birdseye (default: 30) | + + + + +```yaml +birdseye: + enabled: True + # highlight-next-line + inactivity_threshold: 15 +``` + + + + +## Birdseye Layout + +### Birdseye Dimensions + +The resolution and aspect ratio of birdseye can be configured. Resolution will increase the quality but does not affect the layout. Changing the aspect ratio of birdseye does affect how cameras are laid out. + + + + +Navigate to . + +| Field | Description | +| ---------- | ----------------------------------------------- | +| **Width** | Birdseye output width in pixels (default: 1280) | +| **Height** | Birdseye output height in pixels (default: 720) | + + + + +```yaml +birdseye: + enabled: True + width: 1280 + height: 720 +``` + + + + +### Sorting cameras in the Birdseye view + +It is possible to override the order of cameras that are being shown in the Birdseye view. The order is set at the camera level (when using YAML). + + + + +Navigate to and in the **Camera order** field, use the drag handle next to each camera name to control the display order. + + + + +```yaml +# Include all cameras by default in Birdseye view +birdseye: + enabled: True + mode: continuous + +cameras: + front: + birdseye: + # highlight-next-line + order: 1 + back: + birdseye: + # highlight-next-line + order: 2 +``` + + + + +_Note_: Cameras are sorted by default using their name to ensure a constant view inside Birdseye. + +### Birdseye Cameras + +It is possible to limit the number of cameras shown on birdseye at one time. When this is enabled, birdseye will show the cameras with most recent activity. There is a cooldown to ensure that cameras do not switch too frequently. + + + + +Navigate to . + +| Field | Description | +| ------------------------ | ----------------------------------------------------------------------------------- | +| **Layout > Max cameras** | Maximum number of cameras shown at once (e.g., `1` for only the most active camera) | + + + + +```yaml {3-4} +birdseye: + enabled: True + layout: + max_cameras: 1 +``` + + + + +### Birdseye Scaling + +By default birdseye tries to fit 2 cameras in each row and then double in size until a suitable layout is found. The scaling can be configured with a value between 1.0 and 5.0 depending on use case. + + + + +Navigate to . + +| Field | Description | +| --------------------------- | -------------------------------------------------------- | +| **Layout > Scaling factor** | Camera scaling factor between 1.0 and 5.0 (default: 2.0) | + + + + +```yaml {3-4} +birdseye: + enabled: True + layout: + scaling_factor: 3.0 +``` + + + diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md new file mode 100644 index 0000000..27eb729 --- /dev/null +++ b/docs/docs/configuration/camera_specific.md @@ -0,0 +1,316 @@ +--- +id: camera_specific +title: Camera Specific Configurations +--- + +import NavPath from "@site/src/components/NavPath"; + +:::note + +This page makes use of presets of FFmpeg args. For more information on presets, see the [FFmpeg Presets](/configuration/ffmpeg_presets) page. + +::: + +:::note + +Many cameras support encoding options which greatly affect the live view experience, see the [Live view](/configuration/live) page for more info. + +::: + +## H.265 Cameras via Safari + +Some cameras support h265 with different formats, but Safari only supports the annexb format. When using h265 camera streams for recording with devices that use the Safari browser, the `apple_compatibility` option should be used. + +```yaml +cameras: + h265_cam: # <------ Doesn't matter what the camera is called + ffmpeg: + # highlight-next-line + apple_compatibility: true # <- Adds compatibility with MacOS and iPhone +``` + +## MJPEG Cameras + +Note that mjpeg cameras require encoding the video into h264 for recording, and restream roles. This will use significantly more CPU than if the cameras supported h264 feeds directly. It is recommended to use the restream role to create an h264 restream and then use that as the source for ffmpeg. + +```yaml {3,10} +go2rtc: + streams: + mjpeg_cam: "ffmpeg:http://your_mjpeg_stream_url#video=h264#hardware" # <- use hardware acceleration to create an h264 stream usable for other components. + +cameras: + ... + mjpeg_cam: + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/mjpeg_cam + roles: + - detect + - record +``` + +## JPEG Stream Cameras + +Cameras using a live changing jpeg image will need input parameters as below + +```yaml +input_args: preset-http-jpeg-generic +``` + +Outputting the stream will have the same args and caveats as per [MJPEG Cameras](#mjpeg-cameras) + +## RTMP Cameras + +The input parameters need to be adjusted for RTMP cameras + +```yaml +ffmpeg: + input_args: preset-rtmp-generic +``` + +## UDP Only Cameras + +If your cameras do not support TCP connections for RTSP, you can use UDP. + +```yaml +ffmpeg: + input_args: preset-rtsp-udp +``` + +## Model/vendor specific setup + +### Amcrest & Dahua + +Amcrest & Dahua cameras should be connected to via RTSP using the following format: + +``` +rtsp://USERNAME:PASSWORD@CAMERA-IP/cam/realmonitor?channel=1&subtype=0 # this is the main stream +rtsp://USERNAME:PASSWORD@CAMERA-IP/cam/realmonitor?channel=1&subtype=1 # this is the sub stream, typically supporting low resolutions only +rtsp://USERNAME:PASSWORD@CAMERA-IP/cam/realmonitor?channel=1&subtype=2 # higher end cameras support a third stream with a mid resolution (1280x720, 1920x1080) +rtsp://USERNAME:PASSWORD@CAMERA-IP/cam/realmonitor?channel=1&subtype=3 # new higher end cameras support a fourth stream with another mid resolution (1280x720, 1920x1080) + +``` + +### Annke C800 + +This camera is H.265 only. To be able to play clips on some devices (like MacOs or iPhone) the H.265 stream has to be adjusted using the `apple_compatibility` config. + +```yaml +cameras: + annkec800: # <------ Name the camera + ffmpeg: + # highlight-next-line + apple_compatibility: true # <- Adds compatibility with MacOS and iPhone + output_args: + record: preset-record-generic-audio-aac + + inputs: + - path: rtsp://USERNAME:PASSWORD@CAMERA-IP/H264/ch1/main/av_stream # <----- Update for your camera + roles: + - detect + - record + detect: + width: # <- optional, by default Frigate tries to automatically detect resolution + height: # <- optional, by default Frigate tries to automatically detect resolution +``` + +### Blue Iris RTSP Cameras + +You will need to remove `nobuffer` flag for Blue Iris RTSP cameras + +```yaml +ffmpeg: + input_args: preset-rtsp-blue-iris +``` + +### Hikvision Cameras + +Hikvision cameras should be connected to via RTSP using the following format: + +``` +rtsp://USERNAME:PASSWORD@CAMERA-IP/streaming/channels/101 # this is the main stream +rtsp://USERNAME:PASSWORD@CAMERA-IP/streaming/channels/102 # this is the sub stream, typically supporting low resolutions only +rtsp://USERNAME:PASSWORD@CAMERA-IP/streaming/channels/103 # higher end cameras support a third stream with a mid resolution (1280x720, 1920x1080) +``` + +:::note + +[Some users have reported](https://www.reddit.com/r/frigate_nvr/comments/1hg4ze7/hikvision_security_settings) that newer Hikvision cameras require adjustments to the security settings: + +``` +RTSP Authentication - digest/basic +RTSP Digest Algorithm - MD5 +WEB Authentication - digest/basic +WEB Digest Algorithm - MD5 +``` + +::: + +### Reolink Cameras + +Reolink has many different camera models with inconsistently supported features and behavior. The below table shows a summary of various features and recommendations. + +| Camera Resolution | Camera Generation | Recommended Stream Type | Additional Notes | +| ----------------- | ------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------- | +| 5MP or lower | All | http-flv | Stream is h264 | +| 6MP or higher | Latest (ex: Duo3, CX-8##) | http-flv with ffmpeg 8.0, or rtsp | This uses the new http-flv-enhanced over H265 which requires ffmpeg 8.0 (Frigate's default) | +| 6MP or higher | Older (ex: RLC-8##) | rtsp | | + +Frigate works much better with newer Reolink cameras that are setup with the below options: + +If available, recommended settings are: + +- `On, fluency first` this sets the camera to CBR (constant bit rate) +- `Interframe Space 1x` this sets the iframe interval to the same as the frame rate + +#### Setup via the Add Camera Wizard + +The Add Camera Wizard is the recommended way to add a standard Reolink camera. Before starting, make sure [HTTP is enabled](https://support.reolink.com/articles/360003452893-How-to-Access-Reolink-Cameras-NVRs-Home-Hub-Locally-via-Web-Browsers/) in the camera's advanced network settings. The wizard uses the camera's HTTP API to determine its resolution and choose the recommended stream type from the table above. + +1. Click **Add Camera** in . +2. Choose **Manual selection** as the stream detection method and select **Reolink** as the camera brand. +3. The wizard queries the camera and automatically uses an http-flv stream for cameras 5MP and lower, or an RTSP stream for higher resolution cameras. +4. In the validation step, enable **Use stream compatibility mode** for http-flv streams when the wizard recommends it. + +If you use the **Probe camera** method instead, the discovered stream URLs will be RTSP. For Reolink cameras where http-flv is recommended, the wizard will show a warning in the validation step. + +The wizard covers standard single-camera setups. For two way talk, cameras connected through a Reolink NVR, or audio transcoding for WebRTC live view, configure the camera manually as shown below. + +#### Manual configuration + +According to [this discussion](https://github.com/blakeblackshear/frigate/issues/3235#issuecomment-1135876973), the http video streams seem to be the most reliable for Reolink. + +Cameras connected via a Reolink NVR can be connected with the http stream, use `channel[0..15]` in the stream url for the additional channels. +The setup of main stream can be also done via RTSP, but isn't always reliable on all hardware versions. The example configuration is working with the oldest HW version RLN16-410 device with multiple types of cameras. + +
+ Example Config + +:::tip + +Reolink's latest cameras support two way audio via go2rtc and other applications. It is important that the http-flv stream is still used for stability, a secondary rtsp stream can be added that will be using for the two way audio only. + +NOTE: The RTSP stream can not be prefixed with `ffmpeg:`, as go2rtc needs to handle the stream to support two way audio. + +Ensure [HTTP is enabled](https://support.reolink.com/articles/360003452893-How-to-Access-Reolink-Cameras-NVRs-Home-Hub-Locally-via-Web-Browsers/) in the camera's advanced network settings. To use two way talk with Frigate, see the [Live view documentation](/configuration/live#two-way-talk). + +::: + +```yaml +go2rtc: + streams: + # example for connecting to a standard Reolink camera + your_reolink_camera: + - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus" + your_reolink_camera_sub: + - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password" + # example for connecting to a Reolink camera that supports two way talk + your_reolink_camera_twt: + - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus" + - "rtsp://username:password@reolink_ip/Preview_01_sub" + your_reolink_camera_twt_sub: + - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password" + - "rtsp://username:password@reolink_ip/Preview_01_sub" + # example for connecting to a Reolink NVR + your_reolink_camera_via_nvr: + - "ffmpeg:http://reolink_nvr_ip/flv?port=1935&app=bcs&stream=channel3_main.bcs&user=username&password=password" # channel numbers are 0-15 + - "ffmpeg:your_reolink_camera_via_nvr#audio=aac" + your_reolink_camera_via_nvr_sub: + - "ffmpeg:http://reolink_nvr_ip/flv?port=1935&app=bcs&stream=channel3_ext.bcs&user=username&password=password" + +cameras: + your_reolink_camera: + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/your_reolink_camera + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/your_reolink_camera_sub + input_args: preset-rtsp-restream + roles: + - detect + reolink_via_nvr: + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/your_reolink_camera_via_nvr?video=copy&audio=aac + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/your_reolink_camera_via_nvr_sub?video=copy + input_args: preset-rtsp-restream + roles: + - detect +``` + +
+ +### Unifi Protect Cameras + +:::note + +Unifi G5s cameras and newer need a Unifi Protect server to enable rtsps stream, it's not possible to enable it in standalone mode. + +::: + +Unifi protect cameras require the rtspx stream to be used with go2rtc. +To utilize a Unifi protect camera, modify the rtsps link to begin with rtspx. +Additionally, remove the "?enableSrtp" from the end of the Unifi link. + +```yaml +go2rtc: + streams: + front: + - rtspx://192.168.1.1:7441/abcdefghijk +``` + +[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-rtsp) + +In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record if used directly with unifi protect. + +```yaml +ffmpeg: + output_args: + record: preset-record-ubiquiti +``` + +### TP-Link VIGI Cameras + +TP-Link VIGI cameras need some adjustments to the main stream settings on the camera itself to avoid issues. The stream needs to be configured as `H264` with `Smart Coding` set to `off`. Without these settings you may have problems when trying to watch recorded footage. For example Firefox will stop playback after a few seconds and show the following error message: `The media playback was aborted due to a corruption problem or because the media used features your browser did not support.`. + +### Wyze Wireless Cameras + +Some community members have found better performance on Wyze cameras by using an alternative firmware known as [Thingino](https://thingino.com/). + +## USB Cameras (aka Webcams) + +To use a USB camera (webcam) with Frigate, the recommendation is to use go2rtc's [FFmpeg Device](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg-device) support: + +- Preparation outside of Frigate: + - Get USB camera path. Run `v4l2-ctl --list-devices` to get a listing of locally-connected cameras available. (You may need to install `v4l-utils` in a way appropriate for your Linux distribution). In the sample configuration below, we use `video=0` to correlate with a detected device path of `/dev/video0` + - Get USB camera formats & resolutions. Run `ffmpeg -f v4l2 -list_formats all -i /dev/video0` to get an idea of what formats and resolutions the USB Camera supports. In the sample configuration below, we use a width of 1024 and height of 576 in the stream and detection settings based on what was reported back. + - If using Frigate in a container (e.g. Docker on TrueNAS), ensure you have USB Passthrough support enabled, along with a specific Host Device (`/dev/video0`) + Container Device (`/dev/video0`) listed. + +- In your Frigate Configuration File, add the go2rtc stream and roles as appropriate: + +```yaml {4,11-12} +go2rtc: + streams: + usb_camera: + - "ffmpeg:device?video=0&video_size=1024x576#video=h264" + +cameras: + usb_camera: + enabled: true + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/usb_camera + input_args: preset-rtsp-restream + roles: + - detect + - record + detect: + enabled: false # <---- disable detection until you have a working camera feed + width: 1024 + height: 576 +``` diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md new file mode 100644 index 0000000..dc93f30 --- /dev/null +++ b/docs/docs/configuration/cameras.md @@ -0,0 +1,217 @@ +--- +id: cameras +title: Camera Configuration +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +## Setting Up Camera Inputs + +Several inputs can be configured for each camera and the role of each input can be mixed and matched based on your needs. This allows you to use a lower resolution stream for object detection, but create recordings from a higher resolution stream, or vice versa. + +A camera is enabled by default but can be disabled by using `enabled: False`. Cameras that are disabled through the configuration file will not appear in the Frigate UI and will not consume system resources. + +Each role can only be assigned to one input per camera. The options for roles are as follows: + +| Role | Description | +| -------- | ----------------------------------------------------------------------------------- | +| `detect` | Main feed for object detection. [docs](object_detectors.md) | +| `record` | Saves segments of the video feed based on configuration settings. [docs](record.md) | +| `audio` | Feed for audio based detection. [docs](audio_detectors.md) | + + + + +Navigate to . + +| Field | Description | +| ----------------- | ------------------------------------------------------------------- | +| **Camera inputs** | List of input stream definitions (paths and roles) for this camera. | + +For each input you can choose its source: select **Restream (go2rtc)** to pick an existing [go2rtc stream](restream.md) from a dropdown (Frigate uses the `rtsp://127.0.0.1:8554/` path and `preset-rtsp-restream` input args for that input automatically), or **Manual input path** to type the stream URL directly. + +Navigate to . + +| Field | Description | +| ----------------- | ------------------------------------------------------------------------------------------------------ | +| **Detect width** | Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution. | +| **Detect height** | Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution. | + + + + +```yaml +mqtt: + host: mqtt.server.com +cameras: + back: + enabled: True + ffmpeg: + inputs: + - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + roles: + - detect + - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/live + roles: + - record + detect: + width: 1280 # <- optional, by default Frigate tries to automatically detect resolution + height: 720 # <- optional, by default Frigate tries to automatically detect resolution +``` + + + + +Additional cameras are simply added under the camera configuration section. + + + + +Navigate to and use the add camera button to configure each additional camera. + + + + +```yaml +mqtt: ... +cameras: + back: ... + front: ... + side: ... +``` + + + + +:::note + +If you only define one stream in your `inputs` and do not assign a `detect` role to it, Frigate will automatically assign it the `detect` role. Frigate will always decode a stream to support motion detection, Birdseye, the API image endpoints, and other features, even if you have disabled object detection with `enabled: False` in your config's `detect` section. + +If you plan to use Frigate for recording only, it is still recommended to define a `detect` role for a low resolution stream to minimize resource usage from the required stream decoding. + +::: + +For camera model specific settings check the [camera specific](camera_specific.md) infos. + +## Setting up camera PTZ controls + +:::warning + +Not every PTZ supports ONVIF, which is the standard protocol Frigate uses to communicate with your camera. Check the [official list of ONVIF conformant products](https://www.onvif.org/conformant-products/), your camera documentation, or camera manufacturer's website to ensure your PTZ supports ONVIF. Also, ensure your camera is running the latest firmware. + +::: + +Configure the ONVIF connection for your camera to enable PTZ controls. + + + + +1. Navigate to and select your camera. + - Set **ONVIF host** to your camera's IP address, e.g.: `10.0.10.10` + - Set **ONVIF port** to your camera's ONVIF port, e.g.: `8000` + - Set **ONVIF username** to your camera's ONVIF username, e.g.: `admin` + - Set **ONVIF password** to your camera's ONVIF password, e.g.: `password` + + + + +```yaml {4-8} +cameras: + back: + ffmpeg: ... + onvif: + host: 10.0.10.10 + port: 8000 + user: admin + password: password +``` + + + + +If the ONVIF connection is successful, PTZ controls will be available in the camera's WebUI. + +:::note + +Some cameras use a separate ONVIF/service account that is distinct from the device administrator credentials. If ONVIF authentication fails with the admin account, try creating or using an ONVIF/service user in the camera's firmware. Refer to your camera manufacturer's documentation for more. + +::: + +:::tip + +If your ONVIF camera does not require authentication credentials, you may still need to specify an empty string for `user` and `password`, eg: `user: ""` and `password: ""`. + +::: + +If a camera connects but fails to authenticate, two optional fields can help: + +- `tls_insecure`: Skips TLS certificate verification and sends the ONVIF password as plaintext (`PasswordText`) instead of a hashed digest (`PasswordDigest`). Some cameras reject the digest token and only accept plaintext. This weakens connection security, so only enable it on a trusted local network. +- `ignore_time_mismatch`: ONVIF authentication tokens include a timestamp, and a camera will reject the token if its clock differs too much from Frigate's. Enabling this makes Frigate compensate for the time offset so authentication can still succeed. Running NTP on both the camera and the Frigate host is the recommended fix; only use this in a "safe" environment, as it slightly weakens token validation. + +If your camera has multiple ONVIF profiles, you can specify which one to use for PTZ control with the `profile` option, matched by token or name. When not set, Frigate selects the first profile with a valid PTZ configuration. Check the Frigate debug logs (`frigate.ptz.onvif: debug`) to see available profile names and tokens for your camera. + +An ONVIF-capable camera that supports relative movement within the field of view (FOV) can also be configured to automatically track moving objects and keep them in the center of the frame. For autotracking setup, see the [autotracking](autotracking.md) docs. + +## ONVIF PTZ camera recommendations + +This list of working and non-working PTZ cameras is based on user feedback. If you'd like to report specific quirks or issues with a manufacturer or camera that would be helpful for other users, open a pull request to add to this list. + +The FeatureList on the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/) can provide a starting point to determine a camera's compatibility with Frigate's autotracking. Look to see if a camera lists `PTZRelative`, `PTZRelativePanTilt` and/or `PTZRelativeZoom`. These features are required for autotracking, but some cameras still fail to respond even if they claim support. If they are missing, autotracking will not work (though basic PTZ in the WebUI might). Avoid cameras with no database entry unless they are confirmed as working below. + +| Brand or specific camera | PTZ Controls | Autotracking | Notes | +| ---------------------------- | :----------: | :----------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Amcrest | ✅ | ✅ | ⛔️ Generally, Amcrest should work, but some older models (like the common IP2M-841) don't support autotracking | +| Amcrest ASH21 | ✅ | ❌ | ONVIF service port: 80 | +| Amcrest IP4M-S2112EW-AI | ✅ | ❌ | FOV relative movement not supported. | +| Amcrest IP5M-1190EW | ✅ | ❌ | ONVIF Port: 80. FOV relative movement not supported. | +| Annke CZ504 | ✅ | ✅ | Annke support provide specific firmware ([V5.7.1 build 250227](https://github.com/pierrepinon/annke_cz504/raw/refs/heads/main/digicap_V5-7-1_build_250227.dav)) to fix issue with ONVIF "TranslationSpaceFov" | +| Axis Q-6155E | ✅ | ❌ | ONVIF service port: 80; Camera does not support MoveStatus. | +| Ctronics PTZ | ✅ | ❌ | | +| Dahua | ✅ | ✅ | Some low-end Dahuas (lite series, picoo series (commonly), among others) have been reported to not support autotracking. These models usually don't have a four digit model number with chassis prefix and options postfix (e.g. DH-P5AE-PV vs DH-SD49825GB-HNR). | +| Dahua DH-SD2A500HB | ✅ | ❌ | | +| Dahua DH-SD49825GB-HNR | ✅ | ✅ | | +| Dahua DH-P5AE-PV | ❌ | ❌ | | +| Foscam | ✅ | ❌ | In general support PTZ, but not relative move. There are no official ONVIF certifications and tests available on the ONVIF Conformant Products Database | +| Foscam R5 | ✅ | ❌ | | +| Foscam SD4 | ✅ | ❌ | | +| Hanwha XNP-6550RH | ✅ | ❌ | | +| Hikvision | ✅ | ❌ | Incomplete ONVIF support (MoveStatus won't update even on latest firmware) - reported with HWP-N4215IH-DE and DS-2DE3304W-DE, but likely others | +| Hikvision DS-2DE3A404IWG-E/W | ✅ | ✅ | | +| Reolink | ✅ | ❌ | | +| Speco O8P32X | ✅ | ❌ | | +| Sunba 405-D20X | ✅ | ❌ | Incomplete ONVIF support reported on original, and 4k models. All models are suspected incompatible. | +| Tapo | ✅ | ❌ | Many models supported, ONVIF Service Port: 2020 | +| Uniview IPC672LR-AX4DUPK | ✅ | ❌ | Firmware says FOV relative movement is supported, but camera doesn't actually move when sending ONVIF commands | +| Uniview IPC6612SR-X33-VG | ✅ | ✅ | Leave `calibrate_on_startup` as `False`. A user has reported that zooming with `absolute` is working. | +| Vikylin PTZ-2804X-I2 | ❌ | ❌ | Incomplete ONVIF support | + +## Setting up camera groups + +Camera groups let you organize cameras together with a shared name and icon, making it easier to review and filter them. A default group for all cameras is always available. + + + + +On the Live dashboard, press the **pencil icon** in the main navigation to add a new camera group. Configure the group name, select which cameras to include, choose an icon, and set the display order. + + + + +```yaml +camera_groups: + front: + cameras: + - driveway_cam + - garage_cam + icon: LuCar + order: 0 +``` + + + + +## Two-Way Audio + +See the guide [here](/configuration/live/#two-way-talk) diff --git a/docs/docs/configuration/config.md b/docs/docs/configuration/config.md new file mode 100644 index 0000000..edb6d0c --- /dev/null +++ b/docs/docs/configuration/config.md @@ -0,0 +1,382 @@ +--- +id: config +title: Frigate Configuration +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Frigate can be configured through the **Settings UI** or by editing the YAML configuration file directly. The Settings UI is the recommended approach. It provides validation and a guided experience for all configuration options. + +## Using the Settings UI + +The Settings UI groups every configuration option into sections that are listed in the left-hand menu. Each section presents a guided form with validation, so you don't need to remember the structure of the YAML or look up option names by hand. + +### Global vs. camera-level configuration + +Settings are organized into two scopes: + +- **Global configuration**: values under apply to every camera by default. This is where you set the baseline behavior for object detection, recording, snapshots, motion, and so on. +- **Camera configuration**: values under apply to a single camera. Use the camera selector button at the top of these pages to choose which camera you are editing. + +When a camera-level section is left untouched, the camera simply inherits the global values. Changing a value on a camera page **overrides** the global value for that camera only: the global setting and every other camera are unaffected. This mirrors how the YAML works, where a value set under `cameras.` takes precedence over the same value set at the top level. + +To undo an override and go back to inheriting from the parent scope, use the reset button at the bottom of the section: + +- On a camera section, the button is labeled **Reset to Global** and restores the camera to the global value. +- On a global section, the button is labeled **Reset to Default** and restores Frigate's built-in default. + +Resetting asks for confirmation and cannot be undone once applied. + +### Saving changes and the Save All button + +Edits are not applied until you save them. As soon as you change a value, the UI tracks it as a pending change: + +- The edited section shows a **Modified** badge, and the changed fields are highlighted. +- A **You have unsaved changes** notice appears above the section's **Save** and **Undo** buttons. **Save** commits just that section; **Undo** discards its pending edits. + +Because pending changes can span multiple sections (and multiple cameras), the header provides a **Save All** button that writes every pending change at once. Next to it, **Review pending changes** opens a summary that lists each pending edit with its scope (Global or a specific camera), the affected field, and the new value, so you can confirm exactly what will be written before committing. **Undo All** discards every pending change across all sections. + +### Restart-required indicators + +Most settings take effect immediately, but some require Frigate to restart before they apply. Fields that require a restart are marked with a small restart icon and a **Restart required** tooltip next to the field label. + +When you save a change that touches one of these fields, Frigate confirms the save and reminds you that a restart is needed (for example, _"Settings saved successfully. Restart Frigate to apply your changes."_). The notification includes a one-click **Restart Frigate** action so you can apply the change right away, or you can continue editing and restart later. + +### The colored dots in the camera configuration menu + +When you are working under , small colored dots can appear next to a section's name in the menu. They give you an at-a-glance summary of that section's state for the selected camera: + +- **Blue dot**: this section **overrides the global configuration**. One or more values in the section have been set specifically for this camera and differ from the global defaults. +- **Profile-colored dot**: when you are viewing a [camera profile](./profiles.md), a dot in that profile's assigned color indicates the section is **overridden by that profile**. Each profile is given its own distinct color so you can tell at a glance which sections it changes. +- **Amber dot**: this section has **unsaved changes**. It appears alongside the **Modified** badge whenever you have pending edits in the section that haven't been saved yet. + +Hover over any dot to see a tooltip describing what it means. Open a section to see exactly which fields are overridden: the section header indicates how many fields differ from the global (or base) configuration. + +## Configuration File Location + +For users who prefer to edit the YAML configuration file directly, it is recommended to start with a minimal configuration and add to it as described in [the getting started guide](../guides/getting_started.md). + +- **Home Assistant App:** `/addon_configs//config.yml` (see [directory list](#accessing-app-config-dir)) +- **All other installations:** Map to `/config/config.yml` inside the container + +It can be named `config.yml` or `config.yaml`, but if both files exist `config.yml` will be preferred and `config.yaml` will be ignored. + +A minimal starting configuration: + +```yaml +mqtt: + enabled: False + +cameras: + dummy_camera: # <--- this will be changed to your actual camera later + enabled: False + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:554/rtsp + roles: + - detect +``` + +## Accessing the Home Assistant App configuration directory {#accessing-app-config-dir} + +When running Frigate through the HA App, the Frigate `/config` directory is mapped to `/addon_configs/` in the host, where `` is specific to the variant of the Frigate App you are running. + +| App Variant | Configuration directory | +| -------------------------- | ----------------------------------------- | +| Frigate | `/addon_configs/ccab4aaf_frigate` | +| Frigate (Full Access) | `/addon_configs/ccab4aaf_frigate-fa` | +| Frigate Beta | `/addon_configs/ccab4aaf_frigate-beta` | +| Frigate Beta (Full Access) | `/addon_configs/ccab4aaf_frigate-fa-beta` | + +**Whenever you see `/config` in the documentation, it refers to this directory.** + +If for example you are running the standard App variant and use the [VS Code App](https://github.com/hassio-addons/addon-vscode) to browse your files, you can click _File_ > _Open folder..._ and navigate to `/addon_configs/ccab4aaf_frigate` to access the Frigate `/config` directory and edit the `config.yaml` file. You can also use the built-in config editor in the Frigate UI. + +## VS Code Configuration Schema + +VS Code supports JSON schemas for automatically validating configuration files. You can enable this feature by adding `# yaml-language-server: $schema=http://frigate_host:5000/api/config/schema.json` to the beginning of the configuration file. Replace `frigate_host` with the IP address or hostname of your Frigate server. If you're using both VS Code and Frigate as an App, you should use `ccab4aaf-frigate` instead. Make sure to expose the internal unauthenticated port `5000` when accessing the config from VS Code on another machine. + +## Environment Variable Substitution + +Frigate supports the use of environment variables starting with `FRIGATE_` **only** where specifically indicated in the [reference config](./advanced/reference.md). For example, the following values can be replaced at runtime by using environment variables: + +```yaml +mqtt: + host: "{FRIGATE_MQTT_HOST}" + user: "{FRIGATE_MQTT_USER}" + password: "{FRIGATE_MQTT_PASSWORD}" +``` + +```yaml +- path: rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:8554/unicast +``` + +```yaml +onvif: + host: "192.168.1.12" + port: 8000 + user: "{FRIGATE_RTSP_USER}" + password: "{FRIGATE_RTSP_PASSWORD}" +``` + +```yaml +go2rtc: + rtsp: + username: "{FRIGATE_GO2RTC_RTSP_USERNAME}" + password: "{FRIGATE_GO2RTC_RTSP_PASSWORD}" +``` + +```yaml +genai: + api_key: "{FRIGATE_GENAI_API_KEY}" +``` + +## Common configuration examples + +Here are some common starter configuration examples. These can be configured through the Settings UI or via YAML. Refer to the [reference config](./advanced/reference.md) for detailed information about all config values. + +### Raspberry Pi Home Assistant App with USB Coral + +- Single camera with 720p, 5fps stream for detect +- MQTT connected to the Home Assistant Mosquitto App +- Hardware acceleration for decoding video +- USB Coral detector +- Save all video with any detectable motion for 7 days regardless of whether any objects were detected or not +- Continue to keep all video if it qualified as an alert or detection for 30 days +- Save snapshots for 30 days +- Motion mask for the camera timestamp + + + + +1. Navigate to and configure the MQTT connection to your Home Assistant Mosquitto broker +2. Navigate to and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)` +3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb` +4. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion` +5. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30` +6. Navigate to and add your camera with the appropriate RTSP stream URL +7. Navigate to to add a motion mask for the camera timestamp + + + + +```yaml +mqtt: + host: core-mosquitto + user: mqtt-user + password: xxxxxxxxxx + +ffmpeg: + hwaccel_args: preset-rpi-64-h264 + +detectors: + coral: + type: edgetpu + device: usb + +record: + enabled: True + motion: + days: 7 + alerts: + retain: + days: 30 + mode: motion + detections: + retain: + days: 30 + mode: motion + +snapshots: + enabled: True + retain: + default: 30 + +cameras: + name_of_your_camera: + detect: + width: 1280 + height: 720 + fps: 5 + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp + roles: + - detect + motion: + mask: + timestamp: + friendly_name: "Camera timestamp" + enabled: true + coordinates: "0.000,0.427,0.002,0.000,0.999,0.000,0.999,0.781,0.885,0.456,0.700,0.424,0.701,0.311,0.507,0.294,0.453,0.347,0.451,0.400" +``` + + + + +### Standalone Intel Mini PC with USB Coral + +- Single camera with 720p, 5fps stream for detect +- MQTT disabled (not integrated with Home Assistant) +- VAAPI hardware acceleration for decoding video +- USB Coral detector +- Save all video with any detectable motion for 7 days regardless of whether any objects were detected or not +- Continue to keep all video if it qualified as an alert or detection for 30 days +- Save snapshots for 30 days +- Motion mask for the camera timestamp + + + + +1. Navigate to and set **Enable MQTT** to off +2. Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)` +3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb` +4. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion` +5. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30` +6. Navigate to and add your camera with the appropriate RTSP stream URL +7. Navigate to to add a motion mask for the camera timestamp + + + + +```yaml +mqtt: + enabled: False + +ffmpeg: + hwaccel_args: preset-vaapi + +detectors: + coral: + type: edgetpu + device: usb + +record: + enabled: True + motion: + days: 7 + alerts: + retain: + days: 30 + mode: motion + detections: + retain: + days: 30 + mode: motion + +snapshots: + enabled: True + retain: + default: 30 + +cameras: + name_of_your_camera: + detect: + width: 1280 + height: 720 + fps: 5 + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp + roles: + - detect + motion: + mask: + timestamp: + friendly_name: "Camera timestamp" + enabled: true + coordinates: "0.000,0.427,0.002,0.000,0.999,0.000,0.999,0.781,0.885,0.456,0.700,0.424,0.701,0.311,0.507,0.294,0.453,0.347,0.451,0.400" +``` + + + + +### Home Assistant integrated Intel Mini PC with OpenVINO + +- Single camera with 720p, 5fps stream for detect +- MQTT connected to same MQTT server as Home Assistant +- VAAPI hardware acceleration for decoding video +- OpenVINO detector +- Save all video with any detectable motion for 7 days regardless of whether any objects were detected or not +- Continue to keep all video if it qualified as an alert or detection for 30 days +- Save snapshots for 30 days +- Motion mask for the camera timestamp + + + + +1. Navigate to and configure the connection to your MQTT broker +2. Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)` +3. Navigate to and add a detector with **Type** `openvino` and **Device** `AUTO` +4. On the same page, in the **Custom Model** tab, configure the OpenVINO model path and settings +5. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion` +6. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30` +7. Navigate to and add your camera with the appropriate RTSP stream URL +8. Navigate to to add a motion mask for the camera timestamp + + + + +```yaml +mqtt: + host: 192.168.X.X # <---- same mqtt broker that home assistant uses + user: mqtt-user + password: xxxxxxxxxx + +ffmpeg: + hwaccel_args: preset-vaapi + +detectors: + ov: + type: openvino + device: AUTO + +model: + width: 300 + height: 300 + input_tensor: nhwc + input_pixel_format: bgr + path: /openvino-model/ssdlite_mobilenet_v2.xml + labelmap_path: /openvino-model/coco_91cl_bkgr.txt + +record: + enabled: True + motion: + days: 7 + alerts: + retain: + days: 30 + mode: motion + detections: + retain: + days: 30 + mode: motion + +snapshots: + enabled: True + retain: + default: 30 + +cameras: + name_of_your_camera: + detect: + width: 1280 + height: 720 + fps: 5 + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp + roles: + - detect + motion: + mask: + timestamp: + friendly_name: "Camera timestamp" + enabled: true + coordinates: "0.000,0.427,0.002,0.000,0.999,0.000,0.999,0.781,0.885,0.456,0.700,0.424,0.701,0.311,0.507,0.294,0.453,0.347,0.451,0.400" +``` + + + diff --git a/docs/docs/configuration/custom_classification/object_classification.md b/docs/docs/configuration/custom_classification/object_classification.md new file mode 100644 index 0000000..2bfe2c3 --- /dev/null +++ b/docs/docs/configuration/custom_classification/object_classification.md @@ -0,0 +1,195 @@ +--- +id: object_classification +title: Object Classification +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Object classification allows you to train a custom MobileNetV2 classification model to run on tracked objects (persons, cars, animals, etc.) to identify a finer category or attribute for that object. Classification results are visible in the Tracked Object Details pane in Explore, through the `frigate/tracked_object_details` MQTT topic, in Home Assistant sensors via the official Frigate integration, or through the event endpoints in the HTTP API. + +:::info + +Training a custom object classification model requires a one-time internet connection to download MobileNetV2 base weights. Once trained, the model runs fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details. + +::: + +## Minimum System Requirements + +Object classification models are lightweight and run very fast on CPU. + +Training the model does briefly use a high amount of system resources for about 1-3 minutes per training run. On lower-power devices, training may take longer. + +A CPU with AVX + AVX2 instructions is required for training and inference. + +## Classes + +Classes are the categories your model will learn to distinguish between. Each class represents a distinct visual category that the model will predict. + +For object classification: + +- Define classes that represent different types or attributes of the detected object +- Examples: For `person` objects, classes might be `delivery_person`, `resident`, `stranger` +- Include a `none` class for objects that don't fit any specific category +- Keep classes visually distinct to improve accuracy + +### Classification Type + +- **Sub label**: + - Applied to the object's `sub_label` field. + - Ideal for a single, more specific identity or type. + - Example: `cat` → `Leo`, `Charlie`, `None`. + +- **Attribute**: + - Added as metadata to the object, visible in the Tracked Object Details pane in Explore, `frigate/events` MQTT messages, and the HTTP API response as `: `. + - Ideal when multiple attributes can coexist independently. + - Example: Detecting if a `person` in a construction yard is wearing a helmet or not, and if they are wearing a yellow vest or not. + +:::note + +A tracked object can only have a single sub label. If you are using Triggers or Face Recognition and you configure an object classification model for `person` using the sub label type, your sub label may not be assigned correctly as it depends on which enrichment completes its analysis first. This could also occur with `car` objects that are assigned a sub label for a delivery carrier. Consider using the `attribute` type instead. + +::: + +## Assignment Requirements + +Sub labels and attributes are only assigned when both conditions are met: + +1. **Threshold**: Each classification attempt must have a confidence score that meets or exceeds the configured `threshold` (default: `0.8`). +2. **Class Consensus**: After at least 3 classification attempts, 60% of attempts must agree on the same class label. If the consensus class is `none`, no assignment is made. + +This two-step verification prevents false positives by requiring consistent predictions across multiple frames before assigning a sub label or attribute. + +## Example use cases + +### Sub label + +- **Known pet vs unknown**: For `dog` objects, set sub label to your pet's name (e.g., `buddy`) or `none` for others. +- **Mail truck vs normal car**: For `car`, classify as `mail_truck` vs `car` to filter important arrivals. +- **Delivery vs non-delivery person**: For `person`, classify `delivery` vs `visitor` based on uniform/props. + +### Attributes + +- **Backpack**: For `person`, add attribute `backpack: yes/no`. +- **Helmet**: For `person` (worksite), add `helmet: yes/no`. +- **Leash**: For `dog`, add `leash: yes/no` (useful for park or yard rules). +- **Ladder rack**: For `truck`, add `ladder_rack: yes/no` to flag service vehicles. + +## Configuration + +Object classification is configured as a custom classification model. Each model has its own name and settings. Specify which object labels should be classified. + + + + +Navigate to the **Classification** page from the main navigation sidebar, then click **Add Classification**. + +In the **Create New Classification** dialog: + +| Field | Description | +| ----------------------- | ------------------------------------------------------------- | +| **Name** | A name for your classification model (e.g., `dog`) | +| **Type** | Select **Object** for object classification | +| **Object Label** | The object label to classify (e.g., `dog`, `person`, `car`) | +| **Classification Type** | Whether to assign results as a **Sub Label** or **Attribute** | +| **Classes** | The class names the model will learn to distinguish between | + +The `threshold` (default: `0.8`) can be adjusted in the YAML configuration. + + + + +```yaml +classification: + custom: + dog: + threshold: 0.8 + object_config: + objects: [dog] # object labels to classify + classification_type: sub_label # or: attribute +``` + +An optional config, `save_attempts`, can be set as a key under the model name. This defines the number of classification attempts to save in the Recent Classifications tab. For object classification models, the default is 200. + + + + +## Training the model + +Creating and training the model is done within the Frigate UI using the `Classification` page. The process consists of two steps: + +### Step 1: Name and Define + +Enter a name for your model, select the object label to classify (e.g., `person`, `dog`, `car`), choose the classification type (sub label or attribute), and define your classes. Frigate will automatically include a `none` class for objects that don't fit any specific category. + +For example: To classify your two cats, create a model named "Our Cats" and create two classes, "Charlie" and "Leo". A third class, "none", will be created automatically for other neighborhood cats that are not your own. + +### Step 2: Assign Training Examples + +The system will automatically generate example images from detected objects matching your selected label. You'll be guided through each class one at a time to select which images represent that class. Any images not assigned to a specific class will automatically be assigned to `none` when you complete the last class. Once all images are processed, training will begin automatically. + +When choosing which objects to classify, start with a small number of visually distinct classes and ensure your training samples match camera viewpoints and distances typical for those objects. + +If examples for some of your classes do not appear in the grid, you can continue configuring the model without them. New images will begin to appear in the Recent Classifications view. When your missing classes are seen, classify them from this view and retrain your model. + +### Improving the Model + +:::tip Diversity matters far more than volume + +Selecting dozens of nearly identical images is one of the fastest ways to degrade model performance. MobileNetV2 can overfit quickly when trained on homogeneous data. The model learns what _that exact moment_ looked like rather than what actually defines the class. **This is why Frigate does not implement bulk training in the UI.** + +For more detail, see [Frigate Tip: Best Practices for Training Face and Custom Classification Models](https://github.com/blakeblackshear/frigate/discussions/21374). + +::: + +- **Start small and iterate**: Begin with a small, representative set of images per class. Models often begin working well with surprisingly few examples and improve naturally over time. +- **Favor hard examples**: When images appear in the Recent Classifications tab, prioritize images scoring below 90-100% or those captured under new lighting, weather, or distance conditions. +- **Avoid bulk training similar images**: Training large batches of images that already score 100% (or close) adds little new information and increases the risk of overfitting. +- **The wizard is just the starting point**: You don't need to find and label every class upfront. Missing classes will naturally appear in Recent Classifications, and those images tend to be more valuable because they represent new conditions and edge cases. +- **Problem framing**: Keep classes visually distinct and relevant to the chosen object types. +- **Preprocessing**: Ensure examples reflect object crops similar to Frigate's boxes; keep the subject centered. +- **Crop size**: Aim for crops of at least 100×100 pixels (a 10,000 pixel area). Crops smaller than ~80×80 get stretched 3-7× by the model's 224×224 input resize and tend to collapse into a generic "blob" region of feature space where identity becomes unreliable. If most of your detections are small because the camera is far from the subject, consider repositioning the camera for closer crops. +- **Class balance**: Aim to keep your largest class within ~3× the count of your smallest. Beyond that, the model becomes biased toward the dominant class and tends to default borderline predictions to it (the "everything looks like Buddy" failure mode). +- **Threshold**: Tune `threshold` per model to reduce false assignments. Start at `0.8` and adjust based on validation. + +:::tip `none` works differently from named classes + +Named classes work best with visually uniform examples. Every Buddy photo should look like Buddy. The `none` class needs the opposite: visual diversity across sizes, framings, and qualities, because at inference it has to absorb everything that isn't one of your named classes. Don't apply the same "only keep large, well-framed images" rule to `none` that you would to a named class. Mix in small crops, partial views, and false positives deliberately - otherwise the model has no signal for "small/ambiguous thing = not one of my known classes" and will force those crops into a named class by default. + +::: + +## Debugging Classification Models + +To troubleshoot issues with object classification models, enable debug logging to see detailed information about classification attempts, scores, and consensus calculations. + +Enable debug logs for classification models by adding `frigate.data_processing.real_time.custom_classification: debug` to your `logger` configuration. These logs are verbose, so only keep this enabled when necessary. Restart Frigate after this change. + + + + +Navigate to . + +- Set **Logging level** to `debug` +- Set **Per-process log level > `frigate.data_processing.real_time.custom_classification`** to `debug` for verbose classification logging + + + + +```yaml +logger: + default: info + logs: + # highlight-next-line + frigate.data_processing.real_time.custom_classification: debug +``` + + + + +The debug logs will show: + +- Classification probabilities for each attempt +- Whether scores meet the threshold requirement +- Consensus calculations and when assignments are made +- Object classification history and weighted scores diff --git a/docs/docs/configuration/custom_classification/state_classification.md b/docs/docs/configuration/custom_classification/state_classification.md new file mode 100644 index 0000000..7d47994 --- /dev/null +++ b/docs/docs/configuration/custom_classification/state_classification.md @@ -0,0 +1,164 @@ +--- +id: state_classification +title: State Classification +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +State classification allows you to train a custom MobileNetV2 classification model on a fixed region of your camera frame(s) to determine a current state. The model can be configured to run on a schedule and/or when motion is detected in that region. Classification results are available through the `frigate//classification/` MQTT topic and in Home Assistant sensors via the official Frigate integration. + +:::info + +Training a custom state classification model requires a one-time internet connection to download MobileNetV2 base weights. Once trained, the model runs fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details. + +::: + +## Minimum System Requirements + +State classification models are lightweight and run very fast on CPU. + +Training the model does briefly use a high amount of system resources for about 1-3 minutes per training run. On lower-power devices, training may take longer. + +A CPU with AVX + AVX2 instructions is required for training and inference. + +## Classes + +Classes are the different states an area on your camera can be in. Each class represents a distinct visual state that the model will learn to recognize. + +For state classification: + +- Define classes that represent mutually exclusive states +- Examples: `open` and `closed` for a garage door, `on` and `off` for lights +- Use at least 2 classes (typically binary states work best) +- Keep class names clear and descriptive + +## Example use cases + +- **Door state**: Detect if a garage or front door is open vs closed. +- **Gate state**: Track if a driveway gate is open or closed. +- **Trash day**: Bins at curb vs no bins present. +- **Pool cover**: Cover on vs off. + +## Configuration + +State classification is configured as a custom classification model. Each model has its own name and settings. Provide at least one camera crop under `state_config.cameras`. + + + + +Navigate to the **Classification** page from the main navigation sidebar, select the **States** tab, then click **Add Classification**. + +In the **Create New Classification** dialog: + +| Field | Description | +| ----------- | ------------------------------------------------------------------------------------ | +| **Name** | A name for your state classification model (e.g., `front_door`) | +| **Type** | Select **State** for state classification | +| **Classes** | The state names the model will learn to distinguish between (e.g., `open`, `closed`) | + +After creating the model, the wizard will guide you through selecting the camera crop area and assigning training examples. The `threshold` (default: `0.8`), `motion`, and `interval` settings can be adjusted in the YAML configuration. + + + + +```yaml +classification: + custom: + front_door: + threshold: 0.8 + state_config: + motion: true # run when motion overlaps the crop + interval: 10 # also run every N seconds (optional) + cameras: + front: + crop: [0, 180, 220, 400] +``` + +An optional config, `save_attempts`, can be set as a key under the model name. This defines the number of classification attempts to save in the Recent Classifications tab. For state classification models, the default is 100. + + + + +## Training the model + +Creating and training the model is done within the Frigate UI using the `Classification` page. The process consists of three steps: + +### Step 1: Name and Define + +Enter a name for your model and define at least 2 classes (states) that represent mutually exclusive states. For example, `open` and `closed` for a door, or `on` and `off` for lights. + +### Step 2: Select the Crop Area + +Choose one or more cameras and draw a rectangle over the area of interest for each camera. The crop should be tight around the region you want to classify to avoid extra signals unrelated to what is being classified. You can drag and resize the rectangle to adjust the crop area. + +### Step 3: Assign Training Examples + +The system will automatically generate example images from your camera feeds. You'll be guided through each class one at a time to select which images represent that state. It's not strictly required to select all images you see. If a state is missing from the samples, you can train it from the Recent tab later. + +Once some images are assigned, training will begin automatically. + +### Improving the Model + +:::tip Diversity matters far more than volume + +Selecting dozens of nearly identical images is one of the fastest ways to degrade model performance. MobileNetV2 can overfit quickly when trained on homogeneous data. The model learns what _that exact moment_ looked like rather than what actually defines the state. This often leads to models that work perfectly under the original conditions but become unstable when day turns to night, weather changes, or seasonal lighting shifts. **This is why Frigate does not implement bulk training in the UI.** + +For more detail, see [Frigate Tip: Best Practices for Training Face and Custom Classification Models](https://github.com/blakeblackshear/frigate/discussions/21374). + +::: + +- **Start small and iterate**: Begin with a small, representative set of images per class. Models often begin working well with surprisingly few examples and improve naturally over time. +- **Problem framing**: Keep classes visually distinct and state-focused (e.g., `open`, `closed`, `unknown`). Avoid combining object identity with state in a single model unless necessary. +- **Data collection**: Use the model's Recent Classifications tab to gather balanced examples across times of day and weather. +- **When to train**: Focus on cases where the model is entirely incorrect or flips between states when it should not. There's no need to train additional images when the model is already working consistently. +- **Favor hard examples**: When images appear in the Recent Classifications tab, prioritize images scoring below 90-100% or those captured under new conditions (e.g., first snow of the year, seasonal changes, objects temporarily in view, insects at night). These represent scenarios different from the default state and help prevent overfitting. +- **Avoid bulk training similar images**: Training large batches of images that already score 100% (or close) adds little new information and increases the risk of overfitting. +- **The wizard is just the starting point**: You don't need to find and label every state upfront. Missing states will naturally appear in Recent Classifications, and those images tend to be more valuable because they represent new conditions and edge cases. + +## Debugging Classification Models + +To troubleshoot issues with state classification models, enable debug logging to see detailed information about classification attempts, scores, and state verification. + +Enable debug logs for classification models by adding `frigate.data_processing.real_time.custom_classification: debug` to your `logger` configuration. These logs are verbose, so only keep this enabled when necessary. Restart Frigate after this change. + + + + +Navigate to . + +- Set **Logging level** to `debug` +- Set **Per-process log level > `frigate.data_processing.real_time.custom_classification`** to `debug` for verbose classification logging + + + + +```yaml +logger: + default: info + logs: + # highlight-next-line + frigate.data_processing.real_time.custom_classification: debug +``` + + + + +The debug logs will show: + +- Classification probabilities for each attempt +- Whether scores meet the threshold requirement +- State verification progress (consecutive detections needed) +- When state changes are published + +### Recent Classifications + +For state classification, images are only added to recent classifications under specific circumstances: + +- **First detection**: The first classification attempt for a camera is always saved +- **State changes**: Images are saved when the detected state differs from the current verified state +- **Pending verification**: Images are saved when there's a pending state change being verified (requires 3 consecutive identical states) +- **Low confidence**: Images with scores below 100% are saved even if the state matches the current state (useful for training) + +Images are **not** saved when the state is stable (detected state matches current state) **and** the score is 100%. This prevents unnecessary storage of redundant high-confidence classifications. diff --git a/docs/docs/configuration/face_recognition.md b/docs/docs/configuration/face_recognition.md new file mode 100644 index 0000000..96a29c0 --- /dev/null +++ b/docs/docs/configuration/face_recognition.md @@ -0,0 +1,349 @@ +--- +id: face_recognition +title: Face Recognition +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; +import FaqItem from "@site/src/components/FaqItem"; + +Face recognition identifies known individuals by matching detected faces with previously learned facial data. When a known `person` is recognized, their name will be added as a `sub_label`. This information is included in the UI, filters, as well as in notifications. + +:::info + +Face recognition requires a one-time internet connection to download detection and embedding models from GitHub. Once cached, models work fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details. + +::: + +## Model Requirements + +### Face Detection + +When running a Frigate+ model (or any custom model that natively detects faces) should ensure that `face` is added to the [list of objects to track](../plus/index.md#available-label-types) either globally or for a specific camera. This will allow face detection to run at the same time as object detection and be more efficient. + +When running a default COCO model or another model that does not include `face` as a detectable label, face detection will run via CV2 using a lightweight DNN model that runs on the CPU. In this case, you should _not_ define `face` in your list of objects to track. + +:::note + +Frigate needs to first detect a `person` before it can detect and recognize a face. + +::: + +### Face Recognition + +Frigate has support for two face recognition model types: + +- **small**: Frigate will run a FaceNet embedding model to recognize faces, which runs locally on the CPU. This model is optimized for efficiency and is not as accurate. +- **large**: Frigate will run a large ArcFace embedding model that is optimized for accuracy. It is only recommended to be run when an integrated or dedicated GPU / NPU is available. + +In both cases, a lightweight face landmark detection model is also used to align faces before running recognition. + +All of these features run locally on your system. + +## Minimum System Requirements + +A CPU with AVX + AVX2 instructions is required to run Face Recognition. + +The `small` model is optimized for efficiency and runs on the CPU, most CPUs should run the model efficiently. + +The `large` model is optimized for accuracy, an integrated or discrete GPU / NPU is required. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. + +## Configuration + +Face recognition is disabled by default and must be enabled before it can be used. Face recognition is a global configuration setting. + + + + +Navigate to . + +- Set **Enable face recognition** to on + + + + +```yaml +face_recognition: + enabled: true +``` + + + + +Like the other real-time processors in Frigate, face recognition runs on the camera stream defined by the `detect` role in your config. To ensure optimal performance, select a suitable resolution for this stream in your camera's firmware that fits your specific scene and requirements. + +## Advanced Configuration + +Fine-tune face recognition with these optional parameters. The only optional parameters that can be set at the camera level are `enabled` and `min_area`. + +### Detection + + + + +Navigate to . + +- **Detection threshold**: Face detection confidence score required before recognition runs. This field only applies to the standalone face detection model; `min_score` should be used to filter for models that have face detection built in. + - Default: `0.7` +- **Minimum face area**: Minimum size (in pixels) a face must be before recognition runs. Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant faces. + - Default: `750` pixels + + + + +```yaml +face_recognition: + enabled: true + detection_threshold: 0.7 + min_area: 750 +``` + + + + +### Recognition + + + + +Navigate to . + +- **Model size**: Which model size to use, options are `small` or `large`. +- **Unknown score threshold**: Min score to mark a person as a potential match; matches at or below this will be marked as unknown. + - Default: `0.8` +- **Recognition threshold**: Recognition confidence score required to add the face to the object as a sub label. + - Default: `0.9` +- **Minimum faces**: Min face recognitions for the sub label to be applied to the person object. + - Default: `1` +- **Save attempts**: Number of images of recognized faces to save for training. + - Default: `200` +- **Blur confidence filter**: Enables a filter that calculates how blurry the face is and adjusts the confidence based on this. + - Default: `True` +- **Device**: Target a specific device to run the face recognition model on (multi-GPU installation). This setting is only applicable when using the `large` model. See [onnxruntime's provider options](https://onnxruntime.ai/docs/execution-providers/). + - Default: `None` + + + + +```yaml +face_recognition: + enabled: true + model_size: small + unknown_score: 0.8 + recognition_threshold: 0.9 + min_faces: 1 + save_attempts: 200 + blur_confidence_filter: true + device: None +``` + + + + +## Usage + +Follow these steps to begin: + +1. **Enable face recognition** in your configuration and restart Frigate. +2. **Upload one face** using the **Add Face** button's wizard in the Face Library section of the Frigate UI. Read below for the best practices on expanding your training set. +3. When Frigate detects and attempts to recognize a face, it will appear in the **Train** tab of the Face Library, along with its associated recognition confidence. +4. From the **Train** tab, you can **assign the face** to a new or existing person to improve recognition accuracy for the future. + +## Creating a Robust Training Set + +:::tip + +**The short version:** Start with a few clear, front-facing photos of each person. As faces are detected in the Recent Recognitions tab, train clear images that scored lower, adding variety (different angles, lighting, and expressions) slowly. Diversity matters far more than volume, and low-quality images hurt recognition more than they help. + +For a step-by-step narrative of these best practices (and the same principles applied to state and object classification), see the [Frigate Tips: Best Practices for Training](https://github.com/blakeblackshear/frigate/discussions/21374) discussion. + +::: + +The number of images needed for a sufficient training set for face recognition varies depending on several factors: + +- Diversity of the dataset: A dataset with diverse images, including variations in lighting, pose, and facial expressions, will require fewer images per person than a less diverse dataset. +- Desired accuracy: The higher the desired accuracy, the more images are typically needed. + +However, here are some general guidelines: + +- Minimum: For basic face recognition tasks, a minimum of 5-10 images per person is often recommended. +- Recommended: For more robust and accurate systems, 20-30 images per person is a good starting point. +- Ideal: For optimal performance, especially in challenging conditions, 50-100 images per person can be beneficial. + +The accuracy of face recognition is heavily dependent on the quality of data given to it for training. It is recommended to build the face training library in phases. + +:::tip + +When choosing images to include in the face training set it is recommended to always follow these recommendations: + +- If it is difficult to make out details in a persons face it will not be helpful in training. +- Avoid images with extreme under/over-exposure. +- Avoid blurry / pixelated images. +- Avoid training on infrared (gray-scale). The models are trained on color images and will not be able to extract features from gray-scale images. +- Using images of people wearing hats / sunglasses may confuse the model. +- Do not upload too many similar images at the same time, it is recommended to train no more than 4-6 similar images for each person to avoid over-fitting. + +::: + +### Understanding the Recent Recognitions Tab + +The Recent Recognitions tab in the face library displays recent face recognition attempts. Detected face images are grouped according to the person they were identified as potentially matching. + +Each face image is labeled with a name (or `Unknown`) along with the confidence score of that recognition attempt. Images are grouped by the person they were matched against, not by who they actually are, so a group labeled with a person's name can contain a crop that is really someone else but happened to score as a partial match. The name and score shown on each individual crop describe that single attempt. + +While each image can be used to train the system for a specific person, not all images are suitable for training. Refer to the guidelines below for best practices on selecting images for training. + +### How Frigate Decides Who a Person Is + +Recognition does not happen one frame at a time. While a `person` is in view, Frigate runs face recognition on many frames, not just a single frame. The final `sub_label` is decided from all of those attempts together, weighted by the area of each face (larger, closer faces count more), not from any single frame. + +This has a few practical consequences: + +- A handful of wrong guesses on blurry or distant frames usually do not change the result. If Frigate sees a person as "Tom, Tom, Sam, Tom, Tom," it will still conclude the person was Tom. +- The goal is not for every individual face crop to be correct. The goal is for each person to be recognized correctly overall, across all the faces captured while they were present. +- A single very high confidence match will not by itself assign a sub label. Recognition must be consistent. See [I see scores above the threshold in the Recent Recognitions tab, but a sub label wasn't assigned?](#i-see-scores-above-the-threshold-in-the-recent-recognitions-tab-but-a-sub-label-wasnt-assigned) below. + +### Which Faces Are Worth Training? + +Whether a face is worth training has little to do with what it was recognized as. A crop is a good training candidate when all of these are true: + +- It did not already score high and correctly. Faces that are already recognized confidently add little and increase the risk of over-fitting. +- It is clear enough to be useful: not blurry, not heavily off-axis, not infrared (gray-scale). If it is hard for you to make out the face, it will not help the model. +- It adds something new: a different angle, lighting, expression, or distance than what you already have. + +### Step 1 - Building a Strong Foundation + +When first enabling face recognition it is important to build a foundation of strong images. It is recommended to start by uploading 1-5 photos containing just this person's face. It is important that the person's face in the photo is front-facing and not turned, this will ensure a good starting point. + +Then it is recommended to use the `Face Library` tab in Frigate to select and train images for each person as they are detected. When building a strong foundation it is strongly recommended to only train on images that are front-facing. Ignore images from cameras that recognize faces from an angle. Aim to strike a balance between the quality of images while also having a range of conditions (day / night, different weather conditions, different times of day, etc.) in order to have diversity in the images used for each person and not have over-fitting. + +You do not want to train images that are 90%+ as these are already being confidently recognized. In this step the goal is to train on clear, lower scoring front-facing images until the majority of front-facing images for a given person are consistently recognized correctly. Then it is time to move on to step 2. + +### Step 2 - Expanding The Dataset + +Once front-facing images are performing well, start choosing slightly off-angle images to include for training. It is important to still choose images where enough face detail is visible to recognize someone, and you still only want to train on images that score lower. + +## FAQ + +### Getting Recognition Working + + + +Start with the [Usage](#usage) section and re-read the [Model Requirements](#model-requirements) above. + +1. Ensure `person` is being _detected_. A `person` will automatically be scanned by Frigate for a face. Any detected faces will appear in the Recent Recognitions tab in the Frigate UI's Face Library. + + If you are using a Frigate+ or `face` detecting model: + - Watch the [debug view](/usage/live#the-single-camera-view) to ensure that `face` is being detected along with `person`. + - You may need to adjust the `min_score` for the `face` object if faces are not being detected. + + If you are **not** using a Frigate+ or `face` detecting model: + - Check your `detect` stream resolution and ensure it is sufficiently high enough to capture face details on `person` objects. + - You may need to lower your `detection_threshold` if faces are not being detected. + +2. Any detected faces will then be _recognized_. + - Make sure you have trained at least one face per the recommendations above. + - Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration). + + + + + +Face recognition does not run on the recording stream, this would be suboptimal for many reasons: + +1. The latency of accessing the recordings means the notifications would not include the names of recognized people because recognition would not complete until after. +2. The embedding models used run on a set image size, so larger images will be scaled down to match this anyway. +3. Motion clarity is much more important than extra pixels, over-compression and motion blur are much more detrimental to results than resolution. + + + +### Improving Accuracy and Training + + + +Accuracy is definitely going to be improved with higher quality cameras / streams. It is important to look at the DORI (Detection Observation Recognition Identification) range of your camera, if that specification is posted. This specification explains the distance from the camera that a person can be detected, observed, recognized, and identified. The identification range is the most relevant here, and the distance listed by the camera is the furthest that face recognition will realistically work. + +Some users have also noted that setting the stream in camera firmware to a constant bit rate (CBR) leads to better image clarity than with a variable bit rate (VBR). + + + + + +The embedding models are trained on color images, so gray-scale and infrared (IR) faces sit in a different feature distribution and are more easily confused with other people. Prefer color images, and avoid mixing gray-scale samples in early while you are building a foundation. If someone only ever appears at night, gray-scale training is acceptable, but keep those samples limited and as clear as possible, and add them only once color recognition is stable for your other people. + + + + + +It is important to methodically add photos to the library, bulk importing photos (especially from a general photo library) will lead to over-fitting in that particular scenario and hurt recognition performance. + + + + + +Face embedding models work by breaking apart faces into different features. This means that when reprocessing an image, only images from a similar angle will have its score affected. + + + + + +This can happen for a few different reasons, but this is usually an indicator that the training set needs to be improved. This is often related to over-fitting: + +- If you train with only a few images per person, especially if those images are very similar, the recognition model becomes overly specialized to those specific images. +- When you provide images with different poses, lighting, and expressions, the algorithm extracts features that are consistent across those variations. +- By training on a diverse set of images, the algorithm becomes less sensitive to minor variations and noise in the input image. + +Review your face collections and remove most of the unclear or low-quality images. Then, use the **Reprocess** button on each face in the **Train** tab to evaluate how the changes affect recognition scores. + +Avoid training on images that already score highly, as this can lead to over-fitting. Instead, focus on relatively clear images that score lower (ideally with different lighting, angles, and conditions) to help the model generalize more effectively. + + + + + +Only if it is a good image. Reassigning a face does add it to that person's training set, but two things are true at once: + +- Reassigning a single misclassified frame has a small effect. The image is weighted against every other sample for that person, so correcting 1 frame out of 20 will not move recognition much. Occasional wrong guesses on poor frames are normal and do not need to be fixed. +- Reassigning a poor image (blurry, off-angle, low-resolution, gray-scale) can hurt more than the misidentification did, because low-quality samples degrade recognition for that whole person. + +So the decision is about image quality, not about the wrong label. If the crop is clear, well-lit, and reasonably front-facing, and it scored low or was wrong, assigning it to the correct person is useful. If you can barely make out the face yourself, ignore it; do not train it just to correct the label. + +If a person is repeatedly misidentified, do not keep reassigning the same frame. Instead, remove low-quality or misleading images and add a few high-quality samples to the correct person. See [Why do unknown people score similarly to known people?](#why-do-unknown-people-score-similarly-to-known-people) above. + + + + + +No, face recognition does not support negative training (i.e., explicitly telling it who someone is _not_). Instead, the best approach is to improve the training data by using a more diverse and representative set of images for each person. +For more guidance, refer to the section above on improving recognition accuracy. + +This also applies to a stranger who is repeatedly matched to a known person (for example, a delivery driver recognized as you). Do not create a profile for them and do not reassign their faces to yourself, as this pollutes your training set and makes recognition worse. Leave the detection as unknown and improve the known person's training set instead. Face recognition learns who someone is, not who they are not. + + + + + +Frigate considers the recognition scores across all recognition attempts for each person object. The scores are continually weighted based on the area of the face, and a sub label will only be assigned to person if a person is confidently recognized consistently. This avoids cases where a single high confidence recognition would throw off the results. + + + +### Compatibility and Maintenance + + + +No, using another face recognition service will interfere with Frigate's built in face recognition. When using double-take the sub_label feature must be disabled if the built in face recognition is also desired. + + + + + +By default iOS devices will use HEIC (High Efficiency Image Container) for images, but this format is not supported for uploads. Choosing `large` as the format instead of `original` will use JPG which will work correctly. + + + + + +Frigate does not store anything in its database related to face recognition. You can simply delete all of your faces through the Frigate UI or remove the contents of the `/media/frigate/clips/faces` directory. + + diff --git a/docs/docs/configuration/ffmpeg_presets.md b/docs/docs/configuration/ffmpeg_presets.md new file mode 100644 index 0000000..5c1d0fc --- /dev/null +++ b/docs/docs/configuration/ffmpeg_presets.md @@ -0,0 +1,108 @@ +--- +id: ffmpeg_presets +title: FFmpeg presets +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Frigate ships with a set of FFmpeg presets to keep your configuration short and readable. Each preset expands to a longer list of FFmpeg arguments at runtime. You can see exactly what every preset expands to in [this file](https://github.com/blakeblackshear/frigate/blob/master/frigate/ffmpeg_presets.py). + +In the config file you reference a preset by its name (for example, `preset-vaapi`). In the UI, the same preset is shown with a friendly label (for example, **VAAPI (Intel/AMD GPU)**). Both refer to the same thing: the tables below list the config name alongside the label you'll see in the UI. + +### Hwaccel (Hardware Acceleration) Presets {#hwaccel-presets} + +Hardware acceleration arguments tell FFmpeg to decode your camera's video stream on a GPU or integrated graphics chip instead of the CPU, which dramatically lowers CPU usage. Using a preset is highly recommended. Beyond replacing a long list of arguments, each preset also tells Frigate what hardware is available so it can offload additional work to the GPU, for example, encoding the Birdseye restream or scaling a stream whose resolution differs from the camera's native size. + +See [the hardware acceleration docs](/configuration/hardware_acceleration_video.md) for details on setting up hardware acceleration for your GPU / iGPU, then select the preset that matches your hardware. + +| Preset (YAML config) | UI Label | Usage | Notes | +| --------------------- | ----------------------- | --------------------------------- | --------------------------------------------------------------- | +| preset-rpi-64-h264 | Raspberry Pi (H.264) | 64-bit Raspberry Pi, H.264 stream | | +| preset-rpi-64-h265 | Raspberry Pi (H.265) | 64-bit Raspberry Pi, H.265 stream | | +| preset-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected | +| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead | +| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead | +| preset-nvidia | NVIDIA GPU | NVIDIA GPU | | +| preset-jetson-h264 | NVIDIA Jetson (H.264) | NVIDIA Jetson, H.264 stream | | +| preset-jetson-h265 | NVIDIA Jetson (H.265) | NVIDIA Jetson, H.265 stream | | +| preset-rkmpp | Rockchip RKMPP | Rockchip MPP | Use an image with the `-rk` suffix and run in privileged mode | + + + + +1. Navigate to and set **Hardware acceleration arguments** to the appropriate preset for your hardware. +2. To override for a specific camera, navigate to and set **Hardware acceleration arguments** for that camera. + + + + +```yaml +ffmpeg: + hwaccel_args: preset-vaapi + +cameras: + front_door: + ffmpeg: + hwaccel_args: preset-nvidia +``` + + + + +### Input Args Presets + +Input arguments are passed to FFmpeg before your camera source and control how Frigate connects to and reads the stream: the transport protocol, timeouts, reconnection behavior, and how the stream is probed. The right input args ensure a reliable connection and maximum compatibility for each type of stream. + +See [the camera-specific docs](/configuration/camera_specific.md) for more on non-standard cameras and recommendations for using them in Frigate. + +| Preset (config) | UI Label | Usage | Notes | +| -------------------------------- | ----------------------------------------- | --------------------------- | ------------------------------------------------------------------------------- | +| preset-http-jpeg-generic | HTTP JPEG (Generic) | HTTP live JPEG | Restreaming the live JPEG is recommended instead | +| preset-http-mjpeg-generic | HTTP MJPEG (Generic) | HTTP MJPEG stream | Restreaming the MJPEG stream is recommended instead | +| preset-http-reolink | HTTP - Reolink Cameras | Reolink HTTP-FLV stream | Only for Reolink HTTP, not when restreaming as RTSP | +| preset-rtmp-generic | RTMP (Generic) | RTMP stream | | +| preset-rtsp-generic | RTSP (Generic) | RTSP stream | The default when no input args are specified | +| preset-rtsp-restream | RTSP - Restream from go2rtc | RTSP stream from a restream | Use when a go2rtc restream is the source for Frigate | +| preset-rtsp-restream-low-latency | RTSP - Restream from go2rtc (Low Latency) | RTSP stream from a restream | Lowers latency for a go2rtc restream source; may cause issues with some cameras | +| preset-rtsp-udp | RTSP - UDP | RTSP stream over UDP | Use when the camera only supports UDP | +| preset-rtsp-blue-iris | RTSP - Blue Iris | Blue Iris RTSP stream | Use when consuming a stream from Blue Iris | + +:::warning + +Be mindful of input arguments when restreaming, because you can end up with a mix of protocols. The `http` and `rtmp` presets cannot be used with `rtsp` streams. For example, using a Reolink camera with an RTSP restream as the recording source while `preset-http-reolink` is applied will cause a crash. In cases like this, set the preset at the stream level instead. See the example below. + +::: + +```yaml +go2rtc: + streams: + reolink_cam: http://192.168.0.139/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=admin&password=password + +cameras: + reolink_cam: + ffmpeg: + inputs: + - path: http://192.168.0.139/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=admin&password=password + input_args: preset-http-reolink + roles: + - detect + - path: rtsp://127.0.0.1:8554/reolink_cam + input_args: preset-rtsp-generic + roles: + - record +``` + +### Output Args Presets + +Output arguments are passed to FFmpeg after your camera source and control how recordings are written: which codecs are used and whether audio and video are copied as-is or re-encoded. The right output args ensure consistent, playable recordings for each type of stream. + +| Preset (config) | UI Label | Usage | Notes | +| -------------------------------- | ------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| preset-record-generic | Record (Generic, no audio) | Record without audio | Use this if your camera has no audio, or if you don't want to record audio | +| preset-record-generic-audio-copy | Record (Generic + Copy Audio) | Record with the original audio | Use this to keep the camera's audio in recordings without re-encoding | +| preset-record-generic-audio-aac | Record (Generic + Audio to AAC) | Record with audio transcoded to AAC | The default when no output args are specified. Transcodes audio to AAC. If the source is already AAC, use `preset-record-generic-audio-copy` to avoid re-encoding | +| preset-record-mjpeg | Record - MJPEG Cameras | Record an MJPEG stream | Restreaming the MJPEG stream is recommended instead | +| preset-record-jpeg | Record - JPEG Cameras | Record a live JPEG | Restreaming the live JPEG is recommended instead | +| preset-record-ubiquiti | Record - Ubiquiti Cameras | Record a Ubiquiti stream with audio | Handles Ubiquiti's non-standard audio format | diff --git a/docs/docs/configuration/genai/config.md b/docs/docs/configuration/genai/config.md new file mode 100644 index 0000000..738eb5d --- /dev/null +++ b/docs/docs/configuration/genai/config.md @@ -0,0 +1,387 @@ +--- +id: genai_config +title: Configuring Generative AI +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +## Configuration + +A Generative AI provider can be configured in the global config, which will make the Generative AI features available for use. There are currently 4 native providers available to integrate with Frigate. Other providers that support the OpenAI standard API can also be used. See the OpenAI-Compatible section below. + +To use Generative AI, you must define a single provider at the global level of your Frigate configuration. If the provider you choose requires an API key, you may either directly paste it in your configuration, or store it in an environment variable prefixed with `FRIGATE_`. + +## Local Providers + +Local providers run on your own hardware and keep all data processing private. These require a GPU or dedicated hardware for best performance. + +:::warning + +Running Generative AI models on CPU is not recommended, as high inference times make using Generative AI impractical. + +::: + +### Recommended Local Models + +You must use a vision-capable model with Frigate. The following models are recommended for local deployment: + +| Model | Notes | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `qwen3-vl` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. | +| `qwen3.5` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. | +| `qwen3.6` | Strong situational understanding, similar to qwen3-vl | +| `gemma4` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. | + +:::info + +Each model is available in multiple parameter sizes (3b, 4b, 8b, etc.). Larger sizes are more capable of complex tasks and understanding of situations, but requires more memory and computational resources. It is recommended to try multiple models and experiment to see which performs best. + +::: + +:::note + +You should have at least 8 GB of RAM available (or VRAM if running on GPU) to run the 7B models, 16 GB to run the 13B models, and 24 GB to run the 33B models. + +::: + +### Model Types: Instruct vs Thinking + +Vision-language models come in **instruct** variants (fine-tuned to follow instructions and respond concisely), **thinking** variants (fine-tuned for free-form, speculative reasoning), and **hybrid** variants that support both modes per request. Most modern vision-language models are hybrid. + +Frigate manages reasoning per task automatically: + +- **Description tasks** (object descriptions, review descriptions, review summaries) are synthesis-only and benefit from concise, direct output, so Frigate disables thinking for these calls when the model exposes a per-request toggle. +- **Chat** lets you toggle thinking on or off from the composer when the configured model supports it. + +You can use a pure instruct, hybrid, or thinking-capable model with Frigate. No extra configuration is required to disable thinking for descriptions. + +### llama.cpp + +[llama.cpp](https://github.com/ggml-org/llama.cpp) is a C++ implementation of LLaMA that provides a high-performance inference server. + +It is highly recommended to host the llama.cpp server on a machine with a discrete graphics card, or on an Apple silicon Mac for best performance. + +#### Supported Models + +You must use a vision capable model with Frigate. The llama.cpp server supports various vision models in GGUF format. + +#### Configuration + +All llama.cpp native options can be passed through `provider_options`, including `temperature`, `top_k`, `top_p`, `min_p`, `repeat_penalty`, `repeat_last_n`, `seed`, `grammar`, and more. See the [llama.cpp server documentation](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md) for a complete list of available parameters. + + + + +1. Navigate to . + - Set **Provider** to `llamacpp` + - Set **Base URL** to your llama.cpp server address (e.g., `http://localhost:8080`) + - Set **Model** to the name of your model + - Under **Provider Options**, set `context_size` to tell Frigate your context size so it can send the appropriate amount of information + + + + +```yaml +genai: + provider: llamacpp + base_url: http://localhost:8080 + model: your-model-name + provider_options: + context_size: 16000 # Tell Frigate your context size so it can send the appropriate amount of information. +``` + + + + +### Ollama + +[Ollama](https://ollama.com/) allows you to self-host large language models and keep everything running locally. It is highly recommended to host this server on a machine with an Nvidia graphics card, or on a Apple silicon Mac for best performance. + +Most of the 7b parameter 4-bit vision models will fit inside 8GB of VRAM. There is also a [Docker container](https://hub.docker.com/r/ollama/ollama) available. + +Parallel requests also come with some caveats. You will need to set `OLLAMA_NUM_PARALLEL=1` and choose a `OLLAMA_MAX_QUEUE` and `OLLAMA_MAX_LOADED_MODELS` values that are appropriate for your hardware and preferences. See the [Ollama documentation](https://docs.ollama.com/faq#how-does-ollama-handle-concurrent-requests). + +:::tip + +If you are trying to use a single model for Frigate and HomeAssistant, it will need to support vision and tools calling. qwen3-VL supports vision and tools simultaneously in Ollama. + +::: + +Note that Frigate will not automatically download the model you specify in your config. Ollama will try to download the model but it may take longer than the timeout, so it is recommended to pull the model beforehand by running `ollama pull your_model` on your Ollama server/Docker container. The model specified in Frigate's config must match the downloaded model tag. + +#### Configuration + + + + +1. Navigate to . + - Set **Provider** to `ollama` + - Set **Base URL** to your Ollama server address (e.g., `http://localhost:11434`) + - Set **Model** to the model tag (e.g., `qwen3-vl:4b`) + - Under **Provider Options**, set `keep_alive` (e.g., `-1`) and `options.num_ctx` to match your desired context size + + + + +```yaml +genai: + provider: ollama + base_url: http://localhost:11434 + model: qwen3-vl:4b + provider_options: # other Ollama client options can be defined + keep_alive: -1 + options: + num_ctx: 8192 # make sure the context matches other services that are using ollama +``` + + + + +### OpenAI-Compatible + +Frigate supports any provider that implements the OpenAI API standard. This includes self-hosted solutions like [vLLM](https://docs.vllm.ai/), [LocalAI](https://localai.io/), and other OpenAI-compatible servers. + +:::tip + +For OpenAI-compatible servers (such as llama.cpp) that don't expose the configured context size in the API response, you can manually specify the context size in `provider_options`: + +```yaml +genai: + provider: openai + base_url: http://your-llama-server + model: your-model-name + provider_options: + context_size: 8192 # Specify the configured context size +``` + +This ensures Frigate uses the correct context window size when generating prompts. + +::: + +#### Configuration + + + + +1. Navigate to . + - Set **Provider** to `openai` + - Set **Base URL** to your server address (e.g., `http://your-server:port`) + - Set **API key** if required by your server + - Set **Model** to the model name + + + + +```yaml +genai: + provider: openai + base_url: http://your-server:port + api_key: your-api-key # May not be required for local servers + model: your-model-name +``` + + + + +To use a different OpenAI-compatible API endpoint, set the `OPENAI_BASE_URL` environment variable to your provider's API URL. + +## Cloud Providers + +Cloud providers run on remote infrastructure and require an API key for authentication. These services handle all model inference on their servers. + +:::info + +Cloud Generative AI providers require an active internet connection to send images and prompts for processing. Local providers like llama.cpp and Ollama (with local models) do not require internet. See [Network Requirements](/frigate/network_requirements#generative-ai) for details. + +::: + +### Ollama Cloud + +Ollama also supports [cloud models](https://ollama.com/cloud), where model inference is performed in the cloud. You can connect directly to Ollama Cloud by setting `base_url` to `https://ollama.com` and providing an API key. Alternatively, you can run Ollama locally and use a cloud model name so your local instance forwards requests to the cloud. For more details, see the Ollama cloud model [docs](https://docs.ollama.com/cloud). + +#### Configuration + + + + +1. Navigate to . + - Set **Provider** to `ollama` + - Set **Base URL** to your local Ollama address (e.g., `http://localhost:11434`) or `https://ollama.com` for direct cloud inference + - Set **API key** if required by your endpoint (e.g., when using `https://ollama.com`) + - Set **Model** to the cloud model name + + + + +```yaml +genai: + provider: ollama + base_url: http://localhost:11434 + model: cloud-model-name +``` + +or when using Ollama Cloud directly + +```yaml +genai: + provider: ollama + base_url: https://ollama.com + model: cloud-model-name + api_key: your-api-key +``` + + + + +### Google Gemini + +Google Gemini has a [free tier](https://ai.google.dev/pricing) for the API, however the limits may not be sufficient for standard Frigate usage. Choose a plan appropriate for your installation. + +#### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://ai.google.dev/gemini-api/docs/models/gemini). + +#### Get API Key + +To start using Gemini, you must first get an API key from [Google AI Studio](https://aistudio.google.com). + +1. Accept the Terms of Service +2. Click "Get API Key" from the right hand navigation +3. Click "Create API key in new project" +4. Copy the API key for use in your config + +#### Configuration + + + + +1. Navigate to . + - Set **Provider** to `gemini` + - Set **API key** to your Gemini API key (or use an environment variable such as `{FRIGATE_GEMINI_API_KEY}`) + - Set **Model** to the desired model (e.g., `gemini-2.5-flash`) + + + + +```yaml +genai: + provider: gemini + api_key: "{FRIGATE_GEMINI_API_KEY}" + model: gemini-2.5-flash +``` + + + + +:::note + +To use a different Gemini-compatible API endpoint, set the `provider_options` with the `base_url` key to your provider's API URL. For example: + +```yaml {4,5} +genai: + provider: gemini + ... + provider_options: + base_url: https://... +``` + +Other HTTP options are available, see the [python-genai documentation](https://github.com/googleapis/python-genai). + +::: + +### OpenAI + +OpenAI does not have a free tier for their API. + +#### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://platform.openai.com/docs/models). + +#### Get API Key + +To start using OpenAI, you must first [create an API key](https://platform.openai.com/api-keys) and [configure billing](https://platform.openai.com/settings/organization/billing/overview). + +#### Configuration + + + + +1. Navigate to . + - Set **Provider** to `openai` + - Set **API key** to your OpenAI API key (or use an environment variable such as `{FRIGATE_OPENAI_API_KEY}`) + - Set **Model** to the desired model (e.g., `gpt-4o`) + + + + +```yaml +genai: + provider: openai + api_key: "{FRIGATE_OPENAI_API_KEY}" + model: gpt-4o +``` + + + + +:::note + +To use a different OpenAI-compatible API endpoint, set the `OPENAI_BASE_URL` environment variable to your provider's API URL. + +::: + +:::tip + +For OpenAI-compatible servers (such as llama.cpp) that don't expose the configured context size in the API response, you can manually specify the context size in `provider_options`: + +```yaml {5,6} +genai: + provider: openai + base_url: http://your-llama-server + model: your-model-name + provider_options: + context_size: 8192 # Specify the configured context size +``` + +This ensures Frigate uses the correct context window size when generating prompts. + +::: + +### Azure OpenAI + +Microsoft offers several vision models through Azure OpenAI. A subscription is required. + +#### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models). + +#### Create Resource and Get API Key + +To start using Azure OpenAI, you must first [create a resource](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource). You'll need your API key, model name, and resource URL, which must include the `api-version` parameter (see the example below). + +#### Configuration + + + + +1. Navigate to . + - Set **Provider** to `azure_openai` + - Set **Base URL** to your Azure resource URL including the `api-version` parameter (e.g., `https://instance.cognitiveservices.azure.com/openai/responses?api-version=2025-04-01-preview`) + - Set **Model** to your deployed model name (e.g., `gpt-5-mini`) + - Set **API key** to your Azure OpenAI API key (or use an environment variable such as `{FRIGATE_OPENAI_API_KEY}`) + + + + +```yaml +genai: + provider: azure_openai + base_url: https://instance.cognitiveservices.azure.com/openai/responses?api-version=2025-04-01-preview + model: gpt-5-mini + api_key: "{FRIGATE_OPENAI_API_KEY}" +``` + + + diff --git a/docs/docs/configuration/genai/objects.md b/docs/docs/configuration/genai/objects.md new file mode 100644 index 0000000..eb8dade --- /dev/null +++ b/docs/docs/configuration/genai/objects.md @@ -0,0 +1,114 @@ +--- +id: genai_objects +title: Object Descriptions +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Generative AI can be used to automatically generate descriptive text based on the thumbnails of your tracked objects. This helps with [Semantic Search](/configuration/semantic_search) in Frigate to provide more context about your tracked objects. Descriptions are accessed via the _Explore_ view in the Frigate UI by clicking on a tracked object's thumbnail. + +Requests for a description are sent off automatically to your AI provider at the end of the tracked object's lifecycle, or can optionally be sent earlier after a number of significantly changed frames, for example in use in more real-time notifications. Descriptions can also be regenerated manually via the Frigate UI. Note that if you are manually entering a description for tracked objects prior to its end, this will be overwritten by the generated response. + +By default, descriptions will be generated for all tracked objects and all zones. But you can also optionally specify `objects` and `required_zones` to only generate descriptions for certain tracked objects or zones. + +Optionally, you can generate the description using a snapshot (if enabled) by setting `use_snapshot` to `True`. By default, this is set to `False`, which sends the uncompressed images from the `detect` stream collected over the object's lifetime to the model. Once the object lifecycle ends, only a single compressed and cropped thumbnail is saved with the tracked object. Using a snapshot might be useful when you want to _regenerate_ a tracked object's description as it will provide the AI with a higher-quality image (typically downscaled by the AI itself) than the cropped/compressed thumbnail. Using a snapshot otherwise has a trade-off in that only a single image is sent to your provider, which will limit the model's ability to determine object movement or direction. + +Generative AI object descriptions can also be toggled dynamically for a camera via MQTT with the topic `frigate//object_descriptions/set`. See the [MQTT documentation](/integrations/mqtt#frigatecamera_nameobject_descriptionsset). + +## Usage and Best Practices + +Frigate's thumbnail search excels at identifying specific details about tracked objects -- for example, using an "image caption" approach to find a "person wearing a yellow vest," "a white dog running across the lawn," or "a red car on a residential street." To enhance this further, Frigate's default prompts are designed to ask your AI provider about the intent behind the object's actions, rather than just describing its appearance. + +While generating simple descriptions of detected objects is useful, understanding intent provides a deeper layer of insight. Instead of just recognizing "what" is in a scene, Frigate's default prompts aim to infer "why" it might be there or "what" it could do next. Descriptions tell you what's happening, but intent gives context. For instance, a person walking toward a door might seem like a visitor, but if they're moving quickly after hours, you can infer a potential break-in attempt. Detecting a person loitering near a door at night can trigger an alert sooner than simply noting "a person standing by the door," helping you respond based on the situation's context. + +## Custom Prompts + +Frigate sends multiple frames from the tracked object along with a prompt to your Generative AI provider asking it to generate a description. The default prompt is as follows: + +``` +Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next. +``` + +:::tip + +Prompts can use variable replacements `{label}`, `{sub_label}`, and `{camera}` to substitute information from the tracked object as part of the prompt. + +::: + +You can define custom prompts at the global level and per-object type. To configure custom prompts: + + + + +1. Navigate to . + - Expand the **GenAI object config** section + - Set **Caption prompt** to your custom prompt text + - Under **Object prompts**, add entries keyed by object type (e.g., `person`, `car`) with custom prompts for each + + + + +```yaml +genai: + provider: ollama + base_url: http://localhost:11434 + model: qwen3-vl:8b-instruct + +objects: + genai: + prompt: "Analyze the {label} in these images from the {camera} security camera. Focus on the actions, behavior, and potential intent of the {label}, rather than just describing its appearance." + object_prompts: + person: "Examine the main person in these images. What are they doing and what might their actions suggest about their intent (e.g., approaching a door, leaving an area, standing still)? Do not describe the surroundings or static details." + car: "Observe the primary vehicle in these images. Focus on its movement, direction, or purpose (e.g., parking, approaching, circling). If it's a delivery vehicle, mention the company." +``` + + + + +Prompts can also be overridden at the camera level to provide a more detailed prompt to the model about your specific camera. To configure camera-level overrides: + + + + +1. Navigate to for the desired camera. + - Expand the **GenAI object config** section + - Set **Enable GenAI** to on + - Set **Use snapshots** to on if desired + - Set **Caption prompt** to a camera-specific prompt + - Under **Object prompts**, add entries keyed by object type with camera-specific prompts + - Set **GenAI objects** to the list of object types that should receive descriptions (e.g., `person`, `cat`) + - Set **Required zones** to limit descriptions to objects in specific zones (e.g., `steps`) + + + + +```yaml +cameras: + front_door: + objects: + genai: + enabled: True + use_snapshot: True + prompt: "Analyze the {label} in these images from the {camera} security camera at the front door. Focus on the actions and potential intent of the {label}." + object_prompts: + person: "Examine the person in these images. What are they doing, and how might their actions suggest their purpose (e.g., delivering something, approaching, leaving)? If they are carrying or interacting with a package, include details about its source or destination." + cat: "Observe the cat in these images. Focus on its movement and intent (e.g., wandering, hunting, interacting with objects). If the cat is near the flower pots or engaging in any specific actions, mention it." + objects: + - person + - cat + required_zones: + - steps +``` + + + + +### Experiment with prompts + +Many providers also have a public facing chat interface for their models. Download a couple of different thumbnails or snapshots from Frigate and try new things in the playground to get descriptions to your liking before updating the prompt in Frigate. + +- OpenAI - [ChatGPT](https://chatgpt.com) +- Gemini - [Google AI Studio](https://aistudio.google.com) +- Ollama - [Open WebUI](https://docs.openwebui.com/) diff --git a/docs/docs/configuration/genai/review_summaries.md b/docs/docs/configuration/genai/review_summaries.md new file mode 100644 index 0000000..e492a48 --- /dev/null +++ b/docs/docs/configuration/genai/review_summaries.md @@ -0,0 +1,203 @@ +--- +id: genai_review +title: Review Summaries +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Generative AI can be used to automatically generate structured summaries of review items. These summaries will show up in Frigate's native notifications as well as in the UI. Generative AI can also be used to take a collection of summaries over a period of time and provide a report, which may be useful to get a quick report of everything that happened while out for some amount of time. + +Requests for a summary are requested automatically to your AI provider for alert review items when the activity has ended, they can also be optionally enabled for detections as well. + +Generative AI review summaries can also be toggled dynamically for a [camera via MQTT](/integrations/mqtt#frigatecamera_namereview_descriptionsset). + +## Review Summary Usage and Best Practices + +Review summaries provide structured JSON responses that are saved for each review item: + +``` +- `title` (string): A concise, direct title that describes the purpose or overall action (e.g., "Person taking out trash", "Joe walking dog"). +- `scene` (string): A narrative description of what happens across the sequence from start to finish, including setting, detected objects, and their observable actions. +- `shortSummary` (string): A brief 2-sentence summary of the scene, suitable for notifications. This is a condensed version of the scene description. +- `confidence` (float): 0-1 confidence in the analysis. Higher confidence when objects/actions are clearly visible and context is unambiguous. +- `other_concerns` (list): List of user-defined concerns that may need additional investigation. +- `potential_threat_level` (integer): 0, 1, or 2 as defined below. +``` + +This will show in multiple places in the UI to give additional context about each activity, and allow viewing more details when extra attention is required. Frigate's built in notifications will automatically show the title and `shortSummary` when the data is available, while the full `scene` description is available in the UI for detailed review. + +### Defining Typical Activity + +Each installation and even camera can have different parameters for what is considered suspicious activity. Frigate allows the `activity_context_prompt` to be defined globally and at the camera level, which allows you to define more specifically what should be considered normal activity. It is important that this is not overly specific as it can sway the output of the response. + +To configure the activity context prompt: + + + + +Navigate to . + +- Set **GenAI config > Activity context prompt** to your custom activity context text + + + + +```yaml +review: + genai: + activity_context_prompt: | + ### Normal Activity Indicators (Level 0) + - Known/verified people in any zone at any time + ... +``` + + + + +
+ Default Activity Context Prompt + +```yaml +review: + genai: + activity_context_prompt: | + ### Normal Activity Indicators (Level 0) + - Known/verified people in any zone at any time + - People with pets in residential areas + - Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving + - Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime + - Activity confined to public areas only (sidewalks, streets) without entering property at any time + + ### Suspicious Activity Indicators (Level 1) + - **Testing or attempting to open doors/windows/handles on vehicles or buildings** — ALWAYS Level 1 regardless of time or duration + - **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** — ALWAYS Level 1 regardless of activity or duration + - Taking items that don't belong to them (packages, objects from porches/driveways) + - Climbing or jumping fences/barriers to access property + - Attempting to conceal actions or items from view + - Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence + + ### Critical Threat Indicators (Level 2) + - Holding break-in tools (crowbars, pry bars, bolt cutters) + - Weapons visible (guns, knives, bats used aggressively) + - Forced entry in progress + - Physical aggression or violence + - Active property damage or theft in progress + + ### Assessment Guidance + Evaluate in this order: + + 1. **If person is verified/known** → Level 0 regardless of time or activity + 2. **If person is unidentified:** + - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1 + - Check actions: If testing doors/handles, taking items, climbing → Level 1 + - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service worker) → Level 0 + 3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1) + + The mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is. +``` + +
+ +### Image Source + +By default, review summaries use preview images (cached preview frames) which have a lower resolution but use fewer tokens per image. For better image quality and more detailed analysis, configure Frigate to extract frames directly from recordings at a higher resolution. + + + + +Navigate to . + +- Set **GenAI config > Enable GenAI descriptions** to on +- Set **GenAI config > Review image source** to `recordings` (default is `preview`) + + + + +```yaml +review: + genai: + enabled: true + # highlight-next-line + image_source: recordings # Options: "preview" (default) or "recordings" +``` + + + + +When using `recordings`, frames are extracted at 480px height while maintaining the camera's original aspect ratio, providing better detail for the LLM while being mindful of context window size. This is particularly useful for scenarios where fine details matter, such as identifying license plates, reading text, or analyzing distant objects. + +The number of frames sent to the LLM is dynamically calculated based on: + +- Your LLM provider's context window size +- The camera's resolution and aspect ratio (ultrawide cameras like 32:9 use more tokens per image) +- The image source (recordings use more tokens than preview images) + +Frame counts are automatically optimized to use ~98% of the available context window while capping at 20 frames maximum to ensure reasonable inference times. Note that using recordings will: + +- Provide higher quality images to the LLM (480p vs 180p preview images) +- Use more tokens per image due to higher resolution +- Result in fewer frames being sent for ultrawide cameras due to larger image size +- Require that recordings are enabled for the camera + +If recordings are not available for a given time period, the system will automatically fall back to using preview frames. + +### Additional Concerns + +Along with the concern of suspicious activity or immediate threat, you may have concerns such as animals in your garden or a gate being left open. Configure these concerns so that review summaries will make note of them if the activity requires additional review. + + + + +Navigate to . + +- Set **GenAI config > Additional concerns** to a list of your concerns (e.g., `animals in the garden`) + + + + +```yaml {4,5} +review: + genai: + enabled: true + additional_concerns: + - animals in the garden +``` + + + + +### Preferred Language + +By default, review summaries are generated in English. Configure Frigate to generate summaries in your preferred language by setting the `preferred_language` option. + + + + +Navigate to . + +- Set **GenAI config > Preferred language** to the desired language (e.g., `Spanish`) + + + + +```yaml {4} +review: + genai: + enabled: true + preferred_language: Spanish +``` + + + + +## Review Reports + +Along with individual review item summaries, Generative AI can also produce a single report of review items from all cameras marked "suspicious" over a specified time period (for example, a daily summary of suspicious activity while you're on vacation). + +### Requesting Reports Programmatically + +Review reports can be requested via the [API](/integrations/api/generate-review-summary-review-summarize-start-start-ts-end-end-ts-post) by sending a POST request to `/api/review/summarize/start/{start_ts}/end/{end_ts}` with Unix timestamps. + +For Home Assistant users, there is a built-in service (`frigate.review_summarize`) that makes it easy to request review reports as part of automations or scripts. This allows you to automatically generate daily summaries, vacation reports, or custom time period reports based on your specific needs. diff --git a/docs/docs/configuration/go2rtc.md b/docs/docs/configuration/go2rtc.md new file mode 100644 index 0000000..c7e5b08 --- /dev/null +++ b/docs/docs/configuration/go2rtc.md @@ -0,0 +1,70 @@ +--- +id: go2rtc +title: go2rtc +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Frigate uses the bundled go2rtc to power a number of key features: + +- WebRTC or MSE for live viewing with audio, higher resolutions and frame rates than the jsmpeg stream which is limited to the detect stream and does not support audio +- Live stream support for cameras in Home Assistant Integration +- RTSP relay for use with other consumers to reduce the number of connections to your camera streams + +:::tip[Most users no longer need to configure go2rtc by hand] + +The **camera setup wizard** is the recommended way to add cameras. Click **Add Camera** in , and the wizard probes your camera and writes its configuration for you, including the go2rtc restream and the live stream mapping, so go2rtc is set up automatically. + +This guide is mainly useful if you are **upgrading from an older version and have existing cameras that don't yet use go2rtc**, or if you want to fine-tune a stream by hand (for example, to transcode a codec your browser can't play). The [go2rtc troubleshooting guide](/troubleshooting/go2rtc) applies regardless of how your cameras were added. + +::: + +## Adding a go2rtc stream manually + +If you added your cameras with the wizard, go2rtc is already configured. You can skip straight to [troubleshooting](/troubleshooting/go2rtc). The steps below are for upgrading users with existing cameras that aren't using go2rtc yet, or for anyone who prefers to configure a stream by hand. + +Configure go2rtc to connect to your camera by adding the stream you want to use for live view. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#module-streams), not just rtsp. + +:::tip + +For the best experience, set the stream name under `go2rtc` to match the name of your camera so that Frigate will automatically map it and be able to use better live view options for the camera. + +See [the live view docs](/configuration/live#setting-streams-for-live-ui) for more information. + +::: + + + + +Navigate to and click **Add stream**. Give the stream a name (use the camera's name so Frigate can auto-map it - for example, if your camera's name is `back`, use `back` as the go2rtc stream name), then paste the camera's stream URL into the **Source** field. Save the section. + + + + +```yaml +go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 +``` + + + + +After adding this to the config, restart Frigate and try to watch the live stream for a single camera by clicking on it from the dashboard. It should look much clearer and more fluent than the original jsmpeg stream. + +### Next steps + +1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera). +2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports specific audio formats and may require opening ports on your router. +3. If your camera supports two-way talk, you must configure your stream with `#backchannel=0` to prevent go2rtc from blocking other applications from accessing the camera's audio output. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation. + +## Troubleshooting + +If your stream won't play, has no audio, uses excessive CPU, or otherwise misbehaves, see the dedicated [go2rtc troubleshooting guide](/troubleshooting/go2rtc). It walks through how to isolate where the problem is and covers the most common issues: unsupported codecs, H.265/HEVC, audio, WebRTC and two-way talk, hardware-accelerated transcoding with FFmpeg 8, and camera-specific quirks. + +## Homekit Configuration + +To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`. diff --git a/docs/docs/configuration/hardware_acceleration_enrichments.md b/docs/docs/configuration/hardware_acceleration_enrichments.md new file mode 100644 index 0000000..fc246df --- /dev/null +++ b/docs/docs/configuration/hardware_acceleration_enrichments.md @@ -0,0 +1,34 @@ +--- +id: hardware_acceleration_enrichments +title: Enrichments +--- + +# Enrichments + +Some of Frigate's enrichments can use a discrete GPU or integrated GPU for accelerated processing. + +## Requirements + +Object detection and enrichments (like Semantic Search, Face Recognition, and License Plate Recognition) are independent features. To use a GPU / NPU for object detection, see the [Object Detectors](/configuration/object_detectors.md) documentation. If you want to use your GPU for any supported enrichments, you must choose the appropriate Frigate Docker image for your GPU / NPU and configure the enrichment according to its specific documentation. + +- **AMD** + - ROCm support in the `-rocm` Frigate image is automatically detected for enrichments, but only some enrichment models are available due to ROCm's focus on LLMs and limited stability with certain neural network models. Frigate disables models that perform poorly or are unstable to ensure reliable operation, so only compatible enrichments may be active. + +- **Intel** + - OpenVINO will automatically be detected and used for enrichments in the default Frigate image. + - **Note:** Intel NPUs have limited model support for enrichments. GPU is recommended for enrichments when available. + +- **Nvidia** + - Nvidia GPUs will automatically be detected and used for enrichments in the `-tensorrt` Frigate image. + - Jetson devices will automatically be detected and used for enrichments in the `-tensorrt-jp6` Frigate image. + +- **RockChip** + - RockChip NPU will automatically be detected and used for semantic search v1 and face recognition in the `-rk` Frigate image. + +Utilizing a GPU for enrichments does not require you to use the same GPU for object detection. For example, you can run the `tensorrt` Docker image to run enrichments on an Nvidia GPU and still use other dedicated hardware like a Coral or Hailo for object detection. However, one combination that is not supported is the `tensorrt` image for object detection on an Nvidia GPU and Intel iGPU for enrichments. + +:::note + +A Google Coral is a TPU (Tensor Processing Unit), not a dedicated GPU (Graphics Processing Unit) and therefore does not provide any kind of acceleration for Frigate's enrichments. + +::: diff --git a/docs/docs/configuration/hardware_acceleration_video.md b/docs/docs/configuration/hardware_acceleration_video.md new file mode 100644 index 0000000..b9312c3 --- /dev/null +++ b/docs/docs/configuration/hardware_acceleration_video.md @@ -0,0 +1,534 @@ +--- +id: hardware_acceleration_video +title: Video Decoding +--- + +import CommunityBadge from '@site/src/components/CommunityBadge'; +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +# Video Decoding + +It is highly recommended to use an integrated or discrete GPU for hardware acceleration video decoding in Frigate. + +Some types of hardware acceleration are detected and used automatically, but you may need to update your configuration to enable hardware accelerated decoding in ffmpeg. To verify that hardware acceleration is working: + +- Check the logs: A message will either say that hardware acceleration was automatically detected, or there will be a warning that no hardware acceleration was automatically detected +- If hardware acceleration is specified in the config, verification can be done by ensuring the logs are free from errors. There is no CPU fallback for hardware acceleration. + +Frigate supports presets for optimal hardware accelerated video decoding: + +**AMD** + +- [AMD](#amd-based-cpus): Frigate can utilize modern AMD integrated GPUs and AMD discrete GPUs to accelerate video decoding. + +**Intel** + +- [Intel](#intel-based-cpus): Frigate can utilize most Intel integrated GPUs and Arc GPUs to accelerate video decoding. + +**Nvidia GPU** + +- [Nvidia GPU](#nvidia-gpus): Frigate can utilize most modern Nvidia GPUs to accelerate video decoding. + +**Raspberry Pi 3/4** + +- [Raspberry Pi](#raspberry-pi-34): Frigate can utilize the media engine in the Raspberry Pi 3 and 4 to slightly accelerate video decoding. + +**Nvidia Jetson** + +- [Jetson](#nvidia-jetson): Frigate can utilize the media engine in Jetson hardware to accelerate video decoding. + +**Rockchip** + +- [RKNN](#rockchip-platform): Frigate can utilize the media engine in RockChip SOCs to accelerate video decoding. + +**Other Hardware** + +Depending on your system, these presets may not be compatible, and you may need to use manual hwaccel args to take advantage of your hardware. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro + +## Intel-based CPUs + +Frigate can utilize most Intel integrated GPUs and Arc GPUs to accelerate video decoding. + +**Recommended hwaccel Preset** + +| CPU Generation | Intel Driver | Recommended Preset | Notes | +| ------------------ | ------------ | ------------------- | ------------------------------------------- | +| gen1 - gen5 | i965 | preset-vaapi | qsv is not supported, may not support H.265 | +| gen6 - gen7 | iHD | preset-vaapi | qsv is not supported | +| gen8 - gen12 | iHD | preset-vaapi | preset-intel-qsv-\* can also be used | +| gen13+ | iHD / Xe | preset-intel-qsv-\* | | +| Intel Arc A-series | iHD / Xe | preset-intel-qsv-\* | | +| Intel Arc B-series | iHD / Xe | preset-intel-qsv-\* | Requires host kernel 6.12+ | + +:::note + +The default driver is `iHD`. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the `config.yml` for HA App users](advanced/system.md#environment_vars). + +See [The Intel Docs](https://www.intel.com/content/www/us/en/support/articles/000005505/processors.html) to figure out what generation your CPU is. + +::: + +### Via VAAPI + +VAAPI supports automatic profile selection so it will work automatically with both H.264 and H.265 streams. + + + + +Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to . + + + + +```yaml +ffmpeg: + hwaccel_args: preset-vaapi +``` + + + + +### Via Quicksync + +#### H.264 streams + + + + +Navigate to and set **Hardware acceleration arguments** to `Intel QuickSync (H.264)`. For per-camera overrides, navigate to . + + + + +```yaml +ffmpeg: + hwaccel_args: preset-intel-qsv-h264 +``` + + + + +#### H.265 streams + + + + +Navigate to and set **Hardware acceleration arguments** to `Intel QuickSync (H.265)`. For per-camera overrides, navigate to . + + + + +```yaml +ffmpeg: + hwaccel_args: preset-intel-qsv-h265 +``` + + + + +### Configuring Intel GPU Stats + +Frigate reads Intel GPU utilization directly from the kernel's per-client DRM usage counters exposed at `/proc//fdinfo/`. This requires: + +- Linux kernel **5.19 or newer** for the `i915` driver, or any release of the `xe` driver. +- Frigate running with permission to read other processes' fdinfo. Running as root inside the container (the default) satisfies this; non-root setups may need `CAP_SYS_PTRACE`. + +No `intel_gpu_top` binary, `CAP_PERFMON`, privileged mode, or `perf_event_paranoid` tuning is required. + +#### Stats for SR-IOV or specific devices + +If the host has more than one Intel GPU (e.g. an iGPU plus a discrete GPU, or SR-IOV virtual functions), pin stats collection to a specific device by setting `intel_gpu_device` to either its PCI bus address or a DRM card/render-node path: + +```yaml +telemetry: + stats: + intel_gpu_device: "0000:00:02.0" +``` + +```yaml +telemetry: + stats: + intel_gpu_device: "/dev/dri/card1" +``` + +When passing a device path, make sure the device is also passed through to the container. + +## AMD-based CPUs + +Frigate can utilize modern AMD integrated GPUs and AMD GPUs to accelerate video decoding using VAAPI. + +### Configuring Radeon Driver + +You need to change the driver to `radeonsi` by adding the following environment variable `LIBVA_DRIVER_NAME=radeonsi` to your docker-compose file or [in the `config.yml` for HA App users](advanced/system.md#environment_vars). + +### Via VAAPI + +VAAPI supports automatic profile selection so it will work automatically with both H.264 and H.265 streams. + + + + +Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to . + + + + +```yaml +ffmpeg: + hwaccel_args: preset-vaapi +``` + + + + +## NVIDIA GPUs + +While older GPUs may work, it is recommended to use modern, supported GPUs. NVIDIA provides a [matrix of supported GPUs and features](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new). If your card is on the list and supports CUVID/NVDEC, it will most likely work with Frigate for decoding. However, you must also use [a driver version that will work with FFmpeg](https://github.com/FFmpeg/nv-codec-headers/blob/master/README). Older driver versions may be missing symbols and fail to work, and older cards are not supported by newer driver versions. The only way around this is to [provide your own FFmpeg](/configuration/advanced/system#custom-ffmpeg-build) that will work with your driver version, but this is unsupported and may not work well if at all. + +A more complete list of cards and their compatible drivers is available in the [driver release readme](https://download.nvidia.com/XFree86/Linux-x86_64/525.85.05/README/supportedchips.html). + +If your distribution does not offer NVIDIA driver packages, you can [download them here](https://www.nvidia.com/en-us/drivers/unix/). + +### Configuring Nvidia GPUs in Docker + +Additional configuration is needed for the Docker container to be able to access the NVIDIA GPU. The supported method for this is to install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker) and specify the GPU to Docker. How you do this depends on how Docker is being run: + +#### Docker Compose - Nvidia GPU + +```yaml {5-12} +services: + frigate: + ... + image: ghcr.io/blakeblackshear/frigate:stable-tensorrt + deploy: # <------------- Add this section + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['0'] # this is only needed when using multiple GPUs + count: 1 # number of GPUs + capabilities: [gpu] +``` + +#### Docker Run CLI - Nvidia GPU + +```bash {4} +docker run -d \ + --name frigate \ + ... + --gpus=all \ + ghcr.io/blakeblackshear/frigate:stable-tensorrt +``` + +### Setup Decoder + +Using `preset-nvidia` ffmpeg will automatically select the necessary profile for the incoming video, and will log an error if the profile is not supported by your GPU. + + + + +Navigate to and set **Hardware acceleration arguments** to `NVIDIA GPU`. For per-camera overrides, navigate to . + + + + +```yaml +ffmpeg: + hwaccel_args: preset-nvidia +``` + + + + +If everything is working correctly, you should see a significant improvement in performance. +Verify that hardware decoding is working by running `nvidia-smi`, which should show `ffmpeg` +processes: + +:::note + +`nvidia-smi` will not show `ffmpeg` processes when run inside the container [due to docker limitations](https://github.com/NVIDIA/nvidia-docker/issues/179#issuecomment-645579458). + +::: + +``` ++-----------------------------------------------------------------------------+ +| NVIDIA-SMI 455.38 Driver Version: 455.38 CUDA Version: 11.1 | +|-------------------------------+----------------------+----------------------+ +| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | +| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | +| | | MIG M. | +|===============================+======================+======================| +| 0 GeForce GTX 166... Off | 00000000:03:00.0 Off | N/A | +| 38% 41C P2 36W / 125W | 2082MiB / 5942MiB | 5% Default | +| | | N/A | ++-------------------------------+----------------------+----------------------+ + ++-----------------------------------------------------------------------------+ +| Processes: | +| GPU GI CI PID Type Process name GPU Memory | +| ID ID Usage | +|=============================================================================| +| 0 N/A N/A 12737 C ffmpeg 249MiB | +| 0 N/A N/A 12751 C ffmpeg 249MiB | +| 0 N/A N/A 12772 C ffmpeg 249MiB | +| 0 N/A N/A 12775 C ffmpeg 249MiB | +| 0 N/A N/A 12800 C ffmpeg 249MiB | +| 0 N/A N/A 12811 C ffmpeg 417MiB | +| 0 N/A N/A 12827 C ffmpeg 417MiB | ++-----------------------------------------------------------------------------+ +``` + +If you do not see these processes, check the `docker logs` for the container and look for decoding errors. + +These instructions were originally based on the [Jellyfin documentation](https://jellyfin.org/docs/general/administration/hardware-acceleration.html#nvidia-hardware-acceleration-on-docker-linux). + +## Raspberry Pi 3/4 + +Ensure you increase the allocated RAM for your GPU to at least 128 (`raspi-config` > Performance Options > GPU Memory). +If you are using the HA App, you may need to use the full access variant and turn off _Protection mode_ for hardware acceleration. + + + + +Navigate to and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)` (for H.264 streams) or `Raspberry Pi (H.265)` (for H.265/HEVC streams). For per-camera overrides, navigate to . + + + + +```yaml +# if you want to decode a h264 stream +ffmpeg: + hwaccel_args: preset-rpi-64-h264 + +# if you want to decode a h265 (hevc) stream +ffmpeg: + hwaccel_args: preset-rpi-64-h265 +``` + + + + +:::note + +If running Frigate through Docker, you either need to run in privileged mode or +map the `/dev/video*` devices to Frigate. With Docker Compose add: + +```yaml {4-5} +services: + frigate: + ... + devices: + - /dev/video11:/dev/video11 +``` + +Or with `docker run`: + +```bash {4} +docker run -d \ + --name frigate \ + ... + --device /dev/video11 \ + ghcr.io/blakeblackshear/frigate:stable +``` + +`/dev/video11` is the correct device (on Raspberry Pi 4B). You can check +by running the following and looking for `H264`: + +```bash +for d in /dev/video*; do + echo -e "---\n$d" + v4l2-ctl --list-formats-ext -d $d +done +``` + +Or map in all the `/dev/video*` devices. + +::: + +# Community Supported + +## NVIDIA Jetson + +A separate set of docker images is available for Jetson devices. They come with an `ffmpeg` build with codecs that use the Jetson's dedicated media engine. If your Jetson host is running Jetpack 6.0+ use the `stable-tensorrt-jp6` tagged image. Note that the Orin Nano has no video encoder, so frigate will use software encoding on this platform, but the image will still allow hardware decoding and tensorrt object detection. + +You will need to use the image with the nvidia container runtime: + +### Docker Run CLI - Jetson + +```bash {3} +docker run -d \ + ... + --runtime nvidia + ghcr.io/blakeblackshear/frigate:stable-tensorrt-jp6 +``` + +### Docker Compose - Jetson + +```yaml {5} +services: + frigate: + ... + image: ghcr.io/blakeblackshear/frigate:stable-tensorrt-jp6 + runtime: nvidia # Add this +``` + +:::note + +The `runtime:` tag is not supported on older versions of docker-compose. If you run into this, you can instead use the nvidia runtime system-wide by adding `"default-runtime": "nvidia"` to `/etc/docker/daemon.json`: + +``` +{ + "runtimes": { + "nvidia": { + "path": "nvidia-container-runtime", + "runtimeArgs": [] + } + }, + "default-runtime": "nvidia" +} +``` + +::: + +### Setup Decoder + +The decoder you need to pass in the `hwaccel_args` will depend on the input video. + +A list of supported codecs (you can use `ffmpeg -decoders | grep nvmpi` in the container to get the ones your card supports) + +``` + V..... h264_nvmpi h264 (nvmpi) (codec h264) + V..... hevc_nvmpi hevc (nvmpi) (codec hevc) + V..... mpeg2_nvmpi mpeg2 (nvmpi) (codec mpeg2video) + V..... mpeg4_nvmpi mpeg4 (nvmpi) (codec mpeg4) + V..... vp8_nvmpi vp8 (nvmpi) (codec vp8) + V..... vp9_nvmpi vp9 (nvmpi) (codec vp9) +``` + +For example, for H264 video, you'll select `preset-jetson-h264`. + + + + +Navigate to and set **Hardware acceleration arguments** to `NVIDIA Jetson (H.264)` (or `NVIDIA Jetson (H.265)` for HEVC streams). For per-camera overrides, navigate to . + + + + +```yaml +ffmpeg: + hwaccel_args: preset-jetson-h264 +``` + + + + +If everything is working correctly, you should see a significant reduction in ffmpeg CPU load and power consumption. +Verify that hardware decoding is working by running `jtop` (`sudo pip3 install -U jetson-stats`), which should show +that NVDEC/NVDEC1 are in use. + +## Rockchip platform + +Hardware accelerated video de-/encoding is supported on all Rockchip SoCs using [Nyanmisaka's FFmpeg 6.1 Fork](https://github.com/nyanmisaka/ffmpeg-rockchip) based on [Rockchip's mpp library](https://github.com/rockchip-linux/mpp). + +### Prerequisites + +Make sure to follow the [Rockchip specific installation instructions](/frigate/installation#rockchip-platform). + +### Configuration + +Set the FFmpeg hwaccel preset to enable hardware video processing. + + + + +Navigate to and set **Hardware acceleration arguments** to `Rockchip RKMPP`. For per-camera overrides, navigate to . + + + + +```yaml +ffmpeg: + hwaccel_args: preset-rkmpp +``` + + + + +:::note + +Make sure that your SoC supports hardware acceleration for your input stream. For example, if your camera streams with h265 encoding and a 4k resolution, your SoC must be able to de- and encode h265 with a 4k resolution or higher. If you are unsure whether your SoC meets the requirements, take a look at the datasheet. + +::: + +:::warning + +If one or more of your cameras are not properly processed and this error is shown in the logs: + +``` +[segment @ 0xaaaaff694790] Timestamps are unset in a packet for stream 0. This is deprecated and will stop working in the future. Fix your code to set the timestamps properly +[Parsed_scale_rkrga_0 @ 0xaaaaff819070] No hw context provided on input +[Parsed_scale_rkrga_0 @ 0xaaaaff819070] Failed to configure output pad on Parsed_scale_rkrga_0 +Error initializing filters! +Error marking filters as finished +[out#1/rawvideo @ 0xaaaaff3d8730] Nothing was written into output file, because at least one of its streams received no packets. +Restarting ffmpeg... +``` + +you should try to upgrade to FFmpeg 7. This can be done using this config option: + +```yaml +ffmpeg: + path: "7.0" +``` + +You can set this option globally to use FFmpeg 7 for all cameras or on camera level to use it only for specific cameras. Do not confuse this option with: + +```yaml +cameras: + name: + ffmpeg: + inputs: + - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 +``` + +::: + +## Synaptics + +Hardware accelerated video de-/encoding is supported on Synpatics SL-series SoC. + +### Prerequisites + +Make sure to follow the [Synaptics specific installation instructions](/frigate/installation#synaptics). + +### Configuration + +Set the FFmpeg hwaccel args to enable hardware video processing. + + + + +Navigate to and configure the hardware acceleration args and input args manually for Synaptics hardware. For per-camera overrides, navigate to . + + + + +```yaml {2} +ffmpeg: + hwaccel_args: -c:v h264_v4l2m2m + input_args: preset-rtsp-restream +output_args: + record: preset-record-generic-audio-aac +``` + + + + +:::warning + +Make sure that your SoC supports hardware acceleration for your input stream and your input stream is h264 encoding. For example, if your camera streams with h264 encoding, your SoC must be able to de- and encode with it. If you are unsure whether your SoC meets the requirements, take a look at the datasheet. + +::: diff --git a/docs/docs/configuration/license_plate_recognition.md b/docs/docs/configuration/license_plate_recognition.md new file mode 100644 index 0000000..eae92a9 --- /dev/null +++ b/docs/docs/configuration/license_plate_recognition.md @@ -0,0 +1,735 @@ +--- +id: license_plate_recognition +title: License Plate Recognition (LPR) +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; +import FaqItem from "@site/src/components/FaqItem"; + +Frigate can recognize license plates on vehicles and automatically add the detected characters to the `recognized_license_plate` field or a [known](#matching) name as a `sub_label` to tracked objects of type `car` or `motorcycle`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street. + +LPR works best when the license plate is clearly visible to the camera. For moving vehicles, Frigate continuously refines the recognition process, keeping the most confident result. When a vehicle becomes stationary, LPR continues to run for a short time after to attempt recognition. + +:::info + +License plate recognition requires a one-time internet connection to download OCR and detection models from GitHub. Once cached, models work fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details. + +::: + +When a plate is recognized, the details are: + +- Added as a `sub_label` (if [known](#matching)) or the `recognized_license_plate` field (if unknown) to a tracked object. +- Viewable in the Details pane in Review/History. +- Viewable in the Tracked Object Details pane in Explore (sub labels and recognized license plates). +- Filterable through the More Filters menu in Explore. +- Published via the `frigate/events` MQTT topic as a `sub_label` ([known](#matching)) or `recognized_license_plate` (unknown) for the `car` or `motorcycle` tracked object. +- Published via the `frigate/tracked_object_update` MQTT topic with `name` (if [known](#matching)) and `plate`. + +## Model Requirements + +Users running a Frigate+ model (or any custom model that natively detects license plates) should ensure that `license_plate` is added to the [list of objects to track](https://docs.frigate.video/plus/#available-label-types) either globally or for a specific camera. This will improve the accuracy and performance of the LPR model. + +Users without a model that detects license plates can still run LPR. Frigate uses a lightweight YOLOv9 license plate detection model that can be configured to run on your CPU or GPU. In this case, you should _not_ define `license_plate` in your list of objects to track. + +:::note + +In the default mode, Frigate's LPR needs to first detect a `car` or `motorcycle` before it can recognize a license plate. If you're using a dedicated LPR camera and have a zoomed-in view where a `car` or `motorcycle` will not be detected, you can still run LPR, but the configuration parameters will differ from the default mode. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section below. + +::: + +## Minimum System Requirements + +License plate recognition works by running AI models locally on your system. The YOLOv9 plate detector model and the OCR models ([PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)) are relatively lightweight and can run on your CPU or GPU, depending on your configuration. At least 4GB of RAM and a CPU with AVX + AVX2 instructions is required. + +## Configuration + +License plate recognition is disabled by default and must be enabled before it can be used. + + + + +Navigate to . + +- Set **Enable LPR** to on + + + + +```yaml +lpr: + enabled: True +``` + + + + +Like other enrichments in Frigate, LPR **must be enabled globally** to use the feature. Disable it for specific cameras at the camera level if you don't want to run LPR on cars on those cameras. + + + + +Navigate to for the desired camera and disable the **Enable LPR** toggle. + + + + +```yaml {4,5} +cameras: + garage: + ... + lpr: + enabled: False +``` + + + + +For non-dedicated LPR cameras, ensure that your camera is configured to detect objects of type `car` or `motorcycle`, and that a car or motorcycle is actually being detected by Frigate. Otherwise, LPR will not run. + +Like the other real-time processors in Frigate, license plate recognition runs on the camera stream defined by the `detect` role in your config. To ensure optimal performance, select a suitable resolution for this stream in your camera's firmware that fits your specific scene and requirements. + +## Advanced Configuration + +Fine-tune the LPR feature using these optional parameters. The only optional parameters that can be set at the camera level are `enabled`, `min_area`, and `enhancement`. + +### Detection + + + + +Navigate to . + +- **Detection threshold**: License plate object detection confidence score required before recognition runs. This field only applies to the standalone license plate detection model; `threshold` and `min_score` object filters should be used for models like Frigate+ that have license plate detection built in. + - Default: `0.7` +- **Minimum plate area**: Minimum area (in pixels) a license plate must be before recognition runs. This is an _area_ measurement (length x width). For reference, 1000 pixels represents a ~32x32 pixel square in your camera image. Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant plates. + - Default: `1000` pixels +- **Device**: Device to use to run license plate detection _and_ recognition models. Auto-selected by Frigate and can be `CPU`, `GPU`, or the GPU's device number. For users without a model that detects license plates natively, using a GPU may increase performance of the YOLOv9 license plate detector model. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. + - Default: `None` +- **Model size**: The size of the model used to identify regions of text on plates. The `small` model is fast and identifies groups of Latin and Chinese characters. The `large` model identifies Latin characters only, and uses an enhanced text detector to find characters on multi-line plates. If your country or region does not use multi-line plates, you should use the `small` model. + - Default: `small` + + + + +```yaml +lpr: + enabled: True + detection_threshold: 0.7 + min_area: 1000 + device: CPU + model_size: small +``` + + + + +### Recognition + + + + +Navigate to . + +- **Recognition threshold**: Recognition confidence score required to add the plate to the object as a `recognized_license_plate` and/or `sub_label`. + - Default: `0.9` +- **Min plate length**: Minimum number of characters a detected license plate must have to be added as a `recognized_license_plate` and/or `sub_label`. Use this to filter out short, incomplete, or incorrect detections. +- **Plate format regex**: A regular expression defining the expected format of detected plates. Plates that do not match this format will be discarded. Websites like https://regex101.com/ can help test regular expressions for your plates. + + + + +```yaml +lpr: + enabled: True + recognition_threshold: 0.9 + min_plate_length: 4 + format: "^[A-Z]{2}[0-9]{2} [A-Z]{3}$" +``` + + + + +### Matching + + + + +Navigate to . + +- **Known plates**: Assign custom `sub_label` values to `car` and `motorcycle` objects when a recognized plate matches a known value. These labels appear in the UI, filters, and notifications. Unknown plates are still saved but are added to the `recognized_license_plate` field rather than the `sub_label`. +- **Match distance**: Allows for minor variations (missing/incorrect characters) when matching a detected plate to a known plate. For example, setting to `1` allows a plate `ABCDE` to match `ABCBE` or `ABCD`. This parameter will _not_ operate on known plates that are defined as regular expressions. + + + + +```yaml +lpr: + enabled: True + match_distance: 1 + known_plates: + Wife's Car: + - "ABC-1234" + Johnny: + - "J*N-*234" +``` + + + + +### Image Enhancement + + + + +Navigate to . + +- **Enhancement level**: A value between 0 and 10 that adjusts the level of image enhancement applied to captured license plates before they are processed for recognition. Higher values increase contrast, sharpen details, and reduce noise, but excessive enhancement can blur or distort characters. This setting is best adjusted at the camera level if running LPR on multiple cameras. + - Default: `0` (no enhancement) + + + + +```yaml +lpr: + enabled: True + enhancement: 1 +``` + + + + +If Frigate is already recognizing plates correctly, leave enhancement at the default of `0`. However, if you're experiencing frequent character issues or incomplete plates and you can already easily read the plates yourself, try increasing the value gradually, starting at 3 and adjusting as needed. Use the `debug_save_plates` configuration option (see below) to see how different enhancement levels affect your plates. + +### Normalization Rules + + + + +Navigate to . + +Under **Replacement rules**, add regex rules to normalize detected plate strings before matching. Rules fire in order. For example: + +| Pattern | Replacement | Description | +| ---------------- | ----------- | -------------------------------------------------- | +| `[%#*?]` | _(empty)_ | Remove noise symbols | +| `[= ]` | `-` | Normalize `=` or space to dash | +| `O` | `0` | Swap `O` to `0` (common OCR error) | +| `I` | `1` | Swap `I` to `1` | +| `(\w{3})(\w{3})` | `\1-\2` | Split 6 chars into groups (e.g., ABC123 → ABC-123) | + + + + +```yaml +lpr: + replace_rules: + - pattern: "[%#*?]" # Remove noise symbols + replacement: "" + - pattern: "[= ]" # Normalize = or space to dash + replacement: "-" + - pattern: "O" # Swap 'O' to '0' (common OCR error) + replacement: "0" + - pattern: "I" # Swap 'I' to '1' + replacement: "1" + - pattern: '(\w{3})(\w{3})' # Split 6 chars into groups (e.g., ABC123 → ABC-123) - use single quotes to preserve backslashes + replacement: '\1-\2' +``` + + + + +These rules must be defined at the global level of your `lpr` config. + +- Rules fire in order: In the example above: clean noise first, then separators, then swaps, then splits. +- Backrefs (`\1`, `\2`) allow dynamic replacements (e.g., capture groups). +- Any changes made by the rules are printed to the LPR debug log. +- Tip: You can test patterns with tools like regex101.com. + +### Debugging + + + + +Navigate to . + +- **Save debug plates**: Set to on to save captured text on plates for debugging. These images are stored in `/media/frigate/clips/lpr`, organized into subdirectories by `/`, and named based on the capture timestamp. + + + + +```yaml +lpr: + enabled: True + debug_save_plates: True +``` + + + + +The saved images are not full plates but rather the specific areas of text detected on the plates. It is normal for the text detection model to sometimes find multiple areas of text on the plate. Use them to analyze what text Frigate recognized and how image enhancement affects detection. + +**Note:** Frigate does **not** automatically delete these debug images. Once LPR is functioning correctly, you should disable this option and manually remove the saved files to free up storage. + +## Configuration Examples + +These configuration parameters are available at the global level. The only optional parameters that should be set at the camera level are `enabled`, `min_area`, and `enhancement`. + + + + +Navigate to . + +| Field | Description | +| ------------------------------ | ----------------------------------------------------------------------------------------------------- | +| **Enable LPR** | Set to on | +| **Minimum plate area** | Set to `1500` to ignore plates with an area (length x width) smaller than 1500 pixels | +| **Min plate length** | Set to `4` to only recognize plates with 4 or more characters | +| **Known plates > Wife's Car** | `ABC-1234`, `ABC-I234` (accounts for potential confusion between the number one and capital letter I) | +| **Known plates > Johnny** | `J*N-*234` (matches JHN-1234 and JMN-I234; `*` matches any number of characters) | +| **Known plates > Sally** | `[S5]LL 1234` (matches both SLL 1234 and 5LL 1234) | +| **Known plates > Work Trucks** | `EMP-[0-9]{3}[A-Z]` (matches plates like EMP-123A, EMP-456Z) | + + + + +```yaml +lpr: + enabled: True + min_area: 1500 # Ignore plates with an area (length x width) smaller than 1500 pixels + min_plate_length: 4 # Only recognize plates with 4 or more characters + known_plates: + Wife's Car: + - "ABC-1234" + - "ABC-I234" # Accounts for potential confusion between the number one (1) and capital letter I + Johnny: + - "J*N-*234" # Matches JHN-1234 and JMN-I234, but also note that "*" matches any number of characters + Sally: + - "[S5]LL 1234" # Matches both SLL 1234 and 5LL 1234 + Work Trucks: + - "EMP-[0-9]{3}[A-Z]" # Matches plates like EMP-123A, EMP-456Z +``` + + + + +:::note + +If a camera is configured to detect `car` or `motorcycle` but you don't want Frigate to run LPR for that camera, disable LPR at the camera level: + + + + +Navigate to for the desired camera and disable the **Enable LPR** toggle. + + + + +```yaml +cameras: + side_yard: + lpr: + enabled: False + ... +``` + + + + +::: + +## Dedicated LPR Cameras + +Dedicated LPR cameras are single-purpose cameras with powerful optical zoom to capture license plates on distant vehicles, often with fine-tuned settings to capture plates at night. + +To mark a camera as a dedicated LPR camera, set `type: "lpr"` in the camera configuration. + +:::note + +Frigate's dedicated LPR mode is optimized for cameras with a narrow field of view, specifically positioned and zoomed to capture license plates exclusively. If your camera provides a general overview of a scene rather than a tightly focused view, this mode is not recommended. + +::: + +Users can configure Frigate's dedicated LPR mode in two different ways depending on whether a Frigate+ (or native `license_plate` detecting) model is used: + +### Using a Frigate+ (or Native `license_plate` Detecting) Model + +Users running a Frigate+ model (or any model that natively detects `license_plate`) can take advantage of `license_plate` detection. This allows license plates to be treated as standard objects in dedicated LPR mode, meaning that alerts, detections, snapshots, and other Frigate features work as usual, and plates are detected efficiently through your configured object detector. + +An example configuration for a dedicated LPR camera using a `license_plate`-detecting model: + + + + +Navigate to and set **Enable LPR** to on. Set **Device** to `CPU` (can also be `GPU` if available). + +Navigate to and add your camera streams. + +Navigate to . + +| Field | Description | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| **Enable object detection** | Set to on | +| **Detect FPS** | Set to `5`. Increase to `10` if vehicles move quickly across your frame. Higher than 10 is unnecessary and is not recommended. | +| **Minimum initialization frames** | Set to `2` | +| **Detect width** | Set to `1920` | +| **Detect height** | Set to `1080` | + +Navigate to . + +| Field | Description | +| ---------------------------------------------- | ------------------- | +| **Objects to track** | Add `license_plate` | +| **Object filters > License Plate > Threshold** | Set to `0.7` | + +Navigate to . + +| Field | Description | +| -------------------- | --------------------------------------------------------------------- | +| **Motion threshold** | Set to `30` | +| **Contour area** | Set to `60`. Use an increased value to tune out small motion changes. | +| **Improve contrast** | Set to off | + +Also add a motion mask over your camera's timestamp so it is not incorrectly detected as a license plate. + +Navigate to . + +| Field | Description | +| -------------------- | -------------------------------------------------------- | +| **Enable recording** | Set to on. Disable recording if you only want snapshots. | + +Navigate to . + +| Field | Description | +| -------------------- | ----------- | +| **Enable snapshots** | Set to on | + + + + +```yaml +# LPR global configuration +lpr: + enabled: True + device: CPU # can also be GPU if available + +# Dedicated LPR camera configuration +cameras: + dedicated_lpr_camera: + type: "lpr" # required to use dedicated LPR camera mode + ffmpeg: ... # add your streams + detect: + enabled: True + fps: 5 # increase to 10 if vehicles move quickly across your frame. Higher than 10 is unnecessary and is not recommended. + min_initialized: 2 + width: 1920 + height: 1080 + objects: + track: + - license_plate + filters: + license_plate: + threshold: 0.7 + motion: + threshold: 30 + contour_area: 60 # use an increased value to tune out small motion changes + improve_contrast: false + mask: 0.704,0.007,0.709,0.052,0.989,0.055,0.993,0.001 # ensure your camera's timestamp is masked + record: + enabled: True # disable recording if you only want snapshots + snapshots: + enabled: True + review: + detections: + labels: + - license_plate +``` + + + + +With this setup: + +- License plates are treated as normal objects in Frigate. +- Scores, alerts, detections, and snapshots work as expected. +- Snapshots will have license plate bounding boxes on them. +- The `frigate/events` MQTT topic will publish tracked object updates. +- Debug view will display `license_plate` bounding boxes. +- If you are using a Frigate+ model and want to submit images from your dedicated LPR camera for model training and fine-tuning, annotate both the `car` / `motorcycle` and the `license_plate` in the snapshots on the Frigate+ website, even if the car is barely visible. + +### Using the Secondary LPR Pipeline (Without Frigate+) + +If you are not running a Frigate+ model, you can use Frigate's built-in secondary dedicated LPR pipeline. In this mode, Frigate bypasses the standard object detection pipeline and runs a local license plate detector model on the full frame whenever motion activity occurs. + +An example configuration for a dedicated LPR camera using the secondary pipeline: + + + + +Navigate to and set **Enable LPR** to on. Set **Device** to `CPU` (can also be `GPU` if available and the correct Docker image is used). Set **Detection threshold** to `0.7` (change if necessary). + +Navigate to for your dedicated LPR camera. + +| Field | Description | +| --------------------- | -------------------------------------------------------------------------------- | +| **Enable LPR** | Set to on | +| **Enhancement level** | Set to `3` (optional, enhances the image before trying to recognize characters) | + +Navigate to and add your camera streams. + +Navigate to . + +| Field | Description | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| **Enable object detection** | Set to off to disable Frigate's standard object detection pipeline | +| **Detect FPS** | Set to `5`. Increase if necessary, though high values may slow down Frigate's enrichments pipeline and use considerable CPU. | +| **Detect width** | Set to `1920` (recommended value, but depends on your camera) | +| **Detect height** | Set to `1080` (recommended value, but depends on your camera) | + +Navigate to . + +| Field | Description | +| -------------------- | -------------------------------------------------------------------------------------- | +| **Objects to track** | Set to an empty list, required when not using a Frigate+ model for dedicated LPR mode | + +Navigate to . + +| Field | Description | +| -------------------- | --------------------------------------------------------------------- | +| **Motion threshold** | Set to `30` | +| **Contour area** | Set to `60`. Use an increased value to tune out small motion changes. | +| **Improve contrast** | Set to off | + +Navigate to and add a motion mask over your camera's timestamp so it is not incorrectly detected as a license plate. + +Navigate to . + +| Field | Description | +| -------------------- | -------------------------------------------------------- | +| **Enable recording** | Set to on. Disable recording if you only want snapshots. | + +Navigate to . + +| Field | Description | +| ----------------------------------------- | --------------- | +| **Detections config > Enable detections** | Set to on | +| **Detections config > Retain > Default** | Set to `7` days | + + + + +```yaml +# LPR global configuration +lpr: + enabled: True + device: CPU # can also be GPU if available and correct Docker image is used + detection_threshold: 0.7 # change if necessary + +# Dedicated LPR camera configuration +cameras: + dedicated_lpr_camera: + type: "lpr" # required to use dedicated LPR camera mode + lpr: + enabled: True + enhancement: 3 # optional, enhance the image before trying to recognize characters + ffmpeg: ... # add your streams + detect: + enabled: False # disable Frigate's standard object detection pipeline + fps: 5 # increase if necessary, though high values may slow down Frigate's enrichments pipeline and use considerable CPU + width: 1920 + height: 1080 + objects: + track: [] # required when not using a Frigate+ model for dedicated LPR mode + motion: + threshold: 30 + contour_area: 60 # use an increased value here to tune out small motion changes + improve_contrast: false + mask: 0.704,0.007,0.709,0.052,0.989,0.055,0.993,0.001 # ensure your camera's timestamp is masked + record: + enabled: True # disable recording if you only want snapshots + review: + detections: + enabled: True + retain: + default: 7 +``` + + + + +With this setup: + +- The standard object detection pipeline is bypassed. Any detected license plates on dedicated LPR cameras are treated similarly to manual events in Frigate. You must **not** specify `license_plate` as an object to track. +- The license plate detector runs on the full frame whenever motion is detected and processes frames according to your detect `fps` setting. +- Review items will always be classified as a `detection`. +- Snapshots will always be saved. +- Zones and object masks are **not** used. +- The `frigate/events` MQTT topic will **not** publish tracked object updates with the license plate bounding box and score, though `frigate/reviews` will publish if recordings are enabled. If a plate is recognized as a [known](#matching) plate, publishing will occur with an updated `sub_label` field. If characters are recognized, publishing will occur with an updated `recognized_license_plate` field. +- License plate snapshots are saved at the highest-scoring moment and appear in Explore. +- Debug view will not show `license_plate` bounding boxes. + +### Summary + +| Feature | Native `license_plate` detecting Model (like Frigate+) | Secondary Pipeline (without native model or Frigate+) | +| ----------------------- | ------------------------------------------------------ | --------------------------------------------------------------- | +| License Plate Detection | Uses `license_plate` as a tracked object | Runs a dedicated LPR pipeline | +| FPS Setting | 5 (increase for fast-moving cars) | 5 (increase for fast-moving cars, but it may use much more CPU) | +| Object Detection | Standard Frigate+ detection applies | Bypasses standard object detection | +| Debug View | May show `license_plate` bounding boxes | May **not** show `license_plate` bounding boxes | +| MQTT `frigate/events` | Publishes tracked object updates | Publishes limited updates | +| Explore | Recognized plates available in More Filters | Recognized plates available in More Filters | + +By selecting the appropriate configuration, users can optimize their dedicated LPR cameras based on whether they are using a Frigate+ model or the secondary LPR pipeline. + +### Best practices for using Dedicated LPR camera mode + +- Tune your motion detection and increase the `contour_area` until you see only larger motion boxes being created as cars pass through the frame (likely somewhere between 50-90 for a 1920x1080 detect stream). Increasing the `contour_area` filters out small areas of motion and will prevent excessive resource use from looking for license plates in frames that don't even have a car passing through it. +- Disable the `improve_contrast` motion setting, especially if you are running LPR at night and the frame is mostly dark. This will prevent small pixel changes and smaller areas of motion from triggering license plate detection. +- Ensure your camera's timestamp is covered with a motion mask so that it's not incorrectly detected as a license plate. +- For non-Frigate+ users, you may need to change your camera settings for a clearer image or decrease your global `recognition_threshold` config if your plates are not being accurately recognized at night. +- The secondary pipeline mode runs a local AI model on your CPU or GPU (depending on how `device` is configured) to detect plates. Increasing detect `fps` will increase resource usage proportionally. + +## FAQ + +### Detection and Recognition + + + +Ensure that: + +- Your camera has a clear, human-readable, well-lit view of the plate. If you can't read the plate's characters, Frigate certainly won't be able to, even if the model is recognizing a `license_plate`. This may require changing video size, quality, or frame rate settings on your camera, depending on your scene and how fast the vehicles are traveling. +- The plate is large enough in the image (try adjusting `min_area`) or increasing the resolution of your camera's stream. +- Your `enhancement` level (if you've changed it from the default of `0`) is not too high. Too much enhancement will run too much denoising and cause the plate characters to become blurry and unreadable. + +If you are using a Frigate+ model or a custom model that detects license plates, ensure that `license_plate` is added to your list of objects to track. +If you are using the free model that ships with Frigate, you should _not_ add `license_plate` to the list of objects to track. + +Recognized plates will show as object labels in the debug view and will appear in the "Recognized License Plates" select box in the More Filters popout in Explore. + +If you are still having issues detecting plates, start with a basic configuration and see the debugging tips below. + + + +Can I run LPR without detecting car or motorcycle objects?}> + +In normal LPR mode, Frigate requires a `car` or `motorcycle` to be detected first before recognizing a license plate. If you have a dedicated LPR camera, you can change the camera `type` to `"lpr"` to use the Dedicated LPR Camera algorithm. This comes with important caveats, though. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section above. + + + + + +- Use high-quality cameras with good resolution. +- Adjust `detection_threshold` and `recognition_threshold` values. +- Define a `format` regex to filter out invalid detections. + + + + + +Yes, but performance depends on camera quality, lighting, and infrared capabilities. Make sure your camera can capture clear images of plates at night. + + + + + +LPR, like other Frigate enrichments, runs at the camera level rather than the zone level. While you can't restrict LPR to specific zones directly, you can control when recognition runs by setting a `min_area` value to filter out smaller detections. + + + + + +Use `match_distance` to allow small character mismatches. Alternatively, define multiple variations in `known_plates`. + + + +### Performance and Troubleshooting + + + +Start with ["Why isn't my license plate being detected and recognized?"](#why-isnt-my-license-plate-being-detected-and-recognized). If you are still having issues, work through these steps. + +1. Start with a simplified LPR config. + - Remove or comment out everything in your LPR config, including `min_area`, `min_plate_length`, `format`, `known_plates`, or `enhancement` values so that the only values left are `enabled` and `debug_save_plates`. This will run LPR with Frigate's default values. + + + + +Navigate to . + +- Set **Enable LPR** to on +- Set **Device** to `CPU` +- Set **Save debug plates** to on + + + + +```yaml +lpr: + enabled: true + device: CPU + debug_save_plates: true +``` + + + + +2. Enable debug logs to see exactly what Frigate is doing. + - Enable debug logs for LPR by adding `frigate.data_processing.common.license_plate: debug` to your `logger` configuration. These logs are _very_ verbose, so only keep this enabled when necessary. Restart Frigate after this change. + + ```yaml + logger: + default: info + logs: + # highlight-next-line + frigate.data_processing.common.license_plate: debug + ``` + +3. Ensure your plates are being _detected_. + + If you are using a Frigate+ or `license_plate` detecting model: + - Watch the [Debug view](/usage/live#the-single-camera-view) to ensure that `license_plate` is being detected. + - View MQTT messages for `frigate/events` to verify detected plates. + - You may need to adjust your `min_score` and/or `threshold` for the `license_plate` object if your plates are not being detected. + + If you are **not** using a Frigate+ or `license_plate` detecting model: + - Watch the debug logs for messages from the YOLOv9 plate detector. + - You may need to adjust your `detection_threshold` if your plates are not being detected. + +4. Ensure the characters on detected plates are being _recognized_. + - Check the **Plate recognition** inference time in Enrichment metrics (). High inference times (> 100ms) could lead to poor recognition results, especially for dedicated LPR cameras where the plate crosses the frame quickly. + - Enable `debug_save_plates` to save images of detected text on plates to the clips directory (`/media/frigate/clips/lpr`). Ensure these images are readable and the text is clear. + - Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` or `motorcycle` label will change to the recognized plate when LPR is enabled and working. + - Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration). + + + + + +LPR's performance impact depends on your hardware. Ensure you have at least 4GB RAM and a capable CPU or GPU for optimal results. If you are running the Dedicated LPR Camera mode, resource usage will be higher compared to users who run a model that natively detects license plates. Tune your motion detection settings for your dedicated LPR camera so that the license plate detection model runs only when necessary. + + + +I am seeing a YOLOv9 plate detection metric in Enrichment Metrics, but I have a Frigate+ or custom model that detects license_plate. Why is the YOLOv9 model running?}> + +The YOLOv9 license plate detector model will run (and the metric will appear) if you've enabled LPR but haven't defined `license_plate` as an object to track, either at the global or camera level. + +If you are detecting `car` or `motorcycle` on cameras where you don't want to run LPR, make sure you disable LPR it at the camera level. And if you do want to run LPR on those cameras, make sure you define `license_plate` as an object to track. + + + + + +This could happen if cars or motorcycles travel close to your camera's timestamp or overlay text. You could either move the text through your camera's firmware, or apply a mask to it in Frigate. + +If you are using a model that natively detects `license_plate`, add an _object mask_ of type `license_plate` and a _motion mask_ over your text. + +If you are not using a model that natively detects `license_plate` or you are using dedicated LPR camera mode, only a _motion mask_ over your text is required. + + + + + +This usually happens when your GPU is unable to compile or use one of the LPR models. Set your `device` to `CPU` and try again. GPU acceleration only provides a slight performance increase, and the models are lightweight enough to run without issue on most CPUs. + + diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md new file mode 100644 index 0000000..a47c928 --- /dev/null +++ b/docs/docs/configuration/live.md @@ -0,0 +1,440 @@ +--- +id: live +title: Live View +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Frigate intelligently displays your camera streams on the Live view dashboard. By default, Frigate employs "smart streaming" where camera images update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any motion or active objects are detected, cameras seamlessly switch to a live stream. + +### Live View technologies + +Frigate intelligently uses three different streaming technologies to display your camera streams on the dashboard and the single camera view, switching between available modes based on network bandwidth, player errors, or required features like two-way talk. The highest quality and fluency of the Live view requires the bundled `go2rtc` to be [configured](/configuration/go2rtc). + +The jsmpeg live view will use more browser and client GPU resources. Using go2rtc is highly recommended and will provide a superior experience. + +| Source | Frame Rate | Resolution | Audio | Requires go2rtc | Notes | +| ------ | ------------------------------------- | ---------- | ---------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| jsmpeg | same as `detect -> fps`, capped at 10 | 720p | no | no | Resolution is configurable, but go2rtc is recommended if you want higher resolutions and better frame rates. jsmpeg is Frigate's default without go2rtc configured. | +| mse | native | native | yes (depends on audio codec) | yes | iPhone requires iOS 17.1+, Firefox is h.264 only. This is Frigate's default when go2rtc is configured. | +| webrtc | native | native | yes (depends on audio codec) | yes | Requires extra configuration. Frigate attempts to use WebRTC when MSE fails or when using a camera's two-way talk feature. | + +:::info + +WebRTC may use an external STUN server for NAT traversal. MSE and HLS streaming do not require any internet access. See [Network Requirements](/frigate/network_requirements#webrtc-stun) for details. + +::: + +### Camera Settings Recommendations + +If you are using go2rtc, you should adjust the following settings in your camera's firmware for the best experience with Live view: + +- Video codec: **H.264** - provides the most compatible video codec with all Live view technologies and browsers. Avoid any kind of "smart codec" or "+" codec like _H.264+_ or _H.265+_. as these non-standard codecs remove keyframes (see below). +- Audio codec: **AAC** - provides the most compatible audio codec with all Live view technologies and browsers that support audio. +- I-frame interval (sometimes called the keyframe interval, the interframe space, or the GOP length): match your camera's frame rate, or choose "1x" (for interframe space on Reolink cameras). For example, if your stream outputs 20fps, your i-frame interval should be 20 (or 1x on Reolink). Values higher than the frame rate will cause the stream to take longer to begin playback. See [this page](https://gardinal.net/understanding-the-keyframe-interval/) for more on keyframes. For many users this may not be an issue, but it should be noted that a 1x i-frame interval will cause more storage utilization if you are using the stream for the `record` role as well. + +The default video and audio codec on your camera may not always be compatible with your browser, which is why setting them to H.264 and AAC is recommended. See the [go2rtc docs](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#codecs-madness) for codec support information. + +### Audio Support + +MSE Requires PCMA/PCMU or AAC audio, WebRTC requires PCMA/PCMU or opus audio. If you want to support both MSE and WebRTC then your restream config needs to make sure both are enabled. + +```yaml +go2rtc: + streams: + rtsp_cam: # <- for RTSP streams + - rtsp://192.168.1.5:554/live0 # <- stream which supports video & aac audio + - "ffmpeg:rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus) + http_cam: # <- for http streams + - http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=user&password=password # <- stream which supports video & aac audio + - "ffmpeg:http_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus) +``` + +If your camera does not support AAC audio or are having problems with Live view, try transcoding to AAC audio directly: + +```yaml +go2rtc: + streams: + rtsp_cam: # <- for RTSP streams + - "ffmpeg:rtsp://192.168.1.5:554/live0#video=copy#audio=aac" # <- copies video stream and transcodes to aac audio + - "ffmpeg:rtsp_cam#audio=opus" # <- provides support for WebRTC +``` + +If your camera does not have audio and you are having problems with Live view, you should have go2rtc send video only: + +```yaml +go2rtc: + streams: + no_audio_camera: + - ffmpeg:rtsp://192.168.1.5:554/live0#video=copy +``` + +### Setting Streams For Live UI + +You can configure Frigate to allow manual selection of the stream you want to view in the Live UI. For example, you may want to view your camera's substream on mobile devices, but the full resolution stream on desktop devices. Setting the streams list will populate a dropdown in the UI's Live view that allows you to choose between the streams. This stream setting is _per device_ and is saved in your browser's local storage. + +Additionally, when creating and editing camera groups in the UI, you can choose the stream you want to use for your camera group's Live dashboard. + +:::note + +Frigate's default dashboard ("All Cameras") will always use the first entry you've defined in streams when playing live streams from your cameras. + +::: + +Configure a "friendly name" for your stream followed by the go2rtc stream name. Using Frigate's internal version of go2rtc is required to use this feature. You cannot specify paths in the streams configuration, only go2rtc stream names. + + + + +1. Navigate to and select your camera. +2. Under **Live stream names**, click **Add stream** to add a new entry. +3. In the **Stream name** field, enter a friendly name that will appear in the Live UI's stream dropdown (e.g., `Main Stream`). +4. In the **go2rtc stream** field, open the dropdown and select the go2rtc stream this name should map to (e.g., `test_cam`). The dropdown lists every stream configured under `go2rtc.streams`. If the go2rtc stream hasn't been created yet, you can type the name and choose **Use "..."** to save a custom value. +5. Repeat for each additional stream you want to expose (e.g., `Sub Stream` → `test_cam_sub`). +6. Use the trash icon on a row to remove a stream, then **Save** the section. + +:::tip + +Configure your go2rtc streams first under so the dropdown is populated with valid options. + +::: + + + + +```yaml {3,6,8,25-29} +go2rtc: + streams: + test_cam: + - rtsp://192.168.1.5:554/live_main # <- stream which supports video & aac audio. + - "ffmpeg:test_cam#audio=opus" # <- copy of the stream which transcodes audio to opus for webrtc + test_cam_sub: + - rtsp://192.168.1.5:554/live_sub # <- stream which supports video & aac audio. + test_cam_another_sub: + - rtsp://192.168.1.5:554/live_alt # <- stream which supports video & aac audio. + +cameras: + test_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/test_cam # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/test_cam_sub # <--- the name here must match the name of the camera_sub in restream + input_args: preset-rtsp-restream + roles: + - detect + live: + streams: # <--- Multiple streams for Frigate 0.16 and later + Main Stream: test_cam # <--- Specify a "friendly name" followed by the go2rtc stream name + Sub Stream: test_cam_sub + Special Stream: test_cam_another_sub +``` + + + + +### WebRTC extra configuration: + +WebRTC works by creating a TCP or UDP connection on port `8555`. However, it requires additional configuration: + +- For external access, over the internet, setup your router to forward port `8555` to port `8555` on the Frigate device, for both TCP and UDP. +- For internal/local access, unless you are running through the HA App, you will also need to set the WebRTC candidates list in the go2rtc config. For example, if `192.168.1.10` is the local IP of the device running Frigate: + + ```yaml title="config.yml" {4-7} + go2rtc: + streams: + test_cam: ... + webrtc: + candidates: + - 192.168.1.10:8555 + - stun:8555 + ``` + +- For access through Tailscale, the Frigate system's Tailscale IP must be added as a WebRTC candidate. Tailscale IPs all start with `100.`, and are reserved within the `100.64.0.0/10` CIDR block. + +- Note that some browsers may not support H.265 (HEVC). You can check your browser's current version for H.265 compatibility [here](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#codecs-madness). + +:::tip + +This extra configuration may not be required if Frigate has been installed as a Home Assistant App, as Frigate uses the Supervisor's API to generate a WebRTC candidate. + +However, it is recommended if issues occur to define the candidates manually. You should do this if the Frigate App fails to generate a valid candidate. If an error occurs you will see some warnings like the below in the App logs page during the initialization: + +```log +[WARN] Failed to get IP address from supervisor +[WARN] Failed to get WebRTC port from supervisor +``` + +::: + +:::note + +If you are having difficulties getting WebRTC to work and you are running Frigate with docker, you may want to try changing the container network mode: + +- `network: host`, in this mode you don't need to forward any ports. The services inside of the Frigate container will have full access to the network interfaces of your host machine as if they were running natively and not in a container. Any port conflicts will need to be resolved. This network mode is recommended by go2rtc, but we recommend you only use it if necessary. +- `network: bridge` is the default network driver, a bridge network is a Link Layer device which forwards traffic between network segments. You need to forward any ports that you want to be accessible from the host IP. + +If not running in host mode, port 8555 will need to be mapped for the container: + +docker-compose.yml + +```yaml {4-6} +services: + frigate: + ... + ports: + - "8555:8555/tcp" # WebRTC over tcp + - "8555:8555/udp" # WebRTC over udp +``` + +::: + +See [go2rtc WebRTC docs](https://github.com/AlexxIT/go2rtc/tree/v1.8.3#module-webrtc) for more information about this. + +### Two way talk + +For devices that support two way talk, Frigate can be configured to use the feature from the camera's Live view in the Web UI. You should: + +- Set up go2rtc with [WebRTC](#webrtc-extra-configuration). +- Ensure you access Frigate via https (may require [opening port 8971](/frigate/installation/#ports)). +- For the Home Assistant Frigate card, [follow the docs](http://card.camera/#/usage/2-way-audio) for the correct source. + +To use the Reolink Doorbell with two way talk, you should use the [recommended Reolink configuration](/configuration/camera_specific#reolink-cameras) + +As a starting point to check compatibility for your camera, view the list of cameras supported for two-way talk on the [go2rtc repository](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#two-way-audio). For cameras in the category `ONVIF Profile T`, you can use the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/)'s FeatureList to check for the presence of `AudioOutput`. A camera that supports `ONVIF Profile T` _usually_ supports this, but due to inconsistent support, a camera that explicitly lists this feature may still not work. If no entry for your camera exists on the database, it is recommended not to buy it or to consult with the manufacturer's support on the feature availability. + +To prevent go2rtc from blocking other applications from accessing your camera's two-way audio, you must configure your stream with `#backchannel=0`. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation. + +### Streaming options on camera group dashboards + +Frigate provides a dialog in the Camera Group Edit pane with several options for streaming on a camera group's dashboard. These settings are _per device_ and are saved in your device's local storage. + +- Stream selection using the streams configuration option (see _Setting Streams For Live UI_ above) +- Streaming type: + - _No streaming_: Camera images will only update once per minute and no live streaming will occur. + - _Smart Streaming_ (default, recommended setting): Smart streaming will update your camera image once per minute when no detectable activity is occurring to conserve bandwidth and resources, since a static picture is the same as a streaming image with no motion or objects. When motion or objects are detected, the image seamlessly switches to a live stream. + - _Continuous Streaming_: Camera image will always be a live stream when visible on the dashboard, even if no activity is being detected. Continuous streaming may cause high bandwidth usage and performance issues. **Use with caution.** +- _Compatibility mode_: Enable this option only if your camera's live stream is displaying color artifacts and has a diagonal line on the right side of the image. Before enabling this, try setting your camera's `detect` width and height to a standard aspect ratio (for example: 640x352 becomes 640x360, and 800x443 becomes 800x450, 2688x1520 becomes 2688x1512, etc). Depending on your browser and device, more than a few cameras in compatibility mode may not be supported, so only use this option if changing your config fails to resolve the color artifacts and diagonal line. + +:::note + +The default dashboard ("All Cameras") will always use: + +- Smart Streaming, unless you've disabled the global Automatic Live View in Settings. +- The first entry set in your `streams` configuration, if defined. + +Use a camera group if you want to change any of these settings from the defaults. + +::: + +### jsmpeg Stream Quality + +The jsmpeg live view resolution and encoding quality can be adjusted globally or per camera. These settings only affect the jsmpeg player and do not apply when go2rtc is used for live view. + + + + +Navigate to for global defaults, or and select a camera for per-camera overrides. + +| Field | Description | +| ---------------- | --------------------------------------------------------------------------------------------------- | +| **Live height** | Height in pixels for the jsmpeg live stream; must be less than or equal to the detect stream height | +| **Live quality** | Encoding quality for the jsmpeg stream (1 = highest, 31 = lowest) | + + + + +```yaml +# Global defaults +live: + height: 720 + quality: 8 + +# Per-camera override +cameras: + front_door: + live: + height: 480 + quality: 4 +``` + + + + +### Camera state + +Each camera has three possible states, surfaced as a status selector in **Settings → Global configuration → Camera management**: + +- **On**: streams are processed normally. Object detection, recording, and Live view are active. +- **Off**: Frigate's ffmpeg processes are paused. Recording stops, object detection is paused, and the Live dashboard displays a blank image with a "Camera is off" message. The camera is still visible in the Live dashboard and its past review items, tracked objects, and historical footage remain accessible via the UI. The Off state persists across Frigate restarts via a `.runtime_state.json` file alongside `config.yml` (see [Runtime toggle persistence](#runtime-toggle-persistence)). +- **Disabled**: the change is saved to your configuration file (`enabled: False`). The camera stops immediately, Frigate stops ffmpeg processes, and all live and historical UI elements for the camera are no longer visible but remains retained on disk. The camera is still listed in **Settings → Global configuration → Camera management** so it can be re-enabled. **A restart of Frigate is required to bring a disabled camera back to On.** + +#### Turning a camera on or off + +Turning a camera off is temporary and does not require a restart. The available controls are: + +- The power button in the single-camera Live view header +- The right-click context menu on a camera tile on the Live dashboard +- The Camera management settings pane (status set to **Off**) +- The mobile settings drawer on the single-camera Live view (admin users only) +- The [MQTT topic](/integrations/mqtt#frigatecamera_nameenabledset) `frigate//enabled/set` with payload `ON` or `OFF` +- The Home Assistant integration via the [`camera.turn_on` / `camera.turn_off` actions](/integrations/home-assistant#camera-api) + +#### Disabling a camera + +Disabling a camera saves the change to your configuration file. Navigate to **Settings → Global configuration → Camera management** and set the camera's status to **Disabled**. Runtime processing stops immediately; the change persists across restarts. + +Re-enabling a disabled camera requires a restart of Frigate so that the ffmpeg processes and other camera-scoped resources can be initialized. The UI will prompt you to restart when you switch a disabled camera back to On. + +#### Restream behavior + +For both Off and Disabled cameras, go2rtc remains active but does not use system resources for decoding or processing unless there are active external consumers (such as the Advanced Camera Card in Home Assistant using a go2rtc source). + +#### Choosing Off versus Disabled + +If you want a camera's historical data (review items, tracked objects, footage) to stay accessible in the UI while you stop processing, set the camera to **Off**. If you want the camera fully removed from the Live dashboard, review filters, and other UI surfaces, set it to **Disabled**. The Disabled state still keeps the camera in Camera management so it can be re-enabled later; if you want to remove all traces of a camera including its configuration, delete it via Camera management instead. + +#### Runtime toggle persistence + +The Live view toggles for **camera on/off**, **detect**, **recordings**, **snapshots**, and **audio detection** (along with the equivalent MQTT `/set` topics) write the new state to `.runtime_state.json` next to your `config.yml`. The file is replayed on Frigate startup so your last-known toggle states survive a restart. Two interactions worth knowing: + +- **Settings UI saves win.** When you save a field through **Settings → Global configuration**, the matching entry is cleared from `.runtime_state.json` so the new value in your config file is the durable source. +- **Switching profiles clears all runtime overrides.** Activating or deactivating a [profile](/configuration/profiles) is treated as a deliberate state change, so the file is wiped to avoid stale overrides replaying on top of the new profile. + +If you hand-edit `config.yml` while runtime overrides exist, the overrides will still replay on restart. Delete `.runtime_state.json` to reset to the YAML-defined defaults. + +### Live player error messages + +When your browser runs into problems playing back your camera streams, it will log short error messages to the browser console. They indicate playback, codec, or network issues on the client/browser side, not something server side with Frigate itself. Below are the common messages you may see and simple actions you can take to try to resolve them. + +- **startup** + - What it means: The player failed to initialize or connect to the live stream (network or startup error). + - What to try: Reload the Live view or click _Reset_. Verify `go2rtc` is running and the camera stream is reachable. Try switching to a different stream from the Live UI dropdown (if available) or use a different browser. + + - Possible console messages from the player code: + - `Error opening MediaSource.` + - `Browser reported a network error.` + - `Max error count ${errorCount} exceeded.` (the numeric value will vary) + +- **mse-decode** + - What it means: The browser reported a decoding error while trying to play the stream, which usually is a result of a codec incompatibility or corrupted frames. + - What to try: Check the browser console for the supported and negotiated codecs. Ensure your camera/restream is using H.264 video and AAC audio (these are the most compatible). If your camera uses a non-standard audio codec, configure `go2rtc` to transcode the stream to AAC. Try another browser (some browsers have stricter MSE/codec support) and, for iPhone, ensure you're on iOS 17.1 or newer. + + - Possible console messages from the player code: + - `Safari cannot open MediaSource.` + - `Safari reported InvalidStateError.` + - `Safari reported decoding errors.` + +- **stalled** + - What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving). + - What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval: shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings. + + - Possible console messages from the player code: + - `Buffer time (10 seconds) exceeded, browser may not be playing media correctly.` + - `Media playback has stalled after seconds due to insufficient buffering or a network interruption.` (the seconds value will vary) + +## Live view FAQ + +1. **Why don't I have audio in my Live view?** + + You must use go2rtc to hear audio in your live streams. If you have go2rtc already configured, you need to ensure your camera is sending PCMA/PCMU or AAC audio. If you can't change your camera's audio codec, you need to [transcode the audio](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg) using go2rtc. + + Note that the low bandwidth mode player is a video-only stream. You should not expect to hear audio when in low bandwidth mode, even if you've set up go2rtc. + +2. **Frigate shows that my live stream is in "low bandwidth mode". What does this mean?** + + Frigate intelligently selects the live streaming technology based on a number of factors (user-selected modes like two-way talk, camera settings, browser capabilities, available bandwidth) and prioritizes showing an actual up-to-date live view of your camera's stream as quickly as possible. + + When you have go2rtc configured, Live view initially attempts to load and play back your stream with a clearer, fluent stream technology (MSE). An initial timeout, a low bandwidth condition that would cause buffering of the stream, or decoding errors in the stream will cause Frigate to switch to the stream defined by the `detect` role, using the jsmpeg format. This is what the UI labels as "low bandwidth mode". On Live dashboards, the mode will automatically reset when smart streaming is configured and activity stops. Continuous streaming mode does not have an automatic reset mechanism, but you can use the _Reset_ option to force a reload of your stream. + + If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again. + + Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include: + - Network issues (e.g., MSE or WebRTC network connection problems). + - Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers). + - Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg. + - Browser compatibility problems (e.g., iOS Safari limitations with MSE). + + To view browser console logs: + 1. Open the Frigate Live View in your browser. + 2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab). + 3. Reproduce the error (e.g., load a problematic stream or simulate network issues). + 4. Look for messages prefixed with the camera name. + + These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors: + - Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera-settings-recommendations)). + - Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS). + - Test with a different stream via the UI dropdown (if `live -> streams` is configured). + - For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see [WebRTC Extra Configuration](#webrtc-extra-configuration)). + - If your cameras are streaming at a high resolution, your browser may be struggling to load all of the streams before the buffering timeout occurs. Frigate prioritizes showing a true live view as quickly as possible. If the fallback occurs often, change your live view settings to use a lower bandwidth substream. + +3. **It doesn't seem like my cameras are streaming on the Live dashboard. Why?** + + On the default Live dashboard ("All Cameras"), your camera images will update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity is detected, cameras seamlessly switch to a full-resolution live stream. If you want to customize this behavior, use a camera group. + +4. **I see a strange diagonal line on my live view, but my recordings look fine. How can I fix it?** + + This is caused by incorrect dimensions set in your detect width or height (or incorrectly auto-detected), causing the jsmpeg player's rendering engine to display a slightly distorted image. You should enlarge the width and height of your `detect` resolution up to a standard aspect ratio (example: 640x352 becomes 640x360, and 800x443 becomes 800x450, 2688x1520 becomes 2688x1512, etc). If changing the resolution to match a standard (4:3, 16:9, or 32:9, etc) aspect ratio does not solve the issue, you can enable "compatibility mode" in your camera group dashboard's stream settings. Depending on your browser and device, more than a few cameras in compatibility mode may not be supported, so only use this option if changing your `detect` width and height fails to resolve the color artifacts and diagonal line. + +5. **How does "smart streaming" work?** + + Because a static image of a scene looks exactly the same as a live stream with no motion or activity, smart streaming updates your camera images once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity (motion or object/audio detection) occurs, cameras seamlessly switch to a live stream. + + This static image is pulled from the stream defined in your config with the `detect` role. When activity is detected, images from the `detect` stream immediately begin updating at ~5 frames per second so you can see the activity until the live player is loaded and begins playing. This usually only takes a second or two. If the live player times out, buffers, or has streaming errors, the jsmpeg player is loaded and plays a video-only stream from the `detect` role. When activity ends, the players are destroyed and a static image is displayed until activity is detected again, and the process repeats. + + Smart streaming depends on having your camera's motion `threshold` and `contour_area` config values dialed in. Use the Motion Tuner in Settings in the UI to tune these values in real-time. + + This is Frigate's default and recommended setting because it results in a significant bandwidth savings, especially for high resolution cameras. + +6. **I have unmuted some cameras on my dashboard, but I do not hear sound. Why?** + + If your camera is streaming (as indicated by a red dot in the upper right, or if it has been set to continuous streaming mode), your browser may be blocking audio until you interact with the page. This is an intentional browser limitation. See [this article](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability). Many browsers have a whitelist feature to change this behavior. + +7. **My camera streams have lots of visual artifacts / distortion.** + + Some cameras don't include the hardware to support multiple connections to the high resolution stream, and this can cause unexpected behavior. In this case it is recommended to [restream](./restream.md) the high resolution stream so that it can be used for live view and recordings. + +8. **Why does my camera stream switch aspect ratios on the Live dashboard?** + + Your camera may change aspect ratios on the dashboard because Frigate uses different streams for different purposes. With go2rtc and Smart Streaming, Frigate shows a static image from the `detect` stream when no activity is present, and switches to the live stream when motion is detected. The camera image will change size if your streams use different aspect ratios. + + To prevent this, make the `detect` stream match the go2rtc live stream's aspect ratio (resolution does not need to match, just the aspect ratio). You can either adjust the camera's output resolution or set the `width` and `height` values in your config's `detect` section to a resolution with an aspect ratio that matches. + + Example: Resolutions from two streams + - Mismatched (may cause aspect ratio switching on the dashboard): + - Live/go2rtc stream: 1920x1080 (16:9) + - Detect stream: 640x352 (~1.82:1, not 16:9) + + - Matched (prevents switching): + - Live/go2rtc stream: 1920x1080 (16:9) + - Detect stream: 640x360 (16:9) + + You can update the detect settings in your camera config to match the aspect ratio of your go2rtc live stream. For example: + + ```yaml + cameras: + front_door: + detect: + width: 640 + height: 360 # set this to 360 instead of 352 + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/front_door # main stream 1920x1080 + roles: + - record + - path: rtsp://127.0.0.1:8554/front_door_sub # sub stream 640x352 + roles: + - detect + ``` + + The same applies to your `record` stream: if its aspect ratio differs from your `detect` stream, your recordings will appear in a different shape than the live view. For consistent framing across live view and recordings, use the same aspect ratio for all of a camera's streams (the resolution can still differ). + +9. **Why does Frigate prefer MSE over WebRTC for live view?** + + Frigate prefers MSE because it delivers a better out-of-the-box experience than WebRTC on nearly every axis that matters for a security camera system. MSE is an open standard optimized and supported by all modern browsers, works without any extra configuration (WebRTC requires port forwarding and candidate setup, and lacks H.265 support in some browsers), and requires no internet access for NAT traversal. More importantly, MSE runs over TCP, so every frame arrives and is decoded in order, so nothing is ever silently skipped. WebRTC optimizes for latency over UDP by discarding late or incomplete frames, which works against you on cellular or spotty Wi-Fi: you can end up with frozen video, visual corruption, or gaps in the feed without ever knowing you missed something. Frigate's enhanced MSE player has adaptive speed playback and has been tuned for latency and connection robustness that meets or exceeds WebRTC, so you get near-real-time playback with a guarantee that when the video plays, every frame is actually there - which, for an NVR whose whole purpose is letting you see what happened, matters more than shaving fractions of a second off a latency number. That's why Frigate defaults to MSE and reserves WebRTC for cases that require it, like two-way talk. diff --git a/docs/docs/configuration/masks.md b/docs/docs/configuration/masks.md new file mode 100644 index 0000000..50c9fff --- /dev/null +++ b/docs/docs/configuration/masks.md @@ -0,0 +1,148 @@ +--- +id: masks +title: Masks +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Frigate has two kinds of masks: motion masks and object filter masks. Both are narrow tools for fine-tuning, **not for hiding an area from Frigate**. Masks should be used sparingly; in most cases where users reach for one, a [zone](zones.md) with [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones) is the right tool instead. See [Which tool do I need?](#which-tool-do-i-need) and [Common mistakes](#common-mistakes) below if you're new to Frigate's mask behavior. + +## Motion masks + +Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the [Debug view](/usage/live#the-single-camera-view) with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. _Over-masking will make it more difficult for objects to be tracked._ + +See [further clarification](#further-clarification) below on why you may not want to use a motion mask. + +## Object filter masks + +Object filter masks are used to filter out false positives for a given object type based on location. These should be used to filter any areas where it is not possible for an object of that type to be. The bottom center of the detected object's bounding box is evaluated against the mask. If it is in a masked area, it is assumed to be a false positive. For example, you may want to mask out rooftops, walls, the sky, treetops for people. For cars, masking locations other than the street or your driveway will tell Frigate that anything in your yard is a false positive. + +Object filter masks can be used to filter out stubborn false positives in fixed locations. For example, the base of this tree may be frequently detected as a person. The following image shows an example of an object filter mask (shaded red area) over the location where the bottom center is typically located to filter out person detections in a precise location. + +![object mask](/img/bottom-center-mask.jpg) + +## Which tool do I need? + +| What you're trying to do | Recommended tool | How it works | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Only get alerts/detections for activity in the areas you care about, ignoring activity elsewhere (e.g., alert when someone enters your yard, but not when they walk past on the sidewalk) | A [zone](zones.md) combined with [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones) | Frigate keeps detecting and tracking activity everywhere in the frame, but a review item is only created once the bottom-center of an object's bounding box enters a required zone. | +| Stop a stubborn false positive at a specific fixed spot (e.g., a tree base that keeps being detected as a person) | An **object filter mask** for that object type | Any detection of that object type whose bounding-box bottom-center lands inside the mask is treated as a false positive and discarded. | +| Ignore motion in an area that obviously isn't an object of interest (e.g., the camera timestamp, sky, flags, treetops swaying) | A **motion mask** | Motion inside the mask is ignored when deciding whether to run object detection. Objects can still be detected in a motion masked area if motion elsewhere in the frame triggers detection. | +| Stop tracking an object type altogether on this camera (e.g., you never care about cats) | Remove the object from the camera's [`objects.track`](objects.md) list | Frigate skips this object type entirely on this camera, regardless of where it appears. | + +## Using the mask creator + + + + +Navigate to and select a camera. Use the mask editor to draw motion masks and object filter masks directly on the camera feed. Each mask can be given a friendly name and toggled on or off. + + + + +Your config file will be updated with the relative coordinates of the mask/zone: + +```yaml +motion: + mask: + # Motion mask name (required) + mask1: + # Optional: A friendly name for the mask + friendly_name: "Timestamp area" + # Optional: Whether this mask is active (default: true) + enabled: true + # Required: Coordinates polygon for the mask + coordinates: "0.000,0.427,0.002,0.000,0.999,0.000,0.999,0.781,0.885,0.456,0.700,0.424,0.701,0.311,0.507,0.294,0.453,0.347,0.451,0.400" +``` + +Multiple motion masks can be listed in your config: + +```yaml +motion: + mask: + mask1: + friendly_name: "Timestamp area" + enabled: true + coordinates: "0.239,1.246,0.175,0.901,0.165,0.805,0.195,0.802" + mask2: + friendly_name: "Tree area" + enabled: true + coordinates: "0.000,0.427,0.002,0.000,0.999,0.000,0.999,0.781,0.885,0.456" +``` + +Object filter masks are configured under the object filters section for each object type: + +```yaml +objects: + filters: + person: + mask: + person_filter1: + friendly_name: "Roof area" + enabled: true + coordinates: "0.000,0.000,1.000,0.000,1.000,0.400,0.000,0.400" + car: + mask: + car_filter1: + friendly_name: "Sidewalk area" + enabled: true + coordinates: "0.000,0.700,1.000,0.700,1.000,1.000,0.000,1.000" +``` + + + + +## Enabling/Disabling Masks + +Both motion masks and object filter masks can be toggled on or off without removing them from the configuration. Disabled masks are completely ignored at runtime - they will not affect motion detection or object filtering. This is useful for temporarily disabling a mask during certain seasons or times of day without modifying the configuration. + +### Further Clarification + +This is a response to a [question posed on reddit](https://www.reddit.com/r/homeautomation/comments/ppxdve/replacing_my_doorbell_with_a_security_camera_a_6/hd876w4?utm_source=share&utm_medium=web2x&context=3): + +It is helpful to understand a bit about how Frigate uses motion detection and object detection together. + +First, Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection. + +Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the debug viewer. + +After the area with motion is identified, Frigate creates a "region" (the green boxes in the debug viewer) to run object detection on. The models are trained on square images, so these regions are always squares. It adds a margin around the motion area in hopes of capturing a cropped view of the object moving that fills most of the image passed to object detection, but doesn't cut anything off. It also takes into consideration the location of the bounding box from the previous frame if it is tracking an object. + +After object detection runs, if there are detected objects that seem to be cut off, Frigate reframes the region and runs object detection again on the same frame to get a better look. + +All of this happens for each area of motion and tracked object. + +> Are you simply saying that INITIAL triggering of any kind of detection will only happen in un-masked areas, but that once this triggering happens, the masks become irrelevant and object detection takes precedence? + +Essentially, yes. I wouldn't describe it as object detection taking precedence though. The motion masks just prevent those areas from being counted as motion. Those masks do not modify the regions passed to object detection in any way, so you can absolutely detect objects in areas masked for motion. + +> If so, this is completely expected and intuitive behavior for me. Because obviously if a "foot" starts motion detection the camera should be able to check if it's an entire person before it fully crosses into the zone. The docs imply this is the behavior, so I also don't understand why this would be detrimental to object detection on the whole. + +When just a foot is triggering motion, Frigate will zoom in and look only at the foot. If that even qualifies as a person, it will determine the object is being cut off and look again and again until it zooms back out enough to find the whole person. + +It is also detrimental to how Frigate tracks a moving object. Motion nearby the bounding box from the previous frame is used to intelligently determine where the region should be in the next frame. With too much masking, tracking is hampered and if an object walks from an unmasked area into a fully masked area, they essentially disappear and will be picked up as a "new" object if they leave the masked area. This is important because Frigate uses the history of scores while tracking an object to determine if it is a false positive or not. It takes a minimum of 3 frames for Frigate to determine is the object type it thinks it is, and the median score must be greater than the threshold. If a person meets this threshold while on the sidewalk before they walk into your stoop, you will get an alert the instant they step a single foot into a zone. + +> I thought the main point of this feature was to cut down on CPU use when motion is happening in unnecessary areas. + +It is, but the definition of "unnecessary" varies. I want to ignore areas of motion that I know are definitely not being triggered by objects of interest. Timestamps, trees, sky, rooftops. I don't want to ignore motion from objects that I want to track and know where they go. + +> For me, giving my masks ANY padding results in a lot of people detection I'm not interested in. I live in the city and catch a lot of the sidewalk on my camera. People walk by my front door all the time and the margin between the sidewalk and actually walking onto my stoop is very thin, so I basically have everything but the exact contours of my stoop masked out. This results in very tidy detections but this info keeps throwing me off. Am I just overthinking it? + +This is what `required_zones` are for. You should define a zone (remember this is evaluated based on the bottom center of the bounding box) and make it required to save snapshots and clips (previously events in 0.9.0 to 0.13.0 and review items in 0.14.0 and later). You can also use this in your conditions for a notification. + +> Maybe my specific situation just warrants this. I've just been having a hard time understanding the relevance of this information - it seems to be that it's exactly what would be expected when "masking out" an area of ANY image. + +That may be the case for you. Frigate will definitely work harder tracking people on the sidewalk to make sure it doesn't miss anyone who steps foot on your stoop. The trade off with the way you have it now is slower recognition of objects and potential misses. That may be acceptable based on your needs. Also, if your resolution is low enough on the detect stream, your regions may already be so big that they grab the entire object anyway. + +## Common mistakes + +**"I added a motion mask to ignore my driveway/sidewalk."** +A motion mask doesn't hide an area from Frigate. Objects can still be detected and tracked inside a masked area. The mask only stops motion _in that area_ from triggering object detection. If you want activity on the sidewalk to never produce a review item, define a [zone](zones.md) over the area you DO care about (your stoop, your driveway) and add it to [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones). Frigate will still see people on the sidewalk, but it won't create an alert until they cross into the zone. + +**"I added an object filter mask because I don't care about cars in my yard."** +Object filter masks are for stubborn false positives at fixed locations, not for filtering whole areas or whole object types. If you only want alerts when a car enters the driveway, use a [zone](zones.md) with [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones). If you don't care about a whole object type on this camera, remove it from [`objects.track`](objects.md). + +**"I masked everything except a thin strip on my stoop."** +Heavy masking hurts tracking. Frigate uses motion near a tracked object's previous bounding box to decide where to look in the next frame; with most of the frame masked, an object walking from an unmasked area into a masked one effectively disappears and gets picked up as a "new" object when it reappears. For example: someone walks down your sidewalk, stops under a tree (masked area) to tie their shoe, then continues. Frigate sees that as two separate people and can create two separate review items. Because Frigate needs several consecutive frames above the confidence threshold to commit to a detection, each re-appearance can also delay or miss alerts. Use [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones) for "only alert me about this spot" and leave the surrounding area unmasked so tracking stays intact. diff --git a/docs/docs/configuration/metrics.md b/docs/docs/configuration/metrics.md new file mode 100644 index 0000000..69db159 --- /dev/null +++ b/docs/docs/configuration/metrics.md @@ -0,0 +1,128 @@ +--- +id: metrics +title: Metrics +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +# Metrics + +Frigate exposes Prometheus metrics at the `/api/metrics` endpoint that can be used to monitor the performance and health of your Frigate instance. + +## Enabling Telemetry + +Prometheus metrics are exposed via the telemetry configuration. Enable or configure telemetry to control metric availability. + + + + +Navigate to to configure metrics and telemetry settings. + + + + +Metrics are available at `/api/metrics` by default. No additional Frigate configuration is required to expose them. + + + + +## Available Metrics + +### System Metrics + +- `frigate_cpu_usage_percent{pid="", name="", process="", type="", cmdline=""}` - Process CPU usage percentage +- `frigate_mem_usage_percent{pid="", name="", process="", type="", cmdline=""}` - Process memory usage percentage +- `frigate_gpu_usage_percent{gpu_name=""}` - GPU utilization percentage +- `frigate_gpu_mem_usage_percent{gpu_name=""}` - GPU memory usage percentage + +### Camera Metrics + +- `frigate_camera_fps{camera_name=""}` - Frames per second being consumed from your camera +- `frigate_detection_fps{camera_name=""}` - Number of times detection is run per second +- `frigate_process_fps{camera_name=""}` - Frames per second being processed +- `frigate_skipped_fps{camera_name=""}` - Frames per second skipped for processing +- `frigate_detection_enabled{camera_name=""}` - Detection enabled status for camera +- `frigate_audio_dBFS{camera_name=""}` - Audio dBFS for camera +- `frigate_audio_rms{camera_name=""}` - Audio RMS for camera + +### Detector Metrics + +- `frigate_detector_inference_speed_seconds{name=""}` - Time spent running object detection in seconds +- `frigate_detection_start{name=""}` - Detector start time (unix timestamp) + +### Storage Metrics + +- `frigate_storage_free_bytes{storage=""}` - Storage free bytes +- `frigate_storage_total_bytes{storage=""}` - Storage total bytes +- `frigate_storage_used_bytes{storage=""}` - Storage used bytes +- `frigate_storage_mount_type{mount_type="", storage=""}` - Storage mount type info + +These gauges report the operating system's figures for the whole filesystem (the same numbers as `df`), not Frigate's own recording footprint. For how this differs from the recordings usage shown in the UI, see [Understanding storage usage](/configuration/record#understanding-storage-usage). + +### Service Metrics + +- `frigate_service_uptime_seconds` - Uptime in seconds +- `frigate_service_last_updated_timestamp` - Stats recorded time (unix timestamp) +- `frigate_device_temperature{device=""}` - Device Temperature + +### Event Metrics + +- `frigate_camera_events{camera="", label=""}` - Count of camera events since exporter started + +## Configuring Prometheus + +To scrape metrics from Frigate, add the following to your Prometheus configuration: + +```yaml +scrape_configs: + - job_name: "frigate" + metrics_path: "/api/metrics" + static_configs: + - targets: ["frigate:5000"] + scrape_interval: 15s +``` + +## Example Queries + +Here are some example PromQL queries that might be useful: + +```promql +# Average CPU usage across all processes +avg(frigate_cpu_usage_percent) + +# Total GPU memory usage +sum(frigate_gpu_mem_usage_percent) + +# Detection FPS by camera +rate(frigate_detection_fps{camera_name="front_door"}[5m]) + +# Storage usage percentage +(frigate_storage_used_bytes / frigate_storage_total_bytes) * 100 + +# Event count by camera in last hour +increase(frigate_camera_events[1h]) +``` + +## Grafana Dashboard + +You can use these metrics to create Grafana dashboards to monitor your Frigate instance. Here's an example of metrics you might want to track: + +- CPU, Memory and GPU usage over time +- Camera FPS and detection rates +- Storage usage and trends +- Event counts by camera +- System temperatures + +A sample Grafana dashboard JSON will be provided in a future update. + +## Metric Types + +The metrics exposed by Frigate use the following Prometheus metric types: + +- **Counter**: Cumulative values that only increase (e.g., `frigate_camera_events`) +- **Gauge**: Values that can go up and down (e.g., `frigate_cpu_usage_percent`) +- **Info**: Key-value pairs for metadata (e.g., `frigate_storage_mount_type`) + +For more information about Prometheus metric types, see the [Prometheus documentation](https://prometheus.io/docs/concepts/metric_types/). diff --git a/docs/docs/configuration/motion_detection.md b/docs/docs/configuration/motion_detection.md new file mode 100644 index 0000000..a792895 --- /dev/null +++ b/docs/docs/configuration/motion_detection.md @@ -0,0 +1,203 @@ +--- +id: motion_detection +title: Motion Detection +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +# Tuning Motion Detection + +Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection. + +Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the [debug viewer](/usage/live#the-single-camera-view). + +## The Goal + +The default motion settings should work well for the majority of cameras, however there are cases where tuning motion detection can lead to better and more optimal results. Each camera has its own environment with different variables that affect motion, this means that the same motion settings will not fit all of your cameras. + +Before tuning motion it is important to understand the goal. In an optimal configuration, motion from people and cars would be detected, but not grass moving, lighting changes, timestamps, etc. If your motion detection is too sensitive, you will experience higher CPU loads and greater false positives from the increased rate of object detection. If it is not sensitive enough, you will miss objects that you want to track. + +## Create Motion Masks + +First, mask areas with regular motion not caused by the objects you want to detect. The best way to find candidates for motion masks is by watching the debug stream with motion boxes enabled. Good use cases for motion masks are timestamps or tree limbs and large bushes that regularly move due to wind. When possible, avoid creating motion masks that would block motion detection for objects you want to track **even if they are in locations where you don't want alerts or detections**. Motion masks should not be used to avoid detecting objects in specific areas. More details can be found [in the masks docs.](/configuration/masks.md). + +## Prepare For Testing + +The recommended way to tune motion detection is to use the built-in Motion Tuner. Navigate to and select the camera you want to tune. This screen lets you adjust motion detection values live and immediately see the effect on what is detected as motion, making it the fastest way to find optimal settings for each camera. + +## Tuning Motion Detection During The Day + +Now that things are set up, find a time to tune that represents normal circumstances. For example, if you tune your motion on a day that is sunny and windy you may find later that the motion settings are not sensitive enough on a cloudy and still day. + +:::note + +Remember that motion detection is just used to determine when object detection should be used. You should aim to have motion detection sensitive enough that you won't miss objects you want to detect with object detection. The goal is to prevent object detection from running constantly for every small pixel change in the image. Windy days are still going to result in lots of motion being detected. + +::: + +### Threshold + +The threshold value dictates how much of a change in a pixels luminance is required to be considered motion. + + + + +Navigate to to set the threshold globally. + +To override for a specific camera, navigate to and select the camera, or use the to adjust it live. + +| Field | Description | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Motion threshold** | The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. The value should be between 1 and 255. (default: 30) | + + + + +```yaml +motion: + # Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below) + # Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. + # The value should be between 1 and 255. + threshold: 30 +``` + + + + +Lower values mean motion detection is more sensitive to changes in color, making it more likely for example to detect motion when a brown dog blends in with a brown fence or a person wearing a red shirt blends in with a red car. If the threshold is too low however, it may detect things like grass blowing in the wind, shadows, etc. to be detected as motion. + +Watching the motion boxes in the debug view, increase the threshold until you only see motion that is visible to the eye. Once this is done, it is important to test and ensure that desired motion is still detected. + +### Contour Area + + + + +Navigate to to set the contour area globally. + +To override for a specific camera, navigate to and select the camera, or use the to adjust it live. + +| Field | Description | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Contour area** | Minimum size in pixels in the resized motion image that counts as motion. Increasing this value will prevent smaller areas of motion from being detected. Decreasing will make motion detection more sensitive to smaller moving objects. As a rule of thumb: 10 = high sensitivity, 30 = medium sensitivity, 50 = low sensitivity. (default: 10) | + + + + +```yaml +motion: + # Optional: Minimum size in pixels in the resized motion image that counts as motion (default: shown below) + # Increasing this value will prevent smaller areas of motion from being detected. Decreasing will + # make motion detection more sensitive to smaller moving objects. + # As a rule of thumb: + # - 10 - high sensitivity + # - 30 - medium sensitivity + # - 50 - low sensitivity + contour_area: 10 +``` + + + + +Once the threshold calculation is run, the pixels that have changed are grouped together. The contour area value is used to decide which groups of changed pixels qualify as motion. Smaller values are more sensitive meaning people that are far away, small animals, etc. are more likely to be detected as motion, but it also means that small changes in shadows, leaves, etc. are detected as motion. Higher values are less sensitive meaning these things won't be detected as motion but with the risk that desired motion won't be detected until closer to the camera. + +Watching the motion boxes in the debug view, adjust the contour area until there are no motion boxes smaller than the smallest you'd expect frigate to detect something moving. + +### Improve Contrast + +At this point if motion is working as desired there is no reason to continue with tuning for the day. If you were unable to find a balance between desired and undesired motion being detected, you can try disabling improve contrast and going back to the threshold and contour area steps. + +## Tuning Motion Detection During The Night + +Once daytime motion detection is tuned, there is a chance that the settings will work well for motion detection during the night as well. If this is the case then the preferred settings can be written to the config file and left alone. + +However, if the preferred day settings do not work well at night it is recommended to use Home Assistant or some other solution to automate changing the settings. That way completely separate sets of motion settings can be used for optimal day and night motion detection. + +## Tuning For Large Changes In Motion + +### Lightning Threshold + + + + +Navigate to and expand the advanced fields to find the lightning threshold setting. + +To override for a specific camera, navigate to and select the camera. + +| Field | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Lightning threshold** | The percentage of the image used to detect lightning or other substantial changes where motion detection needs to recalibrate. Increasing this value will make motion detection more likely to consider lightning or IR mode changes as valid motion. Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching a doorbell camera. (default: 0.8) | + + + + +```yaml +motion: + # Optional: The percentage of the image used to detect lightning or + # other substantial changes where motion detection needs to + # recalibrate. (default: shown below) + # Increasing this value will make motion detection more likely + # to consider lightning or IR mode changes as valid motion. + # Decreasing this value will make motion detection more likely + # to ignore large amounts of motion such as a person + # approaching a doorbell camera. + lightning_threshold: 0.8 +``` + + + + +Large changes in motion like PTZ moves and camera switches between Color and IR mode should result in a pause in object detection. `lightning_threshold` defines the percentage of the image used to detect these substantial changes. Increasing this value makes motion detection more likely to treat large changes (like IR mode switches) as valid motion. Decreasing it makes motion detection more likely to ignore large amounts of motion, such as a person approaching a doorbell camera. + +Note that `lightning_threshold` does **not** stop motion-based recordings from being saved. It only prevents additional motion analysis after the threshold is exceeded, reducing false positive object detections during high-motion periods (e.g. storms or PTZ sweeps) without interfering with recordings. + +:::warning + +Some cameras, like doorbell cameras, may have missed detections when someone walks directly in front of the camera and the `lightning_threshold` causes motion detection to recalibrate. In this case, it may be desirable to increase the `lightning_threshold` to ensure these objects are not missed. + +::: + +### Skip Motion On Large Scene Changes + + + + +Navigate to and expand the advanced fields to find the skip motion threshold setting. + +To override for a specific camera, navigate to and select the camera. + +| Field | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Skip motion threshold** | Fraction of the frame that must change in a single update before Frigate will completely ignore any motion in that frame. Values range between 0.0 and 1.0; leave unset (null) to disable. For example, setting this to 0.7 causes Frigate to skip reporting motion boxes when more than 70% of the image appears to change (e.g. during lightning storms, IR/color mode switches, or other sudden lighting events). | + + + + +```yaml +motion: + # Optional: Fraction of the frame that must change in a single update + # before Frigate will completely ignore any motion in that frame. + # Values range between 0.0 and 1.0, leave unset (null) to disable. + # Setting this to 0.7 would cause Frigate to **skip** reporting + # motion boxes when more than 70% of the image appears to change + # (e.g. during lightning storms, IR/color mode switches, or other + # sudden lighting events). + skip_motion_threshold: 0.7 +``` + + + + +This option is handy when you want to prevent large transient changes from triggering recordings or object detection. It differs from `lightning_threshold` because it completely suppresses motion instead of just forcing a recalibration. + +:::warning + +When the skip threshold is exceeded, **no motion is reported** for that frame, meaning **nothing is recorded** for that frame. That means you can miss something important, like a PTZ camera auto-tracking an object or activity while the camera is moving. If you prefer to guarantee that every frame is saved, leave this unset and accept occasional recordings containing scene noise. They typically only take up a few megabytes and are quick to scan in the timeline UI. + +::: + +## Reviewing Detected Motion + +To review what the detector picked up, or to search past recordings for motion in a specific region, see [Reviewing Motion](/usage/review#reviewing-motion) on the Review page. diff --git a/docs/docs/configuration/notifications.md b/docs/docs/configuration/notifications.md new file mode 100644 index 0000000..cc9a776 --- /dev/null +++ b/docs/docs/configuration/notifications.md @@ -0,0 +1,106 @@ +--- +id: notifications +title: Notifications +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +# Notifications + +Frigate offers native notifications using the [WebPush Protocol](https://web.dev/articles/push-notifications-web-push-protocol) which uses the [VAPID spec](https://tools.ietf.org/html/draft-thomson-webpush-vapid) to deliver notifications to web apps using encryption. + +:::info + +Push notifications require internet access from the Frigate server to the browser vendor's push service (e.g., Google FCM, Mozilla autopush). See [Network Requirements](/frigate/network_requirements#push-notifications) for details. + +::: + +## Setting up Notifications + +In order to use notifications the following requirements must be met: + +- Frigate must be accessed via a secure `https` connection ([see the authorization docs](/configuration/authentication)). +- A supported browser must be used. Currently Chrome, Firefox, and Safari are known to be supported. +- In order for notifications to be usable externally, Frigate must be accessible externally. +- For iOS devices, some users have also indicated that the Notifications switch needs to be enabled in iOS Settings --> Apps --> Safari --> Advanced --> Features. + +### Configuration + +Enable notifications and fill out the required fields. + +Optionally, change the default cooldown period for notifications. The cooldown can also be overridden at the camera level. + +Notifications will be prevented if either: + +- The global cooldown period hasn't elapsed since any camera's last notification +- The camera-specific cooldown period hasn't elapsed for the specific camera + +#### Global notifications + + + + +1. Navigate to . + - Set **Email** to your email address + - Enable notifications for the desired cameras + + + + +```yaml +notifications: + enabled: True + email: "johndoe@gmail.com" + cooldown: 10 # wait 10 seconds before sending another notification from any camera +``` + + + + +#### Per-camera notifications + + + + +1. Navigate to and select the desired camera. + - Set **Enable notifications** to on + - Set **Cooldown period** to the desired number of seconds to wait before sending another notification from this camera (e.g. `30`) + + + + +```yaml +cameras: + doorbell: + ... + notifications: + enabled: True + cooldown: 30 # wait 30 seconds before sending another notification from the doorbell camera +``` + + + + +### Registration + +Once notifications are enabled, press the `Register for Notifications` button on all devices that you would like to receive notifications on. This will register the background worker. After this Frigate must be restarted and then notifications will begin to be sent. + +## Supported Notifications + +Currently notifications are only supported for review alerts. More notifications will be supported in the future. + +:::note + +Currently, only Chrome supports images in notifications. Safari and Firefox will only show a title and message in the notification. + +::: + +## Reduce Notification Latency + +Different platforms handle notifications differently, some settings changes may be required to get optimal notification delivery. + +### Android + +Most Android phones have battery optimization settings. To get reliable Notification delivery the browser (Chrome, Firefox) should have battery optimizations disabled. If Frigate is running as a PWA then the Frigate app should have battery optimizations disabled as well. diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md new file mode 100644 index 0000000..23595ac --- /dev/null +++ b/docs/docs/configuration/object_detectors.md @@ -0,0 +1,860 @@ +--- +id: object_detectors +title: Object Detectors +--- + +import CommunityBadge from '@site/src/components/CommunityBadge'; +import ConfigTabs from '@site/src/components/ConfigTabs'; +import TabItem from '@theme/TabItem'; +import NavPath from '@site/src/components/NavPath'; +import ModelConfigDropdown from '@site/src/components/ModelConfigDropdown'; +import objectDetectorsModels from '@site/data/object_detectors_models.yaml'; + +### Supported hardware + +Object detection is what allows Frigate to identify _what_ is in your camera's view (people, cars, animals, and more) rather than just reacting to pixel changes. When Frigate's motion detection finds activity in a frame, that region is sent to an **object detector**, which returns the objects it recognizes along with their location and a confidence score. These detections are what drive tracked objects, alerts, detections, and notifications. + +Object detection is computationally intensive, so Frigate is designed to run it on a dedicated AI accelerator or GPU rather than the CPU. A **detector** is the specific hardware-and-model backend Frigate uses to run inference. Choosing a detector that matches your hardware is one of the most important steps in getting good performance, and the right choice depends on what device Frigate is running on. + +:::info +Frigate supports multiple different detectors that work on different types of hardware: + +**Most Hardware** + +- [Coral EdgeTPU](#edge-tpu-detector): The Google Coral EdgeTPU is available in USB, Mini PCIe, and m.2 formats allowing for a wide range of compatibility with devices. +- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices, offering a wide range of compatibility with devices. +- [MemryX](#memryx-mx3): The MX3 Acceleration module is available in m.2 format, offering broad compatibility across various platforms. +- [DeGirum](#degirum): Service for using hardware devices in the cloud or locally. Hardware and models provided on the cloud on [their website](https://hub.degirum.com). + +**AMD** + +- [ROCm](#amdrocm-gpu-detector): ROCm can run on AMD Discrete GPUs to provide efficient object detection. +- [ONNX](#onnx): ROCm will automatically be detected and used as a detector in the `-rocm` Frigate image when a supported ONNX model is configured. + +**Apple Silicon** + +- [Apple Silicon](#apple-silicon-detector): Apple Silicon can run on M1 and newer Apple Silicon devices. + +**Intel** + +- [OpenVino](#openvino-detector): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel CPUs to provide efficient object detection. +- [ONNX](#onnx): OpenVINO will automatically be detected and used as a detector in the default Frigate image when a supported ONNX model is configured. + +**Nvidia GPU** + +- [ONNX](#onnx): Nvidia GPUs will automatically be detected and used as a detector in the `-tensorrt` Frigate image when a supported ONNX model is configured. + +**Nvidia Jetson** + +- [TensortRT](#nvidia-tensorrt-detector): TensorRT can run on Jetson devices, using one of many default models. +- [ONNX](#onnx): TensorRT will automatically be detected and used as a detector in the `-tensorrt-jp6` Frigate image when a supported ONNX model is configured. + +**Rockchip** + +- [RKNN](#rockchip-platform): RKNN models can run on Rockchip devices with included NPUs. + +**Synaptics** + +- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs. + +**AXERA** + +- [AXEngine](#axera): axmodels can run on AXERA AI acceleration. + +**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. + +::: + +:::note + +Multiple detectors can not be mixed for object detection (ex: OpenVINO and Coral EdgeTPU can not be used for object detection at the same time). + +This does not affect using hardware for accelerating other tasks such as [semantic search](./semantic_search.md) + +::: + +### Choosing a model size + +Along with picking a detector for your hardware, you will choose a model's **input resolution** (such as `320x320` or `640x640`) and, for model families like YOLOv9, a **variant size** (`tiny`, `small`, etc.). Both affect the balance between accuracy and the inference time your hardware can sustain. + +**Resolution (320x320 vs 640x640):** Frigate is optimized for `320x320` models, and `320x320` is the best choice for the vast majority of setups. Frigate is specifically designed to compensate for the smaller model by cropping a region of motion from the full frame and zooming into it before running detection, so a `320x320` model is actually _better_ at small and distant objects, not worse. A `640x640` model is slower and uses more resources, and its main benefit is fitting more objects into a single inference when many objects are spread across a large area. Recent versions of Frigate have improved support for `640x640` models, but `320x320` remains the recommended starting point for nearly all setups. + +**Variant size (tiny/small/medium):** Larger variants are gradually more accurate but slower. Whether the difference is noticeable depends on your specific cameras and scenes. A good rule of thumb is to use the largest model your hardware can run without skipping detections, which you can monitor on the page in the UI. Better accuracy only helps if your detector keeps up with the detection load across all cameras. + +**Acceptable inference time depends on your hardware.** Inference time alone does not tell the whole story, because different hardware has different capacity. A GPU can run multiple instances of the same model concurrently, so an inference time around 30ms can still keep up with several cameras. A Google Coral runs only a single instance of the model, so it needs a much lower inference time (around 10ms) to keep up. + +:::tip + +The best detection accuracy comes from a model trained on images that look like what Frigate actually sees: security camera footage cropped to regions of interest. You can train or fine-tune your own model on images like this and run it as a custom model (see the per-detector sections below), but [Frigate+](/plus) makes this much easier by handling the training for you on images submitted from your own cameras. For YOLOv9, the `s` (small) variant at `320x320` resolution is a good place to start. + +::: + +# Officially Supported Detectors + +Frigate provides a number of builtin detector types. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. + +## Edge TPU Detector + +The Edge TPU detector type runs TensorFlow Lite models utilizing the Google Coral delegate for hardware acceleration. To configure an Edge TPU detector, set the `"type"` attribute to `"edgetpu"`. + +The Edge TPU device can be specified using the `"device"` attribute according to the [Documentation for the TensorFlow Lite Python API](https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api). If not set, the delegate will use the first device it finds. + +:::tip + +See [common Edge TPU troubleshooting steps](/troubleshooting/edgetpu) if the Edge TPU is not detected. + +::: + +### Single USB Coral + + + + +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. + + + + +```yaml +detectors: + coral: + type: edgetpu + device: usb +``` + + + + +### Multiple USB Corals + + + + +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `usb:0` and `usb:1` as the device for each. + + + + +```yaml +detectors: + coral1: + type: edgetpu + device: usb:0 + coral2: + type: edgetpu + device: usb:1 +``` + + + + +### Native Coral (Dev Board) + +_warning: may have [compatibility issues](https://github.com/blakeblackshear/frigate/issues/1706) after `v0.9.x`_ + + + + +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then leave the device field empty. + + + + +```yaml +detectors: + coral: + type: edgetpu + device: "" +``` + + + + +### Single PCIE/M.2 Coral + + + + +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `pci`. + + + + +```yaml +detectors: + coral: + type: edgetpu + device: pci +``` + + + + +### Multiple PCIE/M.2 Corals + + + + +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `pci:0` and `pci:1` as the device for each. + + + + +```yaml +detectors: + coral1: + type: edgetpu + device: pci:0 + coral2: + type: edgetpu + device: pci:1 +``` + + + + +### Mixing Corals + + + + +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors with different device types (e.g., `usb` and `pci`). + + + + +```yaml +detectors: + coral_usb: + type: edgetpu + device: usb + coral_pci: + type: edgetpu + device: pci +``` + + + + +### Configuration {#configuration-edgetpu} + + + +--- + +## Hailo-8 + +This detector is available for use with both Hailo-8 and Hailo-8L AI Acceleration Modules. The integration automatically detects your hardware architecture via the Hailo CLI and selects the appropriate default model if no custom model is specified. + +See the [installation docs](../frigate/installation.md#hailo-8) for information on configuring the Hailo hardware. + +:::info + +If no custom model is provided, the Hailo detector downloads a default model from the Hailo Model Zoo on first startup. Once cached, the model works fully offline. See [Network Requirements](/frigate/network_requirements#hardware-specific-detector-models) for details. + +::: + +### Configuration {#configuration-hailo} + +When configuring the Hailo detector, you have two options to specify the model: a local **path** or a **URL**. +If both are provided, the detector will first check for the model at the given local path. If the file is not found, it will download the model from the specified URL. The model file is cached under `/config/model_cache/hailo`. + + + +For additional ready-to-use models, please visit: https://github.com/hailo-ai/hailo_model_zoo + +Hailo8 supports all models in the Hailo Model Zoo that include HailoRT post-processing. You're welcome to choose any of these pre-configured models for your implementation. + +> **Note:** +> The config.path parameter can accept either a local file path or a URL ending with .hef. When provided, the detector will first check if the path is a local file path. If the file exists locally, it will use it directly. If the file is not found locally or if a URL was provided, it will attempt to download the model from the specified URL. + +--- + +## OpenVINO Detector + +The OpenVINO detector type runs an OpenVINO IR model on AMD and Intel CPUs, Intel GPUs and Intel NPUs. To configure an OpenVINO detector, set the `"type"` attribute to `"openvino"`. + +The OpenVINO device to be used is specified using the `"device"` attribute according to the naming conventions in the [Device Documentation](https://docs.openvino.ai/2025/openvino-workflow/running-inference/inference-devices-and-modes.html). The most common devices are `CPU`, `GPU`, or `NPU`. + +OpenVINO is supported on 6th Gen Intel platforms (Skylake) and newer. It will also run on AMD CPUs despite having no official support for it. A supported Intel platform is required to use the `GPU` or `NPU` device with OpenVINO. For detailed system requirements, see [OpenVINO System Requirements](https://docs.openvino.ai/2025/about-openvino/release-notes-openvino/system-requirements.html) + +:::tip + +**NPU + GPU Systems:** If you have both NPU and GPU available (Intel Core Ultra processors), use NPU for object detection and GPU for enrichments (semantic search, face recognition, etc.) for best performance and compatibility. + +When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming GPU resources are available. An example configuration would be: + +```yaml +detectors: + ov_0: + type: openvino + device: GPU # or NPU + ov_1: + type: openvino + device: GPU # or NPU +``` + +::: + +### Configuration {#configuration-openvino} + + + +--- + +## Apple Silicon detector + +The NPU in Apple Silicon can't be accessed from within a container, so the [Apple Silicon detector client](https://github.com/frigate-nvr/apple-silicon-detector) must first be setup. It is recommended to use the Frigate docker image with `-standard-arm64` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-standard-arm64`. + +### Setup {#setup-apple-silicon} + +1. Setup the [Apple Silicon detector client](https://github.com/frigate-nvr/apple-silicon-detector) and run the client +2. Configure the detector in Frigate and startup Frigate + +### Configuration {#configuration-apple-silicon} + +Using the detector config below will connect to the client: + + + +Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. + +## AMD/ROCm GPU detector + +### Setup {#setup-rocm} + +Support for AMD GPUs is provided using the [ONNX detector](#onnx). In order to utilize the AMD GPU for object detection use a frigate docker image with `-rocm` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-rocm`. + +### Docker settings for GPU access + +ROCm needs access to the `/dev/kfd` and `/dev/dri` devices. When docker or frigate is not run under root then also `video` (and possibly `render` and `ssl/_ssl`) groups should be added. + +When running docker directly the following flags should be added for device access: + +```bash +$ docker run --device=/dev/kfd --device=/dev/dri \ + ... +``` + +When using Docker Compose: + +```yaml {4-6} +services: + frigate: + ... + devices: + - /dev/dri + - /dev/kfd +``` + +For reference on recommended settings see [running ROCm/pytorch in Docker](https://rocm.docs.amd.com/projects/install-on-linux/en/develop/how-to/3rd-party/pytorch-install.html#using-docker-with-pytorch-pre-installed). + +### Docker settings for overriding the GPU chipset + +Your GPU might work just fine without any special configuration but in many cases they need manual settings. AMD/ROCm software stack comes with a limited set of GPU drivers and for newer or missing models you will have to override the chipset version to an older/generic version to get things working. + +Also AMD/ROCm does not "officially" support integrated GPUs. It still does work with most of them just fine but requires special settings. One has to configure the `HSA_OVERRIDE_GFX_VERSION` environment variable. See the [ROCm bug report](https://github.com/ROCm/ROCm/issues/1743) for context and examples. + +For the rocm frigate build there is some automatic detection: + +- gfx1031 -> 10.3.0 +- gfx1103 -> 11.0.0 + +If you have something else you might need to override the `HSA_OVERRIDE_GFX_VERSION` at Docker launch. Suppose the version you want is `10.0.0`, then you should configure it from command line as: + +```bash +$ docker run -e HSA_OVERRIDE_GFX_VERSION=10.0.0 \ + ... +``` + +When using Docker Compose: + +```yaml {4-5} +services: + frigate: + ... + environment: + HSA_OVERRIDE_GFX_VERSION: "10.0.0" +``` + +Figuring out what version you need can be complicated as you can't tell the chipset name and driver from the AMD brand name. + +- first make sure that rocm environment is running properly by running `/opt/rocm/bin/rocminfo` in the frigate container -- it should list both the CPU and the GPU with their properties +- find the chipset version you have (gfxNNN) from the output of the `rocminfo` (see below) +- use a search engine to query what `HSA_OVERRIDE_GFX_VERSION` you need for the given gfx name ("gfxNNN ROCm HSA_OVERRIDE_GFX_VERSION") +- override the `HSA_OVERRIDE_GFX_VERSION` with relevant value +- if things are not working check the frigate docker logs + +#### Figuring out if AMD/ROCm is working and found your GPU + +```bash +$ docker exec -it frigate /opt/rocm/bin/rocminfo +``` + +#### Figuring out your AMD GPU chipset version: + +We unset the `HSA_OVERRIDE_GFX_VERSION` to prevent an existing override from messing up the result: + +```bash +$ docker exec -it frigate /bin/bash -c '(unset HSA_OVERRIDE_GFX_VERSION && /opt/rocm/bin/rocminfo |grep gfx)' +``` + +### Configuration {#configuration-rocm} + +:::tip + +The AMD GPU kernel is known problematic especially when converting models to mxr format. The recommended approach is: + +1. Disable object detection in the config. +2. Startup Frigate with the onnx detector configured, the main object detection model will be converted to mxr format and cached in the config directory. +3. Once this is finished as indicated by the logs, enable object detection in the UI and confirm that it is working correctly. +4. Re-enable object detection in the config. + +::: + +See [ONNX supported models](#onnx) for supported models, there are some caveats: + +- D-FINE / DEIMv2 models are not supported +- YOLO-NAS models are known to not run well on integrated GPUs + + + +## ONNX + +ONNX is an open format for building machine learning models, Frigate supports running ONNX models on CPU, OpenVINO, ROCm, and TensorRT. On startup Frigate will automatically try to use a GPU if one is available. + +:::info + +If the correct build is used for your GPU then the GPU will be detected and used automatically. + +- **AMD** + - ROCm will automatically be detected and used with the ONNX detector in the `-rocm` Frigate image. + +- **Intel** + - OpenVINO will automatically be detected and used with the ONNX detector in the default Frigate image. + +- **Nvidia** + - Nvidia GPUs will automatically be detected and used with the ONNX detector in the `-tensorrt` Frigate image. + - Jetson devices will automatically be detected and used with the ONNX detector in the `-tensorrt-jp6` Frigate image. + +::: + +:::tip + +When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming GPU resources are available. An example configuration would be: + +```yaml +detectors: + onnx_0: + type: onnx + onnx_1: + type: onnx +``` + +::: + +### Configuration {#configuration-onnx} + + + +--- + +## CPU Detector (not recommended) + +The CPU detector type runs a TensorFlow Lite model utilizing the CPU without hardware acceleration. It is recommended to use a hardware accelerated detector type instead for better performance. To configure a CPU based detector, set the `"type"` attribute to `"cpu"`. + +:::danger + +The CPU detector is not recommended for general use. If you do not have GPU or Edge TPU hardware, using the [OpenVINO Detector](#openvino-detector) in CPU mode is often more efficient than using the CPU detector. + +::: + +The number of threads used by the interpreter can be specified using the `"num_threads"` attribute, and defaults to `3.` + +A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`. + +### Configuration {#configuration-cpu} + + + +When using CPU detectors, you can add one CPU detector per camera. Adding more detectors than the number of cameras should not improve performance. + +## Deepstack / CodeProject.AI Server Detector + +The Deepstack / CodeProject.AI Server detector for Frigate allows you to integrate Deepstack and CodeProject.AI object detection capabilities into Frigate. CodeProject.AI and DeepStack are open-source AI platforms that can be run on various devices such as the Raspberry Pi, Nvidia Jetson, and other compatible hardware. It is important to note that the integration is performed over the network, so the inference times may not be as fast as native Frigate detectors, but it still provides an efficient and reliable solution for object detection and tracking. + +### Setup {#setup-deepstack} + +To get started with CodeProject.AI, visit their [official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to follow the instructions to download and install the AI server on your preferred device. Detailed setup instructions for CodeProject.AI are outside the scope of the Frigate documentation. + +To integrate CodeProject.AI into Frigate, configure the detector as follows: + +### Configuration {#configuration-deepstack} + + + +Replace `` and `` with the IP address and port of your CodeProject.AI server. + +To verify that the integration is working correctly, start Frigate and observe the logs for any error messages related to CodeProject.AI. Additionally, you can check the Frigate web interface to see if the objects detected by CodeProject.AI are being displayed and tracked properly. + +# Community Supported Detectors + +## MemryX MX3 + +This detector is available for use with the MemryX MX3 accelerator M.2 module. Frigate supports the MX3 on compatible hardware platforms, providing efficient and high-performance object detection. + +See the [installation docs](../frigate/installation.md#memryx-mx3) for information on configuring the MemryX hardware. + +To configure a MemryX detector, simply set the `type` attribute to `memryx` and follow the configuration guide below. + +### Configuration {#configuration-memryx} + + + +#### Using a Custom Model + +To use your own custom model, first compile it into a [.dfp](https://developer.memryx.com/2p1/specs/files.html#dataflow-program) file, which is the format used by MemryX. + +#### Compile the Model + +Custom models must be compiled using **MemryX SDK 2.1**. + +Before compiling your model, install the MemryX Neural Compiler tools from the +[Install Tools](https://developer.memryx.com/2p1/get_started/install_tools.html) page on the **host**. + +> **Note:** It is recommended to compile the model on the host machine, or on another separate machine, rather than inside the Frigate Docker container. Installing the compiler inside Docker may conflict with container packages. It is recommended to create a Python virtual environment and install the compiler there. + +Once the SDK 2.1 environment is set up, follow the +[MemryX Compiler](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage) documentation to compile your model. + +Example: + +```bash +mx_nc -m yolonas.onnx -c 4 --autocrop -v --dfp_fname yolonas.dfp +``` + +For detailed instructions on compiling models, refer to the [MemryX Compiler](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage) docs and [Tutorials](https://developer.memryx.com/2p1/tutorials/tutorials.html). + +#### Package the Compiled Model + +1. Package your compiled model into a `.zip` file. + +2. The `.zip` file must contain the compiled `.dfp` file. + +3. Depending on the model, the compiler may also generate a cropped post-processing network. If present, it will be named with the suffix `_post.onnx`. + +4. Bind-mount the `.zip` file into the container and specify its path using `model.path` in your config. + +5. Update `labelmap_path` to match your custom model's labels. + +```yaml +# The detector automatically selects the default model if nothing is provided in the config. +# +# Optionally, you can specify a local model path as a .zip file to override the default. +# If a local path is provided and the file exists, it will be used instead of downloading. +# +# Example: +# path: /config/yolonas.zip +# +# The .zip file must contain: +# ├── yolonas.dfp (a file ending with .dfp) +# └── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network) +``` + +--- + +## NVidia TensorRT Detector + +Nvidia Jetson devices may be used for object detection using the TensorRT libraries. Due to the size of the additional libraries, this detector is only provided in images with the `-tensorrt-jp6` tag suffix, e.g. `ghcr.io/blakeblackshear/frigate:stable-tensorrt-jp6`. This detector is designed to work with Yolo models for object detection. + +### Generate Models + +The model used for TensorRT must be preprocessed on the same hardware platform that they will run on. This means that each user must run additional setup to generate a model file for the TensorRT library. A script is included that will build several common models. + +The Frigate image will generate model files during startup if the specified model is not found. Processed models are stored in the `/config/model_cache` folder. Typically the `/config` path is mapped to a directory on the host already and the `model_cache` does not need to be mapped separately unless the user wants to store it in a different location on the host. + +By default, no models will be generated, but this can be overridden by specifying the `YOLO_MODELS` environment variable in Docker. One or more models may be listed in a comma-separated format, and each one will be generated. Models will only be generated if the corresponding `{model}.trt` file is not present in the `model_cache` folder, so you can force a model to be regenerated by deleting it from your Frigate data folder. + +If you have a Jetson device with DLAs (Xavier or Orin), you can generate a model that will run on the DLA by appending `-dla` to your model name, e.g. specify `YOLO_MODELS=yolov7-320-dla`. The model will run on DLA0 (Frigate does not currently support DLA1). DLA-incompatible layers will fall back to running on the GPU. + +If your GPU does not support FP16 operations, you can pass the environment variable `USE_FP16=False` to disable it. + +Specific models can be selected by passing an environment variable to the `docker run` command or in your `docker-compose.yml` file. Use the form `-e YOLO_MODELS=yolov4-416,yolov4-tiny-416` to select one or more model names. The models available are shown below. + +
+Available Models +``` +yolov3-288 +yolov3-416 +yolov3-608 +yolov3-spp-288 +yolov3-spp-416 +yolov3-spp-608 +yolov3-tiny-288 +yolov3-tiny-416 +yolov4-288 +yolov4-416 +yolov4-608 +yolov4-csp-256 +yolov4-csp-512 +yolov4-p5-448 +yolov4-p5-896 +yolov4-tiny-288 +yolov4-tiny-416 +yolov4x-mish-320 +yolov4x-mish-640 +yolov7-tiny-288 +yolov7-tiny-416 +yolov7-640 +yolov7-416 +yolov7-320 +yolov7x-640 +yolov7x-320 +``` +
+ +An example `docker-compose.yml` fragment that converts the `yolov4-608` and `yolov7x-640` models would look something like this: + +```yml +frigate: + environment: + - YOLO_MODELS=yolov7-320,yolov7x-640 + - USE_FP16=false +``` + +### Configuration Parameters + +The TensorRT detector can be selected by specifying `tensorrt` as the model type. The GPU will need to be passed through to the docker container using the same methods described in the [Hardware Acceleration](hardware_acceleration_video.md#nvidia-gpus) section. If you pass through multiple GPUs, you can select which GPU is used for a detector with the `device` configuration parameter. The `device` parameter is an integer value of the GPU index, as shown by `nvidia-smi` within the container. + +The TensorRT detector uses `.trt` model files that are located in `/config/model_cache/tensorrt` by default. These model path and dimensions used will depend on which model you have generated. + +Use the config below to work with generated TRT models: + + + +## Synaptics + +Hardware accelerated object detection is supported on the following SoCs: + +- SL1680 + +This implementation uses the [Synaptics model conversion](https://synaptics-synap.github.io/doc/v/latest/docs/manual/introduction.html#offline-model-conversion), version v3.1.0. + +This implementation is based on sdk `v1.5.0`. + +See the [installation docs](../frigate/installation.md#synaptics) for information on configuring the SL-series NPU hardware. + +### Configuration {#configuration-synaptics} + +When configuring the Synap detector, you have to specify the model: a local **path**. + + + +## Rockchip platform + +Hardware accelerated object detection is supported on the following SoCs: + +- RK3562 +- RK3566 +- RK3568 +- RK3576 +- RK3588 + +This implementation uses the [Rockchip's RKNN-Toolkit2](https://github.com/airockchip/rknn-toolkit2/), version v2.3.2. + +:::info + +If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. See [Network Requirements](/frigate/network_requirements#hardware-specific-detector-models) for details. + +::: + +:::tip + +When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming NPU resources are available. An example configuration would be: + +```yaml +detectors: + rknn_0: + type: rknn + num_cores: 0 + rknn_1: + type: rknn + num_cores: 0 +``` + +::: + +### Prerequisites + +Make sure to follow the [Rockchip specific installation instructions](/frigate/installation#rockchip-platform). + +:::tip + +You can get the load of your NPU with the following command: + +```bash +$ cat /sys/kernel/debug/rknpu/load +>> NPU load: Core0: 0%, Core1: 0%, Core2: 0%, +``` + +::: + +### RockChip Supported Models + +This `config.yml` shows all relevant options to configure the detector and explains them. All values shown are the default values (except for two). Lines that are required at least to use the detector are labeled as required, all other lines are optional. + +The inference time was determined on a rk3588 with 3 NPU cores. + +| Model | Size in mb | Inference time in ms | +| --------------------- | ---------- | -------------------- | +| deci-fp16-yolonas_s | 24 | 25 | +| deci-fp16-yolonas_m | 62 | 35 | +| deci-fp16-yolonas_l | 81 | 45 | +| frigate-fp16-yolov9-t | 6 | 35 | +| rock-i8-yolox_nano | 3 | 14 | +| rock-i8_yolox_tiny | 6 | 18 | + +- All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space. +- You can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models. + + + +### Converting your own onnx model to rknn format + +To convert a onnx model to the rknn format using the [rknn-toolkit2](https://github.com/airockchip/rknn-toolkit2/) you have to: + +- Place one or more models in onnx format in the directory `config/model_cache/rknn_cache/onnx` on your docker host (this might require `sudo` privileges). +- Save the configuration file under `config/conv2rknn.yaml` (see below for details). +- Run `docker exec python3 /opt/conv2rknn.py`. If the conversion was successful, the rknn models will be placed in `config/model_cache/rknn_cache`. + +This is an example configuration file that you need to adjust to your specific onnx model: + +```yaml +soc: ["rk3562", "rk3566", "rk3568", "rk3576", "rk3588"] +quantization: false + +output_name: "{input_basename}" + +config: + mean_values: [[0, 0, 0]] + std_values: [[255, 255, 255]] + quant_img_RGB2BGR: true +``` + +Explanation of the parameters: + +- `soc`: A list of all SoCs you want to build the rknn model for. If you don't specify this parameter, the script tries to find out your SoC and builds the rknn model for this one. +- `quantization`: true: 8 bit integer (i8) quantization, false: 16 bit float (fp16). Default: false. +- `output_name`: The output name of the model. The following variables are available: + - `quant`: "i8" or "fp16" depending on the config + - `input_basename`: the basename of the input model (e.g. "my_model" if the input model is called "my_model.onnx") + - `soc`: the SoC this model was build for (e.g. "rk3588") + - `tk_version`: Version of `rknn-toolkit2` (e.g. "2.3.0") + - **example**: Specifying `output_name = "frigate-{quant}-{input_basename}-{soc}-v{tk_version}"` could result in a model called `frigate-i8-my_model-rk3588-v2.3.0.rknn`. +- `config`: Configuration passed to `rknn-toolkit2` for model conversion. For an explanation of all available parameters have a look at section "2.2. Model configuration" of [this manual](https://github.com/MarcA711/rknn-toolkit2/releases/download/v2.3.2/03_Rockchip_RKNPU_API_Reference_RKNN_Toolkit2_V2.3.2_EN.pdf). + +## DeGirum + +DeGirum is a detector that can use any type of hardware listed on [their website](https://hub.degirum.com). DeGirum can be used with local hardware through a DeGirum AI Server, or through the use of `@local`. You can also connect directly to DeGirum's AI Hub to run inferences. **Please Note:** This detector _cannot_ be used for commercial purposes. + +### Configuration {#configuration-degirum} + +#### AI Server Inference + +Before starting with the config file for this section, you must first launch an AI server. DeGirum has an AI server ready to use as a docker container. Add this to your `docker-compose.yml` to get started: + +```yaml +degirum_detector: + container_name: degirum + image: degirum/aiserver:latest + privileged: true + ports: + - "8778:8778" +``` + +All supported hardware will automatically be found on your AI server host as long as relevant runtimes and drivers are properly installed on your machine. Refer to [DeGirum's docs site](https://docs.degirum.com/pysdk/runtimes-and-drivers) if you have any trouble. + +Once completed, configure the detector as follows: + + + +Setting up a model in the `config.yml` is similar to setting up an AI server. +You can set it to: + +- A model listed on the [AI Hub](https://hub.degirum.com), given that the correct zoo name is listed in your detector + - If this is what you choose to do, the correct model will be downloaded onto your machine before running. +- A local directory acting as a zoo. See DeGirum's docs site [for more information](https://docs.degirum.com/pysdk/user-guide-pysdk/organizing-models#model-zoo-directory-structure). +- A path to some model.json. + +```yaml +model: + path: ./mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 # directory to model .json and file + width: 300 # width is in the model name as the first number in the "int"x"int" section + height: 300 # height is in the model name as the second number in the "int"x"int" section + input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here +``` + +#### Local Inference + +It is also possible to eliminate the need for an AI server and run the hardware directly. The benefit of this approach is that you eliminate any bottlenecks that occur when transferring prediction results from the AI server docker container to the frigate one. However, the method of implementing local inference is different for every device and hardware combination, so it's usually more trouble than it's worth. A general guideline to achieve this would be: + +1. Ensuring that the frigate docker container has the runtime you want to use. So for instance, running `@local` for Hailo means making sure the container you're using has the Hailo runtime installed. +2. To double check the runtime is detected by the DeGirum detector, make sure the `degirum sys-info` command properly shows whatever runtimes you mean to install. +3. Create a DeGirum detector in your configuration. + + + +Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file. + +```yaml +model: + path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 + width: 300 # width is in the model name as the first number in the "int"x"int" section + height: 300 # height is in the model name as the second number in the "int"x"int" section + input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here +``` + +#### AI Hub Cloud Inference + +If you do not possess whatever hardware you want to run, there's also the option to run cloud inferences. Do note that your detection fps might need to be lowered as network latency does significantly slow down this method of detection. For use with Frigate, we highly recommend using a local AI server as described above. To set up cloud inferences, + +1. Sign up at [DeGirum's AI Hub](https://hub.degirum.com). +2. Get an access token. +3. Create a DeGirum detector in your configuration. + + + +Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file. + +```yaml +model: + path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 + width: 300 # width is in the model name as the first number in the "int"x"int" section + height: 300 # height is in the model name as the second number in the "int"x"int" section + 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. + +:::info + +The AXEngine detector downloads its default model from HuggingFace on first startup. Once cached, the model works fully offline. See [Network Requirements](/frigate/network_requirements#hardware-specific-detector-models) for details. + +::: + +### Configuration {#configuration-axengine} + +When configuring the AXEngine detector, you have to specify the model name. + + diff --git a/docs/docs/configuration/object_filters.md b/docs/docs/configuration/object_filters.md new file mode 100644 index 0000000..b2e7fca --- /dev/null +++ b/docs/docs/configuration/object_filters.md @@ -0,0 +1,151 @@ +--- +id: object_filters +title: Filters +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +There are several types of object filters that can be used to reduce [false positive](/frigate/glossary#false-positive) rates. + +## Object Scores + +For object filters, any single detection below `min_score` will be ignored as a false positive. `threshold` is based on the median of the history of scores (padded to 3 values) for a tracked object. Consider the following frames when `min_score` is set to 0.6 and threshold is set to 0.85: + +| Frame | Current Score | Score History | Computed Score | Detected Object | +| ----- | ------------- | --------------------------------- | -------------- | --------------- | +| 1 | 0.7 | 0.0, 0, 0.7 | 0.0 | No | +| 2 | 0.55 | 0.0, 0.7, 0.0 | 0.0 | No | +| 3 | 0.85 | 0.7, 0.0, 0.85 | 0.7 | No | +| 4 | 0.90 | 0.7, 0.85, 0.95, 0.90 | 0.875 | Yes | +| 5 | 0.88 | 0.7, 0.85, 0.95, 0.90, 0.88 | 0.88 | Yes | +| 6 | 0.95 | 0.7, 0.85, 0.95, 0.90, 0.88, 0.95 | 0.89 | Yes | + +In frame 2, the score is below the `min_score` value, so Frigate ignores it and it becomes a 0.0. The computed score is the median of the score history (padding to at least 3 values), and only when that computed score crosses the `threshold` is the object marked as a true positive. That happens in frame 4 in the example. + +The **top score** is the highest computed score the tracked object has ever reached during its lifetime. Because the computed score rises and falls as new frames come in, the top score can be thought of as the peak confidence Frigate had in the object. In Frigate's UI (such as the Tracking Details pane in Explore), you may see all three values: + +- **Score**: the raw detector score for that single frame. +- **Computed Score**: the median of the most recent score history at that moment. This is the value compared against `threshold`. +- **Top Score**: the highest computed score reached so far for the tracked object. + +### Minimum Score + +Any detection below `min_score` will be immediately thrown out and never tracked because it is considered a false positive. If `min_score` is too low then false positives may be detected and tracked which can confuse the object tracker and may lead to wasted resources. If `min_score` is too high then lower scoring true positives like objects that are further away or partially occluded may be thrown out which can also confuse the tracker and cause valid tracked objects to be lost or disjointed. + +### Threshold + +`threshold` is used to determine that the object is a true positive. Once an object is detected with a score >= `threshold` object is considered a true positive. If `threshold` is too low then some higher scoring false positives may create a tracked object. If `threshold` is too high then true positive tracked objects may be missed due to the object never scoring high enough. + +## Configuring Object Scores + + + + +Navigate to to set score filters globally. + +| Field | Description | +| --------------------------------------- | ---------------------------------------------------------------- | +| **Object filters > Person > Min Score** | Minimum score for a single detection to initiate tracking | +| **Object filters > Person > Threshold** | Minimum computed (median) score to be considered a true positive | + +To override score filters for a specific camera, navigate to and select the camera. + + + + +```yaml +objects: + filters: + person: + min_score: 0.5 + threshold: 0.7 +``` + +To override at the camera level: + +```yaml +cameras: + front_door: + objects: + filters: + person: + min_score: 0.5 + threshold: 0.7 +``` + + + + +## Object Shape + +False positives can also be reduced by filtering a detection based on its shape. + +### Object Area + +`min_area` and `max_area` filter on the area of an objects bounding box and can be used to reduce false positives that are outside the range of expected sizes. For example when a leaf is detected as a dog or when a large tree is detected as a person, these can be reduced by adding a `min_area` / `max_area` filter. These values can either be in pixels or as a percentage of the frame (for example, 0.12 represents 12% of the frame). + +### Object Proportions + +`min_ratio` and `max_ratio` values are compared against a given detected object's width/height ratio (in pixels). If the ratio is outside this range, the object will be ignored as a false positive. This allows objects that are proportionally too short-and-wide (higher ratio) or too tall-and-narrow (smaller ratio) to be ignored. + +:::info + +Conceptually, a ratio of 1 is a square, 0.5 is a "tall skinny" box, and 2 is a "wide flat" box. If `min_ratio` is 1.0, any object that is taller than it is wide will be ignored. Similarly, if `max_ratio` is 1.0, then any object that is wider than it is tall will be ignored. + +::: + +### Configuring Shape Filters + + + + +Navigate to to set shape filters globally. + +| Field | Description | +| --------------------------------------- | ------------------------------------------------------------------------ | +| **Object filters > Person > Min Area** | Minimum bounding box area in pixels (or decimal for percentage of frame) | +| **Object filters > Person > Max Area** | Maximum bounding box area in pixels (or decimal for percentage of frame) | +| **Object filters > Person > Min Ratio** | Minimum width/height ratio of the bounding box | +| **Object filters > Person > Max Ratio** | Maximum width/height ratio of the bounding box | + +To override shape filters for a specific camera, navigate to and select the camera. + + + + +```yaml +objects: + filters: + person: + min_area: 5000 + max_area: 100000 + min_ratio: 0.5 + max_ratio: 2.0 +``` + +To override at the camera level: + +```yaml +cameras: + front_door: + objects: + filters: + person: + min_area: 5000 + max_area: 100000 +``` + + + + +## Other Tools + +### Zones + +[Required zones](/configuration/zones.md#restricting-alerts-and-detections-to-specific-zones) can be a great tool to reduce false positives that may be detected in the sky or other areas that are not of interest. The required zones will only create tracked objects for objects that enter the zone. + +### Object Masks + +[Object Filter Masks](/configuration/masks#object-filter-masks) are a last resort but can be useful when false positives are in the relatively same place but can not be filtered due to their size or shape. Object filter masks can be configured in . diff --git a/docs/docs/configuration/objects.md b/docs/docs/configuration/objects.md new file mode 100644 index 0000000..e5a24ea --- /dev/null +++ b/docs/docs/configuration/objects.md @@ -0,0 +1,161 @@ +--- +id: objects +title: Available Objects +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; +import labels from "../../../labelmap.txt"; + +Frigate includes the object labels listed below from the Google Coral test data. + +Please note: + +- `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused. +- `person` is the only tracked object by default. To track additional objects, configure them in the objects settings. + +
    + {labels.split("\n").map((label) => ( +
  • {label.replace(/^\d+\s+/, "")}
  • + ))} +
+ +## Configuring Tracked Objects + +By default, Frigate only tracks `person`. To track additional object types, add them to the tracked objects list. + + + + +1. Navigate to . + - Add the desired object types to the **Objects to track** list (e.g., `person`, `car`, `dog`) + +To override the tracked objects list for a specific camera: + +1. Navigate to . + - Add the desired object types to the **Objects to track** list + + + + +```yaml +objects: + track: + - person + - car + - dog +``` + +To override at the camera level: + +```yaml +cameras: + front_door: + objects: + track: + - person + - car +``` + + + + +## Filtering Objects + +Object filters help reduce false positives by constraining the size, shape, and confidence thresholds for each object type. Filters can be configured globally or per camera. + + + + +Navigate to . + +| Field | Description | +| --------------------------------------- | ------------------------------------------------------------------------ | +| **Object filters > Person > Min Area** | Minimum bounding box area in pixels (or decimal for percentage of frame) | +| **Object filters > Person > Max Area** | Maximum bounding box area in pixels (or decimal for percentage of frame) | +| **Object filters > Person > Min Ratio** | Minimum width/height ratio of the bounding box | +| **Object filters > Person > Max Ratio** | Maximum width/height ratio of the bounding box | +| **Object filters > Person > Min Score** | Minimum score for the object to initiate tracking | +| **Object filters > Person > Threshold** | Minimum computed score to be considered a true positive | + +To override filters for a specific camera, navigate to . + + + + +```yaml +objects: + filters: + person: + min_area: 5000 + max_area: 100000 + min_ratio: 0.5 + max_ratio: 2.0 + min_score: 0.5 + threshold: 0.7 +``` + +To override at the camera level: + +```yaml +cameras: + front_door: + objects: + filters: + person: + min_area: 5000 + threshold: 0.7 +``` + + + + +## Object Filter Masks + +Object filter masks prevent specific object types from being detected in certain areas of the camera frame. These masks check the bottom center of the bounding box. A global mask applies to all object types, while per-object masks apply only to the specified type. + + + + +Navigate to and select a camera. Use the mask editor to draw object filter masks directly on the camera feed. Global object masks and per-object masks can both be configured from this view. + + + + +```yaml +objects: + # Global mask applied to all object types + mask: + mask1: + friendly_name: "Object filter mask area" + enabled: true + coordinates: "0.000,0.000,0.781,0.000,0.781,0.278,0.000,0.278" + # Per-object mask + filters: + person: + mask: + mask1: + friendly_name: "Person filter mask" + enabled: true + coordinates: "0.000,0.000,0.781,0.000,0.781,0.278,0.000,0.278" +``` + + + + +:::note + +The global mask is combined with any object-specific mask. Both are checked based on the bottom center of the bounding box. + +::: + +## Custom Models + +Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use your own models with volume mounts: + +- CPU Model: `/cpu_model.tflite` +- EdgeTPU Model: `/edgetpu_model.tflite` +- Labels: `/labelmap.txt` + +You also need to update the [model config](advanced/system.md#model) if they differ from the defaults. diff --git a/docs/docs/configuration/profiles.md b/docs/docs/configuration/profiles.md new file mode 100644 index 0000000..3eab2bd --- /dev/null +++ b/docs/docs/configuration/profiles.md @@ -0,0 +1,243 @@ +--- +id: profiles +title: Profiles +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Profiles allow you to define named sets of camera configuration overrides that can be activated and deactivated at runtime without restarting Frigate. This is useful for scenarios like switching between "Home" and "Away" modes, daytime and nighttime configurations, or any situation where you want to quickly change how multiple cameras behave. + +## How Profiles Work + +Profiles operate as a two-level system: + +1. **Profile definitions** are declared at the top level of your config under `profiles`. Each definition has a machine name (the key) and a `friendly_name` for display in the UI. +2. **Camera profile overrides** are declared under each camera's `profiles` section, keyed by the profile name. Only the settings you want to change need to be specified. Everything else is inherited from the camera's base configuration. + +When a profile is activated, Frigate merges each camera's profile overrides on top of its base config. When the profile is deactivated, all cameras revert to their original settings. Only one profile can be active at a time. + +:::info + +Profile changes are applied in-memory and take effect immediately. No restart is required. The active profile is persisted across Frigate restarts (stored in the `/config/.profiles` file). + +::: + +## Configuration + +The easiest way to define profiles is to use the Frigate UI. Profiles can also be configured manually in your configuration file. + +### Creating and Managing Profiles + + + + +1. **Create a profile**: Navigate to . Click the **Add Profile** button, enter a name (and optionally a profile ID). +2. **Configure overrides**: Navigate to a camera configuration section (e.g. Motion detection, Record, Notifications). In the top right, two buttons will appear - choose a camera and a profile from the profile selector to edit overrides for that camera and section. Only the fields you change will be stored as overrides. Fields that require a restart are hidden since profiles are applied at runtime. You can click the **Remove Profile Override** button to clear overrides. +3. **Activate a profile**: Use the **Profiles** option in Frigate's main menu to choose a profile. Alternatively, in Settings, navigate to , then choose a profile in the Active Profile dropdown to activate it. The active profile is also shown in the status bar at the bottom of the screen on desktop browsers. +4. **Delete a profile**: Navigate to , then click the trash icon for a profile. This removes the profile definition and all camera overrides associated with it. + + + + +First, define your profiles at the top level of your Frigate config. Every profile name referenced by a camera must be defined here. + +```yaml +profiles: + home: + friendly_name: Home + away: + friendly_name: Away + night: + friendly_name: Night Mode +``` + +Under each camera, add a `profiles` section with overrides for each profile. You only need to include the settings you want to change. + +```yaml +cameras: + front_door: + ffmpeg: + inputs: + - path: rtsp://camera:554/stream + roles: + - detect + - record + detect: + enabled: true + record: + enabled: true + profiles: + away: + detect: + enabled: true + notifications: + enabled: true + objects: + track: + - person + - car + - package + review: + alerts: + labels: + - person + - car + - package + home: + detect: + enabled: true + notifications: + enabled: false + objects: + track: + - person +``` + + + + +### Supported Override Sections + +The following camera configuration sections can be overridden in a profile: + +| Section | Description | +| ------------------ | ----------------------------------------- | +| `enabled` | Enable or disable the camera entirely | +| `audio` | Audio detection settings | +| `birdseye` | Birdseye view settings | +| `detect` | Object detection settings | +| `face_recognition` | Face recognition settings | +| `lpr` | License plate recognition settings | +| `motion` | Motion detection settings | +| `notifications` | Notification settings | +| `objects` | Object tracking and filter settings | +| `record` | Recording settings | +| `review` | Review alert and detection settings | +| `snapshots` | Snapshot settings | +| `zones` | Zone definitions (merged with base zones) | + +:::note + +Only the fields you explicitly set in a profile override are applied. All other fields retain their base configuration values. For masks and zones, profile zones **override** the camera's base masks and zones. If configuring profiles via YAML, you should not define masks or zones in profiles that are not defined in the base config. + +::: + +## Activating Profiles + +Profiles can be activated and deactivated via the Frigate UI, [MQTT](/integrations/mqtt#frigateprofileset), or the Home Assistant integration. + +In the Frigate UI, open the Settings cog and select **Profiles** from the submenu to see all defined profiles. From there you can activate any profile or deactivate the current one. The active profile is indicated in the UI so you always know which profile is in effect. + +Activating or deactivating a profile clears any [runtime toggle overrides](/configuration/live#runtime-toggle-persistence) so the profile's settings aren't silently undone by a stale toggle from before the switch. + +## Example: Home / Away Setup + +A common use case is having different detection and notification settings based on whether you are home or away. This example below is for a system with two cameras, `front_door` and `indoor_cam`. + + + + +1. Navigate to and create two profiles: **Home** and **Away**. +2. From to the Camera configuration section in Settings, choose the **front_door** camera, and select the **Away** profile from the profile dropdown. Then, enable notifications from the Notifications pane, and set alert labels to `person` and `car` from the Review pane. Then, from the profile dropdown choose **Home** profile, then navigate to Notifications to disable notifications. +3. For the **indoor_cam** camera, perform similar steps - configure the **Away** profile to enable the camera, detection, and recording. Configure the **Home** profile to disable the camera entirely for privacy. +4. Activate the desired profile from or from the **Profiles** option in Frigate's main menu. + + + + +```yaml +profiles: + home: + friendly_name: Home + away: + friendly_name: Away + +cameras: + front_door: + ffmpeg: + inputs: + - path: rtsp://camera:554/stream + roles: + - detect + - record + detect: + enabled: true + record: + enabled: true + notifications: + enabled: false + profiles: + away: + notifications: + enabled: true + review: + alerts: + labels: + - person + - car + home: + notifications: + enabled: false + + indoor_cam: + ffmpeg: + inputs: + - path: rtsp://camera:554/indoor + roles: + - detect + - record + detect: + enabled: false + record: + enabled: false + profiles: + away: + enabled: true + detect: + enabled: true + record: + enabled: true + home: + enabled: false +``` + + + + +In this example: + +- **Away profile**: The front door camera enables notifications and tracks specific alert labels. The indoor camera is fully enabled with detection and recording. +- **Home profile**: The front door camera disables notifications. The indoor camera is completely disabled for privacy. +- **No profile active**: All cameras use their base configuration values. + +## FAQ + +### Can I define a zone or mask in a profile but not have it in the base config? + +No. Profiles are pure overrides. Every zone and mask defined under a profile must reference an entry that already exists on the base camera config. Configurations that introduce profile-only zones or masks are rejected at startup. + +If you want a zone or mask to be active only under a specific profile, define it on the base config with `enabled: false`, then enable it in that profile's overrides. + +### How do I revert a profile zone or mask override back to the base configuration? + +Delete the override. In the Frigate UI, edit the profile and use the "Revert override" action (the trash can icon) on the zone or mask. The base entry is left untouched, and once the override is removed the profile inherits the base values for that zone or mask. + +### Can multiple profiles be active at the same time? + +No. Only one profile can be active at a time. Activating a new profile automatically deactivates the current one. + +### What happens to my profile overrides if I delete a zone or mask from the base? + +When you delete a base zone or mask in the Frigate UI, any profile overrides for that entry are deleted automatically as part of the same operation. If you remove a base entry by editing your config file directly and leave a profile override behind, the config will fail validation at startup until the orphaned override is removed as well. + +### Why are some settings missing when I configure a profile override? + +Fields that require a Frigate restart to take effect cannot be overridden by profiles, since profiles are applied at runtime without restarting. Those fields are hidden when editing a profile override and can only be changed on the base configuration. + +### Can I schedule profiles to be enabled or disabled at certain times? + +Not within Frigate itself. Frigate is an NVR, not an automation platform, so it intentionally does not include a scheduler for activating profiles. Instead, activate profiles from an automation platform that already handles time- and event-based triggers well, such as [Home Assistant](https://www.home-assistant.io/) or [Node-RED](https://nodered.org/). These integrate with Frigate and give you far more robust and flexible scheduling than a built-in scheduler could. + +If you prefer something lightweight, a simple script driven by a cron job that toggles profiles on a schedule works too. diff --git a/docs/docs/configuration/pwa.md b/docs/docs/configuration/pwa.md new file mode 100644 index 0000000..fbcbdff --- /dev/null +++ b/docs/docs/configuration/pwa.md @@ -0,0 +1,28 @@ +--- +id: pwa +title: Installing Frigate App +--- + +Frigate supports being installed as a [Progressive Web App](https://web.dev/explore/progressive-web-apps) on Desktop, Android, and iOS. + +This adds features including the ability to deep link directly into the app. + +## Requirements + +In order to install Frigate as a PWA, the following requirements must be met: + +- Frigate must be accessed via a secure context (localhost, secure https, VPN, etc.) +- On Android, Firefox, Chrome, Edge, Opera, and Samsung Internet Browser all support installing PWAs. +- On iOS 16.4 and later, PWAs can be installed from the Share menu in Safari, Chrome, Edge, Firefox, and Orion. + +## Installation + +Installation varies slightly based on the device that is being used: + +- Desktop: Use the install button typically found in right edge of the address bar +- Android: Use the `Install as App` button in the more options menu for Chrome, and the `Add app to Home screen` button for Firefox +- iOS: Use the `Add to Homescreen` button in the share menu + +## Usage + +Once setup, the Frigate app can be used wherever it has access to Frigate. This means it can be setup as local-only, VPN-only, or fully accessible depending on your needs. diff --git a/docs/docs/configuration/record.md b/docs/docs/configuration/record.md new file mode 100644 index 0000000..87a24d3 --- /dev/null +++ b/docs/docs/configuration/record.md @@ -0,0 +1,413 @@ +--- +id: record +title: Recording +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM-DD/HH//MM.SS.mp4` in **UTC time**. These recordings are written directly from your camera stream without re-encoding. Each camera supports a configurable retention policy. Frigate chooses the largest matching retention value between the recording retention and the tracked object retention when determining if a recording should be removed. + +New recording segments are written from the camera stream to cache, they are only moved to disk if they match the setup recording retention policy. + +:::tip + +To keep a specific clip beyond your retention window, [export](/usage/exports) it rather than increasing retention for the whole camera. Exports are saved separately and are never removed by retention. + +::: + +H265 recordings can be viewed in Chrome 108+, Edge and Safari only. All other browsers require recordings to be encoded with H264. + +## Common recording configurations + +### Most conservative: Ensure all video is saved + +For users deploying Frigate in environments where it is important to have contiguous video stored even if there was no detectable motion, the following configuration will store all video for 3 days. After 3 days, only video containing motion will be saved for 7 days. After 7 days, only video containing motion and overlapping with alerts or detections will be retained until 30 days have passed. + + + + +Navigate to . + +- Set **Enable recording** to on +- Set **Continuous retention > Retention days** to `3` +- Set **Motion retention > Retention days** to `7` +- Set **Alert retention > Event retention > Retention days** to `30` +- Set **Alert retention > Event retention > Retention mode** to `all` +- Set **Detection retention > Event retention > Retention days** to `30` +- Set **Detection retention > Event retention > Retention mode** to `all` + + + + +```yaml +record: + enabled: True + continuous: + days: 3 + motion: + days: 7 + alerts: + retain: + days: 30 + mode: all + detections: + retain: + days: 30 + mode: all +``` + + + + +### Reduced storage: Only saving video when motion is detected + +To reduce storage requirements, configure recording to only retain video where motion or activity was detected. + + + + +Navigate to . + +- Set **Enable recording** to on +- Set **Motion retention > Retention days** to `3` +- Set **Alert retention > Event retention > Retention days** to `30` +- Set **Alert retention > Event retention > Retention mode** to `motion` +- Set **Detection retention > Event retention > Retention days** to `30` +- Set **Detection retention > Event retention > Retention mode** to `motion` + + + + +```yaml +record: + enabled: True + motion: + days: 3 + alerts: + retain: + days: 30 + mode: motion + detections: + retain: + days: 30 + mode: motion +``` + + + + +### Minimum: Alerts only + +If you only want to retain video that occurs during activity caused by tracked object(s), this configuration will discard video unless an alert is ongoing. + + + + +Navigate to . + +- Set **Enable recording** to on +- Set **Continuous retention > Retention days** to `0` +- Set **Alert retention > Event retention > Retention days** to `30` +- Set **Alert retention > Event retention > Retention mode** to `motion` + + + + +```yaml +record: + enabled: True + continuous: + days: 0 + alerts: + retain: + days: 30 + mode: motion +``` + + + + +## Pre-capture and Post-capture + +The `pre_capture` and `post_capture` settings control how many seconds of video are included before and after an alert or detection. These can be configured independently for alerts and detections, and can be set globally or overridden per camera. + + + + +Navigate to for global defaults, or to override for a specific camera. + +| Field | Description | +| ---------------------------------------------- | ---------------------------------------------------- | +| **Alert retention > Pre-capture seconds** | Seconds of video to include before an alert event | +| **Alert retention > Post-capture seconds** | Seconds of video to include after an alert event | +| **Detection retention > Pre-capture seconds** | Seconds of video to include before a detection event | +| **Detection retention > Post-capture seconds** | Seconds of video to include after a detection event | + + + + +```yaml +record: + enabled: True + alerts: + pre_capture: 5 # seconds before the alert to include + post_capture: 5 # seconds after the alert to include + detections: + pre_capture: 5 # seconds before the detection to include + post_capture: 5 # seconds after the detection to include +``` + + + + +- **Default**: 5 seconds for both pre and post capture. +- **Pre-capture maximum**: 60 seconds. +- These settings apply per review category (alerts and detections), not per object type. + +### How pre/post capture interacts with retention mode + +The `pre_capture` and `post_capture` values define the **time window** around a review item, but only recording segments that also match the configured **retention mode** are actually kept on disk. + +- **`mode: all`**: Retains every segment within the capture window, regardless of whether motion was detected. +- **`mode: motion`** (default): Only retains segments within the capture window that contain motion. This includes segments with active tracked objects, since object motion implies motion. Segments without any motion are discarded even if they fall within the pre/post capture range. +- **`mode: active_objects`**: Only retains segments within the capture window where tracked objects were actively moving. Segments with general motion but no active objects are discarded. + +This means that with the default `motion` mode, you may see less footage than the configured pre/post capture duration if parts of the capture window had no motion. + +To guarantee the full pre/post capture duration is always retained: + +```yaml +record: + enabled: True + alerts: + pre_capture: 10 + post_capture: 10 + retain: + days: 30 + mode: all # retains all segments within the capture window +``` + +:::note + +Because recording segments are written in 10 second chunks, pre-capture timing depends on segment boundaries. The actual pre-capture footage may be slightly shorter or longer than the exact configured value. + +::: + +### Where to view pre/post capture footage + +Pre and post capture footage is included in the **recording timeline**, visible in the History view. Note that pre/post capture settings only affect which recording segments are **retained on disk**. They do not change the start and end points shown in the UI. The History view will still center on the review item's actual time range, but you can scrub backward and forward through the retained pre/post capture footage on the timeline. The Explore view shows object-specific clips that are trimmed to when the tracked object was actually visible, so pre/post capture time will not be reflected there. + +## Configuring Recording Retention + +Frigate supports both continuous and tracked object based recordings with separate retention modes and retention periods. + +:::tip + +Retention configs support decimals meaning they can be configured to retain `0.5` days, for example. + +::: + +### Continuous and Motion Recording + +The number of days to retain continuous and motion recordings can be configured. By default, continuous recording is disabled. + + + + +Navigate to . + +| Field | Description | +| ----------------------------------------- | -------------------------------------------- | +| **Enable recording** | Enable or disable recording for all cameras | +| **Continuous retention > Retention days** | Number of days to keep continuous recordings | +| **Motion retention > Retention days** | Number of days to keep motion recordings | + + + + +```yaml +record: + enabled: True + continuous: + days: 1 # <- number of days to keep continuous recordings + motion: + days: 2 # <- number of days to keep motion recordings +``` + + + + +Continuous recording supports different retention modes [which are described below](#configuring-recording-retention). + +### Object Recording + +The number of days to retain recordings for review items can be specified for items classified as alerts as well as tracked objects. + + + + +Navigate to . + +| Field | Description | +| ---------------------------------------------------------- | ------------------------------------------- | +| **Enable recording** | Enable or disable recording for all cameras | +| **Alert retention > Event retention > Retention days** | Number of days to keep alert recordings | +| **Detection retention > Event retention > Retention days** | Number of days to keep detection recordings | + + + + +```yaml +record: + enabled: True + alerts: + retain: + days: 10 # <- number of days to keep alert recordings + detections: + retain: + days: 10 # <- number of days to keep detections recordings +``` + + + + +This configuration will retain recording segments that overlap with alerts and detections for 10 days. Because multiple tracked objects can reference the same recording segments, this avoids storing duplicate footage for overlapping tracked objects and reduces overall storage needs. + +## Can I have "continuous" recordings, but only at certain times? + +Using Frigate UI, Home Assistant, or MQTT, cameras can be automated to only record in certain situations or at certain times. + +## How do I export recordings? + +Footage can be exported from Frigate by right-clicking (desktop) or long pressing (mobile) on a review item in the Review pane or by clicking the Export button in the History view. Exported footage is then organized and searchable through the Export view, accessible from the main navigation bar. + +### Custom export with FFmpeg arguments + +For advanced use cases, the [custom export HTTP API](../integrations/api/export-recording-custom-export-custom-camera-name-start-start-time-end-end-time-post.api.mdx) lets you pass custom FFmpeg arguments when exporting a recording: + +``` +POST /export/custom/{camera_name}/start/{start_time}/end/{end_time} +``` + +The request body accepts `ffmpeg_input_args` and `ffmpeg_output_args` to control encoding, frame rate, filters, and other FFmpeg options. If neither is provided, Frigate defaults to time-lapse output settings (25x speed, 30 FPS). + +The following example exports a time-lapse at 60x speed with 25 FPS: + +```json +{ + "name": "Front Door Time-lapse", + "ffmpeg_output_args": "-vf setpts=PTS/60 -r 25" +} +``` + +#### CPU fallback + +If hardware acceleration is configured and the export fails (e.g., the GPU is unavailable), set `cpu_fallback: true` in the request body to automatically retry using software encoding. + +```json +{ + "name": "My Export", + "ffmpeg_output_args": "-c:v libx264 -crf 23", + "cpu_fallback": true +} +``` + +:::note + +Non-admin users are restricted from using FFmpeg arguments that can access the filesystem (e.g., `-filter_complex`, file paths, and protocol references). Admin users have full control over FFmpeg arguments. + +::: + +:::tip + +When `hwaccel_args` is configured, hardware encoding is used for exports. This can be overridden per camera (e.g., when camera resolution exceeds hardware encoder limits) by setting a camera-level `hwaccel_args`. Using an unrecognized value or empty string falls back to software encoding (libx264). + +::: + +:::tip + +To reduce output file size, add the FFmpeg parameter `-qp n` to `ffmpeg_output_args` (where `n` is the quantization parameter). Adjust the value to balance quality and file size for your scenario. + +::: + +## Apple Compatibility with H.265 Streams + +Apple devices running the Safari browser may fail to playback h.265 recordings. The [apple compatibility option](../configuration/camera_specific.md#h265-cameras-via-safari) should be used to ensure seamless playback on Apple devices. + +## Syncing Media Files With Disk + +Media files (event snapshots, event thumbnails, review thumbnails, previews, exports, and recordings) can become orphaned when database entries are deleted but the corresponding files remain on disk. + +Normal operation may leave small numbers of orphaned files until Frigate's scheduled cleanup, but crashes, configuration changes, or upgrades may cause more orphaned files that Frigate does not clean up. This feature checks the file system for media files and removes any that are not referenced in the database. + +The Maintenance pane in the Frigate UI or an API endpoint `POST /api/media/sync` can be used to trigger a media sync. When using the API, a job ID is returned and the operation continues on the server. Status can be checked with the `/api/media/sync/status/{job_id}` endpoint. + +Setting `verbose: true` writes a detailed report of every orphaned file and database entry to `/config/media_sync/.txt`. For recordings, the report separates orphaned database entries (DB records whose files are missing from disk) from orphaned files (files on disk with no corresponding database record). + +:::warning + +This operation uses considerable CPU resources and includes a safety threshold that aborts if more than 50% of files would be deleted. Only run when necessary. If you set `force: true` the safety threshold will be bypassed; do not use `force` unless you are certain the deletions are intended. + +::: + +## Understanding storage usage + +The storage usage Frigate reports will not exactly match what the operating system reports with `df` or `du`. This is expected, not a bug. The sections below explain how Frigate derives its storage figures and why they differ from the disk's own accounting. + +### How Frigate measures recording usage + +The **Recordings** value on the Storage Metrics page (), and the per-camera **Camera Storage** breakdown, is the sum of the recording segment sizes Frigate has written, taken from Frigate's database. It is **not** computed by a scan of the disk. Frigate tracks usage this way by design: repeatedly walking the entire drive to total its size would keep hard drives spun up and add unnecessary I/O. + +The disk **total** shown beside it, and the free-space figure Frigate uses to decide when to delete recordings, instead come from the operating system's report for the whole filesystem mounted at `/media/frigate`. As a result, the **Unused** value on the page is _total disk capacity minus Frigate's recordings_, not the drive's real free space, which will be lower whenever anything else is stored on the disk. + +### What counts toward usage, and why it won't match `df` + +Only **recording segments** (`/media/frigate/recordings`) are included in the recordings storage total. Plenty of other things consume real disk space but are **not** part of that number: + +- **Snapshots and thumbnails** (`/media/frigate/clips`): see [Snapshots](/configuration/snapshots). These are retained independently of recordings. +- **Preview videos** and **review thumbnails** (also under `/media/frigate/clips`). +- **Exports** (`/media/frigate/exports`): exports are never removed by retention. +- **The database, downloaded detection models, and face / license plate training images** (stored under `/config`). +- **Debug images from enrichments** (`/media/frigate/clips`): when enabled, License Plate Recognition's `debug_save_plates` and GenAI's `debug_save_thumbnails` save plate crops and request images for troubleshooting. + +These files are the usual explanation for an "other" or seemingly unaccounted bucket of space: it is real, it is Frigate's, and it simply isn't part of the _recordings_ total. They are also why comparing the **Recordings** figure to `df -h` always shows a gap: `df` additionally counts any non-Frigate data on the disk, filesystem overhead and reserved blocks (ext4 reserves ~5% for root by default, so a disk can read "full" before recordings approach the total), and recently deleted recordings whose space has not yet been reclaimed. + +:::tip + +The Storage page is not intended to be a system-wide disk monitor: it shows how much space _Frigate's recordings_ use. To see true disk usage, use `df -h` (free space) and `du -sh` (per-directory usage) on the host. + +::: + +### Free space and the `/media/frigate` mount + +Frigate reports the capacity and free space of whatever filesystem is actually mounted at `/media/frigate` **inside the container**. If an external drive or network share isn't truly mounted there (a missing `/etc/fstab` entry, a share that was offline when the container started, or a host that doesn't pass the path through), the container falls back to the host's OS disk, and Frigate will correctly report that smaller disk instead of the drive you intended. + +If the reported capacity doesn't match your drive, the mount is the place to look, not Frigate. Verify what is actually mounted from inside the container: + +```bash +docker exec -it frigate df -h /media/frigate +docker exec -it frigate mount | grep media +``` + +See the [storage mount layout](/frigate/installation#storage) for how the volumes are expected to be configured. + +### The `/tmp/cache` area is separate + +Recording segments are first written to `/tmp/cache`, a small, in-memory (`tmpfs`) area, before being checked and moved to `/media/frigate/recordings`. Because it is separate and small, `/tmp/cache` can fill up and produce `No space left on device` errors even when the recordings disk has plenty of room. They are different storage areas. See [Recordings troubleshooting](/troubleshooting/recordings) for diagnosing cache and slow-storage issues. + +### When the metrics don't match what's on disk + +Because usage is tracked in the database, deleting recording files directly on disk, or files left behind after an upgrade, will not update the reported usage, and can even push it above 100%. Frigate is unaware of files it didn't record and won't count or remove them automatically. Use [Syncing Media Files With Disk](#syncing-media-files-with-disk) to reconcile the database with what is actually on disk. + +## Will Frigate delete old recordings if my storage runs out? + +Yes. Frigate continuously checks the **free space of the disk** holding `/media/frigate/recordings`. This is different from adding up the size of every recording: free space is a single number the operating system already tracks, so Frigate can ask for it instantly without reading through your files or spinning up the disk, which is exactly why it relies on this check rather than scanning the drive. When less than roughly one hour of recording space remains (estimated from the current recording bitrate, **not** a fixed percentage), Frigate deletes the oldest recordings to reclaim space and logs a message. This emergency cleanup removes the oldest recordings first **regardless of retention settings**. + +Two consequences follow from this being based on whole-disk free space: + +- Because the check uses the disk's real free space, **anything** filling the drive, including non-Frigate files, can trigger deletion of your oldest recordings. +- Cleanup can run while a meaningful percentage of the disk is still free (for example, with high bitrates or many cameras), because the threshold is "less than ~1 hour of recording headroom," not "X% full." + +Frequent emergency cleanups usually mean your configured retention exceeds what the disk can hold. Reduce your retention days so the normal retention cleanup keeps up and the emergency path rarely triggers. diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md new file mode 100644 index 0000000..1ae96b6 --- /dev/null +++ b/docs/docs/configuration/restream.md @@ -0,0 +1,254 @@ +--- +id: restream +title: Restream +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +## RTSP + +Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://:8554/`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate. + +Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.14) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#configuration) for more advanced configurations and features. + +:::note + +You can access the go2rtc stream info at `/api/go2rtc/streams` which can be helpful to debug as well as provide useful information about your camera streams. + +::: + +### Birdseye Restream + +Birdseye RTSP restream can be accessed at `rtsp://:8554/birdseye`. Enabling the birdseye restream will cause birdseye to run 24/7 which may increase CPU usage somewhat. + +```yaml +birdseye: + restream: True +``` + +:::tip + +To improve connection speed when using Birdseye via restream you can enable a small idle heartbeat by setting `birdseye.idle_heartbeat_fps` to a low value (e.g. `1–2`). This makes Frigate periodically push the last frame even when no motion is detected, reducing initial connection latency. + +::: + +### Securing Restream With Authentication + +The go2rtc restream can be secured with RTSP based username / password authentication. Ex: + +```yaml {2-4} +go2rtc: + rtsp: + username: "admin" + password: "pass" + streams: ... +``` + +**NOTE:** This does not apply to localhost requests, there is no need to provide credentials when using the restream as a source for frigate cameras. + +## Reduce Connections To Camera + +Some cameras only support one active connection or you may just want to have a single connection open to the camera. The RTSP restream allows this to be possible. + +### With Single Stream + +One connection is made to the camera. One for the restream, `detect` and `record` connect to the restream. + +Configure the go2rtc stream and point the camera inputs at the local restream. + + + + +Navigate to and add stream entries for each camera. Then navigate to for each camera. For each input, choose **Restream (go2rtc)** and pick the matching stream from the dropdown. Frigate uses the local restream URL (`rtsp://127.0.0.1:8554/`) and the `preset-rtsp-restream` input args for that input automatically. (Choose **Manual input path** instead to type a URL directly.) + + + + +```yaml +go2rtc: + streams: + name_your_rtsp_cam: # <- for RTSP streams + - rtsp://192.168.1.5:554/live0 # <- stream which supports video & aac audio + - "ffmpeg:name_your_rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus) + name_your_http_cam: # <- for other streams + - http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=user&password=password # <- stream which supports video & aac audio + - "ffmpeg:name_your_http_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus) + +cameras: + name_your_rtsp_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/name_your_rtsp_cam # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - detect + - audio # <- only necessary if audio detection is enabled + name_your_http_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/name_your_http_cam # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - detect + - audio # <- only necessary if audio detection is enabled +``` + + + + +### With Sub Stream + +Two connections are made to the camera. One for the sub stream, one for the restream, `record` connects to the restream. + + + + +Navigate to and add stream entries for each camera and its sub stream. Then navigate to for each camera and add separate inputs for the main and sub streams. Set each input's source to **Restream (go2rtc)** and pick the matching stream from the dropdown. Frigate uses the local restream URL and the `preset-rtsp-restream` input args for that input automatically. + + + + +```yaml +go2rtc: + streams: + name_your_rtsp_cam: + - rtsp://192.168.1.5:554/live0 # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg + - "ffmpeg:name_your_rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to opus + name_your_rtsp_cam_sub: + - rtsp://192.168.1.5:554/substream # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg + - "ffmpeg:name_your_rtsp_cam_sub#audio=opus" # <- copy of the stream which transcodes audio to opus + name_your_http_cam: + - http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=user&password=password # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg + - "ffmpeg:name_your_http_cam#audio=opus" # <- copy of the stream which transcodes audio to opus + name_your_http_cam_sub: + - http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=user&password=password # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg + - "ffmpeg:name_your_http_cam_sub#audio=opus" # <- copy of the stream which transcodes audio to opus + +cameras: + name_your_rtsp_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/name_your_rtsp_cam # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/name_your_rtsp_cam_sub # <--- the name here must match the name of the camera_sub in restream + input_args: preset-rtsp-restream + roles: + - audio # <- only necessary if audio detection is enabled + - detect + name_your_http_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/name_your_http_cam # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/name_your_http_cam_sub # <--- the name here must match the name of the camera_sub in restream + input_args: preset-rtsp-restream + roles: + - audio # <- only necessary if audio detection is enabled + - detect +``` + + + + +## Handling Complex Passwords + +go2rtc expects URL-encoded passwords in the config, [urlencoder.org](https://urlencoder.org) can be used for this purpose. + +For example: + +```yaml +go2rtc: + streams: + # highlight-error-line + my_camera: rtsp://username:$@foo%@192.168.1.100 +``` + +becomes + +```yaml +go2rtc: + streams: + # highlight-next-line + my_camera: rtsp://username:$%40foo%25@192.168.1.100 +``` + +See [this comment](https://github.com/AlexxIT/go2rtc/issues/1217#issuecomment-2242296489) for more information. + +## Preventing go2rtc from blocking two-way audio {#two-way-talk-restream} + +For cameras that support two-way talk, go2rtc will automatically establish an audio output backchannel when connecting to an RTSP stream. This backchannel blocks access to the camera's audio output for two-way talk functionality, preventing both Frigate and other applications from using it. + +To prevent this, you must configure two separate stream instances: + +1. One stream instance with `#backchannel=0` for Frigate's viewing, recording, and detection (prevents go2rtc from establishing the blocking backchannel) +2. A second stream instance without `#backchannel=0` for two-way talk functionality (can be used by Frigate's WebRTC viewer or other applications) + +Configuration example: + +```yaml +go2rtc: + streams: + front_door: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#backchannel=0 + front_door_twoway: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 +``` + +In this configuration: + +- `front_door` stream is used by Frigate for viewing, recording, and detection. The `#backchannel=0` parameter prevents go2rtc from establishing the audio output backchannel, so it won't block two-way talk access. +- `front_door_twoway` stream is used for two-way talk functionality. This stream can be used by Frigate's WebRTC viewer when two-way talk is enabled, or by other applications (like Home Assistant Advanced Camera Card) that need access to the camera's audio output channel. + +## Security: Restricted Stream Sources + +For security reasons, the `echo:`, `expr:`, and `exec:` stream sources are disabled by default in go2rtc. These sources allow arbitrary command execution and can pose security risks if misconfigured. + +If you attempt to use these sources in your configuration, the streams will be removed and an error message will be printed in the logs. + +To enable these sources, you must set the environment variable `GO2RTC_ALLOW_ARBITRARY_EXEC=true`. This can be done in your Docker Compose file or container environment: + +```yaml +environment: + - GO2RTC_ALLOW_ARBITRARY_EXEC=true +``` + +:::warning + +Enabling arbitrary exec sources allows execution of arbitrary commands through go2rtc stream configurations. Only enable this if you understand the security implications and trust all sources of your configuration. + +::: + +## Advanced Restream Configurations + +The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-exec) source in go2rtc can be used for custom ffmpeg commands and other applications. An example is below: + +:::warning + +The `exec:`, `echo:`, and `expr:` sources are disabled by default for security. You must set `GO2RTC_ALLOW_ARBITRARY_EXEC=true` to use them. See [Security: Restricted Stream Sources](#security-restricted-stream-sources) for more information. + +::: + +NOTE: RTSP output will need to be passed with two curly braces `{{output}}`, whereas pipe output must be passed without curly braces. + +```yaml +go2rtc: + streams: + stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {{output}} + stream2: exec:rpicam-vid -t 0 --libav-format h264 -o - +``` diff --git a/docs/docs/configuration/review.md b/docs/docs/configuration/review.md new file mode 100644 index 0000000..e9ba4df --- /dev/null +++ b/docs/docs/configuration/review.md @@ -0,0 +1,136 @@ +--- +id: review +title: Review +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +The Review page of the Frigate UI is for quickly reviewing historical footage of interest from your cameras. _Review items_ are indicated on a vertical timeline and displayed as a grid of previews - bandwidth-optimized, low frame rate, low resolution videos. Hovering over or swiping a preview plays the video and marks it as reviewed. If more in-depth analysis is required, the preview can be clicked/tapped and the full frame rate, full resolution recording is displayed. + +Review items are filterable by date, object type, and camera. + +### Review items vs. tracked objects (formerly "events") + +In Frigate 0.13 and earlier versions, the UI presented "events". An event was synonymous with a tracked or detected object. In Frigate 0.14 and later, a review item is a time period where any number of tracked objects were active. + +For example, consider a situation where two people walked past your house. One was walking a dog. At the same time, a car drove by on the street behind them. + +In this scenario, Frigate 0.13 and earlier would show 4 "events" in the UI - one for each person, another for the dog, and yet another for the car. You would have had 4 separate videos to watch even though they would have all overlapped. + +In 0.14 and later, all of that is bundled into a single review item which starts and ends to capture all of that activity. Reviews for a single camera cannot overlap. Once you have watched that time period on that camera, it is marked as reviewed. + +## Alerts and Detections + +Not every segment of video captured by Frigate may be of the same level of interest to you. Video of people who enter your property may be a different priority than those walking by on the sidewalk. For this reason, Frigate categorizes review items as _alerts_ and _detections_. By default, all person and car objects are considered alerts. You can refine categorization of your review items by configuring [required zones](/configuration/zones#restricting-alerts-and-detections-to-specific-zones) for them. + +:::note + +Alerts and detections categorize the tracked objects in review items, but Frigate must first detect those objects with your configured object detector (Coral, OpenVINO, etc). By default, the object tracker only detects `person`. Setting `labels` for `alerts` and `detections` does not automatically enable detection of new objects. To detect more than `person`, you should add more labels via or and select your camera. Alternatively, add the following to your config: + +```yaml +objects: + track: + - person + - car + - ... +``` + +See the [objects documentation](objects.md) for the list of objects that Frigate's default model tracks. +::: + +## Restricting alerts to specific labels + +By default a review item will only be marked as an alert if a person or car is detected. Configure the alert labels to include any object or audio label. + + + + +Navigate to or and select your camera. + +Expand **Alerts config** and configure which labels and zones should generate alerts. + + + + +```yaml +# can be overridden at the camera level +review: + alerts: + labels: + - car + - cat + - dog + - person + - speech +``` + + + + +## Restricting detections to specific labels + +By default all detections that do not qualify as an alert qualify as a detection. However, detections can further be filtered to only include certain labels or certain zones. + + + + +Navigate to or and select your camera. + +Expand **Detections config** and configure which labels should qualify as detections. + + + + +```yaml +# can be overridden at the camera level +review: + detections: + labels: + - bark + - dog +``` + + + + +## Excluding a camera from alerts or detections + +To exclude a specific camera from alerts or detections, provide an empty list to the alerts or detections labels field at the camera level. + +For example, to exclude objects on the camera _gatecamera_ from any detections: + + + + +1. Navigate to and select the **gatecamera** camera. + - Expand **Detections config** and turn off all of the object label switches. + + + + +```yaml {3-5} +cameras: + gatecamera: + review: + detections: + labels: [] +``` + + + + +## Restricting review items to specific zones + +By default a review item will be created if any `review -> alerts -> labels` and `review -> detections -> labels` are detected anywhere in the camera frame. You will likely want to configure review items to only be created when the object enters an area of interest, [see the zone docs for more information](./zones.md#restricting-alerts-and-detections-to-specific-zones) + +:::info + +Because zones don't apply to audio, audio labels will always be marked as a detection by default. + +::: + +## Reviewing Motion + +The Review page can also surface periods of motion that didn't produce a tracked object, and lets you search past recordings for motion in a region you draw. See [Reviewing Motion](/usage/review#reviewing-motion) in the Usage docs for how to use **Motion Previews** and **Motion Search**, and [Tuning Motion Detection](motion_detection.md) for configuring the underlying motion detector. diff --git a/docs/docs/configuration/semantic_search.md b/docs/docs/configuration/semantic_search.md new file mode 100644 index 0000000..c58b634 --- /dev/null +++ b/docs/docs/configuration/semantic_search.md @@ -0,0 +1,282 @@ +--- +id: semantic_search +title: Semantic Search +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one. This feature works by creating _embeddings_, numerical vector representations, for both the images and text descriptions of your tracked objects. By comparing these embeddings, Frigate assesses their similarities to deliver relevant search results. + +Frigate uses models from [Jina AI](https://huggingface.co/jinaai) to create and save embeddings to Frigate's database. All of this runs locally. + +Semantic Search is accessed via the _Explore_ view in the Frigate UI. + +:::info + +Semantic search requires a one-time internet connection to download embedding models from HuggingFace. Once cached, models work fully offline. See [Network Requirements](/frigate/network_requirements#one-time-model-downloads) for details. + +::: + +## Minimum System Requirements + +Semantic Search works by running a large AI model locally on your system. Small or underpowered systems like a Raspberry Pi will not run Semantic Search reliably or at all. + +A minimum of 8GB of RAM is required to use Semantic Search. A CPU with AVX + AVX2 instructions is required to run Semantic Search. A GPU is not strictly required but will provide a significant performance increase over CPU-only systems. + +For best performance, 16GB or more of RAM and a dedicated GPU are recommended. + +## Configuration + +Semantic Search is disabled by default and must be enabled before it can be used. Semantic Search is a global configuration setting. + + + + +Navigate to . + +- Set **Enable semantic search** to on + + + + +```yaml +semantic_search: + enabled: True + reindex: False +``` + + + + +:::tip + +The embeddings database can be re-indexed from the existing tracked objects in your database by pressing the "Reindex" button in the Enrichments Settings in the UI or by adding `reindex: True` to your `semantic_search` configuration and restarting Frigate. Depending on the number of tracked objects you have, it can take a long while to complete and may max out your CPU while indexing. + +If you are enabling Semantic Search for the first time, be advised that Frigate does not automatically index older tracked objects. You will need to reindex as described above. + +::: + +### Jina AI CLIP (version 1) + +The [V1 model from Jina](https://huggingface.co/jinaai/jina-clip-v1) has a vision model which is able to embed both images and text into the same vector space, which allows `image -> image` and `text -> image` similarity searches. Frigate uses this model on tracked objects to encode the thumbnail image and store it in the database. When searching for tracked objects via text in the search box, Frigate will perform a `text -> image` similarity search against this embedding. When clicking "Find Similar" in the tracked object detail pane, Frigate will perform an `image -> image` similarity search to retrieve the closest matching thumbnails. + +The V1 text model is used to embed tracked object descriptions and perform searches against them. Descriptions can be created, viewed, and modified on the Explore page when clicking on thumbnail of a tracked object. See [the object description docs](/configuration/genai/objects.md) for more information on how to automatically generate tracked object descriptions. + +Differently weighted versions of the Jina models are available and can be selected by setting the model size. + + + + +Navigate to . + +| Field | Description | +| ------------------------------------------------ | -------------------------------------------------------------------------- | +| **Semantic search model or GenAI provider name** | Select `jinav1` to use the Jina AI CLIP V1 model | +| **Model size** | `small` (quantized, CPU-friendly) or `large` (full model, GPU-accelerated) | + + + + +```yaml +semantic_search: + enabled: True + model: "jinav1" + model_size: small +``` + + + + +- Configuring the `large` model employs the full Jina model and will automatically run on the GPU if applicable. +- Configuring the `small` model employs a quantized version of the Jina model that uses less RAM and runs on CPU with a very negligible difference in embedding quality. + +### Jina AI CLIP (version 2) + +Frigate also supports the [V2 model from Jina](https://huggingface.co/jinaai/jina-clip-v2), which introduces multilingual support (89 languages). In contrast, the V1 model only supports English. + +V2 offers only a 3% performance improvement over V1 in both text-image and text-text retrieval tasks, an upgrade that is unlikely to yield noticeable real-world benefits. Additionally, V2 has _significantly_ higher RAM and GPU requirements, leading to increased inference time and memory usage. If you plan to use V2, ensure your system has ample RAM and a discrete GPU. CPU inference (with the `small` model) using V2 is not recommended. + +To use the V2 model, set the model to `jinav2`. + + + + +Navigate to . + +| Field | Description | +| ------------------------------------------------ | ----------------------------------------------------- | +| **Semantic search model or GenAI provider name** | Select `jinav2` to use the Jina AI CLIP V2 model | +| **Model size** | `large` is recommended for V2 (requires discrete GPU) | + + + + +```yaml +semantic_search: + enabled: True + model: "jinav2" + model_size: large +``` + + + + +For most users, especially native English speakers, the V1 model remains the recommended choice. + +:::note + +Switching between V1 and V2 requires reindexing your embeddings. The embeddings from V1 and V2 are incompatible, and failing to reindex will result in incorrect search results. + +::: + +### 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 with `embeddings` in its `roles`. +2. Set the semantic search model to the GenAI config key (e.g. `default`). +3. Start the llama.cpp server with `--embeddings` and `--mmproj` for image support. + + + + +Navigate to . + +| Field | Description | +| ------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| **Semantic search model or GenAI provider name** | Set to the GenAI config key (e.g. `default`) to use a configured GenAI provider for embeddings | + +The GenAI provider must also be configured with the `embeddings` role under . + + + + +```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 + +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. + + + + +Navigate to . + +| Field | Description | +| -------------- | ---------------------------------------------------------------------- | +| **Model size** | Set to `large` to enable GPU acceleration | +| **Device** | (Optional) Specify a GPU device index in a multi-GPU system (e.g. `0`) | + + + + +```yaml +semantic_search: + enabled: True + model_size: large + # Optional, if using the 'large' model in a multi-GPU installation + device: 0 +``` + + + + +:::info + +If the correct build is used for your GPU / NPU and the `large` model is configured, then the GPU will be detected and used automatically. +Specify the `device` option to target a specific GPU in a multi-GPU system (see [onnxruntime's provider options](https://onnxruntime.ai/docs/execution-providers/)). +If you do not specify a device, the first available GPU will be used. + +See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. + +::: + +## Usage and Best Practices + +For tips on getting the best results from Semantic Search (choosing between thumbnail and description search, phrasing queries effectively, and combining search with the other Explore filters), see [Usage and best practices](/usage/explore#usage-and-best-practices) in the Usage docs. + +## Triggers + +Triggers utilize Semantic Search to automate actions when a tracked object matches a specified image or description. Triggers can be configured so that Frigate executes specific actions when a tracked object's image or description matches a predefined image or text, based on a similarity threshold. Triggers are managed per camera and can be configured via the Frigate UI in the Settings page under the Triggers tab. + +:::note + +Semantic Search must be enabled to use Triggers. + +::: + +### Configuration + +Triggers are defined within the `semantic_search` configuration for each camera. Each trigger consists of a `friendly_name`, a `type` (either `thumbnail` or `description`), a `data` field (the reference image event ID or text), a `threshold` for similarity matching, and a list of `actions` to perform when the trigger fires - `notification`, `sub_label`, and `attribute`. + +Triggers are best configured through the Frigate UI. + +#### Managing Triggers in the UI + +1. Navigate to and select a camera from the dropdown menu. +2. Click **Add Trigger** to create a new trigger or use the pencil icon to edit an existing one. +3. In the **Create Trigger** wizard: + - Enter a **Name** for the trigger (e.g., "Red Car Alert"). + - Enter a descriptive **Friendly Name** for the trigger (e.g., "Red car on the driveway camera"). + - Select the **Type** (`Thumbnail` or `Description`). + - For `Thumbnail`, select an image to trigger this action when a similar thumbnail image is detected, based on the threshold. + - For `Description`, enter text to trigger this action when a similar tracked object description is detected. + - Set the **Threshold** for similarity matching. + - Select **Actions** to perform when the trigger fires. + If native webpush notifications are enabled, check the `Send Notification` box to send a notification. + Check the `Add Sub Label` box to add the trigger's friendly name as a sub label to any triggering tracked objects. + Check the `Add Attribute` box to add the trigger's internal ID (e.g., "red_car_alert") to a data attribute on the tracked object that can be processed via the API or MQTT. +4. Save the trigger to update the configuration and store the embedding in the database. + +When a trigger fires, the UI highlights the trigger with a blue dot for 3 seconds for easy identification. Additionally, the UI will show the last date/time and tracked object ID that activated your trigger. The last triggered timestamp is not saved to the database or persisted through restarts of Frigate. + +### Usage and Best Practices + +1. **Thumbnail Triggers**: Select a representative image (event ID) from the Explore page that closely matches the object you want to detect. For best results, choose images where the object is prominent and fills most of the frame. +2. **Description Triggers**: Write concise, specific text descriptions (e.g., "Person in a red jacket") that align with the tracked object's description. Avoid vague terms to improve matching accuracy. +3. **Threshold Tuning**: Adjust the threshold to balance sensitivity and specificity. A higher threshold (e.g., 0.8) requires closer matches, reducing false positives but potentially missing similar objects. A lower threshold (e.g., 0.6) is more inclusive but may trigger more often. +4. **Using Explore**: Use the context menu or right-click / long-press on a tracked object in the Grid View in Explore to quickly add a trigger based on the tracked object's thumbnail. +5. **Editing triggers**: For the best experience, triggers should be edited via the UI. However, Frigate will ensure triggers edited in the config will be synced with triggers created and edited in the UI. + +### Notes + +- Triggers rely on the same Jina AI CLIP models (V1 or V2) used for semantic search. Ensure `semantic_search` is enabled and properly configured. +- Reindexing embeddings (via the UI or `reindex: True`) does not affect trigger configurations but may update the embeddings used for matching. +- For optimal performance, use a system with sufficient RAM (8GB minimum, 16GB recommended) and a GPU for `large` model configurations, as described in the Semantic Search requirements. + +### FAQ + +#### Why can't I create a trigger on thumbnails for some text, like "person with a blue shirt" and have it trigger when a person with a blue shirt is detected? + +TL;DR: Text-to-image triggers aren't supported because CLIP can confuse similar images and give inconsistent scores, making automation unreliable. The same word-image pair can give different scores and the score ranges can be too close together to set a clear cutoff. + +Text-to-image triggers are not supported due to fundamental limitations of CLIP-based similarity search. While CLIP works well for exploratory, manual queries, it is unreliable for automated triggers based on a threshold. Issues include embedding drift (the same text-image pair can yield different cosine distances over time), lack of true semantic grounding (visually similar but incorrect matches), and unstable thresholding (distance distributions are dataset-dependent and often too tightly clustered to separate relevant from irrelevant results). Instead, it is recommended to set up a workflow with thumbnail triggers: first use text search to manually select 3-5 representative reference tracked objects, then configure thumbnail triggers based on that visual similarity. This provides robust automation without the semantic ambiguity of text to image matching. diff --git a/docs/docs/configuration/snapshots.md b/docs/docs/configuration/snapshots.md new file mode 100644 index 0000000..b2aba94 --- /dev/null +++ b/docs/docs/configuration/snapshots.md @@ -0,0 +1,148 @@ +--- +id: snapshots +title: Snapshots +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +A snapshot is a single still image that captures a tracked object at its best moment: the clearest frame Frigate saw while following that object across the scene. Unlike a [recording](./record.md), which is continuous video, a snapshot is one representative image saved per tracked object once tracking ends. + +When snapshots are enabled, Frigate saves one image to `/media/frigate/clips` for each tracked object, named `--clean.webp`. A clean image is always stored without any annotations (no timestamp, bounding boxes, or cropping) so you have an unmodified copy of the original frame. Annotations like bounding boxes and timestamps are applied on demand when a snapshot is requested [via the HTTP API](../integrations/api/event-snapshot-events-event-id-snapshot-jpg-get.api.mdx). See [Rendering](#rendering) below. + +A few things to keep in mind: + +- Snapshots are saved per tracked object, so a camera with no detected objects produces no snapshots even if recording is enabled. +- Snapshots and recordings are configured and retained independently. Enabling one does not enable the other. +- Snapshots are accessible in the UI in the Explore pane, which allows for quick submission to the Frigate+ service. +- To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones). +- Snapshots sent via MQTT are configured separately under the camera MQTT settings, not here. + +## Enabling Snapshots + +Enable snapshot saving and configure the default settings that apply to all cameras. + + + + +Navigate to . + +- Set **Enable snapshots** to on + + + + +```yaml +snapshots: + enabled: True +``` + + + + +To override snapshot settings for a specific camera: + + + + +Navigate to and select your camera. + +- Set **Enable snapshots** to on + + + + +```yaml +cameras: + front_door: + snapshots: + enabled: True +``` + + + + +## Snapshot Options + +Configure how snapshots are rendered and stored. These settings control the defaults applied when snapshots are requested via the API. + + + + +Navigate to . + +| Field | Description | +| ------------------------ | ------------------------------------------------------------------------------ | +| **Enable snapshots** | Enable or disable saving snapshots for tracked objects | +| **Timestamp overlay** | Overlay a timestamp on snapshots from API | +| **Bounding box overlay** | Draw bounding boxes for tracked objects on snapshots from API | +| **Crop snapshot** | Crop snapshots from API to the detected object's bounding box | +| **Snapshot height** | Height in pixels to resize snapshots to; leave empty to preserve original size | +| **Snapshot quality** | Encode quality for saved snapshots (0-100) | +| **Required zones** | Zones an object must enter for a snapshot to be saved | + + + + +```yaml +snapshots: + enabled: True + timestamp: False + bounding_box: True + crop: False + height: 175 + required_zones: [] + quality: 60 +``` + + + + +## Snapshot Retention + +Configure how long snapshots are retained on disk. Per-object retention overrides allow different retention periods for specific object types. + + + + +Navigate to . + +| Field | Description | +| -------------------------------------------------- | ----------------------------------------------------------------------------------- | +| **Snapshot retention > Default retention** | Number of days to retain snapshots (default: 10) | +| **Snapshot retention > Object retention > Person** | Per-object overrides for retention days (e.g., keep `person` snapshots for 15 days) | + + + + +```yaml +snapshots: + enabled: True + retain: + default: 10 + objects: + person: 15 +``` + + + + +## Frame Selection + +Frigate does not save every frame. It picks a single "best" frame for each tracked object based on detection confidence, object size, and the presence of key attributes like faces or license plates. Frames where the object touches the edge of the frame are deprioritized. That best frame is written to disk once tracking ends. + +MQTT snapshots are published more frequently: each time a better thumbnail frame is found during tracking, or when the current best image is older than `best_image_timeout` (default: 60s). These use their own annotation settings configured under the camera MQTT settings. + +## Rendering + +Frigate stores a single clean snapshot on disk: + +| API / Use | Result | +| ---------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Stored file | `--clean.webp`, always unannotated | +| `/api/events//snapshot.jpg` | Starts from the camera's `snapshots` defaults, then applies any query param overrides at request time | +| `/api/events//snapshot-clean.webp` | Returns the same stored snapshot without annotations | +| [Frigate+](/plus/first_model) submission | Uses the same stored clean snapshot | + +MQTT snapshots are configured separately under the camera MQTT settings and are unrelated to the stored event snapshot. diff --git a/docs/docs/configuration/stationary_objects.md b/docs/docs/configuration/stationary_objects.md new file mode 100644 index 0000000..40dda40 --- /dev/null +++ b/docs/docs/configuration/stationary_objects.md @@ -0,0 +1,64 @@ +# Stationary Objects + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +An object is considered stationary when it is being tracked and has been in a very similar position for a certain number of frames. This number is defined in the configuration under `detect -> stationary -> threshold`, and is 10x the frame rate (or 10 seconds) by default. Once an object is considered stationary, it will remain stationary until motion occurs within the object at which point object detection will start running again. If the object changes location, it will be considered active. + +## Why does it matter if an object is stationary? + +Once an object becomes stationary, object detection will not be continually run on that object. This serves to reduce resource usage and redundant detections when there has been no motion near the tracked object. This also means that Frigate is contextually aware, and can for example [filter out recording segments](record.md#configuring-recording-retention) to only when the object is considered active. Motion alone does not determine if an object is "active" for active_objects segment retention. Lighting changes for a parked car won't make an object active. + +## Tuning stationary behavior + +Configure how Frigate handles stationary objects. + + + + +Navigate to . + +- Set **Stationary objects config > Stationary interval** to the frequency for running detection on stationary objects (default: 50). Once stationary, detection runs every nth frame to verify the object is still present. There is no way to disable stationary object tracking with this value. +- Set **Stationary objects config > Stationary threshold** to the number of frames an object must remain relatively still before it is considered stationary (default: 50) + + + + +```yaml +detect: + stationary: + interval: 50 + threshold: 50 +``` + + + + +## Why does Frigate track stationary objects? + +Frigate didn't always track stationary objects. In fact, it didn't even track objects at all initially. + +Let's look at an example use case: I want to record any cars that enter my driveway. + +One might simply think "Why not just run object detection any time there is motion around the driveway area and notify if the bounding box is in that zone?" + +With that approach, what video is related to the car that entered the driveway? Did it come from the left or right? Was it parked across the street for an hour before turning into the driveway? One approach is to just record 24/7 or for motion (on any changed pixels) and not attempt to do that at all. This is what most other NVRs do. Just don't even try to identify a start and end for that object since it's hard and you will be wrong some portion of the time. + +Couldn't you just look at when motion stopped and started? Motion for a video feed is nothing more than looking for pixels that are different than they were in previous frames. If the car entered the driveway while someone was mowing the grass, how would you know which motion was for the car and which was for the person when they mow along the driveway or street? What if another car was driving the other direction on the street? Or what if its a windy day and the bush by your mailbox is blowing around? + +In order to do it more accurately, you need to identify objects and track them with a unique id. In each subsequent frame, everything has moved a little and you need to determine which bounding boxes go with each object from the previous frame. + +Tracking objects across frames is a challenging problem. Especially if you want to do it in real time. There are entire competitions for research algorithms to see which of them can do it the most accurately. Zero of them are accurate 100% of the time. Even the ones that can't do it in realtime. There is always an error rate in the algorithm. + +Now consider that the car is driving down a street that has other cars parked along it. It will drive behind some of these cars and in front of others. There may even be a car driving the opposite direction. + +Let's assume for now that we are NOT already tracking two parked cars on the street or the car parked in the driveway, ie, there is no stationary object tracking. + +As the car you are tracking approaches an area with 2 cars parked, the headlights reflect off the parked cars and the car parked in your driveway. The pixel values are different in that area, so there is motion detected. Object detection runs and identifies the remaining 3 cars. In the previous frame, you had a single bounding box from the car you are tracking. Now you have 4. The original object, the 2 cars on the street and the one in your driveway. + +Now you have to determine which of the bounding boxes in this frame should be matched to the tracking id from the previous frame where you only had one. Remember, you have never seen these additional 3 cars before, so you know nothing about them. On top of that the bounding box for the car you are tracking has now moved to a new location, so which of the 4 belongs to the car you were originally tracking? The algorithms here are fairly good. They use a Kalman filter to predict the next location of an object using the historical bounding boxes and the bounding box closest to the predicted location is linked. It's right sometimes, but the error rate is going to be high when there are 4 possible bounding boxes. + +Now let's assume that those other 3 cars were already being tracked as stationary objects, so the car driving down the street is a new 4th car. The object tracker knows we have had 3 cars and we now have 4. As the new car approaches the parked cars, the bounding boxes for all 4 cars is predicted based on the previous frames. The predicted boxes for the parked cars is pretty much a 100% overlap with the bounding boxes in the new frame. The parked cars are slam dunk matches to the tracking ids they had before and the only one left is the remaining bounding box which gets assigned to the new car. This results in a much lower error rate. Not perfect, but better. + +The most difficult scenario that causes IDs to be assigned incorrectly is when an object completely occludes another object. When a car drives in front of another car and it's no longer visible, a bounding box disappeared and it's a bit of a toss up when assigning the id since it's difficult to know which one is in front of the other. This happens for cars passing in front of other cars fairly often. It's something that we want to improve in the future. diff --git a/docs/docs/configuration/tls.md b/docs/docs/configuration/tls.md new file mode 100644 index 0000000..9757a78 --- /dev/null +++ b/docs/docs/configuration/tls.md @@ -0,0 +1,76 @@ +--- +id: tls +title: TLS +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +# TLS + +Frigate's integrated NGINX server supports TLS certificates. By default Frigate will generate a self signed certificate that will be used for port 8971. Frigate is designed to make it easy to use whatever tool you prefer to manage certificates. + +Frigate is often running behind a reverse proxy that manages TLS certificates for multiple services. You will likely need to set your reverse proxy to allow self signed certificates or you can disable TLS in Frigate's config. However, if you are running on a dedicated device that's separate from your proxy or if you expose Frigate directly to the internet, you may want to configure TLS with valid certificates. + +In many deployments, TLS will be unnecessary. Disable it as follows: + + + + +Navigate to . + +- Set **Enable TLS** to off if running behind a reverse proxy that handles TLS (default: on) + + + + +```yaml +tls: + enabled: False +``` + + + + +## Certificates + +TLS certificates can be mounted at `/etc/letsencrypt/live/frigate` using a bind mount or docker volume. + +```yaml {3-4} +frigate: + ... + volumes: + - /path/to/your/certificate_folder:/etc/letsencrypt/live/frigate:ro + ... +``` + +Within the folder, the private key is expected to be named `privkey.pem` and the certificate is expected to be named `fullchain.pem`. + +Note that certbot uses symlinks, and those can't be followed by the container unless it has access to the targets as well, so if using certbot you'll also have to mount the `archive` folder for your domain, e.g.: + +```yaml {3-5} +frigate: + ... + volumes: + - /etc/letsencrypt/live/your.fqdn.net:/etc/letsencrypt/live/frigate:ro + - /etc/letsencrypt/archive/your.fqdn.net:/etc/letsencrypt/archive/your.fqdn.net:ro + ... + +``` + +Frigate automatically compares the fingerprint of the certificate at `/etc/letsencrypt/live/frigate/fullchain.pem` against the fingerprint of the TLS cert in NGINX every minute. If these differ, the NGINX config is reloaded to pick up the updated certificate. + +If you issue Frigate valid certificates you will likely want to configure it to run on port 443 so you can access it without a port number like `https://your-frigate-domain.com` by mapping 8971 to 443. + +```yaml {3-4} +frigate: + ... + ports: + - "443:8971" + ... +``` + +## ACME Challenge + +Frigate also supports hosting the acme challenge files for the HTTP challenge method if needed. The challenge files should be mounted at `/etc/letsencrypt/www`. diff --git a/docs/docs/configuration/zones.md b/docs/docs/configuration/zones.md new file mode 100644 index 0000000..2a26453 --- /dev/null +++ b/docs/docs/configuration/zones.md @@ -0,0 +1,380 @@ +--- +id: zones +title: Zones +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +Zones allow you to define a specific area of the frame and apply additional filters for object types so you can determine whether or not an object is within a particular area. Presence in a zone is evaluated based on the bottom center of the bounding box for the object. It does not matter how much of the bounding box overlaps with the zone. + +For example, the cat in this image is currently in Zone 1, but **not** Zone 2. +![bottom center](/img/bottom-center.jpg) + +Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area by configuring zones with the same name for each camera. + +## Enabling/Disabling Zones + +Zones can be toggled on or off without removing them from the configuration. Disabled zones are completely ignored at runtime - objects will not be tracked for zone presence, and zones will not appear in the debug view. This is useful for temporarily disabling a zone during certain seasons or times of day without modifying the configuration. + +During testing, enable the Zones option for the [Debug view](/usage/live#the-single-camera-view) of your camera so you can adjust as needed. The zone line will increase in thickness when any object enters the zone. + +## Creating a Zone + + + + +1. Navigate to and select the desired camera. +2. Under the **Zones** section, click the plus icon to add a new zone. +3. Click on the camera's latest image to create the points for the zone boundary. Click the first point again to close the polygon. +4. Configure zone options such as **Friendly name**, **Objects**, **Loitering time**, and **Inertia** in the zone editor. +5. Press **Save** when finished. + + + + +Follow [the steps for creating a mask](masks.md), but use the zone section of the web UI instead. Alternatively, define zones directly in your configuration file: + +```yaml +cameras: + name_of_your_camera: + zones: + entire_yard: + friendly_name: Entire yard + coordinates: 0.123,0.456,0.789,0.012,... +``` + + + + +### Restricting alerts and detections to specific zones + +Often you will only want alerts to be created when an object enters areas of interest. This is done by combining zones with required zones for review items. + +To create an alert only when an object enters the `entire_yard` zone: + + + + +Navigate to . + +| Field | Description | +| ---------------------------------- | ----------------------------------------------------------------------------------------- | +| **Alerts config > Required zones** | Set to `entire_yard` so an object must enter that zone to be considered an alert; leave empty to allow alerts anywhere in the frame. | + + + + +```yaml {6,8} +cameras: + name_of_your_camera: + review: + alerts: + required_zones: + - entire_yard + zones: + entire_yard: + friendly_name: Entire yard # You can use characters from any language text + coordinates: ... +``` + + + + +You may also want to filter detections to only be created when an object enters a secondary area of interest. For example, to trigger alerts when an object enters the inner area of the yard (an `inner_yard` zone) but detections when an object enters the edge of the yard (an `edge_yard` zone): + + + + +Navigate to . + +| Field | Description | +| -------------------------------------- | -------------------------------------------------------------------------------------------- | +| **Alerts config > Required zones** | Set to `inner_yard` so an object must enter that zone to be considered an alert; leave empty to allow alerts anywhere in the frame. | +| **Detections config > Required zones** | Set to `edge_yard` so an object must enter that zone to be considered a detection; leave empty to allow detections anywhere in the frame. | + + + + +```yaml +cameras: + name_of_your_camera: + review: + alerts: + required_zones: + - inner_yard + detections: + required_zones: + - edge_yard + zones: + edge_yard: + friendly_name: Edge yard # You can use characters from any language text + coordinates: ... + inner_yard: + friendly_name: Inner yard # You can use characters from any language text + coordinates: ... +``` + + + + +### Restricting snapshots to specific zones + +To only save snapshots when an object enters a specific zone, for example an `entire_yard` zone: + + + + +1. Navigate to and select your camera. + - Set **Required zones** to `entire_yard` + + + + +```yaml +cameras: + name_of_your_camera: + snapshots: + required_zones: + - entire_yard + zones: + entire_yard: + friendly_name: Entire yard + coordinates: ... +``` + + + + +### Restricting zones to specific objects + +Sometimes you want to limit a zone to specific object types to have more granular control of when alerts, detections, and snapshots are saved. The following example limits one zone to person objects and the other to cars. + + + + +1. Navigate to and select the desired camera. +2. Create a zone named `entire_yard` covering everywhere you want to track a person. + - Under **Objects**, add `person` +3. Create a second zone named `front_yard_street` covering just the street. + - Under **Objects**, add `car` + + + + +```yaml +cameras: + name_of_your_camera: + zones: + entire_yard: + coordinates: ... (everywhere you want a person) + objects: + - person + front_yard_street: + coordinates: ... (just the street) + objects: + - car +``` + + + + +Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. Objects will be tracked for any `person` that enter anywhere in the yard, and for cars only if they enter the street. + +### Zone Loitering + +Sometimes objects are expected to be passing through a zone, but an object loitering in an area is unexpected. Zones can be configured to have a minimum loitering time after which the object will be considered in the zone. + +:::note + +When using loitering zones, a review item will behave in the following way: + +- When a person is in a loitering zone, the review item will remain active until the person leaves the loitering zone, regardless of if they are stationary. +- When any other object is in a loitering zone, the review item will remain active until the loitering time is met. Then if the object is stationary the review item will end. + +::: + + + + +1. Navigate to and select the desired camera. +2. Edit or create the zone (e.g., `sidewalk`). + - Set **Loitering time** to the desired number of seconds (e.g., `4`) + - Under **Objects**, add the relevant object types (e.g., `person`) + + + + +```yaml +cameras: + name_of_your_camera: + zones: + sidewalk: + # highlight-next-line + loitering_time: 4 # unit is in seconds + objects: + - person +``` + + + + +### Zone Inertia + +Sometimes an objects bounding box may be slightly incorrect and the bottom center of the bounding box is inside the zone while the object is not actually in the zone. Zone inertia helps guard against this by requiring an object's bounding box to be within the zone for multiple consecutive frames. + + + + +1. Navigate to and select the desired camera. +2. Edit or create the zone (e.g., `front_yard`). + - Set **Inertia** to the desired number of consecutive frames (e.g., `3`) + + + + +```yaml +cameras: + name_of_your_camera: + zones: + front_yard: + # highlight-next-line + inertia: 3 + objects: + - person +``` + + + + +There may also be cases where you expect an object to quickly enter and exit a zone, like when a car is pulling into the driveway, and you may want to have the object be considered present in the zone immediately: + + + + +1. Navigate to and select the desired camera. +2. Edit or create the zone (e.g., `driveway_entrance`). + - Set **Inertia** to `1` + + + + +```yaml +cameras: + name_of_your_camera: + zones: + driveway_entrance: + # highlight-next-line + inertia: 1 + objects: + - car +``` + + + + +### Speed Estimation + +Frigate can be configured to estimate the speed of objects moving through a zone. This works by combining data from Frigate's object tracker and "real world" distance measurements of the edges of the zone. The recommended use case for this feature is to track the speed of vehicles on a road as they move through the zone. + +Your zone must be defined with exactly 4 points and should be aligned to the ground where objects are moving. + +![Ground plane 4-point zone](/img/ground-plane.jpg) + +Speed estimation requires a minimum number of frames for your object to be tracked before a valid estimate can be calculated, so create your zone away from places where objects enter and exit for the best results. The object's bounding box must be stable and remain a constant size as it enters and exits the zone. _Your zone should not take up the full frame, and the zone does **not** need to be the same size or larger than the objects passing through it._ An object's speed is tracked while it passes through the zone and then saved to Frigate's database. + +Accurate real-world distance measurements are required to estimate speeds. These distances can be specified through the `distances` field. Each number represents the real-world distance between consecutive points in the `coordinates` list. The fastest and most accurate way to configure this is through the Zone Editor in the Frigate UI. + + + + +1. Navigate to and select the desired camera. +2. Create or edit a zone with exactly 4 points aligned to the ground plane. +3. In the zone editor, enter the real-world **Distances** between each pair of consecutive points. + - For example, if the distance between the first and second points is 10 meters, between the second and third is 12 meters, etc. +4. Distances are measured in meters (metric) or feet (imperial), depending on the **Unit system** setting. + + + + +```yaml +cameras: + name_of_your_camera: + zones: + street: + coordinates: 0.033,0.306,0.324,0.138,0.439,0.185,0.042,0.428 + distances: 10,12,11,13.5 # in meters or feet +``` + +So in the example above, the distance between the first two points ([0.033,0.306] and [0.324,0.138]) is 10. The distance between the second and third set of points ([0.324,0.138] and [0.439,0.185]) is 12, and so on. + + + + +The `distance` values are measured in meters (metric) or feet (imperial), depending on how `unit_system` is configured in your `ui` config: + + + + +Navigate to . + +| Field | Description | +| --------------- | -------------------------------------------------------------------- | +| **Unit system** | Set to `metric` (kilometers per hour) or `imperial` (miles per hour) | + + + + +```yaml +ui: + # can be "metric" or "imperial", default is metric + unit_system: metric +``` + + + + +The average speed of your object as it moved through your zone is saved in Frigate's database and can be seen in the UI in the Tracked Object Details pane in Explore. Current estimated speed can also be seen on the debug view as the third value in the object label (see the caveats below). Current estimated speed, average estimated speed, and velocity angle (the angle of the direction the object is moving relative to the frame) of tracked objects is also sent through the `events` MQTT topic. See the [MQTT docs](../integrations/mqtt.md#frigateevents). + +These speed values are output as a number in miles per hour (mph) or kilometers per hour (kph). For miles per hour, set `unit_system` to `imperial`. For kilometers per hour, set `unit_system` to `metric`. + +#### Best practices and caveats + +- Speed estimation works best with a straight road or path when your object travels in a straight line across that path. Avoid creating your zone near intersections or anywhere that objects would make a turn. +- Create a zone where the bottom center of your object's bounding box travels directly through it and does not become obscured at any time. +- A large zone can be used (as in the photo example above), but it may cause inaccurate estimation if the object's bounding box changes shape (such as when it turns or becomes partially hidden). Generally it's best to make your zone large enough to capture a few frames, but small enough so that the bounding box doesn't change size as it enters, travels through, and exits the zone. +- Depending on the size and location of your zone, you may want to decrease the zone's `inertia` value from the default of 3. +- The more accurate your real-world dimensions can be measured, the more accurate speed estimation will be. However, due to the way Frigate's tracking algorithm works, you may need to tweak the real-world distance values so that estimated speeds better match real-world speeds. +- Once an object leaves the zone, speed accuracy will likely decrease due to perspective distortion and misalignment with the calibrated area. Therefore, speed values will show as a zero through MQTT and will not be visible on the debug view when an object is outside of a speed tracking zone. +- The speeds are only an _estimation_ and are highly dependent on camera position, zone points, and real-world measurements. This feature should not be used for law enforcement. + +### Speed Threshold + +Zones can be configured with a minimum speed requirement, meaning an object must be moving at or above this speed to be considered inside the zone. Zone `distances` must be defined as described above. + + + + +1. Navigate to and select the desired camera. +2. Edit or create the zone with distances configured. + - Set **Speed threshold** to the desired minimum speed (e.g., `20`) + - The unit is kph or mph, depending on the **Unit system** setting + + + + +```yaml +cameras: + name_of_your_camera: + zones: + sidewalk: + coordinates: ... + distances: ... + inertia: 1 + # highlight-next-line + speed_threshold: 20 # unit is in kph or mph, depending on how unit_system is set (see above) +``` + + + diff --git a/docs/docs/development/contributing-boards.md b/docs/docs/development/contributing-boards.md new file mode 100644 index 0000000..930c99d --- /dev/null +++ b/docs/docs/development/contributing-boards.md @@ -0,0 +1,94 @@ +--- +id: contributing-boards +title: Community Supported Boards +--- + +## About Community Supported Boards + +There are many SBCs (small board computers) that have a passionate community behind them, Jetson Nano for example. These SBCs often have dedicated hardware that can greatly accelerate Frigate's AI and video workloads, but this hardware requires very specific frameworks for interfacing with it. + +This means it would be very difficult for Frigate's maintainers to support these different boards especially given the relatively low userbase. + +The community support boards framework allows a user in the community to be the codeowner to add support for an SBC or other detector by providing the code, maintenance, and user support. + +## Getting Started + +1. Follow the steps from [the main contributing docs](/development/contributing.md). +2. Create a new build type under `docker/` +3. Get build working as expected, all board-specific changes should be done inside of the board specific docker file. + +## Required Structure + +Each board will have different build requirements, run on different architectures, etc. however there are set of files that all boards will need. + +### Bake File .hcl + +The `board.hcl` file is what allows the community boards build to be built using the main build as a cache. This enables a clean base and quicker build times. For more information on the format and options available in the Bake file, [see the official Buildx Bake docs](https://docs.docker.com/build/bake/reference/) + +### Board Make File + +The `board.mk` file is what allows automated and configurable Make targets to be included in the main Make file. Below is the general format for this file: + +```Makefile +BOARDS += board # Replace `board` with the board suffix ex: rpi + +local-rpi: version + docker buildx bake --load --file=docker/board/board.hcl --set board.tags=frigate:latest-board bake-target # Replace `board` with the board suffix ex: rpi. Bake target is the target in the board.hcl file ex: board + +build-rpi: version + docker buildx bake --file=docker/board/board.hcl --set board.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-board bake-target # Replace `board` with the board suffix ex: rpi. Bake target is the target in the board.hcl file ex: board + +push-rpi: build-rpi + docker buildx bake --push --file=docker/board/board.hcl --set board.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-board bake-target # Replace `board` with the board suffix ex: rpi. Bake target is the target in the board.hcl file ex: board +``` + +### Dockerfile + +The `Dockerfile` is what orchestrates the build, this will vary greatly depending on the board but some parts are required for things to work. Below are the required parts of the Dockerfile: + +```Dockerfile +# syntax=docker/dockerfile:1.4 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +# All board-specific work should be done with `deps` as the base +FROM deps AS board-deps + +# do stuff specific +# to the board + +# set workdir +WORKDIR /opt/frigate/ + +# copies base files from the main frigate build +COPY --from=rootfs / / +``` + +## Other Required Changes + +### CI/CD + +The images for each board will be built for each Frigate release, this is done in the `.github/workflows/ci.yml` file. The board build workflow will need to be added here. + +```yml +- name: Build and push board build + uses: docker/bake-action@v3 + with: + push: true + targets: board # this is the target in the board.hcl file + files: docker/board/board.hcl # this should be updated with the actual board type + # the tags should be updated with the actual board types as well + # the community board builds should never push to cache, but it can pull from cache + set: | + board.tags=ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ github.ref_name }}-${{ env.SHORT_SHA }}-board + *.cache-from=type=gha +``` + +### Code Owner File + +The `CODEOWNERS` file should be updated to include the `docker/board` along with `@user` for each user that is a code owner of this board + +# Docs + +At a minimum the `installation`, `object_detectors`, `hardware_acceleration_video`, and `ffmpeg-presets` docs should be updated (if applicable) to reflect the configuration of this community board. diff --git a/docs/docs/development/contributing.md b/docs/docs/development/contributing.md new file mode 100644 index 0000000..14c39e2 --- /dev/null +++ b/docs/docs/development/contributing.md @@ -0,0 +1,276 @@ +--- +id: contributing +title: Contributing To The Main Code Base +--- + +## Getting the source + +### Core, Web, Docker, and Documentation + +This repository holds the main Frigate application and all of its dependencies. + +Fork [blakeblackshear/frigate](https://github.com/blakeblackshear/frigate.git) to your own GitHub profile, then clone the forked repo to your local machine. + +From here, follow the guides for: + +- [Core](#core) +- [Web Interface](#web-interface) +- [Documentation](#documentation) + +### Frigate Home Assistant App + +This repository holds the Home Assistant App, for use with Home Assistant OS and compatible installations. It is the piece that allows you to run Frigate from your Home Assistant Supervisor tab. + +Fork [blakeblackshear/frigate-hass-addons](https://github.com/blakeblackshear/frigate-hass-addons) to your own Github profile, then clone the forked repo to your local machine. + +### Frigate Home Assistant Integration + +This repository holds the custom integration that allows your Home Assistant installation to automatically create entities for your Frigate instance, whether you are running Frigate as a standalone Docker container or as a [Home Assistant App](#frigate-home-assistant-app). + +Fork [blakeblackshear/frigate-hass-integration](https://github.com/blakeblackshear/frigate-hass-integration) to your own GitHub profile, then clone the forked repo to your local machine. + +## Core + +### Prerequisites + +- GNU make +- Docker (including buildx plugin) +- An extra detector (Coral, OpenVINO, etc.) is optional but recommended to simulate real world performance. + +:::note + +A Coral device can only be used by a single process at a time, so an extra Coral device is recommended if using a coral for development purposes. + +::: + +### Setup + +#### 1. Open the repo with Visual Studio Code + +Upon opening, you should be prompted to open the project in a remote container. This will build a container on top of the base Frigate container with all the development dependencies installed. This ensures everyone uses a consistent development environment without the need to install any dependencies on your host machine. + +#### 2. Modify your local config file for testing + +Place the file at `config/config.yml` in the root of the repo. + +Here is an example, but modify for your needs: + +```yaml +mqtt: + host: mqtt + +cameras: + test: + ffmpeg: + inputs: + - path: /media/frigate/car-stopping.mp4 + input_args: -re -stream_loop -1 -fflags +genpts + roles: + - detect +``` + +These input args tell ffmpeg to read the mp4 file in an infinite loop. You can use any valid ffmpeg input here. + +#### 3. Gather some mp4 files for testing + +Create and place these files in a `debug` folder in the root of the repo. This is also where recordings will be created if you enable them in your test config. Update your config from step 2 above to point at the right file. You can check the `docker-compose.yml` file in the repo to see how the volumes are mapped. + +#### 4. Run Frigate from the command line + +VS Code will start the Docker Compose file for you and open a terminal window connected to `frigate-dev`. + +- Depending on what hardware you're developing on, you may need to amend `docker-compose.yml` in the project root to pass through a USB Coral or GPU for hardware acceleration. +- Run `python3 -m frigate` to start the backend. +- In a separate terminal window inside VS Code, change into the `web` directory and run `npm install && npm run dev` to start the frontend. + +#### 5. Teardown + +After closing VS Code, you may still have containers running. To close everything down, just run `docker-compose down -v` to cleanup all containers. + +### Testing + +#### Unit Tests + +GitHub will execute unit tests on new PRs. You must ensure that all tests pass. + +```shell +python3 -u -m unittest +``` + +#### FFMPEG Hardware Acceleration + +The following commands are used inside the container to ensure hardware acceleration is working properly. + +**Raspberry Pi (64bit)** + +This should show less than 50% CPU in top, and ~80% CPU without `-c:v h264_v4l2m2m`. + +```shell +ffmpeg -c:v h264_v4l2m2m -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null +``` + +**NVIDIA GPU** + +```shell +ffmpeg -c:v h264_cuvid -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null +``` + +**NVIDIA Jetson** + +```shell +ffmpeg -c:v h264_nvmpi -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null +``` + +**VAAPI** + +```shell +ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null +``` + +**QSV** + +```shell +ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null +``` + +### Submitting a pull request + +Code must be formatted, linted and type-tested. GitHub will run these checks on pull requests, so it is advised to run them yourself prior to opening. + +**Formatting** + +```shell +ruff format frigate migrations docker *.py +``` + +**Linting** + +```shell +ruff check frigate migrations docker *.py +``` + +**MyPy Static Typing** + +```shell +python3 -u -m mypy --config-file frigate/mypy.ini frigate +``` + +## Web Interface + +### Prerequisites + +- All [core](#core) prerequisites _or_ another running Frigate instance locally available +- Node.js 20 + +### Making changes + +#### 1. Set up a Frigate instance + +The Web UI requires an instance of Frigate to interact with for all of its data. You can either run an instance locally (recommended) or attach to a separate instance accessible on your network. + +To run the local instance, follow the [core](#core) development instructions. + +If you won't be making any changes to the Frigate HTTP API, you can attach the web development server to any Frigate instance on your network. Skip this step and go to [3a](#3a-run-the-development-server-against-a-non-local-instance). + +#### 2. Install dependencies + +```console +cd web && npm install +``` + +#### 3. Run the development server + +```console +cd web && npm run dev +``` + +##### 3a. Run the development server against a non-local instance + +To run the development server against a non-local instance, you will need to +replace the `localhost` values in `vite.config.ts` with the IP address of the +non-local backend server. + +#### 4. Making changes + +The Web UI is built using [Vite](https://vitejs.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com). + +Light guidelines and advice: + +- Avoid adding more dependencies. The web UI intends to be lightweight and fast to load. +- Do not make large sweeping changes. [Open a discussion on GitHub](https://github.com/blakeblackshear/frigate/discussions/new) for any large or architectural ideas. +- Ensure `lint` passes. This command will ensure basic conformance to styles, applying as many automatic fixes as possible, including Prettier formatting. + +```console +npm run lint +``` + +- Add to unit tests and ensure they pass. As much as possible, you should strive to _increase_ test coverage whenever making changes. This will help ensure features do not accidentally become broken in the future. +- If you run into error messages like "TypeError: Cannot read properties of undefined (reading 'context')" when running tests, this may be due to these issues (https://github.com/vitest-dev/vitest/issues/1910, https://github.com/vitest-dev/vitest/issues/1652) in vitest, but I haven't been able to resolve them. + +```console +npm run test +``` + +- Test in different browsers. Firefox, Chrome, and Safari all have different quirks that make them unique targets to interact with. + +## Documentation + +### Prerequisites + +- Node.js 20 + +### Making changes + +#### 1. Installation + +```console +cd docs && npm install +``` + +#### 2. Local Development + +```console +npm run start +``` + +This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. + +The docs are built using [Docusaurus v3](https://docusaurus.io). Please refer to the Docusaurus docs for more information on how to modify Frigate's documentation. + +#### 3. Build (optional) + +```console +npm run build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +## Official builds + +Setup buildx for multiarch + +``` +docker buildx stop builder && docker buildx rm builder # <---- if existing +docker run --privileged --rm tonistiigi/binfmt --install all +docker buildx create --name builder --driver docker-container --driver-opt network=host --use +docker buildx inspect builder --bootstrap +make push +``` + +## Other + +### Nginx + +When testing nginx config changes from within the dev container, the following command can be used to copy and reload the config for testing without rebuilding the container: + +```console +sudo cp docker/main/rootfs/usr/local/nginx/conf/* /usr/local/nginx/conf/ && sudo /usr/local/nginx/sbin/nginx -s reload +``` + +## Contributing translations of the Web UI + +Frigate uses [Weblate](https://weblate.org) to manage translations of the Web UI. To contribute translation, sign up for an account at Weblate and navigate to the Frigate NVR project: + +https://hosted.weblate.org/projects/frigate-nvr/ + +When translating, maintain the existing key structure while translating only the values. Ensure your translations maintain proper formatting, including any placeholder variables (like `{{example}}`). diff --git a/docs/docs/frigate/camera_setup.md b/docs/docs/frigate/camera_setup.md new file mode 100644 index 0000000..0299a9a --- /dev/null +++ b/docs/docs/frigate/camera_setup.md @@ -0,0 +1,83 @@ +--- +id: camera_setup +title: Camera setup +--- + +Cameras configured to output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. H.265 has better compression, but less compatibility. Firefox 134+/136+/137+ (Windows/Mac/Linux & Android), Chrome 108+, Safari and Edge are the only browsers able to play H.265 and only support a limited number of H.265 profiles. Ideally, cameras should be configured directly for the desired resolutions and frame rates you want to use in Frigate. Reducing frame rates within Frigate will waste CPU resources decoding extra frames that are discarded. There are three different goals that you want to tune your stream configurations around. + +- **Detection**: This is the only stream that Frigate will decode for processing. Also, this is the stream where snapshots will be generated from. The resolution for detection should be tuned for the size of the objects you want to detect. See [Choosing a detect resolution](#choosing-a-detect-resolution) for more details. The default frame rate of 5fps is correct for almost all cameras and rarely needs to be changed; see [Choosing a detect frame rate](#choosing-a-detect-frame-rate). Higher resolutions and frame rates will drive higher CPU usage on your server. + +- **Recording**: This stream should be the resolution you wish to store for reference. Typically, this will be the highest resolution your camera supports. I recommend setting this feed in your camera's firmware to 15 fps. + +- **Stream Viewing**: This stream will be rebroadcast as is to Home Assistant for viewing with the stream component. Setting this resolution too high will use significant bandwidth when viewing streams in Home Assistant, and they may not load reliably over slower connections. + +:::tip + +For the best experience in Frigate's UI, configure your camera so that the detection and recording streams use the same aspect ratio. For example, if your main stream is 3840x2160 (16:9), set your substream to 640x360 (also 16:9) instead of 640x480 (4:3). While not strictly required, matching aspect ratios helps ensure seamless live stream display and preview/recordings playback. + +::: + +### Choosing a detect resolution + +The ideal resolution for detection is one where the objects you want to detect fit inside the dimensions of the model used by Frigate (320x320). Frigate does not pass the entire camera frame to object detection. It will crop an area of motion from the full frame and look in that portion of the frame. If the area being inspected is larger than 320x320, Frigate must resize it before running object detection. Higher resolutions do not improve the detection accuracy because the additional detail is lost in the resize. Below you can see a reference for how large a 320x320 area is against common resolutions. + +Larger resolutions **do** improve performance if the objects are very small in the frame. + +![Resolutions](/img/resolutions-min.jpg) + +### Choosing a detect frame rate + +`detect.fps` controls how many times per second Frigate runs object detection. It does **not** need to match your camera's frame rate. The default of **5** is correct for the vast majority of cameras. + +:::warning + +Most users who raise `detect.fps` above the default don't need to. Increasing it consumes more CPU/GPU (detection load scales directly with the frame rate) while providing **no benefit to tracking** once objects are already being followed smoothly. Leave it at **5** unless you have a specific scene that fails the test below, and confirm any change actually helps in the [debug view](/usage/live#the-single-camera-view). + +::: + +#### Why 5 is enough for almost everyone + +Frigate follows an object by matching its bounding box from one detection frame to the next, which requires the object to be detected often enough while it is on screen. At 5 fps this is satisfied in normal scenes: an object crossing a yard, porch, driveway, or walkway is in view for several seconds and produces ~15 or more detections, which is more than enough for a reliable track and a good snapshot. This includes fast subjects such as a running person or a bolting pet, which on a wide-angle view remain on screen for several seconds. + +A higher rate helps only when an object crosses the **entire frame in less than two seconds**, which is determined by camera framing rather than object speed - for example, a camera aimed down a street at fast cross-traffic. In those scenes 5 fps may produce too few detections to hold a track. Cameras covering normal approaches and open areas are unaffected. + +#### Checking whether a higher rate is needed + +Estimate how long an object is visible as it crosses the area of interest, aiming for roughly 8–10 detections during the pass: + +> **`detect.fps` ≈ 10 ÷ (seconds the object is in view)** + +Most objects (people walking or running, pets, and vehicles in a yard, driveway, or walkway) stay in view for two seconds or more, so the default of 5 fps is correct. Slowly try raising it to 10 (the recommended maximum) in increments only when objects routinely cross the entire frame in about a second, such as a camera aimed at a street or sidewalk with fast cross-traffic. Objects that transit in under a second cannot be tracked reliably at any practical rate, so reposition the camera instead. + +:::tip + +If the formula calls for more than 10, the fix is **camera placement, not frame rate**. Angle the camera so objects move toward it rather than across the view, or aim it where traffic slows. A higher `detect.fps` increases CPU load proportionally without producing more detections of a too-brief object. + +::: + +#### Verify in the debug view + +Confirm any change in the Debug view or Debug Replay. Watch a typical object cross the scene: if its bounding box follows it smoothly while visible, the rate is sufficient. A box that jumps erratically, drops out, or splits one object into multiple events indicates the rate should be increased one step. + +#### Dedicated LPR cameras + +A dedicated license plate recognition camera is the most common reason to use something higher than 5 fps: the camera is highly zoomed, the plate is small, and it moves at full vehicle speed, so it transits the frame quickly. However, the same ceiling applies: above 10 fps is unnecessary, and **placement matters most**: aim LPR cameras where vehicles slow down, such as gates, driveways, and parking entrances. A tight view of a fast through-road will not likely read plates reliably at any frame rate. See [License Plate Recognition](/configuration/license_plate_recognition) for details. + +### Example Camera Configuration + +For the Dahua/Loryta 5442 camera, I use the following settings: + +**Main Stream (Recording & RTSP)** + +- Encode Mode: H.264 +- Resolution: 2688\*1520 +- Frame Rate(FPS): 15 +- I Frame Interval: 30 (15 can also be used to prioritize streaming performance - see the [camera settings recommendations](/configuration/live#camera-settings-recommendations) for more info) + +**Sub Stream (Detection)** + +- Enable: Sub Stream 2 +- Encode Mode: H.264 +- Resolution: 1280\*720 +- Frame Rate: 5 +- I Frame Interval: 5 diff --git a/docs/docs/frigate/glossary.md b/docs/docs/frigate/glossary.md new file mode 100644 index 0000000..d550015 --- /dev/null +++ b/docs/docs/frigate/glossary.md @@ -0,0 +1,101 @@ +--- +id: glossary +title: Glossary +--- + +The glossary explains terms commonly used in Frigate's documentation. + +## Alert + +The higher-priority of the two [review item](#review-item) severities, the other being a [detection](#detection). By default a review item is an alert when it involves a `person` or `car`; the qualifying [labels](#label) and [zones](#zone) can be configured. [See the review docs for more info](/configuration/review) + +## Attribute + +A property detected on an [object](#object) that exists alongside its [label](#label). Unlike a [sub label](#sub-label), an object can carry several attributes at once. Some attributes come directly from the object detection [model](#model) (for example `face`, `license_plate`, or delivery carrier logos such as `amazon`, `ups`, and `fedex`), while others come from a [custom object classification model](/configuration/custom_classification/object_classification) configured with the `attribute` type. Attributes are visible in the Tracked Object Details pane in Explore, in `frigate/events` MQTT messages, and through the HTTP API. + +## Bounding Box + +A box returned by the object detection [model](#model) that outlines a detected [object](#object) in the frame. In the [Debug view](/usage/live#the-single-camera-view), bounding boxes are colored by object [label](#label). + +### Bounding Box Colors + +- At startup different colors will be assigned to each object label +- A dark blue thin line indicates that object is not detected at this current point in time +- A gray thin line indicates that object is detected as being stationary +- A thick line indicates that object is the subject of autotracking (when enabled) + +## Class + +The categories a classification [model](#model) is trained to distinguish between. Each class is a distinct visual category the model predicts, plus a `none` class for inputs that don't fit any category. For example, a custom object classification model for `person` objects might use the classes `delivery_person`, `resident`, and `none`. The predicted class is applied to the [object](#object) as either a [sub label](#sub-label) or an [attribute](#attribute), depending on the model's configuration. [See the object classification docs for more info](/configuration/custom_classification/object_classification) + +## Detection + +The lower-priority of the two [review item](#review-item) severities, the other being an [alert](#alert). By default, any review item that does not qualify as an alert is a detection; the qualifying [labels](#label) and [zones](#zone) can be configured. Despite the name, a detection is a category of review item, not the same as the object detection performed by the [model](#model). [See the review docs for more info](/configuration/review) + +## False Positive + +An incorrect result from the object detection [model](#model), where it assigns the wrong [label](#label) to something in the frame, for example a dog identified as a person, or a chair identified as a dog. A person correctly identified in an area you want to ignore is not a false positive. + +## Label + +The type assigned to a detected [object](#object) by the object detection [model](#model), drawn from the model's labelmap, for example `person`, `car`, or `dog`. Frigate tracks `person` by default; additional labels are tracked by adding them to the objects configuration. [See the available objects docs for the full list](/configuration/objects) + +## Mask + +There are two types of masks in Frigate. [See the mask docs for more info](/configuration/masks) + +### Motion Mask + +A motion mask stops [motion](#motion) in the masked area from triggering object detection. It does not stop an object from being detected when object detection runs because of motion in a nearby area. Use motion masks for parts of the frame that change constantly but never contain objects you care about: camera timestamps, the sky, the tops of trees, and so on. + +### Object Mask + +An object filter mask drops any [bounding box](#bounding-box) whose bottom center falls inside the masked area (overlap elsewhere doesn't matter). The object is forced to be treated as a [false positive](#false-positive) and ignored. + +## Min Score + +The lowest score a detected object can have to be kept during tracking. Anything scoring below the minimum is assumed to be a [false positive](#false-positive) and discarded. + +## Model + +A machine learning model that Frigate uses to detect or classify objects. The object detection model locates [objects](#object) in each frame and returns their [labels](#label) and [bounding boxes](#bounding-box). Additional enrichment models run on tracked objects to add detail: face recognition, license plate recognition, bird classification, custom object and state classification, and the embedding models used for semantic search. [See the object detectors docs for more info](/configuration/object_detectors) + +## Motion + +A change in pixels between the current camera frame and previous frames. When many nearby pixels change together, they are grouped and shown as a red motion box in the debug live view. [See the motion detection docs for more info](/configuration/motion_detection) + +## Object + +Something Frigate can detect and follow in a camera frame, identified by its [label](#label) (for example a person or a car). The object types Frigate watches for are set in the `objects` configuration. Once an object is detected and followed across frames it becomes a [tracked object](#tracked-object-event-in-previous-versions), which may also carry a [sub label](#sub-label) and [attributes](#attribute). [See the available objects docs for more info](/configuration/objects) + +## Region + +A portion of the camera frame sent to the object detection [model](#model). Regions are selected because of [motion](#motion), active objects, or occasionally to recheck stationary objects, and are shown as green boxes in the debug live view. + +## Review Item + +A period of time during which one or more [tracked objects](#tracked-object-event-in-previous-versions) were active, grouped together for review. Each review item is categorized as either an [alert](#alert) or a [detection](#detection). [See the review docs for more info](/configuration/review) + +## Snapshot Score + +The object's score at the specific moment the snapshot was captured. + +## Sub Label + +A more specific identity assigned to a [tracked object](#tracked-object-event-in-previous-versions) in addition to its [label](#label). A `person` may get the name of a recognized face, a `car` may get the name of a known license plate, and a `bird` may get its species. An object can have only one sub label at a time. Sub labels are produced by face recognition, license plate recognition, bird classification, custom object classification configured with the `sub label` type, and semantic search triggers. + +## Threshold + +The median score an object must reach to be considered a true positive. + +## Top Score + +The highest median score an object reached over its lifetime. + +## Tracked Object ("event" in previous versions) + +An [object](#object) followed from the moment it enters the frame until it leaves, including any time it stays still. A tracked object is saved once it is considered a [true positive](#threshold) and meets the requirements for a snapshot or recording. + +## Zone + +A user-defined area of interest within the camera frame. Zones can be used for notifications and to limit where Frigate creates a [review item](#review-item). [See the zone docs for more info](/configuration/zones) diff --git a/docs/docs/frigate/hardware.md b/docs/docs/frigate/hardware.md new file mode 100644 index 0000000..1df3bb8 --- /dev/null +++ b/docs/docs/frigate/hardware.md @@ -0,0 +1,319 @@ +--- +id: hardware +title: Recommended hardware +--- + +import CommunityBadge from '@site/src/components/CommunityBadge'; + +## Cameras + +Cameras that output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. It is also helpful if your camera supports multiple substreams to allow different resolutions to be used for detection, streaming, and recordings without re-encoding. + +I recommend Dahua, Hikvision, and Amcrest in that order. Dahua edges out Hikvision because they are easier to find and order, not because they are better cameras. I personally use Dahua cameras because they are easier to purchase directly. In my experience Dahua and Hikvision both have multiple streams with configurable resolutions and frame rates and rock solid streams. They also both have models with large sensors well known for excellent image quality at night. Not all the models are equal. Larger sensors are better than higher resolutions; especially at night. Amcrest is the fallback recommendation because they are rebranded Dahuas. They are rebranding the lower end models with smaller sensors or less configuration options. + +WiFi cameras are not recommended as [their streams are less reliable and cause connection loss and/or lost video data](https://ipcamtalk.com/threads/camera-conflicts.68142/#post-738821), especially when more than a few WiFi cameras will be used at the same time. + +Many users have reported various issues with 4K-plus Reolink cameras, it is best to stick with 5MP and lower for Reolink cameras. If you are using Reolink, I suggest the [Reolink specific configuration](../configuration/camera_specific.md#reolink-cameras). + +Here are some of the cameras I recommend: + +- Loryta(Dahua) IPC-T549M-ALED-S3 (affiliate link) +- Loryta(Dahua) IPC-T54IR-AS (affiliate link) +- Amcrest IP5M-T1179EW-AI-V3 (affiliate link) +- HIKVISION DS-2CD2387G2P-LSU/SL ColorVu 8MP Panoramic Turret IP Camera (affiliate link) + +I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website. + +## Server + +My current favorite is the Beelink EQ13 because of the efficient N100 CPU and dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. There are many used workstation options on eBay that work very well. Anything with an Intel CPU (with AVX + AVX2 instructions) and capable of running Debian should work fine. As a bonus, you may want to look for devices with a M.2 or PCIe express slot that is compatible with the Google Coral, Hailo, or other AI accelerators. + +Note that many of these mini PCs come with Windows pre-installed, and you will need to install Linux according to the [getting started guide](../guides/getting_started.md). + +I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website. + +:::warning + +If the EQ13 is out of stock, the link below may take you to a suggested alternative on Amazon. The Beelink EQ14 has some known compatibility issues, so you should avoid that model for now. + +::: + +| Name | Capabilities | Notes | +| ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------- | +| Beelink EQ13 (Amazon) | Can run object detection on several 1080p cameras with low-medium activity | Dual gigabit NICs for easy isolated camera network. | +| Intel 1120p ([Amazon](https://www.amazon.com/Beelink-i3-1220P-Computer-Display-Gigabit/dp/B0DDCKT9YP)) | Can handle a large number of 1080p cameras with high activity | | +| Intel 125H ([Amazon](https://www.amazon.com/MINISFORUM-Pro-125H-Barebone-Computer-HDMI2-1/dp/B0FH21FSZM)) | Can handle a significant number of 1080p cameras with high activity | Includes NPU for more efficient detection in 0.17+ | + +## Detectors + +A detector is a device which is optimized for running inferences efficiently to detect objects. Using a recommended detector means there will be less latency between detections and more detections can be run per second. Frigate is designed around the expectation that a detector is used to achieve very low inference speeds. Offloading TensorFlow to a detector is an order of magnitude faster and will reduce your CPU load dramatically. + +:::info + +Frigate supports multiple different detectors that work on different types of hardware: + +**Most Hardware** + +- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices offering a wide range of compatibility with devices. + - [Supports many model architectures](../../configuration/object_detectors#configuration-hailo) + - Runs best with tiny or small size models + +- [Google Coral EdgeTPU](#google-coral-tpu): The Google Coral EdgeTPU is available in USB and m.2 format allowing for a wide range of compatibility with devices. + - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#edge-tpu-detector) + +- [MemryX](#memryx-mx3): The MX3 M.2 accelerator module is available in m.2 format allowing for a wide range of compatibility with devices. + - [Supports many model architectures](../../configuration/object_detectors#memryx-mx3) + - Runs best with tiny, small, or medium-size models + +**AMD** + +- [ROCm](#rocm---amd-gpu): ROCm can run on AMD Discrete GPUs to provide efficient object detection + - [Supports limited model architectures](../../configuration/object_detectors#amdrocm-gpu-detector) + - Runs best on discrete AMD GPUs + +**Apple Silicon** + +- [Apple Silicon](#apple-silicon): Apple Silicon is usable on all M1 and newer Apple Silicon devices to provide efficient and fast object detection + - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-detector) + - Runs well with any size models including large + - Runs via ZMQ proxy which adds some latency, only recommended for local connection + +**Intel** + +- [OpenVino](#openvino---intel): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel NPUs to provide efficient object detection. + - [Supports majority of model architectures](../../configuration/object_detectors#openvino-detector) + - Runs best with tiny, small, or medium models + +**Nvidia** + +- [Nvidia GPU](#nvidia-gpus): Nvidia GPUs can provide efficient object detection. + - [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx) + - Runs well with any size models including large + +- [Jetson](#nvidia-jetson): Jetson devices are supported via the TensorRT or ONNX detectors when running Jetpack 6. + +**Rockchip** + +- [RKNN](#rockchip-platform): RKNN models can run on Rockchip devices with included NPUs to provide efficient object detection. + - [Supports limited model architectures](../../configuration/object_detectors#rockchip-supported-models) + - Runs best with tiny or small size models + - Runs efficiently on low power hardware + +**Synaptics** + +- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs to provide efficient object detection. + +**AXERA** + +- [AXEngine](#axera): axera models can run on AXERA NPUs via AXEngine, delivering highly efficient object detection. + +::: + +### Hailo-8 + +Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms, including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided. + +**Default Model Configuration:** + +- **Hailo-8L:** Default model is **YOLOv6n**. +- **Hailo-8:** Default model is **YOLOv6n**. + +In real-world deployments, even with multiple cameras running concurrently, Frigate has demonstrated consistent performance. Testing on x86 platforms, with dual PCIe lanes, yields further improvements in FPS, throughput, and latency compared to the Raspberry Pi setup. + +| Name | Hailo‑8 Inference Time | Hailo‑8L Inference Time | +| ---------------- | ---------------------- | ----------------------- | +| ssd mobilenet v1 | ~ 6 ms | ~ 10 ms | +| yolov9-tiny | | 320: 18ms | +| yolov6n | ~ 7 ms | ~ 11 ms | + +### Google Coral TPU + +:::warning + +The Coral is no longer recommended for new Frigate installations, except in deployments with particularly low power requirements or hardware incapable of utilizing alternative AI accelerators for object detection. Instead, we suggest using one of the numerous other supported object detectors. Frigate will continue to provide support for the Coral TPU for as long as practicably possible given its still one of the most power-efficient devices for executing object detection models. + +::: + +Frigate supports both the USB and M.2 versions of the Google Coral. + +- The USB version is compatible with the widest variety of hardware and does not require a driver on the host machine. However, it does lack the automatic throttling features of the other versions. +- The PCIe and M.2 versions require installation of a driver on the host. https://github.com/jnicolson/gasket-builder should be used. + +A single Coral can handle many cameras using the default model and will be sufficient for the majority of users. You can calculate the maximum performance of your Coral based on the inference speed reported by Frigate. With an inference speed of 10, your Coral will top out at `1000/10=100`, or 100 frames per second. If your detection fps is regularly getting close to that, you should first consider tuning motion masks. If those are already properly configured, a second Coral may be needed. + +### OpenVINO - Intel + +The OpenVINO detector type is able to run on: + +- 6th Gen Intel Platforms and newer that have an iGPU +- x86 hosts with an Intel Arc GPU (including Arc A-series and B-series Battlemage) +- Intel NPUs +- Most modern AMD CPUs (though this is officially not supported by Intel) +- x86 & Arm64 hosts via CPU (generally not recommended) + +More information is available [in the detector docs](/configuration/object_detectors#openvino-detector) + +Inference speeds vary greatly depending on the CPU or GPU used, some known examples of GPU inference times are below: + +| Name | MobileNetV2 Inference Time | YOLOv9 | YOLO-NAS Inference Time | RF-DETR Inference Time | Notes | +| -------------- | -------------------------- | ------------------------------------------------- | ------------------------- | ---------------------- | ---------------------------------- | +| Intel HD 530 | 15 - 35 ms | | | | Can only run one detector instance | +| Intel HD 620 | 15 - 25 ms | | 320: ~ 35 ms | | | +| Intel HD 630 | ~ 15 ms | | 320: ~ 30 ms | | | +| Intel UHD 730 | ~ 10 ms | t-320: 14ms s-320: 24ms t-640: 34ms s-640: 65ms | 320: ~ 19 ms 640: ~ 54 ms | | | +| Intel UHD 770 | ~ 15 ms | t-320: ~ 16 ms s-320: ~ 20 ms s-640: ~ 40 ms | 320: ~ 20 ms 640: ~ 46 ms | | | +| Intel N100 | ~ 15 ms | s-320: 30 ms | 320: ~ 25 ms | | Can only run one detector instance | +| Intel N150 | ~ 15 ms | t-320: 16 ms s-320: 24 ms | | | | +| Intel Iris XE | ~ 10 ms | t-320: 6 ms t-640: 14 ms s-320: 8 ms s-640: 16 ms | 320: ~ 10 ms 640: ~ 20 ms | 320-n: 33 ms | | +| Intel NPU | ~ 6 ms | s-320: 11 ms s-640: 30 ms | 320: ~ 14 ms 640: ~ 34 ms | 320-n: 40 ms | | +| Intel Arc A310 | ~ 5 ms | t-320: 7 ms t-640: 11 ms s-320: 8 ms s-640: 15 ms | 320: ~ 8 ms 640: ~ 14 ms | | | +| Intel Arc A380 | ~ 6 ms | | 320: ~ 10 ms 640: ~ 22 ms | 336: 20 ms 448: 27 ms | | +| Intel Arc A750 | ~ 4 ms | | 320: ~ 8 ms | | | + +### Nvidia GPUs + +Frigate is able to utilize an Nvidia GPU which supports the 12.x series of CUDA libraries. + +#### Minimum Hardware Support + +12.x series of CUDA libraries are used which have minor version compatibility. The minimum driver version on the host system must be `>=545`. Also the GPU must support a Compute Capability of `5.0` or greater. This generally correlates to a Maxwell-era GPU or newer, check the NVIDIA GPU Compute Capability table linked below. + +Make sure your host system has the [nvidia-container-runtime](https://docs.docker.com/config/containers/resource_constraints/#access-an-nvidia-gpu) installed to pass through the GPU to the container and the host system has a compatible driver installed for your GPU. + +#### Compatibility References: + +[NVIDIA TensorRT Support Matrix](https://docs.nvidia.com/deeplearning/tensorrt-rtx/latest/getting-started/support-matrix.html) + +[NVIDIA CUDA Compatibility](https://docs.nvidia.com/deploy/cuda-compatibility/index.html) + +[NVIDIA GPU Compute Capability](https://developer.nvidia.com/cuda-gpus) + +Inference is done with the `onnx` detector type. Speeds will vary greatly depending on the GPU and the model used. +`tiny (t)` variants are faster than the equivalent non-tiny model, some known examples are below: + +✅ - Accelerated with CUDA Graphs +❌ - Not accelerated with CUDA Graphs + +| Name | ✅ YOLOv9 Inference Time | ✅ RF-DETR Inference Time | ❌ YOLO-NAS Inference Time | +| ----------- | ------------------------------------- | ------------------------- | -------------------------- | +| 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 3070 | t-320: 6 ms s-320: 8 ms s-640: 25 ms | Nano-320: ~ 9 ms | 320: ~ 8 ms 640: ~ 14 ms | +| RTX 5060 Ti | t-320: 5 ms s-320: 7 ms s-640: 22 ms | Nano-320: ~ 4 ms | | +| RTX A4000 | | | 320: ~ 15 ms | +| Tesla P40 | | | 320: ~ 105 ms | + +### Apple Silicon + +With the [Apple Silicon](../configuration/object_detectors.md#apple-silicon-detector) detector Frigate can take advantage of the NPU in M1 and newer Apple Silicon. + +:::warning + +Apple Silicon can not run within a container, so a ZMQ proxy is utilized to communicate with [the Apple Silicon Frigate detector](https://github.com/frigate-nvr/apple-silicon-detector) which runs on the host. This should add minimal latency when run on the same device. + +::: + +| Name | YOLOv9 Inference Time | +| ------ | ------------------------------------ | +| M4 | s-320: 10 ms | +| M3 Pro | t-320: 6 ms s-320: 8 ms s-640: 20 ms | +| M1 | s-320: 9ms | + +### ROCm - AMD GPU + +With the [ROCm](../configuration/object_detectors.md#amdrocm-gpu-detector) detector Frigate can take advantage of many discrete AMD GPUs. + +| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time | +| -------------- | --------------------------- | ------------------------- | ---------------------- | +| AMD 780M | t-320: ~ 14 ms s-320: 20 ms | 320: ~ 25 ms 640: ~ 50 ms | | +| AMD 8700G | | 320: ~ 20 ms 640: ~ 40 ms | | +| AMD 9060XT 16G | t-320: ~ 4 ms s-320: 5 ms | 320: ~ 6 ms | Nano-320: ~ 90 ms | + +## Community Supported Detectors + +### MemryX MX3 + +Frigate supports the MemryX MX3 M.2 AI Acceleration Module on compatible hardware platforms, including both x86 (Intel/AMD) and ARM-based SBCs such as Raspberry Pi 5. + +A single MemryX MX3 module is capable of handling multiple camera streams using the default models, making it sufficient for most users. For larger deployments with more cameras or bigger models, multiple MX3 modules can be used. Frigate supports multi-detector configurations, allowing you to connect multiple MX3 modules to scale inference capacity. + +Detailed information is available [in the detector docs](/configuration/object_detectors#memryx-mx3). + +**Default Model Configuration:** + +- Default model is **YOLO-NAS-Small**. + +The MX3 is a pipelined architecture, where the maximum frames per second supported (and thus supported number of cameras) cannot be calculated as `1/latency` (1/"Inference Time") and is measured separately. When estimating how many camera streams you may support with your configuration, use the **MX3 Total FPS** column to approximate of the detector's limit, not the Inference Time. + +| Model | Input Size | MX3 Inference Time | MX3 Total FPS | +| -------------------- | ---------- | ------------------ | ------------- | +| YOLO-NAS-Small | 320 | ~ 9 ms | ~ 378 | +| YOLO-NAS-Small | 640 | ~ 21 ms | ~ 138 | +| YOLOv9s | 320 | ~ 16 ms | ~ 382 | +| YOLOv9s | 640 | ~ 41 ms | ~ 110 | +| YOLOX-Small | 640 | ~ 16 ms | ~ 263 | +| SSDlite MobileNet v2 | 320 | ~ 5 ms | ~ 1056 | + +Inference speeds may vary depending on the host platform. The above data was measured on an **Intel 13700 CPU**. Platforms like Raspberry Pi, Orange Pi, and other ARM-based SBCs have different levels of processing capability, which may limit total FPS. + +### Nvidia Jetson + +Jetson devices are supported via the TensorRT or ONNX detectors when running Jetpack 6. It will [make use of the Jetson's hardware media engine](/configuration/hardware_acceleration_video#nvidia-jetson) when configured with the [appropriate presets](/configuration/ffmpeg_presets#hwaccel-presets), and will make use of the Jetson's GPU and DLA for object detection when configured with the [TensorRT detector](/configuration/object_detectors#nvidia-tensorrt-detector). + +Inference speed will vary depending on the YOLO model, jetson platform and jetson nvpmodel (GPU/DLA/EMC clock speed). It is typically 20-40 ms for most models. The DLA is more efficient than the GPU, but not faster, so using the DLA will reduce power consumption but will slightly increase inference time. + +### Rockchip platform + +Frigate supports hardware video processing on all Rockchip boards. However, hardware object detection is only supported on these boards: + +- RK3562 +- RK3566 +- RK3568 +- RK3576 +- RK3588 + +| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | YOLOx Inference Time | +| -------------- | --------------------- | --------------------------- | ----------------------- | +| rk3588 3 cores | tiny: ~ 35 ms | small: ~ 20 ms med: ~ 30 ms | nano: 14 ms tiny: 18 ms | +| rk3566 1 core | | small: ~ 96 ms | | + +The inference time of a rk3588 with all 3 cores enabled is typically 25-30 ms for yolo-nas s. + +### Synaptics + +- **Synaptics** Default model is **mobilenet** + +| Name | Synaptics SL1680 Inference Time | +| ------------- | ------------------------------- | +| ssd mobilenet | ~ 25 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) + +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. + +CPU Usage: I am a CPU, Mendel is a Google Coral + +My buddy Mendel and I have been tasked with keeping the neighbor's red footed booby off my parent's yard. Now I'm really bad at identifying birds. It takes me forever, but my buddy Mendel is incredible at it. + +Mendel however, struggles at pretty much anything else. So we make an agreement. I wait till I see something that moves, and snap a picture of it for Mendel. I then show him the picture and he tells me what it is. Most of the time it isn't anything. But eventually I see some movement and Mendel tells me it is the Booby. Score! + +_What happens when I increase the resolution of my camera?_ + +However we realize that there is a problem. There is still booby poop all over the yard. How could we miss that! I've been watching all day! My parents check the window and realize its dirty and a bit small to see the entire yard so they clean it and put a bigger one in there. Now there is so much more to see! However I now have a much bigger area to scan for movement and have to work a lot harder! Even my buddy Mendel has to work harder, as now the pictures have a lot more detail in them that he has to look at to see if it is our sneaky booby. + +Basically - When you increase the resolution and/or the frame rate of the stream there is now significantly more data for the CPU to parse. That takes additional computing power. The Google Coral is really good at doing object detection, but it doesn't have time to look everywhere all the time (especially when there are many windows to check). To balance it, Frigate uses the CPU to look for movement, then sends those frames to the Coral to do object detection. This allows the Coral to be available to a large number of cameras and not overload it. + +## Do hwaccel args help if I am using a Coral? + +YES! The Coral does not help with decoding video streams. + +Decompressing video streams takes a significant amount of CPU power. Video compression uses key frames (also known as I-frames) to send a full frame in the video stream. The following frames only include the difference from the key frame, and the CPU has to compile each frame by merging the differences with the key frame. [More detailed explanation](https://support.video.ibm.com/hc/en-us/articles/18106203580316-Keyframes-InterFrame-Video-Compression). Higher resolutions and frame rates mean more processing power is needed to decode the video stream, so try and set them on the camera to avoid unnecessary decoding work. diff --git a/docs/docs/frigate/index.md b/docs/docs/frigate/index.md new file mode 100644 index 0000000..8316202 --- /dev/null +++ b/docs/docs/frigate/index.md @@ -0,0 +1,29 @@ +--- +id: index +title: Introduction +slug: / +--- + +A complete and local NVR designed for Home Assistant with AI object detection. Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras. + +Use of a [Recommended Detector](/frigate/hardware#detectors) is optional, but strongly recommended. CPU detection should only be used for testing purposes. + +- Tight integration with Home Assistant via a [custom component](https://github.com/blakeblackshear/frigate-hass-integration) +- Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary +- Leverages multiprocessing heavily with an emphasis on realtime over processing every frame +- Uses a very low overhead motion detection to determine where to run object detection +- Object detection with TensorFlow runs in separate processes for maximum FPS +- Communicates over MQTT for easy integration into other systems +- Recording with retention based on detected objects +- Re-streaming via RTSP to reduce the number of connections to your camera +- A dynamic combined camera view of all tracked cameras. + +## Screenshots + +![Live View](/img/live-view.png) + +![Review Items](/img/review-items.png) + +![Media Browser](/img/media_browser-min.png) + +![Notification](/img/notification-min.png) diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md new file mode 100644 index 0000000..5cdbc13 --- /dev/null +++ b/docs/docs/frigate/installation.md @@ -0,0 +1,780 @@ +--- +id: installation +title: Installation +--- + +import ShmCalculator from '@site/src/components/ShmCalculator' +import DockerComposeGenerator from '@site/src/components/DockerComposeGenerator' +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Frigate is a Docker container that can be run on any Docker host including as a [Home Assistant App](https://www.home-assistant.io/apps/). Note that the Home Assistant App is **not** the same thing as the integration. The [integration](/integrations/home-assistant) is required to integrate Frigate into Home Assistant, whether you are running Frigate as a standalone Docker container or as a Home Assistant App. + +:::tip + +If you already have Frigate installed as a Home Assistant App, check out the [getting started guide](../guides/getting_started.md#configuring-frigate) to configure Frigate. + +::: + +## Dependencies + +**MQTT broker (optional)** - An MQTT broker is optional with Frigate, but is required for the Home Assistant integration. If using Home Assistant, Frigate and Home Assistant must be connected to the same MQTT broker. + +## Preparing your hardware + +### Operating System + +Frigate runs best with Docker installed on bare metal Debian-based distributions. For ideal performance, Frigate needs low overhead access to underlying hardware for the Coral and GPU devices. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended though [some users have had success with Proxmox](#proxmox). + +Windows is not officially supported, but some users have had success getting it to run under WSL or Virtualbox. Getting the GPU and/or Coral devices properly passed to Frigate may be difficult or impossible. Search previous discussions or issues for help. + +### Storage + +Frigate uses the following locations for read/write operations in the container. Docker volume mappings can be used to map these to any location on your host machine. + +- `/config`: Used to store the Frigate config file and sqlite database. You will also see a few files alongside the database file while Frigate is running. +- `/media/frigate/clips`: Used for snapshot storage. In the future, it will likely be renamed from `clips` to `snapshots`. The file structure here cannot be modified and isn't intended to be browsed or managed manually. +- `/media/frigate/recordings`: Internal system storage for recording segments. The file structure here cannot be modified and isn't intended to be browsed or managed manually. +- `/media/frigate/exports`: Storage for clips and timelapses that have been exported via the WebUI or API. +- `/tmp/cache`: Cache location for recording segments. Initial recordings are written here before being checked and converted to mp4 and moved to the recordings folder. Segments generated via the `clip.mp4` endpoints are also concatenated and processed here. It is recommended to use a [`tmpfs`](https://docs.docker.com/storage/tmpfs/) mount for this. +- `/dev/shm`: Internal cache for raw decoded frames in shared memory. It is not recommended to modify this directory or map it with docker. The minimum size is impacted by the `shm-size` calculations below. + +### Ports + +The following ports are used by Frigate and can be mapped via docker as required. + +| Port | Description | +| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `8971` | Authenticated UI and API access without TLS. Reverse proxies should use this port. | +| `5000` | Internal unauthenticated UI and API access. Access to this port should be limited. Intended to be used within the docker network for services that integrate with Frigate. | +| `8554` | RTSP restreaming. By default, these streams are unauthenticated. Authentication can be configured in go2rtc section of config. | +| `8555` | WebRTC connections for cameras with two-way talk support. | + +#### Common Docker Compose storage configurations + +Writing to a local disk or external USB drive: + +```yaml +services: + frigate: + ... + volumes: + - /path/to/your/config:/config + - /path/to/your/storage:/media/frigate + - type: tmpfs # 1GB In-memory filesystem for recording segment storage + target: /tmp/cache + tmpfs: + size: 1000000000 + ... +``` + +:::warning + +Users of the Snapcraft build of Docker cannot use storage locations outside your $HOME folder. + +::: + +### Calculating required shm-size + +Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is **64MB**. + +The default shm size of **128MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in Docker Compose). + +The Frigate container also stores logs in shm, which can take up to **40MB**, so make sure to take this into account in your math as well. + + + +The shm size cannot be set per container for Home Assistant Apps. However, this is probably not required since by default Home Assistant Supervisor allocates `/dev/shm` with half the size of your total memory. If your machine has 8GB of memory, chances are that Frigate will have access to up to 4GB without any additional configuration. + +## Extra Steps for Specific Hardware + +The following sections contain additional setup steps that are only required if you are using specific hardware. If you are not using any of these hardware types, you can skip to the [Docker](#docker) installation section. + +### Raspberry Pi 3/4 + +By default, the Raspberry Pi limits the amount of memory available to the GPU. In order to use ffmpeg hardware acceleration, you must increase the available memory by setting `gpu_mem` to the maximum recommended value in `config.txt` as described in the [official docs](https://www.raspberrypi.org/documentation/computers/config_txt.html#memory-options). + +Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with its own power supply. Some have reported success with this (affiliate link). + +### Hailo-8 + +The Hailo-8 and Hailo-8L AI accelerators are available in both M.2 and HAT form factors for the Raspberry Pi. The M.2 version typically connects to a carrier board for PCIe, which then interfaces with the Raspberry Pi 5 as part of the AI Kit. The HAT version can be mounted directly onto compatible Raspberry Pi models. Both form factors have been successfully tested on x86 platforms as well, making them versatile options for various computing environments. + +#### Installation + +:::warning + +On Raspberry Pi OS **Bookworm**, the kernel includes an older version of the Hailo driver that is incompatible with Frigate. You **must** follow the installation steps below to install the correct driver version, and you **must** disable the built-in kernel driver as described in step 1. + +On Raspberry Pi OS **Trixie**, the Hailo driver is no longer shipped with the kernel. It is installed via DKMS, and the conflict described below does not apply. You can simply run the installation script. + +::: + +1. **Disable the built-in Hailo driver (Raspberry Pi Bookworm OS only)**: + + :::note + + If you are **not** using a Raspberry Pi with **Bookworm OS**, skip this step and proceed directly to step 2. + + If you are using Raspberry Pi with **Trixie OS**, also skip this step and proceed directly to step 2. + + ::: + + First, check if the driver is currently loaded: + + ```bash + lsmod | grep hailo + ``` + + If it shows `hailo_pci`, unload it: + + ```bash + sudo modprobe -r hailo_pci + ``` + + Then locate the built-in kernel driver and rename it so it cannot be loaded. + Renaming allows the original driver to be restored later if needed. + First, locate the currently installed kernel module: + + ```bash + modinfo -n hailo_pci + ``` + + Example output: + + ``` + /lib/modules/6.6.31+rpt-rpi-2712/kernel/drivers/media/pci/hailo/hailo_pci.ko.xz + ``` + + Save the module path to a variable: + + ```bash + BUILTIN=$(modinfo -n hailo_pci) + ``` + + And rename the module by appending .bak: + + ```bash + sudo mv "$BUILTIN" "${BUILTIN}.bak" + ``` + + Now refresh the kernel module map so the system recognizes the change: + + ```bash + sudo depmod -a + ``` + + Reboot your Raspberry Pi: + + ```bash + sudo reboot + ``` + + After rebooting, verify the built-in driver is not loaded: + + ```bash + lsmod | grep hailo + ``` + + This command should return no results. + +2. **Run the installation script**: + + Download the installation script: + + ```bash + wget https://raw.githubusercontent.com/blakeblackshear/frigate/dev/docker/hailo8l/user_installation.sh + ``` + + Make it executable: + + ```bash + sudo chmod +x user_installation.sh + ``` + + Run the script: + + ```bash + ./user_installation.sh + ``` + + The script will: + - Install necessary build dependencies + - Clone and build the Hailo driver from the official repository + - Install the driver + - Download and install the required firmware + - Set up udev rules + +3. **Reboot your system**: + + After the script completes successfully, reboot to load the firmware: + + ```bash + sudo reboot + ``` + +4. **Verify the installation**: + + After rebooting, verify that the Hailo device is available: + + ```bash + ls -l /dev/hailo0 + ``` + + You should see the device listed. You can also verify the driver is loaded: + + ```bash + lsmod | grep hailo_pci + ``` + + Verify the driver version: + + ```bash + cat /sys/module/hailo_pci/version + ``` + + Verify that the firmware was installed correctly: + + ```bash + ls -l /lib/firmware/hailo/hailo8_fw.bin + ``` + + **Optional: Fix PCIe descriptor page size error** + + If you encounter the following error: + + ``` + [HailoRT] [error] CHECK failed - max_desc_page_size given 16384 is bigger than hw max desc page size 4096 + ``` + + Create a configuration file to force the correct descriptor page size: + + ```bash + echo 'options hailo_pci force_desc_page_size=4096' | sudo tee /etc/modprobe.d/hailo_pci.conf + ``` + + and reboot: + + ```bash + sudo reboot + ``` + +#### 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/hailo0 +``` + +If you are using `docker run`, add this option to your command `--device /dev/hailo0` + +#### Configuration + +Finally, configure [hardware object detection](/configuration/object_detectors#hailo-8) to complete the setup. + +### MemryX MX3 + +The MemryX MX3 Accelerator is available in the M.2 2280 form factor (like an NVMe SSD), and supports a variety of configurations: + +- x86 (Intel/AMD) PCs +- Raspberry Pi 5 +- Orange Pi 5 Plus/Max +- Multi-M.2 PCIe carrier cards + +#### Configuration + +#### Installation + +To get started with MX3 hardware setup for your system, refer to the [Hardware Setup Guide](https://developer.memryx.com/2p1/get_started/install_hardware.html). + +Then follow these steps for installing the correct driver/runtime configuration: + +1. Copy or download [this script](https://github.com/blakeblackshear/frigate/blob/dev/docker/memryx/user_installation.sh). +2. Ensure it has execution permissions with `sudo chmod +x user_installation.sh` +3. Run the script with `./user_installation.sh` +4. **Restart your computer** to complete driver installation. + +:::warning + +For manual setup, use **MemryX SDK 2.1** only. Other SDK versions are not supported for this setup. See the [SDK 2.1 documentation](https://developer.memryx.com/2p1/index.html) + +::: + +#### 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/memx0 +``` + +During configuration, you must run Docker in privileged mode and ensure the container can access the max-manager. + +In your `docker-compose.yml`, also add: + +```yaml +privileged: true + +volumes: + - /run/mxa_manager:/run/mxa_manager +``` + +If you can't use Docker Compose, you can run the container with something similar to this: + +```bash + docker run -d \ + --name frigate-memx \ + --restart=unless-stopped \ + --mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \ + --shm-size=256m \ + -v /path/to/your/storage:/media/frigate \ + -v /path/to/your/config:/config \ + -v /etc/localtime:/etc/localtime:ro \ + -v /run/mxa_manager:/run/mxa_manager \ + -e FRIGATE_RTSP_PASSWORD='password' \ + --privileged=true \ + -p 8971:8971 \ + -p 8554:8554 \ + -p 5000:5000 \ + -p 8555:8555/tcp \ + -p 8555:8555/udp \ + --device /dev/memx0 \ + ghcr.io/blakeblackshear/frigate:stable +``` + +#### Configuration + +Finally, configure [hardware object detection](/configuration/object_detectors#memryx-mx3) to complete the setup. + +### Rockchip platform + +Make sure that you use a linux distribution that comes with the rockchip BSP kernel 5.10 or 6.1 and necessary drivers (especially rkvdec2 and rknpu). To check, enter the following commands: + +``` +$ uname -r +5.10.xxx-rockchip # or 6.1.xxx; the -rockchip suffix is important +$ ls /dev/dri +by-path card0 card1 renderD128 renderD129 # should list renderD128 (VPU) and renderD129 (NPU) +$ sudo cat /sys/kernel/debug/rknpu/version +RKNPU driver: v0.9.2 # or later version +``` + +I recommend [Armbian](https://www.armbian.com/download/?arch=aarch64), if your board is supported. + +#### Setup + +Follow Frigate's default installation instructions, but use a docker image with `-rk` suffix for example `ghcr.io/blakeblackshear/frigate:stable-rk`. + +Next, you need to grant docker permissions to access your hardware: + +- During the configuration process, you should run docker in privileged mode to avoid any errors due to insufficient permissions. To do so, add `privileged: true` to your `docker-compose.yml` file or the `--privileged` flag to your docker run command. +- After everything works, you should only grant necessary permissions to increase security. Disable the privileged mode and add the lines below to your `docker-compose.yml` file: + +```yaml +security_opt: + - apparmor=unconfined + - systempaths=unconfined +devices: + - /dev/dri + - /dev/dma_heap + - /dev/rga + - /dev/mpp_service +volumes: + - /sys/:/sys/:ro +``` + +or add these options to your `docker run` command: + +``` +--security-opt systempaths=unconfined \ +--security-opt apparmor=unconfined \ +--device /dev/dri \ +--device /dev/dma_heap \ +--device /dev/rga \ +--device /dev/mpp_service \ +--volume /sys/:/sys/:ro +``` + +#### Configuration + +Next, you should configure [hardware object detection](/configuration/object_detectors#rockchip-platform) and [hardware video processing](/configuration/hardware_acceleration_video#rockchip-platform). + +### Synaptics + +- SL1680 + +#### Setup + +Follow Frigate's default installation instructions, but use a docker image with `-synaptics` suffix for example `ghcr.io/blakeblackshear/frigate:stable-synaptics`. + +Next, you need to grant docker permissions to access your hardware: + +- During the configuration process, you should run docker in privileged mode to avoid any errors due to insufficient permissions. To do so, add `privileged: true` to your `docker-compose.yml` file or the `--privileged` flag to your docker run command. + +```yaml +devices: + - /dev/synap + - /dev/video0 + - /dev/video1 +``` + +or add these options to your `docker run` command: + +``` +--device /dev/synap \ +--device /dev/video0 \ +--device /dev/video1 +``` + +#### Configuration + +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 +volumes: + - /usr/bin/axcl:/usr/bin/axcl + - /usr/lib/axcl:/usr/lib/axcl +``` + +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 + +Running through Docker with Docker Compose is the recommended install method. + + + + +Generate a Frigate Docker Compose configuration based on your hardware and requirements. + + + + + + +```yaml +services: + frigate: + container_name: frigate + privileged: true # this may not be necessary for all setups + restart: unless-stopped + stop_grace_period: 30s # allow enough time to shut down the various services + image: ghcr.io/blakeblackshear/frigate:stable + shm_size: "512mb" # update for your cameras based on calculation above + devices: + - /dev/bus/usb:/dev/bus/usb # Passes the USB Coral, needs to be modified for other versions + - /dev/apex_0:/dev/apex_0 # Passes a PCIe Coral, follow driver instructions here https://github.com/jnicolson/gasket-builder + - /dev/video11:/dev/video11 # For Raspberry Pi 4B + - /dev/dri/renderD128:/dev/dri/renderD128 # AMD / Intel GPU, needs to be updated for your hardware + - /dev/kfd:/dev/kfd # AMD Kernel Fusion Driver for ROCm + - /dev/accel:/dev/accel # AMD / Intel NPU + volumes: + - /etc/localtime:/etc/localtime:ro + - /path/to/your/config:/config + - /path/to/your/storage:/media/frigate + - type: tmpfs # 1GB In-memory filesystem for recording segment storage + target: /tmp/cache + tmpfs: + size: 1000000000 + ports: + - "8971:8971" + # - "5000:5000" # Internal unauthenticated access. Expose carefully. + - "8554:8554" # RTSP feeds + - "8555:8555/tcp" # WebRTC over tcp + - "8555:8555/udp" # WebRTC over udp + environment: + FRIGATE_RTSP_PASSWORD: "password" +``` + + + +**Docker CLI** + +If you can't use Docker Compose, you can run the container with something similar to this: + +```bash +docker run -d \ + --name frigate \ + --restart=unless-stopped \ + --stop-timeout 30 \ + --mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \ + --device /dev/bus/usb:/dev/bus/usb \ + --device /dev/dri/renderD128 \ + --shm-size=64m \ + -v /path/to/your/storage:/media/frigate \ + -v /path/to/your/config:/config \ + -v /etc/localtime:/etc/localtime:ro \ + -e FRIGATE_RTSP_PASSWORD='password' \ + -p 8971:8971 \ + -p 8554:8554 \ + -p 8555:8555/tcp \ + -p 8555:8555/udp \ + ghcr.io/blakeblackshear/frigate:stable +``` + +The official docker image tags for the current stable version are: + +- `stable` - Standard Frigate build for amd64 & RPi Optimized Frigate build for arm64. This build includes support for Hailo devices as well. +- `stable-standard-arm64` - Standard Frigate build for arm64 +- `stable-tensorrt` - Frigate build specific for amd64 devices running an Nvidia GPU +- `stable-rocm` - Frigate build for [AMD GPUs](../configuration/object_detectors.md#amdrocm-gpu-detector) + +The community supported docker image tags for the current stable version are: + +- `stable-tensorrt-jp6` - Frigate build optimized for Nvidia Jetson devices running Jetpack 6 +- `stable-rk` - Frigate build for SBCs with Rockchip SoC + +## Home Assistant App + +:::warning + +As of Home Assistant Operating System 10.2 and Home Assistant 2023.6 defining separate network storage for media is supported. + +There are important limitations in HA OS to be aware of: + +- Separate local storage for media is not yet supported by Home Assistant +- AMD GPUs are not supported because HA OS does not include the mesa driver. +- Intel NPUs are not supported because HA OS does not include the NPU firmware. +- Nvidia GPUs are not supported because HA Apps do not support the Nvidia runtime. + +::: + +:::tip + +See [the network storage guide](/guides/ha_network_storage.md) for instructions to setup network storage for frigate. + +::: + +Home Assistant OS users can install via the App repository. + +1. In Home Assistant, navigate to _Settings_ > _Apps_ > _App Store_ > _Repositories_ +2. Add `https://github.com/blakeblackshear/frigate-hass-addons` +3. Install the desired variant of the Frigate App (see below) +4. Setup your network configuration in the `Configuration` tab +5. Start the App +6. Use the _Open Web UI_ button to access the Frigate UI, then click in the _cog icon_ > _Configuration editor_ and configure Frigate to your liking + +There are several variants of the App available: + +| App Variant | Description | +| -------------------------- | ---------------------------------------------------------- | +| Frigate | Current release with protection mode on | +| Frigate (Full Access) | Current release with the option to disable protection mode | +| Frigate Beta | Beta release with protection mode on | +| Frigate Beta (Full Access) | Beta release with the option to disable protection mode | + +If you are using hardware acceleration for ffmpeg, you **may** need to use the _Full Access_ variant of the App. This is because the Frigate App runs in a container with limited access to the host system. The _Full Access_ variant allows you to disable _Protection mode_ and give Frigate full access to the host system. + +You can also edit the Frigate configuration file through the [VS Code App](https://github.com/hassio-addons/addon-vscode) or similar. In that case, the configuration file will be at `/addon_configs//config.yml`, where `` is specific to the variant of the Frigate App you are running. See the list of directories [here](../configuration/config.md#accessing-app-config-dir). + +## Kubernetes + +Use the [helm chart](https://github.com/blakeblackshear/blakeshome-charts/tree/master/charts/frigate). + +## Unraid + +Many people have powerful enough NAS devices or home servers to also run docker. There is a Unraid Community App. +To install make sure you have the [community app plugin here](https://forums.unraid.net/topic/38582-plug-in-community-applications/). Then search for "Frigate" in the apps section within Unraid - you can see the online store [here](https://unraid.net/community/apps?q=frigate#r) + +## Proxmox + +[According to Proxmox documentation](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#chapter_pct) it is recommended that you run application containers like Frigate inside a Proxmox QEMU VM. This will give you all the advantages of application containerization, while also providing the benefits that VMs offer, such as strong isolation from the host and the ability to live-migrate, which otherwise isn’t possible with containers. Ensure that ballooning is **disabled**, especially if you are passing through a GPU to the VM. + +:::warning + +If you choose to run Frigate via LXC in Proxmox the setup can be complex so be prepared to read the Proxmox and LXC documentation, Frigate does not officially support running inside of an LXC. + +::: + +Suggestions include: + +- For Intel-based hardware acceleration, to allow access to the `/dev/dri/renderD128` device with major number 226 and minor number 128, add the following lines to the `/etc/pve/lxc/.conf` LXC configuration: + - `lxc.cgroup2.devices.allow: c 226:128 rwm` + - `lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file` +- The LXC configuration will likely also need `features: fuse=1,nesting=1`. This allows running a Docker container in an LXC container (`nesting`) and prevents duplicated files and wasted storage (`fuse`). +- Successfully passing hardware devices through multiple levels of containerization (LXC then Docker) can be difficult. Many people make devices like `/dev/dri/renderD128` world-readable in the host or run Frigate in a privileged LXC container. +- The virtualization layer often introduces a sizable amount of overhead for communication with Coral devices, but [not in all circumstances](https://github.com/blakeblackshear/frigate/discussions/1837). + +See the [Proxmox LXC discussion](https://github.com/blakeblackshear/frigate/discussions/5773) for more general information. + +## ESXi + +For details on running Frigate using ESXi, please see the instructions [here](https://williamlam.com/2023/05/frigate-nvr-with-coral-tpu-igpu-passthrough-using-esxi-on-intel-nuc.html). + +If you're running Frigate on a rack mounted server and want to passthrough the Google Coral, [read this.](https://github.com/blakeblackshear/frigate/issues/305) + +## Synology NAS on DSM 7 + +These settings were tested on DSM 7.1.1-42962 Update 4 + +**General:** + +The `Execute container using high privilege` option needs to be enabled in order to give the frigate container the elevated privileges it may need. + +The `Enable auto-restart` option can be enabled if you want the container to automatically restart whenever it improperly shuts down due to an error. + +![image](https://user-images.githubusercontent.com/4516296/232586790-0b659a82-561d-4bc5-899b-0f5b39c6b11d.png) + +**Advanced Settings:** + +If you want to use the password template feature, you should add the "FRIGATE_RTSP_PASSWORD" environment variable and set it to your preferred password under advanced settings. The rest of the environment variables should be left as default for now. + +![image](https://user-images.githubusercontent.com/4516296/232587163-0eb662d4-5e28-4914-852f-9db1ec4b9c3d.png) + +**Port Settings:** + +The network mode should be set to `bridge`. You need to map the default frigate container ports to your local Synology NAS ports that you want to use to access Frigate. + +There may be other services running on your NAS that are using the same ports that frigate uses. In that instance you can set the ports to auto or a specific port. + +![image](https://user-images.githubusercontent.com/4516296/232582642-773c0e37-7ef5-4373-8ce3-41401b1626e6.png) + +**Volume Settings:** + +You need to configure 2 paths: + +- The location of your config directory which will be different depending on your NAS folder structure e.g. `/docker/frigate/config` will mount to `/config` within the container. +- The location on your NAS where the recordings will be saved this needs to be a folder e.g. `/docker/volumes/frigate-0-media` + +![image](https://user-images.githubusercontent.com/4516296/232585872-44431d15-55e0-4004-b78b-1e512702b911.png) + +## QNAP NAS + +These instructions were tested on a QNAP with an Intel J3455 CPU and 16G RAM, running QTS 4.5.4.2117. + +QNAP has a graphic tool named Container Station to install and manage docker containers. However, there are two limitations with Container Station that make it unsuitable to install Frigate: + +1. Container Station does not incorporate GitHub Container Registry (ghcr), which hosts Frigate docker image version 0.12.0 and above. +2. Container Station uses default 64 Mb shared memory size (shm-size), and does not have a mechanism to adjust it. Frigate requires a larger shm-size to be able to work properly with more than two high resolution cameras. + +Because of above limitations, the installation has to be done from command line. Here are the steps: + +**Preparation** + +1. Install Container Station from QNAP App Center if it is not installed. +2. Enable ssh on your QNAP (please do an Internet search on how to do this). +3. Prepare Frigate config file, name it `config.yml`. +4. Calculate shared memory size according to [documentation](https://docs.frigate.video/frigate/installation). +5. Find your time zone value from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +6. ssh to QNAP. + +**Installation** + +Run the following commands to install Frigate (using `stable` version as example): + +```shell +# Download Frigate image +docker pull ghcr.io/blakeblackshear/frigate:stable +# Create directory to host Frigate config file on QNAP file system. +# E.g., you can choose to create it under /share/Container. +mkdir -p /share/Container/frigate/config +# Copy the config file prepared in step 2 into the newly created config directory. +cp path/to/your/config/file /share/Container/frigate/config +# Create directory to host Frigate media files on QNAP file system. +# (if you have a surveillance disk, create media directory on the surveillance disk. +# Example command assumes share_vol2 is the surveillance drive +mkdir -p /share/share_vol2/frigate/media +# Create Frigate docker container. Replace shm-size value with the value from preparation step 3. +# Also replace the time zone value for 'TZ' in the sample command. +# Example command will create a docker container that uses at most 2 CPUs and 4G RAM. +# You may need to add "--env=LIBVA_DRIVER_NAME=i965 \" to the following docker run command if you +# have certain CPU (e.g., J4125). See https://docs.frigate.video/configuration/hardware_acceleration_video. +docker run \ + --name=frigate \ + --shm-size=256m \ + --restart=unless-stopped \ + --env=TZ=America/New_York \ + --volume=/share/Container/frigate/config:/config:rw \ + --volume=/share/share_vol2/frigate/media:/media/frigate:rw \ + --network=bridge \ + --privileged \ + --workdir=/opt/frigate \ + -p 8971:8971 \ + -p 8554:8554 \ + -p 8555:8555 \ + -p 8555:8555/udp \ + --label='com.qnap.qcs.network.mode=nat' \ + --label='com.qnap.qcs.gpu=False' \ + --memory="4g" \ + --cpus="2" \ + --detach=true \ + -t \ + ghcr.io/blakeblackshear/frigate:stable +``` + +Log into QNAP, open Container Station. Frigate docker container should be listed under 'Overview' and running. Visit Frigate Web UI by clicking Frigate docker, and then clicking the URL shown at the top of the detail page. + +## macOS - Apple Silicon + +:::warning + +macOS uses port 5000 for its Airplay Receiver service. If you want to expose port 5000 in Frigate for local app and API access the port will need to be mapped to another port on the host e.g. 5001 + +Failure to remap port 5000 on the host will result in the WebUI and all API endpoints on port 5000 being unreachable, even if port 5000 is exposed correctly in Docker. + +::: + +Docker containers on macOS can be orchestrated by either [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) or [OrbStack](https://orbstack.dev) (native Swift app). The difference in inference speeds is negligible, however CPU, power consumption and container start times will be lower on OrbStack because it is a native Swift application. + +To allow Frigate to use the Apple Silicon Neural Engine / Processing Unit (NPU) the host must be running [Apple Silicon Detector](../configuration/object_detectors.md#apple-silicon-detector) on the host (outside Docker) + +#### Docker Compose example + +```yaml +services: + frigate: + container_name: frigate + image: ghcr.io/blakeblackshear/frigate:stable-standard-arm64 + restart: unless-stopped + shm_size: "512mb" # update for your cameras based on calculation above + volumes: + - /etc/localtime:/etc/localtime:ro + - /path/to/your/config:/config + - /path/to/your/recordings:/recordings + ports: + - "8971:8971" + # If exposing on macOS map to a different host port like 5001 or any other port with no conflicts + # - "5001:5000" # Internal unauthenticated access. Expose carefully. + - "8554:8554" # RTSP feeds + extra_hosts: + # This is very important + # It allows frigate access to the NPU on Apple Silicon via Apple Silicon Detector + - "host.docker.internal:host-gateway" # Required to talk to the NPU detector + environment: + - FRIGATE_RTSP_PASSWORD: "password" +``` diff --git a/docs/docs/frigate/network_requirements.md b/docs/docs/frigate/network_requirements.md new file mode 100644 index 0000000..0c28f83 --- /dev/null +++ b/docs/docs/frigate/network_requirements.md @@ -0,0 +1,155 @@ +--- +id: network_requirements +title: Network Requirements +--- + +# Network Requirements + +Frigate is designed to run locally and does not require a persistent internet connection for core functionality. However, certain features need internet access for initial setup or ongoing operation. This page describes what connects to the internet, when, and how to control it. + +## How Frigate Uses the Internet + +Frigate's internet usage falls into three categories: + +1. **One-time model downloads**: ML models are downloaded the first time a feature is enabled, then cached locally. No internet is needed on subsequent startups. +2. **Optional cloud services**: Features like Frigate+ and Generative AI connect to external APIs only when explicitly configured. +3. **Build-time dependencies**: Components bundled into the Docker image during the build process. These require no internet at runtime. + +:::tip + +After initial setup, Frigate can run fully offline as long as all required models have been downloaded and no cloud-dependent features are enabled. + +::: + +## One-Time Model Downloads + +The following models are downloaded automatically the first time their associated feature is enabled. Once cached in `/config/model_cache/`, they do not require internet again. + +| Feature | Models Downloaded | Source | +| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------- | +| [Semantic search](/configuration/semantic_search) | Jina CLIP v1 or v2 (ONNX) + tokenizer | HuggingFace | +| [Face recognition](/configuration/face_recognition) | FaceNet, ArcFace, face detection model | GitHub | +| [License plate recognition](/configuration/license_plate_recognition) | PaddleOCR (detection, classification, recognition) + YOLOv9 plate detector | GitHub | +| [Bird classification](/configuration/bird_classification) | MobileNetV2 bird model + label map | GitHub | +| [Custom classification](/configuration/custom_classification/state_classification) (training) | MobileNetV2 ImageNet base weights (via Keras) | Google storage | +| [Audio transcription](/configuration/advanced/system) | Whisper or Sherpa-ONNX streaming model | HuggingFace / OpenAI | + +### Hardware-Specific Detector Models + +If you are using one of the following hardware detectors and have not provided your own model file, a default model will be downloaded on first startup: + +| Detector | Model Downloaded | Source | +| ------------------------------------------------------------------ | -------------------- | ------------------------ | +| [Rockchip RKNN](/configuration/object_detectors#rockchip-platform) | RKNN detection model | GitHub | +| [Hailo 8 / 8L](/configuration/object_detectors#hailo-8) | YOLOv6n (.hef) | Hailo Model Zoo (AWS S3) | +| [AXERA AXEngine](/configuration/object_detectors) | Detection model | HuggingFace | + +:::note + +The default CPU, EdgeTPU, and OpenVINO object detection models are bundled into the Docker image and do not require any download at runtime. + +::: + +### Preventing Model Downloads + +If you have already downloaded all required models and want to prevent Frigate from attempting any outbound connections to HuggingFace or the Transformers library, set the following environment variables on your Frigate container: + +```yaml +environment: + HF_HUB_OFFLINE: "1" + TRANSFORMERS_OFFLINE: "1" +``` + +:::warning + +Setting these variables without having the correct model files already cached in `/config/model_cache/` will cause failures. Only use these after a successful initial setup with internet access. + +::: + +### Mirror Support + +If your Frigate instance has restricted internet access, you can point model downloads at internal mirrors using environment variables: + +| Environment Variable | Default | Used By | +| ----------------------------------- | ----------------------------------- | --------------------------------------------- | +| `HF_ENDPOINT` | `https://huggingface.co` | Semantic search, Sherpa-ONNX, AXEngine models | +| `GITHUB_ENDPOINT` | `https://github.com` | Face recognition, LPR, RKNN models | +| `GITHUB_RAW_ENDPOINT` | `https://raw.githubusercontent.com` | Bird classification | +| `TF_KERAS_MOBILENET_V2_WEIGHTS_URL` | Google storage (Keras default) | Custom classification training | + +## Optional Cloud Services + +These features connect to external services during normal operation and require internet whenever they are active. + +### Frigate+ + +When a Frigate+ API key is configured, Frigate communicates with `https://api.frigate.video` to download models, upload snapshots for training, submit annotations, and report false positives. Remove the API key to disable all Frigate+ network activity. + +See [Frigate+](/integrations/plus) for details. + +### Generative AI + +When a Generative AI provider is configured, Frigate sends images and prompts to the configured provider for event descriptions, chat, and camera monitoring. Available providers: + +| Provider | Internet Required | +| ------------- | --------------------------------------------------------------- | +| OpenAI | Yes, connects to OpenAI API (or custom base URL) | +| Google Gemini | Yes, connects to Google Generative AI API | +| Azure OpenAI | Yes, connects to your Azure endpoint | +| Ollama | Depends: typically local (`localhost:11434`), but can be remote | +| llama.cpp | No, runs entirely locally | + +Disable Generative AI by removing the `genai` configuration from your cameras. See [Generative AI](/configuration/genai/genai_config) for details. + +### Version Check + +Frigate checks GitHub for the latest release version on startup by querying `https://api.github.com`. This can be disabled: + +```yaml +telemetry: + version_check: false +``` + +### Push Notifications + +When [notifications](/configuration/notifications) are enabled and users have registered for push notifications in the web UI, Frigate sends push messages through the browser vendor's push service (e.g., Google FCM, Mozilla autopush). This requires internet access from the Frigate server to these push endpoints. + +### MQTT + +If an [MQTT broker](/integrations/mqtt) is configured, Frigate maintains a connection to the broker's host and port. This is typically a local network connection, but will require internet if you use a cloud-hosted MQTT broker. + +### DeepStack / CodeProject.AI + +When using the [DeepStack detector plugin](/configuration/object_detectors), Frigate sends images to the configured API endpoint for inference. This is typically local but depends on where the service is hosted. + +## WebRTC (STUN) + +For [WebRTC live streaming](/configuration/live), Frigate uses STUN for NAT traversal: + +- **go2rtc** defaults to a local STUN listener (`stun:8555`), no internet required. +- **The web UI's WebRTC player** includes a fallback to Google's public STUN server (`stun:stun.l.google.com:19302`), which requires internet. + +## Home Assistant Supervisor + +When running as a Home Assistant App, the go2rtc startup script queries the local Supervisor API (`http://supervisor/`) to discover the host IP address and WebRTC port. This is a local network call to the Home Assistant host, not an internet connection. + +## What Does NOT Require Internet + +- **Object detection**: CPU, EdgeTPU, OpenVINO, and other bundled detector models are included in the Docker image. +- **Recording and playback**: All video is stored and served locally. +- **Live streaming**: Camera streams are pulled over your local network. MSE and HLS streaming work without any external connections. +- **The web interface**: Fully self-contained with no external fonts, scripts, analytics, or CDN dependencies. All translations are bundled locally. +- **Custom classification inference**: After training, custom models run entirely locally. +- **Audio detection**: The YAMNet audio classification model is bundled in the Docker image. + +## Running Frigate Offline + +To run Frigate in an air-gapped or offline environment: + +1. **Pre-download models**: Start Frigate with internet access once with all desired features enabled. Models will be cached in `/config/model_cache/`. +2. **Disable version check**: Set `telemetry.version_check: false` in your configuration. +3. **Block outbound model requests**: Set the `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1` environment variables to prevent HuggingFace and Transformers from attempting any network requests. +4. **Avoid cloud features**: Do not configure Frigate+, Generative AI providers that require internet, or cloud MQTT brokers. +5. **Use local model mirrors**: If limited internet is available, set the `HF_ENDPOINT`, `GITHUB_ENDPOINT`, and `GITHUB_RAW_ENDPOINT` environment variables to point to local mirrors. + +After these steps, Frigate will operate with no outbound internet connections. diff --git a/docs/docs/frigate/planning_setup.md b/docs/docs/frigate/planning_setup.md new file mode 100644 index 0000000..4e0d82f --- /dev/null +++ b/docs/docs/frigate/planning_setup.md @@ -0,0 +1,79 @@ +--- +id: planning_setup +title: Planning a New Installation +--- + +Choosing the right hardware for your Frigate NVR setup is important for optimal performance and a smooth experience. This guide will walk you through the key considerations, focusing on the number of cameras and the hardware required for efficient object detection. + +## Key Considerations + +### Number of Cameras and Simultaneous Activity + +The most fundamental factor in your hardware decision is the number of cameras you plan to use. However, it's not just about the raw count; it's also about how many of those cameras are likely to see activity and require object detection simultaneously. + +When motion is detected in a camera's feed, regions of that frame are sent to your chosen [object detection hardware](/configuration/object_detectors). + +- **Low Simultaneous Activity (1-6 cameras with occasional motion)**: If you have a few cameras in areas with infrequent activity (e.g., a seldom-used backyard, a quiet interior), the demand on your object detection hardware will be lower. A single, entry-level AI accelerator will suffice. +- **Moderate Simultaneous Activity (6-12 cameras with some overlapping motion)**: For setups with more cameras, especially in areas like a busy street or a property with multiple access points, it's more likely that several cameras will capture activity at the same time. This increases the load on your object detection hardware, requiring more processing power. +- **High Simultaneous Activity (12+ cameras or highly active zones)**: Large installations or scenarios where many cameras frequently capture activity (e.g., busy street with overview, identification, dedicated LPR cameras, etc.) will necessitate robust object detection capabilities. You'll likely need multiple entry-level AI accelerators or a more powerful single unit such as a discrete GPU. +- **Commercial Installations (40+ cameras)**: Commercial installations or scenarios where a substantial number of cameras capture activity (e.g., a commercial property, an active public space) will necessitate robust object detection capabilities. You'll likely need a modern discrete GPU. + +### Video Decoding + +Modern CPUs with integrated GPUs (Intel Quick Sync, AMD VCN) or dedicated GPUs can significantly offload video decoding from the main CPU, freeing up resources. This is highly recommended, especially for multiple cameras. + +:::tip + +For commercial installations it is important to verify the number of supported concurrent streams on your GPU, many consumer GPUs max out at ~20 concurrent camera streams. + +::: + +## Hardware Considerations + +### Object Detection + +There are many different hardware options for object detection depending on priorities and available hardware. See [the recommended hardware page](./hardware.md#detectors) for more specifics on what hardware is recommended for object detection. + +### CPU + +Frigate requires a CPU with AVX + AVX2 instructions. Most modern CPUs (post-2011) support AVX and AVX2, but it is generally absent in low-power or budget-oriented processors, particularly older Intel Pentium, Celeron, and Atom-based chips. Specifically, Intel Celeron and Pentium models prior to the 2020 Tiger Lake generation typically lack AVX. Older Intel Xeon models may have AVX, but may lack AVX2. + +### Storage + +Storage is an important consideration when planning a new installation. To get a more precise estimate of your storage requirements, you can use an IP camera storage calculator. Websites like [IPConfigure Storage Calculator](https://calculator.ipconfigure.com/) can help you determine the necessary disk space based on your camera settings. + +Once running, see [Understanding storage usage](/configuration/record#understanding-storage-usage) for how Frigate measures and reports disk usage, and why its numbers won't exactly match `df` or `du`. + +#### SSDs (Solid State Drives) + +SSDs are an excellent choice for Frigate, offering high speed and responsiveness. The older concern that SSDs would quickly "wear out" from constant video recording is largely no longer valid for modern consumer and enterprise-grade SSDs. + +- Longevity: Modern SSDs are designed with advanced wear-leveling algorithms and significantly higher "Terabytes Written" (TBW) ratings than earlier models. For typical home NVR use, a good quality SSD will likely outlast the useful life of your NVR hardware itself. +- Performance: SSDs excel at handling the numerous small write operations that occur during continuous video recording and can significantly improve the responsiveness of the Frigate UI and clip retrieval. +- Silence and Efficiency: SSDs produce no noise and consume less power than traditional HDDs. + +#### HDDs (Hard Disk Drives) + +Traditional Hard Disk Drives (HDDs) remain a great and often more cost-effective option for long-term video storage, especially for larger setups where raw capacity is prioritized. + +- Cost-Effectiveness: HDDs offer the best cost per gigabyte, making them ideal for storing many days, weeks, or months of continuous footage. +- Capacity: HDDs are available in much larger capacities than most consumer SSDs, which is beneficial for extensive video archives. +- NVR-Rated Drives: If choosing an HDD, consider drives specifically designed for surveillance (NVR) use, such as Western Digital Purple or Seagate SkyHawk. These drives are engineered for 24/7 operation and continuous write workloads, offering improved reliability compared to standard desktop drives. + +Determining Your Storage Needs +The amount of storage you need will depend on several factors: + +- Number of Cameras: More cameras naturally require more space. +- Resolution and Framerate: Higher resolution (e.g., 4K) and higher framerate (e.g., 30fps) streams consume significantly more storage. +- Recording Method: Continuous recording uses the most space. motion-only recording or object-triggered recording can save space, but may miss some footage. +- Retention Period: How many days, weeks, or months of footage do you want to keep? + +#### Network Storage (NFS/SMB) + +While supported, using network-attached storage (NAS) for recordings can introduce latency and network dependency considerations. For optimal performance and reliability, it is generally recommended to have local storage for your Frigate recordings. If using a NAS, ensure your network connection to it is robust and fast (Gigabit Ethernet at minimum) and that the NAS itself can handle the continuous write load. + +### RAM (Memory) + +- **Basic Minimum: 4GB RAM**: This is generally sufficient for a very basic Frigate setup with a few cameras and a dedicated object detection accelerator, without running any enrichments. Performance might be tight, especially with higher resolution streams or numerous detections. +- **Minimum for Enrichments: 8GB RAM**: If you plan to utilize Frigate's enrichment features (e.g., facial recognition, license plate recognition, or other AI models that run alongside standard object detection), 8GB of RAM should be considered the minimum. Enrichments require additional memory to load and process their respective models and data. +- **Recommended: 16GB RAM**: For most users, especially those with many cameras (8+) or who plan to heavily leverage enrichments, 16GB of RAM is highly recommended. This provides ample headroom for smooth operation, reduces the likelihood of swapping to disk (which can impact performance), and allows for future expansion. diff --git a/docs/docs/frigate/updating.md b/docs/docs/frigate/updating.md new file mode 100644 index 0000000..a4dfb7f --- /dev/null +++ b/docs/docs/frigate/updating.md @@ -0,0 +1,114 @@ +--- +id: updating +title: Updating +--- + +# Updating Frigate + +The current stable version of Frigate is **0.18.0**. The release notes and any breaking changes for this version can be found on the [Frigate GitHub releases page](https://github.com/blakeblackshear/frigate/releases/tag/v0.18.0). + +Keeping Frigate up to date ensures you benefit from the latest features, performance improvements, and bug fixes. The update process varies slightly depending on your installation method (Docker, Home Assistant App, etc.). Below are instructions for the most common setups. + +## Before You Begin + +- **Stop Frigate**: For most methods, you’ll need to stop the running Frigate instance before backing up and updating. +- **Backup Your Configuration**: Always back up your `/config` directory (e.g., `config.yml` and `frigate.db`, the SQLite database) before updating. This ensures you can roll back if something goes wrong. +- **Check Release Notes**: Carefully review the [Frigate GitHub releases page](https://github.com/blakeblackshear/frigate/releases) for breaking changes or configuration updates that might affect your setup. + +## Updating with Docker + +If you’re running Frigate via Docker (recommended method), follow these steps: + +1. **Stop the Container**: + - If using Docker Compose: + ```bash + docker compose down frigate + ``` + - If using `docker run`: + ```bash + docker stop frigate + ``` + +2. **Update and Pull the Latest Image**: + - If using Docker Compose: + - Edit your `docker-compose.yml` file to specify the desired version tag (e.g., `0.18.0` instead of `0.17.1`). For example: + ```yaml + services: + frigate: + image: ghcr.io/blakeblackshear/frigate:0.18.0 + ``` + - Then pull the image: + ```bash + docker pull ghcr.io/blakeblackshear/frigate:0.18.0 + ``` + - **Note for `stable` Tag Users**: If your `docker-compose.yml` uses the `stable` tag (e.g., `ghcr.io/blakeblackshear/frigate:stable`), you don’t need to update the tag manually. The `stable` tag always points to the latest stable release after pulling. + - If using `docker run`: + - Pull the image with the appropriate tag (e.g., `0.18.0`, `0.18.0-tensorrt`, or `stable`): + ```bash + docker pull ghcr.io/blakeblackshear/frigate:0.18.0 + ``` + +3. **Start the Container**: + - If using Docker Compose: + ```bash + docker compose up -d + ``` + - If using `docker run`, re-run your original command (e.g., from the [Installation](./installation.md#docker) section) with the updated image tag. + +4. **Verify the Update**: + - Check the container logs to ensure Frigate starts successfully: + ```bash + docker logs frigate + ``` + - Visit the Frigate Web UI (default: `http://:5000`) to confirm the new version is running. The version number is displayed at the top of the System Metrics page. + +### Notes + +- If you’ve customized other settings (e.g., `shm-size`), ensure they’re still appropriate after the update. +- Docker will automatically use the updated image when you restart the container, as long as you pulled the correct version. + +## Updating the Home Assistant App (formerly Addon) + +For users running Frigate as a Home Assistant App: + +1. **Check for Updates**: + - Navigate to **Settings > Apps** in Home Assistant. + - Find your installed Frigate app (e.g., "Frigate NVR" or "Frigate NVR (Full Access)"). + - If an update is available, you’ll see an "Update" button. + +2. **Update the App**: + - Make a backup of the current version of the app. + - Click the "Update" button next to the Frigate app. + - Wait for the process to complete. Home Assistant will handle downloading and installing the new version. + +3. **Restart the App**: + - After updating, go to the app’s page and click "Restart" to apply the changes. + +4. **Verify the Update**: + - Check the app logs (under the "Log" tab) to ensure Frigate starts without errors. + - Access the Frigate Web UI to confirm the new version is running. + +### Notes + +- Ensure your `/config/frigate.yml` is compatible with the new version by reviewing the [Release notes](https://github.com/blakeblackshear/frigate/releases). +- If using custom hardware (e.g., Coral or GPU), verify that configurations still work, as app updates don’t modify your hardware settings. + +## Rolling Back + +If an update causes issues: + +1. Stop Frigate. +2. Restore your backed-up config file and database. +3. Revert to the previous image version: + - For Docker: Specify an older tag (e.g., `ghcr.io/blakeblackshear/frigate:0.17.1`) in your `docker run` command. + - For Docker Compose: Edit your `docker-compose.yml`, specify the older version tag (e.g., `ghcr.io/blakeblackshear/frigate:0.16.4`), and re-run `docker compose up -d`. + - For Home Assistant: Restore from the app/addon backup you took before you updated. +4. Verify the old version is running again. + +## Troubleshooting + +- **Container Fails to Start**: Check logs (`docker logs frigate`) for errors. +- **UI Not Loading**: Ensure ports (e.g., 5000, 8971) are still mapped correctly and the service is running. +- **Hardware Issues**: Revisit hardware-specific setup (e.g., Coral, GPU) if detection or decoding fails post-update. + +Common questions are often answered in the [FAQ](https://github.com/blakeblackshear/frigate/discussions), pinned at the top of the support discussions. diff --git a/docs/docs/frigate/video_pipeline.md b/docs/docs/frigate/video_pipeline.md new file mode 100644 index 0000000..74b804b --- /dev/null +++ b/docs/docs/frigate/video_pipeline.md @@ -0,0 +1,67 @@ +--- +id: video_pipeline +title: Video pipeline +--- + +Frigate uses a sophisticated video pipeline that starts with the camera feed and progressively applies transformations to it (e.g. decoding, motion detection, etc.). + +This guide provides an overview to help users understand some of the key Frigate concepts. + +## Overview + +At a high level, there are five processing steps that could be applied to a camera feed + +```mermaid +%%{init: {"themeVariables": {"edgeLabelBackground": "transparent"}}}%% + +flowchart LR + Feed(Feed acquisition) --> Decode(Video decoding) + Decode --> Motion(Motion detection) + Motion --> Object(Object detection) + Feed --> Recording(Recording and visualization) + Motion --> Recording + Object --> Recording +``` + +As the diagram shows, all feeds first need to be acquired. Depending on the data source, it may be as simple as using FFmpeg to connect to an RTSP source via TCP or something more involved like connecting to an Apple Homekit camera using go2rtc. A single camera can produce a main (i.e. high resolution) and a sub (i.e. lower resolution) video feed. + +Typically, the sub-feed will be decoded to produce full-frame images. As part of this process, the resolution may be downscaled and an image sampling frequency may be imposed (e.g. keep 5 frames per second). + +These frames will then be compared over time to detect movement areas (a.k.a. motion boxes). These motion boxes are combined into motion regions and are analyzed by a machine learning model to detect known objects. Finally, the snapshot and recording retention config will decide what video clips and events should be saved. + +## Detailed view of the video pipeline + +The following diagram adds a lot more detail than the simple view explained before. The goal is to show the detailed data paths between the processing steps. + +```mermaid +%%{init: {"themeVariables": {"edgeLabelBackground": "transparent"}}}%% + +flowchart TD + RecStore[(Recording
store)] + SnapStore[(Snapshot
store)] + + subgraph Acquisition + Cam["Camera"] -->|FFmpeg supported| Stream + Cam -->|"Other streaming
protocols"| go2rtc + go2rtc("go2rtc") --> Stream + Stream[Capture main and
sub streams] --> |detect stream|Decode(Decode and
downscale) + end + subgraph Motion + Decode --> MotionM(Apply
motion masks) + MotionM --> MotionD(Motion
detection) + end + subgraph Detection + MotionD --> |motion regions| ObjectD(Object detection) + Decode --> ObjectD + ObjectD --> ObjectFilter(Apply object filters & zones) + ObjectFilter --> ObjectZ(Track objects) + end + Decode --> |decoded frames|Birdseye + MotionD --> |motion event|Birdseye + ObjectZ --> |object event|Birdseye + + MotionD --> |"video segments
(retain motion)"|RecStore + ObjectZ --> |detection clip|RecStore + Stream -->|"video segments
(retain all)"| RecStore + ObjectZ --> |detection snapshot|SnapStore +``` diff --git a/docs/docs/guides/getting_started.md b/docs/docs/guides/getting_started.md new file mode 100644 index 0000000..fe355ab --- /dev/null +++ b/docs/docs/guides/getting_started.md @@ -0,0 +1,407 @@ +--- +id: getting_started +title: Getting started +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +# Getting Started + +:::tip + +If you already have an environment with Linux and Docker installed, you can continue to [Installing Frigate](#installing-frigate) below. + +If you already have Frigate installed through Docker or through a Home Assistant App, you can continue to [Configuring Frigate](#configuring-frigate) below. + +::: + +## Setting up hardware + +This section guides you through setting up a server with Debian Bookworm and Docker. + +### Install Debian 12 (Bookworm) + +There are many guides on how to install Debian Server, so this will be an abbreviated guide. Connect a temporary monitor and keyboard to your device so you can install a minimal server without a desktop environment. + +#### Prepare installation media + +1. Download the small installation image from the [Debian website](https://www.debian.org/distrib/netinst) +1. Flash the ISO to a USB device (popular tool is [balena Etcher](https://etcher.balena.io/)) +1. Boot your device from USB + +#### Install and setup Debian for remote access + +1. Ensure your device is connected to the network so updates and software options can be installed +1. Choose the non-graphical install option if you don't have a mouse connected, but either install method works fine +1. You will be prompted to set the root user password and create a user with a password +1. Install the minimum software. Fewer dependencies result in less maintenance. + 1. Uncheck "Debian desktop environment" and "GNOME" + 1. Check "SSH server" + 1. Keep "standard system utilities" checked +1. After reboot, login as root at the command prompt to add user to sudoers + 1. Install sudo + ```bash + apt update && apt install -y sudo + ``` + 1. Add the user you created to the sudo group (change `blake` to your own user) + ```bash + usermod -aG sudo blake + ``` +1. Shutdown by running `poweroff` + +At this point, you can install the device in a permanent location. The remaining steps can be performed via SSH from another device. If you don't have an SSH client, you can install one of the options listed in the [Visual Studio Code documentation](https://code.visualstudio.com/docs/remote/troubleshooting#_installing-a-supported-ssh-client). + +#### Finish setup via SSH + +1. Connect via SSH and login with your non-root user created during install +1. Setup passwordless sudo so you don't have to type your password for each sudo command (change `blake` in the command below to your user) + + ```bash + echo 'blake ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/user + ``` + +1. Logout and login again to activate passwordless sudo +1. Setup automatic security updates for the OS (optional) + 1. Ensure everything is up to date by running + ```bash + sudo apt update && sudo apt upgrade -y + ``` + 1. Install unattended upgrades + ```bash + sudo apt install -y unattended-upgrades + echo unattended-upgrades unattended-upgrades/enable_auto_updates boolean true | sudo debconf-set-selections + sudo dpkg-reconfigure -f noninteractive unattended-upgrades + ``` + +Now you have a minimal Debian server that requires very little maintenance. + +### Install Docker + +1. Install Docker Engine (not Docker Desktop) using the [official docs](https://docs.docker.com/engine/install/debian/) + 1. Specifically, follow the steps in the [Install using the apt repository](https://docs.docker.com/engine/install/debian/#install-using-the-repository) section +2. Add your user to the docker group as described in the [Linux postinstall steps](https://docs.docker.com/engine/install/linux-postinstall/) + +## Installing Frigate + +This section shows how to create a minimal directory structure for a Docker installation on Debian. If you have installed Frigate as a Home Assistant App or another way, you can continue to [Configuring Frigate](#configuring-frigate). + +### Setup directories + +Frigate will create a config file if one does not exist on the initial startup. The following directory structure is the bare minimum to get started. + +``` +. +├── docker-compose.yml +├── config/ +└── storage/ +``` + +This will create the above structure: + +```bash +mkdir storage config && touch docker-compose.yml +``` + +If you are setting up Frigate on a Linux device via SSH, you can use [nano](https://itsfoss.com/nano-editor-guide/) to edit the following files. If you prefer to edit remote files with a full editor instead of a terminal, I recommend using [Visual Studio Code](https://code.visualstudio.com/) with the [Remote SSH extension](https://code.visualstudio.com/docs/remote/ssh-tutorial). + +:::note + +This `docker-compose.yml` file is just a starter for amd64 devices. You will need to customize it for your setup as detailed in the [Installation docs](/frigate/installation#docker). + +::: +`docker-compose.yml` + +```yaml +services: + frigate: + container_name: frigate + restart: unless-stopped + stop_grace_period: 30s + image: ghcr.io/blakeblackshear/frigate:stable + volumes: + - ./config:/config + - ./storage:/media/frigate + - type: tmpfs # 1GB In-memory filesystem for recording segment storage + target: /tmp/cache + tmpfs: + size: 1000000000 + ports: + - "8971:8971" + - "8554:8554" # RTSP feeds +``` + +Now you should be able to start Frigate by running `docker compose up -d` from within the folder containing `docker-compose.yml`. On startup, an admin user and password will be created and outputted in the logs. You can see this by running `docker logs frigate`. Frigate should now be accessible at `https://server_ip:8971` where you can login with the `admin` user and finish configuration using the Settings UI. + +## Configuring Frigate + +This section assumes that you already have an environment setup as described in [Installation](../frigate/installation.md). You should also configure your cameras according to the [camera setup guide](/frigate/camera_setup). Pay particular attention to the section on choosing a detect resolution. + +### Step 1: Start Frigate + +At this point you should be able to start Frigate and a basic config will be created automatically. + +### Step 2: Add a camera + +Click the **Add Camera** button in to use the camera setup wizard to get your first camera added into Frigate. + +### Step 3: Configure hardware acceleration (recommended) + +Now that you have a working camera configuration, set up hardware acceleration to minimize the CPU required to decode your video streams. See the [hardware acceleration](../configuration/hardware_acceleration_video.md) docs for examples applicable to your hardware. + +:::note + +Hardware acceleration requires passing the appropriate device to the Docker container. For Intel and AMD GPUs, add the device to your `docker-compose.yml`: + +```yaml {4,5} +services: + frigate: + ... + devices: + - /dev/dri/renderD128:/dev/dri/renderD128 # for intel & amd hwaccel, needs to be updated for your hardware + ... +``` + +After modifying, run `docker compose up -d` to apply changes. + +::: + + + + +Navigate to and set **Hardware acceleration arguments** to the appropriate preset for your hardware (e.g., `VAAPI (Intel/AMD GPU)` for most Intel processors). + + + + +```yaml +mqtt: ... + +cameras: + name_of_your_camera: + ffmpeg: + inputs: ... + # highlight-next-line + hwaccel_args: preset-vaapi + detect: ... +``` + + + + +### Step 4: Configure detectors + +By default, Frigate will use a single OpenVINO detector running on the CPU. + +In many cases, the integrated graphics on Intel CPUs provides sufficient performance for typical Frigate setups. If you have an Intel processor, you can follow the configuration below. + +
+ Use Intel OpenVINO detector + +You need to refer to **Configure hardware acceleration** above to enable the container to use the GPU. + + + + +1. Navigate to and add a detector with **Type** `OpenVINO` and **Device** `GPU` +2. On the same page, in the **Custom Model** tab, configure the model settings for OpenVINO: + +| Field | Value | +| ---------------------------------------- | ------------------------------------------ | +| **Object detection model input width** | `300` | +| **Object detection model input height** | `300` | +| **Model Input Tensor Shape** | `nhwc` | +| **Model Input Pixel Color Format** | `bgr` | +| **Custom object detector model path** | `/openvino-model/ssdlite_mobilenet_v2.xml` | +| **Label map for custom object detector** | `/openvino-model/coco_91cl_bkgr.txt` | + + + + +```yaml {3-6,9-15,20-21} +mqtt: ... + +detectors: # <---- add detectors + ov: + type: openvino # <---- use openvino detector + device: GPU + +# We will use the default MobileNet_v2 model from OpenVINO. +model: + width: 300 + height: 300 + input_tensor: nhwc + input_pixel_format: bgr + path: /openvino-model/ssdlite_mobilenet_v2.xml + labelmap_path: /openvino-model/coco_91cl_bkgr.txt + +cameras: + name_of_your_camera: + ffmpeg: ... + detect: + enabled: True # <---- turn on detection + ... +``` + + + + +
+ +If you have a USB Coral, you will need to add a detectors section to your config. + +
+ Use USB Coral detector + +:::note + +You need to pass the USB Coral device to the Docker container. Add the following to your `docker-compose.yml` and run `docker compose up -d`: + +```yaml {4-6} +services: + frigate: + ... + devices: + - /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions + - /dev/apex_0:/dev/apex_0 # passes a PCIe Coral, follow driver instructions here https://github.com/jnicolson/gasket-builder + ... +``` + +::: + + + + +Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb`. + + + + +```yaml {3-6,11-12} +mqtt: ... + +detectors: # <---- add detectors + coral: + type: edgetpu + device: usb + +cameras: + name_of_your_camera: + ffmpeg: ... + detect: + enabled: True # <---- turn on detection + ... +``` + + + + +
+ +More details on available detectors can be found [here](../configuration/object_detectors.md). + +Restart Frigate and you should start seeing detections for `person`. If you want to track other objects, they can be configured in or via the [configuration file reference](../configuration/advanced/reference.md). + +### Step 5: Setup motion masks + +Now that you have optimized your configuration for decoding the video stream, you will want to check to see where to implement motion masks. Click on the camera from the main dashboard, then select the gear icon in the top right, enable the [Debug view](/usage/live#the-single-camera-view), and finally enable the switch for Motion Boxes. Watch for areas that continuously trigger unwanted motion to be detected. Common areas to mask include camera timestamps and trees that frequently blow in the wind. The goal is to avoid wasting object detection cycles looking at these areas. + +Use the mask editor to draw polygon masks directly on the camera feed. Navigate to and set up a motion mask over the area. More information about masks can be found [here](../configuration/masks.md). + +:::warning + +Note that motion masks should not be used to mark out areas where you do not want objects to be detected or to reduce false positives. They do not alter the image sent to object detection, so you can still get tracked objects, alerts, and detections in areas with motion masks. These only prevent motion in these areas from initiating object detection. + +::: + +If you are using YAML to configure Frigate instead of the UI, your configuration should look similar to this now: + +```yaml {16-18} +mqtt: + enabled: False + +detectors: + coral: + type: edgetpu + device: usb + +cameras: + name_of_your_camera: + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp + roles: + - detect + motion: + mask: + motion_area: + friendly_name: "Motion mask" + enabled: true + coordinates: "0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432" +``` + +### Step 6: Enable recordings + +In order to review activity in the Frigate UI, recordings need to be enabled. + + + + +1. If you have separate streams for detect and record, navigate to , select your camera, and add a second input with the `record` role pointing to your high-resolution stream +2. Navigate to (or for a specific camera) and set **Enable recording** to on + + + + +```yaml {16-17} +mqtt: ... + +detectors: ... + +cameras: + name_of_your_camera: + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp + roles: + - detect + - path: rtsp://10.0.10.10:554/high_res_stream # <----- Add stream you want to record from + roles: + - record + detect: ... + record: # <----- Enable recording + enabled: True + motion: ... +``` + + + + +If you don't have separate streams for detect and record, you would just add the record role to the list on the first input. + +:::note + +If you only define one stream in your `inputs` and do not assign a `detect` role to it, Frigate will automatically assign it the `detect` role. Frigate will always decode a stream to support motion detection, Birdseye, the API image endpoints, and other features, even if you have disabled object detection with `enabled: False` in your config's `detect` section. + +If you only plan to use Frigate for recording, it is still recommended to define a `detect` role for a low resolution stream to minimize resource usage from the required stream decoding. + +::: + +By default, Frigate will retain video of all tracked objects for 10 days. The full set of options for recording can be found [here](../configuration/advanced/reference.md). + +### Step 7: Complete config + +At this point you have a complete config with basic functionality. + +- View [common configuration examples](../configuration/config.md#common-configuration-examples) for a list of common configuration examples. +- View [full config reference](../configuration/advanced/reference.md) for a complete list of configuration options. + +### Follow up + +Now that you have a working install, you can use the following documentation for additional features: + +1. [Zones](../configuration/zones.md) +2. [Review](../configuration/review.md) +3. [Masks](../configuration/masks.md) +4. [Home Assistant Integration](../integrations/home-assistant.md) - Integrate with Home Assistant diff --git a/docs/docs/guides/ha_network_storage.md b/docs/docs/guides/ha_network_storage.md new file mode 100644 index 0000000..134e195 --- /dev/null +++ b/docs/docs/guides/ha_network_storage.md @@ -0,0 +1,40 @@ +--- +id: ha_network_storage +title: Home Assistant network storage +--- + +As of Home Assistant 2023.6, Network Mounted Storage is supported for Apps. + +## Setting Up Remote Storage For Frigate + +### Prerequisites + +- Home Assistant 2023.6 or newer is installed +- Running Home Assistant Operating System 10.2 or newer OR Running Supervised with latest os-agent installed (this is required for supervised install) + +### Initial Setup + +1. Stop the Frigate App + +### Move current data + +Keeping the current data is optional, but the data will need to be moved regardless so the share can be created successfully. + +#### If you want to keep the current data + +1. Move the frigate.db, frigate.db-shm, frigate.db-wal files to the /config directory +2. Rename the /media/frigate folder to /media/frigate_tmp + +#### If you don't want to keep the current data + +1. Delete the /media/frigate folder and all of its contents + +### Create the media share + +1. Go to **Settings -> System -> Storage -> Add Network Storage** +2. Name the share `frigate` (this is required) +3. Choose type `media` +4. Fill out the additional required info for your particular NAS +5. Connect +6. Move files from `/media/frigate_tmp` to `/media/frigate` if they were kept in previous step +7. Start the Frigate App diff --git a/docs/docs/guides/ha_notifications.md b/docs/docs/guides/ha_notifications.md new file mode 100644 index 0000000..a92dab1 --- /dev/null +++ b/docs/docs/guides/ha_notifications.md @@ -0,0 +1,50 @@ +--- +id: ha_notifications +title: Home Assistant notifications +--- + +The best way to get started with notifications for Frigate is to use the [Blueprint](https://community.home-assistant.io/t/frigate-mobile-app-notifications-2-0/559732). You can use the yaml generated from the Blueprint as a starting point and customize from there. + +It is generally recommended to trigger notifications based on the `frigate/reviews` mqtt topic. This provides the event_id(s) needed to fetch [thumbnails/snapshots/clips](../integrations/home-assistant.md#notification-api) and other useful information to customize when and where you want to receive alerts. The data is published in the form of a change feed, which means you can reference the "previous state" of the object in the `before` section and the "current state" of the object in the `after` section. You can see an example [here](../integrations/mqtt.md#frigateevents). + +Here is a simple example of a notification automation of tracked objects which will update the existing notification for each change. This means the image you see in the notification will update as Frigate finds a "better" image. + +```yaml +automation: + - alias: Notify of tracked object + trigger: + platform: mqtt + topic: frigate/events + action: + - service: notify.mobile_app_pixel_3 + data: + message: 'A {{trigger.payload_json["after"]["label"]}} was detected.' + data: + image: 'https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg?format=android' + tag: '{{trigger.payload_json["after"]["id"]}}' + when: '{{trigger.payload_json["after"]["start_time"]|int}}' +``` + +Note that iOS devices support live previews of cameras by adding a camera entity id to the message data. + +```yaml +automation: + - alias: Security_Frigate_Notifications + description: "" + trigger: + - platform: mqtt + topic: frigate/reviews + payload: alert + value_template: "{{ value_json['after']['severity'] }}" + action: + - service: notify.mobile_app_iphone + data: + message: 'A {{trigger.payload_json["after"]["data"]["objects"] | sort | join(", ") | title}} was detected.' + data: + image: >- + https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["data"]["detections"][0]}}/thumbnail.jpg + tag: '{{trigger.payload_json["after"]["id"]}}' + when: '{{trigger.payload_json["after"]["start_time"]|int}}' + entity_id: camera.{{trigger.payload_json["after"]["camera"] | replace("-","_") | lower}} + mode: single +``` diff --git a/docs/docs/guides/reverse_proxy.md b/docs/docs/guides/reverse_proxy.md new file mode 100644 index 0000000..bf33bcc --- /dev/null +++ b/docs/docs/guides/reverse_proxy.md @@ -0,0 +1,220 @@ +--- +id: reverse_proxy +title: Setting up a reverse proxy +--- + +This guide outlines the basic configuration steps needed to set up a reverse proxy in front of your Frigate instance. + +A reverse proxy is typically needed if you want to set up Frigate on a custom URL, on a subdomain, or on a host serving multiple sites. It could also be used to set up your own authentication provider or for more advanced HTTP routing. + +Before setting up a reverse proxy, check if any of the built-in functionality in Frigate suits your needs: +|Topic|Docs| +|-|-| +|TLS|Please see the `tls` [configuration option](../configuration/tls.md)| +|Authentication|Please see the [authentication](../configuration/authentication.md) documentation| +|IPv6|[Enabling IPv6](../configuration/advanced/system.md#enabling-ipv6) + +**Note about TLS** +When using a reverse proxy, the TLS session is usually terminated at the proxy, sending the internal request over plain HTTP. If this is the desired behavior, TLS must first be disabled in Frigate, or you will encounter an HTTP 400 error: "The plain HTTP request was sent to HTTPS port." +To disable TLS, set the following in your Frigate configuration: + +```yml +tls: + enabled: false +``` + +:::warning +A reverse proxy can be used to secure access to an internal web server, but the user will be entirely reliant on the steps they have taken. You must ensure you are following security best practices. +This page does not attempt to outline the specific steps needed to secure your internal website. +Please use your own knowledge to assess and vet the reverse proxy software before you install anything on your system. +::: + +## WebSocket support + +Frigate relies on WebSockets for real-time communication between the browser and the backend. Features such as camera controls (enabling/disabling a camera, audio, detect, recordings, and other toggles), live stream playback, and other live-updating parts of the UI will not function correctly if WebSocket connections are not proxied. + +Your reverse proxy must be configured to forward the `Upgrade` and `Connection` headers so that WebSocket connections can be established. Each proxy example below already includes the directives needed to do this, but if you are adapting your own configuration, ensure these headers are passed through. + +Note that some proxies disable WebSocket support by default. For example, Nginx Proxy Manager has a "Websockets Support" toggle that must be enabled. + +## Proxies + +There are many solutions available to implement reverse proxies and the community is invited to help out documenting others through a contribution to this page. + +- [Apache2](#apache2-reverse-proxy) +- [Nginx](#nginx-reverse-proxy) +- [Traefik](#traefik-reverse-proxy) +- [Caddy](#caddy-reverse-proxy) + +## Apache2 Reverse Proxy + +In the configuration examples below, only the directives relevant to the reverse proxy approach above are included. +On Debian Apache2 the configuration file will be named along the lines of `/etc/apache2/sites-available/cctv.conf` + +### Step 1: Configure the Apache2 Reverse Proxy + +Make life easier for yourself by presenting your Frigate interface as a DNS sub-domain rather than as a sub-folder of your main domain. +Here we access Frigate via https://cctv.mydomain.co.uk + +```xml + + ServerName cctv.mydomain.co.uk + + ProxyPreserveHost On + ProxyPass "/" "http://frigatepi.local:8971/" + ProxyPassReverse "/" "http://frigatepi.local:8971/" + + ProxyPass /ws ws://frigatepi.local:8971/ws + ProxyPassReverse /ws ws://frigatepi.local:8971/ws + + ProxyPass /live/ ws://frigatepi.local:8971/live/ + ProxyPassReverse /live/ ws://frigatepi.local:8971/live/ + + RewriteEngine on + RewriteCond %{HTTP:Upgrade} =websocket [NC] + RewriteRule /(.*) ws://frigatepi.local:8971/$1 [P,L] + RewriteCond %{HTTP:Upgrade} !=websocket [NC] + RewriteRule /(.*) http://frigatepi.local:8971/$1 [P,L] + +``` + +### Step 2: Use SSL to encrypt access to your Frigate instance + +Whilst this won't, on its own, prevent access to your Frigate webserver it will encrypt all content (such as login credentials). +Installing SSL is beyond the scope of this document but [Let's Encrypt](https://letsencrypt.org/) is a widely used approach. +This Apache2 configuration snippet then results in unencrypted requests being redirected to the webserver SSL port + +```xml + +ServerName cctv.mydomain.co.uk +RewriteEngine on +RewriteCond %{SERVER_NAME} =cctv.mydomain.co.uk +RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] + +``` + +### Step 3: Authenticate users at the proxy + +There are many ways to authenticate a website but a straightforward approach is to use [Apache2 password files](https://httpd.apache.org/docs/2.4/howto/auth.html). + +```xml + + + AuthType Basic + AuthName "Restricted Files" + AuthUserFile "/var/www/passwords" + Require user paul + + +``` + +## Nginx Reverse Proxy + +This method shows a working example for subdomain type reverse proxy with SSL enabled. + +### Setup server and port to reverse proxy + +This is set in `$server` and `$port` this should match your ports you have exposed to your docker container. Optionally you listen on port `443` and enable `SSL` + +``` +# ------------------------------------------------------------ +# frigate.domain.com +# ------------------------------------------------------------ + +server { + set $forward_scheme http; + set $server "192.168.100.2"; # FRIGATE SERVER LOCATION + set $port 8971; + + listen 80; + listen 443 ssl; + http2 on; + + server_name frigate.domain.com; +} +``` + +### Setup SSL (optional) + +This section points to your SSL files, the example below shows locations to a default Lets Encrypt SSL certificate. + +``` + # Let's Encrypt SSL + include conf.d/include/letsencrypt-acme-challenge.conf; + include conf.d/include/ssl-ciphers.conf; + ssl_certificate /etc/letsencrypt/live/npm-1/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/npm-1/privkey.pem; +``` + +### Setup reverse proxy settings + +The settings below enabled connection upgrade, sets up logging (optional) and proxies everything from the `/` context to the docker host and port specified earlier in the configuration + +``` + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + + access_log /data/logs/proxy-host-40_access.log proxy; + error_log /data/logs/proxy-host-40_error.log warn; + + location / { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + } + +``` + +## Traefik Reverse Proxy + +This example shows how to add a `label` to the Frigate Docker compose file, enabling Traefik to automatically discover your Frigate instance. +Before using the example below, you must first set up Traefik with the [Docker provider](https://doc.traefik.io/traefik/providers/docker/) + +```yml +services: + frigate: + container_name: frigate + image: ghcr.io/blakeblackshear/frigate:stable + ... + ... + labels: + - "traefik.enable=true" + - "traefik.http.services.frigate.loadbalancer.server.port=8971" + - "traefik.http.routers.frigate.rule=Host(`traefik.example.com`)" +``` + +The above configuration will create a "service" in Traefik, automatically adding your container's IP on port 8971 as a backend. +It will also add a router, routing requests to "traefik.example.com" to your local container. + +Note that with this approach, you don't need to expose any ports for the Frigate instance since all traffic will be routed over the internal Docker network. + +## Caddy Reverse Proxy + +This example shows Frigate running under a subdomain with logging and a tls cert (in this case a wildcard domain cert obtained independently of caddy) handled via imports + +```caddy +(logging) { + log { + output file /var/log/caddy/{args[0]}.log { + roll_size 10MiB + roll_keep 5 + roll_keep_for 10d + } + format json + level INFO + } +} + + +(tls) { + tls /var/lib/caddy/wildcard.YOUR_DOMAIN.TLD.fullchain.pem /var/lib/caddy/wildcard.YOUR_DOMAIN.TLD.privkey.pem +} + +frigate.YOUR_DOMAIN.TLD { + reverse_proxy http://localhost:8971 + import tls + import logging frigate.YOUR_DOMAIN.TLD +} + +``` diff --git a/docs/docs/integrations/api.md b/docs/docs/integrations/api.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/docs/integrations/home-assistant.md b/docs/docs/integrations/home-assistant.md new file mode 100644 index 0000000..4d90b7e --- /dev/null +++ b/docs/docs/integrations/home-assistant.md @@ -0,0 +1,352 @@ +--- +id: home-assistant +title: Home Assistant Integration +--- + +The best way to integrate with Home Assistant is to use the [official integration](https://github.com/blakeblackshear/frigate-hass-integration). + +## Installation + +### Preparation + +The Frigate integration requires the `mqtt` integration to be installed and +manually configured first. + +See the [MQTT integration +documentation](https://www.home-assistant.io/integrations/mqtt/) for more +details. + +In addition, MQTT must be enabled in your Frigate configuration file and Frigate must be connected to the same MQTT server as Home Assistant for many of the entities created by the integration to function, e.g.: + +```yaml +mqtt: + enabled: True + host: mqtt.server.com # the address of your HA server that's running the MQTT integration + user: your_mqtt_broker_username + password: your_mqtt_broker_password +``` + +### Integration installation + +Available via HACS as a default repository. To install: + +- Use [HACS](https://hacs.xyz/) to install the integration: + +``` +Home Assistant > HACS > Click in the Search bar and type "Frigate" > Frigate +``` + +- Restart Home Assistant. +- Then add/configure the integration: + +``` +Home Assistant > Settings > Devices & Services > Add Integration > Frigate +``` + +Note: You will also need +[media_source](https://www.home-assistant.io/integrations/media_source/) enabled +in your Home Assistant configuration for the Media Browser to appear. + +### (Optional) Lovelace Card Installation + +To install the optional companion Lovelace card, please see the [separate +installation instructions](https://github.com/dermotduffy/frigate-hass-card) for +that card. + +## Configuration + +When configuring the integration, you will be asked for the `URL` of your Frigate instance which can be pointed at the internal unauthenticated port (`5000`) or the authenticated port (`8971`) for your instance. This may look like `http://:5000/`. + +### Docker Compose Examples + +If you are running Home Assistant and Frigate with Docker Compose on the same device, here are some examples. + +#### Home Assistant running with host networking + +It is not recommended to run Frigate in host networking mode. In this example, you would use `http://172.17.0.1:5000` or `http://172.17.0.1:8971` when configuring the integration. + +```yaml +services: + homeassistant: + image: ghcr.io/home-assistant/home-assistant:stable + network_mode: host + ... + + frigate: + image: ghcr.io/blakeblackshear/frigate:stable + ... + ports: + - "172.17.0.1:5000:5000" + ... +``` + +#### Home Assistant _not_ running with host networking or in a separate compose file + +In this example, it is recommended to connect to the authenticated port, for example, `http://frigate:8971` when configuring the integration. There is no need to map the port for the Frigate container. + +```yaml +services: + homeassistant: + image: ghcr.io/home-assistant/home-assistant:stable + # network_mode: host + ... + + frigate: + image: ghcr.io/blakeblackshear/frigate:stable + ... + ports: + # - "172.17.0.1:5000:5000" + ... +``` + +### Home Assistant App + +If you are using Home Assistant App, the URL should be one of the following depending on which App variant you are using. Note that if you are using the Proxy App, you should NOT point the integration at the proxy URL. Just enter the same URL used to access Frigate directly from your network. + +| App Variant | URL | +| -------------------------- | -------------------------------------- | +| Frigate | `http://ccab4aaf-frigate:5000` | +| Frigate (Full Access) | `http://ccab4aaf-frigate-fa:5000` | +| Frigate Beta | `http://ccab4aaf-frigate-beta:5000` | +| Frigate Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` | + +### Frigate running on a separate machine + +If you run Frigate on a separate device within your local network, Home Assistant will need access to port 8971. + +#### Local network + +Use `http://:8971` as the URL for the integration so that authentication is required. + +:::tip + +The above URL assumes you have [disabled TLS](../configuration/tls). +By default, TLS is enabled and Frigate will be using a self-signed certificate. HomeAssistant will fail to connect HTTPS to port 8971 since it fails to verify the self-signed certificate. +Either disable TLS and use HTTP from HomeAssistant, or configure Frigate to be accessible with a valid certificate. + +::: + +```yaml +services: + frigate: + image: ghcr.io/blakeblackshear/frigate:stable + ... + ports: + - "8971:8971" + ... +``` + +#### Tailscale or other private networking + +Use `http://:5000` as the URL for the integration. + +```yaml +services: + frigate: + image: ghcr.io/blakeblackshear/frigate:stable + ... + ports: + - ":5000:5000" + ... +``` + +## Options + +``` +Home Assistant > Configuration > Integrations > Frigate > Options +``` + +| Option | Description | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| RTSP URL Template | A [jinja2](https://jinja.palletsprojects.com/) template that is used to override the standard RTSP stream URL (e.g. for use with reverse proxies). This option is only shown to users who have [advanced mode](https://www.home-assistant.io/blog/2019/07/17/release-96/#advanced-mode) enabled. See [RTSP streams](#rtsp-stream) below. | + +## Entities Provided + +| Platform | Description | +| --------------- | ------------------------------------------------------------------------------- | +| `camera` | Live camera stream (requires RTSP). | +| `image` | Image of the latest detected object for each camera. | +| `sensor` | States to monitor Frigate performance, object counts for all zones and cameras. | +| `switch` | Switch entities to toggle detection, recordings and snapshots. | +| `binary_sensor` | A "motion" binary sensor entity per camera/zone/object. | + +## Media Browser Support + +The integration provides: + +- Browsing tracked object recordings with thumbnails +- Browsing snapshots +- Browsing recordings by month, day, camera, time + +This is accessible via "Media Browser" on the left menu panel in Home Assistant. + +## Casting Clips To Media Devices + +The integration supports casting clips and camera streams to supported media devices. + +:::tip +For clips to be castable to media devices, audio is required and may need to be [enabled for recordings](../troubleshooting/faqs.md#audio-in-recordings). + +**NOTE: Even if you camera does not support audio, audio will need to be enabled for Casting to be accepted.** + +::: + + + +## Camera API + +To turn a camera off (pauses Frigate's processing of the stream; does not persist across Frigate restarts; see [Camera state](/configuration/live#camera-state)): + +``` +action: camera.turn_off +data: {} +target: + entity_id: camera.back_deck_cam # your Frigate camera entity ID +``` + +To turn a camera back on: + +``` +action: camera.turn_on +data: {} +target: + entity_id: camera.back_deck_cam # your Frigate camera entity ID +``` + +:::note + +These actions toggle Frigate's runtime On/Off state. To permanently disable a camera, set its status to **Disabled** in **Settings → Camera Management** in the Frigate UI. + +::: + +## Notification API + +Many people do not want to expose Frigate to the web, so the integration creates some public API endpoints that can be used for notifications. + +To load a thumbnail for a tracked object: + +``` +https://HA_URL/api/frigate/notifications//thumbnail.jpg +``` + +To load a snapshot for a tracked object: + +``` +https://HA_URL/api/frigate/notifications//snapshot.jpg +``` + +To load a video clip of a tracked object using an Android device: + +``` +https://HA_URL/api/frigate/notifications//clip.mp4 +``` + +To load a video clip of a tracked object using an iOS device: + +``` +https://HA_URL/api/frigate/notifications//master.m3u8 +``` + +To load a preview gif of a tracked object: + +``` +https://HA_URL/api/frigate/notifications//event_preview.gif +``` + +To load a preview gif of a review item: + +``` +https://HA_URL/api/frigate/notifications//review_preview.gif +``` + +To load the thumbnail of a review item: + +``` +https://HA_URL/api/frigate/notifications///review_thumbnail.webp +``` + + + +## RTSP stream + +In order for the live streams to function they need to be accessible on the RTSP +port (default: `8554`) at `:8554`. Home Assistant will directly +connect to that streaming port when the live camera is viewed. + +#### RTSP URL Template + +For advanced usecases, this behavior can be changed with the [RTSP URL +template](#options) option. When set, this string will override the default stream +address that is derived from the default behavior described above. This option supports +[jinja2 templates](https://jinja.palletsprojects.com/) and has the `camera` dict +variables from [Frigate API](../integrations/api) +available for the template. Note that no Home Assistant state is available to the +template, only the camera dict from Frigate. + +This is potentially useful when Frigate is behind a reverse proxy, and/or when +the default stream port is otherwise not accessible to Home Assistant (e.g. +firewall rules). + +###### RTSP URL Template Examples + +Use a different port number: + +``` +rtsp://:2000/front_door +``` + +Use the camera name in the stream URL: + +``` +rtsp://:2000/{{ name }} +``` + +Use the camera name in the stream URL, converting it to lowercase first: + +``` +rtsp://:2000/{{ name|lower }} +``` + +## Multiple Instance Support + +The Frigate integration seamlessly supports the use of multiple Frigate servers. + +### Requirements for Multiple Instances + +In order for multiple Frigate instances to function correctly, the +`topic_prefix` and `client_id` parameters must be set differently per server. +See [MQTT +configuration](mqtt) +for how to set these. + +#### API URLs + +When multiple Frigate instances are configured, [API](#notification-api) URLs should include an +identifier to tell Home Assistant which Frigate instance to refer to. The +identifier used is the MQTT `client_id` parameter included in the configuration, +and is used like so: + +``` +https://HA_URL/api/frigate//notifications//thumbnail.jpg +``` + +``` +https://HA_URL/api/frigate//clips/front_door-1624599978.427826-976jaa.mp4 +``` + +#### Default Treatment + +When a single Frigate instance is configured, the `client-id` parameter need not +be specified in URLs/identifiers -- that single instance is assumed. When +multiple Frigate instances are configured, the user **must** explicitly specify +which server they are referring to. + +## FAQ + +#### If I am detecting multiple objects, how do I assign the correct `binary_sensor` to the camera in HomeKit? + +The [HomeKit integration](https://www.home-assistant.io/integrations/homekit/) randomly links one of the binary sensors (motion sensor entities) grouped with the camera device in Home Assistant. You can specify a `linked_motion_sensor` in the Home Assistant [HomeKit configuration](https://www.home-assistant.io/integrations/homekit/#linked_motion_sensor) for each camera. + +#### I have set up automations based on the occupancy sensors. Sometimes the automation runs because the sensors are turned on, but then I look at Frigate I can't find the object that triggered the sensor. Is this a bug? + +No. The occupancy sensors have fewer checks in place because they are often used for things like turning the lights on where latency needs to be as low as possible. So false positives can sometimes trigger these sensors. If you want false positive filtering, you should use an mqtt sensor on the `frigate/events` or `frigate/reviews` topic. diff --git a/docs/docs/integrations/homekit.md b/docs/docs/integrations/homekit.md new file mode 100644 index 0000000..5954af4 --- /dev/null +++ b/docs/docs/integrations/homekit.md @@ -0,0 +1,37 @@ +--- +id: homekit +title: HomeKit +--- + +Frigate cameras can be integrated with Apple HomeKit through go2rtc. This allows you to view your camera streams directly in the Apple Home app on your iOS, iPadOS, macOS, and tvOS devices. + +## Overview + +HomeKit integration is handled entirely through go2rtc, which is embedded in Frigate. go2rtc provides the necessary HomeKit Accessory Protocol (HAP) server to expose your cameras to HomeKit. + +## Setup + +All HomeKit configuration and pairing should be done through the **go2rtc WebUI**. + +### Accessing the go2rtc WebUI + +The go2rtc WebUI is available at: + +``` +http://:1984 +``` + +Replace `` with the IP address or hostname of your Frigate server. + +### Pairing Cameras + +1. Navigate to the go2rtc WebUI at `http://:1984` +2. Use the `add` section to add a new camera to HomeKit +3. Follow the on-screen instructions to generate pairing codes for your cameras + +## Requirements + +- Frigate must be accessible on your local network using host network_mode +- Your iOS device must be on the same network as Frigate +- Port 1984 must be accessible for the go2rtc WebUI +- For detailed go2rtc configuration options, refer to the [go2rtc documentation](https://github.com/AlexxIT/go2rtc) diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md new file mode 100644 index 0000000..e929ade --- /dev/null +++ b/docs/docs/integrations/mqtt.md @@ -0,0 +1,571 @@ +--- +id: mqtt +title: MQTT +--- + +These are the MQTT messages generated by Frigate. The default topic_prefix is `frigate`, but can be changed in the config file. + +:::info + +MQTT requires a network connection to your broker. This is typically local, but will require internet if using a cloud-hosted MQTT broker. See [Network Requirements](/frigate/network_requirements#mqtt) for details. + +::: + +## General Frigate Topics + +### `frigate/available` + +Designed to be used as an availability topic with Home Assistant. Possible message are: +"online": published when Frigate is running (on startup) +"stopped": published when Frigate is stopped normally +"offline": published automatically by the MQTT broker if Frigate disconnects unexpectedly (via MQTT Will Message) + +### `frigate/restart` + +Causes Frigate to exit. Docker should be configured to automatically restart the container on exit. + +### `frigate/events` + +Message published for each changed tracked object. The first message is published when the tracked object is no longer marked as a false_positive. When Frigate finds a better snapshot of the tracked object or when a zone change occurs, it will publish a message with the same id. When the tracked object ends, a final message is published with `end_time` set. + +```json +{ + "type": "update", // new, update, end + "before": { + "id": "1607123955.475377-mxklsc", + "camera": "front_door", + "frame_time": 1607123961.837752, + "snapshot": { + "frame_time": 1607123965.975463, + "box": [415, 489, 528, 700], + "area": 12728, + "region": [260, 446, 660, 846], + "score": 0.77546, + "attributes": [] + }, + "label": "person", + "sub_label": null, + "top_score": 0.958984375, + "false_positive": false, + "start_time": 1607123955.475377, + "end_time": null, + "score": 0.7890625, + "box": [424, 500, 536, 712], + "area": 23744, + "ratio": 2.113207, + "region": [264, 450, 667, 853], + "current_zones": ["driveway"], + "entered_zones": ["yard", "driveway"], + "thumbnail": null, + "has_snapshot": false, + "has_clip": false, + "active": true, // convenience attribute, this is strictly opposite of "stationary" + "stationary": false, // whether or not the object is considered stationary + "motionless_count": 0, // number of frames the object has been motionless + "position_changes": 2, // number of times the object has moved from a stationary position + "attributes": { + "face": 0.64 + }, // attributes with top score that have been identified on the object at any point + "current_attributes": [], // detailed data about the current attributes in this frame + "current_estimated_speed": 0.71, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled + "average_estimated_speed": 14.3, // average estimated speed (mph or kph) for objects moving through zones with speed estimation enabled + "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "recognized_license_plate": "ABC12345", // a recognized license plate for car objects + "recognized_license_plate_score": 0.933451 + }, + "after": { + "id": "1607123955.475377-mxklsc", + "camera": "front_door", + "frame_time": 1607123962.082975, + "snapshot": { + "frame_time": 1607123965.975463, + "box": [415, 489, 528, 700], + "area": 12728, + "region": [260, 446, 660, 846], + "score": 0.77546, + "attributes": [] + }, + "label": "person", + "sub_label": ["John Smith", 0.79], + "top_score": 0.958984375, + "false_positive": false, + "start_time": 1607123955.475377, + "end_time": null, + "score": 0.87890625, + "box": [432, 496, 544, 854], + "area": 40096, + "ratio": 1.251397, + "region": [218, 440, 693, 915], + "current_zones": ["yard", "driveway"], + "entered_zones": ["yard", "driveway"], + "thumbnail": null, + "has_snapshot": false, + "has_clip": false, + "active": true, // convenience attribute, this is strictly opposite of "stationary" + "stationary": false, // whether or not the object is considered stationary + "motionless_count": 0, // number of frames the object has been motionless + "position_changes": 2, // number of times the object has changed position + "attributes": { + "face": 0.86 + }, // attributes with top score that have been identified on the object at any point + "current_attributes": [ + // detailed data about the current attributes in this frame + { + "label": "face", + "box": [442, 506, 534, 524], + "score": 0.86 + } + ], + "current_estimated_speed": 0.77, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled + "average_estimated_speed": 14.31, // average estimated speed (mph or kph) for objects moving through zones with speed estimation enabled + "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "recognized_license_plate": "ABC12345", // a recognized license plate for car objects + "recognized_license_plate_score": 0.933451 + } +} +``` + +### `frigate/tracked_object_update` + +Message published for updates to tracked object metadata. All messages include an `id` field which is the tracked object's event ID, and can be used to look up the event via the API or match it to items in the UI. + +#### Generative AI Description Update + +```json +{ + "type": "description", + "id": "1607123955.475377-mxklsc", + "description": "The car is a red sedan moving away from the camera." +} +``` + +#### Face Recognition Update + +Published after each recognition attempt, regardless of whether the score meets `recognition_threshold`. See the [Face Recognition](/configuration/face_recognition) documentation for details on how scoring works. + +```json +{ + "type": "face", + "id": "1607123955.475377-mxklsc", + "name": "John", // best matching person, or null if no match + "score": 0.95, // running weighted average across all recognition attempts + "camera": "front_door_cam", + "timestamp": 1607123958.748393 +} +``` + +#### License Plate Recognition Update + +Published when a license plate is recognized on a car object. See the [License Plate Recognition](/configuration/license_plate_recognition) documentation for details. + +```json +{ + "type": "lpr", + "id": "1607123955.475377-mxklsc", + "name": "John's Car", // known name for the plate, or null + "plate": "123ABC", + "score": 0.95, + "camera": "driveway_cam", + "timestamp": 1607123958.748393, + "plate_box": [917, 487, 1029, 529] // box coordinates of the detected license plate in the frame +} +``` + +#### Object Classification Update + +Message published when [object classification](/configuration/custom_classification/object_classification) reaches consensus on a classification result. + +**Sub label type:** + +```json +{ + "type": "classification", + "id": "1607123955.475377-mxklsc", + "camera": "front_door_cam", + "timestamp": 1607123958.748393, + "model": "person_classifier", + "sub_label": "delivery_person", + "score": 0.87 +} +``` + +**Attribute type:** + +```json +{ + "type": "classification", + "id": "1607123955.475377-mxklsc", + "camera": "front_door_cam", + "timestamp": 1607123958.748393, + "model": "helmet_detector", + "attribute": "yes", + "score": 0.92 +} +``` + +### `frigate/reviews` + +Message published for each changed review item. The first message is published when the `detection` or `alert` is initiated. + +An `update` with the same ID will be published when: + +- The severity changes from `detection` to `alert` +- Additional objects are detected +- An object is recognized via face, lpr, etc. + +When the review activity has ended a final `end` message is published. + +```json +{ + "type": "update", // new, update, end + "before": { + "id": "1718987129.308396-fqk5ka", // review_id + "camera": "front_cam", + "start_time": 1718987129.308396, + "end_time": null, + "severity": "detection", + "thumb_path": "/media/frigate/clips/review/thumb-front_cam-1718987129.308396-fqk5ka.webp", + "data": { + "detections": [ + // list of event IDs + "1718987128.947436-g92ztx", + "1718987148.879516-d7oq7r", + "1718987126.934663-q5ywpt" + ], + "objects": ["person", "car"], + "sub_labels": [], + "zones": [], + "audio": [] + } + }, + "after": { + "id": "1718987129.308396-fqk5ka", + "camera": "front_cam", + "start_time": 1718987129.308396, + "end_time": null, + "severity": "alert", + "thumb_path": "/media/frigate/clips/review/thumb-front_cam-1718987129.308396-fqk5ka.webp", + "data": { + "detections": [ + "1718987128.947436-g92ztx", + "1718987148.879516-d7oq7r", + "1718987126.934663-q5ywpt" + ], + "objects": ["person", "car"], + "sub_labels": ["Bob"], + "zones": ["front_yard"], + "audio": [] + } + } +} +``` + +### `frigate/triggers` + +Message published when a trigger defined in a camera's `semantic_search` configuration fires. + +```json +{ + "name": "car_trigger", + "camera": "driveway", + "event_id": "1751565549.853251-b69j73", + "type": "thumbnail", + "score": 0.85 +} +``` + +### `frigate/stats` + +Same data available at `/api/stats` published at a configurable interval. + +### `frigate/camera_activity` + +Returns data about each camera, its current features, and if it is detecting motion, objects, etc. Can be triggered by publishing to `frigate/onConnect` + +### `frigate/profile/set` + +Topic to activate or deactivate a [profile](/configuration/profiles). Publish a profile name to activate it, or `none` to deactivate the current profile. + +### `frigate/profile/state` + +Topic with the currently active profile name. Published value is the profile name or `none` if no profile is active. This topic is retained. + +### `frigate/notifications/set` + +Topic to turn notifications on and off. Expected values are `ON` and `OFF`. + +### `frigate/notifications/state` + +Topic with current state of notifications. Published values are `ON` and `OFF`. + +## Frigate Camera Topics + +### `frigate//status/` + +Publishes the current health status of each role that is enabled (`audio`, `detect`, `record`). Possible values are: + +- `online`: Stream is running and being processed +- `offline`: Stream is offline and is being restarted +- `disabled`: Camera is currently turned off (either at runtime via the `enabled/set` topic, or persistently via the configuration file). See [Camera state](/configuration/live#camera-state) for the distinction. + +### `frigate//` + +Publishes the count of objects for the camera for use as a sensor in Home Assistant. +`all` can be used as the object_name for the count of all objects for the camera. + +### `frigate///active` + +Publishes the count of active objects for the camera for use as a sensor in Home +Assistant. `all` can be used as the object_name for the count of all active objects +for the camera. + +### `frigate//` + +Publishes the count of objects for the zone for use as a sensor in Home Assistant. +`all` can be used as the object_name for the count of all objects for the zone. + +### `frigate///active` + +Publishes the count of active objects for the zone for use as a sensor in Home +Assistant. `all` can be used as the object_name for the count of all objects for the +zone. + +### `frigate///snapshot` + +Publishes a jpeg encoded frame of the detected object type. When the object is no longer detected, the highest confidence image is published or the original image +is published again. + +The height and crop of snapshots can be configured in the config. + +### `frigate//audio/` + +Publishes "ON" when a type of audio is detected and "OFF" when it is not for the camera for use as a sensor in Home Assistant. + +`all` can be used as the audio_type for the status of all audio types. + +### `frigate//audio/dBFS` + +Publishes the dBFS value for audio detected on this camera. + +**NOTE:** Requires audio detection to be enabled + +### `frigate//audio/rms` + +Publishes the rms value for audio detected on this camera. + +**NOTE:** Requires audio detection to be enabled + +### `frigate//audio/transcription` + +Publishes transcribed text for audio detected on this camera. + +**NOTE:** Requires audio detection and transcription to be enabled + +### `frigate//classification/` + +Publishes the current state detected by a state classification model for the camera. The topic name includes the model name as configured in your classification settings. +The published value is the detected state class name (e.g., `open`, `closed`, `on`, `off`). The state is only published when it changes, helping to reduce unnecessary MQTT traffic. + +### `frigate//enabled/set` + +Topic to turn Frigate's processing of a camera on or off at runtime. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). To permanently change the configured value, use **Settings → Global configuration → Camera management** in the Frigate UI. See [Camera state](/configuration/live#camera-state) for the difference between turning a camera off and disabling it. + +### `frigate//enabled/state` + +Topic with current runtime state of processing for a camera. Published values are `ON` and `OFF`. + +### `frigate//detect/set` + +Topic to turn object detection for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). + +### `frigate//detect/state` + +Topic with current state of object detection for a camera. Published values are `ON` and `OFF`. + +### `frigate//audio/set` + +Topic to turn audio detection for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). + +### `frigate//audio/state` + +Topic with current state of audio detection for a camera. Published values are `ON` and `OFF`. + +### `frigate//recordings/set` + +Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). + +### `frigate//recordings/state` + +Topic with current state of recordings for a camera. Published values are `ON` and `OFF`. + +### `frigate//snapshots/set` + +Topic to turn snapshots for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). + +### `frigate//snapshots/state` + +Topic with current state of snapshots for a camera. Published values are `ON` and `OFF`. + +### `frigate//motion/set` + +Topic to turn motion detection for a camera on and off. Expected values are `ON` and `OFF`. +NOTE: Turning off motion detection will fail if detection is not disabled. + +### `frigate//motion` + +Whether camera_name is currently detecting motion. Expected values are `ON` and `OFF`. +NOTE: After motion is initially detected, `ON` will be set until no motion has +been detected for `mqtt_off_delay` seconds (30 by default). + +### `frigate//motion/state` + +Topic with current state of motion detection for a camera. Published values are `ON` and `OFF`. + +### `frigate//improve_contrast/set` + +Topic to turn improve_contrast for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//improve_contrast/state` + +Topic with current state of improve_contrast for a camera. Published values are `ON` and `OFF`. + +### `frigate//motion_threshold/set` + +Topic to adjust motion threshold for a camera. Expected value is an integer. + +### `frigate//motion_threshold/state` + +Topic with current motion threshold for a camera. Published value is an integer. + +### `frigate//motion_contour_area/set` + +Topic to adjust motion contour area for a camera. Expected value is an integer. + +### `frigate//motion_contour_area/state` + +Topic with current motion contour area for a camera. Published value is an integer. + +### `frigate//motion_mask//set` + +Topic to turn a specific motion mask for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//motion_mask//state` + +Topic with current state of a specific motion mask for a camera. Published values are `ON` and `OFF`. + +### `frigate//object_mask//set` + +Topic to turn a specific object mask for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//object_mask//state` + +Topic with current state of a specific object mask for a camera. Published values are `ON` and `OFF`. + +### `frigate//zone//set` + +Topic to turn a specific zone for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//zone//state` + +Topic with current state of a specific zone for a camera. Published values are `ON` and `OFF`. + +### `frigate//review_status` + +Topic with current activity status of the camera. Possible values are `NONE`, `DETECTION`, or `ALERT`. + +### `frigate//ptz` + +Topic to send PTZ commands to camera. + +| Command | Description | +| ---------------------- | ----------------------------------------------------------------------------------------- | +| `preset_` | send command to move to preset with name `` | +| `MOVE_` | send command to continuously move in ``, possible values are [UP, DOWN, LEFT, RIGHT] | +| `ZOOM_` | send command to continuously zoom ``, possible values are [IN, OUT] | +| `STOP` | send command to stop moving | + +### `frigate//ptz_autotracker/set` + +Topic to turn the PTZ autotracker for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//ptz_autotracker/state` + +Topic with current state of the PTZ autotracker for a camera. Published values are `ON` and `OFF`. + +### `frigate//ptz_autotracker/active` + +Topic to determine if PTZ autotracker is actively tracking an object. Published values are `ON` and `OFF`. + +### `frigate//review_alerts/set` + +Topic to turn review alerts for a camera on or off. Expected values are `ON` and `OFF`. + +### `frigate//review_alerts/state` + +Topic with current state of review alerts for a camera. Published values are `ON` and `OFF`. + +### `frigate//review_detections/set` + +Topic to turn review detections for a camera on or off. Expected values are `ON` and `OFF`. + +### `frigate//review_detections/state` + +Topic with current state of review detections for a camera. Published values are `ON` and `OFF`. + +### `frigate//object_descriptions/set` + +Topic to turn generative AI object descriptions for a camera on or off. Expected values are `ON` and `OFF`. + +### `frigate//object_descriptions/state` + +Topic with current state of generative AI object descriptions for a camera. Published values are `ON` and `OFF`. + +### `frigate//review_descriptions/set` + +Topic to turn generative AI review descriptions for a camera on or off. Expected values are `ON` and `OFF`. + +### `frigate//review_descriptions/state` + +Topic with current state of generative AI review descriptions for a camera. Published values are `ON` and `OFF`. + +### `frigate//birdseye/set` + +Topic to turn Birdseye for a camera on and off. Expected values are `ON` and `OFF`. Birdseye mode +must be enabled in the configuration. + +### `frigate//birdseye/state` + +Topic with current state of Birdseye for a camera. Published values are `ON` and `OFF`. + +### `frigate//birdseye_mode/set` + +Topic to set Birdseye mode for a camera. Birdseye offers different modes to customize under which circumstances the camera is shown. + +_Note: Changing the value from `CONTINUOUS` -> `MOTION | OBJECTS` will take up to 30 seconds for +the camera to be removed from the view._ + +| Command | Description | +| ------------ | ----------------------------------------------------------------- | +| `CONTINUOUS` | Always included | +| `MOTION` | Show when detected motion within the last 30 seconds are included | +| `OBJECTS` | Shown if an active object tracked within the last 30 seconds | + +### `frigate//birdseye_mode/state` + +Topic with current state of the Birdseye mode for a camera. Published values are `CONTINUOUS`, `MOTION`, `OBJECTS`. + +### `frigate//notifications/set` + +Topic to turn notifications on and off. Expected values are `ON` and `OFF`. + +### `frigate//notifications/state` + +Topic with current state of notifications. Published values are `ON` and `OFF`. + +### `frigate//notifications/suspend` + +Topic to suspend notifications for a certain number of minutes. Expected value is an integer. + +### `frigate//notifications/suspended` + +Topic with timestamp that notifications are suspended until. Published value is a UNIX timestamp, or 0 if notifications are not suspended. diff --git a/docs/docs/integrations/plus.md b/docs/docs/integrations/plus.md new file mode 100644 index 0000000..949a9f4 --- /dev/null +++ b/docs/docs/integrations/plus.md @@ -0,0 +1,88 @@ +--- +id: plus +title: Frigate+ +--- + +import NavPath from "@site/src/components/NavPath"; + +For more information about how to use Frigate+ to improve your model, see the [Frigate+ docs](/plus/). + +:::info + +Frigate+ requires an active internet connection to communicate with `https://api.frigate.video` for model downloads, image uploads, and annotations. See [Network Requirements](/frigate/network_requirements#frigate) for details. + +::: + +## Setup + +### Create an account + +Free accounts can be created at [https://plus.frigate.video](https://plus.frigate.video). + +### Generate an API key + +Once logged in, you can generate an API key for Frigate in Settings. + +![API key](/img/plus-api-key-min.png) + +### Set your API key + +In Frigate, you can use an environment variable or a docker secret named `PLUS_API_KEY` to enable the `Frigate+` buttons on the Explore page. Home Assistant App users can set it under Settings > Apps > Frigate > Configuration > Options (be sure to toggle the "Show unused optional configuration options" switch). + +:::warning + +You cannot use the `environment_vars` section of your Frigate configuration file to set this environment variable. It must be defined as an environment variable in the docker config or Home Assistant App config. + +::: + +## Submit examples + +Once your API key is configured, you can submit examples directly from the Explore page in Frigate. From the More Filters menu, select "Has a Snapshot - Yes" and "Submitted to Frigate+ - No", and press Apply at the bottom of the pane. Then, click on a thumbnail and select the Snapshot tab. + +You can use your keyboard's left and right arrow keys to quickly navigate between the tracked object snapshots. + +:::note + +Snapshots must be enabled to be able to submit examples to Frigate+ + +::: + +![Submit To Plus](/img/plus/submit-to-plus.jpg) + +### Annotate and verify + +You can view all of your submitted images at [https://plus.frigate.video](https://plus.frigate.video). Annotations can be added by clicking an image. For more detailed information about labeling, see the documentation on [annotating](../plus/annotating.md). + +![Annotate](/img/annotate.png) + +## Use Models + +Once you have [requested your first model](../plus/first_model.md) and gotten your own model ID, it can be used with a special model path. No other information needs to be configured for Frigate+ models because it fetches the remaining config from Frigate+ automatically. + +You can either choose the new model from the pane in the Frigate UI (the **Frigate+ Model** tab), or manually set the model at the root level in your config: + +```yaml +detectors: ... + +model: + path: plus:// +``` + +:::note + +Model IDs are not secret values and can be shared freely. Access to your model is protected by your API key. + +::: + +Models are downloaded into the `/config/model_cache` folder and only downloaded if needed. + +If needed, you can override the labelmap for Frigate+ models. This is not recommended as renaming labels will break the Submit to Frigate+ feature if the labels are not available in Frigate+. + +```yaml +model: + path: plus:// + labelmap: + 3: animal + 4: animal + 5: animal +``` diff --git a/docs/docs/integrations/third_party_extensions.md b/docs/docs/integrations/third_party_extensions.md new file mode 100644 index 0000000..64b174a --- /dev/null +++ b/docs/docs/integrations/third_party_extensions.md @@ -0,0 +1,60 @@ +--- +id: third_party_extensions +title: Third Party Extensions +--- + +Being open source, others have the possibility to modify and extend the rich functionality Frigate already offers. +This page is meant to be an overview over additions one can make to the home NVR setup. The list is not exhaustive and can be extended via PR to the Frigate docs. Most of these services are designed to interface with Frigate's unauthenticated api over port 5000. + +:::warning + +This page does not recommend or rate the presented projects. +Please use your own knowledge to assess and vet them before you install anything on your system. + +::: + +## [Advanced Camera Card (formerly known as Frigate Card](https://card.camera/#/README) + +The [Advanced Camera Card](https://card.camera/#/README) is a Home Assistant dashboard card with deep Frigate integration. + +## [cctvQL](https://github.com/arunrajiah/cctvql) + +[cctvQL](https://github.com/arunrajiah/cctvql) is a natural language query layer for Frigate and other CCTV systems. It connects to Frigate's REST API and MQTT broker to let you ask conversational questions about cameras and events (e.g. "Was there motion at the front door last night?"), with support for real-time event streaming, anomaly detection, PTZ control, alert rules, and a Home Assistant custom component. + +## [Double Take](https://github.com/skrashevich/double-take) + +[Double Take](https://github.com/skrashevich/double-take) provides a unified UI and API for processing and training images for facial recognition. +It supports automatically setting the sub labels in Frigate for person objects that are detected and recognized. +This is a fork (with fixed errors and new features) of [original Double Take](https://github.com/jakowenko/double-take) project which, unfortunately, isn't being maintained by author. + +## [Frigate Notify](https://github.com/0x2142/frigate-notify) + +[Frigate Notify](https://github.com/0x2142/frigate-notify) is a simple app designed to send notifications from Frigate to your favorite platforms. Intended to be used with standalone Frigate installations - Home Assistant not required, MQTT is optional but recommended. + +## [Frigate Notify Alert](https://github.com/Sysoev86/frigate-notify-alert) + +[Frigate Notify Alert](https://github.com/Sysoev86/frigate-notify-alert) sends Frigate events to Telegram as a photo + video media group. It supports multiple camera groups (each notifying its own chat), optional zone filtering (notify only when an object enters a chosen zone), and in-chat buttons to pause notifications for a set time. Works with standalone Frigate over MQTT; Home Assistant not required. + +## [Frigate Snap-Sync](https://github.com/thequantumphysicist/frigate-snap-sync/) + +[Frigate Snap-Sync](https://github.com/thequantumphysicist/frigate-snap-sync/) is a program that works in tandem with Frigate. It responds to Frigate when a snapshot or a review is made (and more can be added), and uploads them to one or more remote server(s) of your choice. + +## [Frigate telegram](https://github.com/OldTyT/frigate-telegram) + +[Frigate telegram](https://github.com/OldTyT/frigate-telegram) makes it possible to send events from Frigate to Telegram. Events are sent as a message with a text description, video, and thumbnail. + +## [kiosk-monitor](https://github.com/extremeshok/kiosk-monitor) + +[kiosk-monitor](https://github.com/extremeshok/kiosk-monitor) is a Raspberry Pi watchdog that runs Chromium fullscreen on a Frigate dashboard (optionally with VLC on a second monitor for an RTSP camera stream), auto-restarts on frozen screens or unreachable URLs, and ships a Birdseye-aware Chromium helper that auto-sizes the grid to the display. + +## [Periscope](https://github.com/maksz42/periscope) + +[Periscope](https://github.com/maksz42/periscope) is a lightweight Android app that turns old devices into live viewers for Frigate. It works on Android 2.2 and above, including Android TV. It supports authentication and HTTPS. + +## [Scrypted - Frigate bridge plugin](https://github.com/apocaliss92/scrypted-frigate-bridge) + +[Scrypted - Frigate bridge](https://github.com/apocaliss92/scrypted-frigate-bridge) is a plugin that allows you to ingest Frigate detections, motion, videoclips on Scrypted as well as provide templates to export rebroadcast configurations on Frigate. + +## [Strix](https://github.com/eduard256/Strix) + +[Strix](https://github.com/eduard256/Strix) auto-discovers working stream URLs for IP cameras and generates ready-to-use Frigate configs. It tests thousands of URL patterns against your camera and supports cameras without RTSP or ONVIF. 67K+ camera models from 3.6K+ brands. diff --git a/docs/docs/mdx.md b/docs/docs/mdx.md new file mode 100644 index 0000000..f0210fb --- /dev/null +++ b/docs/docs/mdx.md @@ -0,0 +1,17 @@ +--- +id: mdx +title: Powered by MDX +--- + +You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). + +export const Highlight = ({children, color}) => ( {children} ); + +Docusaurus green and Facebook blue are my favorite colors. + +I can write **Markdown** alongside my _JSX_! diff --git a/docs/docs/plus/annotating.md b/docs/docs/plus/annotating.md new file mode 100644 index 0000000..3725ab5 --- /dev/null +++ b/docs/docs/plus/annotating.md @@ -0,0 +1,53 @@ +--- +id: annotating +title: Annotating your images +--- + +For the best results, follow these guidelines. You may also want to review the documentation on [improving your model](./index.md#improving-your-model). + +**Label every object in the image**: It is important that you label all objects in each image before verifying. If you don't label a car for example, the model will be taught that part of the image is _not_ a car and it will start to get confused. You can exclude labels that you don't want detected on any of your cameras. + +**Make tight bounding boxes**: Tighter bounding boxes improve the recognition and ensure that accurate bounding boxes are predicted at runtime. + +**Label the full object even when occluded**: If you have a person standing behind a car, label the full person even though a portion of their body may be hidden behind the car. This helps predict accurate bounding boxes and improves zone accuracy and filters at runtime. If an object is partly out of frame, label it only when a person would reasonably be able to recognize the object from the visible parts. + +**Label objects hard to identify as difficult**: When objects are truly difficult to make out, such as a car barely visible through a bush, or a dog that is hard to distinguish from the background at night, flag it as 'difficult'. This is not used in the model training as of now, but will in the future. + +**Delivery logos such as `amazon`, `ups`, and `fedex` should label the logo**: For a Fedex truck, label the truck as a `car` and make a different bounding box just for the Fedex logo. If there are multiple logos, label each of them. + +![Fedex Logo](/img/plus/fedex-logo.jpg) + +## AI suggested labels + +If you have an active Frigate+ subscription, new uploads will be scanned for the objects configured for your camera and you will see suggested labels as light blue boxes when annotating in Frigate+. These suggestions are processed via a queue and typically complete within a minute after uploading, but processing times can be longer. + +![Suggestions](/img/plus/suggestions.webp) + +Suggestions are converted to labels when saving, so you should remove any errant suggestions. There is already some logic designed to avoid duplicate labels, but you may still occasionally see some duplicate suggestions. You should keep the most accurate bounding box and delete any duplicates so that you have just one label per object remaining. + +## False positive labels + +False positives will be shown with a red box and the label will have a strike through. These can't be adjusted, but they can be deleted if you accidentally submit a true positive as a false positive from Frigate. +![false positive](/img/plus/false-positive.jpg) + +Misidentified objects should have a correct label added. For example, if a person was mistakenly detected as a cat, you should submit it as a false positive in Frigate and add a label for the person. The boxes will overlap. + +![add image](/img/plus/false-positive-overlap.jpg) + +## Shortcuts for a faster workflow + +| Shortcut Key | Description | +| ----------------- | ----------------------------- | +| `?` | Show all keyboard shortcuts | +| `w` | Add box | +| `d` | Toggle difficult | +| `s` | Switch to the next label | +| `Shift + s` | Switch to the previous label | +| `tab` | Select next largest box | +| `del` | Delete current box | +| `esc` | Deselect/Cancel | +| `← ↑ → ↓` | Move box | +| `Shift + ← ↑ → ↓` | Resize box | +| `scrollwheel` | Zoom in/out | +| `f` | Hide/show all but current box | +| `spacebar` | Verify and save | diff --git a/docs/docs/plus/faq.md b/docs/docs/plus/faq.md new file mode 100644 index 0000000..055e2e8 --- /dev/null +++ b/docs/docs/plus/faq.md @@ -0,0 +1,33 @@ +--- +id: faq +title: FAQ +--- + +### Are my models trained just on my image uploads? How are they built? + +Frigate+ models are built by fine tuning a base model with the images you have annotated and verified. The base model is trained from scratch from a sampling of images across all Frigate+ user submissions and takes weeks of expensive GPU resources to train. If the models were built using your image uploads alone, you would need to provide tens of thousands of examples and it would take more than a week (and considerable cost) to train. Diversity helps the model generalize. + +### Are my video feeds sent to the cloud for analysis when using Frigate+ models? + +No. Frigate+ models are a drop in replacement for the default model. All processing is performed locally as always. The only images sent to Frigate+ are the ones you specifically submit via the `Send to Frigate+` button or upload directly. + +### Can I label anything I want and train the model to recognize something custom for me? + +Not currently. At the moment, the set of labels will be consistent for all users. The focus will be on expanding that set of labels before working on completely custom user labels. + +### Can Frigate+ models be used offline? + +Yes. Models and metadata are stored in the `model_cache` directory within the config folder. Frigate will only attempt to download a model if it does not exist in the cache. This means you can backup the directory and/or use it completely offline. + +### Can I keep using my Frigate+ models even if I do not renew my subscription? + +Yes. Subscriptions to Frigate+ provide access to the infrastructure used to train the models. Models trained with your subscription are yours to keep and use forever. However, do note that the terms and conditions prohibit you from sharing, reselling, or creating derivative products from the models. + +### Why can't I submit images to Frigate+? + +If you've configured your API key and the Frigate+ Settings page in the UI shows that the key is active, you need to ensure that snapshots are enabled for the cameras you'd like to submit images for. + +```yaml +snapshots: + enabled: true +``` diff --git a/docs/docs/plus/first_model.md b/docs/docs/plus/first_model.md new file mode 100644 index 0000000..9809555 --- /dev/null +++ b/docs/docs/plus/first_model.md @@ -0,0 +1,116 @@ +--- +id: first_model +title: Requesting your first model +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +## Step 1: Upload and annotate your images + +Before requesting your first model, you will need to upload and verify at least 10 images to Frigate+. The more images you upload, annotate, and verify the better your results will be. Most users start to see very good results once they have at least 100 verified images per camera. Keep in mind that varying conditions should be included. You will want images from cloudy days, sunny days, dawn, dusk, and night. Refer to the [integration docs](../integrations/plus.md#generate-an-api-key) for instructions on how to easily submit images to Frigate+ directly from Frigate. + +It is recommended to submit **both** true positives and false positives. This will help the model differentiate between what is and isn't correct. You should aim for a target of 80% true positive submissions and 20% false positives across all of your images. If you are experiencing false positives in a specific area, submitting true positives for any object type near that area in similar lighting conditions will help teach the model what that area looks like when no objects are present. + +For more detailed recommendations, you can refer to the docs on [annotating](./annotating.md). + +## Step 2: Submit a model request + +Once you have an initial set of verified images, you can request a model on the Models page. For guidance on choosing a model type, refer to [this part of the documentation](./index.md#available-model-types). If you are unsure which type to request, you can test the base model for each version from the "Base Models" tab. Each model request requires 1 of the 12 trainings that you receive with your annual subscription. This model will support all [label types available](./index.md#available-label-types) even if you do not submit any examples for those labels. Model creation can take up to 36 hours. +![Plus Models Page](/img/plus/plus-models.jpg) + +## Step 3: Set your model + +You will receive an email notification when your Frigate+ model is ready. +![Model Ready Email](/img/plus/model-ready-email.jpg) + +Models available in Frigate+ can be used with a special model path. No other information needs to be configured because it fetches the remaining config from Frigate+ automatically. + + + + +Navigate to . In the **Detection Model** section, choose the **Frigate+** tab. Select your new Frigate+ model from the **Available Frigate+ models** dropdown, then click **Save**. Restart Frigate to apply the change. + + + + +```yaml +detectors: ... + +model: + path: plus:// +``` + +:::tip + +When setting the plus model id, all other fields should be removed as these are configured automatically with the Frigate+ model config + +::: + + + + +:::note + +Model IDs are not secret values and can be shared freely. Access to your model is protected by your API key. + +::: + +## Step 4: Adjust your object filters for higher scores + +Frigate+ models generally have much higher scores than the default model provided in Frigate. You will likely need to increase your `threshold` and `min_score` values. Here is an example of how these values can be refined, but you should expect these to evolve as your model improves. For more information about how `threshold` and `min_score` are related, see the docs on [object filters](../configuration/object_filters.md#object-scores). + + + + +Navigate to . Under **Object filters**, set **Min Score** and **Threshold** for each object type, then click **Save**. + +| Object | Min Score | Threshold | +| ----------------- | --------- | --------- | +| **dog** | .7 | .9 | +| **cat** | .65 | .8 | +| **face** | .7 | | +| **package** | .65 | .9 | +| **license_plate** | .6 | | +| **amazon** | .75 | | +| **ups** | .75 | | +| **fedex** | .75 | | +| **person** | .65 | .85 | +| **car** | .65 | .85 | + + + + +```yaml +objects: + filters: + dog: + min_score: .7 + threshold: .9 + cat: + min_score: .65 + threshold: .8 + face: + min_score: .7 + package: + min_score: .65 + threshold: .9 + license_plate: + min_score: .6 + amazon: + min_score: .75 + ups: + min_score: .75 + fedex: + min_score: .75 + person: + min_score: .65 + threshold: .85 + car: + min_score: .65 + threshold: .85 +``` + + + diff --git a/docs/docs/plus/index.md b/docs/docs/plus/index.md new file mode 100644 index 0000000..76792f9 --- /dev/null +++ b/docs/docs/plus/index.md @@ -0,0 +1,113 @@ +--- +id: index +title: Models +--- + +Frigate+ offers models trained on images submitted by Frigate+ users from their security cameras and is specifically designed for the way Frigate NVR analyzes video footage. These models offer higher accuracy with less resources. The images you upload are used to fine tune a base model trained from images uploaded by all Frigate+ users. This fine tuning process results in a model that is optimized for accuracy in your specific conditions. + +With a subscription, 12 model trainings to fine tune your model per year are included. In addition, you will have access to any base models published while your subscription is active. If you cancel your subscription, you will retain access to any trained and base models in your account. An active subscription is required to submit model requests or purchase additional trainings. New base models are published quarterly with target dates of January 15th, April 15th, July 15th, and October 15th. + +Information on how to integrate Frigate+ with Frigate can be found in the [integration docs](../integrations/plus.md). + +## Available model types + +There are three model types offered in Frigate+, `mobiledet`, `yolonas`, and `yolov9`. All of these models are object detection models and are trained to detect the same set of labels [listed below](#available-label-types). + +Not all model types are supported by all detectors, so it's important to choose a model type to match your detector as shown in the table under [supported detector types](#supported-detector-types). You can test model types for compatibility and speed on your hardware by using the base models. + +| Model Type | Description | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `mobiledet` | Based on the same architecture as the default model included with Frigate. Runs on Google Coral devices and CPUs. | +| `yolonas` | A newer architecture that offers slightly higher accuracy and improved detection of small objects. Runs on Intel, NVidia GPUs, and AMD GPUs. | +| `yolov9` | A leading SOTA (state of the art) object detection model with similar performance to yolonas, but on a wider range of hardware options. Runs on most hardware. | + +### YOLOv9 Details + +YOLOv9 models are available in `s`, `t`, `edgetpu` variants. When requesting a `yolov9` model, you will be prompted to choose a variant. If you want the model to be compatible with a Google Coral, you will need to choose the `edgetpu` variant. If you are unsure what variant to choose, you should perform some tests with the base models to find the performance level that suits you. The `s` size is most similar to the current `yolonas` models in terms of inference times and accuracy, and a good place to start is the `320x320` resolution model for `yolov9s`. + +:::info + +When switching to YOLOv9, you may need to adjust your thresholds for some objects. + +::: + +#### Hailo Support + +If you have a Hailo device, you will need to specify the hardware you have when submitting a model request because they are not cross compatible. Please test using the available base models before submitting your model request. + +#### Rockchip (RKNN) Support + +Rockchip models are automatically converted as of 0.17. For 0.16, YOLOv9 onnx models will need to be manually converted. First, you will need to configure Frigate to use the model id for your YOLOv9 onnx model so it downloads the model to your `model_cache` directory. From there, you can follow the [documentation](/configuration/object_detectors.md#converting-your-own-onnx-model-to-rknn-format) to convert it. + +## Supported detector types + +Currently, Frigate+ models support CPU (`cpu`), Google Coral (`edgetpu`), OpenVino (`openvino`), ONNX (`onnx`), Hailo (`hailo8l`), and Rockchip (`rknn`) detectors. + +| Hardware | Recommended Detector Type | Recommended Model Type | +| -------------------------------------------------------------------------------- | ------------------------- | ---------------------- | +| [CPU](/configuration/object_detectors.md#cpu-detector-not-recommended) | `cpu` | `mobiledet` | +| [Coral (all form factors)](/configuration/object_detectors.md#edge-tpu-detector) | `edgetpu` | `yolov9` | +| [Intel](/configuration/object_detectors.md#openvino-detector) | `openvino` | `yolov9` | +| [NVidia GPU](/configuration/object_detectors#onnx) | `onnx` | `yolov9` | +| [AMD ROCm GPU](/configuration/object_detectors#amdrocm-gpu-detector) | `onnx` | `yolov9` | +| [Hailo8/Hailo8L/Hailo8R](/configuration/object_detectors#hailo-8) | `hailo8l` | `yolov9` | +| [Rockchip NPU](/configuration/object_detectors#rockchip-platform) | `rknn` | `yolov9` | + +## Improving your model + +Some users may find that Frigate+ models result in more false positives initially, but by submitting true and false positives, the model will improve. With all the new images now being submitted by subscribers, future base models will improve as more and more examples are incorporated. Note that only images with at least one verified label will be used when training your model. Submitting an image from Frigate as a true or false positive will not verify the image. You still must verify the image in Frigate+ in order for it to be used in training. + +- **Submit both true positives and false positives**. This will help the model differentiate between what is and isn't correct. You should aim for a target of 80% true positive submissions and 20% false positives across all of your images. If you are experiencing false positives in a specific area, submitting true positives for any object type near that area in similar lighting conditions will help teach the model what that area looks like when no objects are present. +- **Lower your thresholds a little in order to generate more false/true positives near the threshold value**. For example, if you have some false positives that are scoring at 68% and some true positives scoring at 72%, you can try lowering your threshold to 65% and submitting both true and false positives within that range. This will help the model learn and widen the gap between true and false positive scores. +- **Submit diverse images**. For the best results, you should provide at least 100 verified images per camera. Keep in mind that varying conditions should be included. You will want images from cloudy days, sunny days, dawn, dusk, and night. As circumstances change, you may need to submit new examples to address new types of false positives. For example, the change from summer days to snowy winter days or other changes such as a new grill or patio furniture may require additional examples and training. + +## Available label types + +Frigate+ models support a more relevant set of objects for security cameras. The labels for annotation in Frigate+ are configurable by editing the camera in the Cameras section of Frigate+. Currently, the following objects are supported: + +- **People**: `person`, `face` +- **Vehicles**: `car`, `motorcycle`, `bicycle`, `boat`, `school_bus`, `license_plate` +- **Delivery Logos**: `amazon`, `usps`, `ups`, `fedex`, `dhl`, `an_post`, `purolator`, `postnl`, `nzpost`, `postnord`, `gls`, `dpd`, `canada_post`, `royal_mail` +- **Animals**: `dog`, `cat`, `deer`, `horse`, `bird`, `raccoon`, `fox`, `bear`, `cow`, `squirrel`, `goat`, `rabbit`, `skunk`, `kangaroo` +- **Other**: `package`, `waste_bin`, `bbq_grill`, `robot_lawnmower`, `umbrella` + +Other object types available in the default Frigate model are not available. Additional object types will be added in future releases. + +### Candidate labels + +Candidate labels are also available for annotation. These labels don't have enough data to be included in the model yet, but using them will help add support sooner. You can enable these labels by editing the camera settings. + +Where possible, these labels are mapped to existing labels during training. For example, any `baby` labels are mapped to `person` until support for new labels is added. + +The candidate labels are: `baby`, `bpost`, `badger`, `possum`, `rodent`, `chicken`, `groundhog`, `boar`, `hedgehog`, `tractor`, `golf cart`, `garbage truck`, `bus`, `sports ball`, `la_poste`, `lawnmower`, `heron`, `rickshaw`, `wombat`, `auspost`, `aramex`, `bobcat`, `mustelid`, `transoflex`, `airplane`, `drone`, `mountain_lion`, `crocodile`, `turkey`, `baby_stroller`, `monkey`, `coyote`, `porcupine`, `parcelforce`, `sheep`, `snake`, `helicopter`, `lizard`, `duck`, `hermes`, `cargus`, `fan_courier`, `sameday` + +Candidate labels are not available for automatic suggestions. + +### Label attributes + +Frigate has special handling for some labels when using Frigate+ models. `face`, `license_plate`, and delivery logos such as `amazon`, `ups`, and `fedex` are considered attribute labels which are not tracked like regular objects and do not generate review items directly. In addition, the `threshold` filter will have no effect on these labels. You should adjust the `min_score` and other filter values as needed. + +In order to have Frigate start using these attribute labels, you will need to add them to the list of objects to track: + +```yaml +objects: + track: + - person + - face + - license_plate + - dog + - cat + - car + - amazon + - fedex + - ups + - package +``` + +When using Frigate+ models, Frigate will choose the snapshot of a person object that has the largest visible face. For cars, the snapshot with the largest visible license plate will be selected. This aids in secondary processing such as facial and license plate recognition for person and car objects. + +![Face Attribute](/img/plus/attribute-example-face.jpg) + +Delivery logos such as `amazon`, `ups`, and `fedex` labels are used to automatically assign a sub label to car objects. + +![Fedex Attribute](/img/plus/attribute-example-fedex.jpg) diff --git a/docs/docs/troubleshooting/cpu.md b/docs/docs/troubleshooting/cpu.md new file mode 100644 index 0000000..a6c2444 --- /dev/null +++ b/docs/docs/troubleshooting/cpu.md @@ -0,0 +1,74 @@ +--- +id: cpu +title: High CPU Usage +--- + +High CPU usage can impact Frigate's performance and responsiveness. This guide outlines the most effective configuration changes to help reduce CPU consumption and optimize resource usage. + +## 1. Hardware Acceleration for Video Decoding + +**Priority: Critical** + +Video decoding is one of the most CPU-intensive tasks in Frigate. While an AI accelerator handles object detection, it does not assist with decoding video streams. Hardware acceleration (hwaccel) offloads this work to your GPU or specialized video decode hardware, significantly reducing CPU usage and enabling you to support more cameras on the same hardware. + +### Key Concepts + +**Resolution & FPS Impact:** The decoding burden grows exponentially with resolution and frame rate. A 4K stream at 30 FPS requires roughly 4 times the processing power of a 1080p stream at the same frame rate, and doubling the frame rate doubles the decode workload. This is why hardware acceleration becomes critical when working with multiple high-resolution cameras. + +**Hardware Acceleration Benefits:** By using dedicated video decode hardware, you can: + +- Significantly reduce CPU usage per camera stream +- Support 2-3x more cameras on the same hardware +- Free up CPU resources for motion detection and other Frigate processes +- Reduce system heat and power consumption + +### Configuration + +Frigate provides preset configurations for common hardware acceleration scenarios. Set up `hwaccel_args` based on your hardware in your [configuration](../configuration/advanced/reference) as described in the [getting started guide](../guides/getting_started). + +### Troubleshooting Hardware Acceleration + +If hardware acceleration isn't working: + +1. Check Frigate logs for FFmpeg errors related to hwaccel +2. Verify the hardware device is accessible inside the container +3. Ensure your camera streams use H.264 or H.265 codecs (most common) +4. Try different presets if the automatic detection fails +5. Check that your GPU drivers are properly installed on the host system + +## 2. Detector Selection and Configuration + +**Priority: Critical** + +Choosing the right detector for your hardware is the single most important factor for detection performance. The detector is responsible for running the AI model that identifies objects in video frames. Different detector types have vastly different performance characteristics and hardware requirements, as detailed in the [hardware documentation](../frigate/hardware). + +### Understanding Detector Performance + +Frigate uses motion detection as a first-line check before running expensive object detection, as explained in the [motion detection documentation](../configuration/motion_detection). When motion is detected, Frigate creates a "region" (the green boxes in the [debug viewer](/usage/live#the-single-camera-view)) and sends it to the detector. The detector's inference speed determines how many detections per second your system can handle. + +**Calculating Detector Capacity:** Your detector has a finite capacity measured in detections per second. With an inference speed of 10ms, your detector can handle approximately 100 detections per second (1000ms / 10ms = 100).If your cameras collectively require more than this capacity, you'll experience delays, missed detections, or the system will fall behind. + +### Choosing the Right Detector + +Different detectors have vastly different performance characteristics, see the expected performance for object detectors in [the hardware docs](../frigate/hardware) + +### Multiple Detector Instances + +When a single detector cannot keep up with your camera count, some detector types (`openvino`, `onnx`) allow you to define multiple detector instances to share the workload. This is particularly useful with GPU-based detectors that have sufficient VRAM to run multiple inference processes. + +For detailed instructions on configuring multiple detectors, see the [Object Detectors documentation](../configuration/object_detectors). + +**When to add a second detector:** + +- Skipped FPS is consistently > 0 even during normal activity + +### Model Selection and Optimization + +The model you use significantly impacts detector performance. Frigate provides default models optimized for each detector type, but you can customize them as described in the [detector documentation](../configuration/object_detectors). + +**Model Size Trade-offs:** + +- Smaller models (320x320): Faster inference, Frigate is specifically optimized for a 320x320 size model. +- Larger models (640x640): Slower inference, can sometimes have higher accuracy on very large objects that take up a majority of the frame. + +For more detail on picking the right size, see [Choosing a model size](../configuration/object_detectors.md#choosing-a-model-size). diff --git a/docs/docs/troubleshooting/dummy-camera.md b/docs/docs/troubleshooting/dummy-camera.md new file mode 100644 index 0000000..cba864b --- /dev/null +++ b/docs/docs/troubleshooting/dummy-camera.md @@ -0,0 +1,120 @@ +--- +id: dummy-camera +title: Analyzing Object Detection +--- + +import NavPath from "@site/src/components/NavPath"; + +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. + +## Reviewing Detections in the UI + +Before setting up a replay, you can often diagnose detection issues by reviewing existing recordings directly in the Frigate UI. + +### Detail View (History) + +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. + +Debug Replay isn't intended to be a one-stop pane for all Frigate diagnostics or a comprehensive debugging environment for every Frigate feature. It merely makes it easier to spin up a "dummy camera" and perform some common adjustments in real-time. You'll still need to use the normal tools (logs, an MQTT client, etc) to debug your feature. + +### When to use + +- 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. + +::: + +### Starting Debug Replay + +Debug Replay can be started from several places in the UI. The starting point determines the time range that gets replayed. + +- **History: Actions menu.** Navigate to , open the **Actions** menu in the toolbar, and choose **Debug Replay**. From here you can pick a preset (**Last 1 Minute**, **Last 5 Minutes**), select a range directly on the timeline with **From Timeline**, or enter exact start and end times with **Custom**. This is the most flexible option and the best choice when you want to add padding around a detection. On mobile, the same options appear in the Actions drawer. +- **History: Detail Stream event menu.** While viewing a review item in the Detail Stream, open the menu on a tracked object's event card and choose **Debug Replay**. The replay range is set automatically to that object's start and end times. +- **Explore: search result menu.** From an Explore card, open the kebab menu and choose **Debug Replay**. The range is taken from the tracked object's lifecycle. +- **Explore: Tracking Details Actions menu.** Open a tracked object's **Tracking Details** dialog, then choose **Debug Replay** from the Actions menu. Same automatic range as the search result menu. +- **Exports: export card menu.** From , open the menu on an export and choose **Debug Replay** to loop the exported clip through the detection pipeline for the camera it was exported from. + +The Detail Stream, Explore, and Exports entry points use the underlying recording or export's bounds with a small amount of padding. This can be convenient for quick checks, but if a detection is short or you want extra "settle" time for motion and the detector, start the replay from the History Actions menu instead and widen the range manually. + +### 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. +- In cases where a detection is short and a replay may only be a small number of frames, it is recommended to manually add some padding before and after the detection so that the motion and object detectors have time to settle into the scene. Rather than starting Debug Replay from Explore, navigate to History for your camera, choose Debug Replay from the Actions menu, and click the "From Timeline" or "Custom" option. +- The replay camera inherits the source camera's zones. Any automations that trigger on those zone names will fire for the replay camera as well. This can be helpful when debugging zone behavior, but may be unexpected. You can add a condition on the source camera's name in your automation if you want to exclude replay triggers. + +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 +cameras: + test: + ffmpeg: + inputs: + - path: /media/frigate/car-stopping.mp4 + input_args: -re -stream_loop -1 -fflags +genpts + roles: + - detect + detect: + enabled: true + record: + enabled: false + snapshots: + enabled: false +``` + +- `-re -stream_loop -1` tells ffmpeg to play the file in real time and loop indefinitely. +- `-fflags +genpts` generates presentation timestamps when they are missing in the file. + +### 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. +2. Add the temporary camera to `config/config.yml` (example above). Use a unique name such as `test` or `replay_camera` so it's easy to remove later. + - If you're debugging a specific camera, copy the settings from that camera (frame rate, model/enrichment settings, zones, etc.) into the temporary camera so the replay closely matches the original environment. Leave `record` and `snapshots` disabled unless you are specifically debugging recording or snapshot behavior. +3. Restart Frigate. +4. Observe the [Debug view](/usage/live#the-single-camera-view) in the UI and logs as the clip is replayed. Watch detections, zones, or any feature you're looking to debug, and note any errors in the logs to reproduce the issue. +5. Iterate on camera or enrichment settings (model, fps, zones, filters) and re-check the replay until the behavior is resolved. +6. Remove the temporary camera from your config after debugging to avoid spurious telemetry or recordings. + +### Troubleshooting + +- **No video**: verify the file path is correct and accessible from the Frigate process/container. +- **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. +- **No detections**: confirm the camera `roles` include `detect` and that the model/detector configuration is enabled. diff --git a/docs/docs/troubleshooting/edgetpu.md b/docs/docs/troubleshooting/edgetpu.md new file mode 100644 index 0000000..bc50ae5 --- /dev/null +++ b/docs/docs/troubleshooting/edgetpu.md @@ -0,0 +1,94 @@ +--- +id: edgetpu +title: EdgeTPU Errors +--- + +## USB Coral Not Detected + +There are many possible causes for a USB coral not being detected and some are OS specific. It is important to understand how the USB coral works: + +1. When the device is first plugged in and has not initialized it will appear as `1a6e:089a Global Unichip Corp.` when running `lsusb` or checking the hardware page in HA OS. +2. Once initialized, the device will appear as `18d1:9302 Google Inc.` when running `lsusb` or checking the hardware page in HA OS. + +:::tip + +Using `lsusb` or checking the hardware page in HA OS will show as `1a6e:089a Global Unichip Corp.` until Frigate runs an inference using the coral. So don't worry about the identification until after Frigate has attempted to detect the coral. + +::: + +If the coral does not initialize then Frigate can not interface with it. Some common reasons for the USB based Coral not initializing are: + +### Not Enough Power + +The USB coral can draw up to 900mA and this can be too much for some on-device USB ports, especially for small board computers like the RPi. If the coral is not initializing then some recommended steps are: + +1. Try a different port, some ports are capable of providing more power than others. +2. Make sure the port is USB3, this is important for power and to ensure the coral runs at max speed. +3. Try a different cable, some users have found the included cable to not work well. +4. Use an externally powered USB hub. + +### Incorrect Device Access + +The USB coral has different IDs when it is uninitialized and initialized. + +- When running Frigate in a VM, Proxmox lxc, etc. you must ensure both device IDs are mapped. +- When running through the Home Assistant OS you may need to run the Full Access variant of the Frigate App with the _Protection mode_ switch disabled so that the coral can be accessed. + +### Synology 716+II running DSM 7.2.1-69057 Update 5 + +Some users have reported that this older device runs an older kernel causing issues with the coral not being detected. The following steps allowed it to be detected correctly: + +1. Plug in the coral TPU in any of the USB ports on the NAS +2. Open the control panel - info screen. The coral TPU would be shown as a generic device. +3. Start the docker container with Coral TPU enabled in the config +4. The TPU would be detected but a few moments later it would disconnect. +5. While leaving the TPU device plugged in, restart the NAS using the reboot command in the UI. Do NOT unplug the NAS/power it off etc. +6. Open the control panel - info screen. The coral TPU will now be recognized as a USB Device - google inc +7. Start the frigate container. Everything should work now! + +### QNAP NAS + +QNAP NAS devices, such as the TS-253A, may use connected Coral TPU devices if [QuMagie](https://www.qnap.com/en/software/qumagie) is installed along with its QNAP AI Core extension. If any of the features (`facial recognition`, `object recognition`, or `similar photo recognition`) are enabled, Container Station applications such as `Frigate` or `CodeProject.AI Server` will be unable to initialize the TPU device in use. +To allow the Coral TPU device to be discovered, you must either: + +1. [Disable the AI recognition features in QuMagie](https://docs.qnap.com/application/qumagie/2.x/en-us/configuring-qnap-ai-core-settings-FB13CE03.html), +2. Remove the QNAP AI Core extension or +3. Manually start the QNAP AI Core extension after Frigate has fully started (not recommended). + +It is also recommended to restart the NAS once the changes have been made. + +## USB Coral Detection Appears to be Stuck + +The USB Coral can become stuck and need to be restarted, this can happen for a number of reasons depending on hardware and software setup. Some common reasons are: + +1. Some users have found the cable included with the coral to cause this problem and that switching to a different cable fixed it entirely. +2. Running Frigate in a VM may cause communication with the device to be lost and need to be reset. + +## PCIe Coral Not Detected + +The most common reason for the PCIe Coral not being detected is that the driver has not been installed. This process varies based on what OS and kernel that is being run. + +- In most cases https://github.com/jnicolson/gasket-builder can be used to build and install the latest version of the driver. + +## Attempting to load TPU as pci & Fatal Python error: Illegal instruction + +This is an issue due to outdated gasket driver when being used with new linux kernels. Installing an updated driver from https://github.com/jnicolson/gasket-builder has been reported to fix the issue. + +### Not detected on Raspberry Pi5 + +A kernel update to the RPi5 means an update to config.txt is required, see [the raspberry pi forum for more info](https://forums.raspberrypi.com/viewtopic.php?t=363682&sid=cb59b026a412f0dc041595951273a9ca&start=25) + +Specifically, add the following to config.txt + +``` +dtoverlay=pciex1-compat-pi5,no-mip +dtoverlay=pcie-32bit-dma-pi5 +``` + +## Only One PCIe Coral Is Detected With Coral Dual EdgeTPU + +Coral Dual EdgeTPU is one card with two identical TPU cores. Each core has its own PCIe interface and motherboard needs to have two PCIe busses on the m.2 slot to make them both work. + +E-key slot implemented to full m.2 electromechanical specification has two PCIe busses. Most motherboard manufacturers implement only one PCIe bus in m.2 E-key connector (this is why only one TPU is working). Some SBCs can have only USB bus on m.2 connector, ie none of TPUs will work. + +In this case it is recommended to use a Dual EdgeTPU Adapter [like the one from MagicBlueSmoke](https://github.com/magic-blue-smoke/Dual-Edge-TPU-Adapter) diff --git a/docs/docs/troubleshooting/faqs.md b/docs/docs/troubleshooting/faqs.md new file mode 100644 index 0000000..1a51cb6 --- /dev/null +++ b/docs/docs/troubleshooting/faqs.md @@ -0,0 +1,155 @@ +--- +id: faqs +title: Frequently Asked Questions +--- + +### Fatal Python error: Bus error + +This error message is due to a shm-size that is too small. Try updating your shm-size according to [this guide](../frigate/installation.md#calculating-required-shm-size). + +### How can I get sound or audio in my recordings? {#audio-in-recordings} + +By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to set a [FFmpeg preset](/configuration/ffmpeg_presets) that supports audio: + +```yaml +ffmpeg: + output_args: + record: preset-record-generic-audio-aac +``` + +### How can I get sound in live view? + +Audio is only supported for live view when go2rtc is configured, see [the live docs](../configuration/live.md) for more information. + +### I can't view recordings in the Web UI. + +Ensure your cameras send h264 encoded video, or [transcode them](/configuration/restream.md). + +You can open `chrome://media-internals/` in another tab and then try to playback, the media internals page will give information about why playback is failing. + +### What do I do if my cameras sub stream is not good enough? + +Frigate generally [recommends cameras with configurable sub streams](/frigate/hardware.md). However, if your camera does not have a sub stream that is a suitable resolution, the main stream can be resized. + +To do this efficiently the following setup is required: + +1. A GPU or iGPU must be available to do the scaling. +2. [ffmpeg presets for hwaccel](/configuration/hardware_acceleration_video.md) must be used +3. Set the desired detection resolution for `detect -> width` and `detect -> height`. + +When this is done correctly, the GPU will do the decoding and scaling which will result in a small increase in CPU usage but with better results. + +### My mjpeg stream or snapshots look green and crazy + +This almost always means that the width/height defined for your camera are not correct. Double check the resolution with VLC or another player. Also make sure you don't have the width and height values backwards. + +![mismatched-resolution](/img/mismatched-resolution-min.jpg) + +### "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found" + +These messages in the logs are expected in certain situations. Frigate checks the integrity of the recordings before storing. Occasionally these cached files will be invalid and cleaned up automatically. + +### "MQTT connected" repeats in the logs + +If you see repeated "MQTT connected" messages in your logs, check for another instance of Frigate. This happens when multiple Frigate containers are trying to connect to MQTT with the same `client_id`. + +### Error: Database Is Locked + +SQLite does not work well on a network share, if the `/media` folder is mapped to a network share then [this guide](../configuration/advanced/system.md#database) should be used to move the database to a location on the internal drive. + +### Unable to publish to MQTT: client is not connected + +If MQTT isn't working in docker try using the IP of the device hosting the MQTT server instead of `localhost`, `127.0.0.1`, or `mosquitto.ix-mosquitto.svc.cluster.local`. + +This is because Frigate does not run in host mode so localhost points to the Frigate container and not the host device's network. + +### How do I know if my camera is offline + +A camera being offline can be detected via MQTT or /api/stats, the camera_fps for any offline camera will be 0. + +Also, Home Assistant will mark any offline camera as being unavailable when the camera is offline. + +### How can I view the Frigate log files without using the Web UI? + +Frigate manages logs internally as well as outputs directly to Docker via standard output. To view these logs using the CLI, follow these steps: + +- Open a terminal or command prompt on the host running your Frigate container. +- Type the following command and press Enter: + ``` + docker logs -f frigate + ``` + This command tells Docker to show you the logs from the Frigate container. + Note: If you've given your Frigate container a different name, replace "frigate" in the command with your container's actual name. The "-f" option means the logs will continue to update in real-time as new entries are added. To stop viewing the logs, press `Ctrl+C`. If you'd like to learn more about using Docker logs, including additional options and features, you can explore Docker's [official documentation](https://docs.docker.com/engine/reference/commandline/logs/). + +Alternatively, when you create the Frigate Docker container, you can bind a directory on the host to the mountpoint `/dev/shm/logs` to not only be able to persist the logs to disk, but also to be able to query them directly from the host using your favorite log parsing/query utility. + +``` +docker run -d \ + --name frigate \ + --restart=unless-stopped \ + --mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \ + --device /dev/bus/usb:/dev/bus/usb \ + --device /dev/dri/renderD128 \ + --shm-size=64m \ + -v /path/to/your/storage:/media/frigate \ + -v /path/to/your/config:/config \ + -v /etc/localtime:/etc/localtime:ro \ + -v /path/to/local/log/dir:/dev/shm/logs \ + -e FRIGATE_RTSP_PASSWORD='password' \ + -p 5000:5000 \ + -p 8554:8554 \ + -p 8555:8555/tcp \ + -p 8555:8555/udp \ + ghcr.io/blakeblackshear/frigate:stable +``` + +### My RTSP stream works fine in VLC, but it does not work when I put the same URL in my Frigate config. Is this a bug? + +No. Frigate uses the TCP protocol to connect to your camera's RTSP URL. VLC automatically switches between UDP and TCP depending on network conditions and stream availability. So a stream that works in VLC but not in Frigate is likely due to VLC selecting UDP as the transfer protocol. + +TCP ensures that all data packets arrive in the correct order. This is crucial for video recording, decoding, and stream processing, which is why Frigate enforces a TCP connection. UDP is faster but less reliable, as it does not guarantee packet delivery or order, and VLC does not have the same requirements as Frigate. + +You can still configure Frigate to use UDP by using ffmpeg input args or the preset `preset-rtsp-udp`. See the [ffmpeg presets](/configuration/ffmpeg_presets) documentation. + +### Frigate is slow to start up with a "probing detect stream" message in the logs + +When `detect.width` and `detect.height` are not set, Frigate probes each camera's detect stream on startup (and when saving the config) to auto-detect its resolution. For RTSP streams Frigate probes with ffprobe and automatically retries over TCP if UDP doesn't respond, with a 5 second timeout per attempt. A camera that cannot be reached over either transport will add up to ~10 seconds to startup before Frigate falls through with default dimensions, which may show up as width `0` and height `0` in Camera Probe Info under System Metrics. + +To skip the probe entirely and make startup instant, set `detect.width` and `detect.height` explicitly in your camera config: + +```yaml +cameras: + my_camera: + detect: + width: 1280 + height: 720 +``` + +### Why does Frigate keep creating new tracked objects for my parked car? + +Stationary tracking is designed to _prevent_ this: a parked car should remain a single tracked object rather than generating new ones. If you're repeatedly getting new tracked objects for the same car, it's likely that Frigate is losing the object and re-detecting it as a new one. + +Open one of the tracked objects in Explore → **Tracking Details**. If the detection scores are low (< 70% or so), the model isn't confident the parked car is a car. This is common with the free [COCO-trained](https://cocodataset.org/#explore) object detection models on steep/top-down angles, partially occluded cars, foliage, or low-light footage. When detections fall below `min_score` for too many frames the tracker loses the object, and the next confident frame creates a brand new one. + +What helps: + +- **Improve the view**: even a small angle change that gets more of the car visible could lift scores enough to stabilize tracking. +- **Use a more accurate model**: switching from `mobiledet` to `yolov9`, or stepping up to a larger variant like `yolov9-s` over `yolov9-t`, can help (at the cost of inference time, and still on the COCO dataset). The biggest gains usually come from fine-tuning a model on images from your own cameras so it learns your specific scene. [Frigate+](https://frigate.video/plus) is a paid option that does this - models are trained on security-camera footage and can be fine-tuned on images you submit from your own setup. +- **Don't set `detect -> stationary -> max_frames` for `car`**: it artificially ends tracking and forces re-detection as a new object. See [Stationary Objects](../configuration/stationary_objects.md). +- **Restrict alerts to the areas you care about** with `required_zones`. See [Zones](../configuration/zones.md#restricting-alerts-and-detections-to-specific-zones). Make sure those zones use the default `loitering_time: 0` unless you specifically want the review item to stay open until the car leaves. +- **Filter impossible locations** with [object filter masks](../configuration/masks.md#object-filter-masks) if cars are being detected on rooftops, treetops, etc. + +See [Object Filters](../configuration/object_filters.md) for more on tuning `min_score` and `threshold`. Note that raising them too high will make this exact problem worse. + +### How do I correct Frigate when it detects something as the wrong object? + +Frigate's object detection relies on a machine learning [model](../frigate/glossary.md#model), and the free [COCO-trained](https://cocodataset.org/#explore) models that ship with Frigate can misidentify objects in scenes they weren't trained on. There are two ways to handle this, depending on whether you want to _teach_ the model or just _suppress_ the bad result. + +**Train or fine-tune a model with your own images.** The most durable fix is to improve the model itself. The biggest gains usually come from fine-tuning a model on images from your own cameras so it learns your specific scene. Some tools are freely available, and [Frigate+](https://frigate.video/plus) is a paid option that does this - models are trained on security-camera footage and can be fine-tuned on images you submit from your own setup. When Frigate mislabels something, open the tracked object in Explore, select the **Snapshot** tab, and use **Submit to Frigate+** to send the example with the correct label (or mark it as a [false positive](../frigate/glossary.md#false-positive)). Once you've submitted examples and [requested a model](../plus/first_model.md), the retrained model will be more accurate for your cameras. See [Submitting examples to Frigate+](../integrations/plus.md#submit-examples) for the full workflow. + +**Suppress the misidentification with filters.** You can use filters to stop a specific false positive from being tracked: + +- Tune `min_score` / `threshold`, or add `min_area` / `max_area` / `min_ratio` / `max_ratio` filters. See [Object Filters](../configuration/object_filters.md). +- If the false positive is always in the same fixed spot (like a statue or mailbox that reads as a person), add an [object filter mask](../configuration/masks.md#object-filter-masks) over that location. + +Filters and masks only hide the incorrect result - they don't teach Frigate what the object actually is. For that, fine-tune your own model or use Frigate+. diff --git a/docs/docs/troubleshooting/go2rtc.md b/docs/docs/troubleshooting/go2rtc.md new file mode 100644 index 0000000..b8f2102 --- /dev/null +++ b/docs/docs/troubleshooting/go2rtc.md @@ -0,0 +1,235 @@ +--- +id: go2rtc +title: Troubleshooting go2rtc +--- + +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + +This page covers common problems with the bundled [go2rtc](/configuration/go2rtc) and how to resolve them, whether your cameras were added with the setup wizard or configured by hand. + +When a stream won't play or behaves oddly, the most important first step is to figure out **where** in the pipeline it breaks. Frigate's live view is a chain (_camera → go2rtc → your browser_), and each stage fails for different reasons. Work through the checks below in order, then jump to the matching problem category. + +## Start by isolating the problem + +### 1. Read the go2rtc logs + +Access the go2rtc logs in the Frigate UI under in the sidebar (select the **go2rtc** tab). If go2rtc cannot connect to your camera you will usually see a clear error here: `401 Unauthorized` (bad or incorrectly encoded credentials), `Connection refused` / `timeout` (wrong IP, port, or the camera is at its connection limit), or `404 Not Found` (wrong RTSP path, or the referenced stream name does not exist). + +### 2. Test the stream in the go2rtc web interface + +If the logs look clean, open go2rtc's own web interface on port `1984`. This is the single most useful diagnostic, because it takes Frigate's UI out of the equation entirely. + +- If using Frigate through Home Assistant, enable the web interface at port `1984` (it is disabled by default, see [Home Assistant ports](#home-assistant-and-port-access)). +- If using Docker, forward port `1984` before accessing the web interface. + +Open the stream page for your camera (`http://:1984/stream.html?src=back`) and try each player link: + +- **If nothing plays here**, the problem is between the camera and go2rtc (codec, credentials, or transport), _not_ your browser. Fix it at the source before touching anything in Frigate. +- **If a player works here but Frigate's live view does not**, the problem is browser/codec related. Compare the **MSE** and **WebRTC** links. Frigate prefers MSE and only attempts WebRTC when MSE fails (or for two-way talk). If `mode=mse` plays but `mode=webrtc` does not, you have a [WebRTC codec problem](#webrtc-and-two-way-talk); if neither plays, your browser cannot decode the codec (commonly H.265, see [H.265 / HEVC cameras](#h265--hevc-cameras)). + +### 3. Inspect the negotiated codecs + +You can view detailed stream info, including the exact video and audio codecs go2rtc negotiated with the camera, at `http://frigate_ip:5000/api/go2rtc/streams` (or `http://frigate_ip:5000/api/go2rtc/streams/back` for a single camera). This is the authoritative answer to "what is my camera actually sending?" and is far more reliable than guessing from the camera's web UI. It also shows whether the audio track is `sendonly`/`recvonly`, which matters for [two-way talk](#webrtc-and-two-way-talk). + +### 4. Fix the codec with the FFmpeg module + +If the camera plays in go2rtc but not in your browser, the video or audio codec is unsupported. Browsers can reliably play **H.264** video and **AAC** audio; many cannot play H.265/HEVC, and some camera audio (G.711/PCM, MJPEG containers, etc.) is not playable at all. The fix is to have go2rtc re-encode the stream on demand using its FFmpeg module. + +In the Frigate UI this is the **Use compatibility mode (ffmpeg)** toggle on a stream source; in YAML it is the `ffmpeg:` prefix on the source URL. + + + + +1. Navigate to and expand your camera's stream. +2. On the source you want to convert, click the **Use compatibility mode (ffmpeg)** button (the sliders icon next to the URL). This routes the source through go2rtc's FFmpeg module and reveals the transcoding options. +3. Set **Video** to **Transcode to H.264** if your browser can't play the camera's video codec (e.g. H.265). Leave it on **Copy** to pass the video through untouched. This is much cheaper and should be your default whenever only the audio needs converting. +4. Set **Audio** to **Transcode to AAC** (for MSE) or **Transcode to Opus** (for WebRTC) if the camera's audio codec is unsupported. Leave it on **Copy** to keep the original, or **Exclude** to drop audio entirely. +5. When transcoding **video**, set **Hardware acceleration** to **Automatic (recommended)** so the encode runs on your GPU instead of the CPU. See [hardware-accelerated transcoding](#hardware-accelerated-transcoding-with-ffmpeg-8) for an important FFmpeg 8 caveat. +6. **Save** the section, then reload the live view. + + + + +```yaml +go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + # transcode video to H.264 on the GPU; only needed if the browser can't play the source codec + - "ffmpeg:back#video=h264#hardware" +``` + +To convert audio only (leaving video untouched), or to convert both: + +```yaml +go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + - "ffmpeg:back#audio=aac" # audio only, preferred when the video already plays + # or, to convert both video and audio: + # - "ffmpeg:back#video=h264#audio=aac#hardware" +``` + + + + +:::warning + +The `#`-modifiers (`#video=`, `#audio=`, `#hardware`, `#backchannel=0`, …) **only take effect on a source that is prefixed with `ffmpeg:`**. Adding them to a bare `rtsp://…#audio=opus` source does nothing: go2rtc ignores them. Likewise, when a source references another stream by name (e.g. `ffmpeg:back#audio=aac`), the name must match the stream key **exactly** (it is case sensitive), or the transcode is silently never produced. This is the single most common configuration mistake. In the Frigate UI, the **Use compatibility mode (ffmpeg)** toggle adds the `ffmpeg:` prefix for you. + +::: + +Transcoding video is resource intensive. Always prefer `#video=copy` (the **Copy** option) and only convert the track that is actually unsupported. If you must transcode video and have no hardware encoder available, the built-in jsmpeg view may be the better option. + +## Live view is black, buffering, or stuck in "low-bandwidth mode" + +When the live view shows a black screen, spins forever, or repeatedly drops to the lower-quality jsmpeg player ("low-bandwidth mode"), the stream almost always contains something the browser cannot decode over MSE, usually H.265 video or a non-AAC audio track. Confirm this in the go2rtc web UI (port `1984`): if MSE won't play there, Frigate can't play it either, since it uses the same pipeline. + +The fix is to produce an **H.264 + AAC** stream, either by changing your camera's firmware codecs or by transcoding in go2rtc (see [Fix the codec with the FFmpeg module](#4-fix-the-codec-with-the-ffmpeg-module)). A few other things worth checking: + +- **Set the camera's I-frame (keyframe) interval to match its frame rate** (or "1x" on Reolink), and avoid "smart"/"+" codecs like _H.264+_ or _H.265+_. A long keyframe interval delays the first decodable frame past Frigate's startup timeout, which forces the fallback to jsmpeg. See [camera settings recommendations](/configuration/live#camera-settings-recommendations). +- **A spinner that never clears, even though video plays in VLC**, is often an unplayable _audio_ track stalling playback. Drop or transcode the audio (see below). +- **Remote/VPN viewing that buffers** while the LAN is fine is usually latency/jitter exceeding MSE's startup buffer. Set up [WebRTC](/configuration/live#webrtc-extra-configuration), which drops late frames instead of buffering. + +The general live-view behavior (smart streaming, the MSE → WebRTC → jsmpeg fallback chain, and how to read browser console errors) is documented in detail in the [Live view FAQ](/configuration/live#live-view-faq). + +## H.265 / HEVC cameras + +H.265/HEVC playback in the browser is unreliable and version-dependent. WebRTC does not support H.265 on some browsers, and MSE/HEVC support varies by browser, OS, and whether a hardware decoder is present. An H.265 stream that plays fine in VLC, the go2rtc web UI, and Frigate's recordings can still be blank in a live view. + +For dependable live viewing, use **H.264** for the stream the live view consumes: + +- Point the live view at the camera's H.264 **substream** and keep the H.265 main stream for recording only, or +- Transcode H.265 → H.264 in go2rtc with the FFmpeg module and `#hardware` (software HEVC transcoding is very CPU heavy). + +Treat browser HEVC playback as best-effort. See also [H.265 cameras via Safari](/configuration/camera_specific#h265-cameras-via-safari). + +## No audio in Live view + +Live view audio has strict codec requirements that differ by player: **MSE requires AAC, PCMA, or PCMU**, and **WebRTC requires Opus, PCMA, or PCMU**. Many cameras default to a codec outside these sets (or to PCM/G.711), so the player loads video only and no audio control appears. + +The most robust approach is to provide both an AAC track (for MSE) and an Opus track (for WebRTC) on the same stream by transcoding audio with the FFmpeg module while copying the video: + + + + +1. Navigate to and expand the camera's stream. +2. Add a second **Source** that references the stream by name (e.g. the URL `ffmpeg:back`), enable **Use compatibility mode (ffmpeg)**, and set **Audio** to **Transcode to Opus** for WebRTC support. +3. Keep the original source as **Source 1** so MSE can use the camera's AAC (or transcode the first source's audio to AAC if the camera doesn't provide it). +4. **Save** the section. + + + + +```yaml +go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 # video + AAC for MSE + - "ffmpeg:back#audio=opus" # adds an Opus track for WebRTC +``` + +If the camera's native audio isn't AAC either, transcode both: + +```yaml +go2rtc: + streams: + back: + - "ffmpeg:rtsp://user:password@10.0.10.10:554/live0#video=copy#audio=aac" # video copy + AAC for MSE + - "ffmpeg:back#audio=opus" # Opus for WebRTC +``` + + + + +Setting the camera firmware to AAC (and H.264) avoids transcoding entirely and is always preferable when the camera supports it. For more detail and examples, see [Audio Support](/configuration/live#audio-support). + +## WebRTC and two-way talk + +WebRTC is only attempted when MSE fails or when using a camera's two-way talk feature; the "All Cameras" dashboard never uses it. When it doesn't work, the cause is almost always one of: + +- **Codec mismatch**: WebRTC cannot carry H.265 or AAC. The stream backing the WebRTC view must provide Opus (or PCMA/PCMU) audio and H.264 video. Add an `ffmpeg:back#audio=opus` source as shown above. +- **Port `8555` not reachable, or no candidates set**: WebRTC needs port `8555` (both TCP and UDP) open and a reachable candidate advertised. On Docker installs running on a custom/overlay network, go2rtc may advertise unreachable container IPs as ICE candidates; setting `webrtc.filters.candidates: []` and supplying only your host's LAN IP resolves this. See [WebRTC extra configuration](/configuration/live#webrtc-extra-configuration). +- **Two-way talk** additionally requires a secure context (HTTPS or the authenticated port `8971`, because browsers block microphone access on plain HTTP). The camera's RTSP backchannel must also be handled correctly: go2rtc seizes the backchannel by default, which blocks two-way audio for other consumers and can inject static. Disable it on the primary stream with `#backchannel=0` and use a separate dedicated stream for talk, as documented in [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream). + +## High CPU usage + +If go2rtc is using a lot of CPU, it is almost always transcoding in software. An FFmpeg source with a codec modifier like `#video=h264` or `#audio=aac` but **no** `#hardware` re-encodes on the CPU. (Frigate's `ffmpeg.hwaccel_args` only applies to Frigate's own detect/record processes. It does _not_ accelerate go2rtc's transcodes.) + +To keep CPU usage down: + +- Only transcode the track that is genuinely unsupported, and use `#video=copy` to pass video through untouched whenever possible. +- When you must transcode video, always add `#hardware` (the **Automatic** hardware option in the UI) so the encode runs on the GPU. Note the [FFmpeg 8 device requirement](#hardware-accelerated-transcoding-with-ffmpeg-8) below. +- Don't restream a high-resolution main stream just to feed the live view: even with `#video=copy`, muxing a 4K/8MP+ stream is inherently expensive. Use the camera's lower-resolution substream for live and detect, and let Frigate pull the main stream directly for recording. + +## Connection, authentication, and complex passwords + +If go2rtc logs `401 Unauthorized` for a URL that works in VLC, the password almost certainly contains reserved URL characters. **Frigate URL-encodes passwords for its own `cameras.ffmpeg.inputs`, but it does not touch what you write under `go2rtc.streams`**: go2rtc parses that URL itself. You must URL-encode special characters yourself in the `go2rtc.streams` section (`@` → `%40`, `#` → `%23`, `?` → `%3F`, `%` → `%25`, etc.). + +Note the asymmetry: under `cameras.ffmpeg.inputs` you should use the **raw** password (Frigate encodes it for you). Pre-encoding it there causes a double-encode and fails. See [Handling Complex Passwords](/configuration/restream#handling-complex-passwords). + +Repeated `401`/`Connection refused` errors can also mean the camera hit its **concurrent connection limit** or triggered a login lockout. Routing all roles through a single [RTSP restream](/configuration/restream#reduce-connections-to-camera) means the camera only ever sees one connection from go2rtc. + +## Stream names must match everywhere + +A surprising number of "the better live options aren't available" or `404 Not Found` problems come down to a name mismatch. The same string must be used consistently: + +- the **go2rtc stream key** (`go2rtc.streams.`), +- any `ffmpeg:#…` source that references it, +- the camera's restream input path (`rtsp://127.0.0.1:8554/`), and +- the camera name itself (so Frigate auto-maps it for MSE/WebRTC), or an explicit `live -> streams` mapping pointing at the go2rtc stream **name** (never a path). + +If you rename or remove a go2rtc stream while experimenting and the live stream selector then shows a blank entry, clear your browser's site data for the Frigate URL. The selected stream is cached per-device in local storage. + +## Camera-specific behavior + +Several camera brands have well-known quirks with go2rtc. Rather than repeat them here, see the [camera-specific configuration](/configuration/camera_specific) page, which covers them in detail. The highlights: + +- **Reolink**: RTSP is unreliable on many models; the **http-flv** stream through the FFmpeg module is recommended, and you must enable HTTP/RTMP in the camera and **reboot** it. 6MP+ models stream H.265 over http-flv-enhanced, which requires FFmpeg 8.0. See [Reolink Cameras](/configuration/camera_specific#reolink-cameras). +- **TP-Link Tapo**: use go2rtc's native `tapo://` source for stability and two-way audio; a stale RTSP credential can often be revived by clicking play once in the go2rtc web UI. +- **Ubiquiti/UniFi Protect**: use the `rtspx://` scheme (not `rtsps://…?enableSrtp`). +- **Amcrest/Dahua**: use the `/cam/realmonitor?channel=1&subtype=N` scheme, where `subtype=0` is the main stream. See [Amcrest & Dahua](/configuration/camera_specific#amcrest--dahua). + +## Non-RTSP sources and the FFmpeg module + +go2rtc's native zero-copy handling only supports well-formed RTSP H.264/H.265. Anything else (MJPEG, HTTP/HTTP-FLV, RTMP, or unusual codecs) must be handed to the FFmpeg module by prefixing the source with `ffmpeg:`. This is also necessary for some camera streams to be parsed at all, at the cost of slightly slower startup. MJPEG and other non-H.264 sources additionally need `#video=h264` (with `#hardware`) before they can be used for the `record`, `detect`, or restream roles. See [MJPEG Cameras](/configuration/camera_specific#mjpeg-cameras) for a complete example. + +## Hardware-accelerated transcoding with FFmpeg 8 + +Frigate 0.18 ships **FFmpeg 8.0** as the default, and FFmpeg 8 is stricter about hardware-accelerated filtering than earlier versions. Whenever go2rtc transcodes video with hardware acceleration (any source using `#hardware`, `#hardware=vaapi`, or the **Automatic** hardware option in the UI), it builds a filter chain that uploads frames to the GPU with the `hwupload` filter. FFmpeg 8 now refuses to do this unless it is told **which device** to use. Earlier versions selected one automatically. The result is that an otherwise-working transcode fails to start, the live view never loads, and go2rtc logs: + +``` +[hwupload] A hardware device reference is required to upload frames to. +[AVFilterGraph] Error initializing filters +Error opening output files: Invalid argument +``` + +The fix is to tell go2rtc's bundled FFmpeg which hardware device to use via the `go2rtc -> ffmpeg -> global` option. For **VAAPI**-based acceleration (which covers most Intel and AMD GPUs, and is what go2rtc selects automatically on that hardware), point it at your render device: + +```yaml +go2rtc: + ffmpeg: + global: "-vaapi_device /dev/dri/renderD128" + streams: + back: + - "ffmpeg:rtsp://user:password@10.0.10.10:554/live0#video=h264#hardware" +``` + +`/dev/dri/renderD128` is the usual render node; on a system with more than one GPU you may need `renderD129` (or higher), and the device must be passed into the container (e.g. `devices: - /dev/dri:/dev/dri` in Docker Compose). + +If you use a **different hardware acceleration backend**, you will likely need to specify its device in the same way, using the option that matches that backend instead of `-vaapi_device`. See the [go2rtc FFmpeg source documentation](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-ffmpeg) and the upstream report ([go2rtc issue #1984](https://github.com/AlexxIT/go2rtc/issues/1984)) for background and other examples. + +:::tip + +If you don't transcode in go2rtc with hardware acceleration, this does not affect you. If you want to avoid the change entirely, you can pin Frigate (and the go2rtc it bundles) back to FFmpeg 7.0 by setting `ffmpeg -> path: "7.0"` in your config. + +::: + +## Home Assistant and port access + +When running Frigate as a Home Assistant App, the go2rtc API (port `1984`), the RTSP restream (port `8554`), and WebRTC (port `8555`) are **disabled and hidden by default**. To use them (for example to reach the go2rtc web interface for troubleshooting, or to open a go2rtc stream externally in an app like VLC), go to , click **Show disabled ports**, enable the port you need, and save. Use the host's IP address rather than an mDNS name like `homeassistant.local`. + +If live view works in the Frigate UI but not in Home Assistant, the most common cause is the go2rtc stream name not matching the camera name: name the primary go2rtc stream exactly like the camera, or add a `live -> streams` mapping, so the integration can resolve the restream. diff --git a/docs/docs/troubleshooting/gpu.md b/docs/docs/troubleshooting/gpu.md new file mode 100644 index 0000000..a39386b --- /dev/null +++ b/docs/docs/troubleshooting/gpu.md @@ -0,0 +1,56 @@ +--- +id: gpu +title: GPU Errors +--- + +## OpenVINO + +### Can't get OPTIMIZATION_CAPABILITIES property as no supported devices found. + +Some users have reported issues using some Intel iGPUs with OpenVINO, where the GPU would not be detected. This error can be caused by various problems, so it is important to ensure the configuration is setup correctly. Some solutions users have noted: + +- In some cases users have noted that an HDMI dummy plug was necessary to be plugged into the motherboard's HDMI port. +- When mixing an Intel iGPU with Nvidia GPU, the devices can be mixed up between `/dev/dri/renderD128` and `/dev/dri/renderD129` so it is important to confirm the correct device, or map the entire `/dev/dri` directory into the Frigate container. + +## Intel/AMD GPU + +### Hardware acceleration is not being used + +For VAAPI or QSV to work, the GPU's render device must be passed through to the Frigate container. Intel and AMD GPUs expose this as a render node under `/dev/dri`, usually `/dev/dri/renderD128`. If it is not passed through, hardware acceleration is unavailable: ffmpeg fails to initialize it (for example `Failed to open the drm device` or `No VA display found for device`) and GPU usage stays at zero while CPU usage remains high. + +Pass the render device through when starting the container. With `docker compose`: + +```yaml +services: + frigate: + devices: + - /dev/dri/renderD128:/dev/dri/renderD128 # Intel / AMD GPU, update for your hardware +``` + +Or with `docker run`, add `--device /dev/dri/renderD128`. See the [installation docs](/frigate/installation) for a complete example. + +If it still isn't working after passing the device through: + +- **Confirm the render node exists and is the correct one.** Run `ls /dev/dri` on the host. You should see one or more `renderD12X` entries. Systems with more than one GPU (an Intel iGPU plus a discrete GPU) can expose both `/dev/dri/renderD128` and `/dev/dri/renderD129`, and the numbering is not guaranteed. Pass through the correct node, or map the entire directory (`/dev/dri:/dev/dri`, or `--device /dev/dri`) so all render nodes are available. +- **Check device permissions.** The Frigate process must be able to access the render node. This is usually automatic when the container runs as root (the default), but nested setups such as an unprivileged Proxmox/LXC container often require making the device accessible on the host (for example, a world-readable render node) or running the container privileged. Note that running Frigate inside an LXC is not officially supported. See the [installation docs](/frigate/installation#proxmox) for details. + +### Failed to download frame: -5 + +When using VAAPI or QSV hardware acceleration, ffmpeg may crash and restart periodically with a signature like this in the `ffmpeg..detect` log: + +``` +[AVHWFramesContext @ 0x...] Failed to sync surface ... (operation failed). +[hwdownload @ 0x...] Failed to download frame: -5. +[vf#0:0 @ 0x...] Error while filtering: Input/output error +[vf#0:0 @ 0x...] Task finished with error code: -5 (Input/output error) +[frigate.video] : Unable to read frames from ffmpeg process. +``` + +This is a hardware frame synchronization failure between ffmpeg and the GPU driver, not a Frigate bug. It comes from how a specific camera stream interacts with the GPU's decode and scaling path, so it is highly dependent on your hardware, driver, and stream. Frigate's automatic hardware acceleration detection is a best-guess effort, so the fix is usually to tune the configuration for your specific hardware and camera. The solutions below are ordered from most to least likely to help: + +- **Switch between the VAAPI and QSV presets.** On Intel Gen 12 and newer iGPUs, `preset-intel-qsv-h264` / `preset-intel-qsv-h265` is often more stable than the auto-detected `preset-vaapi`. See the [hardware acceleration docs](/configuration/hardware_acceleration_video.md#intel-based-cpus) for the recommended preset for your Intel generation. +- **Try a different VAAPI driver.** The default driver is `iHD`. On older Intel CPUs, `LIBVA_DRIVER_NAME=i965` can be more stable; on AMD GPUs use `LIBVA_DRIVER_NAME=radeonsi`. See [the hardware acceleration docs](/configuration/hardware_acceleration_video.md#intel-based-cpus) for how to set the driver. +- **Use a codec that decodes more reliably.** H.265/HEVC streams may trigger this error far more often than H.264 depending on your CPU generation. If your camera exposes a separate sub-stream, assign an H.264 stream to the `detect` role. Cameras that output full-range YUV (for example some Hikvision models) are especially prone to it. +- **Match the detect resolution to the stream resolution.** When the `detect` resolution differs from the stream, Frigate inserts a GPU scaling filter (`scale_vaapi`), which is where these surface-sync failures can often originate. Set the `detect` `width` and `height` to match the exact resolution of the stream assigned the `detect` role. +- **Match the detect `fps` to the camera stream.** Aggressively dropping frames (for example `detect` `fps: 1` on a stream that runs at 15 fps) can cause timing mismatches in the GPU's frame buffer. Lower the sub-stream's frame rate on the camera itself instead of dropping most frames in Frigate. +- **Fall back to software decoding.** If none of the above resolve it, remove the preset for that camera (`hwaccel_args: []`). Hardware decoding is only an optimization. On a capable CPU, software-decoding a low-resolution sub-stream is inexpensive and gives a stable detect pipeline. diff --git a/docs/docs/troubleshooting/memory.md b/docs/docs/troubleshooting/memory.md new file mode 100644 index 0000000..d062944 --- /dev/null +++ b/docs/docs/troubleshooting/memory.md @@ -0,0 +1,134 @@ +--- +id: memory +title: Memory Usage +--- + +Frigate includes built-in memory profiling using [memray](https://bloomberg.github.io/memray/) to help diagnose memory issues. This feature allows you to profile specific Frigate modules to identify memory leaks, excessive allocations, or other memory-related problems. + +## Enabling Memory Profiling + +Memory profiling is controlled via the `FRIGATE_MEMRAY_MODULES` environment variable. Set it to a comma-separated list of module names you want to profile: + +```yaml +# docker-compose example +services: + frigate: + ... + environment: + - FRIGATE_MEMRAY_MODULES=frigate.embeddings,frigate.capture +``` + +```bash +# docker run example +docker run -e FRIGATE_MEMRAY_MODULES="frigate.embeddings" \ + ... + --name frigate +``` + +### Module Names + +Frigate processes are named using a module-based naming scheme. Common module names include: + +- `frigate.review_segment_manager` - Review segment processing +- `frigate.recording_manager` - Recording management +- `frigate.capture` - Camera capture processes (all cameras with this module name) +- `frigate.process` - Camera processing/tracking (all cameras with this module name) +- `frigate.output` - Output processing +- `frigate.audio_manager` - Audio processing +- `frigate.embeddings` - Embeddings processing + +You can also specify the full process name (including camera-specific identifiers) if you want to profile a specific camera: + +```bash +FRIGATE_MEMRAY_MODULES=frigate.capture:front_door +``` + +When you specify a module name (e.g., `frigate.capture`), all processes with that module prefix will be profiled. For example, `frigate.capture` will profile all camera capture processes. + +## How It Works + +1. **Binary File Creation**: When profiling is enabled, memray creates a binary file (`.bin`) in `/config/memray_reports/` that is updated continuously in real-time as the process runs. + +2. **Automatic HTML Generation**: On normal process exit, Frigate automatically: + + - Stops memray tracking + - Generates an HTML flamegraph report + - Saves it to `/config/memray_reports/.html` + +3. **Crash Recovery**: If a process crashes (SIGKILL, segfault, etc.), the binary file is preserved with all data up to the crash point. You can manually generate the HTML report from the binary file. + +## Viewing Reports + +### Automatic Reports + +After a process exits normally, you'll find HTML reports in `/config/memray_reports/`. Open these files in a web browser to view interactive flamegraphs showing memory usage patterns. + +### Manual Report Generation + +If a process crashes or you want to generate a report from an existing binary file, you can manually create the HTML report: + +- Run `memray` inside the Frigate container: + +```bash +docker-compose exec frigate memray flamegraph /config/memray_reports/.bin +# or +docker exec -it memray flamegraph /config/memray_reports/.bin +``` + +- You can also copy the `.bin` file to the host and run `memray` locally if you have it installed: + +```bash +docker cp :/config/memray_reports/.bin /tmp/ +memray flamegraph /tmp/.bin +``` + +## Understanding the Reports + +Memray flamegraphs show: + +- **Memory allocations over time**: See where memory is being allocated in your code +- **Call stacks**: Understand the full call chain leading to allocations +- **Memory hotspots**: Identify functions or code paths that allocate the most memory +- **Memory leaks**: Spot patterns where memory is allocated but not freed + +The interactive HTML reports allow you to: + +- Zoom into specific time ranges +- Filter by function names +- View detailed allocation information +- Export data for further analysis + +## Best Practices + +1. **Profile During Issues**: Enable profiling when you're experiencing memory issues, not all the time, as it adds some overhead. + +2. **Profile Specific Modules**: Instead of profiling everything, focus on the modules you suspect are causing issues. + +3. **Let Processes Run**: Allow processes to run for a meaningful duration to capture representative memory usage patterns. + +4. **Check Binary Files**: If HTML reports aren't generated automatically (e.g., after a crash), check for `.bin` files in `/config/memray_reports/` and generate reports manually. + +5. **Compare Reports**: Generate reports at different times to compare memory usage patterns and identify trends. + +## Troubleshooting + +### No Reports Generated + +- Check that the environment variable is set correctly +- Verify the module name matches exactly (case-sensitive) +- Check logs for memray-related errors +- Ensure `/config/memray_reports/` directory exists and is writable + +### Process Crashed Before Report Generation + +- Look for `.bin` files in `/config/memray_reports/` +- Manually generate HTML reports using: `memray flamegraph .bin` +- The binary file contains all data up to the crash point + +### Reports Show No Data + +- Ensure the process ran long enough to generate meaningful data +- Check that memray is properly installed (included by default in Frigate) +- Verify the process actually started and ran (check process logs) + +For more information about memray and interpreting reports, see the [official memray documentation](https://bloomberg.github.io/memray/). diff --git a/docs/docs/troubleshooting/recordings.md b/docs/docs/troubleshooting/recordings.md new file mode 100644 index 0000000..8acf0f5 --- /dev/null +++ b/docs/docs/troubleshooting/recordings.md @@ -0,0 +1,313 @@ +--- +id: recordings +title: Recordings Errors +--- + +## Why are my recordings not working? (empty Recordings, "No recordings found for this time") + +If Frigate shows live video but the History view is empty, or you see "No recordings found for this time", the cause is almost always in one of the three categories below. Segments are first written to the RAM cache and are only moved to disk if they match a retention policy _and_ the camera's `record` stream is producing valid, storable video. Work through the categories in order: retention configuration is by far the most common cause. + +Before diving in, enable debug logging for the recording maintainer so you can see whether segments are being written to disk at all: + +```yaml +logger: + logs: + frigate.record.maintainer: debug +``` + +A healthy camera logs lines like `Copied /media/frigate/recordings/{segment_path} in 0.2 seconds`. If you never see these, no segments are reaching disk, which points at the camera/stream or storage sections below. + +### Retention configuration issues + +#### Recording is enabled, but nothing is saved + +This is the single most common cause. Setting `record.enabled: True` on its own does **not** keep any footage: **continuous recording is disabled by default**, and segments in the cache are only moved to disk if they match a configured retention policy. You must configure at least one of `continuous`, `motion`, `alerts`, or `detections` retention. + +To store all video (the most conservative option), configure continuous retention: + +```yaml +record: + enabled: True + continuous: + days: 3 # keep all footage for 3 days +``` + +See [Recording](/configuration/record) for the full set of common configurations, including reduced-storage and alerts-only setups. + +#### Motion or event-only recording keeps less than you expect + +If you only configured `motion`, `alerts`, or `detections` retention (with no `continuous`), Frigate keeps footage selectively based on the retention `mode`: + +- **`mode: motion`** (the default) only retains segments that contain motion. If your [motion masks](/configuration/motion_detection) cover the areas where activity happens, or your motion sensitivity is too low, nothing will be retained even though recording is "on". +- **`mode: active_objects`** only retains segments where a tracked object was actively moving. +- **`mode: all`** retains every segment in the window. + +If you expected continuous footage but only configured motion/event retention, add a `continuous` retention period as shown above. To verify motion is actually being detected, watch the motion boxes in the debug view or the Motion Tuner in the UI. + +#### Alert and detection recordings require working object detection + +`alerts` and `detections` retention only keep footage that overlaps a tracked object, so they depend on object detection running: + +- **Detection must be enabled.** If `detect: enabled: False`, no alerts or detections are ever created, so alert/detection retention keeps nothing. (Continuous and motion retention still work with detection disabled.) +- **The object must be supported by your model.** If you track an object your model doesn't support (for example `deer` or `license_plate` on the default model), Frigate never detects it and never records for it. Check your logs for warnings such as `... is configured to track ['deer'] objects, which are not supported by the current model` and remove unsupported objects or switch to a model (e.g. [Frigate+](/plus/)) that includes them. + +#### You're following an outdated guide + +Configuration keys change between major versions. The old `clips` config, for example, has not existed for a long time. If you copied a config from an old blog post or video, verify every key against the current [reference config](/configuration/advanced/reference). + +### Camera and stream issues + +#### Incompatible audio codec (recordings silently fail to save) + +Frigate stores recordings in an MP4 container, and some camera audio codecs (most commonly `pcm_alaw`, `pcm_mulaw`, or other G.711 variants) **cannot be placed in an MP4 container**. When this happens, ffmpeg fails to write the segment and no recording is saved, even though the live view works fine. This is a frequent cause on Tapo, TP-Link VIGI, and some Reolink cameras. + +Transcode the audio to AAC (or drop it entirely) using the appropriate [ffmpeg preset](/configuration/ffmpeg_presets): + +```yaml +cameras: + your_camera: + ffmpeg: + output_args: + record: preset-record-generic-audio-aac # transcode audio to AAC + # or preset-record-generic to record with no audio +``` + +#### The record stream isn't connecting + +A message like `No new recording segments were created for in the last 120s` means ffmpeg cannot read the `record` stream. To diagnose: + +- Confirm a stream is actually assigned the `record` role in your camera's `ffmpeg.inputs`. +- Open the go2rtc web interface on port `1984` and click each stream to confirm it plays. go2rtc errors such as `wrong response on DESCRIBE` or `start from CONN state` indicate the camera connection is failing. +- Test the exact RTSP URL (with the correct path, port, and credentials) in VLC or `ffplay`. +- If you restream through go2rtc, make sure the `record` input path points at the correct go2rtc stream name. Copying a config between cameras without updating the stream name is a common mistake. + +#### Recordings play back with no video (or won't play at all) + +Frigate copies the `record` stream directly without re-encoding, so playback depends on your browser supporting the camera's codec. H265/HEVC recordings may not be playable in some browsers. If recordings appear as audio-only or a black screen, your camera is likely sending a codec your browser can't decode. Configure the camera to output **H264** for maximum compatibility. + +#### Segments are only ~1 second long + +If the record stream uses a "Smart Codec"/H.264+ mode or changes encoding parameters mid-stream, corrupt timestamps cause segments to be split far too frequently and fill the cache. This produces the "Too many unprocessed recording segments" warning. See [that section below](#i-see-the-message-warning--too-many-unprocessed-recording-segments-in-cache-for-camera-this-likely-indicates-an-issue-with-the-detect-stream) for the full diagnosis. + +### Storage and mounting issues + +#### The storage volume isn't mounted correctly + +If the recordings volume (`/media/frigate`) points at the wrong location, isn't writable, or a network/encrypted mount failed to mount at boot, Frigate cannot save recordings, or it silently writes to the boot drive and then purges aggressively because the drive appears far smaller than expected. + +- Compare the host's real capacity (`df -h`) against what the **Storage** page in the Frigate UI reports. A mismatch (for example Frigate reporting ~220 GB when your storage drive is 4 TB) means the bind mount is resolving to the wrong filesystem. +- Verify the host path in your Docker `volumes` mapping (`- /your/storage:/media/frigate`) exists and is writable by the container. +- For a mount that may fail intermittently, protecting the mount point with `chattr +i` on an empty directory forces Frigate to error out (rather than silently writing to the boot drive) when the mount is missing. +- Check `dmesg` and system logs for filesystem or I/O errors around the time recordings disappeared. + +If recordings _are_ being written but the copy is too slow to keep up, see the ["Unable to keep up with recording segments"](#i-see-the-message-warning--unable-to-keep-up-with-recording-segments-in-cache-for-camera-keeping-the-5-most-recent-segments-out-of-6-and-discarding-the-rest) section below. + +## I have Frigate configured for motion recording only, but it still seems to be recording even with no motion. Why? + +You'll want to: + +- Make sure your camera's timestamp is masked out with a motion mask. Even if there is no motion occurring in your scene, your motion settings may be sensitive enough to count your timestamp as motion. +- If you have audio detection enabled, keep in mind that audio that is heard above `min_volume` is considered motion. +- [Tune your motion detection settings](/configuration/motion_detection) either by editing your config file or by using the UI's Motion Tuner. + +## I see the message: WARNING : Unable to keep up with recording segments in cache for camera. Keeping the 5 most recent segments out of 6 and discarding the rest... + +This warning means the recording maintainer cannot move recording segments from the RAM cache to disk fast enough. When the cache fills up, Frigate discards the oldest segments to avoid running out of memory and crashing, so you lose recorded footage. This is almost always a storage throughput or system resource problem. Work through the steps below to identify which. + +### Step 1: Enable recording debug logging + +The first step is to measure how long each segment takes to move from the RAM cache to disk. Enable debug logging for the recording maintainer: + +```yaml +logger: + logs: + frigate.record.maintainer: debug +``` + +This adds log lines showing the copy duration for each segment: + +``` +DEBUG : Copied /media/frigate/recordings/{segment_path} in 0.2 seconds. +``` + +Let this run until the warnings begin to appear, so you can confirm whether the disk is actually slowing down at the moment the error occurs. + +### Step 2: Interpret the copy times + +The copy duration tells you which direction to investigate: + +- **Consistently longer than ~1 second**: your storage cannot keep up with the incoming recordings. Continue with Steps 3–5 to diagnose the slow storage. +- **Consistently well under 1 second**: storage is fast enough, and the problem is more likely CPU or resource contention. Skip to Step 6. + +### Step 3: Check RAM, swap, cache, and disk utilization + +If CPU, RAM, disk throughput, or bus I/O is insufficient, nothing inside Frigate will help. Review each aspect of available system resources while the warnings are occurring. + +On Linux, some helpful tools/commands for diagnosing this are: + +- `docker stats` +- `htop` +- `iotop -o` +- `iostat -sxy --human 1 1` +- `vmstat 1` + +On modern Linux kernels, the system will use some swap if it is enabled. Setting `vm.swappiness=1` no longer means the kernel will only swap in order to avoid OOM. To prevent any swapping inside the container, set the memory and memory+swap allocations to the same value and disable swapping by setting the following docker/podman run parameters: + +**Docker Compose example** + +```yaml +services: + frigate: + ... + mem_swappiness: 0 + memswap_limit: + deploy: + resources: + limits: + memory: +``` + +**Run command example** + +``` +--memory= --memory-swap= --memory-swappiness=0 +``` + +NOTE: These are hard limits for the container, so be sure there is enough headroom above what `docker stats` shows for your container. It will immediately halt if it hits ``. In general, keeping all cache and tmp filespace in RAM is preferable to disk I/O where possible. + +### Step 4: Check your storage type + +Mounting a network share is a popular option for storing recordings, but it can lead to reduced copy times and cause problems. Some users have found that using `NFS` instead of `SMB` considerably decreased copy times and fixed the issue. It is also important to ensure that the network connection between the device running Frigate and the network share is stable and fast. A saturated or unreliable link will stall copies. + +### Step 5: Check your mount options + +Some users found that mounting a drive via `fstab` with the `sync` option dramatically reduced performance and led to this issue. Using `async` instead greatly reduced copy times. + +### Step 6: Rule out CPU load + +If the copy times are consistently under 1 second but you still see the warning, the machine's CPU load is likely too high for Frigate to have the resources to keep up. Try temporarily shutting down other services, and any resource-intensive Frigate features, to see if the issue improves. + +## I see the message: WARNING : Too many unprocessed recording segments in cache for camera. This likely indicates an issue with the detect stream... + +This warning means that the detect stream for the affected camera has fallen behind or stopped processing frames. Frigate's recording cache holds segments waiting to be analyzed by the detector. When more than 6 segments pile up without being processed, Frigate discards the oldest ones to prevent the cache from filling up. + +:::warning + +This error is a **symptom**, not the root cause. The actual cause is always logged **before** these messages start appearing. You must review the full logs from Frigate startup through the first occurrence of this warning to identify the real issue. + +::: + +### Step 1: Get the full logs + +Collect complete Frigate logs from startup through the first occurrence of the error. Look for errors or warnings that appear **before** the "Too many unprocessed" messages begin. That is where the root cause will be found. + +### Step 2: Check the cache directory + +Exec into the Frigate container and inspect the recording cache: + +``` +docker exec -it frigate ls -la /tmp/cache +``` + +Each camera should have a small number of `.mp4` segment files. If one camera has significantly more files than others, that camera is the source of the problem. A problem with a single camera can cascade and cause all cameras to show this error. + +### Step 3: Verify segment duration + +Recording segments should be approximately 10 seconds long. Run `ffprobe` on segments in the cache to check: + +``` +docker exec -it frigate ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1 /tmp/cache/@.mp4 +``` + +If segments are only ~1 second instead of ~10 seconds, the camera is sending corrupt timestamp data, causing segments to be split too frequently and filling the cache 10x faster than expected. + +**Common causes of short segments:** + +- **"Smart Codec" or "Smart+" enabled on the camera**: These features dynamically change encoding parameters mid-stream, which corrupts timestamps. Disable them in your camera's settings. +- **Changing codec, bitrate, or resolution mid-stream**: Any encoding changes during an active stream can cause unpredictable segment splitting. +- **Camera firmware bugs**: Check for firmware updates from your camera manufacturer. + +:::tip + +You don't have to run `ffprobe` by hand to catch this. Open a camera's **Camera Probe Info** dialog (the info icon on the System → Metrics → Cameras page) and check the **Keyframe analysis** section. It probes the record stream and flags sparse or variable keyframes, which is what smart/"+" codecs (H.264+/H.265+) and long keyframe intervals produce. + +::: + +### Step 4: Check for a stuck detector + +If the detect stream is not processing frames, segments will accumulate. Common causes: + +- **Detection resolution too high**: Use a substream for detection, not the full resolution main stream. +- **Detection FPS too high**: 5 fps is the recommended maximum for detection. +- **Model too large**: Use smaller model variants (e.g., YOLO `s` or `t` size, not `e` or `x`). Use 320x320 input size rather than 640x640 unless you have a powerful dedicated detector. +- **Virtualization**: Running Frigate in a VM (especially Proxmox) can cause the detector to hang or stall. This is a known issue with GPU/TPU passthrough in virtualized environments and is not something Frigate can fix. Running Frigate in Docker on bare metal is recommended. + +### Step 5: Check for GPU hangs + +On the host machine, check `dmesg` for GPU-related errors: + +``` +dmesg | grep -i -E "gpu|drm|reset|hang" +``` + +Messages like `trying reset from guc_exec_queue_timedout_job` or similar GPU reset/hang messages indicate a driver or hardware issue. Ensure your kernel and GPU drivers (especially Intel) are up to date. + +### Step 6: Verify hardware acceleration configuration + +An incorrect `hwaccel_args` preset can cause ffmpeg to fail silently or consume excessive CPU, starving the detector of resources. + +- After upgrading Frigate, verify your preset matches your hardware (e.g., `preset-intel-qsv-h264` instead of the deprecated `preset-vaapi`). +- For h265 cameras, use the corresponding h265 preset (e.g., `preset-intel-qsv-h265`). +- Note that `hwaccel_args` are only relevant for the detect stream. Frigate does not decode the record stream. + +### Step 7: Verify go2rtc stream configuration + +Ensure that the ffmpeg source names in your go2rtc configuration match the correct camera stream. A misconfigured stream name (e.g., copying a config from one camera to another without updating the stream reference) will cause the wrong stream to be used or the stream to fail entirely. + +### Step 8: Check system resources + +If none of the above apply, the issue may be a general resource constraint. Monitor the following on your host: + +- **CPU usage**: An overloaded CPU can prevent the detector from keeping up. +- **RAM and swap**: Excessive swapping dramatically slows all I/O operations. +- **Disk I/O**: Use `iotop` or `iostat` to check for saturation. +- **Storage space**: Verify you have free space on the Frigate storage volume (check the Storage page in the Frigate UI). + +Try temporarily disabling resource-intensive features like `genai` and `face_recognition` to see if the issue resolves. This can help isolate whether the detector is being starved of resources. + +## I see the message: ERROR : Error occurred when attempting to maintain recording cache + +This message means the recording maintainer hit an error while moving segments from the cache to disk. It is a **generic wrapper**: the actual cause is always logged on the **very next line**. Frigate usually recovers and keeps running, but any affected segments are lost, so it is worth resolving. + +:::warning + +Always read the line immediately following this message. `Error occurred when attempting to maintain recording cache` on its own tells you nothing; the exception on the next line (for example `[Errno 28] No space left on device` or `[Errno 17] File exists`) is the real problem. + +::: + +Because these are operating-system-level errors, they must be resolved on the **host**, not within Frigate's configuration. The most common underlying errors are below. + +### [Errno 28] No space left on device + +The filesystem Frigate is writing to is full. Things to check: + +- **The recordings volume is genuinely full.** Check free space on the host with `df -h` for the path mapped to `/media/frigate`, and review the **Storage** page in the Frigate UI. +- **The disk shows free space but is still "full".** This usually means the filesystem has run out of **inodes** (check with `df -i`), or recordings are landing on a different, smaller filesystem than you expect because of an incorrect bind mount. See [The storage volume isn't mounted correctly](#the-storage-volume-isnt-mounted-correctly) above. +- **`/tmp/cache` is full.** If you mounted `/tmp/cache` as a small `tmpfs`, a backlog of segments can fill it. Increase the tmpfs size, or address whatever is causing segments to pile up (see the [Too many unprocessed recording segments](#i-see-the-message-warning--too-many-unprocessed-recording-segments-in-cache-for-camera-this-likely-indicates-an-issue-with-the-detect-stream) section above). +- **The host blocks writes before Frigate can purge.** On some systems (for example Unraid with a fill-up threshold), the host stops writes before Frigate's emergency cleanup can run. Leave more headroom on the volume, or lower your retention so Frigate purges sooner. + +### [Errno 17] File exists (with ffmpeg "Error writing trailer" or "unable to re-open output file") + +Errors like `[Errno 17] File exists: '/media/frigate/recordings/.../'`, often alongside ffmpeg errors such as `Unable to re-open ... output file for shifting data` or `Error writing trailer: No such file or directory`, are a hallmark of an **unreliable network share** (NFS or SMB). The mount is dropping, serving stale directory entries, or mishandling file locking. + +- Confirm the network connection to the NAS is stable and fast. An intermittent link produces these errors sporadically. +- Prefer **NFS over SMB** for the recordings mount; several users have found NFS more reliable and faster. +- Review your `fstab`/mount options for settings that hurt consistency or performance (see the `sync` vs `async` note in the [Unable to keep up with recording segments](#i-see-the-message-warning--unable-to-keep-up-with-recording-segments-in-cache-for-camera-keeping-the-5-most-recent-segments-out-of-6-and-discarding-the-rest) section above). +- Enable `frigate.record.maintainer` debug logging to confirm whether the errors line up with the share becoming unavailable. + +### Errors referencing a camera you manually renamed or removed + +If the next-line error references a camera name that no longer exists in your config, orphaned data is left over from a rename or removal in a persistent `/tmp/cache` volume. + +- Using a `tmpfs` mount for `/tmp/cache` as recommended in the [installation docs](/frigate/installation#storage) prevents stale cache files under the old camera name from surviving a restart, which avoids this issue entirely. +- If errors persist, stop Frigate and remove any leftover segments for the old camera name from `/tmp/cache`. diff --git a/docs/docs/usage/explore.md b/docs/docs/usage/explore.md new file mode 100644 index 0000000..49cdc82 --- /dev/null +++ b/docs/docs/usage/explore.md @@ -0,0 +1,101 @@ +--- +id: explore +title: Explore +--- + +import NavPath from "@site/src/components/NavPath"; + +**Explore** is where you browse and search every **tracked object** Frigate has saved. By default it groups recent objects by label; when [Semantic Search](/configuration/semantic_search) is enabled, you can also search by natural-language description or visual similarity. Selecting any object opens a detail pane with its snapshot, lifecycle, and metadata. + +This page describes how to _use_ the Explore view. For how the underlying features are _configured_, see [Semantic Search](/configuration/semantic_search) and [Generative AI descriptions](/configuration/genai/genai_objects). + +:::tip + +If you just want to quickly see what happened on your cameras, it's recommended to use [Review](/usage/review) rather than Explore. Review groups overlapping and adjacent activity on a camera into **review items** and sorts them into Alerts, Detections, and Motion, so you can scan and play back footage in a few clicks instead of sifting through individual objects. Reach for Explore when you need to find a _specific_ tracked object after the fact: by label, time, zone, or description. + +::: + +## Browsing tracked objects + +The default view shows your most recent tracked objects grouped into rows by label (_Person_, _Car_, _Dog_, and so on), each row labeled with the object type and a count. The arrow at the end of a row opens the full, filterable grid for that label. + +Clicking a thumbnail opens its [detail dialog](#tracked-object-details); right-clicking or long-pressing a thumbnail opens an [actions menu](#actions-and-bulk-selection). You can switch to a denser grid layout and adjust the number of columns from the view's settings. + +## Searching + +When [Semantic Search](/configuration/semantic_search) is enabled, a search bar appears that combines two things in one input: + +- **Natural-language search**: type a free-text query and press Enter to run a semantic search over your tracked objects. +- **Filter tokens**: type a `key:` to get suggestions, then a value, to add a structured filter. Each filter becomes a removable chip, and you can chain several together. + +You can save a search with the star icon and reload it later, and clear everything with the clear-search icon. A help popover explains the token syntax, for example: + +``` +cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM +``` + +### Filter reference + +The most common filter tokens are: + +| Filter | Description | +| ---------------------------- | ---------------------------------------------------------------------------------- | +| **Cameras** | Limit to one or more cameras. | +| **Labels** | Object labels (person, car, etc.). | +| **Sub Labels** | Recognized sub labels (e.g. a recognized face or name). | +| **Attributes** | Classification attributes applied to the object. | +| **Recognized License Plate** | Match a recognized plate. | +| **Zones** | Objects that entered specific zones. | +| **Before / After** | Restrict to a date range. | +| **Time Range** | Restrict to a time of day (`HH:MM-HH:MM`). | +| **Min / Max Score** | Restrict by the object's confidence score. | +| **Min / Max Speed** | Restrict by estimated speed (when speed estimation is configured). | +| **Has Snapshot / Has Clip** | Only objects that saved a snapshot or recording. | +| **Submitted to Frigate+** | Only objects already submitted (when Frigate+ is enabled). | +| **Search Type** | Whether semantic search matches the object's **Thumbnail** or its **Description**. | + +### Sorting + +When a filter or search is active, a **Sort** control lets you order results by **date**, **object score**, or **estimated speed** (ascending or descending). When a semantic query or similarity search is active, results can also be ordered by **relevance**. + +### Thumbnail and description search + +- The **Search Type** setting controls whether a text query is matched against each object's **thumbnail** or its **description**. Each result indicates which one it matched and the confidence. + +Natural-language search, thumbnail search, and description search all require [Semantic Search](/configuration/semantic_search) to be enabled. + +## Tracked Object Details + +Selecting an object opens the **Tracked Object Details** dialog. Use the arrows (or the left/right keys) to step to the previous or next object. The dialog has two tabs: + +- **Snapshot** or **Thumbnail**: the saved snapshot (or thumbnail). +- **Tracking Details**: the object's lifecycle, available when the object has a recording. It lists each significant moment (detected, entered a zone, became active or stationary, left, and so on); clicking a moment plays that part of the recording with the bounding box overlaid. A settings popover lets you show all zones and adjust the annotation offset. + +The details pane shows the object's **label**, **scores**, **camera**, **timestamp**, estimated **speed**, any **recognized license plate** and **classification attributes**, and its **description**. Admins can edit the sub label, license plate, and attributes inline. + +The **description** can be edited by hand, and, when [Generative AI descriptions](/configuration/genai/genai_objects) are enabled and the object's lifecycle has ended, regenerated from the snapshot or from thumbnails. For `speech` objects, a **Transcribe** action is available when audio transcription is enabled. When [Frigate+](/integrations/plus) is enabled, admins can submit a snapshot to improve their model directly from this pane. + +## Actions and bulk selection + +Right-clicking or long-pressing an object (in the grid or its thumbnail) opens an actions menu with options to **download** the video, snapshot, or a clean snapshot; **view tracking details**; **find similar**; **add a trigger**; **view in History**; and **delete the tracked object**. + +:::note + +Deleting a tracked object removes its snapshot, embeddings, and tracking-details entries, but the recorded footage of that object in [History](/usage/history) is **not** deleted. + +::: + +To act on many objects at once, Ctrl/Cmd-click or right-click to start a selection (selected tiles gain a blue ring), then use the toolbar to select all, clear the selection, or delete (admins). + +## Semantic Search - Usage and best practices {#usage-and-best-practices} + +1. Semantic Search is used in conjunction with the other filters available on the Explore page. Use a combination of traditional filtering and Semantic Search for the best results. +2. Use the thumbnail search type when searching for particular objects in the scene. Use the description search type when attempting to discern the intent of your object. +3. Because of how the AI models Frigate uses have been trained, the comparison between text and image embedding distances generally means that with multi-modal (`thumbnail` and `description`) searches, results matching `description` will appear first, even if a `thumbnail` embedding may be a better match. Play with the "Search Type" setting to help find what you are looking for. Note that if you are generating descriptions for specific objects or zones only, this may cause search results to prioritize the objects with descriptions even if the ones without them are more relevant. +4. Make your search language and tone closely match exactly what you're looking for. If you are using thumbnail search, **phrase your query as an image caption**. Searching for "red car" may not work as well as "red sedan driving down a residential street on a sunny day". +5. Semantic search on thumbnails tends to return better results when matching large subjects that take up most of the frame. Small things like "cat" tend to not work well. +6. Experiment! Find a tracked object you want to test and start typing keywords and phrases to see what works for you. + +## Triggers + +From an object's actions menu, **Add trigger** sets up a per-camera trigger that uses Semantic Search to automate an action (a notification, sub label, or attribute) whenever a similar object appears. Triggers require Semantic Search and are managed under . See [Triggers](/configuration/semantic_search#triggers) for full configuration and best practices. diff --git a/docs/docs/usage/exports.md b/docs/docs/usage/exports.md new file mode 100644 index 0000000..a593374 --- /dev/null +++ b/docs/docs/usage/exports.md @@ -0,0 +1,43 @@ +--- +id: exports +title: Exports +--- + +**Exports** are how you keep a specific piece of footage permanently. + +Frigate's recordings are governed by your [retention settings](/configuration/record): once footage ages past its retention window (or, depending on your configuration, once it is only kept where motion, alerts, or detections occurred), it is deleted to free up disk space. An **export** saves a copy of a chosen time range to a separate location that is **never removed by retention**, so it stays available until you delete it yourself. + +This is the answer to the common question _"how do I stop Frigate from deleting an important clip?"_ Instead of increasing retention for an entire camera (which uses far more storage to protect a single moment), export just the footage you want to keep. + +:::tip + +Exports are stored under `/media/frigate/exports`, separate from your recordings, and are not counted against or removed by recording retention. They remain on disk until you delete them, so be aware that they accumulate over time. + +::: + +## Creating an export + +There are a few ways to create an export: + +- **From Review**: select (right click or long-press) an individual review item directly, and choose Export from the header menu. You can also select multiple review items and export them all at once, optionally grouping them into a [case](#cases). +- **From History**: open the **Actions** menu and choose **Export**. You can export a preset duration (the last 1, 4, 8, 12, or 24 hours), enter a custom start and end time, or select a range directly on the timeline. A **multi-camera** option lets you export the same time range across several cameras at once. + +In every case you can give the export a name. Frigate then saves the footage from your recordings as a single video file. Larger ranges take time to process; the export is marked _in progress_ until it finishes, and you can keep using Frigate while it runs. + +## Managing exports + +All of your exports live on the **Exports** page, reachable from the main navigation, where you can search for one by name. Each export offers the following actions: + +- **Play** it in the browser, +- **Download** it to save the footage outside of Frigate, +- **Share** it: copies a direct link to the export (or uses your device's share sheet), +- **Rename** it, and +- **Delete** it: deleting is the only way an export is removed. + +You can also select multiple exports at once to **delete** them in bulk, or to **add them to** (or **remove them from**) a [case](#cases). + +## Cases + +A **case** groups related exports together: for example, all the clips from a single incident across multiple cameras. On the **Exports** page you can create a case with a name and description, add existing exports to it (or create a new case while exporting), and **download the entire case as a single archive** to hand off as one package. + +Exports that don't belong to a case appear under **Uncategorized Exports**. Deleting a case lets you either keep its exports (they move back to uncategorized) or delete them along with the case. diff --git a/docs/docs/usage/history.md b/docs/docs/usage/history.md new file mode 100644 index 0000000..5ed99f4 --- /dev/null +++ b/docs/docs/usage/history.md @@ -0,0 +1,75 @@ +--- +id: history +title: History +--- + +import NavPath from "@site/src/components/NavPath"; + +**History** is Frigate's full-resolution recording viewer. Unlike Live, Review, and Explore, there is no menu item for it. You reach it from within another view, then scrub the timeline, switch cameras, inspect a tracked object's lifecycle, and export or share any moment. + +This page describes how to _use_ the History view. For how recordings are _configured_ (retention, pre/post capture), see [Recording](/configuration/record). + +## Opening History + +You can open History from several places: + +- **From [Review](/usage/review):** clicking a review item opens its recording, scrubbed to just before the activity on that camera. +- **From [Live](/usage/live):** the **History** button in a camera's single-camera view opens that camera about 30 seconds in the past. +- **From a share link:** opening a shared timestamp link (see [Share Timestamp](#the-actions-menu) below) jumps straight to that camera and moment. + +Use the **Back** button to return where you came from, or the **Live** button to jump to the current camera's live view. + +:::tip + +If you see **"No recordings found for this time"**, the most common causes are: recording was not enabled for that camera at the time of the event; the retention window has since expired and those segments were removed; or storage ran low and Frigate deleted them early to free space. See [Recording](/configuration/record) to verify your retention settings. + +::: + +## Timeline, Events, and Detail + +A toggle (a drawer on mobile) switches the side panel between three modes: + +- **Timeline**: a scrubbable vertical timeline of the selected camera. Horizontal lines down the center represent motion, with longer lines indicating more motion at that moment. Review items are marked as shaded areas (**red** for alerts, **orange** for detections), and sections with no colored background are times when no recording exists. +- **Events**: a scrollable list of the camera's review items for the time range; clicking one seeks the player to it. +- **Detail**: the [tracking details inspector](#the-detail-view) for the objects in view. + +While you are selecting a range to export, the panel temporarily switches to Timeline. + +## Scrubbing and previews + +Drag the timeline handlebar to move through time; the main player and any secondary camera previews scrub together so everything stays in sync. Press the zoom buttons on the timeline to change its zoom level (from coarse to fine segments). Sections of the timeline with no recordings are shown as gaps. + +On desktop, when more than one camera is available, a **row of secondary previews** shows the other cameras at the same moment. Clicking one of them makes it the main camera at the current timestamp, so you can follow activity across cameras without losing your place. On mobile, use the camera drawer to switch cameras. + +## Filtering and the calendar + +You can filter History by **cameras** and **date**. The calendar behaves the same as it does in [Review](/usage/review#filtering-and-the-calendar): an **underline** under a day means recordings exist for that day, and a **colored dot** (red for unreviewed alerts, orange for unreviewed detections) marks days with unreviewed activity. + +## The Detail view + +The **Detail** mode turns the side panel into a tracking details inspector. It lists one card per review item, each showing the item's severity, start time, the object labels involved, a count of tracked objects, and the duration. The active card is highlighted as the video plays, and clicking a card seeks to it. + +Expanding a card reveals the **lifecycle** of each tracked object: a row for each significant moment (detected, entered a zone, became active, became stationary, left, and so on), with a progress line that follows the current playback position. Hovering a row shows that moment's score, ratio, and area, and clicking a row seeks the video to that exact timestamp. + +The **Detail View Settings** at the bottom let you toggle whether the active item's objects expand automatically, and adjust the **annotation offset**: a fine timing correction that aligns the bounding-box overlays with the recorded video when your camera's snapshot and recording timestamps drift. Admins can save the offset to the camera's configuration. + +## The Actions menu + +On desktop, the **Actions** menu (the film icon) collects the things you can do with the footage you are viewing: + +- **Export**: save a clip of a chosen time range so it is never removed by retention. The dialog pre-selects the last hour; adjust the range or drag the timeline handles, then export. See [Exports](/usage/exports) for managing and downloading exports. +- **Share Timestamp**: generate a link to the current moment (or a custom timestamp) to share with another Frigate user. This is an internal link, not a public share URL. +- **Motion Search**: scan this camera's recordings for changes in a region you draw. This is the same tool documented under [Reviewing Motion](/usage/review#motion-search). +- **Debug Replay** (admins): replay a recorded range back through Frigate's detection pipeline to see how it would be processed. + +You can also capture an instant snapshot of the current frame, and submit a frame to [Frigate+](/integrations/plus) directly from the player (admins only). + +## AI review summaries + +When [Generative AI review](/configuration/genai/genai_review) is configured, Frigate can generate a title, description, and threat classification for review items and surface them as you scrub through History. A review item that has an AI summary exposes its details in a few places: + +- **Over the video**: when the item is on screen, a popup appears over the player. +- **In the Events side panel**: items with a summary show the title below the thumbnail. +- **In the Detail side panel**: the item's card shows the title alongside its tracking details. + +Clicking any of these opens the **AI Analysis** dialog with the generated detail and any flagged concerns for that item. diff --git a/docs/docs/usage/live.md b/docs/docs/usage/live.md new file mode 100644 index 0000000..9366add --- /dev/null +++ b/docs/docs/usage/live.md @@ -0,0 +1,118 @@ +--- +id: live +title: Live View +--- + +import NavPath from "@site/src/components/NavPath"; + +**Live view** is Frigate's real-time dashboard and the page you land on by default. It shows all of your cameras at a glance, streams your most recent alerts across the top, and lets you open any camera in a full-resolution single-camera view with audio, two-way talk, PTZ, and on-demand recording controls. + +This page describes how to _use_ the Live view. For how to _configure_ live streaming (go2rtc, stream selection, smart streaming, WebRTC, and audio), see the [Live View configuration](/configuration/live) docs. + +## The dashboard at a glance + +The default **All Cameras** dashboard shows every camera, with a filmstrip of recent **alerts** scrolling across the top. Clicking an alert opens it in [Review](/usage/review); each card also has a check button to mark it reviewed without leaving the dashboard. Only **alerts** appear in the filmstrip. To suppress a label or zone from showing there, configure it as a detection instead (see [Alerts and Detections](/configuration/review#alerts-and-detections)). + +By default Frigate uses **smart streaming**: a camera's image updates roughly once per minute while nothing is happening, and switches to a full live stream the moment activity is detected. This conserves bandwidth and resources. You can change this for each camera when using a camera group (see [Streaming settings](#streaming-settings-and-the-right-click-menu) below), and the behavior is explained in detail under [Live view technologies](/configuration/live#live-view-technologies). + +On mobile, a toggle in the header switches between a **grid** layout and a single-column **list** layout. On desktop a **fullscreen** button is available in the lower-right corner. + +## Switching dashboards and camera groups + +The icon rail (top-left on desktop, a horizontal strip on mobile) switches between dashboards: + +- The **home** icon is the **All Cameras** dashboard, which shows every camera enabled for the dashboard. +- Each **camera group** you create appears as its own icon. Selecting a group shows only that group's cameras. + +Camera groups are useful for organizing cameras by location (for example, _Front of House_ or _Backyard_) and for giving each group its own dashboard layout and camera streaming preferences. + +You can also view [Birdseye](/configuration/birdseye) on the dashboard, or open it directly at `http://:5000/#birdseye`. Clicking a camera inside the Birdseye view jumps to that camera's live feed. + +## Creating and editing camera groups + +Admins can manage groups from the pencil icon next to the group rail, which opens the **Camera Groups** dialog. From there you can add a group, or edit and delete existing ones. When creating a group you choose: + +- a **Name** (spaces are converted to underscores), +- the **cameras** to include (each camera has a toggle and a gear that opens its [streaming settings](#streaming-settings-and-the-right-click-menu)), and +- an **icon** used for the group's button in the rail. + +Deleting a group also clears any custom layout you saved for it. + +## Rearranging a camera group layout + +On desktop and tablet, each camera group has its own freely-arrangeable grid. Enter **Edit Layout** mode from the layout button in the lower-right corner: camera tiles gain a drag handle and corner resize handles. Drag a tile to reposition it and drag a corner to resize it (the aspect ratio is preserved). Exit edit mode to save. The layout is stored in your browser per device, so each device can have its own arrangement. + +The default **All Cameras** dashboard is not manually arrangeable. It automatically sizes tiles based on each camera's aspect ratio (wide cameras span two columns, tall cameras span two rows). + +## Reading the tile indicators + +Each camera tile surfaces its current state with a few overlays: + +- A **pulsing red dot** in the corner means **motion is currently detected** on that camera. +- A **red outline** around the tile means an **active tracked object** is on that camera. +- A small **label chip** lists the object types currently detected (for example, _Person_, _Car_). +- A **camera-name label** appears when you have enabled always-on camera names, or when a camera is offline or disabled. +- A **Stream Offline** or **Camera is off** placeholder appears when no frames are being received or the camera has been turned off. + +You can optionally overlay live streaming statistics (stream type, bandwidth, latency, and frame counts) on a tile to diagnose playback issues. + +## Streaming settings and the right-click menu + +Right-clicking (or long-pressing) a camera tile opens a context menu with quick controls: an **audio volume** control for streams that support audio, **Mute / Unmute all cameras**, **show or hide streaming statistics**, the **debug view**, **notification** options, and, for admins, turning the camera on or off. If the audio control doesn't appear, see [Audio Support](/configuration/live#audio-support). Audio requires go2rtc configured with a compatible codec. + +A **Low-bandwidth mode** notice may also appear in the context menu with a **Reset** option when Frigate has fallen back to the lower-quality jsmpeg stream. See the [Live view FAQ](/configuration/live#live-view-faq) for why this happens. + +For non-default groups, the context menu also exposes **Streaming Settings** for that camera, which let you choose: + +- the **stream** to display (the dropdown lists the streams you configured under [`live -> streams`](/configuration/live#setting-streams-for-live-ui), and indicates whether audio is available), +- the **streaming method**: **No Streaming**, **Smart Streaming** (recommended), or **Continuous Streaming** (higher bandwidth), and +- **compatibility mode**, for devices that have trouble rendering the default player. + +These settings are saved per group and per device in your browser, not in your config file. + +## The single-camera view + +Clicking a camera tile opens its full-resolution single-camera view. The top bar provides: + +- **Back** (also the `Esc` key) to return to the dashboard, +- **History** to jump to the [recordings](/usage/history) for this camera, starting about 30 seconds in the past, +- **Fullscreen** and **Picture-in-Picture** (if supported by your browser), +- **Two-way talk** (the microphone button, which requires a supported camera and WebRTC; keyboard shortcut `t`), and +- **Camera audio muting** (the speaker button; keyboard shortcut `m`). + +You can pinch or scroll to zoom into the feed. A **settings** gear provides a **stream** selector (with audio and two-way-talk availability indicators), **Play in background**, **Show stats**, and a **Debug view** that overlays Frigate's detection regions and bounding boxes. + +:::tip + +Two-way talk and camera audio have specific codec and port requirements. See [Audio Support](/configuration/live#audio-support) and [WebRTC](/configuration/live#webrtc-extra-configuration) for setup details. + +::: + +## Camera controls + +Admins get a row of toggles in the single-camera view (a settings drawer on mobile) to turn camera features on and off in real time: + +- **Camera** on/off, +- **Object detection**, +- **Recording** (only available when recording is enabled in the camera's config), +- **Snapshots**, +- **Audio detection**, +- **Live audio transcription** (when audio detection is enabled), and +- **Autotracking** (for [autotracking-capable PTZ cameras](/configuration/autotracking)). + +These toggles change runtime behavior immediately. Whether a change persists across a restart depends on the feature. See the relevant configuration page. + +## On-demand recording and snapshots + +The single-camera view can capture footage on demand: + +- **Start on-demand recording** begins a manual recording based on the camera's recording retention settings (the button pulses while active). If recording is disabled for the camera, only a snapshot is saved. Use **End on-demand recording** to stop. +- **Download instant snapshot** saves a still image of the current frame. + +See [Recording](/configuration/record) and [Snapshots](/configuration/snapshots) for how retention is configured, and [Exports](/usage/exports) for keeping a clip permanently. + +## PTZ controls + +For ONVIF cameras that support it, a control panel provides pan/tilt arrows, **zoom**, **focus**, and saved **presets**. You can also enable a **click-to-move / drag-to-zoom** overlay: click a point in the frame to center the camera there, or drag a box to pan and zoom to that area (dragging top-left to bottom-right zooms in, the reverse zooms out). + +For continuous, automatic tracking of a moving object, see [Autotracking](/configuration/autotracking). diff --git a/docs/docs/usage/review.md b/docs/docs/usage/review.md new file mode 100644 index 0000000..6416b50 --- /dev/null +++ b/docs/docs/usage/review.md @@ -0,0 +1,140 @@ +--- +id: review +title: Review +--- + +import NavPath from "@site/src/components/NavPath"; + +**Review** is where you triage what happened on your cameras. It groups activity into **review items**, segments of time on a single camera that bundle together the objects and audio that were active at once, and sorts them into **Alerts**, **Detections**, and **Motion**. From here you can scrub through activity, mark items as reviewed, filter, export, and jump to the full recording in [History](/usage/history). + +This page describes how to _use_ the Review view. For how alerts and detections are _configured_ (labels, zones, required zones, retention), see the [Review configuration](/configuration/review) docs. + +:::info + +Review items are only created for a camera when **object tracking and recording are enabled** for that camera. See [Recording](/configuration/record). + +::: + +## Alerts, Detections, and Motion + +Not every segment of video captured by Frigate is of the same level of interest. The people who enter your property may be a higher priority than those just walking by on the sidewalk. For this reason, Frigate sorts **review items** by importance into **alerts** and **detections**, with a separate **Motion** category for significant motion. + +The toggle at the top of the page switches between these three severities. One is always selected. + +| Tab | Indicator color | What it shows | +| -------------- | --------------- | ---------------------------------------------------------------------------------------------------------------- | +| **Alerts** | dark red | The activity you most want to see. By default, all `person` and `car` tracked objects are alerts. | +| **Detections** | orange | Everything else Frigate tracked that wasn't promoted to an alert. | +| **Motion** | yellow | Periods of significant motion, with the ability to filter to periods which did **not** produce a tracked object. | + +This same color coding is used for the ring around a selected item and the dots on the calendar. How an object is categorized as an alert vs. a detection, and how required zones refine that, is covered in [Alerts and Detections](/configuration/review#alerts-and-detections). + +The **Alerts** and **Detections** tabs show a count next to their label. With **Show Reviewed** turned off (the default), this is the number of items still left to review; with it on, the count reflects every item in the selected time range. + +## Marking items as reviewed + +Review items are shown as a grid of thumbnail cards next to a vertical activity timeline. Hovering a card (desktop) or swiping to the right (mobile) plays a short preview inline. + +- **Clicking** a card opens its recording in [History](/usage/history) and marks the item as reviewed. +- The object chip on each card is **gray** when the item is unreviewed and turns **green** once it has been reviewed. +- The **Mark these items as reviewed** button marks everything currently shown as reviewed at once. + +Reviewed state is tracked per user, so marking an item reviewed does not hide it for other users. Marking an item reviewed does not delete anything: the footage and the review item itself remain until they expire via retention. + +## Selecting and acting on multiple items + +To act on several items at once, start a selection by **Ctrl/Cmd-clicking** a card (desktop) or **long-pressing** one (mobile). Selected cards gain a colored ring matching their severity. Keyboard shortcuts speed this up: `Ctrl+A` selects all, `R` marks the selection reviewed, and `Esc` clears it. + +With items selected, an action bar appears with options to: + +- **Export** the selected items (a single item exports directly; multiple items open the batch [export](/usage/exports) dialog), +- **Mark as reviewed** or **Mark as unreviewed**, and +- **Delete** them (admins only). + +## Filtering and the calendar + +Use the filter controls in the header to narrow what's shown. The available filters depend on the tab: Alerts and Detections can be filtered by **cameras**, **date**, **labels**, **zones**, and whether items are already reviewed; the Motion tab can be filtered by **cameras**, **date**, and **motion only**. + +The **calendar** filter lets you jump to a specific day (it shows **Last 24 Hours** until you pick one). On each day: + +- An **underline** under the day number means **recordings exist** for that day. Days without recordings are dimmed. +- A **colored dot** under the day number means there is **unreviewed activity** that day: a **red dot** for unreviewed alerts, or an **orange dot** for unreviewed detections when there are no unreviewed alerts. Motion is not represented by a dot. + +Future dates are disabled, and the week start and time zone follow your configuration. + +## Reviewing Motion + +The Review page also can show periods of motion that didn't produce a tracked object, and provides a way to search past recordings for motion in a specific region. These tools complement the alerts and detections workflow above. See [Tuning Motion Detection](/configuration/motion_detection) for how the underlying motion detector is configured. + +The **Motion** tab itself shows a multi-camera grid scrubbed to a shared point in time, with a draggable timeline and a playback-speed selector. A camera tile gains a colored ring when a review item or significant motion overlaps the current time, and clicking a tile opens that camera's recording at that moment. Each camera's options menu (the kebab in the corner of its tile) is where you open **Motion Previews** and **Motion Search**, described below. + +### Motion Previews + +The Motion Previews pane shows preview clips for periods of significant motion that did not produce a tracked object. It is useful for spotting things that motion detection picked up but object detection did not, which can help validate tuning or catch missed objects. + +On the page, click the kebab menu on a camera and choose **Motion Previews**. Each card represents a continuous range of motion-only activity and plays back the recorded preview for that range. A heatmap overlay dims areas of the frame with no motion so the moving regions stand out. + +The pane provides a few controls: + +- **Speed**: speeds up or slows down all of the preview clips at once. +- **Dim**: controls how strongly non-motion areas are darkened by the heatmap overlay. Higher values increase motion area visibility. +- **Filter**: opens a 16×16 grid overlaid on a snapshot of the camera. Select one or more cells to only show clips with motion in those regions. This is helpful for filtering out motion in areas like a busy street while keeping motion in your driveway. + +Clicking a preview clip seeks the recording player to that timestamp so you can review the full footage. + +### Motion Search + +Motion Search lets you scan recorded footage for changes inside a region of interest you draw on the camera. Unlike Motion Previews, which surfaces what Frigate's motion detector flagged in real time, Motion Search re-analyzes the saved recordings, so it can find changes that were missed (for example, an object that appeared while motion detection was paused by `lightning_threshold`, or in a region that is normally motion-masked). + +To start a search, open the Actions menu in [History](/usage/history) or click the kebab menu on a camera in the page and choose **Motion Search**. In the dialog: + +1. Pick the camera and time range to scan. In the date pickers, days that have recordings available are underlined. +2. Draw a polygon on the camera frame to define the region of interest. +3. Adjust the search parameters if needed: + +| Field | Description | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Sensitivity Threshold** | Per-pixel luminance change required to count as motion inside the ROI. Behaves like Frigate's motion detection `threshold` setting. | +| **Minimum Change Area** | Minimum size of a single moving region, as a percentage of the ROI, for a frame to count as significant. Raise it to ignore small movements (leaves, distant motion); lower it when your subject covers only a small slice of the ROI. Every result shows the percentage it scored, so you can use those values to tune this. | +| **Maximum Results** | Maximum number of matching timestamps to return. The search stops once it reaches this many results, so a lower value finishes sooner while a higher value scans further into the range. | +| **Parallel mode** | Decode multiple recording ranges at the same time. Speeds up large time ranges at the cost of higher decoding and CPU usage. | + +Motion Search samples each recording's keyframes automatically, so there is no frame-rate or sampling setting to tune. + +Once running, Frigate scans the recording segments that overlap the time range and reports timestamps where changes were detected inside the polygon, along with the percentage of the ROI that changed. Clicking a result seeks the player to that moment so you can review what happened. + +The results panel shows the time range being scanned, a live progress bar with the timestamp currently being analyzed, and the running result count. A collapsible **Search Metrics** section reports how many segments were scanned and processed, how many were skipped because no motion was recorded in the ROI (using the stored motion heatmap), how many frames were decoded, and the total search time. Skipping segments with no recorded motion in the selected ROI is what makes searching long time ranges practical. + +#### Common use cases + +Frigate's main use case is to record and surface tracked objects, so Motion Search is most useful for the cases where object detection produced nothing: there is no object to find in Explore, but you suspect something happened. + +- **Locating an unattributed change.** You know something appeared, disappeared, or moved in a window of footage (a package now gone, a gate left open), but no detection points to it. A search returns the candidate timestamps instead of scrubbing the timeline by hand. +- **An object that was never detected.** Something Frigate doesn't have a model label for, an object too small or distant to be detected, or movement in a region where detection isn't running. The activity left no tracked object but did change the pixels, so a search can still find it. +- **Activity while detection was effectively paused.** Changes that occurred while object detection was disabled, motion was suppressed by `skip_motion_threshold`, or inside an area covered by a motion mask, won't appear as review items or tracked objects but can be recovered by searching the recordings directly. + +#### Examples + +These show how to choose the ROI and **Minimum Change Area** for two common goals. Minimum Change Area is the size of a single moving region as a percentage of the ROI you draw, so the right value depends on how much of the ROI your subject, and its movement between samples, covers. + +Because samples are a second or more apart, a moving subject usually appears in two places at once in the comparison, so even ordinary motion often scores tens of percent and a low threshold lets in almost everything. The most reliable approach is to **run a search, look at the percentage each result scored, and set Minimum Change Area just below the values for the events you care about.** The default is 20%; the suggestions below are starting points. + +- **When did this item first appear (or disappear)?** A package was dropped off, a car parked, or a trash can was moved, and you want the exact moment. Draw a **tight ROI** around the spot the item occupies and **raise Minimum Change Area** (start around 40–60%). Because the item fills most of a tight ROI, its arrival or removal is a large change, while smaller nearby motion (shadows, a passing pedestrian) stays below the threshold. The **earliest result** is when it appeared; if you only care about that moment, a low Maximum Results finishes faster. If you get no hits, the ROI is probably looser than the item: lower the threshold or tighten the ROI. +- **What's been getting into the garden?** Something has been trampling a flower bed overnight and no object was ever tracked. Draw a **looser ROI** covering the whole bed and use a **lower Minimum Change Area than the case above**: start near the 20% default and lower it (toward 5–10%) only if a small or distant subject is missed, since it covers just a slice of a large region. Expect more results to scan through: step through the timestamps and jump to each to see what triggered it. If wind-blown plants add noise, raise Minimum Change Area or the Sensitivity Threshold. + +#### Expected performance + +Motion Search analyzes the saved recordings on demand rather than reading a pre-built index, so a search over a long range takes longer than browsing Motion Previews. Cost scales mainly with how much footage has to be examined: segments with no recorded motion in your ROI are skipped using the stored motion heatmap (shown as "segments skipped" in the status panel), so a quiet range finishes quickly while a busy one takes longer. + +To increase the speed of searches: + +- Draw a tight ROI. Because **Minimum Change Area** is measured as a percentage of the region you draw, a tight ROI around where you expect the change makes the object fill a larger share of the area, so it clears the threshold more easily. A loose ROI makes the same object a small fraction of the region, so it can fall below the threshold and be missed, forcing you to lower Minimum Change Area, which lets in more noise. +- Narrow the time range to the window you care about, so there is less footage to examine. +- Lower **Maximum Results** when you only need the first few hits. Because the search stops once it reaches that many results, a smaller value lets a busy range finish early instead of scanning the whole window. +- Use Parallel mode to shorten wall-clock time on multi-core systems, at the cost of higher decoding and CPU usage while it runs. + +## AI review summaries + +When [Generative AI review](/configuration/genai/genai_review) is configured, Frigate can generate a title, description, and threat classification for review items and surface them automatically in Review and History. Clicking the summary chip opens an **AI Analysis** dialog with the generated detail and any flagged concerns. + +In Review, an additional icon appears on unreviewed items that the AI classified as **suspicious** (Level 1) or **critical** (Level 2), so the activity that most warrants attention stands out before you open it. The icon goes away once the item has been reviewed. diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts new file mode 100644 index 0000000..69ab7c3 --- /dev/null +++ b/docs/docusaurus.config.ts @@ -0,0 +1,235 @@ +import type * as Preset from "@docusaurus/preset-classic"; +import * as path from "node:path"; +import type { Config, PluginConfig } from "@docusaurus/types"; +import type * as OpenApiPlugin from "docusaurus-plugin-openapi-docs"; + +const config: Config = { + title: "Frigate", + tagline: "NVR With Realtime Object Detection for IP Cameras", + url: "https://docs.frigate.video", + baseUrl: "/", + onBrokenLinks: "throw", + onBrokenMarkdownLinks: "warn", + favicon: "img/branding/favicon.ico", + organizationName: "blakeblackshear", + projectName: "frigate", + themes: [ + "@docusaurus/theme-mermaid", + "docusaurus-theme-openapi-docs", + "@inkeep/docusaurus/chatButton", + "@inkeep/docusaurus/searchBar", + ], + markdown: { + mermaid: true, + }, + i18n: { + defaultLocale: 'en', + locales: ['en'], + localeConfigs: { + en: { + label: 'English', + } + }, + }, + themeConfig: { + announcementBar: { + id: 'frigate_plus', + content: ` + 🚀 + Get more relevant and accurate detections with Frigate+ models. + Learn more + + `, + backgroundColor: '#005f73', + textColor: '#e0fbfc', + isCloseable: false, + }, + docs: { + sidebar: { + hideable: true, + }, + }, + inkeepConfig: { + baseSettings: { + apiKey: "b1a4c4d73c9b48aa5b3cdae6e4c81f0bb3d1134eeb5a7100", + integrationId: "cm6xmhn9h000gs601495fkkdx", + organizationId: "org_map2JQEOco8U1ZYY", + primaryBrandColor: "#010101", + }, + aiChatSettings: { + chatSubjectName: "Frigate", + botAvatarSrcUrl: "https://frigate.video/images/favicon.png", + getHelpCallToActions: [ + { + name: "GitHub", + url: "https://github.com/blakeblackshear/frigate", + icon: { + builtIn: "FaGithub", + }, + }, + ], + quickQuestions: [ + "How to configure and setup camera settings?", + "How to setup notifications?", + "Supported builtin detectors?", + "How to restream video feed?", + "How can I get sound or audio in my recordings?", + ], + }, + }, + prism: { + magicComments:[ + { + className: 'theme-code-block-highlighted-line', + line: 'highlight-next-line', + block: {start: 'highlight-start', end: 'highlight-end'}, + }, + { + className: 'code-block-error-line', + line: 'highlight-error-line', + }, + ], + additionalLanguages: ["bash", "json"], + }, + languageTabs: [ + { + highlight: "python", + language: "python", + logoClass: "python", + }, + { + highlight: "javascript", + language: "nodejs", + logoClass: "nodejs", + }, + { + highlight: "javascript", + language: "javascript", + logoClass: "javascript", + }, + { + highlight: "bash", + language: "curl", + logoClass: "curl", + }, + { + highlight: "rust", + language: "rust", + logoClass: "rust", + }, + ], + navbar: { + title: "Frigate", + logo: { + alt: "Frigate", + src: "img/branding/logo.svg", + srcDark: "img/branding/logo-dark.svg", + }, + items: [ + { + to: "/", + activeBasePath: "docs", + label: "Docs", + position: "left", + }, + { + href: "https://frigate.video", + label: "Website", + position: "right", + }, + { + href: "http://demo.frigate.video", + label: "Demo", + position: "right", + }, + { + type: 'localeDropdown', + position: 'right', + dropdownItemsAfter: [ + { + label: '简体中文(社区翻译)', + href: 'https://docs.frigate-cn.video', + } + ] + }, + { + href: 'https://github.com/blakeblackshear/frigate', + label: 'GitHub', + position: 'right', + }, + ], + }, + footer: { + style: "dark", + links: [ + { + title: "Community", + items: [ + { + label: "GitHub", + href: "https://github.com/blakeblackshear/frigate", + }, + { + label: "Discussions", + href: "https://github.com/blakeblackshear/frigate/discussions", + }, + ], + }, + ], + copyright: `Copyright © ${new Date().getFullYear()} Frigate, Inc.`, + }, + }, + plugins: [ + path.resolve(__dirname, "plugins", "raw-loader"), + path.resolve(__dirname, "plugins", "yaml-loader"), + [ + "docusaurus-plugin-openapi-docs", + { + id: "openapi", + docsPluginId: "classic", // configured for preset-classic + config: { + frigateApi: { + specPath: "static/frigate-api.yaml", + outputDir: "docs/integrations/api", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "tag", + sidebarCollapsible: true, + sidebarCollapsed: true, + }, + showSchemas: true, + } satisfies OpenApiPlugin.Options, + }, + }, + ], + ] as PluginConfig[], + presets: [ + [ + "classic", + { + docs: { + routeBasePath: "/", + sidebarPath: "./sidebars.ts", + // Please change this to your repo. + editUrl: + "https://github.com/blakeblackshear/frigate/edit/master/docs/", + sidebarCollapsible: false, + docItemComponent: "@theme/ApiItem", // Derived from docusaurus-theme-openapi + }, + + theme: { + customCss: "./src/css/custom.css", + }, + } satisfies Preset.Options, + ], + ], +}; + +export default async function createConfig() { + return config; +} diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 0000000..2310274 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,23369 @@ +{ + "name": "docs", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "docs", + "version": "0.0.0", + "dependencies": { + "@docusaurus/core": "^3.7.0", + "@docusaurus/plugin-content-docs": "^3.7.0", + "@docusaurus/preset-classic": "^3.7.0", + "@docusaurus/theme-mermaid": "^3.7.0", + "@inkeep/docusaurus": "^2.0.16", + "@mdx-js/react": "^3.1.0", + "@types/js-yaml": "^4.0.9", + "clsx": "^2.1.1", + "docusaurus-plugin-openapi-docs": "^4.5.1", + "docusaurus-theme-openapi-docs": "^4.5.1", + "js-yaml": "^4.1.1", + "prism-react-renderer": "^2.4.1", + "raw-loader": "^4.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "^3.7.0", + "@docusaurus/types": "^3.7.0", + "@types/react": "^18.3.27" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.20.tgz", + "integrity": "sha512-0DKAZP9SiphUHuT/HmCYrv0uNyHfqn4gT3e5LsL+y1n3mMhWrrKNS2QYn+ysVd7yOmrLyv30gzrCCdbjnN+vtw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.19", + "@vercel/oidc": "3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz", + "integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "2.0.113", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.113.tgz", + "integrity": "sha512-jAwWxIHrzRAP5Lwv+Z9be54Uxogd0QhUyfDAx/apVCyhszinotN7ABrQMXBQsbqXUmgvlncMvEEXxWQbpOT6iA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "3.0.19", + "ai": "5.0.111", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1", + "zod": "^3.25.76 || ^4.1.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.12.0.tgz", + "integrity": "sha512-EfW0bfxjPs+C7ANkJDw2TATntfBKsFiy7APh+KO0pQ8A6HYa5I0NjFuCGCXWfzzzLXNZta3QUl3n5Kmm6aJo9Q==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.2" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.46.0.tgz", + "integrity": "sha512-eG5xV8rujK4ZIHXrRshvv9O13NmU/k42Rnd3w43iKH5RaQ2zWuZO6Q7XjaoJjAFVCsJWqRbXzbYyPGrbF3wGNg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.46.0.tgz", + "integrity": "sha512-AYh2uL8IUW9eZrbbT+wZElyb7QkkeV3US2NEKY7doqMlyPWE8lErNfkVN1NvZdVcY4/SVic5GDbeDz2ft8YIiQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.0.tgz", + "integrity": "sha512-0emZTaYOeI9WzJi0TcNd2k3SxiN6DZfdWc2x2gHt855Jl9jPUOzfVTL6gTvCCrOlT4McvpDGg5nGO+9doEjjig==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.46.0.tgz", + "integrity": "sha512-wrBJ8fE+M0TDG1As4DDmwPn2TXajrvmvAN72Qwpuv8e2JOKNohF7+JxBoF70ZLlvP1A1EiH8DBu+JpfhBbNphQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.46.0.tgz", + "integrity": "sha512-LnkeX4p0ENt0DoftDJJDzQQJig/sFQmD1eQifl/iSjhUOGUIKC/7VTeXRcKtQB78naS8njUAwpzFvxy1CDDXDQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.0.tgz", + "integrity": "sha512-aF9tc4ex/smypXw+W3lBPB1jjKoaGHpZezTqofvDOI/oK1dR2sdTpFpK2Ru+7IRzYgwtRqHF3znmTlyoNs9dpA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.0.tgz", + "integrity": "sha512-22SHEEVNjZfFWkFks3P6HilkR3rS7a6GjnCIqR22Zz4HNxdfT0FG+RE7efTcFVfLUkTTMQQybvaUcwMrHXYa7Q==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/events": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", + "license": "MIT" + }, + "node_modules/@algolia/ingestion": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.46.0.tgz", + "integrity": "sha512-2LT0/Z+/sFwEpZLH6V17WSZ81JX2uPjgvv5eNlxgU7rPyup4NXXfuMbtCJ+6uc4RO/LQpEJd3Li59ke3wtyAsA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.46.0.tgz", + "integrity": "sha512-uivZ9wSWZ8mz2ZU0dgDvQwvVZV8XBv6lYBXf8UtkQF3u7WeTqBPeU8ZoeTyLpf0jAXCYOvc1mAVmK0xPLuEwOQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.46.0.tgz", + "integrity": "sha512-O2BB8DuySuddgOAbhyH4jsGbL+KyDGpzJRtkDZkv091OMomqIA78emhhMhX9d/nIRrzS1wNLWB/ix7Hb2eV5rg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.0.tgz", + "integrity": "sha512-eW6xyHCyYrJD0Kjk9Mz33gQ40LfWiEA51JJTVfJy3yeoRSw/NXhAL81Pljpa0qslTs6+LO/5DYPZddct6HvISQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.0.tgz", + "integrity": "sha512-Vn2+TukMGHy4PIxmdvP667tN/MhS7MPT8EEvEhS6JyFLPx3weLcxSa1F9gVvrfHWCUJhLWoMVJVB2PT8YfRGcw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.0.tgz", + "integrity": "sha512-xaqXyna5yBZ+r1SJ9my/DM6vfTqJg9FJgVydRJ0lnO+D5NhqGW/qaRG/iBGKr/d4fho34el6WakV7BqJvrl/HQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", + "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", + "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", + "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", + "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.5", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.4", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.43.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/postcss-alpha-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz", + "integrity": "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", + "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", + "integrity": "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz", + "integrity": "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz", + "integrity": "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz", + "integrity": "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz", + "integrity": "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-contrast-color-function": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz", + "integrity": "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", + "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", + "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz", + "integrity": "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz", + "integrity": "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz", + "integrity": "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz", + "integrity": "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", + "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", + "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", + "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", + "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", + "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", + "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", + "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", + "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", + "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", + "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz", + "integrity": "sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz", + "integrity": "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-position-area-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-position-area-property/-/postcss-position-area-property-1.0.0.tgz", + "integrity": "sha512-fUP6KR8qV2NuUZV3Cw8itx0Ep90aRjAZxAEzC3vrl6yjFv+pFsQbR18UuQctEKmA72K9O27CoYiKEgXxkqjg8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz", + "integrity": "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", + "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz", + "integrity": "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", + "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-sign-functions": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", + "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", + "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-system-ui-font-family": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-system-ui-font-family/-/postcss-system-ui-font-family-1.0.0.tgz", + "integrity": "sha512-s3xdBvfWYfoPSBsikDXbuorcMG1nN1M6GdU0qBsGfcmNR0A/qhloQZpTxjA3Xsyrk1VJvwb2pOfiOT3at/DuIQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz", + "integrity": "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", + "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", + "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/utilities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", + "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docsearch/core": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.3.1.tgz", + "integrity": "sha512-ktVbkePE+2h9RwqCUMbWXOoebFyDOxHqImAqfs+lC8yOU+XwEW4jgvHGJK079deTeHtdhUNj0PXHSnhJINvHzQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@docsearch/css": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.3.2.tgz", + "integrity": "sha512-K3Yhay9MgkBjJJ0WEL5MxnACModX9xuNt3UlQQkDEDZJZ0+aeWKtOkxHNndMRkMBnHdYvQjxkm6mdlneOtU1IQ==", + "license": "MIT" + }, + "node_modules/@docsearch/react": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.3.2.tgz", + "integrity": "sha512-74SFD6WluwvgsOPqifYOviEEVwDxslxfhakTlra+JviaNcs7KK/rjsPj89kVEoQc9FUxRkAofaJnHIR7pb4TSQ==", + "license": "MIT", + "dependencies": { + "@ai-sdk/react": "^2.0.30", + "@algolia/autocomplete-core": "1.19.2", + "@docsearch/core": "4.3.1", + "@docsearch/css": "4.3.2", + "ai": "^5.0.30", + "algoliasearch": "^5.28.0", + "marked": "^16.3.0", + "zod": "^4.1.8" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@docusaurus/babel": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.9.2.tgz", + "integrity": "sha512-GEANdi/SgER+L7Japs25YiGil/AUDnFFHaCGPBbundxoWtCkA2lmy7/tFmgED4y1htAy6Oi4wkJEQdGssnw9MA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/preset-env": "^7.25.9", + "@babel/preset-react": "^7.25.9", + "@babel/preset-typescript": "^7.25.9", + "@babel/runtime": "^7.25.9", + "@babel/runtime-corejs3": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "babel-plugin-dynamic-import-node": "^2.3.3", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/bundler": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.9.2.tgz", + "integrity": "sha512-ZOVi6GYgTcsZcUzjblpzk3wH1Fya2VNpd5jtHoCCFcJlMQ1EYXZetfAnRHLcyiFeBABaI1ltTYbOBtH/gahGVA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@docusaurus/babel": "3.9.2", + "@docusaurus/cssnano-preset": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "babel-loader": "^9.2.1", + "clean-css": "^5.3.3", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.11.0", + "css-minimizer-webpack-plugin": "^5.0.1", + "cssnano": "^6.1.2", + "file-loader": "^6.2.0", + "html-minifier-terser": "^7.2.0", + "mini-css-extract-plugin": "^2.9.2", + "null-loader": "^4.0.1", + "postcss": "^8.5.4", + "postcss-loader": "^7.3.4", + "postcss-preset-env": "^10.2.1", + "terser-webpack-plugin": "^5.3.9", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "webpack": "^5.95.0", + "webpackbar": "^6.0.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/faster": "*" + }, + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } + } + }, + "node_modules/@docusaurus/core": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz", + "integrity": "sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==", + "license": "MIT", + "dependencies": { + "@docusaurus/babel": "3.9.2", + "@docusaurus/bundler": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "boxen": "^6.2.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cli-table3": "^0.6.3", + "combine-promises": "^1.1.0", + "commander": "^5.1.0", + "core-js": "^3.31.1", + "detect-port": "^1.5.1", + "escape-html": "^1.0.3", + "eta": "^2.2.0", + "eval": "^0.1.8", + "execa": "5.1.1", + "fs-extra": "^11.1.1", + "html-tags": "^3.3.1", + "html-webpack-plugin": "^5.6.0", + "leven": "^3.1.0", + "lodash": "^4.17.21", + "open": "^8.4.0", + "p-map": "^4.0.0", + "prompts": "^2.4.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", + "react-loadable-ssr-addon-v5-slorber": "^1.0.1", + "react-router": "^5.3.4", + "react-router-config": "^5.1.1", + "react-router-dom": "^5.3.4", + "semver": "^7.5.4", + "serve-handler": "^6.1.6", + "tinypool": "^1.0.2", + "tslib": "^2.6.0", + "update-notifier": "^6.0.2", + "webpack": "^5.95.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-dev-server": "^5.2.2", + "webpack-merge": "^6.0.1" + }, + "bin": { + "docusaurus": "bin/docusaurus.mjs" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mdx-js/react": "^3.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/cssnano-preset": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.9.2.tgz", + "integrity": "sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==", + "license": "MIT", + "dependencies": { + "cssnano-preset-advanced": "^6.1.2", + "postcss": "^8.5.4", + "postcss-sort-media-queries": "^5.2.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/logger": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.9.2.tgz", + "integrity": "sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/mdx-loader": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.9.2.tgz", + "integrity": "sha512-wiYoGwF9gdd6rev62xDU8AAM8JuLI/hlwOtCzMmYcspEkzecKrP8J8X+KpYnTlACBUUtXNJpSoCwFWJhLRevzQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@mdx-js/mdx": "^3.0.0", + "@slorber/remark-comment": "^1.0.0", + "escape-html": "^1.0.3", + "estree-util-value-to-estree": "^3.0.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "image-size": "^2.0.2", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-raw": "^7.0.0", + "remark-directive": "^3.0.0", + "remark-emoji": "^4.0.0", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "stringify-object": "^3.3.0", + "tslib": "^2.6.0", + "unified": "^11.0.3", + "unist-util-visit": "^5.0.0", + "url-loader": "^4.1.1", + "vfile": "^6.0.1", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/module-type-aliases": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.2.tgz", + "integrity": "sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.9.2", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "@types/react-router-dom": "*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@docusaurus/plugin-content-blog": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.2.tgz", + "integrity": "sha512-3I2HXy3L1QcjLJLGAoTvoBnpOwa6DPUa3Q0dMK19UTY9mhPkKQg/DYhAGTiBUKcTR0f08iw7kLPqOhIgdV3eVQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "cheerio": "1.0.0-rc.12", + "feed": "^4.2.2", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "srcset": "^4.0.0", + "tslib": "^2.6.0", + "unist-util-visit": "^5.0.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-docs": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", + "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@types/react-router-config": "^5.0.7", + "combine-promises": "^1.1.0", + "fs-extra": "^11.1.1", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "tslib": "^2.6.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-pages": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.2.tgz", + "integrity": "sha512-s4849w/p4noXUrGpPUF0BPqIAfdAe76BLaRGAGKZ1gTDNiGxGcpsLcwJ9OTi1/V8A+AzvsmI9pkjie2zjIQZKA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.9.2.tgz", + "integrity": "sha512-w1s3+Ss+eOQbscGM4cfIFBlVg/QKxyYgj26k5AnakuHkKxH6004ZtuLe5awMBotIYF2bbGDoDhpgQ4r/kcj4rQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/plugin-debug": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.9.2.tgz", + "integrity": "sha512-j7a5hWuAFxyQAkilZwhsQ/b3T7FfHZ+0dub6j/GxKNFJp2h9qk/P1Bp7vrGASnvA9KNQBBL1ZXTe7jlh4VdPdA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "fs-extra": "^11.1.1", + "react-json-view-lite": "^2.3.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-analytics": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.9.2.tgz", + "integrity": "sha512-mAwwQJ1Us9jL/lVjXtErXto4p4/iaLlweC54yDUK1a97WfkC6Z2k5/769JsFgwOwOP+n5mUQGACXOEQ0XDuVUw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-gtag": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.9.2.tgz", + "integrity": "sha512-YJ4lDCphabBtw19ooSlc1MnxtYGpjFV9rEdzjLsUnBCeis2djUyCozZaFhCg6NGEwOn7HDDyMh0yzcdRpnuIvA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@types/gtag.js": "^0.0.12", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-tag-manager": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.9.2.tgz", + "integrity": "sha512-LJtIrkZN/tuHD8NqDAW1Tnw0ekOwRTfobWPsdO15YxcicBo2ykKF0/D6n0vVBfd3srwr9Z6rzrIWYrMzBGrvNw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-sitemap": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.9.2.tgz", + "integrity": "sha512-WLh7ymgDXjG8oPoM/T4/zUP7KcSuFYRZAUTl8vR6VzYkfc18GBM4xLhcT+AKOwun6kBivYKUJf+vlqYJkm+RHw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "fs-extra": "^11.1.1", + "sitemap": "^7.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-svgr": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.9.2.tgz", + "integrity": "sha512-n+1DE+5b3Lnf27TgVU5jM1d4x5tUh2oW5LTsBxJX4PsAPV0JGcmI6p3yLYtEY0LRVEIJh+8RsdQmRE66wSV8mw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@svgr/core": "8.1.0", + "@svgr/webpack": "^8.1.0", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/preset-classic": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.9.2.tgz", + "integrity": "sha512-IgyYO2Gvaigi21LuDIe+nvmN/dfGXAiMcV/murFqcpjnZc7jxFAxW+9LEjdPt61uZLxG4ByW/oUmX/DDK9t/8w==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/plugin-content-blog": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/plugin-content-pages": "3.9.2", + "@docusaurus/plugin-css-cascade-layers": "3.9.2", + "@docusaurus/plugin-debug": "3.9.2", + "@docusaurus/plugin-google-analytics": "3.9.2", + "@docusaurus/plugin-google-gtag": "3.9.2", + "@docusaurus/plugin-google-tag-manager": "3.9.2", + "@docusaurus/plugin-sitemap": "3.9.2", + "@docusaurus/plugin-svgr": "3.9.2", + "@docusaurus/theme-classic": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-search-algolia": "3.9.2", + "@docusaurus/types": "3.9.2" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-classic": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.9.2.tgz", + "integrity": "sha512-IGUsArG5hhekXd7RDb11v94ycpJpFdJPkLnt10fFQWOVxAtq5/D7hT6lzc2fhyQKaaCE62qVajOMKL7OiAFAIA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/plugin-content-blog": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/plugin-content-pages": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-translations": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "infima": "0.2.0-alpha.45", + "lodash": "^4.17.21", + "nprogress": "^0.2.0", + "postcss": "^8.5.4", + "prism-react-renderer": "^2.3.0", + "prismjs": "^1.29.0", + "react-router-dom": "^5.3.4", + "rtlcss": "^4.1.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-common": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", + "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", + "license": "MIT", + "dependencies": { + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "clsx": "^2.0.0", + "parse-numeric-range": "^1.3.0", + "prism-react-renderer": "^2.3.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-mermaid": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.2.tgz", + "integrity": "sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "mermaid": ">=11.6.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mermaid-js/layout-elk": "^0.1.9", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@mermaid-js/layout-elk": { + "optional": true + } + } + }, + "node_modules/@docusaurus/theme-search-algolia": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", + "integrity": "sha512-GBDSFNwjnh5/LdkxCKQHkgO2pIMX1447BxYUBG2wBiajS21uj64a+gH/qlbQjDLxmGrbrllBrtJkUHxIsiwRnw==", + "license": "MIT", + "dependencies": { + "@docsearch/react": "^3.9.0 || ^4.1.0", + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-translations": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "algoliasearch": "^5.37.0", + "algoliasearch-helper": "^3.26.0", + "clsx": "^2.0.0", + "eta": "^2.2.0", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-translations": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.9.2.tgz", + "integrity": "sha512-vIryvpP18ON9T9rjgMRFLr2xJVDpw1rtagEGf8Ccce4CkTrvM/fRB8N2nyWYOW5u3DdjkwKw5fBa+3tbn9P4PA==", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/types": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", + "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.0.0", + "@types/history": "^4.7.11", + "@types/mdast": "^4.0.2", + "@types/react": "*", + "commander": "^5.1.0", + "joi": "^17.9.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "utility-types": "^3.10.0", + "webpack": "^5.95.0", + "webpack-merge": "^5.9.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/types/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docusaurus/utils": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", + "integrity": "sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "escape-string-regexp": "^4.0.0", + "execa": "5.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "github-slugger": "^1.5.0", + "globby": "^11.1.0", + "gray-matter": "^4.0.3", + "jiti": "^1.20.0", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "micromatch": "^4.0.5", + "p-queue": "^6.6.2", + "prompts": "^2.4.2", + "resolve-pathname": "^3.0.0", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-common": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.9.2.tgz", + "integrity": "sha512-I53UC1QctruA6SWLvbjbhCpAw7+X7PePoe5pYcwTOEXD/PxeP8LnECAhTHHwWCblyUX5bMi4QLRkxvyZ+IT8Aw==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-validation": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.9.2.tgz", + "integrity": "sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "fs-extra": "^11.2.0", + "joi": "^17.9.2", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "license": "MIT" + }, + "node_modules/@faker-js/faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", + "deprecated": "Please update to a newer version.", + "license": "MIT" + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@hookform/error-message": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.1.tgz", + "integrity": "sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, + "node_modules/@inkeep/docusaurus": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@inkeep/docusaurus/-/docusaurus-2.0.16.tgz", + "integrity": "sha512-dQhjlvFnl3CVr0gWeJ/V/qLnDy1XYrCfkdVSa2D3gJTxI9/vOf9639Y1aPxTxO88DiXuW9CertLrZLB6SoJ2yg==", + "license": "MIT" + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", + "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@redocly/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", + "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.6", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.6.tgz", + "integrity": "sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==", + "license": "MIT", + "dependencies": { + "@redocly/ajv": "^8.11.2", + "@redocly/config": "^0.22.0", + "colorette": "^1.2.0", + "https-proxy-agent": "^7.0.5", + "js-levenshtein": "^1.1.6", + "js-yaml": "^4.1.0", + "minimatch": "^5.0.1", + "pluralize": "^8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", + "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", + "license": "MIT", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@slorber/remark-comment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", + "integrity": "sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.1.0", + "micromark-util-symbol": "^1.0.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/gtag.js": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", + "integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "license": "MIT" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://mirrors.tencent.com/npm/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.0.tgz", + "integrity": "sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", + "license": "MIT" + }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-redux": { + "version": "7.1.34", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", + "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-config": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.11.tgz", + "integrity": "sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "^5.1.0" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "license": "MIT" + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vercel/oidc": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", + "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ai": { + "version": "5.0.111", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.111.tgz", + "integrity": "sha512-kD1eBl3ZbSYIz9lZe0HvQpO23HruBFfqxUl0S/MtoDF4DCmfCtKhsGGGIvoIcMpjiLlJjtF//ZWcYu+v/3YRzg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "2.0.20", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.19", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/algoliasearch": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.46.0.tgz", + "integrity": "sha512-7ML6fa2K93FIfifG3GMWhDEwT5qQzPTmoHKCTvhzGEwdbQ4n0yYUWZlLYT75WllTGJCJtNUI0C1ybN4BCegqvg==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.12.0", + "@algolia/client-abtesting": "5.46.0", + "@algolia/client-analytics": "5.46.0", + "@algolia/client-common": "5.46.0", + "@algolia/client-insights": "5.46.0", + "@algolia/client-personalization": "5.46.0", + "@algolia/client-query-suggestions": "5.46.0", + "@algolia/client-search": "5.46.0", + "@algolia/ingestion": "1.46.0", + "@algolia/monitoring": "1.46.0", + "@algolia/recommend": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/algoliasearch-helper": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.1.tgz", + "integrity": "sha512-CAlCxm4fYBXtvc5MamDzP6Svu8rW4z9me4DCBY1rQ2UDJ0u0flWmusQ8M3nOExZsLLRcUwUPoRAPMrhzOG3erw==", + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, + "node_modules/allof-merge": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/allof-merge/-/allof-merge-0.6.7.tgz", + "integrity": "sha512-slvjkM56OdeVkm1tllrnaumtSHwqyHrepXkAe6Am+CW4WdbHkNqdOKPF6cvY3/IouzvXk1BoLICT5LY7sCoFGw==", + "license": "MIT", + "dependencies": { + "json-crawl": "^0.5.3" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/boxen": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", + "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^6.2.0", + "chalk": "^4.1.2", + "cli-boxes": "^3.0.0", + "string-width": "^5.0.1", + "type-fest": "^2.5.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/charset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "license": "MIT" + }, + "node_modules/combine-promises": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.2.0.tgz", + "integrity": "sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compute-gcd": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.1.tgz", + "integrity": "sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==", + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "node_modules/compute-lcm": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.2.tgz", + "integrity": "sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==", + "dependencies": { + "compute-gcd": "^1.2.1", + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/configstore": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", + "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^6.0.1", + "graceful-fs": "^4.2.6", + "unique-string": "^3.0.0", + "write-file-atomic": "^3.0.3", + "xdg-basedir": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/yeoman/configstore?sponsor=1" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/copy-text-to-clipboard": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz", + "integrity": "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz", + "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/css-blank-pseudo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", + "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", + "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-has-pseudo": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz", + "integrity": "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", + "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "cssnano": "^6.0.1", + "jest-worker": "^29.4.3", + "postcss": "^8.4.24", + "schema-utils": "^4.0.1", + "serialize-javascript": "^6.0.1" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", + "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.5.2.tgz", + "integrity": "sha512-Pmoj9RmD8RIoIzA2EQWO4D4RMeDts0tgAH0VXdlNdxjuBGI3a9wMOIcUwaPNmD4r2qtIa06gqkIf7sECl+cBCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/detect-package-manager": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-3.0.2.tgz", + "integrity": "sha512-8JFjJHutStYrfWwzfretQoyNGoZVW1Fsrp4JO9spa7h/fBfwgTMEIy4/LBzRDGsxwVPHU0q+T9YvwLDJoOApLQ==", + "license": "MIT", + "dependencies": { + "execa": "^5.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/detect-port": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", + "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", + "license": "MIT", + "dependencies": { + "address": "^1.0.1", + "debug": "4" + }, + "bin": { + "detect": "bin/detect-port.js", + "detect-port": "bin/detect-port.js" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/docusaurus-plugin-openapi-docs": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.5.1.tgz", + "integrity": "sha512-3I6Sjz19D/eM86a24/nVkYfqNkl/zuXSP04XVo7qm/vlPeCpHVM4li2DLj7PzElr6dlS9RbaS4HVIQhEOPGBRQ==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.4", + "@redocly/openapi-core": "^1.10.5", + "allof-merge": "^0.6.6", + "chalk": "^4.1.2", + "clsx": "^1.1.1", + "fs-extra": "^9.0.1", + "json-pointer": "^0.6.2", + "json5": "^2.2.3", + "lodash": "^4.17.20", + "mustache": "^4.2.0", + "openapi-to-postmanv2": "^4.21.0", + "postman-collection": "^4.4.0", + "slugify": "^1.6.5", + "swagger2openapi": "^7.0.8", + "xml-formatter": "^2.6.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "^3.5.0", + "@docusaurus/utils": "^3.5.0", + "@docusaurus/utils-validation": "^3.5.0", + "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/docusaurus-plugin-openapi-docs/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/docusaurus-plugin-openapi-docs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/docusaurus-plugin-sass": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.6.tgz", + "integrity": "sha512-2hKQQDkrufMong9upKoG/kSHJhuwd+FA3iAe/qzS/BmWpbIpe7XKmq5wlz4J5CJaOPu4x+iDJbgAxZqcoQf0kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "sass-loader": "^16.0.2" + }, + "peerDependencies": { + "@docusaurus/core": "^2.0.0-beta || ^3.0.0-alpha", + "sass": "^1.30.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.5.1.tgz", + "integrity": "sha512-C7mYh9JC3l9jjRtqJVu0EIyOgxHB08jE0Tp5NSkNkrrBak4A13SrXCisNjvt1eaNjS+tsz7qD0bT3aI5hsRvWA==", + "license": "MIT", + "dependencies": { + "@hookform/error-message": "^2.0.1", + "@reduxjs/toolkit": "^1.7.1", + "allof-merge": "^0.6.6", + "buffer": "^6.0.3", + "clsx": "^1.1.1", + "copy-text-to-clipboard": "^3.1.0", + "crypto-js": "^4.1.1", + "file-saver": "^2.0.5", + "lodash": "^4.17.20", + "pako": "^2.1.0", + "postman-code-generators": "^1.10.1", + "postman-collection": "^4.4.0", + "prism-react-renderer": "^2.3.0", + "process": "^0.11.10", + "react-hook-form": "^7.43.8", + "react-live": "^4.0.0", + "react-magic-dropzone": "^1.0.1", + "react-markdown": "^8.0.1", + "react-modal": "^3.15.1", + "react-redux": "^7.2.0", + "rehype-raw": "^6.1.1", + "remark-gfm": "3.0.1", + "sass": "^1.80.4", + "sass-loader": "^16.0.2", + "unist-util-visit": "^5.0.0", + "url": "^0.11.1", + "xml-formatter": "^2.6.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@docusaurus/theme-common": "^3.5.0", + "docusaurus-plugin-openapi-docs": "^4.0.0", + "docusaurus-plugin-sass": "^0.2.3", + "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-raw": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", + "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-raw/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-to-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", + "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-to-markdown/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "license": "MIT", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/rehype-raw": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", + "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-raw": "^7.2.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/emoticon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "license": "MIT" + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", + "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", + "dependencies": { + "@types/node": "*", + "require-like": ">= 0.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", + "license": "BSD-3-Clause" + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/express/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreach": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", + "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", + "license": "MIT" + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-yarn": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", + "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz", + "integrity": "sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g==", + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/html-webpack-plugin/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-reasons": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/http-reasons/-/http-reasons-0.1.0.tgz", + "integrity": "sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ==", + "license": "Apache-2.0" + }, + "node_modules/http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "license": "MIT" + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infima": { + "version": "0.2.0-alpha.45", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", + "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-yarn-global": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", + "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://mirrors.tencent.com/npm/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-crawl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/json-crawl/-/json-crawl-0.5.3.tgz", + "integrity": "sha512-BEjjCw8c7SxzNK4orhlWD5cXQh8vCk2LqDr4WgQq4CV+5dvopeYwt1Tskg67SuSLKvoFH5g0yuYtg7rcfKV6YA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-pointer": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", + "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", + "license": "MIT", + "dependencies": { + "foreach": "^2.0.4" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-compare": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", + "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.4" + } + }, + "node_modules/json-schema-merge-allof": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", + "integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==", + "license": "MIT", + "dependencies": { + "compute-lcm": "^1.1.2", + "json-schema-compare": "^0.2.2", + "lodash": "^4.17.20" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.16.27", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz", + "integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/launch-editor": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", + "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/liquid-json": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/liquid-json/-/liquid-json-0.3.1.tgz", + "integrity": "sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mdast-util-definitions/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.51.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz", + "integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "11.12.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz", + "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^0.6.3", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-space/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-events-to-acorn/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-format": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mime-format/-/mime-format-2.0.1.tgz", + "integrity": "sha512-XxU3ngPbEnrYnNbIX+lYSaYg0M01v6p2ntd2YaFksTu0vayaw5OJvbdRyWs07EYRlLED5qadUZ+xo+XhOvFhwg==", + "license": "Apache-2.0", + "dependencies": { + "charset": "^1.0.0" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", + "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/neotraverse": { + "version": "0.6.15", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.15.tgz", + "integrity": "sha512-HZpdkco+JeXq0G+WWpMJ4NsX3pqb5O7eR9uGz3FfoFt+LYzU8iRWp49nJtud6hsDoywM8tIrDo3gjgmOqJA8LA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "license": "MIT", + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", + "license": "MIT", + "dependencies": { + "es6-promise": "^3.2.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/null-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", + "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/null-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/null-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/null-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "license": "BSD-3-Clause", + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/oas-linter": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver-browser": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver-browser/-/oas-resolver-browser-2.5.6.tgz", + "integrity": "sha512-Jw5elT/kwUJrnGaVuRWe1D7hmnYWB8rfDDjBnpQ+RYY/dzAewGXeTexXzt4fGEo6PUE4eqKqPWF79MZxxvMppA==", + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "path-browserify": "^1.0.1", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-schema-walker": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-to-postmanv2": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-4.25.0.tgz", + "integrity": "sha512-sIymbkQby0gzxt2Yez8YKB6hoISEel05XwGwNrAhr6+vxJWXNxkmssQc/8UEtVkuJ9ZfUXLkip9PYACIpfPDWg==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "8.11.0", + "ajv-draft-04": "1.0.0", + "ajv-formats": "2.1.1", + "async": "3.2.4", + "commander": "2.20.3", + "graphlib": "2.1.8", + "js-yaml": "4.1.0", + "json-pointer": "0.6.2", + "json-schema-merge-allof": "0.8.1", + "lodash": "4.17.21", + "neotraverse": "0.6.15", + "oas-resolver-browser": "2.5.6", + "object-hash": "3.0.0", + "path-browserify": "1.0.1", + "postman-collection": "^4.4.0", + "swagger2openapi": "7.0.8", + "yaml": "1.10.2" + }, + "bin": { + "openapi2postmanv2": "bin/openapi2postmanv2.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openapi-to-postmanv2/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/openapi-to-postmanv2/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "license": "MIT", + "dependencies": { + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", + "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", + "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", + "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", + "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-custom-media": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-properties": { + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", + "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-unused": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", + "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", + "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", + "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-focus-within": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", + "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", + "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", + "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-lab-function": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", + "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", + "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-merge-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", + "integrity": "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", + "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", + "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.5.0.tgz", + "integrity": "sha512-xgxFQPAPxeWmsgy8cR7GM1PGAL/smA5E9qU7K//D4vucS01es3M0fDujhDJn3kY8Ip7/vVYcecbe1yY+vBo3qQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-alpha-function": "^1.0.1", + "@csstools/postcss-cascade-layers": "^5.0.2", + "@csstools/postcss-color-function": "^4.0.12", + "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", + "@csstools/postcss-color-mix-function": "^3.0.12", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", + "@csstools/postcss-content-alt-text": "^2.0.8", + "@csstools/postcss-contrast-color-function": "^2.0.12", + "@csstools/postcss-exponential-functions": "^2.0.9", + "@csstools/postcss-font-format-keywords": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.11", + "@csstools/postcss-gradients-interpolation-method": "^5.0.12", + "@csstools/postcss-hwb-function": "^4.0.12", + "@csstools/postcss-ic-unit": "^4.0.4", + "@csstools/postcss-initial": "^2.0.1", + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "@csstools/postcss-light-dark-function": "^2.0.11", + "@csstools/postcss-logical-float-and-clear": "^3.0.0", + "@csstools/postcss-logical-overflow": "^2.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", + "@csstools/postcss-logical-resize": "^3.0.0", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", + "@csstools/postcss-nested-calc": "^4.0.0", + "@csstools/postcss-normalize-display-values": "^4.0.0", + "@csstools/postcss-oklab-function": "^4.0.12", + "@csstools/postcss-position-area-property": "^1.0.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/postcss-random-function": "^2.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.12", + "@csstools/postcss-scope-pseudo-class": "^4.0.1", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", + "@csstools/postcss-system-ui-font-family": "^1.0.0", + "@csstools/postcss-text-decoration-shorthand": "^4.0.3", + "@csstools/postcss-trigonometric-functions": "^4.0.9", + "@csstools/postcss-unset-value": "^4.0.0", + "autoprefixer": "^10.4.22", + "browserslist": "^4.28.0", + "css-blank-pseudo": "^7.0.1", + "css-has-pseudo": "^7.0.3", + "css-prefers-color-scheme": "^10.0.0", + "cssdb": "^8.5.2", + "postcss-attribute-case-insensitive": "^7.0.1", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^7.0.12", + "postcss-color-hex-alpha": "^10.0.0", + "postcss-color-rebeccapurple": "^10.0.0", + "postcss-custom-media": "^11.0.6", + "postcss-custom-properties": "^14.0.6", + "postcss-custom-selectors": "^8.0.5", + "postcss-dir-pseudo-class": "^9.0.1", + "postcss-double-position-gradients": "^6.0.4", + "postcss-focus-visible": "^10.0.1", + "postcss-focus-within": "^9.0.1", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^6.0.0", + "postcss-image-set-function": "^7.0.0", + "postcss-lab-function": "^7.0.12", + "postcss-logical": "^8.1.0", + "postcss-nesting": "^13.0.2", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^6.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^10.0.0", + "postcss-pseudo-class-any-link": "^10.0.1", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", + "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-reduce-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", + "integrity": "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", + "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-sort-media-queries": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz", + "integrity": "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==", + "license": "MIT", + "dependencies": { + "sort-css-media-queries": "2.2.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.23" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss-zindex": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz", + "integrity": "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postman-code-generators": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/postman-code-generators/-/postman-code-generators-1.14.2.tgz", + "integrity": "sha512-qZAyyowfQAFE4MSCu2KtMGGQE/+oG1JhMZMJNMdZHYCSfQiVVeKxgk3oI4+KJ3d1y5rrm2D6C6x+Z+7iyqm+fA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "async": "3.2.2", + "detect-package-manager": "3.0.2", + "lodash": "4.17.21", + "path": "0.12.7", + "postman-collection": "^4.4.0", + "shelljs": "0.8.5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/postman-code-generators/node_modules/async": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", + "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==", + "license": "MIT" + }, + "node_modules/postman-collection": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.5.0.tgz", + "integrity": "sha512-152JSW9pdbaoJihwjc7Q8lc3nPg/PC9lPTHdMk7SHnHhu/GBJB7b2yb9zG7Qua578+3PxkQ/HYBuXpDSvsf7GQ==", + "license": "Apache-2.0", + "dependencies": { + "@faker-js/faker": "5.5.3", + "file-type": "3.9.0", + "http-reasons": "0.1.0", + "iconv-lite": "0.6.3", + "liquid-json": "0.3.1", + "lodash": "4.17.21", + "mime-format": "2.0.1", + "mime-types": "2.1.35", + "postman-url-encoder": "3.0.5", + "semver": "7.6.3", + "uuid": "8.3.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-collection/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-collection/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/postman-url-encoder": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postman-url-encoder/-/postman-url-encoder-3.0.5.tgz", + "integrity": "sha512-jOrdVvzUXBC7C+9gkIkpDJ3HIxOHTIqjpQ4C1EMt1ZGeMvSEpbFCKq23DEfgsj46vMnDgyQf+1ZLp2Wm+bKSsA==", + "license": "Apache-2.0", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", + "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/raw-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/raw-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/raw-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/raw-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "name": "@slorber/react-helmet-async", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", + "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.2.0", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.68.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz", + "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-json-view-lite": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-live": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/react-live/-/react-live-4.1.8.tgz", + "integrity": "sha512-B2SgNqwPuS2ekqj4lcxi5TibEcjWkdVyYykBEUBshPAPDQ527x2zPEZg560n8egNtAjUpwXFQm7pcXV65aAYmg==", + "license": "MIT", + "dependencies": { + "prism-react-renderer": "^2.4.0", + "sucrase": "^3.35.0", + "use-editable": "^2.3.3" + }, + "engines": { + "node": ">= 0.12.0", + "npm": ">= 2.0.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/react-loadable": { + "name": "@docusaurus/react-loadable", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", + "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-loadable-ssr-addon-v5-slorber": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", + "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.3" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "react-loadable": "*", + "webpack": ">=4.41.1 || 5.x" + } + }, + "node_modules/react-magic-dropzone": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-magic-dropzone/-/react-magic-dropzone-1.0.1.tgz", + "integrity": "sha512-0BIROPARmXHpk4AS3eWBOsewxoM5ndk2psYP/JmbCq8tz3uR2LIV1XiroZ9PKrmDRMctpW+TvsBCtWasuS8vFA==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", + "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^18.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/react-markdown/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/react-markdown/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/react-markdown/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/react-markdown/node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/react-markdown/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/react-markdown/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/react-markdown/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-markdown/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-markdown/node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==", + "license": "MIT", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-config": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", + "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + }, + "peerDependencies": { + "react": ">=15", + "react-router": ">=5" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "license": "MIT", + "peerDependencies": { + "redux": "^4" + } + }, + "node_modules/reftools": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", + "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remark-directive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-emoji": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz", + "integrity": "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.2", + "emoticon": "^4.0.1", + "mdast-util-find-and-replace": "^3.0.1", + "node-emoji": "^2.1.0", + "unified": "^11.0.4" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", + "engines": { + "node": "*" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rtlcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.96.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.96.0.tgz", + "integrity": "sha512-8u4xqqUeugGNCYwr9ARNtQKTOj4KmYiJAVKXf2CTIivTCR51j96htbMKWDru8H5SaQWpyVgTfOF8Ylyf5pun1Q==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.6", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.6.tgz", + "integrity": "sha512-sglGzId5gmlfxNs4gK2U3h7HlVRfx278YK6Ono5lwzuvi1jxig80YiuHkaDBVsYIKFhx8wN7XSCI0M2IDS/3qA==", + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sax": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "license": "BlueOak-1.0.0" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT", + "peer": true + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", + "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/serve-handler/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "license": "MIT", + "dependencies": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "node_modules/should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "license": "MIT", + "dependencies": { + "should-type": "^1.4.0" + } + }, + "node_modules/should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "node_modules/should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "license": "MIT" + }, + "node_modules/should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "node_modules/should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz", + "integrity": "sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==", + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.6.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/sort-css-media-queries": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz", + "integrity": "sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==", + "license": "MIT", + "engines": { + "node": ">= 6.3.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/srcset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", + "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-js/node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/style-to-js/node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/style-to-object": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", + "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, + "node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/swagger2openapi": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "node-fetch": "^2.6.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "oas-validator": "^5.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "boast": "boast.js", + "oas-validate": "oas-validate.js", + "swagger2openapi": "swagger2openapi.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/swr": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.7.tgz", + "integrity": "sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz", + "integrity": "sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-notifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", + "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^7.0.0", + "chalk": "^5.0.1", + "configstore": "^6.0.0", + "has-yarn": "^3.0.0", + "import-lazy": "^4.0.0", + "is-ci": "^3.0.1", + "is-installed-globally": "^0.4.0", + "is-npm": "^6.0.0", + "is-yarn-global": "^0.4.0", + "latest-version": "^7.0.0", + "pupa": "^3.1.0", + "semver": "^7.3.7", + "semver-diff": "^4.0.0", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/boxen": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", + "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/url-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/url-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/use-editable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/use-editable/-/use-editable-2.3.3.tgz", + "integrity": "sha512-7wVD2JbfAFJ3DK0vITvXBdpd9JAz5BcKAAolsnLBuBn6UDDwBGuCIAGvR3yA2BNKm578vAMVHFCWaOcA+BhhiA==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uvu/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==", + "license": "MIT" + }, + "node_modules/validate.io-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", + "integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==" + }, + "node_modules/validate.io-integer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", + "integrity": "sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==", + "dependencies": { + "validate.io-number": "^1.0.3" + } + }, + "node_modules/validate.io-integer-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz", + "integrity": "sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==", + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-integer": "^1.0.4" + } + }, + "node_modules/validate.io-number": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", + "integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==" + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.103.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/webpack-dev-middleware/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/webpack-dev-middleware/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpackbar": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", + "integrity": "sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "consola": "^3.2.3", + "figures": "^3.2.0", + "markdown-table": "^2.0.0", + "pretty-time": "^1.1.0", + "std-env": "^3.7.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "webpack": "3 || 4 || 5" + } + }, + "node_modules/webpackbar/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/webpackbar/node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "license": "MIT", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webpackbar/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpackbar/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-formatter": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-2.6.1.tgz", + "integrity": "sha512-dOiGwoqm8y22QdTNI7A+N03tyVfBlQ0/oehAzxIZtwnFAHGeSlrfjF73YQvzSsa/Kt6+YZasKsrdu6OIpuBggw==", + "license": "MIT", + "dependencies": { + "xml-parser-xo": "^3.2.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/xml-parser-xo": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz", + "integrity": "sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "license": "Apache-2.0" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..1649df7 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,58 @@ +{ + "name": "docs", + "version": "0.0.0", + "private": true, + "scripts": { + "build:config": "node scripts/build-config.mjs", + "docusaurus": "docusaurus", + "start": "npm run build:config && npm run regen-docs && docusaurus start --host 0.0.0.0", + "build": "npm run build:config && npm run regen-docs && docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "gen-api-docs": "docusaurus gen-api-docs all", + "clear-api-docs": "docusaurus clean-api-docs all", + "regen-docs": "npm run clear-api-docs && npm run gen-api-docs", + "serve": "docusaurus serve --host 0.0.0.0", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids" + }, + "dependencies": { + "@docusaurus/core": "^3.7.0", + "@docusaurus/plugin-content-docs": "^3.7.0", + "@docusaurus/preset-classic": "^3.7.0", + "@docusaurus/theme-mermaid": "^3.7.0", + "@inkeep/docusaurus": "^2.0.16", + "@mdx-js/react": "^3.1.0", + "@types/js-yaml": "^4.0.9", + "clsx": "^2.1.1", + "docusaurus-plugin-openapi-docs": "^4.5.1", + "docusaurus-theme-openapi-docs": "^4.5.1", + "js-yaml": "^4.1.1", + "marked": "^16.4.2", + "prism-react-renderer": "^2.4.1", + "raw-loader": "^4.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "^3.7.0", + "@docusaurus/types": "^3.7.0", + "@types/react": "^18.3.27" + }, + "engines": { + "node": ">=18.0" + } +} diff --git a/docs/plugins/js-yaml-loader.js b/docs/plugins/js-yaml-loader.js new file mode 100644 index 0000000..66451bd --- /dev/null +++ b/docs/plugins/js-yaml-loader.js @@ -0,0 +1,9 @@ +const yaml = require("js-yaml"); + +// Webpack loader that compiles a YAML file into a default-exported JS object, +// so docs data can be authored in YAML (block scalars, no quote/newline +// escaping) but imported exactly like the JSON it replaces. +module.exports = function (source) { + const data = yaml.load(source); + return `export default ${JSON.stringify(data)};`; +}; diff --git a/docs/plugins/raw-loader.js b/docs/plugins/raw-loader.js new file mode 100644 index 0000000..693d099 --- /dev/null +++ b/docs/plugins/raw-loader.js @@ -0,0 +1,12 @@ +module.exports = function (context, options) { + return { + name: 'labelmap', + configureWebpack(config, isServer, utils) { + return { + module: { + rules: [{ test: /\.txt$/, use: 'raw-loader' }], + }, + }; + }, + }; +}; diff --git a/docs/plugins/yaml-loader.js b/docs/plugins/yaml-loader.js new file mode 100644 index 0000000..475940e --- /dev/null +++ b/docs/plugins/yaml-loader.js @@ -0,0 +1,23 @@ +const path = require("node:path"); + +// Enables importing YAML data files from docs/data as plain JS objects. +// Scoped to the data directory so it never intercepts other .yaml files +// (e.g. the OpenAPI spec under static/). +module.exports = function (context, options) { + return { + name: "yaml-data-loader", + configureWebpack(config, isServer, utils) { + return { + module: { + rules: [ + { + test: /\.ya?ml$/, + include: path.resolve(__dirname, "..", "data"), + use: path.resolve(__dirname, "js-yaml-loader.js"), + }, + ], + }, + }; + }, + }; +}; diff --git a/docs/scripts/README.md b/docs/scripts/README.md new file mode 100644 index 0000000..347536a --- /dev/null +++ b/docs/scripts/README.md @@ -0,0 +1,184 @@ +# Documentation Scripts + +## generate_ui_tabs.py + +Automatically generates "Frigate UI" tab content for documentation files based on the YAML config examples already in the docs. + +Instead of manually writing UI instructions for every YAML block, this script reads three data sources from the codebase and generates the UI tabs: + +1. **JSON Schema** (from Pydantic config models) -- field names, types, defaults +2. **i18n translation files** -- the exact labels shown in the Settings UI +3. **Section mappings** (from Settings.tsx) -- config key to UI navigation path + +### Prerequisites + +Run from the repository root. The script imports Frigate's Python config models directly, so the `frigate` package must be importable: + +```bash +# From repo root -- no extra install needed if your environment can import frigate +python3 docs/scripts/generate_ui_tabs.py --help +``` + +### Usage + +#### Preview (default) + +Shows what would be generated for each bare YAML block, without modifying any files: + +```bash +# Single file +python3 docs/scripts/generate_ui_tabs.py docs/docs/configuration/record.md + +# All config docs +python3 docs/scripts/generate_ui_tabs.py docs/docs/configuration/ +``` + +#### Inject + +Wraps bare YAML blocks with `` and inserts the generated UI tab. Also adds the required imports (`ConfigTabs`, `TabItem`, `NavPath`) after the frontmatter if missing. + +Already-wrapped blocks are skipped (idempotent). + +```bash +python3 docs/scripts/generate_ui_tabs.py --inject docs/docs/configuration/record.md +``` + +#### Check + +Compares existing UI tabs against what the script would generate from the current schema and i18n files. Prints a unified diff for each drifted block and exits with code 1 if any drift is found. + +Use this in CI to catch stale docs after schema or i18n changes. + +```bash +python3 docs/scripts/generate_ui_tabs.py --check docs/docs/configuration/ +``` + +#### Regenerate + +Replaces the UI tab content in existing `` blocks with freshly generated content. The YAML tab is preserved exactly as-is. Only blocks that have actually changed are rewritten. + +```bash +# Preview changes without writing +python3 docs/scripts/generate_ui_tabs.py --regenerate --dry-run docs/docs/configuration/ + +# Apply changes +python3 docs/scripts/generate_ui_tabs.py --regenerate docs/docs/configuration/ +``` + +#### Output to directory (`--outdir`) + +Write generated files to a separate directory instead of modifying the originals. The source directory structure is mirrored. Files without changes are copied as-is so the output is a complete snapshot suitable for diffing. + +Works with `--inject` and `--regenerate`. + +```bash +# Generate into a named directory +python3 docs/scripts/generate_ui_tabs.py --inject --outdir /tmp/generated docs/docs/configuration/ + +# Then diff original vs generated +diff -rq docs/docs/configuration/ /tmp/generated/ + +# Or let an AI agent compare them +diff -ru docs/docs/configuration/record.md /tmp/generated/record.md +``` + +This is useful for AI agents that need to review the generated output before applying it, or for previewing what `--inject` or `--regenerate` would do across an entire directory. + +#### Verbose mode + +Add `-v` to any mode for detailed diagnostics (skipped blocks, reasons, unchanged blocks): + +```bash +python3 docs/scripts/generate_ui_tabs.py -v docs/docs/configuration/ +``` + +### Typical workflow + +```bash +# 1. Preview what would be generated (output to temp dir, originals untouched) +python3 docs/scripts/generate_ui_tabs.py --inject --outdir /tmp/ui-preview docs/docs/configuration/ +# Compare: diff -ru docs/docs/configuration/ /tmp/ui-preview/ + +# 2. Apply: inject UI tabs into the actual docs +python3 docs/scripts/generate_ui_tabs.py --inject docs/docs/configuration/ + +# 3. Review and hand-edit where needed (the script gets you 90% there) + +# 4. Later, after schema or i18n changes, check for drift +python3 docs/scripts/generate_ui_tabs.py --check docs/docs/configuration/ + +# 5. If drifted, preview then regenerate +python3 docs/scripts/generate_ui_tabs.py --regenerate --outdir /tmp/ui-regen docs/docs/configuration/ +# Compare: diff -ru docs/docs/configuration/ /tmp/ui-regen/ + +# 6. Apply regeneration +python3 docs/scripts/generate_ui_tabs.py --regenerate docs/docs/configuration/ +``` + +### How it decides what to generate + +The script detects two patterns from the YAML block content: + +**Pattern A -- Field table.** When the YAML has inline comments (e.g., `# <- description`), the script generates a markdown table with field names and descriptions: + +```markdown +Navigate to . + +| Field | Description | +|-------|-------------| +| **Continuous retention > Retention days** | Days to retain recordings. | +| **Motion retention > Retention days** | Days to retain recordings. | +``` + +**Pattern B -- Set instructions.** When the YAML has concrete values without comments, the script generates step-by-step instructions: + +```markdown +Navigate to . + +- Set **Enable recording** to on +- Set **Continuous retention > Retention days** to `3` +- Set **Alert retention > Event retention > Retention days** to `30` +- Set **Alert retention > Event retention > Retention mode** to `all` +``` + +**Camera-level config** is auto-detected when the YAML is nested under `cameras:`. The output uses a generic camera reference rather than the example camera name from the YAML: + +```markdown +1. Navigate to and select your camera. + - Set **Enable recording** to on + - Set **Continuous retention > Retention days** to `5` +``` + +### What gets skipped + +- YAML blocks already inside `` (for `--inject`) +- YAML blocks whose top-level key is not a known config section (e.g., `go2rtc`, `docker-compose`, `scrape_configs`) +- Fields listed in `hiddenFields` in the section configs (e.g., `enabled_in_config`) + +### File structure + +``` +docs/scripts/ +├── generate_ui_tabs.py # CLI entry point +├── README.md # This file +└── lib/ + ├── __init__.py + ├── schema_loader.py # Loads JSON schema from Pydantic models + ├── i18n_loader.py # Loads i18n translation JSON files + ├── section_config_parser.py # Parses TS section configs (hiddenFields, etc.) + ├── yaml_extractor.py # Extracts YAML blocks and ConfigTabs from markdown + ├── ui_generator.py # Generates UI tab markdown content + └── nav_map.py # Maps config sections to Settings UI nav paths +``` + +### Data sources + +| Source | Path | What it provides | +|--------|------|------------------| +| Pydantic models | `frigate/config/` | Field names, types, defaults, nesting | +| JSON schema | Generated from Pydantic at runtime | Full schema with `$defs` and `$ref` | +| i18n (global) | `web/public/locales/en/config/global.json` | Field labels for global settings | +| i18n (cameras) | `web/public/locales/en/config/cameras.json` | Field labels for camera settings | +| i18n (menu) | `web/public/locales/en/views/settings.json` | Sidebar menu labels | +| Section configs | `web/src/components/config-form/section-configs/*.ts` | Hidden fields, advanced fields, field order | +| Navigation map | Hardcoded from `web/src/pages/Settings.tsx` | Config section to UI path mapping | diff --git a/docs/scripts/build-config.mjs b/docs/scripts/build-config.mjs new file mode 100644 index 0000000..78926be --- /dev/null +++ b/docs/scripts/build-config.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node + +/** + * Build script: reads config.yaml and generates TypeScript files + * for the Docker Compose Generator. + * + * Usage: node scripts/build-config.mjs + */ + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import yaml from "js-yaml"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const CONFIG_DIR = path.resolve(__dirname, "../src/components/DockerComposeGenerator/config"); +const YAML_PATH = path.join(CONFIG_DIR, "config.yaml"); + +// Read & parse YAML +const raw = fs.readFileSync(YAML_PATH, "utf8"); +const config = yaml.load(raw); + +if (!config.devices || !config.hardware || !config.ports) { + console.error("config.yaml must contain 'devices', 'hardware', and 'ports' sections."); + process.exit(1); +} + +/** + * Generate a .ts file from a section of the YAML config. + */ +function generateTsFile(sectionName, items, typeName, varName, mapVarName, yamlFilename) { + const jsonItems = JSON.stringify(items, null, 2); + // Indent JSON to fit inside the array literal + const indented = jsonItems + .split("\n") + .map((line, i) => (i === 0 ? line : " " + line)) + .join("\n"); + + const content = `/** + * AUTO-GENERATED FILE — do not edit directly. + * Source: ${yamlFilename} + * To update, edit the YAML file and run: npm run build:config + */ + +import type { ${typeName} } from "./types"; + +export const ${varName}: ${typeName}[] = ${indented}; + +/** Lookup map for quick access by ID */ +export const ${mapVarName}: Map = new Map(${varName}.map((item) => [item.id, item])); +`; + + const outPath = path.join(CONFIG_DIR, `${sectionName}.ts`); + fs.writeFileSync(outPath, content, "utf8"); + console.log(` ✓ Generated ${sectionName}.ts (${items.length} items)`); +} + +console.log("Building config from config.yaml..."); + +generateTsFile("devices", config.devices, "DeviceConfig", "devices", "deviceMap", "config.yaml"); +generateTsFile("hardware", config.hardware, "HardwareOption", "hardwareOptions", "hardwareMap", "config.yaml"); +generateTsFile("ports", config.ports, "PortConfig", "ports", "portMap", "config.yaml"); + +console.log("Done!"); diff --git a/docs/scripts/generate_ui_tabs.py b/docs/scripts/generate_ui_tabs.py new file mode 100644 index 0000000..fa46892 --- /dev/null +++ b/docs/scripts/generate_ui_tabs.py @@ -0,0 +1,660 @@ +#!/usr/bin/env python3 +"""Generate Frigate UI tab content for documentation files. + +This script reads YAML code blocks from documentation markdown files and +generates corresponding "Frigate UI" tab instructions based on: +- JSON Schema (from Pydantic config models) +- i18n translation files (for UI field labels) +- Section configs (for hidden/advanced field info) +- Navigation mappings (for Settings UI paths) + +Usage: + # Preview generated UI tabs for a single file + python docs/scripts/generate_ui_tabs.py docs/docs/configuration/record.md + + # Preview all config docs + python docs/scripts/generate_ui_tabs.py docs/docs/configuration/ + + # Inject UI tabs into files (wraps bare YAML blocks with ConfigTabs) + python docs/scripts/generate_ui_tabs.py --inject docs/docs/configuration/record.md + + # Regenerate existing UI tabs from current schema/i18n + python docs/scripts/generate_ui_tabs.py --regenerate docs/docs/configuration/ + + # Check for drift between existing UI tabs and what would be generated + python docs/scripts/generate_ui_tabs.py --check docs/docs/configuration/ + + # Write generated files to a temp directory for comparison (originals unchanged) + python docs/scripts/generate_ui_tabs.py --inject --outdir /tmp/generated docs/docs/configuration/ + + # Show detailed warnings and diagnostics + python docs/scripts/generate_ui_tabs.py --verbose docs/docs/configuration/ +""" + +import argparse +import difflib +import shutil +import sys +import tempfile +from pathlib import Path + +# Ensure frigate package is importable +sys.path.insert(0, str(Path(__file__).resolve().parents[1].parent)) + +from lib.i18n_loader import load_i18n +from lib.nav_map import ALL_CONFIG_SECTIONS +from lib.schema_loader import load_schema +from lib.section_config_parser import load_section_configs +from lib.ui_generator import generate_ui_content, wrap_with_config_tabs +from lib.yaml_extractor import ( + extract_config_tabs_blocks, + extract_yaml_blocks, +) + + +def process_file( + filepath: Path, + schema: dict, + i18n: dict, + section_configs: dict, + inject: bool = False, + verbose: bool = False, + outpath: Path | None = None, +) -> dict: + """Process a single markdown file for initial injection of bare YAML blocks. + + Args: + outpath: If set, write the result here instead of modifying filepath. + + Returns: + Stats dict with counts of blocks found, generated, skipped, etc. + """ + content = filepath.read_text() + blocks = extract_yaml_blocks(content) + + stats = { + "file": str(filepath), + "total_blocks": len(blocks), + "config_blocks": 0, + "already_wrapped": 0, + "generated": 0, + "skipped": 0, + "warnings": [], + } + + if not blocks: + return stats + + # For injection, we need to track replacements + replacements: list[tuple[int, int, str]] = [] + + for block in blocks: + # Skip non-config YAML blocks + if block.section_key is None or ( + block.section_key not in ALL_CONFIG_SECTIONS + and not block.is_camera_level + ): + stats["skipped"] += 1 + if verbose and block.config_keys: + stats["warnings"].append( + f" Line {block.line_start}: Skipped block with keys " + f"{block.config_keys} (not a known config section)" + ) + continue + + stats["config_blocks"] += 1 + + # Skip already-wrapped blocks + if block.inside_config_tabs: + stats["already_wrapped"] += 1 + if verbose: + stats["warnings"].append( + f" Line {block.line_start}: Already inside ConfigTabs, skipping" + ) + continue + + # Generate UI content + ui_content = generate_ui_content( + block, schema, i18n, section_configs + ) + + if ui_content is None: + stats["skipped"] += 1 + if verbose: + stats["warnings"].append( + f" Line {block.line_start}: Could not generate UI content " + f"for section '{block.section_key}'" + ) + continue + + stats["generated"] += 1 + + if inject: + full_block = wrap_with_config_tabs( + ui_content, block.raw, block.highlight + ) + replacements.append((block.line_start, block.line_end, full_block)) + else: + # Preview mode: print to stdout + print(f"\n{'='*60}") + print(f"File: {filepath}") + print(f"Line {block.line_start}: section={block.section_key}, " + f"camera={block.is_camera_level}") + print(f"{'='*60}") + print() + print("--- Generated UI tab ---") + print(ui_content) + print() + print("--- Would produce ---") + print(wrap_with_config_tabs(ui_content, block.raw, block.highlight)) + print() + + # Apply injections in reverse order (to preserve line numbers) + if inject and replacements: + lines = content.split("\n") + for start, end, replacement in reversed(replacements): + # start/end are 1-based line numbers + # The YAML block spans from the ``` line before start to the ``` line at end + # We need to replace from the opening ``` to the closing ``` + block_start = start - 2 # 0-based index of ```yaml line + block_end = end - 1 # 0-based index of closing ``` line + + replacement_lines = replacement.split("\n") + lines[block_start : block_end + 1] = replacement_lines + + new_content = "\n".join(lines) + + # Ensure imports are present + new_content = _ensure_imports(new_content) + + target = outpath or filepath + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(new_content) + print(f" Injected {len(replacements)} ConfigTabs block(s) into {target}") + elif outpath is not None: + # No changes but outdir requested -- copy original so the output + # directory contains a complete set of files for diffing. + outpath.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(filepath, outpath) + + return stats + + +def regenerate_file( + filepath: Path, + schema: dict, + i18n: dict, + section_configs: dict, + dry_run: bool = False, + verbose: bool = False, + outpath: Path | None = None, +) -> dict: + """Regenerate UI tabs in existing ConfigTabs blocks. + + Strips the current UI tab content and regenerates it from the YAML tab + using the current schema and i18n data. + + Args: + outpath: If set, write the result here instead of modifying filepath. + + Returns: + Stats dict + """ + content = filepath.read_text() + tab_blocks = extract_config_tabs_blocks(content) + + stats = { + "file": str(filepath), + "total_blocks": len(tab_blocks), + "regenerated": 0, + "unchanged": 0, + "skipped": 0, + "warnings": [], + } + + if not tab_blocks: + return stats + + replacements: list[tuple[int, int, str]] = [] + + for tab_block in tab_blocks: + yaml_block = tab_block.yaml_block + + # Skip non-config blocks + if yaml_block.section_key is None or ( + yaml_block.section_key not in ALL_CONFIG_SECTIONS + and not yaml_block.is_camera_level + ): + stats["skipped"] += 1 + if verbose: + stats["warnings"].append( + f" Line {tab_block.line_start}: Skipped (not a config section)" + ) + continue + + # Generate fresh UI content + new_ui = generate_ui_content( + yaml_block, schema, i18n, section_configs + ) + + if new_ui is None: + stats["skipped"] += 1 + if verbose: + stats["warnings"].append( + f" Line {tab_block.line_start}: Could not regenerate " + f"for section '{yaml_block.section_key}'" + ) + continue + + # Compare with existing + existing_ui = tab_block.ui_content + if _normalize_whitespace(new_ui) == _normalize_whitespace(existing_ui): + stats["unchanged"] += 1 + if verbose: + stats["warnings"].append( + f" Line {tab_block.line_start}: Unchanged" + ) + continue + + stats["regenerated"] += 1 + + new_full = wrap_with_config_tabs( + new_ui, yaml_block.raw, yaml_block.highlight + ) + replacements.append( + (tab_block.line_start, tab_block.line_end, new_full) + ) + + if dry_run or verbose: + print(f"\n{'='*60}") + print(f"File: {filepath}, line {tab_block.line_start}") + print(f"Section: {yaml_block.section_key}") + print(f"{'='*60}") + _print_diff(existing_ui, new_ui, filepath, tab_block.line_start) + + # Apply replacements + if not dry_run and replacements: + lines = content.split("\n") + for start, end, replacement in reversed(replacements): + block_start = start - 1 # 0-based index of line + block_end = end - 1 # 0-based index of line + replacement_lines = replacement.split("\n") + lines[block_start : block_end + 1] = replacement_lines + + new_content = "\n".join(lines) + target = outpath or filepath + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(new_content) + print( + f" Regenerated {len(replacements)} ConfigTabs block(s) in {target}", + file=sys.stderr, + ) + elif outpath is not None: + outpath.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(filepath, outpath) + + return stats + + +def check_file( + filepath: Path, + schema: dict, + i18n: dict, + section_configs: dict, + verbose: bool = False, +) -> dict: + """Check for drift between existing UI tabs and what would be generated. + + Returns: + Stats dict with drift info. Non-zero "drifted" means the file is stale. + """ + content = filepath.read_text() + tab_blocks = extract_config_tabs_blocks(content) + + stats = { + "file": str(filepath), + "total_blocks": len(tab_blocks), + "up_to_date": 0, + "drifted": 0, + "skipped": 0, + "warnings": [], + } + + if not tab_blocks: + return stats + + for tab_block in tab_blocks: + yaml_block = tab_block.yaml_block + + if yaml_block.section_key is None or ( + yaml_block.section_key not in ALL_CONFIG_SECTIONS + and not yaml_block.is_camera_level + ): + stats["skipped"] += 1 + continue + + new_ui = generate_ui_content( + yaml_block, schema, i18n, section_configs + ) + + if new_ui is None: + stats["skipped"] += 1 + continue + + existing_ui = tab_block.ui_content + if _normalize_whitespace(new_ui) == _normalize_whitespace(existing_ui): + stats["up_to_date"] += 1 + else: + stats["drifted"] += 1 + print(f"\n{'='*60}") + print(f"DRIFT: {filepath}, line {tab_block.line_start}") + print(f"Section: {yaml_block.section_key}") + print(f"{'='*60}") + _print_diff(existing_ui, new_ui, filepath, tab_block.line_start) + + return stats + + +def _normalize_whitespace(text: str) -> str: + """Normalize whitespace for comparison (strip lines, collapse blanks).""" + lines = [line.rstrip() for line in text.strip().splitlines()] + # Collapse multiple blank lines into one + result: list[str] = [] + prev_blank = False + for line in lines: + if line == "": + if not prev_blank: + result.append(line) + prev_blank = True + else: + result.append(line) + prev_blank = False + return "\n".join(result) + + +def _print_diff(existing: str, generated: str, filepath: Path, line: int): + """Print a unified diff between existing and generated UI content.""" + existing_lines = existing.strip().splitlines(keepends=True) + generated_lines = generated.strip().splitlines(keepends=True) + + diff = difflib.unified_diff( + existing_lines, + generated_lines, + fromfile=f"{filepath}:{line} (existing)", + tofile=f"{filepath}:{line} (generated)", + lineterm="", + ) + diff_text = "\n".join(diff) + if diff_text: + print(diff_text) + else: + print(" (whitespace-only difference)") + + +def _ensure_imports(content: str) -> str: + """Ensure ConfigTabs/TabItem/NavPath imports are present in the file.""" + lines = content.split("\n") + + needed_imports = [] + if "" in content and 'import ConfigTabs' not in content: + needed_imports.append( + 'import ConfigTabs from "@site/src/components/ConfigTabs";' + ) + if "outpath mapping + file_outpaths: dict[Path, Path | None] = {} + for f in files: + if outdir is not None: + try: + rel = f.resolve().relative_to(base_dir) + except ValueError: + rel = Path(f.name) + file_outpaths[f] = outdir / rel + else: + file_outpaths[f] = None + + # Load data sources + print("Loading schema from Pydantic models...", file=sys.stderr) + schema = load_schema() + print("Loading i18n translations...", file=sys.stderr) + i18n = load_i18n() + print("Loading section configs...", file=sys.stderr) + section_configs = load_section_configs() + print(f"Processing {len(files)} file(s)...\n", file=sys.stderr) + + if args.check: + _run_check(files, schema, i18n, section_configs, args.verbose) + elif args.regenerate: + _run_regenerate( + files, schema, i18n, section_configs, + args.dry_run, args.verbose, file_outpaths, + ) + else: + _run_inject( + files, schema, i18n, section_configs, + args.inject, args.verbose, file_outpaths, + ) + + if outdir is not None: + print(f"\nOutput written to: {outdir}", file=sys.stderr) + + +def _run_inject(files, schema, i18n, section_configs, inject, verbose, file_outpaths): + """Run default mode: preview or inject bare YAML blocks.""" + total_stats = { + "files": 0, + "total_blocks": 0, + "config_blocks": 0, + "already_wrapped": 0, + "generated": 0, + "skipped": 0, + } + + for filepath in files: + stats = process_file( + filepath, schema, i18n, section_configs, + inject=inject, verbose=verbose, + outpath=file_outpaths.get(filepath), + ) + + total_stats["files"] += 1 + for key in ["total_blocks", "config_blocks", "already_wrapped", + "generated", "skipped"]: + total_stats[key] += stats[key] + + if verbose and stats["warnings"]: + print(f"\n{filepath}:", file=sys.stderr) + for w in stats["warnings"]: + print(w, file=sys.stderr) + + print("\n" + "=" * 60, file=sys.stderr) + print("Summary:", file=sys.stderr) + print(f" Files processed: {total_stats['files']}", file=sys.stderr) + print(f" Total YAML blocks: {total_stats['total_blocks']}", file=sys.stderr) + print(f" Config blocks: {total_stats['config_blocks']}", file=sys.stderr) + print(f" Already wrapped: {total_stats['already_wrapped']}", file=sys.stderr) + print(f" Generated: {total_stats['generated']}", file=sys.stderr) + print(f" Skipped: {total_stats['skipped']}", file=sys.stderr) + print("=" * 60, file=sys.stderr) + + +def _run_regenerate(files, schema, i18n, section_configs, dry_run, verbose, file_outpaths): + """Run regenerate mode: update existing ConfigTabs blocks.""" + total_stats = { + "files": 0, + "total_blocks": 0, + "regenerated": 0, + "unchanged": 0, + "skipped": 0, + } + + for filepath in files: + stats = regenerate_file( + filepath, schema, i18n, section_configs, + dry_run=dry_run, verbose=verbose, + outpath=file_outpaths.get(filepath), + ) + + total_stats["files"] += 1 + for key in ["total_blocks", "regenerated", "unchanged", "skipped"]: + total_stats[key] += stats[key] + + if verbose and stats["warnings"]: + print(f"\n{filepath}:", file=sys.stderr) + for w in stats["warnings"]: + print(w, file=sys.stderr) + + action = "Would regenerate" if dry_run else "Regenerated" + print("\n" + "=" * 60, file=sys.stderr) + print("Summary:", file=sys.stderr) + print(f" Files processed: {total_stats['files']}", file=sys.stderr) + print(f" ConfigTabs blocks: {total_stats['total_blocks']}", file=sys.stderr) + print(f" {action}: {total_stats['regenerated']}", file=sys.stderr) + print(f" Unchanged: {total_stats['unchanged']}", file=sys.stderr) + print(f" Skipped: {total_stats['skipped']}", file=sys.stderr) + print("=" * 60, file=sys.stderr) + + +def _run_check(files, schema, i18n, section_configs, verbose): + """Run check mode: detect drift without modifying files.""" + total_stats = { + "files": 0, + "total_blocks": 0, + "up_to_date": 0, + "drifted": 0, + "skipped": 0, + } + + for filepath in files: + stats = check_file( + filepath, schema, i18n, section_configs, verbose=verbose, + ) + + total_stats["files"] += 1 + for key in ["total_blocks", "up_to_date", "drifted", "skipped"]: + total_stats[key] += stats[key] + + print("\n" + "=" * 60, file=sys.stderr) + print("Summary:", file=sys.stderr) + print(f" Files processed: {total_stats['files']}", file=sys.stderr) + print(f" ConfigTabs blocks: {total_stats['total_blocks']}", file=sys.stderr) + print(f" Up to date: {total_stats['up_to_date']}", file=sys.stderr) + print(f" Drifted: {total_stats['drifted']}", file=sys.stderr) + print(f" Skipped: {total_stats['skipped']}", file=sys.stderr) + print("=" * 60, file=sys.stderr) + + if total_stats["drifted"] > 0: + print( + f"\n{total_stats['drifted']} block(s) have drifted from schema/i18n. " + "Run with --regenerate to update.", + file=sys.stderr, + ) + sys.exit(1) + else: + print("\nAll UI tabs are up to date.", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/docs/scripts/lib/__init__.py b/docs/scripts/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/scripts/lib/i18n_loader.py b/docs/scripts/lib/i18n_loader.py new file mode 100644 index 0000000..7416e86 --- /dev/null +++ b/docs/scripts/lib/i18n_loader.py @@ -0,0 +1,139 @@ +"""Load i18n translation files for Settings UI field labels.""" + +import json +from pathlib import Path +from typing import Any + +# Base path for locale files +WEB_LOCALES = Path(__file__).resolve().parents[3] / "web" / "public" / "locales" / "en" + + +def load_i18n() -> dict[str, Any]: + """Load and merge all relevant i18n files. + + Returns: + Dict with keys: "global", "cameras", "settings_menu" + """ + global_path = WEB_LOCALES / "config" / "global.json" + cameras_path = WEB_LOCALES / "config" / "cameras.json" + settings_path = WEB_LOCALES / "views" / "settings.json" + + result: dict[str, Any] = {} + + with open(global_path) as f: + result["global"] = json.load(f) + + with open(cameras_path) as f: + result["cameras"] = json.load(f) + + with open(settings_path) as f: + settings = json.load(f) + result["settings_menu"] = settings.get("menu", {}) + + # Build a unified enum value → label lookup from all known sources. + # Merges multiple maps so callers don't need to know which file + # a particular enum lives in. + value_labels: dict[str, str] = {} + + config_form = settings.get("configForm", {}) + + # FFmpeg preset labels (preset-vaapi → "VAAPI (Intel/AMD GPU)") + value_labels.update( + config_form.get("ffmpegArgs", {}).get("presetLabels", {}) + ) + + # Timestamp position (tl → "Top left") + value_labels.update(settings.get("timestampPosition", {})) + + # Input role options (detect → "Detect") + value_labels.update( + config_form.get("inputRoles", {}).get("options", {}) + ) + + # GenAI role options (vision → "Vision") + value_labels.update( + config_form.get("genaiRoles", {}).get("options", {}) + ) + + result["value_labels"] = value_labels + + return result + + +def get_field_label( + i18n: dict[str, Any], + section_key: str, + field_path: list[str], + level: str = "global", +) -> str | None: + """Look up the UI label for a field. + + Args: + i18n: Loaded i18n data from load_i18n() + section_key: Config section (e.g., "record") + field_path: Path within section (e.g., ["continuous", "days"]) + level: "global" or "cameras" + + Returns: + The label string, or None if not found. + """ + source = i18n.get(level, {}) + node = source.get(section_key, {}) + + for key in field_path: + if not isinstance(node, dict): + return None + node = node.get(key, {}) + + if isinstance(node, dict): + return node.get("label") + return None + + +def get_field_description( + i18n: dict[str, Any], + section_key: str, + field_path: list[str], + level: str = "global", +) -> str | None: + """Look up the UI description for a field.""" + source = i18n.get(level, {}) + node = source.get(section_key, {}) + + for key in field_path: + if not isinstance(node, dict): + return None + node = node.get(key, {}) + + if isinstance(node, dict): + return node.get("description") + return None + + +def get_value_label( + i18n: dict[str, Any], + value: str, +) -> str | None: + """Look up the display label for an enum/option value. + + Args: + i18n: Loaded i18n data from load_i18n() + value: The raw config value (e.g., "preset-vaapi", "tl") + + Returns: + The human-readable label (e.g., "VAAPI (Intel/AMD GPU)"), or None. + """ + return i18n.get("value_labels", {}).get(value) + + +def get_section_label( + i18n: dict[str, Any], + section_key: str, + level: str = "global", +) -> str | None: + """Get the top-level label for a config section.""" + source = i18n.get(level, {}) + section = source.get(section_key, {}) + if isinstance(section, dict): + return section.get("label") + return None diff --git a/docs/scripts/lib/nav_map.py b/docs/scripts/lib/nav_map.py new file mode 100644 index 0000000..0fddf40 --- /dev/null +++ b/docs/scripts/lib/nav_map.py @@ -0,0 +1,120 @@ +"""Map config section keys to Settings UI navigation paths.""" + +# Derived from web/src/pages/Settings.tsx section mappings +# and web/public/locales/en/views/settings.json menu labels. +# +# Format: section_key -> (group_label, page_label) +# Navigation path: "Settings > {group_label} > {page_label}" + +GLOBAL_NAV: dict[str, tuple[str, str]] = { + "detect": ("Global configuration", "Object detection"), + "ffmpeg": ("Global configuration", "FFmpeg"), + "record": ("Global configuration", "Recording"), + "snapshots": ("Global configuration", "Snapshots"), + "motion": ("Global configuration", "Motion detection"), + "objects": ("Global configuration", "Objects"), + "review": ("Global configuration", "Review"), + "audio": ("Global configuration", "Audio events"), + "live": ("Global configuration", "Live playback"), + "timestamp_style": ("Global configuration", "Timestamp style"), + "notifications": ("Notifications", "Notifications"), +} + +CAMERA_NAV: dict[str, tuple[str, str]] = { + "detect": ("Camera configuration", "Object detection"), + "ffmpeg": ("Camera configuration", "FFmpeg"), + "record": ("Camera configuration", "Recording"), + "snapshots": ("Camera configuration", "Snapshots"), + "motion": ("Camera configuration", "Motion detection"), + "objects": ("Camera configuration", "Objects"), + "review": ("Camera configuration", "Review"), + "audio": ("Camera configuration", "Audio events"), + "audio_transcription": ("Camera configuration", "Audio transcription"), + "notifications": ("Camera configuration", "Notifications"), + "live": ("Camera configuration", "Live playback"), + "birdseye": ("Camera configuration", "Birdseye"), + "face_recognition": ("Camera configuration", "Face recognition"), + "lpr": ("Camera configuration", "License plate recognition"), + "mqtt": ("Camera configuration", "MQTT"), + "onvif": ("Camera configuration", "ONVIF"), + "ui": ("Camera configuration", "Camera UI"), + "timestamp_style": ("Camera configuration", "Timestamp style"), +} + +ENRICHMENT_NAV: dict[str, tuple[str, str]] = { + "semantic_search": ("Enrichments", "Semantic search"), + "genai": ("Enrichments", "Generative AI"), + "face_recognition": ("Enrichments", "Face recognition"), + "lpr": ("Enrichments", "License plate recognition"), + "classification": ("Enrichments", "Object classification"), + "audio_transcription": ("Enrichments", "Audio transcription"), +} + +SYSTEM_NAV: dict[str, tuple[str, str]] = { + "go2rtc_streams": ("System", "go2rtc streams"), + "database": ("System", "Database"), + "mqtt": ("System", "MQTT"), + "tls": ("System", "TLS"), + "auth": ("System", "Authentication"), + "networking": ("System", "Networking"), + "proxy": ("System", "Proxy"), + "ui": ("System", "UI"), + "logger": ("System", "Logging"), + "environment_vars": ("System", "Environment variables"), + "telemetry": ("System", "Telemetry"), + "birdseye": ("System", "Birdseye"), + "detectors": ("System", "Detectors and model"), + "model": ("System", "Detectors and model"), +} + +# All known top-level config section keys +ALL_CONFIG_SECTIONS = ( + set(GLOBAL_NAV) + | set(CAMERA_NAV) + | set(ENRICHMENT_NAV) + | set(SYSTEM_NAV) + | {"cameras"} +) + + +def get_nav_path(section_key: str, level: str = "global") -> str | None: + """Get the full navigation path for a config section. + + Args: + section_key: Config section key (e.g., "record") + level: "global", "camera", "enrichment", or "system" + + Returns: + NavPath string like "Settings > Global configuration > Recording", + or None if not found. + """ + nav_tables = { + "global": GLOBAL_NAV, + "camera": CAMERA_NAV, + "enrichment": ENRICHMENT_NAV, + "system": SYSTEM_NAV, + } + + table = nav_tables.get(level) + if table is None: + return None + + entry = table.get(section_key) + if entry is None: + return None + + group, page = entry + return f"Settings > {group} > {page}" + + +def detect_level(section_key: str) -> str: + """Detect whether a config section is global, camera, enrichment, or system.""" + if section_key in SYSTEM_NAV: + return "system" + if section_key in ENRICHMENT_NAV: + return "enrichment" + if section_key in GLOBAL_NAV: + return "global" + if section_key in CAMERA_NAV: + return "camera" + return "global" diff --git a/docs/scripts/lib/schema_loader.py b/docs/scripts/lib/schema_loader.py new file mode 100644 index 0000000..a1e88a9 --- /dev/null +++ b/docs/scripts/lib/schema_loader.py @@ -0,0 +1,88 @@ +"""Load JSON schema from Frigate's Pydantic config models.""" + +from typing import Any + + +def load_schema() -> dict[str, Any]: + """Generate and return the full JSON schema for FrigateConfig.""" + from frigate.config.config import FrigateConfig + from frigate.util.schema import get_config_schema + + return get_config_schema(FrigateConfig) + + +def resolve_ref(schema: dict[str, Any], ref: str) -> dict[str, Any]: + """Resolve a $ref pointer within the schema.""" + # ref format: "#/$defs/RecordConfig" + parts = ref.lstrip("#/").split("/") + node = schema + for part in parts: + node = node[part] + return node + + +def resolve_schema_node( + schema: dict[str, Any], node: dict[str, Any] +) -> dict[str, Any]: + """Resolve a schema node, following $ref and allOf if present.""" + if "$ref" in node: + node = resolve_ref(schema, node["$ref"]) + if "allOf" in node: + merged: dict[str, Any] = {} + for item in node["allOf"]: + resolved = resolve_schema_node(schema, item) + merged.update(resolved) + return merged + return node + + +def get_section_schema( + schema: dict[str, Any], section_key: str +) -> dict[str, Any] | None: + """Get the resolved schema for a top-level config section.""" + props = schema.get("properties", {}) + if section_key not in props: + return None + return resolve_schema_node(schema, props[section_key]) + + +def get_field_info( + schema: dict[str, Any], section_key: str, field_path: list[str] +) -> dict[str, Any] | None: + """Get schema info for a specific field path within a section. + + Args: + schema: Full JSON schema + section_key: Top-level section (e.g., "record") + field_path: List of nested keys (e.g., ["continuous", "days"]) + + Returns: + Resolved schema node for the field, or None if not found. + """ + section = get_section_schema(schema, section_key) + if section is None: + return None + + node = section + for key in field_path: + props = node.get("properties", {}) + if key not in props: + return None + node = resolve_schema_node(schema, props[key]) + + return node + + +def is_boolean_field(field_schema: dict[str, Any]) -> bool: + """Check if a schema node represents a boolean field.""" + return field_schema.get("type") == "boolean" + + +def is_enum_field(field_schema: dict[str, Any]) -> bool: + """Check if a schema node is an enum.""" + return "enum" in field_schema + + +def is_object_field(field_schema: dict[str, Any]) -> bool: + """Check if a schema node is an object with properties.""" + return field_schema.get("type") == "object" or "properties" in field_schema diff --git a/docs/scripts/lib/section_config_parser.py b/docs/scripts/lib/section_config_parser.py new file mode 100644 index 0000000..805ab21 --- /dev/null +++ b/docs/scripts/lib/section_config_parser.py @@ -0,0 +1,130 @@ +"""Parse TypeScript section config files for hidden/advanced field info.""" + +import json +import re +from pathlib import Path +from typing import Any + +SECTION_CONFIGS_DIR = ( + Path(__file__).resolve().parents[3] + / "web" + / "src" + / "components" + / "config-form" + / "section-configs" +) + + +def _extract_string_array(text: str, field_name: str) -> list[str]: + """Extract a string array value from TypeScript object literal text.""" + pattern = rf"{field_name}\s*:\s*\[(.*?)\]" + match = re.search(pattern, text, re.DOTALL) + if not match: + return [] + content = match.group(1) + return re.findall(r'"([^"]*)"', content) + + +def _parse_section_file(filepath: Path) -> dict[str, Any]: + """Parse a single section config .ts file.""" + text = filepath.read_text() + + # Extract base block + base_match = re.search(r"base\s*:\s*\{(.*?)\n \}", text, re.DOTALL) + base_text = base_match.group(1) if base_match else "" + + # Extract global block + global_match = re.search(r"global\s*:\s*\{(.*?)\n \}", text, re.DOTALL) + global_text = global_match.group(1) if global_match else "" + + # Extract camera block + camera_match = re.search(r"camera\s*:\s*\{(.*?)\n \}", text, re.DOTALL) + camera_text = camera_match.group(1) if camera_match else "" + + result: dict[str, Any] = { + "fieldOrder": _extract_string_array(base_text, "fieldOrder"), + "hiddenFields": _extract_string_array(base_text, "hiddenFields"), + "advancedFields": _extract_string_array(base_text, "advancedFields"), + } + + # Merge global-level hidden fields + global_hidden = _extract_string_array(global_text, "hiddenFields") + if global_hidden: + result["globalHiddenFields"] = global_hidden + + # Merge camera-level hidden fields + camera_hidden = _extract_string_array(camera_text, "hiddenFields") + if camera_hidden: + result["cameraHiddenFields"] = camera_hidden + + return result + + +def load_section_configs() -> dict[str, dict[str, Any]]: + """Load all section configs from TypeScript files. + + Returns: + Dict mapping section name to parsed config. + """ + # Read sectionConfigs.ts to get the mapping of section keys to filenames + registry_path = SECTION_CONFIGS_DIR.parent / "sectionConfigs.ts" + registry_text = registry_path.read_text() + + configs: dict[str, dict[str, Any]] = {} + + for ts_file in SECTION_CONFIGS_DIR.glob("*.ts"): + if ts_file.name == "types.ts": + continue + + section_name = ts_file.stem + configs[section_name] = _parse_section_file(ts_file) + + # Map section config keys from the registry (handles renames like + # "timestamp_style: timestampStyle") + key_map: dict[str, str] = {} + for match in re.finditer( + r"(\w+)(?:\s*:\s*\w+)?\s*,", registry_text[registry_text.find("{") :] + ): + key = match.group(1) + key_map[key] = key + + # Handle explicit key mappings like `timestamp_style: timestampStyle` + for match in re.finditer(r"(\w+)\s*:\s*(\w+)\s*,", registry_text): + key_map[match.group(1)] = match.group(2) + + return configs + + +def get_hidden_fields( + configs: dict[str, dict[str, Any]], + section_key: str, + level: str = "global", +) -> set[str]: + """Get the set of hidden fields for a section at a given level. + + Args: + configs: Loaded section configs + section_key: Config section name (e.g., "record") + level: "global" or "camera" + + Returns: + Set of hidden field paths (e.g., {"enabled_in_config", "sync_recordings"}) + """ + config = configs.get(section_key, {}) + hidden = set(config.get("hiddenFields", [])) + + if level == "global": + hidden.update(config.get("globalHiddenFields", [])) + elif level == "camera": + hidden.update(config.get("cameraHiddenFields", [])) + + return hidden + + +def get_advanced_fields( + configs: dict[str, dict[str, Any]], + section_key: str, +) -> set[str]: + """Get the set of advanced fields for a section.""" + config = configs.get(section_key, {}) + return set(config.get("advancedFields", [])) diff --git a/docs/scripts/lib/ui_generator.py b/docs/scripts/lib/ui_generator.py new file mode 100644 index 0000000..7b9a592 --- /dev/null +++ b/docs/scripts/lib/ui_generator.py @@ -0,0 +1,283 @@ +"""Generate UI tab markdown content from parsed YAML blocks.""" + +from typing import Any + +from .i18n_loader import get_field_description, get_field_label, get_value_label +from .nav_map import ALL_CONFIG_SECTIONS, detect_level, get_nav_path +from .schema_loader import is_boolean_field, is_object_field +from .section_config_parser import get_hidden_fields +from .yaml_extractor import YamlBlock, get_leaf_paths + + +def _format_value( + value: object, + field_schema: dict[str, Any] | None, + i18n: dict[str, Any] | None = None, +) -> str: + """Format a YAML value for UI display. + + Looks up i18n labels for enum/option values when available. + """ + if field_schema and is_boolean_field(field_schema): + return "on" if value else "off" + if isinstance(value, bool): + return "on" if value else "off" + if isinstance(value, list): + if len(value) == 0: + return "an empty list" + items = [] + for v in value: + label = get_value_label(i18n, str(v)) if i18n else None + items.append(f"`{label}`" if label else f"`{v}`") + return ", ".join(items) + if value is None: + return "empty" + + # Try i18n label for the raw value (enum translations) + if i18n and isinstance(value, str): + label = get_value_label(i18n, value) + if label: + return f"`{label}`" + + return f"`{value}`" + + +def _build_field_label( + i18n: dict[str, Any], + section_key: str, + field_path: list[str], + level: str, +) -> str: + """Build the display label for a field using i18n labels. + + For a path like ["continuous", "days"], produces + "Continuous retention > Retention days" using the actual i18n labels. + """ + parts: list[str] = [] + + for depth in range(len(field_path)): + sub_path = field_path[: depth + 1] + label = get_field_label(i18n, section_key, sub_path, level) + + if label: + parts.append(label) + else: + # Fallback to title-cased field name + parts.append(field_path[depth].replace("_", " ").title()) + + return " > ".join(parts) + + +def _is_hidden( + field_key: str, + full_path: list[str], + hidden_fields: set[str], +) -> bool: + """Check if a field should be hidden from UI output.""" + # Check exact match + if field_key in hidden_fields: + return True + + # Check dotted path match (e.g., "alerts.enabled_in_config") + dotted = ".".join(str(p) for p in full_path) + if dotted in hidden_fields: + return True + + # Check wildcard patterns (e.g., "filters.*.mask") + for pattern in hidden_fields: + if "*" in pattern: + parts = pattern.split(".") + if len(parts) == len(full_path): + match = all( + p == "*" or p == fp for p, fp in zip(parts, full_path) + ) + if match: + return True + + return False + + +def generate_ui_content( + block: YamlBlock, + schema: dict[str, Any], + i18n: dict[str, Any], + section_configs: dict[str, dict[str, Any]], +) -> str | None: + """Generate UI tab markdown content for a YAML block. + + Args: + block: Parsed YAML block from a doc file + schema: Full JSON schema + i18n: Loaded i18n translations + section_configs: Parsed section config data + + Returns: + Generated markdown string for the UI tab, or None if the block + can't be converted (not a config block, etc.) + """ + if block.section_key is None: + return None + + # Determine which config data to walk + if block.is_camera_level: + # Camera-level: unwrap cameras.{name}.{section} + cam_data = block.parsed.get("cameras", {}) + cam_name = block.camera_name or next(iter(cam_data), None) + if not cam_name: + return None + inner = cam_data.get(cam_name, {}) + if not isinstance(inner, dict): + return None + level = "camera" + else: + inner = block.parsed + # Determine level from section key + level = detect_level(block.section_key) + + # Collect sections to process (may span multiple top-level keys) + sections_to_process: list[tuple[str, dict]] = [] + for key in inner: + if key in ALL_CONFIG_SECTIONS or key == block.section_key: + val = inner[key] + if isinstance(val, dict): + sections_to_process.append((key, val)) + else: + # Simple scalar at section level (e.g., record.enabled = True) + sections_to_process.append((key, {key: val})) + + # If inner is the section itself (e.g., parsed = {"record": {...}}) + if not sections_to_process and block.section_key in inner: + section_data = inner[block.section_key] + if isinstance(section_data, dict): + sections_to_process = [(block.section_key, section_data)] + + if not sections_to_process: + # Try treating the whole inner dict as the section data + sections_to_process = [(block.section_key, inner)] + + # Choose pattern based on whether YAML has comments (descriptive) or values + use_table = block.has_comments + + lines: list[str] = [] + step_num = 1 + + for section_key, section_data in sections_to_process: + # Get navigation path + i18n_level = "cameras" if level == "camera" else "global" + nav_path = get_nav_path(section_key, level) + if nav_path is None: + # Try global as fallback + nav_path = get_nav_path(section_key, "global") + if nav_path is None: + continue + + # Get hidden fields for this section + hidden = get_hidden_fields(section_configs, section_key, level) + + # Get leaf paths from the YAML data + leaves = get_leaf_paths(section_data) + + # Filter out hidden fields + visible_leaves: list[tuple[tuple[str, ...], object]] = [] + for path, value in leaves: + path_list = list(path) + if not _is_hidden(path_list[-1], path_list, hidden): + visible_leaves.append((path, value)) + + if not visible_leaves: + continue + + if use_table: + # Pattern A: Field table with descriptions + lines.append( + f'Navigate to .' + ) + lines.append("") + lines.append("| Field | Description |") + lines.append("|-------|-------------|") + + for path, _value in visible_leaves: + path_list = list(path) + label = _build_field_label( + i18n, section_key, path_list, i18n_level + ) + desc = get_field_description( + i18n, section_key, path_list, i18n_level + ) + if not desc: + desc = "" + lines.append(f"| **{label}** | {desc} |") + else: + # Pattern B: Set instructions + multi_section = len(sections_to_process) > 1 + + if multi_section: + camera_note = "" + if block.is_camera_level: + camera_note = ( + " and select your camera" + ) + lines.append( + f'{step_num}. Navigate to {camera_note}.' + ) + else: + if block.is_camera_level: + lines.append( + f'1. Navigate to and select your camera.' + ) + else: + lines.append( + f'Navigate to .' + ) + lines.append("") + + from .schema_loader import get_field_info + + for path, value in visible_leaves: + path_list = list(path) + label = _build_field_label( + i18n, section_key, path_list, i18n_level + ) + field_info = get_field_info(schema, section_key, path_list) + formatted = _format_value(value, field_info, i18n) + + if multi_section or block.is_camera_level: + lines.append(f" - Set **{label}** to {formatted}") + else: + lines.append(f"- Set **{label}** to {formatted}") + + step_num += 1 + + if not lines: + return None + + return "\n".join(lines) + + +def wrap_with_config_tabs(ui_content: str, yaml_raw: str, highlight: str | None = None) -> str: + """Wrap UI content and YAML in ConfigTabs markup. + + Args: + ui_content: Generated UI tab markdown + yaml_raw: Original YAML text + highlight: Optional highlight spec (e.g., "{3-4}") + + Returns: + Full ConfigTabs MDX block + """ + highlight_str = f" {highlight}" if highlight else "" + + return f""" + + +{ui_content} + + + + +```yaml{highlight_str} +{yaml_raw} +``` + + +""" diff --git a/docs/scripts/lib/yaml_extractor.py b/docs/scripts/lib/yaml_extractor.py new file mode 100644 index 0000000..c01451c --- /dev/null +++ b/docs/scripts/lib/yaml_extractor.py @@ -0,0 +1,283 @@ +"""Extract YAML code blocks from markdown documentation files.""" + +import re +from dataclasses import dataclass, field + +import yaml + + +@dataclass +class YamlBlock: + """A YAML code block extracted from a markdown file.""" + + raw: str # Original YAML text + parsed: dict # Parsed YAML content + line_start: int # Line number in the markdown file (1-based) + line_end: int # End line number + highlight: str | None = None # Highlight spec (e.g., "{3-4}") + has_comments: bool = False # Whether the YAML has inline comments + inside_config_tabs: bool = False # Already wrapped in ConfigTabs + section_key: str | None = None # Detected top-level config section + is_camera_level: bool = False # Whether this is camera-level config + camera_name: str | None = None # Camera name if camera-level + config_keys: list[str] = field( + default_factory=list + ) # Top-level keys in the YAML + + +def extract_yaml_blocks(content: str) -> list[YamlBlock]: + """Extract all YAML fenced code blocks from markdown content. + + Args: + content: Markdown file content + + Returns: + List of YamlBlock instances + """ + blocks: list[YamlBlock] = [] + lines = content.split("\n") + i = 0 + in_config_tabs = False + + while i < len(lines): + line = lines[i] + + # Track ConfigTabs context + if "" in line: + in_config_tabs = True + elif "" in line: + in_config_tabs = False + + # Look for YAML fence opening + fence_match = re.match(r"^```yaml\s*(\{[^}]*\})?\s*$", line) + if fence_match: + highlight = fence_match.group(1) + start_line = i + 1 # 1-based + yaml_lines: list[str] = [] + i += 1 + + # Collect until closing fence + while i < len(lines) and not lines[i].startswith("```"): + yaml_lines.append(lines[i]) + i += 1 + + end_line = i + 1 # 1-based, inclusive of closing fence + raw = "\n".join(yaml_lines) + + # Check for inline comments + has_comments = any( + re.search(r"#\s*(<-|[A-Za-z])", yl) for yl in yaml_lines + ) + + # Parse YAML + try: + parsed = yaml.safe_load(raw) + except yaml.YAMLError: + i += 1 + continue + + if not isinstance(parsed, dict): + i += 1 + continue + + # Detect config section and level + config_keys = list(parsed.keys()) + section_key = None + is_camera = False + camera_name = None + + if "cameras" in parsed and isinstance(parsed["cameras"], dict): + is_camera = True + cam_entries = parsed["cameras"] + if len(cam_entries) == 1: + camera_name = list(cam_entries.keys())[0] + inner = cam_entries[camera_name] + if isinstance(inner, dict): + inner_keys = list(inner.keys()) + if len(inner_keys) >= 1: + section_key = inner_keys[0] + elif len(config_keys) >= 1: + section_key = config_keys[0] + + blocks.append( + YamlBlock( + raw=raw, + parsed=parsed, + line_start=start_line, + line_end=end_line, + highlight=highlight, + has_comments=has_comments, + inside_config_tabs=in_config_tabs, + section_key=section_key, + is_camera_level=is_camera, + camera_name=camera_name, + config_keys=config_keys, + ) + ) + + i += 1 + + return blocks + + +@dataclass +class ConfigTabsBlock: + """An existing ConfigTabs block in a markdown file.""" + + line_start: int # 1-based line of + line_end: int # 1-based line of + ui_content: str # Content inside the UI TabItem + yaml_block: YamlBlock # The YAML block inside the YAML TabItem + raw_text: str # Full raw text of the ConfigTabs block + + +def extract_config_tabs_blocks(content: str) -> list[ConfigTabsBlock]: + """Extract existing ConfigTabs blocks from markdown content. + + Parses the structure: + + + ...ui content... + + + ```yaml + ...yaml... + ``` + + + + Returns: + List of ConfigTabsBlock instances + """ + blocks: list[ConfigTabsBlock] = [] + lines = content.split("\n") + i = 0 + + while i < len(lines): + if "" not in lines[i]: + i += 1 + continue + + block_start = i # 0-based + + # Find + j = i + 1 + while j < len(lines) and "" not in lines[j]: + j += 1 + + if j >= len(lines): + i += 1 + continue + + block_end = j # 0-based, line with + block_text = "\n".join(lines[block_start : block_end + 1]) + + # Extract UI content (between and ) + ui_match = re.search( + r'\s*\n(.*?)\n\s*', + block_text, + re.DOTALL, + ) + ui_content = ui_match.group(1).strip() if ui_match else "" + + # Extract YAML block from inside the yaml TabItem + yaml_tab_match = re.search( + r'\s*\n(.*?)\n\s*', + block_text, + re.DOTALL, + ) + + yaml_block = None + if yaml_tab_match: + yaml_tab_text = yaml_tab_match.group(1) + fence_match = re.search( + r"```yaml\s*(\{[^}]*\})?\s*\n(.*?)\n```", + yaml_tab_text, + re.DOTALL, + ) + if fence_match: + highlight = fence_match.group(1) + yaml_raw = fence_match.group(2) + has_comments = bool( + re.search(r"#\s*(<-|[A-Za-z])", yaml_raw) + ) + + try: + parsed = yaml.safe_load(yaml_raw) + except yaml.YAMLError: + parsed = {} + + if isinstance(parsed, dict): + config_keys = list(parsed.keys()) + section_key = None + is_camera = False + camera_name = None + + if "cameras" in parsed and isinstance( + parsed["cameras"], dict + ): + is_camera = True + cam_entries = parsed["cameras"] + if len(cam_entries) == 1: + camera_name = list(cam_entries.keys())[0] + inner = cam_entries[camera_name] + if isinstance(inner, dict): + inner_keys = list(inner.keys()) + if len(inner_keys) >= 1: + section_key = inner_keys[0] + elif len(config_keys) >= 1: + section_key = config_keys[0] + + yaml_block = YamlBlock( + raw=yaml_raw, + parsed=parsed, + line_start=block_start + 1, + line_end=block_end + 1, + highlight=highlight, + has_comments=has_comments, + inside_config_tabs=True, + section_key=section_key, + is_camera_level=is_camera, + camera_name=camera_name, + config_keys=config_keys, + ) + + if yaml_block: + blocks.append( + ConfigTabsBlock( + line_start=block_start + 1, # 1-based + line_end=block_end + 1, # 1-based + ui_content=ui_content, + yaml_block=yaml_block, + raw_text=block_text, + ) + ) + + i = j + 1 + + return blocks + + +def get_leaf_paths( + data: dict, prefix: tuple[str, ...] = () +) -> list[tuple[tuple[str, ...], object]]: + """Walk a parsed YAML dict and return all leaf key paths with values. + + Args: + data: Parsed YAML dict + prefix: Current key path prefix + + Returns: + List of (key_path_tuple, value) pairs. + e.g., [( ("record", "continuous", "days"), 3 ), ...] + """ + results: list[tuple[tuple[str, ...], object]] = [] + + for key, value in data.items(): + path = prefix + (str(key),) + if isinstance(value, dict): + results.extend(get_leaf_paths(value, path)) + else: + results.append((path, value)) + + return results diff --git a/docs/sidebars.ts b/docs/sidebars.ts new file mode 100644 index 0000000..ce6c41c --- /dev/null +++ b/docs/sidebars.ts @@ -0,0 +1,199 @@ +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; +import { PropSidebarItemLink } from "@docusaurus/plugin-content-docs"; +import frigateHttpApiSidebar from "./docs/integrations/api/sidebar"; + +const sidebars: SidebarsConfig = { + docs: { + Frigate: [ + "frigate/index", + "frigate/hardware", + "frigate/planning_setup", + "frigate/installation", + "frigate/updating", + "frigate/camera_setup", + "frigate/video_pipeline", + "frigate/network_requirements", + "frigate/glossary", + ], + Guides: [ + "guides/getting_started", + "guides/ha_notifications", + "guides/ha_network_storage", + "guides/reverse_proxy", + ], + Usage: [ + "usage/live", + "usage/review", + "usage/history", + "usage/explore", + "usage/exports", + ], + Configuration: [ + "configuration/config", + { + type: "category", + label: "Detectors", + items: [ + "configuration/object_detectors", + "configuration/audio_detectors", + ], + }, + { + type: "category", + label: "Enrichments", + items: [ + "configuration/semantic_search", + "configuration/face_recognition", + "configuration/license_plate_recognition", + "configuration/bird_classification", + { + type: "category", + label: "Custom Classification", + link: { + type: "generated-index", + title: "Custom Classification", + description: "Configuration for custom classification models", + }, + items: [ + "configuration/custom_classification/state_classification", + "configuration/custom_classification/object_classification", + ], + }, + { + type: "category", + label: "Generative AI", + link: { + type: "generated-index", + title: "Generative AI", + description: "Generative AI Features", + }, + items: [ + "configuration/genai/genai_config", + "configuration/genai/genai_review", + "configuration/genai/genai_objects", + ], + }, + ], + }, + { + type: "category", + label: "Cameras", + items: [ + "configuration/cameras", + "configuration/review", + "configuration/record", + "configuration/snapshots", + "configuration/motion_detection", + "configuration/birdseye", + "configuration/live", + "configuration/restream", + "configuration/autotracking", + "configuration/camera_specific", + ], + }, + { + type: "category", + label: "Objects", + items: [ + "configuration/object_filters", + "configuration/masks", + "configuration/zones", + "configuration/objects", + "configuration/stationary_objects", + ], + }, + { + type: "category", + label: "Hardware Acceleration", + items: [ + "configuration/hardware_acceleration_video", + "configuration/hardware_acceleration_enrichments", + ], + }, + { + type: "category", + label: "Extra Configuration", + items: [ + "configuration/authentication", + "configuration/notifications", + "configuration/profiles", + "configuration/go2rtc", + "configuration/ffmpeg_presets", + "configuration/pwa", + "configuration/tls", + ], + }, + { + type: "category", + label: "Advanced Configuration", + items: [ + "configuration/advanced/system", + "configuration/advanced/reference", + { + type: "link", + label: "Go2RTC Configuration Reference", + href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.14#configuration", + } as PropSidebarItemLink, + ], + }, + ], + Integrations: [ + "integrations/plus", + "integrations/home-assistant", + // This is the HTTP API generated by OpenAPI + { + type: "category", + label: "HTTP API", + link: { + type: "generated-index", + title: "Frigate HTTP API", + description: "HTTP API", + slug: "/integrations/api/frigate-http-api", + }, + items: frigateHttpApiSidebar, + }, + "integrations/mqtt", + "integrations/homekit", + "configuration/metrics", + "integrations/third_party_extensions", + ], + "Frigate+": [ + "plus/index", + "plus/annotating", + "plus/first_model", + "plus/faq", + ], + Troubleshooting: [ + "troubleshooting/faqs", + "troubleshooting/go2rtc", + "troubleshooting/recordings", + "troubleshooting/dummy-camera", + { + type: "category", + label: "Troubleshooting Hardware", + link: { + type: "generated-index", + title: "Troubleshooting Hardware", + description: "Troubleshooting Problems with Hardware", + }, + items: ["troubleshooting/gpu", "troubleshooting/edgetpu"], + }, + { + type: "category", + label: "Troubleshooting Resource Usage", + link: { + type: "generated-index", + title: "Troubleshooting Resource Usage", + description: "Troubleshooting issues with resource usage", + }, + items: ["troubleshooting/cpu", "troubleshooting/memory"], + }, + ], + Development: [ + "development/contributing", + "development/contributing-boards", + ], + }, +}; + +export default sidebars; diff --git a/docs/src/components/CommunityBadge/index.jsx b/docs/src/components/CommunityBadge/index.jsx new file mode 100644 index 0000000..67b9a9e --- /dev/null +++ b/docs/src/components/CommunityBadge/index.jsx @@ -0,0 +1,23 @@ +import React from "react"; + +export default function CommunityBadge() { + return ( + + Community Supported + + ); +} diff --git a/docs/src/components/ConfigTabs/index.jsx b/docs/src/components/ConfigTabs/index.jsx new file mode 100644 index 0000000..0fbc518 --- /dev/null +++ b/docs/src/components/ConfigTabs/index.jsx @@ -0,0 +1,34 @@ +import React, { Children, cloneElement } from "react"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +export default function ConfigTabs({ children }) { + const wrapped = Children.map(children, (child) => { + if (child?.props?.value === "ui") { + return cloneElement(child, { + className: "config-tab-ui", + }); + } + if (child?.props?.value === "yaml") { + return cloneElement(child, { + className: "config-tab-yaml", + }); + } + return child; + }); + + return ( +
+ + {wrapped} + +
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx b/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx new file mode 100644 index 0000000..b8a8a8f --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx @@ -0,0 +1,108 @@ +import React from "react"; +import Admonition from "@theme/Admonition"; +import DeviceSelector from "./components/DeviceSelector"; +import HardwareOptions from "./components/HardwareOptions"; +import PortConfigSection from "./components/PortConfig"; +import StoragePaths from "./components/StoragePaths"; +import NvidiaGpuConfig from "./components/NvidiaGpuConfig"; +import OtherOptions from "./components/OtherOptions"; +import GeneratedOutput from "./components/GeneratedOutput"; +import { useConfigGenerator } from "./hooks/useConfigGenerator"; +import styles from "./styles.module.css"; + +/** + * Simple markdown-link-to-React renderer for help text. + * Only supports [text](url) syntax — no nested brackets. + */ +function renderHelpText(text: string): React.ReactNode { + const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g); + return parts.map((part, i) => { + const match = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + if (match) { + return ( + + {match[1]} + + ); + } + return {part}; + }); +} + +export default function DockerComposeGenerator() { + const { + deviceId, device, hardwareEnabled, + portEnabled, + nvidiaGpuCount, nvidiaGpuDeviceId, + configPath, mediaPath, rtspPassword, timezone, shmSize, + shmSizeError, gpuDeviceIdError, configPathError, mediaPathError, + hasAnyHardware, generatedYaml, + selectDevice, toggleHardware, togglePort, + handleShmSizeChange, handleConfigPathChange, handleMediaPathChange, + handleNvidiaGpuCountChange, handleNvidiaGpuDeviceIdChange, + setRtspPassword, setTimezone, isHardwareDisabled, + } = useConfigGenerator(); + + return ( +
+
+ + + {device.helpText && ( + + {renderHelpText(device.helpText)} + + )} + + {device.needsNvidiaConfig && ( + + )} + + + + + + + + + + +
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx b/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx new file mode 100644 index 0000000..ddad160 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx @@ -0,0 +1,147 @@ +import React from "react"; +import { useColorMode } from "@docusaurus/theme-common"; +import { devices } from "../config"; +import type { DeviceConfig } from "../config"; +import styles from "../styles.module.css"; + +interface Props { + selectedId: string; + onSelect: (id: string) => void; +} + +/** + * Determine the icon type from the icon string: + * - Starts with " tag. + */ +function hasBackgroundProps(style: React.CSSProperties | undefined): boolean { + if (!style) return false; + return Object.keys(style).some((key) => { + const k = key.toLowerCase().replace(/-/g, ""); + return k === "backgroundsize" || k === "backgroundposition" || k === "backgroundrepeat" || k === "backgroundimage"; + }); +} + +/** + * Convert a style object to CSS custom properties (e.g. { width: "24px" } → { "--svg-width": "24px" }) + * so they can be consumed by CSS rules targeting child elements like . + */ +function toCssVars(style: React.CSSProperties | undefined, prefix: string): React.CSSProperties { + if (!style) return {}; + const vars: Record = {}; + for (const [key, value] of Object.entries(style)) { + const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase(); + vars[`--${prefix}-${cssKey}`] = value; + } + return vars as React.CSSProperties; +} + +function DeviceIcon({ device }: { device: DeviceConfig }) { + const { isDarkTheme } = useColorMode(); + const iconStr = isDarkTheme && device.iconDark ? device.iconDark : device.icon; + const iconStyle = (isDarkTheme && device.iconDarkStyle + ? device.iconDarkStyle + : device.iconStyle) as React.CSSProperties | undefined; + const svgStyle = (isDarkTheme && device.svgDarkStyle + ? device.svgDarkStyle + : device.svgStyle) as React.CSSProperties | undefined; + + const iconType = getIconType(iconStr); + + if (iconType === "svg") { + return ( +
+ ); + } + + if (iconType === "image") { + // When iconStyle contains background-* properties, render as background-image + // on the container div instead of an tag, enabling background-size/position control. + if (hasBackgroundProps(iconStyle)) { + return ( +
+ ); + } + return ( +
+ {device.name} +
+ ); + } + + return ( +
+ {iconStr} +
+ ); +} + +function DeviceCard({ + device, + active, + onClick, +}: { + device: DeviceConfig; + active: boolean; + onClick: () => void; +}) { + return ( +
{ + if (e.key === "Enter" || e.key === " ") onClick(); + }} + > + +
{device.name}
+
{device.description}
+
+ ); +} + +export default function DeviceSelector({ selectedId, onSelect }: Props) { + return ( +
+

Device Type

+
+ {devices.map((d) => ( + onSelect(d.id)} + /> + ))} +
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/GeneratedOutput.tsx b/docs/src/components/DockerComposeGenerator/components/GeneratedOutput.tsx new file mode 100644 index 0000000..f170637 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/GeneratedOutput.tsx @@ -0,0 +1,60 @@ +import React, { useState, useCallback } from "react"; +import CodeBlock from "@theme/CodeBlock"; +import Admonition from "@theme/Admonition"; +import styles from "../styles.module.css"; + +interface Props { + yaml: string; + configPath: string; + mediaPath: string; + hasAnyHardware: boolean; + deviceId: string; +} + +export default function GeneratedOutput({ + yaml, + configPath, + mediaPath, + hasAnyHardware, + deviceId, +}: Props) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(yaml).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, [yaml]); + + return ( +
+
+

Generated Configuration

+ +
+ + {!configPath && ( + +

You haven't specified a config file directory. You may want to modify the default path.

+
+ )} + {!mediaPath && ( + +

You haven't specified a recording storage directory. You may want to modify the default path.

+
+ )} + {deviceId === "stable" && !hasAnyHardware && ( + +

You haven't selected any hardware acceleration. Please check if you have supported hardware available.

+
+ )} + + + {yaml} + +
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx b/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx new file mode 100644 index 0000000..9c261ed --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { hardwareOptions } from "../config"; +import type { HardwareOption } from "../config"; +import styles from "../styles.module.css"; + +interface Props { + deviceId: string; + hardwareEnabled: Record; + onToggle: (hwId: string) => void; + isDisabled: (hwId: string) => boolean; +} + +function renderDescription(text: string): React.ReactNode { + const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g); + return parts.map((part, i) => { + const match = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + if (match) { + return {match[1]}; + } + return {part}; + }); +} + +function HardwareCheckbox({ + hw, disabled, checked, onToggle, +}: { + hw: HardwareOption; disabled: boolean; checked: boolean; onToggle: () => void; +}) { + return ( +
+ + {checked && hw.description && ( +
{renderDescription(hw.description)}
+ )} +
+ ); +} + +export default function HardwareOptions({ deviceId, hardwareEnabled, onToggle, isDisabled }: Props) { + return ( +
+

Generic Hardware Devices

+ {deviceId !== "stable" && ( +

+ Some options have been auto-configured based on your device type. +

+ )} +
+ {hardwareOptions.map((hw) => { + const disabled = isDisabled(hw.id); + const checked = disabled ? false : !!hardwareEnabled[hw.id]; + return ( + onToggle(hw.id)} /> + ); + })} +
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/NvidiaGpuConfig.tsx b/docs/src/components/DockerComposeGenerator/components/NvidiaGpuConfig.tsx new file mode 100644 index 0000000..9c9be5e --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/NvidiaGpuConfig.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import styles from "../styles.module.css"; + +interface Props { + gpuCount: string; + gpuDeviceId: string; + gpuDeviceIdError: boolean; + onGpuCountChange: (value: string) => void; + onGpuDeviceIdChange: (value: string) => void; +} + +export default function NvidiaGpuConfig({ + gpuCount, + gpuDeviceId, + gpuDeviceIdError, + onGpuCountChange, + onGpuDeviceIdChange, +}: Props) { + const showDeviceId = gpuCount !== ""; + + return ( +
+
+ + onGpuCountChange(e.target.value.replace(/\D/g, ""))} + /> +
+ {showDeviceId && ( +
+ + onGpuDeviceIdChange(e.target.value)} + /> + {gpuDeviceIdError ? ( +

+ ⚠️ GPU device IDs are required when GPU count is a number +

+ ) : ( +

+ Single GPU: 0  |  Multiple GPUs: 0,1,2 +

+ )} +
+ )} +
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/OtherOptions.tsx b/docs/src/components/DockerComposeGenerator/components/OtherOptions.tsx new file mode 100644 index 0000000..8d1efef --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/OtherOptions.tsx @@ -0,0 +1,122 @@ +import React, { useMemo } from "react"; +import CodeInline from "@theme/CodeInline"; +import styles from "../styles.module.css"; + +const AUTO_TIMEZONE_VALUE = "__auto__"; + +function getTimezoneList(): string[] { + if (typeof Intl !== "undefined") { + const intl = Intl as typeof Intl & { + supportedValuesOf?: (key: string) => string[]; + }; + const supported = intl.supportedValuesOf?.("timeZone"); + if (supported && supported.length > 0) { + return [...supported].sort(); + } + } + + const fallback = Intl.DateTimeFormat().resolvedOptions().timeZone; + return fallback ? [fallback] : ["UTC"]; +} + +interface Props { + rtspPassword: string; + timezone: string; + shmSize: string; + shmSizeError: boolean; + onRtspPasswordChange: (value: string) => void; + onTimezoneChange: (value: string) => void; + onShmSizeChange: (value: string) => void; +} + +export default function OtherOptions({ + rtspPassword, + timezone, + shmSize, + shmSizeError, + onRtspPasswordChange, + onTimezoneChange, + onShmSizeChange, +}: Props) { + const timezones = useMemo(() => getTimezoneList(), []); + const systemTimezone = + Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC"; + const selectedValue = timezone || AUTO_TIMEZONE_VALUE; + + return ( +
+

Other Options

+
+
+ + +
+
+ + onShmSizeChange(e.target.value)} + /> + {shmSizeError ? ( +

+ ⚠️ Invalid format. Use a number followed by a unit (e.g. 512mb, 1gb) +

+ ) : ( +

+ See{" "} + + calculating required SHM size + {" "} + for the correct value. +

+ )} +
+
+ + onRtspPasswordChange(e.target.value)} + /> +

+ Optional. You can specify{" "} + {"{FRIGATE_RTSP_PASSWORD}"}{" "} + in the config file to reference camera stream passwords. This is NOT + the Frigate login password. +

+
+
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/PortConfig.tsx b/docs/src/components/DockerComposeGenerator/components/PortConfig.tsx new file mode 100644 index 0000000..c4e5acf --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/PortConfig.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import Admonition from "@theme/Admonition"; +import { ports } from "../config"; +import styles from "../styles.module.css"; + +interface Props { + portEnabled: Record; + onTogglePort: (portId: string) => void; +} + +function PortItem({ + port, + enabled, + onToggle, +}: { + port: typeof ports[number]; + enabled: boolean; + onToggle: () => void; +}) { + const showWarning = port.warningContent && ( + port.warningWhen === "checked" ? enabled : + port.warningWhen === "unchecked" ? !enabled : enabled + ); + + return ( +
+ + {port.description && ( +
{port.description}
+ )} + {showWarning && ( + + {port.warningContent} + + )} +
+ ); +} + +export default function PortConfigSection({ + portEnabled, + onTogglePort, +}: Props) { + return ( +
+

Port Configuration

+
+ {ports.map((port) => ( + onTogglePort(port.id)} + /> + ))} +
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/StoragePaths.tsx b/docs/src/components/DockerComposeGenerator/components/StoragePaths.tsx new file mode 100644 index 0000000..1e20189 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/StoragePaths.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import styles from "../styles.module.css"; + +interface Props { + configPath: string; + mediaPath: string; + configPathError: boolean; + mediaPathError: boolean; + onConfigPathChange: (value: string) => void; + onMediaPathChange: (value: string) => void; +} + +export default function StoragePaths({ + configPath, + mediaPath, + configPathError, + mediaPathError, + onConfigPathChange, + onMediaPathChange, +}: Props) { + return ( +
+

Storage Paths

+
+
+ + onConfigPathChange(e.target.value)} + /> + {configPathError && ( +

+ ⚠️ Path contains invalid characters. Only letters, numbers, + underscores, hyphens, slashes, and dots are allowed. +

+ )} +
+
+ + onMediaPathChange(e.target.value)} + /> + {mediaPathError && ( +

+ ⚠️ Path contains invalid characters. Only letters, numbers, + underscores, hyphens, slashes, and dots are allowed. +

+ )} +
+
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/config/config.yaml b/docs/src/components/DockerComposeGenerator/config/config.yaml new file mode 100644 index 0000000..42199ff --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/config/config.yaml @@ -0,0 +1,297 @@ +# Unified configuration for Docker Compose Generator +# This file defines all devices, hardware options, and ports for Frigate Docker Compose generation + +devices: + - id: "stable" + name: "Standard x86_64" + description: "Generic PC / server" + icon: "💻" + imageTag: "stable" + autoHardware: [] + + - id: "intel" + name: "Intel Device" + description: "Intel GPU / NPU" + icon: '' + imageTag: "stable" + autoHardware: + - "gpu" + - "intelNpu" + helpText: "Intel Device automatically configures /dev/dri and /dev/accel device mappings." + helpType: "info" + + - id: "stable-tensorrt" + name: "NVIDIA GPU" + description: "NVIDIA acceleration" + icon: '' + svgStyle: + width: 50px + height: 50px + iconStyle: + padding-bottom: 15px + imageTag: "stable-tensorrt" + autoHardware: [] + helpText: "Requires the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker) to be installed. GPU deploy resources are configured automatically." + helpType: "warning" + needsNvidiaConfig: true + + - id: "stable-tensorrt-jp6" + name: "NVIDIA Jetson" + description: "Jetson development board" + icon: '' + svgStyle: + width: 50px + height: 50px + iconStyle: + padding-bottom: 15px + imageTag: "stable-tensorrt-jp6" + autoHardware: [] + helpText: "NVIDIA Jetson devices automatically configure runtime: nvidia." + helpType: "info" + runtime: "nvidia" + + - id: "stable-rocm" + name: "AMD GPU" + description: "ROCm acceleration" + icon: "https://www.amd.com/content/dam/code/images/header/amd-header-logo.svg" + iconStyle: + filter: invert(1) + background-repeat: no-repeat + background-position: right center + background-size: 338% 90% + iconDark: "https://www.amd.com/content/dam/code/images/header/amd-header-logo.svg" + iconDarkStyle: + filter: invert(0) + background-repeat: no-repeat + background-position: right center + background-size: 338% 90% + imageTag: "stable-rocm" + autoHardware: + - "gpu" + helpText: "AMD GPU automatically configures LIBVA_DRIVER_NAME environment variable and /dev/dri device mapping." + helpType: "info" + env: + LIBVA_DRIVER_NAME: "radeonsi" + + - id: "apple-silicon" + name: "Apple Silicon" + description: "Mac M-series processor" + icon: '' + svgStyle: + width: 90px + height: 90px + svgDarkStyle: + width: 90px + height: 90px + fill: white + imageTag: "stable" + imageTagSuffix: "-standard-arm64" + autoHardware: [] + helpText: "Apple Silicon (M-series) requires an [external detector](/configuration/object_detectors#apple-silicon-detector) running on the host." + helpType: "warning" + extraHosts: + - "host.docker.internal:host-gateway" + + - id: "raspberry-pi" + name: "Raspberry Pi" + description: "ARM device" + icon: '' + svgStyle: + width: 40px + height: 40px + transform: translateX(-3px) + imageTag: "stable" + imageTagSuffix: "-standard-arm64" + autoHardware: + - "video11" + helpText: "Raspberry Pi automatically configures the video11 device (RPi 4) and uses the arm64 image." + helpType: "info" + + - id: "stable-rk" + name: "Rockchip" + description: "Rockchip SoC board" + icon: "https://www.rock-chips.com/favicon.ico" + imageTag: "stable-rk" + autoHardware: + - "gpu" + helpText: "Rockchip devices automatically configure /dev/dri device mapping." + helpType: "info" + devices: + - host: "/dev/dma_heap" + comment: "Rockchip DMA heap" + - host: "/dev/rga" + comment: "Rockchip RGA" + - host: "/dev/mpp_service" + comment: "Rockchip MPP service" + volumes: + - host: "/sys/" + container: "/sys/" + readOnly: true + comment: "Rockchip system info" + securityOpt: + - "apparmor=unconfined" + - "systempaths=unconfined" + + - id: "stable-synaptics" + name: "Synaptics" + description: "Synaptics NPU" + icon: "🔷" + imageTag: "stable-synaptics" + autoHardware: [] + helpText: "Synaptics devices automatically configure /dev/synap and video devices." + helpType: "info" + devices: + - host: "/dev/synap" + comment: "Synaptics NPU" + - host: "/dev/video0" + comment: "Video device 0" + - host: "/dev/video1" + comment: "Video device 1" + +hardware: + - id: "usbCoral" + label: "USB Coral (TPU)" + description: "Enable this if you have a Google Coral USB TPU. Other Coral versions require different device paths." + disabledWhen: + - "apple-silicon" + - "stable-synaptics" + devices: + - host: "/dev/bus/usb" + container: "/dev/bus/usb" + comment: "USB Coral — modify for other versions" + + - id: "pcieCoral" + label: "PCIe Coral (TPU)" + description: "Enable this if you have a Google Coral PCIe/M.2 TPU. You also need to [install the driver](https://github.com/jnicolson/gasket-builder)." + disabledWhen: + - "apple-silicon" + - "stable-synaptics" + devices: + - host: "/dev/apex_0" + container: "/dev/apex_0" + comment: "PCIe Coral — follow driver instructions at https://github.com/jnicolson/gasket-builder" + + - id: "gpu" + label: "Intel/AMD GPU (/dev/dri)" + description: "Pass through /dev/dri for GPU hardware acceleration (Intel/AMD)." + disabledWhen: + - "stable-tensorrt-jp6" + - "apple-silicon" + devices: + - host: "/dev/dri" + container: "/dev/dri" + comment: "Intel/AMD GPU hardware acceleration" + + - id: "intelNpu" + label: "Intel NPU (/dev/accel)" + description: "Pass through /dev/accel for Intel NPU acceleration." + disabledWhen: + - "stable-tensorrt-jp6" + - "apple-silicon" + - "stable-rocm" + - "stable-rk" + - "stable-synaptics" + devices: + - host: "/dev/accel" + container: "/dev/accel" + comment: "Intel NPU" + + - id: "hailo" + label: "Hailo NPU (/dev/hailo0)" + description: "Pass through /dev/hailo0 for Hailo-8 / Hailo-8L NPU acceleration. You also need to [install the driver](#hailo-8)." + disabledWhen: + - "apple-silicon" + - "stable-synaptics" + devices: + - host: "/dev/hailo0" + comment: "Hailo NPU" + + - id: "memryx" + label: "MemryX MX3 (/dev/memx0)" + description: "Pass through /dev/memx0 for MemryX MX3 NPU acceleration. You also need to [install the driver](#memryx-mx3)." + disabledWhen: + - "apple-silicon" + - "stable-synaptics" + devices: + - host: "/dev/memx0" + comment: "MemryX MX3 NPU" + volumes: + - host: "/run/mxa_manager" + container: "/run/mxa_manager" + comment: "MemryX manager" + + - id: "axera" + label: "AXERA Accelerator" + description: "Pass through AXERA accelerator devices. Requires the [AXCL driver](#axera) to be installed first." + disabledWhen: + - "apple-silicon" + - "stable-synaptics" + devices: + - host: "/dev/axcl_host" + comment: "AXERA accelerator device" + - host: "/dev/ax_mmb_dev" + comment: "AXERA MMB device" + - host: "/dev/msg_userdev" + comment: "AXERA message device" + volumes: + - host: "/usr/bin/axcl" + container: "/usr/bin/axcl" + comment: "AXERA binaries" + - host: "/usr/lib/axcl" + container: "/usr/lib/axcl" + comment: "AXERA libraries" + + - id: "video11" + label: "Raspberry Pi (/dev/video11)" + description: "Pass through /dev/video11 for Raspberry Pi 4B hardware acceleration." + disabledWhen: + - "stable-tensorrt" + - "stable-tensorrt-jp6" + - "stable-rocm" + - "stable-rk" + - "stable-synaptics" + - "intel" + - "apple-silicon" + - "stable" + devices: + - host: "/dev/video11" + container: "/dev/video11" + comment: "Raspberry Pi 4B" + +ports: + - id: "8971" + host: 8971 + container: 8971 + protocol: "tcp" + description: "Authenticated UI and API access (default HTTPS)" + defaultEnabled: true + warningContent: "This is the access port for Frigate. Closing it means you will no longer be able to access the instance." + warningWhen: "unchecked" + + - id: "8554" + host: 8554 + container: 8554 + protocol: "tcp" + description: "Access RTSP feeds from go2rtc" + defaultEnabled: true + + - id: "8555-tcp" + host: 8555 + container: 8555 + protocol: "tcp" + description: "WebRTC over TCP" + defaultEnabled: true + + - id: "8555-udp" + host: 8555 + container: 8555 + protocol: "udp" + description: "WebRTC over UDP" + defaultEnabled: true + + - id: "1984" + host: 1984 + container: 1984 + protocol: "tcp" + description: "Go2RTC Web UI" + defaultEnabled: false diff --git a/docs/src/components/DockerComposeGenerator/config/index.ts b/docs/src/components/DockerComposeGenerator/config/index.ts new file mode 100644 index 0000000..5acaba9 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/config/index.ts @@ -0,0 +1,12 @@ +export { devices, deviceMap } from "./devices"; +export { hardwareOptions, hardwareMap } from "./hardware"; +export { ports, portMap } from "./ports"; + +export type { + DeviceConfig, + DeviceMapping, + VolumeMapping, + HardwareOption, + PortConfig, + NvidiaDeployConfig, +} from "./types"; diff --git a/docs/src/components/DockerComposeGenerator/config/types.ts b/docs/src/components/DockerComposeGenerator/config/types.ts new file mode 100644 index 0000000..87bcb60 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/config/types.ts @@ -0,0 +1,154 @@ +/** + * Type definitions for the Docker Compose Generator configuration. + * All device, hardware, and port options are declaratively defined + * so that adding a new device only requires editing config files. + */ + +/** A single device mapping entry (e.g. /dev/dri:/dev/dri) */ +export interface DeviceMapping { + /** Host device path */ + host: string; + /** Container device path (defaults to host if omitted) */ + container?: string; + /** Inline comment for this device line */ + comment?: string; +} + +/** A single volume mapping entry */ +export interface VolumeMapping { + /** Host path */ + host: string; + /** Container path */ + container: string; + /** Whether the mount is read-only */ + readOnly?: boolean; + /** Inline comment */ + comment?: string; +} + +/** NVIDIA deploy configuration for docker-compose */ +export interface NvidiaDeployConfig { + /** "all" or a specific number */ + count: string; + /** Specific GPU device IDs (when count is a number) */ + deviceIds?: string[]; +} + +/** Full device type definition */ +export interface DeviceConfig { + /** Unique identifier, e.g. "intel" */ + id: string; + /** Display name, e.g. "Intel GPU" */ + name: string; + /** Short description */ + description: string; + /** + * Icon for the device card. Supports: + * - Emoji string (e.g. "🖥️") + * - Image URL or static path (e.g. "/img/intel.svg", "https://example.com/icon.png") + * - Inline SVG markup (e.g. "...") + */ + icon: string; + /** + * Additional CSS properties applied to the icon element. + * - For image-type icons: if any `background-*` property (e.g. `background-size`, + * `background-position`) is present, the image is rendered as a CSS `background-image` + * on the container div, enabling full background positioning control. + * Otherwise the image is rendered as an `` tag and styles apply to it. + * - For emoji/SVG icons: styles apply to the container div. + */ + iconStyle?: Record; + /** + * Additional CSS properties applied directly to the inner `` element + * when the icon is an inline SVG. Use this to override the default + * `width: 100%; height: 100%` or set `fill`, `transform`, etc. + * Ignored for emoji and image-type icons. + */ + svgStyle?: Record; + /** + * Icon for dark mode. Same format as `icon`. When provided, this icon + * replaces `icon` when the user is in dark mode. + */ + iconDark?: string; + /** Additional CSS properties for the dark mode icon container */ + iconDarkStyle?: Record; + /** + * SVG-specific styles for dark mode. Same as `svgStyle` but applied + * when dark mode is active. Merged over `svgStyle` in dark mode. + */ + svgDarkStyle?: Record; + /** Docker image tag, e.g. "stable" */ + imageTag: string; + /** + * Image tag suffix appended to the base tag. + * e.g. "-standard-arm64" produces "stable-standard-arm64" + */ + imageTagSuffix?: string; + /** Hardware option IDs to auto-enable when this device is selected */ + autoHardware: string[]; + /** Help text shown as an admonition when this device is selected */ + helpText?: string; + /** Admonition type for help text */ + helpType?: "info" | "warning" | "danger"; + /** Device mappings always added for this device type */ + devices?: DeviceMapping[]; + /** Volume mappings always added for this device type */ + volumes?: VolumeMapping[]; + /** Extra environment variables for this device type */ + env?: Record; + /** NVIDIA deploy config (only for tensorrt) */ + nvidiaDeploy?: NvidiaDeployConfig; + /** Runtime setting, e.g. "nvidia" for Jetson */ + runtime?: string; + /** Extra hosts entries, e.g. "host.docker.internal:host-gateway" */ + extraHosts?: string[]; + /** Security options, e.g. ["apparmor=unconfined"] */ + securityOpt?: string[]; + /** Whether this device type needs the NVIDIA GPU config UI */ + needsNvidiaConfig?: boolean; +} + +/** Generic hardware acceleration option definition */ +export interface HardwareOption { + /** Unique identifier, e.g. "usbCoral" */ + id: string; + /** Display label */ + label: string; + /** + * Description shown below the checkbox when this option is enabled. + * Supports markdown link syntax: [text](url) + */ + description?: string; + /** Device IDs that disable this option */ + disabledWhen?: string[]; + /** Device mappings added when this option is enabled */ + devices?: DeviceMapping[]; + /** Volume mappings added when this option is enabled */ + volumes?: VolumeMapping[]; + /** Extra environment variables */ + env?: Record; +} + +/** Port definition */ +export interface PortConfig { + /** Unique identifier (also the default host port as string) */ + id: string; + /** Host port number */ + host: number; + /** Container port number */ + container: number; + /** Protocol */ + protocol?: "tcp" | "udp"; + /** Description of the port's purpose */ + description: string; + /** Whether enabled by default */ + defaultEnabled: boolean; + /** Whether this port is locked (always enabled, cannot be toggled off) */ + locked?: boolean; + /** Admonition type for the warning */ + warningType?: "warning" | "danger"; + /** Warning content (markdown) */ + warningContent?: string; + /** When to show the warning: when the port is checked or unchecked */ + warningWhen?: "checked" | "unchecked"; +} diff --git a/docs/src/components/DockerComposeGenerator/generator/index.ts b/docs/src/components/DockerComposeGenerator/generator/index.ts new file mode 100644 index 0000000..f6091f7 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/generator/index.ts @@ -0,0 +1,250 @@ +import type { + DeviceConfig, + DeviceMapping, + VolumeMapping, +} from "../config/types"; +import { hardwareMap } from "../config"; + +// --------------------------------------------------------------------------- +// Input type +// --------------------------------------------------------------------------- + +export interface GeneratorInput { + device: DeviceConfig; + selectedHardware: string[]; + enabledPorts: string[]; + configPath: string; + mediaPath: string; + rtspPassword?: string; + timezone: string; + shmSize: string; + nvidiaGpuCount?: string; + nvidiaGpuDeviceId?: string; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function deviceLine(dm: DeviceMapping): string { + const host = dm.host; + const container = dm.container ?? dm.host; + const mapping = host === container ? host : `${host}:${container}`; + const comment = dm.comment ? ` # ${dm.comment}` : ""; + return ` - ${mapping}${comment}`; +} + +function volumeLine(vm: VolumeMapping): string { + const ro = vm.readOnly ? ":ro" : ""; + const comment = vm.comment ? ` # ${vm.comment}` : ""; + return ` - ${vm.host}:${vm.container}${ro}${comment}`; +} + +// --------------------------------------------------------------------------- +// YAML builder — each section returns an array of lines +// --------------------------------------------------------------------------- + +function buildImage(device: DeviceConfig): string[] { + const tag = device.imageTagSuffix + ? `${device.imageTag}${device.imageTagSuffix}` + : device.imageTag; + return [` image: ghcr.io/blakeblackshear/frigate:${tag}`]; +} + +function buildDevices( + device: DeviceConfig, + hwDevices: DeviceMapping[] +): string[] { + const all: DeviceMapping[] = [ + ...(device.devices ?? []), + ...hwDevices, + ]; + if (all.length === 0) return []; + return [ + " devices:", + ...all.map(deviceLine), + ]; +} + +function buildVolumes( + device: DeviceConfig, + hwVolumes: VolumeMapping[], + configPath: string, + mediaPath: string +): string[] { + const all: VolumeMapping[] = [ + ...(device.volumes ?? []), + ...hwVolumes, + ]; + return [ + " volumes:", + " - /etc/localtime:/etc/localtime:ro # Sync host time", + ` - ${configPath}:/config # Config file directory`, + ` - ${mediaPath}:/media/frigate # Recording storage directory`, + " - type: tmpfs # 1GB in-memory filesystem for recording segment storage", + " target: /tmp/cache", + " tmpfs:", + " size: 1000000000", + ...all.map(volumeLine), + ]; +} + +function buildPorts(enabledPorts: string[]): string[] { + return [ + " ports:", + ...enabledPorts, + ]; +} + +function buildEnvironment( + device: DeviceConfig, + hwEnv: Record, + rtspPassword: string | undefined, + timezone: string +): string[] { + const allEnv: Record = { + ...hwEnv, + ...(device.env ?? {}), + }; + + const lines: string[] = [" environment:"]; + + if (rtspPassword) { + lines.push( + ` FRIGATE_RTSP_PASSWORD: "${rtspPassword}" # RTSP password — change to your own` + ); + } + + lines.push(` TZ: "${timezone}" # Timezone`); + + for (const [key, value] of Object.entries(allEnv)) { + lines.push(` ${key}: "${value}"`); + } + + return lines; +} + +function buildDeploy(device: DeviceConfig, input: GeneratorInput): string[] { + if (device.id === "stable-tensorrt") { + const count = input.nvidiaGpuCount || "all"; + const isAll = count === "all"; + const deviceId = input.nvidiaGpuDeviceId?.trim(); + + if (isAll) { + return [ + " deploy:", + " resources:", + " reservations:", + " devices:", + " - driver: nvidia", + " count: all # Use all GPUs", + " capabilities: [gpu]", + ]; + } + + if (deviceId) { + const ids = deviceId + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + .map((s) => `'${s}'`) + .join(", "); + return [ + " deploy:", + " resources:", + " reservations:", + " devices:", + " - driver: nvidia", + ` device_ids: [${ids}] # GPU device IDs`, + ` count: ${count} # GPU count`, + " capabilities: [gpu]", + ]; + } + + return [ + " deploy:", + " resources:", + " reservations:", + " devices:", + " - driver: nvidia", + ` count: ${count} # GPU count`, + " capabilities: [gpu]", + ]; + } + + return []; +} + +function buildRuntime(device: DeviceConfig): string[] { + if (device.runtime) { + return [` runtime: ${device.runtime}`]; + } + return []; +} + +function buildExtraHosts(device: DeviceConfig): string[] { + if (!device.extraHosts?.length) return []; + return [ + " extra_hosts:", + ...device.extraHosts.map( + (h, i) => + ` - "${h}"${i === 0 ? " # Required to talk to the NPU detector" : ""}` + ), + ]; +} + +function buildSecurityOpt(device: DeviceConfig): string[] { + if (!device.securityOpt?.length) return []; + return [ + " security_opt:", + ...device.securityOpt.map((s) => ` - ${s}`), + ]; +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Generate a docker-compose YAML string from the given input. + * The output is pure YAML with inline comments (no Shiki annotations). + */ +export function generateDockerCompose(input: GeneratorInput): string { + const { device } = input; + + // Collect hardware-level devices, volumes, and env + const hwDevices: DeviceMapping[] = []; + const hwVolumes: VolumeMapping[] = []; + const hwEnv: Record = {}; + + for (const hwId of input.selectedHardware) { + const hw = hardwareMap.get(hwId); + if (!hw) continue; + // Skip GPU device mapping for tensorrt images (it uses deploy instead) + if (hw.id === "gpu" && device.imageTag === "stable-tensorrt") continue; + hwDevices.push(...(hw.devices ?? [])); + hwVolumes.push(...(hw.volumes ?? [])); + Object.assign(hwEnv, hw.env ?? {}); + } + + const lines: string[] = [ + "services:", + " frigate:", + " container_name: frigate", + " privileged: true # This may not be necessary for all setups", + " restart: unless-stopped", + " stop_grace_period: 30s # Allow enough time to shut down the various services", + ...buildImage(device), + ` shm_size: "${input.shmSize || "512mb"}" # Update for your cameras based on SHM calculation`, + ...buildRuntime(device), + ...buildDeploy(device, input), + ...buildExtraHosts(device), + ...buildSecurityOpt(device), + ...buildDevices(device, hwDevices), + ...buildVolumes(device, hwVolumes, input.configPath, input.mediaPath), + ...buildPorts(input.enabledPorts), + ...buildEnvironment(device, hwEnv, input.rtspPassword, input.timezone), + ]; + + return lines.join("\n"); +} diff --git a/docs/src/components/DockerComposeGenerator/hooks/useConfigGenerator.ts b/docs/src/components/DockerComposeGenerator/hooks/useConfigGenerator.ts new file mode 100644 index 0000000..19c3976 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/hooks/useConfigGenerator.ts @@ -0,0 +1,195 @@ +import { useState, useCallback, useMemo } from "react"; +import { deviceMap, hardwareMap, portMap } from "../config"; +import { generateDockerCompose } from "../generator"; +import type { GeneratorInput } from "../generator"; + +/** + * Main hook that holds all form state and generates the Docker Compose output. + * Configuration is loaded synchronously from build-time generated .ts files. + */ +export function useConfigGenerator() { + const [deviceId, setDeviceId] = useState("stable"); + + const [hardwareEnabled, setHardwareEnabled] = useState>(() => { + const defaultDevice = deviceMap.get("stable"); + const initial: Record = {}; + if (defaultDevice) { + for (const hwId of defaultDevice.autoHardware) { + initial[hwId] = true; + } + } + return initial; + }); + + const [portEnabled, setPortEnabled] = useState>(() => { + const initial: Record = {}; + for (const p of portMap.values()) { + initial[p.id] = p.defaultEnabled; + } + return initial; + }); + + const [nvidiaGpuCount, setNvidiaGpuCount] = useState(""); + const [nvidiaGpuDeviceId, setNvidiaGpuDeviceId] = useState(""); + const [configPath, setConfigPath] = useState(""); + const [mediaPath, setMediaPath] = useState(""); + const [rtspPassword, setRtspPassword] = useState(""); + const [timezone, setTimezone] = useState(""); + const [shmSize, setShmSize] = useState("512mb"); + const [shmSizeError, setShmSizeError] = useState(false); + const [gpuDeviceIdError, setGpuDeviceIdError] = useState(false); + const [configPathError, setConfigPathError] = useState(false); + const [mediaPathError, setMediaPathError] = useState(false); + + const device = useMemo(() => deviceMap.get(deviceId)!, [deviceId]); + + const selectDevice = useCallback((id: string) => { + const newDevice = deviceMap.get(id); + if (!newDevice) return; + setDeviceId(id); + setHardwareEnabled(() => { + const next: Record = {}; + for (const hwId of newDevice.autoHardware) { + next[hwId] = true; + } + return next; + }); + setNvidiaGpuCount(""); + setNvidiaGpuDeviceId(""); + setGpuDeviceIdError(false); + }, []); + + const toggleHardware = useCallback((hwId: string) => { + setHardwareEnabled((prev) => ({ ...prev, [hwId]: !prev[hwId] })); + }, []); + + const togglePort = useCallback((portId: string) => { + const port = portMap.get(portId); + if (port?.locked) return; + setPortEnabled((prev) => ({ ...prev, [portId]: !prev[portId] })); + }, []); + + const isHardwareDisabled = useCallback( + (hwId: string): boolean => { + const hw = hardwareMap.get(hwId); + if (!hw) return false; + return hw.disabledWhen?.includes(deviceId) ?? false; + }, + [deviceId] + ); + + const validateShmSize = useCallback((value: string): boolean => { + if (!value) return true; + return /^\d+(\.\d+)?[bkmgBKMG]{1,2}$/.test(value); + }, []); + + const validatePath = useCallback((value: string): boolean => { + if (!value) return true; + return /^[a-zA-Z0-9_\-/./]+$/.test(value); + }, []); + + const handleShmSizeChange = useCallback( + (value: string) => { + const filtered = value.replace(/[^0-9.bkmgBKMG]/g, ""); + const valid = validateShmSize(filtered); + setShmSize(filtered); + setShmSizeError(!valid && filtered !== ""); + }, + [validateShmSize] + ); + + const handleConfigPathChange = useCallback( + (value: string) => { + const filtered = value.replace(/[^a-zA-Z0-9_\-/./]/g, ""); + const valid = validatePath(filtered); + setConfigPath(filtered); + setConfigPathError(!valid && filtered !== ""); + }, + [validatePath] + ); + + const handleMediaPathChange = useCallback( + (value: string) => { + const filtered = value.replace(/[^a-zA-Z0-9_\-/./]/g, ""); + const valid = validatePath(filtered); + setMediaPath(filtered); + setMediaPathError(!valid && filtered !== ""); + }, + [validatePath] + ); + + const handleNvidiaGpuCountChange = useCallback((value: string) => { + // Only allow digits + setNvidiaGpuCount(value); + if (value === "") { + setNvidiaGpuDeviceId(""); + setGpuDeviceIdError(false); + } else { + setGpuDeviceIdError(false); + } + }, []); + + const handleNvidiaGpuDeviceIdChange = useCallback((value: string) => { + setNvidiaGpuDeviceId(value.trim()); + setGpuDeviceIdError(false); + }, []); + + const enabledPortLines = useMemo(() => { + const lines: string[] = []; + for (const [id, enabled] of Object.entries(portEnabled)) { + if (!enabled) continue; + const p = portMap.get(id); + if (!p) continue; + const proto = p.protocol && p.protocol !== "tcp" ? `/${p.protocol}` : ""; + const comment = p.description ? ` # ${p.description}` : ""; + lines.push(` - "${p.host}:${p.container}${proto}"${comment}`); + } + return lines; + }, [portEnabled]); + + const selectedHardwareIds = useMemo(() => { + return Object.entries(hardwareEnabled) + .filter(([id, enabled]) => { + if (!enabled) return false; + const hw = hardwareMap.get(id); + if (!hw) return false; + if (hw.disabledWhen?.includes(deviceId)) return false; + return true; + }) + .map(([id]) => id); + }, [hardwareEnabled, deviceId]); + + const generatedYaml = useMemo(() => { + const input: GeneratorInput = { + device, + selectedHardware: selectedHardwareIds, + enabledPorts: enabledPortLines, + configPath: configPath || "/path/to/your/config", + mediaPath: mediaPath || "/path/to/your/storage", + rtspPassword, + timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC", + shmSize: shmSize || "512mb", + nvidiaGpuCount, + nvidiaGpuDeviceId, + }; + return generateDockerCompose(input); + }, [ + device, selectedHardwareIds, enabledPortLines, + configPath, mediaPath, rtspPassword, timezone, shmSize, + nvidiaGpuCount, nvidiaGpuDeviceId, + ]); + + const hasAnyHardware = selectedHardwareIds.length > 0 || !!device?.devices?.length; + + return { + deviceId, device, hardwareEnabled, portEnabled, + nvidiaGpuCount, nvidiaGpuDeviceId, + configPath, mediaPath, rtspPassword, timezone, shmSize, + shmSizeError, gpuDeviceIdError, configPathError, mediaPathError, + hasAnyHardware, generatedYaml, + selectDevice, toggleHardware, togglePort, + handleShmSizeChange, handleConfigPathChange, handleMediaPathChange, + handleNvidiaGpuCountChange, handleNvidiaGpuDeviceIdChange, + setRtspPassword, setTimezone, isHardwareDisabled, + }; +} diff --git a/docs/src/components/DockerComposeGenerator/index.ts b/docs/src/components/DockerComposeGenerator/index.ts new file mode 100644 index 0000000..76dd587 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/index.ts @@ -0,0 +1 @@ +export { default } from "./DockerComposeGenerator"; diff --git a/docs/src/components/DockerComposeGenerator/styles.module.css b/docs/src/components/DockerComposeGenerator/styles.module.css new file mode 100644 index 0000000..d2e1b62 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/styles.module.css @@ -0,0 +1,381 @@ +/* =================================================================== + Docker Compose Generator — styles + Uses Docusaurus / Infima CSS variables for theme compatibility. + =================================================================== */ + +.generator { + margin: 2rem 0; +} + +.card { + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-400); + border-radius: 12px; + padding: 2rem; + box-shadow: var(--ifm-global-shadow-lw); +} + +[data-theme="light"] .card { + background: var(--ifm-color-emphasis-100); + border: 1px solid var(--ifm-color-emphasis-300); +} + +/* --- Form sections --- */ + +.formSection { + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--ifm-color-emphasis-400); +} + +.formSection:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.formSection h4 { + margin: 0 0 1rem 0; + color: var(--ifm-font-color-base); + font-size: 1.1rem; + font-weight: var(--ifm-font-weight-semibold); +} + +/* --- Form controls --- */ + +.formGroup { + margin-bottom: 1rem; +} + +.formGroup:last-child { + margin-bottom: 0; +} + +.label { + display: block; + margin-bottom: 0.25rem; + color: var(--ifm-font-color-base); + font-weight: var(--ifm-font-weight-semibold); + font-size: 0.9rem; +} + +.input { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid var(--ifm-color-emphasis-400); + border-radius: 6px; + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + font-size: 0.95rem; + transition: border-color 0.2s, box-shadow 0.2s; +} + +[data-theme="light"] .input { + background: #fff; + border: 1px solid #d0d7de; +} + +.input:focus { + outline: none; + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest); +} + +[data-theme="dark"] .input { + border-color: var(--ifm-color-emphasis-300); +} + +.inputError { + border-color: #e74c3c; + animation: shake 0.3s ease-in-out; +} + +@keyframes shake { + 0%, + 100% { + transform: translateX(0); + } + 25% { + transform: translateX(-5px); + } + 75% { + transform: translateX(5px); + } +} + +/* --- Select dropdown --- */ + +.select { + cursor: pointer; + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + background: var(--ifm-background-color) + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E") + no-repeat right 0.75rem center / 12px 12px; + padding-right: 2rem; +} + +[data-theme="light"] .select { + background: #fff + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23555' d='M6 8L1 3h10z'/%3E%3C/svg%3E") + no-repeat right 0.75rem center / 12px 12px; +} + +.helpText { + margin: 0.5rem 0 0 0; + font-size: 0.85rem; + color: var(--ifm-font-color-secondary); + line-height: 1.5; +} + +.helpText a { + color: var(--ifm-color-primary); +} + +/* --- Device grid --- */ + +.deviceGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + gap: 0.75rem; + margin-top: 0.5rem; +} + +.deviceCard { + padding: 0.75rem; + border: 2px solid var(--ifm-color-emphasis-400); + border-radius: 12px; + cursor: pointer; + transition: all 0.2s; + text-align: center; + background: var(--ifm-background-color); + display: flex; + flex-direction: column; + align-items: center; +} + +[data-theme="light"] .deviceCard { + border: 2px solid #d0d7de; + background: #fff; +} + +.deviceCard:hover { + border-color: var(--ifm-color-primary); + background: var(--ifm-color-emphasis-100); + transform: translateY(-2px); +} + +.deviceCardActive { + border-color: var(--ifm-color-primary); + background: var(--ifm-color-primary-lightest); + box-shadow: 0 0 0 1px var(--ifm-color-primary); +} + +[data-theme="light"] .deviceCardActive { + background: color-mix(in srgb, var(--ifm-color-primary) 12%, #fff); +} + +[data-theme="dark"] .deviceCardActive { + background: color-mix(in srgb, var(--ifm-color-primary) 25%, #1b1b1b); +} + +[data-theme="dark"] .deviceCardActive .deviceName { + color: var(--ifm-color-primary-light); +} + +[data-theme="dark"] .deviceCardActive .deviceDesc { + color: var(--ifm-color-primary-light); + opacity: 0.85; +} + +.deviceIcon { + font-size: 2rem; + margin-bottom: 0.25rem; + height: 40px; + width: 50px; + display: flex; + align-items: center; + justify-content: center; +} + +.deviceIconSvg { + margin-bottom: 0.25rem; + height: 40px; + width: 50px; + display: flex; + align-items: center; + justify-content: center; + overflow: visible; + /* Allow iconStyle width/height to override */ + flex-shrink: 0; +} + +.deviceIconSvg svg { + width: var(--svg-width, 100%); + height: var(--svg-height, 100%); + fill: var(--svg-fill, currentColor); + transform: var(--svg-transform, none); +} + +.deviceIconImage { + margin-bottom: 0.25rem; + height: 40px; + width: 50px; + display: flex; + align-items: center; + justify-content: center; +} + +.deviceIconImage img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.deviceName { + font-weight: var(--ifm-font-weight-semibold); + color: var(--ifm-font-color-base); + margin-bottom: 0.15rem; + font-size: 0.9rem; +} + +.deviceDesc { + font-size: 0.75rem; + color: var(--ifm-font-color-secondary); + line-height: 1.3; +} + +/* --- Checkbox grid --- */ + +.checkboxGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; +} + +@media (max-width: 576px) { + .checkboxGrid { + grid-template-columns: 1fr; + } +} + +.hardwareItem { + margin-bottom: 0; +} + +.hardwareDescription { + margin: 0.15rem 0 0.4rem 1.6rem; + font-size: 0.8rem; + color: var(--ifm-font-color-secondary); + line-height: 1.5; +} + +.hardwareDescription a { + color: var(--ifm-color-primary); + text-decoration: underline; + text-underline-offset: 2px; +} + +.checkboxLabel { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + padding: 0.4rem 0.5rem; + border-radius: 6px; + transition: background-color 0.2s; + font-size: 0.9rem; +} + +.checkboxLabel:hover { + background: var(--ifm-color-emphasis-100); +} + +.checkboxLabel input[type="checkbox"] { + width: 1.1rem; + height: 1.1rem; + cursor: pointer; + flex-shrink: 0; +} + +.checkboxLabel span { + color: var(--ifm-font-color-base); +} + +.checkboxDisabled { + cursor: not-allowed; +} + +.checkboxDisabled:hover { + background: transparent; +} + +.checkboxDisabled input[type="checkbox"] { + cursor: not-allowed; + opacity: 0.5; +} + +/* --- Form grid (side-by-side) --- */ + +.formGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +@media (max-width: 576px) { + .formGrid { + grid-template-columns: 1fr; + } +} + +.formGrid .formGroup { + margin-bottom: 0; +} + +/* --- Port section --- */ + +.portSection { + margin-bottom: 0.75rem; +} + +.warningBadge { + margin-left: auto; + color: #e67e22; + font-size: 0.85rem; +} + +/* --- NVIDIA config --- */ + +.nvidiaConfig { + margin-top: 1rem; + margin-bottom: 1.5rem; + padding: 1rem; + background: var(--ifm-background-color); + border-radius: 8px; + border-left: 3px solid var(--ifm-color-primary); +} + +[data-theme="light"] .nvidiaConfig { + background: #f6f8fa; + border-left: 3px solid var(--ifm-color-primary); +} + +/* --- Result section --- */ + +.resultSection { + margin-top: 2rem; +} + +.resultHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.resultHeader h4 { + margin: 0; + color: var(--ifm-font-color-base); +} diff --git a/docs/src/components/FaqItem/index.jsx b/docs/src/components/FaqItem/index.jsx new file mode 100644 index 0000000..51d4c07 --- /dev/null +++ b/docs/src/components/FaqItem/index.jsx @@ -0,0 +1,66 @@ +import React, { useState, useEffect } from "react"; +import Heading from "@theme/Heading"; +import styles from "./styles.module.css"; + +// A single FAQ entry. +// +// The question is a real anchored heading (via @theme/Heading), so on desktop +// it gets the standard hover "#" hash link and the answer is always shown. On +// mobile the heading text is a button that toggles its answer, keeping long +// FAQ pages short. The desktop/mobile split is pure CSS (Docusaurus breakpoint: +// 996px), so there is no hydration flash. The answer is always rendered into +// the DOM, so search engines and the docs AI bot can read it regardless of +// layout or collapsed state. The heading id resolves deep links on both layouts +// and auto-expands the entry on mobile when it is the link target. +export default function FaqItem({ id, question, children }) { + const [open, setOpen] = useState(false); + + useEffect(() => { + const openIfTargeted = () => { + if (window.location.hash === `#${id}`) { + setOpen(true); + } + }; + openIfTargeted(); + window.addEventListener("hashchange", openIfTargeted); + return () => window.removeEventListener("hashchange", openIfTargeted); + }, [id]); + + const toggle = () => { + const next = !open; + setOpen(next); + // Reflect the entry in the URL like clicking the heading anchor, so an + // opened answer is shareable. Use replaceState to avoid history spam and + // an abrupt scroll. Clear it on close if it currently points here. + if (next) { + if (window.location.hash !== `#${id}`) { + window.history.replaceState(null, "", `#${id}`); + } + } else if (window.location.hash === `#${id}`) { + window.history.replaceState( + null, + "", + window.location.pathname + window.location.search, + ); + } + }; + + return ( +
+ + + +
+ {children} +
+
+ ); +} diff --git a/docs/src/components/FaqItem/styles.module.css b/docs/src/components/FaqItem/styles.module.css new file mode 100644 index 0000000..7c36721 --- /dev/null +++ b/docs/src/components/FaqItem/styles.module.css @@ -0,0 +1,87 @@ +/* + * FAQ entry: collapsible on mobile, static heading + expanded answer on + * desktop. The split is pure CSS (Docusaurus breakpoint: 996px) so there is + * no hydration flash. The answer is always rendered into the DOM, so search + * engines and the docs AI bot can read it regardless of layout or state. + */ + +.item { + scroll-margin-top: calc(var(--ifm-navbar-height) + 1rem); +} + +.heading { + margin: 0; +} + +/* Mobile: the heading text is a full-width clickable toggle row. */ +.toggle { + display: flex; + align-items: center; + gap: 0.6rem; + width: 100%; + padding: 0.85rem 0; + border: none; + border-bottom: 1px solid var(--ifm-color-emphasis-200); + background: none; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} + +.toggle::before { + content: ""; + flex: 0 0 auto; + width: 0.5rem; + height: 0.5rem; + border-right: 2px solid currentColor; + border-bottom: 2px solid currentColor; + transform: rotate(-45deg); + transition: transform var(--ifm-transition-fast, 200ms) ease; +} + +.item[data-open] .toggle::before { + transform: rotate(45deg); +} + +.content { + display: none; + padding: 0 0 0.85rem; +} + +.item[data-open] .content { + display: block; +} + +/* Hide the hover hash link on mobile (no hover; avoids a stray empty line). */ +.heading :global(.hash-link) { + display: none; +} + +/* Desktop: render as a normal expanded heading + answer. */ +@media (min-width: 997px) { + .heading { + margin: 1.75rem 0 0.5rem; + } + + .toggle { + display: inline; + width: auto; + padding: 0; + border: none; + cursor: default; + } + + .toggle::before { + display: none; + } + + .content { + display: block; + padding: 0; + } + + .heading :global(.hash-link) { + display: inline; + } +} diff --git a/docs/src/components/LanguageAlert/index.jsx b/docs/src/components/LanguageAlert/index.jsx new file mode 100644 index 0000000..b786c8a --- /dev/null +++ b/docs/src/components/LanguageAlert/index.jsx @@ -0,0 +1,25 @@ +import React, { useEffect, useState } from 'react'; +import { useLocation } from '@docusaurus/router'; +import styles from './styles.module.css'; + +export default function LanguageAlert() { + const [showAlert, setShowAlert] = useState(false); + const { pathname } = useLocation(); + + useEffect(() => { + const userLanguage = navigator?.language || 'en'; + const isChineseUser = userLanguage.includes('zh'); + setShowAlert(isChineseUser); + + }, [pathname]); + + if (!showAlert) return null; + + return ( +
+ 检测到您的主要语言为中文,您可以访问由中文社区翻译的 + 中文文档 + 以获得更好的体验 +
+ ); +} \ No newline at end of file diff --git a/docs/src/components/LanguageAlert/styles.module.css b/docs/src/components/LanguageAlert/styles.module.css new file mode 100644 index 0000000..d415849 --- /dev/null +++ b/docs/src/components/LanguageAlert/styles.module.css @@ -0,0 +1,18 @@ +.alert { + padding: 12px; + background: #fff8e6; + border-bottom: 1px solid #ffd166; + text-align: center; + font-size: 15px; +} + +[data-theme="dark"] .alert { + background: #3b2f0b; + border-bottom: 1px solid #665c22; +} + +.alert a { + color: #1890ff; + font-weight: 500; + margin-left: 6px; +} diff --git a/docs/src/components/ModelConfigDropdown/index.jsx b/docs/src/components/ModelConfigDropdown/index.jsx new file mode 100644 index 0000000..737f47e --- /dev/null +++ b/docs/src/components/ModelConfigDropdown/index.jsx @@ -0,0 +1,171 @@ +import React, { useState } from "react"; +import CodeBlock from "@theme/CodeBlock"; +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import { marked } from "marked"; +import styles from "./styles.module.css"; + +marked.setOptions({ gfm: true }); + +/** + * @typedef {Object} Model + * @property {string} key + * @property {string} label + * @property {boolean} recommended + * @property {string} download Markdown for the "download the model" step. + * @property {string} ui Markdown for the Frigate UI configuration step. + * @property {string} yaml Raw YAML for the configuration step. + */ + +// Render a markdown string to React nodes. Fenced code blocks become Docusaurus +// CodeBlock components (so they get syntax highlighting and a copy button); +// everything else is marked-parsed to HTML. +function renderBlocks(md, keyPrefix) { + if (!md.trim()) return []; + const tokens = marked.lexer(md); + const nodes = []; + let buffer = []; + let idx = 0; + + const flush = () => { + if (buffer.length) { + buffer.links = tokens.links; + nodes.push( +
, + ); + buffer = []; + } + }; + + tokens.forEach((token) => { + if (token.type === "code") { + flush(); + const language = (token.lang || "text").split(/\s+/)[0]; + nodes.push( + + {token.text} + , + ); + } else { + buffer.push(token); + } + }); + flush(); + return nodes; +} + +// marked does not understand Docusaurus admonitions (:::warning ... :::), so +// render those blocks ourselves and render everything around them normally. +function renderMarkdown(md) { + if (!md) return null; + const admonition = /:::(\w+)[ \t]*([^\n]*)\n([\s\S]*?)\n:::/g; + const nodes = []; + let lastIndex = 0; + let match; + let k = 0; + while ((match = admonition.exec(md)) !== null) { + nodes.push(...renderBlocks(md.slice(lastIndex, match.index), `seg${k}`)); + const [, type, title, body] = match; + const heading = (title || type).trim(); + nodes.push( +
+
{heading}
+ {renderBlocks(body, `adm${k}`)} +
, + ); + lastIndex = admonition.lastIndex; + k++; + } + nodes.push(...renderBlocks(md.slice(lastIndex), `seg${k}`)); + return nodes; +} + +function Markdown({ children }) { + return
{renderMarkdown(children)}
; +} + +function RecommendedBadge() { + return Recommended; +} + +/** + * @param {{ models: Model[] }} props + */ +export default function ModelConfigDropdown({ models }) { + const [selectedModelIndex, setSelectedModelIndex] = useState(0); + const [isOpen, setIsOpen] = useState(false); + + const selectedModel = models[selectedModelIndex]; + const hasChoices = models.length > 1; + + const handleModelSelect = (index) => { + setSelectedModelIndex(index); + setIsOpen(false); + }; + + return ( +
+
+
+

Step 1 — Choose a model

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

Step 2 — Download the model

+ {selectedModel.download} +
+ +
+

Step 3 — Configure the detector

+ + + {selectedModel.ui} + + + {selectedModel.yaml} + + +
+
+
+ ); +} diff --git a/docs/src/components/ModelConfigDropdown/styles.module.css b/docs/src/components/ModelConfigDropdown/styles.module.css new file mode 100644 index 0000000..06f0333 --- /dev/null +++ b/docs/src/components/ModelConfigDropdown/styles.module.css @@ -0,0 +1,275 @@ +/* =================================================================== + ModelConfigDropdown — styles + =================================================================== */ + +.wrapper { + margin: 1.5rem 0; +} + +/* --- Dropdown button --- */ + +.dropdown { + display: inline-block; + width: 360px; + max-width: 100%; + text-align: left; + border: 1px solid var(--ifm-color-emphasis-400); + border-radius: 8px; + background: var(--ifm-background-color); + cursor: pointer; + transition: + border-color 0.2s, + box-shadow 0.2s; +} + +[data-theme="light"] .dropdown { + border: 1px solid #d0d7de; + background: #fff; +} + +[data-theme="dark"] .dropdown { + border: 1px solid var(--ifm-color-emphasis-300); + background: #21262d; +} + +.dropdown:hover { + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest); +} + +[data-theme="dark"] .dropdown:hover { + box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest); +} + +.dropdown.open { + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest); +} + +[data-theme="dark"] .dropdown.open { + border-color: var(--ifm-color-primary); +} + +/* Single-model detectors render the label without a clickable menu. */ +.dropdown.static { + cursor: default; +} + +.dropdown.static:hover { + border-color: var(--ifm-color-emphasis-400); + box-shadow: none; +} + +[data-theme="light"] .dropdown.static:hover { + border-color: #d0d7de; +} + +[data-theme="dark"] .dropdown.static:hover { + border-color: var(--ifm-color-emphasis-300); +} + +.dropdownContent { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 0.8rem 1rem; +} + +/* --- Model menu --- */ + +.menu { + margin-top: 0.25rem; + width: 360px; + max-width: 100%; + border: 1px solid var(--ifm-color-emphasis-400); + border-radius: 8px; + overflow: hidden; + background: var(--ifm-background-color); +} + +[data-theme="light"] .menu { + border: 1px solid #d0d7de; + background: #fff; +} + +[data-theme="dark"] .menu { + border: 1px solid var(--ifm-color-emphasis-300); + background: #21262d; +} + +.menuItem { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 1rem; + cursor: pointer; + font-size: 0.95rem; + color: var(--ifm-font-color-base); + transition: background 0.15s; +} + +.menuItem:not(:last-child) { + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.menuItem:hover { + background: var(--ifm-color-emphasis-100); +} + +.menuItemActive { + font-weight: var(--ifm-font-weight-semibold); + background: var(--ifm-color-primary-lightest); +} + +[data-theme="dark"] .menuItem:hover { + background: #2b3139; +} + +[data-theme="dark"] .menuItemActive { + background: #2b3139; +} + +.modelName { + font-weight: var(--ifm-font-weight-semibold); + color: var(--ifm-font-color-base); + font-size: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; +} + +.recommendedBadge { + display: inline-block; + background: var(--ifm-color-success); + color: #fff; + font-size: 0.7rem; + font-weight: 600; + padding: 2px 8px; + border-radius: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.arrow { + font-size: 0.7rem; + color: var(--ifm-font-color-secondary); + transition: transform 0.2s; +} + +.dropdown.open .arrow { + transform: rotate(180deg); +} + +/* --- Panel --- */ + +.panel { + margin-top: 0.5rem; + border: 1px solid var(--ifm-color-emphasis-400); + border-radius: 8px; + overflow: hidden; + background: var(--ifm-background-color); +} + +[data-theme="light"] .panel { + border: 1px solid #d0d7de; + background: #fff; +} + +[data-theme="dark"] .panel { + border: 1px solid var(--ifm-color-emphasis-300); + background: #21262d; +} + +/* --- Steps --- */ + +.step { + padding: 1rem; +} + +.step:not(:last-child) { + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.stepTitle { + margin: 0 0 0.75rem 0; + font-size: 1rem; + font-weight: 600; + color: var(--ifm-font-color-base); +} + +/* Rendered markdown (download + Frigate UI instructions). */ + +.markdown { + font-size: 0.9rem; + line-height: 1.6; +} + +.markdown > :last-child { + margin-bottom: 0; +} + +.markdown a { + color: var(--ifm-color-primary); + text-decoration: underline; + text-underline-offset: 2px; +} + +.markdown table { + display: table; + width: 100%; + margin: 0.75rem 0; + font-size: 0.85rem; +} + +/* Docusaurus-style admonitions rendered from markdown. */ + +.admonition { + margin: 0.75rem 0; + padding: 0.75rem 1rem; + border-left: 4px solid var(--ifm-color-info); + border-radius: 4px; + background: var(--ifm-color-info-contrast-background); + font-size: 0.85rem; +} + +.admonition > :last-child { + margin-bottom: 0; +} + +.admonitionTitle { + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + font-size: 0.75rem; + margin-bottom: 0.4rem; + color: var(--ifm-color-info); +} + +.admonition_warning { + border-left-color: var(--ifm-color-warning); + background: var(--ifm-color-warning-contrast-background); +} + +.admonition_warning .admonitionTitle { + color: var(--ifm-color-warning-dark); +} + +.admonition_danger { + border-left-color: var(--ifm-color-danger); + background: var(--ifm-color-danger-contrast-background); +} + +.admonition_danger .admonitionTitle { + color: var(--ifm-color-danger-dark); +} + +.admonition_tip { + border-left-color: var(--ifm-color-success); + background: var(--ifm-color-success-contrast-background); +} + +.admonition_tip .admonitionTitle { + color: var(--ifm-color-success-dark); +} diff --git a/docs/src/components/NavPath/index.jsx b/docs/src/components/NavPath/index.jsx new file mode 100644 index 0000000..e5ec86b --- /dev/null +++ b/docs/src/components/NavPath/index.jsx @@ -0,0 +1,30 @@ +import React from "react"; + +export default function NavPath({ path }) { + const segments = path.split(" > "); + return ( + + {segments.map((seg, i) => ( + + {i > 0 && ( + + → + + )} + {seg} + + ))} + + ); +} diff --git a/docs/src/components/ShmCalculator/index.jsx b/docs/src/components/ShmCalculator/index.jsx new file mode 100644 index 0000000..b7e13ed --- /dev/null +++ b/docs/src/components/ShmCalculator/index.jsx @@ -0,0 +1,201 @@ +import React, { useState, useEffect } from "react"; +import Admonition from "@theme/Admonition"; +import styles from "./styles.module.css"; + +const ShmCalculator = () => { + const [width, setWidth] = useState(1280); + const [height, setHeight] = useState(720); + const [cameraCount, setCameraCount] = useState(1); + const [result, setResult] = useState("26.32MB"); + const [singleCameraShm, setSingleCameraShm] = useState("26.32MB"); + const [totalShm, setTotalShm] = useState("26.32MB"); + + const calculate = () => { + if (!width || !height || !cameraCount) { + setResult("Please enter valid values"); + setSingleCameraShm("-"); + setTotalShm("-"); + return; + } + + // Single camera base SHM calculation (excluding logs) + // Formula: (width * height * 1.5 * 20 + 270480) / 1048576 + const singleCameraBase = + (width * height * 1.5 * 20 + 270480) / 1048576; + setSingleCameraShm(`${singleCameraBase.toFixed(2)}mb`); + + // Total SHM calculation (multiple cameras, including logs) + const totalBase = singleCameraBase * cameraCount; + const finalResult = totalBase + 40; // Default includes logs +40mb + + setTotalShm(`${(totalBase + 40).toFixed(2)}mb`); + + // Format result + if (finalResult < 1) { + setResult(`${(finalResult * 1024).toFixed(2)}kb`); + } else if (finalResult >= 1024) { + setResult(`${(finalResult / 1024).toFixed(2)}gb`); + } else { + setResult(`${finalResult.toFixed(2)}mb`); + } + }; + + const formatWithUnit = (value) => { + const match = value.match(/^([\d.]+)(mb|kb|gb)$/i); + if (match) { + return ( + <> + {match[1]}{match[2]} + + ); + } + return value; + }; + + const applyPreset = (w, h, count) => { + setWidth(w); + setHeight(h); + setCameraCount(count); + calculate(); + }; + + useEffect(() => { + calculate(); + }, [width, height, cameraCount]); + + return ( +
+
+

SHM Calculator

+

+ Calculate required shared memory (SHM) based on camera resolution and + count +

+ + + The resolution below is the detect stream resolution, + not the record stream resolution. SHM size is + determined by the detect resolution used for object detection.{" "} + + Learn more about choosing a detect resolution. + + + + {width * height > 1280 * 720 && ( + + Using a detect resolution higher than 720p is not recommended. + Higher resolutions do not improve object detection accuracy and will + consume significantly more resources. + + )} + +
+
+
+ + setWidth(Number(e.target.value))} + /> +
+
+ +
+
+ + setHeight(Number(e.target.value))} + /> +
+
+
+ +
+ + setCameraCount(Number(e.target.value))} + /> +
+ +
+

Calculation Result

+
+ {formatWithUnit(result)} +
+
+

+ Single Camera: {formatWithUnit(singleCameraShm)} +

+

+ Formula: (width × height × 1.5 × 20 + 270480) ÷ + 1048576 +

+ {cameraCount > 1 && ( +

+ Total ({cameraCount} cameras): {formatWithUnit(totalShm)} +

+ )} +

+ With Logs: + 40mb +

+
+
+ +
+

Common Presets

+
+ + + + +
+
+
+
+ ); +}; + +export default ShmCalculator; diff --git a/docs/src/components/ShmCalculator/styles.module.css b/docs/src/components/ShmCalculator/styles.module.css new file mode 100644 index 0000000..5b48f49 --- /dev/null +++ b/docs/src/components/ShmCalculator/styles.module.css @@ -0,0 +1,131 @@ +.shmCalculator { + margin: 2rem 0; + max-width: 600px; +} + +.card { + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-border-color); + border-radius: 12px; + padding: 2rem; + box-shadow: var(--ifm-global-shadow-lw); +} + +[data-theme='light'] .card { + background: var(--ifm-color-emphasis-100); + border: 1px solid var(--ifm-color-emphasis-300); +} + +.title { + margin: 0 0 0.5rem 0; + font-size: 1.5rem; + color: var(--ifm-font-color-base); + font-weight: var(--ifm-font-weight-semibold); +} + +.description { + margin: 0 0 1.5rem 0; + color: var(--ifm-font-color-secondary); + font-size: 0.9rem; +} + +.formGroup { + margin-bottom: 1rem; +} + +.label { + display: block; + margin-bottom: 0.25rem; + color: var(--ifm-font-color-base); + font-weight: var(--ifm-font-weight-semibold); + font-size: 0.9rem; +} + +.input { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid var(--ifm-border-color); + border-radius: 6px; + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + font-size: 0.95rem; + transition: border-color 0.2s, box-shadow 0.2s; +} + +[data-theme='light'] .input { + background: #fff; + border: 1px solid #d0d7de; +} + +.input:focus { + outline: none; + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest); +} + +.resultSection { + margin-top: 1rem; + padding: 1.5rem; + background: var(--ifm-background-color); + border-radius: 8px; + border: 1px solid var(--ifm-border-color); +} + +[data-theme='light'] .resultSection { + background: #f6f8fa; + border: 1px solid #d0d7de; +} + +.resultSection h4 { + margin: 0 0 1rem 0; + color: var(--ifm-font-color-base); + font-weight: var(--ifm-font-weight-semibold); +} + +.resultValue { + text-align: center; + padding: 1rem; + background: var(--ifm-color-primary); + border-radius: 6px; + margin-bottom: 1rem; +} + +.resultNumber { + font-size: 2rem; + font-weight: var(--ifm-font-weight-bold); + color: #fff; +} + +.formulaDisplay { + font-size: 0.85rem; + color: var(--ifm-font-color-secondary); + line-height: 1.6; +} + +.formulaDisplay p { + margin: 0.25rem 0; +} + +.formulaDisplay strong { + color: var(--ifm-font-color-base); +} + +.unit { + text-transform: uppercase; +} + +.presets { + margin-top: 1.5rem; +} + +.presets h4 { + margin: 0 0 0.75rem 0; + color: var(--ifm-font-color-base); + font-weight: var(--ifm-font-weight-semibold); +} + +.presetButtons { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css new file mode 100644 index 0000000..6d9b7c8 --- /dev/null +++ b/docs/src/css/custom.css @@ -0,0 +1,290 @@ +/* stylelint-disable docusaurus/copyright-header */ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #3b82f7; + --ifm-color-primary-dark: #1d4ed8; + --ifm-color-primary-darker: #1e40af; + --ifm-color-primary-darkest: #1e3a8a; + --ifm-color-primary-light: #60a5fa; + --ifm-color-primary-lighter: #93c5fd; + --ifm-color-primary-lightest: #dbeafe; + --ifm-code-font-size: 95%; +} + +.docusaurus-highlight-code-line { + background-color: rgb(72, 77, 91); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} + +/** + Custom CSS for OpenAPI Specification. Based of openapi https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/tree/main/demo +*/ + +/* Sidebar Method labels */ +.api-method > .menu__link, +.schema > .menu__link { + align-items: center; + justify-content: start; +} + +.api-method > .menu__link::before, +.schema > .menu__link::before { + width: 55px; + height: 20px; + font-size: 12px; + line-height: 20px; + text-transform: uppercase; + font-weight: 600; + border-radius: 0.25rem; + border: 1px solid; + margin-right: var(--ifm-spacing-horizontal); + text-align: center; + flex-shrink: 0; + border-color: transparent; + color: white; +} + +.get > .menu__link::before { + content: "get"; + background-color: var(--ifm-color-primary); +} + +.post > .menu__link::before { + content: "post"; + background-color: var(--ifm-color-success); +} + +.delete > .menu__link::before { + content: "del"; + background-color: var(--openapi-code-red); +} + +.put > .menu__link::before { + content: "put"; + background-color: var(--openapi-code-blue); +} + +.patch > .menu__link::before { + content: "patch"; + background-color: var(--openapi-code-orange); +} + +.head > .menu__link::before { + content: "head"; + background-color: var(--ifm-color-secondary-darkest); +} + +.event > .menu__link::before { + content: "event"; + background-color: var(--ifm-color-secondary-darkest); +} + +.schema > .menu__link::before { + content: "schema"; + background-color: var(--ifm-color-secondary-darkest); +} + +.menu__list-item--deprecated > .menu__link, +.menu__list-item--deprecated > .menu__link:hover { + text-decoration: line-through; +} +/* Sidebar Method labels High Contrast */ +.api-method-contrast > .menu__link, +.schema-contrast > .menu__link { + align-items: center; + justify-content: start; +} + +.api-method-contrast > .menu__link::before, +.schema-contrast > .menu__link::before { + width: 55px; + height: 20px; + font-size: 12px; + line-height: 20px; + text-transform: uppercase; + font-weight: 600; + border-radius: 0.25rem; + border: 1px solid; + border-inline-start-width: 5px; + margin-right: var(--ifm-spacing-horizontal); + text-align: center; + flex-shrink: 0; +} + +.get-contrast > .menu__link::before { + content: "get"; + background-color: var(--ifm-color-info-contrast-background); + color: var(--ifm-color-info-contrast-foreground); + border-color: var(--ifm-color-info-dark); +} + +.post-contrast > .menu__link::before { + content: "post"; + background-color: var(--ifm-color-success-contrast-background); + color: var(--ifm-color-success-contrast-foreground); + border-color: var(--ifm-color-success-dark); +} + +.delete-contrast > .menu__link::before { + content: "del"; + background-color: var(--ifm-color-danger-contrast-background); + color: var(--ifm-color-danger-contrast-foreground); + border-color: var(--ifm-color-danger-dark); +} + +.put-contrast > .menu__link::before { + content: "put"; + background-color: var(--ifm-color-warning-contrast-background); + color: var(--ifm-color-warning-contrast-foreground); + border-color: var(--ifm-color-warning-dark); +} + +.patch-contrast > .menu__link::before { + content: "patch"; + background-color: var(--ifm-color-success-contrast-background); + color: var(--ifm-color-success-contrast-foreground); + border-color: var(--ifm-color-success-dark); +} + +.head-contrast > .menu__link::before { + content: "head"; + background-color: var(--ifm-color-secondary-contrast-background); + color: var(--ifm-color-secondary-contrast-foreground); + border-color: var(--ifm-color-secondary-dark); +} + +.event-contrast > .menu__link::before { + content: "event"; + background-color: var(--ifm-color-secondary-contrast-background); + color: var(--ifm-color-secondary-contrast-foreground); + border-color: var(--ifm-color-secondary-dark); +} + +.schema-contrast > .menu__link::before { + content: "schema"; + background-color: var(--ifm-color-secondary-contrast-background); + color: var(--ifm-color-secondary-contrast-foreground); + border-color: var(--ifm-color-secondary-dark); +} + +/* Simple */ +.api-method-simple > .menu__link { + align-items: center; + justify-content: start; +} +.api-method-simple > .menu__link::before { + width: 55px; + height: 20px; + font-size: 12px; + line-height: 20px; + text-transform: uppercase; + font-weight: 600; + border-radius: 0.25rem; + align-content: start; + margin-right: var(--ifm-spacing-horizontal); + text-align: right; + flex-shrink: 0; + border-color: transparent; +} + +.get-simple > .menu__link::before { + content: "get"; + color: var(--ifm-color-info); +} + +.post-simple > .menu__link::before { + content: "post"; + color: var(--ifm-color-success); +} + +.delete-simple > .menu__link::before { + content: "del"; + color: var(--ifm-color-danger); +} + +.put-simple > .menu__link::before { + content: "put"; + color: var(--ifm-color-warning); +} + +.patch-simple > .menu__link::before { + content: "patch"; + color: var(--ifm-color-warning); +} + +.head-simple > .menu__link::before { + content: "head"; + color: var(--ifm-color-secondary-contrast-foreground); +} + +.event-simple > .menu__link::before { + content: "event"; + color: var(--ifm-color-secondary-contrast-foreground); +} + +.schema-simple > .menu__link::before { + content: "schema"; + color: var(--ifm-color-secondary-contrast-foreground); +} + +.code-block-error-line { + background-color: #ff000020; + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); + border-left: 3px solid #ff000080; +} + +/* ConfigTabs wrapper */ +.config-tabs-wrapper { + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 8px; + overflow: hidden; + margin-bottom: 16px; +} + +.config-tabs-wrapper .tabs-container { + margin-bottom: 0 !important; +} + +.config-tabs-wrapper .tabs { + background: var(--ifm-color-emphasis-100); + border-bottom: 1px solid var(--ifm-color-emphasis-300); + margin-bottom: 0; + padding: 0 12px; +} + +.config-tabs-wrapper .tabs__item { + padding: 8px 16px; + border-radius: 0; +} + +.config-tabs-wrapper .tabs__item--active { + border-bottom-color: var(--ifm-color-primary); +} + +.config-tabs-wrapper .config-tab-ui { + padding: 4px 16px 16px; +} + +.config-tabs-wrapper .config-tab-ui > :last-child { + margin-bottom: 0; +} + +.config-tabs-wrapper div[class*="codeBlockContainer"] { + border-top-left-radius: 0; + border-top-right-radius: 0; + margin: 0; +} + +.config-tabs-wrapper .tabs-container > .margin-top--md:has(.config-tab-yaml:not([hidden])) { + margin-top: 0 !important; +} \ No newline at end of file diff --git a/docs/src/theme/Navbar/index.js b/docs/src/theme/Navbar/index.js new file mode 100644 index 0000000..4dd3aee --- /dev/null +++ b/docs/src/theme/Navbar/index.js @@ -0,0 +1,15 @@ +import React from 'react'; +import NavbarLayout from '@theme/Navbar/Layout'; +import NavbarContent from '@theme/Navbar/Content'; +import LanguageAlert from '../../components/LanguageAlert'; + +export default function Navbar() { + return ( + <> + + + + + + ); +} \ No newline at end of file diff --git a/docs/static/.nojekyll b/docs/static/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/static/_headers b/docs/static/_headers new file mode 100644 index 0000000..7327dc4 --- /dev/null +++ b/docs/static/_headers @@ -0,0 +1,8 @@ +https://:project.pages.dev/* + X-Robots-Tag: noindex + +https://:version.:project.pages.dev/* + X-Robots-Tag: noindex + +https://docs-dev.frigate.video/* + X-Robots-Tag: noindex \ No newline at end of file diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml new file mode 100644 index 0000000..af3a019 --- /dev/null +++ b/docs/static/frigate-api.yaml @@ -0,0 +1,8841 @@ +# Generated by generate_api_auth_spec.py — do not edit by hand. +# Regenerate with: python3 generate_api_auth_spec.py +# The empty info.title is intentional: a docusaurus-openapi-docs convention +# that suppresses the generated API introduction page. +openapi: 3.1.0 +info: + title: '' + version: 0.1.0 +servers: + - url: https://demo.frigate.video/api + - url: http://localhost:5001/api +paths: + /auth/first_time_login: + get: + tags: + - Auth + summary: First Time Login + description: |- + **Access:** Public — no authentication required. + + Return whether the admin first-time login help flag is set in config. + + This endpoint is intentionally unauthenticated so the login page can + query it before a user is authenticated. + operationId: first_time_login_auth_first_time_login_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: [] + x-required-role: public + /auth: + get: + tags: + - Auth + summary: Authenticate request + description: |- + **Access:** Public — no authentication required. + + Authenticates the current request based on proxy headers or JWT token. This endpoint verifies authentication credentials and manages JWT token refresh. On success, no JSON body is returned; authentication state is communicated via response headers and cookies. + operationId: auth_auth_get + responses: + '202': + description: Authentication Accepted (no response body) + content: + application/json: + schema: {} + headers: + remote-user: + description: Authenticated username or "viewer" in proxy-only mode + schema: + type: string + remote-role: + description: Resolved role (e.g., admin, viewer, or custom) + schema: + type: string + Set-Cookie: + description: May include refreshed JWT cookie when applicable + schema: + type: string + '401': + description: Authentication Failed + security: [] + x-required-role: public + /profile: + get: + tags: + - Auth + summary: Get user profile + description: |- + **Access:** Any authenticated user. + + Returns the current authenticated user's profile including username, role, and allowed cameras. This endpoint requires authentication and returns information about the user's permissions. + operationId: profile_profile_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + /logout: + get: + tags: + - Auth + summary: Logout user + description: |- + **Access:** Public — no authentication required. + + Logs out the current user by clearing the session cookie. After logout, subsequent requests will require re-authentication. + operationId: logout_logout_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: [] + x-required-role: public + /login: + post: + tags: + - Auth + summary: Login with credentials + description: |- + **Access:** Public — no authentication required. + + Authenticates a user with username and password. Returns a JWT token as a secure HTTP-only cookie that can be used for subsequent API requests. The JWT token can also be retrieved from the response and used as a Bearer token in the Authorization header. + + Example using Bearer token: + ``` + curl -H "Authorization: Bearer " https://frigate_ip:8971/api/profile + ``` + operationId: login_login_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AppPostLoginBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: [] + x-required-role: public + /users: + get: + tags: + - Auth + summary: Get all users + description: |- + **Access:** Admin role required. + + Returns a list of all users with their usernames and roles. Requires admin role. Each user object contains the username and assigned role. + operationId: get_users_users_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + post: + tags: + - Auth + summary: Create new user + description: |- + **Access:** Admin role required. + + Creates a new user with the specified username, password, and role. Requires admin role. Password must be at least 12 characters long. + operationId: create_user_users_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AppPostUsersBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /users/{username}: + delete: + tags: + - Auth + summary: Delete user + description: |- + **Access:** Admin role required. + + Deletes a user by username. The built-in admin user cannot be deleted. Requires admin role. Returns success message or error if user not found. + operationId: delete_user_users__username__delete + parameters: + - name: username + in: path + required: true + schema: + type: string + title: Username + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /users/{username}/password: + put: + tags: + - Auth + summary: Update user password + description: |- + **Access:** Any authenticated user. + + Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must be at least 12 characters long. If user changes their own password, a new JWT cookie is automatically issued. + operationId: update_password_users__username__password_put + parameters: + - name: username + in: path + required: true + schema: + type: string + title: Username + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AppPutPasswordBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /users/{username}/role: + put: + tags: + - Auth + summary: Update user role + description: |- + **Access:** Admin role required. + + Updates a user's role. The built-in admin user's role cannot be modified. Requires admin role. Valid roles are defined in the configuration. + operationId: update_role_users__username__role_put + parameters: + - name: username + in: path + required: true + schema: + type: string + title: Username + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AppPutRoleBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /go2rtc/streams: + get: + tags: + - Camera + summary: Go2Rtc Streams + operationId: go2rtc_streams_go2rtc_streams_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /go2rtc/streams/{stream_name}: + get: + tags: + - Camera + summary: Go2Rtc Camera Stream + operationId: go2rtc_camera_stream_go2rtc_streams__stream_name__get + parameters: + - name: stream_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Stream Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + put: + tags: + - Camera + summary: Go2Rtc Add Stream + description: |- + **Access:** Admin role required. + + Add or update a go2rtc stream configuration. + operationId: go2rtc_add_stream_go2rtc_streams__stream_name__put + parameters: + - name: stream_name + in: path + required: true + schema: + type: string + title: Stream Name + - name: src + in: query + required: false + schema: + type: string + default: '' + title: Src + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + delete: + tags: + - Camera + summary: Go2Rtc Delete Stream + description: |- + **Access:** Admin role required. + + Delete a go2rtc stream. + operationId: go2rtc_delete_stream_go2rtc_streams__stream_name__delete + parameters: + - name: stream_name + in: path + required: true + schema: + type: string + title: Stream Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /ffprobe: + get: + tags: + - Camera + summary: Ffprobe + operationId: ffprobe_ffprobe_get + parameters: + - name: paths + in: query + required: false + schema: + type: string + default: '' + title: Paths + - name: detailed + in: query + required: false + schema: + type: boolean + default: false + title: Detailed + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' + /keyframe_analysis: + get: + tags: + - Camera + summary: Keyframe Analysis + description: |- + **Access:** Admin role required. + + Probe a camera's record stream and classify its keyframe spacing. + + Detects smart/+ codecs and long/variable GOPs that degrade recording. + operationId: keyframe_analysis_keyframe_analysis_get + parameters: + - name: camera + in: query + required: false + schema: + type: string + default: '' + title: Camera + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /ffprobe/snapshot: + get: + tags: + - Camera + summary: Ffprobe Snapshot + description: |- + **Access:** Admin role required. + + Get a snapshot from a stream URL using ffmpeg. + operationId: ffprobe_snapshot_ffprobe_snapshot_get + parameters: + - name: url + in: query + required: false + schema: + type: string + default: '' + title: Url + - name: timeout + in: query + required: false + schema: + type: integer + default: 10 + title: Timeout + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /reolink/detect: + get: + tags: + - Camera + summary: Reolink Detect + description: |- + **Access:** Admin role required. + + Detect Reolink camera capabilities and recommend optimal protocol. + + Queries the Reolink camera API to determine the camera's resolution + and recommends either http-flv (for 5MP and below) or rtsp (for higher resolutions). + operationId: reolink_detect_reolink_detect_get + parameters: + - name: host + in: query + required: false + schema: + type: string + default: '' + title: Host + - name: username + in: query + required: false + schema: + type: string + default: '' + title: Username + - name: password + in: query + required: false + schema: + type: string + default: '' + title: Password + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /onvif/probe: + get: + tags: + - Camera + summary: Probe ONVIF device + description: |- + **Access:** Admin role required. + + Probe an ONVIF device to determine capabilities and optionally test available stream URIs. Query params: host (required), port (default 80), username, password, test (boolean), auth_type (basic or digest, default basic). + operationId: onvif_probe_onvif_probe_get + parameters: + - name: host + in: query + required: false + schema: + type: string + title: Host + - name: port + in: query + required: false + schema: + type: integer + default: 80 + title: Port + - name: username + in: query + required: false + schema: + type: string + default: '' + title: Username + - name: password + in: query + required: false + schema: + type: string + default: '' + title: Password + - name: test + in: query + required: false + schema: + type: boolean + default: false + title: Test + - name: auth_type + in: query + required: false + schema: + type: string + default: basic + title: Auth Type + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /cameras/{camera_name}: + delete: + tags: + - Camera + summary: Delete Camera + description: |- + **Access:** Admin role required. + + Delete a camera and all its associated data. + + Removes the camera from config, stops processes, and cleans up + 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 + operationId: delete_camera_cameras__camera_name__delete + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: delete_exports + in: query + required: false + schema: + type: boolean + default: false + title: Delete Exports + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /camera/{camera_name}/set/{feature}/{sub_command}: + put: + tags: + - Camera + summary: Camera Set + description: |- + **Access:** Admin role required. + + Set a camera feature state. Use camera_name='*' to target all cameras. + operationId: + camera_set_camera__camera_name__set__feature___sub_command__put + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: feature + in: path + required: true + schema: + type: string + title: Feature + - name: sub_command + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Sub Command + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CameraSetBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /camera/{camera_name}/set/{feature}: + put: + tags: + - Camera + summary: Camera Set + description: |- + **Access:** Admin role required. + + Set a camera feature state. Use camera_name='*' to target all cameras. + operationId: camera_set_camera__camera_name__set__feature__put + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: feature + in: path + required: true + schema: + type: string + title: Feature + - name: sub_command + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Sub Command + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CameraSetBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /chat/tools: + get: + tags: + - Chat + summary: Get available tools + description: |- + **Access:** Admin role required. + + Returns OpenAI-compatible tool definitions for function calling. + operationId: get_tools_chat_tools_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + /chat/execute: + post: + tags: + - Chat + summary: Execute a tool + description: |- + **Access:** Admin role required. + + Execute a tool function call from an LLM. + operationId: execute_tool_chat_execute_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ToolExecuteRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /chat/completion: + post: + tags: + - Chat + summary: Chat completion with tool calling + description: |- + **Access:** Admin role required. + + Send a chat message to the configured GenAI provider with tool calling support. The LLM can call Frigate tools to answer questions about your cameras and events. + operationId: chat_completion_chat_completion_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChatCompletionRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /vlm/monitor: + post: + tags: + - Chat + summary: Start a VLM watch job + description: |- + **Access:** Admin role required. + + Start monitoring a camera with the vision provider. The VLM analyzes live frames until the specified condition is met, then sends a notification. Only one watch job can run at a time. + operationId: start_vlm_monitor_vlm_monitor_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VLMMonitorRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + get: + tags: + - Chat + summary: Get current VLM watch job + description: |- + **Access:** Admin role required. + + Returns the current (or most recently completed) VLM watch job. + operationId: get_vlm_monitor_vlm_monitor_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + delete: + tags: + - Chat + summary: Cancel the current VLM watch job + description: |- + **Access:** Admin role required. + + Cancels the running watch job if one exists. + operationId: cancel_vlm_monitor_vlm_monitor_delete + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces: + get: + tags: + - Classification + summary: Get all registered faces + description: |- + **Access:** Admin role required. + + Returns a dictionary mapping face names to lists of image filenames. + Each key represents a registered face name, and the value is a list of image + files associated with that face. Supported image formats include .webp, .png, + .jpg, and .jpeg. + operationId: get_faces_faces_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/FacesResponse' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/reprocess: + post: + tags: + - Classification + summary: Reprocess a face training image + description: |- + **Access:** Admin role required. + + Reprocesses a face training image to update the prediction. + Requires face recognition to be enabled in the configuration. The training file + must exist in the faces/train directory. Returns a success response or an error + message if face recognition is not enabled or the training file is invalid. + operationId: reclassify_face_faces_reprocess_post + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/train/{name}/classify: + post: + tags: + - Classification + summary: Classify and save a face training image + description: |- + **Access:** Admin role required. + + Adds a training image to a specific face name for face recognition. + Accepts either a training file from the train directory or an event_id to extract + the face from. The image is saved to the face's directory and the face classifier + is cleared to incorporate the new training data. Returns a success message with + the new filename or an error if face recognition is not enabled, the file/event + is invalid, or the face cannot be extracted. + operationId: train_face_faces_train__name__classify_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/{name}/create: + post: + tags: + - Classification + summary: Create a new face name + description: |- + **Access:** Admin role required. + + Creates a new folder for a face name in the faces directory. + This is used to organize face training images. The face name is sanitized and + spaces are replaced with underscores. Returns a success message or an error if + face recognition is not enabled. + operationId: create_face_faces__name__create_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/{name}/register: + post: + tags: + - Classification + summary: Register a face image + description: |- + **Access:** Admin role required. + + Registers a face image for a specific face name by uploading an image file. + The uploaded image is processed and added to the face recognition system. Returns a + success response with details about the registration, or an error if face recognition + is not enabled or the image cannot be processed. + operationId: register_face_faces__name__register_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/Body_register_face_faces__name__register_post' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/recognize: + post: + tags: + - Classification + summary: Recognize a face from an uploaded image + description: |- + **Access:** Admin role required. + + Recognizes a face from an uploaded image file by comparing it against + registered faces in the system. Returns the recognized face name and confidence score, + or an error if face recognition is not enabled or the image cannot be processed. + operationId: recognize_face_faces_recognize_post + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/Body_recognize_face_faces_recognize_post' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/FaceRecognitionResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/{name}/reclassify: + post: + tags: + - Classification + summary: Reclassify a face image to a different name + description: |- + **Access:** Admin role required. + + Moves a single face image from one person's folder to another. + The image is moved and renamed, and the face classifier is cleared to + incorporate the change. Returns a success message or an error if the + image or target name is invalid. + operationId: reclassify_face_image_faces__name__reclassify_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/{name}/delete: + post: + tags: + - Classification + summary: Delete face images + description: |- + **Access:** Admin role required. + + Deletes specific face images for a given face name. The image IDs must belong + to the specified face folder. To delete an entire face folder, all image IDs in that + folder must be sent. Returns a success message or an error if face recognition is not enabled. + operationId: deregister_faces_faces__name__delete_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteFaceImagesBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/{old_name}/rename: + put: + tags: + - Classification + summary: Rename a face name + description: |- + **Access:** Admin role required. + + Renames a face name in the system. The old name must exist and the new + name must be valid. Returns a success message or an error if face recognition is not enabled. + operationId: rename_face_faces__old_name__rename_put + parameters: + - name: old_name + in: path + required: true + schema: + type: string + title: Old Name + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RenameFaceBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /lpr/reprocess: + put: + tags: + - Classification + summary: Reprocess a license plate + description: |- + **Access:** Admin role required. + + Reprocesses a license plate image to update the plate. + Requires license plate recognition to be enabled in the configuration. The event_id + must exist in the database. Returns a success message or an error if license plate + recognition is not enabled or the event_id is invalid. + operationId: reprocess_license_plate_lpr_reprocess_put + parameters: + - name: event_id + in: query + required: true + schema: + type: string + title: Event Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /reindex: + put: + tags: + - Classification + summary: Reindex embeddings + description: |- + **Access:** Admin role required. + + Reindexes the embeddings for all tracked objects. + Requires semantic search to be enabled in the configuration. Returns a success message or an error if semantic search is not enabled. + operationId: reindex_embeddings_reindex_put + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + security: + - frigateAdminAuth: [] + x-required-role: admin + /audio/transcribe: + put: + tags: + - Classification + summary: Transcribe audio + description: |- + **Access:** Admin role required. + + Transcribes audio from a specific event. + Requires audio transcription to be enabled in the configuration. The event_id + must exist in the database. Returns a success message or an error if audio transcription is not enabled or the event_id is invalid. + operationId: transcribe_audio_audio_transcribe_put + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AudioTranscriptionBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset: + get: + tags: + - Classification + summary: Get classification dataset + description: |- + **Access:** Admin role required. + + Gets the dataset for a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid. + operationId: get_classification_dataset_classification__name__dataset_get + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/attributes: + get: + tags: + - Classification + summary: Get custom classification attributes + description: |- + **Access:** Admin role required. + + Returns custom classification attributes for a given object type. + Only includes models with classification_type set to 'attribute'. + By default returns a flat sorted list of all attribute labels. + If group_by_model is true, returns attributes grouped by model name. + operationId: get_custom_attributes_classification_attributes_get + parameters: + - name: object_type + in: query + required: false + schema: + type: string + title: Object Type + - name: group_by_model + in: query + required: false + schema: + type: boolean + default: false + title: Group By Model + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/train: + get: + tags: + - Classification + summary: Get classification train images + description: |- + **Access:** Admin role required. + + Gets the train images for a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid. + operationId: get_classification_images_classification__name__train_get + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + post: + tags: + - Classification + summary: Train a classification model + description: |- + **Access:** Admin role required. + + Trains a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid. + operationId: train_configured_model_classification__name__train_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset/{category}/delete: + post: + tags: + - Classification + summary: Delete classification dataset images + description: |- + **Access:** Admin role required. + + Deletes specific dataset images for a given classification model and category. + The image IDs must belong to the specified category. Returns a success message or an error if the name or category is invalid. + operationId: + delete_classification_dataset_images_classification__name__dataset__category__delete_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + - name: category + in: path + required: true + schema: + type: string + title: Category + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset/{category}/reclassify: + post: + tags: + - Classification + summary: Reclassify a dataset image to a different category + description: |- + **Access:** Admin role required. + + Moves a single dataset image from one category to another. + The image is re-saved as PNG in the target category and removed from the source. + operationId: + reclassify_classification_image_classification__name__dataset__category__reclassify_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + - name: category + in: path + required: true + schema: + type: string + title: Category + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset/{old_category}/rename: + put: + tags: + - Classification + summary: Rename a classification category + description: |- + **Access:** Admin role required. + + Renames a classification category for a given classification model. + The old category must exist and the new name must be valid. Returns a success message or an error if the name is invalid. + operationId: + rename_classification_category_classification__name__dataset__old_category__rename_put + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + - name: old_category + in: path + required: true + schema: + type: string + title: Old Category + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset/categorize: + post: + tags: + - Classification + summary: Categorize a classification image + description: |- + **Access:** Admin role required. + + Categorizes a specific classification image for a given classification model and category. + The image must exist in the specified category. Returns a success message or an error if the name or category is invalid. + operationId: + categorize_classification_image_classification__name__dataset_categorize_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset/{category}/create: + post: + tags: + - Classification + summary: Create an empty classification category folder + description: |- + **Access:** Admin role required. + + Creates an empty folder for a classification category. + This is used to create folders for categories that don't have images yet. + Returns a success message or an error if the name is invalid. + operationId: + create_classification_category_classification__name__dataset__category__create_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + - name: category + in: path + required: true + schema: + type: string + title: Category + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/train/delete: + post: + tags: + - Classification + summary: Delete classification train images + description: |- + **Access:** Admin role required. + + Deletes specific train images for a given classification model. + The image IDs must belong to the specified train folder. Returns a success message or an error if the name is invalid. + operationId: + delete_classification_train_images_classification__name__train_delete_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/generate_examples/state: + post: + tags: + - Classification + summary: Generate state classification examples + description: |- + **Access:** Admin role required. + + Generate examples for state classification. + operationId: + generate_state_examples_classification_generate_examples_state_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateStateExamplesBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/generate_examples/object: + post: + tags: + - Classification + summary: Generate object classification examples + description: |- + **Access:** Admin role required. + + Generate examples for object classification. + operationId: + generate_object_examples_classification_generate_examples_object_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateObjectExamplesBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}: + delete: + tags: + - Classification + summary: Delete a classification model + description: |- + **Access:** Admin role required. + + Deletes a specific classification model and all its associated data. + Works even if the model is not in the config (e.g., partially created during wizard). + Returns a success message. + operationId: delete_classification_model_classification__name__delete + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /review: + get: + tags: + - Review + summary: Review + operationId: review_review_get + parameters: + - name: cameras + in: query + required: false + schema: + type: string + default: all + title: Cameras + - name: labels + in: query + required: false + schema: + type: string + default: all + title: Labels + - name: zones + in: query + required: false + schema: + type: string + default: all + title: Zones + - name: reviewed + in: query + required: false + schema: + type: integer + title: Reviewed + - name: limit + in: query + required: false + schema: + type: integer + title: Limit + - name: severity + in: query + required: false + schema: + $ref: '#/components/schemas/SeverityEnum' + - name: before + in: query + required: false + schema: + type: number + title: Before + - name: after + in: query + required: false + schema: + type: number + title: After + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ReviewSegmentResponse' + title: Response Review Review Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /review_ids: + get: + tags: + - Review + summary: Review Ids + operationId: review_ids_review_ids_get + parameters: + - name: ids + in: query + required: true + schema: + type: string + title: Ids + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ReviewSegmentResponse' + title: Response Review Ids Review Ids Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /review/summary: + get: + tags: + - Review + summary: Review Summary + operationId: review_summary_review_summary_get + parameters: + - name: cameras + in: query + required: false + schema: + type: string + default: all + title: Cameras + - name: labels + in: query + required: false + schema: + type: string + default: all + title: Labels + - name: zones + in: query + required: false + schema: + type: string + default: all + title: Zones + - name: timezone + in: query + required: false + schema: + type: string + default: utc + title: Timezone + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ReviewSummaryResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /reviews/viewed: + post: + tags: + - Review + summary: Set Multiple Reviewed + operationId: set_multiple_reviewed_reviews_viewed_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ReviewModifyMultipleBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /reviews/delete: + post: + tags: + - Review + summary: Delete Reviews + operationId: delete_reviews_reviews_delete_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ReviewModifyMultipleBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' + /review/activity/motion: + get: + tags: + - Review + summary: Motion Activity + description: |- + **Access:** Any authenticated user. + + Get motion and audio activity. + operationId: motion_activity_review_activity_motion_get + parameters: + - name: cameras + in: query + required: false + schema: + type: string + default: all + title: Cameras + - name: before + in: query + required: false + schema: + type: number + title: Before + - name: after + in: query + required: false + schema: + type: number + title: After + - name: scale + in: query + required: false + schema: + type: integer + default: 30 + title: Scale + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ReviewActivityMotionResponse' + title: Response Motion Activity Review Activity Motion Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /review/event/{event_id}: + get: + tags: + - Review + summary: Get Review From Event + operationId: get_review_from_event_review_event__event_id__get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ReviewSegmentResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /review/{review_id}: + get: + tags: + - Review + summary: Get Review + operationId: get_review_review__review_id__get + parameters: + - name: review_id + in: path + required: true + schema: + type: string + title: Review Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ReviewSegmentResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /review/{review_id}/viewed: + delete: + tags: + - Review + summary: Set Not Reviewed + operationId: set_not_reviewed_review__review_id__viewed_delete + parameters: + - name: review_id + in: path + required: true + schema: + type: string + title: Review Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /review/summarize/start/{start_ts}/end/{end_ts}: + post: + tags: + - Review + summary: Generate Review Summary + description: |- + **Access:** Admin role required. + + Use GenAI to summarize review items over a period of time. + operationId: + generate_review_summary_review_summarize_start__start_ts__end__end_ts__post + parameters: + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /: + get: + tags: + - App + summary: Is Healthy + operationId: is_healthy__get + responses: + '200': + description: Successful Response + content: + text/plain: + schema: + type: string + security: [] + x-required-role: public + description: '**Access:** Public — no authentication required.' + /config/schema.json: + get: + tags: + - App + summary: Config Schema + operationId: config_schema_config_schema_json_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: [] + x-required-role: public + description: '**Access:** Public — no authentication required.' + /version: + get: + tags: + - App + summary: Version + operationId: version_version_get + responses: + '200': + description: Successful Response + content: + text/plain: + schema: + type: string + security: [] + x-required-role: public + description: '**Access:** Public — no authentication required.' + /stats: + get: + tags: + - App + summary: Stats + operationId: stats_stats_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /stats/history: + get: + tags: + - App + summary: Stats History + operationId: stats_history_stats_history_get + parameters: + - name: keys + in: query + required: false + schema: + type: string + title: Keys + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' + /metrics: + get: + tags: + - App + summary: Metrics + description: |- + **Access:** Any authenticated user. + + Expose Prometheus metrics endpoint and update metrics with latest stats + operationId: metrics_metrics_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + /genai/models: + get: + tags: + - App + summary: List available GenAI models + description: |- + **Access:** Admin role required. + + Returns available models for each configured GenAI provider. + operationId: genai_models_genai_models_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + /genai/probe: + post: + tags: + - App + summary: Probe a GenAI provider without saving config + description: |- + **Access:** Admin role required. + + Builds a transient client from the request body and returns its available models. Used to validate provider credentials in the UI before saving the configuration. + operationId: genai_probe_genai_probe_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenAIProbeBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /config: + get: + tags: + - App + summary: Config + operationId: config_config_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /profiles: + get: + tags: + - App + summary: Get Profiles + description: |- + **Access:** Any authenticated user. + + List all available profiles and the currently active profile. + operationId: get_profiles_profiles_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + /profile/active: + get: + tags: + - App + summary: Get Active Profile + description: |- + **Access:** Admin role required. + + Get the currently active profile. + operationId: get_active_profile_profile_active_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + /ffmpeg/presets: + get: + tags: + - App + summary: Ffmpeg Presets + description: |- + **Access:** Admin role required. + + Return available ffmpeg preset keys for config UI usage. + operationId: ffmpeg_presets_ffmpeg_presets_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + /config/raw_paths: + get: + tags: + - App + summary: Config Raw Paths + description: |- + **Access:** Admin role required. + + Admin-only endpoint that returns camera paths and go2rtc streams without credential masking. + operationId: config_raw_paths_config_raw_paths_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + /config/raw: + get: + tags: + - App + summary: Config Raw + operationId: config_raw_config_raw_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' + /config/save: + post: + tags: + - App + summary: Config Save + operationId: config_save_config_save_post + parameters: + - name: save_option + in: query + required: true + schema: + type: string + title: Save Option + requestBody: + required: true + content: + text/plain: + schema: + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' + /config/set: + put: + tags: + - App + summary: Config Set + operationId: config_set_config_set_put + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AppConfigSetBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' + /vainfo: + get: + tags: + - App + summary: Vainfo + operationId: vainfo_vainfo_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /nvinfo: + get: + tags: + - App + summary: Nvinfo + operationId: nvinfo_nvinfo_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /logs/{service}: + get: + tags: + - App + - Logs + summary: Logs + description: |- + **Access:** Admin role required. + + Get logs for the requested service (frigate/nginx/go2rtc) + operationId: logs_logs__service__get + parameters: + - name: service + in: path + required: true + schema: + type: string + enum: + - frigate + - nginx + - go2rtc + title: Service + - name: download + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Download + - name: stream + in: query + required: false + schema: + anyOf: + - type: boolean + - type: 'null' + default: false + title: Stream + - name: start + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + default: 0 + title: Start + - name: end + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: End + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /restart: + post: + tags: + - App + summary: Restart + operationId: restart_restart_post + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' + /media/sync: + post: + tags: + - App + summary: Start media sync job + description: |- + **Access:** Admin role required. + + Start an asynchronous media sync job to find and (optionally) remove orphaned media files. + Returns 202 with job details when queued, or 409 if a job is already running. + operationId: sync_media_media_sync_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MediaSyncBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /media/sync/current: + get: + tags: + - App + summary: Get current media sync job + description: |- + **Access:** Admin role required. + + Retrieve the current running media sync job, if any. Returns the job details + or null when no job is active. + operationId: get_media_sync_current_media_sync_current_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + /media/sync/status/{job_id}: + get: + tags: + - App + summary: Get media sync job status + description: |- + **Access:** Admin role required. + + Get status and results for the specified media sync job id. Returns 200 with + job details including results, or 404 if the job is not found. + operationId: get_media_sync_status_media_sync_status__job_id__get + parameters: + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /labels: + get: + tags: + - App + summary: Get Labels + operationId: get_labels_labels_get + parameters: + - name: camera + in: query + required: false + schema: + type: string + default: '' + title: Camera + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /sub_labels: + get: + tags: + - App + summary: Get Sub Labels + operationId: get_sub_labels_sub_labels_get + parameters: + - name: split_joined + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Split Joined + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /audio_labels: + get: + tags: + - App + summary: Get Audio Labels + operationId: get_audio_labels_audio_labels_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' + /plus/models: + get: + tags: + - App + summary: Plusmodels + operationId: plusModels_plus_models_get + parameters: + - name: filterByCurrentModelDetector + in: query + required: false + schema: + type: boolean + default: false + title: Filterbycurrentmodeldetector + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /recognized_license_plates: + get: + tags: + - App + summary: Get Recognized License Plates + operationId: get_recognized_license_plates_recognized_license_plates_get + parameters: + - name: split_joined + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Split Joined + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /timeline: + get: + tags: + - App + summary: Timeline + operationId: timeline_timeline_get + parameters: + - name: camera + in: query + required: false + schema: + type: string + default: all + title: Camera + - name: limit + in: query + required: false + schema: + type: integer + default: 100 + title: Limit + - name: source_id + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Source Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /timeline/hourly: + get: + tags: + - App + summary: Hourly Timeline + description: |- + **Access:** Any authenticated user. + + Get hourly summary for timeline. + operationId: hourly_timeline_timeline_hourly_get + parameters: + - name: cameras + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Cameras + - name: labels + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Labels + - name: after + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: After + - name: before + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Before + - name: limit + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + default: 200 + title: Limit + - name: timezone + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: utc + title: Timezone + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /preview/{camera_name}/start/{start_ts}/end/{end_ts}: + get: + tags: + - Preview + summary: Get preview clips for time range + description: |- + **Access:** Any authenticated user. + + Gets all preview clips for a specified camera and time range. + Returns a list of preview video clips that overlap with the requested time period, + ordered by start time. Use camera_name='all' to get previews from all cameras. + Returns an error if no previews are found. + operationId: + preview_ts_preview__camera_name__start__start_ts__end__end_ts__get + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PreviewModel' + title: Response Preview Ts Preview Camera Name Start Start Ts + End End Ts Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}: + get: + tags: + - Preview + summary: Get preview clips for specific hour + description: |- + **Access:** Any authenticated user. + + Gets all preview clips for a specific hour in a given timezone. + Converts the provided date/time from the specified timezone to UTC and retrieves + all preview clips for that hour. Use camera_name='all' to get previews from all cameras. + The tz_name should be a timezone like 'America/New_York' (use commas instead of slashes). + operationId: + preview_hour_preview__year_month___day___hour___camera_name___tz_name__get + parameters: + - name: year_month + in: path + required: true + schema: + type: string + title: Year Month + - name: day + in: path + required: true + schema: + type: integer + title: Day + - name: hour + in: path + required: true + schema: + type: integer + title: Hour + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: tz_name + in: path + required: true + schema: + type: string + title: Tz Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PreviewModel' + title: Response Preview Hour Preview Year Month Day Hour + Camera Name Tz Name Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames: + get: + tags: + - Preview + summary: Get cached preview frame filenames + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Gets a list of cached preview frame filenames for a specific camera and time range. + Returns an array of filenames for preview frames that fall within the specified time period, + sorted in chronological order. These are individual frame images cached for quick preview display. + operationId: + get_preview_frames_from_cache_preview__camera_name__start__start_ts__end__end_ts__frames_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + type: string + title: Response Get Preview Frames From Cache Preview Camera + Name Start Start Ts End End Ts Frames Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /notifications/pubkey: + get: + tags: + - Notifications + summary: Get VAPID public key + description: |- + **Access:** Any authenticated user. + + Gets the VAPID public key for the notifications. + Returns the public key or an error if notifications are not enabled. + operationId: get_vapid_pub_key_notifications_pubkey_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + /notifications/register: + post: + tags: + - Notifications + summary: Register notifications + description: |- + **Access:** Any authenticated user. + + Registers a notifications subscription. + Returns a success message or an error if the subscription is not provided. + operationId: register_notifications_notifications_register_post + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /exports: + get: + tags: + - Export + summary: Get exports + description: |- + **Access:** Any authenticated user. + + Gets all exports from the database for cameras the user has access to. + Returns a list of exports ordered by date (most recent first). + operationId: get_exports_exports_get + parameters: + - name: export_case_id + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Export Case Id + - name: cameras + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Cameras + - name: start_date + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Start Date + - name: end_date + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: End Date + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ExportModel' + title: Response Get Exports Exports Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /cases: + get: + tags: + - Export + summary: Get export cases + description: |- + **Access:** Any authenticated user. + + Gets all export cases from the database. + operationId: get_export_cases_cases_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ExportCaseModel' + title: Response Get Export Cases Cases Get + security: + - frigateUserAuth: [] + x-required-role: any + post: + tags: + - Export + summary: Create export case + description: |- + **Access:** Admin role required. + + Creates a new export case. + operationId: create_export_case_cases_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportCaseCreateBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ExportCaseModel' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /cases/{case_id}: + get: + tags: + - Export + summary: Get a single export case + description: |- + **Access:** Any authenticated user. + + Gets a specific export case by ID. + operationId: get_export_case_cases__case_id__get + parameters: + - name: case_id + in: path + required: true + schema: + type: string + title: Case Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ExportCaseModel' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + patch: + tags: + - Export + summary: Update export case + description: |- + **Access:** Admin role required. + + Updates an existing export case. + operationId: update_export_case_cases__case_id__patch + parameters: + - name: case_id + in: path + required: true + schema: + type: string + title: Case Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportCaseUpdateBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + delete: + tags: + - Export + summary: Delete export case + description: |- + **Access:** Admin role required. + + Deletes an export case. + Exports that reference this case will have their export_case set to null. + operationId: delete_export_case_cases__case_id__delete + parameters: + - name: case_id + in: path + required: true + schema: + type: string + title: Case Id + - name: delete_exports + in: query + required: false + schema: + type: boolean + default: false + title: Delete Exports + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /cases/{case_id}/download: + get: + tags: + - Export + summary: Download export case as zip + description: |- + **Access:** Any authenticated user. + + Streams a zip archive containing every completed export's mp4 for the given case. + operationId: download_export_case_cases__case_id__download_get + parameters: + - name: case_id + in: path + required: true + schema: + type: string + title: Case Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /jobs/export: + get: + tags: + - Export + summary: Get active export jobs + description: |- + **Access:** Any authenticated user. + + Gets queued and running export jobs. + operationId: get_active_export_jobs_jobs_export_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ExportJobModel' + title: Response Get Active Export Jobs Jobs Export Get + security: + - frigateUserAuth: [] + x-required-role: any + /jobs/export/{export_id}: + get: + tags: + - Export + summary: Get export job status + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Gets queued, running, or completed status for a specific export job. + operationId: get_export_job_status_jobs_export__export_id__get + parameters: + - name: export_id + in: path + required: true + schema: + type: string + title: Export Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ExportJobModel' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /exports/batch: + post: + tags: + - Export + summary: Start recording export batch + description: |- + **Access:** Any authenticated user. + + Starts recording exports for a batch of items, each with its own camera and time range, and assigns them to a single export case. Attaching to an existing case is temporarily admin-only until case-level ACLs exist. + operationId: export_recordings_batch_exports_batch_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BatchExportBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/BatchExportResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /export/{camera_name}/start/{start_time}/end/{end_time}: + post: + tags: + - Export + summary: Start recording export + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Starts an export of a recording for the specified time range. + The export can be from recordings or preview footage. Returns the export ID if + successful, or an error message if the camera is invalid or no recordings/previews + are found for the time range. + operationId: + export_recording_export__camera_name__start__start_time__end__end_time__post + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: start_time + in: path + required: true + schema: + type: number + title: Start Time + - name: end_time + in: path + required: true + schema: + type: number + title: End Time + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportRecordingsBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/StartExportResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /export/{event_id}/rename: + patch: + tags: + - Export + summary: Rename export + description: |- + **Access:** Admin role required. + + Renames an export. + NOTE: This changes the friendly name of the export, not the filename. + operationId: export_rename_export__event_id__rename_patch + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportRenameBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /export/custom/{camera_name}/start/{start_time}/end/{end_time}: + post: + tags: + - Export + summary: Start custom recording export + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Starts an export of a recording for the specified time range using custom FFmpeg arguments. + The export can be from recordings or preview footage. Returns the export ID if + successful, or an error message if the camera is invalid or no recordings/previews + are found for the time range. If ffmpeg_input_args and ffmpeg_output_args are not provided, + defaults to timelapse export settings. + operationId: + export_recording_custom_export_custom__camera_name__start__start_time__end__end_time__post + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: start_time + in: path + required: true + schema: + type: number + title: Start Time + - name: end_time + in: path + required: true + schema: + type: number + title: End Time + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportRecordingsCustomBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/StartExportResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /exports/{export_id}: + get: + tags: + - Export + summary: Get a single export + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Gets a specific export by ID. The user must have access to the camera + associated with the export. + operationId: get_export_exports__export_id__get + parameters: + - name: export_id + in: path + required: true + schema: + type: string + title: Export Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ExportModel' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /exports/delete: + post: + tags: + - Export + summary: Bulk delete exports + description: |- + **Access:** Admin role required. + + Deletes one or more exports by ID. All IDs must exist and none can be in-progress. + operationId: bulk_delete_exports_exports_delete_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportBulkDeleteBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /exports/reassign: + post: + tags: + - Export + summary: Bulk reassign exports to a case + description: |- + **Access:** Admin role required. + + Assigns or unassigns one or more exports to/from a case. All IDs must exist. + operationId: bulk_reassign_exports_exports_reassign_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportBulkReassignBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events: + get: + tags: + - Events + summary: Get events + description: |- + **Access:** Any authenticated user. + + Returns a list of events. + operationId: events_events_get + parameters: + - name: camera + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Camera + - name: cameras + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Cameras + - name: label + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Label + - name: labels + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Labels + - name: sub_label + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Sub Label + - name: sub_labels + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Sub Labels + - name: attributes + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Attributes + - name: zone + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Zone + - name: zones + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Zones + - name: limit + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + default: 100 + title: Limit + - name: after + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: After + - name: before + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Before + - name: time_range + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: 00:00,24:00 + title: Time Range + - name: has_clip + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Has Clip + - name: has_snapshot + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Has Snapshot + - name: in_progress + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: In Progress + - name: include_thumbnails + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + default: 1 + title: Include Thumbnails + - name: favorites + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Favorites + - name: min_score + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Min Score + - name: max_score + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Max Score + - name: min_speed + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Min Speed + - name: max_speed + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Max Speed + - name: recognized_license_plate + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Recognized License Plate + - name: is_submitted + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Is Submitted + - name: min_length + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Min Length + - name: max_length + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Max Length + - name: event_id + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Event Id + - name: sort + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Sort + - name: timezone + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: utc + title: Timezone + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EventResponse' + title: Response Events Events Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /events/explore: + get: + tags: + - Events + summary: Get summary of objects + description: |- + **Access:** Any authenticated user. + + Gets a summary of objects from the database. + Returns a list of objects with a max of `limit` objects for each label. + operationId: events_explore_events_explore_get + parameters: + - name: limit + in: query + required: false + schema: + type: integer + default: 10 + title: Limit + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EventResponse' + title: Response Events Explore Events Explore Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /event_ids: + get: + tags: + - Events + summary: Get events by ids + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Gets events by a list of ids. + Returns a list of events. + operationId: event_ids_event_ids_get + parameters: + - name: ids + in: query + required: true + schema: + type: string + title: Ids + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EventResponse' + title: Response Event Ids Event Ids Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /events/search: + get: + tags: + - Events + summary: Search events + description: |- + **Access:** Any authenticated user. + + Searches for events in the database. + Returns a list of events. + operationId: events_search_events_search_get + parameters: + - name: query + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Query + - name: event_id + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Event Id + - name: search_type + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: thumbnail + title: Search Type + - name: include_thumbnails + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + default: 1 + title: Include Thumbnails + - name: limit + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + default: 50 + title: Limit + - name: cameras + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Cameras + - name: labels + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Labels + - name: sub_labels + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Sub Labels + - name: attributes + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Attributes + - name: zones + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Zones + - name: after + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: After + - name: before + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Before + - name: time_range + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: 00:00,24:00 + title: Time Range + - name: has_clip + in: query + required: false + schema: + anyOf: + - type: boolean + - type: 'null' + title: Has Clip + - name: has_snapshot + in: query + required: false + schema: + anyOf: + - type: boolean + - type: 'null' + title: Has Snapshot + - name: is_submitted + in: query + required: false + schema: + anyOf: + - type: boolean + - type: 'null' + title: Is Submitted + - name: timezone + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: utc + title: Timezone + - name: min_score + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Min Score + - name: max_score + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Max Score + - name: min_speed + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Min Speed + - name: max_speed + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Max Speed + - name: recognized_license_plate + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Recognized License Plate + - name: sort + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Sort + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /events/summary: + get: + tags: + - Events + summary: Events Summary + operationId: events_summary_events_summary_get + parameters: + - name: timezone + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: utc + title: Timezone + - name: has_clip + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Has Clip + - name: has_snapshot + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Has Snapshot + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /events/{event_id}: + get: + tags: + - Events + summary: Get event by id + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Gets an event by its id. + operationId: event_events__event_id__get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/EventResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + delete: + tags: + - Events + summary: Delete event + description: |- + **Access:** Admin role required. + + Deletes an event from the database. + Returns a success message or an error if the event is not found. + operationId: delete_event_events__event_id__delete + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/retain: + post: + tags: + - Events + summary: Set event retain indefinitely. + description: |- + **Access:** Admin role required. + + Sets an event to retain indefinitely. + Returns a success message or an error if the event is not found. + NOTE: This is a legacy endpoint and is not supported in the frontend. + operationId: set_retain_events__event_id__retain_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + delete: + tags: + - Events + summary: Stop event from being retained indefinitely + description: |- + **Access:** Admin role required. + + Stops an event from being retained indefinitely. + Returns a success message or an error if the event is not found. + NOTE: This is a legacy endpoint and is not supported in the frontend. + operationId: delete_retain_events__event_id__retain_delete + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/plus: + post: + tags: + - Events + summary: Send event to Frigate+ + description: |- + **Access:** Admin role required. + + Sends an event to Frigate+. + Returns a success message or an error if the event is not found. + operationId: send_to_plus_events__event_id__plus_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SubmitPlusBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/EventUploadPlusResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/false_positive: + put: + tags: + - Events + summary: Submit false positive to Frigate+ + description: |- + **Access:** Admin role required. + + Submit an event as a false positive to Frigate+. + This endpoint is the same as the standard Frigate+ submission endpoint, + but is specifically for marking an event as a false positive. + operationId: false_positive_events__event_id__false_positive_put + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/EventUploadPlusResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/sub_label: + post: + tags: + - Events + summary: Set event sub label + description: |- + **Access:** Admin role required. + + Sets an event's sub label. + Returns a success message or an error if the event is not found. + operationId: set_sub_label_events__event_id__sub_label_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EventsSubLabelBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/recognized_license_plate: + post: + tags: + - Events + summary: Set event license plate + description: |- + **Access:** Admin role required. + + Sets an event's license plate. + Returns a success message or an error if the event is not found. + operationId: set_plate_events__event_id__recognized_license_plate_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EventsLPRBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/attributes: + post: + tags: + - Events + summary: Set custom classification attributes + description: |- + **Access:** Admin role required. + + Sets an event's custom classification attributes for all attribute-type models that apply to the event's object type. + operationId: set_attributes_events__event_id__attributes_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EventsAttributesBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/description: + post: + tags: + - Events + summary: Set event description + description: |- + **Access:** Admin role required. + + Sets an event's description. + Returns a success message or an error if the event is not found. + operationId: set_description_events__event_id__description_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EventsDescriptionBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/description/regenerate: + put: + tags: + - Events + summary: Regenerate event description + description: |- + **Access:** Admin role required. + + Regenerates an event's description. + Returns a success message or an error if the event is not found. + operationId: + regenerate_description_events__event_id__description_regenerate_put + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: source + in: query + required: false + schema: + anyOf: + - $ref: '#/components/schemas/RegenerateDescriptionEnum' + - type: 'null' + default: thumbnails + title: Source + - name: force + in: query + required: false + schema: + anyOf: + - type: boolean + - type: 'null' + default: false + title: Force + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /description/generate: + post: + tags: + - Events + summary: Generate description embedding + description: |- + **Access:** Admin role required. + + Generates an embedding for an event's description. + Returns a success message or an error if the event is not found. + operationId: generate_description_embedding_description_generate_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EventsDescriptionBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/: + delete: + tags: + - Events + summary: Delete events + description: |- + **Access:** Admin role required. + + Deletes a list of events from the database. + Returns a success message or an error if the events are not found. + operationId: delete_events_events__delete + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EventsDeleteBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/EventMultiDeleteResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{camera_name}/{label}/create: + post: + tags: + - Events + summary: Create manual event + description: |- + **Access:** Admin role required. + + Creates a manual event in the database. + Returns a success message or an error if the event is not found. + NOTES: + - Creating a manual event does not trigger an update to /events MQTT topic. + - If a duration is set to null, the event will need to be ended manually by calling /events/{event_id}/end. + operationId: create_event_events__camera_name___label__create_post + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: label + in: path + required: true + schema: + type: string + title: Label + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EventsCreateBody' + default: + score: 0.0 + duration: 30 + include_recording: true + draw: {} + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/EventCreateResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/end: + put: + tags: + - Events + summary: End manual event + description: |- + **Access:** Admin role required. + + Ends a manual event. + Returns a success message or an error if the event is not found. + NOTE: This should only be used for manual events. + operationId: end_event_events__event_id__end_put + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EventsEndBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /trigger/embedding: + post: + tags: + - Events + summary: Create trigger embedding + description: |- + **Access:** Admin role required. + + Creates a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + operationId: create_trigger_embedding_trigger_embedding_post + parameters: + - name: camera_name + in: query + required: true + schema: + type: string + title: Camera Name + - name: name + in: query + required: true + schema: + type: string + title: Name + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TriggerEmbeddingBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + title: Response Create Trigger Embedding Trigger Embedding Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /trigger/embedding/{camera_name}/{name}: + put: + tags: + - Events + summary: Update trigger embedding + description: |- + **Access:** Admin role required. + + Updates a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + operationId: + update_trigger_embedding_trigger_embedding__camera_name___name__put + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TriggerEmbeddingBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + title: Response Update Trigger Embedding Trigger Embedding + Camera Name Name Put + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + delete: + tags: + - Events + summary: Delete trigger embedding + description: |- + **Access:** Admin role required. + + Deletes a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + operationId: + delete_trigger_embedding_trigger_embedding__camera_name___name__delete + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + title: Response Delete Trigger Embedding Trigger Embedding + Camera Name Name Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /triggers/status/{camera_name}: + get: + tags: + - Events + summary: Get triggers status + description: |- + **Access:** Admin role required. + + Gets the status of all triggers for a specific camera. + Returns a success message or an error if the camera is not found. + operationId: get_triggers_status_triggers_status__camera_name__get + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + title: Response Get Triggers Status Triggers Status Camera Name + Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /{camera_name}: + get: + tags: + - Media + summary: Mjpeg Feed + operationId: mjpeg_feed__camera_name__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: fps + in: query + required: false + schema: + type: integer + default: 3 + title: Fps + - name: height + in: query + required: false + schema: + type: integer + default: 360 + title: Height + - name: bbox + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Bbox + - name: timestamp + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Timestamp + - name: zones + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Zones + - name: mask + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Mask + - name: motion + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Motion + - name: regions + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Regions + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/ptz/info: + get: + tags: + - Media + summary: Camera Ptz Info + operationId: camera_ptz_info__camera_name__ptz_info_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/latest.{extension}: + get: + tags: + - Media + summary: Latest Frame + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns the latest frame from the specified camera in the requested format (jpg, png, webp). Falls back to preview frames if the camera is offline. + operationId: latest_frame__camera_name__latest__extension__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: extension + in: path + required: true + schema: + $ref: '#/components/schemas/Extension' + - name: bbox + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Bbox + - name: timestamp + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Timestamp + - name: zones + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Zones + - name: mask + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Mask + - name: motion + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Motion + - name: paths + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Paths + - name: regions + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Regions + - name: quality + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + default: 70 + title: Quality + - name: height + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Height + - name: store + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Store + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/recordings/{frame_time}/snapshot.{format}: + get: + tags: + - Media + summary: Get Snapshot From Recording + operationId: + get_snapshot_from_recording__camera_name__recordings__frame_time__snapshot__format__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: frame_time + in: path + required: true + schema: + type: number + title: Frame Time + - name: format + in: path + required: true + schema: + type: string + enum: + - png + - jpg + title: Format + - name: height + in: query + required: false + schema: + type: integer + title: Height + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/plus/{frame_time}: + post: + tags: + - Media + summary: Submit Recording Snapshot To Plus + operationId: + submit_recording_snapshot_to_plus__camera_name__plus__frame_time__post + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: frame_time + in: path + required: true + schema: + type: string + title: Frame Time + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4: + get: + tags: + - Media + summary: Recording Clip + description: |- + **Access:** Authenticated user with access to the referenced camera. + + For iOS devices, use the master.m3u8 HLS link instead of clip.mp4. Safari does not reliably process progressive mp4 files. + operationId: + recording_clip__camera_name__start__start_ts__end__end_ts__clip_mp4_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /vod/{camera_name}/start/{start_ts}/end/{end_ts}: + get: + tags: + - Media + summary: Vod Ts + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback. + operationId: vod_ts_vod__camera_name__start__start_ts__end__end_ts__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + - name: force_discontinuity + in: query + required: false + schema: + type: boolean + default: false + title: Force Discontinuity + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /vod/{year_month}/{day}/{hour}/{camera_name}: + get: + tags: + - Media + summary: Vod Hour No Timezone + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns an HLS playlist for the specified date-time on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback. + operationId: + vod_hour_no_timezone_vod__year_month___day___hour___camera_name__get + parameters: + - name: year_month + in: path + required: true + schema: + type: string + title: Year Month + - name: day + in: path + required: true + schema: + type: integer + title: Day + - name: hour + in: path + required: true + schema: + type: integer + title: Hour + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /vod/{year_month}/{day}/{hour}/{camera_name}/{tz_name}: + get: + tags: + - Media + summary: Vod Hour + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns an HLS playlist for the specified date-time (with timezone) on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback. + operationId: + vod_hour_vod__year_month___day___hour___camera_name___tz_name__get + parameters: + - name: year_month + in: path + required: true + schema: + type: string + title: Year Month + - name: day + in: path + required: true + schema: + type: integer + title: Day + - name: hour + in: path + required: true + schema: + type: integer + title: Hour + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: tz_name + in: path + required: true + schema: + type: string + title: Tz Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /vod/event/{event_id}: + get: + tags: + - Media + summary: Vod Event + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns an HLS playlist for the specified object. Append /master.m3u8 or /index.m3u8 for HLS playback. + operationId: vod_event_vod_event__event_id__get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: padding + in: query + required: false + schema: + type: integer + description: Padding to apply to the vod. + default: 0 + title: Padding + description: Padding to apply to the vod. + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /vod/clip/{camera_name}/start/{start_ts}/end/{end_ts}: + get: + tags: + - Media + summary: Vod Clip + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns an HLS playlist for a timestamp range with HLS discontinuity enabled. Append /master.m3u8 or /index.m3u8 for HLS playback. + operationId: + vod_clip_vod_clip__camera_name__start__start_ts__end__end_ts__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /events/{event_id}/snapshot.jpg: + get: + tags: + - Media + summary: Event Snapshot + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns a snapshot image for the specified object id. + operationId: event_snapshot_events__event_id__snapshot_jpg_get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: download + in: query + required: false + schema: + anyOf: + - type: boolean + - type: 'null' + default: false + title: Download + - name: timestamp + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Timestamp + - name: bbox + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Bbox + - name: crop + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Crop + - name: height + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Height + - name: quality + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Quality + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /events/{event_id}/thumbnail.{extension}: + get: + tags: + - Media + summary: Event Thumbnail + operationId: event_thumbnail_events__event_id__thumbnail__extension__get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: extension + in: path + required: true + schema: + $ref: '#/components/schemas/Extension' + - name: max_cache_age + in: query + required: false + schema: + type: integer + description: Max cache age in seconds. Default 30 days in seconds. + default: 2592000 + title: Max Cache Age + description: Max cache age in seconds. Default 30 days in seconds. + - name: format + in: query + required: false + schema: + type: string + enum: + - ios + - android + default: ios + title: Format + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/grid.jpg: + get: + tags: + - Media + summary: Grid Snapshot + operationId: grid_snapshot__camera_name__grid_jpg_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: color + in: query + required: false + schema: + type: string + default: green + title: Color + - name: font_scale + in: query + required: false + schema: + type: number + default: 0.5 + title: Font Scale + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/region_grid: + delete: + tags: + - Media + summary: Clear Region Grid + description: |- + **Access:** Admin role required. + + Clear the region grid for a camera. + operationId: clear_region_grid__camera_name__region_grid_delete + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/snapshot-clean.webp: + get: + tags: + - Media + summary: Event Snapshot Clean + operationId: + event_snapshot_clean_events__event_id__snapshot_clean_webp_get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: download + in: query + required: false + schema: + type: boolean + default: false + title: Download + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /events/{event_id}/clip.mp4: + get: + tags: + - Media + summary: Event Clip + operationId: event_clip_events__event_id__clip_mp4_get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: padding + in: query + required: false + schema: + type: integer + description: Padding to apply to clip. + default: 0 + title: Padding + description: Padding to apply to clip. + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /review/{review_id}/clip.mp4: + get: + tags: + - Media + summary: Review Clip + operationId: review_clip_review__review_id__clip_mp4_get + parameters: + - name: review_id + in: path + required: true + schema: + type: string + title: Review Id + - name: padding + in: query + required: false + schema: + type: integer + description: Padding to apply to clip. + default: 0 + title: Padding + description: Padding to apply to clip. + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /events/{event_id}/preview.gif: + get: + tags: + - Media + summary: Event Preview + operationId: event_preview_events__event_id__preview_gif_get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif: + get: + tags: + - Media + summary: Preview Gif + operationId: + preview_gif__camera_name__start__start_ts__end__end_ts__preview_gif_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + - name: max_cache_age + in: query + required: false + schema: + type: integer + description: Max cache age in seconds. Default 30 days in seconds. + default: 2592000 + title: Max Cache Age + description: Max cache age in seconds. Default 30 days in seconds. + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4: + get: + tags: + - Media + summary: Preview Mp4 + operationId: + preview_mp4__camera_name__start__start_ts__end__end_ts__preview_mp4_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + - name: max_cache_age + in: query + required: false + schema: + type: integer + description: Max cache age in seconds. Default 7 days in seconds. + default: 604800 + title: Max Cache Age + description: Max cache age in seconds. Default 7 days in seconds. + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /review/{event_id}/preview: + get: + tags: + - Media + summary: Review Preview + operationId: review_preview_review__event_id__preview_get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: format + in: query + required: false + schema: + type: string + enum: + - gif + - mp4 + default: gif + title: Format + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /preview/{file_name}/thumbnail.webp: + get: + tags: + - Media + summary: Preview Thumbnail + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Get a thumbnail from the cached preview frames. + operationId: preview_thumbnail_preview__file_name__thumbnail_webp_get + parameters: + - name: file_name + in: path + required: true + schema: + type: string + title: File Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /preview/{file_name}/thumbnail.jpg: + get: + tags: + - Media + summary: Preview Thumbnail + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Get a thumbnail from the cached preview frames. + operationId: preview_thumbnail_preview__file_name__thumbnail_jpg_get + parameters: + - name: file_name + in: path + required: true + schema: + type: string + title: File Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/{label}/thumbnail.jpg: + get: + tags: + - Media + summary: Label Thumbnail + operationId: label_thumbnail__camera_name___label__thumbnail_jpg_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: label + in: path + required: true + schema: + type: string + title: Label + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/{label}/best.jpg: + get: + tags: + - Media + summary: Label Thumbnail + operationId: label_thumbnail__camera_name___label__best_jpg_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: label + in: path + required: true + schema: + type: string + title: Label + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/{label}/clip.mp4: + get: + tags: + - Media + summary: Label Clip + operationId: label_clip__camera_name___label__clip_mp4_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: label + in: path + required: true + schema: + type: string + title: Label + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/{label}/snapshot.jpg: + get: + tags: + - Media + summary: Label Snapshot + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns the snapshot image from the latest event for the given camera and label combo + operationId: label_snapshot__camera_name___label__snapshot_jpg_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: label + in: path + required: true + schema: + type: string + title: Label + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/search/motion: + post: + tags: + - Motion Search + summary: Start motion search job + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Starts an asynchronous search for significant motion changes within + a user-defined Region of Interest (ROI) over a specified time range. Returns a job_id + that can be used to poll for results. + operationId: start_motion_search__camera_name__search_motion_post + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MotionSearchRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/MotionSearchStartResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/search/motion/{job_id}: + get: + tags: + - Motion Search + summary: Get motion search job status + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns the status and results (if complete) of a motion search job. + operationId: + get_motion_search_status_endpoint__camera_name__search_motion__job_id__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/MotionSearchStatusResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/search/motion/{job_id}/cancel: + post: + tags: + - Motion Search + summary: Cancel motion search job + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Cancels an active motion search job if it is still processing. + operationId: + cancel_motion_search_endpoint__camera_name__search_motion__job_id__cancel_post + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: job_id + in: path + required: true + schema: + type: string + title: Job Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /recordings/storage: + get: + tags: + - Recordings + summary: Get Recordings Storage Usage + operationId: get_recordings_storage_usage_recordings_storage_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' + /recordings/summary: + get: + tags: + - Recordings + summary: All Recordings Summary + description: |- + **Access:** Any authenticated user. + + Returns true/false by day indicating if recordings exist + operationId: all_recordings_summary_recordings_summary_get + parameters: + - name: timezone + in: query + required: false + schema: + type: string + default: utc + title: Timezone + - name: cameras + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Cameras + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /{camera_name}/recordings/summary: + get: + tags: + - Recordings + summary: Recordings Summary + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns hourly summary for recordings of given camera + operationId: recordings_summary__camera_name__recordings_summary_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: timezone + in: query + required: false + schema: + type: string + default: utc + title: Timezone + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/recordings: + get: + tags: + - Recordings + summary: Recordings + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Return specific camera recordings between the given 'after'/'end' times. If not provided the last hour will be used + operationId: recordings__camera_name__recordings_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Camera Name + - name: after + in: query + required: false + schema: + type: number + title: After + - name: before + in: query + required: false + schema: + type: number + title: Before + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /recordings/unavailable: + get: + tags: + - Recordings + summary: No Recordings + description: |- + **Access:** Any authenticated user. + + Get time ranges with no recordings. + operationId: no_recordings_recordings_unavailable_get + parameters: + - name: cameras + in: query + required: false + schema: + type: string + default: all + title: Cameras + - name: before + in: query + required: false + schema: + type: number + title: Before + - name: after + in: query + required: false + schema: + type: number + title: After + - name: scale + in: query + required: false + schema: + type: integer + default: 30 + title: Scale + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + type: object + title: Response No Recordings Recordings Unavailable Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /recordings/start/{start}/end/{end}: + delete: + tags: + - Recordings + summary: Delete recordings + description: |- + **Access:** Admin role required. + + Deletes recordings within the specified time range. + Recordings can be filtered by cameras and kept based on motion, objects, or audio attributes. + operationId: delete_recordings_recordings_start__start__end__end__delete + parameters: + - name: start + in: path + required: true + schema: + type: number + description: Start timestamp (unix) + title: Start + description: Start timestamp (unix) + - name: end + in: path + required: true + schema: + type: number + description: End timestamp (unix) + title: End + description: End timestamp (unix) + - name: keep + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Keep + - name: cameras + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + default: all + title: Cameras + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /debug_replay/start: + post: + tags: + - App + summary: Start debug replay + description: |- + **Access:** Admin role required. + + Start a debug replay session from camera recordings. Returns immediately while clip generation runs as a background job; subscribe to the 'debug_replay' job_state WS topic to track progress. + operationId: start_debug_replay_debug_replay_start_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DebugReplayStartBody' + responses: + '202': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DebugReplayStartResponse' + '400': + description: Invalid camera, time range, or no recordings + '409': + description: A replay session is already active + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /debug_replay/start_from_export: + post: + tags: + - App + summary: Start debug replay from an export + description: |- + **Access:** Admin role required. + + Start a debug replay session covering an existing export's time range. The end time is derived from the export's video duration. + operationId: + start_debug_replay_from_export_debug_replay_start_from_export_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DebugReplayStartFromExportBody' + responses: + '202': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DebugReplayStartResponse' + '400': + description: Invalid export, time range, or no recordings + '404': + description: Export not found + '409': + description: A replay session is already active + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /debug_replay/status: + get: + tags: + - App + summary: Get debug replay status + description: |- + **Access:** Admin role required. + + Get the status of the current debug replay session. + operationId: get_debug_replay_status_debug_replay_status_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DebugReplayStatusResponse' + security: + - frigateAdminAuth: [] + x-required-role: admin + /debug_replay/stop: + post: + tags: + - App + summary: Stop debug replay + description: |- + **Access:** Admin role required. + + Stop the active debug replay session and clean up all artifacts. + operationId: stop_debug_replay_debug_replay_stop_post + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DebugReplayStopResponse' + security: + - frigateAdminAuth: [] + x-required-role: admin +components: + schemas: + AppConfigSetBody: + properties: + requires_restart: + type: integer + title: Requires Restart + default: 1 + update_topic: + anyOf: + - type: string + - type: 'null' + title: Update Topic + config_data: + anyOf: + - type: object + - type: 'null' + title: Config Data + skip_save: + type: boolean + title: Skip Save + default: false + type: object + title: AppConfigSetBody + AppPostLoginBody: + properties: + user: + type: string + title: User + password: + type: string + title: Password + type: object + required: + - user + - password + title: AppPostLoginBody + AppPostUsersBody: + properties: + username: + type: string + title: Username + password: + type: string + title: Password + role: + anyOf: + - type: string + - type: 'null' + title: Role + default: viewer + type: object + required: + - username + - password + title: AppPostUsersBody + AppPutPasswordBody: + properties: + password: + type: string + title: Password + old_password: + anyOf: + - type: string + - type: 'null' + title: Old Password + type: object + required: + - password + title: AppPutPasswordBody + AppPutRoleBody: + properties: + role: + type: string + title: Role + type: object + required: + - role + title: AppPutRoleBody + AudioTranscriptionBody: + properties: + event_id: + type: string + title: Event Id + description: ID of the event to transcribe audio for + type: object + required: + - event_id + title: AudioTranscriptionBody + BatchExportBody: + properties: + items: + items: + $ref: '#/components/schemas/BatchExportItem' + type: array + maxItems: 50 + minItems: 1 + title: Items + description: List of export items. Each item has its own camera and + time range. + export_case_id: + anyOf: + - type: string + maxLength: 30 + - type: 'null' + title: Export case ID + description: Existing export case ID to assign all exports to. + Attaching to an existing case is temporarily admin-only until + case-level ACLs exist. + new_case_name: + anyOf: + - type: string + maxLength: 100 + - type: 'null' + title: New case name + description: Name of a new export case to create when export_case_id + is omitted + new_case_description: + anyOf: + - type: string + - type: 'null' + title: New case description + description: Optional description for a newly created export case + type: object + required: + - items + title: BatchExportBody + BatchExportItem: + properties: + camera: + type: string + title: Camera name + start_time: + type: number + title: Start time + end_time: + type: number + title: End time + image_path: + anyOf: + - type: string + - type: 'null' + title: Existing thumbnail path + description: Optional existing image to use as the export thumbnail + friendly_name: + anyOf: + - type: string + maxLength: 256 + - type: 'null' + title: Friendly name + description: Optional friendly name for this specific export item + client_item_id: + anyOf: + - type: string + maxLength: 128 + - type: 'null' + title: Client item ID + description: Optional opaque client identifier echoed back in results + type: object + required: + - camera + - start_time + - end_time + title: BatchExportItem + BatchExportResponse: + properties: + export_case_id: + anyOf: + - type: string + - type: 'null' + title: Export Case Id + description: Export case ID associated with the batch + export_ids: + items: + type: string + type: array + title: Export Ids + description: Export IDs successfully queued + results: + items: + $ref: '#/components/schemas/BatchExportResultModel' + type: array + title: Results + description: Per-item batch export results + type: object + required: + - export_ids + - results + title: BatchExportResponse + description: Response model for starting an export batch. + BatchExportResultModel: + properties: + camera: + type: string + title: Camera + description: Camera name for this export attempt + export_id: + anyOf: + - type: string + - type: 'null' + title: Export Id + description: The export ID when the export was successfully queued + success: + type: boolean + title: Success + description: Whether the export was successfully queued + status: + anyOf: + - type: string + - type: 'null' + title: Status + description: Queue status for this camera export + error: + anyOf: + - type: string + - type: 'null' + title: Error + description: Validation or queueing error for this item, if any + item_index: + anyOf: + - type: integer + - type: 'null' + title: Item Index + description: Zero-based index of this result within the request items + list + client_item_id: + anyOf: + - type: string + - type: 'null' + title: Client Item Id + description: Opaque client-supplied item identifier echoed from the + request + type: object + required: + - camera + - success + title: BatchExportResultModel + description: Per-item result for a batch export request. + Body_recognize_face_faces_recognize_post: + properties: + file: + type: string + format: binary + title: File + type: object + required: + - file + title: Body_recognize_face_faces_recognize_post + Body_register_face_faces__name__register_post: + properties: + file: + type: string + format: binary + title: File + type: object + required: + - file + title: Body_register_face_faces__name__register_post + CameraSetBody: + properties: + value: + type: string + title: Value + description: The value to set for the feature + type: object + required: + - value + title: CameraSetBody + ChaptersEnum: + type: string + enum: + - none + - recording_segments + - review_items + title: ChaptersEnum + ChatCompletionRequest: + properties: + messages: + items: + $ref: '#/components/schemas/ChatMessage' + type: array + title: Messages + description: List of messages in the conversation + max_tool_iterations: + type: integer + maximum: 10.0 + minimum: 1.0 + title: Max Tool Iterations + description: 'Maximum number of tool call iterations (default: 5)' + default: 5 + stream: + type: boolean + title: Stream + description: If true, stream the final assistant response in the body + as newline-delimited JSON. + default: false + enable_thinking: + anyOf: + - type: boolean + - type: 'null' + title: Enable Thinking + description: Per-request thinking toggle. None means use the provider + default. Ignored by providers that do not expose a per-request + thinking switch. + type: object + required: + - messages + title: ChatCompletionRequest + description: Request for chat completion with tool calling. + ChatMessage: + properties: + role: + type: string + title: Role + description: "Message role: 'user', 'assistant', 'system', or 'tool'" + content: + anyOf: + - {} + - type: 'null' + title: Content + description: Message content. Usually a string, but may be a + multimodal content list (e.g. text + image_url) or null for + assistant turns that only request tool calls. + tool_call_id: + anyOf: + - type: string + - type: 'null' + title: Tool Call Id + description: For tool messages, the ID of the tool call + name: + anyOf: + - type: string + - type: 'null' + title: Name + description: For tool messages, the tool name + tool_calls: + anyOf: + - items: + type: object + type: array + - type: 'null' + title: Tool Calls + description: For assistant messages replayed from prior turns, the + OpenAI-format tool calls the model previously requested. Replaying + these verbatim keeps the conversation prefix byte-for-byte identical + so the model server's prompt cache hits on follow-up turns. + type: object + required: + - role + title: ChatMessage + description: A single message in a chat conversation. + DayReview: + properties: + day: + type: string + format: date-time + title: Day + reviewed_alert: + type: integer + title: Reviewed Alert + reviewed_detection: + type: integer + title: Reviewed Detection + total_alert: + type: integer + title: Total Alert + total_detection: + type: integer + title: Total Detection + type: object + required: + - day + - reviewed_alert + - reviewed_detection + - total_alert + - total_detection + title: DayReview + DebugReplayStartBody: + properties: + camera: + type: string + title: Source camera name + start_time: + type: number + title: Start timestamp + end_time: + type: number + title: End timestamp + type: object + required: + - camera + - start_time + - end_time + title: DebugReplayStartBody + description: Request body for starting a debug replay session. + DebugReplayStartFromExportBody: + properties: + export_id: + type: string + title: Export id + type: object + required: + - export_id + title: DebugReplayStartFromExportBody + description: Request body for starting a debug replay session from an + export. + DebugReplayStartResponse: + properties: + success: + type: boolean + title: Success + replay_camera: + type: string + title: Replay Camera + job_id: + type: string + title: Job Id + type: object + required: + - success + - replay_camera + - job_id + title: DebugReplayStartResponse + description: Response for starting a debug replay session. + DebugReplayStatusResponse: + properties: + active: + type: boolean + title: Active + replay_camera: + anyOf: + - type: string + - type: 'null' + title: Replay Camera + source_camera: + anyOf: + - type: string + - type: 'null' + title: Source Camera + start_time: + anyOf: + - type: number + - type: 'null' + title: Start Time + end_time: + anyOf: + - type: number + - type: 'null' + title: End Time + live_ready: + type: boolean + title: Live Ready + default: false + type: object + required: + - active + title: DebugReplayStatusResponse + description: |- + Response for debug replay status. + + Returns only session-presence fields. Startup progress and error + details flow through the job_state WebSocket topic via the + debug_replay job (see frigate.jobs.debug_replay); the + Replay page subscribes there with useJobStatus("debug_replay"). + DebugReplayStopResponse: + properties: + success: + type: boolean + title: Success + type: object + required: + - success + title: DebugReplayStopResponse + description: Response for stopping a debug replay session. + DeleteFaceImagesBody: + properties: + ids: + items: + type: string + type: array + title: Ids + description: List of image filenames to delete from the face folder + type: object + required: + - ids + title: DeleteFaceImagesBody + EventCreateResponse: + properties: + success: + type: boolean + title: Success + message: + type: string + title: Message + event_id: + type: string + title: Event Id + type: object + required: + - success + - message + - event_id + title: EventCreateResponse + EventMultiDeleteResponse: + properties: + success: + type: boolean + title: Success + deleted_events: + items: + type: string + type: array + title: Deleted Events + not_found_events: + items: + type: string + type: array + title: Not Found Events + type: object + required: + - success + - deleted_events + - not_found_events + title: EventMultiDeleteResponse + EventResponse: + properties: + id: + type: string + title: Id + label: + type: string + title: Label + sub_label: + anyOf: + - type: string + - type: 'null' + title: Sub Label + camera: + type: string + title: Camera + start_time: + type: number + title: Start Time + end_time: + anyOf: + - type: number + - type: 'null' + title: End Time + false_positive: + anyOf: + - type: boolean + - type: 'null' + title: False Positive + zones: + items: + type: string + type: array + title: Zones + thumbnail: + anyOf: + - type: string + - type: 'null' + title: Thumbnail + has_clip: + type: boolean + title: Has Clip + has_snapshot: + type: boolean + title: Has Snapshot + retain_indefinitely: + type: boolean + title: Retain Indefinitely + plus_id: + anyOf: + - type: string + - type: 'null' + title: Plus Id + model_hash: + anyOf: + - type: string + - type: 'null' + title: Model Hash + detector_type: + anyOf: + - type: string + - type: 'null' + title: Detector Type + model_type: + anyOf: + - type: string + - type: 'null' + title: Model Type + data: + type: object + title: Data + type: object + required: + - id + - label + - sub_label + - camera + - start_time + - end_time + - false_positive + - zones + - thumbnail + - has_clip + - has_snapshot + - retain_indefinitely + - plus_id + - model_hash + - detector_type + - model_type + - data + title: EventResponse + EventUploadPlusResponse: + properties: + success: + type: boolean + title: Success + plus_id: + type: string + title: Plus Id + type: object + required: + - success + - plus_id + title: EventUploadPlusResponse + EventsAttributesBody: + properties: + attributes: + items: + type: string + type: array + title: Selected classification attributes for the event + type: object + title: EventsAttributesBody + EventsCreateBody: + properties: + sub_label: + anyOf: + - type: string + - type: 'null' + title: Sub Label + score: + anyOf: + - type: number + - type: 'null' + title: Score + default: 0 + duration: + anyOf: + - type: integer + - type: 'null' + title: Duration + default: 30 + include_recording: + anyOf: + - type: boolean + - type: 'null' + title: Include Recording + default: true + draw: + anyOf: + - type: object + - type: 'null' + title: Draw + default: {} + pre_capture: + anyOf: + - type: integer + - type: 'null' + title: Pre Capture + type: object + title: EventsCreateBody + EventsDeleteBody: + properties: + event_ids: + items: + type: string + type: array + title: The event IDs to delete + type: object + required: + - event_ids + title: EventsDeleteBody + EventsDescriptionBody: + properties: + description: + anyOf: + - type: string + - type: 'null' + title: The description of the event + type: object + required: + - description + title: EventsDescriptionBody + EventsEndBody: + properties: + end_time: + anyOf: + - type: number + - type: 'null' + title: End Time + type: object + title: EventsEndBody + EventsLPRBody: + properties: + recognizedLicensePlate: + type: string + maxLength: 100 + title: Recognized License Plate + recognizedLicensePlateScore: + anyOf: + - type: number + maximum: 1.0 + exclusiveMinimum: 0.0 + - type: 'null' + title: Score for recognized license plate + type: object + required: + - recognizedLicensePlate + title: EventsLPRBody + EventsSubLabelBody: + properties: + subLabel: + type: string + maxLength: 100 + title: Sub label + subLabelScore: + anyOf: + - type: number + maximum: 1.0 + exclusiveMinimum: 0.0 + - type: 'null' + title: Score for sub label + camera: + anyOf: + - type: string + - type: 'null' + title: Camera this object is detected on. + type: object + required: + - subLabel + title: EventsSubLabelBody + ExportBulkDeleteBody: + properties: + ids: + items: + type: string + minLength: 1 + type: array + minItems: 1 + title: Ids + type: object + required: + - ids + title: ExportBulkDeleteBody + description: Request body for bulk deleting exports. + ExportBulkReassignBody: + properties: + ids: + items: + type: string + minLength: 1 + type: array + minItems: 1 + title: Ids + export_case_id: + anyOf: + - type: string + maxLength: 30 + - type: 'null' + title: Export Case Id + description: Case ID to assign to, or null to unassign from current + case + type: object + required: + - ids + title: ExportBulkReassignBody + description: Request body for bulk reassigning exports to a case. + ExportCaseCreateBody: + properties: + name: + type: string + maxLength: 100 + title: Name + description: Friendly name of the export case + description: + anyOf: + - type: string + - type: 'null' + title: Description + description: Optional description of the export case + type: object + required: + - name + title: ExportCaseCreateBody + description: Request body for creating a new export case. + ExportCaseModel: + properties: + id: + type: string + title: Id + description: Unique identifier for the export case + name: + type: string + title: Name + description: Friendly name of the export case + description: + anyOf: + - type: string + - type: 'null' + title: Description + description: Optional description of the export case + created_at: + type: number + title: Created At + description: Unix timestamp when the export case was created + updated_at: + type: number + title: Updated At + description: Unix timestamp when the export case was last updated + type: object + required: + - id + - name + - created_at + - updated_at + title: ExportCaseModel + description: Model representing a single export case. + ExportCaseUpdateBody: + properties: + name: + anyOf: + - type: string + maxLength: 100 + - type: 'null' + title: Name + description: Updated friendly name of the export case + description: + anyOf: + - type: string + - type: 'null' + title: Description + description: Updated description of the export case + type: object + title: ExportCaseUpdateBody + description: Request body for updating an existing export case. + ExportJobModel: + properties: + id: + type: string + title: Id + description: Unique identifier for the export job + job_type: + type: string + title: Job Type + description: Job type + status: + type: string + title: Status + description: Current job status + camera: + type: string + title: Camera + description: Camera associated with this export job + name: + anyOf: + - type: string + - type: 'null' + title: Name + description: Friendly name for the export + export_case_id: + anyOf: + - type: string + - type: 'null' + title: Export Case Id + description: ID of the export case this export belongs to + request_start_time: + type: number + title: Request Start Time + description: Requested export start time + request_end_time: + type: number + title: Request End Time + description: Requested export end time + start_time: + anyOf: + - type: number + - type: 'null' + title: Start Time + description: Unix timestamp when execution started + end_time: + anyOf: + - type: number + - type: 'null' + title: End Time + description: Unix timestamp when execution completed + error_message: + anyOf: + - type: string + - type: 'null' + title: Error Message + description: Error message for failed jobs + results: + anyOf: + - type: object + - type: 'null' + title: Results + description: Result metadata for completed jobs + current_step: + type: string + title: Current Step + description: Current execution step (queued, preparing, encoding, + encoding_retry, finalizing) + default: queued + progress_percent: + type: number + title: Progress Percent + description: Progress percentage of the current step (0.0 - 100.0) + default: 0.0 + type: object + required: + - id + - job_type + - status + - camera + - request_start_time + - request_end_time + title: ExportJobModel + description: Model representing a queued or running export job. + ExportModel: + properties: + id: + type: string + title: Id + description: Unique identifier for the export + camera: + type: string + title: Camera + description: Camera name associated with this export + name: + type: string + title: Name + description: Friendly name of the export + date: + type: number + title: Date + description: Unix timestamp when the export was created + video_path: + type: string + title: Video Path + description: File path to the exported video + thumb_path: + type: string + title: Thumb Path + description: File path to the export thumbnail + in_progress: + type: boolean + title: In Progress + description: Whether the export is currently being processed + export_case_id: + anyOf: + - type: string + - type: 'null' + title: Export Case Id + description: ID of the export case this export belongs to + type: object + required: + - id + - camera + - name + - date + - video_path + - thumb_path + - in_progress + title: ExportModel + description: Model representing a single export. + ExportRecordingsBody: + properties: + source: + $ref: '#/components/schemas/PlaybackSourceEnum' + title: Playback source + default: recordings + name: + anyOf: + - type: string + maxLength: 256 + - type: 'null' + title: Friendly name + image_path: + type: string + title: Image Path + export_case_id: + anyOf: + - type: string + maxLength: 30 + - type: 'null' + title: Export case ID + description: ID of the export case to assign this export to + chapters: + anyOf: + - $ref: '#/components/schemas/ChaptersEnum' + - type: 'null' + title: Chapter mode + description: Optional chapter metadata to embed in the export. When + omitted, the camera's configured export chapter mode is used. + type: object + title: ExportRecordingsBody + ExportRecordingsCustomBody: + properties: + source: + $ref: '#/components/schemas/PlaybackSourceEnum' + title: Playback source + default: recordings + name: + type: string + maxLength: 256 + title: Friendly name + image_path: + type: string + title: Image Path + export_case_id: + anyOf: + - type: string + maxLength: 30 + - type: 'null' + title: Export case ID + description: ID of the export case to assign this export to + ffmpeg_input_args: + anyOf: + - type: string + - type: 'null' + title: FFmpeg input arguments + description: Custom FFmpeg input arguments. If not provided, defaults + to timelapse input args. + ffmpeg_output_args: + anyOf: + - type: string + - type: 'null' + title: FFmpeg output arguments + description: Custom FFmpeg output arguments. If not provided, defaults + to timelapse output args. + cpu_fallback: + type: boolean + title: CPU Fallback + description: If true, retry export without hardware acceleration if + the initial export fails. + default: false + type: object + title: ExportRecordingsCustomBody + ExportRenameBody: + properties: + name: + type: string + maxLength: 256 + title: Friendly name + type: object + required: + - name + title: ExportRenameBody + Extension: + type: string + enum: + - webp + - png + - jpg + - jpeg + title: Extension + FaceRecognitionResponse: + properties: + success: + type: boolean + title: Success + description: Whether the face recognition was successful + score: + anyOf: + - type: number + - type: 'null' + title: Score + description: Confidence score of the recognition (0-1) + face_name: + anyOf: + - type: string + - type: 'null' + title: Face Name + description: The recognized face name if successful + type: object + required: + - success + title: FaceRecognitionResponse + description: |- + Response model for face recognition endpoint. + + Returns the result of attempting to recognize a face from an uploaded image. + FacesResponse: + additionalProperties: + items: + type: string + type: array + type: object + title: FacesResponse + description: |- + Response model for the get_faces endpoint. + + Returns a mapping of face names to lists of image filenames. + Each face name corresponds to a directory in the faces folder, + and the list contains the names of image files for that face. + + Example: + { + "john_doe": ["face1.webp", "face2.jpg"], + "jane_smith": ["face3.png"] + } + GenAIProbeBody: + properties: + provider: + $ref: '#/components/schemas/GenAIProviderEnum' + api_key: + anyOf: + - type: string + - type: 'null' + title: Api Key + base_url: + anyOf: + - type: string + - type: 'null' + title: Base Url + provider_options: + type: object + title: Provider Options + type: object + required: + - provider + title: GenAIProbeBody + GenAIProviderEnum: + type: string + enum: + - openai + - azure_openai + - gemini + - ollama + - llamacpp + title: GenAIProviderEnum + GenerateObjectExamplesBody: + properties: + model_name: + type: string + title: Model Name + description: Name of the classification model + label: + type: string + title: Label + description: Object label to collect examples for (e.g., 'person', + 'car') + type: object + required: + - model_name + - label + title: GenerateObjectExamplesBody + GenerateStateExamplesBody: + properties: + model_name: + type: string + title: Model Name + description: Name of the classification model + cameras: + additionalProperties: + prefixItems: + - type: number + - type: number + - type: number + - type: number + type: array + maxItems: 4 + minItems: 4 + type: object + title: Cameras + description: Dictionary mapping camera names to normalized crop + coordinates in [x1, y1, x2, y2] format (values 0-1) + type: object + required: + - model_name + - cameras + title: GenerateStateExamplesBody + GenericResponse: + properties: + success: + type: boolean + title: Success + message: + type: string + title: Message + type: object + required: + - success + - message + title: GenericResponse + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail + type: object + title: HTTPValidationError + Last24HoursReview: + properties: + reviewed_alert: + type: integer + title: Reviewed Alert + reviewed_detection: + type: integer + title: Reviewed Detection + total_alert: + type: integer + title: Total Alert + total_detection: + type: integer + title: Total Detection + type: object + required: + - reviewed_alert + - reviewed_detection + - total_alert + - total_detection + title: Last24HoursReview + MediaSyncBody: + properties: + dry_run: + type: boolean + title: Dry Run + description: If True, only report orphans without deleting them + default: true + media_types: + items: + type: string + type: array + title: Media Types + description: "Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails', + 'review_thumbnails', 'previews', 'exports', 'recordings'" + default: + - all + force: + type: boolean + title: Force + description: If True, bypass safety threshold checks + default: false + verbose: + type: boolean + title: Verbose + description: If True, write full orphan file list to disk + default: false + type: object + title: MediaSyncBody + MotionSearchMetricsResponse: + properties: + segments_scanned: + type: integer + title: Segments Scanned + default: 0 + segments_processed: + type: integer + title: Segments Processed + default: 0 + metadata_inactive_segments: + type: integer + title: Metadata Inactive Segments + default: 0 + heatmap_roi_skip_segments: + type: integer + title: Heatmap Roi Skip Segments + default: 0 + fallback_full_range_segments: + type: integer + title: Fallback Full Range Segments + default: 0 + frames_decoded: + type: integer + title: Frames Decoded + default: 0 + wall_time_seconds: + type: number + title: Wall Time Seconds + default: 0.0 + segments_with_errors: + type: integer + title: Segments With Errors + default: 0 + type: object + title: MotionSearchMetricsResponse + description: Metrics collected during motion search execution. + MotionSearchRequest: + properties: + start_time: + type: number + title: Start Time + description: Start timestamp for the search range + end_time: + type: number + title: End Time + description: End timestamp for the search range + polygon_points: + items: + items: + type: number + type: array + type: array + title: Polygon Points + description: List of [x, y] normalized coordinates (0-1) defining the + ROI polygon + threshold: + type: integer + maximum: 255.0 + minimum: 1.0 + title: Threshold + description: Pixel difference threshold (1-255) + default: 30 + min_area: + type: number + maximum: 100.0 + minimum: 0.1 + title: Min Area + description: Minimum change area as a percentage of the ROI + default: 5.0 + parallel: + type: boolean + title: Parallel + description: Enable parallel scanning across segments + default: false + max_results: + type: integer + maximum: 200.0 + minimum: 1.0 + title: Max Results + description: Maximum number of search results to return + default: 25 + type: object + required: + - start_time + - end_time + - polygon_points + title: MotionSearchRequest + description: Request body for motion search. + MotionSearchResult: + properties: + timestamp: + type: number + title: Timestamp + description: Timestamp where change was detected + change_percentage: + type: number + title: Change Percentage + description: Percentage of ROI area that changed + type: object + required: + - timestamp + - change_percentage + title: MotionSearchResult + description: A single search result with timestamp and change info. + MotionSearchStartResponse: + properties: + success: + type: boolean + title: Success + message: + type: string + title: Message + job_id: + type: string + title: Job Id + type: object + required: + - success + - message + - job_id + title: MotionSearchStartResponse + description: Response when motion search job starts. + MotionSearchStatusResponse: + properties: + success: + type: boolean + title: Success + message: + type: string + title: Message + status: + type: string + title: Status + results: + anyOf: + - items: + $ref: '#/components/schemas/MotionSearchResult' + type: array + - type: 'null' + title: Results + total_frames_processed: + anyOf: + - type: integer + - type: 'null' + title: Total Frames Processed + error_message: + anyOf: + - type: string + - type: 'null' + title: Error Message + metrics: + anyOf: + - $ref: '#/components/schemas/MotionSearchMetricsResponse' + - type: 'null' + scanning_timestamp: + anyOf: + - type: number + - type: 'null' + title: Scanning Timestamp + progress: + anyOf: + - type: number + - type: 'null' + title: Progress + type: object + required: + - success + - message + - status + title: MotionSearchStatusResponse + description: Response containing job status and results. + PlaybackSourceEnum: + type: string + enum: + - recordings + - preview + title: PlaybackSourceEnum + PreviewModel: + properties: + camera: + type: string + title: Camera + description: Camera name for this preview + src: + type: string + title: Src + description: Path to the preview video file + type: + type: string + title: Type + description: MIME type of the preview video (video/mp4) + start: + type: number + title: Start + description: Unix timestamp when the preview starts + end: + type: number + title: End + description: Unix timestamp when the preview ends + type: object + required: + - camera + - src + - type + - start + - end + title: PreviewModel + description: Model representing a single preview clip. + RegenerateDescriptionEnum: + type: string + enum: + - thumbnails + - snapshot + title: RegenerateDescriptionEnum + RenameFaceBody: + properties: + new_name: + type: string + title: New Name + description: New name for the face + type: object + required: + - new_name + title: RenameFaceBody + ReviewActivityMotionResponse: + properties: + start_time: + type: integer + title: Start Time + motion: + type: number + title: Motion + camera: + type: string + title: Camera + type: object + required: + - start_time + - motion + - camera + title: ReviewActivityMotionResponse + ReviewModifyMultipleBody: + properties: + ids: + items: + type: string + minLength: 1 + type: array + minItems: 1 + title: Ids + reviewed: + type: boolean + title: Reviewed + default: true + type: object + required: + - ids + title: ReviewModifyMultipleBody + ReviewSegmentResponse: + properties: + id: + type: string + title: Id + camera: + type: string + title: Camera + start_time: + type: string + format: date-time + title: Start Time + end_time: + type: string + format: date-time + title: End Time + has_been_reviewed: + type: boolean + title: Has Been Reviewed + severity: + $ref: '#/components/schemas/SeverityEnum' + thumb_path: + type: string + title: Thumb Path + data: + title: Data + type: object + required: + - id + - camera + - start_time + - end_time + - has_been_reviewed + - severity + - thumb_path + - data + title: ReviewSegmentResponse + ReviewSummaryResponse: + properties: + last24Hours: + $ref: '#/components/schemas/Last24HoursReview' + root: + additionalProperties: + $ref: '#/components/schemas/DayReview' + type: object + title: Root + type: object + required: + - last24Hours + - root + title: ReviewSummaryResponse + SeverityEnum: + type: string + enum: + - alert + - detection + title: SeverityEnum + StartExportResponse: + properties: + success: + type: boolean + title: Success + description: Whether the export was started successfully + message: + type: string + title: Message + description: Status or error message + export_id: + anyOf: + - type: string + - type: 'null' + title: Export Id + description: The export ID if successfully started + status: + anyOf: + - type: string + - type: 'null' + title: Status + description: Queue status for the export job + type: object + required: + - success + - message + title: StartExportResponse + description: Response model for starting an export. + SubmitPlusBody: + properties: + include_annotation: + type: integer + title: Include Annotation + default: 1 + type: object + title: SubmitPlusBody + ToolExecuteRequest: + properties: + tool_name: + type: string + title: Tool Name + arguments: + type: object + title: Arguments + type: object + required: + - tool_name + - arguments + title: ToolExecuteRequest + description: Request model for tool execution. + TriggerEmbeddingBody: + properties: + type: + $ref: '#/components/schemas/TriggerType' + data: + type: string + title: Data + threshold: + type: number + maximum: 1.0 + minimum: 0.0 + title: Threshold + default: 0.5 + type: object + required: + - type + - data + title: TriggerEmbeddingBody + TriggerType: + type: string + enum: + - thumbnail + - description + title: TriggerType + VLMMonitorRequest: + properties: + camera: + type: string + title: Camera + condition: + type: string + title: Condition + max_duration_minutes: + type: integer + title: Max Duration Minutes + default: 60 + labels: + items: + type: string + type: array + title: Labels + default: [] + zones: + items: + type: string + type: array + title: Zones + default: [] + type: object + required: + - camera + - condition + title: VLMMonitorRequest + description: Request model for starting a VLM watch job. + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + type: object + required: + - loc + - msg + - type + title: ValidationError + securitySchemes: + frigateAdminAuth: + type: apiKey + in: cookie + name: frigate_token + description: Authenticated session whose resolved role is 'admin'. The + session is established via the JWT cookie issued by POST /login, or via + proxy auth headers (remote-user / remote-role) when Frigate runs behind + an authenticating reverse proxy. + frigateUserAuth: + type: apiKey + in: cookie + name: frigate_token + description: Any authenticated session (role 'viewer' or higher), + established via the JWT cookie issued by POST /login, or via proxy auth + headers when Frigate runs behind an authenticating reverse proxy. diff --git a/docs/static/img/annotate.png b/docs/static/img/annotate.png new file mode 100644 index 0000000..c36ec94 Binary files /dev/null and b/docs/static/img/annotate.png differ diff --git a/docs/static/img/autotracking-debug.gif b/docs/static/img/autotracking-debug.gif new file mode 100644 index 0000000..d3bb202 Binary files /dev/null and b/docs/static/img/autotracking-debug.gif differ diff --git a/docs/static/img/bottom-center-mask.jpg b/docs/static/img/bottom-center-mask.jpg new file mode 100644 index 0000000..103875a Binary files /dev/null and b/docs/static/img/bottom-center-mask.jpg differ diff --git a/docs/static/img/bottom-center.jpg b/docs/static/img/bottom-center.jpg new file mode 100644 index 0000000..e11c4c4 Binary files /dev/null and b/docs/static/img/bottom-center.jpg differ diff --git a/docs/static/img/branding/LICENSE.md b/docs/static/img/branding/LICENSE.md new file mode 100644 index 0000000..abb0cb3 --- /dev/null +++ b/docs/static/img/branding/LICENSE.md @@ -0,0 +1,30 @@ +# COPYRIGHT AND TRADEMARK NOTICE + +The images, logos, and icons contained in this directory (the "Brand Assets") are +proprietary to Frigate, Inc. and are NOT covered by the MIT License governing the +rest of this repository. + +1. TRADEMARK STATUS + The "Frigate" name and the accompanying logo are common law trademarks™ of + Frigate, Inc. Frigate, Inc. reserves all rights to these marks. + +2. LIMITED PERMISSION FOR USE + Permission is hereby granted to display these Brand Assets strictly for the + following purposes: + a. To execute the software interface on a local machine. + b. To identify the software in documentation or reviews (nominative use). + +3. RESTRICTIONS + You may NOT: + a. Use these Brand Assets to represent a derivative work (fork) as an official + product of Frigate, Inc. + b. Use these Brand Assets in a way that implies endorsement, sponsorship, or + commercial affiliation with Frigate, Inc. + c. Modify or alter the Brand Assets. + +If you fork this repository with the intent to distribute a modified or competing +version of the software, you must replace these Brand Assets with your own +original content. + +ALL RIGHTS RESERVED. +Copyright (c) 2026 Frigate, Inc. diff --git a/docs/static/img/branding/favicon.ico b/docs/static/img/branding/favicon.ico new file mode 100644 index 0000000..1de8ec8 Binary files /dev/null and b/docs/static/img/branding/favicon.ico differ diff --git a/docs/static/img/branding/frigate.png b/docs/static/img/branding/frigate.png new file mode 100644 index 0000000..1156bc6 Binary files /dev/null and b/docs/static/img/branding/frigate.png differ diff --git a/docs/static/img/branding/logo-dark.svg b/docs/static/img/branding/logo-dark.svg new file mode 100644 index 0000000..16cd275 --- /dev/null +++ b/docs/static/img/branding/logo-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/static/img/branding/logo.svg b/docs/static/img/branding/logo.svg new file mode 100644 index 0000000..3d01f2a --- /dev/null +++ b/docs/static/img/branding/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/static/img/camera-ui.png b/docs/static/img/camera-ui.png new file mode 100644 index 0000000..55418e8 Binary files /dev/null and b/docs/static/img/camera-ui.png differ diff --git a/docs/static/img/diagram.png b/docs/static/img/diagram.png new file mode 100644 index 0000000..1ee8659 Binary files /dev/null and b/docs/static/img/diagram.png differ diff --git a/docs/static/img/driveway_zones-min.png b/docs/static/img/driveway_zones-min.png new file mode 100644 index 0000000..39e1954 Binary files /dev/null and b/docs/static/img/driveway_zones-min.png differ diff --git a/docs/static/img/driveway_zones.png b/docs/static/img/driveway_zones.png new file mode 100644 index 0000000..67ed8f4 Binary files /dev/null and b/docs/static/img/driveway_zones.png differ diff --git a/docs/static/img/events-ui.png b/docs/static/img/events-ui.png new file mode 100644 index 0000000..13ee205 Binary files /dev/null and b/docs/static/img/events-ui.png differ diff --git a/docs/static/img/example-mask-poly-min.png b/docs/static/img/example-mask-poly-min.png new file mode 100644 index 0000000..62a78cd Binary files /dev/null and b/docs/static/img/example-mask-poly-min.png differ diff --git a/docs/static/img/example-mask-poly.png b/docs/static/img/example-mask-poly.png new file mode 100644 index 0000000..e0e7d49 Binary files /dev/null and b/docs/static/img/example-mask-poly.png differ diff --git a/docs/static/img/frigate-autotracking-example.gif b/docs/static/img/frigate-autotracking-example.gif new file mode 100644 index 0000000..bd6ab39 Binary files /dev/null and b/docs/static/img/frigate-autotracking-example.gif differ diff --git a/docs/static/img/ground-plane.jpg b/docs/static/img/ground-plane.jpg new file mode 100644 index 0000000..f7ea4db Binary files /dev/null and b/docs/static/img/ground-plane.jpg differ diff --git a/docs/static/img/home-ui.png b/docs/static/img/home-ui.png new file mode 100644 index 0000000..efdce3a Binary files /dev/null and b/docs/static/img/home-ui.png differ diff --git a/docs/static/img/live-view.png b/docs/static/img/live-view.png new file mode 100644 index 0000000..6bc5eed Binary files /dev/null and b/docs/static/img/live-view.png differ diff --git a/docs/static/img/media_browser-min.png b/docs/static/img/media_browser-min.png new file mode 100644 index 0000000..39229d0 Binary files /dev/null and b/docs/static/img/media_browser-min.png differ diff --git a/docs/static/img/media_browser.png b/docs/static/img/media_browser.png new file mode 100644 index 0000000..9c449d0 Binary files /dev/null and b/docs/static/img/media_browser.png differ diff --git a/docs/static/img/mismatched-resolution-min.jpg b/docs/static/img/mismatched-resolution-min.jpg new file mode 100644 index 0000000..ef5037f Binary files /dev/null and b/docs/static/img/mismatched-resolution-min.jpg differ diff --git a/docs/static/img/mismatched-resolution.jpg b/docs/static/img/mismatched-resolution.jpg new file mode 100644 index 0000000..662a20f Binary files /dev/null and b/docs/static/img/mismatched-resolution.jpg differ diff --git a/docs/static/img/notification-min.png b/docs/static/img/notification-min.png new file mode 100644 index 0000000..0d49cdf Binary files /dev/null and b/docs/static/img/notification-min.png differ diff --git a/docs/static/img/notification.png b/docs/static/img/notification.png new file mode 100644 index 0000000..0df060e Binary files /dev/null and b/docs/static/img/notification.png differ diff --git a/docs/static/img/plus-api-key-min.png b/docs/static/img/plus-api-key-min.png new file mode 100644 index 0000000..3e9c7f4 Binary files /dev/null and b/docs/static/img/plus-api-key-min.png differ diff --git a/docs/static/img/plus/attribute-example-face.jpg b/docs/static/img/plus/attribute-example-face.jpg new file mode 100644 index 0000000..a99c878 Binary files /dev/null and b/docs/static/img/plus/attribute-example-face.jpg differ diff --git a/docs/static/img/plus/attribute-example-fedex.jpg b/docs/static/img/plus/attribute-example-fedex.jpg new file mode 100644 index 0000000..156b507 Binary files /dev/null and b/docs/static/img/plus/attribute-example-fedex.jpg differ diff --git a/docs/static/img/plus/false-positive-overlap.jpg b/docs/static/img/plus/false-positive-overlap.jpg new file mode 100644 index 0000000..feedc3b Binary files /dev/null and b/docs/static/img/plus/false-positive-overlap.jpg differ diff --git a/docs/static/img/plus/false-positive.jpg b/docs/static/img/plus/false-positive.jpg new file mode 100644 index 0000000..1e5cc3d Binary files /dev/null and b/docs/static/img/plus/false-positive.jpg differ diff --git a/docs/static/img/plus/fedex-logo.jpg b/docs/static/img/plus/fedex-logo.jpg new file mode 100644 index 0000000..42c36dd Binary files /dev/null and b/docs/static/img/plus/fedex-logo.jpg differ diff --git a/docs/static/img/plus/model-ready-email.jpg b/docs/static/img/plus/model-ready-email.jpg new file mode 100644 index 0000000..7787237 Binary files /dev/null and b/docs/static/img/plus/model-ready-email.jpg differ diff --git a/docs/static/img/plus/plus-models.jpg b/docs/static/img/plus/plus-models.jpg new file mode 100644 index 0000000..702391d Binary files /dev/null and b/docs/static/img/plus/plus-models.jpg differ diff --git a/docs/static/img/plus/send-to-plus.jpg b/docs/static/img/plus/send-to-plus.jpg new file mode 100644 index 0000000..cffd7e5 Binary files /dev/null and b/docs/static/img/plus/send-to-plus.jpg differ diff --git a/docs/static/img/plus/submit-to-plus.jpg b/docs/static/img/plus/submit-to-plus.jpg new file mode 100644 index 0000000..fd90254 Binary files /dev/null and b/docs/static/img/plus/submit-to-plus.jpg differ diff --git a/docs/static/img/plus/suggestions.webp b/docs/static/img/plus/suggestions.webp new file mode 100644 index 0000000..274eda7 Binary files /dev/null and b/docs/static/img/plus/suggestions.webp differ diff --git a/docs/static/img/resolutions-min.jpg b/docs/static/img/resolutions-min.jpg new file mode 100644 index 0000000..19169ac Binary files /dev/null and b/docs/static/img/resolutions-min.jpg differ diff --git a/docs/static/img/resolutions.png b/docs/static/img/resolutions.png new file mode 100644 index 0000000..59d8c8b Binary files /dev/null and b/docs/static/img/resolutions.png differ diff --git a/docs/static/img/review-items.png b/docs/static/img/review-items.png new file mode 100644 index 0000000..641813b Binary files /dev/null and b/docs/static/img/review-items.png differ diff --git a/frigate/__init__.py b/frigate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/__main__.py b/frigate/__main__.py new file mode 100644 index 0000000..df27b42 --- /dev/null +++ b/frigate/__main__.py @@ -0,0 +1,135 @@ +import argparse +import faulthandler +import multiprocessing as mp +import signal +import sys +import threading + +import ruamel.yaml +from pydantic import ValidationError + +from frigate.app import FrigateApp +from frigate.config import FrigateConfig +from frigate.log import setup_logging +from frigate.util.config import find_config_file + + +def main() -> None: + manager = mp.Manager() + faulthandler.enable() + + # Setup the logging thread + setup_logging(manager) + + threading.current_thread().name = "frigate" + stop_event = mp.Event() + + # send stop event on SIGINT + signal.signal(signal.SIGINT, lambda sig, frame: stop_event.set()) + + # Make sure we exit cleanly on SIGTERM. + signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit()) + + # Parse the cli arguments. + parser = argparse.ArgumentParser( + prog="Frigate", + description="An NVR with realtime local object detection for IP cameras.", + ) + parser.add_argument("--validate-config", action="store_true") + args = parser.parse_args() + + # Load the configuration. + try: + config = FrigateConfig.load(install=True) + except ValidationError as e: + print("*************************************************************") + print("*************************************************************") + print("*** Your config file is not valid! ***") + print("*** Please check the docs at ***") + print("*** https://docs.frigate.video/configuration/ ***") + print("*************************************************************") + print("*************************************************************") + print("*** Config Validation Errors ***") + print("*************************************************************\n") + # Attempt to get the original config file for line number tracking + config_path = find_config_file() + with open(config_path) as f: + yaml_config = ruamel.yaml.YAML() + yaml_config.preserve_quotes = True + full_config = yaml_config.load(f) + + for error in e.errors(): + error_path = error["loc"] + + current = full_config + line_number = "Unknown" + last_line_number = "Unknown" + + try: + for i, part in enumerate(error_path): + key: int | str = ( + int(part) if isinstance(part, str) and part.isdigit() else part + ) + + if isinstance(current, ruamel.yaml.comments.CommentedMap): + current = current[key] + elif isinstance(current, list): + if isinstance(key, int): + current = current[key] + + if hasattr(current, "lc"): + last_line_number = current.lc.line + + if i == len(error_path) - 1: + if hasattr(current, "lc"): + line_number = current.lc.line + else: + line_number = last_line_number + + except Exception as traverse_error: + print(f"Could not determine exact line number: {traverse_error}") + + if current != full_config: + print(f"Line # : {line_number}") + print(f"Key : {' -> '.join(map(str, error_path))}") + print(f"Value : {error.get('input', '-')}") + print(f"Message : {error.get('msg', error.get('type', 'Unknown'))}\n") + + print("*************************************************************") + print("*** End Config Validation Errors ***") + print("*************************************************************") + + # attempt to start Frigate in recovery mode + try: + config = FrigateConfig.load(install=True, safe_load=True) + print("Starting Frigate in safe mode.") + except ValidationError: + print("Unable to start Frigate in safe mode.") + sys.exit(1) + if args.validate_config: + print("*************************************************************") + print("*** Your config file is valid. ***") + print("*************************************************************") + sys.exit(0) + + # Run the main application. + FrigateApp(config, manager, stop_event).start() + + +if __name__ == "__main__": + mp.set_forkserver_preload( + [ + # Standard library and core dependencies + "sqlite3", + # Third-party libraries commonly used in Frigate + "numpy", + "cv2", + "peewee", + "zmq", + "ruamel.yaml", + # Frigate core modules + "frigate.camera.maintainer", + ] + ) + mp.set_start_method("forkserver", force=True) + main() diff --git a/frigate/api/__init__.py b/frigate/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/api/app.py b/frigate/api/app.py new file mode 100644 index 0000000..ac83915 --- /dev/null +++ b/frigate/api/app.py @@ -0,0 +1,1524 @@ +"""Main api runner.""" + +import asyncio +import copy +import json +import logging +import os +import platform +import traceback +import urllib +from datetime import datetime, timedelta +from functools import reduce +from io import StringIO +from pathlib import Path as FilePath +from typing import Any + +import aiofiles +import ruamel.yaml +from fastapi import APIRouter, Body, Path, Request, Response +from fastapi.encoders import jsonable_encoder +from fastapi.params import Depends +from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse +from filelock import FileLock, Timeout +from markupsafe import escape +from peewee import SQL, fn, operator +from pydantic import ValidationError + +from frigate.api.auth import ( + allow_any_authenticated, + allow_public, + get_allowed_cameras_for_filter, + require_role, +) +from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters +from frigate.api.defs.request.app_body import ( + AppConfigSetBody, + GenAIProbeBody, + MediaSyncBody, +) +from frigate.api.defs.tags import Tags +from frigate.config import FrigateConfig, GenAIConfig, GenAIProviderEnum +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateTopic, +) +from frigate.const import REDACTED_CREDENTIAL_SENTINEL +from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector +from frigate.genai import PROVIDERS, load_providers +from frigate.jobs.media_sync import ( + get_current_media_sync_job, + get_media_sync_job_by_id, + start_media_sync_job, +) +from frigate.models import Event, Timeline +from frigate.stats.prometheus import get_metrics, update_metrics +from frigate.types import JobStatusTypesEnum +from frigate.util.builtin import ( + clean_camera_user_pass, + deep_merge, + flatten_config_data, + load_labels, + process_config_query_string, + update_yaml_file_bulk, +) +from frigate.util.config import ( + apply_section_update, + find_config_file, + redact_credential, +) +from frigate.util.schema import get_config_schema +from frigate.util.services import ( + get_nvidia_driver_info, + process_logs, + restart_frigate, + vainfo_hwaccel, +) +from frigate.util.time import get_tz_modifiers +from frigate.version import VERSION + +logger = logging.getLogger(__name__) + + +router = APIRouter(tags=[Tags.app]) + +# Short timeout for the /genai/probe path. The probe is interactive — fail +# fast on hung providers rather than holding an API worker thread. +_PROBE_TIMEOUT_SECONDS = 10 +# Outer cap that returns control to the caller even if the underlying sync +# HTTP call ignores its timeout. The sync work continues in the background +# thread; only the response is bounded. +_PROBE_OUTER_TIMEOUT_SECONDS = 15 + + +@router.get( + "/", response_class=PlainTextResponse, dependencies=[Depends(allow_public())] +) +def is_healthy(): + return "Frigate is running. Alive and healthy!" + + +@router.get("/config/schema.json", dependencies=[Depends(allow_public())]) +def config_schema(request: Request): + return JSONResponse(content=get_config_schema(FrigateConfig)) + + +@router.get( + "/version", response_class=PlainTextResponse, dependencies=[Depends(allow_public())] +) +def version(): + return VERSION + + +@router.get("/stats", dependencies=[Depends(allow_any_authenticated())]) +def stats( + request: Request, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + stats_data = request.app.stats_emitter.get_latest_stats() + + # Admins see the full snapshot + if request.headers.get("remote-role") == "admin": + return JSONResponse(content=stats_data) + + allowed_set = set(allowed_cameras) + + # Shallow-copy so we don't mutate the cached stats history entry. + filtered = {**stats_data} + + cameras = stats_data.get("cameras") + if cameras is not None: + filtered["cameras"] = { + name: data for name, data in cameras.items() if name in allowed_set + } + + bandwidth = stats_data.get("bandwidth_usages") + if bandwidth is not None: + filtered["bandwidth_usages"] = { + name: data for name, data in bandwidth.items() if name in allowed_set + } + + # cmdline can leak camera URLs/paths; strip but keep cpu/mem so + # client-side problem heuristics still work. + cpu_usages = stats_data.get("cpu_usages") + if cpu_usages is not None: + filtered["cpu_usages"] = { + pid: {k: v for k, v in usage.items() if k != "cmdline"} + for pid, usage in cpu_usages.items() + } + + return JSONResponse(content=filtered) + + +@router.get("/stats/history", dependencies=[Depends(require_role(["admin"]))]) +def stats_history(request: Request, keys: str = None): + if keys: + keys = keys.split(",") + + return JSONResponse(content=request.app.stats_emitter.get_stats_history(keys)) + + +@router.get("/metrics", dependencies=[Depends(allow_any_authenticated())]) +def metrics(request: Request): + """Expose Prometheus metrics endpoint and update metrics with latest stats""" + # Retrieve the latest statistics and update the Prometheus metrics + stats = request.app.stats_emitter.get_latest_stats() + # query DB for count of events by camera, label + event_counts: list[dict[str, Any]] = ( + Event.select(Event.camera, Event.label, fn.Count()) + .group_by(Event.camera, Event.label) + .dicts() + ) + + update_metrics(stats=stats, event_counts=event_counts) + content, content_type = get_metrics() + return Response(content=content, media_type=content_type) + + +@router.get( + "/genai/models", + dependencies=[Depends(allow_any_authenticated())], + summary="List available GenAI models", + description="Returns available models for each configured GenAI provider.", +) +def genai_models(request: Request): + return JSONResponse(content=request.app.genai_manager.list_models()) + + +@router.post( + "/genai/probe", + dependencies=[Depends(require_role(["admin"]))], + summary="Probe a GenAI provider without saving config", + description=( + "Builds a transient client from the request body and returns its " + "available models. Used to validate provider credentials in the UI " + "before saving the configuration." + ), +) +async def genai_probe(body: GenAIProbeBody): + load_providers() + + provider_cls = PROVIDERS.get(body.provider) + if not provider_cls: + return JSONResponse( + status_code=400, + content={"success": False, "message": "Unknown provider"}, + ) + + # The OpenAI-compatible SDKs accept "timeout" as a constructor kwarg via + # provider_options; other plugins use GenAIClient.timeout passed below. + # Don't inject timeout for Gemini — its HttpOptions interprets the value + # in milliseconds and would clash with the plugin's own default. + probe_provider_options: dict[str, Any] = dict(body.provider_options or {}) + if body.provider in (GenAIProviderEnum.openai, GenAIProviderEnum.azure_openai): + probe_provider_options.setdefault("timeout", _PROBE_TIMEOUT_SECONDS) + + try: + transient_cfg = GenAIConfig( + provider=body.provider, + api_key=body.api_key, + base_url=body.base_url, + provider_options=probe_provider_options, + # model is required by the schema but irrelevant for listing. + model="probe", + roles=[], + ) + except ValidationError: + logger.exception("GenAI probe: invalid configuration") + return JSONResponse( + status_code=400, + content={"success": False, "message": "Invalid provider configuration"}, + ) + + try: + client = provider_cls( + transient_cfg, + timeout=_PROBE_TIMEOUT_SECONDS, + validate_model=False, + ) + except Exception: + logger.exception("GenAI probe: failed to construct client") + return JSONResponse( + content={ + "success": False, + "message": "Failed to connect to provider", + }, + ) + + try: + models = await asyncio.wait_for( + asyncio.to_thread(client.list_models), + timeout=_PROBE_OUTER_TIMEOUT_SECONDS, + ) + except TimeoutError: + return JSONResponse( + content={"success": False, "message": "Probe timed out"}, + ) + except Exception: + logger.exception("GenAI probe: list_models failed") + return JSONResponse( + content={"success": False, "message": "Provider returned no models"}, + ) + + if not models: + return JSONResponse( + content={ + "success": False, + "message": ( + "No models returned. Check the API key, base URL, and " + "that the provider is reachable." + ), + }, + ) + + return JSONResponse(content={"success": True, "models": models}) + + +@router.get("/config", dependencies=[Depends(allow_any_authenticated())]) +def config(request: Request): + config_obj: FrigateConfig = request.app.frigate_config + config: dict[str, dict[str, Any]] = config_obj.model_dump( + mode="json", warnings="none", exclude_none=True + ) + config["detectors"] = { + name: detector.model_dump(mode="json", warnings="none", exclude_none=True) + for name, detector in config_obj.detectors.items() + } + + # remove environment_vars for non-admin users + if request.headers.get("remote-role") != "admin": + config.pop("environment_vars", None) + + # redact mqtt credentials + redact_credential(config["mqtt"], "password") + + # redact proxy secret + redact_credential(config["proxy"], "auth_secret") + + # redact genai api keys + for _genai_name, genai_cfg in config.get("genai", {}).items(): + if isinstance(genai_cfg, dict): + redact_credential(genai_cfg, "api_key") + + for camera_name, camera in request.app.frigate_config.cameras.items(): + camera_dict = config["cameras"][camera_name] + + # redact onvif credentials + onvif_dict = camera_dict.get("onvif", {}) + if onvif_dict: + redact_credential(onvif_dict, "password") + + # clean paths + for input in camera_dict.get("ffmpeg", {}).get("inputs", []): + input["path"] = clean_camera_user_pass(input["path"]) + + # add clean ffmpeg_cmds + camera_dict["ffmpeg_cmds"] = copy.deepcopy(camera.ffmpeg_cmds) + for cmd in camera_dict["ffmpeg_cmds"]: + cmd["cmd"] = clean_camera_user_pass(" ".join(cmd["cmd"])) + + # ensure that zones are relative + for zone_name, zone in config_obj.cameras[camera_name].zones.items(): + camera_dict["zones"][zone_name]["color"] = zone.color + + # Re-dump profile overrides with exclude_unset so that only + # explicitly-set fields are returned (not Pydantic defaults). + # Without this, the frontend merges defaults (e.g. threshold=30) + # over the camera's actual base values (e.g. threshold=20). + if camera.profiles: + for profile_name, profile_config in camera.profiles.items(): + camera_dict.setdefault("profiles", {})[profile_name] = ( + profile_config.model_dump( + mode="json", warnings="none", exclude_unset=True + ) + ) + + # When a profile is active, the top-level camera sections contain + # profile-merged (effective) values. Include the original base + # configs so the frontend settings can display them separately. + if ( + config_obj.active_profile is not None + and request.app.profile_manager is not None + ): + base_sections = request.app.profile_manager.get_base_configs_for_api( + camera_name + ) + if base_sections: + camera_dict["base_config"] = base_sections + + # remove go2rtc stream passwords + go2rtc: dict[str, Any] = config_obj.go2rtc.model_dump( + mode="json", warnings="none", exclude_none=True + ) + for stream_name, stream in go2rtc.get("streams", {}).items(): + if stream is None: + continue + if isinstance(stream, str): + cleaned = clean_camera_user_pass(stream) + else: + cleaned = [] + + for item in stream: + cleaned.append(clean_camera_user_pass(item)) + + config["go2rtc"]["streams"][stream_name] = cleaned + + config["plus"] = {"enabled": request.app.frigate_config.plus_api.is_active()} + config["model"]["colormap"] = config_obj.model.colormap + config["model"]["all_attributes"] = config_obj.model.all_attributes + config["model"]["non_logo_attributes"] = config_obj.model.non_logo_attributes + + # Add model plus data if plus is enabled + if config["plus"]["enabled"]: + model_path = config.get("model", {}).get("path") + if model_path: + model_json_path = FilePath(model_path).with_suffix(".json") + try: + with open(model_json_path) as f: + model_plus_data = json.load(f) + config["model"]["plus"] = model_plus_data + except FileNotFoundError: + config["model"]["plus"] = None + except json.JSONDecodeError: + config["model"]["plus"] = None + else: + config["model"]["plus"] = None + + # use merged labelamp + for detector_config in config["detectors"].values(): + detector_config["model"]["labelmap"] = ( + request.app.frigate_config.model.merged_labelmap + ) + + return JSONResponse(content=config) + + +@router.get("/profiles", dependencies=[Depends(allow_any_authenticated())]) +def get_profiles(request: Request): + """List all available profiles and the currently active profile.""" + profile_manager = request.app.profile_manager + return JSONResponse(content=profile_manager.get_profile_info()) + + +@router.get("/profile/active", dependencies=[Depends(allow_any_authenticated())]) +def get_active_profile(request: Request): + """Get the currently active profile.""" + config_obj: FrigateConfig = request.app.frigate_config + return JSONResponse(content={"active_profile": config_obj.active_profile}) + + +@router.get("/ffmpeg/presets", dependencies=[Depends(allow_any_authenticated())]) +def ffmpeg_presets(): + """Return available ffmpeg preset keys for config UI usage.""" + machine = platform.machine().lower() + is_arm64 = machine in ("aarch64", "arm64", "armv8", "armv7l") + + if is_arm64: + hwaccel_presets = [ + "preset-rpi-64-h264", + "preset-rpi-64-h265", + "preset-jetson-h264", + "preset-jetson-h265", + "preset-rkmpp", + "preset-vaapi", + ] + else: + hwaccel_presets = [ + "preset-vaapi", + "preset-intel-qsv-h264", + "preset-intel-qsv-h265", + "preset-nvidia", + ] + + input_presets = [ + "preset-http-jpeg-generic", + "preset-http-mjpeg-generic", + "preset-http-reolink", + "preset-rtmp-generic", + "preset-rtsp-generic", + "preset-rtsp-restream", + "preset-rtsp-restream-low-latency", + "preset-rtsp-udp", + "preset-rtsp-blue-iris", + ] + record_output_presets = [ + "preset-record-generic", + "preset-record-generic-audio-copy", + "preset-record-generic-audio-aac", + "preset-record-mjpeg", + "preset-record-jpeg", + "preset-record-ubiquiti", + ] + + return JSONResponse( + content={ + "hwaccel_args": hwaccel_presets, + "input_args": input_presets, + "output_args": { + "record": record_output_presets, + "detect": [], + }, + } + ) + + +@router.get("/config/raw_paths", dependencies=[Depends(require_role(["admin"]))]) +def config_raw_paths(request: Request): + """Admin-only endpoint that returns camera paths and go2rtc streams without credential masking.""" + config_obj: FrigateConfig = request.app.frigate_config + + raw_paths = {"cameras": {}, "go2rtc": {"streams": {}}} + + # Extract raw camera ffmpeg input paths + for camera_name, camera in config_obj.cameras.items(): + raw_paths["cameras"][camera_name] = { + "ffmpeg": { + "inputs": [ + {"path": input.path, "roles": input.roles} + for input in camera.ffmpeg.inputs + ] + } + } + + # Extract raw go2rtc stream URLs + go2rtc_config = config_obj.go2rtc.model_dump( + mode="json", warnings="none", exclude_none=True + ) + for stream_name, stream in go2rtc_config.get("streams", {}).items(): + if stream is None: + continue + raw_paths["go2rtc"]["streams"][stream_name] = stream + + return JSONResponse(content=raw_paths) + + +@router.get("/config/raw", dependencies=[Depends(require_role(["admin"]))]) +def config_raw(): + config_file = find_config_file() + + if not os.path.isfile(config_file): + return JSONResponse( + content=({"success": False, "message": "Could not find file"}), + status_code=404, + ) + + with open(config_file) as f: + raw_config = f.read() + f.close() + + return JSONResponse( + content=raw_config, media_type="text/plain", status_code=200 + ) + + +@router.post("/config/save", dependencies=[Depends(require_role(["admin"]))]) +def config_save(save_option: str, body: Any = Body(media_type="text/plain")): + new_config = body.decode() + if not new_config: + return JSONResponse( + content=( + {"success": False, "message": "Config with body param is required"} + ), + status_code=400, + ) + + # Validate the config schema + try: + # Use ruamel to parse and preserve line numbers + yaml_config = ruamel.yaml.YAML() + yaml_config.preserve_quotes = True + full_config = yaml_config.load(StringIO(new_config)) + + FrigateConfig.parse_yaml(new_config) + + except ValidationError as e: + error_message = [] + + for error in e.errors(): + error_path = error["loc"] + current = full_config + line_number = "Unknown" + last_line_number = "Unknown" + + try: + for i, part in enumerate(error_path): + key = int(part) if part.isdigit() else part + + if isinstance(current, ruamel.yaml.comments.CommentedMap): + current = current[key] + elif isinstance(current, list): + current = current[key] + + if hasattr(current, "lc"): + last_line_number = current.lc.line + + if i == len(error_path) - 1: + if hasattr(current, "lc"): + line_number = current.lc.line + else: + line_number = last_line_number + + except Exception: + line_number = "Unable to determine" + + error_message.append( + f"Line {line_number}: {' -> '.join(map(str, error_path))} - {error.get('msg', error.get('type', 'Unknown'))}" + ) + + return JSONResponse( + content=( + { + "success": False, + "message": "Your configuration is invalid.\nSee the official documentation at docs.frigate.video.\n\n" + + "\n".join(error_message), + } + ), + status_code=400, + ) + + except Exception: + return JSONResponse( + content=( + { + "success": False, + "message": f"\nYour configuration is invalid.\nSee the official documentation at docs.frigate.video.\n\n{escape(str(traceback.format_exc()))}", + } + ), + status_code=400, + ) + + # Save the config to file + try: + config_file = find_config_file() + + with open(config_file, "w") as f: + f.write(new_config) + f.close() + except Exception: + return JSONResponse( + content=( + { + "success": False, + "message": "Could not write config file, be sure that Frigate has write permission on the config file.", + } + ), + status_code=400, + ) + + if save_option == "restart": + try: + restart_frigate() + except Exception as e: + logging.error(f"Error restarting Frigate: {e}") + return JSONResponse( + content=( + { + "success": True, + "message": "Config successfully saved, unable to restart Frigate", + } + ), + status_code=200, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Config successfully saved, restarting (this can take up to one minute)...", + } + ), + status_code=200, + ) + else: + return JSONResponse( + content=({"success": True, "message": "Config successfully saved."}), + status_code=200, + ) + + +def _restore_masked_camera_paths(config_data: dict, config: FrigateConfig) -> None: + """Substitute incoming `*:*` masked credentials with the in-memory ones. + + The /config response masks ffmpeg input credentials, so the settings UI + sends the masked path back when sibling fields (e.g. hwaccel_args) are + edited. Without this we'd write `rtsp://*:*@host` into YAML and lose + the real credentials. Mutates `config_data` in place. + """ + cameras = config_data.get("cameras") + if not isinstance(cameras, dict): + return + + for camera_name, camera_data in cameras.items(): + if not isinstance(camera_data, dict): + continue + inputs = camera_data.get("ffmpeg", {}).get("inputs") + if not isinstance(inputs, list): + continue + existing = config.cameras.get(camera_name) + if existing is None: + continue + existing_paths = [inp.path for inp in existing.ffmpeg.inputs] + for index, input_obj in enumerate(inputs): + if not isinstance(input_obj, dict): + continue + path = input_obj.get("path") + if not isinstance(path, str): + continue + if ("://*:*@" in path or "user=*&password=*" in path) and index < len( + existing_paths + ): + input_obj["path"] = existing_paths[index] + + +def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONResponse: + """Apply config changes in-memory only, without writing to YAML. + + Used for temporary config changes like debug replay camera tuning. + Updates the in-memory Pydantic config and publishes ZMQ updates, + bypassing YAML parsing entirely. + """ + try: + updates = {} + if body.config_data: + _restore_masked_camera_paths(body.config_data, request.app.frigate_config) + updates = flatten_config_data(body.config_data) + updates = {k: ("" if v is None else v) for k, v in updates.items()} + # Drop any field whose value is still the redaction sentinel + updates = { + k: v for k, v in updates.items() if v != REDACTED_CREDENTIAL_SENTINEL + } + + if not updates: + return JSONResponse( + content={"success": False, "message": "No configuration data provided"}, + status_code=400, + ) + + config: FrigateConfig = request.app.frigate_config + + # Group flat key paths into nested per-camera, per-section dicts + grouped: dict[str, dict[str, dict]] = {} + for key_path, value in updates.items(): + parts = key_path.split(".") + if len(parts) < 3 or parts[0] != "cameras": + continue + + cam, section = parts[1], parts[2] + grouped.setdefault(cam, {}).setdefault(section, {}) + + # Build nested dict from remaining path (e.g. "filters.person.threshold") + target = grouped[cam][section] + for part in parts[3:-1]: + target = target.setdefault(part, {}) + if len(parts) > 3: + target[parts[-1]] = value + elif isinstance(value, dict): + grouped[cam][section] = deep_merge( + grouped[cam][section], value, override=True + ) + else: + grouped[cam][section] = value + + # Apply each section update + for cam_name, sections in grouped.items(): + camera_config = config.cameras.get(cam_name) + if not camera_config: + return JSONResponse( + content={ + "success": False, + "message": f"Camera '{cam_name}' not found", + }, + status_code=400, + ) + + for section_name, update in sections.items(): + err = apply_section_update(camera_config, section_name, update) + if err is not None: + return JSONResponse( + content={"success": False, "message": err}, + status_code=400, + ) + + # Publish ZMQ updates so processing threads pick up changes + if body.update_topic and body.update_topic.startswith("config/cameras/"): + _, _, camera, field = body.update_topic.split("/") + settings = getattr(config.cameras.get(camera, None), field, None) + + if settings is not None: + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum[field], camera), + settings, + ) + + # detect resize also republishes motion + objects so other + # processes pick up the rebuilt masks, and fires refresh so + # the camera maintainer recycles the camera process to pick + # up the new ffmpeg cmd / SHM sizing + if field == "detect": + cam_cfg = config.cameras.get(camera) + if cam_cfg is not None: + if cam_cfg.motion is not None: + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.motion, camera + ), + cam_cfg.motion, + ) + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.objects, camera + ), + cam_cfg.objects, + ) + if cam_cfg.zones: + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.zones, camera + ), + cam_cfg.zones, + ) + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.refresh, camera + ), + cam_cfg, + ) + + return JSONResponse( + content={"success": True, "message": "Config applied in-memory"}, + status_code=200, + ) + except Exception as e: + logger.error(f"Error applying config in-memory: {e}") + return JSONResponse( + content={"success": False, "message": "Error applying config"}, + status_code=500, + ) + + +@router.put("/config/set", dependencies=[Depends(require_role(["admin"]))]) +def config_set(request: Request, body: AppConfigSetBody): + config_file = find_config_file() + + if body.skip_save: + return _config_set_in_memory(request, body) + + lock = FileLock(f"{config_file}.lock", timeout=5) + + try: + with lock: + with open(config_file) as f: + old_raw_config = f.read() + + try: + updates = {} + + # process query string parameters (takes precedence over body.config_data) + parsed_url = urllib.parse.urlparse(str(request.url)) + query_string = urllib.parse.parse_qs( + parsed_url.query, keep_blank_values=True + ) + + # Filter out empty keys but keep blank values for non-empty keys + query_string = {k: v for k, v in query_string.items() if k} + + if query_string: + updates = process_config_query_string(query_string) + elif body.config_data: + _restore_masked_camera_paths( + body.config_data, request.app.frigate_config + ) + updates = flatten_config_data(body.config_data) + # Convert None values to empty strings for deletion (e.g., when deleting masks) + updates = {k: ("" if v is None else v) for k, v in updates.items()} + # Drop sentinel-valued fields so untouched credential + # placeholders don't clobber the saved YAML value. + updates = { + k: v + for k, v in updates.items() + if v != REDACTED_CREDENTIAL_SENTINEL + } + + if not updates: + return JSONResponse( + content=( + { + "success": False, + "message": "No configuration data provided", + } + ), + status_code=400, + ) + + # apply all updates in a single operation + update_yaml_file_bulk(config_file, updates) + + # validate the updated config + with open(config_file) as f: + new_raw_config = f.read() + + try: + config = FrigateConfig.parse(new_raw_config) + except ValidationError as e: + with open(config_file, "w") as f: + f.write(old_raw_config) + f.close() + logger.error( + f"Config Validation Error:\n\n{str(traceback.format_exc())}" + ) + error_messages = [] + for err in e.errors(): + msg = err.get("msg", "") + # Strip pydantic "Value error, " prefix for cleaner display + if msg.startswith("Value error, "): + msg = msg[len("Value error, ") :] + error_messages.append(msg) + message = ( + "; ".join(error_messages) + if error_messages + else "Check logs for error message." + ) + return JSONResponse( + content=( + { + "success": False, + "message": f"Error saving config: {message}", + } + ), + status_code=400, + ) + except Exception: + with open(config_file, "w") as f: + f.write(old_raw_config) + f.close() + logger.error(f"\nConfig Error:\n\n{str(traceback.format_exc())}") + return JSONResponse( + content=( + { + "success": False, + "message": "Error parsing config. Check logs for error message.", + } + ), + status_code=400, + ) + except Exception as e: + logging.error(f"Error updating config: {e}") + return JSONResponse( + content=({"success": False, "message": "Error updating config"}), + status_code=500, + ) + + # drop runtime overrides for any fields the user just rewrote in + # yaml so a stale override doesn't silently win after restart + if request.app.dispatcher is not None: + request.app.dispatcher.clear_runtime_state_for_yaml_keys(updates.keys()) + + if body.requires_restart == 0 or body.update_topic: + old_config: FrigateConfig = request.app.frigate_config + request.app.frigate_config = config + request.app.genai_manager.update_config(config) + + if request.app.profile_manager is not None: + request.app.profile_manager.update_config(config) + + if request.app.stats_emitter is not None: + request.app.stats_emitter.config = config + + if request.app.dispatcher is not None: + request.app.dispatcher.config = config + for comm in request.app.dispatcher.comms: + comm.config = config + + if body.update_topic: + if body.update_topic.startswith("config/cameras/"): + _, _, camera, field = body.update_topic.split("/") + + if camera == "*": + # Wildcard: fan out update to all cameras + enum_value = CameraConfigUpdateEnum[field] + 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: + 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( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum[field], camera + ), + settings, + ) + else: + # Generic handling for global config updates + settings = config.get_nested_object(body.update_topic) + + # Publish None for removal, actual config for add/update + request.app.config_publisher.publisher.publish( + body.update_topic, settings + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Config successfully updated, restart to apply", + } + ), + status_code=200, + ) + except Timeout: + return JSONResponse( + content=( + { + "success": False, + "message": "Another process is currently updating the config. Please try again in a few seconds.", + } + ), + status_code=503, + ) + + +@router.get("/vainfo", dependencies=[Depends(allow_any_authenticated())]) +def vainfo(): + # Use LibvaGpuSelector to pick an appropriate libva device (if available) + selected_gpu = "" + try: + selected_gpu = _gpu_selector.get_gpu_arg(FFMPEG_HWACCEL_VAAPI, 0) or "" + except Exception: + selected_gpu = "" + + # If selected_gpu is empty, pass None to vainfo_hwaccel to run plain `vainfo`. + vainfo = vainfo_hwaccel(device_name=selected_gpu or None) + return JSONResponse( + content={ + "return_code": vainfo.returncode, + "stderr": ( + vainfo.stderr.decode("unicode_escape").strip() + if vainfo.returncode != 0 + else "" + ), + "stdout": ( + vainfo.stdout.decode("unicode_escape").strip() + if vainfo.returncode == 0 + else "" + ), + } + ) + + +@router.get("/nvinfo", dependencies=[Depends(allow_any_authenticated())]) +def nvinfo(): + return JSONResponse(content=get_nvidia_driver_info()) + + +@router.get( + "/logs/{service}", + tags=[Tags.logs], + dependencies=[Depends(require_role(["admin"]))], +) +async def logs( + service: str = Path(enum=["frigate", "nginx", "go2rtc"]), + download: str | None = None, + stream: bool | None = False, + start: int | None = 0, + end: int | None = None, +): + """Get logs for the requested service (frigate/nginx/go2rtc)""" + + def download_logs(service_location: str): + try: + file = open(service_location) + contents = file.read() + file.close() + return JSONResponse(jsonable_encoder(contents)) + except FileNotFoundError as e: + logger.error(e) + return JSONResponse( + content={"success": False, "message": "Could not find log file"}, + status_code=500, + ) + + async def stream_logs(file_path: str): + """Asynchronously stream log lines.""" + buffer = "" + try: + async with aiofiles.open(file_path) as file: + await file.seek(0, 2) + while True: + line = await file.readline() + if line: + buffer += line + # Process logs only when there are enough lines in the buffer + if "\n" in buffer: + _, processed_lines = process_logs(buffer, service) + buffer = "" + for processed_line in processed_lines: + yield f"{processed_line}\n" + else: + await asyncio.sleep(0.1) + except FileNotFoundError: + yield "Log file not found.\n" + + log_locations = { + "frigate": "/dev/shm/logs/frigate/current", + "go2rtc": "/dev/shm/logs/go2rtc/current", + "nginx": "/dev/shm/logs/nginx/current", + } + service_location = log_locations.get(service) + + if not service_location: + return JSONResponse( + content={"success": False, "message": "Not a valid service"}, + status_code=404, + ) + + if download: + return download_logs(service_location) + + if stream: + return StreamingResponse(stream_logs(service_location), media_type="text/plain") + + # For full logs initially + try: + async with aiofiles.open(service_location) as file: + contents = await file.read() + + total_lines, log_lines = process_logs(contents, service, start, end) + return JSONResponse( + content={"totalLines": total_lines, "lines": log_lines}, + status_code=200, + ) + except FileNotFoundError as e: + logger.error(e) + return JSONResponse( + content={"success": False, "message": "Could not find log file"}, + status_code=500, + ) + + +@router.post("/restart", dependencies=[Depends(require_role(["admin"]))]) +def restart(): + try: + restart_frigate() + except Exception as e: + logging.error(f"Error restarting Frigate: {e}") + return JSONResponse( + content=( + { + "success": False, + "message": "Unable to restart Frigate.", + } + ), + status_code=500, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Restarting (this can take up to one minute)...", + } + ), + status_code=200, + ) + + +@router.post( + "/media/sync", + dependencies=[Depends(require_role(["admin"]))], + summary="Start media sync job", + description="""Start an asynchronous media sync job to find and (optionally) remove orphaned media files. + Returns 202 with job details when queued, or 409 if a job is already running.""", +) +def sync_media(body: MediaSyncBody = Body(...)): + """Start async media sync job - remove orphaned files. + + Syncs specified media types: event snapshots, event thumbnails, review thumbnails, + previews, exports, and/or recordings. Job runs in background; use /media/sync/current + or /media/sync/status/{job_id} to check status. + + Args: + body: MediaSyncBody with dry_run flag and media_types list. + media_types can include: 'all', 'event_snapshots', 'event_thumbnails', + 'review_thumbnails', 'previews', 'exports', 'recordings' + + Returns: + 202 Accepted with job_id, or 409 Conflict if job already running. + """ + job_id = start_media_sync_job( + dry_run=body.dry_run, + media_types=body.media_types, + force=body.force, + verbose=body.verbose, + ) + + if job_id is None: + # A job is already running + current = get_current_media_sync_job() + return JSONResponse( + content={ + "error": "A media sync job is already running", + "current_job_id": current.id if current else None, + }, + status_code=409, + ) + + return JSONResponse( + content={ + "job": { + "job_type": "media_sync", + "status": JobStatusTypesEnum.queued, + "id": job_id, + } + }, + status_code=202, + ) + + +@router.get( + "/media/sync/current", + dependencies=[Depends(require_role(["admin"]))], + summary="Get current media sync job", + description="""Retrieve the current running media sync job, if any. Returns the job details + or null when no job is active.""", +) +def get_media_sync_current(): + """Get the current running media sync job, if any.""" + job = get_current_media_sync_job() + + if job is None: + return JSONResponse(content={"job": None}, status_code=200) + + return JSONResponse( + content={"job": job.to_dict()}, + status_code=200, + ) + + +@router.get( + "/media/sync/status/{job_id}", + dependencies=[Depends(require_role(["admin"]))], + summary="Get media sync job status", + description="""Get status and results for the specified media sync job id. Returns 200 with + job details including results, or 404 if the job is not found.""", +) +def get_media_sync_status(job_id: str): + """Get the status of a specific media sync job.""" + job = get_media_sync_job_by_id(job_id) + + if job is None: + return JSONResponse( + content={"error": "Job not found"}, + status_code=404, + ) + + return JSONResponse( + content={"job": job.to_dict()}, + status_code=200, + ) + + +@router.get("/labels", dependencies=[Depends(allow_any_authenticated())]) +def get_labels( + camera: str = "", + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + try: + if camera: + if camera not in allowed_cameras: + return JSONResponse( + content={ + "success": False, + "message": f"Access denied to camera '{camera}'", + }, + status_code=403, + ) + events = Event.select(Event.label).where(Event.camera == camera).distinct() + else: + events = ( + Event.select(Event.label) + .where(Event.camera << allowed_cameras) + .distinct() + ) + except Exception as e: + logger.error(e) + return JSONResponse( + content=({"success": False, "message": "Failed to get labels"}), + status_code=404, + ) + + labels = sorted([e.label for e in events]) + return JSONResponse(content=labels) + + +@router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())]) +def get_sub_labels( + split_joined: int | None = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + try: + events = ( + Event.select(Event.sub_label) + .where(Event.camera << allowed_cameras) + .distinct() + ) + except Exception: + return JSONResponse( + content=({"success": False, "message": "Failed to get sub_labels"}), + status_code=404, + ) + + sub_labels = [e.sub_label for e in events] + + if None in sub_labels: + sub_labels.remove(None) + + if split_joined: + original_labels = sub_labels.copy() + + for label in original_labels: + if "," in label: + sub_labels.remove(label) + parts = label.split(",") + + for part in parts: + if part.strip() not in sub_labels: + sub_labels.append(part.strip()) + + sub_labels.sort() + return JSONResponse(content=sub_labels) + + +@router.get("/audio_labels", dependencies=[Depends(allow_any_authenticated())]) +def get_audio_labels(): + labels = load_labels("/audio-labelmap.txt", prefill=521) + return JSONResponse(content=labels) + + +@router.get("/plus/models", dependencies=[Depends(allow_any_authenticated())]) +def plusModels(request: Request, filterByCurrentModelDetector: bool = False): + if not request.app.frigate_config.plus_api.is_active(): + return JSONResponse( + content=({"success": False, "message": "Frigate+ is not enabled"}), + status_code=400, + ) + + models: dict[Any, Any] = request.app.frigate_config.plus_api.get_models() + + if not models["list"]: + return JSONResponse( + content=({"success": False, "message": "No models found"}), + status_code=400, + ) + + modelList = models["list"] + + # current model type + modelType = request.app.frigate_config.model.model_type + + # current detectorType for comparing to supportedDetectors + detectorType = list(request.app.frigate_config.detectors.values())[0].type + + validModels = [] + + for model in sorted( + filter( + lambda m: ( + not filterByCurrentModelDetector + or (detectorType in m["supportedDetectors"] and modelType in m["type"]) + ), + modelList, + ), + key=(lambda m: m["trainDate"]), + reverse=True, + ): + validModels.append(model) + + return JSONResponse(content=validModels) + + +@router.get( + "/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())] +) +def get_recognized_license_plates( + split_joined: int | None = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + try: + query = ( + Event.select( + SQL("json_extract(data, '$.recognized_license_plate') AS plate") + ) + .where( + (SQL("json_extract(data, '$.recognized_license_plate') IS NOT NULL")) + & (Event.camera << allowed_cameras) + ) + .distinct() + ) + recognized_license_plates = [row[0] for row in query.tuples()] + except Exception: + return JSONResponse( + content=( + {"success": False, "message": "Failed to get recognized license plates"} + ), + status_code=404, + ) + + if split_joined: + original_recognized_license_plates = recognized_license_plates.copy() + for recognized_license_plate in original_recognized_license_plates: + if recognized_license_plate and "," in recognized_license_plate: + recognized_license_plates.remove(recognized_license_plate) + parts = recognized_license_plate.split(",") + for part in parts: + if part.strip() not in recognized_license_plates: + recognized_license_plates.append(part.strip()) + + recognized_license_plates = list(set(recognized_license_plates)) + recognized_license_plates.sort() + return JSONResponse(content=recognized_license_plates) + + +@router.get("/timeline", dependencies=[Depends(allow_any_authenticated())]) +def timeline( + camera: str = "all", + limit: int = 100, + source_id: str | None = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + clauses = [] + + selected_columns = [ + Timeline.timestamp, + Timeline.camera, + Timeline.source, + Timeline.source_id, + Timeline.class_type, + Timeline.data, + ] + + if camera != "all": + clauses.append(Timeline.camera == camera) + + if source_id: + source_ids = [sid.strip() for sid in source_id.split(",")] + if len(source_ids) == 1: + clauses.append(Timeline.source_id == source_ids[0]) + else: + clauses.append(Timeline.source_id.in_(source_ids)) + + # Enforce per-camera access control + clauses.append(Timeline.camera << allowed_cameras) + + if len(clauses) == 0: + clauses.append(True) + + timeline = ( + Timeline.select(*selected_columns) + .where(reduce(operator.and_, clauses)) + .order_by(Timeline.timestamp.asc()) + .limit(limit) + .dicts() + ) + + return JSONResponse(content=[t for t in timeline]) + + +@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())]) +def hourly_timeline( + params: AppTimelineHourlyQueryParameters = Depends(), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + """Get hourly summary for timeline.""" + cameras = params.cameras + labels = params.labels + before = params.before + after = params.after + limit = params.limit + tz_name = params.timezone + + _, minute_modifier, _ = get_tz_modifiers(tz_name) + minute_offset = int(minute_modifier.split(" ")[0]) + + clauses = [] + + if cameras != "all": + camera_list = cameras.split(",") + clauses.append(Timeline.camera << camera_list) + + # Enforce per-camera access control + clauses.append(Timeline.camera << allowed_cameras) + + if labels != "all": + label_list = labels.split(",") + clauses.append(Timeline.data["label"] << label_list) + + if before: + clauses.append(Timeline.timestamp < before) + + if after: + clauses.append(Timeline.timestamp > after) + + if len(clauses) == 0: + clauses.append(True) + + timeline = ( + Timeline.select( + Timeline.camera, + Timeline.timestamp, + Timeline.data, + Timeline.class_type, + Timeline.source_id, + Timeline.source, + ) + .where(reduce(operator.and_, clauses)) + .order_by(Timeline.timestamp.desc()) + .limit(limit) + .dicts() + .iterator() + ) + + count = 0 + start = 0 + end = 0 + hours: dict[str, list[dict[str, Any]]] = {} + + for t in timeline: + if count == 0: + start = t["timestamp"] + else: + end = t["timestamp"] + + count += 1 + + hour = ( + datetime.fromtimestamp(t["timestamp"]).replace( + minute=0, second=0, microsecond=0 + ) + + timedelta( + minutes=minute_offset, + ) + ).timestamp() + if hour not in hours: + hours[hour] = [t] + else: + hours[hour].insert(0, t) + + return JSONResponse( + content={ + "start": start, + "end": end, + "count": count, + "hours": hours, + } + ) diff --git a/frigate/api/auth.py b/frigate/api/auth.py new file mode 100644 index 0000000..a6a4c65 --- /dev/null +++ b/frigate/api/auth.py @@ -0,0 +1,1245 @@ +"""Auth apis.""" + +import base64 +import hashlib +import ipaddress +import json +import logging +import os +import re +import secrets +import time +from datetime import datetime +from pathlib import Path +from urllib.parse import parse_qs, urlparse + +from fastapi import APIRouter, Depends, HTTPException, Request, Response +from fastapi.responses import JSONResponse, RedirectResponse +from joserfc import jwt +from peewee import DoesNotExist +from slowapi import Limiter + +from frigate.api.defs.request.app_body import ( + AppPostLoginBody, + AppPostUsersBody, + AppPutPasswordBody, + AppPutRoleBody, +) +from frigate.api.defs.tags import Tags +from frigate.api.media_auth import ( + check_camera_access, + deny_response_for_media_uri, + is_role_restricted, +) +from frigate.config import AuthConfig, NetworkingConfig, ProxyConfig +from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM +from frigate.models import User + +logger = logging.getLogger(__name__) + +# In-memory cache to track which clients we've logged for an anonymous access event. +# Keyed by a hashed value combining remote address + user-agent. The value is +# an expiration timestamp (float). +FIRST_LOAD_TTL_SECONDS = 60 * 60 * 24 * 7 # 7 days +_first_load_seen: dict[str, float] = {} + + +def require_admin_by_default(): + """ + Global admin requirement dependency for all endpoints by default. + + This is set as the default dependency on the FastAPI app to ensure all + endpoints require admin access unless explicitly overridden with + allow_public(), allow_any_authenticated(), or require_role(). + + Internal port always has admin role set by the /auth endpoint, + so this check passes automatically for internal requests. + + Certain paths are exempted from the global admin check because they must + be accessible before authentication (login, auth) or they have their own + route-level authorization dependencies that handle access control. + """ + # Paths that have route-level auth dependencies and should bypass global admin check + # These paths still have authorization - it's handled by their route-level dependencies + EXEMPT_PATHS = { + # Public auth endpoints (allow_public) + "/auth", + "/auth/first_time_login", + "/login", + "/logout", + # Authenticated user endpoints (allow_any_authenticated) + "/profile", + "/profiles", + # Public info endpoints (allow_public) + "/", + "/version", + "/config/schema.json", + # Authenticated user endpoints (allow_any_authenticated) + "/metrics", + "/stats", + "/stats/history", + "/config", + "/vainfo", + "/nvinfo", + "/labels", + "/sub_labels", + "/plus/models", + "/recognized_license_plates", + "/timeline", + "/timeline/hourly", + "/recordings/storage", + "/recordings/summary", + "/recordings/unavailable", + "/go2rtc/streams", + "/event_ids", + "/events", + "/cases", + "/exports", + "/jobs/export", + } + + # Path prefixes that should be exempt (for paths with parameters) + EXEMPT_PREFIXES = ( + "/logs/", # /logs/{service} + "/review", # /review, /review/{id}, /review/summary, /review_ids, etc. + "/reviews/", # /reviews/viewed, /reviews/delete + "/events/", # /events/{id}/thumbnail, /events/summary, etc. (camera-scoped) + "/export/", # /export/{camera}/start/..., /export/{id}/rename, /export/{id} + "/go2rtc/streams/", # /go2rtc/streams/{camera} + "/users/", # /users/{username}/password (has own auth) + "/preview/", # /preview/{file}/thumbnail.jpg + "/cases/", # /cases/{case_id} + "/exports/", # /exports/{export_id} + "/jobs/export/", # /jobs/export/{export_id} + "/vod/", # /vod/{camera_name}/... + "/notifications/", # /notifications/pubkey, /notifications/register + ) + + async def admin_checker(request: Request): + path = request.url.path + + # Check exact path matches + if path in EXEMPT_PATHS: + return + + # Check prefix matches for parameterized paths + if path.startswith(EXEMPT_PREFIXES): + return + + # Dynamic camera path exemption: + # Any path whose first segment matches a configured camera name should + # bypass the global admin requirement. These endpoints enforce access + # via route-level dependencies (e.g. require_camera_access) to ensure + # per-camera authorization. This allows non-admin authenticated users + # (e.g. viewer role) to access camera-specific resources without + # needing admin privileges. + try: + if path.startswith("/"): + first_segment = path.split("/", 2)[1] + if ( + first_segment + and first_segment in request.app.frigate_config.cameras + ): + return + except Exception: + pass + + # For all other paths, require admin role + # Internal port requests have admin role set automatically + role = request.headers.get("remote-role") + if role == "admin": + return + + raise HTTPException( + status_code=403, + detail="Access denied. A user with the admin role is required.", + ) + + return admin_checker + + +def _is_authenticated(request: Request) -> bool: + """ + Helper to determine if a request is from an authenticated user. + + Returns True if the request has a valid authenticated user (not anonymous). + Internal port requests are considered anonymous despite having admin role. + """ + username = request.headers.get("remote-user") + return username is not None and username != "anonymous" + + +def allow_public(): + """ + Override dependency to allow unauthenticated access to an endpoint. + + Use this for endpoints that should be publicly accessible without + authentication, such as login page, health checks, or pre-auth info. + + Example: + @router.get("/public-endpoint", dependencies=[Depends(allow_public())]) + """ + + async def public_checker(request: Request): + return # Always allow + + return public_checker + + +def allow_any_authenticated(): + """ + Override dependency to allow any request that passed through the /auth endpoint. + + Allows: + - Port 5000 internal requests (remote-user: "anonymous", remote-role: "admin") + - Authenticated users with JWT tokens (remote-user: username) + - Unauthenticated requests when auth is disabled (remote-user: "viewer") + + Rejects: + - Requests with no remote-user header (did not pass through /auth endpoint) + - External port requests with anonymous user (auth disabled, no proxy auth) + + Example: + @router.get("/authenticated-endpoint", dependencies=[Depends(allow_any_authenticated())]) + """ + + async def auth_checker(request: Request): + # Ensure a remote-user has been set by the /auth endpoint + username = request.headers.get("remote-user") + + # Internal port requests have admin role and should be allowed + role = request.headers.get("remote-role") + + if role != "admin": + if username is None or not _is_authenticated(request): + raise HTTPException(status_code=401, detail="Authentication required") + + return + + return auth_checker + + +router = APIRouter(tags=[Tags.auth]) + + +@router.get("/auth/first_time_login", dependencies=[Depends(allow_public())]) +def first_time_login(request: Request): + """Return whether the admin first-time login help flag is set in config. + + This endpoint is intentionally unauthenticated so the login page can + query it before a user is authenticated. + """ + auth_config = request.app.frigate_config.auth + + return JSONResponse( + content={ + "admin_first_time_login": auth_config.admin_first_time_login + or auth_config.reset_admin_password + } + ) + + +class RateLimiter: + _limit = "" + + def set_limit(self, limit: str): + self._limit = limit + + def get_limit(self) -> str: + return self._limit + + +rateLimiter = RateLimiter() + + +def get_remote_addr(request: Request): + # fall back to the direct TCP peer when no proxy chain is present + direct_addr = request.client.host if request.client else None + + forwarded_for = request.headers.get("x-forwarded-for") + if not forwarded_for: + return direct_addr or "127.0.0.1" + + route = list(reversed(forwarded_for.split(","))) + logger.debug(f"IP Route: {[r for r in route]}") + trusted_proxies = [] + for proxy in request.app.frigate_config.auth.trusted_proxies: + try: + network = ipaddress.ip_network(proxy) + except ValueError: + logger.warning(f"Unable to parse trusted network: {proxy}") + trusted_proxies.append(network) + + # return the first remote address that is not trusted + for addr in route: + ip = ipaddress.ip_address(addr.strip()) + logger.debug(f"Checking {ip} (v{ip.version})") + trusted = False + for trusted_proxy in trusted_proxies: + logger.debug( + f"Checking against trusted proxy: {trusted_proxy} (v{trusted_proxy.version})" + ) + if trusted_proxy.version == 4: + ipv4 = ip.ipv4_mapped if ip.version == 6 else ip + if ipv4 is not None and ipv4 in trusted_proxy: + trusted = True + logger.debug(f"Trusted: {str(ip)} by {str(trusted_proxy)}") + break + elif trusted_proxy.version == 6 and ip.version == 6: + if ip in trusted_proxy: + trusted = True + logger.debug(f"Trusted: {str(ip)} by {str(trusted_proxy)}") + break + if trusted: + logger.debug(f"{ip} is trusted") + continue + else: + logger.debug(f"First untrusted IP: {str(ip)}") + return str(ip) + + # every hop in the route was trusted, so fall back to the direct peer + return direct_addr or "127.0.0.1" + + +def _cleanup_first_load_seen() -> None: + """Cleanup expired entries in the in-memory first-load cache.""" + now = time.time() + # Build list for removal to avoid mutating dict during iteration + expired = [k for k, exp in _first_load_seen.items() if exp <= now] + for k in expired: + del _first_load_seen[k] + + +def get_jwt_secret() -> str: + jwt_secret = None + # check env var + if JWT_SECRET_ENV_VAR in os.environ: + logger.debug( + f"Using jwt secret from {JWT_SECRET_ENV_VAR} environment variable." + ) + jwt_secret = os.environ.get(JWT_SECRET_ENV_VAR) + # check docker secrets + elif os.path.isfile(os.path.join("/run/secrets", JWT_SECRET_ENV_VAR)): + logger.debug(f"Using jwt secret from {JWT_SECRET_ENV_VAR} docker secret file.") + jwt_secret = ( + Path(os.path.join("/run/secrets", JWT_SECRET_ENV_VAR)).read_text().strip() + ) + # check for the add-on options file + elif os.path.isfile("/data/options.json"): + with open("/data/options.json") as f: + raw_options = f.read() + logger.debug("Using jwt secret from Home Assistant Add-on options file.") + options = json.loads(raw_options) + jwt_secret = options.get("jwt_secret") + + if jwt_secret is None: + jwt_secret_file = os.path.join(CONFIG_DIR, ".jwt_secret") + # check .jwt_secrets file + if not os.path.isfile(jwt_secret_file): + logger.debug( + "No jwt secret found. Generating one and storing in .jwt_secret file in config directory." + ) + jwt_secret = secrets.token_hex(64) + try: + fd = os.open( + jwt_secret_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600 + ) + with os.fdopen(fd, "w") as f: + f.write(str(jwt_secret)) + except Exception: + logger.warning( + "Unable to write jwt token file to config directory. A new jwt token will be created at each startup." + ) + else: + logger.debug("Using jwt secret from .jwt_secret file in config directory.") + with open(jwt_secret_file) as f: + try: + jwt_secret = f.readline().strip() + except Exception: + logger.warning( + "Unable to read jwt token from .jwt_secret file in config directory. A new jwt token will be created at each startup." + ) + jwt_secret = secrets.token_hex(64) + + if len(jwt_secret) < 64: + logger.warning("JWT Secret is recommended to be 64 characters or more") + + return jwt_secret + + +def hash_password(password: str, salt=None, iterations=600000): + if salt is None: + salt = secrets.token_hex(16) + assert salt and isinstance(salt, str) and "$" not in salt + assert isinstance(password, str) + pw_hash = hashlib.pbkdf2_hmac( + "sha256", password.encode("utf-8"), salt.encode("utf-8"), iterations + ) + b64_hash = base64.b64encode(pw_hash).decode("ascii").strip() + return "{}${}${}${}".format(PASSWORD_HASH_ALGORITHM, iterations, salt, b64_hash) + + +def verify_password(password, password_hash): + if (password_hash or "").count("$") != 3: + return False + algorithm, iterations, salt, b64_hash = password_hash.split("$", 3) + iterations = int(iterations) + assert algorithm == PASSWORD_HASH_ALGORITHM + compare_hash = hash_password(password, salt, iterations) + return secrets.compare_digest(password_hash, compare_hash) + + +def validate_password_strength(password: str) -> tuple[bool, str | None]: + """ + Validate password strength. + + Returns a tuple of (is_valid, error_message). + + Longer passwords are harder to crack than shorter complex ones. + https://pages.nist.gov/800-63-3/sp800-63b.html + """ + if not password: + return False, "Password cannot be empty" + + if len(password) < 12: + return False, "Password must be at least 12 characters long" + + return True, None + + +def create_encoded_jwt(user, role, expiration, secret): + return jwt.encode( + {"alg": "HS256"}, + {"sub": user, "role": role, "exp": expiration, "iat": int(time.time())}, + secret, + ) + + +def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, secure): + # TODO: ideally this would set secure as well, but that requires TLS + # SameSite is intentionally left unset (browsers default to Lax). Setting + # SameSite=Lax/Strict would stop the cookie from being sent in cross-origin + # iframes, breaking embedded views such as the Home Assistant Frigate card. + # CSRF is instead mitigated by requiring a custom X-CSRF-TOKEN header, which + # cross-origin pages cannot set without a CORS preflight that Frigate never + # grants (see check_csrf in api/fastapi_app.py). + response.set_cookie( + key=cookie_name, + value=encoded_jwt, + httponly=True, + expires=expiration, + secure=secure, + ) + + +async def get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + + if not username or not role: + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + + return {"username": username, "role": role} + + +def require_role(required_roles: list[str]): + async def role_checker(request: Request): + proxy_config: ProxyConfig = request.app.frigate_config.proxy + config_roles = list(request.app.frigate_config.auth.roles.keys()) + + # Get role from header (could be comma-separated) + role_header = request.headers.get("remote-role") + roles = ( + [r.strip() for r in role_header.split(proxy_config.separator)] + if role_header + else [] + ) + + # Check if we have any roles + if not roles: + raise HTTPException(status_code=403, detail="Role not provided") + + # enforce config roles + valid_roles = [r for r in roles if r in config_roles] + if not valid_roles: + raise HTTPException( + status_code=403, + detail=f"No valid roles found in {roles}. Required: {', '.join(required_roles)}. Available: {', '.join(config_roles)}", + ) + + if not any(role in required_roles for role in valid_roles): + raise HTTPException( + status_code=403, + detail=f"Role {', '.join(valid_roles)} not authorized. Required: {', '.join(required_roles)}", + ) + + return next( + (role for role in valid_roles if role in required_roles), valid_roles[0] + ) + + return role_checker + + +def resolve_role( + headers: dict, proxy_config: ProxyConfig, config_roles: set[str] +) -> str: + """ + Determine the effective role for a request based on proxy headers and configuration. + + Order of resolution: + 1. If a role header is defined in proxy_config.header_map.role: + - If a role_map is configured, treat the header as group claims + (split by proxy_config.separator) and map to roles. + Admin matches short-circuit to admin. + - If no role_map is configured, treat the header as role names directly. + 2. If no valid role is found, return proxy_config.default_role if it's valid in config_roles, else 'viewer'. + + Args: + headers (dict): Incoming request headers (case-insensitive). + proxy_config (ProxyConfig): Proxy configuration. + config_roles (set[str]): Set of valid roles from config. + + Returns: + str: Resolved role (one of config_roles or validated default). + """ + default_role = proxy_config.default_role + role_header = proxy_config.header_map.role + + # Validate default_role against config; fallback to 'viewer' if invalid + validated_default = default_role if default_role in config_roles else "viewer" + if not config_roles: + validated_default = "viewer" # Edge case: no roles defined + + if not role_header: + logger.debug( + "No role header configured in proxy_config.header_map. Returning validated default role '%s'.", + validated_default, + ) + return validated_default + + raw_value = headers.get(role_header, "") + logger.debug("Raw role header value from '%s': %r", role_header, raw_value) + + if not raw_value: + logger.debug( + "Role header missing or empty. Returning validated default role '%s'.", + validated_default, + ) + return validated_default + + # role_map configured, treat header as group claims + if proxy_config.header_map.role_map: + groups = [ + g.strip() for g in raw_value.split(proxy_config.separator) if g.strip() + ] + logger.debug("Parsed groups from role header: %s", groups) + + matched_roles = { + role_name + for role_name, required_groups in proxy_config.header_map.role_map.items() + if any(group in groups for group in required_groups) + } + logger.debug("Matched roles from role_map: %s", matched_roles) + + # If admin matches, prioritize it to avoid accidental downgrade when + # users belong to both admin and lower-privilege groups. + if "admin" in matched_roles and "admin" in config_roles: + logger.debug("Resolved role (with role_map) to 'admin'.") + return "admin" + + if matched_roles: + resolved = next( + (r for r in config_roles if r in matched_roles), validated_default + ) + logger.debug("Resolved role (with role_map) to '%s'.", resolved) + return resolved + + logger.debug( + "No role_map match for groups '%s'. Using validated default role '%s'.", + raw_value, + validated_default, + ) + return validated_default + + # no role_map, treat as role names directly + roles_from_header = [ + r.strip().lower() for r in raw_value.split(proxy_config.separator) if r.strip() + ] + logger.debug("Parsed roles directly from header: %s", roles_from_header) + + resolved = next( + (r for r in config_roles if r in roles_from_header), + validated_default, + ) + if resolved == validated_default and roles_from_header: + logger.debug( + "Provided proxy role header values '%s' did not contain a valid role. Using validated default role '%s'.", + raw_value, + validated_default, + ) + else: + logger.debug("Resolved role (direct header) to '%s'.", resolved) + + return resolved + + +# Endpoints +@router.get( + "/auth", + dependencies=[Depends(allow_public())], + summary="Authenticate request", + description=( + "Authenticates the current request based on proxy headers or JWT token. " + "This endpoint verifies authentication credentials and manages JWT token refresh. " + "On success, no JSON body is returned; authentication state is communicated via response headers and cookies." + ), + status_code=202, + responses={ + 202: { + "description": "Authentication Accepted (no response body)", + "headers": { + "remote-user": { + "description": 'Authenticated username or "viewer" in proxy-only mode', + "schema": {"type": "string"}, + }, + "remote-role": { + "description": "Resolved role (e.g., admin, viewer, or custom)", + "schema": {"type": "string"}, + }, + "Set-Cookie": { + "description": "May include refreshed JWT cookie when applicable", + "schema": {"type": "string"}, + }, + }, + }, + 401: {"description": "Authentication Failed"}, + }, +) +def auth(request: Request): + auth_config: AuthConfig = request.app.frigate_config.auth + proxy_config: ProxyConfig = request.app.frigate_config.proxy + networking_config: NetworkingConfig = request.app.frigate_config.networking + + success_response = Response("", status_code=202) + + # handle case where internal port is a string with ip:port + internal_port = networking_config.listen.internal + if type(internal_port) is str: + internal_port = int(internal_port.split(":")[-1]) + + # dont require auth if the request is on the internal port + # this header is set by Frigate's nginx proxy, so it cant be spoofed + if int(request.headers.get("x-server-port", default=0)) == internal_port: + success_response.headers["remote-user"] = "anonymous" + success_response.headers["remote-role"] = "admin" + return success_response + + fail_response = Response("", status_code=401) + + # ensure the proxy secret matches if configured + if ( + proxy_config.auth_secret is not None + and request.headers.get("x-proxy-secret", "") != proxy_config.auth_secret + ): + logger.debug("X-Proxy-Secret header does not match configured secret value") + return fail_response + + original_url = request.headers.get("x-original-url") + frigate_config = request.app.frigate_config + + # if auth is disabled, just apply the proxy header map and return success + if not auth_config.enabled: + # pass the user header value from the upstream proxy if a mapping is specified + # or use viewer if none are specified + user_header = proxy_config.header_map.user + success_response.headers["remote-user"] = ( + request.headers.get(user_header, default="viewer") + if user_header + else "viewer" + ) + + # parse header and resolve a valid role + config_roles_set = set(auth_config.roles.keys()) + role = resolve_role(request.headers, proxy_config, config_roles_set) + + success_response.headers["remote-role"] = role + + deny_status = deny_response_for_media_uri(original_url, role, frigate_config) + if deny_status is not None: + return Response("", status_code=deny_status) + + deny_status = deny_response_for_go2rtc_stream(original_url, role, request) + if deny_status is not None: + return Response("", status_code=deny_status) + + return success_response + + # now apply authentication + fail_response.headers["location"] = "/login" + + JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name + JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure + JWT_REFRESH = request.app.frigate_config.auth.refresh_time + JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length + + jwt_source = None + encoded_token = None + if "authorization" in request.headers and request.headers[ + "authorization" + ].startswith("Bearer "): + jwt_source = "authorization" + logger.debug("Found authorization header") + encoded_token = request.headers["authorization"].replace("Bearer ", "") + elif JWT_COOKIE_NAME in request.cookies: + jwt_source = "cookie" + logger.debug("Found jwt cookie") + encoded_token = request.cookies[JWT_COOKIE_NAME] + + if encoded_token is None: + logger.debug("No jwt token found") + return fail_response + + try: + token = jwt.decode(encoded_token, request.app.jwt_token) + if "sub" not in token.claims: + logger.debug("user not set in jwt token") + return fail_response + if "role" not in token.claims: + logger.debug("role not set in jwt token") + return fail_response + if "exp" not in token.claims: + logger.debug("exp not set in jwt token") + return fail_response + + user = token.claims.get("sub") + role = token.claims.get("role") + current_time = int(time.time()) + + # if the jwt is expired + expiration = int(token.claims.get("exp")) + logger.debug( + f"current time: {datetime.fromtimestamp(current_time).strftime('%c')}" + ) + logger.debug( + f"jwt expires at: {datetime.fromtimestamp(expiration).strftime('%c')}" + ) + logger.debug( + f"jwt refresh at: {datetime.fromtimestamp(expiration - JWT_REFRESH).strftime('%c')}" + ) + if expiration <= current_time: + logger.debug("jwt token expired") + return fail_response + + # if the jwt cookie is expiring soon + if jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time: + logger.debug("jwt token expiring soon, refreshing cookie") + + # Check if password has been changed since token was issued + # If so, force re-login by rejecting the refresh + try: + user_obj = User.get_by_id(user) + if user_obj.password_changed_at is not None: + token_iat = int(token.claims.get("iat", 0)) + password_changed_timestamp = int( + user_obj.password_changed_at.timestamp() + ) + if token_iat < password_changed_timestamp: + logger.debug( + "jwt token issued before password change, rejecting refresh" + ) + return fail_response + except DoesNotExist: + logger.debug("user not found") + return fail_response + + new_expiration = current_time + JWT_SESSION_LENGTH + new_encoded_jwt = create_encoded_jwt( + user, role, new_expiration, request.app.jwt_token + ) + set_jwt_cookie( + success_response, + JWT_COOKIE_NAME, + new_encoded_jwt, + new_expiration, + JWT_COOKIE_SECURE, + ) + + success_response.headers["remote-user"] = user + success_response.headers["remote-role"] = role + + deny_status = deny_response_for_media_uri(original_url, role, frigate_config) + if deny_status is not None: + return Response("", status_code=deny_status) + + deny_status = deny_response_for_go2rtc_stream(original_url, role, request) + if deny_status is not None: + return Response("", status_code=deny_status) + + return success_response + except Exception as e: + logger.error(f"Error parsing jwt: {e}") + return fail_response + + +@router.get( + "/profile", + dependencies=[Depends(allow_any_authenticated())], + summary="Get user profile", + description="Returns the current authenticated user's profile including username, role, and allowed cameras. This endpoint requires authentication and returns information about the user's permissions.", +) +def profile(request: Request): + username = request.headers.get("remote-user", "viewer") + role = request.headers.get("remote-role", "viewer") + + all_camera_names = set(request.app.frigate_config.cameras.keys()) + roles_dict = request.app.frigate_config.auth.roles + allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names) + + response = JSONResponse( + content={"username": username, "role": role, "allowed_cameras": allowed_cameras} + ) + + if username == "anonymous": + try: + remote_addr = get_remote_addr(request) + except Exception: + remote_addr = ( + request.client.host if hasattr(request, "client") else "unknown" + ) + + ua = request.headers.get("user-agent", "") + key_material = f"{remote_addr}|{ua}" + cache_key = hashlib.sha256(key_material.encode()).hexdigest() + + _cleanup_first_load_seen() + now = time.time() + if cache_key not in _first_load_seen: + _first_load_seen[cache_key] = now + FIRST_LOAD_TTL_SECONDS + logger.info(f"Anonymous user access from {remote_addr} ua={ua[:200]}") + + return response + + +@router.get( + "/logout", + dependencies=[Depends(allow_public())], + summary="Logout user", + description="Logs out the current user by clearing the session cookie. After logout, subsequent requests will require re-authentication.", +) +def logout(request: Request): + auth_config: AuthConfig = request.app.frigate_config.auth + response = RedirectResponse("/login", status_code=303) + response.delete_cookie(auth_config.cookie_name) + return response + + +limiter = Limiter(key_func=get_remote_addr) + + +@router.post( + "/login", + dependencies=[Depends(allow_public())], + summary="Login with credentials", + description='Authenticates a user with username and password. Returns a JWT token as a secure HTTP-only cookie that can be used for subsequent API requests. The JWT token can also be retrieved from the response and used as a Bearer token in the Authorization header.\n\nExample using Bearer token:\n```\ncurl -H "Authorization: Bearer " https://frigate_ip:8971/api/profile\n```', +) +@limiter.limit(limit_value=rateLimiter.get_limit) +def login(request: Request, body: AppPostLoginBody): + if not request.app.frigate_config.auth.enabled: + return JSONResponse( + content={"message": "Authentication is disabled"}, status_code=404 + ) + + JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name + JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure + JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length + user = body.user + password = body.password + + try: + db_user: User = User.get_by_id(user) + except DoesNotExist: + return JSONResponse(content={"message": "Login failed"}, status_code=401) + + password_hash = db_user.password_hash + if verify_password(password, password_hash): + role = getattr(db_user, "role", "viewer") + config_roles_set = set(request.app.frigate_config.auth.roles.keys()) + if role not in config_roles_set: + logger.warning( + f"User {db_user.username} has an invalid role {role}, falling back to 'viewer'." + ) + role = "viewer" + expiration = int(time.time()) + JWT_SESSION_LENGTH + encoded_jwt = create_encoded_jwt(user, role, expiration, request.app.jwt_token) + response = Response("", 200) + set_jwt_cookie( + response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE + ) + # Clear admin_first_time_login flag after successful admin login so the + # UI stops showing the first-time login documentation link. + if role == "admin": + request.app.frigate_config.auth.admin_first_time_login = False + + return response + return JSONResponse(content={"message": "Login failed"}, status_code=401) + + +@router.get( + "/users", + dependencies=[Depends(require_role(["admin"]))], + summary="Get all users", + description="Returns a list of all users with their usernames and roles. Requires admin role. Each user object contains the username and assigned role.", +) +def get_users(): + exports = ( + User.select(User.username, User.role).order_by(User.username).dicts().iterator() + ) + return JSONResponse([e for e in exports]) + + +@router.post( + "/users", + dependencies=[Depends(require_role(["admin"]))], + summary="Create new user", + description="Creates a new user with the specified username, password, and role. Requires admin role. Password must be at least 12 characters long.", +) +def create_user( + request: Request, + body: AppPostUsersBody, +): + HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations + config_roles = list(request.app.frigate_config.auth.roles.keys()) + + if not re.match("^[A-Za-z0-9._]+$", body.username): + return JSONResponse(content={"message": "Invalid username"}, status_code=400) + + if body.role not in config_roles: + return JSONResponse( + content={"message": f"Role must be one of: {', '.join(config_roles)}"}, + status_code=400, + ) + + # Validate password strength + is_valid, error_message = validate_password_strength(body.password) + if not is_valid: + return JSONResponse( + content={"message": error_message}, + status_code=400, + ) + + role = body.role or "viewer" + password_hash = hash_password(body.password, iterations=HASH_ITERATIONS) + User.insert( + { + User.username: body.username, + User.password_hash: password_hash, + User.role: role, + User.notification_tokens: [], + } + ).execute() + request.app.config_publisher.publisher.publish("config/auth", None) + return JSONResponse(content={"username": body.username}) + + +@router.delete( + "/users/{username}", + dependencies=[Depends(require_role(["admin"]))], + summary="Delete user", + description="Deletes a user by username. The built-in admin user cannot be deleted. Requires admin role. Returns success message or error if user not found.", +) +def delete_user(request: Request, username: str): + # Prevent deletion of the built-in admin user + if username == "admin": + return JSONResponse( + content={"message": "Cannot delete admin user"}, status_code=403 + ) + + User.delete_by_id(username) + request.app.config_publisher.publisher.publish("config/auth", None) + return JSONResponse(content={"success": True}) + + +@router.put( + "/users/{username}/password", + dependencies=[Depends(allow_any_authenticated())], + summary="Update user password", + description="Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must be at least 12 characters long. If user changes their own password, a new JWT cookie is automatically issued.", +) +async def update_password( + request: Request, + username: str, + body: AppPutPasswordBody, +): + current_user = await get_current_user(request) + if isinstance(current_user, JSONResponse): + # auth failed + return current_user + + current_username = current_user.get("username") + current_role = current_user.get("role") + + # viewers can only change their own password + if current_role == "viewer" and current_username != username: + raise HTTPException( + status_code=403, detail="Viewers can only update their own password" + ) + + HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations + + try: + user = User.get_by_id(username) + except DoesNotExist: + return JSONResponse(content={"message": "User not found"}, status_code=404) + + # Require old_password when non-admin user is changing any password + # Admin users changing passwords do NOT need to provide the current password + if current_role != "admin": + if not body.old_password: + return JSONResponse( + content={"message": "Current password is required"}, + status_code=400, + ) + if not verify_password(body.old_password, user.password_hash): + return JSONResponse( + content={"message": "Current password is incorrect"}, + status_code=401, + ) + + # Validate new password strength + is_valid, error_message = validate_password_strength(body.password) + if not is_valid: + return JSONResponse( + content={"message": error_message}, + status_code=400, + ) + + password_hash = hash_password(body.password, iterations=HASH_ITERATIONS) + User.update( + { + User.password_hash: password_hash, + User.password_changed_at: datetime.now(), + } + ).where(User.username == username).execute() + + response = JSONResponse(content={"success": True}) + + # If user changed their own password, issue a new JWT to keep them logged in + if current_username == username: + JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name + JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure + JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length + + expiration = int(time.time()) + JWT_SESSION_LENGTH + encoded_jwt = create_encoded_jwt( + username, current_role, expiration, request.app.jwt_token + ) + # Set new JWT cookie on response + set_jwt_cookie( + response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE + ) + + return response + + +@router.put( + "/users/{username}/role", + dependencies=[Depends(require_role(["admin"]))], + summary="Update user role", + description="Updates a user's role. The built-in admin user's role cannot be modified. Requires admin role. Valid roles are defined in the configuration.", +) +async def update_role( + request: Request, + username: str, + body: AppPutRoleBody, +): + current_user = await get_current_user(request) + if isinstance(current_user, JSONResponse): + # auth failed + return current_user + + current_role = current_user.get("role") + # viewers can't change anyone's role + if current_role == "viewer": + raise HTTPException( + status_code=403, detail="Admin role is required to change user roles" + ) + if username == "admin": + return JSONResponse( + content={"message": "Cannot modify admin user's role"}, status_code=403 + ) + config_roles = list(request.app.frigate_config.auth.roles.keys()) + if body.role not in config_roles: + return JSONResponse( + content={"message": f"Role must be one of: {', '.join(config_roles)}"}, + status_code=400, + ) + + User.set_by_id(username, {User.role: body.role}) + request.app.config_publisher.publisher.publish("config/auth", None) + return JSONResponse(content={"success": True}) + + +async def require_camera_access( + camera_name: str | None = None, + request: Request = None, +): + """Dependency to enforce camera access based on user role.""" + if camera_name is None: + return # For lists, filter later + + current_user = await get_current_user(request) + if isinstance(current_user, JSONResponse): + detail = "Authentication required" + try: + error_payload = json.loads(current_user.body) + detail = ( + error_payload.get("message") or error_payload.get("detail") or detail + ) + except Exception: + pass + + raise HTTPException(status_code=current_user.status_code, detail=detail) + + role = current_user["role"] + frigate_config = request.app.frigate_config + + if check_camera_access(role, camera_name, frigate_config): + return + + all_camera_names = set(frigate_config.cameras.keys()) + allowed_cameras = User.get_allowed_cameras( + role, frigate_config.auth.roles, all_camera_names + ) + raise HTTPException( + status_code=403, + detail=f"Access denied to camera '{camera_name}'. Allowed: {allowed_cameras}", + ) + + +def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]: + owner_cameras: set[str] = set() + + for camera_name, camera in request.app.frigate_config.cameras.items(): + if stream_name == camera_name: + owner_cameras.add(camera_name) + continue + + if stream_name in camera.live.streams.values(): + owner_cameras.add(camera_name) + + return owner_cameras + + +# nginx proxies these paths straight to go2rtc with authentication-only checks +# (see auth_request.conf). Each names the desired stream via the `src` query +# param, so the camera-level check must happen here in the `/auth` subrequest — +# `require_go2rtc_stream_access` only guards the REST `/go2rtc/streams/{name}` +# endpoint, not these proxied live-stream paths. +GO2RTC_STREAM_PROXY_PATHS = frozenset( + { + "/live/mse/api/ws", + "/live/webrtc/api/ws", + "/api/go2rtc/webrtc", + } +) + + +def deny_response_for_go2rtc_stream( + original_url: str | None, role: str | None, request: Request +) -> int | None: + """Block role-restricted users from go2rtc live streams they cannot access. + + Returns 403 when any `src` stream named in `original_url` resolves to a + camera outside the role's allow-list (or when no `src` is provided on a + stream-proxy path), otherwise None. Mirrors the resolution logic in + `require_go2rtc_stream_access` so substream names map to their owning + camera correctly. + """ + if not original_url: + return None + + parsed = urlparse(original_url) + if parsed.path not in GO2RTC_STREAM_PROXY_PATHS: + return None + + frigate_config = request.app.frigate_config + + # admin and full-access roles (no allow-list) bypass the camera check + if not role or not is_role_restricted(role, frigate_config): + return None + + sources = parse_qs(parsed.query).get("src", []) + if not sources: + # a stream-proxy request naming no stream has nothing legitimate to + # show a restricted user + return 403 + + allowed_cameras = set( + User.get_allowed_cameras( + role, + frigate_config.auth.roles, + set(frigate_config.cameras.keys()), + ) + ) + + # deny if any requested source resolves outside the allow-list + for src in sources: + if not (_get_stream_owner_cameras(request, src) & allowed_cameras): + return 403 + + return None + + +async def require_go2rtc_stream_access( + stream_name: str | None = None, + request: Request = None, +): + """Dependency to enforce go2rtc stream access based on owning camera access.""" + if stream_name is None: + return + + current_user = await get_current_user(request) + if isinstance(current_user, JSONResponse): + detail = "Authentication required" + try: + error_payload = json.loads(current_user.body) + detail = ( + error_payload.get("message") or error_payload.get("detail") or detail + ) + except Exception: + pass + + raise HTTPException(status_code=current_user.status_code, detail=detail) + + role = current_user["role"] + all_camera_names = set(request.app.frigate_config.cameras.keys()) + roles_dict = request.app.frigate_config.auth.roles + allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names) + + # Admin or full access bypasses + if role == "admin" or not roles_dict.get(role): + return + + owner_cameras = _get_stream_owner_cameras(request, stream_name) + + if owner_cameras & set(allowed_cameras): + return + + raise HTTPException( + status_code=403, + detail=f"Access denied to camera '{stream_name}'. Allowed: {allowed_cameras}", + ) + + +async def get_allowed_cameras_for_filter(request: Request): + """Dependency to get allowed_cameras for filtering lists.""" + current_user = await get_current_user(request) + if isinstance(current_user, JSONResponse): + return [] # Unauthorized: no cameras + + role = current_user["role"] + all_camera_names = set(request.app.frigate_config.cameras.keys()) + roles_dict = request.app.frigate_config.auth.roles + return User.get_allowed_cameras(role, roles_dict, all_camera_names) diff --git a/frigate/api/camera.py b/frigate/api/camera.py new file mode 100644 index 0000000..a86c358 --- /dev/null +++ b/frigate/api/camera.py @@ -0,0 +1,1386 @@ +"""Camera apis.""" + +import asyncio +import json +import logging +import re +from importlib.util import find_spec +from pathlib import Path +from urllib.parse import quote_plus + +import httpx +import requests +from fastapi import APIRouter, Depends, Query, Request, Response +from fastapi.responses import JSONResponse +from filelock import FileLock, Timeout +from onvif import ONVIFCamera, ONVIFError +from ruamel.yaml import YAML +from zeep.exceptions import Fault, TransportError +from zeep.transports import AsyncTransport + +from frigate.api.auth import ( + _get_stream_owner_cameras, + allow_any_authenticated, + get_current_user, + require_go2rtc_stream_access, + require_role, +) +from frigate.api.defs.request.app_body import CameraSetBody +from frigate.api.defs.tags import Tags +from frigate.config import FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateTopic, +) +from frigate.config.env import substitute_frigate_vars +from frigate.models import User +from frigate.util.builtin import clean_camera_user_pass, get_record_segment_time +from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files +from frigate.util.config import find_config_file +from frigate.util.image import run_ffmpeg_snapshot +from frigate.util.services import ( + analyze_record_keyframes, + ffprobe_stream, + is_restricted_go2rtc_source, +) + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.camera]) + + +def _is_valid_host(host: str) -> bool: + """ + Validate that the host is in a valid format. + Allows private IPs since cameras are typically on local networks. + Only blocks obviously malicious input to prevent injection attacks. + """ + try: + # Remove port if present + host_without_port = host.split(":")[0] if ":" in host else host + + # Block whitespace, newlines, and control characters + if not host_without_port or re.search(r"[\s\x00-\x1f]", host_without_port): + return False + + # Allow standard hostname/IP characters: alphanumeric, dots, hyphens + if not re.match(r"^[a-zA-Z0-9.-]+$", host_without_port): + return False + + return True + except Exception: + return False + + +@router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())]) +async def go2rtc_streams(request: Request): + r = await asyncio.to_thread(requests.get, "http://127.0.0.1:1984/api/streams") + if not r.ok: + logger.error("Failed to fetch streams from go2rtc") + return JSONResponse( + content=({"success": False, "message": "Error fetching stream data"}), + status_code=500, + ) + stream_data = r.json() + + # Roles with an explicit camera list see only streams owned by an allowed + # camera. Admin and full-access roles (no list / empty list) see all streams. + current_user = await get_current_user(request) + if not isinstance(current_user, JSONResponse): + role = current_user["role"] + roles_dict = request.app.frigate_config.auth.roles + if role != "admin" and roles_dict.get(role): + all_camera_names = set(request.app.frigate_config.cameras.keys()) + allowed_cameras = set( + User.get_allowed_cameras(role, roles_dict, all_camera_names) + ) + stream_data = { + name: data + for name, data in stream_data.items() + if _get_stream_owner_cameras(request, name) & allowed_cameras + } + + for data in stream_data.values(): + for producer in data.get("producers") or []: + producer["url"] = clean_camera_user_pass(producer.get("url", "")) + return JSONResponse(content=stream_data) + + +@router.get( + "/go2rtc/streams/{stream_name}", + dependencies=[Depends(require_go2rtc_stream_access)], +) +def go2rtc_camera_stream(request: Request, stream_name: str): + r = requests.get( + "http://127.0.0.1:1984/api/streams", + params={ + "src": stream_name, + "video": "all", + "audio": "all", + "microphone": "", + }, + ) + if not r.ok: + camera_config = request.app.frigate_config.cameras.get(stream_name) + + if camera_config is None: + for camera_name, camera in request.app.frigate_config.cameras.items(): + if stream_name in camera.live.streams.values(): + camera_config = request.app.frigate_config.cameras.get(camera_name) + break + + if camera_config and camera_config.enabled: + logger.error("Failed to fetch streams from go2rtc") + + return JSONResponse( + content=({"success": False, "message": "Error fetching stream data"}), + status_code=500, + ) + stream_data = r.json() + for producer in stream_data.get("producers", []): + producer["url"] = clean_camera_user_pass(producer.get("url", "")) + return JSONResponse(content=stream_data) + + +@router.put( + "/go2rtc/streams/{stream_name}", dependencies=[Depends(require_role(["admin"]))] +) +def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""): + """Add or update a go2rtc stream configuration.""" + if src and is_restricted_go2rtc_source(src): + logger.warning( + "Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)", + stream_name, + ) + return JSONResponse( + content={ + "success": False, + "message": "Restricted stream source type", + }, + status_code=400, + ) + + try: + params = {"name": stream_name} + if src: + try: + resolved_src = substitute_frigate_vars(src) + except KeyError: + resolved_src = src + + if is_restricted_go2rtc_source(resolved_src): + logger.warning( + "Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)", + stream_name, + ) + return JSONResponse( + content={ + "success": False, + "message": "Restricted stream source type", + }, + status_code=400, + ) + + params["src"] = resolved_src + + r = requests.put( + "http://127.0.0.1:1984/api/streams", + params=params, + timeout=10, + ) + if not r.ok: + logger.error(f"Failed to add go2rtc stream {stream_name}: {r.text}") + return JSONResponse( + content=( + {"success": False, "message": f"Failed to add stream: {r.text}"} + ), + status_code=r.status_code, + ) + return JSONResponse( + content={"success": True, "message": "Stream added successfully"} + ) + except requests.RequestException as e: + logger.error(f"Error communicating with go2rtc: {e}") + return JSONResponse( + content=( + { + "success": False, + "message": "Error communicating with go2rtc", + } + ), + status_code=500, + ) + + +@router.delete( + "/go2rtc/streams/{stream_name}", dependencies=[Depends(require_role(["admin"]))] +) +def go2rtc_delete_stream(stream_name: str): + """Delete a go2rtc stream.""" + try: + r = requests.delete( + "http://127.0.0.1:1984/api/streams", + params={"src": stream_name}, + timeout=10, + ) + if not r.ok: + logger.error(f"Failed to delete go2rtc stream {stream_name}: {r.text}") + return JSONResponse( + content=( + {"success": False, "message": f"Failed to delete stream: {r.text}"} + ), + status_code=r.status_code, + ) + return JSONResponse( + content={"success": True, "message": "Stream deleted successfully"} + ) + except requests.RequestException as e: + logger.error(f"Error communicating with go2rtc: {e}") + return JSONResponse( + content=( + { + "success": False, + "message": "Error communicating with go2rtc", + } + ), + status_code=500, + ) + + +@router.get("/ffprobe", dependencies=[Depends(require_role(["admin"]))]) +def ffprobe(request: Request, paths: str = "", detailed: bool = False): + path_param = paths + + if not path_param: + return JSONResponse( + content=({"success": False, "message": "Path needs to be provided."}), + status_code=404, + ) + + if path_param.startswith("camera"): + camera = path_param[7:] + + if camera not in request.app.frigate_config.cameras.keys(): + return JSONResponse( + content=( + {"success": False, "message": f"{camera} is not a valid camera."} + ), + status_code=404, + ) + + if not request.app.frigate_config.cameras[camera].enabled: + return JSONResponse( + content=({"success": False, "message": f"{camera} is not enabled."}), + status_code=404, + ) + + paths = map( + lambda input: input.path, + request.app.frigate_config.cameras[camera].ffmpeg.inputs, + ) + elif "," in clean_camera_user_pass(path_param): + paths = path_param.split(",") + else: + paths = [path_param] + + # user has multiple streams + output = [] + + for path in paths: + ffprobe = ffprobe_stream( + request.app.frigate_config.ffmpeg, path.strip(), detailed=detailed + ) + + if ffprobe.returncode != 0: + try: + stderr_decoded = ffprobe.stderr.decode("utf-8") + except UnicodeDecodeError: + try: + stderr_decoded = ffprobe.stderr.decode("unicode_escape") + except Exception: + stderr_decoded = str(ffprobe.stderr) + + stderr_lines = [ + line.strip() for line in stderr_decoded.split("\n") if line.strip() + ] + + result = { + "return_code": ffprobe.returncode, + "stderr": stderr_lines, + "stdout": "", + } + else: + result = { + "return_code": ffprobe.returncode, + "stderr": [], + "stdout": json.loads(ffprobe.stdout.decode("unicode_escape").strip()), + } + + # Add detailed metadata if requested and probe was successful + if detailed and ffprobe.returncode == 0 and result["stdout"]: + try: + probe_data = result["stdout"] + metadata = {} + + # Extract video stream information + video_stream = None + audio_stream = None + + for stream in probe_data.get("streams", []): + if stream.get("codec_type") == "video": + video_stream = stream + elif stream.get("codec_type") == "audio": + audio_stream = stream + + # Video metadata + if video_stream: + metadata["video"] = { + "codec": video_stream.get("codec_name"), + "width": video_stream.get("width"), + "height": video_stream.get("height"), + "fps": _extract_fps(video_stream.get("avg_frame_rate")), + "pixel_format": video_stream.get("pix_fmt"), + "profile": video_stream.get("profile"), + "level": video_stream.get("level"), + } + + # Calculate resolution string + if video_stream.get("width") and video_stream.get("height"): + metadata["video"]["resolution"] = ( + f"{video_stream['width']}x{video_stream['height']}" + ) + + # Audio metadata + if audio_stream: + metadata["audio"] = { + "codec": audio_stream.get("codec_name"), + "channels": audio_stream.get("channels"), + "sample_rate": audio_stream.get("sample_rate"), + "channel_layout": audio_stream.get("channel_layout"), + } + + # Container/format metadata + if probe_data.get("format"): + format_info = probe_data["format"] + metadata["container"] = { + "format": format_info.get("format_name"), + "duration": format_info.get("duration"), + "size": format_info.get("size"), + } + + result["metadata"] = metadata + + except Exception as e: + logger.warning(f"Failed to extract detailed metadata: {e}") + # Continue without metadata if parsing fails + + output.append(result) + + return JSONResponse(content=output) + + +@router.get("/keyframe_analysis", dependencies=[Depends(require_role(["admin"]))]) +async def keyframe_analysis(request: Request, camera: str = ""): + """Probe a camera's record stream and classify its keyframe spacing. + + Detects smart/+ codecs and long/variable GOPs that degrade recording. + """ + config: FrigateConfig = request.app.frigate_config + + if camera not in config.cameras: + return JSONResponse( + content={"success": False, "message": f"{camera} is not a valid camera."}, + status_code=404, + ) + + camera_config = config.cameras[camera] + + if not camera_config.enabled: + return JSONResponse( + content={"success": False, "message": f"{camera} is not enabled."}, + status_code=404, + ) + + # keyframe spacing only matters when this camera is recording + if not camera_config.record.enabled: + return JSONResponse(content={"severity": "record_disabled"}) + + # recording guarantees an input carries the record role; its index matches + # the "Stream N" numbering the ffprobe endpoint surfaces (same input order) + record_index, record_input = next( + (idx, i) + for idx, i in enumerate(camera_config.ffmpeg.inputs) + if "record" in i.roles + ) + + segment_time = get_record_segment_time(camera_config) + result = await analyze_record_keyframes( + config.ffmpeg, record_input.path, segment_time + ) + result["stream_index"] = record_index + return JSONResponse(content=result) + + +@router.get("/ffprobe/snapshot", dependencies=[Depends(require_role(["admin"]))]) +def ffprobe_snapshot(request: Request, url: str = "", timeout: int = 10): + """Get a snapshot from a stream URL using ffmpeg.""" + if not url: + return JSONResponse( + content={"success": False, "message": "URL parameter is required"}, + status_code=400, + ) + + config: FrigateConfig = request.app.frigate_config + + image_data, error = run_ffmpeg_snapshot( + config.ffmpeg, url, "mjpeg", timeout=timeout + ) + + if image_data: + return Response( + image_data, + media_type="image/jpeg", + headers={"Cache-Control": "no-store"}, + ) + elif error == "timeout": + return JSONResponse( + content={"success": False, "message": "Timeout capturing snapshot"}, + status_code=408, + ) + else: + logger.error(f"ffmpeg failed: {error}") + return JSONResponse( + content={"success": False, "message": "Failed to capture snapshot"}, + status_code=500, + ) + + +@router.get("/reolink/detect", dependencies=[Depends(require_role(["admin"]))]) +def reolink_detect(host: str = "", username: str = "", password: str = ""): + """ + Detect Reolink camera capabilities and recommend optimal protocol. + + Queries the Reolink camera API to determine the camera's resolution + and recommends either http-flv (for 5MP and below) or rtsp (for higher resolutions). + """ + if not host: + return JSONResponse( + content={"success": False, "message": "Host parameter is required"}, + status_code=400, + ) + + if not username: + return JSONResponse( + content={"success": False, "message": "Username parameter is required"}, + status_code=400, + ) + + if not password: + return JSONResponse( + content={"success": False, "message": "Password parameter is required"}, + status_code=400, + ) + + # Validate host format to prevent injection attacks + if not _is_valid_host(host): + return JSONResponse( + content={"success": False, "message": "Invalid host format"}, + status_code=400, + ) + + try: + # URL-encode credentials to prevent injection + encoded_user = quote_plus(username) + encoded_password = quote_plus(password) + api_url = f"http://{host}/api.cgi?cmd=GetEnc&user={encoded_user}&password={encoded_password}" + + response = requests.get(api_url, timeout=5) + + if not response.ok: + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": f"Failed to connect to camera API: HTTP {response.status_code}", + }, + status_code=200, + ) + + data = response.json() + enc_data = data[0] if isinstance(data, list) and len(data) > 0 else data + + stream_info = None + if isinstance(enc_data, dict): + if enc_data.get("value", {}).get("Enc"): + stream_info = enc_data["value"]["Enc"] + elif enc_data.get("Enc"): + stream_info = enc_data["Enc"] + + if not stream_info or not stream_info.get("mainStream"): + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": "Could not find stream information in API response", + } + ) + + main_stream = stream_info["mainStream"] + width = main_stream.get("width", 0) + height = main_stream.get("height", 0) + + if not width or not height: + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": "Could not determine camera resolution", + } + ) + + megapixels = (width * height) / 1_000_000 + protocol = "http-flv" if megapixels <= 5.0 else "rtsp" + + return JSONResponse( + content={ + "success": True, + "protocol": protocol, + "resolution": f"{width}x{height}", + "megapixels": round(megapixels, 2), + } + ) + + except requests.exceptions.Timeout: + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": "Connection timeout - camera did not respond", + } + ) + except requests.exceptions.RequestException: + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": "Failed to connect to camera", + } + ) + except Exception: + logger.exception(f"Error detecting Reolink camera at {host}") + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": "Unable to detect camera capabilities", + } + ) + + +def _extract_fps(r_frame_rate: str) -> float | None: + """Extract FPS from ffprobe avg_frame_rate / r_frame_rate string (e.g., '30/1' -> 30.0)""" + if not r_frame_rate: + return None + try: + num, den = r_frame_rate.split("/") + return round(float(num) / float(den), 2) + except (ValueError, ZeroDivisionError): + return None + + +def _build_digest_transport(username: str, password: str) -> AsyncTransport: + """Build a zeep transport backed by an httpx client using HTTP digest auth.""" + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + return AsyncTransport(client=client) + + +async def _connect_onvif_camera( + host: str, + port: int, + username: str, + password: str, + wsdl_base: str | None, + auth_type: str, +) -> ONVIFCamera: + """Connect to an ONVIF device, trying both WS-Security password encodings. + + Cameras disagree on whether the WS-Security UsernameToken should carry a + hashed PasswordDigest or a plaintext PasswordText. The wizard can't know + which a given camera expects, so we try PasswordDigest first (the common + case) and fall back to PasswordText when the device rejects the token. This + is independent of auth_type, which controls HTTP transport-level auth. + """ + first_error: Fault | None = None + + # encrypt=True -> PasswordDigest, encrypt=False -> PasswordText + for encrypt in (True, False): + onvif_camera = ONVIFCamera( + host, + port, + username or "", + password or "", + wsdl_dir=wsdl_base, + encrypt=encrypt, + ) + + try: + await onvif_camera.update_xaddrs() + except Fault as e: + # A SOAP fault here is how a camera signals the wrong password + # encoding, so retry with the other encoding before giving up. + logger.debug( + "ONVIF connect with %s rejected, trying alternate encoding", + "PasswordDigest" if encrypt else "PasswordText", + ) + if first_error is None: + first_error = e + continue + + if auth_type == "digest" and username and password: + transport = _build_digest_transport(username, password) + for service in ("devicemgmt", "media", "ptz"): + if hasattr(onvif_camera, service): + getattr(onvif_camera, service).zeep_client.transport = transport + logger.debug("Configured digest authentication") + + return onvif_camera + + # Both encodings failed authentication; surface the original fault. + raise first_error + + +@router.get( + "/onvif/probe", + dependencies=[Depends(require_role(["admin"]))], + summary="Probe ONVIF device", + description=( + "Probe an ONVIF device to determine capabilities and optionally test available stream URIs. " + "Query params: host (required), port (default 80), username, password, test (boolean), " + "auth_type (basic or digest, default basic)." + ), +) +async def onvif_probe( + request: Request, + host: str = Query(None), + port: int = Query(80), + username: str = Query(""), + password: str = Query(""), + test: bool = Query(False), + auth_type: str = Query("basic"), # Add auth_type parameter +): + """ + Probe a single ONVIF device to determine capabilities. + + Connects to an ONVIF device and queries for: + - Device information (manufacturer, model) + - Media profiles count + - PTZ support + - Available presets + - Autotracking support + + Query Parameters: + host: Device host/IP address (required) + port: Device port (default 80) + username: ONVIF username (optional) + password: ONVIF password (optional) + test: run ffprobe on the stream (optional) + auth_type: Authentication type - "basic" or "digest" (default "basic") + + Returns: + JSON with device capabilities information + """ + if not host: + return JSONResponse( + content={"success": False, "message": "host parameter is required"}, + status_code=400, + ) + + # Validate host format + if not _is_valid_host(host): + return JSONResponse( + content={"success": False, "message": "Invalid host format"}, + status_code=400, + ) + + # Validate auth_type + if auth_type not in ["basic", "digest"]: + return JSONResponse( + content={ + "success": False, + "message": "auth_type must be 'basic' or 'digest'", + }, + status_code=400, + ) + + onvif_camera = None + + try: + logger.debug(f"Probing ONVIF device at {host}:{port} with {auth_type} auth") + + try: + wsdl_base = None + spec = find_spec("onvif") + if spec and getattr(spec, "origin", None): + wsdl_base = str(Path(spec.origin).parent / "wsdl") + except Exception: + wsdl_base = None + + onvif_camera = await _connect_onvif_camera( + host, port, username, password, wsdl_base, auth_type + ) + + # Get device information + device_info = { + "manufacturer": "Unknown", + "model": "Unknown", + "firmware_version": "Unknown", + } + try: + device_service = await onvif_camera.create_devicemgmt_service() + + # Update transport for device service if digest auth + if auth_type == "digest" and username and password: + device_service.zeep_client.transport = _build_digest_transport( + username, password + ) + + device_info_resp = await device_service.GetDeviceInformation() + manufacturer = getattr(device_info_resp, "Manufacturer", None) or ( + device_info_resp.get("Manufacturer") + if isinstance(device_info_resp, dict) + else None + ) + model = getattr(device_info_resp, "Model", None) or ( + device_info_resp.get("Model") + if isinstance(device_info_resp, dict) + else None + ) + firmware = getattr(device_info_resp, "FirmwareVersion", None) or ( + device_info_resp.get("FirmwareVersion") + if isinstance(device_info_resp, dict) + else None + ) + device_info.update( + { + "manufacturer": manufacturer or "Unknown", + "model": model or "Unknown", + "firmware_version": firmware or "Unknown", + } + ) + except Exception as e: + logger.debug(f"Failed to get device info: {e}") + + # Get media profiles + profiles = [] + profiles_count = 0 + first_profile_token = None + ptz_config_token = None + try: + media_service = await onvif_camera.create_media_service() + + # Update transport for media service if digest auth + if auth_type == "digest" and username and password: + media_service.zeep_client.transport = _build_digest_transport( + username, password + ) + + profiles = await media_service.GetProfiles() + profiles_count = len(profiles) if profiles else 0 + if profiles and len(profiles) > 0: + p = profiles[0] + first_profile_token = getattr(p, "token", None) or ( + p.get("token") if isinstance(p, dict) else None + ) + # Get PTZ configuration token from the profile + ptz_configuration = getattr(p, "PTZConfiguration", None) or ( + p.get("PTZConfiguration") if isinstance(p, dict) else None + ) + if ptz_configuration: + ptz_config_token = getattr(ptz_configuration, "token", None) or ( + ptz_configuration.get("token") + if isinstance(ptz_configuration, dict) + else None + ) + except Exception as e: + logger.debug(f"Failed to get media profiles: {e}") + + # Check PTZ support and capabilities + ptz_supported = False + presets_count = 0 + autotrack_supported = False + + try: + ptz_service = await onvif_camera.create_ptz_service() + + # Update transport for PTZ service if digest auth + if auth_type == "digest" and username and password: + ptz_service.zeep_client.transport = _build_digest_transport( + username, password + ) + + # Check if PTZ service is available + try: + await ptz_service.GetServiceCapabilities() + ptz_supported = True + logger.debug("PTZ service is available") + except Exception as e: + logger.debug(f"PTZ service not available: {e}") + ptz_supported = False + + # Try to get presets if PTZ is supported and we have a profile + if ptz_supported and first_profile_token: + try: + presets_resp = await ptz_service.GetPresets( + {"ProfileToken": first_profile_token} + ) + presets_count = len(presets_resp) if presets_resp else 0 + logger.debug(f"Found {presets_count} presets") + except Exception as e: + logger.debug(f"Failed to get presets: {e}") + presets_count = 0 + + # Check for autotracking support - requires both FOV relative movement and MoveStatus + if ptz_supported and first_profile_token and ptz_config_token: + # First check for FOV relative movement support + pt_r_fov_supported = False + try: + config_request = ptz_service.create_type("GetConfigurationOptions") + config_request.ConfigurationToken = ptz_config_token + ptz_config = await ptz_service.GetConfigurationOptions( + config_request + ) + + if ptz_config: + # Check for pt-r-fov support + spaces = getattr(ptz_config, "Spaces", None) or ( + ptz_config.get("Spaces") + if isinstance(ptz_config, dict) + else None + ) + + if spaces: + rel_pan_tilt_space = getattr( + spaces, "RelativePanTiltTranslationSpace", None + ) or ( + spaces.get("RelativePanTiltTranslationSpace") + if isinstance(spaces, dict) + else None + ) + + if rel_pan_tilt_space: + # Look for FOV space + for i, space in enumerate(rel_pan_tilt_space): + uri = None + if isinstance(space, dict): + uri = space.get("URI") + else: + uri = getattr(space, "URI", None) + + if uri and "TranslationSpaceFov" in uri: + pt_r_fov_supported = True + logger.debug( + "FOV relative movement (pt-r-fov) supported" + ) + break + + logger.debug(f"PTZ config spaces: {ptz_config}") + except Exception as e: + logger.debug(f"Failed to check FOV relative movement: {e}") + pt_r_fov_supported = False + + # Now check for MoveStatus support via GetServiceCapabilities + if pt_r_fov_supported: + try: + service_capabilities_request = ptz_service.create_type( + "GetServiceCapabilities" + ) + service_capabilities = await ptz_service.GetServiceCapabilities( + service_capabilities_request + ) + + # Look for MoveStatus in the capabilities + move_status_capable = False + if service_capabilities: + # Try to find MoveStatus key recursively + def find_move_status(obj, key="MoveStatus"): + if isinstance(obj, dict): + if key in obj: + return obj[key] + for v in obj.values(): + result = find_move_status(v, key) + if result is not None: + return result + elif hasattr(obj, key): + return getattr(obj, key) + elif hasattr(obj, "__dict__"): + for v in vars(obj).values(): + result = find_move_status(v, key) + if result is not None: + return result + return None + + move_status_value = find_move_status(service_capabilities) + + # MoveStatus should return "true" if supported + if isinstance(move_status_value, bool): + move_status_capable = move_status_value + elif isinstance(move_status_value, str): + move_status_capable = ( + move_status_value.lower() == "true" + ) + + logger.debug(f"MoveStatus capability: {move_status_value}") + + # Autotracking is supported if both conditions are met + autotrack_supported = pt_r_fov_supported and move_status_capable + + if autotrack_supported: + logger.debug( + "Autotracking fully supported (pt-r-fov + MoveStatus)" + ) + else: + logger.debug( + f"Autotracking not fully supported - pt-r-fov: {pt_r_fov_supported}, MoveStatus: {move_status_capable}" + ) + except Exception as e: + logger.debug(f"Failed to check MoveStatus support: {e}") + autotrack_supported = False + + except Exception as e: + logger.debug(f"Failed to probe PTZ service: {e}") + + result = { + "success": True, + "host": host, + "port": port, + "manufacturer": device_info["manufacturer"], + "model": device_info["model"], + "firmware_version": device_info["firmware_version"], + "profiles_count": profiles_count, + "ptz_supported": ptz_supported, + "presets_count": presets_count, + "autotrack_supported": autotrack_supported, + } + + # Gather RTSP candidates + rtsp_candidates: list[dict] = [] + try: + media_service = await onvif_camera.create_media_service() + + # Update transport for media service if digest auth + if auth_type == "digest" and username and password: + media_service.zeep_client.transport = _build_digest_transport( + username, password + ) + + if profiles_count and media_service: + for p in profiles or []: + token = getattr(p, "token", None) or ( + p.get("token") if isinstance(p, dict) else None + ) + if not token: + continue + try: + stream_setup = { + "Stream": "RTP-Unicast", + "Transport": {"Protocol": "RTSP"}, + } + stream_req = { + "ProfileToken": token, + "StreamSetup": stream_setup, + } + stream_uri_resp = await media_service.GetStreamUri(stream_req) + uri = ( + stream_uri_resp.get("Uri") + if isinstance(stream_uri_resp, dict) + else getattr(stream_uri_resp, "Uri", None) + ) + if uri: + logger.debug( + f"GetStreamUri returned for token {token}: {uri}" + ) + # If credentials were provided, do NOT add the unauthenticated URI. + try: + if isinstance(uri, str) and uri.startswith("rtsp://"): + if username and password and "@" not in uri: + # Inject raw credentials and add only the + # authenticated version. The credentials will be encoded + # later by ffprobe_stream or the config system. + cred = f"{username}:{password}@" + injected = uri.replace( + "rtsp://", f"rtsp://{cred}", 1 + ) + rtsp_candidates.append( + { + "source": "GetStreamUri", + "profile_token": token, + "uri": injected, + } + ) + else: + # No credentials provided or URI already contains + # credentials — add the URI as returned. + rtsp_candidates.append( + { + "source": "GetStreamUri", + "profile_token": token, + "uri": uri, + } + ) + else: + # Non-RTSP URIs (e.g., http-flv) — add as returned. + rtsp_candidates.append( + { + "source": "GetStreamUri", + "profile_token": token, + "uri": uri, + } + ) + except Exception as e: + logger.debug( + f"Skipping stream URI for token {token} due to processing error: {e}" + ) + continue + except Exception: + logger.debug( + f"GetStreamUri failed for token {token}", exc_info=True + ) + continue + + # Add common RTSP patterns as fallback + if not rtsp_candidates: + common_paths = [ + "/h264", + "/live.sdp", + "/media.amp", + "/Streaming/Channels/101", + "/Streaming/Channels/1", + "/stream1", + "/cam/realmonitor?channel=1&subtype=0", + "/11", + ] + # Use raw credentials for pattern fallback URIs when provided + auth_str = f"{username}:{password}@" if username and password else "" + rtsp_port = 554 + for path in common_paths: + uri = f"rtsp://{auth_str}{host}:{rtsp_port}{path}" + rtsp_candidates.append({"source": "pattern", "uri": uri}) + except Exception: + logger.debug("Failed to collect RTSP candidates") + + # Optionally test RTSP candidates using ffprobe_stream + tested_candidates = [] + if test and rtsp_candidates: + for c in rtsp_candidates: + uri = c["uri"] + to_test = [uri] + try: + if ( + username + and password + and isinstance(uri, str) + and uri.startswith("rtsp://") + and "@" not in uri + ): + cred = f"{username}:{password}@" + cred_uri = uri.replace("rtsp://", f"rtsp://{cred}", 1) + if cred_uri not in to_test: + to_test.append(cred_uri) + except Exception: + pass + + for test_uri in to_test: + try: + probe = ffprobe_stream( + request.app.frigate_config.ffmpeg, test_uri, detailed=False + ) + ok = probe is not None and getattr(probe, "returncode", 1) == 0 + tested_candidates.append( + { + "uri": test_uri, + "source": c.get("source"), + "ok": ok, + "profile_token": c.get("profile_token"), + } + ) + except Exception as e: + logger.debug(f"Unable to probe stream: {e}") + tested_candidates.append( + { + "uri": test_uri, + "source": c.get("source"), + "ok": False, + "profile_token": c.get("profile_token"), + } + ) + + result["rtsp_candidates"] = rtsp_candidates + if test: + result["rtsp_tested"] = tested_candidates + + logger.debug(f"ONVIF probe successful: {result}") + return JSONResponse(content=result) + + except ONVIFError as e: + logger.warning(f"ONVIF error probing {host}:{port}: {e}") + return JSONResponse( + content={"success": False, "message": "ONVIF error"}, + status_code=400, + ) + except (Fault, TransportError) as e: + logger.warning(f"Connection error probing {host}:{port}: {e}") + return JSONResponse( + content={"success": False, "message": "Connection error"}, + status_code=503, + ) + except Exception as e: + logger.warning(f"Error probing ONVIF device at {host}:{port}, {e}") + return JSONResponse( + content={"success": False, "message": "Probe failed"}, + status_code=500, + ) + + finally: + # Best-effort cleanup of ONVIF camera client session + if onvif_camera is not None: + try: + # Check if the camera has a close method and call it + if hasattr(onvif_camera, "close"): + await onvif_camera.close() + except Exception as 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) as f: + old_raw_config = f.read() + + try: + yaml = YAML() + yaml.indent(mapping=2, sequence=4, offset=2) + + with open(config_file) 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) 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: + await asyncio.to_thread( + 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, + ) + + +_SUB_COMMAND_FEATURES = {"motion_mask", "object_mask", "zone"} + + +@router.put( + "/camera/{camera_name}/set/{feature}", + dependencies=[Depends(require_role(["admin"]))], +) +@router.put( + "/camera/{camera_name}/set/{feature}/{sub_command}", + dependencies=[Depends(require_role(["admin"]))], +) +def camera_set( + request: Request, + camera_name: str, + feature: str, + body: CameraSetBody, + sub_command: str | None = None, +): + """Set a camera feature state. Use camera_name='*' to target all cameras.""" + dispatcher = request.app.dispatcher + frigate_config: FrigateConfig = request.app.frigate_config + + if feature == "profile": + if camera_name != "*": + return JSONResponse( + content={ + "success": False, + "message": "Profile feature requires camera_name='*'", + }, + status_code=400, + ) + dispatcher._receive("profile/set", body.value) + return JSONResponse(content={"success": True}) + + if feature not in dispatcher._camera_settings_handlers: + return JSONResponse( + content={"success": False, "message": f"Unknown feature: {feature}"}, + status_code=400, + ) + + if sub_command and feature not in _SUB_COMMAND_FEATURES: + return JSONResponse( + content={ + "success": False, + "message": f"Feature '{feature}' does not support sub-commands", + }, + status_code=400, + ) + + if not sub_command and feature in _SUB_COMMAND_FEATURES: + return JSONResponse( + content={ + "success": False, + "message": f"Feature '{feature}' requires a sub-command (e.g. mask or zone name)", + }, + status_code=400, + ) + + if camera_name == "*": + cameras = list(frigate_config.cameras.keys()) + elif camera_name not in frigate_config.cameras: + return JSONResponse( + content={ + "success": False, + "message": f"Camera '{camera_name}' not found", + }, + status_code=404, + ) + else: + cameras = [camera_name] + + for cam in cameras: + topic = ( + f"{cam}/{feature}/{sub_command}/set" + if sub_command + else f"{cam}/{feature}/set" + ) + dispatcher._receive(topic, body.value) + + return JSONResponse(content={"success": True}) diff --git a/frigate/api/chat.py b/frigate/api/chat.py new file mode 100644 index 0000000..ae6ff7a --- /dev/null +++ b/frigate/api/chat.py @@ -0,0 +1,1549 @@ +"""Chat and LLM tool calling APIs.""" + +import base64 +import json +import logging +import operator +import time +from datetime import datetime +from functools import reduce +from typing import Any + +import cv2 +from fastapi import APIRouter, Body, Depends, HTTPException, Request +from fastapi.responses import JSONResponse, StreamingResponse +from pydantic import BaseModel + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, +) +from frigate.api.chat_util import ( + chunk_content, + distance_to_score, + format_events_with_local_time, + fuse_scores, + hydrate_event, + parse_iso_to_timestamp, +) +from frigate.api.defs.query.events_query_parameters import EventsQueryParams +from frigate.api.defs.request.chat_body import ChatCompletionRequest +from frigate.api.defs.response.chat_response import ( + ChatCompletionResponse, + ChatMessageResponse, + ToolCall, +) +from frigate.api.defs.tags import Tags +from frigate.api.event import _build_attribute_filter_clause, events +from frigate.config import FrigateConfig +from frigate.genai.prompts import ( + build_chat_system_prompt, + get_attribute_classifications, + get_tool_definitions, +) +from frigate.genai.utils import build_assistant_message_for_conversation +from frigate.jobs.vlm_watch import ( + get_vlm_watch_job, + start_vlm_watch_job, + stop_vlm_watch_job, +) +from frigate.models import Event + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.chat]) + + +class ToolExecuteRequest(BaseModel): + """Request model for tool execution.""" + + tool_name: str + arguments: dict[str, Any] + + +class VLMMonitorRequest(BaseModel): + """Request model for starting a VLM watch job.""" + + camera: str + condition: str + max_duration_minutes: int = 60 + labels: list[str] = [] + zones: list[str] = [] + + +@router.get( + "/chat/tools", + dependencies=[Depends(allow_any_authenticated())], + summary="Get available tools", + description="Returns OpenAI-compatible tool definitions for function calling.", +) +def get_tools(request: Request) -> JSONResponse: + """Get list of available tools for LLM function calling.""" + config = request.app.frigate_config + semantic_search_enabled = bool(getattr(config.semantic_search, "enabled", False)) + attribute_classifications = get_attribute_classifications(config) + tools = get_tool_definitions( + semantic_search_enabled=semantic_search_enabled, + attribute_classifications=attribute_classifications, + ) + return JSONResponse(content={"tools": tools}) + + +def _resolve_zones( + zones: list[str], + config: FrigateConfig, + target_cameras: list[str], +) -> list[str]: + """Map zone names to their canonical config keys, case-insensitively. + + LLMs frequently echo a user's casing ("Front Yard") instead of the + configured key ("front_yard"). The downstream zone filter is a SQLite GLOB + over the JSON-encoded zones column, which is case-sensitive — so an + unnormalized name silently returns zero matches. Build a lookup over the + relevant cameras' configured zones and substitute when we find a match; + unknown names pass through so behavior matches what the model asked for. + """ + if not zones: + return zones + + lookup: dict[str, str] = {} + for camera_id in target_cameras: + camera_config = config.cameras.get(camera_id) + if camera_config is None: + continue + for zone_name in camera_config.zones.keys(): + lookup.setdefault(zone_name.lower(), zone_name) + + return [lookup.get(z.lower(), z) for z in zones] + + +async def _execute_search_objects( + request: Request, + arguments: dict[str, Any], + allowed_cameras: list[str], +) -> JSONResponse: + """ + Execute the search_objects tool. + + Routes to the semantic path when the LLM supplied a `semantic_query` + and semantic search is enabled; otherwise delegates to the standard + events API logic. + """ + config = request.app.frigate_config + semantic_query = arguments.get("semantic_query") + if isinstance(semantic_query, str): + semantic_query = semantic_query.strip() or None + else: + semantic_query = None + + if semantic_query and getattr(config.semantic_search, "enabled", False): + return await _execute_search_objects_semantic( + request, arguments, allowed_cameras, semantic_query + ) + + # Parse after/before as server local time; convert to Unix timestamp + after = arguments.get("after") + before = arguments.get("before") + + def _parse_as_local_timestamp(s: str): + s = s.replace("Z", "").strip()[:19] + dt = datetime.strptime(s, "%Y-%m-%dT%H:%M:%S") + return time.mktime(dt.timetuple()) + + if after: + try: + after = _parse_as_local_timestamp(after) + except (ValueError, AttributeError, TypeError): + logger.warning(f"Invalid 'after' timestamp format: {after}") + after = None + + if before: + try: + before = _parse_as_local_timestamp(before) + except (ValueError, AttributeError, TypeError): + logger.warning(f"Invalid 'before' timestamp format: {before}") + before = None + + # Convert zones array to comma-separated string if provided + zones = arguments.get("zones") + if isinstance(zones, list): + camera_arg = arguments.get("camera") + target_cameras = ( + [camera_arg] if camera_arg and camera_arg != "all" else allowed_cameras + ) + zones = _resolve_zones(zones, config, target_cameras) + zones = ",".join(zones) + elif zones is None: + zones = "all" + + attribute = arguments.get("attribute") + + # Build query parameters compatible with EventsQueryParams + query_params = EventsQueryParams( + cameras=arguments.get("camera", "all"), + labels=arguments.get("label", "all"), + sub_labels=arguments.get("sub_label", "all"), # case-insensitive on the backend + attributes=attribute if attribute else "all", + zones=zones, + zone=zones, + after=after, + before=before, + limit=arguments.get("limit", 25), + ) + + try: + # Call the events endpoint function directly + # The events function is synchronous and takes params and allowed_cameras + response = events(query_params, allowed_cameras) + + # The response is already a JSONResponse with event data + # Return it as-is for the LLM + return response + except Exception as e: + logger.exception(f"Error executing search_objects: {e}") + return JSONResponse( + content={ + "success": False, + "message": "Error searching objects", + }, + status_code=500, + ) + + +async def _execute_search_objects_semantic( + request: Request, + arguments: dict[str, Any], + allowed_cameras: list[str], + semantic_query: str, +) -> JSONResponse: + """Search objects via fused thumbnail + description embeddings. + + Runs both visual and description vec searches against `semantic_query`, + intersects the candidates with the structured filters (camera, label, + sub_label, zones, time window) the LLM supplied, and ranks the survivors + by fused similarity. Mirrors the candidate-then-filter pattern used by + find_similar_objects since sqlite-vec's IN filter is unreliable. + """ + from peewee import fn + + config = request.app.frigate_config + context = request.app.embeddings + if context is None: + logger.warning( + "semantic_query supplied but embeddings context is unavailable; " + "returning empty results." + ) + return JSONResponse(content=[]) + + after = parse_iso_to_timestamp(arguments.get("after")) + before = parse_iso_to_timestamp(arguments.get("before")) + + camera_arg = arguments.get("camera") + if camera_arg and camera_arg != "all": + if camera_arg not in allowed_cameras: + return JSONResponse(content=[]) + cameras = [camera_arg] + else: + cameras = list(allowed_cameras) if allowed_cameras else [] + + if not cameras: + return JSONResponse(content=[]) + + label = arguments.get("label") + sub_label = arguments.get("sub_label") + attribute = arguments.get("attribute") + + zones = arguments.get("zones") + if isinstance(zones, list) and zones: + zones = _resolve_zones(zones, config, cameras) + else: + zones = None + + limit = int(arguments.get("limit", 25)) + limit = max(1, min(limit, 100)) + + visual_distances: dict[str, float] = {} + description_distances: dict[str, float] = {} + try: + rows = context.search_thumbnail(semantic_query) + visual_distances = {row[0]: row[1] for row in rows} + except Exception: + logger.exception( + "search_thumbnail failed for semantic_query: %s", semantic_query + ) + + try: + rows = context.search_description(semantic_query) + description_distances = {row[0]: row[1] for row in rows} + except Exception: + logger.exception( + "search_description failed for semantic_query: %s", semantic_query + ) + + vec_ids = set(visual_distances) | set(description_distances) + if not vec_ids: + return JSONResponse(content=[]) + + clauses = [Event.id.in_(list(vec_ids)), Event.camera.in_(cameras)] + if after is not None: + clauses.append(Event.start_time >= after) + if before is not None: + clauses.append(Event.start_time <= before) + if label: + clauses.append(Event.label == label) + if sub_label: + # case-insensitive match to mirror events() behavior + clauses.append(fn.LOWER(Event.sub_label.cast("text")) == sub_label.lower()) + if attribute: + attribute_clause = _build_attribute_filter_clause(attribute) + if attribute_clause is not None: + clauses.append(attribute_clause) + if zones: + zone_clauses = [Event.zones.cast("text") % f'*"{zone}"*' for zone in zones] + clauses.append(reduce(operator.or_, zone_clauses)) + + eligible = {e.id: e for e in Event.select().where(reduce(operator.and_, clauses))} + + scored: list[tuple[str, float]] = [] + for eid in eligible: + v_score = ( + distance_to_score(visual_distances[eid], context.thumb_stats) + if eid in visual_distances + else None + ) + d_score = ( + distance_to_score(description_distances[eid], context.desc_stats) + if eid in description_distances + else None + ) + fused = fuse_scores(v_score, d_score) + if fused is None: + continue + scored.append((eid, fused)) + + scored.sort(key=lambda pair: pair[1], reverse=True) + scored = scored[:limit] + + results = [hydrate_event(eligible[eid], score=score) for eid, score in scored] + return JSONResponse(content=results) + + +async def _execute_find_similar_objects( + request: Request, + arguments: dict[str, Any], + allowed_cameras: list[str], +) -> dict[str, Any]: + """Execute the find_similar_objects tool. + + Returns a plain dict (not JSONResponse) so the chat loop can embed it + directly in tool-result messages. + """ + # 1. Semantic search enabled? + config = request.app.frigate_config + if not getattr(config.semantic_search, "enabled", False): + return { + "error": "semantic_search_disabled", + "message": ( + "Semantic search must be enabled to find similar objects. " + "Enable it in the Frigate config under semantic_search." + ), + } + + context = request.app.embeddings + if context is None: + return { + "error": "semantic_search_disabled", + "message": "Embeddings context is not available.", + } + + # 2. Anchor lookup. + event_id = arguments.get("event_id") + if not event_id: + return {"error": "missing_event_id", "message": "event_id is required."} + + try: + anchor = Event.get(Event.id == event_id) + except Event.DoesNotExist: + return { + "error": "anchor_not_found", + "message": f"Could not find event {event_id}.", + } + + # 3. Parse params. + after = parse_iso_to_timestamp(arguments.get("after")) + before = parse_iso_to_timestamp(arguments.get("before")) + + cameras = arguments.get("cameras") + if cameras: + # Respect RBAC: intersect with the user's allowed cameras. + cameras = [c for c in cameras if c in allowed_cameras] + else: + cameras = list(allowed_cameras) if allowed_cameras else None + + labels = arguments.get("labels") or [anchor.label] + sub_labels = arguments.get("sub_labels") + zones = arguments.get("zones") + + if zones: + zones = _resolve_zones( + zones, request.app.frigate_config, cameras or list(allowed_cameras) + ) + + similarity_mode = arguments.get("similarity_mode", "fused") + if similarity_mode not in ("visual", "semantic", "fused"): + similarity_mode = "fused" + + min_score = arguments.get("min_score") + limit = int(arguments.get("limit", 10)) + limit = max(1, min(limit, 50)) + + # 4. Run similarity searches. We deliberately do NOT pass event_ids into + # the vec queries — the IN filter on sqlite-vec is broken in the installed + # version (see frigate/embeddings/__init__.py). Mirror the pattern used by + # frigate/api/event.py events_search: fetch top-k globally, then intersect + # with the structured filters via Peewee. + visual_distances: dict[str, float] = {} + description_distances: dict[str, float] = {} + + try: + if similarity_mode in ("visual", "fused"): + rows = context.search_thumbnail(anchor) + visual_distances = {row[0]: row[1] for row in rows} + + if similarity_mode in ("semantic", "fused"): + query_text = ( + (anchor.data or {}).get("description") + or anchor.sub_label + or anchor.label + ) + rows = context.search_description(query_text) + description_distances = {row[0]: row[1] for row in rows} + except Exception: + logger.exception("Similarity search failed") + return { + "error": "similarity_search_failed", + "message": "Failed to run similarity search.", + } + + vec_ids = set(visual_distances) | set(description_distances) + vec_ids.discard(anchor.id) + # vec layer returns up to k=100 per modality; flag when we hit that ceiling + # so the LLM can mention there may be more matches beyond what we saw. + candidate_truncated = ( + len(visual_distances) >= 100 or len(description_distances) >= 100 + ) + + if not vec_ids: + return { + "anchor": hydrate_event(anchor), + "results": [], + "similarity_mode": similarity_mode, + "candidate_truncated": candidate_truncated, + } + + # 5. Apply structured filters, intersected with vec hits. + clauses = [Event.id.in_(list(vec_ids))] + if after is not None: + clauses.append(Event.start_time >= after) + if before is not None: + clauses.append(Event.start_time <= before) + if cameras: + clauses.append(Event.camera.in_(cameras)) + if labels: + clauses.append(Event.label.in_(labels)) + if sub_labels: + clauses.append(Event.sub_label.in_(sub_labels)) + if zones: + # Mirror the pattern used by frigate/api/event.py for JSON-array zone match. + zone_clauses = [Event.zones.cast("text") % f'*"{zone}"*' for zone in zones] + clauses.append(reduce(operator.or_, zone_clauses)) + + eligible = {e.id: e for e in Event.select().where(reduce(operator.and_, clauses))} + + # 6. Fuse and rank. + scored: list[tuple[str, float]] = [] + for eid in eligible: + v_score = ( + distance_to_score(visual_distances[eid], context.thumb_stats) + if eid in visual_distances + else None + ) + d_score = ( + distance_to_score(description_distances[eid], context.desc_stats) + if eid in description_distances + else None + ) + fused = fuse_scores(v_score, d_score) + if fused is None: + continue + if min_score is not None and fused < min_score: + continue + scored.append((eid, fused)) + + scored.sort(key=lambda pair: pair[1], reverse=True) + scored = scored[:limit] + + results = [hydrate_event(eligible[eid], score=score) for eid, score in scored] + + return { + "anchor": hydrate_event(anchor), + "results": results, + "similarity_mode": similarity_mode, + "candidate_truncated": candidate_truncated, + } + + +@router.post( + "/chat/execute", + dependencies=[Depends(allow_any_authenticated())], + summary="Execute a tool", + description="Execute a tool function call from an LLM.", +) +async def execute_tool( + request: Request, + body: ToolExecuteRequest = Body(...), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +) -> JSONResponse: + """ + Execute a tool function call. + + This endpoint receives tool calls from LLMs and executes the corresponding + Frigate operations, returning results in a format the LLM can understand. + """ + tool_name = body.tool_name + arguments = body.arguments + + logger.debug(f"Executing tool: {tool_name} with arguments: {arguments}") + + if tool_name == "search_objects": + return await _execute_search_objects(request, arguments, allowed_cameras) + + if tool_name == "find_similar_objects": + result = await _execute_find_similar_objects( + request, arguments, allowed_cameras + ) + status_code = 200 if "error" not in result else 400 + return JSONResponse(content=result, status_code=status_code) + + if tool_name == "set_camera_state": + result = await _execute_set_camera_state(request, arguments) + return JSONResponse( + content=result, status_code=200 if result.get("success") else 400 + ) + + return JSONResponse( + content={ + "success": False, + "message": f"Unknown tool: {tool_name}", + "tool": tool_name, + }, + status_code=400, + ) + + +async def _execute_get_live_context( + request: Request, + camera: str, + allowed_cameras: list[str], +) -> dict[str, Any]: + # Reject wildcards explicitly so models retry with a real camera name + # instead of silently fanning out across every camera. + if camera in ("*", "all"): + return { + "error": ( + "get_live_context requires a single camera name; wildcards " + "are not supported. Call this tool once per camera." + ), + "available_cameras": allowed_cameras, + } + + if camera not in allowed_cameras: + return { + "error": f"Camera '{camera}' not found or access denied", + "available_cameras": allowed_cameras, + } + + if camera not in request.app.frigate_config.cameras: + return { + "error": f"Camera '{camera}' not found", + } + + try: + frame_processor = request.app.detected_frames_processor + camera_state = frame_processor.camera_states.get(camera) + + if camera_state is None: + return { + "error": f"Camera '{camera}' state not available", + } + + tracked_objects_dict = {} + with camera_state.current_frame_lock: + tracked_objects = camera_state.tracked_objects.copy() + frame_time = camera_state.current_frame_time + + for obj_id, tracked_obj in tracked_objects.items(): + obj_dict = tracked_obj.to_dict() + if obj_dict.get("frame_time") == frame_time: + tracked_objects_dict[obj_id] = { + "label": obj_dict.get("label"), + "zones": obj_dict.get("current_zones", []), + "sub_label": obj_dict.get("sub_label"), + "stationary": obj_dict.get("stationary", False), + } + + result: dict[str, Any] = { + "camera": camera, + "timestamp": frame_time, + "detections": list(tracked_objects_dict.values()), + } + + # Grab live frame when the chat model supports vision + image_url = await _get_live_frame_image_url(request, camera, allowed_cameras) + if image_url: + chat_client = request.app.genai_manager.chat_client + if chat_client is not None and chat_client.supports_vision: + # Pass image URL so it can be injected as a user message + # (images can't be in tool results) + result["_image_url"] = image_url + + return result + + except Exception as e: + logger.exception(f"Error executing get_live_context: {e}") + return { + "error": "Error getting live context", + } + + +async def _get_live_frame_image_url( + request: Request, + camera: str, + allowed_cameras: list[str], +) -> str | None: + """ + Fetch the current live frame for a camera as a base64 data URL. + + Returns None if the frame cannot be retrieved. Used by get_live_context + to attach the live image to the conversation. + """ + if ( + camera not in allowed_cameras + or camera not in request.app.frigate_config.cameras + ): + return None + try: + frame_processor = request.app.detected_frames_processor + if camera not in frame_processor.camera_states: + return None + frame = frame_processor.get_current_frame(camera, {}) + if frame is None: + return None + height, width = frame.shape[:2] + target_height = 480 + if height > target_height: + scale = target_height / height + frame = cv2.resize( + frame, + (int(width * scale), target_height), + interpolation=cv2.INTER_AREA, + ) + _, img_encoded = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85]) + b64 = base64.b64encode(img_encoded.tobytes()).decode("utf-8") + return f"data:image/jpeg;base64,{b64}" + except Exception as e: + logger.debug("Failed to get live frame for %s: %s", camera, e) + return None + + +async def _execute_set_camera_state( + request: Request, + arguments: dict[str, Any], +) -> dict[str, Any]: + role = request.headers.get("remote-role", "") + if "admin" not in [r.strip() for r in role.split(",")]: + return {"error": "Admin privileges required to change camera settings."} + + camera = arguments.get("camera", "").strip() + feature = arguments.get("feature", "").strip() + value = arguments.get("value", "").strip() + + if not camera or not feature or not value: + return {"error": "camera, feature, and value are all required."} + + dispatcher = request.app.dispatcher + frigate_config = request.app.frigate_config + + if feature == "profile": + if camera != "*": + return {"error": "Profile feature requires camera='*'."} + dispatcher._receive("profile/set", value) + return {"success": True, "camera": camera, "feature": feature, "value": value} + + if feature not in dispatcher._camera_settings_handlers: + return {"error": f"Unknown feature: {feature}"} + + if camera == "*": + cameras = list(frigate_config.cameras.keys()) + elif camera not in frigate_config.cameras: + return {"error": f"Camera '{camera}' not found."} + else: + cameras = [camera] + + for cam in cameras: + dispatcher._receive(f"{cam}/{feature}/set", value) + + return {"success": True, "camera": camera, "feature": feature, "value": value} + + +async def _execute_tool_internal( + tool_name: str, + arguments: dict[str, Any], + request: Request, + allowed_cameras: list[str], +) -> dict[str, Any]: + """ + Internal helper to execute a tool and return the result as a dict. + + This is used by the chat completion endpoint to execute tools. + """ + if tool_name == "search_objects": + response = await _execute_search_objects(request, arguments, allowed_cameras) + try: + if hasattr(response, "body"): + body_str = response.body.decode("utf-8") + return json.loads(body_str) + elif hasattr(response, "content"): + return response.content + else: + return {} + except (json.JSONDecodeError, AttributeError) as e: + logger.warning(f"Failed to extract tool result: {e}") + return {"error": "Failed to parse tool result"} + elif tool_name == "find_similar_objects": + return await _execute_find_similar_objects(request, arguments, allowed_cameras) + elif tool_name == "set_camera_state": + return await _execute_set_camera_state(request, arguments) + elif tool_name == "get_live_context": + camera = arguments.get("camera") + if not camera: + logger.error( + "Tool get_live_context failed: camera parameter is required. " + "Arguments: %s", + json.dumps(arguments), + ) + return { + "error": ( + "get_live_context requires a single camera name; " + "wildcards and empty values are not supported. " + "Call this tool once per camera." + ), + "available_cameras": allowed_cameras, + } + return await _execute_get_live_context(request, camera, allowed_cameras) + elif tool_name == "start_camera_watch": + return await _execute_start_camera_watch(request, arguments) + elif tool_name == "stop_camera_watch": + return _execute_stop_camera_watch() + elif tool_name == "get_profile_status": + return _execute_get_profile_status(request) + elif tool_name == "get_recap": + return _execute_get_recap(arguments, allowed_cameras) + else: + logger.error( + "Tool call failed: unknown tool %r. Expected one of: search_objects, find_similar_objects, " + "get_live_context, start_camera_watch, stop_camera_watch, get_profile_status, get_recap. " + "Arguments received: %s", + tool_name, + json.dumps(arguments), + ) + return {"error": f"Unknown tool: {tool_name}"} + + +async def _execute_start_camera_watch( + request: Request, + arguments: dict[str, Any], +) -> dict[str, Any]: + camera = arguments.get("camera", "").strip() + condition = arguments.get("condition", "").strip() + max_duration_minutes = int(arguments.get("max_duration_minutes", 60)) + labels = arguments.get("labels") or [] + zones = arguments.get("zones") or [] + + if not camera or not condition: + return {"error": "camera and condition are required."} + + config = request.app.frigate_config + if camera not in config.cameras: + return {"error": f"Camera '{camera}' not found."} + + await require_camera_access(camera, request=request) + + if zones: + zones = _resolve_zones(zones, config, [camera]) + + genai_manager = request.app.genai_manager + chat_client = genai_manager.chat_client + if chat_client is None or not chat_client.supports_vision: + return {"error": "VLM watch requires a chat model with vision support."} + + try: + job_id = start_vlm_watch_job( + camera=camera, + condition=condition, + max_duration_minutes=max_duration_minutes, + config=config, + frame_processor=request.app.detected_frames_processor, + genai_manager=genai_manager, + dispatcher=request.app.dispatcher, + labels=labels, + zones=zones, + ) + except RuntimeError as e: + logger.exception("Failed to start VLM watch job: %s", e) + return {"error": "Failed to start VLM watch job."} + + return { + "success": True, + "job_id": job_id, + "message": ( + f"Now watching '{camera}' for: {condition}. " + f"You'll receive a notification when the condition is met (timeout: {max_duration_minutes} min)." + ), + } + + +def _execute_stop_camera_watch() -> dict[str, Any]: + cancelled = stop_vlm_watch_job() + if cancelled: + return {"success": True, "message": "Watch job cancelled."} + return {"success": False, "message": "No active watch job to cancel."} + + +def _execute_get_profile_status(request: Request) -> dict[str, Any]: + """Return profile status including active profile and activation timestamps.""" + profile_manager = getattr(request.app, "profile_manager", None) + if profile_manager is None: + return {"error": "Profile manager is not available."} + + info = profile_manager.get_profile_info() + + # Convert timestamps to human-readable local times inline + last_activated = {} + for name, ts in info.get("last_activated", {}).items(): + try: + dt = datetime.fromtimestamp(ts) + last_activated[name] = dt.strftime("%Y-%m-%d %I:%M:%S %p") + except (TypeError, ValueError, OSError): + last_activated[name] = str(ts) + + return { + "active_profile": info.get("active_profile"), + "profiles": info.get("profiles", []), + "last_activated": last_activated, + } + + +def _execute_get_recap( + arguments: dict[str, Any], + allowed_cameras: list[str], +) -> dict[str, Any]: + """Fetch review segments with GenAI metadata for a time period.""" + from functools import reduce + + from peewee import operator + + from frigate.models import ReviewSegment + + after_str = arguments.get("after") + before_str = arguments.get("before") + + def _parse_as_local_timestamp(s: str): + s = s.replace("Z", "").strip()[:19] + dt = datetime.strptime(s, "%Y-%m-%dT%H:%M:%S") + return time.mktime(dt.timetuple()) + + try: + after = _parse_as_local_timestamp(after_str) + except (ValueError, AttributeError, TypeError): + return {"error": f"Invalid 'after' timestamp: {after_str}"} + + try: + before = _parse_as_local_timestamp(before_str) + except (ValueError, AttributeError, TypeError): + return {"error": f"Invalid 'before' timestamp: {before_str}"} + + cameras = arguments.get("cameras", "all") + if cameras != "all": + requested = set(cameras.split(",")) + camera_list = list(requested.intersection(allowed_cameras)) + if not camera_list: + return {"events": [], "message": "No accessible cameras matched."} + else: + camera_list = allowed_cameras + + clauses = [ + (ReviewSegment.start_time < before) + & ((ReviewSegment.end_time.is_null(True)) | (ReviewSegment.end_time > after)), + (ReviewSegment.camera << camera_list), + ] + + severity_filter = arguments.get("severity") + if severity_filter: + clauses.append(ReviewSegment.severity == severity_filter) + + try: + rows = ( + ReviewSegment.select( + ReviewSegment.camera, + ReviewSegment.start_time, + ReviewSegment.end_time, + ReviewSegment.severity, + ReviewSegment.data, + ) + .where(reduce(operator.and_, clauses)) + .order_by(ReviewSegment.start_time.asc()) + .limit(100) + .dicts() + .iterator() + ) + + events: list[dict[str, Any]] = [] + + for row in rows: + data = row.get("data") or {} + if isinstance(data, str): + try: + data = json.loads(data) + except json.JSONDecodeError: + data = {} + + camera = row["camera"] + event: dict[str, Any] = { + "camera": camera.replace("_", " ").title(), + "severity": row.get("severity", "detection"), + } + + # Include GenAI metadata when available + metadata = data.get("metadata") + if metadata and isinstance(metadata, dict): + if metadata.get("title"): + event["title"] = metadata["title"] + if metadata.get("scene"): + event["description"] = metadata["scene"] + threat = metadata.get("potential_threat_level") + if threat is not None: + threat_labels = { + 0: "normal", + 1: "needs_review", + 2: "security_concern", + } + event["threat_level"] = threat_labels.get(threat, str(threat)) + + # Only include objects/zones/audio when there's no GenAI description + # to keep the payload concise — the description already covers these + if "description" not in event: + objects = data.get("objects", []) + if objects: + event["objects"] = objects + zones = data.get("zones", []) + if zones: + event["zones"] = zones + audio = data.get("audio", []) + if audio: + event["audio"] = audio + + start_ts = row.get("start_time") + end_ts = row.get("end_time") + if start_ts is not None: + try: + event["time"] = datetime.fromtimestamp(start_ts).strftime( + "%I:%M %p" + ) + except (TypeError, ValueError, OSError): + pass + if end_ts is not None and start_ts is not None: + try: + event["duration_seconds"] = round(end_ts - start_ts) + except (TypeError, ValueError): + pass + + events.append(event) + + if not events: + return { + "events": [], + "message": "No activity was found during this time period.", + } + + return {"events": events} + except Exception as e: + logger.exception("Error executing get_recap: %s", e) + return {"error": "Failed to fetch recap data."} + + +async def _execute_pending_tools( + pending_tool_calls: list[dict[str, Any]], + request: Request, + allowed_cameras: list[str], +) -> tuple[list[ToolCall], list[dict[str, Any]], list[dict[str, Any]]]: + """ + Execute a list of tool calls. + + Returns: + (ToolCall list for API response, + tool result dicts for conversation, + extra messages to inject after tool results — e.g. user messages with images) + """ + tool_calls_out: list[ToolCall] = [] + tool_results: list[dict[str, Any]] = [] + extra_messages: list[dict[str, Any]] = [] + for tool_call in pending_tool_calls: + tool_name = tool_call["name"] + tool_args = tool_call.get("arguments") or {} + tool_call_id = tool_call["id"] + logger.debug( + f"Executing tool: {tool_name} (id: {tool_call_id}) with arguments: {json.dumps(tool_args, indent=2)}" + ) + try: + tool_result = await _execute_tool_internal( + tool_name, tool_args, request, allowed_cameras + ) + if isinstance(tool_result, dict) and tool_result.get("error"): + logger.error( + "Tool call %s (id: %s) returned error: %s. Arguments: %s", + tool_name, + tool_call_id, + tool_result.get("error"), + json.dumps(tool_args), + ) + if tool_name == "search_objects" and isinstance(tool_result, list): + tool_result = format_events_with_local_time(tool_result) + _keys = { + "id", + "camera", + "label", + "zones", + "start_time_local", + "end_time_local", + "sub_label", + "event_count", + } + tool_result = [ + {k: evt[k] for k in _keys if k in evt} + for evt in tool_result + if isinstance(evt, dict) + ] + + # Extract _image_url from get_live_context results — images can + # only be sent in user messages, not tool results + if isinstance(tool_result, dict) and "_image_url" in tool_result: + image_url = tool_result.pop("_image_url") + extra_messages.append( + { + "role": "user", + "content": [ + { + "type": "text", + "text": f"Here is the current live image from camera '{tool_result.get('camera', 'unknown')}'.", + }, + { + "type": "image_url", + "image_url": {"url": image_url}, + }, + ], + } + ) + + result_content = ( + json.dumps(tool_result) + if isinstance(tool_result, (dict, list)) + else (tool_result if isinstance(tool_result, str) else str(tool_result)) + ) + tool_calls_out.append( + ToolCall(name=tool_name, arguments=tool_args, response=result_content) + ) + tool_results.append( + { + "role": "tool", + "tool_call_id": tool_call_id, + "content": result_content, + } + ) + except Exception as e: + logger.exception( + "Error executing tool %s (id: %s): %s. Arguments: %s", + tool_name, + tool_call_id, + e, + json.dumps(tool_args), + ) + error_content = json.dumps({"error": f"Tool execution failed: {str(e)}"}) + tool_calls_out.append( + ToolCall(name=tool_name, arguments=tool_args, response=error_content) + ) + tool_results.append( + { + "role": "tool", + "tool_call_id": tool_call_id, + "content": error_content, + } + ) + return (tool_calls_out, tool_results, extra_messages) + + +@router.post( + "/chat/completion", + dependencies=[Depends(allow_any_authenticated())], + summary="Chat completion with tool calling", + description=( + "Send a chat message to the configured GenAI provider with tool calling support. " + "The LLM can call Frigate tools to answer questions about your cameras and events." + ), +) +async def chat_completion( + request: Request, + body: ChatCompletionRequest = Body(...), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + """ + Chat completion endpoint with tool calling support. + + This endpoint: + 1. Gets the configured GenAI client + 2. Gets tool definitions + 3. Sends messages + tools to LLM + 4. Handles tool_calls if present + 5. Executes tools and sends results back to LLM + 6. Repeats until final answer + 7. Returns response to user + """ + genai_client = request.app.genai_manager.chat_client + if not genai_client: + return JSONResponse( + content={ + "error": "GenAI is not configured. Please configure a GenAI provider in your Frigate config.", + }, + status_code=400, + ) + + config = request.app.frigate_config + semantic_search_enabled = bool(getattr(config.semantic_search, "enabled", False)) + attribute_classifications = get_attribute_classifications(config) + tools = get_tool_definitions( + semantic_search_enabled=semantic_search_enabled, + attribute_classifications=attribute_classifications, + ) + conversation = [] + + # Build the system message only when the client hasn't already pinned one. + # The first turn has no system message; we generate it (with the current + # timestamp) and return the whole chain so the client persists it. Later + # turns send it back verbatim, freezing the timestamp so the prompt prefix + # stays byte-identical and the model server's prompt cache keeps hitting. + if not body.messages or body.messages[0].role != "system": + conversation.append( + { + "role": "system", + "content": build_chat_system_prompt( + config=config, + allowed_cameras=allowed_cameras, + semantic_search_enabled=semantic_search_enabled, + attribute_classifications=attribute_classifications, + ), + } + ) + + for msg in body.messages: + msg_dict = { + "role": msg.role, + "content": msg.content, + } + if msg.tool_call_id: + msg_dict["tool_call_id"] = msg.tool_call_id + if msg.name: + msg_dict["name"] = msg.name + if msg.tool_calls is not None: + msg_dict["tool_calls"] = msg.tool_calls + + conversation.append(msg_dict) + + tool_iterations = 0 + tool_calls: list[ToolCall] = [] + max_iterations = body.max_tool_iterations + + logger.debug( + f"Starting chat completion with {len(conversation)} message(s), " + f"{len(tools)} tool(s) available, max_iterations={max_iterations}" + ) + + # True LLM streaming when client supports it and stream requested + if body.stream and hasattr(genai_client, "chat_with_tools_stream"): + stream_iterations = 0 + + async def stream_body_llm(): + nonlocal conversation, stream_iterations + + def _emit_chain(extra: list[dict[str, Any]] | None = None): + # Return the full conversation (including the system message) so + # the client persists and replays it verbatim next turn. + chain = conversation + (extra or []) + return ( + json.dumps({"type": "messages", "messages": chain}).encode("utf-8") + + b"\n" + ) + + while stream_iterations < max_iterations: + if await request.is_disconnected(): + logger.debug("Client disconnected, stopping chat stream") + return + logger.debug( + f"Streaming LLM (iteration {stream_iterations + 1}/{max_iterations}) " + f"with {len(conversation)} message(s)" + ) + async for event in genai_client.chat_with_tools_stream( + messages=conversation, + tools=tools if tools else None, + tool_choice="auto", + enable_thinking=body.enable_thinking, + ): + if await request.is_disconnected(): + logger.debug("Client disconnected, stopping chat stream") + return + kind, value = event + if kind == "content_delta": + yield ( + json.dumps({"type": "content", "delta": value}).encode( + "utf-8" + ) + + b"\n" + ) + elif kind == "reasoning_delta": + yield ( + json.dumps({"type": "reasoning", "delta": value}).encode( + "utf-8" + ) + + b"\n" + ) + elif kind == "stats": + yield ( + json.dumps({"type": "stats", **value}).encode("utf-8") + + b"\n" + ) + elif kind == "message": + msg = value + if msg.get("finish_reason") == "error": + yield ( + json.dumps( + { + "type": "error", + "error": "An error occurred while processing your request.", + } + ).encode("utf-8") + + b"\n" + ) + return + pending = msg.get("tool_calls") + if pending: + stream_iterations += 1 + conversation.append( + build_assistant_message_for_conversation( + msg.get("content"), pending + ) + ) + if await request.is_disconnected(): + logger.debug( + "Client disconnected before tool execution" + ) + return + ( + _executed_calls, + tool_results, + extra_msgs, + ) = await _execute_pending_tools( + pending, request, allowed_cameras + ) + conversation.extend(tool_results) + conversation.extend(extra_msgs) + # Emit the running chain so the client can render tool + # calls live and replay them verbatim next turn. + yield _emit_chain() + break + else: + # Streaming never appends the final assistant message + # to the conversation, so add it to the chain. + yield _emit_chain( + extra=[ + { + "role": "assistant", + "content": msg.get("content"), + } + ] + ) + yield (json.dumps({"type": "done"}).encode("utf-8") + b"\n") + return + else: + yield _emit_chain() + yield json.dumps({"type": "done"}).encode("utf-8") + b"\n" + + return StreamingResponse( + stream_body_llm(), + media_type="application/x-ndjson", + headers={"X-Accel-Buffering": "no"}, + ) + + try: + while tool_iterations < max_iterations: + logger.debug( + f"Calling LLM (iteration {tool_iterations + 1}/{max_iterations}) " + f"with {len(conversation)} message(s) in conversation" + ) + response = genai_client.chat_with_tools( + messages=conversation, + tools=tools if tools else None, + tool_choice="auto", + enable_thinking=body.enable_thinking, + ) + + if response.get("finish_reason") == "error": + logger.error("GenAI client returned an error") + return JSONResponse( + content={ + "error": "An error occurred while processing your request.", + }, + status_code=500, + ) + + conversation.append( + build_assistant_message_for_conversation( + response.get("content"), response.get("tool_calls") + ) + ) + + pending_tool_calls = response.get("tool_calls") + if not pending_tool_calls: + logger.debug( + f"Chat completion finished with final answer (iterations: {tool_iterations})" + ) + final_content = response.get("content") or "" + + if body.stream: + final_reasoning = response.get("reasoning") + + chain = list(conversation) + + async def stream_body() -> Any: + yield ( + json.dumps({"type": "messages", "messages": chain}).encode( + "utf-8" + ) + + b"\n" + ) + # Emit the full reasoning trace up front when the + # underlying client did not stream it + if final_reasoning: + yield ( + json.dumps( + {"type": "reasoning", "delta": final_reasoning} + ).encode("utf-8") + + b"\n" + ) + # Stream content in word-sized chunks for smooth UX + for part in chunk_content(final_content): + yield ( + json.dumps({"type": "content", "delta": part}).encode( + "utf-8" + ) + + b"\n" + ) + yield json.dumps({"type": "done"}).encode("utf-8") + b"\n" + + return StreamingResponse( + stream_body(), + media_type="application/x-ndjson", + ) + + return JSONResponse( + content=ChatCompletionResponse( + message=ChatMessageResponse( + role="assistant", + content=final_content, + reasoning=response.get("reasoning"), + tool_calls=None, + ), + finish_reason=response.get("finish_reason", "stop"), + tool_iterations=tool_iterations, + tool_calls=tool_calls, + messages=list(conversation), + ).model_dump(), + ) + + tool_iterations += 1 + logger.debug( + f"Tool calls detected (iteration {tool_iterations}/{max_iterations}): " + f"{len(pending_tool_calls)} tool(s) to execute" + ) + executed_calls, tool_results, extra_msgs = await _execute_pending_tools( + pending_tool_calls, request, allowed_cameras + ) + tool_calls.extend(executed_calls) + conversation.extend(tool_results) + conversation.extend(extra_msgs) + logger.debug( + f"Added {len(tool_results)} tool result(s) to conversation. " + f"Continuing with next LLM call..." + ) + + logger.warning( + f"Max tool iterations ({max_iterations}) reached. Returning partial response." + ) + return JSONResponse( + content=ChatCompletionResponse( + message=ChatMessageResponse( + role="assistant", + content="I reached the maximum number of tool call iterations. Please try rephrasing your question.", + tool_calls=None, + ), + finish_reason="length", + tool_iterations=tool_iterations, + tool_calls=tool_calls, + messages=list(conversation), + ).model_dump(), + ) + + except Exception as e: + logger.exception(f"Error in chat completion: {e}") + return JSONResponse( + content={ + "error": "An error occurred while processing your request.", + }, + status_code=500, + ) + + +# --------------------------------------------------------------------------- +# VLM Monitor endpoints +# --------------------------------------------------------------------------- + + +@router.post( + "/vlm/monitor", + dependencies=[Depends(allow_any_authenticated())], + summary="Start a VLM watch job", + description=( + "Start monitoring a camera with the vision provider. " + "The VLM analyzes live frames until the specified condition is met, " + "then sends a notification. Only one watch job can run at a time." + ), +) +async def start_vlm_monitor( + request: Request, + body: VLMMonitorRequest, +) -> JSONResponse: + config = request.app.frigate_config + genai_manager = request.app.genai_manager + + if body.camera not in config.cameras: + return JSONResponse( + content={"success": False, "message": f"Camera '{body.camera}' not found."}, + status_code=404, + ) + + await require_camera_access(body.camera, request=request) + + chat_client = genai_manager.chat_client + if chat_client is None or not chat_client.supports_vision: + return JSONResponse( + content={ + "success": False, + "message": "VLM watch requires a chat model with vision support.", + }, + status_code=400, + ) + + try: + job_id = start_vlm_watch_job( + camera=body.camera, + condition=body.condition, + max_duration_minutes=body.max_duration_minutes, + config=config, + frame_processor=request.app.detected_frames_processor, + genai_manager=genai_manager, + dispatcher=request.app.dispatcher, + labels=body.labels, + zones=body.zones, + username=request.headers.get("remote-user", ""), + ) + except RuntimeError as e: + logger.exception("Failed to start VLM watch job: %s", e) + return JSONResponse( + content={"success": False, "message": "Failed to start VLM watch job."}, + status_code=409, + ) + + return JSONResponse( + content={"success": True, "job_id": job_id}, + status_code=201, + ) + + +@router.get( + "/vlm/monitor", + dependencies=[Depends(allow_any_authenticated())], + summary="Get current VLM watch job", + description="Returns the current (or most recently completed) VLM watch job.", +) +async def get_vlm_monitor(request: Request) -> JSONResponse: + job = get_vlm_watch_job() + if job is None: + return JSONResponse(content={"active": False}, status_code=200) + + role = request.headers.get("remote-role", "viewer") + username = request.headers.get("remote-user", "") + + # Admin and the job's creator always see the job. Other users only see it + # if they have access to the camera being watched; otherwise hide it. + if role != "admin" and username != job.username: + try: + await require_camera_access(job.camera, request=request) + except HTTPException: + return JSONResponse(content={"active": False}, status_code=200) + + return JSONResponse(content={"active": True, **job.to_dict()}, status_code=200) + + +@router.delete( + "/vlm/monitor", + dependencies=[Depends(allow_any_authenticated())], + summary="Cancel the current VLM watch job", + description="Cancels the running watch job if one exists.", +) +async def cancel_vlm_monitor(request: Request) -> JSONResponse: + job = get_vlm_watch_job() + if job is None: + return JSONResponse( + content={"success": False, "message": "No active watch job to cancel."}, + status_code=404, + ) + + role = request.headers.get("remote-role", "viewer") + username = request.headers.get("remote-user", "") + + # Admin can cancel any job; other users can only cancel jobs they started. + if role != "admin" and username != job.username: + return JSONResponse( + content={ + "success": False, + "message": "Not authorized to cancel this watch job.", + }, + status_code=403, + ) + + cancelled = stop_vlm_watch_job() + if not cancelled: + return JSONResponse( + content={"success": False, "message": "No active watch job to cancel."}, + status_code=404, + ) + return JSONResponse(content={"success": True}, status_code=200) diff --git a/frigate/api/chat_util.py b/frigate/api/chat_util.py new file mode 100644 index 0000000..a2f29c7 --- /dev/null +++ b/frigate/api/chat_util.py @@ -0,0 +1,136 @@ +"""Pure, stateless helpers used by the chat tool dispatchers. + +These were extracted from frigate/api/chat.py to keep that module focused on +route handlers, tool dispatchers, and streaming loop internals. Nothing in +this file touches the FastAPI request, the embeddings context, or the chat +loop state — all inputs and outputs are plain data. +""" + +import logging +import math +import time +from collections.abc import Generator +from datetime import datetime +from typing import Any + +from frigate.embeddings.util import ZScoreNormalization +from frigate.models import Event + +logger = logging.getLogger(__name__) + + +# Similarity fusion weights for find_similar_objects. +# Visual dominates because the feature's primary use case is "same specific object." +# If these change, update the test in test_chat_find_similar_objects.py. +VISUAL_WEIGHT = 0.65 +DESCRIPTION_WEIGHT = 0.35 + + +def chunk_content(content: str, chunk_size: int = 80) -> Generator[str, None, None]: + """Yield content in word-aware chunks for streaming.""" + if not content: + return + words = content.split(" ") + current: list[str] = [] + current_len = 0 + for w in words: + current.append(w) + current_len += len(w) + 1 + if current_len >= chunk_size: + yield " ".join(current) + " " + current = [] + current_len = 0 + if current: + yield " ".join(current) + + +def format_events_with_local_time( + events_list: list[dict[str, Any]], +) -> list[dict[str, Any]]: + """Add human-readable local start/end times to each event for the LLM.""" + result = [] + for evt in events_list: + if not isinstance(evt, dict): + result.append(evt) + continue + copy_evt = dict(evt) + try: + start_ts = evt.get("start_time") + end_ts = evt.get("end_time") + if start_ts is not None: + dt_start = datetime.fromtimestamp(start_ts) + copy_evt["start_time_local"] = dt_start.strftime("%Y-%m-%d %I:%M:%S %p") + if end_ts is not None: + dt_end = datetime.fromtimestamp(end_ts) + copy_evt["end_time_local"] = dt_end.strftime("%Y-%m-%d %I:%M:%S %p") + except (TypeError, ValueError, OSError): + pass + result.append(copy_evt) + return result + + +def distance_to_score(distance: float, stats: ZScoreNormalization) -> float: + """Convert a cosine distance to a [0, 1] similarity score. + + Uses the existing ZScoreNormalization stats maintained by EmbeddingsContext + to normalize across deployments, then a bounded sigmoid. Lower distance -> + higher score. If stats are uninitialized (stddev == 0), returns a neutral + 0.5 so the fallback ordering by raw distance still dominates. + """ + if stats.stddev == 0: + return 0.5 + z = (distance - stats.mean) / stats.stddev + # Sigmoid on -z so that small distance (good) -> high score. + return 1.0 / (1.0 + math.exp(z)) + + +def fuse_scores( + visual_score: float | None, + description_score: float | None, +) -> float | None: + """Weighted fusion of visual and description similarity scores. + + If one side is missing (e.g., no description embedding for this event), + the other side's score is returned alone with no penalty. If both are + missing, returns None and the caller should drop the event. + """ + if visual_score is None and description_score is None: + return None + if visual_score is None: + return description_score + if description_score is None: + return visual_score + return VISUAL_WEIGHT * visual_score + DESCRIPTION_WEIGHT * description_score + + +def parse_iso_to_timestamp(value: str | None) -> float | None: + """Parse an ISO-8601 string as server-local time -> unix timestamp. + + Mirrors the parsing _execute_search_objects uses so both tools accept the + same format from the LLM. + """ + if value is None: + return None + try: + s = value.replace("Z", "").strip()[:19] + dt = datetime.strptime(s, "%Y-%m-%dT%H:%M:%S") + return time.mktime(dt.timetuple()) + except (ValueError, AttributeError, TypeError): + logger.warning("Invalid timestamp format: %s", value) + return None + + +def hydrate_event(event: Event, score: float | None = None) -> dict[str, Any]: + """Convert an Event row into the dict shape returned by find_similar_objects.""" + data: dict[str, Any] = { + "id": event.id, + "camera": event.camera, + "label": event.label, + "sub_label": event.sub_label, + "start_time": event.start_time, + "end_time": event.end_time, + "zones": event.zones, + } + if score is not None: + data["score"] = score + return data diff --git a/frigate/api/classification.py b/frigate/api/classification.py new file mode 100644 index 0000000..6dd7055 --- /dev/null +++ b/frigate/api/classification.py @@ -0,0 +1,1274 @@ +"""Object classification APIs.""" + +import datetime +import logging +import os +import random +import shutil +import string +from typing import Any + +import cv2 +from fastapi import APIRouter, Depends, Request, UploadFile +from fastapi.responses import JSONResponse +from pathvalidate import sanitize_filename +from peewee import DoesNotExist +from playhouse.shortcuts import model_to_dict + +from frigate.api.auth import require_role +from frigate.api.defs.request.classification_body import ( + AudioTranscriptionBody, + DeleteFaceImagesBody, + GenerateObjectExamplesBody, + GenerateStateExamplesBody, + RenameFaceBody, +) +from frigate.api.defs.response.classification_response import ( + FaceRecognitionResponse, + FacesResponse, +) +from frigate.api.defs.response.generic_response import GenericResponse +from frigate.api.defs.tags import Tags +from frigate.config import FrigateConfig +from frigate.config.camera import DetectConfig +from frigate.config.classification import ObjectClassificationType +from frigate.const import CLIPS_DIR, FACE_DIR, MODEL_CACHE_DIR +from frigate.embeddings import EmbeddingsContext +from frigate.models import Event +from frigate.util.classification import ( + collect_object_classification_examples, + collect_state_classification_examples, + get_dataset_image_count, + read_training_metadata, + write_training_metadata, +) +from frigate.util.file import get_event_snapshot + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.classification]) + + +@router.get( + "/faces", + response_model=FacesResponse, + summary="Get all registered faces", + description="""Returns a dictionary mapping face names to lists of image filenames. + Each key represents a registered face name, and the value is a list of image + files associated with that face. Supported image formats include .webp, .png, + .jpg, and .jpeg.""", +) +def get_faces(): + face_dict: dict[str, list[str]] = {} + + if not os.path.exists(FACE_DIR): + return JSONResponse(status_code=200, content={}) + + for name in os.listdir(FACE_DIR): + face_dir = os.path.join(FACE_DIR, name) + + if not os.path.isdir(face_dir): + continue + + face_dict[name] = [] + + for file in filter( + lambda f: f.lower().endswith((".webp", ".png", ".jpg", ".jpeg")), + os.listdir(face_dir), + ): + face_dict[name].append(file) + + return JSONResponse(status_code=200, content=face_dict) + + +@router.post( + "/faces/reprocess", + dependencies=[Depends(require_role(["admin"]))], + summary="Reprocess a face training image", + description="""Reprocesses a face training image to update the prediction. + Requires face recognition to be enabled in the configuration. The training file + must exist in the faces/train directory. Returns a success response or an error + message if face recognition is not enabled or the training file is invalid.""", +) +def reclassify_face(request: Request, body: dict = None): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + json: dict[str, Any] = body or {} + training_file = os.path.join( + FACE_DIR, f"train/{sanitize_filename(json.get('training_file', ''))}" + ) + + if not training_file or not os.path.isfile(training_file): + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid filename or no file exists: {training_file}", + } + ), + status_code=404, + ) + + context: EmbeddingsContext = request.app.embeddings + response = context.reprocess_face(training_file) + + if not isinstance(response, dict): + return JSONResponse( + status_code=500, + content={ + "success": False, + "message": "Could not process request.", + }, + ) + + return JSONResponse( + status_code=200 if response.get("success", True) else 400, + content=response, + ) + + +@router.post( + "/faces/train/{name}/classify", + response_model=GenericResponse, + summary="Classify and save a face training image", + description="""Adds a training image to a specific face name for face recognition. + Accepts either a training file from the train directory or an event_id to extract + the face from. The image is saved to the face's directory and the face classifier + is cleared to incorporate the new training data. Returns a success message with + the new filename or an error if face recognition is not enabled, the file/event + is invalid, or the face cannot be extracted.""", +) +def train_face(request: Request, name: str, body: dict = None): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + json: dict[str, Any] = body or {} + training_file_name = sanitize_filename(json.get("training_file", "")) + training_file = os.path.join(FACE_DIR, f"train/{training_file_name}") + event_id = json.get("event_id") + + if not training_file_name and not event_id: + return JSONResponse( + content=( + { + "success": False, + "message": "A training file or event_id must be passed.", + } + ), + status_code=400, + ) + + if training_file_name and not os.path.isfile(training_file): + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid filename or no file exists: {training_file_name}", + } + ), + status_code=404, + ) + + sanitized_name = sanitize_filename(name) + new_name = f"{sanitized_name}-{datetime.datetime.now().timestamp()}.webp" + new_file_folder = os.path.join(FACE_DIR, f"{sanitized_name}") + + os.makedirs(new_file_folder, exist_ok=True) + + if training_file_name: + shutil.move(training_file, os.path.join(new_file_folder, new_name)) + else: + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid event_id or no event exists: {event_id}", + } + ), + status_code=404, + ) + + snapshot = get_event_snapshot(event) + face_box = event.data["attributes"][0]["box"] + detect_config: DetectConfig = request.app.frigate_config.cameras[ + event.camera + ].detect + + # crop onto the face box minus the bounding box itself + x1 = int(face_box[0] * detect_config.width) + 2 + y1 = int(face_box[1] * detect_config.height) + 2 + x2 = x1 + int(face_box[2] * detect_config.width) - 4 + y2 = y1 + int(face_box[3] * detect_config.height) - 4 + face = snapshot[y1:y2, x1:x2] + success = True + + if face.size > 0: + try: + cv2.imwrite(os.path.join(new_file_folder, new_name), face) + success = True + except Exception: + pass + + if not success: + return JSONResponse( + content=( + { + "success": False, + "message": "Invalid face box or no face exists", + } + ), + status_code=404, + ) + + context: EmbeddingsContext = request.app.embeddings + context.clear_face_classifier() + + return JSONResponse( + content=( + { + "success": True, + "message": f"Successfully saved {training_file_name} as {new_name}.", + } + ), + status_code=200, + ) + + +@router.post( + "/faces/{name}/create", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Create a new face name", + description="""Creates a new folder for a face name in the faces directory. + This is used to organize face training images. The face name is sanitized and + spaces are replaced with underscores. Returns a success message or an error if + face recognition is not enabled.""", +) +async def create_face(request: Request, name: str): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + os.makedirs( + os.path.join(FACE_DIR, sanitize_filename(name.replace(" ", "_"))), exist_ok=True + ) + return JSONResponse( + status_code=200, + content={"success": False, "message": "Successfully created face folder."}, + ) + + +@router.post( + "/faces/{name}/register", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Register a face image", + description="""Registers a face image for a specific face name by uploading an image file. + The uploaded image is processed and added to the face recognition system. Returns a + success response with details about the registration, or an error if face recognition + is not enabled or the image cannot be processed.""", +) +def register_face(request: Request, name: str, file: UploadFile): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + context: EmbeddingsContext = request.app.embeddings + result = None if context is None else context.register_face(name, file.file.read()) + + if not isinstance(result, dict): + return JSONResponse( + status_code=500, + content={ + "success": False, + "message": "Could not process request. Try restarting Frigate.", + }, + ) + + return JSONResponse( + status_code=200 if result.get("success", True) else 400, + content=result, + ) + + +@router.post( + "/faces/recognize", + response_model=FaceRecognitionResponse, + summary="Recognize a face from an uploaded image", + description="""Recognizes a face from an uploaded image file by comparing it against + registered faces in the system. Returns the recognized face name and confidence score, + or an error if face recognition is not enabled or the image cannot be processed.""", +) +def recognize_face(request: Request, file: UploadFile): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + context: EmbeddingsContext = request.app.embeddings + result = context.recognize_face(file.file.read()) + + if not isinstance(result, dict): + return JSONResponse( + status_code=500, + content={ + "success": False, + "message": "Could not process request. Try restarting Frigate.", + }, + ) + + return JSONResponse( + status_code=200 if result.get("success", True) else 400, + content=result, + ) + + +@router.post( + "/faces/{name}/reclassify", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Reclassify a face image to a different name", + description="""Moves a single face image from one person's folder to another. + The image is moved and renamed, and the face classifier is cleared to + incorporate the change. Returns a success message or an error if the + image or target name is invalid.""", +) +def reclassify_face_image(request: Request, name: str, body: dict = None): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + json: dict[str, Any] = body or {} + image_id = sanitize_filename(json.get("id", "")) + new_name = sanitize_filename(json.get("new_name", "")) + + if not image_id or not new_name: + return JSONResponse( + content=( + { + "success": False, + "message": "Both 'id' and 'new_name' are required.", + } + ), + status_code=400, + ) + + if new_name == name: + return JSONResponse( + content=( + { + "success": False, + "message": "New name must differ from the current name.", + } + ), + status_code=400, + ) + + source_folder = os.path.join(FACE_DIR, sanitize_filename(name)) + source_file = os.path.join(source_folder, image_id) + + if not os.path.isfile(source_file): + return JSONResponse( + content=( + { + "success": False, + "message": f"Image not found: {image_id}", + } + ), + status_code=404, + ) + + target_filename = f"{new_name}-{datetime.datetime.now().timestamp()}.webp" + target_folder = os.path.join(FACE_DIR, new_name) + + os.makedirs(target_folder, exist_ok=True) + shutil.move(source_file, os.path.join(target_folder, target_filename)) + + # Clean up empty source folder + if os.path.exists(source_folder) and not os.listdir(source_folder): + os.rmdir(source_folder) + + context: EmbeddingsContext = request.app.embeddings + context.clear_face_classifier() + + return JSONResponse( + content=({"success": True, "message": "Successfully reclassified face."}), + status_code=200, + ) + + +@router.post( + "/faces/{name}/delete", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete face images", + description="""Deletes specific face images for a given face name. The image IDs must belong + to the specified face folder. To delete an entire face folder, all image IDs in that + folder must be sent. Returns a success message or an error if face recognition is not enabled.""", +) +def deregister_faces(request: Request, name: str, body: DeleteFaceImagesBody): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + context: EmbeddingsContext = request.app.embeddings + context.delete_face_ids(name, map(lambda file: sanitize_filename(file), body.ids)) + return JSONResponse( + content=({"success": True, "message": "Successfully deleted faces."}), + status_code=200, + ) + + +@router.put( + "/faces/{old_name}/rename", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Rename a face name", + description="""Renames a face name in the system. The old name must exist and the new + name must be valid. Returns a success message or an error if face recognition is not enabled.""", +) +def rename_face(request: Request, old_name: str, body: RenameFaceBody): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + context: EmbeddingsContext = request.app.embeddings + try: + context.rename_face(old_name, body.new_name) + return JSONResponse( + content={ + "success": True, + "message": f"Successfully renamed face to {body.new_name}.", + }, + status_code=200, + ) + except ValueError as e: + logger.error(e) + return JSONResponse( + status_code=400, + content={ + "message": "Error renaming face. Check Frigate logs.", + "success": False, + }, + ) + + +@router.put( + "/lpr/reprocess", + summary="Reprocess a license plate", + description="""Reprocesses a license plate image to update the plate. + Requires license plate recognition to be enabled in the configuration. The event_id + must exist in the database. Returns a success message or an error if license plate + recognition is not enabled or the event_id is invalid.""", +) +def reprocess_license_plate(request: Request, event_id: str): + if not request.app.frigate_config.lpr.enabled: + message = "License plate recognition is not enabled." + logger.error(message) + return JSONResponse( + content=( + { + "success": False, + "message": message, + } + ), + status_code=400, + ) + + try: + event = Event.get(Event.id == event_id) + except DoesNotExist: + message = f"Event {event_id} not found" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=404 + ) + + context: EmbeddingsContext = request.app.embeddings + response = context.reprocess_plate(model_to_dict(event)) + + return JSONResponse( + content=response, + status_code=200, + ) + + +@router.put( + "/reindex", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Reindex embeddings", + description="""Reindexes the embeddings for all tracked objects. + Requires semantic search to be enabled in the configuration. Returns a success message or an error if semantic search is not enabled.""", +) +def reindex_embeddings(request: Request): + if not request.app.frigate_config.semantic_search.enabled: + message = ( + "Cannot reindex tracked object embeddings, Semantic Search is not enabled." + ) + logger.error(message) + return JSONResponse( + content=( + { + "success": False, + "message": message, + } + ), + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + response = context.reindex_embeddings() + + if response == "started": + return JSONResponse( + content={ + "success": True, + "message": "Embeddings reindexing has started.", + }, + status_code=202, # 202 Accepted + ) + elif response == "in_progress": + return JSONResponse( + content={ + "success": False, + "message": "Embeddings reindexing is already in progress.", + }, + status_code=409, # 409 Conflict + ) + else: + return JSONResponse( + content={ + "success": False, + "message": "Failed to start reindexing.", + }, + status_code=500, + ) + + +@router.put( + "/audio/transcribe", + response_model=GenericResponse, + summary="Transcribe audio", + description="""Transcribes audio from a specific event. + Requires audio transcription to be enabled in the configuration. The event_id + must exist in the database. Returns a success message or an error if audio transcription is not enabled or the event_id is invalid.""", +) +def transcribe_audio(request: Request, body: AudioTranscriptionBody): + event_id = body.event_id + + try: + event = Event.get(Event.id == event_id) + except DoesNotExist: + message = f"Event {event_id} not found" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=404 + ) + + if not request.app.frigate_config.cameras[event.camera].audio_transcription.enabled: + message = f"Audio transcription is not enabled for {event.camera}." + logger.error(message) + return JSONResponse( + content=( + { + "success": False, + "message": message, + } + ), + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + response = context.transcribe_audio(model_to_dict(event)) + + if response == "started": + return JSONResponse( + content={ + "success": True, + "message": "Audio transcription has started.", + }, + status_code=202, # 202 Accepted + ) + elif response == "in_progress": + return JSONResponse( + content={ + "success": False, + "message": "Audio transcription for a speech event is currently in progress. Try again later.", + }, + status_code=409, # 409 Conflict + ) + else: + logger.debug(f"Failed to transcribe audio, response: {response}") + return JSONResponse( + content={ + "success": False, + "message": "Failed to transcribe audio.", + }, + status_code=500, + ) + + +# custom classification training + + +@router.get( + "/classification/{name}/dataset", + summary="Get classification dataset", + description="""Gets the dataset for a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid.""", +) +def get_classification_dataset(name: str): + dataset_dict: dict[str, list[str]] = {} + + dataset_dir = os.path.join(CLIPS_DIR, sanitize_filename(name), "dataset") + + if not os.path.exists(dataset_dir): + return JSONResponse( + status_code=200, content={"categories": {}, "training_metadata": None} + ) + + for category_name in os.listdir(dataset_dir): + category_dir = os.path.join(dataset_dir, category_name) + + if not os.path.isdir(category_dir): + continue + + dataset_dict[category_name] = [] + + for file in filter( + lambda f: f.lower().endswith((".webp", ".png", ".jpg", ".jpeg")), + os.listdir(category_dir), + ): + dataset_dict[category_name].append(file) + + # Get training metadata + metadata = read_training_metadata(sanitize_filename(name)) + current_image_count = get_dataset_image_count(sanitize_filename(name)) + + if metadata is None: + training_metadata = { + "has_trained": False, + "last_training_date": None, + "last_training_image_count": 0, + "current_image_count": current_image_count, + "new_images_count": current_image_count, + "dataset_changed": current_image_count > 0, + } + else: + last_training_count = metadata.get("last_training_image_count", 0) + # Dataset has changed if count is different (either added or deleted images) + dataset_changed = current_image_count != last_training_count + # Only show positive count for new images (ignore deletions in the count display) + new_images_count = max(0, current_image_count - last_training_count) + training_metadata = { + "has_trained": True, + "last_training_date": metadata.get("last_training_date"), + "last_training_image_count": last_training_count, + "current_image_count": current_image_count, + "new_images_count": new_images_count, + "dataset_changed": dataset_changed, + } + + return JSONResponse( + status_code=200, + content={ + "categories": dataset_dict, + "training_metadata": training_metadata, + }, + ) + + +@router.get( + "/classification/attributes", + summary="Get custom classification attributes", + description="""Returns custom classification attributes for a given object type. + Only includes models with classification_type set to 'attribute'. + By default returns a flat sorted list of all attribute labels. + If group_by_model is true, returns attributes grouped by model name.""", +) +def get_custom_attributes( + request: Request, object_type: str = None, group_by_model: bool = False +): + models_with_attributes = {} + + for ( + model_key, + model_config, + ) in request.app.frigate_config.classification.custom.items(): + if ( + not model_config.enabled + or not model_config.object_config + or model_config.object_config.classification_type + != ObjectClassificationType.attribute + ): + continue + + model_objects = getattr(model_config.object_config, "objects", []) or [] + if object_type is not None and object_type not in model_objects: + continue + + dataset_dir = os.path.join(CLIPS_DIR, sanitize_filename(model_key), "dataset") + if not os.path.exists(dataset_dir): + continue + + attributes = [] + for category_name in os.listdir(dataset_dir): + category_dir = os.path.join(dataset_dir, category_name) + if os.path.isdir(category_dir) and category_name != "none": + attributes.append(category_name) + + if attributes: + model_name = model_config.name or model_key + models_with_attributes[model_name] = sorted(attributes) + + if group_by_model: + return JSONResponse(content=models_with_attributes) + else: + # Flatten to a unique sorted list + all_attributes = set() + for attributes in models_with_attributes.values(): + all_attributes.update(attributes) + return JSONResponse(content=sorted(list(all_attributes))) + + +@router.get( + "/classification/{name}/train", + summary="Get classification train images", + description="""Gets the train images for a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid.""", +) +def get_classification_images(name: str): + train_dir = os.path.join(CLIPS_DIR, sanitize_filename(name), "train") + + if not os.path.exists(train_dir): + return JSONResponse(status_code=200, content=[]) + + return JSONResponse( + status_code=200, + content=list( + filter( + lambda f: f.lower().endswith((".webp", ".png", ".jpg", ".jpeg")), + os.listdir(train_dir), + ) + ), + ) + + +@router.post( + "/classification/{name}/train", + response_model=GenericResponse, + summary="Train a classification model", + description="""Trains a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid.""", +) +async def train_configured_model(request: Request, name: str): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + context: EmbeddingsContext = request.app.embeddings + context.start_classification_training(name) + return JSONResponse( + content={"success": True, "message": "Started classification model training."}, + status_code=200, + ) + + +@router.post( + "/classification/{name}/dataset/{category}/delete", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete classification dataset images", + description="""Deletes specific dataset images for a given classification model and category. + The image IDs must belong to the specified category. Returns a success message or an error if the name or category is invalid.""", +) +def delete_classification_dataset_images( + request: Request, name: str, category: str, body: dict = None +): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + json: dict[str, Any] = body or {} + list_of_ids = json.get("ids", "") + folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", sanitize_filename(category) + ) + + deleted_count = 0 + for id in list_of_ids: + file_path = os.path.join(folder, sanitize_filename(id)) + + if os.path.isfile(file_path): + os.unlink(file_path) + deleted_count += 1 + + if os.path.exists(folder) and not os.listdir(folder) and category.lower() != "none": + os.rmdir(folder) + + # Update training metadata to reflect deleted images + # This ensures the dataset is marked as changed after deletion + # (even if the total count happens to be the same after adding and deleting) + if deleted_count > 0: + sanitized_name = sanitize_filename(name) + metadata = read_training_metadata(sanitized_name) + if metadata: + last_count = metadata.get("last_training_image_count", 0) + updated_count = max(0, last_count - deleted_count) + write_training_metadata(sanitized_name, updated_count) + + return JSONResponse( + content=({"success": True, "message": "Successfully deleted images."}), + status_code=200, + ) + + +@router.post( + "/classification/{name}/dataset/{category}/reclassify", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Reclassify a dataset image to a different category", + description="""Moves a single dataset image from one category to another. + The image is re-saved as PNG in the target category and removed from the source.""", +) +def reclassify_classification_image( + request: Request, name: str, category: str, body: dict = None +): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + json: dict[str, Any] = body or {} + image_id = sanitize_filename(json.get("id", "")) + new_category = sanitize_filename(json.get("new_category", "")) + + if not image_id or not new_category: + return JSONResponse( + content=( + { + "success": False, + "message": "Both 'id' and 'new_category' are required.", + } + ), + status_code=400, + ) + + if new_category == category: + return JSONResponse( + content=( + { + "success": False, + "message": "New category must differ from the current category.", + } + ), + status_code=400, + ) + + sanitized_name = sanitize_filename(name) + source_folder = os.path.join( + CLIPS_DIR, sanitized_name, "dataset", sanitize_filename(category) + ) + source_file = os.path.join(source_folder, image_id) + + if not os.path.isfile(source_file): + return JSONResponse( + content=( + { + "success": False, + "message": f"Image not found: {image_id}", + } + ), + status_code=404, + ) + + random_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + timestamp = datetime.datetime.now().timestamp() + new_name = f"{new_category}-{timestamp}-{random_id}.png" + target_folder = os.path.join(CLIPS_DIR, sanitized_name, "dataset", new_category) + + os.makedirs(target_folder, exist_ok=True) + + img = cv2.imread(source_file) + cv2.imwrite(os.path.join(target_folder, new_name), img) + os.unlink(source_file) + + # Clean up empty source folder (unless it is "none") + if ( + os.path.exists(source_folder) + and not os.listdir(source_folder) + and category.lower() != "none" + ): + os.rmdir(source_folder) + + # Mark dataset as changed so UI knows retraining is needed + write_training_metadata(sanitized_name, 0) + + return JSONResponse( + content=({"success": True, "message": "Successfully reclassified image."}), + status_code=200, + ) + + +@router.put( + "/classification/{name}/dataset/{old_category}/rename", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Rename a classification category", + description="""Renames a classification category for a given classification model. + The old category must exist and the new name must be valid. Returns a success message or an error if the name is invalid.""", +) +def rename_classification_category( + request: Request, name: str, old_category: str, body: dict = None +): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + json: dict[str, Any] = body or {} + new_category = sanitize_filename(json.get("new_category", "")) + + if not new_category: + return JSONResponse( + content=( + { + "success": False, + "message": "New category name is required.", + } + ), + status_code=400, + ) + + old_folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", sanitize_filename(old_category) + ) + new_folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", new_category + ) + + if not os.path.exists(old_folder): + return JSONResponse( + content=( + { + "success": False, + "message": f"Category {old_category} does not exist.", + } + ), + status_code=404, + ) + + if os.path.exists(new_folder): + return JSONResponse( + content=( + { + "success": False, + "message": f"Category {new_category} already exists.", + } + ), + status_code=400, + ) + + try: + os.rename(old_folder, new_folder) + + # Mark dataset as ready to train by resetting training metadata + # This ensures the dataset is marked as changed after renaming + sanitized_name = sanitize_filename(name) + write_training_metadata(sanitized_name, 0) + + return JSONResponse( + content=( + { + "success": True, + "message": f"Successfully renamed category to {new_category}.", + } + ), + status_code=200, + ) + except Exception as e: + logger.error(f"Error renaming category: {e}") + return JSONResponse( + content=( + { + "success": False, + "message": "Failed to rename category", + } + ), + status_code=500, + ) + + +@router.post( + "/classification/{name}/dataset/categorize", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Categorize a classification image", + description="""Categorizes a specific classification image for a given classification model and category. + The image must exist in the specified category. Returns a success message or an error if the name or category is invalid.""", +) +def categorize_classification_image(request: Request, name: str, body: dict = None): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + json: dict[str, Any] = body or {} + category = sanitize_filename(json.get("category", "")) + training_file_name = sanitize_filename(json.get("training_file", "")) + training_file = os.path.join( + CLIPS_DIR, sanitize_filename(name), "train", training_file_name + ) + + if training_file_name and not os.path.isfile(training_file): + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid filename or no file exists: {training_file_name}", + } + ), + status_code=404, + ) + + random_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + timestamp = datetime.datetime.now().timestamp() + new_name = f"{category}-{timestamp}-{random_id}.png" + new_file_folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", category + ) + + os.makedirs(new_file_folder, exist_ok=True) + + # use opencv because webp images can not be used to train + img = cv2.imread(training_file) + cv2.imwrite(os.path.join(new_file_folder, new_name), img) + os.unlink(training_file) + + return JSONResponse( + content=({"success": True, "message": "Successfully categorized image."}), + status_code=200, + ) + + +@router.post( + "/classification/{name}/dataset/{category}/create", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Create an empty classification category folder", + description="""Creates an empty folder for a classification category. + This is used to create folders for categories that don't have images yet. + Returns a success message or an error if the name is invalid.""", +) +def create_classification_category(request: Request, name: str, category: str): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + category_folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", sanitize_filename(category) + ) + + os.makedirs(category_folder, exist_ok=True) + + return JSONResponse( + content=( + { + "success": True, + "message": f"Successfully created category folder: {category}", + } + ), + status_code=200, + ) + + +@router.post( + "/classification/{name}/train/delete", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete classification train images", + description="""Deletes specific train images for a given classification model. + The image IDs must belong to the specified train folder. Returns a success message or an error if the name is invalid.""", +) +def delete_classification_train_images(request: Request, name: str, body: dict = None): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + json: dict[str, Any] = body or {} + list_of_ids = json.get("ids", "") + folder = os.path.join(CLIPS_DIR, sanitize_filename(name), "train") + + for id in list_of_ids: + file_path = os.path.join(folder, sanitize_filename(id)) + + if os.path.isfile(file_path): + os.unlink(file_path) + + return JSONResponse( + content=({"success": True, "message": "Successfully deleted images."}), + status_code=200, + ) + + +@router.post( + "/classification/generate_examples/state", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Generate state classification examples", +) +async def generate_state_examples(request: Request, body: GenerateStateExamplesBody): + """Generate examples for state classification.""" + model_name = sanitize_filename(body.model_name) + cameras_normalized = { + camera_name: tuple(crop) + for camera_name, crop in body.cameras.items() + if camera_name in request.app.frigate_config.cameras + } + + collect_state_classification_examples(model_name, cameras_normalized) + + return JSONResponse( + content={"success": True, "message": "Example generation completed"}, + status_code=200, + ) + + +@router.post( + "/classification/generate_examples/object", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Generate object classification examples", +) +async def generate_object_examples(request: Request, body: GenerateObjectExamplesBody): + """Generate examples for object classification.""" + model_name = sanitize_filename(body.model_name) + collect_object_classification_examples(model_name, body.label) + + return JSONResponse( + content={"success": True, "message": "Example generation completed"}, + status_code=200, + ) + + +@router.delete( + "/classification/{name}", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete a classification model", + description="""Deletes a specific classification model and all its associated data. + Works even if the model is not in the config (e.g., partially created during wizard). + Returns a success message.""", +) +def delete_classification_model(request: Request, name: str): + sanitized_name = sanitize_filename(name) + + # Delete the classification model's data directory in clips + data_dir = os.path.join(CLIPS_DIR, sanitized_name) + if os.path.exists(data_dir): + try: + shutil.rmtree(data_dir) + logger.info(f"Deleted classification data directory for {name}") + except Exception as e: + logger.debug(f"Failed to delete data directory for {name}: {e}") + + # Delete the classification model's files in model_cache + model_dir = os.path.join(MODEL_CACHE_DIR, sanitized_name) + if os.path.exists(model_dir): + try: + shutil.rmtree(model_dir) + logger.info(f"Deleted classification model directory for {name}") + except Exception as e: + logger.debug(f"Failed to delete model directory for {name}: {e}") + + return JSONResponse( + content=( + { + "success": True, + "message": f"Successfully deleted classification model {name}.", + } + ), + status_code=200, + ) diff --git a/frigate/api/debug_replay.py b/frigate/api/debug_replay.py new file mode 100644 index 0000000..034da38 --- /dev/null +++ b/frigate/api/debug_replay.py @@ -0,0 +1,290 @@ +"""Debug replay API endpoints.""" + +import asyncio +import logging +from datetime import datetime + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse +from peewee import DoesNotExist +from pydantic import BaseModel, Field + +from frigate.api.auth import require_role +from frigate.api.defs.tags import Tags +from frigate.jobs.debug_replay import ( + ExportDebugReplaySource, + RecordingDebugReplaySource, + start_debug_replay_job, +) +from frigate.models import Export +from frigate.util.services import get_video_properties + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.app]) + + +class DebugReplayStartBody(BaseModel): + """Request body for starting a debug replay session.""" + + camera: str = Field(title="Source camera name") + start_time: float = Field(title="Start timestamp") + end_time: float = Field(title="End timestamp") + + +class DebugReplayStartFromExportBody(BaseModel): + """Request body for starting a debug replay session from an export.""" + + export_id: str = Field(title="Export id") + + +class DebugReplayStartResponse(BaseModel): + """Response for starting a debug replay session.""" + + success: bool + replay_camera: str + job_id: str + + +class DebugReplayStatusResponse(BaseModel): + """Response for debug replay status. + + Returns only session-presence fields. Startup progress and error + details flow through the job_state WebSocket topic via the + debug_replay job (see frigate.jobs.debug_replay); the + Replay page subscribes there with useJobStatus("debug_replay"). + """ + + active: bool + replay_camera: str | None = None + source_camera: str | None = None + start_time: float | None = None + end_time: float | None = None + live_ready: bool = False + + +class DebugReplayStopResponse(BaseModel): + """Response for stopping a debug replay session.""" + + success: bool + + +@router.post( + "/debug_replay/start", + response_model=DebugReplayStartResponse, + status_code=202, + responses={ + 400: {"description": "Invalid camera, time range, or no recordings"}, + 409: {"description": "A replay session is already active"}, + }, + dependencies=[Depends(require_role(["admin"]))], + summary="Start debug replay", + description="Start a debug replay session from camera recordings. Returns " + "immediately while clip generation runs as a background job; subscribe " + "to the 'debug_replay' job_state WS topic to track progress.", +) +async def start_debug_replay(request: Request, body: DebugReplayStartBody): + """Start a debug replay session asynchronously.""" + replay_manager = request.app.replay_manager + internal_port = request.app.frigate_config.networking.listen.internal + if type(internal_port) is str: + internal_port = int(internal_port.split(":")[-1]) + + source = RecordingDebugReplaySource( + source_camera=body.camera, + start_ts=body.start_time, + end_ts=body.end_time, + internal_port=internal_port, + ) + + try: + job_id = await asyncio.to_thread( + start_debug_replay_job, + source=source, + frigate_config=request.app.frigate_config, + config_publisher=request.app.config_publisher, + replay_manager=replay_manager, + ) + except RuntimeError: + return JSONResponse( + content={ + "success": False, + "message": "A replay session is already active", + }, + status_code=409, + ) + except ValueError: + logger.exception("Rejected debug replay start request") + return JSONResponse( + content={ + "success": False, + "message": "Invalid debug replay parameters", + }, + status_code=400, + ) + + return JSONResponse( + content={ + "success": True, + "replay_camera": replay_manager.replay_camera_name, + "job_id": job_id, + }, + status_code=202, + ) + + +@router.post( + "/debug_replay/start_from_export", + response_model=DebugReplayStartResponse, + status_code=202, + responses={ + 400: {"description": "Invalid export, time range, or no recordings"}, + 404: {"description": "Export not found"}, + 409: {"description": "A replay session is already active"}, + }, + dependencies=[Depends(require_role(["admin"]))], + summary="Start debug replay from an export", + description="Start a debug replay session covering an existing export's " + "time range. The end time is derived from the export's video duration.", +) +async def start_debug_replay_from_export( + request: Request, body: DebugReplayStartFromExportBody +): + """Start a debug replay session from an existing export.""" + try: + export: Export = Export.get(Export.id == body.export_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Export not found"}, + status_code=404, + ) + + properties = await get_video_properties( + request.app.frigate_config.ffmpeg, export.video_path, get_duration=True + ) + duration = properties.get("duration", -1) + + if duration is None or duration <= 0: + return JSONResponse( + content={ + "success": False, + "message": "Could not determine export duration", + }, + status_code=400, + ) + + replay_manager = request.app.replay_manager + source = ExportDebugReplaySource(export=export, duration=float(duration)) + + try: + job_id = await asyncio.to_thread( + start_debug_replay_job, + source=source, + frigate_config=request.app.frigate_config, + config_publisher=request.app.config_publisher, + replay_manager=replay_manager, + ) + except RuntimeError: + return JSONResponse( + content={ + "success": False, + "message": "A replay session is already active", + }, + status_code=409, + ) + except ValueError: + logger.exception("Rejected debug replay start request") + return JSONResponse( + content={ + "success": False, + "message": "Invalid debug replay parameters", + }, + status_code=400, + ) + + return JSONResponse( + content={ + "success": True, + "replay_camera": replay_manager.replay_camera_name, + "job_id": job_id, + }, + status_code=202, + ) + + +@router.get( + "/debug_replay/status", + response_model=DebugReplayStatusResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Get debug replay status", + description="Get the status of the current debug replay session.", +) +def get_debug_replay_status(request: Request): + """Get the current replay session status.""" + replay_manager = request.app.replay_manager + + live_ready = False + replay_camera = replay_manager.replay_camera_name + + if replay_manager.active and replay_camera: + frame_processor = request.app.detected_frames_processor + frame = ( + frame_processor.get_current_frame(replay_camera) + if frame_processor is not None + else None + ) + + if frame is not None: + frame_time = frame_processor.get_current_frame_time(replay_camera) + camera_config = request.app.frigate_config.cameras.get(replay_camera) + retry_interval = 10.0 + + if camera_config is not None: + retry_interval = float(camera_config.ffmpeg.retry_interval or 10) + + live_ready = datetime.now().timestamp() <= frame_time + retry_interval + + return DebugReplayStatusResponse( + active=replay_manager.active, + replay_camera=replay_camera, + source_camera=replay_manager.source_camera, + start_time=replay_manager.start_ts, + end_time=replay_manager.end_ts, + live_ready=live_ready, + ) + + +@router.post( + "/debug_replay/stop", + response_model=DebugReplayStopResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Stop debug replay", + description="Stop the active debug replay session and clean up all artifacts.", +) +async def stop_debug_replay(request: Request): + """Stop the active replay session.""" + replay_manager = request.app.replay_manager + + if not replay_manager.active: + return JSONResponse( + content={"success": False, "message": "No active replay session"}, + status_code=400, + ) + + try: + await asyncio.to_thread( + replay_manager.stop, + frigate_config=request.app.frigate_config, + config_publisher=request.app.config_publisher, + ) + except (ValueError, RuntimeError, OSError) as e: + logger.error("Error stopping replay: %s", e) + return JSONResponse( + content={ + "success": False, + "message": "Failed to stop replay session due to an internal error.", + }, + status_code=500, + ) + + return DebugReplayStopResponse(success=True) diff --git a/frigate/api/defs/__init__.py b/frigate/api/defs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/api/defs/query/app_query_parameters.py b/frigate/api/defs/query/app_query_parameters.py new file mode 100644 index 0000000..626d396 --- /dev/null +++ b/frigate/api/defs/query/app_query_parameters.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class AppTimelineHourlyQueryParameters(BaseModel): + cameras: str | None = "all" + labels: str | None = "all" + after: float | None = None + before: float | None = None + limit: int | None = 200 + timezone: str | None = "utc" diff --git a/frigate/api/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py new file mode 100644 index 0000000..06d0dfc --- /dev/null +++ b/frigate/api/defs/query/events_query_parameters.py @@ -0,0 +1,81 @@ +from pydantic import BaseModel, Field + +DEFAULT_TIME_RANGE = "00:00,24:00" + + +class EventsQueryParams(BaseModel): + camera: str | None = "all" + cameras: str | None = "all" + label: str | None = "all" + labels: str | None = "all" + sub_label: str | None = "all" + sub_labels: str | None = "all" + attributes: str | None = "all" + zone: str | None = "all" + zones: str | None = "all" + limit: int | None = 100 + after: float | None = None + before: float | None = None + time_range: str | None = DEFAULT_TIME_RANGE + has_clip: int | None = None + has_snapshot: int | None = None + in_progress: int | None = None + include_thumbnails: int | None = Field( + 1, + description=( + "Deprecated. Thumbnail data is no longer included in the response. " + "Use the /api/events/:event_id/thumbnail.:extension endpoint instead." + ), + deprecated=True, + ) + favorites: int | None = None + min_score: float | None = None + max_score: float | None = None + min_speed: float | None = None + max_speed: float | None = None + recognized_license_plate: str | None = "all" + is_submitted: int | None = None + min_length: float | None = None + max_length: float | None = None + event_id: str | None = None + sort: str | None = None + timezone: str | None = "utc" + + +class EventsSearchQueryParams(BaseModel): + query: str | None = None + event_id: str | None = None + search_type: str | None = "thumbnail" + include_thumbnails: int | None = Field( + 1, + description=( + "Deprecated. Thumbnail data is no longer included in the response. " + "Use the /api/events/:event_id/thumbnail.:extension endpoint instead." + ), + deprecated=True, + ) + limit: int | None = 50 + cameras: str | None = "all" + labels: str | None = "all" + sub_labels: str | None = "all" + attributes: str | None = "all" + zones: str | None = "all" + after: float | None = None + before: float | None = None + time_range: str | None = DEFAULT_TIME_RANGE + has_clip: bool | None = None + has_snapshot: bool | None = None + is_submitted: bool | None = None + timezone: str | None = "utc" + min_score: float | None = None + max_score: float | None = None + min_speed: float | None = None + max_speed: float | None = None + recognized_license_plate: str | None = "all" + sort: str | None = None + + +class EventsSummaryQueryParams(BaseModel): + timezone: str | None = "utc" + has_clip: int | None = None + has_snapshot: int | None = None diff --git a/frigate/api/defs/query/media_query_parameters.py b/frigate/api/defs/query/media_query_parameters.py new file mode 100644 index 0000000..f115685 --- /dev/null +++ b/frigate/api/defs/query/media_query_parameters.py @@ -0,0 +1,48 @@ +from enum import Enum + +from pydantic import BaseModel + + +class Extension(str, Enum): + webp = "webp" + png = "png" + jpg = "jpg" + jpeg = "jpeg" + + def get_mime_type(self) -> str: + if self in (Extension.jpg, Extension.jpeg): + return "image/jpeg" + return f"image/{self.value}" + + +class MediaLatestFrameQueryParams(BaseModel): + bbox: int | None = None + timestamp: int | None = None + zones: int | None = None + mask: int | None = None + motion: int | None = None + paths: int | None = None + regions: int | None = None + quality: int | None = 70 + height: int | None = None + store: int | None = None + + +class MediaEventsSnapshotQueryParams(BaseModel): + download: bool | None = False + timestamp: int | None = None + bbox: int | None = None + crop: int | None = None + height: int | None = None + quality: int | None = None + + +class MediaMjpegFeedQueryParams(BaseModel): + fps: int = 3 + height: int = 360 + bbox: int | None = None + timestamp: int | None = None + zones: int | None = None + mask: int | None = None + motion: int | None = None + regions: int | None = None diff --git a/frigate/api/defs/query/recordings_query_parameters.py b/frigate/api/defs/query/recordings_query_parameters.py new file mode 100644 index 0000000..770da96 --- /dev/null +++ b/frigate/api/defs/query/recordings_query_parameters.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel +from pydantic.json_schema import SkipJsonSchema + + +class MediaRecordingsSummaryQueryParams(BaseModel): + timezone: str = "utc" + cameras: str | None = "all" + + +class MediaRecordingsAvailabilityQueryParams(BaseModel): + cameras: str = "all" + before: float | SkipJsonSchema[None] = None + after: float | SkipJsonSchema[None] = None + scale: int = 30 + + +class RecordingsDeleteQueryParams(BaseModel): + keep: str | None = None + cameras: str | None = "all" diff --git a/frigate/api/defs/query/regenerate_query_parameters.py b/frigate/api/defs/query/regenerate_query_parameters.py new file mode 100644 index 0000000..20d1016 --- /dev/null +++ b/frigate/api/defs/query/regenerate_query_parameters.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel, Field + +from frigate.events.types import RegenerateDescriptionEnum + + +class RegenerateQueryParameters(BaseModel): + source: RegenerateDescriptionEnum | None = RegenerateDescriptionEnum.thumbnails + force: bool | None = Field( + default=False, + description="Force (re)generating the description even if GenAI is disabled for this camera.", + ) diff --git a/frigate/api/defs/query/review_query_parameters.py b/frigate/api/defs/query/review_query_parameters.py new file mode 100644 index 0000000..b16146a --- /dev/null +++ b/frigate/api/defs/query/review_query_parameters.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel +from pydantic.json_schema import SkipJsonSchema + +from frigate.review.types import SeverityEnum + + +class ReviewQueryParams(BaseModel): + cameras: str = "all" + labels: str = "all" + zones: str = "all" + reviewed: int | SkipJsonSchema[None] = None + limit: int | SkipJsonSchema[None] = None + severity: SeverityEnum | SkipJsonSchema[None] = None + before: float | SkipJsonSchema[None] = None + after: float | SkipJsonSchema[None] = None + + +class ReviewSummaryQueryParams(BaseModel): + cameras: str = "all" + labels: str = "all" + zones: str = "all" + timezone: str = "utc" + + +class ReviewActivityMotionQueryParams(BaseModel): + cameras: str = "all" + before: float | SkipJsonSchema[None] = None + after: float | SkipJsonSchema[None] = None + scale: int = 30 diff --git a/frigate/api/defs/request/__init__.py b/frigate/api/defs/request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py new file mode 100644 index 0000000..b0b85c7 --- /dev/null +++ b/frigate/api/defs/request/app_body.py @@ -0,0 +1,60 @@ +from typing import Any + +from pydantic import BaseModel, Field + +from frigate.config import GenAIProviderEnum + + +class AppConfigSetBody(BaseModel): + requires_restart: int = 1 + update_topic: str | None = None + config_data: dict[str, Any] | None = None + skip_save: bool = False + + +class GenAIProbeBody(BaseModel): + provider: GenAIProviderEnum + api_key: str | None = None + base_url: str | None = None + provider_options: dict[str, Any] = Field(default_factory=dict) + + +class AppPutPasswordBody(BaseModel): + password: str + old_password: str | None = None + + +class AppPostUsersBody(BaseModel): + username: str + password: str + role: str | None = "viewer" + + +class AppPostLoginBody(BaseModel): + user: str + password: str + + +class AppPutRoleBody(BaseModel): + role: str + + +class CameraSetBody(BaseModel): + value: str = Field(..., description="The value to set for the feature") + + +class MediaSyncBody(BaseModel): + dry_run: bool = Field( + default=True, description="If True, only report orphans without deleting them" + ) + media_types: list[str] = Field( + default=["all"], + description="Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails', 'review_thumbnails', 'previews', 'exports', 'recordings'", + ) + force: bool = Field( + default=False, description="If True, bypass safety threshold checks" + ) + verbose: bool = Field( + default=False, + description="If True, write full orphan file list to disk", + ) diff --git a/frigate/api/defs/request/batch_export_body.py b/frigate/api/defs/request/batch_export_body.py new file mode 100644 index 0000000..24078bc --- /dev/null +++ b/frigate/api/defs/request/batch_export_body.py @@ -0,0 +1,63 @@ +from pydantic import BaseModel, Field, model_validator + +MAX_BATCH_EXPORT_ITEMS = 50 + + +class BatchExportItem(BaseModel): + camera: str = Field(title="Camera name") + start_time: float = Field(title="Start time") + end_time: float = Field(title="End time") + image_path: str | None = Field( + default=None, + title="Existing thumbnail path", + description="Optional existing image to use as the export thumbnail", + ) + friendly_name: str | None = Field( + default=None, + title="Friendly name", + max_length=256, + description="Optional friendly name for this specific export item", + ) + client_item_id: str | None = Field( + default=None, + title="Client item ID", + max_length=128, + description="Optional opaque client identifier echoed back in results", + ) + + +class BatchExportBody(BaseModel): + items: list[BatchExportItem] = Field( + title="Items", + min_length=1, + max_length=MAX_BATCH_EXPORT_ITEMS, + description="List of export items. Each item has its own camera and time range.", + ) + export_case_id: str | None = Field( + default=None, + title="Export case ID", + max_length=30, + description=( + "Existing export case ID to assign all exports to. Attaching to an " + "existing case is temporarily admin-only until case-level ACLs exist." + ), + ) + new_case_name: str | None = Field( + default=None, + title="New case name", + max_length=100, + description="Name of a new export case to create when export_case_id is omitted", + ) + new_case_description: str | None = Field( + default=None, + title="New case description", + description="Optional description for a newly created export case", + ) + + @model_validator(mode="after") + def validate_case_target(self) -> "BatchExportBody": + for item in self.items: + if item.end_time <= item.start_time: + raise ValueError("end_time must be after start_time") + + return self diff --git a/frigate/api/defs/request/chat_body.py b/frigate/api/defs/request/chat_body.py new file mode 100644 index 0000000..5ca674f --- /dev/null +++ b/frigate/api/defs/request/chat_body.py @@ -0,0 +1,61 @@ +"""Chat API request models.""" + +from typing import Any + +from pydantic import BaseModel, Field + + +class ChatMessage(BaseModel): + """A single message in a chat conversation.""" + + role: str = Field( + description="Message role: 'user', 'assistant', 'system', or 'tool'" + ) + content: Any | None = Field( + default=None, + description=( + "Message content. Usually a string, but may be a multimodal content " + "list (e.g. text + image_url) or null for assistant turns that only " + "request tool calls." + ), + ) + tool_call_id: str | None = Field( + default=None, description="For tool messages, the ID of the tool call" + ) + name: str | None = Field( + default=None, description="For tool messages, the tool name" + ) + tool_calls: list[dict[str, Any]] | None = Field( + default=None, + description=( + "For assistant messages replayed from prior turns, the OpenAI-format " + "tool calls the model previously requested. Replaying these verbatim " + "keeps the conversation prefix byte-for-byte identical so the model " + "server's prompt cache hits on follow-up turns." + ), + ) + + +class ChatCompletionRequest(BaseModel): + """Request for chat completion with tool calling.""" + + messages: list[ChatMessage] = Field( + description="List of messages in the conversation" + ) + max_tool_iterations: int = Field( + default=5, + ge=1, + le=10, + description="Maximum number of tool call iterations (default: 5)", + ) + stream: bool = Field( + default=False, + description="If true, stream the final assistant response in the body as newline-delimited JSON.", + ) + enable_thinking: bool | None = Field( + default=None, + description=( + "Per-request thinking toggle. None means use the provider default. " + "Ignored by providers that do not expose a per-request thinking switch." + ), + ) diff --git a/frigate/api/defs/request/classification_body.py b/frigate/api/defs/request/classification_body.py new file mode 100644 index 0000000..2aa90f4 --- /dev/null +++ b/frigate/api/defs/request/classification_body.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel, Field + + +class RenameFaceBody(BaseModel): + new_name: str = Field(description="New name for the face") + + +class AudioTranscriptionBody(BaseModel): + event_id: str = Field(description="ID of the event to transcribe audio for") + + +class DeleteFaceImagesBody(BaseModel): + ids: list[str] = Field( + description="List of image filenames to delete from the face folder" + ) + + +class GenerateStateExamplesBody(BaseModel): + model_name: str = Field(description="Name of the classification model") + cameras: dict[str, tuple[float, float, float, float]] = Field( + description="Dictionary mapping camera names to normalized crop coordinates in [x1, y1, x2, y2] format (values 0-1)" + ) + + +class GenerateObjectExamplesBody(BaseModel): + model_name: str = Field(description="Name of the classification model") + label: str = Field( + description="Object label to collect examples for (e.g., 'person', 'car')" + ) diff --git a/frigate/api/defs/request/events_body.py b/frigate/api/defs/request/events_body.py new file mode 100644 index 0000000..c7920ae --- /dev/null +++ b/frigate/api/defs/request/events_body.py @@ -0,0 +1,58 @@ +from pydantic import BaseModel, Field + +from frigate.config.classification import TriggerType + + +class EventsSubLabelBody(BaseModel): + subLabel: str = Field(title="Sub label", max_length=100) + subLabelScore: float | None = Field( + title="Score for sub label", default=None, gt=0.0, le=1.0 + ) + camera: str | None = Field(title="Camera this object is detected on.", default=None) + + +class EventsLPRBody(BaseModel): + recognizedLicensePlate: str = Field( + title="Recognized License Plate", max_length=100 + ) + recognizedLicensePlateScore: float | None = Field( + title="Score for recognized license plate", default=None, gt=0.0, le=1.0 + ) + + +class EventsAttributesBody(BaseModel): + attributes: list[str] = Field( + title="Selected classification attributes for the event", + default_factory=list, + ) + + +class EventsDescriptionBody(BaseModel): + description: str | None = Field(title="The description of the event") + + +class EventsCreateBody(BaseModel): + sub_label: str | None = None + score: float | None = 0 + duration: int | None = 30 + include_recording: bool | None = True + draw: dict | None = {} + pre_capture: int | None = None + + +class EventsEndBody(BaseModel): + end_time: float | None = None + + +class EventsDeleteBody(BaseModel): + event_ids: list[str] = Field(title="The event IDs to delete") + + +class SubmitPlusBody(BaseModel): + include_annotation: int = Field(default=1) + + +class TriggerEmbeddingBody(BaseModel): + type: TriggerType + data: str + threshold: float = Field(default=0.5, ge=0.0, le=1.0) diff --git a/frigate/api/defs/request/export_bulk_body.py b/frigate/api/defs/request/export_bulk_body.py new file mode 100644 index 0000000..07283f8 --- /dev/null +++ b/frigate/api/defs/request/export_bulk_body.py @@ -0,0 +1,22 @@ +"""Request bodies for bulk export operations.""" + +from pydantic import BaseModel, Field, conlist, constr + + +class ExportBulkDeleteBody(BaseModel): + """Request body for bulk deleting exports.""" + + # List of export IDs with at least one element and each element with at least one char + ids: conlist(constr(min_length=1), min_length=1) + + +class ExportBulkReassignBody(BaseModel): + """Request body for bulk reassigning exports to a case.""" + + # List of export IDs with at least one element and each element with at least one char + ids: conlist(constr(min_length=1), min_length=1) + export_case_id: str | None = Field( + default=None, + max_length=30, + description="Case ID to assign to, or null to unassign from current case", + ) diff --git a/frigate/api/defs/request/export_case_body.py b/frigate/api/defs/request/export_case_body.py new file mode 100644 index 0000000..c7b436f --- /dev/null +++ b/frigate/api/defs/request/export_case_body.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, Field + + +class ExportCaseCreateBody(BaseModel): + """Request body for creating a new export case.""" + + name: str = Field(max_length=100, description="Friendly name of the export case") + description: str | None = Field( + default=None, description="Optional description of the export case" + ) + + +class ExportCaseUpdateBody(BaseModel): + """Request body for updating an existing export case.""" + + name: str | None = Field( + default=None, + max_length=100, + description="Updated friendly name of the export case", + ) + description: str | None = Field( + default=None, description="Updated description of the export case" + ) diff --git a/frigate/api/defs/request/export_recordings_body.py b/frigate/api/defs/request/export_recordings_body.py new file mode 100644 index 0000000..beb8f39 --- /dev/null +++ b/frigate/api/defs/request/export_recordings_body.py @@ -0,0 +1,58 @@ +from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema + +from frigate.record.export import ( + ChaptersEnum, + PlaybackSourceEnum, +) + + +class ExportRecordingsBody(BaseModel): + source: PlaybackSourceEnum = Field( + default=PlaybackSourceEnum.recordings, title="Playback source" + ) + name: str | None = Field(title="Friendly name", default=None, max_length=256) + image_path: str | SkipJsonSchema[None] = None + export_case_id: str | None = Field( + default=None, + title="Export case ID", + max_length=30, + description="ID of the export case to assign this export to", + ) + chapters: ChaptersEnum | None = Field( + default=None, + title="Chapter mode", + description=( + "Optional chapter metadata to embed in the export. When omitted, " + "the camera's configured export chapter mode is used." + ), + ) + + +class ExportRecordingsCustomBody(BaseModel): + source: PlaybackSourceEnum = Field( + default=PlaybackSourceEnum.recordings, title="Playback source" + ) + name: str = Field(title="Friendly name", default=None, max_length=256) + image_path: str | SkipJsonSchema[None] = None + export_case_id: str | None = Field( + default=None, + title="Export case ID", + max_length=30, + description="ID of the export case to assign this export to", + ) + ffmpeg_input_args: str | None = Field( + default=None, + title="FFmpeg input arguments", + description="Custom FFmpeg input arguments. If not provided, defaults to timelapse input args.", + ) + ffmpeg_output_args: str | None = Field( + default=None, + title="FFmpeg output arguments", + description="Custom FFmpeg output arguments. If not provided, defaults to timelapse output args.", + ) + cpu_fallback: bool = Field( + default=False, + title="CPU Fallback", + description="If true, retry export without hardware acceleration if the initial export fails.", + ) diff --git a/frigate/api/defs/request/export_rename_body.py b/frigate/api/defs/request/export_rename_body.py new file mode 100644 index 0000000..dc5bc32 --- /dev/null +++ b/frigate/api/defs/request/export_rename_body.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel, Field + + +class ExportRenameBody(BaseModel): + name: str = Field(title="Friendly name", max_length=256) diff --git a/frigate/api/defs/request/review_body.py b/frigate/api/defs/request/review_body.py new file mode 100644 index 0000000..6dc7100 --- /dev/null +++ b/frigate/api/defs/request/review_body.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, conlist, constr + + +class ReviewModifyMultipleBody(BaseModel): + # List of string with at least one element and each element with at least one char + ids: conlist(constr(min_length=1), min_length=1) + # Whether to mark items as reviewed (True) or unreviewed (False) + reviewed: bool = True diff --git a/frigate/api/defs/response/chat_response.py b/frigate/api/defs/response/chat_response.py new file mode 100644 index 0000000..3007c4c --- /dev/null +++ b/frigate/api/defs/response/chat_response.py @@ -0,0 +1,67 @@ +"""Chat API response models.""" + +from typing import Any + +from pydantic import BaseModel, Field + + +class ToolCallInvocation(BaseModel): + """A tool call requested by the LLM (before execution).""" + + id: str = Field(description="Unique identifier for this tool call") + name: str = Field(description="Tool name to call") + arguments: dict[str, Any] = Field(description="Arguments for the tool call") + + +class ChatMessageResponse(BaseModel): + """A message in the chat response.""" + + role: str = Field(description="Message role") + content: str | None = Field( + default=None, description="Message content (None if tool calls present)" + ) + reasoning: str | None = Field( + default=None, + description="Separated reasoning/thinking trace if the model emitted one", + ) + tool_calls: list[ToolCallInvocation] | None = Field( + default=None, description="Tool calls if LLM wants to call tools" + ) + + +class ToolCall(BaseModel): + """A tool that was executed during the completion, with its response.""" + + name: str = Field(description="Tool name that was called") + arguments: dict[str, Any] = Field( + default_factory=dict, description="Arguments passed to the tool" + ) + response: str = Field( + default="", + description="The response or result returned from the tool execution", + ) + + +class ChatCompletionResponse(BaseModel): + """Response from chat completion.""" + + message: ChatMessageResponse = Field(description="The assistant's message") + finish_reason: str = Field( + description="Reason generation stopped: 'stop', 'tool_calls', 'length', 'error'" + ) + tool_iterations: int = Field( + default=0, description="Number of tool call iterations performed" + ) + tool_calls: list[ToolCall] = Field( + default_factory=list, + description="List of tool calls that were executed during this completion", + ) + messages: list[dict[str, Any]] = Field( + default_factory=list, + description=( + "The full conversation chain, including the system message. Persist " + "and replay this verbatim on the next request so the prompt prefix " + "stays byte-identical and the model server's prompt cache keeps " + "hitting." + ), + ) diff --git a/frigate/api/defs/response/classification_response.py b/frigate/api/defs/response/classification_response.py new file mode 100644 index 0000000..09cdfbc --- /dev/null +++ b/frigate/api/defs/response/classification_response.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, Field, RootModel + + +class FacesResponse(RootModel[dict[str, list[str]]]): + """Response model for the get_faces endpoint. + + Returns a mapping of face names to lists of image filenames. + Each face name corresponds to a directory in the faces folder, + and the list contains the names of image files for that face. + + Example: + { + "john_doe": ["face1.webp", "face2.jpg"], + "jane_smith": ["face3.png"] + } + """ + + root: dict[str, list[str]] = Field( + default_factory=dict, + description="Dictionary mapping face names to lists of image filenames", + ) + + +class FaceRecognitionResponse(BaseModel): + """Response model for face recognition endpoint. + + Returns the result of attempting to recognize a face from an uploaded image. + """ + + success: bool = Field(description="Whether the face recognition was successful") + score: float | None = Field( + default=None, description="Confidence score of the recognition (0-1)" + ) + face_name: str | None = Field( + default=None, description="The recognized face name if successful" + ) diff --git a/frigate/api/defs/response/event_response.py b/frigate/api/defs/response/event_response.py new file mode 100644 index 0000000..a366af8 --- /dev/null +++ b/frigate/api/defs/response/event_response.py @@ -0,0 +1,42 @@ +from typing import Any + +from pydantic import BaseModel, ConfigDict + + +class EventResponse(BaseModel): + id: str + label: str + sub_label: str | None + camera: str + start_time: float + end_time: float | None + false_positive: bool | None + zones: list[str] + thumbnail: str | None + has_clip: bool + has_snapshot: bool + retain_indefinitely: bool + plus_id: str | None + model_hash: str | None + detector_type: str | None + model_type: str | None + data: dict[str, Any] + + model_config = ConfigDict(protected_namespaces=()) + + +class EventCreateResponse(BaseModel): + success: bool + message: str + event_id: str + + +class EventMultiDeleteResponse(BaseModel): + success: bool + deleted_events: list[str] + not_found_events: list[str] + + +class EventUploadPlusResponse(BaseModel): + success: bool + plus_id: str diff --git a/frigate/api/defs/response/export_case_response.py b/frigate/api/defs/response/export_case_response.py new file mode 100644 index 0000000..199d5b6 --- /dev/null +++ b/frigate/api/defs/response/export_case_response.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel, Field + + +class ExportCaseModel(BaseModel): + """Model representing a single export case.""" + + id: str = Field(description="Unique identifier for the export case") + name: str = Field(description="Friendly name of the export case") + description: str | None = Field( + default=None, description="Optional description of the export case" + ) + created_at: float = Field( + description="Unix timestamp when the export case was created" + ) + updated_at: float = Field( + description="Unix timestamp when the export case was last updated" + ) + + +ExportCasesResponse = list[ExportCaseModel] diff --git a/frigate/api/defs/response/export_response.py b/frigate/api/defs/response/export_response.py new file mode 100644 index 0000000..5e90059 --- /dev/null +++ b/frigate/api/defs/response/export_response.py @@ -0,0 +1,123 @@ +from typing import Any + +from pydantic import BaseModel, Field + + +class ExportModel(BaseModel): + """Model representing a single export.""" + + id: str = Field(description="Unique identifier for the export") + camera: str = Field(description="Camera name associated with this export") + name: str = Field(description="Friendly name of the export") + date: float = Field(description="Unix timestamp when the export was created") + video_path: str = Field(description="File path to the exported video") + thumb_path: str = Field(description="File path to the export thumbnail") + in_progress: bool = Field( + description="Whether the export is currently being processed" + ) + export_case_id: str | None = Field( + default=None, description="ID of the export case this export belongs to" + ) + + +class StartExportResponse(BaseModel): + """Response model for starting an export.""" + + success: bool = Field(description="Whether the export was started successfully") + message: str = Field(description="Status or error message") + export_id: str | None = Field( + default=None, description="The export ID if successfully started" + ) + status: str | None = Field( + default=None, + description="Queue status for the export job", + ) + + +class BatchExportResultModel(BaseModel): + """Per-item result for a batch export request.""" + + camera: str = Field(description="Camera name for this export attempt") + export_id: str | None = Field( + default=None, + description="The export ID when the export was successfully queued", + ) + success: bool = Field(description="Whether the export was successfully queued") + status: str | None = Field( + default=None, + description="Queue status for this camera export", + ) + error: str | None = Field( + default=None, + description="Validation or queueing error for this item, if any", + ) + item_index: int | None = Field( + default=None, + description="Zero-based index of this result within the request items list", + ) + client_item_id: str | None = Field( + default=None, + description="Opaque client-supplied item identifier echoed from the request", + ) + + +class BatchExportResponse(BaseModel): + """Response model for starting an export batch.""" + + export_case_id: str | None = Field( + default=None, + description="Export case ID associated with the batch", + ) + export_ids: list[str] = Field(description="Export IDs successfully queued") + results: list[BatchExportResultModel] = Field( + description="Per-item batch export results" + ) + + +class ExportJobModel(BaseModel): + """Model representing a queued or running export job.""" + + id: str = Field(description="Unique identifier for the export job") + job_type: str = Field(description="Job type") + status: str = Field(description="Current job status") + camera: str = Field(description="Camera associated with this export job") + name: str | None = Field( + default=None, + description="Friendly name for the export", + ) + export_case_id: str | None = Field( + default=None, + description="ID of the export case this export belongs to", + ) + request_start_time: float = Field(description="Requested export start time") + request_end_time: float = Field(description="Requested export end time") + start_time: float | None = Field( + default=None, + description="Unix timestamp when execution started", + ) + end_time: float | None = Field( + default=None, + description="Unix timestamp when execution completed", + ) + error_message: str | None = Field( + default=None, + description="Error message for failed jobs", + ) + results: dict[str, Any] | None = Field( + default=None, + description="Result metadata for completed jobs", + ) + current_step: str = Field( + default="queued", + description="Current execution step (queued, preparing, encoding, encoding_retry, finalizing)", + ) + progress_percent: float = Field( + default=0.0, + description="Progress percentage of the current step (0.0 - 100.0)", + ) + + +ExportJobsResponse = list[ExportJobModel] + + +ExportsResponse = list[ExportModel] diff --git a/frigate/api/defs/response/generic_response.py b/frigate/api/defs/response/generic_response.py new file mode 100644 index 0000000..dbf9434 --- /dev/null +++ b/frigate/api/defs/response/generic_response.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class GenericResponse(BaseModel): + success: bool + message: str diff --git a/frigate/api/defs/response/preview_response.py b/frigate/api/defs/response/preview_response.py new file mode 100644 index 0000000..70d2cac --- /dev/null +++ b/frigate/api/defs/response/preview_response.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel, Field + + +class PreviewModel(BaseModel): + """Model representing a single preview clip.""" + + camera: str = Field(description="Camera name for this preview") + src: str = Field(description="Path to the preview video file") + type: str = Field(description="MIME type of the preview video (video/mp4)") + start: float = Field(description="Unix timestamp when the preview starts") + end: float = Field(description="Unix timestamp when the preview ends") + + +PreviewsResponse = list[PreviewModel] +PreviewFramesResponse = list[str] diff --git a/frigate/api/defs/response/review_response.py b/frigate/api/defs/response/review_response.py new file mode 100644 index 0000000..a0b755b --- /dev/null +++ b/frigate/api/defs/response/review_response.py @@ -0,0 +1,42 @@ +from datetime import datetime + +from pydantic import BaseModel, Json + +from frigate.review.types import SeverityEnum + + +class ReviewSegmentResponse(BaseModel): + id: str + camera: str + start_time: datetime + end_time: datetime + has_been_reviewed: bool + severity: SeverityEnum + thumb_path: str + data: Json + + +class Last24HoursReview(BaseModel): + reviewed_alert: int + reviewed_detection: int + total_alert: int + total_detection: int + + +class DayReview(BaseModel): + day: datetime + reviewed_alert: int + reviewed_detection: int + total_alert: int + total_detection: int + + +class ReviewSummaryResponse(BaseModel): + last24Hours: Last24HoursReview + root: dict[str, DayReview] + + +class ReviewActivityMotionResponse(BaseModel): + start_time: int + motion: float + camera: str diff --git a/frigate/api/defs/tags.py b/frigate/api/defs/tags.py new file mode 100644 index 0000000..c6f37b6 --- /dev/null +++ b/frigate/api/defs/tags.py @@ -0,0 +1,18 @@ +from enum import Enum + + +class Tags(Enum): + app = "App" + auth = "Auth" + camera = "Camera" + chat = "Chat" + events = "Events" + export = "Export" + classification = "Classification" + logs = "Logs" + media = "Media" + motion_search = "Motion Search" + notifications = "Notifications" + preview = "Preview" + recordings = "Recordings" + review = "Review" diff --git a/frigate/api/event.py b/frigate/api/event.py new file mode 100644 index 0000000..d37a547 --- /dev/null +++ b/frigate/api/event.py @@ -0,0 +1,2291 @@ +"""Event apis.""" + +import asyncio +import base64 +import datetime +import json +import logging +import os +import random +import string +from functools import reduce +from pathlib import Path +from urllib.parse import unquote + +import numpy as np +from fastapi import APIRouter, Request +from fastapi.params import Depends +from fastapi.responses import JSONResponse +from pathvalidate import sanitize_filename +from peewee import JOIN, DoesNotExist, fn, operator +from playhouse.shortcuts import model_to_dict + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, + require_role, +) +from frigate.api.defs.query.events_query_parameters import ( + DEFAULT_TIME_RANGE, + EventsQueryParams, + EventsSearchQueryParams, + EventsSummaryQueryParams, +) +from frigate.api.defs.query.regenerate_query_parameters import ( + RegenerateQueryParameters, +) +from frigate.api.defs.request.events_body import ( + EventsAttributesBody, + EventsCreateBody, + EventsDeleteBody, + EventsDescriptionBody, + EventsEndBody, + EventsLPRBody, + EventsSubLabelBody, + SubmitPlusBody, + TriggerEmbeddingBody, +) +from frigate.api.defs.response.event_response import ( + EventCreateResponse, + EventMultiDeleteResponse, + EventResponse, + EventUploadPlusResponse, +) +from frigate.api.defs.response.generic_response import GenericResponse +from frigate.api.defs.tags import Tags +from frigate.comms.event_metadata_updater import EventMetadataTypeEnum +from frigate.config.classification import ObjectClassificationType +from frigate.const import CLIPS_DIR, TRIGGER_DIR +from frigate.embeddings import EmbeddingsContext +from frigate.models import Event, ReviewSegment, Timeline, Trigger +from frigate.track.object_processing import TrackedObject +from frigate.util.file import get_event_thumbnail_bytes, load_event_snapshot_image +from frigate.util.time import get_dst_transitions, get_tz_modifiers + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.events]) + + +def _build_attribute_filter_clause(attributes: str): + filtered_attributes = [ + attr.strip() for attr in attributes.split(",") if attr.strip() + ] + attribute_clauses = [] + + for attr in filtered_attributes: + attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*') + + escaped_attr = json.dumps(attr, ensure_ascii=True)[1:-1] + if escaped_attr != attr: + attribute_clauses.append(Event.data.cast("text") % f'*:"{escaped_attr}"*') + + if not attribute_clauses: + return None + + return reduce(operator.or_, attribute_clauses) + + +@router.get( + "/events", + response_model=list[EventResponse], + dependencies=[Depends(allow_any_authenticated())], + summary="Get events", + description="Returns a list of events.", +) +def events( + params: EventsQueryParams = Depends(), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + camera = params.camera + cameras = params.cameras + + # handle old camera arg + if cameras == "all" and camera != "all": + cameras = camera + + label = unquote(params.label) + labels = params.labels + + # handle old label arg + if labels == "all" and label != "all": + labels = label + + sub_label = params.sub_label + sub_labels = params.sub_labels + + # handle old sub_label arg + if sub_labels == "all" and sub_label != "all": + sub_labels = sub_label + + attributes = unquote(params.attributes) + + zone = params.zone + zones = params.zones + + # handle old label arg + if zones == "all" and zone != "all": + zones = zone + + limit = params.limit + after = params.after + before = params.before + time_range = params.time_range + has_clip = params.has_clip + has_snapshot = params.has_snapshot + in_progress = params.in_progress + include_thumbnails = params.include_thumbnails + favorites = params.favorites + min_score = params.min_score + max_score = params.max_score + min_speed = params.min_speed + max_speed = params.max_speed + is_submitted = params.is_submitted + min_length = params.min_length + max_length = params.max_length + event_id = params.event_id + recognized_license_plate = params.recognized_license_plate + + sort = params.sort + + clauses = [] + + selected_columns = [ + Event.id, + Event.camera, + Event.label, + Event.zones, + Event.start_time, + Event.end_time, + Event.has_clip, + Event.has_snapshot, + Event.plus_id, + Event.retain_indefinitely, + Event.sub_label, + Event.top_score, + Event.false_positive, + Event.box, + Event.data, + ] + + if camera != "all": + clauses.append(Event.camera == camera) + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + camera_list = list(filtered) + else: + camera_list = allowed_cameras + clauses.append(Event.camera << camera_list) + + if labels != "all": + label_list = labels.split(",") + clauses.append(Event.label << label_list) + + if sub_labels != "all": + # use matching so joined sub labels are included + # for example a sub label 'bob' would get events + # with sub labels 'bob' and 'bob, john' + sub_label_clauses = [] + filtered_sub_labels = sub_labels.split(",") + + if "None" in filtered_sub_labels: + filtered_sub_labels.remove("None") + sub_label_clauses.append(Event.sub_label.is_null()) + + for label in filtered_sub_labels: + lowered = label.lower() + sub_label_clauses.append( + fn.LOWER(Event.sub_label.cast("text")) == lowered + ) # include exact matches (case-insensitive) + + # include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII) + sub_label_clauses.append( + fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*" + ) + sub_label_clauses.append( + fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*" + ) + + sub_label_clause = reduce(operator.or_, sub_label_clauses) + clauses.append(sub_label_clause) + + if attributes != "all": + # Custom classification results are stored as data[model_name] = result_value + attribute_clause = _build_attribute_filter_clause(attributes) + if attribute_clause is not None: + clauses.append(attribute_clause) + + if recognized_license_plate != "all": + filtered_recognized_license_plates = recognized_license_plate.split(",") + + clauses_for_plates = [] + + if "None" in filtered_recognized_license_plates: + filtered_recognized_license_plates.remove("None") + clauses_for_plates.append(Event.data["recognized_license_plate"].is_null()) + + # regex vs exact matching + normal_plates = [] + for plate in filtered_recognized_license_plates: + if plate.startswith("^") or any(ch in plate for ch in ".[]?+*"): + clauses_for_plates.append( + Event.data["recognized_license_plate"].cast("text").regexp(plate) + ) + else: + normal_plates.append(plate) + + # if there are any plain string plates, match them with IN + if normal_plates: + clauses_for_plates.append( + Event.data["recognized_license_plate"].cast("text").in_(normal_plates) + ) + + recognized_license_plate_clause = reduce(operator.or_, clauses_for_plates) + clauses.append(recognized_license_plate_clause) + + if zones != "all": + # use matching so events with multiple zones + # still match on a search where any zone matches + zone_clauses = [] + filtered_zones = zones.split(",") + + if "None" in filtered_zones: + filtered_zones.remove("None") + zone_clauses.append(Event.zones.length() == 0) + + for zone in filtered_zones: + zone_clauses.append(Event.zones.cast("text") % f'*"{zone}"*') + + zone_clause = reduce(operator.or_, zone_clauses) + clauses.append(zone_clause) + + if after: + clauses.append(Event.start_time > after) + + if before: + clauses.append(Event.start_time < before) + + if time_range != DEFAULT_TIME_RANGE: + # get timezone arg to ensure browser times are used + tz_name = params.timezone + hour_modifier, minute_modifier, _ = get_tz_modifiers(tz_name) + + times = time_range.split(",") + time_after = times[0] + time_before = times[1] + + start_hour_fun = fn.strftime( + "%H:%M", + fn.datetime(Event.start_time, "unixepoch", hour_modifier, minute_modifier), + ) + + # cases where user wants events overnight, ex: from 20:00 to 06:00 + # should use or operator + if time_after > time_before: + clauses.append( + reduce( + operator.or_, + [(start_hour_fun > time_after), (start_hour_fun < time_before)], + ) + ) + # all other cases should be and operator + else: + clauses.append(start_hour_fun > time_after) + clauses.append(start_hour_fun < time_before) + + if has_clip is not None: + clauses.append(Event.has_clip == has_clip) + + if has_snapshot is not None: + clauses.append(Event.has_snapshot == has_snapshot) + + if in_progress is not None: + clauses.append(Event.end_time.is_null(in_progress)) + + if include_thumbnails: + selected_columns.append(Event.thumbnail) + + if favorites: + clauses.append(Event.retain_indefinitely == favorites) + + if max_score is not None: + clauses.append(Event.data["score"] <= max_score) + + if min_score is not None: + clauses.append(Event.data["score"] >= min_score) + + if max_speed is not None: + clauses.append(Event.data["average_estimated_speed"] <= max_speed) + + if min_speed is not None: + clauses.append(Event.data["average_estimated_speed"] >= min_speed) + + if min_length is not None: + clauses.append((Event.end_time - Event.start_time) >= min_length) + + if max_length is not None: + clauses.append((Event.end_time - Event.start_time) <= max_length) + + if is_submitted is not None: + if is_submitted == 0: + clauses.append(Event.plus_id.is_null()) + elif is_submitted > 0: + clauses.append(Event.plus_id != "") + + if event_id is not None: + clauses.append(Event.id == event_id) + + if len(clauses) == 0: + clauses.append(True) + + if sort: + if sort == "score_asc": + order_by = Event.data["score"].asc() + elif sort == "score_desc": + order_by = Event.data["score"].desc() + elif sort == "speed_asc": + order_by = Event.data["average_estimated_speed"].asc() + elif sort == "speed_desc": + order_by = Event.data["average_estimated_speed"].desc() + elif sort == "date_asc": + order_by = Event.start_time.asc() + elif sort == "date_desc": + order_by = Event.start_time.desc() + else: + order_by = Event.start_time.desc() + else: + order_by = Event.start_time.desc() + + events = ( + Event.select(*selected_columns) + .where(reduce(operator.and_, clauses)) + .order_by(order_by) + .limit(limit) + .dicts() + .iterator() + ) + + return JSONResponse(content=list(events)) + + +@router.get( + "/events/explore", + response_model=list[EventResponse], + dependencies=[Depends(allow_any_authenticated())], + summary="Get summary of objects", + description="""Gets a summary of objects from the database. + Returns a list of objects with a max of `limit` objects for each label. + """, +) +def events_explore( + limit: int = 10, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + # get distinct labels for all events + distinct_labels = ( + Event.select(Event.label) + .where(Event.camera << allowed_cameras) + .distinct() + .order_by(Event.label) + ) + + label_counts = {} + + def event_generator(): + for label_obj in distinct_labels.iterator(): + label = label_obj.label + + # get most recent events for this label + label_events = ( + Event.select() + .where((Event.label == label) & (Event.camera << allowed_cameras)) + .order_by(Event.start_time.desc()) + .limit(limit) + .iterator() + ) + + # count total events for this label + label_counts[label] = ( + Event.select() + .where((Event.label == label) & (Event.camera << allowed_cameras)) + .count() + ) + + yield from label_events + + def process_events(): + for event in event_generator(): + processed_event = { + "id": event.id, + "camera": event.camera, + "label": event.label, + "zones": event.zones, + "start_time": event.start_time, + "end_time": event.end_time, + "has_clip": event.has_clip, + "has_snapshot": event.has_snapshot, + "plus_id": event.plus_id, + "retain_indefinitely": event.retain_indefinitely, + "sub_label": event.sub_label, + "top_score": event.top_score, + "false_positive": event.false_positive, + "box": event.box, + "data": { + k: v + for k, v in event.data.items() + if k + in [ + "type", + "score", + "top_score", + "description", + "sub_label_score", + "average_estimated_speed", + "velocity_angle", + "path_data", + "recognized_license_plate", + "recognized_license_plate_score", + ] + }, + "event_count": label_counts[event.label], + } + yield processed_event + + # convert iterator to list and sort + processed_events = sorted( + process_events(), + key=lambda x: (x["event_count"], x["start_time"]), + reverse=True, + ) + + return JSONResponse(content=processed_events) + + +@router.get( + "/event_ids", + response_model=list[EventResponse], + dependencies=[Depends(allow_any_authenticated())], + summary="Get events by ids", + description="""Gets events by a list of ids. + Returns a list of events. + """, +) +async def event_ids(ids: str, request: Request): + ids = ids.split(",") + + if not ids: + return JSONResponse( + content=({"success": False, "message": "Valid list of ids must be sent"}), + status_code=400, + ) + + for event_id in ids: + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + # we should not fail the entire request if an event is not found + continue + + try: + events = Event.select().where(Event.id << ids).dicts().iterator() + return JSONResponse(list(events)) + except Exception: + return JSONResponse( + content=({"success": False, "message": "Events not found"}), status_code=400 + ) + + +@router.get( + "/events/search", + dependencies=[Depends(allow_any_authenticated())], + summary="Search events", + description="""Searches for events in the database. + Returns a list of events. + """, +) +def events_search( + request: Request, + params: EventsSearchQueryParams = Depends(), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + query = params.query + search_type = params.search_type + include_thumbnails = params.include_thumbnails + limit = params.limit + sort = params.sort + + # Filters + cameras = params.cameras + labels = params.labels + sub_labels = params.sub_labels + attributes = unquote(params.attributes) + zones = params.zones + after = params.after + before = params.before + min_score = params.min_score + max_score = params.max_score + min_speed = params.min_speed + max_speed = params.max_speed + time_range = params.time_range + has_clip = params.has_clip + has_snapshot = params.has_snapshot + is_submitted = params.is_submitted + recognized_license_plate = params.recognized_license_plate + + # for similarity search + event_id = params.event_id + + if not query and not event_id: + return JSONResponse( + content=( + { + "success": False, + "message": "A search query must be supplied", + } + ), + status_code=400, + ) + + if not request.app.frigate_config.semantic_search.enabled: + return JSONResponse( + content=( + { + "success": False, + "message": "Semantic search is not enabled", + } + ), + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + + selected_columns = [ + Event.id, + Event.camera, + Event.label, + Event.sub_label, + Event.zones, + Event.start_time, + Event.end_time, + Event.has_clip, + Event.has_snapshot, + Event.top_score, + Event.data, + Event.plus_id, + ReviewSegment.thumb_path, + ] + + if include_thumbnails: + selected_columns.append(Event.thumbnail) + + # Build the initial SQLite query filters + event_filters = [] + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + event_filters.append(Event.camera << list(filtered)) + else: + event_filters.append(Event.camera << allowed_cameras) + + if labels != "all": + event_filters.append(Event.label << labels.split(",")) + + if sub_labels != "all": + # use matching so joined sub labels are included + # for example a sub label 'bob' would get events + # with sub labels 'bob' and 'bob, john' + sub_label_clauses = [] + filtered_sub_labels = sub_labels.split(",") + + if "None" in filtered_sub_labels: + filtered_sub_labels.remove("None") + sub_label_clauses.append(Event.sub_label.is_null()) + + for label in filtered_sub_labels: + lowered = label.lower() + sub_label_clauses.append( + fn.LOWER(Event.sub_label.cast("text")) == lowered + ) # include exact matches (case-insensitive) + + # include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII) + sub_label_clauses.append( + fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*" + ) + sub_label_clauses.append( + fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*" + ) + + event_filters.append(reduce(operator.or_, sub_label_clauses)) + + if attributes != "all": + # Custom classification results are stored as data[model_name] = result_value + attribute_clause = _build_attribute_filter_clause(attributes) + if attribute_clause is not None: + event_filters.append(attribute_clause) + + if zones != "all": + zone_clauses = [] + filtered_zones = zones.split(",") + + if "None" in filtered_zones: + filtered_zones.remove("None") + zone_clauses.append(Event.zones.length() == 0) + + for zone in filtered_zones: + zone_clauses.append(Event.zones.cast("text") % f'*"{zone}"*') + + event_filters.append(reduce(operator.or_, zone_clauses)) + + if recognized_license_plate != "all": + filtered_recognized_license_plates = recognized_license_plate.split(",") + + clauses_for_plates = [] + + if "None" in filtered_recognized_license_plates: + filtered_recognized_license_plates.remove("None") + clauses_for_plates.append(Event.data["recognized_license_plate"].is_null()) + + # regex vs exact matching + normal_plates = [] + for plate in filtered_recognized_license_plates: + if plate.startswith("^") or any(ch in plate for ch in ".[]?+*"): + clauses_for_plates.append( + Event.data["recognized_license_plate"].cast("text").regexp(plate) + ) + else: + normal_plates.append(plate) + + # if there are any plain string plates, match them with IN + if normal_plates: + clauses_for_plates.append( + Event.data["recognized_license_plate"].cast("text").in_(normal_plates) + ) + + recognized_license_plate_clause = reduce(operator.or_, clauses_for_plates) + event_filters.append(recognized_license_plate_clause) + + if after: + event_filters.append(Event.start_time > after) + + if before: + event_filters.append(Event.start_time < before) + + if has_clip is not None: + event_filters.append(Event.has_clip == has_clip) + + if has_snapshot is not None: + event_filters.append(Event.has_snapshot == has_snapshot) + + if is_submitted is not None: + if is_submitted == 0: + event_filters.append(Event.plus_id.is_null()) + elif is_submitted > 0: + event_filters.append(Event.plus_id != "") + + if min_score is not None and max_score is not None: + event_filters.append(Event.data["score"].between(min_score, max_score)) + else: + if min_score is not None: + event_filters.append(Event.data["score"] >= min_score) + if max_score is not None: + event_filters.append(Event.data["score"] <= max_score) + + if min_speed is not None and max_speed is not None: + event_filters.append( + Event.data["average_estimated_speed"].between(min_speed, max_speed) + ) + else: + if min_speed is not None: + event_filters.append(Event.data["average_estimated_speed"] >= min_speed) + if max_speed is not None: + event_filters.append(Event.data["average_estimated_speed"] <= max_speed) + + if time_range != DEFAULT_TIME_RANGE: + tz_name = params.timezone + hour_modifier, minute_modifier, _ = get_tz_modifiers(tz_name) + + times = time_range.split(",") + time_after, time_before = times + + start_hour_fun = fn.strftime( + "%H:%M", + fn.datetime(Event.start_time, "unixepoch", hour_modifier, minute_modifier), + ) + + # cases where user wants events overnight, ex: from 20:00 to 06:00 + # should use or operator + if time_after > time_before: + event_filters.append( + reduce( + operator.or_, + [(start_hour_fun > time_after), (start_hour_fun < time_before)], + ) + ) + # all other cases should be and operator + else: + event_filters.append(start_hour_fun > time_after) + event_filters.append(start_hour_fun < time_before) + + # Perform semantic search + search_results = {} + if search_type == "similarity": + try: + search_event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content={ + "success": False, + "message": "Event not found", + }, + status_code=404, + ) + + if search_event.camera not in allowed_cameras: + return JSONResponse( + content={ + "success": False, + "message": "Event not found", + }, + status_code=404, + ) + + thumb_result = context.search_thumbnail(search_event) + thumb_ids = {result[0]: result[1] for result in thumb_result} + search_results = { + event_id: {"distance": distance, "source": "thumbnail"} + for event_id, distance in thumb_ids.items() + } + else: + search_types = search_type.split(",") + + # only save stats for multi-modal searches + save_stats = "thumbnail" in search_types and "description" in search_types + + if "thumbnail" in search_types: + thumb_result = context.search_thumbnail(query) + + thumb_distances = context.thumb_stats.normalize( + [result[1] for result in thumb_result], save_stats + ) + + thumb_ids = dict( + zip([result[0] for result in thumb_result], thumb_distances) + ) + search_results.update( + { + event_id: {"distance": distance, "source": "thumbnail"} + for event_id, distance in thumb_ids.items() + } + ) + + if "description" in search_types: + desc_result = context.search_description(query) + + desc_distances = context.desc_stats.normalize( + [result[1] for result in desc_result], save_stats + ) + + desc_ids = dict(zip([result[0] for result in desc_result], desc_distances)) + + for event_id, distance in desc_ids.items(): + if ( + event_id not in search_results + or distance < search_results[event_id]["distance"] + ): + search_results[event_id] = { + "distance": distance, + "source": "description", + } + + if not search_results: + return JSONResponse(content=[]) + + # Fetch events in a single query + events_query = Event.select(*selected_columns).join( + ReviewSegment, + JOIN.LEFT_OUTER, + on=(fn.json_extract(ReviewSegment.data, "$.detections").contains(Event.id)), + ) + + # Apply filters, if any + if event_filters: + events_query = events_query.where(reduce(operator.and_, event_filters)) + + # If we did a similarity search, limit events to those in search_results + if search_results: + events_query = events_query.where(Event.id << list(search_results.keys())) + + # Fetch events and process them in a single pass + processed_events = [] + for event in events_query.dicts(): + processed_event = {k: v for k, v in event.items() if k != "data"} + processed_event["data"] = { + k: v + for k, v in event["data"].items() + if k + in [ + "attributes", + "type", + "score", + "top_score", + "description", + "sub_label_score", + "average_estimated_speed", + "velocity_angle", + "path_data", + "recognized_license_plate", + "recognized_license_plate_score", + ] + } + + if event["id"] in search_results: + processed_event["search_distance"] = search_results[event["id"]]["distance"] + processed_event["search_source"] = search_results[event["id"]]["source"] + + processed_events.append(processed_event) + + if (sort is None or sort == "relevance") and search_results: + processed_events.sort(key=lambda x: x.get("search_distance", float("inf"))) + elif sort == "score_asc": + processed_events.sort(key=lambda x: x["data"]["score"]) + elif sort == "score_desc": + processed_events.sort(key=lambda x: x["data"]["score"], reverse=True) + elif sort == "speed_asc": + processed_events.sort( + key=lambda x: ( + x["data"].get("average_estimated_speed") is None, + x["data"].get("average_estimated_speed"), + ) + ) + elif sort == "speed_desc": + processed_events.sort( + key=lambda x: ( + x["data"].get("average_estimated_speed") is None, + x["data"].get("average_estimated_speed", float("-inf")), + ), + reverse=True, + ) + elif sort == "date_asc": + processed_events.sort(key=lambda x: x["start_time"]) + else: + # "date_desc" default + processed_events.sort(key=lambda x: x["start_time"], reverse=True) + + # Limit the number of events returned + processed_events = processed_events[:limit] + + return JSONResponse(content=processed_events) + + +@router.get("/events/summary", dependencies=[Depends(allow_any_authenticated())]) +def events_summary( + params: EventsSummaryQueryParams = Depends(), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + tz_name = params.timezone + has_clip = params.has_clip + has_snapshot = params.has_snapshot + + clauses = [] + + if has_clip is not None: + clauses.append(Event.has_clip == has_clip) + + if has_snapshot is not None: + clauses.append(Event.has_snapshot == has_snapshot) + + if len(clauses) == 0: + clauses.append(True) + + time_range_query = ( + Event.select( + fn.MIN(Event.start_time).alias("min_time"), + fn.MAX(Event.start_time).alias("max_time"), + ) + .where(reduce(operator.and_, clauses) & (Event.camera << allowed_cameras)) + .dicts() + .get() + ) + + min_time = time_range_query.get("min_time") + max_time = time_range_query.get("max_time") + + if min_time is None or max_time is None: + return JSONResponse(content=[]) + + dst_periods = get_dst_transitions(tz_name, min_time, max_time) + + grouped: dict[tuple, dict] = {} + + for period_start, period_end, period_offset in dst_periods: + hours_offset = int(period_offset / 60 / 60) + minutes_offset = int(period_offset / 60 - hours_offset * 60) + period_hour_modifier = f"{hours_offset} hour" + period_minute_modifier = f"{minutes_offset} minute" + + period_groups = ( + Event.select( + Event.camera, + Event.label, + Event.sub_label, + Event.data, + fn.strftime( + "%Y-%m-%d", + fn.datetime( + Event.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ).alias("day"), + Event.zones, + fn.COUNT(Event.id).alias("count"), + ) + .where( + reduce(operator.and_, clauses) + & (Event.camera << allowed_cameras) + & (Event.start_time >= period_start) + & (Event.start_time <= period_end) + ) + .group_by( + Event.camera, + Event.label, + Event.sub_label, + Event.data, + (Event.start_time + period_offset).cast("int") / (3600 * 24), + Event.zones, + ) + .namedtuples() + ) + + for g in period_groups: + key = ( + g.camera, + g.label, + g.sub_label, + json.dumps(g.data, sort_keys=True) if g.data is not None else None, + g.day, + json.dumps(g.zones, sort_keys=True) if g.zones is not None else None, + ) + + if key in grouped: + grouped[key]["count"] += int(g.count or 0) + else: + grouped[key] = { + "camera": g.camera, + "label": g.label, + "sub_label": g.sub_label, + "data": g.data, + "day": g.day, + "zones": g.zones, + "count": int(g.count or 0), + } + + return JSONResponse(content=sorted(grouped.values(), key=lambda x: x["day"])) + + +@router.get( + "/events/{event_id}", + response_model=EventResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Get event by id", + description="Gets an event by its id.", +) +async def event(event_id: str, request: Request): + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + return model_to_dict(event) + except DoesNotExist: + return JSONResponse(content="Event not found", status_code=404) + + +@router.post( + "/events/{event_id}/retain", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Set event retain indefinitely.", + description="""Sets an event to retain indefinitely. + Returns a success message or an error if the event is not found. + NOTE: This is a legacy endpoint and is not supported in the frontend. + """, +) +def set_retain(event_id: str): + try: + event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Event " + event_id + " not found"}), + status_code=404, + ) + + event.retain_indefinitely = True + event.save() + + return JSONResponse( + content=({"success": True, "message": "Event " + event_id + " retained"}), + status_code=200, + ) + + +@router.post( + "/events/{event_id}/plus", + response_model=EventUploadPlusResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Send event to Frigate+", + description="""Sends an event to Frigate+. + Returns a success message or an error if the event is not found. + """, +) +async def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None): + if not request.app.frigate_config.plus_api.is_active(): + message = "PLUS_API_KEY environment variable is not set" + logger.error(message) + return JSONResponse( + content=( + { + "success": False, + "message": message, + } + ), + status_code=400, + ) + + include_annotation = body.include_annotation if body is not None else None + + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + message = f"Event {event_id} not found" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=404 + ) + + # events from before the conversion to relative dimensions cant include annotations + if event.data.get("box") is None: + include_annotation = None + + if event.end_time is None: + logger.error(f"Unable to load clean snapshot for in-progress event: {event.id}") + return JSONResponse( + content=( + { + "success": False, + "message": "Unable to load clean snapshot for in-progress event", + } + ), + status_code=400, + ) + + if event.plus_id: + message = "Already submitted to plus" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=400 + ) + + try: + image, is_clean_snapshot = load_event_snapshot_image(event, clean_only=True) + except Exception: + logger.error(f"Unable to load clean snapshot for event: {event.id}") + return JSONResponse( + content=( + {"success": False, "message": "Unable to load clean snapshot for event"} + ), + status_code=400, + ) + + if not is_clean_snapshot or image is None or image.size == 0: + logger.error(f"Unable to find clean snapshot for event: {event.id}") + return JSONResponse( + content=( + { + "success": False, + "message": "Unable to find clean snapshot for event", + } + ), + status_code=400, + ) + + try: + plus_id = await asyncio.to_thread( + request.app.frigate_config.plus_api.upload_image, image, event.camera + ) + except Exception as ex: + logger.exception(ex) + return JSONResponse( + content=({"success": False, "message": "Error uploading image"}), + status_code=400, + ) + + # store image id in the database + event.plus_id = plus_id + event.save() + + if include_annotation is not None: + box = event.data["box"] + + try: + await asyncio.to_thread( + request.app.frigate_config.plus_api.add_annotation, + event.plus_id, + box, + event.label, + ) + except ValueError: + message = "Error uploading annotation, unsupported label provided." + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), + status_code=400, + ) + except Exception as ex: + logger.exception(ex) + return JSONResponse( + content=({"success": False, "message": "Error uploading annotation"}), + status_code=400, + ) + + return JSONResponse( + content=({"success": True, "plus_id": plus_id}), status_code=200 + ) + + +@router.put( + "/events/{event_id}/false_positive", + response_model=EventUploadPlusResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Submit false positive to Frigate+", + description="""Submit an event as a false positive to Frigate+. + This endpoint is the same as the standard Frigate+ submission endpoint, + but is specifically for marking an event as a false positive.""", +) +async def false_positive(request: Request, event_id: str): + if not request.app.frigate_config.plus_api.is_active(): + message = "PLUS_API_KEY environment variable is not set" + logger.error(message) + return JSONResponse( + content=( + { + "success": False, + "message": message, + } + ), + status_code=400, + ) + + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + message = f"Event {event_id} not found" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=404 + ) + + # events from before the conversion to relative dimensions cant include annotations + if event.data.get("box") is None: + message = "Events prior to 0.13 cannot be submitted as false positives" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=400 + ) + + if event.false_positive: + message = "False positive already submitted to Frigate+" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=400 + ) + + if not event.plus_id: + plus_response = await send_to_plus(request, event_id) + if plus_response.status_code != 200: + return plus_response + # need to refetch the event now that it has a plus_id + event = Event.get(Event.id == event_id) + + region = event.data["region"] + box = event.data["box"] + + # provide top score if score is unavailable + score = ( + (event.data["top_score"] if event.data["top_score"] else event.top_score) + if event.data["score"] is None + else event.data["score"] + ) + + try: + await asyncio.to_thread( + request.app.frigate_config.plus_api.add_false_positive, + event.plus_id, + region, + box, + score, + event.label, + event.model_hash, + event.model_type, + event.detector_type, + ) + except ValueError: + message = "Error uploading false positive, unsupported label provided." + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), + status_code=400, + ) + except Exception as ex: + logger.exception(ex) + return JSONResponse( + content=({"success": False, "message": "Error uploading false positive"}), + status_code=400, + ) + + event.false_positive = True + event.save() + + return JSONResponse( + content=({"success": True, "plus_id": event.plus_id}), status_code=200 + ) + + +@router.delete( + "/events/{event_id}/retain", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Stop event from being retained indefinitely", + description="""Stops an event from being retained indefinitely. + Returns a success message or an error if the event is not found. + NOTE: This is a legacy endpoint and is not supported in the frontend. + """, +) +async def delete_retain(event_id: str, request: Request): + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Event " + event_id + " not found"}), + status_code=404, + ) + + event.retain_indefinitely = False + event.save() + + return JSONResponse( + content=({"success": True, "message": "Event " + event_id + " un-retained"}), + status_code=200, + ) + + +@router.post( + "/events/{event_id}/sub_label", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Set event sub label", + description="""Sets an event's sub label. + Returns a success message or an error if the event is not found. + """, +) +async def set_sub_label( + request: Request, + event_id: str, + body: EventsSubLabelBody, +): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + event = None + + if request.app.detected_frames_processor: + tracked_obj: TrackedObject = None + + for state in request.app.detected_frames_processor.camera_states.values(): + tracked_obj = state.tracked_objects.get(event_id) + + if tracked_obj is not None: + break + else: + tracked_obj = None + + if not event and not tracked_obj: + return JSONResponse( + content=( + {"success": False, "message": "Event " + event_id + " not found."} + ), + status_code=404, + ) + + new_sub_label = body.subLabel + new_score = body.subLabelScore + + if new_sub_label == "": + new_sub_label = None + new_score = None + + request.app.event_metadata_updater.publish( + (event_id, new_sub_label, new_score), EventMetadataTypeEnum.sub_label.value + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Event {event_id} sub label set to {new_sub_label if new_sub_label is not None else 'None'}", + }, + status_code=200, + ) + + +@router.post( + "/events/{event_id}/recognized_license_plate", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Set event license plate", + description="""Sets an event's license plate. + Returns a success message or an error if the event is not found. + """, +) +async def set_plate( + request: Request, + event_id: str, + body: EventsLPRBody, +): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + event = None + + if request.app.detected_frames_processor: + tracked_obj: TrackedObject = None + + for state in request.app.detected_frames_processor.camera_states.values(): + tracked_obj = state.tracked_objects.get(event_id) + + if tracked_obj is not None: + break + else: + tracked_obj = None + + if not event and not tracked_obj: + return JSONResponse( + content=( + {"success": False, "message": "Event " + event_id + " not found."} + ), + status_code=404, + ) + + new_plate = body.recognizedLicensePlate + new_score = body.recognizedLicensePlateScore + + if new_plate == "": + new_plate = None + new_score = None + + request.app.event_metadata_updater.publish( + (event_id, "recognized_license_plate", new_plate, new_score), + EventMetadataTypeEnum.attribute.value, + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Event {event_id} license plate set to {new_plate if new_plate is not None else 'None'}", + }, + status_code=200, + ) + + +@router.post( + "/events/{event_id}/attributes", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Set custom classification attributes", + description=( + "Sets an event's custom classification attributes for all attribute-type " + "models that apply to the event's object type." + ), +) +async def set_attributes( + request: Request, + event_id: str, + body: EventsAttributesBody, +): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": f"Event {event_id} not found."}), + status_code=404, + ) + + object_type = event.label + selected_attributes = set(body.attributes or []) + applied_updates: list[dict[str, str | float | None]] = [] + + for ( + model_key, + model_config, + ) in request.app.frigate_config.classification.custom.items(): + # Only apply to enabled attribute classifiers that target this object type + if ( + not model_config.enabled + or not model_config.object_config + or model_config.object_config.classification_type + != ObjectClassificationType.attribute + or object_type not in (model_config.object_config.objects or []) + ): + continue + + # Get available labels from dataset directory + dataset_dir = os.path.join(CLIPS_DIR, sanitize_filename(model_key), "dataset") + available_labels = set() + + if os.path.exists(dataset_dir): + for category_name in os.listdir(dataset_dir): + category_dir = os.path.join(dataset_dir, category_name) + if os.path.isdir(category_dir): + available_labels.add(category_name) + + if not available_labels: + logger.warning( + "No dataset found for custom attribute model %s at %s", + model_key, + dataset_dir, + ) + continue + + # Find all selected attributes that apply to this model + model_name = model_config.name or model_key + matching_attrs = selected_attributes & available_labels + + if matching_attrs: + # Publish updates for each selected attribute + for attr in matching_attrs: + request.app.event_metadata_updater.publish( + (event_id, model_name, attr, 1.0), + EventMetadataTypeEnum.attribute.value, + ) + applied_updates.append( + {"model": model_name, "label": attr, "score": 1.0} + ) + else: + # Clear this model's attribute + request.app.event_metadata_updater.publish( + (event_id, model_name, None, None), + EventMetadataTypeEnum.attribute.value, + ) + applied_updates.append({"model": model_name, "label": None, "score": None}) + + if len(applied_updates) == 0: + return JSONResponse( + content={ + "success": False, + "message": "No matching attributes found for this object type.", + }, + status_code=400, + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Updated {len(applied_updates)} attribute(s)", + "applied": applied_updates, + }, + status_code=200, + ) + + +@router.post( + "/events/{event_id}/description", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Set event description", + description="""Sets an event's description. + Returns a success message or an error if the event is not found. + """, +) +async def set_description( + request: Request, + event_id: str, + body: EventsDescriptionBody, +): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Event " + event_id + " not found"}), + status_code=404, + ) + + new_description = body.description + + event.data["description"] = new_description + event.save() + + # If semantic search is enabled, update the index + if request.app.frigate_config.semantic_search.enabled: + context: EmbeddingsContext = request.app.embeddings + if len(new_description) > 0: + context.update_description( + event_id, + new_description, + ) + else: + context.db.delete_embeddings_description(event_ids=[event_id]) + + response_message = ( + f"Event {event_id} description is now blank" + if new_description is None or len(new_description) == 0 + else f"Event {event_id} description set to {new_description}" + ) + + return JSONResponse( + content=( + { + "success": True, + "message": response_message, + } + ), + status_code=200, + ) + + +@router.put( + "/events/{event_id}/description/regenerate", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Regenerate event description", + description="""Regenerates an event's description. + Returns a success message or an error if the event is not found. + """, +) +async def regenerate_description( + request: Request, event_id: str, params: RegenerateQueryParameters = Depends() +): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Event " + event_id + " not found"}), + status_code=404, + ) + + camera_config = request.app.frigate_config.cameras[event.camera] + + if camera_config.objects.genai.enabled or params.force: + request.app.event_metadata_updater.publish( + (event.id, params.source, params.force), + EventMetadataTypeEnum.regenerate_description.value, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Event " + + event_id + + " description regeneration has been requested using " + + params.source, + } + ), + status_code=200, + ) + + return JSONResponse( + content=( + { + "success": False, + "message": "Semantic Search and Generative AI must be enabled to regenerate a description", + } + ), + status_code=400, + ) + + +@router.post( + "/description/generate", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Generate description embedding", + description="""Generates an embedding for an event's description. + Returns a success message or an error if the event is not found. + """, +) +def generate_description_embedding( + request: Request, + body: EventsDescriptionBody, +): + new_description = body.description + + # If semantic search is enabled, update the index + if request.app.frigate_config.semantic_search.enabled: + context: EmbeddingsContext = request.app.embeddings + if len(new_description) > 0: + result = context.generate_description_embedding( + new_description, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": f"Embedding for description is {result}" + if result + else "Failed to generate embedding", + } + ), + status_code=200, + ) + + +async def delete_single_event(event_id: str, request: Request) -> dict: + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + return {"success": False, "message": f"Event {event_id} not found"} + + media_name = f"{event.camera}-{event.id}" + if event.has_snapshot: + snapshot_paths = [ + Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg"), + Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"), + Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.webp"), + ] + for media in snapshot_paths: + media.unlink(missing_ok=True) + + event.delete_instance() + Timeline.delete().where(Timeline.source_id == event_id).execute() + + # If semantic search is enabled, update the index + if request.app.frigate_config.semantic_search.enabled: + context: EmbeddingsContext = request.app.embeddings + context.db.delete_embeddings_thumbnail(event_ids=[event_id]) + context.db.delete_embeddings_description(event_ids=[event_id]) + + return {"success": True, "message": f"Event {event_id} deleted"} + + +@router.delete( + "/events/{event_id}", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete event", + description="""Deletes an event from the database. + Returns a success message or an error if the event is not found. + """, +) +async def delete_event(request: Request, event_id: str): + result = await delete_single_event(event_id, request) + status_code = 200 if result["success"] else 404 + return JSONResponse(content=result, status_code=status_code) + + +@router.delete( + "/events/", + response_model=EventMultiDeleteResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete events", + description="""Deletes a list of events from the database. + Returns a success message or an error if the events are not found. + """, +) +async def delete_events(request: Request, body: EventsDeleteBody): + if not body.event_ids: + return JSONResponse( + content=({"success": False, "message": "No event IDs provided."}), + status_code=404, + ) + + deleted_events = [] + not_found_events = [] + + for event_id in body.event_ids: + result = await delete_single_event(event_id, request) + if result["success"]: + deleted_events.append(event_id) + else: + not_found_events.append(event_id) + + response = { + "success": True, + "deleted_events": deleted_events, + "not_found_events": not_found_events, + } + return JSONResponse(content=response, status_code=200) + + +@router.post( + "/events/{camera_name}/{label}/create", + response_model=EventCreateResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Create manual event", + description="""Creates a manual event in the database. + Returns a success message or an error if the event is not found. + NOTES: + - Creating a manual event does not trigger an update to /events MQTT topic. + - If a duration is set to null, the event will need to be ended manually by calling /events/{event_id}/end. + """, +) +def create_event( + request: Request, + camera_name: str, + label: str, + body: EventsCreateBody = EventsCreateBody(), +): + if not camera_name or not request.app.frigate_config.cameras.get(camera_name): + return JSONResponse( + content=( + {"success": False, "message": f"{camera_name} is not a valid camera."} + ), + status_code=404, + ) + + if not label: + return JSONResponse( + content=({"success": False, "message": f"{label} must be set."}), + status_code=404, + ) + + now = datetime.datetime.now().timestamp() + rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + event_id = f"{now}-{rand_id}" + + request.app.event_metadata_updater.publish( + ( + now, + camera_name, + label, + event_id, + body.include_recording, + body.score, + body.sub_label, + body.duration, + "api", + body.draw, + body.pre_capture, + ), + EventMetadataTypeEnum.manual_event_create.value, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Successfully created event.", + "event_id": event_id, + } + ), + status_code=200, + ) + + +@router.put( + "/events/{event_id}/end", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="End manual event", + description="""Ends a manual event. + Returns a success message or an error if the event is not found. + NOTE: This should only be used for manual events. + """, +) +async def end_event(request: Request, event_id: str, body: EventsEndBody): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + + if body.end_time is not None and body.end_time < event.start_time: + return JSONResponse( + content=( + { + "success": False, + "message": f"end_time ({body.end_time}) cannot be before start_time ({event.start_time}).", + } + ), + status_code=400, + ) + + end_time = body.end_time or datetime.datetime.now().timestamp() + request.app.event_metadata_updater.publish( + (event_id, end_time), EventMetadataTypeEnum.manual_event_end.value + ) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": f"Event {event_id} not found."}), + status_code=404, + ) + except Exception: + return JSONResponse( + content=( + {"success": False, "message": f"{event_id} must be set and valid."} + ), + status_code=404, + ) + + return JSONResponse( + content=({"success": True, "message": "Event successfully ended."}), + status_code=200, + ) + + +@router.post( + "/trigger/embedding", + response_model=dict, + dependencies=[Depends(require_role(["admin"]))], + summary="Create trigger embedding", + description="""Creates a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + """, +) +def create_trigger_embedding( + request: Request, + body: TriggerEmbeddingBody, + camera_name: str, + name: str, +): + try: + if not request.app.frigate_config.semantic_search.enabled: + return JSONResponse( + content={ + "success": False, + "message": "Semantic search is not enabled", + }, + status_code=400, + ) + + # Check if trigger already exists + if ( + Trigger.select() + .where(Trigger.camera == camera_name, Trigger.name == name) + .exists() + ): + return JSONResponse( + content={ + "success": False, + "message": f"Trigger {camera_name}:{name} already exists", + }, + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + # Generate embedding based on type + embedding = None + if body.type == "description": + embedding = context.generate_description_embedding(body.data) + elif body.type == "thumbnail": + try: + event: Event = Event.get(Event.id == body.data) + except DoesNotExist: + # TODO: check triggers directory for image + return JSONResponse( + content={ + "success": False, + "message": f"Failed to fetch event for {body.type} trigger", + }, + status_code=400, + ) + + # Skip the event if not an object + if event.data.get("type") != "object": + return + + # Get the thumbnail + thumbnail = get_event_thumbnail_bytes(event) + + if thumbnail is None: + return JSONResponse( + content={ + "success": False, + "message": f"Failed to get thumbnail for {body.data} for {body.type} trigger", + }, + status_code=400, + ) + + # Try to reuse existing embedding from database + cursor = context.db.execute_sql( + """ + SELECT thumbnail_embedding FROM vec_thumbnails WHERE id = ? + """, + [body.data], + ) + + row = cursor.fetchone() if cursor else None + + if row: + query_embedding = row[0] + embedding = np.frombuffer(query_embedding, dtype=np.float32) + else: + # Generate new embedding + embedding = context.generate_image_embedding( + body.data, (base64.b64encode(thumbnail).decode("ASCII")) + ) + + if embedding is None or ( + isinstance(embedding, (list, np.ndarray)) and len(embedding) == 0 + ): + return JSONResponse( + content={ + "success": False, + "message": f"Failed to generate embedding for {body.type} trigger", + }, + status_code=400, + ) + + if body.type == "thumbnail": + # Save image to the triggers directory + try: + os.makedirs( + os.path.join(TRIGGER_DIR, sanitize_filename(camera_name)), + exist_ok=True, + ) + with open( + os.path.join( + TRIGGER_DIR, + sanitize_filename(camera_name), + f"{sanitize_filename(body.data)}.webp", + ), + "wb", + ) as f: + f.write(thumbnail) + logger.debug( + f"Writing thumbnail for trigger with data {body.data} in {camera_name}." + ) + except Exception: + logger.exception( + f"Failed to write thumbnail for trigger with data {body.data} in {camera_name}" + ) + + Trigger.create( + camera=camera_name, + name=name, + type=body.type, + data=body.data, + threshold=body.threshold, + model=request.app.frigate_config.semantic_search.model, + embedding=np.array(embedding, dtype=np.float32).tobytes(), + triggering_event_id="", + last_triggered=None, + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Trigger created successfully for {camera_name}:{name}", + }, + status_code=200, + ) + + except Exception: + logger.exception("Error creating trigger embedding") + return JSONResponse( + content={ + "success": False, + "message": "Error creating trigger embedding", + }, + status_code=500, + ) + + +@router.put( + "/trigger/embedding/{camera_name}/{name}", + response_model=dict, + dependencies=[Depends(require_role(["admin"]))], + summary="Update trigger embedding", + description="""Updates a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + """, +) +def update_trigger_embedding( + request: Request, + camera_name: str, + name: str, + body: TriggerEmbeddingBody, +): + try: + if not request.app.frigate_config.semantic_search.enabled: + return JSONResponse( + content={ + "success": False, + "message": "Semantic search is not enabled", + }, + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + # Generate embedding based on type + embedding = None + if body.type == "description": + embedding = context.generate_description_embedding(body.data) + elif body.type == "thumbnail": + webp_file = sanitize_filename(body.data) + ".webp" + webp_path = os.path.join( + TRIGGER_DIR, sanitize_filename(camera_name), webp_file + ) + + try: + event: Event = Event.get(Event.id == body.data) + # Skip the event if not an object + if event.data.get("type") != "object": + return JSONResponse( + content={ + "success": False, + "message": f"Event {body.data} is not a tracked object for {body.type} trigger", + }, + status_code=400, + ) + # Extract valid thumbnail + thumbnail = get_event_thumbnail_bytes(event) + + with open(webp_path, "wb") as f: + f.write(thumbnail) + except DoesNotExist: + # check triggers directory for image + if not os.path.exists(webp_path): + return JSONResponse( + content={ + "success": False, + "message": f"Failed to fetch event for {body.type} trigger", + }, + status_code=400, + ) + else: + # Load the image from the triggers directory + with open(webp_path, "rb") as f: + thumbnail = f.read() + + embedding = context.generate_image_embedding( + body.data, (base64.b64encode(thumbnail).decode("ASCII")) + ) + + if embedding is None or ( + isinstance(embedding, (list, np.ndarray)) and len(embedding) == 0 + ): + return JSONResponse( + content={ + "success": False, + "message": f"Failed to generate embedding for {body.type} trigger", + }, + status_code=400, + ) + + # Check if trigger exists for upsert + trigger = Trigger.get_or_none( + Trigger.camera == camera_name, Trigger.name == name + ) + + if trigger: + # Update existing trigger + if trigger.data != body.data: # Delete old thumbnail only if data changes + try: + os.remove( + os.path.join( + TRIGGER_DIR, + sanitize_filename(camera_name), + f"{trigger.data}.webp", + ) + ) + logger.debug( + f"Deleted thumbnail for trigger with data {trigger.data} in {camera_name}." + ) + except Exception: + logger.exception( + f"Failed to delete thumbnail for trigger with data {trigger.data} in {camera_name}" + ) + + Trigger.update( + data=body.data, + model=request.app.frigate_config.semantic_search.model, + embedding=np.array(embedding, dtype=np.float32).tobytes(), + threshold=body.threshold, + triggering_event_id="", + last_triggered=None, + ).where(Trigger.camera == camera_name, Trigger.name == name).execute() + else: + # Create new trigger (for rename case) + Trigger.create( + camera=camera_name, + name=name, + type=body.type, + data=body.data, + threshold=body.threshold, + model=request.app.frigate_config.semantic_search.model, + embedding=np.array(embedding, dtype=np.float32).tobytes(), + triggering_event_id="", + last_triggered=None, + ) + + if body.type == "thumbnail": + # Save image to the triggers directory + try: + camera_path = os.path.join(TRIGGER_DIR, sanitize_filename(camera_name)) + os.makedirs(camera_path, exist_ok=True) + with open( + os.path.join(camera_path, f"{sanitize_filename(body.data)}.webp"), + "wb", + ) as f: + f.write(thumbnail) + logger.debug( + f"Writing thumbnail for trigger with data {body.data} in {camera_name}." + ) + except Exception: + logger.exception( + f"Failed to write thumbnail for trigger with data {body.data} in {camera_name}" + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Trigger updated successfully for {camera_name}:{name}", + }, + status_code=200, + ) + + except Exception: + logger.exception("Error updating trigger embedding") + return JSONResponse( + content={ + "success": False, + "message": "Error updating trigger embedding", + }, + status_code=500, + ) + + +@router.delete( + "/trigger/embedding/{camera_name}/{name}", + response_model=dict, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete trigger embedding", + description="""Deletes a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + """, +) +def delete_trigger_embedding( + request: Request, + camera_name: str, + name: str, +): + try: + trigger = Trigger.get_or_none( + Trigger.camera == camera_name, Trigger.name == name + ) + if trigger is None: + return JSONResponse( + content={ + "success": False, + "message": f"Trigger {camera_name}:{name} not found", + }, + status_code=500, + ) + + deleted = ( + Trigger.delete() + .where(Trigger.camera == camera_name, Trigger.name == name) + .execute() + ) + if deleted == 0: + return JSONResponse( + content={ + "success": False, + "message": f"Error deleting trigger {camera_name}:{name}", + }, + status_code=401, + ) + + try: + os.remove( + os.path.join( + TRIGGER_DIR, sanitize_filename(camera_name), f"{trigger.data}.webp" + ) + ) + logger.debug( + f"Deleted thumbnail for trigger with data {trigger.data} in {camera_name}." + ) + except Exception: + logger.exception( + f"Failed to delete thumbnail for trigger with data {trigger.data} in {camera_name}" + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Trigger deleted successfully for {camera_name}:{name}", + }, + status_code=200, + ) + + except Exception: + logger.exception("Error deleting trigger embedding") + return JSONResponse( + content={ + "success": False, + "message": "Error deleting trigger embedding", + }, + status_code=500, + ) + + +@router.get( + "/triggers/status/{camera_name}", + response_model=dict, + dependencies=[Depends(require_role(["admin"]))], + summary="Get triggers status", + description="""Gets the status of all triggers for a specific camera. + Returns a success message or an error if the camera is not found. + """, +) +def get_triggers_status( + camera_name: str, +): + try: + # Fetch all triggers for the specified camera + triggers = Trigger.select().where(Trigger.camera == camera_name) + + # Prepare the response with trigger status + status = { + trigger.name: { + "last_triggered": trigger.last_triggered.timestamp() + if trigger.last_triggered + else None, + "triggering_event_id": trigger.triggering_event_id + if trigger.triggering_event_id + else None, + } + for trigger in triggers + } + + if not status: + return JSONResponse( + content={ + "success": False, + "message": f"No triggers found for camera {camera_name}", + }, + status_code=404, + ) + + return {"success": True, "triggers": status} + except Exception as ex: + logger.exception(ex) + return JSONResponse( + content=({"success": False, "message": "Error fetching trigger status"}), + status_code=400, + ) diff --git a/frigate/api/export.py b/frigate/api/export.py new file mode 100644 index 0000000..741a0bb --- /dev/null +++ b/frigate/api/export.py @@ -0,0 +1,1159 @@ +"""Export apis.""" + +import datetime +import logging +import random +import string +import time +import zipfile +from collections import deque +from collections.abc import Iterator +from pathlib import Path + +import psutil +from fastapi import APIRouter, Depends, Query, Request +from fastapi.responses import JSONResponse, StreamingResponse +from pathvalidate import sanitize_filename, sanitize_filepath +from peewee import DoesNotExist +from playhouse.shortcuts import model_to_dict + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + get_current_user, + require_camera_access, + require_role, +) +from frigate.api.defs.request.batch_export_body import ( + BatchExportBody, + BatchExportItem, +) +from frigate.api.defs.request.export_bulk_body import ( + ExportBulkDeleteBody, + ExportBulkReassignBody, +) +from frigate.api.defs.request.export_case_body import ( + ExportCaseCreateBody, + ExportCaseUpdateBody, +) +from frigate.api.defs.request.export_recordings_body import ( + ExportRecordingsBody, + ExportRecordingsCustomBody, +) +from frigate.api.defs.request.export_rename_body import ExportRenameBody +from frigate.api.defs.response.export_case_response import ( + ExportCaseModel, + ExportCasesResponse, +) +from frigate.api.defs.response.export_response import ( + BatchExportResponse, + ExportJobModel, + ExportJobsResponse, + ExportModel, + ExportsResponse, + StartExportResponse, +) +from frigate.api.defs.response.generic_response import GenericResponse +from frigate.api.defs.tags import Tags +from frigate.const import CLIPS_DIR, EXPORT_DIR +from frigate.jobs.export import ( + ExportJob, + ExportQueueFullError, + available_export_queue_slots, + cancel_queued_export_jobs_for_case, + get_export_job, + list_active_export_jobs, + start_export_job, +) +from frigate.models import Export, ExportCase, Previews, Recordings +from frigate.record.export import ( + DEFAULT_TIME_LAPSE_FFMPEG_ARGS, + ChaptersEnum, + PlaybackSourceEnum, + validate_ffmpeg_args, +) +from frigate.util.time import is_current_hour + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.export]) + + +def _generate_id(length: int = 12) -> str: + return "".join(random.choices(string.ascii_lowercase + string.digits, k=length)) + + +def _generate_export_id(camera_name: str) -> str: + return f"{camera_name}_{_generate_id(6)}" + + +def _create_export_case_record( + name: str, + description: str | None, +) -> ExportCase: + now = datetime.datetime.fromtimestamp(time.time()) + return ExportCase.create( + id=_generate_id(), + name=name, + description=description, + created_at=now, + updated_at=now, + ) + + +def _validate_camera_name(request: Request, camera_name: str) -> JSONResponse | None: + if camera_name and request.app.frigate_config.cameras.get(camera_name): + return None + + return JSONResponse( + content={"success": False, "message": f"{camera_name} is not a valid camera."}, + status_code=404, + ) + + +def _validate_export_case(export_case_id: str | None) -> JSONResponse | None: + if export_case_id is None: + return None + + try: + ExportCase.get(ExportCase.id == export_case_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Export case not found"}, + status_code=404, + ) + + return None + + +def _sanitize_existing_image( + image_path: str | None, +) -> tuple[str | None, JSONResponse | None]: + # sanitize_filepath normalizes "\" to "/" but leaves ".." intact, so a path + # like "clips\..\..\etc/passwd" passes the CLIPS_DIR prefix check yet still + # escapes the directory once resolved. A valid snapshot path never uses "..". + if image_path and ".." in image_path: + return None, JSONResponse( + content={"success": False, "message": "Invalid image path"}, + status_code=400, + ) + + existing_image = sanitize_filepath(image_path) if image_path else None + + if existing_image and not existing_image.startswith(CLIPS_DIR): + return None, JSONResponse( + content={"success": False, "message": "Invalid image path"}, + status_code=400, + ) + + return existing_image, None + + +def _validate_export_source( + camera_name: str, + start_time: float, + end_time: float, + playback_source: PlaybackSourceEnum, +) -> str | None: + if playback_source == PlaybackSourceEnum.recordings: + recordings_count = ( + Recordings.select() + .where( + Recordings.start_time.between(start_time, end_time) + | Recordings.end_time.between(start_time, end_time) + | ( + (start_time > Recordings.start_time) + & (end_time < Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .count() + ) + + if recordings_count <= 0: + return "No recordings found for time range" + + return None + + previews_count = ( + Previews.select() + .where( + Previews.start_time.between(start_time, end_time) + | Previews.end_time.between(start_time, end_time) + | ((start_time > Previews.start_time) & (end_time < Previews.end_time)) + ) + .where(Previews.camera == camera_name) + .count() + ) + + if not is_current_hour(start_time) and previews_count <= 0: + return "No previews found for time range" + + return None + + +def _get_item_recording_export_errors( + request: Request, + items: list[BatchExportItem], +) -> dict[int, str]: + """Return {item_index: error message} for items with invalid state. + + Checks camera configuration and recording presence per item. Groups by + camera and issues one query per unique camera covering that camera's + full requested range, then checks each item's range against the returned + rows in Python. This avoids O(N) DB round-trips on large batches. + """ + configured_cameras = request.app.frigate_config.cameras + errors: dict[int, str] = {} + + # Validate camera configuration first + item_ranges_by_camera: dict[str, list[tuple[int, float, float]]] = {} + for index, item in enumerate(items): + if not configured_cameras.get(item.camera): + errors[index] = f"{item.camera} is not a valid camera." + continue + item_ranges_by_camera.setdefault(item.camera, []).append( + (index, item.start_time, item.end_time) + ) + + if not item_ranges_by_camera: + return errors + + # For each camera, fetch recordings that cover the union of ranges + for camera_name, indexed_ranges in item_ranges_by_camera.items(): + min_start = min(r[1] for r in indexed_ranges) + max_end = max(r[2] for r in indexed_ranges) + + recording_ranges = list( + Recordings.select(Recordings.start_time, Recordings.end_time) + .where( + Recordings.camera == camera_name, + Recordings.start_time.between(min_start, max_end) + | Recordings.end_time.between(min_start, max_end) + | ( + (min_start > Recordings.start_time) + & (max_end < Recordings.end_time) + ), + ) + .iterator() + ) + + for index, start_time, end_time in indexed_ranges: + has_recording = any( + ( + start_time <= rec.start_time <= end_time + or start_time <= rec.end_time <= end_time + or (start_time > rec.start_time and end_time < rec.end_time) + ) + for rec in recording_ranges + ) + if not has_recording: + errors[index] = "No recordings found for time range" + + return errors + + +def _build_export_job( + camera_name: str, + start_time: float, + end_time: float, + friendly_name: str | None, + existing_image: str | None, + playback_source: PlaybackSourceEnum, + export_case_id: str | None, + ffmpeg_input_args: str | None = None, + ffmpeg_output_args: str | None = None, + cpu_fallback: bool = False, + chapters: ChaptersEnum | None = None, +) -> ExportJob: + return ExportJob( + id=_generate_export_id(camera_name), + camera=camera_name, + name=friendly_name, + image_path=existing_image, + export_case_id=export_case_id, + request_start_time=int(start_time), + request_end_time=int(end_time), + playback_source=playback_source.value, + ffmpeg_input_args=ffmpeg_input_args, + ffmpeg_output_args=ffmpeg_output_args, + cpu_fallback=cpu_fallback, + chapters=chapters, + ) + + +def _export_case_to_dict(case: ExportCase) -> dict[str, object]: + case_dict = model_to_dict(case) + + for field in ("created_at", "updated_at"): + value = case_dict.get(field) + if isinstance(value, datetime.datetime): + case_dict[field] = value.timestamp() + + return case_dict + + +@router.get( + "/exports", + response_model=ExportsResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Get exports", + description="""Gets all exports from the database for cameras the user has access to. + Returns a list of exports ordered by date (most recent first).""", +) +def get_exports( + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), + export_case_id: str | None = None, + cameras: str | None = Query(default="all"), + start_date: float | None = None, + end_date: float | None = None, +): + query = Export.select().where(Export.camera << allowed_cameras) + + if export_case_id is not None: + if export_case_id == "unassigned": + query = query.where(Export.export_case.is_null(True)) + else: + query = query.where(Export.export_case == export_case_id) + + if cameras and cameras != "all": + requested = set(cameras.split(",")) + filtered_cameras = list(requested.intersection(allowed_cameras)) + if not filtered_cameras: + return JSONResponse(content=[]) + query = query.where(Export.camera << filtered_cameras) + + if start_date is not None: + query = query.where(Export.date >= start_date) + + if end_date is not None: + query = query.where(Export.date <= end_date) + + exports = query.order_by(Export.date.desc()).dicts().iterator() + return JSONResponse(content=[e for e in exports]) + + +@router.get( + "/cases", + response_model=ExportCasesResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Get export cases", + description="Gets all export cases from the database.", +) +def get_export_cases(): + cases = ExportCase.select().order_by(ExportCase.created_at.desc()).iterator() + return JSONResponse(content=[_export_case_to_dict(case) for case in cases]) + + +@router.post( + "/cases", + response_model=ExportCaseModel, + dependencies=[Depends(require_role(["admin"]))], + summary="Create export case", + description="Creates a new export case.", +) +def create_export_case(body: ExportCaseCreateBody): + case = _create_export_case_record(body.name, body.description) + return JSONResponse(content=_export_case_to_dict(case)) + + +@router.get( + "/cases/{case_id}", + response_model=ExportCaseModel, + dependencies=[Depends(allow_any_authenticated())], + summary="Get a single export case", + description="Gets a specific export case by ID.", +) +def get_export_case(case_id: str): + try: + case = ExportCase.get(ExportCase.id == case_id) + return JSONResponse(content=_export_case_to_dict(case)) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Export case not found"}, + status_code=404, + ) + + +_ZIP_STREAM_CHUNK_SIZE = 1024 * 1024 # 1 MiB + + +class _StreamingZipBuffer: + """File-like sink for ZipFile that exposes written bytes via drain(). + + ZipFile writes synchronously into this buffer; the generator drains the + queue between writes so StreamingResponse can yield bytes without + materializing the whole archive in memory. + """ + + def __init__(self) -> None: + self._queue: deque[bytes] = deque() + self._offset = 0 + + def write(self, data: bytes) -> int: + if data: + self._queue.append(bytes(data)) + self._offset += len(data) + return len(data) + + def tell(self) -> int: + return self._offset + + def flush(self) -> None: + pass + + def drain(self) -> Iterator[bytes]: + while self._queue: + yield self._queue.popleft() + + +def _unique_archive_name(export: Export, used: set[str]) -> str: + base = sanitize_filename(export.name) if export.name else None + if not base: + base = f"{export.camera}_{int(export.date)}" + + candidate = f"{base}.mp4" + counter = 1 + while candidate in used: + candidate = f"{base}_{counter}.mp4" + counter += 1 + + used.add(candidate) + return candidate + + +def _stream_case_archive(exports: list[Export]) -> Iterator[bytes]: + """Yield bytes of a zip archive built from the given exports' mp4 files.""" + buffer = _StreamingZipBuffer() + used_names: set[str] = set() + + # ZIP_STORED: mp4 is already compressed, recompressing wastes CPU for ~0% size win. + with zipfile.ZipFile( + buffer, + mode="w", + compression=zipfile.ZIP_STORED, + allowZip64=True, + ) as archive: + for export in exports: + source = Path(export.video_path) + if not source.exists(): + continue + + arcname = _unique_archive_name(export, used_names) + + with ( + archive.open(arcname, mode="w", force_zip64=True) as entry, + source.open("rb") as src, + ): + while True: + chunk = src.read(_ZIP_STREAM_CHUNK_SIZE) + if not chunk: + break + + entry.write(chunk) + yield from buffer.drain() + + yield from buffer.drain() + + yield from buffer.drain() + + +@router.get( + "/cases/{case_id}/download", + dependencies=[Depends(allow_any_authenticated())], + summary="Download export case as zip", + description="Streams a zip archive containing every completed export's mp4 for the given case.", +) +def download_export_case( + case_id: str, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + try: + case = ExportCase.get(ExportCase.id == case_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Export case not found"}, + status_code=404, + ) + + exports = list( + Export.select() + .where( + Export.export_case == case_id, + ~Export.in_progress, + Export.camera << allowed_cameras, + ) + .order_by(Export.date.asc()) + ) + + if not exports: + return JSONResponse( + content={"success": False, "message": "No exports available to download."}, + status_code=404, + ) + + archive_base = sanitize_filename(case.name) if case.name else "" + if not archive_base: + archive_base = case_id + + return StreamingResponse( + _stream_case_archive(exports), + media_type="application/zip", + headers={ + "Content-Disposition": f'attachment; filename="{archive_base}.zip"', + }, + ) + + +@router.patch( + "/cases/{case_id}", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Update export case", + description="Updates an existing export case.", +) +def update_export_case(case_id: str, body: ExportCaseUpdateBody): + try: + case = ExportCase.get(ExportCase.id == case_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Export case not found"}, + status_code=404, + ) + + if body.name is not None: + case.name = body.name + if body.description is not None: + case.description = body.description + + case.updated_at = datetime.datetime.fromtimestamp(time.time()) + + case.save() + + return JSONResponse( + content={"success": True, "message": "Successfully updated export case."} + ) + + +@router.delete( + "/cases/{case_id}", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete export case", + description="""Deletes an export case.\n Exports that reference this case will have their export_case set to null.\n """, +) +def delete_export_case(case_id: str, request: Request, delete_exports: bool = False): + try: + case = ExportCase.get(ExportCase.id == case_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Export case not found"}, + status_code=404, + ) + + if delete_exports: + cancel_queued_export_jobs_for_case(request.app.frigate_config, case_id) + + exports = list(Export.select().where(Export.export_case == case_id)) + for export in exports: + Path(export.video_path).unlink(missing_ok=True) + if export.thumb_path: + Path(export.thumb_path).unlink(missing_ok=True) + export.delete_instance() + else: + # Unassign exports from this case but keep the exports themselves + Export.update(export_case=None).where(Export.export_case == case_id).execute() + + case.delete_instance() + + return JSONResponse( + content={"success": True, "message": "Successfully deleted export case."} + ) + + +@router.get( + "/jobs/export", + response_model=ExportJobsResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Get active export jobs", + description="Gets queued and running export jobs.", +) +def get_active_export_jobs( + request: Request, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + jobs = list_active_export_jobs(request.app.frigate_config) + return JSONResponse( + content=[job.to_dict() for job in jobs if job.camera in allowed_cameras] + ) + + +@router.get( + "/jobs/export/{export_id}", + response_model=ExportJobModel, + dependencies=[Depends(allow_any_authenticated())], + summary="Get export job status", + description="Gets queued, running, or completed status for a specific export job.", +) +async def get_export_job_status(export_id: str, request: Request): + job = get_export_job(request.app.frigate_config, export_id) + if job is None: + return JSONResponse( + content={"success": False, "message": "Job not found"}, + status_code=404, + ) + + await require_camera_access(job.camera, request=request) + + return JSONResponse(content=job.to_dict()) + + +@router.post( + "/exports/batch", + response_model=BatchExportResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Start recording export batch", + description=( + "Starts recording exports for a batch of items, each with its own camera " + "and time range, and assigns them to a single export case. Attaching to " + "an existing case is temporarily admin-only until case-level ACLs exist." + ), +) +def export_recordings_batch( + request: Request, + body: BatchExportBody, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), + current_user: dict = Depends(get_current_user), +): + if isinstance(current_user, JSONResponse): + return current_user + + # Stopgap: attaching to an existing case remains admin-only until + # case-level ACLs exist. Non-admins can still create a fresh case + # as a side effect of queueing items they already have camera access to. + if body.export_case_id is not None and current_user["role"] != "admin": + return JSONResponse( + content={ + "success": False, + "message": "Only admins can attach exports to an existing case.", + }, + status_code=403, + ) + + case_validation_error = _validate_export_case(body.export_case_id) + if case_validation_error is not None: + return case_validation_error + + # Fail-closed camera access: any item referencing an inaccessible + # camera rejects the whole request. The UI's review list is already + # filtered by camera access, so reaching this branch implies a stale + # session or a crafted request — reject loudly rather than silently + # dropping items. + allowed_camera_set = set(allowed_cameras) + for item in body.items: + if item.camera not in allowed_camera_set: + return JSONResponse( + content={ + "success": False, + "message": f"Cannot export from {item.camera}: access denied", + }, + status_code=403, + ) + + # Sanitize each item's image_path up front. A bad path in any item + # kills the whole request, consistent with single-export behavior. + sanitized_images: list[str | None] = [] + for item in body.items: + existing_image, image_validation_error = _sanitize_existing_image( + item.image_path + ) + if image_validation_error is not None: + return image_validation_error + sanitized_images.append(existing_image) + + item_errors = _get_item_recording_export_errors(request, body.items) + + queueable_indexes = [ + index for index in range(len(body.items)) if index not in item_errors + ] + + if not queueable_indexes: + return JSONResponse( + content={ + "success": False, + "message": ( + "No exports could be queued: no recordings found for the " + "requested ranges." + ), + }, + status_code=400, + ) + + # Preflight admission: reject the whole batch if we can't fit every + # queueable item. Prevents partial batches where the tail fails with + # "queue full" after we've already created a case. + if available_export_queue_slots(request.app.frigate_config) < len( + queueable_indexes + ): + return JSONResponse( + content={ + "success": False, + "message": "Export queue is full. Try again once current exports finish.", + }, + status_code=503, + ) + + export_case = None + export_case_id = body.export_case_id + if export_case_id is None and body.new_case_name: + export_case = _create_export_case_record( + body.new_case_name, + body.new_case_description, + ) + export_case_id = export_case.id + + export_ids: list[str] = [] + results: list[dict[str, str | None | bool | int]] = [] + for index, item in enumerate(body.items): + if index in item_errors: + results.append( + { + "camera": item.camera, + "export_id": None, + "success": False, + "status": None, + "error": item_errors[index], + "item_index": index, + "client_item_id": item.client_item_id, + } + ) + continue + + export_job = _build_export_job( + item.camera, + item.start_time, + item.end_time, + item.friendly_name, + sanitized_images[index], + PlaybackSourceEnum.recordings, + export_case_id, + chapters=request.app.frigate_config.cameras[ + item.camera + ].record.export.chapters, + ) + try: + start_export_job(request.app.frigate_config, export_job) + except Exception: + logger.exception("Failed to queue export job %s", export_job.id) + results.append( + { + "camera": item.camera, + "export_id": None, + "success": False, + "status": None, + "error": "Failed to queue export job", + "item_index": index, + "client_item_id": item.client_item_id, + } + ) + continue + + export_ids.append(export_job.id) + results.append( + { + "camera": item.camera, + "export_id": export_job.id, + "success": True, + "status": "queued", + "error": None, + "item_index": index, + "client_item_id": item.client_item_id, + } + ) + + if export_case is not None and not export_ids: + export_case.delete_instance() + export_case_id = None + + return JSONResponse( + content={ + "export_case_id": export_case_id, + "export_ids": export_ids, + "results": results, + }, + status_code=202, + ) + + +@router.post( + "/export/{camera_name}/start/{start_time}/end/{end_time}", + response_model=StartExportResponse, + dependencies=[Depends(require_camera_access)], + summary="Start recording export", + description="""Starts an export of a recording for the specified time range. + The export can be from recordings or preview footage. Returns the export ID if + successful, or an error message if the camera is invalid or no recordings/previews + are found for the time range.""", +) +def export_recording( + request: Request, + camera_name: str, + start_time: float, + end_time: float, + body: ExportRecordingsBody, + current_user: dict = Depends(get_current_user), +): + if isinstance(current_user, JSONResponse): + return current_user + + camera_validation_error = _validate_camera_name(request, camera_name) + if camera_validation_error is not None: + return camera_validation_error + + playback_source = body.source + friendly_name = body.name + existing_image, image_validation_error = _sanitize_existing_image(body.image_path) + if image_validation_error is not None: + return image_validation_error + + export_case_id = body.export_case_id + + # a chapters value in the request body overrides the camera's export config + camera_config = request.app.frigate_config.cameras[camera_name] + chapters = ( + body.chapters + if body.chapters is not None + else camera_config.record.export.chapters + ) + + # Attaching to an existing case requires admin. Single-export for + # cameras the user can access is otherwise non-admin; we only gate + # the case-attachment side effect. + if export_case_id is not None and current_user["role"] != "admin": + return JSONResponse( + content={ + "success": False, + "message": "Only admins can attach exports to an existing case.", + }, + status_code=403, + ) + + case_validation_error = _validate_export_case(export_case_id) + if case_validation_error is not None: + return case_validation_error + + source_error = _validate_export_source( + camera_name, + start_time, + end_time, + playback_source, + ) + if source_error is not None: + return JSONResponse( + content={"success": False, "message": source_error}, + status_code=400, + ) + + export_job = _build_export_job( + camera_name, + start_time, + end_time, + friendly_name, + existing_image, + playback_source, + export_case_id, + chapters=chapters, + ) + try: + start_export_job(request.app.frigate_config, export_job) + except ExportQueueFullError: + logger.warning("Export queue is full; rejecting %s", export_job.id) + return JSONResponse( + content={ + "success": False, + "message": "Export queue is full. Try again once current exports finish.", + }, + status_code=503, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Export queued.", + "export_id": export_job.id, + "status": "queued", + } + ), + status_code=202, + ) + + +@router.patch( + "/export/{event_id}/rename", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Rename export", + description="""Renames an export. + NOTE: This changes the friendly name of the export, not the filename. + """, +) +async def export_rename(event_id: str, body: ExportRenameBody, request: Request): + try: + export: Export = Export.get(Export.id == event_id) + await require_camera_access(export.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=( + { + "success": False, + "message": "Export not found.", + } + ), + status_code=404, + ) + + export.name = body.name + export.save() + return JSONResponse( + content=( + { + "success": True, + "message": "Successfully renamed export.", + } + ), + status_code=200, + ) + + +@router.post( + "/export/custom/{camera_name}/start/{start_time}/end/{end_time}", + response_model=StartExportResponse, + dependencies=[Depends(require_camera_access)], + summary="Start custom recording export", + description="""Starts an export of a recording for the specified time range using custom FFmpeg arguments. + The export can be from recordings or preview footage. Returns the export ID if + successful, or an error message if the camera is invalid or no recordings/previews + are found for the time range. If ffmpeg_input_args and ffmpeg_output_args are not provided, + defaults to timelapse export settings.""", +) +def export_recording_custom( + request: Request, + camera_name: str, + start_time: float, + end_time: float, + body: ExportRecordingsCustomBody, +): + camera_validation_error = _validate_camera_name(request, camera_name) + if camera_validation_error is not None: + return camera_validation_error + + playback_source = body.source + friendly_name = body.name + existing_image, image_validation_error = _sanitize_existing_image(body.image_path) + if image_validation_error is not None: + return image_validation_error + ffmpeg_input_args = body.ffmpeg_input_args + ffmpeg_output_args = body.ffmpeg_output_args + cpu_fallback = body.cpu_fallback + + export_case_id = body.export_case_id + case_validation_error = _validate_export_case(export_case_id) + if case_validation_error is not None: + return case_validation_error + + source_error = _validate_export_source( + camera_name, + start_time, + end_time, + playback_source, + ) + if source_error is not None: + return JSONResponse( + content={"success": False, "message": source_error}, + status_code=400, + ) + + # Validate user-provided ffmpeg args to prevent injection. + # Admin users are trusted and skip validation. + is_admin = request.headers.get("remote-role", "") == "admin" + + if not is_admin: + for args_label, args_value in [ + ("input", ffmpeg_input_args), + ("output", ffmpeg_output_args), + ]: + if args_value is not None: + valid, message = validate_ffmpeg_args(args_value) + if not valid: + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid ffmpeg {args_label} arguments: {message}", + } + ), + status_code=400, + ) + + # Set default values if not provided (timelapse defaults) + if ffmpeg_input_args is None: + ffmpeg_input_args = "" + + if ffmpeg_output_args is None: + ffmpeg_output_args = DEFAULT_TIME_LAPSE_FFMPEG_ARGS + + export_job = _build_export_job( + camera_name, + start_time, + end_time, + friendly_name, + existing_image, + playback_source, + export_case_id, + ffmpeg_input_args, + ffmpeg_output_args, + cpu_fallback, + ) + try: + start_export_job(request.app.frigate_config, export_job) + except ExportQueueFullError: + logger.warning("Export queue is full; rejecting %s", export_job.id) + return JSONResponse( + content={ + "success": False, + "message": "Export queue is full. Try again once current exports finish.", + }, + status_code=503, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Export queued.", + "export_id": export_job.id, + "status": "queued", + } + ), + status_code=202, + ) + + +@router.get( + "/exports/{export_id}", + response_model=ExportModel, + dependencies=[Depends(allow_any_authenticated())], + summary="Get a single export", + description="""Gets a specific export by ID. The user must have access to the camera + associated with the export.""", +) +async def get_export(export_id: str, request: Request): + try: + export = Export.get(Export.id == export_id) + await require_camera_access(export.camera, request=request) + return JSONResponse(content=model_to_dict(export)) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Export not found"}, + status_code=404, + ) + + +def _get_files_in_use() -> set[str]: + """Get set of export filenames currently in use by ffmpeg.""" + files_in_use: set[str] = set() + for process in psutil.process_iter(): + try: + if process.name() != "ffmpeg": + continue + file_list = process.open_files() + if file_list: + for nt in file_list: + if nt.path.startswith(EXPORT_DIR): + files_in_use.add(nt.path.split("/")[-1]) + except psutil.Error: + continue + return files_in_use + + +@router.post( + "/exports/delete", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Bulk delete exports", + description="Deletes one or more exports by ID. All IDs must exist and none can be in-progress.", +) +def bulk_delete_exports(body: ExportBulkDeleteBody): + exports = list(Export.select().where(Export.id << body.ids)) + + if len(exports) != len(body.ids): + return JSONResponse( + content={"success": False, "message": "One or more exports not found."}, + status_code=404, + ) + + files_in_use = _get_files_in_use() + + for export in exports: + if export.video_path.split("/")[-1] in files_in_use: + return JSONResponse( + content={ + "success": False, + "message": "Can not delete in-progress export.", + }, + status_code=400, + ) + + for export in exports: + Path(export.video_path).unlink(missing_ok=True) + if export.thumb_path: + Path(export.thumb_path).unlink(missing_ok=True) + + Export.delete().where(Export.id << body.ids).execute() + + return JSONResponse( + content={ + "success": True, + "message": f"Successfully deleted {len(exports)} export(s).", + }, + status_code=200, + ) + + +@router.post( + "/exports/reassign", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Bulk reassign exports to a case", + description="Assigns or unassigns one or more exports to/from a case. All IDs must exist.", +) +def bulk_reassign_exports(body: ExportBulkReassignBody): + exports = list(Export.select().where(Export.id << body.ids)) + + if len(exports) != len(body.ids): + return JSONResponse( + content={"success": False, "message": "One or more exports not found."}, + status_code=404, + ) + + if body.export_case_id is not None: + try: + ExportCase.get(ExportCase.id == body.export_case_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Export case not found."}, + status_code=404, + ) + + Export.update(export_case=body.export_case_id).where( + Export.id << body.ids + ).execute() + + return JSONResponse( + content={ + "success": True, + "message": f"Successfully updated {len(exports)} export(s).", + }, + status_code=200, + ) diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py new file mode 100644 index 0000000..5e8323b --- /dev/null +++ b/frigate/api/fastapi_app.py @@ -0,0 +1,187 @@ +import asyncio +import logging +import re + +from fastapi import Depends, FastAPI, Request +from fastapi.responses import JSONResponse +from joserfc.jwk import OctKey +from playhouse.sqliteq import SqliteQueueDatabase +from slowapi import _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +from slowapi.middleware import SlowAPIMiddleware +from starlette_context import middleware, plugins +from starlette_context.plugins import Plugin + +from frigate.api import app as main_app +from frigate.api import ( + auth, + camera, + chat, + classification, + debug_replay, + event, + export, + media, + motion_search, + notification, + preview, + record, + review, +) +from frigate.api.auth import get_jwt_secret, limiter, require_admin_by_default +from frigate.comms.dispatcher import Dispatcher +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, +) +from frigate.config import FrigateConfig +from frigate.config.camera.updater import CameraConfigUpdatePublisher +from frigate.config.profile_manager import ProfileManager +from frigate.debug_replay import DebugReplayManager, debug_replay_auto_stop_watchdog +from frigate.embeddings import EmbeddingsContext +from frigate.genai import GenAIClientManager +from frigate.ptz.onvif import OnvifController +from frigate.stats.emitter import StatsEmitter +from frigate.storage import StorageMaintainer + +logger = logging.getLogger(__name__) + + +def check_csrf(request: Request) -> bool: + if request.method in ["GET", "HEAD", "OPTIONS", "TRACE"]: + return True + if "origin" in request.headers and "x-csrf-token" not in request.headers: + return False + + return True + + +# Used to retrieve the remote-user header: https://starlette-context.readthedocs.io/en/latest/plugins.html#easy-mode +class RemoteUserPlugin(Plugin): + key = "Remote-User" + + +def create_fastapi_app( + frigate_config: FrigateConfig, + database: SqliteQueueDatabase, + embeddings: EmbeddingsContext | None, + detected_frames_processor, + storage_maintainer: StorageMaintainer, + onvif: OnvifController, + stats_emitter: StatsEmitter, + event_metadata_updater: EventMetadataPublisher, + config_publisher: CameraConfigUpdatePublisher, + replay_manager: DebugReplayManager, + dispatcher: Dispatcher | None = None, + profile_manager: ProfileManager | None = None, + enforce_default_admin: bool = True, +): + logger.info("Starting FastAPI app") + app = FastAPI( + debug=False, + swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"}, + dependencies=[Depends(require_admin_by_default())] + if enforce_default_admin + else [], + ) + + # update the request_address with the x-forwarded-for header from nginx + # https://starlette-context.readthedocs.io/en/latest/plugins.html#forwarded-for + app.add_middleware( + middleware.ContextMiddleware, + plugins=(plugins.ForwardedForPlugin(),), + ) + + # Middleware to connect to DB before and close connection after request + # https://github.com/fastapi/full-stack-fastapi-template/issues/224#issuecomment-737423886 + # https://fastapi.tiangolo.com/tutorial/middleware/#before-and-after-the-response + @app.middleware("http") + async def frigate_middleware(request: Request, call_next): + # Before request + if not check_csrf(request): + return JSONResponse( + content={"success": False, "message": "Missing CSRF header"}, + status_code=401, + ) + + if database.is_closed(): + database.connect() + + response = await call_next(request) + + # After request https://stackoverflow.com/a/75487519 + if not database.is_closed(): + database.close() + return response + + @app.on_event("startup") + async def startup(): + logger.info("FastAPI started") + asyncio.create_task( + debug_replay_auto_stop_watchdog( + replay_manager, frigate_config, config_publisher + ) + ) + + # Rate limiter (used for login endpoint) + if frigate_config.auth.failed_login_rate_limit is None: + limiter.enabled = False + else: + auth.rateLimiter.set_limit(frigate_config.auth.failed_login_rate_limit) + + app.state.limiter = limiter + app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + app.add_middleware(SlowAPIMiddleware) + + # Routes + # Order of include_router matters: https://fastapi.tiangolo.com/tutorial/path-params/#order-matters + app.include_router(auth.router) + app.include_router(camera.router) + app.include_router(chat.router) + app.include_router(classification.router) + app.include_router(review.router) + app.include_router(main_app.router) + app.include_router(preview.router) + app.include_router(notification.router) + app.include_router(export.router) + app.include_router(event.router) + app.include_router(media.router) + app.include_router(motion_search.router) + app.include_router(record.router) + app.include_router(debug_replay.router) + # App Properties + app.frigate_config = frigate_config + app.genai_manager = GenAIClientManager(frigate_config) + app.embeddings = embeddings + app.detected_frames_processor = detected_frames_processor + app.storage_maintainer = storage_maintainer + app.camera_error_image = None + app.onvif = onvif + app.stats_emitter = stats_emitter + app.event_metadata_updater = event_metadata_updater + app.config_publisher = config_publisher + app.replay_manager = replay_manager + app.dispatcher = dispatcher + app.profile_manager = profile_manager + + if frigate_config.auth.enabled: + secret = get_jwt_secret() + key_bytes = None + if isinstance(secret, str): + # If the secret looks like hex (e.g., generated by secrets.token_hex), use raw bytes + if len(secret) % 2 == 0 and re.fullmatch(r"[0-9a-fA-F]+", secret or ""): + try: + key_bytes = bytes.fromhex(secret) + except ValueError: + key_bytes = secret.encode("utf-8") + else: + key_bytes = secret.encode("utf-8") + elif isinstance(secret, (bytes, bytearray)): + key_bytes = bytes(secret) + else: + key_bytes = str(secret).encode("utf-8") + + app.jwt_token = OctKey.import_key(key_bytes) + else: + app.jwt_token = None + + return app diff --git a/frigate/api/media.py b/frigate/api/media.py new file mode 100644 index 0000000..8d56475 --- /dev/null +++ b/frigate/api/media.py @@ -0,0 +1,1814 @@ +"""Image and video apis.""" + +import asyncio +import glob +import logging +import math +import os +import subprocess as sp +import time +from datetime import UTC, datetime, timedelta +from pathlib import Path as FilePath +from typing import Any +from urllib.parse import unquote + +import cv2 +import numpy as np +import pytz +from fastapi import APIRouter, Depends, Path, Query, Request, Response +from fastapi.responses import FileResponse, JSONResponse, StreamingResponse +from pathvalidate import sanitize_filename +from peewee import DoesNotExist, fn +from tzlocal import get_localzone_name + +from frigate.api.auth import ( + allow_any_authenticated, + require_camera_access, + require_role, +) +from frigate.api.defs.query.media_query_parameters import ( + Extension, + MediaEventsSnapshotQueryParams, + MediaLatestFrameQueryParams, + MediaMjpegFeedQueryParams, +) +from frigate.api.defs.tags import Tags +from frigate.camera.state import CameraState +from frigate.config import FrigateConfig +from frigate.config.camera.snapshots import SnapshotsConfig +from frigate.const import ( + CACHE_DIR, + INSTALL_DIR, + MAX_SEGMENT_DURATION, + PREVIEW_FRAME_TYPE, +) +from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment +from frigate.output.preview import get_most_recent_preview_frame +from frigate.track.object_processing import TrackedObjectProcessor +from frigate.util.file import ( + get_event_snapshot_bytes, + get_event_snapshot_path, + get_event_thumbnail_bytes, + load_event_snapshot_image, +) +from frigate.util.image import get_image_from_recording, get_image_quality_params +from frigate.util.media import get_keyframe_before + +logger = logging.getLogger(__name__) + + +router = APIRouter(tags=[Tags.media]) + + +def _resolve_cache_age(max_cache_age: int) -> int: + """Return max_cache_age as an int. + + When a media handler is invoked directly by another handler instead of + through its route, FastAPI doesn't resolve the Query() default and + max_cache_age arrives as the Query object; fall back to its int default. + """ + if isinstance(max_cache_age, int): + return max_cache_age + + return max_cache_age.default + + +@router.get("/{camera_name}", dependencies=[Depends(require_camera_access)]) +async def mjpeg_feed( + request: Request, + camera_name: str, + params: MediaMjpegFeedQueryParams = Depends(), +): + draw_options = { + "bounding_boxes": params.bbox, + "timestamp": params.timestamp, + "zones": params.zones, + "mask": params.mask, + "motion_boxes": params.motion, + "regions": params.regions, + } + if camera_name in request.app.frigate_config.cameras: + # return a multipart response + return StreamingResponse( + imagestream( + request.app.detected_frames_processor, + camera_name, + params.fps, + params.height, + draw_options, + ), + media_type="multipart/x-mixed-replace;boundary=frame", + ) + else: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + +def imagestream( + detected_frames_processor: TrackedObjectProcessor, + camera_name: str, + fps: int, + height: int, + draw_options: dict[str, Any], +): + while True: + # max out at specified FPS + time.sleep(1 / fps) + frame = detected_frames_processor.get_current_frame(camera_name, draw_options) + if frame is None: + frame = np.zeros((height, int(height * 16 / 9), 3), np.uint8) + + width = int(height * frame.shape[1] / frame.shape[0]) + frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_LINEAR) + + ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + yield ( + b"--frame\r\n" + b"Content-Type: image/jpeg\r\n\r\n" + bytearray(jpg.tobytes()) + b"\r\n\r\n" + ) + + +def _resolve_snapshot_settings( + snapshot_config: SnapshotsConfig, params: MediaEventsSnapshotQueryParams +) -> dict[str, Any]: + return { + "timestamp": snapshot_config.timestamp + if params.timestamp is None + else bool(params.timestamp), + "bounding_box": snapshot_config.bounding_box + if params.bbox is None + else bool(params.bbox), + "crop": snapshot_config.crop if params.crop is None else bool(params.crop), + "height": snapshot_config.height if params.height is None else params.height, + "quality": snapshot_config.quality + if params.quality is None + else params.quality, + } + + +@router.get("/{camera_name}/ptz/info", dependencies=[Depends(require_camera_access)]) +async def camera_ptz_info(request: Request, camera_name: str): + if camera_name in request.app.frigate_config.cameras: + # Schedule get_camera_info in the OnvifController's event loop + future = asyncio.run_coroutine_threadsafe( + request.app.onvif.get_camera_info(camera_name), request.app.onvif.loop + ) + result = future.result() + return JSONResponse(content=result) + else: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + +@router.get( + "/{camera_name}/latest.{extension}", + dependencies=[Depends(require_camera_access)], + description="Returns the latest frame from the specified camera in the requested format (jpg, png, webp). Falls back to preview frames if the camera is offline.", +) +async def latest_frame( + request: Request, + camera_name: str, + extension: Extension, + params: MediaLatestFrameQueryParams = Depends(), +): + frame_processor: TrackedObjectProcessor = request.app.detected_frames_processor + draw_options = { + "bounding_boxes": params.bbox, + "timestamp": params.timestamp, + "zones": params.zones, + "mask": params.mask, + "motion_boxes": params.motion, + "paths": params.paths, + "regions": params.regions, + } + quality_params = get_image_quality_params(extension.value, params.quality) + + camera_config = request.app.frigate_config.cameras.get(camera_name) + if camera_config is not None: + frame = frame_processor.get_current_frame(camera_name, draw_options) + retry_interval = float(camera_config.ffmpeg.retry_interval or 10) + + is_offline = False + if frame is None or datetime.now().timestamp() > ( + frame_processor.get_current_frame_time(camera_name) + retry_interval + ): + last_frame_time = frame_processor.get_current_frame_time(camera_name) + preview_path = get_most_recent_preview_frame( + camera_name, before=last_frame_time + ) + + if preview_path: + logger.debug(f"Using most recent preview frame for {camera_name}") + frame = cv2.imread(preview_path, cv2.IMREAD_UNCHANGED) + + if frame is not None: + is_offline = True + + if frame is None or not is_offline: + logger.debug( + f"No live or preview frame available for {camera_name}. Using error image." + ) + if request.app.camera_error_image is None: + error_image = glob.glob( + os.path.join(INSTALL_DIR, "frigate/images/camera-error.jpg") + ) + + if len(error_image) > 0: + request.app.camera_error_image = cv2.imread( + error_image[0], cv2.IMREAD_UNCHANGED + ) + + frame = request.app.camera_error_image + + height = int(params.height or str(frame.shape[0])) + width = int(height * frame.shape[1] / frame.shape[0]) + + if frame is None: + return JSONResponse( + content={"success": False, "message": "Unable to get valid frame"}, + status_code=500, + ) + + if height < 1 or width < 1: + return JSONResponse( + content="Invalid height / width requested :: {} / {}".format( + height, width + ), + status_code=400, + ) + + frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) + + _, img = cv2.imencode(f".{extension.value}", frame, quality_params) + + headers = { + "Cache-Control": "no-store" if not params.store else "private, max-age=60", + } + + if is_offline: + headers["X-Frigate-Offline"] = "true" + + return Response( + content=img.tobytes(), + media_type=extension.get_mime_type(), + headers=headers, + ) + elif ( + camera_name == "birdseye" + and request.app.frigate_config.birdseye.enabled + and request.app.frigate_config.birdseye.restream + ): + frame = cv2.cvtColor( + frame_processor.get_current_frame(camera_name), + cv2.COLOR_YUV2BGR_I420, + ) + + height = int(params.height or str(frame.shape[0])) + width = int(height * frame.shape[1] / frame.shape[0]) + + frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) + + _, img = cv2.imencode(f".{extension.value}", frame, quality_params) + return Response( + content=img.tobytes(), + media_type=extension.get_mime_type(), + headers={ + "Cache-Control": "no-store" + if not params.store + else "private, max-age=60", + }, + ) + else: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + +@router.get( + "/{camera_name}/recordings/{frame_time}/snapshot.{format}", + dependencies=[Depends(require_camera_access)], +) +async def get_snapshot_from_recording( + request: Request, + camera_name: str, + frame_time: float, + format: str = Path(enum=["png", "jpg"]), + height: int = None, +): + if camera_name not in request.app.frigate_config.cameras: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + recording: Recordings | None = None + + try: + recording = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where( + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.desc()) + .limit(1) + .get() + ) + except DoesNotExist: + # try again with a rounded frame time as it may be between + # the rounded segment start time + frame_time = math.ceil(frame_time) + try: + recording = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where( + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.desc()) + .limit(1) + .get() + ) + except DoesNotExist: + pass + + if recording is not None: + time_in_segment = frame_time - recording.start_time + codec = "png" if format == "png" else "mjpeg" + mime_type = "png" if format == "png" else "jpeg" + config: FrigateConfig = request.app.frigate_config + + image_data = get_image_from_recording( + config.ffmpeg, recording.path, time_in_segment, codec, height + ) + + if not image_data: + return JSONResponse( + content=( + { + "success": False, + "message": f"Unable to parse frame at time {frame_time}", + } + ), + status_code=404, + ) + return Response(image_data, headers={"Content-Type": f"image/{mime_type}"}) + else: + return JSONResponse( + content={ + "success": False, + "message": "Recording not found at {}".format(frame_time), + }, + status_code=404, + ) + + +@router.post( + "/{camera_name}/plus/{frame_time}", dependencies=[Depends(require_camera_access)] +) +async def submit_recording_snapshot_to_plus( + request: Request, camera_name: str, frame_time: str +): + if camera_name not in request.app.frigate_config.cameras: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + frame_time = float(frame_time) + recording_query = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where( + (frame_time >= Recordings.start_time) & (frame_time <= Recordings.end_time) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.desc()) + .limit(1) + ) + + try: + config: FrigateConfig = request.app.frigate_config + recording: Recordings = recording_query.get() + time_in_segment = frame_time - recording.start_time + image_data = get_image_from_recording( + config.ffmpeg, recording.path, time_in_segment, "png" + ) + + if not image_data: + return JSONResponse( + content={ + "success": False, + "message": f"Unable to parse frame at time {frame_time}", + }, + status_code=404, + ) + + nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR) + await asyncio.to_thread( + request.app.frigate_config.plus_api.upload_image, nd, camera_name + ) + + return JSONResponse( + content={ + "success": True, + "message": "Successfully submitted image.", + }, + status_code=200, + ) + except DoesNotExist: + return JSONResponse( + content={ + "success": False, + "message": "Recording not found at {}".format(frame_time), + }, + status_code=404, + ) + + +@router.get( + "/{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4", + dependencies=[Depends(require_camera_access)], + description="For iOS devices, use the master.m3u8 HLS link instead of clip.mp4. Safari does not reliably process progressive mp4 files.", +) +async def recording_clip( + request: Request, + camera_name: str, + start_ts: float, + end_ts: float, +): + def run_download(ffmpeg_cmd: list[str], file_path: str): + with sp.Popen( + ffmpeg_cmd, + stderr=sp.PIPE, + stdout=sp.PIPE, + text=False, + ) as ffmpeg: + while True: + data = ffmpeg.stdout.read(8192) + if data is not None and len(data) > 0: + yield data + else: + if ffmpeg.returncode and ffmpeg.returncode != 0: + logger.error( + f"Failed to generate clip, ffmpeg logs: {ffmpeg.stderr.read()}" + ) + else: + FilePath(file_path).unlink(missing_ok=True) + break + + recordings = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + Recordings.end_time, + ) + .where( + (Recordings.start_time.between(start_ts, end_ts)) + | (Recordings.end_time.between(start_ts, end_ts)) + | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.asc()) + ) + + if recordings.count() == 0: + return JSONResponse( + content={ + "success": False, + "message": "No recordings found for the specified time range", + }, + status_code=400, + ) + + file_name = sanitize_filename(f"playlist_{camera_name}_{start_ts}-{end_ts}.txt") + file_path = os.path.join(CACHE_DIR, file_name) + with open(file_path, "w") as file: + clip: Recordings + for clip in recordings: + file.write(f"file '{clip.path}'\n") + + # if this is the starting clip, add an inpoint + if clip.start_time < start_ts: + file.write(f"inpoint {int(start_ts - clip.start_time)}\n") + + # if this is the ending clip, add an outpoint + if clip.end_time > end_ts: + file.write(f"outpoint {int(end_ts - clip.start_time)}\n") + + if len(file_name) > 1000: + return JSONResponse( + content={ + "success": False, + "message": "Filename exceeded max length of 1000", + }, + status_code=403, + ) + + config: FrigateConfig = request.app.frigate_config + + ffmpeg_cmd = [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-y", + "-protocol_whitelist", + "pipe,file", + "-f", + "concat", + "-safe", + "0", + "-i", + file_path, + "-c", + "copy", + "-movflags", + "frag_keyframe+empty_moov", + "-f", + "mp4", + "pipe:", + ] + + return StreamingResponse( + run_download(ffmpeg_cmd, file_path), + media_type="video/mp4", + ) + + +@router.get( + "/vod/{camera_name}/start/{start_ts}/end/{end_ts}", + dependencies=[Depends(require_camera_access)], + description="Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.", +) +async def vod_ts( + camera_name: str, + start_ts: float, + end_ts: float, + force_discontinuity: bool = False, +): + logger.debug( + "VOD: Generating VOD for %s from %s to %s with force_discontinuity=%s", + camera_name, + start_ts, + end_ts, + force_discontinuity, + ) + recordings = ( + Recordings.select( + Recordings.path, + Recordings.duration, + Recordings.end_time, + Recordings.start_time, + ) + .where( + Recordings.start_time.between(start_ts, end_ts) + | Recordings.end_time.between(start_ts, end_ts) + | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.asc()) + .iterator() + ) + + clips = [] + durations = [] + min_duration_ms = 100 # Minimum 100ms to ensure at least one video frame + max_duration_ms = MAX_SEGMENT_DURATION * 1000 + + recording: Recordings + for recording in recordings: + logger.debug( + "VOD: processing recording: %s start=%s end=%s duration=%s", + recording.path, + recording.start_time, + recording.end_time, + recording.duration, + ) + + clip = {"type": "source", "path": recording.path} + duration = int(recording.duration * 1000) + + # adjust start offset if start_ts is after recording.start_time + if start_ts > recording.start_time: + inpoint = int((start_ts - recording.start_time) * 1000) + clip["clipFrom"] = inpoint + duration -= inpoint + logger.debug( + "VOD: applied clipFrom %sms to %s", + inpoint, + recording.path, + ) + + # adjust end if recording.end_time is after end_ts + if recording.end_time > end_ts: + duration -= int((recording.end_time - end_ts) * 1000) + + # nginx-vod-module pushes clipFrom forward to the next keyframe, + # which can leave too few frames and produce an empty/unplayable + # segment. Snap clipFrom back to the preceding keyframe so the + # segment always starts with a decodable frame. + if "clipFrom" in clip: + keyframe_ms = get_keyframe_before(recording.path, clip["clipFrom"]) + if keyframe_ms is not None: + gained = clip["clipFrom"] - keyframe_ms + clip["clipFrom"] = keyframe_ms + duration += gained + logger.debug( + "VOD: snapped clipFrom to keyframe at %sms for %s, duration now %sms", + keyframe_ms, + recording.path, + duration, + ) + else: + # could not read keyframes, remove clipFrom to use full recording + logger.debug( + "VOD: no keyframe info for %s, removing clipFrom to use full recording", + recording.path, + ) + del clip["clipFrom"] + duration = int(recording.duration * 1000) + if recording.end_time > end_ts: + duration -= int((recording.end_time - end_ts) * 1000) + + if duration < min_duration_ms: + # skip if the clip has no valid duration (too short to contain frames) + logger.debug( + "VOD: skipping recording %s - resulting duration %sms too short", + recording.path, + duration, + ) + continue + + if min_duration_ms <= duration < max_duration_ms: + clip["keyFrameDurations"] = [duration] + clips.append(clip) + durations.append(duration) + logger.debug( + "VOD: added clip %s duration_ms=%s clipFrom=%s", + recording.path, + duration, + clip.get("clipFrom"), + ) + else: + logger.warning(f"Recording clip is missing or empty: {recording.path}") + + if not clips: + logger.error( + f"No recordings found for {camera_name} during the requested time range" + ) + return JSONResponse( + content={ + "success": False, + "message": "No recordings found.", + }, + status_code=404, + ) + + hour_ago = datetime.now() - timedelta(hours=1) + return JSONResponse( + content={ + "cache": hour_ago.timestamp() > start_ts, + "discontinuity": force_discontinuity, + "consistentSequenceMediaInfo": True, + "durations": durations, + "segment_duration": max(durations), + "sequences": [{"clips": clips}], + } + ) + + +@router.get( + "/vod/{year_month}/{day}/{hour}/{camera_name}", + dependencies=[Depends(require_camera_access)], + description="Returns an HLS playlist for the specified date-time on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.", +) +async def vod_hour_no_timezone(year_month: str, day: int, hour: int, camera_name: str): + """VOD for specific hour. Uses the default timezone (UTC).""" + return await vod_hour( + year_month, day, hour, camera_name, get_localzone_name().replace("/", ",") + ) + + +@router.get( + "/vod/{year_month}/{day}/{hour}/{camera_name}/{tz_name}", + dependencies=[Depends(require_camera_access)], + description="Returns an HLS playlist for the specified date-time (with timezone) on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.", +) +async def vod_hour( + year_month: str, day: int, hour: int, camera_name: str, tz_name: str +): + parts = year_month.split("-") + start_date = ( + datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC) + - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() + ) + end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) + start_ts = start_date.timestamp() + end_ts = end_date.timestamp() + + return await vod_ts(camera_name, start_ts, end_ts) + + +@router.get( + "/vod/event/{event_id}", + dependencies=[Depends(allow_any_authenticated())], + description="Returns an HLS playlist for the specified object. Append /master.m3u8 or /index.m3u8 for HLS playback.", +) +async def vod_event( + request: Request, + event_id: str, + padding: int = Query(0, description="Padding to apply to the vod."), +): + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + logger.error(f"Event not found: {event_id}") + return JSONResponse( + content={ + "success": False, + "message": "Event not found.", + }, + status_code=404, + ) + + await require_camera_access(event.camera, request=request) + + end_ts = ( + datetime.now().timestamp() + if event.end_time is None + else (event.end_time + padding) + ) + vod_response = await vod_ts(event.camera, event.start_time - padding, end_ts) + + # If the recordings are not found and the event started more than 5 minutes ago, set has_clip to false + if ( + event.start_time < datetime.now().timestamp() - 300 + and type(vod_response) is tuple + and len(vod_response) == 2 + and vod_response[1] == 404 + ): + Event.update(has_clip=False).where(Event.id == event_id).execute() + + return vod_response + + +@router.get( + "/vod/clip/{camera_name}/start/{start_ts}/end/{end_ts}", + dependencies=[Depends(require_camera_access)], + description="Returns an HLS playlist for a timestamp range with HLS discontinuity enabled. Append /master.m3u8 or /index.m3u8 for HLS playback.", +) +async def vod_clip( + camera_name: str, + start_ts: float, + end_ts: float, +): + return await vod_ts(camera_name, start_ts, end_ts, force_discontinuity=True) + + +@router.get( + "/events/{event_id}/snapshot.jpg", + description="Returns a snapshot image for the specified object id.", +) +async def event_snapshot( + request: Request, + event_id: str, + params: MediaEventsSnapshotQueryParams = Depends(), +): + event_complete = False + jpg_bytes = None + frame_time = 0 + try: + event = Event.get(Event.id == event_id, Event.end_time != None) + event_complete = True + await require_camera_access(event.camera, request=request) + if not event.has_snapshot: + return JSONResponse( + content={"success": False, "message": "Snapshot not available"}, + status_code=404, + ) + snapshot_settings = _resolve_snapshot_settings( + request.app.frigate_config.cameras[event.camera].snapshots, params + ) + jpg_bytes, frame_time = get_event_snapshot_bytes( + event, + ext="jpg", + timestamp=snapshot_settings["timestamp"], + bounding_box=snapshot_settings["bounding_box"], + crop=snapshot_settings["crop"], + height=snapshot_settings["height"], + quality=snapshot_settings["quality"], + timestamp_style=request.app.frigate_config.cameras[ + event.camera + ].timestamp_style, + colormap=request.app.frigate_config.model.colormap, + ) + except DoesNotExist: + # see if the object is currently being tracked + try: + camera_states: list[CameraState] = ( + request.app.detected_frames_processor.camera_states.values() + ) + for camera_state in camera_states: + if event_id in camera_state.tracked_objects: + tracked_obj = camera_state.tracked_objects.get(event_id) + if tracked_obj is not None: + snapshot_settings = _resolve_snapshot_settings( + camera_state.camera_config.snapshots, params + ) + jpg_bytes, frame_time = tracked_obj.get_img_bytes( + ext="jpg", + timestamp=snapshot_settings["timestamp"], + bounding_box=snapshot_settings["bounding_box"], + crop=snapshot_settings["crop"], + height=snapshot_settings["height"], + quality=snapshot_settings["quality"], + ) + await require_camera_access(camera_state.name, request=request) + except Exception: + return JSONResponse( + content={"success": False, "message": "Ongoing event not found"}, + status_code=404, + ) + except Exception: + return JSONResponse( + content={"success": False, "message": "Unknown error occurred"}, + status_code=404, + ) + + if jpg_bytes is None: + return JSONResponse( + content={"success": False, "message": "Live frame not available"}, + status_code=404, + ) + + headers = { + "Content-Type": "image/jpeg", + "Cache-Control": "private, max-age=31536000" if event_complete else "no-store", + "X-Frame-Time": str(frame_time), + } + + if params.download: + headers["Content-Disposition"] = f"attachment; filename=snapshot-{event_id}.jpg" + + return Response( + jpg_bytes, + media_type="image/jpeg", + headers=headers, + ) + + +@router.get( + "/events/{event_id}/thumbnail.{extension}", +) +async def event_thumbnail( + request: Request, + event_id: str, + extension: Extension, + max_cache_age: int = Query( + 2592000, description="Max cache age in seconds. Default 30 days in seconds." + ), + format: str = Query(default="ios", enum=["ios", "android"]), +): + thumbnail_bytes = None + event_complete = False + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + if event.end_time is not None: + event_complete = True + + thumbnail_bytes = get_event_thumbnail_bytes(event) + except DoesNotExist: + thumbnail_bytes = None + + if thumbnail_bytes is None: + # see if the object is currently being tracked + try: + camera_states = request.app.detected_frames_processor.camera_states.values() + for camera_state in camera_states: + if event_id in camera_state.tracked_objects: + tracked_obj = camera_state.tracked_objects.get(event_id) + if tracked_obj is not None: + await require_camera_access(camera_state.name, request=request) + thumbnail_bytes = tracked_obj.get_thumbnail(extension.value) + except Exception: + return JSONResponse( + content={"success": False, "message": "Event not found"}, + status_code=404, + ) + + if thumbnail_bytes is None: + return JSONResponse( + content={"success": False, "message": "Event not found"}, + status_code=404, + ) + + img_as_np = np.frombuffer(thumbnail_bytes, dtype=np.uint8) + img = cv2.imdecode(img_as_np, flags=1) + + # android notifications prefer a 2:1 ratio + if format == "android": + img = cv2.copyMakeBorder( + img, + 0, + 0, + int(img.shape[1] * 0.5), + int(img.shape[1] * 0.5), + cv2.BORDER_CONSTANT, + (0, 0, 0), + ) + + quality_params = None + if extension in (Extension.jpg, Extension.jpeg): + quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), 70] + elif extension == Extension.webp: + quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), 60] + + _, encoded = cv2.imencode(f".{extension.value}", img, quality_params) + thumbnail_bytes = encoded.tobytes() + + return Response( + thumbnail_bytes, + media_type=extension.get_mime_type(), + headers={ + "Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}" + if event_complete + else "no-store", + }, + ) + + +@router.get("/{camera_name}/grid.jpg", dependencies=[Depends(require_camera_access)]) +def grid_snapshot( + request: Request, camera_name: str, color: str = "green", font_scale: float = 0.5 +): + if camera_name in request.app.frigate_config.cameras: + detect = request.app.frigate_config.cameras[camera_name].detect + frame_processor: TrackedObjectProcessor = request.app.detected_frames_processor + frame = frame_processor.get_current_frame(camera_name, {}) + retry_interval = float( + request.app.frigate_config.cameras.get(camera_name).ffmpeg.retry_interval + or 10 + ) + + if frame is None or datetime.now().timestamp() > ( + frame_processor.get_current_frame_time(camera_name) + retry_interval + ): + return JSONResponse( + content={"success": False, "message": "Unable to get valid frame"}, + status_code=500, + ) + + try: + grid = ( + Regions.select(Regions.grid) + .where(Regions.camera == camera_name) + .get() + .grid + ) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Unable to get region grid"}, + status_code=500, + ) + + color_arg = color.lower() + + if color_arg == "red": + draw_color = (0, 0, 255) + elif color_arg == "blue": + draw_color = (255, 0, 0) + elif color_arg == "black": + draw_color = (0, 0, 0) + elif color_arg == "white": + draw_color = (255, 255, 255) + else: + draw_color = (0, 255, 0) # green + + grid_size = len(grid) + grid_coef = 1.0 / grid_size + width = detect.width + height = detect.height + for x in range(grid_size): + for y in range(grid_size): + cell = grid[x][y] + + if len(cell["sizes"]) == 0: + continue + + std_dev = round(cell["std_dev"] * width, 2) + mean = round(cell["mean"] * width, 2) + cv2.rectangle( + frame, + (int(x * grid_coef * width), int(y * grid_coef * height)), + ( + int((x + 1) * grid_coef * width), + int((y + 1) * grid_coef * height), + ), + draw_color, + 2, + ) + cv2.putText( + frame, + f"#: {len(cell['sizes'])}", + ( + int(x * grid_coef * width + 10), + int((y * grid_coef + 0.02) * height), + ), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + color=draw_color, + thickness=2, + ) + cv2.putText( + frame, + f"std: {std_dev}", + ( + int(x * grid_coef * width + 10), + int((y * grid_coef + 0.05) * height), + ), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + color=draw_color, + thickness=2, + ) + cv2.putText( + frame, + f"avg: {mean}", + ( + int(x * grid_coef * width + 10), + int((y * grid_coef + 0.08) * height), + ), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + color=draw_color, + thickness=2, + ) + + ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + + return Response( + jpg.tobytes(), + media_type="image/jpeg", + headers={"Cache-Control": "no-store"}, + ) + else: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + +@router.delete( + "/{camera_name}/region_grid", dependencies=[Depends(require_role(["admin"]))] +) +def clear_region_grid(request: Request, camera_name: str): + """Clear the region grid for a camera.""" + if camera_name not in request.app.frigate_config.cameras: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + Regions.delete().where(Regions.camera == camera_name).execute() + return JSONResponse( + content={"success": True, "message": "Region grid cleared"}, + ) + + +@router.get( + "/events/{event_id}/snapshot-clean.webp", +) +async def event_snapshot_clean(request: Request, event_id: str, download: bool = False): + webp_bytes = None + event_complete = False + try: + event = Event.get(Event.id == event_id) + event_complete = event.end_time is not None + await require_camera_access(event.camera, request=request) + snapshot_config = request.app.frigate_config.cameras[event.camera].snapshots + if not (snapshot_config.enabled and event.has_snapshot): + return JSONResponse( + content={ + "success": False, + "message": "Snapshots must be enabled in the config", + }, + status_code=404, + ) + if event.end_time is None: + # see if the object is currently being tracked + try: + camera_states = ( + request.app.detected_frames_processor.camera_states.values() + ) + for camera_state in camera_states: + if event_id in camera_state.tracked_objects: + tracked_obj = camera_state.tracked_objects.get(event_id) + if tracked_obj is not None: + webp_bytes = tracked_obj.get_clean_webp() + break + except Exception: + return JSONResponse( + content={"success": False, "message": "Event not found"}, + status_code=404, + ) + elif not event.has_snapshot: + return JSONResponse( + content={"success": False, "message": "Snapshot not available"}, + status_code=404, + ) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Event not found"}, status_code=404 + ) + if webp_bytes is None: + try: + image_path, is_clean_snapshot = get_event_snapshot_path( + event, clean_only=True + ) + if not is_clean_snapshot or image_path is None: + return JSONResponse( + content={ + "success": False, + "message": "Clean snapshot not available", + }, + status_code=404, + ) + + if image_path.endswith(".webp"): + with open(image_path, "rb") as image_file: + webp_bytes = image_file.read() + else: + image = load_event_snapshot_image(event, clean_only=True)[0] + if image is None: + return JSONResponse( + content={ + "success": False, + "message": "Unable to load clean snapshot for event", + }, + status_code=400, + ) + + ret, webp_data = cv2.imencode( + ".webp", image, get_image_quality_params("webp", None) + ) + if not ret: + return JSONResponse( + content={ + "success": False, + "message": "Unable to convert snapshot to webp", + }, + status_code=400, + ) + + webp_bytes = webp_data.tobytes() + except Exception: + logger.error(f"Unable to load clean snapshot for event: {event.id}") + return JSONResponse( + content={ + "success": False, + "message": "Unable to load clean snapshot for event", + }, + status_code=400, + ) + + headers = { + "Content-Type": "image/webp", + "Cache-Control": "private, max-age=31536000" if event_complete else "no-cache", + } + + if download: + headers["Content-Disposition"] = ( + f"attachment; filename=snapshot-{event_id}-clean.webp" + ) + + return Response( + webp_bytes, + media_type="image/webp", + headers=headers, + ) + + +@router.get( + "/events/{event_id}/clip.mp4", +) +async def event_clip( + request: Request, + event_id: str, + padding: int = Query(0, description="Padding to apply to clip."), +): + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Event not found"}, status_code=404 + ) + + await require_camera_access(event.camera, request=request) + + if not event.has_clip: + return JSONResponse( + content={"success": False, "message": "Clip not available"}, status_code=404 + ) + + end_ts = ( + datetime.now().timestamp() + if event.end_time is None + else event.end_time + padding + ) + return await recording_clip( + request, event.camera, event.start_time - padding, end_ts + ) + + +@router.get( + "/review/{review_id}/clip.mp4", +) +async def review_clip( + request: Request, + review_id: str, + padding: int = Query(0, description="Padding to apply to clip."), +): + try: + review: ReviewSegment = ReviewSegment.get(ReviewSegment.id == review_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Review not found"}, status_code=404 + ) + + await require_camera_access(review.camera, request=request) + + end_ts = ( + datetime.now().timestamp() + if review.end_time is None + else review.end_time + padding + ) + return await recording_clip( + request, review.camera, review.start_time - padding, end_ts + ) + + +@router.get( + "/events/{event_id}/preview.gif", +) +async def event_preview(request: Request, event_id: str): + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Event not found"}, status_code=404 + ) + + await require_camera_access(event.camera, request=request) + + start_ts = event.start_time + end_ts = start_ts + ( + min(event.end_time - event.start_time, 20) if event.end_time else 20 + ) + return await preview_gif(request, event.camera, start_ts, end_ts) + + +@router.get( + "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif", + dependencies=[Depends(require_camera_access)], +) +async def preview_gif( + request: Request, + camera_name: str, + start_ts: float, + end_ts: float, + max_cache_age: int = Query( + 2592000, description="Max cache age in seconds. Default 30 days in seconds." + ), +): + if datetime.fromtimestamp(start_ts) < datetime.now().replace(minute=0, second=0): + # has preview mp4 + try: + preview: Previews = ( + Previews.select( + Previews.camera, + Previews.path, + Previews.duration, + Previews.start_time, + Previews.end_time, + ) + .where( + Previews.start_time.between(start_ts, end_ts) + | Previews.end_time.between(start_ts, end_ts) + | ((start_ts > Previews.start_time) & (end_ts < Previews.end_time)) + ) + .where(Previews.camera == camera_name) + .limit(1) + .get() + ) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Preview not found"}, + status_code=404, + ) + + diff = start_ts - preview.start_time + minutes = int(diff / 60) + seconds = int(diff % 60) + config: FrigateConfig = request.app.frigate_config + ffmpeg_cmd = [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-ss", + f"00:{minutes}:{seconds}", + "-t", + f"{end_ts - start_ts}", + "-i", + preview.path, + "-r", + "8", + "-vf", + "setpts=0.12*PTS", + "-loop", + "0", + "-c:v", + "gif", + "-f", + "gif", + "-", + ] + + process = await asyncio.to_thread( + sp.run, + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + return JSONResponse( + content={"success": False, "message": "Unable to create preview gif"}, + status_code=500, + ) + + gif_bytes = process.stdout + else: + # need to generate from existing images + 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}-" + start_file = f"{file_start}{start_ts}.{PREVIEW_FRAME_TYPE}" + end_file = f"{file_start}{end_ts}.{PREVIEW_FRAME_TYPE}" + + camera_files = [ + entry.name + for entry in os.scandir(preview_dir) + if entry.name.startswith(file_start) + ] + camera_files.sort() + + selected_previews = [] + + for file in camera_files: + if file < start_file: + continue + + if file > end_file: + break + + selected_previews.append(f"file '{os.path.join(preview_dir, file)}'") + selected_previews.append("duration 0.12") + + if not selected_previews: + return JSONResponse( + content={"success": False, "message": "Preview not found"}, + status_code=404, + ) + + last_file = selected_previews[-2] + selected_previews.append(last_file) + config: FrigateConfig = request.app.frigate_config + + ffmpeg_cmd = [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-f", + "concat", + "-y", + "-protocol_whitelist", + "pipe,file", + "-safe", + "0", + "-i", + "/dev/stdin", + "-loop", + "0", + "-c:v", + "gif", + "-f", + "gif", + "-", + ] + + process = await asyncio.to_thread( + sp.run, + ffmpeg_cmd, + input=str.encode("\n".join(selected_previews)), + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + return JSONResponse( + content={"success": False, "message": "Unable to create preview gif"}, + status_code=500, + ) + + gif_bytes = process.stdout + + return Response( + gif_bytes, + media_type="image/gif", + headers={ + "Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}", + "Content-Type": "image/gif", + }, + ) + + +@router.get( + "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4", + dependencies=[Depends(require_camera_access)], +) +async def preview_mp4( + request: Request, + camera_name: str, + start_ts: float, + end_ts: float, + max_cache_age: int = Query( + 604800, description="Max cache age in seconds. Default 7 days in seconds." + ), +): + file_name = sanitize_filename(f"preview_{camera_name}_{start_ts}-{end_ts}.mp4") + + if len(file_name) > 1000: + return JSONResponse( + content=( + { + "success": False, + "message": "Filename exceeded max length of 1000 characters.", + } + ), + status_code=403, + ) + + path = os.path.join(CACHE_DIR, file_name) + + if datetime.fromtimestamp(start_ts) < datetime.now().replace(minute=0, second=0): + # has preview mp4 + try: + preview: Previews = ( + Previews.select( + Previews.camera, + Previews.path, + Previews.duration, + Previews.start_time, + Previews.end_time, + ) + .where( + Previews.start_time.between(start_ts, end_ts) + | Previews.end_time.between(start_ts, end_ts) + | ((start_ts > Previews.start_time) & (end_ts < Previews.end_time)) + ) + .where(Previews.camera == camera_name) + .limit(1) + .get() + ) + except DoesNotExist: + preview = None + + if not preview: + return JSONResponse( + content={"success": False, "message": "Preview not found"}, + status_code=404, + ) + + diff = start_ts - preview.start_time + minutes = int(diff / 60) + seconds = int(diff % 60) + config: FrigateConfig = request.app.frigate_config + ffmpeg_cmd = [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-y", + "-ss", + f"00:{minutes}:{seconds}", + "-t", + f"{end_ts - start_ts}", + "-i", + preview.path, + "-r", + "8", + "-vf", + "setpts=0.12*PTS", + "-c:v", + "libx264", + "-movflags", + "+faststart", + path, + ] + + process = await asyncio.to_thread( + sp.run, + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + return JSONResponse( + content={"success": False, "message": "Unable to create preview gif"}, + status_code=500, + ) + + else: + # need to generate from existing images + 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}-" + start_file = f"{file_start}{start_ts}.{PREVIEW_FRAME_TYPE}" + end_file = f"{file_start}{end_ts}.{PREVIEW_FRAME_TYPE}" + + camera_files = [ + entry.name + for entry in os.scandir(preview_dir) + if entry.name.startswith(file_start) + ] + camera_files.sort() + + selected_previews = [] + + for file in camera_files: + if file < start_file: + continue + + if file > end_file: + break + + selected_previews.append(f"file '{os.path.join(preview_dir, file)}'") + selected_previews.append("duration 0.12") + + if not selected_previews: + return JSONResponse( + content={"success": False, "message": "Preview not found"}, + status_code=404, + ) + + last_file = selected_previews[-2] + selected_previews.append(last_file) + config: FrigateConfig = request.app.frigate_config + + ffmpeg_cmd = [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-f", + "concat", + "-y", + "-protocol_whitelist", + "pipe,file", + "-safe", + "0", + "-i", + "/dev/stdin", + "-c:v", + "libx264", + "-movflags", + "+faststart", + path, + ] + + process = await asyncio.to_thread( + sp.run, + ffmpeg_cmd, + input=str.encode("\n".join(selected_previews)), + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + return JSONResponse( + content={"success": False, "message": "Unable to create preview gif"}, + status_code=500, + ) + + headers = { + "Content-Description": "File Transfer", + "Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}", + "Content-Type": "video/mp4", + "Content-Length": str(os.path.getsize(path)), + # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers + "X-Accel-Redirect": f"/cache/{file_name}", + } + + return FileResponse( + path, + media_type="video/mp4", + filename=file_name, + headers=headers, + ) + + +@router.get("/review/{event_id}/preview") +async def review_preview( + request: Request, + event_id: str, + format: str = Query(default="gif", enum=["gif", "mp4"]), +): + try: + review: ReviewSegment = ReviewSegment.get(ReviewSegment.id == event_id) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Review segment not found"}), + status_code=404, + ) + + await require_camera_access(review.camera, request=request) + + padding = 8 + start_ts = review.start_time - padding + end_ts = ( + review.end_time + padding if review.end_time else datetime.now().timestamp() + ) + + if format == "gif": + return await preview_gif(request, review.camera, start_ts, end_ts) + else: + return await preview_mp4(request, review.camera, start_ts, end_ts) + + +@router.get( + "/preview/{file_name}/thumbnail.jpg", + dependencies=[Depends(allow_any_authenticated())], +) +@router.get( + "/preview/{file_name}/thumbnail.webp", + dependencies=[Depends(allow_any_authenticated())], +) +async def preview_thumbnail(request: Request, file_name: str): + """Get a thumbnail from the cached preview frames.""" + if len(file_name) > 1000: + return JSONResponse( + content=( + {"success": False, "message": "Filename exceeded max length of 1000"} + ), + status_code=403, + ) + + # Extract camera name from preview filename (format: preview_{camera}-{timestamp}.ext) + if not file_name.startswith("preview_"): + return JSONResponse( + content={"success": False, "message": "Invalid preview filename"}, + status_code=400, + ) + # Use rsplit to handle camera names containing dashes (e.g. front-door) + name_part = file_name[len("preview_") :].rsplit(".", 1)[0] # strip extension + camera_name = name_part.rsplit("-", 1)[0] # split off timestamp + await require_camera_access(camera_name, request=request) + + safe_file_name_current = sanitize_filename(file_name) + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + + try: + with open( + os.path.join(preview_dir, safe_file_name_current), "rb" + ) as image_file: + jpg_bytes = image_file.read() + except FileNotFoundError: + return JSONResponse( + content=({"success": False, "message": "Image file not found"}), + status_code=404, + ) + + return Response( + jpg_bytes, + media_type="image/webp", + headers={ + "Content-Type": "image/webp", + "Cache-Control": "private, max-age=31536000", + }, + ) + + +####################### dynamic routes ########################### + + +@router.get( + "/{camera_name}/{label}/best.jpg", dependencies=[Depends(require_camera_access)] +) +@router.get( + "/{camera_name}/{label}/thumbnail.jpg", + dependencies=[Depends(require_camera_access)], +) +async def label_thumbnail(request: Request, camera_name: str, label: str): + label = unquote(label) + event_query = Event.select(fn.MAX(Event.id)).where(Event.camera == camera_name) + if label != "any": + event_query = event_query.where(Event.label == label) + + try: + event_id = event_query.scalar() + + return await event_thumbnail(request, event_id, Extension.jpg, 60) + except DoesNotExist: + frame = np.zeros((175, 175, 3), np.uint8) + ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + + return Response( + jpg.tobytes(), + media_type="image/jpeg", + headers={"Cache-Control": "no-store"}, + ) + + +@router.get( + "/{camera_name}/{label}/clip.mp4", dependencies=[Depends(require_camera_access)] +) +async def label_clip(request: Request, camera_name: str, label: str): + label = unquote(label) + event_query = Event.select(fn.MAX(Event.id)).where( + Event.camera == camera_name, Event.has_clip == True + ) + if label != "any": + event_query = event_query.where(Event.label == label) + + try: + event = event_query.get() + + return await event_clip(request, event.id, 0) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Event not found"}, status_code=404 + ) + + +@router.get( + "/{camera_name}/{label}/snapshot.jpg", dependencies=[Depends(require_camera_access)] +) +async def label_snapshot(request: Request, camera_name: str, label: str): + """Returns the snapshot image from the latest event for the given camera and label combo""" + label = unquote(label) + if label == "any": + event_query = ( + Event.select(Event.id) + .where(Event.camera == camera_name) + .where(Event.has_snapshot == True) + .order_by(Event.start_time.desc()) + ) + else: + event_query = ( + Event.select(Event.id) + .where(Event.camera == camera_name) + .where(Event.label == label) + .where(Event.has_snapshot == True) + .order_by(Event.start_time.desc()) + ) + + try: + event: Event = event_query.get() + return await event_snapshot(request, event.id, MediaEventsSnapshotQueryParams()) + except DoesNotExist: + frame = np.zeros((720, 1280, 3), np.uint8) + _, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + + return Response( + jpg.tobytes(), + media_type="image/jpeg", + ) diff --git a/frigate/api/media_auth.py b/frigate/api/media_auth.py new file mode 100644 index 0000000..0630e36 --- /dev/null +++ b/frigate/api/media_auth.py @@ -0,0 +1,290 @@ +"""URI-aware authorization for nginx-served static media. + +The `/auth` endpoint (used as nginx `auth_request` target) calls into this +module to classify the requested URI from the `X-Original-URL` header and, for +camera-scoped resources, decide whether the current role may access them. + +Without this, `auth_request` only verifies the JWT — every authenticated user +could read clips, recordings, and exports for *any* camera, bypassing the +per-camera authorization the regular API enforces via `require_camera_access`. +""" + +from __future__ import annotations + +import logging +import os +from enum import Enum +from urllib.parse import unquote, urlparse + +from peewee import DoesNotExist + +from frigate.config import FrigateConfig +from frigate.const import EXPORT_DIR +from frigate.models import Export, User + +logger = logging.getLogger(__name__) + + +class MediaAuthResolution(str, Enum): + """Classification of an `X-Original-URL` path for media-auth purposes.""" + + CAMERA = "camera" + ADMIN_ONLY = "admin_only" + LISTING_MULTI_CAMERA = "listing_multi_camera" + LISTING_NEUTRAL = "listing_neutral" + # Under a recognized media root (/clips, /recordings, /exports) but + # unclassifiable (unknown subtree, no matching DB row, DB error). + # Restricted users are denied; admins/full-access roles are allowed + # (nginx will likely return 404 if the file genuinely doesn't exist). + UNRESOLVED_MEDIA = "unresolved_media" + # Not a media URI at all (e.g. /api/events, /login). + UNKNOWN = "unknown" + + +def extract_path(original_url: str | None) -> str | None: + """Return the decoded path component of nginx's `X-Original-URL` header. + + nginx forwards the *raw* request URI (with `..` segments intact) via + `$request_uri`. nginx normalizes the path before serving the file, so a + request like `/recordings/.../allowed_cam/../forbidden_cam/file.mp4` + would (1) parse as the allowed camera in our auth check, (2) be served + as the forbidden camera by nginx. To close the bypass we reject any URI + whose path contains `.` or `..` segments outright. + """ + if not original_url: + return None + + parsed = urlparse(original_url) + raw_path = parsed.path or original_url + decoded = unquote(raw_path) + if not decoded: + return None + + if not decoded.startswith("/"): + decoded = "/" + decoded + + segments = decoded.split("/") + if ".." in segments or "." in segments: + return None + + return decoded + + +def resolve_media_uri( + uri: str, frigate_config: FrigateConfig | None = None +) -> tuple[MediaAuthResolution, str | None]: + """Classify a URI and return the owning camera if applicable. + + `frigate_config` is used to disambiguate clip/review filenames whose + camera name contains hyphens by matching against the longest configured + camera-name prefix. + """ + if not uri: + return MediaAuthResolution.UNKNOWN, None + + parts = [p for p in uri.split("/") if p] + if not parts: + return MediaAuthResolution.UNKNOWN, None + + root = parts[0] + if root == "recordings": + return _resolve_recording(parts) + if root == "clips": + return _resolve_clip(parts, frigate_config) + if root == "exports": + return _resolve_export(parts) + + return MediaAuthResolution.UNKNOWN, None + + +def _resolve_recording( + parts: list[str], +) -> tuple[MediaAuthResolution, str | None]: + # /recordings → neutral + # /recordings/{date} → neutral + # /recordings/{date}/{hour} → multi-camera listing + # /recordings/{date}/{hour}/{cam}/... → camera + if len(parts) <= 2: + return MediaAuthResolution.LISTING_NEUTRAL, None + if len(parts) == 3: + return MediaAuthResolution.LISTING_MULTI_CAMERA, None + return MediaAuthResolution.CAMERA, parts[3] + + +def _resolve_clip( + parts: list[str], frigate_config: FrigateConfig | None +) -> tuple[MediaAuthResolution, str | None]: + # /clips → multi-camera listing + # /clips/thumbs/{cam}/... → camera + # /clips/previews/{cam}/... → camera + # /clips/review/thumb-{cam}-{review_id}.webp → camera (parsed) + # /clips/faces/... → admin-only + # /clips/genai-requests/... → admin-only + # /clips/preview_restart_cache/... → admin-only + # /clips/{model}/train|dataset/... → admin-only + # /clips/{cam}-{event_id}[-clean].{ext} → camera (parsed) + # other /clips/{subdir}/... → unresolved (deny restricted) + if len(parts) == 1: + return MediaAuthResolution.LISTING_MULTI_CAMERA, None + + second = parts[1] + + if second in ("thumbs", "previews"): + if len(parts) == 2: + return MediaAuthResolution.LISTING_MULTI_CAMERA, None + return MediaAuthResolution.CAMERA, parts[2] + + if second == "review": + if len(parts) == 2: + return MediaAuthResolution.LISTING_MULTI_CAMERA, None + camera = _camera_from_thumb_filename(parts[2], frigate_config) + if camera: + return MediaAuthResolution.CAMERA, camera + return MediaAuthResolution.UNRESOLVED_MEDIA, None + + if second in ("faces", "genai-requests", "preview_restart_cache"): + return MediaAuthResolution.ADMIN_ONLY, None + + if len(parts) >= 3 and parts[2] in ("train", "dataset"): + return MediaAuthResolution.ADMIN_ONLY, None + + if len(parts) == 2: + camera = _camera_from_clip_filename(second, frigate_config) + if camera: + return MediaAuthResolution.CAMERA, camera + return MediaAuthResolution.UNRESOLVED_MEDIA, None + + return MediaAuthResolution.UNRESOLVED_MEDIA, None + + +def _longest_prefix_camera( + stem: str, frigate_config: FrigateConfig | None +) -> str | None: + if frigate_config is None: + return None + for cam in sorted(frigate_config.cameras.keys(), key=len, reverse=True): + if stem.startswith(cam + "-"): + return cam + return None + + +def _camera_from_clip_filename( + filename: str, frigate_config: FrigateConfig | None +) -> str | None: + """Match a flat clip filename `{camera}-{event_id}[-clean].{ext}` against + configured camera names. Longest-prefix wins so camera names containing + hyphens (e.g. `front-door`) resolve correctly. + """ + dot = filename.rfind(".") + stem = filename[:dot] if dot > 0 else filename + return _longest_prefix_camera(stem, frigate_config) + + +def _camera_from_thumb_filename( + filename: str, frigate_config: FrigateConfig | None +) -> str | None: + """Match a review thumbnail filename `thumb-{camera}-{review_id}.webp`.""" + if not filename.startswith("thumb-"): + return None + dot = filename.rfind(".") + stem = filename[len("thumb-") : dot] if dot > 0 else filename[len("thumb-") :] + return _longest_prefix_camera(stem, frigate_config) + + +def _resolve_export( + parts: list[str], +) -> tuple[MediaAuthResolution, str | None]: + # /exports → multi-camera listing + # /exports/{filename}.mp4 → camera (DB lookup by exact path) + if len(parts) == 1: + return MediaAuthResolution.LISTING_MULTI_CAMERA, None + if len(parts) != 2: + return MediaAuthResolution.UNRESOLVED_MEDIA, None + + filename = parts[1] + full_path = os.path.join(EXPORT_DIR, filename) + try: + export = Export.get(Export.video_path == full_path) + return MediaAuthResolution.CAMERA, export.camera + except DoesNotExist: + return MediaAuthResolution.UNRESOLVED_MEDIA, None + except Exception as e: + logger.warning("Export DB lookup failed for %s: %s", filename, e) + return MediaAuthResolution.UNRESOLVED_MEDIA, None + + +def check_camera_access(role: str, camera: str, frigate_config: FrigateConfig) -> bool: + """Return True iff `role` may access `camera`. + + Mirrors the gating logic in `require_camera_access`: admin and any role + without a non-empty allow-list bypass the check. + """ + if role == "admin": + return True + + roles_dict = frigate_config.auth.roles + if not roles_dict.get(role): + return True + + all_camera_names = set(frigate_config.cameras.keys()) + allowed = User.get_allowed_cameras(role, roles_dict, all_camera_names) + return camera in allowed + + +def is_role_restricted(role: str, frigate_config: FrigateConfig) -> bool: + """True if `role` has a non-empty allow-list (i.e. not full-access).""" + if role == "admin": + return False + return bool(frigate_config.auth.roles.get(role)) + + +def deny_response_for_media_uri( + original_url: str | None, role: str | None, frigate_config: FrigateConfig +) -> int | None: + """Decide whether the current role should be blocked from `original_url`. + + Returns an HTTP status code (403) when access should be denied, or `None` + when the request is allowed. + """ + if not original_url: + return None + + path = extract_path(original_url) + + # `extract_path` returns None for URIs containing `.` or `..` segments. + # For media-root URIs that's a traversal attempt — deny outright. For + # non-media URIs, pass through (nginx / the backend handle them). + if path is None: + raw = urlparse(original_url).path or original_url + decoded = unquote(raw) + first = decoded.lstrip("/").split("/", 1)[0] if decoded else "" + if first in ("clips", "recordings", "exports"): + return 403 + return None + + resolution, camera = resolve_media_uri(path, frigate_config) + if resolution == MediaAuthResolution.UNKNOWN: + return None + + if not role or role == "admin": + return None + + if not is_role_restricted(role, frigate_config): + return None + + if resolution == MediaAuthResolution.LISTING_NEUTRAL: + return None + + if resolution in ( + MediaAuthResolution.LISTING_MULTI_CAMERA, + MediaAuthResolution.ADMIN_ONLY, + MediaAuthResolution.UNRESOLVED_MEDIA, + ): + return 403 + + if resolution == MediaAuthResolution.CAMERA: + if camera and check_camera_access(role, camera, frigate_config): + return None + return 403 + + return 403 diff --git a/frigate/api/motion_search.py b/frigate/api/motion_search.py new file mode 100644 index 0000000..8a5f175 --- /dev/null +++ b/frigate/api/motion_search.py @@ -0,0 +1,290 @@ +"""Motion search API for detecting changes within a region of interest.""" + +import logging +from typing import Any + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field + +from frigate.api.auth import require_camera_access +from frigate.api.defs.tags import Tags +from frigate.jobs.motion_search import ( + cancel_motion_search_job, + get_motion_search_job, + start_motion_search_job, +) +from frigate.types import JobStatusTypesEnum + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.motion_search]) + + +class MotionSearchRequest(BaseModel): + """Request body for motion search.""" + + start_time: float = Field(description="Start timestamp for the search range") + end_time: float = Field(description="End timestamp for the search range") + polygon_points: list[list[float]] = Field( + description="List of [x, y] normalized coordinates (0-1) defining the ROI polygon" + ) + threshold: int = Field( + default=30, + ge=1, + le=255, + description="Pixel difference threshold (1-255)", + ) + min_area: float = Field( + default=5.0, + ge=0.1, + le=100.0, + description="Minimum change area as a percentage of the ROI", + ) + parallel: bool = Field( + default=False, + description="Enable parallel scanning across segments", + ) + max_results: int = Field( + default=25, + ge=1, + le=200, + description="Maximum number of search results to return", + ) + + +class MotionSearchResult(BaseModel): + """A single search result with timestamp and change info.""" + + timestamp: float = Field(description="Timestamp where change was detected") + change_percentage: float = Field(description="Percentage of ROI area that changed") + + +class MotionSearchMetricsResponse(BaseModel): + """Metrics collected during motion search execution.""" + + segments_scanned: int = 0 + segments_processed: int = 0 + metadata_inactive_segments: int = 0 + heatmap_roi_skip_segments: int = 0 + fallback_full_range_segments: int = 0 + frames_decoded: int = 0 + wall_time_seconds: float = 0.0 + segments_with_errors: int = 0 + + +class MotionSearchStartResponse(BaseModel): + """Response when motion search job starts.""" + + success: bool + message: str + job_id: str + + +class MotionSearchStatusResponse(BaseModel): + """Response containing job status and results.""" + + success: bool + message: str + status: str # "queued", "running", "success", "failed", or "cancelled" + results: list[MotionSearchResult] | None = None + total_frames_processed: int | None = None + error_message: str | None = None + metrics: MotionSearchMetricsResponse | None = None + scanning_timestamp: float | None = None + progress: float | None = None + + +@router.post( + "/{camera_name}/search/motion", + response_model=MotionSearchStartResponse, + dependencies=[Depends(require_camera_access)], + summary="Start motion search job", + description="""Starts an asynchronous search for significant motion changes within + a user-defined Region of Interest (ROI) over a specified time range. Returns a job_id + that can be used to poll for results.""", +) +async def start_motion_search( + request: Request, + camera_name: str, + body: MotionSearchRequest, +): + """Start an async motion search job.""" + config = request.app.frigate_config + + if camera_name not in config.cameras: + return JSONResponse( + content={"success": False, "message": f"Camera {camera_name} not found"}, + status_code=404, + ) + + # Validate polygon has at least 3 points + if len(body.polygon_points) < 3: + return JSONResponse( + content={ + "success": False, + "message": "Polygon must have at least 3 points", + }, + status_code=400, + ) + + # Validate time range + if body.start_time >= body.end_time: + return JSONResponse( + content={ + "success": False, + "message": "Start time must be before end time", + }, + status_code=400, + ) + + # Start the job using the jobs module + job_id = start_motion_search_job( + config=config, + camera_name=camera_name, + start_time=body.start_time, + end_time=body.end_time, + polygon_points=body.polygon_points, + threshold=body.threshold, + min_area=body.min_area, + parallel=body.parallel, + max_results=body.max_results, + ) + + return JSONResponse( + content={ + "success": True, + "message": "Search job started", + "job_id": job_id, + } + ) + + +@router.get( + "/{camera_name}/search/motion/{job_id}", + response_model=MotionSearchStatusResponse, + dependencies=[Depends(require_camera_access)], + summary="Get motion search job status", + description="Returns the status and results (if complete) of a motion search job.", +) +async def get_motion_search_status_endpoint( + request: Request, + camera_name: str, + job_id: str, +): + """Get the status of a motion search job.""" + config = request.app.frigate_config + + if camera_name not in config.cameras: + return JSONResponse( + content={"success": False, "message": f"Camera {camera_name} not found"}, + status_code=404, + ) + + job = get_motion_search_job(job_id) + if not job: + return JSONResponse( + content={"success": False, "message": "Job not found"}, + status_code=404, + ) + + api_status = job.status + + # Build response content + response_content: dict[str, Any] = { + "success": api_status != JobStatusTypesEnum.failed, + "status": api_status, + } + + if api_status == JobStatusTypesEnum.failed: + response_content["message"] = job.error_message or "Search failed" + response_content["error_message"] = job.error_message + elif api_status == JobStatusTypesEnum.cancelled: + response_content["message"] = "Search cancelled" + response_content["total_frames_processed"] = job.total_frames_processed + elif api_status == JobStatusTypesEnum.success: + response_content["message"] = "Search complete" + if job.results: + response_content["results"] = job.results.get("results", []) + response_content["total_frames_processed"] = job.results.get( + "total_frames_processed", job.total_frames_processed + ) + else: + response_content["results"] = [] + response_content["total_frames_processed"] = job.total_frames_processed + else: + response_content["message"] = "Job processing" + response_content["total_frames_processed"] = job.total_frames_processed + # Include partial results if available (streaming) + if job.results: + response_content["results"] = job.results.get("results", []) + response_content["total_frames_processed"] = job.results.get( + "total_frames_processed", job.total_frames_processed + ) + + # Include metrics if available + if job.metrics: + response_content["metrics"] = job.metrics.to_dict() + + response_content["scanning_timestamp"] = job.scanning_timestamp + response_content["progress"] = job.progress + + return JSONResponse(content=response_content) + + +@router.post( + "/{camera_name}/search/motion/{job_id}/cancel", + dependencies=[Depends(require_camera_access)], + summary="Cancel motion search job", + description="Cancels an active motion search job if it is still processing.", +) +async def cancel_motion_search_endpoint( + request: Request, + camera_name: str, + job_id: str, +): + """Cancel an active motion search job.""" + config = request.app.frigate_config + + if camera_name not in config.cameras: + return JSONResponse( + content={"success": False, "message": f"Camera {camera_name} not found"}, + status_code=404, + ) + + job = get_motion_search_job(job_id) + if not job: + return JSONResponse( + content={"success": False, "message": "Job not found"}, + status_code=404, + ) + + # Check if already finished + api_status = job.status + if api_status not in (JobStatusTypesEnum.queued, JobStatusTypesEnum.running): + return JSONResponse( + content={ + "success": True, + "message": "Job already finished", + "status": api_status, + } + ) + + # Request cancellation + cancelled = cancel_motion_search_job(job_id) + if cancelled: + return JSONResponse( + content={ + "success": True, + "message": "Search cancelled", + "status": "cancelled", + } + ) + + return JSONResponse( + content={ + "success": False, + "message": "Failed to cancel job", + }, + status_code=500, + ) diff --git a/frigate/api/notification.py b/frigate/api/notification.py new file mode 100644 index 0000000..502e76d --- /dev/null +++ b/frigate/api/notification.py @@ -0,0 +1,86 @@ +"""Notification apis.""" + +import logging +import os +from typing import Any + +from cryptography.hazmat.primitives import serialization +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse +from peewee import DoesNotExist +from py_vapid import Vapid01, utils + +from frigate.api.auth import allow_any_authenticated +from frigate.api.defs.tags import Tags +from frigate.const import CONFIG_DIR +from frigate.models import User + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.notifications]) + + +@router.get( + "/notifications/pubkey", + dependencies=[Depends(allow_any_authenticated())], + summary="Get VAPID public key", + description="""Gets the VAPID public key for the notifications. + Returns the public key or an error if notifications are not enabled. + """, +) +def get_vapid_pub_key(request: Request): + config = request.app.frigate_config + notifications_enabled = config.notifications.enabled + camera_notifications_enabled = [ + c for c in config.cameras.values() if c.enabled and c.notifications.enabled + ] + if not (notifications_enabled or camera_notifications_enabled): + return JSONResponse( + content=({"success": False, "message": "Notifications are not enabled."}), + status_code=400, + ) + + key = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem")) + raw_pub = key.public_key.public_bytes( + serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint + ) + return JSONResponse(content=utils.b64urlencode(raw_pub), status_code=200) + + +@router.post( + "/notifications/register", + dependencies=[Depends(allow_any_authenticated())], + summary="Register notifications", + description="""Registers a notifications subscription. + Returns a success message or an error if the subscription is not provided. + """, +) +def register_notifications(request: Request, body: dict = None): + if request.app.frigate_config.auth.enabled: + # FIXME: For FastAPI the remote-user is not being populated + username = request.headers.get("remote-user") or "admin" + else: + username = "admin" + + json: dict[str, Any] = body or {} + sub = json.get("sub") + + if not sub: + return JSONResponse( + content={"success": False, "message": "Subscription must be provided."}, + status_code=400, + ) + + try: + User.update(notification_tokens=User.notification_tokens.append(sub)).where( + User.username == username + ).execute() + return JSONResponse( + content=({"success": True, "message": "Successfully saved token."}), + status_code=200, + ) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Could not find user."}), + status_code=404, + ) diff --git a/frigate/api/preview.py b/frigate/api/preview.py new file mode 100644 index 0000000..1047591 --- /dev/null +++ b/frigate/api/preview.py @@ -0,0 +1,193 @@ +"""Preview apis.""" + +import bisect +import logging +import os +import threading +from datetime import UTC, datetime, timedelta + +import pytz +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import JSONResponse + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, +) +from frigate.api.defs.response.preview_response import ( + PreviewFramesResponse, + PreviewsResponse, +) +from frigate.api.defs.tags import Tags +from frigate.const import BASE_DIR, CACHE_DIR, PREVIEW_FRAME_TYPE +from frigate.models import Previews + +logger = logging.getLogger(__name__) + + +router = APIRouter(tags=[Tags.preview]) + + +@router.get( + "/preview/{camera_name}/start/{start_ts}/end/{end_ts}", + response_model=PreviewsResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Get preview clips for time range", + description="""Gets all preview clips for a specified camera and time range. + Returns a list of preview video clips that overlap with the requested time period, + ordered by start time. Use camera_name='all' to get previews from all cameras. + Returns an error if no previews are found.""", +) +def preview_ts( + camera_name: str, + start_ts: float, + end_ts: float, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + """Get all mp4 previews relevant for time period.""" + if camera_name != "all": + if camera_name not in allowed_cameras: + raise HTTPException(status_code=403, detail="Access denied for camera") + camera_list = [camera_name] + else: + camera_list = allowed_cameras + + if not camera_list: + return JSONResponse( + content={"success": False, "message": "No previews found."}, + status_code=404, + ) + + previews = ( + Previews.select( + Previews.camera, + Previews.path, + Previews.duration, + Previews.start_time, + Previews.end_time, + ) + .where( + Previews.start_time.between(start_ts, end_ts) + | Previews.end_time.between(start_ts, end_ts) + | ((start_ts > Previews.start_time) & (end_ts < Previews.end_time)) + ) + .where(Previews.camera << camera_list) + .order_by(Previews.start_time.asc()) + .dicts() + .iterator() + ) + + clips = [] + + preview: Previews + for preview in previews: + clips.append( + { + "camera": preview["camera"], + "src": preview["path"].replace(BASE_DIR, ""), + "type": "video/mp4", + "start": preview["start_time"], + "end": preview["end_time"], + } + ) + + if not clips: + return JSONResponse( + content={ + "success": False, + "message": "No previews found.", + }, + status_code=404, + ) + + return JSONResponse(content=clips, status_code=200) + + +@router.get( + "/preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}", + response_model=PreviewsResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Get preview clips for specific hour", + description="""Gets all preview clips for a specific hour in a given timezone. + Converts the provided date/time from the specified timezone to UTC and retrieves + all preview clips for that hour. Use camera_name='all' to get previews from all cameras. + The tz_name should be a timezone like 'America/New_York' (use commas instead of slashes).""", +) +def preview_hour( + year_month: str, + day: int, + hour: int, + camera_name: str, + tz_name: str, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + """Get all mp4 previews relevant for time period given the timezone""" + parts = year_month.split("-") + start_date = ( + datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC) + - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() + ) + end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) + start_ts = start_date.timestamp() + end_ts = end_date.timestamp() + + return preview_ts(camera_name, start_ts, end_ts, allowed_cameras) + + +# cache one sorted listing of the shared preview_frames dir +_preview_listing_lock = threading.Lock() +_preview_listing_cache: tuple[float, list[str]] = (-1.0, []) + + +def _get_preview_frame_listing(preview_dir: str) -> list[str]: + """Return the sorted preview_frames listing, cached until the dir changes.""" + global _preview_listing_cache + + # mtime bumps when a frame is added or removed, invalidating the cache + mtime = os.stat(preview_dir).st_mtime + cached_mtime, files = _preview_listing_cache + if mtime == cached_mtime: + return files + + with _preview_listing_lock: + # another thread may have refreshed the cache while we waited + cached_mtime, files = _preview_listing_cache + if mtime == cached_mtime: + return files + + files = sorted(entry.name for entry in os.scandir(preview_dir)) + _preview_listing_cache = (mtime, files) + return files + + +@router.get( + "/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames", + response_model=PreviewFramesResponse, + dependencies=[Depends(require_camera_access)], + summary="Get cached preview frame filenames", + description="""Gets a list of cached preview frame filenames for a specific camera and time range. + Returns an array of filenames for preview frames that fall within the specified time period, + sorted in chronological order. These are individual frame images cached for quick preview display.""", +) +def get_preview_frames_from_cache(camera_name: str, start_ts: float, end_ts: float): + """Get list of cached preview frames""" + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{camera_name}-" + start_file = f"{file_start}{start_ts}.{PREVIEW_FRAME_TYPE}" + end_file = f"{file_start}{end_ts}.{PREVIEW_FRAME_TYPE}" + + files = _get_preview_frame_listing(preview_dir) + + # a camera's frames form a contiguous slice of the sorted listing; + # bisect locates it without scanning the whole directory + left = bisect.bisect_left(files, start_file) + right = bisect.bisect_right(files, end_file) + selected_previews = [ + file for file in files[left:right] if file.startswith(file_start) + ] + + return JSONResponse( + content=selected_previews, + status_code=200, + ) diff --git a/frigate/api/record.py b/frigate/api/record.py new file mode 100644 index 0000000..5db257f --- /dev/null +++ b/frigate/api/record.py @@ -0,0 +1,469 @@ +"""Recording APIs.""" + +import datetime as dt +import logging +from datetime import datetime, timedelta +from functools import reduce +from pathlib import Path +from urllib.parse import unquote + +from fastapi import APIRouter, Depends, Request +from fastapi import Path as PathParam +from fastapi.responses import JSONResponse +from peewee import fn, operator + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, + require_role, +) +from frigate.api.defs.query.recordings_query_parameters import ( + MediaRecordingsAvailabilityQueryParams, + MediaRecordingsSummaryQueryParams, + RecordingsDeleteQueryParams, +) +from frigate.api.defs.response.generic_response import GenericResponse +from frigate.api.defs.tags import Tags +from frigate.const import RECORD_DIR +from frigate.models import Event, Recordings +from frigate.util.time import get_dst_transitions + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.recordings]) + + +@router.get("/recordings/storage", dependencies=[Depends(require_role(["admin"]))]) +def get_recordings_storage_usage(request: Request): + recording_stats = request.app.stats_emitter.get_latest_stats()["service"][ + "storage" + ][RECORD_DIR] + + if not recording_stats: + return JSONResponse({}) + + total_mb = recording_stats["total"] + + camera_usages: dict[str, dict] = ( + request.app.storage_maintainer.calculate_camera_usages() + ) + + for camera_name in camera_usages.keys(): + if camera_usages.get(camera_name, {}).get("usage"): + camera_usages[camera_name]["usage_percent"] = ( + camera_usages.get(camera_name, {}).get("usage", 0) / total_mb + ) * 100 + + return JSONResponse(content=camera_usages) + + +@router.get("/recordings/summary", dependencies=[Depends(allow_any_authenticated())]) +def all_recordings_summary( + request: Request, + params: MediaRecordingsSummaryQueryParams = Depends(), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + """Returns true/false by day indicating if recordings exist""" + + cameras = params.cameras + if cameras != "all": + requested = set(unquote(cameras).split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content={}) + camera_list = list(filtered) + else: + camera_list = allowed_cameras + + time_range_query = ( + Recordings.select( + fn.MIN(Recordings.start_time).alias("min_time"), + fn.MAX(Recordings.start_time).alias("max_time"), + ) + .where(Recordings.camera << camera_list) + .dicts() + .get() + ) + + min_time = time_range_query.get("min_time") + max_time = time_range_query.get("max_time") + + if min_time is None or max_time is None: + return JSONResponse(content={}) + + dst_periods = get_dst_transitions(params.timezone, min_time, max_time) + + days: dict[str, bool] = {} + + for period_start, period_end, period_offset in dst_periods: + day_expr = ((Recordings.start_time + period_offset) / 86400).cast("int") + + period_query = ( + Recordings.select(day_expr.alias("day_idx")) + .where( + (Recordings.camera << camera_list) + & (Recordings.end_time >= period_start) + & (Recordings.start_time <= period_end) + ) + .distinct() + .namedtuples() + ) + + for g in period_query: + 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()))) + + +@router.get( + "/{camera_name}/recordings/summary", dependencies=[Depends(require_camera_access)] +) +async def recordings_summary(camera_name: str, timezone: str = "utc"): + """Returns hourly summary for recordings of given camera""" + + time_range_query = ( + Recordings.select( + fn.MIN(Recordings.start_time).alias("min_time"), + fn.MAX(Recordings.start_time).alias("max_time"), + ) + .where(Recordings.camera == camera_name) + .dicts() + .get() + ) + + min_time = time_range_query.get("min_time") + max_time = time_range_query.get("max_time") + + days: dict[str, dict] = {} + + if min_time is None or max_time is None: + return JSONResponse(content=list(days.values())) + + dst_periods = get_dst_transitions(timezone, min_time, max_time) + + for period_start, period_end, period_offset in dst_periods: + hours_offset = int(period_offset / 60 / 60) + minutes_offset = int(period_offset / 60 - hours_offset * 60) + period_hour_modifier = f"{hours_offset} hour" + period_minute_modifier = f"{minutes_offset} minute" + + recording_groups = ( + Recordings.select( + fn.strftime( + "%Y-%m-%d %H", + fn.datetime( + Recordings.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ).alias("hour"), + fn.SUM(Recordings.duration).alias("duration"), + fn.SUM(Recordings.motion).alias("motion"), + fn.SUM(Recordings.objects).alias("objects"), + ) + .where( + (Recordings.camera == camera_name) + & (Recordings.end_time >= period_start) + & (Recordings.start_time <= period_end) + ) + .group_by((Recordings.start_time + period_offset).cast("int") / 3600) + .order_by(Recordings.start_time.desc()) + .namedtuples() + ) + + event_groups = ( + Event.select( + fn.strftime( + "%Y-%m-%d %H", + fn.datetime( + Event.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ).alias("hour"), + fn.COUNT(Event.id).alias("count"), + ) + .where(Event.camera == camera_name, Event.has_clip) + .where( + (Event.start_time >= period_start) & (Event.start_time <= period_end) + ) + .group_by((Event.start_time + period_offset).cast("int") / 3600) + .namedtuples() + ) + + event_map = {g.hour: g.count for g in event_groups} + + for recording_group in recording_groups: + parts = recording_group.hour.split() + hour = parts[1] + day = parts[0] + events_count = event_map.get(recording_group.hour, 0) + hour_data = { + "hour": hour, + "events": events_count, + "motion": recording_group.motion, + "objects": recording_group.objects, + "duration": round(recording_group.duration), + } + if day in days: + # merge counts if already present (edge-case at DST boundary) + days[day]["events"] += events_count or 0 + days[day]["hours"].append(hour_data) + else: + days[day] = { + "events": events_count or 0, + "hours": [hour_data], + "day": day, + } + + return JSONResponse(content=list(days.values())) + + +@router.get("/{camera_name}/recordings", dependencies=[Depends(require_camera_access)]) +async def recordings( + camera_name: str, + after: float = (datetime.now() - timedelta(hours=1)).timestamp(), + before: float = datetime.now().timestamp(), +): + """Return specific camera recordings between the given 'after'/'end' times. If not provided the last hour will be used""" + recordings = ( + Recordings.select( + Recordings.id, + Recordings.start_time, + Recordings.end_time, + Recordings.segment_size, + Recordings.motion, + Recordings.objects, + Recordings.motion_heatmap, + Recordings.duration, + ) + .where( + Recordings.camera == camera_name, + Recordings.end_time >= after, + Recordings.start_time <= before, + ) + .order_by(Recordings.start_time) + .dicts() + .iterator() + ) + + return JSONResponse(content=list(recordings)) + + +@router.get( + "/recordings/unavailable", + response_model=list[dict], + dependencies=[Depends(allow_any_authenticated())], +) +async def no_recordings( + request: Request, + params: MediaRecordingsAvailabilityQueryParams = Depends(), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + """Get time ranges with no recordings.""" + cameras = params.cameras + if cameras != "all": + requested = set(unquote(cameras).split(",")) + camera_list = list(requested.intersection(allowed_cameras)) + else: + camera_list = list(allowed_cameras) + + if not camera_list: + return JSONResponse(content=[]) + + before = params.before or datetime.datetime.now().timestamp() + after = ( + params.after + or (datetime.datetime.now() - datetime.timedelta(hours=1)).timestamp() + ) + scale = params.scale + + clauses = [ + (Recordings.end_time >= after) & (Recordings.start_time <= before), + (Recordings.camera << camera_list), + ] + + # Get recording start times + data: list[Recordings] = ( + Recordings.select(Recordings.start_time, Recordings.end_time) + .where(reduce(operator.and_, clauses)) + .order_by(Recordings.start_time.asc()) + .dicts() + .iterator() + ) + + # Convert recordings to list of (start, end) tuples, ordered by start_time + recordings = [(r["start_time"], r["end_time"]) for r in data] + + # Merge overlapping/adjacent recordings into covered intervals. The query + # orders by start_time, so a single pass merges them + covered: list[tuple[float, float]] = [] + for rec_start, rec_end in recordings: + if covered and rec_start <= covered[-1][1]: + covered[-1] = (covered[-1][0], max(covered[-1][1], rec_end)) + else: + covered.append((rec_start, rec_end)) + + # Iterate through time segments and check if each has any recording + no_recording_segments = [] + current = after + current_gap_start = None + idx = 0 + covered_count = len(covered) + + while current < before: + segment_end = min(current + scale, before) + + # Advance past covered intervals that end before this segment begins; + # they cannot overlap this or any later segment. + while idx < covered_count and covered[idx][1] <= current: + idx += 1 + + # A covered interval overlaps the segment when it starts before the + # segment ends (its end is already known to be > current). + has_recording = idx < covered_count and covered[idx][0] < segment_end + + if not has_recording: + # This segment has no recordings + if current_gap_start is None: + current_gap_start = current # Start a new gap + else: + # This segment has recordings + if current_gap_start is not None: + # End the current gap and append it + no_recording_segments.append( + {"start_time": int(current_gap_start), "end_time": int(current)} + ) + current_gap_start = None + + current = segment_end + + # Append the last gap if it exists + if current_gap_start is not None: + no_recording_segments.append( + {"start_time": int(current_gap_start), "end_time": int(before)} + ) + + return JSONResponse(content=no_recording_segments) + + +@router.delete( + "/recordings/start/{start}/end/{end}", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete recordings", + description="""Deletes recordings within the specified time range. + Recordings can be filtered by cameras and kept based on motion, objects, or audio attributes. + """, +) +async def delete_recordings( + start: float = PathParam(..., description="Start timestamp (unix)"), + end: float = PathParam(..., description="End timestamp (unix)"), + params: RecordingsDeleteQueryParams = Depends(), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + """Delete recordings in the specified time range.""" + if start >= end: + return JSONResponse( + content={ + "success": False, + "message": "Start time must be less than end time.", + }, + status_code=400, + ) + + cameras = params.cameras + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + + if not filtered: + return JSONResponse( + content={ + "success": False, + "message": "No valid cameras found in the request.", + }, + status_code=400, + ) + + camera_list = list(filtered) + else: + camera_list = allowed_cameras + + # Parse keep parameter + keep_set = set() + + if params.keep: + keep_set = set(params.keep.split(",")) + + # Build query to find overlapping recordings + clauses = [ + ( + Recordings.start_time.between(start, end) + | Recordings.end_time.between(start, end) + | ((start > Recordings.start_time) & (end < Recordings.end_time)) + ), + (Recordings.camera << camera_list), + ] + + keep_clauses = [] + + if "motion" in keep_set: + keep_clauses.append(Recordings.motion.is_null(False) & (Recordings.motion > 0)) + + if "object" in keep_set: + keep_clauses.append( + Recordings.objects.is_null(False) & (Recordings.objects > 0) + ) + + if "audio" in keep_set: + keep_clauses.append(Recordings.dBFS.is_null(False)) + + if keep_clauses: + keep_condition = reduce(operator.or_, keep_clauses) + clauses.append(~keep_condition) + + recordings_to_delete = ( + Recordings.select(Recordings.id, Recordings.path) + .where(reduce(operator.and_, clauses)) + .dicts() + .iterator() + ) + + recording_ids = [] + deleted_count = 0 + error_count = 0 + + for recording in recordings_to_delete: + recording_ids.append(recording["id"]) + + try: + Path(recording["path"]).unlink(missing_ok=True) + deleted_count += 1 + except Exception as e: + logger.error(f"Failed to delete recording file {recording['path']}: {e}") + error_count += 1 + + if recording_ids: + max_deletes = 100000 + recording_ids_list = list(recording_ids) + + for i in range(0, len(recording_ids_list), max_deletes): + Recordings.delete().where( + Recordings.id << recording_ids_list[i : i + max_deletes] + ).execute() + + message = f"Successfully deleted {deleted_count} recording(s)." + + if error_count > 0: + message += f" {error_count} file deletion error(s) occurred." + + return JSONResponse( + content={"success": True, "message": message}, + status_code=200, + ) diff --git a/frigate/api/review.py b/frigate/api/review.py new file mode 100644 index 0000000..08705e3 --- /dev/null +++ b/frigate/api/review.py @@ -0,0 +1,774 @@ +"""Review apis.""" + +import datetime +import logging +from functools import reduce +from pathlib import Path + +import pandas as pd +from fastapi import APIRouter, Request +from fastapi.params import Depends +from fastapi.responses import JSONResponse +from peewee import Case, DoesNotExist, IntegrityError, fn, operator +from playhouse.shortcuts import model_to_dict + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + get_current_user, + require_camera_access, + require_role, +) +from frigate.api.defs.query.review_query_parameters import ( + ReviewActivityMotionQueryParams, + ReviewQueryParams, + ReviewSummaryQueryParams, +) +from frigate.api.defs.request.review_body import ReviewModifyMultipleBody +from frigate.api.defs.response.generic_response import GenericResponse +from frigate.api.defs.response.review_response import ( + ReviewActivityMotionResponse, + ReviewSegmentResponse, + ReviewSummaryResponse, +) +from frigate.api.defs.tags import Tags +from frigate.embeddings import EmbeddingsContext +from frigate.models import Recordings, ReviewSegment, UserReviewStatus +from frigate.review.types import SeverityEnum +from frigate.util.time import get_dst_transitions + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.review]) + + +@router.get( + "/review", + response_model=list[ReviewSegmentResponse], + dependencies=[Depends(allow_any_authenticated())], +) +async def review( + params: ReviewQueryParams = Depends(), + current_user: dict = Depends(get_current_user), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + if isinstance(current_user, JSONResponse): + return current_user + + user_id = current_user["username"] + + cameras = params.cameras + labels = params.labels + zones = params.zones + reviewed = params.reviewed + limit = params.limit + severity = params.severity + before = params.before or datetime.datetime.now().timestamp() + after = ( + params.after + or (datetime.datetime.now() - datetime.timedelta(hours=24)).timestamp() + ) + + clauses = [ + (ReviewSegment.start_time < before) + & ((ReviewSegment.end_time.is_null(True)) | (ReviewSegment.end_time > after)) + ] + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + camera_list = list(filtered) + else: + camera_list = allowed_cameras + clauses.append(ReviewSegment.camera << camera_list) + + if labels != "all": + # use matching so segments with multiple labels + # still match on a search where any label matches + label_clauses = [] + filtered_labels = labels.split(",") + + for label in filtered_labels: + label_clauses.append( + (ReviewSegment.data["objects"].cast("text") % f'*"{label}"*') + | (ReviewSegment.data["audio"].cast("text") % f'*"{label}"*') + ) + clauses.append(reduce(operator.or_, label_clauses)) + + if zones != "all": + # use matching so segments with multiple zones + # still match on a search where any zone matches + zone_clauses = [] + filtered_zones = zones.split(",") + + for zone in filtered_zones: + zone_clauses.append( + ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*' + ) + clauses.append(reduce(operator.or_, zone_clauses)) + + if severity: + clauses.append(ReviewSegment.severity == severity) + + # Join with UserReviewStatus to get per-user review status + review_query = ( + ReviewSegment.select( + ReviewSegment.id, + ReviewSegment.camera, + ReviewSegment.start_time, + ReviewSegment.end_time, + ReviewSegment.severity, + ReviewSegment.thumb_path, + ReviewSegment.data, + fn.COALESCE(UserReviewStatus.has_been_reviewed, False).alias( + "has_been_reviewed" + ), + ) + .left_outer_join( + UserReviewStatus, + on=( + (ReviewSegment.id == UserReviewStatus.review_segment) + & (UserReviewStatus.user_id == user_id) + ), + ) + .where(reduce(operator.and_, clauses)) + ) + + # Filter unreviewed items without subquery + if reviewed == 0: + review_query = review_query.where( + (UserReviewStatus.has_been_reviewed == False) + | (UserReviewStatus.has_been_reviewed.is_null()) + ) + elif reviewed == 1: + review_query = review_query.where(UserReviewStatus.has_been_reviewed == True) + + # Apply ordering and limit + review_query = ( + review_query.order_by(ReviewSegment.severity.asc()) + .order_by(ReviewSegment.start_time.desc()) + .limit(limit) + .dicts() + .iterator() + ) + + return JSONResponse(content=[r for r in review_query]) + + +@router.get( + "/review_ids", + response_model=list[ReviewSegmentResponse], + dependencies=[Depends(allow_any_authenticated())], +) +async def review_ids(request: Request, ids: str): + ids = ids.split(",") + + if not ids: + return JSONResponse( + content=({"success": False, "message": "Valid list of ids must be sent"}), + status_code=400, + ) + + for review_id in ids: + try: + review = ReviewSegment.get(ReviewSegment.id == review_id) + await require_camera_access(review.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=( + {"success": False, "message": f"Review {review_id} not found"} + ), + status_code=404, + ) + + try: + reviews = ( + ReviewSegment.select().where(ReviewSegment.id << ids).dicts().iterator() + ) + return JSONResponse(list(reviews)) + except Exception: + return JSONResponse( + content=({"success": False, "message": "Review segments not found"}), + status_code=400, + ) + + +@router.get( + "/review/summary", + response_model=ReviewSummaryResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def review_summary( + params: ReviewSummaryQueryParams = Depends(), + current_user: dict = Depends(get_current_user), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + if isinstance(current_user, JSONResponse): + return current_user + + user_id = current_user["username"] + + day_ago = (datetime.datetime.now() - datetime.timedelta(hours=24)).timestamp() + + cameras = params.cameras + labels = params.labels + zones = params.zones + + clauses = [(ReviewSegment.start_time > day_ago)] + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content={}) + camera_list = list(filtered) + else: + camera_list = allowed_cameras + clauses.append(ReviewSegment.camera << camera_list) + + if labels != "all": + # use matching so segments with multiple labels + # still match on a search where any label matches + label_clauses = [] + filtered_labels = labels.split(",") + + for label in filtered_labels: + label_clauses.append( + (ReviewSegment.data["objects"].cast("text") % f'*"{label}"*') + | (ReviewSegment.data["audio"].cast("text") % f'*"{label}"*') + ) + clauses.append(reduce(operator.or_, label_clauses)) + if zones != "all": + # use matching so segments with multiple zones + # still match on a search where any zone matches + zone_clauses = [] + filtered_zones = zones.split(",") + + for zone in filtered_zones: + zone_clauses.append( + ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*' + ) + clauses.append(reduce(operator.or_, zone_clauses)) + + last_24_query = ( + ReviewSegment.select( + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.alert) + & (UserReviewStatus.has_been_reviewed == True), + 1, + ) + ], + 0, + ) + ).alias("reviewed_alert"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.detection) + & (UserReviewStatus.has_been_reviewed == True), + 1, + ) + ], + 0, + ) + ).alias("reviewed_detection"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.alert), + 1, + ) + ], + 0, + ) + ).alias("total_alert"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.detection), + 1, + ) + ], + 0, + ) + ).alias("total_detection"), + ) + .left_outer_join( + UserReviewStatus, + on=( + (ReviewSegment.id == UserReviewStatus.review_segment) + & (UserReviewStatus.user_id == user_id) + ), + ) + .where(reduce(operator.and_, clauses)) + .dicts() + .get() + ) + + clauses = [] + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content={}) + camera_list = list(filtered) + else: + camera_list = allowed_cameras + clauses.append(ReviewSegment.camera << camera_list) + + if labels != "all": + # use matching so segments with multiple labels + # still match on a search where any label matches + label_clauses = [] + filtered_labels = labels.split(",") + + for label in filtered_labels: + label_clauses.append( + ReviewSegment.data["objects"].cast("text") % f'*"{label}"*' + ) + clauses.append(reduce(operator.or_, label_clauses)) + + # Find the time range of available data + time_range_query = ( + ReviewSegment.select( + fn.MIN(ReviewSegment.start_time).alias("min_time"), + fn.MAX(ReviewSegment.start_time).alias("max_time"), + ) + .where(reduce(operator.and_, clauses) if clauses else True) + .dicts() + .get() + ) + + min_time = time_range_query.get("min_time") + max_time = time_range_query.get("max_time") + + data = { + "last24Hours": last_24_query, + } + + # If no data, return early + if min_time is None or max_time is None: + return JSONResponse(content=data) + + # Get DST transition periods + dst_periods = get_dst_transitions(params.timezone, min_time, max_time) + + day_in_seconds = 60 * 60 * 24 + + # Query each DST period separately with the correct offset + for period_start, period_end, period_offset in dst_periods: + # Calculate hour/minute modifiers for this period + hours_offset = int(period_offset / 60 / 60) + minutes_offset = int(period_offset / 60 - hours_offset * 60) + period_hour_modifier = f"{hours_offset} hour" + period_minute_modifier = f"{minutes_offset} minute" + + # Build clauses including time range for this period + period_clauses = clauses.copy() + period_clauses.append( + (ReviewSegment.start_time >= period_start) + & (ReviewSegment.start_time <= period_end) + ) + + period_query = ( + ReviewSegment.select( + fn.strftime( + "%Y-%m-%d", + fn.datetime( + ReviewSegment.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ).alias("day"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.alert) + & (UserReviewStatus.has_been_reviewed == True), + 1, + ) + ], + 0, + ) + ).alias("reviewed_alert"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.detection) + & (UserReviewStatus.has_been_reviewed == True), + 1, + ) + ], + 0, + ) + ).alias("reviewed_detection"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.alert), + 1, + ) + ], + 0, + ) + ).alias("total_alert"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.detection), + 1, + ) + ], + 0, + ) + ).alias("total_detection"), + ) + .left_outer_join( + UserReviewStatus, + on=( + (ReviewSegment.id == UserReviewStatus.review_segment) + & (UserReviewStatus.user_id == user_id) + ), + ) + .where(reduce(operator.and_, period_clauses)) + .group_by( + (ReviewSegment.start_time + period_offset).cast("int") / day_in_seconds + ) + .order_by(ReviewSegment.start_time.desc()) + ) + + # Merge results from this period + for e in period_query.dicts().iterator(): + day_key = e["day"] + if day_key in data: + # Merge counts if day already exists (edge case at DST boundary) + data[day_key]["reviewed_alert"] += e["reviewed_alert"] or 0 + data[day_key]["reviewed_detection"] += e["reviewed_detection"] or 0 + data[day_key]["total_alert"] += e["total_alert"] or 0 + data[day_key]["total_detection"] += e["total_detection"] or 0 + else: + data[day_key] = e + + return JSONResponse(content=data) + + +@router.post( + "/reviews/viewed", + response_model=GenericResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def set_multiple_reviewed( + request: Request, + body: ReviewModifyMultipleBody, + current_user: dict = Depends(get_current_user), +): + if isinstance(current_user, JSONResponse): + return current_user + + user_id = current_user["username"] + + for review_id in body.ids: + try: + review = ReviewSegment.get(ReviewSegment.id == review_id) + await require_camera_access(review.camera, request=request) + review_status = UserReviewStatus.get( + UserReviewStatus.user_id == user_id, + UserReviewStatus.review_segment == review_id, + ) + # Update based on the reviewed parameter + if review_status.has_been_reviewed != body.reviewed: + review_status.has_been_reviewed = body.reviewed + review_status.save() + except DoesNotExist: + try: + UserReviewStatus.create( + user_id=user_id, + review_segment=ReviewSegment.get(id=review_id), + has_been_reviewed=body.reviewed, + ) + except (DoesNotExist, IntegrityError): + pass + + return JSONResponse( + content=( + { + "success": True, + "message": f"Marked multiple items as {'reviewed' if body.reviewed else 'unreviewed'}", + } + ), + status_code=200, + ) + + +@router.post( + "/reviews/delete", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], +) +def delete_reviews(body: ReviewModifyMultipleBody): + list_of_ids = body.ids + reviews = ( + ReviewSegment.select( + ReviewSegment.camera, + ReviewSegment.start_time, + ReviewSegment.end_time, + ) + .where(ReviewSegment.id << list_of_ids) + .dicts() + .iterator() + ) + recording_ids = [] + + for review in reviews: + start_time = review["start_time"] + end_time = review["end_time"] + camera_name = review["camera"] + recordings = ( + Recordings.select(Recordings.id, Recordings.path) + .where( + Recordings.start_time.between(start_time, end_time) + | Recordings.end_time.between(start_time, end_time) + | ( + (start_time > Recordings.start_time) + & (end_time < Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .dicts() + .iterator() + ) + + for recording in recordings: + Path(recording["path"]).unlink(missing_ok=True) + recording_ids.append(recording["id"]) + + # delete recordings and review segments + Recordings.delete().where(Recordings.id << recording_ids).execute() + ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute() + UserReviewStatus.delete().where( + UserReviewStatus.review_segment << list_of_ids + ).execute() + + return JSONResponse( + content=({"success": True, "message": "Deleted review items."}), status_code=200 + ) + + +@router.get( + "/review/activity/motion", + response_model=list[ReviewActivityMotionResponse], + dependencies=[Depends(allow_any_authenticated())], +) +def motion_activity( + params: ReviewActivityMotionQueryParams = Depends(), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + """Get motion and audio activity.""" + cameras = params.cameras + before = params.before or datetime.datetime.now().timestamp() + after = ( + params.after + or (datetime.datetime.now() - datetime.timedelta(hours=1)).timestamp() + ) + # get scale in seconds + scale = params.scale + + clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)] + clauses.append(Recordings.motion > 0) + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + camera_list = list(filtered) + else: + camera_list = list(allowed_cameras) + + clauses.append(Recordings.camera << camera_list) + + data: list[Recordings] = ( + Recordings.select( + Recordings.camera, + Recordings.start_time, + Recordings.motion, + ) + .where(reduce(operator.and_, clauses)) + .order_by(Recordings.start_time.asc()) + .dicts() + .iterator() + ) + + # resample data using pandas to get activity on scaled basis + df = pd.DataFrame(data, columns=["start_time", "motion", "camera"]) + + if df.empty: + logger.warning("No motion data found for the requested time range") + return JSONResponse(content=[]) + + df = df.astype(dtype={"motion": "float32"}) + + # set date as datetime index + df["start_time"] = pd.to_datetime(df["start_time"], unit="s") + df.set_index(["start_time"], inplace=True) + + # normalize data + motion = df["motion"].resample(f"{scale}s").max().fillna(0.0).to_frame() + + if len(camera_list) == 1: + cameras = df["camera"].resample(f"{scale}s").first().fillna("") + else: + cameras = df["camera"].resample(f"{scale}s").agg(lambda x: ",".join(set(x))) + df = motion.join(cameras) + + length = df.shape[0] + chunk = int(60 * (60 / scale)) + + for i in range(0, length, chunk): + part = df.iloc[i : i + chunk] + min_val, max_val = part["motion"].min(), part["motion"].max() + if min_val != max_val: + df.iloc[i : i + chunk, 0] = ( + part["motion"].sub(min_val).div(max_val - min_val).mul(100).fillna(0) + ) + else: + df.iloc[i : i + chunk, 0] = 0.0 + + # Drop resample gap-fill buckets. The resample above emits a row for every + # {scale}s bucket spanning the range, and buckets with no recording get a + # motion of 0 (from fillna) and an empty camera (from joining an empty set). + df = df[df["camera"] != ""] + + # change types for output + df.index = df.index.astype(int) // (10**9) + normalized = df.reset_index().to_dict("records") + return JSONResponse(content=normalized) + + +@router.get( + "/review/event/{event_id}", + response_model=ReviewSegmentResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def get_review_from_event(request: Request, event_id: str): + try: + review = ReviewSegment.get( + ReviewSegment.data["detections"].cast("text") % f'*"{event_id}"*' + ) + await require_camera_access(review.camera, request=request) + return JSONResponse(model_to_dict(review)) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Review item not found"}, + status_code=404, + ) + + +@router.get( + "/review/{review_id}", + response_model=ReviewSegmentResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def get_review(request: Request, review_id: str): + try: + review = ReviewSegment.get(ReviewSegment.id == review_id) + await require_camera_access(review.camera, request=request) + return JSONResponse(content=model_to_dict(review)) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Review item not found"}, + status_code=404, + ) + + +@router.delete( + "/review/{review_id}/viewed", + response_model=GenericResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def set_not_reviewed( + review_id: str, + current_user: dict = Depends(get_current_user), +): + if isinstance(current_user, JSONResponse): + return current_user + + user_id = current_user["username"] + + try: + review: ReviewSegment = ReviewSegment.get(ReviewSegment.id == review_id) + except DoesNotExist: + return JSONResponse( + content=( + {"success": False, "message": "Review " + review_id + " not found"} + ), + status_code=404, + ) + + try: + user_review = UserReviewStatus.get( + UserReviewStatus.user_id == user_id, + UserReviewStatus.review_segment == review, + ) + # we could update here instead of delete if we need + user_review.delete_instance() + except DoesNotExist: + pass # Already effectively "not reviewed" + + return JSONResponse( + content=({"success": True, "message": f"Set Review {review_id} as not viewed"}), + status_code=200, + ) + + +@router.post( + "/review/summarize/start/{start_ts}/end/{end_ts}", + dependencies=[Depends(require_role(["admin"]))], + description="Use GenAI to summarize review items over a period of time.", +) +def generate_review_summary(request: Request, start_ts: float, end_ts: float): + if not request.app.genai_manager.description_client: + return JSONResponse( + content=( + { + "success": False, + "message": "GenAI must be configured to use this feature.", + } + ), + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + summary = context.generate_review_summary(start_ts, end_ts) + + if summary: + return JSONResponse( + content=({"success": True, "summary": summary}), status_code=200 + ) + else: + return JSONResponse( + content=({"success": False, "message": "Failed to create summary."}), + status_code=500, + ) diff --git a/frigate/app.py b/frigate/app.py new file mode 100644 index 0000000..b0e29ea --- /dev/null +++ b/frigate/app.py @@ -0,0 +1,736 @@ +import datetime +import logging +import multiprocessing as mp +import os +import secrets +import shutil +from collections.abc import Callable +from multiprocessing import Queue +from multiprocessing.managers import DictProxy, SyncManager +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path + +import psutil +import uvicorn +from peewee_migrate import Router +from playhouse.sqlite_ext import SqliteExtDatabase + +from frigate.api.auth import hash_password +from frigate.api.fastapi_app import create_fastapi_app +from frigate.camera import CameraMetrics, PTZMetrics +from frigate.camera.maintainer import CameraMaintainer +from frigate.comms.base_communicator import Communicator +from frigate.comms.dispatcher import Dispatcher +from frigate.comms.event_metadata_updater import EventMetadataPublisher +from frigate.comms.inter_process import InterProcessCommunicator +from frigate.comms.mqtt import MqttClient +from frigate.comms.object_detector_signaler import DetectorProxy +from frigate.comms.webpush import WebPushClient +from frigate.comms.ws import WebSocketClient +from frigate.comms.zmq_proxy import ZmqProxy +from frigate.config.camera.updater import CameraConfigUpdatePublisher +from frigate.config.config import FrigateConfig +from frigate.config.profile_manager import ProfileManager +from frigate.const import ( + CACHE_DIR, + CLIPS_DIR, + CONFIG_DIR, + EXPORT_DIR, + FACE_DIR, + MODEL_CACHE_DIR, + RECORD_DIR, + THUMB_DIR, + TRIGGER_DIR, +) +from frigate.data_processing.types import DataProcessorMetrics +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.debug_replay import ( + DebugReplayManager, + cleanup_replay_cameras, +) +from frigate.embeddings import EmbeddingProcess, EmbeddingsContext +from frigate.events.audio import AudioProcessor +from frigate.events.cleanup import EventCleanup +from frigate.events.maintainer import EventProcessor +from frigate.jobs.export import reap_stale_exports +from frigate.jobs.motion_search import stop_all_motion_search_jobs +from frigate.log import _stop_logging +from frigate.models import ( + Event, + Export, + Previews, + Recordings, + RecordingsToDelete, + Regions, + ReviewSegment, + Timeline, + Trigger, + User, +) +from frigate.object_detection.base import ObjectDetectProcess +from frigate.output.output import OutputProcess +from frigate.ptz.autotrack import PtzAutoTrackerThread +from frigate.ptz.onvif import OnvifController +from frigate.record.cleanup import RecordingCleanup +from frigate.record.export import migrate_exports +from frigate.record.record import RecordProcess +from frigate.review.review import ReviewProcess +from frigate.stats.emitter import StatsEmitter +from frigate.stats.util import stats_init +from frigate.storage import StorageMaintainer +from frigate.timeline import TimelineProcessor +from frigate.track.object_processing import TrackedObjectProcessor +from frigate.util.builtin import empty_and_close_queue +from frigate.util.image import UntrackedSharedMemory +from frigate.util.process import FrigateProcess +from frigate.util.services import set_file_limit +from frigate.version import VERSION +from frigate.watchdog import FrigateWatchdog + +logger = logging.getLogger(__name__) + + +class FrigateApp: + def __init__( + self, config: FrigateConfig, manager: SyncManager, stop_event: MpEvent + ) -> None: + self.metrics_manager = manager + self.audio_process: mp.Process | None = None + self.stop_event = stop_event + self.detection_queue: Queue = mp.Queue() + self.detectors: dict[str, ObjectDetectProcess] = {} + self.detection_shms: list[mp.shared_memory.SharedMemory] = [] + self.log_queue: Queue = mp.Queue() + self.camera_metrics: DictProxy = self.metrics_manager.dict() + self.embeddings_metrics: DataProcessorMetrics | None = ( + DataProcessorMetrics( + self.metrics_manager, list(config.classification.custom.keys()) + ) + if ( + config.semantic_search.enabled + or any( + c.objects.genai.enabled or c.review.genai.enabled + for c in config.cameras.values() + ) + or config.lpr.enabled + or config.face_recognition.enabled + or len(config.classification.custom) > 0 + ) + else None + ) + self.ptz_metrics: dict[str, PTZMetrics] = {} + self.processes: dict[str, int] = {} + self.embeddings: EmbeddingsContext | None = None + self.profile_manager: ProfileManager | None = None + self.config = config + + def ensure_dirs(self) -> None: + dirs = [ + CONFIG_DIR, + RECORD_DIR, + THUMB_DIR, + f"{CLIPS_DIR}/cache", + CACHE_DIR, + MODEL_CACHE_DIR, + EXPORT_DIR, + ] + + if self.config.face_recognition.enabled: + dirs.append(FACE_DIR) + + if self.config.semantic_search.enabled: + dirs.append(TRIGGER_DIR) + + for d in dirs: + if not os.path.exists(d) and not os.path.islink(d): + logger.info(f"Creating directory: {d}") + os.makedirs(d, exist_ok=True) + else: + logger.debug(f"Skipping directory: {d}") + + def init_debug_replay_manager(self) -> None: + self.replay_manager = DebugReplayManager() + + def init_camera_metrics(self) -> None: + # create camera_metrics + for camera_name in self.config.cameras.keys(): + self.camera_metrics[camera_name] = CameraMetrics(self.metrics_manager) + self.ptz_metrics[camera_name] = PTZMetrics( + autotracker_enabled=self.config.cameras[ + camera_name + ].onvif.autotracking.enabled + ) + + def init_queues(self) -> None: + # Queue for cameras to push tracked objects to + # leaving room for 2 extra cameras to be added + self.detected_frames_queue: Queue = mp.Queue( + maxsize=( + sum( + camera.enabled_in_config == True + for camera in self.config.cameras.values() + ) + + 2 + ) + * 2 + ) + + # Queue for timeline events + self.timeline_queue: Queue = mp.Queue() + + def init_database(self) -> None: + def vacuum_db(db: SqliteExtDatabase) -> None: + logger.info("Running database vacuum") + db.execute_sql("VACUUM;") + + try: + with open(f"{CONFIG_DIR}/.vacuum", "w") as f: + f.write(str(datetime.datetime.now().timestamp())) + except PermissionError: + logger.error("Unable to write to /config to save DB state") + + # Migrate DB schema + migrate_db = SqliteExtDatabase(self.config.database.path) + + # Run migrations + del logging.getLogger("peewee_migrate").handlers[:] + router = Router(migrate_db) + + if len(router.diff) > 0: + logger.info("Making backup of DB before migrations...") + shutil.copyfile( + self.config.database.path, + self.config.database.path.replace("frigate.db", "backup.db"), + ) + + router.run() + + # check if vacuum needs to be run + if os.path.exists(f"{CONFIG_DIR}/.vacuum"): + with open(f"{CONFIG_DIR}/.vacuum") as f: + try: + timestamp = round(float(f.readline())) + except Exception: + timestamp = 0 + + if ( + timestamp + < ( + datetime.datetime.now() - datetime.timedelta(weeks=2) + ).timestamp() + ): + vacuum_db(migrate_db) + else: + vacuum_db(migrate_db) + + migrate_db.close() + + def init_go2rtc(self) -> None: + for proc in psutil.process_iter(["pid", "name"]): + if proc.info["name"] == "go2rtc": + logger.info(f"go2rtc process pid: {proc.info['pid']}") + self.processes["go2rtc"] = proc.info["pid"] + + def init_recording_manager(self) -> None: + recording_process = RecordProcess(self.config, self.stop_event) + self.recording_process = recording_process + recording_process.start() + self.processes["recording"] = recording_process.pid or 0 + logger.info(f"Recording process started: {recording_process.pid}") + + def init_review_segment_manager(self) -> None: + review_segment_process = ReviewProcess(self.config, self.stop_event) + self.review_segment_process = review_segment_process + review_segment_process.start() + self.processes["review_segment"] = review_segment_process.pid or 0 + logger.info(f"Review process started: {review_segment_process.pid}") + + def init_embeddings_manager(self) -> None: + # always start the embeddings process + embedding_process = EmbeddingProcess( + self.config, self.embeddings_metrics, self.stop_event + ) + self.embedding_process = embedding_process + embedding_process.start() + self.processes["embeddings"] = embedding_process.pid or 0 + logger.info(f"Embedding process started: {embedding_process.pid}") + + def bind_database(self) -> None: + """Bind db to the main process.""" + # NOTE: all db accessing processes need to be created before the db can be bound to the main process + self.db = SqliteVecQueueDatabase( + self.config.database.path, + pragmas={ + "auto_vacuum": "FULL", # Does not defragment database + "cache_size": -512 * 1000, # 512MB of cache, + "synchronous": "NORMAL", # Safe when using WAL https://www.sqlite.org/pragma.html#pragma_synchronous + }, + timeout=max( + 60, + 10 + * len([c for c in self.config.cameras.values() if c.enabled_in_config]), + ), + load_vec_extension=self.config.semantic_search.enabled, + ) + models = [ + Event, + Export, + Previews, + Recordings, + RecordingsToDelete, + Regions, + ReviewSegment, + Timeline, + User, + Trigger, + ] + self.db.bind(models) + + def check_db_data_migrations(self) -> None: + # check if vacuum needs to be run + if not os.path.exists(f"{CONFIG_DIR}/.exports"): + try: + with open(f"{CONFIG_DIR}/.exports", "w") as f: + f.write(str(datetime.datetime.now().timestamp())) + except PermissionError: + logger.error("Unable to write to /config to save export state") + + migrate_exports(self.config.ffmpeg, list(self.config.cameras.keys())) + + def init_embeddings_client(self) -> None: + # Create a client for other processes to use + self.embeddings = EmbeddingsContext(self.db) + + def init_inter_process_communicator(self) -> None: + self.inter_process_communicator = InterProcessCommunicator() + self.inter_config_updater = CameraConfigUpdatePublisher() + self.event_metadata_updater = EventMetadataPublisher() + self.inter_zmq_proxy = ZmqProxy() + self.detection_proxy = DetectorProxy() + + def init_onvif(self) -> None: + self.onvif_controller = OnvifController(self.config, self.ptz_metrics) + + def init_dispatcher(self) -> None: + comms: list[Communicator] = [] + + if self.config.mqtt.enabled: + comms.append(MqttClient(self.config)) + + notification_cameras = [ + c + for c in self.config.cameras.values() + if c.enabled and c.notifications.enabled_in_config + ] + + if notification_cameras: + comms.append(WebPushClient(self.config, self.stop_event)) + + comms.append(WebSocketClient(self.config)) + comms.append(self.inter_process_communicator) + + self.dispatcher = Dispatcher( + self.config, + self.inter_config_updater, + self.onvif_controller, + self.ptz_metrics, + comms, + ) + + def init_profile_manager(self) -> None: + self.profile_manager = ProfileManager( + self.config, self.inter_config_updater, self.dispatcher + ) + self.dispatcher.profile_manager = self.profile_manager + + def restore_active_profile(self) -> None: + """Re-activate the persisted profile after subscribers are connected. + + ZMQ PUB/SUB drops messages with no subscribers, so activation must + run after every config_updater subscriber is up. + """ + if self.profile_manager is None: + return + + persisted = ProfileManager.load_persisted_profile() + if persisted and any( + persisted in cam.profiles for cam in self.config.cameras.values() + ): + logger.info("Restoring persisted profile '%s'", persisted) + # runtime overrides are layered on top via restore_runtime_state() + self.profile_manager.activate_profile( + persisted, clear_runtime_overrides=False + ) + + def start_detectors(self) -> None: + for name in self.config.cameras.keys(): + try: + largest_frame = max( + [ + det.model.height * det.model.width * 3 + if det.model is not None + else 320 + for det in self.config.detectors.values() + ] + ) + shm_in = UntrackedSharedMemory( + name=name, + create=True, + size=largest_frame, + ) + except FileExistsError: + shm_in = UntrackedSharedMemory(name=name) + + try: + shm_out = UntrackedSharedMemory( + name=f"out-{name}", create=True, size=20 * 6 * 4 + ) + except FileExistsError: + shm_out = UntrackedSharedMemory(name=f"out-{name}") + + self.detection_shms.append(shm_in) + self.detection_shms.append(shm_out) + + for name, detector_config in self.config.detectors.items(): + self.detectors[name] = ObjectDetectProcess( + name, + self.detection_queue, + list(self.config.cameras.keys()), + self.config, + detector_config, + self.stop_event, + ) + + def start_ptz_autotracker(self) -> None: + self.ptz_autotracker_thread = PtzAutoTrackerThread( + self.config, + self.onvif_controller, + self.ptz_metrics, + self.dispatcher, + self.stop_event, + ) + self.ptz_autotracker_thread.start() + + def start_detected_frames_processor(self) -> None: + self.detected_frames_processor = TrackedObjectProcessor( + self.config, + self.dispatcher, + self.detected_frames_queue, + self.ptz_autotracker_thread, + self.stop_event, + ) + self.detected_frames_processor.start() + + def start_video_output_processor(self) -> None: + output_processor = OutputProcess(self.config, self.stop_event) + self.output_processor = output_processor + output_processor.start() + logger.info(f"Output process started: {output_processor.pid}") + + def start_camera_processor(self) -> None: + self.camera_maintainer = CameraMaintainer( + self.config, + self.detection_queue, + self.detected_frames_queue, + self.camera_metrics, + self.ptz_metrics, + self.stop_event, + self.metrics_manager, + ) + self.camera_maintainer.start() + + def start_audio_processor(self) -> None: + self.audio_process = AudioProcessor( + self.config, self.camera_metrics, self.stop_event + ) + self.audio_process.start() + self.processes["audio_detector"] = self.audio_process.pid or 0 + + def start_timeline_processor(self) -> None: + self.timeline_processor = TimelineProcessor( + self.config, self.timeline_queue, self.stop_event + ) + self.timeline_processor.start() + + def start_event_processor(self) -> None: + self.event_processor = EventProcessor( + self.config, + self.timeline_queue, + self.stop_event, + ) + self.event_processor.start() + + def start_event_cleanup(self) -> None: + self.event_cleanup = EventCleanup(self.config, self.stop_event, self.db) + self.event_cleanup.start() + + def start_record_cleanup(self) -> None: + self.record_cleanup = RecordingCleanup(self.config, self.stop_event) + self.record_cleanup.start() + + def start_storage_maintainer(self) -> None: + self.storage_maintainer = StorageMaintainer(self.config, self.stop_event) + self.storage_maintainer.start() + + def start_stats_emitter(self) -> None: + self.stats_emitter = StatsEmitter( + self.config, + stats_init( + self.config, + self.camera_metrics, + self.embeddings_metrics, + self.detectors, + self.processes, + ), + self.stop_event, + ) + self.stats_emitter.start() + + def start_watchdog(self) -> None: + self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event) + + # (attribute on self, key in self.processes, factory) + specs: list[tuple[str, str, Callable[[], FrigateProcess]]] = [ + ( + "embedding_process", + "embeddings", + lambda: EmbeddingProcess( + self.config, self.embeddings_metrics, self.stop_event + ), + ), + ( + "recording_process", + "recording", + lambda: RecordProcess(self.config, self.stop_event), + ), + ( + "review_segment_process", + "review_segment", + lambda: ReviewProcess(self.config, self.stop_event), + ), + ( + "output_processor", + "output", + lambda: OutputProcess(self.config, self.stop_event), + ), + ] + + for attr, key, factory in specs: + if not hasattr(self, attr): + continue + + def on_restart( + proc: FrigateProcess, _attr: str = attr, _key: str = key + ) -> None: + setattr(self, _attr, proc) + self.processes[_key] = proc.pid or 0 + + self.frigate_watchdog.register( + key, getattr(self, attr), factory, on_restart + ) + + self.frigate_watchdog.start() + + def init_auth(self) -> None: + if self.config.auth.enabled: + if User.select().count() == 0: + password = secrets.token_hex(16) + password_hash = hash_password( + password, iterations=self.config.auth.hash_iterations + ) + User.insert( + { + User.username: "admin", + User.role: "admin", + User.password_hash: password_hash, + User.notification_tokens: [], + } + ).execute() + + self.config.auth.admin_first_time_login = True + + logger.info("********************************************************") + logger.info("********************************************************") + logger.info("*** Auth is enabled, but no users exist. ***") + logger.info("*** Created a default user: ***") + logger.info("*** User: admin ***") + logger.info(f"*** Password: {password} ***") + logger.info("********************************************************") + logger.info("********************************************************") + elif self.config.auth.reset_admin_password: + password = secrets.token_hex(16) + password_hash = hash_password( + password, iterations=self.config.auth.hash_iterations + ) + User.replace( + username="admin", + role="admin", + password_hash=password_hash, + notification_tokens=[], + ).execute() + + logger.info("********************************************************") + logger.info("********************************************************") + logger.info("*** Reset admin password set in the config. ***") + logger.info(f"*** Password: {password} ***") + logger.info("********************************************************") + logger.info("********************************************************") + + def start(self) -> None: + logger.info(f"Starting Frigate ({VERSION})") + + # Ensure global state. + self.ensure_dirs() + + # Set soft file limits. + set_file_limit() + + # Start frigate services. + self.init_debug_replay_manager() + self.init_camera_metrics() + self.init_queues() + self.init_database() + self.init_onvif() + self.init_recording_manager() + self.init_review_segment_manager() + self.init_go2rtc() + self.init_embeddings_manager() + self.bind_database() + self.check_db_data_migrations() + + # Clean up any stale replay camera artifacts (filesystem + DB) + cleanup_replay_cameras() + + # Reap any Export rows still marked in_progress from a previous + # session (crash, kill, broken migration). Runs synchronously before + # uvicorn binds so no API request can observe a stale row. + reap_stale_exports() + + self.init_inter_process_communicator() + self.start_detectors() + self.init_dispatcher() + self.init_profile_manager() + self.init_embeddings_client() + self.start_video_output_processor() + self.start_ptz_autotracker() + self.start_detected_frames_processor() + self.start_camera_processor() + self.start_audio_processor() + self.start_storage_maintainer() + self.start_stats_emitter() + self.start_timeline_processor() + self.start_event_processor() + self.start_event_cleanup() + self.start_record_cleanup() + self.start_watchdog() + + # restore persisted runtime overrides on top of config + self.restore_active_profile() + self.dispatcher.restore_runtime_state() + + self.init_auth() + + try: + uvicorn.run( + create_fastapi_app( + self.config, + self.db, + self.embeddings, + self.detected_frames_processor, + self.storage_maintainer, + self.onvif_controller, + self.stats_emitter, + self.event_metadata_updater, + self.inter_config_updater, + self.replay_manager, + self.dispatcher, + self.profile_manager, + ), + host="127.0.0.1", + port=5001, + log_level="error", + ) + finally: + self.stop() + + def stop(self) -> None: + logger.info("Stopping...") + + # used by the docker healthcheck + Path("/dev/shm/.frigate-is-stopping").touch() + + # Cancel any running motion search jobs before setting stop_event + stop_all_motion_search_jobs() + + self.stop_event.set() + + # set an end_time on entries without an end_time before exiting + Event.update( + end_time=datetime.datetime.now().timestamp(), has_snapshot=False + ).where(Event.end_time == None).execute() + ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where( + ReviewSegment.end_time == None + ).execute() + + # stop the audio process + if self.audio_process: + self.audio_process.terminate() + self.audio_process.join() + + # stop the onvif controller + if self.onvif_controller: + self.onvif_controller.close() + + # ensure the detectors are done + for detector in self.detectors.values(): + detector.stop() + + empty_and_close_queue(self.detection_queue) + logger.info("Detection queue closed") + + self.detected_frames_processor.join() + empty_and_close_queue(self.detected_frames_queue) + logger.info("Detected frames queue closed") + + self.timeline_processor.join() + self.event_processor.join() + empty_and_close_queue(self.timeline_queue) + logger.info("Timeline queue closed") + + self.output_processor.terminate() + self.output_processor.join() + + self.recording_process.terminate() + self.recording_process.join() + + self.review_segment_process.terminate() + self.review_segment_process.join() + + self.dispatcher.stop() + self.ptz_autotracker_thread.join() + + self.event_cleanup.join() + self.record_cleanup.join() + self.stats_emitter.join() + self.frigate_watchdog.join() + self.camera_maintainer.join() + self.db.stop() + + # Save embeddings stats to disk + if self.embeddings: + self.embeddings.stop() + + # Stop Communicators + self.inter_process_communicator.stop() + self.inter_config_updater.stop() + self.event_metadata_updater.stop() + self.inter_zmq_proxy.stop() + self.detection_proxy.stop() + + while len(self.detection_shms) > 0: + shm = self.detection_shms.pop() + shm.close() + shm.unlink() + + _stop_logging() + self.metrics_manager.shutdown() diff --git a/frigate/camera/__init__.py b/frigate/camera/__init__.py new file mode 100644 index 0000000..8583165 --- /dev/null +++ b/frigate/camera/__init__.py @@ -0,0 +1,73 @@ +import multiprocessing as mp +import queue +from multiprocessing.managers import SyncManager, ValueProxy +from multiprocessing.sharedctypes import Synchronized +from multiprocessing.synchronize import Event + + +class CameraMetrics: + camera_fps: ValueProxy[float] + detection_fps: ValueProxy[float] + detection_frame: ValueProxy[float] + process_fps: ValueProxy[float] + skipped_fps: ValueProxy[float] + read_start: ValueProxy[float] + audio_rms: ValueProxy[float] + audio_dBFS: ValueProxy[float] + + frame_queue: queue.Queue + + process_pid: ValueProxy[int] + capture_process_pid: ValueProxy[int] + ffmpeg_pid: ValueProxy[int] + reconnects_last_hour: ValueProxy[int] + stalls_last_hour: ValueProxy[int] + + def __init__(self, manager: SyncManager): + self.camera_fps = manager.Value("d", 0) + self.detection_fps = manager.Value("d", 0) + self.detection_frame = manager.Value("d", 0) + self.process_fps = manager.Value("d", 0) + self.skipped_fps = manager.Value("d", 0) + self.read_start = manager.Value("d", 0) + self.audio_rms = manager.Value("d", 0) + self.audio_dBFS = manager.Value("d", 0) + + self.frame_queue = manager.Queue(maxsize=2) + + self.process_pid = manager.Value("i", 0) + self.capture_process_pid = manager.Value("i", 0) + self.ffmpeg_pid = manager.Value("i", 0) + self.reconnects_last_hour = manager.Value("i", 0) + self.stalls_last_hour = manager.Value("i", 0) + + +class PTZMetrics: + autotracker_enabled: Synchronized + + start_time: Synchronized + stop_time: Synchronized + frame_time: Synchronized + zoom_level: Synchronized + max_zoom: Synchronized + min_zoom: Synchronized + + tracking_active: Event + motor_stopped: Event + reset: Event + + def __init__(self, *, autotracker_enabled: bool): + self.autotracker_enabled = mp.Value("i", autotracker_enabled) # type: ignore[assignment] + + self.start_time = mp.Value("d", 0) # type: ignore[assignment] + self.stop_time = mp.Value("d", 0) # type: ignore[assignment] + self.frame_time = mp.Value("d", 0) # type: ignore[assignment] + self.zoom_level = mp.Value("d", 0) # type: ignore[assignment] + self.max_zoom = mp.Value("d", 0) # type: ignore[assignment] + self.min_zoom = mp.Value("d", 0) # type: ignore[assignment] + + self.tracking_active = mp.Event() + self.motor_stopped = mp.Event() + self.reset = mp.Event() + + self.motor_stopped.set() diff --git a/frigate/camera/activity_manager.py b/frigate/camera/activity_manager.py new file mode 100644 index 0000000..bd3474b --- /dev/null +++ b/frigate/camera/activity_manager.py @@ -0,0 +1,326 @@ +"""Manage camera activity and updating listeners.""" + +import datetime +import json +import logging +import random +import string +from collections import Counter +from collections.abc import Callable +from typing import Any + +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.config import CameraConfig, FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) + +logger = logging.getLogger(__name__) + + +class CameraActivityManager: + def __init__( + self, config: FrigateConfig, publish: Callable[[str, Any], None] + ) -> None: + self.config = config + self.publish = publish + self.last_camera_activity: dict[str, dict[str, Any]] = {} + self.camera_all_object_counts: dict[str, Counter] = {} + self.camera_active_object_counts: dict[str, Counter] = {} + self.zone_all_object_counts: dict[str, Counter] = {} + self.zone_active_object_counts: dict[str, Counter] = {} + self.all_zone_labels: dict[str, set[str]] = {} + self.config_subscriber = CameraConfigUpdateSubscriber( + config, + config.cameras, + [CameraConfigUpdateEnum.zones, CameraConfigUpdateEnum.objects], + ) + + for camera_config in config.cameras.values(): + if not camera_config.enabled_in_config: + continue + + self.__init_camera(camera_config) + + def __init_camera(self, camera_config: CameraConfig) -> None: + if camera_config.name is None: + return + + self.last_camera_activity[camera_config.name] = {} + self.camera_all_object_counts[camera_config.name] = Counter() + self.camera_active_object_counts[camera_config.name] = Counter() + + for zone, zone_config in camera_config.zones.items(): + if zone not in self.all_zone_labels: + self.zone_all_object_counts[zone] = Counter() + self.zone_active_object_counts[zone] = Counter() + self.all_zone_labels[zone] = set() + + self.all_zone_labels[zone].update( + zone_config.objects + if zone_config.objects + else camera_config.objects.track + ) + + def __rebuild_zone_labels(self) -> None: + """Rebuild zone label tracking after a runtime zones/objects change.""" + new_zone_labels: dict[str, set[str]] = {} + + for camera_config in self.config.cameras.values(): + if not camera_config.enabled_in_config or camera_config.name is None: + continue + + for zone, zone_config in camera_config.zones.items(): + new_zone_labels.setdefault(zone, set()).update( + zone_config.objects + if zone_config.objects + else camera_config.objects.track + ) + + # drop counters for zones that no longer exist + for zone in list(self.zone_all_object_counts.keys()): + if zone not in new_zone_labels: + self.zone_all_object_counts.pop(zone, None) + self.zone_active_object_counts.pop(zone, None) + + # ensure counters exist for new zones so the first count is published + for zone in new_zone_labels: + self.zone_all_object_counts.setdefault(zone, Counter()) + self.zone_active_object_counts.setdefault(zone, Counter()) + + self.all_zone_labels = new_zone_labels + + def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: + updated_topics = self.config_subscriber.check_for_updates() + + if "zones" in updated_topics or "objects" in updated_topics: + self.__rebuild_zone_labels() + + all_objects: list[dict[str, Any]] = [] + + for camera in new_activity.keys(): + if camera not in self.config.cameras: + continue + + # handle cameras that were added dynamically + if camera not in self.camera_all_object_counts: + self.__init_camera(self.config.cameras[camera]) + + new_objects = new_activity[camera].get("objects", []) + all_objects.extend(new_objects) + + if self.last_camera_activity.get(camera, {}).get("objects") != new_objects: + self.compare_camera_activity(camera, new_objects) + + # run through every zone, getting a count of objects in that zone right now + for zone, labels in self.all_zone_labels.items(): + all_zone_objects = Counter( + obj["label"].replace("-verified", "") + for obj in all_objects + if zone in obj["current_zones"] + ) + active_zone_objects = Counter( + obj["label"].replace("-verified", "") + for obj in all_objects + if zone in obj["current_zones"] and not obj["stationary"] + ) + any_changed = False + + # run through each object and check what topics need to be updated for this zone + for label in labels: + new_count = all_zone_objects[label] + new_active_count = active_zone_objects[label] + + if ( + new_count != self.zone_all_object_counts[zone][label] + or label not in self.zone_all_object_counts[zone] + ): + any_changed = True + self.publish(f"{zone}/{label}", new_count) + self.zone_all_object_counts[zone][label] = new_count + + if ( + new_active_count != self.zone_active_object_counts[zone][label] + or label not in self.zone_active_object_counts[zone] + ): + any_changed = True + self.publish(f"{zone}/{label}/active", new_active_count) + self.zone_active_object_counts[zone][label] = new_active_count + + if any_changed: + self.publish(f"{zone}/all", sum(list(all_zone_objects.values()))) + self.publish( + f"{zone}/all/active", sum(list(active_zone_objects.values())) + ) + + self.last_camera_activity = new_activity + + def compare_camera_activity( + self, camera: str, new_activity: list[dict[str, Any]] + ) -> None: + all_objects = Counter( + obj["label"].replace("-verified", "") for obj in new_activity + ) + active_objects = Counter( + obj["label"].replace("-verified", "") + for obj in new_activity + if not obj["stationary"] + ) + any_changed = False + + # run through each object and check what topics need to be updated + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return + + for label in camera_config.objects.track: + if label in self.config.model.non_logo_attributes: + continue + + new_count = all_objects[label] + new_active_count = active_objects[label] + + if ( + new_count != self.camera_all_object_counts[camera][label] + or label not in self.camera_all_object_counts[camera] + ): + any_changed = True + self.publish(f"{camera}/{label}", new_count) + self.camera_all_object_counts[camera][label] = new_count + + if ( + new_active_count != self.camera_active_object_counts[camera][label] + or label not in self.camera_active_object_counts[camera] + ): + any_changed = True + self.publish(f"{camera}/{label}/active", new_active_count) + self.camera_active_object_counts[camera][label] = new_active_count + + if any_changed: + self.publish(f"{camera}/all", sum(list(all_objects.values()))) + self.publish(f"{camera}/all/active", sum(list(active_objects.values()))) + + def stop(self) -> None: + self.config_subscriber.stop() + + +class AudioActivityManager: + def __init__( + self, config: FrigateConfig, publish: Callable[[str, Any], None] + ) -> None: + self.config = config + self.publish = publish + self.current_audio_detections: dict[str, dict[str, dict[str, Any]]] = {} + self.event_metadata_publisher = EventMetadataPublisher() + + for camera_config in config.cameras.values(): + if not camera_config.audio.enabled_in_config: + continue + + self.__init_camera(camera_config) + + def __init_camera(self, camera_config: CameraConfig) -> None: + if camera_config.name is None: + return + + self.current_audio_detections[camera_config.name] = {} + + def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: + now = datetime.datetime.now().timestamp() + + for camera in new_activity.keys(): + if camera not in self.config.cameras: + continue + + # handle cameras that were added dynamically + if camera not in self.current_audio_detections: + self.__init_camera(self.config.cameras[camera]) + + new_detections = new_activity[camera].get("detections", []) + if self.compare_audio_activity(camera, new_detections, now): + logger.debug(f"Audio detections for {camera}: {new_activity}") + self.publish( + f"{camera}/audio/all", + "ON" if len(self.current_audio_detections[camera]) > 0 else "OFF", + ) + self.publish( + "audio_detections", + json.dumps(self.current_audio_detections), + ) + + def compare_audio_activity( + self, camera: str, new_detections: list[tuple[str, float]], now: float + ) -> bool: + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return False + + max_not_heard = camera_config.audio.max_not_heard + current = self.current_audio_detections[camera] + + any_changed = False + + for label, score in new_detections: + any_changed = True + if label in current: + current[label]["last_detection"] = now + current[label]["score"] = score + else: + rand_id = "".join( + random.choices(string.ascii_lowercase + string.digits, k=6) + ) + event_id = f"{now}-{rand_id}" + self.publish(f"{camera}/audio/{label}", "ON") + + self.event_metadata_publisher.publish( + ( + now, + camera, + label, + event_id, + True, + score, + None, + None, + "audio", + {}, + None, + ), + EventMetadataTypeEnum.manual_event_create.value, + ) + current[label] = { + "id": event_id, + "score": score, + "last_detection": now, + } + + # expire detections + for label in list(current.keys()): + if now - current[label]["last_detection"] > max_not_heard: + any_changed = True + self.publish(f"{camera}/audio/{label}", "OFF") + + self.event_metadata_publisher.publish( + (current[label]["id"], now), + EventMetadataTypeEnum.manual_event_end.value, + ) + del current[label] + + return any_changed + + def expire_all(self, camera: str) -> None: + now = datetime.datetime.now().timestamp() + current = self.current_audio_detections.get(camera, {}) + + for label in list(current.keys()): + self.publish(f"{camera}/audio/{label}", "OFF") + + self.event_metadata_publisher.publish( + (current[label]["id"], now), + EventMetadataTypeEnum.manual_event_end.value, + ) + del current[label] diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py new file mode 100644 index 0000000..ea8df7b --- /dev/null +++ b/frigate/camera/maintainer.py @@ -0,0 +1,326 @@ +"""Create and maintain camera processes / management.""" + +import logging +import multiprocessing as mp +import threading +from multiprocessing import Queue +from multiprocessing.managers import DictProxy, SyncManager +from multiprocessing.synchronize import Event as MpEvent + +from frigate.camera import CameraMetrics, PTZMetrics +from frigate.config import FrigateConfig +from frigate.config.camera import CameraConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import REPLAY_CAMERA_PREFIX +from frigate.models import Regions +from frigate.util.builtin import empty_and_close_queue +from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory +from frigate.util.object import get_camera_regions_grid +from frigate.util.services import calculate_shm_requirements +from frigate.video import CameraCapture, CameraTracker + +logger = logging.getLogger(__name__) + + +class CameraMaintainer(threading.Thread): + def __init__( + self, + config: FrigateConfig, + detection_queue: Queue, + detected_frames_queue: Queue, + camera_metrics: DictProxy, + ptz_metrics: dict[str, PTZMetrics], + stop_event: MpEvent, + metrics_manager: SyncManager, + ): + super().__init__(name="camera_processor") + self.config = config + self.detection_queue = detection_queue + self.detected_frames_queue = detected_frames_queue + self.stop_event = stop_event + self.camera_metrics = camera_metrics + self.ptz_metrics = ptz_metrics + self.frame_manager = SharedMemoryFrameManager() + self.region_grids: dict[str, list[list[dict[str, int]]]] = {} + self.update_subscriber = CameraConfigUpdateSubscriber( + self.config, + {}, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.refresh, + ], + ) + self.shm_count = self.__calculate_shm_frame_count() + self.camera_processes: dict[str, mp.Process] = {} + self.capture_processes: dict[str, mp.Process] = {} + self.camera_stop_events: dict[str, MpEvent] = {} + self.metrics_manager = metrics_manager + + def __ensure_camera_stop_event(self, camera: str) -> MpEvent: + camera_stop_event = self.camera_stop_events.get(camera) + + if camera_stop_event is None: + camera_stop_event = mp.Event() + self.camera_stop_events[camera] = camera_stop_event + else: + camera_stop_event.clear() + + return camera_stop_event + + def __init_historical_regions(self) -> None: + # delete region grids for removed or renamed cameras + cameras = list(self.config.cameras.keys()) + Regions.delete().where(~(Regions.camera << cameras)).execute() + + # create or update region grids for each camera + for camera in self.config.cameras.values(): + assert camera.name is not None + self.region_grids[camera.name] = get_camera_regions_grid( + camera.name, + camera.detect, + max(self.config.model.width, self.config.model.height), + ) + + def __calculate_shm_frame_count(self) -> int: + shm_stats = calculate_shm_requirements(self.config) + + if not shm_stats: + # /dev/shm not available + return 0 + + logger.debug( + f"Calculated total camera size {shm_stats['available']} / " + f"{shm_stats['camera_frame_size']} :: {shm_stats['shm_frame_count']} " + f"frames for each camera in SHM" + ) + + if shm_stats["shm_frame_count"] < 20: + logger.warning( + f"The current SHM size of {shm_stats['total']}MB is too small, " + f"recommend increasing it to at least {shm_stats['min_shm']}MB." + ) + + return int(shm_stats["shm_frame_count"]) + + def __start_camera_processor( + self, name: str, config: CameraConfig, runtime: bool = False + ) -> None: + if not config.enabled_in_config: + logger.info(f"Camera processor not started for disabled camera {name}") + return + + camera_stop_event = self.__ensure_camera_stop_event(name) + + if runtime: + self.camera_metrics[name] = CameraMetrics(self.metrics_manager) + self.ptz_metrics[name] = PTZMetrics(autotracker_enabled=False) + self.region_grids[name] = get_camera_regions_grid( + name, + config.detect, + max(self.config.model.width, self.config.model.height), + ) + + try: + largest_frame = max( + [ + det.model.height * det.model.width * 3 + if det.model is not None + else 320 + for det in self.config.detectors.values() + ] + ) + UntrackedSharedMemory(name=f"out-{name}", create=True, size=20 * 6 * 4) + UntrackedSharedMemory( + name=name, + create=True, + size=largest_frame, + ) + except FileExistsError: + pass + + camera_process = CameraTracker( + config, + self.config.model, + self.config.model.merged_labelmap, + self.detection_queue, + self.detected_frames_queue, + self.camera_metrics[name], + self.ptz_metrics[name], + self.region_grids[name], + camera_stop_event, + self.config.logger, + ) + self.camera_processes[name] = camera_process + camera_process.start() + self.camera_metrics[name].process_pid.value = camera_process.pid + logger.info(f"Camera processor started for {name}: {camera_process.pid}") + + def __start_camera_capture( + self, name: str, config: CameraConfig, runtime: bool = False + ) -> None: + if not config.enabled_in_config: + logger.info(f"Capture process not started for disabled camera {name}") + return + + camera_stop_event = self.__ensure_camera_stop_event(name) + + # pre-create shms + count = 10 if runtime else self.shm_count + for i in range(count): + frame_size = config.frame_shape_yuv[0] * config.frame_shape_yuv[1] + self.frame_manager.create(f"{config.name}_frame{i}", frame_size) + + capture_process = CameraCapture( + config, + count, + self.camera_metrics[name], + camera_stop_event, + self.config.logger, + ) + capture_process.daemon = True + self.capture_processes[name] = capture_process + capture_process.start() + self.camera_metrics[name].capture_process_pid.value = capture_process.pid + logger.info(f"Capture process started for {name}: {capture_process.pid}") + + def __stop_camera_capture_process(self, camera: str) -> None: + capture_process = self.capture_processes.get(camera) + if capture_process is not None: + logger.info(f"Waiting for capture process for {camera} to stop") + camera_stop_event = self.camera_stop_events.get(camera) + + if camera_stop_event is not None: + camera_stop_event.set() + + capture_process.join(timeout=10) + if capture_process.is_alive(): + logger.warning( + f"Capture process for {camera} didn't exit, forcing termination" + ) + capture_process.terminate() + capture_process.join() + + def __unlink_camera_frame_slots(self, camera: str) -> None: + """Drop the camera's per-frame YUV SHM segments from this + process's frame_manager and unlink them at the OS level. + + Safe to call after the camera's capture/processor subprocesses + have been joined — they no longer hold mappings, so unlink frees + the segments immediately. Other long-lived processes that opened + these slots will continue using their existing mappings until + they call frame_manager.get with a shape that no longer fits + (the get path drops and reopens stale refs). + """ + prefix = f"{camera}_frame" + names = [n for n in list(self.frame_manager.shm_store) if n.startswith(prefix)] + for name in names: + try: + self.frame_manager.delete(name) + except Exception as exc: + logger.debug("Could not unlink SHM %s: %s", name, exc) + + def __stop_camera_process(self, camera: str) -> None: + camera_process = self.camera_processes.get(camera) + if camera_process is not None: + logger.info(f"Waiting for process for {camera} to stop") + camera_stop_event = self.camera_stop_events.get(camera) + + if camera_stop_event is not None: + camera_stop_event.set() + + camera_process.join(timeout=10) + if camera_process.is_alive(): + logger.warning(f"Process for {camera} didn't exit, forcing termination") + camera_process.terminate() + camera_process.join() + logger.info(f"Closing frame queue for {camera}") + empty_and_close_queue(self.camera_metrics[camera].frame_queue) + + def run(self) -> None: + self.__init_historical_regions() + + # start camera processes + for camera, config in self.config.cameras.items(): + self.__start_camera_processor(camera, config) + self.__start_camera_capture(camera, config) + + while not self.stop_event.wait(1): + updates = self.update_subscriber.check_for_updates() + + for update_type, updated_cameras in updates.items(): + if update_type == CameraConfigUpdateEnum.add.name: + for camera in updated_cameras: + if ( + camera in self.camera_processes + or camera in self.capture_processes + ): + continue + + self.__start_camera_processor( + camera, + self.update_subscriber.camera_configs[camera], + runtime=True, + ) + self.__start_camera_capture( + camera, + self.update_subscriber.camera_configs[camera], + runtime=True, + ) + elif update_type == CameraConfigUpdateEnum.remove.name: + for camera in updated_cameras: + self.__stop_camera_capture_process(camera) + self.__stop_camera_process(camera) + self.__unlink_camera_frame_slots(camera) + self.capture_processes.pop(camera, None) + self.camera_processes.pop(camera, None) + self.camera_stop_events.pop(camera, None) + self.region_grids.pop(camera, None) + self.camera_metrics.pop(camera, None) + self.ptz_metrics.pop(camera, None) + elif update_type == CameraConfigUpdateEnum.refresh.name: + # Recycle replay cameras so detect width/height/fps + # propagate through ffmpeg args, SHM sizing, and the + # region grid. Regular cameras detect change still + # requires a full restart. + for camera in updated_cameras: + if not camera.startswith(REPLAY_CAMERA_PREFIX): + continue + + new_config = self.update_subscriber.camera_configs.get(camera) + if new_config is None: + # remove arrived in the same batch + continue + + if ( + camera not in self.camera_processes + and camera not in self.capture_processes + ): + continue + + # rebuild ffmpeg cmds on the shared config so the + # new subprocesses spawn with current args + new_config.recreate_ffmpeg_cmds() + + self.__stop_camera_capture_process(camera) + self.__stop_camera_process(camera) + self.__unlink_camera_frame_slots(camera) + self.capture_processes.pop(camera, None) + self.camera_processes.pop(camera, None) + + self.__start_camera_processor(camera, new_config, runtime=True) + self.__start_camera_capture(camera, new_config, runtime=True) + + # ensure the capture processes are done + for camera in self.capture_processes.keys(): + self.__stop_camera_capture_process(camera) + + # ensure the camera processors are done + for camera in self.camera_processes.keys(): + self.__stop_camera_process(camera) + + self.update_subscriber.stop() + self.frame_manager.cleanup() diff --git a/frigate/camera/state.py b/frigate/camera/state.py new file mode 100644 index 0000000..c0d0f8b --- /dev/null +++ b/frigate/camera/state.py @@ -0,0 +1,662 @@ +"""Maintains state of camera.""" + +import datetime +import logging +import os +import threading +from collections import defaultdict +from collections.abc import Callable +from typing import Any + +import cv2 +import numpy as np + +from frigate.config import ( + FrigateConfig, + ZoomingModeEnum, +) +from frigate.const import CLIPS_DIR, THUMB_DIR +from frigate.ptz.autotrack import PtzAutoTrackerThread +from frigate.track.tracked_object import TrackedObject +from frigate.util.image import ( + SharedMemoryFrameManager, + draw_box_with_label, + draw_timestamp, + is_better_thumbnail, + is_label_printable, +) + +logger = logging.getLogger(__name__) + + +class CameraState: + def __init__( + self, + name: str, + config: FrigateConfig, + frame_manager: SharedMemoryFrameManager, + ptz_autotracker_thread: PtzAutoTrackerThread, + ) -> None: + self.name = name + self.config = config + self.camera_config = config.cameras[name] + self.frame_manager = frame_manager + self.best_objects: dict[str, TrackedObject] = {} + self.tracked_objects: dict[str, TrackedObject] = {} + self.frame_cache: dict[float, dict[str, Any]] = {} + self.zone_objects: defaultdict[str, list[Any]] = defaultdict(list) + self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8) + self._last_frame_shape: tuple[int, int] = self.camera_config.frame_shape_yuv + self.current_frame_lock = threading.Lock() + self.current_frame_time = 0.0 + self.motion_boxes: list[tuple[int, int, int, int]] = [] + self.regions: list[tuple[int, int, int, int]] = [] + self.previous_frame_id: str | None = None + self.callbacks: defaultdict[str, list[Callable]] = defaultdict(list) + self.ptz_autotracker_thread = ptz_autotracker_thread + self.prev_enabled = self.camera_config.enabled + + # Minimum object area thresholds for fast-tracking updates to secondary + # face/LPR pipelines when using a model without built-in detection. + self.face_recognition_min_obj_area: int = 0 + self.lpr_min_obj_area: int = 0 + + if ( + self.camera_config.face_recognition.enabled + and "face" not in config.objects.all_objects + ): + # A face is roughly 1/8 of person box area; use a conservative + # multiplier so fast-tracking starts slightly before the optimal zone + self.face_recognition_min_obj_area = ( + self.camera_config.face_recognition.min_area * 6 + ) + + if ( + self.camera_config.lpr.enabled + and "license_plate" not in self.camera_config.objects.track + ): + # A plate is a smaller fraction of a vehicle box; use ~20x multiplier + self.lpr_min_obj_area = self.camera_config.lpr.min_area * 20 + + def get_current_frame(self, draw_options: dict[str, Any] = {}) -> np.ndarray: + with self.current_frame_lock: + frame_copy = np.copy(self._current_frame) + frame_time = self.current_frame_time + tracked_objects = {k: v.to_dict() for k, v in self.tracked_objects.items()} + motion_boxes = self.motion_boxes.copy() + regions = self.regions.copy() + + frame_copy = cv2.cvtColor(frame_copy, cv2.COLOR_YUV2BGR_I420) # type: ignore[assignment] + # draw on the frame + if draw_options.get("mask"): + mask_overlay = np.where(self.camera_config.motion.rasterized_mask == [0]) # type: ignore[attr-defined] + frame_copy[mask_overlay] = [0, 0, 0] + + if draw_options.get("bounding_boxes"): + # draw the bounding boxes on the frame + for obj in tracked_objects.values(): + if obj["frame_time"] == frame_time: + if obj["stationary"]: + color = (220, 220, 220) + thickness = 1 + else: + thickness = 2 + color = self.config.model.colormap.get( + obj["label"], (255, 255, 255) + ) + else: + thickness = 1 + color = (255, 0, 0) + + # draw thicker box around ptz autotracked object + if ( + self.camera_config.onvif.autotracking.enabled + and self.ptz_autotracker_thread.ptz_autotracker.autotracker_init[ + self.name + ] + and self.ptz_autotracker_thread.ptz_autotracker.tracked_object[ + self.name + ] + is not None + and obj["id"] + == self.ptz_autotracker_thread.ptz_autotracker.tracked_object[ + self.name + ].obj_data["id"] # type: ignore[attr-defined] + and obj["frame_time"] == frame_time + ): + thickness = 5 + color = self.config.model.colormap.get( + obj["label"], (255, 255, 255) + ) + + # debug autotracking zooming - show the zoom factor box + if ( + self.camera_config.onvif.autotracking.zooming + != ZoomingModeEnum.disabled + and self.camera_config.detect.width is not None + and self.camera_config.detect.height is not None + ): + max_target_box = self.ptz_autotracker_thread.ptz_autotracker.tracked_object_metrics[ + self.name + ]["max_target_box"] # type: ignore[index] + side_length = max_target_box * ( + max( + self.camera_config.detect.width, + self.camera_config.detect.height, + ) + ) + + centroid_x = (obj["box"][0] + obj["box"][2]) // 2 + centroid_y = (obj["box"][1] + obj["box"][3]) // 2 + top_left = ( + int(centroid_x - side_length // 2), + int(centroid_y - side_length // 2), + ) + bottom_right = ( + int(centroid_x + side_length // 2), + int(centroid_y + side_length // 2), + ) + cv2.rectangle( + frame_copy, + top_left, + bottom_right, + (255, 255, 0), + 2, + ) + + # draw the bounding boxes on the frame + box = obj["box"] + text = ( + obj["sub_label"][0] + if ( + obj.get("sub_label") and is_label_printable(obj["sub_label"][0]) + ) + else obj.get("recognized_license_plate", [None])[0] + if ( + obj.get("recognized_license_plate") + and obj["recognized_license_plate"][0] + ) + else obj["label"] + ) + draw_box_with_label( + frame_copy, + box[0], + box[1], + box[2], + box[3], + text, + f"{obj['score']:.0%} {int(obj['area'])}" + + ( + f" {float(obj['current_estimated_speed']):.1f}" + if obj["current_estimated_speed"] != 0 + else "" + ), + thickness=thickness, + color=color, + ) + + # draw any attributes + for attribute in obj["current_attributes"]: + box = attribute["box"] + box_area = int((box[2] - box[0]) * (box[3] - box[1])) + draw_box_with_label( + frame_copy, + box[0], + box[1], + box[2], + box[3], + attribute["label"], + f"{attribute['score']:.0%} {str(box_area)}", + thickness=thickness, + color=color, + ) + + if draw_options.get("regions"): + for region in regions: + cv2.rectangle( + frame_copy, + (region[0], region[1]), + (region[2], region[3]), + (0, 255, 0), + 2, + ) + + if draw_options.get("zones"): + for name, zone in self.camera_config.zones.items(): + # skip disabled zones + if not zone.enabled: + continue + + thickness = ( + 8 + if any( + name in obj["current_zones"] for obj in tracked_objects.values() + ) + else 2 + ) + cv2.drawContours(frame_copy, [zone.contour], -1, zone.color, thickness) + + if draw_options.get("motion_boxes"): + for m_box in motion_boxes: + cv2.rectangle( + frame_copy, + (m_box[0], m_box[1]), + (m_box[2], m_box[3]), + (0, 0, 255), + 2, + ) + + if draw_options.get("timestamp"): + ts_color = self.camera_config.timestamp_style.color + draw_timestamp( + frame_copy, + frame_time, + self.camera_config.timestamp_style.format, + font_effect=self.camera_config.timestamp_style.effect, + font_thickness=self.camera_config.timestamp_style.thickness, + font_color=(ts_color.blue, ts_color.green, ts_color.red), + position=self.camera_config.timestamp_style.position, + ) + + if draw_options.get("paths"): + for obj in tracked_objects.values(): + if obj["frame_time"] == frame_time and obj["path_data"]: + color = self.config.model.colormap.get( + obj["label"], (255, 255, 255) + ) + + path_points = [ + ( + int(point[0][0] * self.camera_config.detect.width), + int(point[0][1] * self.camera_config.detect.height), + ) + for point in obj["path_data"] + ] + + for point in path_points: + cv2.circle(frame_copy, point, 5, color, -1) + + for i in range(1, len(path_points)): + cv2.line( + frame_copy, + path_points[i - 1], + path_points[i], + color, + 2, + ) + + bottom_center = ( + int((obj["box"][0] + obj["box"][2]) / 2), + int(obj["box"][3]), + ) + cv2.line( + frame_copy, + path_points[-1], + bottom_center, + color, + 2, + ) + + return frame_copy + + def finished(self, obj_id: str) -> None: + del self.tracked_objects[obj_id] + + def on(self, event_type: str, callback: Callable[..., Any]) -> None: + self.callbacks[event_type].append(callback) + + def _discard_stale_resolution_state( + self, current_detections: dict[str, dict[str, Any]] + ) -> bool: + """Drop tracked state when the camera's detect resolution has + changed, and signal the caller to skip this batch if it contains + out-of-bounds boxes from the pre-recycle detect process. + + Returns True when the batch should be skipped entirely. + """ + # detect resolution changed — drop tracked state so old-grid + # boxes don't leak through end-callbacks + current_shape = self.camera_config.frame_shape_yuv + if current_shape != self._last_frame_shape: + logger.debug( + f"{self.name}: detect resolution changed {self._last_frame_shape} -> {current_shape}, dropping tracked state" + ) + with self.current_frame_lock: + self.tracked_objects.clear() + self.motion_boxes = [] + self.regions = [] + self._last_frame_shape = current_shape + + # drop in-flight batches from the pre-recycle detect process + # whose boxes exceed the current detect resolution + detect = self.camera_config.detect + if detect.width is not None and detect.height is not None: + for obj in current_detections.values(): + box = obj.get("box") + if box and (box[2] > detect.width or box[3] > detect.height): + logger.debug( + f"{self.name}: dropping stale-resolution detection batch (box {box} exceeds {detect.width}x{detect.height})" + ) + return True + + return False + + def update( + self, + frame_name: str, + frame_time: float, + current_detections: dict[str, dict[str, Any]], + motion_boxes: list[tuple[int, int, int, int]], + regions: list[tuple[int, int, int, int]], + ) -> None: + if self._discard_stale_resolution_state(current_detections): + return + + current_frame = self.frame_manager.get( + frame_name, self.camera_config.frame_shape_yuv + ) + + tracked_objects = self.tracked_objects.copy() + current_ids = set(current_detections.keys()) + previous_ids = set(tracked_objects.keys()) + removed_ids = previous_ids.difference(current_ids) + new_ids = current_ids.difference(previous_ids) + updated_ids = current_ids.intersection(previous_ids) + + for id in new_ids: + logger.debug(f"{self.name}: New tracked object ID: {id}") + new_obj = tracked_objects[id] = TrackedObject( + self.config.model, + self.camera_config, + self.config.ui, + self.frame_cache, + current_detections[id], + ) + + # Skip caching when the frame buffer isn't readable — e.g. + # frame_manager.get returned None because the SHM segment was + # unlinked or hasn't been recreated yet during a camera + # add/remove cycle. + if current_frame is not None: + logger.debug( + f"{self.name}: New object, adding {frame_time} to frame cache for {id}" + ) + self.frame_cache[frame_time] = { + "frame": np.copy(current_frame), + "object_id": id, + } + + # save initial thumbnail data and best object + thumbnail_data = { + "frame_time": frame_time, + "box": new_obj.obj_data["box"], + "area": new_obj.obj_data["area"], + "region": new_obj.obj_data["region"], + "score": new_obj.obj_data["score"], + "attributes": new_obj.obj_data["attributes"], + "current_estimated_speed": 0, + "velocity_angle": 0, + "path_data": [], + "recognized_license_plate": None, + "recognized_license_plate_score": None, + } + new_obj.thumbnail_data = thumbnail_data + tracked_objects[id].thumbnail_data = thumbnail_data + object_type = new_obj.obj_data["label"] + + # call event handlers + self.send_mqtt_snapshot(new_obj, object_type) + + for c in self.callbacks["start"]: + c(self.name, new_obj, frame_name) + + for id in updated_ids: + updated_obj = tracked_objects[id] + thumb_update, significant_update, path_update, autotracker_update = ( + updated_obj.update( + frame_time, current_detections[id], current_frame is not None + ) + ) + + if autotracker_update or significant_update: + for c in self.callbacks["autotrack"]: + c(self.name, updated_obj, frame_name) + + if thumb_update and current_frame is not None: + # ensure this frame is stored in the cache + if ( + updated_obj.thumbnail_data is not None + and updated_obj.thumbnail_data["frame_time"] == frame_time + and frame_time not in self.frame_cache + ): + logger.debug( + f"{self.name}: Existing object, adding {frame_time} to frame cache for {id}" + ) + self.frame_cache[frame_time] = { + "frame": np.copy(current_frame), + "object_id": id, + } + + updated_obj.last_updated = frame_time + + # Determine the staleness threshold for publishing updates. + # Fast-track to 1s for objects in the optimal size range for + # secondary face/LPR recognition that don't yet have a sub_label. + obj_area = updated_obj.obj_data.get("area", 0) + obj_label = updated_obj.obj_data.get("label") + publish_threshold = 5 + + if ( + obj_label == "person" + and self.face_recognition_min_obj_area > 0 + and obj_area >= self.face_recognition_min_obj_area + and updated_obj.obj_data.get("sub_label") is None + ) or ( + obj_label in ("car", "motorcycle") + and self.lpr_min_obj_area > 0 + and obj_area >= self.lpr_min_obj_area + and updated_obj.obj_data.get("sub_label") is None + and updated_obj.obj_data.get("recognized_license_plate") is None + ): + publish_threshold = 1 + + if ( + ( + frame_time - updated_obj.last_published > publish_threshold + and updated_obj.last_updated > updated_obj.last_published + ) + or significant_update + or path_update + ): + # call event handlers + for c in self.callbacks["update"]: + c(self.name, updated_obj, frame_name) + updated_obj.last_published = frame_time + + # send MQTT snapshot when object first enters a required zone, + # since the initial snapshot at creation time is blocked before + # zone evaluation has run + if updated_obj.new_zone_entered and not updated_obj.false_positive: + mqtt_required = self.camera_config.mqtt.required_zones + if mqtt_required and set(updated_obj.entered_zones) & set( + mqtt_required + ): + object_type = updated_obj.obj_data["label"] + self.send_mqtt_snapshot(updated_obj, object_type) + updated_obj.new_zone_entered = False + + for id in removed_ids: + # publish events to mqtt + removed_obj = tracked_objects[id] + if "end_time" not in removed_obj.obj_data: + removed_obj.obj_data["end_time"] = frame_time + logger.debug(f"{self.name}: end callback for object {id}") + for c in self.callbacks["end"]: + c(self.name, removed_obj, frame_name) + + # TODO: can i switch to looking this up and only changing when an event ends? + # maintain best objects + camera_activity: dict[str, Any] = { + "motion": len(motion_boxes) > 0, + "objects": [], + } + + for obj in tracked_objects.values(): + object_type = obj.obj_data["label"] + active = obj.is_active() + + if not obj.false_positive: + label = object_type + sub_label = None + + if obj.obj_data.get("sub_label"): + if obj.obj_data["sub_label"][0] in self.config.model.all_attributes: + label = obj.obj_data["sub_label"][0] + else: + label = f"{object_type}-verified" + sub_label = obj.obj_data["sub_label"][0] + + camera_activity["objects"].append( + { + "id": obj.obj_data["id"], + "label": label, + "stationary": not active, + "area": obj.obj_data["area"], + "ratio": obj.obj_data["ratio"], + "score": obj.obj_data["score"], + "sub_label": sub_label, + "current_zones": obj.current_zones, + } + ) + + # if we don't have access to the current frame or + # if the object's thumbnail is not from the current frame, skip + if ( + current_frame is None + or obj.thumbnail_data is None + or obj.false_positive + or obj.thumbnail_data["frame_time"] != frame_time + ): + continue + + if object_type in self.best_objects: + current_best = self.best_objects[object_type] + now = datetime.datetime.now().timestamp() + # if the object is a higher score than the current best score + # or the current object is older than desired, use the new object + if ( + current_best.thumbnail_data is not None + and obj.thumbnail_data is not None + and is_better_thumbnail( + object_type, + current_best.thumbnail_data, + obj.thumbnail_data, + self.camera_config.frame_shape, + ) + or ( + current_best.thumbnail_data is not None + and (now - current_best.thumbnail_data["frame_time"]) + > self.camera_config.best_image_timeout + ) + ): + self.send_mqtt_snapshot(obj, object_type) + else: + self.send_mqtt_snapshot(obj, object_type) + + for c in self.callbacks["camera_activity"]: + c(self.name, camera_activity) + + # cleanup thumbnail frame cache + current_thumb_frames = { + obj.thumbnail_data["frame_time"] + for obj in tracked_objects.values() + if obj.thumbnail_data is not None + } + current_best_frames = { + obj.thumbnail_data["frame_time"] + for obj in self.best_objects.values() + if obj.thumbnail_data is not None + } + thumb_frames_to_delete = [ + t + for t in self.frame_cache.keys() + if t not in current_thumb_frames and t not in current_best_frames + ] + if len(thumb_frames_to_delete) > 0: + logger.debug(f"{self.name}: Current frame cache contents:") + for k, v in self.frame_cache.items(): + logger.debug(f" frame time: {k}, object id: {v['object_id']}") + for obj_id, obj in tracked_objects.items(): + thumb_time = ( + obj.thumbnail_data["frame_time"] if obj.thumbnail_data else None + ) + logger.debug( + f"{self.name}: Tracked object {obj_id} thumbnail frame_time: {thumb_time}, false positive: {obj.false_positive}" + ) + for t in thumb_frames_to_delete: + object_id = self.frame_cache[t].get("object_id", "unknown") + logger.debug(f"{self.name}: Deleting {t} from frame cache for {object_id}") + del self.frame_cache[t] + + with self.current_frame_lock: + self.tracked_objects = tracked_objects + self.motion_boxes = motion_boxes + self.regions = regions + + if current_frame is not None: + self.current_frame_time = frame_time + self._current_frame = np.copy(current_frame) + + if self.previous_frame_id is not None: + self.frame_manager.close(self.previous_frame_id) + + self.previous_frame_id = frame_name + + def send_mqtt_snapshot(self, new_obj: TrackedObject, object_type: str) -> None: + for c in self.callbacks["snapshot"]: + updated = c(self.name, new_obj) + + # if the snapshot was not updated, then this object is not a best object + # but all new objects should be considered the next best object + # so we remove the label from the best objects + if updated: + self.best_objects[object_type] = new_obj + else: + if object_type in self.best_objects: + self.best_objects.pop(object_type) + break + + def save_manual_event_image( + self, + frame: np.ndarray | None, + event_id: str, + label: str, + draw: dict[str, list[dict]], + ) -> None: + img_frame = frame if frame is not None else self.get_current_frame() + + ret, webp = cv2.imencode( + ".webp", img_frame, [int(cv2.IMWRITE_WEBP_QUALITY), 80] + ) + + if ret: + with open( + os.path.join( + CLIPS_DIR, + f"{self.name}-{event_id}-clean.webp", + ), + "wb", + ) as p: + p.write(webp.tobytes()) + + # create thumbnail with max height of 175 and save + width = int(175 * img_frame.shape[1] / img_frame.shape[0]) + thumb = cv2.resize(img_frame, dsize=(width, 175), interpolation=cv2.INTER_AREA) + thumb_path = os.path.join(THUMB_DIR, self.name) + os.makedirs(thumb_path, exist_ok=True) + cv2.imwrite(os.path.join(thumb_path, f"{event_id}.webp"), thumb) + + def shutdown(self) -> None: + for obj in self.tracked_objects.values(): + if not obj.obj_data.get("end_time"): + obj.write_thumbnail_to_disk() diff --git a/frigate/comms/base_communicator.py b/frigate/comms/base_communicator.py new file mode 100644 index 0000000..090552a --- /dev/null +++ b/frigate/comms/base_communicator.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod +from collections.abc import Callable +from typing import Any + + +class Communicator(ABC): + """pub/sub model via specific protocol.""" + + @abstractmethod + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: + """Send data via specific protocol.""" + pass + + @abstractmethod + def subscribe(self, receiver: Callable) -> None: + """Pass receiver so communicators can pass commands.""" + pass + + @abstractmethod + def stop(self) -> None: + """Stop the communicator.""" + pass diff --git a/frigate/comms/config_updater.py b/frigate/comms/config_updater.py new file mode 100644 index 0000000..4552abc --- /dev/null +++ b/frigate/comms/config_updater.py @@ -0,0 +1,59 @@ +"""Facilitates communication between processes.""" + +import multiprocessing as mp +from _pickle import UnpicklingError +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import zmq + +SOCKET_PUB_SUB = "ipc:///tmp/cache/config" + + +class ConfigPublisher: + """Publishes config changes to different processes.""" + + def __init__(self) -> None: + self.context = zmq.Context() + self.socket = self.context.socket(zmq.PUB) + self.socket.bind(SOCKET_PUB_SUB) + self.stop_event: MpEvent = mp.Event() + + def publish(self, topic: str, payload: Any) -> None: + """There is no communication back to the processes.""" + self.socket.send_string(topic, flags=zmq.SNDMORE) + self.socket.send_pyobj(payload) + + def stop(self) -> None: + self.stop_event.set() + self.socket.close(linger=0) + self.context.destroy(linger=0) + + +class ConfigSubscriber: + """Simplifies receiving an updated config.""" + + def __init__(self, topic: str, exact: bool = False) -> None: + self.topic = topic + self.exact = exact + self.context = zmq.Context() + self.socket = self.context.socket(zmq.SUB) + self.socket.setsockopt_string(zmq.SUBSCRIBE, topic) + self.socket.connect(SOCKET_PUB_SUB) + + def check_for_update(self) -> tuple[str, Any] | tuple[None, None]: + """Returns updated config or None if no update.""" + try: + topic = self.socket.recv_string(flags=zmq.NOBLOCK) + obj = self.socket.recv_pyobj() + + if not self.exact or self.topic == topic: + return (topic, obj) + else: + return (None, None) + except (zmq.ZMQError, UnicodeDecodeError, UnpicklingError): + return (None, None) + + def stop(self) -> None: + self.socket.close(linger=0) + self.context.destroy(linger=0) diff --git a/frigate/comms/detections_updater.py b/frigate/comms/detections_updater.py new file mode 100644 index 0000000..dff61c8 --- /dev/null +++ b/frigate/comms/detections_updater.py @@ -0,0 +1,42 @@ +"""Facilitates communication between processes.""" + +from enum import Enum +from typing import Any + +from .zmq_proxy import Publisher, Subscriber + + +class DetectionTypeEnum(str, Enum): + all = "" + api = "api" + video = "video" + audio = "audio" + lpr = "lpr" + + +class DetectionPublisher(Publisher): + """Simplifies receiving video and audio detections.""" + + topic_base = "detection/" + + def __init__(self, topic: str) -> None: + super().__init__(topic) + + +class DetectionSubscriber(Subscriber): + """Simplifies receiving video and audio detections.""" + + topic_base = "detection/" + + def __init__(self, topic: str) -> None: + super().__init__(topic) + + def check_for_update( + self, timeout: float | None = None + ) -> tuple[str, Any] | tuple[None, None] | None: + return super().check_for_update(timeout) + + def _return_object(self, topic: str, payload: Any) -> Any: + if payload is None: + return (None, None) + return (topic[len(self.topic_base) :], payload) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py new file mode 100644 index 0000000..f83a117 --- /dev/null +++ b/frigate/comms/dispatcher.py @@ -0,0 +1,1139 @@ +"""Handle communication between Frigate and other applications.""" + +import datetime +import json +import logging +from collections.abc import Callable, Iterable +from typing import Any, cast + +from frigate.camera import PTZMetrics +from frigate.camera.activity_manager import AudioActivityManager, CameraActivityManager +from frigate.comms.base_communicator import Communicator +from frigate.comms.runtime_state import RuntimeStatePersistence +from frigate.comms.webpush import WebPushClient +from frigate.config import BirdseyeModeEnum, FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdatePublisher, + CameraConfigUpdateTopic, +) +from frigate.config.config import RuntimeFilterConfig, RuntimeMotionConfig +from frigate.config.profile_manager import ProfileManager +from frigate.const import ( + CLEAR_ONGOING_REVIEW_SEGMENTS, + EXPIRE_AUDIO_ACTIVITY, + INSERT_MANY_RECORDINGS, + INSERT_PREVIEW, + NOTIFICATION_TEST, + REQUEST_REGION_GRID, + UPDATE_AUDIO_ACTIVITY, + UPDATE_AUDIO_TRANSCRIPTION_STATE, + UPDATE_BIRDSEYE_LAYOUT, + UPDATE_CAMERA_ACTIVITY, + UPDATE_EMBEDDINGS_REINDEX_PROGRESS, + UPDATE_EVENT_DESCRIPTION, + UPDATE_JOB_STATE, + UPDATE_MODEL_STATE, + UPDATE_REVIEW_DESCRIPTION, + UPSERT_REVIEW_SEGMENT, +) +from frigate.models import Event, Previews, Recordings, ReviewSegment +from frigate.ptz.onvif import OnvifCommandEnum, OnvifController +from frigate.types import ModelStatusTypesEnum, TrackedObjectUpdateTypesEnum +from frigate.util.object import get_camera_regions_grid +from frigate.util.services import restart_frigate + +logger = logging.getLogger(__name__) + + +class Dispatcher: + """Handle communication between Frigate and communicators.""" + + def __init__( + self, + config: FrigateConfig, + config_updater: CameraConfigUpdatePublisher, + onvif: OnvifController, + ptz_metrics: dict[str, PTZMetrics], + communicators: list[Communicator], + ) -> None: + self.config = config + self.config_updater = config_updater + self.onvif = onvif + self.ptz_metrics = ptz_metrics + self.comms = communicators + self.camera_activity = CameraActivityManager(config, self.publish) + self.audio_activity = AudioActivityManager(config, self.publish) + self.model_state: dict[str, ModelStatusTypesEnum] = {} + self.job_state: dict[str, dict[str, Any]] = {} # {job_type: job_data} + self.embeddings_reindex: dict[str, Any] = {} + self.birdseye_layout: dict[str, Any] = {} + self.audio_transcription_state: str = "idle" + self._runtime_state = RuntimeStatePersistence() + self._camera_settings_handlers: dict[str, Callable] = { + "audio": self._on_audio_command, + "audio_transcription": self._on_audio_transcription_command, + "detect": self._on_detect_command, + "enabled": self._on_enabled_command, + "improve_contrast": self._on_motion_improve_contrast_command, + "ptz_autotracker": self._on_ptz_autotracker_command, + "motion": self._on_motion_command, + "motion_contour_area": self._on_motion_contour_area_command, + "motion_threshold": self._on_motion_threshold_command, + "notifications": self._on_camera_notification_command, + "recordings": self._on_recordings_command, + "snapshots": self._on_snapshots_command, + "birdseye": self._on_birdseye_command, + "birdseye_mode": self._on_birdseye_mode_command, + "review_alerts": self._on_alerts_command, + "review_detections": self._on_detections_command, + "object_descriptions": self._on_object_description_command, + "review_descriptions": self._on_review_description_command, + "motion_mask": self._on_motion_mask_command, + "object_mask": self._on_object_mask_command, + "zone": self._on_zone_command, + } + self._global_settings_handlers: dict[str, Callable] = { + "notifications": self._on_global_notification_command, + "profile": self._on_profile_command, + } + self.profile_manager: ProfileManager | None = None + + for comm in self.comms: + comm.subscribe(self._receive) + + self.web_push_client = next( + (comm for comm in communicators if isinstance(comm, WebPushClient)), None + ) + if self.web_push_client is not None: + self.web_push_client.set_suspension_broadcaster(self.publish) + + def _receive(self, topic: str, payload: Any) -> Any | None: + """Handle receiving of payload from communicators.""" + + def handle_camera_command( + command_type: str, + camera_name: str, + command: str, + payload: str, + sub_command: str | None = None, + ) -> None: + if camera_name not in self.config.cameras: + return + + try: + if command_type == "set": + # Commands that require a sub-command (mask/zone name) + sub_command_required = { + "motion_mask", + "object_mask", + "zone", + } + if sub_command: + self._camera_settings_handlers[command]( + camera_name, sub_command, payload + ) + elif command in sub_command_required: + logger.error( + "Command %s requires a sub-command (mask/zone name)", + command, + ) + else: + self._camera_settings_handlers[command](camera_name, payload) + elif command_type == "ptz": + self._on_ptz_command(camera_name, payload) + except KeyError: + logger.error(f"Invalid command type or handler: {command_type}") + + def handle_restart() -> None: + restart_frigate() + + def handle_insert_many_recordings() -> None: + Recordings.insert_many(payload).execute() + + def handle_request_region_grid() -> Any: + camera = payload + if camera not in self.config.cameras: + return None + + grid = get_camera_regions_grid( + camera, + self.config.cameras[camera].detect, + max(self.config.model.width, self.config.model.height), + ) + return grid + + def handle_insert_preview() -> None: + Previews.insert(payload).execute() + + def handle_upsert_review_segment() -> None: + ReviewSegment.insert(payload).on_conflict( + conflict_target=[ReviewSegment.id], + update=payload, + ).execute() + + def handle_clear_ongoing_review_segments() -> None: + ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where( + ReviewSegment.end_time.is_null(True) + ).execute() + + def handle_update_camera_activity() -> None: + self.camera_activity.update_activity(payload) + + def handle_update_audio_activity() -> None: + self.audio_activity.update_activity(payload) + + def handle_expire_audio_activity() -> None: + self.audio_activity.expire_all(payload) + + def handle_update_event_description() -> None: + event: Event = Event.get(Event.id == payload["id"]) + cast(dict, event.data)["description"] = payload["description"] + event.save() + self.publish( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.description, + "id": event.id, + "description": event.data["description"], + "camera": event.camera, + } + ), + ) + + def handle_update_review_description() -> None: + final_data = payload["after"] + ReviewSegment.insert(final_data).on_conflict( + conflict_target=[ReviewSegment.id], + update=final_data, + ).execute() + self.publish("reviews", json.dumps(payload)) + + def handle_update_model_state() -> None: + if payload: + model = payload["model"] + state = payload["state"] + self.model_state[model] = ModelStatusTypesEnum[state] + self.publish("model_state", json.dumps(self.model_state)) + + def handle_model_state() -> None: + self.publish("model_state", json.dumps(self.model_state.copy())) + + def handle_update_job_state() -> None: + if payload and isinstance(payload, dict): + job_type = payload.get("job_type") + if job_type: + self.job_state[job_type] = payload + self.publish( + "job_state", + json.dumps(self.job_state), + ) + + def handle_job_state() -> None: + self.publish("job_state", json.dumps(self.job_state.copy())) + + def handle_update_audio_transcription_state() -> None: + if payload: + self.audio_transcription_state = payload + self.publish( + "audio_transcription_state", + json.dumps(self.audio_transcription_state), + ) + + def handle_audio_transcription_state() -> None: + self.publish( + "audio_transcription_state", json.dumps(self.audio_transcription_state) + ) + + def handle_update_embeddings_reindex_progress() -> None: + self.embeddings_reindex = payload + self.publish( + "embeddings_reindex_progress", + json.dumps(payload), + ) + + def handle_embeddings_reindex_progress() -> None: + self.publish( + "embeddings_reindex_progress", + json.dumps(self.embeddings_reindex.copy()), + ) + + def handle_update_birdseye_layout() -> None: + if payload: + self.birdseye_layout = payload + self.publish("birdseye_layout", json.dumps(self.birdseye_layout)) + + def handle_birdseye_layout() -> None: + self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy())) + + def handle_on_connect() -> None: + camera_status = { + camera: status + for camera, status in self.camera_activity.last_camera_activity.copy().items() + if camera in self.config.cameras + } + audio_detections = self.audio_activity.current_audio_detections.copy() + cameras_with_status = camera_status.keys() + + for camera in self.config.cameras.keys(): + if camera not in cameras_with_status: + camera_status[camera] = {} + + camera_status[camera]["config"] = { + "detect": self.config.cameras[camera].detect.enabled, + "enabled": self.config.cameras[camera].enabled, + "snapshots": self.config.cameras[camera].snapshots.enabled, + "record": self.config.cameras[camera].record.enabled, + "audio": self.config.cameras[camera].audio.enabled, + "audio_transcription": self.config.cameras[ + camera + ].audio_transcription.live_enabled, + "notifications": self.config.cameras[camera].notifications.enabled, + "notifications_suspended": int( + self.web_push_client.suspended_cameras.get(camera, 0) + ) + if self.web_push_client + and camera in self.web_push_client.suspended_cameras + else 0, + "autotracking": self.config.cameras[ + camera + ].onvif.autotracking.enabled, + "alerts": self.config.cameras[camera].review.alerts.enabled, + "detections": self.config.cameras[camera].review.detections.enabled, + "object_descriptions": self.config.cameras[ + camera + ].objects.genai.enabled, + "review_descriptions": self.config.cameras[ + camera + ].review.genai.enabled, + } + + self.publish("camera_activity", json.dumps(camera_status)) + self.publish("model_state", json.dumps(self.model_state.copy())) + self.publish( + "embeddings_reindex_progress", + json.dumps(self.embeddings_reindex.copy()), + ) + self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy())) + self.publish("audio_detections", json.dumps(audio_detections)) + self.publish( + "profile/state", + self.config.active_profile or "none", + retain=True, + ) + + def handle_notification_test() -> None: + self.publish("notification_test", "Test notification") + + # Dictionary mapping topic to handlers + topic_handlers = { + INSERT_MANY_RECORDINGS: handle_insert_many_recordings, + REQUEST_REGION_GRID: handle_request_region_grid, + INSERT_PREVIEW: handle_insert_preview, + UPSERT_REVIEW_SEGMENT: handle_upsert_review_segment, + CLEAR_ONGOING_REVIEW_SEGMENTS: handle_clear_ongoing_review_segments, + UPDATE_CAMERA_ACTIVITY: handle_update_camera_activity, + UPDATE_AUDIO_ACTIVITY: handle_update_audio_activity, + EXPIRE_AUDIO_ACTIVITY: handle_expire_audio_activity, + UPDATE_EVENT_DESCRIPTION: handle_update_event_description, + UPDATE_REVIEW_DESCRIPTION: handle_update_review_description, + UPDATE_MODEL_STATE: handle_update_model_state, + UPDATE_JOB_STATE: handle_update_job_state, + UPDATE_EMBEDDINGS_REINDEX_PROGRESS: handle_update_embeddings_reindex_progress, + UPDATE_BIRDSEYE_LAYOUT: handle_update_birdseye_layout, + UPDATE_AUDIO_TRANSCRIPTION_STATE: handle_update_audio_transcription_state, + NOTIFICATION_TEST: handle_notification_test, + "restart": handle_restart, + "embeddingsReindexProgress": handle_embeddings_reindex_progress, + "modelState": handle_model_state, + "jobState": handle_job_state, + "audioTranscriptionState": handle_audio_transcription_state, + "birdseyeLayout": handle_birdseye_layout, + "onConnect": handle_on_connect, + } + + if topic.endswith("set") or topic.endswith("ptz") or topic.endswith("suspend"): + try: + parts = topic.split("/") + if len(parts) == 3 and topic.endswith("set"): + # example /cam_name/detect/set payload=ON|OFF + camera_name = parts[-3] + command = parts[-2] + handle_camera_command("set", camera_name, command, payload) + elif len(parts) == 4 and topic.endswith("set"): + # example /cam_name/motion_mask/mask_name/set payload=ON|OFF + camera_name = parts[-4] + command = parts[-3] + sub_command = parts[-2] + handle_camera_command( + "set", camera_name, command, payload, sub_command + ) + elif len(parts) == 2 and topic.endswith("set"): + command = parts[-2] + self._global_settings_handlers[command](payload) + elif len(parts) == 2 and topic.endswith("ptz"): + # example /cam_name/ptz payload=MOVE_UP|MOVE_DOWN|STOP... + camera_name = parts[-2] + handle_camera_command("ptz", camera_name, "", payload) + elif len(parts) == 3 and topic.endswith("suspend"): + # example /cam_name/notifications/suspend payload=duration + camera_name = parts[-3] + command = parts[-2] + if camera_name in self.config.cameras: + self._on_camera_notification_suspend(camera_name, payload) + except IndexError: + logger.error( + f"Received invalid {topic.split('/')[-1]} command: {topic}" + ) + return None + elif topic in topic_handlers: + return topic_handlers[topic]() + else: + self.publish(topic, payload, retain=False) + return None + + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: + """Handle publishing to communicators.""" + for comm in self.comms: + comm.publish(topic, payload, retain) + + def stop(self) -> None: + self.camera_activity.stop() + + for comm in self.comms: + comm.stop() + + def restore_runtime_state(self) -> None: + """Replay persisted runtime overrides through the camera settings handlers. + + Called once after Frigate startup completes so processing threads can + receive the resulting ``config_updater`` broadcasts. Unknown cameras + and topics are skipped; handler exceptions are logged and replay + continues for remaining entries. + """ + state = self._runtime_state.load() + for camera_name, features in state.items(): + if camera_name not in self.config.cameras: + continue + for topic, value in features.items(): + handler = self._camera_settings_handlers.get(topic) + if handler is None: + continue + payload = "ON" if value else "OFF" + try: + handler(camera_name, payload) + except Exception: + logger.exception( + "Failed to restore runtime state %s.%s=%s", + camera_name, + topic, + payload, + ) + continue + logger.info( + "Restored runtime state: %s.%s=%s", + camera_name, + topic, + payload, + ) + + def clear_runtime_state_for_yaml_keys(self, dotted_keys: Iterable[str]) -> None: + """Clear stored runtime overrides for YAML keys that were just rewritten. + + Called by ``/api/config/set`` after a successful YAML save so an + explicit settings-UI save isn't silently overridden by an older + runtime toggle on the next restart. + """ + self._runtime_state.clear_for_yaml_keys(dotted_keys) + + def clear_runtime_state(self) -> None: + """Wipe every stored runtime override. + + Called when a profile is activated or deactivated. A profile switch + changes the layer below the runtime overrides, so the stored + "steady state" is no longer valid and must be reset; otherwise a + subsequent restart would replay stale overrides on top of the new + profile-derived in-memory state. + """ + self._runtime_state.clear_all() + + def _on_detect_command(self, camera_name: str, payload: str) -> None: + """Callback for detect topic.""" + detect_settings = self.config.cameras[camera_name].detect + motion_settings = self.config.cameras[camera_name].motion + + if payload == "ON": + if not detect_settings.enabled: + logger.info(f"Turning on detection for {camera_name}") + detect_settings.enabled = True + + if not motion_settings.enabled: + logger.info( + f"Turning on motion for {camera_name} due to detection being enabled." + ) + motion_settings.enabled = True + self.config_updater.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.motion, camera_name + ), + motion_settings, + ) + self.publish(f"{camera_name}/motion/state", payload, retain=True) + elif payload == "OFF": + if detect_settings.enabled: + logger.info(f"Turning off detection for {camera_name}") + detect_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.detect, camera_name), + detect_settings, + ) + self._runtime_state.set(camera_name, "detect", detect_settings.enabled) + self.publish(f"{camera_name}/detect/state", payload, retain=True) + + def _on_enabled_command(self, camera_name: str, payload: str) -> None: + """Callback for camera topic.""" + camera_settings = self.config.cameras[camera_name] + + if payload == "ON": + if not self.config.cameras[camera_name].enabled_in_config: + logger.error( + "Camera must be enabled in the config to be turned on via MQTT." + ) + return + if not camera_settings.enabled: + logger.info(f"Turning on camera {camera_name}") + camera_settings.enabled = True + elif payload == "OFF": + if camera_settings.enabled: + logger.info(f"Turning off camera {camera_name}") + camera_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.enabled, camera_name), + camera_settings.enabled, + ) + self._runtime_state.set(camera_name, "enabled", camera_settings.enabled) + self.publish(f"{camera_name}/enabled/state", payload, retain=True) + + def _on_motion_command(self, camera_name: str, payload: str) -> None: + """Callback for motion topic.""" + detect_settings = self.config.cameras[camera_name].detect + motion_settings = self.config.cameras[camera_name].motion + + if payload == "ON": + if not motion_settings.enabled: + logger.info(f"Turning on motion for {camera_name}") + motion_settings.enabled = True + elif payload == "OFF": + if detect_settings.enabled: + logger.error( + "Turning off motion is not allowed when detection is enabled." + ) + return + + if motion_settings.enabled: + logger.info(f"Turning off motion for {camera_name}") + motion_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name), + motion_settings, + ) + self.publish(f"{camera_name}/motion/state", payload, retain=True) + + def _on_motion_improve_contrast_command( + self, camera_name: str, payload: str + ) -> None: + """Callback for improve_contrast topic.""" + motion_settings = self.config.cameras[camera_name].motion + + if payload == "ON": + if not motion_settings.improve_contrast: + logger.info(f"Turning on improve contrast for {camera_name}") + motion_settings.improve_contrast = True + elif payload == "OFF": + if motion_settings.improve_contrast: + logger.info(f"Turning off improve contrast for {camera_name}") + motion_settings.improve_contrast = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name), + motion_settings, + ) + self.publish(f"{camera_name}/improve_contrast/state", payload, retain=True) + + def _on_ptz_autotracker_command(self, camera_name: str, payload: str) -> None: + """Callback for ptz_autotracker topic.""" + ptz_autotracker_settings = self.config.cameras[camera_name].onvif.autotracking + + if payload == "ON": + if not self.config.cameras[ + camera_name + ].onvif.autotracking.enabled_in_config: + logger.error( + "Autotracking must be enabled in the config to be turned on via MQTT." + ) + return + if not self.ptz_metrics[camera_name].autotracker_enabled.value: + logger.info(f"Turning on ptz autotracker for {camera_name}") + self.ptz_metrics[camera_name].autotracker_enabled.value = True + self.ptz_metrics[camera_name].start_time.value = 0 + ptz_autotracker_settings.enabled = True + elif payload == "OFF": + if self.ptz_metrics[camera_name].autotracker_enabled.value: + logger.info(f"Turning off ptz autotracker for {camera_name}") + self.ptz_metrics[camera_name].autotracker_enabled.value = False + self.ptz_metrics[camera_name].start_time.value = 0 + ptz_autotracker_settings.enabled = False + + self.publish(f"{camera_name}/ptz_autotracker/state", payload, retain=True) + + def _on_motion_contour_area_command(self, camera_name: str, payload: int) -> None: + """Callback for motion contour topic.""" + try: + payload = int(payload) + except ValueError: + f"Received unsupported value for motion contour area: {payload}" + return + + motion_settings = self.config.cameras[camera_name].motion + logger.info(f"Setting motion contour area for {camera_name}: {payload}") + motion_settings.contour_area = payload + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name), + motion_settings, + ) + self.publish(f"{camera_name}/motion_contour_area/state", payload, retain=True) + + def _on_motion_threshold_command(self, camera_name: str, payload: int) -> None: + """Callback for motion threshold topic.""" + try: + payload = int(payload) + except ValueError: + f"Received unsupported value for motion threshold: {payload}" + return + + motion_settings = self.config.cameras[camera_name].motion + logger.info(f"Setting motion threshold for {camera_name}: {payload}") + motion_settings.threshold = payload + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name), + motion_settings, + ) + self.publish(f"{camera_name}/motion_threshold/state", payload, retain=True) + + def _on_global_notification_command(self, payload: str) -> None: + """Callback for global notification topic.""" + if payload != "ON" and payload != "OFF": + f"Received unsupported value for all notification: {payload}" + return + + notification_settings = self.config.notifications + logger.info(f"Setting all notifications: {payload}") + notification_settings.enabled = payload == "ON" + self.config_updater.publisher.publish( + "config/notifications", notification_settings + ) + self.publish("notifications/state", payload, retain=True) + + def _on_profile_command(self, payload: str) -> None: + """Callback for profile/set topic.""" + if self.profile_manager is None: + logger.error("Profile manager not initialized") + return + + profile_name = ( + payload.strip() if payload.strip() not in ("", "none", "None") else None + ) + err = self.profile_manager.activate_profile(profile_name) + if err: + logger.error("Failed to activate profile: %s", err) + return + + self.publish("profile/state", payload.strip() or "none", retain=True) + + def _on_audio_command(self, camera_name: str, payload: str) -> None: + """Callback for audio topic.""" + audio_settings = self.config.cameras[camera_name].audio + + if payload == "ON": + if not self.config.cameras[camera_name].audio.enabled_in_config: + logger.error( + "Audio detection must be enabled in the config to be turned on via MQTT." + ) + return + + if not audio_settings.enabled: + logger.info(f"Turning on audio detection for {camera_name}") + audio_settings.enabled = True + elif payload == "OFF": + if audio_settings.enabled: + logger.info(f"Turning off audio detection for {camera_name}") + audio_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.audio, camera_name), + audio_settings, + ) + self._runtime_state.set(camera_name, "audio", audio_settings.enabled) + self.publish(f"{camera_name}/audio/state", payload, retain=True) + + def _on_audio_transcription_command(self, camera_name: str, payload: str) -> None: + """Callback for live audio transcription topic.""" + audio_transcription_settings = self.config.cameras[ + camera_name + ].audio_transcription + + if payload == "ON": + if not self.config.cameras[ + camera_name + ].audio_transcription.enabled_in_config: + logger.error( + "Audio transcription must be enabled in the config to be turned on via MQTT." + ) + return + + if not audio_transcription_settings.live_enabled: + logger.info(f"Turning on live audio transcription for {camera_name}") + audio_transcription_settings.live_enabled = True + elif payload == "OFF": + if audio_transcription_settings.live_enabled: + logger.info(f"Turning off live audio transcription for {camera_name}") + audio_transcription_settings.live_enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.audio_transcription, camera_name + ), + audio_transcription_settings, + ) + self.publish(f"{camera_name}/audio_transcription/state", payload, retain=True) + + def _on_recordings_command(self, camera_name: str, payload: str) -> None: + """Callback for recordings topic.""" + record_settings = self.config.cameras[camera_name].record + + if payload == "ON": + if not self.config.cameras[camera_name].record.enabled_in_config: + logger.error( + "Recordings must be enabled in the config to be turned on via MQTT." + ) + return + + if not record_settings.enabled: + logger.info(f"Turning on recordings for {camera_name}") + record_settings.enabled = True + elif payload == "OFF": + if record_settings.enabled: + logger.info(f"Turning off recordings for {camera_name}") + record_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.record, camera_name), + record_settings, + ) + self._runtime_state.set(camera_name, "recordings", record_settings.enabled) + self.publish(f"{camera_name}/recordings/state", payload, retain=True) + + def _on_snapshots_command(self, camera_name: str, payload: str) -> None: + """Callback for snapshots topic.""" + snapshots_settings = self.config.cameras[camera_name].snapshots + + if payload == "ON": + if not snapshots_settings.enabled: + logger.info(f"Turning on snapshots for {camera_name}") + snapshots_settings.enabled = True + elif payload == "OFF": + if snapshots_settings.enabled: + logger.info(f"Turning off snapshots for {camera_name}") + snapshots_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.snapshots, camera_name), + snapshots_settings, + ) + self._runtime_state.set(camera_name, "snapshots", snapshots_settings.enabled) + self.publish(f"{camera_name}/snapshots/state", payload, retain=True) + + def _on_ptz_command(self, camera_name: str, payload: str | bytes) -> None: + """Callback for ptz topic.""" + try: + preset: str = ( + payload.decode("utf-8") if isinstance(payload, bytes) else payload + ).lower() + + if "preset" in preset: + command = OnvifCommandEnum.preset + param = preset[preset.index("_") + 1 :] + elif "move_relative" in preset: + command = OnvifCommandEnum.move_relative + param = preset[preset.index("_") + 1 :] + else: + command = OnvifCommandEnum[preset] + param = "" + + self.onvif.handle_command(camera_name, command, param) + logger.info(f"Setting ptz command to {command} for {camera_name}") + except KeyError as k: + logger.error(f"Invalid PTZ command {preset}: {k}") + + def _on_birdseye_command(self, camera_name: str, payload: str) -> None: + """Callback for birdseye topic.""" + birdseye_settings = self.config.cameras[camera_name].birdseye + + if payload == "ON": + if not birdseye_settings.enabled: + logger.info(f"Turning on birdseye for {camera_name}") + birdseye_settings.enabled = True + + elif payload == "OFF": + if birdseye_settings.enabled: + logger.info(f"Turning off birdseye for {camera_name}") + birdseye_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.birdseye, camera_name), + birdseye_settings, + ) + self.publish(f"{camera_name}/birdseye/state", payload, retain=True) + + def _on_birdseye_mode_command(self, camera_name: str, payload: str) -> None: + """Callback for birdseye mode topic.""" + + if payload not in ["CONTINUOUS", "MOTION", "OBJECTS"]: + logger.info(f"Invalid birdseye_mode command: {payload}") + return + + birdseye_settings = self.config.cameras[camera_name].birdseye + + if not birdseye_settings.enabled: + logger.info(f"Birdseye mode not enabled for {camera_name}") + return + + birdseye_settings.mode = BirdseyeModeEnum(payload.lower()) + logger.info( + f"Setting birdseye mode for {camera_name} to {birdseye_settings.mode}" + ) + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.birdseye, camera_name), + birdseye_settings, + ) + self.publish(f"{camera_name}/birdseye_mode/state", payload, retain=True) + + def _on_camera_notification_command(self, camera_name: str, payload: str) -> None: + """Callback for camera level notifications topic.""" + notification_settings = self.config.cameras[camera_name].notifications + + if payload == "ON": + if not self.config.cameras[camera_name].notifications.enabled_in_config: + logger.error( + "Notifications must be enabled in the config to be turned on via MQTT." + ) + return + + if not notification_settings.enabled: + logger.info(f"Turning on notifications for {camera_name}") + notification_settings.enabled = True + if ( + self.web_push_client + and camera_name in self.web_push_client.suspended_cameras + ): + self.web_push_client.suspended_cameras[camera_name] = 0 + elif payload == "OFF": + if notification_settings.enabled: + logger.info(f"Turning off notifications for {camera_name}") + notification_settings.enabled = False + if ( + self.web_push_client + and camera_name in self.web_push_client.suspended_cameras + ): + self.web_push_client.suspended_cameras[camera_name] = 0 + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.notifications, camera_name), + notification_settings, + ) + self.publish(f"{camera_name}/notifications/state", payload, retain=True) + self.publish(f"{camera_name}/notifications/suspended", "0", retain=True) + + def _on_camera_notification_suspend(self, camera_name: str, payload: str) -> None: + """Callback for camera level notifications suspend topic.""" + try: + duration = int(payload) + except ValueError: + logger.error(f"Invalid suspension duration: {payload}") + return + + if self.web_push_client is None: + logger.error("WebPushClient not available for suspension") + return + + notification_settings = self.config.cameras[camera_name].notifications + + if not notification_settings.enabled: + logger.error(f"Notifications are not enabled for {camera_name}") + return + + if duration != 0: + self.web_push_client.suspend_notifications(camera_name, duration) + else: + self.web_push_client.unsuspend_notifications(camera_name) + + self.publish( + f"{camera_name}/notifications/suspended", + str( + int(self.web_push_client.suspended_cameras.get(camera_name, 0)) + if camera_name in self.web_push_client.suspended_cameras + else 0 + ), + retain=True, + ) + + def _on_alerts_command(self, camera_name: str, payload: str) -> None: + """Callback for alerts topic.""" + review_settings = self.config.cameras[camera_name].review + + if payload == "ON": + if not self.config.cameras[camera_name].review.alerts.enabled_in_config: + logger.error( + "Alerts must be enabled in the config to be turned on via MQTT." + ) + return + + if not review_settings.alerts.enabled: + logger.info(f"Turning on alerts for {camera_name}") + review_settings.alerts.enabled = True + elif payload == "OFF": + if review_settings.alerts.enabled: + logger.info(f"Turning off alerts for {camera_name}") + review_settings.alerts.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.review, camera_name), + review_settings, + ) + self.publish(f"{camera_name}/review_alerts/state", payload, retain=True) + + def _on_detections_command(self, camera_name: str, payload: str) -> None: + """Callback for detections topic.""" + review_settings = self.config.cameras[camera_name].review + + if payload == "ON": + if not self.config.cameras[camera_name].review.detections.enabled_in_config: + logger.error( + "Detections must be enabled in the config to be turned on via MQTT." + ) + return + + if not review_settings.detections.enabled: + logger.info(f"Turning on detections for {camera_name}") + review_settings.detections.enabled = True + elif payload == "OFF": + if review_settings.detections.enabled: + logger.info(f"Turning off detections for {camera_name}") + review_settings.detections.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.review, camera_name), + review_settings, + ) + self.publish(f"{camera_name}/review_detections/state", payload, retain=True) + + def _on_object_description_command(self, camera_name: str, payload: str) -> None: + """Callback for object description topic.""" + genai_settings = self.config.cameras[camera_name].objects.genai + + if payload == "ON": + if not self.config.cameras[camera_name].objects.genai.enabled_in_config: + logger.error( + "GenAI must be enabled in the config to be turned on via MQTT." + ) + return + + if not genai_settings.enabled: + logger.info(f"Turning on object descriptions for {camera_name}") + genai_settings.enabled = True + elif payload == "OFF": + if genai_settings.enabled: + logger.info(f"Turning off object descriptions for {camera_name}") + genai_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.object_genai, camera_name), + genai_settings, + ) + self.publish(f"{camera_name}/object_descriptions/state", payload, retain=True) + + def _on_review_description_command(self, camera_name: str, payload: str) -> None: + """Callback for review description topic.""" + genai_settings = self.config.cameras[camera_name].review.genai + + if payload == "ON": + if not self.config.cameras[camera_name].review.genai.enabled_in_config: + logger.error( + "GenAI Alerts or Detections must be enabled in the config to be turned on via MQTT." + ) + return + + if not genai_settings.enabled: + logger.info(f"Turning on review descriptions for {camera_name}") + genai_settings.enabled = True + elif payload == "OFF": + if genai_settings.enabled: + logger.info(f"Turning off review descriptions for {camera_name}") + genai_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.review_genai, camera_name), + genai_settings, + ) + self.publish(f"{camera_name}/review_descriptions/state", payload, retain=True) + + def _on_motion_mask_command( + self, camera_name: str, mask_name: str, payload: str + ) -> None: + """Callback for motion mask topic.""" + if payload not in ["ON", "OFF"]: + logger.error(f"Invalid payload for motion mask {mask_name}: {payload}") + return + + motion_settings = self.config.cameras[camera_name].motion + + if mask_name not in motion_settings.mask: + logger.error(f"Unknown motion mask: {mask_name}") + return + + mask = motion_settings.mask[mask_name] + + if not mask: + logger.error(f"Motion mask {mask_name} is None") + return + + if payload == "ON": + if not mask.enabled_in_config: + logger.error( + f"Motion mask {mask_name} must be enabled in the config to be turned on via MQTT." + ) + return + + mask.enabled = payload == "ON" + + # Recreate RuntimeMotionConfig to update rasterized_mask + motion_settings = RuntimeMotionConfig( + frame_shape=self.config.cameras[camera_name].frame_shape, + **motion_settings.model_dump(exclude_unset=True), + ) + + # Update the dispatcher's own config + self.config.cameras[camera_name].motion = motion_settings + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name), + motion_settings, + ) + self.publish( + f"{camera_name}/motion_mask/{mask_name}/state", payload, retain=True + ) + + def _on_object_mask_command( + self, camera_name: str, mask_name: str, payload: str + ) -> None: + """Callback for object mask topic.""" + if payload not in ["ON", "OFF"]: + logger.error(f"Invalid payload for object mask {mask_name}: {payload}") + return + + object_settings = self.config.cameras[camera_name].objects + + # Check if this is a global mask + mask_found = False + if mask_name in object_settings.mask: + mask = object_settings.mask[mask_name] + if mask: + if payload == "ON": + if not mask.enabled_in_config: + logger.error( + f"Object mask {mask_name} must be enabled in the config to be turned on via MQTT." + ) + return + mask.enabled = payload == "ON" + mask_found = True + + # Check if this is a per-object filter mask + for object_name, filter_config in object_settings.filters.items(): + if mask_name in filter_config.mask: + mask = filter_config.mask[mask_name] + if mask: + if payload == "ON": + if not mask.enabled_in_config: + logger.error( + f"Object mask {mask_name} must be enabled in the config to be turned on via MQTT." + ) + return + mask.enabled = payload == "ON" + mask_found = True + + if not mask_found: + logger.error(f"Unknown object mask: {mask_name}") + return + + # Recreate RuntimeFilterConfig for each object filter to update rasterized_mask + for object_name, filter_config in object_settings.filters.items(): + # Merge global object masks with per-object filter masks + merged_mask = dict(filter_config.mask) # Copy filter-specific masks + + # Add global object masks if they exist + if object_settings.mask: + for global_mask_id, global_mask_config in object_settings.mask.items(): + # Use a global prefix to avoid key collisions + global_mask_id_prefixed = f"global_{global_mask_id}" + merged_mask[global_mask_id_prefixed] = global_mask_config + + object_settings.filters[object_name] = RuntimeFilterConfig( + frame_shape=self.config.cameras[camera_name].frame_shape, + mask=merged_mask, + **filter_config.model_dump( + exclude_unset=True, exclude={"mask", "raw_mask"} + ), + ) + + # Update the dispatcher's own config + self.config.cameras[camera_name].objects = object_settings + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.objects, camera_name), + object_settings, + ) + self.publish( + f"{camera_name}/object_mask/{mask_name}/state", payload, retain=True + ) + + def _on_zone_command(self, camera_name: str, zone_name: str, payload: str) -> None: + """Callback for zone topic.""" + if payload not in ["ON", "OFF"]: + logger.error(f"Invalid payload for zone {zone_name}: {payload}") + return + + camera_config = self.config.cameras[camera_name] + + if zone_name not in camera_config.zones: + logger.error(f"Unknown zone: {zone_name}") + return + + if payload == "ON": + if not camera_config.zones[zone_name].enabled_in_config: + logger.error( + f"Zone {zone_name} must be enabled in the config to be turned on via MQTT." + ) + return + + camera_config.zones[zone_name].enabled = payload == "ON" + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.zones, camera_name), + camera_config.zones, + ) + self.publish(f"{camera_name}/zone/{zone_name}/state", payload, retain=True) diff --git a/frigate/comms/embeddings_updater.py b/frigate/comms/embeddings_updater.py new file mode 100644 index 0000000..cd83709 --- /dev/null +++ b/frigate/comms/embeddings_updater.py @@ -0,0 +1,92 @@ +"""Facilitates communication between processes.""" + +import logging +from collections.abc import Callable +from enum import Enum +from typing import Any + +import zmq + +logger = logging.getLogger(__name__) + + +SOCKET_REP_REQ = "ipc:///tmp/cache/embeddings" + + +class EmbeddingsRequestEnum(Enum): + # audio + transcribe_audio = "transcribe_audio" + # custom classification + reload_classification_model = "reload_classification_model" + # face + clear_face_classifier = "clear_face_classifier" + recognize_face = "recognize_face" + register_face = "register_face" + reprocess_face = "reprocess_face" + # semantic search + embed_description = "embed_description" + embed_thumbnail = "embed_thumbnail" + generate_search = "generate_search" + reindex = "reindex" + # LPR + reprocess_plate = "reprocess_plate" + # Review Descriptions + summarize_review = "summarize_review" + + +class EmbeddingsResponder: + def __init__(self) -> None: + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REP) + self.socket.bind(SOCKET_REP_REQ) + + def check_for_request(self, process: Callable) -> None: + while True: # load all messages that are queued + has_message, _, _ = zmq.select([self.socket], [], [], 0.01) + + if not has_message: + break + + try: + raw = self.socket.recv_json(flags=zmq.NOBLOCK) + + if isinstance(raw, list): + (topic, value) = raw + response = process(topic, value) + else: + logging.warning( + f"Received unexpected data type in ZMQ recv_json: {type(raw)}" + ) + response = None + + if response is not None: + self.socket.send_json(response) + else: + self.socket.send_json([]) + except zmq.ZMQError: + break + + def stop(self) -> None: + self.socket.close() + self.context.destroy() + + +class EmbeddingsRequestor: + """Simplifies sending data to EmbeddingsResponder and getting a reply.""" + + def __init__(self) -> None: + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REQ) + self.socket.connect(SOCKET_REP_REQ) + + def send_data(self, topic: str, data: Any) -> Any: + """Sends data and then waits for reply.""" + try: + self.socket.send_json((topic, data)) + return self.socket.recv_json() + except zmq.ZMQError: + return "" + + def stop(self) -> None: + self.socket.close() + self.context.destroy() diff --git a/frigate/comms/event_metadata_updater.py b/frigate/comms/event_metadata_updater.py new file mode 100644 index 0000000..8977788 --- /dev/null +++ b/frigate/comms/event_metadata_updater.py @@ -0,0 +1,49 @@ +"""Facilitates communication between processes.""" + +import logging +from enum import Enum +from typing import Any + +from .zmq_proxy import Publisher, Subscriber + +logger = logging.getLogger(__name__) + + +class EventMetadataTypeEnum(str, Enum): + all = "" + manual_event_create = "manual_event_create" + manual_event_end = "manual_event_end" + regenerate_description = "regenerate_description" + sub_label = "sub_label" + attribute = "attribute" + lpr_event_create = "lpr_event_create" + save_lpr_snapshot = "save_lpr_snapshot" + + +class EventMetadataPublisher(Publisher): + """Simplifies receiving event metadata.""" + + topic_base = "event_metadata/" + + def __init__(self) -> None: + super().__init__() + + def publish(self, payload: Any, sub_topic: str = "") -> None: + super().publish(payload, sub_topic) + + +class EventMetadataSubscriber(Subscriber): + """Simplifies receiving event metadata.""" + + topic_base = "event_metadata/" + + def __init__(self, topic: EventMetadataTypeEnum) -> None: + super().__init__(topic.value) + + def _return_object( + self, topic: str, payload: tuple | None + ) -> tuple[str, Any] | tuple[None, None]: + if payload is None: + return (None, None) + + return (topic, payload) diff --git a/frigate/comms/events_updater.py b/frigate/comms/events_updater.py new file mode 100644 index 0000000..cfd958d --- /dev/null +++ b/frigate/comms/events_updater.py @@ -0,0 +1,61 @@ +"""Facilitates communication between processes.""" + +from typing import Any + +from frigate.events.types import EventStateEnum, EventTypeEnum + +from .zmq_proxy import Publisher, Subscriber + + +class EventUpdatePublisher( + Publisher[tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]]] +): + """Publishes events (objects, audio, manual).""" + + topic_base = "event/" + + def __init__(self) -> None: + super().__init__("update") + + def publish( + self, + payload: tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]], + sub_topic: str = "", + ) -> None: + super().publish(payload, sub_topic) + + +class EventUpdateSubscriber(Subscriber): + """Receives event updates.""" + + topic_base = "event/" + + def __init__(self) -> None: + super().__init__("update") + + +class EventEndPublisher( + Publisher[tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]]] +): + """Publishes events that have ended.""" + + topic_base = "event/" + + def __init__(self) -> None: + super().__init__("finalized") + + def publish( + self, + payload: tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]], + sub_topic: str = "", + ) -> None: + super().publish(payload, sub_topic) + + +class EventEndSubscriber(Subscriber): + """Receives events that have ended.""" + + topic_base = "event/" + + def __init__(self) -> None: + super().__init__("finalized") diff --git a/frigate/comms/inter_process.py b/frigate/comms/inter_process.py new file mode 100644 index 0000000..6897b12 --- /dev/null +++ b/frigate/comms/inter_process.py @@ -0,0 +1,87 @@ +"""Facilitates communication between processes.""" + +import logging +import multiprocessing as mp +import threading +from collections.abc import Callable +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import zmq + +from frigate.comms.base_communicator import Communicator + +logger = logging.getLogger(__name__) + +SOCKET_REP_REQ = "ipc:///tmp/cache/comms" + + +class InterProcessCommunicator(Communicator): + def __init__(self) -> None: + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REP) + self.socket.bind(SOCKET_REP_REQ) + self.stop_event: MpEvent = mp.Event() + + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: + """There is no communication back to the processes.""" + pass + + def subscribe(self, receiver: Callable) -> None: + self._dispatcher = receiver + self.reader_thread = threading.Thread(target=self.read) + self.reader_thread.start() + + def read(self) -> None: + while not self.stop_event.is_set(): + while True: # load all messages that are queued + has_message, _, _ = zmq.select([self.socket], [], [], 1) + + if not has_message: + break + + try: + raw = self.socket.recv_json(flags=zmq.NOBLOCK) + + if isinstance(raw, list): + (topic, value) = raw + response = self._dispatcher(topic, value) + else: + logging.warning( + f"Received unexpected data type in ZMQ recv_json: {type(raw)}" + ) + response = None + + if response is not None: + self.socket.send_json(response) + else: + self.socket.send_json([]) + except zmq.ZMQError: + break + + def stop(self) -> None: + self.stop_event.set() + self.reader_thread.join() + self.socket.close(linger=0) + self.context.destroy(linger=0) + + +class InterProcessRequestor: + """Simplifies sending data to InterProcessCommunicator and getting a reply.""" + + def __init__(self) -> None: + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REQ) + self.socket.connect(SOCKET_REP_REQ) + + def send_data(self, topic: str, data: Any) -> Any: + """Sends data and then waits for reply.""" + try: + self.socket.send_json((topic, data)) + return self.socket.recv_json() + except zmq.ZMQError: + return "" + + def stop(self) -> None: + self.socket.close(linger=0) + self.context.destroy(linger=0) diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py new file mode 100644 index 0000000..979fc6d --- /dev/null +++ b/frigate/comms/mqtt.py @@ -0,0 +1,356 @@ +import logging +import threading +from collections.abc import Callable +from typing import Any + +import paho.mqtt.client as mqtt +from paho.mqtt.enums import CallbackAPIVersion + +from frigate.comms.base_communicator import Communicator +from frigate.config import FrigateConfig + +logger = logging.getLogger(__name__) + + +class MqttClient(Communicator): + """Frigate wrapper for mqtt client.""" + + def __init__(self, config: FrigateConfig) -> None: + self.config = config + self.mqtt_config = config.mqtt + self.connected = False + + def subscribe(self, receiver: Callable) -> None: + """Wrapper for allowing dispatcher to subscribe.""" + self._dispatcher = receiver + self._start() + + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: + """Wrapper for publishing when client is in valid state.""" + if not self.connected: + logger.debug(f"Unable to publish to {topic}: client is not connected") + return + + self.client.publish( + f"{self.mqtt_config.topic_prefix}/{topic}", + payload, + qos=self.config.mqtt.qos, + retain=retain, + ) + + def stop(self) -> None: + self.publish("available", "stopped", retain=True) + self.client.disconnect() + + def _notifications_enabled_in_config(self) -> bool: + """Whether notifications are configured globally or on any camera. + + Notifications can be enabled per camera with the global config left + disabled, so the global topics must consider both (matching how + app.py decides to create the WebPushClient). + """ + return self.config.notifications.enabled_in_config or any( + cam.enabled and cam.notifications.enabled_in_config + for cam in self.config.cameras.values() + ) + + def _set_initial_topics(self) -> None: + """Set initial state topics.""" + for camera_name, camera in self.config.cameras.items(): + self.publish( + f"{camera_name}/enabled/state", + "ON" if camera.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/recordings/state", + "ON" if camera.record.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/snapshots/state", + "ON" if camera.snapshots.enabled else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/audio/state", + "ON" if camera.audio.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/detect/state", + "ON" if camera.detect.enabled else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/motion/state", + "ON", + retain=True, + ) + self.publish( + f"{camera_name}/improve_contrast/state", + "ON" if camera.motion.improve_contrast else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/ptz_autotracker/state", + "ON" if camera.onvif.autotracking.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/motion_threshold/state", + camera.motion.threshold, + retain=True, + ) + self.publish( + f"{camera_name}/motion_contour_area/state", + camera.motion.contour_area, + retain=True, + ) + self.publish( + f"{camera_name}/motion", + "OFF", + retain=False, + ) + self.publish( + f"{camera_name}/birdseye/state", + "ON" if camera.birdseye.enabled else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/birdseye_mode/state", + ( + camera.birdseye.mode.value.upper() + if camera.birdseye.enabled + else "OFF" + ), + retain=True, + ) + self.publish( + f"{camera_name}/review_alerts/state", + "ON" if camera.review.alerts.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/review_detections/state", + "ON" if camera.review.detections.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/object_descriptions/state", + "ON" if camera.objects.genai.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/review_descriptions/state", + "ON" if camera.review.genai.enabled_in_config else "OFF", + retain=True, + ) + + for mask_name, motion_mask in camera.motion.mask.items(): + if motion_mask: + self.publish( + f"{camera_name}/motion_mask/{mask_name}/state", + "ON" if motion_mask.enabled else "OFF", + retain=True, + ) + + for mask_name, object_mask in camera.objects.mask.items(): + if object_mask: + self.publish( + f"{camera_name}/object_mask/{mask_name}/state", + "ON" if object_mask.enabled else "OFF", + retain=True, + ) + + for zone_name, zone in camera.zones.items(): + self.publish( + f"{camera_name}/zone/{zone_name}/state", + "ON" if zone.enabled else "OFF", + retain=True, + ) + + if self._notifications_enabled_in_config(): + self.publish( + "notifications/state", + "ON" if self.config.notifications.enabled else "OFF", + retain=True, + ) + + self.publish( + "profile/state", + self.config.active_profile or "none", + retain=True, + ) + self.publish("available", "online", retain=True) + + def on_mqtt_command( + self, client: mqtt.Client, userdata: Any, message: mqtt.MQTTMessage + ) -> None: + self._dispatcher( + message.topic.replace(f"{self.mqtt_config.topic_prefix}/", "", 1), + message.payload.decode(), + ) + + def _on_connect( + self, + client: mqtt.Client, + userdata: Any, + flags: Any, + reason_code: mqtt.ReasonCode, # type: ignore[name-defined] + properties: Any, + ) -> None: + """Mqtt connection callback.""" + threading.current_thread().name = "mqtt" + if reason_code != 0: + if reason_code == "Server unavailable": + logger.error( + "Unable to connect to MQTT server: MQTT Server unavailable" + ) + elif reason_code == "Bad user name or password": + logger.error( + "Unable to connect to MQTT server: MQTT Bad username or password" + ) + elif reason_code == "Not authorized": + logger.error("Unable to connect to MQTT server: MQTT Not authorized") + else: + logger.error( + "Unable to connect to MQTT server: Connection refused. Error code: %s", + reason_code.getName(), + ) + + self.connected = True + logger.debug("MQTT connected") + client.subscribe(f"{self.mqtt_config.topic_prefix}/#", qos=self.config.mqtt.qos) + self._set_initial_topics() + + def _on_disconnect( + self, + client: mqtt.Client, + userdata: Any, + flags: Any, + reason_code: mqtt.ReasonCode, # type: ignore[name-defined] + properties: Any, + ) -> None: + """Mqtt disconnection callback.""" + self.connected = False + logger.error("MQTT disconnected") + + def _start(self) -> None: + """Start mqtt client.""" + self.client = mqtt.Client( + callback_api_version=CallbackAPIVersion.VERSION2, + client_id=self.mqtt_config.client_id, + ) + self.client.on_connect = self._on_connect + self.client.on_disconnect = self._on_disconnect + self.client.will_set( + self.mqtt_config.topic_prefix + "/available", + payload="offline", + qos=1, + retain=True, + ) + + # register callbacks + callback_types = [ + "enabled", + "recordings", + "snapshots", + "detect", + "audio", + "motion", + "improve_contrast", + "ptz_autotracker", + "motion_threshold", + "motion_contour_area", + "birdseye", + "birdseye_mode", + "review_alerts", + "review_detections", + "object_descriptions", + "review_descriptions", + "notifications", + ] + + for name in self.config.cameras.keys(): + for callback in callback_types: + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/{callback}/set", + self.on_mqtt_command, + ) + + # notifications suspend doesn't follow the /set topic pattern + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/notifications/suspend", + self.on_mqtt_command, + ) + + if self.config.cameras[name].onvif.host: + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/ptz", + self.on_mqtt_command, + ) + + for mask_name in self.config.cameras[name].motion.mask.keys(): + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/motion_mask/{mask_name}/set", + self.on_mqtt_command, + ) + + for mask_name in self.config.cameras[name].objects.mask.keys(): + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/object_mask/{mask_name}/set", + self.on_mqtt_command, + ) + + for zone_name in self.config.cameras[name].zones.keys(): + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/zone/{zone_name}/set", + self.on_mqtt_command, + ) + + if self._notifications_enabled_in_config(): + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/notifications/set", + self.on_mqtt_command, + ) + + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/profile/set", + self.on_mqtt_command, + ) + + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/onConnect", self.on_mqtt_command + ) + + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/restart", self.on_mqtt_command + ) + + if self.mqtt_config.tls_ca_certs is not None: + if ( + self.mqtt_config.tls_client_cert is not None + and self.mqtt_config.tls_client_key is not None + ): + self.client.tls_set( + self.mqtt_config.tls_ca_certs, + self.mqtt_config.tls_client_cert, + self.mqtt_config.tls_client_key, + ) + else: + self.client.tls_set(self.mqtt_config.tls_ca_certs) + if self.mqtt_config.tls_insecure is not None: + self.client.tls_insecure_set(self.mqtt_config.tls_insecure) + if self.mqtt_config.user is not None: + self.client.username_pw_set( + self.mqtt_config.user, password=self.mqtt_config.password + ) + try: + # https://stackoverflow.com/a/55390477 + # with connect_async, retries are handled automatically + self.client.connect_async(self.mqtt_config.host, self.mqtt_config.port, 60) + self.client.loop_start() + except Exception as e: + logger.error(f"Unable to connect to MQTT server: {e}") + return diff --git a/frigate/comms/object_detector_signaler.py b/frigate/comms/object_detector_signaler.py new file mode 100644 index 0000000..e8871db --- /dev/null +++ b/frigate/comms/object_detector_signaler.py @@ -0,0 +1,92 @@ +"""Facilitates communication between processes for object detection signals.""" + +import threading + +import zmq + +SOCKET_PUB = "ipc:///tmp/cache/detector_pub" +SOCKET_SUB = "ipc:///tmp/cache/detector_sub" + + +class ZmqProxyRunner(threading.Thread): + def __init__(self, context: zmq.Context[zmq.Socket]) -> None: + super().__init__(name="detector_proxy") + self.context = context + + def run(self) -> None: + """Run the proxy.""" + incoming = self.context.socket(zmq.XSUB) + incoming.bind(SOCKET_PUB) + outgoing = self.context.socket(zmq.XPUB) + outgoing.bind(SOCKET_SUB) + + # Blocking: This will unblock (via exception) when we destroy the context + # The incoming and outgoing sockets will be closed automatically + # when the context is destroyed as well. + try: + zmq.proxy(incoming, outgoing) + except zmq.ZMQError: + pass + + +class DetectorProxy: + """Proxies object detection signals.""" + + def __init__(self) -> None: + self.context = zmq.Context() + self.runner = ZmqProxyRunner(self.context) + self.runner.start() + + def stop(self) -> None: + # destroying the context will tell the proxy to stop + self.context.destroy() + self.runner.join() + + +class ObjectDetectorPublisher: + """Publishes signal for object detection to different processes.""" + + topic_base = "object_detector/" + + def __init__(self, topic: str = "") -> None: + self.topic = f"{self.topic_base}{topic}" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.PUB) + self.socket.connect(SOCKET_PUB) + + def publish(self, sub_topic: str = "") -> None: + """Publish message.""" + self.socket.send_string(f"{self.topic}{sub_topic}/") + + def stop(self) -> None: + self.socket.close() + self.context.destroy() + + +class ObjectDetectorSubscriber: + """Simplifies receiving a signal for object detection.""" + + topic_base = "object_detector/" + + def __init__(self, topic: str = "") -> None: + self.topic = f"{self.topic_base}{topic}/" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.SUB) + self.socket.setsockopt_string(zmq.SUBSCRIBE, self.topic) + self.socket.connect(SOCKET_SUB) + + def check_for_update(self, timeout: float = 5) -> str | None: + """Returns message or None if no update.""" + try: + has_update, _, _ = zmq.select([self.socket], [], [], timeout) + + if has_update: + return self.socket.recv_string(flags=zmq.NOBLOCK) + except zmq.ZMQError: + pass + + return None + + def stop(self) -> None: + self.socket.close() + self.context.destroy() diff --git a/frigate/comms/recordings_updater.py b/frigate/comms/recordings_updater.py new file mode 100644 index 0000000..249c2f6 --- /dev/null +++ b/frigate/comms/recordings_updater.py @@ -0,0 +1,46 @@ +"""Facilitates communication between processes.""" + +import logging +from enum import Enum +from typing import Any + +from .zmq_proxy import Publisher, Subscriber + +logger = logging.getLogger(__name__) + + +class RecordingsDataTypeEnum(str, Enum): + all = "" + saved = "saved" # segment has been saved to db + latest = "latest" # segment is in cache + valid = "valid" # segment is valid + invalid = "invalid" # segment is invalid + + +class RecordingsDataPublisher(Publisher[Any]): + """Publishes latest recording data.""" + + topic_base = "recordings/" + + def __init__(self) -> None: + super().__init__() + + def publish(self, payload: Any, sub_topic: str = "") -> None: + super().publish(payload, sub_topic) + + +class RecordingsDataSubscriber(Subscriber): + """Receives latest recording data.""" + + topic_base = "recordings/" + + def __init__(self, topic: RecordingsDataTypeEnum) -> None: + super().__init__(topic.value) + + def _return_object( + self, topic: str, payload: tuple | None + ) -> tuple[str, Any] | tuple[None, None]: + if payload is None: + return (None, None) + + return (topic, payload) diff --git a/frigate/comms/review_updater.py b/frigate/comms/review_updater.py new file mode 100644 index 0000000..2b3a5b3 --- /dev/null +++ b/frigate/comms/review_updater.py @@ -0,0 +1,30 @@ +"""Facilitates communication between processes.""" + +import logging + +from .zmq_proxy import Publisher, Subscriber + +logger = logging.getLogger(__name__) + + +class ReviewDataPublisher( + Publisher +): # update when typing improvement is added Publisher[tuple[str, float]] + """Publishes review item data.""" + + topic_base = "review/" + + def __init__(self, topic: str) -> None: + super().__init__(topic) + + def publish(self, payload: tuple[str, float], sub_topic: str = "") -> None: + super().publish(payload, sub_topic) + + +class ReviewDataSubscriber(Subscriber): + """Receives review item data.""" + + topic_base = "review/" + + def __init__(self, topic: str) -> None: + super().__init__(topic) diff --git a/frigate/comms/runtime_state.py b/frigate/comms/runtime_state.py new file mode 100644 index 0000000..222d488 --- /dev/null +++ b/frigate/comms/runtime_state.py @@ -0,0 +1,163 @@ +"""Persistence layer for dispatcher runtime state overrides.""" + +import json +import logging +import os +from collections.abc import Iterable +from typing import Any + +from filelock import FileLock, Timeout + +from frigate.util.config import find_config_file + +logger = logging.getLogger(__name__) + + +class RuntimeStatePersistence: + """Persist last-known runtime states for dispatcher toggles. + + Stores boolean overrides applied to camera-level toggles by the dispatcher. + Overrides are replayed at startup on top of the YAML-derived in-memory + config, so changes made via MQTT or the live-view UI survive a restart. + """ + + # Maps dispatcher topic name -> YAML key suffix under cameras. + TRACKED_TOPICS: dict[str, str] = { + "enabled": "enabled", + "detect": "detect.enabled", + "snapshots": "snapshots.enabled", + "recordings": "record.enabled", + "audio": "audio.enabled", + } + + _SUFFIX_TO_TOPIC: dict[str, str] = {v: k for k, v in TRACKED_TOPICS.items()} + + def __init__(self) -> None: + self._path = os.path.join( + os.path.dirname(find_config_file()), ".runtime_state.json" + ) + self._lock_path = f"{self._path}.lock" + self._lock_timeout = 5 + + def load(self) -> dict[str, dict[str, bool]]: + """Return {camera: {topic: bool}} or {} if missing/corrupt.""" + try: + with FileLock(self._lock_path, timeout=self._lock_timeout): + data = self._read_locked() + except Timeout: + logger.error("Timed out acquiring runtime state lock for load") + return {} + cameras = data.get("cameras", {}) + if not isinstance(cameras, dict): + return {} + # Filter out malformed camera entries so callers can trust the shape. + return { + name: features + for name, features in cameras.items() + if isinstance(features, dict) + } + + def set(self, camera: str, topic: str, value: bool) -> None: + """Persist a single (camera, topic, value). No-op if topic untracked.""" + if topic not in self.TRACKED_TOPICS: + return + try: + with FileLock(self._lock_path, timeout=self._lock_timeout): + data = self._read_locked() + cameras = data.setdefault("cameras", {}) + if not isinstance(cameras, dict): + cameras = {} + data["cameras"] = cameras + cam = cameras.setdefault(camera, {}) + if not isinstance(cam, dict): + cam = {} + cameras[camera] = cam + cam[topic] = bool(value) + self._write_locked(data) + except Timeout: + logger.error("Timed out persisting runtime state for %s/%s", camera, topic) + except OSError: + logger.exception("Failed to persist runtime state for %s/%s", camera, topic) + + def clear_all(self) -> None: + """Wipe every stored runtime override. + + Called when the "layer below" changes in a way that invalidates all + runtime overrides for the current session (currently: profile + activation or deactivation). + """ + try: + with FileLock(self._lock_path, timeout=self._lock_timeout): + if not os.path.exists(self._path): + return + self._write_locked({"cameras": {}}) + except Timeout: + logger.error("Timed out clearing runtime state") + except OSError: + logger.exception("Failed to clear runtime state") + + def clear_for_yaml_keys(self, dotted_keys: Iterable[str]) -> None: + """Remove stored entries whose YAML key was just rewritten. + + Each dotted key must be of the form ``cameras..``. + Keys that don't match a tracked topic are ignored. + """ + to_remove: list[tuple[str, str]] = [] + for key in dotted_keys: + parts = key.split(".") + if len(parts) < 3 or parts[0] != "cameras": + continue + camera = parts[1] + suffix = ".".join(parts[2:]) + topic = self._SUFFIX_TO_TOPIC.get(suffix) + if topic is not None: + to_remove.append((camera, topic)) + + if not to_remove: + return + + try: + with FileLock(self._lock_path, timeout=self._lock_timeout): + data = self._read_locked() + cameras = data.get("cameras") + if not isinstance(cameras, dict): + return + changed = False + for camera, topic in to_remove: + cam = cameras.get(camera) + if isinstance(cam, dict) and topic in cam: + del cam[topic] + changed = True + if not cam: + del cameras[camera] + if changed: + self._write_locked(data) + except Timeout: + logger.error("Timed out clearing runtime state for YAML keys") + except OSError: + logger.exception("Failed to clear runtime state for YAML keys") + + def _read_locked(self) -> dict[str, Any]: + """Read the JSON file while the FileLock is held. + + Returns ``{}`` on a missing or corrupt file so the caller can write a + fresh structure on the next mutation. + """ + if not os.path.exists(self._path): + return {} + try: + with open(self._path) as f: + data = json.load(f) + except (OSError, json.JSONDecodeError): + logger.exception( + "Failed to read runtime state file %s; starting fresh", self._path + ) + return {} + return data if isinstance(data, dict) else {} + + def _write_locked(self, data: dict[str, Any]) -> None: + """Atomically write the JSON file while the FileLock is held.""" + tmp_path = f"{self._path}.tmp" + with open(tmp_path, "w") as f: + json.dump(data, f, indent=2, sort_keys=True) + os.replace(tmp_path, self._path) diff --git a/frigate/comms/webpush.py b/frigate/comms/webpush.py new file mode 100644 index 0000000..6f2d9ab --- /dev/null +++ b/frigate/comms/webpush.py @@ -0,0 +1,601 @@ +"""Handle sending notifications for Frigate via Firebase.""" + +import datetime +import json +import logging +import os +import queue +import threading +from collections.abc import Callable +from dataclasses import dataclass +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +from py_vapid import Vapid01 +from pywebpush import WebPusher +from titlecase import titlecase + +from frigate.comms.base_communicator import Communicator +from frigate.comms.config_updater import ConfigSubscriber +from frigate.config import FrigateConfig +from frigate.config.auth import AuthConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import BASE_DIR, CONFIG_DIR +from frigate.models import User + +logger = logging.getLogger(__name__) + + +@dataclass +class PushNotification: + user: str + payload: dict[str, Any] + title: str + message: str + direct_url: str = "" + image: str = "" + notification_type: str = "alert" + ttl: int = 0 + + +class WebPushClient(Communicator): + """Frigate wrapper for webpush client.""" + + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + self.config = config + self.stop_event = stop_event + self.claim_headers: dict[str, dict[str, str]] = {} + self.refresh: int = 0 + self.web_pushers: dict[str, list[WebPusher]] = {} + self.expired_subs: dict[str, list[str]] = {} + self.suspended_cameras: dict[str, int] = { + c.name: 0 # type: ignore[misc] + for c in self.config.cameras.values() + } + self.suspension_broadcaster: Callable[[str, Any, bool], None] | None = None + self.last_camera_notification_time: dict[str, float] = { + c.name: 0 # type: ignore[misc] + for c in self.config.cameras.values() + } + self.last_notification_time: float = 0 + self.user_cameras: dict[str, set[str]] = {} + self.notification_queue: queue.Queue[PushNotification] = queue.Queue() + self.notification_thread = threading.Thread( + target=self._process_notifications, daemon=True + ) + self.notification_thread.start() + self.suspension_thread = threading.Thread( + target=self._process_suspensions, daemon=True + ) + self.suspension_thread.start() + + if not self.config.notifications.email: + logger.warning("Email must be provided for push notifications to be sent.") + + # Pull keys from PEM or generate if they do not exist + self.vapid = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem")) + + users: list[dict[str, Any]] = ( + User.select(User.username, User.notification_tokens).dicts().iterator() + ) + for user in users: + self.web_pushers[user["username"]] = [] + for sub in user["notification_tokens"]: + self.web_pushers[user["username"]].append(WebPusher(sub)) + + # notification and auth config updater + self.global_config_subscriber = ConfigSubscriber("config/") + self.config_subscriber = CameraConfigUpdateSubscriber( + self.config, self.config.cameras, [CameraConfigUpdateEnum.notifications] + ) + self._refresh_user_cameras() + + def subscribe(self, receiver: Callable) -> None: + """Wrapper for allowing dispatcher to subscribe.""" + pass + + def check_registrations(self) -> None: + # check for valid claim or create new one + now = datetime.datetime.now().timestamp() + if len(self.claim_headers) == 0 or self.refresh < now: + self.refresh = int( + (datetime.datetime.now() + datetime.timedelta(hours=1)).timestamp() + ) + endpoints: set[str] = set() + + # get a unique set of push endpoints + for pushers in self.web_pushers.values(): + for push in pushers: + endpoint: str = push.subscription_info["endpoint"] + endpoints.add(endpoint[0 : endpoint.index("/", 10)]) + + # create new claim + for endpoint in endpoints: + claim = { + "sub": f"mailto:{self.config.notifications.email}", + "aud": endpoint, + "exp": self.refresh, + } + self.claim_headers[endpoint] = self.vapid.sign(claim) + + def cleanup_registrations(self) -> None: + # delete any expired subs + if len(self.expired_subs) > 0: + for user, expired in self.expired_subs.items(): + user_subs = [] + + # get all subscriptions, removing ones that are expired + stored_user: User = User.get_by_id(user) + for token in stored_user.notification_tokens: + if token["endpoint"] in expired: + continue + + user_subs.append(token) + + # overwrite the database and reset web pushers + User.update(notification_tokens=user_subs).where( + User.username == user + ).execute() + + self.web_pushers[user] = [] + + for sub in user_subs: + self.web_pushers[user].append(WebPusher(sub)) + + logger.info( + f"Cleaned up {len(expired)} notification subscriptions for {user}" + ) + + self.expired_subs = {} + + def suspend_notifications(self, camera: str, minutes: int) -> None: + """Suspend notifications for a specific camera.""" + suspend_until = int( + (datetime.datetime.now() + datetime.timedelta(minutes=minutes)).timestamp() + ) + self.suspended_cameras[camera] = suspend_until + logger.info( + f"Notifications for {camera} suspended until {datetime.datetime.fromtimestamp(suspend_until).strftime('%Y-%m-%d %H:%M:%S')}" + ) + + def unsuspend_notifications(self, camera: str) -> None: + """Unsuspend notifications for a specific camera.""" + self.suspended_cameras[camera] = 0 + logger.info(f"Notifications for {camera} unsuspended") + + def is_camera_suspended(self, camera: str) -> bool: + return datetime.datetime.now().timestamp() <= self.suspended_cameras[camera] + + def set_suspension_broadcaster( + self, broadcaster: Callable[[str, Any, bool], None] + ) -> None: + """Register the callback used to broadcast suspension state changes.""" + self.suspension_broadcaster = broadcaster + + def _process_suspensions(self) -> None: + while not self.stop_event.wait(1): + self._clear_expired_suspensions() + + def _clear_expired_suspensions(self) -> None: + """Reset and broadcast cameras whose suspension window has elapsed.""" + now = datetime.datetime.now().timestamp() + for camera, suspended_until in list(self.suspended_cameras.items()): + if suspended_until and now > suspended_until: + self.unsuspend_notifications(camera) + if self.suspension_broadcaster is not None: + self.suspension_broadcaster( + f"{camera}/notifications/suspended", "0", True + ) + + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: + """Wrapper for publishing when client is in valid state.""" + # check for updated global config (notifications, auth) + while True: + config_topic, config_payload = ( + self.global_config_subscriber.check_for_update() + ) + if config_topic is None: + break + if config_topic == "config/notifications" and config_payload: + self.config.notifications = config_payload + elif config_topic == "config/auth": + if isinstance(config_payload, AuthConfig): + self.config.auth = config_payload + self._refresh_user_cameras() + + updates = self.config_subscriber.check_for_updates() + + if "add" in updates: + for camera in updates["add"]: + self.suspended_cameras[camera] = 0 + self.last_camera_notification_time[camera] = 0 + + if topic == "reviews": + decoded = json.loads(payload) + camera = decoded["before"]["camera"] + if not self.config.cameras[camera].notifications.enabled: + return + if self.is_camera_suspended(camera): + logger.debug(f"Notifications for {camera} are currently suspended.") + return + self.send_alert(decoded) + if topic == "triggers": + decoded = json.loads(payload) + + camera = decoded["camera"] + name = decoded["name"] + + # ensure notifications are enabled and the specific trigger has + # notification action enabled + if ( + not self.config.cameras[camera].notifications.enabled + or name not in self.config.cameras[camera].semantic_search.triggers + or "notification" + not in self.config.cameras[camera] + .semantic_search.triggers[name] + .actions + ): + return + + if self.is_camera_suspended(camera): + logger.debug(f"Notifications for {camera} are currently suspended.") + return + self.send_trigger(decoded) + elif topic == "camera_monitoring": + decoded = json.loads(payload) + camera = decoded["camera"] + if not self.config.cameras[camera].notifications.enabled: + return + if self.is_camera_suspended(camera): + logger.debug(f"Notifications for {camera} are currently suspended.") + return + self.send_camera_monitoring(decoded) + elif topic == "notification_test": + if not self.config.notifications.enabled and not any( + cam.notifications.enabled for cam in self.config.cameras.values() + ): + logger.debug( + "No cameras have notifications enabled, test notification not sent" + ) + return + self.send_notification_test() + + def send_push_notification( + self, + user: str, + payload: dict[str, Any], + title: str, + message: str, + direct_url: str = "", + image: str = "", + notification_type: str = "alert", + ttl: int = 0, + ) -> None: + notification = PushNotification( + user=user, + payload=payload, + title=title, + message=message, + direct_url=direct_url, + image=image, + notification_type=notification_type, + ttl=ttl, + ) + self.notification_queue.put(notification) + + def _process_notifications(self) -> None: + while not self.stop_event.is_set(): + try: + notification = self.notification_queue.get(timeout=1.0) + self.check_registrations() + + for pusher in self.web_pushers[notification.user]: + endpoint = pusher.subscription_info["endpoint"] + headers = self.claim_headers[ + endpoint[: endpoint.index("/", 10)] + ].copy() + headers["urgency"] = "high" + + resp = pusher.send( + headers=headers, + ttl=notification.ttl, + data=json.dumps( + { + "title": notification.title, + "message": notification.message, + "direct_url": notification.direct_url, + "image": notification.image, + "id": notification.payload.get("after", {}).get( + "id", "" + ), + "type": notification.notification_type, + } + ), + timeout=10, + ) + + if resp.status_code in (404, 410): + self.expired_subs.setdefault(notification.user, []).append( + endpoint + ) + logger.debug( + f"Notification endpoint expired for {notification.user}, received {resp.status_code}" + ) + elif resp.status_code != 201: + logger.warning( + f"Failed to send notification to {notification.user} :: {resp.status_code}" + ) + + except queue.Empty: + continue + except Exception as e: + logger.error(f"Error processing notification: {str(e)}") + + def _refresh_user_cameras(self) -> None: + """Rebuild the user-to-cameras access cache from the database.""" + all_camera_names = set(self.config.cameras.keys()) + roles_dict = self.config.auth.roles + updated: dict[str, set[str]] = {} + for user in User.select(User.username, User.role).dicts().iterator(): + allowed = User.get_allowed_cameras( + user["role"], roles_dict, all_camera_names + ) + updated[user["username"]] = set(allowed) + logger.debug( + "User %s has access to cameras: %s", + user["username"], + ", ".join(allowed), + ) + self.user_cameras = updated + + def _user_has_camera_access(self, username: str, camera: str) -> bool: + """Check if a user has access to a specific camera based on cached roles.""" + allowed = self.user_cameras.get(username) + if allowed is None: + logger.debug(f"No camera access information found for user {username}") + return False + return camera in allowed + + def _within_cooldown(self, camera: str) -> bool: + now = datetime.datetime.now().timestamp() + if now - self.last_notification_time < self.config.notifications.cooldown: + logger.debug( + f"Skipping notification for {camera} - in global cooldown period" + ) + return True + if ( + now - self.last_camera_notification_time[camera] + < self.config.cameras[camera].notifications.cooldown + ): + logger.debug( + f"Skipping notification for {camera} - in camera-specific cooldown period" + ) + return True + return False + + def send_notification_test(self) -> None: + if not self.config.notifications.email: + return + + self.check_registrations() + + logger.debug("Sending test notification") + + for user in self.web_pushers: + self.send_push_notification( + user=user, + payload={}, + title="Test Notification", + message="This is a test notification from Frigate.", + direct_url="/", + notification_type="test", + ) + + def send_alert(self, payload: dict[str, Any]) -> None: + if ( + not self.config.notifications.email + or payload["after"]["severity"] != "alert" + ): + return + + camera: str = payload["after"]["camera"] + camera_name: str = getattr( + self.config.cameras[camera], "friendly_name", None + ) or titlecase(camera.replace("_", " ")) + current_time = datetime.datetime.now().timestamp() + + if self._within_cooldown(camera): + return + + self.check_registrations() + + state = payload["type"] + + # Don't notify if message is an update and important fields don't have an update + if ( + state == "update" + and len(payload["before"]["data"]["objects"]) + == len(payload["after"]["data"]["objects"]) + and len(payload["before"]["data"]["zones"]) + == len(payload["after"]["data"]["zones"]) + ): + logger.debug( + f"Skipping notification for {camera} - message is an update and important fields don't have an update" + ) + return + + self.last_camera_notification_time[camera] = current_time + self.last_notification_time = current_time + + reviewId = payload["after"]["id"] + sorted_objects: set[str] = set() + + for obj in payload["after"]["data"]["objects"]: + if "-verified" not in obj: + sorted_objects.add(obj) + + sorted_objects.update(payload["after"]["data"]["sub_labels"]) + + image = f"{payload['after']['thumb_path'].replace(BASE_DIR, '')}" + ended = state == "end" or state == "genai" + + if state == "genai" and payload["after"]["data"]["metadata"]: + base_title = payload["after"]["data"]["metadata"]["title"] + threat_level = payload["after"]["data"]["metadata"].get( + "potential_threat_level", 0 + ) + + # Add prefix for threat levels 1 and 2 + if threat_level == 1: + title = f"Needs Review: {base_title}" + elif threat_level == 2: + title = f"Security Concern: {base_title}" + else: + title = base_title + + if payload["after"]["data"]["metadata"].get("shortSummary"): + message = payload["after"]["data"]["metadata"]["shortSummary"] + else: + message = f"Detected on {camera_name}" + else: + zone_names = payload["after"]["data"]["zones"] + formatted_zone_names = [] + + for zone_name in zone_names: + if zone_name in self.config.cameras[camera].zones: + formatted_zone_names.append( + self.config.cameras[camera] + .zones[zone_name] + .get_formatted_name(zone_name) + ) + else: + formatted_zone_names.append(titlecase(zone_name.replace("_", " "))) + + title = f"{titlecase(', '.join(sorted_objects).replace('_', ' '))}{' was' if state == 'end' else ''} detected in {', '.join(formatted_zone_names)}" + message = f"Detected on {camera_name}" + + if ended: + logger.debug( + f"Sending a notification with state {state} and message {message}" + ) + + # if event is ongoing open to live view otherwise open to recordings view + direct_url = f"/review?id={reviewId}" if ended else f"/#{camera}" + ttl = 3600 if ended else 0 + + logger.debug(f"Sending push notification for {camera}, review ID {reviewId}") + + for user in self.web_pushers: + if not self._user_has_camera_access(user, camera): + logger.debug( + "Skipping notification for user %s - no access to camera %s", + user, + camera, + ) + continue + + self.send_push_notification( + user=user, + payload=payload, + title=title, + message=message, + direct_url=direct_url, + image=image, + ttl=ttl, + ) + + self.cleanup_registrations() + + def send_trigger(self, payload: dict[str, Any]) -> None: + if not self.config.notifications.email: + return + + camera: str = payload["camera"] + camera_name: str = getattr( + self.config.cameras[camera], "friendly_name", None + ) or titlecase(camera.replace("_", " ")) + current_time = datetime.datetime.now().timestamp() + + if self._within_cooldown(camera): + return + + self.check_registrations() + + self.last_camera_notification_time[camera] = current_time + self.last_notification_time = current_time + + trigger_type = payload["type"] + event_id = payload["event_id"] + name = payload["name"] + score = payload["score"] + + title = f"{name.replace('_', ' ')} triggered on {camera_name}" + message = f"{titlecase(trigger_type)} trigger fired for {camera_name} with score {score:.2f}" + image = f"clips/triggers/{camera}/{event_id}.webp" + + direct_url = f"/explore?event_id={event_id}" + ttl = 0 + + logger.debug( + f"Sending push notification for {camera_name}, trigger name {name}" + ) + + for user in self.web_pushers: + if not self._user_has_camera_access(user, camera): + logger.debug( + "Skipping notification for user %s - no access to camera %s", + user, + camera, + ) + continue + + self.send_push_notification( + user=user, + payload=payload, + title=title, + message=message, + direct_url=direct_url, + image=image, + ttl=ttl, + ) + + self.cleanup_registrations() + + def send_camera_monitoring(self, payload: dict[str, Any]) -> None: + camera: str = payload["camera"] + camera_name: str = getattr( + self.config.cameras[camera], "friendly_name", None + ) or titlecase(camera.replace("_", " ")) + + self.check_registrations() + + text: str = payload.get("message") or payload.get("reasoning", "") + title = f"{camera_name}: Monitoring Alert" + message = (text[:197] + "...") if len(text) > 200 else text + + logger.debug(f"Sending camera monitoring push notification for {camera_name}") + + for user in self.web_pushers: + if not self._user_has_camera_access(user, camera): + logger.debug( + "Skipping notification for user %s - no access to camera %s", + user, + camera, + ) + continue + + self.send_push_notification( + user=user, + payload=payload, + title=title, + message=message, + ) + + self.cleanup_registrations() + + def stop(self) -> None: + logger.info("Closing notification queue") + self.notification_thread.join() diff --git a/frigate/comms/ws.py b/frigate/comms/ws.py new file mode 100644 index 0000000..ac047b0 --- /dev/null +++ b/frigate/comms/ws.py @@ -0,0 +1,598 @@ +"""Websocket communicator.""" + +import errno +import json +import logging +import threading +from collections.abc import Callable +from typing import Any +from wsgiref.simple_server import make_server + +from ws4py.server.wsgirefserver import ( + WebSocketWSGIHandler, + WebSocketWSGIRequestHandler, + WSGIServer, +) +from ws4py.server.wsgiutils import WebSocketWSGIApplication +from ws4py.websocket import WebSocket as WebSocket_ + +from frigate.comms.base_communicator import Communicator +from frigate.config import FrigateConfig +from frigate.const import ( + CLEAR_ONGOING_REVIEW_SEGMENTS, + EXPIRE_AUDIO_ACTIVITY, + INSERT_MANY_RECORDINGS, + INSERT_PREVIEW, + NOTIFICATION_TEST, + REQUEST_REGION_GRID, + UPDATE_AUDIO_ACTIVITY, + UPDATE_AUDIO_TRANSCRIPTION_STATE, + UPDATE_BIRDSEYE_LAYOUT, + UPDATE_CAMERA_ACTIVITY, + UPDATE_EMBEDDINGS_REINDEX_PROGRESS, + UPDATE_EVENT_DESCRIPTION, + UPDATE_MODEL_STATE, + UPDATE_REVIEW_DESCRIPTION, + UPSERT_REVIEW_SEGMENT, +) +from frigate.models import User +from frigate.output.ws_auth import ws_has_camera_access + +logger = logging.getLogger(__name__) + +# Internal IPC topics — NEVER allowed from WebSocket, regardless of role +_WS_BLOCKED_TOPICS = frozenset( + { + INSERT_MANY_RECORDINGS, + INSERT_PREVIEW, + REQUEST_REGION_GRID, + UPSERT_REVIEW_SEGMENT, + CLEAR_ONGOING_REVIEW_SEGMENTS, + UPDATE_CAMERA_ACTIVITY, + UPDATE_AUDIO_ACTIVITY, + EXPIRE_AUDIO_ACTIVITY, + UPDATE_EVENT_DESCRIPTION, + UPDATE_REVIEW_DESCRIPTION, + UPDATE_MODEL_STATE, + UPDATE_EMBEDDINGS_REINDEX_PROGRESS, + UPDATE_BIRDSEYE_LAYOUT, + UPDATE_AUDIO_TRANSCRIPTION_STATE, + NOTIFICATION_TEST, + } +) + +# Read-only topics any authenticated user (including viewer) can send +_WS_VIEWER_TOPICS = frozenset( + { + "onConnect", + "modelState", + "audioTranscriptionState", + "birdseyeLayout", + "embeddingsReindexProgress", + "jobState", + } +) + +# Camera-scoped command topics a camera-authorized (non-admin) user may send. +_WS_CAMERA_COMMAND_TOPICS = frozenset({"ptz"}) + + +def _check_ws_authorization( + topic: str, + role_header: str | None, + separator: str, + roles_config: dict[str, list[str]] | None = None, + camera_names: set[str] | None = None, +) -> bool: + """Check if a WebSocket message is authorized. + + Args: + topic: The message topic. + role_header: The HTTP_REMOTE_ROLE header value, or None. + separator: The role separator character from proxy config. + roles_config: The auth.roles mapping (role -> allowed cameras), used to + authorize camera-scoped commands for non-admin users. + camera_names: All configured camera names, used to resolve a role's + allowed cameras. + + Returns: + True if authorized, False if blocked. + """ + # Block IPC-only topics unconditionally + if topic in _WS_BLOCKED_TOPICS: + return False + + # No role header: default to viewer (fail-closed) + roles = [r.strip() for r in role_header.split(separator)] if role_header else [] + + # Admin can send anything + if "admin" in roles: + return True + + # Read-only topics any authenticated user can send + if topic in _WS_VIEWER_TOPICS: + return True + + # Camera-scoped command like "/ptz": allow when the user's role(s) + # grant access to that camera. + parts = topic.split("/") + if ( + roles_config is not None + and len(parts) == 2 + and parts[1] in _WS_CAMERA_COMMAND_TOPICS + ): + allowed: set[str] = set() + # No role header maps to the default viewer role (e.g. proxy-only setups) + for role in roles or ["viewer"]: + allowed.update( + User.get_allowed_cameras(role, roles_config, camera_names or set()) + ) + return parts[0] in allowed + + return False + + +# ---- Outbound filtering --------------------------------------------------- +# +# Every WebSocket broadcast is classified into one of a small set of scopes, +# then materialized per recipient. Connections with restricted roles only see +# data for cameras they are authorized to access; admin and full-access roles +# behave as today. + +# Topics that are safe to broadcast to every authenticated client. +_WS_GLOBAL_OUTBOUND_TOPICS = frozenset( + { + "model_state", + "embeddings_reindex_progress", + "audio_transcription_state", + "profile/state", + "notifications/state", + "notification_test", + } +) + +# Topics that restricted roles must never receive. Birdseye composites span +# all cameras, so the existing JSMPEG policy already restricts birdseye access +# to unrestricted roles; the layout broadcast follows the same rule. +_WS_UNRESTRICTED_ONLY_TOPICS = frozenset( + { + "birdseye_layout", + } +) + +# Topics whose payload (parsed as JSON) names a single owning camera at the +# given key path. Used to scope events, reviews, triggers, etc. +_WS_PAYLOAD_CAMERA_TOPICS: dict[str, tuple[str, ...]] = { + "events": ("after", "camera"), + "reviews": ("after", "camera"), + "tracked_object_update": ("camera",), + "triggers": ("camera",), + "camera_monitoring": ("camera",), +} + +# Topics whose payload is a dict keyed by camera name; filter keys per +# recipient. +_WS_RESHAPE_BY_CAMERA_KEY_TOPICS = frozenset( + { + "camera_activity", + "audio_detections", + } +) + +# Topics whose payload is a dict keyed by job_type, where each entry may +# contain a "camera" or "source_camera" field, or a nested ``results.jobs`` +# list of per-camera sub-jobs (export broadcasts). +_WS_RESHAPE_JOB_STATE_TOPICS = frozenset( + { + "job_state", + } +) + +# Topics whose payload mixes global aggregates with a ``cameras`` sub-dict +# keyed by camera name. Aggregates and detector data stay; per-camera entries +# are filtered. +_WS_RESHAPE_STATS_TOPICS = frozenset( + { + "stats", + } +) + + +def _collect_zone_names(config: FrigateConfig) -> set[str]: + """Return the set of all zone names defined across cameras.""" + names: set[str] = set() + for camera in config.cameras.values(): + zones = getattr(camera, "zones", None) or {} + names.update(zones.keys()) + return names + + +def _parse_json_payload(payload: Any) -> Any: + """Return payload parsed as JSON if it is a string, else as-is.""" + if isinstance(payload, str): + try: + return json.loads(payload) + except (ValueError, TypeError): + return None + return payload + + +def _scope_job_entry_to_allowed(entry: Any, allowed: set[str]) -> dict[str, Any] | None: + """Filter a single job_state entry to the recipient's allowed cameras. + + Returns the (possibly reshaped) entry, or None to drop it. Four shapes + are handled: + + * Top-level ``camera`` or ``source_camera`` (motion_search, vlm_watch, + export sub-job dicts): drop the entry if not allowed. + * Nested ``results.jobs`` list of per-camera sub-jobs (the aggregated + export broadcast): filter the list; drop the entry if nothing remains. + * Nested ``results.camera`` or ``results.source_camera`` (debug_replay, + which puts replay-specific fields inside ``results``): drop the entry + if not allowed. + * No camera anywhere (e.g. ``media_sync``): treat as global and keep. + """ + if not isinstance(entry, dict): + return None + + cam = entry.get("camera") or entry.get("source_camera") + + if cam is None: + results = entry.get("results") + if isinstance(results, dict): + sub_jobs = results.get("jobs") + if isinstance(sub_jobs, list): + filtered_jobs = [ + j + for j in sub_jobs + if isinstance(j, dict) + and (j.get("camera") or j.get("source_camera")) in allowed + ] + if not filtered_jobs: + return None + reshaped = dict(entry) + reshaped["results"] = dict(results) + reshaped["results"]["jobs"] = filtered_jobs + return reshaped + + cam = results.get("camera") or results.get("source_camera") + + if cam is not None: + return entry if cam in allowed else None + + return entry + + +def _extract_payload_camera(payload: Any, path: tuple[str, ...]) -> str | None: + """Walk the dotted path through a (possibly JSON-encoded) payload.""" + cur = _parse_json_payload(payload) + for key in path: + if not isinstance(cur, dict): + return None + cur = cur.get(key) + return cur if isinstance(cur, str) else None + + +def _classify_outbound( + topic: str, all_cameras: set[str], all_zones: set[str] +) -> tuple[str, Any]: + """Classify an outbound topic into (kind, extra). + + kind values: + - "global" : send to every authenticated client + - "drop" : send to nobody (fail-closed for unknowns) + - "unrestricted_only" : send only to admin/full-access roles + - "camera" : extra is the owning camera name + - "payload_camera" : extra is the JSON key path to the camera name + - "reshape_by_camera_key" + - "reshape_job_state" + - "reshape_stats" + """ + if topic in _WS_GLOBAL_OUTBOUND_TOPICS: + return ("global", None) + if topic in _WS_UNRESTRICTED_ONLY_TOPICS: + return ("unrestricted_only", None) + if topic in _WS_RESHAPE_BY_CAMERA_KEY_TOPICS: + return ("reshape_by_camera_key", None) + if topic in _WS_RESHAPE_JOB_STATE_TOPICS: + return ("reshape_job_state", None) + if topic in _WS_RESHAPE_STATS_TOPICS: + return ("reshape_stats", None) + if topic in _WS_PAYLOAD_CAMERA_TOPICS: + return ("payload_camera", _WS_PAYLOAD_CAMERA_TOPICS[topic]) + + # Topic-prefix based: first segment names the owning camera or zone. + first = topic.split("/", 1)[0] + if first in all_cameras: + return ("camera", first) + if first in all_zones: + # Zone aggregates span cameras; restricted users see nothing here. + return ("unrestricted_only", None) + + return ("drop", None) + + +def _ws_role_header(ws: Any) -> str | None: + """Return the HTTP_REMOTE_ROLE header value, if any.""" + environ = getattr(ws, "environ", None) + if not environ: + return None + value = environ.get("HTTP_REMOTE_ROLE") + return value if isinstance(value, str) else None + + +def _ws_valid_roles(ws: Any, config: FrigateConfig) -> list[str]: + """Return the list of recognized roles for this connection.""" + header = _ws_role_header(ws) + if not header: + return [] + roles = [r.strip() for r in header.split(config.proxy.separator) if r.strip()] + return [r for r in roles if r in config.auth.roles] + + +def _ws_is_unrestricted(ws: Any, config: FrigateConfig) -> bool: + """True when the connection has unrestricted camera access. + + Mirrors the policy in ``frigate.output.ws_auth``: admin or any role with + an empty allow-list grants full access. + """ + roles = _ws_valid_roles(ws, config) + if not roles: + return False + roles_dict = config.auth.roles + return any(r == "admin" or not roles_dict.get(r) for r in roles) + + +def _ws_allowed_cameras(ws: Any, config: FrigateConfig) -> set[str]: + """Return the union of cameras this connection may access across its roles.""" + roles = _ws_valid_roles(ws, config) + if not roles: + return set() + all_cameras = set(config.cameras.keys()) + allowed: set[str] = set() + for role in roles: + if role == "admin" or not config.auth.roles.get(role): + return all_cameras + allowed.update(User.get_allowed_cameras(role, config.auth.roles, all_cameras)) + return allowed + + +def _wrap_envelope(topic: str, inner_payload: Any) -> str: + """Re-serialize a (topic, payload) message after payload reshaping. + + Frigate's wire format keeps payloads as JSON-encoded strings inside the + outer envelope, mirroring what producers send today. + """ + return json.dumps({"topic": topic, "payload": json.dumps(inner_payload)}) + + +def _materialize_for_ws( + ws: Any, + topic: str, + full_message: str, + scope: tuple[str, Any], + parsed_payload: Any, + config: FrigateConfig, +) -> str | None: + """Return the JSON string to deliver to ``ws``, or None to skip it.""" + kind, extra = scope + has_role = _ws_role_header(ws) is not None + + if kind == "drop": + return None + + if kind == "global": + # Globals still require an authenticated connection. Missing role + # falls back to viewer semantics (matching the inbound rule). + return full_message + + # Beyond globals, an authenticated role header is required (fail-closed). + if not has_role: + return None + + if kind == "unrestricted_only": + return full_message if _ws_is_unrestricted(ws, config) else None + + if kind == "camera": + return full_message if ws_has_camera_access(ws, extra, config) else None + + if kind == "payload_camera": + camera = _extract_payload_camera(parsed_payload, extra) + if camera is None: + return None + return full_message if ws_has_camera_access(ws, camera, config) else None + + if kind == "reshape_by_camera_key": + if _ws_is_unrestricted(ws, config): + return full_message + if not isinstance(parsed_payload, dict): + return None + allowed = _ws_allowed_cameras(ws, config) + filtered = {cam: data for cam, data in parsed_payload.items() if cam in allowed} + if not filtered: + return None + return _wrap_envelope(topic, filtered) + + if kind == "reshape_job_state": + if _ws_is_unrestricted(ws, config): + return full_message + if not isinstance(parsed_payload, dict): + return None + allowed = _ws_allowed_cameras(ws, config) + filtered_jobs: dict[str, Any] = {} + for job_type, job_payload in parsed_payload.items(): + scoped = _scope_job_entry_to_allowed(job_payload, allowed) + if scoped is not None: + filtered_jobs[job_type] = scoped + if not filtered_jobs: + return None + return _wrap_envelope(topic, filtered_jobs) + + if kind == "reshape_stats": + if _ws_is_unrestricted(ws, config): + return full_message + if not isinstance(parsed_payload, dict): + return None + allowed = _ws_allowed_cameras(ws, config) + cameras_block = parsed_payload.get("cameras") + if isinstance(cameras_block, dict): + filtered_cameras = { + name: data for name, data in cameras_block.items() if name in allowed + } + reshaped = dict(parsed_payload) + reshaped["cameras"] = filtered_cameras + return _wrap_envelope(topic, reshaped) + return full_message + + return None + + +class WebSocket(WebSocket_): # type: ignore[misc] + def unhandled_error(self, error: Any) -> None: + """ + Handles the unfriendly socket closures on the server side + without showing a confusing error message + """ + if hasattr(error, "errno") and error.errno == errno.ECONNRESET: + pass + else: + logging.getLogger("ws4py").exception("Failed to receive data") + + +class WebSocketClient(Communicator): + """Frigate wrapper for ws client.""" + + def __init__(self, config: FrigateConfig) -> None: + self.config = config + self.websocket_server: WSGIServer | None = None + + def subscribe(self, receiver: Callable) -> None: + self._dispatcher = receiver + self.start() + + def start(self) -> None: + """Start the websocket client.""" + + class _WebSocketHandler(WebSocket): + receiver = self._dispatcher + role_separator = self.config.proxy.separator or "," + roles_config = self.config.auth.roles + camera_names = set(self.config.cameras.keys()) + + def received_message(self, message: WebSocket.received_message) -> None: # type: ignore[name-defined] + try: + json_message = json.loads(message.data.decode("utf-8")) + json_message = { + "topic": json_message.get("topic"), + "payload": json_message.get("payload"), + } + except Exception: + logger.warning( + f"Unable to parse websocket message as valid json: {message.data.decode('utf-8')}" + ) + return + + topic = json_message["topic"] + + # Authorization check (skip when environ is None — direct internal connection) + role_header = ( + self.environ.get("HTTP_REMOTE_ROLE") if self.environ else None + ) + if self.environ is not None and not _check_ws_authorization( + topic, + role_header, + self.role_separator, + self.roles_config, + self.camera_names, + ): + logger.warning( + "Blocked unauthorized WebSocket message: topic=%s, role=%s", + topic, + role_header, + ) + return + + logger.debug(f"Publishing mqtt message from websockets at {topic}.") + self.receiver( + topic, + json_message["payload"], + ) + + # start a websocket server on 5002 + WebSocketWSGIHandler.http_version = "1.1" + self.websocket_server = make_server( + "127.0.0.1", + 5002, + server_class=WSGIServer, + handler_class=WebSocketWSGIRequestHandler, + app=WebSocketWSGIApplication(handler_cls=_WebSocketHandler), + ) + self.websocket_server.initialize_websockets_manager() + self.websocket_thread = threading.Thread( + target=self.websocket_server.serve_forever + ) + self.websocket_thread.start() + + def publish(self, topic: str, payload: Any, _: bool = False) -> None: + if self.websocket_server is None: + logger.debug("Skipping message, websocket not connected yet") + return + + try: + ws_message = json.dumps( + { + "topic": topic, + "payload": payload, + } + ) + except Exception: + # if the payload can't be decoded don't relay to clients + logger.debug(f"payload for {topic} wasn't text. Skipping...") + return + + all_cameras = set(self.config.cameras.keys()) + all_zones = _collect_zone_names(self.config) + scope = _classify_outbound(topic, all_cameras, all_zones) + + if scope[0] == "drop": + return + + # Pre-parse payload once for topics that need to read its contents. + parsed_payload: Any = None + if scope[0] in ( + "payload_camera", + "reshape_by_camera_key", + "reshape_job_state", + "reshape_stats", + ): + parsed_payload = _parse_json_payload(payload) + if parsed_payload is None: + # malformed payload — fail closed + return + + manager = self.websocket_server.manager + with manager.lock: + websockets = list(manager.websockets.values()) + + for ws in websockets: + if getattr(ws, "terminated", False): + continue + message = _materialize_for_ws( + ws, topic, ws_message, scope, parsed_payload, self.config + ) + if message is None: + continue + try: + ws.send(message) + except (ConnectionResetError, BrokenPipeError, ValueError): + pass + + def stop(self) -> None: + if self.websocket_server is not None: + self.websocket_server.manager.close_all() + self.websocket_server.manager.stop() + self.websocket_server.manager.join() + self.websocket_server.shutdown() + + self.websocket_thread.join() + logger.info("Exiting websocket client...") diff --git a/frigate/comms/zmq_proxy.py b/frigate/comms/zmq_proxy.py new file mode 100644 index 0000000..4a4a049 --- /dev/null +++ b/frigate/comms/zmq_proxy.py @@ -0,0 +1,103 @@ +"""Facilitates communication over zmq proxy.""" + +import json +import threading +from typing import Generic, TypeVar + +import zmq + +from frigate.const import FAST_QUEUE_TIMEOUT + +SOCKET_PUB = "ipc:///tmp/cache/proxy_pub" +SOCKET_SUB = "ipc:///tmp/cache/proxy_sub" + + +class ZmqProxyRunner(threading.Thread): + def __init__(self, context: zmq.Context[zmq.Socket]) -> None: + super().__init__(name="detection_proxy") + self.context = context + + def run(self) -> None: + """Run the proxy.""" + incoming = self.context.socket(zmq.XSUB) + incoming.bind(SOCKET_PUB) + outgoing = self.context.socket(zmq.XPUB) + outgoing.bind(SOCKET_SUB) + + # Blocking: This will unblock (via exception) when we destroy the context + # The incoming and outgoing sockets will be closed automatically + # when the context is destroyed as well. + try: + zmq.proxy(incoming, outgoing) + except zmq.ZMQError: + pass + + +class ZmqProxy: + """Proxies video and audio detections.""" + + def __init__(self) -> None: + self.context = zmq.Context() + self.runner = ZmqProxyRunner(self.context) + self.runner.start() + + def stop(self) -> None: + # destroying the context will tell the proxy to stop + self.context.destroy(linger=0) + self.runner.join() + + +T = TypeVar("T") + + +class Publisher(Generic[T]): + """Publishes messages.""" + + topic_base: str = "" + + def __init__(self, topic: str = "") -> None: + self.topic = f"{self.topic_base}{topic}" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.PUB) + self.socket.connect(SOCKET_PUB) + + def publish(self, payload: T, sub_topic: str = "") -> None: + """Publish message.""" + self.socket.send_string(f"{self.topic}{sub_topic} {json.dumps(payload)}") + + def stop(self) -> None: + self.socket.close(linger=0) + self.context.destroy(linger=0) + + +class Subscriber(Generic[T]): + """Receives messages.""" + + topic_base: str = "" + + def __init__(self, topic: str = "") -> None: + self.topic = f"{self.topic_base}{topic}" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.SUB) + self.socket.setsockopt_string(zmq.SUBSCRIBE, self.topic) + self.socket.connect(SOCKET_SUB) + + def check_for_update(self, timeout: float | None = FAST_QUEUE_TIMEOUT) -> T | None: + """Returns message or None if no update.""" + try: + has_update, _, _ = zmq.select([self.socket], [], [], timeout) + + if has_update: + parts = self.socket.recv_string(flags=zmq.NOBLOCK).split(maxsplit=1) + return self._return_object(parts[0], json.loads(parts[1])) + except zmq.ZMQError: + pass + + return self._return_object("", None) + + def stop(self) -> None: + self.socket.close(linger=0) + self.context.destroy(linger=0) + + def _return_object(self, topic: str, payload: T | None) -> T | None: + return payload diff --git a/frigate/config/__init__.py b/frigate/config/__init__.py new file mode 100644 index 0000000..88f7b79 --- /dev/null +++ b/frigate/config/__init__.py @@ -0,0 +1,15 @@ +from frigate.detectors import DetectorConfig, ModelConfig # noqa: F401 + +from .auth import * # noqa: F403 +from .camera import * # noqa: F403 +from .camera_group import * # noqa: F403 +from .classification import * # noqa: F403 +from .config import * # noqa: F403 +from .database import * # noqa: F403 +from .logger import * # noqa: F403 +from .mqtt import * # noqa: F403 +from .network import * # noqa: F403 +from .proxy import * # noqa: F403 +from .telemetry import * # noqa: F403 +from .tls import * # noqa: F403 +from .ui import * # noqa: F403 diff --git a/frigate/config/auth.py b/frigate/config/auth.py new file mode 100644 index 0000000..04beeb7 --- /dev/null +++ b/frigate/config/auth.py @@ -0,0 +1,103 @@ +from pydantic import Field, field_validator, model_validator + +from .base import FrigateBaseModel + +__all__ = ["AuthConfig"] + + +class AuthConfig(FrigateBaseModel): + enabled: bool = Field( + default=True, + title="Enable authentication", + description="Enable native authentication for the Frigate UI.", + ) + reset_admin_password: bool = Field( + default=False, + title="Reset admin password", + description="If true, reset the admin user's password on startup and print the new password in logs.", + ) + cookie_name: str = Field( + default="frigate_token", + title="JWT cookie name", + description="Name of the cookie used to store the JWT token for native authentication.", + pattern=r"^[a-z_]+$", + ) + cookie_secure: bool = Field( + default=False, + title="Secure cookie flag", + description="Set the secure flag on the auth cookie; should be true when using TLS.", + ) + session_length: int = Field( + default=86400, + title="Session length", + description="Session duration in seconds for JWT-based sessions.", + ge=60, + ) + refresh_time: int = Field( + default=1800, + title="Session refresh window", + description="When a session is within this many seconds of expiring, refresh it back to full length.", + ge=30, + ) + failed_login_rate_limit: str | None = Field( + default=None, + title="Failed login limits", + description="Rate limiting rules for failed login attempts to reduce brute-force attacks.", + ) + trusted_proxies: list[str] = Field( + default=[], + title="Trusted proxies", + description="List of trusted proxy IPs used when determining client IP for rate limiting.", + ) + # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256 + hash_iterations: int = Field( + default=600000, + title="Hash iterations", + description="Number of PBKDF2-SHA256 iterations to use when hashing user passwords.", + ) + roles: dict[str, list[str]] = Field( + default_factory=dict, + title="Role mappings", + description="Map roles to camera lists. An empty list grants access to all cameras for the role.", + ) + admin_first_time_login: bool | None = Field( + default=False, + title="First-time admin flag", + description=( + "When true the UI may show a help link on the login page informing users how to sign in after an admin password reset. " + ), + ) + + @field_validator("roles") + @classmethod + def validate_roles(cls, v: dict[str, list[str]]) -> dict[str, list[str]]: + # Ensure role names are valid (alphanumeric with underscores) + for role in v.keys(): + if not role.replace("_", "").isalnum(): + raise ValueError( + f"Invalid role name '{role}'. Must be alphanumeric with underscores." + ) + + # Ensure 'admin' and 'viewer' are not used as custom role names + reserved_roles = {"admin", "viewer"} + if v.keys() & reserved_roles: + raise ValueError( + f"Reserved roles {reserved_roles} cannot be used as custom roles." + ) + + # Ensure no role has an empty camera list + for role, allowed_cameras in v.items(): + if not allowed_cameras: + raise ValueError( + f"Role '{role}' has no cameras assigned. Custom roles must have at least one camera." + ) + + return v + + @model_validator(mode="after") + def ensure_default_roles(self): + # Ensure admin and viewer are never overridden + self.roles["admin"] = [] + self.roles["viewer"] = [] + + return self diff --git a/frigate/config/base.py b/frigate/config/base.py new file mode 100644 index 0000000..1e369e2 --- /dev/null +++ b/frigate/config/base.py @@ -0,0 +1,29 @@ +from typing import Any + +from pydantic import BaseModel, ConfigDict + + +class FrigateBaseModel(BaseModel): + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + def get_nested_object(self, path: str) -> Any: + parts = path.split("/") + obj = self + for part in parts: + if part == "config": + continue + + if isinstance(obj, BaseModel): + try: + obj = getattr(obj, part) + except AttributeError: + return None + elif isinstance(obj, dict): + try: + obj = obj[part] + except KeyError: + return None + else: + return None + + return obj diff --git a/frigate/config/camera/__init__.py b/frigate/config/camera/__init__.py new file mode 100644 index 0000000..573fd2e --- /dev/null +++ b/frigate/config/camera/__init__.py @@ -0,0 +1,17 @@ +from .audio import * # noqa: F403 +from .birdseye import * # noqa: F403 +from .camera import * # noqa: F403 +from .detect import * # noqa: F403 +from .ffmpeg import * # noqa: F403 +from .genai import * # noqa: F403 +from .live import * # noqa: F403 +from .motion import * # noqa: F403 +from .mqtt import * # noqa: F403 +from .objects import * # noqa: F403 +from .onvif import * # noqa: F403 +from .record import * # noqa: F403 +from .review import * # noqa: F403 +from .snapshots import * # noqa: F403 +from .timestamp import * # noqa: F403 +from .ui import * # noqa: F403 +from .zone import * # noqa: F403 diff --git a/frigate/config/camera/audio.py b/frigate/config/camera/audio.py new file mode 100644 index 0000000..813c298 --- /dev/null +++ b/frigate/config/camera/audio.py @@ -0,0 +1,59 @@ +from pydantic import Field + +from frigate.const import AUDIO_MIN_CONFIDENCE + +from ..base import FrigateBaseModel + +__all__ = ["AudioConfig", "AudioFilterConfig"] + + +DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "speech", "yell"] + + +class AudioFilterConfig(FrigateBaseModel): + threshold: float = Field( + default=0.8, + ge=AUDIO_MIN_CONFIDENCE, + lt=1.0, + title="Minimum audio confidence", + description="Minimum confidence threshold for the audio event to be counted.", + ) + + +class AudioConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable audio detection", + description="Enable or disable audio event detection for all cameras; can be overridden per-camera.", + ) + max_not_heard: int = Field( + default=30, + title="End timeout", + description="Amount of seconds without the configured audio type before the audio event is ended.", + ) + min_volume: int = Field( + default=500, + title="Minimum volume", + description="Minimum RMS volume threshold required to run audio detection; lower values increase sensitivity (e.g., 200 high, 500 medium, 1000 low).", + ) + listen: list[str] = Field( + default=DEFAULT_LISTEN_AUDIO, + title="Listen types", + description="List of audio event types to detect (for example: bark, fire_alarm, speech, yell).", + ) + filters: dict[str, AudioFilterConfig] | None = Field( + None, + title="Audio filters", + description="Per-audio-type filter settings such as confidence thresholds used to reduce false positives.", + ) + enabled_in_config: bool | None = Field( + None, + title="Original audio state", + description="Indicates whether audio detection was originally enabled in the static config file.", + ) + num_threads: int = Field( + default=2, + title="Detection threads", + description="Number of threads to use for audio detection processing.", + ge=1, + ) diff --git a/frigate/config/camera/birdseye.py b/frigate/config/camera/birdseye.py new file mode 100644 index 0000000..b51c73d --- /dev/null +++ b/frigate/config/camera/birdseye.py @@ -0,0 +1,115 @@ +from enum import Enum + +from pydantic import BaseModel, Field + +from ..base import FrigateBaseModel + +__all__ = [ + "BirdseyeCameraConfig", + "BirdseyeConfig", + "BirdseyeLayoutConfig", + "BirdseyeModeEnum", +] + + +class BirdseyeModeEnum(str, Enum): + objects = "objects" + motion = "motion" + continuous = "continuous" + + @classmethod + def get_index(cls, type): + return list(cls).index(type) + + @classmethod + def get(cls, index): + return list(cls)[index] + + +class BirdseyeLayoutConfig(FrigateBaseModel): + scaling_factor: float = Field( + default=2.0, + title="Scaling factor", + description="Scaling factor used by the layout calculator (range 1.0 to 5.0).", + ge=1.0, + le=5.0, + ) + max_cameras: int | None = Field( + default=None, + title="Max cameras", + description="Maximum number of cameras to display at once in Birdseye; shows the most recent cameras.", + ) + + +class BirdseyeConfig(FrigateBaseModel): + enabled: bool = Field( + default=True, + title="Enable Birdseye", + description="Enable or disable the Birdseye view feature.", + ) + mode: BirdseyeModeEnum = Field( + default=BirdseyeModeEnum.objects, + title="Tracking mode", + description="Mode for including cameras in Birdseye: 'objects', 'motion', or 'continuous'.", + ) + + restream: bool = Field( + default=False, + title="Restream RTSP", + description="Re-stream the Birdseye output as an RTSP feed; enabling this will keep Birdseye running continuously.", + ) + width: int = Field( + default=1280, + title="Width", + description="Output width (pixels) of the composed Birdseye frame.", + ) + height: int = Field( + default=720, + title="Height", + description="Output height (pixels) of the composed Birdseye frame.", + ) + quality: int = Field( + default=8, + title="Encoding quality", + description="Encoding quality for the Birdseye mpeg1 feed (1 highest quality, 31 lowest).", + ge=1, + le=31, + ) + inactivity_threshold: int = Field( + default=30, + title="Inactivity threshold", + description="Seconds of inactivity after which a camera will stop being shown in Birdseye.", + gt=0, + ) + layout: BirdseyeLayoutConfig = Field( + default_factory=BirdseyeLayoutConfig, + title="Layout", + description="Layout options for the Birdseye composition.", + ) + idle_heartbeat_fps: float = Field( + default=0.0, + ge=0.0, + le=10.0, + title="Idle heartbeat FPS", + description="Frames-per-second to resend the last composed Birdseye frame when idle; set to 0 to disable.", + ) + + +# uses BaseModel because some global attributes are not available at the camera level +class BirdseyeCameraConfig(BaseModel): + enabled: bool = Field( + default=True, + title="Enable Birdseye", + description="Enable or disable the Birdseye view feature.", + ) + mode: BirdseyeModeEnum = Field( + default=BirdseyeModeEnum.objects, + title="Tracking mode", + description="Mode for including cameras in Birdseye: 'objects', 'motion', or 'continuous'.", + ) + + order: int = Field( + default=0, + title="Position", + description="Numeric position controlling the camera's ordering in the Birdseye layout.", + ) diff --git a/frigate/config/camera/camera.py b/frigate/config/camera/camera.py new file mode 100644 index 0000000..b9d2cef --- /dev/null +++ b/frigate/config/camera/camera.py @@ -0,0 +1,343 @@ +import os +from enum import Enum + +from pydantic import Field, PrivateAttr, model_validator + +from frigate.const import CACHE_DIR, CACHE_SEGMENT_FORMAT, REGEX_CAMERA_NAME +from frigate.ffmpeg_presets import ( + parse_preset_hardware_acceleration_decode, + parse_preset_hardware_acceleration_scale, + parse_preset_input, + parse_preset_output_record, +) +from frigate.util.builtin import ( + escape_special_characters, + generate_color_palette, + get_ffmpeg_arg_list, +) + +from ..base import FrigateBaseModel +from ..classification import ( + CameraAudioTranscriptionConfig, + CameraFaceRecognitionConfig, + CameraLicensePlateRecognitionConfig, + CameraSemanticSearchConfig, +) +from .audio import AudioConfig +from .birdseye import BirdseyeCameraConfig +from .detect import DetectConfig +from .ffmpeg import CameraFfmpegConfig, CameraInput +from .live import CameraLiveConfig +from .motion import MotionConfig +from .mqtt import CameraMqttConfig +from .notification import NotificationConfig +from .objects import ObjectConfig +from .onvif import OnvifConfig +from .profile import CameraProfileConfig +from .record import RecordConfig +from .review import ReviewConfig +from .snapshots import SnapshotsConfig +from .timestamp import TimestampStyleConfig +from .ui import CameraUiConfig +from .zone import ZoneConfig + +__all__ = ["CameraConfig"] + + +class CameraTypeEnum(str, Enum): + generic = "generic" + lpr = "lpr" + + +class CameraConfig(FrigateBaseModel): + name: str | None = Field( + None, + title="Camera name", + description="Camera name is required", + pattern=REGEX_CAMERA_NAME, + ) + + friendly_name: str | None = Field( + None, + title="Friendly name", + description="Camera friendly name used in the Frigate UI", + ) + + @model_validator(mode="before") + @classmethod + def handle_friendly_name(cls, values): + if isinstance(values, dict) and "friendly_name" in values: + pass + return values + + enabled: bool = Field(default=True, title="Enabled", description="Enabled") + + # Options with global fallback + audio: AudioConfig = Field( + default_factory=AudioConfig, + title="Audio detection", + description="Settings for audio-based event detection for this camera.", + ) + audio_transcription: CameraAudioTranscriptionConfig = Field( + default_factory=CameraAudioTranscriptionConfig, + title="Audio transcription", + description="Settings for live and speech audio transcription used for events and live captions.", + ) + birdseye: BirdseyeCameraConfig = Field( + default_factory=BirdseyeCameraConfig, + title="Birdseye", + description="Settings for the Birdseye composite view that composes multiple camera feeds into a single layout.", + ) + detect: DetectConfig = Field( + default_factory=DetectConfig, + title="Object Detection", + description="Settings for the detection/detect role used to run object detection and initialize trackers.", + ) + face_recognition: CameraFaceRecognitionConfig = Field( + default_factory=CameraFaceRecognitionConfig, + title="Face recognition", + description="Settings for face detection and recognition for this camera.", + ) + ffmpeg: CameraFfmpegConfig = Field( + title="Streams (FFmpeg)", + description="Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.", + ) + live: CameraLiveConfig = Field( + default_factory=CameraLiveConfig, + title="Live playback", + description="Settings used by the Web UI to control live stream selection, resolution and quality.", + ) + lpr: CameraLicensePlateRecognitionConfig = Field( + default_factory=CameraLicensePlateRecognitionConfig, + title="License Plate Recognition", + description="License plate recognition settings including detection thresholds, formatting, and known plates.", + ) + motion: MotionConfig = Field( + None, + title="Motion detection", + description="Default motion detection settings for this camera.", + ) + objects: ObjectConfig = Field( + default_factory=ObjectConfig, + title="Objects", + description="Object tracking defaults including which labels to track and per-object filters.", + ) + record: RecordConfig = Field( + default_factory=RecordConfig, + title="Recording", + description="Recording and retention settings for this camera.", + ) + review: ReviewConfig = Field( + default_factory=ReviewConfig, + title="Review", + description="Settings that control alerts, detections, and GenAI review summaries used by the UI and storage for this camera.", + ) + semantic_search: CameraSemanticSearchConfig = Field( + default_factory=CameraSemanticSearchConfig, + title="Semantic Search", + description="Settings for semantic search which builds and queries object embeddings to find similar items.", + ) + snapshots: SnapshotsConfig = Field( + default_factory=SnapshotsConfig, + title="Snapshots", + description="Settings for API-generated snapshots of tracked objects for this camera.", + ) + timestamp_style: TimestampStyleConfig = Field( + default_factory=TimestampStyleConfig, + title="Timestamp style", + description="Styling options for timestamps applied to snapshots and Debug view.", + ) + + # Options without global fallback + best_image_timeout: int = Field( + default=60, + title="Best image timeout", + description="How long to wait for the image with the highest confidence score.", + ) + mqtt: CameraMqttConfig = Field( + default_factory=CameraMqttConfig, + title="MQTT", + description="MQTT image publishing settings.", + ) + notifications: NotificationConfig = Field( + default_factory=NotificationConfig, + title="Notifications", + description="Settings to enable and control notifications for this camera.", + ) + onvif: OnvifConfig = Field( + default_factory=OnvifConfig, + title="ONVIF", + description="ONVIF connection and PTZ autotracking settings for this camera.", + ) + type: CameraTypeEnum = Field( + default=CameraTypeEnum.generic, + title="Camera type", + description="Camera Type", + ) + ui: CameraUiConfig = Field( + default_factory=CameraUiConfig, + title="Camera UI", + description="Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.", + ) + webui_url: str | None = Field( + None, + title="Camera URL", + description="URL to visit the camera directly from system page", + ) + + profiles: dict[str, CameraProfileConfig] = Field( + default_factory=dict, + title="Profiles", + description="Named config profiles with partial overrides that can be activated at runtime.", + ) + zones: dict[str, ZoneConfig] = Field( + default_factory=dict, + title="Zones", + description="Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.", + ) + enabled_in_config: bool | None = Field( + default=None, + title="Original camera state", + description="Keep track of original state of camera.", + ) + + _ffmpeg_cmds: list[dict[str, list[str]]] = PrivateAttr() + + def __init__(self, **config): + # Set zone colors + if "zones" in config: + colors = generate_color_palette(len(config["zones"])) + + config["zones"] = { + name: {**z, "color": color} + for (name, z), color in zip(config["zones"].items(), colors) + } + + # add roles to the input if there is only one + if len(config["ffmpeg"]["inputs"]) == 1: + has_audio = "audio" in config["ffmpeg"]["inputs"][0].get("roles", []) + + config["ffmpeg"]["inputs"][0]["roles"] = [ + "record", + "detect", + ] + + if has_audio: + config["ffmpeg"]["inputs"][0]["roles"].append("audio") + + super().__init__(**config) + + @property + def frame_shape(self) -> tuple[int, int]: + return self.detect.height, self.detect.width + + @property + def frame_shape_yuv(self) -> tuple[int, int]: + return self.detect.height * 3 // 2, self.detect.width + + @property + def ffmpeg_cmds(self) -> list[dict[str, list[str]]]: + return self._ffmpeg_cmds + + def get_formatted_name(self) -> str: + """Return the friendly name if set, otherwise return a formatted version of the camera name.""" + if self.friendly_name: + return self.friendly_name + return self.name.replace("_", " ").title() if self.name else "" + + def create_ffmpeg_cmds(self): + if "_ffmpeg_cmds" in self: + 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 = [] + for ffmpeg_input in self.ffmpeg.inputs: + ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input) + if ffmpeg_cmd is None: + continue + + ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd}) + self._ffmpeg_cmds = ffmpeg_cmds + + def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput): + ffmpeg_output_args = [] + if "detect" in ffmpeg_input.roles: + detect_args = get_ffmpeg_arg_list(self.ffmpeg.output_args.detect) + scale_detect_args = parse_preset_hardware_acceleration_scale( + ffmpeg_input.hwaccel_args or self.ffmpeg.hwaccel_args, + detect_args, + self.detect.fps, + self.detect.width, + self.detect.height, + ) + + ffmpeg_output_args = scale_detect_args + ffmpeg_output_args + ["pipe:"] + + if "record" in ffmpeg_input.roles and self.record.enabled: + record_args = get_ffmpeg_arg_list( + parse_preset_output_record( + self.ffmpeg.output_args.record, + self.ffmpeg.apple_compatibility, + ) + or self.ffmpeg.output_args.record + ) + + ffmpeg_output_args = ( + record_args + + [f"{os.path.join(CACHE_DIR, self.name)}@{CACHE_SEGMENT_FORMAT}.mp4"] + + ffmpeg_output_args + ) + + # if there aren't any outputs enabled for this input + if len(ffmpeg_output_args) == 0: + return None + + global_args = get_ffmpeg_arg_list( + ffmpeg_input.global_args or self.ffmpeg.global_args + ) + + camera_arg = ( + self.ffmpeg.hwaccel_args if self.ffmpeg.hwaccel_args != "auto" else None + ) + hwaccel_args = get_ffmpeg_arg_list( + parse_preset_hardware_acceleration_decode( + ffmpeg_input.hwaccel_args, + self.detect.fps, + self.detect.width, + self.detect.height, + self.ffmpeg.gpu, + ) + or ffmpeg_input.hwaccel_args + or parse_preset_hardware_acceleration_decode( + camera_arg, + self.detect.fps, + self.detect.width, + self.detect.height, + self.ffmpeg.gpu, + ) + or camera_arg + or [] + ) + input_args = get_ffmpeg_arg_list( + parse_preset_input(ffmpeg_input.input_args, self.detect.fps) + or ffmpeg_input.input_args + or parse_preset_input(self.ffmpeg.input_args, self.detect.fps) + or self.ffmpeg.input_args + ) + + cmd = ( + [self.ffmpeg.ffmpeg_path] + + global_args + + (hwaccel_args if "detect" in ffmpeg_input.roles else []) + + input_args + + ["-i", escape_special_characters(ffmpeg_input.path)] + + ffmpeg_output_args + ) + + return [part for part in cmd if part != ""] diff --git a/frigate/config/camera/detect.py b/frigate/config/camera/detect.py new file mode 100644 index 0000000..d093ed9 --- /dev/null +++ b/frigate/config/camera/detect.py @@ -0,0 +1,96 @@ +from pydantic import Field, model_validator + +from ..base import FrigateBaseModel + +__all__ = ["DetectConfig", "StationaryConfig", "StationaryMaxFramesConfig"] + + +class StationaryMaxFramesConfig(FrigateBaseModel): + default: int | None = Field( + default=None, + title="Default max frames", + description="Default maximum frames to track a stationary object before stopping.", + ge=1, + ) + objects: dict[str, int] = Field( + default_factory=dict, + title="Object max frames", + description="Per-object overrides for maximum frames to track stationary objects.", + ) + + +class StationaryConfig(FrigateBaseModel): + interval: int | None = Field( + default=None, + title="Stationary interval", + description="How often (in frames) to run a detection check to confirm a stationary object.", + gt=0, + ) + threshold: int | None = Field( + default=None, + title="Stationary threshold", + description="Number of frames with no position change required to mark an object as stationary.", + ge=1, + ) + max_frames: StationaryMaxFramesConfig = Field( + default_factory=StationaryMaxFramesConfig, + title="Max frames", + description="Limits how long stationary objects are tracked before being discarded.", + ) + classifier: bool = Field( + default=True, + title="Enable visual classifier", + description="Use a visual classifier to detect truly stationary objects even when bounding boxes jitter.", + ) + + +class DetectConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable object detection", + description="Enable or disable object detection for all cameras; can be overridden per-camera.", + ) + height: int | None = Field( + default=None, + title="Detect height", + description="Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", + ) + width: int | None = Field( + default=None, + title="Detect width", + description="Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", + ) + fps: int = Field( + default=5, + title="Detect FPS", + description="Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects).", + ) + min_initialized: int | None = Field( + default=None, + title="Minimum initialization frames", + description="Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2.", + ge=2, + ) + max_disappeared: int | None = Field( + default=None, + title="Maximum disappeared frames", + description="Number of frames without a detection before a tracked object is considered gone.", + ) + stationary: StationaryConfig = Field( + default_factory=StationaryConfig, + title="Stationary objects config", + description="Settings to detect and manage objects that remain stationary for a period of time.", + ) + annotation_offset: int = Field( + default=0, + title="Annotation offset", + description="Milliseconds to shift detect annotations to better align timeline bounding boxes with recordings; can be positive or negative.", + ) + + @model_validator(mode="after") + def validate_dimensions(self) -> "DetectConfig": + if (self.width is None) != (self.height is None): + raise ValueError( + "detect -> both width and height must be specified together, or both omitted" + ) + return self diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py new file mode 100644 index 0000000..ad7cbc8 --- /dev/null +++ b/frigate/config/camera/ffmpeg.py @@ -0,0 +1,148 @@ +from enum import Enum + +from pydantic import Field, field_validator + +from frigate.util.config import resolve_ffmpeg_path + +from ..base import FrigateBaseModel +from ..env import EnvString + +__all__ = [ + "CameraFfmpegConfig", + "CameraInput", + "CameraRoleEnum", + "FfmpegConfig", + "FfmpegOutputArgsConfig", +] + +# Note: Setting threads to less than 2 caused several issues with recording segments +# https://github.com/blakeblackshear/frigate/issues/5659 +FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning", "-threads", "2"] +FFMPEG_INPUT_ARGS_DEFAULT = "preset-rtsp-generic" + +RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = "preset-record-generic-audio-aac" +DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [ + "-threads", + "2", + "-f", + "rawvideo", + "-pix_fmt", + "yuv420p", +] + + +class FfmpegOutputArgsConfig(FrigateBaseModel): + detect: str | list[str] = Field( + default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT, + title="Detect output arguments", + description="Default output arguments for detect role streams.", + ) + record: str | list[str] = Field( + default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT, + title="Record output arguments", + description="Default output arguments for record role streams.", + ) + + +class FfmpegConfig(FrigateBaseModel): + path: str = Field( + default="default", + title="FFmpeg path", + description='Path to the FFmpeg binary to use or a version alias ("7.0" or "8.0").', + ) + global_args: str | list[str] = Field( + default=FFMPEG_GLOBAL_ARGS_DEFAULT, + title="FFmpeg global arguments", + description="Global arguments passed to FFmpeg processes.", + ) + hwaccel_args: str | list[str] = Field( + default="auto", + title="Hardware acceleration arguments", + description="Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.", + ) + input_args: str | list[str] = Field( + default=FFMPEG_INPUT_ARGS_DEFAULT, + title="Input arguments", + description="Input arguments applied to FFmpeg input streams.", + ) + output_args: FfmpegOutputArgsConfig = Field( + default_factory=FfmpegOutputArgsConfig, + title="Output arguments", + description="Default output arguments used for different FFmpeg roles such as detect and record.", + ) + retry_interval: float = Field( + default=10.0, + title="FFmpeg retry time", + description="Seconds to wait before attempting to reconnect a camera stream after failure. Default is 10.", + gt=0.0, + ) + apple_compatibility: bool = Field( + default=False, + title="Apple compatibility", + description="Enable HEVC tagging for better Apple player compatibility when recording H.265.", + ) + gpu: int = Field( + default=0, + title="GPU index", + description="Default GPU index used for hardware acceleration if available.", + ) + + @property + def ffmpeg_path(self) -> str: + return resolve_ffmpeg_path(self.path, "ffmpeg") + + @property + def ffprobe_path(self) -> str: + return resolve_ffmpeg_path(self.path, "ffprobe") + + +class CameraRoleEnum(str, Enum): + audio = "audio" + record = "record" + detect = "detect" + + +class CameraInput(FrigateBaseModel): + path: EnvString = Field( + title="Input path", + description="Camera input stream URL or path.", + ) + roles: list[CameraRoleEnum] = Field( + title="Input roles", + description="Roles for this input stream.", + ) + global_args: str | list[str] = Field( + default_factory=list, + title="FFmpeg global arguments", + description="FFmpeg global arguments for this input stream.", + ) + hwaccel_args: str | list[str] = Field( + default_factory=list, + title="Hardware acceleration arguments", + description="Hardware acceleration arguments for this input stream.", + ) + input_args: str | list[str] = Field( + default_factory=list, + title="Input arguments", + description="Input arguments specific to this stream.", + ) + + +class CameraFfmpegConfig(FfmpegConfig): + inputs: list[CameraInput] = Field( + title="Camera inputs", + description="List of input stream definitions (paths and roles) for this camera.", + ) + + @field_validator("inputs") + @classmethod + def validate_roles(cls, v): + roles = [role for input in v for role in input.roles] + + if len(roles) != len(set(roles)): + raise ValueError("Each input role may only be used once.") + + if "detect" not in roles: + raise ValueError("The detect role is required.") + + return v diff --git a/frigate/config/camera/genai.py b/frigate/config/camera/genai.py new file mode 100644 index 0000000..8235825 --- /dev/null +++ b/frigate/config/camera/genai.py @@ -0,0 +1,68 @@ +from enum import Enum +from typing import Any + +from pydantic import Field + +from ..base import FrigateBaseModel +from ..env import EnvString + +__all__ = ["GenAIConfig", "GenAIProviderEnum", "GenAIRoleEnum"] + + +class GenAIProviderEnum(str, Enum): + openai = "openai" + azure_openai = "azure_openai" + gemini = "gemini" + ollama = "ollama" + llamacpp = "llamacpp" + + +class GenAIRoleEnum(str, Enum): + chat = "chat" + descriptions = "descriptions" + embeddings = "embeddings" + + +class GenAIConfig(FrigateBaseModel): + """Primary GenAI Config to define GenAI Provider.""" + + api_key: EnvString | None = Field( + default=None, + title="API key", + description="API key required by some providers (can also be set via environment variables).", + ) + base_url: str | None = Field( + default=None, + title="Base URL", + description="Base URL for self-hosted or compatible providers (for example an Ollama instance).", + ) + model: str = Field( + default="", + title="Model", + description="The model to use from the provider for generating descriptions or summaries.", + ) + provider: GenAIProviderEnum = Field( + title="Provider", + description="The GenAI provider to use (for example: ollama, gemini, openai).", + ) + roles: list[GenAIRoleEnum] = Field( + default_factory=lambda: [ + GenAIRoleEnum.embeddings, + GenAIRoleEnum.descriptions, + GenAIRoleEnum.chat, + ], + title="Roles", + description="GenAI roles (chat, descriptions, embeddings); one provider per role.", + ) + provider_options: dict[str, Any] = Field( + default={}, + title="Provider options", + description="Additional provider-specific options to pass to the GenAI client.", + json_schema_extra={"additionalProperties": {}}, + ) + runtime_options: dict[str, Any] = Field( + default={}, + title="Runtime options", + description="Runtime options passed to the provider for each inference call.", + json_schema_extra={"additionalProperties": {}}, + ) diff --git a/frigate/config/camera/live.py b/frigate/config/camera/live.py new file mode 100644 index 0000000..90a7c8e --- /dev/null +++ b/frigate/config/camera/live.py @@ -0,0 +1,25 @@ +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = ["CameraLiveConfig"] + + +class CameraLiveConfig(FrigateBaseModel): + streams: dict[str, str] = Field( + default_factory=list, + title="Live stream names", + description="Mapping of configured stream names to restream/go2rtc names used for live playback.", + ) + height: int = Field( + default=720, + title="Live height", + description="Height (pixels) to render the jsmpeg live stream in the Web UI; must be <= detect stream height.", + ) + quality: int = Field( + default=8, + ge=1, + le=31, + title="Live quality", + description="Encoding quality for the jsmpeg stream (1 highest, 31 lowest).", + ) diff --git a/frigate/config/camera/mask.py b/frigate/config/camera/mask.py new file mode 100644 index 0000000..ecfdb41 --- /dev/null +++ b/frigate/config/camera/mask.py @@ -0,0 +1,85 @@ +"""Mask configuration for motion and object masks.""" + +from typing import Any + +from pydantic import Field, field_serializer + +from ..base import FrigateBaseModel + +__all__ = ["MotionMaskConfig", "ObjectMaskConfig"] + + +class MotionMaskConfig(FrigateBaseModel): + """Configuration for a single motion mask.""" + + friendly_name: str | None = Field( + default=None, + title="Friendly name", + description="A friendly name for this motion mask used in the Frigate UI", + ) + enabled: bool = Field( + default=True, + title="Enabled", + description="Enable or disable this motion mask", + ) + coordinates: str | list[str] = Field( + default="", + title="Coordinates", + description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", + ) + raw_coordinates: str | list[str] = "" + enabled_in_config: bool | None = Field( + default=None, title="Keep track of original state of motion mask." + ) + + def get_formatted_name(self, mask_id: str) -> str: + """Return the friendly name if set, otherwise return a formatted version of the mask ID.""" + if self.friendly_name: + return self.friendly_name + return mask_id.replace("_", " ").title() + + @field_serializer("coordinates", when_used="json") + def serialize_coordinates(self, value: Any, info): + return self.raw_coordinates if self.raw_coordinates else value + + @field_serializer("raw_coordinates", when_used="json") + def serialize_raw_coordinates(self, value: Any, info): + return None + + +class ObjectMaskConfig(FrigateBaseModel): + """Configuration for a single object mask.""" + + friendly_name: str | None = Field( + default=None, + title="Friendly name", + description="A friendly name for this object mask used in the Frigate UI", + ) + enabled: bool = Field( + default=True, + title="Enabled", + description="Enable or disable this object mask", + ) + coordinates: str | list[str] = Field( + default="", + title="Coordinates", + description="Ordered x,y coordinates defining the object mask polygon used to include/exclude areas.", + ) + raw_coordinates: str | list[str] = "" + enabled_in_config: bool | None = Field( + default=None, title="Keep track of original state of object mask." + ) + + @field_serializer("coordinates", when_used="json") + def serialize_coordinates(self, value: Any, info): + return self.raw_coordinates if self.raw_coordinates else value + + @field_serializer("raw_coordinates", when_used="json") + def serialize_raw_coordinates(self, value: Any, info): + return None + + def get_formatted_name(self, mask_id: str) -> str: + """Return the friendly name if set, otherwise return a formatted version of the mask ID.""" + if self.friendly_name: + return self.friendly_name + return mask_id.replace("_", " ").title() diff --git a/frigate/config/camera/motion.py b/frigate/config/camera/motion.py new file mode 100644 index 0000000..c44c145 --- /dev/null +++ b/frigate/config/camera/motion.py @@ -0,0 +1,90 @@ +from typing import Any + +from pydantic import Field, field_serializer + +from ..base import FrigateBaseModel +from .mask import MotionMaskConfig + +__all__ = ["MotionConfig"] + + +class MotionConfig(FrigateBaseModel): + enabled: bool = Field( + default=True, + title="Enable motion detection", + description="Enable or disable motion detection for all cameras; can be overridden per-camera.", + ) + threshold: int = Field( + default=30, + title="Motion threshold", + description="Pixel difference threshold used by the motion detector; higher values reduce sensitivity (range 1-255).", + ge=1, + le=255, + ) + lightning_threshold: float = Field( + default=0.8, + title="Lightning threshold", + description="Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events.", + ge=0.3, + le=1.0, + ) + skip_motion_threshold: float | None = Field( + default=None, + title="Skip motion threshold", + description="If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.", + ge=0.0, + le=1.0, + ) + improve_contrast: bool = Field( + default=True, + title="Improve contrast", + description="Apply contrast improvement to frames before motion analysis to help detection.", + ) + contour_area: int | None = Field( + default=10, + title="Contour area", + description="Minimum contour area in pixels required for a motion contour to be counted.", + ) + delta_alpha: float = Field( + default=0.2, + title="Delta alpha", + description="Alpha blending factor used in frame differencing for motion calculation.", + ) + frame_alpha: float = Field( + default=0.01, + title="Frame alpha", + description="Alpha value used when blending frames for motion preprocessing.", + ) + frame_height: int | None = Field( + default=100, + title="Frame height", + description="Height in pixels to scale frames to when computing motion.", + ) + mask: dict[str, MotionMaskConfig | None] = Field( + default_factory=dict, + title="Mask coordinates", + description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", + ) + mqtt_off_delay: int = Field( + default=30, + title="MQTT off delay", + description="Seconds to wait after last motion before publishing an MQTT 'off' state.", + ) + enabled_in_config: bool | None = Field( + default=None, + title="Original motion state", + description="Indicates whether motion detection was enabled in the original static configuration.", + ) + raw_mask: dict[str, MotionMaskConfig | None] = Field( + default_factory=dict, exclude=True + ) + + @field_serializer("mask", when_used="json") + def serialize_mask(self, value: Any, info): + if self.raw_mask: + return self.raw_mask + return value + + @field_serializer("raw_mask", when_used="json") + def serialize_raw_mask(self, value: Any, info): + return None diff --git a/frigate/config/camera/mqtt.py b/frigate/config/camera/mqtt.py new file mode 100644 index 0000000..5f8da1a --- /dev/null +++ b/frigate/config/camera/mqtt.py @@ -0,0 +1,45 @@ +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = ["CameraMqttConfig"] + + +class CameraMqttConfig(FrigateBaseModel): + enabled: bool = Field( + default=True, + title="Send image", + description="Enable publishing image snapshots for objects to MQTT topics for this camera.", + ) + timestamp: bool = Field( + default=True, + title="Add timestamp", + description="Overlay a timestamp on images published to MQTT.", + ) + bounding_box: bool = Field( + default=True, + title="Add bounding box", + description="Draw bounding boxes on images published over MQTT.", + ) + crop: bool = Field( + default=True, + title="Crop image", + description="Crop images published to MQTT to the detected object's bounding box.", + ) + height: int = Field( + default=270, + title="Image height", + description="Height (pixels) to resize images published over MQTT.", + ) + required_zones: list[str] = Field( + default_factory=list, + title="Required zones", + description="Zones that an object must enter for an MQTT image to be published.", + ) + quality: int = Field( + default=70, + title="JPEG quality", + description="JPEG quality for images published to MQTT (0-100).", + ge=0, + le=100, + ) diff --git a/frigate/config/camera/notification.py b/frigate/config/camera/notification.py new file mode 100644 index 0000000..7f59681 --- /dev/null +++ b/frigate/config/camera/notification.py @@ -0,0 +1,29 @@ +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = ["NotificationConfig"] + + +class NotificationConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable notifications", + description="Enable or disable notifications for all cameras; can be overridden per-camera.", + ) + email: str | None = Field( + default=None, + title="Notification email", + description="Email address used for push notifications or required by certain notification providers.", + ) + cooldown: int = Field( + default=0, + ge=0, + title="Cooldown period", + description="Cooldown (seconds) between notifications to avoid spamming recipients.", + ) + enabled_in_config: bool | None = Field( + default=None, + title="Original notifications state", + description="Indicates whether notifications were enabled in the original static configuration.", + ) diff --git a/frigate/config/camera/objects.py b/frigate/config/camera/objects.py new file mode 100644 index 0000000..6b6759e --- /dev/null +++ b/frigate/config/camera/objects.py @@ -0,0 +1,186 @@ +from typing import Any + +from pydantic import Field, PrivateAttr, field_serializer, field_validator + +from ..base import FrigateBaseModel +from .mask import ObjectMaskConfig + +__all__ = ["ObjectConfig", "GenAIObjectConfig", "FilterConfig"] + + +DEFAULT_TRACKED_OBJECTS = ["person"] + + +class FilterConfig(FrigateBaseModel): + min_area: int | float = Field( + default=0, + title="Minimum object area", + description="Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", + ) + max_area: int | float = Field( + default=24000000, + title="Maximum object area", + description="Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", + ) + min_ratio: float = Field( + default=0, + title="Minimum aspect ratio", + description="Minimum width/height ratio required for the bounding box to qualify.", + ) + max_ratio: float = Field( + default=24000000, + title="Maximum aspect ratio", + description="Maximum width/height ratio allowed for the bounding box to qualify.", + ) + threshold: float = Field( + default=0.7, + title="Confidence threshold", + description="Average detection confidence threshold required for the object to be considered a true positive.", + ) + min_score: float = Field( + default=0.5, + title="Minimum confidence", + description="Minimum single-frame detection confidence required for the object to be counted.", + ) + mask: dict[str, ObjectMaskConfig | None] = Field( + default_factory=dict, + title="Filter mask", + description="Polygon coordinates defining where this filter applies within the frame.", + ) + raw_mask: dict[str, ObjectMaskConfig | None] = Field( + default_factory=dict, exclude=True + ) + + @field_serializer("mask", when_used="json") + def serialize_mask(self, value: Any, info): + if self.raw_mask: + return self.raw_mask + return value + + @field_serializer("raw_mask", when_used="json") + def serialize_raw_mask(self, value: Any, info): + return None + + +class GenAIObjectTriggerConfig(FrigateBaseModel): + tracked_object_end: bool = Field( + default=True, + title="Send on end", + description="Send a request to GenAI when the tracked object ends.", + ) + after_significant_updates: int | None = Field( + default=None, + title="Early GenAI trigger", + description="Send a request to GenAI after a specified number of significant updates for the tracked object.", + ge=1, + ) + + +class GenAIObjectConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable GenAI", + description="Enable GenAI generation of descriptions for tracked objects by default.", + ) + use_snapshot: bool = Field( + default=False, + title="Use snapshots", + description="Use object snapshots instead of thumbnails for GenAI description generation.", + ) + prompt: str = Field( + default="Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", + title="Caption prompt", + description="Default prompt template used when generating descriptions with GenAI.", + ) + object_prompts: dict[str, str] = Field( + default_factory=dict, + title="Object prompts", + description="Per-object prompts to customize GenAI outputs for specific labels.", + ) + + objects: str | list[str] = Field( + default_factory=list, + title="GenAI objects", + description="List of object labels to send to GenAI by default.", + ) + required_zones: str | list[str] = Field( + default_factory=list, + title="Required zones", + description="Zones that must be entered for objects to qualify for GenAI description generation.", + ) + debug_save_thumbnails: bool = Field( + default=False, + title="Save thumbnails", + description="Save thumbnails sent to GenAI for debugging and review.", + ) + send_triggers: GenAIObjectTriggerConfig = Field( + default_factory=GenAIObjectTriggerConfig, + title="GenAI triggers", + description="Defines when frames should be sent to GenAI (on end, after updates, etc.).", + ) + enabled_in_config: bool | None = Field( + default=None, + title="Original GenAI state", + description="Indicates whether GenAI was enabled in the original static config.", + ) + + @field_validator("required_zones", mode="before") + @classmethod + def validate_required_zones(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v + + +class ObjectConfig(FrigateBaseModel): + track: list[str] = Field( + default=DEFAULT_TRACKED_OBJECTS, + title="Objects to track", + description="List of object labels to track for all cameras; can be overridden per-camera.", + ) + filters: dict[str, FilterConfig] = Field( + default_factory=dict, + title="Object filters", + description="Filters applied to detected objects to reduce false positives (area, ratio, confidence).", + ) + mask: dict[str, ObjectMaskConfig | None] = Field( + default_factory=dict, + title="Object mask", + description="Mask polygon used to prevent object detection in specified areas.", + ) + raw_mask: dict[str, ObjectMaskConfig | None] = Field( + default_factory=dict, exclude=True + ) + genai: GenAIObjectConfig = Field( + default_factory=GenAIObjectConfig, + title="GenAI object config", + description="GenAI options for describing tracked objects and sending frames for generation.", + ) + _all_objects: list[str] = PrivateAttr() + + @property + def all_objects(self) -> list[str]: + return self._all_objects + + def parse_all_objects(self, cameras): + if "_all_objects" in self: + return + + # get list of unique enabled labels for tracking + enabled_labels = set(self.track) + + for camera in cameras.values(): + enabled_labels.update(camera.objects.track) + + self._all_objects = list(enabled_labels) + + @field_serializer("mask", when_used="json") + def serialize_mask(self, value: Any, info): + if self.raw_mask: + return self.raw_mask + return value + + @field_serializer("raw_mask", when_used="json") + def serialize_raw_mask(self, value: Any, info): + return None diff --git a/frigate/config/camera/onvif.py b/frigate/config/camera/onvif.py new file mode 100644 index 0000000..01c9432 --- /dev/null +++ b/frigate/config/camera/onvif.py @@ -0,0 +1,133 @@ +from enum import Enum + +from pydantic import Field, field_validator + +from ..base import FrigateBaseModel +from ..env import EnvString +from .objects import DEFAULT_TRACKED_OBJECTS + +__all__ = ["OnvifConfig", "PtzAutotrackConfig", "ZoomingModeEnum"] + + +class ZoomingModeEnum(str, Enum): + disabled = "disabled" + absolute = "absolute" + relative = "relative" + + +class PtzAutotrackConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable Autotracking", + description="Enable or disable automatic PTZ camera tracking of detected objects.", + ) + calibrate_on_startup: bool = Field( + default=False, + title="Calibrate on start", + description="Measure PTZ motor speeds on startup to improve tracking accuracy. Frigate will update config with movement_weights after calibration.", + ) + zooming: ZoomingModeEnum = Field( + default=ZoomingModeEnum.disabled, + title="Zoom mode", + description="Control zoom behavior: disabled (pan/tilt only), absolute (most compatible), or relative (concurrent pan/tilt/zoom).", + ) + zoom_factor: float = Field( + default=0.3, + title="Zoom factor", + description="Control zoom level on tracked objects. Lower values keep more scene in view; higher values zoom in closer but may lose tracking. Values between 0.1 and 0.75.", + ge=0.1, + le=0.75, + ) + track: list[str] = Field( + default=DEFAULT_TRACKED_OBJECTS, + title="Tracked objects", + description="List of object types that should trigger autotracking.", + ) + required_zones: list[str] = Field( + default_factory=list, + title="Required zones", + description="Objects must enter one of these zones before autotracking begins.", + ) + return_preset: str = Field( + default="home", + title="Return preset", + description="ONVIF preset name configured in camera firmware to return to after tracking ends.", + ) + timeout: int = Field( + default=10, + title="Return timeout", + description="Wait this many seconds after losing tracking before returning camera to preset position.", + ) + movement_weights: str | list[str] | None = Field( + default_factory=list, + title="Movement weights", + description="Calibration values automatically generated by camera calibration. Do not modify manually.", + ) + enabled_in_config: bool | None = Field( + default=None, + title="Original autotrack state", + description="Internal field to track whether autotracking was enabled in configuration.", + ) + + @field_validator("movement_weights", mode="before") + @classmethod + def validate_weights(cls, v): + if v is None: + return None + + if isinstance(v, str): + weights = list(map(str, map(float, v.split(",")))) + elif isinstance(v, list): + weights = [str(float(val)) for val in v] + else: + raise ValueError("Invalid type for movement_weights") + + if len(weights) != 6: + raise ValueError( + "movement_weights must have exactly 6 floats, remove this line from your config and run autotracking calibration" + ) + + return weights + + +class OnvifConfig(FrigateBaseModel): + host: EnvString = Field( + default="", + title="ONVIF host", + description="Host (and optional scheme) for the ONVIF service for this camera.", + ) + port: int = Field( + default=8000, + title="ONVIF port", + description="Port number for the ONVIF service.", + ) + user: EnvString | None = Field( + default=None, + title="ONVIF username", + description="Username for ONVIF authentication; some devices require admin user for ONVIF.", + ) + password: EnvString | None = Field( + default=None, + title="ONVIF password", + description="Password for ONVIF authentication.", + ) + tls_insecure: bool = Field( + default=False, + title="Disable TLS verify", + description="Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).", + ) + profile: str | None = Field( + default=None, + title="ONVIF profile", + description="Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically.", + ) + autotracking: PtzAutotrackConfig = Field( + default_factory=PtzAutotrackConfig, + title="Autotracking", + description="Automatically track moving objects and keep them centered in the frame using PTZ camera movements.", + ) + ignore_time_mismatch: bool = Field( + default=False, + title="Ignore time mismatch", + description="Ignore time synchronization differences between camera and Frigate server for ONVIF communication.", + ) diff --git a/frigate/config/camera/profile.py b/frigate/config/camera/profile.py new file mode 100644 index 0000000..e3014aa --- /dev/null +++ b/frigate/config/camera/profile.py @@ -0,0 +1,42 @@ +"""Camera profile configuration for named config overrides.""" + +from ..base import FrigateBaseModel +from ..classification import ( + CameraFaceRecognitionConfig, + CameraLicensePlateRecognitionConfig, +) +from .audio import AudioConfig +from .birdseye import BirdseyeCameraConfig +from .detect import DetectConfig +from .motion import MotionConfig +from .notification import NotificationConfig +from .objects import ObjectConfig +from .record import RecordConfig +from .review import ReviewConfig +from .snapshots import SnapshotsConfig +from .zone import ZoneConfig + +__all__ = ["CameraProfileConfig"] + + +class CameraProfileConfig(FrigateBaseModel): + """A named profile containing partial camera config overrides. + + Sections set to None inherit from the camera's base config. + Sections that are defined get Pydantic-validated, then only + explicitly-set fields are used as overrides via exclude_unset. + """ + + enabled: bool | None = None + audio: AudioConfig | None = None + birdseye: BirdseyeCameraConfig | None = None + detect: DetectConfig | None = None + face_recognition: CameraFaceRecognitionConfig | None = None + lpr: CameraLicensePlateRecognitionConfig | None = None + motion: MotionConfig | None = None + notifications: NotificationConfig | None = None + objects: ObjectConfig | None = None + record: RecordConfig | None = None + review: ReviewConfig | None = None + snapshots: SnapshotsConfig | None = None + zones: dict[str, ZoneConfig] | None = None diff --git a/frigate/config/camera/record.py b/frigate/config/camera/record.py new file mode 100644 index 0000000..44a71c9 --- /dev/null +++ b/frigate/config/camera/record.py @@ -0,0 +1,177 @@ +from enum import Enum + +from pydantic import Field + +from frigate.const import MAX_PRE_CAPTURE +from frigate.review.types import SeverityEnum + +from ..base import FrigateBaseModel + +__all__ = [ + "ChaptersEnum", + "RecordConfig", + "RecordExportConfig", + "RecordPreviewConfig", + "RecordQualityEnum", + "EventsConfig", + "ReviewRetainConfig", + "RecordRetainConfig", + "RetainModeEnum", +] + + +class RecordRetainConfig(FrigateBaseModel): + days: float = Field( + default=0, + ge=0, + title="Retention days", + description="Days to retain recordings.", + ) + + +class RetainModeEnum(str, Enum): + all = "all" + motion = "motion" + active_objects = "active_objects" + + +class ReviewRetainConfig(FrigateBaseModel): + days: float = Field( + default=10, + ge=0, + title="Retention days", + description="Number of days to retain recordings of detection events.", + ) + mode: RetainModeEnum = Field( + default=RetainModeEnum.motion, + title="Retention mode", + description="Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects).", + ) + + +class EventsConfig(FrigateBaseModel): + pre_capture: int = Field( + default=5, + title="Pre-capture seconds", + description="Number of seconds before the detection event to include in the recording.", + le=MAX_PRE_CAPTURE, + ge=0, + ) + post_capture: int = Field( + default=5, + ge=0, + title="Post-capture seconds", + description="Number of seconds after the detection event to include in the recording.", + ) + retain: ReviewRetainConfig = Field( + default_factory=ReviewRetainConfig, + title="Event retention", + description="Retention settings for recordings of detection events.", + ) + + +class RecordQualityEnum(str, Enum): + very_low = "very_low" + low = "low" + medium = "medium" + high = "high" + very_high = "very_high" + + +class RecordPreviewConfig(FrigateBaseModel): + quality: RecordQualityEnum = Field( + default=RecordQualityEnum.medium, + title="Preview quality", + description="Preview quality level (very_low, low, medium, high, very_high).", + ) + + +class ChaptersEnum(str, Enum): + none = "none" + recording_segments = "recording_segments" + review_items = "review_items" + + +class RecordExportConfig(FrigateBaseModel): + hwaccel_args: str | list[str] = Field( + default="auto", + title="Export hwaccel args", + description="Hardware acceleration args to use for export/transcode operations.", + ) + max_concurrent: int = Field( + default=3, + ge=1, + title="Maximum concurrent exports", + description="Maximum number of export jobs to process at the same time.", + ) + chapters: ChaptersEnum = Field( + default=ChaptersEnum.review_items, + title="Chapter metadata to embed in exported recordings", + ) + + +class RecordConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable recording", + description="Enable or disable recording for all cameras; can be overridden per-camera.", + ) + expire_interval: int = Field( + default=60, + title="Record cleanup interval", + description="Minutes between cleanup passes that remove expired recording segments.", + ) + continuous: RecordRetainConfig = Field( + default_factory=RecordRetainConfig, + title="Continuous retention", + description="Number of days to retain recordings regardless of tracked objects or motion. Set to 0 if you only want to retain recordings of alerts and detections.", + ) + motion: RecordRetainConfig = Field( + default_factory=RecordRetainConfig, + title="Motion retention", + description="Number of days to retain recordings triggered by motion regardless of tracked objects. Set to 0 if you only want to retain recordings of alerts and detections.", + ) + detections: EventsConfig = Field( + default_factory=EventsConfig, + title="Detection retention", + description="Recording retention settings for detection events including pre/post capture durations.", + ) + alerts: EventsConfig = Field( + default_factory=EventsConfig, + title="Alert retention", + description="Recording retention settings for alert events including pre/post capture durations.", + ) + export: RecordExportConfig = Field( + default_factory=RecordExportConfig, + title="Export config", + description="Settings used when exporting recordings such as timelapse and hardware acceleration.", + ) + preview: RecordPreviewConfig = Field( + default_factory=RecordPreviewConfig, + title="Preview config", + description="Settings controlling the quality of recording previews shown in the UI.", + ) + enabled_in_config: bool | None = Field( + default=None, + title="Original recording state", + description="Indicates whether recording was enabled in the original static configuration.", + ) + + @property + def event_pre_capture(self) -> int: + return max( + self.alerts.pre_capture, + self.detections.pre_capture, + ) + + def get_review_pre_capture(self, severity: SeverityEnum) -> int: + if severity == SeverityEnum.alert: + return self.alerts.pre_capture + else: + return self.detections.pre_capture + + def get_review_post_capture(self, severity: SeverityEnum) -> int: + if severity == SeverityEnum.alert: + return self.alerts.post_capture + else: + return self.detections.post_capture diff --git a/frigate/config/camera/review.py b/frigate/config/camera/review.py new file mode 100644 index 0000000..f267f46 --- /dev/null +++ b/frigate/config/camera/review.py @@ -0,0 +1,196 @@ +from enum import Enum + +from pydantic import Field, field_validator + +from ..base import FrigateBaseModel + +__all__ = ["ReviewConfig", "DetectionsConfig", "AlertsConfig", "ImageSourceEnum"] + + +class ImageSourceEnum(str, Enum): + """Image source options for GenAI Review.""" + + preview = "preview" + recordings = "recordings" + + +DEFAULT_ALERT_OBJECTS = ["person", "car"] + + +class AlertsConfig(FrigateBaseModel): + """Configure alerts""" + + enabled: bool = Field( + default=True, + title="Enable alerts", + description="Enable or disable alert generation for all cameras; can be overridden per-camera.", + ) + + labels: list[str] = Field( + default=DEFAULT_ALERT_OBJECTS, + title="Alert labels", + description="List of object labels that qualify as alerts (for example: car, person).", + ) + required_zones: str | list[str] = Field( + default_factory=list, + title="Required zones", + description="Zones that an object must enter to be considered an alert; leave empty to allow any zone.", + ) + + enabled_in_config: bool | None = Field( + default=None, + title="Original alerts state", + description="Tracks whether alerts were originally enabled in the static configuration.", + ) + cutoff_time: int = Field( + default=40, + title="Alerts cutoff time", + description="Seconds to wait after no alert-causing activity before cutting off an alert.", + ) + + @field_validator("required_zones", mode="before") + @classmethod + def validate_required_zones(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v + + +class DetectionsConfig(FrigateBaseModel): + """Configure detections""" + + enabled: bool = Field( + default=True, + title="Enable detections", + description="Enable or disable detection events for all cameras; can be overridden per-camera.", + ) + + labels: list[str] | None = Field( + default=None, + title="Detection labels", + description="List of object labels that qualify as detection events.", + ) + required_zones: str | list[str] = Field( + default_factory=list, + title="Required zones", + description="Zones that an object must enter to be considered a detection; leave empty to allow any zone.", + ) + cutoff_time: int = Field( + default=30, + title="Detections cutoff time", + description="Seconds to wait after no detection-causing activity before cutting off a detection.", + ) + + enabled_in_config: bool | None = Field( + default=None, + title="Original detections state", + description="Tracks whether detections were originally enabled in the static configuration.", + ) + + @field_validator("required_zones", mode="before") + @classmethod + def validate_required_zones(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v + + +class GenAIReviewConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable GenAI descriptions", + description="Enable or disable GenAI-generated descriptions and summaries for review items.", + ) + alerts: bool = Field( + default=True, + title="Enable GenAI for alerts", + description="Use GenAI to generate descriptions for alert items.", + ) + detections: bool = Field( + default=False, + title="Enable GenAI for detections", + description="Use GenAI to generate descriptions for detection items.", + ) + image_source: ImageSourceEnum = Field( + default=ImageSourceEnum.preview, + title="Review image source", + description="Source of images sent to GenAI ('preview' or 'recordings'); 'recordings' uses higher quality frames but more tokens.", + ) + additional_concerns: list[str] = Field( + default=[], + title="Additional concerns", + description="A list of additional concerns or notes the GenAI should consider when evaluating activity on this camera.", + ) + debug_save_thumbnails: bool = Field( + default=False, + title="Save thumbnails", + description="Save thumbnails that are sent to the GenAI provider for debugging and review.", + ) + enabled_in_config: bool | None = Field( + default=None, + title="Original GenAI state", + description="Tracks whether GenAI review was originally enabled in the static configuration.", + ) + preferred_language: str | None = Field( + title="Preferred language", + description="Preferred language to request from the GenAI provider for generated responses.", + default=None, + ) + activity_context_prompt: str = Field( + default="""### Normal Activity Indicators (Level 0) +- Known/verified people in any zone at any time +- People with pets in residential areas +- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items — normal commute and travel patterns +- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving +- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime +- Activity confined to public areas only (sidewalks, streets) without entering property at any time + +### Suspicious Activity Indicators (Level 1) +- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools — Level 1 +- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** — ALWAYS Level 1 regardless of activity or duration +- Taking items that don't belong to them (packages, objects from porches/driveways) +- Climbing or jumping fences/barriers to access property +- Attempting to conceal actions or items from view +- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence + +### Critical Threat Indicators (Level 2) +- Holding break-in tools (crowbars, pry bars, bolt cutters) +- Weapons visible (guns, knives, bats used aggressively) +- Forced entry in progress +- Physical aggression or violence +- Active property damage or theft in progress + +### Assessment Guidance +Evaluate in this order: + +1. **If person is verified/known** → Level 0 regardless of time or activity +2. **If person is unidentified:** + - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1 + - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing → Level 1 + - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) → Level 0 +3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1) + +The mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is.""", + title="Activity context prompt", + description="Custom prompt describing what is and is not suspicious activity to provide context for GenAI summaries.", + ) + + +class ReviewConfig(FrigateBaseModel): + alerts: AlertsConfig = Field( + default_factory=AlertsConfig, + title="Alerts config", + description="Settings for which tracked objects generate alerts and how alerts are retained.", + ) + detections: DetectionsConfig = Field( + default_factory=DetectionsConfig, + title="Detections config", + description="Settings for which tracked objects generate detections (non-alert) and how detections are retained.", + ) + genai: GenAIReviewConfig = Field( + default_factory=GenAIReviewConfig, + title="GenAI config", + description="Controls use of generative AI for producing descriptions and summaries of review items.", + ) diff --git a/frigate/config/camera/snapshots.py b/frigate/config/camera/snapshots.py new file mode 100644 index 0000000..7f5590a --- /dev/null +++ b/frigate/config/camera/snapshots.py @@ -0,0 +1,63 @@ +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = ["SnapshotsConfig", "RetainConfig"] + + +class RetainConfig(FrigateBaseModel): + default: float = Field( + default=10, + title="Default retention", + description="Default number of days to retain snapshots.", + ) + objects: dict[str, float] = Field( + default_factory=dict, + title="Object retention", + description="Per-object overrides for snapshot retention days.", + ) + + +class SnapshotsConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable snapshots", + description="Enable or disable saving snapshots for all cameras; can be overridden per-camera.", + ) + timestamp: bool = Field( + default=False, + title="Timestamp overlay", + description="Overlay a timestamp on snapshots from API.", + ) + bounding_box: bool = Field( + default=True, + title="Bounding box overlay", + description="Draw bounding boxes for tracked objects on snapshots from API.", + ) + crop: bool = Field( + default=False, + title="Crop snapshot", + description="Crop snapshots from API to the detected object's bounding box.", + ) + required_zones: list[str] = Field( + default_factory=list, + title="Required zones", + description="Zones an object must enter for a snapshot to be saved.", + ) + height: int | None = Field( + default=None, + title="Snapshot height", + description="Height (pixels) to resize snapshots from API to; leave empty to preserve original size.", + ) + retain: RetainConfig = Field( + default_factory=RetainConfig, + title="Snapshot retention", + description="Retention settings for snapshots including default days and per-object overrides.", + ) + quality: int = Field( + default=60, + title="Snapshot quality", + description="Encode quality for saved snapshots (0-100).", + ge=0, + le=100, + ) diff --git a/frigate/config/camera/timestamp.py b/frigate/config/camera/timestamp.py new file mode 100644 index 0000000..1245c31 --- /dev/null +++ b/frigate/config/camera/timestamp.py @@ -0,0 +1,82 @@ +from enum import Enum + +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = [ + "TimestampStyleConfig", + "TimestampEffectEnum", + "ColorConfig", + "TimestampPositionEnum", +] + + +# TODO: Identify what the default format to display timestamps is +DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S" +# German Style: +# DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S" + + +class TimestampPositionEnum(str, Enum): + tl = "tl" + tr = "tr" + bl = "bl" + br = "br" + + +class ColorConfig(FrigateBaseModel): + red: int = Field( + default=255, + ge=0, + le=255, + title="Red", + description="Red component (0-255) for timestamp color.", + ) + green: int = Field( + default=255, + ge=0, + le=255, + title="Green", + description="Green component (0-255) for timestamp color.", + ) + blue: int = Field( + default=255, + ge=0, + le=255, + title="Blue", + description="Blue component (0-255) for timestamp color.", + ) + + +class TimestampEffectEnum(str, Enum): + solid = "solid" + shadow = "shadow" + + +class TimestampStyleConfig(FrigateBaseModel): + position: TimestampPositionEnum = Field( + default=TimestampPositionEnum.tl, + title="Timestamp position", + description="Position of the timestamp on the image (tl/tr/bl/br).", + ) + format: str = Field( + default=DEFAULT_TIME_FORMAT, + title="Timestamp format", + description="Datetime format string used for timestamps (Python datetime format codes).", + ) + color: ColorConfig = Field( + default_factory=ColorConfig, + title="Timestamp color", + description="RGB color values for the timestamp text (all values 0-255).", + ) + thickness: int = Field( + default=2, + title="Timestamp thickness", + description="Line thickness of the timestamp text.", + ) + effect: TimestampEffectEnum | None = Field( + default=None, + title="Timestamp effect", + description="Visual effect for the timestamp text (none, solid, shadow).", + ) diff --git a/frigate/config/camera/ui.py b/frigate/config/camera/ui.py new file mode 100644 index 0000000..7db1c53 --- /dev/null +++ b/frigate/config/camera/ui.py @@ -0,0 +1,23 @@ +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = ["CameraUiConfig"] + + +class CameraUiConfig(FrigateBaseModel): + order: int = Field( + default=0, + title="UI order", + description="Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later.", + ) + dashboard: bool = Field( + default=True, + title="Show in UI", + description="Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again.", + ) + review: bool = Field( + default=True, + title="Show in review", + description="Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view).", + ) diff --git a/frigate/config/camera/updater.py b/frigate/config/camera/updater.py new file mode 100644 index 0000000..afa5019 --- /dev/null +++ b/frigate/config/camera/updater.py @@ -0,0 +1,177 @@ +"""Convenience classes for updating configurations dynamically.""" + +from dataclasses import dataclass +from enum import Enum +from typing import Any + +from frigate.comms.config_updater import ConfigPublisher, ConfigSubscriber +from frigate.config import CameraConfig, FrigateConfig + + +class CameraConfigUpdateEnum(str, Enum): + """Supported camera config update types.""" + + add = "add" # for adding a camera + audio = "audio" + audio_transcription = "audio_transcription" + birdseye = "birdseye" + detect = "detect" + enabled = "enabled" + ffmpeg = "ffmpeg" + live = "live" + motion = "motion" # includes motion and motion masks + mqtt = "mqtt" + notifications = "notifications" + objects = "objects" + object_genai = "object_genai" + onvif = "onvif" + record = "record" + refresh = "refresh" # signals the camera maintainer to recycle the camera process + remove = "remove" # for removing a camera + review = "review" + review_genai = "review_genai" + semantic_search = "semantic_search" # for semantic search triggers + face_recognition = "face_recognition" + lpr = "lpr" + snapshots = "snapshots" + timestamp_style = "timestamp_style" + ui = "ui" + zones = "zones" + + +@dataclass +class CameraConfigUpdateTopic: + update_type: CameraConfigUpdateEnum + camera: str + + @property + def topic(self) -> str: + return f"config/cameras/{self.camera}/{self.update_type.name}" + + +class CameraConfigUpdatePublisher: + def __init__(self): + self.publisher = ConfigPublisher() + + def publish_update(self, topic: CameraConfigUpdateTopic, config: Any) -> None: + self.publisher.publish(topic.topic, config) + + def stop(self) -> None: + self.publisher.stop() + + +class CameraConfigUpdateSubscriber: + def __init__( + self, + config: FrigateConfig | None, + camera_configs: dict[str, CameraConfig], + topics: list[CameraConfigUpdateEnum], + ): + self.config = config + self.camera_configs = camera_configs + self.topics = topics + + base_topic = "config/cameras" + + # global subscribers must hear every camera; only narrow per-camera workers + is_global_subscriber = ( + CameraConfigUpdateEnum.add in self.topics + or CameraConfigUpdateEnum.remove in self.topics + ) + if not is_global_subscriber and len(self.camera_configs) == 1: + base_topic += f"/{list(self.camera_configs.keys())[0]}" + + self.subscriber = ConfigSubscriber( + base_topic, + exact=False, + ) + + def __update_config( + self, camera: str, update_type: CameraConfigUpdateEnum, updated_config: Any + ) -> None: + if update_type == CameraConfigUpdateEnum.add: + shared = self.config.cameras.setdefault(camera, updated_config) + self.camera_configs[camera] = shared + return + elif update_type == CameraConfigUpdateEnum.remove: + self.config.cameras.pop(camera, None) + self.camera_configs.pop(camera, None) + return + + config = self.camera_configs.get(camera) + + if not config: + return + + if update_type == CameraConfigUpdateEnum.audio: + config.audio = updated_config + elif update_type == CameraConfigUpdateEnum.ffmpeg: + config.ffmpeg = updated_config + config.recreate_ffmpeg_cmds() + elif update_type == CameraConfigUpdateEnum.audio_transcription: + config.audio_transcription = updated_config + elif update_type == CameraConfigUpdateEnum.birdseye: + config.birdseye = updated_config + elif update_type == CameraConfigUpdateEnum.detect: + config.detect = updated_config + elif update_type == CameraConfigUpdateEnum.enabled: + config.enabled = updated_config + elif update_type == CameraConfigUpdateEnum.object_genai: + config.objects.genai = updated_config + elif update_type == CameraConfigUpdateEnum.live: + config.live = updated_config + elif update_type == CameraConfigUpdateEnum.motion: + config.motion = updated_config + elif update_type == CameraConfigUpdateEnum.notifications: + config.notifications = updated_config + elif update_type == CameraConfigUpdateEnum.objects: + config.objects = updated_config + elif update_type == CameraConfigUpdateEnum.record: + old_enabled_in_config = config.record.enabled_in_config + config.record = updated_config + if old_enabled_in_config != updated_config.enabled_in_config: + config.recreate_ffmpeg_cmds() + elif update_type == CameraConfigUpdateEnum.review: + config.review = updated_config + elif update_type == CameraConfigUpdateEnum.review_genai: + config.review.genai = updated_config + elif update_type == CameraConfigUpdateEnum.semantic_search: + config.semantic_search = updated_config + elif update_type == CameraConfigUpdateEnum.face_recognition: + config.face_recognition = updated_config + elif update_type == CameraConfigUpdateEnum.lpr: + config.lpr = updated_config + elif update_type == CameraConfigUpdateEnum.snapshots: + config.snapshots = updated_config + elif update_type == CameraConfigUpdateEnum.onvif: + config.onvif = updated_config + elif update_type == CameraConfigUpdateEnum.timestamp_style: + config.timestamp_style = updated_config + elif update_type == CameraConfigUpdateEnum.zones: + config.zones = updated_config + + def check_for_updates(self) -> dict[str, list[str]]: + updated_topics: dict[str, list[str]] = {} + + # get all updates available + while True: + update_topic, update_config = self.subscriber.check_for_update() + + if update_topic is None or update_config is None: + break + + _, _, camera, raw_type = update_topic.split("/") + update_type = CameraConfigUpdateEnum[raw_type] + + if update_type in self.topics: + if update_type.name in updated_topics: + updated_topics[update_type.name].append(camera) + else: + updated_topics[update_type.name] = [camera] + + self.__update_config(camera, update_type, update_config) + + return updated_topics + + def stop(self) -> None: + self.subscriber.stop() diff --git a/frigate/config/camera/zone.py b/frigate/config/camera/zone.py new file mode 100644 index 0000000..24c62e0 --- /dev/null +++ b/frigate/config/camera/zone.py @@ -0,0 +1,188 @@ +# this uses the base model because the color is an extra attribute +import logging + +import numpy as np +from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator + +from .objects import FilterConfig + +__all__ = ["ZoneConfig"] + +logger = logging.getLogger(__name__) + + +class ZoneConfig(BaseModel): + friendly_name: str | None = Field( + None, + title="Zone name", + description="A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used.", + ) + enabled: bool = Field( + default=True, + title="Enabled", + description="Enable or disable this zone. Disabled zones are ignored at runtime.", + ) + enabled_in_config: bool | None = Field( + default=None, title="Keep track of original state of zone." + ) + filters: dict[str, FilterConfig] = Field( + default_factory=dict, + title="Zone filters", + description="Filters to apply to objects within this zone. Used to reduce false positives or restrict which objects are considered present in the zone.", + ) + coordinates: str | list[str] = Field( + title="Coordinates", + description="Polygon coordinates that define the zone area. Can be a comma-separated string or a list of coordinate strings. Coordinates should be relative (0-1) or absolute (legacy).", + ) + distances: str | list[str] | None = Field( + default_factory=list, + title="Real-world distances", + description="Optional real-world distances for each side of the zone quadrilateral, used for speed or distance calculations. Must have exactly 4 values if set.", + ) + inertia: int = Field( + default=3, + title="Inertia frames", + gt=0, + description="Number of consecutive frames an object must be detected in the zone before it is considered present. Helps filter out transient detections.", + ) + loitering_time: int = Field( + default=0, + ge=0, + title="Loitering seconds", + description="Number of seconds an object must remain in the zone to be considered as loitering. Set to 0 to disable loitering detection.", + ) + speed_threshold: float | None = Field( + default=None, + ge=0.1, + title="Minimum speed", + description="Minimum speed (in real-world units if distances are set) required for an object to be considered present in the zone. Used for speed-based zone triggers.", + ) + objects: str | list[str] = Field( + default_factory=list, + title="Trigger objects", + description="List of object types (from labelmap) that can trigger this zone. Can be a string or a list of strings. If empty, all objects are considered.", + ) + _color: tuple[int, int, int] | None = PrivateAttr() + _contour: np.ndarray = PrivateAttr() + + @property + def color(self) -> tuple[int, int, int]: + return self._color + + @property + def contour(self) -> np.ndarray: + return self._contour + + def get_formatted_name(self, zone_name: str) -> str: + """Return the friendly name if set, otherwise return a formatted version of the zone name.""" + if self.friendly_name: + return self.friendly_name + return zone_name.replace("_", " ").title() + + @field_validator("objects", mode="before") + @classmethod + def validate_objects(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v + + @field_validator("distances", mode="before") + @classmethod + def validate_distances(cls, v): + if v is None: + return None + + if isinstance(v, str): + distances = list(map(str, map(float, v.split(",")))) + elif isinstance(v, list): + distances = [str(float(val)) for val in v] + else: + raise ValueError("Invalid type for distances") + + if len(distances) != 4: + raise ValueError("distances must have exactly 4 values") + + return distances + + @model_validator(mode="after") + def check_loitering_time_constraints(self): + if self.loitering_time > 0 and ( + self.speed_threshold is not None or len(self.distances) > 0 + ): + logger.warning( + "loitering_time should not be set on a zone if speed_threshold or distances is set." + ) + return self + + def __init__(self, **config): + super().__init__(**config) + + self._color = config.get("color", (0, 0, 0)) + self._contour = config.get("contour", np.array([])) + + def generate_contour(self, frame_shape: tuple[int, int]): + coordinates = self.coordinates + + # masks and zones are saved as relative coordinates + # we know if any points are > 1 then it is using the + # old native resolution coordinates + if isinstance(coordinates, list): + explicit = any(p.split(",")[0] > "1.0" for p in coordinates) + try: + self._contour = np.array( + [ + ( + [int(p.split(",")[0]), int(p.split(",")[1])] + if explicit + else [ + int(float(p.split(",")[0]) * frame_shape[1]), + int(float(p.split(",")[1]) * frame_shape[0]), + ] + ) + for p in coordinates + ] + ) + except ValueError: + raise ValueError( + f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}" + ) from None + + if explicit: + self.coordinates = ",".join( + [ + f"{round(int(p.split(',')[0]) / frame_shape[1], 3)},{round(int(p.split(',')[1]) / frame_shape[0], 3)}" + for p in coordinates + ] + ) + elif isinstance(coordinates, str): + points = coordinates.split(",") + explicit = any(p > "1.0" for p in points) + try: + self._contour = np.array( + [ + ( + [int(points[i]), int(points[i + 1])] + if explicit + else [ + int(float(points[i]) * frame_shape[1]), + int(float(points[i + 1]) * frame_shape[0]), + ] + ) + for i in range(0, len(points), 2) + ] + ) + except ValueError: + raise ValueError( + f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}" + ) from None + + if explicit: + self.coordinates = ",".join( + [ + f"{round(int(points[i]) / frame_shape[1], 3)},{round(int(points[i + 1]) / frame_shape[0], 3)}" + for i in range(0, len(points), 2) + ] + ) + else: + self._contour = np.array([]) diff --git a/frigate/config/camera_group.py b/frigate/config/camera_group.py new file mode 100644 index 0000000..8e08f56 --- /dev/null +++ b/frigate/config/camera_group.py @@ -0,0 +1,31 @@ +from pydantic import Field, field_validator + +from .base import FrigateBaseModel + +__all__ = ["CameraGroupConfig"] + + +class CameraGroupConfig(FrigateBaseModel): + cameras: str | list[str] = Field( + default_factory=list, + title="Camera list", + description="Array of camera names included in this group.", + ) + icon: str = Field( + default="generic", + title="Group icon", + description="Icon used to represent the camera group in the UI.", + ) + order: int = Field( + default=0, + title="Sort order", + description="Numeric order used to sort camera groups in the UI; larger numbers appear later.", + ) + + @field_validator("cameras", mode="before") + @classmethod + def validate_cameras(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v diff --git a/frigate/config/classification.py b/frigate/config/classification.py new file mode 100644 index 0000000..f4dc5f6 --- /dev/null +++ b/frigate/config/classification.py @@ -0,0 +1,454 @@ +from enum import Enum + +from pydantic import ConfigDict, Field, field_validator + +from .base import FrigateBaseModel + +__all__ = [ + "CameraFaceRecognitionConfig", + "CameraLicensePlateRecognitionConfig", + "CameraAudioTranscriptionConfig", + "FaceRecognitionConfig", + "SemanticSearchConfig", + "CameraSemanticSearchConfig", + "LicensePlateRecognitionConfig", +] + + +class SemanticSearchModelEnum(str, Enum): + jinav1 = "jinav1" + jinav2 = "jinav2" + + +class EnrichmentsDeviceEnum(str, Enum): + GPU = "GPU" + CPU = "CPU" + + +class ModelSizeEnum(str, Enum): + small = "small" + large = "large" + + +class TriggerType(str, Enum): + THUMBNAIL = "thumbnail" + DESCRIPTION = "description" + + +class TriggerAction(str, Enum): + NOTIFICATION = "notification" + SUB_LABEL = "sub_label" + ATTRIBUTE = "attribute" + + +class ObjectClassificationType(str, Enum): + sub_label = "sub_label" + attribute = "attribute" + + +class AudioTranscriptionConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable audio transcription", + description="Enable or disable automatic audio transcription for all cameras; can be overridden per-camera.", + ) + language: str = Field( + default="en", + title="Transcription language", + description="Language code used for transcription/translation (for example 'en' for English). See https://whisper-api.com/docs/languages/ for supported language codes.", + ) + device: EnrichmentsDeviceEnum = Field( + default=EnrichmentsDeviceEnum.CPU, + title="Transcription device", + description="Device key (CPU/GPU) to run the transcription model on. Only NVIDIA CUDA GPUs are currently supported for transcription.", + ) + model_size: ModelSizeEnum = Field( + default=ModelSizeEnum.small, + title="Model size", + description="Model size to use for offline audio event transcription.", + ) + live_enabled: bool | None = Field( + default=False, + title="Live transcription", + description="Enable streaming live transcription for audio as it is received.", + ) + + +class BirdClassificationConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Bird classification", + description="Enable or disable bird classification.", + ) + threshold: float = Field( + default=0.9, + title="Minimum score", + description="Minimum classification score required to accept a bird classification.", + gt=0.0, + le=1.0, + ) + + +class CustomClassificationStateCameraConfig(FrigateBaseModel): + crop: list[float, float, float, float] = Field( + title="Classification crop", + description="Crop coordinates to use for running classification on this camera.", + ) + + +class CustomClassificationStateConfig(FrigateBaseModel): + cameras: dict[str, CustomClassificationStateCameraConfig] = Field( + title="Classification cameras", + description="Per-camera crop and settings for running state classification.", + ) + motion: bool = Field( + default=False, + title="Run on motion", + description="If true, run classification when motion is detected within the specified crop.", + ) + interval: int | None = Field( + default=None, + title="Classification interval", + description="Interval (seconds) between periodic classification runs for state classification.", + gt=0, + ) + + +class CustomClassificationObjectConfig(FrigateBaseModel): + objects: list[str] = Field( + default_factory=list, + title="Classify objects", + description="List of object types to run object classification on.", + ) + classification_type: ObjectClassificationType = Field( + default=ObjectClassificationType.sub_label, + title="Classification type", + description="Classification type applied: 'sub_label' (adds sub_label) or other supported types.", + ) + + +class CustomClassificationConfig(FrigateBaseModel): + enabled: bool = Field( + default=True, + title="Enable model", + description="Enable or disable the custom classification model.", + ) + name: str | None = Field( + default=None, + title="Model name", + description="Identifier for the custom classification model to use.", + ) + threshold: float = Field( + default=0.8, + title="Score threshold", + description="Score threshold used to change the classification state.", + ) + save_attempts: int | None = Field( + default=None, + title="Save attempts", + description="How many classification attempts to save for recent classifications UI.", + ge=0, + ) + object_config: CustomClassificationObjectConfig | None = Field(default=None) + state_config: CustomClassificationStateConfig | None = Field(default=None) + + +class ClassificationConfig(FrigateBaseModel): + bird: BirdClassificationConfig = Field( + default_factory=BirdClassificationConfig, + title="Bird classification config", + description="Settings specific to bird classification models.", + ) + custom: dict[str, CustomClassificationConfig] = Field( + default={}, + title="Custom Classification Models", + description="Configuration for custom classification models used for objects or state detection.", + ) + + +class SemanticSearchConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable semantic search", + description="Enable or disable the semantic search feature.", + ) + reindex: bool | None = Field( + default=False, + title="Reindex on startup", + description="Trigger a full reindex of historical tracked objects into the embeddings database.", + ) + model: SemanticSearchModelEnum | str | None = Field( + default=SemanticSearchModelEnum.jinav1, + title="Semantic search model or GenAI provider name", + description="The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role.", + ) + + @field_validator("model", mode="before") + @classmethod + def coerce_model_enum(cls, v): + if isinstance(v, str): + try: + return SemanticSearchModelEnum(v) + except ValueError: + return v + return v + + model_size: ModelSizeEnum = Field( + default=ModelSizeEnum.small, + title="Model size", + description="Select model size; 'small' runs on CPU and 'large' typically requires GPU.", + ) + device: str | None = Field( + default=None, + title="Device", + description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", + ) + + +class TriggerConfig(FrigateBaseModel): + friendly_name: str | None = Field( + None, + title="Friendly name", + description="Optional friendly name displayed in the UI for this trigger.", + ) + enabled: bool = Field( + default=True, + title="Enable this trigger", + description="Enable or disable this semantic search trigger.", + ) + type: TriggerType = Field( + default=TriggerType.DESCRIPTION, + title="Trigger type", + description="Type of trigger: 'thumbnail' (match against image) or 'description' (match against text).", + ) + data: str = Field( + title="Trigger content", + description="Text phrase or thumbnail ID to match against tracked objects.", + ) + threshold: float = Field( + title="Trigger threshold", + description="Minimum similarity score (0-1) required to activate this trigger.", + default=0.8, + gt=0.0, + le=1.0, + ) + actions: list[TriggerAction] = Field( + default=[], + title="Trigger actions", + description="List of actions to execute when trigger matches (notification, sub_label, attribute).", + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class CameraSemanticSearchConfig(FrigateBaseModel): + triggers: dict[str, TriggerConfig] = Field( + default={}, + title="Triggers", + description="Actions and matching criteria for camera-specific semantic search triggers.", + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class FaceRecognitionConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable face recognition", + description="Enable or disable face recognition for all cameras; can be overridden per-camera.", + ) + model_size: ModelSizeEnum = Field( + default=ModelSizeEnum.small, + title="Model size", + description="Model size to use for face embeddings (small/large); larger may require GPU.", + ) + unknown_score: float = Field( + title="Unknown score threshold", + description="Distance threshold below which a face is considered a potential match (higher = stricter).", + default=0.8, + gt=0.0, + le=1.0, + ) + detection_threshold: float = Field( + default=0.7, + title="Detection threshold", + description="Minimum detection confidence required to consider a face detection valid.", + gt=0.0, + le=1.0, + ) + recognition_threshold: float = Field( + default=0.9, + title="Recognition threshold", + description="Face embedding distance threshold to consider two faces a match.", + gt=0.0, + le=1.0, + ) + min_area: int = Field( + default=750, + title="Minimum face area", + description="Minimum area (pixels) of a detected face box required to attempt recognition.", + ) + min_faces: int = Field( + default=1, + gt=0, + le=6, + title="Minimum faces", + description="Minimum number of face recognitions required before applying a recognized sub-label to a person.", + ) + save_attempts: int = Field( + default=200, + ge=0, + title="Save attempts", + description="Number of face recognition attempts to retain for recent recognition UI.", + ) + blur_confidence_filter: bool = Field( + default=True, + title="Blur confidence filter", + description="Adjust confidence scores based on image blur to reduce false positives for poor quality faces.", + ) + device: str | None = Field( + default=None, + title="Device", + description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", + ) + + +class CameraFaceRecognitionConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable face recognition", + description="Enable or disable face recognition.", + ) + min_area: int = Field( + default=750, + title="Minimum face area", + description="Minimum area (pixels) of a detected face box required to attempt recognition.", + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class ReplaceRule(FrigateBaseModel): + pattern: str = Field(..., title="Regex pattern") + replacement: str = Field(..., title="Replacement string") + + +class LicensePlateRecognitionConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable LPR", + description="Enable or disable license plate recognition for all cameras; can be overridden per-camera.", + ) + model_size: ModelSizeEnum = Field( + default=ModelSizeEnum.small, + title="Model size", + description="Model size used for text detection/recognition. Most users should use 'small'.", + ) + detection_threshold: float = Field( + default=0.7, + title="Detection threshold", + description="Detection confidence threshold to begin running OCR on a suspected plate.", + gt=0.0, + le=1.0, + ) + min_area: int = Field( + default=1000, + title="Minimum plate area", + description="Minimum plate area (pixels) required to attempt recognition.", + ) + recognition_threshold: float = Field( + default=0.9, + title="Recognition threshold", + description="Confidence threshold required for recognized plate text to be attached as a sub-label.", + gt=0.0, + le=1.0, + ) + min_plate_length: int = Field( + default=4, + title="Min plate length", + description="Minimum number of characters a recognized plate must contain to be considered valid.", + ) + format: str | None = Field( + default=None, + title="Plate format regex", + description="Optional regex to validate recognized plate strings against an expected format.", + ) + match_distance: int = Field( + default=1, + title="Match distance", + description="Number of character mismatches allowed when comparing detected plates to known plates.", + ge=0, + ) + known_plates: dict[str, list[str]] | None = Field( + default={}, + title="Known plates", + description="List of plates or regexes to specially track or alert on.", + ) + enhancement: int = Field( + default=0, + title="Enhancement level", + description="Enhancement level (0-10) to apply to plate crops prior to OCR; higher values may not always improve results, levels above 5 may only work with night time plates and should be used with caution.", + ge=0, + le=10, + ) + debug_save_plates: bool = Field( + default=False, + title="Save debug plates", + description="Save plate crop images for debugging LPR performance.", + ) + device: str | None = Field( + default=None, + title="Device", + description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", + ) + replace_rules: list[ReplaceRule] = Field( + default_factory=list, + title="Replacement rules", + description="Regex replacement rules used to normalize detected plate strings before matching.", + ) + + +class CameraLicensePlateRecognitionConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable LPR", + description="Enable or disable LPR on this camera.", + ) + expire_time: int = Field( + default=3, + title="Expire seconds", + description="Time in seconds after which an unseen plate is expired from the tracker (for dedicated LPR cameras only).", + gt=0, + ) + min_area: int = Field( + default=1000, + title="Minimum plate area", + description="Minimum plate area (pixels) required to attempt recognition.", + ) + enhancement: int = Field( + default=0, + title="Enhancement level", + description="Enhancement level (0-10) to apply to plate crops prior to OCR; higher values may not always improve results, levels above 5 may only work with night time plates and should be used with caution.", + ge=0, + le=10, + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class CameraAudioTranscriptionConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable transcription", + description="Enable or disable manually triggered audio event transcription.", + ) + enabled_in_config: bool | None = Field( + default=None, title="Original transcription state" + ) + live_enabled: bool | None = Field( + default=False, + title="Live transcription", + description="Enable streaming live transcription for audio as it is received.", + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) diff --git a/frigate/config/config.py b/frigate/config/config.py new file mode 100644 index 0000000..8e4c7f2 --- /dev/null +++ b/frigate/config/config.py @@ -0,0 +1,1142 @@ +from __future__ import annotations + +import io +import json +import logging +import os +from typing import Any, Self + +import numpy as np +from pydantic import ( + BaseModel, + ConfigDict, + Field, + TypeAdapter, + ValidationInfo, + field_validator, + model_validator, +) +from ruamel.yaml import YAML + +from frigate.const import REGEX_JSON +from frigate.detectors import DetectorConfig, ModelConfig +from frigate.detectors.detector_config import BaseDetectorConfig +from frigate.plus import PlusApi +from frigate.util.builtin import ( + deep_merge, + get_ffmpeg_arg_list, +) +from frigate.util.config import ( + CURRENT_CONFIG_VERSION, + StreamInfoRetriever, + convert_area_to_pixels, + find_config_file, + get_relative_coordinates, + migrate_frigate_config, +) +from frigate.util.image import create_mask +from frigate.util.services import auto_detect_hwaccel + +from .auth import AuthConfig +from .base import FrigateBaseModel +from .camera import CameraConfig, CameraLiveConfig +from .camera.audio import AudioConfig, AudioFilterConfig +from .camera.birdseye import BirdseyeConfig +from .camera.detect import DetectConfig +from .camera.ffmpeg import FfmpegConfig +from .camera.genai import GenAIConfig, GenAIRoleEnum +from .camera.mask import ObjectMaskConfig +from .camera.motion import MotionConfig +from .camera.notification import NotificationConfig +from .camera.objects import FilterConfig, ObjectConfig +from .camera.record import RecordConfig +from .camera.review import ReviewConfig +from .camera.snapshots import SnapshotsConfig +from .camera.timestamp import TimestampStyleConfig +from .camera_group import CameraGroupConfig +from .classification import ( + AudioTranscriptionConfig, + ClassificationConfig, + FaceRecognitionConfig, + LicensePlateRecognitionConfig, + SemanticSearchConfig, + SemanticSearchModelEnum, +) +from .database import DatabaseConfig +from .env import EnvVars +from .logger import LoggerConfig +from .mqtt import MqttConfig +from .network import NetworkingConfig +from .profile import ProfileDefinitionConfig +from .proxy import ProxyConfig +from .telemetry import TelemetryConfig +from .tls import TlsConfig +from .ui import UIConfig + +__all__ = ["FrigateConfig"] + +logger = logging.getLogger(__name__) + +yaml = YAML() + +# Pydantic field default applied when an existing config omits `detectors:`. +# Kept as cpu tflite for backwards compatibility with 0.17 configs. +DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}} + +# Used by the openvino branch below and rendered into the new-config YAML +# template so first-time setups default to openvino on CPU. +DEFAULT_MODEL = { + "width": 300, + "height": 300, + "input_tensor": "nhwc", + "input_pixel_format": "bgr", + "path": "/openvino-model/ssdlite_mobilenet_v2.xml", + "labelmap_path": "/openvino-model/coco_91cl_bkgr.txt", +} +NEW_CONFIG_DETECTORS = {"ov": {"type": "openvino", "device": "CPU"}} +DEFAULT_DETECT_DIMENSIONS = {"width": 1280, "height": 720} + + +def _render_default_yaml(data: dict) -> str: + buf = io.StringIO() + _yaml_writer = YAML() + _yaml_writer.indent(mapping=2, sequence=4, offset=2) + _yaml_writer.dump(data, buf) + return buf.getvalue() + + +DEFAULT_CONFIG = f""" +mqtt: + enabled: False + +{_render_default_yaml({"detectors": NEW_CONFIG_DETECTORS, "model": DEFAULT_MODEL})} +cameras: {{}} # No cameras defined, UI wizard should be used +version: {CURRENT_CONFIG_VERSION} +""" + +# stream info handler +stream_info_retriever = StreamInfoRetriever() + + +class RuntimeMotionConfig(MotionConfig): + """Runtime version of MotionConfig with rasterized masks.""" + + rasterized_mask: np.ndarray = Field(default=None, exclude=True) + + def __init__(self, **config): + frame_shape = config.get("frame_shape", (1, 1)) + + # Store original mask dict for serialization + original_mask = config.get("mask", {}) + if isinstance(original_mask, dict): + # Process the new dict format - update raw_coordinates for each mask + processed_mask = {} + for mask_id, mask_config in original_mask.items(): + if isinstance(mask_config, dict): + coords = mask_config.get("coordinates", "") + relative_coords = get_relative_coordinates(coords, frame_shape) + mask_config_copy = mask_config.copy() + mask_config_copy["raw_coordinates"] = ( + relative_coords if relative_coords else coords + ) + mask_config_copy["coordinates"] = ( + relative_coords if relative_coords else coords + ) + processed_mask[mask_id] = mask_config_copy + else: + processed_mask[mask_id] = mask_config + config["mask"] = processed_mask + config["raw_mask"] = processed_mask + + super().__init__(**config) + + # Rasterize only enabled masks + enabled_coords = [] + for mask_config in self.mask.values(): + if mask_config.enabled and mask_config.coordinates: + coords = mask_config.coordinates + if isinstance(coords, list): + enabled_coords.extend(coords) + else: + enabled_coords.append(coords) + + if enabled_coords: + self.rasterized_mask = create_mask(frame_shape, enabled_coords) + else: + empty_mask = np.zeros(frame_shape, np.uint8) + empty_mask[:] = 255 + self.rasterized_mask = empty_mask + + model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore") + + +class RuntimeFilterConfig(FilterConfig): + """Runtime version of FilterConfig with rasterized masks.""" + + rasterized_mask: np.ndarray | None = Field(default=None, exclude=True) + + def __init__(self, **config): + frame_shape = config.get("frame_shape", (1, 1)) + + # Store original mask dict for serialization + original_mask = config.get("mask", {}) + if isinstance(original_mask, dict): + # Process the new dict format - update raw_coordinates for each mask + processed_mask = {} + for mask_id, mask_config in original_mask.items(): + # Handle both dict and ObjectMaskConfig formats + if hasattr(mask_config, "model_dump"): + # It's an ObjectMaskConfig object + mask_dict = mask_config.model_dump() + coords = mask_dict.get("coordinates", "") + relative_coords = get_relative_coordinates(coords, frame_shape) + mask_dict["raw_coordinates"] = ( + relative_coords if relative_coords else coords + ) + mask_dict["coordinates"] = ( + relative_coords if relative_coords else coords + ) + processed_mask[mask_id] = mask_dict + elif isinstance(mask_config, dict): + coords = mask_config.get("coordinates", "") + relative_coords = get_relative_coordinates(coords, frame_shape) + mask_config_copy = mask_config.copy() + mask_config_copy["raw_coordinates"] = ( + relative_coords if relative_coords else coords + ) + mask_config_copy["coordinates"] = ( + relative_coords if relative_coords else coords + ) + processed_mask[mask_id] = mask_config_copy + else: + processed_mask[mask_id] = mask_config + config["mask"] = processed_mask + config["raw_mask"] = processed_mask + + # Convert min_area and max_area to pixels if they're percentages + if "min_area" in config: + config["min_area"] = convert_area_to_pixels(config["min_area"], frame_shape) + + if "max_area" in config: + config["max_area"] = convert_area_to_pixels(config["max_area"], frame_shape) + + super().__init__(**config) + + # Rasterize only enabled masks + enabled_coords = [] + for mask_config in self.mask.values(): + if mask_config.enabled and mask_config.coordinates: + coords = mask_config.coordinates + if isinstance(coords, list): + enabled_coords.extend(coords) + else: + enabled_coords.append(coords) + + if enabled_coords: + self.rasterized_mask = create_mask(frame_shape, enabled_coords) + else: + self.rasterized_mask = None + + model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore") + + +class RestreamConfig(BaseModel): + model_config = ConfigDict(extra="allow") + + +def verify_config_roles(camera_config: CameraConfig) -> None: + """Verify that roles are setup in the config correctly.""" + assigned_roles = list( + set([r for i in camera_config.ffmpeg.inputs for r in i.roles]) + ) + + if camera_config.record.enabled and "record" not in assigned_roles: + raise ValueError( + f"Camera {camera_config.name} has record enabled, but record is not assigned to an input." + ) + + if camera_config.audio.enabled and "audio" not in assigned_roles: + raise ValueError( + f"Camera {camera_config.name} has audio events enabled, but audio is not assigned to an input." + ) + + +def verify_valid_live_stream_names( + frigate_config: FrigateConfig, camera_config: CameraConfig +) -> ValueError | None: + """Verify that a restream exists to use for live view.""" + for _, stream_name in camera_config.live.streams.items(): + if ( + stream_name + not in frigate_config.go2rtc.model_dump().get("streams", {}).keys() + ): + return ValueError( + f"No restream with name {stream_name} exists for camera {camera_config.name}." + ) + + +def verify_recording_segments_setup_with_reasonable_time( + camera_config: CameraConfig, +) -> None: + """Verify that recording segments are setup and segment time is not greater than 60.""" + record_args: list[str] = get_ffmpeg_arg_list( + camera_config.ffmpeg.output_args.record + ) + + if record_args[0].startswith("preset"): + return + + try: + seg_arg_index = record_args.index("-segment_time") + except ValueError: + raise ValueError( + f"Camera {camera_config.name} has no segment_time in \ + recording output args, segment args are required for record." + ) from None + + if int(record_args[seg_arg_index + 1]) > 60: + raise ValueError( + f"Camera {camera_config.name} has invalid segment_time output arg, \ + segment_time must be 60 or less." + ) + + +def verify_zone_objects_are_tracked(camera_config: CameraConfig) -> None: + """Verify that user has not entered zone objects that are not in the tracking config.""" + for zone_name, zone in camera_config.zones.items(): + for obj in zone.objects: + if obj not in camera_config.objects.track: + raise ValueError( + f"Zone {zone_name} is configured to track {obj} but that object type is not added to objects -> track." + ) + + +def verify_required_zones_exist(camera_config: CameraConfig) -> None: + for det_zone in camera_config.review.detections.required_zones: + if det_zone not in camera_config.zones.keys(): + raise ValueError( + f"Camera {camera_config.name} has a required zone for detections {det_zone} that is not defined." + ) + + for det_zone in camera_config.review.alerts.required_zones: + if det_zone not in camera_config.zones.keys(): + raise ValueError( + f"Camera {camera_config.name} has a required zone for alerts {det_zone} that is not defined." + ) + + +def verify_profile_overrides_match_base(camera_config: CameraConfig) -> None: + """Verify that profile zone and mask IDs reference entries defined on the base camera.""" + for profile_name, profile in camera_config.profiles.items(): + if profile.zones: + for zone_name in profile.zones: + if zone_name not in camera_config.zones: + raise ValueError( + f"Camera '{camera_config.name}' profile '{profile_name}' defines " + f"zone '{zone_name}' that does not exist on the base config" + ) + + if profile.motion and profile.motion.mask: + for mask_name in profile.motion.mask: + if mask_name not in camera_config.motion.mask: + raise ValueError( + f"Camera '{camera_config.name}' profile '{profile_name}' defines " + f"motion mask '{mask_name}' that does not exist on the base config" + ) + + if profile.objects: + for mask_name in profile.objects.mask or {}: + if mask_name not in (camera_config.objects.mask or {}): + raise ValueError( + f"Camera '{camera_config.name}' profile '{profile_name}' defines " + f"object mask '{mask_name}' that does not exist on the base config" + ) + for label, filter_config in (profile.objects.filters or {}).items(): + base_filter = (camera_config.objects.filters or {}).get(label) + profile_filter_masks = ( + filter_config.mask if filter_config else None + ) or {} + base_filter_masks = (base_filter.mask if base_filter else None) or {} + for mask_name in profile_filter_masks: + if mask_name not in base_filter_masks: + raise ValueError( + f"Camera '{camera_config.name}' profile '{profile_name}' defines " + f"object mask '{mask_name}' for '{label}' that does not exist " + f"on the base config" + ) + + +def verify_autotrack_zones(camera_config: CameraConfig) -> ValueError | None: + """Verify that required_zones are specified when autotracking is enabled.""" + if ( + camera_config.onvif.autotracking.enabled + and not camera_config.onvif.autotracking.required_zones + ): + raise ValueError( + f"Camera {camera_config.name} has autotracking enabled, required_zones must be set to at least one of the camera's zones." + ) + + +def verify_motion_and_detect(camera_config: CameraConfig) -> ValueError | None: + """Verify that motion detection is not disabled and object detection is enabled.""" + if camera_config.detect.enabled and not camera_config.motion.enabled: + raise ValueError( + f"Camera {camera_config.name} has motion detection disabled and object detection enabled but object detection requires motion detection." + ) + + +def verify_objects_track( + camera_config: CameraConfig, enabled_objects: list[str] +) -> None: + """Verify that a user has not specified an object to track that is not in the labelmap.""" + valid_objects = [ + obj for obj in camera_config.objects.track if obj in enabled_objects + ] + + if len(valid_objects) != len(camera_config.objects.track): + invalid_objects = set(camera_config.objects.track) - set(valid_objects) + logger.warning( + f"{camera_config.name} is configured to track {list(invalid_objects)} objects, which are not supported by the current model." + ) + camera_config.objects.track = valid_objects + + +def verify_lpr_and_face( + frigate_config: FrigateConfig, camera_config: CameraConfig +) -> ValueError | None: + """Verify that lpr and face are enabled at the global level if enabled at the camera level.""" + if camera_config.lpr.enabled and not frigate_config.lpr.enabled: + raise ValueError( + f"Camera {camera_config.name} has lpr enabled but lpr is disabled at the global level of the config. You must enable lpr at the global level." + ) + if ( + camera_config.face_recognition.enabled + and not frigate_config.face_recognition.enabled + ): + raise ValueError( + f"Camera {camera_config.name} has face_recognition enabled but face_recognition is disabled at the global level of the config. You must enable face_recognition at the global level." + ) + + +class FrigateConfig(FrigateBaseModel): + version: str | None = Field( + default=None, + title="Current config version", + description="Numeric or string version of the active configuration to help detect migrations or format changes.", + ) + safe_mode: bool = Field( + default=False, + title="Safe mode", + description="When enabled, start Frigate in safe mode with reduced features for troubleshooting.", + ) + + # Fields that install global state should be defined first, so that their validators run first. + environment_vars: EnvVars = Field( + default_factory=dict, + title="Environment variables", + description="Key/value pairs of environment variables to set for the Frigate process in Home Assistant OS. Non-HAOS users must use Docker environment variable configuration instead.", + ) + logger: LoggerConfig = Field( + default_factory=LoggerConfig, + title="Logging", + description="Controls default log verbosity and per-component log level overrides.", + validate_default=True, + ) + + # Global config + auth: AuthConfig = Field( + default_factory=AuthConfig, + title="Authentication", + description="Authentication and session-related settings including cookie and rate limit options.", + ) + database: DatabaseConfig = Field( + default_factory=DatabaseConfig, + title="Database", + description="Settings for the SQLite database used by Frigate to store tracked object and recording metadata.", + ) + go2rtc: RestreamConfig = Field( + default_factory=RestreamConfig, + title="go2rtc", + description="Settings for the integrated go2rtc restreaming service used for live stream relaying and translation.", + ) + mqtt: MqttConfig = Field( + title="MQTT", + description="Settings for connecting and publishing telemetry, snapshots, and event details to an MQTT broker.", + ) + notifications: NotificationConfig = Field( + default_factory=NotificationConfig, + title="Notifications", + description="Settings to enable and control notifications for all cameras; can be overridden per-camera.", + ) + networking: NetworkingConfig = Field( + default_factory=NetworkingConfig, + title="Networking", + description="Network-related settings such as IPv6 enablement for Frigate endpoints.", + ) + proxy: ProxyConfig = Field( + default_factory=ProxyConfig, + title="Proxy", + description="Settings for integrating Frigate behind a reverse proxy that passes authenticated user headers.", + ) + telemetry: TelemetryConfig = Field( + default_factory=TelemetryConfig, + title="Telemetry", + description="System telemetry and stats options including GPU and network bandwidth monitoring.", + ) + tls: TlsConfig = Field( + default_factory=TlsConfig, + title="TLS", + description="TLS settings for Frigate's web endpoints (port 8971).", + ) + ui: UIConfig = Field( + default_factory=UIConfig, + title="UI", + description="User interface preferences such as timezone, time/date formatting, and units.", + ) + + # Detector config + detectors: dict[str, BaseDetectorConfig] = Field( + default=DEFAULT_DETECTORS, + title="Detector hardware", + description="Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.", + ) + model: ModelConfig = Field( + default_factory=ModelConfig, + title="Detection model", + description="Settings to configure a custom object detection model and its input shape.", + ) + + # GenAI config (named provider configs: name -> GenAIConfig) + genai: dict[str, GenAIConfig] = Field( + default_factory=dict, + title="Generative AI configuration", + description="Settings for integrated generative AI providers used to generate object descriptions and review summaries.", + ) + + # Camera config + cameras: dict[str, CameraConfig] = Field(title="Cameras", description="Cameras") + audio: AudioConfig = Field( + default_factory=AudioConfig, + title="Audio detection", + description="Settings for audio-based event detection for all cameras; can be overridden per-camera.", + ) + birdseye: BirdseyeConfig = Field( + default_factory=BirdseyeConfig, + title="Birdseye", + description="Settings for the Birdseye composite view that composes multiple camera feeds into a single layout.", + ) + detect: DetectConfig = Field( + default_factory=DetectConfig, + title="Object Detection", + description="Settings for the detection/detect role used to run object detection and initialize trackers.", + ) + ffmpeg: FfmpegConfig = Field( + default_factory=FfmpegConfig, + title="FFmpeg", + description="FFmpeg settings including binary path, args, hwaccel options, and per-role output args.", + ) + live: CameraLiveConfig = Field( + default_factory=CameraLiveConfig, + title="Live playback", + description="Settings to control the jsmpeg live stream resolution and quality. This does not affect restreamed cameras that use go2rtc for live view.", + ) + motion: MotionConfig | None = Field( + default=None, + title="Motion detection", + description="Default motion detection settings applied to cameras unless overridden per-camera.", + ) + objects: ObjectConfig = Field( + default_factory=ObjectConfig, + title="Objects", + description="Object tracking defaults including which labels to track and per-object filters.", + ) + record: RecordConfig = Field( + default_factory=RecordConfig, + title="Recording", + description="Recording and retention settings applied to cameras unless overridden per-camera.", + ) + review: ReviewConfig = Field( + default_factory=ReviewConfig, + title="Review", + description="Settings that control alerts, detections, and GenAI review summaries used by the UI and storage.", + ) + snapshots: SnapshotsConfig = Field( + default_factory=SnapshotsConfig, + title="Snapshots", + description="Settings for API-generated snapshots of tracked objects for all cameras; can be overridden per-camera.", + ) + timestamp_style: TimestampStyleConfig = Field( + default_factory=TimestampStyleConfig, + title="Timestamp style", + description="Styling options for in-feed timestamps applied to debug view and snapshots.", + ) + + # Classification Config + audio_transcription: AudioTranscriptionConfig = Field( + default_factory=AudioTranscriptionConfig, + title="Audio transcription", + description="Settings for live and speech audio transcription used for events and live captions.", + ) + classification: ClassificationConfig = Field( + default_factory=ClassificationConfig, + title="Object classification", + description="Settings for classification models used to refine object labels or state classification.", + ) + semantic_search: SemanticSearchConfig = Field( + default_factory=SemanticSearchConfig, + title="Semantic Search", + description="Settings for Semantic Search which builds and queries object embeddings to find similar items.", + ) + face_recognition: FaceRecognitionConfig = Field( + default_factory=FaceRecognitionConfig, + title="Face recognition", + description="Settings for face detection and recognition for all cameras; can be overridden per-camera.", + ) + lpr: LicensePlateRecognitionConfig = Field( + default_factory=LicensePlateRecognitionConfig, + title="License Plate Recognition", + description="License plate recognition settings including detection thresholds, formatting, and known plates.", + ) + + camera_groups: dict[str, CameraGroupConfig] = Field( + default_factory=dict, + title="Camera groups", + description="Configuration for named camera groups used to organize cameras in the UI.", + ) + + profiles: dict[str, ProfileDefinitionConfig] = Field( + default_factory=dict, + title="Profiles", + description="Named profile definitions with friendly names. Camera profiles must reference names defined here.", + ) + + active_profile: str | None = Field( + default=None, + title="Active profile", + description="Currently active profile name. Runtime-only, not persisted in YAML.", + exclude=True, + ) + + _plus_api: PlusApi + + @property + def plus_api(self) -> PlusApi: + return self._plus_api + + @model_validator(mode="after") + def post_validation(self, info: ValidationInfo) -> Self: + # Load plus api from context, if possible. + self._plus_api = None + if isinstance(info.context, dict): + self._plus_api = info.context.get("plus_api") + + # Ensure self._plus_api is set, if no explicit value is provided. + if self._plus_api is None: + self._plus_api = PlusApi() + + # set notifications state + self.notifications.enabled_in_config = self.notifications.enabled + + # validate genai: each role (tools, vision, embeddings) at most once + role_to_name: dict[GenAIRoleEnum, str] = {} + for name, genai_cfg in self.genai.items(): + for role in genai_cfg.roles: + if role in role_to_name: + raise ValueError( + f"GenAI role '{role.value}' is assigned to both " + f"'{role_to_name[role]}' and '{name}'; each role must have " + "exactly one provider." + ) + 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 + for attribute in self.model.all_attributes: + existing = self.objects.filters.get(attribute) + if existing is None: + self.objects.filters[attribute] = FilterConfig(min_score=0.7) + elif "min_score" not in existing.model_fields_set: + existing.min_score = 0.7 + + # auto detect hwaccel args + if self.ffmpeg.hwaccel_args == "auto": + self.ffmpeg.hwaccel_args = auto_detect_hwaccel() + + # Resolve global export hwaccel_args so it matches the per-camera + # resolution below. Without this, every camera reads as overriding + # record.export.hwaccel_args because the global stays "auto" while + # the camera value gets resolved to the actual args list. + if self.record.export.hwaccel_args == "auto": + self.record.export.hwaccel_args = self.ffmpeg.hwaccel_args + + # Populate global audio filters from listen. Existing user-defined + # entries for labels not in listen are preserved but unused at runtime. + if self.audio.filters is None: + self.audio.filters = {} + + for key in sorted(set(self.audio.listen) - self.audio.filters.keys()): + self.audio.filters[key] = AudioFilterConfig() + + self.audio.filters = dict(sorted(self.audio.filters.items())) + + # Global config to propagate down to camera level + global_config = self.model_dump( + include={ + "audio": ..., + "audio_transcription": ..., + "birdseye": ..., + "face_recognition": ..., + "lpr": ..., + "record": ..., + "snapshots": ..., + "live": ..., + "objects": ..., + "review": ..., + "motion": ..., + "notifications": ..., + "detect": ..., + "ffmpeg": ..., + "timestamp_style": ..., + }, + exclude_unset=True, + ) + + for key, detector in self.detectors.items(): + adapter = TypeAdapter(DetectorConfig) + model_dict = ( + detector + if isinstance(detector, dict) + else detector.model_dump(warnings="none") + ) + detector_config: BaseDetectorConfig = adapter.validate_python(model_dict) + + # users should not set model themselves + if detector_config.model: + logger.warning( + "The model key should be specified at the root level of the config, not under detectors. The nested model key will be ignored." + ) + detector_config.model = None + + model_config = self.model.model_dump(exclude_unset=True, warnings="none") + + if detector_config.model_path: + model_config["path"] = detector_config.model_path + + if "path" not in model_config: + if detector_config.type == "cpu" or detector_config.type.endswith( + "_tfl" + ): + model_config["path"] = "/cpu_model.tflite" + elif detector_config.type == "edgetpu": + model_config["path"] = "/edgetpu_model.tflite" + elif detector_config.type == "openvino": + for default_key, default_value in DEFAULT_MODEL.items(): + model_config.setdefault(default_key, default_value) + + model = ModelConfig.model_validate(model_config) + model.check_and_load_plus_model(self.plus_api, detector_config.type) + model.compute_model_hash() + labelmap_objects = model.merged_labelmap.values() + detector_config.model = model + self.detectors[key] = detector_config + + for name, camera in self.cameras.items(): + modified_global_config = global_config.copy() + + # only populate some fields down to the camera level for specific keys + allowed_fields_map = { + "face_recognition": ["enabled", "min_area"], + "lpr": ["enabled", "expire_time", "min_area", "enhancement"], + "audio_transcription": ["enabled", "live_enabled"], + } + + for section in allowed_fields_map: + if section in modified_global_config: + modified_global_config[section] = { + k: v + for k, v in modified_global_config[section].items() + if k in allowed_fields_map[section] + } + + merged_config = deep_merge( + camera.model_dump(exclude_unset=True), modified_global_config + ) + camera_config: CameraConfig = CameraConfig.model_validate( + {"name": name, **merged_config} + ) + + if camera_config.ffmpeg.hwaccel_args == "auto": + camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args + + # Resolve export hwaccel_args: camera export -> camera ffmpeg -> global ffmpeg + # This allows per-camera override for exports (e.g., when camera resolution + # exceeds hardware encoder limits) + if camera_config.record.export.hwaccel_args == "auto": + camera_config.record.export.hwaccel_args = ( + camera_config.ffmpeg.hwaccel_args + ) + + for input in camera_config.ffmpeg.inputs: + need_detect_dimensions = "detect" in input.roles and ( + camera_config.detect.height is None + or camera_config.detect.width is None + ) + + if need_detect_dimensions: + logger.info( + f"detect.width and detect.height not set for {camera_config.name}, probing detect stream to determine resolution." + ) + stream_info = {"width": 0, "height": 0, "fourcc": None} + try: + stream_info = stream_info_retriever.get_stream_info( + self.ffmpeg, input.path + ) + except Exception: + logger.warning( + f"Error detecting stream parameters automatically for {input.path} Applying default values." + ) + stream_info = {"width": 0, "height": 0, "fourcc": None} + + if need_detect_dimensions: + camera_config.detect.width = ( + stream_info["width"] + if stream_info.get("width") + else DEFAULT_DETECT_DIMENSIONS["width"] + ) + camera_config.detect.height = ( + stream_info["height"] + if stream_info.get("height") + else DEFAULT_DETECT_DIMENSIONS["height"] + ) + + # Warn if detect fps > 10 + if camera_config.detect.fps > 10 and camera_config.type != "lpr": + logger.warning( + f"{camera_config.name} detect fps is set to {camera_config.detect.fps}. This does NOT need to match your camera's frame rate. High values could lead to reduced performance. Recommended value is 5." + ) + if camera_config.detect.fps > 15 and camera_config.type == "lpr": + logger.warning( + f"{camera_config.name} detect fps is set to {camera_config.detect.fps}. This does NOT need to match your camera's frame rate. High values could lead to reduced performance. Recommended value for LPR cameras are between 5-15." + ) + + # Default min_initialized configuration + min_initialized = max(int(camera_config.detect.fps / 2), 2) + if camera_config.detect.min_initialized is None: + camera_config.detect.min_initialized = min_initialized + + # Default max_disappeared configuration + max_disappeared = camera_config.detect.fps * 5 + if camera_config.detect.max_disappeared is None: + camera_config.detect.max_disappeared = max_disappeared + + # Default stationary_threshold configuration + stationary_threshold = camera_config.detect.fps * 10 + if camera_config.detect.stationary.threshold is None: + camera_config.detect.stationary.threshold = stationary_threshold + # default to the stationary_threshold if not defined + if camera_config.detect.stationary.interval is None: + camera_config.detect.stationary.interval = stationary_threshold + + # set config pre-value + camera_config.enabled_in_config = camera_config.enabled + camera_config.audio.enabled_in_config = camera_config.audio.enabled + camera_config.audio_transcription.enabled_in_config = ( + camera_config.audio_transcription.enabled + ) + camera_config.record.enabled_in_config = camera_config.record.enabled + camera_config.notifications.enabled_in_config = ( + camera_config.notifications.enabled + ) + camera_config.onvif.autotracking.enabled_in_config = ( + camera_config.onvif.autotracking.enabled + ) + camera_config.review.alerts.enabled_in_config = ( + camera_config.review.alerts.enabled + ) + camera_config.review.detections.enabled_in_config = ( + camera_config.review.detections.enabled + ) + camera_config.objects.genai.enabled_in_config = ( + camera_config.objects.genai.enabled + ) + camera_config.review.genai.enabled_in_config = ( + camera_config.review.genai.enabled + ) + + if camera_config.audio.filters is None: + camera_config.audio.filters = {} + + for key in sorted( + set(camera_config.audio.listen) - camera_config.audio.filters.keys() + ): + camera_config.audio.filters[key] = AudioFilterConfig() + + camera_config.audio.filters = dict( + sorted(camera_config.audio.filters.items()) + ) + + # Add default filters + object_keys = camera_config.objects.track + if camera_config.objects.filters is None: + camera_config.objects.filters = {} + object_keys = object_keys - camera_config.objects.filters.keys() + for key in object_keys: + camera_config.objects.filters[key] = FilterConfig() + + # Process global object masks to set raw_coordinates + if camera_config.objects.mask: + processed_global_masks = {} + for mask_id, mask_config in camera_config.objects.mask.items(): + if mask_config: + coords = mask_config.coordinates + relative_coords = get_relative_coordinates( + coords, + camera_config.frame_shape, + camera_name=camera_config.name, + ) + # Create a new ObjectMaskConfig with raw_coordinates set + processed_global_masks[mask_id] = ObjectMaskConfig( + friendly_name=mask_config.friendly_name, + enabled=mask_config.enabled, + coordinates=relative_coords if relative_coords else coords, + raw_coordinates=relative_coords + if relative_coords + else coords, + enabled_in_config=mask_config.enabled, + ) + else: + processed_global_masks[mask_id] = mask_config + camera_config.objects.mask = processed_global_masks + camera_config.objects.raw_mask = processed_global_masks + + # Apply global object masks and convert masks to numpy array + for object, filter in camera_config.objects.filters.items(): + # Set enabled_in_config for per-object masks before processing + for mask_config in filter.mask.values(): + if mask_config: + mask_config.enabled_in_config = mask_config.enabled + + # Merge global object masks with per-object filter masks + merged_mask = dict(filter.mask) # Copy filter-specific masks + + # Add global object masks if they exist + if camera_config.objects.mask: + for mask_id, mask_config in camera_config.objects.mask.items(): + # Use a global prefix to avoid key collisions + global_mask_id = f"global_{mask_id}" + merged_mask[global_mask_id] = mask_config + + # Set runtime filter to create masks + camera_config.objects.filters[object] = RuntimeFilterConfig( + frame_shape=camera_config.frame_shape, + mask=merged_mask, + **filter.model_dump( + exclude_unset=True, exclude={"mask", "raw_mask"} + ), + ) + + # Set enabled_in_config for motion masks to match config file state BEFORE creating RuntimeMotionConfig + if camera_config.motion: + camera_config.motion.enabled_in_config = camera_config.motion.enabled + for mask_config in camera_config.motion.mask.values(): + if mask_config: + mask_config.enabled_in_config = mask_config.enabled + + # Convert motion configuration + if camera_config.motion is None: + camera_config.motion = RuntimeMotionConfig( + frame_shape=camera_config.frame_shape + ) + else: + camera_config.motion = RuntimeMotionConfig( + frame_shape=camera_config.frame_shape, + **camera_config.motion.model_dump(exclude_unset=True), + ) + + # generate zone contours + if len(camera_config.zones) > 0: + for zone in camera_config.zones.values(): + if zone.filters: + for object_name, filter_config in zone.filters.items(): + zone.filters[object_name] = RuntimeFilterConfig( + frame_shape=camera_config.frame_shape, + **filter_config.model_dump(exclude_unset=True), + ) + + zone.generate_contour(camera_config.frame_shape) + + # Set enabled_in_config for zones to match config file state + for zone in camera_config.zones.values(): + zone.enabled_in_config = zone.enabled + + # Set live view stream if none is set + if not camera_config.live.streams: + camera_config.live.streams = {name: name} + + # generate the ffmpeg commands + camera_config.create_ffmpeg_cmds() + self.cameras[name] = camera_config + + verify_config_roles(camera_config) + verify_valid_live_stream_names(self, camera_config) + verify_recording_segments_setup_with_reasonable_time(camera_config) + verify_zone_objects_are_tracked(camera_config) + verify_required_zones_exist(camera_config) + verify_profile_overrides_match_base(camera_config) + verify_autotrack_zones(camera_config) + verify_motion_and_detect(camera_config) + verify_objects_track(camera_config, labelmap_objects) + verify_lpr_and_face(self, camera_config) + + # Validate camera profiles reference top-level profile definitions + for cam_name, cam_config in self.cameras.items(): + for profile_name in cam_config.profiles: + if profile_name not in self.profiles: + raise ValueError( + f"Camera '{cam_name}' references profile '{profile_name}' " + f"which is not defined in the top-level 'profiles' section" + ) + + # set names on classification configs + for name, config in self.classification.custom.items(): + config.name = name + + self.objects.parse_all_objects(self.cameras) + self.model.create_colormap(sorted(self.objects.all_objects)) + self.model.check_and_load_plus_model(self.plus_api) + + # Check audio transcription and audio detection requirements + if self.audio_transcription.enabled: + # If audio transcription is enabled globally, at least one camera must have audio detection enabled + if not any(camera.audio.enabled for camera in self.cameras.values()): + raise ValueError( + "Audio transcription is enabled globally, but no cameras have audio detection enabled. At least one camera must have audio detection enabled." + ) + else: + # If audio transcription is disabled globally, check each camera with audio_transcription enabled + for camera in self.cameras.values(): + if camera.audio_transcription.enabled and not camera.audio.enabled: + raise ValueError( + f"Camera {camera.name} has audio transcription enabled, but audio detection is not enabled for this camera. Audio detection must be enabled for cameras with audio transcription when it is disabled globally." + ) + + # Validate auth roles against cameras + camera_names = set(self.cameras.keys()) + + for role, allowed_cameras in self.auth.roles.items(): + invalid_cameras = [ + cam for cam in allowed_cameras if cam not in camera_names + ] + if invalid_cameras: + logger.warning( + f"Role '{role}' references non-existent cameras: {invalid_cameras}. " + ) + + return self + + @field_validator("cameras") + @classmethod + def ensure_zones_and_cameras_have_different_names(cls, v: dict[str, CameraConfig]): + zones = [zone for camera in v.values() for zone in camera.zones.keys()] + for zone in zones: + if zone in v.keys(): + raise ValueError("Zones cannot share names with cameras") + return v + + @classmethod + def load(cls, **kwargs): + """Loads the Frigate config file, runs migrations, and creates the config object.""" + config_path = find_config_file() + + # No configuration file found, create one. + new_config = False + if not os.path.isfile(config_path): + logger.info("No config file found, saving default config") + config_path = config_path + new_config = True + else: + # Check if the config file needs to be migrated. + migrate_frigate_config(config_path) + + # Finally, load the resulting configuration file. + with open(config_path, "a+" if new_config else "r") as f: + # Only write the default config if the opened file is non-empty. This can happen as + # a race condition. It's extremely unlikely, but eh. Might as well check it. + if new_config and f.tell() == 0: + f.write(DEFAULT_CONFIG) + logger.info( + "Created default config file, see the getting started docs for configuration: https://docs.frigate.video/guides/getting_started" + ) + + f.seek(0) + return FrigateConfig.parse(f, **kwargs) + + @classmethod + def parse(cls, config, *, is_json=None, safe_load=False, **context): + # If config is a file, read its contents. + if hasattr(config, "read"): + fname = getattr(config, "name", None) + config = config.read() + + # Try to guess the value of is_json from the file extension. + if is_json is None and fname: + _, ext = os.path.splitext(fname) + if ext in (".yaml", ".yml"): + is_json = False + elif ext == ".json": + is_json = True + + # At this point, try to sniff the config string, to guess if it is json or not. + if is_json is None: + is_json = REGEX_JSON.match(config) is not None + + # Parse the config into a dictionary. + if is_json: + config = json.load(config) + else: + config = yaml.load(config) + + # load minimal Frigate config after the full config did not validate + if safe_load: + safe_config = {"safe_mode": True, "cameras": {}, "mqtt": {"enabled": False}} + + # copy over auth and proxy config in case auth needs to be enforced + safe_config["auth"] = config.get("auth", {}) + safe_config["proxy"] = config.get("proxy", {}) + + # copy over database config for auth and so a new db is not created + safe_config["database"] = config.get("database", {}) + + return cls.parse_object(safe_config, **context) + + # Validate and return the config dict. + return cls.parse_object(config, **context) + + @classmethod + def parse_yaml(cls, config_yaml, **context): + return cls.parse(config_yaml, is_json=False, **context) + + @classmethod + def parse_object( + cls, obj: Any, *, plus_api: PlusApi | None = None, install: bool = False + ): + return cls.model_validate( + obj, context={"plus_api": plus_api, "install": install} + ) diff --git a/frigate/config/database.py b/frigate/config/database.py new file mode 100644 index 0000000..8064561 --- /dev/null +++ b/frigate/config/database.py @@ -0,0 +1,15 @@ +from pydantic import Field + +from frigate.const import DEFAULT_DB_PATH + +from .base import FrigateBaseModel + +__all__ = ["DatabaseConfig"] + + +class DatabaseConfig(FrigateBaseModel): + path: str = Field( + default=DEFAULT_DB_PATH, + title="Database path", + description="Filesystem path where the Frigate SQLite database file will be stored.", + ) # noqa: F821 diff --git a/frigate/config/env.py b/frigate/config/env.py new file mode 100644 index 0000000..209dda6 --- /dev/null +++ b/frigate/config/env.py @@ -0,0 +1,105 @@ +import os +import re +from pathlib import Path +from typing import Annotated + +from pydantic import AfterValidator, ValidationInfo + +FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")} +secrets_dir = os.environ.get("CREDENTIALS_DIRECTORY", "/run/secrets") +# read secret files as env vars too +if os.path.isdir(secrets_dir) and os.access(secrets_dir, os.R_OK): + for secret_file in os.listdir(secrets_dir): + if secret_file.startswith("FRIGATE_"): + FRIGATE_ENV_VARS[secret_file] = ( + Path(os.path.join(secrets_dir, secret_file)).read_text().strip() + ) + + +# Matches a FRIGATE_* identifier following an opening brace. +_FRIGATE_IDENT_RE = re.compile(r"FRIGATE_[A-Za-z0-9_]+") + + +def substitute_frigate_vars(value: str) -> str: + """Substitute `{FRIGATE_*}` placeholders in *value*. + + Reproduces the subset of `str.format()` brace semantics that Frigate's + config has historically supported, while leaving unrelated brace content + (e.g. ffmpeg `%{localtime\\:...}` expressions) untouched: + + * `{{` and `}}` collapse to literal `{` / `}` (the documented escape). + * `{FRIGATE_NAME}` is replaced from `FRIGATE_ENV_VARS`; an unknown name + raises `KeyError` to preserve the existing "Invalid substitution" + error path. + * A `{` that begins `{FRIGATE_` but is not a well-formed + `{FRIGATE_NAME}` placeholder raises `ValueError` (malformed + placeholder). Callers that catch `KeyError` to allow unknown-var + passthrough will still surface malformed syntax as an error. + * Any other `{` or `}` is treated as a literal and passed through. + """ + out: list[str] = [] + i = 0 + n = len(value) + while i < n: + ch = value[i] + if ch == "{": + # Escaped literal `{{`. + if i + 1 < n and value[i + 1] == "{": + out.append("{") + i += 2 + continue + # Possible `{FRIGATE_*}` placeholder. + if value.startswith("{FRIGATE_", i): + ident_match = _FRIGATE_IDENT_RE.match(value, i + 1) + if ( + ident_match is not None + and ident_match.end() < n + and value[ident_match.end()] == "}" + ): + key = ident_match.group(0) + if key not in FRIGATE_ENV_VARS: + raise KeyError(key) + out.append(FRIGATE_ENV_VARS[key]) + i = ident_match.end() + 1 + continue + # Looks like a FRIGATE placeholder but is malformed + # (no closing brace, illegal char, format spec, etc.). + raise ValueError( + f"Malformed FRIGATE_ placeholder near {value[i : i + 32]!r}" + ) + # Plain `{` — pass through (e.g. `%{localtime\:...}`). + out.append("{") + i += 1 + continue + if ch == "}": + # Escaped literal `}}`. + if i + 1 < n and value[i + 1] == "}": + out.append("}") + i += 2 + continue + out.append("}") + i += 1 + continue + out.append(ch) + i += 1 + return "".join(out) + + +def validate_env_string(v: str) -> str: + return substitute_frigate_vars(v) + + +EnvString = Annotated[str, AfterValidator(validate_env_string)] + + +def validate_env_vars(v: dict[str, str], info: ValidationInfo) -> dict[str, str]: + if isinstance(info.context, dict) and info.context.get("install", False): + for k, val in v.items(): + os.environ[k] = val + if k.startswith("FRIGATE_"): + FRIGATE_ENV_VARS[k] = val + + return v + + +EnvVars = Annotated[dict[str, str], AfterValidator(validate_env_vars)] diff --git a/frigate/config/logger.py b/frigate/config/logger.py new file mode 100644 index 0000000..906f75e --- /dev/null +++ b/frigate/config/logger.py @@ -0,0 +1,29 @@ +from typing import Self + +from pydantic import Field, ValidationInfo, model_validator + +from frigate.log import LogLevel, apply_log_levels + +from .base import FrigateBaseModel + +__all__ = ["LoggerConfig"] + + +class LoggerConfig(FrigateBaseModel): + default: LogLevel = Field( + default=LogLevel.info, + title="Logging level", + description="Default global log verbosity (debug, info, warning, error).", + ) + logs: dict[str, LogLevel] = Field( + default_factory=dict, + title="Per-process log level", + description="Per-component log level overrides to increase or decrease verbosity for specific modules.", + ) + + @model_validator(mode="after") + def post_validation(self, info: ValidationInfo) -> Self: + if isinstance(info.context, dict) and info.context.get("install", False): + apply_log_levels(self.default.value.upper(), self.logs) + + return self diff --git a/frigate/config/mqtt.py b/frigate/config/mqtt.py new file mode 100644 index 0000000..28c002a --- /dev/null +++ b/frigate/config/mqtt.py @@ -0,0 +1,86 @@ +from typing import Self + +from pydantic import Field, ValidationInfo, model_validator + +from frigate.const import FREQUENCY_STATS_POINTS + +from .base import FrigateBaseModel +from .env import EnvString + +__all__ = ["MqttConfig"] + + +class MqttConfig(FrigateBaseModel): + enabled: bool = Field( + default=True, + title="Enable MQTT", + description="Enable or disable MQTT integration for state, events, and snapshots.", + ) + host: EnvString = Field( + default="", + title="MQTT host", + description="Hostname or IP address of the MQTT broker.", + ) + port: int = Field( + default=1883, + title="MQTT port", + description="Port of the MQTT broker (usually 1883 for plain MQTT).", + ) + topic_prefix: str = Field( + default="frigate", + title="Topic prefix", + description="MQTT topic prefix for all Frigate topics; must be unique if running multiple instances.", + ) + client_id: str = Field( + default="frigate", + title="Client ID", + description="Client identifier used when connecting to the MQTT broker; should be unique per instance.", + ) + stats_interval: int = Field( + default=60, + ge=FREQUENCY_STATS_POINTS, + title="Stats interval", + description="Interval in seconds for publishing system and camera stats to MQTT.", + ) + user: EnvString | None = Field( + default=None, + title="MQTT username", + description="Optional MQTT username; can be provided via environment variables or secrets.", + ) + password: EnvString | None = Field( + default=None, + title="MQTT password", + description="Optional MQTT password; can be provided via environment variables or secrets.", + validate_default=True, + ) + tls_ca_certs: str | None = Field( + default=None, + title="TLS CA certs", + description="Path to CA certificate for TLS connections to the broker (for self-signed certs).", + ) + tls_client_cert: str | None = Field( + default=None, + title="Client cert", + description="Client certificate path for TLS mutual authentication; do not set user/password when using client certs.", + ) + tls_client_key: str | None = Field( + default=None, + title="Client key", + description="Private key path for the client certificate.", + ) + tls_insecure: bool | None = Field( + default=None, + title="TLS insecure", + description="Allow insecure TLS connections by skipping hostname verification (not recommended).", + ) + qos: int = Field( + default=0, + title="MQTT QoS", + description="Quality of Service level for MQTT publishes/subscriptions (0, 1, or 2).", + ) + + @model_validator(mode="after") + def user_requires_pass(self, info: ValidationInfo) -> Self: + if (self.user is None) != (self.password is None): + raise ValueError("Password must be provided with username.") + return self diff --git a/frigate/config/network.py b/frigate/config/network.py new file mode 100644 index 0000000..7feaab0 --- /dev/null +++ b/frigate/config/network.py @@ -0,0 +1,39 @@ +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["IPv6Config", "ListenConfig", "NetworkingConfig"] + + +class IPv6Config(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable IPv6", + description="Enable IPv6 support for Frigate services (API and UI) where applicable.", + ) + + +class ListenConfig(FrigateBaseModel): + internal: int | str = Field( + default=5000, + title="Internal port", + description="Internal listening port for Frigate (default 5000).", + ) + external: int | str = Field( + default=8971, + title="External port", + description="External listening port for Frigate (default 8971).", + ) + + +class NetworkingConfig(FrigateBaseModel): + ipv6: IPv6Config = Field( + default_factory=IPv6Config, + title="IPv6 configuration", + description="IPv6-specific settings for Frigate network services.", + ) + listen: ListenConfig = Field( + default_factory=ListenConfig, + title="Listening ports configuration", + description="Configuration for internal and external listening ports. This is for advanced users. For the majority of use cases it's recommended to change the ports section of your Docker compose file.", + ) diff --git a/frigate/config/profile.py b/frigate/config/profile.py new file mode 100644 index 0000000..2d6dd1b --- /dev/null +++ b/frigate/config/profile.py @@ -0,0 +1,20 @@ +"""Top-level profile definition configuration.""" + +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["ProfileDefinitionConfig"] + + +class ProfileDefinitionConfig(FrigateBaseModel): + """Defines a named profile with a human-readable display name. + + The dict key is the machine name used internally; friendly_name + is the label shown in the UI and API responses. + """ + + friendly_name: str = Field( + title="Friendly name", + description="Display name for this profile shown in the UI.", + ) diff --git a/frigate/config/profile_manager.py b/frigate/config/profile_manager.py new file mode 100644 index 0000000..cfd0dc9 --- /dev/null +++ b/frigate/config/profile_manager.py @@ -0,0 +1,416 @@ +"""Profile manager for activating/deactivating named config profiles.""" + +import copy +import json +import logging +from collections.abc import Callable +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdatePublisher, + CameraConfigUpdateTopic, +) +from frigate.config.camera.zone import ZoneConfig +from frigate.const import CONFIG_DIR +from frigate.util.builtin import deep_merge +from frigate.util.config import apply_section_update + +logger = logging.getLogger(__name__) + +PROFILE_SECTION_UPDATES: dict[str, CameraConfigUpdateEnum] = { + "audio": CameraConfigUpdateEnum.audio, + "birdseye": CameraConfigUpdateEnum.birdseye, + "detect": CameraConfigUpdateEnum.detect, + "face_recognition": CameraConfigUpdateEnum.face_recognition, + "lpr": CameraConfigUpdateEnum.lpr, + "motion": CameraConfigUpdateEnum.motion, + "notifications": CameraConfigUpdateEnum.notifications, + "objects": CameraConfigUpdateEnum.objects, + "record": CameraConfigUpdateEnum.record, + "review": CameraConfigUpdateEnum.review, + "snapshots": CameraConfigUpdateEnum.snapshots, + "zones": CameraConfigUpdateEnum.zones, +} + +# Retained MQTT switch topics per profile section, with a payload getter. +# Republished on profile change so MQTT/HA don't show a stale toggle. +SECTION_STATE_TOPICS: dict[str, list[tuple[str, Callable[[Any], Any]]]] = { + "audio": [("audio", lambda c: "ON" if c.audio.enabled else "OFF")], + "birdseye": [ + ("birdseye", lambda c: "ON" if c.birdseye.enabled else "OFF"), + ( + "birdseye_mode", + lambda c: c.birdseye.mode.value.upper() if c.birdseye.enabled else "OFF", + ), + ], + "detect": [("detect", lambda c: "ON" if c.detect.enabled else "OFF")], + "motion": [ + ("motion", lambda c: "ON" if c.motion.enabled else "OFF"), + ("improve_contrast", lambda c: "ON" if c.motion.improve_contrast else "OFF"), + ("motion_threshold", lambda c: c.motion.threshold), + ("motion_contour_area", lambda c: c.motion.contour_area), + ], + "notifications": [ + ("notifications", lambda c: "ON" if c.notifications.enabled else "OFF"), + ], + "objects": [ + ("object_descriptions", lambda c: "ON" if c.objects.genai.enabled else "OFF"), + ], + "record": [("recordings", lambda c: "ON" if c.record.enabled else "OFF")], + "review": [ + ("review_alerts", lambda c: "ON" if c.review.alerts.enabled else "OFF"), + ( + "review_detections", + lambda c: "ON" if c.review.detections.enabled else "OFF", + ), + ( + "review_descriptions", + lambda c: "ON" if c.review.genai.enabled else "OFF", + ), + ], + "snapshots": [("snapshots", lambda c: "ON" if c.snapshots.enabled else "OFF")], +} + +PERSISTENCE_FILE = Path(CONFIG_DIR) / ".profiles" + + +class ProfileManager: + """Manages profile activation, persistence, and config application.""" + + def __init__( + self, + config, + config_updater: CameraConfigUpdatePublisher, + dispatcher=None, + ): + from frigate.config.config import FrigateConfig + + self.config: FrigateConfig = config + self.config_updater = config_updater + self.dispatcher = dispatcher + self._base_configs: dict[str, dict[str, dict]] = {} + self._base_api_configs: dict[str, dict[str, dict]] = {} + self._base_enabled: dict[str, bool] = {} + self._base_zones: dict[str, dict[str, ZoneConfig]] = {} + self._snapshot_base_configs() + + def _snapshot_base_configs(self) -> None: + """Snapshot each camera's current section configs, enabled, and zones.""" + for cam_name, cam_config in self.config.cameras.items(): + self._base_configs[cam_name] = {} + self._base_api_configs[cam_name] = {} + self._base_enabled[cam_name] = cam_config.enabled + self._base_zones[cam_name] = copy.deepcopy(cam_config.zones) + for section in PROFILE_SECTION_UPDATES: + section_value = getattr(cam_config, section, None) + if section_value is None: + continue + + if section == "zones": + # zones is a dict of ZoneConfig models + self._base_configs[cam_name][section] = { + name: zone.model_dump() for name, zone in section_value.items() + } + self._base_api_configs[cam_name][section] = { + name: { + **zone.model_dump( + mode="json", + warnings="none", + exclude_none=True, + ), + "color": zone.color, + } + for name, zone in section_value.items() + } + else: + self._base_configs[cam_name][section] = section_value.model_dump() + self._base_api_configs[cam_name][section] = ( + section_value.model_dump( + mode="json", + warnings="none", + exclude_none=True, + ) + ) + + def update_config(self, new_config) -> None: + """Update config reference after config/set replaces the in-memory config. + + Preserves active profile state: re-snapshots base configs from the new + (freshly parsed) config, then re-applies profile overrides if a profile + was active. + """ + current_active = self.config.active_profile + self.config = new_config + + # Re-snapshot base configs from the new config (which has base values) + self._base_configs.clear() + self._base_api_configs.clear() + self._base_enabled.clear() + self._base_zones.clear() + self._snapshot_base_configs() + + # Re-apply profile overrides without publishing ZMQ updates + # (the config/set caller handles its own ZMQ publishing) + if current_active is not None: + if current_active in self.config.profiles: + changed: dict[str, set[str]] = {} + self._apply_profile_overrides(current_active, changed) + self.config.active_profile = current_active + else: + # Profile was deleted — deactivate + self.config.active_profile = None + self._persist_active_profile(None) + + # drop all runtime overrides so they don't replay stale values on restart + if self.dispatcher is not None: + self.dispatcher.clear_runtime_state() + + def activate_profile( + self, + profile_name: str | None, + clear_runtime_overrides: bool = True, + ) -> str | None: + """Activate a profile by name, or deactivate if None. + + Args: + profile_name: Profile name to activate, or None to deactivate. + clear_runtime_overrides: When True (the default, for user-initiated + activations) drop the dispatcher's runtime override file because + the layer below changed. Startup callers that are replaying a + persisted profile pass False so the runtime state stays + available for the subsequent replay step. + + Returns: + None on success, or an error message string on failure. + """ + if profile_name is not None: + if profile_name not in self.config.profiles: + return ( + f"Profile '{profile_name}' is not defined in the profiles section" + ) + + # Track which camera/section pairs get changed for ZMQ publishing + changed: dict[str, set[str]] = {} + + # Reset all cameras to base config + self._reset_to_base(changed) + + # Apply new profile overrides if activating + if profile_name is not None: + err = self._apply_profile_overrides(profile_name, changed) + if err: + return err + + # Publish ZMQ updates only for sections that actually changed + self._publish_updates(changed) + + self.config.active_profile = profile_name + self._persist_active_profile(profile_name) + + # a profile switch invalidates the steady-state runtime overrides + if clear_runtime_overrides and self.dispatcher is not None: + self.dispatcher.clear_runtime_state() + + logger.info( + "Profile %s", + f"'{profile_name}' activated" if profile_name else "deactivated", + ) + return None + + def _reset_to_base(self, changed: dict[str, set[str]]) -> None: + """Reset all cameras to their base (no-profile) config.""" + for cam_name, cam_config in self.config.cameras.items(): + # Restore enabled state + base_enabled = self._base_enabled.get(cam_name) + if base_enabled is not None and cam_config.enabled != base_enabled: + cam_config.enabled = base_enabled + changed.setdefault(cam_name, set()).add("enabled") + + # Restore zones (always restore from snapshot; direct Pydantic + # comparison fails when ZoneConfig contains numpy arrays) + base_zones = self._base_zones.get(cam_name) + if base_zones is not None: + cam_config.zones = copy.deepcopy(base_zones) + changed.setdefault(cam_name, set()).add("zones") + + # Restore section configs (zones handled above) + base = self._base_configs.get(cam_name, {}) + for section in PROFILE_SECTION_UPDATES: + if section == "zones": + continue + base_data = base.get(section) + if base_data is None: + continue + err = apply_section_update(cam_config, section, base_data) + if err: + logger.error( + "Failed to reset section '%s' on camera '%s': %s", + section, + cam_name, + err, + ) + else: + changed.setdefault(cam_name, set()).add(section) + + def _apply_profile_overrides( + self, profile_name: str, changed: dict[str, set[str]] + ) -> str | None: + """Apply profile overrides for all cameras that have the named profile.""" + for cam_name, cam_config in self.config.cameras.items(): + profile = cam_config.profiles.get(profile_name) + if profile is None: + continue + + # Apply enabled override + if profile.enabled is not None and cam_config.enabled != profile.enabled: + cam_config.enabled = profile.enabled + changed.setdefault(cam_name, set()).add("enabled") + + # Apply zones override — merge profile zones into base zones + if profile.zones is not None: + base_zones = self._base_zones.get(cam_name, {}) + merged_zones = copy.deepcopy(base_zones) + merged_zones.update(profile.zones) + # Profile zone objects are parsed without colors or contours + # (those are set during CameraConfig init / post-validation). + # Inherit the base zone's color when available, and ensure + # every zone has a valid contour for rendering. + for name, zone in merged_zones.items(): + if zone.contour.size == 0: + zone.generate_contour(cam_config.frame_shape) + if zone.color == (0, 0, 0) and name in base_zones: + zone._color = base_zones[name].color + cam_config.zones = merged_zones + changed.setdefault(cam_name, set()).add("zones") + + base = self._base_configs.get(cam_name, {}) + + for section in PROFILE_SECTION_UPDATES: + if section == "zones": + continue + profile_section = getattr(profile, section, None) + if profile_section is None: + continue + + overrides = profile_section.model_dump(exclude_unset=True) + if not overrides: + continue + + base_data = base.get(section, {}) + merged = deep_merge(overrides, base_data) + + err = apply_section_update(cam_config, section, merged) + if err: + return f"Failed to apply profile '{profile_name}' section '{section}' on camera '{cam_name}': {err}" + + changed.setdefault(cam_name, set()).add(section) + + return None + + def _publish_updates(self, changed: dict[str, set[str]]) -> None: + """Publish ZMQ config updates only for sections that changed.""" + for cam_name, sections in changed.items(): + cam_config = self.config.cameras.get(cam_name) + if cam_config is None: + continue + + for section in sections: + if section == "enabled": + self.config_updater.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.enabled, cam_name + ), + cam_config.enabled, + ) + if self.dispatcher is not None: + self.dispatcher.publish( + f"{cam_name}/enabled/state", + "ON" if cam_config.enabled else "OFF", + retain=True, + ) + continue + + if section == "zones": + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.zones, cam_name), + cam_config.zones, + ) + continue + + update_enum = PROFILE_SECTION_UPDATES.get(section) + if update_enum is None: + continue + settings = getattr(cam_config, section, None) + if settings is not None: + self.config_updater.publish_update( + CameraConfigUpdateTopic(update_enum, cam_name), + settings, + ) + + # republish MQTT switch states + if self.dispatcher is not None: + for suffix, get_payload in SECTION_STATE_TOPICS.get(section, ()): + self.dispatcher.publish( + f"{cam_name}/{suffix}/state", + get_payload(cam_config), + retain=True, + ) + + def _persist_active_profile(self, profile_name: str | None) -> None: + """Persist the active profile state to disk as JSON.""" + try: + data = self._load_persisted_data() + data["active"] = profile_name + if profile_name is not None: + data.setdefault("last_activated", {})[profile_name] = datetime.now( + UTC + ).timestamp() + PERSISTENCE_FILE.write_text(json.dumps(data)) + except OSError: + logger.exception("Failed to persist active profile") + + @staticmethod + def _load_persisted_data() -> dict: + """Load the full persisted profile data from disk.""" + try: + if PERSISTENCE_FILE.exists(): + raw = PERSISTENCE_FILE.read_text().strip() + if raw: + return json.loads(raw) + except (OSError, json.JSONDecodeError): + logger.exception("Failed to load persisted profile data") + return {"active": None, "last_activated": {}} + + @staticmethod + def load_persisted_profile() -> str | None: + """Load the persisted active profile name from disk.""" + data = ProfileManager._load_persisted_data() + name = data.get("active") + return name if name else None + + def get_base_configs_for_api(self, camera_name: str) -> dict[str, dict]: + """Return base (pre-profile) section configs for a camera. + + These are JSON-serializable dicts suitable for direct inclusion in + the /api/config response, with None values already excluded. + """ + return self._base_api_configs.get(camera_name, {}) + + def get_available_profiles(self) -> list[dict[str, str]]: + """Get list of all profile definitions from the top-level config.""" + return [ + {"name": name, "friendly_name": defn.friendly_name} + for name, defn in sorted(self.config.profiles.items()) + ] + + def get_profile_info(self) -> dict: + """Get profile state info for API responses.""" + data = self._load_persisted_data() + return { + "profiles": self.get_available_profiles(), + "active_profile": self.config.active_profile, + "last_activated": data.get("last_activated", {}), + } diff --git a/frigate/config/proxy.py b/frigate/config/proxy.py new file mode 100644 index 0000000..1961105 --- /dev/null +++ b/frigate/config/proxy.py @@ -0,0 +1,59 @@ +from pydantic import Field, field_validator + +from .base import FrigateBaseModel +from .env import EnvString + +__all__ = ["ProxyConfig", "HeaderMappingConfig"] + + +class HeaderMappingConfig(FrigateBaseModel): + user: str = Field( + default=None, + title="User header", + description="Header containing the authenticated username provided by the upstream proxy.", + ) + role: str = Field( + default=None, + title="Role header", + description="Header containing the authenticated user's role or groups from the upstream proxy.", + ) + role_map: dict[str, list[str]] | None = Field( + default_factory=dict, + title=("Role mapping"), + description="Map upstream group values to Frigate roles (for example map admin groups to the admin role).", + ) + + +class ProxyConfig(FrigateBaseModel): + header_map: HeaderMappingConfig = Field( + default_factory=HeaderMappingConfig, + title="Header mapping", + description="Map incoming proxy headers to Frigate user and role fields for proxy-based auth.", + ) + logout_url: str | None = Field( + default=None, + title="Logout URL", + description="URL to redirect users to when logging out via the proxy.", + ) + auth_secret: EnvString | None = Field( + default=None, + title="Proxy secret", + description="Optional secret checked against the X-Proxy-Secret header to verify trusted proxies.", + ) + default_role: str | None = Field( + default="viewer", + title="Default role", + description="Default role assigned to proxy-authenticated users when no role mapping applies.", + ) + separator: str | None = Field( + default=",", + title="Separator character", + description="Character used to split multiple values provided in proxy headers.", + ) + + @field_validator("separator", mode="before") + @classmethod + def validate_separator_length(cls, v): + if v is not None and len(v) != 1: + raise ValueError("Separator must be exactly one character") + return v diff --git a/frigate/config/telemetry.py b/frigate/config/telemetry.py new file mode 100644 index 0000000..3c219d7 --- /dev/null +++ b/frigate/config/telemetry.py @@ -0,0 +1,46 @@ +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["TelemetryConfig", "StatsConfig"] + + +class StatsConfig(FrigateBaseModel): + amd_gpu_stats: bool = Field( + default=True, + title="AMD GPU stats", + description="Enable collection of AMD GPU statistics if an AMD GPU is present.", + ) + intel_gpu_stats: bool = Field( + default=True, + title="Intel GPU stats", + description="Enable collection of Intel GPU statistics if an Intel GPU is present.", + ) + network_bandwidth: bool = Field( + default=False, + title="Network bandwidth", + description="Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities).", + ) + intel_gpu_device: str | None = Field( + default=None, + title="Intel GPU device", + description="PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present.", + ) + + +class TelemetryConfig(FrigateBaseModel): + network_interfaces: list[str] = Field( + default=[], + title="Network interfaces", + description="List of network interface name prefixes to monitor for bandwidth statistics.", + ) + stats: StatsConfig = Field( + default_factory=StatsConfig, + title="System stats", + description="Options to enable/disable collection of various system and GPU statistics.", + ) + version_check: bool = Field( + default=True, + title="Version check", + description="Enable an outbound check to detect if a newer Frigate version is available.", + ) diff --git a/frigate/config/tls.py b/frigate/config/tls.py new file mode 100644 index 0000000..cada110 --- /dev/null +++ b/frigate/config/tls.py @@ -0,0 +1,13 @@ +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["TlsConfig"] + + +class TlsConfig(FrigateBaseModel): + enabled: bool = Field( + default=True, + title="Enable TLS", + description="Enable TLS for Frigate's web UI and API on the configured TLS port.", + ) diff --git a/frigate/config/ui.py b/frigate/config/ui.py new file mode 100644 index 0000000..5958c0e --- /dev/null +++ b/frigate/config/ui.py @@ -0,0 +1,36 @@ +from enum import Enum + +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["TimeFormatEnum", "UnitSystemEnum", "UIConfig"] + + +class TimeFormatEnum(str, Enum): + browser = "browser" + hours12 = "12hour" + hours24 = "24hour" + + +class UnitSystemEnum(str, Enum): + imperial = "imperial" + metric = "metric" + + +class UIConfig(FrigateBaseModel): + timezone: str | None = Field( + default=None, + title="Timezone", + description="Optional timezone to display across the UI (defaults to browser local time if unset).", + ) + time_format: TimeFormatEnum = Field( + default=TimeFormatEnum.browser, + title="Time format", + description="Time format to use in the UI (browser, 12hour, or 24hour).", + ) + unit_system: UnitSystemEnum = Field( + default=UnitSystemEnum.metric, + title="Unit system", + description="Unit system for display (metric or imperial) used in the UI and MQTT.", + ) diff --git a/frigate/const.py b/frigate/const.py new file mode 100644 index 0000000..dac04c4 --- /dev/null +++ b/frigate/const.py @@ -0,0 +1,174 @@ +import os +import re + +INSTALL_DIR = "/opt/frigate" +CONFIG_DIR = "/config" +DEFAULT_DB_PATH = f"{CONFIG_DIR}/frigate.db" +MODEL_CACHE_DIR = f"{CONFIG_DIR}/model_cache" +BASE_DIR = "/media/frigate" +CLIPS_DIR = f"{BASE_DIR}/clips" +EXPORT_DIR = f"{BASE_DIR}/exports" +FACE_DIR = f"{CLIPS_DIR}/faces" +THUMB_DIR = f"{CLIPS_DIR}/thumbs" +RECORD_DIR = f"{BASE_DIR}/recordings" +TRIGGER_DIR = f"{CLIPS_DIR}/triggers" +BIRDSEYE_PIPE = "/tmp/cache/birdseye" +CACHE_DIR = "/tmp/cache" +REPLAY_CAMERA_PREFIX = "_replay_" +REPLAY_DIR = os.path.join(CLIPS_DIR, "replay") +PLUS_ENV_VAR = "PLUS_API_KEY" +PLUS_API_HOST = "https://api.frigate.video" + +SHM_FRAMES_VAR = "SHM_MAX_FRAMES" + +REDACTED_CREDENTIAL_SENTINEL = "__FRIGATE_SAVED_CREDENTIAL__" + +# Attribute & Object constants + +DEFAULT_ATTRIBUTE_LABEL_MAP = { + "person": ["amazon", "face"], + "car": [ + "amazon", + "an_post", + "canada_post", + "dhl", + "dpd", + "fedex", + "gls", + "license_plate", + "nzpost", + "postnl", + "postnord", + "purolator", + "royal_mail", + "ups", + "usps", + ], + "motorcycle": ["license_plate"], +} +ATTRIBUTE_LABEL_DISPLAY_MAP = { + "amazon": "Amazon", + "an_post": "An Post", + "canada_post": "Canada Post", + "dhl": "DHL", + "dpd": "DPD", + "fedex": "FedEx", + "gls": "GLS", + "nzpost": "NZ Post", + "postnl": "PostNL", + "postnord": "PostNord", + "purolator": "Purolator", + "royal_mail": "Royal Mail", + "ups": "UPS", + "usps": "USPS", +} +LABEL_CONSOLIDATION_MAP = { + "car": 0.8, + "face": 0.5, +} +LABEL_CONSOLIDATION_DEFAULT = 0.9 +LABEL_NMS_MAP = { + "car": 0.6, +} +LABEL_NMS_DEFAULT = 0.4 + +# Audio constants + +AUDIO_DURATION = 0.975 +AUDIO_FORMAT = "s16le" +AUDIO_MAX_BIT_RANGE = 32768.0 +AUDIO_SAMPLE_RATE = 16000 +AUDIO_MIN_CONFIDENCE = 0.5 + +# DB constants + +MAX_WAL_SIZE = 10 # MB + +# Ffmpeg constants + +DEFAULT_FFMPEG_VERSION = os.environ.get("DEFAULT_FFMPEG_VERSION", "") +INCLUDED_FFMPEG_VERSIONS = os.environ.get("INCLUDED_FFMPEG_VERSIONS", "").split(":") +LIBAVFORMAT_VERSION_MAJOR = int(os.environ.get("LIBAVFORMAT_VERSION_MAJOR", "59")) +FFMPEG_HWACCEL_NVIDIA = "preset-nvidia" +FFMPEG_HWACCEL_VAAPI = "preset-vaapi" +FFMPEG_HWACCEL_VULKAN = "preset-vulkan" +FFMPEG_HWACCEL_RKMPP = "preset-rkmpp" +FFMPEG_HWACCEL_AMF = "preset-amd-amf" +FFMPEG_HVC1_ARGS = ["-tag:v", "hvc1"] + +# RKNN constants +SUPPORTED_RK_SOCS = ["rk3562", "rk3566", "rk3568", "rk3576", "rk3588"] + +# Regex constants + +REGEX_CAMERA_NAME = r"^[a-zA-Z0-9_-]+$" +REGEX_RTSP_CAMERA_USER_PASS = r":\/\/[a-zA-Z0-9_-]+:[\S]+@" +REGEX_HTTP_CAMERA_USER_PASS = r"user=[a-zA-Z0-9_-]+&password=[\S]+" +REGEX_JSON = re.compile(r"^\s*\{") + +# Known Driver Names + +DRIVER_ENV_VAR = "LIBVA_DRIVER_NAME" +DRIVER_AMD = "radeonsi" +DRIVER_INTEL_i965 = "i965" +DRIVER_INTEL_iHD = "iHD" + +# Preview Values + +PREVIEW_FRAME_TYPE = "webp" + +# Record Values + +CACHE_SEGMENT_FORMAT = "%Y%m%d%H%M%S%z" +MAX_PRE_CAPTURE = 60 +MAX_SEGMENT_DURATION = 600 +MAX_SEGMENTS_IN_CACHE = 6 +MAX_PLAYLIST_SECONDS = 7200 # support 2 hour segments for a single playlist to account for cameras with inconsistent segment times + +# Internal Comms Topics + +INSERT_MANY_RECORDINGS = "insert_many_recordings" +INSERT_PREVIEW = "insert_preview" +REQUEST_REGION_GRID = "request_region_grid" +UPSERT_REVIEW_SEGMENT = "upsert_review_segment" +CLEAR_ONGOING_REVIEW_SEGMENTS = "clear_ongoing_review_segments" +UPDATE_CAMERA_ACTIVITY = "update_camera_activity" +UPDATE_AUDIO_ACTIVITY = "update_audio_activity" +EXPIRE_AUDIO_ACTIVITY = "expire_audio_activity" +UPDATE_AUDIO_TRANSCRIPTION_STATE = "update_audio_transcription_state" +UPDATE_EVENT_DESCRIPTION = "update_event_description" +UPDATE_REVIEW_DESCRIPTION = "update_review_description" +UPDATE_MODEL_STATE = "update_model_state" +UPDATE_EMBEDDINGS_REINDEX_PROGRESS = "handle_embeddings_reindex_progress" +UPDATE_BIRDSEYE_LAYOUT = "update_birdseye_layout" +UPDATE_JOB_STATE = "update_job_state" +NOTIFICATION_TEST = "notification_test" + +# IO Nice Values + +PROCESS_PRIORITY_HIGH = 0 +PROCESS_PRIORITY_MED = 10 +PROCESS_PRIORITY_LOW = 19 + +# Stats Values + +FREQUENCY_STATS_POINTS = 15 + +# Autotracking + +AUTOTRACKING_MAX_AREA_RATIO = 0.6 +AUTOTRACKING_MOTION_MIN_DISTANCE = 20 +AUTOTRACKING_MOTION_MAX_POINTS = 500 +AUTOTRACKING_MAX_MOVE_METRICS = 500 +AUTOTRACKING_ZOOM_OUT_HYSTERESIS = 1.1 +AUTOTRACKING_ZOOM_IN_HYSTERESIS = 0.95 +AUTOTRACKING_ZOOM_EDGE_THRESHOLD = 0.05 + +# Auth + +JWT_SECRET_ENV_VAR = "FRIGATE_JWT_SECRET" +PASSWORD_HASH_ALGORITHM = "pbkdf2_sha256" + +# Queues + +FAST_QUEUE_TIMEOUT = 0.00001 # seconds diff --git a/frigate/data_processing/common/audio_transcription/model.py b/frigate/data_processing/common/audio_transcription/model.py new file mode 100644 index 0000000..a610ca9 --- /dev/null +++ b/frigate/data_processing/common/audio_transcription/model.py @@ -0,0 +1,83 @@ +"""Set up audio transcription models based on model size.""" + +import logging +import os + +import sherpa_onnx + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import MODEL_CACHE_DIR +from frigate.data_processing.types import AudioTranscriptionModel +from frigate.util.downloader import ModelDownloader + +logger = logging.getLogger(__name__) + + +class AudioTranscriptionModelRunner: + def __init__( + self, + device: str = "CPU", + model_size: str = "small", + ): + self.model: AudioTranscriptionModel = None + self.requestor = InterProcessRequestor() + + if model_size == "large": + # use the Whisper download function instead of our own + # Import dynamically to avoid crashes on systems without AVX support + from faster_whisper.utils import download_model + + logger.debug("Downloading Whisper audio transcription model") + download_model( + size_or_id="small" if device == "cuda" else "tiny", + local_files_only=False, + cache_dir=os.path.join(MODEL_CACHE_DIR, "whisper"), + ) + logger.debug("Whisper audio transcription model downloaded") + + else: + # small model as default + download_path = os.path.join(MODEL_CACHE_DIR, "sherpa-onnx") + HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co") + self.model_files = { + "encoder.onnx": f"{HF_ENDPOINT}/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-06-26/resolve/main/encoder-epoch-99-avg-1-chunk-16-left-128.onnx", + "decoder.onnx": f"{HF_ENDPOINT}/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-06-26/resolve/main/decoder-epoch-99-avg-1-chunk-16-left-128.onnx", + "joiner.onnx": f"{HF_ENDPOINT}/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-06-26/resolve/main/joiner-epoch-99-avg-1-chunk-16-left-128.onnx", + "tokens.txt": f"{HF_ENDPOINT}/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-06-26/resolve/main/tokens.txt", + } + + if not all( + os.path.exists(os.path.join(download_path, n)) + for n in self.model_files.keys() + ): + self.downloader = ModelDownloader( + model_name="sherpa-onnx", + download_path=download_path, + file_names=list(self.model_files.keys()), + download_func=self.__download_models, + ) + self.downloader.ensure_model_files() + self.downloader.wait_for_download() + + self.model = sherpa_onnx.OnlineRecognizer.from_transducer( + tokens=os.path.join(MODEL_CACHE_DIR, "sherpa-onnx/tokens.txt"), + encoder=os.path.join(MODEL_CACHE_DIR, "sherpa-onnx/encoder.onnx"), + decoder=os.path.join(MODEL_CACHE_DIR, "sherpa-onnx/decoder.onnx"), + joiner=os.path.join(MODEL_CACHE_DIR, "sherpa-onnx/joiner.onnx"), + num_threads=2, + sample_rate=16000, + feature_dim=80, + enable_endpoint_detection=True, + rule1_min_trailing_silence=2.4, + rule2_min_trailing_silence=1.2, + rule3_min_utterance_length=300, + decoding_method="greedy_search", + provider="cpu", + ) + + def __download_models(self, path: str) -> None: + try: + file_name = os.path.basename(path) + ModelDownloader.download_from_url(self.model_files[file_name], path) + except Exception as e: + logger.error(f"Failed to download {path}: {e}") diff --git a/frigate/data_processing/common/face/model.py b/frigate/data_processing/common/face/model.py new file mode 100644 index 0000000..87293f7 --- /dev/null +++ b/frigate/data_processing/common/face/model.py @@ -0,0 +1,436 @@ +import logging +import os +import queue +import threading +from abc import ABC, abstractmethod + +import cv2 +import numpy as np +from scipy import stats + +from frigate.config import FrigateConfig +from frigate.const import FACE_DIR, MODEL_CACHE_DIR +from frigate.embeddings.onnx.face_embedding import ArcfaceEmbedding, FaceNetEmbedding +from frigate.log import redirect_output_to_logger + +logger = logging.getLogger(__name__) + + +class FaceRecognizer(ABC): + """Face recognition runner.""" + + def __init__(self, config: FrigateConfig) -> None: + self.config = config + self.landmark_detector: cv2.face.Facemark | None = None + self.init_landmark_detector() + + @abstractmethod + def build(self) -> None: + """Build face recognition model.""" + pass + + @abstractmethod + def clear(self) -> None: + """Clear current built model.""" + pass + + @abstractmethod + def classify(self, face_image: np.ndarray) -> tuple[str, float] | None: + pass + + @redirect_output_to_logger(logger, logging.DEBUG) # type: ignore[misc] + def init_landmark_detector(self) -> None: + landmark_model = os.path.join(MODEL_CACHE_DIR, "facedet/landmarkdet.yaml") + + if os.path.exists(landmark_model): + landmark_detector = cv2.face.createFacemarkLBF() + landmark_detector.loadModel(landmark_model) + self.landmark_detector = landmark_detector + + def align_face( + self, + image: np.ndarray, + output_width: int, + output_height: int, + ) -> np.ndarray: + if not self.landmark_detector: + raise ValueError("Landmark detector not initialized") + + # landmark is run on grayscale images + if image.ndim == 3: + land_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + else: + land_image = image + + _, lands = self.landmark_detector.fit( + land_image, np.array([(0, 0, land_image.shape[1], land_image.shape[0])]) + ) + landmarks: np.ndarray = lands[0][0] + + # get landmarks for eyes + leftEyePts = landmarks[42:48] + rightEyePts = landmarks[36:42] + + # compute the center of mass for each eye + leftEyeCenter = leftEyePts.mean(axis=0).astype("int") + rightEyeCenter = rightEyePts.mean(axis=0).astype("int") + + # compute the angle between the eye centroids + dY = rightEyeCenter[1] - leftEyeCenter[1] + dX = rightEyeCenter[0] - leftEyeCenter[0] + angle = np.degrees(np.arctan2(dY, dX)) - 180 + + # compute the desired right eye x-coordinate based on the + # desired x-coordinate of the left eye + desiredRightEyeX = 1.0 - 0.35 + + # determine the scale of the new resulting image by taking + # the ratio of the distance between eyes in the *current* + # image to the ratio of distance between eyes in the + # *desired* image + dist = np.sqrt((dX**2) + (dY**2)) + desiredDist = desiredRightEyeX - 0.35 + desiredDist *= output_width + scale = desiredDist / dist + + # compute center (x, y)-coordinates (i.e., the median point) + # between the two eyes in the input image + # grab the rotation matrix for rotating and scaling the face + eyesCenter = ( + int((leftEyeCenter[0] + rightEyeCenter[0]) // 2), + int((leftEyeCenter[1] + rightEyeCenter[1]) // 2), + ) + M = cv2.getRotationMatrix2D(eyesCenter, angle, scale) + + # update the translation component of the matrix + tX = output_width * 0.5 + tY = output_height * 0.35 + M[0, 2] += tX - eyesCenter[0] + M[1, 2] += tY - eyesCenter[1] + + # apply the affine transformation + return cv2.warpAffine( + image, M, (output_width, output_height), flags=cv2.INTER_CUBIC + ) + + def get_blur_confidence_reduction(self, input: np.ndarray) -> float: + """Calculates the reduction in confidence based on the blur of the image.""" + if not self.config.face_recognition.blur_confidence_filter: + return 0.0 + + variance = cv2.Laplacian(input, cv2.CV_64F).var() + logger.debug(f"face detected with blurriness {variance}") + + if variance < 120: # image is very blurry + return 0.06 + elif variance < 160: # image moderately blurry + return 0.04 + elif variance < 200: # image is slightly blurry + return 0.02 + elif variance < 250: # image is mostly clear + return 0.01 + else: + return 0.0 + + +def build_class_mean( + embs: list[np.ndarray], + trim: float = 0.15, + outlier_threshold: float = 0.30, + min_keep_frac: float = 0.7, + max_iters: int = 3, +) -> np.ndarray: + """Build a class-mean embedding with two-layer outlier protection. + + Layer 1 (iterative, vector-wise): drop whole embeddings whose cosine + similarity to the current class mean is below ``outlier_threshold``. + Catches mislabeled or corrupted training samples (wrong face in the + folder, full-frame screenshots, extreme crops) that per-dimension + trimming cannot detect. + + Layer 2 (per-dimension): ``scipy.stats.trim_mean`` on the retained set + to smooth per-component noise (lighting, expression, alignment jitter). + + Collections with fewer than 5 images bypass outlier rejection — too few + samples to establish a reliable class center. + """ + arr = np.stack(embs, axis=0) + + if len(arr) < 5: + return np.asarray(stats.trim_mean(arr, trim, axis=0)) + + keep = np.ones(len(arr), dtype=bool) + floor = max(5, int(np.ceil(min_keep_frac * len(arr)))) + + for _ in range(max_iters): + mean = stats.trim_mean(arr[keep], trim, axis=0) + m_norm = mean / (np.linalg.norm(mean) + 1e-9) + e_norms = arr / (np.linalg.norm(arr, axis=1, keepdims=True) + 1e-9) + cos = e_norms @ m_norm + new_keep = cos >= outlier_threshold + + if new_keep.sum() < floor: + top = np.argsort(-cos)[:floor] + new_keep = np.zeros(len(arr), dtype=bool) + new_keep[top] = True + + if np.array_equal(new_keep, keep): + break + keep = new_keep + + dropped = int((~keep).sum()) + + if dropped: + logger.debug( + f"Vector-wise outlier filter dropped {dropped}/{len(arr)} embeddings" + ) + + return np.asarray(stats.trim_mean(arr[keep], trim, axis=0)) + + +def similarity_to_confidence( + cosine_similarity: float, + median: float = 0.3, + range_width: float = 0.6, + slope_factor: float = 12, +) -> float: + """ + Default sigmoid function to map cosine similarity to confidence. + + Args: + cosine_similarity (float): The input cosine similarity. + median (float): Assumed median of cosine similarity distribution. + range_width (float): Assumed range of cosine similarity distribution (90th percentile - 10th percentile). + slope_factor (float): Adjusts the steepness of the curve. + + Returns: + float: The confidence score. + """ + + # Calculate slope and bias + slope = slope_factor / range_width + bias = median + + # Calculate confidence + confidence: float = 1 / (1 + np.exp(-slope * (cosine_similarity - bias))) + return confidence + + +class FaceNetRecognizer(FaceRecognizer): + def __init__(self, config: FrigateConfig): + super().__init__(config) + self.mean_embs: dict[str, np.ndarray] = {} + self.face_embedder: FaceNetEmbedding = FaceNetEmbedding() + self.model_builder_queue: queue.Queue | None = None + + def clear(self) -> None: + self.mean_embs = {} + + def run_build_task(self) -> None: + self.model_builder_queue = queue.Queue() + + def build_model() -> None: + face_embeddings_map: dict[str, list[np.ndarray]] = {} + idx = 0 + + dir = FACE_DIR + for name in os.listdir(dir): + if name == "train": + continue + + face_folder = os.path.join(dir, name) + + if not os.path.isdir(face_folder): + continue + + face_embeddings_map[name] = [] + for image in os.listdir(face_folder): + img = cv2.imread(os.path.join(face_folder, image)) + + if img is None: + continue # type: ignore[unreachable] + + img = self.align_face(img, img.shape[1], img.shape[0]) + emb = self.face_embedder([img])[0].squeeze() + face_embeddings_map[name].append(emb) + + idx += 1 + + assert self.model_builder_queue is not None + self.model_builder_queue.put(face_embeddings_map) + + thread = threading.Thread(target=build_model, daemon=True) + thread.start() + + def build(self) -> None: + if not self.landmark_detector: + self.init_landmark_detector() + return None + + if self.model_builder_queue is not None: + try: + face_embeddings_map: dict[str, list[np.ndarray]] = ( + self.model_builder_queue.get(timeout=0.1) + ) + self.model_builder_queue = None + except queue.Empty: + return + else: + self.run_build_task() + return + + if not face_embeddings_map: + return + + for name, embs in face_embeddings_map.items(): + if embs: + self.mean_embs[name] = build_class_mean(embs) + + logger.debug("Finished building ArcFace model") + + def classify(self, face_image: np.ndarray) -> tuple[str, float] | None: + if not self.landmark_detector: + return None + + if not self.mean_embs: + self.build() + + if not self.mean_embs: + return None + + # face recognition is best run on grayscale images + + # get blur factor before aligning face + blur_reduction = self.get_blur_confidence_reduction(face_image) + + # align face and run recognition + img = self.align_face(face_image, face_image.shape[1], face_image.shape[0]) + embedding = self.face_embedder([img])[0].squeeze() + + score: float = 0 + label = "" + + for name, mean_emb in self.mean_embs.items(): + dot_product = np.dot(embedding, mean_emb) + magnitude_A = np.linalg.norm(embedding) + magnitude_B = np.linalg.norm(mean_emb) + + cosine_similarity = dot_product / (magnitude_A * magnitude_B) + confidence = similarity_to_confidence( + cosine_similarity, median=0.5, range_width=0.6 + ) + + if confidence > score: + score = confidence + label = name + + return label, max(0, round(score - blur_reduction, 2)) + + +class ArcFaceRecognizer(FaceRecognizer): + def __init__(self, config: FrigateConfig): + super().__init__(config) + self.mean_embs: dict[str, np.ndarray] = {} + self.face_embedder: ArcfaceEmbedding = ArcfaceEmbedding(config.face_recognition) + self.model_builder_queue: queue.Queue | None = None + + def clear(self) -> None: + self.mean_embs = {} + + def run_build_task(self) -> None: + self.model_builder_queue = queue.Queue() + + def build_model() -> None: + face_embeddings_map: dict[str, list[np.ndarray]] = {} + idx = 0 + + dir = FACE_DIR + for name in os.listdir(dir): + if name == "train": + continue + + face_folder = os.path.join(dir, name) + + if not os.path.isdir(face_folder): + continue + + face_embeddings_map[name] = [] + for image in os.listdir(face_folder): + img = cv2.imread(os.path.join(face_folder, image)) + + if img is None: + continue # type: ignore[unreachable] + + img = self.align_face(img, img.shape[1], img.shape[0]) + emb = self.face_embedder([img])[0].squeeze() # type: ignore[arg-type] + face_embeddings_map[name].append(emb) + + idx += 1 + + assert self.model_builder_queue is not None + self.model_builder_queue.put(face_embeddings_map) + + thread = threading.Thread(target=build_model, daemon=True) + thread.start() + + def build(self) -> None: + if not self.landmark_detector: + self.init_landmark_detector() + return None + + if self.model_builder_queue is not None: + try: + face_embeddings_map: dict[str, list[np.ndarray]] = ( + self.model_builder_queue.get(timeout=0.1) + ) + self.model_builder_queue = None + except queue.Empty: + return + else: + self.run_build_task() + return + + if not face_embeddings_map: + return + + for name, embs in face_embeddings_map.items(): + if embs: + self.mean_embs[name] = build_class_mean(embs) + + logger.debug("Finished building ArcFace model") + + def classify(self, face_image: np.ndarray) -> tuple[str, float] | None: + if not self.landmark_detector: + return None + + if not self.mean_embs: + self.build() + + if not self.mean_embs: + return None + + # face recognition is best run on grayscale images + + # get blur reduction before aligning face + blur_reduction = self.get_blur_confidence_reduction(face_image) + + # align face and run recognition + img = self.align_face(face_image, face_image.shape[1], face_image.shape[0]) + embedding = self.face_embedder([img])[0].squeeze() # type: ignore[arg-type] + + score: float = 0 + label = "" + + for name, mean_emb in self.mean_embs.items(): + dot_product = np.dot(embedding, mean_emb) + magnitude_A = np.linalg.norm(embedding) + magnitude_B = np.linalg.norm(mean_emb) + + cosine_similarity = dot_product / (magnitude_A * magnitude_B) + confidence = similarity_to_confidence(cosine_similarity) + + if confidence > score: + score = confidence + label = name + + return label, max(0, round(score - blur_reduction, 2)) diff --git a/frigate/data_processing/common/license_plate/mixin.py b/frigate/data_processing/common/license_plate/mixin.py new file mode 100644 index 0000000..432787e --- /dev/null +++ b/frigate/data_processing/common/license_plate/mixin.py @@ -0,0 +1,1851 @@ +"""Handle processing images for face detection and recognition.""" + +import base64 +import datetime +import json +import logging +import math +import os +import random +import re +import string +from pathlib import Path +from typing import Any + +import cv2 +import numpy as np +from pyclipper import ET_CLOSEDPOLYGON, JT_ROUND, PyclipperOffset +from rapidfuzz.distance import JaroWinkler, Levenshtein +from shapely.geometry import Polygon + +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.config.classification import LicensePlateRecognitionConfig +from frigate.const import CLIPS_DIR, MODEL_CACHE_DIR +from frigate.data_processing.common.license_plate.model import LicensePlateModelRunner +from frigate.embeddings.onnx.lpr_embedding import LPR_EMBEDDING_SIZE +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.builtin import EventsPerSecond, InferenceSpeed +from frigate.util.image import area + +from ...types import DataProcessorMetrics + +logger = logging.getLogger(__name__) + +WRITE_DEBUG_IMAGES = False + + +class LicensePlateProcessingMixin: + # Attributes expected from consuming classes (set before super().__init__) + config: FrigateConfig + metrics: DataProcessorMetrics + model_runner: LicensePlateModelRunner + lpr_config: LicensePlateRecognitionConfig + requestor: InterProcessRequestor + detected_license_plates: dict[str, dict[str, Any]] + camera_current_cars: dict[str, list[str]] + sub_label_publisher: EventMetadataPublisher + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.plate_rec_speed = InferenceSpeed(self.metrics.alpr_speed) + self.plates_rec_second = EventsPerSecond() + self.plates_rec_second.start() + self.plate_det_speed = InferenceSpeed(self.metrics.yolov9_lpr_speed) + self.plates_det_second = EventsPerSecond() + self.plates_det_second.start() + self.event_metadata_publisher = EventMetadataPublisher() + self.ctc_decoder = CTCDecoder( + character_dict_path=os.path.join( + MODEL_CACHE_DIR, "paddleocr-onnx", "ppocr_keys_v1.txt" + ) + ) + # process plates that are stationary and have no position changes for 5 seconds + self.stationary_scan_duration = 5 + + self.batch_size = 6 + + # Object config + self.lp_objects: list[str] = [] + + for obj, attributes in self.config.model.attributes_map.items(): + if "license_plate" in attributes: + self.lp_objects.append(obj) + + # Detection specific parameters + self.min_size = 8 + self.max_size = 960 + self.box_thresh = 0.6 + self.mask_thresh = 0.6 + + # matching + self.similarity_threshold = 0.8 + self.cluster_threshold = 0.85 + + def _detect(self, image: np.ndarray, debug_frame_id: int) -> list[np.ndarray]: + """ + Detect possible areas of text in the input image by first resizing and normalizing it, + running a detection model, and filtering out low-probability regions. + + Args: + image (np.ndarray): The input image in which license plates will be detected. + debug_frame_id (int): Shared id used to name debug images so all artifacts + from a single LPR pass share the same filename suffix. + + Returns: + List[np.ndarray]: A list of bounding box coordinates representing detected license plates. + """ + h, w = image.shape[:2] + + if sum([h, w]) < 64: + image = self._zero_pad(image) + + resized_image = self._resize_image(image) + normalized_image = self._normalize_image(resized_image) + + if WRITE_DEBUG_IMAGES: + cv2.imwrite( + f"debug/frames/license_plate_resized_{debug_frame_id}.jpg", + resized_image, + ) + + try: + outputs = self.model_runner.detection_model([normalized_image])[0] # type: ignore[arg-type] + except Exception as e: + logger.warning(f"Error running LPR box detection model: {e}") + return [] + + outputs = outputs[0, :, :] + + if False: + current_time = int(datetime.datetime.now().timestamp()) # type: ignore[unreachable] + cv2.imwrite( + f"debug/frames/probability_map_{current_time}.jpg", + (outputs * 255).astype(np.uint8), + ) + + boxes, _ = self._boxes_from_bitmap(outputs, outputs > self.mask_thresh, w, h) + return self._filter_polygon(boxes, (h, w)) # type: ignore[return-value,arg-type] + + def _classify( + self, images: list[np.ndarray] + ) -> tuple[list[np.ndarray], list[tuple[str, float]]] | None: + """ + Classify the orientation or category of each detected license plate. + + Args: + images (List[np.ndarray]): A list of images of detected license plates. + + Returns: + Tuple[List[np.ndarray], List[Tuple[str, float]]]: A tuple of rotated/normalized plate images + and classification results with confidence scores. + """ + num_images = len(images) + indices = np.argsort([x.shape[1] / x.shape[0] for x in images]) + + for i in range(0, num_images, self.batch_size): + norm_images = [] + for j in range(i, min(num_images, i + self.batch_size)): + norm_img = self._preprocess_classification_image(images[indices[j]]) + norm_img = norm_img[np.newaxis, :] + norm_images.append(norm_img) + + try: + outputs = self.model_runner.classification_model(norm_images) # type: ignore[arg-type] + except Exception as e: + logger.warning(f"Error running LPR classification model: {e}") + return None + + return self._process_classification_output(images, outputs) + + def _recognize( + self, camera: str, images: list[np.ndarray] + ) -> tuple[list[str], list[list[float]]]: + """ + Recognize the characters on the detected license plates using the recognition model. + + Args: + images (List[np.ndarray]): A list of images of license plates to recognize. + + Returns: + Tuple[List[str], List[List[float]]]: A tuple of recognized license plate texts and confidence scores. + """ + input_shape = [3, 48, 320] + num_images = len(images) + + for index in range(0, num_images, self.batch_size): + input_h, input_w = input_shape[1], input_shape[2] + max_wh_ratio = input_w / input_h + norm_images = [] + + # calculate the maximum aspect ratio in the current batch + for i in range(index, min(num_images, index + self.batch_size)): + h, w = images[i].shape[0:2] + max_wh_ratio = max(max_wh_ratio, w * 1.0 / h) + + # preprocess the images based on the max aspect ratio + for i in range(index, min(num_images, index + self.batch_size)): + norm_image = self._preprocess_recognition_image( + camera, images[i], max_wh_ratio + ) + norm_image = norm_image[np.newaxis, :] + norm_images.append(norm_image) + + try: + outputs = self.model_runner.recognition_model(norm_images) # type: ignore[arg-type] + except Exception as e: + logger.warning(f"Error running LPR recognition model: {e}") + return [], [] + + return self.ctc_decoder(outputs) + + def _process_license_plate( + self, camera: str, id: str, image: np.ndarray, debug_frame_id: int + ) -> tuple[list[str], list[list[float]], list[int]]: + """ + Complete pipeline for detecting, classifying, and recognizing license plates in the input image. + Combines multi-line plates into a single plate string, grouping boxes by vertical alignment and ordering top to bottom, + but only combines boxes if their average confidence scores meet the threshold and their heights are similar. + + Args: + camera (str): Camera identifier. + id (str): Event identifier. + image (np.ndarray): The input image in which to detect, classify, and recognize license plates. + debug_frame_id (int): Shared id used to name debug images so all artifacts + from a single LPR pass share the same filename suffix. + + Returns: + Tuple[List[str], List[List[float]], List[int]]: Detected license plate texts, character-level confidence scores for each plate (flattened into a single list per plate), and areas of the plates. + """ + if ( + self.model_runner.detection_model.runner is None + or self.model_runner.classification_model.runner is None + or self.model_runner.recognition_model.runner is None + ): + # we might still be downloading the models + logger.debug("Model runners not loaded") + return [], [], [] + + boxes = self._detect(image, debug_frame_id) + if len(boxes) == 0: + logger.debug(f"{camera}: No boxes found by OCR detector model") + return [], [], [] + + if len(boxes) > 0: + plate_left = np.min([np.min(box[:, 0]) for box in boxes]) + plate_right = np.max([np.max(box[:, 0]) for box in boxes]) + plate_width = plate_right - plate_left + else: + plate_width = 0 + + boxes = self._merge_nearby_boxes( + boxes, plate_width=plate_width, gap_fraction=0.1 + ) + + if WRITE_DEBUG_IMAGES: + debug_image = image.copy() + for box in boxes: + box = box.astype(int) + x_min, y_min = np.min(box[:, 0]), np.min(box[:, 1]) + x_max, y_max = np.max(box[:, 0]), np.max(box[:, 1]) + cv2.rectangle( + debug_image, + (x_min, y_min), + (x_max, y_max), + color=(0, 255, 0), + thickness=2, + ) + + cv2.imwrite( + f"debug/frames/license_plate_boxes_{debug_frame_id}.jpg", debug_image + ) + + boxes = self._sort_boxes(list(boxes)) + + # Step 1: Compute box heights and group boxes by vertical alignment and height similarity + box_info = [] + for i, box in enumerate(boxes): + y_coords = box[:, 1] + y_min, y_max = np.min(y_coords), np.max(y_coords) + height = y_max - y_min + box_info.append((y_min, y_max, height, i)) + + # Initial grouping based on y-coordinate overlap and height similarity + initial_groups = [] + current_group = [box_info[0]] + height_tolerance = 0.25 # Allow 25% difference in height for grouping + + for i in range(1, len(box_info)): + prev_y_min, prev_y_max, prev_height, _ = current_group[-1] + curr_y_min, _, curr_height, _ = box_info[i] + + # Check y-coordinate overlap + overlap_threshold = 0.1 * (prev_y_max - prev_y_min) + overlaps = curr_y_min <= prev_y_max + overlap_threshold + + # Check height similarity + height_ratio = min(prev_height, curr_height) / max(prev_height, curr_height) + height_similar = height_ratio >= (1 - height_tolerance) + + if overlaps and height_similar: + current_group.append(box_info[i]) + else: + initial_groups.append(current_group) + current_group = [box_info[i]] + initial_groups.append(current_group) + + # Step 2: Process each initial group, filter by confidence + all_license_plates = [] + all_confidences = [] + all_areas = [] + processed_indices = set() + + recognition_threshold = self.lpr_config.recognition_threshold + + for group in initial_groups: + # Sort group by y-coordinate (top to bottom) + group.sort(key=lambda x: x[0]) + group_indices = [item[3] for item in group] + + # Skip if all indices in this group have already been processed + if all(idx in processed_indices for idx in group_indices): + continue + + # Crop images for the group + group_boxes = [boxes[i] for i in group_indices] + group_plate_images = [ + self._crop_license_plate(image, box) for box in group_boxes + ] + + if WRITE_DEBUG_IMAGES: + for i, img in enumerate(group_plate_images): + cv2.imwrite( + f"debug/frames/license_plate_cropped_{debug_frame_id}_{group_indices[i] + 1}.jpg", + img, + ) + + if self.config.lpr.debug_save_plates: + logger.debug(f"{camera}: Saving plates for event {id}") + Path(os.path.join(CLIPS_DIR, f"lpr/{camera}/{id}")).mkdir( + parents=True, exist_ok=True + ) + for i, img in enumerate(group_plate_images): + cv2.imwrite( + os.path.join( + CLIPS_DIR, + f"lpr/{camera}/{id}/{debug_frame_id}_{group_indices[i] + 1}.jpg", + ), + img, + ) + + # Recognize text in each cropped image + results, confidences = self._recognize(camera, group_plate_images) + + if not results: + continue + + if not confidences: + confidences = [[0.0] for _ in results] + + # Compute average confidence for each box's recognized text + avg_confidences = [] + for conf_list in confidences: + avg_conf = sum(conf_list) / len(conf_list) if conf_list else 0.0 + avg_confidences.append(avg_conf) + + # Filter boxes based on the recognition threshold + qualifying_indices = [] + qualifying_results = [] + qualifying_confidences = [] + for i, (avg_conf, result, conf_list) in enumerate( + zip(avg_confidences, results, confidences) + ): + if avg_conf >= recognition_threshold: + qualifying_indices.append(group_indices[i]) + qualifying_results.append(result) + qualifying_confidences.append(conf_list) + + if not qualifying_results: + continue + + processed_indices.update(qualifying_indices) + + # Combine the qualifying results into a single plate string + combined_plate = " ".join(qualifying_results) + + flat_confidences = [ + conf for conf_list in qualifying_confidences for conf in conf_list + ] + + # Apply replace rules to combined_plate if configured + original_combined = combined_plate + if self.lpr_config.replace_rules: + for rule in self.lpr_config.replace_rules: + try: + pattern = getattr(rule, "pattern", "") + replacement = getattr(rule, "replacement", "") + if pattern: + combined_plate = re.sub( + pattern, replacement, combined_plate + ) + logger.debug( + f"{camera}: Processing replace rule: '{pattern}' -> '{replacement}', result: '{combined_plate}'" + ) + except re.error as e: + logger.warning( + f"{camera}: Invalid regex in replace_rules '{pattern}': {e}" + ) + + if combined_plate != original_combined: + logger.debug( + f"{camera}: All rules applied: '{original_combined}' -> '{combined_plate}'" + ) + + # Compute the combined area for qualifying boxes + qualifying_boxes = [boxes[i] for i in qualifying_indices] + qualifying_plate_images = [ + self._crop_license_plate(image, box) for box in qualifying_boxes + ] + group_areas = [ + img.shape[0] * img.shape[1] for img in qualifying_plate_images + ] + combined_area = sum(group_areas) + + all_license_plates.append(combined_plate) + all_confidences.append(flat_confidences) + all_areas.append(combined_area) + + # Step 3: Sort the combined plates + if all_license_plates: + sorted_data = sorted( + 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), + reverse=True, + ) + + if sorted_data: + plates, confs, areas_list = zip(*sorted_data) + return list(plates), list(confs), list(areas_list) + + return [], [], [] + + def _resize_image(self, image: np.ndarray) -> np.ndarray: + """ + Resize the input image while maintaining the aspect ratio, ensuring dimensions are multiples of 32. + + Args: + image (np.ndarray): The input image to resize. + + Returns: + np.ndarray: The resized image. + """ + h, w = image.shape[:2] + ratio = min(self.max_size / max(h, w), 1.0) + resize_h = max(int(round(int(h * ratio) / 32) * 32), 32) + resize_w = max(int(round(int(w * ratio) / 32) * 32), 32) + return cv2.resize(image, (resize_w, resize_h)) + + def _normalize_image(self, image: np.ndarray) -> np.ndarray: + """ + Normalize the input image by subtracting the mean and multiplying by the standard deviation. + + Args: + image (np.ndarray): The input image to normalize. + + Returns: + np.ndarray: The normalized image, transposed to match the model's expected input format. + """ + mean = np.array([123.675, 116.28, 103.53]).reshape(1, -1).astype("float64") + std = 1 / np.array([58.395, 57.12, 57.375]).reshape(1, -1).astype("float64") + + image = image.astype("float32") + cv2.subtract(image, mean, image) + cv2.multiply(image, std, image) + return image.transpose((2, 0, 1))[np.newaxis, ...] + + def _merge_nearby_boxes( + self, + boxes: list[np.ndarray], + plate_width: float, + gap_fraction: float = 0.1, + min_overlap_fraction: float = -0.2, + ) -> list[np.ndarray]: + """ + Merge bounding boxes that are likely part of the same license plate based on proximity, + with a dynamic max_gap based on the provided width of the entire license plate. + + Args: + boxes (List[np.ndarray]): List of bounding boxes with shape (n, 4, 2), where n is the number of boxes, + each box has 4 corners, and each corner has (x, y) coordinates. + plate_width (float): The width of the entire license plate in pixels, used to calculate max_gap. + gap_fraction (float): Fraction of the plate width to use as the maximum gap. + Default is 0.1 (10% of the plate width). + + Returns: + List[np.ndarray]: List of merged bounding boxes. + """ + if len(boxes) == 0: + return [] + + max_gap = plate_width * gap_fraction + min_overlap = plate_width * min_overlap_fraction + + # Sort boxes by top left x + sorted_boxes = sorted(boxes, key=lambda x: x[0][0]) + + merged_boxes = [] + current_box = sorted_boxes[0] + + for i in range(1, len(sorted_boxes)): + next_box = sorted_boxes[i] + + # Calculate the horizontal gap between the current box and the next box + current_right = np.max( + current_box[:, 0] + ) # Rightmost x-coordinate of current box + next_left = np.min(next_box[:, 0]) # Leftmost x-coordinate of next box + horizontal_gap = next_left - current_right + + # Check if the boxes are vertically aligned (similar y-coordinates) + current_top = np.min(current_box[:, 1]) + current_bottom = np.max(current_box[:, 1]) + next_top = np.min(next_box[:, 1]) + next_bottom = np.max(next_box[:, 1]) + + # Consider boxes part of the same plate if they are close horizontally or overlap + # within the allowed limit and their vertical positions overlap significantly + if min_overlap <= horizontal_gap <= max_gap and max( + current_top, next_top + ) <= min(current_bottom, next_bottom): + merged_points = np.vstack((current_box, next_box)) + new_box = np.array( + [ + [ + np.min(merged_points[:, 0]), + np.min(merged_points[:, 1]), + ], + [ + np.max(merged_points[:, 0]), + np.min(merged_points[:, 1]), + ], + [ + np.max(merged_points[:, 0]), + np.max(merged_points[:, 1]), + ], + [ + np.min(merged_points[:, 0]), + np.max(merged_points[:, 1]), + ], + ] + ) + current_box = new_box + else: + # If the boxes are not close enough or overlap too much, add the current box to the result + merged_boxes.append(current_box) + current_box = next_box + + # Add the last box + merged_boxes.append(current_box) + + return np.array(merged_boxes, dtype=np.int32) # type: ignore[return-value] + + def _boxes_from_bitmap( + self, output: np.ndarray, mask: np.ndarray, dest_width: int, dest_height: int + ) -> tuple[np.ndarray, list[float]]: + """ + Process the binary mask to extract bounding boxes and associated confidence scores. + + Args: + output (np.ndarray): Output confidence map from the model. + mask (np.ndarray): Binary mask of detected regions. + dest_width (int): Target width for scaling the box coordinates. + dest_height (int): Target height for scaling the box coordinates. + + Returns: + Tuple[np.ndarray, List[float]]: Array of bounding boxes and list of corresponding scores. + """ + + mask = (mask * 255).astype(np.uint8) + height, width = mask.shape + outs = cv2.findContours(mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) + + # handle different return values of findContours between OpenCV versions + contours = outs[0] if len(outs) == 2 else outs[1] + + boxes = [] + scores = [] + + for index in range(len(contours)): # type: ignore[arg-type] + contour = contours[index] # type: ignore[index] + + # get minimum bounding box (rotated rectangle) around the contour and the smallest side length. + points, sside = self._get_min_boxes(contour) + if sside < self.min_size: + continue + + points = np.array(points, dtype=np.float32) # type: ignore[assignment] + + score = self._box_score(output, contour) + if self.box_thresh > score: + continue + + points = self._expand_box(points) # type: ignore[assignment] + + # Get the minimum area rectangle again after expansion + points, sside = self._get_min_boxes(points.reshape(-1, 1, 2)) # type: ignore[attr-defined] + if sside < self.min_size + 2: + continue + + points = np.array(points, dtype=np.float32) # type: ignore[assignment] + + # normalize and clip box coordinates to fit within the destination image size. + points[:, 0] = np.clip( # type: ignore[call-overload] + np.round(points[:, 0] / width * dest_width), # type: ignore[call-overload] + 0, + dest_width, + ) + points[:, 1] = np.clip( # type: ignore[call-overload] + np.round(points[:, 1] / height * dest_height), # type: ignore[call-overload] + 0, + dest_height, + ) + + boxes.append(points.astype("int32")) # type: ignore[attr-defined] + scores.append(score) + + return np.array(boxes, dtype="int32"), scores + + @staticmethod + def _get_min_boxes(contour: np.ndarray) -> tuple[list[tuple[float, float]], float]: + """ + Calculate the minimum bounding box (rotated rectangle) for a given contour. + + Args: + contour (np.ndarray): The contour points of the detected shape. + + Returns: + Tuple[List[Tuple[float, float]], float]: A list of four points representing the + corners of the bounding box, and the length of the shortest side. + """ + bounding_box = cv2.minAreaRect(contour) + points = sorted(cv2.boxPoints(bounding_box), key=lambda x: x[0]) + index_1, index_4 = (0, 1) if points[1][1] > points[0][1] else (1, 0) + index_2, index_3 = (2, 3) if points[3][1] > points[2][1] else (3, 2) + box = [points[index_1], points[index_2], points[index_3], points[index_4]] + return box, min(bounding_box[1]) + + @staticmethod + def _box_score(bitmap: np.ndarray, contour: np.ndarray) -> float: + """ + Calculate the average score within the bounding box of a contour. + + Args: + bitmap (np.ndarray): The output confidence map from the model. + contour (np.ndarray): The contour of the detected shape. + + Returns: + float: The average score of the pixels inside the contour region. + """ + h, w = bitmap.shape[:2] + contour = contour.reshape(-1, 2) + x1, y1 = np.clip(contour.min(axis=0), 0, [w - 1, h - 1]) + x2, y2 = np.clip(contour.max(axis=0), 0, [w - 1, h - 1]) + mask = np.zeros((y2 - y1 + 1, x2 - x1 + 1), dtype=np.uint8) + cv2.fillPoly(mask, [contour - [x1, y1]], 1) # type: ignore[call-overload] + return cv2.mean(bitmap[y1 : y2 + 1, x1 : x2 + 1], mask)[0] + + @staticmethod + def _expand_box(points: list[tuple[float, float]]) -> np.ndarray: + """ + Expand a polygonal shape slightly by a factor determined by the area-to-perimeter ratio. + + Args: + points (List[Tuple[float, float]]): Points of the polygon to expand. + + Returns: + np.ndarray: Expanded polygon points. + """ + polygon = Polygon(points) + distance = polygon.area / polygon.length + offset = PyclipperOffset() + offset.AddPath(points, JT_ROUND, ET_CLOSEDPOLYGON) + expanded = np.array(offset.Execute(distance * 1.5)).reshape((-1, 2)) + return expanded + + def _filter_polygon( + self, points: list[np.ndarray], shape: tuple[int, int] + ) -> np.ndarray: + """ + Filter a set of polygons to include only valid ones that fit within an image shape + and meet size constraints. + + Args: + points (List[np.ndarray]): List of polygons to filter. + shape (Tuple[int, int]): Shape of the image (height, width). + + Returns: + np.ndarray: List of filtered polygons. + """ + height, width = shape + return np.array( + [ + self._clockwise_order(point) + for point in points + if self._is_valid_polygon(point, width, height) + ] + ) + + @staticmethod + def _is_valid_polygon(point: np.ndarray, width: int, height: int) -> bool: + """ + Check if a polygon is valid, meaning it fits within the image bounds + and has sides of a minimum length. + + Args: + point (np.ndarray): The polygon to validate. + width (int): Image width. + height (int): Image height. + + Returns: + bool: Whether the polygon is valid or not. + """ + return bool( + point[:, 0].min() >= 0 + and point[:, 0].max() < width + and point[:, 1].min() >= 0 + and point[:, 1].max() < height + and np.linalg.norm(point[0] - point[1]) > 3 + and np.linalg.norm(point[0] - point[3]) > 3 + ) + + @staticmethod + def _clockwise_order(pts: np.ndarray) -> np.ndarray: + """ + Arrange the points of a polygon in order: top-left, top-right, bottom-right, bottom-left. + taken from https://github.com/PyImageSearch/imutils/blob/master/imutils/perspective.py + + Args: + pts (np.ndarray): Array of points of the polygon. + + Returns: + np.ndarray: Points ordered clockwise starting from top-left. + """ + # Sort the points based on their x-coordinates + x_sorted = pts[np.argsort(pts[:, 0]), :] + + # Separate the left-most and right-most points + left_most = x_sorted[:2, :] + right_most = x_sorted[2:, :] + + # Sort the left-most coordinates by y-coordinates + left_most = left_most[np.argsort(left_most[:, 1]), :] + (tl, bl) = left_most # Top-left and bottom-left + + # Use the top-left as an anchor to calculate distances to right points + # The further point will be the bottom-right + distances = np.sqrt( + ((tl[0] - right_most[:, 0]) ** 2) + ((tl[1] - right_most[:, 1]) ** 2) + ) + + # Sort right points by distance (descending) + right_idx = np.argsort(distances)[::-1] + (br, tr) = right_most[right_idx, :] # Bottom-right and top-right + + return np.array([tl, tr, br, bl]) + + @staticmethod + def _sort_boxes(boxes: list[np.ndarray]) -> list[np.ndarray]: + """ + Sort polygons based on their position in the image. If boxes are close in vertical + position (within 5 pixels), sort them by horizontal position. + + Args: + points: detected text boxes with shape [4, 2] + + Returns: + List: sorted boxes(array) with shape [4, 2] + """ + boxes.sort(key=lambda x: (x[0][1], x[0][0])) + for i in range(len(boxes) - 1): + for j in range(i, -1, -1): + if abs(boxes[j + 1][0][1] - boxes[j][0][1]) < 5 and ( + boxes[j + 1][0][0] < boxes[j][0][0] + ): + temp = boxes[j] + boxes[j] = boxes[j + 1] + boxes[j + 1] = temp + else: + break + return boxes + + @staticmethod + def _zero_pad(image: np.ndarray) -> np.ndarray: + """ + Apply zero-padding to an image, ensuring its dimensions are at least 32x32. + The padding is added only if needed. + + Args: + image (np.ndarray): Input image. + + Returns: + np.ndarray: Zero-padded image. + """ + h, w, c = image.shape + pad = np.zeros((max(32, h), max(32, w), c), np.uint8) + pad[:h, :w, :] = image + return pad + + @staticmethod + def _preprocess_classification_image(image: np.ndarray) -> np.ndarray: + """ + Preprocess a single image for classification by resizing, normalizing, and padding. + + This method resizes the input image to a fixed height of 48 pixels while adjusting + the width dynamically up to a maximum of 192 pixels. The image is then normalized and + padded to fit the required input dimensions for classification. + + Args: + image (np.ndarray): Input image to preprocess. + + Returns: + np.ndarray: Preprocessed and padded image. + """ + # fixed height of 48, dynamic width up to 192 + input_shape = (3, 48, 192) + input_c, input_h, input_w = input_shape + + h, w = image.shape[:2] + ratio = w / h + resized_w = min(input_w, math.ceil(input_h * ratio)) + + resized_image = cv2.resize(image, (resized_w, input_h)) + + # handle single-channel images (grayscale) if needed + if input_c == 1 and resized_image.ndim == 2: + resized_image = resized_image[np.newaxis, :, :] + else: + resized_image = resized_image.transpose((2, 0, 1)) + + # normalize + resized_image = (resized_image.astype("float32") / 255.0 - 0.5) / 0.5 + + padded_image = np.zeros((input_c, input_h, input_w), dtype=np.float32) + padded_image[:, :, :resized_w] = resized_image + + return padded_image + + def _process_classification_output( + self, images: list[np.ndarray], outputs: list[np.ndarray] + ) -> tuple[list[np.ndarray], list[tuple[str, float]]]: + """ + Process the classification model output by matching labels with confidence scores. + + This method processes the outputs from the classification model and rotates images + with high confidence of being labeled "180". It ensures that results are mapped to + the original image order. + + Args: + images (List[np.ndarray]): List of input images. + outputs (List[np.ndarray]): Corresponding model outputs. + + Returns: + Tuple[List[np.ndarray], List[Tuple[str, float]]]: A tuple of processed images and + classification results (label and confidence score). + """ + labels = ["0", "180"] + results = [["", 0.0]] * len(images) + indices = np.argsort(np.array([x.shape[1] / x.shape[0] for x in images])) + + stacked_outputs = np.stack(outputs) + + stacked_outputs = [ + (labels[idx], stacked_outputs[i, idx]) + for i, idx in enumerate(stacked_outputs.argmax(axis=1)) + ] + + for i in range(0, len(images), self.batch_size): + for j in range(len(stacked_outputs)): + label, score = stacked_outputs[j] + results[indices[i + j]] = [label, score] + # make sure we have high confidence if we need to flip a box + if "180" in label and score >= 0.7: + images[indices[i + j]] = cv2.rotate( + images[indices[i + j]], cv2.ROTATE_180 + ) + + return images, results # type: ignore[return-value] + + def _preprocess_recognition_image( + self, camera: str, image: np.ndarray, max_wh_ratio: float + ) -> np.ndarray: + """ + Preprocess an image for recognition by dynamically adjusting its width. + + This method adjusts the width of the image based on the maximum width-to-height ratio + while keeping the height fixed at 48 pixels. The image is then normalized and padded + to fit the required input dimensions for recognition. + + Args: + image (np.ndarray): Input image to preprocess. + max_wh_ratio (float): Maximum width-to-height ratio for resizing. + + Returns: + np.ndarray: Preprocessed and padded image. + """ + # fixed height of 48, dynamic width based on ratio + input_shape = [3, 48, 320] + input_h, input_w = input_shape[1], input_shape[2] + + assert image.shape[2] == input_shape[0], "Unexpected number of image channels." + + # convert to grayscale + if image.shape[2] == 3: + gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + else: + gray = image + + if self.config.cameras[camera].lpr.enhancement > 3: + # denoise using a configurable pixel neighborhood value + logger.debug( + f"{camera}: Denoising recognition image (level: {self.config.cameras[camera].lpr.enhancement})" + ) + smoothed = cv2.bilateralFilter( + gray, + d=5 + self.config.cameras[camera].lpr.enhancement, + sigmaColor=10 * self.config.cameras[camera].lpr.enhancement, + sigmaSpace=10 * self.config.cameras[camera].lpr.enhancement, + ) + sharpening_kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]) + processed = cv2.filter2D(smoothed, -1, sharpening_kernel) + else: + processed = gray + + if self.config.cameras[camera].lpr.enhancement > 0: + # always apply the same CLAHE for contrast enhancement when enhancement level is above 3 + logger.debug( + f"{camera}: Enhancing contrast for recognition image (level: {self.config.cameras[camera].lpr.enhancement})" + ) + grid_size = ( + max(4, input_w // 40), + max(4, input_h // 40), + ) + clahe = cv2.createCLAHE( + clipLimit=2 if self.config.cameras[camera].lpr.enhancement > 5 else 1.5, + tileGridSize=grid_size, + ) + enhanced = clahe.apply(processed) + else: + enhanced = processed + + # Convert back to 3-channel for model compatibility + image = cv2.cvtColor(enhanced, cv2.COLOR_GRAY2RGB) + + # dynamically adjust input width based on max_wh_ratio + input_w = int(input_h * max_wh_ratio) + + # check for model-specific input width + model_input_w = self.model_runner.recognition_model.runner.get_input_width() # type: ignore[union-attr] + if isinstance(model_input_w, int) and model_input_w > 0: + input_w = model_input_w + + h, w = image.shape[:2] + aspect_ratio = w / h + resized_w = min(input_w, math.ceil(input_h * aspect_ratio)) + + resized_image = cv2.resize(image, (resized_w, input_h)) + resized_image = resized_image.transpose((2, 0, 1)) + resized_image = (resized_image.astype("float32") / 255.0 - 0.5) / 0.5 + + # Compute mean pixel value of the resized image (per channel) + mean_pixel = np.mean(resized_image, axis=(1, 2), keepdims=True) + padded_image = np.full( + (input_shape[0], input_h, input_w), mean_pixel, dtype=np.float32 + ) + padded_image[:, :, :resized_w] = resized_image + + if False: + current_time = int(datetime.datetime.now().timestamp() * 1000) # type: ignore[unreachable] + cv2.imwrite( + f"debug/frames/preprocessed_recognition_{current_time}.jpg", + image, + ) + + return padded_image + + @staticmethod + def _crop_license_plate(image: np.ndarray, points: np.ndarray) -> np.ndarray: + """ + Crop the license plate from the image using four corner points. + + This method crops the region containing the license plate by using the perspective + transformation based on four corner points. If the resulting image is significantly + taller than wide, the image is rotated to the correct orientation. + + Args: + image (np.ndarray): Input image containing the license plate. + points (np.ndarray): Four corner points defining the plate's position. + + Returns: + np.ndarray: Cropped and potentially rotated license plate image. + """ + assert len(points) == 4, "shape of points must be 4*2" + points = points.astype(np.float32) + crop_width = int( + max( + np.linalg.norm(points[0] - points[1]), + np.linalg.norm(points[2] - points[3]), + ) + ) + crop_height = int( + max( + np.linalg.norm(points[0] - points[3]), + np.linalg.norm(points[1] - points[2]), + ) + ) + pts_std = np.array( + [[0, 0], [crop_width, 0], [crop_width, crop_height], [0, crop_height]], + dtype=np.float32, + ) + matrix = cv2.getPerspectiveTransform(points, pts_std) + image = cv2.warpPerspective( + image, + matrix, + (crop_width, crop_height), + borderMode=cv2.BORDER_REPLICATE, + flags=cv2.INTER_CUBIC, + ) + height, width = image.shape[0:2] + if height * 1.0 / width >= 1.5: + image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) + return image + + def _detect_license_plate( + self, camera: str, input: np.ndarray + ) -> tuple[int, int, int, int] | None: + """ + Use a lightweight YOLOv9 model to detect license plates for users without Frigate+ + + Return the dimensions of the detected plate as [x1, y1, x2, y2]. + """ + try: + predictions = self.model_runner.yolov9_detection_model(input) # type: ignore[arg-type] + except Exception as e: + logger.warning(f"Error running YOLOv9 license plate detection model: {e}") + return None + + confidence_threshold = self.lpr_config.detection_threshold + + top_score = -1 + top_box = None + + img_h, img_w = input.shape[0], input.shape[1] + + # Calculate resized dimensions and padding based on _preprocess_inputs + if img_w > img_h: + resized_h = int(((img_h / img_w) * LPR_EMBEDDING_SIZE) // 4 * 4) + resized_w = LPR_EMBEDDING_SIZE + x_offset = (LPR_EMBEDDING_SIZE - resized_w) // 2 + y_offset = (LPR_EMBEDDING_SIZE - resized_h) // 2 + scale_x = img_w / resized_w + scale_y = img_h / resized_h + else: + resized_w = int(((img_w / img_h) * LPR_EMBEDDING_SIZE) // 4 * 4) + resized_h = LPR_EMBEDDING_SIZE + x_offset = (LPR_EMBEDDING_SIZE - resized_w) // 2 + y_offset = (LPR_EMBEDDING_SIZE - resized_h) // 2 + scale_x = img_w / resized_w + scale_y = img_h / resized_h + + # Loop over predictions + for prediction in predictions: + score = prediction[6] + if score >= confidence_threshold: + bbox = prediction[1:5] + # Adjust for padding and scale to original image + bbox[0] = (bbox[0] - x_offset) * scale_x + bbox[1] = (bbox[1] - y_offset) * scale_y + bbox[2] = (bbox[2] - x_offset) * scale_x + bbox[3] = (bbox[3] - y_offset) * scale_y + + if score > top_score: + top_score = score + top_box = bbox + + # Return the top scoring bounding box if found + if top_box is not None: + # expand box by 5% to help with OCR + expansion = (top_box[2:] - top_box[:2]) * 0.05 + + # Expand box + expanded_box = np.array( + [ + top_box[0] - expansion[0], # x1 + top_box[1] - expansion[1], # y1 + top_box[2] + expansion[0], # x2 + top_box[3] + expansion[1], # y2 + ] + ).clip(0, [input.shape[1], input.shape[0]] * 2) + + return tuple(int(x) for x in expanded_box) # type: ignore[return-value] + else: + return None # No detection above the threshold + + def _get_cluster_rep( + self, plates: list[dict] + ) -> tuple[str, float, list[float], int]: + """ + Cluster plate variants and select the representative from the best cluster. + """ + if len(plates) == 0: + return "", 0.0, [], 0 + + if len(plates) == 1: + p = plates[0] + return p["plate"], p["conf"], p["char_confidences"], p["area"] + + # Log initial variants + logger.debug(f"Clustering {len(plates)} plate variants:") + for i, p in enumerate(plates): + logger.debug( + f" Variant {i + 1}: '{p['plate']}' (conf: {p['conf']:.3f}, area: {p['area']})" + ) + + clusters: list[list[dict[str, Any]]] = [] + for i, plate in enumerate(plates): + merged = False + for j, cluster in enumerate(clusters): + sims = [ + JaroWinkler.similarity(plate["plate"], v["plate"]) for v in cluster + ] + if len(sims) > 0: + avg_sim = sum(sims) / len(sims) + if avg_sim >= self.cluster_threshold: + cluster.append(plate) + logger.debug( + f" Merged variant {i + 1} '{plate['plate']}' (conf: {plate['conf']:.3f}) into cluster {j + 1} (avg_sim: {avg_sim:.3f})" + ) + merged = True + break + if not merged: + clusters.append([plate]) + logger.debug( + f" Started new cluster {len(clusters)} with variant {i + 1} '{plate['plate']}' (conf: {plate['conf']:.3f})" + ) + + if not clusters: + return "", 0.0, [], 0 + + # Log cluster summaries + for j, cluster in enumerate(clusters): + cluster_size = len(cluster) + max_conf = max(v["conf"] for v in cluster) + sample_variants = [v["plate"] for v in cluster[:3]] # First 3 for brevity + logger.debug( + f" Cluster {j + 1}: size {cluster_size}, max conf {max_conf:.3f}, variants: {sample_variants}{'...' if cluster_size > 3 else ''}" + ) + + # Best cluster: largest size, tiebroken by max conf + def cluster_score(c: list[dict[str, Any]]) -> tuple[int, float]: + return (len(c), max(v["conf"] for v in c)) + + best_cluster_idx = max( + range(len(clusters)), key=lambda j: cluster_score(clusters[j]) + ) + best_cluster = clusters[best_cluster_idx] + best_size, best_max_conf = cluster_score(best_cluster) + logger.debug( + f" Selected best cluster {best_cluster_idx + 1}: size {best_size}, max conf {best_max_conf:.3f}" + ) + + # Rep: highest conf in best cluster + rep = max(best_cluster, key=lambda v: v["conf"]) + logger.debug( + f" Selected rep from best cluster: '{rep['plate']}' (conf: {rep['conf']:.3f})" + ) + logger.debug( + f" Final clustered plate: '{rep['plate']}' (conf: {rep['conf']:.3f})" + ) + + return rep["plate"], rep["conf"], rep["char_confidences"], rep["area"] + + def _generate_plate_event(self, camera: str, plate: str, plate_score: float) -> str: + """Generate a unique ID for a plate event based on camera and text.""" + now = datetime.datetime.now().timestamp() + rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + event_id = f"{now}-{rand_id}" + + self.event_metadata_publisher.publish( + ( + now, + camera, + "license_plate", + event_id, + True, + plate_score, + None, + plate, + ), + EventMetadataTypeEnum.lpr_event_create.value, + ) + return event_id + + def lpr_process( + self, obj_data: dict[str, Any], frame: np.ndarray, dedicated_lpr: bool = False + ) -> None: + """Look for license plates in image.""" + self.metrics.alpr_pps.value = self.plates_rec_second.eps() + self.metrics.yolov9_lpr_pps.value = self.plates_det_second.eps() + camera = obj_data if dedicated_lpr else obj_data["camera"] + current_time = int(datetime.datetime.now().timestamp()) + debug_frame_id = int(datetime.datetime.now().timestamp() * 1000) + + if not self.config.cameras[camera].lpr.enabled: + return + + # dedicated LPR cam without frigate+ + if dedicated_lpr: + id = "dedicated-lpr" + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + + # apply motion mask + rgb[self.config.cameras[camera].motion.rasterized_mask == 0] = [0, 0, 0] # type: ignore[attr-defined] + + if WRITE_DEBUG_IMAGES: + cv2.imwrite( + f"debug/frames/dedicated_lpr_masked_{debug_frame_id}.jpg", + rgb, + ) + + yolov9_start = datetime.datetime.now().timestamp() + license_plate = self._detect_license_plate(camera, rgb) + + logger.debug( + f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms" + ) + self.plates_det_second.update() + self.plate_det_speed.update( + datetime.datetime.now().timestamp() - yolov9_start + ) + + if not license_plate: + logger.debug(f"{camera}: Detected no license plates in full frame.") + return + + license_plate_area = (license_plate[2] - license_plate[0]) * ( + license_plate[3] - license_plate[1] + ) + if license_plate_area < self.config.cameras[camera].lpr.min_area: + logger.debug(f"{camera}: License plate area below minimum threshold.") + return + + plate_box = license_plate + + license_plate_frame = rgb[ + license_plate[1] : license_plate[3], + license_plate[0] : license_plate[2], + ] + + # Double the size for better OCR + license_plate_frame = cv2.resize( + license_plate_frame, + ( + int(2 * license_plate_frame.shape[1]), + int(2 * license_plate_frame.shape[0]), + ), + ) + + else: + id = obj_data["id"] + + # don't run for non car/motorcycle or non license plate (dedicated lpr with frigate+) objects + if ( + obj_data.get("label") not in self.lp_objects + and obj_data.get("label") != "license_plate" + ): + logger.debug( + f"{camera}: Not a processing license plate for non car/motorcycle object." + ) + return + + # don't run for non-stationary objects with no position changes to avoid processing uncertain moving objects + # zero position_changes is the initial state after registering a new tracked object + # LPR will run 2 frames after detect.min_initialized is reached + if obj_data.get("position_changes", 0) == 0 and not obj_data.get( + "stationary", False + ): + logger.debug( + f"{camera}: Skipping LPR for non-stationary {obj_data['label']} object {id} with no position changes. (Detected in {self.config.cameras[camera].detect.min_initialized + 1} concurrent frames, threshold to run is {self.config.cameras[camera].detect.min_initialized + 2} frames)" # type: ignore[operator] + ) + return + + # run for stationary objects for a limited time after they become stationary + if obj_data.get("stationary") == True: + threshold = self.config.cameras[camera].detect.stationary.threshold + if obj_data.get("motionless_count", 0) >= threshold: + frames_since_stationary = ( + obj_data.get("motionless_count", 0) - threshold + ) + fps = self.config.cameras[camera].detect.fps + time_since_stationary = frames_since_stationary / fps + + # only print this log for a short time to avoid log spam + if ( + self.stationary_scan_duration + < time_since_stationary + <= self.stationary_scan_duration + 1 + ): + logger.debug( + f"{camera}: {obj_data.get('label', 'An')} object {id} has been stationary for > {self.stationary_scan_duration} seconds, skipping LPR." + ) + + if time_since_stationary > self.stationary_scan_duration: + return + + license_plate = None + + if "license_plate" not in self.config.cameras[camera].objects.track: + logger.debug(f"{camera}: Running manual license_plate detection.") + + car_box = obj_data.get("box") + + if not car_box: + return + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + + # apply motion mask + rgb[self.config.cameras[camera].motion.rasterized_mask == 0] = [0, 0, 0] # type: ignore[attr-defined] + + left, top, right, bottom = car_box + car = rgb[top:bottom, left:right] + + # double the size of the car for better box detection + car = cv2.resize(car, (int(2 * car.shape[1]), int(2 * car.shape[0]))) + + if WRITE_DEBUG_IMAGES: + cv2.imwrite( + f"debug/frames/car_frame_{debug_frame_id}.jpg", + car, + ) + + yolov9_start = datetime.datetime.now().timestamp() + license_plate = self._detect_license_plate(camera, car) + logger.debug( + f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms" + ) + self.plates_det_second.update() + self.plate_det_speed.update( + datetime.datetime.now().timestamp() - yolov9_start + ) + + if not license_plate: + logger.debug( + f"{camera}: Detected no license plates for car/motorcycle object." + ) + return + + license_plate_area = max( + 0, + (license_plate[2] - license_plate[0]) + * (license_plate[3] - license_plate[1]), + ) + + # check that license plate is valid + # quadruple the value because we've doubled both dimensions of the car + if license_plate_area < self.config.cameras[camera].lpr.min_area * 4: + logger.debug(f"{camera}: License plate is less than min_area") + 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[1] : license_plate[3], + license_plate[0] : license_plate[2], + ] + else: + # don't run for object without attributes if this isn't dedicated lpr with frigate+ + if ( + not obj_data.get("current_attributes") + and obj_data.get("label") != "license_plate" + ): + logger.debug(f"{camera}: No attributes to parse.") + return + + if obj_data.get("label") in self.lp_objects: + attributes: list[dict[str, Any]] = obj_data.get( + "current_attributes", [] + ) + for attr in attributes: + if attr.get("label") != "license_plate": + continue + + if license_plate is None or attr.get( # type: ignore[unreachable] + "score", 0.0 + ) > license_plate.get("score", 0.0): + license_plate = attr # type: ignore[assignment] + + # no license plates detected in this frame + if not license_plate: + return + + # we are using dedicated lpr with frigate+ + if obj_data.get("label") == "license_plate": + license_plate = obj_data # type: ignore[assignment] + + license_plate_box = license_plate.get("box") # type: ignore[attr-defined] + + # check that license plate is valid + if ( + not license_plate_box + or area(license_plate_box) + < self.config.cameras[camera].lpr.min_area + ): + logger.debug( + f"{camera}: Area for license plate box {area(license_plate_box)} is less than min_area {self.config.cameras[camera].lpr.min_area}" + ) + return + + license_plate_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + + # Expand the license_plate_box by 10% + box_array = np.array(license_plate_box) + expansion = (box_array[2:] - box_array[:2]) * 0.10 + expanded_box = np.array( + [ + license_plate_box[0] - expansion[0], + license_plate_box[1] - expansion[1], + license_plate_box[2] + expansion[0], + license_plate_box[3] + expansion[1], + ] + ).clip( + 0, [license_plate_frame.shape[1], license_plate_frame.shape[0]] * 2 + ) + + plate_box = tuple(int(x) for x in expanded_box) # type: ignore[assignment] + + # Crop using the expanded box + license_plate_frame = license_plate_frame[ + int(expanded_box[1]) : int(expanded_box[3]), + int(expanded_box[0]) : int(expanded_box[2]), + ] + + # double the size of the license plate frame for better OCR + license_plate_frame = cv2.resize( + license_plate_frame, + ( + int(2 * license_plate_frame.shape[1]), + int(2 * license_plate_frame.shape[0]), + ), + ) + + if WRITE_DEBUG_IMAGES: + cv2.imwrite( + f"debug/frames/license_plate_frame_{debug_frame_id}.jpg", + license_plate_frame, + ) + + logger.debug(f"{camera}: Found license plate. Bounding box: {list(plate_box)}") + logger.debug(f"{camera}: Running plate recognition for id: {id}.") + + # run detection, returns results sorted by confidence, best first + start = datetime.datetime.now().timestamp() + license_plates, confidences, areas = self._process_license_plate( + camera, id, license_plate_frame, debug_frame_id + ) + self.plates_rec_second.update() + self.plate_rec_speed.update(datetime.datetime.now().timestamp() - start) + + if license_plates: + for plate, confidence, text_area in zip(license_plates, confidences, areas): + avg_confidence = ( + (sum(confidence) / len(confidence)) if confidence else 0 + ) + + logger.debug( + f"{camera}: Detected text: {plate} (average confidence: {avg_confidence:.2f}, area: {text_area} pixels)" + ) + else: + logger.debug(f"{camera}: No text detected") + return + + top_plate, top_char_confidences, top_area = ( + license_plates[0], + confidences[0], + areas[0], + ) + avg_confidence = ( + (sum(top_char_confidences) / len(top_char_confidences)) + if top_char_confidences + else 0 + ) + + # Check against minimum confidence threshold + if avg_confidence < self.lpr_config.recognition_threshold: + logger.debug( + f"{camera}: Average character confidence {avg_confidence} is less than recognition_threshold ({self.lpr_config.recognition_threshold})" + ) + return + + # For dedicated LPR cameras, match or assign plate ID using Jaro-Winkler distance + if ( + dedicated_lpr + and "license_plate" not in self.config.cameras[camera].objects.track + ): + plate_id = None + + for existing_id, data in self.detected_license_plates.items(): + if ( + data["camera"] == camera + and data["last_seen"] is not None + and current_time - data["last_seen"] + <= self.config.cameras[camera].lpr.expire_time + ): + similarity = JaroWinkler.similarity(data["plate"], top_plate) + if similarity >= self.similarity_threshold: + plate_id = existing_id + logger.debug( + f"{camera}: Matched plate {top_plate} to {data['plate']} (similarity: {similarity:.3f})" + ) + break + if plate_id is None: + plate_id = self._generate_plate_event(camera, top_plate, avg_confidence) + logger.debug( + f"{camera}: New plate event for dedicated LPR camera {plate_id}: {top_plate}" + ) + else: + logger.debug( + f"{camera}: Matched existing plate event for dedicated LPR camera {plate_id}: {top_plate}" + ) + self.detected_license_plates[plate_id]["last_seen"] = current_time + + id = plate_id + + is_new = id not in self.detected_license_plates + + # Collect variant + variant = { + "plate": top_plate, + "conf": avg_confidence, + "char_confidences": top_char_confidences, + "area": top_area, + "timestamp": current_time, + } + + # Initialize or append to plates + self.detected_license_plates.setdefault(id, {"plates": [], "camera": camera}) + self.detected_license_plates[id]["plates"].append(variant) + + # Prune old variants - this is probably higher than it needs to be + # since we don't detect a plate every frame + num_variants = self.config.cameras[camera].detect.fps * 5 + if len(self.detected_license_plates[id]["plates"]) > num_variants: + self.detected_license_plates[id]["plates"] = self.detected_license_plates[ + id + ]["plates"][-num_variants:] + + # Cluster and select rep + plates = self.detected_license_plates[id]["plates"] + rep_plate, rep_conf, rep_char_confs, rep_area = self._get_cluster_rep(plates) + + if rep_plate != top_plate: + logger.debug( + 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 + self.detected_license_plates[id].update( + { + "plate": rep_plate, + "char_confidences": rep_char_confs, + "area": rep_area, + "last_seen": current_time if dedicated_lpr else None, + } + ) + + if not dedicated_lpr: + self.detected_license_plates[id]["obj_data"] = obj_data + + if is_new: + if camera not in self.camera_current_cars: + self.camera_current_cars[camera] = [] + self.camera_current_cars[camera].append(id) + + # Determine subLabel based on known plates, use regex matching + # Default to the detected plate, use label name if there's a match + sub_label = None + try: + sub_label = next( + ( + label + for label, plates_list in self.lpr_config.known_plates.items() # type: ignore[union-attr] + if any( + re.match(f"^{plate}$", rep_plate) + or Levenshtein.distance(plate, rep_plate) + <= self.lpr_config.match_distance + for plate in plates_list + ) + ), + None, + ) + except re.error: + logger.error( + f"{camera}: Invalid regex in known plates configuration: {self.lpr_config.known_plates}" + ) + + # If it's a known plate, publish to sub_label + if sub_label is not None: + self.sub_label_publisher.publish( + (id, sub_label, rep_conf), EventMetadataTypeEnum.sub_label.value + ) + + # always publish to recognized_license_plate field + self.requestor.send_data( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.lpr, + "name": sub_label, + "plate": rep_plate, + "score": rep_conf, + "id": id, + "camera": camera, + "timestamp": start, + "plate_box": plate_box, + } + ), + ) + self.sub_label_publisher.publish( + (id, "recognized_license_plate", rep_plate, rep_conf), + EventMetadataTypeEnum.attribute.value, + ) + + # save the best snapshot for dedicated lpr cams not using frigate+ + if ( + dedicated_lpr + and "license_plate" not in self.config.cameras[camera].objects.track + ): + logger.debug( + f"{camera}: Writing snapshot for {id}, {rep_plate}, {current_time}" + ) + frame_bgr = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + _, encoded_img = cv2.imencode(".jpg", frame_bgr) + self.sub_label_publisher.publish( + (base64.b64encode(encoded_img.tobytes()).decode("ASCII"), id, camera), + EventMetadataTypeEnum.save_lpr_snapshot.value, + ) + + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + return None + + def lpr_expire(self, object_id: str, camera: str) -> None: + if object_id in self.detected_license_plates: + self.detected_license_plates.pop(object_id) + + if object_id in self.camera_current_cars.get(camera, []): + self.camera_current_cars[camera].remove(object_id) + + +class CTCDecoder: + """ + A decoder for interpreting the output of a CTC (Connectionist Temporal Classification) model. + + This decoder converts the model's output probabilities into readable sequences of characters + while removing duplicates and handling blank tokens. It also calculates the confidence scores + for each decoded character sequence. + """ + + def __init__(self, character_dict_path: str | None = None) -> None: + """ + Initializes the CTCDecoder. + :param character_dict_path: Path to the character dictionary file. + If None, a default (English-focused) list is used. + For Chinese models, this should point to the correct + character dictionary file provided with the model. + """ + self.characters = [] + if character_dict_path and os.path.exists(character_dict_path): + with open(character_dict_path, encoding="utf-8") as f: + self.characters = ( + ["blank"] + [line.strip() for line in f if line.strip()] + [" "] + ) + else: + self.characters = [ + "blank", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ":", + ";", + "<", + "=", + ">", + "?", + "@", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "[", + "\\", + "]", + "^", + "_", + "`", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "{", + "|", + "}", + "~", + "!", + '"', + "#", + "$", + "%", + "&", + "'", + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", + " ", + " ", + ] + + self.char_map = {i: char for i, char in enumerate(self.characters)} + + def __call__( + self, outputs: list[np.ndarray] + ) -> tuple[list[str], list[list[float]]]: + """ + Decode a batch of model outputs into character sequences and their confidence scores. + + The method takes the output probability distributions for each time step and uses + the best path decoding strategy. It then merges repeating characters and ignores + blank tokens. Confidence scores for each decoded character are also calculated. + + Args: + outputs (List[np.ndarray]): A list of model outputs, where each element is + a probability distribution for each time step. + + Returns: + Tuple[List[str], List[List[float]]]: A tuple of decoded character sequences + and confidence scores for each sequence. + """ + results = [] + confidences = [] + for output in outputs: + seq_log_probs = np.log(output + 1e-8) + best_path = np.argmax(seq_log_probs, axis=1) + + merged_path = [] + merged_probs = [] + for t, char_index in enumerate(best_path): + if char_index != 0 and (t == 0 or char_index != best_path[t - 1]): + merged_path.append(char_index) + merged_probs.append(seq_log_probs[t, char_index]) + + result = "".join(self.char_map.get(idx, "") for idx in merged_path) + results.append(result) + + confidence = np.exp(merged_probs).tolist() + confidences.append(confidence) + + return results, confidences diff --git a/frigate/data_processing/common/license_plate/model.py b/frigate/data_processing/common/license_plate/model.py new file mode 100644 index 0000000..f7121e6 --- /dev/null +++ b/frigate/data_processing/common/license_plate/model.py @@ -0,0 +1,37 @@ +from frigate.comms.inter_process import InterProcessRequestor +from frigate.embeddings.onnx.lpr_embedding import ( + LicensePlateDetector, + PaddleOCRClassification, + PaddleOCRDetection, + PaddleOCRRecognition, +) + +from ...types import DataProcessorModelRunner + + +class LicensePlateModelRunner(DataProcessorModelRunner): + def __init__( + self, + requestor: InterProcessRequestor, + device: str = "CPU", + model_size: str = "small", + ): + super().__init__(requestor, device, model_size) + self.detection_model = PaddleOCRDetection( + model_size=model_size, requestor=requestor, device=device + ) + self.classification_model = PaddleOCRClassification( + model_size=model_size, requestor=requestor, device=device + ) + self.recognition_model = PaddleOCRRecognition( + model_size=model_size, requestor=requestor, device=device + ) + self.yolov9_detection_model = LicensePlateDetector( + model_size=model_size, requestor=requestor, device=device + ) + + # Load all models once + self.detection_model._load_model_and_utils() + self.classification_model._load_model_and_utils() + self.recognition_model._load_model_and_utils() + self.yolov9_detection_model._load_model_and_utils() diff --git a/frigate/data_processing/post/api.py b/frigate/data_processing/post/api.py new file mode 100644 index 0000000..044e5d2 --- /dev/null +++ b/frigate/data_processing/post/api.py @@ -0,0 +1,65 @@ +"""Local or remote processors to handle post processing.""" + +import logging +from abc import ABC, abstractmethod +from typing import Any + +from frigate.config import FrigateConfig + +from ..types import DataProcessorMetrics, DataProcessorModelRunner, PostProcessDataEnum + +logger = logging.getLogger(__name__) + + +class PostProcessorApi(ABC): + @abstractmethod + def __init__( + self, + config: FrigateConfig, + metrics: DataProcessorMetrics, + model_runner: DataProcessorModelRunner | None, + ) -> None: + self.config = config + self.metrics = metrics + self.model_runner = model_runner + pass + + @abstractmethod + def process_data( + self, data: dict[str, Any], data_type: PostProcessDataEnum + ) -> None: + """Processes the data of data type. + Args: + data (dict): containing data about the input. + data_type (enum): Describing the data that is being processed. + + Returns: + None. + """ + pass + + @abstractmethod + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | str | None: + """Handle metadata requests. + Args: + request_data (dict): containing data about requested change to process. + + Returns: + None if request was not handled, otherwise return response. + """ + 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 diff --git a/frigate/data_processing/post/audio_transcription.py b/frigate/data_processing/post/audio_transcription.py new file mode 100644 index 0000000..16895dc --- /dev/null +++ b/frigate/data_processing/post/audio_transcription.py @@ -0,0 +1,220 @@ +"""Handle post-processing for audio transcription.""" + +import logging +import os +import threading +import time +from typing import Any + +from peewee import DoesNotExist + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import ( + CACHE_DIR, + MODEL_CACHE_DIR, + UPDATE_AUDIO_TRANSCRIPTION_STATE, + UPDATE_EVENT_DESCRIPTION, +) +from frigate.data_processing.types import PostProcessDataEnum +from frigate.embeddings.embeddings import Embeddings +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.audio import get_audio_from_recording + +from ..types import DataProcessorMetrics +from .api import PostProcessorApi + +logger = logging.getLogger(__name__) + + +class AudioTranscriptionPostProcessor(PostProcessorApi): + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + embeddings: Embeddings, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics, None) + self.config = config + self.requestor = requestor + self.embeddings = embeddings + self.recognizer = None + self.transcription_lock = threading.Lock() + self.transcription_thread: threading.Thread | None = None + self.transcription_running = False + + # faster-whisper handles model downloading automatically + self.model_path = os.path.join(MODEL_CACHE_DIR, "whisper") + os.makedirs(self.model_path, exist_ok=True) + + self.__build_recognizer() + + def __build_recognizer(self) -> None: + try: + # Import dynamically to avoid crashes on systems without AVX support + from faster_whisper import WhisperModel + + self.recognizer = WhisperModel( + model_size_or_path="small", + device="cuda" + if self.config.audio_transcription.device == "GPU" + else "cpu", + download_root=self.model_path, + local_files_only=False, # Allow downloading if not cached + compute_type="int8", + ) + logger.debug("Audio transcription (recordings) initialized") + except Exception as e: + logger.error(f"Failed to initialize recordings audio transcription: {e}") + self.recognizer = None + + def process_data( + self, data: dict[str, Any], data_type: PostProcessDataEnum + ) -> None: + """Transcribe audio from a recording. + + Args: + data (dict): Contains data about the input (event_id, camera, etc.). + data_type (enum): Describes the data being processed (recording or tracked_object). + + Returns: + None + """ + event_id = data["event_id"] + camera_name = data["camera"] + + if data_type == PostProcessDataEnum.recording: + start_ts = data["frame_time"] + recordings_available_through = data["recordings_available"] + end_ts = min(recordings_available_through, start_ts + 60) # Default 60s + + elif data_type == PostProcessDataEnum.tracked_object: + obj_data = data["event"]["data"] + obj_data["id"] = data["event"]["id"] + obj_data["camera"] = data["event"]["camera"] + start_ts = data["event"]["start_time"] + end_ts = data["event"].get( + "end_time", start_ts + 60 + ) # Use end_time if available + + else: + logger.error("No data type passed to audio transcription post-processing") + return + + try: + audio_data = get_audio_from_recording( + self.config.cameras[camera_name].ffmpeg, + camera_name, + start_ts, + end_ts, + sample_rate=16000, + ) + + if not audio_data: + logger.debug(f"No audio data extracted for {event_id}") + return + + transcription = self.__transcribe_audio(audio_data) + if not transcription: + logger.debug("No transcription generated from audio") + return + + logger.debug(f"Transcribed audio for {event_id}: '{transcription}'") + + self.requestor.send_data( + UPDATE_EVENT_DESCRIPTION, + { + "type": TrackedObjectUpdateTypesEnum.description, + "id": event_id, + "description": transcription, + "camera": camera_name, + }, + ) + + # Embed the description if semantic search is enabled + if self.config.semantic_search.enabled: + self.embeddings.embed_description(event_id, transcription) + + except DoesNotExist: + logger.debug("No recording found for audio transcription post-processing") + return + except Exception as e: + logger.error(f"Error in audio transcription post-processing: {e}") + + def __transcribe_audio(self, audio_data: bytes) -> str | None: + """Transcribe WAV audio data using faster-whisper.""" + if not self.recognizer: + logger.debug("Recognizer not initialized") + return None + + try: # type: ignore[unreachable] + # Save audio data to a temporary wav (faster-whisper expects a file) + temp_wav = os.path.join(CACHE_DIR, f"temp_audio_{int(time.time())}.wav") + with open(temp_wav, "wb") as f: + f.write(audio_data) + + segments, info = self.recognizer.transcribe( + temp_wav, + language=self.config.audio_transcription.language, + beam_size=5, + ) + + os.remove(temp_wav) + + # Combine all segment texts + text = " ".join(segment.text.strip() for segment in segments) + if not text: + return None + + logger.debug( + "Detected language '%s' with probability %f", + info.language, + info.language_probability, + ) + + return text + except Exception as e: + logger.error(f"Error transcribing audio: {e}") + return None + + def _transcription_wrapper(self, event: dict[str, Any]) -> None: + """Wrapper to run transcription and reset running flag when done.""" + try: + self.process_data( + { + "event_id": event["id"], + "camera": event["camera"], + "event": event, + }, + PostProcessDataEnum.tracked_object, + ) + finally: + with self.transcription_lock: + self.transcription_running = False + self.transcription_thread = None + + self.requestor.send_data(UPDATE_AUDIO_TRANSCRIPTION_STATE, "idle") + + def handle_request(self, topic: str, request_data: dict[str, Any]) -> str | None: + if topic == "transcribe_audio": + event = request_data["event"] + + with self.transcription_lock: + if self.transcription_running: + logger.warning( + "Audio transcription for a speech event is already running." + ) + return "in_progress" + + # Mark as running and start the thread + self.transcription_running = True + self.requestor.send_data(UPDATE_AUDIO_TRANSCRIPTION_STATE, "processing") + + self.transcription_thread = threading.Thread( + target=self._transcription_wrapper, args=(event,), daemon=True + ) + self.transcription_thread.start() + return "started" + + return None diff --git a/frigate/data_processing/post/license_plate.py b/frigate/data_processing/post/license_plate.py new file mode 100644 index 0000000..55f863c --- /dev/null +++ b/frigate/data_processing/post/license_plate.py @@ -0,0 +1,244 @@ +"""Handle post processing for license plate recognition.""" + +import datetime +import logging +from typing import Any + +import cv2 +import numpy as np +from peewee import DoesNotExist + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum +from frigate.comms.event_metadata_updater import EventMetadataPublisher +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.data_processing.common.license_plate.mixin import ( + WRITE_DEBUG_IMAGES, + LicensePlateProcessingMixin, +) +from frigate.data_processing.common.license_plate.model import ( + LicensePlateModelRunner, +) +from frigate.data_processing.types import PostProcessDataEnum +from frigate.models import Recordings +from frigate.util.image import get_image_from_recording + +from ..types import DataProcessorMetrics +from .api import PostProcessorApi + +logger = logging.getLogger(__name__) + + +class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi): # type: ignore[misc] + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + sub_label_publisher: EventMetadataPublisher, + metrics: DataProcessorMetrics, + model_runner: LicensePlateModelRunner, + detected_license_plates: dict[str, dict[str, Any]], + ): + self.requestor = requestor + self.detected_license_plates = detected_license_plates + self.model_runner = model_runner + self.lpr_config = config.lpr + self.config = config + self.sub_label_publisher = sub_label_publisher + 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( + self, data: dict[str, Any], data_type: PostProcessDataEnum + ) -> None: + """Look for license plates in recording stream image + Args: + data (dict): containing data about the input. + data_type (enum): Describing the data that is being processed. + + Returns: + None. + """ + # don't run LPR post processing for now + return + + event_id = data["event_id"] # type: ignore[unreachable] + camera_name = data["camera"] + + if data_type == PostProcessDataEnum.recording: + obj_data = data["obj_data"] + frame_time = obj_data["frame_time"] + recordings_available_through = data["recordings_available"] + + if frame_time > recordings_available_through: + logger.debug( + f"LPR post processing: No recordings available for this frame time {frame_time}, available through {recordings_available_through}" + ) + + elif data_type == PostProcessDataEnum.tracked_object: + # non-functional, need to think about snapshot time + obj_data = data["event"]["data"] + obj_data["id"] = data["event"]["id"] + obj_data["camera"] = data["event"]["camera"] + # TODO: snapshot time? + frame_time = data["event"]["start_time"] + + else: + logger.error("No data type passed to LPR postprocessing") + return + + recording_query = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where( + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.desc()) + .limit(1) + ) + + try: + recording: Recordings = recording_query.get() + time_in_segment = frame_time - recording.start_time + codec = "mjpeg" + + image_data = get_image_from_recording( + self.config.ffmpeg, recording.path, time_in_segment, codec, None + ) + + if not image_data: + logger.debug( + "LPR post processing: Unable to fetch license plate from recording" + ) + + # Convert bytes to numpy array + image_array = np.frombuffer(image_data, dtype=np.uint8) + + if len(image_array) == 0: + logger.debug("LPR post processing: No image") + return + + image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + + except DoesNotExist: + logger.debug("Error fetching license plate for postprocessing") + return + + if WRITE_DEBUG_IMAGES: + cv2.imwrite( + f"debug/frames/lpr_post_{datetime.datetime.now().timestamp()}.jpg", + image, + ) + + # convert to yuv for processing + frame = cv2.cvtColor(image, cv2.COLOR_BGR2YUV_I420) + + detect_width = self.config.cameras[camera_name].detect.width + detect_height = self.config.cameras[camera_name].detect.height + + # Scale the boxes based on detect dimensions + scale_x = image.shape[1] / detect_width + scale_y = image.shape[0] / detect_height + + # Determine which box to enlarge based on detection mode + if "license_plate" not in self.config.cameras[camera_name].objects.track: + # Scale and enlarge the car box + box = obj_data.get("box") + if not box: + return + + # Scale original car box to detection dimensions + left = int(box[0] * scale_x) + top = int(box[1] * scale_y) + right = int(box[2] * scale_x) + bottom = int(box[3] * scale_y) + box = [left, top, right, bottom] + else: + # Get the license plate box from attributes + if not obj_data.get("current_attributes"): + return + + license_plate = None + for attr in obj_data["current_attributes"]: + if attr.get("label") != "license_plate": + continue + if license_plate is None or attr.get("score", 0.0) > license_plate.get( + "score", 0.0 + ): + license_plate = attr + + if not license_plate or not license_plate.get("box"): + return + + # Scale license plate box to detection dimensions + orig_box = license_plate["box"] + left = int(orig_box[0] * scale_x) + top = int(orig_box[1] * scale_y) + right = int(orig_box[2] * scale_x) + bottom = int(orig_box[3] * scale_y) + box = [left, top, right, bottom] + + width_box = right - left + height_box = bottom - top + + # Enlarge box slightly to account for drift in detect vs recording stream + enlarge_factor = 0.3 + new_left = max(0, int(left - (width_box * enlarge_factor / 2))) + new_top = max(0, int(top - (height_box * enlarge_factor / 2))) + new_right = min(image.shape[1], int(right + (width_box * enlarge_factor / 2))) + new_bottom = min( + image.shape[0], int(bottom + (height_box * enlarge_factor / 2)) + ) + + keyframe_obj_data = obj_data.copy() + if "license_plate" not in self.config.cameras[camera_name].objects.track: + # car box + keyframe_obj_data["box"] = [new_left, new_top, new_right, new_bottom] + else: + # Update the license plate box in the attributes + new_attributes = [] + for attr in obj_data["current_attributes"]: + if attr.get("label") == "license_plate": + new_attr = attr.copy() + new_attr["box"] = [new_left, new_top, new_right, new_bottom] + new_attributes.append(new_attr) + else: + new_attributes.append(attr) + keyframe_obj_data["current_attributes"] = new_attributes + + # run the frame through lpr processing + logger.debug(f"Post processing plate: {event_id}, {frame_time}") + self.lpr_process(keyframe_obj_data, frame) + + def handle_request(self, topic: str, request_data: dict) -> dict[str, Any] | None: + if topic == EmbeddingsRequestEnum.reprocess_plate.value: + event = request_data["event"] + + self.process_data( + { + "event_id": event["id"], + "camera": event["camera"], + "event": event, + }, + PostProcessDataEnum.tracked_object, + ) + + return { + "message": "Successfully requested reprocessing of license plate.", + "success": True, + } + + return None diff --git a/frigate/data_processing/post/object_descriptions.py b/frigate/data_processing/post/object_descriptions.py new file mode 100644 index 0000000..6404b38 --- /dev/null +++ b/frigate/data_processing/post/object_descriptions.py @@ -0,0 +1,381 @@ +"""Post processor for object descriptions using GenAI.""" + +import datetime +import logging +import os +import threading +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import cv2 +import numpy as np +from peewee import DoesNotExist + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import CameraConfig, FrigateConfig +from frigate.const import CLIPS_DIR, UPDATE_EVENT_DESCRIPTION +from frigate.data_processing.post.semantic_trigger import SemanticTriggerProcessor +from frigate.data_processing.types import PostProcessDataEnum +from frigate.genai.manager import GenAIClientManager +from frigate.models import Event +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.builtin import EventsPerSecond, InferenceSpeed +from frigate.util.file import get_event_thumbnail_bytes, load_event_snapshot_image +from frigate.util.image import create_thumbnail, ensure_jpeg_bytes + +if TYPE_CHECKING: + from frigate.embeddings.embeddings import Embeddings + +from ..post.api import PostProcessorApi +from ..types import DataProcessorMetrics + +logger = logging.getLogger(__name__) + +MAX_THUMBNAILS = 10 + + +class ObjectDescriptionProcessor(PostProcessorApi): + def __init__( + self, + config: FrigateConfig, + embeddings: "Embeddings", + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + genai_manager: GenAIClientManager, + semantic_trigger_processor: SemanticTriggerProcessor | None, + ): + super().__init__(config, metrics, None) + self.config = config + self.embeddings = embeddings + self.requestor = requestor + self.metrics = metrics + self.genai_manager = genai_manager + self.semantic_trigger_processor = semantic_trigger_processor + self.tracked_events: dict[str, list[Any]] = {} + self.early_request_sent: dict[str, bool] = {} + self.object_desc_speed = InferenceSpeed(self.metrics.object_desc_speed) + self.object_desc_dps = EventsPerSecond() + self.object_desc_dps.start() + + def __handle_frame_update( + self, camera: str, data: dict, yuv_frame: np.ndarray + ) -> None: + """Handle an update to a frame for an object.""" + camera_config = self.config.cameras[camera] + + # no need to save our own thumbnails if genai is not enabled + # or if the object has become stationary + if not data["stationary"]: + if data["id"] not in self.tracked_events: + self.tracked_events[data["id"]] = [] + + data["thumbnail"] = create_thumbnail(yuv_frame, data["box"]) + + # Limit the number of thumbnails saved + if len(self.tracked_events[data["id"]]) >= MAX_THUMBNAILS: + # Always keep the first thumbnail for the event + self.tracked_events[data["id"]].pop(1) + + self.tracked_events[data["id"]].append(data) + + # check if we're configured to send an early request after a minimum number of updates received + if camera_config.objects.genai.send_triggers.after_significant_updates: + if ( + len(self.tracked_events.get(data["id"], [])) + >= camera_config.objects.genai.send_triggers.after_significant_updates + and data["id"] not in self.early_request_sent + ): + if data["has_clip"] and data["has_snapshot"]: + try: + event: Event = Event.get(Event.id == data["id"]) + except DoesNotExist: + logger.error(f"Event {data['id']} not found") + return + + if ( + not camera_config.objects.genai.objects + or event.label in camera_config.objects.genai.objects + ) and ( + not camera_config.objects.genai.required_zones + or set(data["entered_zones"]) + & set(camera_config.objects.genai.required_zones) + ): + logger.debug(f"{camera} sending early request to GenAI") + + self.early_request_sent[data["id"]] = True + # Copy thumbnails to avoid holding references after cleanup + thumbnails_copy = [ + data["thumbnail"][:] if data.get("thumbnail") else None + for data in self.tracked_events[data["id"]] + if data.get("thumbnail") + ] + threading.Thread( + target=self._genai_embed_description, + name=f"_genai_embed_description_{event.id}", + daemon=True, + args=( + event, + thumbnails_copy, + ), + ).start() + + def __handle_frame_finalize( + self, camera: str, event: Event, thumbnail: bytes + ) -> None: + """Handle the finalization of a frame.""" + camera_config = self.config.cameras[camera] + + if ( + camera_config.objects.genai.enabled + and camera_config.objects.genai.send_triggers.tracked_object_end + and ( + not camera_config.objects.genai.objects + or event.label in camera_config.objects.genai.objects + ) + and ( + not camera_config.objects.genai.required_zones + or set(event.zones) & set(camera_config.objects.genai.required_zones) + ) + ): + self._process_genai_description(event, camera_config, thumbnail) + else: + self.cleanup_event(str(event.id)) + + def __regenerate_description(self, event_id: str, source: str, force: bool) -> None: + """Regenerate the description for an event.""" + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + logger.error(f"Event {event_id} not found for description regeneration") + return + + camera_config = self.config.cameras[str(event.camera)] + if not camera_config.objects.genai.enabled and not force: + logger.error(f"GenAI not enabled for camera {event.camera}") + return + + thumbnail = get_event_thumbnail_bytes(event) + + if thumbnail is None: + logger.error("No thumbnail available for %s", event.id) + return + + # ensure we have a jpeg to pass to the model + thumbnail = ensure_jpeg_bytes(thumbnail) + + logger.debug( + f"Trying {source} regeneration for {event}, has_snapshot: {event.has_snapshot}" + ) + + if event.has_snapshot and source == "snapshot": + snapshot_image = self._read_and_crop_snapshot(event) + if not snapshot_image: + return + + embed_image = ( + [snapshot_image] + if event.has_snapshot and source == "snapshot" + # Copy thumbnails to avoid holding references + else ( + [ + data["thumbnail"][:] if data.get("thumbnail") else None + for data in self.tracked_events[event_id] + if data.get("thumbnail") + ] + if len(self.tracked_events.get(event_id, [])) > 0 + else [thumbnail] + ) + ) + + self._genai_embed_description( + event, [img for img in embed_image if img is not None] + ) + + def process_data(self, frame_data: dict, data_type: PostProcessDataEnum) -> None: + """Process a frame update.""" + self.metrics.object_desc_dps.value = self.object_desc_dps.eps() + + if data_type != PostProcessDataEnum.tracked_object: + return + + if self.genai_manager.description_client is None: + return + + state: str | None = frame_data.get("state", None) + + if state is not None: + logger.debug(f"Processing {state} for {frame_data['camera']}") + + if state == "update": + self.__handle_frame_update( + frame_data["camera"], frame_data["data"], frame_data["yuv_frame"] + ) + elif state == "finalize": + self.__handle_frame_finalize( + frame_data["camera"], frame_data["event"], frame_data["thumbnail"] + ) + + def handle_request(self, topic: str, data: dict[str, Any]) -> str | None: + """Handle a request.""" + if topic == "regenerate_description": + self.__regenerate_description( + data["event_id"], data["source"], data["force"] + ) + return None + + def cleanup_event(self, event_id: str) -> None: + """Clean up tracked event data to prevent memory leaks. + + This should be called when an event ends, regardless of whether + genai processing is triggered. + """ + if event_id in self.tracked_events: + del self.tracked_events[event_id] + if event_id in self.early_request_sent: + del self.early_request_sent[event_id] + + def _read_and_crop_snapshot(self, event: Event) -> bytes | None: + """Read, decode, and crop the snapshot image.""" + + try: + img, _ = load_event_snapshot_image(event) + if img is None: + logger.error(f"Cannot load snapshot for {event.id}, file not found") + return None + + # Crop snapshot based on region + # provide full image if region doesn't exist (manual events) + height, width = img.shape[:2] + x1_rel, y1_rel, width_rel, height_rel = event.data.get( # type: ignore[attr-defined] + "region", [0, 0, 1, 1] + ) + x1, y1 = int(x1_rel * width), int(y1_rel * height) + + cropped_image = img[ + y1 : y1 + int(height_rel * height), + x1 : x1 + int(width_rel * width), + ] + + _, buffer = cv2.imencode(".jpg", cropped_image) + + return buffer.tobytes() + except Exception: + return None + + def _process_genai_description( + self, event: Event, camera_config: CameraConfig, thumbnail: bytes + ) -> None: + event_id = str(event.id) + + if event.has_snapshot and camera_config.objects.genai.use_snapshot: + snapshot_image = self._read_and_crop_snapshot(event) + + if not snapshot_image: + self.cleanup_event(event_id) + return + + num_thumbnails = len(self.tracked_events.get(event_id, [])) + + # ensure we have a jpeg to pass to the model + thumbnail = ensure_jpeg_bytes(thumbnail) + + embed_image = ( + [snapshot_image] + if event.has_snapshot and camera_config.objects.genai.use_snapshot + # Copy thumbnails to avoid holding references after cleanup + else ( + [ + data["thumbnail"][:] if data.get("thumbnail") else None + for data in self.tracked_events[event_id] + if data.get("thumbnail") + ] + if num_thumbnails > 0 + else [thumbnail] + ) + ) + + if camera_config.objects.genai.debug_save_thumbnails and num_thumbnails > 0: + logger.debug(f"Saving {num_thumbnails} thumbnails for event {event_id}") + + Path(os.path.join(CLIPS_DIR, f"genai-requests/{event_id}")).mkdir( + parents=True, exist_ok=True + ) + + for idx, data in enumerate(self.tracked_events[event_id], 1): + jpg_bytes: bytes | None = data["thumbnail"] + + if jpg_bytes is None: + logger.warning(f"Unable to save thumbnail {idx} for {event_id}.") + else: + with open( + os.path.join( + CLIPS_DIR, + f"genai-requests/{event_id}/{idx}.jpg", + ), + "wb", + ) as j: + j.write(jpg_bytes) + + # Generate the description. Call happens in a thread since it is network bound. + threading.Thread( + target=self._genai_embed_description, + name=f"_genai_embed_description_{event_id}", + daemon=True, + args=( + event, + embed_image, + ), + ).start() + + # Clean up tracked events and early request state + self.cleanup_event(event_id) + + def _genai_embed_description(self, event: Event, thumbnails: list[bytes]) -> None: + """Embed the description for an event.""" + start = datetime.datetime.now().timestamp() + camera_config = self.config.cameras[str(event.camera)] + client = self.genai_manager.description_client + + if client is None: + return + + description = client.generate_object_description( + camera_config, thumbnails, event + ) + + if not description: + logger.debug("Failed to generate description for %s", event.id) + return + + # fire and forget description update + self.requestor.send_data( + UPDATE_EVENT_DESCRIPTION, + { + "type": TrackedObjectUpdateTypesEnum.description, + "id": event.id, + "description": description, + "camera": event.camera, + }, + ) + + # Embed the description + if self.config.semantic_search.enabled: + self.embeddings.embed_description(str(event.id), description) + + # Check semantic trigger for this description + if self.semantic_trigger_processor is not None: + self.semantic_trigger_processor.process_data( + {"event_id": event.id, "camera": event.camera, "type": "text"}, + PostProcessDataEnum.tracked_object, + ) + + # Update inference timing metrics + self.object_desc_speed.update(datetime.datetime.now().timestamp() - start) + self.object_desc_dps.update() + + logger.debug( + "Generated description for %s (%d images): %s", + event.id, + len(thumbnails), + description, + ) diff --git a/frigate/data_processing/post/review_descriptions.py b/frigate/data_processing/post/review_descriptions.py new file mode 100644 index 0000000..3740ac2 --- /dev/null +++ b/frigate/data_processing/post/review_descriptions.py @@ -0,0 +1,614 @@ +"""Post processor for review items to get descriptions.""" + +import copy +import datetime +import logging +import math +import os +import shutil +import threading +from pathlib import Path +from typing import Any + +import cv2 +from peewee import DoesNotExist +from titlecase import titlecase + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.config.camera import CameraConfig +from frigate.config.camera.review import GenAIReviewConfig, ImageSourceEnum +from frigate.const import ( + ATTRIBUTE_LABEL_DISPLAY_MAP, + CACHE_DIR, + CLIPS_DIR, + UPDATE_REVIEW_DESCRIPTION, +) +from frigate.data_processing.types import PostProcessDataEnum +from frigate.genai import GenAIClient +from frigate.genai.manager import GenAIClientManager +from frigate.models import Recordings, ReviewSegment +from frigate.util.builtin import EventsPerSecond, InferenceSpeed +from frigate.util.image import get_image_from_recording + +from ..post.api import PostProcessorApi +from ..types import DataProcessorMetrics + +logger = logging.getLogger(__name__) + +RECORDING_BUFFER_EXTENSION_PERCENT = 0.10 +MIN_RECORDING_DURATION = 10 +MAX_IMAGE_TOKENS = 24000 +MAX_FRAMES_PER_SECOND = 1 + + +class ReviewDescriptionProcessor(PostProcessorApi): + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + genai_manager: GenAIClientManager, + ): + super().__init__(config, metrics, None) + self.requestor = requestor + self.metrics = metrics + self.genai_manager = genai_manager + self.review_desc_speed = InferenceSpeed(self.metrics.review_desc_speed) + self.review_desc_dps = EventsPerSecond() + self.review_desc_dps.start() + + def calculate_frame_count( + self, + camera: str, + duration: float, + image_source: ImageSourceEnum = ImageSourceEnum.preview, + height: int = 480, + ) -> int: + """Calculate optimal number of frames based on event duration, context size, + image source, and resolution. + + Per-image token cost is asked of the GenAI provider so providers that know + their model's true cost (e.g. llama.cpp can probe the loaded mmproj) can + diverge from the default ~1-token-per-1250-pixels heuristic. The frame + budget is bounded by: + - remaining context window after prompt + response reservations + - a fixed MAX_IMAGE_TOKENS ceiling + - MAX_FRAMES_PER_SECOND x duration, to avoid drowning short events in + near-duplicate frames where the model latches onto the redundant middle + and skips the start/end action + """ + client = self.genai_manager.description_client + + if client is None: + return 3 + + context_size = client.get_context_size() + camera_config = self.config.cameras[camera] + + detect_width = camera_config.detect.width + detect_height = camera_config.detect.height + + if not detect_width or not detect_height: + aspect_ratio = 16 / 9 + else: + aspect_ratio = detect_width / detect_height + + if image_source == ImageSourceEnum.recordings: + if aspect_ratio >= 1: + # Landscape or square: constrain height + width = int(height * aspect_ratio) + else: + # Portrait: constrain width + width = height + height = int(width / aspect_ratio) + else: + if aspect_ratio >= 1: + # Landscape or square: constrain height + target_height = 180 + width = int(target_height * aspect_ratio) + height = target_height + else: + # Portrait: constrain width + target_width = 180 + width = target_width + height = int(target_width / aspect_ratio) + + tokens_per_image = client.estimate_image_tokens(width, height) + prompt_tokens = 3800 + response_tokens = 300 + context_budget = context_size - prompt_tokens - response_tokens + image_token_budget = min(context_budget, MAX_IMAGE_TOKENS) + max_frames_by_tokens = int(image_token_budget / tokens_per_image) + max_frames_by_duration = int(duration * MAX_FRAMES_PER_SECOND) + max_frames = min(max_frames_by_tokens, max_frames_by_duration) + return max(max_frames, 3) + + def process_data( + self, data: dict[str, Any], data_type: PostProcessDataEnum + ) -> None: + self.metrics.review_desc_dps.value = self.review_desc_dps.eps() + + if data_type != PostProcessDataEnum.review: + return + + if self.genai_manager.description_client is None: + return + + camera = data["after"]["camera"] + camera_config = self.config.cameras[camera] + + if not camera_config.review.genai.enabled: + return + + id = data["after"]["id"] + + if data["type"] == "new" or data["type"] == "update": + return + else: + final_data = data["after"] + + if ( + final_data["severity"] == "alert" + and not camera_config.review.genai.alerts + ): + return + elif ( + final_data["severity"] == "detection" + and not camera_config.review.genai.detections + ): + return + + image_source = camera_config.review.genai.image_source + + if image_source == ImageSourceEnum.recordings: + duration = final_data["end_time"] - final_data["start_time"] + buffer_extension = min(5, duration * RECORDING_BUFFER_EXTENSION_PERCENT) + + # Ensure minimum total duration for short review items + # This provides better context for brief events + total_duration = duration + (2 * buffer_extension) + if total_duration < MIN_RECORDING_DURATION: + # Expand buffer to reach minimum duration, still respecting max of 5s per side + additional_buffer_per_side = (MIN_RECORDING_DURATION - duration) / 2 + buffer_extension = min(5, additional_buffer_per_side) + + final_data["start_time"] -= buffer_extension + final_data["end_time"] += buffer_extension + + thumbs = self.get_recording_frames( + camera, + final_data["start_time"], + final_data["end_time"], + height=480, # Use 480p for good balance between quality and token usage + ) + + if not thumbs: + # Fallback to preview frames if no recordings available + logger.warning( + f"No recording frames found for {camera}, falling back to preview frames" + ) + thumbs = self.get_preview_frames_as_bytes( + camera, + final_data["start_time"], + final_data["end_time"], + final_data["thumb_path"], + id, + camera_config.review.genai.debug_save_thumbnails, + ) + elif camera_config.review.genai.debug_save_thumbnails: + # Save debug thumbnails for recordings + Path(os.path.join(CLIPS_DIR, "genai-requests", id)).mkdir( + parents=True, exist_ok=True + ) + for idx, frame_bytes in enumerate(thumbs): + with open( + os.path.join(CLIPS_DIR, f"genai-requests/{id}/{idx}.jpg"), + "wb", + ) as f: + f.write(frame_bytes) + else: + # Use preview frames + thumbs = self.get_preview_frames_as_bytes( + camera, + final_data["start_time"], + final_data["end_time"], + final_data["thumb_path"], + id, + camera_config.review.genai.debug_save_thumbnails, + ) + + # kickoff analysis + self.review_desc_dps.update() + threading.Thread( + target=run_analysis, + args=( + self.requestor, + self.genai_manager.description_client, + self.review_desc_speed, + camera_config, + final_data, + thumbs, + camera_config.review.genai, + list(self.config.model.merged_labelmap.values()), + self.config.model.all_attributes, + ), + ).start() + + def handle_request(self, topic: str, request_data: dict[str, Any]) -> str | None: + if topic == EmbeddingsRequestEnum.summarize_review.value: + start_ts = request_data["start_ts"] + end_ts = request_data["end_ts"] + logger.debug( + f"Found GenAI Review Summary request for {start_ts} to {end_ts}" + ) + + # Query all review segments with camera and time information + segments: list[dict[str, Any]] = [ + { + "camera": r["camera"].replace("_", " ").title(), + "start_time": r["start_time"], + "end_time": r["end_time"], + "metadata": r["data"]["metadata"], + } + for r in ( + ReviewSegment.select( + ReviewSegment.camera, + ReviewSegment.start_time, + ReviewSegment.end_time, + ReviewSegment.data, + ) + .where( + (ReviewSegment.data["metadata"].is_null(False)) + & (ReviewSegment.start_time < end_ts) + & (ReviewSegment.end_time > start_ts) + ) + .order_by(ReviewSegment.start_time.asc()) + .dicts() + .iterator() + ) + ] + + if len(segments) == 0: + logger.debug("No review items with metadata found during time period") + return "No activity was found during this time period." + + # Identify primary items (important items that need review) + primary_segments = [ + seg + for seg in segments + if seg["metadata"].get("potential_threat_level", 0) > 0 + or seg["metadata"].get("other_concerns") + ] + + if not primary_segments: + return "No concerns were found during this time period." + + # Build hierarchical structure: each primary event with its contextual items + events_with_context = [] + + for primary_seg in primary_segments: + # Start building the primary event structure + primary_item = copy.deepcopy(primary_seg["metadata"]) + primary_item["camera"] = primary_seg["camera"] + primary_item["start_time"] = primary_seg["start_time"] + primary_item["end_time"] = primary_seg["end_time"] + + # Find overlapping contextual items from other cameras + primary_start = primary_seg["start_time"] + primary_end = primary_seg["end_time"] + primary_camera = primary_seg["camera"] + contextual_items = [] + seen_contextual_cameras = set() + + for seg in segments: + seg_camera = seg["camera"] + + if seg_camera == primary_camera: + continue + + if seg in primary_segments: + continue + + seg_start = seg["start_time"] + seg_end = seg["end_time"] + + if seg_start < primary_end and primary_start < seg_end: + # Avoid duplicates if same camera has multiple overlapping segments + if seg_camera not in seen_contextual_cameras: + contextual_item = copy.deepcopy(seg["metadata"]) + contextual_item["camera"] = seg_camera + contextual_item["start_time"] = seg_start + contextual_item["end_time"] = seg_end + contextual_items.append(contextual_item) + seen_contextual_cameras.add(seg_camera) + + # Add context array to primary item + primary_item["context"] = contextual_items + events_with_context.append(primary_item) + + total_context_items = sum( + len(event.get("context", [])) for event in events_with_context + ) + logger.debug( + f"Summary includes {len(events_with_context)} primary events with " + f"{total_context_items} total contextual items" + ) + + if self.config.review.genai.debug_save_thumbnails: + Path( + os.path.join(CLIPS_DIR, "genai-requests", f"{start_ts}-{end_ts}") + ).mkdir(parents=True, exist_ok=True) + + client = self.genai_manager.description_client + + if client is None: + return None + + return client.generate_review_summary( + start_ts, + end_ts, + events_with_context, + self.config.review.genai.preferred_language, + self.config.review.genai.debug_save_thumbnails, + ) + else: + return None + + def get_cache_frames( + self, + camera: str, + start_time: float, + end_time: float, + ) -> list[str]: + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{camera}-" + start_file = f"{file_start}{start_time}.webp" + end_file = f"{file_start}{end_time}.webp" + + camera_files = [ + entry.name + for entry in os.scandir(preview_dir) + if entry.name.startswith(file_start) + ] + camera_files.sort() + + all_frames: list[str] = [] + + for file in camera_files: + if file < start_file: + if len(all_frames): + all_frames[0] = os.path.join(preview_dir, file) + else: + all_frames.append(os.path.join(preview_dir, file)) + + continue + + if file > end_file: + all_frames.append(os.path.join(preview_dir, file)) + break + + all_frames.append(os.path.join(preview_dir, file)) + + frame_count = len(all_frames) + desired_frame_count = self.calculate_frame_count( + camera, duration=end_time - start_time + ) + + if frame_count <= desired_frame_count: + return all_frames + + selected_frames = [] + step_size = (frame_count - 1) / (desired_frame_count - 1) + + for i in range(desired_frame_count): + index = round(i * step_size) + selected_frames.append(all_frames[index]) + + return selected_frames + + def get_recording_frames( + self, + camera: str, + start_time: float, + end_time: float, + height: int = 480, + ) -> list[bytes]: + """Get frames from recordings at specified timestamps.""" + duration = end_time - start_time + desired_frame_count = self.calculate_frame_count( + camera, duration, ImageSourceEnum.recordings, height + ) + + # Calculate evenly spaced timestamps throughout the duration + if desired_frame_count == 1: + timestamps = [start_time + duration / 2] + else: + step = duration / (desired_frame_count - 1) + timestamps = [start_time + (i * step) for i in range(desired_frame_count)] + + def extract_frame_from_recording(ts: float) -> bytes | None: + """Extract a single frame from recording at given timestamp.""" + try: + recording = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where((ts >= Recordings.start_time) & (ts <= Recordings.end_time)) + .where(Recordings.camera == camera) + .order_by(Recordings.start_time.desc()) + .limit(1) + .get() + ) + + time_in_segment = ts - recording.start_time + return get_image_from_recording( + self.config.ffmpeg, + recording.path, + time_in_segment, + "mjpeg", + height=height, + ) + except DoesNotExist: + return None + + frames = [] + + for timestamp in timestamps: + try: + # Try to extract frame at exact timestamp + image_data = extract_frame_from_recording(timestamp) + + if not image_data: + # Try with rounded timestamp as fallback + rounded_timestamp = math.ceil(timestamp) + image_data = extract_frame_from_recording(rounded_timestamp) + + if image_data: + frames.append(image_data) + else: + logger.warning( + f"No recording found for {camera} at timestamp {timestamp}" + ) + except Exception as e: + logger.error( + f"Error extracting frame from recording for {camera} at {timestamp}: {e}" + ) + continue + + return frames + + def get_preview_frames_as_bytes( + self, + camera: str, + start_time: float, + end_time: float, + thumb_path_fallback: str, + review_id: str, + save_debug: bool, + ) -> list[bytes]: + """Get preview frames and convert them to JPEG bytes. + + Args: + camera: Camera name + start_time: Start timestamp + end_time: End timestamp + thumb_path_fallback: Fallback thumbnail path if no preview frames found + review_id: Review item ID for debug saving + save_debug: Whether to save debug thumbnails + + Returns: + List of JPEG image bytes + """ + frame_paths = self.get_cache_frames(camera, start_time, end_time) + if not frame_paths: + frame_paths = [thumb_path_fallback] + + thumbs = [] + for idx, thumb_path in enumerate(frame_paths): + thumb_data = cv2.imread(thumb_path) + + if thumb_data is None: + logger.warning( # type: ignore[unreachable] + "Could not read preview frame at %s, skipping", thumb_path + ) + continue + + ret, jpg = cv2.imencode( + ".jpg", thumb_data, [int(cv2.IMWRITE_JPEG_QUALITY), 100] + ) + if ret: + thumbs.append(jpg.tobytes()) + + if save_debug: + Path(os.path.join(CLIPS_DIR, "genai-requests", review_id)).mkdir( + parents=True, exist_ok=True + ) + shutil.copy( + thumb_path, + os.path.join(CLIPS_DIR, f"genai-requests/{review_id}/{idx}.webp"), + ) + + return thumbs + + +def run_analysis( + requestor: InterProcessRequestor, + genai_client: GenAIClient, + review_inference_speed: InferenceSpeed, + camera_config: CameraConfig, + final_data: dict[str, Any], + thumbs: list[bytes], + genai_config: GenAIReviewConfig, + labelmap_objects: list[str], + attribute_labels: list[str], +) -> None: + start = datetime.datetime.now().timestamp() + + # Format zone names using zone config friendly names if available + formatted_zones = [] + for zone_name in final_data["data"]["zones"]: + if zone_name in camera_config.zones: + formatted_zones.append( + camera_config.zones[zone_name].get_formatted_name(zone_name) + ) + + analytics_data = { + "id": final_data["id"], + "camera": camera_config.get_formatted_name(), + "zones": formatted_zones, + "start": datetime.datetime.fromtimestamp(final_data["start_time"]).strftime( + "%A, %I:%M %p" + ), + "duration": round(final_data["end_time"] - final_data["start_time"]), + } + + unified_objects = [] + + objects_list = final_data["data"]["objects"] + sub_labels_list = final_data["data"]["sub_labels"] + + for i, verified_label in enumerate(final_data["data"]["verified_objects"]): + object_type = verified_label.replace("-verified", "").replace("_", " ") + name = titlecase(sub_labels_list[i].replace("_", " ")) + unified_objects.append(f"{name} ← {object_type}") + + for label in objects_list: + if "-verified" in label: + continue + elif label in labelmap_objects: + object_type = label.replace("_", " ") + + if label in attribute_labels: + display_name = ATTRIBUTE_LABEL_DISPLAY_MAP.get(label, object_type) + unified_objects.append(f"{display_name} (delivery/service)") + else: + unified_objects.append(object_type) + + analytics_data["unified_objects"] = unified_objects + + metadata = genai_client.generate_review_description( + analytics_data, + thumbs, + genai_config.additional_concerns, + genai_config.preferred_language, + genai_config.debug_save_thumbnails, + genai_config.activity_context_prompt, + ) + review_inference_speed.update(datetime.datetime.now().timestamp() - start) + + if not metadata: + return None + + prev_data = copy.deepcopy(final_data) + final_data["data"]["metadata"] = metadata.model_dump() + requestor.send_data( + UPDATE_REVIEW_DESCRIPTION, + { + "type": "genai", + "before": {k: v for k, v in prev_data.items()}, + "after": {k: v for k, v in final_data.items()}, + }, + ) diff --git a/frigate/data_processing/post/semantic_trigger.py b/frigate/data_processing/post/semantic_trigger.py new file mode 100644 index 0000000..e2b305e --- /dev/null +++ b/frigate/data_processing/post/semantic_trigger.py @@ -0,0 +1,275 @@ +"""Post time processor to trigger actions based on similar embeddings.""" + +import datetime +import json +import logging +import os +from typing import Any + +import cv2 +import numpy as np +from peewee import DoesNotExist + +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import CONFIG_DIR +from frigate.data_processing.types import PostProcessDataEnum +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.embeddings.embeddings import Embeddings +from frigate.embeddings.util import ZScoreNormalization +from frigate.models import Event, Trigger +from frigate.util.builtin import cosine_distance +from frigate.util.file import get_event_thumbnail_bytes + +from ..post.api import PostProcessorApi +from ..types import DataProcessorMetrics + +logger = logging.getLogger(__name__) + +WRITE_DEBUG_IMAGES = False + + +class SemanticTriggerProcessor(PostProcessorApi): + def __init__( + self, + db: SqliteVecQueueDatabase, + config: FrigateConfig, + requestor: InterProcessRequestor, + sub_label_publisher: EventMetadataPublisher, + metrics: DataProcessorMetrics, + embeddings: Embeddings, + ) -> None: + super().__init__(config, metrics, None) + self.db = db + self.embeddings = embeddings + self.requestor = requestor + self.sub_label_publisher = sub_label_publisher + self.trigger_embeddings: list[np.ndarray] = [] + + self.thumb_stats = ZScoreNormalization() + self.desc_stats = ZScoreNormalization() + + # load stats from disk + try: + with open(os.path.join(CONFIG_DIR, ".search_stats.json")) as f: + data = json.loads(f.read()) + self.thumb_stats.from_dict(data["thumb_stats"]) + self.desc_stats.from_dict(data["desc_stats"]) + except FileNotFoundError: + pass + + def process_data( + self, data: dict[str, Any], data_type: PostProcessDataEnum + ) -> None: + event_id = data["event_id"] + camera = data["camera"] + process_type = data["type"] + + if self.config.cameras[camera].semantic_search.triggers is None: + return + + triggers = ( + Trigger.select( + Trigger.camera, + Trigger.name, + Trigger.data, + Trigger.type, + Trigger.embedding, + Trigger.threshold, + ) + .where(Trigger.camera == camera) + .dicts() + .iterator() + ) + + for trigger in triggers: + if ( + trigger["name"] + not in self.config.cameras[camera].semantic_search.triggers + or not self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .enabled + ): + logger.debug( + f"Trigger {trigger['name']} is disabled for camera {camera}" + ) + continue + + logger.debug( + f"Processing {trigger['type']} trigger for {event_id} on {trigger['camera']}: {trigger['name']}" + ) + + trigger_embedding = np.frombuffer(trigger["embedding"], dtype=np.float32) + + # Get embeddings based on type + thumbnail_embedding = None + description_embedding = None + + if process_type == "image": + cursor = self.db.execute_sql( + """ + SELECT thumbnail_embedding FROM vec_thumbnails WHERE id = ? + """, + [event_id], + ) + row = cursor.fetchone() if cursor else None + if row: + thumbnail_embedding = np.frombuffer(row[0], dtype=np.float32) + + if process_type == "text": + cursor = self.db.execute_sql( + """ + SELECT description_embedding FROM vec_descriptions WHERE id = ? + """, + [event_id], + ) + row = cursor.fetchone() if cursor else None + if row: + description_embedding = np.frombuffer(row[0], dtype=np.float32) + + # Skip processing if we don't have any embeddings + if thumbnail_embedding is None and description_embedding is None: + logger.debug(f"No embeddings found for {event_id}") + return + + # Determine which embedding to compare based on trigger type + if ( + trigger["type"] in ["text", "thumbnail"] + and thumbnail_embedding is not None + ): + data_embedding = thumbnail_embedding + normalized_distance = self.thumb_stats.normalize( + [cosine_distance(data_embedding, trigger_embedding)], + save_stats=False, + )[0] + elif trigger["type"] == "description" and description_embedding is not None: + data_embedding = description_embedding + normalized_distance = self.desc_stats.normalize( + [cosine_distance(data_embedding, trigger_embedding)], + save_stats=False, + )[0] + + else: + continue + + similarity = 1 - normalized_distance + + logger.debug( + f"Trigger {trigger['name']} ({trigger['data'] if trigger['type'] == 'text' or trigger['type'] == 'description' else 'image'}): " + f"normalized distance: {normalized_distance:.4f}, " + f"similarity: {similarity:.4f}, threshold: {trigger['threshold']}" + ) + + # Check if similarity meets threshold + if similarity >= trigger["threshold"]: + logger.debug( + f"Trigger {trigger['name']} activated with similarity {similarity:.4f}" + ) + + # Update the trigger's last_triggered and triggering_event_id + Trigger.update( + last_triggered=datetime.datetime.now(), triggering_event_id=event_id + ).where( + Trigger.camera == camera, Trigger.name == trigger["name"] + ).execute() + + # Always publish MQTT message + self.requestor.send_data( + "triggers", + json.dumps( + { + "name": trigger["name"], + "camera": camera, + "event_id": event_id, + "type": trigger["type"], + "score": similarity, + } + ), + ) + + friendly_name = ( + self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .friendly_name + ) + + if ( + self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .actions + ): + # handle actions for the trigger + # notifications already handled by webpush + if ( + "sub_label" + in self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .actions + ): + self.sub_label_publisher.publish( + (event_id, friendly_name, similarity), + EventMetadataTypeEnum.sub_label, + ) + if ( + "attribute" + in self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .actions + ): + self.sub_label_publisher.publish( + ( + event_id, + trigger["name"], + trigger["type"], + similarity, + ), + EventMetadataTypeEnum.attribute.value, + ) + + if WRITE_DEBUG_IMAGES: + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return + + # Skip the event if not an object + if event.data.get("type") != "object": # type: ignore[attr-defined] + return + + thumbnail_bytes = get_event_thumbnail_bytes(event) + + if thumbnail_bytes is None: + return + + nparr = np.frombuffer(thumbnail_bytes, np.uint8) + thumbnail = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + + font_scale = 0.5 + font = cv2.FONT_HERSHEY_SIMPLEX + cv2.putText( + thumbnail, + f"{similarity:.4f}", + (10, 30), + font, + fontScale=font_scale, + color=(0, 255, 0), + thickness=2, + ) + + current_time = int(datetime.datetime.now().timestamp()) + cv2.imwrite( + f"debug/frames/trigger-{event_id}_{current_time}.jpg", + thumbnail, + ) + + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | str | None: + return None + + def expire_object(self, object_id: str, camera: str) -> None: + pass diff --git a/frigate/data_processing/post/types.py b/frigate/data_processing/post/types.py new file mode 100644 index 0000000..3698e99 --- /dev/null +++ b/frigate/data_processing/post/types.py @@ -0,0 +1,45 @@ +from typing import Annotated + +from pydantic import BaseModel, ConfigDict, Field, StringConstraints + +ObservationItem = Annotated[str, StringConstraints(min_length=20, max_length=200)] + + +class ReviewMetadata(BaseModel): + model_config = ConfigDict(extra="ignore", protected_namespaces=()) + + observations: list[ObservationItem] = Field( + ..., + min_length=3, + max_length=8, + description="Enumerate the significant observations across all frames, in chronological order.", + ) + scene: str = Field( + min_length=150, + max_length=600, + description="A chronological narrative of what happens from start to finish, drawing directly from the items in observations.", + ) + title: str = Field( + max_length=80, + description="Title for the activity.", + ) + shortSummary: str = Field( + min_length=70, + max_length=140, + description="A brief summary for the activity.", + ) + confidence: float = Field( + ge=0.0, + le=1.0, + description="Confidence in the analysis as a decimal between 0.0 and 1.0, where 0.0 means no confidence and 1.0 means complete confidence. Express ONLY as a decimal.", + ) + potential_threat_level: int = Field( + ge=0, + le=2, + description="Threat level: 0 = normal, 1 = suspicious, 2 = critical threat.", + ) + other_concerns: list[str] | None = Field( + default=None, + description="Other concerns highlighted by the user that are observed.", + ) + time: str | None = Field(default=None, description="Time of activity.") diff --git a/frigate/data_processing/real_time/api.py b/frigate/data_processing/real_time/api.py new file mode 100644 index 0000000..98efb53 --- /dev/null +++ b/frigate/data_processing/real_time/api.py @@ -0,0 +1,201 @@ +"""Local only processors for handling real time object processing.""" + +import logging +import threading +from abc import ABC, abstractmethod +from collections import deque +from collections.abc import Callable +from concurrent.futures import Future +from queue import Empty, Full, Queue +from typing import Any + +import numpy as np + +from frigate.config import FrigateConfig + +from ..types import DataProcessorMetrics + +logger = logging.getLogger(__name__) + + +class RealTimeProcessorApi(ABC): + @abstractmethod + def __init__( + self, + config: FrigateConfig, + metrics: DataProcessorMetrics, + ) -> None: + self.config = config + self.metrics = metrics + pass + + @abstractmethod + def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray) -> None: + """Processes the frame with object data. + Args: + obj_data (dict): containing data about focused object in frame. + frame (ndarray): full yuv frame. + + Returns: + None. + """ + pass + + @abstractmethod + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + """Handle metadata requests. + Args: + topic (str): topic that dictates what work is requested. + request_data (dict): containing data about requested change to process. + + Returns: + None if request was not handled, otherwise return response. + """ + pass + + @abstractmethod + def expire_object(self, object_id: str, camera: str) -> None: + """Handle objects that are no longer detected. + Args: + object_id (str): id of object that is no longer detected. + camera (str): name of camera that object was detected on. + + Returns: + None. + """ + 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 + + def drain_results(self) -> list[dict[str, Any]]: + """Return pending results that need IPC side-effects. + + Deferred processors accumulate results on a worker thread. + The maintainer calls this each loop iteration to collect them + and perform publishes on the main thread. + + Synchronous processors return an empty list (default). + """ + return [] + + def shutdown(self) -> None: + """Stop any background work and release resources. + + Called when the processor is being removed or the maintainer + is shutting down. Default is a no-op for synchronous processors. + """ + pass + + +class DeferredRealtimeProcessorApi(RealTimeProcessorApi): + """Base class for processors that offload heavy work to a background thread. + + Subclasses implement: + - process_frame(): do cheap gating + crop + copy, then call _enqueue_task() + - _process_task(task): heavy work (inference, consensus) on the worker thread + - handle_request(): optionally use _enqueue_request() for sync request/response + - expire_object(): call _enqueue_task() with a control message + + The worker thread owns all processor state. No locks are needed because + only the worker mutates state. Results that need IPC are placed in + _pending_results via _emit_result(), and the maintainer drains them + each loop iteration. + """ + + def __init__( + self, + config: FrigateConfig, + metrics: DataProcessorMetrics, + max_queue: int = 8, + ) -> None: + super().__init__(config, metrics) + self._task_queue: Queue = Queue(maxsize=max_queue) + self._pending_results: deque[dict[str, Any]] = deque() + self._results_lock = threading.Lock() + self._stop_event = threading.Event() + self._worker = threading.Thread( + target=self._drain_loop, + daemon=True, + name=f"{type(self).__name__}_worker", + ) + self._worker.start() + + def _drain_loop(self) -> None: + """Worker thread main loop — drains the task queue until stopped.""" + while not self._stop_event.is_set(): + try: + task = self._task_queue.get(timeout=0.5) + except Empty: + continue + + if ( + isinstance(task, tuple) + and len(task) == 2 + and isinstance(task[1], Future) + ): + # Request/response: (callable_and_args, future) + (func, args), future = task + try: + result = func(args) + future.set_result(result) + except Exception as e: + future.set_exception(e) + else: + try: + self._process_task(task) + except Exception: + logger.exception("Error processing deferred task") + + def _enqueue_task(self, task: Any) -> bool: + """Enqueue a task for the worker. Returns False if queue is full (dropped).""" + try: + self._task_queue.put_nowait(task) + return True + except Full: + logger.debug("Deferred processor queue full, dropping task") + return False + + def _enqueue_request(self, func: Callable, args: Any, timeout: float = 10.0) -> Any: + """Enqueue a request and block until the worker returns a result.""" + future: Future = Future() + self._task_queue.put(((func, args), future), timeout=timeout) + return future.result(timeout=timeout) + + def _emit_result(self, result: dict[str, Any]) -> None: + """Called by the worker thread to stage a result for the maintainer.""" + with self._results_lock: + self._pending_results.append(result) + + def drain_results(self) -> list[dict[str, Any]]: + """Called by the maintainer on the main thread to collect pending results.""" + with self._results_lock: + results = list(self._pending_results) + self._pending_results.clear() + return results + + def shutdown(self) -> None: + """Signal the worker to stop and wait for it to finish.""" + self._stop_event.set() + self._worker.join(timeout=5.0) + + @abstractmethod + def _process_task(self, task: Any) -> None: + """Process a single task on the worker thread. + + Subclasses implement inference, consensus, training image saves here. + Call _emit_result() to stage results for the maintainer to publish. + """ + pass diff --git a/frigate/data_processing/real_time/audio_transcription.py b/frigate/data_processing/real_time/audio_transcription.py new file mode 100644 index 0000000..0e1842b --- /dev/null +++ b/frigate/data_processing/real_time/audio_transcription.py @@ -0,0 +1,279 @@ +"""Handle processing audio for speech transcription using sherpa-onnx with FFmpeg pipe.""" + +import logging +import os +import queue +import threading +from typing import Any + +import numpy as np + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import CameraConfig, FrigateConfig +from frigate.const import MODEL_CACHE_DIR +from frigate.data_processing.common.audio_transcription.model import ( + AudioTranscriptionModelRunner, +) +from frigate.data_processing.real_time.whisper_online import ( + FasterWhisperASR, + OnlineASRProcessor, +) + +from ..types import DataProcessorMetrics +from .api import RealTimeProcessorApi + +logger = logging.getLogger(__name__) + + +class AudioTranscriptionRealTimeProcessor(RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + camera_config: CameraConfig, + requestor: InterProcessRequestor, + model_runner: AudioTranscriptionModelRunner, + metrics: DataProcessorMetrics, + stop_event: threading.Event, + ): + super().__init__(config, metrics) + self.config = config + self.camera_config = camera_config + self.requestor = requestor + self.stream: Any = None + self.whisper_model: FasterWhisperASR | None = None + self.model_runner = model_runner + self.transcription_segments: list[str] = [] + self.audio_queue: queue.Queue[tuple[dict[str, Any], np.ndarray]] = queue.Queue() + self.stop_event = stop_event + + def __build_recognizer(self) -> None: + try: + if self.config.audio_transcription.model_size == "large": + # Whisper models need to be per-process and can only run one stream at a time + # TODO: try parallel: https://github.com/SYSTRAN/faster-whisper/issues/100 + logger.debug(f"Loading Whisper model for {self.camera_config.name}") + self.whisper_model = FasterWhisperASR( + modelsize="tiny", + device="cuda" + if self.config.audio_transcription.device == "GPU" + else "cpu", + lan=self.config.audio_transcription.language, + model_dir=os.path.join(MODEL_CACHE_DIR, "whisper"), + ) + self.whisper_model.use_vad() + self.stream = OnlineASRProcessor( + asr=self.whisper_model, + ) + else: + logger.debug(f"Loading sherpa stream for {self.camera_config.name}") + self.stream = self.model_runner.model.create_stream() + logger.debug( + f"Audio transcription (live) initialized for {self.camera_config.name}" + ) + except Exception as e: + logger.error( + f"Failed to initialize live streaming audio transcription: {e}" + ) + + def __process_audio_stream(self, audio_data: np.ndarray) -> tuple[str, bool] | None: + if ( + self.model_runner.model is None + and self.config.audio_transcription.model_size == "small" + ): + logger.debug("Audio transcription (live) model not initialized") + return None + + if not self.stream: + self.__build_recognizer() + + try: + if audio_data.dtype != np.float32: + audio_data = audio_data.astype(np.float32) + + if audio_data.max() > 1.0 or audio_data.min() < -1.0: + audio_data = audio_data / 32768.0 # Normalize from int16 + + rms = float(np.sqrt(np.mean(np.absolute(np.square(audio_data))))) + logger.debug(f"Audio chunk size: {audio_data.size}, RMS: {rms:.4f}") + + if self.config.audio_transcription.model_size == "large": + # large model + self.stream.insert_audio_chunk(audio_data) + output = self.stream.process_iter() + text = output[2].strip() + is_endpoint = ( + text.endswith((".", "!", "?")) + and sum(len(str(lines)) for lines in self.transcription_segments) + > 300 + ) + + if text: + self.transcription_segments.append(text) + concatenated_text = " ".join(self.transcription_segments) + logger.debug(f"Concatenated transcription: '{concatenated_text}'") + text = concatenated_text + + else: + # small model + self.stream.accept_waveform(16000, audio_data) + + while self.model_runner.model.is_ready(self.stream): + self.model_runner.model.decode_stream(self.stream) + + text = self.model_runner.model.get_result(self.stream).strip() + is_endpoint = self.model_runner.model.is_endpoint(self.stream) + + logger.debug(f"Transcription result: '{text}'") + + if not text: + logger.debug("No transcription, returning") + return None + + logger.debug(f"Endpoint detected: {is_endpoint}") + + if is_endpoint and self.config.audio_transcription.model_size == "small": + # reset sherpa if we've reached an endpoint + self.model_runner.model.reset(self.stream) + + return text, is_endpoint + except Exception as e: + logger.error(f"Error processing audio stream: {e}") + return None + + def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray) -> None: + pass + + def process_audio(self, obj_data: dict[str, Any], audio: np.ndarray) -> bool | None: + if audio is None or audio.size == 0: + logger.debug("No audio data provided for transcription") + return None + + # enqueue audio data for processing in the thread + self.audio_queue.put((obj_data, audio)) + return None + + def run(self) -> None: + """Run method for the transcription thread to process queued audio data.""" + logger.debug( + f"Starting audio transcription thread for {self.camera_config.name}" + ) + + # start with an empty transcription + self.requestor.send_data( + f"{self.camera_config.name}/audio/transcription", + "", + ) + + while not self.stop_event.is_set(): + try: + # Get audio data from queue with a timeout to check stop_event + _, audio = self.audio_queue.get(timeout=0.1) + result = self.__process_audio_stream(audio) + + if not result: + continue + + text, is_endpoint = result + logger.debug(f"Transcribed audio: '{text}', Endpoint: {is_endpoint}") + + self.requestor.send_data( + f"{self.camera_config.name}/audio/transcription", text + ) + + self.audio_queue.task_done() + + if is_endpoint: + self.reset() + + except queue.Empty: + continue + except Exception as e: + logger.error(f"Error processing audio in thread: {e}") + self.audio_queue.task_done() + + logger.debug( + f"Stopping audio transcription thread for {self.camera_config.name}" + ) + + def clear_audio_queue(self) -> None: + # Clear the audio queue + while not self.audio_queue.empty(): + try: + self.audio_queue.get_nowait() + self.audio_queue.task_done() + except queue.Empty: + break + + def reset(self) -> None: + if self.config.audio_transcription.model_size == "large": + # get final output from whisper + output = self.stream.finish() + self.transcription_segments = [] + + self.requestor.send_data( + f"{self.camera_config.name}/audio/transcription", + (output[2].strip() + " "), + ) + + # reset whisper + self.stream.init() + self.transcription_segments = [] + else: + # reset sherpa + self.model_runner.model.reset(self.stream) + + logger.debug("Stream reset") + + def check_unload_model(self) -> None: + # regularly called in the loop in audio maintainer + if ( + self.config.audio_transcription.model_size == "large" + and self.whisper_model is not None + ): + logger.debug(f"Unloading Whisper model for {self.camera_config.name}") + self.clear_audio_queue() + self.transcription_segments = [] + self.stream = None + self.whisper_model = None + + self.requestor.send_data( + f"{self.camera_config.name}/audio/transcription", + "", + ) + if ( + self.config.audio_transcription.model_size == "small" + and self.stream is not None + ): + logger.debug(f"Clearing sherpa stream for {self.camera_config.name}") + self.stream = None + + self.requestor.send_data( + f"{self.camera_config.name}/audio/transcription", + "", + ) + + def stop(self) -> None: + """Stop the transcription thread and clean up.""" + self.stop_event.set() + # Clear the queue to prevent processing stale data + while not self.audio_queue.empty(): + try: + self.audio_queue.get_nowait() + self.audio_queue.task_done() + except queue.Empty: + break + logger.debug( + f"Transcription thread stop signaled for {self.camera_config.name}" + ) + + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + if topic == "clear_audio_recognizer": + self.stream = None + self.__build_recognizer() + return {"message": "Audio recognizer cleared and rebuilt", "success": True} + return None + + def expire_object(self, object_id: str, camera: str) -> None: + pass diff --git a/frigate/data_processing/real_time/bird.py b/frigate/data_processing/real_time/bird.py new file mode 100644 index 0000000..48663f9 --- /dev/null +++ b/frigate/data_processing/real_time/bird.py @@ -0,0 +1,193 @@ +"""Handle processing images to classify birds.""" + +import logging +import os +from typing import Any + +import cv2 +import numpy as np + +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.config import FrigateConfig +from frigate.const import MODEL_CACHE_DIR +from frigate.log import suppress_stderr_during +from frigate.util.image import calculate_region + +from ..types import DataProcessorMetrics +from .api import RealTimeProcessorApi + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from ai_edge_litert.interpreter import Interpreter + +logger = logging.getLogger(__name__) + + +class BirdRealTimeProcessor(RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + sub_label_publisher: EventMetadataPublisher, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics) + self.interpreter: Interpreter | None = None + self.sub_label_publisher = sub_label_publisher + self.tensor_input_details: list[dict[str, Any]] | None = None + self.tensor_output_details: list[dict[str, Any]] | None = None + self.detected_birds: dict[str, float] = {} + self.labelmap: dict[int, str] = {} + + GITHUB_RAW_ENDPOINT = os.environ.get( + "GITHUB_RAW_ENDPOINT", "https://raw.githubusercontent.com" + ) + download_path = os.path.join(MODEL_CACHE_DIR, "bird") + self.model_files = { + "bird.tflite": f"{GITHUB_RAW_ENDPOINT}/google-coral/test_data/master/mobilenet_v2_1.0_224_inat_bird_quant.tflite", + "birdmap.txt": f"{GITHUB_RAW_ENDPOINT}/google-coral/test_data/master/inat_bird_labels.txt", + } + + if not all( + os.path.exists(os.path.join(download_path, n)) + for n in self.model_files.keys() + ): + # conditionally import ModelDownloader + from frigate.util.downloader import ModelDownloader + + self.downloader = ModelDownloader( + model_name="bird", + download_path=download_path, + file_names=list(self.model_files.keys()), + download_func=self.__download_models, + complete_func=self.__build_detector, + ) + self.downloader.ensure_model_files() + else: + self.__build_detector() + + def __download_models(self, path: str) -> None: + try: + file_name = os.path.basename(path) + + # conditionally import ModelDownloader + from frigate.util.downloader import ModelDownloader + + ModelDownloader.download_from_url(self.model_files[file_name], path) + except Exception as e: + logger.error(f"Failed to download {path}: {e}") + + def __build_detector(self) -> None: + # Suppress TFLite delegate creation messages that bypass Python logging + with suppress_stderr_during("tflite_interpreter_init"): + self.interpreter = Interpreter( + model_path=os.path.join(MODEL_CACHE_DIR, "bird/bird.tflite"), + num_threads=2, + ) + self.interpreter.allocate_tensors() + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + + i = 0 + + with open(os.path.join(MODEL_CACHE_DIR, "bird/birdmap.txt")) as f: + line = f.readline() + while line: + start = line.find("(") + end = line.find(")") + self.labelmap[i] = line[start + 1 : end] + i += 1 + line = f.readline() + + def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray) -> None: + if ( + not self.interpreter + or not self.tensor_input_details + or not self.tensor_output_details + ): + return + + if obj_data["label"] != "bird": + return + + x, y, x2, y2 = calculate_region( + frame.shape, + obj_data["box"][0], + obj_data["box"][1], + obj_data["box"][2], + obj_data["box"][3], + int( + max( + obj_data["box"][1] - obj_data["box"][0], + obj_data["box"][3] - obj_data["box"][2], + ) + * 1.1 + ), + 1.0, + ) + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2RGB_I420) + input = rgb[ + y:y2, + x:x2, + ] + + if input.shape != (224, 224): + try: + input = cv2.resize(input, (224, 224)) + except Exception: + logger.warning("Failed to resize image for bird classification") + return + + input = np.expand_dims(input, axis=0) + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], input) + self.interpreter.invoke() + res: np.ndarray = self.interpreter.get_tensor( + self.tensor_output_details[0]["index"] + )[0] + probs = res / res.sum(axis=0) + best_id = int(np.argmax(probs)) + + if best_id == 964: + logger.debug("No bird classification was detected.") + return + + score = round(probs[best_id], 2) + + if score < self.config.classification.bird.threshold: + logger.debug(f"Score {score} is not above required threshold") + return + + previous_score = self.detected_birds.get(obj_data["id"], 0.0) + + if score <= previous_score: + logger.debug(f"Score {score} is worse than previous score {previous_score}") + return + + self.sub_label_publisher.publish( + (obj_data["id"], self.labelmap[best_id], score), + EventMetadataTypeEnum.sub_label.value, + ) + 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: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + return None + + def expire_object(self, object_id: str, camera: str) -> None: + if object_id in self.detected_birds: + self.detected_birds.pop(object_id) diff --git a/frigate/data_processing/real_time/custom_classification.py b/frigate/data_processing/real_time/custom_classification.py new file mode 100644 index 0000000..e3b0e23 --- /dev/null +++ b/frigate/data_processing/real_time/custom_classification.py @@ -0,0 +1,744 @@ +"""Real time processor that works with classification tflite models.""" + +import datetime +import logging +import os +from typing import Any + +import cv2 +import numpy as np + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum +from frigate.comms.event_metadata_updater import EventMetadataPublisher +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.config.classification import CustomClassificationConfig +from frigate.const import CLIPS_DIR, MODEL_CACHE_DIR +from frigate.log import suppress_stderr_during +from frigate.util.builtin import EventsPerSecond, InferenceSpeed, load_labels +from frigate.util.image import calculate_region +from frigate.util.object import box_overlaps + +from ..types import DataProcessorMetrics +from .api import DeferredRealtimeProcessorApi + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from ai_edge_litert.interpreter import Interpreter + +logger = logging.getLogger(__name__) + +MAX_OBJECT_CLASSIFICATIONS = 16 + + +class CustomStateClassificationProcessor(DeferredRealtimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + model_config: CustomClassificationConfig, + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics, max_queue=4) + self.model_config = model_config + + if not self.model_config.name: + raise ValueError("Custom classification model name must be set.") + + self.requestor = requestor + self.model_dir = os.path.join(MODEL_CACHE_DIR, self.model_config.name) + self.train_dir = os.path.join(CLIPS_DIR, self.model_config.name, "train") + self.interpreter: Interpreter | None = None + self.tensor_input_details: list[dict[str, Any]] | None = None + self.tensor_output_details: list[dict[str, Any]] | None = None + self.labelmap: dict[int, str] = {} + self.classifications_per_second = EventsPerSecond() + self.state_history: dict[str, dict[str, Any]] = {} + + if ( + self.metrics + and self.model_config.name in self.metrics.classification_speeds + ): + self.inference_speed: InferenceSpeed | None = InferenceSpeed( + self.metrics.classification_speeds[self.model_config.name] + ) + else: + self.inference_speed = None + + self.last_run = datetime.datetime.now().timestamp() + self.__build_detector() + + def __build_detector(self) -> None: + model_path = os.path.join(self.model_dir, "model.tflite") + labelmap_path = os.path.join(self.model_dir, "labelmap.txt") + + if not os.path.exists(model_path) or not os.path.exists(labelmap_path): + self.interpreter = None + self.tensor_input_details = None + self.tensor_output_details = None + self.labelmap = {} + return + + # Suppress TFLite delegate creation messages that bypass Python logging + with suppress_stderr_during("tflite_interpreter_init"): + self.interpreter = Interpreter( + model_path=model_path, + num_threads=2, + ) + self.interpreter.allocate_tensors() + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + self.labelmap = load_labels(labelmap_path, prefill=0, indexed=False) + self.classifications_per_second.start() + + def __update_metrics(self, duration: float) -> None: + self.classifications_per_second.update() + if self.inference_speed: + self.inference_speed.update(duration) + + def _should_save_image( + self, camera: str, detected_state: str, score: float = 1.0 + ) -> bool: + """ + Determine if we should save the image for training. + Save when: + - State is changing or being verified (regardless of score) + - Score is less than 100% (even if state matches, useful for training) + Don't save when: + - State is stable (matches current_state) AND score is 100% + """ + if camera not in self.state_history: + # First detection for this camera, save it + return True + + verification = self.state_history[camera] + current_state = verification.get("current_state") + pending_state = verification.get("pending_state") + + # Save if there's a pending state change being verified + if pending_state is not None: + return True + + # Save if the detected state differs from the current verified state + # (state is changing) + if current_state is not None and detected_state != current_state: + return True + + # If score is less than 100%, save even if state matches + # (useful for training to improve confidence) + if score < 1.0: + return True + + # Don't save if state is stable (detected_state == current_state) AND score is 100% + return False + + def verify_state_change(self, camera: str, detected_state: str) -> str | None: + """ + Verify state change requires 3 consecutive identical states before publishing. + Returns state to publish or None if verification not complete. + """ + if camera not in self.state_history: + self.state_history[camera] = { + "current_state": None, + "pending_state": None, + "consecutive_count": 0, + } + + verification = self.state_history[camera] + + if detected_state == verification["current_state"]: + verification["pending_state"] = None + verification["consecutive_count"] = 0 + return None + + if detected_state == verification["pending_state"]: + verification["consecutive_count"] += 1 + + if verification["consecutive_count"] >= 3: + verification["current_state"] = detected_state + verification["pending_state"] = None + verification["consecutive_count"] = 0 + return detected_state + else: + verification["pending_state"] = detected_state + verification["consecutive_count"] = 1 + logger.debug( + f"New state '{detected_state}' detected for {camera}, need {3 - verification['consecutive_count']} more consecutive detections" + ) + + return None + + def process_frame(self, frame_data: dict[str, Any], frame: np.ndarray) -> None: + if ( + not self.model_config.name + or not self.model_config.state_config + or not self.tensor_input_details + or not self.tensor_output_details + ): + return + + if self.metrics and self.model_config.name in self.metrics.classification_cps: + self.metrics.classification_cps[ + self.model_config.name + ].value = self.classifications_per_second.eps() + camera = str(frame_data.get("camera")) + + if camera not in self.model_config.state_config.cameras: + return + + camera_config = self.model_config.state_config.cameras[camera] + crop = [ + camera_config.crop[0] * self.config.cameras[camera].detect.width, + camera_config.crop[1] * self.config.cameras[camera].detect.height, + camera_config.crop[2] * self.config.cameras[camera].detect.width, + camera_config.crop[3] * self.config.cameras[camera].detect.height, + ] + should_run = False + + now = datetime.datetime.now().timestamp() + if ( + self.model_config.state_config.interval + and now > self.last_run + self.model_config.state_config.interval + ): + self.last_run = now + should_run = True + + if ( + not should_run + and self.model_config.state_config.motion + and any([box_overlaps(crop, mb) for mb in frame_data.get("motion", [])]) + ): + # classification should run at most once per second + if now > self.last_run + 1: + self.last_run = now + should_run = True + + # Shortcut: always run if we have a pending state verification to complete + if ( + not should_run + and camera in self.state_history + and self.state_history[camera]["pending_state"] is not None + and now > self.last_run + 0.5 + ): + self.last_run = now + should_run = True + logger.debug( + f"Running verification check for pending state: {self.state_history[camera]['pending_state']} ({self.state_history[camera]['consecutive_count']}/3)" + ) + + if not should_run: + return + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2RGB_I420) + height, width = rgb.shape[:2] + + # Convert normalized crop coordinates to pixel values + x1 = int(camera_config.crop[0] * width) + y1 = int(camera_config.crop[1] * height) + x2 = int(camera_config.crop[2] * width) + y2 = int(camera_config.crop[3] * height) + + # Clip coordinates to frame boundaries + x1 = max(0, min(x1, width)) + y1 = max(0, min(y1, height)) + x2 = max(0, min(x2, width)) + y2 = max(0, min(y2, height)) + + if x2 <= x1 or y2 <= y1: + logger.warning( + f"Invalid crop coordinates for {camera}: [{x1}, {y1}, {x2}, {y2}]" + ) + return + + cropped_frame = rgb[y1:y2, x1:x2] + + try: + resized_frame = cv2.resize(cropped_frame, (224, 224)) + except Exception: + logger.warning("Failed to resize image for state classification") + return + + # Copy for training image saves on worker thread + crop_bgr = cv2.cvtColor(cropped_frame, cv2.COLOR_RGB2BGR) + + self._enqueue_task(("classify", camera, now, resized_frame, crop_bgr)) + + def _process_task(self, task: Any) -> None: + kind = task[0] + if kind == "classify": + _, camera, timestamp, resized_frame, crop_bgr = task + self._classify_state(camera, timestamp, resized_frame, crop_bgr) + elif kind == "reload": + self.__build_detector() + + def _classify_state( + self, + camera: str, + timestamp: float, + resized_frame: np.ndarray, + crop_bgr: np.ndarray, + ) -> None: + if self.interpreter is None: + # When interpreter is None, always save (score is 0.0, which is < 1.0) + if self._should_save_image(camera, "unknown", 0.0): + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 100 + ) + write_classification_attempt( + self.train_dir, + crop_bgr, + "none-none", + timestamp, + "unknown", + 0.0, + max_files=save_attempts, + ) + return + + if not self.tensor_input_details or not self.tensor_output_details: + return + + input = np.expand_dims(resized_frame, axis=0) + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], input) + self.interpreter.invoke() + res: np.ndarray = self.interpreter.get_tensor( + self.tensor_output_details[0]["index"] + )[0] + probs = res / res.sum(axis=0) + logger.debug( + f"{self.model_config.name} Ran state classification with probabilities: {probs}" + ) + best_id = int(np.argmax(probs)) + score = round(probs[best_id], 2) + self.__update_metrics(datetime.datetime.now().timestamp() - timestamp) + + detected_state = self.labelmap[best_id] + + if self._should_save_image(camera, detected_state, score): + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 100 + ) + write_classification_attempt( + self.train_dir, + crop_bgr, + "none-none", + timestamp, + detected_state, + score, + max_files=save_attempts, + ) + + if score < self.model_config.threshold: + logger.debug( + f"Score {score} below threshold {self.model_config.threshold}, skipping verification" + ) + return + + verified_state = self.verify_state_change(camera, detected_state) + + if verified_state is not None: + self._emit_result( + { + "type": "classification", + "processor": "state", + "model_name": self.model_config.name, + "camera": camera, + "state": verified_state, + } + ) + + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + if topic == EmbeddingsRequestEnum.reload_classification_model.value: + if request_data.get("model_name") == self.model_config.name: + + def _do_reload(data: dict[str, Any]) -> dict[str, Any]: + self.__build_detector() + logger.info( + f"Successfully loaded updated model for {self.model_config.name}" + ) + return { + "success": True, + "message": f"Loaded {self.model_config.name} model.", + } + + result: dict[str, Any] = self._enqueue_request(_do_reload, request_data) + return result + else: + return None + else: + return None + + def expire_object(self, object_id: str, camera: str) -> None: + pass + + +class CustomObjectClassificationProcessor(DeferredRealtimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + model_config: CustomClassificationConfig, + sub_label_publisher: EventMetadataPublisher, + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics, max_queue=8) + self.model_config = model_config + + if not self.model_config.name: + raise ValueError("Custom classification model name must be set.") + + self.model_dir = os.path.join(MODEL_CACHE_DIR, self.model_config.name) + self.train_dir = os.path.join(CLIPS_DIR, self.model_config.name, "train") + self.interpreter: Interpreter | None = None + self.sub_label_publisher = sub_label_publisher + self.requestor = requestor + self.tensor_input_details: list[dict[str, Any]] | None = None + self.tensor_output_details: list[dict[str, Any]] | None = None + self.classification_history: dict[str, list[tuple[str, float, float]]] = {} + self.labelmap: dict[int, str] = {} + self.classifications_per_second = EventsPerSecond() + + if ( + self.metrics + and self.model_config.name in self.metrics.classification_speeds + ): + self.inference_speed: InferenceSpeed | None = InferenceSpeed( + self.metrics.classification_speeds[self.model_config.name] + ) + else: + self.inference_speed = None + + self.__build_detector() + + def __build_detector(self) -> None: + model_path = os.path.join(self.model_dir, "model.tflite") + labelmap_path = os.path.join(self.model_dir, "labelmap.txt") + + if not os.path.exists(model_path) or not os.path.exists(labelmap_path): + self.interpreter = None + self.tensor_input_details = None + self.tensor_output_details = None + self.labelmap = {} + return + + # Suppress TFLite delegate creation messages that bypass Python logging + with suppress_stderr_during("tflite_interpreter_init"): + self.interpreter = Interpreter( + model_path=model_path, + num_threads=2, + ) + self.interpreter.allocate_tensors() + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + self.labelmap = load_labels(labelmap_path, prefill=0, indexed=False) + + def __update_metrics(self, duration: float) -> None: + self.classifications_per_second.update() + if self.inference_speed: + self.inference_speed.update(duration) + + def get_weighted_score( + self, + object_id: str, + current_label: str, + current_score: float, + current_time: float, + ) -> tuple[str | None, float]: + """ + Determine weighted score based on history to prevent false positives/negatives. + Requires 60% of attempts to agree on a label before publishing. + Returns (weighted_label, weighted_score) or (None, 0.0) if no weighted score. + """ + if object_id not in self.classification_history: + self.classification_history[object_id] = [] + logger.debug(f"Created new classification history for {object_id}") + + self.classification_history[object_id].append( + (current_label, current_score, current_time) + ) + + history = self.classification_history[object_id] + logger.debug( + f"History for {object_id}: {len(history)} entries, latest=({current_label}, {current_score})" + ) + + if len(history) < 3: + logger.debug( + f"History for {object_id} has {len(history)} entries, need at least 3" + ) + return None, 0.0 + + label_counts: dict[str, int] = {} + label_scores: dict[str, list[float]] = {} + total_attempts = len(history) + + for label, score, timestamp in history: + if label not in label_counts: + label_counts[label] = 0 + label_scores[label] = [] + + label_counts[label] += 1 + label_scores[label].append(score) + + best_label = max(label_counts, key=lambda k: label_counts[k]) + best_count = label_counts[best_label] + + consensus_threshold = total_attempts * 0.6 + logger.debug( + f"Consensus calc for {object_id}: label_counts={label_counts}, " + f"best_label={best_label}, best_count={best_count}, " + f"total={total_attempts}, threshold={consensus_threshold}" + ) + + if best_count < consensus_threshold: + logger.debug( + f"No consensus for {object_id}: {best_count} < {consensus_threshold}" + ) + return None, 0.0 + + avg_score = sum(label_scores[best_label]) / len(label_scores[best_label]) + + if best_label == "none": + logger.debug(f"Filtering 'none' label for {object_id}") + return None, 0.0 + + logger.debug( + f"Consensus reached for {object_id}: {best_label} with avg_score={avg_score}" + ) + return best_label, avg_score + + def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray) -> None: + if ( + not self.model_config.name + or not self.model_config.object_config + or not self.tensor_input_details + or not self.tensor_output_details + ): + return + + if self.metrics and self.model_config.name in self.metrics.classification_cps: + self.metrics.classification_cps[ + self.model_config.name + ].value = self.classifications_per_second.eps() + + if obj_data["false_positive"]: + return + + if obj_data["label"] not in self.model_config.object_config.objects: + return + + if obj_data.get("end_time") is not None: + return + + object_id = obj_data["id"] + + if ( + object_id in self.classification_history + and len(self.classification_history[object_id]) + >= MAX_OBJECT_CLASSIFICATIONS + ): + return + + now = datetime.datetime.now().timestamp() + x, y, x2, y2 = calculate_region( + frame.shape, + obj_data["box"][0], + obj_data["box"][1], + obj_data["box"][2], + obj_data["box"][3], + max( + obj_data["box"][2] - obj_data["box"][0], + obj_data["box"][3] - obj_data["box"][1], + ), + 1.0, + ) + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2RGB_I420) + crop = rgb[y:y2, x:x2] + + try: + resized_crop = cv2.resize(crop, (224, 224)) + except Exception: + logger.warning("Failed to resize image for object classification") + return + + # Copy crop for training images (will be used on worker thread) + crop_bgr = cv2.cvtColor(crop, cv2.COLOR_RGB2BGR) + + self._enqueue_task( + ("classify", object_id, obj_data["camera"], now, resized_crop, crop_bgr) + ) + + def _process_task(self, task: Any) -> None: + kind = task[0] + if kind == "classify": + _, object_id, camera, timestamp, resized_crop, crop_bgr = task + self._classify_object(object_id, camera, timestamp, resized_crop, crop_bgr) + elif kind == "expire": + _, object_id = task + if object_id in self.classification_history: + self.classification_history.pop(object_id) + elif kind == "reload": + self.__build_detector() + + def _classify_object( + self, + object_id: str, + camera: str, + timestamp: float, + resized_crop: np.ndarray, + crop_bgr: np.ndarray, + ) -> None: + if self.interpreter is None: + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 200 + ) + write_classification_attempt( + self.train_dir, + crop_bgr, + object_id, + timestamp, + "unknown", + 0.0, + max_files=save_attempts, + ) + + # Still track history even when model doesn't exist to respect MAX_OBJECT_CLASSIFICATIONS + # Add an entry with "unknown" label so the history limit is enforced + if object_id not in self.classification_history: + self.classification_history[object_id] = [] + + self.classification_history[object_id].append(("unknown", 0.0, timestamp)) + return + + if not self.tensor_input_details or not self.tensor_output_details: + return + + input = np.expand_dims(resized_crop, axis=0) + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], input) + self.interpreter.invoke() + res: np.ndarray = self.interpreter.get_tensor( + self.tensor_output_details[0]["index"] + )[0] + probs = res / res.sum(axis=0) + logger.debug( + f"{self.model_config.name} Ran object classification with probabilities: {probs}" + ) + best_id = int(np.argmax(probs)) + score = round(probs[best_id], 2) + self.__update_metrics(datetime.datetime.now().timestamp() - timestamp) + + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 200 + ) + write_classification_attempt( + self.train_dir, + crop_bgr, + object_id, + timestamp, + self.labelmap[best_id], + score, + max_files=save_attempts, + ) + + if score < self.model_config.threshold: + logger.debug( + f"{self.model_config.name}: Score {score} < threshold {self.model_config.threshold} for {object_id}, skipping" + ) + return + + sub_label = self.labelmap[best_id] + + logger.debug( + f"{self.model_config.name}: Object {object_id} passed threshold with sub_label={sub_label}, score={score}" + ) + + consensus_label, consensus_score = self.get_weighted_score( + object_id, sub_label, score, timestamp + ) + + logger.debug( + f"{self.model_config.name}: get_weighted_score returned consensus_label={consensus_label}, consensus_score={consensus_score} for {object_id}" + ) + + if consensus_label is not None and self.model_config.object_config is not None: + self._emit_result( + { + "type": "classification", + "processor": "object", + "model_name": self.model_config.name, + "classification_type": self.model_config.object_config.classification_type, + "object_id": object_id, + "camera": camera, + "timestamp": timestamp, + "label": consensus_label, + "score": consensus_score, + } + ) + + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + if topic == EmbeddingsRequestEnum.reload_classification_model.value: + if request_data.get("model_name") == self.model_config.name: + + def _do_reload(data: dict[str, Any]) -> dict[str, Any]: + self.__build_detector() + logger.info( + f"Successfully loaded updated model for {self.model_config.name}" + ) + return { + "success": True, + "message": f"Loaded {self.model_config.name} model.", + } + + result: dict[str, Any] = self._enqueue_request(_do_reload, request_data) + return result + else: + return None + else: + return None + + def expire_object(self, object_id: str, camera: str) -> None: + self._enqueue_task(("expire", object_id)) + + +def write_classification_attempt( + folder: str, + frame: np.ndarray, + event_id: str, + timestamp: float, + label: str, + score: float, + max_files: int = 100, +) -> None: + if "-" in label: + label = label.replace("-", "_") + + file = os.path.join(folder, f"{event_id}-{timestamp}-{label}-{score}.webp") + os.makedirs(folder, exist_ok=True) + cv2.imwrite(file, frame) + + # delete oldest face image if maximum is reached + try: + files = sorted( + filter(lambda f: f.endswith(".webp"), os.listdir(folder)), + key=lambda f: os.path.getctime(os.path.join(folder, f)), + reverse=True, + ) + + if len(files) > max_files: + os.unlink(os.path.join(folder, files[-1])) + except (FileNotFoundError, OSError): + pass diff --git a/frigate/data_processing/real_time/face.py b/frigate/data_processing/real_time/face.py new file mode 100644 index 0000000..83c8a2e --- /dev/null +++ b/frigate/data_processing/real_time/face.py @@ -0,0 +1,566 @@ +"""Handle processing images for face detection and recognition.""" + +import base64 +import datetime +import json +import logging +import os +import shutil +from pathlib import Path +from typing import Any + +import cv2 +import numpy as np + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import FACE_DIR, MODEL_CACHE_DIR +from frigate.data_processing.common.face.model import ( + ArcFaceRecognizer, + FaceNetRecognizer, + FaceRecognizer, +) +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.builtin import EventsPerSecond, InferenceSpeed +from frigate.util.image import area + +from ..types import DataProcessorMetrics +from .api import RealTimeProcessorApi + +logger = logging.getLogger(__name__) + + +MAX_DETECTION_HEIGHT = 1080 +MAX_FACES_ATTEMPTS_AFTER_REC = 6 +MAX_FACE_ATTEMPTS = 12 + + +class FaceRealTimeProcessor(RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + sub_label_publisher: EventMetadataPublisher, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics) + self.face_config = config.face_recognition + self.requestor = requestor + self.sub_label_publisher = sub_label_publisher + self.face_detector: cv2.FaceDetectorYN | None = None + self.requires_face_detection = "face" not in self.config.objects.all_objects + self.person_face_history: dict[str, list[tuple[str, float, int]]] = {} + self.camera_current_people: dict[str, list[str]] = {} + self.recognizer: FaceRecognizer + self.faces_per_second = EventsPerSecond() + self.inference_speed = InferenceSpeed(self.metrics.face_rec_speed) + + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + + download_path = os.path.join(MODEL_CACHE_DIR, "facedet") + self.model_files = { + "facedet.onnx": f"{GITHUB_ENDPOINT}/NickM-27/facenet-onnx/releases/download/v1.0/facedet.onnx", + "landmarkdet.yaml": f"{GITHUB_ENDPOINT}/NickM-27/facenet-onnx/releases/download/v1.0/landmarkdet.yaml", + } + + if not all( + os.path.exists(os.path.join(download_path, n)) + for n in self.model_files.keys() + ): + # conditionally import ModelDownloader + from frigate.util.downloader import ModelDownloader + + self.downloader = ModelDownloader( + model_name="facedet", + download_path=download_path, + file_names=list(self.model_files.keys()), + download_func=self.__download_models, + complete_func=self.__build_detector, + ) + self.downloader.ensure_model_files() + else: + self.__build_detector() + + self.label_map: dict[int, str] = {} + + if self.face_config.model_size == "small": + self.recognizer = FaceNetRecognizer(self.config) + else: + self.recognizer = ArcFaceRecognizer(self.config) + + 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: + try: + file_name = os.path.basename(path) + # conditionally import ModelDownloader + from frigate.util.downloader import ModelDownloader + + ModelDownloader.download_from_url(self.model_files[file_name], path) + except Exception as e: + logger.error(f"Failed to download {path}: {e}") + + def __build_detector(self) -> None: + self.face_detector = cv2.FaceDetectorYN.create( + os.path.join(MODEL_CACHE_DIR, "facedet/facedet.onnx"), + config="", + input_size=(320, 320), + score_threshold=0.5, + nms_threshold=0.3, + ) + self.faces_per_second.start() + + def __detect_face( + self, input: np.ndarray, threshold: float + ) -> tuple[int, int, int, int] | None: + """Detect faces in input image.""" + if not self.face_detector: + return None + + # YN face detector fails at extreme definitions + # this rescales to a size that can properly detect faces + # still retaining plenty of detail + if input.shape[0] > MAX_DETECTION_HEIGHT: + scale_factor = MAX_DETECTION_HEIGHT / input.shape[0] + new_width = int(scale_factor * input.shape[1]) + input = cv2.resize(input, (new_width, MAX_DETECTION_HEIGHT)) + else: + scale_factor = 1 + + self.face_detector.setInputSize((input.shape[1], input.shape[0])) + faces = self.face_detector.detect(input) + + if faces is None or faces[1] is None: + return None # type: ignore[unreachable] + + face = None + + for _, potential_face in enumerate(faces[1]): + if potential_face[-1] < threshold: + continue + + raw_bbox = potential_face[0:4].astype(np.uint16) + x: int = int(max(raw_bbox[0], 0) / scale_factor) + y: int = int(max(raw_bbox[1], 0) / scale_factor) + w: int = int(raw_bbox[2] / scale_factor) + h: int = int(raw_bbox[3] / scale_factor) + bbox = (x, y, x + w, y + h) + + if face is None or area(bbox) > area(face): # type: ignore[unreachable] + face = bbox + + return face + + def __update_metrics(self, duration: float) -> None: + self.faces_per_second.update() + self.inference_speed.update(duration) + + def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray) -> None: + """Look for faces in image.""" + self.metrics.face_rec_fps.value = self.faces_per_second.eps() + camera = obj_data["camera"] + + if not self.config.cameras[camera].face_recognition.enabled: + logger.debug(f"Face recognition disabled for camera {camera}, skipping") + return + + start = datetime.datetime.now().timestamp() + id = obj_data["id"] + + # don't run for non person objects + if obj_data.get("label") != "person": + logger.debug("Not processing face for a non person object.") + return + + # don't overwrite sub label for objects that have a sub label + # that is not a face + if obj_data.get("sub_label") and id not in self.person_face_history: + logger.debug( + f"Not processing face due to existing sub label: {obj_data.get('sub_label')}." + ) + return + + # check if we have hit limits + if ( + id in self.person_face_history + and len(self.person_face_history[id]) >= MAX_FACES_ATTEMPTS_AFTER_REC + ): + # if we are at max attempts after rec and we have a rec + if obj_data.get("sub_label"): + logger.debug( + "Not processing due to hitting max attempts after true recognition." + ) + return + + # if we don't have a rec and are at max attempts + if len(self.person_face_history[id]) >= MAX_FACE_ATTEMPTS: + logger.debug("Not processing due to hitting max rec attempts.") + return + + face: dict[str, Any] | None = None + + if self.requires_face_detection: + logger.debug("Running manual face detection.") + person_box = obj_data.get("box") + + if not person_box: + logger.debug(f"No person box available for {id}") + return + + # YuNet (cv2.FaceDetectorYN) is trained on BGR + bgr = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + left, top, right, bottom = person_box + person = bgr[top:bottom, left:right] + face_box = self.__detect_face(person, self.face_config.detection_threshold) + + if not face_box: + logger.debug("Detected no faces for person object.") + return + + face_frame = person[ + max(0, face_box[1]) : min(frame.shape[0], face_box[3]), + max(0, face_box[0]) : min(frame.shape[1], face_box[2]), + ] + + # check that face is correct size + if area(face_box) < self.config.cameras[camera].face_recognition.min_area: + logger.debug( + f"Detected face that is smaller than the min_area {face} < {self.config.cameras[camera].face_recognition.min_area}" + ) + return + + else: + # don't run for object without attributes + if not obj_data.get("current_attributes"): + logger.debug("No attributes to parse.") + return + + attributes: list[dict[str, Any]] = obj_data.get("current_attributes", []) + for attr in attributes: + if attr.get("label") != "face": + continue + + if face is None or attr.get("score", 0.0) > face.get("score", 0.0): + face = attr + + # no faces detected in this frame + if not face: + logger.debug(f"No face attributes found for {id}") + return + + face_box = face.get("box") + + # check that face is valid + if ( + not face_box + or area(face_box) + < self.config.cameras[camera].face_recognition.min_area + ): + logger.debug(f"Invalid face box {face}") + return + + face_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + + face_frame = face_frame[ + max(0, face_box[1]) : min(frame.shape[0], face_box[3]), + max(0, face_box[0]) : min(frame.shape[1], face_box[2]), + ] + + res = self.recognizer.classify(face_frame) + + if not res: + logger.debug(f"Face recognizer returned no result for {id}") + self.__update_metrics(datetime.datetime.now().timestamp() - start) + return + + sub_label, score = res + + if score <= self.face_config.unknown_score: + sub_label = "unknown" + + logger.debug( + f"Detected best face for person as: {sub_label} with probability {score}" + ) + + self.write_face_attempt( + face_frame, id, datetime.datetime.now().timestamp(), sub_label, score + ) + + if id not in self.person_face_history: + self.person_face_history[id] = [] + + if camera not in self.camera_current_people: + self.camera_current_people[camera] = [] + + self.camera_current_people[camera].append(id) + + self.person_face_history[id].append( + (sub_label, score, face_frame.shape[0] * face_frame.shape[1]) + ) + (weighted_sub_label, weighted_score) = self.weighted_average( + self.person_face_history[id] + ) + + self.requestor.send_data( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.face, + "name": weighted_sub_label, + "score": weighted_score, + "id": id, + "camera": camera, + "timestamp": start, + } + ), + ) + + if weighted_score >= self.face_config.recognition_threshold: + self.sub_label_publisher.publish( + (id, weighted_sub_label, weighted_score), + EventMetadataTypeEnum.sub_label.value, + ) + + self.__update_metrics(datetime.datetime.now().timestamp() - start) + + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + if topic == EmbeddingsRequestEnum.clear_face_classifier.value: + self.recognizer.clear() + return {"success": True, "message": "Face classifier cleared."} + elif topic == EmbeddingsRequestEnum.recognize_face.value: + img = cv2.imdecode( + np.frombuffer(base64.b64decode(request_data["image"]), dtype=np.uint8), + cv2.IMREAD_COLOR, + ) + + # detect faces with lower confidence since we expect the face + # to be visible in uploaded images + face_box = self.__detect_face(img, 0.5) + + if not face_box: + return {"message": "No face was detected.", "success": False} + + face = img[face_box[1] : face_box[3], face_box[0] : face_box[2]] + res = self.recognizer.classify(face) + + if not res: + return {"success": False, "message": "No face was recognized."} + + sub_label, score = res + + if score <= self.face_config.unknown_score: + sub_label = "unknown" + + return {"success": True, "score": score, "face_name": sub_label} + elif topic == EmbeddingsRequestEnum.register_face.value: + label = request_data["face_name"] + + if request_data.get("cropped"): + thumbnail = request_data["image"] + else: + img = cv2.imdecode( + np.frombuffer( + base64.b64decode(request_data["image"]), dtype=np.uint8 + ), + cv2.IMREAD_COLOR, + ) + + # detect faces with lower confidence since we expect the face + # to be visible in uploaded images + face_box = self.__detect_face(img, 0.5) + + if not face_box: + return { + "message": "No face was detected.", + "success": False, + } + + face = img[face_box[1] : face_box[3], face_box[0] : face_box[2]] + _, thumbnail = cv2.imencode( + ".webp", face, [int(cv2.IMWRITE_WEBP_QUALITY), 100] + ) + + # write face to library + folder = os.path.join(FACE_DIR, label) + file = os.path.join( + folder, f"{label}_{datetime.datetime.now().timestamp()}.webp" + ) + os.makedirs(folder, exist_ok=True) + + # save face image + with open(file, "wb") as output: + output.write(thumbnail.tobytes()) + + self.recognizer.clear() + return { + "message": "Successfully registered face.", + "success": True, + } + elif topic == EmbeddingsRequestEnum.reprocess_face.value: + current_file: str = request_data["image_file"] + (id_time, id_rand, timestamp, _, _) = current_file.split("-") + img = None + id = f"{id_time}-{id_rand}" + + if current_file: + img = cv2.imread(current_file) + + if img is None: + return { # type: ignore[unreachable] + "message": "Invalid image file.", + "success": False, + } + + res = self.recognizer.classify(img) + + if not res: + return { + "message": "Model is still training, please try again in a few moments.", + "success": False, + } + + sub_label, score = res + + if score <= self.face_config.unknown_score: + sub_label = "unknown" + + if "-" in sub_label: + sub_label = sub_label.replace("-", "_") + + if self.config.face_recognition.save_attempts: + # write face to library + folder = os.path.join(FACE_DIR, "train") + os.makedirs(folder, exist_ok=True) + new_file = os.path.join( + folder, f"{id}-{timestamp}-{sub_label}-{score}.webp" + ) + shutil.move(current_file, new_file) + + return { + "message": f"Successfully reprocessed face. Result: {sub_label} (score: {score:.2f})", + "success": True, + "face_name": sub_label, + "score": score, + } + + return None + + def expire_object(self, object_id: str, camera: str) -> None: + if object_id in self.person_face_history: + self.person_face_history.pop(object_id) + + if object_id in self.camera_current_people.get(camera, []): + self.camera_current_people[camera].remove(object_id) + + def weighted_average( + self, results_list: list[tuple[str, float, int]], max_weight: int = 4000 + ) -> tuple[str | None, float]: + """ + Calculates a robust weighted average, capping the area weight and giving more weight to higher scores. + + Args: + results_list: A list of tuples, where each tuple contains (name, score, face_area). + max_weight: The maximum weight to apply based on face area. + + Returns: + A tuple containing the prominent name and its weighted average score, or (None, 0.0) if the list is empty. + """ + if not results_list: + return None, 0.0 + + counts: dict[str, int] = {} + weighted_scores: dict[str, float] = {} + total_weights: dict[str, float] = {} + + for name, score, face_area in results_list: + if name == "unknown": + continue + + if name not in weighted_scores: + counts[name] = 0 + weighted_scores[name] = 0.0 + total_weights[name] = 0.0 + + # increase count + counts[name] += 1 + + # Capped weight based on face area + weight: float = min(face_area, max_weight) + + # Score-based weighting (higher scores get more weight) + weight *= (score - self.face_config.unknown_score) * 10 + weighted_scores[name] += score * weight + total_weights[name] += weight + + if not weighted_scores: + return None, 0.0 + + best_name = max(weighted_scores, key=lambda k: weighted_scores[k]) + + # If the number of faces for this person < min_faces, we are not confident it is a correct result + if counts[best_name] < self.face_config.min_faces: + return None, 0.0 + + # If the best name has the same number of results as another name, we are not confident it is a correct result + for name, count in counts.items(): + if name != best_name and counts[best_name] == count: + return None, 0.0 + + weighted_average = weighted_scores[best_name] / total_weights[best_name] + + return best_name, weighted_average + + def write_face_attempt( + self, + frame: np.ndarray, + event_id: str, + timestamp: float, + sub_label: str, + score: float, + ) -> None: + if self.config.face_recognition.save_attempts: + # write face to library + folder = os.path.join(FACE_DIR, "train") + + if "-" in sub_label: + sub_label = sub_label.replace("-", "_") + + file = os.path.join( + folder, f"{event_id}-{timestamp}-{sub_label}-{score}.webp" + ) + os.makedirs(folder, exist_ok=True) + cv2.imwrite(file, frame) + + files = sorted( + filter(lambda f: f.endswith(".webp"), os.listdir(folder)), + key=lambda f: os.path.getctime(os.path.join(folder, f)), + reverse=True, + ) + + # delete oldest face image if maximum is reached + if len(files) > self.config.face_recognition.save_attempts: + Path(os.path.join(folder, files[-1])).unlink(missing_ok=True) diff --git a/frigate/data_processing/real_time/license_plate.py b/frigate/data_processing/real_time/license_plate.py new file mode 100644 index 0000000..c2ea28b --- /dev/null +++ b/frigate/data_processing/real_time/license_plate.py @@ -0,0 +1,76 @@ +"""Handle processing images for face detection and recognition.""" + +import logging +from typing import Any + +import numpy as np + +from frigate.comms.event_metadata_updater import EventMetadataPublisher +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.data_processing.common.license_plate.mixin import ( + LicensePlateProcessingMixin, +) +from frigate.data_processing.common.license_plate.model import ( + LicensePlateModelRunner, +) + +from ..types import DataProcessorMetrics +from .api import RealTimeProcessorApi + +logger = logging.getLogger(__name__) + + +class LicensePlateRealTimeProcessor(LicensePlateProcessingMixin, RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + sub_label_publisher: EventMetadataPublisher, + metrics: DataProcessorMetrics, + model_runner: LicensePlateModelRunner, + detected_license_plates: dict[str, dict[str, Any]], + ): + self.requestor = requestor + self.detected_license_plates = detected_license_plates + self.model_runner = model_runner + self.lpr_config = config.lpr + self.config = config + self.sub_label_publisher = sub_label_publisher + self.camera_current_cars: dict[str, list[str]] = {} + 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( + self, + obj_data: dict[str, Any], + frame: np.ndarray, + dedicated_lpr: bool = False, + ) -> None: + """Look for license plates in image.""" + self.lpr_process(obj_data, frame, dedicated_lpr) + + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + return None + + def expire_object(self, object_id: str, camera: str) -> None: + """Expire lpr objects.""" + self.lpr_expire(object_id, camera) diff --git a/frigate/data_processing/real_time/whisper_online.py b/frigate/data_processing/real_time/whisper_online.py new file mode 100644 index 0000000..066303c --- /dev/null +++ b/frigate/data_processing/real_time/whisper_online.py @@ -0,0 +1,1160 @@ +# imported to Frigate from https://github.com/ufal/whisper_streaming +# with only minor modifications +import io +import logging +import math +import sys +import time +from functools import lru_cache + +import librosa +import numpy as np +import soundfile as sf + +logger = logging.getLogger(__name__) + + +@lru_cache(10**6) +def load_audio(fname): + a, _ = librosa.load(fname, sr=16000, dtype=np.float32) + return a + + +def load_audio_chunk(fname, beg, end): + audio = load_audio(fname) + beg_s = int(beg * 16000) + end_s = int(end * 16000) + return audio[beg_s:end_s] + + +# Whisper backend + + +class ASRBase: + sep = "" # join transcribe words with this character (" " for whisper_timestamped, + # "" for faster-whisper because it emits the spaces when neeeded) + + def __init__( + self, + lan, + modelsize=None, + cache_dir=None, + model_dir=None, + logfile=sys.stderr, + device="cpu", + ): + self.logfile = logfile + + self.transcribe_kargs = {} + if lan == "auto": + self.original_language = None + else: + self.original_language = lan + + self.model = self.load_model(modelsize, cache_dir, model_dir, device) + + def load_model(self, modelsize, cache_dir): + raise NotImplementedError("must be implemented in the child class") + + def transcribe(self, audio, init_prompt=""): + raise NotImplementedError("must be implemented in the child class") + + def use_vad(self): + raise NotImplementedError("must be implemented in the child class") + + +class WhisperTimestampedASR(ASRBase): + """Uses whisper_timestamped library as the backend. Initially, we tested the code on this backend. It worked, but slower than faster-whisper. + On the other hand, the installation for GPU could be easier. + """ + + sep = " " + + def load_model(self, modelsize=None, cache_dir=None, model_dir=None): + import whisper + from whisper_timestamped import transcribe_timestamped + + self.transcribe_timestamped = transcribe_timestamped + if model_dir is not None: + logger.debug("ignoring model_dir, not implemented") + return whisper.load_model(modelsize, download_root=cache_dir) + + def transcribe(self, audio, init_prompt=""): + result = self.transcribe_timestamped( + self.model, + audio, + language=self.original_language, + initial_prompt=init_prompt, + verbose=None, + condition_on_previous_text=True, + **self.transcribe_kargs, + ) + return result + + def ts_words(self, r): + # return: transcribe result object to [(beg,end,"word1"), ...] + o = [] + for s in r["segments"]: + for w in s["words"]: + t = (w["start"], w["end"], w["text"]) + o.append(t) + return o + + def segments_end_ts(self, res): + return [s["end"] for s in res["segments"]] + + def use_vad(self): + self.transcribe_kargs["vad"] = True + + def set_translate_task(self): + self.transcribe_kargs["task"] = "translate" + + +class FasterWhisperASR(ASRBase): + """Uses faster-whisper library as the backend. Works much faster, appx 4-times (in offline mode). For GPU, it requires installation with a specific CUDNN version.""" + + sep = "" + + def load_model(self, modelsize=None, cache_dir=None, model_dir=None, device="cpu"): + from faster_whisper import WhisperModel + + logging.getLogger("faster_whisper").setLevel(logging.WARNING) + + # this worked fast and reliably on NVIDIA L40 + model = WhisperModel( + model_size_or_path="small" if device == "cuda" else "tiny", + device=device, + compute_type="float16" if device == "cuda" else "int8", + local_files_only=False, + download_root=model_dir, + ) + + # or run on GPU with INT8 + # tested: the transcripts were different, probably worse than with FP16, and it was slightly (appx 20%) slower + # model = WhisperModel(model_size, device="cuda", compute_type="int8_float16") + + # or run on CPU with INT8 + # tested: works, but slow, appx 10-times than cuda FP16 + # model = WhisperModel(modelsize, device="cpu", compute_type="int8") #, download_root="faster-disk-cache-dir/") + return model + + def transcribe(self, audio, init_prompt=""): + from faster_whisper import BatchedInferencePipeline + + logging.getLogger("faster_whisper").setLevel(logging.WARNING) + + # tested: beam_size=5 is faster and better than 1 (on one 200 second document from En ESIC, min chunk 0.01) + batched_model = BatchedInferencePipeline(model=self.model) + segments, info = batched_model.transcribe( + audio, + language=self.original_language, + initial_prompt=init_prompt, + beam_size=5, + word_timestamps=True, + condition_on_previous_text=True, + **self.transcribe_kargs, + ) + # print(info) # info contains language detection result + + return list(segments) + + def ts_words(self, segments): + o = [] + for segment in segments: + for word in segment.words: + if segment.no_speech_prob > 0.9: + continue + # not stripping the spaces -- should not be merged with them! + w = word.word + t = (word.start, word.end, w) + o.append(t) + return o + + def segments_end_ts(self, res): + return [s.end for s in res] + + def use_vad(self): + self.transcribe_kargs["vad_filter"] = True + + def set_translate_task(self): + self.transcribe_kargs["task"] = "translate" + + +class MLXWhisper(ASRBase): + """ + Uses MLX Whisper library as the backend, optimized for Apple Silicon. + Models available: https://huggingface.co/collections/mlx-community/whisper-663256f9964fbb1177db93dc + Significantly faster than faster-whisper (without CUDA) on Apple M1. + """ + + sep = " " + + def load_model(self, modelsize=None, cache_dir=None, model_dir=None): + """ + Loads the MLX-compatible Whisper model. + + Args: + modelsize (str, optional): The size or name of the Whisper model to load. + If provided, it will be translated to an MLX-compatible model path using the `translate_model_name` method. + Example: "large-v3-turbo" -> "mlx-community/whisper-large-v3-turbo". + cache_dir (str, optional): Path to the directory for caching models. + **Note**: This is not supported by MLX Whisper and will be ignored. + model_dir (str, optional): Direct path to a custom model directory. + If specified, it overrides the `modelsize` parameter. + """ + import mlx.core as mx # Is installed with mlx-whisper + from mlx_whisper.transcribe import ModelHolder, transcribe + + if model_dir is not None: + logger.debug( + f"Loading whisper model from model_dir {model_dir}. modelsize parameter is not used." + ) + model_size_or_path = model_dir + elif modelsize is not None: + model_size_or_path = self.translate_model_name(modelsize) + logger.debug( + f"Loading whisper model {modelsize}. You use mlx whisper, so {model_size_or_path} will be used." + ) + + self.model_size_or_path = model_size_or_path + + # Note: ModelHolder.get_model loads the model into a static class variable, + # making it a global resource. This means: + # - Only one model can be loaded at a time; switching models requires reloading. + # - This approach may not be suitable for scenarios requiring multiple models simultaneously, + # such as using whisper-streaming as a module with varying model sizes. + dtype = mx.float16 # Default to mx.float16. In mlx_whisper.transcribe: dtype = mx.float16 if decode_options.get("fp16", True) else mx.float32 + ModelHolder.get_model( + model_size_or_path, dtype + ) # Model is preloaded to avoid reloading during transcription + + return transcribe + + def translate_model_name(self, model_name): + """ + Translates a given model name to its corresponding MLX-compatible model path. + + Args: + model_name (str): The name of the model to translate. + + Returns: + str: The MLX-compatible model path. + """ + # Dictionary mapping model names to MLX-compatible paths + model_mapping = { + "tiny.en": "mlx-community/whisper-tiny.en-mlx", + "tiny": "mlx-community/whisper-tiny-mlx", + "base.en": "mlx-community/whisper-base.en-mlx", + "base": "mlx-community/whisper-base-mlx", + "small.en": "mlx-community/whisper-small.en-mlx", + "small": "mlx-community/whisper-small-mlx", + "medium.en": "mlx-community/whisper-medium.en-mlx", + "medium": "mlx-community/whisper-medium-mlx", + "large-v1": "mlx-community/whisper-large-v1-mlx", + "large-v2": "mlx-community/whisper-large-v2-mlx", + "large-v3": "mlx-community/whisper-large-v3-mlx", + "large-v3-turbo": "mlx-community/whisper-large-v3-turbo", + "large": "mlx-community/whisper-large-mlx", + } + + # Retrieve the corresponding MLX model path + mlx_model_path = model_mapping.get(model_name) + + if mlx_model_path: + return mlx_model_path + else: + raise ValueError( + f"Model name '{model_name}' is not recognized or not supported." + ) + + def transcribe(self, audio, init_prompt=""): + segments = self.model( + audio, + language=self.original_language, + initial_prompt=init_prompt, + word_timestamps=True, + condition_on_previous_text=True, + path_or_hf_repo=self.model_size_or_path, + **self.transcribe_kargs, + ) + return segments.get("segments", []) + + def ts_words(self, segments): + """ + Extract timestamped words from transcription segments and skips words with high no-speech probability. + """ + return [ + (word["start"], word["end"], word["word"]) + for segment in segments + for word in segment.get("words", []) + if segment.get("no_speech_prob", 0) <= 0.9 + ] + + def segments_end_ts(self, res): + return [s["end"] for s in res] + + def use_vad(self): + self.transcribe_kargs["vad_filter"] = True + + def set_translate_task(self): + self.transcribe_kargs["task"] = "translate" + + +class OpenaiApiASR(ASRBase): + """Uses OpenAI's Whisper API for audio transcription.""" + + def __init__(self, lan=None, temperature=0, logfile=sys.stderr): + self.logfile = logfile + + self.modelname = "whisper-1" + self.original_language = ( + None if lan == "auto" else lan + ) # ISO-639-1 language code + self.response_format = "verbose_json" + self.temperature = temperature + + self.load_model() + + self.use_vad_opt = False + + # reset the task in set_translate_task + self.task = "transcribe" + + def load_model(self, *args, **kwargs): + from openai import OpenAI + + self.client = OpenAI() + + self.transcribed_seconds = ( + 0 # for logging how many seconds were processed by API, to know the cost + ) + + def ts_words(self, segments): + no_speech_segments = [] + if self.use_vad_opt: + for segment in segments.segments: + # TODO: threshold can be set from outside + if segment["no_speech_prob"] > 0.8: + no_speech_segments.append( + (segment.get("start"), segment.get("end")) + ) + + o = [] + for word in segments.words: + start = word.start + end = word.end + if any(s[0] <= start <= s[1] for s in no_speech_segments): + # print("Skipping word", word.get("word"), "because it's in a no-speech segment") + continue + o.append((start, end, word.word)) + return o + + def segments_end_ts(self, res): + return [s.end for s in res.words] + + def transcribe(self, audio_data, prompt=None, *args, **kwargs): + # Write the audio data to a buffer + buffer = io.BytesIO() + buffer.name = "temp.wav" + sf.write(buffer, audio_data, samplerate=16000, format="WAV", subtype="PCM_16") + buffer.seek(0) # Reset buffer's position to the beginning + + self.transcribed_seconds += math.ceil( + len(audio_data) / 16000 + ) # it rounds up to the whole seconds + + params = { + "model": self.modelname, + "file": buffer, + "response_format": self.response_format, + "temperature": self.temperature, + "timestamp_granularities": ["word", "segment"], + } + if self.task != "translate" and self.original_language: + params["language"] = self.original_language + if prompt: + params["prompt"] = prompt + + if self.task == "translate": + proc = self.client.audio.translations + else: + proc = self.client.audio.transcriptions + + # Process transcription/translation + transcript = proc.create(**params) + logger.debug( + f"OpenAI API processed accumulated {self.transcribed_seconds} seconds" + ) + + return transcript + + def use_vad(self): + self.use_vad_opt = True + + def set_translate_task(self): + self.task = "translate" + + +class HypothesisBuffer: + def __init__(self, logfile=sys.stderr): + self.commited_in_buffer = [] + self.buffer = [] + self.new = [] + + self.last_commited_time = 0 + self.last_commited_word = None + + self.logfile = logfile + + def insert(self, new, offset): + # compare self.commited_in_buffer and new. It inserts only the words in new that extend the commited_in_buffer, it means they are roughly behind last_commited_time and new in content + # the new tail is added to self.new + + new = [(a + offset, b + offset, t) for a, b, t in new] + self.new = [(a, b, t) for a, b, t in new if a > self.last_commited_time - 0.1] + + if len(self.new) >= 1: + a, b, t = self.new[0] + if abs(a - self.last_commited_time) < 1: + if self.commited_in_buffer: + # it's going to search for 1, 2, ..., 5 consecutive words (n-grams) that are identical in commited and new. If they are, they're dropped. + cn = len(self.commited_in_buffer) + nn = len(self.new) + for i in range(1, min(min(cn, nn), 5) + 1): # 5 is the maximum + c = " ".join( + [self.commited_in_buffer[-j][2] for j in range(1, i + 1)][ + ::-1 + ] + ) + tail = " ".join(self.new[j - 1][2] for j in range(1, i + 1)) + if c == tail: + words = [] + for j in range(i): + words.append(repr(self.new.pop(0))) + words_msg = " ".join(words) + logger.debug(f"removing last {i} words: {words_msg}") + break + + def flush(self): + # returns commited chunk = the longest common prefix of 2 last inserts. + + commit = [] + while self.new: + na, nb, nt = self.new[0] + + if len(self.buffer) == 0: + break + + if nt == self.buffer[0][2]: + commit.append((na, nb, nt)) + self.last_commited_word = nt + self.last_commited_time = nb + self.buffer.pop(0) + self.new.pop(0) + else: + break + self.buffer = self.new + self.new = [] + self.commited_in_buffer.extend(commit) + return commit + + def pop_commited(self, time): + while self.commited_in_buffer and self.commited_in_buffer[0][1] <= time: + self.commited_in_buffer.pop(0) + + def complete(self): + return self.buffer + + +class OnlineASRProcessor: + SAMPLING_RATE = 16000 + + def __init__( + self, asr, tokenizer=None, buffer_trimming=("segment", 15), logfile=sys.stderr + ): + """asr: WhisperASR object + tokenizer: sentence tokenizer object for the target language. Must have a method *split* that behaves like the one of MosesTokenizer. It can be None, if "segment" buffer trimming option is used, then tokenizer is not used at all. + ("segment", 15) + buffer_trimming: a pair of (option, seconds), where option is either "sentence" or "segment", and seconds is a number. Buffer is trimmed if it is longer than "seconds" threshold. Default is the most recommended option. + logfile: where to store the log. + """ + self.asr = asr + self.tokenizer = tokenizer + self.logfile = logfile + + self.init() + + self.buffer_trimming_way, self.buffer_trimming_sec = buffer_trimming + + def init(self, offset=None): + """run this when starting or restarting processing""" + self.audio_buffer = np.array([], dtype=np.float32) + self.transcript_buffer = HypothesisBuffer(logfile=self.logfile) + self.buffer_time_offset = 0 + if offset is not None: + self.buffer_time_offset = offset + self.transcript_buffer.last_commited_time = self.buffer_time_offset + self.commited = [] + + def insert_audio_chunk(self, audio): + self.audio_buffer = np.append(self.audio_buffer, audio) + + def prompt(self): + """Returns a tuple: (prompt, context), where "prompt" is a 200-character suffix of commited text that is inside of the scrolled away part of audio buffer. + "context" is the commited text that is inside the audio buffer. It is transcribed again and skipped. It is returned only for debugging and logging reasons. + """ + k = max(0, len(self.commited) - 1) + while k > 0 and self.commited[k - 1][1] > self.buffer_time_offset: + k -= 1 + + p = self.commited[:k] + p = [t for _, _, t in p] + prompt = [] + y = 0 + while p and y < 200: # 200 characters prompt size + x = p.pop(-1) + y += len(x) + 1 + prompt.append(x) + non_prompt = self.commited[k:] + return self.asr.sep.join(prompt[::-1]), self.asr.sep.join( + t for _, _, t in non_prompt + ) + + def process_iter(self): + """Runs on the current audio buffer. + Returns: a tuple (beg_timestamp, end_timestamp, "text"), or (None, None, ""). + The non-emty text is confirmed (committed) partial transcript. + """ + + prompt, non_prompt = self.prompt() + logger.debug(f"PROMPT: {prompt}") + logger.debug(f"CONTEXT: {non_prompt}") + logger.debug( + f"transcribing {len(self.audio_buffer) / self.SAMPLING_RATE:2.2f} seconds from {self.buffer_time_offset:2.2f}" + ) + res = self.asr.transcribe(self.audio_buffer, init_prompt=prompt) + + # transform to [(beg,end,"word1"), ...] + tsw = self.asr.ts_words(res) + + self.transcript_buffer.insert(tsw, self.buffer_time_offset) + o = self.transcript_buffer.flush() + self.commited.extend(o) + completed = self.to_flush(o) + logger.debug(f">>>>COMPLETE NOW: {completed}") + the_rest = self.to_flush(self.transcript_buffer.complete()) + logger.debug(f"INCOMPLETE: {the_rest}") + + # there is a newly confirmed text + + if o and self.buffer_trimming_way == "sentence": # trim the completed sentences + if ( + len(self.audio_buffer) / self.SAMPLING_RATE > self.buffer_trimming_sec + ): # longer than this + self.chunk_completed_sentence() + + if self.buffer_trimming_way == "segment": + s = self.buffer_trimming_sec # trim the completed segments longer than s, + else: + s = 30 # if the audio buffer is longer than 30s, trim it + + if len(self.audio_buffer) / self.SAMPLING_RATE > s: + self.chunk_completed_segment(res) + + # alternative: on any word + # l = self.buffer_time_offset + len(self.audio_buffer)/self.SAMPLING_RATE - 10 + # let's find commited word that is less + # k = len(self.commited)-1 + # while k>0 and self.commited[k][1] > l: + # k -= 1 + # t = self.commited[k][1] + logger.debug("chunking segment") + # self.chunk_at(t) + + logger.debug( + f"len of buffer now: {len(self.audio_buffer) / self.SAMPLING_RATE:2.2f}" + ) + return self.to_flush(o) + + def chunk_completed_sentence(self): + if self.commited == []: + return + logger.debug(self.commited) + sents = self.words_to_sentences(self.commited) + for s in sents: + logger.debug(f"\t\tSENT: {s}") + if len(sents) < 2: + return + while len(sents) > 2: + sents.pop(0) + # we will continue with audio processing at this timestamp + chunk_at = sents[-2][1] + + logger.debug(f"--- sentence chunked at {chunk_at:2.2f}") + self.chunk_at(chunk_at) + + def chunk_completed_segment(self, res): + if self.commited == []: + return + + ends = self.asr.segments_end_ts(res) + + t = self.commited[-1][1] + + if len(ends) > 1: + e = ends[-2] + self.buffer_time_offset + while len(ends) > 2 and e > t: + ends.pop(-1) + e = ends[-2] + self.buffer_time_offset + if e <= t: + logger.debug(f"--- segment chunked at {e:2.2f}") + self.chunk_at(e) + else: + logger.debug("--- last segment not within commited area") + else: + logger.debug("--- not enough segments to chunk") + + def chunk_at(self, time): + """trims the hypothesis and audio buffer at "time" """ + self.transcript_buffer.pop_commited(time) + cut_seconds = time - self.buffer_time_offset + self.audio_buffer = self.audio_buffer[int(cut_seconds * self.SAMPLING_RATE) :] + self.buffer_time_offset = time + + def words_to_sentences(self, words): + """Uses self.tokenizer for sentence segmentation of words. + Returns: [(beg,end,"sentence 1"),...] + """ + + cwords = [w for w in words] + t = " ".join(o[2] for o in cwords) + s = self.tokenizer.split(t) + out = [] + while s: + beg = None + end = None + sent = s.pop(0).strip() + fsent = sent + while cwords: + b, e, w = cwords.pop(0) + w = w.strip() + if beg is None and sent.startswith(w): + beg = b + elif end is None and sent == w: + end = e + out.append((beg, end, fsent)) + break + sent = sent[len(w) :].strip() + return out + + def finish(self): + """Flush the incomplete text when the whole processing ends. + Returns: the same format as self.process_iter() + """ + o = self.transcript_buffer.complete() + f = self.to_flush(o) + logger.debug(f"last, noncommited: {f}") + self.buffer_time_offset += len(self.audio_buffer) / 16000 + return f + + def to_flush( + self, + sents, + sep=None, + offset=0, + ): + # concatenates the timestamped words or sentences into one sequence that is flushed in one line + # sents: [(beg1, end1, "sentence1"), ...] or [] if empty + # return: (beg1,end-of-last-sentence,"concatenation of sentences") or (None, None, "") if empty + if sep is None: + sep = self.asr.sep + t = sep.join(s[2] for s in sents) + if len(sents) == 0: + b = None + e = None + else: + b = offset + sents[0][0] + e = offset + sents[-1][1] + return (b, e, t) + + +class VACOnlineASRProcessor(OnlineASRProcessor): + """Wraps OnlineASRProcessor with VAC (Voice Activity Controller). + + It works the same way as OnlineASRProcessor: it receives chunks of audio (e.g. 0.04 seconds), + it runs VAD and continuously detects whether there is speech or not. + When it detects end of speech (non-voice for 500ms), it makes OnlineASRProcessor to end the utterance immediately. + """ + + def __init__(self, online_chunk_size, *a, **kw): + self.online_chunk_size = online_chunk_size + + self.online = OnlineASRProcessor(*a, **kw) + + # VAC: + import torch + + model, _ = torch.hub.load(repo_or_dir="snakers4/silero-vad", model="silero_vad") + from silero_vad_iterator import FixedVADIterator + + self.vac = FixedVADIterator( + model + ) # we use the default options there: 500ms silence, 100ms padding, etc. + + self.logfile = self.online.logfile + self.init() + + def init(self): + self.online.init() + self.vac.reset_states() + self.current_online_chunk_buffer_size = 0 + + self.is_currently_final = False + + self.status = None # or "voice" or "nonvoice" + self.audio_buffer = np.array([], dtype=np.float32) + self.buffer_offset = 0 # in frames + + def clear_buffer(self): + self.buffer_offset += len(self.audio_buffer) + self.audio_buffer = np.array([], dtype=np.float32) + + def insert_audio_chunk(self, audio): + res = self.vac(audio) + self.audio_buffer = np.append(self.audio_buffer, audio) + + if res is not None: + frame = list(res.values())[0] - self.buffer_offset + if "start" in res and "end" not in res: + self.status = "voice" + send_audio = self.audio_buffer[frame:] + self.online.init( + offset=(frame + self.buffer_offset) / self.SAMPLING_RATE + ) + self.online.insert_audio_chunk(send_audio) + self.current_online_chunk_buffer_size += len(send_audio) + self.clear_buffer() + elif "end" in res and "start" not in res: + self.status = "nonvoice" + send_audio = self.audio_buffer[:frame] + self.online.insert_audio_chunk(send_audio) + self.current_online_chunk_buffer_size += len(send_audio) + self.is_currently_final = True + self.clear_buffer() + else: + beg = res["start"] - self.buffer_offset + end = res["end"] - self.buffer_offset + self.status = "nonvoice" + send_audio = self.audio_buffer[beg:end] + self.online.init(offset=(beg + self.buffer_offset) / self.SAMPLING_RATE) + self.online.insert_audio_chunk(send_audio) + self.current_online_chunk_buffer_size += len(send_audio) + self.is_currently_final = True + self.clear_buffer() + else: + if self.status == "voice": + self.online.insert_audio_chunk(self.audio_buffer) + self.current_online_chunk_buffer_size += len(self.audio_buffer) + self.clear_buffer() + else: + # We keep 1 second because VAD may later find start of voice in it. + # But we trim it to prevent OOM. + self.buffer_offset += max( + 0, len(self.audio_buffer) - self.SAMPLING_RATE + ) + self.audio_buffer = self.audio_buffer[-self.SAMPLING_RATE :] + + def process_iter(self): + if self.is_currently_final: + return self.finish() + elif ( + self.current_online_chunk_buffer_size + > self.SAMPLING_RATE * self.online_chunk_size + ): + self.current_online_chunk_buffer_size = 0 + ret = self.online.process_iter() + return ret + else: + print("no online update, only VAD", self.status, file=self.logfile) + return (None, None, "") + + def finish(self): + ret = self.online.finish() + self.current_online_chunk_buffer_size = 0 + self.is_currently_final = False + return ret + + +WHISPER_LANG_CODES = "af,am,ar,as,az,ba,be,bg,bn,bo,br,bs,ca,cs,cy,da,de,el,en,es,et,eu,fa,fi,fo,fr,gl,gu,ha,haw,he,hi,hr,ht,hu,hy,id,is,it,ja,jw,ka,kk,km,kn,ko,la,lb,ln,lo,lt,lv,mg,mi,mk,ml,mn,mr,ms,mt,my,ne,nl,nn,no,oc,pa,pl,ps,pt,ro,ru,sa,sd,si,sk,sl,sn,so,sq,sr,su,sv,sw,ta,te,tg,th,tk,tl,tr,tt,uk,ur,uz,vi,yi,yo,zh".split( + "," +) + + +def create_tokenizer(lan): + """returns an object that has split function that works like the one of MosesTokenizer""" + + assert lan in WHISPER_LANG_CODES, ( + "language must be Whisper's supported lang code: " + + " ".join(WHISPER_LANG_CODES) + ) + + if lan == "uk": + import tokenize_uk + + class UkrainianTokenizer: + def split(self, text): + return tokenize_uk.tokenize_sents(text) + + return UkrainianTokenizer() + + # supported by fast-mosestokenizer + if ( + lan + in "as bn ca cs de el en es et fi fr ga gu hi hu is it kn lt lv ml mni mr nl or pa pl pt ro ru sk sl sv ta te yue zh".split() + ): + from mosestokenizer import MosesTokenizer + + return MosesTokenizer(lan) + + # the following languages are in Whisper, but not in wtpsplit: + if ( + lan + in "as ba bo br bs fo haw hr ht jw lb ln lo mi nn oc sa sd sn so su sw tk tl tt".split() + ): + logger.debug( + f"{lan} code is not supported by wtpsplit. Going to use None lang_code option." + ) + lan = None + + from wtpsplit import WtP + + # downloads the model from huggingface on the first use + wtp = WtP("wtp-canine-s-12l-no-adapters") + + class WtPtok: + def split(self, sent): + return wtp.split(sent, lang_code=lan) + + return WtPtok() + + +def add_shared_args(parser): + """shared args for simulation (this entry point) and server + parser: argparse.ArgumentParser object + """ + parser.add_argument( + "--min-chunk-size", + type=float, + default=1.0, + help="Minimum audio chunk size in seconds. It waits up to this time to do processing. If the processing takes shorter time, it waits, otherwise it processes the whole segment that was received by this time.", + ) + parser.add_argument( + "--model", + type=str, + default="large-v2", + choices="tiny.en,tiny,base.en,base,small.en,small,medium.en,medium,large-v1,large-v2,large-v3,large,large-v3-turbo".split( + "," + ), + help="Name size of the Whisper model to use (default: large-v2). The model is automatically downloaded from the model hub if not present in model cache dir.", + ) + parser.add_argument( + "--model_cache_dir", + type=str, + default=None, + help="Overriding the default model cache dir where models downloaded from the hub are saved", + ) + parser.add_argument( + "--model_dir", + type=str, + default=None, + help="Dir where Whisper model.bin and other files are saved. This option overrides --model and --model_cache_dir parameter.", + ) + parser.add_argument( + "--lan", + "--language", + type=str, + default="auto", + help="Source language code, e.g. en,de,cs, or 'auto' for language detection.", + ) + parser.add_argument( + "--task", + type=str, + default="transcribe", + choices=["transcribe", "translate"], + help="Transcribe or translate.", + ) + parser.add_argument( + "--backend", + type=str, + default="faster-whisper", + choices=["faster-whisper", "whisper_timestamped", "mlx-whisper", "openai-api"], + help="Load only this backend for Whisper processing.", + ) + parser.add_argument( + "--vac", + action="store_true", + default=False, + help="Use VAC = voice activity controller. Recommended. Requires torch.", + ) + parser.add_argument( + "--vac-chunk-size", type=float, default=0.04, help="VAC sample size in seconds." + ) + parser.add_argument( + "--vad", + action="store_true", + default=False, + help="Use VAD = voice activity detection, with the default parameters.", + ) + parser.add_argument( + "--buffer_trimming", + type=str, + default="segment", + choices=["sentence", "segment"], + help='Buffer trimming strategy -- trim completed sentences marked with punctuation mark and detected by sentence segmenter, or the completed segments returned by Whisper. Sentence segmenter must be installed for "sentence" option.', + ) + parser.add_argument( + "--buffer_trimming_sec", + type=float, + default=15, + help="Buffer trimming length threshold in seconds. If buffer length is longer, trimming sentence/segment is triggered.", + ) + parser.add_argument( + "-l", + "--log-level", + dest="log_level", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help="Set the log level", + default="DEBUG", + ) + + +def asr_factory(args, logfile=sys.stderr): + """ + Creates and configures an ASR and ASR Online instance based on the specified backend and arguments. + """ + backend = args.backend + if backend == "openai-api": + logger.debug("Using OpenAI API.") + asr = OpenaiApiASR(lan=args.lan) + else: + if backend == "faster-whisper": + asr_cls = FasterWhisperASR + elif backend == "mlx-whisper": + asr_cls = MLXWhisper + else: + asr_cls = WhisperTimestampedASR + + # Only for FasterWhisperASR and WhisperTimestampedASR + size = args.model + t = time.time() + logger.info(f"Loading Whisper {size} model for {args.lan}...") + asr = asr_cls( + modelsize=size, + lan=args.lan, + cache_dir=args.model_cache_dir, + model_dir=args.model_dir, + ) + e = time.time() + logger.info(f"done. It took {round(e - t, 2)} seconds.") + + # Apply common configurations + if getattr(args, "vad", False): # Checks if VAD argument is present and True + logger.info("Setting VAD filter") + asr.use_vad() + + language = args.lan + if args.task == "translate": + asr.set_translate_task() + tgt_language = "en" # Whisper translates into English + else: + tgt_language = language # Whisper transcribes in this language + + # Create the tokenizer + if args.buffer_trimming == "sentence": + tokenizer = create_tokenizer(tgt_language) + else: + tokenizer = None + + # Create the OnlineASRProcessor + if args.vac: + online = VACOnlineASRProcessor( + args.min_chunk_size, + asr, + tokenizer, + logfile=logfile, + buffer_trimming=(args.buffer_trimming, args.buffer_trimming_sec), + ) + else: + online = OnlineASRProcessor( + asr, + tokenizer, + logfile=logfile, + buffer_trimming=(args.buffer_trimming, args.buffer_trimming_sec), + ) + + return asr, online + + +def set_logging(args, logger, other="_server"): + logging.basicConfig( # format='%(name)s + format="%(levelname)s\t%(message)s" + ) + logger.setLevel(args.log_level) + logging.getLogger("whisper_online" + other).setLevel(args.log_level) + + +# logging.getLogger("whisper_online_server").setLevel(args.log_level) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "audio_path", + type=str, + help="Filename of 16kHz mono channel wav, on which live streaming is simulated.", + ) + add_shared_args(parser) + parser.add_argument( + "--start_at", + type=float, + default=0.0, + help="Start processing audio at this time.", + ) + parser.add_argument( + "--offline", action="store_true", default=False, help="Offline mode." + ) + parser.add_argument( + "--comp_unaware", + action="store_true", + default=False, + help="Computationally unaware simulation.", + ) + + args = parser.parse_args() + + # reset to store stderr to different file stream, e.g. open(os.devnull,"w") + logfile = sys.stderr + + if args.offline and args.comp_unaware: + logger.error( + "No or one option from --offline and --comp_unaware are available, not both. Exiting." + ) + sys.exit(1) + + # if args.log_level: + # logging.basicConfig(format='whisper-%(levelname)s:%(name)s: %(message)s', + # level=getattr(logging, args.log_level)) + + set_logging(args, logger) + + audio_path = args.audio_path + + SAMPLING_RATE = 16000 + duration = len(load_audio(audio_path)) / SAMPLING_RATE + logger.info("Audio duration is: %2.2f seconds", duration) + + asr, online = asr_factory(args, logfile=logfile) + if args.vac: + min_chunk = args.vac_chunk_size + else: + min_chunk = args.min_chunk_size + + # load the audio into the LRU cache before we start the timer + a = load_audio_chunk(audio_path, 0, 1) + + # warm up the ASR because the very first transcribe takes much more time than the other + asr.transcribe(a) + + beg = args.start_at + start = time.time() - beg + + def output_transcript(o, now=None): + # output format in stdout is like: + # 4186.3606 0 1720 Takhle to je + # - the first three words are: + # - emission time from beginning of processing, in milliseconds + # - beg and end timestamp of the text segment, as estimated by Whisper model. The timestamps are not accurate, but they're useful anyway + # - the next words: segment transcript + if now is None: + now = time.time() - start + if o[0] is not None: + print( + "%1.4f %1.0f %1.0f %s" % (now * 1000, o[0] * 1000, o[1] * 1000, o[2]), + file=logfile, + flush=True, + ) + print( + "%1.4f %1.0f %1.0f %s" % (now * 1000, o[0] * 1000, o[1] * 1000, o[2]), + flush=True, + ) + else: + # No text, so no output + pass + + if args.offline: ## offline mode processing (for testing/debugging) + a = load_audio(audio_path) + online.insert_audio_chunk(a) + try: + o = online.process_iter() + except AssertionError as e: + logger.error(f"assertion error: {repr(e)}") + else: + output_transcript(o) + now = None + elif args.comp_unaware: # computational unaware mode + end = beg + min_chunk + while True: + a = load_audio_chunk(audio_path, beg, end) + online.insert_audio_chunk(a) + try: + o = online.process_iter() + except AssertionError as e: + logger.error(f"assertion error: {repr(e)}") + pass + else: + output_transcript(o, now=end) + + logger.debug(f"## last processed {end:.2f}s") + + if end >= duration: + break + + beg = end + + if end + min_chunk > duration: + end = duration + else: + end += min_chunk + now = duration + + else: # online = simultaneous mode + end = 0 + while True: + now = time.time() - start + if now < end + min_chunk: + time.sleep(min_chunk + end - now) + end = time.time() - start + a = load_audio_chunk(audio_path, beg, end) + beg = end + online.insert_audio_chunk(a) + + try: + o = online.process_iter() + except AssertionError as e: + logger.error(f"assertion error: {e}") + pass + else: + output_transcript(o) + now = time.time() - start + logger.debug( + f"## last processed {end:.2f} s, now is {now:.2f}, the latency is {now - end:.2f}" + ) + + if end >= duration: + break + now = None + + o = online.finish() + output_transcript(o, now=now) diff --git a/frigate/data_processing/types.py b/frigate/data_processing/types.py new file mode 100644 index 0000000..5cd1f50 --- /dev/null +++ b/frigate/data_processing/types.py @@ -0,0 +1,69 @@ +"""Embeddings types.""" + +from __future__ import annotations + +from enum import Enum +from multiprocessing.managers import DictProxy, SyncManager, ValueProxy +from typing import Any + +import sherpa_onnx + +from frigate.data_processing.real_time.whisper_online import FasterWhisperASR + + +class DataProcessorMetrics: + image_embeddings_speed: ValueProxy[float] + image_embeddings_eps: ValueProxy[float] + text_embeddings_speed: ValueProxy[float] + text_embeddings_eps: ValueProxy[float] + face_rec_speed: ValueProxy[float] + face_rec_fps: ValueProxy[float] + alpr_speed: ValueProxy[float] + alpr_pps: ValueProxy[float] + yolov9_lpr_speed: ValueProxy[float] + yolov9_lpr_pps: ValueProxy[float] + review_desc_speed: ValueProxy[float] + review_desc_dps: ValueProxy[float] + object_desc_speed: ValueProxy[float] + object_desc_dps: ValueProxy[float] + classification_speeds: DictProxy[str, ValueProxy[float]] + classification_cps: DictProxy[str, ValueProxy[float]] + + def __init__(self, manager: SyncManager, custom_classification_models: list[str]): + self.image_embeddings_speed = manager.Value("d", 0.0) + self.image_embeddings_eps = manager.Value("d", 0.0) + self.text_embeddings_speed = manager.Value("d", 0.0) + self.text_embeddings_eps = manager.Value("d", 0.0) + self.face_rec_speed = manager.Value("d", 0.0) + self.face_rec_fps = manager.Value("d", 0.0) + self.alpr_speed = manager.Value("d", 0.0) + self.alpr_pps = manager.Value("d", 0.0) + self.yolov9_lpr_speed = manager.Value("d", 0.0) + self.yolov9_lpr_pps = manager.Value("d", 0.0) + self.review_desc_speed = manager.Value("d", 0.0) + self.review_desc_dps = manager.Value("d", 0.0) + self.object_desc_speed = manager.Value("d", 0.0) + self.object_desc_dps = manager.Value("d", 0.0) + self.classification_speeds = manager.dict() + self.classification_cps = manager.dict() + + if custom_classification_models: + for key in custom_classification_models: + self.classification_speeds[key] = manager.Value("d", 0.0) + self.classification_cps[key] = manager.Value("d", 0.0) + + +class DataProcessorModelRunner: + def __init__(self, requestor: Any, device: str = "CPU", model_size: str = "large"): + self.requestor = requestor + self.device = device + self.model_size = model_size + + +class PostProcessDataEnum(str, Enum): + recording = "recording" + review = "review" + tracked_object = "tracked_object" + + +AudioTranscriptionModel = FasterWhisperASR | sherpa_onnx.OnlineRecognizer | None diff --git a/frigate/db/sqlitevecq.py b/frigate/db/sqlitevecq.py new file mode 100644 index 0000000..a72e99b --- /dev/null +++ b/frigate/db/sqlitevecq.py @@ -0,0 +1,72 @@ +import re +import sqlite3 +from typing import Any + +from playhouse.sqliteq import SqliteQueueDatabase + + +class SqliteVecQueueDatabase(SqliteQueueDatabase): + def __init__( + self, *args: Any, load_vec_extension: bool = False, **kwargs: Any + ) -> None: + self.load_vec_extension: bool = load_vec_extension + # no extension necessary, sqlite will load correctly for each platform + self.sqlite_vec_path = "/usr/local/lib/vec0" + super().__init__(*args, **kwargs) + + def _connect(self, *args: Any, **kwargs: Any) -> sqlite3.Connection: + conn: sqlite3.Connection = super()._connect(*args, **kwargs) # type: ignore[misc] + if self.load_vec_extension: + self._load_vec_extension(conn) + + # register REGEXP support + self._register_regexp(conn) + + return conn + + def _load_vec_extension(self, conn: sqlite3.Connection) -> None: + conn.enable_load_extension(True) + conn.load_extension(self.sqlite_vec_path) + conn.enable_load_extension(False) + + def _register_regexp(self, conn: sqlite3.Connection) -> None: + def regexp(expr: str, item: str | None) -> bool: + if item is None: + return False + try: + return re.search(expr, item) is not None + except re.error: + return False + + conn.create_function("REGEXP", 2, regexp) + + def delete_embeddings_thumbnail(self, event_ids: list[str]) -> None: + ids = ",".join(["?" for _ in event_ids]) + self.execute_sql(f"DELETE FROM vec_thumbnails WHERE id IN ({ids})", event_ids) + + def delete_embeddings_description(self, event_ids: list[str]) -> None: + ids = ",".join(["?" for _ in event_ids]) + self.execute_sql(f"DELETE FROM vec_descriptions WHERE id IN ({ids})", event_ids) + + def drop_embeddings_tables(self) -> None: + self.execute_sql(""" + DROP TABLE vec_descriptions; + """) + self.execute_sql(""" + DROP TABLE vec_thumbnails; + """) + + def create_embeddings_tables(self) -> None: + """Create vec0 virtual table for embeddings""" + self.execute_sql(""" + CREATE VIRTUAL TABLE IF NOT EXISTS vec_thumbnails USING vec0( + id TEXT PRIMARY KEY, + thumbnail_embedding FLOAT[768] distance_metric=cosine + ); + """) + self.execute_sql(""" + CREATE VIRTUAL TABLE IF NOT EXISTS vec_descriptions USING vec0( + id TEXT PRIMARY KEY, + description_embedding FLOAT[768] distance_metric=cosine + ); + """) diff --git a/frigate/debug_replay.py b/frigate/debug_replay.py new file mode 100644 index 0000000..17a727c --- /dev/null +++ b/frigate/debug_replay.py @@ -0,0 +1,398 @@ +"""Debug replay camera management for replaying recordings with detection overlays. + +The startup work (ffmpeg concat + camera config publish) lives in +frigate.jobs.debug_replay. This module owns only session presence +(active), session metadata, and post-session cleanup. +""" + +import asyncio +import logging +import os +import shutil +import threading +import time + +from ruamel.yaml import YAML + +from frigate.config import FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdatePublisher, + CameraConfigUpdateTopic, +) +from frigate.const import ( + CLIPS_DIR, + RECORD_DIR, + REPLAY_CAMERA_PREFIX, + REPLAY_DIR, + THUMB_DIR, +) +from frigate.jobs.debug_replay import ( + JOB_TYPE as DEBUG_REPLAY_JOB_TYPE, +) +from frigate.jobs.debug_replay import ( + cancel_debug_replay_job, + wait_for_runner, +) +from frigate.jobs.export import JobStatePublisher +from frigate.types import JobStatusTypesEnum +from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files +from frigate.util.config import find_config_file + +logger = logging.getLogger(__name__) + +MAX_SESSION_DURATION_SECONDS = 12 * 60 * 60 +AUTO_STOP_CHECK_INTERVAL_SECONDS = 60 + + +class DebugReplayManager: + """Owns the lifecycle pointers for a single debug replay session. + + A session exists from the moment mark_starting is called (synchronously, + inside the API handler) until clear_session runs (on success cleanup, + failure, or stop). The active property is the source of truth that the + status bar consumes — broader than the startup job, which only covers the + preparing_clip / starting_camera window. + """ + + def __init__(self) -> None: + self._lock = threading.Lock() + self.replay_camera_name: str | None = None + self.source_camera: str | None = None + self.clip_path: str | None = None + self.start_ts: float | None = None + self.end_ts: float | None = None + self.session_started_at: float | None = None + self._job_state_publisher = JobStatePublisher() + + @property + def active(self) -> bool: + """True from mark_starting until clear_session.""" + return self.replay_camera_name is not None + + def mark_starting( + self, + source_camera: str, + replay_camera_name: str, + start_ts: float, + end_ts: float, + ) -> None: + """Synchronously claim the session before the job runner starts. + + Called inside the API handler so the status bar sees active=True + immediately, before the worker thread does any ffmpeg work. + """ + with self._lock: + self.replay_camera_name = replay_camera_name + self.source_camera = source_camera + self.start_ts = start_ts + self.end_ts = end_ts + self.clip_path = None + self.session_started_at = time.time() + + def mark_session_ready(self, clip_path: str) -> None: + """Record the on-disk clip path after the camera has been published.""" + with self._lock: + self.clip_path = clip_path + + def clear_session(self) -> None: + """Reset session pointers without publishing camera removal. + + Used by the job runner on failure paths. stop() does the camera + teardown plus this clear in one step. + """ + with self._lock: + self._clear_locked() + + def _clear_locked(self) -> None: + self.replay_camera_name = None + self.source_camera = None + self.clip_path = None + self.start_ts = None + self.end_ts = None + self.session_started_at = None + + def publish_camera( + self, + source_camera: str, + replay_name: str, + clip_path: str, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + ) -> None: + """Build the in-memory replay camera config and publish the add event. + + Called by the job runner during the starting_camera phase. + """ + source_config = frigate_config.cameras[source_camera] + camera_dict = self._build_camera_config_dict( + source_config, replay_name, clip_path + ) + + config_file = find_config_file() + yaml_parser = YAML() + with open(config_file) as f: + config_data = yaml_parser.load(f) + + if "cameras" not in config_data or config_data["cameras"] is None: + config_data["cameras"] = {} + config_data["cameras"][replay_name] = camera_dict + + try: + new_config = FrigateConfig.parse_object(config_data) + except Exception as e: + raise RuntimeError(f"Failed to validate replay camera config: {e}") from e + frigate_config.cameras[replay_name] = new_config.cameras[replay_name] + + config_publisher.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.add, replay_name), + new_config.cameras[replay_name], + ) + + def stop( + self, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + ) -> None: + """Cancel any in-flight startup job and tear down the active session. + + Safe to call when no session is active (no-op with a warning). + """ + cancel_debug_replay_job() + wait_for_runner(timeout=2.0) + + with self._lock: + if not self.active: + logger.warning("No active replay session to stop") + return + + replay_name = self.replay_camera_name + source_camera = self.source_camera + + # Only publish remove if the camera was actually added to the live + # config (i.e. the runner reached the starting_camera phase). + if replay_name is not None and replay_name in frigate_config.cameras: + config_publisher.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, replay_name), + frigate_config.cameras[replay_name], + ) + frigate_config.cameras.pop(replay_name, None) + + if replay_name is not None: + self._cleanup_db(replay_name) + self._cleanup_files(replay_name) + + self._job_state_publisher.publish( + { + "id": "stopped", + "job_type": DEBUG_REPLAY_JOB_TYPE, + "status": JobStatusTypesEnum.cancelled, + "start_time": None, + "end_time": time.time(), + "error_message": None, + "results": { + "source_camera": source_camera, + "replay_camera_name": replay_name, + }, + } + ) + + self._clear_locked() + + logger.info("Debug replay stopped and cleaned up: %s", replay_name) + + def _build_camera_config_dict( + self, + source_config, + replay_name: str, + clip_path: str, + ) -> dict: + """Build a camera config dictionary for the replay camera.""" + # Extract detect config (exclude computed fields) + detect_dict = source_config.detect.model_dump( + exclude={"min_initialized", "max_disappeared", "enabled_in_config"} + ) + + # Extract objects config, using .dict() on filters to convert + # RuntimeFilterConfig ndarray masks back to string coordinates + objects_dict = { + "track": source_config.objects.track, + "mask": { + mask_id: ( + mask_cfg.model_dump( + exclude={"raw_coordinates", "enabled_in_config"} + ) + if mask_cfg is not None + else None + ) + for mask_id, mask_cfg in source_config.objects.mask.items() + } + if source_config.objects.mask + else {}, + "filters": { + name: filt.dict() if hasattr(filt, "dict") else filt.model_dump() + for name, filt in source_config.objects.filters.items() + }, + } + + # Extract zones (exclude_defaults avoids serializing empty defaults + # like distances=[] that fail validation on re-parse) + zones_dict = {} + for zone_name, zone_config in source_config.zones.items(): + zone_dump = zone_config.model_dump( + exclude={"contour", "color"}, exclude_defaults=True + ) + zone_dump.setdefault("coordinates", zone_config.coordinates) + zones_dict[zone_name] = zone_dump + + # Extract LPR and face recognition configs + lpr_dict = source_config.lpr.model_dump() + face_recognition_dict = source_config.face_recognition.model_dump() + + # Extract motion config (exclude runtime fields) + motion_dict = {} + if source_config.motion is not None: + motion_dict = source_config.motion.model_dump( + exclude={ + "frame_shape", + "raw_mask", + "mask", + "enabled_in_config", + "rasterized_mask", + } + ) + + if source_config.motion.mask: + motion_dict["mask"] = { + mask_id: ( + mask_cfg.model_dump( + exclude={"raw_coordinates", "enabled_in_config"} + ) + if mask_cfg is not None + else None + ) + for mask_id, mask_cfg in source_config.motion.mask.items() + } + + return { + "enabled": True, + "ffmpeg": { + "inputs": [ + { + "path": clip_path, + "roles": ["detect"], + "input_args": "-re -stream_loop -1 -fflags +genpts", + } + ], + "hwaccel_args": [], + }, + "detect": detect_dict, + "objects": objects_dict, + "zones": zones_dict, + "motion": motion_dict, + "record": {"enabled": False}, + "snapshots": {"enabled": False}, + "review": { + "alerts": {"enabled": False}, + "detections": {"enabled": False}, + }, + "birdseye": {"enabled": False}, + "audio": {"enabled": False}, + "lpr": lpr_dict, + "face_recognition": face_recognition_dict, + } + + def _cleanup_db(self, camera_name: str) -> None: + """Defensively remove any database rows for the replay camera.""" + cleanup_camera_db(camera_name) + + def _cleanup_files(self, camera_name: str) -> None: + """Remove filesystem artifacts for the replay camera.""" + cleanup_camera_files(camera_name) + + # Remove replay-specific cache directory + if os.path.exists(REPLAY_DIR): + try: + shutil.rmtree(REPLAY_DIR) + logger.debug("Removed replay cache directory") + except Exception as e: + logger.error("Failed to remove replay cache: %s", e) + + +def cleanup_replay_cameras() -> None: + """Remove any stale replay camera artifacts on startup. + + Since replay cameras are memory-only and never written to YAML, they + won't appear in the config after a restart. This function cleans up + filesystem and database artifacts from any replay that was running when + the process stopped. + + Must be called AFTER the database is bound. + """ + stale_cameras: set[str] = set() + + # Scan filesystem for leftover replay artifacts to derive camera names + for dir_path in [RECORD_DIR, CLIPS_DIR, THUMB_DIR]: + if os.path.isdir(dir_path): + for entry in os.listdir(dir_path): + if entry.startswith(REPLAY_CAMERA_PREFIX): + stale_cameras.add(entry) + + if os.path.isdir(REPLAY_DIR): + for entry in os.listdir(REPLAY_DIR): + if entry.startswith(REPLAY_CAMERA_PREFIX) and entry.endswith(".mp4"): + stale_cameras.add(entry.removesuffix(".mp4")) + + if not stale_cameras: + return + + logger.info("Cleaning up stale replay camera artifacts: %s", list(stale_cameras)) + + manager = DebugReplayManager() + for camera_name in stale_cameras: + manager._cleanup_db(camera_name) + manager._cleanup_files(camera_name) + + if os.path.exists(REPLAY_DIR): + try: + shutil.rmtree(REPLAY_DIR) + except Exception as e: + logger.error("Failed to remove replay cache directory: %s", e) + + +async def debug_replay_auto_stop_watchdog( + manager: DebugReplayManager, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, +) -> None: + """Auto-stop debug replay sessions that exceed MAX_SESSION_DURATION_SECONDS. + + Backstop against a session left running for days. The cap is intentionally + generous so realistic tuning and overnight soak workflows aren't disrupted. + """ + while True: + try: + await asyncio.sleep(AUTO_STOP_CHECK_INTERVAL_SECONDS) + + started_at = manager.session_started_at + if not manager.active or started_at is None: + continue + + if time.time() - started_at < MAX_SESSION_DURATION_SECONDS: + continue + + replay_name = manager.replay_camera_name + await asyncio.to_thread( + manager.stop, + frigate_config=frigate_config, + config_publisher=config_publisher, + ) + logger.info( + "Debug replay auto-stopped after exceeding max session duration of %d hours: %s", + MAX_SESSION_DURATION_SECONDS // 3600, + replay_name, + ) + except asyncio.CancelledError: + raise + except Exception: + logger.exception("Error in debug replay auto-stop watchdog") diff --git a/frigate/detectors/__init__.py b/frigate/detectors/__init__.py new file mode 100644 index 0000000..7465ed7 --- /dev/null +++ b/frigate/detectors/__init__.py @@ -0,0 +1,18 @@ +import logging + +from .detector_config import InputTensorEnum, ModelConfig, PixelFormatEnum # noqa: F401 +from .detector_types import DetectorConfig, DetectorTypeEnum, api_types # noqa: F401 + +logger = logging.getLogger(__name__) + + +def create_detector(detector_config): + if detector_config.type == DetectorTypeEnum.cpu: + logger.warning( + "CPU detectors are not recommended and should only be used for testing or for trial purposes." + ) + + api = api_types.get(detector_config.type) + if not api: + raise ValueError(detector_config.type) + return api(detector_config) diff --git a/frigate/detectors/detection_api.py b/frigate/detectors/detection_api.py new file mode 100644 index 0000000..b03d607 --- /dev/null +++ b/frigate/detectors/detection_api.py @@ -0,0 +1,56 @@ +import logging +from abc import ABC, abstractmethod + +import numpy as np + +from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum + +logger = logging.getLogger(__name__) + + +class DetectionApi(ABC): + type_key: str + supported_models: list[ModelTypeEnum] + + @abstractmethod + def __init__(self, detector_config: BaseDetectorConfig): + self.detector_config = detector_config + self.thresh = 0.4 + self.height = detector_config.model.height + self.width = detector_config.model.width + + @abstractmethod + def detect_raw(self, tensor_input): + pass + + def calculate_grids_strides(self, expanded=True) -> None: + grids = [] + expanded_strides = [] + + # decode and orient predictions + strides = [8, 16, 32] + hsizes = [self.height // stride for stride in strides] + wsizes = [self.width // stride for stride in strides] + + for hsize, wsize, stride in zip(hsizes, wsizes, strides): + xv, yv = np.meshgrid(np.arange(wsize), np.arange(hsize)) + + if expanded: + grid = np.stack((xv, yv), 2).reshape(1, -1, 2) + grids.append(grid) + shape = grid.shape[:2] + expanded_strides.append(np.full((*shape, 1), stride)) + else: + xv = xv.reshape(1, 1, hsize, wsize) + yv = yv.reshape(1, 1, hsize, wsize) + grids.extend(np.concatenate((xv, yv), axis=1).tolist()) + expanded_strides.extend( + np.array([stride, stride]).reshape(1, 2, 1, 1).tolist() + ) + + if expanded: + self.grids = np.concatenate(grids, 1) + self.expanded_strides = np.concatenate(expanded_strides, 1) + else: + self.grids = grids + self.expanded_strides = expanded_strides diff --git a/frigate/detectors/detection_runners.py b/frigate/detectors/detection_runners.py new file mode 100644 index 0000000..ee30f4f --- /dev/null +++ b/frigate/detectors/detection_runners.py @@ -0,0 +1,636 @@ +"""Base runner implementation for ONNX models.""" + +import logging +import os +import platform +import threading +from abc import ABC, abstractmethod +from typing import Any + +import numpy as np +import onnxruntime as ort + +from frigate.util.model import get_ort_providers +from frigate.util.rknn_converter import auto_convert_model, is_rknn_compatible + +logger = logging.getLogger(__name__) + +# Process-wide lock serializing all OpenVINO compile/inference calls +_OPENVINO_LOCK = threading.Lock() + + +def is_arm64_platform() -> bool: + """Check if we're running on an ARM platform.""" + machine = platform.machine().lower() + return machine in ("aarch64", "arm64", "armv8", "armv7l") + + +def get_ort_session_options( + is_complex_model: bool = False, +) -> ort.SessionOptions | None: + """Get ONNX Runtime session options with appropriate settings. + + Args: + is_complex_model: Whether the model needs basic optimization to avoid graph fusion issues. + + Returns: + SessionOptions with appropriate optimization level, or None for default settings. + """ + if is_complex_model: + sess_options = ort.SessionOptions() + sess_options.graph_optimization_level = ( + ort.GraphOptimizationLevel.ORT_ENABLE_BASIC + ) + return sess_options + + return None + + +# Import OpenVINO only when needed to avoid circular dependencies +try: + import openvino as ov +except ImportError: + ov = None + + +def get_openvino_available_devices() -> list[str]: + """Get available OpenVINO devices without using ONNX Runtime. + + Returns: + List of available OpenVINO device names (e.g., ['CPU', 'GPU', 'MYRIAD']) + """ + if ov is None: + logger.debug("OpenVINO is not available") + return [] + + try: + core = ov.Core() + available_devices = core.available_devices + logger.debug(f"OpenVINO available devices: {available_devices}") + return available_devices + except Exception as e: + logger.warning(f"Failed to get OpenVINO available devices: {e}") + return [] + + +def is_openvino_gpu_npu_available() -> bool: + """Check if OpenVINO GPU or NPU devices are available. + + Returns: + True if GPU or NPU devices are available, False otherwise + """ + available_devices = get_openvino_available_devices() + # Check for GPU, NPU, or other acceleration devices (excluding CPU) + acceleration_devices = ["GPU", "MYRIAD", "NPU", "GNA", "HDDL"] + return any( + avail_dev == accel_dev or avail_dev.startswith(accel_dev + ".") + for avail_dev in available_devices + for accel_dev in acceleration_devices + ) + + +class BaseModelRunner(ABC): + """Abstract base class for model runners.""" + + def __init__(self, model_path: str, device: str, **kwargs): + self.model_path = model_path + self.device = device + + @abstractmethod + def get_input_names(self) -> list[str]: + """Get input names for the model.""" + pass + + @abstractmethod + def get_input_width(self) -> int: + """Get the input width of the model.""" + pass + + @abstractmethod + def run(self, input: dict[str, Any]) -> Any | None: + """Run inference with the model.""" + pass + + +class ONNXModelRunner(BaseModelRunner): + """Run ONNX models using ONNX Runtime.""" + + @staticmethod + def is_cpu_complex_model(model_type: str) -> bool: + """Check if model needs basic optimization level to avoid graph fusion issues. + + Some models (like Jina-CLIP) have issues with aggressive optimizations like + SimplifiedLayerNormFusion that create or expect nodes that don't exist. + """ + # Import here to avoid circular imports + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type in [ + EnrichmentModelTypeEnum.jina_v1.value, + EnrichmentModelTypeEnum.jina_v2.value, + ] + + @staticmethod + def is_migraphx_complex_model(model_type: str) -> bool: + # Import here to avoid circular imports + from frigate.detectors.detector_config import ModelTypeEnum + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type in [ + EnrichmentModelTypeEnum.paddleocr.value, + EnrichmentModelTypeEnum.jina_v2.value, + ModelTypeEnum.rfdetr.value, + ModelTypeEnum.dfine.value, + ] + + @staticmethod + def is_concurrent_model(model_type: str | None) -> bool: + """Check if model requires thread locking for concurrent inference. + + Some models (like JinaV2) share one runner between text and vision embeddings + called from different threads, requiring thread synchronization. + """ + if not model_type: + return False + + # Import here to avoid circular imports + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type == EnrichmentModelTypeEnum.jina_v2.value + + def __init__(self, ort: ort.InferenceSession, model_type: str | None = None): + self.ort = ort + self.model_type = model_type + + # Thread lock to prevent concurrent inference (needed for JinaV2 which shares + # one runner between text and vision embeddings called from different threads) + if self.is_concurrent_model(model_type): + self._inference_lock = threading.Lock() + else: + self._inference_lock = None + + def get_input_names(self) -> list[str]: + return [input.name for input in self.ort.get_inputs()] + + def get_input_width(self) -> int: + """Get the input width of the model.""" + return self.ort.get_inputs()[0].shape[3] + + def run(self, input: dict[str, Any]) -> Any | None: + if self._inference_lock: + with self._inference_lock: + return self.ort.run(None, input) + + return self.ort.run(None, input) + + +class CudaGraphRunner(BaseModelRunner): + """Encapsulates CUDA Graph capture and replay using ONNX Runtime IOBinding. + + This runner assumes a single tensor input and binds all model outputs. + + NOTE: CUDA Graphs limit supported model operations, so they are not usable + for more complex models like CLIP or PaddleOCR. + """ + + @staticmethod + def is_model_supported(model_type: str) -> bool: + # Import here to avoid circular imports + from frigate.detectors.detector_config import ModelTypeEnum + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type not in [ + ModelTypeEnum.yolonas.value, + ModelTypeEnum.dfine.value, + EnrichmentModelTypeEnum.paddleocr.value, + EnrichmentModelTypeEnum.jina_v1.value, + EnrichmentModelTypeEnum.jina_v2.value, + EnrichmentModelTypeEnum.yolov9_license_plate.value, + ] + + def __init__(self, session: ort.InferenceSession, cuda_device_id: int): + self._session = session + self._cuda_device_id = cuda_device_id + self._captured = False + self._io_binding: ort.IOBinding | None = None + self._input_name: str | None = None + self._output_names: list[str] | None = None + self._input_ortvalue: ort.OrtValue | None = None + self._output_ortvalues: ort.OrtValue | None = None + + def get_input_names(self) -> list[str]: + """Get input names for the model.""" + return [input.name for input in self._session.get_inputs()] + + def get_input_width(self) -> int: + """Get the input width of the model.""" + return self._session.get_inputs()[0].shape[3] + + def run(self, input: dict[str, Any]): + # Extract the single tensor input (assuming one input) + input_name = list(input.keys())[0] + tensor_input = input[input_name] + tensor_input = np.ascontiguousarray(tensor_input) + + if not self._captured: + # Prepare IOBinding with CUDA buffers and let ORT allocate outputs on device + self._io_binding = self._session.io_binding() + self._input_name = input_name + self._output_names = [o.name for o in self._session.get_outputs()] + + self._input_ortvalue = ort.OrtValue.ortvalue_from_numpy( + tensor_input, "cuda", self._cuda_device_id + ) + self._io_binding.bind_ortvalue_input(self._input_name, self._input_ortvalue) + + for name in self._output_names: + # Bind outputs to CUDA and allow ORT to allocate appropriately + self._io_binding.bind_output(name, "cuda", self._cuda_device_id) + + # First IOBinding run to allocate, execute, and capture CUDA Graph + ro = ort.RunOptions() + self._session.run_with_iobinding(self._io_binding, ro) + self._captured = True + return self._io_binding.copy_outputs_to_cpu() + + # Replay using updated input, copy results to CPU + self._input_ortvalue.update_inplace(tensor_input) + ro = ort.RunOptions() + self._session.run_with_iobinding(self._io_binding, ro) + return self._io_binding.copy_outputs_to_cpu() + + +class OpenVINOModelRunner(BaseModelRunner): + """OpenVINO model runner that handles inference efficiently.""" + + @staticmethod + def is_complex_model(model_type: str) -> bool: + # Import here to avoid circular imports + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type in [ + EnrichmentModelTypeEnum.paddleocr.value, + EnrichmentModelTypeEnum.jina_v2.value, + ] + + @staticmethod + def is_model_npu_supported(model_type: str) -> bool: + # Import here to avoid circular imports + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type not in [ + EnrichmentModelTypeEnum.paddleocr.value, + EnrichmentModelTypeEnum.jina_v1.value, + EnrichmentModelTypeEnum.jina_v2.value, + EnrichmentModelTypeEnum.arcface.value, + ] + + @staticmethod + def is_detection_model(model_type: str) -> bool: + # Import here to avoid circular imports + from frigate.detectors.detector_config import ModelTypeEnum + + return model_type in [m.value for m in ModelTypeEnum] + + def __init__(self, model_path: str, device: str, model_type: str, **kwargs): + self.model_path = model_path + self.device = device + self.model_type = model_type + + if device == "NPU" and not OpenVINOModelRunner.is_model_npu_supported( + model_type + ): + logger.warning( + f"OpenVINO model {model_type} is not supported on NPU, using GPU instead" + ) + device = "GPU" + + self.complex_model = OpenVINOModelRunner.is_complex_model(model_type) + + if not os.path.isfile(model_path): + raise FileNotFoundError(f"OpenVINO model file {model_path} not found.") + + if ov is None: + raise ImportError( + "OpenVINO is not available. Please install openvino package." + ) + + self.ov_core = ov.Core() + + # Apply performance optimization + self.ov_core.set_property(device, {"PERF_COUNT": "NO"}) + + if device in ["GPU", "AUTO", "NPU"]: + self.ov_core.set_property(device, {"PERFORMANCE_HINT": "LATENCY"}) + + if device == "NPU" and OpenVINOModelRunner.is_detection_model(model_type): + try: + self.ov_core.set_property(device, {"NPU_TURBO": "YES"}) + except Exception as e: + logger.debug(f"NPU_TURBO not supported by driver: {e}") + + # Compile model under the shared lock + with _OPENVINO_LOCK: + self.compiled_model = self.ov_core.compile_model( + model=model_path, device_name=device + ) + + # Create reusable inference request + self.infer_request = self.compiled_model.create_infer_request() + + self.input_tensor: ov.Tensor | None = None + + if not self.complex_model: + try: + input_shape = self.compiled_model.inputs[0].get_shape() + input_element_type = self.compiled_model.inputs[0].get_element_type() + self.input_tensor = ov.Tensor(input_element_type, input_shape) + except RuntimeError: + # model is complex and has dynamic shape + pass + + def get_input_names(self) -> list[str]: + """Get input names for the model.""" + return [input.get_any_name() for input in self.compiled_model.inputs] + + def get_input_width(self) -> int: + """Get the input width of the model.""" + input_info = self.compiled_model.inputs + first_input = input_info[0] + + try: + partial_shape = first_input.get_partial_shape() + # width dimension + if len(partial_shape) >= 4 and partial_shape[3].is_static: + return partial_shape[3].get_length() + + # If width is dynamic or we can't determine it + return -1 + except Exception: + try: + # gemini says some ov versions might still allow this + input_shape = first_input.shape + return input_shape[3] if len(input_shape) >= 4 else -1 + except Exception: + return -1 + + def run(self, inputs: dict[str, Any]) -> list[np.ndarray]: + """Run inference with the model. + + Args: + inputs: Dictionary mapping input names to input data + + Returns: + List of output tensors + """ + # Shared lock serializes inference across every OpenVINO runner in this + # process — both the shared-runner JinaV2 case (genai text thread + + # embeddings vision thread) and distinct runners running on separate + # threads (e.g. the ArcFace face-model build vs the LPR detector). + with _OPENVINO_LOCK: + from frigate.embeddings.types import EnrichmentModelTypeEnum + + if self.model_type in [EnrichmentModelTypeEnum.arcface.value]: + # For face recognition models, create a fresh infer_request + # for each inference to avoid state pollution that causes incorrect results. + self.infer_request = self.compiled_model.create_infer_request() + + # Handle single input case for backward compatibility + if ( + len(inputs) == 1 + and len(self.compiled_model.inputs) == 1 + and self.input_tensor is not None + ): + # Single input case - use the pre-allocated tensor for efficiency + input_data = list(inputs.values())[0] + np.copyto(self.input_tensor.data, input_data) + self.infer_request.infer(self.input_tensor) + else: + if self.complex_model: + try: + # This ensures the model starts with a clean state for each sequence + # Important for RNN models like PaddleOCR recognition + self.infer_request.reset_state() + except Exception: + # this will raise an exception for models with AUTO set as the device + pass + + # Multiple inputs case - set each input by name + for input_name, input_data in inputs.items(): + # Find the input by name and its index + input_port = None + input_index = None + for idx, port in enumerate(self.compiled_model.inputs): + if port.get_any_name() == input_name: + input_port = port + input_index = idx + break + + if input_port is None: + raise ValueError(f"Input '{input_name}' not found in model") + + # Create tensor with the correct element type + input_element_type = input_port.get_element_type() + + # Ensure input data matches the expected dtype to prevent type mismatches + # that can occur with models like Jina-CLIP v2 running on OpenVINO + expected_dtype = input_element_type.to_dtype() + if input_data.dtype != expected_dtype: + logger.debug( + f"Converting input '{input_name}' from {input_data.dtype} to {expected_dtype}" + ) + input_data = input_data.astype(expected_dtype) + + input_tensor = ov.Tensor(input_element_type, input_data.shape) + np.copyto(input_tensor.data, input_data) + + # Set the input tensor for the specific port index + self.infer_request.set_input_tensor(input_index, input_tensor) + + # Run inference + try: + self.infer_request.infer() + except Exception as e: + logger.error(f"Error during OpenVINO inference: {e}") + return [] + + # Get all output tensors + outputs = [] + for i in range(len(self.compiled_model.outputs)): + outputs.append(self.infer_request.get_output_tensor(i).data) + + return outputs + + +class RKNNModelRunner(BaseModelRunner): + """Run RKNN models for embeddings.""" + + def __init__(self, model_path: str, model_type: str = None, core_mask: int = 0): + self.model_path = model_path + self.model_type = model_type + self.core_mask = core_mask + self.rknn = None + self._load_model() + + def _load_model(self): + """Load the RKNN model.""" + try: + from rknnlite.api import RKNNLite + + self.rknn = RKNNLite(verbose=False) + + if self.rknn.load_rknn(self.model_path) != 0: + logger.error(f"Failed to load RKNN model: {self.model_path}") + raise RuntimeError("Failed to load RKNN model") + + if self.rknn.init_runtime(core_mask=self.core_mask) != 0: + logger.error("Failed to initialize RKNN runtime") + raise RuntimeError("Failed to initialize RKNN runtime") + + logger.info(f"Successfully loaded RKNN model: {self.model_path}") + + except ImportError: + logger.error("RKNN Lite not available") + raise ImportError("RKNN Lite not available") from None + except Exception as e: + logger.error(f"Error loading RKNN model: {e}") + raise + + def get_input_names(self) -> list[str]: + """Get input names for the model.""" + # For detection models, we typically use "input" as the default input name + # For CLIP models, we need to determine the model type from the path + model_name = os.path.basename(self.model_path).lower() + + if "vision" in model_name: + return ["pixel_values"] + elif "arcface" in model_name: + return ["data"] + else: + # Default fallback - try to infer from model type + if self.model_type and "jina-clip" in self.model_type: + if "vision" in self.model_type: + return ["pixel_values"] + + # Generic fallback + return ["input"] + + def get_input_width(self) -> int: + """Get the input width of the model.""" + # For CLIP vision models, this is typically 224 + model_name = os.path.basename(self.model_path).lower() + if "vision" in model_name: + return 224 # CLIP V1 uses 224x224 + elif "arcface" in model_name: + return 112 + # For detection models, we can't easily determine this from the RKNN model + # The calling code should provide this information + return -1 + + def run(self, inputs: dict[str, Any]) -> Any: + """Run inference with the RKNN model.""" + if not self.rknn: + raise RuntimeError("RKNN model not loaded") + + try: + input_names = self.get_input_names() + rknn_inputs = [] + + for name in input_names: + if name in inputs: + if name == "pixel_values": + # RKNN expects NHWC format, but ONNX typically provides NCHW + # Transpose from [batch, channels, height, width] to [batch, height, width, channels] + pixel_data = inputs[name] + if len(pixel_data.shape) == 4 and pixel_data.shape[1] == 3: + # Transpose from NCHW to NHWC + pixel_data = np.transpose(pixel_data, (0, 2, 3, 1)) + rknn_inputs.append(pixel_data) + elif name == "data": + # ArcFace: undo Python normalisation to uint8 [0,255] + # RKNN runtime applies mean=127.5/std=127.5 internally before first layer + face_data = inputs[name] + if len(face_data.shape) == 4 and face_data.shape[1] == 3: + # Transpose from NCHW to NHWC + face_data = np.transpose(face_data, (0, 2, 3, 1)) + face_data = ( + ((face_data + 1.0) * 127.5).clip(0, 255).astype(np.uint8) + ) + rknn_inputs.append(face_data) + else: + rknn_inputs.append(inputs[name]) + + outputs = self.rknn.inference(inputs=rknn_inputs) + return outputs + + except Exception as e: + logger.error(f"Error during RKNN inference: {e}") + raise + + def __del__(self): + """Cleanup when the runner is destroyed.""" + if self.rknn: + try: + self.rknn.release() + except Exception: + pass + + +def get_optimized_runner( + model_path: str, device: str | None, model_type: str, **kwargs +) -> BaseModelRunner: + """Get an optimized runner for the hardware.""" + device = device or "AUTO" + + if device != "CPU" and is_rknn_compatible(model_path): + rknn_path = auto_convert_model(model_path) + + if rknn_path: + return RKNNModelRunner(rknn_path) + + providers, options = get_ort_providers(device == "CPU", device, **kwargs) + + if providers[0] == "CPUExecutionProvider": + # In the default image, ONNXRuntime is used so we will only get CPUExecutionProvider + # In other images we will get CUDA / ROCm which are preferred over OpenVINO + # There is currently no way to prioritize OpenVINO over CUDA / ROCm in these images + if device != "CPU" and is_openvino_gpu_npu_available(): + return OpenVINOModelRunner(model_path, device, model_type, **kwargs) + + if ( + CudaGraphRunner.is_model_supported(model_type) + and providers[0] == "CUDAExecutionProvider" + ): + options[0] = { + **options[0], + "enable_cuda_graph": True, + } + return CudaGraphRunner( + ort.InferenceSession( + model_path, + providers=providers, + provider_options=options, + ), + options[0]["device_id"], + ) + + if ( + providers + and providers[0] == "MIGraphXExecutionProvider" + and ONNXModelRunner.is_migraphx_complex_model(model_type) + ): + # Don't use MIGraphX for models that are not supported + providers.pop(0) + options.pop(0) + + return ONNXModelRunner( + ort.InferenceSession( + model_path, + sess_options=get_ort_session_options( + ONNXModelRunner.is_cpu_complex_model(model_type) + ), + providers=providers, + provider_options=options, + ), + model_type=model_type, + ) diff --git a/frigate/detectors/detector_config.py b/frigate/detectors/detector_config.py new file mode 100644 index 0000000..788fb58 --- /dev/null +++ b/frigate/detectors/detector_config.py @@ -0,0 +1,255 @@ +import hashlib +import json +import logging +import os +from enum import Enum +from typing import Any + +import requests +from pydantic import BaseModel, ConfigDict, Field +from pydantic.fields import PrivateAttr + +from frigate.const import DEFAULT_ATTRIBUTE_LABEL_MAP, MODEL_CACHE_DIR +from frigate.plus import PlusApi +from frigate.util.builtin import generate_color_palette, load_labels + +logger = logging.getLogger(__name__) + + +class PixelFormatEnum(str, Enum): + rgb = "rgb" + bgr = "bgr" + yuv = "yuv" + + +class InputTensorEnum(str, Enum): + nchw = "nchw" + nhwc = "nhwc" + hwnc = "hwnc" + hwcn = "hwcn" + + +class InputDTypeEnum(str, Enum): + float = "float" + float_denorm = "float_denorm" # non-normalized float + int = "int" + + +class ModelTypeEnum(str, Enum): + dfine = "dfine" + rfdetr = "rfdetr" + ssd = "ssd" + yolox = "yolox" + yolonas = "yolonas" + yologeneric = "yolo-generic" + + +class ModelConfig(BaseModel): + path: str | None = Field( + None, + title="Custom object detector model path", + description="Path to a custom detection model file (or plus:// for Frigate+ models).", + ) + labelmap_path: str | None = Field( + None, + title="Label map for custom object detector", + description="Path to a labelmap file that maps numeric classes to string labels for the detector.", + ) + width: int = Field( + default=320, + title="Object detection model input width", + description="Width of the model input tensor in pixels.", + ) + height: int = Field( + default=320, + title="Object detection model input height", + description="Height of the model input tensor in pixels.", + ) + labelmap: dict[int, str] = Field( + default_factory=dict, + title="Labelmap customization", + description="Overrides or remapping entries to merge into the standard labelmap.", + ) + attributes_map: dict[str, list[str]] = Field( + default=DEFAULT_ATTRIBUTE_LABEL_MAP, + title="Map of object labels to their attribute labels", + description="Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate']).", + ) + input_tensor: InputTensorEnum = Field( + default=InputTensorEnum.nhwc, + title="Model Input Tensor Shape", + description="Tensor format expected by the model: 'nhwc' or 'nchw'.", + ) + input_pixel_format: PixelFormatEnum = Field( + default=PixelFormatEnum.rgb, + title="Model Input Pixel Color Format", + description="Pixel colorspace expected by the model: 'rgb', 'bgr', or 'yuv'.", + ) + input_dtype: InputDTypeEnum = Field( + default=InputDTypeEnum.int, + title="Model Input D Type", + description="Data type of the model input tensor (for example 'float32').", + ) + model_type: ModelTypeEnum = Field( + default=ModelTypeEnum.ssd, + title="Object Detection Model Type", + description="Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization.", + ) + _merged_labelmap: dict[int, str] | None = PrivateAttr() + _colormap: dict[int, tuple[int, int, int]] = PrivateAttr() + _all_attributes: list[str] = PrivateAttr() + _all_attribute_logos: list[str] = PrivateAttr() + _model_hash: str = PrivateAttr() + + @property + def merged_labelmap(self) -> dict[int, str]: + return self._merged_labelmap + + @property + def colormap(self) -> dict[int, tuple[int, int, int]]: + return self._colormap + + @property + def non_logo_attributes(self) -> list[str]: + return ["face", "license_plate"] + + @property + def all_attributes(self) -> list[str]: + return self._all_attributes + + @property + def all_attribute_logos(self) -> list[str]: + return self._all_attribute_logos + + @property + def model_hash(self) -> str: + return self._model_hash + + def __init__(self, **config): + super().__init__(**config) + + self._merged_labelmap = { + **load_labels(config.get("labelmap_path", "/labelmap.txt")), + **config.get("labelmap", {}), + } + self._colormap = {} + + # generate list of attribute labels + unique_attributes = set() + + for attributes in self.attributes_map.values(): + unique_attributes.update(attributes) + + self._all_attributes = list(unique_attributes) + self._all_attribute_logos = list( + unique_attributes - set(self.non_logo_attributes) + ) + + def check_and_load_plus_model( + self, plus_api: PlusApi, detector: str = None + ) -> None: + if not self.path or not self.path.startswith("plus://"): + return + + # ensure that model cache dir exists + os.makedirs(MODEL_CACHE_DIR, exist_ok=True) + + model_id = self.path[7:] + self.path = os.path.join(MODEL_CACHE_DIR, model_id) + model_info_path = f"{self.path}.json" + + # download the model if it doesn't exist + if not os.path.isfile(self.path): + download_url = plus_api.get_model_download_url(model_id) + r = requests.get(download_url) + with open(self.path, "wb") as f: + f.write(r.content) + + # download the model info if it doesn't exist + if not os.path.isfile(model_info_path): + model_info = plus_api.get_model_info(model_id) + with open(model_info_path, "w") as f: + json.dump(model_info, f) + else: + with open(model_info_path) as f: + model_info: dict[str, Any] = json.load(f) + + if detector and detector not in model_info["supportedDetectors"]: + raise ValueError(f"Model does not support detector type of {detector}") + + self.width = model_info["width"] + self.height = model_info["height"] + self.input_tensor = InputTensorEnum(model_info["inputShape"]) + self.input_pixel_format = PixelFormatEnum(model_info["pixelFormat"]) + self.model_type = ModelTypeEnum(model_info["type"]) + + if model_info.get("inputDataType"): + self.input_dtype = InputDTypeEnum(model_info["inputDataType"]) + + # RKNN always uses NHWC + if detector == "rknn": + self.input_tensor = InputTensorEnum.nhwc + + # generate list of attribute labels + self.attributes_map = { + **model_info.get("attributes", DEFAULT_ATTRIBUTE_LABEL_MAP), + **self.attributes_map, + } + unique_attributes = set() + + for attributes in self.attributes_map.values(): + unique_attributes.update(attributes) + + self._all_attributes = list(unique_attributes) + self._all_attribute_logos = list( + unique_attributes - set(["face", "license_plate"]) + ) + + self._merged_labelmap = { + **{int(key): val for key, val in model_info["labelMap"].items()}, + **self.labelmap, + } + + def compute_model_hash(self) -> None: + if not self.path or not os.path.exists(self.path): + self._model_hash = hashlib.md5(b"unknown").hexdigest() + else: + with open(self.path, "rb") as f: + file_hash = hashlib.md5() + while chunk := f.read(8192): + file_hash.update(chunk) + self._model_hash = file_hash.hexdigest() + + def create_colormap(self, enabled_labels: set[str]) -> None: + """Get a list of colors for enabled labels that aren't attributes.""" + enabled_trackable_labels = list( + filter(lambda label: label not in self._all_attributes, enabled_labels) + ) + colors = generate_color_palette(len(enabled_trackable_labels)) + self._colormap = { + label: color for label, color in zip(enabled_trackable_labels, colors) + } + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class BaseDetectorConfig(BaseModel): + # the type field must be defined in all subclasses + type: str = Field( + default="cpu", + title="Detector Type", + description="Type of detector to use for object detection (for example 'cpu', 'edgetpu', 'openvino').", + ) + model: ModelConfig | None = Field( + default=None, + title="Detector specific model configuration", + description="Detector-specific model configuration options (path, input size, etc.).", + ) + model_path: str | None = Field( + default=None, + title="Detector specific model path", + description="File path to the detector model binary if required by the chosen detector.", + ) + model_config = ConfigDict( + extra="allow", arbitrary_types_allowed=True, protected_namespaces=() + ) diff --git a/frigate/detectors/detector_types.py b/frigate/detectors/detector_types.py new file mode 100644 index 0000000..42129c5 --- /dev/null +++ b/frigate/detectors/detector_types.py @@ -0,0 +1,41 @@ +import importlib +import logging +import pkgutil +from enum import Enum +from typing import Annotated, Union + +from pydantic import Field + +from . import plugins +from .detection_api import DetectionApi +from .detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) + + +_included_modules = pkgutil.iter_modules(plugins.__path__, plugins.__name__ + ".") + +plugin_modules = [] + +for _, name, _ in _included_modules: + try: + # currently openvino may fail when importing + # on an arm device with 64 KiB page size. + plugin_modules.append(importlib.import_module(name)) + except ImportError as e: + logger.error(f"Error importing detector runtime: {e}") + + +api_types = {det.type_key: det for det in DetectionApi.__subclasses__()} + + +class StrEnum(str, Enum): + pass + + +DetectorTypeEnum = StrEnum("DetectorTypeEnum", {k: k for k in api_types}) + +DetectorConfig = Annotated[ + Union[tuple(BaseDetectorConfig.__subclasses__())], # noqa: UP007 + Field(discriminator="type"), +] diff --git a/frigate/detectors/detector_utils.py b/frigate/detectors/detector_utils.py new file mode 100644 index 0000000..d8930b2 --- /dev/null +++ b/frigate/detectors/detector_utils.py @@ -0,0 +1,74 @@ +import logging +import os + +import numpy as np + +try: + from tflite_runtime.interpreter import Interpreter, load_delegate +except ModuleNotFoundError: + from ai_edge_litert.interpreter import Interpreter, load_delegate + + +logger = logging.getLogger(__name__) + + +def tflite_init(self, interpreter): + self.interpreter = interpreter + + self.interpreter.allocate_tensors() + + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + + +def tflite_detect_raw(self, tensor_input): + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input) + self.interpreter.invoke() + + boxes = self.interpreter.tensor(self.tensor_output_details[0]["index"])()[0] + class_ids = self.interpreter.tensor(self.tensor_output_details[1]["index"])()[0] + scores = self.interpreter.tensor(self.tensor_output_details[2]["index"])()[0] + count = int(self.interpreter.tensor(self.tensor_output_details[3]["index"])()[0]) + + detections = np.zeros((20, 6), np.float32) + + for i in range(count): + if scores[i] < 0.4 or i == 20: + break + detections[i] = [ + class_ids[i], + float(scores[i]), + boxes[i][0], + boxes[i][1], + boxes[i][2], + boxes[i][3], + ] + + return detections + + +def tflite_load_delegate_interpreter( + delegate_library: str, detector_config, device_config +): + try: + logger.info("Attempting to load NPU") + tf_delegate = load_delegate(delegate_library, device_config) + logger.info("NPU found") + interpreter = Interpreter( + model_path=detector_config.model.path, + experimental_delegates=[tf_delegate], + ) + return interpreter + except ValueError: + _, ext = os.path.splitext(detector_config.model.path) + + if ext and ext != ".tflite": + logger.error( + "Incorrect model used with NPU. Only .tflite models can be used with a TFLite delegate." + ) + else: + logger.error( + "No NPU was detected. If you do not have a TFLite device yet, you must configure CPU detectors." + ) + + raise diff --git a/frigate/detectors/plugins/__init__.py b/frigate/detectors/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/detectors/plugins/axengine.py b/frigate/detectors/plugins/axengine.py new file mode 100644 index 0000000..43a1fff --- /dev/null +++ b/frigate/detectors/plugins/axengine.py @@ -0,0 +1,97 @@ +import logging +import os.path +import re +import urllib.request +from typing import Literal + +from pydantic import ConfigDict + +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): + """AXERA AX650N/AX8850N NPU detector running compiled .axmodel files via the AXEngine runtime.""" + + model_config = ConfigDict( + title="AXEngine NPU", + ) + + type: Literal[DETECTOR_KEY] + + +class Axengine(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, config: AxengineDetectorConfig): + try: + import axengine as axe + except ModuleNotFoundError: + raise ImportError("AXEngine is not installed.") from None + + 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.' + ) diff --git a/frigate/detectors/plugins/cpu_tfl.py b/frigate/detectors/plugins/cpu_tfl.py new file mode 100644 index 0000000..d8cfe33 --- /dev/null +++ b/frigate/detectors/plugins/cpu_tfl.py @@ -0,0 +1,52 @@ +import logging +from typing import Literal + +from pydantic import ConfigDict, Field + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig +from frigate.log import suppress_stderr_during + +from ..detector_utils import tflite_detect_raw, tflite_init + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from ai_edge_litert.interpreter import Interpreter + + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "cpu" + + +class CpuDetectorConfig(BaseDetectorConfig): + """CPU TFLite detector that runs TensorFlow Lite models on the host CPU without hardware acceleration. Not recommended.""" + + model_config = ConfigDict( + title="CPU", + ) + + type: Literal[DETECTOR_KEY] + num_threads: int = Field( + default=3, + title="Number of detection threads", + description="The number of threads used for CPU-based inference.", + ) + + +class CpuTfl(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: CpuDetectorConfig): + # Suppress TFLite delegate creation messages that bypass Python logging + with suppress_stderr_during("tflite_interpreter_init"): + interpreter = Interpreter( + model_path=detector_config.model.path, + num_threads=detector_config.num_threads or 3, + ) + + tflite_init(self, interpreter) + + def detect_raw(self, tensor_input): + return tflite_detect_raw(self, tensor_input) diff --git a/frigate/detectors/plugins/deepstack.py b/frigate/detectors/plugins/deepstack.py new file mode 100644 index 0000000..e87b07c --- /dev/null +++ b/frigate/detectors/plugins/deepstack.py @@ -0,0 +1,105 @@ +import io +import logging +from typing import Literal + +import numpy as np +import requests +from PIL import Image +from pydantic import ConfigDict, Field + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "deepstack" + + +class DeepstackDetectorConfig(BaseDetectorConfig): + """DeepStack/CodeProject.AI detector that sends images to a remote DeepStack HTTP API for inference. Not recommended.""" + + model_config = ConfigDict( + title="DeepStack", + ) + + type: Literal[DETECTOR_KEY] + api_url: str = Field( + default="http://localhost:80/v1/vision/detection", + title="DeepStack API URL", + description="The URL of the DeepStack API.", + ) + api_timeout: float = Field( + default=0.1, + title="DeepStack API timeout (in seconds)", + description="Maximum time allowed for a DeepStack API request.", + ) + api_key: str = Field( + default="", + title="DeepStack API key (if required)", + description="Optional API key for authenticated DeepStack services.", + ) + + +class DeepStack(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: DeepstackDetectorConfig): + self.api_url = detector_config.api_url + self.api_timeout = detector_config.api_timeout + self.api_key = detector_config.api_key + self.labels = detector_config.model.merged_labelmap + self.session = requests.Session() + + def get_label_index(self, label_value): + if label_value.lower() == "truck": + label_value = "car" + for index, value in self.labels.items(): + if value == label_value.lower(): + return index + return -1 + + def detect_raw(self, tensor_input): + image_data = np.squeeze(tensor_input).astype(np.uint8) + image = Image.fromarray(image_data) + self.w, self.h = image.size + with io.BytesIO() as output: + image.save(output, format="JPEG") + image_bytes = output.getvalue() + data = {"api_key": self.api_key} + + try: + response = self.session.post( + self.api_url, + data=data, + files={"image": image_bytes}, + timeout=self.api_timeout, + ) + except requests.exceptions.RequestException as ex: + logger.error("Error calling deepstack API: %s", ex) + return np.zeros((20, 6), np.float32) + + response_json = response.json() + detections = np.zeros((20, 6), np.float32) + if response_json.get("predictions") is None: + logger.debug(f"Error in parsing response json: {response_json}") + return detections + + for i, detection in enumerate(response_json.get("predictions")): + logger.debug(f"Response: {detection}") + if detection["confidence"] < 0.4: + logger.debug("Break due to confidence < 0.4") + break + label = self.get_label_index(detection["label"]) + if label < 0: + logger.debug("Break due to unknown label") + break + detections[i] = [ + label, + float(detection["confidence"]), + detection["y_min"] / self.h, + detection["x_min"] / self.w, + detection["y_max"] / self.h, + detection["x_max"] / self.w, + ] + + return detections diff --git a/frigate/detectors/plugins/degirum.py b/frigate/detectors/plugins/degirum.py new file mode 100644 index 0000000..9aaf0e6 --- /dev/null +++ b/frigate/detectors/plugins/degirum.py @@ -0,0 +1,157 @@ +import logging +import queue +from typing import Literal + +import numpy as np +from pydantic import ConfigDict, Field + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) +DETECTOR_KEY = "degirum" + + +### DETECTOR CONFIG ### +class DGDetectorConfig(BaseDetectorConfig): + """DeGirum detector for running models via DeGirum cloud or local inference services.""" + + model_config = ConfigDict( + title="DeGirum", + ) + + type: Literal[DETECTOR_KEY] + location: str = Field( + default=None, + title="Inference Location", + description="Location of the DeGirim inference engine (e.g. '@cloud', '127.0.0.1').", + ) + zoo: str = Field( + default=None, + title="Model Zoo", + description="Path or URL to the DeGirum model zoo.", + ) + token: str = Field( + default=None, + title="DeGirum Cloud Token", + description="Token for DeGirum Cloud access.", + ) + + +### ACTUAL DETECTOR ### +class DGDetector(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: DGDetectorConfig): + try: + import degirum as dg + except ModuleNotFoundError: + raise ImportError("Unable to import DeGirum detector.") from None + + self._queue = queue.Queue() + self._zoo = dg.connect( + detector_config.location, detector_config.zoo, detector_config.token + ) + + logger.debug(f"Models in zoo: {self._zoo.list_models()}") + + self.dg_model = self._zoo.load_model( + detector_config.model.path, + ) + + # Setting input image format to raw reduces preprocessing time + self.dg_model.input_image_format = "RAW" + + # Prioritize the most powerful hardware available + self.select_best_device_type() + # Frigate handles pre processing as long as these are all set + input_shape = self.dg_model.input_shape[0] + self.model_height = input_shape[1] + self.model_width = input_shape[2] + + # Passing in dummy frame so initial connection latency happens in + # init function and not during actual prediction + frame = np.zeros( + (detector_config.model.width, detector_config.model.height, 3), + dtype=np.uint8, + ) + # Pass in frame to overcome first frame latency + self.dg_model(frame) + self.prediction = self.prediction_generator() + + def select_best_device_type(self): + """ + Helper function that selects fastest hardware available per model runtime + """ + types = self.dg_model.supported_device_types + + device_map = { + "OPENVINO": ["GPU", "NPU", "CPU"], + "HAILORT": ["HAILO8L", "HAILO8"], + "N2X": ["ORCA1", "CPU"], + "ONNX": ["VITIS_NPU", "CPU"], + "RKNN": ["RK3566", "RK3568", "RK3588"], + "TENSORRT": ["DLA", "GPU", "DLA_ONLY"], + "TFLITE": ["ARMNN", "EDGETPU", "CPU"], + } + + runtime = types[0].split("/")[0] + # Just create an array of format {runtime}/{hardware} for every hardware + # in the value for appropriate key in device_map + self.dg_model.device_type = [ + f"{runtime}/{hardware}" for hardware in device_map[runtime] + ] + + def prediction_generator(self): + """ + Generator for all incoming frames. By using this generator, we don't have to keep + reconnecting our websocket on every "predict" call. + """ + logger.debug("Prediction generator was called") + with self.dg_model as model: + while 1: + logger.info(f"q size before calling get: {self._queue.qsize()}") + data = self._queue.get(block=True) + logger.info(f"q size after calling get: {self._queue.qsize()}") + logger.debug( + f"Data we're passing into model predict: {data}, shape of data: {data.shape}" + ) + result = model.predict(data) + logger.debug(f"Prediction result: {result}") + yield result + + def detect_raw(self, tensor_input): + # Reshaping tensor to work with pysdk + truncated_input = tensor_input.reshape(tensor_input.shape[1:]) + logger.debug(f"Detect raw was called for tensor input: {tensor_input}") + + # add tensor_input to input queue + self._queue.put(truncated_input) + logger.debug(f"Queue size after adding truncated input: {self._queue.qsize()}") + + # define empty detection result + detections = np.zeros((20, 6), np.float32) + # grab prediction + res = next(self.prediction) + + # If we have an empty prediction, return immediately + if len(res.results) == 0 or len(res.results[0]) == 0: + return detections + + i = 0 + for result in res.results: + if i >= 20: + break + + detections[i] = [ + result["category_id"], + float(result["score"]), + result["bbox"][1] / self.model_height, + result["bbox"][0] / self.model_width, + result["bbox"][3] / self.model_height, + result["bbox"][2] / self.model_width, + ] + i += 1 + + logger.debug(f"Detections output: {detections}") + return detections diff --git a/frigate/detectors/plugins/edgetpu_tfl.py b/frigate/detectors/plugins/edgetpu_tfl.py new file mode 100644 index 0000000..c4ea485 --- /dev/null +++ b/frigate/detectors/plugins/edgetpu_tfl.py @@ -0,0 +1,371 @@ +import logging +import math +import os +from typing import Literal + +import cv2 +import numpy as np +from pydantic import ConfigDict, Field + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum + +try: + from tflite_runtime.interpreter import Interpreter, load_delegate +except ModuleNotFoundError: + from ai_edge_litert.interpreter import Interpreter, load_delegate + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "edgetpu" + + +class EdgeTpuDetectorConfig(BaseDetectorConfig): + """EdgeTPU detector that runs TensorFlow Lite models compiled for Coral EdgeTPU using the EdgeTPU delegate.""" + + model_config = ConfigDict( + title="EdgeTPU", + ) + + type: Literal[DETECTOR_KEY] + device: str = Field( + default=None, + title="Device Type", + description="The device to use for EdgeTPU inference (e.g. 'usb', 'pci').", + ) + + +class EdgeTpuTfl(DetectionApi): + type_key = DETECTOR_KEY + supported_models = [ + ModelTypeEnum.ssd, + ModelTypeEnum.yologeneric, + ] + + def __init__(self, detector_config: EdgeTpuDetectorConfig): + device_config = {} + if detector_config.device is not None: + device_config = {"device": detector_config.device} + + edge_tpu_delegate = None + + try: + device_type = ( + device_config["device"] if "device" in device_config else "auto" + ) + logger.info(f"Attempting to load TPU as {device_type}") + edge_tpu_delegate = load_delegate("libedgetpu.so.1.0", device_config) + logger.info("TPU found") + self.interpreter = Interpreter( + model_path=detector_config.model.path, + experimental_delegates=[edge_tpu_delegate], + ) + except ValueError: + _, ext = os.path.splitext(detector_config.model.path) + + if ext and ext != ".tflite": + logger.error( + "Incorrect model used with EdgeTPU. Only .tflite models can be used with a Coral EdgeTPU." + ) + else: + logger.error( + "No EdgeTPU was detected. If you do not have a Coral device yet, you must configure CPU detectors." + ) + + raise + + self.interpreter.allocate_tensors() + + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + self.model_width = detector_config.model.width + self.model_height = detector_config.model.height + + self.min_score = 0.4 + self.max_detections = 20 + + self.model_type = detector_config.model.model_type + self.model_requires_int8 = self.tensor_input_details[0]["dtype"] == np.int8 + + if self.model_type == ModelTypeEnum.yologeneric: + logger.debug("Using YOLO preprocessing/postprocessing") + + if len(self.tensor_output_details) not in [2, 3]: + logger.error( + f"Invalid count of output tensors in YOLO model. Found {len(self.tensor_output_details)}, expecting 2 or 3." + ) + raise + + self.reg_max = 16 # = 64 dfl_channels // 4 # YOLO standard + self.min_logit_value = np.log( + self.min_score / (1 - self.min_score) + ) # for filtering + self._generate_anchors_and_strides() # decode bounding box DFL + self.project = np.arange( + self.reg_max, dtype=np.float32 + ) # for decoding bounding box DFL information + + # Determine YOLO tensor indices and quantization scales for + # boxes and class_scores the tensor ordering and names are + # not reliable, so use tensor shape to detect which tensor + # holds boxes or class scores. + # The tensors have shapes (B, N, C) + # where N is the number of candidates (=2100 for 320x320) + # this may guess wrong if the number of classes is exactly 64 + output_boxes_index = None + output_classes_index = None + for i, x in enumerate(self.tensor_output_details): + # the nominal index seems to start at 1 instead of 0 + if len(x["shape"]) == 3 and x["shape"][2] == 64: + output_boxes_index = i + elif len(x["shape"]) == 3 and x["shape"][2] > 1: + # require the number of classes to be more than 1 + # to differentiate from (not used) max score tensor + output_classes_index = i + if output_boxes_index is None or output_classes_index is None: + logger.warning("Unrecognized model output, unexpected tensor shapes.") + output_classes_index = ( + 0 + if (output_boxes_index is None or output_classes_index == 1) + else 1 + ) # 0 is default guess + output_boxes_index = 1 if (output_boxes_index == 0) else 0 + + scores_details = self.tensor_output_details[output_classes_index] + self.scores_tensor_index = scores_details["index"] + self.scores_scale, self.scores_zero_point = scores_details["quantization"] + # calculate the quantized version of the min_score + self.min_score_quantized = int( + (self.min_logit_value / self.scores_scale) + self.scores_zero_point + ) + self.logit_shift_to_positive_values = ( + max(0, math.ceil((128 + self.scores_zero_point) * self.scores_scale)) + + 1 + ) # round up + + boxes_details = self.tensor_output_details[output_boxes_index] + self.boxes_tensor_index = boxes_details["index"] + self.boxes_scale, self.boxes_zero_point = boxes_details["quantization"] + + elif self.model_type == ModelTypeEnum.ssd: + logger.debug("Using SSD preprocessing/postprocessing") + + # SSD model indices (4 outputs: boxes, class_ids, scores, count) + for x in self.tensor_output_details: + if len(x["shape"]) == 3: + self.output_boxes_index = x["index"] + elif len(x["shape"]) == 1: + self.output_count_index = x["index"] + + self.output_class_ids_index = None + self.output_class_scores_index = None + + else: + raise Exception( + f"{self.model_type} is currently not supported for edgetpu. See the docs for more info on supported models." + ) + + def _generate_anchors_and_strides(self): + # for decoding the bounding box DFL information into xy coordinates + all_anchors = [] + all_strides = [] + strides = (8, 16, 32) # YOLO's small, medium, large detection heads + + for stride in strides: + feat_h, feat_w = self.model_height // stride, self.model_width // stride + + grid_y, grid_x = np.meshgrid( + np.arange(feat_h, dtype=np.float32), + np.arange(feat_w, dtype=np.float32), + indexing="ij", + ) + + grid_coords = np.stack((grid_x.flatten(), grid_y.flatten()), axis=1) + anchor_points = grid_coords + 0.5 + + all_anchors.append(anchor_points) + all_strides.append(np.full((feat_h * feat_w, 1), stride, dtype=np.float32)) + + self.anchors = np.concatenate(all_anchors, axis=0) + self.anchor_strides = np.concatenate(all_strides, axis=0) + + def determine_indexes_for_non_yolo_models(self): + """Legacy method for SSD models.""" + if ( + self.output_class_ids_index is None + or self.output_class_scores_index is None + ): + for i in range(4): + index = self.tensor_output_details[i]["index"] + if ( + index != self.output_boxes_index + and index != self.output_count_index + ): + if ( + np.mod(np.float32(self.interpreter.tensor(index)()[0][0]), 1) + == 0.0 + ): + self.output_class_ids_index = index + else: + self.output_scores_index = index + + def pre_process(self, tensor_input): + if self.model_requires_int8: + tensor_input = np.bitwise_xor(tensor_input, 128).view( + np.int8 + ) # shift by -128 + return tensor_input + + def detect_raw(self, tensor_input): + tensor_input = self.pre_process(tensor_input) + + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input) + self.interpreter.invoke() + + if self.model_type == ModelTypeEnum.yologeneric: + # Multi-tensor YOLO model with (non-standard B(H*W)C output format). + # (the comments indicate the shape of tensors, + # using "2100" as the anchor count (for image size of 320x320), + # "NC" as number of classes, + # "N" as the count that survive after min-score filtering) + # TENSOR A) class scores (1, 2100, NC) with logit values + # TENSOR B) box coordinates (1, 2100, 64) encoded as dfl scores + # Recommend that the model clamp the logit values in tensor (A) + # to the range [-4,+4] to preserve precision from [2%,98%] + # and because NMS requires the min_score parameter to be >= 0 + + # don't dequantize scores data yet, wait until the low-confidence + # candidates are filtered out from the overall result set. + # This reduces the work and makes post-processing faster. + # this method works with raw quantized numbers when possible, + # which relies on the value of the scale factor to be >0. + # This speeds up max and argmax operations. + # Get max confidence for each detection and create the mask + detections = np.zeros( + (self.max_detections, 6), np.float32 + ) # initialize zero results + scores_output_quantized = self.interpreter.get_tensor( + self.scores_tensor_index + )[0] # (2100, NC) + max_scores_quantized = np.max(scores_output_quantized, axis=1) # (2100,) + mask = max_scores_quantized >= self.min_score_quantized # (2100,) + + if not np.any(mask): + return detections # empty results + + max_scores_filtered_shiftedpositive = ( + (max_scores_quantized[mask] - self.scores_zero_point) + * self.scores_scale + ) + self.logit_shift_to_positive_values # (N,1) shifted logit values + scores_output_quantized_filtered = scores_output_quantized[mask] + + # dequantize boxes. NMS needs them to be in float format + # remove candidates with probabilities < threshold + boxes_output_quantized_filtered = ( + self.interpreter.get_tensor(self.boxes_tensor_index)[0] + )[mask] # (N, 64) + boxes_output_filtered = ( + boxes_output_quantized_filtered.astype(np.float32) + - self.boxes_zero_point + ) * self.boxes_scale + + # 2. Decode DFL to distances (ltrb) + dfl_distributions = boxes_output_filtered.reshape( + -1, 4, self.reg_max + ) # (N, 4, 16) + + # Softmax over the 16 bins + dfl_max = np.max(dfl_distributions, axis=2, keepdims=True) + dfl_exp = np.exp(dfl_distributions - dfl_max) + dfl_probs = dfl_exp / np.sum(dfl_exp, axis=2, keepdims=True) # (N, 4, 16) + + # Weighted sum: (N, 4, 16) * (16,) -> (N, 4) + distances = np.einsum("pcr,r->pc", dfl_probs, self.project) + + # Calculate box corners in pixel coordinates + anchors_filtered = self.anchors[mask] + anchor_strides_filtered = self.anchor_strides[mask] + x1y1 = ( + anchors_filtered - distances[:, [0, 1]] + ) * anchor_strides_filtered # (N, 2) + x2y2 = ( + anchors_filtered + distances[:, [2, 3]] + ) * anchor_strides_filtered # (N, 2) + boxes_filtered_decoded = np.concatenate((x1y1, x2y2), axis=-1) # (N, 4) + + # 9. Apply NMS. Use logit scores here to defer sigmoid() + # until after filtering out redundant boxes + # Shift the logit scores to be non-negative (required by cv2) + indices = cv2.dnn.NMSBoxes( + bboxes=boxes_filtered_decoded, + scores=max_scores_filtered_shiftedpositive, + score_threshold=( + self.min_logit_value + self.logit_shift_to_positive_values + ), + nms_threshold=0.4, # should this be a model config setting? + ) + num_detections = len(indices) + if num_detections == 0: + return detections # empty results + + nms_indices = np.array(indices, dtype=np.int32).ravel() # or .flatten() + if num_detections > self.max_detections: + nms_indices = nms_indices[: self.max_detections] + num_detections = self.max_detections + kept_logits_quantized = scores_output_quantized_filtered[nms_indices] + class_ids_post_nms = np.argmax(kept_logits_quantized, axis=1) + + # Extract the final boxes and scores using fancy indexing + final_boxes = boxes_filtered_decoded[nms_indices] + final_scores_logits = ( + max_scores_filtered_shiftedpositive[nms_indices] + - self.logit_shift_to_positive_values + ) # Unshifted logits + + # Detections array format: [class_id, score, ymin, xmin, ymax, xmax] + detections[:num_detections, 0] = class_ids_post_nms + detections[:num_detections, 1] = 1.0 / ( + 1.0 + np.exp(-final_scores_logits) + ) # sigmoid + detections[:num_detections, 2] = final_boxes[:, 1] / self.model_height + detections[:num_detections, 3] = final_boxes[:, 0] / self.model_width + detections[:num_detections, 4] = final_boxes[:, 3] / self.model_height + detections[:num_detections, 5] = final_boxes[:, 2] / self.model_width + return detections + + elif self.model_type == ModelTypeEnum.ssd: + self.determine_indexes_for_non_yolo_models() + boxes = self.interpreter.tensor(self.tensor_output_details[0]["index"])()[0] + class_ids = self.interpreter.tensor( + self.tensor_output_details[1]["index"] + )()[0] + scores = self.interpreter.tensor(self.tensor_output_details[2]["index"])()[ + 0 + ] + count = int( + self.interpreter.tensor(self.tensor_output_details[3]["index"])()[0] + ) + + detections = np.zeros((self.max_detections, 6), np.float32) + + for i in range(count): + if scores[i] < self.min_score: + break + if i == self.max_detections: + logger.debug(f"Too many detections ({count})!") + break + detections[i] = [ + class_ids[i], + float(scores[i]), + boxes[i][0], + boxes[i][1], + boxes[i][2], + boxes[i][3], + ] + + return detections + + else: + raise Exception( + f"{self.model_type} is currently not supported for edgetpu. See the docs for more info on supported models." + ) diff --git a/frigate/detectors/plugins/hailo8l.py b/frigate/detectors/plugins/hailo8l.py new file mode 100755 index 0000000..63a3c9a --- /dev/null +++ b/frigate/detectors/plugins/hailo8l.py @@ -0,0 +1,423 @@ +import logging +import os +import subprocess +import threading +import urllib.request +from functools import partial +from typing import Literal + +import cv2 +import numpy as np +from pydantic import ConfigDict, Field + +from frigate.const import MODEL_CACHE_DIR +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import ( + BaseDetectorConfig, +) +from frigate.object_detection.util import RequestStore, ResponseStore + +logger = logging.getLogger(__name__) + + +# ----------------- Utility Functions ----------------- # + + +def preprocess_tensor(image: np.ndarray, model_w: int, model_h: int) -> np.ndarray: + """ + Resize an image with unchanged aspect ratio using padding. + Assumes input image shape is (H, W, 3). + """ + if image.ndim == 4 and image.shape[0] == 1: + image = image[0] + + h, w = image.shape[:2] + scale = min(model_w / w, model_h / h) + new_w, new_h = int(w * scale), int(h * scale) + resized_image = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_CUBIC) + padded_image = np.full((model_h, model_w, 3), 114, dtype=image.dtype) + x_offset = (model_w - new_w) // 2 + y_offset = (model_h - new_h) // 2 + padded_image[y_offset : y_offset + new_h, x_offset : x_offset + new_w] = ( + resized_image + ) + return padded_image + + +# ----------------- Global Constants ----------------- # +DETECTOR_KEY = "hailo8l" +ARCH = None +H8_DEFAULT_MODEL = "yolov6n.hef" +H8L_DEFAULT_MODEL = "yolov6n.hef" +H8_DEFAULT_URL = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef" +H8L_DEFAULT_URL = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/yolov6n.hef" + + +def detect_hailo_arch(): + try: + result = subprocess.run( + ["hailortcli", "fw-control", "identify"], capture_output=True, text=True + ) + if result.returncode != 0: + logger.error(f"Inference error: {result.stderr}") + return None + for line in result.stdout.split("\n"): + if "Device Architecture" in line: + if "HAILO8L" in line: + return "hailo8l" + elif "HAILO8" in line: + return "hailo8" + logger.error("Inference error: Could not determine Hailo architecture.") + return None + except Exception as e: + logger.error(f"Inference error: {e}") + return None + + +# ----------------- HailoAsyncInference Class ----------------- # +class HailoAsyncInference: + def __init__( + self, + hef_path: str, + input_store: RequestStore, + output_store: ResponseStore, + batch_size: int = 1, + input_type: str | None = None, + output_type: dict[str, str] | None = None, + send_original_frame: bool = False, + ) -> None: + # when importing hailo it activates the driver + # which leaves processes running even though it may not be used. + try: + from hailo_platform import ( + HEF, + FormatType, + HailoSchedulingAlgorithm, + VDevice, + ) + except ModuleNotFoundError: + pass + + self.input_store = input_store + self.output_store = output_store + + params = VDevice.create_params() + params.scheduling_algorithm = HailoSchedulingAlgorithm.ROUND_ROBIN + + self.hef = HEF(hef_path) + self.target = VDevice(params) + self.infer_model = self.target.create_infer_model(hef_path) + self.infer_model.set_batch_size(batch_size) + + if input_type is not None: + self.infer_model.input().set_format_type(getattr(FormatType, input_type)) + + if output_type is not None: + for output_name, output_type in output_type.items(): + self.infer_model.output(output_name).set_format_type( + getattr(FormatType, output_type) + ) + + self.output_type = output_type + self.send_original_frame = send_original_frame + + def callback( + self, + completion_info, + bindings_list: list, + input_batch: list, + request_ids: list[int], + ): + if completion_info.exception: + logger.error(f"Inference error: {completion_info.exception}") + else: + for i, bindings in enumerate(bindings_list): + if len(bindings._output_names) == 1: + result = bindings.output().get_buffer() + else: + result = { + name: np.expand_dims(bindings.output(name).get_buffer(), axis=0) + for name in bindings._output_names + } + self.output_store.put(request_ids[i], (input_batch[i], result)) + + def _create_bindings(self, configured_infer_model) -> object: + if self.output_type is None: + output_buffers = { + output_info.name: np.empty( + self.infer_model.output(output_info.name).shape, + dtype=getattr( + np, str(output_info.format.type).split(".")[1].lower() + ), + ) + for output_info in self.hef.get_output_vstream_infos() + } + else: + output_buffers = { + name: np.empty( + self.infer_model.output(name).shape, + dtype=getattr(np, self.output_type[name].lower()), + ) + for name in self.output_type + } + return configured_infer_model.create_bindings(output_buffers=output_buffers) + + def get_input_shape(self) -> tuple[int, ...]: + return self.hef.get_input_vstream_infos()[0].shape + + def run(self) -> None: + job = None + with self.infer_model.configure() as configured_infer_model: + while True: + batch_data = self.input_store.get() + + if batch_data is None: + break + + request_id, frame_data = batch_data + preprocessed_batch = [frame_data] + request_ids = [request_id] + input_batch = preprocessed_batch # non-send_original_frame mode + + bindings_list = [] + for frame in preprocessed_batch: + bindings = self._create_bindings(configured_infer_model) + bindings.input().set_buffer(np.array(frame)) + bindings_list.append(bindings) + configured_infer_model.wait_for_async_ready(timeout_ms=10000) + job = configured_infer_model.run_async( + bindings_list, + partial( + self.callback, + input_batch=input_batch, + request_ids=request_ids, + bindings_list=bindings_list, + ), + ) + + if job is not None: + job.wait(100) + + +# ----------------- HailoDetector Class ----------------- # +class HailoDetector(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: "HailoDetectorConfig"): + global ARCH + ARCH = detect_hailo_arch() + self.cache_dir = MODEL_CACHE_DIR + self.device_type = detector_config.device + self.model_height = ( + detector_config.model.height + if hasattr(detector_config.model, "height") + else None + ) + self.model_width = ( + detector_config.model.width + if hasattr(detector_config.model, "width") + else None + ) + self.model_type = ( + detector_config.model.model_type + if hasattr(detector_config.model, "model_type") + else None + ) + self.tensor_format = ( + detector_config.model.input_tensor + if hasattr(detector_config.model, "input_tensor") + else None + ) + self.pixel_format = ( + detector_config.model.input_pixel_format + if hasattr(detector_config.model, "input_pixel_format") + else None + ) + self.input_dtype = ( + detector_config.model.input_dtype + if hasattr(detector_config.model, "input_dtype") + else None + ) + self.output_type = "FLOAT32" + self.set_path_and_url(detector_config.model.path) + self.working_model_path = self.check_and_prepare() + + self.batch_size = 1 + self.input_store = RequestStore() + self.response_store = ResponseStore() + + try: + logger.debug(f"[INIT] Loading HEF model from {self.working_model_path}") + self.inference_engine = HailoAsyncInference( + self.working_model_path, + self.input_store, + self.response_store, + self.batch_size, + ) + self.input_shape = self.inference_engine.get_input_shape() + logger.debug(f"[INIT] Model input shape: {self.input_shape}") + self.inference_thread = threading.Thread( + target=self.inference_engine.run, daemon=True + ) + self.inference_thread.start() + except Exception as e: + logger.error(f"[INIT] Failed to initialize HailoAsyncInference: {e}") + raise + + def set_path_and_url(self, path: str = None): + if not path: + self.model_path = None + self.url = None + return + if self.is_url(path): + self.url = path + self.model_path = None + else: + self.model_path = path + self.url = None + + def is_url(self, url: str) -> bool: + return ( + url.startswith("http://") + or url.startswith("https://") + or url.startswith("www.") + ) + + @staticmethod + def extract_model_name(path: str = None, url: str = None) -> str: + if path and path.endswith(".hef"): + return os.path.basename(path) + elif url and url.endswith(".hef"): + return os.path.basename(url) + else: + if ARCH == "hailo8": + return H8_DEFAULT_MODEL + else: + return H8L_DEFAULT_MODEL + + @staticmethod + def download_model(url: str, destination: str): + if not url.endswith(".hef"): + raise ValueError("Invalid model URL. Only .hef files are supported.") + try: + urllib.request.urlretrieve(url, destination) + logger.debug(f"Downloaded model to {destination}") + except Exception as e: + raise RuntimeError(f"Failed to download model from {url}: {str(e)}") from e + + def check_and_prepare(self) -> str: + if not os.path.exists(self.cache_dir): + os.makedirs(self.cache_dir) + model_name = self.extract_model_name(self.model_path, self.url) + cached_model_path = os.path.join(self.cache_dir, model_name) + if not self.model_path and not self.url: + if os.path.exists(cached_model_path): + logger.debug(f"Model found in cache: {cached_model_path}") + return cached_model_path + else: + logger.debug(f"Downloading default model: {model_name}") + if ARCH == "hailo8": + self.download_model(H8_DEFAULT_URL, cached_model_path) + else: + self.download_model(H8L_DEFAULT_URL, cached_model_path) + elif self.url: + logger.debug(f"Downloading model from URL: {self.url}") + self.download_model(self.url, cached_model_path) + elif self.model_path: + if os.path.exists(self.model_path): + logger.debug(f"Using existing model at: {self.model_path}") + return self.model_path + else: + raise FileNotFoundError(f"Model file not found at: {self.model_path}") + return cached_model_path + + def detect_raw(self, tensor_input): + tensor_input = self.preprocess(tensor_input) + + if isinstance(tensor_input, np.ndarray) and len(tensor_input.shape) == 3: + tensor_input = np.expand_dims(tensor_input, axis=0) + + request_id = self.input_store.put(tensor_input) + + try: + _, infer_results = self.response_store.get(request_id, timeout=1.0) + except TimeoutError: + logger.error( + f"Timeout waiting for inference results for request {request_id}" + ) + + if not self.inference_thread.is_alive(): + raise RuntimeError( + "HailoRT inference thread has stopped, restart required." + ) from None + + return np.zeros((20, 6), dtype=np.float32) + + if isinstance(infer_results, list) and len(infer_results) == 1: + infer_results = infer_results[0] + + threshold = 0.4 + all_detections = [] + for class_id, detection_set in enumerate(infer_results): + if not isinstance(detection_set, np.ndarray) or detection_set.size == 0: + continue + for det in detection_set: + if det.shape[0] < 5: + continue + score = float(det[4]) + if score < threshold: + continue + all_detections.append([class_id, score, det[0], det[1], det[2], det[3]]) + + if len(all_detections) == 0: + detections_array = np.zeros((20, 6), dtype=np.float32) + else: + detections_array = np.array(all_detections, dtype=np.float32) + if detections_array.shape[0] > 20: + detections_array = detections_array[:20, :] + elif detections_array.shape[0] < 20: + pad = np.zeros((20 - detections_array.shape[0], 6), dtype=np.float32) + detections_array = np.vstack((detections_array, pad)) + + return detections_array + + def preprocess(self, image): + if isinstance(image, np.ndarray): + processed = preprocess_tensor( + image, self.input_shape[1], self.input_shape[0] + ) + return np.expand_dims(processed, axis=0) + else: + raise ValueError("Unsupported image format for preprocessing") + + def close(self): + """Properly shuts down the inference engine and releases the VDevice.""" + logger.debug("[CLOSE] Closing HailoDetector") + try: + if hasattr(self, "inference_engine"): + if hasattr(self.inference_engine, "target"): + self.inference_engine.target.release() + logger.debug("Hailo VDevice released successfully") + except Exception as e: + logger.error(f"Failed to close Hailo device: {e}") + raise + + def __del__(self): + """Destructor to ensure cleanup when the object is deleted.""" + self.close() + + +# ----------------- HailoDetectorConfig Class ----------------- # +class HailoDetectorConfig(BaseDetectorConfig): + """Hailo-8/Hailo-8L detector using HEF models and the HailoRT SDK for inference on Hailo hardware.""" + + model_config = ConfigDict( + title="Hailo-8/Hailo-8L", + ) + + type: Literal[DETECTOR_KEY] + device: str = Field( + default="PCIe", + title="Device Type", + description="The device to use for Hailo inference (e.g. 'PCIe', 'M.2').", + ) diff --git a/frigate/detectors/plugins/memryx.py b/frigate/detectors/plugins/memryx.py new file mode 100644 index 0000000..8853727 --- /dev/null +++ b/frigate/detectors/plugins/memryx.py @@ -0,0 +1,878 @@ +import glob +import logging +import os +import shutil +import urllib.request +import zipfile +from queue import Queue +from typing import Literal + +import cv2 +import numpy as np +from pydantic import BaseModel, ConfigDict, Field + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import ( + BaseDetectorConfig, + ModelTypeEnum, +) +from frigate.util.file import FileLock + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "memryx" + + +# Configuration class for model settings +class ModelConfig(BaseModel): + path: str = Field(default=None, title="Model Path") # Path to the DFP file + labelmap_path: str = Field(default=None, title="Path to Label Map") + + +class MemryXDetectorConfig(BaseDetectorConfig): + """MemryX MX3 detector that runs compiled DFP models on MemryX accelerators.""" + + model_config = ConfigDict( + title="MemryX", + ) + + type: Literal[DETECTOR_KEY] + device: str = Field( + default="PCIe", + title="Device Path", + description="The device to use for MemryX inference (e.g. 'PCIe').", + ) + + +class MemryXDetector(DetectionApi): + type_key = DETECTOR_KEY # Set the type key + supported_models = [ + ModelTypeEnum.ssd, + ModelTypeEnum.yolonas, + ModelTypeEnum.yologeneric, # Treated as yolov9 in MemryX implementation + ModelTypeEnum.yolox, + ] + + def __init__(self, detector_config): + """Initialize MemryX detector with the provided configuration.""" + try: + # Import MemryX SDK + from memryx import AsyncAccl + except ModuleNotFoundError: + raise ImportError( + "MemryX SDK is not installed. Install it and set up MIX environment." + ) from None + return + + # Initialize stop_event as None, will be set later by set_stop_event() + self.stop_event = None + + model_cfg = getattr(detector_config, "model", None) + + # Check if model_type was explicitly set by the user + if "model_type" in getattr(model_cfg, "__fields_set__", set()): + detector_config.model.model_type = model_cfg.model_type + else: + logger.info( + "model_type not set in config — defaulting to yolonas for MemryX." + ) + detector_config.model.model_type = ModelTypeEnum.yolonas + + self.capture_queue = Queue(maxsize=10) + self.output_queue = Queue(maxsize=10) + self.capture_id_queue = Queue(maxsize=10) + self.logger = logger + + self.memx_model_path = detector_config.model.path # Path to .dfp file + self.memx_post_model = None # Path to .post file + self.expected_post_model = None + + self.memx_device_path = detector_config.device # Device path + # Parse the device string to split PCIe: + device_str = self.memx_device_path + self.device_id = [] + self.device_id.append(int(device_str.split(":")[1])) + + self.memx_model_height = detector_config.model.height + self.memx_model_width = detector_config.model.width + self.memx_model_type = detector_config.model.model_type + + self.cache_dir = "/memryx_models" + + if self.memx_model_type == ModelTypeEnum.yologeneric: + model_mapping = { + (640, 640): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolov9_640.zip", + "yolov9_640", + ), + (320, 320): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolov9_320.zip", + "yolov9_320", + ), + } + self.model_url, self.model_folder = model_mapping.get( + (self.memx_model_height, self.memx_model_width), + ( + "https://developer.memryx.com/example_files/2p0_frigate/yolov9_320.zip", + "yolov9_320", + ), + ) + self.expected_dfp_model = "YOLO_v9_small_onnx.dfp" + + elif self.memx_model_type == ModelTypeEnum.yolonas: + model_mapping = { + (640, 640): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolonas_640.zip", + "yolonas_640", + ), + (320, 320): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolonas_320.zip", + "yolonas_320", + ), + } + self.model_url, self.model_folder = model_mapping.get( + (self.memx_model_height, self.memx_model_width), + ( + "https://developer.memryx.com/example_files/2p0_frigate/yolonas_320.zip", + "yolonas_320", + ), + ) + self.expected_dfp_model = "yolo_nas_s.dfp" + self.expected_post_model = "yolo_nas_s_post.onnx" + + elif self.memx_model_type == ModelTypeEnum.yolox: + self.model_folder = "yolox" + self.model_url = ( + "https://developer.memryx.com/example_files/2p0_frigate/yolox.zip" + ) + self.expected_dfp_model = "YOLOX_640_640_3_onnx.dfp" + self.set_strides_grids() + + elif self.memx_model_type == ModelTypeEnum.ssd: + self.model_folder = "ssd" + self.model_url = ( + "https://developer.memryx.com/example_files/2p0_frigate/ssd.zip" + ) + self.expected_dfp_model = "SSDlite_MobileNet_v2_320_320_3_onnx.dfp" + self.expected_post_model = "SSDlite_MobileNet_v2_320_320_3_onnx_post.onnx" + + self.check_and_prepare_model() + logger.info( + f"Initializing MemryX with model: {self.memx_model_path} on device {self.memx_device_path}" + ) + + try: + # Load MemryX Model + logger.info(f"dfp path: {self.memx_model_path}") + + # Initialization code + # Load MemryX Model with a device target + self.accl = AsyncAccl( + self.memx_model_path, + device_ids=self.device_id, # AsyncAccl device ids + local_mode=True, + ) + + # Models that use cropped post-processing sections (YOLO-NAS and SSD) + # --> These will be moved to pure numpy in the future to improve performance on low-end CPUs + if self.memx_post_model: + self.accl.set_postprocessing_model(self.memx_post_model, model_idx=0) + + self.accl.connect_input(self.process_input) + self.accl.connect_output(self.process_output) + + logger.info( + f"Loaded MemryX model from {self.memx_model_path} and {self.memx_post_model}" + ) + + except Exception as e: + logger.error(f"Failed to initialize MemryX model: {e}") + raise + + def check_and_prepare_model(self): + if not os.path.exists(self.cache_dir): + os.makedirs(self.cache_dir, exist_ok=True) + + lock_path = os.path.join(self.cache_dir, f".{self.model_folder}.lock") + lock = FileLock(lock_path, timeout=60) + + with lock: + # ---------- CASE 1: user provided a custom model path ---------- + if self.memx_model_path: + if not self.memx_model_path.endswith(".zip"): + raise ValueError( + f"Invalid model path: {self.memx_model_path}. " + "Only .zip files are supported. Please provide a .zip model archive." + ) + if not os.path.exists(self.memx_model_path): + raise FileNotFoundError( + f"Custom model zip not found: {self.memx_model_path}" + ) + + logger.info(f"User provided zip model: {self.memx_model_path}") + + # Extract custom zip into a separate area so it never clashes with MemryX cache + custom_dir = os.path.join( + self.cache_dir, "custom_models", self.model_folder + ) + if os.path.isdir(custom_dir): + shutil.rmtree(custom_dir) + os.makedirs(custom_dir, exist_ok=True) + + with zipfile.ZipFile(self.memx_model_path, "r") as zip_ref: + zip_ref.extractall(custom_dir) + logger.info(f"Custom model extracted to {custom_dir}.") + + # Find .dfp and optional *_post.onnx recursively + dfp_candidates = glob.glob( + os.path.join(custom_dir, "**", "*.dfp"), recursive=True + ) + post_candidates = glob.glob( + os.path.join(custom_dir, "**", "*_post.onnx"), recursive=True + ) + + if not dfp_candidates: + raise FileNotFoundError( + "No .dfp file found in custom model zip after extraction." + ) + + self.memx_model_path = dfp_candidates[0] + + # Handle post model requirements by model type + if self.memx_model_type in [ + ModelTypeEnum.yolonas, + ModelTypeEnum.ssd, + ]: + if not post_candidates: + raise FileNotFoundError( + f"No *_post.onnx file found in custom model zip for {self.memx_model_type.name}." + ) + self.memx_post_model = post_candidates[0] + elif self.memx_model_type in [ + ModelTypeEnum.yolox, + ModelTypeEnum.yologeneric, + ]: + # Explicitly ignore any post model even if present + self.memx_post_model = None + else: + # Future model types can optionally use post if present + self.memx_post_model = ( + post_candidates[0] if post_candidates else None + ) + + logger.info(f"Using custom model: {self.memx_model_path}") + return + + # ---------- CASE 2: no custom model path -> use MemryX cached models ---------- + model_subdir = os.path.join(self.cache_dir, self.model_folder) + dfp_path = os.path.join(model_subdir, self.expected_dfp_model) + post_path = ( + os.path.join(model_subdir, self.expected_post_model) + if self.expected_post_model + else None + ) + + dfp_exists = os.path.exists(dfp_path) + post_exists = os.path.exists(post_path) if post_path else True + + if dfp_exists and post_exists: + logger.info("Using cached models.") + self.memx_model_path = dfp_path + self.memx_post_model = post_path + return + + # ---------- CASE 3: download MemryX model (no cache) ---------- + logger.info( + f"Model files not found locally. Downloading from {self.model_url}..." + ) + zip_path = os.path.join(self.cache_dir, f"{self.model_folder}.zip") + + try: + if not os.path.exists(zip_path): + urllib.request.urlretrieve(self.model_url, zip_path) + logger.info(f"Model ZIP downloaded to {zip_path}. Extracting...") + + if not os.path.exists(model_subdir): + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(self.cache_dir) + logger.info(f"Model extracted to {self.cache_dir}.") + + # Re-assign model paths after extraction + self.memx_model_path = os.path.join( + model_subdir, self.expected_dfp_model + ) + self.memx_post_model = ( + os.path.join(model_subdir, self.expected_post_model) + if self.expected_post_model + else None + ) + + finally: + if os.path.exists(zip_path): + try: + os.remove(zip_path) + logger.info("Cleaned up ZIP file after extraction.") + except Exception as e: + logger.warning( + f"Failed to remove downloaded zip {zip_path}: {e}" + ) + + def send_input(self, connection_id, tensor_input: np.ndarray) -> None: + """Pre-process (if needed) and send frame to MemryX input queue""" + if tensor_input is None: + raise ValueError("[send_input] No image data provided for inference") + + if self.memx_model_type == ModelTypeEnum.yolonas: + if tensor_input.ndim == 4 and tensor_input.shape[1:] == (320, 320, 3): + logger.debug("Transposing tensor from NHWC to NCHW for YOLO-NAS") + tensor_input = np.transpose( + tensor_input, (0, 3, 1, 2) + ) # (1, H, W, C) → (1, C, H, W) + tensor_input = tensor_input.astype(np.float32) + tensor_input /= 255 + + if self.memx_model_type == ModelTypeEnum.yolox: + # Remove batch dim → (3, 640, 640) + tensor_input = tensor_input.squeeze(0) + + # Convert CHW to HWC for OpenCV + tensor_input = np.transpose(tensor_input, (1, 2, 0)) # (640, 640, 3) + + padded_img = np.ones((640, 640, 3), dtype=np.uint8) * 114 + + scale = min( + 640 / float(tensor_input.shape[0]), 640 / float(tensor_input.shape[1]) + ) + sx, sy = ( + int(tensor_input.shape[1] * scale), + int(tensor_input.shape[0] * scale), + ) + + resized_img = cv2.resize( + tensor_input, (sx, sy), interpolation=cv2.INTER_LINEAR + ) + padded_img[:sy, :sx] = resized_img.astype(np.uint8) + + # Step 4: Slice the padded image into 4 quadrants and concatenate them into 12 channels + x0 = padded_img[0::2, 0::2, :] # Top-left + x1 = padded_img[1::2, 0::2, :] # Bottom-left + x2 = padded_img[0::2, 1::2, :] # Top-right + x3 = padded_img[1::2, 1::2, :] # Bottom-right + + # Step 5: Concatenate along the channel dimension (axis 2) + concatenated_img = np.concatenate([x0, x1, x2, x3], axis=2) + tensor_input = concatenated_img.astype(np.float32) + # Convert to CHW format (12, 320, 320) + tensor_input = np.transpose(tensor_input, (2, 0, 1)) + + # Add batch dimension → (1, 12, 320, 320) + tensor_input = np.expand_dims(tensor_input, axis=0) + + # Send frame to MemryX for processing + self.capture_queue.put(tensor_input) + self.capture_id_queue.put(connection_id) + + def process_input(self): + """Input callback function: wait for frames in the input queue, preprocess, and send to MX3 (return)""" + while True: + # Check if shutdown is requested + if self.stop_event and self.stop_event.is_set(): + logger.debug("[process_input] Stop event detected, returning None") + return None + try: + # Wait for a frame from the queue with timeout to check stop_event periodically + frame = self.capture_queue.get(block=True, timeout=0.5) + + return frame + + except Exception as e: + # Silently handle queue.Empty timeouts (expected during normal operation) + # Log any other unexpected exceptions + if "Empty" not in str(type(e).__name__): + logger.warning(f"[process_input] Unexpected error: {e}") + # Loop continues and will check stop_event at the top + + def receive_output(self): + """Retrieve processed results from MemryX output queue + a copy of the original frame""" + try: + # Get connection ID with timeout + connection_id = self.capture_id_queue.get( + block=True, timeout=1.0 + ) # Get the corresponding connection ID + detections = self.output_queue.get() # Get detections from MemryX + + return connection_id, detections + + except Exception as e: + # On timeout or stop event, return None + if self.stop_event and self.stop_event.is_set(): + logger.debug("[receive_output] Stop event detected, exiting") + # Silently handle queue.Empty timeouts, they're expected during normal operation + elif "Empty" not in str(type(e).__name__): + logger.warning(f"[receive_output] Error receiving output: {e}") + + return None, None + + def post_process_yolonas(self, output): + predictions = output[0] + + detections = np.zeros((20, 6), np.float32) + + for i, prediction in enumerate(predictions): + if i == 20: + break + + (_, x_min, y_min, x_max, y_max, confidence, class_id) = prediction + + if class_id < 0: + break + + detections[i] = [ + class_id, + confidence, + y_min / self.memx_model_height, + x_min / self.memx_model_width, + y_max / self.memx_model_height, + x_max / self.memx_model_width, + ] + + # Return the list of final detections + self.output_queue.put(detections) + + def process_yolo(self, class_id, conf, pos): + """ + Takes in class ID, confidence score, and array of [x, y, w, h] that describes detection position, + returns an array that's easily passable back to Frigate. + """ + return [ + class_id, # class ID + conf, # confidence score + (pos[1] - (pos[3] / 2)) / self.memx_model_height, # y_min + (pos[0] - (pos[2] / 2)) / self.memx_model_width, # x_min + (pos[1] + (pos[3] / 2)) / self.memx_model_height, # y_max + (pos[0] + (pos[2] / 2)) / self.memx_model_width, # x_max + ] + + def set_strides_grids(self): + grids = [] + expanded_strides = [] + + strides = [8, 16, 32] + + hsize_list = [self.memx_model_height // stride for stride in strides] + wsize_list = [self.memx_model_width // stride for stride in strides] + + for hsize, wsize, stride in zip(hsize_list, wsize_list, strides): + xv, yv = np.meshgrid(np.arange(wsize), np.arange(hsize)) + grid = np.stack((xv, yv), 2).reshape(1, -1, 2) + grids.append(grid) + shape = grid.shape[:2] + expanded_strides.append(np.full((*shape, 1), stride)) + self.grids = np.concatenate(grids, 1) + self.expanded_strides = np.concatenate(expanded_strides, 1) + + def sigmoid(self, x: np.ndarray) -> np.ndarray: + return 1 / (1 + np.exp(-x)) + + def onnx_concat(self, inputs: list, axis: int) -> np.ndarray: + # Ensure all inputs are numpy arrays + if not all(isinstance(x, np.ndarray) for x in inputs): + raise TypeError("All inputs must be numpy arrays.") + + # Ensure shapes match on non-concat axes + ref_shape = list(inputs[0].shape) + for i, tensor in enumerate(inputs[1:], start=1): + for ax in range(len(ref_shape)): + if ax == axis: + continue + if tensor.shape[ax] != ref_shape[ax]: + raise ValueError( + f"Shape mismatch at axis {ax} between input[0] and input[{i}]" + ) + + return np.concatenate(inputs, axis=axis) + + def onnx_reshape(self, data: np.ndarray, shape: np.ndarray) -> np.ndarray: + # Ensure shape is a 1D array of integers + target_shape = shape.astype(int).tolist() + + # Use NumPy reshape with dynamic handling of -1 + reshaped = np.reshape(data, target_shape) + + return reshaped + + def post_process_yolox(self, output): + output_785 = output[0] # 785 + output_794 = output[1] # 794 + output_795 = output[2] # 795 + output_811 = output[3] # 811 + output_820 = output[4] # 820 + output_821 = output[5] # 821 + output_837 = output[6] # 837 + output_846 = output[7] # 846 + output_847 = output[8] # 847 + + output_795 = self.sigmoid(output_795) + output_785 = self.sigmoid(output_785) + output_821 = self.sigmoid(output_821) + output_811 = self.sigmoid(output_811) + output_847 = self.sigmoid(output_847) + output_837 = self.sigmoid(output_837) + + concat_1 = self.onnx_concat([output_794, output_795, output_785], axis=1) + concat_2 = self.onnx_concat([output_820, output_821, output_811], axis=1) + concat_3 = self.onnx_concat([output_846, output_847, output_837], axis=1) + + shape = np.array([1, 85, -1], dtype=np.int64) + + reshape_1 = self.onnx_reshape(concat_1, shape) + reshape_2 = self.onnx_reshape(concat_2, shape) + reshape_3 = self.onnx_reshape(concat_3, shape) + + concat_out = self.onnx_concat([reshape_1, reshape_2, reshape_3], axis=2) + + output = concat_out.transpose(0, 2, 1) # 1, 840, 85 + + self.num_classes = output.shape[2] - 5 + + # [x, y, h, w, box_score, class_no_1, ..., class_no_80], + results = output + + results[..., :2] = (results[..., :2] + self.grids) * self.expanded_strides + results[..., 2:4] = np.exp(results[..., 2:4]) * self.expanded_strides + image_pred = results[0, ...] + + class_conf = np.max( + image_pred[:, 5 : 5 + self.num_classes], axis=1, keepdims=True + ) + class_pred = np.argmax(image_pred[:, 5 : 5 + self.num_classes], axis=1) + class_pred = np.expand_dims(class_pred, axis=1) + + conf_mask = (image_pred[:, 4] * class_conf.squeeze() >= 0.3).squeeze() + # Detections ordered as (x1, y1, x2, y2, obj_conf, class_conf, class_pred) + detections = np.concatenate((image_pred[:, :5], class_conf, class_pred), axis=1) + detections = detections[conf_mask] + + # Sort by class confidence (index 5) and keep top 20 detections + ordered = detections[detections[:, 5].argsort()[::-1]][:20] + + # Prepare a final detections array of shape (20, 6) + final_detections = np.zeros((20, 6), np.float32) + for i, object_detected in enumerate(ordered): + final_detections[i] = self.process_yolo( + object_detected[6], object_detected[5], object_detected[:4] + ) + + self.output_queue.put(final_detections) + + def post_process_ssdlite(self, outputs): + dets = outputs[0].squeeze(0) # Shape: (1, num_dets, 5) + labels = outputs[1].squeeze(0) + + detections = [] + + for i in range(dets.shape[0]): + x_min, y_min, x_max, y_max, confidence = dets[i] + class_id = int(labels[i]) # Convert label to integer + + if confidence < 0.45: + continue # Skip detections below threshold + + # Convert coordinates to integers + x_min, y_min, x_max, y_max = map(int, [x_min, y_min, x_max, y_max]) + + # Append valid detections [class_id, confidence, x, y, width, height] + detections.append([class_id, confidence, x_min, y_min, x_max, y_max]) + + final_detections = np.zeros((20, 6), np.float32) + + if len(detections) == 0: + # logger.info("No detections found.") + self.output_queue.put(final_detections) + return + + # Convert to NumPy array + detections = np.array(detections, dtype=np.float32) + + # Apply Non-Maximum Suppression (NMS) + bboxes = detections[:, 2:6].tolist() # (x_min, y_min, width, height) + scores = detections[:, 1].tolist() # Confidence scores + + indices = cv2.dnn.NMSBoxes(bboxes, scores, 0.45, 0.5) + + if len(indices) > 0: + indices = indices.flatten()[:20] # Keep only the top 20 detections + selected_detections = detections[indices] + + # Normalize coordinates AFTER NMS + for i, det in enumerate(selected_detections): + class_id, confidence, x_min, y_min, x_max, y_max = det + + # Normalize coordinates + x_min /= self.memx_model_width + y_min /= self.memx_model_height + x_max /= self.memx_model_width + y_max /= self.memx_model_height + + final_detections[i] = [class_id, confidence, y_min, x_min, y_max, x_max] + + self.output_queue.put(final_detections) + + def _generate_anchors(self, sizes=[80, 40, 20]): + """Generate anchor points for YOLOv9 style processing""" + yscales = [] + xscales = [] + for s in sizes: + r = np.arange(s) + 0.5 + yscales.append(np.repeat(r, s)) + xscales.append(np.repeat(r[None, ...], s, axis=0).flatten()) + + yscales = np.concatenate(yscales) + xscales = np.concatenate(xscales) + anchors = np.stack([xscales, yscales], axis=1) + return anchors + + def _generate_scales(self, sizes=[80, 40, 20]): + """Generate scaling factors for each detection level""" + factors = [8, 16, 32] + s = np.concatenate([np.ones([int(s * s)]) * f for s, f in zip(sizes, factors)]) + return s[:, None] + + @staticmethod + def _softmax(x: np.ndarray, axis: int) -> np.ndarray: + """Efficient softmax implementation""" + x = x - np.max(x, axis=axis, keepdims=True) + np.exp(x, out=x) + x /= np.sum(x, axis=axis, keepdims=True) + return x + + def dfl(self, x: np.ndarray) -> np.ndarray: + """Distribution Focal Loss decoding - YOLOv9 style""" + x = x.reshape(-1, 4, 16) + weights = np.arange(16, dtype=np.float32) + p = self._softmax(x, axis=2) + p = p * weights[None, None, :] + out = np.sum(p, axis=2, keepdims=False) + return out + + def dist2bbox( + self, x: np.ndarray, anchors: np.ndarray, scales: np.ndarray + ) -> np.ndarray: + """Convert distances to bounding boxes - YOLOv9 style""" + lt = x[:, :2] + rb = x[:, 2:] + + x1y1 = anchors - lt + x2y2 = anchors + rb + + wh = x2y2 - x1y1 + c_xy = (x1y1 + x2y2) / 2 + + out = np.concatenate([c_xy, wh], axis=1) + out = out * scales + return out + + def post_process_yolo_optimized(self, outputs): + """ + Custom YOLOv9 post-processing optimized for MemryX ONNX outputs. + Implements DFL decoding, confidence filtering, and NMS in pure NumPy. + """ + # YOLOv9 outputs: 6 outputs (lbox, lcls, mbox, mcls, sbox, scls) + conv_out1, conv_out2, conv_out3, conv_out4, conv_out5, conv_out6 = outputs + + # Determine grid sizes based on input resolution + # YOLOv9 uses 3 detection heads with strides [8, 16, 32] + # Grid sizes = input_size / stride + sizes = [ + self.memx_model_height + // 8, # Large objects (e.g., 80 for 640x640, 40 for 320x320) + self.memx_model_height + // 16, # Medium objects (e.g., 40 for 640x640, 20 for 320x320) + self.memx_model_height + // 32, # Small objects (e.g., 20 for 640x640, 10 for 320x320) + ] + + # Generate anchors and scales if not already done + if not hasattr(self, "anchors"): + self.anchors = self._generate_anchors(sizes) + self.scales = self._generate_scales(sizes) + + # Process outputs in YOLOv9 format: reshape and moveaxis for ONNX format + lbox = np.moveaxis(conv_out1, 1, -1) # Large boxes + lcls = np.moveaxis(conv_out2, 1, -1) # Large classes + mbox = np.moveaxis(conv_out3, 1, -1) # Medium boxes + mcls = np.moveaxis(conv_out4, 1, -1) # Medium classes + sbox = np.moveaxis(conv_out5, 1, -1) # Small boxes + scls = np.moveaxis(conv_out6, 1, -1) # Small classes + + # Determine number of classes dynamically from the class output shape + # lcls shape should be (batch, height, width, num_classes) + num_classes = lcls.shape[-1] + + # Validate that all class outputs have the same number of classes + if not (mcls.shape[-1] == num_classes and scls.shape[-1] == num_classes): + raise ValueError( + f"Class output shapes mismatch: lcls={lcls.shape}, mcls={mcls.shape}, scls={scls.shape}" + ) + + # Concatenate boxes and classes + boxes = np.concatenate( + [ + lbox.reshape(-1, 64), # 64 is for 4 bbox coords * 16 DFL bins + mbox.reshape(-1, 64), + sbox.reshape(-1, 64), + ], + axis=0, + ) + + classes = np.concatenate( + [ + lcls.reshape(-1, num_classes), + mcls.reshape(-1, num_classes), + scls.reshape(-1, num_classes), + ], + axis=0, + ) + + # Apply sigmoid to classes + classes = self.sigmoid(classes) + + # Apply DFL to box predictions + boxes = self.dfl(boxes) + + # YOLOv9 postprocessing with confidence filtering and NMS + confidence_thres = 0.4 + iou_thres = 0.6 + + # Find the class with the highest score for each detection + max_scores = np.max(classes, axis=1) # Maximum class score for each detection + class_ids = np.argmax(classes, axis=1) # Index of the best class + + # Filter out detections with scores below the confidence threshold + valid_indices = np.where(max_scores >= confidence_thres)[0] + if len(valid_indices) == 0: + # Return empty detections array + final_detections = np.zeros((20, 6), np.float32) + return final_detections + + # Select only valid detections + valid_boxes = boxes[valid_indices] + valid_class_ids = class_ids[valid_indices] + valid_scores = max_scores[valid_indices] + + # Convert distances to actual bounding boxes using anchors and scales + valid_boxes = self.dist2bbox( + valid_boxes, self.anchors[valid_indices], self.scales[valid_indices] + ) + + # Convert bounding box coordinates from (x_center, y_center, w, h) to (x_min, y_min, x_max, y_max) + x_center, y_center, width, height = ( + valid_boxes[:, 0], + valid_boxes[:, 1], + valid_boxes[:, 2], + valid_boxes[:, 3], + ) + x_min = x_center - width / 2 + y_min = y_center - height / 2 + x_max = x_center + width / 2 + y_max = y_center + height / 2 + + # Convert to format expected by cv2.dnn.NMSBoxes: [x, y, width, height] + boxes_for_nms = [] + scores_for_nms = [] + + for i in range(len(valid_indices)): + # Ensure coordinates are within bounds and positive + x_min_clipped = max(0, x_min[i]) + y_min_clipped = max(0, y_min[i]) + x_max_clipped = min(self.memx_model_width, x_max[i]) + y_max_clipped = min(self.memx_model_height, y_max[i]) + + width_clipped = x_max_clipped - x_min_clipped + height_clipped = y_max_clipped - y_min_clipped + + if width_clipped > 0 and height_clipped > 0: + boxes_for_nms.append( + [x_min_clipped, y_min_clipped, width_clipped, height_clipped] + ) + scores_for_nms.append(float(valid_scores[i])) + + final_detections = np.zeros((20, 6), np.float32) + + if len(boxes_for_nms) == 0: + return final_detections + + # Apply NMS using OpenCV + indices = cv2.dnn.NMSBoxes( + boxes_for_nms, scores_for_nms, confidence_thres, iou_thres + ) + + if len(indices) > 0: + # Flatten indices if they are returned as a list of arrays + if isinstance(indices[0], list) or isinstance(indices[0], np.ndarray): + indices = [i[0] for i in indices] + + # Limit to top 20 detections + indices = indices[:20] + + # Convert to Frigate format: [class_id, confidence, y_min, x_min, y_max, x_max] (normalized) + for i, idx in enumerate(indices): + class_id = valid_class_ids[idx] + confidence = valid_scores[idx] + + # Get the box coordinates + box = boxes_for_nms[idx] + x_min_norm = box[0] / self.memx_model_width + y_min_norm = box[1] / self.memx_model_height + x_max_norm = (box[0] + box[2]) / self.memx_model_width + y_max_norm = (box[1] + box[3]) / self.memx_model_height + + final_detections[i] = [ + class_id, + confidence, + y_min_norm, # Frigate expects y_min first + x_min_norm, + y_max_norm, + x_max_norm, + ] + + return final_detections + + def process_output(self, *outputs): + """Output callback function -- receives frames from the MX3 and triggers post-processing""" + if self.memx_model_type == ModelTypeEnum.yologeneric: + # Use complete YOLOv9-style postprocessing (includes NMS) + final_detections = self.post_process_yolo_optimized(outputs) + + self.output_queue.put(final_detections) + + elif self.memx_model_type == ModelTypeEnum.yolonas: + return self.post_process_yolonas(outputs) + + elif self.memx_model_type == ModelTypeEnum.yolox: + return self.post_process_yolox(outputs) + + elif self.memx_model_type == ModelTypeEnum.ssd: + return self.post_process_ssdlite(outputs) + + else: + raise Exception( + f"{self.memx_model_type} is currently not supported for memryx. See the docs for more info on supported models." + ) + + def set_stop_event(self, stop_event): + """Set the stop event for graceful shutdown.""" + self.stop_event = stop_event + + def shutdown(self): + """Gracefully shutdown the MemryX accelerator""" + try: + if hasattr(self, "accl") and self.accl is not None: + self.accl.shutdown() + logger.info("MemryX accelerator shutdown complete") + except Exception as e: + logger.error(f"Error during MemryX shutdown: {e}") + + def detect_raw(self, tensor_input: np.ndarray): + """Removed synchronous detect_raw() function so that we only use async""" + return 0 diff --git a/frigate/detectors/plugins/onnx.py b/frigate/detectors/plugins/onnx.py new file mode 100644 index 0000000..cbf1899 --- /dev/null +++ b/frigate/detectors/plugins/onnx.py @@ -0,0 +1,143 @@ +import logging +from typing import Literal + +import numpy as np +from pydantic import ConfigDict, Field + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detection_runners import get_optimized_runner +from frigate.detectors.detector_config import ( + BaseDetectorConfig, + InputDTypeEnum, + InputTensorEnum, + ModelTypeEnum, +) +from frigate.util.model import ( + post_process_dfine, + post_process_rfdetr, + post_process_yolo, + post_process_yolox, +) + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "onnx" + + +class ONNXDetectorConfig(BaseDetectorConfig): + """ONNX detector for running ONNX models; will use available acceleration backends (CUDA/ROCm/OpenVINO) when available.""" + + model_config = ConfigDict( + title="ONNX", + ) + + type: Literal[DETECTOR_KEY] + device: str = Field( + default="AUTO", + title="Device Type", + description="The device to use for ONNX inference (e.g. 'AUTO', 'CPU', 'GPU').", + ) + + +class ONNXDetector(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: ONNXDetectorConfig): + super().__init__(detector_config) + + path = detector_config.model.path + logger.info(f"ONNX: loading {detector_config.model.path}") + + self.runner = get_optimized_runner( + path, + detector_config.device, + model_type=detector_config.model.model_type, + ) + + self.onnx_model_type = detector_config.model.model_type + self.onnx_model_px = detector_config.model.input_pixel_format + self.onnx_model_shape = detector_config.model.input_tensor + + if self.onnx_model_type == ModelTypeEnum.yolox: + self.calculate_grids_strides() + + self._warmup(detector_config) + logger.info(f"ONNX: {path} loaded") + + def _warmup(self, detector_config: ONNXDetectorConfig) -> None: + """Run a warmup inference to front-load one-time compilation costs. + + Some GPU backends have a slow first inference: CUDA may need PTX JIT + compilation on newer architectures (e.g. NVIDIA 50-series / Blackwell), + and MIGraphX compiles the model graph on first run. Running it here + (during detector creation) keeps the watchdog start_time at 0.0 so the + process won't be killed. + """ + if detector_config.model.input_tensor == InputTensorEnum.nchw: + shape = (1, 3, detector_config.model.height, detector_config.model.width) + else: + shape = (1, detector_config.model.height, detector_config.model.width, 3) + + if detector_config.model.input_dtype in ( + InputDTypeEnum.float, + InputDTypeEnum.float_denorm, + ): + dtype = np.float32 + else: + dtype = np.uint8 + + logger.info("ONNX: warming up detector (may take a while on first run)...") + self.detect_raw(np.zeros(shape, dtype=dtype)) + + def detect_raw(self, tensor_input: np.ndarray): + if self.onnx_model_type == ModelTypeEnum.dfine: + tensor_output = self.runner.run( + { + "images": tensor_input, + "orig_target_sizes": np.array( + [[self.height, self.width]], dtype=np.int64 + ), + } + ) + return post_process_dfine(tensor_output, self.width, self.height) + + model_input_name = self.runner.get_input_names()[0] + tensor_output = self.runner.run({model_input_name: tensor_input}) + + if self.onnx_model_type == ModelTypeEnum.rfdetr: + return post_process_rfdetr(tensor_output) + elif self.onnx_model_type == ModelTypeEnum.yolonas: + predictions = tensor_output[0] + + detections = np.zeros((20, 6), np.float32) + + for i, prediction in enumerate(predictions): + if i == 20: + break + (_, x_min, y_min, x_max, y_max, confidence, class_id) = prediction + # when running in GPU mode, empty predictions in the output have class_id of -1 + if class_id < 0: + break + detections[i] = [ + class_id, + confidence, + y_min / self.height, + x_min / self.width, + y_max / self.height, + x_max / self.width, + ] + return detections + elif self.onnx_model_type == ModelTypeEnum.yologeneric: + return post_process_yolo(tensor_output, self.width, self.height) + elif self.onnx_model_type == ModelTypeEnum.yolox: + return post_process_yolox( + tensor_output[0], + self.width, + self.height, + self.grids, + self.expanded_strides, + ) + else: + raise Exception( + f"{self.onnx_model_type} is currently not supported for onnx. See the docs for more info on supported models." + ) diff --git a/frigate/detectors/plugins/openvino.py b/frigate/detectors/plugins/openvino.py new file mode 100644 index 0000000..5bd55d4 --- /dev/null +++ b/frigate/detectors/plugins/openvino.py @@ -0,0 +1,240 @@ +import logging +from typing import Literal + +import numpy as np +import openvino as ov +from pydantic import ConfigDict, Field + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detection_runners import OpenVINOModelRunner +from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum +from frigate.util.model import ( + post_process_dfine, + post_process_rfdetr, + post_process_yolo, +) + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "openvino" + + +class OvDetectorConfig(BaseDetectorConfig): + """OpenVINO detector for AMD and Intel CPUs, Intel GPUs and Intel VPU hardware.""" + + model_config = ConfigDict( + title="OpenVINO", + ) + + type: Literal[DETECTOR_KEY] + device: str = Field( + default=None, + title="Device Type", + description="The device to use for OpenVINO inference (e.g. 'CPU', 'GPU', 'NPU').", + ) + + +class OvDetector(DetectionApi): + type_key = DETECTOR_KEY + supported_models = [ + ModelTypeEnum.dfine, + ModelTypeEnum.rfdetr, + ModelTypeEnum.ssd, + ModelTypeEnum.yolonas, + ModelTypeEnum.yologeneric, + ModelTypeEnum.yolox, + ] + + def __init__(self, detector_config: OvDetectorConfig): + super().__init__(detector_config) + self.ov_model_type = detector_config.model.model_type + + self.h = detector_config.model.height + self.w = detector_config.model.width + + logger.info( + "Loading OpenVINO model %s on device %s", + detector_config.model.path, + detector_config.device, + ) + + self.runner = OpenVINOModelRunner( + model_path=detector_config.model.path, + device=detector_config.device, + model_type=detector_config.model.model_type, + ) + + # For dfine models, also pre-allocate target sizes tensor + if self.ov_model_type == ModelTypeEnum.dfine: + self.target_sizes_tensor = ov.Tensor( + np.array([[self.h, self.w]], dtype=np.int64) + ) + + self.model_invalid = False + + if self.ov_model_type not in self.supported_models: + logger.error( + f"OpenVino detector does not support {self.ov_model_type} models." + ) + self.model_invalid = True + + if self.ov_model_type == ModelTypeEnum.ssd: + model_inputs = self.runner.compiled_model.inputs + model_outputs = self.runner.compiled_model.outputs + + if len(model_inputs) != 1: + logger.error( + f"SSD models must only have 1 input. Found {len(model_inputs)}." + ) + self.model_invalid = True + if len(model_outputs) != 1: + logger.error( + f"SSD models must only have 1 output. Found {len(model_outputs)}." + ) + self.model_invalid = True + + output_shape = model_outputs[0].get_shape() + if output_shape[0] != 1 or output_shape[1] != 1 or output_shape[3] != 7: + logger.error(f"SSD model output doesn't match. Found {output_shape}.") + self.model_invalid = True + + if self.ov_model_type == ModelTypeEnum.yolonas: + model_inputs = self.runner.compiled_model.inputs + model_outputs = self.runner.compiled_model.outputs + + if len(model_inputs) != 1: + logger.error( + f"YoloNAS models must only have 1 input. Found {len(model_inputs)}." + ) + self.model_invalid = True + if len(model_outputs) != 1: + logger.error( + f"YoloNAS models must be exported in flat format and only have 1 output. Found {len(model_outputs)}." + ) + self.model_invalid = True + output_shape = model_outputs[0].partial_shape + if output_shape[-1] != 7: + logger.error( + f"YoloNAS models must be exported in flat format. Model output doesn't match. Found {output_shape}." + ) + self.model_invalid = True + + if self.ov_model_type == ModelTypeEnum.yolox: + self.output_indexes = 0 + while True: + try: + tensor_shape = self.runner.compiled_model.output( + self.output_indexes + ).shape + logger.info( + f"Model Output-{self.output_indexes} Shape: {tensor_shape}" + ) + self.output_indexes += 1 + except Exception: + logger.info(f"Model has {self.output_indexes} Output Tensors") + break + self.num_classes = tensor_shape[2] - 5 + logger.info(f"YOLOX model has {self.num_classes} classes") + self.calculate_grids_strides() + + ## Takes in class ID, confidence score, and array of [x, y, w, h] that describes detection position, + ## returns an array that's easily passable back to Frigate. + def process_yolo(self, class_id, conf, pos): + return [ + class_id, # class ID + conf, # confidence score + (pos[1] - (pos[3] / 2)) / self.h, # y_min + (pos[0] - (pos[2] / 2)) / self.w, # x_min + (pos[1] + (pos[3] / 2)) / self.h, # y_max + (pos[0] + (pos[2] / 2)) / self.w, # x_max + ] + + def detect_raw(self, tensor_input): + if self.model_invalid: + return np.zeros((20, 6), np.float32) + + if self.ov_model_type == ModelTypeEnum.dfine: + # Use named inputs for dfine models + inputs = { + "images": tensor_input, + "orig_target_sizes": np.array([[self.h, self.w]], dtype=np.int64), + } + outputs = self.runner.run(inputs) + tensor_output = ( + outputs[0], + outputs[1], + outputs[2], + ) + return post_process_dfine(tensor_output, self.w, self.h) + + # Run inference using the runner + input_name = self.runner.get_input_names()[0] + outputs = self.runner.run({input_name: tensor_input}) + + detections = np.zeros((20, 6), np.float32) + + if self.ov_model_type == ModelTypeEnum.rfdetr: + return post_process_rfdetr(outputs) + elif self.ov_model_type == ModelTypeEnum.ssd: + results = outputs[0][0][0] + + for i, (_, class_id, score, xmin, ymin, xmax, ymax) in enumerate(results): + if i == 20: + break + detections[i] = [ + class_id, + float(score), + ymin, + xmin, + ymax, + xmax, + ] + return detections + elif self.ov_model_type == ModelTypeEnum.yolonas: + predictions = outputs[0] + + for i, prediction in enumerate(predictions): + if i == 20: + break + (_, x_min, y_min, x_max, y_max, confidence, class_id) = prediction + # when running in GPU mode, empty predictions in the output have class_id of -1 + if class_id < 0: + break + detections[i] = [ + class_id, + confidence, + y_min / self.h, + x_min / self.w, + y_max / self.h, + x_max / self.w, + ] + return detections + elif self.ov_model_type == ModelTypeEnum.yologeneric: + return post_process_yolo(outputs, self.w, self.h) + elif self.ov_model_type == ModelTypeEnum.yolox: + # [x, y, h, w, box_score, class_no_1, ..., class_no_80], + results = outputs[0] + results[..., :2] = (results[..., :2] + self.grids) * self.expanded_strides + results[..., 2:4] = np.exp(results[..., 2:4]) * self.expanded_strides + image_pred = results[0, ...] + + class_conf = np.max( + image_pred[:, 5 : 5 + self.num_classes], axis=1, keepdims=True + ) + class_pred = np.argmax(image_pred[:, 5 : 5 + self.num_classes], axis=1) + class_pred = np.expand_dims(class_pred, axis=1) + + conf_mask = (image_pred[:, 4] * class_conf.squeeze() >= 0.3).squeeze() + # Detections ordered as (x1, y1, x2, y2, obj_conf, class_conf, class_pred) + detections = np.concatenate( + (image_pred[:, :5], class_conf, class_pred), axis=1 + ) + detections = detections[conf_mask] + + ordered = detections[detections[:, 5].argsort()[::-1]][:20] + + for i, object_detected in enumerate(ordered): + detections[i] = self.process_yolo( + object_detected[6], object_detected[5], object_detected[:4] + ) + return detections diff --git a/frigate/detectors/plugins/rknn.py b/frigate/detectors/plugins/rknn.py new file mode 100644 index 0000000..7bf5e03 --- /dev/null +++ b/frigate/detectors/plugins/rknn.py @@ -0,0 +1,326 @@ +import logging +import os.path +import re +import urllib.request +from typing import Literal + +import cv2 +import numpy as np +from pydantic import ConfigDict, Field + +from frigate.const import MODEL_CACHE_DIR, SUPPORTED_RK_SOCS +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detection_runners import RKNNModelRunner +from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum +from frigate.util.model import post_process_yolo +from frigate.util.rknn_converter import auto_convert_model + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "rknn" + +supported_models = { + ModelTypeEnum.yologeneric: "^frigate-fp16-yolov9-[cemst]$", + ModelTypeEnum.yolonas: "^deci-fp16-yolonas_[sml]$", + ModelTypeEnum.yolox: "^rock-(fp16|i8)-yolox_(nano|tiny)$", +} + +model_cache_dir = os.path.join(MODEL_CACHE_DIR, "rknn_cache/") + + +class RknnDetectorConfig(BaseDetectorConfig): + """RKNN detector for Rockchip NPUs; runs compiled RKNN models on Rockchip hardware.""" + + model_config = ConfigDict( + title="RKNN", + ) + + type: Literal[DETECTOR_KEY] + num_cores: int = Field( + default=0, + ge=0, + le=3, + title="Number of NPU cores to use.", + description="The number of NPU cores to use (0 for auto).", + ) + + +class Rknn(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, config: RknnDetectorConfig): + super().__init__(config) + self.height = config.model.height + self.width = config.model.width + core_mask = 2**config.num_cores - 1 + soc = self.get_soc() + + model_path = config.model.path or "deci-fp16-yolonas_s" + + model_props = self.parse_model_input(model_path, soc) + + if self.detector_config.model.model_type == ModelTypeEnum.yolox: + self.calculate_grids_strides(expanded=False) + + if model_props["preset"]: + config.model.model_type = model_props["model_type"] + + if model_props["model_type"] == ModelTypeEnum.yolonas: + logger.info( + "You are using yolo-nas with weights from DeciAI. " + "These weights are subject to their license and can't be used commercially. " + "For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html" + ) + + self.runner = RKNNModelRunner( + model_path=model_props["path"], + model_type=config.model.model_type.value + if config.model.model_type + else None, + core_mask=core_mask, + ) + + def __del__(self): + if hasattr(self, "runner") and self.runner: + # The runner's __del__ method will handle cleanup + pass + + def get_soc(self): + try: + with open("/proc/device-tree/compatible") as file: + soc = file.read().split(",")[-1].strip("\x00") + except FileNotFoundError: + raise Exception("Make sure to run docker in privileged mode.") from None + + if soc not in SUPPORTED_RK_SOCS: + raise Exception( + f"Your SoC is not supported. Your SoC is: {soc}. Currently these SoCs are supported: {SUPPORTED_RK_SOCS}." + ) + + return soc + + def parse_model_input(self, model_path, soc): + model_props = {} + + # find out if user provides his own model + # user provided models should be a path and contain a "/" + if "/" in model_path: + model_props["preset"] = False + + # Check if this is an ONNX model or model without extension that needs conversion + if model_path.endswith(".onnx") or not os.path.splitext(model_path)[1]: + # Try to auto-convert to RKNN format + logger.info( + f"Attempting to auto-convert {model_path} to RKNN format..." + ) + + # Determine model type from config + model_type = self.detector_config.model.model_type + + # Convert enum to string if needed + model_type_str = model_type.value if model_type else None + + # Auto-convert the model + converted_path = auto_convert_model(model_path, model_type_str) + + if converted_path: + model_props["path"] = converted_path + logger.info(f"Successfully converted model to: {converted_path}") + else: + # Fall back to original path if conversion fails + logger.warning( + f"Failed to convert {model_path} to RKNN format, using original path" + ) + model_props["path"] = model_path + else: + model_props["path"] = model_path + else: + model_props["preset"] = True + + """ + Filenames follow this pattern: + origin-quant-basename-soc-tk_version-rev.rknn + origin: From where comes the model? default: upstream repo; rknn: modifications from airockchip + quant: i8 or fp16 + basename: e.g. yolonas_s + soc: e.g. rk3588 + tk_version: e.g. v2.0.0 + rev: e.g. 1 + + Full name could be: default-fp16-yolonas_s-rk3588-v2.0.0-1.rknn + """ + + 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 + f"-{soc}-v2.3.2-2.rknn" + + 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) + + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + urllib.request.urlretrieve( + f"{GITHUB_ENDPOINT}/MarcA711/rknn-models/releases/download/v2.3.2-2/{filename}", + model_cache_dir + filename, + ) + + def post_process_yolonas(self, output: list[np.ndarray]): + """ + @param output: output of inference + expected shape: [np.array(1, N, 4), np.array(1, N, 80)] + where N depends on the input size e.g. N=2100 for 320x320 images + + @return: best results: np.array(20, 6) where each row is + in this order (class_id, score, y1/height, x1/width, y2/height, x2/width) + """ + + N = output[0].shape[1] + + boxes = output[0].reshape(N, 4) + scores = output[1].reshape(N, 80) + + class_ids = np.argmax(scores, axis=1) + scores = scores[np.arange(N), class_ids] + + args_best = np.argwhere(scores > self.thresh)[:, 0] + + num_matches = len(args_best) + if num_matches == 0: + return np.zeros((20, 6), np.float32) + elif num_matches > 20: + args_best20 = np.argpartition(scores[args_best], -20)[-20:] + args_best = args_best[args_best20] + + boxes = boxes[args_best] + class_ids = class_ids[args_best] + scores = scores[args_best] + + boxes = np.transpose( + np.vstack( + ( + boxes[:, 1] / self.height, + boxes[:, 0] / self.width, + boxes[:, 3] / self.height, + boxes[:, 2] / self.width, + ) + ) + ) + + results = np.hstack( + (class_ids[..., np.newaxis], scores[..., np.newaxis], boxes) + ) + + return np.resize(results, (20, 6)) + + def post_process_yolox( + self, + predictions: list[np.ndarray], + grids: np.ndarray, + expanded_strides: np.ndarray, + ) -> np.ndarray: + def sp_flatten(_in: np.ndarray): + ch = _in.shape[1] + _in = _in.transpose(0, 2, 3, 1) + return _in.reshape(-1, ch) + + boxes, scores, classes_conf = [], [], [] + + input_data = [ + _in.reshape([1, -1] + list(_in.shape[-2:])) for _in in predictions + ] + + for i in range(len(input_data)): + unprocessed_box = input_data[i][:, :4, :, :] + box_xy = unprocessed_box[:, :2, :, :] + box_wh = np.exp(unprocessed_box[:, 2:4, :, :]) * expanded_strides[i] + + box_xy += grids[i] + box_xy *= expanded_strides[i] + box = np.concatenate((box_xy, box_wh), axis=1) + + # Convert [c_x, c_y, w, h] to [x1, y1, x2, y2] + xyxy = np.copy(box) + xyxy[:, 0, :, :] = box[:, 0, :, :] - box[:, 2, :, :] / 2 # top left x + xyxy[:, 1, :, :] = box[:, 1, :, :] - box[:, 3, :, :] / 2 # top left y + xyxy[:, 2, :, :] = box[:, 0, :, :] + box[:, 2, :, :] / 2 # bottom right x + xyxy[:, 3, :, :] = box[:, 1, :, :] + box[:, 3, :, :] / 2 # bottom right y + + boxes.append(xyxy) + scores.append(input_data[i][:, 4:5, :, :]) + classes_conf.append(input_data[i][:, 5:, :, :]) + + # flatten data + boxes = np.concatenate([sp_flatten(_v) for _v in boxes]) + classes_conf = np.concatenate([sp_flatten(_v) for _v in classes_conf]) + scores = np.concatenate([sp_flatten(_v) for _v in scores]) + + # reshape and filter boxes + box_confidences = scores.reshape(-1) + class_max_score = np.max(classes_conf, axis=-1) + classes = np.argmax(classes_conf, axis=-1) + _class_pos = np.where(class_max_score * box_confidences >= 0.4) + scores = (class_max_score * box_confidences)[_class_pos] + boxes = boxes[_class_pos] + classes = classes[_class_pos] + + # run nms + indices = cv2.dnn.NMSBoxes( + bboxes=boxes, + scores=scores, + score_threshold=0.4, + nms_threshold=0.4, + ) + + results = np.zeros((20, 6), np.float32) + + if len(indices) > 0: + for i, idx in enumerate(indices.flatten()[:20]): + box = boxes[idx] + results[i] = [ + classes[idx], + scores[idx], + box[1] / self.height, + box[0] / self.width, + box[3] / self.height, + box[2] / self.width, + ] + + return results + + def post_process(self, output): + if self.detector_config.model.model_type == ModelTypeEnum.yolonas: + return self.post_process_yolonas(output) + elif self.detector_config.model.model_type == ModelTypeEnum.yologeneric: + return post_process_yolo(output, self.width, self.height) + elif self.detector_config.model.model_type == ModelTypeEnum.yolox: + return self.post_process_yolox(output, self.grids, self.expanded_strides) + else: + raise ValueError( + f'Model type "{self.detector_config.model.model_type}" is currently not supported.' + ) + + def detect_raw(self, tensor_input): + # Prepare input for the runner + inputs = {"input": tensor_input} + output = self.runner.run(inputs) + return self.post_process(output) diff --git a/frigate/detectors/plugins/synaptics.py b/frigate/detectors/plugins/synaptics.py new file mode 100644 index 0000000..6d0ac7f --- /dev/null +++ b/frigate/detectors/plugins/synaptics.py @@ -0,0 +1,110 @@ +import logging +import os +from typing import Literal + +import numpy as np +from pydantic import ConfigDict + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import ( + BaseDetectorConfig, + InputTensorEnum, + ModelTypeEnum, +) + +try: + from synap import Network + from synap.postprocessor import Detector + from synap.preprocessor import Preprocessor + from synap.types import Layout, Shape + + SYNAP_SUPPORT = True +except ImportError: + SYNAP_SUPPORT = False + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "synaptics" + + +class SynapDetectorConfig(BaseDetectorConfig): + """Synaptics NPU detector for models in .synap format using the Synap SDK on Synaptics hardware.""" + + model_config = ConfigDict( + title="Synaptics", + ) + + type: Literal[DETECTOR_KEY] + + +class SynapDetector(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: SynapDetectorConfig): + if not SYNAP_SUPPORT: + logger.error( + "Error importing Synaptics SDK modules. You must use the -synaptics Docker image variant for Synaptics detector support." + ) + return + + try: + _, ext = os.path.splitext(detector_config.model.path) + if ext and ext != ".synap": + raise ValueError("Model path config for Synap1680 is incorrect.") + + synap_network = Network(detector_config.model.path) + logger.info(f"Synap NPU loaded model: {detector_config.model.path}") + except ValueError as ve: + logger.error(f"Synap1680 setup has failed: {ve}") + raise + except Exception as e: + logger.error(f"Failed to init Synap NPU: {e}") + raise + + self.width = detector_config.model.width + self.height = detector_config.model.height + self.model_type = detector_config.model.model_type + self.network = synap_network + self.network_input_details = self.network.inputs[0] + self.input_tensor_layout = detector_config.model.input_tensor + + # Create Inference Engine + self.preprocessor = Preprocessor() + self.detector = Detector(score_threshold=0.4, iou_threshold=0.4) + + def detect_raw(self, tensor_input: np.ndarray): + # It has only been testing for pre-converted mobilenet80 .tflite -> .synap model currently + layout = Layout.nhwc # default layout + detections = np.zeros((20, 6), np.float32) + + if self.input_tensor_layout == InputTensorEnum.nhwc: + layout = Layout.nhwc + + postprocess_data = self.preprocessor.assign( + self.network.inputs, tensor_input, Shape(tensor_input.shape), layout + ) + output_tensor_obj = self.network.predict() + output = self.detector.process(output_tensor_obj, postprocess_data) + + if self.model_type == ModelTypeEnum.ssd: + for i, item in enumerate(output.items): + if i == 20: + break + + bb = item.bounding_box + # Convert corner coordinates to normalized [0,1] range + x1 = bb.origin.x / self.width # Top-left X + y1 = bb.origin.y / self.height # Top-left Y + x2 = (bb.origin.x + bb.size.x) / self.width # Bottom-right X + y2 = (bb.origin.y + bb.size.y) / self.height # Bottom-right Y + detections[i] = [ + item.class_index, + float(item.confidence), + y1, + x1, + y2, + x2, + ] + else: + logger.error(f"Unsupported model type: {self.model_type}") + return detections diff --git a/frigate/detectors/plugins/teflon_tfl.py b/frigate/detectors/plugins/teflon_tfl.py new file mode 100644 index 0000000..7dd44e6 --- /dev/null +++ b/frigate/detectors/plugins/teflon_tfl.py @@ -0,0 +1,45 @@ +import logging +from typing import Literal + +from pydantic import ConfigDict + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +from ..detector_utils import ( + tflite_detect_raw, + tflite_init, + tflite_load_delegate_interpreter, +) + +logger = logging.getLogger(__name__) + +# Use _tfl suffix to default tflite model +DETECTOR_KEY = "teflon_tfl" + + +class TeflonDetectorConfig(BaseDetectorConfig): + """Teflon delegate detector for TFLite using Mesa Teflon delegate library to accelerate inference on supported GPUs.""" + + model_config = ConfigDict( + title="Teflon", + ) + + type: Literal[DETECTOR_KEY] + + +class TeflonTfl(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: TeflonDetectorConfig): + # Location in Debian's mesa-teflon-delegate + delegate_library = "/usr/lib/teflon/libteflon.so" + device_config = {} + + interpreter = tflite_load_delegate_interpreter( + delegate_library, detector_config, device_config + ) + tflite_init(self, interpreter) + + def detect_raw(self, tensor_input): + return tflite_detect_raw(self, tensor_input) diff --git a/frigate/detectors/plugins/tensorrt.py b/frigate/detectors/plugins/tensorrt.py new file mode 100644 index 0000000..6b39212 --- /dev/null +++ b/frigate/detectors/plugins/tensorrt.py @@ -0,0 +1,355 @@ +import ctypes +import logging +import platform + +import numpy as np + +try: + import tensorrt as trt + from cuda import cuda + + TRT_VERSION = int(trt.__version__[0 : trt.__version__.find(".")]) + + TRT_SUPPORT = True +except ModuleNotFoundError: + TRT_SUPPORT = False + +from typing import Literal + +from pydantic import ConfigDict, Field + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "tensorrt" + +if TRT_SUPPORT: + + class TrtLogger(trt.ILogger): + def log(self, severity, msg): + logger.log(self.getSeverity(severity), msg) + + def getSeverity(self, sev: trt.ILogger.Severity) -> int: + if sev == trt.ILogger.VERBOSE: + return logging.DEBUG + elif sev == trt.ILogger.INFO: + return logging.INFO + elif sev == trt.ILogger.WARNING: + return logging.WARNING + elif sev == trt.ILogger.ERROR: + return logging.ERROR + elif sev == trt.ILogger.INTERNAL_ERROR: + return logging.CRITICAL + else: + return logging.DEBUG + + +class TensorRTDetectorConfig(BaseDetectorConfig): + """TensorRT detector for Nvidia Jetson devices using serialized TensorRT engines for accelerated inference.""" + + model_config = ConfigDict( + title="TensorRT", + ) + + type: Literal[DETECTOR_KEY] + device: int = Field( + default=0, title="GPU Device Index", description="The GPU device index to use." + ) + + +class HostDeviceMem: + """Simple helper data class that's a little nicer to use than a 2-tuple.""" + + def __init__(self, host_mem, device_mem, nbytes, size): + self.host = host_mem + err, self.host_dev = cuda.cuMemHostGetDevicePointer(self.host, 0) + self.device = device_mem + self.nbytes = nbytes + self.size = size + + def __str__(self): + return "Host:\n" + str(self.host) + "\nDevice:\n" + str(self.device) + + def __repr__(self): + return self.__str__() + + def __del__(self): + cuda.cuMemFreeHost(self.host) + cuda.cuMemFree(self.device) + + +class TensorRtDetector(DetectionApi): + type_key = DETECTOR_KEY + + def _load_engine(self, model_path): + try: + trt.init_libnvinfer_plugins(self.trt_logger, "") + + ctypes.cdll.LoadLibrary("/usr/local/lib/libyolo_layer.so") + except OSError as e: + logger.error( + "ERROR: failed to load libraries. %s", + e, + ) + + with open(model_path, "rb") as f, trt.Runtime(self.trt_logger) as runtime: + return runtime.deserialize_cuda_engine(f.read()) + + def _binding_is_input(self, binding): + if TRT_VERSION < 10: + return self.engine.binding_is_input(binding) + else: + return binding == "input" + + def _get_binding_dims(self, binding): + if TRT_VERSION < 10: + return self.engine.get_binding_shape(binding) + else: + return self.engine.get_tensor_shape(binding) + + def _get_binding_dtype(self, binding): + if TRT_VERSION < 10: + return self.engine.get_binding_dtype(binding) + else: + return self.engine.get_tensor_dtype(binding) + + def _execute(self): + if TRT_VERSION < 10: + return self.context.execute_async_v2( + bindings=self.bindings, stream_handle=self.stream + ) + else: + return self.context.execute_v2(self.bindings) + + def _get_input_shape(self): + """Get input shape of the TensorRT YOLO engine.""" + binding = self.engine[0] + assert self._binding_is_input(binding) + binding_dims = self._get_binding_dims(binding) + if len(binding_dims) == 4: + return ( + tuple(binding_dims[2:]), + trt.nptype(self._get_binding_dtype(binding)), + ) + elif len(binding_dims) == 3: + return ( + tuple(binding_dims[1:]), + trt.nptype(self._get_binding_dtype(binding)), + ) + else: + raise ValueError( + "bad dims of binding %s: %s" % (binding, str(binding_dims)) + ) + + def _allocate_buffers(self): + """Allocates all host/device in/out buffers required for an engine.""" + inputs = [] + outputs = [] + bindings = [] + output_idx = 0 + for binding in self.engine: + binding_dims = self._get_binding_dims(binding) + if len(binding_dims) == 4: + # explicit batch case (TensorRT 7+) + size = trt.volume(binding_dims) + elif len(binding_dims) == 3: + # implicit batch case (TensorRT 6 or older) + size = trt.volume(binding_dims) * self.engine.max_batch_size + else: + raise ValueError( + "bad dims of binding %s: %s" % (binding, str(binding_dims)) + ) + nbytes = size * self._get_binding_dtype(binding).itemsize + # Allocate host and device buffers + err, host_mem = cuda.cuMemHostAlloc( + nbytes, Flags=cuda.CU_MEMHOSTALLOC_DEVICEMAP + ) + assert err is cuda.CUresult.CUDA_SUCCESS, f"cuMemAllocHost returned {err}" + logger.debug( + f"Allocated Tensor Binding {binding} Memory {nbytes} Bytes ({size} * {self._get_binding_dtype(binding)})" + ) + err, device_mem = cuda.cuMemAlloc(nbytes) + assert err is cuda.CUresult.CUDA_SUCCESS, f"cuMemAlloc returned {err}" + # Append the device buffer to device bindings. + bindings.append(int(device_mem)) + # Append to the appropriate list. + if self._binding_is_input(binding): + logger.debug(f"Input has Shape {binding_dims}") + inputs.append(HostDeviceMem(host_mem, device_mem, nbytes, size)) + else: + # each grid has 3 anchors, each anchor generates a detection + # output of 7 float32 values + assert size % 7 == 0, f"output size was {size}" + logger.debug(f"Output has Shape {binding_dims}") + outputs.append(HostDeviceMem(host_mem, device_mem, nbytes, size)) + output_idx += 1 + assert len(inputs) == 1, f"inputs len was {len(inputs)}" + assert len(outputs) == 1, f"output len was {len(outputs)}" + return inputs, outputs, bindings + + def _do_inference(self): + """do_inference (for TensorRT 7.0+) + This function is generalized for multiple inputs/outputs for full + dimension networks. + Inputs and outputs are expected to be lists of HostDeviceMem objects. + """ + # Push CUDA Context + cuda.cuCtxPushCurrent(self.cu_ctx) + + # Transfer input data to the GPU. + [ + cuda.cuMemcpyHtoDAsync(inp.device, inp.host, inp.nbytes, self.stream) + for inp in self.inputs + ] + + # Run inference. + if not self._execute(): + logger.warning("Execute returned false") + + # Transfer predictions back from the GPU. + [ + cuda.cuMemcpyDtoHAsync(out.host, out.device, out.nbytes, self.stream) + for out in self.outputs + ] + + # Synchronize the stream + cuda.cuStreamSynchronize(self.stream) + + # Pop CUDA Context + cuda.cuCtxPopCurrent() + + # Return only the host outputs. + return [ + np.array( + (ctypes.c_float * out.size).from_address(out.host), dtype=np.float32 + ) + for out in self.outputs + ] + + def __init__(self, detector_config: TensorRTDetectorConfig): + if platform.machine() == "x86_64": + logger.error( + "TensorRT detector is no longer supported on amd64 system. Please use ONNX detector instead, see https://docs.frigate.video/configuration/object_detectors#onnx for more information." + ) + raise ImportError( + "TensorRT detector is no longer supported on amd64 system. Please use ONNX detector instead, see https://docs.frigate.video/configuration/object_detectors#onnx for more information." + ) + + assert TRT_SUPPORT, ( + f"TensorRT libraries not found, {DETECTOR_KEY} detector not present" + ) + + (cuda_err,) = cuda.cuInit(0) + assert cuda_err == cuda.CUresult.CUDA_SUCCESS, ( + f"Failed to initialize cuda {cuda_err}" + ) + err, dev_count = cuda.cuDeviceGetCount() + logger.debug(f"Num Available Devices: {dev_count}") + assert detector_config.device < dev_count, ( + f"Invalid TensorRT Device Config. Device {detector_config.device} Invalid." + ) + err, self.cu_ctx = cuda.cuCtxCreate( + cuda.CUctx_flags.CU_CTX_MAP_HOST, detector_config.device + ) + + self.conf_th = 0.4 ##TODO: model config parameter + self.nms_threshold = 0.4 + err, self.stream = cuda.cuStreamCreate(0) + self.trt_logger = TrtLogger() + self.engine = self._load_engine(detector_config.model.path) + self.input_shape = self._get_input_shape() + + try: + self.context = self.engine.create_execution_context() + ( + self.inputs, + self.outputs, + self.bindings, + ) = self._allocate_buffers() + except Exception as e: + logger.error(e) + raise RuntimeError("fail to allocate CUDA resources") from e + + logger.debug("TensorRT loaded. Input shape is %s", self.input_shape) + logger.debug("TensorRT version is %s", TRT_VERSION) + + def __del__(self): + """Free CUDA memories.""" + if self.outputs is not None: + del self.outputs + if self.inputs is not None: + del self.inputs + if self.stream is not None: + cuda.cuStreamDestroy(self.stream) + del self.stream + del self.engine + del self.context + del self.trt_logger + cuda.cuCtxDestroy(self.cu_ctx) + + def _postprocess_yolo(self, trt_outputs, conf_th): + """Postprocess TensorRT outputs. + # Args + trt_outputs: a list of 2 or 3 tensors, where each tensor + contains a multiple of 7 float32 numbers in + the order of [x, y, w, h, box_confidence, class_id, class_prob] + conf_th: confidence threshold + # Returns + boxes, scores, classes + """ + # filter low-conf detections and concatenate results of all yolo layers + detection_list = [] + for o in trt_outputs: + detections = o.reshape((-1, 7)) + detections = detections[detections[:, 4] * detections[:, 6] >= conf_th] + detection_list.append(detections) + detection_list = np.concatenate(detection_list, axis=0) + + return detection_list + + def detect_raw(self, tensor_input): + # Input tensor has the shape of the [height, width, 3] + # Output tensor of float32 of shape [20, 6] where: + # O - class id + # 1 - score + # 2..5 - a value between 0 and 1 of the box: [top, left, bottom, right] + + # normalize + if self.input_shape[-1] != trt.int8: + tensor_input = tensor_input.astype(self.input_shape[-1]) + tensor_input /= 255.0 + + self.inputs[0].host = np.ascontiguousarray( + tensor_input.astype(self.input_shape[-1]) + ) + trt_outputs = self._do_inference() + + raw_detections = self._postprocess_yolo(trt_outputs, self.conf_th) + + if len(raw_detections) == 0: + return np.zeros((20, 6), np.float32) + + # raw_detections: Nx7 numpy arrays of + # [[x, y, w, h, box_confidence, class_id, class_prob], + + # Calculate score as box_confidence x class_prob + raw_detections[:, 4] = raw_detections[:, 4] * raw_detections[:, 6] + # Reorder elements by the score, best on top, remove class_prob + ordered = raw_detections[raw_detections[:, 4].argsort()[::-1]][:, 0:6] + # transform width to right with clamp to 0..1 + ordered[:, 2] = np.clip(ordered[:, 2] + ordered[:, 0], 0, 1) + # transform height to bottom with clamp to 0..1 + ordered[:, 3] = np.clip(ordered[:, 3] + ordered[:, 1], 0, 1) + # put result into the correct order and limit to top 20 + detections = ordered[:, [5, 4, 1, 0, 3, 2]][:20] + + # pad to 20x6 shape + append_cnt = 20 - len(detections) + if append_cnt > 0: + detections = np.append( + detections, np.zeros((append_cnt, 6), np.float32), axis=0 + ) + + return detections diff --git a/frigate/detectors/plugins/zmq_ipc.py b/frigate/detectors/plugins/zmq_ipc.py new file mode 100644 index 0000000..cc9a538 --- /dev/null +++ b/frigate/detectors/plugins/zmq_ipc.py @@ -0,0 +1,344 @@ +import json +import logging +import os +from typing import Any, Literal + +import numpy as np +import zmq +from pydantic import ConfigDict, Field + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "zmq" + + +class ZmqDetectorConfig(BaseDetectorConfig): + """ZMQ IPC detector that offloads inference to an external process via a ZeroMQ IPC endpoint.""" + + model_config = ConfigDict( + title="ZMQ IPC", + ) + + type: Literal[DETECTOR_KEY] + endpoint: str = Field( + default="ipc:///tmp/cache/zmq_detector", + title="ZMQ IPC endpoint", + description="The ZMQ endpoint to connect to.", + ) + request_timeout_ms: int = Field( + default=200, + title="ZMQ request timeout in milliseconds", + description="Timeout for ZMQ requests in milliseconds.", + ) + linger_ms: int = Field( + default=0, + title="ZMQ socket linger in milliseconds", + description="Socket linger period in milliseconds.", + ) + + +class ZmqIpcDetector(DetectionApi): + """ + ZMQ-based detector plugin using a REQ/REP socket over an IPC endpoint. + + Protocol: + - Request is sent as a multipart message: + [ header_json_bytes, tensor_bytes ] + where header is a JSON object containing: + { + "shape": List[int], + "dtype": str, # numpy dtype string, e.g. "uint8", "float32" + } + tensor_bytes are the raw bytes of the numpy array in C-order. + + - Response is expected to be either: + a) Multipart [ header_json_bytes, tensor_bytes ] with header specifying + shape [20,6] and dtype "float32"; or + b) Single frame tensor_bytes of length 20*6*4 bytes (float32). + + On any error or timeout, this detector returns a zero array of shape (20, 6). + + Model Management: + - On initialization, sends model request to check if model is available + - If model not available, sends model data via ZMQ + - Only starts inference after model is ready + """ + + type_key = DETECTOR_KEY + + def __init__(self, detector_config: ZmqDetectorConfig): + super().__init__(detector_config) + + self._context = zmq.Context() + self._endpoint = detector_config.endpoint + self._request_timeout_ms = detector_config.request_timeout_ms + self._linger_ms = detector_config.linger_ms + self._socket = None + self._create_socket() + + # Model management + self._model_ready = False + self._model_name = self._get_model_name() + + # Initialize model if needed + self._initialize_model() + + # Preallocate zero result for error paths + self._zero_result = np.zeros((20, 6), np.float32) + + def _create_socket(self) -> None: + if self._socket is not None: + try: + self._socket.close(linger=self._linger_ms) + except Exception: + pass + self._socket = self._context.socket(zmq.REQ) + # Apply timeouts and linger so calls don't block indefinitely + self._socket.setsockopt(zmq.RCVTIMEO, self._request_timeout_ms) + self._socket.setsockopt(zmq.SNDTIMEO, self._request_timeout_ms) + self._socket.setsockopt(zmq.LINGER, self._linger_ms) + + logger.debug(f"ZMQ detector connecting to {self._endpoint}") + self._socket.connect(self._endpoint) + + def _get_model_name(self) -> str: + """Get the model filename from the detector config.""" + model_path = self.detector_config.model.path + return os.path.basename(model_path) + + def _initialize_model(self) -> None: + """Initialize the model by checking availability and transferring if needed.""" + try: + logger.info(f"Initializing model: {self._model_name}") + + # Check if model is available and transfer if needed + if self._check_and_transfer_model(): + logger.info(f"Model {self._model_name} is ready") + self._model_ready = True + else: + logger.error(f"Failed to initialize model {self._model_name}") + + except Exception as e: + logger.error(f"Failed to initialize model: {e}") + + def _check_and_transfer_model(self) -> bool: + """Check if model is available and transfer if needed in one atomic operation.""" + try: + # Send model availability request + header = {"model_request": True, "model_name": self._model_name} + header_bytes = json.dumps(header).encode("utf-8") + + self._socket.send_multipart([header_bytes]) + + # Temporarily increase timeout for model operations + original_timeout = self._socket.getsockopt(zmq.RCVTIMEO) + self._socket.setsockopt(zmq.RCVTIMEO, 30000) + + try: + response_frames = self._socket.recv_multipart() + finally: + self._socket.setsockopt(zmq.RCVTIMEO, original_timeout) + + if len(response_frames) == 1: + try: + response = json.loads(response_frames[0].decode("utf-8")) + model_available = response.get("model_available", False) + model_loaded = response.get("model_loaded", False) + + if model_available and model_loaded: + return True + elif model_available and not model_loaded: + logger.error("Model exists but failed to load") + return False + else: + return self._send_model_data() + + except json.JSONDecodeError: + logger.warning( + "Received non-JSON response for model availability check" + ) + return False + else: + logger.warning( + "Received unexpected response format for model availability check" + ) + return False + + except Exception as e: + logger.error(f"Failed to check and transfer model: {e}") + return False + + def _check_model_availability(self) -> bool: + """Check if the model is available on the detector.""" + try: + # Send model availability request + header = {"model_request": True, "model_name": self._model_name} + header_bytes = json.dumps(header).encode("utf-8") + + self._socket.send_multipart([header_bytes]) + + # Receive response + response_frames = self._socket.recv_multipart() + + # Check if this is a JSON response (model management) + if len(response_frames) == 1: + try: + response = json.loads(response_frames[0].decode("utf-8")) + model_available = response.get("model_available", False) + model_loaded = response.get("model_loaded", False) + logger.debug( + f"Model availability check: available={model_available}, loaded={model_loaded}" + ) + return model_available and model_loaded + except json.JSONDecodeError: + logger.warning( + "Received non-JSON response for model availability check" + ) + return False + else: + logger.warning( + "Received unexpected response format for model availability check" + ) + return False + + except Exception as e: + logger.error(f"Failed to check model availability: {e}") + return False + + def _send_model_data(self) -> bool: + """Send model data to the detector.""" + try: + model_path = self.detector_config.model.path + + if not os.path.exists(model_path): + logger.error(f"Model file not found: {model_path}") + return False + + logger.info(f"Transferring model to detector: {self._model_name}") + with open(model_path, "rb") as f: + model_data = f.read() + + header = {"model_data": True, "model_name": self._model_name} + header_bytes = json.dumps(header).encode("utf-8") + + self._socket.send_multipart([header_bytes, model_data]) + + # Temporarily increase timeout for model loading (can take several seconds) + original_timeout = self._socket.getsockopt(zmq.RCVTIMEO) + self._socket.setsockopt(zmq.RCVTIMEO, 30000) + + try: + # Receive response + response_frames = self._socket.recv_multipart() + finally: + # Restore original timeout + self._socket.setsockopt(zmq.RCVTIMEO, original_timeout) + + # Check if this is a JSON response (model management) + if len(response_frames) == 1: + try: + response = json.loads(response_frames[0].decode("utf-8")) + model_saved = response.get("model_saved", False) + model_loaded = response.get("model_loaded", False) + if model_saved and model_loaded: + logger.info( + f"Model {self._model_name} transferred and loaded successfully" + ) + else: + logger.error( + f"Model transfer failed: saved={model_saved}, loaded={model_loaded}" + ) + return model_saved and model_loaded + except json.JSONDecodeError: + logger.warning("Received non-JSON response for model data transfer") + return False + else: + logger.warning( + "Received unexpected response format for model data transfer" + ) + return False + + except Exception as e: + logger.error(f"Failed to send model data: {e}") + return False + + def _build_header(self, tensor_input: np.ndarray) -> bytes: + header: dict[str, Any] = { + "shape": list(tensor_input.shape), + "dtype": str(tensor_input.dtype.name), + "model_type": str(self.detector_config.model.model_type.name), + } + return json.dumps(header).encode("utf-8") + + def _decode_response(self, frames: list[bytes]) -> np.ndarray: + try: + if len(frames) == 1: + # Single-frame raw float32 (20x6) + buf = frames[0] + if len(buf) != 20 * 6 * 4: + logger.warning( + f"ZMQ detector received unexpected payload size: {len(buf)}" + ) + return self._zero_result + return np.frombuffer(buf, dtype=np.float32).reshape((20, 6)) + + if len(frames) >= 2: + header = json.loads(frames[0].decode("utf-8")) + shape = tuple(header.get("shape", [])) + dtype = np.dtype(header.get("dtype", "float32")) + return np.frombuffer(frames[1], dtype=dtype).reshape(shape) + + logger.warning("ZMQ detector received empty reply") + return self._zero_result + except Exception as exc: # noqa: BLE001 + logger.error(f"ZMQ detector failed to decode response: {exc}") + return self._zero_result + + def detect_raw(self, tensor_input: np.ndarray) -> np.ndarray: + if not self._model_ready: + logger.warning("Model not ready, returning zero detections") + return self._zero_result + + try: + header_bytes = self._build_header(tensor_input) + payload_bytes = memoryview(tensor_input.tobytes(order="C")) + + # Send request + self._socket.send_multipart([header_bytes, payload_bytes]) + + # Receive reply + reply_frames = self._socket.recv_multipart() + detections = self._decode_response(reply_frames) + + # Ensure output shape and dtype are exactly as expected + return detections + except zmq.Again: + # Timeout + logger.debug("ZMQ detector request timed out; resetting socket") + try: + self._create_socket() + self._initialize_model() + except Exception: + pass + return self._zero_result + except zmq.ZMQError as exc: + logger.error(f"ZMQ detector ZMQError: {exc}; resetting socket") + try: + self._create_socket() + self._initialize_model() + except Exception: + pass + return self._zero_result + except Exception as exc: # noqa: BLE001 + logger.error(f"ZMQ detector unexpected error: {exc}") + return self._zero_result + + def __del__(self) -> None: # pragma: no cover - best-effort cleanup + try: + if self._socket is not None: + self._socket.close(linger=self.detector_config.linger_ms) + except Exception: + pass diff --git a/frigate/embeddings/__init__.py b/frigate/embeddings/__init__.py new file mode 100644 index 0000000..bf74df6 --- /dev/null +++ b/frigate/embeddings/__init__.py @@ -0,0 +1,330 @@ +"""SQLite-vec embeddings database.""" + +import base64 +import json +import logging +import os +import sys +import threading +from json.decoder import JSONDecodeError +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import regex +from pathvalidate import ValidationError, sanitize_filename + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum, EmbeddingsRequestor +from frigate.config import FrigateConfig +from frigate.const import CONFIG_DIR, FACE_DIR, PROCESS_PRIORITY_HIGH +from frigate.data_processing.types import DataProcessorMetrics +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.models import Event +from frigate.util.builtin import serialize +from frigate.util.classification import kickoff_model_training +from frigate.util.process import FrigateProcess + +from .maintainer import EmbeddingMaintainer +from .util import ZScoreNormalization + +logger = logging.getLogger(__name__) + + +class EmbeddingProcess(FrigateProcess): + def __init__( + self, + config: FrigateConfig, + metrics: DataProcessorMetrics | None, + stop_event: MpEvent, + ) -> None: + super().__init__( + stop_event, + PROCESS_PRIORITY_HIGH, + name="frigate.embeddings_manager", + daemon=True, + ) + self.config = config + self.metrics = metrics + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + maintainer = EmbeddingMaintainer( + self.config, + self.metrics, + self.stop_event, + ) + maintainer.start() + maintainer.join() + + # If the maintainer thread exited but no shutdown was requested, it + # crashed. Surface as a non-zero exit so the watchdog restarts us + # instead of treating the silent thread death as a clean shutdown. + if not self.stop_event.is_set(): + logger.error("Embeddings maintainer thread exited unexpectedly") + sys.exit(1) + + +class EmbeddingsContext: + def __init__(self, db: SqliteVecQueueDatabase): + self.db = db + self.thumb_stats = ZScoreNormalization() + self.desc_stats = ZScoreNormalization() + self.requestor = EmbeddingsRequestor() + + # load stats from disk + stats_file = os.path.join(CONFIG_DIR, ".search_stats.json") + try: + with open(stats_file) as f: + data = json.loads(f.read()) + self.thumb_stats.from_dict(data["thumb_stats"]) + self.desc_stats.from_dict(data["desc_stats"]) + except FileNotFoundError: + pass + except JSONDecodeError: + logger.warning("Failed to decode semantic search stats, clearing file") + try: + with open(stats_file, "w") as f: + f.write("") + except OSError as e: + logger.error(f"Failed to clear corrupted stats file: {e}") + + def stop(self): + """Write the stats to disk as JSON on exit.""" + contents = { + "thumb_stats": self.thumb_stats.to_dict(), + "desc_stats": self.desc_stats.to_dict(), + } + with open(os.path.join(CONFIG_DIR, ".search_stats.json"), "w") as f: + json.dump(contents, f) + self.requestor.stop() + + def search_thumbnail( + self, query: Event | str, event_ids: list[str] = None + ) -> list[tuple[str, float]]: + if query.__class__ == Event: + cursor = self.db.execute_sql( + """ + SELECT thumbnail_embedding FROM vec_thumbnails WHERE id = ? + """, + [query.id], + ) + + row = cursor.fetchone() if cursor else None + + if row: + query_embedding = row[0] + else: + # If no embedding found, generate it and return it + data = self.requestor.send_data( + EmbeddingsRequestEnum.embed_thumbnail.value, + {"id": str(query.id), "thumbnail": str(query.thumbnail)}, + ) + + if not data: + return [] + + query_embedding = serialize(data) + else: + data = self.requestor.send_data( + EmbeddingsRequestEnum.generate_search.value, query + ) + + if not data: + return [] + + query_embedding = serialize(data) + + sql_query = """ + SELECT + id, + distance + FROM vec_thumbnails + WHERE thumbnail_embedding MATCH ? + AND k = 100 + """ + + # Add the IN clause if event_ids is provided and not empty + # this is the only filter supported by sqlite-vec as of 0.1.3 + # but it seems to be broken in this version + if event_ids: + sql_query += " AND id IN ({})".format(",".join("?" * len(event_ids))) + + # order by distance DESC is not implemented in this version of sqlite-vec + # when it's implemented, we can use cosine similarity + sql_query += " ORDER BY distance" + + parameters = [query_embedding] + event_ids if event_ids else [query_embedding] + + results = self.db.execute_sql(sql_query, parameters).fetchall() + + return results + + def search_description( + self, query_text: str, event_ids: list[str] = None + ) -> list[tuple[str, float]]: + data = self.requestor.send_data( + EmbeddingsRequestEnum.generate_search.value, query_text + ) + + if not data: + return [] + + query_embedding = serialize(data) + + # Prepare the base SQL query + sql_query = """ + SELECT + id, + distance + FROM vec_descriptions + WHERE description_embedding MATCH ? + AND k = 100 + """ + + # Add the IN clause if event_ids is provided and not empty + # this is the only filter supported by sqlite-vec as of 0.1.3 + # but it seems to be broken in this version + if event_ids: + sql_query += " AND id IN ({})".format(",".join("?" * len(event_ids))) + + # order by distance DESC is not implemented in this version of sqlite-vec + # when it's implemented, we can use cosine similarity + sql_query += " ORDER BY distance" + + parameters = [query_embedding] + event_ids if event_ids else [query_embedding] + + results = self.db.execute_sql(sql_query, parameters).fetchall() + + return results + + def register_face(self, face_name: str, image_data: bytes) -> dict[str, Any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.register_face.value, + { + "face_name": face_name, + "image": base64.b64encode(image_data).decode("ASCII"), + }, + ) + + def recognize_face(self, image_data: bytes) -> dict[str, Any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.recognize_face.value, + { + "image": base64.b64encode(image_data).decode("ASCII"), + }, + ) + + def get_face_ids(self, name: str) -> list[str]: + sql_query = """ + SELECT + id + FROM vec_descriptions + WHERE id LIKE ? + """ + + return self.db.execute_sql(sql_query, (f"%{name}%",)).fetchall() + + def reprocess_face(self, face_file: str) -> dict[str, Any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.reprocess_face.value, {"image_file": face_file} + ) + + def clear_face_classifier(self) -> None: + self.requestor.send_data( + EmbeddingsRequestEnum.clear_face_classifier.value, None + ) + + def delete_face_ids(self, face: str, ids: list[str]) -> None: + folder = os.path.join(FACE_DIR, face) + for id in ids: + file_path = os.path.join(folder, id) + + if os.path.isfile(file_path): + os.unlink(file_path) + + if face != "train" and len(os.listdir(folder)) == 0: + os.rmdir(folder) + + self.requestor.send_data( + EmbeddingsRequestEnum.clear_face_classifier.value, None + ) + + def rename_face(self, old_name: str, new_name: str) -> None: + valid_name_pattern = r"^[\p{L}\p{N}\s'_-]{1,50}$" + + try: + sanitized_old_name = sanitize_filename(old_name, replacement_text="_") + sanitized_new_name = sanitize_filename(new_name, replacement_text="_") + except ValidationError as e: + raise ValueError(f"Invalid face name: {str(e)}") from e + + if not regex.match(valid_name_pattern, old_name): + raise ValueError(f"Invalid old face name: {old_name}") + if not regex.match(valid_name_pattern, new_name): + raise ValueError(f"Invalid new face name: {new_name}") + if sanitized_old_name != old_name: + raise ValueError(f"Old face name contains invalid characters: {old_name}") + if sanitized_new_name != new_name: + raise ValueError(f"New face name contains invalid characters: {new_name}") + + old_path = os.path.normpath(os.path.join(FACE_DIR, old_name)) + new_path = os.path.normpath(os.path.join(FACE_DIR, new_name)) + + # Prevent path traversal + if not old_path.startswith( + os.path.normpath(FACE_DIR) + ) or not new_path.startswith(os.path.normpath(FACE_DIR)): + raise ValueError("Invalid path detected") + + if not os.path.exists(old_path): + raise ValueError(f"Face {old_name} not found.") + + os.rename(old_path, new_path) + + self.requestor.send_data( + EmbeddingsRequestEnum.clear_face_classifier.value, None + ) + + def update_description(self, event_id: str, description: str) -> None: + self.requestor.send_data( + EmbeddingsRequestEnum.embed_description.value, + {"id": event_id, "description": description}, + ) + + def reprocess_plate(self, event: dict[str, Any]) -> dict[str, Any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.reprocess_plate.value, {"event": event} + ) + + def reindex_embeddings(self) -> dict[str, Any]: + return self.requestor.send_data(EmbeddingsRequestEnum.reindex.value, {}) + + def start_classification_training(self, model_name: str) -> dict[str, Any]: + threading.Thread( + target=kickoff_model_training, + args=(self.requestor, model_name), + daemon=True, + ).start() + return {"success": True, "message": f"Began training {model_name} model."} + + def transcribe_audio(self, event: dict[str, any]) -> dict[str, any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.transcribe_audio.value, {"event": event} + ) + + def generate_description_embedding(self, text: str) -> None: + return self.requestor.send_data( + EmbeddingsRequestEnum.embed_description.value, + {"id": None, "description": text, "upsert": False}, + ) + + def generate_image_embedding(self, event_id: str, thumbnail: bytes) -> None: + return self.requestor.send_data( + EmbeddingsRequestEnum.embed_thumbnail.value, + {"id": str(event_id), "thumbnail": str(thumbnail), "upsert": False}, + ) + + def generate_review_summary(self, start_ts: float, end_ts: float) -> str | None: + return self.requestor.send_data( + EmbeddingsRequestEnum.summarize_review.value, + {"start_ts": start_ts, "end_ts": end_ts}, + ) diff --git a/frigate/embeddings/embeddings.py b/frigate/embeddings/embeddings.py new file mode 100644 index 0000000..91144c3 --- /dev/null +++ b/frigate/embeddings/embeddings.py @@ -0,0 +1,670 @@ +"""SQLite-vec embeddings database.""" + +import datetime +import io +import logging +import os +import threading +import time + +import numpy as np +from peewee import DoesNotExist, IntegrityError +from PIL import Image +from playhouse.shortcuts import model_to_dict + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.config.classification import SemanticSearchModelEnum +from frigate.const import ( + CONFIG_DIR, + TRIGGER_DIR, + UPDATE_EMBEDDINGS_REINDEX_PROGRESS, + UPDATE_MODEL_STATE, +) +from frigate.data_processing.types import DataProcessorMetrics +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.models import Event, Trigger +from frigate.types import ModelStatusTypesEnum +from frigate.util.builtin import EventsPerSecond, InferenceSpeed, serialize +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_v2_embedding import JinaV2Embedding + +logger = logging.getLogger(__name__) + + +def get_metadata(event: Event) -> dict: + """Extract valid event metadata.""" + event_dict = model_to_dict(event) + return ( + { + k: v + for k, v in event_dict.items() + if k not in ["thumbnail"] + and v is not None + and isinstance(v, (str, int, float, bool)) + } + | { + k: v + for k, v in event_dict["data"].items() + if k not in ["description"] + and v is not None + and isinstance(v, (str, int, float, bool)) + } + | { + # Metadata search doesn't support $contains + # and an event can have multiple zones, so + # we need to create a key for each zone + f"{k}_{x}": True + for k, v in event_dict.items() + if isinstance(v, list) and len(v) > 0 + for x in v + if isinstance(x, str) + } + ) + + +class Embeddings: + """SQLite-vec embeddings database.""" + + def __init__( + self, + config: FrigateConfig, + db: SqliteVecQueueDatabase, + metrics: DataProcessorMetrics, + genai_manager=None, + ) -> None: + self.config = config + self.db = db + self.metrics = metrics + self.requestor = InterProcessRequestor() + + self.image_inference_speed = InferenceSpeed(self.metrics.image_embeddings_speed) + self.image_eps = EventsPerSecond() + self.image_eps.start() + self.text_inference_speed = InferenceSpeed(self.metrics.text_embeddings_speed) + self.text_eps = EventsPerSecond() + self.text_eps.start() + + self.reindex_lock = threading.Lock() + self.reindex_thread = None + self.reindex_running = False + + # Create tables if they don't exist + self.db.create_embeddings_tables() + + models = self.get_model_definitions() + + for model in models: + self.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": model, + "state": ModelStatusTypesEnum.not_downloaded, + }, + ) + + 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 + self.embedding = JinaV2Embedding( + model_size=self.config.semantic_search.model_size, + requestor=self.requestor, + device=config.semantic_search.device + or ("GPU" if config.semantic_search.model_size == "large" else "CPU"), + ) + 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" + ) + else: + # Default to jinav1 + self.text_embedding = JinaV1TextEmbedding( + model_size=config.semantic_search.model_size, + requestor=self.requestor, + device="CPU", + ) + self.vision_embedding = JinaV1ImageEmbedding( + model_size=config.semantic_search.model_size, + requestor=self.requestor, + device=config.semantic_search.device + or ("GPU" if config.semantic_search.model_size == "large" else "CPU"), + ) + + def update_stats(self) -> None: + self.metrics.image_embeddings_eps.value = self.image_eps.eps() + self.metrics.text_embeddings_eps.value = self.text_eps.eps() + + def get_model_definitions(self): + model_cfg = self.config.semantic_search.model + if not isinstance(model_cfg, SemanticSearchModelEnum): + # GenAI provider: no ONNX models to download + models = [] + elif model_cfg == SemanticSearchModelEnum.jinav2: + models = [ + "jinaai/jina-clip-v2-tokenizer", + "jinaai/jina-clip-v2-model_fp16.onnx" + if self.config.semantic_search.model_size == "large" + else "jinaai/jina-clip-v2-model_quantized.onnx", + "jinaai/jina-clip-v2-preprocessor_config.json", + ] + else: # Default to jinav1 + models = [ + "jinaai/jina-clip-v1-text_model_fp16.onnx", + "jinaai/jina-clip-v1-tokenizer", + "jinaai/jina-clip-v1-vision_model_fp16.onnx" + if self.config.semantic_search.model_size == "large" + else "jinaai/jina-clip-v1-vision_model_quantized.onnx", + "jinaai/jina-clip-v1-preprocessor_config.json", + ] + + # Add common models + models.extend( + [ + "facenet-facenet.onnx", + "paddleocr-onnx-detection.onnx", + "paddleocr-onnx-classification.onnx", + "paddleocr-onnx-recognition.onnx", + ] + ) + + return models + + def embed_thumbnail( + self, event_id: str, thumbnail: bytes, upsert: bool = True + ) -> np.ndarray: + """Embed thumbnail and optionally insert into DB. + + @param: event_id in Events DB + @param: thumbnail bytes in jpg format + @param: upsert If embedding should be upserted into vec DB + """ + start = datetime.datetime.now().timestamp() + # Convert thumbnail bytes to PIL Image + embedding = self.vision_embedding([thumbnail])[0] + + if upsert: + self.db.execute_sql( + """ + INSERT OR REPLACE INTO vec_thumbnails(id, thumbnail_embedding) + VALUES(?, ?) + """, + (event_id, serialize(embedding)), + ) + + self.image_inference_speed.update(datetime.datetime.now().timestamp() - start) + self.image_eps.update() + + return embedding + + def batch_embed_thumbnail( + self, event_thumbs: dict[str, bytes], upsert: bool = True + ) -> list[np.ndarray]: + """Embed thumbnails and optionally insert into DB. + + @param: event_thumbs Map of Event IDs in DB to thumbnail bytes in jpg format + @param: upsert If embedding should be upserted into vec DB + """ + start = datetime.datetime.now().timestamp() + valid_ids = [] + valid_thumbs = [] + for eid, thumb in event_thumbs.items(): + try: + img = Image.open(io.BytesIO(thumb)) + img.verify() # Will raise if corrupt + valid_ids.append(eid) + valid_thumbs.append(thumb) + except Exception as e: + logger.warning( + f"Embeddings reindexing: Skipping corrupt thumbnail for event {eid}: {e}" + ) + + if not valid_thumbs: + logger.warning( + "Embeddings reindexing: No valid thumbnails to embed in this batch." + ) + return [] + + embeddings = self.vision_embedding(valid_thumbs) + + if upsert: + items = [] + for i in range(len(valid_ids)): + items.append(valid_ids[i]) + items.append(serialize(embeddings[i])) + self.image_eps.update() + + self.db.execute_sql( + """ + INSERT OR REPLACE INTO vec_thumbnails(id, thumbnail_embedding) + VALUES {} + """.format(", ".join(["(?, ?)"] * len(valid_ids))), + items, + ) + + duration = datetime.datetime.now().timestamp() - start + self.image_inference_speed.update(duration / len(valid_ids)) + + return embeddings + + def embed_description( + self, event_id: str, description: str, upsert: bool = True + ) -> np.ndarray: + start = datetime.datetime.now().timestamp() + embedding = self.text_embedding([description])[0] + + if upsert: + self.db.execute_sql( + """ + INSERT OR REPLACE INTO vec_descriptions(id, description_embedding) + VALUES(?, ?) + """, + (event_id, serialize(embedding)), + ) + + self.text_inference_speed.update(datetime.datetime.now().timestamp() - start) + self.text_eps.update() + + return embedding + + def batch_embed_description( + self, event_descriptions: dict[str, str], upsert: bool = True + ) -> np.ndarray: + start = datetime.datetime.now().timestamp() + # upsert embeddings one by one to avoid token limit + embeddings = [] + + for desc in event_descriptions.values(): + embeddings.append(self.text_embedding([desc])[0]) + + if upsert: + ids = list(event_descriptions.keys()) + items = [] + + for i in range(len(ids)): + items.append(ids[i]) + items.append(serialize(embeddings[i])) + self.text_eps.update() + + self.db.execute_sql( + """ + INSERT OR REPLACE INTO vec_descriptions(id, description_embedding) + VALUES {} + """.format(", ".join(["(?, ?)"] * len(ids))), + items, + ) + + self.text_inference_speed.update(datetime.datetime.now().timestamp() - start) + + return embeddings + + def reindex(self) -> None: + logger.info("Indexing tracked object embeddings...") + + self.db.drop_embeddings_tables() + logger.debug("Dropped embeddings tables.") + self.db.create_embeddings_tables() + logger.debug("Created embeddings tables.") + + # Delete the saved stats file + if os.path.exists(os.path.join(CONFIG_DIR, ".search_stats.json")): + os.remove(os.path.join(CONFIG_DIR, ".search_stats.json")) + + st = time.time() + + # Get total count of events to process + total_events = Event.select().count() + + if not isinstance(self.config.semantic_search.model, SemanticSearchModelEnum): + batch_size = 1 + elif self.config.semantic_search.model == SemanticSearchModelEnum.jinav2: + batch_size = 4 + else: + batch_size = 32 + current_page = 1 + + totals = { + "thumbnails": 0, + "descriptions": 0, + "processed_objects": total_events - 1 if total_events < batch_size else 0, + "total_objects": total_events, + "time_remaining": 0 if total_events < batch_size else -1, + "status": "indexing", + } + + self.requestor.send_data(UPDATE_EMBEDDINGS_REINDEX_PROGRESS, totals) + + events = ( + Event.select() + .order_by(Event.start_time.desc()) + .paginate(current_page, batch_size) + ) + + while events: + event: Event + batch_thumbs = {} + batch_descs = {} + for event in events: + totals["processed_objects"] += 1 + + if description := event.data.get("description", "").strip(): + batch_descs[event.id] = description + totals["descriptions"] += 1 + + if thumbnail := get_event_thumbnail_bytes(event): + batch_thumbs[event.id] = thumbnail + totals["thumbnails"] += 1 + + # run batch embedding + if batch_thumbs: + self.batch_embed_thumbnail(batch_thumbs) + + if batch_descs: + self.batch_embed_description(batch_descs) + + # report progress every batch so we don't spam the logs + progress = (totals["processed_objects"] / total_events) * 100 + logger.debug( + "Processed %d/%d events (%.2f%% complete) | Thumbnails: %d, Descriptions: %d", + totals["processed_objects"], + total_events, + progress, + totals["thumbnails"], + totals["descriptions"], + ) + + # Calculate time remaining + elapsed_time = time.time() - st + avg_time_per_event = elapsed_time / totals["processed_objects"] + remaining_events = total_events - totals["processed_objects"] + time_remaining = avg_time_per_event * remaining_events + totals["time_remaining"] = int(time_remaining) + + self.requestor.send_data(UPDATE_EMBEDDINGS_REINDEX_PROGRESS, totals) + + # Move to the next page + current_page += 1 + events = ( + Event.select() + .order_by(Event.start_time.desc()) + .paginate(current_page, batch_size) + ) + + logger.info( + "Embedded %d thumbnails and %d descriptions in %s seconds", + totals["thumbnails"], + totals["descriptions"], + round(time.time() - st, 1), + ) + totals["status"] = "completed" + + self.requestor.send_data(UPDATE_EMBEDDINGS_REINDEX_PROGRESS, totals) + + def start_reindex(self) -> bool: + """Start reindexing in a separate thread if not already running.""" + with self.reindex_lock: + if self.reindex_running: + logger.warning("Reindex embeddings is already running.") + return False + + # Mark as running and start the thread + self.reindex_running = True + self.reindex_thread = threading.Thread( + target=self._reindex_wrapper, daemon=True + ) + self.reindex_thread.start() + return True + + def _reindex_wrapper(self) -> None: + """Wrapper to run reindex and reset running flag when done.""" + try: + self.reindex() + finally: + with self.reindex_lock: + self.reindex_running = False + self.reindex_thread = None + + def sync_triggers(self) -> None: + for camera in self.config.cameras.values(): + # Get all existing triggers for this camera + existing_triggers = { + trigger.name: trigger + for trigger in Trigger.select().where(Trigger.camera == camera.name) + } + + # Get all configured trigger names + configured_trigger_names = set(camera.semantic_search.triggers or {}) + + # Create or update triggers from config + for trigger_name, trigger in ( + camera.semantic_search.triggers or {} + ).items(): + if trigger_name in existing_triggers: + existing_trigger = existing_triggers[trigger_name] + needs_embedding_update = False + thumbnail_missing = False + + # Check if data has changed or thumbnail is missing for thumbnail type + if trigger.type == "thumbnail": + thumbnail_path = os.path.join( + TRIGGER_DIR, camera.name, f"{trigger.data}.webp" + ) + try: + event = Event.get(Event.id == trigger.data) + if event.data.get("type") != "object": + logger.warning( + f"Event {trigger.data} is not a tracked object for {trigger.type} trigger" + ) + continue # Skip if not an object + + # Check if thumbnail needs to be updated (data changed or missing) + if ( + existing_trigger.data != trigger.data + or not os.path.exists(thumbnail_path) + ): + thumbnail = get_event_thumbnail_bytes(event) + if not thumbnail: + logger.warning( + f"Unable to retrieve thumbnail for event ID {trigger.data} for {trigger_name}." + ) + continue + self.write_trigger_thumbnail( + camera.name, trigger.data, thumbnail + ) + thumbnail_missing = True + except DoesNotExist: + logger.debug( + f"Event ID {trigger.data} for trigger {trigger_name} does not exist." + ) + continue + + # Update existing trigger if data has changed + if ( + existing_trigger.type != trigger.type + or existing_trigger.data != trigger.data + or existing_trigger.threshold != trigger.threshold + ): + existing_trigger.type = trigger.type + existing_trigger.data = trigger.data + existing_trigger.threshold = trigger.threshold + needs_embedding_update = True + + # Check if embedding is missing or needs update + if ( + not existing_trigger.embedding + or needs_embedding_update + or thumbnail_missing + ): + existing_trigger.embedding = self._calculate_trigger_embedding( + trigger, trigger_name, camera.name + ) + needs_embedding_update = True + + if needs_embedding_update: + existing_trigger.save() + continue + else: + # Create new trigger + try: + # For thumbnail triggers, validate the event exists + if trigger.type == "thumbnail": + try: + event: Event = Event.get(Event.id == trigger.data) + except DoesNotExist: + logger.warning( + f"Event ID {trigger.data} for trigger {trigger_name} does not exist." + ) + continue + + # Skip the event if not an object + if event.data.get("type") != "object": + logger.warning( + f"Event ID {trigger.data} for trigger {trigger_name} is not a tracked object." + ) + continue + + thumbnail = get_event_thumbnail_bytes(event) + + if not thumbnail: + logger.warning( + f"Unable to retrieve thumbnail for event ID {trigger.data} for {trigger_name}." + ) + continue + + self.write_trigger_thumbnail( + camera.name, trigger.data, thumbnail + ) + + # Calculate embedding for new trigger + embedding = self._calculate_trigger_embedding( + trigger, trigger_name, camera.name + ) + + Trigger.create( + camera=camera.name, + name=trigger_name, + type=trigger.type, + data=trigger.data, + threshold=trigger.threshold, + model=self.config.semantic_search.model, + embedding=embedding, + triggering_event_id="", + last_triggered=None, + ) + + except IntegrityError: + pass # Handle duplicate creation attempts + + # Remove triggers that are no longer in config + triggers_to_remove = ( + set(existing_triggers.keys()) - configured_trigger_names + ) + if triggers_to_remove: + Trigger.delete().where( + Trigger.camera == camera.name, Trigger.name.in_(triggers_to_remove) + ).execute() + for trigger_name in triggers_to_remove: + # Only remove thumbnail files for thumbnail triggers + if existing_triggers[trigger_name].type == "thumbnail": + self.remove_trigger_thumbnail( + camera.name, existing_triggers[trigger_name].data + ) + + def write_trigger_thumbnail( + self, camera: str, event_id: str, thumbnail: bytes + ) -> None: + """Write the thumbnail to the trigger directory.""" + try: + os.makedirs(os.path.join(TRIGGER_DIR, camera), exist_ok=True) + with open(os.path.join(TRIGGER_DIR, camera, f"{event_id}.webp"), "wb") as f: + f.write(thumbnail) + logger.debug( + f"Writing thumbnail for trigger with data {event_id} in {camera}." + ) + except Exception as e: + logger.error( + f"Failed to write thumbnail for trigger with data {event_id} in {camera}: {e}" + ) + + def remove_trigger_thumbnail(self, camera: str, event_id: str) -> None: + """Write the thumbnail to the trigger directory.""" + try: + os.remove(os.path.join(TRIGGER_DIR, camera, f"{event_id}.webp")) + logger.debug( + f"Deleted thumbnail for trigger with data {event_id} in {camera}." + ) + except Exception as e: + logger.error( + f"Failed to delete thumbnail for trigger with data {event_id} in {camera}: {e}" + ) + + def _calculate_trigger_embedding( + self, trigger, trigger_name: str, camera_name: str + ) -> bytes: + """Calculate embedding for a trigger based on its type and data.""" + if trigger.type == "description": + logger.debug(f"Generating embedding for trigger description {trigger_name}") + embedding = self.embed_description(None, trigger.data, upsert=False) + return embedding.astype(np.float32).tobytes() + + elif trigger.type == "thumbnail": + # For image triggers, trigger.data should be an image ID + # Try to get embedding from vec_thumbnails table first + cursor = self.db.execute_sql( + "SELECT thumbnail_embedding FROM vec_thumbnails WHERE id = ?", + [trigger.data], + ) + row = cursor.fetchone() if cursor else None + if row: + return row[0] # Already in bytes format + else: + logger.debug( + f"No thumbnail embedding found for image ID: {trigger.data}, generating from saved trigger thumbnail" + ) + + try: + with open( + os.path.join(TRIGGER_DIR, camera_name, f"{trigger.data}.webp"), + "rb", + ) as f: + thumbnail = f.read() + except Exception as e: + logger.error( + f"Failed to read thumbnail for trigger {trigger_name} with ID {trigger.data}: {e}" + ) + return b"" + + logger.debug( + f"Generating embedding for trigger thumbnail {trigger_name} with ID {trigger.data}" + ) + embedding = self.embed_thumbnail( + str(trigger.data), thumbnail, upsert=False + ) + return embedding.astype(np.float32).tobytes() + + else: + logger.warning(f"Unknown trigger type: {trigger.type}") + return b"" diff --git a/frigate/embeddings/genai_embedding.py b/frigate/embeddings/genai_embedding.py new file mode 100644 index 0000000..d3637bb --- /dev/null +++ b/frigate/embeddings/genai_embedding.py @@ -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 diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py new file mode 100644 index 0000000..042c272 --- /dev/null +++ b/frigate/embeddings/maintainer.py @@ -0,0 +1,824 @@ +"""Maintain embeddings in SQLite-vec.""" + +import base64 +import datetime +import json +import logging +import threading +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +from peewee import DoesNotExist + +from frigate.comms.config_updater import ConfigSubscriber +from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum +from frigate.comms.embeddings_updater import ( + EmbeddingsRequestEnum, + EmbeddingsResponder, +) +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataSubscriber, + EventMetadataTypeEnum, +) +from frigate.comms.events_updater import EventEndSubscriber, EventUpdateSubscriber +from frigate.comms.inter_process import InterProcessRequestor +from frigate.comms.recordings_updater import ( + RecordingsDataSubscriber, + RecordingsDataTypeEnum, +) +from frigate.comms.review_updater import ReviewDataSubscriber +from frigate.config import FrigateConfig +from frigate.config.camera.camera import CameraTypeEnum +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.config.classification import ObjectClassificationType +from frigate.data_processing.common.license_plate.model import ( + LicensePlateModelRunner, +) +from frigate.data_processing.post.api import PostProcessorApi +from frigate.data_processing.post.audio_transcription import ( + AudioTranscriptionPostProcessor, +) +from frigate.data_processing.post.license_plate import ( + LicensePlatePostProcessor, +) +from frigate.data_processing.post.object_descriptions import ObjectDescriptionProcessor +from frigate.data_processing.post.review_descriptions import ReviewDescriptionProcessor +from frigate.data_processing.post.semantic_trigger import SemanticTriggerProcessor +from frigate.data_processing.real_time.api import RealTimeProcessorApi +from frigate.data_processing.real_time.bird import BirdRealTimeProcessor +from frigate.data_processing.real_time.custom_classification import ( + CustomObjectClassificationProcessor, + CustomStateClassificationProcessor, +) +from frigate.data_processing.real_time.face import FaceRealTimeProcessor +from frigate.data_processing.real_time.license_plate import ( + LicensePlateRealTimeProcessor, +) +from frigate.data_processing.types import DataProcessorMetrics, PostProcessDataEnum +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.events.types import ( + EventStateEnum, + EventTypeEnum, + RegenerateDescriptionEnum, +) +from frigate.genai import GenAIClientManager +from frigate.models import Event, Recordings, ReviewSegment, Trigger +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.builtin import serialize +from frigate.util.file import get_event_thumbnail_bytes +from frigate.util.image import SharedMemoryFrameManager + +from .embeddings import Embeddings + +logger = logging.getLogger(__name__) + +MAX_THUMBNAILS = 10 + + +class EmbeddingMaintainer(threading.Thread): + """Handle embedding queue and post event updates.""" + + def __init__( + self, + config: FrigateConfig, + metrics: DataProcessorMetrics | None, + stop_event: MpEvent, + ) -> None: + super().__init__(name="embeddings_maintainer") + self.config = config + self.metrics = metrics + self.embeddings = None + self.config_updater = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.detect, + CameraConfigUpdateEnum.face_recognition, + CameraConfigUpdateEnum.ffmpeg, + CameraConfigUpdateEnum.lpr, + CameraConfigUpdateEnum.motion, + CameraConfigUpdateEnum.objects, + CameraConfigUpdateEnum.object_genai, + CameraConfigUpdateEnum.review, + CameraConfigUpdateEnum.review_genai, + CameraConfigUpdateEnum.semantic_search, + CameraConfigUpdateEnum.zones, + ], + ) + self.enrichment_config_subscriber = ConfigSubscriber("config/") + + # Configure Frigate DB + db = SqliteVecQueueDatabase( + config.database.path, + pragmas={ + "auto_vacuum": "FULL", # Does not defragment database + "cache_size": -512 * 1000, # 512MB of cache + "synchronous": "NORMAL", # Safe when using WAL https://www.sqlite.org/pragma.html#pragma_synchronous + }, + timeout=max( + 60, 10 * len([c for c in config.cameras.values() if c.enabled]) + ), + load_vec_extension=True, + ) + models = [Event, Recordings, ReviewSegment, Trigger] + db.bind(models) + + self.genai_manager = GenAIClientManager(config) + + if config.semantic_search.enabled: + self.embeddings = Embeddings(config, db, metrics, self.genai_manager) + + # Check if we need to re-index events + if config.semantic_search.reindex: + self.embeddings.reindex() + + # Sync semantic search triggers in db with config + self.embeddings.sync_triggers() + + # create communication for updating event descriptions + self.requestor = InterProcessRequestor() + + self.event_subscriber = EventUpdateSubscriber() + self.event_end_subscriber = EventEndSubscriber() + self.event_metadata_publisher = EventMetadataPublisher() + self.event_metadata_subscriber = EventMetadataSubscriber( + EventMetadataTypeEnum.regenerate_description + ) + self.recordings_subscriber = RecordingsDataSubscriber( + RecordingsDataTypeEnum.saved + ) + self.review_subscriber = ReviewDataSubscriber("") + self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video.value) + self.embeddings_responder = EmbeddingsResponder() + self.frame_manager = SharedMemoryFrameManager() + + self.detected_license_plates: dict[str, dict[str, Any]] = {} + + # model runners to share between realtime and post processors + if self.config.lpr.enabled: + lpr_model_runner = LicensePlateModelRunner( + self.requestor, + device=self.config.lpr.device, + model_size=self.config.lpr.model_size, + ) + + # realtime processors + self.realtime_processors: list[RealTimeProcessorApi] = [] + + if self.config.face_recognition.enabled: + logger.debug("Face recognition enabled, initializing FaceRealTimeProcessor") + self.realtime_processors.append( + FaceRealTimeProcessor( + self.config, self.requestor, self.event_metadata_publisher, metrics + ) + ) + logger.debug("FaceRealTimeProcessor initialized successfully") + + if self.config.classification.bird.enabled: + self.realtime_processors.append( + BirdRealTimeProcessor( + self.config, self.event_metadata_publisher, metrics + ) + ) + + if self.config.lpr.enabled: + self.realtime_processors.append( + LicensePlateRealTimeProcessor( + self.config, + self.requestor, + self.event_metadata_publisher, + metrics, + lpr_model_runner, + self.detected_license_plates, + ) + ) + + for model_config in self.config.classification.custom.values(): + if not model_config.enabled: + continue + + self.realtime_processors.append( + CustomStateClassificationProcessor( + self.config, model_config, self.requestor, self.metrics + ) + if model_config.state_config != None + else CustomObjectClassificationProcessor( + self.config, + model_config, + self.event_metadata_publisher, + self.requestor, + self.metrics, + ) + ) + + # post processors + self.post_processors: list[PostProcessorApi] = [] + + if any(c.review.genai.enabled_in_config for c in self.config.cameras.values()): + self.post_processors.append( + ReviewDescriptionProcessor( + self.config, + self.requestor, + self.metrics, + self.genai_manager, + ) + ) + + if self.config.lpr.enabled: + self.post_processors.append( + LicensePlatePostProcessor( + self.config, + self.requestor, + self.event_metadata_publisher, + metrics, + lpr_model_runner, + self.detected_license_plates, + ) + ) + + if any( + c.enabled_in_config and c.audio_transcription.enabled + for c in self.config.cameras.values() + ): + self.post_processors.append( + AudioTranscriptionPostProcessor( + self.config, self.requestor, self.embeddings, metrics + ) + ) + + semantic_trigger_processor: SemanticTriggerProcessor | None = None + if self.config.semantic_search.enabled: + semantic_trigger_processor = SemanticTriggerProcessor( + db, + self.config, + self.requestor, + self.event_metadata_publisher, + metrics, + self.embeddings, + ) + self.post_processors.append(semantic_trigger_processor) + + if any(c.objects.genai.enabled_in_config for c in self.config.cameras.values()): + self.post_processors.append( + ObjectDescriptionProcessor( + self.config, + self.embeddings, + self.requestor, + self.metrics, + self.genai_manager, + semantic_trigger_processor, + ) + ) + + self.stop_event = stop_event + + # recordings data + self.recordings_available_through: dict[str, float] = {} + + def run(self) -> None: + """Maintain a SQLite-vec database for semantic search.""" + while not self.stop_event.is_set(): + self.config_updater.check_for_updates() + self._check_enrichment_config_updates() + self._process_requests() + self._process_updates() + self._process_recordings_updates() + self._process_review_updates() + self._process_frame_updates() + self._process_deferred_results() + self._expire_dedicated_lpr() + self._process_finalized() + self._process_event_metadata() + + # Shutdown deferred processors + for processor in self.realtime_processors: + processor.shutdown() + + self.config_updater.stop() + self.enrichment_config_subscriber.stop() + self.event_subscriber.stop() + self.event_end_subscriber.stop() + self.recordings_subscriber.stop() + self.detection_subscriber.stop() + self.event_metadata_publisher.stop() + self.event_metadata_subscriber.stop() + self.embeddings_responder.stop() + self.requestor.stop() + logger.info("Exiting embeddings maintenance...") + + def _check_enrichment_config_updates(self) -> None: + """Check for enrichment config updates and delegate to processors.""" + topic, payload = self.enrichment_config_subscriber.check_for_update() + + if topic is None: + return + + # Custom classification add/remove requires managing the processor list + if topic.startswith("config/classification/custom/"): + self._handle_custom_classification_update(topic, payload) + return + + if topic == "config/genai": + self.config.genai = payload + self.genai_manager.update_config(self.config) + + # Broadcast to all processors — each decides if the topic is relevant + for processor in self.realtime_processors: + processor.update_config(topic, payload) + + for processor in self.post_processors: + processor.update_config(topic, payload) + + def _remove_custom_classification_processor(self, model_name: str) -> None: + """Shut down and drop any running processor for a custom model.""" + remaining = [] + for processor in self.realtime_processors: + if ( + isinstance( + processor, + ( + CustomStateClassificationProcessor, + CustomObjectClassificationProcessor, + ), + ) + and processor.model_config.name == model_name + ): + processor.shutdown() + else: + remaining.append(processor) + self.realtime_processors = remaining + + def _handle_custom_classification_update( + self, topic: str, model_config: Any + ) -> None: + """Handle add/remove of custom classification processors.""" + model_name = topic.split("/")[-1] + + if model_config is None: + self._remove_custom_classification_processor(model_name) + logger.info( + f"Successfully removed classification processor for model: {model_name}" + ) + return + + self.config.classification.custom[model_name] = model_config + + # A disabled model must not run; tear down any existing processor and + # do not register a new one. + if not model_config.enabled: + self._remove_custom_classification_processor(model_name) + logger.info(f"Disabled classification processor for model: {model_name}") + return + + # 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: + """Process embeddings requests""" + + def _handle_request(topic: str, data: dict[str, Any]) -> str: + try: + # First handle the embedding-specific topics when semantic search is enabled + if self.config.semantic_search.enabled: + if topic == EmbeddingsRequestEnum.embed_description.value: + return serialize( + self.embeddings.embed_description( + data["id"], data["description"] + ), + pack=False, + ) + elif topic == EmbeddingsRequestEnum.embed_thumbnail.value: + thumbnail = base64.b64decode(data["thumbnail"]) + return serialize( + self.embeddings.embed_thumbnail(data["id"], thumbnail), + pack=False, + ) + elif topic == EmbeddingsRequestEnum.generate_search.value: + return serialize( + self.embeddings.embed_description("", data, upsert=False), + pack=False, + ) + elif topic == EmbeddingsRequestEnum.reindex.value: + response = self.embeddings.start_reindex() + return "started" if response else "in_progress" + + processors = [self.realtime_processors, self.post_processors] + for processor_list in processors: + for processor in processor_list: + resp = processor.handle_request(topic, data) + if resp is not None: + return resp + + logger.error(f"No processor handled the topic {topic}") + return None + except Exception as e: + logger.exception(f"Unable to handle embeddings request {e}") + + self.embeddings_responder.check_for_request(_handle_request) + + def _process_updates(self) -> None: + """Process event updates""" + update = self.event_subscriber.check_for_update() + + if update is None: + return + + source_type, event_type, camera, frame_name, data = update + + logger.debug( + f"Received update - source_type: {source_type}, camera: {camera}, data label: {data.get('label') if data else 'None'}" + ) + + if not camera or source_type != EventTypeEnum.tracked_object: + logger.debug( + f"Skipping update - camera: {camera}, source_type: {source_type}" + ) + return + + if self.config.semantic_search.enabled: + self.embeddings.update_stats() + + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return + + # no need to process updated objects if no processors are active + if len(self.realtime_processors) == 0 and len(self.post_processors) == 0: + logger.debug( + f"No processors active - realtime: {len(self.realtime_processors)}, post: {len(self.post_processors)}" + ) + return + + # Create our own thumbnail based on the bounding box and the frame time + try: + yuv_frame = self.frame_manager.get( + frame_name, camera_config.frame_shape_yuv + ) + except FileNotFoundError: + logger.debug(f"Frame {frame_name} not found for camera {camera}") + pass + + if yuv_frame is None: + logger.debug( + "Unable to process object update because frame is unavailable." + ) + return + + logger.debug( + f"Processing {len(self.realtime_processors)} realtime processors for object {data.get('id')} (label: {data.get('label')})" + ) + for processor in self.realtime_processors: + logger.debug(f"Calling process_frame on {processor.__class__.__name__}") + processor.process_frame(data, yuv_frame) + + for processor in self.post_processors: + if isinstance(processor, ObjectDescriptionProcessor): + # skip end events — _process_finalized handles them via event_end_subscriber. + # processing them here can re-create tracked_events entries after cleanup + # when the event_subscriber queue is backlogged behind event_end_subscriber. + if event_type == EventStateEnum.end: + continue + + processor.process_data( + { + "camera": camera, + "data": data, + "state": "update", + "yuv_frame": yuv_frame, + }, + PostProcessDataEnum.tracked_object, + ) + + self.frame_manager.close(frame_name) + + def _process_finalized(self) -> None: + """Process the end of an event.""" + while True: + ended = self.event_end_subscriber.check_for_update() + + if ended == None: + break + + event_id, camera, updated_db = ended + + # expire in realtime processors + for processor in self.realtime_processors: + processor.expire_object(event_id, camera) + + thumbnail: bytes | None = None + + if updated_db: + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + for processor in self.post_processors: + if isinstance(processor, ObjectDescriptionProcessor): + processor.cleanup_event(event_id) + continue + + # Skip the event if not an object + if event.data.get("type") != "object": + for processor in self.post_processors: + if isinstance(processor, ObjectDescriptionProcessor): + processor.cleanup_event(event_id) + continue + + # Extract valid thumbnail + thumbnail = get_event_thumbnail_bytes(event) + + # Embed the thumbnail + self._embed_thumbnail(event_id, thumbnail) + + # call any defined post processors + for processor in self.post_processors: + if isinstance(processor, LicensePlatePostProcessor): + recordings_available = self.recordings_available_through.get(camera) + if ( + recordings_available is not None + and event_id in self.detected_license_plates + and self.config.cameras[camera].type != "lpr" + ): + processor.process_data( + { + "event_id": event_id, + "camera": camera, + "recordings_available": self.recordings_available_through[ + camera + ], + "obj_data": self.detected_license_plates[event_id][ + "obj_data" + ], + }, + PostProcessDataEnum.recording, + ) + elif isinstance(processor, AudioTranscriptionPostProcessor): + continue + elif isinstance(processor, SemanticTriggerProcessor): + processor.process_data( + {"event_id": event_id, "camera": camera, "type": "image"}, + PostProcessDataEnum.tracked_object, + ) + elif isinstance(processor, ObjectDescriptionProcessor): + if not updated_db: + # Still need to cleanup tracked events even if not processing + processor.cleanup_event(event_id) + continue + + processor.process_data( + { + "event": event, + "camera": camera, + "state": "finalize", + "thumbnail": thumbnail, + }, + PostProcessDataEnum.tracked_object, + ) + else: + processor.process_data( + {"event_id": event_id, "camera": camera}, + PostProcessDataEnum.tracked_object, + ) + + def _expire_dedicated_lpr(self) -> None: + """Remove plates not seen for longer than expiration timeout for dedicated lpr cameras.""" + now = datetime.datetime.now().timestamp() + + to_remove = [] + + for id, data in self.detected_license_plates.items(): + last_seen = data.get("last_seen", 0) + if not last_seen: + continue + + if now - last_seen > self.config.cameras[data["camera"]].lpr.expire_time: + to_remove.append(id) + for id in to_remove: + self.event_metadata_publisher.publish( + (id, now), + EventMetadataTypeEnum.manual_event_end.value, + ) + self.detected_license_plates.pop(id) + + def _process_recordings_updates(self) -> None: + """Process recordings updates.""" + while True: + update = self.recordings_subscriber.check_for_update() + + if not update: + break + + (raw_topic, payload) = update + + if not raw_topic or not payload: + break + + topic = str(raw_topic) + + if topic.endswith(RecordingsDataTypeEnum.saved.value): + camera, recordings_available_through_timestamp, _ = payload + + self.recordings_available_through[camera] = ( + recordings_available_through_timestamp + ) + + logger.debug( + f"{camera} now has recordings available through {recordings_available_through_timestamp}" + ) + + def _process_review_updates(self) -> None: + """Process review updates.""" + while True: + review_updates = self.review_subscriber.check_for_update() + + if review_updates == None: + break + + for processor in self.post_processors: + if isinstance(processor, ReviewDescriptionProcessor): + processor.process_data(review_updates, PostProcessDataEnum.review) + + def _process_event_metadata(self): + # Check for regenerate description requests + (topic, payload) = self.event_metadata_subscriber.check_for_update() + + if topic is None: + return + + event_id, source, force = payload + + if event_id: + for processor in self.post_processors: + if isinstance(processor, ObjectDescriptionProcessor): + processor.handle_request( + "regenerate_description", + { + "event_id": event_id, + "source": RegenerateDescriptionEnum(source), + "force": force, + }, + ) + + def _process_frame_updates(self) -> None: + """Process event updates""" + (topic, data) = self.detection_subscriber.check_for_update() + + if topic is None: + return + + camera, frame_name, _, _, motion_boxes, _ = data + + if not camera or camera not in self.config.cameras: + return + + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return + + dedicated_lpr_enabled = ( + camera_config.type == CameraTypeEnum.lpr + and "license_plate" not in camera_config.objects.track + ) + + has_enabled_custom = any( + c.enabled for c in self.config.classification.custom.values() + ) + + if not dedicated_lpr_enabled and not has_enabled_custom: + # no active features that use this data + return + + try: + yuv_frame = self.frame_manager.get( + frame_name, camera_config.frame_shape_yuv + ) + except FileNotFoundError: + pass + + if yuv_frame is None: + logger.debug( + "Unable to process dedicated LPR update because frame is unavailable." + ) + return + + for processor in self.realtime_processors: + if ( + dedicated_lpr_enabled + and len(motion_boxes) > 0 + and isinstance(processor, LicensePlateRealTimeProcessor) + ): + processor.process_frame(camera, yuv_frame, True) + + if isinstance(processor, CustomStateClassificationProcessor): + processor.process_frame( + {"camera": camera, "motion": motion_boxes}, yuv_frame + ) + + self.frame_manager.close(frame_name) + + def _process_deferred_results(self) -> None: + """Drain results from deferred processors and perform IPC side-effects.""" + for processor in self.realtime_processors: + results = processor.drain_results() + + for result in results: + if result.get("type") != "classification": + continue + + if result["processor"] == "state": + self.requestor.send_data( + f"{result['camera']}/classification/{result['model_name']}", + result["state"], + ) + elif result["processor"] == "object": + object_id = result["object_id"] + camera = result["camera"] + timestamp = result["timestamp"] + model_name = result["model_name"] + label = result["label"] + score = result["score"] + classification_type = result["classification_type"] + + if classification_type == ObjectClassificationType.sub_label: + self.event_metadata_publisher.publish( + (object_id, label, score), + EventMetadataTypeEnum.sub_label, + ) + self.requestor.send_data( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.classification, + "id": object_id, + "camera": camera, + "timestamp": timestamp, + "model": model_name, + "sub_label": label, + "score": score, + } + ), + ) + elif classification_type == ObjectClassificationType.attribute: + self.event_metadata_publisher.publish( + (object_id, model_name, label, score), + EventMetadataTypeEnum.attribute.value, + ) + self.requestor.send_data( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.classification, + "id": object_id, + "camera": camera, + "timestamp": timestamp, + "model": model_name, + "attribute": label, + "score": score, + } + ), + ) + + def _embed_thumbnail(self, event_id: str, thumbnail: bytes) -> None: + """Embed the thumbnail for an event.""" + if not self.config.semantic_search.enabled: + return + + try: + self.embeddings.embed_thumbnail(event_id, thumbnail) + except ValueError: + logger.warning(f"Failed to embed thumbnail for event {event_id}") diff --git a/frigate/embeddings/onnx/base_embedding.py b/frigate/embeddings/onnx/base_embedding.py new file mode 100644 index 0000000..c0bd584 --- /dev/null +++ b/frigate/embeddings/onnx/base_embedding.py @@ -0,0 +1,97 @@ +"""Base class for onnx embedding implementations.""" + +import logging +import os +from abc import ABC, abstractmethod +from io import BytesIO +from typing import Any + +import numpy as np +import requests +from PIL import Image + +from frigate.const import UPDATE_MODEL_STATE +from frigate.types import ModelStatusTypesEnum +from frigate.util.downloader import ModelDownloader + +logger = logging.getLogger(__name__) + + +class BaseEmbedding(ABC): + """Base embedding class.""" + + def __init__(self, model_name: str, model_file: str, download_urls: dict[str, str]): + self.model_name = model_name + self.model_file = model_file + self.download_urls = download_urls + self.downloader: ModelDownloader = None + + def _download_model(self, path: str): + try: + file_name = os.path.basename(path) + + if file_name in self.download_urls: + ModelDownloader.download_from_url(self.download_urls[file_name], path) + + self.downloader.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.downloaded, + }, + ) + except Exception: + self.downloader.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.error, + }, + ) + + @abstractmethod + def _load_model_and_utils(self): + pass + + @abstractmethod + def _preprocess_inputs(self, raw_inputs: Any) -> Any: + pass + + def _process_image(self, image, output: str = "RGB") -> Image.Image: + if isinstance(image, str): + if image.startswith("http"): + response = requests.get(image) + image = Image.open(BytesIO(response.content)).convert(output) + elif isinstance(image, bytes): + image = Image.open(BytesIO(image)).convert(output) + elif isinstance(image, np.ndarray): + image = Image.fromarray(image) + + return image + + def _postprocess_outputs(self, outputs: Any) -> Any: + return outputs + + def __call__( + self, inputs: list[str] | list[Image.Image] | list[str] + ) -> list[np.ndarray]: + self._load_model_and_utils() + processed = self._preprocess_inputs(inputs) + input_names = self.runner.get_input_names() + onnx_inputs = {name: [] for name in input_names} + input: dict[str, Any] + for input in processed: + for key, value in input.items(): + if key in input_names: + onnx_inputs[key].append(value[0]) + + for key in input_names: + if onnx_inputs.get(key): + onnx_inputs[key] = np.stack(onnx_inputs[key]) + else: + logger.warning(f"Expected input '{key}' not found in onnx_inputs") + + outputs = self.runner.run(onnx_inputs)[0] + embeddings = self._postprocess_outputs(outputs) + + return [embedding for embedding in embeddings] diff --git a/frigate/embeddings/onnx/face_embedding.py b/frigate/embeddings/onnx/face_embedding.py new file mode 100644 index 0000000..75dfedc --- /dev/null +++ b/frigate/embeddings/onnx/face_embedding.py @@ -0,0 +1,194 @@ +"""Facenet Embeddings.""" + +import logging +import os + +import numpy as np + +from frigate.const import MODEL_CACHE_DIR +from frigate.detectors.detection_runners import get_optimized_runner +from frigate.embeddings.types import EnrichmentModelTypeEnum +from frigate.log import suppress_stderr_during +from frigate.util.downloader import ModelDownloader + +from ...config import FaceRecognitionConfig +from .base_embedding import BaseEmbedding + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from ai_edge_litert.interpreter import Interpreter + +logger = logging.getLogger(__name__) + +ARCFACE_INPUT_SIZE = 112 +FACENET_INPUT_SIZE = 160 + + +class FaceNetEmbedding(BaseEmbedding): + def __init__(self): + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="facedet", + model_file="facenet.tflite", + download_urls={ + "facenet.tflite": f"{GITHUB_ENDPOINT}/NickM-27/facenet-onnx/releases/download/v1.0/facenet.tflite", + }, + ) + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.tokenizer = None + self.feature_extractor = None + self.runner = None + files_names = list(self.download_urls.keys()) + + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + # Suppress TFLite delegate creation messages that bypass Python logging + with suppress_stderr_during("tflite_interpreter_init"): + self.runner = Interpreter( + model_path=os.path.join(MODEL_CACHE_DIR, "facedet/facenet.tflite"), + num_threads=2, + ) + self.runner.allocate_tensors() + self.tensor_input_details = self.runner.get_input_details() + self.tensor_output_details = self.runner.get_output_details() + + def _preprocess_inputs(self, raw_inputs): + pil = self._process_image(raw_inputs[0]) + + # handle images larger than input size + width, height = pil.size + if width != FACENET_INPUT_SIZE or height != FACENET_INPUT_SIZE: + if width > height: + new_height = int(((height / width) * FACENET_INPUT_SIZE) // 4 * 4) + pil = pil.resize((FACENET_INPUT_SIZE, new_height)) + else: + new_width = int(((width / height) * FACENET_INPUT_SIZE) // 4 * 4) + pil = pil.resize((new_width, FACENET_INPUT_SIZE)) + + og = np.array(pil).astype(np.float32) + + # Image must be FACE_EMBEDDING_SIZExFACE_EMBEDDING_SIZE + og_h, og_w, channels = og.shape + frame = np.zeros( + (FACENET_INPUT_SIZE, FACENET_INPUT_SIZE, channels), dtype=np.float32 + ) + + # compute center offset + x_center = (FACENET_INPUT_SIZE - og_w) // 2 + y_center = (FACENET_INPUT_SIZE - og_h) // 2 + + # copy img image into center of result image + frame[y_center : y_center + og_h, x_center : x_center + og_w] = og + + # run facenet normalization + frame = (frame / 127.5) - 1.0 + + frame = np.expand_dims(frame, axis=0) + return frame + + def __call__(self, inputs): + self._load_model_and_utils() + processed = self._preprocess_inputs(inputs) + self.runner.set_tensor(self.tensor_input_details[0]["index"], processed) + self.runner.invoke() + return self.runner.get_tensor(self.tensor_output_details[0]["index"]) + + +class ArcfaceEmbedding(BaseEmbedding): + def __init__(self, config: FaceRecognitionConfig): + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="facedet", + model_file="arcface.onnx", + download_urls={ + "arcface.onnx": f"{GITHUB_ENDPOINT}/NickM-27/facenet-onnx/releases/download/v1.0/arcface.onnx", + }, + ) + self.config = config + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.tokenizer = None + self.feature_extractor = None + self.runner = None + files_names = list(self.download_urls.keys()) + + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + device=self.config.device or "GPU", + model_type=EnrichmentModelTypeEnum.arcface.value, + ) + + def _preprocess_inputs(self, raw_inputs): + pil = self._process_image(raw_inputs[0]) + + # handle images larger than input size + width, height = pil.size + if width != ARCFACE_INPUT_SIZE or height != ARCFACE_INPUT_SIZE: + if width > height: + new_height = int(((height / width) * ARCFACE_INPUT_SIZE) // 4 * 4) + pil = pil.resize((ARCFACE_INPUT_SIZE, new_height)) + else: + new_width = int(((width / height) * ARCFACE_INPUT_SIZE) // 4 * 4) + pil = pil.resize((new_width, ARCFACE_INPUT_SIZE)) + + og = np.array(pil).astype(np.float32) + + # Image must be FACE_EMBEDDING_SIZExFACE_EMBEDDING_SIZE + og_h, og_w, channels = og.shape + frame = np.zeros( + (ARCFACE_INPUT_SIZE, ARCFACE_INPUT_SIZE, channels), dtype=np.float32 + ) + + # compute center offset + x_center = (ARCFACE_INPUT_SIZE - og_w) // 2 + y_center = (ARCFACE_INPUT_SIZE - og_h) // 2 + + # copy img image into center of result image + frame[y_center : y_center + og_h, x_center : x_center + og_w] = og + + # run arcface normalization + frame = (frame / 127.5) - 1.0 + + frame = np.transpose(frame, (2, 0, 1)) + frame = np.expand_dims(frame, axis=0) + return [{"data": frame}] diff --git a/frigate/embeddings/onnx/jina_v1_embedding.py b/frigate/embeddings/onnx/jina_v1_embedding.py new file mode 100644 index 0000000..5e3ee7f --- /dev/null +++ b/frigate/embeddings/onnx/jina_v1_embedding.py @@ -0,0 +1,228 @@ +"""JinaV1 Embeddings.""" + +import logging +import os +import threading +import warnings + +from transformers import AutoFeatureExtractor, AutoTokenizer +from transformers.utils.logging import disable_progress_bar + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import MODEL_CACHE_DIR, UPDATE_MODEL_STATE +from frigate.detectors.detection_runners import BaseModelRunner, get_optimized_runner + +# importing this without pytorch or others causes a warning +# https://github.com/huggingface/transformers/issues/27214 +# suppressed by setting env TRANSFORMERS_NO_ADVISORY_WARNINGS=1 +from frigate.embeddings.types import EnrichmentModelTypeEnum +from frigate.types import ModelStatusTypesEnum +from frigate.util.downloader import ModelDownloader + +from .base_embedding import BaseEmbedding + +warnings.filterwarnings( + "ignore", + category=FutureWarning, + message="The class CLIPFeatureExtractor is deprecated", +) + +# disables the progress bar for downloading tokenizers and feature extractors +disable_progress_bar() +logger = logging.getLogger(__name__) + + +class JinaV1TextEmbedding(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co") + super().__init__( + model_name="jinaai/jina-clip-v1", + model_file="text_model_fp16.onnx", + download_urls={ + "text_model_fp16.onnx": f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/onnx/text_model_fp16.onnx", + }, + ) + self.tokenizer_file = "tokenizer" + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.tokenizer = None + self.feature_extractor = None + self.runner = None + self._lock = threading.Lock() + files_names = list(self.download_urls.keys()) + [self.tokenizer_file] + + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _download_model(self, path: str): + try: + file_name = os.path.basename(path) + + if file_name in self.download_urls: + ModelDownloader.download_from_url(self.download_urls[file_name], path) + elif file_name == self.tokenizer_file: + if not os.path.exists(path + "/" + self.model_name): + logger.info(f"Downloading {self.model_name} tokenizer") + + tokenizer = AutoTokenizer.from_pretrained( + self.model_name, + trust_remote_code=True, + cache_dir=f"{MODEL_CACHE_DIR}/{self.model_name}/tokenizer", + clean_up_tokenization_spaces=True, + ) + tokenizer.save_pretrained(path) + + self.downloader.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.downloaded, + }, + ) + except Exception: + self.downloader.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.error, + }, + ) + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + tokenizer_path = os.path.join( + f"{MODEL_CACHE_DIR}/{self.model_name}/tokenizer" + ) + self.tokenizer = AutoTokenizer.from_pretrained( + self.model_name, + cache_dir=tokenizer_path, + trust_remote_code=True, + clean_up_tokenization_spaces=True, + ) + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.jina_v1.value, + ) + + def _preprocess_inputs(self, raw_inputs): + with self._lock: + max_length = max(len(self.tokenizer.encode(text)) for text in raw_inputs) + return [ + self.tokenizer( + text, + padding="max_length", + truncation=True, + max_length=max_length, + return_tensors="np", + ) + for text in raw_inputs + ] + + +class JinaV1ImageEmbedding(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + model_file = ( + "vision_model_fp16.onnx" + if model_size == "large" + else "vision_model_quantized.onnx" + ) + HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co") + super().__init__( + model_name="jinaai/jina-clip-v1", + model_file=model_file, + download_urls={ + model_file: f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/onnx/{model_file}", + "preprocessor_config.json": f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/preprocessor_config.json", + }, + ) + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.feature_extractor = None + self.runner: BaseModelRunner | None = None + self._lock = threading.Lock() + files_names = list(self.download_urls.keys()) + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + # Avoid lazy loading in worker threads: block until downloads complete + # and load the model on the main thread during initialization. + self._load_model_and_utils() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.feature_extractor = AutoFeatureExtractor.from_pretrained( + f"{MODEL_CACHE_DIR}/{self.model_name}", + ) + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.jina_v1.value, + ) + + def _preprocess_inputs(self, raw_inputs): + with self._lock: + processed_images = [self._process_image(img) for img in raw_inputs] + return [ + self.feature_extractor(images=image, return_tensors="np") + for image in processed_images + ] diff --git a/frigate/embeddings/onnx/jina_v2_embedding.py b/frigate/embeddings/onnx/jina_v2_embedding.py new file mode 100644 index 0000000..1abd968 --- /dev/null +++ b/frigate/embeddings/onnx/jina_v2_embedding.py @@ -0,0 +1,245 @@ +"""JinaV2 Embeddings.""" + +import io +import logging +import os +import threading + +import numpy as np +from PIL import Image +from transformers import AutoTokenizer +from transformers.utils.logging import disable_progress_bar, set_verbosity_error + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import MODEL_CACHE_DIR, UPDATE_MODEL_STATE +from frigate.detectors.detection_runners import get_optimized_runner +from frigate.embeddings.types import EnrichmentModelTypeEnum +from frigate.types import ModelStatusTypesEnum +from frigate.util.downloader import ModelDownloader + +from .base_embedding import BaseEmbedding + +# disables the progress bar and download logging for downloading tokenizers and image processors +disable_progress_bar() +set_verbosity_error() +logger = logging.getLogger(__name__) + + +class JinaV2Embedding(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + embedding_type: str = None, + ): + model_file = ( + "model_fp16.onnx" if model_size == "large" else "model_quantized.onnx" + ) + HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co") + super().__init__( + model_name="jinaai/jina-clip-v2", + model_file=model_file, + download_urls={ + model_file: f"{HF_ENDPOINT}/jinaai/jina-clip-v2/resolve/main/onnx/{model_file}", + "preprocessor_config.json": f"{HF_ENDPOINT}/jinaai/jina-clip-v2/resolve/main/preprocessor_config.json", + }, + ) + self.tokenizer_file = "tokenizer" + self.embedding_type = embedding_type + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.tokenizer = None + self.image_processor = None + self.runner = None + + # Lock to prevent concurrent calls (text and vision share this instance) + self._call_lock = threading.Lock() + + # download the model and tokenizer + files_names = list(self.download_urls.keys()) + [self.tokenizer_file] + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + # Avoid lazy loading in worker threads: block until downloads complete + # and load the model on the main thread during initialization. + self._load_model_and_utils() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _download_model(self, path: str): + try: + file_name = os.path.basename(path) + + if file_name in self.download_urls: + ModelDownloader.download_from_url(self.download_urls[file_name], path) + elif file_name == self.tokenizer_file: + if not os.path.exists(os.path.join(path, self.model_name)): + logger.info(f"Downloading {self.model_name} tokenizer") + + tokenizer = AutoTokenizer.from_pretrained( + self.model_name, + trust_remote_code=True, + cache_dir=os.path.join( + MODEL_CACHE_DIR, self.model_name, "tokenizer" + ), + clean_up_tokenization_spaces=True, + ) + tokenizer.save_pretrained(path) + self.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.downloaded, + }, + ) + except Exception: + self.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.error, + }, + ) + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + tokenizer_path = os.path.join( + f"{MODEL_CACHE_DIR}/{self.model_name}/tokenizer" + ) + self.tokenizer = AutoTokenizer.from_pretrained( + self.model_name, + cache_dir=tokenizer_path, + trust_remote_code=True, + clean_up_tokenization_spaces=True, + ) + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.jina_v2.value, + ) + + def _preprocess_image(self, image_data: bytes | Image.Image) -> np.ndarray: + """ + Manually preprocess a single image from bytes or PIL.Image to (3, 512, 512). + """ + if isinstance(image_data, bytes): + image = Image.open(io.BytesIO(image_data)) + else: + image = image_data + + if image.mode != "RGB": + image = image.convert("RGB") + + image = image.resize((512, 512), Image.Resampling.LANCZOS) + + # Convert to numpy array, normalize to [0, 1], and transpose to (channels, height, width) + image_array = np.array(image, dtype=np.float32) / 255.0 + image_array = np.transpose(image_array, (2, 0, 1)) # (H, W, C) -> (C, H, W) + + return image_array + + def _preprocess_inputs(self, raw_inputs): + """ + Preprocess inputs into a list of real input tensors (no dummies). + - For text: Returns list of input_ids. + - For vision: Returns list of pixel_values. + """ + if not isinstance(raw_inputs, list): + raw_inputs = [raw_inputs] + + processed = [] + if self.embedding_type == "text": + for text in raw_inputs: + input_ids = self.tokenizer([text], return_tensors="np")["input_ids"] + processed.append(input_ids) + elif self.embedding_type == "vision": + for img in raw_inputs: + pixel_values = self._preprocess_image(img) + processed.append( + pixel_values[np.newaxis, ...] + ) # Add batch dim: (1, 3, 512, 512) + else: + raise ValueError( + f"Invalid embedding_type: {self.embedding_type}. Must be 'text' or 'vision'." + ) + return processed + + def _postprocess_outputs(self, outputs): + """ + Process ONNX model outputs, truncating each embedding in the array to truncate_dim. + - outputs: NumPy array of embeddings. + - Returns: List of truncated embeddings. + """ + # size of vector in database + truncate_dim = 768 + + # jina v2 defaults to 1024 and uses Matryoshka representation, so + # truncating only causes an extremely minor decrease in retrieval accuracy + if outputs.shape[-1] > truncate_dim: + outputs = outputs[..., :truncate_dim] + + return outputs + + def __call__( + self, inputs: list[str] | list[Image.Image] | list[str], embedding_type=None + ) -> list[np.ndarray]: + # Lock the entire call to prevent race conditions when text and vision + # embeddings are called concurrently from different threads + with self._call_lock: + self.embedding_type = embedding_type + if not self.embedding_type: + raise ValueError( + "embedding_type must be specified either in __init__ or __call__" + ) + + self._load_model_and_utils() + processed = self._preprocess_inputs(inputs) + batch_size = len(processed) + + # Prepare ONNX inputs with matching batch sizes + onnx_inputs = {} + if self.embedding_type == "text": + onnx_inputs["input_ids"] = np.stack([x[0] for x in processed]) + onnx_inputs["pixel_values"] = np.zeros( + (batch_size, 3, 512, 512), dtype=np.float32 + ) + elif self.embedding_type == "vision": + onnx_inputs["input_ids"] = np.zeros((batch_size, 16), dtype=np.int64) + onnx_inputs["pixel_values"] = np.stack([x[0] for x in processed]) + else: + raise ValueError("Invalid embedding type") + + # Run inference + outputs = self.runner.run(onnx_inputs) + if self.embedding_type == "text": + embeddings = outputs[2] # text embeddings + elif self.embedding_type == "vision": + embeddings = outputs[3] # image embeddings + else: + raise ValueError("Invalid embedding type") + + embeddings = self._postprocess_outputs(embeddings) + return [embedding for embedding in embeddings] diff --git a/frigate/embeddings/onnx/lpr_embedding.py b/frigate/embeddings/onnx/lpr_embedding.py new file mode 100644 index 0000000..ad20999 --- /dev/null +++ b/frigate/embeddings/onnx/lpr_embedding.py @@ -0,0 +1,308 @@ +import logging +import os +import warnings + +import cv2 +import numpy as np + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import MODEL_CACHE_DIR +from frigate.detectors.detection_runners import BaseModelRunner, get_optimized_runner +from frigate.embeddings.types import EnrichmentModelTypeEnum +from frigate.types import ModelStatusTypesEnum +from frigate.util.downloader import ModelDownloader + +from .base_embedding import BaseEmbedding + +warnings.filterwarnings( + "ignore", + category=FutureWarning, + message="The class CLIPFeatureExtractor is deprecated", +) + +logger = logging.getLogger(__name__) + +LPR_EMBEDDING_SIZE = 256 + + +class PaddleOCRDetection(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + model_file = ( + "detection_v3-large.onnx" + if model_size == "large" + else "detection_v5-small.onnx" + ) + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="paddleocr-onnx", + model_file=model_file, + download_urls={ + model_file: f"{GITHUB_ENDPOINT}/hawkeye217/paddleocr-onnx/raw/refs/heads/master/models/{'v3' if model_size == 'large' else 'v5'}/{model_file}" + }, + ) + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.runner: BaseModelRunner | None = None + files_names = list(self.download_urls.keys()) + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.paddleocr.value, + ) + + def _preprocess_inputs(self, raw_inputs): + preprocessed = [] + for x in raw_inputs: + preprocessed.append(x) + return [{"x": preprocessed[0]}] + + +class PaddleOCRClassification(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="paddleocr-onnx", + model_file="classification.onnx", + download_urls={ + "classification.onnx": f"{GITHUB_ENDPOINT}/hawkeye217/paddleocr-onnx/raw/refs/heads/master/models/classification.onnx" + }, + ) + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.runner: BaseModelRunner | None = None + files_names = list(self.download_urls.keys()) + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.paddleocr.value, + ) + + def _preprocess_inputs(self, raw_inputs): + processed = [] + for img in raw_inputs: + processed.append({"x": img}) + return processed + + +class PaddleOCRRecognition(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="paddleocr-onnx", + model_file="recognition_v4.onnx", + download_urls={ + "recognition_v4.onnx": f"{GITHUB_ENDPOINT}/hawkeye217/paddleocr-onnx/raw/refs/heads/master/models/v4/recognition_v4.onnx", + "ppocr_keys_v1.txt": f"{GITHUB_ENDPOINT}/hawkeye217/paddleocr-onnx/raw/refs/heads/master/models/v4/ppocr_keys_v1.txt", + }, + ) + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.runner: BaseModelRunner | None = None + files_names = list(self.download_urls.keys()) + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.paddleocr.value, + ) + + def _preprocess_inputs(self, raw_inputs): + processed = [] + for img in raw_inputs: + processed.append({"x": img}) + return processed + + +class LicensePlateDetector(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="yolov9_license_plate", + model_file="yolov9-256-license-plates.onnx", + download_urls={ + "yolov9-256-license-plates.onnx": f"{GITHUB_ENDPOINT}/hawkeye217/yolov9-license-plates/raw/refs/heads/master/models/yolov9-256-license-plates.onnx" + }, + ) + + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.runner: BaseModelRunner | None = None + files_names = list(self.download_urls.keys()) + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.yolov9_license_plate.value, + ) + + def _preprocess_inputs(self, raw_inputs): + if isinstance(raw_inputs, list): + raise ValueError("License plate embedding does not support batch inputs.") + + img = raw_inputs + height, width, channels = img.shape + + # Resize maintaining aspect ratio + if width > height: + new_height = int(((height / width) * LPR_EMBEDDING_SIZE) // 4 * 4) + img = cv2.resize(img, (LPR_EMBEDDING_SIZE, new_height)) + else: + new_width = int(((width / height) * LPR_EMBEDDING_SIZE) // 4 * 4) + img = cv2.resize(img, (new_width, LPR_EMBEDDING_SIZE)) + + # Get new dimensions after resize + og_h, og_w, channels = img.shape + + # Create black square frame + frame = np.full( + (LPR_EMBEDDING_SIZE, LPR_EMBEDDING_SIZE, channels), + (0, 0, 0), + dtype=np.float32, + ) + + # Center the resized image in the square frame + x_center = (LPR_EMBEDDING_SIZE - og_w) // 2 + y_center = (LPR_EMBEDDING_SIZE - og_h) // 2 + frame[y_center : y_center + og_h, x_center : x_center + og_w] = img + + # Normalize to 0-1 + frame = frame / 255.0 + + # Convert from HWC to CHW format and add batch dimension + frame = np.transpose(frame, (2, 0, 1)) + frame = np.expand_dims(frame, axis=0) + return [{"images": frame}] diff --git a/frigate/embeddings/types.py b/frigate/embeddings/types.py new file mode 100644 index 0000000..32cbe5d --- /dev/null +++ b/frigate/embeddings/types.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class EmbeddingTypeEnum(str, Enum): + thumbnail = "thumbnail" + description = "description" + + +class EnrichmentModelTypeEnum(str, Enum): + arcface = "arcface" + facenet = "facenet" + jina_v1 = "jina_v1" + jina_v2 = "jina_v2" + paddleocr = "paddleocr" + yolov9_license_plate = "yolov9_license_plate" diff --git a/frigate/embeddings/util.py b/frigate/embeddings/util.py new file mode 100644 index 0000000..bc1a952 --- /dev/null +++ b/frigate/embeddings/util.py @@ -0,0 +1,54 @@ +"""Z-score normalization for search distance.""" + +import math + + +class ZScoreNormalization: + def __init__(self, scale_factor: float = 1.0, bias: float = 0.0): + """Initialize with optional scaling and bias adjustments.""" + """scale_factor adjusts the magnitude of each score""" + """bias will artificially shift the entire distribution upwards""" + self.n = 0 + self.mean = 0 + self.m2 = 0 + self.scale_factor = scale_factor + self.bias = bias + + @property + def variance(self): + return self.m2 / (self.n - 1) if self.n > 1 else 0.0 + + @property + def stddev(self): + return math.sqrt(self.variance) if self.variance > 0 else 0.0 + + def normalize(self, distances: list[float], save_stats: bool): + if save_stats: + self._update(distances) + if self.stddev == 0: + return distances + return [ + (x - self.mean) / self.stddev * self.scale_factor + self.bias + for x in distances + ] + + def _update(self, distances: list[float]): + for x in distances: + self.n += 1 + delta = x - self.mean + self.mean += delta / self.n + delta2 = x - self.mean + self.m2 += delta * delta2 + + def to_dict(self): + return { + "n": self.n, + "mean": self.mean, + "m2": self.m2, + } + + def from_dict(self, data: dict): + self.n = data["n"] + self.mean = data["mean"] + self.m2 = data["m2"] + return self diff --git a/frigate/events/__init__.py b/frigate/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/events/audio.py b/frigate/events/audio.py new file mode 100644 index 0000000..9af1bd2 --- /dev/null +++ b/frigate/events/audio.py @@ -0,0 +1,513 @@ +"""Handle creating audio events.""" + +import datetime +import logging +import subprocess +import threading +import time +from multiprocessing.managers import DictProxy +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import numpy as np + +from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import CameraConfig, CameraInput, FrigateConfig +from frigate.config.camera.ffmpeg import CameraFfmpegConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + AUDIO_DURATION, + AUDIO_FORMAT, + AUDIO_MAX_BIT_RANGE, + AUDIO_MIN_CONFIDENCE, + AUDIO_SAMPLE_RATE, + EXPIRE_AUDIO_ACTIVITY, + PROCESS_PRIORITY_HIGH, + UPDATE_AUDIO_ACTIVITY, +) +from frigate.data_processing.common.audio_transcription.model import ( + AudioTranscriptionModelRunner, +) +from frigate.data_processing.real_time.audio_transcription import ( + AudioTranscriptionRealTimeProcessor, +) +from frigate.ffmpeg_presets import parse_preset_input +from frigate.log import LogPipe, suppress_stderr_during +from frigate.util.builtin import get_ffmpeg_arg_list, load_labels +from frigate.util.ffmpeg import start_or_restart_ffmpeg, stop_ffmpeg +from frigate.util.process import FrigateProcess + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from ai_edge_litert.interpreter import Interpreter + + +logger = logging.getLogger(__name__) + + +def get_ffmpeg_command(ffmpeg: CameraFfmpegConfig) -> list[str]: + ffmpeg_input: CameraInput = [i for i in ffmpeg.inputs if "audio" in i.roles][0] + input_args = get_ffmpeg_arg_list(ffmpeg.global_args) + ( + parse_preset_input(ffmpeg_input.input_args, 1) + or get_ffmpeg_arg_list(ffmpeg_input.input_args) + or parse_preset_input(ffmpeg.input_args, 1) + or get_ffmpeg_arg_list(ffmpeg.input_args) + ) + return ( + [ffmpeg.ffmpeg_path, "-vn", "-threads", "1"] + + input_args + + ["-i"] + + [ffmpeg_input.path] + + [ + "-threads", + "1", + "-f", + f"{AUDIO_FORMAT}", + "-ar", + f"{AUDIO_SAMPLE_RATE}", + "-ac", + "1", + "-y", + "pipe:", + ] + ) + + +class AudioProcessor(FrigateProcess): + name = "frigate.audio_manager" + + def __init__( + self, + config: FrigateConfig, + camera_metrics: DictProxy, + stop_event: MpEvent, + ): + super().__init__( + stop_event, PROCESS_PRIORITY_HIGH, name="frigate.audio_manager", daemon=True + ) + + self.camera_metrics = camera_metrics + self.config = config + + def __stop_audio_thread(self, camera: str) -> None: + thread = self.audio_threads.pop(camera, None) + if thread is None: + return + + thread.stop() + thread.join(10) + if thread.is_alive(): + self.logger.warning(f"Audio maintainer thread for {camera} is still alive") + else: + self.logger.info(f"Audio maintainer stopped for {camera}") + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + self.audio_threads: dict[str, AudioEventMaintainer] = {} + + threading.current_thread().name = "process:audio_manager" + + if any( + c.enabled_in_config and c.audio_transcription.enabled + for c in self.config.cameras.values() + ): + self.transcription_model_runner: AudioTranscriptionModelRunner | None = ( + AudioTranscriptionModelRunner( + self.config.audio_transcription.device or "AUTO", + self.config.audio_transcription.model_size, + ) + ) + else: + self.transcription_model_runner = None + + config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.audio, + CameraConfigUpdateEnum.ffmpeg, + CameraConfigUpdateEnum.remove, + ], + ) + + def spawn_if_needed(camera: CameraConfig) -> None: + name = camera.name + if name is None or name in self.audio_threads: + return + if not camera.enabled or not camera.audio.enabled: + return + # ffmpeg update may not have arrived yet; wait for next poll + if not any("audio" in i.roles for i in camera.ffmpeg.inputs): + return + thread = AudioEventMaintainer( + camera, + self.config, + self.camera_metrics, + self.transcription_model_runner, + self.stop_event, # type: ignore[arg-type] + ) + self.audio_threads[name] = thread + thread.start() + self.logger.info(f"Audio maintainer started for {name}") + + for camera in self.config.cameras.values(): + spawn_if_needed(camera) + + self.logger.info(f"Audio processor started (pid: {self.pid})") + + # poll for newly added/removed cameras or cameras flipped to + # audio.enabled at runtime + while not self.stop_event.wait(timeout=1.0): + updated_topics = config_subscriber.check_for_updates() + + # stop maintainers for removed cameras so their ffmpeg process is + # torn down and they stop touching camera_metrics (which the camera + # maintainer has already popped for the removed camera) + for removed_camera in updated_topics.get( + CameraConfigUpdateEnum.remove.name, [] + ): + self.__stop_audio_thread(removed_camera) + + for camera in self.config.cameras.values(): + spawn_if_needed(camera) + + config_subscriber.stop() + + for thread in self.audio_threads.values(): + thread.join(1) + if thread.is_alive(): + self.logger.info(f"Waiting for thread {thread.name:s} to exit") + thread.join(10) + + for thread in self.audio_threads.values(): + if thread.is_alive(): + self.logger.warning(f"Thread {thread.name} is still alive") + + self.logger.info("Exiting audio processor") + + +class AudioEventMaintainer(threading.Thread): + def __init__( + self, + camera: CameraConfig, + config: FrigateConfig, + camera_metrics: DictProxy, + audio_transcription_model_runner: AudioTranscriptionModelRunner | None, + stop_event: threading.Event, + ) -> None: + super().__init__(name=f"{camera.name}_audio_event_processor") + + self.config = config + self.camera_config = camera + self.camera_metrics = camera_metrics + self.stop_event = stop_event + # per-camera stop signal so a single maintainer can be torn down at + # runtime (e.g. on camera removal) without stopping the whole process + self.camera_stop_event = threading.Event() + self.detector = AudioTfl(stop_event, self.camera_config.audio.num_threads) + self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),) + self.chunk_size = int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE * 2)) + self.logger = logging.getLogger(f"audio.{self.camera_config.name}") + self.ffmpeg_cmd = get_ffmpeg_command(self.camera_config.ffmpeg) + self.logpipe = LogPipe(f"ffmpeg.{self.camera_config.name}.audio") + self.audio_listener: subprocess.Popen[Any] | None = None + self.audio_transcription_model_runner = audio_transcription_model_runner + self.transcription_processor = None + self.transcription_thread = None + + # create communication for audio detections + self.requestor = InterProcessRequestor() + self.config_subscriber = CameraConfigUpdateSubscriber( + None, + {str(self.camera_config.name): self.camera_config}, + [ + CameraConfigUpdateEnum.audio, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.audio_transcription, + ], + ) + self.detection_publisher = DetectionPublisher(DetectionTypeEnum.audio.value) + + if ( + self.camera_config.audio_transcription.enabled + and self.audio_transcription_model_runner is not None + ): + # init the transcription processor for this camera + self.transcription_processor = AudioTranscriptionRealTimeProcessor( + config=self.config, + camera_config=self.camera_config, + requestor=self.requestor, + model_runner=self.audio_transcription_model_runner, + metrics=self.camera_metrics[self.camera_config.name], + stop_event=self.stop_event, + ) + + self.transcription_thread = threading.Thread( + target=self.transcription_processor.run, + name=f"{self.camera_config.name}_transcription_processor", + daemon=True, + ) + self.transcription_thread.start() + + self.was_enabled = camera.enabled + self.was_audio_enabled = camera.audio.enabled + + def detect_audio(self, audio: np.ndarray) -> None: + if ( + not self.camera_config.audio.enabled + or self.stop_event.is_set() + or self.camera_stop_event.is_set() + ): + return + + audio_as_float: np.ndarray = audio.astype(np.float32) + rms, dBFS = self.calculate_audio_levels(audio_as_float) + + self.camera_metrics[self.camera_config.name].audio_rms.value = rms + self.camera_metrics[self.camera_config.name].audio_dBFS.value = dBFS + + audio_detections: list[tuple[str, float]] = [] + + # only run audio detection when volume is above min_volume + if rms >= self.camera_config.audio.min_volume: + # create waveform relative to max range and look for detections + waveform = (audio / AUDIO_MAX_BIT_RANGE).astype(np.float32) + model_detections = self.detector.detect(waveform) + + for label, score, _ in model_detections: + self.logger.debug( + f"{self.camera_config.name} heard {label} with a score of {score}" + ) + + if label not in self.camera_config.audio.listen: + continue + + if score > dict( + (self.camera_config.audio.filters or {}).get(label, {}) + ).get("threshold", 0.8): + audio_detections.append((label, score)) + + # send audio detection data + self.detection_publisher.publish( + ( + self.camera_config.name, + datetime.datetime.now().timestamp(), + dBFS, + [label for label, _ in audio_detections], + ) + ) + + # send audio activity update + self.requestor.send_data( + UPDATE_AUDIO_ACTIVITY, + {self.camera_config.name: {"detections": audio_detections}}, + ) + + # run audio transcription + if self.transcription_processor is not None: + if self.camera_config.audio_transcription.live_enabled: + # process audio until we've reached the endpoint + self.transcription_processor.process_audio( + { + "id": f"{self.camera_config.name}_audio", + "camera": self.camera_config.name, + }, + audio, + ) + else: + self.transcription_processor.check_unload_model() + + def calculate_audio_levels(self, audio_as_float: np.ndarray) -> tuple[float, float]: + # Calculate RMS (Root-Mean-Square) which represents the average signal amplitude + # Note: np.float32 isn't serializable, we must use np.float64 to publish the message + rms = np.sqrt(np.mean(np.absolute(np.square(audio_as_float)))) + + # Transform RMS to dBFS (decibels relative to full scale) + if rms > 0: + dBFS = 20 * np.log10(np.abs(rms) / AUDIO_MAX_BIT_RANGE) + else: + dBFS = 0 + + self.requestor.send_data(f"{self.camera_config.name}/audio/dBFS", float(dBFS)) + self.requestor.send_data(f"{self.camera_config.name}/audio/rms", float(rms)) + + return float(rms), float(dBFS) + + def start_or_restart_ffmpeg(self) -> None: + self.audio_listener = start_or_restart_ffmpeg( + self.ffmpeg_cmd, + self.logger, + self.logpipe, + self.chunk_size, + self.audio_listener, + ) + self.requestor.send_data(f"{self.camera_config.name}/status/audio", "online") + + def read_audio(self) -> None: + def log_and_restart() -> None: + if self.stop_event.is_set(): + return + + time.sleep(self.camera_config.ffmpeg.retry_interval) + self.logpipe.dump() + self.start_or_restart_ffmpeg() + + if self.audio_listener is None or self.audio_listener.stdout is None: + log_and_restart() + return + + try: + chunk = self.audio_listener.stdout.read(self.chunk_size) + + if not chunk: + if self.audio_listener.poll() is not None: + self.requestor.send_data( + f"{self.camera_config.name}/status/audio", "offline" + ) + self.logger.error("ffmpeg process is not running, restarting...") + log_and_restart() + return + + return + + audio = np.frombuffer(chunk, dtype=np.int16) + self.detect_audio(audio) + except Exception as e: + self.logger.error(f"Error reading audio data from ffmpeg process: {e}") + log_and_restart() + + def stop(self) -> None: + """Signal this maintainer to exit its run loop and clean up.""" + self.camera_stop_event.set() + + def run(self) -> None: + if self.camera_config.enabled: + self.start_or_restart_ffmpeg() + + while not self.stop_event.is_set() and not self.camera_stop_event.is_set(): + # check if there is an updated config + self.config_subscriber.check_for_updates() + + enabled = self.camera_config.enabled + if enabled != self.was_enabled: + if enabled: + self.logger.debug( + f"Enabling audio detections for {self.camera_config.name}" + ) + self.start_or_restart_ffmpeg() + else: + self.requestor.send_data( + f"{self.camera_config.name}/status/audio", "disabled" + ) + self.logger.debug( + f"Disabling audio detections for {self.camera_config.name}, ending events" + ) + self.requestor.send_data( + EXPIRE_AUDIO_ACTIVITY, self.camera_config.name + ) + + if self.audio_listener: + stop_ffmpeg(self.audio_listener, self.logger) + + self.audio_listener = None + self.was_enabled = enabled + continue + + if not enabled: + time.sleep(0.1) + continue + + audio_enabled = self.camera_config.audio.enabled + if audio_enabled != self.was_audio_enabled: + if not audio_enabled: + self.logger.debug( + f"Disabling audio detections for {self.camera_config.name}, ending events" + ) + self.requestor.send_data( + EXPIRE_AUDIO_ACTIVITY, self.camera_config.name + ) + self.was_audio_enabled = audio_enabled + + self.read_audio() + + if self.audio_listener: + stop_ffmpeg(self.audio_listener, self.logger) + if self.transcription_thread: + self.transcription_thread.join(timeout=2) + if self.transcription_thread.is_alive(): + self.logger.warning( + f"Audio transcription thread {self.transcription_thread.name} is still alive" + ) + self.logpipe.close() + self.requestor.stop() + self.config_subscriber.stop() + self.detection_publisher.stop() + + +class AudioTfl: + def __init__(self, stop_event: threading.Event, num_threads: int = 2) -> None: + self.stop_event = stop_event + self.num_threads = num_threads + self.labels = load_labels("/audio-labelmap.txt", prefill=521) + # Suppress TFLite delegate creation messages that bypass Python logging + with suppress_stderr_during("tflite_interpreter_init"): + self.interpreter = Interpreter( + model_path="/cpu_audio_model.tflite", + num_threads=self.num_threads, + ) + self.interpreter.allocate_tensors() + + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + + def _detect_raw(self, tensor_input: np.ndarray) -> np.ndarray: + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input) + self.interpreter.invoke() + detections = np.zeros((20, 6), np.float32) + + res = self.interpreter.get_tensor(self.tensor_output_details[0]["index"])[0] + non_zero_indices = res > 0 + class_ids = np.argpartition(-res, 20)[:20] + class_ids = class_ids[np.argsort(-res[class_ids])] + class_ids = class_ids[non_zero_indices[class_ids]] + scores = res[class_ids] + boxes = np.full((scores.shape[0], 4), -1, np.float32) + count = len(scores) + + for i in range(count): + if scores[i] < AUDIO_MIN_CONFIDENCE or i == 20: + break + detections[i] = [ + class_ids[i], + float(scores[i]), + boxes[i][0], + boxes[i][1], + boxes[i][2], + boxes[i][3], + ] + + return detections + + def detect( + self, tensor_input: np.ndarray, threshold: float = AUDIO_MIN_CONFIDENCE + ) -> list[tuple[str, float, tuple[float, float, float, float]]]: + detections: list[tuple[str, float, tuple[float, float, float, float]]] = [] + + if self.stop_event.is_set(): + return detections + + raw_detections = self._detect_raw(tensor_input) + + for d in raw_detections: + if d[1] < threshold: + break + detections.append( + (self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5])) + ) + return detections diff --git a/frigate/events/cleanup.py b/frigate/events/cleanup.py new file mode 100644 index 0000000..b867bf9 --- /dev/null +++ b/frigate/events/cleanup.py @@ -0,0 +1,374 @@ +"""Cleanup events based on configured retention.""" + +import datetime +import logging +import os +import threading +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path +from typing import Any + +from frigate.config import FrigateConfig +from frigate.const import CLIPS_DIR +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.models import Event, Timeline +from frigate.util.file import delete_event_snapshot, delete_event_thumbnail + +logger = logging.getLogger(__name__) + + +CHUNK_SIZE = 50 + + +class EventCleanup(threading.Thread): + def __init__( + self, config: FrigateConfig, stop_event: MpEvent, db: SqliteVecQueueDatabase + ): + super().__init__(name="event_cleanup") + self.config = config + self.stop_event = stop_event + self.db = db + self.camera_keys = list(self.config.cameras.keys()) + self.removed_camera_labels: list[Event] | None = None + self.camera_labels: dict[str, dict[str, Any]] = {} + + def get_removed_camera_labels(self) -> list[Event]: + """Get a list of distinct labels for removed cameras.""" + if self.removed_camera_labels is None: + self.removed_camera_labels = list( + Event.select(Event.label) + .where(Event.camera.not_in(self.camera_keys)) # type: ignore[arg-type,call-arg,misc] + .distinct() + .execute() + ) + + return self.removed_camera_labels + + def get_camera_labels(self, camera: str) -> list[Event]: + """Get a list of distinct labels for each camera, updating once a day.""" + if ( + self.camera_labels.get(camera) is None + or self.camera_labels[camera]["last_update"] + < (datetime.datetime.now() - datetime.timedelta(days=1)).timestamp() + ): + self.camera_labels[camera] = { + "last_update": datetime.datetime.now().timestamp(), + "labels": list( + Event.select(Event.label) + .where(Event.camera == camera) + .distinct() + .execute() + ), + } + + return self.camera_labels[camera]["labels"] # type: ignore[no-any-return] + + def expire_snapshots(self) -> list[str]: + ## Expire events from unlisted cameras based on the global config + retain_config = self.config.snapshots.retain + update_params = {"has_snapshot": False} + + distinct_labels = self.get_removed_camera_labels() + + ## Expire events from cameras no longer in the config + # loop over object types in db + for event in distinct_labels: + # get expiration time for this label + expire_days = retain_config.objects.get( + str(event.label), retain_config.default + ) + + expire_after = ( + datetime.datetime.now() - datetime.timedelta(days=expire_days) + ).timestamp() + # grab all events after specific time + expired_events: list[Event] = ( + Event.select( + Event.id, + Event.camera, + Event.thumbnail, + ) + .where( + Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc] + Event.start_time < expire_after, + Event.label == event.label, + Event.retain_indefinitely == False, + ) + .namedtuples() + .iterator() + ) + expired_events = list(expired_events) + logger.debug(f"{len(expired_events)} events can be expired") + + # delete the media from disk + for expired in expired_events: + deleted = delete_event_snapshot(expired) + + if not deleted: + logger.warning( + f"Unable to delete event images for {expired.camera}: {expired.id}" + ) + + # update the clips attribute for the db entry + query = Event.select(Event.id).where( + Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc] + Event.start_time < expire_after, + Event.label == event.label, + Event.retain_indefinitely == False, + ) + + events_to_update: list[str] = [] + + for event in query.iterator(): + events_to_update.append(str(event.id)) + if len(events_to_update) >= CHUNK_SIZE: + logger.debug( + f"Updating {update_params} for {len(events_to_update)} events" + ) + Event.update(update_params).where( + Event.id << events_to_update + ).execute() + events_to_update = [] + + # Update any remaining events + if events_to_update: + logger.debug( + f"Updating clips/snapshots attribute for {len(events_to_update)} events" + ) + Event.update(update_params).where( + Event.id << events_to_update + ).execute() + + events_to_update = [] + + ## Expire events from cameras based on the camera config + for name, camera in self.config.cameras.items(): + retain_config = camera.snapshots.retain + + # get distinct objects in database for this camera + distinct_labels = self.get_camera_labels(name) + + # loop over object types in db + for event in distinct_labels: + # get expiration time for this label + expire_days = retain_config.objects.get( + str(event.label), retain_config.default + ) + + expire_after = ( + datetime.datetime.now() - datetime.timedelta(days=expire_days) + ).timestamp() + # grab all events after specific time + expired_events = ( + Event.select( + Event.id, + Event.camera, + Event.thumbnail, + ) + .where( + Event.camera == name, + Event.start_time < expire_after, + Event.label == event.label, + Event.retain_indefinitely == False, + ) + .namedtuples() + .iterator() + ) + + # delete the grabbed clips from disk + # only snapshots are stored in /clips + # so no need to delete mp4 files + for event in expired_events: + events_to_update.append(str(event.id)) + deleted = delete_event_snapshot(event) + + if not deleted: + logger.warning( + f"Unable to delete event images for {event.camera}: {event.id}" + ) + + # update the clips attribute for the db entry + for i in range(0, len(events_to_update), CHUNK_SIZE): + batch = events_to_update[i : i + CHUNK_SIZE] + logger.debug(f"Updating {update_params} for {len(batch)} events") + Event.update(update_params).where(Event.id << batch).execute() + + return events_to_update + + def expire_clips(self) -> list[str]: + ## Expire events from unlisted cameras based on the global config + expire_days = max( + self.config.record.alerts.retain.days, + self.config.record.detections.retain.days, + ) + file_extension = None # mp4 clips are no longer stored in /clips + update_params = {"has_clip": False} + + # get expiration time for this label + + expire_after = ( + datetime.datetime.now() - datetime.timedelta(days=expire_days) + ).timestamp() + # grab all events after specific time + expired_events: list[Event] = ( + Event.select( + Event.id, + Event.camera, + ) + .where( + Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc] + Event.start_time < expire_after, + Event.retain_indefinitely == False, + ) + .namedtuples() + .iterator() + ) + expired_events = list(expired_events) + logger.debug(f"{len(expired_events)} events can be expired") + # delete the media from disk + for expired in expired_events: + media_name = f"{expired.camera}-{expired.id}" + media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}") + + try: + media_path.unlink(missing_ok=True) + if file_extension == "jpg": + media_path = Path( + f"{os.path.join(CLIPS_DIR, media_name)}-clean.webp" + ) + media_path.unlink(missing_ok=True) + # Also delete clean.png (legacy) for backward compatibility + media_path = Path( + f"{os.path.join(CLIPS_DIR, media_name)}-clean.png" + ) + media_path.unlink(missing_ok=True) + except OSError as e: + logger.warning(f"Unable to delete event images: {e}") + + # update the clips attribute for the db entry + query = Event.select(Event.id).where( + Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc] + Event.start_time < expire_after, + Event.retain_indefinitely == False, + ) + + events_to_update = [] + + for event in query.iterator(): + events_to_update.append(event.id) + + if len(events_to_update) >= CHUNK_SIZE: + logger.debug( + f"Updating {update_params} for {len(events_to_update)} events" + ) + Event.update(update_params).where( + Event.id << events_to_update + ).execute() + events_to_update = [] + + # Update any remaining events + if events_to_update: + logger.debug( + f"Updating clips/snapshots attribute for {len(events_to_update)} events" + ) + Event.update(update_params).where(Event.id << events_to_update).execute() + + events_to_update = [] + now = datetime.datetime.now() + + ## Expire events from cameras based on the camera config + for name, camera in self.config.cameras.items(): + expire_days = max( + camera.record.alerts.retain.days, + camera.record.detections.retain.days, + ) + alert_expire_date = ( + now - datetime.timedelta(days=camera.record.alerts.retain.days) + ).timestamp() + detection_expire_date = ( + now - datetime.timedelta(days=camera.record.detections.retain.days) + ).timestamp() + # grab all events after specific time + expired_events = ( + Event.select( + Event.id, + Event.camera, + ) + .where( + Event.camera == name, + Event.retain_indefinitely == False, + ( + ( + (Event.data["max_severity"] != "detection") + | (Event.data["max_severity"].is_null()) + ) + & (Event.end_time < alert_expire_date) + ) + | ( + (Event.data["max_severity"] == "detection") + & (Event.end_time < detection_expire_date) + ), + ) + .namedtuples() + .iterator() + ) + + # delete the grabbed clips from disk + # only snapshots are stored in /clips + # so no need to delete mp4 files + for event in expired_events: + events_to_update.append(event.id) + + # update the clips attribute for the db entry + for i in range(0, len(events_to_update), CHUNK_SIZE): + batch = events_to_update[i : i + CHUNK_SIZE] + logger.debug(f"Updating {update_params} for {len(batch)} events") + Event.update(update_params).where(Event.id << batch).execute() + + return events_to_update + + def run(self) -> None: + if self.config.safe_mode: + logger.info("Safe mode enabled, skipping event cleanup") + return + + # only expire events every 5 minutes + while not self.stop_event.wait(300): + events_with_expired_clips = self.expire_clips() + + # delete timeline entries for events that have expired recordings + # delete up to 100,000 at a time + max_deletes = 100000 + deleted_events_list = list(events_with_expired_clips) + for i in range(0, len(deleted_events_list), max_deletes): + Timeline.delete().where( + Timeline.source_id << deleted_events_list[i : i + max_deletes] + ).execute() + + self.expire_snapshots() + + # drop events from db where has_clip and has_snapshot are false + events = ( + Event.select() + .where(Event.has_clip == False, Event.has_snapshot == False) + .iterator() + ) + events_to_delete: list[Event] = [e for e in events] + + for e in events_to_delete: + delete_event_thumbnail(e) + + logger.debug(f"Found {len(events_to_delete)} events that can be expired") + if len(events_to_delete) > 0: + ids_to_delete = [str(e.id) for e in events_to_delete] + for i in range(0, len(ids_to_delete), CHUNK_SIZE): + chunk = ids_to_delete[i : i + CHUNK_SIZE] + logger.debug(f"Deleting {len(chunk)} events from the database") + Event.delete().where(Event.id << chunk).execute() + + if self.config.semantic_search.enabled: + self.db.delete_embeddings_description(event_ids=chunk) + self.db.delete_embeddings_thumbnail(event_ids=chunk) + logger.debug(f"Deleted {len(ids_to_delete)} embeddings") + + logger.info("Exiting event cleanup...") diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py new file mode 100644 index 0000000..169b213 --- /dev/null +++ b/frigate/events/maintainer.py @@ -0,0 +1,348 @@ +import logging +import threading +from multiprocessing import Queue +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber +from frigate.config import FrigateConfig +from frigate.config.classification import ObjectClassificationType +from frigate.const import REPLAY_CAMERA_PREFIX +from frigate.events.types import EventStateEnum, EventTypeEnum +from frigate.models import Event +from frigate.util.builtin import to_relative_box + +logger = logging.getLogger(__name__) + + +def should_update_db(prev_event: dict[str, Any], current_event: dict[str, Any]) -> bool: + """If current_event has updated fields and (clip or snapshot).""" + # If event is ending and was previously saved, always update to set end_time + # This ensures events are properly ended even when alerts/detections are disabled + # mid-event (which can cause has_clip/has_snapshot to become False) + if ( + prev_event["end_time"] is None + and current_event["end_time"] is not None + and (prev_event["has_clip"] or prev_event["has_snapshot"]) + ): + return True + + if current_event["has_clip"] or current_event["has_snapshot"]: + # if this is the first time has_clip or has_snapshot turned true + if not prev_event["has_clip"] and not prev_event["has_snapshot"]: + return True + # or if any of the following values changed + if ( + prev_event["top_score"] != current_event["top_score"] + or prev_event["entered_zones"] != current_event["entered_zones"] + or prev_event["end_time"] != current_event["end_time"] + or prev_event["average_estimated_speed"] + != current_event["average_estimated_speed"] + or prev_event["velocity_angle"] != current_event["velocity_angle"] + or prev_event["recognized_license_plate"] + != current_event["recognized_license_plate"] + or prev_event["path_data"] != current_event["path_data"] + ): + return True + return False + + +def should_update_state( + prev_event: dict[str, Any], current_event: dict[str, Any] +) -> bool: + """If current event should update state, but not necessarily update the db.""" + if prev_event["stationary"] != current_event["stationary"]: + return True + + if prev_event["attributes"] != current_event["attributes"]: + return True + + if prev_event["sub_label"] != current_event["sub_label"]: + return True + + if set(prev_event["current_zones"]) != set(current_event["current_zones"]): + return True + + return False + + +class EventProcessor(threading.Thread): + def __init__( + self, + config: FrigateConfig, + timeline_queue: Queue, + stop_event: MpEvent, + ): + super().__init__(name="event_processor") + self.config = config + self.timeline_queue = timeline_queue + self.events_in_process: dict[str, dict[str, Any]] = {} + self.stop_event = stop_event + + self.event_receiver = EventUpdateSubscriber() + self.event_end_publisher = EventEndPublisher() + + def run(self) -> None: + # set an end_time on events without an end_time on startup + Event.update(end_time=Event.start_time + 30).where( + Event.end_time == None + ).execute() + + while not self.stop_event.is_set(): + update = self.event_receiver.check_for_update(timeout=1) + + if update == None: + continue + + source_type, event_type, camera, _, event_data = update # type: ignore[misc] + + logger.debug( + f"Event received: {source_type} {event_type} {camera} {event_data['id']}" + ) + + if source_type == EventTypeEnum.tracked_object: + id = event_data["id"] + self.timeline_queue.put( + ( + camera, + source_type, + event_type, + self.events_in_process.get(id), + event_data, + ) + ) + + # if this is the first message, just store it and continue, its not time to insert it in the db + if ( + event_type == EventStateEnum.start + or id not in self.events_in_process + ): + self.events_in_process[id] = event_data + continue + + self.handle_object_detection(event_type, camera, event_data) + elif source_type == EventTypeEnum.api: + self.timeline_queue.put( + ( + camera, + source_type, + event_type, + {}, + event_data, + ) + ) + + self.handle_external_detection(event_type, event_data) + + self.event_receiver.stop() + self.event_end_publisher.stop() + logger.info("Exiting event processor...") + + def handle_object_detection( + self, + event_type: str, + camera: str, + event_data: dict[str, Any], + ) -> None: + """handle tracked object event updates.""" + updated_db = False + + if should_update_db(self.events_in_process[event_data["id"]], event_data): + updated_db = True + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return + + width = camera_config.detect.width + height = camera_config.detect.height + + if width is None or height is None: + return + + first_detector = list(self.config.detectors.values())[0] + + start_time = event_data["start_time"] + end_time = ( + None if event_data["end_time"] is None else event_data["end_time"] + ) + snapshot = event_data["snapshot"] + # score of the snapshot + score = None if snapshot is None else snapshot["score"] + # detection region in the snapshot + region = ( + None + if snapshot is None + else to_relative_box( + width, + height, + snapshot["region"], + ) + ) + # bounding box for the snapshot + box = ( + None + if snapshot is None + else to_relative_box( + width, + height, + snapshot["box"], + ) + ) + + attributes = ( + None + if snapshot is None + else [ + { + "box": to_relative_box( + width, + height, + a["box"], + ), + "label": a["label"], + "score": a["score"], + } + for a in snapshot["attributes"] + ] + ) + snapshot_frame_time = None if snapshot is None else snapshot["frame_time"] + snapshot_area = None if snapshot is None else snapshot["area"] + snapshot_estimated_speed = ( + None if snapshot is None else snapshot["current_estimated_speed"] + ) + + # keep these from being set back to false because the event + # may have started while recordings/snapshots/alerts/detections were enabled + # this would be an issue for long running events + if self.events_in_process[event_data["id"]]["has_clip"]: + event_data["has_clip"] = True + if self.events_in_process[event_data["id"]]["has_snapshot"]: + event_data["has_snapshot"] = True + + event = { + Event.id: event_data["id"], + Event.label: event_data["label"], + Event.camera: camera, + Event.start_time: start_time, + Event.end_time: end_time, + Event.zones: list(event_data["entered_zones"]), + Event.thumbnail: event_data.get("thumbnail"), + Event.has_clip: event_data["has_clip"], + Event.has_snapshot: event_data["has_snapshot"], + Event.model_hash: first_detector.model.model_hash + if first_detector.model + else None, + Event.model_type: first_detector.model.model_type + if first_detector.model + else None, + Event.detector_type: first_detector.type, + Event.data: { + "box": box, + "region": region, + "score": score, + "top_score": event_data["top_score"], + "attributes": attributes, + "snapshot_clean": event_data.get("snapshot_clean", False), + "snapshot_frame_time": snapshot_frame_time, + "snapshot_area": snapshot_area, + "snapshot_estimated_speed": snapshot_estimated_speed, + "average_estimated_speed": event_data["average_estimated_speed"], + "velocity_angle": event_data["velocity_angle"], + "type": "object", + "max_severity": event_data.get("max_severity"), + "path_data": event_data.get("path_data"), + }, + } + + # only overwrite the sub_label in the database if it's set + if event_data.get("sub_label") is not None: + event[Event.sub_label] = event_data["sub_label"][0] + event[Event.data]["sub_label_score"] = event_data["sub_label"][1] + + # only overwrite the recognized_license_plate in the database if it's set + if event_data.get("recognized_license_plate") is not None: + event[Event.data]["recognized_license_plate"] = event_data[ + "recognized_license_plate" + ][0] + event[Event.data]["recognized_license_plate_score"] = event_data[ + "recognized_license_plate" + ][1] + + # only overwrite attribute-type custom model fields in the database if they're set + for name, model_config in self.config.classification.custom.items(): + if ( + model_config.object_config + and model_config.object_config.classification_type + == ObjectClassificationType.attribute + ): + value = event_data.get(name) + if value is not None: + event[Event.data][name] = value[0] + event[Event.data][f"{name}_score"] = value[1] + + ( + Event.insert(event) + .on_conflict( + conflict_target=[Event.id], + update=event, + ) + .execute() + ) + + # check if the stored event_data should be updated + if updated_db or should_update_state( + self.events_in_process[event_data["id"]], event_data + ): + # update the stored copy for comparison on future update messages + self.events_in_process[event_data["id"]] = event_data + + if event_type == EventStateEnum.end: + del self.events_in_process[event_data["id"]] + self.event_end_publisher.publish((event_data["id"], camera, updated_db)) # type: ignore[arg-type] + + def handle_external_detection( + self, event_type: EventStateEnum, event_data: dict[str, Any] + ) -> None: + # Skip replay cameras + if event_data.get("camera", "").startswith(REPLAY_CAMERA_PREFIX): + return + + if event_type == EventStateEnum.start: + event = { + Event.id: event_data["id"], + Event.label: event_data["label"], + Event.sub_label: event_data["sub_label"], + Event.camera: event_data["camera"], + Event.start_time: event_data["start_time"], + Event.end_time: event_data["end_time"], + Event.thumbnail: event_data.get("thumbnail"), + Event.has_clip: event_data["has_clip"], + Event.has_snapshot: event_data["has_snapshot"], + Event.zones: [], + Event.data: { + "type": event_data["type"], + "score": event_data["score"], + "top_score": event_data["score"], + "snapshot_clean": event_data.get("snapshot_clean", False), + }, + } + if event_data.get("draw") is not None: + event[Event.data]["draw"] = event_data["draw"] + if event_data.get("recognized_license_plate") is not None: + event[Event.data]["recognized_license_plate"] = event_data[ + "recognized_license_plate" + ] + event[Event.data]["recognized_license_plate_score"] = event_data[ + "score" + ] + Event.insert(event).execute() + elif event_type == EventStateEnum.end: + event = { + Event.id: event_data["id"], + Event.end_time: event_data["end_time"], + } + + try: + Event.update(event).where(Event.id == event_data["id"]).execute() + except Exception: + logger.warning(f"Failed to update manual event: {event_data['id']}") diff --git a/frigate/events/types.py b/frigate/events/types.py new file mode 100644 index 0000000..1461c1f --- /dev/null +++ b/frigate/events/types.py @@ -0,0 +1,19 @@ +"""Types for event management.""" + +from enum import Enum + + +class EventTypeEnum(str, Enum): + api = "api" + tracked_object = "tracked_object" + + +class EventStateEnum(str, Enum): + start = "start" + update = "update" + end = "end" + + +class RegenerateDescriptionEnum(str, Enum): + thumbnails = "thumbnails" + snapshot = "snapshot" diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py new file mode 100644 index 0000000..d005c80 --- /dev/null +++ b/frigate/ffmpeg_presets.py @@ -0,0 +1,547 @@ +"""Handles inserting and maintaining ffmpeg presets.""" + +import logging +import os +from enum import Enum +from typing import Any + +from frigate.const import ( + FFMPEG_HVC1_ARGS, + FFMPEG_HWACCEL_AMF, + FFMPEG_HWACCEL_NVIDIA, + FFMPEG_HWACCEL_RKMPP, + FFMPEG_HWACCEL_VAAPI, + FFMPEG_HWACCEL_VULKAN, + LIBAVFORMAT_VERSION_MAJOR, +) +from frigate.util.services import vainfo_hwaccel +from frigate.version import VERSION + +logger = logging.getLogger(__name__) + + +class LibvaGpuSelector: + "Automatically selects the correct libva GPU." + + _valid_gpus: list[str] | None = None + + def __get_valid_gpus(self) -> None: + """Get valid libva GPUs.""" + if not os.path.exists("/dev/dri"): + self._valid_gpus = [] + return + + if self._valid_gpus: + return + + devices = list(filter(lambda d: d.startswith("render"), os.listdir("/dev/dri"))) + + if not devices: + self._valid_gpus = ["/dev/dri/renderD128"] + return + + if len(devices) < 2: + self._valid_gpus = [f"/dev/dri/{devices[0]}"] + return + + self._valid_gpus = [] + for device in devices: + check = vainfo_hwaccel(device_name=device) + + logger.debug(f"{device} return vainfo status code: {check.returncode}") + + if check.returncode == 0: + self._valid_gpus.append(f"/dev/dri/{device}") + + def get_gpu_arg(self, preset: str, gpu: int) -> str: + if "nvidia" in preset: + return str(gpu) + + if self._valid_gpus is None: + self.__get_valid_gpus() + + if not self._valid_gpus: + return "" + + if gpu < len(self._valid_gpus): + return self._valid_gpus[gpu] + else: + logger.warning(f"Invalid GPU index {gpu}, using first valid GPU") + return self._valid_gpus[0] + + +FPS_VFR_PARAM = "-fps_mode vfr" if LIBAVFORMAT_VERSION_MAJOR >= 59 else "-vsync 2" +TIMEOUT_PARAM = "-timeout" if LIBAVFORMAT_VERSION_MAJOR >= 59 else "-stimeout" + +_gpu_selector = LibvaGpuSelector() +_user_agent_args = [ + "-user_agent", + f"FFmpeg Frigate/{VERSION}", +] + +# Presets for FFMPEG Stream Decoding (detect role) + +PRESETS_HW_ACCEL_DECODE = { + "preset-rpi-64-h264": "-c:v:1 h264_v4l2m2m", + "preset-rpi-64-h265": "-c:v:1 hevc_v4l2m2m", + FFMPEG_HWACCEL_VAAPI: "-hwaccel_flags allow_profile_mismatch -hwaccel vaapi -hwaccel_device {3} -hwaccel_output_format vaapi", + "preset-intel-qsv-h264": f"-hwaccel qsv -qsv_device {{3}} -hwaccel_output_format qsv -c:v h264_qsv{' -bsf:v dump_extra' if LIBAVFORMAT_VERSION_MAJOR >= 61 else ''}", # https://trac.ffmpeg.org/ticket/9766#comment:17 + "preset-intel-qsv-h265": f"-load_plugin hevc_hw -hwaccel qsv -qsv_device {{3}} -hwaccel_output_format qsv{' -bsf:v dump_extra' if LIBAVFORMAT_VERSION_MAJOR >= 61 else ''}", # https://trac.ffmpeg.org/ticket/9766#comment:17 + FFMPEG_HWACCEL_NVIDIA: "-hwaccel_device {3} -hwaccel cuda -hwaccel_output_format cuda", + "preset-jetson-h264": "-c:v h264_nvmpi -resize {1}x{2}", + "preset-jetson-h265": "-c:v hevc_nvmpi -resize {1}x{2}", + f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra": "-hwaccel rkmpp -hwaccel_output_format drm_prime", + # experimental presets + FFMPEG_HWACCEL_VULKAN: "-hwaccel vulkan -init_hw_device vulkan=gpu:0 -filter_hw_device gpu -hwaccel_output_format vulkan", + FFMPEG_HWACCEL_AMF: "-hwaccel amf -init_hw_device amf=gpu:0 -filter_hw_device gpu -hwaccel_output_format amf", +} +PRESETS_HW_ACCEL_DECODE["preset-nvidia-h264"] = PRESETS_HW_ACCEL_DECODE[ + FFMPEG_HWACCEL_NVIDIA +] +PRESETS_HW_ACCEL_DECODE["preset-nvidia-h265"] = PRESETS_HW_ACCEL_DECODE[ + FFMPEG_HWACCEL_NVIDIA +] +PRESETS_HW_ACCEL_DECODE["preset-nvidia-mjpeg"] = PRESETS_HW_ACCEL_DECODE[ + FFMPEG_HWACCEL_NVIDIA +] + +PRESETS_HW_ACCEL_DECODE[FFMPEG_HWACCEL_RKMPP] = ( + f"{PRESETS_HW_ACCEL_DECODE[f'{FFMPEG_HWACCEL_RKMPP}-no-dump_extra']}{' -bsf:v dump_extra' if LIBAVFORMAT_VERSION_MAJOR >= 61 else ''}" +) +PRESETS_HW_ACCEL_DECODE["preset-rk-h264"] = PRESETS_HW_ACCEL_DECODE[ + FFMPEG_HWACCEL_RKMPP +] +PRESETS_HW_ACCEL_DECODE["preset-rk-h265"] = PRESETS_HW_ACCEL_DECODE[ + FFMPEG_HWACCEL_RKMPP +] + +# Presets for FFMPEG Stream Scaling (detect role) + +PRESETS_HW_ACCEL_SCALE = { + "preset-rpi-64-h264": "-r {0} -vf fps={0},scale={1}:{2}", + "preset-rpi-64-h265": "-r {0} -vf fps={0},scale={1}:{2}", + FFMPEG_HWACCEL_VAAPI: "-r {0} -vf fps={0},scale_vaapi=w={1}:h={2},hwdownload,format=nv12", + "preset-intel-qsv-h264": "-r {0} -vf vpp_qsv=w={1}:h={2}:format=nv12,hwdownload,format=nv12,fps={0},format=yuv420p", + "preset-intel-qsv-h265": "-r {0} -vf vpp_qsv=w={1}:h={2}:format=nv12,hwdownload,format=nv12,fps={0},format=yuv420p", + FFMPEG_HWACCEL_NVIDIA: "-r {0} -vf fps={0},scale_cuda=w={1}:h={2},hwdownload,format=nv12", + "preset-jetson-h264": "-r {0}", # scaled in decoder + "preset-jetson-h265": "-r {0}", # scaled in decoder + FFMPEG_HWACCEL_RKMPP: "-r {0} -vf scale_rkrga=w={1}:h={2}:format=yuv420p:force_original_aspect_ratio=0,hwmap=mode=read,format=yuv420p", + "default": "-r {0} -vf fps={0},scale={1}:{2}", + # experimental presets + FFMPEG_HWACCEL_VULKAN: "-r {0} -vf fps={0},hwupload,scale_vulkan=w={1}:h={2},hwdownload", + FFMPEG_HWACCEL_AMF: "-r {0} -vf fps={0},hwupload,scale_amf=w={1}:h={2},hwdownload", +} +PRESETS_HW_ACCEL_SCALE["preset-nvidia-h264"] = PRESETS_HW_ACCEL_SCALE[ + FFMPEG_HWACCEL_NVIDIA +] +PRESETS_HW_ACCEL_SCALE["preset-nvidia-h265"] = PRESETS_HW_ACCEL_SCALE[ + FFMPEG_HWACCEL_NVIDIA +] + +PRESETS_HW_ACCEL_SCALE[f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra"] = ( + PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL_RKMPP] +) +PRESETS_HW_ACCEL_SCALE["preset-rk-h264"] = PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL_RKMPP] +PRESETS_HW_ACCEL_SCALE["preset-rk-h265"] = PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL_RKMPP] + +# Presets for FFMPEG Stream Encoding (birdseye feature) + +PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = { + "preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}", + "preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m {2}", + FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}", + "preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {2}", + "preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v main -level:v 4.1 -async_depth:v 1 {2}", + FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner {1} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {2}", + "preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}", + "preset-jetson-h265": "{0} -hide_banner {1} -c:v h264_nvmpi -profile main {2}", + FFMPEG_HWACCEL_RKMPP: "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}", + "preset-rk-h265": "{0} -hide_banner {1} -c:v hevc_rkmpp -profile:v main {2}", + FFMPEG_HWACCEL_AMF: "{0} -hide_banner {1} -c:v h264_amf -g 50 -profile:v high {2}", + "default": "{0} -hide_banner {1} -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency {2}", +} +PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-nvidia-h264"] = ( + PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_NVIDIA] +) +PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-nvidia-h265"] = ( + PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_NVIDIA] +) + +PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra"] = ( + PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_RKMPP] +) +PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-rk-h264"] = PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[ + FFMPEG_HWACCEL_RKMPP +] + +# Presets for FFMPEG Stream Encoding (timelapse feature) + +PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = { + "preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m -pix_fmt yuv420p {2}", + "preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m -pix_fmt yuv420p {2}", + FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi {2}", + "preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {2}", + "preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v hevc_qsv -profile:v main -level:v 4.1 -async_depth:v 1 {2}", + FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {1} -c:v h264_nvenc {2}", + "preset-nvidia-h265": "{0} -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {1} -c:v hevc_nvenc {2}", + "preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}", + "preset-jetson-h265": "{0} -hide_banner {1} -c:v hevc_nvmpi -profile main {2}", + FFMPEG_HWACCEL_RKMPP: "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}", + "preset-rk-h265": "{0} -hide_banner {1} -c:v hevc_rkmpp -profile:v main {2}", + FFMPEG_HWACCEL_AMF: "{0} -hide_banner {1} -c:v h264_amf -profile:v high {2}", + "default": "{0} -hide_banner {1} -c:v libx264 -preset:v ultrafast -tune:v zerolatency {2}", +} +PRESETS_HW_ACCEL_ENCODE_TIMELAPSE["preset-nvidia-h264"] = ( + PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[FFMPEG_HWACCEL_NVIDIA] +) + +PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra"] = ( + PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[FFMPEG_HWACCEL_RKMPP] +) +PRESETS_HW_ACCEL_ENCODE_TIMELAPSE["preset-rk-h264"] = PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[ + FFMPEG_HWACCEL_RKMPP +] + +# encoding of previews is only done on CPU due to comparable encode times and better quality from libx264 +PRESETS_HW_ACCEL_ENCODE_PREVIEW = { + "default": "{0} -hide_banner {1} -c:v libx264 -profile:v baseline -preset:v ultrafast {2}", +} + + +def parse_preset_hardware_acceleration_decode( + arg: Any, + fps: int, + width: int, + height: int, + gpu: int, +) -> list[str] | None: + """Return the correct preset if in preset format otherwise return None.""" + if not isinstance(arg, str): + return None + + decode = PRESETS_HW_ACCEL_DECODE.get(arg, None) + + if not decode: + return None + + gpu_arg = _gpu_selector.get_gpu_arg(arg, gpu) + return decode.format(fps, width, height, gpu_arg).split(" ") + + +def parse_preset_hardware_acceleration_scale( + arg: Any, + detect_args: list[str], + fps: int, + width: int, + height: int, +) -> list[str]: + """Return the correct scaling preset or default preset if none is set.""" + if not isinstance(arg, str) or " " in arg: + scale = PRESETS_HW_ACCEL_SCALE["default"] + else: + scale = PRESETS_HW_ACCEL_SCALE.get(arg, PRESETS_HW_ACCEL_SCALE["default"]) + + scale_args = scale.format(fps, width, height).split(" ") + scale_args.extend(detect_args) + return scale_args + + +class EncodeTypeEnum(str, Enum): + birdseye = "birdseye" + preview = "preview" + timelapse = "timelapse" + + +def parse_preset_hardware_acceleration_encode( + ffmpeg_path: str, + arg: Any, + input: str, + output: str, + type: EncodeTypeEnum = EncodeTypeEnum.birdseye, +) -> str: + """Return the correct scaling preset or default preset if none is set.""" + if type == EncodeTypeEnum.birdseye: + arg_map = PRESETS_HW_ACCEL_ENCODE_BIRDSEYE + elif type == EncodeTypeEnum.preview: + arg_map = PRESETS_HW_ACCEL_ENCODE_PREVIEW + elif type == EncodeTypeEnum.timelapse: + arg_map = PRESETS_HW_ACCEL_ENCODE_TIMELAPSE + + if not isinstance(arg, str): + return arg_map["default"].format(ffmpeg_path, input, output) + + # Not all jetsons have HW encoders, so fall back to default SW encoder if not + if arg.startswith("preset-jetson-") and not os.path.exists("/dev/nvhost-msenc"): + arg = "default" + + return arg_map.get(arg, arg_map["default"]).format( + ffmpeg_path, + input, + output, + _gpu_selector.get_gpu_arg(arg, 0), + ) + + +PRESETS_INPUT = { + "preset-http-jpeg-generic": [ + "-r", + "{}", + "-stream_loop", + "-1", + "-f", + "image2", + "-avoid_negative_ts", + "make_zero", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-http-mjpeg-generic": _user_agent_args + + [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-http-reolink": _user_agent_args + + [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "+genpts+discardcorrupt", + "-flags", + "low_delay", + "-strict", + "experimental", + "-analyzeduration", + "1000M", + "-probesize", + "1000M", + "-rw_timeout", + "10000000", + ], + "preset-rtmp-generic": [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-rw_timeout", + "10000000", + "-use_wallclock_as_timestamps", + "1", + "-f", + "live_flv", + ], + "preset-rtsp-generic": _user_agent_args + + [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "+genpts+discardcorrupt", + "-rtsp_transport", + "tcp", + TIMEOUT_PARAM, + "10000000", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-rtsp-restream": _user_agent_args + + [ + "-rtsp_transport", + "tcp", + TIMEOUT_PARAM, + "10000000", + ], + "preset-rtsp-restream-low-latency": _user_agent_args + + [ + "-rtsp_transport", + "tcp", + TIMEOUT_PARAM, + "10000000", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + ], + "preset-rtsp-udp": _user_agent_args + + [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "+genpts+discardcorrupt", + "-rtsp_transport", + "udp", + TIMEOUT_PARAM, + "10000000", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-rtsp-blue-iris": _user_agent_args + + [ + "-user_agent", + f"FFmpeg Frigate/{VERSION}", + "-avoid_negative_ts", + "make_zero", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-rtsp_transport", + "tcp", + TIMEOUT_PARAM, + "10000000", + "-use_wallclock_as_timestamps", + "1", + ], +} + + +def parse_preset_input(arg: Any, detect_fps: int) -> list[str] | None: + """Return the correct preset if in preset format otherwise return None.""" + if not isinstance(arg, str): + return None + + if arg == "preset-http-jpeg-generic": + input = PRESETS_INPUT[arg].copy() + input[1] = str(detect_fps) + return input + + return PRESETS_INPUT.get(arg, None) + + +PRESETS_RECORD_OUTPUT = { + "preset-record-generic": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c", + "copy", + "-an", + ], + "preset-record-generic-audio-aac": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "copy", + "-c:a", + "aac", + ], + "preset-record-generic-audio-copy": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c", + "copy", + ], + "preset-record-mjpeg": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "libx264", + "-an", + ], + "preset-record-jpeg": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "libx264", + "-an", + ], + "preset-record-ubiquiti": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "copy", + "-ar", + "44100", + "-c:a", + "aac", + ], +} + + +def parse_preset_output_record(arg: Any, force_record_hvc1: bool) -> list[str] | None: + """Return the correct preset if in preset format otherwise return None.""" + if not isinstance(arg, str): + return None + + preset = PRESETS_RECORD_OUTPUT.get(arg, None) + + if not preset: + return None + + if force_record_hvc1: + # Apple only supports HEVC if it is hvc1 (vs. hev1) + return preset + FFMPEG_HVC1_ARGS + + return preset diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py new file mode 100644 index 0000000..d02fb89 --- /dev/null +++ b/frigate/genai/__init__.py @@ -0,0 +1,437 @@ +"""Generative AI module for Frigate.""" + +import importlib +import json +import logging +import os +import re +import time +from collections.abc import AsyncGenerator, Callable +from typing import Any + +import numpy as np +from pydantic import ValidationError + +from frigate.config import CameraConfig, GenAIConfig, GenAIProviderEnum +from frigate.const import CLIPS_DIR +from frigate.data_processing.post.types import ReviewMetadata +from frigate.genai.manager import GenAIClientManager +from frigate.genai.prompts import ( + build_object_description_prompt, + build_review_description_prompt, + build_review_description_response_format, + build_review_summary_prompt, +) +from frigate.models import Event + +logger = logging.getLogger(__name__) + +__all__ = [ + "GenAIClient", + "GenAIClientManager", + "GenAIConfig", + "GenAIProviderEnum", + "PROVIDERS", + "load_providers", + "register_genai_provider", +] + +PROVIDERS = {} + + +def register_genai_provider(key: GenAIProviderEnum) -> Callable: + """Register a GenAI provider.""" + + def decorator(cls: type) -> type: + PROVIDERS[key] = cls + return cls + + return decorator + + +class GenAIClient: + """Generative AI client for Frigate.""" + + # Minimum seconds between re-initialization attempts when the provider was + # offline at startup + REINIT_INTERVAL = 60.0 + + def __init__( + self, + genai_config: GenAIConfig, + timeout: int = 120, + validate_model: bool = True, + ) -> None: + self.genai_config: GenAIConfig = genai_config + self.timeout = timeout + self.validate_model = validate_model + self.provider = self._init_provider() + self._last_init_attempt = time.monotonic() + + def ensure_provider(self) -> bool: + """Ensure a provider is available, retrying initialization if needed. + + Providers can fail to initialize at startup when their backing service + isn't online yet (common when both are started together). This retries + ``_init_provider`` lazily — throttled to ``REINIT_INTERVAL`` — so the + client recovers on its own once the service is reachable, without a + config reload. + + Returns True if a provider is available. + """ + if self.provider is not None: + return True + + now = time.monotonic() + if now - self._last_init_attempt < self.REINIT_INTERVAL: + return False + + self._last_init_attempt = now + self.provider = self._init_provider() + if self.provider is not None: + logger.info( + "GenAI provider %s is now available", + self.genai_config.provider, + ) + return self.provider is not None + + def generate_review_description( + self, + review_data: dict[str, Any], + thumbnails: list[bytes], + concerns: list[str], + preferred_language: str | None, + debug_save: bool, + activity_context_prompt: str, + ) -> ReviewMetadata | None: + """Generate a description for the review item activity.""" + context_prompt = build_review_description_prompt( + review_data, + thumbnails, + concerns, + preferred_language, + activity_context_prompt, + ) + + logger.debug( + f"Sending {len(thumbnails)} images to create review description on {review_data['camera']}" + ) + + if debug_save: + with open( + os.path.join( + CLIPS_DIR, "genai-requests", review_data["id"], "prompt.txt" + ), + "w", + ) as f: + f.write(context_prompt) + + response_format = build_review_description_response_format(concerns) + + response = self._send(context_prompt, thumbnails, response_format) + + if debug_save and response: + with open( + os.path.join( + CLIPS_DIR, "genai-requests", review_data["id"], "response.txt" + ), + "w", + ) as f: + f.write(response) + + if response: + clean_json = re.sub( + r"\n?```$", "", re.sub(r"^```[a-zA-Z0-9]*\n?", "", response) + ) + + try: + metadata = ReviewMetadata.model_validate_json(clean_json) + except ValidationError as ve: + # Constraint violations (length, item count, ranges) are logged + # at debug and the response is kept anyway — a slightly + # off-spec answer is still usable, and dropping the whole + # response loses the narrative content the model produced. + for err in ve.errors(): + loc = ".".join(str(p) for p in err["loc"]) or "" + logger.debug( + "Review metadata soft validation: %s — %s (input: %r)", + loc, + err["msg"], + err.get("input"), + ) + try: + raw = json.loads(clean_json) + except json.JSONDecodeError as je: + logger.error("Failed to parse review description JSON: %s", je) + return None + # observations and confidence are required on the model; fill an empty default + # if the response omitted it so attribute access stays safe. + raw.setdefault("observations", []) + raw.setdefault("confidence", 0.0) + metadata = ReviewMetadata.model_construct(**raw) + except Exception as e: + logger.error( + f"Failed to parse review description as the response did not match expected format. {e}" + ) + return None + + try: + # 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 ← separator), set to 0 + if any("←" in obj for obj in review_data["unified_objects"]): + metadata.potential_threat_level = 0 + + metadata.title = metadata.title[0].upper() + metadata.title[1:] + metadata.time = review_data["start"] + return metadata + except Exception as e: + logger.error(f"Failed to post-process review metadata: {e}") + return None + else: + logger.debug( + f"Invalid response received from GenAI provider for review description on {review_data['camera']}. Response: {response}", + ) + return None + + def generate_review_summary( + self, + start_ts: float, + end_ts: float, + events: list[dict[str, Any]], + preferred_language: str | None, + debug_save: bool, + ) -> str | None: + """Generate a summary of review item descriptions over a period of time.""" + timeline_summary_prompt = build_review_summary_prompt( + start_ts, end_ts, events, preferred_language + ) + + if debug_save: + with open( + os.path.join( + CLIPS_DIR, "genai-requests", f"{start_ts}-{end_ts}", "prompt.txt" + ), + "w", + ) as f: + f.write(timeline_summary_prompt) + + response = self._send(timeline_summary_prompt, []) + + if debug_save and response: + with open( + os.path.join( + CLIPS_DIR, "genai-requests", f"{start_ts}-{end_ts}", "response.txt" + ), + "w", + ) as f: + f.write(response) + + return response + + def generate_object_description( + self, + camera_config: CameraConfig, + thumbnails: list[bytes], + event: Event, + ) -> str | None: + """Generate a description for the frame.""" + try: + prompt = build_object_description_prompt(camera_config, event) + except KeyError as e: + logger.error(f"Invalid key in GenAI prompt: {e}") + return None + + logger.debug(f"Sending images to genai provider with prompt: {prompt}") + return self._send(prompt, thumbnails) + + def _init_provider(self) -> Any: + """Initialize the client.""" + return None + + def _send( + self, + prompt: str, + images: list[bytes], + response_format: dict | None = None, + enable_thinking: bool = False, + ) -> str | None: + """Submit a request to the provider. + + ``enable_thinking`` is honored only by providers that report + ``supports_toggleable_thinking``. Description-style callers leave it + at the default (off) since synthesis tasks don't benefit from + reasoning traces. + """ + return None + + @property + def supports_vision(self) -> bool: + """Whether the model supports vision/image input. + + Defaults to True for cloud providers. Providers that can detect + capability at runtime (e.g. llama.cpp) should override this. + """ + return True + + @property + def supports_toggleable_thinking(self) -> bool: + """Whether the configured model exposes a per-request thinking toggle.""" + return False + + def list_models(self) -> list[str]: + """Return the list of model names available from this provider. + + Providers should override this to query their backend. + """ + return [] + + def get_context_size(self) -> int: + """Get the context window size for this provider in tokens.""" + return 4096 + + def estimate_image_tokens(self, width: int, height: int) -> float: + """Estimate prompt tokens consumed by a single image of the given dimensions. + + Default heuristic: ~1 token per 1250 pixels. Providers that can measure or + know their model's exact image-token cost should override. + """ + return (width * height) / 1250 + + 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( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, + ) -> dict[str, Any]: + """ + Send chat messages to LLM with optional tool definitions. + + This method handles conversation-style interactions with the LLM, + including function calling/tool usage capabilities. + + Args: + messages: List of message dictionaries. Each message should have: + - 'role': str - One of 'user', 'assistant', 'system', or 'tool' + - 'content': str - The message content + - 'tool_call_id': Optional[str] - For tool responses, the ID of the tool call + - 'name': Optional[str] - For tool messages, the tool name + tools: Optional list of tool definitions in OpenAI-compatible format. + Each tool should have 'type': 'function' and 'function' with: + - 'name': str - Tool name + - 'description': str - Tool description + - 'parameters': dict - JSON schema for parameters + tool_choice: How the model should handle tools: + - 'auto': Model decides whether to call tools + - 'none': Model must not call tools + - 'required': Model must call at least one tool + - Or a dict specifying a specific tool to call + enable_thinking: Per-request thinking toggle. None means use the + provider default. Ignored by providers without a per-request + toggle (see `supports_toggleable_thinking`). + + Returns: + Dictionary with: + - 'content': Optional[str] - The text response from the LLM, None if tool calls + - 'reasoning': Optional[str] - The separated reasoning/thinking trace + if the model emitted one (e.g. via OpenAI-compatible + `reasoning_content`). None when the model does not surface a + trace or the provider does not parse it. + - 'tool_calls': Optional[List[Dict]] - List of tool calls if LLM wants to call tools. + Each tool call dict has: + - 'id': str - Unique identifier for this tool call + - 'name': str - Tool name to call + - 'arguments': dict - Arguments for the tool call (parsed JSON) + - 'finish_reason': str - Reason generation stopped: + - 'stop': Normal completion + - 'tool_calls': LLM wants to call tools + - 'length': Hit token limit + - 'error': An error occurred + + Streaming counterpart `chat_with_tools_stream` yields + ``(kind, value)`` tuples where ``kind`` is one of: + - 'content_delta': value is a string fragment of the answer + - 'reasoning_delta': value is a string fragment of the reasoning + trace (emitted before content for thinking models) + - 'stats': value is a usage stats dict + - 'message': value is the final dict shape described above + + Raises: + NotImplementedError: If the provider doesn't implement this method. + """ + # Base implementation - each provider should override this + logger.warning( + f"{self.__class__.__name__} does not support chat_with_tools. " + "This method should be overridden by the provider implementation." + ) + return { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + } + + async def chat_with_tools_stream( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, + ) -> AsyncGenerator[tuple[str, Any], None]: + """Streaming counterpart to `chat_with_tools`. + + Yields ``(kind, value)`` tuples where ``kind`` is one of: + - 'content_delta': value is a string fragment of the answer + - 'reasoning_delta': value is a string fragment of the reasoning + trace (emitted before content for thinking models) + - 'stats': value is a usage stats dict + - 'message': value is the final dict shape described in + `chat_with_tools` + + Argument semantics — including ``enable_thinking`` — match + `chat_with_tools`. Providers that don't support streaming should + override this and yield an error 'message' event. + """ + logger.warning( + f"{self.__class__.__name__} does not support chat_with_tools_stream. " + "This method should be overridden by the provider implementation." + ) + yield ( + "message", + { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) + + +def load_providers() -> None: + plugins_dir = os.path.join(os.path.dirname(__file__), "plugins") + for filename in os.listdir(plugins_dir): + if filename.endswith(".py") and filename != "__init__.py": + module_name = f"frigate.genai.plugins.{filename[:-3]}" + importlib.import_module(module_name) diff --git a/frigate/genai/manager.py b/frigate/genai/manager.py new file mode 100644 index 0000000..594e3e4 --- /dev/null +++ b/frigate/genai/manager.py @@ -0,0 +1,125 @@ +"""GenAI client manager for Frigate. + +Manages GenAI provider clients from Frigate config. Clients are created lazily +on first access so that providers whose roles are never used (e.g. chat when +no chat feature is active) are never initialized. +""" + +import logging +from typing import TYPE_CHECKING, Any + +from frigate.config import FrigateConfig +from frigate.config.camera.genai import GenAIConfig, GenAIRoleEnum + +if TYPE_CHECKING: + from frigate.genai import GenAIClient + +logger = logging.getLogger(__name__) + + +class GenAIClientManager: + """Manages GenAI provider clients from Frigate config.""" + + def __init__(self, config: FrigateConfig) -> None: + self._configs: dict[str, GenAIConfig] = {} + self._role_map: dict[GenAIRoleEnum, str] = {} + self._clients: dict[str, GenAIClient] = {} + self.update_config(config) + + def update_config(self, config: FrigateConfig) -> None: + """Store provider configs and build the role→name mapping. + + Called from __init__ and can be called again when config is reloaded. + Clients are not created here; they are instantiated lazily on first + access via a role property or list_models(). + """ + from frigate.genai import PROVIDERS, load_providers + + self._configs = {} + self._role_map = {} + self._clients = {} + + if not config.genai: + return + + load_providers() + + for name, genai_cfg in config.genai.items(): + if not genai_cfg.provider: + continue + if genai_cfg.provider not in PROVIDERS: + logger.warning( + "Unknown GenAI provider %s in config, skipping.", + genai_cfg.provider, + ) + continue + + self._configs[name] = genai_cfg + + for role in genai_cfg.roles: + self._role_map[role] = name + + def _get_client(self, name: str) -> "GenAIClient | None": + """Return the client for *name*, creating it on first access.""" + if name in self._clients: + client = self._clients[name] + client.ensure_provider() + return client + + from frigate.genai import PROVIDERS + + genai_cfg = self._configs.get(name) + if not genai_cfg: + return None + + if not genai_cfg.provider: + return None + + provider_cls = PROVIDERS.get(genai_cfg.provider) + if not provider_cls: + return None + + try: + client = provider_cls(genai_cfg) + except Exception as e: + logger.exception( + "Failed to create GenAI client for provider %s: %s", + genai_cfg.provider, + e, + ) + return None + + self._clients[name] = client + return client + + @property + def chat_client(self) -> "GenAIClient | None": + """Client configured for the chat role (e.g. chat with function calling).""" + name = self._role_map.get(GenAIRoleEnum.chat) + return self._get_client(name) if name else None + + @property + def description_client(self) -> "GenAIClient | None": + """Client configured for the descriptions role (e.g. review descriptions, object descriptions).""" + name = self._role_map.get(GenAIRoleEnum.descriptions) + return self._get_client(name) if name else None + + @property + def embeddings_client(self) -> "GenAIClient | None": + """Client configured for the embeddings role.""" + name = self._role_map.get(GenAIRoleEnum.embeddings) + return self._get_client(name) if name else None + + def list_models(self) -> dict[str, dict[str, Any]]: + """Return per-entry model lists and capabilities, keyed by config entry name.""" + result: dict[str, dict[str, Any]] = {} + for name, genai_cfg in self._configs.items(): + client = self._get_client(name) + if not client: + continue + result[name] = { + "models": client.list_models(), + "roles": [r.value for r in genai_cfg.roles], + "supports_toggleable_thinking": client.supports_toggleable_thinking, + } + return result diff --git a/frigate/genai/plugins/__init__.py b/frigate/genai/plugins/__init__.py new file mode 100644 index 0000000..e6d6607 --- /dev/null +++ b/frigate/genai/plugins/__init__.py @@ -0,0 +1 @@ +"""GenAI provider plugins.""" diff --git a/frigate/genai/plugins/azure-openai.py b/frigate/genai/plugins/azure-openai.py new file mode 100644 index 0000000..3599eb0 --- /dev/null +++ b/frigate/genai/plugins/azure-openai.py @@ -0,0 +1,53 @@ +"""Azure OpenAI Provider for Frigate AI. + +Azure OpenAI exposes the same chat completions API as OpenAI once the +client is constructed, so this provider inherits all transport, streaming, +reasoning, and tool-calling logic from :class:`OpenAIClient` and only +overrides what is genuinely Azure-specific: + +- Client construction: parses ``api-version`` out of the configured + ``base_url`` query string and instantiates :class:`openai.AzureOpenAI` + with ``azure_endpoint`` instead of ``base_url``. Raises if the URL is + malformed; :class:`GenAIClientManager` catches the exception and + disables the provider. +- Context size: Azure does not expose a per-model ``max_model_len`` field + reliably, so we keep the historical 128K default rather than the + model-name heuristic used by OpenAI. +""" + +import logging +from urllib.parse import parse_qs, urlparse + +from openai import AzureOpenAI + +from frigate.config import GenAIProviderEnum +from frigate.genai import register_genai_provider +from frigate.genai.plugins.openai import OpenAIClient + +logger = logging.getLogger(__name__) + + +@register_genai_provider(GenAIProviderEnum.azure_openai) +class AzureOpenAIClient(OpenAIClient): + """Generative AI client for Frigate using Azure OpenAI.""" + + def _init_provider(self) -> AzureOpenAI: + """Initialize the AzureOpenAI client from the configured base_url.""" + parsed_url = urlparse(self.genai_config.base_url or "") + query_params = parse_qs(parsed_url.query) + api_version = query_params.get("api-version", [None])[0] + + if not api_version: + raise ValueError("Azure OpenAI base_url is missing api-version.") + + azure_endpoint = f"{parsed_url.scheme}://{parsed_url.netloc}/" + + return AzureOpenAI( + api_key=self.genai_config.api_key, + api_version=api_version, + azure_endpoint=azure_endpoint, + ) + + def get_context_size(self) -> int: + """Azure does not reliably surface per-model context size; use 128K.""" + return 128000 diff --git a/frigate/genai/plugins/gemini.py b/frigate/genai/plugins/gemini.py new file mode 100644 index 0000000..915a124 --- /dev/null +++ b/frigate/genai/plugins/gemini.py @@ -0,0 +1,711 @@ +"""Gemini Provider for Frigate AI.""" + +import base64 +import binascii +import json +import logging +from collections.abc import AsyncGenerator +from typing import Any + +from google import genai +from google.genai import errors, types +from google.genai.types import FunctionCallingConfigMode + +from frigate.config import GenAIProviderEnum +from frigate.genai import GenAIClient, register_genai_provider + +logger = logging.getLogger(__name__) + + +def _decode_thought_signature(value: Any) -> bytes | None: + """Decode a base64-encoded thought_signature carried across conversation turns.""" + if not value: + return None + if isinstance(value, bytes): + return value + if isinstance(value, str): + try: + return base64.b64decode(value) + except (binascii.Error, ValueError): + return None + return None + + +def _encode_thought_signature(signature: bytes | None) -> str | None: + """Encode bytes thought_signature as base64 so it survives JSON-friendly transport.""" + if not signature: + return None + return base64.b64encode(signature).decode("ascii") + + +def _stats_from_gemini_usage(usage: Any) -> dict[str, Any] | None: + """Build a stats dict from a Gemini usage_metadata object.""" + prompt_tokens = getattr(usage, "prompt_token_count", None) + completion_tokens = getattr(usage, "candidates_token_count", None) + if prompt_tokens is None and completion_tokens is None: + return None + stats: dict[str, Any] = {} + if isinstance(prompt_tokens, int): + stats["prompt_tokens"] = prompt_tokens + if isinstance(completion_tokens, int): + stats["completion_tokens"] = completion_tokens + return stats or None + + +@register_genai_provider(GenAIProviderEnum.gemini) +class GeminiClient(GenAIClient): + """Generative AI client for Frigate using Gemini.""" + + provider: genai.Client + + def _init_provider(self) -> genai.Client: + """Initialize the client.""" + # Merge provider_options into HttpOptions + http_options_dict: dict[str, Any] = { + "timeout": int(self.timeout * 1000), # requires milliseconds + "retry_options": types.HttpRetryOptions( + attempts=3, + initial_delay=1.0, + max_delay=60.0, + exp_base=2.0, + jitter=1.0, + http_status_codes=[429, 500, 502, 503, 504], + ), + } + + if isinstance(self.genai_config.provider_options, dict): + http_options_dict.update(self.genai_config.provider_options) + + return genai.Client( + api_key=self.genai_config.api_key, + http_options=types.HttpOptions(**http_options_dict), + ) + + def _send( + self, + prompt: str, + images: list[bytes], + response_format: dict | None = None, + enable_thinking: bool = False, + ) -> str | None: + """Submit a request to Gemini.""" + contents = [prompt] + [ + types.Part.from_bytes(data=img, mime_type="image/jpeg") for img in images + ] + try: + # Merge runtime_options into generation_config if provided + generation_config_dict: dict[str, Any] = {"candidate_count": 1} + 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( + model=self.genai_config.model, + contents=contents, # type: ignore[arg-type] + config=types.GenerateContentConfig( + **generation_config_dict, + ), + ) + except errors.APIError as e: + logger.warning("Gemini returned an error: %s", str(e)) + return None + except Exception as e: + logger.warning("An unexpected error occurred with Gemini: %s", str(e)) + return None + + try: + if response.text is None: + return None + description = response.text.strip() + except (ValueError, AttributeError): + # No description was generated + return None + return description + + def list_models(self) -> list[str]: + """Return available model names from Gemini.""" + try: + return sorted(m.name or "" for m in self.provider.models.list()) + except Exception as e: + logger.warning("Failed to list Gemini models: %s", e) + return [] + + def get_context_size(self) -> int: + """Get the context window size for Gemini.""" + # Gemini Pro Vision has a 1M token context window + return 1000000 + + def chat_with_tools( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, + ) -> dict[str, Any]: + """ + Send chat messages to Gemini with optional tool definitions. + + Implements function calling/tool usage for Gemini models. Thinking is + configured at the model level for Gemini, so ``enable_thinking`` is + accepted for interface parity and ignored. + """ + try: + # Convert messages to Gemini format + gemini_messages: list[types.Content] = [] + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + + # Map roles to Gemini format + if role == "system": + # Gemini doesn't have system role, prepend to first user message + if ( + gemini_messages + and gemini_messages[0].role == "user" + and gemini_messages[0].parts + ): + gemini_messages[0].parts[ + 0 + ].text = f"{content}\n\n{gemini_messages[0].parts[0].text}" + else: + gemini_messages.append( + types.Content( + role="user", parts=[types.Part.from_text(text=content)] + ) + ) + elif role == "assistant": + parts: list[types.Part] = [] + if content: + parts.append(types.Part.from_text(text=content)) + for tc in msg.get("tool_calls") or []: + func = tc.get("function") or {} + tc_name = func.get("name") or "" + tc_args: Any = func.get("arguments") + if isinstance(tc_args, str): + try: + tc_args = json.loads(tc_args) + except (json.JSONDecodeError, TypeError): + tc_args = {} + if not isinstance(tc_args, dict): + tc_args = {} + if tc_name: + fc_part = types.Part.from_function_call( + name=tc_name, args=tc_args + ) + # Thinking-capable Gemini models require the original + # thought_signature to be echoed back on functionCall + # parts after a tool response, or the next request + # fails with INVALID_ARGUMENT. + sig = _decode_thought_signature(tc.get("thought_signature")) + if sig: + fc_part.thought_signature = sig + parts.append(fc_part) + if not parts: + parts.append(types.Part.from_text(text=" ")) + gemini_messages.append(types.Content(role="model", parts=parts)) + elif role == "tool": + # Handle tool response + response_payload = ( + content if isinstance(content, dict) else {"result": content} + ) + gemini_messages.append( + types.Content( + role="function", + parts=[ + types.Part.from_function_response( + name=msg.get("name") + or msg.get("tool_call_id") + or "", + response=response_payload, + ) + ], + ) + ) + else: # user + gemini_messages.append( + types.Content( + role="user", parts=[types.Part.from_text(text=content)] + ) + ) + + # Convert tools to Gemini format + gemini_tools = None + if tools: + gemini_tools = [] + for tool in tools: + if tool.get("type") == "function": + func = tool.get("function", {}) + gemini_tools.append( + types.Tool( + function_declarations=[ + types.FunctionDeclaration( + name=func.get("name", ""), + description=func.get("description", ""), + parameters=func.get("parameters", {}), + ) + ] + ) + ) + + # Configure tool choice + tool_config = None + if tool_choice: + if tool_choice == "none": + tool_config = types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.NONE + ) + ) + elif tool_choice == "auto": + tool_config = types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.AUTO + ) + ) + elif tool_choice == "required": + tool_config = types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.ANY + ) + ) + + # Build request config + config_params: dict[str, Any] = {"candidate_count": 1} + + if gemini_tools: + config_params["tools"] = gemini_tools + + if tool_config: + config_params["tool_config"] = tool_config + + # Ask thinking-capable models (Gemini 2.5+) to include their + # reasoning trace as separate `thought` parts so we can surface + # it on the reasoning channel. Older models ignore this field. + config_params["thinking_config"] = types.ThinkingConfig( + include_thoughts=True + ) + + # Merge runtime_options + if isinstance(self.genai_config.runtime_options, dict): + config_params.update(self.genai_config.runtime_options) + + response = self.provider.models.generate_content( + model=self.genai_config.model, + contents=gemini_messages, # type: ignore[arg-type] + config=types.GenerateContentConfig(**config_params), + ) + + # Check if response is valid + if not response or not response.candidates: + return { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + } + + candidate = response.candidates[0] + content = None + reasoning_parts: list[str] = [] + tool_calls = None + + # Extract content, reasoning, and tool calls from response + if candidate.content and candidate.content.parts: + for part in candidate.content.parts: + if part.text: + if getattr(part, "thought", False): + reasoning_parts.append(part.text) + else: + content = part.text.strip() + elif part.function_call: + # Handle function call + if tool_calls is None: + tool_calls = [] + + try: + arguments = ( + dict(part.function_call.args) + if part.function_call.args + else {} + ) + except Exception: + arguments = {} + + tool_calls.append( + { + "id": part.function_call.name or "", + "name": part.function_call.name or "", + "arguments": arguments, + "thought_signature": _encode_thought_signature( + getattr(part, "thought_signature", None) + ), + } + ) + + reasoning = "".join(reasoning_parts).strip() or None + + # Determine finish reason + finish_reason = "error" + if hasattr(candidate, "finish_reason") and candidate.finish_reason: + from google.genai.types import FinishReason + + if candidate.finish_reason == FinishReason.STOP: + finish_reason = "stop" + elif candidate.finish_reason == FinishReason.MAX_TOKENS: + finish_reason = "length" + elif candidate.finish_reason in [ + FinishReason.SAFETY, + FinishReason.RECITATION, + ]: + finish_reason = "error" + elif tool_calls: + finish_reason = "tool_calls" + elif content: + finish_reason = "stop" + elif tool_calls: + finish_reason = "tool_calls" + elif content: + finish_reason = "stop" + + return { + "content": content, + "reasoning": reasoning, + "tool_calls": tool_calls, + "finish_reason": finish_reason, + } + + except errors.APIError as e: + logger.warning("Gemini API error during chat_with_tools: %s", str(e)) + return { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + } + except Exception as e: + logger.warning( + "Gemini returned an error during chat_with_tools: %s", str(e) + ) + return { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + } + + async def chat_with_tools_stream( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, + ) -> AsyncGenerator[tuple[str, Any], None]: + """ + Stream chat with tools; yields content deltas then final message. + + Implements streaming function calling/tool usage for Gemini models. + ``enable_thinking`` is accepted for interface parity; Gemini configures + thinking at the model level, so it is ignored here. + """ + try: + # Convert messages to Gemini format + gemini_messages: list[types.Content] = [] + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + + # Map roles to Gemini format + if role == "system": + # Gemini doesn't have system role, prepend to first user message + if ( + gemini_messages + and gemini_messages[0].role == "user" + and gemini_messages[0].parts + ): + gemini_messages[0].parts[ + 0 + ].text = f"{content}\n\n{gemini_messages[0].parts[0].text}" + else: + gemini_messages.append( + types.Content( + role="user", parts=[types.Part.from_text(text=content)] + ) + ) + elif role == "assistant": + parts: list[types.Part] = [] + if content: + parts.append(types.Part.from_text(text=content)) + for tc in msg.get("tool_calls") or []: + func = tc.get("function") or {} + tc_name = func.get("name") or "" + tc_args: Any = func.get("arguments") + if isinstance(tc_args, str): + try: + tc_args = json.loads(tc_args) + except (json.JSONDecodeError, TypeError): + tc_args = {} + if not isinstance(tc_args, dict): + tc_args = {} + if tc_name: + fc_part = types.Part.from_function_call( + name=tc_name, args=tc_args + ) + # Thinking-capable Gemini models require the original + # thought_signature to be echoed back on functionCall + # parts after a tool response, or the next request + # fails with INVALID_ARGUMENT. + sig = _decode_thought_signature(tc.get("thought_signature")) + if sig: + fc_part.thought_signature = sig + parts.append(fc_part) + if not parts: + parts.append(types.Part.from_text(text=" ")) + gemini_messages.append(types.Content(role="model", parts=parts)) + elif role == "tool": + # Handle tool response + response_payload = ( + content if isinstance(content, dict) else {"result": content} + ) + gemini_messages.append( + types.Content( + role="function", + parts=[ + types.Part.from_function_response( + name=msg.get("name") + or msg.get("tool_call_id") + or "", + response=response_payload, + ) + ], + ) + ) + else: # user + gemini_messages.append( + types.Content( + role="user", parts=[types.Part.from_text(text=content)] + ) + ) + + # Convert tools to Gemini format + gemini_tools = None + if tools: + gemini_tools = [] + for tool in tools: + if tool.get("type") == "function": + func = tool.get("function", {}) + gemini_tools.append( + types.Tool( + function_declarations=[ + types.FunctionDeclaration( + name=func.get("name", ""), + description=func.get("description", ""), + parameters=func.get("parameters", {}), + ) + ] + ) + ) + + # Configure tool choice + tool_config = None + if tool_choice: + if tool_choice == "none": + tool_config = types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.NONE + ) + ) + elif tool_choice == "auto": + tool_config = types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.AUTO + ) + ) + elif tool_choice == "required": + tool_config = types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=FunctionCallingConfigMode.ANY + ) + ) + + # Build request config + config_params: dict[str, Any] = {"candidate_count": 1} + + if gemini_tools: + config_params["tools"] = gemini_tools + + if tool_config: + config_params["tool_config"] = tool_config + + # Ask thinking-capable models to include their reasoning trace + # as separate `thought` parts (Gemini 2.5+; ignored elsewhere). + config_params["thinking_config"] = types.ThinkingConfig( + include_thoughts=True + ) + + # Merge runtime_options + if isinstance(self.genai_config.runtime_options, dict): + config_params.update(self.genai_config.runtime_options) + + # Use streaming API + content_parts: list[str] = [] + reasoning_parts: list[str] = [] + tool_calls_by_index: dict[int, dict[str, Any]] = {} + finish_reason = "stop" + usage_stats: dict[str, Any] | None = None + + stream = await self.provider.aio.models.generate_content_stream( + model=self.genai_config.model, + contents=gemini_messages, # type: ignore[arg-type] + config=types.GenerateContentConfig(**config_params), + ) + + async for chunk in stream: + chunk_usage = getattr(chunk, "usage_metadata", None) + if chunk_usage is not None: + maybe_stats = _stats_from_gemini_usage(chunk_usage) + if maybe_stats is not None: + usage_stats = maybe_stats + + if not chunk or not chunk.candidates: + continue + + candidate = chunk.candidates[0] + + # Check for finish reason + if hasattr(candidate, "finish_reason") and candidate.finish_reason: + from google.genai.types import FinishReason + + if candidate.finish_reason == FinishReason.STOP: + finish_reason = "stop" + elif candidate.finish_reason == FinishReason.MAX_TOKENS: + finish_reason = "length" + elif candidate.finish_reason in [ + FinishReason.SAFETY, + FinishReason.RECITATION, + ]: + finish_reason = "error" + + # Extract content, reasoning, and tool calls from chunk + if candidate.content and candidate.content.parts: + for part in candidate.content.parts: + if part.text: + if getattr(part, "thought", False): + reasoning_parts.append(part.text) + yield ("reasoning_delta", part.text) + else: + content_parts.append(part.text) + yield ("content_delta", part.text) + elif part.function_call: + # Handle function call + try: + arguments = ( + dict(part.function_call.args) + if part.function_call.args + else {} + ) + except Exception: + arguments = {} + + # Store tool call + tool_call_id = part.function_call.name or "" + tool_call_name = part.function_call.name or "" + + # Check if we already have this tool call + found_index = None + for idx, tc in tool_calls_by_index.items(): + if tc["name"] == tool_call_name: + found_index = idx + break + + if found_index is None: + found_index = len(tool_calls_by_index) + tool_calls_by_index[found_index] = { + "id": tool_call_id, + "name": tool_call_name, + "arguments": "", + "thought_signature": None, + } + + # Accumulate arguments + if arguments: + tool_calls_by_index[found_index]["arguments"] += ( + json.dumps(arguments) + if isinstance(arguments, dict) + else str(arguments) + ) + + # Capture latest thought_signature for this call + chunk_sig = getattr(part, "thought_signature", None) + if chunk_sig: + tool_calls_by_index[found_index][ + "thought_signature" + ] = chunk_sig + + # Build final message + full_content = "".join(content_parts).strip() or None + full_reasoning = "".join(reasoning_parts).strip() or None + + # Convert tool calls to list format + tool_calls_list = None + if tool_calls_by_index: + tool_calls_list = [] + for tc in tool_calls_by_index.values(): + try: + # Try to parse accumulated arguments as JSON + parsed_args = json.loads(tc["arguments"]) + except (json.JSONDecodeError, Exception): + parsed_args = tc["arguments"] + + tool_calls_list.append( + { + "id": tc["id"], + "name": tc["name"], + "arguments": parsed_args, + "thought_signature": _encode_thought_signature( + tc.get("thought_signature") + ), + } + ) + finish_reason = "tool_calls" + + if usage_stats is not None: + yield ("stats", usage_stats) + + yield ( + "message", + { + "content": full_content, + "reasoning": full_reasoning, + "tool_calls": tool_calls_list, + "finish_reason": finish_reason, + }, + ) + + except errors.APIError as e: + logger.warning("Gemini API error during streaming: %s", str(e)) + yield ( + "message", + { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) + except Exception as e: + logger.warning( + "Gemini returned an error during chat_with_tools_stream: %s", str(e) + ) + yield ( + "message", + { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) diff --git a/frigate/genai/plugins/llama_cpp.py b/frigate/genai/plugins/llama_cpp.py new file mode 100644 index 0000000..f4bf437 --- /dev/null +++ b/frigate/genai/plugins/llama_cpp.py @@ -0,0 +1,959 @@ +"""llama.cpp Provider for Frigate AI.""" + +import base64 +import io +import json +import logging +from collections.abc import AsyncGenerator +from typing import Any, cast + +import httpx +import numpy as np +import requests +from PIL import Image + +from frigate.config import GenAIProviderEnum +from frigate.genai import GenAIClient, register_genai_provider +from frigate.genai.utils import parse_tool_calls_from_message + +logger = logging.getLogger(__name__) + + +def _stats_from_llama_cpp_chunk(data: dict[str, Any]) -> dict[str, Any] | None: + """Build a stats dict from a llama.cpp streaming chunk. + + Final-chunk `usage` carries authoritative token counts. Per-chunk + `timings` (enabled via timings_per_token) carries the running token + counts (prompt_n, predicted_n) and generation rate, so live updates + work mid-stream. + """ + usage = data.get("usage") or {} + timings = data.get("timings") or {} + prompt_tokens = usage.get("prompt_tokens") + completion_tokens = usage.get("completion_tokens") + predicted_ms = timings.get("predicted_ms") + tps = timings.get("predicted_per_second") + stats: dict[str, Any] = {} + + if not isinstance(prompt_tokens, int): + prompt_n = timings.get("prompt_n") + + if isinstance(prompt_n, int): + prompt_tokens = prompt_n + + if not isinstance(completion_tokens, int): + predicted_n = timings.get("predicted_n") + + if isinstance(predicted_n, int): + completion_tokens = predicted_n + + if not isinstance(prompt_tokens, int) and not isinstance(completion_tokens, int): + return None + + if isinstance(prompt_tokens, int): + stats["prompt_tokens"] = prompt_tokens + + if isinstance(completion_tokens, int): + stats["completion_tokens"] = completion_tokens + + if isinstance(predicted_ms, (int, float)) and predicted_ms > 0: + stats["completion_duration_ms"] = float(predicted_ms) + + if isinstance(tps, (int, float)) and tps > 0: + stats["tokens_per_second"] = float(tps) + + return stats or None + + +def _parse_launch_arg(args: list[str], flag: str) -> str | None: + """Return the value following `flag` in a positional argv list, or None.""" + try: + idx = args.index(flag) + except ValueError: + return None + if idx + 1 >= len(args): + return None + return args[idx + 1] + + +def _fetch_llama_props(base_url: str, model: str) -> dict[str, Any]: + """Fetch /props from a llama.cpp server, with llama-swap fallback. + + Raises the underlying RequestException if both endpoints fail; callers + decide how to surface the failure. + """ + try: + response = requests.get( + f"{base_url}/props", + params={"model": model}, + timeout=10, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + except Exception: + response = requests.get( + f"{base_url}/upstream/{model}/props", + timeout=10, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + +def _to_jpeg(img_bytes: bytes) -> bytes | None: + """Convert image bytes to JPEG. llama.cpp/STB does not support WebP.""" + try: + img = Image.open(io.BytesIO(img_bytes)) + if img.mode != "RGB": + img = img.convert("RGB") # type: ignore[assignment] + 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) +class LlamaCppClient(GenAIClient): + """Generative AI client for Frigate using llama.cpp server.""" + + provider: str | None # base_url + provider_options: dict[str, Any] + _context_size: int | None + _supports_vision: bool + _supports_audio: bool + _supports_tools: bool + _supports_reasoning: bool + _image_token_cache: dict[tuple[int, int], int] + _text_baseline_tokens: int | None + _media_marker: str + + def _init_provider(self) -> str | None: + """Initialize the client and query model metadata from the server.""" + self.provider_options = { + **self.genai_config.provider_options, + } + self._context_size = None + self._supports_vision = False + self._supports_audio = False + self._supports_tools = False + self._supports_reasoning = False + self._image_token_cache = {} + self._text_baseline_tokens = None + self._media_marker = "<__media__>" + + base_url = ( + self.genai_config.base_url.rstrip("/") + if self.genai_config.base_url + else None + ) + + if base_url is None: + return None + else: + base_url = base_url.replace("/v1", "") # Strip /v1 if included in base_url + + if not self.validate_model: + # Probe path + return base_url + + configured_model = self.genai_config.model + info = self._get_model_info(base_url, configured_model) + + if info is None: + return None + + self._context_size = info["context_size"] + self._supports_vision = info["supports_vision"] + self._supports_audio = info["supports_audio"] + self._supports_tools = info["supports_tools"] + self._supports_reasoning = info["supports_reasoning"] + self._media_marker = info["media_marker"] + + logger.info( + "llama.cpp model '%s' initialized — context: %s, vision: %s, audio: %s, tools: %s, reasoning: %s", + configured_model, + self._context_size or "unknown", + self._supports_vision, + self._supports_audio, + self._supports_tools, + self._supports_reasoning, + ) + + return base_url + + def _get_model_info( + self, base_url: str, configured_model: str + ) -> dict[str, Any] | None: + """Resolve model metadata from /v1/models with /props fallback. + + Returns a dict of capability fields, or None if the server's model + registry was reachable and reported the configured model as missing. + A reachable-but-unparseable /v1/models is treated as soft-pass and + falls through to /props, matching prior behavior. + + After ggml-org/llama.cpp#22952, /v1/models exposes per-model + `architecture.input_modalities` (text/image/audio) — the primary + source. When proxied through llama-swap, the same entry carries + `status.args` (server launch argv) and, for the loaded model, + `meta.n_ctx`. /props remains the only source for `media_marker`, + which the server randomizes per startup unless LLAMA_MEDIA_MARKER + is set. + """ + info: dict[str, Any] = { + "context_size": None, + "supports_vision": False, + "supports_audio": False, + "supports_tools": False, + "supports_reasoning": False, + "media_marker": "<__media__>", + } + + model_entry: dict[str, Any] | None = None + try: + response = requests.get(f"{base_url}/v1/models", timeout=10) + response.raise_for_status() + models_data = response.json() + + for model in models_data.get("data", []): + model_ids = {model.get("id")} + for alias in model.get("aliases", []): + model_ids.add(alias) + if configured_model in model_ids: + model_entry = model + break + + if model_entry is None: + available = [] + for m in models_data.get("data", []): + available.append(m.get("id", "unknown")) + for alias in m.get("aliases", []): + available.append(alias) + logger.error( + "Model '%s' not found on llama.cpp server. Available models: %s", + configured_model, + available, + ) + return None + except Exception as e: + logger.warning( + "Failed to query llama.cpp /v1/models endpoint: %s. " + "Model validation skipped.", + e, + ) + + if model_entry is not None: + architecture = model_entry.get("architecture") or {} + input_modalities = architecture.get("input_modalities") or [] + + if isinstance(input_modalities, list): + info["supports_vision"] = "image" in input_modalities + info["supports_audio"] = "audio" in input_modalities + + status = model_entry.get("status") or {} + launch_args = status.get("args") if isinstance(status, dict) else None + if not isinstance(launch_args, list): + launch_args = [] + + meta = model_entry.get("meta") if isinstance(model_entry, dict) else None + n_ctx = meta.get("n_ctx") if isinstance(meta, dict) else None + + if not n_ctx: + n_ctx = _parse_launch_arg(launch_args, "--ctx-size") + + if n_ctx: + try: + info["context_size"] = int(n_ctx) + except (TypeError, ValueError): + pass + + # Tool calling on llama-server requires --jinja. + if "--jinja" in launch_args: + info["supports_tools"] = True + + try: + props = _fetch_llama_props(base_url, configured_model) + + if info["context_size"] is None: + default_settings = props.get("default_generation_settings", {}) + n_ctx = default_settings.get("n_ctx") + if n_ctx: + info["context_size"] = int(n_ctx) + + if not (info["supports_vision"] or info["supports_audio"]): + modalities = props.get("modalities", {}) + info["supports_vision"] = bool(modalities.get("vision", False)) + info["supports_audio"] = bool(modalities.get("audio", False)) + + chat_caps = props.get("chat_template_caps") or {} + + if not info["supports_tools"]: + info["supports_tools"] = bool(chat_caps.get("supports_tools", False)) + + # llama.cpp does not advertise per-template reasoning support, so + # detect it by looking for the `enable_thinking` toggle variable + # in the Jinja chat template itself. + chat_template = props.get("chat_template") or "" + info["supports_reasoning"] = "enable_thinking" in chat_template + + media_marker = props.get("media_marker") + if isinstance(media_marker, str) and media_marker: + info["media_marker"] = media_marker + except Exception as e: + logger.warning( + "Failed to query llama.cpp /props endpoint: %s. " + "Image embeddings may fail if the server randomized its media marker.", + e, + ) + + return info + + def _send( + self, + prompt: str, + images: list[bytes], + response_format: dict | None = None, + enable_thinking: bool = False, + ) -> str | None: + """Submit a request to llama.cpp server.""" + if self.provider is None: + logger.warning( + "llama.cpp provider has not been initialized, a description will not be generated. Check your llama.cpp configuration." + ) + return None + + try: + content = [ + { + "type": "text", + "text": prompt, + } + ] + for image in images: + encoded_image = base64.b64encode(image).decode("utf-8") + content.append( + { + "type": "image_url", + "image_url": { # type: ignore[dict-item] + "url": f"data:image/jpeg;base64,{encoded_image}", + }, + } + ) + + # Build request payload with llama.cpp native options + payload: dict[str, Any] = { + "model": self.genai_config.model, + "messages": [ + { + "role": "user", + "content": content, + }, + ], + **self.provider_options, + } + + if response_format: + payload["response_format"] = response_format + + if self.supports_toggleable_thinking: + payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking} + + response = requests.post( + f"{self.provider}/v1/chat/completions", + json=payload, + timeout=self.timeout, + ) + response.raise_for_status() + result = response.json() + + if ( + result is not None + and "choices" in result + and len(result["choices"]) > 0 + ): + choice = result["choices"][0] + if "message" in choice and "content" in choice["message"]: + return str(choice["message"]["content"].strip()) + return None + except Exception as e: + logger.warning("llama.cpp returned an error: %s", str(e)) + return None + + @property + def supports_vision(self) -> bool: + """Whether the loaded model supports vision/image input.""" + return self._supports_vision + + @property + def supports_audio(self) -> bool: + """Whether the loaded model supports audio input.""" + return self._supports_audio + + @property + def supports_tools(self) -> bool: + """Whether the loaded model supports tool/function calling.""" + return self._supports_tools + + @property + def supports_toggleable_thinking(self) -> bool: + return self._supports_reasoning + + def list_models(self) -> list[str]: + """Return available model IDs from the llama.cpp server.""" + base_url = self.provider or ( + self.genai_config.base_url.rstrip("/") + if self.genai_config.base_url + else None + ) + if base_url is None: + return [] + try: + response = requests.get(f"{base_url}/v1/models", timeout=10) + response.raise_for_status() + models = [] + for m in response.json().get("data", []): + models.append(m.get("id", "unknown")) + for alias in m.get("aliases", []): + models.append(alias) + return sorted(models) + except Exception as e: + logger.warning("Failed to list llama.cpp models: %s", e) + return [] + + def get_context_size(self) -> int: + """Get the context window size for llama.cpp. + + Resolution order: + 1. provider_options["context_size"] (user override) + 2. Value queried from llama.cpp server at init + 3. Default fallback of 4096 + """ + if "context_size" in self.provider_options: + return int(self.provider_options["context_size"]) + if self._context_size is not None: + return self._context_size + return 4096 + + def estimate_image_tokens(self, width: int, height: int) -> float: + """Probe the llama.cpp server to learn the model's image-token cost at the + requested dimensions. + + llama.cpp's image tokenization is a deterministic function of dimensions and + the loaded mmproj, so the result is cached per (width, height) for the + lifetime of the process. Falls back to the base pixel heuristic if the + server is unreachable or the response is malformed. + """ + if self.provider is None: + return super().estimate_image_tokens(width, height) + + cached = self._image_token_cache.get((width, height)) + + if cached is not None: + return cached + + try: + baseline = self._probe_baseline_tokens() + with_image = self._probe_image_prompt_tokens(width, height) + tokens = max(1, with_image - baseline) + except Exception as e: + logger.debug( + "llama.cpp image-token probe failed for %dx%d (%s); using heuristic", + width, + height, + e, + ) + return super().estimate_image_tokens(width, height) + + self._image_token_cache[(width, height)] = tokens + logger.debug( + "llama.cpp model '%s' uses ~%d tokens for %dx%d images", + self.genai_config.model, + tokens, + width, + height, + ) + return tokens + + def _probe_baseline_tokens(self) -> int: + """Return prompt_tokens for a minimal text-only request. Cached after first call.""" + if self._text_baseline_tokens is not None: + return self._text_baseline_tokens + + self._text_baseline_tokens = self._probe_prompt_tokens( + [{"type": "text", "text": "."}] + ) + return self._text_baseline_tokens + + def _probe_image_prompt_tokens(self, width: int, height: int) -> int: + """Return prompt_tokens for a single synthetic image plus minimal text.""" + img = Image.new("RGB", (width, height), (128, 128, 128)) + buf = io.BytesIO() + img.save(buf, format="JPEG", quality=60) + encoded = base64.b64encode(buf.getvalue()).decode("utf-8") + return self._probe_prompt_tokens( + [ + {"type": "text", "text": "."}, + { + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{encoded}"}, + }, + ] + ) + + def _probe_prompt_tokens(self, content: list[dict[str, Any]]) -> int: + """POST a 1-token chat completion and return reported prompt_tokens. + + Uses a generous timeout to absorb a cold model load on the first probe + when the server lazily loads models on demand (e.g. llama-swap). + """ + payload = { + "model": self.genai_config.model, + "messages": [{"role": "user", "content": content}], + "max_tokens": 1, + } + response = requests.post( + f"{self.provider}/v1/chat/completions", + json=payload, + timeout=60, + ) + response.raise_for_status() + return int(response.json()["usage"]["prompt_tokens"]) + + def _build_payload( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None, + tool_choice: str | None, + stream: bool = False, + enable_thinking: bool | None = None, + ) -> dict[str, Any]: + """Build request payload for chat completions (sync or stream).""" + openai_tool_choice = None + if tool_choice: + if tool_choice == "none": + openai_tool_choice = "none" + elif tool_choice == "auto": + openai_tool_choice = "auto" + elif tool_choice == "required": + openai_tool_choice = "required" + + payload: dict[str, Any] = { + "messages": messages, + "model": self.genai_config.model, + } + + if stream: + payload["stream"] = True + payload["stream_options"] = {"include_usage": True} + payload["timings_per_token"] = True + + if tools: + payload["tools"] = tools + + if openai_tool_choice is not None: + payload["tool_choice"] = openai_tool_choice + + if enable_thinking is not None and self._supports_reasoning: + payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking} + + provider_opts = { + k: v for k, v in self.provider_options.items() if k != "context_size" + } + payload.update(provider_opts) + payload.update(self.genai_config.runtime_options) + return payload + + def _message_from_choice(self, choice: dict[str, Any]) -> dict[str, Any]: + """Parse OpenAI-style choice into {content, reasoning, tool_calls, finish_reason}. + + llama.cpp's `--reasoning-format` puts the trace in + `message.reasoning_content` (preferred) or `message.thinking`; both + keys are accepted so different builds work without configuration. + """ + message = choice.get("message", {}) + content = message.get("content") + content = content.strip() if content else None + reasoning = message.get("reasoning_content") or message.get("thinking") + reasoning = reasoning.strip() if reasoning else None + tool_calls = parse_tool_calls_from_message(message) + finish_reason = choice.get("finish_reason") or ( + "tool_calls" if tool_calls else "stop" if content else "error" + ) + return { + "content": content, + "reasoning": reasoning, + "tool_calls": tool_calls, + "finish_reason": finish_reason, + } + + @staticmethod + def _streamed_tool_calls_to_list( + tool_calls_by_index: dict[int, dict[str, Any]], + ) -> list[dict[str, Any]] | None: + """Convert streamed tool_calls index map to list of {id, name, arguments}.""" + if not tool_calls_by_index: + return None + result = [] + for idx in sorted(tool_calls_by_index.keys()): + t = tool_calls_by_index[idx] + args_str = t.get("arguments") or "{}" + try: + arguments = json.loads(args_str) + except json.JSONDecodeError: + arguments = {} + result.append( + { + "id": t.get("id", ""), + "name": t.get("name", ""), + "arguments": arguments, + } + ) + return result if result else None + + def _refresh_media_marker(self) -> bool: + """Re-fetch /props and update the cached media marker if it changed. + + The server randomizes the marker per startup (unless LLAMA_MEDIA_MARKER + is set), so a stale marker indicates a restart. Returns True iff the + marker was updated to a new value — used to gate a one-shot retry of + a failed embeddings request. + """ + if self.provider is None: + return False + try: + props = _fetch_llama_props(self.provider, self.genai_config.model) + except Exception as e: + logger.warning("Failed to refresh llama.cpp media marker: %s", e) + return False + + marker = props.get("media_marker") + + if not isinstance(marker, str) or not marker or marker == self._media_marker: + return False + + logger.info("llama.cpp media marker changed (server restart); refreshed") + self._media_marker = marker + return True + + 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 + + encoded_images: list[str] = [] + 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_images.append(base64.b64encode(to_encode).decode("utf-8")) + + def build_content() -> list[dict[str, Any]]: + # prompt_string must contain the server's media marker placeholder + # for each image. The marker is randomized per server startup. + content: list[dict[str, Any]] = [] + for text in texts: + content.append({"prompt_string": text}) + for encoded in encoded_images: + content.append( + { + "prompt_string": f"{self._media_marker}\n", + "multimodal_data": [encoded], + } + ) + return content + + def post_embeddings() -> requests.Response: + return requests.post( + f"{self.provider}/embeddings", + json={"model": self.genai_config.model, "content": build_content()}, + timeout=self.timeout, + ) + + try: + try: + response = post_embeddings() + response.raise_for_status() + except requests.exceptions.RequestException: + # The server may have restarted with a new media marker. + # Refresh from /props; only retry if the marker actually changed. + if not encoded_images or not self._refresh_media_marker(): + raise + response = post_embeddings() + 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( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, + ) -> dict[str, Any]: + """ + Send chat messages to llama.cpp server with optional tool definitions. + + Uses the OpenAI-compatible endpoint but passes through all native llama.cpp + parameters (like slot_id, temperature, etc.) via provider_options. + """ + if self.provider is None: + logger.warning( + "llama.cpp provider has not been initialized. Check your llama.cpp configuration." + ) + return { + "content": None, + "tool_calls": None, + "finish_reason": "error", + } + try: + payload = self._build_payload( + messages, + tools, + tool_choice, + stream=False, + enable_thinking=enable_thinking, + ) + response = requests.post( + f"{self.provider}/v1/chat/completions", + json=payload, + timeout=self.timeout, + ) + response.raise_for_status() + result = response.json() + if result is None or "choices" not in result or len(result["choices"]) == 0: + return { + "content": None, + "tool_calls": None, + "finish_reason": "error", + } + return self._message_from_choice(result["choices"][0]) + except requests.exceptions.Timeout as e: + logger.warning("llama.cpp request timed out: %s", str(e)) + return { + "content": None, + "tool_calls": None, + "finish_reason": "error", + } + 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 returned an error: %s", error_detail) + return { + "content": None, + "tool_calls": None, + "finish_reason": "error", + } + except Exception as e: + logger.warning("Unexpected error in llama.cpp chat_with_tools: %s", str(e)) + return { + "content": None, + "tool_calls": None, + "finish_reason": "error", + } + + async def chat_with_tools_stream( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, + ) -> AsyncGenerator[tuple[str, Any], None]: + """Stream chat with tools via OpenAI-compatible streaming API.""" + if self.provider is None: + logger.warning( + "llama.cpp provider has not been initialized. Check your llama.cpp configuration." + ) + yield ( + "message", + { + "content": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) + return + try: + payload = self._build_payload( + messages, + tools, + tool_choice, + stream=True, + enable_thinking=enable_thinking, + ) + content_parts: list[str] = [] + reasoning_parts: list[str] = [] + tool_calls_by_index: dict[int, dict[str, Any]] = {} + finish_reason = "stop" + + async with httpx.AsyncClient(timeout=float(self.timeout)) as client: + async with client.stream( + "POST", + f"{self.provider}/v1/chat/completions", + json=payload, + ) as response: + response.raise_for_status() + async for line in response.aiter_lines(): + if not line.startswith("data: "): + continue + data_str = line[6:].strip() + if data_str == "[DONE]": + break + try: + data = json.loads(data_str) + except json.JSONDecodeError: + continue + maybe_stats = _stats_from_llama_cpp_chunk(data) + if maybe_stats is not None: + yield ("stats", maybe_stats) + choices = data.get("choices") or [] + if not choices: + continue + delta = choices[0].get("delta", {}) + if choices[0].get("finish_reason"): + finish_reason = choices[0]["finish_reason"] + # llama.cpp emits separated thinking under + # reasoning_content (preferred) or thinking before any + # content tokens arrive + reasoning_delta = delta.get("reasoning_content") or delta.get( + "thinking" + ) + if reasoning_delta: + reasoning_parts.append(reasoning_delta) + yield ("reasoning_delta", reasoning_delta) + if delta.get("content"): + content_parts.append(delta["content"]) + yield ("content_delta", delta["content"]) + for tc in delta.get("tool_calls") or []: + idx = tc.get("index", 0) + fn = tc.get("function") or {} + if idx not in tool_calls_by_index: + tool_calls_by_index[idx] = { + "id": tc.get("id", ""), + "name": tc.get("name") or fn.get("name", ""), + "arguments": "", + } + t = tool_calls_by_index[idx] + if tc.get("id"): + t["id"] = tc["id"] + name = tc.get("name") or fn.get("name") + if name: + t["name"] = name + arg = tc.get("arguments") or fn.get("arguments") + if arg is not None: + t["arguments"] += ( + arg if isinstance(arg, str) else json.dumps(arg) + ) + + full_content = "".join(content_parts).strip() or None + full_reasoning = "".join(reasoning_parts).strip() or None + tool_calls_list = self._streamed_tool_calls_to_list(tool_calls_by_index) + if tool_calls_list: + finish_reason = "tool_calls" + yield ( + "message", + { + "content": full_content, + "reasoning": full_reasoning, + "tool_calls": tool_calls_list, + "finish_reason": finish_reason, + }, + ) + except httpx.HTTPStatusError as e: + logger.warning("llama.cpp streaming HTTP error: %s", e) + yield ( + "message", + { + "content": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) + except Exception as e: + logger.warning( + "Unexpected error in llama.cpp chat_with_tools_stream: %s", str(e) + ) + yield ( + "message", + { + "content": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) diff --git a/frigate/genai/plugins/ollama.py b/frigate/genai/plugins/ollama.py new file mode 100644 index 0000000..f323b0f --- /dev/null +++ b/frigate/genai/plugins/ollama.py @@ -0,0 +1,565 @@ +"""Ollama Provider for Frigate AI.""" + +import base64 +import binascii +import json +import logging +from collections.abc import AsyncGenerator +from typing import Any + +from httpx import RemoteProtocolError, TimeoutException +from ollama import AsyncClient as OllamaAsyncClient +from ollama import Client as ApiClient +from ollama import ResponseError + +from frigate.config import GenAIProviderEnum +from frigate.genai import GenAIClient, register_genai_provider +from frigate.genai.utils import parse_tool_calls_from_message + +logger = logging.getLogger(__name__) + + +def _extract_ollama_stats(response: Any) -> dict[str, Any] | None: + """Build a stats dict from Ollama's response metadata. + + Ollama reports eval_count/eval_duration (generation) and + prompt_eval_count (context size). Durations are nanoseconds. + """ + if not response: + return None + if hasattr(response, "get"): + getter = response.get + else: + getter = lambda key: getattr(response, key, None) # noqa: E731 + + eval_count = getter("eval_count") + eval_duration_ns = getter("eval_duration") + prompt_eval_count = getter("prompt_eval_count") + if eval_count is None and prompt_eval_count is None: + return None + + stats: dict[str, Any] = {} + if isinstance(prompt_eval_count, int): + stats["prompt_tokens"] = prompt_eval_count + if isinstance(eval_count, int): + stats["completion_tokens"] = eval_count + if isinstance(eval_duration_ns, int) and eval_duration_ns > 0: + stats["completion_duration_ms"] = eval_duration_ns / 1_000_000 + if isinstance(eval_count, int) and eval_count > 0: + stats["tokens_per_second"] = eval_count / (eval_duration_ns / 1_000_000_000) + return stats or None + + +def _normalize_multimodal_content( + content: Any, +) -> tuple[str | None, list[bytes] | None]: + """Convert OpenAI-style multimodal content to Ollama's (text, images) shape. + + The chat API constructs user messages with content as a list of + ``{"type": "text"}`` and ``{"type": "image_url"}`` parts when a tool + returns a live frame. Ollama's SDK requires content to be a string and + images to be passed in a separate field, so we extract each. + """ + if not isinstance(content, list): + return content, None + + text_parts: list[str] = [] + images: list[bytes] = [] + for part in content: + if not isinstance(part, dict): + continue + part_type = part.get("type") + if part_type == "text": + text = part.get("text") + if text: + text_parts.append(str(text)) + elif part_type == "image_url": + url = (part.get("image_url") or {}).get("url", "") + if isinstance(url, str) and url.startswith("data:"): + try: + encoded = url.split(",", 1)[1] + images.append(base64.b64decode(encoded, validate=True)) + except (ValueError, IndexError, binascii.Error) as e: + logger.debug("Failed to decode multimodal image url: %s", e) + + return ("\n".join(text_parts) if text_parts else None), (images or None) + + +@register_genai_provider(GenAIProviderEnum.ollama) +class OllamaClient(GenAIClient): + """Generative AI client for Frigate using Ollama.""" + + LOCAL_OPTIMIZED_OPTIONS = { + "options": { + "temperature": 0.5, + "repeat_penalty": 1.05, + "presence_penalty": 0.3, + }, + } + + provider: ApiClient | None + provider_options: dict[str, Any] + _supports_thinking_cache: bool | None = None + + @property + def supports_toggleable_thinking(self) -> bool: + if self._supports_thinking_cache is not None: + return self._supports_thinking_cache + if self.provider is None: + return False + try: + response = self.provider.show(self.genai_config.model) + capabilities = response.get("capabilities") or [] + self._supports_thinking_cache = "thinking" in capabilities + except Exception as e: + logger.debug("Failed to query Ollama model capabilities: %s", e) + self._supports_thinking_cache = False + return self._supports_thinking_cache + + def _auth_headers(self) -> dict | None: + if self.genai_config.api_key: + return {"Authorization": "Bearer " + self.genai_config.api_key} + + return None + + def _init_provider(self) -> ApiClient | None: + """Initialize the client.""" + self.provider_options = { + **self.LOCAL_OPTIMIZED_OPTIONS, + **self.genai_config.provider_options, + } + + try: + client = ApiClient( + host=self.genai_config.base_url, + timeout=self.timeout, + headers=self._auth_headers(), + ) + if not self.validate_model: + # Probe path + return client + # ensure the model is available locally + response = client.show(self.genai_config.model) + if response.get("error"): + logger.error( + "Ollama error: %s", + response["error"], + ) + return None + return client + except Exception as e: + logger.warning("Error initializing Ollama: %s", str(e)) + return None + + @staticmethod + def _clean_schema_for_ollama(schema: dict, *, _is_properties: bool = False) -> dict: + """Strip Pydantic metadata from a JSON schema for Ollama compatibility. + + Ollama's grammar-based constrained generation works best with minimal + schemas. Pydantic adds title/description/constraint fields that can + cause the grammar generator to silently skip required fields. + + Keys inside a ``properties`` dict are actual field names and must never + be stripped, even if they collide with a metadata key name (e.g. a + model field called ``title``). + """ + STRIP_KEYS = { + "title", + "description", + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + } + result: dict[str, Any] = {} + for key, value in schema.items(): + if not _is_properties and key in STRIP_KEYS: + continue + if isinstance(value, dict): + result[key] = OllamaClient._clean_schema_for_ollama( + value, _is_properties=(key == "properties") + ) + elif isinstance(value, list): + result[key] = [ + OllamaClient._clean_schema_for_ollama(item) + if isinstance(item, dict) + else item + for item in value + ] + else: + result[key] = value + return result + + def _send( + self, + prompt: str, + images: list[bytes], + response_format: dict | None = None, + enable_thinking: bool = False, + ) -> str | None: + """Submit a request to Ollama""" + if self.provider is None: + logger.warning( + "Ollama provider has not been initialized, a description will not be generated. Check your Ollama configuration." + ) + return None + try: + ollama_options = { + **self.provider_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"] = self._clean_schema_for_ollama(schema) + if self.supports_toggleable_thinking: + ollama_options["think"] = enable_thinking + logger.debug( + "Ollama generate request: model=%s, prompt_len=%s, image_count=%s, " + "has_format=%s, options=%s", + self.genai_config.model, + len(prompt), + len(images) if images else 0, + "format" in ollama_options, + {k: v for k, v in ollama_options.items() if k != "format"}, + ) + result = self.provider.generate( + self.genai_config.model, + prompt, + images=images if images else None, + **ollama_options, + ) + logger.debug( + "Ollama generate response: done=%s, done_reason=%s, eval_count=%s, " + "prompt_eval_count=%s, response_len=%s", + result.get("done"), + result.get("done_reason"), + result.get("eval_count"), + result.get("prompt_eval_count"), + len(result.get("response", "") or ""), + ) + response_text = str(result["response"]).strip() + if not response_text: + logger.warning( + "Ollama returned a blank response for model %s (done_reason=%s, " + "eval_count=%s). Check model output, ensure thinking is disabled.", + self.genai_config.model, + result.get("done_reason"), + result.get("eval_count"), + ) + return response_text + except ( + TimeoutException, + ResponseError, + RemoteProtocolError, + ConnectionError, + ) as e: + logger.warning("Ollama returned an error: %s", str(e)) + return None + + def list_models(self) -> list[str]: + """Return available model names from the Ollama server.""" + client = self.provider + if client is None: + # Provider init may have failed due to invalid model, but we can + # still list available models with a fresh client. + if not self.genai_config.base_url: + return [] + try: + client = ApiClient( + host=self.genai_config.base_url, + timeout=self.timeout, + headers=self._auth_headers(), + ) + except Exception: + return [] + try: + response = client.list() + return sorted( + m.get("name", m.get("model", "")) for m in response.get("models", []) + ) + except Exception as e: + logger.warning("Failed to list Ollama models: %s", e) + return [] + + def get_context_size(self) -> int: + """Get the context window size for Ollama.""" + return int( + self.genai_config.provider_options.get("options", {}).get("num_ctx", 4096) + ) + + def _build_request_params( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None, + tool_choice: str | None, + stream: bool = False, + enable_thinking: bool | None = None, + ) -> dict[str, Any]: + """Build request_messages and params for chat (sync or stream).""" + request_messages = [] + for msg in messages: + content, images = _normalize_multimodal_content(msg.get("content", "")) + msg_dict: dict[str, Any] = { + "role": msg.get("role"), + "content": content if content is not None else "", + } + if images: + msg_dict["images"] = images + if msg.get("tool_call_id"): + msg_dict["tool_call_id"] = msg["tool_call_id"] + if msg.get("name"): + msg_dict["name"] = msg["name"] + if msg.get("tool_calls"): + # Ollama requires tool call arguments as dicts, but the + # conversation format (OpenAI-style) stores them as JSON + # strings. Convert back to dicts for Ollama. + ollama_tool_calls = [] + for tc in msg["tool_calls"]: + func = tc.get("function") or {} + args = func.get("arguments") or {} + if isinstance(args, str): + try: + args = json.loads(args) + except (json.JSONDecodeError, TypeError): + args = {} + ollama_tool_calls.append( + {"function": {"name": func.get("name", ""), "arguments": args}} + ) + msg_dict["tool_calls"] = ollama_tool_calls + request_messages.append(msg_dict) + + request_params: dict[str, Any] = { + "model": self.genai_config.model, + "messages": request_messages, + **self.provider_options, + **self.genai_config.runtime_options, + } + if stream: + request_params["stream"] = True + if tools: + request_params["tools"] = tools + if enable_thinking is not None and self.supports_toggleable_thinking: + request_params["think"] = enable_thinking + return request_params + + def _message_from_response(self, response: dict[str, Any]) -> dict[str, Any]: + """Parse Ollama chat response into {content, tool_calls, finish_reason}.""" + if not response or "message" not in response: + logger.debug("Ollama response empty or missing 'message' key") + return { + "content": None, + "tool_calls": None, + "finish_reason": "error", + } + message = response["message"] + logger.debug( + "Ollama response message keys: %s, content_len=%s, thinking_len=%s, " + "tool_calls=%s, done=%s", + list(message.keys()) if hasattr(message, "keys") else "N/A", + len(message.get("content", "") or "") if message.get("content") else 0, + len(message.get("thinking", "") or "") if message.get("thinking") else 0, + bool(message.get("tool_calls")), + response.get("done"), + ) + content = message.get("content", "").strip() if message.get("content") else None + reasoning = ( + message.get("thinking", "").strip() if message.get("thinking") else None + ) + tool_calls = parse_tool_calls_from_message(message) + finish_reason = "error" + if response.get("done"): + finish_reason = ( + "tool_calls" if tool_calls else "stop" if content else "error" + ) + elif tool_calls: + finish_reason = "tool_calls" + elif content: + finish_reason = "stop" + return { + "content": content, + "reasoning": reasoning, + "tool_calls": tool_calls, + "finish_reason": finish_reason, + } + + def chat_with_tools( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, + ) -> dict[str, Any]: + if self.provider is None: + logger.warning( + "Ollama provider has not been initialized. Check your Ollama configuration." + ) + return { + "content": None, + "tool_calls": None, + "finish_reason": "error", + } + try: + request_params = self._build_request_params( + messages, + tools, + tool_choice, + stream=False, + enable_thinking=enable_thinking, + ) + response = self.provider.chat(**request_params) + return self._message_from_response(response) + except (TimeoutException, ResponseError, ConnectionError) as e: + logger.warning("Ollama returned an error: %s", str(e)) + return { + "content": None, + "tool_calls": None, + "finish_reason": "error", + } + except Exception as e: + logger.warning("Unexpected error in Ollama chat_with_tools: %s", str(e)) + return { + "content": None, + "tool_calls": None, + "finish_reason": "error", + } + + async def chat_with_tools_stream( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, + ) -> AsyncGenerator[tuple[str, Any], None]: + """Stream chat with tools; yields content deltas then final message. + + When tools are provided, Ollama streaming does not include tool_calls + in the response chunks. To work around this, we use a non-streaming + call when tools are present to ensure tool calls are captured, then + emit the content as a single delta followed by the final message. + """ + if self.provider is None: + logger.warning( + "Ollama provider has not been initialized. Check your Ollama configuration." + ) + yield ( + "message", + { + "content": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) + return + try: + # Ollama does not return tool_calls in streaming mode, so fall + # back to a non-streaming call when tools are provided. + if tools: + logger.debug( + "Ollama: tools provided, using non-streaming call for tool support" + ) + request_params = self._build_request_params( + messages, + tools, + tool_choice, + stream=False, + enable_thinking=enable_thinking, + ) + async_client = OllamaAsyncClient( + host=self.genai_config.base_url, + timeout=self.timeout, + headers=self._auth_headers(), + ) + response = await async_client.chat(**request_params) + result = self._message_from_response(response) + reasoning = result.get("reasoning") + if reasoning: + yield ("reasoning_delta", reasoning) + content = result.get("content") + if content: + yield ("content_delta", content) + stats = _extract_ollama_stats(response) + if stats is not None: + yield ("stats", stats) + yield ("message", result) + return + + request_params = self._build_request_params( + messages, + tools, + tool_choice, + stream=True, + enable_thinking=enable_thinking, + ) + async_client = OllamaAsyncClient( + host=self.genai_config.base_url, + timeout=self.timeout, + headers=self._auth_headers(), + ) + content_parts: list[str] = [] + reasoning_parts: list[str] = [] + final_message: dict[str, Any] | None = None + final_chunk: Any = None + stream = await async_client.chat(**request_params) + async for chunk in stream: + if not chunk or "message" not in chunk: + continue + msg = chunk.get("message", {}) + reasoning_delta = msg.get("thinking") or "" + if reasoning_delta: + reasoning_parts.append(reasoning_delta) + yield ("reasoning_delta", reasoning_delta) + delta = msg.get("content") or "" + if delta: + content_parts.append(delta) + yield ("content_delta", delta) + if chunk.get("done"): + final_chunk = chunk + full_content = "".join(content_parts).strip() or None + full_reasoning = "".join(reasoning_parts).strip() or None + final_message = { + "content": full_content, + "reasoning": full_reasoning, + "tool_calls": None, + "finish_reason": "stop", + } + break + + stats = _extract_ollama_stats(final_chunk) + if stats is not None: + yield ("stats", stats) + + if final_message is not None: + yield ("message", final_message) + else: + yield ( + "message", + { + "content": "".join(content_parts).strip() or None, + "reasoning": "".join(reasoning_parts).strip() or None, + "tool_calls": None, + "finish_reason": "stop", + }, + ) + except (TimeoutException, ResponseError, ConnectionError) as e: + logger.warning("Ollama streaming error: %s", str(e)) + yield ( + "message", + { + "content": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) + except Exception as e: + logger.warning( + "Unexpected error in Ollama chat_with_tools_stream: %s", str(e) + ) + yield ( + "message", + { + "content": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) diff --git a/frigate/genai/plugins/openai.py b/frigate/genai/plugins/openai.py new file mode 100644 index 0000000..40b1ee0 --- /dev/null +++ b/frigate/genai/plugins/openai.py @@ -0,0 +1,473 @@ +"""OpenAI Provider for Frigate AI.""" + +import base64 +import json +import logging +from collections.abc import AsyncGenerator +from typing import Any + +from httpx import TimeoutException +from openai import OpenAI + +from frigate.config import GenAIProviderEnum +from frigate.genai import GenAIClient, register_genai_provider + +logger = logging.getLogger(__name__) + + +def _stats_from_openai_usage(usage: Any) -> dict[str, Any] | None: + """Build a stats dict from an OpenAI-compatible usage object.""" + if usage is None: + return None + prompt_tokens = getattr(usage, "prompt_tokens", None) + completion_tokens = getattr(usage, "completion_tokens", None) + if prompt_tokens is None and completion_tokens is None: + return None + stats: dict[str, Any] = {} + if isinstance(prompt_tokens, int): + stats["prompt_tokens"] = prompt_tokens + if isinstance(completion_tokens, int): + stats["completion_tokens"] = completion_tokens + return stats or None + + +@register_genai_provider(GenAIProviderEnum.openai) +class OpenAIClient(GenAIClient): + """Generative AI client for Frigate using OpenAI.""" + + provider: OpenAI + context_size: int | None = None + + def _init_provider(self) -> OpenAI: + """Initialize the client. + + Subclasses (e.g. Azure) should raise on configuration errors; the + manager catches construction failures and disables the provider. + """ + # Extract context_size from provider_options as it's not a valid OpenAI client parameter + # It will be used in get_context_size() instead + provider_opts = { + k: v + for k, v in self.genai_config.provider_options.items() + if k != "context_size" + } + + if self.genai_config.base_url: + provider_opts["base_url"] = self.genai_config.base_url + + return OpenAI(api_key=self.genai_config.api_key, **provider_opts) + + def _send( + self, + prompt: str, + images: list[bytes], + response_format: dict | None = None, + enable_thinking: bool = False, + ) -> str | None: + """Submit a request to OpenAI.""" + encoded_images = [base64.b64encode(image).decode("utf-8") for image in images] + messages_content: list[dict] = [ + { + "type": "text", + "text": prompt, + } + ] + for image in encoded_images: + messages_content.append( + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{image}", + "detail": "low", + }, + } + ) + try: + request_params = { + "model": self.genai_config.model, + "messages": [ + { + "role": "user", + "content": messages_content, + }, + ], + "timeout": self.timeout, + **self.genai_config.runtime_options, + } + if response_format: + # OpenAI strict mode requires additionalProperties: false on the schema + if response_format.get("type") == "json_schema" and response_format.get( + "json_schema", {} + ).get("strict"): + schema = response_format.get("json_schema", {}).get("schema") + if isinstance(schema, dict): + schema["additionalProperties"] = False + request_params["response_format"] = response_format + + result = self.provider.chat.completions.create(**request_params) + + if ( + result is not None + and hasattr(result, "choices") + and len(result.choices) > 0 + ): + message = result.choices[0].message + content = message.content + + if not content: + # When reasoning is enabled for some OpenAI backends the actual response + # is incorrectly placed in reasoning_content instead of content. + # This is buggy/incorrect behavior — reasoning should not be + # enabled for these models. + reasoning_content = getattr(message, "reasoning_content", None) + if reasoning_content: + logger.warning( + "Response content was empty but reasoning_content was provided; " + "reasoning appears to be enabled and should be disabled for this model." + ) + content = reasoning_content + + return str(content.strip()) if content else None + return None + except (TimeoutException, Exception) as e: + logger.warning("OpenAI returned an error: %s", str(e)) + return None + + def list_models(self) -> list[str]: + """Return available model IDs from the OpenAI-compatible API.""" + try: + return sorted(m.id for m in self.provider.models.list().data) + except Exception as e: + logger.warning("Failed to list OpenAI models: %s", e) + return [] + + def get_context_size(self) -> int: + """Get the context window size for OpenAI.""" + if self.context_size is not None: + return self.context_size + + # First check provider_options for manually specified context size + # This is necessary for llama.cpp and other OpenAI-compatible servers + # that don't expose the configured runtime context size in the API response + if "context_size" in self.genai_config.provider_options: + self.context_size = self.genai_config.provider_options["context_size"] + logger.debug( + f"Using context size {self.context_size} from provider_options for model {self.genai_config.model}" + ) + return self.context_size + + try: + models = self.provider.models.list() + for model in models.data: + if model.id == self.genai_config.model: + if hasattr(model, "max_model_len") and model.max_model_len: + self.context_size = model.max_model_len + logger.debug( + f"Retrieved context size {self.context_size} for model {self.genai_config.model}" + ) + return self.context_size + + except Exception as e: + logger.debug( + f"Failed to fetch model context size from API: {e}, using default" + ) + + # Default to 128K for ChatGPT models, 8K for others + model_name = self.genai_config.model.lower() + if "gpt" in model_name: + self.context_size = 128000 + else: + self.context_size = 8192 + + logger.debug( + f"Using default context size {self.context_size} for model {self.genai_config.model}" + ) + return self.context_size + + def chat_with_tools( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, + ) -> dict[str, Any]: + """ + Send chat messages to OpenAI with optional tool definitions. + + Implements function calling/tool usage for OpenAI models. The OpenAI + chat completions API does not expose a per-request thinking toggle, + so ``enable_thinking`` is accepted for interface parity and ignored. + """ + try: + openai_tool_choice = None + if tool_choice: + if tool_choice == "none": + openai_tool_choice = "none" + elif tool_choice == "auto": + openai_tool_choice = "auto" + elif tool_choice == "required": + openai_tool_choice = "required" + + request_params = { + "model": self.genai_config.model, + "messages": messages, + "timeout": self.timeout, + **self.genai_config.runtime_options, + } + + if tools: + request_params["tools"] = tools + if openai_tool_choice is not None: + request_params["tool_choice"] = openai_tool_choice + + if isinstance(self.genai_config.provider_options, dict): + excluded_options = {"context_size"} + provider_opts = { + k: v + for k, v in self.genai_config.provider_options.items() + if k not in excluded_options + } + request_params.update(provider_opts) + + result = self.provider.chat.completions.create(**request_params) + + if ( + result is None + or not hasattr(result, "choices") + or len(result.choices) == 0 + ): + return { + "content": None, + "tool_calls": None, + "finish_reason": "error", + } + + choice = result.choices[0] + message = choice.message + content = message.content.strip() if message.content else None + raw_reasoning = getattr(message, "reasoning_content", None) or getattr( + message, "reasoning", None + ) + reasoning = raw_reasoning.strip() if raw_reasoning else None + + tool_calls = None + if message.tool_calls: + tool_calls = [] + for tool_call in message.tool_calls: + try: + arguments = json.loads(tool_call.function.arguments) + except (json.JSONDecodeError, AttributeError) as e: + logger.warning( + f"Failed to parse tool call arguments: {e}, " + f"tool: {tool_call.function.name if hasattr(tool_call.function, 'name') else 'unknown'}" + ) + arguments = {} + + tool_calls.append( + { + "id": tool_call.id if hasattr(tool_call, "id") else "", + "name": tool_call.function.name + if hasattr(tool_call.function, "name") + else "", + "arguments": arguments, + } + ) + + finish_reason = "error" + if hasattr(choice, "finish_reason") and choice.finish_reason: + finish_reason = choice.finish_reason + elif tool_calls: + finish_reason = "tool_calls" + elif content: + finish_reason = "stop" + + return { + "content": content, + "reasoning": reasoning, + "tool_calls": tool_calls, + "finish_reason": finish_reason, + } + + except TimeoutException as e: + logger.warning("OpenAI request timed out: %s", str(e)) + return { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + } + except Exception as e: + logger.warning("OpenAI returned an error: %s", str(e)) + return { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + } + + async def chat_with_tools_stream( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, + ) -> AsyncGenerator[tuple[str, Any], None]: + """ + Stream chat with tools; yields content deltas then final message. + + Implements streaming function calling/tool usage for OpenAI models. + The OpenAI chat completions API does not expose a per-request thinking + toggle, so ``enable_thinking`` is accepted for interface parity and + ignored. + """ + try: + openai_tool_choice = None + if tool_choice: + if tool_choice == "none": + openai_tool_choice = "none" + elif tool_choice == "auto": + openai_tool_choice = "auto" + elif tool_choice == "required": + openai_tool_choice = "required" + + request_params = { + "model": self.genai_config.model, + "messages": messages, + "timeout": self.timeout, + "stream": True, + "stream_options": {"include_usage": True}, + **self.genai_config.runtime_options, + } + + if tools: + request_params["tools"] = tools + if openai_tool_choice is not None: + request_params["tool_choice"] = openai_tool_choice + + if isinstance(self.genai_config.provider_options, dict): + excluded_options = {"context_size"} + provider_opts = { + k: v + for k, v in self.genai_config.provider_options.items() + if k not in excluded_options + } + request_params.update(provider_opts) + + # Use streaming API + content_parts: list[str] = [] + reasoning_parts: list[str] = [] + tool_calls_by_index: dict[int, dict[str, Any]] = {} + finish_reason = "stop" + usage_stats: dict[str, Any] | None = None + + stream = self.provider.chat.completions.create(**request_params) + + for chunk in stream: + chunk_usage = getattr(chunk, "usage", None) + if chunk_usage is not None: + usage_stats = _stats_from_openai_usage(chunk_usage) + + if not chunk or not chunk.choices: + continue + + choice = chunk.choices[0] + delta = choice.delta + + # Check for finish reason + if choice.finish_reason: + finish_reason = choice.finish_reason + + # Extract reasoning deltas (reasoning_content or reasoning, + # depending on the server) + reasoning_delta = getattr(delta, "reasoning_content", None) or getattr( + delta, "reasoning", None + ) + if reasoning_delta: + reasoning_parts.append(reasoning_delta) + yield ("reasoning_delta", reasoning_delta) + + # Extract content deltas + if delta.content: + content_parts.append(delta.content) + yield ("content_delta", delta.content) + + # Extract tool calls + if delta.tool_calls: + for tc in delta.tool_calls: + idx = tc.index + fn = tc.function + + if idx not in tool_calls_by_index: + tool_calls_by_index[idx] = { + "id": tc.id or "", + "name": fn.name if fn and fn.name else "", + "arguments": "", + } + + t = tool_calls_by_index[idx] + if tc.id: + t["id"] = tc.id + if fn and fn.name: + t["name"] = fn.name + if fn and fn.arguments: + t["arguments"] += fn.arguments + + # Build final message + full_content = "".join(content_parts).strip() or None + full_reasoning = "".join(reasoning_parts).strip() or None + + # Convert tool calls to list format + tool_calls_list = None + if tool_calls_by_index: + tool_calls_list = [] + for tc in tool_calls_by_index.values(): + try: + # Parse accumulated arguments as JSON + parsed_args = json.loads(tc["arguments"]) + except (json.JSONDecodeError, Exception): + parsed_args = tc["arguments"] + + tool_calls_list.append( + { + "id": tc["id"], + "name": tc["name"], + "arguments": parsed_args, + } + ) + finish_reason = "tool_calls" + + if usage_stats is not None: + yield ("stats", usage_stats) + + yield ( + "message", + { + "content": full_content, + "reasoning": full_reasoning, + "tool_calls": tool_calls_list, + "finish_reason": finish_reason, + }, + ) + + except TimeoutException as e: + logger.warning("OpenAI streaming request timed out: %s", str(e)) + yield ( + "message", + { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) + except Exception as e: + logger.warning("OpenAI streaming returned an error: %s", str(e)) + yield ( + "message", + { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) diff --git a/frigate/genai/prompts.py b/frigate/genai/prompts.py new file mode 100644 index 0000000..5f1d328 --- /dev/null +++ b/frigate/genai/prompts.py @@ -0,0 +1,744 @@ +"""Prompt and response-format builders for GenAI features. + +Centralizes the per-feature prompt framing and structured-output schema +shaping so provider clients in :mod:`frigate.genai.plugins` only handle +transport. +""" + +import datetime +from typing import Any + +from playhouse.shortcuts import model_to_dict + +from frigate.config import CameraConfig, FrigateConfig +from frigate.config.classification import ObjectClassificationType +from frigate.config.ui import UnitSystemEnum +from frigate.data_processing.post.types import ReviewMetadata +from frigate.models import Event + + +def build_review_description_prompt( + review_data: dict[str, Any], + thumbnails: list[bytes], + concerns: list[str], + preferred_language: str | None, + activity_context_prompt: str, +) -> str: + """Build the prompt for review activity description generation.""" + + def get_concern_prompt() -> str: + if concerns: + concern_list = "\n - ".join(concerns) + return ( + "\n- `other_concerns` (list of strings): Include a list of any of " + "the following concerns that are occurring:\n" + f" - {concern_list}" + ) + else: + return "" + + def get_language_prompt() -> str: + if preferred_language: + return f"Provide your answer in {preferred_language}" + else: + return "" + + def get_objects_list() -> str: + if review_data["unified_objects"]: + return "\n- " + "\n- ".join(review_data["unified_objects"]) + else: + return "\n- (No objects detected)" + + return f""" +Your task is to analyze a sequence of images taken in chronological order from a security camera. + +## Normal Activity Patterns for This Property + +{activity_context_prompt} + +## Task Instructions + +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. + +## Analysis Guidelines + +When forming your description: +- **Treat "Objects in Scene" as the list of tracked subjects to describe.** Do not introduce additional people or vehicles that are not present in this list. You may freely reference other items, surfaces, and environmental details visible in the frames when describing what the listed subjects are doing. +- **Describe the most likely activity from visible cues across the sequence** — the subject's path, what they are carrying, and what they interact with. Avoid asserting completed outcomes you do not observe; describe in-progress actions rather than results. +- Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity). +- Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects. +- Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved. +- **Use the actual timestamp provided in "Activity started at"** below for time of day context—do not infer time from image brightness or darkness. Unusual hours (late night/early morning) should increase suspicion when the observable behavior itself appears questionable. However, recognize that some legitimate activities can occur at any hour. +- **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. + +## Response Field Guidelines + +Respond with a JSON object matching the provided schema. Field-specific guidance: +- `observations`: Include the very start of the activity — for example, a vehicle entering the frame or pulling into the driveway — even if it lasts only a few frames and the rest of the clip is dominated by a longer activity. Include each arrival, departure, object handled, and notable change in position or state. Each item is a single concrete fact written as a complete sentence. +- `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. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name — do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign. +- `title`: Name the primary activity across the observations, together with the location. An activity is what is being done with objects, tools, or surfaces; locomotion through the scene qualifies as the activity only when no other interaction is observed. For named subjects, always use their name. For unnamed objects, refer to them naturally with articles. +- `shortSummary`: Briefly summarize the primary activity across the observations. +- `potential_threat_level`: Must be consistent with your scene description and the activity patterns above. +{get_concern_prompt()} + +## Sequence Details + +- Camera: {review_data["camera"]} +- Total frames: {len(thumbnails)} (Frame 1 = earliest, Frame {len(thumbnails)} = latest) +- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds +- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"} + +## Objects in Scene + +Each line represents a detection state, not necessarily unique individuals. The `←` symbol separates a recognized subject's name from their object type — use only the name (before the `←`) in your response, not the type after it. The same subject may appear across multiple lines if detected multiple times. + +**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_language_prompt()} +""" + + +def build_review_description_response_format(concerns: list[str]) -> dict[str, Any]: + """Build the structured-output JSON schema for review descriptions. + + Strips the `time` field (populated server-side) and drops + `other_concerns` when no concerns are configured. + """ + 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") + + return { + "type": "json_schema", + "json_schema": { + "name": "review_metadata", + "strict": True, + "schema": schema, + }, + } + + +def build_review_summary_prompt( + start_ts: float, + end_ts: float, + events: list[dict[str, Any]], + preferred_language: str | None, +) -> str: + """Build the prompt for a multi-event review summary.""" + time_range = ( + f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')}" + f" to " + f"{datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}" + ) + prompt = f""" +You are a security officer writing a concise security report. + +Time range: {time_range} + +Input format: Each event is a JSON object with: +- "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "camera", "time", "start_time", "end_time" +- "context": array of related events from other cameras that occurred during overlapping time periods + +**Note: Use the "scene" field for event descriptions in the report. Ignore any "shortSummary" field if present.** + +Report Structure - Use this EXACT format: + +# Security Summary - {time_range} + +## Overview +[Write 1-2 sentences summarizing the overall activity pattern during this period.] + +--- + +## Timeline + +[Group events by time periods (e.g., "Morning (6:00 AM - 12:00 PM)", "Afternoon (12:00 PM - 5:00 PM)", "Evening (5:00 PM - 9:00 PM)", "Night (9:00 PM - 6:00 AM)"). Use appropriate time blocks based on when events occurred.] + +### [Time Block Name] + +**HH:MM AM/PM** | [Camera Name] | [Threat Level Indicator] +- [Event title]: [Clear description incorporating contextual information from the "context" array] +- Context: [If context array has items, mention them here, e.g., "Delivery truck present on Front Driveway Cam (HH:MM AM/PM)"] +- Assessment: [Brief assessment incorporating context - if context explains the event, note it here] + +[Repeat for each event in chronological order within the time block] + +--- + +## Summary +[One sentence summarizing the period. If all events are normal/explained: "Routine activity observed." If review needed: "Some activity requires review but no security concerns." If security concerns: "Security concerns requiring immediate attention."] + +Guidelines: +- List ALL events in chronological order, grouped by time blocks +- Threat level indicators: ✓ Normal, ⚠️ Needs review, 🔴 Security concern +- Integrate contextual information naturally - use the "context" array to enrich each event's description +- If context explains the event (e.g., delivery truck explains person at door), describe it accordingly (e.g., "delivery person" not "unidentified person") +- Be concise but informative - focus on what happened and what it means +- If contextual information makes an event clearly normal, reflect that in your assessment +- Only create time blocks that have events - don't create empty sections +""" + + prompt += "\n\nEvents:\n" + for event in events: + prompt += f"\n{event}\n" + + if preferred_language: + prompt += f"\nProvide your answer in {preferred_language}" + + return prompt + + +def build_object_description_prompt( + camera_config: CameraConfig, + event: Event, +) -> str: + """Build the prompt for a per-object description. + + Pulls the per-label override from `objects.genai.object_prompts`, falling + back to the camera default, and interpolates event fields. + + Raises: + KeyError: if the user-defined prompt template references an unknown + event field. + """ + template = camera_config.objects.genai.object_prompts.get( + str(event.label), + camera_config.objects.genai.prompt, + ) + return template.format(**model_to_dict(event)) + + +def get_attribute_classifications(config: FrigateConfig) -> list[dict[str, Any]]: + """Return enabled custom classification models of `attribute` type. + + Each entry: {"name": , "objects": [, ...]}. + These models attach attribute metadata to events on the listed object + types, which can later be filtered via the search_objects `attribute` + field. + """ + result: list[dict[str, Any]] = [] + + for model_key, model_config in config.classification.custom.items(): + if not model_config.enabled or model_config.object_config is None: + continue + + if ( + model_config.object_config.classification_type + != ObjectClassificationType.attribute + ): + continue + + result.append( + { + "name": model_config.name or model_key, + "objects": list(model_config.object_config.objects or []), + } + ) + + return result + + +def get_tool_definitions( + semantic_search_enabled: bool = False, + attribute_classifications: list[dict[str, Any]] | None = None, +) -> list[dict[str, Any]]: + """ + Get OpenAI-compatible tool definitions for Frigate. + + Returns a list of tool definitions that can be used with OpenAI-compatible + function calling APIs. When semantic search is enabled, the search_objects + tool exposes an additional `semantic_query` parameter for descriptive + queries (e.g. "person riding a lawn mower") and find_similar_objects is + included. When attribute classification models are configured, an + `attribute` parameter is exposed for filtering by their labels. + """ + search_objects_properties: dict[str, Any] = { + "camera": { + "type": "string", + "description": "Camera name to filter by (optional).", + }, + "label": { + "type": "string", + "description": ( + "Generic object class to filter by — one of the tracked detector " + "labels such as 'person', 'package', 'car', 'dog', 'bird'. Use " + "this for broad queries like 'show me all cars today'. Combine " + "with semantic_query when the user also describes appearance or " + "behavior (e.g. label='person', semantic_query='riding a lawn " + "mower')." + ), + }, + "sub_label": { + "type": "string", + "description": ( + "Filter by a DISCRETE NAMED entity recognized in the detection. " + "Use this for: a known person's name ('John'), a delivery " + "company ('Amazon', 'UPS'), a recognized animal species or " + "breed ('blue jay', 'cardinal', 'golden retriever'), or a " + "license plate string. When filtering by a specific name, set " + "only sub_label and leave label unset. Do NOT use sub_label " + "for descriptions of appearance, clothing, or actions — those " + "belong in semantic_query." + ), + }, + "after": { + "type": "string", + "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').", + }, + "before": { + "type": "string", + "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').", + }, + "zones": { + "type": "array", + "items": {"type": "string"}, + "description": "List of zone names to filter by.", + }, + "limit": { + "type": "integer", + "description": "Maximum number of objects to return (default: 25).", + "default": 25, + }, + } + + if attribute_classifications: + model_outline = "; ".join( + f"{m['name']} (applies to {', '.join(m['objects']) or 'any object'})" + for m in attribute_classifications + ) + search_objects_properties["attribute"] = { + "type": "string", + "description": ( + "Filter by a classification attribute label produced by a " + "configured attribute classification model. Use this INSTEAD " + "of semantic_query when the user's request matches one of " + "these classifications. Configured models: " + f"{model_outline}. " + "Set the value to the attribute label that matches the user's " + "phrasing (case-sensitive)." + ), + } + + if semantic_search_enabled: + search_objects_properties["semantic_query"] = { + "type": "string", + "description": ( + "Optional natural-language description of a PHYSICAL " + "CHARACTERISTIC, APPEARANCE, or ACTIVITY the user mentioned, " + "used to semantically narrow results. Only set this when the " + "user describes something beyond what label and sub_label can " + "express on their own.\n" + "USE for descriptive phrases like: 'riding a lawn mower', " + "'wearing a red jacket', 'carrying a package', 'walking a " + "dog', 'on a bicycle', 'holding an umbrella'.\n" + "DO NOT USE for:\n" + "- specific named people, pets, or delivery companies → use sub_label\n" + "- animal species or breed names like 'blue jay', 'cardinal', " + "'golden retriever' → use sub_label\n" + "- license plate strings → use sub_label\n" + "- generic object queries like 'all cars today' or 'every " + "person' → use label alone with no semantic_query\n" + "When set, combine with label/time/camera/zone filters as " + "usual (e.g. label='person', semantic_query='riding a lawn " + "mower', after='2024-05-01T00:00:00Z')." + ), + } + + search_objects_description = ( + "Search the historical record of detected objects in Frigate. " + "Use this ONLY for questions about the PAST — e.g. 'did anyone come by today?', " + "'when was the last car?', 'show me detections from yesterday'. " + "Do NOT use this for monitoring or alerting requests about future events — " + "use start_camera_watch instead for those. " + "An 'object' in Frigate represents a tracked detection (e.g., a person, package, car).\n\n" + "Choose filters based on what the user is asking for:\n" + "- Generic class query ('show me all cars today'): set `label` only.\n" + "- Specific NAMED entity (known person, delivery company, animal " + "species/breed like 'blue jay' or 'golden retriever', license " + "plate): set `sub_label` only and leave `label` unset.\n" + ) + if semantic_search_enabled: + search_objects_description += ( + "- Physical CHARACTERISTIC, APPEARANCE, or ACTIVITY that is not a " + "discrete name ('person riding a lawn mower', 'someone in a red " + "jacket', 'person carrying a package'): set `semantic_query` with " + "the descriptive phrase, optionally alongside `label` for the " + "object class. Do NOT put descriptive phrases in sub_label." + ) + + return [ + { + "type": "function", + "function": { + "name": "search_objects", + "description": search_objects_description, + "parameters": { + "type": "object", + "properties": search_objects_properties, + }, + "required": [], + }, + }, + { + "type": "function", + "function": { + "name": "find_similar_objects", + "description": ( + "Find tracked objects that are visually and semantically similar " + "to a specific past event. Use this when the user references a " + "particular object they have seen and wants to find other " + "sightings of the same or similar one ('that green car', 'the " + "person in the red jacket', 'the package that was delivered'). " + "Prefer this over search_objects whenever the user's intent is " + "'find more like this specific one.' Use search_objects first " + "only if you need to locate the anchor event. Requires semantic " + "search to be enabled." + ), + "parameters": { + "type": "object", + "properties": { + "event_id": { + "type": "string", + "description": "The id of the anchor event to find similar objects to.", + }, + "after": { + "type": "string", + "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').", + }, + "before": { + "type": "string", + "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').", + }, + "cameras": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of cameras to restrict to. Defaults to all.", + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of labels to restrict to. Defaults to the anchor event's label.", + }, + "sub_labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of sub_labels (names) to restrict to.", + }, + "zones": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of zones. An event matches if any of its zones overlap.", + }, + "similarity_mode": { + "type": "string", + "enum": ["visual", "semantic", "fused"], + "description": "Which similarity signal(s) to use. 'fused' (default) combines visual and semantic.", + "default": "fused", + }, + "min_score": { + "type": "number", + "description": "Drop matches with a similarity score below this threshold (0.0-1.0).", + }, + "limit": { + "type": "integer", + "description": "Maximum number of matches to return (default: 10).", + "default": 10, + }, + }, + "required": ["event_id"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "set_camera_state", + "description": ( + "Change a camera's feature state (e.g., turn detection on/off, enable/disable recordings). " + "Use camera='*' to apply to all cameras at once. " + "Only call this tool when the user explicitly asks to change a camera setting. " + "Requires admin privileges." + ), + "parameters": { + "type": "object", + "properties": { + "camera": { + "type": "string", + "description": "Camera name to target, or '*' to target all cameras.", + }, + "feature": { + "type": "string", + "enum": [ + "detect", + "record", + "snapshots", + "audio", + "motion", + "enabled", + "birdseye", + "birdseye_mode", + "improve_contrast", + "ptz_autotracker", + "motion_contour_area", + "motion_threshold", + "notifications", + "audio_transcription", + "review_alerts", + "review_detections", + "object_descriptions", + "review_descriptions", + "profile", + ], + "description": ( + "The feature to change. Most features accept ON or OFF. " + "birdseye_mode accepts CONTINUOUS, MOTION, or OBJECTS. " + "motion_contour_area and motion_threshold accept a number. " + "profile accepts a profile name or 'none' to deactivate (requires camera='*')." + ), + }, + "value": { + "type": "string", + "description": "The value to set. ON or OFF for toggles, a number for thresholds, a profile name or 'none' for profile.", + }, + }, + "required": ["camera", "feature", "value"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_live_context", + "description": ( + "Get the current live image and detection information for a single camera: objects being tracked, " + "zones, timestamps. Use this to understand what is visible in the live view. " + "Call this when answering questions about what is happening right now on a specific camera. " + "Operates on one camera at a time; call the tool again for each additional camera. " + "Wildcards and empty values are not accepted." + ), + "parameters": { + "type": "object", + "properties": { + "camera": { + "type": "string", + "description": ( + "Exact name of a single camera to get live context for. " + "Wildcards (e.g. '*', 'all') and empty strings are not accepted." + ), + }, + }, + "required": ["camera"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "start_camera_watch", + "description": ( + "Start a continuous VLM watch job that monitors a camera and sends a notification " + "when a specified condition is met. Use this when the user wants to be alerted about " + "a future event, e.g. 'tell me when guests arrive' or 'notify me when the package is picked up'. " + "Only one watch job can run at a time. Returns a job ID." + ), + "parameters": { + "type": "object", + "properties": { + "camera": { + "type": "string", + "description": "Camera ID to monitor.", + }, + "condition": { + "type": "string", + "description": ( + "Natural-language description of the condition to watch for, " + "e.g. 'a person arrives at the front door'." + ), + }, + "max_duration_minutes": { + "type": "integer", + "description": "Maximum time to watch before giving up (minutes, default 60).", + "default": 60, + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Object labels that should trigger a VLM check (e.g. ['person', 'car']). If omitted, any detection on the camera triggers a check.", + }, + "zones": { + "type": "array", + "items": {"type": "string"}, + "description": "Zone names to filter by. If specified, only detections in these zones trigger a VLM check.", + }, + }, + "required": ["camera", "condition"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "stop_camera_watch", + "description": ( + "Cancel the currently running VLM watch job. Use this when the user wants to " + "stop a previously started watch, e.g. 'stop watching the front door'." + ), + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_profile_status", + "description": ( + "Get the current profile status including the active profile and " + "timestamps of when each profile was last activated. Use this to " + "determine time periods for recap requests — e.g. when the user asks " + "'what happened while I was away?', call this first to find the relevant " + "time window based on profile activation history." + ), + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_recap", + "description": ( + "Get a recap of all activity (alerts and detections) for a given time period. " + "Use this after calling get_profile_status to retrieve what happened during " + "a specific window — e.g. 'what happened while I was away?'. Returns a " + "chronological list of activity with camera, objects, zones, and GenAI-generated " + "descriptions when available. Summarize the results for the user." + ), + "parameters": { + "type": "object", + "properties": { + "after": { + "type": "string", + "description": "Start of the time period in ISO 8601 format (e.g. '2025-03-15T08:00:00').", + }, + "before": { + "type": "string", + "description": "End of the time period in ISO 8601 format (e.g. '2025-03-15T17:00:00').", + }, + "cameras": { + "type": "string", + "description": "Comma-separated camera IDs to include, or 'all' for all cameras. Default is 'all'.", + }, + "severity": { + "type": "string", + "enum": ["alert", "detection"], + "description": "Filter by severity level. Omit to include both alerts and detections.", + }, + }, + "required": ["after", "before"], + }, + }, + }, + ] + + +def build_chat_system_prompt( + config: FrigateConfig, + allowed_cameras: list[str], + semantic_search_enabled: bool, + attribute_classifications: list[dict[str, Any]], +) -> str: + """Build the system prompt for the chat completion endpoint. + + Composes the static framing with conditional sections describing the + available cameras, speed units, semantic-search routing guidance, and + configured attribute classifications. + """ + current_datetime = datetime.datetime.now() + current_date_str = current_datetime.strftime("%Y-%m-%d") + current_time_str = current_datetime.strftime("%I:%M:%S %p") + + cameras_info: list[str] = [] + has_speed_zone = False + for camera_id in allowed_cameras: + if camera_id not in config.cameras: + continue + camera_config = config.cameras[camera_id] + friendly_name = ( + camera_config.friendly_name + if camera_config.friendly_name + else camera_id.replace("_", " ").title() + ) + zone_names = list(camera_config.zones.keys()) + if not has_speed_zone: + has_speed_zone = any( + zone.distances for zone in camera_config.zones.values() + ) + if zone_names: + cameras_info.append( + f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_names)})" + ) + else: + cameras_info.append(f" - {friendly_name} (ID: {camera_id})") + + cameras_section = "" + if cameras_info: + cameras_section = ( + "\n\nAvailable cameras:\n" + + "\n".join(cameras_info) + + "\n\nWhen users refer to cameras by their friendly name (e.g., 'Back Deck Camera'), use the corresponding camera ID (e.g., 'back_deck_cam') in tool calls." + ) + + speed_units_section = "" + if has_speed_zone: + speed_unit = ( + "mph" if config.ui.unit_system == UnitSystemEnum.imperial else "km/h" + ) + speed_units_section = f"\n\nReport object speeds to the user in {speed_unit}." + + semantic_search_section = "" + if semantic_search_enabled: + semantic_search_section = ( + "\n\nWhen routing a search_objects call, pick filters by the shape of the user's request:\n" + "- Generic class ('show me all cars today'): set `label` only.\n" + "- Specific named entity — a known person ('John'), delivery company ('Amazon'), animal species/breed ('blue jay', 'cardinal', 'golden retriever'), or license plate: set `sub_label` only and leave `label` unset.\n" + "- Physical characteristic, appearance, or activity that is NOT a discrete name ('find me people riding a lawn mower', 'someone in a red jacket', 'a person carrying a package'): set `semantic_query` with the descriptive phrase, optionally combined with `label` for the object class. Never put descriptive phrases in `sub_label`." + ) + + attribute_classification_section = "" + if attribute_classifications: + model_lines = "\n".join( + f"- {m['name']}: applies to {', '.join(m['objects']) or 'any object'}" + for m in attribute_classifications + ) + attribute_classification_section = ( + "\n\nAttribute classification models are configured for the following object types:\n" + f"{model_lines}\n" + "When the user's request matches one of these classifications, set the search_objects `attribute` field to the matching label rather than using `semantic_query`. Reserve `semantic_query` for descriptive phrases that fall outside the configured attribute labels." + ) + + return f"""You are a helpful assistant for Frigate, a security camera NVR system. You help users answer questions about their cameras, detected objects, and events. + +Current server local date and time: {current_date_str} at {current_time_str} + +Do not start your response with phrases like "I will check...", "Let me see...", or "Let me look...". Answer directly. + +Always present times to the user in the server's local timezone. When tool results include start_time_local and end_time_local, use those exact strings when listing or describing detection times—do not convert or invent timestamps. Do not use UTC or ISO format with Z for the user-facing answer unless the tool result only provides Unix timestamps without local time fields. +When users ask about "today", "yesterday", "this week", etc., use the current date above as reference. +When searching for objects or events, use ISO 8601 format for dates (e.g., {current_date_str}T00:00:00Z for the start of today). +Always be accurate with time calculations based on the current date provided. + +When a user refers to a specific object they have seen or describe with identifying details ("that green car", "the person in the red jacket", "a package left today"), prefer the find_similar_objects tool over search_objects. Use search_objects first only to locate the anchor event, then pass its id to find_similar_objects. For generic queries like "show me all cars today", keep using search_objects. If a user message begins with [attached_event:], treat that event id as the anchor for any similarity or "tell me more" request in the same message and call find_similar_objects with that id.{semantic_search_section}{attribute_classification_section}{cameras_section}{speed_units_section}""" diff --git a/frigate/genai/utils.py b/frigate/genai/utils.py new file mode 100644 index 0000000..fbd6f21 --- /dev/null +++ b/frigate/genai/utils.py @@ -0,0 +1,83 @@ +"""Shared helpers for GenAI providers and chat (OpenAI-style messages, tool call parsing).""" + +import json +import logging +from typing import Any + +logger = logging.getLogger(__name__) + + +def parse_tool_calls_from_message( + message: dict[str, Any], +) -> list[dict[str, Any]] | None: + """ + Parse tool_calls from an OpenAI-style message dict. + + Message may have "tool_calls" as a list of: + {"id": str, "function": {"name": str, "arguments": str}, ...} + + Returns a list of {"id", "name", "arguments"} with arguments parsed as dict, + or None if no tool_calls. Used by Ollama and LlamaCpp (non-stream) responses. + """ + raw = message.get("tool_calls") + if not raw or not isinstance(raw, list): + return None + result = [] + for idx, tool_call in enumerate(raw): + function_data = tool_call.get("function") or {} + raw_arguments = function_data.get("arguments") or {} + if isinstance(raw_arguments, dict): + arguments = raw_arguments + elif isinstance(raw_arguments, str): + try: + arguments = json.loads(raw_arguments) + except (json.JSONDecodeError, KeyError, TypeError) as e: + logger.warning( + "Failed to parse tool call arguments: %s, tool: %s", + e, + function_data.get("name", "unknown"), + ) + arguments = {} + else: + arguments = {} + result.append( + { + "id": tool_call.get("id", "") or f"call_{idx}", + "name": function_data.get("name", ""), + "arguments": arguments, + } + ) + return result if result else None + + +def build_assistant_message_for_conversation( + content: Any, + tool_calls_raw: list[dict[str, Any]] | None, +) -> dict[str, Any]: + """ + Build the assistant message dict in OpenAI format for appending to a conversation. + + tool_calls_raw: list of {"id", "name", "arguments"} (arguments as dict), or None. + """ + msg: dict[str, Any] = {"role": "assistant", "content": content} + if tool_calls_raw: + msg["tool_calls"] = [ + { + "id": tc["id"], + "type": "function", + "function": { + "name": tc["name"], + "arguments": json.dumps(tc.get("arguments") or {}), + }, + # Gemini-only: opaque signature that must be echoed back on + # the same functionCall part in the next turn. Other providers + # do not set or read this. + **( + {"thought_signature": tc["thought_signature"]} + if tc.get("thought_signature") + else {} + ), + } + for tc in tool_calls_raw + ] + return msg diff --git a/frigate/images/birdseye.png b/frigate/images/birdseye.png new file mode 100644 index 0000000..3f7f768 Binary files /dev/null and b/frigate/images/birdseye.png differ diff --git a/frigate/images/camera-error.jpg b/frigate/images/camera-error.jpg new file mode 100644 index 0000000..ace648f Binary files /dev/null and b/frigate/images/camera-error.jpg differ diff --git a/frigate/jobs/__init__.py b/frigate/jobs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/jobs/debug_replay.py b/frigate/jobs/debug_replay.py new file mode 100644 index 0000000..c5e4ed8 --- /dev/null +++ b/frigate/jobs/debug_replay.py @@ -0,0 +1,493 @@ +"""Debug replay startup job: ffmpeg remux + camera config publish. + +The runner orchestrates the async portion of starting a debug replay +session. The DebugReplayManager (in frigate.debug_replay) owns session +presence so the status bar can keep reading a single `active` flag from +/debug_replay/status for the entire session window — which is broader +than this job's lifetime. +""" + +import logging +import os +import subprocess as sp +import threading +import time +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Optional, cast + +from peewee import ModelSelect + +from frigate.config import FrigateConfig +from frigate.config.camera.updater import CameraConfigUpdatePublisher +from frigate.const import REPLAY_CAMERA_PREFIX, REPLAY_DIR +from frigate.jobs.export import JobStatePublisher +from frigate.jobs.job import Job +from frigate.jobs.manager import job_is_running, set_current_job +from frigate.models import Export, Recordings +from frigate.types import JobStatusTypesEnum +from frigate.util.ffmpeg import run_ffmpeg_with_progress + +if TYPE_CHECKING: + from frigate.debug_replay import DebugReplayManager + +logger = logging.getLogger(__name__) + +# Coalesce frequent ffmpeg progress callbacks so the WS isn't flooded. +PROGRESS_BROADCAST_MIN_INTERVAL = 1.0 + +JOB_TYPE = "debug_replay" + +STEP_PREPARING_CLIP = "preparing_clip" +STEP_STARTING_CAMERA = "starting_camera" + + +_active_runner: Optional["DebugReplayJobRunner"] = None +_runner_lock = threading.Lock() + + +def _set_active_runner(runner: Optional["DebugReplayJobRunner"]) -> None: + global _active_runner + with _runner_lock: + _active_runner = runner + + +def get_active_runner() -> Optional["DebugReplayJobRunner"]: + with _runner_lock: + return _active_runner + + +@dataclass +class DebugReplayJob(Job): + """Job state for a debug replay startup.""" + + job_type: str = JOB_TYPE + source_camera: str = "" + replay_camera_name: str = "" + start_ts: float = 0.0 + end_ts: float = 0.0 + current_step: str | None = None + progress_percent: float = 0.0 + + def to_dict(self) -> dict[str, Any]: + """Whitelisted payload for the job_state WS topic. + + Replay-specific fields land in results so the frontend's + generic Job type can be parameterised cleanly. + """ + return { + "id": self.id, + "job_type": self.job_type, + "status": self.status, + "start_time": self.start_time, + "end_time": self.end_time, + "error_message": self.error_message, + "results": { + "current_step": self.current_step, + "progress_percent": self.progress_percent, + "source_camera": self.source_camera, + "replay_camera_name": self.replay_camera_name, + "start_ts": self.start_ts, + "end_ts": self.end_ts, + }, + } + + +def query_recordings(source_camera: str, start_ts: float, end_ts: float) -> ModelSelect: + """Return the Recordings query for the time range. + + Module-level so tests can patch it without instantiating a runner. + """ + query = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + Recordings.end_time, + ) + .where( + Recordings.start_time.between(start_ts, end_ts) + | Recordings.end_time.between(start_ts, end_ts) + | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) + ) + .where(Recordings.camera == source_camera) + .order_by(Recordings.start_time.asc()) + ) + return cast(ModelSelect, query) + + +class DebugReplaySource(ABC): + """Abstract source for a debug replay session. + + Provides the camera identity and time range the replay represents, + validates that usable content exists, and supplies the ffmpeg input + args used to build the replay clip. + """ + + @property + @abstractmethod + def source_camera(self) -> str: + """Camera name the replay is derived from.""" + + @property + @abstractmethod + def start_ts(self) -> float: + """Unix timestamp marking the start of the replay range.""" + + @property + @abstractmethod + def end_ts(self) -> float: + """Unix timestamp marking the end of the replay range.""" + + @abstractmethod + def validate(self) -> None: + """Raise ValueError if the source has no usable content.""" + + @abstractmethod + def ffmpeg_input_args(self, working_dir: str) -> list[str]: + """Return ffmpeg input args (including -i). May write temp files in working_dir.""" + + def cleanup(self, working_dir: str) -> None: + """Remove any temp files the source created in working_dir. Default no-op.""" + + +class RecordingDebugReplaySource(DebugReplaySource): + """Replay source backed by the Recordings table. + + Feeds ffmpeg the internal VOD endpoint so segments with mismatched + SPS/PPS (e.g. across day/night transitions) stitch cleanly via HLS + discontinuities. + """ + + def __init__( + self, + source_camera: str, + start_ts: float, + end_ts: float, + internal_port: int, + ) -> None: + self._camera = source_camera + self._start_ts = start_ts + self._end_ts = end_ts + self._internal_port = internal_port + + @property + def source_camera(self) -> str: + return self._camera + + @property + def start_ts(self) -> float: + return self._start_ts + + @property + def end_ts(self) -> float: + return self._end_ts + + def validate(self) -> None: + if self._end_ts <= self._start_ts: + raise ValueError("End time must be after start time") + + if not query_recordings(self._camera, self._start_ts, self._end_ts).count(): + raise ValueError( + f"No recordings found for camera '{self._camera}' in the specified time range" + ) + + def ffmpeg_input_args(self, working_dir: str) -> list[str]: + playlist_url = ( + f"http://127.0.0.1:{self._internal_port}/vod/{self._camera}" + f"/start/{self._start_ts}/end/{self._end_ts}/index.m3u8" + ) + return [ + "-protocol_whitelist", + "pipe,file,http,tcp", + "-i", + playlist_url, + ] + + +class ExportDebugReplaySource(DebugReplaySource): + """Replay source backed by an existing Export. + + Uses the export's video file directly as the ffmpeg input — does not + require recordings to still exist for the time range. + """ + + def __init__(self, export: Export, duration: float) -> None: + self._camera = cast(str, export.camera) + # Export.date is declared DateTimeField but Frigate writes raw unix + # timestamps to the column. + self._start_ts = float(cast(Any, export.date)) + self._video_path = cast(str, export.video_path) + self._duration = duration + + @property + def source_camera(self) -> str: + return self._camera + + @property + def start_ts(self) -> float: + return self._start_ts + + @property + def end_ts(self) -> float: + return self._start_ts + self._duration + + def validate(self) -> None: + if not os.path.exists(self._video_path): + raise ValueError(f"Export video file not found: {self._video_path}") + + def ffmpeg_input_args(self, working_dir: str) -> list[str]: + return ["-i", self._video_path] + + +class DebugReplayJobRunner(threading.Thread): + """Worker thread that drives the startup job to completion. + + Owns the live ffmpeg Popen reference for cancellation. Cancellation + is two-step (threading.Event + proc.terminate()) so the runner + both knows it should stop and is unblocked from its blocking subprocess + wait. + """ + + def __init__( + self, + job: DebugReplayJob, + source: DebugReplaySource, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + replay_manager: "DebugReplayManager", + publisher: JobStatePublisher | None = None, + ) -> None: + super().__init__(daemon=True, name=f"debug_replay_{job.id}") + self.job = job + self.source = source + self.frigate_config = frigate_config + self.config_publisher = config_publisher + self.replay_manager = replay_manager + self.publisher = publisher if publisher is not None else JobStatePublisher() + self._cancel_event = threading.Event() + self._active_process: sp.Popen | None = None + self._proc_lock = threading.Lock() + self._last_broadcast_monotonic: float = 0.0 + + def cancel(self) -> None: + """Request cancellation. Idempotent.""" + self._cancel_event.set() + with self._proc_lock: + proc = self._active_process + if proc is not None: + try: + proc.terminate() + except Exception as exc: + logger.warning("Failed to terminate ffmpeg subprocess: %s", exc) + + def is_cancelled(self) -> bool: + return self._cancel_event.is_set() + + def _record_proc(self, proc: sp.Popen) -> None: + with self._proc_lock: + self._active_process = proc + # Race: cancel arrived between Popen and _record_proc. + if self._cancel_event.is_set(): + try: + proc.terminate() + except Exception: + pass + + def _broadcast(self, force: bool = False) -> None: + now = time.monotonic() + if ( + not force + and now - self._last_broadcast_monotonic < PROGRESS_BROADCAST_MIN_INTERVAL + ): + return + self._last_broadcast_monotonic = now + + try: + self.publisher.publish(self.job.to_dict()) + except Exception as err: + logger.warning("Publisher raised during job state broadcast: %s", err) + + def run(self) -> None: + replay_name = self.job.replay_camera_name + os.makedirs(REPLAY_DIR, exist_ok=True) + clip_path = os.path.join(REPLAY_DIR, f"{replay_name}.mp4") + + self.job.status = JobStatusTypesEnum.running + self.job.start_time = time.time() + self.job.current_step = STEP_PREPARING_CLIP + self._broadcast(force=True) + + try: + input_args = self.source.ffmpeg_input_args(REPLAY_DIR) + + ffmpeg_cmd = [ + self.frigate_config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-y", + *input_args, + "-c", + "copy", + "-movflags", + "+faststart", + clip_path, + ] + + logger.info( + "Generating replay clip for %s (%.1f - %.1f)", + self.job.source_camera, + self.job.start_ts, + self.job.end_ts, + ) + + def _on_progress(percent: float) -> None: + self.job.progress_percent = percent + self._broadcast() + + try: + returncode, stderr = run_ffmpeg_with_progress( + ffmpeg_cmd, + expected_duration_seconds=max( + 0.0, self.job.end_ts - self.job.start_ts + ), + on_progress=_on_progress, + process_started=self._record_proc, + use_low_priority=True, + ) + finally: + with self._proc_lock: + self._active_process = None + + if self._cancel_event.is_set(): + self._finalize_cancelled(clip_path) + return + + if returncode != 0: + raise RuntimeError(f"FFmpeg failed: {stderr[-500:]}") + + if not os.path.exists(clip_path): + raise RuntimeError("Clip file was not created") + + self.job.current_step = STEP_STARTING_CAMERA + self.job.progress_percent = 100.0 + self._broadcast(force=True) + + if self._cancel_event.is_set(): + self._finalize_cancelled(clip_path) + return + + self.replay_manager.publish_camera( + source_camera=self.job.source_camera, + replay_name=replay_name, + clip_path=clip_path, + frigate_config=self.frigate_config, + config_publisher=self.config_publisher, + ) + self.replay_manager.mark_session_ready(clip_path) + + self.job.status = JobStatusTypesEnum.success + self.job.end_time = time.time() + self._broadcast(force=True) + logger.info( + "Debug replay started: %s -> %s", + self.job.source_camera, + replay_name, + ) + except Exception as exc: + logger.exception("Debug replay startup failed") + self.job.status = JobStatusTypesEnum.failed + self.job.error_message = str(exc) + self.job.end_time = time.time() + self._broadcast(force=True) + self.replay_manager.clear_session() + _remove_silent(clip_path) + finally: + self.source.cleanup(REPLAY_DIR) + _set_active_runner(None) + + def _finalize_cancelled(self, clip_path: str) -> None: + logger.info("Debug replay startup cancelled") + self.job.status = JobStatusTypesEnum.cancelled + self.job.end_time = time.time() + self._broadcast(force=True) + # The caller of cancel_debug_replay_job (DebugReplayManager.stop) owns + # session cleanup — db rows, filesystem artifacts, clear_session. We + # only clean up the partial concat output we created. + _remove_silent(clip_path) + + +def _remove_silent(path: str) -> None: + try: + if os.path.exists(path): + os.remove(path) + except OSError: + pass + + +def start_debug_replay_job( + *, + source: DebugReplaySource, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + replay_manager: "DebugReplayManager", +) -> str: + """Validate, create job, start runner. Returns the job id. + + Raises ValueError for an invalid source (camera missing, source has + no usable content) and RuntimeError if a session is already active. + """ + if job_is_running(JOB_TYPE) or replay_manager.active: + raise RuntimeError("A replay session is already active") + + if source.source_camera not in frigate_config.cameras: + raise ValueError(f"Camera '{source.source_camera}' not found") + + source.validate() + + replay_name = f"{REPLAY_CAMERA_PREFIX}{source.source_camera}" + replay_manager.mark_starting( + source_camera=source.source_camera, + replay_camera_name=replay_name, + start_ts=source.start_ts, + end_ts=source.end_ts, + ) + + job = DebugReplayJob( + source_camera=source.source_camera, + replay_camera_name=replay_name, + start_ts=source.start_ts, + end_ts=source.end_ts, + ) + set_current_job(job) + + runner = DebugReplayJobRunner( + job=job, + source=source, + frigate_config=frigate_config, + config_publisher=config_publisher, + replay_manager=replay_manager, + ) + _set_active_runner(runner) + runner.start() + + return job.id + + +def cancel_debug_replay_job() -> bool: + """Signal the active runner to cancel. + + Returns True if a runner was signalled, False if no job was active. + """ + runner = get_active_runner() + if runner is None: + return False + runner.cancel() + return True + + +def wait_for_runner(timeout: float = 2.0) -> bool: + """Join the active runner. Returns True if the runner ended in time.""" + runner = get_active_runner() + if runner is None: + return True + runner.join(timeout=timeout) + return not runner.is_alive() diff --git a/frigate/jobs/export.py b/frigate/jobs/export.py new file mode 100644 index 0000000..1c88ec5 --- /dev/null +++ b/frigate/jobs/export.py @@ -0,0 +1,508 @@ +"""Export job management with queued background execution.""" + +import logging +import os +import threading +import time +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path +from queue import Full, Queue +from typing import Any + +from peewee import DoesNotExist + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.config.camera.record import ChaptersEnum +from frigate.const import UPDATE_JOB_STATE +from frigate.jobs.job import Job +from frigate.models import Export +from frigate.record.export import PlaybackSourceEnum, RecordingExporter +from frigate.types import JobStatusTypesEnum + +logger = logging.getLogger(__name__) + +# Maximum number of jobs that can sit in the queue waiting to run. +# Prevents a runaway client from unbounded memory growth. +MAX_QUEUED_EXPORT_JOBS = 100 + +# Minimum interval between progress broadcasts. FFmpeg can emit progress +# events many times per second; we coalesce them so the WebSocket isn't +# flooded with redundant updates. +PROGRESS_BROADCAST_MIN_INTERVAL = 1.0 + +# Delay before removing a completed job from the in-memory map. Gives the +# frontend a chance to receive the final state via WebSocket before SWR +# polling takes over. +COMPLETED_JOB_CLEANUP_DELAY = 5.0 + + +class ExportQueueFullError(RuntimeError): + """Raised when the export queue is at capacity.""" + + +@dataclass +class ExportJob(Job): + """Job state for export operations.""" + + job_type: str = "export" + camera: str = "" + name: str | None = None + image_path: str | None = None + export_case_id: str | None = None + request_start_time: float = 0.0 + request_end_time: float = 0.0 + playback_source: str = PlaybackSourceEnum.recordings.value + ffmpeg_input_args: str | None = None + ffmpeg_output_args: str | None = None + cpu_fallback: bool = False + chapters: ChaptersEnum | None = None + current_step: str = "queued" + progress_percent: float = 0.0 + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for API responses. + + Only exposes fields that are part of the public ExportJobModel schema. + Internal execution details (image_path, ffmpeg args, cpu_fallback) are + intentionally omitted so they don't leak through the API. + """ + return { + "id": self.id, + "job_type": self.job_type, + "status": self.status, + "camera": self.camera, + "name": self.name, + "export_case_id": self.export_case_id, + "request_start_time": self.request_start_time, + "request_end_time": self.request_end_time, + "start_time": self.start_time, + "end_time": self.end_time, + "error_message": self.error_message, + "results": self.results, + "current_step": self.current_step, + "progress_percent": self.progress_percent, + } + + +class ExportQueueWorker(threading.Thread): + """Worker that executes queued exports.""" + + def __init__(self, manager: "ExportJobManager", worker_index: int) -> None: + super().__init__( + daemon=True, + name=f"export_queue_worker_{worker_index}", + ) + self.manager = manager + + def run(self) -> None: + while True: + job = self.manager.queue.get() + + try: + self.manager.run_job(job) + except Exception: + logger.exception( + "Export queue worker failed while processing %s", job.id + ) + finally: + self.manager.queue.task_done() + + +class JobStatePublisher: + """Publishes a single job state payload to the dispatcher. + + Each call opens a short-lived :py:class:`InterProcessRequestor`, sends + the payload, and closes the socket. The short-lived design avoids + REQ/REP state corruption that would arise from sharing a single REQ + socket across the API thread and worker threads (REQ sockets must + strictly alternate send/recv). + + With the 1s broadcast throttle in place, socket creation overhead is + negligible. The class also exists so tests can substitute a no-op + instance instead of stubbing ZMQ — see ``BaseTestHttp.setUp``. + """ + + def publish(self, payload: dict[str, Any]) -> None: + try: + requestor = InterProcessRequestor() + except Exception as err: + logger.warning("Failed to open job state requestor: %s", err) + return + + try: + requestor.send_data(UPDATE_JOB_STATE, payload) + except Exception as err: + logger.debug("Job state broadcast failed: %s", err) + finally: + try: + requestor.stop() + except Exception: + pass + + +class ExportJobManager: + """Concurrency-limited manager for queued export jobs.""" + + def __init__( + self, + config: FrigateConfig, + max_concurrent: int, + max_queued: int = MAX_QUEUED_EXPORT_JOBS, + publisher: JobStatePublisher | None = None, + ) -> None: + self.config = config + self.max_concurrent = max(1, max_concurrent) + self.queue: Queue[ExportJob] = Queue(maxsize=max(1, max_queued)) + self.jobs: dict[str, ExportJob] = {} + self.lock = threading.Lock() + self.workers: list[ExportQueueWorker] = [] + self.started = False + self.publisher = publisher if publisher is not None else JobStatePublisher() + self._last_broadcast_monotonic: float = 0.0 + self._broadcast_throttle_lock = threading.Lock() + + def _broadcast_all_jobs(self, force: bool = False) -> None: + """Publish aggregate export job state via the job_state WS topic. + + When ``force`` is False, broadcasts within + ``PROGRESS_BROADCAST_MIN_INTERVAL`` of the previous one are skipped + to avoid flooding the WebSocket with rapid progress updates. + ``force`` bypasses the throttle and is used for status transitions + (enqueue/start/finish) where the frontend needs the latest state. + """ + now = time.monotonic() + with self._broadcast_throttle_lock: + if ( + not force + and now - self._last_broadcast_monotonic + < PROGRESS_BROADCAST_MIN_INTERVAL + ): + return + self._last_broadcast_monotonic = now + + with self.lock: + active = [ + j + for j in self.jobs.values() + if j.status in (JobStatusTypesEnum.queued, JobStatusTypesEnum.running) + ] + + any_running = any(j.status == JobStatusTypesEnum.running for j in active) + payload: dict[str, Any] = { + "job_type": "export", + "status": "running" if any_running else "queued", + "results": {"jobs": [j.to_dict() for j in active]}, + } + + try: + self.publisher.publish(payload) + except Exception as err: + logger.warning("Publisher raised during job state broadcast: %s", err) + + def _make_progress_callback(self, job: ExportJob) -> Callable[[str, float], None]: + """Build a callback the exporter can invoke during execution.""" + + def on_progress(step: str, percent: float) -> None: + job.current_step = step + job.progress_percent = percent + self._broadcast_all_jobs() + + return on_progress + + def _schedule_job_cleanup(self, job_id: str) -> None: + """Drop a completed job from ``self.jobs`` after a short delay.""" + + def cleanup() -> None: + with self.lock: + self.jobs.pop(job_id, None) + + timer = threading.Timer(COMPLETED_JOB_CLEANUP_DELAY, cleanup) + timer.daemon = True + timer.start() + + def ensure_started(self) -> None: + """Ensure worker threads are started exactly once.""" + with self.lock: + if self.started: + self._restart_dead_workers_locked() + return + + for index in range(self.max_concurrent): + worker = ExportQueueWorker(self, index) + worker.start() + self.workers.append(worker) + + self.started = True + + def _restart_dead_workers_locked(self) -> None: + for index, worker in enumerate(self.workers): + if worker.is_alive(): + continue + + logger.error( + "Export queue worker %s died unexpectedly, restarting", worker.name + ) + replacement = ExportQueueWorker(self, index) + replacement.start() + self.workers[index] = replacement + + def enqueue(self, job: ExportJob) -> str: + """Queue a job for background execution. + + Raises ExportQueueFullError if the queue is at capacity. + """ + self.ensure_started() + + try: + self.queue.put_nowait(job) + except Full as err: + raise ExportQueueFullError( + "Export queue is full; try again once current exports finish" + ) from err + + with self.lock: + self.jobs[job.id] = job + + self._broadcast_all_jobs(force=True) + + return job.id + + def get_job(self, job_id: str) -> ExportJob | None: + """Get a job by ID.""" + with self.lock: + return self.jobs.get(job_id) + + def list_active_jobs(self) -> list[ExportJob]: + """List queued and running jobs.""" + with self.lock: + return [ + job + for job in self.jobs.values() + if job.status in (JobStatusTypesEnum.queued, JobStatusTypesEnum.running) + ] + + def cancel_queued_jobs_for_case(self, case_id: str) -> list[ExportJob]: + """Cancel queued export jobs assigned to a deleted case.""" + cancelled_jobs: list[ExportJob] = [] + + with self.lock: + with self.queue.mutex: + retained_jobs: list[ExportJob] = [] + + while self.queue.queue: + job = self.queue.queue.popleft() + + if ( + job.export_case_id == case_id + and job.status == JobStatusTypesEnum.queued + ): + job.status = JobStatusTypesEnum.cancelled + job.end_time = time.time() + cancelled_jobs.append(job) + continue + + retained_jobs.append(job) + + self.queue.queue.extend(retained_jobs) + + if cancelled_jobs: + self.queue.unfinished_tasks = max( + 0, + self.queue.unfinished_tasks - len(cancelled_jobs), + ) + if self.queue.unfinished_tasks == 0: + self.queue.all_tasks_done.notify_all() + self.queue.not_full.notify_all() + + return cancelled_jobs + + def available_slots(self) -> int: + """Approximate number of additional jobs that could be queued right now. + + Uses Queue.qsize() which is best-effort; callers should treat the + result as advisory since another thread could enqueue between + checking and enqueueing. + """ + return max(0, self.queue.maxsize - self.queue.qsize()) + + def run_job(self, job: ExportJob) -> None: + """Execute a queued export job.""" + job.status = JobStatusTypesEnum.running + job.start_time = time.time() + self._broadcast_all_jobs(force=True) + + exporter = RecordingExporter( + self.config, + job.id, + job.camera, + job.name, + job.image_path, + int(job.request_start_time), + int(job.request_end_time), + PlaybackSourceEnum(job.playback_source), + job.export_case_id, + job.ffmpeg_input_args, + job.ffmpeg_output_args, + job.cpu_fallback, + job.chapters, + on_progress=self._make_progress_callback(job), + ) + + try: + exporter.run() + export = Export.get_or_none(Export.id == job.id) + if export is None: + job.status = JobStatusTypesEnum.failed + job.error_message = "Export failed" + elif export.in_progress: + job.status = JobStatusTypesEnum.failed + job.error_message = "Export did not complete" + else: + job.status = JobStatusTypesEnum.success + job.results = { + "export_id": export.id, + "export_case_id": export.export_case_id, + "video_path": export.video_path, + "thumb_path": export.thumb_path, + } + except DoesNotExist: + job.status = JobStatusTypesEnum.failed + job.error_message = "Export not found" + except Exception as err: + logger.exception("Export job %s failed: %s", job.id, err) + job.status = JobStatusTypesEnum.failed + job.error_message = str(err) + finally: + job.end_time = time.time() + self._broadcast_all_jobs(force=True) + self._schedule_job_cleanup(job.id) + + +_job_manager: ExportJobManager | None = None +_job_manager_lock = threading.Lock() + + +def _get_max_concurrent(config: FrigateConfig) -> int: + return int(config.record.export.max_concurrent) + + +def reap_stale_exports() -> None: + """Sweep Export rows stuck with in_progress=True from previous sessions. + + On Frigate startup no export job is alive yet, so any in_progress=True + row must be a leftover from a previous session that crashed, was killed + mid-export, or returned early from RecordingExporter.run() without + flipping the flag. For each stale row we either: + + - delete the row (and any thumb) if the video file is missing or empty, + since there is nothing worth recovering + - flip in_progress to False if the video file exists on disk and is + non-empty, treating it as a completed export the user can manage + through the normal UI + + Must only be called when the export job manager is certain to have no + active jobs — i.e., at Frigate startup, before any worker runs. + + All exceptions are caught and logged; the caller does not need to wrap + this in a try/except. A failure on a single row will not stop the rest + of the sweep, and a failure in the top-level query will log and return. + """ + try: + stale_exports = list(Export.select().where(Export.in_progress == True)) # noqa: E712 + except Exception: + logger.exception("Failed to query stale in-progress exports") + return + + if not stale_exports: + logger.debug("No stale in-progress exports found on startup") + return + + flipped = 0 + deleted = 0 + errored = 0 + + for export in stale_exports: + try: + video_path = export.video_path + has_usable_file = False + + if video_path: + try: + has_usable_file = os.path.getsize(video_path) > 0 + except OSError: + has_usable_file = False + + if has_usable_file: + # Unassign from any case on recovery: the user should + # re-triage a recovered export rather than have it silently + # reappear inside a case they curated. + Export.update( + {Export.in_progress: False, Export.export_case: None} + ).where(Export.id == export.id).execute() + flipped += 1 + logger.info( + "Recovered stale in-progress export %s (file intact on disk)", + export.id, + ) + continue + + if export.thumb_path: + Path(export.thumb_path).unlink(missing_ok=True) + if video_path: + Path(video_path).unlink(missing_ok=True) + Export.delete().where(Export.id == export.id).execute() + deleted += 1 + logger.info( + "Deleted stale in-progress export %s (no usable file on disk)", + export.id, + ) + except Exception: + errored += 1 + logger.exception("Failed to reap stale export %s", export.id) + + logger.info( + "Stale export cleanup complete: %d recovered, %d deleted, %d errored", + flipped, + deleted, + errored, + ) + + +def get_export_job_manager(config: FrigateConfig) -> ExportJobManager: + """Get or create the singleton export job manager.""" + global _job_manager + + with _job_manager_lock: + if _job_manager is None: + _job_manager = ExportJobManager(config, _get_max_concurrent(config)) + _job_manager.ensure_started() + return _job_manager + + +def start_export_job(config: FrigateConfig, job: ExportJob) -> str: + """Queue an export job and return its ID.""" + return get_export_job_manager(config).enqueue(job) + + +def get_export_job(config: FrigateConfig, job_id: str) -> ExportJob | None: + """Get a queued or completed export job by ID.""" + return get_export_job_manager(config).get_job(job_id) + + +def list_active_export_jobs(config: FrigateConfig) -> list[ExportJob]: + """List queued and running export jobs.""" + return get_export_job_manager(config).list_active_jobs() + + +def cancel_queued_export_jobs_for_case( + config: FrigateConfig, case_id: str +) -> list[ExportJob]: + """Cancel queued export jobs that still point at a deleted case.""" + return get_export_job_manager(config).cancel_queued_jobs_for_case(case_id) + + +def available_export_queue_slots(config: FrigateConfig) -> int: + """Approximate number of additional export jobs that could be queued now.""" + return get_export_job_manager(config).available_slots() diff --git a/frigate/jobs/job.py b/frigate/jobs/job.py new file mode 100644 index 0000000..c40087d --- /dev/null +++ b/frigate/jobs/job.py @@ -0,0 +1,21 @@ +"""Generic base class for long-running background jobs.""" + +from dataclasses import asdict, dataclass, field +from typing import Any + + +@dataclass +class Job: + """Base class for long-running background jobs.""" + + id: str = field(default_factory=lambda: __import__("uuid").uuid4().__str__()[:12]) + job_type: str = "" # Must be set by subclasses + status: str = "queued" # queued, running, success, failed, cancelled + results: dict[str, Any] | None = None + start_time: float | None = None + end_time: float | None = None + error_message: str | None = None + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for WebSocket transmission.""" + return asdict(self) diff --git a/frigate/jobs/manager.py b/frigate/jobs/manager.py new file mode 100644 index 0000000..c2fb44a --- /dev/null +++ b/frigate/jobs/manager.py @@ -0,0 +1,69 @@ +"""Generic job management for long-running background tasks.""" + +import threading + +from frigate.jobs.job import Job +from frigate.types import JobStatusTypesEnum + +# Global state and locks for enforcing single concurrent job per job type +_job_locks: dict[str, threading.Lock] = {} +_current_jobs: dict[str, Job | None] = {} +# Keep completed jobs for retrieval, keyed by (job_type, job_id) +_completed_jobs: dict[tuple[str, str], Job] = {} + + +def _get_lock(job_type: str) -> threading.Lock: + """Get or create a lock for the specified job type.""" + if job_type not in _job_locks: + _job_locks[job_type] = threading.Lock() + return _job_locks[job_type] + + +def set_current_job(job: Job) -> None: + """Set the current job for a given job type.""" + lock = _get_lock(job.job_type) + with lock: + # Store the previous job if it was completed + old_job = _current_jobs.get(job.job_type) + if old_job and old_job.status in ( + JobStatusTypesEnum.success, + JobStatusTypesEnum.failed, + JobStatusTypesEnum.cancelled, + ): + _completed_jobs[(job.job_type, old_job.id)] = old_job + _current_jobs[job.job_type] = job + + +def clear_current_job(job_type: str, job_id: str | None = None) -> None: + """Clear the current job for a given job type, optionally checking the ID.""" + lock = _get_lock(job_type) + with lock: + if job_type in _current_jobs: + current = _current_jobs[job_type] + if current is None or (job_id is None or current.id == job_id): + _current_jobs[job_type] = None + + +def get_current_job(job_type: str) -> Job | None: + """Get the current running/queued job for a given job type, if any.""" + lock = _get_lock(job_type) + with lock: + return _current_jobs.get(job_type) + + +def get_job_by_id(job_type: str, job_id: str) -> Job | None: + """Get job by ID. Checks current job first, then completed jobs.""" + lock = _get_lock(job_type) + with lock: + # Check if it's the current job + current = _current_jobs.get(job_type) + if current and current.id == job_id: + return current + # Check if it's a completed job + return _completed_jobs.get((job_type, job_id)) + + +def job_is_running(job_type: str) -> bool: + """Check if a job of the given type is currently running or queued.""" + job = get_current_job(job_type) + return job is not None and job.status in ("queued", "running") diff --git a/frigate/jobs/media_sync.py b/frigate/jobs/media_sync.py new file mode 100644 index 0000000..1cd8209 --- /dev/null +++ b/frigate/jobs/media_sync.py @@ -0,0 +1,154 @@ +"""Media sync job management with background execution.""" + +import logging +import os +import threading +from dataclasses import dataclass, field +from datetime import datetime +from typing import cast + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import CONFIG_DIR, UPDATE_JOB_STATE +from frigate.jobs.job import Job +from frigate.jobs.manager import ( + get_current_job, + get_job_by_id, + job_is_running, + set_current_job, +) +from frigate.types import JobStatusTypesEnum +from frigate.util.media import sync_all_media, write_orphan_report + +logger = logging.getLogger(__name__) + + +@dataclass +class MediaSyncJob(Job): + """In-memory job state for media sync operations.""" + + job_type: str = "media_sync" + dry_run: bool = False + media_types: list[str] = field(default_factory=lambda: ["all"]) + force: bool = False + verbose: bool = False + + +class MediaSyncRunner(threading.Thread): + """Thread-based runner for media sync jobs.""" + + def __init__(self, job: MediaSyncJob) -> None: + super().__init__(daemon=True, name="media_sync") + self.job = job + self.requestor = InterProcessRequestor() + + def run(self) -> None: + """Execute the media sync job and broadcast status updates.""" + try: + # Update job status to running + self.job.status = JobStatusTypesEnum.running + self.job.start_time = datetime.now().timestamp() + self._broadcast_status() + + # Execute sync with provided parameters + logger.debug( + f"Starting media sync job {self.job.id}: " + f"media_types={self.job.media_types}, " + f"dry_run={self.job.dry_run}, " + f"force={self.job.force}" + ) + + results = sync_all_media( + dry_run=self.job.dry_run, + media_types=self.job.media_types, + force=self.job.force, + ) + + # Write verbose report if requested + if self.job.verbose: + report_dir = os.path.join(CONFIG_DIR, "media_sync") + os.makedirs(report_dir, exist_ok=True) + report_path = os.path.join(report_dir, f"{self.job.id}.txt") + write_orphan_report( + results, + report_path, + job_id=self.job.id, + dry_run=self.job.dry_run, + ) + logger.info( + "Media sync verbose orphan report written to %s", report_path + ) + + # Store results and mark as complete + self.job.results = results.to_dict() + self.job.status = JobStatusTypesEnum.success + self.job.end_time = datetime.now().timestamp() + + logger.debug(f"Media sync job {self.job.id} completed successfully") + self._broadcast_status() + + except Exception as e: + logger.exception(f"Media sync job {self.job.id} failed: {e}") + self.job.status = JobStatusTypesEnum.failed + self.job.error_message = str(e) + self.job.end_time = datetime.now().timestamp() + self._broadcast_status() + + finally: + if self.requestor: + self.requestor.stop() + + def _broadcast_status(self) -> None: + """Broadcast job status update via IPC to all WebSocket subscribers.""" + try: + self.requestor.send_data( + UPDATE_JOB_STATE, + self.job.to_dict(), + ) + except Exception as e: + logger.warning(f"Failed to broadcast media sync status: {e}") + + +def start_media_sync_job( + dry_run: bool = False, + media_types: list[str] | None = None, + force: bool = False, + verbose: bool = False, +) -> str | None: + """Start a new media sync job if none is currently running. + + Returns job ID on success, None if job already running. + """ + # Check if a job is already running + if job_is_running("media_sync"): + current = get_current_job("media_sync") + logger.warning( + f"Media sync job {current.id if current else 'unknown'} is already running. Rejecting new request." + ) + return None + + # Create and start new job + job = MediaSyncJob( + dry_run=dry_run, + media_types=media_types or ["all"], + force=force, + verbose=verbose, + ) + + logger.debug(f"Creating new media sync job: {job.id}") + set_current_job(job) + + # Start the background runner + runner = MediaSyncRunner(job) + runner.start() + + return job.id + + +def get_current_media_sync_job() -> MediaSyncJob | None: + """Get the current running/queued media sync job, if any.""" + return cast(MediaSyncJob | None, get_current_job("media_sync")) + + +def get_media_sync_job_by_id(job_id: str) -> MediaSyncJob | None: + """Get media sync job by ID. Currently only tracks the current job.""" + return cast(MediaSyncJob | None, get_job_by_id("media_sync", job_id)) diff --git a/frigate/jobs/motion_search.py b/frigate/jobs/motion_search.py new file mode 100644 index 0000000..15cd104 --- /dev/null +++ b/frigate/jobs/motion_search.py @@ -0,0 +1,957 @@ +"""Motion search job management with background execution and parallel verification.""" + +import logging +import os +import threading +import time +from collections.abc import Callable, Generator, Iterable +from concurrent.futures import Future, ThreadPoolExecutor, as_completed +from dataclasses import asdict, dataclass, field +from datetime import datetime +from typing import Any, cast + +import cv2 +import numpy as np + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import UPDATE_JOB_STATE +from frigate.jobs.job import Job +from frigate.jobs.manager import ( + get_job_by_id, + set_current_job, +) +from frigate.jobs.motion_search_batch import ( + build_segment_time_map, + coalesce_runs, + stream_time_to_absolute, +) +from frigate.jobs.motion_search_decode import ( + iter_vod_frames, + keyframe_sampling_eligible, + probe_video_dimensions, + probe_vod_keyframe_pts, + resolve_motion_decode_args, +) +from frigate.models import Recordings +from frigate.types import JobStatusTypesEnum + +logger = logging.getLogger(__name__) + +# Constants +HEATMAP_GRID_SIZE = 16 +# Max wall-clock span of one VOD run request (seconds). Bounds per-request size +# and gives streaming/cancel/early-exit granularity. +MAX_RUN_SECONDS = 600.0 +# Treat segments within this many seconds end-to-start as time-contiguous. +RUN_GAP_EPSILON = 1.0 +# Longest-side pixels for the ROI downscale before motion detection. +SCALE_TARGET = 400 +# Minimum wall seconds between intra-run progress broadcasts. +PROGRESS_BROADCAST_INTERVAL = 1.0 +# Output frame rate for the fixed-cadence fallback used on long-GOP cameras +# (where keyframe sampling is too sparse). Keyframe cameras ignore this. +FALLBACK_SAMPLE_FPS = 2.0 + + +@dataclass +class MotionSearchMetrics: + """Metrics collected during motion search execution.""" + + segments_scanned: int = 0 + segments_processed: int = 0 + metadata_inactive_segments: int = 0 + heatmap_roi_skip_segments: int = 0 + fallback_full_range_segments: int = 0 + frames_decoded: int = 0 + wall_time_seconds: float = 0.0 + segments_with_errors: int = 0 + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary.""" + return asdict(self) + + +@dataclass +class MotionSearchResult: + """A single search result with timestamp and change info.""" + + timestamp: float + change_percentage: float + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary.""" + return asdict(self) + + +@dataclass +class MotionSearchJob(Job): + """Job state for motion search operations.""" + + job_type: str = "motion_search" + camera: str = "" + start_time_range: float = 0.0 + end_time_range: float = 0.0 + polygon_points: list[list[float]] = field(default_factory=list) + threshold: int = 30 + min_area: float = 5.0 + parallel: bool = False + max_results: int = 25 + + # Track progress + total_frames_processed: int = 0 + + # Live progress (ride the existing to_dict() websocket broadcast) + scanning_timestamp: float | None = None + progress: float = 0.0 + + # Metrics for observability + metrics: MotionSearchMetrics | None = None + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for WebSocket transmission.""" + d = asdict(self) + if self.metrics: + d["metrics"] = self.metrics.to_dict() + return d + + +def create_polygon_mask( + polygon_points: list[list[float]], frame_width: int, frame_height: int +) -> np.ndarray: + """Create a binary mask from normalized polygon coordinates.""" + motion_points = np.array( + [[int(p[0] * frame_width), int(p[1] * frame_height)] for p in polygon_points], + dtype=np.int32, + ) + mask = np.zeros((frame_height, frame_width), dtype=np.uint8) + cv2.fillPoly(mask, [motion_points], (255,)) + return mask + + +def compute_roi_crop_and_scale( + polygon_points: list[list[float]], + frame_width: int, + frame_height: int, + scale_target: int, +) -> tuple[tuple[int, int, int, int], tuple[int, int]]: + """Compute the ROI crop box and never-upscale scaled dimensions. + + Returns ((crop_w, crop_h, crop_x, crop_y), (scaled_w, scaled_h)) in pixels. + The crop is the polygon's bounding box in frame pixels; the scaled size fits + the crop's longest side to ``scale_target`` without ever enlarging it. + """ + xs = [p[0] for p in polygon_points] + ys = [p[1] for p in polygon_points] + # nv12 (4:2:0) hwdownload requires even crop offsets and even crop/scale + # dimensions; otherwise ffmpeg rounds the chroma planes and the raw byte + # stream stops matching the expected frame size. Force even values, and the + # mask is built from these same values so the two stay aligned. + crop_x = int(min(xs) * frame_width) + crop_y = int(min(ys) * frame_height) + crop_x -= crop_x % 2 + crop_y -= crop_y % 2 + crop_w = max(2, int(max(xs) * frame_width) - crop_x) + crop_h = max(2, int(max(ys) * frame_height) - crop_y) + crop_w -= crop_w % 2 + crop_h -= crop_h % 2 + + longest = max(crop_w, crop_h) + factor = min(1.0, scale_target / longest) + scaled_w = max(2, round(crop_w * factor)) + scaled_h = max(2, round(crop_h * factor)) + scaled_w -= scaled_w % 2 + scaled_h -= scaled_h % 2 + return (crop_w, crop_h, crop_x, crop_y), (scaled_w, scaled_h) + + +def build_scaled_roi_mask( + polygon_points: list[list[float]], + frame_width: int, + frame_height: int, + crop: tuple[int, int, int, int], + scaled: tuple[int, int], +) -> np.ndarray: + """Rasterize the polygon mask at the scaled ROI size. + + Builds the full-resolution mask, crops it to the ROI box, and nearest- + neighbor resizes it to the scaled dimensions so it lines up exactly with the + frames ffmpeg crops and scales. + """ + crop_w, crop_h, crop_x, crop_y = crop + scaled_w, scaled_h = scaled + full_mask = create_polygon_mask(polygon_points, frame_width, frame_height) + cropped = full_mask[crop_y : crop_y + crop_h, crop_x : crop_x + crop_w] + return cv2.resize(cropped, (scaled_w, scaled_h), interpolation=cv2.INTER_NEAREST) + + +def detect_motion_scaled( + frames: Iterable[tuple[int, np.ndarray]], + mask: np.ndarray, + threshold: int, + min_area: float, + timestamp_fn: Callable[[int], float], +) -> list[MotionSearchResult]: + """Detect motion across pre-cropped, pre-scaled gray frames. + + ``frames`` yields (absolute_frame_index, gray_roi_frame); ``mask`` is the + scaled ROI mask. ``min_area`` is a percentage of the masked ROI. Mirrors the + full-res detection math (absdiff -> blur -> threshold -> dilate -> contours) + on the already-reduced frames. + """ + results: list[MotionSearchResult] = [] + mask_area = np.count_nonzero(mask) + if mask_area == 0: + return results + min_area_pixels = int((min_area / 100.0) * mask_area) + + prev: np.ndarray | None = None + for frame_idx, gray in frames: + masked = cv2.bitwise_and(gray, gray, mask=mask) + if prev is not None: + diff = cv2.absdiff(prev, masked) + diff_blurred = cv2.GaussianBlur(diff, (3, 3), 0) + _, thresh = cv2.threshold(diff_blurred, threshold, 255, cv2.THRESH_BINARY) + thresh_dilated = cv2.dilate(thresh, None, iterations=1) # type: ignore[call-overload] + thresh_masked = cv2.bitwise_and(thresh_dilated, thresh_dilated, mask=mask) + change_pixels = cv2.countNonZero(thresh_masked) + if change_pixels > min_area_pixels: + contours, _ = cv2.findContours( + thresh_masked, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + total_change_area = sum( + cv2.contourArea(c) + for c in contours + if cv2.contourArea(c) >= min_area_pixels + ) + if total_change_area > 0: + change_percentage = (total_change_area / mask_area) * 100 + results.append( + MotionSearchResult( + timestamp=timestamp_fn(frame_idx), + change_percentage=round(change_percentage, 2), + ) + ) + prev = masked + return results + + +def compute_roi_bbox_normalized( + polygon_points: list[list[float]], +) -> tuple[float, float, float, float]: + """Compute the bounding box of the ROI in normalized coordinates (0-1). + + Returns (x_min, y_min, x_max, y_max) in normalized coordinates. + """ + if not polygon_points: + return (0.0, 0.0, 1.0, 1.0) + + x_coords = [p[0] for p in polygon_points] + y_coords = [p[1] for p in polygon_points] + return (min(x_coords), min(y_coords), max(x_coords), max(y_coords)) + + +def heatmap_overlaps_roi( + heatmap: object, roi_bbox: tuple[float, float, float, float] +) -> bool: + """Check if a sparse motion heatmap has any overlap with the ROI bounding box. + + Args: + heatmap: Sparse dict mapping cell index (str) to intensity (1-255). + roi_bbox: (x_min, y_min, x_max, y_max) in normalized coordinates (0-1). + + Returns: + True if there is overlap (any active cell in the ROI region). + """ + if not isinstance(heatmap, dict): + # Invalid heatmap, assume overlap to be safe + return True + + x_min, y_min, x_max, y_max = roi_bbox + + # Convert normalized coordinates to grid cells (0-15) + grid_x_min = max(0, int(x_min * HEATMAP_GRID_SIZE)) + grid_y_min = max(0, int(y_min * HEATMAP_GRID_SIZE)) + grid_x_max = min(HEATMAP_GRID_SIZE - 1, int(x_max * HEATMAP_GRID_SIZE)) + grid_y_max = min(HEATMAP_GRID_SIZE - 1, int(y_max * HEATMAP_GRID_SIZE)) + + # Check each cell in the ROI bbox + for y in range(grid_y_min, grid_y_max + 1): + for x in range(grid_x_min, grid_x_max + 1): + idx = str(y * HEATMAP_GRID_SIZE + x) + if idx in heatmap: + return True + + return False + + +def segment_passes_activity_gate(recording: Recordings) -> bool: + """Check if a segment passes the activity gate. + + Returns True if any of motion, objects, or regions is non-zero/non-null. + Returns True if all are null (old segments without data). + """ + motion: Any = recording.motion + objects: Any = recording.objects + regions: Any = recording.regions + + # Old segments without metadata - pass through (conservative) + if motion is None and objects is None and regions is None: + return True + + # Pass if any activity indicator is positive + return bool(motion) or bool(objects) or bool(regions) + + +def segment_passes_heatmap_gate( + recording: Recordings, roi_bbox: tuple[float, float, float, float] +) -> bool: + """Check if a segment passes the heatmap overlap gate. + + Returns True if: + - No heatmap is stored (old segments). + - The heatmap overlaps with the ROI bbox. + """ + heatmap = getattr(recording, "motion_heatmap", None) + if heatmap is None: + # No heatmap stored, fall back to activity gate + return True + + return heatmap_overlaps_roi(heatmap, roi_bbox) + + +def resolve_internal_port(config: FrigateConfig) -> int: + """Return the unauthenticated internal nginx port for VOD requests.""" + listen = config.networking.listen.internal + if isinstance(listen, str): + return int(listen.split(":")[-1]) + return int(listen) + + +def build_vod_url(internal_port: int, camera: str, start: float, end: float) -> str: + """Build the internal VOD HLS URL for a camera time range.""" + return ( + f"http://127.0.0.1:{internal_port}/vod/{camera}" + f"/start/{start}/end/{end}/index.m3u8" + ) + + +class MotionSearchRunner(threading.Thread): + """Thread-based runner for motion search jobs with parallel verification.""" + + def __init__( + self, + job: MotionSearchJob, + config: FrigateConfig, + cancel_event: threading.Event, + ) -> None: + super().__init__(daemon=True, name=f"motion_search_{job.id}") + self.job = job + self.config = config + self.cancel_event = cancel_event + self.internal_stop_event = threading.Event() + self.requestor = InterProcessRequestor() + self.metrics = MotionSearchMetrics() + self.job.metrics = self.metrics + + # Worker cap: min(4, cpu_count) + cpu_count = os.cpu_count() or 1 + self.max_workers = min(4, cpu_count) + + # Resolved once per job in _execute_search + self.ffmpeg_path: str = "ffmpeg" + self.ffprobe_path: str = "ffprobe" + self.decode_args: list[str] = [] + # Keyframe sampling decision, decided once per job from the first run's + # GOP. The fallback cadence is a fixed rate (see FALLBACK_SAMPLE_FPS). + self.use_keyframe: bool = True + self.fps_rate: float = FALLBACK_SAMPLE_FPS + # ROI crop/scale + scaled mask, computed once from the VOD-stream + # dimensions (which can differ from the detect resolution). + self.crop: tuple[int, int, int, int] = (0, 0, 0, 0) + self.scaled: tuple[int, int] = (0, 0) + self.scaled_mask: np.ndarray = np.zeros((0, 0), dtype=np.uint8) + self.channels: int = 1 + self.internal_port: int = 5000 + self._last_progress_broadcast: float = 0.0 + + def run(self) -> None: + """Execute the motion search job.""" + try: + self.job.status = JobStatusTypesEnum.running + self.job.start_time = datetime.now().timestamp() + self._broadcast_status() + + results = self._execute_search() + + if self.cancel_event.is_set(): + self.job.status = JobStatusTypesEnum.cancelled + else: + self.job.status = JobStatusTypesEnum.success + self.job.results = { + "results": [r.to_dict() for r in results], + "total_frames_processed": self.job.total_frames_processed, + } + + self.job.end_time = datetime.now().timestamp() + self.metrics.wall_time_seconds = self.job.end_time - self.job.start_time + self.job.metrics = self.metrics + + logger.debug( + "Motion search job %s completed: status=%s, results=%d, frames=%d", + self.job.id, + self.job.status, + len(results), + self.job.total_frames_processed, + ) + self._broadcast_status() + + except Exception as e: + logger.exception("Motion search job %s failed: %s", self.job.id, e) + self.job.status = JobStatusTypesEnum.failed + self.job.error_message = str(e) + self.job.end_time = datetime.now().timestamp() + self.metrics.wall_time_seconds = self.job.end_time - ( + self.job.start_time or 0 + ) + self.job.metrics = self.metrics + self._broadcast_status() + + finally: + if self.requestor: + self.requestor.stop() + + def _broadcast_status(self) -> None: + """Broadcast job status update via IPC to WebSocket subscribers.""" + if self.job.status == JobStatusTypesEnum.running and self.job.start_time: + self.metrics.wall_time_seconds = ( + datetime.now().timestamp() - self.job.start_time + ) + + try: + self.requestor.send_data(UPDATE_JOB_STATE, self.job.to_dict()) + except Exception as e: + logger.warning("Failed to broadcast motion search status: %s", e) + + def _should_stop(self) -> bool: + """Check if processing should stop due to cancellation or internal limits.""" + return self.cancel_event.is_set() or self.internal_stop_event.is_set() + + def _execute_search(self) -> list[MotionSearchResult]: + """Main search execution logic.""" + camera_name = self.job.camera + camera_config = self.config.cameras.get(camera_name) + if not camera_config: + raise ValueError(f"Camera {camera_name} not found") + + frame_width = camera_config.detect.width + frame_height = camera_config.detect.height + + if frame_width is None or frame_height is None: + raise ValueError(f"Camera {camera_name} detect dimensions not configured") + + self.ffmpeg_path = camera_config.ffmpeg.ffmpeg_path + self.ffprobe_path = camera_config.ffmpeg.ffprobe_path + + # Create polygon mask + polygon_mask = create_polygon_mask( + self.job.polygon_points, frame_width, frame_height + ) + + if np.count_nonzero(polygon_mask) == 0: + logger.warning("Polygon mask is empty for job %s", self.job.id) + return [] + + # Compute ROI bbox in normalized coordinates for heatmap gate + roi_bbox = compute_roi_bbox_normalized(self.job.polygon_points) + + # Query recordings + recordings = list( + Recordings.select() + .where( + ( + Recordings.start_time.between( + self.job.start_time_range, self.job.end_time_range + ) + ) + | ( + Recordings.end_time.between( + self.job.start_time_range, self.job.end_time_range + ) + ) + | ( + (self.job.start_time_range > Recordings.start_time) + & (self.job.end_time_range < Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.asc()) + ) + + if not recordings: + logger.debug("No recordings found for motion search job %s", self.job.id) + return [] + + logger.debug( + "Motion search job %s: queried %d recording segments for camera %s " + "(range %.1f - %.1f)", + self.job.id, + len(recordings), + camera_name, + self.job.start_time_range, + self.job.end_time_range, + ) + + self.metrics.segments_scanned = len(recordings) + + # Apply activity and heatmap gates + filtered_recordings = [] + for recording in recordings: + if not segment_passes_activity_gate(recording): + self.metrics.metadata_inactive_segments += 1 + self.metrics.segments_processed += 1 + logger.debug( + "Motion search job %s: segment %s skipped by activity gate " + "(motion=%s, objects=%s, regions=%s)", + self.job.id, + recording.id, + recording.motion, + recording.objects, + recording.regions, + ) + continue + if not segment_passes_heatmap_gate(recording, roi_bbox): + self.metrics.heatmap_roi_skip_segments += 1 + self.metrics.segments_processed += 1 + logger.debug( + "Motion search job %s: segment %s skipped by heatmap gate " + "(heatmap present=%s, roi_bbox=%s)", + self.job.id, + recording.id, + recording.motion_heatmap is not None, + roi_bbox, + ) + continue + filtered_recordings.append(recording) + + self._broadcast_status() + + # Fallback: if all segments were filtered out, scan all segments + # This allows motion search to find things the detector missed + if not filtered_recordings and recordings: + logger.info( + "All %d segments filtered by gates, falling back to full scan", + len(recordings), + ) + self.metrics.fallback_full_range_segments = len(recordings) + filtered_recordings = recordings + + logger.debug( + "Motion search job %s: %d/%d segments passed gates " + "(activity_skipped=%d, heatmap_skipped=%d)", + self.job.id, + len(filtered_recordings), + len(recordings), + self.metrics.metadata_inactive_segments, + self.metrics.heatmap_roi_skip_segments, + ) + + # Resolve decode backend (allowlisted hwaccel or software), coalesce the + # gate-passing segments into time-contiguous runs, and probe the first + # run's VOD stream once for dimensions + keyframe layout. VOD output is + # what we decode, so crop/scale/mask are computed against it. + self.internal_port = resolve_internal_port(self.config) + self.decode_args = resolve_motion_decode_args(camera_config) + ffprobe_path = self.ffprobe_path + + runs = coalesce_runs(filtered_recordings, MAX_RUN_SECONDS, RUN_GAP_EPSILON) + if not runs: + return [] + + first_run = runs[0] + first_url = build_vod_url( + self.internal_port, + camera_name, + float(first_run[0].start_time), + float(first_run[-1].end_time), + ) + dims = probe_video_dimensions(ffprobe_path, first_url) + if dims is None: + raise ValueError(f"Could not probe VOD dimensions for camera {camera_name}") + rec_width, rec_height, _rec_fps = dims + + self.crop, self.scaled = compute_roi_crop_and_scale( + self.job.polygon_points, rec_width, rec_height, SCALE_TARGET + ) + self.scaled_mask = build_scaled_roi_mask( + self.job.polygon_points, rec_width, rec_height, self.crop, self.scaled + ) + self.channels = 1 # always gray output + + # Decide keyframe vs fixed-cadence sampling once from the first run's GOP + # (keyframe structure is a per-camera constant). + first_pts = probe_vod_keyframe_pts(ffprobe_path, first_url) + self.use_keyframe = keyframe_sampling_eligible(first_pts) + + logger.debug( + "Motion search job %s: %d runs, sampling=%s, hwaccel=%s, vod=%dx%d", + self.job.id, + len(runs), + "keyframe" if self.use_keyframe else "cadence", + bool(self.decode_args), + rec_width, + rec_height, + ) + + return self._search_runs(runs) + + def _emit_progress(self, abs_ts: float) -> None: + """Throttled intra-run progress broadcast (scanning cursor).""" + now = time.monotonic() + if now - self._last_progress_broadcast < PROGRESS_BROADCAST_INTERVAL: + return + self._last_progress_broadcast = now + self.job.scanning_timestamp = abs_ts + self._broadcast_status() + + def _detect_with_progress( + self, + indexed_frames: list[tuple[int, np.ndarray]], + timestamp_fn: Callable[[int], float], + ) -> list[MotionSearchResult]: + """Run detection while firing throttled progress as frames are scanned.""" + + def _gen() -> Generator[tuple[int, np.ndarray], None, None]: + for i, frame in indexed_frames: + if not self._should_stop(): + self._emit_progress(timestamp_fn(i)) + yield i, frame + + return detect_motion_scaled( + _gen(), + self.scaled_mask, + self.job.threshold, + self.job.min_area, + timestamp_fn, + ) + + def _process_run( + self, run: list[Recordings] + ) -> tuple[list[MotionSearchResult], int]: + """Decode one run's VOD stream and detect motion. + + Keyframe mode compares every decoded keyframe (free recall, since they + are all decoded anyway) paired with its probed PTS; if the decoded and + probed counts disagree (the decoder ignored ``-skip_frame nokey`` or the + stream is corrupt) this run re-runs in the fixed-cadence fallback. + Returns ``(results, frame_count)``. + """ + run_start: float = run[0].start_time # type: ignore[assignment] + run_end: float = run[-1].end_time # type: ignore[assignment] + vod_url = build_vod_url(self.internal_port, self.job.camera, run_start, run_end) + time_map = build_segment_time_map(run) + + if self.use_keyframe: + kf_pts = probe_vod_keyframe_pts(self.ffprobe_path, vod_url) + frames = list( + iter_vod_frames( + self.ffmpeg_path, + vod_url, + self.scaled[0], + self.scaled[1], + self.channels, + self.decode_args, + self.crop, + self.scaled, + True, + self._should_stop, + skip_nonkey=True, + fps_rate=None, + ) + ) + if kf_pts and len(frames) == len(kf_pts): + abs_times = [stream_time_to_absolute(time_map, p) for p in kf_pts] + indexed = list(enumerate(frames)) + + def _ts_kf(i: int) -> float: + return abs_times[i] + + results = self._detect_with_progress(indexed, _ts_kf) + return results, len(frames) + + logger.debug( + "Keyframe count mismatch (%d decoded vs %d probed), using cadence", + len(frames), + len(kf_pts), + ) + + return self._process_run_cadence(vod_url, time_map) + + def _process_run_cadence( + self, vod_url: str, time_map: list[tuple[float, float, float]] + ) -> tuple[list[MotionSearchResult], int]: + """Fixed-cadence fallback: fps-filtered VOD decode, evenly spaced times.""" + frames = list( + iter_vod_frames( + self.ffmpeg_path, + vod_url, + self.scaled[0], + self.scaled[1], + self.channels, + self.decode_args, + self.crop, + self.scaled, + True, + self._should_stop, + skip_nonkey=False, + fps_rate=self.fps_rate, + ) + ) + indexed = list(enumerate(frames)) + + def _ts_fps(i: int) -> float: + return stream_time_to_absolute(time_map, i / self.fps_rate) + + results = self._detect_with_progress(indexed, _ts_fps) + return results, len(frames) + + def _merge_run( + self, + run: list[Recordings], + run_results: list[MotionSearchResult], + frames: int, + state: dict[str, Any], + ) -> bool: + """Fold one run's output into the running results; stream + dedup. + + Returns True once ``max_results`` deduped hits have accumulated. + """ + state["completed_runs"] += 1 + state["all_results"].extend(run_results) + state["total_frames"] += frames + self.job.total_frames_processed = state["total_frames"] + self.metrics.frames_decoded = state["total_frames"] + self.metrics.segments_processed += len(run) + self.job.progress = state["completed_runs"] / state["total_runs"] + + state["all_results"].sort(key=lambda r: r.timestamp) + deduped = self._deduplicate_results(state["all_results"])[ + : self.job.max_results + ] + self.job.results = { + "results": [r.to_dict() for r in deduped], + "total_frames_processed": state["total_frames"], + } + self._broadcast_status() + return len(deduped) >= self.job.max_results + + def _search_runs(self, runs: list[list[Recordings]]) -> list[MotionSearchResult]: + """Decode runs (parallel pool when enabled), merge in order, stream.""" + state: dict[str, Any] = { + "all_results": [], + "total_frames": 0, + "completed_runs": 0, + "total_runs": len(runs), + } + self.job.results = {"results": [], "total_frames_processed": 0} + + logger.debug( + "Motion search job %s: searching %d runs (parallel=%s, workers=%d)", + self.job.id, + len(runs), + self.job.parallel, + self.max_workers, + ) + + if self.job.parallel and len(runs) > 1: + with ThreadPoolExecutor(max_workers=self.max_workers) as executor: + futures: dict[Future, int] = {} + for idx, run in enumerate(runs): + if self._should_stop(): + break + futures[executor.submit(self._process_run, run)] = idx + + completed: dict[int, tuple[list[MotionSearchResult], int]] = {} + next_idx = 0 + for future in as_completed(futures): + if self._should_stop(): + break + run_idx = futures[future] + try: + completed[run_idx] = future.result() + except Exception as e: + self.metrics.segments_with_errors += 1 + logger.warning("Error processing run %d: %s", run_idx, e) + completed[run_idx] = ([], 0) + + while next_idx in completed: + run_results, frames = completed.pop(next_idx) + if self._merge_run(runs[next_idx], run_results, frames, state): + self.internal_stop_event.set() + for pending in futures: + pending.cancel() + break + next_idx += 1 + + if self.internal_stop_event.is_set(): + break + else: + for run in runs: + if self._should_stop(): + break + try: + run_results, frames = self._process_run(run) + except Exception as e: + self.metrics.segments_with_errors += 1 + self.metrics.segments_processed += len(run) + self._broadcast_status() + logger.warning("Error processing run: %s", e) + continue + if self._merge_run(run, run_results, frames, state): + break + + all_results: list[MotionSearchResult] = state["all_results"] + self.job.total_frames_processed = state["total_frames"] + self.metrics.frames_decoded = state["total_frames"] + self.job.progress = 1.0 + + logger.debug( + "Motion search job %s: complete, %d raw results, %d frames, %d errors", + self.job.id, + len(all_results), + state["total_frames"], + self.metrics.segments_with_errors, + ) + + all_results.sort(key=lambda r: r.timestamp) + return self._deduplicate_results(all_results)[: self.job.max_results] + + def _deduplicate_results( + self, results: list[MotionSearchResult], min_gap: float = 1.0 + ) -> list[MotionSearchResult]: + """Deduplicate results that are too close together.""" + if not results: + return results + + deduplicated: list[MotionSearchResult] = [] + last_timestamp = 0.0 + + for result in results: + if result.timestamp - last_timestamp >= min_gap: + deduplicated.append(result) + last_timestamp = result.timestamp + + return deduplicated + + +# Module-level state for managing per-camera jobs +_motion_search_jobs: dict[str, tuple[MotionSearchJob, threading.Event]] = {} +_jobs_lock = threading.Lock() + + +def stop_all_motion_search_jobs() -> None: + """Cancel all running motion search jobs for clean shutdown.""" + with _jobs_lock: + for job_id, (job, cancel_event) in _motion_search_jobs.items(): + if job.status in (JobStatusTypesEnum.queued, JobStatusTypesEnum.running): + cancel_event.set() + logger.debug("Signalling motion search job %s to stop", job_id) + + +def start_motion_search_job( + config: FrigateConfig, + camera_name: str, + start_time: float, + end_time: float, + polygon_points: list[list[float]], + threshold: int = 30, + min_area: float = 5.0, + parallel: bool = False, + max_results: int = 25, +) -> str: + """Start a new motion search job. + + Returns the job ID. + """ + job = MotionSearchJob( + camera=camera_name, + start_time_range=start_time, + end_time_range=end_time, + polygon_points=polygon_points, + threshold=threshold, + min_area=min_area, + parallel=parallel, + max_results=max_results, + ) + + cancel_event = threading.Event() + + with _jobs_lock: + _motion_search_jobs[job.id] = (job, cancel_event) + + set_current_job(job) + + runner = MotionSearchRunner(job, config, cancel_event) + runner.start() + + logger.debug( + "Started motion search job %s for camera %s: " + "time_range=%.1f-%.1f, threshold=%d, min_area=%.1f%%, " + "parallel=%s, max_results=%d, polygon_points=%d vertices", + job.id, + camera_name, + start_time, + end_time, + threshold, + min_area, + parallel, + max_results, + len(polygon_points), + ) + return job.id + + +def get_motion_search_job(job_id: str) -> MotionSearchJob | None: + """Get a motion search job by ID.""" + with _jobs_lock: + job_entry = _motion_search_jobs.get(job_id) + if job_entry: + return job_entry[0] + # Check completed jobs via manager + return cast(MotionSearchJob | None, get_job_by_id("motion_search", job_id)) + + +def cancel_motion_search_job(job_id: str) -> bool: + """Cancel a motion search job. + + Returns True if cancellation was initiated, False if job not found. + """ + with _jobs_lock: + job_entry = _motion_search_jobs.get(job_id) + if not job_entry: + return False + + job, cancel_event = job_entry + + if job.status not in (JobStatusTypesEnum.queued, JobStatusTypesEnum.running): + # Already finished + return True + + cancel_event.set() + job.status = JobStatusTypesEnum.cancelled + job_payload = job.to_dict() + logger.info("Cancelled motion search job %s", job_id) + + requestor: InterProcessRequestor | None = None + try: + requestor = InterProcessRequestor() + requestor.send_data(UPDATE_JOB_STATE, job_payload) + except Exception as e: + logger.warning( + "Failed to broadcast cancelled motion search job %s: %s", job_id, e + ) + finally: + if requestor: + requestor.stop() + + return True diff --git a/frigate/jobs/motion_search_batch.py b/frigate/jobs/motion_search_batch.py new file mode 100644 index 0000000..f916da5 --- /dev/null +++ b/frigate/jobs/motion_search_batch.py @@ -0,0 +1,75 @@ +"""Pure helpers for VOD-batched motion search. + +Coalescing gate-passing segments into time-contiguous runs, mapping a frame's +VOD stream time back to an absolute timestamp, and thinning sample times to a +target interval. No I/O or ffmpeg here so the tricky math stays unit-testable. +""" + +from bisect import bisect_right +from typing import Any + + +def coalesce_runs( + segments: list[Any], max_seconds: float, epsilon: float +) -> list[list[Any]]: + """Group gate-passing segments into time-contiguous runs. + + A run extends while each segment's ``start_time`` is within ``epsilon`` of + the previous segment's ``end_time`` (no recording gap) and the run's total + span stays at or below ``max_seconds``. A gap or the cap starts a new run. + Each segment must expose ``start_time`` / ``end_time``. + """ + runs: list[list[Any]] = [] + current: list[Any] = [] + for seg in segments: + if not current: + current = [seg] + continue + prev_end = float(current[-1].end_time) + run_start = float(current[0].start_time) + contiguous = abs(float(seg.start_time) - prev_end) <= epsilon + within_cap = (float(seg.end_time) - run_start) <= max_seconds + if contiguous and within_cap: + current.append(seg) + else: + runs.append(current) + current = [seg] + if current: + runs.append(current) + return runs + + +def build_segment_time_map( + run: list[Any], +) -> list[tuple[float, float, float]]: + """Build a (stream_offset, abs_start, duration) row per segment in a run. + + ``stream_offset`` is the segment's start in continuous VOD stream time (the + cumulative sum of preceding segment durations); ``abs_start`` is its absolute + ``start_time``. Built from each segment's own duration; for a gap-free run + this makes stream time equal ``run_start + offset``. + """ + rows: list[tuple[float, float, float]] = [] + offset = 0.0 + for seg in run: + duration = float(seg.end_time) - float(seg.start_time) + rows.append((offset, float(seg.start_time), duration)) + offset += duration + return rows + + +def stream_time_to_absolute( + time_map: list[tuple[float, float, float]], stream_time: float +) -> float: + """Map a VOD stream time to an absolute timestamp via the run's table. + + Binary-searches the segment whose stream range contains ``stream_time`` and + returns ``abs_start + (stream_time - stream_offset)``. Times past the last + segment map into the last segment (clamped at the run edge). + """ + offsets = [row[0] for row in time_map] + idx = bisect_right(offsets, stream_time) - 1 + if idx < 0: + idx = 0 + stream_offset, abs_start, _duration = time_map[idx] + return abs_start + (stream_time - stream_offset) diff --git a/frigate/jobs/motion_search_decode.py b/frigate/jobs/motion_search_decode.py new file mode 100644 index 0000000..4b1d518 --- /dev/null +++ b/frigate/jobs/motion_search_decode.py @@ -0,0 +1,382 @@ +"""Hardware-accelerated ffmpeg decode for motion search. + +Decodes a recording run's VOD/HLS stream with an ffmpeg subprocess, optionally +selecting only keyframes, and streams raw frames over a pipe for the motion +math. Output is the requested ``pix_fmt`` (gray or ``bgr24``) with optional +crop/scale applied in the filter graph so downstream pixels are unchanged. +""" + +import json +import logging +import subprocess as sp +import tempfile +from collections.abc import Callable, Generator +from typing import IO + +import numpy as np + +from frigate.config import CameraConfig +from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_decode +from frigate.util.services import auto_detect_hwaccel + +logger = logging.getLogger(__name__) + +# Output-format surfaces that download cleanly to nv12 via the fixed +# ``hwdownload,format=nv12`` step the decode path appends. Other surfaces +# (drm_prime from rkmpp, vulkan, amf) need a different download step, so motion +# search decodes them in software to keep results byte-identical rather than risk +# a wrong-but-valid-sized frame the zero-frame fallback gate would not catch. +_NV12_OUTPUT_FORMATS = frozenset({"vaapi", "cuda", "qsv"}) + + +def _hwaccel_output_format(decode_args: list[str]) -> str | None: + """Return the ``-hwaccel_output_format`` value in ffmpeg args, or None.""" + try: + idx = decode_args.index("-hwaccel_output_format") + except ValueError: + return None + return decode_args[idx + 1] if idx + 1 < len(decode_args) else None + + +def resolve_motion_decode_args(camera_config: CameraConfig) -> list[str]: + """Resolve the ffmpeg hwaccel decode args for a camera's recordings. + + ``auto`` is resolved via ``auto_detect_hwaccel`` and the preset is expanded + by ``parse_preset_hardware_acceleration_decode`` (the same table the live + pipeline uses). Acceleration is kept only when the decoded surface downloads + cleanly to nv12 -- decided by reading ``-hwaccel_output_format`` back from the + resolved args rather than a separate preset allowlist that could drift from + ``PRESETS_HW_ACCEL_DECODE``. Anything else (custom args, a software-only + preset, or an nv12-incompatible surface) returns an empty list, meaning + software decode, so results stay byte-identical. + """ + raw = camera_config.ffmpeg.hwaccel_args + preset = auto_detect_hwaccel() if raw == "auto" else raw + + # Custom args (a list) decode in software so results stay byte-identical. + if not isinstance(preset, str): + return [] + + decode_args = parse_preset_hardware_acceleration_decode( + preset, + camera_config.detect.fps, + camera_config.detect.width or 0, + camera_config.detect.height or 0, + camera_config.ffmpeg.gpu, + ) + if not decode_args: + return [] + + if _hwaccel_output_format(decode_args) not in _NV12_OUTPUT_FORMATS: + return [] + + return decode_args + + +def _read_exact(stream: IO[bytes], size: int) -> bytes | None: + """Read exactly ``size`` bytes from a pipe, or None at clean EOF. + + Pipe reads can return fewer bytes than requested, so loop until the frame + is complete. A short read at the start of a frame means end-of-stream. + """ + buf = bytearray() + while len(buf) < size: + chunk = stream.read(size - len(buf)) + if not chunk: + return None + buf.extend(chunk) + return bytes(buf) + + +def _terminate(proc: sp.Popen[bytes]) -> None: + """Stop an ffmpeg decode process promptly.""" + # Close the read end first so a blocked ffmpeg write unblocks (ffmpeg then + # sees a broken pipe), then signal it. The resulting ffmpeg write error is + # harmless and goes to the captured stderr. + if proc.stdout is not None: + try: + proc.stdout.close() + except OSError: + pass + if proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=5) + except sp.TimeoutExpired: + proc.kill() + proc.wait() + + +KEYFRAME_MAX_GAP_SECONDS = 2.0 + + +def keyframe_sampling_eligible( + keyframe_pts: list[float], max_gap: float = KEYFRAME_MAX_GAP_SECONDS +) -> bool: + """True if keyframes are dense and regular enough for keyframe-only sampling. + + Requires at least two keyframes and no gap longer than ``max_gap`` seconds, so + a multi-second motion event necessarily spans a sampled keyframe. + """ + if len(keyframe_pts) < 2: + return False + gaps = [b - a for a, b in zip(keyframe_pts, keyframe_pts[1:])] + return max(gaps) <= max_gap + + +VOD_PROTOCOL_ARGS = ["-protocol_whitelist", "pipe,file,http,tcp"] + + +def build_vod_decode_command( + ffmpeg_path: str, + vod_url: str, + decode_args: list[str], + crop: tuple[int, int, int, int] | None, + scale: tuple[int, int] | None, + gray: bool, + *, + skip_nonkey: bool, + fps_rate: float | None, +) -> list[str]: + """Build the ffmpeg argv to decode a VOD HLS URL. + + ``skip_nonkey`` adds ``-skip_frame nokey`` (keyframe-only). ``fps_rate`` adds + an ``fps`` filter for the fixed-cadence fallback. They are mutually + exclusive: keyframe mode passes ``skip_nonkey=True``/``fps_rate=None``; the + fallback passes ``skip_nonkey=False`` with a rate. + """ + filters: list[str] = [] + # With hwaccel the decoded frames are GPU surfaces; pull them back to system + # memory before the CPU fps/crop/scale filters and the rawvideo encoder. + if decode_args: + filters.append("hwdownload") + filters.append("format=nv12") + if fps_rate is not None: + filters.append(f"fps={fps_rate}") + if crop is not None: + cw, ch, cx, cy = crop + filters.append(f"crop={cw}:{ch}:{cx}:{cy}") + if scale is not None: + sw, sh = scale + filters.append(f"scale={sw}:{sh}") + + pix_fmt = "gray" if gray else "bgr24" + cmd = [ffmpeg_path, "-hide_banner", "-loglevel", "error"] + if skip_nonkey: + cmd += ["-skip_frame", "nokey"] + cmd += [*decode_args, *VOD_PROTOCOL_ARGS, "-i", vod_url, "-an"] + if filters: + cmd += ["-vf", ",".join(filters)] + cmd += ["-vsync", "0", "-f", "rawvideo", "-pix_fmt", pix_fmt, "pipe:"] + return cmd + + +def _run_vod_decode( + ffmpeg_path: str, + vod_url: str, + out_width: int, + out_height: int, + channels: int, + decode_args: list[str], + crop: tuple[int, int, int, int] | None, + scale: tuple[int, int] | None, + gray: bool, + should_stop: Callable[[], bool], + *, + skip_nonkey: bool, + fps_rate: float | None, + software_retry: bool, +) -> Generator[np.ndarray, None, None]: + """Run one VOD decode, yielding raw frames; retry in software if empty.""" + cmd = build_vod_decode_command( + ffmpeg_path, + vod_url, + decode_args, + crop, + scale, + gray, + skip_nonkey=skip_nonkey, + fps_rate=fps_rate, + ) + frame_size = out_width * out_height * channels + stderr_file = tempfile.SpooledTemporaryFile(max_size=65536) + proc = sp.Popen(cmd, stdout=sp.PIPE, stderr=stderr_file) + assert proc.stdout is not None + + count = 0 + try: + while True: + if should_stop(): + break + buf = _read_exact(proc.stdout, frame_size) + if buf is None: + break + if channels == 1: + frame = np.frombuffer(buf, dtype=np.uint8).reshape( + (out_height, out_width) + ) + else: + frame = np.frombuffer(buf, dtype=np.uint8).reshape( + (out_height, out_width, channels) + ) + count += 1 + yield frame + finally: + _terminate(proc) + stderr_file.close() + + if count == 0 and software_retry and not should_stop(): + logger.warning("Hardware VOD decode produced no frames, retrying in software") + yield from _run_vod_decode( + ffmpeg_path, + vod_url, + out_width, + out_height, + channels, + [], + crop, + scale, + gray, + should_stop, + skip_nonkey=skip_nonkey, + fps_rate=fps_rate, + software_retry=False, + ) + + +def iter_vod_frames( + ffmpeg_path: str, + vod_url: str, + out_width: int, + out_height: int, + channels: int, + decode_args: list[str], + crop: tuple[int, int, int, int] | None, + scale: tuple[int, int] | None, + gray: bool, + should_stop: Callable[[], bool], + *, + skip_nonkey: bool, + fps_rate: float | None, +) -> Generator[np.ndarray, None, None]: + """Decode a VOD HLS URL and yield raw frames in order. + + Pair keyframe-mode output with probed keyframe PTS; pair fallback output with + a fixed cadence. Falls back once to software decode if a hwaccel decode yields + no frames. + """ + yield from _run_vod_decode( + ffmpeg_path, + vod_url, + out_width, + out_height, + channels, + decode_args, + crop, + scale, + gray, + should_stop, + skip_nonkey=skip_nonkey, + fps_rate=fps_rate, + software_retry=bool(decode_args), + ) + + +def probe_vod_keyframe_pts(ffprobe_path: str, vod_url: str) -> list[float]: + """Return keyframe presentation timestamps (VOD stream time) in order. + + Reads packet flags via ffprobe over the VOD URL (no decode). Returns [] on + any failure so the caller can fall back. + """ + cmd = [ + ffprobe_path, + "-v", + "error", + *VOD_PROTOCOL_ARGS, + "-i", + vod_url, + "-select_streams", + "v:0", + "-show_packets", + "-show_entries", + "packet=pts_time,flags", + "-of", + "json", + ] + try: + completed = sp.run(cmd, capture_output=True, text=True, timeout=120) + except (OSError, sp.SubprocessError): + logger.warning("ffprobe failed for VOD keyframe probe") + return [] + + if completed.returncode != 0 or not completed.stdout: + return [] + + try: + packets = json.loads(completed.stdout).get("packets", []) + except json.JSONDecodeError: + return [] + + pts: list[float] = [] + for pkt in packets: + flags = pkt.get("flags", "") + pts_time = pkt.get("pts_time") + if flags.startswith("K") and pts_time is not None: + try: + pts.append(float(pts_time)) + except ValueError: + continue + return sorted(pts) + + +def probe_video_dimensions( + ffprobe_path: str, recording_path: str +) -> tuple[int, int, float] | None: + """Return (width, height, fps) for a recording's video stream, or None. + + Reads stream metadata via ffprobe (no decode). The record stream resolution + can differ from the camera's detect resolution, so this is probed once per + job against a real segment. + """ + cmd = [ + ffprobe_path, + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "stream=width,height,avg_frame_rate", + "-of", + "json", + recording_path, + ] + try: + completed = sp.run(cmd, capture_output=True, text=True, timeout=30) + except (OSError, sp.SubprocessError): + return None + + if completed.returncode != 0 or not completed.stdout: + return None + + try: + streams = json.loads(completed.stdout).get("streams", []) + except json.JSONDecodeError: + return None + + if not streams: + return None + + stream = streams[0] + width = int(stream.get("width", 0) or 0) + height = int(stream.get("height", 0) or 0) + rate = stream.get("avg_frame_rate", "0/0") or "0/0" + try: + num, _, den = rate.partition("/") + fps = float(num) / float(den) if float(den) != 0 else 0.0 + except (ValueError, ZeroDivisionError): + fps = 0.0 + + if width <= 0 or height <= 0: + return None + + return width, height, fps diff --git a/frigate/jobs/vlm_watch.py b/frigate/jobs/vlm_watch.py new file mode 100644 index 0000000..1e6a542 --- /dev/null +++ b/frigate/jobs/vlm_watch.py @@ -0,0 +1,449 @@ +"""VLM watch job: continuously monitors a camera and notifies when a condition is met.""" + +import base64 +import json +import logging +import re +import threading +import time +from dataclasses import asdict, dataclass, field +from datetime import datetime +from typing import Any + +import cv2 + +from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import UPDATE_JOB_STATE +from frigate.jobs.job import Job +from frigate.types import JobStatusTypesEnum + +logger = logging.getLogger(__name__) + +# Polling interval bounds (seconds) +_MIN_INTERVAL = 1 +_MAX_INTERVAL = 300 + +# Minimum seconds between VLM iterations when woken by detections (no zone filter) +_DETECTION_COOLDOWN_WITHOUT_ZONE = 10 + +# Max user/assistant turn pairs to keep in conversation history +_MAX_HISTORY = 10 + + +@dataclass +class VLMWatchJob(Job): + """Job state for a VLM watch monitor.""" + + job_type: str = "vlm_watch" + camera: str = "" + condition: str = "" + max_duration_minutes: int = 60 + labels: list = field(default_factory=list) + zones: list = field(default_factory=list) + last_reasoning: str = "" + notification_message: str = "" + iteration_count: int = 0 + username: str = "" + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +class VLMWatchRunner(threading.Thread): + """Background thread that polls a camera with the vision client until a condition is met.""" + + def __init__( + self, + job: VLMWatchJob, + config: FrigateConfig, + cancel_event: threading.Event, + frame_processor: Any, + genai_manager: Any, + dispatcher: Any, + ) -> None: + super().__init__(daemon=True, name=f"vlm_watch_{job.id}") + self.job = job + self.config = config + self.cancel_event = cancel_event + self.frame_processor = frame_processor + self.genai_manager = genai_manager + self.dispatcher = dispatcher + self.requestor = InterProcessRequestor() + self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video.value) + self.conversation: list[dict[str, Any]] = [] + + def run(self) -> None: + self.job.status = JobStatusTypesEnum.running + self.job.start_time = time.time() + self._broadcast_status() + self.conversation = [{"role": "system", "content": self._build_system_prompt()}] + + max_end_time = self.job.start_time + self.job.max_duration_minutes * 60 + + try: + while not self.cancel_event.is_set(): + if time.time() > max_end_time: + logger.debug( + "VLM watch job %s timed out after %d minutes", + self.job.id, + self.job.max_duration_minutes, + ) + self.job.status = JobStatusTypesEnum.failed + self.job.error_message = f"Monitor timed out after {self.job.max_duration_minutes} minutes" + break + + next_run_in = self._run_iteration() + + if self.job.status == JobStatusTypesEnum.success: + break + + self._wait_for_trigger(next_run_in) + + except Exception as e: + logger.exception("VLM watch job %s failed: %s", self.job.id, e) + self.job.status = JobStatusTypesEnum.failed + self.job.error_message = str(e) + + finally: + if self.job.status == JobStatusTypesEnum.running: + self.job.status = JobStatusTypesEnum.cancelled + self.job.end_time = time.time() + self._broadcast_status() + try: + self.detection_subscriber.stop() + except Exception: + pass + try: + self.requestor.stop() + except Exception: + pass + + def _run_iteration(self) -> float: + """Run one VLM analysis iteration. Returns seconds until next run.""" + chat_client = self.genai_manager.chat_client + if chat_client is None or not chat_client.supports_vision: + logger.warning( + "VLM watch job %s: no chat client with vision support available", + self.job.id, + ) + return 30 + + frame = self.frame_processor.get_current_frame(self.job.camera, {}) + if frame is None: + logger.debug( + "VLM watch job %s: frame unavailable for camera %s", + self.job.id, + self.job.camera, + ) + self.job.last_reasoning = "Camera frame unavailable" + return 10 + + # Downscale frame to 480p max height + h, w = frame.shape[:2] + if h > 480: + scale = 480.0 / h + frame = cv2.resize( + frame, (int(w * scale), 480), interpolation=cv2.INTER_AREA + ) + + _, enc = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85]) + b64 = base64.b64encode(enc.tobytes()).decode() + + timestamp = datetime.now().strftime("%H:%M:%S") + self.conversation.append( + { + "role": "user", + "content": [ + {"type": "text", "text": f"Frame captured at {timestamp}."}, + { + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{b64}"}, + }, + ], + } + ) + + response = chat_client.chat_with_tools( + messages=self.conversation, + tools=None, + tool_choice=None, + ) + response_str = response.get("content") or "" + + if not response_str: + logger.warning( + "VLM watch job %s: empty response from vision client", self.job.id + ) + # Remove the user message we just added so we don't leave a dangling turn + self.conversation.pop() + return 30 + + logger.debug("VLM watch job %s response: %s", self.job.id, response_str) + + self.conversation.append({"role": "assistant", "content": response_str}) + + # Keep system prompt + last _MAX_HISTORY user/assistant pairs + max_msgs = 1 + _MAX_HISTORY * 2 + if len(self.conversation) > max_msgs: + self.conversation = [self.conversation[0]] + self.conversation[ + -(max_msgs - 1) : + ] + + try: + clean = re.sub( + r"\n?```$", "", re.sub(r"^```[a-zA-Z0-9]*\n?", "", response_str) + ) + parsed = json.loads(clean) + condition_met = bool(parsed.get("condition_met", False)) + next_run_in = max( + _MIN_INTERVAL, + min(_MAX_INTERVAL, int(parsed.get("next_run_in", 30))), + ) + reasoning = str(parsed.get("reasoning", "")) + notification_message = str(parsed.get("notification_message", "")) + except (json.JSONDecodeError, ValueError, TypeError) as e: + logger.warning( + "VLM watch job %s: failed to parse VLM response: %s", self.job.id, e + ) + return 30 + + self.job.last_reasoning = reasoning + self.job.notification_message = notification_message + self.job.iteration_count += 1 + self._broadcast_status() + + if condition_met: + logger.debug( + "VLM watch job %s: condition met on camera %s — %s", + self.job.id, + self.job.camera, + reasoning, + ) + self._send_notification(notification_message or reasoning) + self.job.status = JobStatusTypesEnum.success + return 0 + + return next_run_in + + def _wait_for_trigger(self, max_wait: float) -> None: + """Wait up to max_wait seconds, returning early if a relevant detection fires on the target camera. + + With zones configured, a matching detection wakes immediately (events + are already filtered). Without zones, detections are frequent so a + cooldown is enforced: messages are continuously drained to prevent + queue backup, but the loop only exits once a match has been seen + *and* the cooldown period has elapsed. + """ + now = time.time() + deadline = now + max_wait + use_cooldown = not self.job.zones + earliest_wake = now + _DETECTION_COOLDOWN_WITHOUT_ZONE if use_cooldown else 0 + triggered = False + + while not self.cancel_event.is_set(): + remaining = deadline - time.time() + if remaining <= 0: + break + + if triggered and time.time() >= earliest_wake: + break + + result = self.detection_subscriber.check_for_update( + timeout=min(1.0, remaining) + ) + if result is None: + continue + topic, payload = result + if topic is None or payload is None: + continue + # payload = (camera, frame_name, frame_time, tracked_objects, motion_boxes, regions) + cam = payload[0] + tracked_objects = payload[3] + logger.debug( + "VLM watch job %s: detection event cam=%s (want %s), objects=%s", + self.job.id, + cam, + self.job.camera, + [ + {"label": o.get("label"), "zones": o.get("current_zones")} + for o in (tracked_objects or []) + ], + ) + if cam != self.job.camera or not tracked_objects: + continue + if self._detection_matches_filters(tracked_objects): + if not use_cooldown: + logger.debug( + "VLM watch job %s: woken early by detection event on %s", + self.job.id, + self.job.camera, + ) + break + + if not triggered: + logger.debug( + "VLM watch job %s: detection match on %s, draining for %.0fs", + self.job.id, + self.job.camera, + max(0, earliest_wake - time.time()), + ) + triggered = True + + def _detection_matches_filters(self, tracked_objects: list) -> bool: + """Return True if any tracked object passes the label and zone filters.""" + labels = self.job.labels + zones = self.job.zones + for obj in tracked_objects: + label_ok = not labels or obj.get("label") in labels + zone_ok = not zones or bool(set(obj.get("current_zones", [])) & set(zones)) + if label_ok and zone_ok: + return True + return False + + def _build_system_prompt(self) -> str: + focus_text = "" + if self.job.labels or self.job.zones: + parts = [] + if self.job.labels: + parts.append(f"object types: {', '.join(self.job.labels)}") + if self.job.zones: + parts.append(f"zones: {', '.join(self.job.zones)}") + focus_text = f"\nFocus on {' and '.join(parts)}.\n" + + return ( + f'You are monitoring a security camera. Your task: determine when "{self.job.condition}" occurs.\n' + f"{focus_text}\n" + f"You will receive a sequence of frames over time. Use the conversation history to understand " + f"what is stationary vs. actively changing.\n\n" + f"For each frame respond with JSON only:\n" + f'{{"condition_met": , "next_run_in": , "reasoning": "", "notification_message": ""}}\n\n' + f"Guidelines for notification_message:\n" + f"- Only required when condition_met is true.\n" + f"- Write a short, natural notification a user would want to receive on their phone.\n" + f'- Example: "Your package has been delivered to the front porch."\n\n' + f"Guidelines for next_run_in:\n" + f"- Scene is empty / nothing of interest visible: 60-300.\n" + f"- Relevant object(s) visible anywhere in frame (even outside the target zone): 3-10. " + f"They may be moving toward the zone.\n" + f"- Condition is actively forming (object approaching zone or threshold): 1-5.\n" + f"- Set condition_met to true only when you are confident the condition is currently met.\n" + f"- Keep reasoning to 1-2 sentences." + ) + + def _send_notification(self, message: str) -> None: + """Publish a camera_monitoring event so downstream handlers (web push, MQTT) can notify users.""" + payload = { + "camera": self.job.camera, + "condition": self.job.condition, + "message": message, + "reasoning": self.job.last_reasoning, + "job_id": self.job.id, + } + + if self.dispatcher: + try: + self.dispatcher.publish("camera_monitoring", json.dumps(payload)) + except Exception as e: + logger.warning( + "VLM watch job %s: failed to publish alert: %s", self.job.id, e + ) + + def _broadcast_status(self) -> None: + try: + self.requestor.send_data(UPDATE_JOB_STATE, self.job.to_dict()) + except Exception as e: + logger.warning( + "VLM watch job %s: failed to broadcast status: %s", self.job.id, e + ) + + +# Module-level singleton (only one watch job at a time) +_current_job: VLMWatchJob | None = None +_cancel_event: threading.Event | None = None +_job_lock = threading.Lock() + + +def start_vlm_watch_job( + camera: str, + condition: str, + max_duration_minutes: int, + config: FrigateConfig, + frame_processor: Any, + genai_manager: Any, + dispatcher: Any, + labels: list[str] | None = None, + zones: list[str] | None = None, + username: str = "", +) -> str: + """Start a new VLM watch job. Returns the job ID. + + Raises RuntimeError if a job is already running. + """ + global _current_job, _cancel_event + + with _job_lock: + if _current_job is not None and _current_job.status in ( + JobStatusTypesEnum.queued, + JobStatusTypesEnum.running, + ): + raise RuntimeError( + f"A VLM watch job is already running (id={_current_job.id}). " + "Cancel it before starting a new one." + ) + + job = VLMWatchJob( + camera=camera, + condition=condition, + max_duration_minutes=max_duration_minutes, + labels=labels or [], + zones=zones or [], + username=username, + ) + cancel_ev = threading.Event() + _current_job = job + _cancel_event = cancel_ev + + runner = VLMWatchRunner( + job=job, + config=config, + cancel_event=cancel_ev, + frame_processor=frame_processor, + genai_manager=genai_manager, + dispatcher=dispatcher, + ) + runner.start() + + logger.debug( + "Started VLM watch job %s: camera=%s, condition=%r, max_duration=%dm", + job.id, + camera, + condition, + max_duration_minutes, + ) + return job.id + + +def stop_vlm_watch_job() -> bool: + """Cancel the current VLM watch job. Returns True if a job was cancelled.""" + global _current_job, _cancel_event + + with _job_lock: + if _current_job is None or _current_job.status not in ( + JobStatusTypesEnum.queued, + JobStatusTypesEnum.running, + ): + return False + + if _cancel_event: + _cancel_event.set() + + _current_job.status = JobStatusTypesEnum.cancelled + logger.debug("Cancelled VLM watch job %s", _current_job.id) + return True + + +def get_vlm_watch_job() -> VLMWatchJob | None: + """Return the current (or most recent) VLM watch job.""" + return _current_job diff --git a/frigate/log.py b/frigate/log.py new file mode 100644 index 0000000..3c5f8ec --- /dev/null +++ b/frigate/log.py @@ -0,0 +1,356 @@ +# In log.py +import atexit +import io +import logging +import os +import sys +import threading +from collections import deque +from collections.abc import Callable, Generator +from contextlib import contextmanager +from enum import Enum +from functools import wraps +from logging.handlers import QueueHandler, QueueListener +from multiprocessing.managers import SyncManager +from queue import Empty, Queue +from typing import Any + +from frigate.util.builtin import clean_camera_user_pass + +LOG_HANDLER = logging.StreamHandler() +LOG_HANDLER.setFormatter( + logging.Formatter( + "[%(asctime)s] %(name)-30s %(levelname)-8s: %(message)s", + "%Y-%m-%d %H:%M:%S", + ) +) + +# filter out norfair warning +LOG_HANDLER.addFilter( + lambda record: ( + not record.getMessage().startswith("You are using a scalar distance function") + ) +) + +# filter out tflite logging +LOG_HANDLER.addFilter( + lambda record: ( + "Created TensorFlow Lite XNNPACK delegate for CPU." not in record.getMessage() + ) +) + + +class LogLevel(str, Enum): + debug = "debug" + info = "info" + warning = "warning" + error = "error" + critical = "critical" + + +log_listener: QueueListener | None = None +log_queue: Queue | None = None + + +def setup_logging(manager: SyncManager) -> None: + global log_listener, log_queue + log_queue = manager.Queue() + log_listener = QueueListener(log_queue, LOG_HANDLER, respect_handler_level=True) + + atexit.register(_stop_logging) + log_listener.start() + + logging.basicConfig( + level=logging.INFO, + handlers=[], + force=True, + ) + + logging.getLogger().addHandler(QueueHandler(log_listener.queue)) + + +def _stop_logging() -> None: + global log_listener + if log_listener is not None: + log_listener.stop() + log_listener = None + + +def apply_log_levels(default: str, log_levels: dict[str, LogLevel]) -> None: + logging.getLogger().setLevel(default) + + log_levels = { + "absl": LogLevel.error, + "httpx": LogLevel.error, + "h5py": LogLevel.error, + "keras": LogLevel.error, + "matplotlib": LogLevel.error, + "tensorflow": LogLevel.error, + "tensorflow.python": LogLevel.error, + "werkzeug": LogLevel.error, + "ws4py": LogLevel.error, + "PIL": LogLevel.warning, + "numba": LogLevel.warning, + "google_genai.models": LogLevel.warning, + **log_levels, + } + + for log, level in log_levels.items(): + logging.getLogger(log).setLevel(level.value.upper()) + + +# When a multiprocessing.Process exits, python tries to flush stdout and stderr. However, if the +# process is created after a thread (for example a logging thread) is created and the process fork +# happens while an internal lock is held, the stdout/err flush can cause a deadlock. +# +# https://github.com/python/cpython/issues/91776 +def reopen_std_streams() -> None: + sys.stdout = os.fdopen(1, "w") + sys.stderr = os.fdopen(2, "w") + + +os.register_at_fork(after_in_child=reopen_std_streams) + + +# based on https://codereview.stackexchange.com/a/17959 +class LogPipe(threading.Thread): + def __init__(self, log_name: str, level: int = logging.ERROR): + """Setup the object with a logger and start the thread""" + super().__init__(daemon=False) + self.logger = logging.getLogger(log_name) + self.level = level + self.deque: deque[str] = deque(maxlen=100) + self.fdRead, self.fdWrite = os.pipe() + self.pipeReader = os.fdopen(self.fdRead) + self.start() + + def cleanup_log(self, log: str) -> str: + """Cleanup the log line to remove sensitive info and string tokens.""" + log = clean_camera_user_pass(log).strip("\n") + return log + + def fileno(self) -> int: + """Return the write file descriptor of the pipe""" + return self.fdWrite + + def run(self) -> None: + """Run the thread, logging everything.""" + for line in iter(self.pipeReader.readline, ""): + self.deque.append(self.cleanup_log(line)) + + self.pipeReader.close() + + def dump(self) -> None: + while len(self.deque) > 0: + self.logger.log(self.level, self.deque.popleft()) + + def close(self) -> None: + """Close the write end of the pipe.""" + os.close(self.fdWrite) + + +class LogRedirect(io.StringIO): + """ + A custom file-like object to capture stdout and process it. + It extends io.StringIO to capture output and then processes it + line by line. + """ + + def __init__(self, logger_instance: logging.Logger, level: int): + super().__init__() + self.logger = logger_instance + self.log_level = level + self._line_buffer: list[str] = [] + + def write(self, s: Any) -> int: + if not isinstance(s, str): + s = str(s) + + self._line_buffer.append(s) + + # Process output line by line if a newline is present + if "\n" in s: + full_output = "".join(self._line_buffer) + lines = full_output.splitlines(keepends=True) + self._line_buffer = [] + + for line in lines: + if line.endswith("\n"): + self._process_line(line.rstrip("\n")) + else: + self._line_buffer.append(line) + + return len(s) + + def _process_line(self, line: str) -> None: + self.logger.log(self.log_level, line) + + def flush(self) -> None: + if self._line_buffer: + full_output = "".join(self._line_buffer) + self._line_buffer = [] + if full_output: # Only process if there's content + self._process_line(full_output) + + def __enter__(self) -> "LogRedirect": + """Context manager entry point.""" + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Context manager exit point. Ensures buffered content is flushed.""" + self.flush() + + +@contextmanager +def __redirect_fd_to_queue(queue: Queue[str]) -> Generator[None, None, None]: + """Redirect file descriptor 1 (stdout) to a pipe and capture output in a queue.""" + stdout_fd = os.dup(1) + read_fd, write_fd = os.pipe() + os.dup2(write_fd, 1) + os.close(write_fd) + + stop_event = threading.Event() + + def reader() -> None: + """Read from pipe and put lines in queue until stop_event is set.""" + try: + with os.fdopen(read_fd, "r") as pipe: + while not stop_event.is_set(): + line = pipe.readline() + if not line: # EOF + break + queue.put(line.strip()) + except OSError as e: + queue.put(f"Reader error: {e}") + finally: + if not stop_event.is_set(): + stop_event.set() + + reader_thread = threading.Thread(target=reader, daemon=False) + reader_thread.start() + + try: + yield + finally: + os.dup2(stdout_fd, 1) + os.close(stdout_fd) + stop_event.set() + reader_thread.join(timeout=1.0) + try: + os.close(read_fd) + except OSError: + pass + + +def redirect_output_to_logger(logger: logging.Logger, level: int) -> Any: + """Decorator to redirect both Python sys.stdout/stderr and C-level stdout to logger.""" + + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + queue: Queue[str] = Queue() + + log_redirect = LogRedirect(logger, level) + old_stdout = sys.stdout + old_stderr = sys.stderr + sys.stdout = log_redirect + sys.stderr = log_redirect + + try: + # Redirect C-level stdout + with __redirect_fd_to_queue(queue): + result = func(*args, **kwargs) + finally: + # Restore Python stdout/stderr + sys.stdout = old_stdout + sys.stderr = old_stderr + log_redirect.flush() + + # Log C-level output from queue + while True: + try: + logger.log(level, queue.get_nowait()) + except Empty: + break + + return result + + return wrapper + + return decorator + + +def suppress_os_output(func: Callable) -> Callable: + """ + A decorator that suppresses all output (stdout and stderr) + at the operating system file descriptor level for the decorated function. + This is useful for silencing noisy C/C++ libraries. + Note: This is a Unix-specific solution using os.dup2 and os.pipe. + It temporarily redirects file descriptors 1 (stdout) and 2 (stderr) + to a non-read pipe, effectively discarding their output. + """ + + @wraps(func) + def wrapper(*args: tuple, **kwargs: dict[str, Any]) -> Any: + # Save the original file descriptors for stdout (1) and stderr (2) + original_stdout_fd = os.dup(1) + original_stderr_fd = os.dup(2) + + # Create dummy pipes. We only need the write ends to redirect to. + # The data written to these pipes will be discarded as nothing + # will read from the read ends. + devnull_read_fd, devnull_write_fd = os.pipe() + + try: + # Redirect stdout (FD 1) and stderr (FD 2) to the write end of our dummy pipe + os.dup2(devnull_write_fd, 1) # Redirect stdout to devnull pipe + os.dup2(devnull_write_fd, 2) # Redirect stderr to devnull pipe + + # Execute the original function + result = func(*args, **kwargs) + + finally: + # Restore original stdout and stderr file descriptors (1 and 2) + # This is crucial to ensure normal printing resumes after the decorated function. + os.dup2(original_stdout_fd, 1) + os.dup2(original_stderr_fd, 2) + + # Close all duplicated and pipe file descriptors to prevent resource leaks. + # It's important to close the read end of the dummy pipe too, + # as nothing is explicitly reading from it. + os.close(original_stdout_fd) + os.close(original_stderr_fd) + os.close(devnull_read_fd) + os.close(devnull_write_fd) + + return result + + return wrapper + + +@contextmanager +def suppress_stderr_during(operation_name: str) -> Generator[None, None, None]: + """ + Context manager to suppress stderr output during a specific operation. + + Useful for silencing LLVM debug output, CUDA messages, and other native + library logging that cannot be controlled via Python logging or environment + variables. Completely redirects file descriptor 2 (stderr) to /dev/null. + + Usage: + with suppress_stderr_during("model_conversion"): + converter = tf.lite.TFLiteConverter.from_keras_model(model) + tflite_model = converter.convert() + + Args: + operation_name: Name of the operation for debugging purposes + """ + original_stderr_fd = os.dup(2) + devnull = os.open(os.devnull, os.O_WRONLY) + try: + os.dup2(devnull, 2) + yield + finally: + os.dup2(original_stderr_fd, 2) + os.close(devnull) + os.close(original_stderr_fd) diff --git a/frigate/models.py b/frigate/models.py new file mode 100644 index 0000000..d927a12 --- /dev/null +++ b/frigate/models.py @@ -0,0 +1,179 @@ +from peewee import ( + BlobField, + BooleanField, + CharField, + CompositeKey, + DateTimeField, + FloatField, + ForeignKeyField, + IntegerField, + Model, + TextField, +) +from playhouse.sqlite_ext import JSONField + + +class Event(Model): + id = CharField(null=False, primary_key=True, max_length=30) + label = CharField(index=True, max_length=20) + sub_label = CharField(max_length=100, null=True) + camera = CharField(index=True, max_length=20) + start_time = DateTimeField() + end_time = DateTimeField() + top_score = ( + FloatField() + ) # TODO remove when columns can be dropped without rebuilding table + score = ( + FloatField() + ) # TODO remove when columns can be dropped without rebuilding table + false_positive = BooleanField() + zones = JSONField() + thumbnail = TextField() + has_clip = BooleanField(default=True) + has_snapshot = BooleanField(default=True) + region = ( + JSONField() + ) # TODO remove when columns can be dropped without rebuilding table + box = ( + JSONField() + ) # TODO remove when columns can be dropped without rebuilding table + area = ( + IntegerField() + ) # TODO remove when columns can be dropped without rebuilding table + retain_indefinitely = BooleanField(default=False) + ratio = FloatField( + default=1.0 + ) # TODO remove when columns can be dropped without rebuilding table + plus_id = CharField(max_length=30) + model_hash = CharField(max_length=32) + detector_type = CharField(max_length=32) + model_type = CharField(max_length=32) + data = JSONField() # ex: tracked object box, region, etc. + + +class Timeline(Model): + timestamp = DateTimeField() + camera = CharField(index=True, max_length=20) + source = CharField(index=True, max_length=20) # ex: tracked object, audio, external + source_id = CharField(index=True, max_length=30) + class_type = CharField(max_length=50) # ex: entered_zone, audio_heard + data = JSONField() # ex: tracked object id, region, box, etc. + + +class Regions(Model): + camera = CharField(null=False, primary_key=True, max_length=20) + grid = JSONField() # json blob of grid + last_update = DateTimeField() + + +class Recordings(Model): + id = CharField(null=False, primary_key=True, max_length=30) + camera = CharField(index=True, max_length=20) + path = CharField(unique=True) + start_time = DateTimeField() + end_time = DateTimeField() + duration = FloatField() + motion = IntegerField(null=True) + objects = IntegerField(null=True) + dBFS = IntegerField(null=True) + segment_size = FloatField(default=0) # this should be stored as MB + regions = IntegerField(null=True) + motion_heatmap = JSONField(null=True) # 16x16 grid, 256 values (0-255) + + +class ExportCase(Model): + id = CharField(null=False, primary_key=True, max_length=30) + name = CharField(index=True, max_length=100) + description = TextField(null=True) + created_at = DateTimeField() + updated_at = DateTimeField() + + +class Export(Model): + id = CharField(null=False, primary_key=True, max_length=30) + camera = CharField(index=True, max_length=20) + name = CharField(index=True, max_length=100) + date = DateTimeField() + video_path = CharField(unique=True) + thumb_path = CharField(unique=True) + in_progress = BooleanField() + export_case = ForeignKeyField( + ExportCase, + null=True, + backref="exports", + column_name="export_case_id", + ) + + +class ReviewSegment(Model): + id = CharField(null=False, primary_key=True, max_length=30) + camera = CharField(index=True, max_length=20) + start_time = DateTimeField() + end_time = DateTimeField() + severity = CharField(max_length=30) # alert, detection + thumb_path = CharField(unique=True) + data = JSONField() # additional data about detection like list of labels, zone, areas of significant motion + + +class UserReviewStatus(Model): + user_id = CharField(max_length=30) + review_segment = ForeignKeyField(ReviewSegment, backref="user_reviews") + has_been_reviewed = BooleanField(default=False) + + class Meta: + indexes = ((("user_id", "review_segment"), True),) + + +class Previews(Model): + id = CharField(null=False, primary_key=True, max_length=30) + camera = CharField(index=True, max_length=20) + path = CharField(unique=True) + start_time = DateTimeField() + end_time = DateTimeField() + duration = FloatField() + + +# Used for temporary table in record/cleanup.py +class RecordingsToDelete(Model): + id = CharField(null=False, primary_key=False, max_length=30) + + class Meta: + temporary = True + + +class User(Model): + username = CharField(null=False, primary_key=True, max_length=30) + role = CharField( + max_length=20, + default="admin", + ) + password_hash = CharField(null=False, max_length=120) + password_changed_at = DateTimeField(null=True) + notification_tokens = JSONField() + + @classmethod + def get_allowed_cameras( + cls, role: str, roles_dict: dict[str, list[str]], all_camera_names: set[str] + ) -> list[str]: + if role not in roles_dict: + return [] # Invalid role grants no access + allowed = roles_dict[role] + if not allowed: # Empty list means all cameras + return list(all_camera_names) + + return [cam for cam in allowed if cam in all_camera_names] + + +class Trigger(Model): + camera = CharField(max_length=20) + name = CharField() + type = CharField(max_length=10) + data = TextField() + threshold = FloatField() + model = CharField(max_length=30) + embedding = BlobField() + triggering_event_id = CharField(max_length=30) + last_triggered = DateTimeField() + + class Meta: + primary_key = CompositeKey("camera", "name") diff --git a/frigate/motion/__init__.py b/frigate/motion/__init__.py new file mode 100644 index 0000000..e6ead35 --- /dev/null +++ b/frigate/motion/__init__.py @@ -0,0 +1,39 @@ +from abc import ABC, abstractmethod + +from numpy import ndarray + +from frigate.config import MotionConfig + + +class MotionDetector(ABC): + @abstractmethod + def __init__( + self, + frame_shape: tuple[int, int, int], + config: MotionConfig, + fps: int, + improve_contrast: bool, + threshold: int, + contour_area: int | None, + ) -> None: + pass + + @abstractmethod + def detect(self, frame: ndarray) -> list: + """Detect motion and return motion boxes.""" + pass + + @abstractmethod + def is_calibrating(self) -> bool: + """Return if motion is recalibrating.""" + pass + + @abstractmethod + def update_mask(self) -> None: + """Update the motion mask after a config change.""" + pass + + @abstractmethod + def stop(self) -> None: + """Stop any ongoing work and processes.""" + pass diff --git a/frigate/motion/frigate_motion.py b/frigate/motion/frigate_motion.py new file mode 100644 index 0000000..8a067e1 --- /dev/null +++ b/frigate/motion/frigate_motion.py @@ -0,0 +1,160 @@ +from typing import Any + +import cv2 +import numpy as np + +from frigate.config.config import RuntimeMotionConfig +from frigate.motion import MotionDetector +from frigate.util.image import grab_cv2_contours + + +class FrigateMotionDetector(MotionDetector): + def __init__( + self, + frame_shape: tuple[int, ...], + config: RuntimeMotionConfig, + fps: int, + improve_contrast: Any, + threshold: Any, + contour_area: Any, + ) -> None: + self.config = config + self.frame_shape = frame_shape + frame_height = config.frame_height or frame_shape[0] + self.resize_factor = frame_shape[0] / frame_height + self.motion_frame_size = ( + frame_height, + frame_height * frame_shape[1] // frame_shape[0], + ) + self.avg_frame = np.zeros(self.motion_frame_size, np.float32) + self.avg_delta = np.zeros(self.motion_frame_size, np.float32) + self.motion_frame_count = 0 + self.frame_counter = 0 + resized_mask = cv2.resize( + config.rasterized_mask, + dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), + interpolation=cv2.INTER_LINEAR, + ) + self.mask = np.where(resized_mask == [0]) + self.save_images = False + self.improve_contrast = improve_contrast + self.threshold = threshold + self.contour_area = contour_area + + def is_calibrating(self) -> bool: + return False + + def detect(self, frame: np.ndarray) -> list: + motion_boxes = [] + + gray = frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] + + # resize frame + resized_frame = cv2.resize( + gray, + dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), + interpolation=cv2.INTER_LINEAR, + ) + + # Improve contrast + if self.improve_contrast.value: + min_value = np.percentile(resized_frame, 4) + max_value = np.percentile(resized_frame, 96) + # don't adjust if the image is a single color + if min_value < max_value: + resized_frame = np.clip(resized_frame, min_value, max_value) + resized_frame = ( + ((resized_frame - min_value) / (max_value - min_value)) * 255 + ).astype(np.uint8) + + # mask frame + resized_frame[self.mask] = [255] + + # it takes ~30 frames to establish a baseline + # dont bother looking for motion + if self.frame_counter < 30: + self.frame_counter += 1 + else: + if self.save_images: + self.frame_counter += 1 + # compare to average + frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame)) + + # compute the average delta over the past few frames + # higher values mean the current frame impacts the delta a lot, and a single raindrop may + # register as motion, too low and a fast moving person wont be detected as motion + cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha) + + # compute the threshold image for the current frame + current_thresh = cv2.threshold( + frameDelta, self.threshold.value, 255, cv2.THRESH_BINARY + )[1] + + # black out everything in the avg_delta where there isn't motion in the current frame + avg_delta_image = cv2.convertScaleAbs(self.avg_delta) + avg_delta_image = cv2.bitwise_and(avg_delta_image, current_thresh) + + # then look for deltas above the threshold, but only in areas where there is a delta + # in the current frame. this prevents deltas from previous frames from being included + thresh = cv2.threshold( + avg_delta_image, self.threshold.value, 255, cv2.THRESH_BINARY + )[1] + + # dilate the thresholded image to fill in holes, then find contours + # on thresholded image + thresh_dilated = cv2.dilate(thresh, None, iterations=2) # type: ignore[call-overload] + contours = cv2.findContours( + thresh_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + contours = grab_cv2_contours(contours) + + # loop over the contours + for c in contours: + # if the contour is big enough, count it as motion + contour_area = cv2.contourArea(c) + if contour_area > self.contour_area.value: + x, y, w, h = cv2.boundingRect(c) + motion_boxes.append( + ( + int(x * self.resize_factor), + int(y * self.resize_factor), + int((x + w) * self.resize_factor), + int((y + h) * self.resize_factor), + ) + ) + + if self.save_images: + thresh_dilated = cv2.cvtColor(thresh_dilated, cv2.COLOR_GRAY2BGR) + # print("--------") + # print(self.frame_counter) + for c in contours: + contour_area = cv2.contourArea(c) + if contour_area > self.contour_area.value: + x, y, w, h = cv2.boundingRect(c) + cv2.rectangle( + thresh_dilated, + (x, y), + (x + w, y + h), + (0, 0, 255), + 2, + ) + + cv2.imwrite( + f"debug/frames/frigate-{self.frame_counter}.jpg", thresh_dilated + ) + + if len(motion_boxes) > 0: + self.motion_frame_count += 1 + if self.motion_frame_count >= 10: + # only average in the current frame if the difference persists for a bit + cv2.accumulateWeighted( + resized_frame, self.avg_frame, self.config.frame_alpha + ) + else: + # when no motion, just keep averaging the frames together + cv2.accumulateWeighted( + resized_frame, self.avg_frame, self.config.frame_alpha + ) + self.motion_frame_count = 0 + + return motion_boxes diff --git a/frigate/motion/improved_motion.py b/frigate/motion/improved_motion.py new file mode 100644 index 0000000..598eeac --- /dev/null +++ b/frigate/motion/improved_motion.py @@ -0,0 +1,272 @@ +import logging + +import cv2 +import numpy as np +from scipy.ndimage import gaussian_filter + +from frigate.camera import PTZMetrics +from frigate.config.config import RuntimeMotionConfig +from frigate.motion import MotionDetector +from frigate.util.image import grab_cv2_contours + +logger = logging.getLogger(__name__) + + +class ImprovedMotionDetector(MotionDetector): + def __init__( + self, + frame_shape: tuple[int, ...], + config: RuntimeMotionConfig, + fps: int, + ptz_metrics: PTZMetrics | None = None, + name: str = "improved", + blur_radius: int = 1, + interpolation: int = cv2.INTER_NEAREST, + contrast_frame_history: int = 50, + ) -> None: + self.name = name + self.config = config + self.frame_shape = frame_shape + frame_height = config.frame_height or frame_shape[0] + self.resize_factor = frame_shape[0] / frame_height + self.motion_frame_size = ( + frame_height, + frame_height * frame_shape[1] // frame_shape[0], + ) + self.avg_frame = np.zeros(self.motion_frame_size, np.float32) + self.motion_frame_count = 0 + self.frame_counter = 0 + self.update_mask() + self.save_images = False + self.calibrating = True + self.blur_radius = blur_radius + self.interpolation = interpolation + self.contrast_values = np.zeros((contrast_frame_history, 2), np.uint8) + self.contrast_values[:, 1:2] = 255 + self.contrast_values_index = 0 + self.ptz_metrics = ptz_metrics + self.last_stop_time: float | None = None + + def is_calibrating(self) -> bool: + return self.calibrating + + def detect(self, frame: np.ndarray) -> list[tuple[int, int, int, int]]: + motion_boxes: list[tuple[int, int, int, int]] = [] + + if not self.config.enabled: + return motion_boxes + + # if ptz motor is moving from autotracking, quickly return + # a single box that is 80% of the frame + if self.ptz_metrics is not None and ( + self.ptz_metrics.autotracker_enabled.value + and not self.ptz_metrics.motor_stopped.is_set() + ): + return [ + ( + int(self.frame_shape[1] * 0.1), + int(self.frame_shape[0] * 0.1), + int(self.frame_shape[1] * 0.9), + int(self.frame_shape[0] * 0.9), + ) + ] + + gray = frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] + + # resize frame + resized_frame = cv2.resize( + gray, + dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), + interpolation=self.interpolation, + ) + + if self.save_images: + resized_saved = resized_frame.copy() + + # Improve contrast + if self.config.improve_contrast: + # TODO tracking moving average of min/max to avoid sudden contrast changes + min_value = np.percentile(resized_frame, 4).astype(np.uint8) + max_value = np.percentile(resized_frame, 96).astype(np.uint8) + # skip contrast calcs if the image is a single color + if min_value < max_value: + # keep track of the last 50 contrast values + self.contrast_values[self.contrast_values_index] = [ + min_value, + max_value, + ] + self.contrast_values_index += 1 + if self.contrast_values_index == len(self.contrast_values): + self.contrast_values_index = 0 + + avg_min, avg_max = np.mean(self.contrast_values, axis=0) + + resized_frame = np.clip(resized_frame, avg_min, avg_max) + resized_frame = ( + ((resized_frame - avg_min) / (avg_max - avg_min)) * 255 + ).astype(np.uint8) + + if self.save_images: + contrasted_saved = resized_frame.copy() + + # mask frame + # this has to come after contrast improvement + # Setting masked pixels to zero, to match the average frame at startup + resized_frame[self.mask] = [0] + + resized_frame = gaussian_filter(resized_frame, sigma=1, radius=self.blur_radius) + + if self.save_images: + blurred_saved = resized_frame.copy() + + if self.save_images or self.calibrating: + self.frame_counter += 1 + # compare to average + frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame)) + + # compute the threshold image for the current frame + thresh = cv2.threshold( + frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY + )[1] + + # dilate the thresholded image to fill in holes, then find contours + # on thresholded image + thresh_dilated = cv2.dilate(thresh, None, iterations=1) # type: ignore[call-overload] + contours = cv2.findContours( + thresh_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + contours = grab_cv2_contours(contours) + + # loop over the contours + total_contour_area: float = 0 + for c in contours: + # if the contour is big enough, count it as motion + contour_area = cv2.contourArea(c) + total_contour_area += contour_area + if contour_area > (self.config.contour_area or 0): + x, y, w, h = cv2.boundingRect(c) + motion_boxes.append( + ( + int(x * self.resize_factor), + int(y * self.resize_factor), + int((x + w) * self.resize_factor), + int((y + h) * self.resize_factor), + ) + ) + + pct_motion = total_contour_area / ( + self.motion_frame_size[0] * self.motion_frame_size[1] + ) + + # check if the motor has just stopped from autotracking + # if so, reassign the average to the current frame so we begin with a new baseline + if self.ptz_metrics is not None and ( + # ensure we only do this for cameras with autotracking enabled + self.ptz_metrics.autotracker_enabled.value + and self.ptz_metrics.motor_stopped.is_set() + and ( + self.last_stop_time is None + or self.ptz_metrics.stop_time.value != self.last_stop_time + ) + # value is 0 on startup or when motor is moving + and self.ptz_metrics.stop_time.value != 0 + ): + self.last_stop_time = self.ptz_metrics.stop_time.value + + self.avg_frame = resized_frame.astype(np.float32) + motion_boxes = [] + pct_motion = 0 + + # skip motion entirely if the scene change percentage exceeds configured + # threshold. this is useful to ignore lighting storms, IR mode switches, + # etc. rather than registering them as brief motion and then recalibrating. + # note: skipping means the frame is dropped and **no recording will be + # created**, which could hide a legitimate object if the camera is actively + # auto‑tracking. the alternative is to allow motion and accept a small + # recording that can be reviewed in the timeline. disabled by default (None). + if ( + self.config.skip_motion_threshold is not None + and pct_motion > self.config.skip_motion_threshold + ): + # force a recalibration so we transition to the new background + self.calibrating = True + return [] + + # once the motion is less than 5% and the number of contours is < 4, assume its calibrated + if pct_motion < 0.05 and len(motion_boxes) <= 4: + self.calibrating = False + + # if calibrating or the motion contours are > 80% of the image area + # (lightning, ir, ptz) recalibrate. the lightning threshold does **not** + # stop motion detection entirely; it simply halts additional processing for + # the current frame once the percentage crosses the threshold. this helps + # reduce false positive object detections and CPU usage during high‑motion + # events. recordings continue to be generated because users expect data + # while a PTZ camera is moving. + if self.calibrating or pct_motion > self.config.lightning_threshold: + self.calibrating = True + + if self.save_images: + thresh_dilated = cv2.cvtColor(thresh_dilated, cv2.COLOR_GRAY2BGR) + for b in motion_boxes: + cv2.rectangle( + thresh_dilated, + (int(b[0] / self.resize_factor), int(b[1] / self.resize_factor)), + (int(b[2] / self.resize_factor), int(b[3] / self.resize_factor)), + (0, 0, 255), + 2, + ) + frames = [ + cv2.cvtColor(resized_saved, cv2.COLOR_GRAY2BGR), + cv2.cvtColor(contrasted_saved, cv2.COLOR_GRAY2BGR), + cv2.cvtColor(blurred_saved, cv2.COLOR_GRAY2BGR), + cv2.cvtColor(frameDelta, cv2.COLOR_GRAY2BGR), + cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), + thresh_dilated, + ] + cv2.imwrite( + f"debug/frames/{self.name}-{self.frame_counter}.jpg", + ( + cv2.hconcat(frames) + if self.frame_shape[0] > self.frame_shape[1] + else cv2.vconcat(frames) + ), + ) + + if len(motion_boxes) > 0: + self.motion_frame_count += 1 + if self.motion_frame_count >= 10: + # only average in the current frame if the difference persists for a bit + cv2.accumulateWeighted( + resized_frame, + self.avg_frame, + 0.2 if self.calibrating else self.config.frame_alpha, + ) + else: + # when no motion, just keep averaging the frames together + cv2.accumulateWeighted( + resized_frame, + self.avg_frame, + 0.2 if self.calibrating else self.config.frame_alpha, + ) + self.motion_frame_count = 0 + + return motion_boxes + + def update_mask(self) -> None: + resized_mask = cv2.resize( + self.config.rasterized_mask, + dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), + interpolation=cv2.INTER_AREA, + ) + self.mask = np.where(resized_mask == [0]) + + # Reset motion detection state when mask changes + # so motion detection can quickly recalibrate with the new mask + self.avg_frame = np.zeros(self.motion_frame_size, np.float32) + self.calibrating = True + self.motion_frame_count = 0 + + def stop(self) -> None: + """stop the motion detector.""" + pass diff --git a/frigate/mypy.ini b/frigate/mypy.ini new file mode 100644 index 0000000..3c64323 --- /dev/null +++ b/frigate/mypy.ini @@ -0,0 +1,64 @@ +[mypy] +python_version = 3.11 +show_error_codes = true +follow_imports = normal +ignore_missing_imports = true +strict_equality = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unused_ignores = true +enable_error_code = ignore-without-code +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +# https://github.com/python/mypy/issues/10757 +disallow_untyped_calls = false +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true +no_implicit_reexport = true + +[mypy-frigate.*] +ignore_errors = false + +# Third-party code imported from https://github.com/ufal/whisper_streaming +[mypy-frigate.data_processing.real_time.whisper_online] +ignore_errors = true + +# TODO: Remove ignores for these modules as they are updated with type annotations. + +[mypy-frigate.api.*] +ignore_errors = true + +[mypy-frigate.config.*] +ignore_errors = true + +[mypy-frigate.debug_replay] +ignore_errors = true + +[mypy-frigate.detectors.*] +ignore_errors = true + +[mypy-frigate.embeddings.*] +ignore_errors = true + +[mypy-frigate.http] +ignore_errors = true + +[mypy-frigate.ptz.*] +ignore_errors = true + +[mypy-frigate.stats.*] +ignore_errors = true + +[mypy-frigate.test.*] +ignore_errors = true + +[mypy-frigate.util.*] +ignore_errors = true + +[mypy-frigate.video.*] +ignore_errors = true diff --git a/frigate/object_detection/base.py b/frigate/object_detection/base.py new file mode 100644 index 0000000..bc7910e --- /dev/null +++ b/frigate/object_detection/base.py @@ -0,0 +1,448 @@ +import datetime +import logging +import queue +import threading +import time +from abc import ABC, abstractmethod +from collections import deque +from multiprocessing import Queue, Value +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import numpy as np +import zmq + +from frigate.comms.object_detector_signaler import ( + ObjectDetectorPublisher, + ObjectDetectorSubscriber, +) +from frigate.config import FrigateConfig +from frigate.const import PROCESS_PRIORITY_HIGH +from frigate.detectors import create_detector +from frigate.detectors.detector_config import ( + BaseDetectorConfig, + InputDTypeEnum, + ModelConfig, +) +from frigate.util.builtin import EventsPerSecond, load_labels +from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory +from frigate.util.process import FrigateProcess + +from .util import tensor_transform + +logger = logging.getLogger(__name__) + + +class ObjectDetector(ABC): + @abstractmethod + def detect(self, tensor_input: np.ndarray, threshold: float = 0.4) -> list: + pass + + +class BaseLocalDetector(ObjectDetector): + def __init__( + self, + detector_config: BaseDetectorConfig | None = None, + labels: str | None = None, + stop_event: MpEvent | None = None, + ) -> None: + self.fps = EventsPerSecond() + if labels is None: + self.labels: dict[int, str] = {} + else: + self.labels = load_labels(labels) + + if detector_config and detector_config.model: + self.input_transform = tensor_transform(detector_config.model.input_tensor) + self.dtype = detector_config.model.input_dtype + else: + self.input_transform = None + self.dtype = InputDTypeEnum.int + + self.detect_api = create_detector(detector_config) + + # If the detector supports stop_event, pass it + if hasattr(self.detect_api, "set_stop_event") and stop_event: + self.detect_api.set_stop_event(stop_event) + + def _transform_input(self, tensor_input: np.ndarray) -> np.ndarray: + if self.input_transform: + tensor_input = np.transpose(tensor_input, self.input_transform) + + if self.dtype == InputDTypeEnum.float: + tensor_input = tensor_input.astype(np.float32) + tensor_input /= 255 + elif self.dtype == InputDTypeEnum.float_denorm: + tensor_input = tensor_input.astype(np.float32) + + return tensor_input + + def detect(self, tensor_input: np.ndarray, threshold: float = 0.4) -> list: + detections = [] + + raw_detections = self.detect_raw(tensor_input) # type: ignore[attr-defined] + + for d in raw_detections: + if int(d[0]) < 0 or int(d[0]) >= len(self.labels): + logger.warning(f"Raw Detect returned invalid label: {d}") + continue + if d[1] < threshold: + break + detections.append( + (self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5])) + ) + self.fps.update() + return detections + + +class LocalObjectDetector(BaseLocalDetector): + def detect_raw(self, tensor_input: np.ndarray) -> np.ndarray: + tensor_input = self._transform_input(tensor_input) + return self.detect_api.detect_raw(tensor_input=tensor_input) # type: ignore[no-any-return] + + +class AsyncLocalObjectDetector(BaseLocalDetector): + def async_send_input(self, tensor_input: np.ndarray, connection_id: str) -> None: + tensor_input = self._transform_input(tensor_input) + self.detect_api.send_input(connection_id, tensor_input) + + def async_receive_output(self) -> Any: + return self.detect_api.receive_output() + + +class DetectorRunner(FrigateProcess): + def __init__( + self, + name: str, + detection_queue: Queue, + cameras: list[str], + avg_speed: Any, + start_time: Any, + config: FrigateConfig, + detector_config: BaseDetectorConfig, + stop_event: MpEvent, + ) -> None: + super().__init__(stop_event, PROCESS_PRIORITY_HIGH, name=name, daemon=True) + self.detection_queue = detection_queue + self.cameras = cameras + self.avg_speed = avg_speed + self.start_time = start_time + self.config = config + self.detector_config = detector_config + self.outputs: dict[str, Any] = {} + + def create_output_shm(self, name: str) -> None: + out_shm = UntrackedSharedMemory(name=f"out-{name}", create=False) + out_np: np.ndarray = np.ndarray((20, 6), dtype=np.float32, buffer=out_shm.buf) + self.outputs[name] = {"shm": out_shm, "np": out_np} + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + + frame_manager = SharedMemoryFrameManager() + object_detector = LocalObjectDetector(detector_config=self.detector_config) + detector_publisher = ObjectDetectorPublisher() + + for name in self.cameras: + self.create_output_shm(name) + + while not self.stop_event.is_set(): + try: + connection_id = self.detection_queue.get(timeout=1) + except queue.Empty: + continue + input_frame = frame_manager.get( + connection_id, + ( + 1, + self.detector_config.model.height, # type: ignore[union-attr] + self.detector_config.model.width, # type: ignore[union-attr] + 3, + ), + ) + + if input_frame is None: + logger.warning(f"Failed to get frame {connection_id} from SHM") + continue + + # detect and send the output + self.start_time.value = datetime.datetime.now().timestamp() + mono_start = time.monotonic() + detections = object_detector.detect_raw(input_frame) + duration = time.monotonic() - mono_start + frame_manager.close(connection_id) + + if connection_id not in self.outputs: + self.create_output_shm(connection_id) + + self.outputs[connection_id]["np"][:] = detections[:] + detector_publisher.publish(connection_id) + self.start_time.value = 0.0 + + self.avg_speed.value = (self.avg_speed.value * 9 + duration) / 10 + + detector_publisher.stop() + logger.info("Exited detection process...") + + +class AsyncDetectorRunner(FrigateProcess): + def __init__( + self, + name: str, + detection_queue: Queue, + cameras: list[str], + avg_speed: Any, + start_time: Any, + config: FrigateConfig, + detector_config: BaseDetectorConfig, + stop_event: MpEvent, + ) -> None: + super().__init__(stop_event, PROCESS_PRIORITY_HIGH, name=name, daemon=True) + self.detection_queue = detection_queue + self.cameras = cameras + self.avg_speed = avg_speed + self.start_time = start_time + self.config = config + self.detector_config = detector_config + self.outputs: dict[str, Any] = {} + self._frame_manager: SharedMemoryFrameManager | None = None + self._publisher: ObjectDetectorPublisher | None = None + self._detector: AsyncLocalObjectDetector | None = None + self.send_times: deque[float] = deque() + + def create_output_shm(self, name: str) -> None: + out_shm = UntrackedSharedMemory(name=f"out-{name}", create=False) + out_np: np.ndarray = np.ndarray((20, 6), dtype=np.float32, buffer=out_shm.buf) + self.outputs[name] = {"shm": out_shm, "np": out_np} + + def _detect_worker(self) -> None: + logger.info("Starting Detect Worker Thread") + while not self.stop_event.is_set(): + try: + connection_id = self.detection_queue.get(timeout=1) + except queue.Empty: + continue + + assert self._frame_manager is not None + input_frame = self._frame_manager.get( + connection_id, + ( + 1, + self.detector_config.model.height, # type: ignore[union-attr] + self.detector_config.model.width, # type: ignore[union-attr] + 3, + ), + ) + + if input_frame is None: + logger.warning(f"Failed to get frame {connection_id} from SHM") + continue + + # mark start time and send to accelerator + self.send_times.append(time.perf_counter()) + assert self._detector is not None + self._detector.async_send_input(input_frame, connection_id) + + def _result_worker(self) -> None: + logger.info("Starting Result Worker Thread") + while not self.stop_event.is_set(): + assert self._detector is not None + connection_id, detections = self._detector.async_receive_output() + + # Handle timeout case (queue.Empty) - just continue + if connection_id is None: + continue + + if not self.send_times: + # guard; shouldn't happen if send/recv are balanced + continue + ts = self.send_times.popleft() + duration = time.perf_counter() - ts + + # release input buffer + assert self._frame_manager is not None + self._frame_manager.close(connection_id) + + if connection_id not in self.outputs: + self.create_output_shm(connection_id) + + # write results and publish + if detections is not None: + self.outputs[connection_id]["np"][:] = detections[:] + assert self._publisher is not None + self._publisher.publish(connection_id) + + # update timers + self.avg_speed.value = (self.avg_speed.value * 9 + duration) / 10 + self.start_time.value = 0.0 + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + + self._frame_manager = SharedMemoryFrameManager() + self._publisher = ObjectDetectorPublisher() + self._detector = AsyncLocalObjectDetector( + detector_config=self.detector_config, stop_event=self.stop_event + ) + + for name in self.cameras: + self.create_output_shm(name) + + t_detect = threading.Thread(target=self._detect_worker, daemon=False) + t_result = threading.Thread(target=self._result_worker, daemon=False) + t_detect.start() + t_result.start() + + try: + while not self.stop_event.is_set(): + time.sleep(0.5) + + logger.info( + "Stop event detected, waiting for detector threads to finish..." + ) + + # Wait for threads to finish processing + t_detect.join(timeout=5) + t_result.join(timeout=5) + + # Shutdown the AsyncDetector + self._detector.detect_api.shutdown() + + self._publisher.stop() + except Exception as e: + logger.error(f"Error during async detector shutdown: {e}") + finally: + logger.info("Exited Async detection process...") + + +class ObjectDetectProcess: + def __init__( + self, + name: str, + detection_queue: Queue, + cameras: list[str], + config: FrigateConfig, + detector_config: BaseDetectorConfig, + stop_event: MpEvent, + ): + self.name = name + self.cameras = cameras + self.detection_queue = detection_queue + self.avg_inference_speed = Value("d", 0.01) + self.detection_start = Value("d", 0.0) + self.detect_process: FrigateProcess | None = None + self.config = config + self.detector_config = detector_config + self.stop_event = stop_event + self.start_or_restart() + + def stop(self) -> None: + # if the process has already exited on its own, just return + if self.detect_process and self.detect_process.exitcode: + return + + if self.detect_process is None: + return + + logging.info("Waiting for detection process to exit gracefully...") + self.detect_process.join(timeout=30) + if self.detect_process.exitcode is None: + logging.info("Detection process didn't exit. Force killing...") + self.detect_process.kill() + self.detect_process.join() + logging.info("Detection process has exited...") + + def start_or_restart(self) -> None: + self.detection_start.value = 0.0 # type: ignore[attr-defined] + if (self.detect_process is not None) and self.detect_process.is_alive(): + self.stop() + + # Async path for MemryX + if self.detector_config.type == "memryx": + self.detect_process = AsyncDetectorRunner( + f"frigate.detector:{self.name}", + self.detection_queue, + self.cameras, + self.avg_inference_speed, + self.detection_start, + self.config, + self.detector_config, + self.stop_event, + ) + else: + self.detect_process = DetectorRunner( + f"frigate.detector:{self.name}", + self.detection_queue, + self.cameras, + self.avg_inference_speed, + self.detection_start, + self.config, + self.detector_config, + self.stop_event, + ) + self.detect_process.start() + + +class RemoteObjectDetector: + def __init__( + self, + name: str, + labels: dict[int, str], + detection_queue: Queue, + model_config: ModelConfig, + stop_event: MpEvent, + ): + self.labels = labels + self.name = name + self.fps = EventsPerSecond() + self.detection_queue = detection_queue + self.stop_event = stop_event + self.shm = UntrackedSharedMemory(name=self.name, create=False) + self.np_shm: np.ndarray = np.ndarray( + (1, model_config.height, model_config.width, 3), + dtype=np.uint8, + buffer=self.shm.buf, + ) + self.out_shm = UntrackedSharedMemory(name=f"out-{self.name}", create=False) + self.out_np_shm: np.ndarray = np.ndarray( + (20, 6), dtype=np.float32, buffer=self.out_shm.buf + ) + self.detector_subscriber = ObjectDetectorSubscriber(name) + + def detect(self, tensor_input: np.ndarray, threshold: float = 0.4) -> list: + detections: list = [] + + if self.stop_event.is_set(): + return detections + + # Drain any stale detection results from the ZMQ buffer before making a new request + # This prevents reading detection results from a previous request + # NOTE: This should never happen, but can in some rare cases + while True: + try: + self.detector_subscriber.socket.recv_string(flags=zmq.NOBLOCK) + except zmq.Again: + break + + # copy input to shared memory + self.np_shm[:] = tensor_input[:] + self.detection_queue.put(self.name) + result = self.detector_subscriber.check_for_update() + + # if it timed out + if result is None: + return detections + + for d in self.out_np_shm: + if d[1] < threshold: + break + detections.append( + (self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5])) + ) + self.fps.update() + return detections + + def cleanup(self) -> None: + self.detector_subscriber.stop() + self.shm.unlink() + self.out_shm.unlink() diff --git a/frigate/object_detection/util.py b/frigate/object_detection/util.py new file mode 100644 index 0000000..4e351d6 --- /dev/null +++ b/frigate/object_detection/util.py @@ -0,0 +1,81 @@ +"""Object detection utilities.""" + +import queue +import threading + +from numpy import ndarray + +from frigate.detectors.detector_config import InputTensorEnum + + +class RequestStore: + """ + A thread-safe hash-based response store that handles creating requests. + """ + + def __init__(self) -> None: + self.request_counter = 0 + self.request_counter_lock = threading.Lock() + self.input_queue: queue.Queue[tuple[int, ndarray]] = queue.Queue() + + def __get_request_id(self) -> int: + with self.request_counter_lock: + request_id = self.request_counter + self.request_counter += 1 + if self.request_counter > 1000000: + self.request_counter = 0 + return request_id + + def put(self, tensor_input: ndarray) -> int: + request_id = self.__get_request_id() + self.input_queue.put((request_id, tensor_input)) + return request_id + + def get(self) -> tuple[int, ndarray] | None: + try: + return self.input_queue.get() + except Exception: + return None + + +class ResponseStore: + """ + A thread-safe hash-based response store that maps request IDs + to their results. Threads can wait on the condition variable until + their request's result appears. + """ + + def __init__(self) -> None: + self.responses: dict[ + int, ndarray + ] = {} # Maps request_id -> (original_input, infer_results) + self.lock = threading.Lock() + self.cond = threading.Condition(self.lock) + + def put(self, request_id: int, response: ndarray) -> None: + with self.cond: + self.responses[request_id] = response + self.cond.notify_all() + + def get(self, request_id: int, timeout: float | None = None) -> ndarray: + with self.cond: + if not self.cond.wait_for( + lambda: request_id in self.responses, timeout=timeout + ): + raise TimeoutError(f"Timeout waiting for response {request_id}") + + return self.responses.pop(request_id) + + +def tensor_transform( + desired_shape: InputTensorEnum, +) -> tuple[int, int, int, int] | None: + # Currently this function only supports BHWC permutations + if desired_shape == InputTensorEnum.nhwc: + return None + elif desired_shape == InputTensorEnum.nchw: + return (0, 3, 1, 2) + elif desired_shape == InputTensorEnum.hwnc: + return (1, 2, 0, 3) + elif desired_shape == InputTensorEnum.hwcn: + return (1, 2, 3, 0) diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py new file mode 100644 index 0000000..cd29dfe --- /dev/null +++ b/frigate/output/birdseye.py @@ -0,0 +1,900 @@ +"""Handle outputting birdseye frames via jsmpeg and go2rtc.""" + +import datetime +import glob +import logging +import math +import os +import queue +import subprocess as sp +import threading +import traceback +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import cv2 +import numpy as np + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import BirdseyeModeEnum, FfmpegConfig, FrigateConfig +from frigate.const import BASE_DIR, BIRDSEYE_PIPE, INSTALL_DIR, UPDATE_BIRDSEYE_LAYOUT +from frigate.output.ws_auth import ws_has_camera_access +from frigate.util.image import ( + SharedMemoryFrameManager, + copy_yuv_to_position, + get_yuv_crop, +) + +logger = logging.getLogger(__name__) + + +def get_standard_aspect_ratio(width: int, height: int) -> tuple[int, int]: + """Ensure that only standard aspect ratios are used.""" + # it is important that all ratios have the same scale + known_aspects = [ + (16, 9), + (9, 16), + (20, 10), + (16, 3), # max wide camera + (16, 6), # reolink duo 2 + (32, 9), # panoramic cameras + (12, 9), + (9, 12), + (22, 15), # Amcrest, NTSC DVT + (1, 1), # fisheye + ] # aspects are scaled to have common relative size + known_aspects_ratios = list( + map(lambda aspect: aspect[0] / aspect[1], known_aspects) + ) + closest = min( + known_aspects_ratios, + key=lambda x: abs(x - (width / height)), + ) + return known_aspects[known_aspects_ratios.index(closest)] + + +def get_canvas_shape(width: int, height: int) -> tuple[int, int]: + """Get birdseye canvas shape.""" + canvas_width = width + canvas_height = height + a_w, a_h = get_standard_aspect_ratio(width, height) + + if round(a_w / a_h, 2) != round(width / height, 2): + canvas_width = int(width // 4 * 4) + canvas_height = int((canvas_width / a_w * a_h) // 4 * 4) + logger.error( + f"Birdseye resolution {width}x{height} is not a supported aspect ratio " + f"and may cause visual distortion; falling back to {canvas_width}x{canvas_height}. " + f"Set width and height to a supported aspect ratio (16:9, 20:10, 16:6, 32:9, 12:9, 22:15, 9:16, 9:12, 16:3, or 1:1)" + ) + + return (canvas_width, canvas_height) + + +class Canvas: + def __init__( + self, + canvas_width: int, + canvas_height: int, + scaling_factor: float, + ) -> None: + self.scaling_factor = scaling_factor + gcd = math.gcd(canvas_width, canvas_height) + self.aspect = get_standard_aspect_ratio( + int(canvas_width / gcd), int(canvas_height / gcd) + ) + self.width = canvas_width + self.height: float = (self.width * self.aspect[1]) / self.aspect[0] + self.coefficient_cache: dict[int, float] = {} + self.aspect_cache: dict[str, tuple[int, int]] = {} + + def get_aspect(self, coefficient: float) -> tuple[float, float]: + return (self.aspect[0] * coefficient, self.aspect[1] * coefficient) + + def get_coefficient(self, camera_count: int) -> float: + return self.coefficient_cache.get(camera_count, self.scaling_factor) + + def set_coefficient(self, camera_count: int, coefficient: float) -> None: + self.coefficient_cache[camera_count] = coefficient + + def get_camera_aspect( + self, cam_name: str, camera_width: int, camera_height: int + ) -> tuple[int, int]: + cached = self.aspect_cache.get(cam_name) + + if cached: + return cached + + gcd = math.gcd(camera_width, camera_height) + camera_aspect = get_standard_aspect_ratio( + int(camera_width / gcd), int(camera_height / gcd) + ) + self.aspect_cache[cam_name] = camera_aspect + return camera_aspect + + +class FFMpegConverter(threading.Thread): + def __init__( + self, + ffmpeg: FfmpegConfig, + input_queue: queue.Queue, + stop_event: MpEvent, + in_width: int, + in_height: int, + out_width: int, + out_height: int, + quality: int, + birdseye_rtsp: bool = False, + ): + super().__init__(name="birdseye_output_converter") + self.camera = "birdseye" + self.input_queue = input_queue + self.stop_event = stop_event + self.bd_pipe: int | None = None + + if birdseye_rtsp: + self.recreate_birdseye_pipe() + + ffmpeg_cmd = [ + ffmpeg.ffmpeg_path, + "-threads", + "1", + "-f", + "rawvideo", + "-pix_fmt", + "yuv420p", + "-video_size", + f"{in_width}x{in_height}", + "-i", + "pipe:", + "-threads", + "1", + "-f", + "mpegts", + "-s", + f"{out_width}x{out_height}", + "-codec:v", + "mpeg1video", + "-q", + f"{quality}", + "-bf", + "0", + "pipe:", + ] + + self.process = sp.Popen( + ffmpeg_cmd, + stdout=sp.PIPE, + stderr=sp.DEVNULL, + stdin=sp.PIPE, + start_new_session=True, + ) + + def recreate_birdseye_pipe(self) -> None: + if self.bd_pipe: + os.close(self.bd_pipe) + + if os.path.exists(BIRDSEYE_PIPE): + os.remove(BIRDSEYE_PIPE) + + os.mkfifo(BIRDSEYE_PIPE, mode=0o777) + stdin = os.open(BIRDSEYE_PIPE, os.O_RDONLY | os.O_NONBLOCK) + self.bd_pipe = os.open(BIRDSEYE_PIPE, os.O_WRONLY) + os.close(stdin) + self.reading_birdseye = False + + def __write(self, b: bytes) -> None: + assert self.process.stdin is not None + self.process.stdin.write(b) + + if self.bd_pipe: + try: + os.write(self.bd_pipe, b) + self.reading_birdseye = True + except BrokenPipeError: + if self.reading_birdseye: + # we know the pipe was being read from and now it is not + # so we should recreate the pipe to ensure no partially-read + # frames exist + logger.debug( + "Recreating the birdseye pipe because it was read from and now is not" + ) + self.recreate_birdseye_pipe() + + return + + def read(self, length: int) -> Any: + try: + return self.process.stdout.read1(length) # type: ignore[union-attr] + except ValueError: + return False + + def exit(self) -> None: + if self.bd_pipe: + os.close(self.bd_pipe) + + self.process.terminate() + try: + self.process.communicate(timeout=30) + except sp.TimeoutExpired: + self.process.kill() + self.process.communicate() + + def run(self) -> None: + while not self.stop_event.is_set(): + try: + frame = self.input_queue.get(True, timeout=1) + self.__write(frame) + except queue.Empty: + pass + + self.exit() + + +class BroadcastThread(threading.Thread): + def __init__( + self, + camera: str, + converter: FFMpegConverter, + websocket_server: Any, + stop_event: MpEvent, + config: FrigateConfig, + ): + super().__init__() + self.camera = camera + self.converter = converter + self.websocket_server = websocket_server + self.stop_event = stop_event + self.config = config + + def run(self) -> None: + while not self.stop_event.is_set(): + buf = self.converter.read(65536) + if buf: + manager = self.websocket_server.manager + with manager.lock: + websockets = manager.websockets.copy() + ws_iter = iter(websockets.values()) + + for ws in ws_iter: + if ( + not ws.terminated + and ws.environ["PATH_INFO"] == f"/{self.camera}" + and ws_has_camera_access(ws, self.camera, self.config) + ): + try: + ws.send(buf, binary=True) + except ValueError: + pass + except (BrokenPipeError, ConnectionResetError, OSError) as e: + logger.debug(f"Websocket unexpectedly closed {e}") + elif self.converter.process.poll() is not None: + break + + +class BirdsEyeFrameManager: + def __init__( + self, + config: FrigateConfig, + stop_event: MpEvent, + ): + self.config = config + width, height = get_canvas_shape(config.birdseye.width, config.birdseye.height) + self.frame_shape = (height, width) + self.yuv_shape = (height * 3 // 2, width) + self.frame: np.ndarray = np.ndarray(self.yuv_shape, dtype=np.uint8) + self.canvas = Canvas(width, height, config.birdseye.layout.scaling_factor) + self.stop_event = stop_event + self.last_refresh_time: float = 0 + + # initialize the frame as black and with the Frigate logo + self.blank_frame = np.zeros(self.yuv_shape, np.uint8) + self.blank_frame[:] = 128 + self.blank_frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] = 16 + + # find and copy the logo on the blank frame + birdseye_logo = None + + custom_logo_files = glob.glob(f"{BASE_DIR}/custom.png") + + if len(custom_logo_files) > 0: + birdseye_logo = cv2.imread(custom_logo_files[0], cv2.IMREAD_UNCHANGED) + + if birdseye_logo is None: + logo_files = glob.glob( + os.path.join(INSTALL_DIR, "frigate/images/birdseye.png") + ) + + if len(logo_files) > 0: + birdseye_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED) + + if birdseye_logo is not None: + if birdseye_logo.ndim == 2: + # Grayscale image (no channels) — use directly as luminance + transparent_layer = birdseye_logo + elif birdseye_logo.shape[2] >= 4: + # RGBA — use alpha channel as luminance + transparent_layer = birdseye_logo[:, :, 3] + else: + # RGB or other format without alpha — convert to grayscale + transparent_layer = cv2.cvtColor(birdseye_logo, cv2.COLOR_BGR2GRAY) + y_offset = height // 2 - transparent_layer.shape[0] // 2 + x_offset = width // 2 - transparent_layer.shape[1] // 2 + self.blank_frame[ + y_offset : y_offset + transparent_layer.shape[0], + x_offset : x_offset + transparent_layer.shape[1], + ] = transparent_layer + else: + logger.warning("Unable to read Frigate logo") + + self.frame[:] = self.blank_frame + + self.cameras: dict[str, Any] = {} + for camera in self.config.cameras.keys(): + self.add_camera(camera) + + self.camera_layout: list[Any] = [] + self.active_cameras: set[str] = set() + self.last_output_time = 0.0 + + def add_camera(self, cam: str) -> None: + """Add a camera to self.cameras with the correct structure.""" + settings = self.config.cameras[cam] + # precalculate the coordinates for all the channels + y, u1, u2, v1, v2 = get_yuv_crop( + settings.frame_shape_yuv, + ( + 0, + 0, + settings.frame_shape[1], + settings.frame_shape[0], + ), + ) + self.cameras[cam] = { + "dimensions": [ + settings.detect.width, + settings.detect.height, + ], + "last_active_frame": 0.0, + "current_frame": 0.0, + "layout_frame": 0.0, + "channel_dims": { + "y": y, + "u1": u1, + "u2": u2, + "v1": v1, + "v2": v2, + }, + } + + def remove_camera(self, cam: str) -> None: + """Remove a camera from self.cameras.""" + if cam in self.cameras: + del self.cameras[cam] + + def clear_frame(self) -> None: + logger.debug("Clearing the birdseye frame") + self.frame[:] = self.blank_frame + + def copy_to_position( + self, + position: Any, + camera: str | None = None, + frame: np.ndarray | None = None, + ) -> None: + if camera is None: + frame = None + channel_dims = None + else: + if frame is None: + logger.debug(f"Unable to copy frame {camera} to birdseye.") + return + + channel_dims = self.cameras[camera]["channel_dims"] + + copy_yuv_to_position( + self.frame, + [position[1], position[0]], + [position[3], position[2]], + frame, + channel_dims, + ) + + def camera_active( + self, mode: Any, object_box_count: int, motion_box_count: int + ) -> bool: + if mode == BirdseyeModeEnum.continuous: + return True + + if mode == BirdseyeModeEnum.motion and motion_box_count > 0: + return True + + if mode == BirdseyeModeEnum.objects and object_box_count > 0: + return True + + return False + + def get_camera_coordinates(self) -> dict[str, dict[str, int]]: + """Return the coordinates of each camera in the current layout.""" + coordinates = {} + for row in self.camera_layout: + for position in row: + camera_name, (x, y, width, height) = position + coordinates[camera_name] = { + "x": x, + "y": y, + "width": width, + "height": height, + } + return coordinates + + def update_frame(self, frame: np.ndarray | None = None) -> tuple[bool, bool]: + """ + Update birdseye, optionally with a new frame. + Returns (frame_changed, layout_changed) to indicate if the frame or layout changed. + """ + + # determine how many cameras are tracking objects within the last inactivity_threshold seconds + active_cameras: set[str] = set( + [ + cam + for cam, cam_data in self.cameras.items() + if cam in self.config.cameras + and self.config.cameras[cam].birdseye.enabled + and self.config.cameras[cam].enabled_in_config + and self.config.cameras[cam].enabled + and cam_data["last_active_frame"] > 0 + and cam_data["current_frame_time"] - cam_data["last_active_frame"] + < self.config.birdseye.inactivity_threshold + ] + ) + logger.debug(f"Active cameras: {active_cameras}") + + max_cameras = self.config.birdseye.layout.max_cameras + max_camera_refresh = False + if max_cameras: + now = datetime.datetime.now().timestamp() + + if len(active_cameras) == max_cameras and now - self.last_refresh_time < 10: + # don't refresh cameras too often + active_cameras = self.active_cameras + else: + limited_active_cameras = sorted( + active_cameras, + key=lambda active_camera: ( + self.cameras[active_camera]["current_frame_time"] + - self.cameras[active_camera]["last_active_frame"] + ), + ) + active_cameras = set(limited_active_cameras[:max_cameras]) + max_camera_refresh = True + self.last_refresh_time = now + + # Track if the frame or layout changes + frame_changed = False + layout_changed = False + + # If no active cameras and layout is already empty, no update needed + if len(active_cameras) == 0: + # if the layout is already cleared + if len(self.camera_layout) == 0: + return False, False + # if the layout needs to be cleared + self.camera_layout = [] + self.active_cameras = set() + self.clear_frame() + frame_changed = True + layout_changed = True + else: + # Determine if layout needs resetting + if len(self.active_cameras) - len(active_cameras) == 0: + if ( + len(self.active_cameras) == 1 + and self.active_cameras != active_cameras + ): + reset_layout = True + elif max_camera_refresh: + reset_layout = True + else: + reset_layout = False + else: + reset_layout = True + + if reset_layout: + logger.debug("Resetting Birdseye layout...") + self.clear_frame() + self.active_cameras = active_cameras + layout_changed = True # Layout is changing due to reset + # this also converts added_cameras from a set to a list since we need + # to pop elements in order + active_cameras_to_add = sorted( + active_cameras, + # sort cameras by order and by name if the order is the same + key=lambda active_camera: ( + self.config.cameras[active_camera].birdseye.order, + active_camera, + ), + ) + if len(active_cameras) == 1: + # show single camera as fullscreen + camera = active_cameras_to_add[0] + camera_dims = self.cameras[camera]["dimensions"].copy() + scaled_width = int( + self.canvas.height * camera_dims[0] / camera_dims[1] + ) + + # center camera view in canvas and ensure that it fits + if scaled_width < self.canvas.width: + coefficient: float = 1 + x_offset = int((self.canvas.width - scaled_width) / 2) + else: + coefficient = self.canvas.width / scaled_width + x_offset = int( + (self.canvas.width - (scaled_width * coefficient)) / 2 + ) + + self.camera_layout = [ + [ + ( + camera, + ( + x_offset, + 0, + int(scaled_width * coefficient), + int(self.canvas.height * coefficient), + ), + ) + ] + ] + else: + # calculate optimal layout + coefficient = self.canvas.get_coefficient(len(active_cameras)) + calculating = True + + # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas + while calculating: + if self.stop_event.is_set(): + return frame_changed, layout_changed + + layout_candidate = self.calculate_layout( + active_cameras_to_add, coefficient + ) + + if not layout_candidate: + if coefficient < 10: + coefficient += 1 + continue + else: + logger.error( + "Error finding appropriate birdseye layout" + ) + return frame_changed, layout_changed + calculating = False + self.canvas.set_coefficient(len(active_cameras), coefficient) + + self.camera_layout = layout_candidate or [] + frame_changed = True + + # Draw the layout + for row in self.camera_layout: + for position in row: + src_frame = self.cameras[position[0]]["current_frame"] + if src_frame is None or src_frame.size == 0: + logger.debug(f"Skipping invalid frame for {position[0]}") + continue + self.copy_to_position(position[1], position[0], src_frame) + if frame is not None: # Frame presence indicates a potential change + frame_changed = True + + return frame_changed, layout_changed + + def calculate_layout( + self, + cameras_to_add: list[str], + coefficient: float, + ) -> list[list[Any]] | None: + """Calculate the optimal layout for 2+ cameras.""" + + def map_layout( + camera_layout: list[list[Any]], row_height: int + ) -> tuple[int, int, list[list[Any]] | None]: + """Map the calculated layout.""" + candidate_layout = [] + starting_x = 0 + x = 0 + max_width = 0 + y = 0 + + for row in camera_layout: + final_row = [] + max_width = max(max_width, x) + x = starting_x + for cameras in row: + camera_dims = self.cameras[cameras[0]]["dimensions"].copy() + camera_aspect = cameras[1] + + if camera_dims[1] > camera_dims[0]: + scaled_height = int(row_height * 2) + scaled_width = int(scaled_height * camera_aspect) + starting_x = scaled_width + else: + scaled_height = row_height + scaled_width = int(scaled_height * camera_aspect) + + # layout is too large + if ( + x + scaled_width > self.canvas.width + or y + scaled_height > self.canvas.height + ): + return x + scaled_width, y + scaled_height, None + + final_row.append((cameras[0], (x, y, scaled_width, scaled_height))) + x += scaled_width + + y += row_height + candidate_layout.append(final_row) + + if max_width == 0: + max_width = x + + return max_width, y, candidate_layout + + canvas_aspect_x, canvas_aspect_y = self.canvas.get_aspect(coefficient) + camera_layout: list[list[Any]] = [] + camera_layout.append([]) + starting_x = 0 + x = starting_x + y = 0 + y_i = 0 + max_y = 0 + for camera in cameras_to_add: + camera_dims = self.cameras[camera]["dimensions"].copy() + camera_aspect_x, camera_aspect_y = self.canvas.get_camera_aspect( + camera, camera_dims[0], camera_dims[1] + ) + + if camera_dims[1] > camera_dims[0]: + portrait = True + else: + portrait = False + + if (x + camera_aspect_x) <= canvas_aspect_x: + # insert if camera can fit on current row + camera_layout[y_i].append( + ( + camera, + camera_aspect_x / camera_aspect_y, + ) + ) + + if portrait: + starting_x = camera_aspect_x + else: + max_y = max( + max_y, + camera_aspect_y, + ) + + x += camera_aspect_x + else: + # move on to the next row and insert + y += max_y + y_i += 1 + camera_layout.append([]) + x = starting_x + + if x + camera_aspect_x > canvas_aspect_x: + return None + + camera_layout[y_i].append( + ( + camera, + camera_aspect_x / camera_aspect_y, + ) + ) + x += camera_aspect_x + + if y + max_y > canvas_aspect_y: + return None + + row_height = int(self.canvas.height / coefficient) + total_width, total_height, standard_candidate_layout = map_layout( + camera_layout, row_height + ) + + if not standard_candidate_layout: + # if standard layout didn't work + # try reducing row_height by the % overflow + scale_down_percent = max( + total_width / self.canvas.width, + total_height / self.canvas.height, + ) + row_height = int(row_height / scale_down_percent) + total_width, total_height, standard_candidate_layout = map_layout( + camera_layout, row_height + ) + + if not standard_candidate_layout: + return None + + # layout can't be optimized more + if total_width / self.canvas.width >= 0.99: + return standard_candidate_layout + + scale_up_percent = min( + 1 / (total_width / self.canvas.width), + 1 / (total_height / self.canvas.height), + ) + row_height = int(row_height * scale_up_percent) + _, _, scaled_layout = map_layout(camera_layout, row_height) + + if scaled_layout: + return scaled_layout + else: + return standard_candidate_layout + + def update( + self, + camera: str, + object_count: int, + motion_count: int, + frame_time: float, + frame: np.ndarray, + ) -> tuple[bool, bool]: + """ + Update birdseye for a specific camera with new frame data. + Returns (frame_changed, layout_changed) to indicate if the frame or layout changed. + """ + # don't process if camera was removed or birdseye is disabled + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return False, False + + force_update = False + + # disabling birdseye is a little tricky + if not camera_config.birdseye.enabled or not camera_config.enabled: + # if we've rendered a frame (we have a value for last_active_frame) + # then we need to set it to zero + if self.cameras[camera]["last_active_frame"] > 0: + self.cameras[camera]["last_active_frame"] = 0 + force_update = True + else: + return False, False + + # update the last active frame for the camera + self.cameras[camera]["current_frame"] = frame.copy() + self.cameras[camera]["current_frame_time"] = frame_time + if self.camera_active(camera_config.birdseye.mode, object_count, motion_count): + self.cameras[camera]["last_active_frame"] = frame_time + + now = datetime.datetime.now().timestamp() + + # limit output to 10 fps + if not force_update and (now - self.last_output_time) < 1 / 10: + return False, False + + try: + frame_changed, layout_changed = self.update_frame(frame) + except Exception: + frame_changed, layout_changed = False, False + self.active_cameras = set() + self.camera_layout = [] + print(traceback.format_exc()) + + # if the frame was updated or the fps is too low, send frame + if force_update or frame_changed or (now - self.last_output_time) > 1: + self.last_output_time = now + return True, layout_changed + + return False, layout_changed + + +class Birdseye: + def __init__( + self, + config: FrigateConfig, + stop_event: MpEvent, + websocket_server: Any, + ) -> None: + self.config = config + canvas_width, canvas_height = get_canvas_shape( + config.birdseye.width, config.birdseye.height + ) + self.input: queue.Queue[bytes] = queue.Queue(maxsize=10) + self.converter = FFMpegConverter( + config.ffmpeg, + self.input, + stop_event, + canvas_width, + canvas_height, + canvas_width, + canvas_height, + config.birdseye.quality, + config.birdseye.restream, + ) + self.broadcaster = BroadcastThread( + "birdseye", + self.converter, + websocket_server, + stop_event, + config, + ) + self.birdseye_manager = BirdsEyeFrameManager(self.config, stop_event) + self.frame_manager = SharedMemoryFrameManager() + self.stop_event = stop_event + self.requestor = InterProcessRequestor() + self.idle_fps: float = self.config.birdseye.idle_heartbeat_fps + self._idle_interval: float | None = ( + (1.0 / self.idle_fps) if self.idle_fps > 0 else None + ) + + if config.birdseye.restream: + self.birdseye_buffer: Any = self.frame_manager.create( + "birdseye", + self.birdseye_manager.yuv_shape[0] * self.birdseye_manager.yuv_shape[1], + ) + + self.converter.start() + self.broadcaster.start() + + def __send_new_frame(self) -> None: + frame_bytes = self.birdseye_manager.frame.tobytes() + + if self.config.birdseye.restream: + self.birdseye_buffer[:] = frame_bytes + + try: + self.input.put_nowait(frame_bytes) + except queue.Full: + # drop frames if queue is full + pass + + def all_cameras_disabled(self) -> None: + self.birdseye_manager.clear_frame() + self.__send_new_frame() + + def add_camera(self, camera: str) -> None: + """Add a camera to the birdseye manager.""" + self.birdseye_manager.add_camera(camera) + logger.debug(f"Added camera {camera} to birdseye") + + def remove_camera(self, camera: str) -> None: + """Remove a camera from the birdseye manager.""" + self.birdseye_manager.remove_camera(camera) + logger.debug(f"Removed camera {camera} from birdseye") + + def write_data( + self, + camera: str, + current_tracked_objects: list[dict[str, Any]], + motion_boxes: list[list[int]], + frame_time: float, + frame: np.ndarray, + ) -> None: + frame_changed, frame_layout_changed = self.birdseye_manager.update( + camera, + len([o for o in current_tracked_objects if not o["stationary"]]), + len(motion_boxes), + frame_time, + frame, + ) + if frame_changed: + self.__send_new_frame() + + if frame_layout_changed: + coordinates = self.birdseye_manager.get_camera_coordinates() + self.requestor.send_data(UPDATE_BIRDSEYE_LAYOUT, coordinates) + if self._idle_interval: + now = datetime.datetime.now().timestamp() + is_idle = len(self.birdseye_manager.camera_layout) == 0 + if ( + is_idle + and (now - self.birdseye_manager.last_output_time) + >= self._idle_interval + ): + self.__send_new_frame() + + def stop(self) -> None: + self.converter.join() + self.broadcaster.join() diff --git a/frigate/output/camera.py b/frigate/output/camera.py new file mode 100644 index 0000000..88d16ed --- /dev/null +++ b/frigate/output/camera.py @@ -0,0 +1,184 @@ +"""Handle outputting individual cameras via jsmpeg.""" + +import logging +import queue +import subprocess as sp +import threading +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +from frigate.config import CameraConfig, FfmpegConfig, FrigateConfig +from frigate.output.ws_auth import ws_has_camera_access + +logger = logging.getLogger(__name__) + + +class FFMpegConverter(threading.Thread): + def __init__( + self, + camera: str, + ffmpeg: FfmpegConfig, + input_queue: queue.Queue, + stop_event: MpEvent, + in_width: int, + in_height: int, + out_width: int, + out_height: int, + quality: int, + ): + super().__init__(name=f"{camera}_output_converter") + self.camera = camera + self.input_queue = input_queue + self.stop_event = stop_event + + ffmpeg_cmd = [ + ffmpeg.ffmpeg_path, + "-threads", + "1", + "-f", + "rawvideo", + "-pix_fmt", + "yuv420p", + "-video_size", + f"{in_width}x{in_height}", + "-i", + "pipe:", + "-threads", + "1", + "-f", + "mpegts", + "-s", + f"{out_width}x{out_height}", + "-codec:v", + "mpeg1video", + "-q", + f"{quality}", + "-bf", + "0", + "pipe:", + ] + + self.process = sp.Popen( + ffmpeg_cmd, + stdout=sp.PIPE, + stderr=sp.DEVNULL, + stdin=sp.PIPE, + start_new_session=True, + ) + + def __write(self, b: bytes) -> None: + assert self.process.stdin is not None + self.process.stdin.write(b) + + def read(self, length: int) -> Any: + try: + return self.process.stdout.read1(length) # type: ignore[union-attr] + except ValueError: + return False + + def exit(self) -> None: + self.process.terminate() + + try: + self.process.communicate(timeout=30) + except sp.TimeoutExpired: + self.process.kill() + self.process.communicate() + + def run(self) -> None: + while not self.stop_event.is_set(): + try: + frame = self.input_queue.get(True, timeout=1) + self.__write(frame) + except queue.Empty: + pass + + self.exit() + + +class BroadcastThread(threading.Thread): + def __init__( + self, + camera: str, + converter: FFMpegConverter, + websocket_server: Any, + stop_event: MpEvent, + config: FrigateConfig, + ): + super().__init__() + self.camera = camera + self.converter = converter + self.websocket_server = websocket_server + self.stop_event = stop_event + self.config = config + + def run(self) -> None: + while not self.stop_event.is_set(): + buf = self.converter.read(65536) + if buf: + manager = self.websocket_server.manager + with manager.lock: + websockets = manager.websockets.copy() + ws_iter = iter(websockets.values()) + + for ws in ws_iter: + if ( + not ws.terminated + and ws.environ["PATH_INFO"] == f"/{self.camera}" + and ws_has_camera_access(ws, self.camera, self.config) + ): + try: + ws.send(buf, binary=True) + except ValueError: + pass + except (BrokenPipeError, ConnectionResetError) as e: + logger.debug(f"Websocket unexpectedly closed {e}") + elif self.converter.process.poll() is not None: + break + + +class JsmpegCamera: + def __init__( + self, + config: CameraConfig, + frigate_config: FrigateConfig, + stop_event: MpEvent, + websocket_server: Any, + ) -> None: + self.config = config + self.input: queue.Queue[bytes] = queue.Queue(maxsize=config.detect.fps) + width = int( + config.live.height * (config.frame_shape[1] / config.frame_shape[0]) + ) + self.converter = FFMpegConverter( + config.name or "", + config.ffmpeg, + self.input, + stop_event, + config.frame_shape[1], + config.frame_shape[0], + width, + config.live.height, + config.live.quality, + ) + self.broadcaster = BroadcastThread( + config.name or "", + self.converter, + websocket_server, + stop_event, + frigate_config, + ) + + self.converter.start() + self.broadcaster.start() + + def write_frame(self, frame_bytes: bytes) -> None: + try: + self.input.put_nowait(frame_bytes) + except queue.Full: + # drop frames if queue is full + pass + + def stop(self) -> None: + self.converter.join() + self.broadcaster.join() diff --git a/frigate/output/output.py b/frigate/output/output.py new file mode 100644 index 0000000..67dba52 --- /dev/null +++ b/frigate/output/output.py @@ -0,0 +1,371 @@ +"""Handle outputting raw frigate frames""" + +import datetime +import logging +import os +import shutil +import threading +from multiprocessing.synchronize import Event as MpEvent +from wsgiref.simple_server import make_server + +from ws4py.server.wsgirefserver import ( + WebSocketWSGIHandler, + WebSocketWSGIRequestHandler, + WSGIServer, +) +from ws4py.server.wsgiutils import WebSocketWSGIApplication + +from frigate.comms.config_updater import ConfigSubscriber +from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum +from frigate.comms.ws import WebSocket +from frigate.config import FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + CACHE_DIR, + CLIPS_DIR, + PROCESS_PRIORITY_MED, + REPLAY_CAMERA_PREFIX, +) +from frigate.output.birdseye import Birdseye +from frigate.output.camera import JsmpegCamera +from frigate.output.preview import PreviewRecorder +from frigate.output.ws_auth import ws_has_camera_access +from frigate.util.image import SharedMemoryFrameManager, get_blank_yuv_frame +from frigate.util.process import FrigateProcess + +logger = logging.getLogger(__name__) + + +def check_disabled_camera_update( + config: FrigateConfig, + birdseye: Birdseye | None, + previews: dict[str, PreviewRecorder], + write_times: dict[str, float], +) -> None: + """Check if camera is disabled / offline and needs an update.""" + now = datetime.datetime.now().timestamp() + has_enabled_camera = False + + for camera, last_update in write_times.items(): + offline_time = now - last_update + + if config.cameras[camera].enabled: + has_enabled_camera = True + else: + # flag camera as offline when it is disabled + previews[camera].flag_offline(now) + + if offline_time > 1: + # last camera update was more than 1 second ago + # need to send empty data to birdseye because current + # frame is now out of date + cam_width = config.cameras[camera].detect.width + cam_height = config.cameras[camera].detect.height + + if cam_width is None or cam_height is None: + raise ValueError(f"Camera {camera} detect dimensions not configured") + + if birdseye and offline_time < 10: + # we only need to send blank frames to birdseye at the beginning of a camera being offline + birdseye.write_data( + camera, + [], + [], + now, + get_blank_yuv_frame(cam_width, cam_height), + ) + + if not has_enabled_camera and birdseye: + birdseye.all_cameras_disabled() + + +class OutputProcess(FrigateProcess): + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + super().__init__( + stop_event, PROCESS_PRIORITY_MED, name="frigate.output", daemon=True + ) + self.config = config + + def is_debug_replay_camera(self, camera: str) -> bool: + return camera.startswith(REPLAY_CAMERA_PREFIX) + + def add_camera( + self, + camera: str, + websocket_server: WSGIServer, + jsmpeg_cameras: dict[str, JsmpegCamera], + preview_recorders: dict[str, PreviewRecorder], + preview_write_times: dict[str, float], + birdseye: Birdseye | None, + ) -> None: + camera_config = self.config.cameras[camera] + jsmpeg_cameras[camera] = JsmpegCamera( + camera_config, self.config, self.stop_event, websocket_server + ) + preview_recorders[camera] = PreviewRecorder(camera_config) + preview_write_times[camera] = 0 + + if ( + birdseye is not None + and self.config.birdseye.enabled + and camera_config.birdseye.enabled + ): + birdseye.add_camera(camera) + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + + frame_manager = SharedMemoryFrameManager() + + # start a websocket server on 8082 + WebSocketWSGIHandler.http_version = "1.1" + websocket_server = make_server( + "127.0.0.1", + 8082, + server_class=WSGIServer, + handler_class=WebSocketWSGIRequestHandler, + app=WebSocketWSGIApplication(handler_cls=WebSocket), + ) + websocket_server.initialize_websockets_manager() + websocket_thread = threading.Thread(target=websocket_server.serve_forever) + + detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video.value) + config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.birdseye, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.record, + ], + ) + birdseye_config_subscriber = ConfigSubscriber("config/birdseye", exact=True) + + jsmpeg_cameras: dict[str, JsmpegCamera] = {} + birdseye: Birdseye | None = None + preview_recorders: dict[str, PreviewRecorder] = {} + preview_write_times: dict[str, float] = {} + failed_frame_requests: dict[str, int] = {} + last_disabled_cam_check = datetime.datetime.now().timestamp() + + move_preview_frames("cache") + + for camera, cam_config in self.config.cameras.items(): + if not cam_config.enabled_in_config or self.is_debug_replay_camera(camera): + continue + + self.add_camera( + camera, + websocket_server, + jsmpeg_cameras, + preview_recorders, + preview_write_times, + birdseye, + ) + + if self.config.birdseye.enabled: + birdseye = Birdseye(self.config, self.stop_event, websocket_server) + + websocket_thread.start() + + while not self.stop_event.is_set(): + update_topic, birdseye_config = ( + birdseye_config_subscriber.check_for_update() + ) + + if update_topic is not None and birdseye_config 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 + updates = config_subscriber.check_for_updates() + + if CameraConfigUpdateEnum.add in updates: + for camera in updates["add"]: + if not self.is_debug_replay_camera(camera): + self.add_camera( + camera, + websocket_server, + jsmpeg_cameras, + preview_recorders, + preview_write_times, + birdseye, + ) + + _result = detection_subscriber.check_for_update(timeout=1) + if _result is None: + continue + (topic, data) = _result + now = datetime.datetime.now().timestamp() + + if now - last_disabled_cam_check > 5: + # check disabled cameras every 5 seconds + last_disabled_cam_check = now + check_disabled_camera_update( + self.config, birdseye, preview_recorders, preview_write_times + ) + + if not topic or data is None: + continue + + ( + camera, + frame_name, + frame_time, + current_tracked_objects, + motion_boxes, + _, + ) = data + + if ( + camera not in self.config.cameras + or not self.config.cameras[camera].enabled + or self.is_debug_replay_camera(camera) + ): + continue + + frame = frame_manager.get( + frame_name, self.config.cameras[camera].frame_shape_yuv + ) + + if frame is None: + logger.debug(f"Failed to get frame {frame_name} from SHM") + failed_frame_requests[camera] = failed_frame_requests.get(camera, 0) + 1 + + if ( + failed_frame_requests[camera] + > self.config.cameras[camera].detect.fps + ): + logger.warning( + f"Failed to retrieve many frames for {camera} from SHM, consider increasing SHM size if this continues." + ) + + continue + else: + failed_frame_requests[camera] = 0 + + # send frames for low fps recording + preview_recorders[camera].write_data( + current_tracked_objects, motion_boxes, frame_time, frame + ) + preview_write_times[camera] = frame_time + + # send camera frame to ffmpeg process if websockets are connected + if any( + ws.environ["PATH_INFO"].endswith(camera) + and ws_has_camera_access(ws, camera, self.config) + for ws in websocket_server.manager + ): + # write to the converter for the camera if clients are listening to the specific camera + jsmpeg_cameras[camera].write_frame(frame.tobytes()) + + # send output data to birdseye if websocket is connected or restreaming + if ( + self.config.birdseye.enabled + and birdseye is not None + and ( + self.config.birdseye.restream + or any( + ws.environ["PATH_INFO"].endswith("birdseye") + and ws_has_camera_access(ws, "birdseye", self.config) + for ws in websocket_server.manager + ) + ) + ): + birdseye.write_data( + camera, + current_tracked_objects, + motion_boxes, + frame_time, + frame, + ) + + frame_manager.close(frame_name) + + move_preview_frames("clips") + + while True: + _cleanup_result = detection_subscriber.check_for_update(timeout=0) + if _cleanup_result is None: + break + (topic, data) = _cleanup_result + + if not topic or data is None: + break + + ( + camera, + frame_name, + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) = data + + frame = frame_manager.get( + frame_name, self.config.cameras[camera].frame_shape_yuv + ) + frame_manager.close(frame_name) + + detection_subscriber.stop() + + for jsmpeg in jsmpeg_cameras.values(): + jsmpeg.stop() + + for preview in preview_recorders.values(): + preview.stop() + + if birdseye is not None: + birdseye.stop() + + config_subscriber.stop() + birdseye_config_subscriber.stop() + websocket_server.manager.close_all() + websocket_server.manager.stop() + websocket_server.manager.join() + websocket_server.shutdown() + websocket_thread.join() + logger.info("exiting output process...") + + +def move_preview_frames(loc: str) -> None: + preview_holdover = os.path.join(CLIPS_DIR, "preview_restart_cache") + preview_cache = os.path.join(CACHE_DIR, "preview_frames") + + if loc == "clips": + src = preview_cache + dst = preview_holdover + elif loc == "cache": + src = preview_holdover + dst = preview_cache + else: + return + + try: + if not os.path.exists(src): + return + + shutil.move(src, dst) + + except PermissionError: + logger.error( + "Insufficient permissions while moving preview restart cache from %s to %s", + src, + dst, + ) + except shutil.Error: + logger.error( + "Failed to move preview restart cache from %s to %s", + src, + dst, + ) diff --git a/frigate/output/preview.py b/frigate/output/preview.py new file mode 100644 index 0000000..cce0556 --- /dev/null +++ b/frigate/output/preview.py @@ -0,0 +1,471 @@ +"""Handle outputting low res / fps preview segments from decoded frames.""" + +import datetime +import logging +import os +import shutil +import subprocess as sp +import threading +import time +from pathlib import Path +from typing import Any + +import cv2 +import numpy as np + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import CameraConfig, RecordQualityEnum +from frigate.const import CACHE_DIR, CLIPS_DIR, INSERT_PREVIEW, PREVIEW_FRAME_TYPE +from frigate.ffmpeg_presets import ( + FPS_VFR_PARAM, + EncodeTypeEnum, + parse_preset_hardware_acceleration_encode, +) +from frigate.models import Previews +from frigate.util.image import copy_yuv_to_position, get_blank_yuv_frame, get_yuv_crop + +logger = logging.getLogger(__name__) + +FOLDER_PREVIEW_FRAMES = "preview_frames" +PREVIEW_CACHE_DIR = os.path.join(CACHE_DIR, FOLDER_PREVIEW_FRAMES) +PREVIEW_SEGMENT_DURATION = 3600 # one hour +# important to have lower keyframe to maintain scrubbing performance +PREVIEW_KEYFRAME_INTERVAL = 40 +PREVIEW_HEIGHT = 180 +PREVIEW_QUALITY_WEBP = { + RecordQualityEnum.very_low: 70, + RecordQualityEnum.low: 80, + RecordQualityEnum.medium: 80, + RecordQualityEnum.high: 80, + RecordQualityEnum.very_high: 86, +} +PREVIEW_QUALITY_BIT_RATES = { + RecordQualityEnum.very_low: 7168, + RecordQualityEnum.low: 8196, + RecordQualityEnum.medium: 9216, + RecordQualityEnum.high: 9864, + RecordQualityEnum.very_high: 10096, +} +# the -qmax param for ffmpeg prevents the encoder from overly compressing frames while still trying to hit the bitrate target +# lower values are higher quality. This is especially important for iniitial frames in the segment +PREVIEW_QMAX_PARAM = { + RecordQualityEnum.very_low: "", + RecordQualityEnum.low: "", + RecordQualityEnum.medium: "", + RecordQualityEnum.high: " -qmax 25", + RecordQualityEnum.very_high: " -qmax 25", +} + + +def get_cache_image_name(camera: str, frame_time: float) -> str: + """Get the image name in cache.""" + return os.path.join( + CACHE_DIR, + f"{FOLDER_PREVIEW_FRAMES}/preview_{camera}-{frame_time}.{PREVIEW_FRAME_TYPE}", + ) + + +def get_most_recent_preview_frame( + camera: str, before: float | None = None +) -> str | None: + """Get the most recent preview frame for a camera.""" + if not os.path.exists(PREVIEW_CACHE_DIR): + return None + + try: + # files are named preview_{camera}-{timestamp}.webp + # we want the largest timestamp that is less than or equal to before + preview_files = [ + f + for f in os.listdir(PREVIEW_CACHE_DIR) + if f.startswith(f"preview_{camera}-") + and f.endswith(f".{PREVIEW_FRAME_TYPE}") + ] + + if not preview_files: + return None + + # sort by timestamp in descending order + # filenames are like preview_front-1712345678.901234.webp + preview_files.sort(reverse=True) + + if before is None: + return os.path.join(PREVIEW_CACHE_DIR, preview_files[0]) + + for file_name in preview_files: + try: + # Extract timestamp: preview_front-1712345678.901234.webp + # Split by dash and extension + timestamp_part = file_name.split("-")[-1].split( + f".{PREVIEW_FRAME_TYPE}" + )[0] + timestamp = float(timestamp_part) + + if timestamp <= before: + return os.path.join(PREVIEW_CACHE_DIR, file_name) + except (ValueError, IndexError): + continue + + return None + except Exception as e: + logger.error(f"Error searching for most recent preview frame: {e}") + return None + + +class FFMpegConverter(threading.Thread): + """Convert a list of still frames into a vfr mp4.""" + + def __init__( + self, + config: CameraConfig, + frame_times: list[float], + requestor: InterProcessRequestor, + ): + super().__init__(name=f"{config.name}_preview_converter") + self.config = config + self.frame_times = frame_times + self.requestor = requestor + self.path = os.path.join( + CLIPS_DIR, + f"previews/{self.config.name}/{self.frame_times[0]}-{self.frame_times[-1]}.mp4", + ) + + # write a PREVIEW at fps and 1 key frame per clip + self.ffmpeg_cmd = parse_preset_hardware_acceleration_encode( + config.ffmpeg.ffmpeg_path, + "default", + input="-f concat -y -protocol_whitelist pipe,file -safe 0 -threads 1 -i /dev/stdin", + output=f"-threads 1 -g {PREVIEW_KEYFRAME_INTERVAL} -bf 0 -b:v {PREVIEW_QUALITY_BIT_RATES[self.config.record.preview.quality]}{PREVIEW_QMAX_PARAM[self.config.record.preview.quality]} {FPS_VFR_PARAM} -movflags +faststart -pix_fmt yuv420p {self.path}", + type=EncodeTypeEnum.preview, + ) + + def run(self) -> None: + # generate input list + item_count = len(self.frame_times) + playlist = [] + + for t_idx in range(0, item_count): + if t_idx == item_count - 1: + # last frame does not get a duration + playlist.append( + f"file '{get_cache_image_name(self.config.name, self.frame_times[t_idx])}'" # type: ignore[arg-type] + ) + continue + + playlist.append( + f"file '{get_cache_image_name(self.config.name, self.frame_times[t_idx])}'" # type: ignore[arg-type] + ) + playlist.append( + f"duration {self.frame_times[t_idx + 1] - self.frame_times[t_idx]}" + ) + + try: + p = sp.run( + self.ffmpeg_cmd.split(" "), + input="\n".join(playlist), + encoding="ascii", + capture_output=True, + ) + except BlockingIOError: + logger.warning( + f"Failed to create preview for {self.config.name}, retrying..." + ) + time.sleep(2) + p = sp.run( + self.ffmpeg_cmd.split(" "), + input="\n".join(playlist), + encoding="ascii", + capture_output=True, + ) + + start = self.frame_times[0] + end = self.frame_times[-1] + + if p.returncode == 0: + logger.debug("successfully saved preview") + self.requestor.send_data( + INSERT_PREVIEW, + { + Previews.id.name: f"{self.config.name}_{end}", + Previews.camera.name: self.config.name, + Previews.path.name: self.path, + Previews.start_time.name: start, + Previews.end_time.name: end, + Previews.duration.name: end - start, + }, + ) + else: + logger.error(f"Error saving preview for {self.config.name} :: {p.stderr}") + + # unlink files from cache + # don't delete last frame as it will be used as first frame in next segment + for t in self.frame_times[0:-1]: + Path(get_cache_image_name(self.config.name, t)).unlink(missing_ok=True) # type: ignore[arg-type] + + +class PreviewRecorder: + def __init__(self, config: CameraConfig) -> None: + self.config = config + self.camera_name: str = config.name or "" + self.start_time: float = 0 + self.last_output_time: float = 0 + self.offline = False + self.output_frames: list[float] = [] + + if config.detect.width is None or config.detect.height is None: + raise ValueError("Detect width and height must be set for previews.") + + self.detect_width: int = config.detect.width + self.detect_height: int = config.detect.height + + if self.detect_width > self.detect_height: + self.out_height = PREVIEW_HEIGHT + self.out_width = ( + int((self.detect_width / self.detect_height) * self.out_height) // 4 * 4 + ) + else: + self.out_width = PREVIEW_HEIGHT + self.out_height = ( + int((self.detect_height / self.detect_width) * self.out_width) // 4 * 4 + ) + + # create communication for finished previews + self.requestor = InterProcessRequestor() + + y, u1, u2, v1, v2 = get_yuv_crop( + self.config.frame_shape_yuv, + ( + 0, + 0, + self.config.frame_shape[1], + self.config.frame_shape[0], + ), + ) + self.channel_dims = { + "y": y, + "u1": u1, + "u2": u2, + "v1": v1, + "v2": v2, + } + + # end segment at end of hour (use UTC to avoid DST issues) + self.segment_end = ( + (datetime.datetime.now(datetime.UTC) + datetime.timedelta(hours=1)) + .replace(minute=0, second=0, microsecond=0) + .timestamp() + ) + + Path(PREVIEW_CACHE_DIR).mkdir(exist_ok=True) + Path(os.path.join(CLIPS_DIR, f"previews/{config.name}")).mkdir( + parents=True, exist_ok=True + ) + + # check for existing items in cache + start_ts = ( + datetime.datetime.now(datetime.UTC) + .replace(minute=0, second=0, microsecond=0) + .timestamp() + ) + + file_start = f"preview_{config.name}-" + start_file = f"{file_start}{start_ts}.webp" + + for file in sorted(os.listdir(os.path.join(CACHE_DIR, FOLDER_PREVIEW_FRAMES))): + if not file.startswith(file_start): + continue + + if file < start_file: + os.unlink(os.path.join(PREVIEW_CACHE_DIR, file)) + continue + + try: + file_time = file.split("-")[-1][: -(len(PREVIEW_FRAME_TYPE) + 1)] + + if not file_time: + continue + + ts = float(file_time) + except ValueError: + continue + + if self.start_time == 0: + self.start_time = ts + + self.last_output_time = ts + self.output_frames.append(ts) + + def reset_frame_cache(self, frame_time: float) -> None: + self.segment_end = ( + ( + datetime.datetime.fromtimestamp(frame_time, tz=datetime.UTC) + + datetime.timedelta(hours=1) + ) + .replace(minute=0, second=0, microsecond=0) + .timestamp() + ) + self.start_time = frame_time + self.last_output_time = frame_time + self.output_frames = [] + + def should_write_frame( + self, + current_tracked_objects: list[dict[str, Any]], + motion_boxes: list[list[int]], + frame_time: float, + ) -> bool: + """Decide if this frame should be added to PREVIEW.""" + if not self.config.record.enabled: + return False + + active_objs = get_active_objects( + frame_time, self.config, current_tracked_objects + ) + + preview_output_fps = 2 if any(o["label"] == "car" for o in active_objs) else 1 + + # limit output to 1 fps + if (frame_time - self.last_output_time) < 1 / preview_output_fps: + return False + + # send frame if a non-stationary object is in a zone + if len(active_objs) > 0: + self.last_output_time = frame_time + return True + + if len(motion_boxes) > 0: + self.last_output_time = frame_time + return True + + # ensure that at least 2 frames are written every minute + if frame_time - self.last_output_time > 30: + self.last_output_time = frame_time + return True + + return False + + def write_frame_to_cache(self, frame_time: float, frame: np.ndarray) -> None: + # resize yuv frame + small_frame: np.ndarray = np.zeros( + (self.out_height * 3 // 2, self.out_width), np.uint8 + ) + copy_yuv_to_position( + small_frame, + (0, 0), + (self.out_height, self.out_width), + frame, + self.channel_dims, + cv2.INTER_AREA, + ) + small_frame = cv2.cvtColor( + small_frame, + cv2.COLOR_YUV2BGR_I420, + ) + cache_path = get_cache_image_name(self.camera_name, frame_time) + + if not cv2.imwrite( + cache_path, + small_frame, + [ + int(cv2.IMWRITE_WEBP_QUALITY), + PREVIEW_QUALITY_WEBP[self.config.record.preview.quality], + ], + ): + logger.error("Failed to write preview frame to %s", cache_path) + + def write_data( + self, + current_tracked_objects: list[dict[str, Any]], + motion_boxes: list[list[int]], + frame_time: float, + frame: np.ndarray, + ) -> None: + self.offline = False + + # always write the first frame + if self.start_time == 0: + self.start_time = frame_time + self.output_frames.append(frame_time) + self.write_frame_to_cache(frame_time, frame) + return + + # check if PREVIEW clip should be generated and cached frames reset + if frame_time >= self.segment_end: + if len(self.output_frames) > 0: + # save last frame to ensure consistent duration + if self.config.record: + self.output_frames.append(frame_time) + self.write_frame_to_cache(frame_time, frame) + + # write the preview if any frames exist for this hour + FFMpegConverter( + self.config, + self.output_frames, + self.requestor, + ).start() + else: + logger.debug( + f"Not saving preview for {self.camera_name} because there are no saved frames." + ) + + self.reset_frame_cache(frame_time) + + # include first frame to ensure consistent duration + if self.config.record.enabled: + self.output_frames.append(frame_time) + self.write_frame_to_cache(frame_time, frame) + + return + elif self.should_write_frame(current_tracked_objects, motion_boxes, frame_time): + self.output_frames.append(frame_time) + self.write_frame_to_cache(frame_time, frame) + return + + def flag_offline(self, frame_time: float) -> None: + if not self.offline: + self.write_frame_to_cache( + frame_time, + get_blank_yuv_frame(self.detect_width, self.detect_height), + ) + self.offline = True + + # check if PREVIEW clip should be generated and cached frames reset + if frame_time >= self.segment_end: + if len(self.output_frames) == 0: + # camera has been offline for entire hour + # we have no preview to create + self.reset_frame_cache(frame_time) + return + + old_frame_path = get_cache_image_name( + self.camera_name, self.output_frames[-1] + ) + new_frame_path = get_cache_image_name(self.camera_name, frame_time) + shutil.copy(old_frame_path, new_frame_path) + + # save last frame to ensure consistent duration + self.output_frames.append(frame_time) + FFMpegConverter( + self.config, + self.output_frames, + self.requestor, + ).start() + + self.reset_frame_cache(frame_time) + + def stop(self) -> None: + self.requestor.stop() + + +def get_active_objects( + frame_time: float, camera_config: CameraConfig, all_objects: list[dict[str, Any]] +) -> list[dict[str, Any]]: + """get active objects for detection.""" + return [ + o + for o in all_objects + if o["motionless_count"] < camera_config.detect.stationary.threshold + and o["position_changes"] > 0 + and o["frame_time"] == frame_time + and not o["false_positive"] + ] diff --git a/frigate/output/ws_auth.py b/frigate/output/ws_auth.py new file mode 100644 index 0000000..33ec4e4 --- /dev/null +++ b/frigate/output/ws_auth.py @@ -0,0 +1,43 @@ +"""Authorization helpers for JSMPEG websocket clients.""" + +from typing import Any + +from frigate.config import FrigateConfig +from frigate.models import User + + +def _get_valid_ws_roles(ws: Any, config: FrigateConfig) -> list[str]: + role_header = ws.environ.get("HTTP_REMOTE_ROLE", "") + roles = [ + role.strip() + for role in role_header.split(config.proxy.separator) + if role.strip() + ] + return [role for role in roles if role in config.auth.roles] + + +def ws_has_camera_access(ws: Any, camera_name: str, config: FrigateConfig) -> bool: + """Return True when a websocket client is authorized for the camera path.""" + roles = _get_valid_ws_roles(ws, config) + + if not roles: + return False + + roles_dict = config.auth.roles + + # Birdseye is a composite stream, so only users with unrestricted access + # should receive it. + if camera_name == "birdseye": + return any(role == "admin" or not roles_dict.get(role) for role in roles) + + all_camera_names = set(config.cameras.keys()) + + for role in roles: + if role == "admin" or not roles_dict.get(role): + return True + + allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names) + if camera_name in allowed_cameras: + return True + + return False diff --git a/frigate/plus.py b/frigate/plus.py new file mode 100644 index 0000000..d528aa1 --- /dev/null +++ b/frigate/plus.py @@ -0,0 +1,244 @@ +import datetime +import json +import logging +import os +import re +from pathlib import Path +from typing import Any + +import cv2 +import requests +from numpy import ndarray +from requests.models import Response + +from frigate.const import PLUS_API_HOST, PLUS_ENV_VAR + +logger = logging.getLogger(__name__) + + +def get_jpg_bytes(image: ndarray, max_dim: int, quality: int) -> bytes: + if image.shape[1] >= image.shape[0]: + width = min(max_dim, image.shape[1]) + height = int(width * image.shape[0] / image.shape[1]) + else: + height = min(max_dim, image.shape[0]) + width = int(height * image.shape[1] / image.shape[0]) + + original = cv2.resize(image, dsize=(width, height), interpolation=cv2.INTER_AREA) + + ret, jpg = cv2.imencode(".jpg", original, [int(cv2.IMWRITE_JPEG_QUALITY), quality]) + jpg_bytes = jpg.tobytes() + return jpg_bytes if isinstance(jpg_bytes, bytes) else b"" + + +class PlusApi: + def __init__(self) -> None: + self.host = PLUS_API_HOST + self.key = None + if PLUS_ENV_VAR in os.environ: + self.key = os.environ.get(PLUS_ENV_VAR) + elif ( + os.path.isdir("/run/secrets") + and os.access("/run/secrets", os.R_OK) + and PLUS_ENV_VAR in os.listdir("/run/secrets") + ): + self.key = ( + Path(os.path.join("/run/secrets", PLUS_ENV_VAR)).read_text().strip() + ) + # check for the add-on options file + elif os.path.isfile("/data/options.json"): + with open("/data/options.json") as f: + raw_options = f.read() + options = json.loads(raw_options) + self.key = options.get("plus_api_key") + + if self.key is not None and not re.match( + r"[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}:[a-z0-9]{40}", + self.key, + ): + logger.error("Plus API Key is not formatted correctly.") + self.key = None + + self._is_active: bool = self.key is not None + self._token_data: dict = {} + + def _refresh_token_if_needed(self) -> None: + if ( + self._token_data.get("expires") is None + or self._token_data["expires"] - datetime.datetime.now().timestamp() < 60 + ): + if self.key is None: + raise Exception( + "Plus API key not set. See https://docs.frigate.video/integrations/plus#set-your-api-key" + ) + parts = self.key.split(":") + r = requests.get(f"{self.host}/v1/auth/token", auth=(parts[0], parts[1])) + if not r.ok: + raise Exception(f"Unable to refresh API token: {r.text}") + self._token_data = r.json() + + def _get_authorization_header(self) -> dict: + self._refresh_token_if_needed() + return {"authorization": f"Bearer {self._token_data.get('accessToken')}"} + + def _get(self, path: str) -> Response: + return requests.get( + f"{self.host}/v1/{path}", headers=self._get_authorization_header() + ) + + def _post(self, path: str, data: dict) -> Response: + return requests.post( + f"{self.host}/v1/{path}", + headers=self._get_authorization_header(), + json=data, + ) + + def _put(self, path: str, data: dict) -> Response: + return requests.put( + f"{self.host}/v1/{path}", + headers=self._get_authorization_header(), + json=data, + ) + + def is_active(self) -> bool: + return self._is_active + + def upload_image(self, image: ndarray, camera: str) -> str: + r = self._get("image/signed_urls") + if not r.ok: + raise Exception("Unable to get signed urls") + presigned_urls = r.json() + + # resize and submit original + files = {"file": get_jpg_bytes(image, 1920, 85)} + data = presigned_urls["original"]["fields"] + data["content-type"] = "image/jpeg" + r = requests.post(presigned_urls["original"]["url"], files=files, data=data) + if not r.ok: + logger.error(f"Failed to upload original: {r.status_code} {r.text}") + raise Exception(r.text) + + # resize and submit thumbnail + files = {"file": get_jpg_bytes(image, 200, 70)} + data = presigned_urls["thumbnail"]["fields"] + data["content-type"] = "image/jpeg" + r = requests.post(presigned_urls["thumbnail"]["url"], files=files, data=data) + if not r.ok: + logger.error(f"Failed to upload thumbnail: {r.status_code} {r.text}") + raise Exception(r.text) + + # create image + r = self._post( + "image/create", {"id": presigned_urls["imageId"], "camera": camera} + ) + if not r.ok: + raise Exception(r.text) + + # return image id + return str(presigned_urls.get("imageId")) + + def add_false_positive( + self, + plus_id: str, + region: list[float], + bbox: list[float], + score: float, + label: str, + model_hash: str, + model_type: str, + detector_type: str, + ) -> None: + r = self._put( + f"image/{plus_id}/false_positive", + { + "label": label, + "x": bbox[0], + "y": bbox[1], + "w": bbox[2], + "h": bbox[3], + "regionX": region[0], + "regionY": region[1], + "regionW": region[2], + "regionH": region[3], + "score": score, + "model_hash": model_hash, + "model_type": model_type, + "detector_type": detector_type, + }, + ) + + if not r.ok: + try: + error_response = r.json() + errors = error_response.get("errors", []) + for error in errors: + if ( + error.get("param") == "label" + and error.get("type") == "invalid_enum_value" + ): + raise ValueError(f"Unsupported label value provided: {label}") + except ValueError as e: + raise e + raise Exception(r.text) + + def add_annotation( + self, + plus_id: str, + bbox: list[float], + label: str, + difficult: bool = False, + ) -> None: + r = self._put( + f"image/{plus_id}/annotation", + { + "label": label, + "x": bbox[0], + "y": bbox[1], + "w": bbox[2], + "h": bbox[3], + "difficult": difficult, + }, + ) + + if not r.ok: + try: + error_response = r.json() + errors = error_response.get("errors", []) + for error in errors: + if ( + error.get("param") == "label" + and error.get("type") == "invalid_enum_value" + ): + raise ValueError(f"Unsupported label value provided: {label}") + except ValueError as e: + raise e + raise Exception(r.text) + + def get_model_download_url( + self, + model_id: str, + ) -> str: + r = self._get(f"model/{model_id}/signed_url") + + if not r.ok: + raise Exception(r.text) + + presigned_url = r.json() + + return str(presigned_url.get("url")) + + def get_model_info(self, model_id: str) -> Any: + r = self._get(f"model/{model_id}") + + if not r.ok: + raise Exception(r.text) + + return r.json() + + def get_models(self) -> Any: + r = self._get("model/list") + + if not r.ok: + raise Exception(r.text) + + return r.json() diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py new file mode 100644 index 0000000..9a114c9 --- /dev/null +++ b/frigate/ptz/autotrack.py @@ -0,0 +1,1538 @@ +"""Automatically pan, tilt, and zoom on detected objects via onvif.""" + +import asyncio +import copy +import logging +import threading +import time +from collections import deque +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import cv2 +import numpy as np +from norfair.camera_motion import ( + HomographyTransformationGetter, + MotionEstimator, + TranslationTransformationGetter, +) + +from frigate.camera import PTZMetrics +from frigate.comms.dispatcher import Dispatcher +from frigate.config import CameraConfig, FrigateConfig, ZoomingModeEnum +from frigate.const import ( + AUTOTRACKING_MAX_AREA_RATIO, + AUTOTRACKING_MAX_MOVE_METRICS, + AUTOTRACKING_MOTION_MAX_POINTS, + AUTOTRACKING_MOTION_MIN_DISTANCE, + AUTOTRACKING_ZOOM_EDGE_THRESHOLD, + AUTOTRACKING_ZOOM_IN_HYSTERESIS, + AUTOTRACKING_ZOOM_OUT_HYSTERESIS, +) +from frigate.ptz.onvif import OnvifController +from frigate.track.tracked_object import TrackedObject +from frigate.util.builtin import update_yaml_file_bulk +from frigate.util.config import find_config_file +from frigate.util.image import SharedMemoryFrameManager, intersection_over_union + +logger = logging.getLogger(__name__) + + +def ptz_moving_at_frame_time(frame_time, ptz_start_time, ptz_stop_time): + # Determine if the PTZ was in motion at the set frame time + # for non ptz/autotracking cameras, this will always return False + # ptz_start_time is initialized to 0 on startup and only changes + # when autotracking movements are made + return (ptz_start_time != 0.0 and frame_time > ptz_start_time) and ( + ptz_stop_time == 0.0 or (ptz_start_time <= frame_time <= ptz_stop_time) + ) + + +def transform_is_finite(coord_transformations) -> bool: + """Return True if a norfair coordinate transform contains only finite values. + + A near-singular homography (common when the motion estimator can't find + enough stable features during zoom on a low-texture scene) can produce + inf/nan matrix entries. norfair accumulates the homography across frames, so + a single bad transform poisons every subsequent one and propagates nan into + the tracker's distance function, crashing the camera process. + """ + for attr in ("homography_matrix", "inverse_homography_matrix", "movement_vector"): + value = getattr(coord_transformations, attr, None) + if value is not None and not np.all(np.isfinite(value)): + return False + return True + + +class PtzMotionEstimator: + def __init__(self, config: CameraConfig, ptz_metrics: PTZMetrics) -> None: + self.frame_manager = SharedMemoryFrameManager() + self.norfair_motion_estimator = None + self.camera_config = config + self.coord_transformations = None + self.ptz_metrics = ptz_metrics + self.ptz_metrics.reset.set() + logger.debug(f"{config.name}: Motion estimator init") + + def motion_estimator( + self, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], + frame_name: str, + frame_time: float, + camera: str | None, + ): + # If we've just started up or returned to our preset, reset motion estimator for new tracking session + if self.ptz_metrics.reset.is_set(): + self.ptz_metrics.reset.clear() + + # homography is nice (zooming) but slow, translation is pan/tilt only but fast. + if ( + self.camera_config.onvif.autotracking.zooming + != ZoomingModeEnum.disabled + ): + logger.debug(f"{camera}: Motion estimator reset - homography") + transformation_type = HomographyTransformationGetter() + else: + logger.debug(f"{camera}: Motion estimator reset - translation") + transformation_type = TranslationTransformationGetter() + + self.norfair_motion_estimator = MotionEstimator( + transformations_getter=transformation_type, + min_distance=AUTOTRACKING_MOTION_MIN_DISTANCE, + max_points=AUTOTRACKING_MOTION_MAX_POINTS, + ) + + self.coord_transformations = None + + if ptz_moving_at_frame_time( + frame_time, + self.ptz_metrics.start_time.value, + self.ptz_metrics.stop_time.value, + ): + logger.debug( + f"{camera}: Motion estimator running - frame time: {frame_time}" + ) + + yuv_frame = self.frame_manager.get( + frame_name, self.camera_config.frame_shape_yuv + ) + + if yuv_frame is None: + self.coord_transformations = None + return None + + frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2GRAY_I420) + + # mask out detections for better motion estimation + mask = np.ones(frame.shape[:2], frame.dtype) + + detection_boxes = [x[2] for x in detections] + for detection in detection_boxes: + x1, y1, x2, y2 = detection + mask[y1:y2, x1:x2] = 0 + + # merge camera config motion mask with detections. Norfair function needs 0,1 mask + mask = np.bitwise_and(mask, self.camera_config.motion.rasterized_mask).clip( + max=1 + ) + + # Norfair estimator function needs color so it can convert it right back to gray + frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGRA) + + try: + self.coord_transformations = self.norfair_motion_estimator.update( + frame, mask + ) + except Exception: + # sometimes opencv can't find enough features in the image to find homography, so catch this error + # https://github.com/tryolabs/norfair/pull/278 + logger.warning( + f"Autotracker: motion estimator couldn't get transformations for {camera} at frame time {frame_time}" + ) + self.coord_transformations = None + + # A degenerate homography can yield non-finite transform values that + # norfair would accumulate and feed to the tracker as nan estimates. + # Drop the bad transform and request a reset so the estimator rebuilds + # a fresh reference frame instead of poisoning every following frame. + if self.coord_transformations is not None and not transform_is_finite( + self.coord_transformations + ): + logger.warning( + f"Autotracker: motion estimator produced a non-finite transform for {camera} at frame time {frame_time}, resetting" + ) + self.coord_transformations = None + self.ptz_metrics.reset.set() + + try: + logger.debug( + f"{camera}: Motion estimator transformation: {self.coord_transformations.rel_to_abs([[0, 0]])}" + ) + except Exception: + pass + + self.frame_manager.close(frame_name) + + return self.coord_transformations + + +class PtzAutoTrackerThread(threading.Thread): + def __init__( + self, + config: FrigateConfig, + onvif: OnvifController, + ptz_metrics: dict[str, PTZMetrics], + dispatcher: Dispatcher, + stop_event: MpEvent, + ) -> None: + super().__init__(name="ptz_autotracker") + self.ptz_autotracker = PtzAutoTracker( + config, onvif, ptz_metrics, dispatcher, stop_event + ) + self.stop_event = stop_event + self.config = config + + def run(self): + while not self.stop_event.wait(1): + for camera, camera_config in self.config.cameras.items(): + if not camera_config.enabled: + continue + + if camera_config.onvif.autotracking.enabled: + future = asyncio.run_coroutine_threadsafe( + self.ptz_autotracker.camera_maintenance(camera), + self.ptz_autotracker.onvif.loop, + ) + # Wait for the coroutine to complete + future.result() + else: + # disabled dynamically by mqtt + if self.ptz_autotracker.tracked_object.get(camera): + self.ptz_autotracker.tracked_object[camera] = None + self.ptz_autotracker.tracked_object_history[camera].clear() + + logger.info("Exiting autotracker...") + + +class PtzAutoTracker: + def __init__( + self, + config: FrigateConfig, + onvif: OnvifController, + ptz_metrics: PTZMetrics, + dispatcher: Dispatcher, + stop_event: MpEvent, + ) -> None: + self.config = config + self.onvif = onvif + self.ptz_metrics = ptz_metrics + self.dispatcher = dispatcher + self.stop_event = stop_event + self.tracked_object: dict[str, object] = {} + self.tracked_object_history: dict[str, object] = {} + self.tracked_object_metrics: dict[str, object] = {} + self.object_types: dict[str, object] = {} + self.required_zones: dict[str, object] = {} + self.move_queues: dict[str, object] = {} + self.move_queue_locks: dict[str, object] = {} + self.move_threads: dict[str, object] = {} + self.autotracker_init: dict[str, object] = {} + self.move_metrics: dict[str, object] = {} + self.calibrating: dict[str, object] = {} + self.intercept: dict[str, object] = {} + self.move_coefficients: dict[str, object] = {} + self.zoom_time: dict[str, float] = {} + self.zoom_factor: dict[str, object] = {} + + # if cam is set to autotrack, onvif should be set up + for camera, camera_config in self.config.cameras.items(): + if not camera_config.enabled: + continue + + self.autotracker_init[camera] = False + if ( + camera_config.onvif.autotracking.enabled + and camera_config.onvif.autotracking.enabled_in_config + ): + future = asyncio.run_coroutine_threadsafe( + self._autotracker_setup(camera_config, camera), self.onvif.loop + ) + # Wait for the coroutine to complete + future.result() + + async def _autotracker_setup(self, camera_config: CameraConfig, camera: str): + logger.debug(f"{camera}: Autotracker init") + + self.object_types[camera] = camera_config.onvif.autotracking.track + self.required_zones[camera] = camera_config.onvif.autotracking.required_zones + self.zoom_factor[camera] = camera_config.onvif.autotracking.zoom_factor + + self.tracked_object[camera] = None + self.tracked_object_history[camera] = deque( + maxlen=round(camera_config.detect.fps * 1.5) + ) + self.tracked_object_metrics[camera] = { + "max_target_box": AUTOTRACKING_MAX_AREA_RATIO + ** (1 / self.zoom_factor[camera]) + } + + self.calibrating[camera] = False + self.move_metrics[camera] = [] + self.intercept[camera] = None + self.move_coefficients[camera] = [] + + self.move_queues[camera] = asyncio.Queue() + self.move_queue_locks[camera] = asyncio.Lock() + + # handle onvif constructor failing due to no connection + if camera not in self.onvif.cams: + logger.warning( + f"Disabling autotracking for {camera}: onvif connection failed" + ) + camera_config.onvif.autotracking.enabled = False + self.ptz_metrics[camera].autotracker_enabled.value = False + return + + if not self.onvif.cams[camera]["init"]: + if not await self.onvif._init_onvif(camera): + logger.warning( + f"Disabling autotracking for {camera}: Unable to initialize onvif" + ) + camera_config.onvif.autotracking.enabled = False + self.ptz_metrics[camera].autotracker_enabled.value = False + return + + if "pt-r-fov" not in self.onvif.cams[camera]["features"]: + logger.warning( + f"Disabling autotracking for {camera}: FOV relative movement not supported" + ) + camera_config.onvif.autotracking.enabled = False + self.ptz_metrics[camera].autotracker_enabled.value = False + return + + move_status_supported = await self.onvif.get_service_capabilities(camera) + + if not ( + isinstance(move_status_supported, bool) and move_status_supported + ) and not ( + isinstance(move_status_supported, str) + and move_status_supported.lower() == "true" + ): + logger.warning( + f"Disabling autotracking for {camera}: ONVIF MoveStatus not supported" + ) + camera_config.onvif.autotracking.enabled = False + self.ptz_metrics[camera].autotracker_enabled.value = False + return + + if self.onvif.cams[camera]["init"]: + await self.onvif.get_camera_status(camera) + + # movement queue with asyncio on OnvifController loop + asyncio.run_coroutine_threadsafe( + self._process_move_queue(camera), self.onvif.loop + ) + + if camera_config.onvif.autotracking.movement_weights: + if len(camera_config.onvif.autotracking.movement_weights) == 6: + camera_config.onvif.autotracking.movement_weights = [ + float(val) + for val in camera_config.onvif.autotracking.movement_weights + ] + self.ptz_metrics[ + camera + ].min_zoom.value = ( + camera_config.onvif.autotracking.movement_weights[0] + ) + self.ptz_metrics[ + camera + ].max_zoom.value = ( + camera_config.onvif.autotracking.movement_weights[1] + ) + self.intercept[camera] = ( + camera_config.onvif.autotracking.movement_weights[2] + ) + self.move_coefficients[camera] = ( + camera_config.onvif.autotracking.movement_weights[3:5] + ) + self.zoom_time[camera] = ( + camera_config.onvif.autotracking.movement_weights[5] + ) + else: + camera_config.onvif.autotracking.enabled = False + self.ptz_metrics[camera].autotracker_enabled.value = False + logger.warning( + f"Autotracker recalibration is required for {camera}. Disabling autotracking." + ) + + if camera_config.onvif.autotracking.calibrate_on_startup: + await self._calibrate_camera(camera) + + self.ptz_metrics[camera].tracking_active.clear() + self.dispatcher.publish(f"{camera}/ptz_autotracker/active", "OFF", retain=False) + self.autotracker_init[camera] = True + + def _write_config(self, camera): + config_file = find_config_file() + + logger.debug( + f"{camera}: Writing new config with autotracker motion coefficients: {self.config.cameras[camera].onvif.autotracking.movement_weights}" + ) + + update_yaml_file_bulk( + config_file, + { + f"cameras.{camera}.onvif.autotracking.movement_weights": self.config.cameras[ + camera + ].onvif.autotracking.movement_weights + }, + ) + + async def _calibrate_camera(self, camera): + # move the camera from the preset in steps and measure the time it takes to move that amount + # this will allow us to predict movement times with a simple linear regression + # start with 0 so we can determine a baseline (to be used as the intercept in the regression calc) + # TODO: take zooming into account too + num_steps = 30 + step_sizes = np.linspace(0, 1, num_steps) + zoom_in_values = [] + zoom_out_values = [] + + self.calibrating[camera] = True + + logger.info(f"Camera calibration for {camera} in progress") + + # zoom levels test + self.zoom_time[camera] = 0 + + if ( + self.config.cameras[camera].onvif.autotracking.zooming + != ZoomingModeEnum.disabled + ): + logger.info(f"Calibration for {camera} in progress: 0% complete") + + for i in range(2): + # absolute move to 0 - fully zoomed out + await self.onvif._zoom_absolute( + camera, + self.onvif.cams[camera]["absolute_zoom_range"]["XRange"]["Min"], + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + zoom_out_values.append(self.ptz_metrics[camera].zoom_level.value) + + await self.onvif._zoom_absolute( + camera, + self.onvif.cams[camera]["absolute_zoom_range"]["XRange"]["Max"], + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + zoom_in_values.append(self.ptz_metrics[camera].zoom_level.value) + + if ( + self.config.cameras[camera].onvif.autotracking.zooming + == ZoomingModeEnum.relative + ): + # relative move to -0.01 + await self.onvif._move_relative( + camera, + 0, + 0, + -1e-2, + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + zoom_out_values.append(self.ptz_metrics[camera].zoom_level.value) + + zoom_start_time = time.time() + # relative move to 0.01 + await self.onvif._move_relative( + camera, + 0, + 0, + 1e-2, + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + zoom_stop_time = time.time() + + full_relative_start_time = time.time() + + await self.onvif._move_relative( + camera, + -1, + -1, + -1e-2, + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + full_relative_stop_time = time.time() + + await self.onvif._move_relative( + camera, + 1, + 1, + 1e-2, + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + self.zoom_time[camera] = ( + full_relative_stop_time - full_relative_start_time + ) - (zoom_stop_time - zoom_start_time) + + zoom_in_values.append(self.ptz_metrics[camera].zoom_level.value) + + self.ptz_metrics[camera].max_zoom.value = max(zoom_in_values) + self.ptz_metrics[camera].min_zoom.value = min(zoom_out_values) + + logger.debug( + f"{camera}: Calibration values: max zoom: {self.ptz_metrics[camera].max_zoom.value}, min zoom: {self.ptz_metrics[camera].min_zoom.value}, zoom time: {self.zoom_time[camera]}" + ) + + else: + self.ptz_metrics[camera].max_zoom.value = 1 + self.ptz_metrics[camera].min_zoom.value = 0 + + await self.onvif._move_to_preset( + camera, + self.config.cameras[camera].onvif.autotracking.return_preset.lower(), + ) + self.ptz_metrics[camera].reset.set() + self.ptz_metrics[camera].motor_stopped.clear() + + # Wait until the camera finishes moving + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + for step in range(num_steps): + pan = step_sizes[step] + tilt = step_sizes[step] + + start_time = time.time() + await self.onvif._move_relative(camera, pan, tilt, 0, 1) + + # Wait until the camera finishes moving + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + stop_time = time.time() + + self.move_metrics[camera].append( + { + "pan": pan, + "tilt": tilt, + "start_timestamp": start_time, + "end_timestamp": stop_time, + } + ) + + await self.onvif._move_to_preset( + camera, + self.config.cameras[camera].onvif.autotracking.return_preset.lower(), + ) + self.ptz_metrics[camera].reset.set() + self.ptz_metrics[camera].motor_stopped.clear() + + # Wait until the camera finishes moving + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + logger.info( + f"Calibration for {camera} in progress: {round((step / num_steps) * 100)}% complete" + ) + + self.calibrating[camera] = False + + logger.info(f"Calibration for {camera} complete") + + # calculate and save new intercept and coefficients + self._calculate_move_coefficients(camera, True) + + def _calculate_move_coefficients(self, camera, calibration=False): + # calculate new coefficients when we have 50 more new values. Save up to 500 + if calibration or ( + len(self.move_metrics[camera]) % 50 == 0 + and len(self.move_metrics[camera]) != 0 + and len(self.move_metrics[camera]) <= AUTOTRACKING_MAX_MOVE_METRICS + ): + X = np.array( + [abs(d["pan"]) + abs(d["tilt"]) for d in self.move_metrics[camera]] + ) + y = np.array( + [ + d["end_timestamp"] - d["start_timestamp"] + for d in self.move_metrics[camera] + ] + ) + + # simple linear regression with intercept + X_with_intercept = np.column_stack((np.ones(X.shape[0]), X)) + coefficients = np.linalg.lstsq(X_with_intercept, y, rcond=None)[0] + + intercept, slope = coefficients + + # Define reasonable bounds for PTZ movement times + MIN_MOVEMENT_TIME = 0.1 # Minimum time for any movement (100ms) + MAX_MOVEMENT_TIME = 10.0 # Maximum time for any movement + MAX_SLOPE = 2.0 # Maximum seconds per unit of movement + + coefficients_valid = ( + MIN_MOVEMENT_TIME <= intercept <= MAX_MOVEMENT_TIME + and 0 < slope <= MAX_SLOPE + ) + + if not coefficients_valid: + logger.warning( + f"{camera}: Autotracking calibration failed. See the Frigate documentation." + ) + return False + + # If coefficients are valid, proceed with updates + self.move_coefficients[camera] = coefficients + + # only assign a new intercept if we're calibrating + if calibration: + self.intercept[camera] = y[0] + + # write the min zoom, max zoom, intercept, and coefficients + # back to the config file as a comma separated string + self.config.cameras[camera].onvif.autotracking.movement_weights = ", ".join( + str(v) + for v in [ + self.ptz_metrics[camera].min_zoom.value, + self.ptz_metrics[camera].max_zoom.value, + self.intercept[camera], + *self.move_coefficients[camera], + self.zoom_time[camera], + ] + ) + + logger.debug( + f"{camera}: New regression parameters - intercept: {self.intercept[camera]}, coefficients: {self.move_coefficients[camera]}" + ) + + self._write_config(camera) + + def _predict_movement_time(self, camera, pan, tilt): + combined_movement = abs(pan) + abs(tilt) + input_data = np.array([self.intercept[camera], combined_movement]) + + return np.dot(self.move_coefficients[camera], input_data) + + def _predict_area_after_time(self, camera, time): + return np.dot( + self.tracked_object_metrics[camera]["area_coefficients"], + [self.tracked_object_history[camera][-1]["frame_time"] + time], + ) + + def _calculate_tracked_object_metrics(self, camera, obj): + def remove_outliers(data): + areas = [item["area"] for item in data] + + Q1 = np.percentile(areas, 25) + Q3 = np.percentile(areas, 75) + IQR = Q3 - Q1 + lower_bound = Q1 - 1.5 * IQR + upper_bound = Q3 + 1.5 * IQR + + filtered_data = [ + item for item in data if lower_bound <= item["area"] <= upper_bound + ] + + # Find and log the removed values + removed_values = [item for item in data if item not in filtered_data] + logger.debug(f"{camera}: Removed area outliers: {removed_values}") + + return filtered_data + + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + + # Extract areas and calculate weighted average + # grab the largest dimension of the bounding box and create a square from that + # Filter out the initial frame and use a recent time window + current_time = obj.obj_data["frame_time"] + time_window = 1.5 # seconds + history = [ + entry + for entry in self.tracked_object_history[camera] + if not entry.get("is_initial_frame", False) + and current_time - entry["frame_time"] <= time_window + ] + if not history: # Fallback to latest if no recent entries + history = [self.tracked_object_history[camera][-1]] + + areas = [ + { + "frame_time": entry["frame_time"], + "box": entry["box"], + "area": max( + entry["box"][2] - entry["box"][0], entry["box"][3] - entry["box"][1] + ) + ** 2, + } + for entry in history + ] + + filtered_areas = remove_outliers(areas) if len(areas) > 3 else areas + + # Filter entries that are not touching the frame edge + filtered_areas_not_touching_edge = [ + entry + for entry in filtered_areas + if self._touching_frame_edges(camera, entry["box"]) == 0 + ] + + # Calculate regression for area change predictions + if len(filtered_areas_not_touching_edge): + X = np.array( + [item["frame_time"] for item in filtered_areas_not_touching_edge] + ) + y = np.array([item["area"] for item in filtered_areas_not_touching_edge]) + + self.tracked_object_metrics[camera]["area_coefficients"] = np.linalg.lstsq( + X.reshape(-1, 1), y, rcond=None + )[0] + else: + self.tracked_object_metrics[camera]["area_coefficients"] = np.array([0]) + + weights = np.arange(1, len(filtered_areas) + 1) + weighted_area = np.average( + [item["area"] for item in filtered_areas], weights=weights + ) + + self.tracked_object_metrics[camera]["target_box"] = ( + weighted_area / (camera_width * camera_height) + ) ** self.zoom_factor[camera] + + if "original_target_box" not in self.tracked_object_metrics[camera]: + self.tracked_object_metrics[camera]["original_target_box"] = ( + self.tracked_object_metrics[camera]["target_box"] + ) + + ( + self.tracked_object_metrics[camera]["valid_velocity"], + self.tracked_object_metrics[camera]["velocity"], + ) = self._get_valid_velocity(camera, obj) + self.tracked_object_metrics[camera]["distance"] = self._get_distance_threshold( + camera, obj + ) + + centroid_distance = np.linalg.norm( + [ + obj.obj_data["centroid"][0] - camera_config.detect.width / 2, + obj.obj_data["centroid"][1] - camera_config.detect.height / 2, + ] + ) + + logger.debug(f"{camera}: Centroid distance: {centroid_distance}") + + self.tracked_object_metrics[camera]["below_distance_threshold"] = ( + centroid_distance < self.tracked_object_metrics[camera]["distance"] + ) + + async def _process_move_queue(self, camera): + move_queue = self.move_queues[camera] + + while not self.stop_event.is_set(): + try: + # Asynchronously wait for move data with a timeout + move_data = await asyncio.wait_for(move_queue.get(), timeout=0.1) + except TimeoutError: + continue + + async with self.move_queue_locks[camera]: + frame_time, pan, tilt, zoom = move_data + + # if we're receiving move requests during a PTZ move, ignore them + if ptz_moving_at_frame_time( + frame_time, + self.ptz_metrics[camera].start_time.value, + self.ptz_metrics[camera].stop_time.value, + ): + logger.debug( + f"{camera}: Move queue: PTZ moving, dequeueing move request - frame time: {frame_time}, final pan: {pan}, final tilt: {tilt}, final zoom: {zoom}" + ) + continue + + else: + if ( + self.config.cameras[camera].onvif.autotracking.zooming + == ZoomingModeEnum.relative + ): + await self.onvif._move_relative(camera, pan, tilt, zoom, 1) + else: + if pan != 0 or tilt != 0: + await self.onvif._move_relative(camera, pan, tilt, 0, 1) + + # Wait until the camera finishes moving + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + if ( + zoom > 0 + and self.ptz_metrics[camera].zoom_level.value != zoom + ): + await self.onvif._zoom_absolute(camera, zoom, 1) + + # Wait until the camera finishes moving + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + if self.config.cameras[camera].onvif.autotracking.movement_weights: + logger.debug( + f"{camera}: Predicted movement time: {self._predict_movement_time(camera, pan, tilt)}" + ) + logger.debug( + f"{camera}: Actual movement time: {self.ptz_metrics[camera].stop_time.value - self.ptz_metrics[camera].start_time.value}" + ) + + # save metrics for better estimate calculations + if ( + self.intercept[camera] is not None + and len(self.move_metrics[camera]) + < AUTOTRACKING_MAX_MOVE_METRICS + and (pan != 0 or tilt != 0) + and self.config.cameras[ + camera + ].onvif.autotracking.calibrate_on_startup + ): + logger.debug(f"{camera}: Adding new values to move metrics") + self.move_metrics[camera].append( + { + "pan": pan, + "tilt": tilt, + "start_timestamp": self.ptz_metrics[ + camera + ].start_time.value, + "end_timestamp": self.ptz_metrics[ + camera + ].stop_time.value, + } + ) + + # calculate new coefficients if we have enough data + self._calculate_move_coefficients(camera) + + # Clean up the queue on exit + while not move_queue.empty(): + await move_queue.get() + + def _enqueue_move(self, camera, frame_time, pan, tilt, zoom): + def split_value(value, suppress_diff=True): + clipped = np.clip(value, -1, 1) + + # don't make small movements + if -0.05 < clipped < 0.05 and suppress_diff: + diff = 0.0 + else: + diff = value - clipped + + return clipped, diff + + if ( + frame_time > self.ptz_metrics[camera].start_time.value + and frame_time > self.ptz_metrics[camera].stop_time.value + and not self.move_queue_locks[camera].locked() + ): + # we can split up any large moves caused by velocity estimated movements if necessary + # get an excess amount and assign it instead of 0 below + while pan != 0 or tilt != 0 or zoom != 0: + pan, _ = split_value(pan) + tilt, _ = split_value(tilt) + zoom, _ = split_value(zoom, False) + + logger.debug( + f"{camera}: Enqueue movement for frame time: {frame_time} pan: {pan}, tilt: {tilt}, zoom: {zoom}" + ) + move_data = (frame_time, pan, tilt, zoom) + self.onvif.loop.call_soon_threadsafe( + self.move_queues[camera].put_nowait, move_data + ) + + # reset values to not split up large movements + pan = 0 + tilt = 0 + zoom = 0 + + def _touching_frame_edges(self, camera, box): + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + bb_left, bb_top, bb_right, bb_bottom = box + + edge_threshold = AUTOTRACKING_ZOOM_EDGE_THRESHOLD + + return int( + (bb_left < edge_threshold * camera_width) + + (bb_right > (1 - edge_threshold) * camera_width) + + (bb_top < edge_threshold * camera_height) + + (bb_bottom > (1 - edge_threshold) * camera_height) + ) + + def _get_valid_velocity(self, camera, obj): + # returns a tuple and euclidean distance if the estimated velocity is valid + # if invalid, returns [0, 0] and -1 + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + camera_fps = camera_config.detect.fps + + # estimate_velocity is a numpy array of bbox top,left and bottom,right velocities + velocities = obj.obj_data["estimate_velocity"] + logger.debug( + f"{camera}: Velocity (Norfair): {tuple(np.round(velocities).flatten().astype(int))}" + ) + + # if we are close enough to zero, return right away + if np.all(np.round(velocities) == 0): + return True, np.zeros((4,)) + + # Thresholds + x_mags_thresh = camera_width / camera_fps / 2 + y_mags_thresh = camera_height / camera_fps / 2 + dir_thresh = 0.93 + delta_thresh = 20 + var_thresh = 10 + + # Check magnitude + x_mags = np.abs(velocities[:, 0]) + y_mags = np.abs(velocities[:, 1]) + invalid_x_mags = np.any(x_mags > x_mags_thresh) + invalid_y_mags = np.any(y_mags > y_mags_thresh) + + # Check delta + delta = np.abs(velocities[0] - velocities[1]) + invalid_delta = np.any(delta > delta_thresh) + + # Check variance + stdev_list = np.std(velocities, axis=0) + high_variances = np.any(stdev_list > var_thresh) + + # Check direction difference + velocities = np.round(velocities) + invalid_dirs = False + if np.all(np.linalg.norm(velocities, axis=1)): + cosine_sim = np.dot(velocities[0], velocities[1]) / ( + np.linalg.norm(velocities[0]) * np.linalg.norm(velocities[1]) + ) + dir_thresh = 0.6 if np.all(delta < delta_thresh / 2) else dir_thresh + invalid_dirs = cosine_sim < dir_thresh + + # Combine + invalid = ( + invalid_x_mags + or invalid_y_mags + or invalid_dirs + or invalid_delta + or high_variances + ) + + if invalid: + logger.debug( + f"{camera}: Invalid velocity: {tuple(np.round(velocities, 2).flatten().astype(int))}: Invalid because: %s", + ", ".join( + [ + var_name + for var_name, is_invalid in [ + ("invalid_x_mags", invalid_x_mags), + ("invalid_y_mags", invalid_y_mags), + ("invalid_dirs", invalid_dirs), + ("invalid_delta", invalid_delta), + ("high_variances", high_variances), + ] + if is_invalid + ] + ), + ) + # invalid velocity + return False, np.zeros((4,)) + else: + logger.debug(f"{camera}: Valid velocity ") + return True, velocities.flatten() + + def _get_distance_threshold(self, camera: str, obj: TrackedObject): + # Returns true if Euclidean distance from object to center of frame is + # less than 10% of the of the larger dimension (width or height) of the frame, + # multiplied by a scaling factor for object size. + # Distance is increased if object is not moving to prevent small ptz moves + # Adjusting this percentage slightly lower will effectively cause the camera to move + # more often to keep the object in the center. Raising the percentage will cause less + # movement and will be more flexible with objects not quite being centered. + # TODO: there's probably a better way to approach this + camera_config = self.config.cameras[camera] + + obj_width = obj.obj_data["box"][2] - obj.obj_data["box"][0] + obj_height = obj.obj_data["box"][3] - obj.obj_data["box"][1] + + max_obj = max(obj_width, obj_height) + max_frame = ( + camera_config.detect.width + if max_obj == obj_width + else camera_config.detect.height + ) + + # larger objects should lower the threshold, smaller objects should raise it + scaling_factor = 1 - np.log(max_obj / max_frame) + + percentage = ( + 0.08 + if camera_config.onvif.autotracking.movement_weights + and self.tracked_object_metrics[camera]["valid_velocity"] + else 0.03 + ) + distance_threshold = percentage * max_frame * scaling_factor + + logger.debug(f"{camera}: Distance threshold: {distance_threshold}") + + return distance_threshold + + def _should_zoom_in( + self, camera: str, obj: TrackedObject, box, predicted_time, debug_zooming=False + ): + # returns True if we should zoom in, False if we should zoom out, None to do nothing + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + camera_fps = camera_config.detect.fps + + average_velocity = self.tracked_object_metrics[camera]["velocity"] + + bb_left, bb_top, bb_right, bb_bottom = box + + # calculate a velocity threshold based on movement coefficients if available + if camera_config.onvif.autotracking.movement_weights: + predicted_movement_time = self._predict_movement_time(camera, 1, 1) + velocity_threshold_x = camera_width / predicted_movement_time / camera_fps + velocity_threshold_y = camera_height / predicted_movement_time / camera_fps + else: + # use a generic velocity threshold + velocity_threshold_x = camera_width * 0.02 + velocity_threshold_y = camera_height * 0.02 + + # return a count of the number of frame edges the bounding box is touching + touching_frame_edges = self._touching_frame_edges(camera, box) + + # make sure object is centered in the frame + below_distance_threshold = self.tracked_object_metrics[camera][ + "below_distance_threshold" + ] + + below_dimension_threshold = (bb_right - bb_left) <= camera_width * ( + self.zoom_factor[camera] + 0.1 + ) and (bb_bottom - bb_top) <= camera_height * (self.zoom_factor[camera] + 0.1) + + # ensure object is not moving quickly + below_velocity_threshold = np.all( + np.abs(average_velocity) + < np.tile([velocity_threshold_x, velocity_threshold_y], 2) + ) or np.all(average_velocity == 0) + + if not predicted_time: + calculated_target_box = self.tracked_object_metrics[camera]["target_box"] + else: + calculated_target_box = self.tracked_object_metrics[camera][ + "target_box" + ] + self._predict_area_after_time(camera, predicted_time) / ( + camera_width * camera_height + ) + + below_area_threshold = ( + calculated_target_box + < self.tracked_object_metrics[camera]["max_target_box"] + ) + + # introduce some hysteresis to prevent a yo-yo zooming effect + zoom_out_hysteresis = ( + calculated_target_box + > self.tracked_object_metrics[camera]["max_target_box"] + * AUTOTRACKING_ZOOM_OUT_HYSTERESIS + ) + zoom_in_hysteresis = ( + calculated_target_box + < self.tracked_object_metrics[camera]["max_target_box"] + * AUTOTRACKING_ZOOM_IN_HYSTERESIS + ) + + at_max_zoom = ( + self.ptz_metrics[camera].zoom_level.value + == self.ptz_metrics[camera].max_zoom.value + ) + at_min_zoom = ( + self.ptz_metrics[camera].zoom_level.value + == self.ptz_metrics[camera].min_zoom.value + ) + + # debug zooming + if debug_zooming: + logger.debug( + f"{camera}: Zoom test: touching edges: count: {touching_frame_edges} left: {bb_left < AUTOTRACKING_ZOOM_EDGE_THRESHOLD * camera_width}, right: {bb_right > (1 - AUTOTRACKING_ZOOM_EDGE_THRESHOLD) * camera_width}, top: {bb_top < AUTOTRACKING_ZOOM_EDGE_THRESHOLD * camera_height}, bottom: {bb_bottom > (1 - AUTOTRACKING_ZOOM_EDGE_THRESHOLD) * camera_height}" + ) + logger.debug( + f"{camera}: Zoom test: below distance threshold: {(below_distance_threshold)}" + ) + logger.debug( + f"{camera}: Zoom test: below area threshold: {(below_area_threshold)} target: {self.tracked_object_metrics[camera]['target_box']}, calculated: {calculated_target_box}, max: {self.tracked_object_metrics[camera]['max_target_box']}" + ) + logger.debug( + f"{camera}: Zoom test: below dimension threshold: {below_dimension_threshold} width: {bb_right - bb_left}, max width: {camera_width * (self.zoom_factor[camera] + 0.1)}, height: {bb_bottom - bb_top}, max height: {camera_height * (self.zoom_factor[camera] + 0.1)}" + ) + logger.debug( + f"{camera}: Zoom test: below velocity threshold: {below_velocity_threshold} velocity x: {abs(average_velocity[0])}, x threshold: {velocity_threshold_x}, velocity y: {abs(average_velocity[1])}, y threshold: {velocity_threshold_y}" + ) + logger.debug(f"{camera}: Zoom test: at max zoom: {at_max_zoom}") + logger.debug(f"{camera}: Zoom test: at min zoom: {at_min_zoom}") + logger.debug( + f"{camera}: Zoom test: zoom in hysteresis limit: {zoom_in_hysteresis} value: {AUTOTRACKING_ZOOM_IN_HYSTERESIS} original: {self.tracked_object_metrics[camera]['original_target_box']} max: {self.tracked_object_metrics[camera]['max_target_box']} target: {calculated_target_box if calculated_target_box else self.tracked_object_metrics[camera]['target_box']}" + ) + logger.debug( + f"{camera}: Zoom test: zoom out hysteresis limit: {zoom_out_hysteresis} value: {AUTOTRACKING_ZOOM_OUT_HYSTERESIS} original: {self.tracked_object_metrics[camera]['original_target_box']} max: {self.tracked_object_metrics[camera]['max_target_box']} target: {calculated_target_box if calculated_target_box else self.tracked_object_metrics[camera]['target_box']}" + ) + + # Zoom in conditions (and) + if ( + zoom_in_hysteresis + and touching_frame_edges == 0 + and below_velocity_threshold + and below_dimension_threshold + and below_area_threshold + and not at_max_zoom + ): + return True + + # Zoom out conditions (or) + if ( + ( + zoom_out_hysteresis + and not at_max_zoom + and (not below_area_threshold or not below_dimension_threshold) + ) + or (zoom_out_hysteresis and not below_area_threshold and at_max_zoom) + or ( + touching_frame_edges == 1 + and (below_distance_threshold or not below_dimension_threshold) + ) + or touching_frame_edges > 1 + or not below_velocity_threshold + ) and not at_min_zoom: + return False + + # Don't zoom at all + return None + + def _autotrack_move_ptz(self, camera: str, obj: TrackedObject): + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + camera_fps = camera_config.detect.fps + predicted_movement_time = 0 + zoom_distance = 0 + + average_velocity = np.zeros((4,)) + predicted_box = obj.obj_data["box"] + zoom_predicted_box = obj.obj_data["box"] + + centroid_x = obj.obj_data["centroid"][0] + centroid_y = obj.obj_data["centroid"][1] + + # Normalize coordinates. top right of the fov is (1,1), center is (0,0), bottom left is (-1, -1). + pan = ((centroid_x / camera_width) - 0.5) * 2 + tilt = (0.5 - (centroid_y / camera_height)) * 2 + + _, average_velocity = ( + self._get_valid_velocity(camera, obj) + if "velocity" not in self.tracked_object_metrics[camera] + else ( + self.tracked_object_metrics[camera]["valid_velocity"], + self.tracked_object_metrics[camera]["velocity"], + ) + ) + + if ( + camera_config.onvif.autotracking.movement_weights + ): # use estimates if we have available coefficients + predicted_movement_time = self._predict_movement_time(camera, pan, tilt) + + if np.any(average_velocity): + # this box could exceed the frame boundaries if velocity is high + # but we'll handle that in _enqueue_move() as two separate moves + current_box = np.array(obj.obj_data["box"]) + predicted_box = ( + current_box + + camera_fps * predicted_movement_time * average_velocity + ) + + predicted_box = np.round(predicted_box).astype(int) + + centroid_x = round((predicted_box[0] + predicted_box[2]) / 2) + centroid_y = round((predicted_box[1] + predicted_box[3]) / 2) + + # recalculate pan and tilt with new centroid + pan = ((centroid_x / camera_width) - 0.5) * 2 + tilt = (0.5 - (centroid_y / camera_height)) * 2 + + logger.debug(f"{camera}: Original box: {obj.obj_data['box']}") + logger.debug(f"{camera}: Predicted box: {tuple(predicted_box)}") + logger.debug( + f"{camera}: Velocity: {tuple(np.round(average_velocity).flatten().astype(int))}" + ) + + zoom = self._get_zoom_amount( + camera, obj, predicted_box, predicted_movement_time, debug_zoom=True + ) + + if ( + camera_config.onvif.autotracking.movement_weights + and camera_config.onvif.autotracking.zooming == ZoomingModeEnum.relative + and zoom != 0 + ): + zoom_predicted_movement_time = 0 + + if np.any(average_velocity): + # Calculate the intended change in zoom level + zoom_change = (1 - abs(zoom)) * (1 if zoom >= 0 else -1) + + # Calculate new zoom level and clamp to [0, 1] + new_zoom = max( + 0, min(1, self.ptz_metrics[camera].zoom_level.value + zoom_change) + ) + + # Calculate the actual zoom distance + zoom_distance = abs( + new_zoom - self.ptz_metrics[camera].zoom_level.value + ) + + zoom_predicted_movement_time = zoom_distance * self.zoom_time[camera] + + zoom_predicted_box = ( + predicted_box + + camera_fps * zoom_predicted_movement_time * average_velocity + ) + + zoom_predicted_box = np.round(zoom_predicted_box).astype(int) + + centroid_x = round((zoom_predicted_box[0] + zoom_predicted_box[2]) / 2) + centroid_y = round((zoom_predicted_box[1] + zoom_predicted_box[3]) / 2) + + # recalculate pan and tilt with new centroid + pan = ((centroid_x / camera_width) - 0.5) * 2 + tilt = (0.5 - (centroid_y / camera_height)) * 2 + + logger.debug( + f"{camera}: Zoom amount: {zoom}, zoom distance: {zoom_distance}, zoom predicted time: {zoom_predicted_movement_time}, zoom predicted box: {tuple(zoom_predicted_box)}" + ) + + self._enqueue_move(camera, obj.obj_data["frame_time"], pan, tilt, zoom) + + def _autotrack_move_zoom_only(self, camera, obj): + camera_config = self.config.cameras[camera] + + if camera_config.onvif.autotracking.zooming != ZoomingModeEnum.disabled: + zoom = self._get_zoom_amount(camera, obj, obj.obj_data["box"], 0) + + if zoom != 0: + self._enqueue_move(camera, obj.obj_data["frame_time"], 0, 0, zoom) + + def _get_zoom_amount( + self, + camera: str, + obj: TrackedObject, + predicted_box, + predicted_movement_time, + debug_zoom=True, + ): + camera_config = self.config.cameras[camera] + + # frame width and height + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + + zoom = 0 + result = None + current_zoom_level = self.ptz_metrics[camera].zoom_level.value + target_box = max( + obj.obj_data["box"][2] - obj.obj_data["box"][0], + obj.obj_data["box"][3] - obj.obj_data["box"][1], + ) ** 2 / (camera_width * camera_height) + + # absolute zooming separately from pan/tilt + if camera_config.onvif.autotracking.zooming == ZoomingModeEnum.absolute: + # don't zoom on initial move + if "target_box" not in self.tracked_object_metrics[camera]: + zoom = current_zoom_level + else: + if ( + result := self._should_zoom_in( + camera, + obj, + obj.obj_data["box"], + predicted_movement_time, + debug_zoom, + ) + ) is not None: + # divide zoom in 10 increments and always zoom out more than in + level = ( + self.ptz_metrics[camera].max_zoom.value + - self.ptz_metrics[camera].min_zoom.value + ) / 20 + if result: + zoom = min(1.0, current_zoom_level + level) + else: + zoom = max(0.0, current_zoom_level - 2 * level) + + # relative zooming concurrently with pan/tilt + if camera_config.onvif.autotracking.zooming == ZoomingModeEnum.relative: + # this is our initial zoom in on a new object + if "target_box" not in self.tracked_object_metrics[camera]: + zoom = target_box ** self.zoom_factor[camera] + if zoom > self.tracked_object_metrics[camera]["max_target_box"]: + zoom = -(1 - zoom) + logger.debug( + f"{camera}: target box: {target_box}, max: {self.tracked_object_metrics[camera]['max_target_box']}, calc zoom: {zoom}" + ) + else: + if ( + result := self._should_zoom_in( + camera, + obj, + predicted_box + if camera_config.onvif.autotracking.movement_weights + else obj.obj_data["box"], + predicted_movement_time, + debug_zoom, + ) + ) is not None: + if predicted_movement_time: + calculated_target_box = self.tracked_object_metrics[camera][ + "target_box" + ] + self._predict_area_after_time( + camera, predicted_movement_time + ) / (camera_width * camera_height) + logger.debug( + f"{camera}: Zooming prediction: predicted movement time: {predicted_movement_time}, original box: {self.tracked_object_metrics[camera]['target_box']}, calculated box: {calculated_target_box}" + ) + else: + calculated_target_box = self.tracked_object_metrics[camera][ + "target_box" + ] + # zoom value + ratio = ( + self.tracked_object_metrics[camera]["max_target_box"] + / calculated_target_box + ) + zoom = (ratio - 1) / (ratio + 1) + logger.debug( + f"{camera}: limit: {self.tracked_object_metrics[camera]['max_target_box']}, ratio: {ratio} zoom calculation: {zoom}" + ) + if not result: + # zoom out with special condition if zooming out because of velocity, edges, etc. + zoom = -(1 - zoom) if zoom > 0 else -(zoom * 2 + 1) + if result: + # zoom in + zoom = 1 - zoom if zoom > 0 else (zoom * 2 + 1) + + logger.debug(f"{camera}: Zooming: {result} Zoom amount: {zoom}") + + return zoom + + def is_autotracking(self, camera: str): + return self.tracked_object[camera] is not None + + def autotracked_object_region(self, camera: str): + return self.tracked_object[camera]["region"] + + def autotrack_object(self, camera: str, obj: TrackedObject): + if camera not in self.config.cameras: + return + camera_config = self.config.cameras[camera] + + if camera_config.onvif.autotracking.enabled: + if not self.autotracker_init[camera]: + future = asyncio.run_coroutine_threadsafe( + self._autotracker_setup(camera_config, camera), self.onvif.loop + ) + # Wait for the coroutine to complete + future.result() + + if self.calibrating[camera]: + logger.debug(f"{camera}: Calibrating camera") + return + + # this is a brand new object that's on our camera, has our label, entered the zone, + # is not a false positive, and is active + if ( + # new object + self.tracked_object[camera] is None + and obj.camera_config.name == camera + and obj.obj_data["label"] in self.object_types[camera] + and set(obj.entered_zones) & set(self.required_zones[camera]) + and not obj.previous["false_positive"] + and not obj.false_positive + and not self.tracked_object_history[camera] + and obj.active + ): + logger.debug( + f"{camera}: New object: {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" + ) + self.ptz_metrics[camera].tracking_active.set() + self.dispatcher.publish( + f"{camera}/ptz_autotracker/active", "ON", retain=False + ) + self.tracked_object[camera] = obj + + self.tracked_object_history[camera].append(copy.deepcopy(obj.obj_data)) + self._autotrack_move_ptz(camera, obj) + + return + + if ( + # already tracking an object + self.tracked_object[camera] is not None + and self.tracked_object_history[camera] + and obj.obj_data["id"] == self.tracked_object[camera].obj_data["id"] + and obj.obj_data["frame_time"] + != self.tracked_object_history[camera][-1]["frame_time"] + ): + self.tracked_object_history[camera].append(copy.deepcopy(obj.obj_data)) + self._calculate_tracked_object_metrics(camera, obj) + + if not ptz_moving_at_frame_time( + obj.obj_data["frame_time"], + self.ptz_metrics[camera].start_time.value, + self.ptz_metrics[camera].stop_time.value, + ): + if self.tracked_object_metrics[camera]["below_distance_threshold"]: + logger.debug( + f"{camera}: Existing object (do NOT move ptz): {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" + ) + + # no need to move, but try zooming + self._autotrack_move_zoom_only(camera, obj) + else: + logger.debug( + f"{camera}: Existing object (need to move ptz): {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" + ) + + self._autotrack_move_ptz(camera, obj) + + return + + if ( + # The tracker lost an object, so let's check the previous object's region and compare it with the incoming object + # If it's within bounds, start tracking that object. + # Should we check region (maybe too broad) or expand the previous object's box a bit and check that? + self.tracked_object[camera] is None + and obj.camera_config.name == camera + and obj.obj_data["label"] in self.object_types[camera] + and not obj.previous["false_positive"] + and not obj.false_positive + and self.tracked_object_history[camera] + ): + if ( + intersection_over_union( + self.tracked_object_history[camera][-1]["region"], + obj.obj_data["box"], + ) + < 0.2 + ): + logger.debug( + f"{camera}: Reacquired object: {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" + ) + self.tracked_object[camera] = obj + + self.tracked_object_history[camera].clear() + self.tracked_object_history[camera].append( + copy.deepcopy(obj.obj_data) + ) + self._calculate_tracked_object_metrics(camera, obj) + self._autotrack_move_ptz(camera, obj) + + return + + def end_object(self, camera, obj): + if self.config.cameras[camera].onvif.autotracking.enabled: + if ( + self.tracked_object[camera] is not None + and obj.obj_data["id"] == self.tracked_object[camera].obj_data["id"] + ): + logger.debug( + f"{camera}: End object: {obj.obj_data['id']} {obj.obj_data['box']}" + ) + self.tracked_object[camera] = None + self.tracked_object_metrics[camera] = { + "max_target_box": AUTOTRACKING_MAX_AREA_RATIO + ** (1 / self.zoom_factor[camera]) + } + + async def camera_maintenance(self, camera): + # bail and don't check anything if we're calibrating or tracking an object + if ( + not self.autotracker_init[camera] + or self.calibrating[camera] + or self.tracked_object[camera] is not None + ): + return + + # calls get_camera_status to check/update ptz movement + # returns camera to preset after timeout when tracking is over + autotracker_config = self.config.cameras[camera].onvif.autotracking + + if not self.autotracker_init[camera]: + self._autotracker_setup(self.config.cameras[camera], camera) + # regularly update camera status + if not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + # return to preset if tracking is over + if ( + self.tracked_object[camera] is None + and self.tracked_object_history[camera] + and ( + # might want to use a different timestamp here? + self.ptz_metrics[camera].frame_time.value + - self.tracked_object_history[camera][-1]["frame_time"] + >= autotracker_config.timeout + ) + and autotracker_config.return_preset + ): + # clear tracked object and reset zoom level + self.tracked_object[camera] = None + self.tracked_object_history[camera].clear() + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + logger.debug( + f"{camera}: Time is {self.ptz_metrics[camera].frame_time.value}, returning to preset: {autotracker_config.return_preset}" + ) + await self.onvif._move_to_preset( + camera, + autotracker_config.return_preset.lower(), + ) + + # update stored zoom level from preset + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + self.ptz_metrics[camera].tracking_active.clear() + self.dispatcher.publish( + f"{camera}/ptz_autotracker/active", "OFF", retain=False + ) + self.ptz_metrics[camera].reset.set() diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py new file mode 100644 index 0000000..d29923e --- /dev/null +++ b/frigate/ptz/onvif.py @@ -0,0 +1,1100 @@ +"""Configure and control camera via onvif.""" + +import asyncio +import logging +import threading +import time +from enum import Enum +from importlib.util import find_spec +from pathlib import Path +from typing import Any + +import numpy +from onvif import ONVIFCamera, ONVIFError, ONVIFService +from zeep.exceptions import Fault, TransportError + +from frigate.camera import PTZMetrics +from frigate.config import FrigateConfig, ZoomingModeEnum +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.util.builtin import find_by_key + +logger = logging.getLogger(__name__) + + +class OnvifCommandEnum(str, Enum): + """Holds all possible move commands""" + + init = "init" + move_down = "move_down" + move_left = "move_left" + move_relative = "move_relative" + move_right = "move_right" + move_up = "move_up" + preset = "preset" + stop = "stop" + zoom_in = "zoom_in" + zoom_out = "zoom_out" + focus_in = "focus_in" + focus_out = "focus_out" + + +class OnvifController: + ptz_metrics: dict[str, PTZMetrics] + + def __init__( + self, config: FrigateConfig, ptz_metrics: dict[str, PTZMetrics] + ) -> None: + self.cams: dict[str, dict] = {} + self.failed_cams: dict[str, dict] = {} + self.max_retries = 5 + self.reset_timeout = 900 # 15 minutes + self.config = config + self.ptz_metrics = ptz_metrics + + self.status_locks: dict[str, asyncio.Lock] = {} + + # Create a dedicated event loop and run it in a separate thread + self.loop = asyncio.new_event_loop() + self.loop_thread = threading.Thread(target=self._run_event_loop, daemon=True) + self.loop_thread.start() + + self.camera_configs = {} + for cam_name, cam in config.cameras.items(): + if not cam.enabled: + continue + if cam.onvif.host: + self.camera_configs[cam_name] = cam + self.status_locks[cam_name] = asyncio.Lock() + + self.config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [CameraConfigUpdateEnum.onvif], + ) + + asyncio.run_coroutine_threadsafe(self._init_cameras(), self.loop) + asyncio.run_coroutine_threadsafe(self._poll_config_updates(), self.loop) + + def _run_event_loop(self) -> None: + """Run the event loop in a separate thread.""" + asyncio.set_event_loop(self.loop) + try: + self.loop.run_forever() + except Exception as e: + logger.error(f"Onvif event loop terminated unexpectedly: {e}") + + async def _init_cameras(self) -> None: + """Initialize all configured cameras.""" + for cam_name in self.camera_configs: + await self._init_single_camera(cam_name) + + async def _poll_config_updates(self) -> None: + """Poll for ONVIF config updates and re-initialize cameras as needed.""" + while True: + await asyncio.sleep(1) + try: + updates = self.config_subscriber.check_for_updates() + for update_type, cameras in updates.items(): + if update_type == CameraConfigUpdateEnum.onvif.name: + for cam_name in cameras: + await self._reinit_camera(cam_name) + except Exception: + logger.error("Error checking for ONVIF config updates") + + async def _close_camera(self, cam_name: str) -> None: + """Close the ONVIF client session for a camera.""" + cam_state = self.cams.get(cam_name) + if cam_state and "onvif" in cam_state: + try: + await cam_state["onvif"].close() + except Exception: + logger.debug(f"Error closing ONVIF session for {cam_name}") + + async def _reinit_camera(self, cam_name: str) -> None: + """Re-initialize a camera after config change.""" + logger.info(f"Re-initializing ONVIF for {cam_name} due to config change") + + # close existing session before re-init + await self._close_camera(cam_name) + + cam = self.config.cameras.get(cam_name) + if not cam or not cam.onvif.host: + # ONVIF removed from config, clean up + self.cams.pop(cam_name, None) + self.camera_configs.pop(cam_name, None) + self.failed_cams.pop(cam_name, None) + return + + # update stored config and reset state + self.camera_configs[cam_name] = cam + if cam_name not in self.status_locks: + self.status_locks[cam_name] = asyncio.Lock() + self.cams.pop(cam_name, None) + self.failed_cams.pop(cam_name, None) + + await self._init_single_camera(cam_name) + + async def _init_single_camera(self, cam_name: str) -> bool: + """Initialize a single camera by name. + + Args: + cam_name: The name of the camera to initialize + + Returns: + bool: True if initialization succeeded, False otherwise + """ + if cam_name not in self.camera_configs: + logger.error(f"No configuration found for camera {cam_name}") + return False + + cam = self.camera_configs[cam_name] + try: + self.cams[cam_name] = { + "onvif": ONVIFCamera( + cam.onvif.host, + cam.onvif.port, + cam.onvif.user, + cam.onvif.password, + wsdl_dir=str(Path(find_spec("onvif").origin).parent / "wsdl"), + adjust_time=cam.onvif.ignore_time_mismatch, + encrypt=not cam.onvif.tls_insecure, + ), + "init": False, + "active": False, + "features": [], + "presets": {}, + "profiles": [], + } + return True + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.error(f"Failed to create ONVIF camera instance for {cam_name}: {e}") + # track initial failures + self.failed_cams[cam_name] = { + "retry_attempts": 0, + "last_error": str(e), + "last_attempt": time.time(), + } + return False + + async def _init_onvif(self, camera_name: str) -> bool: + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + try: + await onvif.update_xaddrs() + except Exception as e: + logger.error(f"Onvif connection failed for {camera_name}: {e}") + return False + + # create init services + media: ONVIFService = await onvif.create_media_service() + logger.debug(f"Onvif media xaddr for {camera_name}: {media.xaddr}") + + try: + # this will fire an exception if camera is not a ptz + capabilities = onvif.get_definition("ptz") + logger.debug(f"Onvif capabilities for {camera_name}: {capabilities}") + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.error( + f"Unable to get Onvif capabilities for camera: {camera_name}: {e}" + ) + return False + + try: + profiles = await media.GetProfiles() + logger.debug(f"Onvif profiles for {camera_name}: {profiles}") + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.error( + f"Unable to get Onvif media profiles for camera: {camera_name}: {e}" + ) + return False + + # build list of valid PTZ profiles + valid_profiles = [ + p + for p in profiles + if p.VideoEncoderConfiguration + and p.PTZConfiguration + and ( + p.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace is not None + or p.PTZConfiguration.DefaultContinuousZoomVelocitySpace is not None + ) + ] + + # store available profiles for API response and log for debugging + self.cams[camera_name]["profiles"] = [ + {"name": getattr(p, "Name", None) or p.token, "token": p.token} + for p in valid_profiles + ] + for p in valid_profiles: + logger.debug( + "Onvif profile for %s: name='%s', token='%s'", + camera_name, + getattr(p, "Name", None), + p.token, + ) + + configured_profile = self.config.cameras[camera_name].onvif.profile + profile = None + + if configured_profile is not None: + # match by exact token first, then by name + for p in valid_profiles: + if p.token == configured_profile: + profile = p + break + if profile is None: + for p in valid_profiles: + if getattr(p, "Name", None) == configured_profile: + profile = p + break + if profile is None: + available = [ + f"name='{getattr(p, 'Name', None)}', token='{p.token}'" + for p in valid_profiles + ] + logger.error( + "Onvif profile '%s' not found for camera %s. Available profiles: %s", + configured_profile, + camera_name, + available, + ) + return False + else: + # use the first profile that has a valid ptz configuration + profile = valid_profiles[0] if valid_profiles else None + + if profile is None: + logger.error( + f"No appropriate Onvif profiles found for camera: {camera_name}." + ) + return False + + logger.debug(f"Selected Onvif profile for {camera_name}: {profile}") + + # get the PTZ config for the profile + try: + configs = profile.PTZConfiguration + logger.debug( + f"Onvif ptz config for media profile in {camera_name}: {configs}" + ) + except Exception as e: + logger.error( + f"Invalid Onvif PTZ configuration for camera: {camera_name}: {e}" + ) + return False + + ptz: ONVIFService = await onvif.create_ptz_service() + self.cams[camera_name]["ptz"] = ptz + + try: + imaging: ONVIFService = await onvif.create_imaging_service() + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.debug(f"Imaging service not supported for {camera_name}: {e}") + imaging = None + self.cams[camera_name]["imaging"] = imaging + try: + video_sources = await media.GetVideoSources() + if video_sources and len(video_sources) > 0: + self.cams[camera_name]["video_source_token"] = video_sources[0].token + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.debug(f"Unable to get video sources for {camera_name}: {e}") + self.cams[camera_name]["video_source_token"] = None + + # setup continuous moving request + move_request = ptz.create_type("ContinuousMove") + move_request.ProfileToken = profile.token + self.cams[camera_name]["move_request"] = move_request + + # get PTZ configuration options for feature detection and relative movement + ptz_config = None + fov_space_id = None + + try: + request = ptz.create_type("GetConfigurationOptions") + request.ConfigurationToken = profile.PTZConfiguration.token + ptz_config = await ptz.GetConfigurationOptions(request) + logger.debug( + f"Onvif PTZ configuration options for {camera_name}: {ptz_config}" + ) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.debug( + f"Unable to get PTZ configuration options for {camera_name}: {e}" + ) + + # detect FOV translation space for relative movement + if ptz_config is not None: + try: + fov_space_id = next( + ( + i + for i, space in enumerate( + ptz_config.Spaces.RelativePanTiltTranslationSpace + ) + if "TranslationSpaceFov" in space["URI"] + ), + None, + ) + except (AttributeError, TypeError): + fov_space_id = None + + autotracking_config = self.config.cameras[camera_name].onvif.autotracking + autotracking_enabled = ( + autotracking_config.enabled_in_config and autotracking_config.enabled + ) + + # autotracking-only: status request and service capabilities + if autotracking_enabled: + status_request = ptz.create_type("GetStatus") + status_request.ProfileToken = profile.token + self.cams[camera_name]["status_request"] = status_request + + service_capabilities_request = ptz.create_type("GetServiceCapabilities") + self.cams[camera_name]["service_capabilities_request"] = ( + service_capabilities_request + ) + + # setup relative move request when FOV relative movement is supported + if ( + fov_space_id is not None + and configs.DefaultRelativePanTiltTranslationSpace is not None + ): + # one-off GetStatus to seed Translation field + status = None + try: + one_off_status_request = ptz.create_type("GetStatus") + one_off_status_request.ProfileToken = profile.token + status = await ptz.GetStatus(one_off_status_request) + logger.debug(f"Onvif status for {camera_name}: {status}") + except Exception as e: + logger.warning(f"Unable to get status from camera {camera_name}: {e}") + + rel_move_request = ptz.create_type("RelativeMove") + rel_move_request.ProfileToken = profile.token + logger.debug(f"{camera_name}: Relative move request: {rel_move_request}") + + fov_uri = ptz_config["Spaces"]["RelativePanTiltTranslationSpace"][ + fov_space_id + ]["URI"] + + if rel_move_request.Translation is None: + if status is not None: + # seed from current position + rel_move_request.Translation = status.Position + rel_move_request.Translation.PanTilt.space = fov_uri + else: + # fallback: construct Translation explicitly + rel_move_request.Translation = { + "PanTilt": {"x": 0, "y": 0, "space": fov_uri} + } + + # configure zoom on relative move request + if ( + autotracking_enabled + and autotracking_config.zooming != ZoomingModeEnum.disabled + ): + zoom_space_id = next( + ( + i + for i, space in enumerate( + ptz_config.Spaces.RelativeZoomTranslationSpace + ) + if "TranslationGenericSpace" in space["URI"] + ), + None, + ) + try: + if zoom_space_id is not None: + rel_move_request.Translation.Zoom.space = ptz_config["Spaces"][ + "RelativeZoomTranslationSpace" + ][zoom_space_id]["URI"] + except Exception as e: + autotracking_config.zooming = ZoomingModeEnum.disabled + logger.warning( + f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}" + ) + else: + # remove zoom fields from relative move request + if ( + rel_move_request["Translation"] is not None + and "Zoom" in rel_move_request["Translation"] + ): + del rel_move_request["Translation"]["Zoom"] + if ( + rel_move_request["Speed"] is not None + and "Zoom" in rel_move_request["Speed"] + ): + del rel_move_request["Speed"]["Zoom"] + logger.debug( + f"{camera_name}: Relative move request after deleting zoom: {rel_move_request}" + ) + + if rel_move_request.Speed is None: + rel_move_request.Speed = configs.DefaultPTZSpeed if configs else None + logger.debug( + f"{camera_name}: Relative move request after setup: {rel_move_request}" + ) + self.cams[camera_name]["relative_move_request"] = rel_move_request + + # setup absolute move request + abs_move_request = ptz.create_type("AbsoluteMove") + abs_move_request.ProfileToken = profile.token + self.cams[camera_name]["absolute_move_request"] = abs_move_request + + # setup existing presets + try: + presets: list[dict] = await ptz.GetPresets({"ProfileToken": profile.token}) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.warning(f"Unable to get presets from camera: {camera_name}: {e}") + presets = [] + + for preset in presets: + preset_name = getattr(preset, "Name") or f"preset {preset['token']}" + # Some cameras (e.g. Reolink) return UTF-8 bytes that zeep decodes + # as latin-1, producing mojibake. Detect that and repair it by + # round-tripping through latin-1 -> utf-8. + try: + preset_name = preset_name.encode("latin-1").decode("utf-8") + except (UnicodeEncodeError, UnicodeDecodeError): + pass + self.cams[camera_name]["presets"][preset_name.lower()] = preset["token"] + + # get list of supported features + supported_features = [] + + if configs.DefaultContinuousPanTiltVelocitySpace: + supported_features.append("pt") + + if configs.DefaultContinuousZoomVelocitySpace: + supported_features.append("zoom") + + if configs.DefaultRelativePanTiltTranslationSpace: + supported_features.append("pt-r") + + if configs.DefaultRelativeZoomTranslationSpace: + supported_features.append("zoom-r") + if ptz_config is not None: + try: + self.cams[camera_name]["relative_zoom_range"] = ( + ptz_config.Spaces.RelativeZoomTranslationSpace[0] + ) + except Exception as e: + if autotracking_config.zooming == ZoomingModeEnum.relative: + autotracking_config.zooming = ZoomingModeEnum.disabled + logger.warning( + f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}" + ) + + if configs.DefaultAbsoluteZoomPositionSpace: + supported_features.append("zoom-a") + if ptz_config is not None: + try: + self.cams[camera_name]["absolute_zoom_range"] = ( + ptz_config.Spaces.AbsoluteZoomPositionSpace[0] + ) + self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits + except Exception as e: + if autotracking_config.zooming != ZoomingModeEnum.disabled: + autotracking_config.zooming = ZoomingModeEnum.disabled + logger.warning( + f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported. Exception: {e}" + ) + + # disable autotracking zoom if required ranges are unavailable + if autotracking_config.zooming != ZoomingModeEnum.disabled: + if autotracking_config.zooming == ZoomingModeEnum.relative: + if "relative_zoom_range" not in self.cams[camera_name]: + autotracking_config.zooming = ZoomingModeEnum.disabled + logger.warning( + f"Disabling autotracking zooming for {camera_name}: Relative zoom range unavailable" + ) + if autotracking_config.zooming == ZoomingModeEnum.absolute: + if "absolute_zoom_range" not in self.cams[camera_name]: + autotracking_config.zooming = ZoomingModeEnum.disabled + logger.warning( + f"Disabling autotracking zooming for {camera_name}: Absolute zoom range unavailable" + ) + + if ( + self.cams[camera_name]["video_source_token"] is not None + and imaging is not None + ): + try: + imaging_capabilities = await imaging.GetImagingSettings( + {"VideoSourceToken": self.cams[camera_name]["video_source_token"]} + ) + if ( + hasattr(imaging_capabilities, "Focus") + and imaging_capabilities.Focus + ): + supported_features.append("focus") + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.debug(f"Focus not supported for {camera_name}: {e}") + + # detect FOV relative movement support + if ( + fov_space_id is not None + and configs.DefaultRelativePanTiltTranslationSpace is not None + ): + supported_features.append("pt-r-fov") + self.cams[camera_name]["relative_fov_range"] = ( + ptz_config.Spaces.RelativePanTiltTranslationSpace[fov_space_id] + ) + + self.cams[camera_name]["features"] = supported_features + self.cams[camera_name]["init"] = True + return True + + async def _stop(self, camera_name: str) -> None: + move_request = self.cams[camera_name]["move_request"] + await self.cams[camera_name]["ptz"].Stop( + { + "ProfileToken": move_request.ProfileToken, + "PanTilt": True, + "Zoom": True, + } + ) + if ( + "focus" in self.cams[camera_name]["features"] + and self.cams[camera_name]["video_source_token"] + and self.cams[camera_name]["imaging"] is not None + ): + try: + stop_request = self.cams[camera_name]["imaging"].create_type("Stop") + stop_request.VideoSourceToken = self.cams[camera_name][ + "video_source_token" + ] + await self.cams[camera_name]["imaging"].Stop(stop_request) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.warning(f"Failed to stop focus for {camera_name}: {e}") + self.cams[camera_name]["active"] = False + + async def _move(self, camera_name: str, command: OnvifCommandEnum) -> None: + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, stopping..." + ) + await self._stop(camera_name) + + if "pt" not in self.cams[camera_name]["features"]: + logger.error(f"{camera_name} does not support ONVIF pan/tilt movement.") + return + + self.cams[camera_name]["active"] = True + move_request = self.cams[camera_name]["move_request"] + + if command == OnvifCommandEnum.move_left: + move_request.Velocity = {"PanTilt": {"x": -0.5, "y": 0}} + elif command == OnvifCommandEnum.move_right: + move_request.Velocity = {"PanTilt": {"x": 0.5, "y": 0}} + elif command == OnvifCommandEnum.move_up: + move_request.Velocity = { + "PanTilt": { + "x": 0, + "y": 0.5, + } + } + elif command == OnvifCommandEnum.move_down: + move_request.Velocity = { + "PanTilt": { + "x": 0, + "y": -0.5, + } + } + + try: + await self.cams[camera_name]["ptz"].ContinuousMove(move_request) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.warning(f"Onvif sending move request to {camera_name} failed: {e}") + + async def _move_relative(self, camera_name: str, pan, tilt, zoom, speed) -> None: + if "pt-r-fov" not in self.cams[camera_name]["features"]: + logger.error(f"{camera_name} does not support ONVIF RelativeMove (FOV).") + return + + logger.debug( + f"{camera_name} called RelativeMove: pan: {pan} tilt: {tilt} zoom: {zoom}" + ) + + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, not moving..." + ) + return + + self.cams[camera_name]["active"] = True + self.ptz_metrics[camera_name].motor_stopped.clear() + logger.debug( + f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + self.ptz_metrics[camera_name].start_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + self.ptz_metrics[camera_name].stop_time.value = 0 + move_request = self.cams[camera_name]["relative_move_request"] + + # function takes in -1 to 1 for pan and tilt, interpolate to the values of the camera. + # The onvif spec says this can report as +INF and -INF, so this may need to be modified + pan = numpy.interp( + pan, + [-1, 1], + [ + self.cams[camera_name]["relative_fov_range"]["XRange"]["Min"], + self.cams[camera_name]["relative_fov_range"]["XRange"]["Max"], + ], + ) + tilt = numpy.interp( + tilt, + [-1, 1], + [ + self.cams[camera_name]["relative_fov_range"]["YRange"]["Min"], + self.cams[camera_name]["relative_fov_range"]["YRange"]["Max"], + ], + ) + + move_request.Speed = { + "PanTilt": { + "x": speed, + "y": speed, + }, + } + + move_request.Translation.PanTilt.x = pan + move_request.Translation.PanTilt.y = tilt + + # include zoom if requested and camera supports relative zoom + if zoom != 0 and "zoom-r" in self.cams[camera_name]["features"]: + move_request.Speed = { + "PanTilt": { + "x": speed, + "y": speed, + }, + "Zoom": {"x": speed}, + } + move_request["Translation"]["Zoom"] = {"x": zoom} + + await self.cams[camera_name]["ptz"].RelativeMove(move_request) + + # reset after the move request + move_request.Translation.PanTilt.x = 0 + move_request.Translation.PanTilt.y = 0 + + if zoom != 0 and "zoom-r" in self.cams[camera_name]["features"]: + del move_request["Translation"]["Zoom"] + + self.cams[camera_name]["active"] = False + + async def _move_to_preset(self, camera_name: str, preset: str) -> None: + preset = preset.lower() + + if preset not in self.cams[camera_name]["presets"]: + logger.error(f"{preset} is not a valid preset for {camera_name}") + return + + self.cams[camera_name]["active"] = True + self.ptz_metrics[camera_name].start_time.value = 0 + self.ptz_metrics[camera_name].stop_time.value = 0 + move_request = self.cams[camera_name]["move_request"] + preset_token = self.cams[camera_name]["presets"][preset] + + await self.cams[camera_name]["ptz"].GotoPreset( + { + "ProfileToken": move_request.ProfileToken, + "PresetToken": preset_token, + } + ) + + self.cams[camera_name]["active"] = False + + async def _zoom(self, camera_name: str, command: OnvifCommandEnum) -> None: + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, stopping..." + ) + await self._stop(camera_name) + + if "zoom" not in self.cams[camera_name]["features"]: + logger.error(f"{camera_name} does not support ONVIF zooming.") + return + + self.cams[camera_name]["active"] = True + move_request = self.cams[camera_name]["move_request"] + + if command == OnvifCommandEnum.zoom_in: + move_request.Velocity = {"Zoom": {"x": 0.5}} + elif command == OnvifCommandEnum.zoom_out: + move_request.Velocity = {"Zoom": {"x": -0.5}} + + await self.cams[camera_name]["ptz"].ContinuousMove(move_request) + + async def _zoom_absolute(self, camera_name: str, zoom, speed) -> None: + if "zoom-a" not in self.cams[camera_name]["features"]: + logger.error(f"{camera_name} does not support ONVIF AbsoluteMove zooming.") + return + + logger.debug(f"{camera_name} called AbsoluteMove: zoom: {zoom}") + + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, not moving..." + ) + return + + self.cams[camera_name]["active"] = True + self.ptz_metrics[camera_name].motor_stopped.clear() + logger.debug( + f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + self.ptz_metrics[camera_name].start_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + self.ptz_metrics[camera_name].stop_time.value = 0 + move_request = self.cams[camera_name]["absolute_move_request"] + + # function takes in 0 to 1 for zoom, interpolate to the values of the camera. + zoom = numpy.interp( + zoom, + [0, 1], + [ + self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Min"], + self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Max"], + ], + ) + + move_request.Speed = {"Zoom": speed} + move_request.Position = {"Zoom": zoom} + + logger.debug(f"{camera_name}: Absolute zoom: {zoom}") + + await self.cams[camera_name]["ptz"].AbsoluteMove(move_request) + + self.cams[camera_name]["active"] = False + + async def _focus(self, camera_name: str, command: OnvifCommandEnum) -> None: + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, not moving..." + ) + await self._stop(camera_name) + + if ( + "focus" not in self.cams[camera_name]["features"] + or not self.cams[camera_name]["video_source_token"] + or self.cams[camera_name]["imaging"] is None + ): + logger.error(f"{camera_name} does not support ONVIF continuous focus.") + return + + self.cams[camera_name]["active"] = True + move_request = self.cams[camera_name]["imaging"].create_type("Move") + move_request.VideoSourceToken = self.cams[camera_name]["video_source_token"] + move_request.Focus = { + "Continuous": { + "Speed": 0.5 if command == OnvifCommandEnum.focus_in else -0.5 + } + } + + try: + await self.cams[camera_name]["imaging"].Move(move_request) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.warning(f"Onvif sending focus request to {camera_name} failed: {e}") + self.cams[camera_name]["active"] = False + + async def handle_command_async( + self, camera_name: str, command: OnvifCommandEnum, param: str = "" + ) -> None: + """Handle ONVIF commands asynchronously""" + if camera_name not in self.cams.keys(): + logger.error(f"ONVIF is not configured for {camera_name}") + return + + if not self.cams[camera_name]["init"]: + if not await self._init_onvif(camera_name): + return + + try: + if command == OnvifCommandEnum.init: + # already init + return + elif command == OnvifCommandEnum.stop: + await self._stop(camera_name) + elif command == OnvifCommandEnum.preset: + await self._move_to_preset(camera_name, param) + elif command == OnvifCommandEnum.move_relative: + parts = param.split("_") + if len(parts) == 3: + _, pan, tilt = parts + zoom = 0.0 + elif len(parts) == 4: + _, pan, tilt, zoom = parts + else: + logger.error(f"Invalid move_relative params: {param}") + return + await self._move_relative( + camera_name, float(pan), float(tilt), float(zoom), 1 + ) + elif command in (OnvifCommandEnum.zoom_in, OnvifCommandEnum.zoom_out): + await self._zoom(camera_name, command) + elif command in (OnvifCommandEnum.focus_in, OnvifCommandEnum.focus_out): + await self._focus(camera_name, command) + else: + await self._move(camera_name, command) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.error(f"Unable to handle onvif command: {e}") + + def handle_command( + self, camera_name: str, command: OnvifCommandEnum, param: str = "" + ) -> None: + """ + Handle ONVIF commands by scheduling them in the event loop. + """ + future = asyncio.run_coroutine_threadsafe( + self.handle_command_async(camera_name, command, param), self.loop + ) + + try: + # Wait with a timeout to prevent blocking indefinitely + future.result(timeout=10) + except TimeoutError: + logger.error(f"Command {command} timed out for camera {camera_name}") + except Exception as e: + logger.error( + f"Error executing command {command} for camera {camera_name}: {e}" + ) + + async def get_camera_info(self, camera_name: str) -> dict[str, Any]: + """ + Get ptz capabilities and presets, attempting to reconnect if ONVIF is configured + but not initialized. + + Returns camera details including features and presets if available. + """ + if not self.config.cameras[camera_name].enabled: + logger.debug( + f"Camera {camera_name} disabled, won't try to initialize ONVIF" + ) + return {} + + if camera_name not in self.cams.keys() and ( + camera_name not in self.config.cameras + or not self.config.cameras[camera_name].onvif.host + ): + logger.debug(f"ONVIF is not configured for {camera_name}") + return {} + + if camera_name in self.cams.keys() and self.cams[camera_name]["init"]: + return { + "name": camera_name, + "features": self.cams[camera_name]["features"], + "presets": list(self.cams[camera_name]["presets"].keys()), + "profiles": self.cams[camera_name].get("profiles", []), + } + + if camera_name not in self.cams.keys() and camera_name in self.config.cameras: + success = await self._init_single_camera(camera_name) + if not success: + return {} + + # Reset retry count after timeout + attempts = self.failed_cams.get(camera_name, {}).get("retry_attempts", 0) + last_attempt = self.failed_cams.get(camera_name, {}).get("last_attempt", 0) + + if last_attempt and (time.time() - last_attempt) > self.reset_timeout: + logger.debug(f"Resetting retry count for {camera_name} after timeout") + attempts = 0 + self.failed_cams[camera_name]["retry_attempts"] = 0 + + # Attempt initialization/reconnection + if attempts < self.max_retries: + logger.info( + f"Attempting ONVIF initialization for {camera_name} (retry {attempts + 1}/{self.max_retries})" + ) + try: + if await self._init_onvif(camera_name): + if camera_name in self.failed_cams: + del self.failed_cams[camera_name] + return { + "name": camera_name, + "features": self.cams[camera_name]["features"], + "presets": list(self.cams[camera_name]["presets"].keys()), + } + else: + logger.warning(f"ONVIF initialization failed for {camera_name}") + except Exception as e: + logger.error( + f"Error during ONVIF initialization for {camera_name}: {e}" + ) + if camera_name not in self.failed_cams: + self.failed_cams[camera_name] = {"retry_attempts": 0} + self.failed_cams[camera_name].update( + { + "retry_attempts": attempts + 1, + "last_error": str(e), + "last_attempt": time.time(), + } + ) + + if attempts >= self.max_retries: + remaining_time = max( + 0, int((self.reset_timeout - (time.time() - last_attempt)) / 60) + ) + logger.error( + f"Too many ONVIF initialization attempts for {camera_name}, retry in {remaining_time} minute{'s' if remaining_time != 1 else ''}" + ) + + logger.debug(f"Could not initialize ONVIF for {camera_name}") + return {} + + async def get_service_capabilities(self, camera_name: str) -> None: + if camera_name not in self.cams.keys(): + logger.error(f"ONVIF is not configured for {camera_name}") + return {} + + if not self.cams[camera_name]["init"]: + await self._init_onvif(camera_name) + + service_capabilities_request = self.cams[camera_name][ + "service_capabilities_request" + ] + try: + service_capabilities = await self.cams[camera_name][ + "ptz" + ].GetServiceCapabilities(service_capabilities_request) + + logger.debug( + f"Onvif service capabilities for {camera_name}: {service_capabilities}" + ) + + # MoveStatus is required for autotracking - should return "true" if supported + return find_by_key(vars(service_capabilities), "MoveStatus") + except Exception as e: + logger.warning( + f"Camera {camera_name} does not support the ONVIF GetServiceCapabilities method. Autotracking will not function correctly and must be disabled in your config. Exception: {e}" + ) + return False + + async def get_camera_status(self, camera_name: str) -> None: + async with self.status_locks[camera_name]: + if camera_name not in self.cams.keys(): + logger.error(f"ONVIF is not configured for {camera_name}") + return + + if not self.cams[camera_name]["init"]: + if not await self._init_onvif(camera_name): + return + + status_request = self.cams[camera_name]["status_request"] + try: + status = await self.cams[camera_name]["ptz"].GetStatus(status_request) + except Exception: + pass # We're unsupported, that'll be reported in the next check. + + try: + pan_tilt_status = getattr(status.MoveStatus, "PanTilt", None) + zoom_status = getattr(status.MoveStatus, "Zoom", None) + + # if it's not an attribute, see if MoveStatus even exists in the status result + if pan_tilt_status is None: + pan_tilt_status = getattr(status, "MoveStatus", None) + + # we're unsupported + if pan_tilt_status is None or pan_tilt_status not in [ + "IDLE", + "MOVING", + ]: + raise Exception + except Exception: + logger.warning( + f"Camera {camera_name} does not support the ONVIF GetStatus method. Autotracking will not function correctly and must be disabled in your config." + ) + return + + logger.debug( + f"{camera_name}: Pan/tilt status: {pan_tilt_status}, Zoom status: {zoom_status}" + ) + + if pan_tilt_status == "IDLE" and ( + zoom_status is None or zoom_status == "IDLE" + ): + self.cams[camera_name]["active"] = False + if not self.ptz_metrics[camera_name].motor_stopped.is_set(): + self.ptz_metrics[camera_name].motor_stopped.set() + + logger.debug( + f"{camera_name}: PTZ stop time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + + self.ptz_metrics[camera_name].stop_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + else: + self.cams[camera_name]["active"] = True + if self.ptz_metrics[camera_name].motor_stopped.is_set(): + self.ptz_metrics[camera_name].motor_stopped.clear() + + logger.debug( + f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + + self.ptz_metrics[camera_name].start_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + self.ptz_metrics[camera_name].stop_time.value = 0 + + if ( + self.config.cameras[camera_name].onvif.autotracking.zooming + != ZoomingModeEnum.disabled + ): + # store absolute zoom level as 0 to 1 interpolated from the values of the camera + self.ptz_metrics[camera_name].zoom_level.value = numpy.interp( + round(status.Position.Zoom.x, 2), + [ + self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Min"], + self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Max"], + ], + [0, 1], + ) + logger.debug( + f"{camera_name}: Camera zoom level: {self.ptz_metrics[camera_name].zoom_level.value}" + ) + + # some hikvision cams won't update MoveStatus, so warn if it hasn't changed + if ( + not self.ptz_metrics[camera_name].motor_stopped.is_set() + and not self.ptz_metrics[camera_name].reset.is_set() + and self.ptz_metrics[camera_name].start_time.value != 0 + and self.ptz_metrics[camera_name].frame_time.value + > (self.ptz_metrics[camera_name].start_time.value + 10) + and self.ptz_metrics[camera_name].stop_time.value == 0 + ): + logger.debug( + f"Start time: {self.ptz_metrics[camera_name].start_time.value}, Stop time: {self.ptz_metrics[camera_name].stop_time.value}, Frame time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + # set the stop time so we don't come back into this again and spam the logs + self.ptz_metrics[camera_name].stop_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + logger.warning( + f"Camera {camera_name} is still in ONVIF 'MOVING' status." + ) + + def close(self) -> None: + """Gracefully shut down the ONVIF controller.""" + if not hasattr(self, "loop") or self.loop.is_closed(): + logger.debug("ONVIF controller already closed") + return + + logger.info("Exiting ONVIF controller...") + self.config_subscriber.stop() + + def stop_and_cleanup(): + try: + self.loop.stop() + except Exception as e: + logger.error(f"Error during loop cleanup: {e}") + + # Schedule stop and cleanup in the loop thread + self.loop.call_soon_threadsafe(stop_and_cleanup) + + self.loop_thread.join() diff --git a/frigate/record/__init__.py b/frigate/record/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py new file mode 100644 index 0000000..71097f1 --- /dev/null +++ b/frigate/record/cleanup.py @@ -0,0 +1,392 @@ +"""Cleanup recordings that are expired based on retention config.""" + +import datetime +import itertools +import logging +import os +import threading +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path +from typing import Any + +from playhouse.sqlite_ext import SqliteExtDatabase + +from frigate.config import CameraConfig, FrigateConfig, RetainModeEnum +from frigate.const import CACHE_DIR, CLIPS_DIR, MAX_WAL_SIZE, RECORD_DIR +from frigate.models import Previews, Recordings, ReviewSegment, UserReviewStatus +from frigate.util.builtin import clear_and_unlink +from frigate.util.media import remove_empty_directories + +logger = logging.getLogger(__name__) + + +class RecordingCleanup(threading.Thread): + """Cleanup existing recordings based on retention config.""" + + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + super().__init__(name="recording_cleanup") + self.config = config + self.stop_event = stop_event + + def clean_tmp_previews(self) -> None: + """delete any previews in the cache that are more than 1 hour old.""" + for p in Path(CACHE_DIR).rglob("preview_*.mp4"): + logger.debug(f"Checking preview {p}.") + if p.stat().st_mtime < (datetime.datetime.now().timestamp() - 60 * 60): + logger.debug("Deleting preview.") + clear_and_unlink(p) + + def clean_tmp_clips(self) -> None: + """delete any clips in the cache that are more than 1 hour old.""" + for p in Path(os.path.join(CLIPS_DIR, "cache")).rglob("clip_*.mp4"): + logger.debug(f"Checking tmp clip {p}.") + if p.stat().st_mtime < (datetime.datetime.now().timestamp() - 60 * 60): + logger.debug("Deleting tmp clip.") + clear_and_unlink(p) + + def truncate_wal(self) -> None: + """check if the WAL needs to be manually truncated.""" + + # by default the WAL should be check-pointed automatically + # however, high levels of activity can prevent an opportunity + # for the checkpoint to be finished which means the WAL will grow + # without bound + + # with auto checkpoint most users should never hit this + + if ( + os.stat(f"{self.config.database.path}-wal").st_size / (1024 * 1024) + ) > MAX_WAL_SIZE: + db = SqliteExtDatabase(self.config.database.path) + db.execute_sql("PRAGMA wal_checkpoint(TRUNCATE);") + db.close() + + def expire_review_segments( + self, config: CameraConfig, now: datetime.datetime + ) -> set[Path]: + """Delete review segments that are expired""" + alert_expire_date = ( + now - datetime.timedelta(days=config.record.alerts.retain.days) + ).timestamp() + detection_expire_date = ( + now - datetime.timedelta(days=config.record.detections.retain.days) + ).timestamp() + expired_reviews = ( + ReviewSegment.select(ReviewSegment.id, ReviewSegment.thumb_path) + .where(ReviewSegment.camera == config.name) + .where( + ( + (ReviewSegment.severity == "alert") + & (ReviewSegment.end_time < alert_expire_date) + ) + | ( + (ReviewSegment.severity == "detection") + & (ReviewSegment.end_time < detection_expire_date) + ) + ) + .namedtuples() + ) + + maybe_empty_dirs = set() + thumbs_to_delete = list(map(lambda x: x[1], expired_reviews)) + for thumb_path in thumbs_to_delete: + thumb_path = Path(thumb_path) + thumb_path.unlink(missing_ok=True) + maybe_empty_dirs.add(thumb_path.parent) + + max_deletes = 100000 + deleted_reviews_list = list(map(lambda x: x[0], expired_reviews)) + for i in range(0, len(deleted_reviews_list), max_deletes): + ReviewSegment.delete().where( + ReviewSegment.id << deleted_reviews_list[i : i + max_deletes] + ).execute() + UserReviewStatus.delete().where( + UserReviewStatus.review_segment + << deleted_reviews_list[i : i + max_deletes] + ).execute() + + return maybe_empty_dirs + + def expire_existing_camera_recordings( + self, + continuous_expire_date: float, + motion_expire_date: float, + config: CameraConfig, + reviews: list[Any], + ) -> set[Path]: + """Delete recordings for existing camera based on retention config.""" + # Get the timestamp for cutoff of retained days + + # Get recordings to check for expiration + recordings = ( + Recordings.select( + Recordings.id, + Recordings.start_time, + Recordings.end_time, + Recordings.path, + Recordings.objects, + Recordings.motion, + Recordings.dBFS, + ) + .where( + (Recordings.camera == config.name) + & ( + ( + (Recordings.end_time < continuous_expire_date) + & (Recordings.motion == 0) + & (Recordings.dBFS == 0) + ) + | (Recordings.end_time < motion_expire_date) + ) + ) + .order_by(Recordings.start_time) + .namedtuples() + .iterator() + ) + + maybe_empty_dirs = set() + + # loop over recordings and see if they overlap with any non-expired reviews + # TODO: expire segments based on segment stats according to config + review_start = 0 + deleted_recordings = set() + kept_recordings: list[tuple[float, float]] = [] + for recording in recordings: + keep = False + mode = None + # Now look for a reason to keep this recording segment + for idx in range(review_start, len(reviews)): + review = reviews[idx] + severity = review.severity + pre_capture = config.record.get_review_pre_capture(severity) + post_capture = config.record.get_review_post_capture(severity) + + # if the review starts in the future, stop checking reviews + # and let this recording segment expire + if review.start_time - pre_capture > recording.end_time: + keep = False + break + + # if the review is in progress or ends after the recording starts, keep it + # and stop looking at reviews + if ( + review.end_time is None + or review.end_time + post_capture >= recording.start_time + ): + keep = True + mode = ( + config.record.alerts.retain.mode + if review.severity == "alert" + else config.record.detections.retain.mode + ) + break + + # if the review ends before this recording segment starts, skip + # this review and check the next review for an overlap. + # since the review and recordings are sorted, we can skip review + # that end before the previous recording segment started on future segments + if review.end_time + post_capture < recording.start_time: + review_start = idx + + # Delete recordings outside of the retention window or based on the retention mode + if ( + not keep + or ( + mode == RetainModeEnum.motion + and recording.motion == 0 + and recording.objects == 0 + and recording.dBFS == 0 + ) + or (mode == RetainModeEnum.active_objects and recording.objects == 0) + ): + recording_path = Path(recording.path) + recording_path.unlink(missing_ok=True) + deleted_recordings.add(recording.id) + maybe_empty_dirs.add(recording_path.parent) + else: + kept_recordings.append((recording.start_time, recording.end_time)) + + # expire recordings + logger.debug(f"Expiring {len(deleted_recordings)} recordings") + # delete up to 100,000 at a time + max_deletes = 100000 + deleted_recordings_list = list(deleted_recordings) + for i in range(0, len(deleted_recordings_list), max_deletes): + Recordings.delete().where( + Recordings.id << deleted_recordings_list[i : i + max_deletes] + ).execute() + + previews = ( + Previews.select( + Previews.id, + Previews.start_time, + Previews.end_time, + Previews.path, + ) + .where( + (Previews.camera == config.name) + & (Previews.end_time < continuous_expire_date) + & (Previews.end_time < motion_expire_date) + ) + .order_by(Previews.start_time) + .namedtuples() + .iterator() + ) + + # expire previews + recording_start = 0 + deleted_previews = set() + for preview in previews: + keep = False + # look for a reason to keep this preview + for idx in range(recording_start, len(kept_recordings)): + start_time, end_time = kept_recordings[idx] + + # if the recording starts in the future, stop checking recordings + # and let this preview expire + if start_time > preview.end_time: + keep = False + break + + # if the recording ends after the preview starts, keep it + # and stop looking at recordings + if end_time >= preview.start_time: + keep = True + break + + # if the recording ends before this preview starts, skip + # this recording and check the next recording for an overlap. + # since the kept recordings and previews are sorted, we can skip recordings + # that end before the current preview started + if end_time < preview.start_time: + recording_start = idx + + # Delete previews without any relevant recordings + if not keep: + preview_path = Path(preview.path) + preview_path.unlink(missing_ok=True) + deleted_previews.add(preview.id) + maybe_empty_dirs.add(preview_path.parent) + + # expire previews + logger.debug(f"Expiring {len(deleted_previews)} previews") + # delete up to 100,000 at a time + max_deletes = 100000 + deleted_previews_list = list(deleted_previews) + for i in range(0, len(deleted_previews_list), max_deletes): + Previews.delete().where( + Previews.id << deleted_previews_list[i : i + max_deletes] + ).execute() + + return maybe_empty_dirs + + def expire_recordings(self) -> set[Path]: + """Delete recordings based on retention config.""" + logger.debug("Start expire recordings.") + logger.debug("Start deleted cameras.") + + # Handle deleted cameras + expire_days = max( + self.config.record.continuous.days, self.config.record.motion.days + ) + expire_before = ( + datetime.datetime.now() - datetime.timedelta(days=expire_days) + ).timestamp() + no_camera_recordings = ( + Recordings.select( + Recordings.id, + Recordings.path, + ) + .where( + Recordings.camera.not_in(list(self.config.cameras.keys())), # type: ignore[call-arg, arg-type, misc] + Recordings.end_time < expire_before, + ) + .namedtuples() + .iterator() + ) + + maybe_empty_dirs = set() + + deleted_recordings = set() + for recording in no_camera_recordings: + recording_path = Path(recording.path) + recording_path.unlink(missing_ok=True) + deleted_recordings.add(recording.id) + maybe_empty_dirs.add(recording_path.parent) + + logger.debug(f"Expiring {len(deleted_recordings)} recordings") + # delete up to 100,000 at a time + max_deletes = 100000 + deleted_recordings_list = list(deleted_recordings) + for i in range(0, len(deleted_recordings_list), max_deletes): + Recordings.delete().where( + Recordings.id << deleted_recordings_list[i : i + max_deletes] + ).execute() + logger.debug("End deleted cameras.") + + logger.debug("Start all cameras.") + for camera, config in self.config.cameras.items(): + logger.debug(f"Start camera: {camera}.") + now = datetime.datetime.now() + + maybe_empty_dirs |= self.expire_review_segments(config, now) + continuous_expire_date = ( + now - datetime.timedelta(days=config.record.continuous.days) + ).timestamp() + motion_expire_date = ( + now + - datetime.timedelta( + days=max( + config.record.motion.days, config.record.continuous.days + ) # can't keep motion for less than continuous + ) + ).timestamp() + + # Get all the reviews to check against + reviews = ( + ReviewSegment.select( + ReviewSegment.start_time, + ReviewSegment.end_time, + ReviewSegment.severity, + ) + .where( + ReviewSegment.camera == camera, + # candidate recordings can extend up to continuous_expire_date + # (the no-motion no-audio branch of the recordings query), + # so reviews must cover that full range to avoid deleting + # segments that overlap recent alerts/detections. + ReviewSegment.start_time < continuous_expire_date, + ) + .order_by(ReviewSegment.start_time) + .namedtuples() + ) + + maybe_empty_dirs |= self.expire_existing_camera_recordings( + continuous_expire_date, motion_expire_date, config, reviews + ) + logger.debug(f"End camera: {camera}.") + + logger.debug("End all cameras.") + logger.debug("End expire recordings.") + + return maybe_empty_dirs + + def run(self) -> None: + + if self.config.safe_mode: + logger.info("Safe mode enabled, skipping recording cleanup") + return + + # Expire tmp clips every minute, recordings and clean directories every hour. + for counter in itertools.cycle(range(self.config.record.expire_interval)): + if self.stop_event.wait(60): + logger.info("Exiting recording cleanup...") + break + + self.clean_tmp_previews() + + if counter == 0: + self.clean_tmp_clips() + maybe_empty_dirs = self.expire_recordings() + remove_empty_directories(Path(RECORD_DIR), maybe_empty_dirs) + self.truncate_wal() diff --git a/frigate/record/export.py b/frigate/record/export.py new file mode 100644 index 0000000..5d30707 --- /dev/null +++ b/frigate/record/export.py @@ -0,0 +1,1051 @@ +"""Export recordings to storage.""" + +import datetime +import logging +import os +import random +import re +import shutil +import string +import subprocess as sp +import threading +from collections.abc import Callable +from enum import Enum +from pathlib import Path + +import pytz # type: ignore[import-untyped] +from peewee import DoesNotExist + +from frigate.config import FfmpegConfig, FrigateConfig +from frigate.config.camera.record import ChaptersEnum +from frigate.const import ( + CACHE_DIR, + CLIPS_DIR, + EXPORT_DIR, + MAX_PLAYLIST_SECONDS, + PREVIEW_FRAME_TYPE, +) +from frigate.ffmpeg_presets import ( + EncodeTypeEnum, + parse_preset_hardware_acceleration_encode, +) +from frigate.models import Export, Previews, Recordings, ReviewSegment +from frigate.util.ffmpeg import run_ffmpeg_with_progress +from frigate.util.time import is_current_hour + +logger = logging.getLogger(__name__) + + +DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30" +TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey" + +# Matches the setpts factor used in timelapse exports (e.g. setpts=0.04*PTS). +# Captures the floating-point factor so we can scale expected duration. +SETPTS_FACTOR_RE = re.compile(r"setpts=([0-9]*\.?[0-9]+)\*PTS") + +# Allowlisted flags that take no value. +_VALUELESS_FLAGS = frozenset({"-an", "-sn", "-dn"}) + +# Allowlisted filter flags. Their value is validated as a filtergraph and may +# only reference filters in _SAFE_FILTERS. +_FILTER_FLAGS = frozenset({"-vf", "-af", "-filter"}) + +# Allowlisted flags that take exactly one value (encoder / muxer-safe options). +_VALUE_FLAGS = frozenset( + { + "-c", + "-codec", + "-b", + "-crf", + "-qp", + "-q", + "-qscale", + "-preset", + "-tune", + "-profile", + "-level", + "-pix_fmt", + "-r", + "-g", + "-keyint_min", + "-sc_threshold", + "-bf", + "-refs", + "-qmin", + "-qmax", + "-maxrate", + "-minrate", + "-bufsize", + "-movflags", + "-threads", + "-aspect", + "-fps_mode", + "-vsync", + "-skip_frame", + } +) + +_ALLOWED_FLAGS = _VALUELESS_FLAGS | _FILTER_FLAGS | _VALUE_FLAGS + +# Filters that cannot read files, load plugins, or open network sources. +_SAFE_FILTERS = frozenset( + { + "setpts", + "fps", + "scale", + "format", + "transpose", + "hflip", + "vflip", + "crop", + "pad", + "setsar", + "setdar", + } +) + +# Conservative shape for a non-filter flag value. Excludes "/" (paths / +# filtergraph division), whitespace, brackets, and a leading "-" so a value +# can never be a path or swallow a following flag. ":" is permitted for values +# like "16:9". +_SAFE_VALUE_RE = re.compile(r"^[A-Za-z0-9_.:+][A-Za-z0-9_.:+-]*$") + +# Substrings inside a filtergraph that indicate a file-reading filter option. +# "movie=" also matches "amovie=" as a substring. +_BLOCKED_FILTER_VALUE_MARKERS = ("movie=", "textfile=", "filename=", "fontfile=") + + +def _base_flag(token: str) -> str: + """Return a flag's base name, lowercased and without its stream specifier. + + e.g. "-c:v" -> "-c", "-filter:a:0" -> "-filter". + """ + return token.lower().split(":", 1)[0] + + +def _validate_filtergraph(value: str) -> tuple[bool, str]: + """Validate a filtergraph value, allowing only filters in _SAFE_FILTERS.""" + # None of the safe filters need any of these + if any(token in value for token in ("://", "..", "[", "]")): + return False, "Invalid filter graph in custom ffmpeg arguments" + + lowered = value.lower() + if any(marker in lowered for marker in _BLOCKED_FILTER_VALUE_MARKERS): + return False, "File-reading filters are not allowed in custom ffmpeg arguments" + + # Filters are separated by "," within a chain and ";" between chains. Safe + # filters never use unescaped "," or ";" in their arguments, so splitting on + # them to recover filter names cannot hide a disallowed filter. + for spec in re.split(r"[;,]", value): + spec = spec.strip() + if not spec: + continue + + name = spec.split("=", 1)[0].strip().lower() + if name not in _SAFE_FILTERS: + return False, f"Filter not allowed in custom ffmpeg arguments: {name}" + + return True, "" + + +def validate_ffmpeg_args(args: str) -> tuple[bool, str]: + """Validate user-provided custom export ffmpeg args with an allowlist. + + Every token must be an allowlisted flag or the value of one; filter values + may only reference safe filters; and no token may become a bare input or + output URL. This structurally prevents arbitrary file read/write, network + exfiltration/SSRF, and resource-exhaustion via the export endpoint. + + Admin users skip this validation entirely since they are trusted. + """ + if not args or not args.strip(): + return True, "" + + tokens = args.split() + i = 0 + while i < len(tokens): + token = tokens[i] + + # A bare (non-flag) token here would be parsed by ffmpeg as an input or + # output URL. Only the server sets inputs/outputs, never the user. + if not token.startswith("-"): + return False, f"Unexpected argument in custom ffmpeg arguments: {token}" + + base = _base_flag(token) + if base not in _ALLOWED_FLAGS: + return False, f"Forbidden ffmpeg argument: {token}" + + if base in _VALUELESS_FLAGS: + i += 1 + continue + + # Remaining flags consume exactly one value. + if i + 1 >= len(tokens): + return False, f"Missing value for ffmpeg argument: {token}" + + value = tokens[i + 1] + if base in _FILTER_FLAGS: + valid, message = _validate_filtergraph(value) + if not valid: + return False, message + elif not _SAFE_VALUE_RE.match(value): + return False, f"Invalid value for {token}: {value}" + + i += 2 + + return True, "" + + +class PlaybackSourceEnum(str, Enum): + recordings = "recordings" + preview = "preview" + + +class RecordingExporter(threading.Thread): + """Exports a specific set of recordings for a camera to storage as a single file.""" + + def __init__( + self, + config: FrigateConfig, + id: str, + camera: str, + name: str | None, + image: str | None, + start_time: int, + end_time: int, + playback_source: PlaybackSourceEnum, + export_case_id: str | None = None, + ffmpeg_input_args: str | None = None, + ffmpeg_output_args: str | None = None, + cpu_fallback: bool = False, + chapters: ChaptersEnum | None = None, + on_progress: Callable[[str, float], None] | None = None, + ) -> None: + super().__init__() + self.config = config + self.export_id = id + self.camera = camera + self.user_provided_name = name + self.user_provided_image = image + self.start_time = start_time + self.end_time = end_time + self.playback_source = playback_source + self.export_case_id = export_case_id + self.ffmpeg_input_args = ffmpeg_input_args + self.ffmpeg_output_args = ffmpeg_output_args + self.cpu_fallback = cpu_fallback + self.chapters = chapters + self.on_progress = on_progress + + # ensure export thumb dir + Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True) + + def _emit_progress(self, step: str, percent: float) -> None: + """Invoke the progress callback if one was supplied.""" + if self.on_progress is None: + return + try: + self.on_progress(step, max(0.0, min(100.0, percent))) + except Exception: + logger.exception("Export progress callback failed") + + def _expected_output_duration_seconds(self) -> float: + """Compute the expected duration of the output video in seconds. + + Users often request a wide time range (e.g. a full hour) when only + a few minutes of recordings actually live on disk for that span, + so the requested range overstates the work and progress would + plateau very early. We sum the actual saved seconds from the + Recordings/Previews tables and use that as the input duration. + Timelapse exports then scale this by the setpts factor. + """ + requested_duration = max(0.0, float(self.end_time - self.start_time)) + + recorded = self._sum_source_duration_seconds() + input_duration = ( + recorded if recorded is not None and recorded > 0 else requested_duration + ) + + if not self.ffmpeg_output_args: + return input_duration + + match = SETPTS_FACTOR_RE.search(self.ffmpeg_output_args) + if match is None: + return input_duration + + try: + factor = float(match.group(1)) + except ValueError: + return input_duration + + if factor <= 0: + return input_duration + + return input_duration * factor + + def _sum_source_duration_seconds(self) -> float | None: + """Sum saved-video seconds inside [start_time, end_time]. + + Queries Recordings or Previews depending on the playback source, + clamps each segment to the requested range, and returns the total. + Returns ``None`` on any error so the caller can fall back to the + requested range duration without losing progress reporting. + """ + try: + if self.playback_source == PlaybackSourceEnum.recordings: + rows = ( + Recordings.select(Recordings.start_time, Recordings.end_time) + .where( + Recordings.start_time.between(self.start_time, self.end_time) + | Recordings.end_time.between(self.start_time, self.end_time) + | ( + (self.start_time > Recordings.start_time) + & (self.end_time < Recordings.end_time) + ) + ) + .where(Recordings.camera == self.camera) + .iterator() + ) + else: + rows = ( + Previews.select(Previews.start_time, Previews.end_time) + .where( + Previews.start_time.between(self.start_time, self.end_time) + | Previews.end_time.between(self.start_time, self.end_time) + | ( + (self.start_time > Previews.start_time) + & (self.end_time < Previews.end_time) + ) + ) + .where(Previews.camera == self.camera) + .iterator() + ) + except Exception: + logger.exception( + "Failed to sum source duration for export %s", self.export_id + ) + return None + + total = 0.0 + try: + for row in rows: + clipped_start = max(float(row.start_time), float(self.start_time)) + clipped_end = min(float(row.end_time), float(self.end_time)) + if clipped_end > clipped_start: + total += clipped_end - clipped_start + except Exception: + logger.exception( + "Failed to read recording rows for export %s", self.export_id + ) + return None + + return total + + def _run_ffmpeg_with_progress( + self, + ffmpeg_cmd: list[str], + playlist_lines: str | list[str], + step: str = "encoding", + ) -> tuple[int, str]: + """Delegate to the shared helper, mapping percent → (step, percent). + + Returns ``(returncode, captured_stderr)``. + """ + if isinstance(playlist_lines, list): + stdin_payload = "\n".join(playlist_lines) + else: + stdin_payload = playlist_lines + + return run_ffmpeg_with_progress( + ffmpeg_cmd, + expected_duration_seconds=self._expected_output_duration_seconds(), + on_progress=lambda percent: self._emit_progress(step, percent), + stdin_payload=stdin_payload, + use_low_priority=True, + ) + + def get_datetime_from_timestamp(self, timestamp: int) -> str: + # return in iso format using the configured ui.timezone when set, + # so the auto-generated export name reflects local time rather + # than the container's UTC clock + tz_name = self.config.ui.timezone + if tz_name: + try: + tz = pytz.timezone(tz_name) + except pytz.UnknownTimeZoneError: + tz = None + if tz is not None: + return datetime.datetime.fromtimestamp(timestamp, tz=tz).strftime( + "%Y-%m-%d %H:%M:%S" + ) + return datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + + def _chapter_metadata_path(self) -> str: + return os.path.join(CACHE_DIR, f"export_chapters_{self.export_id}.txt") + + def _build_chapter_metadata_file(self, recordings: list) -> str | None: + """Write an FFmpeg metadata file with chapters for review items in range. + + Chapter offsets are computed in *output time*: the VOD endpoint + concatenates recording clips back-to-back, so wall-clock gaps + between recordings collapse in the produced video. We walk the + same recording rows that feed the playlist and convert each + review item's wall-clock boundaries into output-time offsets. + Returns ``None`` when there are no recordings, no review items, + or any chapter would have zero output duration. + """ + if not recordings: + return None + + windows: list[tuple[float, float, float]] = [] + output_offset = 0.0 + for rec in recordings: + clipped_start = max(float(rec.start_time), float(self.start_time)) + clipped_end = min(float(rec.end_time), float(self.end_time)) + if clipped_end <= clipped_start: + continue + windows.append((clipped_start, clipped_end, output_offset)) + output_offset += clipped_end - clipped_start + + if not windows: + return None + + try: + review_rows = list( + ReviewSegment.select( + ReviewSegment.start_time, + ReviewSegment.end_time, + ReviewSegment.severity, + ReviewSegment.data, + ) + .where( + ReviewSegment.start_time.between(self.start_time, self.end_time) + | ReviewSegment.end_time.between(self.start_time, self.end_time) + | ( + (self.start_time > ReviewSegment.start_time) + & (self.end_time < ReviewSegment.end_time) + ) + ) + .where(ReviewSegment.camera == self.camera) + .order_by(ReviewSegment.start_time.asc()) + .iterator() + ) + except Exception: + logger.exception( + "Failed to query review segments for export %s", self.export_id + ) + return None + + if not review_rows: + return None + + total_output = windows[-1][2] + (windows[-1][1] - windows[-1][0]) + last_recorded_end = windows[-1][1] + + def wall_to_output(t: float) -> float: + t = max(float(self.start_time), min(float(self.end_time), t)) + for w_start, w_end, w_offset in windows: + if t < w_start: + return w_offset + if t <= w_end: + return w_offset + (t - w_start) + return total_output + + chapter_blocks: list[str] = [] + for review in review_rows: + if review.start_time is None: + continue + # In-progress segments have a NULL end_time until the activity + # closes; clamp to the last recorded second so the chapter never + # extends past the actual video. + review_end = ( + float(review.end_time) + if review.end_time is not None + else last_recorded_end + ) + start_out = wall_to_output(float(review.start_time)) + end_out = wall_to_output(review_end) + + # Drop chapters that fall entirely in a recording gap, or are + # too short to be navigable in a player. + if end_out - start_out < 1.0: + continue + + data = review.data or {} + labels: list[str] = [] + for obj in data.get("objects") or []: + label = str(obj).split("-")[0] + if label and label not in labels: + labels.append(label) + + metadata = data.get("metadata") or {} + title = metadata.get("title") + + if not title: + title = str(review.severity).capitalize() + + if labels: + title = f"{title}: {', '.join(labels)}" + + chapter_blocks.append( + "[CHAPTER]\n" + "TIMEBASE=1/1000\n" + f"START={int(start_out * 1000)}\n" + f"END={int(end_out * 1000)}\n" + f"title={title}" + ) + + if not chapter_blocks: + return None + + meta_path = self._chapter_metadata_path() + try: + with open(meta_path, "w", encoding="utf-8") as f: + f.write(";FFMETADATA1\n") + f.write("\n".join(chapter_blocks)) + f.write("\n") + except OSError: + logger.exception( + "Failed to write chapter metadata file for export %s", self.export_id + ) + return None + + return meta_path + + def _build_recording_segment_chapter_metadata_file( + self, recordings: list + ) -> str | None: + """Write an FFmpeg metadata file with one chapter per recording segment. + + Each chapter's title is the segment's wallclock start time in + strict ISO 8601 form so a viewer can map any point in the + export's playback timeline back to real-world time without + OCR-ing a burnt-in timestamp. Chapter offsets are computed in + *output time*: the VOD endpoint concatenates recording clips + back-to-back, so wall-clock gaps between recordings collapse in + the produced video. Returns ``None`` when there are no + recordings or every segment is empty after clipping. + """ + if not recordings: + return None + + tz_name = self.config.ui.timezone + tz: datetime.tzinfo | None = None + if tz_name: + try: + tz = pytz.timezone(tz_name) + except pytz.UnknownTimeZoneError: + tz = None + if tz is None: + tz = datetime.UTC + + chapter_blocks: list[str] = [] + output_offset_ms = 0 + for rec in recordings: + clipped_start = max(float(rec.start_time), float(self.start_time)) + clipped_end = min(float(rec.end_time), float(self.end_time)) + if clipped_end <= clipped_start: + continue + + duration_ms = int(round((clipped_end - clipped_start) * 1000)) + if duration_ms <= 0: + continue + + title = datetime.datetime.fromtimestamp(clipped_start, tz=tz).isoformat( + timespec="seconds" + ) + chapter_blocks.append( + "[CHAPTER]\n" + "TIMEBASE=1/1000\n" + f"START={output_offset_ms}\n" + f"END={output_offset_ms + duration_ms}\n" + f"title={title}" + ) + output_offset_ms += duration_ms + + if not chapter_blocks: + return None + + meta_path = self._chapter_metadata_path() + try: + with open(meta_path, "w", encoding="utf-8") as f: + f.write(";FFMETADATA1\n") + f.write("\n".join(chapter_blocks)) + f.write("\n") + except OSError: + logger.exception( + "Failed to write chapter metadata file for export %s", self.export_id + ) + return None + + return meta_path + + def save_thumbnail(self, id: str) -> str: + thumb_path = os.path.join(CLIPS_DIR, f"export/{id}.webp") + + if self.user_provided_image is not None and os.path.isfile( + self.user_provided_image + ): + shutil.copy(self.user_provided_image, thumb_path) + return thumb_path + + if ( + self.start_time + < datetime.datetime.now(datetime.UTC) + .replace(minute=0, second=0, microsecond=0) + .timestamp() + ): + # has preview mp4 + try: + preview = ( + Previews.select( + Previews.camera, + Previews.path, + Previews.duration, + Previews.start_time, + Previews.end_time, + ) + .where( + Previews.start_time.between(self.start_time, self.end_time) + | Previews.end_time.between(self.start_time, self.end_time) + | ( + (self.start_time > Previews.start_time) + & (self.end_time < Previews.end_time) + ) + ) + .where(Previews.camera == self.camera) + .limit(1) + .get() + ) + except DoesNotExist: + return "" + + diff = max(0.0, float(self.start_time) - float(preview.start_time)) + ffmpeg_cmd = [ + "/usr/lib/ffmpeg/8.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support + "-hide_banner", + "-loglevel", + "warning", + "-ss", + f"{diff:.3f}", + "-i", + preview.path, + "-frames", + "1", + "-c:v", + "libwebp", + thumb_path, + ] + + process = sp.run( + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + return "" + + else: + # need to generate from existing images + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{self.camera}-" + start_file = f"{file_start}{self.start_time}.{PREVIEW_FRAME_TYPE}" + end_file = f"{file_start}{self.end_time}.{PREVIEW_FRAME_TYPE}" + selected_preview = None + # Preview frames are written at most 1-2 fps during activity + # and as little as one every 30s during quiet periods, so a + # short export window can contain zero frames. Track the most + # recent frame before the window as a fallback. + fallback_preview = None + + for file in sorted(os.listdir(preview_dir)): + if not file.startswith(file_start): + continue + + if file < start_file: + fallback_preview = os.path.join(preview_dir, file) + continue + + if file > end_file: + break + + selected_preview = os.path.join(preview_dir, file) + break + + if not selected_preview: + selected_preview = fallback_preview + + if not selected_preview: + return "" + + shutil.copyfile(selected_preview, thumb_path) + + return thumb_path + + def get_record_export_command( + self, video_path: str, use_hwaccel: bool = True + ) -> tuple[list[str], str | list[str]]: + # handle case where internal port is a string with ip:port + internal_port = self.config.networking.listen.internal + if type(internal_port) is str: + internal_port = int(internal_port.split(":")[-1]) + + recordings = list( + Recordings.select( + Recordings.start_time, + Recordings.end_time, + ) + .where( + Recordings.start_time.between(self.start_time, self.end_time) + | Recordings.end_time.between(self.start_time, self.end_time) + | ( + (self.start_time > Recordings.start_time) + & (self.end_time < Recordings.end_time) + ) + ) + .where(Recordings.camera == self.camera) + .order_by(Recordings.start_time.asc()) + .iterator() + ) + + playlist_lines: list[str] = [] + if (self.end_time - self.start_time) <= MAX_PLAYLIST_SECONDS: + playlist_url = f"http://127.0.0.1:{internal_port}/vod/{self.camera}/start/{self.start_time}/end/{self.end_time}/index.m3u8" + ffmpeg_input = ( + f"-y -protocol_whitelist pipe,file,http,tcp -i {playlist_url}" + ) + else: + # Chunk the recording rows into pages so each playlist line + # references a bounded sub-range rather than the full export. + page_size = 1000 + for i in range(0, len(recordings), page_size): + chunk = recordings[i : i + page_size] + playlist_lines.append( + f"file 'http://127.0.0.1:{internal_port}/vod/{self.camera}/start/{float(chunk[0].start_time)}/end/{float(chunk[-1].end_time)}/index.m3u8'" + ) + + ffmpeg_input = "-y -protocol_whitelist pipe,file,http,tcp -f concat -safe 0 -i /dev/stdin" + + if self.ffmpeg_input_args is not None and self.ffmpeg_output_args is not None: + hwaccel_args = ( + self.config.cameras[self.camera].record.export.hwaccel_args + if use_hwaccel + else None + ) + ffmpeg_cmd = ( + parse_preset_hardware_acceleration_encode( + self.config.ffmpeg.ffmpeg_path, + hwaccel_args, + f"{self.ffmpeg_input_args} -an {ffmpeg_input}".strip(), + f"{self.ffmpeg_output_args} -movflags +faststart".strip(), + EncodeTypeEnum.timelapse, + ) + ).split(" ") + else: + # Realtime/stream-copy export. Embed chapter metadata according to + # the camera's configured chapter mode: per-recording-segment + # timestamps or per-review-item titles. + if self.chapters == ChaptersEnum.recording_segments: + chapters_path = self._build_recording_segment_chapter_metadata_file( + recordings + ) + elif self.chapters == ChaptersEnum.review_items: + chapters_path = self._build_chapter_metadata_file(recordings) + else: + chapters_path = None + + chapter_args = ( + f" -i {chapters_path} -map 0 -dn -map_metadata 1" + if chapters_path + else "" + ) + ffmpeg_cmd = ( + f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input}{chapter_args} -c copy -movflags +faststart" + ).split(" ") + + # add metadata + title = f"Frigate Recording for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}" + creation_time = datetime.datetime.fromtimestamp( + self.start_time, tz=datetime.UTC + ).strftime("%Y-%m-%dT%H:%M:%S.%fZ") + ffmpeg_cmd.extend( + [ + "-metadata", + f"title={title}", + "-metadata", + f"creation_time={creation_time}", + "-metadata", + f"comment=Camera: {self.camera}", + ] + ) + + ffmpeg_cmd.append(video_path) + + return ffmpeg_cmd, playlist_lines + + def get_preview_export_command( + self, video_path: str, use_hwaccel: bool = True + ) -> tuple[list[str], list[str]]: + playlist_lines = [] + codec = "-c copy" + + if is_current_hour(self.start_time): + # get list of current preview frames + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{self.camera}-" + start_file = f"{file_start}{self.start_time}.{PREVIEW_FRAME_TYPE}" + end_file = f"{file_start}{self.end_time}.{PREVIEW_FRAME_TYPE}" + + for file in sorted(os.listdir(preview_dir)): + if not file.startswith(file_start): + continue + + if file < start_file: + continue + + if file > end_file: + break + + playlist_lines.append(f"file '{os.path.join(preview_dir, file)}'") + playlist_lines.append("duration 0.12") + + if playlist_lines: + last_file = playlist_lines[-2] + playlist_lines.append(last_file) + codec = "-c:v libx264" + + # get full set of previews + export_previews = ( + Previews.select( + Previews.path, + Previews.start_time, + Previews.end_time, + ) + .where( + Previews.start_time.between(self.start_time, self.end_time) + | Previews.end_time.between(self.start_time, self.end_time) + | ( + (self.start_time > Previews.start_time) + & (self.end_time < Previews.end_time) + ) + ) + .where(Previews.camera == self.camera) + .order_by(Previews.start_time.asc()) + .namedtuples() + .iterator() + ) + + for preview in export_previews: + playlist_lines.append(f"file '{preview.path}'") + + if preview.start_time < self.start_time: + playlist_lines.append( + f"inpoint {int(self.start_time - preview.start_time)}" + ) + + if preview.end_time > self.end_time: + playlist_lines.append( + f"outpoint {int(preview.end_time - self.end_time)}" + ) + + ffmpeg_input = ( + "-y -protocol_whitelist pipe,file,tcp -f concat -safe 0 -i /dev/stdin" + ) + + if self.ffmpeg_input_args is not None and self.ffmpeg_output_args is not None: + hwaccel_args = ( + self.config.cameras[self.camera].record.export.hwaccel_args + if use_hwaccel + else None + ) + ffmpeg_cmd = ( + parse_preset_hardware_acceleration_encode( + self.config.ffmpeg.ffmpeg_path, + hwaccel_args, + f"{self.ffmpeg_input_args} {TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}".strip(), + f"{self.ffmpeg_output_args} -movflags +faststart".strip(), + EncodeTypeEnum.timelapse, + ) + ).split(" ") + else: + ffmpeg_cmd = ( + f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart" + ).split(" ") + + # add metadata + title = f"Frigate Preview for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}" + creation_time = datetime.datetime.fromtimestamp( + self.start_time, tz=datetime.UTC + ).strftime("%Y-%m-%dT%H:%M:%S.%fZ") + ffmpeg_cmd.extend( + [ + "-metadata", + f"title={title}", + "-metadata", + f"creation_time={creation_time}", + "-metadata", + f"comment=Camera: {self.camera}", + ] + ) + + ffmpeg_cmd.append(video_path) + + return ffmpeg_cmd, playlist_lines + + def run(self) -> None: + logger.debug( + f"Beginning export for {self.camera} from {self.start_time} to {self.end_time}" + ) + self._emit_progress("preparing", 0.0) + export_name = ( + self.user_provided_name + or f"{self.camera.replace('_', ' ')} {self.get_datetime_from_timestamp(self.start_time)} {self.get_datetime_from_timestamp(self.end_time)}" + ) + filename_start_datetime = datetime.datetime.fromtimestamp( + self.start_time + ).strftime("%Y%m%d_%H%M%S") + filename_end_datetime = datetime.datetime.fromtimestamp(self.end_time).strftime( + "%Y%m%d_%H%M%S" + ) + cleaned_export_id = self.export_id.split("_")[-1] + video_path = f"{EXPORT_DIR}/{self.camera}_{filename_start_datetime}-{filename_end_datetime}_{cleaned_export_id}.mp4" + thumb_path = self.save_thumbnail(self.export_id) + + export_values = { + Export.id: self.export_id, + Export.camera: self.camera, + Export.name: export_name, + Export.date: self.start_time, + Export.video_path: video_path, + Export.thumb_path: thumb_path, + Export.in_progress: True, + } + + if self.export_case_id is not None: + export_values[Export.export_case] = self.export_case_id + + Export.insert(export_values).execute() + + try: + if self.playback_source == PlaybackSourceEnum.recordings: + ffmpeg_cmd, playlist_lines = self.get_record_export_command(video_path) + else: + ffmpeg_cmd, playlist_lines = self.get_preview_export_command(video_path) + except DoesNotExist: + return + + # When neither custom ffmpeg arg is set the default path uses + # `-c copy` (stream copy — no re-encoding). Report that as a + # distinct step so the UI doesn't mislabel a remux as encoding. + # The retry branch below always re-encodes because cpu_fallback + # requires custom args; it stays "encoding_retry". + is_stream_copy = ( + self.ffmpeg_input_args is None and self.ffmpeg_output_args is None + ) + initial_step = "copying" if is_stream_copy else "encoding" + + returncode, stderr = self._run_ffmpeg_with_progress( + ffmpeg_cmd, playlist_lines, step=initial_step + ) + + # If export failed and cpu_fallback is enabled, retry without hwaccel + if ( + returncode != 0 + and self.cpu_fallback + and self.ffmpeg_input_args is not None + and self.ffmpeg_output_args is not None + ): + logger.warning( + f"Export with hardware acceleration failed, retrying without hwaccel for {self.export_id}" + ) + + if self.playback_source == PlaybackSourceEnum.recordings: + ffmpeg_cmd, playlist_lines = self.get_record_export_command( + video_path, use_hwaccel=False + ) + else: + ffmpeg_cmd, playlist_lines = self.get_preview_export_command( + video_path, use_hwaccel=False + ) + + returncode, stderr = self._run_ffmpeg_with_progress( + ffmpeg_cmd, playlist_lines, step="encoding_retry" + ) + + Path(self._chapter_metadata_path()).unlink(missing_ok=True) + + if returncode != 0: + logger.error( + f"Failed to export {self.playback_source.value} for command {' '.join(ffmpeg_cmd)}" + ) + logger.error(stderr) + Path(video_path).unlink(missing_ok=True) + Export.delete().where(Export.id == self.export_id).execute() + Path(thumb_path).unlink(missing_ok=True) + return + else: + self._emit_progress("finalizing", 100.0) + Export.update({Export.in_progress: False}).where( + Export.id == self.export_id + ).execute() + + logger.debug(f"Finished exporting {video_path}") + + +def migrate_exports(ffmpeg: FfmpegConfig, camera_names: list[str]) -> None: + Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True) + + exports = [] + for export_file in os.listdir(EXPORT_DIR): + camera = "unknown" + + for cam_name in camera_names: + if cam_name in export_file: + camera = cam_name + break + + id = f"{camera}_{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}" + video_path = os.path.join(EXPORT_DIR, export_file) + thumb_path = os.path.join( + CLIPS_DIR, f"export/{id}.jpg" + ) # use jpg because webp encoder can't get quality low enough + + ffmpeg_cmd = [ + ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-i", + video_path, + "-vf", + "scale=-1:180", + "-frames", + "1", + "-q:v", + "8", + thumb_path, + ] + + process = sp.run( + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + continue + + exports.append( + { + Export.id: id, + Export.camera: camera, + Export.name: export_file.replace(".mp4", ""), + Export.date: os.path.getctime(video_path), + Export.video_path: video_path, + Export.thumb_path: thumb_path, + Export.in_progress: False, + } + ) + + Export.insert_many(exports).execute() diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py new file mode 100644 index 0000000..7f9dbc1 --- /dev/null +++ b/frigate/record/maintainer.py @@ -0,0 +1,799 @@ +"""Maintain recording segments in cache.""" + +import asyncio +import datetime +import logging +import os +import random +import string +import threading +import time +from collections import defaultdict +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path +from typing import Any + +import numpy as np +import psutil + +from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum +from frigate.comms.inter_process import InterProcessRequestor +from frigate.comms.recordings_updater import ( + RecordingsDataPublisher, + RecordingsDataTypeEnum, +) +from frigate.config import FrigateConfig, RetainModeEnum +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + CACHE_DIR, + CACHE_SEGMENT_FORMAT, + FAST_QUEUE_TIMEOUT, + INSERT_MANY_RECORDINGS, + MAX_SEGMENT_DURATION, + MAX_SEGMENTS_IN_CACHE, + RECORD_DIR, +) +from frigate.models import Recordings, ReviewSegment +from frigate.review.types import SeverityEnum +from frigate.util.services import get_video_properties + +logger = logging.getLogger(__name__) + +STALE_RECORDINGS_INFO_TTL = MAX_SEGMENTS_IN_CACHE * MAX_SEGMENT_DURATION * 2 + + +class SegmentInfo: + def __init__( + self, + motion_count: int, + active_object_count: int, + region_count: int, + average_dBFS: int, + motion_heatmap: dict[str, int] | None = None, + ) -> None: + self.motion_count = motion_count + self.active_object_count = active_object_count + self.region_count = region_count + self.average_dBFS = average_dBFS + self.motion_heatmap = motion_heatmap + + def should_discard_segment(self, retain_mode: RetainModeEnum) -> bool: + keep = False + + # all mode should never discard + if retain_mode == RetainModeEnum.all: + keep = True + + # motion mode should keep if motion or audio is detected + if ( + not keep + and retain_mode == RetainModeEnum.motion + and (self.motion_count > 0 or self.average_dBFS != 0) + ): + keep = True + + # active objects mode should keep if any active objects are detected + if not keep and self.active_object_count > 0: + keep = True + + return not keep + + +class RecordingMaintainer(threading.Thread): + def __init__(self, config: FrigateConfig, stop_event: MpEvent): + super().__init__(name="recording_maintainer") + self.config = config + + # create communication for retained recordings + self.requestor = InterProcessRequestor() + self.config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.record], + ) + self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all.value) + self.recordings_publisher = RecordingsDataPublisher() + + self.stop_event = stop_event + self.object_recordings_info: dict[str, list] = defaultdict(list) + self.audio_recordings_info: dict[str, list] = defaultdict(list) + self.end_time_cache: dict[str, tuple[datetime.datetime, float]] = {} + self.unexpected_cache_files_logged: bool = False + + async def move_files(self) -> None: + cache_files = [ + d + for d in os.listdir(CACHE_DIR) + if os.path.isfile(os.path.join(CACHE_DIR, d)) + and d.endswith(".mp4") + and not d.startswith("preview_") + ] + + # publish newest cached segment per camera (including in use files) + newest_cache_segments: dict[str, dict[str, Any]] = {} + for cache in cache_files: + cache_path = os.path.join(CACHE_DIR, cache) + basename = os.path.splitext(cache)[0] + try: + camera, date = basename.rsplit("@", maxsplit=1) + except ValueError: + if not self.unexpected_cache_files_logged: + logger.warning("Skipping unexpected files in cache") + self.unexpected_cache_files_logged = True + continue + + start_time = datetime.datetime.strptime( + date, CACHE_SEGMENT_FORMAT + ).astimezone(datetime.UTC) + if ( + camera not in newest_cache_segments + or start_time > newest_cache_segments[camera]["start_time"] + ): + newest_cache_segments[camera] = { + "start_time": start_time, + "cache_path": cache_path, + } + + for camera, newest in newest_cache_segments.items(): + self.recordings_publisher.publish( + ( + camera, + newest["start_time"].timestamp(), + newest["cache_path"], + ), + RecordingsDataTypeEnum.latest.value, + ) + # publish None for cameras with no cache files (but only if we know the camera exists) + for camera_name in self.config.cameras: + if camera_name not in newest_cache_segments: + self.recordings_publisher.publish( + (camera_name, None, None), + RecordingsDataTypeEnum.latest.value, + ) + + files_in_use = [] + for process in psutil.process_iter(): + try: + if process.name() != "ffmpeg": + continue + file_list = process.open_files() + if file_list: + for nt in file_list: + if nt.path.startswith(CACHE_DIR): + files_in_use.append(nt.path.split("/")[-1]) + except psutil.Error: + continue + + # group recordings by camera (skip in-use for validation/moving) + grouped_recordings: defaultdict[str, list[dict[str, Any]]] = defaultdict(list) + for cache in cache_files: + # Skip files currently in use + if cache in files_in_use: + continue + + cache_path = os.path.join(CACHE_DIR, cache) + basename = os.path.splitext(cache)[0] + try: + camera, date = basename.rsplit("@", maxsplit=1) + except ValueError: + if not self.unexpected_cache_files_logged: + logger.warning("Skipping unexpected files in cache") + self.unexpected_cache_files_logged = True + continue + + # important that start_time is utc because recordings are stored and compared in utc + start_time = datetime.datetime.strptime( + date, CACHE_SEGMENT_FORMAT + ).astimezone(datetime.UTC) + + grouped_recordings[camera].append( + { + "cache_path": cache_path, + "start_time": start_time, + } + ) + + # delete all cached files past the most recent MAX_SEGMENTS_IN_CACHE + keep_count = MAX_SEGMENTS_IN_CACHE + for camera in grouped_recordings.keys(): + # sort based on start time + grouped_recordings[camera] = sorted( + grouped_recordings[camera], key=lambda s: s["start_time"] + ) + + camera_info = self.object_recordings_info[camera] + most_recently_processed_frame_time = ( + camera_info[-1][0] if len(camera_info) > 0 else 0 + ) + + processed_segment_count = len( + list( + filter( + lambda r: ( + r["start_time"].timestamp() + < most_recently_processed_frame_time + ), + grouped_recordings[camera], + ) + ) + ) + + # see if the recording mover is too slow and segments need to be deleted + if processed_segment_count > keep_count: + logger.warning( + f"Unable to keep up with recording segments in cache for {camera}. Keeping the {keep_count} most recent segments out of {processed_segment_count} and discarding the rest..." + ) + to_remove = grouped_recordings[camera][:-keep_count] + for rec in to_remove: + cache_path = rec["cache_path"] + Path(cache_path).unlink(missing_ok=True) + self.end_time_cache.pop(cache_path, None) + grouped_recordings[camera] = grouped_recordings[camera][-keep_count:] + + # see if detection has failed and unprocessed segments need to be deleted + unprocessed_segment_count = ( + len(grouped_recordings[camera]) - processed_segment_count + ) + if unprocessed_segment_count > keep_count: + logger.warning( + f"Too many unprocessed recording segments in cache for {camera}. This likely indicates an issue with the detect stream, keeping the {keep_count} most recent segments out of {unprocessed_segment_count} and discarding the rest..." + ) + to_remove = grouped_recordings[camera][:-keep_count] + for rec in to_remove: + cache_path = rec["cache_path"] + Path(cache_path).unlink(missing_ok=True) + self.end_time_cache.pop(cache_path, None) + grouped_recordings[camera] = grouped_recordings[camera][-keep_count:] + + tasks = [] + for camera, recordings in grouped_recordings.items(): + # clear out all the object recording info for old frames + while ( + len(self.object_recordings_info[camera]) > 0 + and self.object_recordings_info[camera][0][0] + < recordings[0]["start_time"].timestamp() + ): + self.object_recordings_info[camera].pop(0) + + # clear out all the audio recording info for old frames + while ( + len(self.audio_recordings_info[camera]) > 0 + and self.audio_recordings_info[camera][0][0] + < recordings[0]["start_time"].timestamp() + ): + self.audio_recordings_info[camera].pop(0) + + # get all reviews with the end time after the start of the oldest cache file + # or with end_time None + reviews = ( + ReviewSegment.select( + ReviewSegment.start_time, + ReviewSegment.end_time, + ReviewSegment.severity, + ReviewSegment.data, + ) + .where( + ReviewSegment.camera == camera, + (ReviewSegment.end_time == None) + | ( + ReviewSegment.end_time + >= recordings[0]["start_time"].timestamp() + ), + ) + .order_by(ReviewSegment.start_time) + ) + + tasks.extend( + [self.validate_and_move_segment(camera, reviews, r) for r in recordings] + ) + + # publish most recently available recording time and None if disabled + camera_cfg = self.config.cameras.get(camera) + self.recordings_publisher.publish( + ( + camera, + recordings[0]["start_time"].timestamp() + if camera_cfg and camera_cfg.record.enabled + else None, + None, + ), + RecordingsDataTypeEnum.saved.value, + ) + + self._expire_stale_recordings_info(grouped_recordings) + + recordings_to_insert: list[dict[str, Any] | None] = await asyncio.gather(*tasks) + + # fire and forget recordings entries + self.requestor.send_data( + INSERT_MANY_RECORDINGS, + [r for r in recordings_to_insert if r is not None], + ) + + def _expire_stale_recordings_info( + self, grouped_recordings: defaultdict[str, list[dict[str, Any]]] + ) -> None: + expire_before = datetime.datetime.now().timestamp() - STALE_RECORDINGS_INFO_TTL + for recordings_info in ( + self.object_recordings_info, + self.audio_recordings_info, + ): + for camera in list(recordings_info.keys()): + if camera in grouped_recordings: + continue + info = recordings_info[camera] + while info and info[0][0] < expire_before: + info.pop(0) + + def drop_segment(self, cache_path: str) -> None: + Path(cache_path).unlink(missing_ok=True) + self.end_time_cache.pop(cache_path, None) + + async def validate_and_move_segment( + self, camera: str, reviews: Any, recording: dict[str, Any] + ) -> dict[str, Any] | None: + cache_path: str = recording["cache_path"] + start_time: datetime.datetime = recording["start_time"] + + # Just delete files if camera removed or recordings are turned off + if ( + camera not in self.config.cameras + or not self.config.cameras[camera].record.enabled + ): + self.drop_segment(cache_path) + return None + + if cache_path in self.end_time_cache: + end_time, duration = self.end_time_cache[cache_path] + else: + segment_info = await get_video_properties( + self.config.ffmpeg, cache_path, get_duration=True + ) + + if not segment_info.get("has_valid_video", False): + logger.warning( + f"Invalid or missing video stream in segment {cache_path}. Discarding." + ) + self.recordings_publisher.publish( + (camera, start_time.timestamp(), cache_path), + RecordingsDataTypeEnum.invalid.value, + ) + self.drop_segment(cache_path) + return None + + duration = float(segment_info.get("duration", -1)) + + # ensure duration is within expected length + if 0 < duration < MAX_SEGMENT_DURATION: + end_time = start_time + datetime.timedelta(seconds=duration) + self.end_time_cache[cache_path] = (end_time, duration) + else: + if duration == -1: + logger.warning(f"Failed to probe corrupt segment {cache_path}") + + logger.warning(f"Discarding a corrupt recording segment: {cache_path}") + self.recordings_publisher.publish( + (camera, start_time.timestamp(), cache_path), + RecordingsDataTypeEnum.invalid.value, + ) + self.drop_segment(cache_path) + return None + + # this segment has a valid duration and has video data, so publish an update + self.recordings_publisher.publish( + (camera, start_time.timestamp(), cache_path), + RecordingsDataTypeEnum.valid.value, + ) + + record_config = self.config.cameras[camera].record + segment_stats: SegmentInfo | None = None + highest = None + + if record_config.continuous.days > 0: + highest = "continuous" + elif record_config.motion.days > 0: + highest = "motion" + + # if we have continuous or motion recording enabled + # we should first just check if this segment matches that + # and avoid any DB calls + if highest is not None: + # assume that empty means the relevant recording info has not been received yet + camera_info = self.object_recordings_info[camera] + most_recently_processed_frame_time = ( + camera_info[-1][0] if len(camera_info) > 0 else 0 + ) + + # ensure delayed segment info does not lead to lost segments + if ( + datetime.datetime.fromtimestamp( + most_recently_processed_frame_time + ).astimezone(datetime.UTC) + >= end_time + ): + record_mode = ( + RetainModeEnum.all + if highest == "continuous" + else RetainModeEnum.motion + ) + segment_stats = self.segment_stats(camera, start_time, end_time) + + # Here we only check if we should move the segment based on non-object recording retention + # we will always want to check for overlapping review items below before dropping the segment + if not segment_stats.should_discard_segment(record_mode): + return await self.move_segment( + camera, + start_time, + end_time, + duration, + cache_path, + segment_stats, + ) + + # we fell through the continuous / motion check, so we need to check the review items + # if the cached segment overlaps with the review items: + overlaps = False + for review in reviews: + severity = SeverityEnum[review.severity] + + # if the review item starts in the future, stop checking review items + # and remove this segment + if ( + review.start_time - record_config.get_review_pre_capture(severity) + ) > end_time.timestamp(): + overlaps = False + break + + # if the review item is in progress or ends after the recording starts, keep it + # and stop looking at review items + if ( + review.end_time is None + or (review.end_time + record_config.get_review_post_capture(severity)) + >= start_time.timestamp() + ): + overlaps = True + break + + if overlaps: + record_mode = ( + record_config.alerts.retain.mode + if review.severity == "alert" + else record_config.detections.retain.mode + ) + + if segment_stats is None: + segment_stats = self.segment_stats(camera, start_time, end_time) + + if not segment_stats.should_discard_segment(record_mode): + # move from cache to recordings immediately + return await self.move_segment( + camera, + start_time, + end_time, + duration, + cache_path, + segment_stats, + ) + else: + self.drop_segment(cache_path) + return None + + # if it doesn't overlap with a review item, drop the segment once it + # ends more than event_pre_capture before the most recently processed + # frame. at this point we've already decided not to keep it for + # continuous/motion retention (either disabled or segment_stats said + # discard), so waiting longer just fills the cache. + else: + camera_info = self.object_recordings_info[camera] + most_recently_processed_frame_time = ( + camera_info[-1][0] if len(camera_info) > 0 else 0 + ) + retain_cutoff = datetime.datetime.fromtimestamp( + most_recently_processed_frame_time - record_config.event_pre_capture + ).astimezone(datetime.UTC) + + if end_time < retain_cutoff: + self.drop_segment(cache_path) + + return None + + def _compute_motion_heatmap( + self, camera: str, motion_boxes: list[tuple[int, int, int, int]] + ) -> dict[str, int] | None: + """Compute a 16x16 motion intensity heatmap from motion boxes. + + Returns a sparse dict mapping cell index (as string) to intensity (1-255). + Only cells with motion are included. + + Args: + camera: Camera name to get detect dimensions from. + motion_boxes: List of (x1, y1, x2, y2) pixel coordinates. + + Returns: + Sparse dict like {"45": 3, "46": 5}, or None if no boxes. + """ + if not motion_boxes: + return None + + camera_config = self.config.cameras.get(camera) + if not camera_config: + return None + + frame_width = camera_config.detect.width + frame_height = camera_config.detect.height + + if not frame_width or frame_width <= 0 or not frame_height or frame_height <= 0: + return None + + GRID_SIZE = 16 + counts: dict[int, int] = {} + + for box in motion_boxes: + if len(box) < 4: + continue + x1, y1, x2, y2 = box + + # Convert pixel coordinates to grid cells + grid_x1 = max(0, int((x1 / frame_width) * GRID_SIZE)) + grid_y1 = max(0, int((y1 / frame_height) * GRID_SIZE)) + grid_x2 = min(GRID_SIZE - 1, int((x2 / frame_width) * GRID_SIZE)) + grid_y2 = min(GRID_SIZE - 1, int((y2 / frame_height) * GRID_SIZE)) + + for y in range(grid_y1, grid_y2 + 1): + for x in range(grid_x1, grid_x2 + 1): + idx = y * GRID_SIZE + x + counts[idx] = min(255, counts.get(idx, 0) + 1) + + if not counts: + return None + + # Convert to string keys for JSON storage + return {str(k): v for k, v in counts.items()} + + def segment_stats( + self, camera: str, start_time: datetime.datetime, end_time: datetime.datetime + ) -> SegmentInfo: + video_frame_count = 0 + active_count = 0 + region_count = 0 + motion_count = 0 + all_motion_boxes: list[tuple[int, int, int, int]] = [] + + for frame in self.object_recordings_info[camera]: + # frame is after end time of segment + if frame[0] > end_time.timestamp(): + break + # frame is before start time of segment + if frame[0] < start_time.timestamp(): + continue + + video_frame_count += 1 + active_count += len( + [ + o + for o in frame[1] + if not o["false_positive"] and o["motionless_count"] == 0 + ] + ) + motion_count += len(frame[2]) + region_count += len(frame[3]) + # Collect motion boxes for heatmap computation + all_motion_boxes.extend(frame[2]) + + audio_values = [] + for frame in self.audio_recordings_info[camera]: + # frame is after end time of segment + if frame[0] > end_time.timestamp(): + break + + # frame is before start time of segment + if frame[0] < start_time.timestamp(): + continue + + # add active audio label count to count of active objects + active_count += len(frame[2]) + + # add sound level to audio values + audio_values.append(frame[1]) + + average_dBFS = 0 if not audio_values else np.average(audio_values) + + motion_heatmap = self._compute_motion_heatmap(camera, all_motion_boxes) + + return SegmentInfo( + motion_count, + active_count, + region_count, + round(average_dBFS), + motion_heatmap, + ) + + async def move_segment( + self, + camera: str, + start_time: datetime.datetime, + end_time: datetime.datetime, + duration: float, + cache_path: str, + segment_info: SegmentInfo, + ) -> dict[str, Any] | None: + # directory will be in utc due to start_time being in utc + directory = os.path.join( + RECORD_DIR, + start_time.strftime("%Y-%m-%d/%H"), + camera, + ) + + os.makedirs(directory, exist_ok=True) + + # file will be in utc due to start_time being in utc + file_name = f"{start_time.strftime('%M.%S.mp4')}" + file_path = os.path.join(directory, file_name) + + try: + if not os.path.exists(file_path): + start_frame = datetime.datetime.now().timestamp() + + # add faststart to kept segments to improve metadata reading + p = await asyncio.create_subprocess_exec( + self.config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-y", + "-i", + cache_path, + "-c", + "copy", + "-movflags", + "+faststart", + "-metadata", + f"creation_time={start_time.strftime('%Y-%m-%dT%H:%M:%S.%fZ')}", + file_path, + stderr=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.DEVNULL, + ) + await p.wait() + + if p.returncode != 0: + logger.error(f"Unable to convert {cache_path} to {file_path}") + if p.stderr: + logger.error((await p.stderr.read()).decode("ascii")) + return None + else: + logger.debug( + f"Copied {file_path} in {datetime.datetime.now().timestamp() - start_frame} seconds." + ) + + try: + # get the segment size of the cache file + # file without faststart is same size + segment_size = round( + float(os.path.getsize(cache_path)) / pow(2, 20), 2 + ) + except OSError: + segment_size = 0 + + os.remove(cache_path) + + rand_id = "".join( + random.choices(string.ascii_lowercase + string.digits, k=6) + ) + + return { + Recordings.id.name: f"{start_time.timestamp()}-{rand_id}", + Recordings.camera.name: camera, + Recordings.path.name: file_path, + Recordings.start_time.name: start_time.timestamp(), + Recordings.end_time.name: end_time.timestamp(), + Recordings.duration.name: duration, + Recordings.motion.name: segment_info.motion_count, + # TODO: update this to store list of active objects at some point + Recordings.objects.name: segment_info.active_object_count, + Recordings.regions.name: segment_info.region_count, + Recordings.dBFS.name: segment_info.average_dBFS, + Recordings.segment_size.name: segment_size, + Recordings.motion_heatmap.name: segment_info.motion_heatmap, + } + except Exception as e: + logger.error(f"Unable to store recording segment {cache_path}") + Path(cache_path).unlink(missing_ok=True) + logger.error(e) + + # clear end_time cache + self.end_time_cache.pop(cache_path, None) + return None + + def run(self) -> None: + # Check for new files every 5 seconds + wait_time = 0.0 + while not self.stop_event.is_set(): + time.sleep(wait_time) + + if self.stop_event.is_set(): + break + + run_start = datetime.datetime.now().timestamp() + + # check if there is an updated config + self.config_subscriber.check_for_updates() + + stale_frame_count = 0 + stale_frame_count_threshold = 10 + # empty the object recordings info queue + while True: + result = self.detection_subscriber.check_for_update( + timeout=FAST_QUEUE_TIMEOUT + ) + + if not result: + break + + topic, data = result + + if not topic or not data: + break + + if topic == DetectionTypeEnum.video.value: + ( + camera, + _, + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) = data + + if self.config.cameras[camera].record.enabled: + self.object_recordings_info[camera].append( + ( + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) + ) + elif topic == DetectionTypeEnum.audio.value: + ( + camera, + frame_time, + dBFS, + audio_detections, + ) = data + + if self.config.cameras[camera].record.enabled: + self.audio_recordings_info[camera].append( + ( + frame_time, + dBFS, + audio_detections, + ) + ) + elif ( + topic == DetectionTypeEnum.api.value + or topic == DetectionTypeEnum.lpr.value + ): + continue + + if frame_time < run_start - stale_frame_count_threshold: + stale_frame_count += 1 + + if stale_frame_count > 0: + logger.debug(f"Found {stale_frame_count} old frames.") + + try: + asyncio.run(self.move_files()) + except Exception as e: + logger.error( + "Error occurred when attempting to maintain recording cache" + ) + logger.error(e) + duration = datetime.datetime.now().timestamp() - run_start + wait_time = max(0, 5 - duration) + + self.requestor.stop() + self.config_subscriber.stop() + self.detection_subscriber.stop() + self.recordings_publisher.stop() + logger.info("Exiting recording maintenance...") diff --git a/frigate/record/record.py b/frigate/record/record.py new file mode 100644 index 0000000..624ed6e --- /dev/null +++ b/frigate/record/record.py @@ -0,0 +1,47 @@ +"""Run recording maintainer and cleanup.""" + +import logging +from multiprocessing.synchronize import Event as MpEvent + +from playhouse.sqliteq import SqliteQueueDatabase + +from frigate.config import FrigateConfig +from frigate.const import PROCESS_PRIORITY_HIGH +from frigate.models import Recordings, ReviewSegment +from frigate.record.maintainer import RecordingMaintainer +from frigate.util.process import FrigateProcess + +logger = logging.getLogger(__name__) + + +class RecordProcess(FrigateProcess): + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + super().__init__( + stop_event, + PROCESS_PRIORITY_HIGH, + name="frigate.recording_manager", + daemon=True, + ) + self.config = config + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + db = SqliteQueueDatabase( + self.config.database.path, + pragmas={ + "auto_vacuum": "FULL", # Does not defragment database + "cache_size": -512 * 1000, # 512MB of cache + "synchronous": "NORMAL", # Safe when using WAL https://www.sqlite.org/pragma.html#pragma_synchronous + }, + timeout=max( + 60, 10 * len([c for c in self.config.cameras.values() if c.enabled]) + ), + ) + models = [ReviewSegment, Recordings] + db.bind(models) + + maintainer = RecordingMaintainer( + self.config, + self.stop_event, + ) + maintainer.start() diff --git a/frigate/review/__init__.py b/frigate/review/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py new file mode 100644 index 0000000..f3abc1b --- /dev/null +++ b/frigate/review/maintainer.py @@ -0,0 +1,912 @@ +"""Maintain review segments in db.""" + +import copy +import datetime +import json +import logging +import os +import random +import string +import sys +import threading +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path +from typing import Any + +import cv2 +import numpy as np + +from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum +from frigate.comms.inter_process import InterProcessRequestor +from frigate.comms.review_updater import ReviewDataPublisher +from frigate.config import CameraConfig, FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + CLEAR_ONGOING_REVIEW_SEGMENTS, + CLIPS_DIR, + UPSERT_REVIEW_SEGMENT, +) +from frigate.models import ReviewSegment +from frigate.review.types import SeverityEnum +from frigate.track.object_processing import ManualEventState +from frigate.util.image import SharedMemoryFrameManager, calculate_16_9_crop + +logger = logging.getLogger(__name__) + + +THUMB_HEIGHT = 180 +THUMB_WIDTH = 320 + + +class PendingReviewSegment: + def __init__( + self, + camera: str, + frame_time: float, + severity: SeverityEnum, + detections: dict[str, str], + sub_labels: dict[str, str], + zones: list[str], + audio: set[str], + ): + rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + self.id = f"{frame_time}-{rand_id}" + self.camera = camera + self.start_time = frame_time + self.severity = severity + self.detections = detections + self.sub_labels = sub_labels + self.zones = zones + self.audio = audio + self.thumb_time: float | None = None + self.last_alert_time: float | None = None + self.last_detection_time: float = frame_time + + if severity == SeverityEnum.alert: + self.last_alert_time = frame_time + + # thumbnail + self._frame: np.ndarray[Any, Any] = np.zeros( + (THUMB_HEIGHT * 3 // 2, THUMB_WIDTH), np.uint8 + ) + self.has_frame = False + self.frame_active_count = 0 + self.frame_path = os.path.join( + CLIPS_DIR, f"review/thumb-{self.camera}-{self.id}.webp" + ) + + def update_frame( + self, + camera_config: CameraConfig, + frame: np.ndarray, + objects: list[dict[str, Any]], + ) -> None: + min_x = camera_config.frame_shape[1] + min_y = camera_config.frame_shape[0] + max_x = 0 + max_y = 0 + + # find bounds for all boxes + for o in objects: + min_x = min(o["box"][0], min_x) + min_y = min(o["box"][1], min_y) + max_x = max(o["box"][2], max_x) + max_y = max(o["box"][3], max_y) + + region = calculate_16_9_crop( + camera_config.frame_shape, min_x, min_y, max_x, max_y + ) + + # could not find suitable 16:9 region + if not region: + return + + self.frame_active_count = len(objects) + color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + color_frame = color_frame[region[1] : region[3], region[0] : region[2]] + width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0]) + self._frame = cv2.resize( + color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA + ) + + if self._frame is not None: + self.thumb_time = datetime.datetime.now().timestamp() + self.has_frame = True + cv2.imwrite( + self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] + ) + + def save_full_frame(self, camera_config: CameraConfig, frame: np.ndarray) -> None: + color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0]) + self._frame = cv2.resize( + color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA + ) + + if self._frame is not None: + self.has_frame = True + cv2.imwrite( + self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] + ) + + def get_data(self, ended: bool) -> dict: + end_time = None + + if ended: + if self.severity == SeverityEnum.alert: + end_time = self.last_alert_time + else: + end_time = self.last_detection_time + + return copy.deepcopy( + { + ReviewSegment.id.name: self.id, + ReviewSegment.camera.name: self.camera, + ReviewSegment.start_time.name: self.start_time, + ReviewSegment.end_time.name: end_time, + ReviewSegment.severity.name: self.severity.value, + ReviewSegment.thumb_path.name: self.frame_path, + ReviewSegment.data.name: { + "detections": list(set(self.detections.keys())), + "objects": list(set(self.detections.values())), + "verified_objects": [ + o for o in self.detections.values() if "-verified" in o + ], + "sub_labels": list(self.sub_labels.values()), + "zones": self.zones, + "audio": list(self.audio), + "thumb_time": self.thumb_time, + "metadata": None, + }, + } + ) + + +class ActiveObjects: + def __init__( + self, + frame_time: float, + camera_config: CameraConfig, + all_objects: list[dict[str, Any]], + ): + self.camera_config = camera_config + + # get current categorization of objects to know if + # these objects are currently being categorized + self.categorized_objects: dict[str, list[dict[str, Any]]] = { + "alerts": [], + "detections": [], + } + + for o in all_objects: + if ( + o["motionless_count"] >= camera_config.detect.stationary.threshold + and not o["pending_loitering"] + ): + # no stationary objects unless loitering + continue + + if o["position_changes"] == 0: + # object must have moved at least once + continue + + if o["frame_time"] != frame_time: + # object must be detected in this frame + continue + + if o["false_positive"]: + # object must not be a false positive + continue + + if ( + o["label"] in camera_config.review.alerts.labels + and ( + not camera_config.review.alerts.required_zones + or ( + len(o["current_zones"]) > 0 + and set(o["current_zones"]) + & set(camera_config.review.alerts.required_zones) + ) + ) + and camera_config.review.alerts.enabled + ): + self.categorized_objects["alerts"].append(o) + continue + + if ( + ( + camera_config.review.detections.labels is None + or o["label"] in camera_config.review.detections.labels + ) + and ( + not camera_config.review.detections.required_zones + or ( + len(o["current_zones"]) > 0 + and set(o["current_zones"]) + & set(camera_config.review.detections.required_zones) + ) + ) + and camera_config.review.detections.enabled + ): + self.categorized_objects["detections"].append(o) + continue + + def has_active_objects(self) -> bool: + return ( + len(self.categorized_objects["alerts"]) > 0 + or len(self.categorized_objects["detections"]) > 0 + ) + + def has_activity_category(self, severity: SeverityEnum) -> bool: + if ( + severity == SeverityEnum.alert + and len(self.categorized_objects["alerts"]) > 0 + ): + return True + + if ( + severity == SeverityEnum.detection + and len(self.categorized_objects["detections"]) > 0 + ): + return True + + return False + + def get_all_objects(self) -> list[dict[str, Any]]: + return ( + self.categorized_objects["alerts"] + self.categorized_objects["detections"] + ) + + +class ReviewSegmentMaintainer(threading.Thread): + """Maintain review segments.""" + + def __init__(self, config: FrigateConfig, stop_event: MpEvent): + super().__init__(name="review_segment_maintainer") + self.config = config + self.active_review_segments: dict[str, PendingReviewSegment | None] = {} + self.frame_manager = SharedMemoryFrameManager() + + # create communication for review segments + self.requestor = InterProcessRequestor() + self.config_subscriber = CameraConfigUpdateSubscriber( + config, + config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.record, + CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.review, + ], + ) + self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all.value) + self.review_publisher = ReviewDataPublisher("") + + # manual events + self.indefinite_events: dict[str, dict[str, Any]] = {} + + # ensure dirs + Path(os.path.join(CLIPS_DIR, "review")).mkdir(exist_ok=True) + + self.stop_event = stop_event + + # clear ongoing review segments from last instance + self.requestor.send_data(CLEAR_ONGOING_REVIEW_SEGMENTS, "") + + def _publish_segment_start( + self, + segment: PendingReviewSegment, + ) -> None: + """New segment.""" + new_data = segment.get_data(ended=False) + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data) + start_data = {k: v for k, v in new_data.items()} + review_update = { + "type": "new", + "before": start_data, + "after": start_data, + } + self.requestor.send_data( + "reviews", + json.dumps(review_update), + ) + self.review_publisher.publish(review_update, segment.camera) # type: ignore[arg-type] + self.requestor.send_data( + f"{segment.camera}/review_status", segment.severity.value.upper() + ) + + def _publish_segment_update( + self, + segment: PendingReviewSegment, + camera_config: CameraConfig, + frame: np.ndarray | None, + objects: list[dict[str, Any]], + prev_data: dict[str, Any], + ) -> None: + """Update segment.""" + if frame is not None: + segment.update_frame(camera_config, frame, objects) + + new_data = segment.get_data(ended=False) + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data) + review_update = { + "type": "update", + "before": {k: v for k, v in prev_data.items()}, + "after": {k: v for k, v in new_data.items()}, + } + self.requestor.send_data( + "reviews", + json.dumps(review_update), + ) + self.review_publisher.publish(review_update, segment.camera) # type: ignore[arg-type] + self.requestor.send_data( + f"{segment.camera}/review_status", segment.severity.value.upper() + ) + + def _publish_segment_end( + self, + segment: PendingReviewSegment, + prev_data: dict[str, Any], + ) -> Any: + """End segment.""" + final_data = segment.get_data(ended=True) + end_time = final_data[ReviewSegment.end_time.name] + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, final_data) + review_update = { + "type": "end", + "before": {k: v for k, v in prev_data.items()}, + "after": {k: v for k, v in final_data.items()}, + } + self.requestor.send_data( + "reviews", + json.dumps(review_update), + ) + self.review_publisher.publish(review_update, segment.camera) # type: ignore[arg-type] + self.requestor.send_data(f"{segment.camera}/review_status", "NONE") + self.active_review_segments[segment.camera] = None + return end_time + + def forcibly_end_segment(self, camera: str) -> Any: + """Forcibly end the pending segment for a camera.""" + segment = self.active_review_segments.get(camera) + if segment: + prev_data = segment.get_data(False) + return self._publish_segment_end(segment, prev_data) + return None + + def update_existing_segment( + self, + segment: PendingReviewSegment, + frame_name: str, + frame_time: float, + objects: list[dict[str, Any]], + ) -> None: + """Validate if existing review segment should continue.""" + camera_config = self.config.cameras[segment.camera] + + # get active objects + objects loitering in loitering zones + activity = ActiveObjects(frame_time, camera_config, objects) + prev_data = segment.get_data(False) + has_activity = False + + if activity.has_active_objects(): + has_activity = True + should_update_image = False + should_update_state = False + + if activity.has_activity_category(SeverityEnum.alert): + # update current time for last alert activity + if ( + segment.last_alert_time is None + or frame_time > segment.last_alert_time + ): + segment.last_alert_time = frame_time + + if segment.severity != SeverityEnum.alert: + # if segment is not alert category but current activity is + # update this segment to be an alert + segment.severity = SeverityEnum.alert + should_update_state = True + should_update_image = True + + if activity.has_activity_category(SeverityEnum.detection): + if ( + segment.last_detection_time is None + or frame_time > segment.last_detection_time + ): + segment.last_detection_time = frame_time + + for object in activity.get_all_objects(): + # Alert-level objects should always be added (they extend/upgrade the segment) + # Detection-level objects should only be added if: + # - The segment is a detection segment (matching severity), OR + # - The segment is an alert AND the object started before the alert ended + # (objects starting after will be in the new detection segment) + is_alert_object = object in activity.categorized_objects["alerts"] + + if not is_alert_object and segment.severity == SeverityEnum.alert: + # This is a detection-level object + # Only add if it started during the alert's active period + if object["start_time"] > segment.last_alert_time: + continue + + if not object["sub_label"]: + segment.detections[object["id"]] = object["label"] + elif object["sub_label"][0] in self.config.model.all_attributes: + segment.detections[object["id"]] = object["sub_label"][0] + else: + segment.detections[object["id"]] = f"{object['label']}-verified" + segment.sub_labels[object["id"]] = object["sub_label"][0] + + # keep zones up to date + if len(object["current_zones"]) > 0: + for zone in object["current_zones"]: + if zone not in segment.zones: + segment.zones.append(zone) + + if len(activity.get_all_objects()) > segment.frame_active_count: + should_update_state = True + should_update_image = True + + if prev_data["data"]["sub_labels"] != list(segment.sub_labels.values()): + should_update_state = True + + if should_update_state: + try: + if should_update_image: + yuv_frame = self.frame_manager.get( + frame_name, camera_config.frame_shape_yuv + ) + + if yuv_frame is None: + logger.debug(f"Failed to get frame {frame_name} from SHM") + return + else: + yuv_frame = None + + self._publish_segment_update( + segment, + camera_config, + yuv_frame, + activity.get_all_objects(), + prev_data, + ) + self.frame_manager.close(frame_name) + except FileNotFoundError: + return + + if not has_activity: + if not segment.has_frame: + try: + yuv_frame = self.frame_manager.get( + frame_name, camera_config.frame_shape_yuv + ) + + if yuv_frame is None: + logger.debug(f"Failed to get frame {frame_name} from SHM") + return + + segment.save_full_frame(camera_config, yuv_frame) + self.frame_manager.close(frame_name) + self._publish_segment_update( + segment, camera_config, None, [], prev_data + ) + except FileNotFoundError: + return + + if ( + segment.severity == SeverityEnum.alert + and segment.last_alert_time is not None + and frame_time + > (segment.last_alert_time + camera_config.review.alerts.cutoff_time) + ): + needs_new_detection = ( + segment.last_detection_time > segment.last_alert_time + and ( + segment.last_detection_time + + camera_config.review.detections.cutoff_time + ) + > frame_time + ) + last_detection_time = segment.last_detection_time + + end_time = self._publish_segment_end(segment, prev_data) + + if needs_new_detection: + new_detections: dict[str, str] = {} + new_zones = set() + + for o in activity.categorized_objects["detections"]: + new_detections[o["id"]] = o["label"] + new_zones.update(o["current_zones"]) + + if new_detections: + new_segment = PendingReviewSegment( + segment.camera, + end_time, + SeverityEnum.detection, + new_detections, + sub_labels={}, + audio=set(), + zones=list(new_zones), + ) + self.active_review_segments[segment.camera] = new_segment + self._publish_segment_start(new_segment) + new_segment.last_detection_time = last_detection_time + elif segment.severity == SeverityEnum.detection and frame_time > ( + segment.last_detection_time + + camera_config.review.detections.cutoff_time + ): + self._publish_segment_end(segment, prev_data) + + def check_if_new_segment( + self, + camera: str, + frame_name: str, + frame_time: float, + objects: list[dict[str, Any]], + ) -> None: + """Check if a new review segment should be created.""" + camera_config = self.config.cameras[camera] + activity = ActiveObjects(frame_time, camera_config, objects) + + if activity.has_active_objects(): + detections: dict[str, str] = {} + sub_labels: dict[str, str] = {} + zones: list[str] = [] + severity: SeverityEnum | None = None + + # if activity is alert category mark this review as alert + if severity != SeverityEnum.alert and activity.has_activity_category( + SeverityEnum.alert + ): + severity = SeverityEnum.alert + + # if object is detection label and not already higher severity + # mark this review as detection + if not severity and activity.has_activity_category(SeverityEnum.detection): + severity = SeverityEnum.detection + + for object in activity.get_all_objects(): + if not object["sub_label"]: + detections[object["id"]] = object["label"] + elif object["sub_label"][0] in self.config.model.all_attributes: + detections[object["id"]] = object["sub_label"][0] + else: + detections[object["id"]] = f"{object['label']}-verified" + sub_labels[object["id"]] = object["sub_label"][0] + + for zone in object["current_zones"]: + if zone not in zones: + zones.append(zone) + + if severity: + new_segment = PendingReviewSegment( + camera, + frame_time, + severity, + detections, + sub_labels=sub_labels, + audio=set(), + zones=zones, + ) + self.active_review_segments[camera] = new_segment + + try: + yuv_frame = self.frame_manager.get( + frame_name, camera_config.frame_shape_yuv + ) + + if yuv_frame is None: + logger.debug(f"Failed to get frame {frame_name} from SHM") + return + + new_segment.update_frame( + camera_config, yuv_frame, activity.get_all_objects() + ) + self.frame_manager.close(frame_name) + self._publish_segment_start(new_segment) + except FileNotFoundError: + return + + def run(self) -> None: + while not self.stop_event.is_set(): + # check if there is an updated config + updated_topics = self.config_subscriber.check_for_updates() + + if "record" in updated_topics: + for camera in updated_topics["record"]: + self.forcibly_end_segment(camera) + + if "enabled" in updated_topics: + for camera in updated_topics["enabled"]: + self.forcibly_end_segment(camera) + + result = self.detection_subscriber.check_for_update(timeout=1) + + if not result: + continue + + topic, data = result + + if not topic or not data: + continue + + if topic == DetectionTypeEnum.video.value: + ( + camera, + frame_name, + frame_time, + current_tracked_objects, + _, + _, + ) = data + elif topic == DetectionTypeEnum.audio.value: + ( + camera, + frame_time, + _, + audio_detections, + ) = data + elif ( + topic == DetectionTypeEnum.api.value + or topic == DetectionTypeEnum.lpr.value + ): + ( + camera, + frame_time, + manual_info, + ) = data + + if camera not in self.indefinite_events: + self.indefinite_events[camera] = {} + + if camera not in self.config.cameras: + continue + + if ( + not self.config.cameras[camera].enabled + or not self.config.cameras[camera].record.enabled + ): + continue + + current_segment = self.active_review_segments.get(camera) + + # Check if the current segment should be processed based on enabled settings + if current_segment: + if ( + current_segment.severity == SeverityEnum.alert + and not self.config.cameras[camera].review.alerts.enabled + ) or ( + current_segment.severity == SeverityEnum.detection + and not self.config.cameras[camera].review.detections.enabled + ): + self.forcibly_end_segment(camera) + continue + + # If we reach here, the segment can be processed (if it exists) + if current_segment is not None: + if topic == DetectionTypeEnum.video: + self.update_existing_segment( + current_segment, + frame_name, + frame_time, + current_tracked_objects, + ) + elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0: + camera_config = self.config.cameras[camera] + + for audio in audio_detections: + if ( + audio in camera_config.review.alerts.labels + and camera_config.review.alerts.enabled + ): + current_segment.audio.add(audio) + current_segment.severity = SeverityEnum.alert + current_segment.last_alert_time = frame_time + elif ( + camera_config.review.detections.labels is None + or audio in camera_config.review.detections.labels + ) and camera_config.review.detections.enabled: + current_segment.audio.add(audio) + current_segment.last_detection_time = frame_time + elif topic == DetectionTypeEnum.api or topic == DetectionTypeEnum.lpr: + if manual_info["state"] == ManualEventState.complete: + current_segment.detections[manual_info["event_id"]] = ( + manual_info["label"] + ) + if topic == DetectionTypeEnum.api: + # manual_info["label"] contains 'label: sub_label' + # so split out the label without modifying manual_info + det_labels = self.config.cameras[ + camera + ].review.detections.labels + if ( + self.config.cameras[camera].review.detections.enabled + and det_labels is not None + and manual_info["label"].split(": ")[0] in det_labels + ): + current_segment.last_detection_time = manual_info[ + "end_time" + ] + elif self.config.cameras[camera].review.alerts.enabled: + current_segment.severity = SeverityEnum.alert + current_segment.last_alert_time = manual_info[ + "end_time" + ] + elif ( + topic == DetectionTypeEnum.lpr + and self.config.cameras[camera].review.detections.enabled + ): + current_segment.severity = SeverityEnum.detection + current_segment.last_alert_time = manual_info["end_time"] + elif manual_info["state"] == ManualEventState.start: + self.indefinite_events[camera][manual_info["event_id"]] = ( + manual_info["label"] + ) + current_segment.detections[manual_info["event_id"]] = ( + manual_info["label"] + ) + if ( + topic == DetectionTypeEnum.api + and self.config.cameras[camera].review.alerts.enabled + ): + # manual_info["label"] contains 'label: sub_label' + # so split out the label without modifying manual_info + det_labels = self.config.cameras[ + camera + ].review.detections.labels + if ( + not self.config.cameras[ + camera + ].review.detections.enabled + or det_labels is None + or manual_info["label"].split(": ")[0] not in det_labels + ): + current_segment.severity = SeverityEnum.alert + elif ( + topic == DetectionTypeEnum.lpr + and self.config.cameras[camera].review.detections.enabled + ): + current_segment.severity = SeverityEnum.detection + + # temporarily make it so this event can not end + current_segment.last_alert_time = sys.maxsize + current_segment.last_detection_time = sys.maxsize + elif manual_info["state"] == ManualEventState.end: + event_id = manual_info["event_id"] + + if event_id in self.indefinite_events[camera]: + self.indefinite_events[camera].pop(event_id) + + if len(self.indefinite_events[camera]) == 0: + current_segment.last_alert_time = manual_info[ + "end_time" + ] + current_segment.last_detection_time = manual_info[ + "end_time" + ] + else: + logger.error( + f"Event with ID {event_id} has a set duration and can not be ended manually." + ) + else: + if topic == DetectionTypeEnum.video: + if ( + self.config.cameras[camera].review.alerts.enabled + or self.config.cameras[camera].review.detections.enabled + ): + self.check_if_new_segment( + camera, + frame_name, + frame_time, + current_tracked_objects, + ) + elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0: + severity = None + + camera_config = self.config.cameras[camera] + detections = set() + + for audio in audio_detections: + if ( + audio in camera_config.review.alerts.labels + and camera_config.review.alerts.enabled + ): + detections.add(audio) + severity = SeverityEnum.alert + elif ( + camera_config.review.detections.labels is None + or audio in camera_config.review.detections.labels + ) and camera_config.review.detections.enabled: + detections.add(audio) + + if not severity: + severity = SeverityEnum.detection + + if severity: + self.active_review_segments[camera] = PendingReviewSegment( + camera, + frame_time, + severity, + {}, + {}, + [], + detections, + ) + elif topic == DetectionTypeEnum.api: + severity = None + # manual_info["label"] contains 'label: sub_label' + # so split out the label without modifying manual_info + det_labels = self.config.cameras[camera].review.detections.labels + if ( + self.config.cameras[camera].review.detections.enabled + and det_labels is not None + and manual_info["label"].split(": ")[0] in det_labels + ): + severity = SeverityEnum.detection + elif self.config.cameras[camera].review.alerts.enabled: + severity = SeverityEnum.alert + + if severity: + api_segment = PendingReviewSegment( + camera, + frame_time, + severity, + {manual_info["event_id"]: manual_info["label"]}, + {}, + [], + set(), + ) + self.active_review_segments[camera] = api_segment + + if manual_info["state"] == ManualEventState.start: + self.indefinite_events[camera][manual_info["event_id"]] = ( + manual_info["label"] + ) + # temporarily make it so this event can not end + api_segment.last_alert_time = sys.maxsize + api_segment.last_detection_time = sys.maxsize + elif manual_info["state"] == ManualEventState.complete: + api_segment.last_alert_time = manual_info["end_time"] + api_segment.last_detection_time = manual_info["end_time"] + else: + logger.warning( + f"Manual event API has been called for {camera}, but alerts and detections are disabled. This manual event will not appear as an alert or detection." + ) + elif topic == DetectionTypeEnum.lpr: + if self.config.cameras[camera].review.detections.enabled: + lpr_segment = PendingReviewSegment( + camera, + frame_time, + SeverityEnum.detection, + {manual_info["event_id"]: manual_info["label"]}, + {}, + [], + set(), + ) + self.active_review_segments[camera] = lpr_segment + + if manual_info["state"] == ManualEventState.start: + self.indefinite_events[camera][manual_info["event_id"]] = ( + manual_info["label"] + ) + # temporarily make it so this event can not end + lpr_segment.last_alert_time = sys.maxsize + lpr_segment.last_detection_time = sys.maxsize + elif manual_info["state"] == ManualEventState.complete: + lpr_segment.last_alert_time = manual_info["end_time"] + lpr_segment.last_detection_time = manual_info["end_time"] + else: + logger.warning( + f"Dedicated LPR camera API has been called for {camera}, but detections are disabled. LPR events will not appear as a detection." + ) + + self.config_subscriber.stop() + self.requestor.stop() + self.detection_subscriber.stop() + logger.info("Exiting review maintainer...") diff --git a/frigate/review/review.py b/frigate/review/review.py new file mode 100644 index 0000000..c00c302 --- /dev/null +++ b/frigate/review/review.py @@ -0,0 +1,30 @@ +"""Run recording maintainer and cleanup.""" + +import logging +from multiprocessing.synchronize import Event as MpEvent + +from frigate.config import FrigateConfig +from frigate.const import PROCESS_PRIORITY_MED +from frigate.review.maintainer import ReviewSegmentMaintainer +from frigate.util.process import FrigateProcess + +logger = logging.getLogger(__name__) + + +class ReviewProcess(FrigateProcess): + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + super().__init__( + stop_event, + PROCESS_PRIORITY_MED, + name="frigate.review_segment_manager", + daemon=True, + ) + self.config = config + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + maintainer = ReviewSegmentMaintainer( + self.config, + self.stop_event, + ) + maintainer.start() diff --git a/frigate/review/types.py b/frigate/review/types.py new file mode 100644 index 0000000..0046f9b --- /dev/null +++ b/frigate/review/types.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class SeverityEnum(str, Enum): + alert = "alert" + detection = "detection" diff --git a/frigate/service_manager/__init__.py b/frigate/service_manager/__init__.py new file mode 100644 index 0000000..2da23b8 --- /dev/null +++ b/frigate/service_manager/__init__.py @@ -0,0 +1,4 @@ +from .multiprocessing import ServiceProcess +from .service import Service, ServiceManager + +__all__ = ["Service", "ServiceProcess", "ServiceManager"] diff --git a/frigate/service_manager/multiprocessing.py b/frigate/service_manager/multiprocessing.py new file mode 100644 index 0000000..88d497d --- /dev/null +++ b/frigate/service_manager/multiprocessing.py @@ -0,0 +1,162 @@ +import asyncio +import faulthandler +import logging +import multiprocessing as mp +import signal +import sys +import threading +from abc import ABC, abstractmethod +from asyncio.exceptions import TimeoutError +from logging.handlers import QueueHandler +from types import FrameType + +import frigate.log + +from .multiprocessing_waiter import wait as mp_wait +from .service import Service, ServiceManager + +DEFAULT_STOP_TIMEOUT = 10 # seconds + + +class BaseServiceProcess(Service, ABC): + """A Service the manages a multiprocessing.Process.""" + + _process: mp.Process | None + + def __init__( + self, + *, + name: str | None = None, + manager: ServiceManager | None = None, + ) -> None: + super().__init__(name=name, manager=manager) + + self._process = None + + async def on_start(self) -> None: + if self._process is not None: + if self._process.is_alive(): + return # Already started. + else: + self._process.close() + + # At this point, the process is either stopped or dead, so we can recreate it. + self._process = mp.Process(target=self._run) + self._process.name = self.name + self._process.daemon = True + self.before_start() + self._process.start() + self.after_start() + + self.manager.logger.info(f"Started {self.name} (pid: {self._process.pid})") + + async def on_stop( + self, + *, + force: bool = False, + timeout: float | None = None, + ) -> None: + if timeout is None: + timeout = DEFAULT_STOP_TIMEOUT + + if self._process is None: + return # Already stopped. + + running = True + + if not force: + self._process.terminate() + try: + await asyncio.wait_for(mp_wait(self._process), timeout) + running = False + except TimeoutError: + self.manager.logger.warning( + f"{self.name} is still running after {timeout} seconds. Killing." + ) + + if running: + self._process.kill() + await mp_wait(self._process) + + self._process.close() + self._process = None + + self.manager.logger.info(f"{self.name} stopped") + + @property + def pid(self) -> int | None: + return self._process.pid if self._process else None + + def _run(self) -> None: + self.before_run() + self.run() + self.after_run() + + def before_start(self) -> None: + pass + + def after_start(self) -> None: + pass + + def before_run(self) -> None: + pass + + def after_run(self) -> None: + pass + + @abstractmethod + def run(self) -> None: + pass + + def __getstate__(self) -> dict: + return { + k: v + for k, v in self.__dict__.items() + if not (k.startswith("_Service__") or k == "_process") + } + + +class ServiceProcess(BaseServiceProcess): + logger: logging.Logger + + @property + def stop_event(self) -> threading.Event: + # Lazily create the stop_event. This allows the signal handler to tell if anyone is + # monitoring the stop event, and to raise a SystemExit if not. + if "stop_event" not in self.__dict__: + stop_event = threading.Event() + self.__dict__["stop_event"] = stop_event + else: + stop_event = self.__dict__["stop_event"] + assert isinstance(stop_event, threading.Event) + + return stop_event + + def before_start(self) -> None: + if frigate.log.log_listener is None: + raise RuntimeError("Logging has not yet been set up.") + self.__log_queue = frigate.log.log_listener.queue + + def before_run(self) -> None: + super().before_run() + + faulthandler.enable() + + def receiveSignal(signalNumber: int, frame: FrameType | None) -> None: + # Get the stop_event through the dict to bypass lazy initialization. + stop_event = self.__dict__.get("stop_event") + if stop_event is not None: + # Someone is monitoring stop_event. We should set it. + stop_event.set() + else: + # Nobody is monitoring stop_event. We should raise SystemExit. + sys.exit() + + signal.signal(signal.SIGTERM, receiveSignal) + signal.signal(signal.SIGINT, receiveSignal) + + self.logger = logging.getLogger(self.name) + + logging.basicConfig(handlers=[], force=True) + logging.getLogger().addHandler(QueueHandler(self.__log_queue)) + del self.__log_queue diff --git a/frigate/service_manager/multiprocessing_waiter.py b/frigate/service_manager/multiprocessing_waiter.py new file mode 100644 index 0000000..e356fac --- /dev/null +++ b/frigate/service_manager/multiprocessing_waiter.py @@ -0,0 +1,150 @@ +import asyncio +import functools +import logging +import multiprocessing as mp +import queue +import threading +from multiprocessing.connection import Connection +from multiprocessing.connection import wait as mp_wait +from socket import socket +from typing import Any + +logger = logging.getLogger(__name__) + + +class MultiprocessingWaiter(threading.Thread): + """A background thread that manages futures for the multiprocessing.connection.wait() method.""" + + def __init__(self) -> None: + super().__init__(daemon=True) + + # Queue of objects to wait for and futures to set results for. + self._queue: queue.Queue[tuple[Any, asyncio.Future[None]]] = queue.Queue() + + # This is required to get mp_wait() to wake up when new objects to wait for are received. + receive, send = mp.Pipe(duplex=False) + self._receive_connection = receive + self._send_connection = send + + def wait_for_sentinel(self, sentinel: Any) -> asyncio.Future[None]: + """Create an asyncio.Future tracking a sentinel for multiprocessing.connection.wait() + + Warning: This method is NOT thread-safe. + """ + # This would be incredibly stupid, but you never know. + assert sentinel != self._receive_connection + + # Send the future to the background thread for processing. + future = asyncio.get_running_loop().create_future() + self._queue.put((sentinel, future)) + + # Notify the background thread. + # + # This is the non-thread-safe part, but since this method is not really meant to be called + # by users, we can get away with not adding a lock at this point (to avoid adding 2 locks). + self._send_connection.send_bytes(b".") + + return future + + def run(self) -> None: + logger.debug("Started background thread") + + wait_dict: dict[Any, set[asyncio.Future[None]]] = { + self._receive_connection: set() + } + while True: + for ready_obj in mp_wait(wait_dict.keys()): + # Make sure we never remove the receive connection from the wait dict + if ready_obj is self._receive_connection: + continue + + logger.debug( + f"Sentinel {ready_obj!r} is ready. " + f"Notifying {len(wait_dict[ready_obj])} future(s)." + ) + + # Go over all the futures attached to this object and mark them as ready. + for fut in wait_dict.pop(ready_obj): + if fut.cancelled(): + logger.debug( + f"A future for sentinel {ready_obj!r} is ready, " + "but the future is cancelled. Skipping." + ) + else: + fut.get_loop().call_soon_threadsafe( + # Note: We need to check fut.cancelled() again, since it might + # have been set before the event loop's definition of "soon". + functools.partial( + lambda fut: fut.cancelled() or fut.set_result(None), fut + ) + ) + + # Check for cancellations in the remaining futures. + done_objects = [] + for obj, fut_set in wait_dict.items(): + if obj is self._receive_connection: + continue + + # Find any cancelled futures and remove them. + cancelled = [fut for fut in fut_set if fut.cancelled()] + fut_set.difference_update(cancelled) + logger.debug( + f"Removing {len(cancelled)} future(s) from sentinel: {obj!r}" + ) + + # Mark objects with no remaining futures for removal. + if len(fut_set) == 0: + done_objects.append(obj) + + # Remove any objects that are done after removing cancelled futures. + for obj in done_objects: + logger.debug( + f"Sentinel {obj!r} no longer has any futures waiting for it." + ) + del wait_dict[obj] + + # Get new objects to wait for from the queue. + while True: + try: + obj, fut = self._queue.get_nowait() + self._receive_connection.recv_bytes(maxlength=1) + self._queue.task_done() + + logger.debug(f"Received new sentinel: {obj!r}") + + wait_dict.setdefault(obj, set()).add(fut) + except queue.Empty: + break + + +waiter_lock = threading.Lock() +waiter_thread: MultiprocessingWaiter | None = None + + +async def wait(object: mp.Process | Connection | socket) -> None: + """Wait for the supplied object to be ready. + + Under the hood, this uses multiprocessing.connection.wait() and a background thread manage the + returned futures. + """ + global waiter_thread, waiter_lock + + sentinel: Connection | socket | int + if isinstance(object, mp.Process): + sentinel = object.sentinel + elif isinstance(object, Connection) or isinstance(object, socket): + sentinel = object + else: + raise ValueError(f"Cannot wait for object of type {type(object).__qualname__}") + + with waiter_lock: + if waiter_thread is None: + # Start a new waiter thread. + waiter_thread = MultiprocessingWaiter() + waiter_thread.start() + + # Create the future while still holding the lock, + # since wait_for_sentinel() is not thread safe. + fut = waiter_thread.wait_for_sentinel(sentinel) + + await fut diff --git a/frigate/service_manager/service.py b/frigate/service_manager/service.py new file mode 100644 index 0000000..34631b6 --- /dev/null +++ b/frigate/service_manager/service.py @@ -0,0 +1,445 @@ +from __future__ import annotations + +import asyncio +import atexit +import logging +import threading +from abc import ABC, abstractmethod +from collections.abc import Coroutine +from contextvars import ContextVar +from dataclasses import dataclass +from functools import partial +from typing import Self, cast + + +class Service(ABC): + """An abstract service instance.""" + + def __init__( + self, + *, + name: str | None = None, + manager: ServiceManager | None = None, + ): + if name: + self.__dict__["name"] = name + + self.__manager = manager or ServiceManager.current() + self.__lock = asyncio.Lock(loop=self.__manager._event_loop) # type: ignore[call-arg] + self.__manager._register(self) + + @property + def name(self) -> str: + try: + return cast(str, self.__dict__["name"]) + except KeyError: + return type(self).__qualname__ + + @property + def manager(self) -> ServiceManager: + """The service manager this service is registered with.""" + try: + return self.__manager + except AttributeError: + raise RuntimeError("Cannot access associated service manager") from None + + def start( + self, + *, + wait: bool = False, + wait_timeout: float | None = None, + ) -> Self: + """Start this service. + + :param wait: If set, this function will block until the task is complete. + :param wait_timeout: If set, this function will not return until the task is complete or the + specified timeout has elapsed. + """ + + self.manager.run_task( + self.on_start(), + wait=wait, + wait_timeout=wait_timeout, + lock=self.__lock, + ) + + return self + + def stop( + self, + *, + force: bool = False, + timeout: float | None = None, + wait: bool = False, + wait_timeout: float | None = None, + ) -> Self: + """Stop this service. + + :param force: If set, the service will be killed immediately. + :param timeout: Maximum amount of time to wait before force-killing the service. + + :param wait: If set, this function will block until the task is complete. + :param wait_timeout: If set, this function will not return until the task is complete or the + specified timeout has elapsed. + """ + + self.manager.run_task( + self.on_stop(force=force, timeout=timeout), + wait=wait, + wait_timeout=wait_timeout, + lock=self.__lock, + ) + + return self + + def restart( + self, + *, + force: bool = False, + stop_timeout: float | None = None, + wait: bool = False, + wait_timeout: float | None = None, + ) -> Self: + """Restart this service. + + :param force: If set, the service will be killed immediately. + :param timeout: Maximum amount of time to wait before force-killing the service. + + :param wait: If set, this function will block until the task is complete. + :param wait_timeout: If set, this function will not return until the task is complete or the + specified timeout has elapsed. + """ + + self.manager.run_task( + self.on_restart(force=force, stop_timeout=stop_timeout), + wait=wait, + wait_timeout=wait_timeout, + lock=self.__lock, + ) + + return self + + @abstractmethod + async def on_start(self) -> None: + pass + + @abstractmethod + async def on_stop( + self, + *, + force: bool = False, + timeout: float | None = None, + ) -> None: + pass + + async def on_restart( + self, + *, + force: bool = False, + stop_timeout: float | None = None, + ) -> None: + await self.on_stop(force=force, timeout=stop_timeout) + await self.on_start() + + +default_service_manager_lock = threading.Lock() +default_service_manager: ServiceManager | None = None + +current_service_manager: ContextVar[ServiceManager] = ContextVar( + "current_service_manager" +) + + +@dataclass +class Command: + """A coroutine to execute in the service manager thread. + + Attributes: + coro: The coroutine to execute. + lock: An async lock to acquire before calling the coroutine. + done: If specified, the service manager will set this event after the command completes. + """ + + coro: Coroutine + lock: asyncio.Lock | None = None + done: threading.Event | None = None + + +class ServiceManager: + """A set of services, along with the global state required to manage them efficiently. + + Typically users of the service infrastructure will not interact with a service manager directly, + but rather through individual Service subclasses that will automatically manage a service + manager instance. + + Each service manager instance has a background thread in which service lifecycle tasks are + executed in an async executor. This is done to avoid head-of-line blocking in the business logic + that spins up individual services. This thread is automatically started when the service manager + is created and stopped either manually, or on application exit. + + All (public) service manager methods are thread-safe. + """ + + _name: str + _logger: logging.Logger + + # The set of services this service manager knows about. + _services: dict[str, Service] + _services_lock: threading.Lock + + # Commands will be queued with associated event loop. Queueing `None` signals shutdown. + _command_queue: asyncio.Queue[Command | None] + _event_loop: asyncio.AbstractEventLoop + + # The pending command counter is used to ensure all commands have been queued before shutdown. + _pending_commands: AtomicCounter + + # The set of pending tasks after they have been received by the background thread and spawned. + _tasks: set + + # Fired after the async runtime starts. Object initialization completes after this is set. + _setup_event: threading.Event + + # Will be acquired to ensure the shutdown sentinel is sent only once. Never released. + _shutdown_lock: threading.Lock + + def __init__(self, *, name: str | None = None): + self._name = name if name is not None else (__package__ or __name__) + self._logger = logging.getLogger(self.name) + + self._services = dict() + self._services_lock = threading.Lock() + + self._pending_commands = AtomicCounter() + self._tasks = set() + + self._shutdown_lock = threading.Lock() + + # --- Start the manager thread and wait for it to be ready. --- + + self._setup_event = threading.Event() + + async def start_manager() -> None: + self._event_loop = asyncio.get_running_loop() + self._command_queue = asyncio.Queue() + + self._setup_event.set() + await self._monitor_command_queue() + + self._manager_thread = threading.Thread( + name=self.name, + target=lambda: asyncio.run(start_manager()), + daemon=True, + ) + + self._manager_thread.start() + atexit.register(partial(self.shutdown, wait=True)) + + self._setup_event.wait() + + @property + def name(self) -> str: + """The name of this service manager. Primarily intended for logging purposes.""" + return self._name + + @property + def logger(self) -> logging.Logger: + """The logger used by this service manager.""" + return self._logger + + @classmethod + def current(cls) -> ServiceManager: + """The service manager set in the current context (async task or thread). + + A global default service manager will be automatically created on first access.""" + + global default_service_manager + + current = current_service_manager.get(None) + if current is None: + with default_service_manager_lock: + if default_service_manager is None: + default_service_manager = cls() + + current = default_service_manager + current_service_manager.set(current) + return current + + def make_current(self) -> None: + """Make this the current service manager.""" + + current_service_manager.set(self) + + def run_task( + self, + coro: Coroutine, + *, + wait: bool = False, + wait_timeout: float | None = None, + lock: asyncio.Lock | None = None, + ) -> None: + """Run an async task in the service manager thread. + + :param wait: If set, this function will block until the task is complete. + :param wait_timeout: If set, this function will not return until the task is complete or the + specified timeout has elapsed. + """ + + if not isinstance(coro, Coroutine): + raise TypeError(f"Cannot schedule task for object of type {type(coro)}") + + cmd = Command(coro=coro, lock=lock) + if wait or wait_timeout is not None: + cmd.done = threading.Event() + + self._send_command(cmd) + + if cmd.done is not None: + cmd.done.wait(timeout=wait_timeout) + + def shutdown( + self, *, wait: bool = False, wait_timeout: float | None = None + ) -> None: + """Shutdown the service manager thread. + + After the shutdown process completes, any subsequent calls to the service manager will + produce an error. + + :param wait: If set, this function will block until the shutdown process is complete. + :param wait_timeout: If set, this function will not return until the shutdown process is + complete or the specified timeout has elapsed. + """ + + if self._shutdown_lock.acquire(blocking=False): + self._send_command(None) + if wait: + self._manager_thread.join(timeout=wait_timeout) + + def _ensure_running(self) -> None: + self._setup_event.wait() + if not self._manager_thread.is_alive(): + raise RuntimeError(f"ServiceManager {self.name} is not running") + + def _send_command(self, command: Command | None) -> None: + self._ensure_running() + + async def queue_command() -> None: + await self._command_queue.put(command) + self._pending_commands.sub() + + self._pending_commands.add() + asyncio.run_coroutine_threadsafe(queue_command(), self._event_loop) + + def _register(self, service: Service) -> None: + """Register a service with the service manager. This is done by the service constructor.""" + + self._ensure_running() + with self._services_lock: + name_conflict: Service | None = next( + ( + existing + for name, existing in self._services.items() + if name == service.name + ), + None, + ) + + if name_conflict is service: + raise RuntimeError(f"Attempt to re-register service: {service.name}") + elif name_conflict is not None: + raise RuntimeError(f"Duplicate service name: {service.name}") + + self.logger.debug(f"Registering service: {service.name}") + self._services[service.name] = service + + def _run_command(self, command: Command) -> None: + """Execute a command and add it to the tasks set.""" + + def task_done(task: asyncio.Task) -> None: + exc = task.exception() + if exc: + self.logger.exception("Exception in service manager task", exc_info=exc) + self._tasks.discard(task) + if command.done is not None: + command.done.set() + + async def task_harness() -> None: + if command.lock is not None: + async with command.lock: + await command.coro + else: + await command.coro + + task = asyncio.create_task(task_harness()) + task.add_done_callback(task_done) + self._tasks.add(task) + + async def _monitor_command_queue(self) -> None: + """The main function of the background thread.""" + + self.logger.info("Started service manager") + + # Main command processing loop. + while (command := await self._command_queue.get()) is not None: + self._run_command(command) + + # Send a stop command to all services. We don't have a status command yet, so we can just + # stop everything and be done with it. + with self._services_lock: + self.logger.debug(f"Stopping {len(self._services)} services") + for service in self._services.values(): + service.stop() + + # Wait for all commands to finish executing. + await self._shutdown() + + self.logger.info("Exiting service manager") + + async def _shutdown(self) -> None: + """Ensure all commands have been queued & executed.""" + + while True: + command = None + try: + # Try and get a command from the queue. + command = self._command_queue.get_nowait() + except asyncio.QueueEmpty: + if self._pending_commands.value > 0: + # If there are pending commands to queue, await them. + command = await self._command_queue.get() + elif self._tasks: + # If there are still pending tasks, wait for them. These tasks might queue + # commands though, so we have to loop again. + await asyncio.wait(self._tasks) + else: + # Nothing is pending at this point, so we're done here. + break + + # If we got a command, run it. + if command is not None: + self._run_command(command) + + +class AtomicCounter: + """A lock-protected atomic counter.""" + + # Modern CPUs have atomics, but python doesn't seem to include them in the standard library. + # Besides, the performance penalty is negligible compared to, well, using python. + # So this will do just fine. + + def __init__(self, initial: int = 0): + self._lock = threading.Lock() + self._value = initial + + def add(self, value: int = 1) -> None: + with self._lock: + self._value += value + + def sub(self, value: int = 1) -> None: + with self._lock: + self._value -= value + + @property + def value(self) -> int: + with self._lock: + return self._value diff --git a/frigate/stats/__init__.py b/frigate/stats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/stats/emitter.py b/frigate/stats/emitter.py new file mode 100644 index 0000000..7c2e328 --- /dev/null +++ b/frigate/stats/emitter.py @@ -0,0 +1,147 @@ +"""Emit stats to listeners.""" + +import itertools +import json +import logging +import threading +import time +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import FREQUENCY_STATS_POINTS +from frigate.stats.prometheus import update_metrics +from frigate.stats.util import stats_snapshot +from frigate.types import StatsTrackingTypes + +logger = logging.getLogger(__name__) + + +MAX_STATS_POINTS = 80 + + +class StatsEmitter(threading.Thread): + def __init__( + self, + config: FrigateConfig, + stats_tracking: StatsTrackingTypes, + stop_event: MpEvent, + ): + super().__init__(name="frigate_stats_emitter") + self.config = config + self.stats_tracking = stats_tracking + self.stop_event = stop_event + self.hwaccel_errors: dict[str, float] = {} + self.stats_history: list[dict[str, Any]] = [] + + # create communication for stats + self.requestor = InterProcessRequestor() + + def get_latest_stats(self) -> dict[str, Any]: + """Get latest stats.""" + if len(self.stats_history) > 0: + return self.stats_history[-1] + else: + stats = stats_snapshot( + self.config, self.stats_tracking, self.hwaccel_errors + ) + self.stats_history.append(stats) + return stats + + def get_stats_history(self, keys: list[str] | None = None) -> list[dict[str, Any]]: + """Get stats history. + + Supports dot-notation for nested keys to avoid returning large objects + when only specific subfields are needed. Handles two patterns: + + - Flat dict: "service.last_updated" returns {"service": {"last_updated": ...}} + - Dict-of-dicts: "cameras.camera_fps" returns each camera entry filtered + to only include "camera_fps" + """ + if not keys: + return self.stats_history + + # Pre-parse keys into top-level keys and dot-notation fields + top_level_keys: list[str] = [] + nested_keys: dict[str, list[str]] = {} + + for k in keys: + if "." in k: + parent_key, child_key = k.split(".", 1) + nested_keys.setdefault(parent_key, []).append(child_key) + else: + top_level_keys.append(k) + + selected_stats: list[dict[str, Any]] = [] + + for s in self.stats_history: + selected: dict[str, Any] = {} + + for k in top_level_keys: + selected[k] = s.get(k) + + for parent_key, child_keys in nested_keys.items(): + parent = s.get(parent_key) + + if not isinstance(parent, dict): + selected[parent_key] = parent + continue + + # Check if values are dicts (dict-of-dicts like cameras/detectors) + first_value = next(iter(parent.values()), None) + + if isinstance(first_value, dict): + # Filter each nested entry to only requested fields, + # omitting None values to preserve key-absence semantics + selected[parent_key] = { + entry_key: { + field: val + for field in child_keys + if (val := entry.get(field)) is not None + } + for entry_key, entry in parent.items() + } + else: + # Flat dict (like service) - pick individual fields + if parent_key not in selected: + selected[parent_key] = {} + + for child_key in child_keys: + selected[parent_key][child_key] = parent.get(child_key) + + selected_stats.append(selected) + + return selected_stats + + def stats_init(config, camera_metrics, detectors, processes): + stats = { + "cameras": camera_metrics, + "detectors": detectors, + "processes": processes, + } + # Update Prometheus metrics with initial stats + update_metrics(stats) + return stats + + def run(self) -> None: + time.sleep(10) + for counter in itertools.cycle( + range(int(self.config.mqtt.stats_interval / FREQUENCY_STATS_POINTS)) + ): + if self.stop_event.wait(FREQUENCY_STATS_POINTS): + break + + logger.debug("Starting stats collection") + stats = stats_snapshot( + self.config, self.stats_tracking, self.hwaccel_errors + ) + self.stats_history.append(stats) + self.stats_history = self.stats_history[-MAX_STATS_POINTS:] + + if counter == 0: + self.requestor.send_data("stats", json.dumps(stats)) + + logger.debug("Finished stats collection") + + logger.info("Exiting stats emitter...") diff --git a/frigate/stats/intel_gpu_info.py b/frigate/stats/intel_gpu_info.py new file mode 100644 index 0000000..5bbb5c3 --- /dev/null +++ b/frigate/stats/intel_gpu_info.py @@ -0,0 +1,108 @@ +"""Resolve human-readable names for Intel GPUs via OpenVINO.""" + +import logging +import re + +logger = logging.getLogger(__name__) + + +class IntelGpuNameResolver: + """Build a pdev -> normalized device name map by enumerating OpenVINO GPUs. + + The lookup is performed once on first access and cached for the process + lifetime. OpenVINO exposes DEVICE_PCI_INFO (domain/bus/device/function) and + FULL_DEVICE_NAME for each GPU it can see, which is enough to associate the + name with the pdev string used by DRM fdinfo. + """ + + _names: dict[str, str] | None = None + + def get_names(self) -> dict[str, str]: + if self._names is not None: + return self._names + + names: dict[str, str] = {} + + try: + from openvino import Core + except ImportError: + logger.debug("OpenVINO unavailable; cannot resolve Intel GPU names") + self._names = names + return names + + try: + core = Core() + devices = core.available_devices + except Exception as exc: + logger.debug(f"OpenVINO Core initialization failed: {exc}") + self._names = names + return names + + cpu_name: str | None = None + if "CPU" in devices: + try: + cpu_name = self._strip_trademarks( + core.get_property("CPU", "FULL_DEVICE_NAME") + ) + except Exception as exc: + logger.debug(f"Failed to read CPU FULL_DEVICE_NAME: {exc}") + + for device in devices: + if not device.startswith("GPU"): + continue + + try: + pci = core.get_property(device, "DEVICE_PCI_INFO") + raw_name = core.get_property(device, "FULL_DEVICE_NAME") + device_type = core.get_property(device, "DEVICE_TYPE") + except Exception as exc: + logger.debug(f"Failed to read properties for {device}: {exc}") + continue + + pdev = self._format_pdev(pci) + if not pdev: + continue + + names[pdev] = self._resolve_name(raw_name, device_type, cpu_name) + + self._names = names + return names + + @staticmethod + def _format_pdev(pci) -> str | None: + try: + return f"{pci.domain:04x}:{pci.bus:02x}:{pci.device:02x}.{pci.function:x}" + except AttributeError: + return None + + @classmethod + def _resolve_name(cls, raw_name: str, device_type, cpu_name: str | None) -> str: + """Build a display name for a GPU. + + Modern integrated Intel GPUs are reported by OpenVINO with a generic + FULL_DEVICE_NAME like "Intel(R) Graphics (iGPU)" that gives no model + information. Since the iGPU is part of the CPU on these platforms, fall + back to the CPU name (which OpenVINO does report specifically) and + suffix it with "iGPU" so it's clear what the entry is. + """ + is_integrated = "INTEGRATED" in str(device_type).upper() + + if is_integrated and cpu_name: + short_cpu = re.sub(r"^Intel\s+", "", cpu_name) + return f"{short_cpu} iGPU" + + return cls._normalize_name(raw_name) + + @classmethod + def _normalize_name(cls, name: str) -> str: + cleaned = cls._strip_trademarks(name) + cleaned = re.sub(r"\s*\((?:i|d)GPU\)\s*$", "", cleaned, flags=re.IGNORECASE) + return " ".join(cleaned.split()) + + @staticmethod + def _strip_trademarks(name: str) -> str: + cleaned = re.sub(r"\(R\)|\(TM\)", "", name) + return " ".join(cleaned.split()) + + +intel_gpu_name_resolver = IntelGpuNameResolver() diff --git a/frigate/stats/prometheus.py b/frigate/stats/prometheus.py new file mode 100644 index 0000000..9797d56 --- /dev/null +++ b/frigate/stats/prometheus.py @@ -0,0 +1,513 @@ +import logging +import re +from typing import Any + +from prometheus_client import CONTENT_TYPE_LATEST, generate_latest +from prometheus_client.core import ( + REGISTRY, + CounterMetricFamily, + GaugeMetricFamily, + InfoMetricFamily, +) + + +class CustomCollector: + def __init__(self, _url): + self.complete_stats = {} # Store complete stats data + self.process_stats = {} # Keep for CPU processing + self.previous_event_id = None + self.previous_event_start_time = None + self.all_events = {} + + def add_metric(self, metric, label, stats, key, multiplier=1.0): # Now a method + try: + string = str(stats[key]) + value = float(re.findall(r"-?\d*\.?\d*", string)[0]) + metric.add_metric(label, value * multiplier) + except (KeyError, TypeError, IndexError, ValueError): + pass + + def add_metric_process( + self, + metric, + camera_stats, + camera_name, + pid_name, + process_name, + cpu_or_memory, + process_type, + cpu_usages, + ): + try: + pid = str(camera_stats[pid_name]) + label_values = [pid, camera_name, process_name, process_type] + try: + # new frigate:0.13.0-beta3 stat 'cmdline' + label_values.append(cpu_usages[pid]["cmdline"]) + except KeyError: + pass + metric.add_metric(label_values, cpu_usages[pid][cpu_or_memory]) + # Don't modify the original data + except (KeyError, TypeError, IndexError): + pass + + def collect(self): + # Work with a copy of the complete stats + stats = self.complete_stats.copy() + + # Create a local copy of CPU usages to work with + cpu_usages = {} + try: + cpu_usages = stats.get("cpu_usages", {}).copy() + except (KeyError, AttributeError): + pass + + # process stats for cameras, detectors and other + cpu_usages_metric = GaugeMetricFamily( + "frigate_cpu_usage_percent", + "Process CPU usage %", + labels=["pid", "name", "process", "type", "cmdline"], + ) + mem_usages = GaugeMetricFamily( + "frigate_mem_usage_percent", + "Process memory usage %", + labels=["pid", "name", "process", "type", "cmdline"], + ) + + # camera stats + audio_dBFS = GaugeMetricFamily( + "frigate_audio_dBFS", "Audio dBFS for camera", labels=["camera_name"] + ) + audio_rms = GaugeMetricFamily( + "frigate_audio_rms", "Audio RMS for camera", labels=["camera_name"] + ) + camera_fps = GaugeMetricFamily( + "frigate_camera_fps", + "Frames per second being consumed from your camera.", + labels=["camera_name"], + ) + detection_enabled = GaugeMetricFamily( + "frigate_detection_enabled", + "Detection enabled for camera", + labels=["camera_name"], + ) + detection_fps = GaugeMetricFamily( + "frigate_detection_fps", + "Number of times detection is run per second.", + labels=["camera_name"], + ) + process_fps = GaugeMetricFamily( + "frigate_process_fps", + "Frames per second being processed by frigate.", + labels=["camera_name"], + ) + skipped_fps = GaugeMetricFamily( + "frigate_skipped_fps", + "Frames per second skip for processing by frigate.", + labels=["camera_name"], + ) + + # read camera stats assuming version < frigate:0.13.0-beta3 + cameras = stats + try: + # try to read camera stats in case >= frigate:0.13.0-beta3 + cameras = stats["cameras"] + except KeyError: + pass + + for camera_name, camera_stats in cameras.items(): + self.add_metric(audio_dBFS, [camera_name], camera_stats, "audio_dBFS") + self.add_metric(audio_rms, [camera_name], camera_stats, "audio_rms") + self.add_metric(camera_fps, [camera_name], camera_stats, "camera_fps") + self.add_metric( + detection_enabled, [camera_name], camera_stats, "detection_enabled" + ) + self.add_metric(detection_fps, [camera_name], camera_stats, "detection_fps") + self.add_metric(process_fps, [camera_name], camera_stats, "process_fps") + self.add_metric(skipped_fps, [camera_name], camera_stats, "skipped_fps") + + self.add_metric_process( + cpu_usages_metric, + camera_stats, + camera_name, + "ffmpeg_pid", + "ffmpeg", + "cpu", + "Camera", + cpu_usages, + ) + self.add_metric_process( + cpu_usages_metric, + camera_stats, + camera_name, + "capture_pid", + "capture", + "cpu", + "Camera", + cpu_usages, + ) + self.add_metric_process( + cpu_usages_metric, + camera_stats, + camera_name, + "pid", + "detect", + "cpu", + "Camera", + cpu_usages, + ) + + self.add_metric_process( + mem_usages, + camera_stats, + camera_name, + "ffmpeg_pid", + "ffmpeg", + "mem", + "Camera", + cpu_usages, + ) + self.add_metric_process( + mem_usages, + camera_stats, + camera_name, + "capture_pid", + "capture", + "mem", + "Camera", + cpu_usages, + ) + self.add_metric_process( + mem_usages, + camera_stats, + camera_name, + "pid", + "detect", + "mem", + "Camera", + cpu_usages, + ) + + yield audio_dBFS + yield audio_rms + yield camera_fps + yield detection_enabled + yield detection_fps + yield process_fps + yield skipped_fps + + # bandwidth stats + bandwidth_usages = GaugeMetricFamily( + "frigate_bandwidth_usages_kBps", + "bandwidth usages kilobytes per second", + labels=["pid", "name", "process", "cmdline"], + ) + + try: + for b_pid, b_stats in stats["bandwidth_usages"].items(): + label = [b_pid] # pid label + try: + n = stats["cpu_usages"][b_pid]["cmdline"] + for p_name, p_stats in stats["processes"].items(): + if str(p_stats["pid"]) == b_pid: + n = p_name + break + + # new frigate:0.13.0-beta3 stat 'cmdline' + label.append(n) # name label + label.append(stats["cpu_usages"][b_pid]["cmdline"]) # process label + label.append(stats["cpu_usages"][b_pid]["cmdline"]) # cmdline label + self.add_metric(bandwidth_usages, label, b_stats, "bandwidth") + except KeyError: + pass + except KeyError: + pass + + yield bandwidth_usages + + # detector stats + try: + yield GaugeMetricFamily( + "frigate_detection_total_fps", + "Sum of detection_fps across all cameras and detectors.", + value=stats["detection_fps"], + ) + except KeyError: + pass + + detector_inference_speed = GaugeMetricFamily( + "frigate_detector_inference_speed_seconds", + "Time spent running object detection in seconds.", + labels=["name"], + ) + + detector_detection_start = GaugeMetricFamily( + "frigate_detection_start", + "Detector start time (unix timestamp)", + labels=["name"], + ) + + try: + for detector_name, detector_stats in stats["detectors"].items(): + self.add_metric( + detector_inference_speed, + [detector_name], + detector_stats, + "inference_speed", + 0.001, + ) # ms to seconds + self.add_metric( + detector_detection_start, + [detector_name], + detector_stats, + "detection_start", + ) + self.add_metric_process( + cpu_usages_metric, + stats["detectors"], + detector_name, + "pid", + "detect", + "cpu", + "Detector", + cpu_usages, + ) + self.add_metric_process( + mem_usages, + stats["detectors"], + detector_name, + "pid", + "detect", + "mem", + "Detector", + cpu_usages, + ) + except KeyError: + pass + + yield detector_inference_speed + yield detector_detection_start + + # detector process stats + try: + for detector_name, detector_stats in stats["detectors"].items(): + p_pid = str(detector_stats["pid"]) + label = [p_pid] # pid label + try: + # new frigate:0.13.0-beta3 stat 'cmdline' + label.append(detector_name) # name label + label.append(detector_name) # process label + label.append("detectors") # type label + label.append(cpu_usages[p_pid]["cmdline"]) # cmdline label + self.add_metric(cpu_usages_metric, label, cpu_usages[p_pid], "cpu") + self.add_metric(mem_usages, label, cpu_usages[p_pid], "mem") + # Don't modify the original data + except KeyError: + pass + + except KeyError: + pass + + # other named process stats + try: + for process_name, process_stats in stats["processes"].items(): + p_pid = str(process_stats["pid"]) + label = [p_pid] # pid label + try: + # new frigate:0.13.0-beta3 stat 'cmdline' + label.append(process_name) # name label + label.append(process_name) # process label + label.append(process_name) # type label + label.append(cpu_usages[p_pid]["cmdline"]) # cmdline label + self.add_metric(cpu_usages_metric, label, cpu_usages[p_pid], "cpu") + self.add_metric(mem_usages, label, cpu_usages[p_pid], "mem") + # Don't modify the original data + except KeyError: + pass + + except KeyError: + pass + + # remaining process stats + try: + for process_id, pid_stats in cpu_usages.items(): + label = [process_id] # pid label + try: + # new frigate:0.13.0-beta3 stat 'cmdline' + label.append(pid_stats["cmdline"]) # name label + label.append(pid_stats["cmdline"]) # process label + label.append("Other") # type label + label.append(pid_stats["cmdline"]) # cmdline label + except KeyError: + pass + self.add_metric(cpu_usages_metric, label, pid_stats, "cpu") + self.add_metric(mem_usages, label, pid_stats, "mem") + except KeyError: + pass + + yield cpu_usages_metric + yield mem_usages + + # gpu stats + gpu_usages = GaugeMetricFamily( + "frigate_gpu_usage_percent", "GPU utilisation %", labels=["gpu_name"] + ) + gpu_mem_usages = GaugeMetricFamily( + "frigate_gpu_mem_usage_percent", "GPU memory usage %", labels=["gpu_name"] + ) + gpu_enc_usages = GaugeMetricFamily( + "frigate_gpu_encoder_usage_percent", + "GPU encoder utilisation %", + labels=["gpu_name"], + ) + gpu_compute_usages = GaugeMetricFamily( + "frigate_gpu_compute_usage_percent", + "GPU compute / encode utilisation %", + labels=["gpu_name"], + ) + gpu_dec_usages = GaugeMetricFamily( + "frigate_gpu_decoder_usage_percent", + "GPU decoder utilisation %", + labels=["gpu_name"], + ) + + try: + for gpu_name, gpu_stats in stats["gpu_usages"].items(): + self.add_metric(gpu_usages, [gpu_name], gpu_stats, "gpu") + self.add_metric(gpu_mem_usages, [gpu_name], gpu_stats, "mem") + self.add_metric(gpu_enc_usages, [gpu_name], gpu_stats, "enc") + self.add_metric(gpu_compute_usages, [gpu_name], gpu_stats, "compute") + self.add_metric(gpu_dec_usages, [gpu_name], gpu_stats, "dec") + except KeyError: + pass + + yield gpu_usages + yield gpu_mem_usages + yield gpu_enc_usages + yield gpu_compute_usages + yield gpu_dec_usages + + # service stats + uptime_seconds = GaugeMetricFamily( + "frigate_service_uptime_seconds", "Uptime seconds" + ) + last_updated_timestamp = GaugeMetricFamily( + "frigate_service_last_updated_timestamp", + "Stats recorded time (unix timestamp)", + ) + + try: + service_stats = stats["service"] + self.add_metric(uptime_seconds, [""], service_stats, "uptime") + self.add_metric(last_updated_timestamp, [""], service_stats, "last_updated") + + info = { + "latest_version": stats["service"]["latest_version"], + "version": stats["service"]["version"], + } + yield InfoMetricFamily( + "frigate_service", "Frigate version info", value=info + ) + + except KeyError: + pass + + yield uptime_seconds + yield last_updated_timestamp + + temperatures = GaugeMetricFamily( + "frigate_device_temperature", "Device Temperature", labels=["device"] + ) + try: + for device_name in stats["service"]["temperatures"]: + self.add_metric( + temperatures, + [device_name], + stats["service"]["temperatures"], + device_name, + ) + except KeyError: + pass + + yield temperatures + + storage_free = GaugeMetricFamily( + "frigate_storage_free_bytes", "Storage free bytes", labels=["storage"] + ) + storage_mount_type = InfoMetricFamily( + "frigate_storage_mount_type", + "Storage mount type", + labels=["mount_type", "storage"], + ) + storage_total = GaugeMetricFamily( + "frigate_storage_total_bytes", "Storage total bytes", labels=["storage"] + ) + storage_used = GaugeMetricFamily( + "frigate_storage_used_bytes", "Storage used bytes", labels=["storage"] + ) + + try: + for storage_path, storage_stats in stats["service"]["storage"].items(): + self.add_metric( + storage_free, [storage_path], storage_stats, "free", 1e6 + ) # MB to bytes + self.add_metric( + storage_total, [storage_path], storage_stats, "total", 1e6 + ) # MB to bytes + self.add_metric( + storage_used, [storage_path], storage_stats, "used", 1e6 + ) # MB to bytes + storage_mount_type.add_metric( + storage_path, + { + "mount_type": storage_stats["mount_type"], + "storage": storage_path, + }, + ) + except KeyError: + pass + + yield storage_free + yield storage_mount_type + yield storage_total + yield storage_used + + camera_events = CounterMetricFamily( + "frigate_camera_events", + "Count of camera events since exporter started", + labels=["camera", "label"], + ) + + if len(self.all_events) > 0: + for event_count in self.all_events: + camera_events.add_metric( + [event_count["camera"], event_count["label"]], event_count["Count"] + ) + + yield camera_events + + +collector = CustomCollector(None) +REGISTRY.register(collector) + + +def update_metrics(stats: dict[str, Any], event_counts: list[dict[str, Any]]): + """Updates the Prometheus metrics with the given stats data.""" + try: + # Store the complete stats for later use by collect() + collector.complete_stats = stats.copy() + + # For backwards compatibility + collector.process_stats = stats.copy() + + collector.all_events = event_counts + + # No need to call collect() here - it will be called by get_metrics() + except Exception as e: + logging.error(f"Error updating metrics: {e}") + + +def get_metrics(): + """Returns the Prometheus metrics in text format.""" + content = generate_latest(REGISTRY) # Use generate_latest + return content, CONTENT_TYPE_LATEST diff --git a/frigate/stats/util.py b/frigate/stats/util.py new file mode 100644 index 0000000..d3cb819 --- /dev/null +++ b/frigate/stats/util.py @@ -0,0 +1,547 @@ +"""Utilities for stats.""" + +import asyncio +import logging +import os +import shutil +import time +from json import JSONDecodeError +from multiprocessing.managers import DictProxy +from typing import Any + +import requests +from requests.exceptions import RequestException + +from frigate.config import FrigateConfig +from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR +from frigate.data_processing.types import DataProcessorMetrics +from frigate.object_detection.base import ObjectDetectProcess +from frigate.types import StatsTrackingTypes +from frigate.util.services import ( + calculate_shm_requirements, + get_amd_gpu_stats, + get_axcl_npu_stats, + get_bandwidth_stats, + get_cpu_stats, + get_fs_type, + get_hailo_temps, + get_intel_gpu_stats, + get_jetson_stats, + get_nvidia_gpu_stats, + get_openvino_npu_stats, + get_rockchip_gpu_stats, + get_rockchip_npu_stats, + is_vaapi_amd_driver, +) +from frigate.version import VERSION + +logger = logging.getLogger(__name__) + +HWACCEL_ERROR_COOLDOWN_SECONDS = 3600 + + +def get_latest_version(config: FrigateConfig) -> str: + if not config.telemetry.version_check: + return "disabled" + + try: + request = requests.get( + "https://api.github.com/repos/blakeblackshear/frigate/releases/latest", + timeout=10, + ) + response = request.json() + except (RequestException, JSONDecodeError): + return "unknown" + + if request.ok and response and "tag_name" in response: + return str(response.get("tag_name").replace("v", "")) + else: + return "unknown" + + +def stats_init( + config: FrigateConfig, + camera_metrics: DictProxy, + embeddings_metrics: DataProcessorMetrics | None, + detectors: dict[str, ObjectDetectProcess], + processes: dict[str, int], +) -> StatsTrackingTypes: + stats_tracking: StatsTrackingTypes = { + "camera_metrics": camera_metrics, + "embeddings_metrics": embeddings_metrics, + "detectors": detectors, + "started": int(time.time()), + "latest_frigate_version": get_latest_version(config), + "last_updated": int(time.time()), + "processes": processes, + } + return stats_tracking + + +def read_temperature(path: str) -> float | None: + if os.path.isfile(path): + with open(path) as f: + line = f.readline().strip() + return int(line) / 1000 + return None + + +def get_temperatures() -> dict[str, float]: + temps = {} + + # Get temperatures for all attached Corals + base = "/sys/class/apex/" + if os.path.isdir(base): + for apex in os.listdir(base): + temp = read_temperature(os.path.join(base, apex, "temp")) + if temp is not None: + temps[apex] = temp + + # Get temperatures for Hailo devices + temps.update(get_hailo_temps()) + + return temps + + +def get_detector_temperature( + detector_type: str, + detector_index_by_type: dict[str, int], +) -> float | None: + """Get temperature for a specific detector based on its type.""" + if detector_type == "edgetpu": + # Get temperatures for all attached Corals + base = "/sys/class/apex/" + if os.path.isdir(base): + apex_devices = sorted(os.listdir(base)) + index = detector_index_by_type.get("edgetpu", 0) + if index < len(apex_devices): + apex_name = apex_devices[index] + temp = read_temperature(os.path.join(base, apex_name, "temp")) + if temp is not None: + return temp + elif detector_type == "hailo8l": + # Get temperatures for Hailo devices + hailo_temps = get_hailo_temps() + if hailo_temps: + hailo_device_names = sorted(hailo_temps.keys()) + index = detector_index_by_type.get("hailo8l", 0) + if index < len(hailo_device_names): + device_name = hailo_device_names[index] + return hailo_temps[device_name] + elif detector_type == "rknn": + # Rockchip temperatures are handled by the GPU / NPU stats + # as there are not detector specific temperatures + pass + + return None + + +def get_detector_stats( + stats_tracking: StatsTrackingTypes, +) -> dict[str, dict[str, Any]]: + """Get stats for all detectors, including temperatures based on detector type.""" + detector_stats: dict[str, dict[str, Any]] = {} + detector_type_indices: dict[str, int] = {} + + for name, detector in stats_tracking["detectors"].items(): + pid = detector.detect_process.pid if detector.detect_process else None + detector_type = detector.detector_config.type + + # Keep track of the index for each detector type to match temperatures correctly + current_index = detector_type_indices.get(detector_type, 0) + detector_type_indices[detector_type] = current_index + 1 + + detector_stat = { + "inference_speed": round(detector.avg_inference_speed.value * 1000, 2), # type: ignore[attr-defined] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "detection_start": detector.detection_start.value, # type: ignore[attr-defined] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "pid": pid, + } + + temp = get_detector_temperature(detector_type, {detector_type: current_index}) + + if temp is not None: + detector_stat["temperature"] = round(temp, 1) + + detector_stats[name] = detector_stat + + return detector_stats + + +def get_processing_stats( + config: FrigateConfig, + stats: dict[str, str], + hwaccel_errors: dict[str, float], +) -> None: + """Get stats for cpu / gpu.""" + + async def run_tasks() -> None: + stats_tasks = [ + asyncio.create_task(set_gpu_stats(config, stats, hwaccel_errors)), + asyncio.create_task(set_cpu_stats(stats)), + asyncio.create_task(set_npu_usages(config, stats)), + ] + + if config.telemetry.stats.network_bandwidth: + stats_tasks.append(asyncio.create_task(set_bandwidth_stats(config, stats))) + + await asyncio.wait(stats_tasks) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(run_tasks()) + loop.close() + + +async def set_cpu_stats(all_stats: dict[str, Any]) -> None: + """Set cpu usage from top.""" + cpu_stats = get_cpu_stats() + + if cpu_stats: + all_stats["cpu_usages"] = cpu_stats + + +async def set_bandwidth_stats(config: FrigateConfig, all_stats: dict[str, Any]) -> None: + """Set bandwidth from nethogs.""" + bandwidth_stats = get_bandwidth_stats(config) + + if bandwidth_stats: + all_stats["bandwidth_usages"] = bandwidth_stats + + +async def set_gpu_stats( + config: FrigateConfig, + all_stats: dict[str, Any], + hwaccel_errors: dict[str, float], +) -> None: + """Parse GPUs from hwaccel args and use for stats.""" + hwaccel_args = [] + + for camera in config.cameras.values(): + args = camera.ffmpeg.hwaccel_args + + if isinstance(args, list): + args = " ".join(args) + + if args and args not in hwaccel_args: + hwaccel_args.append(args) + + for stream_input in camera.ffmpeg.inputs: + args = stream_input.hwaccel_args + + if isinstance(args, list): + args = " ".join(args) + + if args and args not in hwaccel_args: + hwaccel_args.append(args) + + stats: dict[str, dict] = {} + intel_gpu_collected = False + now = time.monotonic() + + for args in hwaccel_args: + last_error = hwaccel_errors.get(args) + if last_error is not None: + if now - last_error < HWACCEL_ERROR_COOLDOWN_SECONDS: + continue + hwaccel_errors.pop(args, None) + + if "cuvid" in args or "nvidia" in args: + # nvidia GPU + nvidia_usage = get_nvidia_gpu_stats() + + if nvidia_usage: + for i in range(len(nvidia_usage)): + stats[nvidia_usage[i]["name"]] = { + "vendor": "nvidia", + "gpu": str(round(float(nvidia_usage[i]["gpu"]), 2)) + "%", + "mem": str(round(float(nvidia_usage[i]["mem"]), 2)) + "%", + "enc": str(round(float(nvidia_usage[i]["enc"]), 2)) + "%", + "dec": str(round(float(nvidia_usage[i]["dec"]), 2)) + "%", + "temp": str(nvidia_usage[i]["temp"]), + } + + else: + stats["nvidia-gpu"] = {"vendor": "nvidia", "gpu": "", "mem": ""} + hwaccel_errors[args] = time.monotonic() + elif "nvmpi" in args or "jetson" in args: + # nvidia Jetson + jetson_usage = get_jetson_stats() + + if jetson_usage: + stats["jetson-gpu"] = {"vendor": "nvidia", **jetson_usage} + else: + stats["jetson-gpu"] = {"vendor": "nvidia", "gpu": "", "mem": ""} + hwaccel_errors[args] = time.monotonic() + elif "qsv" in args or ("vaapi" in args and not is_vaapi_amd_driver()): + if not config.telemetry.stats.intel_gpu_stats: + continue + + if not intel_gpu_collected: + # intel GPU (QSV or VAAPI both use the same physical GPU) + intel_gpu_collected = True + intel_usage = get_intel_gpu_stats( + config.telemetry.stats.intel_gpu_device + ) + + if intel_usage: + for entry in intel_usage.values(): + name = entry.pop("name") + stats[name] = entry + else: + stats["intel-gpu"] = {"vendor": "intel", "gpu": "", "mem": ""} + hwaccel_errors[args] = time.monotonic() + elif "vaapi" in args: + if not config.telemetry.stats.amd_gpu_stats: + continue + + # AMD VAAPI GPU + amd_usage = get_amd_gpu_stats() + + if amd_usage: + stats["amd-vaapi"] = {"vendor": "amd", **amd_usage} + else: + stats["amd-vaapi"] = {"vendor": "amd", "gpu": "", "mem": ""} + hwaccel_errors[args] = time.monotonic() + elif "preset-rk" in args: + rga_usage = get_rockchip_gpu_stats() + + if rga_usage: + stats["rockchip"] = {"vendor": "rockchip", **rga_usage} + elif "v4l2m2m" in args or "rpi" in args: + # RPi v4l2m2m is currently not able to get usage stats + stats["rpi-v4l2m2m"] = {"vendor": "rpi", "gpu": "", "mem": ""} + + if stats: + all_stats["gpu_usages"] = stats + + +async def set_npu_usages(config: FrigateConfig, all_stats: dict[str, Any]) -> None: + stats: dict[str, dict] = {} + + for detector in config.detectors.values(): + if detector.type == "rknn": + # Rockchip NPU usage + rk_usage = get_rockchip_npu_stats() + stats["rockchip"] = rk_usage + elif detector.type == "openvino" and detector.device == "NPU": + # OpenVINO NPU usage + ov_usage = get_openvino_npu_stats() + stats["openvino"] = ov_usage + elif detector.type == "axengine": + # AXERA NPU usage + axcl_usage = get_axcl_npu_stats() + stats["axengine"] = axcl_usage + + if stats: + all_stats["npu_usages"] = stats + + +def stats_snapshot( + config: FrigateConfig, + stats_tracking: StatsTrackingTypes, + hwaccel_errors: dict[str, float], +) -> dict[str, Any]: + """Get a snapshot of the current stats that are being tracked.""" + camera_metrics = stats_tracking["camera_metrics"] + stats: dict[str, Any] = {} + + total_camera_fps = total_process_fps = total_skipped_fps = total_detection_fps = 0 + + stats["cameras"] = {} + for name, camera_stats in camera_metrics.items(): + if name not in config.cameras: + continue + + total_camera_fps += camera_stats.camera_fps.value + total_process_fps += camera_stats.process_fps.value + total_skipped_fps += camera_stats.skipped_fps.value + total_detection_fps += camera_stats.detection_fps.value + pid = camera_stats.process_pid.value if camera_stats.process_pid.value else None + ffmpeg_pid = camera_stats.ffmpeg_pid.value if camera_stats.ffmpeg_pid else None + capture_pid = ( + camera_stats.capture_process_pid.value + if camera_stats.capture_process_pid.value + else None + ) + # Calculate connection quality based on current state + # This is computed at stats-collection time so offline cameras + # correctly show as unusable rather than excellent + expected_fps = config.cameras[name].detect.fps + current_fps = camera_stats.camera_fps.value + reconnects = camera_stats.reconnects_last_hour.value + stalls = camera_stats.stalls_last_hour.value + + if current_fps < 0.1: + quality_str = "unusable" + elif reconnects == 0 and current_fps >= 0.9 * expected_fps and stalls < 5: + quality_str = "excellent" + elif reconnects <= 2 and current_fps >= 0.6 * expected_fps: + quality_str = "fair" + elif reconnects > 10 or current_fps < 1.0 or stalls > 100: + quality_str = "unusable" + else: + quality_str = "poor" + + connection_quality = { + "connection_quality": quality_str, + "expected_fps": expected_fps, + "reconnects_last_hour": reconnects, + "stalls_last_hour": stalls, + } + + stats["cameras"][name] = { + "camera_fps": round(camera_stats.camera_fps.value, 2), + "process_fps": round(camera_stats.process_fps.value, 2), + "skipped_fps": round(camera_stats.skipped_fps.value, 2), + "detection_fps": round(camera_stats.detection_fps.value, 2), + "detection_enabled": config.cameras[name].detect.enabled, + "pid": pid, + "capture_pid": capture_pid, + "ffmpeg_pid": ffmpeg_pid, + "audio_rms": round(camera_stats.audio_rms.value, 4), + "audio_dBFS": round(camera_stats.audio_dBFS.value, 4), + **connection_quality, + } + + stats["detectors"] = get_detector_stats(stats_tracking) + stats["camera_fps"] = round(total_camera_fps, 2) + stats["process_fps"] = round(total_process_fps, 2) + stats["skipped_fps"] = round(total_skipped_fps, 2) + stats["detection_fps"] = round(total_detection_fps, 2) + + stats["embeddings"] = {} + + # Get metrics if available + embeddings_metrics = stats_tracking.get("embeddings_metrics") + + if embeddings_metrics: + # Add metrics based on what's enabled + if config.semantic_search.enabled: + stats["embeddings"].update( + { + "image_embedding_speed": round( + embeddings_metrics.image_embeddings_speed.value * 1000, 2 + ), + "image_embedding": round( + embeddings_metrics.image_embeddings_eps.value, 2 + ), + "text_embedding_speed": round( + embeddings_metrics.text_embeddings_speed.value * 1000, 2 + ), + "text_embedding": round( + embeddings_metrics.text_embeddings_eps.value, 2 + ), + } + ) + + if config.face_recognition.enabled: + stats["embeddings"]["face_recognition_speed"] = round( + embeddings_metrics.face_rec_speed.value * 1000, 2 + ) + stats["embeddings"]["face_recognition"] = round( + embeddings_metrics.face_rec_fps.value, 2 + ) + + if config.lpr.enabled: + stats["embeddings"]["plate_recognition_speed"] = round( + embeddings_metrics.alpr_speed.value * 1000, 2 + ) + stats["embeddings"]["plate_recognition"] = round( + embeddings_metrics.alpr_pps.value, 2 + ) + + if embeddings_metrics.yolov9_lpr_pps.value > 0.0: + stats["embeddings"]["yolov9_plate_detection_speed"] = round( + embeddings_metrics.yolov9_lpr_speed.value * 1000, 2 + ) + stats["embeddings"]["yolov9_plate_detection"] = round( + embeddings_metrics.yolov9_lpr_pps.value, 2 + ) + + if embeddings_metrics.review_desc_speed.value > 0.0: + stats["embeddings"]["review_description_speed"] = round( + embeddings_metrics.review_desc_speed.value * 1000, 2 + ) + stats["embeddings"]["review_description_events_per_second"] = round( + embeddings_metrics.review_desc_dps.value, 2 + ) + + if embeddings_metrics.object_desc_speed.value > 0.0: + stats["embeddings"]["object_description_speed"] = round( + embeddings_metrics.object_desc_speed.value * 1000, 2 + ) + stats["embeddings"]["object_description_events_per_second"] = round( + embeddings_metrics.object_desc_dps.value, 2 + ) + + for key in embeddings_metrics.classification_speeds.keys(): + stats["embeddings"][f"{key}_classification_speed"] = round( + embeddings_metrics.classification_speeds[key].value * 1000, 2 + ) + stats["embeddings"][f"{key}_classification_events_per_second"] = round( + embeddings_metrics.classification_cps[key].value, 2 + ) + + get_processing_stats(config, stats, hwaccel_errors) + + stats["service"] = { + "uptime": (int(time.time()) - stats_tracking["started"]), + "version": VERSION, + "latest_version": stats_tracking["latest_frigate_version"], + "storage": {}, + "last_updated": int(time.time()), + } + + for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]: + try: + storage_stats = shutil.disk_usage(path) + except (FileNotFoundError, OSError): + stats["service"]["storage"][path] = {} + continue + + stats["service"]["storage"][path] = { + "total": round(storage_stats.total / pow(2, 20), 1), + "used": round(storage_stats.used / pow(2, 20), 1), + "free": round(storage_stats.free / pow(2, 20), 1), + "mount_type": get_fs_type(path), + } + + stats["service"]["storage"]["/dev/shm"] = calculate_shm_requirements(config) + + stats["processes"] = {} + for name, pid in stats_tracking["processes"].items(): + stats["processes"][name] = { + "pid": pid, + } + + # Embed cpu/mem stats into detectors, cameras, and processes + # so history consumers don't need the full cpu_usages dict + cpu_usages = stats.get("cpu_usages", {}) + + for det_stats in stats["detectors"].values(): + pid_str = str(det_stats.get("pid", "")) + usage = cpu_usages.get(pid_str, {}) + det_stats["cpu"] = usage.get("cpu") + det_stats["mem"] = usage.get("mem") + + for cam_stats in stats["cameras"].values(): + for pid_key, field in [ + ("ffmpeg_pid", "ffmpeg_cpu"), + ("capture_pid", "capture_cpu"), + ("pid", "detect_cpu"), + ]: + pid_str = str(cam_stats.get(pid_key, "")) + usage = cpu_usages.get(pid_str, {}) + cam_stats[field] = usage.get("cpu") + + for proc_stats in stats["processes"].values(): + pid_str = str(proc_stats.get("pid", "")) + usage = cpu_usages.get(pid_str, {}) + proc_stats["cpu"] = usage.get("cpu") + proc_stats["mem"] = usage.get("mem") + + return stats diff --git a/frigate/storage.py b/frigate/storage.py new file mode 100644 index 0000000..585a5d8 --- /dev/null +++ b/frigate/storage.py @@ -0,0 +1,302 @@ +"""Handle storage retention and usage.""" + +import logging +import shutil +import threading +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path + +from peewee import SQL, fn + +from frigate.config import FrigateConfig +from frigate.const import RECORD_DIR, REPLAY_CAMERA_PREFIX +from frigate.models import Event, Recordings +from frigate.util.builtin import clear_and_unlink + +logger = logging.getLogger(__name__) +bandwidth_equation = Recordings.segment_size / ( + Recordings.end_time - Recordings.start_time +) + +MAX_CALCULATED_BANDWIDTH = 10000 # 10Gb/hr + + +class StorageMaintainer(threading.Thread): + """Maintain frigates recording storage.""" + + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + super().__init__(name="storage_maintainer") + self.config = config + self.stop_event = stop_event + self.camera_storage_stats: dict[str, dict] = {} + + def calculate_camera_bandwidth(self) -> None: + """Calculate an average MB/hr for each camera.""" + for camera in self.config.cameras.keys(): + # Skip replay cameras + if camera.startswith(REPLAY_CAMERA_PREFIX): + continue + + # cameras with < 50 segments should be refreshed to keep size accurate + # when few segments are available + if self.camera_storage_stats.get(camera, {}).get("needs_refresh", True): + self.camera_storage_stats[camera] = { + "needs_refresh": ( + Recordings.select(fn.COUNT("*")) + .where(Recordings.camera == camera, Recordings.segment_size > 0) + .scalar() + < 50 + ) + } + + # calculate MB/hr from last 100 segments + try: + # Subquery to get last 100 segments, then average their bandwidth + last_100 = ( + Recordings.select(bandwidth_equation.alias("bw")) + .where(Recordings.camera == camera, Recordings.segment_size > 0) + .order_by(Recordings.start_time.desc()) + .limit(100) + .alias("recent") + ) + + bandwidth = round( + Recordings.select(fn.AVG(SQL("bw"))).from_(last_100).scalar() + * 3600, + 2, + ) + + if bandwidth > MAX_CALCULATED_BANDWIDTH: + logger.warning( + f"{camera} has a bandwidth of {bandwidth} MB/hr which exceeds the expected maximum. This typically indicates an issue with the cameras recordings." + ) + bandwidth = MAX_CALCULATED_BANDWIDTH + except TypeError: + bandwidth = 0 + + self.camera_storage_stats[camera]["bandwidth"] = bandwidth + logger.debug(f"{camera} has a bandwidth of {bandwidth} MiB/hr.") + + def calculate_camera_usages(self) -> dict[str, dict]: + """Calculate the storage usage of each camera.""" + usages: dict[str, dict] = {} + + for camera in self.config.cameras.keys(): + # Skip replay cameras + if camera.startswith(REPLAY_CAMERA_PREFIX): + continue + + camera_storage = ( + Recordings.select(fn.SUM(Recordings.segment_size)) + .where(Recordings.camera == camera, Recordings.segment_size != 0) + .scalar() + ) + + camera_key = ( + getattr(self.config.cameras[camera], "friendly_name", None) or camera + ) + usages[camera_key] = { + "usage": camera_storage, + "bandwidth": self.camera_storage_stats.get(camera, {}).get( + "bandwidth", 0 + ), + } + + return usages + + def check_storage_needs_cleanup(self) -> bool: + """Return if storage needs cleanup.""" + # currently runs cleanup if less than 1 hour of space is left + # disk_usage should not spin up disks + hourly_bandwidth = sum( + [b["bandwidth"] for b in self.camera_storage_stats.values()] + ) + remaining_storage = round(shutil.disk_usage(RECORD_DIR).free / pow(2, 20), 1) + logger.debug( + f"Storage cleanup check: {hourly_bandwidth} hourly with remaining storage: {remaining_storage}." + ) + return remaining_storage < float(hourly_bandwidth) + + def reduce_storage_consumption(self) -> None: + """Remove oldest hour of recordings.""" + logger.debug("Starting storage cleanup.") + deleted_segments_size = 0 + hourly_bandwidth = sum( + [b["bandwidth"] for b in self.camera_storage_stats.values()] + ) + + recordings = ( + Recordings.select( + Recordings.id, + Recordings.camera, + Recordings.start_time, + Recordings.end_time, + Recordings.segment_size, + Recordings.path, + ) + .order_by(Recordings.start_time.asc()) + .namedtuples() + .iterator() + ) + + retained_events = ( + Event.select( + Event.start_time, + Event.end_time, + ) + .where( + Event.retain_indefinitely == True, + Event.has_clip, + ) + .order_by(Event.start_time.asc()) + .namedtuples() + ) + + event_start = 0 + deleted_recordings = [] + for recording in recordings: + # check if 1 hour of storage has been reclaimed + if deleted_segments_size > hourly_bandwidth: + break + + keep = False + + # Now look for a reason to keep this recording segment + for idx in range(event_start, len(retained_events)): + event = retained_events[idx] + + # if the event starts in the future, stop checking events + # and let this recording segment expire + if event.start_time > recording.end_time: + keep = False + break + + # if the event is in progress or ends after the recording starts, keep it + # and stop looking at events + if event.end_time is None or event.end_time >= recording.start_time: + keep = True + break + + # if the event ends before this recording segment starts, skip + # this event and check the next event for an overlap. + # since the events and recordings are sorted, we can skip events + # that end before the previous recording segment started on future segments + if event.end_time < recording.start_time: + event_start = idx + + # Delete recordings not retained indefinitely + if not keep: + try: + clear_and_unlink(Path(recording.path), missing_ok=False) + deleted_recordings.append(recording) + deleted_segments_size += recording.segment_size + except FileNotFoundError: + # this file was not found so we must assume no space was cleaned up + pass + + # check if need to delete retained segments + if deleted_segments_size < hourly_bandwidth: + logger.error( + f"Could not clear {hourly_bandwidth} MB, currently {deleted_segments_size:.2f} MB have been cleared. Retained recordings must be deleted." + ) + recordings = ( + Recordings.select( + Recordings.id, + Recordings.camera, + Recordings.start_time, + Recordings.end_time, + Recordings.path, + Recordings.segment_size, + ) + .order_by(Recordings.start_time.asc()) + .namedtuples() + .iterator() + ) + + for recording in recordings: + if deleted_segments_size > hourly_bandwidth: + break + + try: + clear_and_unlink(Path(recording.path), missing_ok=False) + deleted_segments_size += recording.segment_size + deleted_recordings.append(recording) + except FileNotFoundError: + # this file was not found so we must assume no space was cleaned up + pass + else: + logger.info(f"Cleaned up {deleted_segments_size:.2f} MB of recordings") + + logger.debug(f"Expiring {len(deleted_recordings)} recordings") + # delete up to 100,000 at a time + max_deletes = 100000 + + # Update has_clip for events that overlap with deleted recordings + if deleted_recordings: + # Group deleted recordings by camera + camera_recordings = {} + for recording in deleted_recordings: + if recording.camera not in camera_recordings: + camera_recordings[recording.camera] = { + "min_start": recording.start_time, + "max_end": recording.end_time, + } + else: + camera_recordings[recording.camera]["min_start"] = min( + camera_recordings[recording.camera]["min_start"], + recording.start_time, + ) + camera_recordings[recording.camera]["max_end"] = max( + camera_recordings[recording.camera]["max_end"], + recording.end_time, + ) + + # Find all events that overlap with deleted recordings time range per camera + events_to_update = [] + for camera, time_range in camera_recordings.items(): + overlapping_events = Event.select(Event.id).where( + Event.camera == camera, + Event.has_clip == True, + Event.start_time < time_range["max_end"], + Event.end_time > time_range["min_start"], + ) + + for event in overlapping_events: + events_to_update.append(event.id) + + # Update has_clip to False for overlapping events + if events_to_update: + for i in range(0, len(events_to_update), max_deletes): + batch = events_to_update[i : i + max_deletes] + Event.update(has_clip=False).where(Event.id << batch).execute() + logger.debug( + f"Updated has_clip to False for {len(events_to_update)} events" + ) + + deleted_recordings_list = [r.id for r in deleted_recordings] + for i in range(0, len(deleted_recordings_list), max_deletes): + Recordings.delete().where( + Recordings.id << deleted_recordings_list[i : i + max_deletes] + ).execute() + + def run(self) -> None: + """Check every 5 minutes if storage needs to be cleaned up.""" + if self.config.safe_mode: + logger.info("Safe mode enabled, skipping storage maintenance") + return + + self.calculate_camera_bandwidth() + while not self.stop_event.wait(300): + if not self.camera_storage_stats or True in [ + r["needs_refresh"] for r in self.camera_storage_stats.values() + ]: + self.calculate_camera_bandwidth() + logger.debug(f"Default camera bandwidths: {self.camera_storage_stats}.") + + if self.check_storage_needs_cleanup(): + logger.info( + "Less than 1 hour of recording space left, running storage maintenance..." + ) + self.reduce_storage_consumption() + + logger.info("Exiting storage maintainer...") diff --git a/frigate/test/__init__.py b/frigate/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/test/const.py b/frigate/test/const.py new file mode 100644 index 0000000..3cbd377 --- /dev/null +++ b/frigate/test/const.py @@ -0,0 +1,4 @@ +"""Constants for testing.""" + +TEST_DB = "test.db" +TEST_DB_CLEANUPS = ["test.db", "test.db-shm", "test.db-wal"] diff --git a/frigate/test/http_api/__init__.py b/frigate/test/http_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/test/http_api/base_http_test.py b/frigate/test/http_api/base_http_test.py new file mode 100644 index 0000000..32d1109 --- /dev/null +++ b/frigate/test/http_api/base_http_test.py @@ -0,0 +1,272 @@ +import datetime +import logging +import os +import unittest +from unittest.mock import patch + +from fastapi import Request +from fastapi.testclient import TestClient +from peewee_migrate import Router +from playhouse.sqlite_ext import SqliteExtDatabase +from playhouse.sqliteq import SqliteQueueDatabase +from pydantic import Json + +from frigate.api.fastapi_app import create_fastapi_app +from frigate.config import FrigateConfig +from frigate.const import BASE_DIR, CACHE_DIR +from frigate.debug_replay import DebugReplayManager +from frigate.jobs.export import JobStatePublisher +from frigate.models import Event, Recordings, ReviewSegment +from frigate.review.types import SeverityEnum +from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS + + +class AuthTestClient(TestClient): + """TestClient that automatically adds auth headers to all requests.""" + + def request(self, *args, **kwargs): + # Add default auth headers if not already present + headers = kwargs.get("headers") or {} + if "remote-user" not in headers: + headers["remote-user"] = "admin" + if "remote-role" not in headers: + headers["remote-role"] = "admin" + kwargs["headers"] = headers + return super().request(*args, **kwargs) + + +class BaseTestHttp(unittest.TestCase): + def setUp(self, models): + # setup clean database for each test run + migrate_db = SqliteExtDatabase("test.db") + del logging.getLogger("peewee_migrate").handlers[:] + router = Router(migrate_db) + router.run() + migrate_db.close() + self.db = SqliteQueueDatabase(TEST_DB) + self.db.bind(models) + + # The export job manager broadcasts via JobStatePublisher on + # enqueue/start/finish. There is no dispatcher process bound to + # the IPC socket in tests, so a real publish() would block on + # recv_json forever. Replace publish with a no-op for the + # lifetime of this test; the lookup goes through the class so any + # already-instantiated publisher (the singleton manager's) picks + # up the no-op too. + publisher_patch = patch.object( + JobStatePublisher, "publish", lambda self, payload: None + ) + publisher_patch.start() + self.addCleanup(publisher_patch.stop) + + 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, + }, + } + }, + } + self.test_stats = { + "camera_fps": 5.0, + "process_fps": 5.0, + "skipped_fps": 0.0, + "detection_fps": 13.7, + "detectors": { + "cpu1": { + "detection_start": 0.0, + "inference_speed": 91.43, + "pid": 42, + }, + "cpu2": { + "detection_start": 0.0, + "inference_speed": 84.99, + "pid": 44, + }, + }, + "front_door": { + "camera_fps": 0.0, + "capture_pid": 53, + "detection_fps": 0.0, + "pid": 52, + "process_fps": 0.0, + "skipped_fps": 0.0, + }, + "service": { + "storage": { + "/dev/shm": { + "free": 50.5, + "mount_type": "tmpfs", + "total": 67.1, + "used": 16.6, + }, + os.path.join(BASE_DIR, "clips"): { + "free": 42429.9, + "mount_type": "ext4", + "total": 244529.7, + "used": 189607.0, + }, + os.path.join(BASE_DIR, "recordings"): { + "free": 0.2, + "mount_type": "ext4", + "total": 8.0, + "used": 7.8, + }, + CACHE_DIR: { + "free": 976.8, + "mount_type": "tmpfs", + "total": 1000.0, + "used": 23.2, + }, + }, + "uptime": 101113, + "version": "0.10.1", + "latest_version": "0.11", + }, + } + + def tearDown(self): + if not self.db.is_closed(): + self.db.close() + + try: + for file in TEST_DB_CLEANUPS: + os.remove(file) + except OSError: + pass + + def create_app(self, stats=None, event_metadata_publisher=None): + from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user + + app = create_fastapi_app( + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + stats, + event_metadata_publisher, + None, + DebugReplayManager(), + enforce_default_admin=False, + ) + + # Default test mocks for authentication + # Tests can override these in their setUp if needed + # This mock uses headers set by AuthTestClient + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + 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 + + def insert_mock_event( + self, + id: str, + start_time: float | None = None, + end_time: float | None = None, + has_clip: bool = True, + top_score: int = 100, + score: int = 0, + data: Json = {}, + camera: str = "front_door", + ) -> Event: + """Inserts a basic event model with a given id.""" + if start_time is None: + start_time = datetime.datetime.now().timestamp() + if end_time is None: + end_time = start_time + 20 + + return Event.insert( + id=id, + label="Mock", + camera=camera, + start_time=start_time, + end_time=end_time, + top_score=top_score, + score=score, + false_positive=False, + zones=list(), + thumbnail="", + region=[], + box=[], + area=0, + has_clip=has_clip, + has_snapshot=True, + data=data, + ).execute() + + def insert_mock_review_segment( + self, + id: str, + start_time: float | None = None, + end_time: float | None = None, + severity: SeverityEnum = SeverityEnum.alert, + data: dict | None = None, + camera: str = "front_door", + ) -> ReviewSegment: + """Inserts a review segment model with a given id.""" + if start_time is None: + start_time = datetime.datetime.now().timestamp() + if end_time is None: + end_time = start_time + 20 + if data is None: + data = {} + + return ReviewSegment.insert( + id=id, + camera=camera, + start_time=start_time, + end_time=end_time, + severity=severity, + thumb_path=False, + data=data, + ).execute() + + def insert_mock_recording( + self, + id: str, + start_time: float | None = None, + end_time: float | None = None, + motion: int = 0, + ) -> Event: + """Inserts a recording model with a given id.""" + if start_time is None: + start_time = datetime.datetime.now().timestamp() + if end_time is None: + end_time = start_time + 20 + + return Recordings.insert( + id=id, + path=id, + camera="front_door", + start_time=start_time, + end_time=end_time, + duration=end_time - start_time, + motion=motion, + ).execute() diff --git a/frigate/test/http_api/test_debug_replay_api.py b/frigate/test/http_api/test_debug_replay_api.py new file mode 100644 index 0000000..be4e7f4 --- /dev/null +++ b/frigate/test/http_api/test_debug_replay_api.py @@ -0,0 +1,124 @@ +"""Tests for /debug_replay API endpoints.""" + +from unittest.mock import patch + +from frigate.models import Event, Recordings, ReviewSegment +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestDebugReplayAPI(BaseTestHttp): + def setUp(self): + super().setUp([Event, Recordings, ReviewSegment]) + self.app = self.create_app() + + def test_start_returns_202_with_job_id(self): + # Stub the factory to skip validation/threading and just record the + # name on the manager the way the real factory's mark_starting would. + def fake_start(**kwargs): + source = kwargs["source"] + kwargs["replay_manager"].mark_starting( + source_camera=source.source_camera, + replay_camera_name="_replay_front", + start_ts=source.start_ts, + end_ts=source.end_ts, + ) + return "job-1234" + + with patch( + "frigate.api.debug_replay.start_debug_replay_job", + side_effect=fake_start, + ): + with AuthTestClient(self.app) as client: + resp = client.post( + "/debug_replay/start", + json={ + "camera": "front", + "start_time": 100, + "end_time": 200, + }, + ) + + self.assertEqual(resp.status_code, 202) + body = resp.json() + self.assertTrue(body["success"]) + self.assertEqual(body["job_id"], "job-1234") + self.assertEqual(body["replay_camera"], "_replay_front") + + def test_start_returns_400_on_validation_error(self): + with patch( + "frigate.api.debug_replay.start_debug_replay_job", + side_effect=ValueError("Camera 'missing' not found"), + ): + with AuthTestClient(self.app) as client: + resp = client.post( + "/debug_replay/start", + json={ + "camera": "missing", + "start_time": 100, + "end_time": 200, + }, + ) + + self.assertEqual(resp.status_code, 400) + body = resp.json() + self.assertFalse(body["success"]) + # Message is hard-coded so we don't echo exception text back to clients + # (CodeQL: information exposure through an exception). + self.assertEqual(body["message"], "Invalid debug replay parameters") + + def test_start_returns_409_when_session_already_active(self): + with patch( + "frigate.api.debug_replay.start_debug_replay_job", + side_effect=RuntimeError("A replay session is already active"), + ): + with AuthTestClient(self.app) as client: + resp = client.post( + "/debug_replay/start", + json={ + "camera": "front", + "start_time": 100, + "end_time": 200, + }, + ) + + self.assertEqual(resp.status_code, 409) + body = resp.json() + self.assertFalse(body["success"]) + + def test_status_inactive_when_no_session(self): + with AuthTestClient(self.app) as client: + resp = client.get("/debug_replay/status") + + self.assertEqual(resp.status_code, 200) + body = resp.json() + self.assertFalse(body["active"]) + self.assertIsNone(body["replay_camera"]) + self.assertIsNone(body["source_camera"]) + self.assertIsNone(body["start_time"]) + self.assertIsNone(body["end_time"]) + self.assertFalse(body["live_ready"]) + # Make sure deprecated fields are gone + self.assertNotIn("state", body) + self.assertNotIn("progress_percent", body) + self.assertNotIn("error_message", body) + + def test_status_active_after_mark_starting(self): + manager = self.app.replay_manager + manager.mark_starting( + source_camera="front", + replay_camera_name="_replay_front", + start_ts=100.0, + end_ts=200.0, + ) + + with AuthTestClient(self.app) as client: + resp = client.get("/debug_replay/status") + + self.assertEqual(resp.status_code, 200) + body = resp.json() + self.assertTrue(body["active"]) + self.assertEqual(body["replay_camera"], "_replay_front") + self.assertEqual(body["source_camera"], "front") + self.assertEqual(body["start_time"], 100.0) + self.assertEqual(body["end_time"], 200.0) + self.assertFalse(body["live_ready"]) diff --git a/frigate/test/http_api/test_http_app.py b/frigate/test/http_api/test_http_app.py new file mode 100644 index 0000000..4c581dd --- /dev/null +++ b/frigate/test/http_api/test_http_app.py @@ -0,0 +1,182 @@ +from unittest.mock import Mock, patch + +import frigate.genai +from frigate.config import GenAIProviderEnum +from frigate.const import REDACTED_CREDENTIAL_SENTINEL +from frigate.genai import GenAIClient +from frigate.models import Event, Recordings, ReviewSegment +from frigate.stats.emitter import StatsEmitter +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpApp(BaseTestHttp): + def setUp(self): + super().setUp([Event, Recordings, ReviewSegment]) + self.app = super().create_app() + + #################################################################################################################### + ################################### GET /stats Endpoint ######################################################### + #################################################################################################################### + def test_stats_endpoint(self): + stats = Mock(spec=StatsEmitter) + stats.get_latest_stats.return_value = self.test_stats + app = super().create_app(stats) + + with AuthTestClient(app) as client: + response = client.get("/stats") + response_json = response.json() + assert response_json == self.test_stats + + def test_recordings_storage_requires_admin(self): + stats = Mock(spec=StatsEmitter) + stats.get_latest_stats.return_value = self.test_stats + app = super().create_app(stats) + app.storage_maintainer = Mock() + app.storage_maintainer.calculate_camera_usages.return_value = { + "front_door": {"usage": 2.0}, + } + + with AuthTestClient(app) as client: + response = client.get( + "/recordings/storage", + headers={"remote-user": "viewer", "remote-role": "viewer"}, + ) + assert response.status_code == 403 + + response = client.get("/recordings/storage") + assert response.status_code == 200 + assert response.json()["front_door"]["usage_percent"] == 25.0 + + def test_config_set_in_memory_replaces_objects_track_list(self): + self.minimal_config["cameras"]["front_door"]["objects"] = { + "track": ["person", "car"], + } + app = super().create_app() + app.config_publisher = Mock() + + with AuthTestClient(app) as client: + response = client.put( + "/config/set", + json={ + "requires_restart": 0, + "skip_save": True, + "update_topic": "config/cameras/front_door/objects", + "config_data": { + "cameras": { + "front_door": { + "objects": { + "track": ["person"], + } + } + } + }, + }, + ) + + assert response.status_code == 200 + assert app.frigate_config.cameras["front_door"].objects.track == ["person"] + + #################################################################################################################### + ################################### Credential redaction sentinel ################################################ + #################################################################################################################### + def test_config_response_redacts_mqtt_password_with_sentinel(self): + self.minimal_config["mqtt"]["user"] = "mqttuser" + self.minimal_config["mqtt"]["password"] = "supersecret" + app = super().create_app() + + with AuthTestClient(app) as client: + response = client.get("/config") + assert response.status_code == 200 + mqtt = response.json()["mqtt"] + assert mqtt["password"] == REDACTED_CREDENTIAL_SENTINEL + + #################################################################################################################### + ################################### POST /genai/probe Endpoint ################################################## + #################################################################################################################### + def test_genai_probe_requires_admin(self): + app = super().create_app() + + with AuthTestClient(app) as client: + response = client.post( + "/genai/probe", + json={"provider": "openai"}, + headers={"remote-user": "viewer", "remote-role": "viewer"}, + ) + assert response.status_code == 403 + + def test_genai_probe_returns_models_from_transient_client(self): + class FakeClient(GenAIClient): + def list_models(self): + return ["fake-model-a", "fake-model-b"] + + app = super().create_app() + + with ( + AuthTestClient(app) as client, + patch.dict( + frigate.genai.PROVIDERS, + {GenAIProviderEnum.openai: FakeClient}, + ), + ): + response = client.post( + "/genai/probe", + json={ + "provider": "openai", + "api_key": "sk-test", + "base_url": "https://example.invalid", + }, + ) + assert response.status_code == 200 + assert response.json() == { + "success": True, + "models": ["fake-model-a", "fake-model-b"], + } + + def test_genai_probe_empty_list_is_treated_as_failure(self): + # The plugin's list_models() returns [] on connection failure rather + # than raising. The endpoint should surface that as success=false so + # the UI can show a meaningful error. + class EmptyClient(GenAIClient): + def list_models(self): + return [] + + app = super().create_app() + + with ( + AuthTestClient(app) as client, + patch.dict( + frigate.genai.PROVIDERS, + {GenAIProviderEnum.openai: EmptyClient}, + ), + ): + response = client.post( + "/genai/probe", + json={"provider": "openai"}, + ) + assert response.status_code == 200 + payload = response.json() + assert payload["success"] is False + assert "message" in payload + + def test_genai_probe_handles_provider_failure(self): + class FailingClient(GenAIClient): + def list_models(self): + raise RuntimeError("provider unreachable") + + app = super().create_app() + + with ( + AuthTestClient(app) as client, + patch.dict( + frigate.genai.PROVIDERS, + {GenAIProviderEnum.openai: FailingClient}, + ), + ): + response = client.post( + "/genai/probe", + json={"provider": "openai"}, + ) + assert response.status_code == 200 + payload = response.json() + assert payload["success"] is False + assert "message" in payload diff --git a/frigate/test/http_api/test_http_camera_access.py b/frigate/test/http_api/test_http_camera_access.py new file mode 100644 index 0000000..44520d7 --- /dev/null +++ b/frigate/test/http_api/test_http_camera_access.py @@ -0,0 +1,442 @@ +import os +from unittest.mock import patch + +from fastapi import HTTPException, Request +from fastapi.testclient import TestClient + +from frigate.api.auth import ( + get_allowed_cameras_for_filter, + get_current_user, +) +from frigate.models import Event, Recordings, ReviewSegment +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + +# Minimal multi-camera config used by go2rtc stream access tests. +# front_door has a stream alias "front_door_main"; back_door uses its own name. +# The "limited_user" role is restricted to front_door only. +_MULTI_CAMERA_CONFIG = { + "mqtt": {"host": "mqtt"}, + "auth": { + "roles": { + "limited_user": ["front_door"], + } + }, + "cameras": { + "front_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "live": {"streams": {"default": "front_door_main"}}, + }, + "back_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + }, +} + + +class TestCameraAccessEventReview(BaseTestHttp): + def setUp(self): + super().setUp([Event, ReviewSegment, Recordings]) + self.app = super().create_app() + + # Mock get_current_user for all tests + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} + + self.app.dependency_overrides[get_current_user] = mock_get_current_user + + def tearDown(self): + self.app.dependency_overrides.clear() + super().tearDown() + + def test_event_camera_access(self): + super().insert_mock_event("event1", camera="front_door") + super().insert_mock_event("event2", camera="back_door") + + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events") + assert resp.status_code == 200 + ids = [e["id"] for e in resp.json()] + assert "event1" in ids + assert "event2" not in ids + + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events") + assert resp.status_code == 200 + ids = [e["id"] for e in resp.json()] + assert "event1" in ids and "event2" in ids + + def test_review_camera_access(self): + super().insert_mock_review_segment("rev1", camera="front_door") + super().insert_mock_review_segment("rev2", camera="back_door") + + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/review") + assert resp.status_code == 200 + ids = [r["id"] for r in resp.json()] + assert "rev1" in ids + assert "rev2" not in ids + + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/review") + assert resp.status_code == 200 + ids = [r["id"] for r in resp.json()] + assert "rev1" in ids and "rev2" in ids + + def test_event_single_access(self): + super().insert_mock_event("event1", camera="front_door") + + # Allowed + async def mock_require_allowed(camera: str, request: Request = None): + if camera == "front_door": + return + raise HTTPException(status_code=403, detail="Access denied") + + with patch("frigate.api.event.require_camera_access", mock_require_allowed): + with AuthTestClient(self.app) as client: + resp = client.get("/events/event1") + assert resp.status_code == 200 + assert resp.json()["id"] == "event1" + + # Disallowed + async def mock_require_disallowed(camera: str, request: Request = None): + raise HTTPException(status_code=403, detail="Access denied") + + with patch("frigate.api.event.require_camera_access", mock_require_disallowed): + with AuthTestClient(self.app) as client: + resp = client.get("/events/event1") + assert resp.status_code == 403 + + def test_review_single_access(self): + super().insert_mock_review_segment("rev1", camera="front_door") + + # Allowed + async def mock_require_allowed(camera: str, request: Request = None): + if camera == "front_door": + return + raise HTTPException(status_code=403, detail="Access denied") + + with patch("frigate.api.review.require_camera_access", mock_require_allowed): + with AuthTestClient(self.app) as client: + resp = client.get("/review/rev1") + assert resp.status_code == 200 + assert resp.json()["id"] == "rev1" + + # Disallowed + async def mock_require_disallowed(camera: str, request: Request = None): + raise HTTPException(status_code=403, detail="Access denied") + + with patch("frigate.api.review.require_camera_access", mock_require_disallowed): + with AuthTestClient(self.app) as client: + resp = client.get("/review/rev1") + assert resp.status_code == 403 + + def test_event_search_access(self): + super().insert_mock_event("event1", camera="front_door") + super().insert_mock_event("event2", camera="back_door") + + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events", params={"cameras": "all"}) + assert resp.status_code == 200 + ids = [e["id"] for e in resp.json()] + assert "event1" in ids + assert "event2" not in ids + + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events", params={"cameras": "all"}) + assert resp.status_code == 200 + ids = [e["id"] for e in resp.json()] + assert "event1" in ids and "event2" in ids + + def test_event_summary_access(self): + super().insert_mock_event("event1", camera="front_door") + super().insert_mock_event("event2", camera="back_door") + + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events/summary") + assert resp.status_code == 200 + summary_list = resp.json() + assert len(summary_list) == 1 + + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events/summary") + summary_list = resp.json() + assert len(summary_list) == 2 + + +class TestGo2rtcStreamAccess(BaseTestHttp): + """Tests for require_go2rtc_stream_access — the auth dependency on + GET /go2rtc/streams/{stream_name}. + + go2rtc is not running in unit tests, so an authorized request returns + 500 (the proxy call fails), while an unauthorized request returns 401/403 + before the proxy is ever reached. + """ + + def _make_app(self, config_override: dict | None = None): + """Build a test app, optionally replacing self.minimal_config.""" + if config_override is not None: + self.minimal_config = config_override + app = super().create_app() + + # Allow tests to control the current user via request headers. + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, + status_code=401, + ) + return {"username": username, "role": role} + + app.dependency_overrides[get_current_user] = mock_get_current_user + return app + + def setUp(self): + super().setUp([Event, ReviewSegment, Recordings]) + + def tearDown(self): + super().tearDown() + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _get_stream( + self, app, stream_name: str, role: str = "admin", user: str = "test" + ): + """Issue GET /go2rtc/streams/{stream_name} with the given role.""" + with AuthTestClient(app) as client: + return client.get( + f"/go2rtc/streams/{stream_name}", + headers={"remote-user": user, "remote-role": role}, + ) + + # ------------------------------------------------------------------ + # Tests + # ------------------------------------------------------------------ + + def test_admin_can_access_any_stream(self): + """Admin role bypasses camera restrictions.""" + app = self._make_app(_MULTI_CAMERA_CONFIG) + # front_door stream — go2rtc is not running so expect 500, not 401/403 + resp = self._get_stream(app, "front_door", role="admin") + assert resp.status_code not in (401, 403), ( + f"Admin should not be blocked; got {resp.status_code}" + ) + + # back_door stream + resp = self._get_stream(app, "back_door", role="admin") + assert resp.status_code not in (401, 403) + + def test_missing_auth_headers_returns_401(self): + """Requests without auth headers must be rejected with 401.""" + app = self._make_app(_MULTI_CAMERA_CONFIG) + # Use plain TestClient (not AuthTestClient) so no headers are injected. + with TestClient(app, raise_server_exceptions=False) as client: + resp = client.get("/go2rtc/streams/front_door") + assert resp.status_code == 401, f"Expected 401, got {resp.status_code}" + + def test_unconfigured_role_can_access_any_stream(self): + """When no camera restrictions are configured for a role the user + should have access to all streams (no roles_dict entry ⇒ no restriction).""" + no_roles_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_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]} + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + }, + } + app = self._make_app(no_roles_config) + + # "myuser" role is not listed in roles_dict — should be allowed everywhere + for stream in ("front_door", "back_door"): + resp = self._get_stream(app, stream, role="myuser") + assert resp.status_code not in (401, 403), ( + f"Unconfigured role should not be blocked on '{stream}'; " + f"got {resp.status_code}" + ) + + def test_restricted_role_can_access_allowed_camera(self): + """limited_user role (restricted to front_door) can access front_door stream.""" + app = self._make_app(_MULTI_CAMERA_CONFIG) + resp = self._get_stream(app, "front_door", role="limited_user") + assert resp.status_code not in (401, 403), ( + f"limited_user should be allowed on front_door; got {resp.status_code}" + ) + + def test_restricted_role_blocked_from_disallowed_camera(self): + """limited_user role (restricted to front_door) cannot access back_door stream.""" + app = self._make_app(_MULTI_CAMERA_CONFIG) + resp = self._get_stream(app, "back_door", role="limited_user") + assert resp.status_code == 403, ( + f"limited_user should be denied on back_door; got {resp.status_code}" + ) + + def test_stream_alias_allowed_for_owning_camera(self): + """Stream alias 'front_door_main' is owned by front_door; limited_user (who + is allowed front_door) should be permitted.""" + app = self._make_app(_MULTI_CAMERA_CONFIG) + # front_door_main is the alias defined in live.streams for front_door + resp = self._get_stream(app, "front_door_main", role="limited_user") + assert resp.status_code not in (401, 403), ( + f"limited_user should be allowed on alias front_door_main; " + f"got {resp.status_code}" + ) + + def test_add_stream_rejects_restricted_source(self): + """PUT /go2rtc/streams must reject exec:/echo:/expr: sources even for + admins""" + app = self._make_app(_MULTI_CAMERA_CONFIG) + with AuthTestClient(app) as client: + for src in ( + "exec:/tmp/rev.sh", + "echo:foo", + "expr:bar", + " exec:/tmp/rev.sh", + ): + resp = client.put(f"/go2rtc/streams/revshell?src={src}") + assert resp.status_code == 400, ( + f"Expected 400 for restricted src {src!r}; got {resp.status_code}" + ) + assert resp.json().get("success") is False + + def test_add_stream_allows_non_restricted_source(self): + """A normal stream URL should pass the restricted-source check and reach + the (unavailable in tests) go2rtc proxy — so we expect 500, not 400.""" + app = self._make_app(_MULTI_CAMERA_CONFIG) + with AuthTestClient(app) as client: + resp = client.put("/go2rtc/streams/legit?src=rtsp://10.0.0.1:554/video") + assert resp.status_code != 400, ( + f"Non-restricted source should not be rejected with 400; got {resp.status_code}" + ) + + def test_add_stream_allows_restricted_source_when_override_set(self): + """When GO2RTC_ALLOW_ARBITRARY_EXEC is set, the API must defer to operator + intent and forward the request to go2rtc instead of short-circuiting with 400.""" + app = self._make_app(_MULTI_CAMERA_CONFIG) + mock_response = type("R", (), {"ok": True, "status_code": 200, "text": "ok"})() + with patch.dict(os.environ, {"GO2RTC_ALLOW_ARBITRARY_EXEC": "true"}): + with patch( + "frigate.api.camera.requests.put", return_value=mock_response + ) as mock_put: + with AuthTestClient(app) as client: + resp = client.put("/go2rtc/streams/legit?src=exec:/tmp/something") + assert resp.status_code == 200, ( + f"Restricted src should be forwarded when override set; got {resp.status_code}" + ) + mock_put.assert_called_once() + forwarded_src = mock_put.call_args.kwargs["params"]["src"] + assert forwarded_src == "exec:/tmp/something" + + def test_stream_alias_blocked_when_owning_camera_disallowed(self): + """limited_user cannot access a stream alias that belongs to a camera they + are not allowed to see.""" + # Give back_door a stream alias and restrict limited_user to front_door only + config = { + "mqtt": {"host": "mqtt"}, + "auth": { + "roles": { + "limited_user": ["front_door"], + } + }, + "cameras": { + "front_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + "back_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]} + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "live": {"streams": {"default": "back_door_main"}}, + }, + }, + } + app = self._make_app(config) + resp = self._get_stream(app, "back_door_main", role="limited_user") + assert resp.status_code == 403, ( + f"limited_user should be denied on alias back_door_main; " + f"got {resp.status_code}" + ) diff --git a/frigate/test/http_api/test_http_config_set.py b/frigate/test/http_api/test_http_config_set.py new file mode 100644 index 0000000..48b1ac2 --- /dev/null +++ b/frigate/test/http_api/test_http_config_set.py @@ -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() diff --git a/frigate/test/http_api/test_http_event.py b/frigate/test/http_api/test_http_event.py new file mode 100644 index 0000000..8aca657 --- /dev/null +++ b/frigate/test/http_api/test_http_event.py @@ -0,0 +1,483 @@ +from datetime import datetime +from typing import Any +from unittest.mock import Mock + +from playhouse.shortcuts import model_to_dict + +from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user +from frigate.comms.event_metadata_updater import EventMetadataPublisher +from frigate.models import Event, Recordings, ReviewSegment, Timeline +from frigate.stats.emitter import StatsEmitter +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp, Request +from frigate.test.test_storage import _insert_mock_event + + +class TestHttpApp(BaseTestHttp): + def setUp(self): + super().setUp([Event, Recordings, ReviewSegment, Timeline]) + self.app = super().create_app() + + # Mock get_current_user for all tests + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} + + self.app.dependency_overrides[get_current_user] = mock_get_current_user + + async def mock_get_allowed_cameras_for_filter(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) + + def tearDown(self): + self.app.dependency_overrides.clear() + super().tearDown() + + #################################################################################################################### + ################################### GET /events Endpoint ######################################################### + #################################################################################################################### + def test_get_event_list_no_events(self): + with AuthTestClient(self.app) as client: + events = client.get("/events").json() + assert len(events) == 0 + + def test_get_event_list_no_match_event_id(self): + id = "123456.random" + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + events = client.get("/events", params={"event_id": "abc"}).json() + assert len(events) == 0 + + def test_get_event_list_match_event_id(self): + id = "123456.random" + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + events = client.get("/events", params={"event_id": id}).json() + assert len(events) == 1 + assert events[0]["id"] == id + + def test_get_event_list_match_length(self): + now = int(datetime.now().timestamp()) + + id = "123456.random" + with AuthTestClient(self.app) as client: + super().insert_mock_event(id, now, now + 1) + events = client.get( + "/events", params={"max_length": 1, "min_length": 1} + ).json() + assert len(events) == 1 + assert events[0]["id"] == id + + def test_get_event_list_no_match_max_length(self): + now = int(datetime.now().timestamp()) + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_event(id, now, now + 2) + events = client.get("/events", params={"max_length": 1}).json() + assert len(events) == 0 + + def test_get_event_list_no_match_min_length(self): + now = int(datetime.now().timestamp()) + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_event(id, now, now + 2) + events = client.get("/events", params={"min_length": 3}).json() + assert len(events) == 0 + + def test_get_event_list_limit(self): + now = datetime.now().timestamp() + id = "123456.random" + id2 = "54321.random" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(id, start_time=now + 1) + events = client.get("/events").json() + assert len(events) == 1 + assert events[0]["id"] == id + + super().insert_mock_event(id2, start_time=now) + events = client.get("/events").json() + assert len(events) == 2 + + events = client.get("/events", params={"limit": 1}).json() + assert len(events) == 1 + assert events[0]["id"] == id + + events = client.get("/events", params={"limit": 3}).json() + assert len(events) == 2 + + def test_get_event_list_no_match_has_clip(self): + now = int(datetime.now().timestamp()) + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_event(id, now, now + 2) + events = client.get("/events", params={"has_clip": 0}).json() + assert len(events) == 0 + + def test_get_event_list_has_clip(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_event(id, has_clip=True) + events = client.get("/events", params={"has_clip": 1}).json() + assert len(events) == 1 + assert events[0]["id"] == id + + def test_get_event_list_sort_score(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + id2 = "54321.random" + super().insert_mock_event(id, top_score=37, score=37, data={"score": 50}) + super().insert_mock_event(id2, top_score=47, score=47, data={"score": 20}) + events = client.get("/events", params={"sort": "score_asc"}).json() + assert len(events) == 2 + assert events[0]["id"] == id2 + assert events[1]["id"] == id + + events = client.get("/events", params={"sort": "score_desc"}).json() + assert len(events) == 2 + assert events[0]["id"] == id + assert events[1]["id"] == id2 + + def test_get_event_list_sort_start_time(self): + now = int(datetime.now().timestamp()) + + with AuthTestClient(self.app) as client: + id = "123456.random" + id2 = "54321.random" + super().insert_mock_event(id, start_time=now + 3) + super().insert_mock_event(id2, start_time=now) + events = client.get("/events", params={"sort": "date_asc"}).json() + assert len(events) == 2 + assert events[0]["id"] == id2 + assert events[1]["id"] == id + + events = client.get("/events", params={"sort": "date_desc"}).json() + assert len(events) == 2 + assert events[0]["id"] == id + assert events[1]["id"] == id2 + + def test_get_event_list_match_multilingual_attribute(self): + event_id = "123456.zh" + attribute = "中文标签" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(event_id, data={"custom_attr": attribute}) + + events = client.get("/events", params={"attributes": attribute}).json() + assert len(events) == 1 + assert events[0]["id"] == event_id + + events = client.get( + "/events", params={"attributes": "%E4%B8%AD%E6%96%87%E6%A0%87%E7%AD%BE"} + ).json() + assert len(events) == 1 + assert events[0]["id"] == event_id + + def test_events_search_match_multilingual_attribute(self): + event_id = "123456.zh.search" + attribute = "中文标签" + mock_embeddings = Mock() + mock_embeddings.search_thumbnail.return_value = [(event_id, 0.05)] + + self.app.frigate_config.semantic_search.enabled = True + self.app.embeddings = mock_embeddings + + with AuthTestClient(self.app) as client: + super().insert_mock_event(event_id, data={"custom_attr": attribute}) + + events = client.get( + "/events/search", + params={ + "search_type": "similarity", + "event_id": event_id, + "attributes": attribute, + }, + ).json() + assert len(events) == 1 + assert events[0]["id"] == event_id + + events = client.get( + "/events/search", + params={ + "search_type": "similarity", + "event_id": event_id, + "attributes": "%E4%B8%AD%E6%96%87%E6%A0%87%E7%AD%BE", + }, + ).json() + assert len(events) == 1 + assert events[0]["id"] == event_id + + def test_similarity_search_hides_unauthorized_anchor_event(self): + mock_embeddings = Mock() + self.app.frigate_config.semantic_search.enabled = True + self.app.embeddings = mock_embeddings + + with AuthTestClient(self.app) as client: + super().insert_mock_event("hidden.anchor", camera="back_door") + response = client.get( + "/events/search", + params={ + "search_type": "similarity", + "event_id": "hidden.anchor", + }, + ) + + assert response.status_code == 404 + assert response.json()["message"] == "Event not found" + mock_embeddings.search_thumbnail.assert_not_called() + + def test_get_good_event(self): + id = "123456.random" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + event = client.get(f"/events/{id}").json() + + assert event + assert event["id"] == id + assert event["id"] == model_to_dict(Event.get(Event.id == id))["id"] + + def test_get_bad_event(self): + id = "123456.random" + bad_id = "654321.other" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + event_response = client.get(f"/events/{bad_id}") + assert event_response.status_code == 404 + assert event_response.json() == "Event not found" + + def test_delete_event(self): + id = "123456.random" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + event = client.get(f"/events/{id}").json() + assert event + assert event["id"] == id + response = client.delete(f"/events/{id}", headers={"remote-role": "admin"}) + assert response.status_code == 200 + event_after_delete = client.get(f"/events/{id}") + assert event_after_delete.status_code == 404 + + def test_event_retention(self): + id = "123456.random" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + client.post(f"/events/{id}/retain", headers={"remote-role": "admin"}) + event = client.get(f"/events/{id}").json() + assert event + assert event["id"] == id + assert event["retain_indefinitely"] is True + client.delete(f"/events/{id}/retain", headers={"remote-role": "admin"}) + event = client.get(f"/events/{id}").json() + assert event + assert event["id"] == id + assert event["retain_indefinitely"] is False + + def test_event_time_filtering(self): + morning_id = "123456.random" + evening_id = "654321.random" + morning = 1656590400 # 06/30/2022 6 am (GMT) + evening = 1656633600 # 06/30/2022 6 pm (GMT) + + with AuthTestClient(self.app) as client: + super().insert_mock_event(morning_id, morning) + super().insert_mock_event(evening_id, evening) + # both events come back + events = client.get("/events").json() + assert events + assert len(events) == 2 + # morning event is excluded + events = client.get( + "/events", + params={"time_range": "07:00,24:00"}, + ).json() + assert events + assert len(events) == 1 + # evening event is excluded + events = client.get( + "/events", + params={"time_range": "00:00,18:00"}, + ).json() + assert events + assert len(events) == 1 + + def test_set_delete_sub_label(self): + mock_event_updater = Mock(spec=EventMetadataPublisher) + app = super().create_app(event_metadata_publisher=mock_event_updater) + id = "123456.random" + sub_label = "sub" + + def update_event(payload: Any, topic: str): + event = Event.get(id=id) + event.sub_label = payload[1] + event.save() + + mock_event_updater.publish.side_effect = update_event + + with AuthTestClient(app) as client: + super().insert_mock_event(id) + new_sub_label_response = client.post( + f"/events/{id}/sub_label", + json={"subLabel": sub_label}, + headers={"remote-role": "admin"}, + ) + assert new_sub_label_response.status_code == 200 + event = client.get(f"/events/{id}").json() + assert event + assert event["id"] == id + assert event["sub_label"] == sub_label + empty_sub_label_response = client.post( + f"/events/{id}/sub_label", + json={"subLabel": ""}, + headers={"remote-role": "admin"}, + ) + assert empty_sub_label_response.status_code == 200 + event = client.get(f"/events/{id}").json() + assert event + assert event["id"] == id + assert event["sub_label"] == None + + def test_sub_label_list(self): + mock_event_updater = Mock(spec=EventMetadataPublisher) + app = super().create_app(event_metadata_publisher=mock_event_updater) + app.event_metadata_publisher = mock_event_updater + id = "123456.random" + sub_label = "sub" + + def update_event(payload: Any, _: str): + event = Event.get(id=id) + event.sub_label = payload[1] + event.save() + + mock_event_updater.publish.side_effect = update_event + + with AuthTestClient(app) as client: + super().insert_mock_event(id) + client.post( + f"/events/{id}/sub_label", + json={"subLabel": sub_label}, + headers={"remote-role": "admin"}, + ) + sub_labels = client.get("/sub_labels").json() + assert sub_labels + assert sub_labels == [sub_label] + + #################################################################################################################### + ################################### GET /metrics Endpoint ######################################################### + #################################################################################################################### + def test_get_metrics(self): + """ensure correct prometheus metrics api response""" + with AuthTestClient(self.app) as client: + ts_start = datetime.now().timestamp() + ts_end = ts_start + 30 + _insert_mock_event( + id="abcde.random", start=ts_start, end=ts_end, retain=True + ) + _insert_mock_event( + id="01234.random", start=ts_start, end=ts_end, retain=True + ) + _insert_mock_event( + id="56789.random", start=ts_start, end=ts_end, retain=True + ) + _insert_mock_event( + id="101112.random", + label="outside", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="131415.random", + label="outside", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="161718.random", + camera="porch", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="192021.random", + camera="porch", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="222324.random", + camera="porch", + label="inside", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="252627.random", + camera="porch", + label="inside", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="282930.random", + label="inside", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="313233.random", + label="inside", + start=ts_start, + end=ts_end, + retain=True, + ) + + stats_emitter = Mock(spec=StatsEmitter) + stats_emitter.get_latest_stats.return_value = self.test_stats + self.app.stats_emitter = stats_emitter + event = client.get("/metrics") + + assert "# TYPE frigate_detection_total_fps gauge" in event.text + assert "frigate_detection_total_fps 13.7" in event.text + assert ( + "# HELP frigate_camera_events_total Count of camera events since exporter started" + in event.text + ) + assert "# TYPE frigate_camera_events_total counter" in event.text + assert ( + 'frigate_camera_events_total{camera="front_door",label="Mock"} 3.0' + in event.text + ) + assert ( + 'frigate_camera_events_total{camera="front_door",label="inside"} 2.0' + in event.text + ) + assert ( + 'frigate_camera_events_total{camera="front_door",label="outside"} 2.0' + in event.text + ) + assert ( + 'frigate_camera_events_total{camera="porch",label="Mock"} 2.0' in event.text + ) + assert 'frigate_camera_events_total{camera="porch",label="inside"} 2.0' diff --git a/frigate/test/http_api/test_http_export.py b/frigate/test/http_api/test_http_export.py new file mode 100644 index 0000000..e0ceec5 --- /dev/null +++ b/frigate/test/http_api/test_http_export.py @@ -0,0 +1,1433 @@ +import os +import tempfile +from unittest.mock import patch + +from frigate.jobs.export import ( + ExportJob, + get_export_job_manager, + reap_stale_exports, + start_export_job, +) +from frigate.models import Export, ExportCase, Previews, Recordings +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpExport(BaseTestHttp): + def setUp(self): + super().setUp([Export, ExportCase, Previews, Recordings]) + self.minimal_config["cameras"]["backyard"] = { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + self.app = super().create_app() + + def tearDown(self): + self.app.dependency_overrides.clear() + super().tearDown() + + def _insert_recording( + self, + recording_id: str, + camera: str, + start_time: float, + end_time: float, + ) -> None: + Recordings.create( + id=recording_id, + camera=camera, + path=f"/tmp/{recording_id}.mp4", + start_time=start_time, + end_time=end_time, + duration=end_time - start_time, + motion=0, + objects=0, + dBFS=0, + segment_size=1, + regions=0, + motion_heatmap=[], + ) + + def test_create_export_case_uses_wall_clock_time(self): + with patch("frigate.api.export.time.time", return_value=1234.5): + with AuthTestClient(self.app) as client: + response = client.post( + "/cases", + json={ + "name": "Investigation", + "description": "A test case", + }, + ) + + assert response.status_code == 200 + response_json = response.json() + assert response_json["created_at"] == 1234.5 + assert response_json["updated_at"] == 1234.5 + + case = ExportCase.get(ExportCase.id == response_json["id"]) + assert case.created_at.timestamp() == 1234.5 + assert case.updated_at.timestamp() == 1234.5 + + def test_update_export_case_refreshes_updated_at(self): + case = ExportCase.create( + id="case123", + name="Old name", + description="Old description", + created_at=10, + updated_at=10, + ) + + with patch("frigate.api.export.time.time", return_value=2222.0): + with AuthTestClient(self.app) as client: + response = client.patch( + f"/cases/{case.id}", + json={"name": "New name", "description": "Updated"}, + ) + + assert response.status_code == 200 + + refreshed = ExportCase.get(ExportCase.id == case.id) + assert refreshed.name == "New name" + assert refreshed.description == "Updated" + assert refreshed.updated_at.timestamp() == 2222.0 + + def test_delete_export_case_delete_exports_cancels_queued_jobs(self): + case = ExportCase.create( + id="case_delete_me", + name="Delete me", + description="", + created_at=10, + updated_at=10, + ) + other_case = ExportCase.create( + id="case_keep_me", + name="Keep me", + description="", + created_at=20, + updated_at=20, + ) + + with tempfile.TemporaryDirectory() as tmpdir: + video_path = os.path.join(tmpdir, "case_export.mp4") + thumb_path = os.path.join(tmpdir, "case_export.webp") + other_video_path = os.path.join(tmpdir, "other_export.mp4") + other_thumb_path = os.path.join(tmpdir, "other_export.webp") + + with open(video_path, "wb") as handle: + handle.write(b"case") + with open(thumb_path, "wb") as handle: + handle.write(b"thumb") + with open(other_video_path, "wb") as handle: + handle.write(b"other") + with open(other_thumb_path, "wb") as handle: + handle.write(b"thumb") + + Export.create( + id="export_in_case", + camera="front_door", + name="Case export", + date=100, + video_path=video_path, + thumb_path=thumb_path, + in_progress=False, + export_case=case, + ) + Export.create( + id="export_other_case", + camera="front_door", + name="Other export", + date=110, + video_path=other_video_path, + thumb_path=other_thumb_path, + in_progress=False, + export_case=other_case, + ) + + with ( + patch("frigate.jobs.export._job_manager", None), + patch( + "frigate.jobs.export.ExportJobManager.ensure_started", + autospec=True, + return_value=None, + ), + ): + start_export_job( + self.app.frigate_config, + ExportJob( + id="queued_case_job", + camera="front_door", + export_case_id=case.id, + request_start_time=100, + request_end_time=120, + ), + ) + start_export_job( + self.app.frigate_config, + ExportJob( + id="queued_other_job", + camera="front_door", + export_case_id=other_case.id, + request_start_time=130, + request_end_time=150, + ), + ) + + manager = get_export_job_manager(self.app.frigate_config) + assert {job.id for job in manager.list_active_jobs()} == { + "queued_case_job", + "queued_other_job", + } + + with AuthTestClient(self.app) as client: + response = client.delete(f"/cases/{case.id}?delete_exports=true") + + assert response.status_code == 200 + assert ExportCase.get_or_none(ExportCase.id == case.id) is None + assert ExportCase.get_or_none(ExportCase.id == other_case.id) is not None + assert Export.get_or_none(Export.id == "export_in_case") is None + assert Export.get_or_none(Export.id == "export_other_case") is not None + assert not os.path.exists(video_path) + assert not os.path.exists(thumb_path) + + cancelled_job = manager.get_job("queued_case_job") + assert cancelled_job is not None + assert cancelled_job.status == "cancelled" + + remaining_job = manager.get_job("queued_other_job") + assert remaining_job is not None + assert remaining_job.status == "queued" + assert [job.id for job in manager.list_active_jobs()] == [ + "queued_other_job" + ] + + def test_batch_export_creates_case_and_reports_partial_success(self): + self._insert_recording("rec-front", "front_door", 100, 200) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + "friendly_name": "Incident - Front Door", + }, + { + "camera": "backyard", + "start_time": 110, + "end_time": 150, + "friendly_name": "Incident - Backyard", + }, + ], + "new_case_name": "Case Alpha", + "new_case_description": "Batch export", + }, + ) + + assert response.status_code == 202 + response_json = response.json() + assert len(response_json["export_ids"]) == 1 + assert response_json["results"] == [ + { + "camera": "front_door", + "export_id": response_json["export_ids"][0], + "success": True, + "status": "queued", + "error": None, + "item_index": 0, + "client_item_id": None, + }, + { + "camera": "backyard", + "export_id": None, + "success": False, + "status": None, + "error": "No recordings found for time range", + "item_index": 1, + "client_item_id": None, + }, + ] + start_export_job.assert_called_once() + + case = ExportCase.get(ExportCase.id == response_json["export_case_id"]) + assert case.name == "Case Alpha" + assert case.description == "Batch export" + + def test_single_export_is_queued_immediately(self): + self._insert_recording("rec-front", "front_door", 100, 200) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/export/front_door/start/110/end/150", + json={ + "name": "Queued export", + }, + ) + + assert response.status_code == 202 + response_json = response.json() + assert response_json["success"] is True + assert response_json["status"] == "queued" + assert response_json["export_id"].startswith("front_door_") + start_export_job.assert_called_once() + + def test_single_export_returns_503_when_queue_full(self): + self._insert_recording("rec-front", "front_door", 100, 200) + + from frigate.jobs.export import ExportQueueFullError + + with patch( + "frigate.api.export.start_export_job", + side_effect=ExportQueueFullError("Export queue is full"), + ): + with AuthTestClient(self.app) as client: + response = client.post( + "/export/front_door/start/110/end/150", + json={ + "name": "Rejected export", + }, + ) + + assert response.status_code == 503 + response_json = response.json() + assert response_json["success"] is False + assert "queue is full" in response_json["message"].lower() + + def test_batch_export_returns_503_when_queue_cannot_fit_batch(self): + self._insert_recording("rec-front", "front_door", 100, 200) + self._insert_recording("rec-back", "backyard", 100, 200) + + with patch( + "frigate.api.export.available_export_queue_slots", + return_value=1, + ): + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + }, + { + "camera": "backyard", + "start_time": 110, + "end_time": 150, + }, + ], + "new_case_name": "Overflow Case", + }, + ) + + assert response.status_code == 503 + assert response.json()["success"] is False + start_export_job.assert_not_called() + + # Empty case should NOT have been created + assert ExportCase.select().count() == 0 + + def test_get_active_export_jobs_returns_queue_state(self): + queued_job = ExportJob( + id="front_door_queued", + camera="front_door", + status="queued", + request_start_time=100, + request_end_time=150, + ) + + with patch( + "frigate.api.export.list_active_export_jobs", + return_value=[queued_job], + ): + with AuthTestClient(self.app) as client: + response = client.get("/jobs/export") + + assert response.status_code == 200 + assert response.json() == [queued_job.to_dict()] + + def test_reap_stale_exports_deletes_rows_with_no_file(self): + with tempfile.TemporaryDirectory() as tmpdir: + stale_video = os.path.join(tmpdir, "stale.mp4") + stale_thumb = os.path.join(tmpdir, "stale.webp") + # stale_video is intentionally NOT created + with open(stale_thumb, "w") as handle: + handle.write("thumb") + + Export.create( + id="stale_no_file", + camera="front_door", + name="Stuck export", + date=100, + video_path=stale_video, + thumb_path=stale_thumb, + in_progress=True, + ) + + reap_stale_exports() + + assert Export.get_or_none(Export.id == "stale_no_file") is None + assert not os.path.exists(stale_thumb) + + def test_reap_stale_exports_recovers_rows_with_file(self): + with tempfile.TemporaryDirectory() as tmpdir: + intact_video = os.path.join(tmpdir, "intact.mp4") + intact_thumb = os.path.join(tmpdir, "intact.webp") + with open(intact_video, "wb") as handle: + handle.write(b"not actually an mp4 but non-empty") + with open(intact_thumb, "wb") as handle: + handle.write(b"thumb") + + case = ExportCase.create( + id="case_for_stale", + name="Curated case", + description="", + created_at=10, + updated_at=10, + ) + + Export.create( + id="stale_with_file", + camera="front_door", + name="Recoverable export", + date=200, + video_path=intact_video, + thumb_path=intact_thumb, + in_progress=True, + export_case=case, + ) + + reap_stale_exports() + + recovered = Export.get(Export.id == "stale_with_file") + assert recovered.in_progress is False + # Case link must be cleared so the user re-triages the recovered row + assert recovered.export_case is None + # The case itself is untouched + assert ExportCase.get_or_none(ExportCase.id == "case_for_stale") is not None + # Recovered files must NOT be unlinked + assert os.path.exists(intact_video) + assert os.path.exists(intact_thumb) + + def test_reap_stale_exports_delete_path_severs_case_link(self): + with tempfile.TemporaryDirectory() as tmpdir: + missing_video = os.path.join(tmpdir, "missing.mp4") + # file intentionally not created + + case = ExportCase.create( + id="case_losing_member", + name="Case losing a member", + description="", + created_at=20, + updated_at=20, + ) + + Export.create( + id="stale_in_case_no_file", + camera="front_door", + name="Stuck and in a case", + date=250, + video_path=missing_video, + thumb_path="", + in_progress=True, + export_case=case, + ) + + reap_stale_exports() + + # The export row is gone entirely + assert Export.get_or_none(Export.id == "stale_in_case_no_file") is None + # The case stays but has no exports pointing at it + remaining_case = ExportCase.get(ExportCase.id == "case_losing_member") + assert list(remaining_case.exports) == [] + + def test_reap_stale_exports_deletes_rows_with_empty_file(self): + with tempfile.TemporaryDirectory() as tmpdir: + empty_video = os.path.join(tmpdir, "empty.mp4") + # Create a zero-byte file — partial ffmpeg output + open(empty_video, "w").close() + + Export.create( + id="stale_empty_file", + camera="front_door", + name="Zero byte export", + date=300, + video_path=empty_video, + thumb_path="", + in_progress=True, + ) + + reap_stale_exports() + + assert Export.get_or_none(Export.id == "stale_empty_file") is None + assert not os.path.exists(empty_video) + + def test_reap_stale_exports_skips_completed_rows(self): + with tempfile.TemporaryDirectory() as tmpdir: + done_video = os.path.join(tmpdir, "done.mp4") + with open(done_video, "wb") as handle: + handle.write(b"done") + + Export.create( + id="already_done", + camera="front_door", + name="Completed export", + date=400, + video_path=done_video, + thumb_path="", + in_progress=False, + ) + + reap_stale_exports() + + row = Export.get(Export.id == "already_done") + assert row.in_progress is False + assert os.path.exists(done_video) + + def test_batch_export_without_case_goes_to_uncategorized(self): + """Exports without a case target go to uncategorized.""" + self._insert_recording("rec-front", "front_door", 100, 400) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ): + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + } + ], + }, + ) + + assert response.status_code == 202 + response_json = response.json() + assert response_json["export_case_id"] is None + assert ExportCase.select().count() == 0 + + # --- /exports/batch (item-shaped multi-export) --------------------------- + + def test_batch_export_happy_path_creates_case_and_queues_all(self): + self._insert_recording("rec-front", "front_door", 100, 400) + self._insert_recording("rec-back", "backyard", 100, 400) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + }, + { + "camera": "front_door", + "start_time": 200, + "end_time": 240, + }, + { + "camera": "backyard", + "start_time": 300, + "end_time": 340, + }, + ], + "new_case_name": "Incident Apr 11", + "new_case_description": "Review items", + }, + ) + + assert response.status_code == 202 + response_json = response.json() + assert len(response_json["export_ids"]) == 3 + assert all(r["success"] for r in response_json["results"]) + assert [r["item_index"] for r in response_json["results"]] == [0, 1, 2] + assert start_export_job.call_count == 3 + + case = ExportCase.get(ExportCase.id == response_json["export_case_id"]) + assert case.name == "Incident Apr 11" + assert case.description == "Review items" + + def test_batch_export_existing_case_does_not_create_new_case(self): + self._insert_recording("rec-front", "front_door", 100, 400) + ExportCase.create( + id="existing_case", + name="Existing", + description="", + created_at=10, + updated_at=10, + ) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ): + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + } + ], + "export_case_id": "existing_case", + }, + ) + + assert response.status_code == 202 + assert response.json()["export_case_id"] == "existing_case" + # No additional case was created + assert ExportCase.select().count() == 1 + + def test_batch_export_empty_items_rejected(self): + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={"items": [], "new_case_name": "Empty"}, + ) + + assert response.status_code == 422 + + def test_batch_export_over_limit_rejected(self): + items = [ + {"camera": "front_door", "start_time": 100 + i, "end_time": 100 + i + 5} + for i in range(51) + ] + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={"items": items, "new_case_name": "Too many"}, + ) + + assert response.status_code == 422 + + def test_batch_export_end_before_start_rejected(self): + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 200, + "end_time": 100, + } + ], + "new_case_name": "Bad range", + }, + ) + + assert response.status_code == 422 + assert ( + response.json()["detail"][0]["msg"] + == "Value error, end_time must be after start_time" + ) + + def test_batch_export_non_admin_without_case_goes_to_uncategorized(self): + """Non-admin batch exports go to uncategorized.""" + self._insert_recording("rec-front", "front_door", 100, 400) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ): + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + headers={"remote-user": "viewer", "remote-role": "viewer"}, + json={ + "items": [ + { + "camera": "front_door", + "start_time": 100, + "end_time": 150, + } + ], + }, + ) + + assert response.status_code == 202 + response_json = response.json() + assert response_json["export_case_id"] is None + assert ExportCase.select().count() == 0 + + def test_batch_export_camera_access_denied_fails_closed(self): + from fastapi import Request + + from frigate.api.auth import get_allowed_cameras_for_filter + + self._insert_recording("rec-front", "front_door", 100, 400) + + async def restricted(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = restricted + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + }, + { + "camera": "backyard", # not in allowed list + "start_time": 110, + "end_time": 150, + }, + ], + "new_case_name": "Nope", + }, + ) + + assert response.status_code == 403 + start_export_job.assert_not_called() + # No case created + assert ExportCase.select().count() == 0 + + def test_batch_export_case_not_found(self): + self._insert_recording("rec-front", "front_door", 100, 400) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + } + ], + "export_case_id": "does_not_exist", + }, + ) + + assert response.status_code == 404 + + def test_batch_export_per_item_missing_recordings_partial_success(self): + self._insert_recording("rec-front", "front_door", 100, 200) + # backyard has no recordings at all + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + }, + { + "camera": "backyard", + "start_time": 110, + "end_time": 150, + }, + ], + "new_case_name": "Partial", + }, + ) + + assert response.status_code == 202 + response_json = response.json() + assert len(response_json["export_ids"]) == 1 + results_by_camera = {r["camera"]: r for r in response_json["results"]} + assert results_by_camera["front_door"]["success"] is True + assert results_by_camera["backyard"]["success"] is False + assert ( + results_by_camera["backyard"]["error"] + == "No recordings found for time range" + ) + start_export_job.assert_called_once() + + # Case is still created because at least one item succeeded + assert ( + ExportCase.get(ExportCase.id == response_json["export_case_id"]) is not None + ) + + def test_batch_export_same_camera_different_ranges_one_missing(self): + # Recording covers 100-200 only. First item fits, second does not. + self._insert_recording("rec-front", "front_door", 100, 200) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + }, + { + "camera": "front_door", + "start_time": 500, + "end_time": 540, + }, + ], + "new_case_name": "Split recordings", + }, + ) + + assert response.status_code == 202 + response_json = response.json() + assert len(response_json["export_ids"]) == 1 + results = response_json["results"] + assert results[0]["success"] is True + assert results[0]["item_index"] == 0 + assert results[1]["success"] is False + assert results[1]["item_index"] == 1 + assert results[1]["error"] == "No recordings found for time range" + # Both results carry the same camera — item_index is the only way + # the client can tell them apart. + assert results[0]["camera"] == results[1]["camera"] == "front_door" + start_export_job.assert_called_once() + + def test_batch_export_all_missing_recordings_rolls_back_case(self): + # No recordings inserted at all + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + } + ], + "new_case_name": "Should rollback", + }, + ) + + assert response.status_code == 400 + start_export_job.assert_not_called() + assert ExportCase.select().count() == 0 + + def test_batch_export_preflight_queue_full(self): + self._insert_recording("rec-front", "front_door", 100, 400) + self._insert_recording("rec-back", "backyard", 100, 400) + + with patch( + "frigate.api.export.available_export_queue_slots", + return_value=1, + ): + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + }, + { + "camera": "backyard", + "start_time": 110, + "end_time": 150, + }, + ], + "new_case_name": "Queue full", + }, + ) + + assert response.status_code == 503 + start_export_job.assert_not_called() + assert ExportCase.select().count() == 0 + + def test_batch_export_all_enqueue_calls_fail_rolls_back_case(self): + self._insert_recording("rec-front", "front_door", 100, 400) + + def boom(_config, _job): + raise RuntimeError("simulated enqueue failure") + + with patch( + "frigate.api.export.start_export_job", + side_effect=boom, + ): + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + } + ], + "new_case_name": "Will fail", + }, + ) + + assert response.status_code == 202 + response_json = response.json() + assert response_json["export_ids"] == [] + assert response_json["export_case_id"] is None + assert ExportCase.select().count() == 0 + + def test_batch_export_rejects_invalid_image_path(self): + self._insert_recording("rec-front", "front_door", 100, 400) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + "image_path": "/etc/passwd", + } + ], + "new_case_name": "Bad image", + }, + ) + + assert response.status_code == 400 + assert ExportCase.select().count() == 0 + + def test_batch_export_non_admin_can_queue(self): + self._insert_recording("rec-front", "front_door", 100, 400) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ): + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + headers={"remote-user": "viewer", "remote-role": "viewer"}, + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + } + ], + "new_case_name": "Viewer export", + }, + ) + + assert response.status_code == 202 + assert len(response.json()["export_ids"]) == 1 + + def test_batch_export_non_admin_cannot_attach_to_existing_case(self): + """Non-admins can create cases via new_case_name but cannot attach + to existing cases they did not create. Closes a write-path hole that + would otherwise be reachable through the unfiltered GET /cases list. + """ + self._insert_recording("rec-front", "front_door", 100, 400) + ExportCase.create( + id="admins_only_case", + name="Admins only", + description="", + created_at=10, + updated_at=10, + ) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + headers={"remote-user": "viewer", "remote-role": "viewer"}, + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + } + ], + "export_case_id": "admins_only_case", + }, + ) + + assert response.status_code == 403 + start_export_job.assert_not_called() + # No exports should have been created in the target case + assert Export.select().count() == 0 + + def test_batch_export_admin_can_attach_to_existing_case(self): + self._insert_recording("rec-front", "front_door", 100, 400) + ExportCase.create( + id="shared_case", + name="Shared", + description="", + created_at=10, + updated_at=10, + ) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ): + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + } + ], + "export_case_id": "shared_case", + }, + ) + + assert response.status_code == 202 + assert response.json()["export_case_id"] == "shared_case" + # No additional case created + assert ExportCase.select().count() == 1 + + def test_batch_export_roundtrips_client_item_id(self): + self._insert_recording("rec-front", "front_door", 100, 400) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ): + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/batch", + json={ + "items": [ + { + "camera": "front_door", + "start_time": 110, + "end_time": 150, + "client_item_id": "review-123", + } + ], + "new_case_name": "Client id test", + }, + ) + + assert response.status_code == 202 + assert response.json()["results"][0]["client_item_id"] == "review-123" + + def test_single_export_non_admin_cannot_attach_to_existing_case(self): + """The single-export route has the same hole: non-admins should not + be able to smuggle exports into an existing case via export_case_id. + Admin-gating this matches /exports/batch. + """ + self._insert_recording("rec-front", "front_door", 100, 400) + ExportCase.create( + id="admins_only_case", + name="Admins only", + description="", + created_at=10, + updated_at=10, + ) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ) as start_export_job: + with AuthTestClient(self.app) as client: + response = client.post( + "/export/front_door/start/110/end/150", + headers={"remote-user": "viewer", "remote-role": "viewer"}, + json={"export_case_id": "admins_only_case"}, + ) + + assert response.status_code == 403 + start_export_job.assert_not_called() + assert Export.select().count() == 0 + + def test_single_export_non_admin_can_still_export_without_case(self): + """Regression guard: the admin gate only applies to export_case_id, + not to single exports in general. Non-admins should still be able + to start a single export for a camera they have access to. + """ + self._insert_recording("rec-front", "front_door", 100, 400) + + with patch( + "frigate.api.export.start_export_job", + side_effect=lambda _config, job: job.id, + ): + with AuthTestClient(self.app) as client: + response = client.post( + "/export/front_door/start/110/end/150", + headers={"remote-user": "viewer", "remote-role": "viewer"}, + json={}, + ) + + assert response.status_code == 202 + assert response.json()["success"] is True + + # ── Bulk delete exports ──────────────────────────────────────── + + def test_bulk_delete_exports_success(self): + """All IDs exist, none in-progress → 200, all deleted.""" + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path="/tmp/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=False, + ) + Export.create( + id="exp2", + camera="front_door", + name="export_2", + date=200, + video_path="/tmp/exp2.mp4", + thumb_path="/tmp/exp2.jpg", + in_progress=False, + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/delete", + json={"ids": ["exp1", "exp2"]}, + ) + + assert response.status_code == 200 + assert response.json()["success"] is True + assert Export.select().count() == 0 + + def test_bulk_delete_exports_single_item(self): + """Regression: single-item delete via batch endpoint.""" + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path="/tmp/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=False, + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/delete", + json={"ids": ["exp1"]}, + ) + + assert response.status_code == 200 + assert Export.select().count() == 0 + + def test_bulk_delete_exports_some_missing(self): + """Some IDs don't exist → 404, nothing deleted.""" + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path="/tmp/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=False, + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/delete", + json={"ids": ["exp1", "nonexistent"]}, + ) + + assert response.status_code == 404 + # Nothing deleted + assert Export.select().count() == 1 + + def test_bulk_delete_exports_all_missing(self): + """All IDs don't exist → 404.""" + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/delete", + json={"ids": ["nope1", "nope2"]}, + ) + + assert response.status_code == 404 + + def test_bulk_delete_exports_in_progress(self): + """Some exports in-progress → 400, nothing deleted.""" + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path=f"{os.environ.get('EXPORT_DIR', '/media/frigate/exports')}/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=True, + ) + + with patch( + "frigate.api.export._get_files_in_use", + return_value={"exp1.mp4"}, + ): + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/delete", + json={"ids": ["exp1"]}, + ) + + assert response.status_code == 400 + assert Export.select().count() == 1 + + def test_bulk_delete_exports_non_admin_rejected(self): + """Non-admin users cannot bulk delete.""" + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path="/tmp/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=False, + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/delete", + headers={"remote-user": "viewer", "remote-role": "viewer"}, + json={"ids": ["exp1"]}, + ) + + assert response.status_code == 403 + assert Export.select().count() == 1 + + # ── Bulk reassign exports ────────────────────────────────────── + + def test_bulk_reassign_exports_to_case(self): + """All IDs exist, case exists → 200, all reassigned.""" + ExportCase.create( + id="case1", + name="Test Case", + description="", + created_at=10, + updated_at=10, + ) + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path="/tmp/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=False, + ) + Export.create( + id="exp2", + camera="front_door", + name="export_2", + date=200, + video_path="/tmp/exp2.mp4", + thumb_path="/tmp/exp2.jpg", + in_progress=False, + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/reassign", + json={"ids": ["exp1", "exp2"], "export_case_id": "case1"}, + ) + + assert response.status_code == 200 + assert response.json()["success"] is True + for exp_id in ["exp1", "exp2"]: + exp = Export.get(Export.id == exp_id) + assert exp.export_case_id == "case1" + + def test_bulk_reassign_exports_to_null(self): + """Reassign to null (uncategorize) → 200.""" + ExportCase.create( + id="case1", + name="Test Case", + description="", + created_at=10, + updated_at=10, + ) + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path="/tmp/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=False, + export_case="case1", + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/reassign", + json={"ids": ["exp1"], "export_case_id": None}, + ) + + assert response.status_code == 200 + exp = Export.get(Export.id == "exp1") + assert exp.export_case_id is None + + def test_bulk_reassign_exports_single_item(self): + """Regression: single-item reassign via batch endpoint.""" + ExportCase.create( + id="case1", + name="Test Case", + description="", + created_at=10, + updated_at=10, + ) + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path="/tmp/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=False, + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/reassign", + json={"ids": ["exp1"], "export_case_id": "case1"}, + ) + + assert response.status_code == 200 + exp = Export.get(Export.id == "exp1") + assert exp.export_case_id == "case1" + + def test_bulk_reassign_exports_some_missing(self): + """Some IDs don't exist → 404, nothing reassigned.""" + ExportCase.create( + id="case1", + name="Test Case", + description="", + created_at=10, + updated_at=10, + ) + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path="/tmp/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=False, + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/reassign", + json={ + "ids": ["exp1", "nonexistent"], + "export_case_id": "case1", + }, + ) + + assert response.status_code == 404 + # Nothing reassigned + exp = Export.get(Export.id == "exp1") + assert exp.export_case_id is None + + def test_bulk_reassign_exports_case_not_found(self): + """Target case doesn't exist → 404.""" + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path="/tmp/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=False, + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/reassign", + json={"ids": ["exp1"], "export_case_id": "nonexistent"}, + ) + + assert response.status_code == 404 + exp = Export.get(Export.id == "exp1") + assert exp.export_case_id is None + + def test_bulk_reassign_exports_non_admin_rejected(self): + """Non-admin users cannot bulk reassign.""" + Export.create( + id="exp1", + camera="front_door", + name="export_1", + date=100, + video_path="/tmp/exp1.mp4", + thumb_path="/tmp/exp1.jpg", + in_progress=False, + ) + + with AuthTestClient(self.app) as client: + response = client.post( + "/exports/reassign", + headers={"remote-user": "viewer", "remote-role": "viewer"}, + json={"ids": ["exp1"], "export_case_id": None}, + ) + + assert response.status_code == 403 diff --git a/frigate/test/http_api/test_http_keyframe_analysis.py b/frigate/test/http_api/test_http_keyframe_analysis.py new file mode 100644 index 0000000..6a49e10 --- /dev/null +++ b/frigate/test/http_api/test_http_keyframe_analysis.py @@ -0,0 +1,58 @@ +from unittest.mock import AsyncMock, patch + +from frigate.models import Event, Recordings, ReviewSegment +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpKeyframeAnalysis(BaseTestHttp): + def setUp(self): + super().setUp([Event, Recordings, ReviewSegment]) + + def test_invalid_camera_returns_404(self): + app = super().create_app() + with AuthTestClient(app) as client: + response = client.get("/keyframe_analysis?camera=does_not_exist") + assert response.status_code == 404 + + def test_record_disabled_returns_neutral(self): + # default minimal_config has recording disabled + app = super().create_app() + with AuthTestClient(app) as client: + response = client.get("/keyframe_analysis?camera=front_door") + assert response.status_code == 200 + assert response.json()["severity"] == "record_disabled" + + def test_probes_record_input_and_returns_severity(self): + self.minimal_config["cameras"]["front_door"]["ffmpeg"]["inputs"] = [ + { + "path": "rtsp://10.0.0.1:554/record", + "roles": ["detect", "record"], + } + ] + self.minimal_config["cameras"]["front_door"]["record"] = {"enabled": True} + app = super().create_app() + + canned = { + "severity": "ok", + "keyframe_count": 5, + "max_gap": 1.0, + "mean_gap": 1.0, + "min_gap": 1.0, + "segment_time": 10, + "duration_observed": 4.0, + "thresholds": {"warning": 4.0, "error": 10}, + } + + with patch( + "frigate.api.camera.analyze_record_keyframes", + AsyncMock(return_value=canned), + ) as mock_probe: + with AuthTestClient(app) as client: + response = client.get("/keyframe_analysis?camera=front_door") + + assert response.status_code == 200 + assert response.json()["severity"] == "ok" + # index matches the input carrying the record role ("Stream 1") + assert response.json()["stream_index"] == 0 + # the record-role input path was probed + assert mock_probe.await_args.args[1] == "rtsp://10.0.0.1:554/record" diff --git a/frigate/test/http_api/test_http_latest_frame.py b/frigate/test/http_api/test_http_latest_frame.py new file mode 100644 index 0000000..755ee6e --- /dev/null +++ b/frigate/test/http_api/test_http_latest_frame.py @@ -0,0 +1,107 @@ +import os +import shutil +from unittest.mock import MagicMock + +import cv2 +import numpy as np + +from frigate.output.preview import PREVIEW_CACHE_DIR, PREVIEW_FRAME_TYPE +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpLatestFrame(BaseTestHttp): + def setUp(self): + super().setUp([]) + self.app = super().create_app() + self.app.detected_frames_processor = MagicMock() + + if os.path.exists(PREVIEW_CACHE_DIR): + shutil.rmtree(PREVIEW_CACHE_DIR) + os.makedirs(PREVIEW_CACHE_DIR) + + def tearDown(self): + if os.path.exists(PREVIEW_CACHE_DIR): + shutil.rmtree(PREVIEW_CACHE_DIR) + super().tearDown() + + def test_latest_frame_fallback_to_preview(self): + camera = "front_door" + # 1. Mock frame processor to return None (simulating offline/missing frame) + self.app.detected_frames_processor.get_current_frame.return_value = None + # Return a timestamp that is after our dummy preview frame + self.app.detected_frames_processor.get_current_frame_time.return_value = ( + 1234567891.0 + ) + + # 2. Create a dummy preview file + dummy_frame = np.zeros((180, 320, 3), np.uint8) + cv2.putText( + dummy_frame, + "PREVIEW", + (50, 50), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (255, 255, 255), + 2, + ) + preview_path = os.path.join( + PREVIEW_CACHE_DIR, f"preview_{camera}-1234567890.0.{PREVIEW_FRAME_TYPE}" + ) + cv2.imwrite(preview_path, dummy_frame) + + with AuthTestClient(self.app) as client: + response = client.get(f"/{camera}/latest.webp") + assert response.status_code == 200 + assert response.headers.get("X-Frigate-Offline") == "true" + # Verify we got an image (webp) + assert response.headers.get("content-type") == "image/webp" + + def test_latest_frame_no_fallback_when_live(self): + camera = "front_door" + # 1. Mock frame processor to return a live frame + dummy_frame = np.zeros((180, 320, 3), np.uint8) + self.app.detected_frames_processor.get_current_frame.return_value = dummy_frame + self.app.detected_frames_processor.get_current_frame_time.return_value = ( + 2000000000.0 # Way in the future + ) + + with AuthTestClient(self.app) as client: + response = client.get(f"/{camera}/latest.webp") + assert response.status_code == 200 + assert "X-Frigate-Offline" not in response.headers + + def test_latest_frame_stale_falls_back_to_preview(self): + camera = "front_door" + # 1. Mock frame processor to return a stale frame + dummy_frame = np.zeros((180, 320, 3), np.uint8) + self.app.detected_frames_processor.get_current_frame.return_value = dummy_frame + # Return a timestamp that is after our dummy preview frame, but way in the past + self.app.detected_frames_processor.get_current_frame_time.return_value = 1000.0 + + # 2. Create a dummy preview file + preview_path = os.path.join( + PREVIEW_CACHE_DIR, f"preview_{camera}-999.0.{PREVIEW_FRAME_TYPE}" + ) + cv2.imwrite(preview_path, dummy_frame) + + with AuthTestClient(self.app) as client: + response = client.get(f"/{camera}/latest.webp") + assert response.status_code == 200 + assert response.headers.get("X-Frigate-Offline") == "true" + + def test_latest_frame_no_preview_found(self): + camera = "front_door" + # 1. Mock frame processor to return None + self.app.detected_frames_processor.get_current_frame.return_value = None + + # 2. No preview file created + + with AuthTestClient(self.app) as client: + response = client.get(f"/{camera}/latest.webp") + # Should fall back to camera-error.jpg (which might not exist in test env, but let's see) + # If camera-error.jpg is not found, it returns 500 "Unable to get valid frame" in latest_frame + # OR it uses request.app.camera_error_image if already loaded. + + # Since we didn't provide camera-error.jpg, it might 500 if glob fails or return 500 if frame is None. + assert response.status_code in [200, 500] + assert "X-Frigate-Offline" not in response.headers diff --git a/frigate/test/http_api/test_http_media.py b/frigate/test/http_api/test_http_media.py new file mode 100644 index 0000000..b2d83cb --- /dev/null +++ b/frigate/test/http_api/test_http_media.py @@ -0,0 +1,529 @@ +"""Unit tests for recordings/media API endpoints.""" + +from datetime import UTC, datetime + +import pytz +from fastapi import Request + +from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user +from frigate.models import Recordings +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpMedia(BaseTestHttp): + """Test media API endpoints, particularly recordings with DST handling.""" + + def setUp(self): + """Set up test fixtures.""" + super().setUp([Recordings]) + self.app = super().create_app() + + # Mock get_current_user for all tests + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} + + self.app.dependency_overrides[get_current_user] = mock_get_current_user + + async def mock_get_allowed_cameras_for_filter(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) + + def tearDown(self): + """Clean up after tests.""" + self.app.dependency_overrides.clear() + super().tearDown() + + def test_recordings_summary_across_dst_spring_forward(self): + """ + Test recordings summary across spring DST transition (spring forward). + + In 2024, DST in America/New_York transitions on March 10, 2024 at 2:00 AM + Clocks spring forward from 2:00 AM to 3:00 AM (EST to EDT) + """ + tz = pytz.timezone("America/New_York") + + # March 9, 2024 at 12:00 PM EST (before DST) + march_9_noon = tz.localize(datetime(2024, 3, 9, 12, 0, 0)).timestamp() + + # March 10, 2024 at 12:00 PM EDT (after DST transition) + march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp() + + # March 11, 2024 at 12:00 PM EDT (after DST) + march_11_noon = tz.localize(datetime(2024, 3, 11, 12, 0, 0)).timestamp() + + with AuthTestClient(self.app) as client: + # Insert recordings for each day + Recordings.insert( + id="recording_march_9", + path="/media/recordings/march_9.mp4", + camera="front_door", + start_time=march_9_noon, + end_time=march_9_noon + 3600, # 1 hour recording + duration=3600, + motion=100, + objects=5, + ).execute() + + Recordings.insert( + id="recording_march_10", + path="/media/recordings/march_10.mp4", + camera="front_door", + start_time=march_10_noon, + end_time=march_10_noon + 3600, + duration=3600, + motion=150, + objects=8, + ).execute() + + Recordings.insert( + id="recording_march_11", + path="/media/recordings/march_11.mp4", + camera="front_door", + start_time=march_11_noon, + end_time=march_11_noon + 3600, + duration=3600, + motion=200, + objects=10, + ).execute() + + # Test recordings summary with America/New_York timezone + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "all"}, + ) + + assert response.status_code == 200 + summary = response.json() + + # Verify we get exactly 3 days + assert len(summary) == 3, f"Expected 3 days, got {len(summary)}" + + # Verify the correct dates are returned (API returns dict with True values) + assert "2024-03-09" in summary, f"Expected 2024-03-09 in {summary}" + assert "2024-03-10" in summary, f"Expected 2024-03-10 in {summary}" + assert "2024-03-11" in summary, f"Expected 2024-03-11 in {summary}" + assert summary["2024-03-09"] is True + assert summary["2024-03-10"] is True + assert summary["2024-03-11"] is True + + def test_recordings_summary_across_dst_fall_back(self): + """ + Test recordings summary across fall DST transition (fall back). + + In 2024, DST in America/New_York transitions on November 3, 2024 at 2:00 AM + Clocks fall back from 2:00 AM to 1:00 AM (EDT to EST) + """ + tz = pytz.timezone("America/New_York") + + # November 2, 2024 at 12:00 PM EDT (before DST transition) + nov_2_noon = tz.localize(datetime(2024, 11, 2, 12, 0, 0)).timestamp() + + # November 3, 2024 at 12:00 PM EST (after DST transition) + # Need to specify is_dst=False to get the time after fall back + nov_3_noon = tz.localize( + datetime(2024, 11, 3, 12, 0, 0), is_dst=False + ).timestamp() + + # November 4, 2024 at 12:00 PM EST (after DST) + nov_4_noon = tz.localize(datetime(2024, 11, 4, 12, 0, 0)).timestamp() + + with AuthTestClient(self.app) as client: + # Insert recordings for each day + Recordings.insert( + id="recording_nov_2", + path="/media/recordings/nov_2.mp4", + camera="front_door", + start_time=nov_2_noon, + end_time=nov_2_noon + 3600, + duration=3600, + motion=100, + objects=5, + ).execute() + + Recordings.insert( + id="recording_nov_3", + path="/media/recordings/nov_3.mp4", + camera="front_door", + start_time=nov_3_noon, + end_time=nov_3_noon + 3600, + duration=3600, + motion=150, + objects=8, + ).execute() + + Recordings.insert( + id="recording_nov_4", + path="/media/recordings/nov_4.mp4", + camera="front_door", + start_time=nov_4_noon, + end_time=nov_4_noon + 3600, + duration=3600, + motion=200, + objects=10, + ).execute() + + # Test recordings summary with America/New_York timezone + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "all"}, + ) + + assert response.status_code == 200 + summary = response.json() + + # Verify we get exactly 3 days + assert len(summary) == 3, f"Expected 3 days, got {len(summary)}" + + # Verify the correct dates are returned (API returns dict with True values) + assert "2024-11-02" in summary, f"Expected 2024-11-02 in {summary}" + assert "2024-11-03" in summary, f"Expected 2024-11-03 in {summary}" + assert "2024-11-04" in summary, f"Expected 2024-11-04 in {summary}" + assert summary["2024-11-02"] is True + assert summary["2024-11-03"] is True + assert summary["2024-11-04"] is True + + def test_recordings_summary_multiple_cameras_across_dst(self): + """ + Test recordings summary with multiple cameras across DST boundary. + """ + tz = pytz.timezone("America/New_York") + + # March 9, 2024 at 10:00 AM EST (before DST) + march_9_morning = tz.localize(datetime(2024, 3, 9, 10, 0, 0)).timestamp() + + # March 10, 2024 at 3:00 PM EDT (after DST transition) + march_10_afternoon = tz.localize(datetime(2024, 3, 10, 15, 0, 0)).timestamp() + + with AuthTestClient(self.app) as client: + # Override allowed cameras for this test to include both + async def mock_get_allowed_cameras_for_filter(_request: Request): + return ["front_door", "back_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) + + # Insert recordings for front_door on March 9 + Recordings.insert( + id="front_march_9", + path="/media/recordings/front_march_9.mp4", + camera="front_door", + start_time=march_9_morning, + end_time=march_9_morning + 3600, + duration=3600, + motion=100, + objects=5, + ).execute() + + # Insert recordings for back_door on March 10 + Recordings.insert( + id="back_march_10", + path="/media/recordings/back_march_10.mp4", + camera="back_door", + start_time=march_10_afternoon, + end_time=march_10_afternoon + 3600, + duration=3600, + motion=150, + objects=8, + ).execute() + + # Test with all cameras + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "all"}, + ) + + assert response.status_code == 200 + summary = response.json() + + # Verify we get both days + assert len(summary) == 2, f"Expected 2 days, got {len(summary)}" + assert "2024-03-09" in summary + assert "2024-03-10" in summary + assert summary["2024-03-09"] is True + assert summary["2024-03-10"] is True + + # Reset dependency override back to default single camera for other tests + async def reset_allowed_cameras(_request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + reset_allowed_cameras + ) + + def test_recordings_summary_at_dst_transition_time(self): + """ + Test recordings that span the exact DST transition time. + """ + tz = pytz.timezone("America/New_York") + + # March 10, 2024 at 1:00 AM EST (1 hour before DST transition) + # At 2:00 AM, clocks jump to 3:00 AM + before_transition = tz.localize(datetime(2024, 3, 10, 1, 0, 0)).timestamp() + + # Recording that spans the transition (1:00 AM to 3:30 AM EDT) + # This is 1.5 hours of actual time but spans the "missing" hour + after_transition = tz.localize(datetime(2024, 3, 10, 3, 30, 0)).timestamp() + + with AuthTestClient(self.app) as client: + Recordings.insert( + id="recording_during_transition", + path="/media/recordings/transition.mp4", + camera="front_door", + start_time=before_transition, + end_time=after_transition, + duration=after_transition - before_transition, + motion=100, + objects=5, + ).execute() + + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "all"}, + ) + + assert response.status_code == 200 + summary = response.json() + + # The recording should appear on March 10 + assert len(summary) == 1 + assert "2024-03-10" in summary + assert summary["2024-03-10"] is True + + def test_recordings_summary_utc_timezone(self): + """ + Test recordings summary with UTC timezone (no DST). + """ + # Use UTC timestamps directly + march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=UTC).timestamp() + march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=UTC).timestamp() + + with AuthTestClient(self.app) as client: + Recordings.insert( + id="recording_march_9_utc", + path="/media/recordings/march_9_utc.mp4", + camera="front_door", + start_time=march_9_utc, + end_time=march_9_utc + 3600, + duration=3600, + motion=100, + objects=5, + ).execute() + + Recordings.insert( + id="recording_march_10_utc", + path="/media/recordings/march_10_utc.mp4", + camera="front_door", + start_time=march_10_utc, + end_time=march_10_utc + 3600, + duration=3600, + motion=150, + objects=8, + ).execute() + + # Test with UTC timezone + response = client.get( + "/recordings/summary", params={"timezone": "utc", "cameras": "all"} + ) + + assert response.status_code == 200 + summary = response.json() + + # Verify we get both days + assert len(summary) == 2 + assert "2024-03-09" in summary + assert "2024-03-10" in summary + assert summary["2024-03-09"] is True + assert summary["2024-03-10"] is True + + def test_recordings_summary_no_recordings(self): + """ + Test recordings summary when no recordings exist. + """ + with AuthTestClient(self.app) as client: + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "all"}, + ) + + assert response.status_code == 200 + summary = response.json() + assert len(summary) == 0 + + def test_recordings_summary_single_camera_filter(self): + """ + Test recordings summary filtered to a single camera. + """ + tz = pytz.timezone("America/New_York") + march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp() + + with AuthTestClient(self.app) as client: + # Insert recordings for both cameras + Recordings.insert( + id="front_recording", + path="/media/recordings/front.mp4", + camera="front_door", + start_time=march_10_noon, + end_time=march_10_noon + 3600, + duration=3600, + motion=100, + objects=5, + ).execute() + + Recordings.insert( + id="back_recording", + path="/media/recordings/back.mp4", + camera="back_door", + start_time=march_10_noon, + end_time=march_10_noon + 3600, + duration=3600, + motion=150, + objects=8, + ).execute() + + # Test with only front_door camera + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "front_door"}, + ) + + assert response.status_code == 200 + summary = response.json() + assert len(summary) == 1 + assert "2024-03-10" in summary + assert summary["2024-03-10"] is True + + def test_recordings_unavailable_reports_gap_between_recordings(self): + """A gap between two recordings is reported as an unavailable segment.""" + with AuthTestClient(self.app) as client: + # Two recordings with a 20s gap (1010-1030) between them. + Recordings.insert( + id="rec_a", + path="/media/recordings/a.mp4", + camera="front_door", + start_time=1000, + end_time=1010, + duration=10, + motion=0, + ).execute() + Recordings.insert( + id="rec_b", + path="/media/recordings/b.mp4", + camera="front_door", + start_time=1030, + end_time=1040, + duration=10, + motion=0, + ).execute() + + response = client.get( + "/recordings/unavailable", + params={ + "after": 1000, + "before": 1040, + "scale": 5, + "cameras": "front_door", + }, + ) + + assert response.status_code == 200 + assert response.json() == [{"start_time": 1010, "end_time": 1030}] + + def test_recordings_unavailable_merges_overlapping_recordings(self): + """Overlapping recordings are merged so no false gap is reported.""" + with AuthTestClient(self.app) as client: + # Overlapping recordings spanning the whole requested range. + Recordings.insert( + id="rec_a", + path="/media/recordings/a.mp4", + camera="front_door", + start_time=1000, + end_time=1020, + duration=20, + motion=0, + ).execute() + Recordings.insert( + id="rec_b", + path="/media/recordings/b.mp4", + camera="front_door", + start_time=1010, + end_time=1030, + duration=20, + motion=0, + ).execute() + + response = client.get( + "/recordings/unavailable", + params={ + "after": 1000, + "before": 1030, + "scale": 5, + "cameras": "front_door", + }, + ) + + assert response.status_code == 200 + assert response.json() == [] + + def test_recordings_unavailable_cameras_all_scopes_to_allowed_cameras(self): + """cameras=all must not error and must only consider allowed cameras. + + allowed_cameras is mocked to ["front_door"]. A back_door recording that + would otherwise fill the gap must be ignored, and the request must not + 500 the way it did when cameras was reassigned to a list. + """ + with AuthTestClient(self.app) as client: + # front_door has a 20s gap (1010-1030). + Recordings.insert( + id="front_a", + path="/media/recordings/front_a.mp4", + camera="front_door", + start_time=1000, + end_time=1010, + duration=10, + motion=0, + ).execute() + Recordings.insert( + id="front_b", + path="/media/recordings/front_b.mp4", + camera="front_door", + start_time=1030, + end_time=1040, + duration=10, + motion=0, + ).execute() + # back_door is not in allowed_cameras; its full-window coverage must + # not mask the front_door gap. + Recordings.insert( + id="back_a", + path="/media/recordings/back_a.mp4", + camera="back_door", + start_time=1000, + end_time=1040, + duration=40, + motion=0, + ).execute() + + response = client.get( + "/recordings/unavailable", + params={ + "after": 1000, + "before": 1040, + "scale": 5, + "cameras": "all", + }, + ) + + assert response.status_code == 200 + assert response.json() == [{"start_time": 1010, "end_time": 1030}] diff --git a/frigate/test/http_api/test_http_review.py b/frigate/test/http_api/test_http_review.py new file mode 100644 index 0000000..d13a7bd --- /dev/null +++ b/frigate/test/http_api/test_http_review.py @@ -0,0 +1,751 @@ +from datetime import datetime, timedelta + +from fastapi import Request +from peewee import DoesNotExist + +from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user +from frigate.models import Event, Recordings, ReviewSegment, UserReviewStatus +from frigate.review.types import SeverityEnum +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpReview(BaseTestHttp): + def setUp(self): + super().setUp([Event, Recordings, ReviewSegment, UserReviewStatus]) + self.app = super().create_app() + self.user_id = "admin" + + # Mock get_current_user for all tests + # This mock uses headers set by AuthTestClient + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} + + self.app.dependency_overrides[get_current_user] = mock_get_current_user + + async def mock_get_allowed_cameras_for_filter(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) + + def tearDown(self): + self.app.dependency_overrides.clear() + super().tearDown() + + def _get_reviews(self, ids: list[str]): + return list( + ReviewSegment.select(ReviewSegment.id) + .where(ReviewSegment.id.in_(ids)) + .execute() + ) + + def _get_recordings(self, ids: list[str]): + return list( + Recordings.select(Recordings.id).where(Recordings.id.in_(ids)).execute() + ) + + def _insert_user_review_status(self, review_id: str, reviewed: bool = True): + UserReviewStatus.create( + user_id=self.user_id, + review_segment=ReviewSegment.get(ReviewSegment.id == review_id), + has_been_reviewed=reviewed, + ) + + #################################################################################################################### + ################################### GET /review Endpoint ######################################################## + #################################################################################################################### + + def test_get_review_that_overlaps_default_period(self): + """Test that a review item that starts during the default period + but ends after is included in the results.""" + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random", now, now + 2) + response = client.get("/review") + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + + def test_get_review_no_filters(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now - 2, now - 1) + response = client.get("/review") + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id + assert response_json[0]["has_been_reviewed"] == False + + def test_get_review_with_time_filter_no_matches(self): + """Test that review items outside the range are not returned.""" + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now - 2, now - 1) + super().insert_mock_review_segment(f"{id}2", now + 4, now + 5) + params = { + "after": now, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 0 + + def test_get_review_with_time_filter(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now, now + 2) + params = { + "after": now - 1, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id + + def test_get_review_with_limit_filter(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + id2 = "654321.random" + super().insert_mock_review_segment(id, now, now + 2) + super().insert_mock_review_segment(id2, now + 1, now + 2) + params = { + "limit": 1, + "after": now, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id2 + + def test_get_review_with_severity_filters_no_matches(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection) + params = { + "severity": "detection", + "after": now - 1, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id + + def test_get_review_with_severity_filters(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection) + params = { + "severity": "alert", + "after": now - 1, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 0 + + def test_get_review_with_all_filters(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now, now + 2) + params = { + "cameras": "front_door", + "labels": "all", + "zones": "all", + "reviewed": 0, + "limit": 1, + "severity": "alert", + "after": now - 1, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id + + def test_get_review_with_reviewed_filter_unreviewed(self): + """Test that reviewed=0 returns only unreviewed items.""" + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id_unreviewed = "123456.unreviewed" + id_reviewed = "123456.reviewed" + super().insert_mock_review_segment(id_unreviewed, now, now + 2) + super().insert_mock_review_segment(id_reviewed, now, now + 2) + self._insert_user_review_status(id_reviewed, reviewed=True) + + params = { + "reviewed": 0, + "after": now - 1, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id_unreviewed + + def test_get_review_with_reviewed_filter_reviewed(self): + """Test that reviewed=1 returns only reviewed items.""" + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id_unreviewed = "123456.unreviewed" + id_reviewed = "123456.reviewed" + super().insert_mock_review_segment(id_unreviewed, now, now + 2) + super().insert_mock_review_segment(id_reviewed, now, now + 2) + self._insert_user_review_status(id_reviewed, reviewed=True) + + params = { + "reviewed": 1, + "after": now - 1, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id_reviewed + + #################################################################################################################### + ################################### GET /review/summary Endpoint ################################################# + #################################################################################################################### + def test_get_review_summary_all_filters(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + params = { + "cameras": "front_door", + "labels": "all", + "zones": "all", + "timezone": "utc", + } + response = client.get("/review/summary", params=params) + assert response.status_code == 200 + response_json = response.json() + # e.g. '2024-11-24' + today_formatted = datetime.today().strftime("%Y-%m-%d") + expected_response = { + "last24Hours": { + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + today_formatted: { + "day": today_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + } + self.assertEqual(response_json, expected_response) + + def test_get_review_summary_no_filters(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + response = client.get("/review/summary") + assert response.status_code == 200 + response_json = response.json() + # e.g. '2024-11-24' + today_formatted = datetime.today().strftime("%Y-%m-%d") + expected_response = { + "last24Hours": { + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + today_formatted: { + "day": today_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + } + self.assertEqual(response_json, expected_response) + + def test_get_review_summary_multiple_days(self): + now = datetime.now() + five_days_ago = datetime.today() - timedelta(days=5) + + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment( + "123456.random", now.timestamp() - 2, now.timestamp() - 1 + ) + super().insert_mock_review_segment( + "654321.random", + five_days_ago.timestamp(), + five_days_ago.timestamp() + 1, + ) + response = client.get("/review/summary") + assert response.status_code == 200 + response_json = response.json() + # e.g. '2024-11-24' + today_formatted = now.strftime("%Y-%m-%d") + # e.g. '2024-11-19' + five_days_ago_formatted = five_days_ago.strftime("%Y-%m-%d") + expected_response = { + "last24Hours": { + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + today_formatted: { + "day": today_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + five_days_ago_formatted: { + "day": five_days_ago_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + } + self.assertEqual(response_json, expected_response) + + def test_get_review_summary_multiple_in_same_day(self): + now = datetime.now() + five_days_ago = datetime.today() - timedelta(days=5) + + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random", now.timestamp()) + five_days_ago_ts = five_days_ago.timestamp() + for i in range(20): + super().insert_mock_review_segment( + f"123456_{i}.random_alert", + five_days_ago_ts, + five_days_ago_ts, + SeverityEnum.alert, + ) + for i in range(15): + super().insert_mock_review_segment( + f"123456_{i}.random_detection", + five_days_ago_ts, + five_days_ago_ts, + SeverityEnum.detection, + ) + response = client.get("/review/summary") + assert response.status_code == 200 + response_json = response.json() + # e.g. '2024-11-24' + today_formatted = now.strftime("%Y-%m-%d") + # e.g. '2024-11-19' + five_days_ago_formatted = five_days_ago.strftime("%Y-%m-%d") + expected_response = { + "last24Hours": { + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + today_formatted: { + "day": today_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + five_days_ago_formatted: { + "day": five_days_ago_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 20, + "total_detection": 15, + }, + } + self.assertEqual(response_json, expected_response) + + def test_get_review_summary_multiple_in_same_day_with_reviewed(self): + five_days_ago = datetime.today() - timedelta(days=5) + + with AuthTestClient(self.app) as client: + five_days_ago_ts = five_days_ago.timestamp() + for i in range(10): + id = f"123456_{i}.random_alert_not_reviewed" + super().insert_mock_review_segment( + id, five_days_ago_ts, five_days_ago_ts, SeverityEnum.alert + ) + for i in range(10): + id = f"123456_{i}.random_alert_reviewed" + super().insert_mock_review_segment( + id, five_days_ago_ts, five_days_ago_ts, SeverityEnum.alert + ) + self._insert_user_review_status(id, reviewed=True) + for i in range(10): + id = f"123456_{i}.random_detection_not_reviewed" + super().insert_mock_review_segment( + id, five_days_ago_ts, five_days_ago_ts, SeverityEnum.detection + ) + for i in range(5): + id = f"123456_{i}.random_detection_reviewed" + super().insert_mock_review_segment( + id, five_days_ago_ts, five_days_ago_ts, SeverityEnum.detection + ) + self._insert_user_review_status(id, reviewed=True) + response = client.get("/review/summary") + assert response.status_code == 200 + response_json = response.json() + # e.g. '2024-11-19' + five_days_ago_formatted = five_days_ago.strftime("%Y-%m-%d") + expected_response = { + "last24Hours": { + "reviewed_alert": None, + "reviewed_detection": None, + "total_alert": None, + "total_detection": None, + }, + five_days_ago_formatted: { + "day": five_days_ago_formatted, + "reviewed_alert": 10, + "reviewed_detection": 5, + "total_alert": 20, + "total_detection": 15, + }, + } + self.assertEqual(response_json, expected_response) + + #################################################################################################################### + ################################### POST reviews/viewed Endpoint ################################################ + #################################################################################################################### + + def test_post_reviews_viewed_no_body(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + response = client.post("/reviews/viewed") + # Missing ids + assert response.status_code == 422 + + def test_post_reviews_viewed_no_body_ids(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + body = {"ids": [""]} + response = client.post("/reviews/viewed", json=body) + # Missing ids + assert response.status_code == 422 + + def test_post_reviews_viewed_non_existent_id(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id) + body = {"ids": ["1"]} + response = client.post("/reviews/viewed", json=body) + assert response.status_code == 200 + response = response.json() + assert response["success"] == True + assert response["message"] == "Marked multiple items as reviewed" + # Verify that in DB the review segment was not changed + with self.assertRaises(DoesNotExist): + UserReviewStatus.get( + UserReviewStatus.user_id == self.user_id, + UserReviewStatus.review_segment == "1", + ) + + def test_post_reviews_viewed(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id) + body = {"ids": [id]} + response = client.post("/reviews/viewed", json=body) + assert response.status_code == 200 + response_json = response.json() + assert response_json["success"] == True + assert response_json["message"] == "Marked multiple items as reviewed" + # Verify UserReviewStatus was created + user_review = UserReviewStatus.get( + UserReviewStatus.user_id == self.user_id, + UserReviewStatus.review_segment == id, + ) + assert user_review.has_been_reviewed == True + + #################################################################################################################### + ################################### POST reviews/delete Endpoint ################################################ + #################################################################################################################### + def test_post_reviews_delete_no_body(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + response = client.post("/reviews/delete", headers={"remote-role": "admin"}) + # Missing ids + assert response.status_code == 422 + + def test_post_reviews_delete_no_body_ids(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + body = {"ids": [""]} + response = client.post( + "/reviews/delete", json=body, headers={"remote-role": "admin"} + ) + # Missing ids + assert response.status_code == 422 + + def test_post_reviews_delete_non_existent_id(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id) + body = {"ids": ["1"]} + response = client.post( + "/reviews/delete", json=body, headers={"remote-role": "admin"} + ) + assert response.status_code == 200 + response_json = response.json() + assert response_json["success"] == True + assert response_json["message"] == "Deleted review items." + # Verify that in DB the review segment was not deleted + review_ids_in_db_after = self._get_reviews([id]) + assert len(review_ids_in_db_after) == 1 + assert review_ids_in_db_after[0].id == id + + def test_post_reviews_delete(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id) + body = {"ids": [id]} + response = client.post( + "/reviews/delete", json=body, headers={"remote-role": "admin"} + ) + assert response.status_code == 200 + response_json = response.json() + assert response_json["success"] == True + assert response_json["message"] == "Deleted review items." + # Verify that in DB the review segment was deleted + review_ids_in_db_after = self._get_reviews([id]) + assert len(review_ids_in_db_after) == 0 + + def test_post_reviews_delete_many(self): + with AuthTestClient(self.app) as client: + ids = ["123456.random", "654321.random"] + for id in ids: + super().insert_mock_review_segment(id) + super().insert_mock_recording(id) + + review_ids_in_db_before = self._get_reviews(ids) + recordings_ids_in_db_before = self._get_recordings(ids) + assert len(review_ids_in_db_before) == 2 + assert len(recordings_ids_in_db_before) == 2 + + body = {"ids": ids} + response = client.post( + "/reviews/delete", json=body, headers={"remote-role": "admin"} + ) + assert response.status_code == 200 + response_json = response.json() + assert response_json["success"] == True + assert response_json["message"] == "Deleted review items." + + # Verify that in DB all review segments and recordings that were passed were deleted + review_ids_in_db_after = self._get_reviews(ids) + recording_ids_in_db_after = self._get_recordings(ids) + assert len(review_ids_in_db_after) == 0 + assert len(recording_ids_in_db_after) == 0 + + #################################################################################################################### + ################################### GET /review/activity/motion Endpoint ######################################## + #################################################################################################################### + def test_review_activity_motion_no_data_for_time_range(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + params = { + "after": now, + "before": now + 3, + } + response = client.get("/review/activity/motion", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 0 + + def test_review_activity_motion(self): + now = int(datetime.now().timestamp()) + + with AuthTestClient(self.app) as client: + one_m = int((datetime.now() + timedelta(minutes=1)).timestamp()) + id = "123456.random" + id2 = "123451.random" + super().insert_mock_recording(id, now + 1, now + 2, motion=101) + super().insert_mock_recording(id2, one_m + 1, one_m + 2, motion=200) + params = { + "after": now, + "before": one_m + 3, + "scale": 1, + } + response = client.get("/review/activity/motion", params=params) + assert response.status_code == 200 + response_json = response.json() + # Only buckets with an actual recording are returned. Empty + # gap-fill buckets between the two recordings are dropped. + assert len(response_json) == 2 + self.assertDictEqual( + {"motion": 50.5, "camera": "front_door", "start_time": now + 1}, + response_json[0], + ) + self.assertDictEqual( + {"motion": 100.0, "camera": "front_door", "start_time": one_m + 1}, + response_json[1], + ) + + #################################################################################################################### + ################################### GET /review/event/{event_id} Endpoint ####################################### + #################################################################################################################### + def test_review_event_not_found(self): + with AuthTestClient(self.app) as client: + response = client.get("/review/event/123456.random") + assert response.status_code == 404 + response_json = response.json() + self.assertDictEqual( + {"success": False, "message": "Review item not found"}, + response_json, + ) + + def test_review_event_not_found_in_data(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now + 1, now + 2) + response = client.get(f"/review/event/{id}") + assert response.status_code == 404 + response_json = response.json() + self.assertDictEqual( + {"success": False, "message": "Review item not found"}, + response_json, + ) + + def test_review_get_specific_event(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + event_id = "123456.event.random" + super().insert_mock_event(event_id) + review_id = "123456.review.random" + super().insert_mock_review_segment( + review_id, now + 1, now + 2, data={"detections": {"event_id": event_id}} + ) + response = client.get(f"/review/event/{event_id}") + assert response.status_code == 200 + response_json = response.json() + self.assertDictEqual( + { + "id": review_id, + "camera": "front_door", + "start_time": now + 1, + "end_time": now + 2, + "severity": "alert", + "thumb_path": "False", + "data": {"detections": {"event_id": event_id}}, + }, + response_json, + ) + + #################################################################################################################### + ################################### GET /review/{review_id} Endpoint ####################################### + #################################################################################################################### + def test_review_not_found(self): + with AuthTestClient(self.app) as client: + response = client.get("/review/123456.random") + assert response.status_code == 404 + response_json = response.json() + self.assertDictEqual( + {"success": False, "message": "Review item not found"}, + response_json, + ) + + def test_get_review(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + review_id = "123456.review.random" + super().insert_mock_review_segment(review_id, now + 1, now + 2) + response = client.get(f"/review/{review_id}") + assert response.status_code == 200 + response_json = response.json() + self.assertDictEqual( + { + "id": review_id, + "camera": "front_door", + "start_time": now + 1, + "end_time": now + 2, + "severity": "alert", + "thumb_path": "False", + "data": {}, + }, + response_json, + ) + + #################################################################################################################### + ################################### DELETE /review/{review_id}/viewed Endpoint ################################## + #################################################################################################################### + + def test_delete_review_viewed_review_not_found(self): + with AuthTestClient(self.app) as client: + review_id = "123456.random" + response = client.delete(f"/review/{review_id}/viewed") + assert response.status_code == 404 + response_json = response.json() + self.assertDictEqual( + {"success": False, "message": f"Review {review_id} not found"}, + response_json, + ) + + def test_delete_review_viewed(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + review_id = "123456.review.random" + super().insert_mock_review_segment(review_id, now + 1, now + 2) + self._insert_user_review_status(review_id, reviewed=True) + # Verify it’s reviewed before + response = client.get(f"/review/{review_id}") + + response = client.delete(f"/review/{review_id}/viewed") + assert response.status_code == 200 + response_json = response.json() + self.assertDictEqual( + {"success": True, "message": f"Set Review {review_id} as not viewed"}, + response_json, + ) + + # Verify it’s unreviewed after + with self.assertRaises(DoesNotExist): + UserReviewStatus.get( + UserReviewStatus.user_id == self.user_id, + UserReviewStatus.review_segment == review_id, + ) diff --git a/frigate/test/test_birdseye.py b/frigate/test/test_birdseye.py new file mode 100644 index 0000000..33683f5 --- /dev/null +++ b/frigate/test/test_birdseye.py @@ -0,0 +1,47 @@ +"""Test camera user and password cleanup.""" + +import unittest + +from frigate.output.birdseye import get_canvas_shape + + +class TestBirdseye(unittest.TestCase): + def test_16x9(self): + """Test 16x9 aspect ratio works as expected for birdseye.""" + width = 1280 + height = 720 + canvas_width, canvas_height = get_canvas_shape(width, height) + assert canvas_width == width + assert canvas_height == height + + def test_4x3(self): + """Test 4x3 aspect ratio works as expected for birdseye.""" + width = 1280 + height = 960 + canvas_width, canvas_height = get_canvas_shape(width, height) + assert canvas_width == width + assert canvas_height == height + + def test_32x9(self): + """Test 32x9 aspect ratio works as expected for birdseye.""" + width = 2560 + height = 720 + canvas_width, canvas_height = get_canvas_shape(width, height) + assert canvas_width == width + assert canvas_height == height + + def test_9x16(self): + """Test 9x16 aspect ratio works as expected for birdseye.""" + width = 720 + height = 1280 + canvas_width, canvas_height = get_canvas_shape(width, height) + assert canvas_width == width + assert canvas_height == height + + def test_non_16x9(self): + """Test non 16x9 aspect ratio fails for birdseye.""" + width = 1280 + height = 840 + canvas_width, canvas_height = get_canvas_shape(width, height) + assert canvas_width == width # width will be the same + assert canvas_height != height diff --git a/frigate/test/test_builtin.py b/frigate/test/test_builtin.py new file mode 100644 index 0000000..7cf47de --- /dev/null +++ b/frigate/test/test_builtin.py @@ -0,0 +1,41 @@ +"""Tests for frigate.util.builtin helpers.""" + +import unittest +from unittest.mock import patch + +from frigate.util.builtin import EventsPerSecond + + +class TestEventsPerSecond(unittest.TestCase): + def test_eps_is_zero_before_any_events(self) -> None: + eps = EventsPerSecond() + with patch("frigate.util.builtin.time.monotonic", return_value=100.0): + self.assertEqual(eps.eps(), 0.0) + + def test_eps_counts_events_in_window(self) -> None: + eps = EventsPerSecond(last_n_seconds=10) + clock = [1000.0] + with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]): + eps.start() + # one event per second for five seconds + for _ in range(5): + clock[0] += 1.0 + eps.update() + # five events over the five seconds since start + self.assertAlmostEqual(eps.eps(), 1.0) + + def test_old_timestamps_expire_from_window(self) -> None: + eps = EventsPerSecond(last_n_seconds=10) + clock = [0.0] + with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]): + eps.start() + for _ in range(10): + clock[0] += 1.0 + eps.update() + # jump well past the window so every timestamp ages out + clock[0] += 100.0 + self.assertEqual(eps.eps(), 0.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_camera_maintainer.py b/frigate/test/test_camera_maintainer.py new file mode 100644 index 0000000..c03d965 --- /dev/null +++ b/frigate/test/test_camera_maintainer.py @@ -0,0 +1,79 @@ +"""Tests for CameraMaintainer SHM cleanup on camera remove. + +Regression coverage for the case where a camera is removed and then a +new camera is added with the same name. Without unlinking the per-frame +YUV SHM slots, the maintainer's frame_manager.create call hits +FileExistsError and falls back to reopening the existing segment at the +*old* size, which the new ffmpeg process then writes mismatched-size +frames into. +""" + +import unittest +from unittest.mock import MagicMock, patch + +from frigate.camera.maintainer import CameraMaintainer + + +class TestMaintainerUnlinkFrameSlotsOnRemove(unittest.TestCase): + def _make_maintainer(self) -> CameraMaintainer: + """Build a maintainer without invoking __init__ (avoids needing real + FrigateConfig, queues, multiprocessing manager, etc.). We're only + exercising the SHM-cleanup helper, so the surrounding init is + irrelevant.""" + maintainer = CameraMaintainer.__new__(CameraMaintainer) + maintainer.frame_manager = MagicMock() + return maintainer + + def test_unlinks_only_segments_with_matching_prefix(self) -> None: + maintainer = self._make_maintainer() + maintainer.frame_manager.shm_store = { + "front_frame0": object(), + "front_frame1": object(), + "front_frame2": object(), + # Different camera; must not be touched. + "side_frame0": object(), + # Detector input/output buffers are sized by the model and + # cached by the long-lived DetectorRunner — must not be + # touched even when their owning camera is removed. + "front": object(), + "out-front": object(), + } + + # __name-mangled access from outside the class. + maintainer._CameraMaintainer__unlink_camera_frame_slots("front") + + deleted = [c.args[0] for c in maintainer.frame_manager.delete.call_args_list] + self.assertEqual( + sorted(deleted), + ["front_frame0", "front_frame1", "front_frame2"], + ) + + def test_handles_camera_with_no_slots(self) -> None: + """Cameras that were removed before any frame slot was ever + created (e.g. cancelled during preparing_clip) should be a no-op.""" + maintainer = self._make_maintainer() + maintainer.frame_manager.shm_store = {"other_frame0": object()} + + maintainer._CameraMaintainer__unlink_camera_frame_slots("front") + + maintainer.frame_manager.delete.assert_not_called() + + def test_swallows_delete_errors(self) -> None: + """Unlink failures shouldn't abort the remove loop — best-effort.""" + maintainer = self._make_maintainer() + maintainer.frame_manager.shm_store = { + "front_frame0": object(), + "front_frame1": object(), + } + maintainer.frame_manager.delete.side_effect = OSError("simulated") + + # Both slots are attempted; the OSError on the first doesn't + # prevent the second from being tried. + with patch("frigate.camera.maintainer.logger"): + maintainer._CameraMaintainer__unlink_camera_frame_slots("front") + + self.assertEqual(maintainer.frame_manager.delete.call_count, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_camera_pw.py b/frigate/test/test_camera_pw.py new file mode 100644 index 0000000..0964f38 --- /dev/null +++ b/frigate/test/test_camera_pw.py @@ -0,0 +1,49 @@ +"""Test camera user and password cleanup.""" + +import unittest + +from frigate.util.builtin import clean_camera_user_pass, escape_special_characters + + +class TestUserPassCleanup(unittest.TestCase): + def setUp(self) -> None: + self.rtsp_with_pass = "rtsp://user:password@192.168.0.2:554/live" + self.rtsp_with_special_pass = ( + "rtsp://user:password`~!@#$%^&*()-_;',.<>:\"\{\}\[\]@@192.168.0.2:554/live" + ) + self.rtsp_no_pass = "rtsp://192.168.0.3:554/live" + + def test_cleanup(self): + """Test that user / pass are cleaned up.""" + clean = clean_camera_user_pass(self.rtsp_with_pass) + assert clean != self.rtsp_with_pass + assert "user:password" not in clean + + def test_no_cleanup(self): + """Test that nothing changes when no user / pass are defined.""" + clean = clean_camera_user_pass(self.rtsp_no_pass) + assert clean == self.rtsp_no_pass + + def test_special_char_password(self): + """Test that special characters in pw are escaped, but not others.""" + escaped = escape_special_characters(self.rtsp_with_special_pass) + assert ( + escaped + == "rtsp://user:password%60~%21%40%23%24%25%5E%26%2A%28%29-_%3B%27%2C.%3C%3E%3A%22%5C%7B%5C%7D%5C%5B%5C%5D%40@192.168.0.2:554/live" + ) + + def test_no_special_char_password(self): + """Test that no change is made to path with no special characters.""" + escaped = escape_special_characters(self.rtsp_with_pass) + assert escaped == self.rtsp_with_pass + + +class TestUserPassMasking(unittest.TestCase): + def setUp(self) -> None: + self.rtsp_log_message = "Did you mean file:rtsp://user:password@192.168.1.3:554" + + def test_rtsp_in_log_message(self): + """Test that the rtsp url in a log message is escaped.""" + escaped = clean_camera_user_pass(self.rtsp_log_message) + print(f"The escaped is {escaped}") + assert escaped == "Did you mean file:rtsp://*:*@192.168.1.3:554" diff --git a/frigate/test/test_chat_find_similar_objects.py b/frigate/test/test_chat_find_similar_objects.py new file mode 100644 index 0000000..73fd3b2 --- /dev/null +++ b/frigate/test/test_chat_find_similar_objects.py @@ -0,0 +1,306 @@ +"""Tests for the find_similar_objects chat tool.""" + +import asyncio +import os +import tempfile +import unittest +from types import SimpleNamespace +from unittest.mock import MagicMock + +from playhouse.sqlite_ext import SqliteExtDatabase + +from frigate.api.chat import ( + _execute_find_similar_objects, + get_tool_definitions, +) +from frigate.api.chat_util import ( + DESCRIPTION_WEIGHT, + VISUAL_WEIGHT, + distance_to_score, + fuse_scores, +) +from frigate.embeddings.util import ZScoreNormalization +from frigate.models import Event + + +def _run(coro): + return asyncio.new_event_loop().run_until_complete(coro) + + +class TestDistanceToScore(unittest.TestCase): + def test_lower_distance_gives_higher_score(self): + stats = ZScoreNormalization() + # Seed the stats with a small distribution so stddev > 0. + stats._update([0.1, 0.2, 0.3, 0.4, 0.5]) + + close_score = distance_to_score(0.1, stats) + far_score = distance_to_score(0.5, stats) + + self.assertGreater(close_score, far_score) + self.assertGreaterEqual(close_score, 0.0) + self.assertLessEqual(close_score, 1.0) + self.assertGreaterEqual(far_score, 0.0) + self.assertLessEqual(far_score, 1.0) + + def test_uninitialized_stats_returns_neutral_score(self): + stats = ZScoreNormalization() # n == 0, stddev == 0 + self.assertEqual(distance_to_score(0.3, stats), 0.5) + + +class TestFuseScores(unittest.TestCase): + def test_weights_sum_to_one(self): + self.assertAlmostEqual(VISUAL_WEIGHT + DESCRIPTION_WEIGHT, 1.0) + + def test_fuses_both_sides(self): + fused = fuse_scores(visual_score=0.8, description_score=0.4) + expected = VISUAL_WEIGHT * 0.8 + DESCRIPTION_WEIGHT * 0.4 + self.assertAlmostEqual(fused, expected) + + def test_missing_description_uses_visual_only(self): + fused = fuse_scores(visual_score=0.7, description_score=None) + self.assertAlmostEqual(fused, 0.7) + + def test_missing_visual_uses_description_only(self): + fused = fuse_scores(visual_score=None, description_score=0.6) + self.assertAlmostEqual(fused, 0.6) + + def test_both_missing_returns_none(self): + self.assertIsNone(fuse_scores(visual_score=None, description_score=None)) + + +class TestToolDefinition(unittest.TestCase): + def test_find_similar_objects_is_registered(self): + tools = get_tool_definitions() + names = [t["function"]["name"] for t in tools] + self.assertIn("find_similar_objects", names) + + def test_find_similar_objects_schema(self): + tools = get_tool_definitions() + tool = next(t for t in tools if t["function"]["name"] == "find_similar_objects") + params = tool["function"]["parameters"]["properties"] + self.assertIn("event_id", params) + self.assertIn("after", params) + self.assertIn("before", params) + self.assertIn("cameras", params) + self.assertIn("labels", params) + self.assertIn("sub_labels", params) + self.assertIn("zones", params) + self.assertIn("similarity_mode", params) + self.assertIn("min_score", params) + self.assertIn("limit", params) + self.assertEqual(tool["function"]["parameters"]["required"], ["event_id"]) + self.assertEqual( + params["similarity_mode"]["enum"], ["visual", "semantic", "fused"] + ) + + +class TestExecuteFindSimilarObjects(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False) + self.tmp.close() + self.db = SqliteExtDatabase(self.tmp.name) + Event.bind(self.db, bind_refs=False, bind_backrefs=False) + self.db.connect() + self.db.create_tables([Event]) + + # Insert an anchor plus two candidates. + def make(event_id, label="car", camera="driveway", start=1_700_000_100): + Event.create( + id=event_id, + label=label, + sub_label=None, + camera=camera, + start_time=start, + end_time=start + 10, + top_score=0.9, + score=0.9, + false_positive=False, + zones=[], + thumbnail="", + has_clip=True, + has_snapshot=True, + region=[0, 0, 1, 1], + box=[0, 0, 1, 1], + area=1, + retain_indefinitely=False, + ratio=1.0, + plus_id="", + model_hash="", + detector_type="", + model_type="", + data={"description": "a green sedan"}, + ) + + make("anchor", start=1_700_000_200) + make("cand_a", start=1_700_000_100) + make("cand_b", start=1_700_000_150) + self.make = make + + def tearDown(self): + self.db.close() + os.unlink(self.tmp.name) + + def _make_request(self, semantic_enabled=True, embeddings=None): + app = SimpleNamespace( + embeddings=embeddings, + frigate_config=SimpleNamespace( + semantic_search=SimpleNamespace(enabled=semantic_enabled), + cameras={"driveway": object()}, + auth=SimpleNamespace(roles={"admin": [], "viewer": ["driveway"]}), + proxy=SimpleNamespace(separator=","), + ), + ) + return SimpleNamespace(app=app, headers={}) + + def test_semantic_search_disabled_returns_error(self): + req = self._make_request(semantic_enabled=False) + result = _run( + _execute_find_similar_objects( + req, + {"event_id": "anchor"}, + allowed_cameras=["driveway"], + ) + ) + self.assertEqual(result["error"], "semantic_search_disabled") + + def test_anchor_not_found_returns_error(self): + embeddings = MagicMock() + req = self._make_request(embeddings=embeddings) + result = _run( + _execute_find_similar_objects( + req, + {"event_id": "nope"}, + allowed_cameras=["driveway"], + ) + ) + self.assertEqual(result["error"], "anchor_not_found") + + def test_empty_candidates_returns_empty_results(self): + embeddings = MagicMock() + req = self._make_request(embeddings=embeddings) + # Filter to a camera with no other events. + result = _run( + _execute_find_similar_objects( + req, + {"event_id": "anchor", "cameras": ["nonexistent_cam"]}, + allowed_cameras=["driveway"], + ) + ) + self.assertEqual(result["results"], []) + self.assertFalse(result["candidate_truncated"]) + self.assertEqual(result["anchor"]["id"], "anchor") + + def test_fused_calls_both_searches_and_ranks(self): + embeddings = MagicMock() + # cand_a visually closer, cand_b semantically closer. + embeddings.search_thumbnail.return_value = [ + ("cand_a", 0.10), + ("cand_b", 0.40), + ] + embeddings.search_description.return_value = [ + ("cand_a", 0.50), + ("cand_b", 0.20), + ] + embeddings.thumb_stats = ZScoreNormalization() + embeddings.thumb_stats._update([0.1, 0.2, 0.3, 0.4, 0.5]) + embeddings.desc_stats = ZScoreNormalization() + embeddings.desc_stats._update([0.1, 0.2, 0.3, 0.4, 0.5]) + + req = self._make_request(embeddings=embeddings) + result = _run( + _execute_find_similar_objects( + req, + {"event_id": "anchor"}, + allowed_cameras=["driveway"], + ) + ) + embeddings.search_thumbnail.assert_called_once() + embeddings.search_description.assert_called_once() + # cand_a should rank first because visual is weighted higher. + self.assertEqual(result["results"][0]["id"], "cand_a") + self.assertIn("score", result["results"][0]) + self.assertEqual(result["similarity_mode"], "fused") + + def test_visual_mode_only_calls_thumbnail(self): + embeddings = MagicMock() + embeddings.search_thumbnail.return_value = [("cand_a", 0.1)] + embeddings.thumb_stats = ZScoreNormalization() + embeddings.thumb_stats._update([0.1, 0.2, 0.3]) + + req = self._make_request(embeddings=embeddings) + _run( + _execute_find_similar_objects( + req, + {"event_id": "anchor", "similarity_mode": "visual"}, + allowed_cameras=["driveway"], + ) + ) + embeddings.search_thumbnail.assert_called_once() + embeddings.search_description.assert_not_called() + + def test_semantic_mode_only_calls_description(self): + embeddings = MagicMock() + embeddings.search_description.return_value = [("cand_a", 0.1)] + embeddings.desc_stats = ZScoreNormalization() + embeddings.desc_stats._update([0.1, 0.2, 0.3]) + + req = self._make_request(embeddings=embeddings) + _run( + _execute_find_similar_objects( + req, + {"event_id": "anchor", "similarity_mode": "semantic"}, + allowed_cameras=["driveway"], + ) + ) + embeddings.search_description.assert_called_once() + embeddings.search_thumbnail.assert_not_called() + + def test_min_score_drops_low_scoring_results(self): + embeddings = MagicMock() + embeddings.search_thumbnail.return_value = [ + ("cand_a", 0.10), + ("cand_b", 0.90), + ] + embeddings.search_description.return_value = [] + embeddings.thumb_stats = ZScoreNormalization() + embeddings.thumb_stats._update([0.1, 0.2, 0.3, 0.4, 0.5]) + embeddings.desc_stats = ZScoreNormalization() + + req = self._make_request(embeddings=embeddings) + result = _run( + _execute_find_similar_objects( + req, + {"event_id": "anchor", "similarity_mode": "visual", "min_score": 0.6}, + allowed_cameras=["driveway"], + ) + ) + ids = [r["id"] for r in result["results"]] + self.assertIn("cand_a", ids) + self.assertNotIn("cand_b", ids) + + def test_labels_defaults_to_anchor_label(self): + self.make("person_a", label="person") + embeddings = MagicMock() + embeddings.search_thumbnail.return_value = [ + ("cand_a", 0.1), + ("cand_b", 0.2), + ] + embeddings.search_description.return_value = [] + embeddings.thumb_stats = ZScoreNormalization() + embeddings.thumb_stats._update([0.1, 0.2, 0.3]) + embeddings.desc_stats = ZScoreNormalization() + + req = self._make_request(embeddings=embeddings) + result = _run( + _execute_find_similar_objects( + req, + {"event_id": "anchor", "similarity_mode": "visual"}, + allowed_cameras=["driveway"], + ) + ) + ids = [r["id"] for r in result["results"]] + self.assertNotIn("person_a", ids) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_classification_enabled.py b/frigate/test/test_classification_enabled.py new file mode 100644 index 0000000..abb139c --- /dev/null +++ b/frigate/test/test_classification_enabled.py @@ -0,0 +1,106 @@ +"""Tests that disabled custom classification models are not registered or run.""" + +import sys +import unittest +from unittest.mock import MagicMock + +# Mock TFLite before importing the maintainer / classification modules +_MOCK_MODULES = [ + "tflite_runtime", + "tflite_runtime.interpreter", + "ai_edge_litert", + "ai_edge_litert.interpreter", +] +for mod in _MOCK_MODULES: + if mod not in sys.modules: + sys.modules[mod] = MagicMock() + +from frigate.data_processing.real_time.custom_classification import ( # noqa: E402 + CustomObjectClassificationProcessor, +) +from frigate.embeddings.maintainer import EmbeddingMaintainer # noqa: E402 + + +class TestCustomClassificationEnabledGating(unittest.TestCase): + """A model with enabled: false must not keep a processor registered.""" + + def _make_maintainer(self) -> EmbeddingMaintainer: + # Bypass the heavy __init__; only the attributes touched by the + # config update path are needed for these tests. + maintainer = EmbeddingMaintainer.__new__(EmbeddingMaintainer) + maintainer.realtime_processors = [] + maintainer.config = MagicMock() + maintainer.config.classification.custom = {} + maintainer.requestor = MagicMock() + maintainer.metrics = MagicMock() + maintainer.event_metadata_publisher = MagicMock() + return maintainer + + def _make_model_config(self, name: str, enabled: bool) -> MagicMock: + model_config = MagicMock() + model_config.name = name + model_config.enabled = enabled + model_config.state_config = None + return model_config + + def _make_processor(self, name: str) -> MagicMock: + processor = MagicMock(spec=CustomObjectClassificationProcessor) + processor.model_config = MagicMock() + processor.model_config.name = name + return processor + + def test_disabled_update_tears_down_existing_processor(self): + """Toggling a running model to disabled shuts down and drops its processor.""" + maintainer = self._make_maintainer() + processor = self._make_processor("atli") + maintainer.realtime_processors = [processor] + + maintainer._handle_custom_classification_update( + "config/classification/custom/atli", + self._make_model_config("atli", enabled=False), + ) + + processor.shutdown.assert_called_once() + self.assertEqual(maintainer.realtime_processors, []) + + def test_disabled_update_does_not_register_processor(self): + """A disabled model that has no processor is never registered.""" + maintainer = self._make_maintainer() + + maintainer._handle_custom_classification_update( + "config/classification/custom/atli", + self._make_model_config("atli", enabled=False), + ) + + self.assertEqual(maintainer.realtime_processors, []) + + def test_disabled_update_leaves_other_processors_untouched(self): + """Disabling one model must not affect other running processors.""" + maintainer = self._make_maintainer() + other = self._make_processor("simbi") + maintainer.realtime_processors = [other] + + maintainer._handle_custom_classification_update( + "config/classification/custom/atli", + self._make_model_config("atli", enabled=False), + ) + + other.shutdown.assert_not_called() + self.assertEqual(maintainer.realtime_processors, [other]) + + def test_removed_model_tears_down_processor(self): + """A None payload (model deleted) still shuts down its processor.""" + maintainer = self._make_maintainer() + processor = self._make_processor("atli") + maintainer.realtime_processors = [processor] + + maintainer._handle_custom_classification_update( + "config/classification/custom/atli", None + ) + + processor.shutdown.assert_called_once() + self.assertEqual(maintainer.realtime_processors, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py new file mode 100644 index 0000000..6490a65 --- /dev/null +++ b/frigate/test/test_config.py @@ -0,0 +1,1732 @@ +import json +import os +import unittest +from unittest.mock import patch + +import numpy as np +from pydantic import ValidationError +from ruamel.yaml.constructor import DuplicateKeyError + +from frigate.config import BirdseyeModeEnum, FrigateConfig +from frigate.const import MODEL_CACHE_DIR +from frigate.detectors import DetectorTypeEnum +from frigate.util.builtin import deep_merge + + +class TestConfig(unittest.TestCase): + def setUp(self): + self.minimal = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + self.plus_model_info = { + "id": "e63b7345cc83a84ed79dedfc99c16616", + "name": "SSDLite Mobiledet", + "description": "Fine tuned model", + "trainDate": "2023-04-28T23:22:01.262Z", + "type": "ssd", + "supportedDetectors": ["cpu", "edgetpu"], + "width": 320, + "height": 320, + "inputShape": "nhwc", + "pixelFormat": "rgb", + "labelMap": { + "0": "amazon", + "1": "car", + "2": "cat", + "3": "deer", + "4": "dog", + "5": "face", + "6": "fedex", + "7": "license_plate", + "8": "package", + "9": "person", + "10": "ups", + }, + } + + if not os.path.exists(MODEL_CACHE_DIR) and not os.path.islink(MODEL_CACHE_DIR): + os.makedirs(MODEL_CACHE_DIR) + + def test_config_class(self): + frigate_config = FrigateConfig(**self.minimal) + assert "cpu" in frigate_config.detectors.keys() + assert frigate_config.detectors["cpu"].type == DetectorTypeEnum.cpu + assert frigate_config.detectors["cpu"].model.width == 320 + + @patch("frigate.detectors.detector_config.load_labels") + def test_detector_custom_model_path(self, mock_labels): + mock_labels.return_value = {} + config = { + "detectors": { + "cpu": { + "type": "cpu", + "model_path": "/cpu_model.tflite", + }, + "edgetpu": { + "type": "edgetpu", + "model_path": "/edgetpu_model.tflite", + }, + "openvino": { + "type": "openvino", + }, + }, + # needs to be a file that will exist, doesn't matter what + "model": {"path": "/etc/hosts", "width": 512}, + } + + frigate_config = FrigateConfig(**(deep_merge(config, self.minimal))) + + assert "cpu" in frigate_config.detectors.keys() + assert "edgetpu" in frigate_config.detectors.keys() + assert "openvino" in frigate_config.detectors.keys() + + assert frigate_config.detectors["cpu"].type == DetectorTypeEnum.cpu + assert frigate_config.detectors["edgetpu"].type == DetectorTypeEnum.edgetpu + assert frigate_config.detectors["openvino"].type == DetectorTypeEnum.openvino + + assert frigate_config.detectors["cpu"].num_threads == 3 + assert frigate_config.detectors["edgetpu"].device is None + assert frigate_config.detectors["openvino"].device is None + + assert frigate_config.model.path == "/etc/hosts" + assert frigate_config.detectors["cpu"].model.path == "/cpu_model.tflite" + assert frigate_config.detectors["edgetpu"].model.path == "/edgetpu_model.tflite" + assert frigate_config.detectors["openvino"].model.path == "/etc/hosts" + + def test_invalid_mqtt_config(self): + config = { + "mqtt": {"host": "mqtt", "user": "test"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + self.assertRaises(ValidationError, lambda: FrigateConfig(**config)) + + def test_inherit_tracked_objects(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": {"track": ["person", "dog"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "dog" in frigate_config.cameras["back"].objects.track + + def test_deep_merge_override_replaces_list_values(self): + base = {"objects": {"track": ["person", "face"]}} + update = {"objects": {"track": ["person"]}} + + merged = deep_merge(base, update, override=True) + + assert merged["objects"]["track"] == ["person"] + + def test_deep_merge_merge_lists_still_appends(self): + base = {"track": ["person"]} + update = {"track": ["face"]} + + merged = deep_merge(base, update, override=True, merge_lists=True) + + assert merged["track"] == ["person", "face"] + + def test_override_birdseye(self): + config = { + "mqtt": {"host": "mqtt"}, + "birdseye": {"enabled": True, "mode": "continuous"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "birdseye": {"enabled": False, "mode": "motion"}, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert not frigate_config.cameras["back"].birdseye.enabled + assert frigate_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.motion + + def test_override_birdseye_non_inheritable(self): + config = { + "mqtt": {"host": "mqtt"}, + "birdseye": {"enabled": True, "mode": "continuous", "height": 1920}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].birdseye.enabled + + def test_inherit_birdseye(self): + config = { + "mqtt": {"host": "mqtt"}, + "birdseye": {"enabled": True, "mode": "continuous"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].birdseye.enabled + assert ( + frigate_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.continuous + ) + + def test_override_tracked_objects(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": {"track": ["person", "dog"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": {"track": ["cat"]}, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "cat" in frigate_config.cameras["back"].objects.track + + def test_default_object_filters(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": {"track": ["person", "dog"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "dog" in frigate_config.cameras["back"].objects.filters + + def test_default_audio_filters(self): + config = { + "mqtt": {"host": "mqtt"}, + "audio": {"listen": ["speech", "yell"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert set(frigate_config.cameras["back"].audio.filters.keys()) == { + "speech", + "yell", + } + + def test_override_audio_filters(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "audio": { + "listen": ["speech", "yell"], + "filters": {"speech": {"threshold": 0.9}}, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "speech" in frigate_config.cameras["back"].audio.filters + assert frigate_config.cameras["back"].audio.filters["speech"].threshold == 0.9 + assert "yell" in frigate_config.cameras["back"].audio.filters + assert "babbling" not in frigate_config.cameras["back"].audio.filters + + def test_inherit_object_filters(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"threshold": 0.7}}, + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "dog" in frigate_config.cameras["back"].objects.filters + assert frigate_config.cameras["back"].objects.filters["dog"].threshold == 0.7 + + def test_override_object_filters(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"threshold": 0.7}}, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "dog" in frigate_config.cameras["back"].objects.filters + assert frigate_config.cameras["back"].objects.filters["dog"].threshold == 0.7 + + def test_global_object_mask(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": {"track": ["person", "dog"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "mask": { + "global_mask_1": { + "friendly_name": "Global Mask 1", + "enabled": True, + "coordinates": "0,0,1,1,0,1", + } + }, + "filters": { + "dog": { + "mask": { + "dog_mask_1": { + "friendly_name": "Dog Mask 1", + "enabled": True, + "coordinates": "1,1,1,1,1,1", + } + } + } + }, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + back_camera = frigate_config.cameras["back"] + assert "dog" in back_camera.objects.filters + # dog filter has its own mask + global mask merged + assert len(back_camera.objects.filters["dog"].mask) == 2 + # person filter only has the global mask + assert len(back_camera.objects.filters["person"].mask) == 1 + + def test_motion_mask_relative_matches_explicit(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": {"alerts": {"retain": {"days": 20}}}, + "cameras": { + "explicit": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 400, + "width": 800, + "fps": 5, + }, + "motion": { + "mask": { + "explicit_mask": { + "friendly_name": "Explicit Mask", + "enabled": True, + "coordinates": "0,0,200,100,600,300,800,400", + } + } + }, + }, + "relative": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 400, + "width": 800, + "fps": 5, + }, + "motion": { + "mask": { + "relative_mask": { + "friendly_name": "Relative Mask", + "enabled": True, + "coordinates": "0.0,0.0,0.25,0.25,0.75,0.75,1.0,1.0", + } + } + }, + }, + }, + } + + frigate_config = FrigateConfig(**config) + assert np.array_equal( + frigate_config.cameras["explicit"].motion.rasterized_mask, + frigate_config.cameras["relative"].motion.rasterized_mask, + ) + + def test_default_input_args(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "-rtsp_transport" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + + def test_ffmpeg_params_global(self): + config = { + "ffmpeg": {"input_args": "-re"}, + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"threshold": 0.7}}, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "-re" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + + def test_ffmpeg_params_camera(self): + config = { + "mqtt": {"host": "mqtt"}, + "ffmpeg": {"input_args": ["test"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ], + "input_args": ["-re"], + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"threshold": 0.7}}, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "-re" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + assert "test" not in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + + def test_ffmpeg_params_input(self): + config = { + "mqtt": {"host": "mqtt"}, + "ffmpeg": {"input_args": ["test2"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + "input_args": "-re test", + } + ], + "input_args": "test3", + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"threshold": 0.7}}, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "-re" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + assert "test" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + assert "test2" not in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + assert "test3" not in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + + def test_inherit_clips_retention(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": {"alerts": {"retain": {"days": 20}}}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].record.alerts.retain.days == 20 + + def test_roles_listed_twice_throws_error(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": { + "alerts": { + "retain": { + "days": 20, + } + } + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}, + {"path": "rtsp://10.0.0.1:554/video2", "roles": ["detect"]}, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + self.assertRaises(ValidationError, lambda: FrigateConfig(**config)) + + def test_zone_matching_camera_name_throws_error(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": { + "alerts": { + "retain": { + "days": 20, + } + } + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "zones": {"back": {"coordinates": "1,1,1,1,1,1"}}, + } + }, + } + self.assertRaises(ValidationError, lambda: FrigateConfig(**config)) + + def test_zone_assigns_color_and_contour(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": { + "alerts": { + "retain": { + "days": 20, + } + } + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "zones": {"test": {"coordinates": "1,1,1,1,1,1"}}, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert isinstance( + frigate_config.cameras["back"].zones["test"].contour, np.ndarray + ) + assert frigate_config.cameras["back"].zones["test"].color != (0, 0, 0) + + def test_zone_filter_area_percent_converts_to_pixels(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": { + "alerts": { + "retain": { + "days": 20, + } + } + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "zones": { + "notification": { + "coordinates": "0.03,1,0.025,0,0.626,0,0.643,1", + "objects": ["person"], + "filters": {"person": {"min_area": 0.1}}, + } + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + expected_min_area = int(1080 * 1920 * 0.1) + assert ( + frigate_config.cameras["back"] + .zones["notification"] + .filters["person"] + .min_area + == expected_min_area + ) + + def test_zone_relative_matches_explicit(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": { + "alerts": { + "retain": { + "days": 20, + } + } + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 400, + "width": 800, + "fps": 5, + }, + "zones": { + "explicit": { + "coordinates": "0,0,200,100,600,300,800,400", + }, + "relative": { + "coordinates": "0.0,0.0,0.25,0.25,0.75,0.75,1.0,1.0", + }, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert np.array_equal( + frigate_config.cameras["back"].zones["explicit"].contour, + frigate_config.cameras["back"].zones["relative"].contour, + ) + + def test_role_assigned_but_not_enabled(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + {"path": "rtsp://10.0.0.1:554/record", "roles": ["record"]}, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + ffmpeg_cmds = frigate_config.cameras["back"].ffmpeg_cmds + assert len(ffmpeg_cmds) == 1 + assert "clips" not in ffmpeg_cmds[0]["roles"] + + def test_max_disappeared_default(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "enabled": True, + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].detect.max_disappeared == 5 * 5 + + def test_motion_frame_height_wont_go_below_120(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].motion.frame_height == 100 + + def test_motion_contour_area_dynamic(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert round(frigate_config.cameras["back"].motion.contour_area) == 10 + + def test_merge_labelmap(self): + config = { + "mqtt": {"host": "mqtt"}, + "model": {"labelmap": {7: "truck"}}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.model.merged_labelmap[7] == "truck" + + def test_default_labelmap_empty(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.model.merged_labelmap[0] == "person" + + def test_default_labelmap(self): + config = { + "mqtt": {"host": "mqtt"}, + "model": {"width": 320, "height": 320}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.model.merged_labelmap[0] == "person" + + def test_plus_labelmap(self): + with open(os.path.join(MODEL_CACHE_DIR, "test"), "w") as f: + json.dump(self.plus_model_info, f) + with open(os.path.join(MODEL_CACHE_DIR, "test.json"), "w") as f: + json.dump(self.plus_model_info, f) + + config = { + "mqtt": {"host": "mqtt"}, + "detectors": {"cpu": {"type": "cpu"}}, + "model": {"path": "plus://test"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.model.merged_labelmap[0] == "amazon" + + def test_fails_on_invalid_role(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + { + "path": "rtsp://10.0.0.1:554/video2", + "roles": ["clips"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + self.assertRaises(ValidationError, lambda: FrigateConfig(**config)) + + def test_fails_on_missing_role(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + { + "path": "rtsp://10.0.0.1:554/video2", + "roles": ["record"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "audio": {"enabled": True}, + } + }, + } + + self.assertRaises(ValueError, lambda: FrigateConfig(**config)) + + def test_works_on_missing_role_multiple_cams(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + { + "path": "rtsp://10.0.0.1:554/video2", + "roles": ["record"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + }, + "cam2": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + { + "path": "rtsp://10.0.0.1:554/video2", + "roles": ["record"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + }, + }, + } + + FrigateConfig(**config) + + def test_global_detect(self): + config = { + "mqtt": {"host": "mqtt"}, + "detect": {"max_disappeared": 1}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].detect.max_disappeared == 1 + assert frigate_config.cameras["back"].detect.height == 1080 + + def test_default_detect(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 720, + "width": 1280, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].detect.max_disappeared == 25 + assert frigate_config.cameras["back"].detect.height == 720 + + def test_global_detect_merge(self): + config = { + "mqtt": {"host": "mqtt"}, + "detect": {"max_disappeared": 1, "height": 720, "width": 1280}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].detect.max_disappeared == 1 + assert frigate_config.cameras["back"].detect.height == 1080 + assert frigate_config.cameras["back"].detect.width == 1920 + + def test_global_snapshots(self): + config = { + "mqtt": {"host": "mqtt"}, + "snapshots": {"enabled": True}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "snapshots": { + "height": 100, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].snapshots.enabled + assert frigate_config.cameras["back"].snapshots.height == 100 + + def test_default_snapshots(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].snapshots.bounding_box + assert frigate_config.cameras["back"].snapshots.quality == 60 + + def test_global_snapshots_merge(self): + config = { + "mqtt": {"host": "mqtt"}, + "snapshots": {"bounding_box": False, "height": 300}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "snapshots": { + "height": 150, + "enabled": True, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].snapshots.bounding_box is False + assert frigate_config.cameras["back"].snapshots.height == 150 + assert frigate_config.cameras["back"].snapshots.enabled + + def test_global_jsmpeg(self): + config = { + "mqtt": {"host": "mqtt"}, + "live": {"quality": 4}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].live.quality == 4 + + def test_default_live(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].live.quality == 8 + + def test_global_live_merge(self): + config = { + "mqtt": {"host": "mqtt"}, + "live": {"quality": 4, "height": 480}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "live": { + "quality": 7, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].live.quality == 7 + assert frigate_config.cameras["back"].live.height == 480 + + def test_global_timestamp_style(self): + config = { + "mqtt": {"host": "mqtt"}, + "timestamp_style": {"position": "bl"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].timestamp_style.position == "bl" + + def test_default_timestamp_style(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].timestamp_style.position == "tl" + + def test_global_timestamp_style_merge(self): + config = { + "mqtt": {"host": "mqtt"}, + "timestamp_style": {"position": "br", "thickness": 2}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "timestamp_style": {"position": "bl", "thickness": 4}, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].timestamp_style.position == "bl" + assert frigate_config.cameras["back"].timestamp_style.thickness == 4 + + def test_allow_retain_to_be_a_decimal(self): + config = { + "mqtt": {"host": "mqtt"}, + "snapshots": {"retain": {"default": 1.5}}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].snapshots.retain.default == 1.5 + + def test_fails_on_bad_camera_name(self): + config = { + "mqtt": {"host": "mqtt"}, + "snapshots": {"retain": {"default": 1.5}}, + "cameras": { + "back camer#": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + self.assertRaises(ValidationError, lambda: FrigateConfig(**config).cameras) + + def test_fails_on_bad_segment_time(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": {"enabled": True}, + "cameras": { + "back": { + "ffmpeg": { + "output_args": { + "record": "-f segment -segment_time 70 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an" + }, + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ], + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + self.assertRaises( + ValueError, + lambda: FrigateConfig(**config).ffmpeg.output_args.record, + ) + + def test_fails_zone_defines_untracked_object(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": {"track": ["person"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "zones": { + "steps": { + "coordinates": "0,0,0,0", + "objects": ["car", "person"], + }, + }, + } + }, + } + + self.assertRaises(ValueError, lambda: FrigateConfig(**config).cameras) + + def test_fails_duplicate_keys(self): + raw_config = """ + cameras: + test: + ffmpeg: + inputs: + - one + - two + inputs: + - three + - four + """ + + self.assertRaises( + DuplicateKeyError, lambda: FrigateConfig.parse_yaml(raw_config) + ) + + def test_object_filter_ratios_work(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"min_ratio": 0.2, "max_ratio": 10.1}}, + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "dog" in frigate_config.cameras["back"].objects.filters + assert frigate_config.cameras["back"].objects.filters["dog"].min_ratio == 0.2 + assert frigate_config.cameras["back"].objects.filters["dog"].max_ratio == 10.1 + + def test_valid_movement_weights(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "onvif": { + "autotracking": { + "movement_weights": "0, 1, 1.23, 2.34, 0.50, 1" + } + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].onvif.autotracking.movement_weights == [ + "0.0", + "1.0", + "1.23", + "2.34", + "0.5", + "1.0", + ] + + def test_fails_invalid_movement_weights(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "onvif": {"autotracking": {"movement_weights": "1.234, 2.345a"}}, + } + }, + } + + self.assertRaises(ValueError, lambda: FrigateConfig(**config)) + + +class TestAttributeFilterDefaults(unittest.TestCase): + """Verify attribute filter min_score handling at config load.""" + + def setUp(self): + self.minimal = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + def _build_config(self, object_filters: dict | None = None) -> FrigateConfig: + config = deep_merge({}, self.minimal) + if object_filters is not None: + config.setdefault("objects", {})["filters"] = object_filters + return FrigateConfig(**config) + + def test_attribute_with_no_filter_gets_default_min_score(self): + """Attribute with no user-provided filter gets created with min_score=0.7.""" + config = self._build_config() + face_filter = config.objects.filters.get("face") + self.assertIsNotNone(face_filter) + self.assertEqual(face_filter.min_score, 0.7) + + def test_attribute_filter_without_min_score_gets_bumped(self): + """If user sets some FilterConfig field but not min_score, min_score is bumped to 0.7.""" + config = self._build_config({"face": {"min_area": 500}}) + face_filter = config.objects.filters["face"] + self.assertEqual(face_filter.min_area, 500) + self.assertEqual(face_filter.min_score, 0.7) + + def test_attribute_filter_explicit_min_score_half_is_preserved(self): + """User-provided min_score=0.5 must NOT be silently rewritten to 0.7.""" + config = self._build_config({"face": {"min_score": 0.5}}) + face_filter = config.objects.filters["face"] + self.assertEqual(face_filter.min_score, 0.5) + + def test_attribute_filter_explicit_min_score_other_value_is_preserved(self): + """Sanity: explicit non-0.5 values pass through unchanged.""" + config = self._build_config({"face": {"min_score": 0.3}}) + face_filter = config.objects.filters["face"] + self.assertEqual(face_filter.min_score, 0.3) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/frigate/test/test_copy_yuv_to_position.py b/frigate/test/test_copy_yuv_to_position.py new file mode 100644 index 0000000..4a31928 --- /dev/null +++ b/frigate/test/test_copy_yuv_to_position.py @@ -0,0 +1,68 @@ +from unittest import TestCase, main + +import cv2 +import numpy as np + +from frigate.util.image import copy_yuv_to_position, get_yuv_crop + + +class TestCopyYuvToPosition(TestCase): + def setUp(self): + self.source_frame_bgr = np.zeros((400, 800, 3), np.uint8) + self.source_frame_bgr[:] = (0, 0, 255) + self.source_yuv_frame = cv2.cvtColor( + self.source_frame_bgr, cv2.COLOR_BGR2YUV_I420 + ) + y, u1, u2, v1, v2 = get_yuv_crop( + self.source_yuv_frame.shape, + ( + 0, + 0, + self.source_frame_bgr.shape[1], + self.source_frame_bgr.shape[0], + ), + ) + self.source_channel_dims = { + "y": y, + "u1": u1, + "u2": u2, + "v1": v1, + "v2": v2, + } + + self.dest_frame_bgr = np.zeros((400, 800, 3), np.uint8) + self.dest_frame_bgr[:] = (112, 202, 50) + self.dest_frame_bgr[100:300, 200:600] = (255, 0, 0) + self.dest_yuv_frame = cv2.cvtColor(self.dest_frame_bgr, cv2.COLOR_BGR2YUV_I420) + + def test_clear_position(self): + copy_yuv_to_position(self.dest_yuv_frame, (100, 100), (100, 100)) + # cv2.imwrite(f"source_frame_yuv.jpg", self.source_yuv_frame) + # cv2.imwrite(f"dest_frame_yuv.jpg", self.dest_yuv_frame) + + def test_copy_position(self): + copy_yuv_to_position( + self.dest_yuv_frame, + (100, 100), + (100, 200), + self.source_yuv_frame, + self.source_channel_dims, + ) + + # cv2.imwrite(f"source_frame_yuv.jpg", self.source_yuv_frame) + # cv2.imwrite(f"dest_frame_yuv.jpg", self.dest_yuv_frame) + + def test_copy_position_full_screen(self): + copy_yuv_to_position( + self.dest_yuv_frame, + (0, 0), + (400, 800), + self.source_yuv_frame, + self.source_channel_dims, + ) + # cv2.imwrite(f"source_frame_yuv.jpg", self.source_yuv_frame) + # cv2.imwrite(f"dest_frame_yuv.jpg", self.dest_yuv_frame) + + +if __name__ == "__main__": + main(verbosity=2) diff --git a/frigate/test/test_debug_replay.py b/frigate/test/test_debug_replay.py new file mode 100644 index 0000000..a91f759 --- /dev/null +++ b/frigate/test/test_debug_replay.py @@ -0,0 +1,250 @@ +"""Tests for the simplified DebugReplayManager. + +Startup orchestration lives in ``frigate.jobs.debug_replay`` (covered by +``test_debug_replay_job``). The manager owns only session presence and +cleanup. +""" + +import unittest +import unittest.mock +from unittest.mock import MagicMock, patch + + +class TestDebugReplayManagerSession(unittest.TestCase): + def test_inactive_by_default(self) -> None: + from frigate.debug_replay import DebugReplayManager + + manager = DebugReplayManager() + + self.assertFalse(manager.active) + self.assertIsNone(manager.replay_camera_name) + self.assertIsNone(manager.source_camera) + self.assertIsNone(manager.clip_path) + self.assertIsNone(manager.start_ts) + self.assertIsNone(manager.end_ts) + + def test_mark_starting_sets_session_pointers_and_active(self) -> None: + from frigate.debug_replay import DebugReplayManager + + manager = DebugReplayManager() + + manager.mark_starting( + source_camera="front", + replay_camera_name="_replay_front", + start_ts=100.0, + end_ts=200.0, + ) + + self.assertTrue(manager.active) + self.assertEqual(manager.replay_camera_name, "_replay_front") + self.assertEqual(manager.source_camera, "front") + self.assertEqual(manager.start_ts, 100.0) + self.assertEqual(manager.end_ts, 200.0) + self.assertIsNone(manager.clip_path) + + def test_mark_session_ready_sets_clip_path(self) -> None: + from frigate.debug_replay import DebugReplayManager + + manager = DebugReplayManager() + manager.mark_starting("front", "_replay_front", 100.0, 200.0) + + manager.mark_session_ready(clip_path="/tmp/replay/_replay_front.mp4") + + self.assertEqual(manager.clip_path, "/tmp/replay/_replay_front.mp4") + self.assertTrue(manager.active) + + def test_clear_session_resets_all_pointers(self) -> None: + from frigate.debug_replay import DebugReplayManager + + manager = DebugReplayManager() + manager.mark_starting("front", "_replay_front", 100.0, 200.0) + manager.mark_session_ready("/tmp/replay/clip.mp4") + + manager.clear_session() + + self.assertFalse(manager.active) + self.assertIsNone(manager.replay_camera_name) + self.assertIsNone(manager.source_camera) + self.assertIsNone(manager.clip_path) + self.assertIsNone(manager.start_ts) + self.assertIsNone(manager.end_ts) + + +class TestDebugReplayManagerStop(unittest.TestCase): + def setUp(self) -> None: + # stop() publishes a terminal job_state via a real JobStatePublisher, + # which opens a ZMQ REQ socket and blocks on REP. No dispatcher runs + # in unit tests, so substitute a no-op publisher. + patcher = patch("frigate.debug_replay.JobStatePublisher") + patcher.start() + self.addCleanup(patcher.stop) + + def test_stop_when_inactive_is_a_noop(self) -> None: + from frigate.debug_replay import DebugReplayManager + + manager = DebugReplayManager() + frigate_config = MagicMock() + frigate_config.cameras = {} + publisher = MagicMock() + + # Should not raise; should not publish any events. + manager.stop(frigate_config=frigate_config, config_publisher=publisher) + + publisher.publish_update.assert_not_called() + + def test_stop_publishes_remove_when_camera_was_published(self) -> None: + from frigate.config.camera.updater import CameraConfigUpdateEnum + from frigate.debug_replay import DebugReplayManager + + manager = DebugReplayManager() + manager.mark_starting("front", "_replay_front", 100.0, 200.0) + manager.mark_session_ready("/tmp/replay/_replay_front.mp4") + + camera_config = MagicMock() + frigate_config = MagicMock() + frigate_config.cameras = {"_replay_front": camera_config} + publisher = MagicMock() + + with ( + patch.object(manager, "_cleanup_db"), + patch.object(manager, "_cleanup_files"), + patch("frigate.debug_replay.cancel_debug_replay_job", return_value=False), + ): + manager.stop(frigate_config=frigate_config, config_publisher=publisher) + + # One publish_update call with a remove topic. + self.assertEqual(publisher.publish_update.call_count, 1) + topic_arg = publisher.publish_update.call_args.args[0] + self.assertEqual(topic_arg.update_type, CameraConfigUpdateEnum.remove) + self.assertFalse(manager.active) + + def test_stop_skips_remove_publish_when_camera_not_in_config(self) -> None: + """Cancellation during preparing_clip: no camera was published yet.""" + from frigate.debug_replay import DebugReplayManager + + manager = DebugReplayManager() + manager.mark_starting("front", "_replay_front", 100.0, 200.0) + # clip_path stays None because we cancelled before camera publish. + + frigate_config = MagicMock() + frigate_config.cameras = {} # _replay_front not present + publisher = MagicMock() + + with ( + patch.object(manager, "_cleanup_db"), + patch.object(manager, "_cleanup_files"), + patch("frigate.debug_replay.cancel_debug_replay_job", return_value=True), + ): + manager.stop(frigate_config=frigate_config, config_publisher=publisher) + + publisher.publish_update.assert_not_called() + self.assertFalse(manager.active) + + def test_stop_calls_cancel_debug_replay_job(self) -> None: + from frigate.debug_replay import DebugReplayManager + + manager = DebugReplayManager() + manager.mark_starting("front", "_replay_front", 100.0, 200.0) + + frigate_config = MagicMock() + frigate_config.cameras = {} + publisher = MagicMock() + + with ( + patch.object(manager, "_cleanup_db"), + patch.object(manager, "_cleanup_files"), + patch( + "frigate.debug_replay.cancel_debug_replay_job", + return_value=True, + ) as mock_cancel, + ): + manager.stop(frigate_config=frigate_config, config_publisher=publisher) + + mock_cancel.assert_called_once() + + +class TestDebugReplayManagerPublishCamera(unittest.TestCase): + def test_publish_camera_invokes_publisher_with_add_topic(self) -> None: + from frigate.config.camera.updater import CameraConfigUpdateEnum + from frigate.debug_replay import DebugReplayManager + + manager = DebugReplayManager() + + source_config = MagicMock() + new_camera_config = MagicMock() + frigate_config = MagicMock() + frigate_config.cameras = {"front": source_config} + publisher = MagicMock() + + with ( + patch.object( + manager, + "_build_camera_config_dict", + return_value={"enabled": True}, + ), + patch("frigate.debug_replay.find_config_file", return_value="/cfg.yml"), + patch("frigate.debug_replay.YAML") as yaml_cls, + patch("frigate.debug_replay.FrigateConfig.parse_object") as parse_object, + patch("builtins.open", unittest.mock.mock_open(read_data="cameras:\n")), + ): + yaml_instance = yaml_cls.return_value + yaml_instance.load.return_value = {"cameras": {}} + parsed = MagicMock() + parsed.cameras = {"_replay_front": new_camera_config} + parse_object.return_value = parsed + + manager.publish_camera( + source_camera="front", + replay_name="_replay_front", + clip_path="/tmp/clip.mp4", + frigate_config=frigate_config, + config_publisher=publisher, + ) + + # Camera registered into the live config dict + self.assertIn("_replay_front", frigate_config.cameras) + # Publisher invoked with an add topic + self.assertEqual(publisher.publish_update.call_count, 1) + topic_arg = publisher.publish_update.call_args.args[0] + self.assertEqual(topic_arg.update_type, CameraConfigUpdateEnum.add) + + def test_publish_camera_wraps_parse_failure_in_runtime_error(self) -> None: + from frigate.debug_replay import DebugReplayManager + + manager = DebugReplayManager() + frigate_config = MagicMock() + frigate_config.cameras = {"front": MagicMock()} + publisher = MagicMock() + + with ( + patch.object( + manager, + "_build_camera_config_dict", + return_value={"enabled": True}, + ), + patch("frigate.debug_replay.find_config_file", return_value="/cfg.yml"), + patch("frigate.debug_replay.YAML") as yaml_cls, + patch( + "frigate.debug_replay.FrigateConfig.parse_object", + side_effect=ValueError("zone foo has invalid coordinates"), + ), + patch("builtins.open", unittest.mock.mock_open(read_data="cameras:\n")), + ): + yaml_cls.return_value.load.return_value = {"cameras": {}} + + with self.assertRaises(RuntimeError) as ctx: + manager.publish_camera( + source_camera="front", + replay_name="_replay_front", + clip_path="/tmp/clip.mp4", + frigate_config=frigate_config, + config_publisher=publisher, + ) + + self.assertIn("replay camera config", str(ctx.exception)) + self.assertIn("invalid coordinates", str(ctx.exception)) + publisher.publish_update.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_debug_replay_job.py b/frigate/test/test_debug_replay_job.py new file mode 100644 index 0000000..12c4be8 --- /dev/null +++ b/frigate/test/test_debug_replay_job.py @@ -0,0 +1,488 @@ +"""Tests for the debug replay job runner and factory.""" + +import threading +import time +import unittest +import unittest.mock +from unittest.mock import MagicMock, patch + +from frigate.debug_replay import DebugReplayManager +from frigate.jobs.debug_replay import ( + DebugReplayJob, + RecordingDebugReplaySource, + cancel_debug_replay_job, + get_active_runner, + start_debug_replay_job, +) +from frigate.jobs.export import JobStatePublisher +from frigate.jobs.manager import _completed_jobs, _current_jobs +from frigate.types import JobStatusTypesEnum + + +def _reset_job_manager() -> None: + """Clear the global job manager state between tests.""" + _current_jobs.clear() + _completed_jobs.clear() + + +def _patch_publisher(test_case: unittest.TestCase) -> None: + """Replace JobStatePublisher.publish with a no-op to avoid hanging on IPC.""" + publisher_patch = patch.object( + JobStatePublisher, "publish", lambda self, payload: None + ) + publisher_patch.start() + test_case.addCleanup(publisher_patch.stop) + + +class TestDebugReplayJob(unittest.TestCase): + def test_default_fields(self) -> None: + job = DebugReplayJob() + + self.assertEqual(job.job_type, "debug_replay") + self.assertEqual(job.status, JobStatusTypesEnum.queued) + self.assertIsNone(job.current_step) + self.assertEqual(job.progress_percent, 0.0) + + def test_to_dict_whitelist(self) -> None: + job = DebugReplayJob( + source_camera="front", + replay_camera_name="_replay_front", + start_ts=100.0, + end_ts=200.0, + ) + job.current_step = "preparing_clip" + job.progress_percent = 42.5 + + payload = job.to_dict() + + # Top-level matches the standard Job shape. + for key in ( + "id", + "job_type", + "status", + "start_time", + "end_time", + "error_message", + "results", + ): + self.assertIn(key, payload, f"missing top-level field: {key}") + + results = payload["results"] + self.assertEqual(results["source_camera"], "front") + self.assertEqual(results["replay_camera_name"], "_replay_front") + self.assertEqual(results["current_step"], "preparing_clip") + self.assertEqual(results["progress_percent"], 42.5) + self.assertEqual(results["start_ts"], 100.0) + self.assertEqual(results["end_ts"], 200.0) + + +class TestStartDebugReplayJob(unittest.TestCase): + def setUp(self) -> None: + _reset_job_manager() + _patch_publisher(self) + self.manager = DebugReplayManager() + self.frigate_config = MagicMock() + self.frigate_config.cameras = {"front": MagicMock()} + self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true" + self.publisher = MagicMock() + + self.recordings_qs = MagicMock() + self.recordings_qs.count.return_value = 1 + self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")]) + + def tearDown(self) -> None: + runner = get_active_runner() + if runner is not None: + runner.cancel() + runner.join(timeout=2.0) + _reset_job_manager() + + def test_rejects_unknown_camera(self) -> None: + with self.assertRaises(ValueError): + start_debug_replay_job( + source=RecordingDebugReplaySource( + source_camera="missing", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, + ), + frigate_config=self.frigate_config, + config_publisher=self.publisher, + replay_manager=self.manager, + ) + + def test_rejects_invalid_time_range(self) -> None: + with self.assertRaises(ValueError): + start_debug_replay_job( + source=RecordingDebugReplaySource( + source_camera="front", + start_ts=200.0, + end_ts=100.0, + internal_port=5000, + ), + frigate_config=self.frigate_config, + config_publisher=self.publisher, + replay_manager=self.manager, + ) + + def test_rejects_when_no_recordings(self) -> None: + empty_qs = MagicMock() + empty_qs.count.return_value = 0 + with patch("frigate.jobs.debug_replay.query_recordings", return_value=empty_qs): + with self.assertRaises(ValueError): + start_debug_replay_job( + source=RecordingDebugReplaySource( + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, + ), + frigate_config=self.frigate_config, + config_publisher=self.publisher, + replay_manager=self.manager, + ) + + def test_returns_job_id_and_marks_session_starting(self) -> None: + block = threading.Event() + + def slow_helper(cmd, **kwargs): + block.wait(timeout=5) + return 0, "" + + with ( + patch( + "frigate.jobs.debug_replay.query_recordings", + return_value=self.recordings_qs, + ), + patch( + "frigate.jobs.debug_replay.run_ffmpeg_with_progress", + side_effect=slow_helper, + ), + patch.object(self.manager, "publish_camera"), + patch("os.path.exists", return_value=True), + patch("os.makedirs"), + patch("builtins.open", unittest.mock.mock_open()), + ): + job_id = start_debug_replay_job( + source=RecordingDebugReplaySource( + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, + ), + frigate_config=self.frigate_config, + config_publisher=self.publisher, + replay_manager=self.manager, + ) + + self.assertIsInstance(job_id, str) + self.assertTrue(self.manager.active) + self.assertEqual(self.manager.replay_camera_name, "_replay_front") + self.assertEqual(self.manager.source_camera, "front") + + block.set() + + def test_rejects_concurrent_calls(self) -> None: + block = threading.Event() + + def slow_helper(cmd, **kwargs): + block.wait(timeout=5) + return 0, "" + + with ( + patch( + "frigate.jobs.debug_replay.query_recordings", + return_value=self.recordings_qs, + ), + patch( + "frigate.jobs.debug_replay.run_ffmpeg_with_progress", + side_effect=slow_helper, + ), + patch.object(self.manager, "publish_camera"), + patch("os.path.exists", return_value=True), + patch("os.makedirs"), + patch("builtins.open", unittest.mock.mock_open()), + ): + start_debug_replay_job( + source=RecordingDebugReplaySource( + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, + ), + frigate_config=self.frigate_config, + config_publisher=self.publisher, + replay_manager=self.manager, + ) + + with self.assertRaises(RuntimeError): + start_debug_replay_job( + source=RecordingDebugReplaySource( + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, + ), + frigate_config=self.frigate_config, + config_publisher=self.publisher, + replay_manager=self.manager, + ) + + block.set() + + +class TestRunnerHappyPath(unittest.TestCase): + def setUp(self) -> None: + _reset_job_manager() + _patch_publisher(self) + self.manager = DebugReplayManager() + self.frigate_config = MagicMock() + self.frigate_config.cameras = {"front": MagicMock()} + self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true" + self.publisher = MagicMock() + + self.recordings_qs = MagicMock() + self.recordings_qs.count.return_value = 1 + self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")]) + + def tearDown(self) -> None: + runner = get_active_runner() + if runner is not None: + runner.cancel() + runner.join(timeout=2.0) + _reset_job_manager() + + def _wait_for(self, predicate, timeout: float = 5.0) -> bool: + deadline = time.time() + timeout + while time.time() < deadline: + if predicate(): + return True + time.sleep(0.02) + return False + + def test_progress_callback_updates_job_percent(self) -> None: + captured: list[float] = [] + + def fake_helper(cmd, *, on_progress=None, **kwargs): + on_progress(0.0) + on_progress(50.0) + on_progress(100.0) + return 0, "" + + with ( + patch( + "frigate.jobs.debug_replay.query_recordings", + return_value=self.recordings_qs, + ), + patch( + "frigate.jobs.debug_replay.run_ffmpeg_with_progress", + side_effect=fake_helper, + ), + patch.object( + self.manager, + "publish_camera", + side_effect=lambda *a, **kw: captured.append("published"), + ), + patch("os.path.exists", return_value=True), + patch("os.makedirs"), + patch("builtins.open", unittest.mock.mock_open()), + ): + start_debug_replay_job( + source=RecordingDebugReplaySource( + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, + ), + frigate_config=self.frigate_config, + config_publisher=self.publisher, + replay_manager=self.manager, + ) + + self.assertTrue( + self._wait_for(lambda: get_active_runner() is None), + "runner did not finish", + ) + + from frigate.jobs.manager import get_current_job + + job = get_current_job("debug_replay") + self.assertIsNotNone(job) + self.assertEqual(job.status, JobStatusTypesEnum.success) + self.assertEqual(job.progress_percent, 100.0) + self.assertEqual(captured, ["published"]) + # Manager should have been told the session is ready with the clip path. + self.assertIsNotNone(self.manager.clip_path) + + +class TestRunnerFailurePath(unittest.TestCase): + def setUp(self) -> None: + _reset_job_manager() + _patch_publisher(self) + self.manager = DebugReplayManager() + self.frigate_config = MagicMock() + self.frigate_config.cameras = {"front": MagicMock()} + self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true" + self.publisher = MagicMock() + self.recordings_qs = MagicMock() + self.recordings_qs.count.return_value = 1 + self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")]) + + def tearDown(self) -> None: + runner = get_active_runner() + if runner is not None: + runner.cancel() + runner.join(timeout=2.0) + _reset_job_manager() + + def _wait_for(self, predicate, timeout: float = 5.0) -> bool: + deadline = time.time() + timeout + while time.time() < deadline: + if predicate(): + return True + time.sleep(0.02) + return False + + def test_ffmpeg_failure_marks_job_failed_and_clears_session(self) -> None: + def failing_helper(cmd, **kwargs): + return 1, "ffmpeg exploded" + + with ( + patch( + "frigate.jobs.debug_replay.query_recordings", + return_value=self.recordings_qs, + ), + patch( + "frigate.jobs.debug_replay.run_ffmpeg_with_progress", + side_effect=failing_helper, + ), + patch("os.path.exists", return_value=True), + patch("os.makedirs"), + patch("os.remove"), + patch("builtins.open", unittest.mock.mock_open()), + ): + start_debug_replay_job( + source=RecordingDebugReplaySource( + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, + ), + frigate_config=self.frigate_config, + config_publisher=self.publisher, + replay_manager=self.manager, + ) + + self.assertTrue( + self._wait_for(lambda: get_active_runner() is None), + "runner did not finish", + ) + + from frigate.jobs.manager import get_current_job + + job = get_current_job("debug_replay") + self.assertIsNotNone(job) + self.assertEqual(job.status, JobStatusTypesEnum.failed) + self.assertIsNotNone(job.error_message) + self.assertIn("ffmpeg", job.error_message.lower()) + # Session cleared so a new /start is allowed + self.assertFalse(self.manager.active) + + +class TestRunnerCancellation(unittest.TestCase): + def setUp(self) -> None: + _reset_job_manager() + _patch_publisher(self) + self.manager = DebugReplayManager() + self.frigate_config = MagicMock() + self.frigate_config.cameras = {"front": MagicMock()} + self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true" + self.publisher = MagicMock() + self.recordings_qs = MagicMock() + self.recordings_qs.count.return_value = 1 + self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")]) + + def tearDown(self) -> None: + runner = get_active_runner() + if runner is not None: + runner.cancel() + runner.join(timeout=2.0) + _reset_job_manager() + + def _wait_for(self, predicate, timeout: float = 5.0) -> bool: + deadline = time.time() + timeout + while time.time() < deadline: + if predicate(): + return True + time.sleep(0.02) + return False + + def test_cancel_terminates_ffmpeg_and_marks_cancelled(self) -> None: + terminated = threading.Event() + fake_proc = MagicMock() + fake_proc.terminate = MagicMock(side_effect=lambda: terminated.set()) + + def fake_helper(cmd, *, process_started=None, **kwargs): + if process_started is not None: + process_started(fake_proc) + terminated.wait(timeout=5) + return -15, "killed" + + with ( + patch( + "frigate.jobs.debug_replay.query_recordings", + return_value=self.recordings_qs, + ), + patch( + "frigate.jobs.debug_replay.run_ffmpeg_with_progress", + side_effect=fake_helper, + ), + patch("os.path.exists", return_value=True), + patch("os.makedirs"), + patch("os.remove"), + patch("builtins.open", unittest.mock.mock_open()), + ): + start_debug_replay_job( + source=RecordingDebugReplaySource( + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, + ), + frigate_config=self.frigate_config, + config_publisher=self.publisher, + replay_manager=self.manager, + ) + + # Wait for the runner to register the active process. + self.assertTrue( + self._wait_for( + lambda: ( + get_active_runner() is not None + and get_active_runner()._active_process is fake_proc + ) + ) + ) + + cancelled = cancel_debug_replay_job() + self.assertTrue(cancelled) + self.assertTrue(fake_proc.terminate.called) + + self.assertTrue( + self._wait_for(lambda: get_active_runner() is None), + "runner did not finish", + ) + + from frigate.jobs.manager import get_current_job + + job = get_current_job("debug_replay") + self.assertEqual(job.status, JobStatusTypesEnum.cancelled) + # Runner must not clear the manager session on cancellation — + # that belongs to the caller of cancel_debug_replay_job (stop()). + # If the runner cleared it, stop() would log "no active session" + # and skip its cleanup_db / cleanup_files calls. + self.assertTrue(self.manager.active) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_deferred_processor.py b/frigate/test/test_deferred_processor.py new file mode 100644 index 0000000..c76b445 --- /dev/null +++ b/frigate/test/test_deferred_processor.py @@ -0,0 +1,211 @@ +"""Tests for DeferredRealtimeProcessorApi.""" + +import sys +import time +import unittest +from typing import Any +from unittest.mock import MagicMock, patch + +import numpy as np + +from frigate.data_processing.real_time.api import DeferredRealtimeProcessorApi + +# Mock TFLite before importing classification module +_MOCK_MODULES = [ + "tflite_runtime", + "tflite_runtime.interpreter", + "ai_edge_litert", + "ai_edge_litert.interpreter", +] +for mod in _MOCK_MODULES: + if mod not in sys.modules: + sys.modules[mod] = MagicMock() + +from frigate.data_processing.real_time.custom_classification import ( # noqa: E402 + CustomObjectClassificationProcessor, +) + + +class StubDeferredProcessor(DeferredRealtimeProcessorApi): + """Minimal concrete subclass for testing the deferred base.""" + + def __init__(self, max_queue: int = 8): + config = MagicMock() + metrics = MagicMock() + super().__init__(config, metrics, max_queue=max_queue) + self.processed_items: list[tuple] = [] + + def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray) -> None: + """Enqueue every call — no gating logic in the stub.""" + self._enqueue_task(("frame", obj_data, frame.copy())) + + def _process_task(self, task: tuple) -> None: + kind = task[0] + if kind == "frame": + _, obj_data, frame = task + self.processed_items.append((obj_data["id"], frame.shape)) + self._emit_result( + { + "type": "test_result", + "id": obj_data["id"], + "label": "cat", + "score": 0.95, + } + ) + elif kind == "expire": + _, object_id = task + self.processed_items.append(("expired", object_id)) + + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + if topic == "reload": + + def _do_reload(data): + return {"success": True, "model": data.get("name")} + + return self._enqueue_request(_do_reload, request_data) + return None + + def expire_object(self, object_id: str, camera: str) -> None: + self._enqueue_task(("expire", object_id)) + + +class TestDeferredProcessorBase(unittest.TestCase): + def test_enqueue_and_drain(self): + """Tasks enqueued on main thread are processed by worker, results are drainable.""" + proc = StubDeferredProcessor() + frame = np.zeros((100, 100, 3), dtype=np.uint8) + proc.process_frame({"id": "obj1"}, frame) + proc.process_frame({"id": "obj2"}, frame) + + # Give the worker time to process + time.sleep(0.1) + + results = proc.drain_results() + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["id"], "obj1") + self.assertEqual(results[1]["id"], "obj2") + + # Second drain should be empty + self.assertEqual(len(proc.drain_results()), 0) + + def test_backpressure_drops_tasks(self): + """When queue is full, new tasks are silently dropped.""" + proc = StubDeferredProcessor(max_queue=2) + + frame = np.zeros((10, 10, 3), dtype=np.uint8) + for i in range(10): + proc.process_frame({"id": f"obj{i}"}, frame) + + time.sleep(0.2) + results = proc.drain_results() + # The key property: no crash, no unbounded growth + self.assertLessEqual(len(results), 10) + self.assertGreater(len(results), 0) + + def test_handle_request_through_worker(self): + """handle_request blocks until the worker processes it and returns a response.""" + proc = StubDeferredProcessor() + result = proc.handle_request("reload", {"name": "my_model"}) + self.assertEqual(result, {"success": True, "model": "my_model"}) + + def test_expire_object_serialized_with_work(self): + """expire_object goes through the queue, serialized with inference work.""" + proc = StubDeferredProcessor() + frame = np.zeros((10, 10, 3), dtype=np.uint8) + proc.process_frame({"id": "obj1"}, frame) + proc.expire_object("obj1", "front_door") + + time.sleep(0.1) + # Both should have been processed in order + self.assertEqual(len(proc.processed_items), 2) + self.assertEqual(proc.processed_items[0][0], "obj1") + self.assertEqual(proc.processed_items[1], ("expired", "obj1")) + + def test_shutdown_joins_worker(self): + """shutdown() signals the worker to stop and joins the thread.""" + proc = StubDeferredProcessor() + proc.shutdown() + self.assertFalse(proc._worker.is_alive()) + + def test_drain_results_returns_list(self): + """drain_results returns a plain list, not a deque.""" + proc = StubDeferredProcessor() + results = proc.drain_results() + self.assertIsInstance(results, list) + + +class TestCustomObjectClassificationDeferred(unittest.TestCase): + """Test that CustomObjectClassificationProcessor uses the deferred pattern correctly.""" + + def _make_processor(self): + config = MagicMock() + model_config = MagicMock() + model_config.name = "test_breed" + model_config.object_config = MagicMock() + model_config.object_config.objects = ["dog"] + model_config.threshold = 0.5 + model_config.save_attempts = 10 + model_config.object_config.classification_type = "sub_label" + publisher = MagicMock() + requestor = MagicMock() + metrics = MagicMock() + metrics.classification_speeds = {} + metrics.classification_cps = {} + + with patch.object( + CustomObjectClassificationProcessor, + "_CustomObjectClassificationProcessor__build_detector", + ): + proc = CustomObjectClassificationProcessor( + config, model_config, publisher, requestor, metrics + ) + proc.interpreter = None + proc.tensor_input_details = [{"index": 0}] + proc.tensor_output_details = [{"index": 0}] + proc.labelmap = {0: "labrador", 1: "poodle", 2: "none"} + return proc + + def test_is_deferred_processor(self): + """CustomObjectClassificationProcessor should be a DeferredRealtimeProcessorApi.""" + proc = self._make_processor() + self.assertIsInstance(proc, DeferredRealtimeProcessorApi) + + def test_expire_clears_history(self): + """expire_object should clear classification history for the object.""" + proc = self._make_processor() + proc.classification_history["obj1"] = [("labrador", 0.9, 1.0)] + + proc.expire_object("obj1", "front") + time.sleep(0.1) + + self.assertNotIn("obj1", proc.classification_history) + + def test_drain_results_empty_when_no_model(self): + """With no interpreter, process_frame saves training images but emits no results.""" + proc = self._make_processor() + proc.interpreter = None + + frame = np.zeros((150, 100), dtype=np.uint8) + obj_data = { + "id": "obj1", + "label": "dog", + "false_positive": False, + "end_time": None, + "box": [10, 10, 50, 50], + "camera": "front", + } + + with patch( + "frigate.data_processing.real_time.custom_classification.write_classification_attempt" + ): + proc.process_frame(obj_data, frame) + + time.sleep(0.1) + results = proc.drain_results() + self.assertEqual(len(results), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_dispatcher_runtime_state.py b/frigate/test/test_dispatcher_runtime_state.py new file mode 100644 index 0000000..dae0518 --- /dev/null +++ b/frigate/test/test_dispatcher_runtime_state.py @@ -0,0 +1,217 @@ +"""Tests for Dispatcher runtime state persistence wiring.""" + +import os +import tempfile +import unittest +from unittest.mock import MagicMock, patch + +from frigate.comms.dispatcher import Dispatcher +from frigate.comms.runtime_state import RuntimeStatePersistence + + +def _make_camera_mock( + *, + enabled: bool = True, + enabled_in_config: bool = True, + detect_enabled: bool = True, + record_enabled: bool = True, + record_enabled_in_config: bool = True, + snapshots_enabled: bool = True, + audio_enabled: bool = True, + audio_enabled_in_config: bool = True, +) -> MagicMock: + """Build a camera config mock with the fields the in-scope handlers read.""" + camera = MagicMock() + camera.enabled = enabled + camera.enabled_in_config = enabled_in_config + camera.detect.enabled = detect_enabled + camera.motion.enabled = True # avoid the detect→motion side-effect path + camera.record.enabled = record_enabled + camera.record.enabled_in_config = record_enabled_in_config + camera.snapshots.enabled = snapshots_enabled + camera.audio.enabled = audio_enabled + camera.audio.enabled_in_config = audio_enabled_in_config + return camera + + +def _build_dispatcher(cameras: dict[str, MagicMock]) -> Dispatcher: + """Construct a Dispatcher with the bare-minimum mocks the tests need.""" + config = MagicMock() + config.cameras = cameras + config_updater = MagicMock() + onvif = MagicMock() + ptz_metrics: dict = {} + communicators: list = [] + + with ( + patch("frigate.comms.dispatcher.CameraActivityManager"), + patch("frigate.comms.dispatcher.AudioActivityManager"), + ): + return Dispatcher(config, config_updater, onvif, ptz_metrics, communicators) + + +class TestRestoreRuntimeState(unittest.TestCase): + """Verify replay routes through handlers and tolerates missing entries.""" + + def setUp(self) -> None: + self.dispatcher = _build_dispatcher( + { + "front_door": _make_camera_mock(), + "back_yard": _make_camera_mock(), + } + ) + # Swap each in-scope handler for a MagicMock so we can assert calls + # without exercising the handler's own logic. + self.handler_mocks: dict[str, MagicMock] = {} + for topic in ("enabled", "detect", "snapshots", "recordings", "audio"): + mock = MagicMock() + self.dispatcher._camera_settings_handlers[topic] = mock + self.handler_mocks[topic] = mock + + def test_replays_each_stored_entry_through_its_handler(self) -> None: + self.dispatcher._runtime_state = MagicMock( + spec=RuntimeStatePersistence, + load=MagicMock( + return_value={ + "front_door": {"detect": False, "recordings": False}, + "back_yard": {"audio": False}, + } + ), + ) + self.dispatcher.restore_runtime_state() + + self.handler_mocks["detect"].assert_called_once_with("front_door", "OFF") + self.handler_mocks["recordings"].assert_called_once_with("front_door", "OFF") + self.handler_mocks["audio"].assert_called_once_with("back_yard", "OFF") + self.handler_mocks["enabled"].assert_not_called() + self.handler_mocks["snapshots"].assert_not_called() + + def test_skips_unknown_cameras(self) -> None: + self.dispatcher._runtime_state = MagicMock( + spec=RuntimeStatePersistence, + load=MagicMock(return_value={"removed_cam": {"detect": False}}), + ) + self.dispatcher.restore_runtime_state() + for mock in self.handler_mocks.values(): + mock.assert_not_called() + + def test_skips_unknown_topics(self) -> None: + self.dispatcher._runtime_state = MagicMock( + spec=RuntimeStatePersistence, + load=MagicMock(return_value={"front_door": {"some_old_topic": True}}), + ) + self.dispatcher.restore_runtime_state() + for mock in self.handler_mocks.values(): + mock.assert_not_called() + + def test_continues_after_handler_exception(self) -> None: + self.handler_mocks["detect"].side_effect = RuntimeError("boom") + self.dispatcher._runtime_state = MagicMock( + spec=RuntimeStatePersistence, + load=MagicMock( + return_value={ + "front_door": {"detect": False, "recordings": False}, + } + ), + ) + # Must not raise; the recordings handler must still run. + self.dispatcher.restore_runtime_state() + self.handler_mocks["recordings"].assert_called_once_with("front_door", "OFF") + + def test_true_value_routes_as_on_payload(self) -> None: + self.dispatcher._runtime_state = MagicMock( + spec=RuntimeStatePersistence, + load=MagicMock(return_value={"front_door": {"detect": True}}), + ) + self.dispatcher.restore_runtime_state() + self.handler_mocks["detect"].assert_called_once_with("front_door", "ON") + + +class TestHandlersPersistViaSet(unittest.TestCase): + """Verify each in-scope handler writes to the runtime state on success.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.mkdtemp() + self.config_path = os.path.join(self.tmp_dir, "config.yml") + with open(self.config_path, "w") as f: + f.write("") + self._patcher = patch( + "frigate.comms.runtime_state.find_config_file", + return_value=self.config_path, + ) + self._patcher.start() + + # Start with everything OFF so each ON payload triggers a real change + self.cameras = { + "front_door": _make_camera_mock( + enabled=False, + detect_enabled=False, + record_enabled=False, + snapshots_enabled=False, + audio_enabled=False, + ) + } + self.dispatcher = _build_dispatcher(self.cameras) + + def tearDown(self) -> None: + self._patcher.stop() + for name in os.listdir(self.tmp_dir): + os.remove(os.path.join(self.tmp_dir, name)) + os.rmdir(self.tmp_dir) + + def _stored_state(self) -> dict: + return RuntimeStatePersistence().load() + + def test_enabled_handler_persists(self) -> None: + self.dispatcher._on_enabled_command("front_door", "ON") + self.assertEqual(self._stored_state(), {"front_door": {"enabled": True}}) + + def test_detect_handler_persists(self) -> None: + self.dispatcher._on_detect_command("front_door", "ON") + self.assertEqual(self._stored_state(), {"front_door": {"detect": True}}) + + def test_recordings_handler_persists(self) -> None: + self.dispatcher._on_recordings_command("front_door", "ON") + self.assertEqual(self._stored_state(), {"front_door": {"recordings": True}}) + + def test_snapshots_handler_persists(self) -> None: + self.dispatcher._on_snapshots_command("front_door", "ON") + self.assertEqual(self._stored_state(), {"front_door": {"snapshots": True}}) + + def test_audio_handler_persists(self) -> None: + self.dispatcher._on_audio_command("front_door", "ON") + self.assertEqual(self._stored_state(), {"front_door": {"audio": True}}) + + def test_enabled_in_config_gate_blocks_persistence(self) -> None: + """An ON payload rejected by the gate must not be persisted.""" + cam = self.cameras["front_door"] + cam.enabled_in_config = False + cam.record.enabled_in_config = False + cam.audio.enabled_in_config = False + + self.dispatcher._on_enabled_command("front_door", "ON") + self.dispatcher._on_recordings_command("front_door", "ON") + self.dispatcher._on_audio_command("front_door", "ON") + + self.assertEqual(self._stored_state(), {}) + + +class TestClearPassthrough(unittest.TestCase): + """The dispatcher's public clear methods delegate to the store.""" + + def test_clear_runtime_state_for_yaml_keys_passthrough(self) -> None: + dispatcher = _build_dispatcher({}) + dispatcher._runtime_state = MagicMock(spec=RuntimeStatePersistence) + keys = ["cameras.front_door.detect.enabled"] + dispatcher.clear_runtime_state_for_yaml_keys(keys) + dispatcher._runtime_state.clear_for_yaml_keys.assert_called_once_with(keys) + + def test_clear_runtime_state_passthrough(self) -> None: + dispatcher = _build_dispatcher({}) + dispatcher._runtime_state = MagicMock(spec=RuntimeStatePersistence) + dispatcher.clear_runtime_state() + dispatcher._runtime_state.clear_all.assert_called_once_with() + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_env.py b/frigate/test/test_env.py new file mode 100644 index 0000000..37b81a6 --- /dev/null +++ b/frigate/test/test_env.py @@ -0,0 +1,237 @@ +"""Tests for environment variable handling.""" + +import os +import unittest +from unittest.mock import MagicMock, patch + +from frigate.config.env import ( + FRIGATE_ENV_VARS, + validate_env_string, + validate_env_vars, +) + + +class TestGo2RtcAddStreamSubstitution(unittest.TestCase): + """Covers the API path: PUT /go2rtc/streams/{stream_name}. + + The route shells out to go2rtc via `requests.put`; we mock the HTTP call + and assert that the substituted `src` parameter handles the same mixed + {FRIGATE_*} + literal-brace strings as the config-loading path. + """ + + def setUp(self): + self._original_env_vars = dict(FRIGATE_ENV_VARS) + + def tearDown(self): + FRIGATE_ENV_VARS.clear() + FRIGATE_ENV_VARS.update(self._original_env_vars) + + def _call_route(self, src: str) -> str: + """Invoke go2rtc_add_stream and return the substituted src param.""" + from frigate.api import camera as camera_api + + captured = {} + + def fake_put(url, params=None, timeout=None): + captured["params"] = params + resp = MagicMock() + resp.ok = True + resp.text = "" + resp.status_code = 200 + return resp + + with patch.object(camera_api.requests, "put", side_effect=fake_put): + camera_api.go2rtc_add_stream( + request=MagicMock(), stream_name="cam1", src=src + ) + return captured["params"]["src"] + + def test_mixed_localtime_and_frigate_var(self): + """%{localtime\\:...} alongside {FRIGATE_USER} substitutes only the var.""" + FRIGATE_ENV_VARS["FRIGATE_USER"] = "admin" + src = ( + "ffmpeg:rtsp://host/s#raw=-vf " + "drawtext=text=%{localtime\\:%Y-%m-%d}:user={FRIGATE_USER}" + ) + self.assertEqual( + self._call_route(src), + "ffmpeg:rtsp://host/s#raw=-vf " + "drawtext=text=%{localtime\\:%Y-%m-%d}:user=admin", + ) + + def test_unknown_var_falls_back_to_raw_src(self): + """Existing route behavior: unknown {FRIGATE_*} keeps raw src.""" + src = "rtsp://host/{FRIGATE_NONEXISTENT}/stream" + self.assertEqual(self._call_route(src), src) + + def test_malformed_placeholder_rejected_via_api(self): + """Malformed FRIGATE placeholders raise (not silently passed through). + + Regression: previously camera.py caught any KeyError and fell back + to the raw src, so `{FRIGATE_FOO:>5}` was silently accepted via the + API while config loading rejected it. The helper now raises + ValueError for malformed syntax to keep the two paths consistent. + """ + with self.assertRaises(ValueError): + self._call_route("rtsp://host/{FRIGATE_FOO:>5}/stream") + + +class TestEnvString(unittest.TestCase): + def setUp(self): + self._original_env_vars = dict(FRIGATE_ENV_VARS) + + def tearDown(self): + FRIGATE_ENV_VARS.clear() + FRIGATE_ENV_VARS.update(self._original_env_vars) + + def test_substitution(self): + """EnvString substitutes FRIGATE_ env vars.""" + FRIGATE_ENV_VARS["FRIGATE_TEST_HOST"] = "192.168.1.100" + result = validate_env_string("{FRIGATE_TEST_HOST}") + self.assertEqual(result, "192.168.1.100") + + def test_substitution_in_url(self): + """EnvString substitutes vars embedded in a URL.""" + FRIGATE_ENV_VARS["FRIGATE_CAM_USER"] = "admin" + FRIGATE_ENV_VARS["FRIGATE_CAM_PASS"] = "secret" + result = validate_env_string( + "rtsp://{FRIGATE_CAM_USER}:{FRIGATE_CAM_PASS}@10.0.0.1/stream" + ) + self.assertEqual(result, "rtsp://admin:secret@10.0.0.1/stream") + + def test_no_placeholder(self): + """Plain strings pass through unchanged.""" + result = validate_env_string("192.168.1.1") + self.assertEqual(result, "192.168.1.1") + + def test_unknown_var_raises(self): + """Referencing an unknown var raises KeyError.""" + with self.assertRaises(KeyError): + validate_env_string("{FRIGATE_NONEXISTENT_VAR}") + + def test_non_frigate_braces_passthrough(self): + """Braces that are not {FRIGATE_*} placeholders pass through untouched. + + Regression test for ffmpeg drawtext expressions like + "%{localtime\\:%Y-%m-%d}" being mangled by str.format(). + """ + expr = ( + "ffmpeg:rtsp://127.0.0.1/src#raw=-vf " + "drawtext=text=%{localtime\\:%Y-%m-%d_%H\\:%M\\:%S}" + ":x=5:fontcolor=white" + ) + self.assertEqual(validate_env_string(expr), expr) + + def test_double_brace_escape_preserved(self): + """`{{output}}` collapses to `{output}` (documented go2rtc escape).""" + result = validate_env_string( + "exec:ffmpeg -i /media/file.mp4 -f rtsp {{output}}" + ) + self.assertEqual(result, "exec:ffmpeg -i /media/file.mp4 -f rtsp {output}") + + def test_double_brace_around_frigate_var(self): + """`{{FRIGATE_FOO}}` stays literal — escape takes precedence.""" + FRIGATE_ENV_VARS["FRIGATE_FOO"] = "bar" + self.assertEqual(validate_env_string("{{FRIGATE_FOO}}"), "{FRIGATE_FOO}") + + def test_mixed_frigate_var_and_braces(self): + """A FRIGATE_ var alongside literal single braces substitutes only the var.""" + FRIGATE_ENV_VARS["FRIGATE_USER"] = "admin" + result = validate_env_string( + "drawtext=text=%{localtime}:user={FRIGATE_USER}:x=5" + ) + self.assertEqual(result, "drawtext=text=%{localtime}:user=admin:x=5") + + def test_triple_braces_around_frigate_var(self): + """`{{{FRIGATE_FOO}}}` collapses like str.format(): `{bar}`.""" + FRIGATE_ENV_VARS["FRIGATE_FOO"] = "bar" + self.assertEqual(validate_env_string("{{{FRIGATE_FOO}}}"), "{bar}") + + def test_trailing_double_brace_after_var(self): + """`{FRIGATE_FOO}}}` collapses like str.format(): `bar}`.""" + FRIGATE_ENV_VARS["FRIGATE_FOO"] = "bar" + self.assertEqual(validate_env_string("{FRIGATE_FOO}}}"), "bar}") + + def test_leading_double_brace_then_var(self): + """`{{{FRIGATE_FOO}` collapses like str.format(): `{bar`.""" + FRIGATE_ENV_VARS["FRIGATE_FOO"] = "bar" + self.assertEqual(validate_env_string("{{{FRIGATE_FOO}"), "{bar") + + def test_malformed_unterminated_placeholder_raises(self): + """`{FRIGATE_FOO` (no closing brace) raises like str.format() did.""" + FRIGATE_ENV_VARS["FRIGATE_FOO"] = "bar" + with self.assertRaises(ValueError): + validate_env_string("prefix-{FRIGATE_FOO") + + def test_malformed_format_spec_raises(self): + """`{FRIGATE_FOO:>5}` (format spec) raises like str.format() did.""" + FRIGATE_ENV_VARS["FRIGATE_FOO"] = "bar" + with self.assertRaises(ValueError): + validate_env_string("{FRIGATE_FOO:>5}") + + def test_malformed_conversion_raises(self): + """`{FRIGATE_FOO!r}` (conversion) raises like str.format() did.""" + FRIGATE_ENV_VARS["FRIGATE_FOO"] = "bar" + with self.assertRaises(ValueError): + validate_env_string("{FRIGATE_FOO!r}") + + +class TestEnvVars(unittest.TestCase): + def setUp(self): + self._original_env_vars = dict(FRIGATE_ENV_VARS) + self._original_environ = os.environ.copy() + + def tearDown(self): + FRIGATE_ENV_VARS.clear() + FRIGATE_ENV_VARS.update(self._original_env_vars) + # Clean up any env vars we set + for key in list(os.environ.keys()): + if key not in self._original_environ: + del os.environ[key] + + def _make_context(self, install: bool): + """Create a mock ValidationInfo with the given install flag.""" + + class MockContext: + def __init__(self, ctx): + self.context = ctx + + mock = MockContext({"install": install}) + return mock + + def test_install_sets_os_environ(self): + """validate_env_vars with install=True sets os.environ.""" + ctx = self._make_context(install=True) + validate_env_vars({"MY_CUSTOM_VAR": "value123"}, ctx) + self.assertEqual(os.environ.get("MY_CUSTOM_VAR"), "value123") + + def test_install_updates_frigate_env_vars(self): + """validate_env_vars with install=True updates FRIGATE_ENV_VARS for FRIGATE_ keys.""" + ctx = self._make_context(install=True) + validate_env_vars({"FRIGATE_MQTT_PASS": "secret"}, ctx) + self.assertEqual(FRIGATE_ENV_VARS["FRIGATE_MQTT_PASS"], "secret") + + def test_install_skips_non_frigate_in_env_vars_dict(self): + """Non-FRIGATE_ keys are set in os.environ but not in FRIGATE_ENV_VARS.""" + ctx = self._make_context(install=True) + validate_env_vars({"OTHER_VAR": "value"}, ctx) + self.assertEqual(os.environ.get("OTHER_VAR"), "value") + self.assertNotIn("OTHER_VAR", FRIGATE_ENV_VARS) + + def test_no_install_does_not_set(self): + """validate_env_vars without install=True does not modify state.""" + ctx = self._make_context(install=False) + validate_env_vars({"FRIGATE_SKIP": "nope"}, ctx) + self.assertNotIn("FRIGATE_SKIP", FRIGATE_ENV_VARS) + self.assertNotIn("FRIGATE_SKIP", os.environ) + + def test_env_vars_available_for_env_string(self): + """Vars set via validate_env_vars are usable in validate_env_string.""" + ctx = self._make_context(install=True) + validate_env_vars({"FRIGATE_BROKER": "mqtt.local"}, ctx) + result = validate_env_string("{FRIGATE_BROKER}") + self.assertEqual(result, "mqtt.local") + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_export.py b/frigate/test/test_export.py new file mode 100644 index 0000000..7612a41 --- /dev/null +++ b/frigate/test/test_export.py @@ -0,0 +1,132 @@ +import unittest + +from frigate.record.export import validate_ffmpeg_args + + +class TestValidateFfmpegArgs(unittest.TestCase): + """Tests for the non-admin custom export ffmpeg arg validator. + + The validator uses a structural allowlist: every token must be an + allowlisted flag or the value of one, filter values are restricted to a + safe set of filters, and no token may become a bare input/output URL. + """ + + def assertRejected(self, args: str) -> None: + valid, message = validate_ffmpeg_args(args) + self.assertFalse(valid, f"expected {args!r} to be rejected") + self.assertNotEqual(message, "") + + def assertAllowed(self, args: str) -> None: + valid, message = validate_ffmpeg_args(args) + self.assertTrue(valid, f"expected {args!r} to be allowed, got: {message}") + self.assertEqual(message, "") + + # --- legitimate use cases must keep working --------------------------- + + def test_timelapse_setpts_allowed(self): + # The whole reason -vf cannot simply be blocked: timelapse exports. + self.assertAllowed("-vf setpts=PTS/60 -r 25") + self.assertAllowed("-vf setpts=0.04*PTS -r 30") # server default + self.assertAllowed("-filter:v setpts=PTS/60 -r 25") + + def test_default_input_args_allowed(self): + self.assertAllowed("") + self.assertAllowed("-an -skip_frame nokey") + + def test_encoding_args_allowed(self): + self.assertAllowed("-c:v libx264 -crf 23 -preset fast") + self.assertAllowed("-c:v copy -c:a copy") + self.assertAllowed("-c:v libx264 -b:v 2M -maxrate 2M -bufsize 4M") + self.assertAllowed("-movflags +faststart") + self.assertAllowed("-pix_fmt yuv420p -r 30 -g 30") + + def test_safe_filters_allowed(self): + self.assertAllowed("-vf scale=640:480") + self.assertAllowed("-vf scale=640:480,setpts=0.5*PTS") + self.assertAllowed("-vf format=yuv420p") + self.assertAllowed("-vf transpose=1") + self.assertAllowed("-vf hflip") + self.assertAllowed("-vf fps=15") + self.assertAllowed("-vf setsar=1 -an") + self.assertAllowed("-vf setdar=16/9") + + # --- the reported advisory and file-read class ------------------------ + + def test_reported_advisory_rejected(self): + self.assertRejected( + "-filter:v drawtext=textfile=/etc/passwd:fontcolor=white:fontsize=20" + ) + + def test_file_reading_filters_rejected(self): + self.assertRejected("-vf movie=/etc/passwd") + self.assertRejected("-vf drawtext=textfile=/etc/passwd") + self.assertRejected("-vf subtitles=/etc/passwd") + # marker embedded as an option of an otherwise-allowed filter name + self.assertRejected("-vf scale=movie=/etc/passwd") + + def test_filtergraph_brackets_rejected(self): + # link labels aren't needed for safe filters; rejecting "[" / "]" keeps + # filtergraph validation linear (no ReDoS on attacker input) + self.assertRejected("-vf [in]scale=640:480[out]") + self.assertRejected("-vf " + "[" * 5000) + + def test_preset_file_read_rejected(self): + # cwd-anchored traversal slipped past the old startswith() path check + self.assertRejected("-fpre frigate/../../../etc/passwd") + self.assertRejected("-fpre evil.preset") + self.assertRejected("-vpre x") + self.assertRejected("-apre x") + self.assertRejected("-pre x") + + def test_slash_option_file_read_rejected(self): + # ffmpeg "-/option file" reads the option value from a file + self.assertRejected("-/filter:v graph.txt") + self.assertRejected("-/filter_complex graph.txt") + + # --- network / SSRF class --------------------------------------------- + + def test_schemeless_protocol_rejected(self): + self.assertRejected("-f mpegts tcp:10.0.0.5:4444") + self.assertRejected("tcp:10.0.0.5:4444") + self.assertRejected("udp:10.0.0.5:4444") + self.assertRejected("-progress http:attacker.example.com:80/p") + + # --- file-write class -------------------------------------------------- + + def test_tee_write_rejected(self): + self.assertRejected("-c:v libx264 -map 0 -f tee [f=mpegts]/tmp/owned.ts") + self.assertRejected("-f tee [f=mpegts]/etc/frigate/x.ts") + self.assertRejected("tee:/tmp/x") + + def test_bare_output_token_rejected(self): + self.assertRejected("evil.mp4") + self.assertRejected("-c copy evil.mp4") + self.assertRejected("x/../escaped.mkv") + + def test_file_producing_muxers_rejected(self): + self.assertRejected("-f hls -hls_segment_filename pwn%03d.ts out.m3u8") + self.assertRejected("-f md5 victim.txt") + self.assertRejected("-f segment seg%03d.ts") + + def test_write_flags_rejected(self): + self.assertRejected("-progress evil.log") + self.assertRejected("-stats_enc_pre evil.csv") + self.assertRejected("-report") + + # --- resource exhaustion / misc --------------------------------------- + + def test_dos_input_flags_rejected(self): + self.assertRejected("-stream_loop -1") + self.assertRejected("-readrate 0.001") + + def test_disallowed_flags_rejected(self): + self.assertRejected("-map 0") + self.assertRejected("-i /etc/passwd") + self.assertRejected("-attach evil.bin") + self.assertRejected("-dump_attachment evil.bin") + self.assertRejected("/etc/passwd") + self.assertRejected("-metadata comment=x") + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_export_progress.py b/frigate/test/test_export_progress.py new file mode 100644 index 0000000..81dcdb9 --- /dev/null +++ b/frigate/test/test_export_progress.py @@ -0,0 +1,553 @@ +"""Tests for export progress tracking, broadcast, and FFmpeg parsing.""" + +import io +import os +import shutil +import tempfile +import unittest +from unittest.mock import MagicMock, patch + +from frigate.jobs.export import ( + PROGRESS_BROADCAST_MIN_INTERVAL, + ExportJob, + ExportJobManager, +) +from frigate.record.export import PlaybackSourceEnum, RecordingExporter +from frigate.types import JobStatusTypesEnum +from frigate.util.ffmpeg import inject_progress_flags + + +def _make_exporter( + end_minus_start: int = 100, + ffmpeg_input_args=None, + ffmpeg_output_args=None, + on_progress=None, +) -> RecordingExporter: + """Build a RecordingExporter without invoking its real __init__ side + effects (which create directories and require a full FrigateConfig).""" + exporter = RecordingExporter.__new__(RecordingExporter) + exporter.config = MagicMock() + exporter.export_id = "test_export" + exporter.camera = "front" + exporter.user_provided_name = None + exporter.user_provided_image = None + exporter.start_time = 1_000 + exporter.end_time = 1_000 + end_minus_start + exporter.playback_source = PlaybackSourceEnum.recordings + exporter.export_case_id = None + exporter.ffmpeg_input_args = ffmpeg_input_args + exporter.ffmpeg_output_args = ffmpeg_output_args + exporter.cpu_fallback = False + exporter.on_progress = on_progress + return exporter + + +class TestExportJobToDict(unittest.TestCase): + def test_to_dict_includes_progress_fields(self) -> None: + job = ExportJob(camera="front", request_start_time=0, request_end_time=10) + result = job.to_dict() + + assert "current_step" in result + assert "progress_percent" in result + assert result["current_step"] == "queued" + assert result["progress_percent"] == 0.0 + + def test_to_dict_reflects_updated_progress(self) -> None: + job = ExportJob(camera="front", request_start_time=0, request_end_time=10) + job.current_step = "encoding" + job.progress_percent = 42.5 + + result = job.to_dict() + + assert result["current_step"] == "encoding" + assert result["progress_percent"] == 42.5 + + +class TestExpectedOutputDuration(unittest.TestCase): + def test_normal_export_uses_input_duration(self) -> None: + exporter = _make_exporter(end_minus_start=600) + assert exporter._expected_output_duration_seconds() == 600.0 + + def test_timelapse_uses_setpts_factor(self) -> None: + exporter = _make_exporter( + end_minus_start=1000, + ffmpeg_input_args="-y", + ffmpeg_output_args="-vf setpts=0.04*PTS -r 30", + ) + # 1000s input * 0.04 = 40s of output + assert exporter._expected_output_duration_seconds() == 40.0 + + def test_unknown_factor_falls_back_to_input_duration(self) -> None: + exporter = _make_exporter( + end_minus_start=300, + ffmpeg_input_args="-y", + ffmpeg_output_args="-c:v libx264 -preset veryfast", + ) + assert exporter._expected_output_duration_seconds() == 300.0 + + def test_zero_factor_falls_back_to_input_duration(self) -> None: + exporter = _make_exporter( + end_minus_start=300, + ffmpeg_input_args="-y", + ffmpeg_output_args="-vf setpts=0*PTS", + ) + assert exporter._expected_output_duration_seconds() == 300.0 + + def test_uses_actual_recorded_seconds_when_available(self) -> None: + """If the DB shows only 120s of saved recordings inside a 1h + requested range, progress should be computed against 120s.""" + exporter = _make_exporter(end_minus_start=3600) + exporter._sum_source_duration_seconds = lambda: 120.0 # type: ignore[method-assign] + assert exporter._expected_output_duration_seconds() == 120.0 + + def test_actual_recorded_seconds_scaled_by_setpts(self) -> None: + """Recorded duration must still be scaled by the timelapse factor.""" + exporter = _make_exporter( + end_minus_start=3600, + ffmpeg_input_args="-y", + ffmpeg_output_args="-vf setpts=0.04*PTS -r 30", + ) + exporter._sum_source_duration_seconds = lambda: 600.0 # type: ignore[method-assign] + # 600s * 0.04 = 24s of output + assert exporter._expected_output_duration_seconds() == 24.0 + + def test_db_failure_falls_back_to_requested_range(self) -> None: + exporter = _make_exporter(end_minus_start=300) + exporter._sum_source_duration_seconds = lambda: None # type: ignore[method-assign] + assert exporter._expected_output_duration_seconds() == 300.0 + + +class TestProgressFlagInjection(unittest.TestCase): + def test_inserts_before_output_path(self) -> None: + cmd = ["ffmpeg", "-i", "input.m3u8", "-c", "copy", "/tmp/output.mp4"] + + result = inject_progress_flags(cmd) + + assert result == [ + "ffmpeg", + "-i", + "input.m3u8", + "-c", + "copy", + "-progress", + "pipe:2", + "-nostats", + "/tmp/output.mp4", + ] + + def test_handles_empty_cmd(self) -> None: + assert inject_progress_flags([]) == [] + + +class TestFfmpegProgressParsing(unittest.TestCase): + """Verify percentage calculation from FFmpeg ``-progress`` output.""" + + def _run_with_stderr( + self, + stderr_text: str, + expected_duration_seconds: int = 90, + ) -> list[tuple[str, float]]: + """Helper: run _run_ffmpeg_with_progress against a mocked Popen + whose stderr emits the supplied text. Returns the list of + (step, percent) tuples that the on_progress callback received.""" + captured: list[tuple[str, float]] = [] + + def on_progress(step: str, percent: float) -> None: + captured.append((step, percent)) + + exporter = _make_exporter( + end_minus_start=expected_duration_seconds, + on_progress=on_progress, + ) + + fake_proc = MagicMock() + fake_proc.stdin = io.StringIO() + fake_proc.stderr = io.StringIO(stderr_text) + fake_proc.returncode = 0 + fake_proc.wait = MagicMock(return_value=0) + + with patch("frigate.util.ffmpeg.sp.Popen", return_value=fake_proc): + returncode, _stderr = exporter._run_ffmpeg_with_progress( + ["ffmpeg", "-i", "x.m3u8", "/tmp/out.mp4"], "playlist", step="encoding" + ) + + assert returncode == 0 + return captured + + def test_parses_out_time_us_into_percent(self) -> None: + # 90s duration; 45s out_time => 50% + stderr = "out_time_us=45000000\nprogress=continue\n" + captured = self._run_with_stderr(stderr, expected_duration_seconds=90) + + # The first call is the synchronous 0.0 emit before Popen runs. + assert captured[0] == ("encoding", 0.0) + assert any(percent == 50.0 for step, percent in captured if step == "encoding") + + def test_progress_end_emits_100_percent(self) -> None: + stderr = "out_time_us=10000000\nprogress=end\n" + captured = self._run_with_stderr(stderr, expected_duration_seconds=90) + + assert captured[-1] == ("encoding", 100.0) + + def test_clamps_overshoot_at_100(self) -> None: + # 150s of output reported against 90s expected duration. + stderr = "out_time_us=150000000\nprogress=continue\n" + captured = self._run_with_stderr(stderr, expected_duration_seconds=90) + + encoding_values = [p for s, p in captured if s == "encoding" and p > 0] + assert all(p <= 100.0 for p in encoding_values) + assert encoding_values[-1] == 100.0 + + def test_ignores_garbage_lines(self) -> None: + stderr = ( + "frame= 120 fps= 30 q=23.0 size= 512kB\n" + "out_time_us=not-a-number\n" + "out_time_us=30000000\n" + "progress=continue\n" + ) + captured = self._run_with_stderr(stderr, expected_duration_seconds=90) + + # We expect 0.0 (from initial emit) plus the 30s/90s = 33.33...% step + encoding_percents = sorted({round(p, 2) for s, p in captured}) + assert 0.0 in encoding_percents + assert any(abs(p - (30 / 90 * 100)) < 0.01 for p in encoding_percents) + + +class TestBroadcastAggregation(unittest.TestCase): + """Verify ExportJobManager broadcast payload shape and throttling.""" + + def _make_manager(self) -> tuple[ExportJobManager, MagicMock]: + """Build a manager with an injected mock publisher. Returns + ``(manager, publisher)`` so tests can assert on broadcast payloads + without touching ZMQ at all.""" + config = MagicMock() + publisher = MagicMock() + manager = ExportJobManager( + config, max_concurrent=2, max_queued=10, publisher=publisher + ) + return manager, publisher + + @staticmethod + def _last_payload(publisher: MagicMock) -> dict: + return publisher.publish.call_args.args[0] + + def test_empty_jobs_broadcasts_empty_list(self) -> None: + manager, publisher = self._make_manager() + manager._broadcast_all_jobs(force=True) + + publisher.publish.assert_called_once() + payload = self._last_payload(publisher) + assert payload["job_type"] == "export" + assert payload["status"] == "queued" + assert payload["results"]["jobs"] == [] + + def test_single_running_job_payload(self) -> None: + manager, publisher = self._make_manager() + job = ExportJob(camera="front", request_start_time=0, request_end_time=10) + job.status = JobStatusTypesEnum.running + job.current_step = "encoding" + job.progress_percent = 75.0 + manager.jobs[job.id] = job + + manager._broadcast_all_jobs(force=True) + + payload = self._last_payload(publisher) + assert payload["status"] == "running" + assert len(payload["results"]["jobs"]) == 1 + broadcast_job = payload["results"]["jobs"][0] + assert broadcast_job["current_step"] == "encoding" + assert broadcast_job["progress_percent"] == 75.0 + + def test_multiple_jobs_broadcast(self) -> None: + manager, publisher = self._make_manager() + for i, status in enumerate( + (JobStatusTypesEnum.queued, JobStatusTypesEnum.running) + ): + job = ExportJob( + id=f"job_{i}", + camera="front", + request_start_time=0, + request_end_time=10, + ) + job.status = status + manager.jobs[job.id] = job + + manager._broadcast_all_jobs(force=True) + + payload = self._last_payload(publisher) + assert payload["status"] == "running" + assert len(payload["results"]["jobs"]) == 2 + + def test_completed_jobs_are_excluded(self) -> None: + manager, publisher = self._make_manager() + active = ExportJob(id="active", camera="front") + active.status = JobStatusTypesEnum.running + finished = ExportJob(id="done", camera="front") + finished.status = JobStatusTypesEnum.success + manager.jobs[active.id] = active + manager.jobs[finished.id] = finished + + manager._broadcast_all_jobs(force=True) + + payload = self._last_payload(publisher) + ids = [j["id"] for j in payload["results"]["jobs"]] + assert ids == ["active"] + + def test_throttle_skips_rapid_unforced_broadcasts(self) -> None: + manager, publisher = self._make_manager() + job = ExportJob(camera="front") + job.status = JobStatusTypesEnum.running + manager.jobs[job.id] = job + + manager._broadcast_all_jobs(force=True) + # Immediately following non-forced broadcasts should be skipped. + for _ in range(5): + manager._broadcast_all_jobs(force=False) + + assert publisher.publish.call_count == 1 + + def test_throttle_allows_broadcast_after_interval(self) -> None: + manager, publisher = self._make_manager() + job = ExportJob(camera="front") + job.status = JobStatusTypesEnum.running + manager.jobs[job.id] = job + + with patch("frigate.jobs.export.time.monotonic") as mock_mono: + mock_mono.return_value = 100.0 + manager._broadcast_all_jobs(force=True) + + mock_mono.return_value = 100.0 + PROGRESS_BROADCAST_MIN_INTERVAL + 0.01 + manager._broadcast_all_jobs(force=False) + + assert publisher.publish.call_count == 2 + + def test_force_bypasses_throttle(self) -> None: + manager, publisher = self._make_manager() + job = ExportJob(camera="front") + job.status = JobStatusTypesEnum.running + manager.jobs[job.id] = job + + manager._broadcast_all_jobs(force=True) + manager._broadcast_all_jobs(force=True) + + assert publisher.publish.call_count == 2 + + def test_publisher_exceptions_do_not_propagate(self) -> None: + """A failing publisher must not break the manager: broadcasts are + best-effort since the dispatcher may not be available (tests, + startup races).""" + manager, publisher = self._make_manager() + publisher.publish.side_effect = RuntimeError("comms down") + + job = ExportJob(camera="front") + job.status = JobStatusTypesEnum.running + manager.jobs[job.id] = job + + # Swallow our own RuntimeError if the manager doesn't; the real + # JobStatePublisher handles its own exceptions internally, so the + # manager can stay naive. But if something bubbles up it should + # not escape _broadcast_all_jobs — enforce that contract here. + try: + manager._broadcast_all_jobs(force=True) + except RuntimeError: + self.fail("_broadcast_all_jobs must tolerate publisher failures") + + def test_progress_callback_updates_job_and_broadcasts(self) -> None: + manager, _publisher = self._make_manager() + job = ExportJob(camera="front") + job.status = JobStatusTypesEnum.running + manager.jobs[job.id] = job + + callback = manager._make_progress_callback(job) + callback("encoding", 33.0) + + assert job.current_step == "encoding" + assert job.progress_percent == 33.0 + + +class TestGetDatetimeFromTimestamp(unittest.TestCase): + """Auto-generated export name should honor config.ui.timezone, not + fall back to the container's UTC clock when a timezone is configured. + """ + + def test_uses_configured_ui_timezone(self) -> None: + exporter = _make_exporter() + exporter.config.ui.timezone = "America/New_York" + # 2025-01-15 12:00:00 UTC is 07:00:00 EST + assert exporter.get_datetime_from_timestamp(1736942400) == "2025-01-15 07:00:00" + + def test_falls_back_to_local_when_timezone_unset(self) -> None: + exporter = _make_exporter() + exporter.config.ui.timezone = None + # No assertion on the exact wall-clock value — just confirm no + # exception and that pytz isn't required when the field is unset. + assert isinstance(exporter.get_datetime_from_timestamp(1736942400), str) + + def test_invalid_timezone_falls_back_to_local(self) -> None: + exporter = _make_exporter() + exporter.config.ui.timezone = "Not/A_Real_Zone" + assert isinstance(exporter.get_datetime_from_timestamp(1736942400), str) + + +class TestSaveThumbnailFromPreviewFrames(unittest.TestCase): + """Short exports in the current hour can fall between preview frame + writes (1-2 fps during activity, every 30s otherwise). When no frame + falls inside the export window, save_thumbnail should fall back to + the most recent prior frame instead of returning no thumbnail.""" + + def setUp(self) -> None: + self.tmp_root = tempfile.mkdtemp(prefix="frigate_thumb_test_") + self.preview_dir = os.path.join(self.tmp_root, "cache", "preview_frames") + self.export_clips = os.path.join(self.tmp_root, "clips", "export") + os.makedirs(self.preview_dir, exist_ok=True) + os.makedirs(self.export_clips, exist_ok=True) + + def tearDown(self) -> None: + shutil.rmtree(self.tmp_root, ignore_errors=True) + + def _write_frame(self, camera: str, frame_time: float) -> str: + path = os.path.join(self.preview_dir, f"preview_{camera}-{frame_time}.webp") + with open(path, "wb") as f: + f.write(b"fake-webp-bytes") + return path + + def _make_short_current_hour_exporter(self) -> RecordingExporter: + # Use a "now-ish" timestamp so save_thumbnail's start-of-hour + # comparison takes the current-hour branch (preview frames). + import datetime + + now = datetime.datetime.now(datetime.UTC).timestamp() + exporter = _make_exporter() + exporter.export_id = "thumb_short" + exporter.start_time = now + exporter.end_time = now + 3 + return exporter + + def test_short_export_falls_back_to_prior_preview_frame(self) -> None: + exporter = self._make_short_current_hour_exporter() + # Most recent preview frame is 10s before the export window + prior = self._write_frame(exporter.camera, exporter.start_time - 10.0) + thumb_target = os.path.join(self.export_clips, f"{exporter.export_id}.webp") + + with ( + patch( + "frigate.record.export.CACHE_DIR", os.path.join(self.tmp_root, "cache") + ), + patch( + "frigate.record.export.CLIPS_DIR", os.path.join(self.tmp_root, "clips") + ), + ): + result = exporter.save_thumbnail(exporter.export_id) + + assert result == thumb_target + assert os.path.isfile(thumb_target) + with open(thumb_target, "rb") as f, open(prior, "rb") as src: + assert f.read() == src.read() + + def test_returns_empty_when_no_preview_frames_exist(self) -> None: + exporter = self._make_short_current_hour_exporter() + + with ( + patch( + "frigate.record.export.CACHE_DIR", os.path.join(self.tmp_root, "cache") + ), + patch( + "frigate.record.export.CLIPS_DIR", os.path.join(self.tmp_root, "clips") + ), + ): + result = exporter.save_thumbnail(exporter.export_id) + + assert result == "" + + def test_prefers_in_window_frame_over_prior_frame(self) -> None: + exporter = self._make_short_current_hour_exporter() + self._write_frame(exporter.camera, exporter.start_time - 10.0) + in_window = self._write_frame(exporter.camera, exporter.start_time + 1.0) + thumb_target = os.path.join(self.export_clips, f"{exporter.export_id}.webp") + + with ( + patch( + "frigate.record.export.CACHE_DIR", os.path.join(self.tmp_root, "cache") + ), + patch( + "frigate.record.export.CLIPS_DIR", os.path.join(self.tmp_root, "clips") + ), + ): + result = exporter.save_thumbnail(exporter.export_id) + + assert result == thumb_target + with open(thumb_target, "rb") as f, open(in_window, "rb") as src: + assert f.read() == src.read() + + +class TestSchedulesCleanup(unittest.TestCase): + def test_schedule_job_cleanup_removes_after_delay(self) -> None: + config = MagicMock() + manager = ExportJobManager(config, max_concurrent=1, max_queued=1) + job = ExportJob(id="cleanup_me", camera="front") + manager.jobs[job.id] = job + + with patch("frigate.jobs.export.threading.Timer") as mock_timer: + manager._schedule_job_cleanup(job.id) + mock_timer.assert_called_once() + delay, fn = mock_timer.call_args.args + assert delay > 0 + + # Invoke the callback directly to confirm it removes the job. + fn() + assert job.id not in manager.jobs + + +class TestChapterMetadataInProgressReview(unittest.TestCase): + """Regression: in-progress review segments have end_time=NULL until the + activity closes. The chapter builder must clamp the chapter end to the + last recorded second instead of crashing on float(None).""" + + def _fake_select_returning(self, rows: list) -> MagicMock: + mock_query = MagicMock() + mock_query.where.return_value = mock_query + mock_query.order_by.return_value = mock_query + mock_query.iterator.return_value = iter(rows) + return mock_query + + def test_in_progress_review_does_not_crash_and_clamps_to_last_recording( + self, + ) -> None: + exporter = _make_exporter(end_minus_start=200) + # Recordings cover [1000, 1150]; export window is [1000, 1200] so + # the last recorded second is 1150 (a 50s gap at the tail). + recordings = [ + MagicMock(start_time=1000.0, end_time=1150.0), + ] + in_progress = MagicMock( + start_time=1100.0, + end_time=None, + severity="alert", + data={"objects": ["person"]}, + ) + + with tempfile.TemporaryDirectory() as tmpdir: + chapter_path = os.path.join(tmpdir, "chapters.txt") + exporter._chapter_metadata_path = lambda: chapter_path # type: ignore[method-assign] + + with patch( + "frigate.record.export.ReviewSegment.select", + return_value=self._fake_select_returning([in_progress]), + ): + result = exporter._build_chapter_metadata_file(recordings) + + assert result == chapter_path + with open(chapter_path) as f: + content = f.read() + + # Output time is windows[-1][1] - windows[-1][0] = 150s. + # Review starts at wall=1100, output offset = 100s -> 100000ms. + # Clamped end = last_recorded_end (1150) -> output offset = 150s -> 150000ms. + assert "[CHAPTER]" in content + assert "START=100000" in content + assert "END=150000" in content + assert "title=Alert: person" in content + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_ffmpeg_presets.py b/frigate/test/test_ffmpeg_presets.py new file mode 100644 index 0000000..86fdd5f --- /dev/null +++ b/frigate/test/test_ffmpeg_presets.py @@ -0,0 +1,146 @@ +import unittest + +from frigate.config import FrigateConfig +from frigate.config.camera.ffmpeg import FFMPEG_INPUT_ARGS_DEFAULT +from frigate.ffmpeg_presets import parse_preset_input + + +class TestFfmpegPresets(unittest.TestCase): + def setUp(self): + self.default_ffmpeg = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ], + "output_args": { + "detect": "-f rawvideo -pix_fmt yuv420p", + "record": "-f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an", + }, + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "record": { + "enabled": True, + }, + "name": "back", + } + }, + } + + def test_default_ffmpeg(self): + FrigateConfig(**self.default_ffmpeg) + + def test_ffmpeg_hwaccel_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = ( + "preset-rpi-64-h264" + ) + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "preset-rpi-64-h264" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert "-c:v:1 h264_v4l2m2m" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_hwaccel_not_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = ( + "-other-hwaccel args" + ) + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "-other-hwaccel args" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_hwaccel_scale_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = ( + "preset-nvidia-h264" + ) + self.default_ffmpeg["cameras"]["back"]["detect"] = { + "height": 1920, + "width": 2560, + "fps": 10, + } + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "preset-nvidia-h264" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert "fps=10,scale_cuda=w=2560:h=1920,hwdownload,format=nv12" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_default_ffmpeg_input_arg_preset(self): + frigate_config = FrigateConfig(**self.default_ffmpeg) + + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = ( + "preset-rtsp-generic" + ) + frigate_preset_config = FrigateConfig(**self.default_ffmpeg) + assert ( + # Ignore global and user_agent args in comparison + frigate_preset_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + == frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + ) + + def test_ffmpeg_input_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = ( + "preset-rtmp-generic" + ) + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "preset-rtmp-generic" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert (" ".join(parse_preset_input("preset-rtmp-generic", 5))) in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_input_args_as_string(self): + # Strip user_agent args here to avoid handling quoting issues + defaultArgsList = parse_preset_input(FFMPEG_INPUT_ARGS_DEFAULT, 5)[2::] + argsString = " ".join(defaultArgsList) + ' -some "arg with space"' + argsList = defaultArgsList + ["-some", "arg with space"] + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = argsString + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert set(argsList).issubset( + frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + ) + + def test_ffmpeg_input_not_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = "-some inputs" + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "-some inputs" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_output_record_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"]["record"] = ( + "preset-record-generic-audio-aac" + ) + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "preset-record-generic-audio-aac" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert "-c:v copy -c:a aac" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_output_record_not_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"]["record"] = ( + "-some output -segment_time 10" + ) + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "-some output" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/frigate/test/test_ffmpeg_progress.py b/frigate/test/test_ffmpeg_progress.py new file mode 100644 index 0000000..5210511 --- /dev/null +++ b/frigate/test/test_ffmpeg_progress.py @@ -0,0 +1,111 @@ +"""Tests for the shared ffmpeg progress helper.""" + +import unittest +from unittest.mock import MagicMock, patch + +from frigate.util.ffmpeg import inject_progress_flags, run_ffmpeg_with_progress + + +class TestInjectProgressFlags(unittest.TestCase): + def test_inserts_flags_before_output_path(self): + cmd = ["ffmpeg", "-i", "in.mp4", "-c", "copy", "out.mp4"] + result = inject_progress_flags(cmd) + self.assertEqual( + result, + [ + "ffmpeg", + "-i", + "in.mp4", + "-c", + "copy", + "-progress", + "pipe:2", + "-nostats", + "out.mp4", + ], + ) + + def test_empty_cmd_returns_empty(self): + self.assertEqual(inject_progress_flags([]), []) + + +class TestRunFfmpegWithProgress(unittest.TestCase): + def _make_fake_proc(self, stderr_lines, returncode=0): + proc = MagicMock() + proc.stderr = iter(stderr_lines) + proc.stdin = MagicMock() + proc.returncode = returncode + proc.wait = MagicMock() + return proc + + def test_emits_percent_from_out_time_us_lines(self): + captured: list[float] = [] + + def on_progress(percent: float) -> None: + captured.append(percent) + + stderr_lines = [ + "out_time_us=1000000\n", + "out_time_us=5000000\n", + "progress=end\n", + ] + proc = self._make_fake_proc(stderr_lines) + proc.stderr = MagicMock() + proc.stderr.__iter__ = lambda self: iter(stderr_lines) + proc.stderr.read = MagicMock(return_value="") + + with patch("subprocess.Popen", return_value=proc): + returncode, _stderr = run_ffmpeg_with_progress( + ["ffmpeg", "-i", "in", "out"], + expected_duration_seconds=10.0, + on_progress=on_progress, + use_low_priority=False, + ) + + self.assertEqual(returncode, 0) + self.assertEqual(len(captured), 4) # initial 0.0 + two parsed + final 100.0 + self.assertAlmostEqual(captured[0], 0.0) + self.assertAlmostEqual(captured[1], 10.0) + self.assertAlmostEqual(captured[2], 50.0) + self.assertAlmostEqual(captured[3], 100.0) + + def test_passes_started_process_to_callback(self): + proc = self._make_fake_proc([]) + proc.stderr = MagicMock() + proc.stderr.__iter__ = lambda self: iter([]) + proc.stderr.read = MagicMock(return_value="") + + seen: list = [] + + with patch("subprocess.Popen", return_value=proc): + run_ffmpeg_with_progress( + ["ffmpeg", "out"], + expected_duration_seconds=1.0, + process_started=lambda p: seen.append(p), + use_low_priority=False, + ) + + self.assertEqual(seen, [proc]) + + def test_clamps_percent_to_0_100(self): + captured: list[float] = [] + + def on_progress(percent: float) -> None: + captured.append(percent) + + stderr_lines = ["out_time_us=999999999999\n"] + proc = self._make_fake_proc(stderr_lines) + proc.stderr = MagicMock() + proc.stderr.__iter__ = lambda self: iter(stderr_lines) + proc.stderr.read = MagicMock(return_value="") + + with patch("subprocess.Popen", return_value=proc): + run_ffmpeg_with_progress( + ["ffmpeg", "out"], + expected_duration_seconds=10.0, + on_progress=on_progress, + use_low_priority=False, + ) + + # initial 0.0 then a clamped reading + self.assertEqual(captured[-1], 100.0) diff --git a/frigate/test/test_file.py b/frigate/test/test_file.py new file mode 100644 index 0000000..6bbe2b6 --- /dev/null +++ b/frigate/test/test_file.py @@ -0,0 +1,72 @@ +import os +import tempfile +from types import SimpleNamespace +from unittest import TestCase +from unittest.mock import patch + +import cv2 +import numpy as np + +from frigate.util import file as file_util + + +class TestFileUtils(TestCase): + def _write_clean_snapshot( + self, clips_dir: str, event_id: str, image: np.ndarray + ) -> None: + assert cv2.imwrite( + os.path.join(clips_dir, f"front_door-{event_id}-clean.webp"), + image, + ) + + def test_get_event_snapshot_bytes_reads_clean_webp(self): + event_id = "clean-webp" + image = np.zeros((100, 200, 3), np.uint8) + event = SimpleNamespace( + id=event_id, + camera="front_door", + label="Mock", + top_score=100, + score=0, + start_time=0, + data={ + "box": [0.25, 0.25, 0.25, 0.5], + "score": 0.85, + "attributes": [], + }, + ) + + with ( + tempfile.TemporaryDirectory() as clips_dir, + patch.object(file_util, "CLIPS_DIR", clips_dir), + ): + self._write_clean_snapshot(clips_dir, event_id, image) + + snapshot_image, is_clean = file_util.load_event_snapshot_image( + event, clean_only=True + ) + + assert is_clean + assert snapshot_image is not None + assert snapshot_image.shape[:2] == image.shape[:2] + + rendered_bytes, _ = file_util.get_event_snapshot_bytes( + event, + ext="jpg", + timestamp=False, + bounding_box=True, + crop=False, + height=40, + quality=None, + timestamp_style=None, + colormap={}, + ) + assert rendered_bytes is not None + + rendered_image = cv2.imdecode( + np.frombuffer(rendered_bytes, dtype=np.uint8), + cv2.IMREAD_COLOR, + ) + assert rendered_image is not None + assert rendered_image.shape[0] == 40 + assert rendered_image.max() > 0 diff --git a/frigate/test/test_go2rtc_stream_auth.py b/frigate/test/test_go2rtc_stream_auth.py new file mode 100644 index 0000000..b525c94 --- /dev/null +++ b/frigate/test/test_go2rtc_stream_auth.py @@ -0,0 +1,175 @@ +"""Unit tests for `deny_response_for_go2rtc_stream`. + +Covers the camera-level authorization enforced in the `/auth` subrequest for +the nginx-proxied go2rtc live-stream paths (MSE/WebRTC WebSockets and the +WebRTC signaling endpoint). These paths name the stream via the `src` query +param, which the static-media auth in `media_auth` does not inspect. +""" + +import types +import unittest + +from frigate.api.auth import deny_response_for_go2rtc_stream +from frigate.config import FrigateConfig + +_CONFIG = { + "mqtt": {"host": "mqtt"}, + "auth": { + "roles": { + "limited_user": ["front_door"], + "dual_user": ["front_door", "back_door"], + } + }, + "cameras": { + "front_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + # go2rtc stream name differs from the camera name (substream) + "live": {"streams": {"Main Stream": "front_door_sub"}}, + }, + "back_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + "garage": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + }, +} + + +def _request(config: FrigateConfig) -> types.SimpleNamespace: + return types.SimpleNamespace(app=types.SimpleNamespace(frigate_config=config)) + + +class TestDenyResponseForGo2rtcStream(unittest.TestCase): + def setUp(self) -> None: + self.config = FrigateConfig(**_CONFIG) + self.request = _request(self.config) + + def _deny(self, url: str, role: str): + return deny_response_for_go2rtc_stream(url, role, self.request) + + # --- non-stream paths pass through --- + + def test_non_stream_path_passes_through(self): + self.assertIsNone( + self._deny("http://host/clips/back_door-1.jpg", "limited_user") + ) + + def test_empty_url_passes_through(self): + self.assertIsNone(self._deny("", "limited_user")) + + def test_jsmpeg_path_not_handled_here(self): + # jsmpeg is authorized per-frame in the output pipeline, not here + self.assertIsNone( + self._deny("http://host/live/jsmpeg/back_door", "limited_user") + ) + + # --- restricted role: allowed vs forbidden cameras --- + + def test_mse_allowed_camera(self): + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=front_door", "limited_user") + ) + + def test_mse_forbidden_camera_denied(self): + self.assertEqual( + self._deny("http://host/live/mse/api/ws?src=back_door", "limited_user"), + 403, + ) + + def test_webrtc_ws_forbidden_camera_denied(self): + self.assertEqual( + self._deny("http://host/live/webrtc/api/ws?src=back_door", "limited_user"), + 403, + ) + + def test_webrtc_signaling_forbidden_camera_denied(self): + self.assertEqual( + self._deny("http://host/api/go2rtc/webrtc?src=back_door", "limited_user"), + 403, + ) + + def test_unknown_camera_denied(self): + self.assertEqual( + self._deny("http://host/live/mse/api/ws?src=nonexistent", "limited_user"), + 403, + ) + + def test_missing_src_denied(self): + self.assertEqual(self._deny("http://host/live/mse/api/ws", "limited_user"), 403) + + # --- multi-camera role: each assigned camera allowed, others denied --- + + def test_multi_camera_role_allows_first_assigned(self): + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=front_door", "dual_user") + ) + + def test_multi_camera_role_allows_second_assigned(self): + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=back_door", "dual_user") + ) + + def test_multi_camera_role_denies_unassigned(self): + # garage is configured but not in dual_user's allow-list + self.assertEqual( + self._deny("http://host/live/mse/api/ws?src=garage", "dual_user"), + 403, + ) + + # --- substream names resolve to their owning camera --- + + def test_allowed_substream_resolves_to_owning_camera(self): + # front_door_sub is owned by front_door, which limited_user may access + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=front_door_sub", "limited_user") + ) + + # --- multiple src values: deny if any is forbidden --- + + def test_multiple_src_one_forbidden_denied(self): + self.assertEqual( + self._deny( + "http://host/live/mse/api/ws?src=front_door&src=back_door", + "limited_user", + ), + 403, + ) + + def test_multiple_src_all_allowed(self): + self.assertIsNone( + self._deny( + "http://host/live/mse/api/ws?src=front_door&src=front_door_sub", + "limited_user", + ) + ) + + # --- privileged roles bypass the check --- + + def test_admin_bypasses(self): + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=back_door", "admin") + ) + + def test_builtin_viewer_role_bypasses(self): + # the built-in viewer role is not in the config allow-list map, so it + # is treated as full access + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=back_door", "viewer") + ) + + def test_missing_role_bypasses(self): + self.assertIsNone(self._deny("http://host/live/mse/api/ws?src=back_door", None)) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_gpu_stats.py b/frigate/test/test_gpu_stats.py new file mode 100644 index 0000000..f698691 --- /dev/null +++ b/frigate/test/test_gpu_stats.py @@ -0,0 +1,98 @@ +import unittest +from unittest.mock import MagicMock, patch + +from frigate.util.services import get_amd_gpu_stats, get_intel_gpu_stats + + +class TestGpuStats(unittest.TestCase): + def setUp(self): + self.amd_results = "Unknown Radeon card. <= R500 won't work, new cards might.\nDumping to -, line limit 1.\n1664070990.607556: bus 10, gpu 4.17%, ee 0.00%, vgt 0.00%, ta 0.00%, tc 0.00%, sx 0.00%, sh 0.00%, spi 0.83%, smx 0.00%, cr 0.00%, sc 0.00%, pa 0.00%, db 0.00%, cb 0.00%, vram 60.37% 294.04mb, gtt 0.33% 52.21mb, mclk 100.00% 1.800ghz, sclk 26.65% 0.533ghz\n" + + @patch("subprocess.run") + def test_amd_gpu_stats(self, sp): + process = MagicMock() + process.returncode = 0 + process.stdout = self.amd_results + sp.return_value = process + amd_stats = get_amd_gpu_stats() + assert amd_stats == {"gpu": "4.17%", "mem": "60.37%"} + + @patch("frigate.stats.intel_gpu_info.intel_gpu_name_resolver.get_names") + @patch("frigate.util.services.time.sleep") + @patch("frigate.util.services.time.monotonic") + @patch("frigate.util.services._read_intel_drm_fdinfo") + def test_intel_gpu_stats_fdinfo(self, read_fdinfo, monotonic, sleep, get_names): + # 1 second of wall clock between snapshots + monotonic.side_effect = [0.0, 1.0] + get_names.return_value = {"0000:00:02.0": "Intel Graphics"} + + # Two i915 clients on the same iGPU. Engine values are cumulative ns. + # Deltas over the 1s window: + # client A (pid 100): render +200_000_000 (20%), video +500_000_000 (50%), + # video-enhance +100_000_000 (10%) + # client B (pid 200): compute +100_000_000 (10%) + # Engine totals → render 20, video 50, video-enhance 10, compute 10 + # → compute = render + compute = 30 + # → dec = video + video-enhance = 60 + # → gpu = compute + dec = 90 + snapshot_a = { + ("0000:00:02.0", "1", "100"): { + "driver": "i915", + "pid": "100", + "engines": { + "render": (1_000_000_000, 0), + "video": (5_000_000_000, 0), + "video-enhance": (200_000_000, 0), + "compute": (0, 0), + }, + }, + ("0000:00:02.0", "2", "200"): { + "driver": "i915", + "pid": "200", + "engines": { + "render": (0, 0), + "compute": (2_000_000_000, 0), + }, + }, + } + snapshot_b = { + ("0000:00:02.0", "1", "100"): { + "driver": "i915", + "pid": "100", + "engines": { + "render": (1_200_000_000, 0), + "video": (5_500_000_000, 0), + "video-enhance": (300_000_000, 0), + "compute": (0, 0), + }, + }, + ("0000:00:02.0", "2", "200"): { + "driver": "i915", + "pid": "200", + "engines": { + "render": (0, 0), + "compute": (2_100_000_000, 0), + }, + }, + } + read_fdinfo.side_effect = [snapshot_a, snapshot_b] + + intel_stats = get_intel_gpu_stats(None) + + sleep.assert_called_once() + assert intel_stats == { + "0000:00:02.0": { + "name": "Intel Graphics", + "vendor": "intel", + "gpu": "90.0%", + "mem": "-%", + "compute": "30.0%", + "dec": "60.0%", + "clients": {"100": "80.0%", "200": "10.0%"}, + }, + } + + @patch("frigate.util.services._read_intel_drm_fdinfo") + def test_intel_gpu_stats_no_clients(self, read_fdinfo): + read_fdinfo.return_value = {} + assert get_intel_gpu_stats(None) is None diff --git a/frigate/test/test_keyframe_analysis.py b/frigate/test/test_keyframe_analysis.py new file mode 100644 index 0000000..14dad58 --- /dev/null +++ b/frigate/test/test_keyframe_analysis.py @@ -0,0 +1,110 @@ +"""Tests for keyframe-spacing analysis used to detect smart/+ codecs.""" + +import unittest +from unittest.mock import AsyncMock, MagicMock, patch + +from frigate.util.services import ( + analyze_record_keyframes, + classify_keyframe_gaps, + parse_keyframe_packets, +) + + +class TestClassifyKeyframeGaps(unittest.TestCase): + def test_ok_when_gaps_small(self): + # keyframes every ~1s + pts = [0.0, 1.0, 2.0, 3.0, 4.0] + result = classify_keyframe_gaps(pts, segment_time=10) + self.assertEqual(result["severity"], "ok") + self.assertEqual(result["max_gap"], 1.0) + self.assertEqual(result["keyframe_count"], 5) + self.assertEqual(result["thresholds"], {"warning": 4.0, "error": 10}) + + def test_warning_when_gap_exceeds_four_seconds(self): + pts = [0.0, 1.0, 6.5] # 5.5s gap + result = classify_keyframe_gaps(pts, segment_time=10) + self.assertEqual(result["severity"], "warning") + self.assertEqual(result["max_gap"], 5.5) + + def test_error_when_gap_exceeds_segment_time(self): + pts = [0.0, 12.0] # 12s gap > 10s segment + result = classify_keyframe_gaps(pts, segment_time=10) + self.assertEqual(result["severity"], "error") + + def test_error_threshold_tracks_segment_time(self): + pts = [0.0, 6.0] # 6s gap, segment_time=5 -> error + result = classify_keyframe_gaps(pts, segment_time=5) + self.assertEqual(result["severity"], "error") + + def test_unknown_with_single_keyframe(self): + result = classify_keyframe_gaps([1.0], segment_time=10) + self.assertEqual(result["severity"], "unknown") + self.assertIsNone(result["max_gap"]) + self.assertEqual(result["keyframe_count"], 1) + + def test_unknown_with_no_keyframes(self): + result = classify_keyframe_gaps([], segment_time=10) + self.assertEqual(result["severity"], "unknown") + self.assertEqual(result["keyframe_count"], 0) + + +class TestParseKeyframePackets(unittest.TestCase): + def test_extracts_keyframe_pts_and_max(self): + output = "0.000000,K__\n0.033333,___\n1.000000,K__\n1.500000,___\n" + keyframe_pts, max_pts = parse_keyframe_packets(output) + self.assertEqual(keyframe_pts, [0.0, 1.0]) + self.assertEqual(max_pts, 1.5) + + def test_skips_unparseable_and_empty_lines(self): + output = "N/A,K__\n\n2.0,K__\nbad line\n" + keyframe_pts, max_pts = parse_keyframe_packets(output) + self.assertEqual(keyframe_pts, [2.0]) + self.assertEqual(max_pts, 2.0) + + def test_empty_output(self): + keyframe_pts, max_pts = parse_keyframe_packets("") + self.assertEqual(keyframe_pts, []) + self.assertIsNone(max_pts) + + +class TestAnalyzeRecordKeyframes(unittest.IsolatedAsyncioTestCase): + async def test_merges_duration_and_classification(self): + csv = b"0.0,K__\n1.0,___\n6.0,K__\n7.0,___\n" + proc = MagicMock() + proc.communicate = AsyncMock(return_value=(csv, b"")) + ffmpeg = MagicMock() + ffmpeg.ffprobe_path = "/usr/bin/ffprobe" + + with patch( + "frigate.util.services.asyncio.create_subprocess_exec", + AsyncMock(return_value=proc), + ): + result = await analyze_record_keyframes( + ffmpeg, "rtsp://cam/stream", segment_time=10 + ) + + self.assertEqual(result["severity"], "warning") # 6s gap > 4s + self.assertEqual(result["max_gap"], 6.0) + self.assertEqual(result["duration_observed"], 7.0) + + async def test_timeout_returns_unknown(self): + proc = MagicMock() + proc.communicate = AsyncMock(side_effect=TimeoutError()) + proc.kill = MagicMock() + ffmpeg = MagicMock() + ffmpeg.ffprobe_path = "/usr/bin/ffprobe" + + with patch( + "frigate.util.services.asyncio.create_subprocess_exec", + AsyncMock(return_value=proc), + ): + result = await analyze_record_keyframes( + ffmpeg, "rtsp://cam/stream", segment_time=10 + ) + + self.assertEqual(result["severity"], "unknown") + proc.kill.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_maintainer.py b/frigate/test/test_maintainer.py new file mode 100644 index 0000000..715cd5a --- /dev/null +++ b/frigate/test/test_maintainer.py @@ -0,0 +1,160 @@ +import datetime +import sys +import unittest +from unittest.mock import MagicMock, patch + +# Mock complex imports before importing maintainer, saving originals so we can +# restore them after import and avoid polluting sys.modules for other tests. +_MOCKED_MODULES = [ + "frigate.comms.inter_process", + "frigate.comms.detections_updater", + "frigate.comms.recordings_updater", + "frigate.config.camera.updater", +] +_originals = {name: sys.modules.get(name) for name in _MOCKED_MODULES} +for name in _MOCKED_MODULES: + sys.modules[name] = MagicMock() + +# Now import the class under test +from frigate.config import FrigateConfig # noqa: E402 +from frigate.record.maintainer import RecordingMaintainer # noqa: E402 + +# Restore original modules (or remove mock if there was no original) +for name, orig in _originals.items(): + if orig is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = orig + + +class TestMaintainer(unittest.IsolatedAsyncioTestCase): + async def test_move_files_survives_bad_filename(self): + config = MagicMock(spec=FrigateConfig) + config.cameras = {} + stop_event = MagicMock() + + maintainer = RecordingMaintainer(config, stop_event) + + # We need to mock end_time_cache to avoid key errors if logic proceeds + maintainer.end_time_cache = {} + + # Mock filesystem + # One bad file, one good file + files = ["bad_filename.mp4", "camera@20210101000000+0000.mp4"] + + with patch("os.listdir", return_value=files): + with patch("os.path.isfile", return_value=True): + with patch( + "frigate.record.maintainer.psutil.process_iter", return_value=[] + ): + with patch("frigate.record.maintainer.logger.warning") as warn: + # Mock validate_and_move_segment to avoid further logic + maintainer.validate_and_move_segment = MagicMock() + + try: + await maintainer.move_files() + except ValueError as e: + if "not enough values to unpack" in str(e): + self.fail("move_files() crashed on bad filename!") + raise e + except Exception: + # Ignore other errors (like DB connection) as we only care about the unpack crash + pass + + # The bad filename is encountered in multiple loops, but should only warn once. + matching = [ + c + for c in warn.call_args_list + if c.args + and isinstance(c.args[0], str) + and "Skipping unexpected files in cache" in c.args[0] + ] + self.assertEqual( + 1, + len(matching), + f"Expected a single warning for unexpected files, got {len(matching)}", + ) + + async def test_drops_quiet_segment_when_only_motion_retention(self): + # Regression: when motion retention is enabled but a segment has no + # motion and no review overlaps it, the segment must still be dropped. + # Otherwise it sits in cache forever, accumulates, and triggers the + # "Unable to keep up with recording segments in cache" warning every + # ~10s as the overflow trim in move_files discards the oldest one. + config = MagicMock(spec=FrigateConfig) + + camera_config = MagicMock() + camera_config.record.enabled = True + camera_config.record.continuous.days = 0 + camera_config.record.motion.days = 1 + camera_config.record.event_pre_capture = 5 + config.cameras = {"test_cam": camera_config} + + stop_event = MagicMock() + maintainer = RecordingMaintainer(config, stop_event) + + now = datetime.datetime.now(datetime.UTC) + start_time = now - datetime.timedelta(seconds=20) + end_time = now - datetime.timedelta(seconds=10) + cache_path = "/tmp/cache/test_cam@20260417150000+0000.mp4" + + maintainer.end_time_cache = {cache_path: (end_time, 10.0)} + # Single processed frame well past end_time with no motion/objects. + maintainer.object_recordings_info["test_cam"] = [(now.timestamp(), [], [], [])] + maintainer.audio_recordings_info["test_cam"] = [] + + maintainer.drop_segment = MagicMock() + maintainer.recordings_publisher = MagicMock() + + result = await maintainer.validate_and_move_segment( + "test_cam", + reviews=[], + recording={"start_time": start_time, "cache_path": cache_path}, + ) + + self.assertIsNone(result) + maintainer.drop_segment.assert_called_once_with(cache_path) + + async def test_expire_stale_recordings_info_drops_only_absent_cameras(self): + config = MagicMock(spec=FrigateConfig) + config.cameras = {} + stop_event = MagicMock() + maintainer = RecordingMaintainer(config, stop_event) + + now = datetime.datetime.now().timestamp() + ancient = now - 86400 + recent = now - 1 + + maintainer.object_recordings_info["present_cam"] = [(ancient, [], [], [])] + maintainer.audio_recordings_info["present_cam"] = [(ancient, 0, [])] + + maintainer.object_recordings_info["absent_cam"] = [ + (ancient, [], [], []), + (recent, [], [], []), + ] + maintainer.audio_recordings_info["absent_cam"] = [ + (ancient, 0, []), + (recent, 0, []), + ] + + grouped_recordings = {"present_cam": [{"start_time": ancient}]} + + maintainer._expire_stale_recordings_info(grouped_recordings) + + self.assertEqual( + maintainer.object_recordings_info["present_cam"], [(ancient, [], [], [])] + ) + self.assertEqual( + maintainer.audio_recordings_info["present_cam"], [(ancient, 0, [])] + ) + + self.assertEqual( + maintainer.object_recordings_info["absent_cam"], [(recent, [], [], [])] + ) + self.assertEqual( + maintainer.audio_recordings_info["absent_cam"], [(recent, 0, [])] + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_media_auth.py b/frigate/test/test_media_auth.py new file mode 100644 index 0000000..d025fea --- /dev/null +++ b/frigate/test/test_media_auth.py @@ -0,0 +1,381 @@ +"""Unit tests for `frigate.api.media_auth`. + +Covers URI classification, the role-vs-camera decision matrix, and the export +DB-lookup path. These are pure functions/DB lookups — no HTTP stack involved. +""" + +import datetime +import logging +import os +import unittest + +from peewee_migrate import Router +from playhouse.sqlite_ext import SqliteExtDatabase +from playhouse.sqliteq import SqliteQueueDatabase + +from frigate.api.media_auth import ( + MediaAuthResolution, + deny_response_for_media_uri, + extract_path, + resolve_media_uri, +) +from frigate.config import FrigateConfig +from frigate.models import Event, Export, Recordings, ReviewSegment +from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS + +_CONFIG = { + "mqtt": {"host": "mqtt"}, + "auth": {"roles": {"limited_user": ["front_door"]}}, + "cameras": { + "front_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + "back_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + # Camera name with a hyphen — exercises longest-prefix match. + "back-yard": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + }, +} + + +class TestExtractPath(unittest.TestCase): + def test_full_url(self): + self.assertEqual( + extract_path("http://host:8971/clips/front_door-1.jpg"), + "/clips/front_door-1.jpg", + ) + + def test_strips_query_string(self): + self.assertEqual( + extract_path("http://h/recordings/2026-05-11/14/front_door/00.00.mp4?t=1"), + "/recordings/2026-05-11/14/front_door/00.00.mp4", + ) + + def test_path_only(self): + self.assertEqual(extract_path("/exports/x.mp4"), "/exports/x.mp4") + + def test_percent_decoded(self): + self.assertEqual( + extract_path("http://h/clips/front%20door-1.jpg"), + "/clips/front door-1.jpg", + ) + + def test_empty(self): + self.assertIsNone(extract_path(None)) + self.assertIsNone(extract_path("")) + + +class TestResolveMediaUri(unittest.TestCase): + def setUp(self): + self.config = FrigateConfig(**_CONFIG) + + def _assert(self, uri, resolution, camera=None): + got_resolution, got_camera = resolve_media_uri(uri, self.config) + self.assertEqual(got_resolution, resolution, uri) + self.assertEqual(got_camera, camera, uri) + + def test_unknown_paths(self): + self._assert("/api/events", MediaAuthResolution.UNKNOWN) + self._assert("/", MediaAuthResolution.UNKNOWN) + self._assert("", MediaAuthResolution.UNKNOWN) + + def test_recordings(self): + self._assert("/recordings/", MediaAuthResolution.LISTING_NEUTRAL) + self._assert("/recordings/2026-05-11/", MediaAuthResolution.LISTING_NEUTRAL) + self._assert( + "/recordings/2026-05-11/14/", MediaAuthResolution.LISTING_MULTI_CAMERA + ) + self._assert( + "/recordings/2026-05-11/14/front_door/", + MediaAuthResolution.CAMERA, + camera="front_door", + ) + self._assert( + "/recordings/2026-05-11/14/back_door/00.00.mp4", + MediaAuthResolution.CAMERA, + camera="back_door", + ) + + def test_clip_flat_filename_resolves_camera(self): + self._assert( + "/clips/front_door-1234.jpg", + MediaAuthResolution.CAMERA, + camera="front_door", + ) + self._assert( + "/clips/back_door-1234-clean.webp", + MediaAuthResolution.CAMERA, + camera="back_door", + ) + + def test_clip_filename_with_hyphenated_camera_name(self): + # Camera name "back-yard" itself contains a hyphen; longest-prefix + # match must pick `back-yard`, not the bogus `back` prefix. + self._assert( + "/clips/back-yard-1234.jpg", + MediaAuthResolution.CAMERA, + camera="back-yard", + ) + + def test_clip_filename_no_matching_camera(self): + # Looks like a media path but couldn't classify — fail closed for + # restricted users (UNRESOLVED_MEDIA), not pass-through. + self._assert( + "/clips/nonexistent-1234.jpg", MediaAuthResolution.UNRESOLVED_MEDIA + ) + + def test_clip_thumbs(self): + self._assert("/clips/thumbs/", MediaAuthResolution.LISTING_MULTI_CAMERA) + self._assert( + "/clips/thumbs/front_door/", + MediaAuthResolution.CAMERA, + camera="front_door", + ) + self._assert( + "/clips/thumbs/back_door/abc.webp", + MediaAuthResolution.CAMERA, + camera="back_door", + ) + + def test_clip_previews(self): + self._assert("/clips/previews/", MediaAuthResolution.LISTING_MULTI_CAMERA) + self._assert( + "/clips/previews/front_door/", + MediaAuthResolution.CAMERA, + camera="front_door", + ) + self._assert( + "/clips/previews/back_door/segment.mp4", + MediaAuthResolution.CAMERA, + camera="back_door", + ) + + def test_clip_review_thumbs(self): + # Format: /clips/review/thumb-{camera}-{review_id}.webp (frigate/review/maintainer.py). + self._assert( + "/clips/review/thumb-front_door-abc123.webp", + MediaAuthResolution.CAMERA, + camera="front_door", + ) + # Hyphenated camera name — longest-prefix match. + self._assert( + "/clips/review/thumb-back-yard-abc123.webp", + MediaAuthResolution.CAMERA, + camera="back-yard", + ) + # Unknown camera prefix → unresolved, not allowed for restricted users. + self._assert( + "/clips/review/thumb-unknown-cam-abc123.webp", + MediaAuthResolution.UNRESOLVED_MEDIA, + ) + + def test_clip_admin_only_subtrees(self): + self._assert("/clips/faces/train/foo.webp", MediaAuthResolution.ADMIN_ONLY) + self._assert("/clips/faces/", MediaAuthResolution.ADMIN_ONLY) + self._assert("/clips/genai-requests/x/0.webp", MediaAuthResolution.ADMIN_ONLY) + self._assert( + "/clips/preview_restart_cache/x.mp4", MediaAuthResolution.ADMIN_ONLY + ) + self._assert("/clips/some_model/train/x.jpg", MediaAuthResolution.ADMIN_ONLY) + self._assert("/clips/some_model/dataset/x.jpg", MediaAuthResolution.ADMIN_ONLY) + + def test_clip_unknown_subtree_is_unresolved(self): + # Unknown /clips/{x}/{y}/... subtree falls through as unresolved (not + # admin-only) so restricted users get 403 without admins being denied + # access to legitimate but unrecognized resources. + self._assert("/clips/random_dir/foo.jpg", MediaAuthResolution.UNRESOLVED_MEDIA) + + def test_clip_top_level_listing(self): + self._assert("/clips/", MediaAuthResolution.LISTING_MULTI_CAMERA) + + def test_exports_listing(self): + self._assert("/exports/", MediaAuthResolution.LISTING_MULTI_CAMERA) + + +class TestExportResolution(unittest.TestCase): + """Export resolution requires a DB lookup.""" + + def setUp(self): + migrate_db = SqliteExtDatabase("test.db") + del logging.getLogger("peewee_migrate").handlers[:] + Router(migrate_db).run() + migrate_db.close() + self.db = SqliteQueueDatabase(TEST_DB) + self.db.bind([Event, ReviewSegment, Recordings, Export]) + self.config = FrigateConfig(**_CONFIG) + + def tearDown(self): + if not self.db.is_closed(): + self.db.close() + for f in TEST_DB_CLEANUPS: + try: + os.remove(f) + except OSError: + pass + + def _insert_export(self, export_id, camera, filename): + Export.insert( + id=export_id, + camera=camera, + name=f"export-{export_id}", + date=int(datetime.datetime.now().timestamp()), + video_path=f"/media/frigate/exports/{filename}", + thumb_path=f"/media/frigate/exports/{filename}.jpg", + in_progress=False, + ).execute() + + def test_export_resolves_camera(self): + self._insert_export( + "exp1", "back_door", "back_door_20260511_140000-20260511_150000_abc123.mp4" + ) + resolution, camera = resolve_media_uri( + "/exports/back_door_20260511_140000-20260511_150000_abc123.mp4", + self.config, + ) + self.assertEqual(resolution, MediaAuthResolution.CAMERA) + self.assertEqual(camera, "back_door") + + def test_unknown_export_is_unresolved(self): + # No matching row → UNRESOLVED_MEDIA (fail closed for restricted users), + # not UNKNOWN (which would pass-through). + resolution, camera = resolve_media_uri( + "/exports/does_not_exist.mp4", self.config + ) + self.assertEqual(resolution, MediaAuthResolution.UNRESOLVED_MEDIA) + self.assertIsNone(camera) + + def test_export_anchored_match_not_endswith(self): + # Anchored exact-path equality must NOT match by filename suffix. + # A request like /exports/clip.mp4 must not authorize against a row at + # /media/frigate/exports/back_door_clip.mp4 just because the suffix matches. + self._insert_export("exp_bd", "back_door", "back_door_clip.mp4") + self._insert_export("exp_fd", "front_door", "front_door_clip.mp4") + resolution, _ = resolve_media_uri("/exports/clip.mp4", self.config) + self.assertEqual(resolution, MediaAuthResolution.UNRESOLVED_MEDIA) + + +class TestDenyResponseForMediaUri(unittest.TestCase): + """End-to-end decision check used by /auth.""" + + def setUp(self): + self.config = FrigateConfig(**_CONFIG) + + def _deny(self, url, role): + return deny_response_for_media_uri(url, role, self.config) + + def test_admin_always_allowed(self): + self.assertIsNone(self._deny("/clips/back_door-1.jpg", "admin")) + self.assertIsNone(self._deny("/clips/", "admin")) + self.assertIsNone(self._deny("/clips/faces/x.webp", "admin")) + self.assertIsNone( + self._deny("/recordings/2026-05-11/14/back_door/00.00.mp4", "admin") + ) + + def test_unrestricted_role_allowed(self): + # "viewer" role has no entry in roles_dict → full access (matches the + # behavior of require_camera_access). + self.assertIsNone(self._deny("/clips/back_door-1.jpg", "viewer")) + self.assertIsNone(self._deny("/clips/", "viewer")) + + def test_restricted_role_allowed_camera(self): + self.assertIsNone(self._deny("/clips/front_door-1.jpg", "limited_user")) + self.assertIsNone( + self._deny("/recordings/2026-05-11/14/front_door/00.00.mp4", "limited_user") + ) + self.assertIsNone( + self._deny("/clips/thumbs/front_door/abc.webp", "limited_user") + ) + + def test_restricted_role_blocked_other_camera(self): + self.assertEqual(self._deny("/clips/back_door-1.jpg", "limited_user"), 403) + self.assertEqual( + self._deny("/recordings/2026-05-11/14/back_door/00.00.mp4", "limited_user"), + 403, + ) + self.assertEqual( + self._deny("/clips/thumbs/back_door/abc.webp", "limited_user"), 403 + ) + + def test_restricted_role_blocked_admin_only(self): + self.assertEqual(self._deny("/clips/faces/train/foo.webp", "limited_user"), 403) + + def test_restricted_role_blocked_multi_camera_listing(self): + self.assertEqual(self._deny("/clips/", "limited_user"), 403) + self.assertEqual(self._deny("/exports/", "limited_user"), 403) + self.assertEqual(self._deny("/recordings/2026-05-11/14/", "limited_user"), 403) + + def test_restricted_role_allowed_neutral_listing(self): + self.assertIsNone(self._deny("/recordings/", "limited_user")) + self.assertIsNone(self._deny("/recordings/2026-05-11/", "limited_user")) + + def test_non_media_uri_passes_through(self): + self.assertIsNone(self._deny("/api/events", "limited_user")) + self.assertIsNone(self._deny("http://h/login", "limited_user")) + + def test_missing_header(self): + self.assertIsNone(self._deny(None, "limited_user")) + self.assertIsNone(self._deny("", "limited_user")) + + def test_traversal_in_media_uri_denied_for_all_roles(self): + # Bypass attempt: parts[3] looks like an allowed camera, but the + # normalized path nginx would serve points at a forbidden camera. + # Both restricted and admin should be denied — the URI is malformed + # and we refuse to make an auth decision against it. + traversal_uris = [ + "/recordings/2026-05-11/14/front_door/../back_door/00.00.mp4", + "/clips/front_door-1.jpg/../back_door-1.jpg", + "/exports/../recordings/2026-05-11/14/back_door/00.00.mp4", + "/clips/./back_door-1.jpg", + ] + for uri in traversal_uris: + self.assertEqual(self._deny(uri, "limited_user"), 403, uri) + self.assertEqual(self._deny(uri, "admin"), 403, uri) + self.assertEqual(self._deny(uri, "viewer"), 403, uri) + + def test_traversal_outside_media_passes_through(self): + # `..` in non-media URIs is not our problem; the backend handles it. + self.assertIsNone(self._deny("/api/foo/../bar", "limited_user")) + + def test_percent_encoded_traversal_denied(self): + # nginx may decode percent-encoded `%2E%2E` to `..` before serving; + # we must apply the same denial after percent-decoding. + self.assertEqual( + self._deny( + "/recordings/2026-05-11/14/front_door/%2E%2E/back_door/00.mp4", + "limited_user", + ), + 403, + ) + + def test_unresolved_media_fails_closed_for_restricted(self): + # Restricted user requesting a media URI we can't classify (no DB row, + # unknown clip prefix, unknown clip subtree) must be denied. + self.assertEqual(self._deny("/clips/nonexistent-1.jpg", "limited_user"), 403) + self.assertEqual(self._deny("/clips/random_dir/foo.jpg", "limited_user"), 403) + self.assertEqual( + self._deny("/clips/review/thumb-unknown_cam-1.webp", "limited_user"), + 403, + ) + + def test_unresolved_media_allowed_for_admin(self): + # Admin and full-access roles are *not* denied on UNRESOLVED_MEDIA — + # nginx returns 404 if the file doesn't exist on disk anyway, and we + # don't want a stale DB to lock out admins. + self.assertIsNone(self._deny("/clips/nonexistent-1.jpg", "admin")) + self.assertIsNone(self._deny("/clips/nonexistent-1.jpg", "viewer")) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_motion_detector.py b/frigate/test/test_motion_detector.py new file mode 100644 index 0000000..cdf4210 --- /dev/null +++ b/frigate/test/test_motion_detector.py @@ -0,0 +1,91 @@ +import unittest + +import numpy as np + +from frigate.config.camera.motion import MotionConfig +from frigate.motion.improved_motion import ImprovedMotionDetector + + +class TestImprovedMotionDetector(unittest.TestCase): + def setUp(self): + # small frame for testing; actual frames are grayscale + self.frame_shape = (100, 100) # height, width + self.config = MotionConfig() + # motion detector assumes a rasterized_mask attribute exists on config + # when update_mask() is called; add one manually by bypassing pydantic. + object.__setattr__( + self.config, + "rasterized_mask", + np.ones((self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8), + ) + + # create minimal PTZ metrics stub to satisfy detector checks + class _Stub: + def __init__(self, value=False): + self.value = value + + def is_set(self): + return bool(self.value) + + class DummyPTZ: + def __init__(self): + self.autotracker_enabled = _Stub(False) + self.motor_stopped = _Stub(False) + self.stop_time = _Stub(0) + + self.detector = ImprovedMotionDetector( + self.frame_shape, self.config, fps=30, ptz_metrics=DummyPTZ() + ) + + # establish a baseline frame (all zeros) + base_frame = np.zeros( + (self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8 + ) + self.detector.detect(base_frame) + + def _half_change_frame(self) -> np.ndarray: + """Produce a frame where roughly half of the pixels are different.""" + frame = np.zeros((self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8) + # flip the top half to white + frame[: self.frame_shape[0] // 2, :] = 255 + return frame + + def test_skip_motion_threshold_default(self): + """With the default (None) setting, motion should always be reported.""" + frame = self._half_change_frame() + boxes = self.detector.detect(frame) + self.assertTrue( + boxes, "Expected motion boxes when skip threshold is unset (disabled)" + ) + + def test_skip_motion_threshold_applied(self): + """Setting a low skip threshold should prevent any boxes from being returned.""" + # change the config and update the detector reference + self.config.skip_motion_threshold = 0.4 + self.detector.config = self.config + self.detector.update_mask() + + frame = self._half_change_frame() + boxes = self.detector.detect(frame) + self.assertEqual( + boxes, + [], + "Motion boxes should be empty when scene change exceeds skip threshold", + ) + + def test_skip_motion_threshold_does_not_affect_calibration(self): + """Even when skipping, the detector should go into calibrating state.""" + self.config.skip_motion_threshold = 0.4 + self.detector.config = self.config + self.detector.update_mask() + + frame = self._half_change_frame() + _ = self.detector.detect(frame) + self.assertTrue( + self.detector.calibrating, + "Detector should be in calibrating state after skip event", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_motion_search_batch.py b/frigate/test/test_motion_search_batch.py new file mode 100644 index 0000000..0e7fcdf --- /dev/null +++ b/frigate/test/test_motion_search_batch.py @@ -0,0 +1,58 @@ +"""Tests for motion search batch helpers (runs + timestamp mapping).""" + +import unittest +from dataclasses import dataclass + +from frigate.jobs.motion_search_batch import ( + build_segment_time_map, + coalesce_runs, + stream_time_to_absolute, +) + + +@dataclass +class _Seg: + path: str + start_time: float + end_time: float + + +def _run_seconds(run): + return float(run[-1].end_time) - float(run[0].start_time) + + +class TestCoalesceRuns(unittest.TestCase): + def test_contiguous_segments_form_one_run(self): + segs = [_Seg("a", 0.0, 10.0), _Seg("b", 10.0, 20.0), _Seg("c", 20.0, 30.0)] + runs = coalesce_runs(segs, max_seconds=600.0, epsilon=0.5) + self.assertEqual(len(runs), 1) + self.assertEqual(len(runs[0]), 3) + + def test_time_gap_splits_runs(self): + # b ends 20, c starts 25 -> 5s gap > epsilon -> two runs. + segs = [_Seg("a", 0.0, 10.0), _Seg("b", 10.0, 20.0), _Seg("c", 25.0, 35.0)] + runs = coalesce_runs(segs, max_seconds=600.0, epsilon=0.5) + self.assertEqual([len(r) for r in runs], [2, 1]) + + def test_max_duration_caps_a_run(self): + # Five contiguous 10s segments, cap 25s. + segs = [_Seg(str(i), i * 10.0, i * 10.0 + 10.0) for i in range(5)] + runs = coalesce_runs(segs, max_seconds=25.0, epsilon=0.5) + self.assertTrue(all(_run_seconds(r) <= 30.0 for r in runs)) + self.assertEqual(sum(len(r) for r in runs), 5) + + def test_empty(self): + self.assertEqual(coalesce_runs([], max_seconds=600.0, epsilon=0.5), []) + + +class TestTimestampMapping(unittest.TestCase): + def test_gapfree_run_maps_to_start_plus_pts(self): + run = [_Seg("a", 1000.0, 1010.0), _Seg("b", 1010.0, 1020.0)] + time_map = build_segment_time_map(run) + self.assertAlmostEqual(stream_time_to_absolute(time_map, 3.0), 1003.0) + self.assertAlmostEqual(stream_time_to_absolute(time_map, 12.0), 1012.0) + + def test_past_end_clamps(self): + run = [_Seg("a", 1000.0, 1010.0)] + time_map = build_segment_time_map(run) + self.assertAlmostEqual(stream_time_to_absolute(time_map, 9.9), 1009.9) diff --git a/frigate/test/test_motion_search_decode.py b/frigate/test/test_motion_search_decode.py new file mode 100644 index 0000000..fda0837 --- /dev/null +++ b/frigate/test/test_motion_search_decode.py @@ -0,0 +1,190 @@ +"""Tests for the motion search hardware-accelerated decode helpers.""" + +import unittest +from types import SimpleNamespace +from unittest import mock + +from frigate.jobs.motion_search_decode import ( + KEYFRAME_MAX_GAP_SECONDS, + build_vod_decode_command, + keyframe_sampling_eligible, + probe_video_dimensions, + probe_vod_keyframe_pts, + resolve_motion_decode_args, +) + + +def _fake_camera_config( + hwaccel_args, gpu=0, fps=5, width=1280, height=720, ffmpeg_path="ffmpeg" +): + return SimpleNamespace( + ffmpeg=SimpleNamespace( + hwaccel_args=hwaccel_args, gpu=gpu, ffmpeg_path=ffmpeg_path + ), + detect=SimpleNamespace(fps=fps, width=width, height=height), + ) + + +class TestResolveMotionDecodeArgs(unittest.TestCase): + def test_vaapi_preset_is_accelerated(self): + args = resolve_motion_decode_args(_fake_camera_config("preset-vaapi")) + self.assertIn("-hwaccel", args) + self.assertIn("vaapi", args) + + def test_non_nv12_preset_falls_back_to_software(self): + # rkmpp produces drm_prime surfaces that do not download to nv12, so it + # must resolve to software decode (empty args) rather than risk corrupt + # frames. + self.assertEqual( + resolve_motion_decode_args(_fake_camera_config("preset-rkmpp")), [] + ) + + def test_custom_args_fall_back_to_software(self): + # Arbitrary custom hwaccel args (a list, not a preset) decode in software + # to preserve byte-identical results. + self.assertEqual( + resolve_motion_decode_args(_fake_camera_config(["-hwaccel", "vulkan"])), + [], + ) + + def test_nvidia_codec_preset_is_accelerated(self): + # Codec-specific nvidia presets resolve to the same cuda decode args as + # the bare preset, so eligibility is derived from -hwaccel_output_format + # rather than a hardcoded list that omitted these aliases. + args = resolve_motion_decode_args(_fake_camera_config("preset-nvidia-h264")) + self.assertIn("-hwaccel_output_format", args) + self.assertIn("cuda", args) + + def test_software_only_preset_falls_back_to_software(self): + # A preset with no -hwaccel_output_format (decoder-based, no GPU surface) + # cannot use the nv12 download step, so it decodes in software. + self.assertEqual( + resolve_motion_decode_args(_fake_camera_config("preset-rpi-64-h264")), [] + ) + + +class TestKeyframeEligibility(unittest.TestCase): + def test_regular_short_gop_is_eligible(self): + pts = [0.0, 0.5, 1.0, 1.5, 2.0] # 0.5s gaps + self.assertTrue(keyframe_sampling_eligible(pts)) + + def test_long_gop_is_ineligible(self): + pts = [0.0, 5.0, 10.0] # 5s gaps + self.assertFalse(keyframe_sampling_eligible(pts)) + + def test_irregular_gop_ineligible_when_a_gap_is_long(self): + pts = [0.0, 0.5, 1.0, 8.0] # one 7s gap + self.assertFalse(keyframe_sampling_eligible(pts)) + + def test_too_few_keyframes_ineligible(self): + self.assertFalse(keyframe_sampling_eligible([1.0])) + self.assertFalse(keyframe_sampling_eligible([])) + + def test_default_max_gap_constant(self): + self.assertEqual(KEYFRAME_MAX_GAP_SECONDS, 2.0) + + +class TestVodDecodeCommand(unittest.TestCase): + URL = "http://127.0.0.1:5000/vod/cam/start/1/end/2/index.m3u8" + + def test_keyframe_command_shape(self): + cmd = build_vod_decode_command( + "ffmpeg", + self.URL, + decode_args=[], + crop=(100, 80, 10, 20), + scale=(50, 40), + gray=True, + skip_nonkey=True, + fps_rate=None, + ) + joined = " ".join(cmd) + self.assertIn("-skip_frame nokey", joined) + self.assertIn("-protocol_whitelist pipe,file,http,tcp", joined) + self.assertIn(f"-i {self.URL}", joined) + self.assertIn("crop=100:80:10:20", joined) + self.assertIn("scale=50:40", joined) + self.assertIn("-pix_fmt gray", joined) + self.assertNotIn("fps=", joined) + + def test_fps_command_uses_fps_filter_not_skip_frame(self): + cmd = build_vod_decode_command( + "ffmpeg", + self.URL, + decode_args=[], + crop=None, + scale=None, + gray=False, + skip_nonkey=False, + fps_rate=2.0, + ) + joined = " ".join(cmd) + self.assertNotIn("skip_frame", joined) + self.assertIn("fps=2.0", joined) + self.assertIn("-pix_fmt bgr24", joined) + + def test_hwaccel_inserts_hwdownload(self): + cmd = build_vod_decode_command( + "ffmpeg", + self.URL, + decode_args=["-hwaccel", "vaapi"], + crop=None, + scale=None, + gray=True, + skip_nonkey=True, + fps_rate=None, + ) + joined = " ".join(cmd) + self.assertIn("hwdownload", joined) + self.assertIn("format=nv12", joined) + + +class TestProbeVodKeyframePts(unittest.TestCase): + def test_parses_keyframe_packets(self): + sample = ( + '{"packets":[' + '{"pts_time":"0.000000","flags":"K__"},' + '{"pts_time":"1.000000","flags":"___"},' + '{"pts_time":"2.000000","flags":"K__"}]}' + ) + completed = mock.Mock(stdout=sample, returncode=0) + with mock.patch( + "frigate.jobs.motion_search_decode.sp.run", return_value=completed + ): + pts = probe_vod_keyframe_pts("ffprobe", "http://x/index.m3u8") + self.assertEqual(pts, [0.0, 2.0]) + + def test_returns_empty_on_failure(self): + with mock.patch( + "frigate.jobs.motion_search_decode.sp.run", + side_effect=OSError("boom"), + ): + self.assertEqual(probe_vod_keyframe_pts("ffprobe", "http://x"), []) + + +class TestProbeVideoDimensions(unittest.TestCase): + def test_parses_dimensions_and_fps(self): + sample = ( + '{"streams":[{"width":1920,"height":1080,"avg_frame_rate":"30000/1001"}]}' + ) + completed = mock.Mock(stdout=sample, returncode=0) + with mock.patch( + "frigate.jobs.motion_search_decode.sp.run", return_value=completed + ): + dims = probe_video_dimensions("ffprobe", "/tmp/a.mp4") + assert dims is not None + width, height, fps = dims + self.assertEqual((width, height), (1920, 1080)) + self.assertAlmostEqual(fps, 29.97, places=2) + + def test_returns_none_on_zero_dimensions(self): + sample = '{"streams":[{"width":0,"height":0,"avg_frame_rate":"0/0"}]}' + completed = mock.Mock(stdout=sample, returncode=0) + with mock.patch( + "frigate.jobs.motion_search_decode.sp.run", return_value=completed + ): + self.assertIsNone(probe_video_dimensions("ffprobe", "/tmp/a.mp4")) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_motion_search_spatial.py b/frigate/test/test_motion_search_spatial.py new file mode 100644 index 0000000..b8044df --- /dev/null +++ b/frigate/test/test_motion_search_spatial.py @@ -0,0 +1,87 @@ +"""Tests for motion search spatial (crop/scale/mask) helpers.""" + +import unittest + +import numpy as np + +from frigate.jobs.motion_search import ( + build_scaled_roi_mask, + compute_roi_crop_and_scale, + detect_motion_scaled, +) + + +class TestComputeRoiCropAndScale(unittest.TestCase): + def test_crop_box_in_record_pixels(self): + # ROI covering x [0.25, 0.75], y [0.5, 1.0] of a 1000x600 frame. + polygon = [[0.25, 0.5], [0.75, 0.5], [0.75, 1.0], [0.25, 1.0]] + crop, scaled = compute_roi_crop_and_scale(polygon, 1000, 600, scale_target=125) + cw, ch, cx, cy = crop + self.assertEqual((cx, cy), (250, 300)) + self.assertEqual((cw, ch), (500, 300)) + # longest side 500 -> factor 0.25 -> (125, 75), rounded down to even. + self.assertEqual(scaled, (124, 74)) + + def test_never_upscales(self): + polygon = [[0.0, 0.0], [0.1, 0.0], [0.1, 0.1], [0.0, 0.1]] + crop, scaled = compute_roi_crop_and_scale(polygon, 200, 200, scale_target=400) + cw, ch, _, _ = crop + # crop is 20x20; target 400 would upscale, so scaled == crop size. + self.assertEqual(scaled, (cw, ch)) + + def test_scaled_dims_are_at_least_one(self): + polygon = [[0.0, 0.0], [0.02, 0.0], [0.02, 0.02], [0.0, 0.02]] + crop, scaled = compute_roi_crop_and_scale(polygon, 50, 50, scale_target=1) + self.assertGreaterEqual(scaled[0], 1) + self.assertGreaterEqual(scaled[1], 1) + + def test_all_dims_are_even_for_nv12(self): + # Odd-aligned ROI on an odd-ish frame must still yield even crop/scale so + # the nv12 hwdownload byte stream matches the expected frame size. + polygon = [[0.123, 0.321], [0.777, 0.321], [0.777, 0.901], [0.123, 0.901]] + crop, scaled = compute_roi_crop_and_scale(polygon, 1377, 911, scale_target=257) + for value in (*crop, *scaled): + self.assertEqual(value % 2, 0, f"{value} is not even") + + +class TestBuildScaledRoiMask(unittest.TestCase): + def test_mask_matches_scaled_dims_and_has_coverage(self): + polygon = [[0.25, 0.5], [0.75, 0.5], [0.75, 1.0], [0.25, 1.0]] + crop, scaled = compute_roi_crop_and_scale(polygon, 1000, 600, scale_target=125) + mask = build_scaled_roi_mask(polygon, 1000, 600, crop, scaled) + self.assertEqual(mask.shape, (scaled[1], scaled[0])) + self.assertEqual(mask.dtype, np.uint8) + # A full rectangle ROI fills its whole crop -> mask is all 255. + self.assertGreater(np.count_nonzero(mask), 0) + self.assertEqual(np.count_nonzero(mask), mask.size) + + +class TestDetectMotionScaled(unittest.TestCase): + def _ts(self, idx): + return float(idx) + + def test_finds_change_between_frames(self): + mask = np.full((60, 80), 255, dtype=np.uint8) + f0 = np.zeros((60, 80), dtype=np.uint8) + f1 = np.zeros((60, 80), dtype=np.uint8) + f1[10:50, 20:60] = 255 # big bright block appears + frames = [(0, f0), (30, f1)] + results = detect_motion_scaled( + frames, mask, threshold=30, min_area=1.0, timestamp_fn=self._ts + ) + self.assertEqual(len(results), 1) + self.assertEqual(results[0].timestamp, 30.0) + self.assertGreater(results[0].change_percentage, 0.0) + + def test_no_change_yields_nothing(self): + mask = np.full((60, 80), 255, dtype=np.uint8) + f0 = np.zeros((60, 80), dtype=np.uint8) + f1 = np.zeros((60, 80), dtype=np.uint8) + results = detect_motion_scaled( + [(0, f0), (30, f1)], mask, threshold=30, min_area=1.0, timestamp_fn=self._ts + ) + self.assertEqual(results, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_mqtt_topic_registration.py b/frigate/test/test_mqtt_topic_registration.py new file mode 100644 index 0000000..309fafb --- /dev/null +++ b/frigate/test/test_mqtt_topic_registration.py @@ -0,0 +1,100 @@ +"""Tests for MQTT command topic callback registration.""" + +import unittest +from unittest.mock import MagicMock, patch + +from frigate.comms.mqtt import MqttClient + + +def _make_camera_mock( + *, + enabled: bool = True, + notifications_enabled_in_config: bool = False, +) -> MagicMock: + """Build a camera config mock with the fields _start() reads.""" + camera = MagicMock() + camera.enabled = enabled + camera.notifications.enabled_in_config = notifications_enabled_in_config + camera.onvif.host = None + camera.motion.mask = {} + camera.objects.mask = {} + camera.zones = {} + return camera + + +def _registered_topics( + cameras: dict[str, MagicMock], + *, + global_notifications_enabled_in_config: bool = False, +) -> set[str]: + """Start an MqttClient against a mocked paho client and collect the + topics registered via message_callback_add.""" + config = MagicMock() + config.cameras = cameras + config.notifications.enabled_in_config = global_notifications_enabled_in_config + config.mqtt.topic_prefix = "frigate" + config.mqtt.client_id = "frigate" + config.mqtt.user = None + config.mqtt.tls_ca_certs = None + config.mqtt.tls_insecure = None + + with patch("frigate.comms.mqtt.mqtt.Client") as client_cls: + mqtt_client = MqttClient(config) + mqtt_client.subscribe(MagicMock()) + + paho_client = client_cls.return_value + return {call.args[0] for call in paho_client.message_callback_add.call_args_list} + + +class TestMqttTopicRegistration(unittest.TestCase): + def test_camera_notification_topics_registered(self): + """Per-camera notification set/suspend must be registered so paho + routes them to the dispatcher (unregistered topics drop silently).""" + topics = _registered_topics( + {"front_door": _make_camera_mock(notifications_enabled_in_config=True)} + ) + + self.assertIn("frigate/front_door/notifications/set", topics) + self.assertIn("frigate/front_door/notifications/suspend", topics) + + def test_global_set_registered_with_camera_only_notifications(self): + """The global topic must work when notifications are enabled only at + the camera level, matching the WebPushClient gating in app.py.""" + topics = _registered_topics( + {"front_door": _make_camera_mock(notifications_enabled_in_config=True)}, + global_notifications_enabled_in_config=False, + ) + + self.assertIn("frigate/notifications/set", topics) + + def test_global_set_registered_with_global_notifications(self): + topics = _registered_topics( + {"front_door": _make_camera_mock()}, + global_notifications_enabled_in_config=True, + ) + + self.assertIn("frigate/notifications/set", topics) + + def test_global_set_not_registered_when_notifications_unconfigured(self): + topics = _registered_topics( + {"front_door": _make_camera_mock()}, + global_notifications_enabled_in_config=False, + ) + + self.assertNotIn("frigate/notifications/set", topics) + + def test_disabled_camera_does_not_enable_global_set(self): + topics = _registered_topics( + { + "front_door": _make_camera_mock( + enabled=False, notifications_enabled_in_config=True + ) + }, + global_notifications_enabled_in_config=False, + ) + + self.assertNotIn("frigate/notifications/set", topics) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_norfair_distance.py b/frigate/test/test_norfair_distance.py new file mode 100644 index 0000000..a79b91c --- /dev/null +++ b/frigate/test/test_norfair_distance.py @@ -0,0 +1,91 @@ +import math +import unittest + +import numpy as np +from norfair.camera_motion import ( + HomographyTransformation, + TranslationTransformation, +) + +from frigate.ptz.autotrack import transform_is_finite +from frigate.track.norfair_tracker import distance + + +class TestNorfairDistance(unittest.TestCase): + """Regression tests for the tracker distance guard. + + norfair raises a hard ValueError on any nan distance, which kills the camera + process. During autotracking, an ill-conditioned homography can hand the + tracker a non-finite or degenerate estimate box, so distance() must never + return nan for any input. + """ + + def setUp(self) -> None: + # boxes are [[x1, y1], [x2, y2]] + self.detection = np.array([[805.0, 402.0], [864.0, 521.0]]) + self.estimate = np.array([[800.0, 400.0], [860.0, 520.0]]) + + def test_finite_boxes_give_finite_distance(self) -> None: + d = distance(self.detection, self.estimate) + self.assertTrue(math.isfinite(d)) + + def test_inf_estimate_corner_does_not_return_nan(self) -> None: + estimate = np.array([[np.inf, 400.0], [860.0, 520.0]]) + d = distance(self.detection, estimate) + self.assertFalse(math.isnan(d)) + self.assertEqual(d, float("inf")) + + def test_nan_estimate_corner_does_not_return_nan(self) -> None: + # the actual autotracking crash: a positive-only guard would miss this + # because nan <= 0 is False + estimate = np.array([[np.nan, 400.0], [860.0, 520.0]]) + d = distance(self.detection, estimate) + self.assertFalse(math.isnan(d)) + self.assertEqual(d, float("inf")) + + def test_zero_area_estimate_does_not_return_nan(self) -> None: + estimate = np.array([[900.0, 500.0], [900.0, 500.0]]) + d = distance(self.detection, estimate) + self.assertFalse(math.isnan(d)) + self.assertEqual(d, float("inf")) + + def test_zero_area_detection_does_not_return_nan(self) -> None: + detection = np.array([[805.0, 402.0], [805.0, 521.0]]) + d = distance(detection, self.estimate) + self.assertFalse(math.isnan(d)) + self.assertEqual(d, float("inf")) + + def test_inverted_estimate_corners_do_not_return_nan(self) -> None: + # Kalman estimates can occasionally cross corners (x2 < x1) + estimate = np.array([[860.0, 520.0], [800.0, 400.0]]) + d = distance(self.detection, estimate) + self.assertFalse(math.isnan(d)) + self.assertEqual(d, float("inf")) + + +class TestTransformIsFinite(unittest.TestCase): + def test_finite_homography_is_finite(self) -> None: + matrix = np.array([[1.0, 0.0, 5.0], [0.0, 1.0, 3.0], [0.0, 0.0, 1.0]]) + self.assertTrue(transform_is_finite(HomographyTransformation(matrix))) + + def test_finite_translation_is_finite(self) -> None: + self.assertTrue( + transform_is_finite(TranslationTransformation(np.array([12.0, -4.0]))) + ) + + def test_non_finite_homography_is_not_finite(self) -> None: + transform = HomographyTransformation(np.eye(3)) + # simulate accumulation overflowing to a non-finite matrix + transform.homography_matrix = np.array( + [[1.0, 0.0, np.inf], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] + ) + self.assertFalse(transform_is_finite(transform)) + + def test_nan_translation_is_not_finite(self) -> None: + self.assertFalse( + transform_is_finite(TranslationTransformation(np.array([np.nan, 0.0]))) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_obects.py b/frigate/test/test_obects.py new file mode 100644 index 0000000..ee0162e --- /dev/null +++ b/frigate/test/test_obects.py @@ -0,0 +1,66 @@ +import random +import unittest + +import numpy as np + +from frigate.track.tracked_object import TrackedObjectAttribute +from frigate.util.object import average_boxes + + +class TestBoxStatistics(unittest.TestCase): + def test_average_boxes_matches_numpy(self) -> None: + rng = random.Random(0) + for _ in range(5000): + boxes = [ + [rng.randint(0, 4000) for _ in range(4)] + for _ in range(rng.randint(1, 10)) + ] + expected = [float(np.mean([b[i] for b in boxes])) for i in range(4)] + self.assertEqual(average_boxes(boxes), expected) + + +class TestAttribute(unittest.TestCase): + def test_overlapping_object_selection(self) -> None: + attribute = TrackedObjectAttribute( + ( + "amazon", + 0.80078125, + (847, 242, 883, 255), + 468, + 2.769230769230769, + (702, 134, 1050, 482), + ) + ) + objects = [ + { + "label": "car", + "score": 0.98828125, + "box": (728, 223, 1266, 719), + "area": 266848, + "ratio": 1.0846774193548387, + "region": (349, 0, 1397, 1048), + "frame_time": 1727785394.498972, + "centroid": (997, 471), + "id": "1727785349.150633-408hal", + "start_time": 1727785349.150633, + "motionless_count": 362, + "position_changes": 0, + "score_history": [0.98828125, 0.95703125, 0.98828125, 0.98828125], + }, + { + "label": "person", + "score": 0.76953125, + "box": (826, 172, 939, 417), + "area": 27685, + "ratio": 0.46122448979591835, + "region": (702, 134, 1050, 482), + "frame_time": 1727785394.498972, + "centroid": (882, 294), + "id": "1727785390.499768-9fbhem", + "start_time": 1727785390.499768, + "motionless_count": 2, + "position_changes": 1, + "score_history": [0.8828125, 0.83984375, 0.91796875, 0.94140625], + }, + ] + assert attribute.find_best_object(objects) == "1727785390.499768-9fbhem" diff --git a/frigate/test/test_object_detector.py b/frigate/test/test_object_detector.py new file mode 100644 index 0000000..dc15b23 --- /dev/null +++ b/frigate/test/test_object_detector.py @@ -0,0 +1,138 @@ +import unittest +from unittest.mock import Mock, patch + +import numpy as np +from pydantic import parse_obj_as + +import frigate.detectors as detectors +import frigate.object_detection.base +from frigate.config import DetectorConfig, ModelConfig +from frigate.detectors import DetectorTypeEnum +from frigate.detectors.detector_config import InputTensorEnum + + +class TestLocalObjectDetector(unittest.TestCase): + def test_localdetectorprocess_should_only_create_specified_detector_type(self): + for det_type in detectors.api_types: + with self.subTest(det_type=det_type): + with patch.dict( + "frigate.detectors.api_types", + {det_type: Mock() for det_type in DetectorTypeEnum}, + ): + test_cfg = parse_obj_as( + DetectorConfig, ({"type": det_type, "model": {}}) + ) + test_cfg.model.path = "/test/modelpath" + test_obj = frigate.object_detection.base.LocalObjectDetector( + detector_config=test_cfg + ) + + assert test_obj is not None + for api_key, mock_detector in detectors.api_types.items(): + if test_cfg.type == api_key: + mock_detector.assert_called_once_with(test_cfg) + else: + mock_detector.assert_not_called() + + @patch.dict( + "frigate.detectors.api_types", + {det_type: Mock() for det_type in DetectorTypeEnum}, + ) + def test_detect_raw_given_tensor_input_should_return_api_detect_raw_result(self): + mock_cputfl = detectors.api_types[DetectorTypeEnum.cpu] + + TEST_DATA = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + TEST_DETECT_RESULT = np.ndarray([1, 2, 4, 8, 16, 32]) + test_obj_detect = frigate.object_detection.base.LocalObjectDetector( + detector_config=parse_obj_as(DetectorConfig, {"type": "cpu", "model": {}}) + ) + + mock_det_api = mock_cputfl.return_value + mock_det_api.detect_raw.return_value = TEST_DETECT_RESULT + + test_result = test_obj_detect.detect_raw(TEST_DATA) + + mock_det_api.detect_raw.assert_called_once_with(tensor_input=TEST_DATA) + assert test_result is mock_det_api.detect_raw.return_value + + @patch.dict( + "frigate.detectors.api_types", + {det_type: Mock() for det_type in DetectorTypeEnum}, + ) + def test_detect_raw_given_tensor_input_should_call_api_detect_raw_with_transposed_tensor( + self, + ): + mock_cputfl = detectors.api_types[DetectorTypeEnum.cpu] + + TEST_DATA = np.zeros((1, 32, 32, 3), np.uint8) + TEST_DETECT_RESULT = np.ndarray([1, 2, 4, 8, 16, 32]) + + test_cfg = parse_obj_as(DetectorConfig, {"type": "cpu", "model": {}}) + test_cfg.model.input_tensor = InputTensorEnum.nchw + + test_obj_detect = frigate.object_detection.base.LocalObjectDetector( + detector_config=test_cfg + ) + + mock_det_api = mock_cputfl.return_value + mock_det_api.detect_raw.return_value = TEST_DETECT_RESULT + + test_result = test_obj_detect.detect_raw(TEST_DATA) + + mock_det_api.detect_raw.assert_called_once() + assert ( + mock_det_api.detect_raw.call_args.kwargs["tensor_input"].shape + == np.zeros((1, 3, 32, 32)).shape + ) + + assert test_result is mock_det_api.detect_raw.return_value + + @patch.dict( + "frigate.detectors.api_types", + {det_type: Mock() for det_type in DetectorTypeEnum}, + ) + @patch("frigate.object_detection.base.load_labels") + def test_detect_given_tensor_input_should_return_lfiltered_detections( + self, mock_load_labels + ): + mock_cputfl = detectors.api_types[DetectorTypeEnum.cpu] + + TEST_DATA = np.zeros((1, 32, 32, 3), np.uint8) + TEST_DETECT_RAW = [ + [2, 0.9, 5, 4, 3, 2], + [1, 0.5, 8, 7, 6, 5], + [0, 0.4, 2, 4, 8, 16], + ] + TEST_DETECT_RESULT = [ + ("label-3", 0.9, (5, 4, 3, 2)), + ("label-2", 0.5, (8, 7, 6, 5)), + ] + TEST_LABEL_FILE = "/test_labels.txt" + mock_load_labels.return_value = [ + "label-1", + "label-2", + "label-3", + "label-4", + "label-5", + ] + + test_cfg = parse_obj_as(DetectorConfig, {"type": "cpu", "model": {}}) + test_cfg.model = ModelConfig() + test_obj_detect = frigate.object_detection.base.LocalObjectDetector( + detector_config=test_cfg, + labels=TEST_LABEL_FILE, + ) + + mock_load_labels.assert_called_once_with(TEST_LABEL_FILE) + + mock_det_api = mock_cputfl.return_value + mock_det_api.detect_raw.return_value = TEST_DETECT_RAW + + test_result = test_obj_detect.detect(tensor_input=TEST_DATA, threshold=0.5) + + mock_det_api.detect_raw.assert_called_once() + assert ( + mock_det_api.detect_raw.call_args.kwargs["tensor_input"].shape + == np.zeros((1, 32, 32, 3)).shape + ) + assert test_result == TEST_DETECT_RESULT diff --git a/frigate/test/test_onvif_probe.py b/frigate/test/test_onvif_probe.py new file mode 100644 index 0000000..aee7cc1 --- /dev/null +++ b/frigate/test/test_onvif_probe.py @@ -0,0 +1,124 @@ +import unittest +from unittest.mock import AsyncMock, MagicMock, patch + +from zeep.exceptions import Fault, TransportError +from zeep.transports import AsyncTransport + +from frigate.api.camera import _build_digest_transport, _connect_onvif_camera + + +def _make_camera(update_side_effect=None): + """Build a mock ONVIFCamera whose update_xaddrs can raise or succeed.""" + camera = MagicMock() + camera.update_xaddrs = AsyncMock(side_effect=update_side_effect) + return camera + + +class TestConnectOnvifCamera(unittest.IsolatedAsyncioTestCase): + async def test_password_digest_succeeds_first(self): + # Cameras that accept PasswordDigest authenticate on the first attempt + # and should never trigger the PasswordText fallback. + camera = _make_camera() + + with patch("frigate.api.camera.ONVIFCamera", return_value=camera) as mock_cls: + result = await _connect_onvif_camera( + "cam.local", 80, "user", "pass", None, "basic" + ) + + self.assertIs(result, camera) + mock_cls.assert_called_once() + self.assertTrue(mock_cls.call_args.kwargs["encrypt"]) + + async def test_falls_back_to_password_text(self): + # A PasswordDigest rejection should retry once with PasswordText. + camera_digest = _make_camera(update_side_effect=Fault("token rejected")) + camera_text = _make_camera() + + with patch( + "frigate.api.camera.ONVIFCamera", + side_effect=[camera_digest, camera_text], + ) as mock_cls: + result = await _connect_onvif_camera( + "cam.local", 80, "user", "pass", None, "basic" + ) + + self.assertIs(result, camera_text) + self.assertEqual(mock_cls.call_count, 2) + self.assertTrue(mock_cls.call_args_list[0].kwargs["encrypt"]) + self.assertFalse(mock_cls.call_args_list[1].kwargs["encrypt"]) + + async def test_both_encodings_fail_raises_first_fault(self): + # When both encodings fault, the original (PasswordDigest) fault is + # surfaced so the caller's existing Fault handler reports it. + first_fault = Fault("digest rejected") + camera_digest = _make_camera(update_side_effect=first_fault) + camera_text = _make_camera(update_side_effect=Fault("text rejected")) + + with patch( + "frigate.api.camera.ONVIFCamera", + side_effect=[camera_digest, camera_text], + ) as mock_cls: + with self.assertRaises(Fault) as ctx: + await _connect_onvif_camera( + "cam.local", 80, "user", "pass", None, "basic" + ) + + self.assertIs(ctx.exception, first_fault) + self.assertEqual(mock_cls.call_count, 2) + + async def test_transport_error_is_not_retried(self): + # Connection-level errors (timeout, refused, unreachable) should + # propagate immediately without doubling latency on a second encoding. + camera = _make_camera(update_side_effect=TransportError("unreachable")) + + with patch("frigate.api.camera.ONVIFCamera", side_effect=[camera]) as mock_cls: + with self.assertRaises(TransportError): + await _connect_onvif_camera( + "cam.local", 80, "user", "pass", None, "basic" + ) + + mock_cls.assert_called_once() + + async def test_digest_auth_replaces_service_transports(self): + # auth_type "digest" wires an HTTP digest transport onto each service, + # independently of the WS-Security encoding. + camera = _make_camera() + + with ( + patch("frigate.api.camera.ONVIFCamera", return_value=camera), + patch( + "frigate.api.camera._build_digest_transport", + return_value="TRANSPORT", + ) as mock_transport, + ): + result = await _connect_onvif_camera( + "cam.local", 80, "user", "pass", None, "digest" + ) + + self.assertIs(result, camera) + mock_transport.assert_called_once_with("user", "pass") + self.assertEqual(camera.devicemgmt.zeep_client.transport, "TRANSPORT") + self.assertEqual(camera.media.zeep_client.transport, "TRANSPORT") + self.assertEqual(camera.ptz.zeep_client.transport, "TRANSPORT") + + async def test_basic_auth_does_not_replace_transports(self): + # Without digest auth, no transport override is built. + camera = _make_camera() + + with ( + patch("frigate.api.camera.ONVIFCamera", return_value=camera), + patch("frigate.api.camera._build_digest_transport") as mock_transport, + ): + await _connect_onvif_camera("cam.local", 80, "user", "pass", None, "basic") + + mock_transport.assert_not_called() + + +class TestBuildDigestTransport(unittest.TestCase): + def test_returns_async_transport(self): + transport = _build_digest_transport("user", "pass") + self.assertIsInstance(transport, AsyncTransport) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_output_ws_auth.py b/frigate/test/test_output_ws_auth.py new file mode 100644 index 0000000..ea4834e --- /dev/null +++ b/frigate/test/test_output_ws_auth.py @@ -0,0 +1,57 @@ +"""Tests for JSMPEG websocket authorization.""" + +import unittest +from types import SimpleNamespace + +from frigate.config import FrigateConfig +from frigate.output.ws_auth import ws_has_camera_access + + +class TestWsHasCameraAccess(unittest.TestCase): + def setUp(self): + self.config = FrigateConfig( + mqtt={"host": "mqtt"}, + auth={"roles": {"limited_user": ["front_door"]}}, + cameras={ + "front_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + "back_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]} + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + }, + ) + + def _make_ws(self, role: str): + return SimpleNamespace(environ={"HTTP_REMOTE_ROLE": role}) + + def test_restricted_role_only_gets_allowed_camera(self): + ws = self._make_ws("limited_user") + self.assertTrue(ws_has_camera_access(ws, "front_door", self.config)) + self.assertFalse(ws_has_camera_access(ws, "back_door", self.config)) + + def test_unrestricted_role_can_access_any_camera(self): + ws = self._make_ws("viewer") + self.assertTrue(ws_has_camera_access(ws, "front_door", self.config)) + self.assertTrue(ws_has_camera_access(ws, "back_door", self.config)) + + def test_birdseye_requires_unrestricted_access(self): + self.assertTrue( + ws_has_camera_access(self._make_ws("admin"), "birdseye", self.config) + ) + self.assertTrue( + ws_has_camera_access(self._make_ws("viewer"), "birdseye", self.config) + ) + self.assertFalse( + ws_has_camera_access(self._make_ws("limited_user"), "birdseye", self.config) + ) diff --git a/frigate/test/test_preview_loader.py b/frigate/test/test_preview_loader.py new file mode 100644 index 0000000..e2062fc --- /dev/null +++ b/frigate/test/test_preview_loader.py @@ -0,0 +1,80 @@ +import os +import shutil +import unittest + +from frigate.output.preview import ( + PREVIEW_CACHE_DIR, + PREVIEW_FRAME_TYPE, + get_most_recent_preview_frame, +) + + +class TestPreviewLoader(unittest.TestCase): + def setUp(self): + if os.path.exists(PREVIEW_CACHE_DIR): + shutil.rmtree(PREVIEW_CACHE_DIR) + os.makedirs(PREVIEW_CACHE_DIR) + + def tearDown(self): + if os.path.exists(PREVIEW_CACHE_DIR): + shutil.rmtree(PREVIEW_CACHE_DIR) + + def test_get_most_recent_preview_frame_missing(self): + self.assertIsNone(get_most_recent_preview_frame("test_camera")) + + def test_get_most_recent_preview_frame_exists(self): + camera = "test_camera" + # create dummy preview files + for ts in ["1000.0", "2000.0", "1500.0"]: + with open( + os.path.join( + PREVIEW_CACHE_DIR, f"preview_{camera}-{ts}.{PREVIEW_FRAME_TYPE}" + ), + "w", + ) as f: + f.write(f"test_{ts}") + + expected_path = os.path.join( + PREVIEW_CACHE_DIR, f"preview_{camera}-2000.0.{PREVIEW_FRAME_TYPE}" + ) + self.assertEqual(get_most_recent_preview_frame(camera), expected_path) + + def test_get_most_recent_preview_frame_before(self): + camera = "test_camera" + # create dummy preview files + for ts in ["1000.0", "2000.0"]: + with open( + os.path.join( + PREVIEW_CACHE_DIR, f"preview_{camera}-{ts}.{PREVIEW_FRAME_TYPE}" + ), + "w", + ) as f: + f.write(f"test_{ts}") + + # Test finding frame before or at 1500 + expected_path = os.path.join( + PREVIEW_CACHE_DIR, f"preview_{camera}-1000.0.{PREVIEW_FRAME_TYPE}" + ) + self.assertEqual( + get_most_recent_preview_frame(camera, before=1500.0), expected_path + ) + + # Test finding frame before or at 999 + self.assertIsNone(get_most_recent_preview_frame(camera, before=999.0)) + + def test_get_most_recent_preview_frame_other_camera(self): + camera = "test_camera" + other_camera = "other_camera" + with open( + os.path.join( + PREVIEW_CACHE_DIR, f"preview_{other_camera}-3000.0.{PREVIEW_FRAME_TYPE}" + ), + "w", + ) as f: + f.write("test") + + self.assertIsNone(get_most_recent_preview_frame(camera)) + + def test_get_most_recent_preview_frame_no_directory(self): + shutil.rmtree(PREVIEW_CACHE_DIR) + self.assertIsNone(get_most_recent_preview_frame("test_camera")) diff --git a/frigate/test/test_profiles.py b/frigate/test/test_profiles.py new file mode 100644 index 0000000..59dc077 --- /dev/null +++ b/frigate/test/test_profiles.py @@ -0,0 +1,952 @@ +"""Tests for the profiles system.""" + +import copy +import json +import os +import unittest +from unittest.mock import MagicMock, patch + +from frigate.config import FrigateConfig +from frigate.config.camera.profile import CameraProfileConfig +from frigate.config.profile import ProfileDefinitionConfig +from frigate.config.profile_manager import PERSISTENCE_FILE, ProfileManager +from frigate.const import MODEL_CACHE_DIR + + +class TestCameraProfileConfig(unittest.TestCase): + """Test the CameraProfileConfig Pydantic model.""" + + def test_empty_profile(self): + """All sections default to None.""" + profile = CameraProfileConfig() + assert profile.detect is None + assert profile.motion is None + assert profile.objects is None + assert profile.review is None + assert profile.notifications is None + + def test_partial_detect(self): + """Profile with only detect.enabled set.""" + profile = CameraProfileConfig(detect={"enabled": False}) + assert profile.detect is not None + assert profile.detect.enabled is False + dumped = profile.detect.model_dump(exclude_unset=True) + assert dumped == {"enabled": False} + + def test_partial_notifications(self): + """Profile with only notifications.enabled set.""" + profile = CameraProfileConfig(notifications={"enabled": True}) + assert profile.notifications is not None + assert profile.notifications.enabled is True + dumped = profile.notifications.model_dump(exclude_unset=True) + assert dumped == {"enabled": True} + + def test_partial_objects(self): + """Profile with objects.track set.""" + profile = CameraProfileConfig(objects={"track": ["car", "package"]}) + assert profile.objects is not None + assert profile.objects.track == ["car", "package"] + + def test_partial_review(self): + """Profile with nested review.alerts.labels.""" + profile = CameraProfileConfig(review={"alerts": {"labels": ["person", "car"]}}) + assert profile.review is not None + assert profile.review.alerts.labels == ["person", "car"] + + def test_enabled_field(self): + """Profile with enabled set to False.""" + profile = CameraProfileConfig(enabled=False) + assert profile.enabled is False + dumped = profile.model_dump(exclude_unset=True) + assert dumped == {"enabled": False} + + def test_enabled_field_true(self): + """Profile with enabled set to True.""" + profile = CameraProfileConfig(enabled=True) + assert profile.enabled is True + + def test_enabled_default_none(self): + """Enabled defaults to None when not set.""" + profile = CameraProfileConfig() + assert profile.enabled is None + + def test_zones_field(self): + """Profile with zones override.""" + profile = CameraProfileConfig( + zones={ + "driveway": { + "coordinates": "0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9", + "objects": ["car"], + } + } + ) + assert profile.zones is not None + assert "driveway" in profile.zones + + def test_zones_default_none(self): + """Zones defaults to None when not set.""" + profile = CameraProfileConfig() + assert profile.zones is None + + def test_none_sections_not_in_dump(self): + """Sections left as None should not appear in exclude_unset dump.""" + profile = CameraProfileConfig(detect={"enabled": False}) + dumped = profile.model_dump(exclude_unset=True) + assert "detect" in dumped + assert "motion" not in dumped + assert "objects" not in dumped + + def test_invalid_field_value_rejected(self): + """Invalid field values are caught by Pydantic.""" + from pydantic import ValidationError + + with self.assertRaises(ValidationError): + CameraProfileConfig(detect={"fps": "not_a_number"}) + + def test_invalid_section_key_rejected(self): + """Unknown section keys are rejected (extra=forbid from FrigateBaseModel).""" + from pydantic import ValidationError + + with self.assertRaises(ValidationError): + CameraProfileConfig(ffmpeg={"inputs": []}) + + def test_invalid_nested_field_rejected(self): + """Invalid nested field values are caught.""" + from pydantic import ValidationError + + with self.assertRaises(ValidationError): + CameraProfileConfig(review={"alerts": {"labels": "not_a_list"}}) + + def test_invalid_profile_in_camera_config(self): + """Invalid profile section in full config is caught at parse time.""" + from pydantic import ValidationError + + config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": { + "armed": {"friendly_name": "Armed"}, + }, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "profiles": { + "armed": { + "detect": {"fps": "invalid"}, + }, + }, + }, + }, + } + with self.assertRaises(ValidationError): + FrigateConfig(**config_data) + + def test_undefined_profile_reference_rejected(self): + """Camera referencing a profile not defined in top-level profiles is rejected.""" + from pydantic import ValidationError + + config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": { + "armed": {"friendly_name": "Armed"}, + }, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "profiles": { + "nonexistent": { + "detect": {"enabled": False}, + }, + }, + }, + }, + } + with self.assertRaises(ValidationError): + FrigateConfig(**config_data) + + def test_profile_zone_without_base_rejected(self): + """Profile defining a zone not present on the base camera is rejected.""" + from pydantic import ValidationError + + config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": { + "armed": {"friendly_name": "Armed"}, + }, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "zones": { + "front_yard": {"coordinates": "0,0,100,0,100,100,0,100"}, + }, + "profiles": { + "armed": { + "zones": { + "phantom": { + "coordinates": "0,0,50,0,50,50,0,50", + }, + }, + }, + }, + }, + }, + } + with self.assertRaises(ValidationError) as ctx: + FrigateConfig(**config_data) + self.assertIn("phantom", str(ctx.exception)) + + def test_profile_motion_mask_without_base_rejected(self): + """Profile defining a motion mask not present on the base camera is rejected.""" + from pydantic import ValidationError + + config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": { + "armed": {"friendly_name": "Armed"}, + }, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "motion": { + "mask": { + "base_mask": { + "coordinates": "0,0,100,0,100,100,0,100", + }, + }, + }, + "profiles": { + "armed": { + "motion": { + "mask": { + "phantom_mask": { + "coordinates": "0,0,50,0,50,50,0,50", + }, + }, + }, + }, + }, + }, + }, + } + with self.assertRaises(ValidationError) as ctx: + FrigateConfig(**config_data) + self.assertIn("phantom_mask", str(ctx.exception)) + + def test_profile_overrides_matching_base_accepted(self): + """Profile overrides that reference existing base zones/masks parse cleanly.""" + config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": { + "armed": {"friendly_name": "Armed"}, + }, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "zones": { + "front_yard": {"coordinates": "0,0,100,0,100,100,0,100"}, + }, + "motion": { + "mask": { + "tree": { + "coordinates": "0,0,100,0,100,100,0,100", + }, + }, + }, + "profiles": { + "armed": { + "zones": { + "front_yard": { + "coordinates": "0,0,50,0,50,50,0,50", + "inertia": 5, + }, + }, + "motion": { + "mask": { + "tree": { + "coordinates": "0,0,75,0,75,75,0,75", + }, + }, + }, + }, + }, + }, + }, + } + config = FrigateConfig(**config_data) + assert "armed" in config.cameras["front"].profiles + + +class TestProfileInConfig(unittest.TestCase): + """Test that profiles parse correctly in FrigateConfig.""" + + def setUp(self): + self.base_config = { + "mqtt": {"host": "mqtt"}, + "profiles": { + "armed": {"friendly_name": "Armed"}, + "disarmed": {"friendly_name": "Disarmed"}, + }, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "profiles": { + "armed": { + "notifications": {"enabled": True}, + "objects": {"track": ["person", "car", "package"]}, + }, + "disarmed": { + "notifications": {"enabled": False}, + "objects": {"track": ["package"]}, + }, + }, + }, + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.2:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "profiles": { + "armed": { + "detect": {"enabled": True}, + }, + }, + }, + }, + } + + if not os.path.exists(MODEL_CACHE_DIR) and not os.path.islink(MODEL_CACHE_DIR): + os.makedirs(MODEL_CACHE_DIR) + + def test_profiles_parse(self): + """Profiles are parsed into Dict[str, CameraProfileConfig].""" + config = FrigateConfig(**self.base_config) + front = config.cameras["front"] + assert "armed" in front.profiles + assert "disarmed" in front.profiles + assert isinstance(front.profiles["armed"], CameraProfileConfig) + + def test_profile_sections_parsed(self): + """Profile sections are properly typed.""" + config = FrigateConfig(**self.base_config) + armed = config.cameras["front"].profiles["armed"] + assert armed.notifications is not None + assert armed.notifications.enabled is True + assert armed.objects is not None + assert armed.objects.track == ["person", "car", "package"] + assert armed.detect is None # not set in this profile + + def test_camera_without_profiles(self): + """Camera with no profiles has empty dict.""" + config_data = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + }, + } + config = FrigateConfig(**config_data) + assert config.cameras["front"].profiles == {} + + +class TestProfileManager(unittest.TestCase): + """Test ProfileManager activation, deactivation, and switching.""" + + def setUp(self): + self.config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": { + "armed": {"friendly_name": "Armed"}, + "disarmed": {"friendly_name": "Disarmed"}, + }, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "notifications": {"enabled": False}, + "objects": {"track": ["person"]}, + "profiles": { + "armed": { + "notifications": {"enabled": True}, + "objects": {"track": ["person", "car", "package"]}, + }, + "disarmed": { + "notifications": {"enabled": False}, + "objects": {"track": ["package"]}, + }, + }, + }, + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.2:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "profiles": { + "armed": { + "notifications": {"enabled": True}, + }, + }, + }, + }, + } + + if not os.path.exists(MODEL_CACHE_DIR) and not os.path.islink(MODEL_CACHE_DIR): + os.makedirs(MODEL_CACHE_DIR) + + self.config = FrigateConfig(**self.config_data) + self.mock_updater = MagicMock() + self.manager = ProfileManager(self.config, self.mock_updater) + + def test_get_available_profiles(self): + """Available profiles come from top-level profile definitions.""" + profiles = self.manager.get_available_profiles() + assert len(profiles) == 2 + names = [p["name"] for p in profiles] + assert "armed" in names + assert "disarmed" in names + # Verify friendly_name is included + armed = next(p for p in profiles if p["name"] == "armed") + assert armed["friendly_name"] == "Armed" + + def test_activate_invalid_profile(self): + """Activating non-existent profile returns error.""" + err = self.manager.activate_profile("nonexistent") + assert err is not None + assert "not defined" in err + + @patch.object(ProfileManager, "_persist_active_profile") + def test_activate_profile(self, mock_persist): + """Activating a profile applies overrides.""" + err = self.manager.activate_profile("armed") + assert err is None + assert self.config.active_profile == "armed" + + # Front camera should have armed overrides + front = self.config.cameras["front"] + assert front.notifications.enabled is True + assert front.objects.track == ["person", "car", "package"] + + # Back camera should have armed overrides + back = self.config.cameras["back"] + assert back.notifications.enabled is True + + @patch.object(ProfileManager, "_persist_active_profile") + def test_deactivate_profile(self, mock_persist): + """Deactivating a profile restores base config.""" + # Activate first + self.manager.activate_profile("armed") + assert self.config.cameras["front"].notifications.enabled is True + + # Deactivate + err = self.manager.activate_profile(None) + assert err is None + assert self.config.active_profile is None + + # Should be back to base + front = self.config.cameras["front"] + assert front.notifications.enabled is False + assert front.objects.track == ["person"] + + @patch.object(ProfileManager, "_persist_active_profile") + def test_switch_profiles(self, mock_persist): + """Switching from one profile to another works.""" + self.manager.activate_profile("armed") + assert self.config.cameras["front"].objects.track == [ + "person", + "car", + "package", + ] + + self.manager.activate_profile("disarmed") + assert self.config.active_profile == "disarmed" + assert self.config.cameras["front"].objects.track == ["package"] + assert self.config.cameras["front"].notifications.enabled is False + + @patch.object(ProfileManager, "_persist_active_profile") + def test_unaffected_camera(self, mock_persist): + """Camera without the activated profile is unaffected.""" + back_base_notifications = self.config.cameras["back"].notifications.enabled + + self.manager.activate_profile("disarmed") + + # Back camera has no "disarmed" profile, should be unchanged + assert ( + self.config.cameras["back"].notifications.enabled == back_base_notifications + ) + + @patch.object(ProfileManager, "_persist_active_profile") + def test_activate_profile_disables_camera(self, mock_persist): + """Profile with enabled=false disables the camera.""" + self.config.profiles["away"] = ProfileDefinitionConfig(friendly_name="Away") + self.config.cameras["front"].profiles["away"] = CameraProfileConfig( + enabled=False + ) + self.manager = ProfileManager(self.config, self.mock_updater) + + assert self.config.cameras["front"].enabled is True + err = self.manager.activate_profile("away") + assert err is None + assert self.config.cameras["front"].enabled is False + + @patch.object(ProfileManager, "_persist_active_profile") + def test_deactivate_restores_enabled(self, mock_persist): + """Deactivating a profile restores the camera's base enabled state.""" + self.config.profiles["away"] = ProfileDefinitionConfig(friendly_name="Away") + self.config.cameras["front"].profiles["away"] = CameraProfileConfig( + enabled=False + ) + self.manager = ProfileManager(self.config, self.mock_updater) + + self.manager.activate_profile("away") + assert self.config.cameras["front"].enabled is False + + self.manager.activate_profile(None) + assert self.config.cameras["front"].enabled is True + + @patch.object(ProfileManager, "_persist_active_profile") + def test_activate_profile_adds_zone(self, mock_persist): + """Profile with zones adds/overrides zones on camera.""" + from frigate.config.camera.zone import ZoneConfig + + self.config.profiles["away"] = ProfileDefinitionConfig(friendly_name="Away") + self.config.cameras["front"].profiles["away"] = CameraProfileConfig( + zones={ + "driveway": ZoneConfig( + coordinates="0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9", + objects=["car"], + ) + } + ) + self.manager = ProfileManager(self.config, self.mock_updater) + + assert "driveway" not in self.config.cameras["front"].zones + + err = self.manager.activate_profile("away") + assert err is None + assert "driveway" in self.config.cameras["front"].zones + + @patch.object(ProfileManager, "_persist_active_profile") + def test_deactivate_restores_zones(self, mock_persist): + """Deactivating a profile restores base zones.""" + from frigate.config.camera.zone import ZoneConfig + + self.config.profiles["away"] = ProfileDefinitionConfig(friendly_name="Away") + self.config.cameras["front"].profiles["away"] = CameraProfileConfig( + zones={ + "driveway": ZoneConfig( + coordinates="0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9", + objects=["car"], + ) + } + ) + self.manager = ProfileManager(self.config, self.mock_updater) + + self.manager.activate_profile("away") + assert "driveway" in self.config.cameras["front"].zones + + self.manager.activate_profile(None) + assert "driveway" not in self.config.cameras["front"].zones + + @patch.object(ProfileManager, "_persist_active_profile") + def test_zones_zmq_published(self, mock_persist): + """ZMQ update is published for zones change.""" + from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateTopic, + ) + from frigate.config.camera.zone import ZoneConfig + + self.config.profiles["away"] = ProfileDefinitionConfig(friendly_name="Away") + self.config.cameras["front"].profiles["away"] = CameraProfileConfig( + zones={ + "driveway": ZoneConfig( + coordinates="0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9", + objects=["car"], + ) + } + ) + self.manager = ProfileManager(self.config, self.mock_updater) + self.mock_updater.reset_mock() + + self.manager.activate_profile("away") + + zones_calls = [ + call + for call in self.mock_updater.publish_update.call_args_list + if call[0][0] + == CameraConfigUpdateTopic(CameraConfigUpdateEnum.zones, "front") + ] + assert len(zones_calls) == 1 + + @patch.object(ProfileManager, "_persist_active_profile") + def test_enabled_zmq_published(self, mock_persist): + """ZMQ update is published for enabled state change.""" + from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateTopic, + ) + + self.config.profiles["away"] = ProfileDefinitionConfig(friendly_name="Away") + self.config.cameras["front"].profiles["away"] = CameraProfileConfig( + enabled=False + ) + self.manager = ProfileManager(self.config, self.mock_updater) + self.mock_updater.reset_mock() + + self.manager.activate_profile("away") + + # Find the enabled update call + enabled_calls = [ + call + for call in self.mock_updater.publish_update.call_args_list + if call[0][0] + == CameraConfigUpdateTopic(CameraConfigUpdateEnum.enabled, "front") + ] + assert len(enabled_calls) == 1 + assert enabled_calls[0][0][1] is False + + @patch.object(ProfileManager, "_persist_active_profile") + def test_zmq_updates_published(self, mock_persist): + """ZMQ updates are published when a profile is activated.""" + self.manager.activate_profile("armed") + assert self.mock_updater.publish_update.called + + def test_get_profile_info(self): + """Profile info returns correct structure with friendly names.""" + with patch.object( + ProfileManager, + "_load_persisted_data", + return_value={"active": None, "last_activated": {}}, + ): + info = self.manager.get_profile_info() + assert "profiles" in info + assert "active_profile" in info + assert "last_activated" in info + assert info["active_profile"] is None + assert info["last_activated"] == {} + names = [p["name"] for p in info["profiles"]] + assert "armed" in names + assert "disarmed" in names + + @patch.object(ProfileManager, "_persist_active_profile") + def test_base_configs_for_api_unchanged_after_activation(self, mock_persist): + """API base configs reflect pre-profile values after activation.""" + base_track = self.config.cameras["front"].objects.track[:] + assert base_track == ["person"] + + self.manager.activate_profile("armed") + + # In-memory config has the profile-merged values + assert self.config.cameras["front"].objects.track == [ + "person", + "car", + "package", + ] + + # But the API base configs still return the original base values + api_base = self.manager.get_base_configs_for_api("front") + assert "objects" in api_base + assert api_base["objects"]["track"] == ["person"] + + def test_base_configs_for_api_are_json_serializable(self): + """API base configs are JSON-serializable (mode='json').""" + import json + + api_base = self.manager.get_base_configs_for_api("front") + # Should not raise + json.dumps(api_base) + + @patch.object(ProfileManager, "_persist_active_profile") + def test_activate_profile_clears_dispatcher_runtime_state(self, mock_persist): + """User-initiated activation drops runtime overrides (steady-state rule).""" + dispatcher = MagicMock() + manager = ProfileManager(self.config, self.mock_updater, dispatcher) + manager.activate_profile("armed") + dispatcher.clear_runtime_state.assert_called_once_with() + + @patch.object(ProfileManager, "_persist_active_profile") + def test_deactivate_profile_clears_dispatcher_runtime_state(self, mock_persist): + """Deactivating a profile also drops runtime overrides.""" + dispatcher = MagicMock() + manager = ProfileManager(self.config, self.mock_updater, dispatcher) + manager.activate_profile("armed") + dispatcher.clear_runtime_state.reset_mock() + + manager.activate_profile(None) + dispatcher.clear_runtime_state.assert_called_once_with() + + @patch.object(ProfileManager, "_persist_active_profile") + def test_profile_change_republishes_switch_states(self, mock_persist): + """Profile changes republish MQTT switch states so HA stays in sync. + + Regression: activating/deactivating a profile updated the in-memory + config (and Frigate's behavior) but left the retained MQTT state + topics stale, so external integrations like Home Assistant kept + showing the pre-profile toggle position. + """ + config_data = copy.deepcopy(self.config_data) + config_data["cameras"]["front"]["profiles"]["disarmed"]["review"] = { + "alerts": {"enabled": False}, + } + config = FrigateConfig(**config_data) + dispatcher = MagicMock() + manager = ProfileManager(config, self.mock_updater, dispatcher) + + # Activating disarmed turns alerts off -> MQTT state must follow + manager.activate_profile("disarmed") + dispatcher.publish.assert_any_call( + "front/review_alerts/state", "OFF", retain=True + ) + + # Deactivating restores the base (alerts on) -> MQTT state must follow + dispatcher.publish.reset_mock() + manager.activate_profile(None) + dispatcher.publish.assert_any_call( + "front/review_alerts/state", "ON", retain=True + ) + + @patch.object(ProfileManager, "_persist_active_profile") + def test_startup_replay_does_not_clear_runtime_state(self, mock_persist): + """Startup callers pass clear_runtime_overrides=False to preserve state.""" + dispatcher = MagicMock() + manager = ProfileManager(self.config, self.mock_updater, dispatcher) + manager.activate_profile("armed", clear_runtime_overrides=False) + dispatcher.clear_runtime_state.assert_not_called() + + @patch.object(ProfileManager, "_persist_active_profile") + def test_update_config_clears_when_active_profile_reapplies(self, mock_persist): + """After /api/config/set, an active-profile re-application drops state.""" + dispatcher = MagicMock() + manager = ProfileManager(self.config, self.mock_updater, dispatcher) + manager.activate_profile("armed") + dispatcher.clear_runtime_state.reset_mock() + + new_config = FrigateConfig(**self.config_data) + manager.update_config(new_config) + dispatcher.clear_runtime_state.assert_called_once_with() + + @patch.object(ProfileManager, "_persist_active_profile") + def test_update_config_does_not_clear_when_no_active_profile(self, mock_persist): + """Plain /api/config/set without a profile doesn't trigger the broad clear.""" + dispatcher = MagicMock() + manager = ProfileManager(self.config, self.mock_updater, dispatcher) + # No activate_profile call — config.active_profile is None + new_config = FrigateConfig(**self.config_data) + manager.update_config(new_config) + dispatcher.clear_runtime_state.assert_not_called() + + +class TestProfilePersistence(unittest.TestCase): + """Test profile persistence to disk.""" + + def test_persist_and_load(self): + """Active profile name can be persisted and loaded via JSON.""" + data = {"active": "armed", "last_activated": {"armed": 1700000000.0}} + with patch.object( + ProfileManager, + "_load_persisted_data", + return_value=data, + ): + result = ProfileManager.load_persisted_profile() + assert result == "armed" + + def test_load_empty_file(self): + """Empty persistence file returns None.""" + with patch.object(type(PERSISTENCE_FILE), "exists", return_value=True): + with patch.object(type(PERSISTENCE_FILE), "read_text", return_value=""): + result = ProfileManager.load_persisted_profile() + assert result is None + + def test_load_missing_file(self): + """Missing persistence file returns None.""" + with patch.object(type(PERSISTENCE_FILE), "exists", return_value=False): + result = ProfileManager.load_persisted_profile() + assert result is None + + def test_load_persisted_data_valid_json(self): + """Valid JSON file is loaded correctly.""" + data = {"active": "home", "last_activated": {"home": 1700000000.0}} + with patch.object(type(PERSISTENCE_FILE), "exists", return_value=True): + with patch.object( + type(PERSISTENCE_FILE), + "read_text", + return_value=json.dumps(data), + ): + result = ProfileManager._load_persisted_data() + assert result == data + + def test_load_persisted_data_invalid_json(self): + """Invalid JSON returns default structure.""" + with patch.object(type(PERSISTENCE_FILE), "exists", return_value=True): + with patch.object( + type(PERSISTENCE_FILE), "read_text", return_value="not json" + ): + result = ProfileManager._load_persisted_data() + assert result == {"active": None, "last_activated": {}} + + def test_load_persisted_data_missing_file(self): + """Missing file returns default structure.""" + with patch.object(type(PERSISTENCE_FILE), "exists", return_value=False): + result = ProfileManager._load_persisted_data() + assert result == {"active": None, "last_activated": {}} + + def test_persist_records_timestamp(self): + """Persisting a profile records the activation timestamp.""" + config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": {"armed": {"friendly_name": "Armed"}}, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "profiles": {"armed": {"detect": {"enabled": True}}}, + }, + }, + } + if not os.path.exists(MODEL_CACHE_DIR) and not os.path.islink(MODEL_CACHE_DIR): + os.makedirs(MODEL_CACHE_DIR) + config = FrigateConfig(**config_data) + manager = ProfileManager(config, MagicMock()) + + written_data = {} + + def mock_write(_self, content): + written_data.update(json.loads(content)) + + with patch.object( + ProfileManager, + "_load_persisted_data", + return_value={"active": None, "last_activated": {}}, + ): + with patch.object(type(PERSISTENCE_FILE), "write_text", mock_write): + manager._persist_active_profile("armed") + + assert written_data["active"] == "armed" + assert "armed" in written_data["last_activated"] + assert isinstance(written_data["last_activated"]["armed"], float) + + def test_persist_deactivate_keeps_timestamps(self): + """Deactivating sets active to None but preserves last_activated.""" + existing = { + "active": "armed", + "last_activated": {"armed": 1700000000.0}, + } + written_data = {} + + def mock_write(_self, content): + written_data.update(json.loads(content)) + + config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": {"armed": {"friendly_name": "Armed"}}, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "profiles": {"armed": {"detect": {"enabled": True}}}, + }, + }, + } + if not os.path.exists(MODEL_CACHE_DIR) and not os.path.islink(MODEL_CACHE_DIR): + os.makedirs(MODEL_CACHE_DIR) + config = FrigateConfig(**config_data) + manager = ProfileManager(config, MagicMock()) + + with patch.object( + ProfileManager, "_load_persisted_data", return_value=existing + ): + with patch.object(type(PERSISTENCE_FILE), "write_text", mock_write): + manager._persist_active_profile(None) + + assert written_data["active"] is None + assert written_data["last_activated"]["armed"] == 1700000000.0 + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_proxy_auth.py b/frigate/test/test_proxy_auth.py new file mode 100644 index 0000000..e4d2c9c --- /dev/null +++ b/frigate/test/test_proxy_auth.py @@ -0,0 +1,130 @@ +import unittest + +from frigate.api.auth import resolve_role +from frigate.config import HeaderMappingConfig, ProxyConfig +from frigate.config.env import FRIGATE_ENV_VARS + + +class TestProxyRoleResolution(unittest.TestCase): + def setUp(self): + self.proxy_config = ProxyConfig( + auth_secret=None, + default_role="viewer", + separator="|", + header_map=HeaderMappingConfig( + user="x-remote-user", + role="x-remote-role", + role_map={ + "admin": ["group_admin"], + "viewer": ["group_viewer"], + }, + ), + ) + self.config_roles = list(["admin", "viewer"]) + + def test_role_map_single_group_match(self): + headers = {"x-remote-role": "group_admin"} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, "admin") + + def test_role_map_multiple_groups(self): + headers = {"x-remote-role": "group_admin|group_viewer"} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, "admin") + + def test_role_map_or_matching(self): + config = self.proxy_config + config.header_map.role_map = { + "admin": ["group_admin", "group_privileged"], + } + + # OR semantics: a single matching group should map to the role + headers = {"x-remote-role": "group_admin"} + role = resolve_role(headers, config, self.config_roles) + self.assertEqual(role, "admin") + + headers = {"x-remote-role": "group_admin|group_privileged"} + role = resolve_role(headers, config, self.config_roles) + self.assertEqual(role, "admin") + + def test_direct_role_header_with_separator(self): + config = self.proxy_config + config.header_map.role_map = None # disable role_map + headers = {"x-remote-role": "admin|viewer"} + role = resolve_role(headers, config, self.config_roles) + self.assertEqual(role, "admin") + + def test_invalid_role_header(self): + config = self.proxy_config + config.header_map.role_map = None + headers = {"x-remote-role": "notarole"} + role = resolve_role(headers, config, self.config_roles) + self.assertEqual(role, config.default_role) + + def test_missing_role_header(self): + headers = {} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, self.proxy_config.default_role) + + def test_empty_role_header(self): + headers = {"x-remote-role": ""} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, self.proxy_config.default_role) + + def test_whitespace_groups(self): + headers = {"x-remote-role": " | group_admin | "} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, "admin") + + def test_mixed_valid_and_invalid_groups(self): + headers = {"x-remote-role": "bogus|group_viewer"} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, "viewer") + + def test_case_insensitive_role_direct(self): + config = self.proxy_config + config.header_map.role_map = None + headers = {"x-remote-role": "AdMiN"} + role = resolve_role(headers, config, self.config_roles) + self.assertEqual(role, "admin") + + def test_role_map_no_match_falls_back(self): + headers = {"x-remote-role": "group_unknown"} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, self.proxy_config.default_role) + + +class TestProxyAuthSecretEnvString(unittest.TestCase): + def setUp(self): + self._original_env_vars = dict(FRIGATE_ENV_VARS) + + def tearDown(self): + FRIGATE_ENV_VARS.clear() + FRIGATE_ENV_VARS.update(self._original_env_vars) + + def test_auth_secret_env_substitution(self): + """auth_secret resolves FRIGATE_ env vars via EnvString.""" + FRIGATE_ENV_VARS["FRIGATE_PROXY_SECRET"] = "my_secret_value" + config = ProxyConfig(auth_secret="{FRIGATE_PROXY_SECRET}") + self.assertEqual(config.auth_secret, "my_secret_value") + + def test_auth_secret_env_embedded_in_string(self): + """auth_secret resolves env vars embedded in a larger string.""" + FRIGATE_ENV_VARS["FRIGATE_SECRET_PART"] = "abc123" + config = ProxyConfig(auth_secret="prefix-{FRIGATE_SECRET_PART}-suffix") + self.assertEqual(config.auth_secret, "prefix-abc123-suffix") + + def test_auth_secret_plain_string(self): + """auth_secret accepts a plain string without substitution.""" + config = ProxyConfig(auth_secret="literal_secret") + self.assertEqual(config.auth_secret, "literal_secret") + + def test_auth_secret_none(self): + """auth_secret defaults to None.""" + config = ProxyConfig() + self.assertIsNone(config.auth_secret) + + def test_auth_secret_unknown_var_raises(self): + """auth_secret raises KeyError for unknown env var references.""" + with self.assertRaises(Exception): + ProxyConfig(auth_secret="{FRIGATE_NONEXISTENT_VAR}") diff --git a/frigate/test/test_record_retention.py b/frigate/test/test_record_retention.py new file mode 100644 index 0000000..b826c3a --- /dev/null +++ b/frigate/test/test_record_retention.py @@ -0,0 +1,33 @@ +import unittest + +from frigate.config import RetainModeEnum +from frigate.record.maintainer import SegmentInfo + + +class TestRecordRetention(unittest.TestCase): + def test_motion_should_keep_motion_not_object(self): + segment_info = SegmentInfo( + motion_count=1, active_object_count=0, region_count=0, average_dBFS=0 + ) + assert not segment_info.should_discard_segment(RetainModeEnum.motion) + assert segment_info.should_discard_segment(RetainModeEnum.active_objects) + + def test_object_should_keep_object_when_motion(self): + segment_info = SegmentInfo( + motion_count=0, active_object_count=1, region_count=0, average_dBFS=0 + ) + assert not segment_info.should_discard_segment(RetainModeEnum.motion) + assert not segment_info.should_discard_segment(RetainModeEnum.active_objects) + + def test_all_should_keep_all(self): + segment_info = SegmentInfo( + motion_count=0, active_object_count=0, region_count=0, average_dBFS=0 + ) + assert not segment_info.should_discard_segment(RetainModeEnum.all) + + def test_should_keep_audio_in_motion_mode(self): + segment_info = SegmentInfo( + motion_count=0, active_object_count=0, region_count=0, average_dBFS=1 + ) + assert not segment_info.should_discard_segment(RetainModeEnum.motion) + assert segment_info.should_discard_segment(RetainModeEnum.active_objects) diff --git a/frigate/test/test_reduce_boxes.py b/frigate/test/test_reduce_boxes.py new file mode 100644 index 0000000..5ac913d --- /dev/null +++ b/frigate/test/test_reduce_boxes.py @@ -0,0 +1,26 @@ +from unittest import TestCase, main + +from frigate.util.object import box_overlaps, reduce_boxes + + +class TestBoxOverlaps(TestCase): + def test_overlap(self): + assert box_overlaps((100, 100, 200, 200), (50, 50, 150, 150)) + + def test_overlap_2(self): + assert box_overlaps((50, 50, 150, 150), (100, 100, 200, 200)) + + def test_no_overlap(self): + assert not box_overlaps((100, 100, 200, 200), (250, 250, 350, 350)) + + +class TestReduceBoxes(TestCase): + def test_cluster(self): + clusters = reduce_boxes( + [(144, 290, 221, 459), (225, 178, 426, 341), (343, 105, 584, 250)] + ) + assert len(clusters) == 2 + + +if __name__ == "__main__": + main(verbosity=2) diff --git a/frigate/test/test_runtime_state.py b/frigate/test/test_runtime_state.py new file mode 100644 index 0000000..6143184 --- /dev/null +++ b/frigate/test/test_runtime_state.py @@ -0,0 +1,136 @@ +"""Tests for RuntimeStatePersistence.""" + +import json +import os +import tempfile +import unittest +from unittest.mock import patch + +from frigate.comms.runtime_state import RuntimeStatePersistence + + +class TestRuntimeStatePersistence(unittest.TestCase): + """Unit tests for the JSON-backed runtime state store.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.mkdtemp() + self.config_path = os.path.join(self.tmp_dir, "config.yml") + # Touch a placeholder config.yml so find_config_file returns a real path + with open(self.config_path, "w") as f: + f.write("") + self._patcher = patch( + "frigate.comms.runtime_state.find_config_file", + return_value=self.config_path, + ) + self._patcher.start() + self.store = RuntimeStatePersistence() + + def tearDown(self) -> None: + self._patcher.stop() + for name in os.listdir(self.tmp_dir): + os.remove(os.path.join(self.tmp_dir, name)) + os.rmdir(self.tmp_dir) + + def test_load_returns_empty_when_file_missing(self) -> None: + self.assertEqual(self.store.load(), {}) + + def test_set_then_load_round_trip(self) -> None: + self.store.set("front_door", "detect", False) + self.store.set("front_door", "recordings", True) + self.store.set("back_yard", "audio", False) + + result = self.store.load() + self.assertEqual( + result, + { + "front_door": {"detect": False, "recordings": True}, + "back_yard": {"audio": False}, + }, + ) + + def test_set_with_untracked_topic_is_noop(self) -> None: + self.store.set("front_door", "ptz_autotracker", True) + self.assertEqual(self.store.load(), {}) + # File should not even be created if no tracked entries were written + runtime_path = os.path.join(self.tmp_dir, ".runtime_state.json") + self.assertFalse(os.path.exists(runtime_path)) + + def test_set_overwrites_previous_value(self) -> None: + self.store.set("front_door", "detect", True) + self.store.set("front_door", "detect", False) + self.assertEqual(self.store.load(), {"front_door": {"detect": False}}) + + def test_load_returns_empty_when_file_corrupt(self) -> None: + runtime_path = os.path.join(self.tmp_dir, ".runtime_state.json") + with open(runtime_path, "w") as f: + f.write("{not valid json") + self.assertEqual(self.store.load(), {}) + + def test_load_handles_unexpected_top_level_shape(self) -> None: + runtime_path = os.path.join(self.tmp_dir, ".runtime_state.json") + with open(runtime_path, "w") as f: + json.dump(["unexpected", "list"], f) + self.assertEqual(self.store.load(), {}) + + def test_clear_for_yaml_keys_removes_matching_entries(self) -> None: + self.store.set("front_door", "detect", False) + self.store.set("front_door", "recordings", False) + self.store.set("back_yard", "audio", False) + + self.store.clear_for_yaml_keys( + [ + "cameras.front_door.detect.enabled", + "cameras.back_yard.audio.enabled", + ] + ) + + self.assertEqual( + self.store.load(), + {"front_door": {"recordings": False}}, + ) + + def test_clear_for_yaml_keys_collapses_empty_camera_dict(self) -> None: + self.store.set("front_door", "detect", False) + self.store.clear_for_yaml_keys(["cameras.front_door.detect.enabled"]) + self.assertEqual(self.store.load(), {}) + + def test_clear_for_yaml_keys_ignores_unrelated_keys(self) -> None: + self.store.set("front_door", "detect", False) + self.store.clear_for_yaml_keys( + [ + "ui.theme", + "go2rtc.streams.x", + "cameras.front_door.ffmpeg.inputs", + "not_cameras.front_door.detect.enabled", + ] + ) + self.assertEqual(self.store.load(), {"front_door": {"detect": False}}) + + def test_clear_for_yaml_keys_handles_empty_iterable(self) -> None: + self.store.set("front_door", "detect", False) + self.store.clear_for_yaml_keys([]) + self.assertEqual(self.store.load(), {"front_door": {"detect": False}}) + + def test_camera_level_enabled_uses_top_level_yaml_key(self) -> None: + """`enabled` topic maps to the camera-level `cameras..enabled` key.""" + self.store.set("front_door", "enabled", False) + self.store.clear_for_yaml_keys(["cameras.front_door.enabled"]) + self.assertEqual(self.store.load(), {}) + + def test_clear_all_wipes_every_entry(self) -> None: + self.store.set("front_door", "detect", False) + self.store.set("front_door", "recordings", True) + self.store.set("back_yard", "audio", False) + + self.store.clear_all() + + self.assertEqual(self.store.load(), {}) + + def test_clear_all_is_safe_when_file_missing(self) -> None: + # No prior set() calls — file does not exist + self.store.clear_all() + self.assertEqual(self.store.load(), {}) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_shared_memory_frame_manager.py b/frigate/test/test_shared_memory_frame_manager.py new file mode 100644 index 0000000..63c96f7 --- /dev/null +++ b/frigate/test/test_shared_memory_frame_manager.py @@ -0,0 +1,156 @@ +"""Tests for SharedMemoryFrameManager cache invalidation. + +Covers the case where a SHM segment is unlinked and recreated at a +different size across a camera add/remove cycle while a long-lived +in-process cache (e.g. TrackedObjectProcessor) still holds a ref to +the old, smaller segment. +""" + +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +import numpy as np + +from frigate.util.image import SharedMemoryFrameManager + + +def _fake_shm(size: int) -> SimpleNamespace: + """A minimal stand-in for UntrackedSharedMemory with .size and .buf.""" + return SimpleNamespace(size=size, buf=bytearray(size), close=lambda: None) + + +class TestSharedMemoryFrameManagerGet(unittest.TestCase): + def test_get_reopens_when_cached_segment_is_smaller_than_shape(self) -> None: + """A cached ref to an older smaller segment must be dropped and the + current (correctly sized) segment reopened. Without this, np.ndarray + would raise "buffer is too small for requested array" when the + in-memory cache pointed at an old SHM after a same-name resize.""" + manager = SharedMemoryFrameManager() + + small = _fake_shm(size=100) + current = _fake_shm(size=2_500) + manager.shm_store["cam_frame0"] = small + + with patch("frigate.util.image.UntrackedSharedMemory", return_value=current): + arr = manager.get("cam_frame0", (50, 50)) + + self.assertIsNotNone(arr) + self.assertEqual(arr.shape, (50, 50)) + self.assertIs(manager.shm_store["cam_frame0"], current) + + def test_get_reopens_when_cached_segment_is_larger_than_shape(self) -> None: + """Symmetric to the smaller-cache case: when detect resolution drops, + the SHM is unlinked and recreated at a smaller size. A cached ref to + the old, larger segment still satisfies any size check but points at + an orphaned inode whose stale bytes get reinterpreted at the new + shape — producing miscolored, distorted YUV frames downstream. Drop + the cache so we reopen by name and bind to the current segment.""" + manager = SharedMemoryFrameManager() + + old_large = _fake_shm(size=10_000) + current = _fake_shm(size=2_500) + manager.shm_store["cam_frame0"] = old_large + + with patch("frigate.util.image.UntrackedSharedMemory", return_value=current): + arr = manager.get("cam_frame0", (50, 50)) + + self.assertIsNotNone(arr) + self.assertEqual(arr.shape, (50, 50)) + self.assertIs(manager.shm_store["cam_frame0"], current) + + def test_get_keeps_cached_segment_when_size_matches(self) -> None: + """Don't pay the reopen cost when the cached ref is the right size.""" + manager = SharedMemoryFrameManager() + + cached = _fake_shm(size=2_500) + manager.shm_store["cam_frame0"] = cached + + with patch("frigate.util.image.UntrackedSharedMemory") as untracked_shm_cls: + arr = manager.get("cam_frame0", (50, 50)) + untracked_shm_cls.assert_not_called() + + self.assertIsNotNone(arr) + self.assertIs(manager.shm_store["cam_frame0"], cached) + + def test_get_opens_fresh_when_no_cache_entry(self) -> None: + manager = SharedMemoryFrameManager() + fresh = _fake_shm(size=2_500) + + with patch("frigate.util.image.UntrackedSharedMemory", return_value=fresh): + arr = manager.get("cam_frame0", (50, 50)) + + self.assertIsNotNone(arr) + self.assertIs(manager.shm_store["cam_frame0"], fresh) + + def test_get_returns_none_when_segment_missing(self) -> None: + manager = SharedMemoryFrameManager() + + with patch( + "frigate.util.image.UntrackedSharedMemory", + side_effect=FileNotFoundError, + ): + arr = manager.get("cam_frame0", (50, 50)) + + self.assertIsNone(arr) + + def test_get_returns_none_when_reopened_segment_is_still_too_small(self) -> None: + """Race during a same-name SHM recreate: cache is stale, we reopen + by name, but the maintainer hasn't allocated the new segment yet — + the reopened ref is also too small. Skip the frame (return None) + rather than crash on np.ndarray.""" + manager = SharedMemoryFrameManager() + + small_cached = _fake_shm(size=100) + still_small_after_reopen = _fake_shm(size=100) + manager.shm_store["cam_frame0"] = small_cached + + with patch( + "frigate.util.image.UntrackedSharedMemory", + return_value=still_small_after_reopen, + ): + arr = manager.get("cam_frame0", (50, 50)) + + self.assertIsNone(arr) + # Don't cache the too-small reopened ref — next call will re-open + # once the maintainer has finished recreating the segment. + self.assertNotIn("cam_frame0", manager.shm_store) + + def test_get_handles_n_dimensional_shape(self) -> None: + """np.prod must be used (not raw multiplication) for tuple shapes.""" + manager = SharedMemoryFrameManager() + # YUV-shaped frame: (height * 3/2, width) for 1920x1080 = 3,110,400 + big_enough = _fake_shm(size=3_110_400) + manager.shm_store["cam_frame0"] = big_enough + + with patch("frigate.util.image.UntrackedSharedMemory") as untracked_shm_cls: + arr = manager.get("cam_frame0", (1620, 1920)) + untracked_shm_cls.assert_not_called() + + self.assertIsNotNone(arr) + self.assertEqual(arr.shape, (1620, 1920)) + + +class TestSharedMemoryFrameManagerGetRecreatesLargerSegment(unittest.TestCase): + """End-to-end-style: simulates the full unlink-and-recreate cycle.""" + + def test_segment_grows_then_get_succeeds(self) -> None: + manager = SharedMemoryFrameManager() + + # Phase 1: existing camera at 320x240 YUV — 320 * 240 * 1.5 = 115_200 + small = _fake_shm(size=115_200) + manager.shm_store["cam_frame0"] = small + arr_small = np.ndarray((360, 320), dtype=np.uint8, buffer=small.buf) + self.assertEqual(arr_small.shape, (360, 320)) + + # Phase 2: restart at 1920x1080 — new SHM segment, larger size. + large = _fake_shm(size=3_110_400) + with patch("frigate.util.image.UntrackedSharedMemory", return_value=large): + arr_large = manager.get("cam_frame0", (1620, 1920)) + + self.assertIsNotNone(arr_large) + self.assertEqual(arr_large.shape, (1620, 1920)) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_stationary_classifier.py b/frigate/test/test_stationary_classifier.py new file mode 100644 index 0000000..bd5e2c4 --- /dev/null +++ b/frigate/test/test_stationary_classifier.py @@ -0,0 +1,39 @@ +"""Tests for stationary object classification thresholds.""" + +import unittest + +from frigate.track.stationary_classifier import ( + DEFAULT_OBJECT_THRESHOLDS, + DYNAMIC_OBJECT_THRESHOLDS, + NON_STATIONARY_OBJECT_THRESHOLDS, + STATIONARY_OBJECT_THRESHOLDS, + StationaryThresholds, + get_stationary_threshold, +) + + +class TestStationaryThresholds(unittest.TestCase): + def test_known_labels_return_expected_singletons(self) -> None: + self.assertIs(get_stationary_threshold("package"), STATIONARY_OBJECT_THRESHOLDS) + self.assertIs(get_stationary_threshold("car"), DYNAMIC_OBJECT_THRESHOLDS) + self.assertIs( + get_stationary_threshold("license_plate"), + NON_STATIONARY_OBJECT_THRESHOLDS, + ) + + def test_unknown_label_returns_shared_default(self) -> None: + # an unknown label must reuse the shared default instance, not allocate + # a fresh one on every call (this runs per object per frame) + first = get_stationary_threshold("person") + second = get_stationary_threshold("dog") + self.assertIs(first, DEFAULT_OBJECT_THRESHOLDS) + self.assertIs(second, DEFAULT_OBJECT_THRESHOLDS) + + def test_default_matches_a_fresh_instance(self) -> None: + # the shared default must be value-equivalent to the previous + # per-call StationaryThresholds() + self.assertEqual(DEFAULT_OBJECT_THRESHOLDS, StationaryThresholds()) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_storage.py b/frigate/test/test_storage.py new file mode 100644 index 0000000..4ae5715 --- /dev/null +++ b/frigate/test/test_storage.py @@ -0,0 +1,316 @@ +import datetime +import logging +import os +import tempfile +import unittest +from unittest.mock import MagicMock + +from peewee import DoesNotExist +from peewee_migrate import Router +from playhouse.sqlite_ext import SqliteExtDatabase +from playhouse.sqliteq import SqliteQueueDatabase + +from frigate.config import FrigateConfig +from frigate.models import Event, Recordings +from frigate.storage import StorageMaintainer +from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS + + +class TestHttp(unittest.TestCase): + def setUp(self): + # setup clean database for each test run + migrate_db = SqliteExtDatabase("test.db") + del logging.getLogger("peewee_migrate").handlers[:] + router = Router(migrate_db) + router.run() + migrate_db.close() + self.db = SqliteQueueDatabase(TEST_DB) + models = [Event, Recordings] + self.db.bind(models) + self.test_dir = tempfile.mkdtemp() + + 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, + }, + } + }, + } + self.double_cam_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_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + }, + }, + } + + def tearDown(self): + if not self.db.is_closed(): + self.db.close() + + try: + for file in TEST_DB_CLEANUPS: + os.remove(file) + except OSError: + pass + + def test_segment_calculations(self): + """Test that the segment calculations are correct.""" + config = FrigateConfig(**self.double_cam_config) + storage = StorageMaintainer(config, MagicMock()) + + time_keep = datetime.datetime.now().timestamp() + rec_fd_id = "1234567.frontdoor" + rec_bd_id = "1234568.backdoor" + _insert_mock_recording( + rec_fd_id, + os.path.join(self.test_dir, f"{rec_fd_id}.tmp"), + time_keep, + time_keep + 10, + camera="front_door", + seg_size=4, + seg_dur=10, + ) + _insert_mock_recording( + rec_bd_id, + os.path.join(self.test_dir, f"{rec_bd_id}.tmp"), + time_keep + 10, + time_keep + 20, + camera="back_door", + seg_size=8, + seg_dur=20, + ) + storage.calculate_camera_bandwidth() + assert storage.camera_storage_stats == { + "front_door": {"bandwidth": 1440, "needs_refresh": True}, + "back_door": {"bandwidth": 2880, "needs_refresh": True}, + } + + def test_segment_calculations_with_zero_segments(self): + """Ensure segment calculation does not fail when migrating from previous version.""" + config = FrigateConfig(**self.minimal_config) + storage = StorageMaintainer(config, MagicMock()) + + time_keep = datetime.datetime.now().timestamp() + rec_fd_id = "1234567.frontdoor" + _insert_mock_recording( + rec_fd_id, + os.path.join(self.test_dir, f"{rec_fd_id}.tmp"), + time_keep, + time_keep + 10, + camera="front_door", + seg_size=0, + seg_dur=10, + ) + storage.calculate_camera_bandwidth() + assert storage.camera_storage_stats == { + "front_door": {"bandwidth": 0, "needs_refresh": True}, + } + + def test_storage_cleanup(self): + """Ensure that all recordings are cleaned up when necessary.""" + config = FrigateConfig(**self.minimal_config) + storage = StorageMaintainer(config, MagicMock()) + + id = "123456.keep" + time_keep = datetime.datetime.now().timestamp() + _insert_mock_event( + id, + time_keep, + time_keep + 30, + True, + ) + rec_k_id = "1234567.keep" + rec_k2_id = "1234568.keep" + rec_k3_id = "1234569.keep" + _insert_mock_recording( + rec_k_id, + os.path.join(self.test_dir, f"{rec_k_id}.tmp"), + time_keep, + time_keep + 10, + ) + _insert_mock_recording( + rec_k2_id, + os.path.join(self.test_dir, f"{rec_k2_id}.tmp"), + time_keep + 10, + time_keep + 20, + ) + _insert_mock_recording( + rec_k3_id, + os.path.join(self.test_dir, f"{rec_k3_id}.tmp"), + time_keep + 20, + time_keep + 30, + ) + + id2 = "7890.delete" + time_delete = datetime.datetime.now().timestamp() - 360 + _insert_mock_event(id2, time_delete, time_delete + 30, False) + rec_d_id = "78901.delete" + rec_d2_id = "78902.delete" + rec_d3_id = "78903.delete" + _insert_mock_recording( + rec_d_id, + os.path.join(self.test_dir, f"{rec_d_id}.tmp"), + time_delete, + time_delete + 10, + ) + _insert_mock_recording( + rec_d2_id, + os.path.join(self.test_dir, f"{rec_d2_id}.tmp"), + time_delete + 10, + time_delete + 20, + ) + _insert_mock_recording( + rec_d3_id, + os.path.join(self.test_dir, f"{rec_d3_id}.tmp"), + time_delete + 20, + time_delete + 30, + ) + + storage.calculate_camera_bandwidth() + storage.reduce_storage_consumption() + with self.assertRaises(DoesNotExist): + assert Recordings.get(Recordings.id == rec_k_id) + assert Recordings.get(Recordings.id == rec_k2_id) + assert Recordings.get(Recordings.id == rec_k3_id) + Recordings.get(Recordings.id == rec_d_id) + Recordings.get(Recordings.id == rec_d2_id) + Recordings.get(Recordings.id == rec_d3_id) + + def test_storage_cleanup_keeps_retained(self): + """Ensure that all recordings are cleaned up when necessary.""" + config = FrigateConfig(**self.minimal_config) + storage = StorageMaintainer(config, MagicMock()) + + id = "123456.keep" + time_keep = datetime.datetime.now().timestamp() + _insert_mock_event( + id, + time_keep, + time_keep + 30, + True, + ) + rec_k_id = "1234567.keep" + rec_k2_id = "1234568.keep" + rec_k3_id = "1234569.keep" + _insert_mock_recording( + rec_k_id, + os.path.join(self.test_dir, f"{rec_k_id}.tmp"), + time_keep, + time_keep + 10, + ) + _insert_mock_recording( + rec_k2_id, + os.path.join(self.test_dir, f"{rec_k2_id}.tmp"), + time_keep + 10, + time_keep + 20, + ) + _insert_mock_recording( + rec_k3_id, + os.path.join(self.test_dir, f"{rec_k3_id}.tmp"), + time_keep + 20, + time_keep + 30, + ) + + time_delete = datetime.datetime.now().timestamp() - 7200 + for i in range(0, 59): + id = f"{123456 + i}.delete" + _insert_mock_recording( + id, + os.path.join(self.test_dir, f"{id}.tmp"), + time_delete, + time_delete + 600, + ) + + storage.calculate_camera_bandwidth() + storage.reduce_storage_consumption() + assert Recordings.get(Recordings.id == rec_k_id) + assert Recordings.get(Recordings.id == rec_k2_id) + assert Recordings.get(Recordings.id == rec_k3_id) + + +def _insert_mock_event( + id: str, + start: int, + end: int, + retain: bool, + camera: str = "front_door", + label: str = "Mock", +) -> Event: + """Inserts a basic event model with a given id.""" + return Event.insert( + id=id, + label=label, + camera=camera, + start_time=start, + end_time=end, + top_score=100, + false_positive=False, + zones=list(), + thumbnail="", + region=[], + box=[], + area=0, + has_clip=True, + has_snapshot=True, + retain_indefinitely=retain, + ).execute() + + +def _insert_mock_recording( + id: str, + file: str, + start: int, + end: int, + camera="front_door", + seg_size=8, + seg_dur=10, +) -> Event: + """Inserts a basic recording model with a given id.""" + # we must open the file so storage maintainer will delete it + with open(file, "w"): + pass + + return Recordings.insert( + id=id, + camera=camera, + path=file, + start_time=start, + end_time=end, + duration=seg_dur, + motion=True, + objects=True, + segment_size=seg_size, + ).execute() diff --git a/frigate/test/test_update_yaml.py b/frigate/test/test_update_yaml.py new file mode 100644 index 0000000..e9e160c --- /dev/null +++ b/frigate/test/test_update_yaml.py @@ -0,0 +1,183 @@ +"""Test in-place yaml config updates.""" + +import os +import tempfile +import unittest + +from ruamel.yaml import YAML + +from frigate.util.builtin import update_yaml_file_bulk + + +class TestUpdateYaml(unittest.TestCase): + def setUp(self) -> None: + self.yaml = YAML() + fd, self.config_path = tempfile.mkstemp(suffix=".yml") + os.close(fd) + + def tearDown(self) -> None: + os.unlink(self.config_path) + + def _write(self, text: str) -> None: + with open(self.config_path, "w") as f: + f.write(text) + + def _read(self) -> str: + with open(self.config_path) as f: + return f.read() + + def _load(self): + with open(self.config_path) as f: + return self.yaml.load(f) + + def test_delete_key(self): + """Deleting a key removes it and leaves valid yaml.""" + self._write( + "cameras:\n" + " cam1:\n" + " objects:\n" + " filters:\n" + " car:\n" + " mask: 0,0.45,0.245,0.45\n" + ) + update_yaml_file_bulk( + self.config_path, {"cameras.cam1.objects.filters.car.mask": ""} + ) + data = self._load() + assert "mask" not in data["cameras"]["cam1"]["objects"]["filters"]["car"] + + def test_delete_commented_key_emptying_map(self): + """Deleting the only key of a map whose key carries comments must not + emit unparseable yaml (orphaned comment tokens above a flow-style {}).""" + self._write( + "cameras:\n" + " cam1:\n" + " objects:\n" + " filters:\n" + " car:\n" + " # cars parked across the street\n" + " # second comment line\n" + " mask: 0,0.45,0.245,0.45\n" + " motion:\n" + " mask: 0,0.449,0.686,0.395\n" + ) + update_yaml_file_bulk( + self.config_path, {"cameras.cam1.objects.filters.car.mask": ""} + ) + # must re-parse cleanly + data = self._load() + assert "mask" not in data["cameras"]["cam1"]["objects"]["filters"]["car"] + assert data["cameras"]["cam1"]["motion"]["mask"] == "0,0.449,0.686,0.395" + # the orphaned comments must be gone from the file, not just parseable + content = self._read() + assert "cars parked across the street" not in content + assert "second comment line" not in content + + def test_delete_last_named_mask_emptying_map(self): + """The path the current UI actually sends: a named object mask deleted + down to an empty `mask` map, with a comment inside that map.""" + self._write( + "cameras:\n" + " cam1:\n" + " objects:\n" + " filters:\n" + " car:\n" + " mask:\n" + " # ignore the neighbor's driveway\n" + " driveway:\n" + " coordinates: 0,0.1,0.2,0.3\n" + ) + update_yaml_file_bulk( + self.config_path, + {"cameras.cam1.objects.filters.car.mask.driveway": ""}, + ) + data = self._load() + assert data["cameras"]["cam1"]["objects"]["filters"]["car"]["mask"] == {} + assert "ignore the neighbor's driveway" not in self._read() + + def test_delete_last_commented_list_item(self): + """Deleting the last element of a commented sequence must not emit + an orphaned comment above a flow-style [] at column 0.""" + self._write( + "cameras:\n" + " cam1:\n" + " motion:\n" + " mask:\n" + " # driveway motion mask\n" + " - 0,0.4,0.6,0.4\n" + ) + update_yaml_file_bulk(self.config_path, {"cameras.cam1.motion.mask.0": ""}) + data = self._load() + assert data["cameras"]["cam1"]["motion"]["mask"] == [] + assert "driveway motion mask" not in self._read() + + def test_delete_list_item_preserves_remaining(self): + """Deleting one element of a sequence keeps the others and stays valid.""" + self._write( + "cameras:\n" + " cam1:\n" + " motion:\n" + " mask:\n" + " - 0,0.4,0.6,0.4\n" + " - 0,0.1,0.2,0.3\n" + ) + update_yaml_file_bulk(self.config_path, {"cameras.cam1.motion.mask.0": ""}) + data = self._load() + assert data["cameras"]["cam1"]["motion"]["mask"] == ["0,0.1,0.2,0.3"] + + def test_delete_key_preserves_siblings(self): + """Deleting one key among several keeps the sibling entries and any + comments on keys preceding the deleted one.""" + self._write( + "cameras:\n" + " cam1:\n" + " objects:\n" + " filters:\n" + " car:\n" + " # mask drawn around the parked suv\n" + " mask: 0,0.45,0.245,0.45\n" + " threshold: 0.8\n" + ) + update_yaml_file_bulk( + self.config_path, {"cameras.cam1.objects.filters.car.threshold": ""} + ) + data = self._load() + car = data["cameras"]["cam1"]["objects"]["filters"]["car"] + assert "threshold" not in car + assert car["mask"] == "0,0.45,0.245,0.45" + assert "# mask drawn around the parked suv" in self._read() + + def test_delete_first_commented_key_keeps_map_valid(self): + """Deleting a commented key from a map that still has other keys + leaves the remaining entries intact and the file parseable.""" + self._write( + "cameras:\n" + " cam1:\n" + " objects:\n" + " filters:\n" + " car:\n" + " # comment on the deleted key\n" + " mask: 0,0.45,0.245,0.45\n" + " threshold: 0.8\n" + ) + update_yaml_file_bulk( + self.config_path, {"cameras.cam1.objects.filters.car.mask": ""} + ) + data = self._load() + car = data["cameras"]["cam1"]["objects"]["filters"]["car"] + assert "mask" not in car + assert car["threshold"] == 0.8 + + def test_update_value_preserves_comments(self): + """Updating a value keeps surrounding comments intact.""" + self._write( + "cameras:\n cam1:\n detect:\n # tuned for the pi\n fps: 4\n" + ) + update_yaml_file_bulk(self.config_path, {"cameras.cam1.detect.fps": 5}) + data = self._load() + assert data["cameras"]["cam1"]["detect"]["fps"] == 5 + assert "# tuned for the pi" in self._read() + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/frigate/test/test_video.py b/frigate/test/test_video.py new file mode 100644 index 0000000..d09a605 --- /dev/null +++ b/frigate/test/test_video.py @@ -0,0 +1,372 @@ +import unittest + +import cv2 +import numpy as np +from norfair.drawing.color import Palette +from norfair.drawing.drawer import Drawer + +from frigate.util.image import intersection, transliterate_to_latin +from frigate.util.object import ( + get_cluster_boundary, + get_cluster_candidates, + get_cluster_region, + get_region_from_grid, + reduce_detections, +) + + +def draw_box(frame, box, color=(255, 0, 0), thickness=2): + cv2.rectangle( + frame, + (box[0], box[1]), + (box[2], box[3]), + color, + thickness, + ) + + +def save_clusters_image(name, boxes, candidates, regions=[]): + canvas = np.zeros((1000, 2000, 3), np.uint8) + for cluster in candidates: + color = Palette.choose_color(np.random.rand()) + for b in cluster: + box = boxes[b] + draw_box(canvas, box, color, 2) + # bottom right + text_anchor = ( + box[2], + box[3], + ) + canvas = Drawer.text( + canvas, + str(b), + position=text_anchor, + size=None, + color=(255, 255, 255), + thickness=None, + ) + for r in regions: + draw_box(canvas, r, (0, 255, 0), 2) + cv2.imwrite( + f"debug/frames/{name}.jpg", + canvas, + ) + + +def save_cluster_boundary_image(name, boxes, bounding_boxes): + canvas = np.zeros((1000, 2000, 3), np.uint8) + color = Palette.choose_color(np.random.rand()) + for box in boxes: + draw_box(canvas, box, color, 2) + for bound in bounding_boxes: + draw_box(canvas, bound, (0, 255, 0), 2) + cv2.imwrite( + f"debug/frames/{name}.jpg", + canvas, + ) + + +class TestRegion(unittest.TestCase): + def setUp(self): + self.frame_shape = (1000, 2000) + self.min_region_size = 160 + + def test_cluster_candidates(self): + boxes = [(100, 100, 200, 200), (202, 150, 252, 200), (900, 900, 950, 950)] + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + # save_clusters_image("cluster_candidates", boxes, cluster_candidates) + + assert len(cluster_candidates) == 2 + + def test_cluster_candidates_partition_boxes(self): + # every box index must appear in exactly one cluster (no box used twice, + # none dropped) - the invariant the used-box tracking enforces + boxes = [ + (100, 100, 200, 200), + (202, 150, 252, 200), + (210, 160, 260, 210), + (900, 900, 950, 950), + (905, 905, 955, 955), + ] + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + assigned = [idx for cluster in cluster_candidates for idx in cluster] + self.assertEqual(sorted(assigned), list(range(len(boxes)))) + + def test_transliterate_to_latin(self): + self.assertEqual(transliterate_to_latin("frégate"), "fregate") + self.assertEqual(transliterate_to_latin("utilité"), "utilite") + self.assertEqual(transliterate_to_latin("imágé"), "image") + + def test_cluster_boundary(self): + boxes = [(100, 100, 200, 200), (215, 215, 325, 325)] + boundary_boxes = [ + get_cluster_boundary(box, self.min_region_size) for box in boxes + ] + + # save_cluster_boundary_image("bound", boxes, boundary_boxes) + assert len(boundary_boxes) == 2 + + def test_cluster_regions(self): + boxes = [(100, 100, 200, 200), (202, 150, 252, 200), (900, 900, 950, 950)] + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + # save_clusters_image("regions", boxes, cluster_candidates, regions) + assert len(regions) == 2 + + def test_box_too_small_for_cluster(self): + boxes = [(100, 100, 600, 600), (655, 100, 700, 145)] + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + save_clusters_image("too_small", boxes, cluster_candidates, regions) + + assert len(cluster_candidates) == 2 + assert len(regions) == 2 + + def test_redundant_clusters(self): + boxes = [(100, 100, 200, 200), (305, 305, 415, 415)] + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + # save_clusters_image("redundant", boxes, cluster_candidates, regions) + + assert len(cluster_candidates) == 2 + assert all([len(c) == 1 for c in cluster_candidates]) + assert len(regions) == 2 + + def test_combine_boxes(self): + boxes = [ + (480, 0, 540, 128), + (536, 0, 558, 99), + ] + + # boundary_boxes = [get_cluster_boundary(box) for box in boxes] + # save_cluster_boundary_image("combine_bound", boxes, boundary_boxes) + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + # save_clusters_image("combine", boxes, cluster_candidates, regions) + assert len(regions) == 1 + + def test_dont_combine_smaller_boxes(self): + boxes = [ + (460, 0, 561, 144), + (565, 0, 586, 71), + ] + + # boundary_boxes = [get_cluster_boundary(box) for box in boxes] + # save_cluster_boundary_image("combine_bound", boxes, boundary_boxes) + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + # save_clusters_image("combine", boxes, cluster_candidates, regions) + assert len(regions) == 2 + + def test_dont_combine_boxes(self): + boxes = [ + (460, 0, 532, 129), + (586, 0, 606, 46), + ] + + # boundary_boxes = [get_cluster_boundary(box) for box in boxes] + # save_cluster_boundary_image("dont_combine_bound", boxes, boundary_boxes) + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + # save_clusters_image("dont_combine", boxes, cluster_candidates, regions) + assert len(regions) == 2 + + +class TestObjectBoundingBoxes(unittest.TestCase): + def setUp(self) -> None: + pass + + def test_box_intersection(self): + box_a = [2012, 191, 2031, 205] + box_b = [887, 92, 985, 151] + box_c = [899, 128, 1080, 175] + + assert intersection(box_a, box_b) == None + assert intersection(box_b, box_c) == (899, 128, 985, 151) + + def test_overlapping_objects_reduced(self): + """Test that object not on edge of region is used when a higher scoring object at the edge of region is provided.""" + detections = [ + ( + "car", + 0.81, + (1209, 73, 1437, 163), + 20520, + 2.53333333, + (1150, 0, 1500, 200), + ), + ( + "car", + 0.88, + (1238, 73, 1401, 171), + 15974, + 1.663265306122449, + (1242, 0, 1602, 360), + ), + ] + frame_shape = (720, 2560) + consolidated_detections = reduce_detections(frame_shape, detections) + assert consolidated_detections == [ + ( + "car", + 0.81, + (1209, 73, 1437, 163), + 20520, + 2.53333333, + (1150, 0, 1500, 200), + ) + ] + + def test_non_overlapping_objects_not_reduced(self): + """Test that non overlapping objects are not reduced.""" + detections = [ + ( + "car", + 0.81, + (1209, 73, 1437, 163), + 20520, + 2.53333333, + (1150, 0, 1500, 200), + ), + ( + "car", + 0.83203125, + (1121, 55, 1214, 100), + 4185, + 2.066666666666667, + (922, 0, 1242, 320), + ), + ( + "car", + 0.85546875, + (1414, 97, 1571, 186), + 13973, + 1.7640449438202248, + (1248, 0, 1568, 320), + ), + ] + frame_shape = (720, 2560) + consolidated_detections = reduce_detections(frame_shape, detections) + assert len(consolidated_detections) == len(detections) + + def test_overlapping_different_size_objects_not_reduced(self): + """Test that overlapping objects that are significantly different in size are not reduced.""" + detections = [ + ( + "car", + 0.81, + (164, 279, 816, 719), + 286880, + 1.48, + (90, 0, 910, 820), + ), + ( + "car", + 0.83203125, + (248, 340, 328, 385), + 3600, + 1.777, + (0, 0, 460, 460), + ), + ] + frame_shape = (720, 2560) + consolidated_detections = reduce_detections(frame_shape, detections) + assert len(consolidated_detections) == len(detections) + + def test_vert_stacked_cars_not_reduced(self): + detections = [ + ("car", 0.8, (954, 312, 1247, 475), 498512, 1.48, (800, 200, 1400, 600)), + ("car", 0.85, (970, 380, 1273, 610), 698752, 1.56, (800, 200, 1400, 700)), + ] + frame_shape = (720, 1280) + consolidated_detections = reduce_detections(frame_shape, detections) + assert len(consolidated_detections) == len(detections) + + +class TestRegionGrid(unittest.TestCase): + def setUp(self) -> None: + pass + + def test_region_in_range(self): + """Test that region is kept at minimal size when within std dev.""" + frame_shape = (720, 1280) + box = [450, 450, 550, 550] + region_grid = [ + [], + [], + [], + [{}, {}, {}, {}, {}, {"sizes": [0.25], "mean": 0.26, "std_dev": 0.01}], + ] + + region = get_region_from_grid(frame_shape, box, 320, region_grid) + assert region[2] - region[0] == 320 + + def test_region_out_of_range(self): + """Test that region is upsized when outside of std dev.""" + frame_shape = (720, 1280) + box = [450, 450, 550, 550] + region_grid = [ + [], + [], + [], + [{}, {}, {}, {}, {}, {"sizes": [0.5], "mean": 0.5, "std_dev": 0.1}], + ] + + region = get_region_from_grid(frame_shape, box, 320, region_grid) + assert region[2] - region[0] > 320 diff --git a/frigate/test/test_webpush_camera_monitoring.py b/frigate/test/test_webpush_camera_monitoring.py new file mode 100644 index 0000000..fa9172a --- /dev/null +++ b/frigate/test/test_webpush_camera_monitoring.py @@ -0,0 +1,29 @@ +"""Tests for camera monitoring notification authorization.""" + +import unittest +from types import SimpleNamespace +from unittest.mock import MagicMock + +from frigate.comms.webpush import WebPushClient + + +class TestCameraMonitoringNotifications(unittest.TestCase): + def test_send_camera_monitoring_filters_by_camera_access(self): + client = WebPushClient.__new__(WebPushClient) + client.config = SimpleNamespace( + cameras={"front_door": SimpleNamespace(friendly_name=None)} + ) + client.web_pushers = {"allowed": [], "denied": []} + client.user_cameras = {"allowed": {"front_door"}, "denied": set()} + client.check_registrations = MagicMock() + client.cleanup_registrations = MagicMock() + client.send_push_notification = MagicMock() + + client.send_camera_monitoring( + {"camera": "front_door", "message": "Monitoring condition met"} + ) + + self.assertEqual(client.send_push_notification.call_count, 1) + self.assertEqual( + client.send_push_notification.call_args.kwargs["user"], "allowed" + ) diff --git a/frigate/test/test_webpush_suspend.py b/frigate/test/test_webpush_suspend.py new file mode 100644 index 0000000..1442bad --- /dev/null +++ b/frigate/test/test_webpush_suspend.py @@ -0,0 +1,64 @@ +"""Tests for notification suspension expiry handling.""" + +import datetime +import unittest +from unittest.mock import MagicMock + +from frigate.comms.webpush import WebPushClient + + +class TestSuspensionExpiry(unittest.TestCase): + def _make_client(self, suspended_cameras: dict[str, int]) -> WebPushClient: + client = WebPushClient.__new__(WebPushClient) + client.suspended_cameras = suspended_cameras + client.suspension_broadcaster = MagicMock() + return client + + def test_clears_and_broadcasts_expired_suspension(self): + now = datetime.datetime.now().timestamp() + client = self._make_client({"front_door": int(now - 60)}) + + client._clear_expired_suspensions() + + self.assertEqual(client.suspended_cameras["front_door"], 0) + client.suspension_broadcaster.assert_called_once_with( + "front_door/notifications/suspended", "0", True + ) + + def test_leaves_active_suspension_untouched(self): + now = datetime.datetime.now().timestamp() + suspend_until = int(now + 3600) + client = self._make_client({"front_door": suspend_until}) + + client._clear_expired_suspensions() + + self.assertEqual(client.suspended_cameras["front_door"], suspend_until) + client.suspension_broadcaster.assert_not_called() + + def test_ignores_unsuspended_camera(self): + client = self._make_client({"front_door": 0}) + + client._clear_expired_suspensions() + + self.assertEqual(client.suspended_cameras["front_door"], 0) + client.suspension_broadcaster.assert_not_called() + + def test_only_expired_cameras_are_cleared(self): + now = datetime.datetime.now().timestamp() + active_until = int(now + 3600) + client = self._make_client( + { + "expired": int(now - 5), + "active": active_until, + "idle": 0, + } + ) + + client._clear_expired_suspensions() + + self.assertEqual(client.suspended_cameras["expired"], 0) + self.assertEqual(client.suspended_cameras["active"], active_until) + self.assertEqual(client.suspended_cameras["idle"], 0) + client.suspension_broadcaster.assert_called_once_with( + "expired/notifications/suspended", "0", True + ) diff --git a/frigate/test/test_ws_auth.py b/frigate/test/test_ws_auth.py new file mode 100644 index 0000000..a9fc6e1 --- /dev/null +++ b/frigate/test/test_ws_auth.py @@ -0,0 +1,294 @@ +"""Tests for WebSocket authorization checks.""" + +import unittest + +from frigate.comms.ws import _check_ws_authorization +from frigate.const import INSERT_MANY_RECORDINGS, UPDATE_CAMERA_ACTIVITY + + +class TestCheckWsAuthorization(unittest.TestCase): + """Tests for the _check_ws_authorization pure function.""" + + DEFAULT_SEPARATOR = "," + + # admin/viewer are reserved and always map to all cameras (empty list); + # custom roles map to a specific set of cameras. + ROLES_CONFIG = { + "admin": [], + "viewer": [], + "yard": ["front_door", "backyard"], + "garage_only": ["garage"], + } + CAMERA_NAMES = {"front_door", "backyard", "garage"} + + # --- IPC topic blocking (unconditional, regardless of role) --- + + def test_ipc_topic_blocked_for_admin(self): + self.assertFalse( + _check_ws_authorization( + INSERT_MANY_RECORDINGS, "admin", self.DEFAULT_SEPARATOR + ) + ) + + def test_ipc_topic_blocked_for_viewer(self): + self.assertFalse( + _check_ws_authorization( + UPDATE_CAMERA_ACTIVITY, "viewer", self.DEFAULT_SEPARATOR + ) + ) + + def test_ipc_topic_blocked_when_no_role(self): + self.assertFalse( + _check_ws_authorization( + INSERT_MANY_RECORDINGS, None, self.DEFAULT_SEPARATOR + ) + ) + + # --- Viewer allowed topics --- + + def test_viewer_can_send_on_connect(self): + self.assertTrue( + _check_ws_authorization("onConnect", "viewer", self.DEFAULT_SEPARATOR) + ) + + def test_viewer_can_send_model_state(self): + self.assertTrue( + _check_ws_authorization("modelState", "viewer", self.DEFAULT_SEPARATOR) + ) + + def test_viewer_can_send_audio_transcription_state(self): + self.assertTrue( + _check_ws_authorization( + "audioTranscriptionState", "viewer", self.DEFAULT_SEPARATOR + ) + ) + + def test_viewer_can_send_birdseye_layout(self): + self.assertTrue( + _check_ws_authorization("birdseyeLayout", "viewer", self.DEFAULT_SEPARATOR) + ) + + def test_viewer_can_send_embeddings_reindex_progress(self): + self.assertTrue( + _check_ws_authorization( + "embeddingsReindexProgress", "viewer", self.DEFAULT_SEPARATOR + ) + ) + + # --- Viewer blocked from admin topics --- + + def test_viewer_blocked_from_restart(self): + self.assertFalse( + _check_ws_authorization("restart", "viewer", self.DEFAULT_SEPARATOR) + ) + + def test_viewer_blocked_from_camera_detect_set(self): + self.assertFalse( + _check_ws_authorization( + "front_door/detect/set", "viewer", self.DEFAULT_SEPARATOR + ) + ) + + def test_viewer_blocked_from_camera_ptz(self): + self.assertFalse( + _check_ws_authorization("front_door/ptz", "viewer", self.DEFAULT_SEPARATOR) + ) + + def test_viewer_blocked_from_global_notifications_set(self): + self.assertFalse( + _check_ws_authorization( + "notifications/set", "viewer", self.DEFAULT_SEPARATOR + ) + ) + + def test_viewer_blocked_from_camera_notifications_suspend(self): + self.assertFalse( + _check_ws_authorization( + "front_door/notifications/suspend", "viewer", self.DEFAULT_SEPARATOR + ) + ) + + def test_viewer_blocked_from_arbitrary_unknown_topic(self): + self.assertFalse( + _check_ws_authorization( + "some_random_topic", "viewer", self.DEFAULT_SEPARATOR + ) + ) + + # --- Admin access --- + + def test_admin_can_send_restart(self): + self.assertTrue( + _check_ws_authorization("restart", "admin", self.DEFAULT_SEPARATOR) + ) + + def test_admin_can_send_camera_detect_set(self): + self.assertTrue( + _check_ws_authorization( + "front_door/detect/set", "admin", self.DEFAULT_SEPARATOR + ) + ) + + def test_admin_can_send_camera_ptz(self): + self.assertTrue( + _check_ws_authorization("front_door/ptz", "admin", self.DEFAULT_SEPARATOR) + ) + + # --- Comma-separated roles --- + + def test_comma_separated_admin_viewer_grants_admin(self): + self.assertTrue( + _check_ws_authorization("restart", "admin,viewer", self.DEFAULT_SEPARATOR) + ) + + def test_comma_separated_viewer_admin_grants_admin(self): + self.assertTrue( + _check_ws_authorization("restart", "viewer,admin", self.DEFAULT_SEPARATOR) + ) + + def test_comma_separated_with_spaces(self): + self.assertTrue( + _check_ws_authorization("restart", "viewer, admin", self.DEFAULT_SEPARATOR) + ) + + # --- Custom separator --- + + def test_pipe_separator(self): + self.assertTrue(_check_ws_authorization("restart", "viewer|admin", "|")) + + def test_pipe_separator_no_admin(self): + self.assertFalse(_check_ws_authorization("restart", "viewer|editor", "|")) + + # --- No role header (fail-closed) --- + + def test_no_role_header_blocks_admin_topics(self): + self.assertFalse( + _check_ws_authorization("restart", None, self.DEFAULT_SEPARATOR) + ) + + def test_no_role_header_allows_viewer_topics(self): + self.assertTrue( + _check_ws_authorization("onConnect", None, self.DEFAULT_SEPARATOR) + ) + + # --- Camera-scoped PTZ access (non-admin with camera access) --- + + def test_viewer_can_ptz_camera_with_access(self): + # viewer maps to all cameras, so PTZ is allowed + self.assertTrue( + _check_ws_authorization( + "front_door/ptz", + "viewer", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_custom_role_can_ptz_assigned_camera(self): + self.assertTrue( + _check_ws_authorization( + "front_door/ptz", + "yard", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_custom_role_blocked_from_ptz_unassigned_camera(self): + self.assertFalse( + _check_ws_authorization( + "garage/ptz", + "yard", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_multiple_roles_union_camera_access_for_ptz(self): + # "yard" covers front_door/backyard, "garage_only" covers garage + self.assertTrue( + _check_ws_authorization( + "garage/ptz", + "yard,garage_only", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_unknown_role_blocked_from_ptz(self): + self.assertFalse( + _check_ws_authorization( + "front_door/ptz", + "nonexistent", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_no_role_header_treated_as_viewer_for_ptz(self): + # proxy-only / auth-disabled setups default to the viewer role + self.assertTrue( + _check_ws_authorization( + "front_door/ptz", + None, + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_camera_access_does_not_grant_set_commands(self): + # camera access enables PTZ only, not config-changing "set" commands + self.assertFalse( + _check_ws_authorization( + "front_door/detect/set", + "yard", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_ptz_autotracker_stays_admin_only(self): + # ptz_autotracker is a config toggle, not a live-view action + self.assertFalse( + _check_ws_authorization( + "front_door/ptz_autotracker/set", + "viewer", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_admin_can_ptz_any_camera_with_config(self): + self.assertTrue( + _check_ws_authorization( + "garage/ptz", + "admin", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_ipc_topic_still_blocked_with_camera_access(self): + # IPC topics are blocked unconditionally, even with camera access + self.assertFalse( + _check_ws_authorization( + UPDATE_CAMERA_ACTIVITY, + "viewer", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_ws_outbound_filter.py b/frigate/test/test_ws_outbound_filter.py new file mode 100644 index 0000000..ab1489d --- /dev/null +++ b/frigate/test/test_ws_outbound_filter.py @@ -0,0 +1,806 @@ +"""Tests for outbound WebSocket broadcast filtering.""" + +import json +import threading +import unittest +from types import SimpleNamespace +from typing import Any + +from frigate.comms.ws import ( + WebSocketClient, + _classify_outbound, + _collect_zone_names, + _extract_payload_camera, + _materialize_for_ws, + _ws_allowed_cameras, + _ws_is_unrestricted, +) +from frigate.config import FrigateConfig + + +def _build_config( + *, + extra_roles: dict[str, list[str]] | None = None, + extra_cameras: dict[str, dict[str, Any]] | None = None, + extra_zones: dict[str, dict[str, dict[str, Any]]] | None = None, +) -> FrigateConfig: + """Construct a FrigateConfig used by the outbound filter tests. + + The default fixture has three cameras: front_door, back_door, garage. + Restricted role "house_only" sees front_door + back_door but not garage. + """ + cameras: dict[str, dict[str, Any]] = { + "front_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.1:554/v", "roles": ["detect"]}], + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + "back_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.2:554/v", "roles": ["detect"]}], + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + "garage": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.3:554/v", "roles": ["detect"]}], + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + } + if extra_cameras: + cameras.update(extra_cameras) + if extra_zones: + for cam_name, zones in extra_zones.items(): + cameras[cam_name]["zones"] = zones + + roles = {"house_only": ["front_door", "back_door"]} + if extra_roles: + roles.update(extra_roles) + + return FrigateConfig( + mqtt={"host": "mqtt"}, + auth={"roles": roles}, + cameras=cameras, + ) + + +def _ws(role: str | None) -> Any: + """Build a fake ws4py-style websocket exposing ``environ``.""" + environ = {} if role is None else {"HTTP_REMOTE_ROLE": role} + return SimpleNamespace(environ=environ, terminated=False, sent=[]) + + +class TestClassifyOutbound(unittest.TestCase): + """The pure classifier — bucket every topic into a scope.""" + + def setUp(self): + self.config = _build_config( + extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}} + ) + self.all_cameras = set(self.config.cameras.keys()) + self.all_zones = _collect_zone_names(self.config) + + def _classify(self, topic: str) -> tuple[str, Any]: + return _classify_outbound(topic, self.all_cameras, self.all_zones) + + # --- Global allowlist --- + + def test_model_state_is_global(self): + self.assertEqual(self._classify("model_state"), ("global", None)) + + def test_profile_state_is_global(self): + self.assertEqual(self._classify("profile/state"), ("global", None)) + + def test_bare_notifications_state_is_global(self): + """The 2-segment ``notifications/state`` is global; the 3-segment + ``/notifications/state`` is camera-scoped (see below).""" + self.assertEqual(self._classify("notifications/state"), ("global", None)) + + def test_notification_test_is_global(self): + self.assertEqual(self._classify("notification_test"), ("global", None)) + + # --- Unrestricted-only --- + + def test_birdseye_layout_is_unrestricted_only(self): + self.assertEqual(self._classify("birdseye_layout"), ("unrestricted_only", None)) + + # --- Camera-prefixed --- + + def test_camera_state_topic_resolves_to_camera(self): + self.assertEqual( + self._classify("front_door/detect/state"), ("camera", "front_door") + ) + + def test_camera_motion_topic_resolves_to_camera(self): + self.assertEqual(self._classify("back_door/motion"), ("camera", "back_door")) + + def test_camera_per_notification_topic_resolves_to_camera(self): + self.assertEqual( + self._classify("front_door/notifications/state"), + ("camera", "front_door"), + ) + + def test_camera_label_counter_resolves_to_camera(self): + self.assertEqual(self._classify("front_door/person"), ("camera", "front_door")) + + def test_camera_object_mask_state_resolves_to_camera(self): + self.assertEqual( + self._classify("front_door/object_mask/zone_1/state"), + ("camera", "front_door"), + ) + + # --- Zone-prefixed --- + + def test_zone_aggregate_topic_is_unrestricted_only(self): + self.assertEqual(self._classify("driveway/person"), ("unrestricted_only", None)) + + def test_zone_all_topic_is_unrestricted_only(self): + self.assertEqual(self._classify("driveway/all"), ("unrestricted_only", None)) + + # --- Payload-camera --- + + def test_events_topic_marks_payload_camera_path(self): + self.assertEqual( + self._classify("events"), ("payload_camera", ("after", "camera")) + ) + + def test_reviews_topic_marks_payload_camera_path(self): + self.assertEqual( + self._classify("reviews"), ("payload_camera", ("after", "camera")) + ) + + def test_triggers_topic_marks_payload_camera_path(self): + self.assertEqual(self._classify("triggers"), ("payload_camera", ("camera",))) + + def test_tracked_object_update_marks_payload_camera_path(self): + self.assertEqual( + self._classify("tracked_object_update"), ("payload_camera", ("camera",)) + ) + + # --- Reshape --- + + def test_camera_activity_is_reshape_by_camera_key(self): + self.assertEqual( + self._classify("camera_activity"), ("reshape_by_camera_key", None) + ) + + def test_audio_detections_is_reshape_by_camera_key(self): + self.assertEqual( + self._classify("audio_detections"), ("reshape_by_camera_key", None) + ) + + def test_job_state_is_reshape_job_state(self): + self.assertEqual(self._classify("job_state"), ("reshape_job_state", None)) + + def test_stats_is_reshape_stats(self): + self.assertEqual(self._classify("stats"), ("reshape_stats", None)) + + # --- Fail-closed --- + + def test_unknown_topic_is_dropped(self): + self.assertEqual(self._classify("some_random_topic"), ("drop", None)) + + def test_unknown_camera_prefix_is_dropped(self): + self.assertEqual(self._classify("ghost_camera/detect/state"), ("drop", None)) + + +class TestCollectZoneNames(unittest.TestCase): + def test_zones_from_all_cameras(self): + config = _build_config( + extra_zones={ + "front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}, + "back_door": {"yard": {"coordinates": "0,0,1,0,1,1,0,1"}}, + } + ) + self.assertEqual(_collect_zone_names(config), {"driveway", "yard"}) + + def test_no_zones_returns_empty(self): + self.assertEqual(_collect_zone_names(_build_config()), set()) + + +class TestExtractPayloadCamera(unittest.TestCase): + def test_extract_from_dict_path(self): + payload = {"after": {"camera": "front_door"}} + self.assertEqual( + _extract_payload_camera(payload, ("after", "camera")), "front_door" + ) + + def test_extract_from_json_string(self): + payload = json.dumps({"after": {"camera": "front_door"}}) + self.assertEqual( + _extract_payload_camera(payload, ("after", "camera")), "front_door" + ) + + def test_extract_single_segment_path(self): + self.assertEqual( + _extract_payload_camera({"camera": "garage"}, ("camera",)), "garage" + ) + + def test_missing_key_returns_none(self): + self.assertIsNone(_extract_payload_camera({}, ("after", "camera"))) + + def test_malformed_json_returns_none(self): + self.assertIsNone(_extract_payload_camera("not-json", ("camera",))) + + def test_non_string_camera_returns_none(self): + self.assertIsNone(_extract_payload_camera({"camera": 42}, ("camera",))) + + +class TestWsRoleHelpers(unittest.TestCase): + def setUp(self): + self.config = _build_config() + + def test_admin_is_unrestricted(self): + self.assertTrue(_ws_is_unrestricted(_ws("admin"), self.config)) + + def test_viewer_is_unrestricted(self): + self.assertTrue(_ws_is_unrestricted(_ws("viewer"), self.config)) + + def test_restricted_role_is_not_unrestricted(self): + self.assertFalse(_ws_is_unrestricted(_ws("house_only"), self.config)) + + def test_missing_role_is_not_unrestricted(self): + self.assertFalse(_ws_is_unrestricted(_ws(None), self.config)) + + def test_unknown_role_is_not_unrestricted(self): + self.assertFalse(_ws_is_unrestricted(_ws("ghost"), self.config)) + + def test_admin_allowed_cameras_is_all(self): + self.assertEqual( + _ws_allowed_cameras(_ws("admin"), self.config), + {"front_door", "back_door", "garage"}, + ) + + def test_restricted_role_allowed_cameras_is_subset(self): + self.assertEqual( + _ws_allowed_cameras(_ws("house_only"), self.config), + {"front_door", "back_door"}, + ) + + def test_missing_role_allowed_cameras_is_empty(self): + self.assertEqual(_ws_allowed_cameras(_ws(None), self.config), set()) + + def test_multi_role_union_grants_widest(self): + self.assertEqual( + _ws_allowed_cameras(_ws("house_only,admin"), self.config), + {"front_door", "back_door", "garage"}, + ) + + +class TestMaterializeForWs(unittest.TestCase): + def setUp(self): + self.config = _build_config( + extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}} + ) + self.all_cameras = set(self.config.cameras.keys()) + self.all_zones = _collect_zone_names(self.config) + + def _materialize(self, ws: Any, topic: str, payload: Any) -> str | None: + scope = _classify_outbound(topic, self.all_cameras, self.all_zones) + from frigate.comms.ws import _parse_json_payload + + parsed = ( + _parse_json_payload(payload) + if scope[0] + in ( + "payload_camera", + "reshape_by_camera_key", + "reshape_job_state", + "reshape_stats", + ) + else None + ) + full = json.dumps({"topic": topic, "payload": payload}) + return _materialize_for_ws(ws, topic, full, scope, parsed, self.config) + + # --- Globals: every authenticated client sees them --- + + def test_globals_reach_admin(self): + self.assertIsNotNone(self._materialize(_ws("admin"), "model_state", "{}")) + + def test_globals_reach_restricted(self): + self.assertIsNotNone(self._materialize(_ws("house_only"), "model_state", "{}")) + + def test_globals_reach_no_role(self): + """A missing role header still gets globals (matches viewer-default + for inbound).""" + self.assertIsNotNone(self._materialize(_ws(None), "model_state", "{}")) + + # --- Unknown topic dropped for everyone --- + + def test_unknown_topic_dropped_for_admin(self): + self.assertIsNone(self._materialize(_ws("admin"), "rogue_topic", "{}")) + + # --- Non-global topics require a role (fail-closed) --- + + def test_no_role_blocked_from_camera_topic(self): + self.assertIsNone(self._materialize(_ws(None), "front_door/detect/state", "ON")) + + def test_no_role_blocked_from_events(self): + payload = json.dumps({"after": {"camera": "front_door"}}) + self.assertIsNone(self._materialize(_ws(None), "events", payload)) + + # --- Camera-prefixed --- + + def test_restricted_role_sees_allowed_camera(self): + self.assertIsNotNone( + self._materialize(_ws("house_only"), "front_door/detect/state", "ON") + ) + + def test_restricted_role_blocked_from_unallowed_camera(self): + self.assertIsNone( + self._materialize(_ws("house_only"), "garage/detect/state", "ON") + ) + + def test_admin_sees_all_camera_topics(self): + self.assertIsNotNone( + self._materialize(_ws("admin"), "garage/detect/state", "ON") + ) + + # --- Unrestricted-only (zones, birdseye_layout) --- + + def test_zone_aggregate_blocked_for_restricted(self): + self.assertIsNone(self._materialize(_ws("house_only"), "driveway/person", 3)) + + def test_zone_aggregate_visible_to_admin(self): + self.assertIsNotNone(self._materialize(_ws("admin"), "driveway/person", 3)) + + def test_birdseye_layout_blocked_for_restricted(self): + payload = json.dumps( + {"front_door": {"x": 0, "y": 0, "width": 100, "height": 100}} + ) + self.assertIsNone( + self._materialize(_ws("house_only"), "birdseye_layout", payload) + ) + + def test_birdseye_layout_visible_to_admin(self): + payload = json.dumps( + {"front_door": {"x": 0, "y": 0, "width": 100, "height": 100}} + ) + self.assertIsNotNone( + self._materialize(_ws("admin"), "birdseye_layout", payload) + ) + + # --- Payload-camera --- + + def test_events_filtered_by_payload_camera(self): + payload = json.dumps({"after": {"camera": "garage"}}) + self.assertIsNone(self._materialize(_ws("house_only"), "events", payload)) + + payload = json.dumps({"after": {"camera": "front_door"}}) + self.assertIsNotNone(self._materialize(_ws("house_only"), "events", payload)) + + def test_events_with_missing_camera_dropped(self): + payload = json.dumps({"after": {}}) + self.assertIsNone(self._materialize(_ws("house_only"), "events", payload)) + + def test_triggers_filtered_by_payload_camera(self): + payload = json.dumps({"name": "t1", "camera": "garage"}) + self.assertIsNone(self._materialize(_ws("house_only"), "triggers", payload)) + + # --- Reshape: dict keyed by camera --- + + def test_camera_activity_filtered_to_allowed_keys(self): + payload = json.dumps( + { + "front_door": {"objects": 1}, + "back_door": {"objects": 0}, + "garage": {"objects": 2}, + } + ) + message = self._materialize(_ws("house_only"), "camera_activity", payload) + self.assertIsNotNone(message) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertEqual(set(inner.keys()), {"front_door", "back_door"}) + self.assertNotIn("garage", inner) + + def test_camera_activity_unchanged_for_admin(self): + payload = json.dumps({"front_door": {}, "back_door": {}, "garage": {}}) + message = self._materialize(_ws("admin"), "camera_activity", payload) + envelope = json.loads(message) # type: ignore[arg-type] + self.assertEqual(envelope["payload"], payload) + + def test_camera_activity_with_no_allowed_returns_none(self): + payload = json.dumps({"garage": {"objects": 2}}) + self.assertIsNone( + self._materialize(_ws("house_only"), "camera_activity", payload) + ) + + def test_audio_detections_filtered_to_allowed_keys(self): + payload = json.dumps({"front_door": {"bark": {}}, "garage": {"speech": {}}}) + message = self._materialize(_ws("house_only"), "audio_detections", payload) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertEqual(set(inner.keys()), {"front_door"}) + + # --- Reshape: job_state --- + + def test_job_state_admin_sees_full_payload(self): + payload = json.dumps( + { + "motion_search": {"job_type": "motion_search", "camera": "garage"}, + "media_sync": {"job_type": "media_sync"}, + } + ) + message = self._materialize(_ws("admin"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + self.assertEqual(envelope["payload"], payload) + + def test_job_state_restricted_keeps_allowed_camera_jobs(self): + """Top-level camera field on a job entry: drop if not allowed.""" + payload = json.dumps( + { + "motion_search": {"job_type": "motion_search", "camera": "front_door"}, + "vlm_watch": {"job_type": "vlm_watch", "camera": "garage"}, + } + ) + message = self._materialize(_ws("house_only"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertIn("motion_search", inner) + self.assertNotIn("vlm_watch", inner) + + def test_job_state_export_results_jobs_filtered_per_recipient(self): + """The aggregated export broadcast nests per-camera sub-jobs under + ``results.jobs``. Restricted users must only see allowed entries.""" + payload = json.dumps( + { + "export": { + "job_type": "export", + "status": "running", + "results": { + "jobs": [ + {"job_type": "export", "camera": "front_door", "id": "a"}, + {"job_type": "export", "camera": "garage", "id": "b"}, + {"job_type": "export", "camera": "back_door", "id": "c"}, + ] + }, + } + } + ) + message = self._materialize(_ws("house_only"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertIn("export", inner) + kept_cameras = [j["camera"] for j in inner["export"]["results"]["jobs"]] + self.assertEqual(kept_cameras, ["front_door", "back_door"]) + # Sibling fields like ``status`` must survive reshaping. + self.assertEqual(inner["export"]["status"], "running") + + def test_job_state_export_entry_dropped_when_no_jobs_allowed(self): + payload = json.dumps( + { + "export": { + "job_type": "export", + "status": "running", + "results": { + "jobs": [ + {"job_type": "export", "camera": "garage", "id": "b"}, + ] + }, + } + } + ) + self.assertIsNone(self._materialize(_ws("house_only"), "job_state", payload)) + + # --- Reshape: stats --- + + def _stats_payload(self) -> str: + return json.dumps( + { + "cameras": { + "front_door": {"camera_fps": 5.0, "pid": 1234}, + "back_door": {"camera_fps": 5.0, "pid": 1235}, + "garage": {"camera_fps": 5.0, "pid": 1236}, + }, + "detectors": {"cpu": {"detection_start": 0.0, "inference_speed": 10}}, + "service": {"uptime": 12345, "version": "0.16.0"}, + "camera_fps": 15.0, + "detection_fps": 6.0, + } + ) + + def test_stats_admin_sees_full_payload(self): + message = self._materialize(_ws("admin"), "stats", self._stats_payload()) + envelope = json.loads(message) # type: ignore[arg-type] + self.assertEqual(envelope["payload"], self._stats_payload()) + + def test_stats_restricted_filters_camera_keys_but_keeps_aggregates(self): + message = self._materialize(_ws("house_only"), "stats", self._stats_payload()) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertEqual(set(inner["cameras"].keys()), {"front_door", "back_door"}) + self.assertNotIn("garage", inner["cameras"]) + # Aggregates, detectors, and service block must survive. + self.assertEqual(inner["camera_fps"], 15.0) + self.assertEqual(inner["detection_fps"], 6.0) + self.assertIn("detectors", inner) + self.assertIn("service", inner) + + def test_stats_restricted_with_no_allowed_cameras_still_sends_aggregates(self): + """A restricted role whose allow-list contains only nonexistent cameras + still gets the global aggregates and service block.""" + config = _build_config(extra_roles={"empty_role": ["nonexistent"]}) + from frigate.comms.ws import _parse_json_payload + + payload = self._stats_payload() + all_cameras = set(config.cameras.keys()) + scope = _classify_outbound("stats", all_cameras, _collect_zone_names(config)) + full = json.dumps({"topic": "stats", "payload": payload}) + message = _materialize_for_ws( + _ws("empty_role"), + "stats", + full, + scope, + _parse_json_payload(payload), + config, + ) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertEqual(inner["cameras"], {}) + self.assertEqual(inner["camera_fps"], 15.0) + self.assertIn("service", inner) + + def test_stats_without_cameras_key_passes_through(self): + """A malformed stats payload missing the cameras sub-dict shouldn't + break delivery for restricted users — fall back to the full message.""" + payload = json.dumps({"detectors": {}, "service": {}, "detection_fps": 0.0}) + message = self._materialize(_ws("house_only"), "stats", payload) + envelope = json.loads(message) # type: ignore[arg-type] + self.assertEqual(envelope["payload"], payload) + + def test_job_state_export_entry_unchanged_for_admin(self): + payload = json.dumps( + { + "export": { + "job_type": "export", + "status": "running", + "results": { + "jobs": [ + {"job_type": "export", "camera": "garage", "id": "b"}, + ] + }, + } + } + ) + message = self._materialize(_ws("admin"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + self.assertEqual(envelope["payload"], payload) + + def test_job_state_restricted_keeps_global_jobs(self): + """media_sync has no camera field; restricted users still see it.""" + payload = json.dumps( + {"media_sync": {"job_type": "media_sync", "status": "running"}} + ) + message = self._materialize(_ws("house_only"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertIn("media_sync", inner) + + def test_job_state_debug_replay_nested_source_camera_filtered(self): + """debug_replay puts ``source_camera`` inside ``results`` (see + jobs/debug_replay.py:to_dict). Restricted users must not receive + entries whose nested source camera is unauthorized.""" + payload = json.dumps( + { + "debug_replay": { + "id": "bd6dc99d-a7d", + "job_type": "debug_replay", + "status": "running", + "start_time": 1.0, + "end_time": None, + "error_message": None, + "results": { + "current_step": "preparing_clip", + "progress_percent": 0.0, + "source_camera": "garage", + "replay_camera_name": "_replay_garage", + "start_ts": 0.0, + "end_ts": 1.0, + }, + } + } + ) + self.assertIsNone(self._materialize(_ws("house_only"), "job_state", payload)) + + def test_job_state_debug_replay_nested_source_camera_allowed(self): + payload = json.dumps( + { + "debug_replay": { + "id": "bd6dc99d-a7d", + "job_type": "debug_replay", + "status": "running", + "results": { + "source_camera": "front_door", + "replay_camera_name": "_replay_front_door", + }, + } + } + ) + message = self._materialize(_ws("house_only"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertIn("debug_replay", inner) + self.assertEqual( + inner["debug_replay"]["results"]["source_camera"], "front_door" + ) + + +class _FakeManager: + """Minimal ws4py manager: holds clients and exposes a lock.""" + + def __init__(self, clients: list[Any]) -> None: + self.lock = threading.Lock() + self.websockets = {id(c): c for c in clients} + + +class _FakeServer: + def __init__(self, manager: _FakeManager) -> None: + self.manager = manager + + +class _CapturingWs(SimpleNamespace): + """Fake ws4py client that records what was sent.""" + + def __init__(self, role: str | None) -> None: + environ = {} if role is None else {"HTTP_REMOTE_ROLE": role} + super().__init__(environ=environ, terminated=False) + self.sent: list[str] = [] + + def send(self, message: str) -> None: # noqa: D401 - matches ws4py API + self.sent.append(message) + + +class TestPublishEndToEnd(unittest.TestCase): + """Drive WebSocketClient.publish() against fake clients with different roles.""" + + def setUp(self): + self.config = _build_config( + extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}} + ) + self.admin = _CapturingWs("admin") + self.restricted = _CapturingWs("house_only") + self.anon = _CapturingWs(None) + self.client = WebSocketClient(self.config) + self.client.websocket_server = _FakeServer( + _FakeManager([self.admin, self.restricted, self.anon]) + ) + + def _payloads(self, ws: _CapturingWs) -> list[Any]: + return [json.loads(m)["payload"] for m in ws.sent] + + def test_global_topic_reaches_everyone(self): + self.client.publish("model_state", "{}") + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 1) + self.assertEqual(len(self.anon.sent), 1) + + def test_camera_topic_filters_restricted_recipient(self): + self.client.publish("garage/detect/state", "ON") + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 0) + self.assertEqual(len(self.anon.sent), 0) + + def test_camera_topic_allows_restricted_recipient_for_allowed_camera(self): + self.client.publish("front_door/detect/state", "ON") + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 1) + self.assertEqual(len(self.anon.sent), 0) + + def test_events_payload_filtered(self): + self.client.publish("events", json.dumps({"after": {"camera": "garage"}})) + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 0) + + def test_camera_activity_reshaped_per_recipient(self): + self.client.publish( + "camera_activity", + json.dumps( + { + "front_door": {"objects": 1}, + "back_door": {"objects": 0}, + "garage": {"objects": 2}, + } + ), + ) + self.assertEqual(len(self.admin.sent), 1) + admin_inner = json.loads(self._payloads(self.admin)[0]) + self.assertEqual(set(admin_inner.keys()), {"front_door", "back_door", "garage"}) + + self.assertEqual(len(self.restricted.sent), 1) + restricted_inner = json.loads(self._payloads(self.restricted)[0]) + self.assertEqual(set(restricted_inner.keys()), {"front_door", "back_door"}) + + self.assertEqual(len(self.anon.sent), 0) + + def test_birdseye_layout_blocked_for_restricted_and_anon(self): + self.client.publish( + "birdseye_layout", + json.dumps({"front_door": {"x": 0, "y": 0, "width": 1, "height": 1}}), + ) + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 0) + self.assertEqual(len(self.anon.sent), 0) + + def test_zone_aggregate_blocked_for_restricted(self): + self.client.publish("driveway/person", 2) + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 0) + + def test_stats_reshaped_per_recipient(self): + self.client.publish( + "stats", + json.dumps( + { + "cameras": { + "front_door": {"camera_fps": 5.0}, + "garage": {"camera_fps": 5.0}, + }, + "service": {"uptime": 1}, + "camera_fps": 10.0, + } + ), + ) + self.assertEqual(len(self.admin.sent), 1) + admin_inner = json.loads(self._payloads(self.admin)[0]) + self.assertEqual(set(admin_inner["cameras"].keys()), {"front_door", "garage"}) + + self.assertEqual(len(self.restricted.sent), 1) + restricted_inner = json.loads(self._payloads(self.restricted)[0]) + self.assertEqual(set(restricted_inner["cameras"].keys()), {"front_door"}) + self.assertEqual(restricted_inner["camera_fps"], 10.0) + self.assertIn("service", restricted_inner) + + # Stats requires a role; anonymous gets nothing. + self.assertEqual(len(self.anon.sent), 0) + + def test_export_job_state_filters_results_jobs_per_recipient(self): + self.client.publish( + "job_state", + json.dumps( + { + "export": { + "job_type": "export", + "status": "running", + "results": { + "jobs": [ + {"camera": "front_door", "id": "a"}, + {"camera": "garage", "id": "b"}, + ] + }, + } + } + ), + ) + self.assertEqual(len(self.admin.sent), 1) + admin_inner = json.loads(self._payloads(self.admin)[0]) + self.assertEqual( + [j["camera"] for j in admin_inner["export"]["results"]["jobs"]], + ["front_door", "garage"], + ) + + self.assertEqual(len(self.restricted.sent), 1) + restricted_inner = json.loads(self._payloads(self.restricted)[0]) + self.assertEqual( + [j["camera"] for j in restricted_inner["export"]["results"]["jobs"]], + ["front_door"], + ) + + def test_unknown_topic_dropped_for_everyone(self): + self.client.publish("some_rogue_topic", "data") + self.assertEqual(self.admin.sent, []) + self.assertEqual(self.restricted.sent, []) + self.assertEqual(self.anon.sent, []) + + def test_terminated_client_is_skipped(self): + self.restricted.terminated = True + self.client.publish("front_door/detect/state", "ON") + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_yuv_region_2_rgb.py b/frigate/test/test_yuv_region_2_rgb.py new file mode 100644 index 0000000..10144e6 --- /dev/null +++ b/frigate/test/test_yuv_region_2_rgb.py @@ -0,0 +1,43 @@ +from unittest import TestCase, main + +import cv2 +import numpy as np + +from frigate.util.image import yuv_region_2_rgb + + +class TestYuvRegion2RGB(TestCase): + def setUp(self): + self.bgr_frame = np.zeros((100, 200, 3), np.uint8) + self.bgr_frame[:] = (0, 0, 255) + self.bgr_frame[5:55, 5:55] = (255, 0, 0) + # cv2.imwrite(f"bgr_frame.jpg", self.bgr_frame) + self.yuv_frame = cv2.cvtColor(self.bgr_frame, cv2.COLOR_BGR2YUV_I420) + + def test_crop_yuv(self): + cropped = yuv_region_2_rgb(self.yuv_frame, (10, 10, 50, 50)) + # ensure the upper left pixel is blue + assert np.all(cropped[0, 0] == [0, 0, 255]) + + def test_crop_yuv_out_of_bounds(self): + cropped = yuv_region_2_rgb(self.yuv_frame, (0, 0, 200, 200)) + # cv2.imwrite(f"cropped.jpg", cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR)) + # ensure the upper left pixel is red + # the yuv conversion has some noise + assert np.all(cropped[0, 0] == [255, 1, 0]) + # ensure the bottom right is black + assert np.all(cropped[199, 199] == [0, 0, 0]) + + def test_crop_yuv_portrait(self): + bgr_frame = np.zeros((1920, 1080, 3), np.uint8) + bgr_frame[:] = (0, 0, 255) + bgr_frame[5:55, 5:55] = (255, 0, 0) + # cv2.imwrite(f"bgr_frame.jpg", self.bgr_frame) + yuv_frame = cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2YUV_I420) + + yuv_region_2_rgb(yuv_frame, (0, 852, 648, 1500)) + # cv2.imwrite(f"cropped.jpg", cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR)) + + +if __name__ == "__main__": + main(verbosity=2) diff --git a/frigate/timeline.py b/frigate/timeline.py new file mode 100644 index 0000000..d82f17c --- /dev/null +++ b/frigate/timeline.py @@ -0,0 +1,229 @@ +"""Record events for object, audio, etc. detections.""" + +import logging +import queue +import threading +from multiprocessing import Queue +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +from frigate.config import FrigateConfig +from frigate.events.types import EventStateEnum, EventTypeEnum +from frigate.models import Timeline +from frigate.util.builtin import to_relative_box + +logger = logging.getLogger(__name__) + + +class TimelineProcessor(threading.Thread): + """Handle timeline queue and update DB.""" + + def __init__( + self, + config: FrigateConfig, + queue: Queue, + stop_event: MpEvent, + ) -> None: + super().__init__(name="timeline_processor") + self.config = config + self.queue = queue + self.stop_event = stop_event + self.pre_event_cache: dict[str, list[dict[Any, Any]]] = {} + + def run(self) -> None: + while not self.stop_event.is_set(): + try: + ( + camera, + input_type, + event_type, + prev_event_data, + event_data, + ) = self.queue.get(timeout=1) + except queue.Empty: + continue + + if input_type == EventTypeEnum.tracked_object: + # None prev_event_data is only allowed for the start of an event + if event_type != EventStateEnum.start and prev_event_data is None: + continue + + self.handle_object_detection( + camera, event_type, prev_event_data, event_data + ) + elif input_type == EventTypeEnum.api: + self.handle_api_entry(camera, event_type, event_data) + + def insert_or_save( + self, + entry: dict[Any, Any], + prev_event_data: dict[Any, Any], + event_data: dict[Any, Any], + ) -> None: + """Insert into db or cache.""" + id = entry[Timeline.source_id] + if not event_data["has_clip"] and not event_data["has_snapshot"]: + # the related event has not been saved yet, should be added to cache + if id in self.pre_event_cache.keys(): + self.pre_event_cache[id].append(entry) + else: + self.pre_event_cache[id] = [entry] + else: + # the event is saved, insert to db and insert cached into db + if id in self.pre_event_cache.keys(): + for e in self.pre_event_cache[id]: + Timeline.insert(e).execute() + + self.pre_event_cache.pop(id) + + Timeline.insert(entry).execute() + + def handle_object_detection( + self, + camera: str, + event_type: str, + prev_event_data: dict[Any, Any], + event_data: dict[Any, Any], + ) -> None: + """Handle object detection.""" + camera_config = self.config.cameras.get(camera) + if ( + camera_config is None + or camera_config.detect.width is None + or camera_config.detect.height is None + ): + return + event_id = event_data["id"] + + # Base timeline entry data that all entries will share + base_entry = { + Timeline.timestamp: event_data["frame_time"], + Timeline.camera: camera, + Timeline.source: "tracked_object", + Timeline.source_id: event_id, + Timeline.data: { + "box": to_relative_box( + camera_config.detect.width, + camera_config.detect.height, + event_data["box"], + ), + "label": event_data["label"], + "sub_label": event_data.get("sub_label"), + "region": to_relative_box( + camera_config.detect.width, + camera_config.detect.height, + event_data["region"], + ), + "attribute": "", + "score": event_data["score"], + "computed_score": event_data.get("computed_score"), + "top_score": event_data.get("top_score"), + }, + } + + # update sub labels for existing entries that haven't been added yet + if ( + prev_event_data != None + and prev_event_data["sub_label"] != event_data["sub_label"] + and event_id in self.pre_event_cache.keys() + ): + for e in self.pre_event_cache[event_id]: + e[Timeline.data]["sub_label"] = event_data["sub_label"] + + if event_type == EventStateEnum.start: + timeline_entry = base_entry.copy() + timeline_entry[Timeline.class_type] = "visible" + self.insert_or_save(timeline_entry, prev_event_data, event_data) + elif event_type == EventStateEnum.update: + # Check all conditions and create timeline entries for each change + entries_to_save = [] + + # Check for zone changes + prev_zones = set(prev_event_data["current_zones"]) + current_zones = set(event_data["current_zones"]) + zones_changed = prev_zones != current_zones + + # Only save "entered_zone" events when the object is actually IN zones + if ( + zones_changed + and not event_data["stationary"] + and len(current_zones) > 0 + ): + zone_entry = base_entry.copy() + zone_entry[Timeline.class_type] = "entered_zone" + zone_entry[Timeline.data] = base_entry[Timeline.data].copy() + zone_entry[Timeline.data]["zones"] = event_data["current_zones"] + entries_to_save.append(zone_entry) + + # Check for stationary status change + if prev_event_data["stationary"] != event_data["stationary"]: + stationary_entry = base_entry.copy() + stationary_entry[Timeline.class_type] = ( + "stationary" if event_data["stationary"] else "active" + ) + stationary_entry[Timeline.data] = base_entry[Timeline.data].copy() + entries_to_save.append(stationary_entry) + + # Check for new attributes + if prev_event_data["attributes"] == {} and event_data["attributes"] != {}: + attribute_entry = base_entry.copy() + attribute_entry[Timeline.class_type] = "attribute" + attribute_entry[Timeline.data] = base_entry[Timeline.data].copy() + attribute_entry[Timeline.data]["attribute"] = list( + event_data["attributes"].keys() + )[0] + + if len(event_data["current_attributes"]) > 0: + attribute_entry[Timeline.data]["attribute_box"] = to_relative_box( + camera_config.detect.width, + camera_config.detect.height, + event_data["current_attributes"][0]["box"], + ) + + entries_to_save.append(attribute_entry) + + # Save all entries + for entry in entries_to_save: + self.insert_or_save(entry, prev_event_data, event_data) + + elif event_type == EventStateEnum.end: + timeline_entry = base_entry.copy() + timeline_entry[Timeline.class_type] = "gone" + self.insert_or_save(timeline_entry, prev_event_data, event_data) + + def handle_api_entry( + self, + camera: str, + event_type: str, + event_data: dict[Any, Any], + ) -> bool: + if event_type != "start": + return False + + if event_data.get("type", "api") == "audio": + timeline_entry = { + Timeline.class_type: "heard", + Timeline.timestamp: event_data["start_time"], + Timeline.camera: camera, + Timeline.source: "audio", + Timeline.source_id: event_data["id"], + Timeline.data: { + "label": event_data["label"], + "sub_label": event_data.get("sub_label"), + }, + } + else: + timeline_entry = { + Timeline.class_type: "external", + Timeline.timestamp: event_data["start_time"], + Timeline.camera: camera, + Timeline.source: "api", + Timeline.source_id: event_data["id"], + Timeline.data: { + "label": event_data["label"], + "sub_label": event_data.get("sub_label"), + }, + } + + Timeline.insert(timeline_entry).execute() + return True diff --git a/frigate/track/__init__.py b/frigate/track/__init__.py new file mode 100644 index 0000000..b5453aa --- /dev/null +++ b/frigate/track/__init__.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from typing import Any + +from frigate.config import DetectConfig + + +class ObjectTracker(ABC): + @abstractmethod + def __init__(self, config: DetectConfig) -> None: + pass + + @abstractmethod + def match_and_update( + self, + frame_name: str, + frame_time: float, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], + ) -> None: + pass diff --git a/frigate/track/centroid_tracker.py b/frigate/track/centroid_tracker.py new file mode 100644 index 0000000..56f2062 --- /dev/null +++ b/frigate/track/centroid_tracker.py @@ -0,0 +1,247 @@ +import random +import string +from collections import defaultdict +from typing import Any + +import numpy as np +from scipy.spatial import distance as dist + +from frigate.config import DetectConfig +from frigate.track import ObjectTracker +from frigate.util.image import intersection_over_union + + +class CentroidTracker(ObjectTracker): + def __init__(self, config: DetectConfig): + self.tracked_objects: dict[str, dict[str, Any]] = {} + self.untracked_object_boxes: list[tuple[int, int, int, int]] = [] + self.disappeared: dict[str, Any] = {} + self.positions: dict[str, Any] = {} + self.max_disappeared = config.max_disappeared + self.detect_config = config + + def register(self, obj: dict[str, Any]) -> None: + rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + id = f"{obj['frame_time']}-{rand_id}" + obj["id"] = id + obj["start_time"] = obj["frame_time"] + obj["motionless_count"] = 0 + obj["position_changes"] = 0 + self.tracked_objects[id] = obj + self.disappeared[id] = 0 + self.positions[id] = { + "xmins": [], + "ymins": [], + "xmaxs": [], + "ymaxs": [], + "xmin": 0, + "ymin": 0, + "xmax": self.detect_config.width, + "ymax": self.detect_config.height, + } + + def deregister(self, id: str) -> None: + del self.tracked_objects[id] + del self.disappeared[id] + + # tracks the current position of the object based on the last N bounding boxes + # returns False if the object has moved outside its previous position + def update_position(self, id: str, box: tuple[int, int, int, int]) -> bool: + position = self.positions[id] + position_box = ( + position["xmin"], + position["ymin"], + position["xmax"], + position["ymax"], + ) + + xmin, ymin, xmax, ymax = box + + iou = intersection_over_union(position_box, box) + + # if the iou drops below the threshold + # assume the object has moved to a new position and reset the computed box + if iou < 0.6: + self.positions[id] = { + "xmins": [xmin], + "ymins": [ymin], + "xmaxs": [xmax], + "ymaxs": [ymax], + "xmin": xmin, + "ymin": ymin, + "xmax": xmax, + "ymax": ymax, + } + return False + + # if there are less than 10 entries for the position, add the bounding box + # and recompute the position box + if len(position["xmins"]) < 10: + position["xmins"].append(xmin) + position["ymins"].append(ymin) + position["xmaxs"].append(xmax) + position["ymaxs"].append(ymax) + # by using percentiles here, we hopefully remove outliers + position["xmin"] = np.percentile(position["xmins"], 15) + position["ymin"] = np.percentile(position["ymins"], 15) + position["xmax"] = np.percentile(position["xmaxs"], 85) + position["ymax"] = np.percentile(position["ymaxs"], 85) + + return True + + def is_expired(self, id: str) -> bool: + obj = self.tracked_objects[id] + # get the max frames for this label type or the default + max_frames = self.detect_config.stationary.max_frames.objects.get( + obj["label"], self.detect_config.stationary.max_frames.default + ) + + # if there is no max_frames for this label type, continue + if max_frames is None: + return False + + # if the object has exceeded the max_frames setting, deregister + if ( + obj["motionless_count"] - self.detect_config.stationary.threshold + > max_frames + ): + return True + + return False + + def update(self, id: str, new_obj: dict[str, Any]) -> None: + self.disappeared[id] = 0 + # update the motionless count if the object has not moved to a new position + if self.update_position(id, new_obj["box"]): + self.tracked_objects[id]["motionless_count"] += 1 + if self.is_expired(id): + self.deregister(id) + return + else: + # register the first position change and then only increment if + # the object was previously stationary + if ( + self.tracked_objects[id]["position_changes"] == 0 + or self.tracked_objects[id]["motionless_count"] + >= self.detect_config.stationary.threshold + ): + self.tracked_objects[id]["position_changes"] += 1 + self.tracked_objects[id]["motionless_count"] = 0 + + self.tracked_objects[id].update(new_obj) + + def update_frame_times(self, frame_name: str, frame_time: float) -> None: + for id in list(self.tracked_objects.keys()): + self.tracked_objects[id]["frame_time"] = frame_time + self.tracked_objects[id]["motionless_count"] += 1 + if self.is_expired(id): + self.deregister(id) + + def match_and_update( + self, + frame_name: str, + frame_time: float, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], + ) -> None: + # group by name + detection_groups = defaultdict(lambda: []) + for det in detections: + detection_groups[det[0]].append( + { + "label": det[0], + "score": det[1], + "box": det[2], + "area": det[3], + "ratio": det[4], + "region": det[5], + "frame_time": frame_time, + } + ) + + # update any tracked objects with labels that are not + # seen in the current objects and deregister if needed + for obj in list(self.tracked_objects.values()): + if obj["label"] not in detection_groups: + if self.disappeared[obj["id"]] >= self.max_disappeared: + self.deregister(obj["id"]) + else: + self.disappeared[obj["id"]] += 1 + + if len(detections) == 0: + return + + # track objects for each label type + for label, group in detection_groups.items(): + current_objects = [ + o for o in self.tracked_objects.values() if o["label"] == label + ] + current_ids = [o["id"] for o in current_objects] + current_centroids = np.array([o["centroid"] for o in current_objects]) + + # compute centroids of new objects + for obj in group: + centroid_x = int((obj["box"][0] + obj["box"][2]) / 2.0) + centroid_y = int((obj["box"][1] + obj["box"][3]) / 2.0) + obj["centroid"] = (centroid_x, centroid_y) + + if len(current_objects) == 0: + for index, obj in enumerate(group): + self.register(obj) + continue + + new_centroids = np.array([o["centroid"] for o in group]) + + # compute the distance between each pair of tracked + # centroids and new centroids, respectively -- our + # goal will be to match each current centroid to a new + # object centroid + D = dist.cdist(current_centroids, new_centroids) + + # in order to perform this matching we must (1) find the smallest + # value in each row (i.e. the distance from each current object to + # the closest new object) and then (2) sort the row indexes based + # on their minimum values so that the row with the smallest + # distance (the best match) is at the *front* of the index list + rows = D.min(axis=1).argsort() + + # next, we determine which new object each existing object matched + # against, and apply the same sorting as was applied previously + cols = D.argmin(axis=1)[rows] + + # many current objects may register with each new object, so only + # match the closest ones. unique returns the indices of the first + # occurrences of each value, and because the rows are sorted by + # distance, this will be index of the closest match + _, index = np.unique(cols, return_index=True) + rows = rows[index] + cols = cols[index] + + # loop over the combination of the (row, column) index tuples + for row, col in zip(rows, cols): + # grab the object ID for the current row, set its new centroid, + # and reset the disappeared counter + objectID = current_ids[row] + self.update(objectID, group[col]) + + # compute the row and column indices we have NOT yet examined + unusedRows = set(range(D.shape[0])).difference(rows) + unusedCols = set(range(D.shape[1])).difference(cols) + + # in the event that the number of object centroids is + # equal or greater than the number of input centroids + # we need to check and see if some of these objects have + # potentially disappeared + if D.shape[0] >= D.shape[1]: + for row in unusedRows: + id = current_ids[row] + + if self.disappeared[id] >= self.max_disappeared: + self.deregister(id) + else: + self.disappeared[id] += 1 + # if the number of input centroids is greater + # than the number of existing object centroids we need to + # register each new input centroid as a trackable object + else: + for col in unusedCols: + self.register(group[col]) diff --git a/frigate/track/norfair_tracker.py b/frigate/track/norfair_tracker.py new file mode 100644 index 0000000..6b39885 --- /dev/null +++ b/frigate/track/norfair_tracker.py @@ -0,0 +1,742 @@ +import logging +import random +import string +from collections.abc import Sequence +from typing import Any, cast + +import cv2 +import numpy as np +from norfair.drawing.draw_boxes import draw_boxes +from norfair.drawing.drawer import Drawable, Drawer +from norfair.filter import OptimizedKalmanFilterFactory +from norfair.tracker import Detection, TrackedObject, Tracker +from rich import print +from rich.console import Console +from rich.table import Table + +from frigate.camera import PTZMetrics +from frigate.config import CameraConfig +from frigate.ptz.autotrack import PtzMotionEstimator +from frigate.track import ObjectTracker +from frigate.track.stationary_classifier import ( + StationaryMotionClassifier, + StationaryThresholds, + get_stationary_threshold, +) +from frigate.util.image import ( + SharedMemoryFrameManager, + get_histogram, + intersection_over_union, +) +from frigate.util.object import average_boxes, median_of_boxes + +logger = logging.getLogger(__name__) + + +# Normalizes distance from estimate relative to object size +# Other ideas: +# - if estimates are inaccurate for first N detections, compare with last_detection (may be fine) +# - could be variable based on time since last_detection +# - include estimated velocity in the distance (car driving by of a parked car) +# - include some visual similarity factor in the distance for occlusions +def distance(detection: np.ndarray, estimate: np.ndarray) -> float: + # ultimately, this should try and estimate distance in 3-dimensional space + # consider change in location, width, and height + + estimate_dim = np.diff(estimate, axis=0).flatten() + detection_dim = np.diff(detection, axis=0).flatten() + + # Guard against degenerate or non-finite boxes + if ( + not np.all(np.isfinite(estimate_dim)) + or not np.all(np.isfinite(detection_dim)) + or estimate_dim[0] <= 0 + or estimate_dim[1] <= 0 + or detection_dim[0] <= 0 + or detection_dim[1] <= 0 + ): + return float("inf") + + # get bottom center positions + detection_position = np.array( + [np.average(detection[:, 0]), np.max(detection[:, 1])] + ) + estimate_position = np.array([np.average(estimate[:, 0]), np.max(estimate[:, 1])]) + + distance = (detection_position - estimate_position).astype(float) + # change in x relative to w + distance[0] /= estimate_dim[0] + # change in y relative to h + distance[1] /= estimate_dim[1] + + # get ratio of widths and heights + # normalize to 1 + widths = np.sort([estimate_dim[0], detection_dim[0]]) + heights = np.sort([estimate_dim[1], detection_dim[1]]) + width_ratio = widths[1] / widths[0] - 1.0 + height_ratio = heights[1] / heights[0] - 1.0 + + # change vector is relative x,y change and w,h ratio + change = np.append(distance, np.array([width_ratio, height_ratio])) + + # calculate euclidean distance of the change vector + return float(np.linalg.norm(change)) + + +def frigate_distance(detection: Detection, tracked_object: TrackedObject) -> float: + return distance(detection.points, tracked_object.estimate) + + +def histogram_distance( + matched_not_init_trackers: TrackedObject, unmatched_trackers: TrackedObject +) -> float: + snd_embedding = unmatched_trackers.last_detection.embedding + + if snd_embedding is None: + for detection in reversed(unmatched_trackers.past_detections): + if detection.embedding is not None: + snd_embedding = detection.embedding + break + else: + return 1 + + for detection_fst in matched_not_init_trackers.past_detections: + if detection_fst.embedding is None: + continue + + distance = 1 - cv2.compareHist( + snd_embedding, detection_fst.embedding, cv2.HISTCMP_CORREL + ) + if distance < 0.5: + return distance + return 1 + + +class NorfairTracker(ObjectTracker): + def __init__( + self, + config: CameraConfig, + ptz_metrics: PTZMetrics, + ): + self.frame_manager = SharedMemoryFrameManager() + self.tracked_objects: dict[str, dict[str, Any]] = {} + self.untracked_object_boxes: list[list[int]] = [] + self.disappeared: dict[str, int] = {} + self.positions: dict[str, dict[str, Any]] = {} + self.stationary_box_history: dict[str, list[list[int]]] = {} + self.camera_config = config + self.detect_config = config.detect + self.ptz_metrics = ptz_metrics + self.ptz_motion_estimator: PtzMotionEstimator | None = None + self.camera_name = config.name + self.track_id_map: dict[str, str] = {} + self.stationary_classifier = StationaryMotionClassifier() + + # Define tracker configurations for static camera + self.object_type_configs = { + "car": { + "filter_factory": OptimizedKalmanFilterFactory(R=3.4, Q=0.03), + "distance_function": frigate_distance, + "distance_threshold": 2.5, + }, + "license_plate": { + "filter_factory": OptimizedKalmanFilterFactory(R=2.5, Q=0.05), + "distance_function": frigate_distance, + "distance_threshold": 3.75, + }, + } + + # Define autotracking PTZ-specific configurations + self.ptz_object_type_configs = { + "person": { + "filter_factory": OptimizedKalmanFilterFactory( + R=4.5, + Q=0.25, + ), + "distance_function": frigate_distance, + "distance_threshold": 2, + "past_detections_length": 5, + "reid_distance_function": histogram_distance, + "reid_distance_threshold": 0.5, + "reid_hit_counter_max": 10, + }, + } + + # Default tracker configuration + # use default filter factory with custom values + # R is the multiplier for the sensor measurement noise matrix, default of 4.0 + # lowering R means that we trust the position of the bounding boxes more + # testing shows that the prediction was being relied on a bit too much + self.default_tracker_config = { + "filter_factory": OptimizedKalmanFilterFactory(R=3.4), + "distance_function": frigate_distance, + "distance_threshold": 2.5, + } + + self.default_ptz_tracker_config = { + "filter_factory": OptimizedKalmanFilterFactory(R=4, Q=0.2), + "distance_function": frigate_distance, + "distance_threshold": 3, + } + + self.trackers: dict[str, dict[str, Tracker]] = {} + # Handle static trackers + for obj_type, tracker_config in self.object_type_configs.items(): + if obj_type in self.camera_config.objects.track: + if obj_type not in self.trackers: + self.trackers[obj_type] = {} + self.trackers[obj_type]["static"] = self._create_tracker( + obj_type, tracker_config + ) + + # Handle PTZ trackers + for obj_type, tracker_config in self.ptz_object_type_configs.items(): + if ( + obj_type in self.camera_config.onvif.autotracking.track + and self.camera_config.onvif.autotracking.enabled_in_config + ): + if obj_type not in self.trackers: + self.trackers[obj_type] = {} + self.trackers[obj_type]["ptz"] = self._create_tracker( + obj_type, tracker_config + ) + + # Initialize default trackers + self.default_tracker = { + "static": Tracker( + distance_function=frigate_distance, + distance_threshold=self.default_tracker_config[ # type: ignore[arg-type] + "distance_threshold" + ], + initialization_delay=self.detect_config.min_initialized, + hit_counter_max=self.detect_config.max_disappeared, # type: ignore[arg-type] + filter_factory=self.default_tracker_config["filter_factory"], # type: ignore[arg-type] + ), + "ptz": Tracker( + distance_function=frigate_distance, + distance_threshold=self.default_ptz_tracker_config[ + "distance_threshold" + ], # type: ignore[arg-type] + initialization_delay=self.detect_config.min_initialized, + hit_counter_max=self.detect_config.max_disappeared, # type: ignore[arg-type] + filter_factory=self.default_ptz_tracker_config["filter_factory"], # type: ignore[arg-type] + ), + } + + if self.ptz_metrics.autotracker_enabled.value: + self.ptz_motion_estimator = PtzMotionEstimator( + self.camera_config, self.ptz_metrics + ) + + def _create_tracker(self, obj_type: str, tracker_config: dict[str, Any]) -> Tracker: + """Helper function to create a tracker with given configuration.""" + tracker_params = { + "distance_function": tracker_config["distance_function"], + "distance_threshold": tracker_config["distance_threshold"], + "initialization_delay": self.detect_config.min_initialized, + "hit_counter_max": self.detect_config.max_disappeared, + "filter_factory": tracker_config["filter_factory"], + } + + # Add reid parameters if max_frames is None + if ( + self.detect_config.stationary.max_frames.objects.get( + obj_type, self.detect_config.stationary.max_frames.default + ) + is None + ): + reid_keys = [ + "past_detections_length", + "reid_distance_function", + "reid_distance_threshold", + "reid_hit_counter_max", + ] + tracker_params.update( + {key: tracker_config[key] for key in reid_keys if key in tracker_config} + ) + + return Tracker(**tracker_params) + + def get_tracker(self, object_type: str) -> Tracker: + """Get the appropriate tracker based on object type and camera mode.""" + mode = ( + "ptz" + if self.camera_config.onvif.autotracking.enabled_in_config + and object_type in self.camera_config.onvif.autotracking.track + and object_type in self.ptz_object_type_configs.keys() + else "static" + ) + if object_type in self.trackers: + return self.trackers[object_type][mode] + return self.default_tracker[mode] + + def register(self, track_id: str, obj: dict[str, Any]) -> None: + rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + id = f"{obj['frame_time']}-{rand_id}" + self.track_id_map[track_id] = id + obj["id"] = id + obj["start_time"] = obj["frame_time"] + obj["motionless_count"] = 0 + obj["position_changes"] = 0 + + # Get the correct tracker for this object's label + tracker = self.get_tracker(obj["label"]) + obj_match = next( + (o for o in tracker.tracked_objects if str(o.global_id) == track_id), None + ) + # if we don't have a match, we have a new object + obj["score_history"] = ( + [p.data["score"] for p in obj_match.past_detections] if obj_match else [] + ) + self.tracked_objects[id] = obj + self.disappeared[id] = 0 + if obj_match: + boxes = [p.data["box"] for p in obj_match.past_detections] + else: + boxes = [obj["box"]] + + xmins, ymins, xmaxs, ymaxs = zip(*boxes) + + self.positions[id] = { + "xmins": list(xmins), + "ymins": list(ymins), + "xmaxs": list(xmaxs), + "ymaxs": list(ymaxs), + "xmin": 0, + "ymin": 0, + "xmax": self.detect_config.width, + "ymax": self.detect_config.height, + } + self.stationary_box_history[id] = boxes + + def deregister(self, id: str, track_id: str) -> None: + obj = self.tracked_objects[id] + + del self.tracked_objects[id] + del self.disappeared[id] + + # only manually deregister objects from norfair's list if max_frames is defined + if ( + self.detect_config.stationary.max_frames.objects.get( + obj["label"], self.detect_config.stationary.max_frames.default + ) + is not None + ): + tracker = self.get_tracker(obj["label"]) + tracker.tracked_objects = [ + o + for o in tracker.tracked_objects + if str(o.global_id) != track_id and o.hit_counter < 0 + ] + + del self.track_id_map[track_id] + + # tracks the current position of the object based on the last N bounding boxes + # returns False if the object has moved outside its previous position + def update_position( + self, + id: str, + box: list[int], + stationary: bool, + thresholds: StationaryThresholds, + yuv_frame: np.ndarray | None, + ) -> bool: + def reset_position(xmin: int, ymin: int, xmax: int, ymax: int) -> None: + self.positions[id] = { + "xmins": [xmin], + "ymins": [ymin], + "xmaxs": [xmax], + "ymaxs": [ymax], + "xmin": xmin, + "ymin": ymin, + "xmax": xmax, + "ymax": ymax, + } + + xmin, ymin, xmax, ymax = box + position = self.positions[id] + self.stationary_box_history[id].append(box) + + if len(self.stationary_box_history[id]) > thresholds.max_stationary_history: + self.stationary_box_history[id] = self.stationary_box_history[id][ + -thresholds.max_stationary_history : + ] + + avg_box = average_boxes(self.stationary_box_history[id]) + avg_iou = intersection_over_union(box, avg_box) + median_box = median_of_boxes(self.stationary_box_history[id]) + + # Establish anchor early when stationary and stable + if stationary and yuv_frame is not None: + history = self.stationary_box_history[id] + if id not in self.stationary_classifier.anchor_crops and len(history) >= 5: + stability_iou = intersection_over_union(avg_box, median_box) + if stability_iou >= 0.7: + self.stationary_classifier.ensure_anchor( + id, yuv_frame, cast(tuple[int, int, int, int], median_box) + ) + + # object has minimal or zero iou + # assume object is active + if avg_iou < thresholds.known_active_iou: + if stationary and yuv_frame is not None: + if not self.stationary_classifier.evaluate( + id, yuv_frame, cast(tuple[int, int, int, int], tuple(box)) + ): + reset_position(xmin, ymin, xmax, ymax) + return False + else: + reset_position(xmin, ymin, xmax, ymax) + return False + + threshold = ( + thresholds.stationary_check_iou + if stationary + else thresholds.active_check_iou + ) + + # object has iou below threshold, check median and optionally crop similarity + if avg_iou < threshold: + median_iou = intersection_over_union( + ( + position["xmin"], + position["ymin"], + position["xmax"], + position["ymax"], + ), + median_box, + ) + + # if the median iou drops below the threshold + # assume object is no longer stationary + if median_iou < threshold: + # If we have a yuv_frame to check before flipping to active, check with classifier if we have YUV frame + if stationary and yuv_frame is not None: + if not self.stationary_classifier.evaluate( + id, yuv_frame, cast(tuple[int, int, int, int], tuple(box)) + ): + reset_position(xmin, ymin, xmax, ymax) + return False + else: + reset_position(xmin, ymin, xmax, ymax) + return False + + # if there are more than 5 and less than 10 entries for the position, add the bounding box + # and recompute the position box + if len(position["xmins"]) < 10: + position["xmins"].append(xmin) + position["ymins"].append(ymin) + position["xmaxs"].append(xmax) + position["ymaxs"].append(ymax) + # by using percentiles here, we hopefully remove outliers + position["xmin"] = np.percentile(position["xmins"], 15) + position["ymin"] = np.percentile(position["ymins"], 15) + position["xmax"] = np.percentile(position["xmaxs"], 85) + position["ymax"] = np.percentile(position["ymaxs"], 85) + + return True + + def is_expired(self, id: str) -> bool: + obj = self.tracked_objects[id] + # get the max frames for this label type or the default + max_frames = self.detect_config.stationary.max_frames.objects.get( + obj["label"], self.detect_config.stationary.max_frames.default + ) + + # if there is no max_frames for this label type, continue + if max_frames is None: + return False + + # if the object has exceeded the max_frames setting, deregister + if ( + obj["motionless_count"] - self.detect_config.stationary.threshold + > max_frames + ): + return True + + return False + + def update( + self, + track_id: str, + obj: dict[str, Any], + thresholds: StationaryThresholds, + yuv_frame: np.ndarray | None, + ) -> None: + id = self.track_id_map[track_id] + self.disappeared[id] = 0 + stationary = ( + self.tracked_objects[id]["motionless_count"] + >= self.detect_config.stationary.threshold + ) + # update the motionless count if the object has not moved to a new position + if self.update_position(id, obj["box"], stationary, thresholds, yuv_frame): + self.tracked_objects[id]["motionless_count"] += 1 + if self.is_expired(id): + self.deregister(id, track_id) + return + else: + # register the first position change and then only increment if + # the object was previously stationary + if ( + self.tracked_objects[id]["position_changes"] == 0 + or self.tracked_objects[id]["motionless_count"] + >= self.detect_config.stationary.threshold + ): + self.tracked_objects[id]["position_changes"] += 1 + self.tracked_objects[id]["motionless_count"] = 0 + self.stationary_box_history[id] = [] + self.stationary_classifier.on_active(id) + + self.tracked_objects[id].update(obj) + + def update_frame_times(self, frame_name: str, frame_time: float) -> None: + # if the object was there in the last frame, assume it's still there + detections = [ + ( + obj["label"], + obj["score"], + obj["box"], + obj["area"], + obj["ratio"], + obj["region"], + ) + for id, obj in self.tracked_objects.items() + if self.disappeared[id] == 0 + ] + self.match_and_update(frame_name, frame_time, detections=detections) + + def match_and_update( + self, + frame_name: str, + frame_time: float, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], + ) -> None: + # Group detections by object type + detections_by_type: dict[str, list[Detection]] = {} + yuv_frame: np.ndarray | None = None + + if ( + self.ptz_metrics.autotracker_enabled.value + or self.detect_config.stationary.classifier + ): + yuv_frame = self.frame_manager.get( + frame_name, self.camera_config.frame_shape_yuv + ) + for obj in detections: + label = obj[0] + if label not in detections_by_type: + detections_by_type[label] = [] + + # centroid is used for other things downstream + centroid_x = int((obj[2][0] + obj[2][2]) / 2.0) + centroid_y = int((obj[2][1] + obj[2][3]) / 2.0) + + # track based on top,left and bottom,right corners instead of centroid + points = np.array([[obj[2][0], obj[2][1]], [obj[2][2], obj[2][3]]]) + + embedding = None + if self.ptz_metrics.autotracker_enabled.value: + embedding = get_histogram( + yuv_frame, obj[2][0], obj[2][1], obj[2][2], obj[2][3] + ) + + detection = Detection( + points=points, + label=label, + # TODO: stationary objects won't have embeddings + embedding=embedding, + data={ + "label": label, + "score": obj[1], + "box": obj[2], + "area": obj[3], + "ratio": obj[4], + "region": obj[5], + "frame_time": frame_time, + "centroid": (centroid_x, centroid_y), + }, + ) + detections_by_type[label].append(detection) + + coord_transformations = None + + if self.ptz_metrics.autotracker_enabled.value: + # we must have been enabled by mqtt, so set up the estimator + if not self.ptz_motion_estimator: + self.ptz_motion_estimator = PtzMotionEstimator( + self.camera_config, self.ptz_metrics + ) + + coord_transformations = self.ptz_motion_estimator.motion_estimator( + detections, frame_name, frame_time, self.camera_name + ) + + # Update all configured trackers + all_tracked_objects = [] + for label in self.trackers: + tracker = self.get_tracker(label) + tracked_objects = tracker.update( + detections=detections_by_type.get(label, []), + coord_transformations=coord_transformations, + ) + all_tracked_objects.extend(tracked_objects) + + # Collect detections for objects without specific trackers + default_detections = [] + for label, dets in detections_by_type.items(): + if label not in self.trackers: + default_detections.extend(dets) + + # Update default tracker with untracked detections + mode = ( + "ptz" + if self.camera_config.onvif.autotracking.enabled_in_config + else "static" + ) + tracked_objects = self.default_tracker[mode].update( + detections=default_detections, coord_transformations=coord_transformations + ) + all_tracked_objects.extend(tracked_objects) + + # update or create new tracks + active_ids = [] + for t in all_tracked_objects: + estimate = tuple(t.estimate.flatten().astype(int)) + # keep the estimate within the bounds of the image + estimate = ( + max(0, estimate[0]), + max(0, estimate[1]), + min(self.detect_config.width - 1, estimate[2]), # type: ignore[operator] + min(self.detect_config.height - 1, estimate[3]), # type: ignore[operator] + ) + new_obj = { + **t.last_detection.data, + "estimate": estimate, + "estimate_velocity": t.estimate_velocity, + } + active_ids.append(str(t.global_id)) + if str(t.global_id) not in self.track_id_map: + self.register(str(t.global_id), new_obj) + # if there wasn't a detection in this frame, increment disappeared + elif t.last_detection.data["frame_time"] != frame_time: + id = self.track_id_map[str(t.global_id)] + self.disappeared[id] += 1 + # sometimes the estimate gets way off + # only update if the upper left corner is actually upper left + if estimate[0] < estimate[2] and estimate[1] < estimate[3]: + self.tracked_objects[id]["estimate"] = new_obj["estimate"] + # else update it + else: + thresholds = get_stationary_threshold(new_obj["label"]) + self.update( + str(t.global_id), + new_obj, + thresholds, + yuv_frame if thresholds.motion_classifier_enabled else None, + ) + + # clear expired tracks + expired_ids = [k for k in self.track_id_map.keys() if k not in active_ids] + for e_id in expired_ids: + self.deregister(self.track_id_map[e_id], e_id) + + # update list of object boxes that don't have a tracked object yet + tracked_object_boxes = { + tuple(obj["box"]) for obj in self.tracked_objects.values() + } + self.untracked_object_boxes = [ + o[2] for o in detections if tuple(o[2]) not in tracked_object_boxes + ] + + def print_objects_as_table(self, tracked_objects: Sequence) -> None: + """Used for helping in debugging""" + print() + console = Console() + table = Table(show_header=True, header_style="bold magenta") + table.add_column("Id", style="yellow", justify="center") + table.add_column("Age", justify="right") + table.add_column("Hit Counter", justify="right") + table.add_column("Last distance", justify="right") + table.add_column("Init Id", justify="center") + for obj in tracked_objects: + table.add_row( + str(obj.id), + str(obj.age), + str(obj.hit_counter), + f"{obj.last_distance:.4f}" if obj.last_distance is not None else "N/A", + str(obj.initializing_id), + ) + console.print(table) + + def debug_draw(self, frame: np.ndarray, frame_time: float) -> None: + # Collect all tracked objects from each tracker + all_tracked_objects: list[TrackedObject] = [] + + # print a table to the console with norfair tracked object info + if False: + if len(self.trackers["license_plate"]["static"].tracked_objects) > 0: # type: ignore[unreachable] + self.print_objects_as_table( + self.trackers["license_plate"]["static"].tracked_objects + ) + + # Get tracked objects from type-specific trackers + for object_trackers in self.trackers.values(): + for tracker in object_trackers.values(): + all_tracked_objects.extend(tracker.tracked_objects) + + # Get tracked objects from default trackers + for tracker in self.default_tracker.values(): + all_tracked_objects.extend(tracker.tracked_objects) + + active_detections = [ + Drawable(id=obj.id, points=obj.last_detection.points, label=obj.label) + for obj in all_tracked_objects + if obj.last_detection.data["frame_time"] == frame_time + ] + missing_detections = [ + Drawable(id=obj.id, points=obj.last_detection.points, label=obj.label) + for obj in all_tracked_objects + if obj.last_detection.data["frame_time"] != frame_time + ] + # draw the estimated bounding box + draw_boxes(frame, all_tracked_objects, color="green", draw_ids=True) + # draw the detections that were detected in the current frame + draw_boxes(frame, active_detections, color="blue", draw_ids=True) # type: ignore[arg-type] + # draw the detections that are missing in the current frame + draw_boxes(frame, missing_detections, color="red", draw_ids=True) # type: ignore[arg-type] + + # draw the distance calculation for the last detection + # estimate vs detection + for obj in all_tracked_objects: + ld = obj.last_detection + # bottom right + text_anchor = ( + ld.points[1, 0], # type: ignore[index] + ld.points[1, 1], # type: ignore[index] + ) + frame = Drawer.text( + frame, + f"{obj.id}: {str(obj.last_distance)}", + position=text_anchor, + size=None, + color=(255, 0, 0), + thickness=None, + ) + + if False: + # draw the current formatted time on the frame + from datetime import datetime # type: ignore[unreachable] + + formatted_time = datetime.fromtimestamp(frame_time).strftime( + "%m/%d/%Y %I:%M:%S %p" + ) + + frame = Drawer.text( + frame, + formatted_time, + position=(10, 50), + size=1.5, + color=(255, 255, 255), + thickness=None, + ) diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py new file mode 100644 index 0000000..5832d8c --- /dev/null +++ b/frigate/track/object_processing.py @@ -0,0 +1,829 @@ +import base64 +import datetime +import json +import logging +import queue +import threading +from collections import defaultdict +from enum import Enum +from multiprocessing import Queue as MpQueue +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import cv2 +import numpy as np +from peewee import SQL, DoesNotExist + +from frigate.camera.state import CameraState +from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum +from frigate.comms.dispatcher import Dispatcher +from frigate.comms.event_metadata_updater import ( + EventMetadataSubscriber, + EventMetadataTypeEnum, +) +from frigate.comms.events_updater import EventEndSubscriber, EventUpdatePublisher +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import ( + CameraMqttConfig, + FrigateConfig, + RecordConfig, + SnapshotsConfig, +) +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + FAST_QUEUE_TIMEOUT, + UPDATE_CAMERA_ACTIVITY, + UPSERT_REVIEW_SEGMENT, +) +from frigate.events.types import EventStateEnum, EventTypeEnum +from frigate.models import Event, ReviewSegment, Timeline +from frigate.ptz.autotrack import PtzAutoTrackerThread +from frigate.track.tracked_object import TrackedObject +from frigate.util.image import SharedMemoryFrameManager + +logger = logging.getLogger(__name__) + + +class ManualEventState(str, Enum): + complete = "complete" + start = "start" + end = "end" + + +class TrackedObjectProcessor(threading.Thread): + def __init__( + self, + config: FrigateConfig, + dispatcher: Dispatcher, + tracked_objects_queue: MpQueue, + ptz_autotracker_thread: PtzAutoTrackerThread, + stop_event: MpEvent, + ) -> None: + super().__init__(name="detected_frames_processor") + self.config = config + self.dispatcher = dispatcher + self.tracked_objects_queue = tracked_objects_queue + self.stop_event: MpEvent = stop_event + self.camera_states: dict[str, CameraState] = {} + self.frame_manager = SharedMemoryFrameManager() + self.last_motion_detected: dict[str, float] = {} + self.ptz_autotracker_thread = ptz_autotracker_thread + + self.camera_config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.motion, + CameraConfigUpdateEnum.objects, + CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.timestamp_style, + CameraConfigUpdateEnum.zones, + ], + ) + + self.requestor = InterProcessRequestor() + self.detection_publisher = DetectionPublisher(DetectionTypeEnum.all.value) + self.event_sender = EventUpdatePublisher() + self.event_end_subscriber = EventEndSubscriber() + self.sub_label_subscriber = EventMetadataSubscriber(EventMetadataTypeEnum.all) + + self.camera_activity: dict[str, dict[str, Any]] = {} + self.ongoing_manual_events: dict[str, str] = {} + + # { + # 'zone_name': { + # 'person': { + # 'camera_1': 2, + # 'camera_2': 1 + # } + # } + # } + self.zone_data: dict[str, dict[str, Any]] = defaultdict( + lambda: defaultdict(dict) + ) + self.active_zone_data: dict[str, dict[str, Any]] = defaultdict( + lambda: defaultdict(dict) + ) + + for camera in self.config.cameras.keys(): + self.create_camera_state(camera) + + def create_camera_state(self, camera: str) -> None: + """Creates a new camera state.""" + + def start(camera: str, obj: TrackedObject, frame_name: str) -> None: + self.event_sender.publish( + ( + EventTypeEnum.tracked_object, + EventStateEnum.start, + camera, + frame_name, + obj.to_dict(), + ) + ) + + def update(camera: str, obj: TrackedObject, frame_name: str) -> None: + obj.has_snapshot = self.should_save_snapshot(camera, obj) + obj.has_clip = self.should_retain_recording(camera, obj) + after = obj.to_dict() + message = { + "before": obj.previous, + "after": after, + "type": "new" if obj.previous["false_positive"] else "update", + } + self.dispatcher.publish("events", json.dumps(message), retain=False) + obj.previous = after + self.event_sender.publish( + ( + EventTypeEnum.tracked_object, + EventStateEnum.update, + camera, + frame_name, + obj.to_dict(), + ) + ) + + def autotrack(camera: str, obj: TrackedObject, frame_name: str) -> None: + self.ptz_autotracker_thread.ptz_autotracker.autotrack_object(camera, obj) + + def end(camera: str, obj: TrackedObject, frame_name: str) -> None: + # populate has_snapshot + obj.has_snapshot = self.should_save_snapshot(camera, obj) + obj.has_clip = self.should_retain_recording(camera, obj) + + # write thumbnail to disk if it will be saved as an event + if obj.has_snapshot or obj.has_clip: + obj.write_thumbnail_to_disk() + + # write the snapshot to disk + if obj.has_snapshot: + obj.write_snapshot_to_disk() + + if not obj.false_positive: + message = { + "before": obj.previous, + "after": obj.to_dict(), + "type": "end", + } + self.dispatcher.publish("events", json.dumps(message), retain=False) + self.ptz_autotracker_thread.ptz_autotracker.end_object(camera, obj) + + self.event_sender.publish( + ( + EventTypeEnum.tracked_object, + EventStateEnum.end, + camera, + frame_name, + obj.to_dict(), + ) + ) + + def snapshot(camera: str, obj: TrackedObject) -> bool: + mqtt_config: CameraMqttConfig = self.config.cameras[camera].mqtt + if mqtt_config.enabled and self.should_mqtt_snapshot(camera, obj): + jpg_bytes, _ = obj.get_img_bytes( + ext="jpg", + timestamp=mqtt_config.timestamp, + bounding_box=mqtt_config.bounding_box, + crop=mqtt_config.crop, + height=mqtt_config.height, + quality=mqtt_config.quality, + ) + + if jpg_bytes is None: + logger.warning( + f"Unable to send mqtt snapshot for {obj.obj_data['id']}." + ) + else: + self.dispatcher.publish( + f"{camera}/{obj.obj_data['label']}/snapshot", + jpg_bytes, + retain=True, + ) + + if obj.obj_data.get("sub_label"): + sub_label = obj.obj_data["sub_label"][0] + + if sub_label in self.config.model.all_attribute_logos: + self.dispatcher.publish( + f"{camera}/{sub_label}/snapshot", + jpg_bytes, + retain=True, + ) + + return True + + return False + + def camera_activity(camera: str, activity: dict[str, Any]) -> None: + last_activity = self.camera_activity.get(camera) + + if not last_activity or activity != last_activity: + self.camera_activity[camera] = activity + self.requestor.send_data(UPDATE_CAMERA_ACTIVITY, self.camera_activity) + + camera_state = CameraState( + camera, self.config, self.frame_manager, self.ptz_autotracker_thread + ) + camera_state.on("start", start) + camera_state.on("autotrack", autotrack) + camera_state.on("update", update) + camera_state.on("end", end) + camera_state.on("snapshot", snapshot) + camera_state.on("camera_activity", camera_activity) + self.camera_states[camera] = camera_state + + def should_save_snapshot(self, camera: str, obj: TrackedObject) -> bool: + if obj.false_positive: + return False + + snapshot_config: SnapshotsConfig = self.config.cameras[camera].snapshots + + if not snapshot_config.enabled: + return False + + # object never changed position + if obj.obj_data["position_changes"] == 0: + return False + + # if there are required zones and there is no overlap + required_zones = snapshot_config.required_zones + if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones): + logger.debug( + f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones" + ) + return False + + return True + + def should_retain_recording(self, camera: str, obj: TrackedObject) -> bool: + if obj.false_positive: + return False + + record_config: RecordConfig = self.config.cameras[camera].record + + # Recording is disabled + if not record_config.enabled: + return False + + # object never changed position + if obj.obj_data["position_changes"] == 0: + return False + + # If the object is not considered an alert or detection + if obj.max_severity is None: + return False + + return True + + def should_mqtt_snapshot(self, camera: str, obj: TrackedObject) -> bool: + # object never changed position + if obj.is_stationary(): + return False + + # if there are required zones and there is no overlap + required_zones = self.config.cameras[camera].mqtt.required_zones + if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones): + logger.debug( + f"Not sending mqtt for {obj.obj_data['id']} because it did not enter required zones" + ) + return False + + return True + + def update_mqtt_motion( + self, camera: str, frame_time: float, motion_boxes: list + ) -> None: + # publish if motion is currently being detected + if motion_boxes: + # only send ON if motion isn't already active + if self.last_motion_detected.get(camera, 0) == 0: + self.dispatcher.publish( + f"{camera}/motion", + "ON", + retain=False, + ) + + # always updated latest motion + self.last_motion_detected[camera] = frame_time + elif self.last_motion_detected.get(camera, 0) > 0: + mqtt_delay = self.config.cameras[camera].motion.mqtt_off_delay + + # If no motion, make sure the off_delay has passed + if frame_time - self.last_motion_detected.get(camera, 0) >= mqtt_delay: + self.dispatcher.publish( + f"{camera}/motion", + "OFF", + retain=False, + ) + # reset the last_motion so redundant `off` commands aren't sent + self.last_motion_detected[camera] = 0 + + def get_best(self, camera: str, label: str) -> dict[str, Any]: + # TODO: need a lock here + camera_state = self.camera_states[camera] + if label in camera_state.best_objects: + best_obj = camera_state.best_objects[label] + + if not best_obj.thumbnail_data: + return {} + + best = best_obj.thumbnail_data.copy() + best["frame"] = camera_state.frame_cache.get( + best_obj.thumbnail_data["frame_time"] + ) + return best + else: + return {} + + def get_current_frame( + self, camera: str, draw_options: dict[str, Any] = {} + ) -> np.ndarray | None: + if camera == "birdseye": + return self.frame_manager.get( + "birdseye", + (self.config.birdseye.height * 3 // 2, self.config.birdseye.width), + ) + + if camera not in self.camera_states: + return None + + return self.camera_states[camera].get_current_frame(draw_options) + + def get_current_frame_time(self, camera: str) -> float: + """Returns the latest frame time for a given camera.""" + if camera not in self.camera_states: + return 0.0 + + return self.camera_states[camera].current_frame_time + + def set_sub_label( + self, event_id: str, sub_label: str | None, score: float | None + ) -> None: + """Update sub label for given event id.""" + tracked_obj: TrackedObject | None = None + + for state in self.camera_states.values(): + tracked_obj = state.tracked_objects.get(event_id) + + if tracked_obj is not None: + break + + try: + event: Event | None = Event.get(Event.id == event_id) + except DoesNotExist: + event = None + + if not tracked_obj and not event: + return + + if tracked_obj: + tracked_obj.obj_data["sub_label"] = (sub_label, score) + + if event: + event.sub_label = sub_label # type: ignore[assignment] + data = event.data + if sub_label is None: + data["sub_label_score"] = None # type: ignore[index] + elif score is not None: + data["sub_label_score"] = score # type: ignore[index] + event.data = data + event.save() + + # update timeline items + Timeline.update( + data=Timeline.data.update({"sub_label": (sub_label, score)}) + ).where(Timeline.source_id == event_id).execute() + + # only update ended review segments + # manually updating a sub_label from the UI is only possible for ended tracked objects + try: + review_segment = ReviewSegment.get( + ( + SQL( + "json_extract(data, '$.detections') LIKE ?", + [f'%"{event_id}"%'], + ) + ) + & (ReviewSegment.end_time.is_null(False)) + ) + + segment_data = review_segment.data + detection_ids = segment_data.get("detections", []) + + # Rebuild objects list and sync sub_labels + objects_list = [] + sub_labels = set() + events = Event.select(Event.id, Event.label, Event.sub_label).where( + Event.id.in_(detection_ids) # type: ignore[call-arg, misc] + ) + for det_event in events: + if det_event.sub_label: + sub_labels.add(det_event.sub_label) + objects_list.append( + f"{det_event.label}-verified" + ) # eg, "bird-verified" + else: + objects_list.append(det_event.label) # eg, "bird" + + segment_data["sub_labels"] = list(sub_labels) + segment_data["objects"] = objects_list + + updated_data = { + ReviewSegment.id.name: review_segment.id, + ReviewSegment.camera.name: review_segment.camera, + ReviewSegment.start_time.name: review_segment.start_time, + ReviewSegment.end_time.name: review_segment.end_time, + ReviewSegment.severity.name: review_segment.severity, + ReviewSegment.thumb_path.name: review_segment.thumb_path, + ReviewSegment.data.name: segment_data, + } + + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, updated_data) + logger.debug( + f"Updated sub_label for event {event_id} in review segment {review_segment.id}" + ) + + except DoesNotExist: + logger.debug( + f"No review segment found with event ID {event_id} when updating sub_label" + ) + + def set_object_attribute( + self, + event_id: str, + field_name: str, + field_value: str | None, + score: float | None, + ) -> None: + """Update attribute for given event id.""" + tracked_obj: TrackedObject | None = None + + for state in self.camera_states.values(): + tracked_obj = state.tracked_objects.get(event_id) + + if tracked_obj is not None: + break + + try: + event: Event | None = Event.get(Event.id == event_id) + except DoesNotExist: + event = None + + if not tracked_obj and not event: + return + + if tracked_obj: + tracked_obj.obj_data[field_name] = ( + field_value, + score, + ) + + if event: + data = event.data + data[field_name] = field_value # type: ignore[index] + if field_value is None: + data[f"{field_name}_score"] = None # type: ignore[index] + elif score is not None: + data[f"{field_name}_score"] = score # type: ignore[index] + event.data = data + event.save() + + def save_lpr_snapshot(self, payload: tuple) -> None: + # save the snapshot image + (frame, event_id, camera) = payload + + img = cv2.imdecode( + np.frombuffer(base64.b64decode(frame), dtype=np.uint8), + cv2.IMREAD_COLOR, + ) + + self.camera_states[camera].save_manual_event_image( + img, event_id, "license_plate", {} + ) + + def create_manual_event(self, payload: tuple) -> None: + ( + frame_time, + camera_name, + label, + event_id, + include_recording, + score, + sub_label, + duration, + source_type, + draw, + pre_capture, + ) = payload + + # save the snapshot image + self.camera_states[camera_name].save_manual_event_image( + None, event_id, label, draw + ) + end_time = frame_time + duration if duration is not None else None + start_time = ( + frame_time - self.config.cameras[camera_name].record.event_pre_capture + if pre_capture is None + else frame_time - pre_capture + ) + + # send event to event maintainer + self.event_sender.publish( + ( + EventTypeEnum.api, + EventStateEnum.start, + camera_name, + "", + { + "id": event_id, + "label": label, + "sub_label": sub_label, + "score": score, + "camera": camera_name, + "start_time": start_time, + "end_time": end_time, + "has_clip": self.config.cameras[camera_name].record.enabled + and include_recording, + "has_snapshot": True, + "snapshot_clean": True, + "snapshot_frame_time": frame_time, + "type": source_type, + "draw": draw, + }, + ) + ) + + if source_type == "api": + self.ongoing_manual_events[event_id] = camera_name + self.detection_publisher.publish( + ( + camera_name, + frame_time, + { + "state": ( + ManualEventState.complete + if end_time + else ManualEventState.start + ), + "label": f"{label}: {sub_label}" if sub_label else label, + "event_id": event_id, + "end_time": end_time, + }, + ), + DetectionTypeEnum.api.value, + ) + + def create_lpr_event(self, payload: tuple) -> None: + ( + frame_time, + camera_name, + label, + event_id, + include_recording, + score, + sub_label, + plate, + ) = payload + + # send event to event maintainer + self.event_sender.publish( + ( + EventTypeEnum.api, + EventStateEnum.start, + camera_name, + "", + { + "id": event_id, + "label": label, + "sub_label": sub_label, + "score": score, + "camera": camera_name, + "start_time": frame_time + - self.config.cameras[camera_name].record.event_pre_capture, + "end_time": None, + "has_clip": self.config.cameras[camera_name].record.enabled + and include_recording, + "has_snapshot": True, + "snapshot_clean": True, + "type": "api", + "recognized_license_plate": plate, + "recognized_license_plate_score": score, + }, + ) + ) + + self.ongoing_manual_events[event_id] = camera_name + self.detection_publisher.publish( + ( + camera_name, + frame_time, + { + "state": ManualEventState.start, + "label": f"{label}: {sub_label}" if sub_label else label, + "event_id": event_id, + "end_time": None, + }, + ), + DetectionTypeEnum.lpr.value, + ) + + def end_manual_event(self, payload: tuple) -> None: + (event_id, end_time) = payload + + self.event_sender.publish( + ( + EventTypeEnum.api, + EventStateEnum.end, + None, + "", + {"id": event_id, "end_time": end_time}, + ) + ) + + if event_id in self.ongoing_manual_events: + self.detection_publisher.publish( + ( + self.ongoing_manual_events[event_id], + end_time, + { + "state": ManualEventState.end, + "event_id": event_id, + "end_time": end_time, + }, + ), + DetectionTypeEnum.api.value, + ) + self.ongoing_manual_events.pop(event_id) + + def force_end_all_events(self, camera: str, camera_state: CameraState) -> None: + """Ends all active events on camera when disabling.""" + last_frame_name = camera_state.previous_frame_id + for obj_id, obj in list(camera_state.tracked_objects.items()): + if "end_time" not in obj.obj_data: + logger.debug(f"Camera {camera} disabled, ending active event {obj_id}") + obj.obj_data["end_time"] = datetime.datetime.now().timestamp() + # end callbacks + for callback in camera_state.callbacks["end"]: + callback(camera, obj, last_frame_name) + + # camera activity callbacks + for callback in camera_state.callbacks["camera_activity"]: + callback( + camera, + {"enabled": False, "motion": 0, "objects": []}, + ) + + def run(self) -> None: + while not self.stop_event.is_set(): + # check for config updates + updated_topics = self.camera_config_subscriber.check_for_updates() + + if "enabled" in updated_topics: + for camera in updated_topics["enabled"]: + if self.camera_states[camera].prev_enabled is None: + self.camera_states[camera].prev_enabled = self.config.cameras[ + camera + ].enabled + elif "add" in updated_topics: + for camera in updated_topics["add"]: + self.config.cameras[camera] = ( + self.camera_config_subscriber.camera_configs[camera] + ) + self.create_camera_state(camera) + elif "remove" in updated_topics: + for camera in updated_topics["remove"]: + removed_camera_state = self.camera_states[camera] + removed_camera_state.shutdown() + self.camera_states.pop(camera) + self.camera_activity.pop(camera, None) + self.last_motion_detected.pop(camera, None) + + self.requestor.send_data(UPDATE_CAMERA_ACTIVITY, self.camera_activity) + + # manage camera disabled state + for camera, config in self.config.cameras.items(): + if not config.enabled_in_config: + continue + + current_enabled = config.enabled + camera_state = self.camera_states.get(camera) + if camera_state is None: + continue + + camera_state = self.camera_states[camera] + + if camera_state.prev_enabled and not current_enabled: + logger.debug(f"Not processing objects for disabled camera {camera}") + self.force_end_all_events(camera, camera_state) + + camera_state.prev_enabled = current_enabled + + if not current_enabled: + continue + + # check for sub label updates + while True: + update = self.sub_label_subscriber.check_for_update(timeout=0) + + if not update: + break + + (raw_topic, payload) = update + + if not raw_topic or not payload: + break + + topic = str(raw_topic) + + if topic.endswith(EventMetadataTypeEnum.sub_label.value): + (event_id, sub_label, score) = payload + self.set_sub_label(event_id, sub_label, score) + if topic.endswith(EventMetadataTypeEnum.attribute.value): + (event_id, field_name, field_value, score) = payload + self.set_object_attribute(event_id, field_name, field_value, score) + elif topic.endswith(EventMetadataTypeEnum.lpr_event_create.value): + self.create_lpr_event(payload) + elif topic.endswith(EventMetadataTypeEnum.save_lpr_snapshot.value): + self.save_lpr_snapshot(payload) + elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value): + self.create_manual_event(payload) + elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value): + self.end_manual_event(payload) + + try: + ( + camera, + frame_name, + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) = self.tracked_objects_queue.get(True, 1) + except queue.Empty: + continue + + camera_config = self.config.cameras.get(camera) + if camera_config is None: + continue + + if not camera_config.enabled: + logger.debug(f"Camera {camera} disabled, skipping update") + continue + + camera_state = self.camera_states.get(camera) + if camera_state is None: + continue + + camera_state.update( + frame_name, frame_time, current_tracked_objects, motion_boxes, regions + ) + + self.update_mqtt_motion(camera, frame_time, motion_boxes) + + tracked_objects = [ + o.to_dict() for o in camera_state.tracked_objects.values() + ] + + # publish info on this frame + self.detection_publisher.publish( + ( + camera, + frame_name, + frame_time, + tracked_objects, + motion_boxes, + regions, + ), + DetectionTypeEnum.video.value, + ) + + # cleanup event finished queue + while not self.stop_event.is_set(): + update = self.event_end_subscriber.check_for_update( + timeout=FAST_QUEUE_TIMEOUT + ) + + if not update: + break + + event_id, camera, _ = update + self.camera_states[camera].finished(event_id) + + # shut down camera states + for state in self.camera_states.values(): + state.shutdown() + + self.requestor.stop() + self.detection_publisher.stop() + self.event_sender.stop() + self.event_end_subscriber.stop() + self.sub_label_subscriber.stop() + self.camera_config_subscriber.stop() + + logger.info("Exiting object processor...") diff --git a/frigate/track/stationary_classifier.py b/frigate/track/stationary_classifier.py new file mode 100644 index 0000000..1e22ec6 --- /dev/null +++ b/frigate/track/stationary_classifier.py @@ -0,0 +1,268 @@ +"""Tools for determining if an object is stationary.""" + +import logging +from dataclasses import dataclass, field +from typing import Any, cast + +import cv2 +import numpy as np +from scipy.ndimage import gaussian_filter + +logger = logging.getLogger(__name__) + + +@dataclass +class StationaryThresholds: + """IOU thresholds and history parameters for stationary object classification. + + This allows different sensitivity settings for different object types. + """ + + # Objects to apply these thresholds to + # If None, apply to all objects + objects: list[str] = field(default_factory=list) + + # Threshold of IoU that causes the object to immediately be considered active + # Below this threshold, assume object is active + known_active_iou: float = 0.2 + + # IOU threshold for checking if stationary object has moved + # If mean and median IOU drops below this, assume object is no longer stationary + stationary_check_iou: float = 0.6 + + # IOU threshold for checking if active object has changed position + # Higher threshold makes it more difficult for the object to be considered stationary + active_check_iou: float = 0.9 + + # Maximum number of bounding boxes to keep in stationary history + max_stationary_history: int = 10 + + # Whether to use the motion classifier + motion_classifier_enabled: bool = False + + +# Thresholds for objects that are expected to be stationary +STATIONARY_OBJECT_THRESHOLDS = StationaryThresholds( + objects=["bbq_grill", "package", "waste_bin"], + known_active_iou=0.0, + motion_classifier_enabled=True, +) + +# Thresholds for objects that are active but can be stationary for longer periods of time +DYNAMIC_OBJECT_THRESHOLDS = StationaryThresholds( + objects=["bicycle", "boat", "car", "motorcycle", "tractor", "truck"], + active_check_iou=0.75, + motion_classifier_enabled=True, +) + +# Thresholds for objects that are not expected to be stationary +NON_STATIONARY_OBJECT_THRESHOLDS = StationaryThresholds( + objects=["license_plate"], + known_active_iou=0.9, + stationary_check_iou=0.9, + max_stationary_history=4, +) + +# Default thresholds for any other object label +DEFAULT_OBJECT_THRESHOLDS = StationaryThresholds() + + +def get_stationary_threshold(label: str) -> StationaryThresholds: + """Get the stationary thresholds for a given object label.""" + + if label in STATIONARY_OBJECT_THRESHOLDS.objects: + return STATIONARY_OBJECT_THRESHOLDS + + if label in DYNAMIC_OBJECT_THRESHOLDS.objects: + return DYNAMIC_OBJECT_THRESHOLDS + + if label in NON_STATIONARY_OBJECT_THRESHOLDS.objects: + return NON_STATIONARY_OBJECT_THRESHOLDS + + return DEFAULT_OBJECT_THRESHOLDS + + +class StationaryMotionClassifier: + """Fallback classifier to prevent false flips from stationary to active. + + Uses appearance consistency on a fixed spatial region (historical median box) + to detect actual movement, ignoring bounding box detection variations. + """ + + CROP_SIZE = 96 + NCC_KEEP_THRESHOLD = 0.90 # High correlation = keep stationary + NCC_ACTIVE_THRESHOLD = 0.85 # Low correlation = consider active + SHIFT_KEEP_THRESHOLD = 0.02 # Small shift = keep stationary + SHIFT_ACTIVE_THRESHOLD = 0.04 # Large shift = consider active + DRIFT_ACTIVE_THRESHOLD = 0.12 # Cumulative drift over 5 frames + CHANGED_FRAMES_TO_FLIP = 2 + + def __init__(self) -> None: + self.anchor_crops: dict[str, np.ndarray] = {} + self.anchor_boxes: dict[str, tuple[int, int, int, int]] = {} + self.changed_counts: dict[str, int] = {} + self.shift_histories: dict[str, list[float]] = {} + + # Pre-compute Hanning window for phase correlation + hann = np.hanning(self.CROP_SIZE).astype(np.float64) + self._hann2d = np.outer(hann, hann) + + def reset(self, id: str) -> None: + logger.debug("StationaryMotionClassifier.reset: id=%s", id) + if id in self.anchor_crops: + del self.anchor_crops[id] + if id in self.anchor_boxes: + del self.anchor_boxes[id] + self.changed_counts[id] = 0 + self.shift_histories[id] = [] + + def _extract_y_crop( + self, yuv_frame: np.ndarray, box: tuple[int, int, int, int] + ) -> np.ndarray: + """Extract and normalize Y-plane crop from bounding box.""" + y_height = yuv_frame.shape[0] // 3 * 2 + width = yuv_frame.shape[1] + x1 = max(0, min(width - 1, box[0])) + y1 = max(0, min(y_height - 1, box[1])) + x2 = max(0, min(width - 1, box[2])) + y2 = max(0, min(y_height - 1, box[3])) + + if x2 <= x1: + x2 = min(width - 1, x1 + 1) + if y2 <= y1: + y2 = min(y_height - 1, y1 + 1) + + # Extract Y-plane crop, resize, and blur + y_plane = yuv_frame[0:y_height, 0:width] + crop = y_plane[y1:y2, x1:x2] + crop_resized = cv2.resize( + crop, (self.CROP_SIZE, self.CROP_SIZE), interpolation=cv2.INTER_AREA + ) + result = cast(np.ndarray[Any, Any], gaussian_filter(crop_resized, sigma=0.5)) + logger.debug( + "_extract_y_crop: box=%s clamped=(%d,%d,%d,%d) crop_shape=%s", + box, + x1, + y1, + x2, + y2, + crop.shape if "crop" in locals() else None, + ) + return result + + def ensure_anchor( + self, id: str, yuv_frame: np.ndarray, median_box: tuple[int, int, int, int] + ) -> None: + """Initialize anchor crop from stable median box when object becomes stationary.""" + if id not in self.anchor_crops: + self.anchor_boxes[id] = median_box + self.anchor_crops[id] = self._extract_y_crop(yuv_frame, median_box) + self.changed_counts[id] = 0 + self.shift_histories[id] = [] + logger.debug( + "ensure_anchor: initialized id=%s median_box=%s crop_shape=%s", + id, + median_box, + self.anchor_crops[id].shape, + ) + + def on_active(self, id: str) -> None: + """Reset state when object becomes active to allow re-anchoring.""" + logger.debug("on_active: id=%s became active; resetting state", id) + self.reset(id) + + def evaluate( + self, id: str, yuv_frame: np.ndarray, current_box: tuple[int, int, int, int] + ) -> bool: + """Return True to keep stationary, False to flip to active. + + Compares the same spatial region (historical median box) across frames + to detect actual movement, ignoring bounding box variations. + """ + + if id not in self.anchor_crops or id not in self.anchor_boxes: + logger.debug("evaluate: id=%s has no anchor; default keep stationary", id) + return True + + # Compare same spatial region across frames + anchor_box = self.anchor_boxes[id] + anchor_crop = self.anchor_crops[id] + curr_crop = self._extract_y_crop(yuv_frame, anchor_box) + + # Compute appearance and motion metrics + ncc = cv2.matchTemplate(curr_crop, anchor_crop, cv2.TM_CCOEFF_NORMED)[0, 0] + a64 = anchor_crop.astype(np.float64) * self._hann2d + c64 = curr_crop.astype(np.float64) * self._hann2d + (shift_x, shift_y), _ = cv2.phaseCorrelate(a64, c64) + shift_norm = float(np.hypot(shift_x, shift_y)) / float(self.CROP_SIZE) + + logger.debug( + "evaluate: id=%s metrics ncc=%.4f shift_norm=%.4f (shift_x=%.3f, shift_y=%.3f)", + id, + float(ncc), + shift_norm, + float(shift_x), + float(shift_y), + ) + + # Update rolling shift history + history = self.shift_histories.get(id, []) + history.append(shift_norm) + if len(history) > 5: + history = history[-5:] + self.shift_histories[id] = history + drift_sum = float(sum(history)) + + logger.debug( + "evaluate: id=%s history_len=%d last_shift=%.4f drift_sum=%.4f", + id, + len(history), + history[-1] if history else -1.0, + drift_sum, + ) + + # Early exit for clear stationary case + if ncc >= self.NCC_KEEP_THRESHOLD and shift_norm < self.SHIFT_KEEP_THRESHOLD: + self.changed_counts[id] = 0 + logger.debug( + "evaluate: id=%s early-stationary keep=True (ncc>=%.2f and shift<%.2f)", + id, + self.NCC_KEEP_THRESHOLD, + self.SHIFT_KEEP_THRESHOLD, + ) + return True + + # Check for movement indicators + movement_detected = ( + ncc < self.NCC_ACTIVE_THRESHOLD + or shift_norm >= self.SHIFT_ACTIVE_THRESHOLD + or drift_sum >= self.DRIFT_ACTIVE_THRESHOLD + ) + + if movement_detected: + cnt = self.changed_counts.get(id, 0) + 1 + self.changed_counts[id] = cnt + if ( + cnt >= self.CHANGED_FRAMES_TO_FLIP + or drift_sum >= self.DRIFT_ACTIVE_THRESHOLD + ): + logger.debug( + "evaluate: id=%s flip_to_active=True cnt=%d drift_sum=%.4f thresholds(changed>=%d drift>=%.2f)", + id, + cnt, + drift_sum, + self.CHANGED_FRAMES_TO_FLIP, + self.DRIFT_ACTIVE_THRESHOLD, + ) + return False + logger.debug( + "evaluate: id=%s movement_detected cnt=%d keep_until_cnt>=%d", + id, + cnt, + self.CHANGED_FRAMES_TO_FLIP, + ) + else: + self.changed_counts[id] = 0 + logger.debug("evaluate: id=%s no_movement keep=True", id) + + return True diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py new file mode 100644 index 0000000..0a9c6f7 --- /dev/null +++ b/frigate/track/tracked_object.py @@ -0,0 +1,623 @@ +"""Object attribute.""" + +import logging +import math +import os +from collections import defaultdict +from statistics import median +from typing import Any, cast + +import cv2 +import numpy as np + +from frigate.config import ( + CameraConfig, + FilterConfig, + UIConfig, +) +from frigate.const import CLIPS_DIR, REPLAY_CAMERA_PREFIX, THUMB_DIR +from frigate.detectors.detector_config import ModelConfig +from frigate.review.types import SeverityEnum +from frigate.util.builtin import sanitize_float +from frigate.util.image import ( + area, + get_snapshot_bytes, + is_better_thumbnail, +) +from frigate.util.object import box_inside +from frigate.util.velocity import calculate_real_world_speed + +logger = logging.getLogger(__name__) + + +# In most cases objects that loiter in a loitering zone should alert, +# but can still be expected to stay stationary for extended periods of time +# (ex: car loitering on the street vs when a known person parks on the street) +# person is the main object that should keep alerts going as long as they loiter +# even if they are stationary. +EXTENDED_LOITERING_OBJECTS = ["person"] + + +class TrackedObject: + def __init__( + self, + model_config: ModelConfig, + camera_config: CameraConfig, + ui_config: UIConfig, + frame_cache: dict[float, dict[str, Any]], + obj_data: dict[str, Any], + ) -> None: + # set the score history then remove as it is not part of object state + self.score_history: list[float] = obj_data["score_history"] + del obj_data["score_history"] + + self.obj_data = obj_data + self.colormap = model_config.colormap + self.logos = model_config.all_attribute_logos + self.camera_config = camera_config + self.ui_config = ui_config + self.frame_cache = frame_cache + self.zone_presence: dict[str, int] = {} + self.zone_loitering: dict[str, int] = {} + self.current_zones: list[str] = [] + self.entered_zones: list[str] = [] + self.new_zone_entered: bool = False + self.attributes: dict[str, float] = defaultdict(float) + self.false_positive = True + self.has_clip = False + self.has_snapshot = False + self.top_score = self.computed_score = 0.0 + self.thumbnail_data: dict[str, Any] | None = None + self.last_updated: float = 0 + self.last_published: float = 0 + self.frame = None + self.active = True + self.pending_loitering = False + self.speed_history: list[float] = [] + self.current_estimated_speed: float = 0 + self.average_estimated_speed: float = 0 + self.velocity_angle = 0 + self.path_data: list[tuple[Any, float]] = [] + self.previous = self.to_dict() + + @property + def max_severity(self) -> str | None: + review_config = self.camera_config.review + + if ( + self.camera_config.review.alerts.enabled + and self.obj_data["label"] in review_config.alerts.labels + and ( + not review_config.alerts.required_zones + or set(self.entered_zones) & set(review_config.alerts.required_zones) + ) + ): + return SeverityEnum.alert + + if ( + self.camera_config.review.detections.enabled + and ( + not review_config.detections.labels + or self.obj_data["label"] in review_config.detections.labels + ) + and ( + not review_config.detections.required_zones + or set(self.entered_zones) + & set(review_config.detections.required_zones) + ) + ): + return SeverityEnum.detection + + return None + + def _is_false_positive(self) -> bool: + # once a true positive, always a true positive + if not self.false_positive: + return False + + threshold = self.camera_config.objects.filters[self.obj_data["label"]].threshold + return self.computed_score < threshold + + def compute_score(self) -> float: + """get median of scores for object.""" + return median(self.score_history) + + def update( + self, current_frame_time: float, obj_data: dict[str, Any], has_valid_frame: bool + ) -> tuple[bool, bool, bool, bool]: + thumb_update = False + significant_change = False + path_update = False + autotracker_update = False + # if the object is not in the current frame, add a 0.0 to the score history + if obj_data["frame_time"] != current_frame_time: + self.score_history.append(0.0) + else: + self.score_history.append(obj_data["score"]) + + # only keep the last 10 scores + if len(self.score_history) > 10: + self.score_history = self.score_history[-10:] + + # calculate if this is a false positive + self.computed_score = self.compute_score() + if self.computed_score > self.top_score: + self.top_score = self.computed_score + self.false_positive = self._is_false_positive() + self.active = self.is_active() + + if not self.false_positive and has_valid_frame: + # determine if this frame is a better thumbnail + if self.thumbnail_data is None or is_better_thumbnail( + self.obj_data["label"], + self.thumbnail_data, + obj_data, + self.camera_config.frame_shape, + ): + if obj_data["frame_time"] == current_frame_time: + self.thumbnail_data = { + "frame_time": obj_data["frame_time"], + "box": obj_data["box"], + "area": obj_data["area"], + "region": obj_data["region"], + "score": obj_data["score"], + "attributes": obj_data["attributes"], + "current_estimated_speed": self.current_estimated_speed, + "velocity_angle": self.velocity_angle, + "path_data": self.path_data.copy(), + "recognized_license_plate": obj_data.get( + "recognized_license_plate" + ), + "recognized_license_plate_score": obj_data.get( + "recognized_license_plate_score" + ), + } + thumb_update = True + else: + logger.debug( + f"{self.camera_config.name}: Object frame time {obj_data['frame_time']} is not equal to the current frame time {current_frame_time}, not updating thumbnail" + ) + + # check zones + current_zones = [] + bottom_center = (obj_data["centroid"][0], obj_data["box"][3]) + in_loitering_zone = False + in_speed_zone = False + + # check each zone + for name, zone in self.camera_config.zones.items(): + # skip disabled zones + if not zone.enabled: + continue + + # if the zone is not for this object type, skip + if len(zone.objects) > 0 and obj_data["label"] not in zone.objects: + continue + contour = zone.contour + zone_score = self.zone_presence.get(name, 0) + 1 + + # check if the object is in the zone + if cv2.pointPolygonTest(contour, bottom_center, False) >= 0: + # if the object passed the filters once, dont apply again + if name in self.current_zones or not zone_filtered(self, zone.filters): + # Calculate speed first if this is a speed zone + if ( + zone.distances + and obj_data["frame_time"] == current_frame_time + and self.active + ): + speed_magnitude, self.velocity_angle = ( + calculate_real_world_speed( + zone.contour, + zone.distances, + self.obj_data["estimate_velocity"], + bottom_center, + self.camera_config.detect.fps, + ) + ) + + # users can configure speed zones incorrectly, so sanitize speed_magnitude + # and velocity_angle in case the values come back as inf or NaN + speed_magnitude = sanitize_float(speed_magnitude) + self.velocity_angle = sanitize_float(self.velocity_angle) + + if self.ui_config.unit_system == "metric": + self.current_estimated_speed = ( + speed_magnitude * 3.6 + ) # m/s to km/h + else: + self.current_estimated_speed = ( + speed_magnitude * 0.681818 + ) # ft/s to mph + + self.speed_history.append(self.current_estimated_speed) + if len(self.speed_history) > 10: + self.speed_history = self.speed_history[-10:] + + self.average_estimated_speed = sum(self.speed_history) / len( + self.speed_history + ) + + # we've exceeded the speed threshold on the zone + # or we don't have a speed threshold set + if ( + zone.speed_threshold is None + or self.average_estimated_speed > zone.speed_threshold + ): + in_speed_zone = True + + logger.debug( + f"Camera: {self.camera_config.name}, tracked object ID: {self.obj_data['id']}, " + f"zone: {name}, pixel velocity: {str(tuple(np.round(self.obj_data['estimate_velocity']).flatten().astype(int)))}, " + f"speed magnitude: {speed_magnitude}, velocity angle: {self.velocity_angle}, " + f"estimated speed: {self.current_estimated_speed:.1f}, " + f"average speed: {self.average_estimated_speed:.1f}, " + f"length: {len(self.speed_history)}" + ) + + # Check zone entry conditions - for speed zones, require both inertia and speed + if zone_score >= zone.inertia: + if zone.distances and not in_speed_zone: + continue # Skip zone entry for speed zones until speed threshold met + + # if the zone has loitering time, and the object is an extended loiter object + # always mark it as loitering actively + if ( + self.obj_data["label"] in EXTENDED_LOITERING_OBJECTS + and zone.loitering_time > 0 + ): + in_loitering_zone = True + + loitering_score = self.zone_loitering.get(name, 0) + 1 + + # loitering time is configured as seconds, convert to count of frames + if loitering_score >= ( + self.camera_config.zones[name].loitering_time + * self.camera_config.detect.fps + ): + current_zones.append(name) + + if name not in self.entered_zones: + self.entered_zones.append(name) + self.new_zone_entered = True + else: + self.zone_loitering[name] = loitering_score + + # this object is pending loitering but has not entered the zone yet + if zone.loitering_time > 0: + in_loitering_zone = True + else: + self.zone_presence[name] = zone_score + else: + # once an object has a zone inertia of 3+ it is not checked anymore + if 0 < zone_score < zone.inertia: + self.zone_presence[name] = zone_score - 1 + + # Reset speed if not in speed zone + if zone.distances and name not in current_zones: + self.current_estimated_speed = 0 + + # update loitering status + self.pending_loitering = in_loitering_zone + + # maintain attributes + for attr in obj_data["attributes"]: + if self.attributes[attr["label"]] < attr["score"]: + self.attributes[attr["label"]] = attr["score"] + + # populate the sub_label for object with highest scoring logo + if self.obj_data["label"] in ["car", "motorcycle", "package", "person"]: + recognized_logos = { + k: self.attributes[k] for k in self.logos if k in self.attributes + } + if len(recognized_logos) > 0: + max_logo = max(recognized_logos, key=recognized_logos.get) # type: ignore[arg-type] + + # don't overwrite sub label if it is already set + if ( + self.obj_data.get("sub_label") is None + or self.obj_data["sub_label"][0] == max_logo + ): + self.obj_data["sub_label"] = (max_logo, recognized_logos[max_logo]) + + # check for significant change + if not self.false_positive: + # if the zones changed, signal an update + if set(self.current_zones) != set(current_zones): + significant_change = True + + # if the position changed, signal an update + if self.obj_data["position_changes"] != obj_data["position_changes"]: + significant_change = True + + # disappearance of a per-frame attribute can be caused by detection + # skipping the object on a frame (stationary objects on non-interval + # frames), so only flag when a new attribute label appears + prev_labels = {a["label"] for a in self.obj_data["attributes"]} + curr_labels = {a["label"] for a in obj_data["attributes"]} + if curr_labels - prev_labels: + significant_change = True + + # if the state changed between stationary and active + if self.previous["active"] != self.active: + significant_change = True + + # update at least once per minute + if self.obj_data["frame_time"] - self.previous["frame_time"] > 60: + significant_change = True + + # update autotrack at most 3 objects per second + if self.obj_data["frame_time"] - self.previous["frame_time"] >= (1 / 3): + autotracker_update = True + + # update path + width = self.camera_config.detect.width + height = self.camera_config.detect.height + + if width is not None and height is not None: + bottom_center = ( + round(obj_data["centroid"][0] / width, 4), + round(obj_data["box"][3] / height, 4), + ) + + # calculate a reasonable movement threshold (e.g., 5% of the frame diagonal) + threshold = 0.05 * math.sqrt(width**2 + height**2) / max(width, height) + + if not self.path_data: + self.path_data.append((bottom_center, obj_data["frame_time"])) + path_update = True + elif ( + math.dist(self.path_data[-1][0], bottom_center) >= threshold + or len(self.path_data) == 1 + ): + # check Euclidean distance before appending + self.path_data.append((bottom_center, obj_data["frame_time"])) + path_update = True + logger.debug( + f"Point tracking: {obj_data['id']}, {bottom_center}, {obj_data['frame_time']}" + ) + + self.obj_data.update(obj_data) + self.current_zones = current_zones + logger.debug( + f"{self.camera_config.name}: Updating {obj_data['id']}: thumb update? {thumb_update}, significant change? {significant_change}, path update? {path_update}, autotracker update? {autotracker_update} " + ) + return (thumb_update, significant_change, path_update, autotracker_update) + + def to_dict(self) -> dict[str, Any]: + # Tracking internals excluded from output (centroid, estimate, estimate_velocity) + _EXCLUDED_OBJ_DATA_KEYS = { + "centroid", + "estimate", + "estimate_velocity", + } + + event: dict[str, Any] = { + "id": self.obj_data["id"], + "camera": self.camera_config.name, + "frame_time": self.obj_data["frame_time"], + "snapshot": self.thumbnail_data, + "snapshot_clean": True, + "label": self.obj_data["label"], + "sub_label": self.obj_data.get("sub_label"), + "top_score": self.top_score, + "false_positive": self.false_positive, + "start_time": self.obj_data["start_time"], + "end_time": self.obj_data.get("end_time", None), + "score": self.obj_data["score"], + "computed_score": self.computed_score, + "box": self.obj_data["box"], + "area": self.obj_data["area"], + "ratio": self.obj_data["ratio"], + "region": self.obj_data["region"], + "active": self.active, + "stationary": not self.active, + "motionless_count": self.obj_data["motionless_count"], + "position_changes": self.obj_data["position_changes"], + "current_zones": self.current_zones.copy(), + "entered_zones": self.entered_zones.copy(), + "has_clip": self.has_clip, + "has_snapshot": self.has_snapshot, + "attributes": self.attributes, + "current_attributes": self.obj_data["attributes"], + "pending_loitering": self.pending_loitering, + "max_severity": self.max_severity, + "current_estimated_speed": self.current_estimated_speed, + "average_estimated_speed": self.average_estimated_speed, + "velocity_angle": self.velocity_angle, + "path_data": self.path_data.copy(), + "recognized_license_plate": self.obj_data.get("recognized_license_plate"), + } + + # Add any other obj_data keys (e.g. custom attribute fields) not yet included + for key, value in self.obj_data.items(): + if key not in _EXCLUDED_OBJ_DATA_KEYS and key not in event: + event[key] = value + + return event + + def is_active(self) -> bool: + return not self.is_stationary() + + def is_stationary(self) -> bool: + count = cast(int | float, self.obj_data["motionless_count"]) + return count > (self.camera_config.detect.stationary.threshold or 50) + + def get_thumbnail(self, ext: str) -> bytes | None: + img_bytes, _ = self.get_img_bytes( + ext, timestamp=False, bounding_box=False, crop=True, height=175 + ) + + if img_bytes: + return img_bytes + else: + _, img = cv2.imencode(f".{ext}", np.zeros((175, 175, 3), np.uint8)) + return img.tobytes() + + def get_clean_webp(self) -> bytes | None: + webp_bytes, _ = self.get_img_bytes( + ext="webp", + timestamp=False, + bounding_box=False, + crop=False, + height=None, + quality=self.camera_config.snapshots.quality, + ) + return webp_bytes + + def get_img_bytes( + self, + ext: str, + timestamp: bool = False, + bounding_box: bool = False, + crop: bool = False, + height: int | None = None, + quality: int | None = None, + ) -> tuple[bytes | None, float | None]: + if self.thumbnail_data is None: + return None, None + + try: + frame_time = self.thumbnail_data["frame_time"] + best_frame = cv2.cvtColor( + self.frame_cache[frame_time]["frame"], + cv2.COLOR_YUV2BGR_I420, + ) + except KeyError: + logger.warning( + f"Unable to create snapshot because frame {frame_time} is not in the cache" + ) + return None, None + + return get_snapshot_bytes( + best_frame, + frame_time, + ext=ext, + timestamp=timestamp, + bounding_box=bounding_box, + crop=crop, + height=height, + quality=quality, + label=self.obj_data["label"], + box=self.thumbnail_data["box"], + score=self.thumbnail_data["score"], + area=self.thumbnail_data["area"], + attributes=self.thumbnail_data["attributes"], + color=self.colormap.get(self.obj_data["label"], (255, 255, 255)), + timestamp_style=self.camera_config.timestamp_style, + estimated_speed=self.thumbnail_data["current_estimated_speed"], + ) + + def write_snapshot_to_disk(self) -> None: + webp_bytes = self.get_clean_webp() + if webp_bytes is None: + logger.warning(f"Unable to save snapshot for {self.obj_data['id']}.") + else: + with open( + os.path.join( + CLIPS_DIR, + f"{self.camera_config.name}-{self.obj_data['id']}-clean.webp", + ), + "wb", + ) as p: + p.write(webp_bytes) + + def write_thumbnail_to_disk(self) -> None: + if not self.camera_config.name: + return + + if self.camera_config.name.startswith(REPLAY_CAMERA_PREFIX): + return + + directory = os.path.join(THUMB_DIR, self.camera_config.name) + + os.makedirs(directory, exist_ok=True) + + thumb_bytes = self.get_thumbnail("webp") + + if thumb_bytes: + with open( + os.path.join(directory, f"{self.obj_data['id']}.webp"), "wb" + ) as f: + f.write(thumb_bytes) + + +def zone_filtered(obj: TrackedObject, object_config: dict[str, FilterConfig]) -> bool: + object_name = obj.obj_data["label"] + + if object_name in object_config: + obj_settings = object_config[object_name] + + # if the min area is larger than the + # detected object, don't add it to detected objects + if obj_settings.min_area > obj.obj_data["area"]: + return True + + # if the detected object is larger than the + # max area, don't add it to detected objects + if obj_settings.max_area < obj.obj_data["area"]: + return True + + # if the score is lower than the threshold, skip + if obj_settings.threshold > obj.computed_score: + return True + + # if the object is not proportionally wide enough + if obj_settings.min_ratio > obj.obj_data["ratio"]: + return True + + # if the object is proportionally too wide + if obj_settings.max_ratio < obj.obj_data["ratio"]: + return True + + return False + + +class TrackedObjectAttribute: + def __init__(self, raw_data: tuple) -> None: + self.label = raw_data[0] + self.score = raw_data[1] + self.box = raw_data[2] + self.area = raw_data[3] + self.ratio = raw_data[4] + self.region = raw_data[5] + + def get_tracking_data(self) -> dict[str, Any]: + """Return data saved to the object.""" + return { + "label": self.label, + "score": self.score, + "box": self.box, + } + + def find_best_object(self, objects: list[dict[str, Any]]) -> str | None: + """Find the best attribute for each object and return its ID.""" + best_object_area: float | None = None + best_object_id: str | None = None + best_object_label: str | None = None + + for obj in objects: + if not box_inside(obj["box"], self.box): + continue + + object_area = area(obj["box"]) + + # if multiple objects have the same attribute then they + # are overlapping, it is most likely that the smaller object + # is the one with the attribute + if best_object_area is None: + best_object_area = object_area + best_object_id = obj["id"] + best_object_label = obj["label"] + else: + if best_object_label == obj["label"]: + # if multiple objects of the same type are overlapping + # then the attribute will not be assigned + return None + elif object_area < best_object_area: + # if a car and person are overlapping then assign the label to the smaller object (which should be the person) + best_object_area = object_area + best_object_id = obj["id"] + best_object_label = obj["label"] + + return best_object_id diff --git a/frigate/types.py b/frigate/types.py new file mode 100644 index 0000000..77bb508 --- /dev/null +++ b/frigate/types.py @@ -0,0 +1,42 @@ +from enum import Enum +from typing import TypedDict + +from frigate.camera import CameraMetrics +from frigate.data_processing.types import DataProcessorMetrics +from frigate.object_detection.base import ObjectDetectProcess + + +class StatsTrackingTypes(TypedDict): + camera_metrics: dict[str, CameraMetrics] + embeddings_metrics: DataProcessorMetrics | None + detectors: dict[str, ObjectDetectProcess] + started: int + latest_frigate_version: str + last_updated: int + processes: dict[str, int] + + +class ModelStatusTypesEnum(str, Enum): + not_downloaded = "not_downloaded" + downloading = "downloading" + downloaded = "downloaded" + error = "error" + training = "training" + complete = "complete" + failed = "failed" + + +class JobStatusTypesEnum(str, Enum): + pending = "pending" + queued = "queued" + running = "running" + success = "success" + failed = "failed" + cancelled = "cancelled" + + +class TrackedObjectUpdateTypesEnum(str, Enum): + description = "description" + face = "face" + lpr = "lpr" + classification = "classification" diff --git a/frigate/util/__init__.py b/frigate/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frigate/util/audio.py b/frigate/util/audio.py new file mode 100644 index 0000000..28fb0a6 --- /dev/null +++ b/frigate/util/audio.py @@ -0,0 +1,115 @@ +"""Utilities for creating and manipulating audio.""" + +import logging +import os +import subprocess as sp + +from pathvalidate import sanitize_filename + +from frigate.const import CACHE_DIR +from frigate.models import Recordings + +logger = logging.getLogger(__name__) + + +def get_audio_from_recording( + ffmpeg, + camera_name: str, + start_ts: float, + end_ts: float, + sample_rate: int = 16000, +) -> bytes | None: + """Extract audio from recording files between start_ts and end_ts in WAV format suitable for sherpa-onnx. + + Args: + ffmpeg: FFmpeg configuration object + camera_name: Name of the camera + start_ts: Start timestamp in seconds + end_ts: End timestamp in seconds + sample_rate: Sample rate for output audio (default 16kHz for sherpa-onnx) + + Returns: + Bytes of WAV audio data or None if extraction failed + """ + # Fetch all relevant recording segments + recordings = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + Recordings.end_time, + ) + .where( + (Recordings.start_time.between(start_ts, end_ts)) + | (Recordings.end_time.between(start_ts, end_ts)) + | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.asc()) + ) + + if not recordings: + logger.debug( + f"No recordings found for {camera_name} between {start_ts} and {end_ts}" + ) + return None + + # Generate concat playlist file + file_name = sanitize_filename( + f"audio_playlist_{camera_name}_{start_ts}-{end_ts}.txt" + ) + file_path = os.path.join(CACHE_DIR, file_name) + try: + with open(file_path, "w") as file: + for clip in recordings: + file.write(f"file '{clip.path}'\n") + if clip.start_time < start_ts: + file.write(f"inpoint {int(start_ts - clip.start_time)}\n") + if clip.end_time > end_ts: + file.write(f"outpoint {int(end_ts - clip.start_time)}\n") + + ffmpeg_cmd = [ + ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-protocol_whitelist", + "pipe,file", + "-f", + "concat", + "-safe", + "0", + "-i", + file_path, + "-vn", # No video + "-acodec", + "pcm_s16le", # 16-bit PCM encoding + "-ar", + str(sample_rate), + "-ac", + "1", # Mono audio + "-f", + "wav", + "-", + ] + + process = sp.run( + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode == 0: + logger.debug( + f"Successfully extracted audio for {camera_name} from {start_ts} to {end_ts}" + ) + return process.stdout + else: + logger.error(f"Failed to extract audio: {process.stderr.decode()}") + return None + except Exception as e: + logger.error(f"Error extracting audio from recordings: {e}") + return None + finally: + try: + os.unlink(file_path) + except OSError: + pass diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py new file mode 100644 index 0000000..35dba2f --- /dev/null +++ b/frigate/util/builtin.py @@ -0,0 +1,488 @@ +"""Utilities for builtin types manipulation.""" + +import ast +import copy +import logging +import math +import multiprocessing.queues +import queue +import re +import shlex +import struct +import time +import urllib.parse +from collections import deque +from collections.abc import Mapping +from multiprocessing.managers import ValueProxy +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import numpy as np +from ruamel.yaml import YAML + +from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS + +if TYPE_CHECKING: + from frigate.config import CameraConfig + +logger = logging.getLogger(__name__) + + +class EventsPerSecond: + def __init__(self, max_events=1000, last_n_seconds=10) -> None: + self._start = None + self._max_events = max_events + self._last_n_seconds = last_n_seconds + self._timestamps: deque[float] = deque(maxlen=max_events) + + def start(self) -> None: + self._start = time.monotonic() + + def update(self) -> None: + now = time.monotonic() + if self._start is None: + self._start = now + self._timestamps.append(now) + self.expire_timestamps(now) + + def eps(self) -> float: + now = time.monotonic() + if self._start is None: + self._start = now + # compute the (approximate) events in the last n seconds + self.expire_timestamps(now) + seconds = min(now - self._start, self._last_n_seconds) + # avoid divide by zero + if seconds == 0: + seconds = 1 + return len(self._timestamps) / seconds + + # remove aged out timestamps + def expire_timestamps(self, now: float) -> None: + threshold = now - self._last_n_seconds + while self._timestamps and self._timestamps[0] < threshold: + self._timestamps.popleft() + + +class InferenceSpeed: + def __init__(self, metric: ValueProxy[float]) -> None: + self.__metric = metric + self.__initialized = False + + def update(self, inference_time: float) -> None: + if not self.__initialized: + self.__metric.value = inference_time + self.__initialized = True + return + + self.__metric.value = (self.__metric.value * 9 + inference_time) / 10 + + def current(self) -> float: + return self.__metric.value + + +def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dict: + """ + :param dct1: First dict to merge + :param dct2: Second dict to merge + :param override: if same key exists in both dictionaries, should override? otherwise ignore. + :param merge_lists: if True, lists will be merged. + :return: The merge dictionary + """ + merged = copy.deepcopy(dct1) + for k, v2 in dct2.items(): + if k in merged: + v1 = merged[k] + if isinstance(v1, dict) and isinstance(v2, Mapping): + merged[k] = deep_merge(v1, v2, override) + elif isinstance(v1, list) and isinstance(v2, list): + if merge_lists: + merged[k] = v1 + v2 + elif override: + merged[k] = copy.deepcopy(v2) + else: + if override: + merged[k] = copy.deepcopy(v2) + else: + merged[k] = copy.deepcopy(v2) + return merged + + +def clean_camera_user_pass(line: str) -> str: + """Removes user and password from line.""" + rtsp_cleaned = re.sub(REGEX_RTSP_CAMERA_USER_PASS, "://*:*@", line) + return re.sub(REGEX_HTTP_CAMERA_USER_PASS, "user=*&password=*", rtsp_cleaned) + + +def escape_special_characters(path: str) -> str: + """Cleans reserved characters to encodings for ffmpeg.""" + if len(path) > 1000: + raise ValueError("Input too long to check") + + try: + found = re.search(REGEX_RTSP_CAMERA_USER_PASS, path).group(0)[3:-1] + pw = found[(found.index(":") + 1) :] + return path.replace(pw, urllib.parse.quote_plus(pw)) + except AttributeError: + # path does not have user:pass + return path + + +def get_ffmpeg_arg_list(arg: Any) -> list: + """Use arg if list or convert to list format.""" + return arg if isinstance(arg, list) else shlex.split(arg) + + +# all built-in record presets use this segment_time +DEFAULT_RECORD_SEGMENT_TIME = 10 + + +def get_record_segment_time(config: "CameraConfig") -> int: + """Extract -segment_time from the camera's record output args.""" + record_args = get_ffmpeg_arg_list(config.ffmpeg.output_args.record) + + if record_args and record_args[0].startswith("preset"): + return DEFAULT_RECORD_SEGMENT_TIME + + try: + idx = record_args.index("-segment_time") + return int(record_args[idx + 1]) + except (ValueError, IndexError): + return DEFAULT_RECORD_SEGMENT_TIME + + +def load_labels( + path: str | None, encoding="utf-8", prefill=91, indexed: bool | None = None +): + """Loads labels from file (with or without index numbers). + Args: + path: path to label file. + encoding: label file encoding. + Returns: + Dictionary mapping indices to labels. + """ + if path is None: + return {} + + with open(path, encoding=encoding) as f: + labels = {index: "unknown" for index in range(prefill)} + lines = f.readlines() + if not lines: + return {} + + if indexed != False and lines[0].split(" ", maxsplit=1)[0].isdigit(): + pairs = [line.split(" ", maxsplit=1) for line in lines] + labels.update({int(index): label.strip() for index, label in pairs}) + else: + labels.update({index: line.strip() for index, line in enumerate(lines)}) + + return labels + + +def to_relative_box( + width: int, height: int, box: tuple[int, int, int, int] +) -> tuple[int | float, int | float, int | float, int | float]: + return ( + box[0] / width, # x + box[1] / height, # y + (box[2] - box[0]) / width, # w + (box[3] - box[1]) / height, # h + ) + + +def create_mask(frame_shape, mask): + mask_img = np.zeros(frame_shape, np.uint8) + mask_img[:] = 255 + + +def process_config_query_string(query_string: dict[str, list]) -> dict[str, Any]: + updates = {} + for key_path_str, new_value_list in query_string.items(): + # use the string key as-is for updates dictionary + if len(new_value_list) > 1: + updates[key_path_str] = new_value_list + else: + value = new_value_list[0] + try: + # no need to convert if we have a mask/zone string + value = ast.literal_eval(value) if "," not in value else value + except (ValueError, SyntaxError): + pass + updates[key_path_str] = value + return updates + + +def flatten_config_data( + config_data: dict[str, Any], parent_key: str = "" +) -> dict[str, Any]: + items = [] + for key, value in config_data.items(): + escaped_key = escape_config_key_segment(str(key)) + new_key = f"{parent_key}.{escaped_key}" if parent_key else escaped_key + if isinstance(value, dict): + items.extend(flatten_config_data(value, new_key).items()) + else: + items.append((new_key, value)) + return dict(items) + + +def escape_config_key_segment(segment: str) -> str: + """Escape dots and backslashes so they can be treated as literal key chars.""" + return segment.replace("\\", "\\\\").replace(".", "\\.") + + +def split_config_key_path(key_path_str: str) -> list[str]: + """Split a dotted config path, honoring \\. as a literal dot in a key.""" + parts: list[str] = [] + current: list[str] = [] + escaped = False + + for char in key_path_str: + if escaped: + current.append(char) + escaped = False + continue + + if char == "\\": + escaped = True + continue + + if char == ".": + parts.append("".join(current)) + current = [] + continue + + current.append(char) + + if escaped: + current.append("\\") + + parts.append("".join(current)) + return parts + + +def update_yaml_file_bulk(file_path: str, updates: dict[str, Any]): + yaml = YAML() + yaml.indent(mapping=2, sequence=4, offset=2) + + try: + with open(file_path) as f: + data = yaml.load(f) + except FileNotFoundError: + logger.error( + f"Unable to read from Frigate config file {file_path}. Make sure it exists and is readable." + ) + return + + # Apply all updates + for key_path_str, new_value in updates.items(): + key_path = split_config_key_path(key_path_str) + for i in range(len(key_path)): + try: + index = int(key_path[i]) + key_path[i] = (key_path[i - 1], index) + key_path.pop(i - 1) + except ValueError: + pass + data = update_yaml(data, key_path, new_value) + + try: + with open(file_path, "w") as f: + yaml.dump(data, f) + except Exception as e: + logger.error(f"Unable to write to Frigate config file {file_path}: {e}") + + +def clear_orphaned_comments(collection, parent, parent_key) -> None: + """Drop stale ruamel comment tokens after a deletion empties a collection. + + When the last entry of a mapping or sequence is removed, any comments that + lived inside that collection's block are orphaned. ruamel then emits them + above a flow-style `{}`/`[]` dedented to column 0, which is unparseable and + corrupts the config. Clearing the emptied collection's own comment metadata + (and the parent's entry pointing at it) keeps the dump valid. Non-empty + collections are left untouched so comments on remaining siblings survive. + """ + if not hasattr(collection, "ca") or len(collection) != 0: + return + + collection.ca.items.clear() + collection.ca.comment = None + if parent is not None and hasattr(parent, "ca"): + parent.ca.items.pop(parent_key, None) + + +def update_yaml(data, key_path, new_value): + temp = data + parent = None + parent_key = None + for key in key_path[:-1]: + if isinstance(key, tuple): + if key[0] not in temp: + temp[key[0]] = [{}] * max(1, key[1] + 1) + elif len(temp[key[0]]) <= key[1]: + temp[key[0]] += [{}] * (key[1] - len(temp[key[0]]) + 1) + parent, parent_key = temp[key[0]], key[1] + temp = temp[key[0]][key[1]] + else: + if key not in temp or temp[key] is None: + temp[key] = {} + parent, parent_key = temp, key + temp = temp[key] + + last_key = key_path[-1] + if new_value == "": + if isinstance(last_key, tuple): + del temp[last_key[0]][last_key[1]] + clear_orphaned_comments(temp[last_key[0]], temp, last_key[0]) + else: + del temp[last_key] + clear_orphaned_comments(temp, parent, parent_key) + else: + if isinstance(last_key, tuple): + if last_key[0] not in temp: + temp[last_key[0]] = [{}] * max(1, last_key[1] + 1) + elif len(temp[last_key[0]]) <= last_key[1]: + temp[last_key[0]] += [{}] * (last_key[1] - len(temp[last_key[0]]) + 1) + temp[last_key[0]][last_key[1]] = new_value + else: + if ( + last_key in temp + and isinstance(temp[last_key], dict) + and isinstance(new_value, dict) + ): + temp[last_key].update(new_value) + else: + temp[last_key] = new_value + + return data + + +def find_by_key(dictionary, target_key): + if target_key in dictionary: + return dictionary[target_key] + else: + for value in dictionary.values(): + if isinstance(value, dict): + result = find_by_key(value, target_key) + if result is not None: + return result + return None + + +def clear_and_unlink(file: Path, missing_ok: bool = True) -> None: + """clear file then unlink to avoid space retained by file descriptors.""" + if not missing_ok and not file.exists(): + raise FileNotFoundError() + + # empty contents of file before unlinking https://github.com/blakeblackshear/frigate/issues/4769 + with open(file, "w"): + pass + + file.unlink(missing_ok=missing_ok) + + +def empty_and_close_queue(q): + while True: + try: + q.get(block=True, timeout=0.5) + except (queue.Empty, EOFError): + break + except Exception as e: + logger.debug(f"Error while emptying queue: {e}") + break + + # close the queue if it is a multiprocessing queue + # manager proxy queues do not have close or join_thread method + if isinstance(q, multiprocessing.queues.Queue): + try: + q.close() + q.join_thread() + except Exception: + pass + + +def generate_color_palette(n): + # mimic matplotlib's color scheme + base_colors = [ + (31, 119, 180), # blue + (255, 127, 14), # orange + (44, 160, 44), # green + (214, 39, 40), # red + (148, 103, 189), # purple + (140, 86, 75), # brown + (227, 119, 194), # pink + (127, 127, 127), # gray + (188, 189, 34), # olive + (23, 190, 207), # cyan + ] + + def interpolate(color1, color2, factor): + return tuple(int(c1 + (c2 - c1) * factor) for c1, c2 in zip(color1, color2)) + + if n <= len(base_colors): + return base_colors[:n] + + colors = base_colors.copy() + step = 1 / (n - len(base_colors) + 1) + extra_colors_needed = n - len(base_colors) + + # interpolate between the base colors to generate more if needed + for i in range(extra_colors_needed): + index = i % (len(base_colors) - 1) + factor = (i + 1) * step + color1 = base_colors[index] + color2 = base_colors[index + 1] + colors.append(interpolate(color1, color2, factor)) + + return colors + + +def serialize(vector: list[float] | np.ndarray | float, pack: bool = True) -> bytes: + """Serializes a list of floats, numpy array, or single float into a compact "raw bytes" format""" + if isinstance(vector, np.ndarray): + # Convert numpy array to list of floats + vector = vector.flatten().tolist() + elif isinstance(vector, (float, np.float32, np.float64)): + # Handle single float values + vector = [vector] + elif not isinstance(vector, list): + raise TypeError( + f"Input must be a list of floats, a numpy array, or a single float. Got {type(vector)}" + ) + + try: + if pack: + return struct.pack("%sf" % len(vector), *vector) + else: + return vector + except struct.error as e: + raise ValueError(f"Failed to pack vector: {e}. Vector: {vector}") from e + + +def deserialize(bytes_data: bytes) -> list[float]: + """Deserializes a compact "raw bytes" format into a list of floats""" + return list(struct.unpack("%sf" % (len(bytes_data) // 4), bytes_data)) + + +def sanitize_float(value): + """Replace NaN or inf with 0.0.""" + if isinstance(value, (int, float)) and not math.isfinite(value): + return 0.0 + return value + + +def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: + return 1 - cosine_distance(a, b) + + +def cosine_distance(a: np.ndarray, b: np.ndarray) -> float: + """Returns cosine distance to match sqlite-vec's calculation.""" + dot = np.dot(a, b) + a_mag = np.dot(a, a) # ||a||^2 + b_mag = np.dot(b, b) # ||b||^2 + + if a_mag == 0 or b_mag == 0: + return 1.0 + + return 1.0 - (dot / (np.sqrt(a_mag) * np.sqrt(b_mag))) diff --git a/frigate/util/camera_cleanup.py b/frigate/util/camera_cleanup.py new file mode 100644 index 0000000..76a6891 --- /dev/null +++ b/frigate/util/camera_cleanup.py @@ -0,0 +1,165 @@ +"""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) + + for snapshot in glob.glob(os.path.join(CLIPS_DIR, f"{camera_name}-*-clean.webp")): + try: + os.remove(snapshot) + except Exception as e: + logger.error("Failed to remove snapshot %s: %s", snapshot, e) + + for snapshot in glob.glob(os.path.join(CLIPS_DIR, f"{camera_name}-*-clean.png")): + 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) diff --git a/frigate/util/classification.py b/frigate/util/classification.py new file mode 100644 index 0000000..a9345bb --- /dev/null +++ b/frigate/util/classification.py @@ -0,0 +1,941 @@ +"""Util for classification models.""" + +import datetime +import json +import logging +import os +import random +import shutil +from collections import defaultdict + +import cv2 +import numpy as np + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum, EmbeddingsRequestor +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FfmpegConfig +from frigate.const import ( + CLIPS_DIR, + MODEL_CACHE_DIR, + PROCESS_PRIORITY_LOW, + UPDATE_MODEL_STATE, +) +from frigate.log import redirect_output_to_logger, suppress_stderr_during +from frigate.models import Event, Recordings, ReviewSegment +from frigate.types import ModelStatusTypesEnum +from frigate.util.downloader import ModelDownloader +from frigate.util.file import get_event_thumbnail_bytes, load_event_snapshot_image +from frigate.util.image import ( + calculate_region, + get_image_from_recording, + relative_box_to_absolute, +) +from frigate.util.process import FrigateProcess + +BATCH_SIZE = 16 +EPOCHS = 50 +LEARNING_RATE = 0.001 +TRAINING_METADATA_FILE = ".training_metadata.json" + +logger = logging.getLogger(__name__) + + +def write_training_metadata(model_name: str, image_count: int) -> None: + """ + Write training metadata to a hidden file in the model's clips directory. + + Args: + model_name: Name of the classification model + image_count: Number of images used in training + """ + model_name = model_name.strip() + clips_model_dir = os.path.join(CLIPS_DIR, model_name) + os.makedirs(clips_model_dir, exist_ok=True) + + metadata_path = os.path.join(clips_model_dir, TRAINING_METADATA_FILE) + metadata = { + "last_training_date": datetime.datetime.now().isoformat(), + "last_training_image_count": image_count, + } + + try: + with open(metadata_path, "w") as f: + json.dump(metadata, f, indent=2) + logger.info(f"Wrote training metadata for {model_name}: {image_count} images") + except Exception as e: + logger.error(f"Failed to write training metadata for {model_name}: {e}") + + +def read_training_metadata(model_name: str) -> dict[str, any] | None: + """ + Read training metadata from the hidden file in the model's clips directory. + + Args: + model_name: Name of the classification model + + Returns: + Dictionary with last_training_date and last_training_image_count, or None if not found + """ + model_name = model_name.strip() + clips_model_dir = os.path.join(CLIPS_DIR, model_name) + metadata_path = os.path.join(clips_model_dir, TRAINING_METADATA_FILE) + + if not os.path.exists(metadata_path): + return None + + try: + with open(metadata_path) as f: + metadata = json.load(f) + return metadata + except Exception as e: + logger.error(f"Failed to read training metadata for {model_name}: {e}") + return None + + +def get_dataset_image_count(model_name: str) -> int: + """ + Count the total number of images in the model's dataset directory. + + Args: + model_name: Name of the classification model + + Returns: + Total count of images across all categories + """ + model_name = model_name.strip() + dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset") + + if not os.path.exists(dataset_dir): + return 0 + + total_count = 0 + try: + for category in os.listdir(dataset_dir): + category_dir = os.path.join(dataset_dir, category) + if not os.path.isdir(category_dir): + continue + + image_files = [ + f + for f in os.listdir(category_dir) + if f.lower().endswith((".webp", ".png", ".jpg", ".jpeg")) + ] + total_count += len(image_files) + except Exception as e: + logger.error(f"Failed to count dataset images for {model_name}: {e}") + return 0 + + return total_count + + +class ClassificationTrainingProcess(FrigateProcess): + def __init__(self, model_name: str) -> None: + self.BASE_WEIGHT_URL = os.environ.get( + "TF_KERAS_MOBILENET_V2_WEIGHTS_URL", + "", + ) + model_name = model_name.strip() + super().__init__( + stop_event=None, + priority=PROCESS_PRIORITY_LOW, + name=f"model_training:{model_name}", + ) + self.model_name = model_name + + def run(self) -> None: + self.pre_run_setup() + success = self.__train_classification_model() + exit(0 if success else 1) + + def __generate_representative_dataset_factory(self, dataset_dir: str): + def generate_representative_dataset(): + image_paths = [] + for root, dirs, files in os.walk(dataset_dir): + for file in files: + if file.lower().endswith((".jpg", ".jpeg", ".png")): + image_paths.append(os.path.join(root, file)) + + for path in image_paths[:300]: + img = cv2.imread(path) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = cv2.resize(img, (224, 224)) + img_array = np.array(img, dtype=np.float32) / 255.0 + img_array = img_array[None, ...] + yield [img_array] + + return generate_representative_dataset + + @redirect_output_to_logger(logger, logging.DEBUG) + def __train_classification_model(self) -> bool: + """Train a classification model.""" + try: + # import in the function so that tensorflow is not initialized multiple times + import tensorflow as tf + from tensorflow.keras import layers, models, optimizers + from tensorflow.keras.applications import MobileNetV2 + from tensorflow.keras.preprocessing.image import ImageDataGenerator + + dataset_dir = os.path.join(CLIPS_DIR, self.model_name, "dataset") + model_dir = os.path.join(MODEL_CACHE_DIR, self.model_name) + os.makedirs(model_dir, exist_ok=True) + + num_classes = len( + [ + d + for d in os.listdir(dataset_dir) + if os.path.isdir(os.path.join(dataset_dir, d)) + ] + ) + + if num_classes < 2: + logger.error( + f"Training failed for {self.model_name}: Need at least 2 classes, found {num_classes}" + ) + return False + + weights_path = "imagenet" + # Download MobileNetV2 weights if not present + if self.BASE_WEIGHT_URL: + weights_path = os.path.join( + MODEL_CACHE_DIR, "MobileNet", "mobilenet_v2_weights.h5" + ) + if not os.path.exists(weights_path): + logger.info("Downloading MobileNet V2 weights file") + ModelDownloader.download_from_url( + self.BASE_WEIGHT_URL, weights_path + ) + + # Start with imagenet base model with 35% of channels in each layer + base_model = MobileNetV2( + input_shape=(224, 224, 3), + include_top=False, + weights=weights_path, + alpha=0.35, + ) + base_model.trainable = False # Freeze pre-trained layers + + model = models.Sequential( + [ + base_model, + layers.GlobalAveragePooling2D(), + layers.Dense(128, activation="relu"), + layers.Dropout(0.3), + layers.Dense(num_classes, activation="softmax"), + ] + ) + + model.compile( + optimizer=optimizers.Adam(learning_rate=LEARNING_RATE), + loss="categorical_crossentropy", + metrics=["accuracy"], + ) + + # create training set + datagen = ImageDataGenerator(rescale=1.0 / 255, validation_split=0.2) + train_gen = datagen.flow_from_directory( + dataset_dir, + target_size=(224, 224), + batch_size=BATCH_SIZE, + class_mode="categorical", + subset="training", + ) + + total_images = train_gen.samples + logger.debug( + f"Training {self.model_name}: {total_images} images across {num_classes} classes" + ) + + # write labelmap + class_indices = train_gen.class_indices + index_to_class = {v: k for k, v in class_indices.items()} + sorted_classes = [index_to_class[i] for i in range(len(index_to_class))] + with open(os.path.join(model_dir, "labelmap.txt"), "w") as f: + for class_name in sorted_classes: + f.write(f"{class_name}\n") + + # train the model + logger.debug(f"Training {self.model_name} for {EPOCHS} epochs...") + model.fit(train_gen, epochs=EPOCHS, verbose=0) + logger.debug(f"Converting {self.model_name} to TFLite...") + + # convert model to tflite + # Suppress stderr during conversion to avoid LLVM debug output + # (fully_quantize, inference_type, MLIR optimization messages, etc) + with suppress_stderr_during("tflite_conversion"): + converter = tf.lite.TFLiteConverter.from_keras_model(model) + converter.optimizations = [tf.lite.Optimize.DEFAULT] + converter.representative_dataset = ( + self.__generate_representative_dataset_factory(dataset_dir) + ) + converter.target_spec.supported_ops = [ + tf.lite.OpsSet.TFLITE_BUILTINS_INT8 + ] + converter.inference_input_type = tf.uint8 + converter.inference_output_type = tf.uint8 + tflite_model = converter.convert() + + # write model + model_path = os.path.join(model_dir, "model.tflite") + with open(model_path, "wb") as f: + f.write(tflite_model) + + # verify model file was written successfully + if not os.path.exists(model_path) or os.path.getsize(model_path) == 0: + logger.error( + f"Training failed for {self.model_name}: Model file was not created or is empty" + ) + return False + + # write training metadata with image count + dataset_image_count = get_dataset_image_count(self.model_name) + write_training_metadata(self.model_name, dataset_image_count) + + logger.info(f"Finished training {self.model_name}") + return True + + except Exception as e: + logger.exception(f"Training failed for {self.model_name}: {e}") + return False + + +def kickoff_model_training( + embeddingRequestor: EmbeddingsRequestor, model_name: str +) -> None: + model_name = model_name.strip() + requestor = InterProcessRequestor() + requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": model_name, + "state": ModelStatusTypesEnum.training, + }, + ) + + # run training in sub process so that + # tensorflow will free CPU / GPU memory + # upon training completion + training_process = ClassificationTrainingProcess(model_name) + training_process.start() + training_process.join() + + # check if training succeeded by examining the exit code + training_success = training_process.exitcode == 0 + + if training_success: + # reload model and mark training as complete + embeddingRequestor.send_data( + EmbeddingsRequestEnum.reload_classification_model.value, + {"model_name": model_name}, + ) + requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": model_name, + "state": ModelStatusTypesEnum.complete, + }, + ) + else: + logger.error( + f"Training subprocess failed for {model_name} (exit code: {training_process.exitcode})" + ) + # mark training as failed so UI shows error state + # don't reload the model since it failed + requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": model_name, + "state": ModelStatusTypesEnum.failed, + }, + ) + + requestor.stop() + + +@staticmethod +def collect_state_classification_examples( + model_name: str, cameras: dict[str, tuple[float, float, float, float]] +) -> None: + """ + Collect representative state classification examples from review items. + + This function: + 1. Queries review items from specified cameras + 2. Selects 100 balanced timestamps across the data + 3. Extracts keyframes from recordings (cropped to specified regions) + 4. Selects 24 most visually distinct images + 5. Saves them to the dataset directory + + Args: + model_name: Name of the classification model + cameras: Dict mapping camera names to normalized crop coordinates [x1, y1, x2, y2] (0-1) + """ + model_name = model_name.strip() + dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset") + + # Step 1: Get review items for the cameras + camera_names = list(cameras.keys()) + review_items = list( + ReviewSegment.select() + .where(ReviewSegment.camera.in_(camera_names)) + .where(ReviewSegment.end_time.is_null(False)) + .order_by(ReviewSegment.start_time.asc()) + ) + + if not review_items: + logger.warning(f"No review items found for cameras: {camera_names}") + return + + # The temp directory is only created when there are review_items. + temp_dir = os.path.join(dataset_dir, "temp") + os.makedirs(temp_dir, exist_ok=True) + + # Step 2: Create balanced timestamp selection (100 samples) + timestamps = _select_balanced_timestamps(review_items, target_count=100) + + # Step 3: Extract keyframes from recordings with crops applied + keyframes = _extract_keyframes( + "/usr/lib/ffmpeg/8.0/bin/ffmpeg", timestamps, temp_dir, cameras + ) + + # Step 4: Select 24 most visually distinct images (they're already cropped) + distinct_images = _select_distinct_images(keyframes, target_count=24) + + # Step 5: Save to train directory for later classification + train_dir = os.path.join(CLIPS_DIR, model_name, "train") + if os.path.exists(train_dir): + shutil.rmtree(train_dir) + os.makedirs(train_dir, exist_ok=True) + + saved_count = 0 + for idx, image_path in enumerate(distinct_images): + dest_path = os.path.join(train_dir, f"example_{idx:03d}.jpg") + try: + img = cv2.imread(image_path) + + if img is not None: + cv2.imwrite(dest_path, img) + saved_count += 1 + except Exception as e: + logger.error(f"Failed to save image {image_path}: {e}") + + try: + shutil.rmtree(temp_dir) + except Exception as e: + logger.warning(f"Failed to clean up temp directory: {e}") + + +def _select_balanced_timestamps( + review_items: list[ReviewSegment], target_count: int = 100 +) -> list[dict]: + """ + Select balanced timestamps from review items. + + Strategy: + - Group review items by camera and time of day + - Sample evenly across groups to ensure diversity + - For each selected review item, pick a random timestamp within its duration + + Returns: + List of dicts with keys: camera, timestamp, review_item + """ + # Group by camera and hour of day for temporal diversity + grouped = defaultdict(list) + + for item in review_items: + camera = item.camera + # Group by 6-hour blocks for temporal diversity + hour_block = int(item.start_time // (6 * 3600)) + key = f"{camera}_{hour_block}" + grouped[key].append(item) + + # Calculate how many samples per group + num_groups = len(grouped) + if num_groups == 0: + return [] + + samples_per_group = max(1, target_count // num_groups) + timestamps = [] + + # Sample from each group + for group_items in grouped.values(): + # Take samples_per_group items from this group + sample_size = min(samples_per_group, len(group_items)) + sampled_items = random.sample(group_items, sample_size) + + for item in sampled_items: + # Pick a random timestamp within the review item's duration + duration = item.end_time - item.start_time + if duration <= 0: + continue + + # Sample from middle 80% to avoid edge artifacts + offset = random.uniform(duration * 0.1, duration * 0.9) + timestamp = item.start_time + offset + + timestamps.append( + { + "camera": item.camera, + "timestamp": timestamp, + "review_item": item, + } + ) + + # If we don't have enough, sample more from larger groups + while len(timestamps) < target_count and len(timestamps) < len(review_items): + for group_items in grouped.values(): + if len(timestamps) >= target_count: + break + + # Pick a random item not already sampled + item = random.choice(group_items) + duration = item.end_time - item.start_time + if duration <= 0: + continue + + offset = random.uniform(duration * 0.1, duration * 0.9) + timestamp = item.start_time + offset + + # Check if we already have a timestamp near this one + if not any(abs(t["timestamp"] - timestamp) < 1.0 for t in timestamps): + timestamps.append( + { + "camera": item.camera, + "timestamp": timestamp, + "review_item": item, + } + ) + + return timestamps[:target_count] + + +def _extract_keyframes( + ffmpeg_path: str, + timestamps: list[dict], + output_dir: str, + camera_crops: dict[str, tuple[float, float, float, float]], +) -> list[str]: + """ + Extract keyframes from recordings at specified timestamps and crop to specified regions. + + This implementation batches work by running multiple ffmpeg snapshot commands + concurrently, which significantly reduces total runtime compared to + processing each timestamp serially. + + Args: + ffmpeg_path: Path to ffmpeg binary + timestamps: List of timestamp dicts from _select_balanced_timestamps + output_dir: Directory to save extracted frames + camera_crops: Dict mapping camera names to normalized crop coordinates [x1, y1, x2, y2] (0-1) + + Returns: + List of paths to successfully extracted and cropped keyframe images + """ + from concurrent.futures import ThreadPoolExecutor, as_completed + + if not timestamps: + return [] + + # Limit the number of concurrent ffmpeg processes so we don't overload the host. + max_workers = min(5, len(timestamps)) + + def _process_timestamp(idx: int, ts_info: dict) -> tuple[int, str | None]: + camera = ts_info["camera"] + timestamp = ts_info["timestamp"] + + if camera not in camera_crops: + logger.warning(f"No crop coordinates for camera {camera}") + return idx, None + + norm_x1, norm_y1, norm_x2, norm_y2 = camera_crops[camera] + + try: + recording = ( + Recordings.select() + .where( + (timestamp >= Recordings.start_time) + & (timestamp <= Recordings.end_time) + & (Recordings.camera == camera) + ) + .order_by(Recordings.start_time.desc()) + .limit(1) + .get() + ) + except Exception: + return idx, None + + relative_time = timestamp - recording.start_time + + try: + config = FfmpegConfig(path="/usr/lib/ffmpeg/8.0") + image_data = get_image_from_recording( + config, + recording.path, + relative_time, + codec="mjpeg", + height=None, + ) + + if not image_data: + return idx, None + + nparr = np.frombuffer(image_data, np.uint8) + img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + + if img is None: + return idx, None + + height, width = img.shape[:2] + + x1 = int(norm_x1 * width) + y1 = int(norm_y1 * height) + x2 = int(norm_x2 * width) + y2 = int(norm_y2 * height) + + x1_clipped = max(0, min(x1, width)) + y1_clipped = max(0, min(y1, height)) + x2_clipped = max(0, min(x2, width)) + y2_clipped = max(0, min(y2, height)) + + if x2_clipped <= x1_clipped or y2_clipped <= y1_clipped: + return idx, None + + cropped = img[y1_clipped:y2_clipped, x1_clipped:x2_clipped] + resized = cv2.resize(cropped, (224, 224)) + + output_path = os.path.join(output_dir, f"frame_{idx:04d}.jpg") + cv2.imwrite(output_path, resized) + return idx, output_path + except Exception as e: + logger.debug( + f"Failed to extract frame from {recording.path} at {relative_time}s: {e}" + ) + return idx, None + + keyframes_with_index: list[tuple[int, str]] = [] + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + future_to_idx = { + executor.submit(_process_timestamp, idx, ts_info): idx + for idx, ts_info in enumerate(timestamps) + } + + for future in as_completed(future_to_idx): + _, path = future.result() + if path: + keyframes_with_index.append((future_to_idx[future], path)) + + keyframes_with_index.sort(key=lambda item: item[0]) + return [path for _, path in keyframes_with_index] + + +def _select_distinct_images( + image_paths: list[str], target_count: int = 20 +) -> list[str]: + """ + Select the most visually distinct images from a set of keyframes. + + Uses a greedy algorithm based on image histograms: + 1. Start with a random image + 2. Iteratively add the image that is most different from already selected images + 3. Difference is measured using histogram comparison + + Args: + image_paths: List of paths to candidate images + target_count: Number of distinct images to select + + Returns: + List of paths to selected images + """ + if len(image_paths) <= target_count: + return image_paths + + histograms = {} + valid_paths = [] + + for path in image_paths: + try: + img = cv2.imread(path) + + if img is None: + continue + + hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) + hist = cv2.calcHist( + [hsv], [0, 1, 2], None, [8, 8, 8], [0, 180, 0, 256, 0, 256] + ) + hist = cv2.normalize(hist, hist).flatten() + histograms[path] = hist + valid_paths.append(path) + except Exception as e: + logger.debug(f"Failed to process image {path}: {e}") + continue + + if len(valid_paths) <= target_count: + return valid_paths + + selected = [] + first_image = random.choice(valid_paths) + selected.append(first_image) + remaining = [p for p in valid_paths if p != first_image] + + while len(selected) < target_count and remaining: + max_min_distance = -1 + best_candidate = None + + for candidate in remaining: + min_distance = float("inf") + + for selected_img in selected: + distance = cv2.compareHist( + histograms[candidate], + histograms[selected_img], + cv2.HISTCMP_BHATTACHARYYA, + ) + min_distance = min(min_distance, distance) + + if min_distance > max_min_distance: + max_min_distance = min_distance + best_candidate = candidate + + if best_candidate: + selected.append(best_candidate) + remaining.remove(best_candidate) + else: + break + + return selected + + +@staticmethod +def collect_object_classification_examples( + model_name: str, + label: str, +) -> None: + """ + Collect representative object classification examples from event thumbnails. + + This function: + 1. Queries events for the specified label + 2. Selects 100 balanced events across different cameras and times + 3. Crops each event's clean snapshot around the object bounding box + 4. Selects 24 most visually distinct thumbnails + 5. Saves to dataset directory + + Args: + model_name: Name of the classification model + label: Object label to collect (e.g., "person", "car") + """ + model_name = model_name.strip() + dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset") + temp_dir = os.path.join(dataset_dir, "temp") + os.makedirs(temp_dir, exist_ok=True) + + # Step 1: Query events for the specified label and cameras + events = list( + Event.select().where(Event.label == label).order_by(Event.start_time.asc()) + ) + + if not events: + logger.warning(f"No events found for label '{label}'") + return + + logger.debug(f"Found {len(events)} events") + + # Step 2: Select balanced events (100 samples) + selected_events = _select_balanced_events(events, target_count=100) + logger.debug(f"Selected {len(selected_events)} events") + + # Step 3: Extract thumbnails from events + thumbnails = _extract_event_thumbnails(selected_events, temp_dir) + logger.debug(f"Successfully extracted {len(thumbnails)} thumbnails") + + # Step 4: Select 24 most visually distinct thumbnails + distinct_images = _select_distinct_images(thumbnails, target_count=24) + logger.debug(f"Selected {len(distinct_images)} distinct images") + + # Step 5: Save to train directory for later classification + train_dir = os.path.join(CLIPS_DIR, model_name, "train") + if os.path.exists(train_dir): + shutil.rmtree(train_dir) + os.makedirs(train_dir, exist_ok=True) + + saved_count = 0 + for idx, image_path in enumerate(distinct_images): + dest_path = os.path.join(train_dir, f"example_{idx:03d}.jpg") + try: + img = cv2.imread(image_path) + + if img is not None: + cv2.imwrite(dest_path, img) + saved_count += 1 + except Exception as e: + logger.error(f"Failed to save image {image_path}: {e}") + + try: + shutil.rmtree(temp_dir) + except Exception as e: + logger.warning(f"Failed to clean up temp directory: {e}") + + logger.debug( + f"Successfully collected {saved_count} classification examples in {train_dir}" + ) + + +def _select_balanced_events( + events: list[Event], target_count: int = 100 +) -> list[Event]: + """ + Select balanced events from the event list. + + Strategy: + - Group events by camera and time of day + - Sample evenly across groups to ensure diversity + - Prioritize events with higher scores + + Returns: + List of selected events + """ + grouped = defaultdict(list) + + for event in events: + camera = event.camera + hour_block = int(event.start_time // (6 * 3600)) + key = f"{camera}_{hour_block}" + grouped[key].append(event) + + num_groups = len(grouped) + if num_groups == 0: + return [] + + samples_per_group = max(1, target_count // num_groups) + selected = [] + + for group_events in grouped.values(): + # Take top events by score, then randomly sample from them + sorted_events = sorted( + group_events, + key=lambda e: e.data.get("score", 0) if e.data else 0, + reverse=True, + ) + + # Consider top 3x candidates to allow randomness while preferring higher scores + candidate_pool = sorted_events[: samples_per_group * 3] + sample_size = min(samples_per_group, len(candidate_pool)) + selected.extend(random.sample(candidate_pool, sample_size)) + + if len(selected) < target_count: + remaining = [e for e in events if e not in selected] + needed = target_count - len(selected) + if len(remaining) > needed: + selected.extend(random.sample(remaining, needed)) + else: + selected.extend(remaining) + + return selected[:target_count] + + +def _extract_event_thumbnails(events: list[Event], output_dir: str) -> list[str]: + """ + Extract a training image for each event. + + Preferred path: load the full-frame clean snapshot and crop around the + stored bounding box with the same calculate_region(..., max(w, h), 1.0) + call the live ObjectClassificationProcessor uses, so wizard examples + are framed like inference-time inputs. + + Fallback: if no clean snapshot exists (snapshots disabled, or only a + legacy annotated JPG is on disk), center-crop the stored thumbnail + using a step ladder sized from the box/region area ratio. + + Args: + events: List of Event objects + output_dir: Directory to save crops + + Returns: + List of paths to successfully extracted images + """ + image_paths = [] + + for idx, event in enumerate(events): + try: + img = _load_event_classification_crop(event) + if img is None: + continue + + resized = cv2.resize(img, (224, 224)) + output_path = os.path.join(output_dir, f"thumbnail_{idx:04d}.jpg") + cv2.imwrite(output_path, resized) + image_paths.append(output_path) + + except Exception as e: + logger.debug(f"Failed to extract image for event {event.id}: {e}") + continue + + return image_paths + + +def _load_event_classification_crop(event: Event) -> np.ndarray | None: + """Prefer a snapshot-based object crop; fall back to a center-cropped thumbnail.""" + if event.data and "box" in event.data: + snapshot, _ = load_event_snapshot_image(event, clean_only=True) + if snapshot is not None: + abs_box = relative_box_to_absolute(snapshot.shape, event.data["box"]) + if abs_box is not None: + xmin, ymin, xmax, ymax = abs_box + box_w = xmax - xmin + box_h = ymax - ymin + if box_w > 0 and box_h > 0: + x1, y1, x2, y2 = calculate_region( + snapshot.shape, + xmin, + ymin, + xmax, + ymax, + max(box_w, box_h), + 1.0, + ) + cropped = snapshot[y1:y2, x1:x2] + if cropped.size > 0: + return cropped + + thumbnail_bytes = get_event_thumbnail_bytes(event) + if not thumbnail_bytes: + return None + + nparr = np.frombuffer(thumbnail_bytes, np.uint8) + img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + if img is None or img.size == 0: + return None + + height, width = img.shape[:2] + crop_size = 1.0 + + if event.data and "box" in event.data and "region" in event.data: + box = event.data["box"] + region = event.data["region"] + + if len(box) == 4 and len(region) == 4: + box_w, box_h = box[2], box[3] + region_w, region_h = region[2], region[3] + box_area = (box_w * box_h) / (region_w * region_h) + + if box_area < 0.05: + crop_size = 0.4 + elif box_area < 0.10: + crop_size = 0.5 + elif box_area < 0.20: + crop_size = 0.65 + elif box_area < 0.35: + crop_size = 0.80 + else: + crop_size = 0.95 + + crop_width = int(width * crop_size) + crop_height = int(height * crop_size) + x1 = (width - crop_width) // 2 + y1 = (height - crop_height) // 2 + cropped = img[y1 : y1 + crop_height, x1 : x1 + crop_width] + if cropped.size == 0: + return None + + return cropped diff --git a/frigate/util/config.py b/frigate/util/config.py new file mode 100644 index 0000000..3e093a9 --- /dev/null +++ b/frigate/util/config.py @@ -0,0 +1,874 @@ +"""configuration utils.""" + +import asyncio +import logging +import os +import shutil +from typing import Any + +from ruamel.yaml import YAML + +from frigate.const import ( + CONFIG_DIR, + DEFAULT_FFMPEG_VERSION, + EXPORT_DIR, + INCLUDED_FFMPEG_VERSIONS, + REDACTED_CREDENTIAL_SENTINEL, +) +from frigate.util.builtin import deep_merge +from frigate.util.services import get_video_properties + +logger = logging.getLogger(__name__) + +CURRENT_CONFIG_VERSION = "0.18-0" +DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yml") + + +def resolve_ffmpeg_path(path: str, binary: str = "ffmpeg") -> str: + """Resolve an ffmpeg version alias or custom path to a binary path. + + A bare version alias that is no longer bundled (for example one that was + dropped when the default version changed) falls back to the default + bundled version so existing configs keep working across an upgrade or a + revert. Custom install paths (anything absolute) are used as-is. + """ + if path == "default" or ( + not path.startswith("/") and path not in INCLUDED_FFMPEG_VERSIONS + ): + version = DEFAULT_FFMPEG_VERSION + elif path in INCLUDED_FFMPEG_VERSIONS: + version = path + else: + return f"{path}/bin/{binary}" + + return f"/usr/lib/ffmpeg/{version}/bin/{binary}" + + +def redact_credential(obj: dict[str, Any], key: str) -> None: + """Replace obj[key] with the redaction sentinel if a value is saved, else drop. + + Used when shaping the /config response so saved credentials never leave + the server. The frontend recognizes REDACTED_CREDENTIAL_SENTINEL, renders + the field as empty with a "saved — leave blank to keep" placeholder, and + /config/set strips it from any incoming payload so the YAML value is + preserved when the user doesn't touch the field. + """ + if obj.get(key): + obj[key] = REDACTED_CREDENTIAL_SENTINEL + else: + obj.pop(key, None) + + +def find_config_file() -> str: + config_path = os.environ.get("CONFIG_FILE", DEFAULT_CONFIG_FILE) + + if not os.path.isfile(config_path): + config_path = config_path.replace("yml", "yaml") + + return config_path + + +def migrate_frigate_config(config_file: str): + """handle migrating the frigate config.""" + logger.info("Checking if frigate config needs migration...") + + if not os.access(config_file, mode=os.W_OK): + logger.error("Config file is read-only, unable to migrate config file.") + return + + yaml = YAML() + yaml.indent(mapping=2, sequence=4, offset=2) + with open(config_file) as f: + config: dict[str, dict[str, Any]] = yaml.load(f) + + if config is None: + logger.error(f"Failed to load config at {config_file}") + return + + previous_version = str(config.get("version", "0.13")) + + if previous_version == CURRENT_CONFIG_VERSION: + logger.info("frigate config does not need migration...") + return + + logger.info("copying config as backup...") + shutil.copy(config_file, os.path.join(CONFIG_DIR, "backup_config.yaml")) + + if previous_version < "0.14": + logger.info(f"Migrating frigate config from {previous_version} to 0.14...") + new_config = migrate_014(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.14" + + logger.info("Migrating export file names...") + if os.path.isdir(EXPORT_DIR): + for file in os.listdir(EXPORT_DIR): + if "@" not in file: + continue + + new_name = file.replace("@", "_") + os.rename( + os.path.join(EXPORT_DIR, file), os.path.join(EXPORT_DIR, new_name) + ) + + if previous_version < "0.15-0": + logger.info(f"Migrating frigate config from {previous_version} to 0.15-0...") + new_config = migrate_015_0(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.15-0" + + if previous_version < "0.15-1": + logger.info(f"Migrating frigate config from {previous_version} to 0.15-1...") + new_config = migrate_015_1(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.15-1" + + if previous_version < "0.16-0": + logger.info(f"Migrating frigate config from {previous_version} to 0.16-0...") + new_config = migrate_016_0(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.16-0" + + if previous_version < "0.17-0": + logger.info(f"Migrating frigate config from {previous_version} to 0.17-0...") + new_config = migrate_017_0(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.17-0" + + if previous_version < "0.18-0": + logger.info(f"Migrating frigate config from {previous_version} to 0.18-0...") + new_config = migrate_018_0(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.18-0" + + logger.info("Finished frigate config migration...") + + +def migrate_014(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.14""" + # migrate record.events.required_zones to review.alerts.required_zones + new_config = config.copy() + global_required_zones = ( + config.get("record", {}).get("events", {}).get("required_zones", []) + ) + + if global_required_zones: + # migrate to new review config + if not new_config.get("review"): + new_config["review"] = {} + + if not new_config["review"].get("alerts"): + new_config["review"]["alerts"] = {} + + if not new_config["review"]["alerts"].get("required_zones"): + new_config["review"]["alerts"]["required_zones"] = global_required_zones + + # remove record required zones config + del new_config["record"]["events"]["required_zones"] + + # remove record altogether if there is not other config + if not new_config["record"]["events"]: + del new_config["record"]["events"] + + if not new_config["record"]: + del new_config["record"] + + # Remove UI fields + if new_config.get("ui"): + if new_config["ui"].get("use_experimental"): + del new_config["ui"]["use_experimental"] + + if new_config["ui"].get("live_mode"): + del new_config["ui"]["live_mode"] + + if not new_config["ui"]: + del new_config["ui"] + + # remove rtmp + if new_config.get("ffmpeg", {}).get("output_args", {}).get("rtmp"): + del new_config["ffmpeg"]["output_args"]["rtmp"] + + if new_config.get("rtmp"): + del new_config["rtmp"] + + for name, camera in config.get("cameras", {}).items(): + camera_config: dict[str, dict[str, Any]] = camera.copy() + required_zones = ( + camera_config.get("record", {}).get("events", {}).get("required_zones", []) + ) + + if required_zones: + # migrate to new review config + if not camera_config.get("review"): + camera_config["review"] = {} + + if not camera_config["review"].get("alerts"): + camera_config["review"]["alerts"] = {} + + if not camera_config["review"]["alerts"].get("required_zones"): + camera_config["review"]["alerts"]["required_zones"] = required_zones + + # remove record required zones config + del camera_config["record"]["events"]["required_zones"] + + # remove record altogether if there is not other config + if not camera_config["record"]["events"]: + del camera_config["record"]["events"] + + if not camera_config["record"]: + del camera_config["record"] + + # remove rtmp + if camera_config.get("ffmpeg", {}).get("output_args", {}).get("rtmp"): + del camera_config["ffmpeg"]["output_args"]["rtmp"] + + if camera_config.get("rtmp"): + del camera_config["rtmp"] + + new_config["cameras"][name] = camera_config + + new_config["version"] = "0.14" + return new_config + + +def migrate_015_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.15-0""" + new_config = config.copy() + + # migrate record.events to record.alerts and record.detections + global_record_events = config.get("record", {}).get("events") + if global_record_events: + alerts_retention = {"retain": {}} + detections_retention = {"retain": {}} + + if global_record_events.get("pre_capture"): + alerts_retention["pre_capture"] = global_record_events["pre_capture"] + + if global_record_events.get("post_capture"): + alerts_retention["post_capture"] = global_record_events["post_capture"] + + if global_record_events.get("retain", {}).get("default"): + alerts_retention["retain"]["days"] = global_record_events["retain"][ + "default" + ] + + # decide logical detections retention based on current detections config + if not config.get("review", {}).get("alerts", {}).get( + "required_zones" + ) or config.get("review", {}).get("detections"): + if global_record_events.get("pre_capture"): + detections_retention["pre_capture"] = global_record_events[ + "pre_capture" + ] + + if global_record_events.get("post_capture"): + detections_retention["post_capture"] = global_record_events[ + "post_capture" + ] + + if global_record_events.get("retain", {}).get("default"): + detections_retention["retain"]["days"] = global_record_events["retain"][ + "default" + ] + else: + continuous_days = config.get("record", {}).get("retain", {}).get("days") + detections_retention["retain"]["days"] = ( + continuous_days if continuous_days else 1 + ) + + new_config["record"]["alerts"] = alerts_retention + new_config["record"]["detections"] = detections_retention + + del new_config["record"]["events"] + + for name, camera in config.get("cameras", {}).items(): + camera_config: dict[str, dict[str, Any]] = camera.copy() + + record_events: dict[str, Any] = camera_config.get("record", {}).get("events") + + if record_events: + alerts_retention = {"retain": {}} + detections_retention = {"retain": {}} + + if record_events.get("pre_capture"): + alerts_retention["pre_capture"] = record_events["pre_capture"] + + if record_events.get("post_capture"): + alerts_retention["post_capture"] = record_events["post_capture"] + + if record_events.get("retain", {}).get("default"): + alerts_retention["retain"]["days"] = record_events["retain"]["default"] + + # decide logical detections retention based on current detections config + if not camera_config.get("review", {}).get("alerts", {}).get( + "required_zones" + ) or camera_config.get("review", {}).get("detections"): + if record_events.get("pre_capture"): + detections_retention["pre_capture"] = record_events["pre_capture"] + + if record_events.get("post_capture"): + detections_retention["post_capture"] = record_events["post_capture"] + + if record_events.get("retain", {}).get("default"): + detections_retention["retain"]["days"] = record_events["retain"][ + "default" + ] + else: + continuous_days = ( + camera_config.get("record", {}).get("retain", {}).get("days") + ) + detections_retention["retain"]["days"] = ( + continuous_days if continuous_days else 1 + ) + + camera_config["record"]["alerts"] = alerts_retention + camera_config["record"]["detections"] = detections_retention + del camera_config["record"]["events"] + + new_config["cameras"][name] = camera_config + + new_config["version"] = "0.15-0" + return new_config + + +def migrate_015_1(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.15-1""" + new_config = config.copy() + + for detector, detector_config in config.get("detectors", {}).items(): + path = detector_config.get("model", {}).get("path") + + if path: + new_config["detectors"][detector]["model_path"] = path + del new_config["detectors"][detector]["model"] + + new_config["version"] = "0.15-1" + return new_config + + +def migrate_016_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.16-0""" + new_config = config.copy() + + # migrate config that does not have detect -> enabled explicitly set to have it enabled + if new_config.get("detect", {}).get("enabled") is None: + detect_config = new_config.get("detect", {}) + detect_config["enabled"] = True + new_config["detect"] = detect_config + + for name, camera in config.get("cameras", {}).items(): + camera_config: dict[str, dict[str, Any]] = camera.copy() + + live_config = camera_config.get("live", {}) + if "stream_name" in live_config: + # Migrate from live -> stream_name to live -> streams -> dict + stream_name = live_config["stream_name"] + live_config["streams"] = {stream_name: stream_name} + + del live_config["stream_name"] + + camera_config["live"] = live_config + + # add another value to movement_weights for autotracking cams + onvif_config = camera_config.get("onvif", {}) + if "autotracking" in onvif_config: + movement_weights = ( + camera_config.get("onvif", {}) + .get("autotracking") + .get("movement_weights", {}) + ) + + if movement_weights and len(movement_weights.split(",")) == 5: + onvif_config["autotracking"]["movement_weights"] = ( + movement_weights + ", 0" + ) + camera_config["onvif"] = onvif_config + + new_config["cameras"][name] = camera_config + + new_config["version"] = "0.16-0" + return new_config + + +def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.17-0""" + new_config = config.copy() + + # migrate global to new recording configuration + global_record_retain = config.get("record", {}).get("retain") + + if global_record_retain: + continuous = {"days": 0} + motion = {"days": 0} + days = global_record_retain.get("days") + mode = global_record_retain.get("mode", "all") + + if days: + if mode == "all": + continuous["days"] = days + + # if a user was keeping all for number of days + # we need to keep motion and all for that number of days + motion["days"] = days + else: + motion["days"] = days + + new_config["record"]["continuous"] = continuous + new_config["record"]["motion"] = motion + + del new_config["record"]["retain"] + + # migrate global genai to new objects config + global_genai = config.get("genai", {}) + + if global_genai: + new_genai_config = {} + new_object_config = new_config.get("objects", {}) + new_object_config["genai"] = {} + + for key in global_genai.keys(): + if key in ["model", "provider", "base_url", "api_key"]: + new_genai_config[key] = global_genai[key] + else: + new_object_config["genai"][key] = global_genai[key] + + new_config["genai"] = new_genai_config + new_config["objects"] = new_object_config + + for name, camera in config.get("cameras", {}).items(): + camera_config: dict[str, dict[str, Any]] = camera.copy() + camera_record_retain = camera_config.get("record", {}).get("retain") + + if camera_record_retain: + continuous = {"days": 0} + motion = {"days": 0} + days = camera_record_retain.get("days") + mode = camera_record_retain.get("mode", "all") + + if days: + if mode == "all": + continuous["days"] = days + else: + motion["days"] = days + + camera_config["record"]["continuous"] = continuous + camera_config["record"]["motion"] = motion + + del camera_config["record"]["retain"] + + camera_genai = camera_config.get("genai", {}) + + if camera_genai: + camera_object_config = camera_config.get("objects", {}) + camera_object_config["genai"] = camera_genai + camera_config["objects"] = camera_object_config + del camera_config["genai"] + + new_config["cameras"][name] = camera_config + + new_config["version"] = "0.17-0" + return new_config + + +def _convert_legacy_mask_to_dict( + mask: str | list | None, mask_type: str = "motion_mask", label: str = "" +) -> dict[str, dict[str, Any]]: + """Convert legacy mask format (str or list[str]) to new dict format. + + Args: + mask: Legacy mask format (string or list of strings) + mask_type: Type of mask for naming ("motion_mask" or "object_mask") + label: Optional label for object masks (e.g., "person") + + Returns: + Dictionary with mask_id as key and mask config as value + """ + if not mask: + return {} + + result = {} + + if isinstance(mask, str): + if mask: + mask_id = f"{mask_type}_1" + friendly_name = ( + f"Object Mask 1 ({label})" + if label + else f"{mask_type.replace('_', ' ').title()} 1" + ) + result[mask_id] = { + "friendly_name": friendly_name, + "enabled": True, + "coordinates": mask, + } + elif isinstance(mask, list): + for i, coords in enumerate(mask): + if coords: + mask_id = f"{mask_type}_{i + 1}" + friendly_name = ( + f"Object Mask {i + 1} ({label})" + if label + else f"{mask_type.replace('_', ' ').title()} {i + 1}" + ) + result[mask_id] = { + "friendly_name": friendly_name, + "enabled": True, + "coordinates": coords, + } + + return result + + +def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.18-0""" + new_config = config.copy() + + # Migrate GenAI to new format + genai = new_config.get("genai") + + if genai and genai.get("provider"): + genai["roles"] = ["embeddings", "descriptions", "chat"] + new_config["genai"] = {"default": genai} + + # Remove deprecated sync_recordings from global record config + if new_config.get("record", {}).get("sync_recordings") is not None: + del new_config["record"]["sync_recordings"] + + # Remove deprecated timelapse_args from global record export config + if new_config.get("record", {}).get("export", {}).get("timelapse_args") is not None: + del new_config["record"]["export"]["timelapse_args"] + # Remove export section if empty + if not new_config.get("record", {}).get("export"): + del new_config["record"]["export"] + # Remove record section if empty + if not new_config.get("record"): + del new_config["record"] + + # Migrate global motion masks + global_motion = new_config.get("motion", {}) + if global_motion and "mask" in global_motion: + mask = global_motion.get("mask") + if mask is not None and not isinstance(mask, dict): + new_config["motion"]["mask"] = _convert_legacy_mask_to_dict( + mask, "motion_mask" + ) + + # Migrate global object masks + global_objects = new_config.get("objects", {}) + if global_objects and "mask" in global_objects: + mask = global_objects.get("mask") + if mask is not None and not isinstance(mask, dict): + new_config["objects"]["mask"] = _convert_legacy_mask_to_dict( + mask, "object_mask" + ) + + # Migrate global object filters masks + if global_objects and "filters" in global_objects: + for obj_name, filter_config in global_objects.get("filters", {}).items(): + if isinstance(filter_config, dict) and "mask" in filter_config: + mask = filter_config.get("mask") + if mask is not None and not isinstance(mask, dict): + new_config["objects"]["filters"][obj_name]["mask"] = ( + _convert_legacy_mask_to_dict(mask, "object_mask", obj_name) + ) + + # Remove deprecated sync_recordings and migrate masks for camera-specific configs + for name, camera in config.get("cameras", {}).items(): + camera_config: dict[str, dict[str, Any]] = camera.copy() + + if camera_config.get("record", {}).get("sync_recordings") is not None: + del camera_config["record"]["sync_recordings"] + + if ( + camera_config.get("record", {}).get("export", {}).get("timelapse_args") + is not None + ): + del camera_config["record"]["export"]["timelapse_args"] + # Remove export section if empty + if not camera_config.get("record", {}).get("export"): + del camera_config["record"]["export"] + # Remove record section if empty + if not camera_config.get("record"): + del camera_config["record"] + + # Migrate camera motion masks + camera_motion = camera_config.get("motion", {}) + if camera_motion and "mask" in camera_motion: + mask = camera_motion.get("mask") + if mask is not None and not isinstance(mask, dict): + camera_config["motion"]["mask"] = _convert_legacy_mask_to_dict( + mask, "motion_mask" + ) + + # Migrate camera global object masks + camera_objects = camera_config.get("objects", {}) + if camera_objects and "mask" in camera_objects: + mask = camera_objects.get("mask") + if mask is not None and not isinstance(mask, dict): + camera_config["objects"]["mask"] = _convert_legacy_mask_to_dict( + mask, "object_mask" + ) + + # Migrate camera object filter masks + if camera_objects and "filters" in camera_objects: + for obj_name, filter_config in camera_objects.get("filters", {}).items(): + if isinstance(filter_config, dict) and "mask" in filter_config: + mask = filter_config.get("mask") + if mask is not None and not isinstance(mask, dict): + camera_config["objects"]["filters"][obj_name]["mask"] = ( + _convert_legacy_mask_to_dict(mask, "object_mask", obj_name) + ) + + new_config["cameras"][name] = camera_config + + # Remove deprecated clean_copy from global snapshots config + if new_config.get("snapshots", {}).get("clean_copy") is not None: + del new_config["snapshots"]["clean_copy"] + if not new_config["snapshots"]: + del new_config["snapshots"] + + # Remove deprecated clean_copy from camera snapshots configs + for name, camera in new_config.get("cameras", {}).items(): + camera_config: dict[str, dict[str, Any]] = camera.copy() + + if camera_config.get("snapshots", {}).get("clean_copy") is not None: + del camera_config["snapshots"]["clean_copy"] + if not camera_config["snapshots"]: + del camera_config["snapshots"] + + new_config["cameras"][name] = camera_config + + # Remove deprecated date_style and time_style from global ui config + global_ui = new_config.get("ui", {}) + if global_ui.get("date_style") is not None: + del new_config["ui"]["date_style"] + if global_ui.get("time_style") is not None: + del new_config["ui"]["time_style"] + # Remove ui section if empty + if "ui" in new_config and not new_config["ui"]: + del new_config["ui"] + + new_config["version"] = "0.18-0" + return new_config + + +def get_relative_coordinates( + mask: str | list | None, + frame_shape: tuple[int, int], + camera_name: str = "", +) -> str | list: + # masks and zones are saved as relative coordinates + # we know if any points are > 1 then it is using the + # old native resolution coordinates + where = f" for camera {camera_name}" if camera_name else "" + if mask: + if isinstance(mask, list) and any(x > "1.0" for x in mask[0].split(",")): + relative_masks = [] + for m in mask: + points = m.split(",") + + if any(x > "1.0" for x in points): + rel_points = [] + for i in range(0, len(points), 2): + x = int(points[i]) + y = int(points[i + 1]) + + if x > frame_shape[1] or y > frame_shape[0]: + logger.error( + f"Not applying mask due to invalid coordinates{where}. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask." + ) + continue + + rel_points.append( + f"{round(x / frame_shape[1], 3)},{round(y / frame_shape[0], 3)}" + ) + + relative_masks.append(",".join(rel_points)) + else: + relative_masks.append(m) + + mask = relative_masks + elif isinstance(mask, str) and any(x > "1.0" for x in mask.split(",")): + points = mask.split(",") + rel_points = [] + + for i in range(0, len(points), 2): + x = int(points[i]) + y = int(points[i + 1]) + + if x > frame_shape[1] or y > frame_shape[0]: + logger.error( + f"Not applying mask due to invalid coordinates{where}. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask." + ) + return [] + + rel_points.append( + f"{round(x / frame_shape[1], 3)},{round(y / frame_shape[0], 3)}" + ) + + mask = ",".join(rel_points) + + return mask + + return mask + + +def convert_area_to_pixels( + area_value: int | float, frame_shape: tuple[int, int] +) -> int: + """ + Convert area specification to pixels. + + Args: + area_value: Area value (pixels or percentage) + frame_shape: Tuple of (height, width) for the frame + + Returns: + Area in pixels + """ + # If already an integer, assume it's in pixels + if isinstance(area_value, int): + return area_value + + # Check if it's a percentage + if isinstance(area_value, float): + if 0.000001 <= area_value <= 0.99: + frame_area = frame_shape[0] * frame_shape[1] + return max(1, int(frame_area * area_value)) + else: + raise ValueError( + f"Percentage must be between 0.000001 and 0.99, got {area_value}" + ) + + raise TypeError(f"Unexpected type for area: {type(area_value)}") + + +class StreamInfoRetriever: + def __init__(self) -> None: + self.stream_cache: dict[str, tuple[int, int]] = {} + + def get_stream_info(self, ffmpeg, path: str) -> str: + if path in self.stream_cache: + return self.stream_cache[path] + + info = asyncio.run(get_video_properties(ffmpeg, path)) + self.stream_cache[path] = info + return info + + +def apply_section_update(camera_config, section: str, update: dict) -> str | None: + """Merge an update dict into a camera config section and rebuild runtime variants. + + For motion and object filter sections, the plain Pydantic models are rebuilt + as RuntimeMotionConfig / RuntimeFilterConfig so that rasterized numpy masks + are recomputed. This mirrors the logic in FrigateConfig.post_validation. + + Args: + camera_config: The CameraConfig instance to update. + section: Config section name (e.g. "motion", "objects"). + update: Nested dict of field updates to merge. + + Returns: + None on success, or an error message string on failure. + """ + from frigate.config.config import RuntimeFilterConfig, RuntimeMotionConfig + + current = getattr(camera_config, section, None) + if current is None: + return f"Section '{section}' not found on camera '{camera_config.name}'" + + try: + frame_shape = camera_config.frame_shape + + if section == "motion": + merged = deep_merge( + current.model_dump(exclude_unset=True), + update, + override=True, + ) + camera_config.motion = RuntimeMotionConfig( + frame_shape=frame_shape, **merged + ) + + elif section == "objects": + merged = deep_merge( + current.model_dump(), + update, + override=True, + ) + new_objects = current.__class__.model_validate(merged) + + # Preserve private _all_objects from original config + try: + new_objects._all_objects = current._all_objects + except AttributeError: + pass + + # Rebuild RuntimeFilterConfig with merged global + per-object masks + for obj_name, filt in new_objects.filters.items(): + merged_mask = dict(filt.mask) + if new_objects.mask: + for gid, gmask in new_objects.mask.items(): + merged_mask[f"global_{gid}"] = gmask + + new_objects.filters[obj_name] = RuntimeFilterConfig( + frame_shape=frame_shape, + mask=merged_mask, + **filt.model_dump(exclude_unset=True, exclude={"mask", "raw_mask"}), + ) + camera_config.objects = new_objects + + elif section == "detect": + # apply detect first so frame_shape reflects the new resolution + # before we rebuild mask-dependent runtime configs below + merged = deep_merge(current.model_dump(), update, override=True) + camera_config.detect = current.__class__.model_validate(merged) + + new_frame_shape = camera_config.frame_shape + + # rebuild motion's rasterized_mask at the new frame_shape + if camera_config.motion is not None: + camera_config.motion = RuntimeMotionConfig( + frame_shape=new_frame_shape, + **camera_config.motion.model_dump(exclude_unset=True), + ) + + # rebuild per-object filter masks at the new frame_shape + for obj_name, filt in camera_config.objects.filters.items(): + merged_mask = dict(filt.mask) + if camera_config.objects.mask: + for gid, gmask in camera_config.objects.mask.items(): + merged_mask[f"global_{gid}"] = gmask + + camera_config.objects.filters[obj_name] = RuntimeFilterConfig( + frame_shape=new_frame_shape, + mask=merged_mask, + **filt.model_dump(exclude_unset=True, exclude={"mask", "raw_mask"}), + ) + + # Regenerate zone contours and per-zone filter masks at the new + # frame_shape so zone outlines and membership stay relative + for zone in camera_config.zones.values(): + if zone.filters: + for zone_obj_name, zone_filter in zone.filters.items(): + zone.filters[zone_obj_name] = RuntimeFilterConfig( + frame_shape=new_frame_shape, + **zone_filter.model_dump(exclude_unset=True), + ) + zone.generate_contour(new_frame_shape) + + else: + merged = deep_merge(current.model_dump(), update, override=True) + setattr(camera_config, section, current.__class__.model_validate(merged)) + + except Exception: + logger.exception("Config validation error") + return "Validation error. Check logs for details." + + return None diff --git a/frigate/util/downloader.py b/frigate/util/downloader.py new file mode 100644 index 0000000..a8b593f --- /dev/null +++ b/frigate/util/downloader.py @@ -0,0 +1,120 @@ +import logging +import os +import threading +from collections.abc import Callable +from pathlib import Path + +import requests + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import UPDATE_MODEL_STATE +from frigate.types import ModelStatusTypesEnum +from frigate.util.file import FileLock + +logger = logging.getLogger(__name__) + + +class ModelDownloader: + def __init__( + self, + model_name: str, + download_path: str, + file_names: list[str], + download_func: Callable[[str], None], + complete_func: Callable[[], None] | None = None, + silent: bool = False, + ): + self.model_name = model_name + self.download_path = download_path + self.file_names = file_names + self.download_func = download_func + self.complete_func = complete_func + self.silent = silent + self.requestor = InterProcessRequestor() + self.download_thread = None + self.download_complete = threading.Event() + + def ensure_model_files(self): + self.mark_files_state( + self.requestor, + self.model_name, + self.file_names, + ModelStatusTypesEnum.downloading, + ) + self.download_thread = threading.Thread( + target=self._download_models, + name=f"_download_model_{self.model_name}", + daemon=True, + ) + self.download_thread.start() + + def _download_models(self): + for file_name in self.file_names: + path = os.path.join(self.download_path, file_name) + lock_path = f"{path}.lock" + lock = FileLock(lock_path, cleanup_stale_on_init=True) + + if not os.path.exists(path): + with lock: + if not os.path.exists(path): + self.download_func(path) + + self.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.downloaded, + }, + ) + + if self.complete_func: + self.complete_func() + + self.requestor.stop() + self.download_complete.set() + + @staticmethod + def download_from_url(url: str, save_path: str, silent: bool = False) -> Path: + temporary_filename = Path(save_path).with_name( + os.path.basename(save_path) + ".part" + ) + temporary_filename.parent.mkdir(parents=True, exist_ok=True) + + if not silent: + logger.info(f"Downloading model file from: {url}") + + try: + with requests.get(url, stream=True, allow_redirects=True) as r: + r.raise_for_status() + with open(temporary_filename, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + temporary_filename.rename(save_path) + except Exception as e: + logger.error(f"Error downloading model: {str(e)}") + raise + + if not silent: + logger.info(f"Downloading complete: {url}") + + return Path(save_path) + + @staticmethod + def mark_files_state( + requestor: InterProcessRequestor, + model_name: str, + files: list[str], + state: ModelStatusTypesEnum, + ) -> None: + for file_name in files: + requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{model_name}-{file_name}", + "state": state, + }, + ) + + def wait_for_download(self): + self.download_complete.wait() diff --git a/frigate/util/ffmpeg.py b/frigate/util/ffmpeg.py new file mode 100644 index 0000000..87601b9 --- /dev/null +++ b/frigate/util/ffmpeg.py @@ -0,0 +1,171 @@ +"""FFmpeg utility functions for managing ffmpeg processes.""" + +import logging +import subprocess as sp +from collections.abc import Callable +from typing import Any + +from frigate.const import PROCESS_PRIORITY_LOW +from frigate.log import LogPipe + + +def stop_ffmpeg(ffmpeg_process: sp.Popen[Any], logger: logging.Logger): + logger.info("Terminating the existing ffmpeg process...") + ffmpeg_process.terminate() + try: + logger.info("Waiting for ffmpeg to exit gracefully...") + ffmpeg_process.communicate(timeout=30) + logger.info("FFmpeg has exited") + except sp.TimeoutExpired: + logger.info("FFmpeg didn't exit. Force killing...") + ffmpeg_process.kill() + ffmpeg_process.communicate() + logger.info("FFmpeg has been killed") + ffmpeg_process = None + + +def start_or_restart_ffmpeg( + ffmpeg_cmd, logger, logpipe: LogPipe, frame_size=None, ffmpeg_process=None +) -> sp.Popen[Any]: + if ffmpeg_process is not None: + stop_ffmpeg(ffmpeg_process, logger) + + if frame_size is None: + process = sp.Popen( + ffmpeg_cmd, + stdout=sp.DEVNULL, + stderr=logpipe, + stdin=sp.DEVNULL, + start_new_session=True, + ) + else: + process = sp.Popen( + ffmpeg_cmd, + stdout=sp.PIPE, + stderr=logpipe, + stdin=sp.DEVNULL, + bufsize=frame_size * 10, + start_new_session=True, + ) + return process + + +logger = logging.getLogger(__name__) + + +def inject_progress_flags(cmd: list[str]) -> list[str]: + """Insert `-progress pipe:2 -nostats` immediately before the output path. + + `-progress pipe:2` writes structured key=value lines to stderr; + `-nostats` suppresses the noisy default stats output. The output path + is conventionally the last token in an FFmpeg argv. + """ + if not cmd: + return cmd + return cmd[:-1] + ["-progress", "pipe:2", "-nostats", cmd[-1]] + + +def run_ffmpeg_with_progress( + cmd: list[str], + *, + expected_duration_seconds: float, + on_progress: Callable[[float], None] | None = None, + stdin_payload: str | None = None, + process_started: Callable[[sp.Popen], None] | None = None, + use_low_priority: bool = True, +) -> tuple[int, str]: + """Run an ffmpeg command, streaming progress via `-progress pipe:2`. + + Args: + cmd: ffmpeg argv. Output path must be the last token. + expected_duration_seconds: Duration of the expected output clip in + seconds. Used to convert ffmpeg's `out_time_us` into a percent. + on_progress: Optional callback invoked with a percent in [0, 100]. + Called once with 0.0 at start, again on each `out_time_us=` + stderr line, and once with 100.0 on `progress=end`. + stdin_payload: Optional string written to ffmpeg stdin (used by + export for concat playlists). + process_started: Optional callback invoked with the live `Popen` + once spawned — lets callers store the ref for cancellation. + use_low_priority: When True, prepend `nice -n PROCESS_PRIORITY_LOW` + so concat doesn't starve detection. + + Returns: + Tuple of `(returncode, captured_stderr)`. Stdout is left attached + to the parent process to avoid buffer-full deadlocks. + """ + full_cmd = inject_progress_flags(cmd) + if use_low_priority: + full_cmd = ["nice", "-n", str(PROCESS_PRIORITY_LOW)] + full_cmd + + def emit(percent: float) -> None: + if on_progress is None: + return + try: + on_progress(max(0.0, min(100.0, percent))) + except Exception: + logger.exception("FFmpeg progress callback failed") + + emit(0.0) + + proc = sp.Popen( + full_cmd, + stdin=sp.PIPE if stdin_payload is not None else None, + stderr=sp.PIPE, + text=True, + encoding="ascii", + errors="replace", + ) + if process_started is not None: + try: + process_started(proc) + except Exception: + logger.exception("FFmpeg process_started callback failed") + + if stdin_payload is not None and proc.stdin is not None: + try: + proc.stdin.write(stdin_payload) + except (BrokenPipeError, OSError): + pass + finally: + try: + proc.stdin.close() + except (BrokenPipeError, OSError): + pass + + captured: list[str] = [] + if proc.stderr is not None: + try: + for raw_line in proc.stderr: + captured.append(raw_line) + line = raw_line.strip() + if not line: + continue + if line.startswith("out_time_us="): + if expected_duration_seconds <= 0: + continue + try: + out_time_us = int(line.split("=", 1)[1]) + except (ValueError, IndexError): + continue + if out_time_us < 0: + continue + out_seconds = out_time_us / 1_000_000.0 + emit((out_seconds / expected_duration_seconds) * 100.0) + elif line == "progress=end": + emit(100.0) + break + except Exception: + logger.exception("Failed reading FFmpeg progress stream") + + proc.wait() + + if proc.stderr is not None: + try: + remaining = proc.stderr.read() + if remaining: + captured.append(remaining) + except Exception: + pass + + return proc.returncode or 0, "".join(captured) diff --git a/frigate/util/file.py b/frigate/util/file.py new file mode 100644 index 0000000..e259d13 --- /dev/null +++ b/frigate/util/file.py @@ -0,0 +1,476 @@ +"""Path and file utilities.""" + +import base64 +import fcntl +import logging +import os +import time +from datetime import datetime +from pathlib import Path +from typing import Any + +import cv2 +from numpy import ndarray + +from frigate.const import CLIPS_DIR, THUMB_DIR +from frigate.models import Event +from frigate.util.image import get_snapshot_bytes, relative_box_to_absolute + +logger = logging.getLogger(__name__) + + +def get_event_thumbnail_bytes(event: Event) -> bytes | None: + if event.thumbnail: + return base64.b64decode(event.thumbnail) + else: + try: + with open( + os.path.join(THUMB_DIR, event.camera, f"{event.id}.webp"), "rb" + ) as f: + return f.read() + except Exception: + return None + + +def get_event_snapshot(event: Event) -> ndarray | None: + image, _ = load_event_snapshot_image(event) + return image + + +def get_event_snapshot_path( + event: Event, *, clean_only: bool = False +) -> tuple[str | None, bool]: + clean_snapshot_paths = [ + os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}-clean.webp"), + os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}-clean.png"), + ] + + for image_path in clean_snapshot_paths: + if os.path.exists(image_path): + return image_path, True + + snapshot_path = os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.jpg") + if not os.path.exists(snapshot_path): + return None, False + + # Legacy JPG snapshots may already include overlays, so they should never + # be treated as clean input for additional rendering. + if clean_only: + return None, False + + return snapshot_path, False + + +def load_event_snapshot_image( + event: Event, *, clean_only: bool = False +) -> tuple[ndarray | None, bool]: + image_path, is_clean_snapshot = get_event_snapshot_path( + event, clean_only=clean_only + ) + if image_path is None: + return None, False + + image = cv2.imread(image_path) + if image is None: + logger.warning("Unable to load snapshot from %s", image_path) + return None, False + + return image, is_clean_snapshot + + +def _get_event_snapshot_overlay_boxes( + frame_shape: tuple[int, ...], event: Event +) -> list[dict[str, Any]]: + overlay_boxes: list[dict[str, Any]] = [] + draw_data = event.data.get("draw") if event.data else {} + draw_boxes = draw_data.get("boxes", []) if isinstance(draw_data, dict) else [] + + for draw_box in draw_boxes: + box = relative_box_to_absolute(frame_shape, draw_box.get("box")) + if box is None: + continue + + draw_color = draw_box.get("color", (255, 0, 0)) + color = ( + tuple(draw_color) if isinstance(draw_color, (list, tuple)) else (255, 0, 0) + ) + overlay_boxes.append( + { + "box": box, + "label": event.label, + "score": draw_box.get("score"), + "color": color, + } + ) + + return overlay_boxes + + +def get_event_snapshot_bytes( + event: Event, + *, + ext: str, + timestamp: bool = False, + bounding_box: bool = False, + crop: bool = False, + height: int | None = None, + quality: int | None = None, + timestamp_style: Any | None = None, + colormap: dict[str, tuple[int, int, int]] | None = None, +) -> tuple[bytes | None, float]: + best_frame, is_clean_snapshot = load_event_snapshot_image(event) + if best_frame is None: + return None, 0 + + frame_time = _get_event_snapshot_frame_time(event) + box = relative_box_to_absolute( + best_frame.shape, + event.data.get("box") if event.data else None, + ) + overlay_boxes = _get_event_snapshot_overlay_boxes(best_frame.shape, event) + + if (bounding_box or crop or timestamp) and not is_clean_snapshot: + logger.warning( + "Unable to fully honor snapshot query parameters for completed event %s because the clean snapshot is unavailable.", + event.id, + ) + + return get_snapshot_bytes( + best_frame, + frame_time, + ext=ext, + timestamp=timestamp and is_clean_snapshot, + bounding_box=bounding_box and is_clean_snapshot, + crop=crop and is_clean_snapshot, + height=height, + quality=quality, + label=event.label, + box=box, + score=_get_event_snapshot_score(event), + area=_get_event_snapshot_area(event), + attributes=_get_event_snapshot_attributes( + best_frame.shape, + event.data.get("attributes") if event.data else None, + ), + color=(colormap or {}).get(event.label, (255, 255, 255)), + overlay_boxes=overlay_boxes, + timestamp_style=timestamp_style, + estimated_speed=_get_event_snapshot_estimated_speed(event), + ) + + +def _as_timestamp(value: Any) -> float: + if isinstance(value, datetime): + return value.timestamp() + + return float(value) + + +def _get_event_snapshot_frame_time(event: Event) -> float: + if event.data: + snapshot_frame_time = event.data.get("snapshot_frame_time") + if snapshot_frame_time is not None: + return _as_timestamp(snapshot_frame_time) + + frame_time = event.data.get("frame_time") + if frame_time is not None: + return _as_timestamp(frame_time) + + return _as_timestamp(event.start_time) + + +def _get_event_snapshot_attributes( + frame_shape: tuple[int, ...], attributes: list[dict[str, Any]] | None +) -> list[dict[str, Any]]: + absolute_attributes: list[dict[str, Any]] = [] + + for attribute in attributes or []: + box = relative_box_to_absolute(frame_shape, attribute.get("box")) + if box is None: + continue + + absolute_attributes.append( + { + "box": box, + "label": attribute.get("label", "attribute"), + "score": attribute.get("score", 0), + } + ) + + return absolute_attributes + + +def _get_event_snapshot_score(event: Event) -> float: + if event.data: + score = event.data.get("score") + if score is not None: + return score + + top_score = event.data.get("top_score") + if top_score is not None: + return top_score + + return event.top_score or event.score or 0 + + +def _get_event_snapshot_area(event: Event) -> int | None: + if event.data: + area = event.data.get("snapshot_area") + if area is not None: + return int(area) + + return None + + +def _get_event_snapshot_estimated_speed(event: Event) -> float: + if event.data: + estimated_speed = event.data.get("snapshot_estimated_speed") + if estimated_speed is not None: + return float(estimated_speed) + + average_speed = event.data.get("average_estimated_speed") + if average_speed is not None: + return float(average_speed) + + return 0 + + +### Deletion + + +def delete_event_images(event: Event) -> bool: + return delete_event_snapshot(event) and delete_event_thumbnail(event) + + +def delete_event_snapshot(event: Event) -> bool: + media_name = f"{event.camera}-{event.id}" + media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") + + try: + media_path.unlink(missing_ok=True) + media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.webp") + media_path.unlink(missing_ok=True) + # also delete clean.png (legacy) for backward compatibility + media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png") + media_path.unlink(missing_ok=True) + return True + except OSError: + return False + + +def delete_event_thumbnail(event: Event) -> bool: + if event.thumbnail: + return True + else: + Path(os.path.join(THUMB_DIR, event.camera, f"{event.id}.webp")).unlink( + missing_ok=True + ) + return True + + +### File Locking + + +class FileLock: + """ + A file-based lock for coordinating access to resources across processes. + + Uses fcntl.flock() for proper POSIX file locking on Linux. Supports timeouts, + stale lock detection, and can be used as a context manager. + + Example: + ```python + # Using as a context manager (recommended) + with FileLock("/path/to/resource.lock", timeout=60): + # Critical section + do_something() + + # Manual acquisition and release + lock = FileLock("/path/to/resource.lock") + if lock.acquire(timeout=60): + try: + do_something() + finally: + lock.release() + ``` + + Attributes: + lock_path: Path to the lock file + timeout: Maximum time to wait for lock acquisition (seconds) + poll_interval: Time to wait between lock acquisition attempts (seconds) + stale_timeout: Time after which a lock is considered stale (seconds) + """ + + def __init__( + self, + lock_path: str | Path, + timeout: int = 300, + poll_interval: float = 1.0, + stale_timeout: int = 600, + cleanup_stale_on_init: bool = False, + ): + """ + Initialize a FileLock. + + Args: + lock_path: Path to the lock file + timeout: Maximum time to wait for lock acquisition in seconds (default: 300) + poll_interval: Time to wait between lock attempts in seconds (default: 1.0) + stale_timeout: Time after which a lock is considered stale in seconds (default: 600) + cleanup_stale_on_init: Whether to clean up stale locks on initialization (default: False) + """ + self.lock_path = Path(lock_path) + self.timeout = timeout + self.poll_interval = poll_interval + self.stale_timeout = stale_timeout + self._fd: int | None = None + self._acquired = False + + if cleanup_stale_on_init: + self._cleanup_stale_lock() + + def _cleanup_stale_lock(self) -> bool: + """ + Clean up a stale lock file if it exists and is old. + + Returns: + True if lock was cleaned up, False otherwise + """ + try: + if self.lock_path.exists(): + # Check if lock file is older than stale_timeout + lock_age = time.time() - self.lock_path.stat().st_mtime + if lock_age > self.stale_timeout: + logger.warning( + f"Removing stale lock file: {self.lock_path} (age: {lock_age:.1f}s)" + ) + self.lock_path.unlink() + return True + except Exception as e: + logger.error(f"Error cleaning up stale lock: {e}") + + return False + + def is_stale(self) -> bool: + """ + Check if the lock file is stale (older than stale_timeout). + + Returns: + True if lock is stale, False otherwise + """ + try: + if self.lock_path.exists(): + lock_age = time.time() - self.lock_path.stat().st_mtime + return lock_age > self.stale_timeout + except Exception: + pass + + return False + + def acquire(self, timeout: int | None = None) -> bool: + """ + Acquire the file lock using fcntl.flock(). + + Args: + timeout: Maximum time to wait for lock in seconds (uses instance timeout if None) + + Returns: + True if lock acquired, False if timeout or error + """ + if self._acquired: + logger.warning(f"Lock already acquired: {self.lock_path}") + return True + + if timeout is None: + timeout = self.timeout + + # Ensure parent directory exists + self.lock_path.parent.mkdir(parents=True, exist_ok=True) + + # Clean up stale lock before attempting to acquire + self._cleanup_stale_lock() + + try: + self._fd = os.open(self.lock_path, os.O_CREAT | os.O_RDWR) + + start_time = time.time() + while time.time() - start_time < timeout: + try: + fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + self._acquired = True + logger.debug(f"Acquired lock: {self.lock_path}") + return True + except OSError: + # Lock is held by another process + if time.time() - start_time >= timeout: + logger.warning(f"Timeout waiting for lock: {self.lock_path}") + os.close(self._fd) + self._fd = None + return False + + time.sleep(self.poll_interval) + + # Timeout reached + if self._fd is not None: + os.close(self._fd) + self._fd = None + return False + + except Exception as e: + logger.error(f"Error acquiring lock: {e}") + if self._fd is not None: + try: + os.close(self._fd) + except Exception: + pass + self._fd = None + return False + + def release(self) -> None: + """ + Release the file lock. + + This closes the file descriptor and removes the lock file. + """ + if not self._acquired: + return + + try: + # Close file descriptor and release fcntl lock + if self._fd is not None: + try: + fcntl.flock(self._fd, fcntl.LOCK_UN) + os.close(self._fd) + except Exception as e: + logger.warning(f"Error closing lock file descriptor: {e}") + finally: + self._fd = None + + # Remove lock file + if self.lock_path.exists(): + self.lock_path.unlink() + logger.debug(f"Released lock: {self.lock_path}") + + except FileNotFoundError: + # Lock file already removed, that's fine + pass + except Exception as e: + logger.error(f"Error releasing lock: {e}") + finally: + self._acquired = False + + def __enter__(self): + """Context manager entry - acquire the lock.""" + if not self.acquire(): + raise TimeoutError(f"Failed to acquire lock: {self.lock_path}") + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - release the lock.""" + self.release() + return False + + def __del__(self): + """Destructor - ensure lock is released.""" + if self._acquired: + self.release() diff --git a/frigate/util/image.py b/frigate/util/image.py new file mode 100644 index 0000000..dde25e2 --- /dev/null +++ b/frigate/util/image.py @@ -0,0 +1,1297 @@ +"""Utilities for creating and manipulating image frames.""" + +import datetime +import logging +import subprocess as sp +import threading +from abc import ABC, abstractmethod +from multiprocessing import resource_tracker as _mprt +from multiprocessing import shared_memory as _mpshm +from string import printable +from typing import Any, AnyStr + +import cv2 +import numpy as np +from unidecode import unidecode + +logger = logging.getLogger(__name__) + + +def transliterate_to_latin(text: str) -> str: + """ + Transliterate a given text to Latin. + + This function uses the unidecode library to transliterate the input text to Latin. + It is useful for converting texts with diacritics or non-Latin characters to a + Latin equivalent. + + Args: + text (str): The text to be transliterated. + + Returns: + str: The transliterated text. + + Example: + >>> transliterate_to_latin('frégate') + 'fregate' + """ + return unidecode(text) + + +def on_edge(box, frame_shape): + if ( + box[0] == 0 + or box[1] == 0 + or box[2] == frame_shape[1] - 1 + or box[3] == frame_shape[0] - 1 + ): + return True + + +def has_better_attr(current_thumb, new_obj, attr_label) -> bool: + max_new_attr = max( + [0] + + [area(a["box"]) for a in new_obj["attributes"] if a["label"] == attr_label] + ) + max_current_attr = max( + [0] + + [ + area(a["box"]) + for a in current_thumb["attributes"] + if a["label"] == attr_label + ] + ) + + # if the thumb has a higher scoring attr + return max_new_attr > max_current_attr + + +def is_better_thumbnail( + label: str, + current_thumb: dict[str, Any], + new_obj: dict[str, Any], + frame_shape: tuple[int, int], +) -> bool: + # larger is better + # cutoff images are less ideal, but they should also be smaller? + # better scores are obviously better too + + # check face on person + if label == "person": + if has_better_attr(current_thumb, new_obj, "face"): + return True + # if the current thumb has a face attr, dont update unless it gets better + if any([a["label"] == "face" for a in current_thumb["attributes"]]): + return False + + # check license_plate on car + if label in ["car", "motorcycle"]: + if has_better_attr(current_thumb, new_obj, "license_plate"): + return True + # if the current thumb has a license_plate attr, dont update unless it gets better + if any([a["label"] == "license_plate" for a in current_thumb["attributes"]]): + return False + + # if the new_thumb is on an edge, and the current thumb is not + if on_edge(new_obj["box"], frame_shape) and not on_edge( + current_thumb["box"], frame_shape + ): + return False + + # if the score is better by more than 5% + if new_obj["score"] > current_thumb["score"] + 0.05: + return True + + # if the area is 10% larger + if new_obj["area"] > current_thumb["area"] * 1.1: + return True + + return False + + +def draw_timestamp( + frame, + timestamp, + timestamp_format, + font_effect=None, + font_thickness=2, + font_color=(255, 255, 255), + position="tl", +): + time_to_show = datetime.datetime.fromtimestamp(timestamp).strftime(timestamp_format) + + # calculate a dynamic font size + size = cv2.getTextSize( + time_to_show, + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=1.0, + thickness=font_thickness, + ) + + text_width = size[0][0] + desired_size = max(150, 0.33 * frame.shape[1]) + font_scale = desired_size / text_width + + # calculate the actual size with the dynamic scale + size = cv2.getTextSize( + time_to_show, + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + thickness=font_thickness, + ) + + image_width = frame.shape[1] + image_height = frame.shape[0] + text_width = size[0][0] + text_height = size[0][1] + line_height = text_height + size[1] + + if position == "tl": + text_offset_x = 0 + text_offset_y = 0 if 0 < line_height else 0 - (line_height + 8) + elif position == "tr": + text_offset_x = image_width - text_width + text_offset_y = 0 if 0 < line_height else 0 - (line_height + 8) + elif position == "bl": + text_offset_x = 0 + text_offset_y = image_height - (line_height + 8) + elif position == "br": + text_offset_x = image_width - text_width + text_offset_y = image_height - (line_height + 8) + + if font_effect == "solid": + # make the coords of the box with a small padding of two pixels + timestamp_box_coords = np.array( + [ + [text_offset_x, text_offset_y], + [text_offset_x + text_width, text_offset_y], + [text_offset_x + text_width, text_offset_y + line_height + 8], + [text_offset_x, text_offset_y + line_height + 8], + ] + ) + + cv2.fillPoly( + frame, + [timestamp_box_coords], + # inverse color of text for background for max. contrast + (255 - font_color[0], 255 - font_color[1], 255 - font_color[2]), + ) + elif font_effect == "shadow": + cv2.putText( + frame, + time_to_show, + (text_offset_x + 3, text_offset_y + line_height), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + color=(255 - font_color[0], 255 - font_color[1], 255 - font_color[2]), + thickness=font_thickness, + ) + + cv2.putText( + frame, + time_to_show, + (text_offset_x, text_offset_y + line_height - 3), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + color=font_color, + thickness=font_thickness, + ) + + +def draw_box_with_label( + frame, + x_min, + y_min, + x_max, + y_max, + label, + info, + thickness=2, + color=None, + position="ul", +): + if color is None: + color = (0, 0, 255) + try: + display_text = transliterate_to_latin("{}: {}".format(label, info)) + except Exception: + display_text = "{}: {}".format(label, info) + cv2.rectangle(frame, (x_min, y_min), (x_max, y_max), color, thickness) + font_scale = 0.5 + font = cv2.FONT_HERSHEY_SIMPLEX + # get the width and height of the text box + size = cv2.getTextSize(display_text, font, fontScale=font_scale, thickness=2) + text_width = size[0][0] + text_height = size[0][1] + line_height = text_height + size[1] + # get frame height + frame_height = frame.shape[0] + # set the text start position + if position == "ul": + text_offset_x = x_min + text_offset_y = max(0, y_min - (line_height + 8)) + elif position == "ur": + text_offset_x = max(0, x_max - (text_width + 8)) + text_offset_y = max(0, y_min - (line_height + 8)) + elif position == "bl": + text_offset_x = x_min + text_offset_y = min(frame_height - line_height, y_max) + elif position == "br": + text_offset_x = max(0, x_max - (text_width + 8)) + text_offset_y = min(frame_height - line_height, y_max) + # Adjust position if it overlaps with the box or goes out of frame + if position in {"ul", "ur"}: + if text_offset_y < y_min + thickness: # Label overlaps with the box + if y_min - (line_height + 8) < 0 and y_max + line_height <= frame_height: + # Not enough space above, and there is space below + text_offset_y = y_max + elif y_min - (line_height + 8) >= 0: + # Enough space above, keep the label at the top + text_offset_y = max(0, y_min - (line_height + 8)) + elif position in {"bl", "br"}: + if text_offset_y + line_height > frame_height: + # If there's not enough space below, try above the box + text_offset_y = max(0, y_min - (line_height + 8)) + + # make the coords of the box with a small padding of two pixels + textbox_coords = ( + (text_offset_x, text_offset_y), + (text_offset_x + text_width + 2, text_offset_y + line_height), + ) + cv2.rectangle(frame, textbox_coords[0], textbox_coords[1], color, cv2.FILLED) + cv2.putText( + frame, + display_text, + (text_offset_x, text_offset_y + line_height - 3), + font, + fontScale=font_scale, + color=(0, 0, 0), + thickness=2, + ) + + +def get_image_quality_params(ext: str, quality: int | None) -> list[int]: + if ext in ("jpg", "jpeg"): + return [int(cv2.IMWRITE_JPEG_QUALITY), quality if quality is not None else 70] + + if ext == "webp": + return [int(cv2.IMWRITE_WEBP_QUALITY), quality if quality is not None else 60] + + return [] + + +def relative_box_to_absolute( + frame_shape: tuple[int, ...], box: list[float] | tuple[float, ...] | None +) -> tuple[int, int, int, int] | None: + if box is None or len(box) != 4: + return None + + frame_height = frame_shape[0] + frame_width = frame_shape[1] + x_min = int(box[0] * frame_width) + y_min = int(box[1] * frame_height) + x_max = x_min + int(box[2] * frame_width) + y_max = y_min + int(box[3] * frame_height) + + x_min = max(0, min(frame_width - 1, x_min)) + y_min = max(0, min(frame_height - 1, y_min)) + x_max = max(x_min + 1, min(frame_width - 1, x_max)) + y_max = max(y_min + 1, min(frame_height - 1, y_max)) + + return (x_min, y_min, x_max, y_max) + + +def _format_snapshot_label( + score: float | None, + area: int | None, + box: tuple[int, int, int, int] | None, + estimated_speed: float = 0, +) -> str: + score_value = score or 0 + score_text = ( + f"{int(score_value * 100)}%" if score_value <= 1 else f"{int(score_value)}%" + ) + + if area is None and box is not None: + area = int((box[2] - box[0]) * (box[3] - box[1])) + + label = f"{score_text} {int(area or 0)}" + if estimated_speed: + label = f"{label} {estimated_speed:.1f}" + + return label + + +def draw_snapshot_bounding_boxes( + frame: np.ndarray, + label: str, + box: tuple[int, int, int, int] | None, + score: float | None, + area: int | None, + attributes: list[dict[str, Any]] | None, + color: tuple[int, int, int], + estimated_speed: float = 0, +) -> None: + if box is None: + return + + draw_box_with_label( + frame, + box[0], + box[1], + box[2], + box[3], + label, + _format_snapshot_label(score, area, box, estimated_speed), + thickness=2, + color=color, + ) + + for attribute in attributes or []: + attribute_box = attribute.get("box") + if attribute_box is None: + continue + + box_area = int( + (attribute_box[2] - attribute_box[0]) + * (attribute_box[3] - attribute_box[1]) + ) + draw_box_with_label( + frame, + attribute_box[0], + attribute_box[1], + attribute_box[2], + attribute_box[3], + attribute.get("label", "attribute"), + f"{attribute.get('score', 0):.0%} {box_area}", + thickness=2, + color=color, + ) + + +def _get_snapshot_overlay_box_label( + score: float | int | None, box: tuple[int, int, int, int] +) -> str: + area = int((box[2] - box[0]) * (box[3] - box[1])) + + if score is None: + return f"- {area}" + + score_value = float(score) + score_text = ( + f"{int(score_value * 100)}%" if score_value <= 1 else f"{int(score_value)}%" + ) + return f"{score_text} {area}" + + +def draw_snapshot_overlay_boxes( + frame: np.ndarray, + overlay_boxes: list[dict[str, Any]] | None, + default_label: str, + default_color: tuple[int, int, int], +) -> None: + for overlay_box in overlay_boxes or []: + box = overlay_box.get("box") + if box is None: + continue + + box_color = overlay_box.get("color", default_color) + color = ( + tuple(box_color) if isinstance(box_color, (list, tuple)) else default_color + ) + draw_box_with_label( + frame, + box[0], + box[1], + box[2], + box[3], + overlay_box.get("label", default_label), + _get_snapshot_overlay_box_label(overlay_box.get("score"), box), + thickness=2, + color=color, + ) + + +def get_snapshot_bytes( + frame: np.ndarray, + frame_time: float, + ext: str, + *, + timestamp: bool = False, + bounding_box: bool = False, + crop: bool = False, + height: int | None = None, + quality: int | None = None, + label: str, + box: tuple[int, int, int, int] | None, + score: float | None, + area: int | None, + attributes: list[dict[str, Any]] | None, + color: tuple[int, int, int], + overlay_boxes: list[dict[str, Any]] | None = None, + timestamp_style: Any | None = None, + estimated_speed: float = 0, +) -> tuple[bytes | None, float]: + best_frame = frame.copy() + crop_box = box + + if crop_box is None and overlay_boxes and len(overlay_boxes) == 1: + crop_box = overlay_boxes[0].get("box") + + if bounding_box and box: + draw_snapshot_bounding_boxes( + best_frame, + label, + box, + score, + area, + attributes, + color, + estimated_speed, + ) + + if bounding_box and overlay_boxes: + draw_snapshot_overlay_boxes(best_frame, overlay_boxes, label, color) + + if crop and crop_box: + region = calculate_region( + best_frame.shape, + crop_box[0], + crop_box[1], + crop_box[2], + crop_box[3], + 300, + multiplier=1.1, + ) + best_frame = best_frame[region[1] : region[3], region[0] : region[2]] + + if height: + width = int(height * best_frame.shape[1] / best_frame.shape[0]) + best_frame = cv2.resize( + best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA + ) + + if timestamp and timestamp_style is not None: + colors = timestamp_style.color + draw_timestamp( + best_frame, + frame_time, + timestamp_style.format, + font_effect=timestamp_style.effect, + font_thickness=timestamp_style.thickness, + font_color=(colors.blue, colors.green, colors.red), + position=timestamp_style.position, + ) + + ret, img = cv2.imencode( + f".{ext}", best_frame, get_image_quality_params(ext, quality) + ) + + if ret: + return img.tobytes(), frame_time + + return None, frame_time + + +def grab_cv2_contours(cnts): + # if the length the contours tuple returned by cv2.findContours + # is '2' then we are using either OpenCV v2.4, v4-beta, or + # v4-official + if len(cnts) == 2: + return cnts[0] + + # if the length of the contours tuple is '3' then we are using + # either OpenCV v3, v4-pre, or v4-alpha + elif len(cnts) == 3: + return cnts[1] + + +def is_label_printable(label) -> bool: + """Check if label is printable.""" + return not bool(set(label) - set(printable)) + + +def calculate_region(frame_shape, xmin, ymin, xmax, ymax, model_size, multiplier=2): + # size is the longest edge and divisible by 4 + size = int((max(xmax - xmin, ymax - ymin) * multiplier) // 4 * 4) + # dont go any smaller than the model_size + if size < model_size: + size = model_size + + # x_offset is midpoint of bounding box minus half the size + x_offset = int((xmax - xmin) / 2.0 + xmin - size / 2.0) + # if outside the image + if x_offset < 0: + x_offset = 0 + elif x_offset > (frame_shape[1] - size): + x_offset = max(0, (frame_shape[1] - size)) + + # y_offset is midpoint of bounding box minus half the size + y_offset = int((ymax - ymin) / 2.0 + ymin - size / 2.0) + # # if outside the image + if y_offset < 0: + y_offset = 0 + elif y_offset > (frame_shape[0] - size): + y_offset = max(0, (frame_shape[0] - size)) + + return (x_offset, y_offset, x_offset + size, y_offset + size) + + +def calculate_16_9_crop(frame_shape, xmin, ymin, xmax, ymax, multiplier=1.25): + min_size = 200 + + # size is the longest edge and divisible by 4 + x_size = int((xmax - xmin) * multiplier) + + if x_size < min_size: + x_size = min_size + + y_size = int((ymax - ymin) * multiplier) + + if y_size < min_size: + y_size = min_size + + if frame_shape[1] / frame_shape[0] > 16 / 9 and x_size / y_size > 4: + return None + + # calculate 16x9 using height + aspect_y_size = int(9 / 16 * x_size) + + # if 16:9 by height is too small + if aspect_y_size < y_size or aspect_y_size > frame_shape[0]: + x_size = int((16 / 9) * y_size) // 4 * 4 + + if x_size / y_size > 1.8: + return None + else: + y_size = aspect_y_size // 4 * 4 + + # x_offset is midpoint of bounding box minus half the size + x_offset = int((xmax - xmin) / 2.0 + xmin - x_size / 2.0) + # if outside the image + if x_offset < 0: + x_offset = 0 + elif x_offset > (frame_shape[1] - x_size): + x_offset = max(0, (frame_shape[1] - x_size)) + + # y_offset is midpoint of bounding box minus half the size + y_offset = int((ymax - ymin) / 2.0 + ymin - y_size / 2.0) + # # if outside the image + if y_offset < 0: + y_offset = 0 + elif y_offset > (frame_shape[0] - y_size): + y_offset = max(0, (frame_shape[0] - y_size)) + + return (x_offset, y_offset, x_offset + x_size, y_offset + y_size) + + +def get_yuv_crop(frame_shape, crop): + # crop should be (x1,y1,x2,y2) + frame_height = frame_shape[0] // 3 * 2 + frame_width = frame_shape[1] + + # compute the width/height of the uv channels + uv_width = frame_width // 2 # width of the uv channels + uv_height = frame_height // 4 # height of the uv channels + + # compute the offset for upper left corner of the uv channels + uv_x_offset = crop[0] // 2 # x offset of the uv channels + uv_y_offset = crop[1] // 4 # y offset of the uv channels + + # compute the width/height of the uv crops + uv_crop_width = (crop[2] - crop[0]) // 2 # width of the cropped uv channels + uv_crop_height = (crop[3] - crop[1]) // 4 # height of the cropped uv channels + + # ensure crop dimensions are multiples of 2 and 4 + y = (crop[0], crop[1], crop[0] + uv_crop_width * 2, crop[1] + uv_crop_height * 4) + + u1 = ( + 0 + uv_x_offset, + frame_height + uv_y_offset, + 0 + uv_x_offset + uv_crop_width, + frame_height + uv_y_offset + uv_crop_height, + ) + + u2 = ( + uv_width + uv_x_offset, + frame_height + uv_y_offset, + uv_width + uv_x_offset + uv_crop_width, + frame_height + uv_y_offset + uv_crop_height, + ) + + v1 = ( + 0 + uv_x_offset, + frame_height + uv_height + uv_y_offset, + 0 + uv_x_offset + uv_crop_width, + frame_height + uv_height + uv_y_offset + uv_crop_height, + ) + + v2 = ( + uv_width + uv_x_offset, + frame_height + uv_height + uv_y_offset, + uv_width + uv_x_offset + uv_crop_width, + frame_height + uv_height + uv_y_offset + uv_crop_height, + ) + + return y, u1, u2, v1, v2 + + +def yuv_crop_and_resize(frame, region, height=None): + # Crops and resizes a YUV frame while maintaining aspect ratio + # https://stackoverflow.com/a/57022634 + height = frame.shape[0] // 3 * 2 + width = frame.shape[1] + + # get the crop box if the region extends beyond the frame + crop_x1 = max(0, region[0]) + crop_y1 = max(0, region[1]) + # ensure these are a multiple of 4 + crop_x2 = min(width, region[2]) + crop_y2 = min(height, region[3]) + crop_box = (crop_x1, crop_y1, crop_x2, crop_y2) + + y, u1, u2, v1, v2 = get_yuv_crop(frame.shape, crop_box) + + # if the region starts outside the frame, indent the start point in the cropped frame + y_channel_x_offset = abs(min(0, region[0])) + y_channel_y_offset = abs(min(0, region[1])) + + uv_channel_x_offset = y_channel_x_offset // 2 + uv_channel_y_offset = y_channel_y_offset // 4 + + # create the yuv region frame + # make sure the size is a multiple of 4 + # TODO: this should be based on the size after resize now + size = (region[3] - region[1]) // 4 * 4 + yuv_cropped_frame = np.zeros((size + size // 2, size), np.uint8) + # fill in black + yuv_cropped_frame[:] = 128 + yuv_cropped_frame[0:size, 0:size] = 16 + + # copy the y channel + yuv_cropped_frame[ + y_channel_y_offset : y_channel_y_offset + y[3] - y[1], + y_channel_x_offset : y_channel_x_offset + y[2] - y[0], + ] = frame[y[1] : y[3], y[0] : y[2]] + + uv_crop_width = u1[2] - u1[0] + uv_crop_height = u1[3] - u1[1] + + # copy u1 + yuv_cropped_frame[ + size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height, + 0 + uv_channel_x_offset : 0 + uv_channel_x_offset + uv_crop_width, + ] = frame[u1[1] : u1[3], u1[0] : u1[2]] + + # copy u2 + yuv_cropped_frame[ + size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height, + size // 2 + uv_channel_x_offset : size // 2 + + uv_channel_x_offset + + uv_crop_width, + ] = frame[u2[1] : u2[3], u2[0] : u2[2]] + + # copy v1 + yuv_cropped_frame[ + size + size // 4 + uv_channel_y_offset : size + + size // 4 + + uv_channel_y_offset + + uv_crop_height, + 0 + uv_channel_x_offset : 0 + uv_channel_x_offset + uv_crop_width, + ] = frame[v1[1] : v1[3], v1[0] : v1[2]] + + # copy v2 + yuv_cropped_frame[ + size + size // 4 + uv_channel_y_offset : size + + size // 4 + + uv_channel_y_offset + + uv_crop_height, + size // 2 + uv_channel_x_offset : size // 2 + + uv_channel_x_offset + + uv_crop_width, + ] = frame[v2[1] : v2[3], v2[0] : v2[2]] + + return yuv_cropped_frame + + +def yuv_to_3_channel_yuv(yuv_frame): + height = yuv_frame.shape[0] // 3 * 2 + width = yuv_frame.shape[1] + + # flatten the image into array + yuv_data = yuv_frame.ravel() + + # create a numpy array to hold all the 3 channel yuv data + all_yuv_data = np.empty((height, width, 3), dtype=np.uint8) + + y_count = height * width + uv_count = y_count // 4 + + # copy the y_channel + all_yuv_data[:, :, 0] = yuv_data[0:y_count].reshape((height, width)) + # copy the u channel doubling each dimension + all_yuv_data[:, :, 1] = np.repeat( + np.reshape( + np.repeat(yuv_data[y_count : y_count + uv_count], repeats=2, axis=0), + (height // 2, width), + ), + repeats=2, + axis=0, + ) + # copy the v channel doubling each dimension + all_yuv_data[:, :, 2] = np.repeat( + np.reshape( + np.repeat( + yuv_data[y_count + uv_count : y_count + uv_count + uv_count], + repeats=2, + axis=0, + ), + (height // 2, width), + ), + repeats=2, + axis=0, + ) + + return all_yuv_data + + +def copy_yuv_to_position( + destination_frame, + destination_offset, + destination_shape, + source_frame=None, + source_channel_dim=None, + interpolation=cv2.INTER_LINEAR, +): + # get the coordinates of the channels for this position in the layout + y, u1, u2, v1, v2 = get_yuv_crop( + destination_frame.shape, + ( + destination_offset[1], + destination_offset[0], + destination_offset[1] + destination_shape[1], + destination_offset[0] + destination_shape[0], + ), + ) + + # clear y + destination_frame[ + y[1] : y[3], + y[0] : y[2], + ] = 16 + + # clear u1 + destination_frame[u1[1] : u1[3], u1[0] : u1[2]] = 128 + # clear u2 + destination_frame[u2[1] : u2[3], u2[0] : u2[2]] = 128 + # clear v1 + destination_frame[v1[1] : v1[3], v1[0] : v1[2]] = 128 + # clear v2 + destination_frame[v2[1] : v2[3], v2[0] : v2[2]] = 128 + + if source_frame is not None: + # calculate the resized frame, maintaining the aspect ratio + source_aspect_ratio = source_frame.shape[1] / (source_frame.shape[0] // 3 * 2) + dest_aspect_ratio = destination_shape[1] / destination_shape[0] + + if source_aspect_ratio <= dest_aspect_ratio: + y_resize_height = int(destination_shape[0] // 4 * 4) + y_resize_width = int((y_resize_height * source_aspect_ratio) // 4 * 4) + else: + y_resize_width = int(destination_shape[1] // 4 * 4) + y_resize_height = int((y_resize_width / source_aspect_ratio) // 4 * 4) + + uv_resize_width = int(y_resize_width // 2) + uv_resize_height = int(y_resize_height // 4) + + y_y_offset = int((destination_shape[0] - y_resize_height) / 4 // 4 * 4) + y_x_offset = int((destination_shape[1] - y_resize_width) / 2 // 4 * 4) + + uv_y_offset = y_y_offset // 4 + uv_x_offset = y_x_offset // 2 + + # resize/copy y channel + destination_frame[ + y[1] + y_y_offset : y[1] + y_y_offset + y_resize_height, + y[0] + y_x_offset : y[0] + y_x_offset + y_resize_width, + ] = cv2.resize( + source_frame[ + source_channel_dim["y"][1] : source_channel_dim["y"][3], + source_channel_dim["y"][0] : source_channel_dim["y"][2], + ], + dsize=(y_resize_width, y_resize_height), + interpolation=interpolation, + ) + + # resize/copy u1 + destination_frame[ + u1[1] + uv_y_offset : u1[1] + uv_y_offset + uv_resize_height, + u1[0] + uv_x_offset : u1[0] + uv_x_offset + uv_resize_width, + ] = cv2.resize( + source_frame[ + source_channel_dim["u1"][1] : source_channel_dim["u1"][3], + source_channel_dim["u1"][0] : source_channel_dim["u1"][2], + ], + dsize=(uv_resize_width, uv_resize_height), + interpolation=interpolation, + ) + # resize/copy u2 + destination_frame[ + u2[1] + uv_y_offset : u2[1] + uv_y_offset + uv_resize_height, + u2[0] + uv_x_offset : u2[0] + uv_x_offset + uv_resize_width, + ] = cv2.resize( + source_frame[ + source_channel_dim["u2"][1] : source_channel_dim["u2"][3], + source_channel_dim["u2"][0] : source_channel_dim["u2"][2], + ], + dsize=(uv_resize_width, uv_resize_height), + interpolation=interpolation, + ) + # resize/copy v1 + destination_frame[ + v1[1] + uv_y_offset : v1[1] + uv_y_offset + uv_resize_height, + v1[0] + uv_x_offset : v1[0] + uv_x_offset + uv_resize_width, + ] = cv2.resize( + source_frame[ + source_channel_dim["v1"][1] : source_channel_dim["v1"][3], + source_channel_dim["v1"][0] : source_channel_dim["v1"][2], + ], + dsize=(uv_resize_width, uv_resize_height), + interpolation=interpolation, + ) + # resize/copy v2 + destination_frame[ + v2[1] + uv_y_offset : v2[1] + uv_y_offset + uv_resize_height, + v2[0] + uv_x_offset : v2[0] + uv_x_offset + uv_resize_width, + ] = cv2.resize( + source_frame[ + source_channel_dim["v2"][1] : source_channel_dim["v2"][3], + source_channel_dim["v2"][0] : source_channel_dim["v2"][2], + ], + dsize=(uv_resize_width, uv_resize_height), + interpolation=interpolation, + ) + + +def get_blank_yuv_frame(width: int, height: int) -> np.ndarray: + """Creates a black YUV 4:2:0 frame.""" + yuv_height = height * 3 // 2 + yuv_frame = np.zeros((yuv_height, width), dtype=np.uint8) + + uv_height = height // 2 + + # The U and V planes are stored after the Y plane. + u_start = height # U plane starts right after Y plane + v_start = u_start + uv_height // 2 # V plane starts after U plane + yuv_frame[u_start : u_start + uv_height, :width] = 128 + yuv_frame[v_start : v_start + uv_height, :width] = 128 + + return yuv_frame + + +def yuv_region_2_yuv(frame, region): + try: + # TODO: does this copy the numpy array? + yuv_cropped_frame = yuv_crop_and_resize(frame, region) + return yuv_to_3_channel_yuv(yuv_cropped_frame) + except: + print(f"frame.shape: {frame.shape}") + print(f"region: {region}") + raise + + +def yuv_region_2_rgb(frame, region): + try: + # TODO: does this copy the numpy array? + yuv_cropped_frame = yuv_crop_and_resize(frame, region) + return cv2.cvtColor(yuv_cropped_frame, cv2.COLOR_YUV2RGB_I420) + except: + print(f"frame.shape: {frame.shape}") + print(f"region: {region}") + raise + + +def yuv_region_2_bgr(frame, region): + try: + yuv_cropped_frame = yuv_crop_and_resize(frame, region) + return cv2.cvtColor(yuv_cropped_frame, cv2.COLOR_YUV2BGR_I420) + except: + print(f"frame.shape: {frame.shape}") + print(f"region: {region}") + raise + + +def intersection(box_a, box_b) -> list[int] | None: + """Return intersection box or None if boxes do not intersect.""" + if ( + box_a[2] < box_b[0] + or box_a[0] > box_b[2] + or box_a[1] > box_b[3] + or box_a[3] < box_b[1] + ): + return None + + return ( + max(box_a[0], box_b[0]), + max(box_a[1], box_b[1]), + min(box_a[2], box_b[2]), + min(box_a[3], box_b[3]), + ) + + +def area(box): + return (box[2] - box[0] + 1) * (box[3] - box[1] + 1) + + +def intersection_over_union(box_a, box_b): + # determine the (x, y)-coordinates of the intersection rectangle + intersect = intersection(box_a, box_b) + + if intersect is None: + return 0.0 + + # compute the area of intersection rectangle + inter_area = max(0, intersect[2] - intersect[0] + 1) * max( + 0, intersect[3] - intersect[1] + 1 + ) + + if inter_area == 0: + return 0.0 + + # compute the area of both the prediction and ground-truth + # rectangles + box_a_area = (box_a[2] - box_a[0] + 1) * (box_a[3] - box_a[1] + 1) + box_b_area = (box_b[2] - box_b[0] + 1) * (box_b[3] - box_b[1] + 1) + + # compute the intersection over union by taking the intersection + # area and dividing it by the sum of prediction + ground-truth + # areas - the intersection area + iou = inter_area / float(box_a_area + box_b_area - inter_area) + + # return the intersection over union value + return iou + + +def clipped(obj, frame_shape): + # if the object is within 5 pixels of the region border, and the region is not on the edge + # consider the object to be clipped + box = obj[2] + region = obj[5] + if ( + (region[0] > 5 and box[0] - region[0] <= 5) + or (region[1] > 5 and box[1] - region[1] <= 5) + or (frame_shape[1] - region[2] > 5 and region[2] - box[2] <= 5) + or (frame_shape[0] - region[3] > 5 and region[3] - box[3] <= 5) + ): + return True + else: + return False + + +class FrameManager(ABC): + @abstractmethod + def create(self, name: str, size: int) -> AnyStr: + pass + + @abstractmethod + def write(self, name: str) -> memoryview | None: + pass + + @abstractmethod + def get(self, name: str, timeout_ms: int = 0): + pass + + @abstractmethod + def close(self, name: str): + pass + + @abstractmethod + def delete(self, name: str): + pass + + @abstractmethod + def cleanup(self): + pass + + +class UntrackedSharedMemory(_mpshm.SharedMemory): + # https://github.com/python/cpython/issues/82300#issuecomment-2169035092 + + __lock = threading.Lock() + + def __init__( + self, + name: str | None = None, + create: bool = False, + size: int = 0, + *, + track: bool = False, + ) -> None: + self._track = track + + # if tracking, normal init will suffice + if track: + return super().__init__(name=name, create=create, size=size) + + # lock so that other threads don't attempt to use the + # register function during this time + with self.__lock: + # temporarily disable registration during initialization + orig_register = _mprt.register + _mprt.register = self.__tmp_register + + # initialize; ensure original register function is + # re-instated + try: + super().__init__(name=name, create=create, size=size) + finally: + _mprt.register = orig_register + + @staticmethod + def __tmp_register(*args, **kwargs) -> None: + return + + def unlink(self) -> None: + if _mpshm._USE_POSIX and self._name: + _mpshm._posixshmem.shm_unlink(self._name) + if self._track: + _mprt.unregister(self._name, "shared_memory") + + +class SharedMemoryFrameManager(FrameManager): + def __init__(self): + self.shm_store: dict[str, UntrackedSharedMemory] = {} + + def create(self, name: str, size) -> AnyStr: + try: + shm = UntrackedSharedMemory( + name=name, + create=True, + size=size, + ) + except FileExistsError: + shm = UntrackedSharedMemory(name=name) + + self.shm_store[name] = shm + return shm.buf + + def write(self, name: str) -> memoryview | None: + try: + if name in self.shm_store: + shm = self.shm_store[name] + else: + shm = UntrackedSharedMemory(name=name) + self.shm_store[name] = shm + return shm.buf + except FileNotFoundError: + logger.info(f"the file {name} not found") + return None + + def get(self, name: str, shape) -> np.ndarray | None: + try: + required = int(np.prod(shape)) + shm = self.shm_store.get(name) + if shm is not None and shm.size != required: + # stale cached ref from a same-name recreate — drop and reopen + try: + shm.close() + except Exception: + pass + self.shm_store.pop(name, None) + shm = None + if shm is None: + shm = UntrackedSharedMemory(name=name) + if shm.size != required: + # mid-recreate: OS segment doesn't match shape yet; skip + try: + shm.close() + except Exception: + pass + return None + self.shm_store[name] = shm + return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf) + except FileNotFoundError: + return None + + def close(self, name: str): + if name in self.shm_store: + self.shm_store[name].close() + del self.shm_store[name] + + def delete(self, name: str): + if name in self.shm_store: + self.shm_store[name].close() + + try: + self.shm_store[name].unlink() + except FileNotFoundError: + pass + + del self.shm_store[name] + else: + try: + shm = UntrackedSharedMemory(name=name) + shm.close() + shm.unlink() + except FileNotFoundError: + pass + + def cleanup(self) -> None: + for shm in self.shm_store.values(): + shm.close() + + try: + shm.unlink() + except FileNotFoundError: + pass + + +def create_mask(frame_shape, mask): + mask_img = np.zeros(frame_shape, np.uint8) + mask_img[:] = 255 + + if isinstance(mask, list): + for m in mask: + add_mask(m, mask_img) + + elif isinstance(mask, str): + add_mask(mask, mask_img) + + return mask_img + + +def add_mask(mask: str, mask_img: np.ndarray): + points = mask.split(",") + + # masks and zones are saved as relative coordinates + # we know if any points are > 1 then it is using the + # old native resolution coordinates + if any(x > "1.0" for x in points): + raise Exception("add mask expects relative coordinates only") + + contour = np.array( + [ + [ + int(float(points[i]) * mask_img.shape[1]), + int(float(points[i + 1]) * mask_img.shape[0]), + ] + for i in range(0, len(points), 2) + ] + ) + cv2.fillPoly(mask_img, pts=[contour], color=(0)) + + +def run_ffmpeg_snapshot( + ffmpeg, + input_path: str, + codec: str, + seek_time: float | None = None, + height: int | None = None, + timeout: int | None = None, +) -> tuple[bytes | None, str]: + """Run ffmpeg to extract a snapshot/image from a video source.""" + ffmpeg_cmd = [ + ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + ] + + if seek_time is not None: + ffmpeg_cmd.extend(["-ss", f"00:00:{seek_time}"]) + + ffmpeg_cmd.extend( + [ + "-i", + input_path, + "-frames:v", + "1", + "-c:v", + codec, + "-f", + "image2pipe", + "-", + ] + ) + + if height is not None: + ffmpeg_cmd.insert(-3, "-vf") + ffmpeg_cmd.insert(-3, f"scale=-1:{height}") + + try: + process = sp.run( + ffmpeg_cmd, + capture_output=True, + timeout=timeout, + ) + + if process.returncode == 0 and process.stdout: + return process.stdout, "" + else: + return None, process.stderr.decode() if process.stderr else "ffmpeg failed" + except sp.TimeoutExpired: + return None, "timeout" + + +def get_image_from_recording( + ffmpeg, # Ffmpeg Config + file_path: str, + relative_frame_time: float, + codec: str, + height: int | None = None, +) -> Any | None: + """retrieve a frame from given time in recording file.""" + + image_data, _ = run_ffmpeg_snapshot( + ffmpeg, file_path, codec, seek_time=relative_frame_time, height=height + ) + + return image_data + + +def get_histogram(image, x_min, y_min, x_max, y_max): + image_bgr = cv2.cvtColor(image, cv2.COLOR_YUV2BGR_I420) + image_bgr = image_bgr[y_min:y_max, x_min:x_max] + + hist = cv2.calcHist( + [image_bgr], [0, 1, 2], None, [8, 8, 8], [0, 256, 0, 256, 0, 256] + ) + return cv2.normalize(hist, hist).flatten() + + +def create_thumbnail( + yuv_frame: np.ndarray, box: tuple[int, int, int, int], height=500 +) -> bytes | None: + """Return jpg thumbnail of a region of the frame.""" + frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2BGR_I420) + region = calculate_region( + frame.shape, box[0], box[1], box[2], box[3], height, multiplier=1.4 + ) + frame = frame[region[1] : region[3], region[0] : region[2]] + width = int(height * frame.shape[1] / frame.shape[0]) + frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) + ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 100]) + + if ret: + return jpg.tobytes() + + return None + + +def ensure_jpeg_bytes(image_data: bytes) -> bytes: + """Ensure image data is jpeg bytes for genai""" + try: + img_array = np.frombuffer(image_data, dtype=np.uint8) + img = cv2.imdecode(img_array, cv2.IMREAD_COLOR) + + if img is None: + return image_data + + success, encoded_img = cv2.imencode(".jpg", img) + + if success: + return encoded_img.tobytes() + except Exception as e: + logger.warning(f"Error when converting thumbnail to jpeg for genai: {e}") + + return image_data diff --git a/frigate/util/media.py b/frigate/util/media.py new file mode 100644 index 0000000..e4e84e9 --- /dev/null +++ b/frigate/util/media.py @@ -0,0 +1,929 @@ +"""Recordings Utilities.""" + +import datetime +import errno +import logging +import os +import subprocess as sp +from collections.abc import Iterable +from dataclasses import dataclass, field +from pathlib import Path + +from peewee import DatabaseError, chunked + +from frigate.const import ( + CLIPS_DIR, + DEFAULT_FFMPEG_VERSION, + EXPORT_DIR, + RECORD_DIR, + THUMB_DIR, +) +from frigate.models import ( + Event, + Export, + Previews, + Recordings, + RecordingsToDelete, + ReviewSegment, +) + +logger = logging.getLogger(__name__) + + +# Safety threshold - abort if more than 50% of files would be deleted +SAFETY_THRESHOLD = 0.5 + +FFPROBE_PATH = ( + f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffprobe" + if DEFAULT_FFMPEG_VERSION + else "ffprobe" +) + + +@dataclass +class SyncResult: + """Result of a sync operation.""" + + media_type: str + files_checked: int = 0 + orphans_found: int = 0 + orphans_deleted: int = 0 + orphan_paths: list[str] = field(default_factory=list) + orphan_db_paths: list[str] = field(default_factory=list) + aborted: bool = False + error: str | None = None + + def to_dict(self) -> dict: + return { + "media_type": self.media_type, + "files_checked": self.files_checked, + "orphans_found": self.orphans_found, + "orphans_deleted": self.orphans_deleted, + "aborted": self.aborted, + "error": self.error, + } + + +def remove_empty_directories(root: Path, paths: Iterable[Path]) -> None: + """ + Remove directories if they exist and are empty. + Silently ignores non-existent and non-empty directories. + Attempts to remove parent directories as well, stopping at the given root. + """ + count = 0 + while True: + parents = set() + for path in paths: + if path == root: + continue + + try: + path.rmdir() + count += 1 + except FileNotFoundError: + pass + except OSError as e: + if e.errno == errno.ENOTEMPTY: + continue + raise + + parents.add(path.parent) + + if not parents: + break + + paths = parents + + logger.debug(f"Removed {count} empty directories") + + +def sync_recordings( + limited: bool = False, dry_run: bool = False, force: bool = False +) -> SyncResult: + """Sync recordings between the database and disk using the SyncResult format.""" + + result = SyncResult(media_type="recordings") + + try: + logger.debug("Start sync recordings.") + + # start checking on the hour 36 hours ago + check_point = datetime.datetime.now().replace( + minute=0, second=0, microsecond=0 + ).astimezone(datetime.UTC) - datetime.timedelta(hours=36) + + # Gather DB recordings to inspect + if limited: + recordings_query = Recordings.select(Recordings.id, Recordings.path).where( + Recordings.start_time >= check_point.timestamp() + ) + else: + recordings_query = Recordings.select(Recordings.id, Recordings.path) + + recordings_count = recordings_query.count() + page_size = 1000 + num_pages = (recordings_count + page_size - 1) // page_size + recordings_to_delete: list[dict] = [] + + for page in range(num_pages): + for recording in recordings_query.paginate(page, page_size): + if not os.path.exists(recording.path): + recordings_to_delete.append( + {"id": recording.id, "path": recording.path} + ) + + result.orphans_found += len(recordings_to_delete) + result.orphan_db_paths.extend( + [ + recording["path"] + for recording in recordings_to_delete + if recording.get("path") + ] + ) + + if ( + recordings_count + and len(recordings_to_delete) / recordings_count > SAFETY_THRESHOLD + ): + if force: + logger.warning( + f"Deleting {(len(recordings_to_delete) / max(1, recordings_count) * 100):.2f}% of recordings DB entries (force=True, bypassing safety threshold)" + ) + else: + logger.warning( + f"Deleting {(len(recordings_to_delete) / max(1, recordings_count) * 100):.2f}% of recordings DB entries, could be due to configuration error. Aborting..." + ) + result.aborted = True + return result + + if recordings_to_delete and not dry_run: + logger.info( + f"Deleting {len(recordings_to_delete)} recording DB entries with missing files" + ) + + RecordingsToDelete.create_table(temporary=True) + + max_inserts = 1000 + for batch in chunked(recordings_to_delete, max_inserts): + RecordingsToDelete.insert_many( + [{"id": r["id"]} for r in batch] + ).execute() + + try: + deleted = ( + Recordings.delete() + .where( + Recordings.id.in_( + RecordingsToDelete.select(RecordingsToDelete.id) + ) + ) + .execute() + ) + result.orphans_deleted += int(deleted) + except DatabaseError as e: + logger.error(f"Database error during recordings db cleanup: {e}") + result.error = str(e) + result.aborted = True + return result + + if result.aborted: + logger.warning("Recording DB sync aborted; skipping file cleanup.") + return result + + # Only try to cleanup files if db cleanup was successful or dry_run + if limited: + # get recording files from last 36 hours + hour_check = f"{RECORD_DIR}/{check_point.strftime('%Y-%m-%d/%H')}" + files_on_disk = { + os.path.join(root, file) + for root, _, files in os.walk(RECORD_DIR) + for file in files + if root > hour_check + } + else: + # get all recordings files on disk and put them in a set + files_on_disk = { + os.path.join(root, file) + for root, _, files in os.walk(RECORD_DIR) + for file in files + } + + result.files_checked = len(files_on_disk) + + files_to_delete: list[str] = [] + for file in files_on_disk: + if not Recordings.select().where(Recordings.path == file).exists(): + files_to_delete.append(file) + + result.orphans_found += len(files_to_delete) + result.orphan_paths.extend(files_to_delete) + + if ( + files_on_disk + and len(files_to_delete) / len(files_on_disk) > SAFETY_THRESHOLD + ): + if force: + logger.warning( + f"Deleting {(len(files_to_delete) / max(1, len(files_on_disk)) * 100):.2f}% of recordings files (force=True, bypassing safety threshold)" + ) + else: + logger.warning( + f"Deleting {(len(files_to_delete) / max(1, len(files_on_disk)) * 100):.2f}% of recordings files, could be due to configuration error. Aborting..." + ) + result.aborted = True + return result + + if dry_run: + logger.info( + f"Recordings sync (dry run): Found {len(files_to_delete)} orphaned files" + ) + return result + + # Delete orphans + logger.info(f"Deleting {len(files_to_delete)} orphaned recordings files") + for file in files_to_delete: + try: + os.unlink(file) + result.orphans_deleted += 1 + except OSError as e: + logger.error(f"Failed to delete {file}: {e}") + + logger.debug("End sync recordings.") + + except Exception as e: + logger.error(f"Error syncing recordings: {e}") + result.error = str(e) + + return result + + +def sync_event_snapshots(dry_run: bool = False, force: bool = False) -> SyncResult: + """Sync event snapshots - delete files not referenced by any event. + + Event snapshots are stored at: CLIPS_DIR/{camera}-{event_id}-clean.webp + Also checks legacy variants: {camera}-{event_id}.jpg and -clean.png + """ + result = SyncResult(media_type="event_snapshots") + + try: + # Get all event IDs with snapshots from DB + events_with_snapshots = set( + f"{e.camera}-{e.id}" + for e in Event.select(Event.id, Event.camera).where( + Event.has_snapshot == True + ) + ) + + # Find snapshot files on disk (directly in CLIPS_DIR, not subdirectories) + snapshot_files: list[tuple[str, str]] = [] # (full_path, base_name) + if os.path.isdir(CLIPS_DIR): + for file in os.listdir(CLIPS_DIR): + file_path = os.path.join(CLIPS_DIR, file) + if os.path.isfile(file_path) and file.endswith( + (".jpg", "-clean.webp", "-clean.png") + ): + # Extract base name (camera-event_id) from filename + base_name = file + for suffix in ["-clean.webp", "-clean.png", ".jpg"]: + if file.endswith(suffix): + base_name = file[: -len(suffix)] + break + snapshot_files.append((file_path, base_name)) + + result.files_checked = len(snapshot_files) + + # Find orphans + orphans: list[str] = [] + for file_path, base_name in snapshot_files: + if base_name not in events_with_snapshots: + orphans.append(file_path) + + result.orphans_found = len(orphans) + result.orphan_paths = orphans + + if len(orphans) == 0: + return result + + # Safety check + if ( + result.files_checked > 0 + and len(orphans) / result.files_checked > SAFETY_THRESHOLD + ): + if force: + logger.warning( + f"Event snapshots sync: Would delete {len(orphans)}/{result.files_checked} " + f"({len(orphans) / result.files_checked * 100:.2f}%) files (force=True, bypassing safety threshold)." + ) + else: + logger.warning( + f"Event snapshots sync: Would delete {len(orphans)}/{result.files_checked} " + f"({len(orphans) / result.files_checked * 100:.2f}%) files. " + "Aborting due to safety threshold." + ) + result.aborted = True + return result + + if dry_run: + logger.info( + f"Event snapshots sync (dry run): Found {len(orphans)} orphaned files" + ) + return result + + # Delete orphans + logger.info(f"Deleting {len(orphans)} orphaned event snapshot files") + for file_path in orphans: + try: + os.unlink(file_path) + result.orphans_deleted += 1 + except OSError as e: + logger.error(f"Failed to delete {file_path}: {e}") + + except Exception as e: + logger.error(f"Error syncing event snapshots: {e}") + result.error = str(e) + + return result + + +def sync_event_thumbnails(dry_run: bool = False, force: bool = False) -> SyncResult: + """Sync event thumbnails - delete files not referenced by any event. + + Event thumbnails are stored at: THUMB_DIR/{camera}/{event_id}.webp + Only events without inline thumbnail (thumbnail field is None/empty) use files. + """ + result = SyncResult(media_type="event_thumbnails") + + try: + # Get all events that use file-based thumbnails + # Events with thumbnail field populated don't need files + events_with_file_thumbs = set( + (e.camera, e.id) + for e in Event.select(Event.id, Event.camera, Event.thumbnail).where( + (Event.thumbnail.is_null(True)) | (Event.thumbnail == "") + ) + ) + + # Find thumbnail files on disk + thumbnail_files: list[ + tuple[str, str, str] + ] = [] # (full_path, camera, event_id) + if os.path.isdir(THUMB_DIR): + for camera_dir in os.listdir(THUMB_DIR): + camera_path = os.path.join(THUMB_DIR, camera_dir) + if not os.path.isdir(camera_path): + continue + for file in os.listdir(camera_path): + if file.endswith(".webp"): + event_id = file[:-5] # Remove .webp + file_path = os.path.join(camera_path, file) + thumbnail_files.append((file_path, camera_dir, event_id)) + + result.files_checked = len(thumbnail_files) + + # Find orphans - files where event doesn't exist or event has inline thumbnail + orphans: list[str] = [] + for file_path, camera, event_id in thumbnail_files: + if (camera, event_id) not in events_with_file_thumbs: + # Check if event exists with inline thumbnail + event_exists = Event.select().where(Event.id == event_id).exists() + if not event_exists: + orphans.append(file_path) + # If event exists with inline thumbnail, the file is also orphaned + elif event_exists: + event = Event.get_or_none(Event.id == event_id) + if event and event.thumbnail: + orphans.append(file_path) + + result.orphans_found = len(orphans) + result.orphan_paths = orphans + + if len(orphans) == 0: + return result + + # Safety check + if ( + result.files_checked > 0 + and len(orphans) / result.files_checked > SAFETY_THRESHOLD + ): + if force: + logger.warning( + f"Event thumbnails sync: Would delete {len(orphans)}/{result.files_checked} " + f"({len(orphans) / result.files_checked * 100:.2f}%) files (force=True, bypassing safety threshold)." + ) + else: + logger.warning( + f"Event thumbnails sync: Would delete {len(orphans)}/{result.files_checked} " + f"({len(orphans) / result.files_checked * 100:.2f}%) files. " + "Aborting due to safety threshold." + ) + result.aborted = True + return result + + if dry_run: + logger.info( + f"Event thumbnails sync (dry run): Found {len(orphans)} orphaned files" + ) + return result + + # Delete orphans + logger.info(f"Deleting {len(orphans)} orphaned event thumbnail files") + for file_path in orphans: + try: + os.unlink(file_path) + result.orphans_deleted += 1 + except OSError as e: + logger.error(f"Failed to delete {file_path}: {e}") + + except Exception as e: + logger.error(f"Error syncing event thumbnails: {e}") + result.error = str(e) + + return result + + +def sync_review_thumbnails(dry_run: bool = False, force: bool = False) -> SyncResult: + """Sync review segment thumbnails - delete files not referenced by any review segment. + + Review thumbnails are stored at: CLIPS_DIR/review/thumb-{camera}-{review_id}.webp + The full path is stored in ReviewSegment.thumb_path + """ + result = SyncResult(media_type="review_thumbnails") + + try: + # Get all thumb paths from DB + review_thumb_paths = set( + r.thumb_path + for r in ReviewSegment.select(ReviewSegment.thumb_path) + if r.thumb_path + ) + + # Find review thumbnail files on disk + review_dir = os.path.join(CLIPS_DIR, "review") + thumbnail_files: list[str] = [] + if os.path.isdir(review_dir): + for file in os.listdir(review_dir): + if file.startswith("thumb-") and file.endswith(".webp"): + file_path = os.path.join(review_dir, file) + thumbnail_files.append(file_path) + + result.files_checked = len(thumbnail_files) + + # Find orphans + orphans: list[str] = [] + for file_path in thumbnail_files: + if file_path not in review_thumb_paths: + orphans.append(file_path) + + result.orphans_found = len(orphans) + result.orphan_paths = orphans + + if len(orphans) == 0: + return result + + # Safety check + if ( + result.files_checked > 0 + and len(orphans) / result.files_checked > SAFETY_THRESHOLD + ): + if force: + logger.warning( + f"Review thumbnails sync: Would delete {len(orphans)}/{result.files_checked} " + f"({len(orphans) / result.files_checked * 100:.2f}%) files (force=True, bypassing safety threshold)." + ) + else: + logger.warning( + f"Review thumbnails sync: Would delete {len(orphans)}/{result.files_checked} " + f"({len(orphans) / result.files_checked * 100:.2f}%) files. " + "Aborting due to safety threshold." + ) + result.aborted = True + return result + + if dry_run: + logger.info( + f"Review thumbnails sync (dry run): Found {len(orphans)} orphaned files" + ) + return result + + # Delete orphans + logger.info(f"Deleting {len(orphans)} orphaned review thumbnail files") + for file_path in orphans: + try: + os.unlink(file_path) + result.orphans_deleted += 1 + except OSError as e: + logger.error(f"Failed to delete {file_path}: {e}") + + except Exception as e: + logger.error(f"Error syncing review thumbnails: {e}") + result.error = str(e) + + return result + + +def sync_previews(dry_run: bool = False, force: bool = False) -> SyncResult: + """Sync preview files - delete files not referenced by any preview record. + + Previews are stored at: CLIPS_DIR/previews/{camera}/*.mp4 + The full path is stored in Previews.path + """ + result = SyncResult(media_type="previews") + + try: + # Get all preview paths from DB + preview_paths = set(p.path for p in Previews.select(Previews.path) if p.path) + + # Find preview files on disk + previews_dir = os.path.join(CLIPS_DIR, "previews") + preview_files: list[str] = [] + if os.path.isdir(previews_dir): + for camera_dir in os.listdir(previews_dir): + camera_path = os.path.join(previews_dir, camera_dir) + if not os.path.isdir(camera_path): + continue + for file in os.listdir(camera_path): + if file.endswith(".mp4"): + file_path = os.path.join(camera_path, file) + preview_files.append(file_path) + + result.files_checked = len(preview_files) + + # Find orphans + orphans: list[str] = [] + for file_path in preview_files: + if file_path not in preview_paths: + orphans.append(file_path) + + result.orphans_found = len(orphans) + result.orphan_paths = orphans + + if len(orphans) == 0: + return result + + # Safety check + if ( + result.files_checked > 0 + and len(orphans) / result.files_checked > SAFETY_THRESHOLD + ): + if force: + logger.warning( + f"Previews sync: Would delete {len(orphans)}/{result.files_checked} " + f"({len(orphans) / result.files_checked * 100:.2f}%) files (force=True, bypassing safety threshold)." + ) + else: + logger.warning( + f"Previews sync: Would delete {len(orphans)}/{result.files_checked} " + f"({len(orphans) / result.files_checked * 100:.2f}%) files. " + "Aborting due to safety threshold." + ) + result.aborted = True + return result + + if dry_run: + logger.info(f"Previews sync (dry run): Found {len(orphans)} orphaned files") + return result + + # Delete orphans + logger.info(f"Deleting {len(orphans)} orphaned preview files") + for file_path in orphans: + try: + os.unlink(file_path) + result.orphans_deleted += 1 + except OSError as e: + logger.error(f"Failed to delete {file_path}: {e}") + + except Exception as e: + logger.error(f"Error syncing previews: {e}") + result.error = str(e) + + return result + + +def sync_exports(dry_run: bool = False, force: bool = False) -> SyncResult: + """Sync export files - delete files not referenced by any export record. + + Export videos are stored at: EXPORT_DIR/*.mp4 + Export thumbnails are stored at: CLIPS_DIR/export/*.jpg + The paths are stored in Export.video_path and Export.thumb_path + """ + result = SyncResult(media_type="exports") + + try: + # Get all export paths from DB + export_video_paths = set() + export_thumb_paths = set() + for e in Export.select(Export.video_path, Export.thumb_path): + if e.video_path: + export_video_paths.add(e.video_path) + if e.thumb_path: + export_thumb_paths.add(e.thumb_path) + + # Find export video files on disk + export_files: list[str] = [] + if os.path.isdir(EXPORT_DIR): + for file in os.listdir(EXPORT_DIR): + if file.endswith(".mp4"): + file_path = os.path.join(EXPORT_DIR, file) + export_files.append(file_path) + + # Find export thumbnail files on disk + export_thumb_dir = os.path.join(CLIPS_DIR, "export") + thumb_files: list[str] = [] + if os.path.isdir(export_thumb_dir): + for file in os.listdir(export_thumb_dir): + if file.endswith(".jpg"): + file_path = os.path.join(export_thumb_dir, file) + thumb_files.append(file_path) + + result.files_checked = len(export_files) + len(thumb_files) + + # Find orphans + orphans: list[str] = [] + for file_path in export_files: + if file_path not in export_video_paths: + orphans.append(file_path) + for file_path in thumb_files: + if file_path not in export_thumb_paths: + orphans.append(file_path) + + result.orphans_found = len(orphans) + result.orphan_paths = orphans + + if len(orphans) == 0: + return result + + # Safety check + if ( + result.files_checked > 0 + and len(orphans) / result.files_checked > SAFETY_THRESHOLD + ): + if force: + logger.warning( + f"Exports sync: Would delete {len(orphans)}/{result.files_checked} " + f"({len(orphans) / result.files_checked * 100:.2f}%) files (force=True, bypassing safety threshold)." + ) + else: + logger.warning( + f"Exports sync: Would delete {len(orphans)}/{result.files_checked} " + f"({len(orphans) / result.files_checked * 100:.2f}%) files. " + "Aborting due to safety threshold." + ) + result.aborted = True + return result + + if dry_run: + logger.info(f"Exports sync (dry run): Found {len(orphans)} orphaned files") + return result + + # Delete orphans + logger.info(f"Deleting {len(orphans)} orphaned export files") + for file_path in orphans: + try: + os.unlink(file_path) + result.orphans_deleted += 1 + except OSError as e: + logger.error(f"Failed to delete {file_path}: {e}") + + except Exception as e: + logger.error(f"Error syncing exports: {e}") + result.error = str(e) + + return result + + +@dataclass +class MediaSyncResults: + """Combined results from all media sync operations.""" + + event_snapshots: SyncResult | None = None + event_thumbnails: SyncResult | None = None + review_thumbnails: SyncResult | None = None + previews: SyncResult | None = None + exports: SyncResult | None = None + recordings: SyncResult | None = None + + @property + def total_files_checked(self) -> int: + total = 0 + for result in [ + self.event_snapshots, + self.event_thumbnails, + self.review_thumbnails, + self.previews, + self.exports, + self.recordings, + ]: + if result: + total += result.files_checked + return total + + @property + def total_orphans_found(self) -> int: + total = 0 + for result in [ + self.event_snapshots, + self.event_thumbnails, + self.review_thumbnails, + self.previews, + self.exports, + self.recordings, + ]: + if result: + total += result.orphans_found + return total + + @property + def total_orphans_deleted(self) -> int: + total = 0 + for result in [ + self.event_snapshots, + self.event_thumbnails, + self.review_thumbnails, + self.previews, + self.exports, + self.recordings, + ]: + if result: + total += result.orphans_deleted + return total + + def to_dict(self) -> dict: + """Convert results to dictionary for API response.""" + results = {} + for name, result in [ + ("event_snapshots", self.event_snapshots), + ("event_thumbnails", self.event_thumbnails), + ("review_thumbnails", self.review_thumbnails), + ("previews", self.previews), + ("exports", self.exports), + ("recordings", self.recordings), + ]: + if result: + results[name] = { + "files_checked": result.files_checked, + "orphans_found": result.orphans_found, + "orphans_deleted": result.orphans_deleted, + "aborted": result.aborted, + "error": result.error, + } + results["totals"] = { + "files_checked": self.total_files_checked, + "orphans_found": self.total_orphans_found, + "orphans_deleted": self.total_orphans_deleted, + } + return results + + +def write_orphan_report( + results: "MediaSyncResults", + path: str, + job_id: str = "", + dry_run: bool = False, +) -> None: + """Write a verbose orphan report file listing all orphan paths by media type. + + Args: + results: The completed MediaSyncResults. + path: File path to write the report to. + job_id: Job ID for the report header. + dry_run: Whether the sync was a dry run, for the report header. + """ + try: + with open(path, "w") as f: + f.write("# Media Sync Orphan Report\n") + f.write(f"# Job: {job_id}\n") + f.write( + f"# Date: {datetime.datetime.now().astimezone(datetime.UTC).isoformat()}\n" + ) + f.write(f"# Mode: dry_run={dry_run}\n\n") + + for name, result in [ + ("recordings", results.recordings), + ("event_snapshots", results.event_snapshots), + ("event_thumbnails", results.event_thumbnails), + ("review_thumbnails", results.review_thumbnails), + ("previews", results.previews), + ("exports", results.exports), + ]: + if result is None: + continue + + if result.orphan_db_paths: + f.write( + f"## {name} - orphaned db entries ({len(result.orphan_db_paths)})\n" + ) + for orphan_path in result.orphan_db_paths: + f.write(f"{orphan_path}\n") + f.write("\n") + + if result.orphan_paths: + f.write( + f"## {name} - orphaned files ({len(result.orphan_paths)})\n" + ) + for orphan_path in result.orphan_paths: + f.write(f"{orphan_path}\n") + f.write("\n") + + logger.debug("Wrote verbose orphan report to %s", path) + except OSError as e: + logger.error("Failed to write orphan report to %s: %s", path, e) + + +def sync_all_media( + dry_run: bool = False, media_types: list[str] = ["all"], force: bool = False +) -> MediaSyncResults: + """Sync specified media types with the database. + + Args: + dry_run: If True, only report orphans without deleting them. + media_types: List of media types to sync. Can include: 'all', 'event_snapshots', + 'event_thumbnails', 'review_thumbnails', 'previews', 'exports', 'recordings' + force: If True, bypass safety threshold checks. + + Returns: + MediaSyncResults with details of each sync operation. + """ + logger.debug( + f"Starting media sync (dry_run={dry_run}, media_types={media_types}, force={force})" + ) + + results = MediaSyncResults() + + # Determine which media types to sync + sync_all = "all" in media_types + + if sync_all or "event_snapshots" in media_types: + results.event_snapshots = sync_event_snapshots(dry_run=dry_run, force=force) + + if sync_all or "event_thumbnails" in media_types: + results.event_thumbnails = sync_event_thumbnails(dry_run=dry_run, force=force) + + if sync_all or "review_thumbnails" in media_types: + results.review_thumbnails = sync_review_thumbnails(dry_run=dry_run, force=force) + + if sync_all or "previews" in media_types: + results.previews = sync_previews(dry_run=dry_run, force=force) + + if sync_all or "exports" in media_types: + results.exports = sync_exports(dry_run=dry_run, force=force) + + if sync_all or "recordings" in media_types: + results.recordings = sync_recordings(dry_run=dry_run, force=force) + + logger.info( + f"Media sync complete: checked {results.total_files_checked} files, " + f"found {results.total_orphans_found} orphans, " + f"deleted {results.total_orphans_deleted}" + ) + + return results + + +def get_keyframe_before(path: str, offset_ms: int) -> int | None: + """Get the timestamp (ms) of the last keyframe at or before offset_ms. + + Uses ffprobe packet index to read keyframe positions from the mp4 file. + Returns None if ffprobe fails or no keyframe is found before the offset. + """ + try: + result = sp.run( + [ + FFPROBE_PATH, + "-select_streams", + "v:0", + "-show_entries", + "packet=pts_time,flags", + "-of", + "csv=p=0", + "-loglevel", + "error", + path, + ], + capture_output=True, + timeout=5, + ) + except (sp.TimeoutExpired, FileNotFoundError): + return None + + if result.returncode != 0: + return None + + offset_s = offset_ms / 1000.0 + best_ms = None + for line in result.stdout.decode().strip().splitlines(): + parts = line.strip().split(",") + if len(parts) != 2: + continue + ts_str, flags = parts + if "K" not in flags: + continue + try: + ts = float(ts_str) + except ValueError: + continue + if ts <= offset_s: + best_ms = int(ts * 1000) + else: + break + + return best_ms diff --git a/frigate/util/model.py b/frigate/util/model.py new file mode 100644 index 0000000..338303e --- /dev/null +++ b/frigate/util/model.py @@ -0,0 +1,380 @@ +"""Model Utils""" + +import logging +import os +from typing import Any + +import cv2 +import numpy as np +import onnxruntime as ort + +from frigate.const import MODEL_CACHE_DIR + +logger = logging.getLogger(__name__) + + +### Post Processing + + +def post_process_dfine( + tensor_output: np.ndarray, width: int, height: int +) -> np.ndarray: + class_ids = tensor_output[0][tensor_output[2] > 0.4] + boxes = tensor_output[1][tensor_output[2] > 0.4] + scores = tensor_output[2][tensor_output[2] > 0.4] + + input_shape = np.array([height, width, height, width]) + boxes = np.divide(boxes, input_shape, dtype=np.float32) + indices = cv2.dnn.NMSBoxes(boxes, scores, score_threshold=0.4, nms_threshold=0.4) + detections = np.zeros((20, 6), np.float32) + + for i, (bbox, confidence, class_id) in enumerate( + zip(boxes[indices], scores[indices], class_ids[indices]) + ): + if i == 20: + break + + detections[i] = [ + class_id, + confidence, + bbox[1], + bbox[0], + bbox[3], + bbox[2], + ] + + return detections + + +def post_process_rfdetr(tensor_output: list[np.ndarray, np.ndarray]) -> np.ndarray: + boxes = tensor_output[0] + raw_scores = tensor_output[1] + + # apply soft max to scores + exp = np.exp(raw_scores - np.max(raw_scores, axis=-1, keepdims=True)) + all_scores = exp / np.sum(exp, axis=-1, keepdims=True) + + # get highest scoring class from every detection + scores = np.max(all_scores[0, :, 1:], axis=-1) + labels = np.argmax(all_scores[0, :, 1:], axis=-1) + + idxs = scores > 0.4 + filtered_boxes = boxes[0, idxs] + filtered_scores = scores[idxs] + filtered_labels = labels[idxs] + + # convert boxes from [x_center, y_center, width, height] + x_center, y_center, w, h = ( + filtered_boxes[:, 0], + filtered_boxes[:, 1], + filtered_boxes[:, 2], + filtered_boxes[:, 3], + ) + x_min = x_center - w / 2 + y_min = y_center - h / 2 + x_max = x_center + w / 2 + y_max = y_center + h / 2 + filtered_boxes = np.stack([x_min, y_min, x_max, y_max], axis=-1) + + # apply nms + indices = cv2.dnn.NMSBoxes( + filtered_boxes, filtered_scores, score_threshold=0.4, nms_threshold=0.4 + ) + detections = np.zeros((20, 6), np.float32) + + for i, (bbox, confidence, class_id) in enumerate( + zip(filtered_boxes[indices], filtered_scores[indices], filtered_labels[indices]) + ): + if i == 20: + break + + detections[i] = [ + class_id, + confidence, + bbox[1], + bbox[0], + bbox[3], + bbox[2], + ] + + return detections + + +def __post_process_multipart_yolo( + output_list, + width, + height, +): + anchors = [ + [(12, 16), (19, 36), (40, 28)], + [(36, 75), (76, 55), (72, 146)], + [(142, 110), (192, 243), (459, 401)], + ] + + stride_map = {0: 8, 1: 16, 2: 32} + + all_boxes = [] + all_scores = [] + all_class_ids = [] + + for i, output in enumerate(output_list): + bs, _, ny, nx = output.shape + stride = stride_map[i] + anchor_set = anchors[i] + + num_anchors = len(anchor_set) + output = output.reshape(bs, num_anchors, 85, ny, nx) + output = output.transpose(0, 1, 3, 4, 2) + output = output[0] + + for a_idx, (anchor_w, anchor_h) in enumerate(anchor_set): + for y in range(ny): + for x in range(nx): + pred = output[a_idx, y, x] + class_probs = pred[5:] + class_id = np.argmax(class_probs) + class_conf = class_probs[class_id] + conf = class_conf * pred[4] + + if conf < 0.4: + continue + + dx = pred[0] + dy = pred[1] + dw = pred[2] + dh = pred[3] + + bx = ((dx * 2.0 - 0.5) + x) * stride + by = ((dy * 2.0 - 0.5) + y) * stride + bw = ((dw * 2.0) ** 2) * anchor_w + bh = ((dh * 2.0) ** 2) * anchor_h + + x1 = max(0, bx - bw / 2) + y1 = max(0, by - bh / 2) + x2 = min(width, bx + bw / 2) + y2 = min(height, by + bh / 2) + + all_boxes.append([x1, y1, x2, y2]) + all_scores.append(conf) + all_class_ids.append(class_id) + + indices = cv2.dnn.NMSBoxes( + bboxes=all_boxes, + scores=all_scores, + score_threshold=0.4, + nms_threshold=0.4, + ) + + results = np.zeros((20, 6), np.float32) + + if len(indices) > 0: + for i, idx in enumerate(indices.flatten()[:20]): + class_id = all_class_ids[idx] + conf = all_scores[idx] + x1, y1, x2, y2 = all_boxes[idx] + results[i] = [ + class_id, + conf, + y1 / height, + x1 / width, + y2 / height, + x2 / width, + ] + + return results + + +def __post_process_nms_yolo(predictions: np.ndarray, width, height) -> np.ndarray: + predictions = np.squeeze(predictions) + + # transpose the output so it has order (inferences, class_ids) + if predictions.shape[0] < predictions.shape[1]: + predictions = predictions.T + + scores = np.max(predictions[:, 4:], axis=1) + predictions = predictions[scores > 0.4, :] + scores = scores[scores > 0.4] + class_ids = np.argmax(predictions[:, 4:], axis=1) + + # Rescale box + boxes = predictions[:, :4] + boxes_xyxy = np.ones_like(boxes) + boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2 + boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2 + boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2 + boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2 + boxes = boxes_xyxy + + # run NMS + indices = cv2.dnn.NMSBoxes(boxes, scores, score_threshold=0.4, nms_threshold=0.4) + detections = np.zeros((20, 6), np.float32) + for i, (bbox, confidence, class_id) in enumerate( + zip(boxes[indices], scores[indices], class_ids[indices]) + ): + if i == 20: + break + + detections[i] = [ + class_id, + confidence, + bbox[1] / height, + bbox[0] / width, + bbox[3] / height, + bbox[2] / width, + ] + + return detections + + +def post_process_yolo(output: list[np.ndarray], width: int, height: int) -> np.ndarray: + if len(output) > 1: + return __post_process_multipart_yolo(output, width, height) + else: + return __post_process_nms_yolo(output[0], width, height) + + +def post_process_yolox( + predictions: np.ndarray, + width: int, + height: int, + grids: np.ndarray, + expanded_strides: np.ndarray, +) -> np.ndarray: + predictions[..., :2] = (predictions[..., :2] + grids) * expanded_strides + predictions[..., 2:4] = np.exp(predictions[..., 2:4]) * expanded_strides + + # process organized predictions + predictions = predictions[0] + boxes = predictions[:, :4] + scores = predictions[:, 4:5] * predictions[:, 5:] + + boxes_xyxy = np.ones_like(boxes) + boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2 + boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2 + boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2 + boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2 + + cls_inds = scores.argmax(1) + scores = scores[np.arange(len(cls_inds)), cls_inds] + + indices = cv2.dnn.NMSBoxes( + boxes_xyxy, scores, score_threshold=0.4, nms_threshold=0.4 + ) + + detections = np.zeros((20, 6), np.float32) + for i, (bbox, confidence, class_id) in enumerate( + zip(boxes_xyxy[indices], scores[indices], cls_inds[indices]) + ): + if i == 20: + break + + detections[i] = [ + class_id, + confidence, + bbox[1] / height, + bbox[0] / width, + bbox[3] / height, + bbox[2] / width, + ] + + return detections + + +### ONNX Utilities + + +def get_ort_providers( + force_cpu: bool = False, + device: str | None = "AUTO", + requires_fp16: bool = False, +) -> tuple[list[str], list[dict[str, Any]]]: + if force_cpu: + return ( + ["CPUExecutionProvider"], + [ + { + "enable_cpu_mem_arena": False, + } + ], + ) + + providers = [] + options = [] + + for provider in ort.get_available_providers(): + if provider == "CUDAExecutionProvider": + device_id = 0 if (not device or not device.isdigit()) else int(device) + providers.append(provider) + options.append( + { + "arena_extend_strategy": "kSameAsRequested", + "use_ep_level_unified_stream": True, + "device_id": device_id, + } + ) + elif provider == "TensorrtExecutionProvider": + # TensorrtExecutionProvider uses too much memory without options to control it + # so it is not enabled by default + if device == "Tensorrt": + os.makedirs( + os.path.join(MODEL_CACHE_DIR, "tensorrt/ort/trt-engines"), + exist_ok=True, + ) + device_id = 0 if not device.isdigit() else int(device) + providers.append(provider) + options.append( + { + "device_id": device_id, + "trt_fp16_enable": requires_fp16 + and os.environ.get("USE_FP_16", "True") != "False", + "trt_timing_cache_enable": True, + "trt_engine_cache_enable": True, + "trt_timing_cache_path": os.path.join( + MODEL_CACHE_DIR, "tensorrt/ort" + ), + "trt_engine_cache_path": os.path.join( + MODEL_CACHE_DIR, "tensorrt/ort/trt-engines" + ), + } + ) + else: + continue + elif provider == "OpenVINOExecutionProvider": + # OpenVINO is used directly + if device == "OpenVINO": + os.makedirs( + os.path.join(MODEL_CACHE_DIR, "openvino/ort"), exist_ok=True + ) + providers.append(provider) + options.append( + { + "cache_dir": os.path.join(MODEL_CACHE_DIR, "openvino/ort"), + "device_type": device, + } + ) + elif provider == "MIGraphXExecutionProvider": + migraphx_cache_dir = os.path.join(MODEL_CACHE_DIR, "migraphx") + os.makedirs(migraphx_cache_dir, exist_ok=True) + + providers.append(provider) + options.append( + { + "migraphx_model_cache_dir": migraphx_cache_dir, + } + ) + elif provider == "CPUExecutionProvider": + providers.append(provider) + options.append( + { + "enable_cpu_mem_arena": False, + } + ) + elif provider == "AzureExecutionProvider": + # Skip Azure provider - not typically available on local hardware + # and prevents fallback to OpenVINO when it's the first provider + continue + else: + providers.append(provider) + options.append({}) + + return (providers, options) diff --git a/frigate/util/object.py b/frigate/util/object.py new file mode 100644 index 0000000..b4ee72d --- /dev/null +++ b/frigate/util/object.py @@ -0,0 +1,588 @@ +"""Utils for reading and writing object detection data.""" + +import datetime +import logging +import math +from collections import defaultdict +from typing import Any + +import cv2 +import numpy as np +from peewee import DoesNotExist + +from frigate.config import DetectConfig, ModelConfig +from frigate.const import ( + LABEL_CONSOLIDATION_DEFAULT, + LABEL_CONSOLIDATION_MAP, + LABEL_NMS_DEFAULT, + LABEL_NMS_MAP, +) +from frigate.detectors.detector_config import PixelFormatEnum +from frigate.models import Event, Regions, Timeline +from frigate.util.image import ( + area, + calculate_region, + clipped, + intersection, + intersection_over_union, + yuv_region_2_bgr, + yuv_region_2_rgb, + yuv_region_2_yuv, +) + +logger = logging.getLogger(__name__) + +GRID_SIZE = 8 + + +def get_camera_regions_grid( + name: str, + detect: DetectConfig, + min_region_size: int, +) -> list[list[dict[str, Any]]]: + """Build a grid of expected region sizes for a camera.""" + # get grid from db if available + try: + regions: Regions = Regions.select().where(Regions.camera == name).get() + grid = regions.grid + last_update = regions.last_update + except DoesNotExist: + grid = [] + for x in range(GRID_SIZE): + row = [] + for y in range(GRID_SIZE): + row.append({"sizes": []}) + grid.append(row) + last_update = 0 + + # get events for timeline entries + events = ( + Event.select(Event.id) + .where(Event.camera == name) + .where((Event.false_positive == None) | (Event.false_positive == False)) + .where(Event.start_time > last_update) + ) + + event_count = events.count() + logger.debug(f"Found {event_count} new events for {name}") + + # no new events, return as is + if event_count == 0: + return grid + + new_update = datetime.datetime.now().timestamp() + timeline = ( + Timeline.select( + *[ + Timeline.camera, + Timeline.source, + Timeline.data, + ] + ) + .where(Timeline.source_id << events) + .limit(10000) + .dicts() + ) + + logger.debug(f"Found {len(timeline)} new entries for {name}") + + width = detect.width + height = detect.height + + for t in timeline: + if t.get("source") != "tracked_object": + continue + + box = t["data"]["box"] + + # calculate centroid position + x = box[0] + (box[2] / 2) + y = box[1] + (box[3] / 2) + + x_pos = int(x * GRID_SIZE) + y_pos = int(y * GRID_SIZE) + + calculated_region = calculate_region( + (height, width), + box[0] * width, + box[1] * height, + (box[0] + box[2]) * width, + (box[1] + box[3]) * height, + min_region_size, + 1.35, + ) + # save width of region to grid as relative + grid[x_pos][y_pos]["sizes"].append( + (calculated_region[2] - calculated_region[0]) / width + ) + + for x in range(GRID_SIZE): + for y in range(GRID_SIZE): + cell = grid[x][y] + + if len(cell["sizes"]) == 0: + continue + + std_dev = np.std(cell["sizes"]) + mean = np.mean(cell["sizes"]) + logger.debug(f"std dev: {std_dev} mean: {mean}") + cell["x"] = x + cell["y"] = y + cell["std_dev"] = std_dev + cell["mean"] = mean + + # update db with new grid + region = { + Regions.camera: name, + Regions.grid: grid, + Regions.last_update: new_update, + } + ( + Regions.insert(region) + .on_conflict( + conflict_target=[Regions.camera], + update=region, + ) + .execute() + ) + + return grid + + +def get_cluster_region_from_grid(frame_shape, min_region, cluster, boxes, region_grid): + min_x = frame_shape[1] + min_y = frame_shape[0] + max_x = 0 + max_y = 0 + for b in cluster: + min_x = min(boxes[b][0], min_x) + min_y = min(boxes[b][1], min_y) + max_x = max(boxes[b][2], max_x) + max_y = max(boxes[b][3], max_y) + return get_region_from_grid( + frame_shape, [min_x, min_y, max_x, max_y], min_region, region_grid + ) + + +def get_region_from_grid( + frame_shape: tuple[int, int], + cluster: list[int], + min_region: int, + region_grid: list[list[dict[str, Any]]], +) -> list[int]: + """Get a region for a box based on the region grid.""" + box = calculate_region( + frame_shape, cluster[0], cluster[1], cluster[2], cluster[3], min_region + ) + centroid = ( + box[0] + (min(frame_shape[1], box[2]) - box[0]) / 2, + box[1] + (min(frame_shape[0], box[3]) - box[1]) / 2, + ) + grid_x = int(centroid[0] / frame_shape[1] * GRID_SIZE) + grid_y = int(centroid[1] / frame_shape[0] * GRID_SIZE) + + cell = region_grid[grid_x][grid_y] + + # if there is no known data, use original region calculation + if not cell or not cell["sizes"]: + return box + + # convert the calculated region size to relative + calc_size = (box[2] - box[0]) / frame_shape[1] + + # if region is within expected size, don't resize + if ( + (cell["mean"] - cell["std_dev"]) + <= calc_size + <= (cell["mean"] + cell["std_dev"]) + ): + return box + # TODO not sure how to handle case where cluster is larger than expected region + elif calc_size > (cell["mean"] + cell["std_dev"]): + return box + + size = cell["mean"] * frame_shape[1] + + # get region based on grid size + return calculate_region( + frame_shape, + max(0, centroid[0] - size / 2), + max(0, centroid[1] - size / 2), + min(frame_shape[1], centroid[0] + size / 2), + min(frame_shape[0], centroid[1] + size / 2), + min_region, + ) + + +def is_object_filtered(obj, objects_to_track, object_filters): + object_name = obj[0] + object_score = obj[1] + object_box = obj[2] + object_area = obj[3] + object_ratio = obj[4] + + if object_name not in objects_to_track: + return True + + if object_name in object_filters: + obj_settings = object_filters[object_name] + + # if the min area is larger than the + # detected object, don't add it to detected objects + if obj_settings.min_area > object_area: + return True + + # if the detected object is larger than the + # max area, don't add it to detected objects + if obj_settings.max_area < object_area: + return True + + # if the score is lower than the min_score, skip + if obj_settings.min_score > object_score: + return True + + # if the object is not proportionally wide enough + if obj_settings.min_ratio > object_ratio: + return True + + # if the object is proportionally too wide + if obj_settings.max_ratio < object_ratio: + return True + + if obj_settings.rasterized_mask is not None: + # compute the coordinates of the object and make sure + # the location isn't outside the bounds of the image (can happen from rounding) + object_xmin = object_box[0] + object_xmax = object_box[2] + object_ymax = object_box[3] + y_location = min(int(object_ymax), len(obj_settings.rasterized_mask) - 1) + x_location = min( + int((object_xmax + object_xmin) / 2.0), + len(obj_settings.rasterized_mask[0]) - 1, + ) + + # if the object is in a masked location, don't add it to detected objects + if obj_settings.rasterized_mask[y_location][x_location] == 0: + return True + + return False + + +def get_min_region_size(model_config: ModelConfig) -> int: + """Get the min region size.""" + largest_dimension = max(model_config.height, model_config.width) + + # return largest dimension for smaller models, but make sure the dimension is normalized + if largest_dimension < 320: + if largest_dimension % 4 == 0: + return largest_dimension + + return int((largest_dimension + 3) / 4) * 4 + + # Any model that is 320 or larger should have a minimum region size of 320 + # this allows larger models to use smaller regions to detect smaller objects + # in the case that the motion area is smaller so that it can be upscaled. + return 320 + + +def create_tensor_input(frame, model_config: ModelConfig, region): + if model_config.input_pixel_format == PixelFormatEnum.rgb: + cropped_frame = yuv_region_2_rgb(frame, region) + elif model_config.input_pixel_format == PixelFormatEnum.bgr: + cropped_frame = yuv_region_2_bgr(frame, region) + else: + cropped_frame = yuv_region_2_yuv(frame, region) + + # Resize if needed + if cropped_frame.shape != (model_config.height, model_config.width, 3): + cropped_frame = cv2.resize( + cropped_frame, + dsize=(model_config.width, model_config.height), + interpolation=cv2.INTER_LINEAR, + ) + + # Expand dimensions since the model expects images to have shape: [1, height, width, 3] + return np.expand_dims(cropped_frame, axis=0) + + +def box_overlaps(b1, b2): + if b1[2] < b2[0] or b1[0] > b2[2] or b1[1] > b2[3] or b1[3] < b2[1]: + return False + return True + + +def box_inside(b1, b2): + # check if b2 is inside b1 + if b2[0] >= b1[0] and b2[1] >= b1[1] and b2[2] <= b1[2] and b2[3] <= b1[3]: + return True + return False + + +def reduce_boxes(boxes, iou_threshold=0.0): + clusters = [] + + for box in boxes: + matched = 0 + for cluster in clusters: + if intersection_over_union(box, cluster) > iou_threshold: + matched = 1 + cluster[0] = min(cluster[0], box[0]) + cluster[1] = min(cluster[1], box[1]) + cluster[2] = max(cluster[2], box[2]) + cluster[3] = max(cluster[3], box[3]) + + if not matched: + clusters.append(list(box)) + + return [tuple(c) for c in clusters] + + +def average_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]: + """Return a box that is the average of a list of boxes.""" + n = len(boxes) + return [ + sum(box[0] for box in boxes) / n, + sum(box[1] for box in boxes) / n, + sum(box[2] for box in boxes) / n, + sum(box[3] for box in boxes) / n, + ] + + +def median_of_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]: + """Return a box that is the median of a list of boxes.""" + sorted_boxes = sorted(boxes, key=lambda x: area(x)) + return sorted_boxes[int(len(sorted_boxes) / 2.0)] + + +def intersects_any(box_a, boxes): + for box in boxes: + if box_overlaps(box_a, box): + return True + return False + + +def inside_any(box_a, boxes): + for box in boxes: + # check if box_a is inside of box + if box_inside(box, box_a): + return True + return False + + +def get_cluster_boundary(box, min_region): + # compute the max region size for the current box (box is 10% of region) + box_width = box[2] - box[0] + box_height = box[3] - box[1] + max_region_area = abs(box_width * box_height) / 0.1 + max_region_size = max(min_region, int(math.sqrt(max_region_area))) + + centroid = (box_width / 2 + box[0], box_height / 2 + box[1]) + + max_x_dist = int(max_region_size - box_width / 2 * 1.1) + max_y_dist = int(max_region_size - box_height / 2 * 1.1) + + return [ + int(centroid[0] - max_x_dist), + int(centroid[1] - max_y_dist), + int(centroid[0] + max_x_dist), + int(centroid[1] + max_y_dist), + ] + + +def get_cluster_candidates(frame_shape, min_region, boxes): + # and create a cluster of other boxes using it's max region size + # only include boxes where the region is an appropriate(except the region could possibly be smaller?) + # size in the cluster. in order to be in the cluster, the furthest corner needs to be within x,y offset + # determined by the max_region size minus half the box + 20% + # TODO: see if we can do this with numpy + cluster_candidates = [] + used_boxes = set() + # loop over each box + for current_index, b in enumerate(boxes): + if current_index in used_boxes: + continue + cluster = [current_index] + used_boxes.add(current_index) + cluster_boundary = get_cluster_boundary(b, min_region) + # find all other boxes that fit inside the boundary + for compare_index, compare_box in enumerate(boxes): + if compare_index in used_boxes: + continue + + # if the box is not inside the potential cluster area, cluster them + if not box_inside(cluster_boundary, compare_box): + continue + + # get the region if you were to add this box to the cluster + potential_cluster = cluster + [compare_index] + cluster_region = get_cluster_region( + frame_shape, min_region, potential_cluster, boxes + ) + # if region could be smaller and either box would be too small + # for the resulting region, dont cluster + should_cluster = True + if (cluster_region[2] - cluster_region[0]) > min_region: + for b in potential_cluster: + box = boxes[b] + # boxes should be more than 5% of the area of the region + if area(box) / area(cluster_region) < 0.05: + should_cluster = False + break + + if should_cluster: + cluster.append(compare_index) + used_boxes.add(compare_index) + cluster_candidates.append(cluster) + + # return the unique clusters only + unique = {tuple(sorted(c)) for c in cluster_candidates} + return [list(tup) for tup in unique] + + +def get_cluster_region(frame_shape, min_region, cluster, boxes): + min_x = frame_shape[1] + min_y = frame_shape[0] + max_x = 0 + max_y = 0 + for b in cluster: + min_x = min(boxes[b][0], min_x) + min_y = min(boxes[b][1], min_y) + max_x = max(boxes[b][2], max_x) + max_y = max(boxes[b][3], max_y) + return calculate_region( + frame_shape, min_x, min_y, max_x, max_y, min_region, multiplier=1.35 + ) + + +def get_startup_regions( + frame_shape: tuple[int, int], + region_min_size: int, + region_grid: list[list[dict[str, Any]]], +) -> list[list[int]]: + """Get a list of regions to run on startup.""" + # return 8 most popular regions for the camera + all_cells = np.concatenate(region_grid).flat + startup_cells = sorted(all_cells, key=lambda c: len(c["sizes"]), reverse=True)[0:8] + regions = [] + + for cell in startup_cells: + # rest of the cells are empty + if not cell["sizes"]: + break + + x = frame_shape[1] / GRID_SIZE * (0.5 + cell["x"]) + y = frame_shape[0] / GRID_SIZE * (0.5 + cell["y"]) + size = cell["mean"] * frame_shape[1] + regions.append( + calculate_region( + frame_shape, + x - size / 2, + y - size / 2, + x + size / 2, + y + size / 2, + region_min_size, + multiplier=1, + ) + ) + + return regions + + +def reduce_detections( + frame_shape: tuple[int, int], + all_detections: list[tuple[Any]], +) -> list[tuple[Any]]: + """Take a list of detections and reduce overlaps to create a list of confident detections.""" + + def reduce_overlapping_detections(detections: list[tuple[Any]]) -> list[tuple[Any]]: + """apply non-maxima suppression to suppress weak, overlapping bounding boxes.""" + detected_object_groups = defaultdict(lambda: []) + for detection in detections: + detected_object_groups[detection[0]].append(detection) + + selected_objects = [] + for group in detected_object_groups.values(): + label = group[0][0] + # o[2] is the box of the object: xmin, ymin, xmax, ymax + # apply max/min to ensure values do not exceed the known frame size + boxes = [ + ( + o[2][0], + o[2][1], + o[2][2] - o[2][0], + o[2][3] - o[2][1], + ) + for o in group + ] + + # reduce confidences for objects that are on edge of region + # 0.6 should be used to ensure that the object is still considered and not dropped + # due to min score requirement of NMSBoxes + confidences = [0.6 if clipped(o, frame_shape) else o[1] for o in group] + + indices = cv2.dnn.NMSBoxes( + boxes, confidences, 0.5, LABEL_NMS_MAP.get(label, LABEL_NMS_DEFAULT) + ) + + # add objects + for index in indices: + index = index if isinstance(index, np.int32) else index[0] + obj = group[index] + selected_objects.append(obj) + + # set the detections list to only include top objects + return selected_objects + + def get_consolidated_object_detections(detections: list[tuple[Any]]): + """Drop detections that overlap too much.""" + detected_object_groups = defaultdict(lambda: []) + for detection in detections: + detected_object_groups[detection[0]].append(detection) + + consolidated_detections = [] + for group in detected_object_groups.values(): + # if the group only has 1 item, skip + if len(group) == 1: + consolidated_detections.append(group[0]) + continue + + # sort smallest to largest by area + sorted_by_area = sorted(group, key=lambda g: g[3]) + + for current_detection_idx in range(0, len(sorted_by_area)): + current_detection = sorted_by_area[current_detection_idx] + current_label = current_detection[0] + current_box = current_detection[2] + current_area = area(current_box) + overlap = 0 + for to_check_idx in range( + min(current_detection_idx + 1, len(sorted_by_area)), + len(sorted_by_area), + ): + to_check = sorted_by_area[to_check_idx][2] + + # if area of current detection / area of check < 5% they should not be compared + # this covers cases where a large car parked in a driveway doesn't block detections + # of cars in the street behind it + if current_area / area(to_check) < 0.05: + continue + + intersect_box = intersection(current_box, to_check) + # if % of smaller detection is inside of another detection, consolidate + if intersect_box is not None and area( + intersect_box + ) / current_area > LABEL_CONSOLIDATION_MAP.get( + current_label, LABEL_CONSOLIDATION_DEFAULT + ): + overlap = 1 + break + if overlap == 0: + consolidated_detections.append( + sorted_by_area[current_detection_idx] + ) + + return consolidated_detections + + return get_consolidated_object_detections( + reduce_overlapping_detections(all_detections) + ) diff --git a/frigate/util/process.py b/frigate/util/process.py new file mode 100644 index 0000000..060c39c --- /dev/null +++ b/frigate/util/process.py @@ -0,0 +1,159 @@ +import atexit +import faulthandler +import logging +import multiprocessing as mp +import os +import pathlib +import subprocess +import threading +from collections.abc import Callable +from logging.handlers import QueueHandler +from multiprocessing.synchronize import Event as MpEvent + +from setproctitle import setproctitle + +import frigate.log +from frigate.config.logger import LoggerConfig +from frigate.const import CONFIG_DIR + + +class BaseProcess(mp.Process): + def __init__( + self, + stop_event: MpEvent, + priority: int, + *, + name: str | None = None, + target: Callable | None = None, + args: tuple = (), + kwargs: dict = {}, + daemon: bool | None = None, + ): + self.priority = priority + self.stop_event = stop_event + super().__init__( + name=name, target=target, args=args, kwargs=kwargs, daemon=daemon + ) + + def start(self, *args, **kwargs): + self.before_start() + super().start(*args, **kwargs) + self.after_start() + + def before_start(self) -> None: + pass + + def after_start(self) -> None: + pass + + +class FrigateProcess(BaseProcess): + logger: logging.Logger + + def before_start(self) -> None: + self.__log_queue = frigate.log.log_listener.queue + self.__memray_tracker = None + + def pre_run_setup(self, logConfig: LoggerConfig | None = None) -> None: + os.nice(self.priority) + setproctitle(self.name) + threading.current_thread().name = f"process:{self.name}" + faulthandler.enable() + + # setup logging + self.logger = logging.getLogger(self.name) + logging.basicConfig(handlers=[], force=True) + logging.getLogger().addHandler(QueueHandler(self.__log_queue)) + + # Always apply base log level suppressions for noisy third-party libraries + # even if no specific logConfig is provided + if logConfig: + frigate.log.apply_log_levels( + logConfig.default.value.upper(), logConfig.logs + ) + else: + # Apply default INFO level with standard library suppressions + frigate.log.apply_log_levels("INFO", {}) + + self._setup_memray() + + def _setup_memray(self) -> None: + """Setup memray profiling if enabled via environment variable.""" + memray_modules = os.environ.get("FRIGATE_MEMRAY_MODULES", "") + + if not memray_modules: + return + + # Extract module name from process name (e.g., "frigate.capture:camera" -> "frigate.capture") + process_name = self.name + module_name = ( + process_name.split(":")[0] if ":" in process_name else process_name + ) + + enabled_modules = [m.strip() for m in memray_modules.split(",")] + + if module_name not in enabled_modules and process_name not in enabled_modules: + return + + try: + import memray + + reports_dir = pathlib.Path(CONFIG_DIR) / "memray_reports" + reports_dir.mkdir(parents=True, exist_ok=True) + safe_name = ( + process_name.replace(":", "_").replace("/", "_").replace("\\", "_") + ) + + binary_file = reports_dir / f"{safe_name}.bin" + + self.__memray_tracker = memray.Tracker(str(binary_file)) + self.__memray_tracker.__enter__() + + # Register cleanup handler to stop tracking and generate HTML report + # atexit runs on normal exits and most signal-based terminations (SIGTERM, SIGINT) + # For hard kills (SIGKILL) or segfaults, the binary file is preserved for manual generation + atexit.register(self._cleanup_memray, safe_name, binary_file) + + self.logger.info( + f"Memray profiling enabled for module {module_name} (process: {self.name}). " + f"Binary file (updated continuously): {binary_file}. " + f"HTML report will be generated on exit: {reports_dir}/{safe_name}.html. " + f"If process crashes, manually generate with: memray flamegraph {binary_file}" + ) + except Exception as e: + self.logger.exception(f"Failed to setup memray profiling: {e}") + + def _cleanup_memray(self, safe_name: str, binary_file: pathlib.Path) -> None: + """Stop memray tracking and generate HTML report.""" + if self.__memray_tracker is None: + return + + try: + self.__memray_tracker.__exit__(None, None, None) + self.__memray_tracker = None + + reports_dir = pathlib.Path(CONFIG_DIR) / "memray_reports" + html_file = reports_dir / f"{safe_name}.html" + + result = subprocess.run( + ["memray", "flamegraph", "--output", str(html_file), str(binary_file)], + capture_output=True, + text=True, + timeout=10, + ) + + if result.returncode == 0: + self.logger.info(f"Memray report generated: {html_file}") + else: + self.logger.error( + f"Failed to generate memray report: {result.stderr}. " + f"Binary file preserved at {binary_file} for manual generation." + ) + + # Keep the binary file for manual report generation if needed + # Users can run: memray flamegraph {binary_file} + + except subprocess.TimeoutExpired: + self.logger.error("Memray report generation timed out") + except Exception as e: + self.logger.exception(f"Failed to cleanup memray profiling: {e}") diff --git a/frigate/util/rknn_converter.py b/frigate/util/rknn_converter.py new file mode 100644 index 0000000..ad38741 --- /dev/null +++ b/frigate/util/rknn_converter.py @@ -0,0 +1,413 @@ +"""RKNN model conversion utility for Frigate.""" + +import logging +import os +import subprocess +import sys +import time +from pathlib import Path + +from frigate.const import SUPPORTED_RK_SOCS +from frigate.util.file import FileLock + +logger = logging.getLogger(__name__) + +MODEL_TYPE_CONFIGS = { + "yolo-generic": { + "mean_values": [[0, 0, 0]], + "std_values": [[1, 1, 1]], + "target_platform": None, # Will be set dynamically + }, + "yolonas": { + "mean_values": [[0, 0, 0]], + "std_values": [[255, 255, 255]], + "target_platform": None, # Will be set dynamically + }, + "yolox": { + "mean_values": [[0, 0, 0]], + "std_values": [[255, 255, 255]], + "target_platform": None, # Will be set dynamically + }, + "jina-clip-v1-vision": { + "mean_values": [[0.48145466 * 255, 0.4578275 * 255, 0.40821073 * 255]], + "std_values": [[0.26862954 * 255, 0.26130258 * 255, 0.27577711 * 255]], + "target_platform": None, # Will be set dynamically + }, + "arcface-r100": { + "mean_values": [[127.5, 127.5, 127.5]], + "std_values": [[127.5, 127.5, 127.5]], + "target_platform": None, # Will be set dynamically + }, +} + + +def get_rknn_model_type(model_path: str) -> str | None: + if all(keyword in str(model_path) for keyword in ["jina-clip-v1", "vision"]): + return "jina-clip-v1-vision" + + model_name = os.path.basename(str(model_path)).lower() + + if "arcface" in model_name: + return "arcface-r100" + + if any(keyword in model_name for keyword in ["yolo", "yolox", "yolonas"]): + return model_name + + return None + + +def is_rknn_compatible(model_path: str, model_type: str | None = None) -> bool: + """ + Check if a model is compatible with RKNN conversion. + + Args: + model_path: Path to the model file + model_type: Type of the model (if known) + + Returns: + True if the model is RKNN-compatible, False otherwise + """ + soc = get_soc_type() + + if soc is None: + return False + + # Check if the SoC is actually a supported RK device + # This prevents false positives on non-RK devices (e.g., macOS Docker) + # where /proc/device-tree/compatible might exist but contain non-RK content + if soc not in SUPPORTED_RK_SOCS: + logger.debug( + f"SoC '{soc}' is not a supported RK device for RKNN conversion. " + f"Supported SoCs: {SUPPORTED_RK_SOCS}" + ) + return False + + if not model_type: + model_type = get_rknn_model_type(model_path) + + if model_type and model_type in MODEL_TYPE_CONFIGS: + return True + + return False + + +def ensure_torch_dependencies() -> bool: + """Dynamically install torch dependencies if not available.""" + try: + import torch # type: ignore + + logger.debug("PyTorch is already available") + return True + except ImportError: + logger.info("PyTorch not found, attempting to install...") + + try: + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "--break-system-packages", + "setuptools<81", + "torch", + "torchvision", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + import torch # type: ignore # noqa: F401 + + logger.info("PyTorch installed successfully") + return True + except (subprocess.CalledProcessError, ImportError) as e: + logger.error(f"Failed to install PyTorch: {e}") + return False + + +def ensure_rknn_toolkit() -> bool: + """Ensure RKNN toolkit is available.""" + try: + from rknn.api import RKNN # type: ignore # noqa: F401 + + logger.debug("RKNN toolkit is already available") + return True + except ImportError as e: + logger.error(f"RKNN toolkit not found. Please ensure it's installed. {e}") + return False + + +def get_soc_type() -> str | None: + """Get the SoC type from device tree.""" + try: + with open("/proc/device-tree/compatible") as file: + content = file.read() + + # Check for Jetson devices + if "nvidia" in content: + return None + + return content.split(",")[-1].strip("\x00") + except FileNotFoundError: + logger.debug("Could not determine SoC type from device tree") + return None + + +def convert_onnx_to_rknn( + onnx_path: str, + output_path: str, + model_type: str, + quantization: bool = False, + soc: str | None = None, +) -> bool: + """ + Convert ONNX model to RKNN format. + + Args: + onnx_path: Path to input ONNX model + output_path: Path for output RKNN model + model_type: Type of model (yolo-generic, yolonas, yolox, ssd) + quantization: Whether to use 8-bit quantization (i8) or 16-bit float (fp16) + soc: Target SoC platform (auto-detected if None) + + Returns: + True if conversion successful, False otherwise + """ + if not ensure_torch_dependencies(): + logger.debug("PyTorch dependencies not available") + return False + + if not ensure_rknn_toolkit(): + logger.debug("RKNN toolkit not available") + return False + + # Get SoC type if not provided + if soc is None: + soc = get_soc_type() + if soc is None: + logger.debug("Could not determine SoC type") + return False + + # Get model config for the specified type + if model_type not in MODEL_TYPE_CONFIGS: + logger.debug(f"Unsupported model type: {model_type}") + return False + + config = MODEL_TYPE_CONFIGS[model_type].copy() + config["target_platform"] = soc + + # RKNN toolkit requires .onnx extension, create temporary copy if needed + temp_onnx_path = None + onnx_model_path = onnx_path + + if not onnx_path.endswith(".onnx"): + import shutil + + temp_onnx_path = f"{onnx_path}.onnx" + logger.debug(f"Creating temporary ONNX copy: {temp_onnx_path}") + try: + shutil.copy2(onnx_path, temp_onnx_path) + onnx_model_path = temp_onnx_path + except Exception as e: + logger.error(f"Failed to create temporary ONNX copy: {e}") + return False + + try: + from rknn.api import RKNN # type: ignore + + logger.info(f"Converting {onnx_path} to RKNN format for {soc}") + rknn = RKNN(verbose=True) + rknn.config(**config) + + if model_type == "jina-clip-v1-vision": + load_output = rknn.load_onnx( + model=onnx_model_path, + inputs=["pixel_values"], + input_size_list=[[1, 3, 224, 224]], + ) + elif model_type == "arcface-r100": + load_output = rknn.load_onnx( + model=onnx_model_path, + inputs=["data"], + input_size_list=[[1, 3, 112, 112]], + ) + else: + load_output = rknn.load_onnx(model=onnx_model_path) + + if load_output != 0: + logger.error("Failed to load ONNX model") + return False + + if rknn.build(do_quantization=quantization) != 0: + logger.error("Failed to build RKNN model") + return False + + if rknn.export_rknn(output_path) != 0: + logger.error("Failed to export RKNN model") + return False + + logger.info(f"Successfully converted model to {output_path}") + return True + + except Exception as e: + logger.error(f"Error during RKNN conversion: {e}") + return False + finally: + # Clean up temporary file if created + if temp_onnx_path and os.path.exists(temp_onnx_path): + try: + os.remove(temp_onnx_path) + logger.debug(f"Removed temporary ONNX file: {temp_onnx_path}") + except Exception as e: + logger.warning(f"Failed to remove temporary ONNX file: {e}") + + +def wait_for_conversion_completion( + model_type: str, rknn_path: Path, lock_file_path: Path, timeout: int = 300 +) -> bool: + """ + Wait for another process to complete the conversion. + + Args: + model_type: Type of model being converted + rknn_path: Path to the expected RKNN model + lock_file_path: Path to the lock file to monitor + timeout: Maximum time to wait in seconds + + Returns: + True if RKNN model appears, False if timeout + """ + start_time = time.time() + lock = FileLock(lock_file_path, stale_timeout=600) + + while time.time() - start_time < timeout: + # Check if RKNN model appeared + if rknn_path.exists(): + logger.info(f"RKNN model appeared: {rknn_path}") + return True + + # Check if lock file is gone (conversion completed or failed) + if not lock_file_path.exists(): + logger.info("Lock file removed, checking for RKNN model...") + if rknn_path.exists(): + logger.info(f"RKNN model found after lock removal: {rknn_path}") + return True + else: + logger.warning( + "Lock file removed but RKNN model not found, conversion may have failed" + ) + return False + + # Check if lock is stale + if lock.is_stale(): + logger.warning("Lock file is stale, attempting to clean up and retry...") + lock._cleanup_stale_lock() + # Try to acquire lock again + retry_lock = FileLock( + lock_file_path, timeout=60, cleanup_stale_on_init=True + ) + if retry_lock.acquire(): + try: + # Check if RKNN file appeared while waiting + if rknn_path.exists(): + logger.info(f"RKNN model appeared while waiting: {rknn_path}") + return True + + # Convert ONNX to RKNN + logger.info( + f"Retrying conversion of {rknn_path} after stale lock cleanup..." + ) + + # Get the original model path from rknn_path + base_path = rknn_path.parent / rknn_path.stem + onnx_path = base_path.with_suffix(".onnx") + + if onnx_path.exists(): + if convert_onnx_to_rknn( + str(onnx_path), str(rknn_path), model_type, False + ): + return True + + logger.error("Failed to convert model after stale lock cleanup") + return False + + finally: + retry_lock.release() + + logger.debug("Waiting for RKNN model to appear...") + time.sleep(1) + + logger.warning(f"Timeout waiting for RKNN model: {rknn_path}") + return False + + +def auto_convert_model( + model_path: str, model_type: str | None = None, quantization: bool = False +) -> str | None: + """ + Automatically convert a model to RKNN format if needed. + + Args: + model_path: Path to the model file + model_type: Type of the model + quantization: Whether to use quantization + + Returns: + Path to the RKNN model if successful, None otherwise + """ + if model_path.endswith(".rknn"): + return model_path + + # Check if equivalent .rknn file exists + base_path = Path(model_path) + if base_path.suffix.lower() in [".onnx", ""]: + base_name = base_path.stem if base_path.suffix else base_path.name + rknn_path = base_path.parent / f"{base_name}.rknn" + + if rknn_path.exists(): + logger.info(f"Found existing RKNN model: {rknn_path}") + return str(rknn_path) + + lock_file_path = base_path.parent / f"{base_name}.conversion.lock" + lock = FileLock(lock_file_path, timeout=300, cleanup_stale_on_init=True) + + if lock.acquire(): + try: + if rknn_path.exists(): + logger.info( + f"RKNN model appeared while waiting for lock: {rknn_path}" + ) + return str(rknn_path) + + logger.info(f"Converting {model_path} to RKNN format...") + rknn_path.parent.mkdir(parents=True, exist_ok=True) + + if not model_type: + model_type = get_rknn_model_type(base_path) + + if convert_onnx_to_rknn( + str(base_path), str(rknn_path), model_type, quantization + ): + return str(rknn_path) + else: + logger.error(f"Failed to convert {model_path} to RKNN format") + return None + + finally: + lock.release() + else: + logger.info( + f"Another process is converting {model_path}, waiting for completion..." + ) + + if not model_type: + model_type = get_rknn_model_type(base_path) + + if wait_for_conversion_completion(model_type, rknn_path, lock_file_path): + return str(rknn_path) + else: + logger.error(f"Timeout waiting for conversion of {model_path}") + return None + + return None diff --git a/frigate/util/schema.py b/frigate/util/schema.py new file mode 100644 index 0000000..9af651e --- /dev/null +++ b/frigate/util/schema.py @@ -0,0 +1,46 @@ +"""JSON schema utilities for Frigate.""" + +from typing import Any + +from pydantic import BaseModel, TypeAdapter + + +def get_config_schema(config_class: type[BaseModel]) -> dict[str, Any]: + """ + Returns the JSON schema for FrigateConfig with polymorphic detectors. + + This utility patches the FrigateConfig schema to include the full polymorphic + definitions for detectors. By default, Pydantic's schema for Dict[str, BaseDetectorConfig] + only includes the base class fields. This function replaces it with a reference + to the DetectorConfig union, which includes all available detector subclasses. + """ + # Import here to ensure all detector plugins are loaded through the detectors module + from frigate.detectors import DetectorConfig + + # Get the base schema for FrigateConfig + schema = config_class.model_json_schema() + + # Get the schema for the polymorphic DetectorConfig union + detector_adapter: TypeAdapter = TypeAdapter(DetectorConfig) + detector_schema = detector_adapter.json_schema() + + # Ensure $defs exists in FrigateConfig schema + if "$defs" not in schema: + schema["$defs"] = {} + + # Merge $defs from DetectorConfig into FrigateConfig schema + # This includes the specific schemas for each detector plugin (OvDetectorConfig, etc.) + if "$defs" in detector_schema: + schema["$defs"].update(detector_schema["$defs"]) + + # Extract the union schema (oneOf/discriminator) and add it as a definition + detector_union_schema = {k: v for k, v in detector_schema.items() if k != "$defs"} + schema["$defs"]["DetectorConfig"] = detector_union_schema + + # Update the 'detectors' property to use the polymorphic DetectorConfig definition + if "detectors" in schema.get("properties", {}): + schema["properties"]["detectors"]["additionalProperties"] = { + "$ref": "#/$defs/DetectorConfig" + } + + return schema diff --git a/frigate/util/services.py b/frigate/util/services.py new file mode 100644 index 0000000..956e789 --- /dev/null +++ b/frigate/util/services.py @@ -0,0 +1,1341 @@ +"""Utilities for services.""" + +import asyncio +import json +import logging +import os +import re +import resource +import shutil +import signal +import subprocess as sp +import time +import traceback +from datetime import datetime +from typing import Any + +import cv2 +import psutil +import py3nvml.py3nvml as nvml +import requests + +from frigate.const import ( + DRIVER_AMD, + DRIVER_ENV_VAR, + FFMPEG_HWACCEL_NVIDIA, + FFMPEG_HWACCEL_VAAPI, + SHM_FRAMES_VAR, +) +from frigate.util.builtin import clean_camera_user_pass, escape_special_characters + +logger = logging.getLogger(__name__) + + +def restart_frigate(): + proc = psutil.Process(1) + # if this is running via s6, sigterm pid 1 + if proc.name() == "s6-svscan": + proc.terminate() + # otherwise, just try and exit frigate + else: + os.kill(os.getpid(), signal.SIGINT) + + +def print_stack(sig, frame): + traceback.print_stack(frame) + + +def listen(): + signal.signal(signal.SIGUSR1, print_stack) + + +def get_cgroups_version() -> str: + """Determine what version of cgroups is enabled.""" + + cgroup_path = "/sys/fs/cgroup" + + if not os.path.ismount(cgroup_path): + logger.debug(f"{cgroup_path} is not a mount point.") + return "unknown" + + try: + with open("/proc/mounts") as f: + mounts = f.readlines() + + for mount in mounts: + mount_info = mount.split() + if mount_info[1] == cgroup_path: + fs_type = mount_info[2] + if fs_type == "cgroup2fs" or fs_type == "cgroup2": + return "cgroup2" + elif fs_type == "tmpfs": + return "cgroup" + else: + logger.debug( + f"Could not determine cgroups version: unhandled filesystem {fs_type}" + ) + break + except Exception as e: + logger.debug(f"Could not determine cgroups version: {e}") + + return "unknown" + + +def get_docker_memlimit_bytes() -> int: + """Get mem limit in bytes set in docker if present. Returns -1 if no limit detected.""" + + # check running a supported cgroups version + if get_cgroups_version() == "cgroup2": + memlimit_path = "/sys/fs/cgroup/memory.max" + + try: + with open(memlimit_path) as f: + value = f.read().strip() + + if value.isnumeric(): + return int(value) + elif value.lower() == "max": + return -1 + except Exception as e: + logger.debug(f"Unable to get docker memlimit: {e}") + + return -1 + + +def get_cpu_stats() -> dict[str, dict]: + """Get cpu usages for each process id""" + usages = {} + docker_memlimit = get_docker_memlimit_bytes() / 1024 + total_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1024 + + system_cpu = psutil.cpu_percent( + interval=None + ) # no interval as we don't want to be blocking + system_mem = psutil.virtual_memory() + usages["frigate.full_system"] = { + "cpu": str(system_cpu), + "mem": str(system_mem.percent), + } + + keywords = ["ffmpeg", "go2rtc", "frigate.", "python3"] + for process in psutil.process_iter(["pid", "name", "cpu_percent", "cmdline"]): + pid = str(process.info["pid"]) + try: + cpu_percent = process.info["cpu_percent"] + cmdline = " ".join(process.info["cmdline"]).rstrip() + + if not any(keyword in cmdline for keyword in keywords): + continue + + with open(f"/proc/{pid}/stat") as f: + stats = f.readline().split() + utime = int(stats[13]) + stime = int(stats[14]) + start_time = int(stats[21]) + + with open("/proc/uptime") as f: + system_uptime_sec = int(float(f.read().split()[0])) + + clk_tck = os.sysconf(os.sysconf_names["SC_CLK_TCK"]) + + process_utime_sec = utime // clk_tck + process_stime_sec = stime // clk_tck + process_start_time_sec = start_time // clk_tck + + process_elapsed_sec = system_uptime_sec - process_start_time_sec + process_usage_sec = process_utime_sec + process_stime_sec + cpu_average_usage = process_usage_sec * 100 // process_elapsed_sec + + with open(f"/proc/{pid}/statm") as f: + mem_stats = f.readline().split() + mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024 + + if docker_memlimit > 0: + mem_pct = round((mem_res / docker_memlimit) * 100, 1) + else: + mem_pct = round((mem_res / total_mem) * 100, 1) + + usages[pid] = { + "cpu": str(cpu_percent), + "cpu_average": str(round(cpu_average_usage, 2)), + "mem": f"{mem_pct}", + "cmdline": clean_camera_user_pass(cmdline), + } + except Exception: + continue + + return usages + + +def get_physical_interfaces(interfaces) -> list: + if not interfaces: + return [] + + with open("/proc/net/dev") as file: + lines = file.readlines() + + physical_interfaces = [] + for line in lines: + if ":" in line: + interface = line.split(":")[0].strip() + for int in interfaces: + if interface.startswith(int): + physical_interfaces.append(interface) + + return physical_interfaces + + +def get_bandwidth_stats(config) -> dict[str, dict]: + """Get bandwidth usages for each ffmpeg process id""" + usages = {} + top_command = ["nethogs", "-t", "-v0", "-c5", "-d1"] + get_physical_interfaces( + config.telemetry.network_interfaces + ) + + p = sp.run( + top_command, + encoding="ascii", + capture_output=True, + ) + + if p.returncode != 0: + logger.error(f"Error getting network stats :: {p.stderr}") + return usages + else: + lines = p.stdout.split("\n") + for line in lines: + stats = list(filter(lambda a: a != "", line.strip().split("\t"))) + try: + if re.search( + r"(^ffmpeg|\/go2rtc|frigate\.detector\.[a-z]+)/([0-9]+)/", stats[0] + ): + process = stats[0].split("/") + usages[process[len(process) - 2]] = { + "bandwidth": round(float(stats[1]) + float(stats[2]), 1), + } + except (IndexError, ValueError): + continue + + return usages + + +def is_vaapi_amd_driver() -> bool: + # Use the explicitly configured driver, if available + driver = os.environ.get(DRIVER_ENV_VAR) + if driver: + return driver == DRIVER_AMD + + # Otherwise, ask vainfo what is has autodetected + p = vainfo_hwaccel() + + if p.returncode != 0: + logger.error(f"Unable to poll vainfo: {p.stderr}") + return False + else: + output = p.stdout.decode("unicode_escape").split("\n") + + # VA Info will print out the friendly name of the driver + return any("AMD Radeon Graphics" in line for line in output) + + +def get_amd_gpu_stats() -> dict[str, str] | None: + """Get stats using radeontop.""" + radeontop_command = ["radeontop", "-d", "-", "-l", "1"] + + p = sp.run( + radeontop_command, + encoding="ascii", + capture_output=True, + ) + + if p.returncode != 0: + logger.error(f"Unable to poll radeon GPU stats: {p.stderr}") + return None + else: + usages = p.stdout.split(",") + results: dict[str, str] = {} + + for hw in usages: + if "gpu" in hw: + results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" + elif "vram" in hw: + results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" + + return results + + +_INTEL_FDINFO_SAMPLE_SECONDS = 1.0 + +# Engines we track. Render/3D and Compute are pooled into "compute"; Video and +# VideoEnhance into "dec" (VideoEnhance is the post-process engine that handles +# VAAPI scaling/deinterlace/CSC, e.g. ffmpeg `-vf scale_vaapi=...`). The Copy +# (DMA blitter) engine is intentionally ignored — it represents transparent +# memory transfers, not user-visible GPU work. +# i915 fdinfo keys (cumulative ns) → logical engine name. +_I915_ENGINE_KEYS = { + "drm-engine-render": "render", + "drm-engine-video": "video", + "drm-engine-video-enhance": "video-enhance", + "drm-engine-compute": "compute", +} +# Xe fdinfo suffixes (cumulative cycles, paired with drm-total-cycles-*). +_XE_ENGINE_KEYS = { + "rcs": "render", + "vcs": "video", + "vecs": "video-enhance", + "ccs": "compute", +} + + +def _resolve_intel_gpu_pdev(device: str | None) -> str | None: + """Map a configured GPU hint (/dev/dri/card1, renderD128, or a PCI bus + address) to its drm-pdev string so we can filter fdinfo entries to that + device. Returns None when no hint is supplied or it cannot be resolved.""" + if not device: + return None + + if re.match(r"^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$", device): + return device + + name = os.path.basename(device.rstrip("/")) + try: + return os.path.basename(os.path.realpath(f"/sys/class/drm/{name}/device")) + except OSError: + return None + + +def _read_intel_drm_fdinfo(target_pdev: str | None) -> dict: + """Snapshot DRM fdinfo for every Intel client visible in /proc. + + Returns a dict keyed by (pdev, drm-client-id, pid) so the same context + seen via multiple file descriptors on a single process collapses to one + entry. + """ + snapshot: dict = {} + + try: + proc_entries = os.listdir("/proc") + except OSError: + return snapshot + + for entry in proc_entries: + if not entry.isdigit(): + continue + + fdinfo_dir = f"/proc/{entry}/fdinfo" + try: + fds = os.listdir(fdinfo_dir) + except (FileNotFoundError, PermissionError, NotADirectoryError, OSError): + continue + + for fd in fds: + try: + with open(f"{fdinfo_dir}/{fd}") as f: + content = f.read() + except (FileNotFoundError, PermissionError, OSError): + continue + + if "drm-driver" not in content: + continue + + fields: dict[str, str] = {} + for line in content.splitlines(): + key, sep, value = line.partition(":") + if sep: + fields[key.strip()] = value.strip() + + driver = fields.get("drm-driver") + if driver not in ("i915", "xe"): + continue + + pdev = fields.get("drm-pdev", "") + if target_pdev and pdev != target_pdev: + continue + + client_id = fields.get("drm-client-id") + if not client_id: + continue + + key = (pdev, client_id, entry) + if key in snapshot: + continue + + engines: dict[str, tuple[int, int]] = {} + + if driver == "i915": + for fkey, engine in _I915_ENGINE_KEYS.items(): + raw = fields.get(fkey) + if not raw: + continue + try: + engines[engine] = (int(raw.split()[0]), 0) + except (ValueError, IndexError): + continue + else: + for suffix, engine in _XE_ENGINE_KEYS.items(): + busy_raw = fields.get(f"drm-cycles-{suffix}") + total_raw = fields.get(f"drm-total-cycles-{suffix}") + if not (busy_raw and total_raw): + continue + try: + engines[engine] = ( + int(busy_raw.split()[0]), + int(total_raw.split()[0]), + ) + except (ValueError, IndexError): + continue + + if not engines: + continue + + snapshot[key] = {"driver": driver, "pid": entry, "engines": engines} + + return snapshot + + +def get_intel_gpu_stats( + intel_gpu_device: str | None, +) -> dict[str, dict[str, Any]] | None: + """Get stats by reading DRM fdinfo files, bucketed per-pdev. + + Each DRM client FD exposes monotonic per-engine busy counters via + /proc//fdinfo/ (i915 since kernel 5.19, Xe since first release). + We sample twice and divide busy-time deltas by wall-clock to derive + utilization. Render/3D and Compute are pooled into "compute"; Video and + VideoEnhance into "dec". Overall "gpu" is the sum of those pools (clamped + to 100%). + + The return value is keyed by the GPU's drm-pdev string so multiple Intel + GPUs in the same system are reported separately. Each entry carries a + "name" populated from OpenVINO (falling back to the pdev) so callers can + surface a real device name in the UI. + """ + from frigate.stats.intel_gpu_info import intel_gpu_name_resolver + + target_pdev = _resolve_intel_gpu_pdev(intel_gpu_device) + + snapshot_a = _read_intel_drm_fdinfo(target_pdev) + if not snapshot_a: + logger.warning( + "Unable to collect Intel GPU stats: no DRM fdinfo entries found" + "%s. Check that /proc is readable and the i915/xe driver is loaded", + f" for pdev {target_pdev}" if target_pdev else "", + ) + return None + + start = time.monotonic() + time.sleep(_INTEL_FDINFO_SAMPLE_SECONDS) + elapsed_ns = (time.monotonic() - start) * 1e9 + + snapshot_b = _read_intel_drm_fdinfo(target_pdev) + if not snapshot_b or elapsed_ns <= 0: + logger.warning( + "Unable to collect Intel GPU stats: second DRM fdinfo sample was empty" + ) + return None + + def _new_engine_pct() -> dict[str, float]: + return {"render": 0.0, "video": 0.0, "video-enhance": 0.0, "compute": 0.0} + + per_pdev_engine_pct: dict[str, dict[str, float]] = {} + per_pdev_pid_pct: dict[str, dict[str, float]] = {} + + for key, data_b in snapshot_b.items(): + data_a = snapshot_a.get(key) + if not data_a or data_a["driver"] != data_b["driver"]: + continue + + pdev = key[0] + engine_pct = per_pdev_engine_pct.setdefault(pdev, _new_engine_pct()) + pid_pct = per_pdev_pid_pct.setdefault(pdev, {}) + + client_total = 0.0 + for engine, (busy_b, total_b) in data_b["engines"].items(): + if engine not in engine_pct: + continue + + busy_a, total_a = data_a["engines"].get(engine, (busy_b, total_b)) + + if data_b["driver"] == "i915": + delta = max(0, busy_b - busy_a) + pct = min(100.0, delta / elapsed_ns * 100.0) + else: + delta_busy = max(0, busy_b - busy_a) + delta_total = total_b - total_a + if delta_total <= 0: + continue + pct = min(100.0, delta_busy / delta_total * 100.0) + + engine_pct[engine] += pct + client_total += pct + + pid_pct[data_b["pid"]] = pid_pct.get(data_b["pid"], 0.0) + client_total + + if not per_pdev_engine_pct: + logger.warning( + "Unable to collect Intel GPU stats: no per-engine counters available " + "(i915 requires kernel >= 5.19)" + ) + return None + + names = intel_gpu_name_resolver.get_names() + results: dict[str, dict[str, Any]] = {} + + for pdev, engine_pct in per_pdev_engine_pct.items(): + for engine in engine_pct: + engine_pct[engine] = min(100.0, engine_pct[engine]) + + compute_pct = min(100.0, engine_pct["render"] + engine_pct["compute"]) + dec_pct = min(100.0, engine_pct["video"] + engine_pct["video-enhance"]) + overall_pct = min(100.0, compute_pct + dec_pct) + + entry: dict[str, Any] = { + "name": names.get(pdev) or "Intel iGPU", + "vendor": "intel", + "gpu": f"{round(overall_pct, 2)}%", + "mem": "-%", + "compute": f"{round(compute_pct, 2)}%", + "dec": f"{round(dec_pct, 2)}%", + } + + pid_pct = per_pdev_pid_pct.get(pdev) + if pid_pct: + entry["clients"] = { + pid: f"{round(min(100.0, pct), 2)}%" for pid, pct in pid_pct.items() + } + + results[pdev] = entry + + return results + + +def get_openvino_npu_stats() -> dict[str, str] | None: + """Get NPU stats using openvino.""" + NPU_RUNTIME_PATH = "/sys/devices/pci0000:00/0000:00:0b.0/power/runtime_active_time" + + try: + with open(NPU_RUNTIME_PATH) as f: + initial_runtime = float(f.read().strip()) + + initial_time = time.time() + + # Sleep for 1 second to get an accurate reading + time.sleep(1.0) + + # Read runtime value again + with open(NPU_RUNTIME_PATH) as f: + current_runtime = float(f.read().strip()) + + current_time = time.time() + + # Calculate usage percentage + runtime_diff = current_runtime - initial_runtime + time_diff = (current_time - initial_time) * 1000.0 # Convert to milliseconds + + if time_diff > 0: + usage = min(100.0, max(0.0, (runtime_diff / time_diff * 100.0))) + else: + usage = 0.0 + + return {"npu": f"{round(usage, 2)}", "mem": "-%"} + except (FileNotFoundError, PermissionError, ValueError): + return None + + +def get_rockchip_gpu_stats() -> dict[str, str | float] | None: + """Get GPU stats using rk.""" + try: + with open("/sys/kernel/debug/rkrga/load") as f: + content = f.read() + except FileNotFoundError: + return None + + load_values = [] + for line in content.splitlines(): + match = re.search(r"load = (\d+)%", line) + if match: + load_values.append(int(match.group(1))) + + if not load_values: + return None + + average_load = f"{round(sum(load_values) / len(load_values), 2)}%" + stats: dict[str, str | float] = {"gpu": average_load, "mem": "-%"} + + try: + with open("/sys/class/thermal/thermal_zone5/temp") as f: + line = f.readline().strip() + stats["temp"] = round(int(line) / 1000, 1) + except (FileNotFoundError, OSError, ValueError): + pass + + return stats + + +def get_rockchip_npu_stats() -> dict[str, float | str] | None: + """Get NPU stats using rk.""" + try: + with open("/sys/kernel/debug/rknpu/load") as f: + npu_output = f.read() + + if "Core0:" in npu_output: + # multi core NPU + core_loads = re.findall(r"Core\d+:\s*(\d+)%", npu_output) + else: + # single core NPU + core_loads = re.findall(r"NPU load:\s+(\d+)%", npu_output) + except FileNotFoundError: + core_loads = None + + if not core_loads: + return None + + percentages = [int(load) for load in core_loads] + mean = round(sum(percentages) / len(percentages), 2) + stats: dict[str, float | str] = {"npu": mean, "mem": "-%"} + + try: + with open("/sys/class/thermal/thermal_zone6/temp") as f: + line = f.readline().strip() + stats["temp"] = round(int(line) / 1000, 1) + except (FileNotFoundError, OSError, ValueError): + pass + + return stats + + +def get_axcl_npu_stats() -> dict[str, str | float] | None: + """Get NPU stats using axcl.""" + # Check if axcl-smi exists + axcl_smi_path = "/usr/bin/axcl/axcl-smi" + if not os.path.exists(axcl_smi_path): + return None + + try: + # Run axcl-smi command to get NPU stats + axcl_command = [axcl_smi_path, "sh", "cat", "/proc/ax_proc/npu/top"] + p = sp.run( + axcl_command, + capture_output=True, + text=True, + ) + + if p.returncode != 0: + pass + else: + utilization = None + + for line in p.stdout.strip().splitlines(): + line = line.strip() + if line.startswith("utilization:"): + match = re.search(r"utilization:(\d+)%", line) + if match: + utilization = float(match.group(1)) + + if utilization is not None: + stats: dict[str, str | float] = {"npu": utilization, "mem": "-%"} + return stats + except Exception: + pass + + return None + + +def try_get_info(f, h, default="N/A", sensor=None): + try: + if h: + if sensor is not None: + v = f(h, sensor) + else: + v = f(h) + else: + v = f() + except nvml.NVMLError_NotSupported: + v = default + return v + + +def get_nvidia_gpu_stats() -> dict[int, dict]: + names: dict[str, int] = {} + results = {} + try: + nvml.nvmlInit() + deviceCount = nvml.nvmlDeviceGetCount() + for i in range(deviceCount): + handle = nvml.nvmlDeviceGetHandleByIndex(i) + gpu_name = nvml.nvmlDeviceGetName(handle) + + # handle case where user has multiple of same GPU + if gpu_name in names: + names[gpu_name] += 1 + gpu_name += f" ({names.get(gpu_name)})" + else: + names[gpu_name] = 1 + + meminfo = try_get_info(nvml.nvmlDeviceGetMemoryInfo, handle) + util = try_get_info(nvml.nvmlDeviceGetUtilizationRates, handle) + enc = try_get_info(nvml.nvmlDeviceGetEncoderUtilization, handle) + dec = try_get_info(nvml.nvmlDeviceGetDecoderUtilization, handle) + temp = try_get_info( + nvml.nvmlDeviceGetTemperature, handle, default=None, sensor=0 + ) + pstate = try_get_info(nvml.nvmlDeviceGetPowerState, handle, default=None) + + if util != "N/A": + gpu_util = util.gpu + else: + gpu_util = 0 + + if meminfo != "N/A": + gpu_mem_util = meminfo.used / meminfo.total * 100 + else: + gpu_mem_util = -1 + + if temp != "N/A" and temp is not None: + temp = float(temp) + else: + temp = None + + if enc != "N/A": + enc_util = enc[0] + else: + enc_util = -1 + + if dec != "N/A": + dec_util = dec[0] + else: + dec_util = -1 + + results[i] = { + "name": gpu_name, + "gpu": gpu_util, + "mem": gpu_mem_util, + "enc": enc_util, + "dec": dec_util, + "pstate": pstate or "unknown", + "temp": temp, + } + except Exception: + pass + finally: + return results + + +def get_jetson_stats() -> dict[int, dict] | None: + results = {} + + try: + results["mem"] = "-" # no discrete gpu memory + + if os.path.exists("/sys/devices/gpu.0/load"): + with open("/sys/devices/gpu.0/load") as f: + gpuload = float(f.readline()) / 10 + results["gpu"] = f"{gpuload}%" + elif os.path.exists("/sys/devices/platform/gpu.0/load"): + with open("/sys/devices/platform/gpu.0/load") as f: + gpuload = float(f.readline()) / 10 + results["gpu"] = f"{gpuload}%" + else: + results["gpu"] = "-" + except Exception: + return None + + return results + + +def get_hailo_temps() -> dict[str, float]: + """Get temperatures for Hailo devices.""" + try: + from hailo_platform import Device + except ModuleNotFoundError: + return {} + + temps = {} + + try: + device_ids = Device.scan() + for i, device_id in enumerate(device_ids): + try: + with Device(device_id) as device: + temp_info = device.control.get_chip_temperature() + + # Get board name and normalise it + identity = device.control.identify() + board_name = None + for line in str(identity).split("\n"): + if line.startswith("Board Name:"): + board_name = ( + line.split(":", 1)[1].strip().lower().replace("-", "") + ) + break + + if not board_name: + board_name = f"hailo{i}" + + # Use indexed name if multiple devices, otherwise just the board name + device_name = ( + f"{board_name}-{i}" if len(device_ids) > 1 else board_name + ) + + # ts1_temperature is also available, but appeared to be the same as ts0 in testing. + temps[device_name] = round(temp_info.ts0_temperature, 1) + except Exception as e: + logger.debug( + f"Failed to get temperature for Hailo device {device_id}: {e}" + ) + continue + except Exception as e: + logger.debug(f"Failed to scan for Hailo devices: {e}") + + return temps + + +def is_go2rtc_arbitrary_exec_allowed() -> bool: + """Read the GO2RTC_ALLOW_ARBITRARY_EXEC override from env, docker + secrets, or the Home Assistant add-on options file.""" + raw: str | None = None + if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ: + raw = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC") + elif ( + os.path.isdir("/run/secrets") + and os.access("/run/secrets", os.R_OK) + and "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.listdir("/run/secrets") + ): + try: + with open("/run/secrets/GO2RTC_ALLOW_ARBITRARY_EXEC") as f: + raw = f.read().strip() + except OSError: + raw = None + elif os.path.isfile("/data/options.json"): + try: + with open("/data/options.json") as f: + options = json.loads(f.read()) + raw = options.get("go2rtc_allow_arbitrary_exec") + except (OSError, json.JSONDecodeError): + raw = None + + return raw is not None and str(raw).lower() in ("true", "1", "yes") + + +def is_restricted_go2rtc_source(stream_source: str) -> bool: + """Check if a stream source is a restricted type (echo, expr, or exec) + and the GO2RTC_ALLOW_ARBITRARY_EXEC override is not set.""" + if not stream_source.strip().startswith(("echo:", "expr:", "exec:")): + return False + return not is_go2rtc_arbitrary_exec_allowed() + + +def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess: + """Run ffprobe on stream.""" + clean_path = escape_special_characters(path) + + # Base entries that are always included + stream_entries = "codec_long_name,width,height,bit_rate,duration,display_aspect_ratio,avg_frame_rate" + + # Additional detailed entries + if detailed: + stream_entries += ",codec_name,profile,level,pix_fmt,channels,sample_rate,channel_layout,r_frame_rate" + format_entries = "format_name,size,bit_rate,duration" + else: + format_entries = None + + def run(rtsp_transport: str | None = None) -> sp.CompletedProcess: + cmd = [ffmpeg.ffprobe_path] + if rtsp_transport: + cmd += ["-rtsp_transport", rtsp_transport] + cmd += [ + "-timeout", + "1000000", + "-print_format", + "json", + "-show_entries", + f"stream={stream_entries}", + ] + if detailed and format_entries: + cmd.extend(["-show_entries", f"format={format_entries}"]) + cmd.extend(["-loglevel", "error", clean_path]) + try: + return sp.run(cmd, capture_output=True, timeout=6) + except sp.TimeoutExpired as e: + logger.info( + "ffprobe timed out while probing %s (transport=%s)", + clean_camera_user_pass(path), + rtsp_transport or "default", + ) + return sp.CompletedProcess( + args=cmd, + returncode=1, + stdout=e.stdout or b"", + stderr=(e.stderr or b"") + b"\nffprobe timed out", + ) + + result = run() + + # For RTSP: retry with explicit TCP transport if the first attempt failed + # (default UDP may be blocked) + if result.returncode != 0 and clean_path.startswith("rtsp://"): + result = run(rtsp_transport="tcp") + + return result + + +KEYFRAME_PROBE_WINDOW_SECONDS = 20 +KEYFRAME_GAP_WARNING_SECONDS = 4.0 + + +def parse_keyframe_packets(output: str) -> tuple[list[float], float | None]: + """Parse ffprobe CSV `pts_time,flags` output. + + Returns the presentation timestamps of keyframes (flags containing "K") + and the maximum timestamp observed across all packets. + """ + keyframe_pts: list[float] = [] + max_pts: float | None = None + + for line in output.splitlines(): + parts = line.split(",") + if len(parts) < 2: + continue + try: + pts = float(parts[0]) + except ValueError: + continue + if max_pts is None or pts > max_pts: + max_pts = pts + if "K" in parts[1]: + keyframe_pts.append(pts) + + return keyframe_pts, max_pts + + +def classify_keyframe_gaps( + keyframe_pts: list[float], segment_time: int +) -> dict[str, Any]: + """Classify keyframe spacing for recording suitability. + + A camera using a smart/+ codec or a long/variable GOP produces large or + irregular gaps between keyframes, which breaks time-based recording + segmentation. Severity: + - "unknown" when fewer than two keyframes were observed + - "error" when the longest gap exceeds the record segment length + - "warning" when the longest gap exceeds the warning threshold + - "ok" otherwise + """ + thresholds = { + "warning": KEYFRAME_GAP_WARNING_SECONDS, + "error": segment_time, + } + + if len(keyframe_pts) < 2: + return { + "keyframe_count": len(keyframe_pts), + "max_gap": None, + "mean_gap": None, + "min_gap": None, + "segment_time": segment_time, + "severity": "unknown", + "thresholds": thresholds, + } + + gaps = [b - a for a, b in zip(keyframe_pts, keyframe_pts[1:])] + max_gap = max(gaps) + + if max_gap > segment_time: + severity = "error" + elif max_gap > KEYFRAME_GAP_WARNING_SECONDS: + severity = "warning" + else: + severity = "ok" + + return { + "keyframe_count": len(keyframe_pts), + "max_gap": round(max_gap, 2), + "mean_gap": round(sum(gaps) / len(gaps), 2), + "min_gap": round(min(gaps), 2), + "segment_time": segment_time, + "severity": severity, + "thresholds": thresholds, + } + + +async def analyze_record_keyframes( + ffmpeg, url: str, segment_time: int, window: int = KEYFRAME_PROBE_WINDOW_SECONDS +) -> dict[str, Any]: + """Probe a stream for ~`window` seconds and classify its keyframe spacing. + + Reads video packet flags via ffprobe to find keyframes, then measures the + gaps between them. On timeout or failure returns an "unknown" result rather + than a false all-clear. + """ + clean_url = escape_special_characters(url) + cmd = [ + ffmpeg.ffprobe_path, + "-v", + "error", + "-select_streams", + "v:0", + "-read_intervals", + f"%+{window}", + "-show_entries", + "packet=pts_time,flags", + "-of", + "csv=p=0", + clean_url, + ] + + try: + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=window + 15) + except TimeoutError: + logger.warning("Keyframe probe timed out for record stream") + proc.kill() + return classify_keyframe_gaps([], segment_time) + except OSError as err: + logger.error("Keyframe probe failed: %s", err) + return classify_keyframe_gaps([], segment_time) + + keyframe_pts, max_pts = parse_keyframe_packets(stdout.decode("utf-8", "replace")) + result = classify_keyframe_gaps(keyframe_pts, segment_time) + result["duration_observed"] = round(max_pts, 2) if max_pts is not None else None + return result + + +def vainfo_hwaccel(device_name: str | None = None) -> sp.CompletedProcess: + """Run vainfo.""" + if not device_name: + cmd = ["vainfo"] + else: + if os.path.isabs(device_name) and device_name.startswith("/dev/dri/"): + device_path = device_name + else: + device_path = f"/dev/dri/{device_name}" + + cmd = ["vainfo", "--display", "drm", "--device", device_path] + + return sp.run(cmd, capture_output=True) + + +def get_nvidia_driver_info() -> dict[str, Any]: + """Get general hardware info for nvidia GPU.""" + results = {} + try: + nvml.nvmlInit() + deviceCount = nvml.nvmlDeviceGetCount() + for i in range(deviceCount): + handle = nvml.nvmlDeviceGetHandleByIndex(i) + driver = try_get_info(nvml.nvmlSystemGetDriverVersion, None, default=None) + cuda_compute = try_get_info( + nvml.nvmlDeviceGetCudaComputeCapability, handle, default=None + ) + vbios = try_get_info(nvml.nvmlDeviceGetVbiosVersion, handle, default=None) + results[i] = { + "name": nvml.nvmlDeviceGetName(handle), + "driver": driver or "unknown", + "cuda_compute": cuda_compute or "unknown", + "vbios": vbios or "unknown", + } + except Exception: + pass + finally: + return results + + +def auto_detect_hwaccel() -> str: + """Detect hwaccel args by default.""" + try: + cuda = False + vaapi = False + resp = requests.get("http://127.0.0.1:1984/api/ffmpeg/hardware", timeout=3) + + if resp.status_code == 200: + data: dict[str, list[dict[str, str]]] = resp.json() + for source in data.get("sources", []): + if "cuda" in source.get("url", "") and source.get("name") == "OK": + cuda = True + + if "vaapi" in source.get("url", "") and source.get("name") == "OK": + vaapi = True + except requests.RequestException: + pass + + if cuda: + logger.info("Automatically detected nvidia hwaccel for video decoding") + return FFMPEG_HWACCEL_NVIDIA + + if vaapi: + logger.info("Automatically detected vaapi hwaccel for video decoding") + return FFMPEG_HWACCEL_VAAPI + + logger.warning( + "Did not detect hwaccel, using a GPU for accelerated video decoding is highly recommended" + ) + return "" + + +async def get_video_properties( + ffmpeg, url: str, get_duration: bool = False +) -> dict[str, Any]: + async def probe_with_ffprobe( + url: str, + rtsp_transport: str | None = None, + ) -> tuple[bool, int, int, str | None, float]: + """Fallback using ffprobe: returns (valid, width, height, codec, duration).""" + cmd = [ffmpeg.ffprobe_path] + if rtsp_transport: + cmd += ["-rtsp_transport", rtsp_transport] + cmd += [ + "-rw_timeout", + "5000000", + "-v", + "quiet", + "-print_format", + "json", + "-show_format", + "-show_streams", + url, + ] + proc = None + try: + proc = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + try: + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=6) + except TimeoutError: + logger.info( + "ffprobe timed out while probing %s (transport=%s)", + clean_camera_user_pass(url), + rtsp_transport or "default", + ) + proc.kill() + await proc.wait() + return False, 0, 0, None, -1 + + if proc.returncode != 0: + return False, 0, 0, None, -1 + + data = json.loads(stdout.decode()) + video_streams = [ + s for s in data.get("streams", []) if s.get("codec_type") == "video" + ] + if not video_streams: + return False, 0, 0, None, -1 + + v = video_streams[0] + width = int(v.get("width", 0)) + height = int(v.get("height", 0)) + codec = v.get("codec_name") + + duration_str = data.get("format", {}).get("duration") + duration = float(duration_str) if duration_str else -1.0 + + return True, width, height, codec, duration + except (json.JSONDecodeError, ValueError, KeyError, sp.SubprocessError): + return False, 0, 0, None, -1 + + def probe_with_cv2(url: str) -> tuple[bool, int, int, str | None, float]: + """Primary attempt using cv2: returns (valid, width, height, fourcc, duration).""" + cap = cv2.VideoCapture(url) + if not cap.isOpened(): + cap.release() + return False, 0, 0, None, -1 + + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + valid = width > 0 and height > 0 + fourcc = None + duration = -1.0 + + if valid: + fourcc_int = int(cap.get(cv2.CAP_PROP_FOURCC)) + fourcc = fourcc_int.to_bytes(4, "little").decode("latin-1").strip() + + if get_duration: + fps = cap.get(cv2.CAP_PROP_FPS) + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + if fps > 0 and total_frames > 0: + duration = total_frames / fps + + cap.release() + return valid, width, height, fourcc, duration + + is_rtsp = url.startswith("rtsp://") + + if is_rtsp: + # skip cv2 for RTSP: its FFmpeg backend has a hardcoded ~30s internal + # timeout that cannot be shortened per-call, and ffprobe bounded by + # -rw_timeout handles RTSP probing reliably + has_video, width, height, fourcc, duration = await probe_with_ffprobe(url) + else: + # try cv2 first for local files, HTTP, RTMP + has_video, width, height, fourcc, duration = probe_with_cv2(url) + + # fallback to ffprobe if needed + if not has_video or (get_duration and duration < 0): + has_video, width, height, fourcc, duration = await probe_with_ffprobe(url) + + # last resort for RTSP: try TCP transport, since default UDP may be blocked + if (not has_video or (get_duration and duration < 0)) and is_rtsp: + has_video, width, height, fourcc, duration = await probe_with_ffprobe( + url, rtsp_transport="tcp" + ) + + result: dict[str, Any] = {"has_valid_video": has_video} + if has_video: + result.update({"width": width, "height": height}) + if fourcc: + result["fourcc"] = fourcc + if get_duration: + result["duration"] = duration + + return result + + +def process_logs( + contents: str, + service: str | None = None, + start: int | None = None, + end: int | None = None, +) -> tuple[int, list[str]]: + log_lines = [] + last_message = None + last_timestamp = None + repeat_count = 0 + + for raw_line in contents.splitlines(): + clean_line = raw_line.strip() + + if len(clean_line) < 10: + continue + + # Handle cases where S6 does not include date in log line + if " " not in clean_line: + clean_line = f"{datetime.now()} {clean_line}" + + try: + # Find the position of the first double space to extract timestamp and message + date_end = clean_line.index(" ") + timestamp = clean_line[:date_end] + full_message = clean_line[date_end:].strip() + + # For frigate, remove the date part from message comparison + if service == "frigate": + # Skip the date at the start of the message if it exists + date_parts = full_message.split("]", 1) + if len(date_parts) > 1: + message_part = date_parts[1].strip() + else: + message_part = full_message + else: + message_part = full_message + + if message_part == last_message: + repeat_count += 1 + continue + else: + if repeat_count > 0: + # Insert a deduplication message formatted the same way as logs + dedup_message = f"{last_timestamp} [LOGGING] Last message repeated {repeat_count} times" + log_lines.append(dedup_message) + repeat_count = 0 + + log_lines.append(clean_line) + last_timestamp = timestamp + + last_message = message_part + + except ValueError: + # If we can't parse the line properly, just add it as is + log_lines.append(clean_line) + continue + + # If there were repeated messages at the end, log the count + if repeat_count > 0: + dedup_message = ( + f"{last_timestamp} [LOGGING] Last message repeated {repeat_count} times" + ) + log_lines.append(dedup_message) + + return len(log_lines), log_lines[start:end] + + +def set_file_limit() -> None: + # Newer versions of containerd 2.X+ impose a very low soft file limit of 1024 + # This applies to OSs like HA OS (see https://github.com/home-assistant/operating-system/issues/4110) + # Attempt to increase this limit + soft_limit = int(os.getenv("SOFT_FILE_LIMIT", "65536") or "65536") + + current_soft, current_hard = resource.getrlimit(resource.RLIMIT_NOFILE) + logger.debug(f"Current file limits - Soft: {current_soft}, Hard: {current_hard}") + + new_soft = min(soft_limit, current_hard) + resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, current_hard)) + logger.debug( + f"File limit set. New soft limit: {new_soft}, Hard limit remains: {current_hard}" + ) + + +def get_fs_type(path: str) -> str: + bestMatch = "" + fsType = "" + for part in psutil.disk_partitions(all=True): + if path.startswith(part.mountpoint) and len(bestMatch) < len(part.mountpoint): + fsType = part.fstype + bestMatch = part.mountpoint + return fsType + + +def calculate_shm_requirements(config) -> dict: + try: + storage_stats = shutil.disk_usage("/dev/shm") + except (FileNotFoundError, OSError): + return {} + + total_mb = round(storage_stats.total / pow(2, 20), 1) + used_mb = round(storage_stats.used / pow(2, 20), 1) + free_mb = round(storage_stats.free / pow(2, 20), 1) + + # required for log files + nginx cache + min_req_shm = 40 + 10 + + if config.birdseye.restream: + min_req_shm += 8 + + available_shm = total_mb - min_req_shm + cam_total_frame_size = 0.0 + + for camera in config.cameras.values(): + if camera.enabled_in_config and camera.detect.width and camera.detect.height: + cam_total_frame_size += round( + (camera.detect.width * camera.detect.height * 1.5 + 270480) / 1048576, + 1, + ) + + # leave room for 2 cameras that are added dynamically, if a user wants to add more cameras they may need to increase the SHM size and restart after adding them. + cam_total_frame_size += 2 * round( + (1280 * 720 * 1.5 + 270480) / 1048576, + 1, + ) + + shm_frame_count = min( + int(os.environ.get(SHM_FRAMES_VAR, "50")), + int(available_shm / cam_total_frame_size), + ) + + # minimum required shm recommendation + min_shm = round(min_req_shm + cam_total_frame_size * 20) + + return { + "total": total_mb, + "used": used_mb, + "free": free_mb, + "mount_type": get_fs_type("/dev/shm"), + "available": round(available_shm, 1), + "camera_frame_size": cam_total_frame_size, + "shm_frame_count": shm_frame_count, + "min_shm": min_shm, + } diff --git a/frigate/util/time.py b/frigate/util/time.py new file mode 100644 index 0000000..861bec1 --- /dev/null +++ b/frigate/util/time.py @@ -0,0 +1,93 @@ +"""Time utilities.""" + +import datetime +import logging +from zoneinfo import ZoneInfoNotFoundError + +import pytz +from tzlocal import get_localzone + +logger = logging.getLogger(__name__) + + +def get_tz_modifiers(tz_name: str) -> tuple[str, str, float]: + seconds_offset = ( + datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds() + ) + hours_offset = int(seconds_offset / 60 / 60) + minutes_offset = int(seconds_offset / 60 - hours_offset * 60) + hour_modifier = f"{hours_offset} hour" + minute_modifier = f"{minutes_offset} minute" + return hour_modifier, minute_modifier, seconds_offset + + +def get_tomorrow_at_time(hour: int) -> datetime.datetime: + """Returns the datetime of the following day at 2am.""" + try: + tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1) + except ZoneInfoNotFoundError: + tomorrow = datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=1) + logger.warning( + "Using utc for maintenance due to missing or incorrect timezone set" + ) + + return tomorrow.replace(hour=hour, minute=0, second=0).astimezone(datetime.UTC) + + +def is_current_hour(timestamp: int) -> bool: + """Returns if timestamp is in the current UTC hour.""" + start_of_next_hour = ( + datetime.datetime.now(datetime.UTC).replace(minute=0, second=0, microsecond=0) + + datetime.timedelta(hours=1) + ).timestamp() + return timestamp < start_of_next_hour + + +def get_dst_transitions( + tz_name: str, start_time: float, end_time: float +) -> list[tuple[float, float]]: + """ + Find DST transition points and return time periods with consistent offsets. + + Args: + tz_name: Timezone name (e.g., 'America/New_York') + start_time: Start timestamp (UTC) + end_time: End timestamp (UTC) + + Returns: + List of (period_start, period_end, seconds_offset) tuples representing + continuous periods with the same UTC offset + """ + try: + tz = pytz.timezone(tz_name) + except pytz.UnknownTimeZoneError: + # If timezone is invalid, return single period with no offset + return [(start_time, end_time, 0)] + + periods = [] + current = start_time + + # Get initial offset + dt = datetime.datetime.utcfromtimestamp(current).replace(tzinfo=pytz.UTC) + local_dt = dt.astimezone(tz) + prev_offset = local_dt.utcoffset().total_seconds() + period_start = start_time + + # Check each day for offset changes + while current <= end_time: + dt = datetime.datetime.utcfromtimestamp(current).replace(tzinfo=pytz.UTC) + local_dt = dt.astimezone(tz) + current_offset = local_dt.utcoffset().total_seconds() + + if current_offset != prev_offset: + # Found a transition - close previous period + periods.append((period_start, current, prev_offset)) + period_start = current + prev_offset = current_offset + + current += 86400 # Check daily + + # Add final period + periods.append((period_start, end_time, prev_offset)) + + return periods diff --git a/frigate/util/velocity.py b/frigate/util/velocity.py new file mode 100644 index 0000000..61f1a0d --- /dev/null +++ b/frigate/util/velocity.py @@ -0,0 +1,132 @@ +import math + +import numpy as np + + +def order_points_clockwise(points): + """ + Ensure points are sorted in clockwise order starting from the top left + + :param points: Array of zone corner points in pixel coordinates + :return: Ordered list of points + """ + top_left = min( + points, key=lambda p: (p[1], p[0]) + ) # Find the top-left point (min y, then x) + + # Remove the top-left point from the list of points + remaining_points = [p for p in points if not np.array_equal(p, top_left)] + + # Sort the remaining points based on the angle relative to the top-left point + def angle_from_top_left(point): + x, y = point[0] - top_left[0], point[1] - top_left[1] + return math.atan2(y, x) + + sorted_points = sorted(remaining_points, key=angle_from_top_left) + + return [top_left] + sorted_points + + +def create_ground_plane(zone_points, distances): + """ + Create a ground plane that accounts for perspective distortion using real-world dimensions for each side of the zone. + + :param zone_points: Array of zone corner points in pixel coordinates + [[x1, y1], [x2, y2], [x3, y3], [x4, y4]] + :param distances: Real-world dimensions ordered by A, B, C, D + :return: Function that calculates real-world distance per pixel at any coordinate + """ + A, B, C, D = zone_points + + # Calculate pixel lengths of each side + AB_px = np.linalg.norm(np.array(B) - np.array(A)) + BC_px = np.linalg.norm(np.array(C) - np.array(B)) + CD_px = np.linalg.norm(np.array(D) - np.array(C)) + DA_px = np.linalg.norm(np.array(A) - np.array(D)) + + AB, BC, CD, DA = map(float, distances) + + AB_scale = AB / AB_px + BC_scale = BC / BC_px + CD_scale = CD / CD_px + DA_scale = DA / DA_px + + def distance_per_pixel(x, y): + """ + Calculate the real-world distance per pixel at a given (x, y) coordinate. + + :param x: X-coordinate in the image + :param y: Y-coordinate in the image + :return: Real-world distance per pixel at the given (x, y) coordinate + """ + + # Return 0 if divide by zero would occur + if (B[0] - A[0]) == 0 or (D[1] - A[1]) == 0: + return 0 + + # Normalize x and y within the zone + x_norm = (x - A[0]) / (B[0] - A[0]) + y_norm = (y - A[1]) / (D[1] - A[1]) + + # Interpolate scales horizontally and vertically + vertical_scale = AB_scale + (CD_scale - AB_scale) * y_norm + horizontal_scale = DA_scale + (BC_scale - DA_scale) * x_norm + + # Combine horizontal and vertical scales + return (vertical_scale + horizontal_scale) / 2 + + return distance_per_pixel + + +def calculate_real_world_speed( + zone_contour, + distances, + velocity_pixels, + position, + camera_fps, +): + """ + Calculate the real-world speed of a tracked object, accounting for perspective, + directly from the zone string. + + :param zone_contour: Array of absolute zone points + :param distances: List of distances of each side, ordered by A, B, C, D + :param velocity_pixels: List of tuples representing velocity in pixels/frame + :param position: Current position of the object (x, y) in pixels + :param camera_fps: Frames per second of the camera + :return: speed and velocity angle direction + """ + # order the zone_contour points clockwise starting at top left + ordered_zone_contour = order_points_clockwise(zone_contour) + + # find the indices that would sort the original zone_contour to match ordered_zone_contour + sort_indices = [ + np.where((zone_contour == point).all(axis=1))[0][0] + for point in ordered_zone_contour + ] + + # Reorder distances to match the new order of zone_contour + distances = np.array(distances) + ordered_distances = distances[sort_indices] + + ground_plane = create_ground_plane(ordered_zone_contour, ordered_distances) + + if not isinstance(velocity_pixels, np.ndarray): + velocity_pixels = np.array(velocity_pixels) + + avg_velocity_pixels = velocity_pixels.mean(axis=0) + + # get the real-world distance per pixel at the object's current position and calculate real speed + scale = ground_plane(position[0], position[1]) + speed_real = avg_velocity_pixels * scale * camera_fps + + # euclidean speed in real-world units/second + speed_magnitude = np.linalg.norm(speed_real) + + # movement direction + dx, dy = avg_velocity_pixels + angle = math.degrees(math.atan2(dy, dx)) + if angle < 0: + angle += 360 + + return speed_magnitude, angle diff --git a/frigate/video/__init__.py b/frigate/video/__init__.py new file mode 100644 index 0000000..2458983 --- /dev/null +++ b/frigate/video/__init__.py @@ -0,0 +1,2 @@ +from .detect import * # noqa: F403 +from .ffmpeg import * # noqa: F403 diff --git a/frigate/video/detect.py b/frigate/video/detect.py new file mode 100644 index 0000000..8c5a31e --- /dev/null +++ b/frigate/video/detect.py @@ -0,0 +1,561 @@ +"""Manages camera object detection processes.""" + +import logging +import queue +import time +from datetime import UTC, datetime +from multiprocessing import Queue +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import cv2 + +from frigate.camera import CameraMetrics, PTZMetrics +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import CameraConfig, DetectConfig, LoggerConfig, ModelConfig +from frigate.config.camera.camera import CameraTypeEnum +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + PROCESS_PRIORITY_HIGH, + REQUEST_REGION_GRID, +) +from frigate.motion import MotionDetector +from frigate.motion.improved_motion import ImprovedMotionDetector +from frigate.object_detection.base import RemoteObjectDetector +from frigate.ptz.autotrack import ptz_moving_at_frame_time +from frigate.track import ObjectTracker +from frigate.track.norfair_tracker import NorfairTracker +from frigate.track.tracked_object import TrackedObjectAttribute +from frigate.util.builtin import EventsPerSecond +from frigate.util.image import ( + FrameManager, + SharedMemoryFrameManager, + draw_box_with_label, +) +from frigate.util.object import ( + create_tensor_input, + get_cluster_candidates, + get_cluster_region, + get_cluster_region_from_grid, + get_min_region_size, + get_startup_regions, + inside_any, + intersects_any, + is_object_filtered, + reduce_detections, +) +from frigate.util.process import FrigateProcess +from frigate.util.time import get_tomorrow_at_time + +logger = logging.getLogger(__name__) + + +class CameraTracker(FrigateProcess): + def __init__( + self, + config: CameraConfig, + model_config: ModelConfig, + labelmap: dict[int, str], + detection_queue: Queue, + detected_objects_queue, + camera_metrics: CameraMetrics, + ptz_metrics: PTZMetrics, + region_grid: list[list[dict[str, Any]]], + stop_event: MpEvent, + log_config: LoggerConfig | None = None, + ) -> None: + super().__init__( + stop_event, + PROCESS_PRIORITY_HIGH, + name=f"frigate.process:{config.name}", + daemon=True, + ) + self.config = config + self.model_config = model_config + self.labelmap = labelmap + self.detection_queue = detection_queue + self.detected_objects_queue = detected_objects_queue + self.camera_metrics = camera_metrics + self.ptz_metrics = ptz_metrics + self.region_grid = region_grid + self.log_config = log_config + + def run(self) -> None: + self.pre_run_setup(self.log_config) + frame_queue = self.camera_metrics.frame_queue + frame_shape = self.config.frame_shape + + motion_detector = ImprovedMotionDetector( + frame_shape, + self.config.motion, + self.config.detect.fps, + name=self.config.name, + ptz_metrics=self.ptz_metrics, + ) + object_detector = RemoteObjectDetector( + self.config.name, + self.labelmap, + self.detection_queue, + self.model_config, + self.stop_event, + ) + + object_tracker = NorfairTracker(self.config, self.ptz_metrics) + + frame_manager = SharedMemoryFrameManager() + + # create communication for region grid updates + requestor = InterProcessRequestor() + + process_frames( + requestor, + frame_queue, + frame_shape, + self.model_config, + self.config, + frame_manager, + motion_detector, + object_detector, + object_tracker, + self.detected_objects_queue, + self.camera_metrics, + self.stop_event, + self.ptz_metrics, + self.region_grid, + ) + + # empty the frame queue + logger.info(f"{self.config.name}: emptying frame queue") + while not frame_queue.empty(): + (frame_name, _) = frame_queue.get(False) + frame_manager.delete(frame_name) + + logger.info(f"{self.config.name}: exiting subprocess") + + +def detect( + detect_config: DetectConfig, + object_detector, + frame, + model_config: ModelConfig, + region, + objects_to_track, + object_filters, +): + tensor_input = create_tensor_input(frame, model_config, region) + + detections = [] + region_detections = object_detector.detect(tensor_input) + for d in region_detections: + box = d[2] + size = region[2] - region[0] + x_min = int(max(0, (box[1] * size) + region[0])) + y_min = int(max(0, (box[0] * size) + region[1])) + x_max = int(min(detect_config.width - 1, (box[3] * size) + region[0])) + y_max = int(min(detect_config.height - 1, (box[2] * size) + region[1])) + + # ignore objects that were detected outside the frame + if (x_min >= detect_config.width - 1) or (y_min >= detect_config.height - 1): + continue + + width = x_max - x_min + height = y_max - y_min + area = width * height + ratio = width / max(1, height) + det = (d[0], d[1], (x_min, y_min, x_max, y_max), area, ratio, region) + # apply object filters + if is_object_filtered(det, objects_to_track, object_filters): + continue + detections.append(det) + return detections + + +def process_frames( + requestor: InterProcessRequestor, + frame_queue: Queue, + frame_shape: tuple[int, int], + model_config: ModelConfig, + camera_config: CameraConfig, + frame_manager: FrameManager, + motion_detector: MotionDetector, + object_detector: RemoteObjectDetector, + object_tracker: ObjectTracker, + detected_objects_queue: Queue, + camera_metrics: CameraMetrics, + stop_event: MpEvent, + ptz_metrics: PTZMetrics, + region_grid: list[list[dict[str, Any]]], + exit_on_empty: bool = False, +): + next_region_update = get_tomorrow_at_time(2) + config_subscriber = CameraConfigUpdateSubscriber( + None, + {camera_config.name: camera_config}, + [ + CameraConfigUpdateEnum.detect, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.motion, + CameraConfigUpdateEnum.objects, + ], + ) + + fps_tracker = EventsPerSecond() + fps_tracker.start() + + startup_scan = True + stationary_frame_counter = 0 + camera_enabled = True + + region_min_size = get_min_region_size(model_config) + + attributes_map = model_config.attributes_map + all_attributes = model_config.all_attributes + + # remove license_plate from attributes if this camera is a dedicated LPR cam + if camera_config.type == CameraTypeEnum.lpr: + modified_attributes_map = model_config.attributes_map.copy() + + if ( + "car" in modified_attributes_map + and "license_plate" in modified_attributes_map["car"] + ): + modified_attributes_map["car"] = [ + attr + for attr in modified_attributes_map["car"] + if attr != "license_plate" + ] + + attributes_map = modified_attributes_map + + all_attributes = [ + attr for attr in model_config.all_attributes if attr != "license_plate" + ] + + while not stop_event.is_set(): + updated_configs = config_subscriber.check_for_updates() + + if "enabled" in updated_configs: + prev_enabled = camera_enabled + camera_enabled = camera_config.enabled + + if "motion" in updated_configs: + motion_detector.config = camera_config.motion + motion_detector.update_mask() + + if ( + not camera_enabled + and prev_enabled != camera_enabled + and camera_metrics.frame_queue.empty() + ): + logger.debug( + f"Camera {camera_config.name} disabled, clearing tracked objects" + ) + prev_enabled = camera_enabled + + # Clear norfair's dictionaries + object_tracker.tracked_objects.clear() + object_tracker.disappeared.clear() + object_tracker.stationary_box_history.clear() + object_tracker.positions.clear() + object_tracker.track_id_map.clear() + + # Clear internal norfair states + for trackers_by_type in object_tracker.trackers.values(): + for tracker in trackers_by_type.values(): + tracker.tracked_objects = [] + for tracker in object_tracker.default_tracker.values(): + tracker.tracked_objects = [] + + if not camera_enabled: + time.sleep(0.1) + continue + + if datetime.now().astimezone(UTC) > next_region_update: + region_grid = requestor.send_data(REQUEST_REGION_GRID, camera_config.name) + next_region_update = get_tomorrow_at_time(2) + + try: + if exit_on_empty: + frame_name, frame_time = frame_queue.get(False) + else: + frame_name, frame_time = frame_queue.get(True, 1) + except queue.Empty: + if exit_on_empty: + logger.info("Exiting track_objects...") + break + continue + + camera_metrics.detection_frame.value = frame_time + ptz_metrics.frame_time.value = frame_time + + frame = frame_manager.get(frame_name, (frame_shape[0] * 3 // 2, frame_shape[1])) + + if frame is None: + logger.debug( + f"{camera_config.name}: frame {frame_time} is not in memory store." + ) + continue + + # look for motion if enabled + motion_boxes = motion_detector.detect(frame) + + regions = [] + consolidated_detections = [] + + # if detection is disabled + if not camera_config.detect.enabled: + object_tracker.match_and_update(frame_name, frame_time, []) + else: + # get stationary object ids + # check every Nth frame for stationary objects + # disappeared objects are not stationary + # also check for overlapping motion boxes + if stationary_frame_counter == camera_config.detect.stationary.interval: + stationary_frame_counter = 0 + stationary_object_ids = [] + else: + stationary_frame_counter += 1 + stationary_object_ids = [ + obj["id"] + for obj in object_tracker.tracked_objects.values() + # if it has exceeded the stationary threshold + if obj["motionless_count"] + >= camera_config.detect.stationary.threshold + # and it hasn't disappeared + and object_tracker.disappeared[obj["id"]] == 0 + # and it doesn't overlap with any current motion boxes when not calibrating + and not intersects_any( + obj["box"], + [] if motion_detector.is_calibrating() else motion_boxes, + ) + ] + + # get tracked object boxes that aren't stationary + tracked_object_boxes = [ + ( + # use existing object box for stationary objects + obj["estimate"] + if obj["motionless_count"] + < camera_config.detect.stationary.threshold + else obj["box"] + ) + for obj in object_tracker.tracked_objects.values() + if obj["id"] not in stationary_object_ids + ] + object_boxes = tracked_object_boxes + object_tracker.untracked_object_boxes + + # get consolidated regions for tracked objects + regions = [ + get_cluster_region( + frame_shape, region_min_size, candidate, object_boxes + ) + for candidate in get_cluster_candidates( + frame_shape, region_min_size, object_boxes + ) + ] + + # only add in the motion boxes when not calibrating and a ptz is not moving via autotracking + # ptz_moving_at_frame_time() always returns False for non-autotracking cameras + if not motion_detector.is_calibrating() and not ptz_moving_at_frame_time( + frame_time, + ptz_metrics.start_time.value, + ptz_metrics.stop_time.value, + ): + # find motion boxes that are not inside tracked object regions + standalone_motion_boxes = [ + b for b in motion_boxes if not inside_any(b, regions) + ] + + if standalone_motion_boxes: + motion_clusters = get_cluster_candidates( + frame_shape, + region_min_size, + standalone_motion_boxes, + ) + motion_regions = [ + get_cluster_region_from_grid( + frame_shape, + region_min_size, + candidate, + standalone_motion_boxes, + region_grid, + ) + for candidate in motion_clusters + ] + regions += motion_regions + + # if starting up, get the next startup scan region + if startup_scan: + for region in get_startup_regions( + frame_shape, region_min_size, region_grid + ): + regions.append(region) + startup_scan = False + + # resize regions and detect + # seed with stationary objects + detections = [ + ( + obj["label"], + obj["score"], + obj["box"], + obj["area"], + obj["ratio"], + obj["region"], + ) + for obj in object_tracker.tracked_objects.values() + if obj["id"] in stationary_object_ids + ] + + for region in regions: + detections.extend( + detect( + camera_config.detect, + object_detector, + frame, + model_config, + region, + camera_config.objects.track, + camera_config.objects.filters, + ) + ) + + consolidated_detections = reduce_detections(frame_shape, detections) + + # if detection was run on this frame, consolidate + if len(regions) > 0: + tracked_detections = [ + d for d in consolidated_detections if d[0] not in all_attributes + ] + # now that we have refined our detections, we need to track objects + object_tracker.match_and_update( + frame_name, frame_time, tracked_detections + ) + # else, just update the frame times for the stationary objects + else: + object_tracker.update_frame_times(frame_name, frame_time) + + # build detections + detections = {} + for obj in object_tracker.tracked_objects.values(): + detections[obj["id"]] = {**obj, "attributes": []} + + # assign each detected attribute to the best matching object. + # iterate consolidated_detections once so attributes that appear under + # multiple parent labels in attributes_map (e.g. license_plate is in + # both "car" and "motorcycle") are not appended more than once + all_objects: list[dict[str, Any]] = object_tracker.tracked_objects.values() + detected_attributes = [ + TrackedObjectAttribute(d) + for d in consolidated_detections + if d[0] in all_attributes + ] + for attribute in detected_attributes: + filtered_objects = filter( + lambda o: attribute.label in attributes_map.get(o["label"], []), + all_objects, + ) + selected_object_id = attribute.find_best_object(filtered_objects) + + if selected_object_id is not None: + detections[selected_object_id]["attributes"].append( + attribute.get_tracking_data() + ) + + # debug object tracking + if False: + bgr_frame = cv2.cvtColor( + frame, + cv2.COLOR_YUV2BGR_I420, + ) + object_tracker.debug_draw(bgr_frame, frame_time) + cv2.imwrite( + f"debug/frames/track-{'{:.6f}'.format(frame_time)}.jpg", bgr_frame + ) + # debug + if False: + bgr_frame = cv2.cvtColor( + frame, + cv2.COLOR_YUV2BGR_I420, + ) + + for m_box in motion_boxes: + cv2.rectangle( + bgr_frame, + (m_box[0], m_box[1]), + (m_box[2], m_box[3]), + (0, 0, 255), + 2, + ) + + for b in tracked_object_boxes: + cv2.rectangle( + bgr_frame, + (b[0], b[1]), + (b[2], b[3]), + (255, 0, 0), + 2, + ) + + for obj in object_tracker.tracked_objects.values(): + if obj["frame_time"] == frame_time: + thickness = 2 + color = model_config.colormap.get(obj["label"], (255, 255, 255)) + else: + thickness = 1 + color = (255, 0, 0) + + # draw the bounding boxes on the frame + box = obj["box"] + + draw_box_with_label( + bgr_frame, + box[0], + box[1], + box[2], + box[3], + obj["label"], + obj["id"], + thickness=thickness, + color=color, + ) + + for region in regions: + cv2.rectangle( + bgr_frame, + (region[0], region[1]), + (region[2], region[3]), + (0, 255, 0), + 2, + ) + + cv2.imwrite( + f"debug/frames/{camera_config.name}-{'{:.6f}'.format(frame_time)}.jpg", + bgr_frame, + ) + # add to the queue if not full + if detected_objects_queue.full(): + frame_manager.close(frame_name) + continue + else: + fps_tracker.update() + camera_metrics.process_fps.value = fps_tracker.eps() + detected_objects_queue.put( + ( + camera_config.name, + frame_name, + frame_time, + detections, + motion_boxes, + regions, + ) + ) + camera_metrics.detection_fps.value = object_detector.fps.eps() + frame_manager.close(frame_name) + + motion_detector.stop() + requestor.stop() + config_subscriber.stop() diff --git a/frigate/video/ffmpeg.py b/frigate/video/ffmpeg.py new file mode 100644 index 0000000..24b7805 --- /dev/null +++ b/frigate/video/ffmpeg.py @@ -0,0 +1,671 @@ +"""Manages ffmpeg processes for camera frame capture.""" + +import logging +import queue +import subprocess as sp +import threading +import time +from collections import deque +from datetime import UTC, datetime, timedelta +from multiprocessing import Queue, Value +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +from frigate.camera import CameraMetrics +from frigate.comms.inter_process import InterProcessRequestor +from frigate.comms.recordings_updater import ( + RecordingsDataSubscriber, + RecordingsDataTypeEnum, +) +from frigate.config import CameraConfig, LoggerConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import PROCESS_PRIORITY_HIGH +from frigate.log import LogPipe +from frigate.util.builtin import EventsPerSecond, get_record_segment_time +from frigate.util.ffmpeg import start_or_restart_ffmpeg, stop_ffmpeg +from frigate.util.image import ( + FrameManager, + SharedMemoryFrameManager, +) +from frigate.util.process import FrigateProcess + +logger = logging.getLogger(__name__) + + +def capture_frames( + ffmpeg_process: sp.Popen[Any], + config: CameraConfig, + shm_frame_count: int, + frame_index: int, + frame_shape: tuple[int, int], + frame_manager: FrameManager, + frame_queue, + fps: Value, + skipped_fps: Value, + current_frame: Value, + stop_event: MpEvent, +) -> None: + frame_size = frame_shape[0] * frame_shape[1] + frame_rate = EventsPerSecond() + frame_rate.start() + skipped_eps = EventsPerSecond() + skipped_eps.start() + + config_subscriber = CameraConfigUpdateSubscriber( + None, {config.name: config}, [CameraConfigUpdateEnum.enabled] + ) + + def get_enabled_state(): + """Fetch the latest enabled state from ZMQ.""" + config_subscriber.check_for_updates() + return config.enabled + + try: + while not stop_event.is_set(): + if not get_enabled_state(): + logger.debug(f"Stopping capture thread for disabled {config.name}") + break + + fps.value = frame_rate.eps() + skipped_fps.value = skipped_eps.eps() + current_frame.value = datetime.now().timestamp() + frame_name = f"{config.name}_frame{frame_index}" + frame_buffer = frame_manager.write(frame_name) + try: + frame_buffer[:] = ffmpeg_process.stdout.read(frame_size) + except Exception: + # shutdown has been initiated + if stop_event.is_set(): + break + + logger.error( + f"{config.name}: Unable to read frames from ffmpeg process." + ) + + if ffmpeg_process.poll() is not None: + logger.error( + f"{config.name}: ffmpeg process is not running. exiting capture thread..." + ) + break + + continue + + frame_rate.update() + + # don't lock the queue to check, just try since it should rarely be full + try: + # add to the queue + frame_queue.put((frame_name, current_frame.value), False) + frame_manager.close(frame_name) + except queue.Full: + # if the queue is full, skip this frame + skipped_eps.update() + + frame_index = 0 if frame_index == shm_frame_count - 1 else frame_index + 1 + finally: + config_subscriber.stop() + + +class CameraWatchdog(threading.Thread): + def __init__( + self, + config: CameraConfig, + shm_frame_count: int, + frame_queue: Queue, + camera_fps, + skipped_fps, + ffmpeg_pid, + stalls, + reconnects, + detection_frame, + stop_event, + ): + threading.Thread.__init__(self) + self.logger = logging.getLogger(f"watchdog.{config.name}") + self.config = config + self.shm_frame_count = shm_frame_count + self.capture_thread = None + self.ffmpeg_detect_process = None + self.logpipe = LogPipe(f"ffmpeg.{self.config.name}.detect") + self.ffmpeg_other_processes: list[dict[str, Any]] = [] + self.camera_fps = camera_fps + self.skipped_fps = skipped_fps + self.ffmpeg_pid = ffmpeg_pid + self.frame_queue = frame_queue + self.frame_shape = self.config.frame_shape_yuv + self.frame_size = self.frame_shape[0] * self.frame_shape[1] + self.fps_overflow_count = 0 + self.frame_index = 0 + self.stop_event = stop_event + self.sleeptime = self.config.ffmpeg.retry_interval + self.reconnect_timestamps = deque() + self.stalls = stalls + self.reconnects = reconnects + self.detection_frame = detection_frame + + self.config_subscriber = CameraConfigUpdateSubscriber( + None, + {config.name: config}, + [ + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.ffmpeg, + CameraConfigUpdateEnum.record, + ], + ) + self.requestor = InterProcessRequestor() + self.was_enabled = self.config.enabled + self.was_record_enabled_in_config = self.config.record.enabled_in_config + + self.segment_subscriber = RecordingsDataSubscriber(RecordingsDataTypeEnum.all) + self.latest_valid_segment_time: float = 0 + self.latest_invalid_segment_time: float = 0 + self.latest_cache_segment_time: float = 0 + self.record_enable_time: datetime | None = None + + # `valid` segments are published with the segment's start time, so the + # gap between consecutive publishes can reach 2 * segment_time. Pad the + # staleness threshold so it's never tighter than that worst case. + segment_time = get_record_segment_time(self.config) + self.record_stale_threshold = max(120, 2 * segment_time + 30) + + # Stall tracking (based on last processed frame) + self._stall_timestamps: deque[float] = deque() + self._stall_active: bool = False + + # Status caching to reduce message volume + self._last_detect_status: str | None = None + self._last_record_status: str | None = None + self._last_status_update_time: float = 0.0 + + def _send_detect_status(self, status: str, now: float) -> None: + """Send detect status only if changed or retry_interval has elapsed.""" + if ( + status != self._last_detect_status + or (now - self._last_status_update_time) >= self.sleeptime + ): + self.requestor.send_data(f"{self.config.name}/status/detect", status) + self._last_detect_status = status + self._last_status_update_time = now + + def _send_record_status(self, status: str, now: float) -> None: + """Send record status only if changed or retry_interval has elapsed.""" + if ( + status != self._last_record_status + or (now - self._last_status_update_time) >= self.sleeptime + ): + self.requestor.send_data(f"{self.config.name}/status/record", status) + self._last_record_status = status + 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: + """Fetch the latest config and update enabled state.""" + self._check_config_updates() + return self.config.enabled + + def reset_capture_thread( + self, terminate: bool = True, drain_output: bool = True + ) -> None: + if terminate: + self.ffmpeg_detect_process.terminate() + try: + self.logger.info("Waiting for ffmpeg to exit gracefully...") + + if drain_output: + self.ffmpeg_detect_process.communicate(timeout=30) + else: + self.ffmpeg_detect_process.wait(timeout=30) + except sp.TimeoutExpired: + self.logger.info("FFmpeg did not exit. Force killing...") + self.ffmpeg_detect_process.kill() + + if drain_output: + self.ffmpeg_detect_process.communicate() + else: + self.ffmpeg_detect_process.wait() + + # Update reconnects + now = datetime.now().timestamp() + self.reconnect_timestamps.append(now) + while self.reconnect_timestamps and self.reconnect_timestamps[0] < now - 3600: + self.reconnect_timestamps.popleft() + if self.reconnects: + self.reconnects.value = len(self.reconnect_timestamps) + + # Wait for old capture thread to fully exit before starting a new one + if self.capture_thread is not None and self.capture_thread.is_alive(): + self.logger.info("Waiting for capture thread to exit...") + self.capture_thread.join(timeout=5) + + if self.capture_thread.is_alive(): + self.logger.warning( + f"Capture thread for {self.config.name} did not exit in time" + ) + + self.logger.error( + "The following ffmpeg logs include the last 100 lines prior to exit." + ) + self.logpipe.dump() + self.logger.info("Restarting ffmpeg...") + self.start_ffmpeg_detect() + + def run(self) -> None: + if self._update_enabled_state(): + self.start_all_ffmpeg() + # If recording is enabled at startup, set the grace period timer + if self.config.record.enabled: + self.record_enable_time = datetime.now().astimezone(UTC) + + time.sleep(self.sleeptime) + last_restart_time = datetime.now().timestamp() + + # 1 second watchdog loop + while not self.stop_event.wait(1): + 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(UTC) + last_restart_time = datetime.now().timestamp() + continue + + enabled = self.config.enabled + if enabled != self.was_enabled: + if enabled: + self.logger.debug(f"Enabling camera {self.config.name}") + self.start_all_ffmpeg() + + # reset all timestamps and record the enable time for grace period + 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(UTC) + else: + self.logger.debug(f"Disabling camera {self.config.name}") + self.stop_all_ffmpeg() + self.record_enable_time = None + + # update camera status + now = datetime.now().timestamp() + self._send_detect_status("disabled", now) + self._send_record_status("disabled", now) + self.was_enabled = enabled + continue + + record_enabled_in_config = self.config.record.enabled_in_config + if record_enabled_in_config != self.was_record_enabled_in_config: + if record_enabled_in_config and enabled: + self.logger.debug( + f"Record enabled in config for {self.config.name}, restarting ffmpeg" + ) + 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(UTC) + last_restart_time = datetime.now().timestamp() + self.was_record_enabled_in_config = record_enabled_in_config + continue + + if not enabled: + continue + + while True: + update = self.segment_subscriber.check_for_update(timeout=0) + + if update == (None, None): + break + + raw_topic, payload = update + if raw_topic and payload: + topic = str(raw_topic) + camera, segment_time, _ = payload + + if camera != self.config.name: + continue + + if topic.endswith(RecordingsDataTypeEnum.invalid.value): + self.logger.warning( + f"Invalid recording segment detected for {camera} at {segment_time}" + ) + self.latest_invalid_segment_time = segment_time + elif topic.endswith(RecordingsDataTypeEnum.valid.value): + self.logger.debug( + f"Latest valid recording segment time on {camera}: {segment_time}" + ) + self.latest_valid_segment_time = segment_time + elif topic.endswith(RecordingsDataTypeEnum.latest.value): + if segment_time is not None: + self.latest_cache_segment_time = segment_time + else: + self.latest_cache_segment_time = 0 + + now = datetime.now().timestamp() + + # Check if enough time has passed to allow ffmpeg restart (backoff pacing) + time_since_last_restart = now - last_restart_time + can_restart = time_since_last_restart >= self.sleeptime + + if not self.capture_thread.is_alive(): + self._send_detect_status("offline", now) + self.camera_fps.value = 0 + self.logger.error( + f"Ffmpeg process crashed unexpectedly for {self.config.name}." + ) + if can_restart: + self.reset_capture_thread(terminate=False) + last_restart_time = now + elif self.camera_fps.value >= (self.config.detect.fps + 10): + self.fps_overflow_count += 1 + + if self.fps_overflow_count == 3: + self._send_detect_status("offline", now) + self.fps_overflow_count = 0 + self.camera_fps.value = 0 + self.logger.info( + f"{self.config.name} exceeded fps limit. Exiting ffmpeg..." + ) + if can_restart: + self.reset_capture_thread(drain_output=False) + last_restart_time = now + elif now - self.capture_thread.current_frame.value > 20: + self._send_detect_status("offline", now) + self.camera_fps.value = 0 + self.logger.info( + f"No frames received from {self.config.name} in 20 seconds. Exiting ffmpeg..." + ) + if can_restart: + self.reset_capture_thread() + last_restart_time = now + else: + # process is running normally + self._send_detect_status("online", now) + self.fps_overflow_count = 0 + + for p in self.ffmpeg_other_processes: + poll = p["process"].poll() + + if self.config.record.enabled and "record" in p["roles"]: + now_utc = datetime.now().astimezone(UTC) + + # Check if we're within the grace period after enabling recording + # Grace period: 90 seconds allows time for ffmpeg to start and create first segment + in_grace_period = self.record_enable_time is not None and ( + now_utc - self.record_enable_time + ) < timedelta(seconds=90) + + latest_cache_dt = ( + datetime.fromtimestamp(self.latest_cache_segment_time, tz=UTC) + if self.latest_cache_segment_time > 0 + else now_utc - timedelta(seconds=1) + ) + + latest_valid_dt = ( + datetime.fromtimestamp(self.latest_valid_segment_time, tz=UTC) + if self.latest_valid_segment_time > 0 + else now_utc - timedelta(seconds=1) + ) + + latest_invalid_dt = ( + datetime.fromtimestamp(self.latest_invalid_segment_time, tz=UTC) + if self.latest_invalid_segment_time > 0 + else now_utc - timedelta(seconds=1) + ) + + # ensure segments are still being created and that they have valid video data + # Skip checks during grace period to allow segments to start being created + stale_window = timedelta(seconds=self.record_stale_threshold) + cache_stale = not in_grace_period and now_utc > ( + latest_cache_dt + stale_window + ) + valid_stale = not in_grace_period and now_utc > ( + latest_valid_dt + stale_window + ) + invalid_stale_condition = ( + self.latest_invalid_segment_time > 0 + and not in_grace_period + and now_utc > (latest_invalid_dt + stale_window) + and self.latest_valid_segment_time + <= self.latest_invalid_segment_time + ) + invalid_stale = invalid_stale_condition + + if cache_stale or valid_stale or invalid_stale: + if cache_stale: + reason = "No new recording segments were created" + elif valid_stale: + reason = "No new valid recording segments were created" + else: # invalid_stale + reason = ( + "No valid segments created since last invalid segment" + ) + + self.logger.error( + f"{reason} for {self.config.name} in the last {self.record_stale_threshold}s. Restarting the ffmpeg record process..." + ) + p["process"] = start_or_restart_ffmpeg( + p["cmd"], + self.logger, + p["logpipe"], + ffmpeg_process=p["process"], + ) + + for role in p["roles"]: + self.requestor.send_data( + f"{self.config.name}/status/{role.value}", "offline" + ) + + continue + else: + self._send_record_status("online", now) + p["latest_segment_time"] = self.latest_cache_segment_time + + if poll is None: + continue + + for role in p["roles"]: + self.requestor.send_data( + f"{self.config.name}/status/{role.value}", "offline" + ) + + p["logpipe"].dump() + p["process"] = start_or_restart_ffmpeg( + p["cmd"], self.logger, p["logpipe"], ffmpeg_process=p["process"] + ) + + # Prune expired reconnect timestamps + now = datetime.now().timestamp() + while ( + self.reconnect_timestamps and self.reconnect_timestamps[0] < now - 3600 + ): + self.reconnect_timestamps.popleft() + if self.reconnects: + self.reconnects.value = len(self.reconnect_timestamps) + + # Update stall metrics based on last processed frame timestamp + processed_ts = ( + float(self.detection_frame.value) if self.detection_frame else 0.0 + ) + if processed_ts > 0: + delta = now - processed_ts + observed_fps = ( + self.camera_fps.value + if self.camera_fps.value > 0 + else self.config.detect.fps + ) + interval = 1.0 / max(observed_fps, 0.1) + stall_threshold = max(2.0 * interval, 2.0) + + if delta > stall_threshold: + if not self._stall_active: + self._stall_timestamps.append(now) + self._stall_active = True + else: + self._stall_active = False + + while self._stall_timestamps and self._stall_timestamps[0] < now - 3600: + self._stall_timestamps.popleft() + + if self.stalls: + self.stalls.value = len(self._stall_timestamps) + + self.stop_all_ffmpeg() + self.logpipe.close() + self.config_subscriber.stop() + self.segment_subscriber.stop() + + def start_ffmpeg_detect(self): + ffmpeg_cmd = [ + c["cmd"] for c in self.config.ffmpeg_cmds if "detect" in c["roles"] + ][0] + self.ffmpeg_detect_process = start_or_restart_ffmpeg( + ffmpeg_cmd, self.logger, self.logpipe, self.frame_size + ) + self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid + self.capture_thread = CameraCaptureRunner( + self.config, + self.shm_frame_count, + self.frame_index, + self.ffmpeg_detect_process, + self.frame_shape, + self.frame_queue, + self.camera_fps, + self.skipped_fps, + self.stop_event, + ) + self.capture_thread.start() + + def start_all_ffmpeg(self): + """Start all ffmpeg processes (detection and others).""" + logger.debug(f"Starting all ffmpeg processes for {self.config.name}") + self.start_ffmpeg_detect() + for c in self.config.ffmpeg_cmds: + if "detect" in c["roles"]: + continue + logpipe = LogPipe( + f"ffmpeg.{self.config.name}.{'_'.join(sorted(c['roles']))}" + ) + self.ffmpeg_other_processes.append( + { + "cmd": c["cmd"], + "roles": c["roles"], + "logpipe": logpipe, + "process": start_or_restart_ffmpeg(c["cmd"], self.logger, logpipe), + } + ) + + def stop_all_ffmpeg(self): + """Stop all ffmpeg processes (detection and others).""" + logger.debug(f"Stopping all ffmpeg processes for {self.config.name}") + if self.capture_thread is not None and self.capture_thread.is_alive(): + self.capture_thread.join(timeout=5) + if self.capture_thread.is_alive(): + self.logger.warning( + f"Capture thread for {self.config.name} did not stop gracefully." + ) + if self.ffmpeg_detect_process is not None: + stop_ffmpeg(self.ffmpeg_detect_process, self.logger) + self.ffmpeg_detect_process = None + for p in self.ffmpeg_other_processes[:]: + if p["process"] is not None: + stop_ffmpeg(p["process"], self.logger) + p["logpipe"].close() + self.ffmpeg_other_processes.clear() + + +class CameraCaptureRunner(threading.Thread): + def __init__( + self, + config: CameraConfig, + shm_frame_count: int, + frame_index: int, + ffmpeg_process, + frame_shape: tuple[int, int], + frame_queue: Queue, + fps: Value, + skipped_fps: Value, + stop_event: MpEvent, + ): + threading.Thread.__init__(self) + self.name = f"capture:{config.name}" + self.config = config + self.shm_frame_count = shm_frame_count + self.frame_index = frame_index + self.frame_shape = frame_shape + self.frame_queue = frame_queue + self.fps = fps + self.stop_event = stop_event + self.skipped_fps = skipped_fps + self.frame_manager = SharedMemoryFrameManager() + self.ffmpeg_process = ffmpeg_process + self.current_frame = Value("d", 0.0) + self.last_frame = 0 + + def run(self): + capture_frames( + self.ffmpeg_process, + self.config, + self.shm_frame_count, + self.frame_index, + self.frame_shape, + self.frame_manager, + self.frame_queue, + self.fps, + self.skipped_fps, + self.current_frame, + self.stop_event, + ) + + +class CameraCapture(FrigateProcess): + def __init__( + self, + config: CameraConfig, + shm_frame_count: int, + camera_metrics: CameraMetrics, + stop_event: MpEvent, + log_config: LoggerConfig | None = None, + ) -> None: + super().__init__( + stop_event, + PROCESS_PRIORITY_HIGH, + name=f"frigate.capture:{config.name}", + daemon=True, + ) + self.config = config + self.shm_frame_count = shm_frame_count + self.camera_metrics = camera_metrics + self.log_config = log_config + + def run(self) -> None: + self.pre_run_setup(self.log_config) + camera_watchdog = CameraWatchdog( + self.config, + self.shm_frame_count, + self.camera_metrics.frame_queue, + self.camera_metrics.camera_fps, + self.camera_metrics.skipped_fps, + self.camera_metrics.ffmpeg_pid, + self.camera_metrics.stalls_last_hour, + self.camera_metrics.reconnects_last_hour, + self.camera_metrics.detection_frame, + self.stop_event, + ) + camera_watchdog.start() + camera_watchdog.join() diff --git a/frigate/watchdog.py b/frigate/watchdog.py new file mode 100644 index 0000000..88023d5 --- /dev/null +++ b/frigate/watchdog.py @@ -0,0 +1,139 @@ +import datetime +import logging +import threading +import time +from collections import deque +from collections.abc import Callable +from dataclasses import dataclass, field +from multiprocessing.synchronize import Event as MpEvent + +from frigate.object_detection.base import ObjectDetectProcess +from frigate.util.process import FrigateProcess +from frigate.util.services import restart_frigate + +logger = logging.getLogger(__name__) + +MAX_RESTARTS = 5 +RESTART_WINDOW_S = 60 + + +@dataclass +class MonitoredProcess: + """A process monitored by the watchdog for automatic restart.""" + + name: str + process: FrigateProcess + factory: Callable[[], FrigateProcess] + on_restart: Callable[[FrigateProcess], None] | None = None + restart_timestamps: deque[float] = field( + default_factory=lambda: deque(maxlen=MAX_RESTARTS) + ) + clean_exit_logged: bool = False + + def is_restarting_too_fast(self, now: float) -> bool: + while ( + self.restart_timestamps + and now - self.restart_timestamps[0] > RESTART_WINDOW_S + ): + self.restart_timestamps.popleft() + return len(self.restart_timestamps) >= MAX_RESTARTS + + +class FrigateWatchdog(threading.Thread): + def __init__( + self, + detectors: dict[str, ObjectDetectProcess], + stop_event: MpEvent, + ): + super().__init__(name="frigate_watchdog") + self.detectors = detectors + self.stop_event = stop_event + self._monitored: list[MonitoredProcess] = [] + + def register( + self, + name: str, + process: FrigateProcess, + factory: Callable[[], FrigateProcess], + on_restart: Callable[[FrigateProcess], None] | None = None, + ) -> None: + """Register a FrigateProcess for monitoring and automatic restart.""" + self._monitored.append( + MonitoredProcess( + name=name, + process=process, + factory=factory, + on_restart=on_restart, + ) + ) + + def _check_process(self, entry: MonitoredProcess) -> None: + if entry.process.is_alive(): + return + + exitcode = entry.process.exitcode + if exitcode == 0: + if not entry.clean_exit_logged: + logger.info("Process %s exited cleanly, not restarting", entry.name) + entry.clean_exit_logged = True + return + + logger.warning( + "Process %s (PID %s) exited with code %s", + entry.name, + entry.process.pid, + exitcode, + ) + + now = datetime.datetime.now().timestamp() + + if entry.is_restarting_too_fast(now): + logger.error( + "Process %s restarting too frequently (%d times in %ds), backing off", + entry.name, + MAX_RESTARTS, + RESTART_WINDOW_S, + ) + return + + try: + entry.process.close() + new_process = entry.factory() + new_process.start() + + entry.process = new_process + entry.restart_timestamps.append(now) + + if entry.on_restart: + entry.on_restart(new_process) + + logger.info("Restarted %s (PID %s)", entry.name, new_process.pid) + except Exception: + logger.exception("Failed to restart %s", entry.name) + + def run(self) -> None: + time.sleep(10) + while not self.stop_event.wait(10): + now = datetime.datetime.now().timestamp() + + # check the detection processes + for detector in self.detectors.values(): + detection_start = detector.detection_start.value # type: ignore[attr-defined] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + if detection_start > 0.0 and now - detection_start > 10: + logger.info( + "Detection appears to be stuck. Restarting detection process..." + ) + detector.start_or_restart() + elif ( + detector.detect_process is not None + and not detector.detect_process.is_alive() + ): + logger.info("Detection appears to have stopped. Exiting Frigate...") + restart_frigate() + + for entry in self._monitored: + self._check_process(entry) + + logger.info("Exiting watchdog...") diff --git a/generate_api_auth_spec.py b/generate_api_auth_spec.py new file mode 100644 index 0000000..5768a6d --- /dev/null +++ b/generate_api_auth_spec.py @@ -0,0 +1,606 @@ +"""Generate the OpenAPI spec from the app, annotated with auth requirements. + +This generator builds the FastAPI application, exports its OpenAPI document via +``app.openapi()``, and enriches every operation with authentication metadata: + + * a ``components.securitySchemes`` block, + * a per-operation ``security`` requirement (so the docs render a lock badge), + * an ``x-required-role`` extension for machine readers, and + * a short bold ``Access:`` note prepended to each operation description. + +The committed docs/static/frigate-api.yaml is the output of this script. It is +generated rather than hand-maintained so it stays complete and current; the docs +build (docusaurus-plugin-openapi-docs) consumes it as-is. + +The access level for an endpoint is determined by BOTH its route-level +dependency (``require_role``/``allow_any_authenticated``/``allow_public``/ +``require_camera_access``) AND the global "secure by default" admin dependency, +which is bypassed only for the paths listed in ``require_admin_by_default``. +Those exempt lists are read directly from the function's closure so this script +stays in lockstep with ``frigate/api/auth.py`` instead of duplicating them. + +Many handlers enforce per-camera access by calling ``require_camera_access`` +inside the handler body rather than as a route dependency, which dependency +introspection cannot see. We recover those from the handler's bytecode (see +``_handler_enforces_camera``) and promote an otherwise "any authenticated" +operation to camera-scoped. + +Usage (from the repository root): + + python3 generate_api_auth_spec.py # write the spec + python3 generate_api_auth_spec.py --check # CI guard: fail if stale + +The process exits non-zero if the generated document fails structural +validation, or (in --check mode) if the committed spec is out of date. +""" + +import argparse +import difflib +import inspect +import io +import logging +import sys +from pathlib import Path + +from fastapi import FastAPI +from fastapi.routing import APIRoute +from ruamel.yaml import YAML +from ruamel.yaml.scalarstring import LiteralScalarString + +from frigate.api import app as main_app +from frigate.api import ( + auth, + camera, + chat, + classification, + debug_replay, + event, + export, + media, + motion_search, + notification, + preview, + record, + review, +) +from frigate.api.auth import require_admin_by_default + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger("generate_api_auth_spec") + +REPO_ROOT = Path(__file__).resolve().parent +OUTPUT_SPEC = REPO_ROOT / "docs" / "static" / "frigate-api.yaml" + +HTTP_METHODS = {"get", "post", "put", "delete", "patch"} + +# Banner written at the top of the generated spec. +HEADER = ( + "# Generated by generate_api_auth_spec.py — do not edit by hand.\n" + "# Regenerate with: python3 generate_api_auth_spec.py\n" + "# The empty info.title is intentional: a docusaurus-openapi-docs convention\n" + "# that suppresses the generated API introduction page.\n" +) + +# Post-processing applied on top of the raw app.openapi() export. These live +# only in the published spec, not in the app, so they are reproduced here. +SPEC_TITLE = "" +SPEC_SERVERS = [ + {"url": "https://demo.frigate.video/api"}, + {"url": "http://localhost:5001/api"}, +] + +# Access levels, ordered from least to most privileged. The string values are +# also what we emit as ``x-required-role``. +PUBLIC = "public" +AUTHENTICATED = "any" +CAMERA = "camera" +ADMIN = "admin" + +ADMIN_SCHEME = "frigateAdminAuth" +USER_SCHEME = "frigateUserAuth" + +SECURITY_SCHEMES = { + ADMIN_SCHEME: { + "type": "apiKey", + "in": "cookie", + "name": "frigate_token", + "description": ( + "Authenticated session whose resolved role is 'admin'. The session " + "is established via the JWT cookie issued by POST /login, or via " + "proxy auth headers (remote-user / remote-role) when Frigate runs " + "behind an authenticating reverse proxy." + ), + }, + USER_SCHEME: { + "type": "apiKey", + "in": "cookie", + "name": "frigate_token", + "description": ( + "Any authenticated session (role 'viewer' or higher), established " + "via the JWT cookie issued by POST /login, or via proxy auth " + "headers when Frigate runs behind an authenticating reverse proxy." + ), + }, +} + +# How each access level maps to a rendered note. +ACCESS_NOTES = { + PUBLIC: "**Access:** Public — no authentication required.", + AUTHENTICATED: "**Access:** Any authenticated user.", + CAMERA: "**Access:** Authenticated user with access to the referenced camera.", + ADMIN: "**Access:** Admin role required.", +} + + +def build_app() -> FastAPI: + """Build a bare app with every router mounted. + + This mirrors the router set wired up in frigate.api.fastapi_app. It omits + the global admin dependency and all runtime state; the OpenAPI route table + and the per-route dependencies are all we need to export and classify. + """ + app = FastAPI() + routers = [ + auth.router, + camera.router, + chat.router, + classification.router, + review.router, + main_app.router, + preview.router, + notification.router, + export.router, + event.router, + media.router, + motion_search.router, + record.router, + debug_replay.router, + ] + for router in routers: + app.include_router(router) + return app + + +def read_exempt_rules() -> tuple[set[str], tuple[str, ...]]: + """Read the admin-exemption lists straight from the auth dependency closure. + + Reading them here (rather than copying) keeps this generator in sync with + frigate/api/auth.py automatically. + """ + closure = inspect.getclosurevars(require_admin_by_default()).nonlocals + exempt_paths = set(closure["EXEMPT_PATHS"]) + exempt_prefixes = tuple(closure["EXEMPT_PREFIXES"]) + return exempt_paths, exempt_prefixes + + +def _first_segment(path: str) -> str: + return path.split("/", 2)[1] if path.startswith("/") and len(path) > 1 else "" + + +def _route_markers(route: APIRoute) -> tuple[set[str], list[str] | None]: + """Return the set of recognized auth markers on a route's dependencies.""" + markers: set[str] = set() + admin_roles: list[str] | None = None + + for dep in route.dependant.dependencies: + call = dep.call + qualname = getattr(call, "__qualname__", "") or "" + name = getattr(call, "__name__", "") or "" + + if "role_checker" in qualname: + markers.add(ADMIN) + try: + roles = inspect.getclosurevars(call).nonlocals.get("required_roles") + if roles: + admin_roles = list(roles) + except (TypeError, ValueError): + pass + elif name in ("require_camera_access", "require_go2rtc_stream_access"): + markers.add(CAMERA) + elif "auth_checker" in qualname: + markers.add(AUTHENTICATED) + elif "public_checker" in qualname: + markers.add(PUBLIC) + + return markers, admin_roles + + +def _handler_enforces_camera(route: APIRoute) -> bool: + """True if the route handler calls require_camera_access in its body. + + Such calls are invisible to dependency introspection. We detect them from + the handler's compiled bytecode: a global name referenced anywhere in the + function appears in ``__code__.co_names``. This catches direct calls (all of + them, currently); a call hidden behind a helper function would be missed. + """ + code = getattr(route.endpoint, "__code__", None) + return bool(code and "require_camera_access" in code.co_names) + + +def classify_route( + route: APIRoute, + exempt_paths: set[str], + exempt_prefixes: tuple[str, ...], +) -> tuple[str, list[str] | None, str | None]: + """Resolve the effective access level for a route. + + Returns (access_level, roles, flag). ``flag`` is a human-readable note when + the result needed inference or revealed a possible inconsistency. + """ + level, roles, flag = _classify_base(route, exempt_paths, exempt_prefixes) + + # In-body require_camera_access enforcement is invisible to dependency + # introspection. When the effective access would otherwise be "any + # authenticated", the handler's per-camera check is the real constraint, so + # promote it to camera-scoped. Admin/public are left alone: for admin the + # role is the binding requirement and the camera check is only defensive. + if level == AUTHENTICATED and _handler_enforces_camera(route): + return CAMERA, None, None + + return level, roles, flag + + +def _classify_base( + route: APIRoute, + exempt_paths: set[str], + exempt_prefixes: tuple[str, ...], +) -> tuple[str, list[str] | None, str | None]: + """Resolve the access level from route-level dependencies and exempt rules.""" + markers, admin_roles = _route_markers(route) + path = route.path + is_camera_path = _first_segment(path) == "{camera_name}" + exempt = path in exempt_paths or path.startswith(exempt_prefixes) or is_camera_path + + # Explicit route-level markers win, in order of specificity. + if ADMIN in markers: + return ADMIN, admin_roles or ["admin"], None + if CAMERA in markers: + return CAMERA, None, None + if AUTHENTICATED in markers: + if exempt: + return AUTHENTICATED, None, None + # The route opts in to any-authenticated, but the global admin check is + # not bypassed for this path, so admin is what actually gets enforced. + return ( + ADMIN, + ["admin"], + ( + "route declares allow_any_authenticated but path is not exempt from " + "the global admin check; admin is effectively enforced" + ), + ) + if PUBLIC in markers: + if exempt: + return PUBLIC, None, None + return ( + ADMIN, + ["admin"], + ( + "route declares allow_public but path is not exempt from the global " + "admin check; admin is effectively enforced" + ), + ) + + # No explicit auth marker: governed purely by the global default. + if not exempt: + return ADMIN, ["admin"], None + + # Exempt with no route dependency: the global admin check is bypassed and + # there is no route-level gate, so authorization (if any) happens inside the + # handler. Infer from the path shape and flag for confirmation. + if is_camera_path: + return ( + CAMERA, + None, + ( + "no route-level dependency; camera-scoped path, authorization " + "assumed to be enforced in the handler" + ), + ) + return ( + AUTHENTICATED, + None, + ( + "path is exempt from the global admin check but has no route-level " + "dependency; confirm authorization is enforced in the handler" + ), + ) + + +def build_access_map( + app: FastAPI, + exempt_paths: set[str], + exempt_prefixes: tuple[str, ...], +) -> dict[tuple[str, str], dict]: + """Map (path, lowercase method) -> classification details.""" + access_map: dict[tuple[str, str], dict] = {} + for route in app.routes: + if not isinstance(route, APIRoute): + continue + level, roles, flag = classify_route(route, exempt_paths, exempt_prefixes) + for method in route.methods: + if method in ("HEAD", "OPTIONS"): + continue + access_map[(route.path, method.lower())] = { + "level": level, + "roles": roles, + "flag": flag, + "path": route.path, + "method": method, + } + return access_map + + +def security_for(level: str) -> list: + """Build the OpenAPI ``security`` value for an access level.""" + if level == PUBLIC: + return [] + if level == ADMIN: + return [{ADMIN_SCHEME: []}] + # AUTHENTICATED and CAMERA both require any authenticated session; the + # camera-specific scoping is conveyed in the note and x-required-role. + return [{USER_SCHEME: []}] + + +def required_role_value(level: str, roles: list[str] | None): + if level == ADMIN and roles and roles != ["admin"]: + return roles + return level + + +def annotate_description(operation: dict, note: str) -> None: + existing = operation.get("description") + if not existing: + operation["description"] = note + return + operation["description"] = LiteralScalarString( + f"{note}\n\n{str(existing).rstrip()}" + ) + + +def base_document(raw: dict) -> dict: + """Apply the docs pipeline post-processing with a stable top-level order.""" + info = dict(raw.get("info", {})) + info["title"] = SPEC_TITLE + return { + "openapi": raw["openapi"], + "info": info, + "servers": [dict(server) for server in SPEC_SERVERS], + "paths": raw["paths"], + "components": raw.get("components", {}), + } + + +def enrich(spec: dict, access_map: dict) -> tuple[dict, list, list]: + """Add security schemes and per-operation auth metadata in place.""" + components = spec.setdefault("components", {}) + components["securitySchemes"] = dict(SECURITY_SCHEMES) + + counts: dict[str, int] = {} + flagged: list[dict] = [] + unmatched: list[tuple[str, str]] = [] + + for path, path_item in spec["paths"].items(): + for method, operation in path_item.items(): + if method.lower() not in HTTP_METHODS: + continue + details = access_map.get((path, method.lower())) + if details is None: + unmatched.append((method.upper(), path)) + continue + + level = details["level"] + counts[level] = counts.get(level, 0) + 1 + operation["security"] = security_for(level) + operation["x-required-role"] = required_role_value(level, details["roles"]) + annotate_description(operation, ACCESS_NOTES[level]) + + if details["flag"]: + flagged.append(details) + + return counts, flagged, unmatched + + +# Numeric defaults at or above this magnitude are treated as live Unix +# timestamps baked into the schema at import time (e.g. the /{camera_name} +# /recordings after/before params default to datetime.now()). They make the +# export non-deterministic and document a meaningless frozen epoch, so they are +# stripped. The proper fix is to default those route params to None and resolve +# "now" inside the handler. +VOLATILE_DEFAULT_THRESHOLD = 1_000_000_000 + + +def strip_volatile_defaults(node, trail: str = "") -> list[tuple[str, float]]: + """Remove epoch-like numeric ``default`` values so the export is stable. + + Returns the (location, value) pairs that were removed, for reporting. + """ + removed: list[tuple[str, float]] = [] + if isinstance(node, dict): + default = node.get("default") + if ( + isinstance(default, (int, float)) + and not isinstance(default, bool) + and default >= VOLATILE_DEFAULT_THRESHOLD + ): + removed.append((trail, default)) + del node["default"] + for key, value in node.items(): + removed.extend(strip_volatile_defaults(value, f"{trail}/{key}")) + elif isinstance(node, list): + for index, value in enumerate(node): + removed.extend(strip_volatile_defaults(value, f"{trail}[{index}]")) + return removed + + +def to_block_scalars(node): + """Recursively render multi-line strings as literal block scalars. + + Produces readable, deterministic YAML (``|-`` blocks) instead of long + double-quoted lines with escaped newlines. + """ + if isinstance(node, dict): + return {key: to_block_scalars(value) for key, value in node.items()} + if isinstance(node, list): + return [to_block_scalars(value) for value in node] + if isinstance(node, str) and "\n" in node: + return LiteralScalarString(node) + return node + + +def _iter_refs(node): + if isinstance(node, dict): + for key, value in node.items(): + if key == "$ref" and isinstance(value, str): + yield value + else: + yield from _iter_refs(value) + elif isinstance(node, list): + for value in node: + yield from _iter_refs(value) + + +def validate(spec: dict) -> list[str]: + """Structural sanity checks on the generated document.""" + problems: list[str] = [] + schemas = set(spec.get("components", {}).get("schemas", {})) + defined_schemes = set(spec.get("components", {}).get("securitySchemes", {})) + + for ref in _iter_refs(spec): + if ref.startswith("#/components/schemas/"): + name = ref.rsplit("/", 1)[-1] + if name not in schemas: + problems.append(f"dangling $ref: {ref}") + + for path, path_item in spec.get("paths", {}).items(): + for method, operation in path_item.items(): + if method.lower() not in HTTP_METHODS or not isinstance(operation, dict): + continue + location = f"{method.upper()} {path}" + if "x-required-role" not in operation: + problems.append(f"missing x-required-role: {location}") + if "security" not in operation: + problems.append(f"missing security: {location}") + continue + for requirement in operation["security"]: + for scheme in requirement: + if scheme not in defined_schemes: + problems.append( + f"undefined security scheme {scheme}: {location}" + ) + + return sorted(set(problems)) + + +def render(spec: dict) -> str: + """Serialize the spec to the canonical YAML string (with the header).""" + yaml = YAML() + yaml.width = 80 + yaml.indent(mapping=2, sequence=4, offset=2) + stream = io.StringIO() + yaml.dump(spec, stream) + return HEADER + stream.getvalue() + + +def build_spec() -> tuple[dict, dict, list, list, list]: + app = build_app() + exempt_paths, exempt_prefixes = read_exempt_rules() + access_map = build_access_map(app, exempt_paths, exempt_prefixes) + + spec = base_document(app.openapi()) + normalized = strip_volatile_defaults(spec) + counts, flagged, unmatched = enrich(spec, access_map) + spec = to_block_scalars(spec) + return spec, counts, flagged, unmatched, normalized + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Generate the annotated OpenAPI spec.") + parser.add_argument( + "--check", + action="store_true", + help="verify the committed spec is up to date without writing; " + "exit non-zero if it would change", + ) + args = parser.parse_args(argv) + + spec, counts, flagged, unmatched, normalized = build_spec() + problems = validate(spec) + rendered = render(spec) + + if args.check: + return _check(rendered, problems) + + if problems: + logger.error("Refusing to write — generated spec failed validation:") + for problem in problems: + logger.error(" %s", problem) + return 1 + + OUTPUT_SPEC.write_text(rendered) + _report(counts, flagged, unmatched, normalized) + logger.info("\nWrote %s", OUTPUT_SPEC.relative_to(REPO_ROOT)) + return 0 + + +def _check(rendered: str, problems: list[str]) -> int: + name = OUTPUT_SPEC.relative_to(REPO_ROOT) + if problems: + logger.error("Generated spec failed validation:") + for problem in problems: + logger.error(" %s", problem) + return 1 + + current = OUTPUT_SPEC.read_text() if OUTPUT_SPEC.exists() else "" + if current == rendered: + logger.info("%s is up to date", name) + return 0 + + logger.error( + "%s is out of date. Regenerate with: python3 %s", + name, + Path(__file__).name, + ) + diff = difflib.unified_diff( + current.splitlines(), + rendered.splitlines(), + fromfile=f"{name} (committed)", + tofile=f"{name} (generated)", + lineterm="", + n=2, + ) + for shown, line in enumerate(diff): + if shown >= 60: + logger.error(" ... (diff truncated)") + break + logger.error(" %s", line) + return 1 + + +def _report(counts, flagged, unmatched, normalized) -> None: + logger.info("Access levels applied:") + for level in (PUBLIC, AUTHENTICATED, CAMERA, ADMIN): + logger.info(" %-14s %d", level, counts.get(level, 0)) + logger.info(" %-14s %d", "total", sum(counts.values())) + + if normalized: + logger.info("\nStripped volatile timestamp defaults (%d):", len(normalized)) + for location, value in normalized: + logger.info(" %s = %s", location.lstrip("/"), value) + + if flagged: + logger.info("\nFlagged for manual confirmation (%d):", len(flagged)) + for item in flagged: + logger.info(" %-6s %s", item["method"], item["path"]) + logger.info(" -> %s (%s)", item["level"], item["flag"]) + + if unmatched: + logger.info( + "\nOperations with no classification (%d) [unexpected]:", len(unmatched) + ) + for method, path in unmatched: + logger.info(" %-6s %s", method, path) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/generate_config_translations.py b/generate_config_translations.py new file mode 100644 index 0000000..aa115e4 --- /dev/null +++ b/generate_config_translations.py @@ -0,0 +1,651 @@ +#!/usr/bin/env python3 +""" +Generate English translation JSON files from Pydantic config models. + +This script dynamically extracts all top-level config sections from FrigateConfig +and generates JSON translation files with titles and descriptions for the web UI. +""" + +import json +import logging +import sys +from pathlib import Path +from typing import Any, get_args, get_origin + +from frigate.config.config import FrigateConfig +from frigate.util.schema import get_config_schema + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def get_field_translations(field_info) -> dict[str, str]: + """Extract title and description from a Pydantic field.""" + translations = {} + + if field_info.title: + translations["label"] = field_info.title + + if field_info.description: + translations["description"] = field_info.description + + return translations + + +def extract_translations_from_schema( + schema: dict[str, Any], defs: dict[str, Any] = None +) -> dict[str, Any]: + """ + Recursively extract translations (titles and descriptions) from a JSON schema. + + Returns a dictionary structure with label and description for each field, + and nested fields directly under their parent keys. + """ + if defs is None: + defs = schema.get("$defs", {}) + + translations = {} + + # Add top-level title and description if present + if "title" in schema: + translations["label"] = schema["title"] + if "description" in schema: + translations["description"] = schema["description"] + + # Process nested properties + properties = schema.get("properties", {}) + for field_name, field_schema in properties.items(): + field_translations = {} + + # Handle $ref references + if "$ref" in field_schema: + ref_path = field_schema["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + ref_schema = defs[ref_name] + # Extract from the referenced schema + ref_translations = extract_translations_from_schema( + ref_schema, defs=defs + ) + # Use the $ref field's own title/description if present + if "title" in field_schema: + field_translations["label"] = field_schema["title"] + elif "label" in ref_translations: + field_translations["label"] = ref_translations["label"] + if "description" in field_schema: + field_translations["description"] = field_schema["description"] + elif "description" in ref_translations: + field_translations["description"] = ref_translations[ + "description" + ] + # Add nested properties from referenced schema + nested_without_root = { + k: v + for k, v in ref_translations.items() + if k not in ("label", "description") + } + field_translations.update(nested_without_root) + # Handle additionalProperties with $ref (for dict types) + elif "additionalProperties" in field_schema: + additional_props = field_schema["additionalProperties"] + # Extract title and description from the field itself + if "title" in field_schema: + field_translations["label"] = field_schema["title"] + if "description" in field_schema: + field_translations["description"] = field_schema["description"] + + # If additionalProperties contains a $ref, extract nested translations + if "$ref" in additional_props: + ref_path = additional_props["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + ref_schema = defs[ref_name] + nested = extract_translations_from_schema(ref_schema, defs=defs) + nested_without_root = { + k: v + for k, v in nested.items() + if k not in ("label", "description") + } + field_translations.update(nested_without_root) + # Handle items with $ref (for array types) + elif "items" in field_schema: + items = field_schema["items"] + # Extract title and description from the field itself + if "title" in field_schema: + field_translations["label"] = field_schema["title"] + if "description" in field_schema: + field_translations["description"] = field_schema["description"] + + # If items contains a $ref, extract nested translations + if "$ref" in items: + ref_path = items["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + ref_schema = defs[ref_name] + nested = extract_translations_from_schema(ref_schema, defs=defs) + nested_without_root = { + k: v + for k, v in nested.items() + if k not in ("label", "description") + } + field_translations.update(nested_without_root) + else: + # Extract title and description + if "title" in field_schema: + field_translations["label"] = field_schema["title"] + if "description" in field_schema: + field_translations["description"] = field_schema["description"] + + # Recursively process nested properties + if "properties" in field_schema: + nested = extract_translations_from_schema(field_schema, defs=defs) + # Merge nested translations + nested_without_root = { + k: v for k, v in nested.items() if k not in ("label", "description") + } + field_translations.update(nested_without_root) + # Handle anyOf cases + elif "anyOf" in field_schema: + for item in field_schema["anyOf"]: + nested = None + if item.get("type") == "null": + continue + if "properties" in item: + nested = extract_translations_from_schema(item, defs=defs) + elif "$ref" in item: + ref_path = item["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + nested = extract_translations_from_schema( + defs[ref_name], defs=defs + ) + elif ( + "additionalProperties" in item + and isinstance(item["additionalProperties"], dict) + and "$ref" in item["additionalProperties"] + ): + ref_path = item["additionalProperties"]["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + nested = extract_translations_from_schema( + defs[ref_name], defs=defs + ) + elif ( + "items" in item + and isinstance(item["items"], dict) + and ("$ref" in item["items"]) + ): + ref_path = item["items"]["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + nested = extract_translations_from_schema( + defs[ref_name], defs=defs + ) + + if nested: + nested_without_root = { + k: v + for k, v in nested.items() + if k not in ("label", "description") + } + field_translations.update(nested_without_root) + + if field_translations: + translations[field_name] = field_translations + + return translations + + +def generate_section_translation(config_class: type) -> dict[str, Any]: + """ + Generate translation structure for a config section using its JSON schema. + """ + schema = config_class.model_json_schema() + return extract_translations_from_schema(schema) + + +def get_detector_translations( + config_schema: dict[str, Any], +) -> tuple[dict[str, Any], dict[str, Any], set[str]]: + """Build detector type translations with nested fields based on schema definitions. + + Returns a tuple of (type_translations, shared_fields, nested_field_keys). + Shared fields (identical across all detector types) are returned separately + to avoid duplication in the output. + """ + defs = config_schema.get("$defs", {}) + detector_schema = defs.get("DetectorConfig", {}) + discriminator = detector_schema.get("discriminator", {}) + mapping = discriminator.get("mapping", {}) + + # First pass: collect all nested fields per detector type + all_nested: dict[str, dict[str, Any]] = {} + type_meta: dict[str, dict[str, str]] = {} + + for detector_type, ref in mapping.items(): + if not isinstance(ref, str) or not ref.startswith("#/$defs/"): + continue + + ref_name = ref.split("/")[-1] + ref_schema = defs.get(ref_name, {}) + if not ref_schema: + continue + + meta: dict[str, str] = {} + title = ref_schema.get("title") + description = ref_schema.get("description") + if title: + meta["label"] = title + if description: + meta["description"] = description + type_meta[detector_type] = meta + + nested = extract_translations_from_schema(ref_schema, defs=defs) + all_nested[detector_type] = { + k: v for k, v in nested.items() if k not in ("label", "description") + } + + # Find fields that are identical across all types that have them + shared_fields: dict[str, Any] = {} + if all_nested: + # Collect all field keys across all types + all_keys: set[str] = set() + for nested in all_nested.values(): + all_keys.update(nested.keys()) + + for key in all_keys: + values = [nested[key] for nested in all_nested.values() if key in nested] + if len(values) == len(all_nested) and all(v == values[0] for v in values): + shared_fields[key] = values[0] + + # Build per-type translations with only unique (non-shared) fields + type_translations: dict[str, Any] = {} + nested_field_keys: set[str] = set() + for detector_type, nested in all_nested.items(): + type_entry: dict[str, Any] = {} + type_entry.update(type_meta.get(detector_type, {})) + + unique_fields = {k: v for k, v in nested.items() if k not in shared_fields} + if unique_fields: + type_entry.update(unique_fields) + nested_field_keys.update(unique_fields.keys()) + + if type_entry: + type_translations[detector_type] = type_entry + + return type_translations, shared_fields, nested_field_keys + + +def main(): + """Main function to generate config translations.""" + + # Define output directory + if len(sys.argv) > 1: + output_dir = Path(sys.argv[1]) + else: + output_dir = ( + Path(__file__).parent / "web" / "public" / "locales" / "en" / "config" + ) + + logger.info(f"Output directory: {output_dir}") + + # Ensure the output directory exists; do not delete existing files. + output_dir.mkdir(parents=True, exist_ok=True) + logger.info( + f"Using output directory (existing files will be overwritten): {output_dir}" + ) + + config_fields = FrigateConfig.model_fields + config_schema = get_config_schema(FrigateConfig) + logger.info(f"Found {len(config_fields)} top-level config sections") + + global_translations = {} + + for field_name, field_info in config_fields.items(): + if field_name.startswith("_"): + continue + + logger.info(f"Processing section: {field_name}") + + # Get the field's type + field_type = field_info.annotation + from typing import Optional, Union + + origin = get_origin(field_type) + if ( + origin is Optional + or origin is Union + or ( + hasattr(origin, "__name__") + and origin.__name__ in ("UnionType", "Union") + ) + ): + args = get_args(field_type) + field_type = next( + (arg for arg in args if arg is not type(None)), field_type + ) + + # Handle Dict[str, SomeModel] - extract the value type + if origin is dict: + args = get_args(field_type) + if args and len(args) > 1: + field_type = args[1] # Get value type from Dict[key, value] + + # Start with field's top-level metadata (label, description) + section_data = get_field_translations(field_info) + + # Generate nested translations from the field type's schema + if hasattr(field_type, "model_json_schema"): + schema = field_type.model_json_schema() + # Extract nested properties from schema + nested = extract_translations_from_schema(schema) + # Remove top-level label/description from nested since we got those from field_info + nested_without_root = { + k: v for k, v in nested.items() if k not in ("label", "description") + } + section_data.update(nested_without_root) + + if field_name == "detectors": + detector_types, shared_fields, detector_field_keys = ( + get_detector_translations(config_schema) + ) + # Add shared fields at the base detectors level + section_data.update(shared_fields) + # Add per-type translations (only unique fields per type) + section_data.update(detector_types) + for key in detector_field_keys: + if key == "type": + continue + section_data.pop(key, None) + + if field_name == "objects": + # Produce a parallel `filters_attribute` block alongside `filters`, + # with object-wording rewritten for attribute filters (face, + # license_plate, courier logos). The frontend's + # buildTranslationPath routes `filters..` lookups to + # `filters_attribute.` when `` is in + # `model.all_attributes`. Keep this rewrite list explicit rather + # than running a blanket s/object/attribute/ so unrelated + # descriptions (e.g. "JSON object") never accidentally flip. + filters_block = section_data.get("filters") + if isinstance(filters_block, dict): + attribute_rewrites = [ + ("Object filters", "Attribute filters"), + ("detected objects", "detected attributes"), + ("object area", "attribute area"), + ("object type", "attribute"), + ("the object", "the attribute"), + ] + + # Per-field overrides for cases where the generic rewrite + # doesn't capture the attribute-specific semantics. Keys + # match the FilterConfig field name; values are partial + # overrides applied AFTER the generic rewrites. + attribute_field_overrides: dict[str, dict[str, str]] = { + "min_score": { + "description": ( + "Minimum single-frame detection confidence required " + "to associate this attribute with its parent object." + ), + }, + } + + def rewrite(text: str) -> str: + for source, replacement in attribute_rewrites: + text = text.replace(source, replacement) + return text + + attribute_variant: dict[str, Any] = {} + for key, value in filters_block.items(): + if key in ("label", "description"): + if isinstance(value, str): + attribute_variant[key] = rewrite(value) + continue + if not isinstance(value, dict): + continue + field_trans: dict[str, str] = {} + if isinstance(value.get("label"), str): + field_trans["label"] = rewrite(value["label"]) + if isinstance(value.get("description"), str): + field_trans["description"] = rewrite(value["description"]) + overrides = attribute_field_overrides.get(key) + if overrides: + field_trans.update(overrides) + if field_trans: + attribute_variant[key] = field_trans + if attribute_variant: + section_data["filters_attribute"] = attribute_variant + + if not section_data: + logger.warning(f"No translations found for section: {field_name}") + continue + + # Add camera-level fields to global config documentation if applicable + CAMERA_LEVEL_FIELDS = { + "birdseye": ( + "frigate.config.camera.birdseye", + "BirdseyeCameraConfig", + ["order"], + ), + "ffmpeg": ( + "frigate.config.camera.ffmpeg", + "CameraFfmpegConfig", + ["inputs"], + ), + "lpr": ( + "frigate.config.classification", + "CameraLicensePlateRecognitionConfig", + ["expire_time"], + ), + "semantic_search": ( + "frigate.config.classification", + "CameraSemanticSearchConfig", + ["triggers"], + ), + } + + if field_name in CAMERA_LEVEL_FIELDS: + module_path, class_name, field_names = CAMERA_LEVEL_FIELDS[field_name] + try: + import importlib + + module = importlib.import_module(module_path) + camera_class = getattr(module, class_name) + schema = camera_class.model_json_schema() + camera_fields = schema.get("properties", {}) + defs = schema.get("$defs", {}) + + for fname in field_names: + if fname in camera_fields: + field_schema = camera_fields[fname] + field_trans = {} + if "title" in field_schema: + field_trans["label"] = field_schema["title"] + if "description" in field_schema: + field_trans["description"] = field_schema["description"] + + # Extract nested properties based on schema type + nested_to_extract = None + + # Handle direct $ref + if "$ref" in field_schema: + ref_path = field_schema["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + nested_to_extract = defs[ref_name] + + # Handle additionalProperties with $ref (for dict types) + elif "additionalProperties" in field_schema: + additional_props = field_schema["additionalProperties"] + if "$ref" in additional_props: + ref_path = additional_props["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + nested_to_extract = defs[ref_name] + + # Handle items with $ref (for array types) + elif "items" in field_schema: + items = field_schema["items"] + if "$ref" in items: + ref_path = items["$ref"] + if ref_path.startswith("#/$defs/"): + ref_name = ref_path.split("/")[-1] + if ref_name in defs: + nested_to_extract = defs[ref_name] + + # Extract nested properties if we found a schema to use + if nested_to_extract: + nested = extract_translations_from_schema( + nested_to_extract, defs=defs + ) + nested_without_root = { + k: v + for k, v in nested.items() + if k not in ("label", "description") + } + field_trans.update(nested_without_root) + + if field_trans: + section_data[fname] = field_trans + except Exception as e: + logger.warning( + f"Could not add camera-level fields for {field_name}: {e}" + ) + + # Add to global translations instead of writing separate files + global_translations[field_name] = section_data + + logger.info(f"Added section to global translations: {field_name}") + + # Handle camera-level configs that aren't top-level FrigateConfig fields + # These are defined as fields in CameraConfig, so we extract title/description from there + camera_level_configs = { + "camera_mqtt": ("frigate.config.camera.mqtt", "CameraMqttConfig", "mqtt"), + "camera_ui": ("frigate.config.camera.ui", "CameraUiConfig", "ui"), + "onvif": ("frigate.config.camera.onvif", "OnvifConfig", "onvif"), + } + + # Import CameraConfig to extract field metadata + from frigate.config.camera.camera import CameraConfig + + camera_config_schema = CameraConfig.model_json_schema() + camera_properties = camera_config_schema.get("properties", {}) + + for config_name, ( + module_path, + class_name, + camera_field_name, + ) in camera_level_configs.items(): + try: + logger.info(f"Processing camera-level section: {config_name}") + import importlib + + module = importlib.import_module(module_path) + config_class = getattr(module, class_name) + + section_data = {} + + # Extract top-level label and description from CameraConfig field definition + if camera_field_name in camera_properties: + field_schema = camera_properties[camera_field_name] + if "title" in field_schema: + section_data["label"] = field_schema["title"] + if "description" in field_schema: + section_data["description"] = field_schema["description"] + + # Process model fields from schema + schema = config_class.model_json_schema() + nested = extract_translations_from_schema(schema) + # Remove top-level label/description since we got those from CameraConfig + nested_without_root = { + k: v for k, v in nested.items() if k not in ("label", "description") + } + section_data.update(nested_without_root) + + # Add camera-level section into global translations (do not write separate file) + global_translations[config_name] = section_data + logger.info( + f"Added camera-level section to global translations: {config_name}" + ) + except Exception as e: + logger.error(f"Failed to generate {config_name}: {e}") + + # Remove top-level 'cameras' field if present so it remains a separate file + if "cameras" in global_translations: + logger.info( + "Removing top-level 'cameras' from global translations to keep it as a separate cameras.json" + ) + del global_translations["cameras"] + + # Write consolidated global.json with per-section keys + global_file = output_dir / "global.json" + with open(global_file, "w", encoding="utf-8") as f: + json.dump(global_translations, f, indent=2, ensure_ascii=False) + f.write("\n") + + logger.info(f"Generated consolidated translations: {global_file}") + + if not global_translations: + logger.warning("No global translations were generated!") + else: + logger.info(f"Global contains {len(global_translations)} sections") + + # Generate cameras.json from CameraConfig schema + cameras_file = output_dir / "cameras.json" + logger.info(f"Generating cameras.json: {cameras_file}") + try: + if "camera_config_schema" in locals(): + camera_schema = camera_config_schema + else: + from frigate.config.camera.camera import CameraConfig + + camera_schema = CameraConfig.model_json_schema() + + camera_translations = extract_translations_from_schema(camera_schema) + + # Change descriptions to use 'for this camera' for fields that are global + def sanitize_camera_descriptions(obj): + if isinstance(obj, dict): + for k, v in list(obj.items()): + if k == "description" and isinstance(v, str): + obj[k] = v.replace( + "for all cameras; can be overridden per-camera", + "for this camera", + ) + else: + sanitize_camera_descriptions(v) + elif isinstance(obj, list): + for item in obj: + sanitize_camera_descriptions(item) + + sanitize_camera_descriptions(camera_translations) + + # Profiles contain the same sections as the camera itself; only keep + # label and description to avoid duplicating every camera section. + if "profiles" in camera_translations: + camera_translations["profiles"] = { + k: v + for k, v in camera_translations["profiles"].items() + if k in ("label", "description") + } + + with open(cameras_file, "w", encoding="utf-8") as f: + json.dump(camera_translations, f, indent=2, ensure_ascii=False) + f.write("\n") + logger.info(f"Generated cameras.json: {cameras_file}") + except Exception as e: + logger.error(f"Failed to generate cameras.json: {e}") + + logger.info("Translation generation complete!") + + +if __name__ == "__main__": + main() diff --git a/labelmap.txt b/labelmap.txt new file mode 100644 index 0000000..79fff17 --- /dev/null +++ b/labelmap.txt @@ -0,0 +1,91 @@ +0 person +1 bicycle +2 car +3 motorcycle +4 airplane +5 bus +6 train +7 car +8 boat +9 traffic light +10 fire hydrant +11 street sign +12 stop sign +13 parking meter +14 bench +15 bird +16 cat +17 dog +18 horse +19 sheep +20 cow +21 elephant +22 bear +23 zebra +24 giraffe +25 hat +26 backpack +27 umbrella +28 shoe +29 eye glasses +30 handbag +31 tie +32 suitcase +33 frisbee +34 skis +35 snowboard +36 sports ball +37 kite +38 baseball bat +39 baseball glove +40 skateboard +41 surfboard +42 tennis racket +43 bottle +44 plate +45 wine glass +46 cup +47 fork +48 knife +49 spoon +50 bowl +51 banana +52 apple +53 sandwich +54 orange +55 broccoli +56 carrot +57 hot dog +58 pizza +59 donut +60 cake +61 chair +62 couch +63 potted plant +64 bed +65 mirror +66 dining table +67 window +68 desk +69 toilet +70 door +71 tv +72 laptop +73 mouse +74 remote +75 keyboard +76 cell phone +77 microwave +78 oven +79 toaster +80 sink +81 refrigerator +82 blender +83 book +84 clock +85 vase +86 scissors +87 teddy bear +88 hair drier +89 toothbrush +90 hair brush \ No newline at end of file diff --git a/migrations/001_create_events_table.py b/migrations/001_create_events_table.py new file mode 100644 index 0000000..57f9aa6 --- /dev/null +++ b/migrations/001_create_events_table.py @@ -0,0 +1,38 @@ +"""Peewee migrations -- 001_create_events_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "event" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "label" VARCHAR(20) NOT NULL, "camera" VARCHAR(20) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME NOT NULL, "top_score" REAL NOT NULL, "false_positive" INTEGER NOT NULL, "zones" JSON NOT NULL, "thumbnail" TEXT NOT NULL)' + ) + migrator.sql('CREATE INDEX IF NOT EXISTS "event_label" ON "event" ("label")') + migrator.sql('CREATE INDEX IF NOT EXISTS "event_camera" ON "event" ("camera")') + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/002_add_clip_snapshot.py b/migrations/002_add_clip_snapshot.py new file mode 100644 index 0000000..47a46f5 --- /dev/null +++ b/migrations/002_add_clip_snapshot.py @@ -0,0 +1,40 @@ +"""Peewee migrations -- 002_add_clip_snapshot.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + has_clip=pw.BooleanField(default=True), + has_snapshot=pw.BooleanField(default=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["has_clip", "has_snapshot"]) diff --git a/migrations/003_create_recordings_table.py b/migrations/003_create_recordings_table.py new file mode 100644 index 0000000..3956ae9 --- /dev/null +++ b/migrations/003_create_recordings_table.py @@ -0,0 +1,45 @@ +"""Peewee migrations -- 003_create_recordings_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "recordings" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "path" VARCHAR(255) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME NOT NULL, "duration" REAL NOT NULL)' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "recordings_camera" ON "recordings" ("camera")' + ) + migrator.sql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "recordings_path" ON "recordings" ("path")' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "recordings_start_time_end_time" ON "recordings" (start_time, end_time)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/004_add_bbox_region_area.py b/migrations/004_add_bbox_region_area.py new file mode 100644 index 0000000..a1aa35a --- /dev/null +++ b/migrations/004_add_bbox_region_area.py @@ -0,0 +1,42 @@ +"""Peewee migrations -- 004_add_bbox_region_area.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw +from playhouse.sqlite_ext import JSONField + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + region=JSONField(default=[]), + box=JSONField(default=[]), + area=pw.IntegerField(default=0), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["region", "box", "area"]) diff --git a/migrations/005_make_end_time_nullable.py b/migrations/005_make_end_time_nullable.py new file mode 100644 index 0000000..d80d31d --- /dev/null +++ b/migrations/005_make_end_time_nullable.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 004_add_bbox_region_area.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.drop_not_null(Event, "end_time") + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/006_add_motion_active_objects.py b/migrations/006_add_motion_active_objects.py new file mode 100644 index 0000000..2fe1f90 --- /dev/null +++ b/migrations/006_add_motion_active_objects.py @@ -0,0 +1,43 @@ +"""Peewee migrations -- 004_add_bbox_region_area.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Recordings + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Recordings, + objects=pw.IntegerField(null=True), + motion=pw.IntegerField(null=True), + ) + migrator.sql( + 'CREATE INDEX "recordings_activity" ON "recordings" ("camera", "start_time" DESC, "regions")' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Recordings, ["objects", "motion"]) diff --git a/migrations/007_add_retain_indefinitely.py b/migrations/007_add_retain_indefinitely.py new file mode 100644 index 0000000..e5d07ab --- /dev/null +++ b/migrations/007_add_retain_indefinitely.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 007_add_retain_indefinitely.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + retain_indefinitely=pw.BooleanField(default=False), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["retain_indefinitely"]) diff --git a/migrations/008_add_sub_label.py b/migrations/008_add_sub_label.py new file mode 100644 index 0000000..bba3834 --- /dev/null +++ b/migrations/008_add_sub_label.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 008_add_sub_label.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + sub_label=pw.CharField(max_length=20, null=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["sub_label"]) diff --git a/migrations/009_add_object_filter_ratio.py b/migrations/009_add_object_filter_ratio.py new file mode 100644 index 0000000..77a25eb --- /dev/null +++ b/migrations/009_add_object_filter_ratio.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 009_add_object_filter_ratio.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + ratio=pw.FloatField(default=1.0), # Assume that existing detections are square + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["ratio"]) diff --git a/migrations/010_add_plus_image_id.py b/migrations/010_add_plus_image_id.py new file mode 100644 index 0000000..d403dbb --- /dev/null +++ b/migrations/010_add_plus_image_id.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 010_add_plus_image_id.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + plus_id=pw.CharField(max_length=30, null=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["plus_id"]) diff --git a/migrations/011_update_indexes.py b/migrations/011_update_indexes.py new file mode 100644 index 0000000..6d411d3 --- /dev/null +++ b/migrations/011_update_indexes.py @@ -0,0 +1,40 @@ +"""Peewee migrations -- 011_update_indexes.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE INDEX "event_start_time_end_time" ON "event" ("start_time" DESC, "end_time" DESC)' + ) + migrator.sql("DROP INDEX recordings_start_time_end_time") + migrator.sql( + 'CREATE INDEX "recordings_end_time_start_time" ON "recordings" ("end_time" DESC, "start_time" DESC)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/012_add_segment_size.py b/migrations/012_add_segment_size.py new file mode 100644 index 0000000..8ea91a1 --- /dev/null +++ b/migrations/012_add_segment_size.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 012_add_segment_size.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Recordings + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Recordings, + segment_size=pw.FloatField(default=0), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Recordings, ["segment_size"]) diff --git a/migrations/013_create_timeline_table.py b/migrations/013_create_timeline_table.py new file mode 100644 index 0000000..9ed2606 --- /dev/null +++ b/migrations/013_create_timeline_table.py @@ -0,0 +1,45 @@ +"""Peewee migrations -- 013_create_timeline_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "timeline" ("timestamp" DATETIME NOT NULL, "camera" VARCHAR(20) NOT NULL, "source" VARCHAR(20) NOT NULL, "source_id" VARCHAR(30), "class_type" VARCHAR(50) NOT NULL, "data" JSON)' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "timeline_camera" ON "timeline" ("camera")' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "timeline_source" ON "timeline" ("source")' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "timeline_source_id" ON "timeline" ("source_id")' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/014_event_updates_for_fp.py b/migrations/014_event_updates_for_fp.py new file mode 100644 index 0000000..f44f6c9 --- /dev/null +++ b/migrations/014_event_updates_for_fp.py @@ -0,0 +1,45 @@ +"""Peewee migrations + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + score=pw.FloatField(null=True), + model_hash=pw.CharField(max_length=32, null=True), + detector_type=pw.CharField(max_length=32, null=True), + model_type=pw.CharField(max_length=32, null=True), + ) + + migrator.drop_not_null(Event, "area", "false_positive") + migrator.add_default(Event, "false_positive", 0) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/015_event_refactor.py b/migrations/015_event_refactor.py new file mode 100644 index 0000000..92d8a16 --- /dev/null +++ b/migrations/015_event_refactor.py @@ -0,0 +1,43 @@ +"""Peewee migrations + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw +from playhouse.sqlite_ext import JSONField + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.drop_not_null( + Event, "top_score", "score", "region", "box", "area", "ratio" + ) + migrator.add_fields( + Event, + data=JSONField(default={}), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/016_sublabel_increase.py b/migrations/016_sublabel_increase.py new file mode 100644 index 0000000..66411ff --- /dev/null +++ b/migrations/016_sublabel_increase.py @@ -0,0 +1,11 @@ +import peewee as pw + +from frigate.models import Event + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.change_fields(Event, sub_label=pw.CharField(max_length=100, null=True)) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.change_fields(Event, sub_label=pw.CharField(max_length=20, null=True)) diff --git a/migrations/017_update_indexes.py b/migrations/017_update_indexes.py new file mode 100644 index 0000000..63685ea --- /dev/null +++ b/migrations/017_update_indexes.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 017_update_indexes.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE INDEX "recordings_camera_segment_size" ON "recordings" ("camera", "segment_size")' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/018_add_dbfs.py b/migrations/018_add_dbfs.py new file mode 100644 index 0000000..485e954 --- /dev/null +++ b/migrations/018_add_dbfs.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 018_add_dbfs.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Recordings + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Recordings, + dBFS=pw.IntegerField(null=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Recordings, ["dBFS"]) diff --git a/migrations/019_create_regions_table.py b/migrations/019_create_regions_table.py new file mode 100644 index 0000000..961aaf8 --- /dev/null +++ b/migrations/019_create_regions_table.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 019_create_regions_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "regions" ("camera" VARCHAR(20) NOT NULL PRIMARY KEY, "last_update" DATETIME NOT NULL, "grid" JSON)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/020_update_index_recordings.py b/migrations/020_update_index_recordings.py new file mode 100644 index 0000000..d6af71c --- /dev/null +++ b/migrations/020_update_index_recordings.py @@ -0,0 +1,41 @@ +"""Peewee migrations -- 020_update_index_recordings.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql("DROP INDEX recordings_end_time_start_time") + migrator.sql( + 'CREATE INDEX "recordings_camera_start_time_end_time" ON "recordings" ("camera", "start_time" DESC, "end_time" DESC)' + ) + migrator.sql( + 'CREATE INDEX "recordings_api_recordings_summary" ON "recordings" ("camera", "start_time" DESC, "duration", "motion", "objects")' + ) + migrator.sql('CREATE INDEX "recordings_start_time" ON "recordings" ("start_time")') + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/021_create_previews_table.py b/migrations/021_create_previews_table.py new file mode 100644 index 0000000..b775360 --- /dev/null +++ b/migrations/021_create_previews_table.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 021_create_previews_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "previews" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "path" VARCHAR(255) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME NOT NULL, "duration" REAL NOT NULL)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/022_create_review_segment_table.py b/migrations/022_create_review_segment_table.py new file mode 100644 index 0000000..91d0c8c --- /dev/null +++ b/migrations/022_create_review_segment_table.py @@ -0,0 +1,42 @@ +"""Peewee migrations -- 022_create_review_segment_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "reviewsegment" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME, "has_been_reviewed" INTEGER NOT NULL, "severity" VARCHAR(30) NOT NULL, "thumb_path" VARCHAR(255) NOT NULL, "data" JSON NOT NULL)' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "review_segment_camera" ON "reviewsegment" ("camera")' + ) + migrator.sql( + 'CREATE INDEX "review_segment_start_time_end_time" ON "reviewsegment" ("start_time" DESC, "end_time" DESC)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/023_add_regions.py b/migrations/023_add_regions.py new file mode 100644 index 0000000..7649baa --- /dev/null +++ b/migrations/023_add_regions.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 023_add_regions.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Recordings + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Recordings, + regions=pw.IntegerField(null=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Recordings, ["regions"]) diff --git a/migrations/024_create_export_table.py b/migrations/024_create_export_table.py new file mode 100644 index 0000000..8de2f17 --- /dev/null +++ b/migrations/024_create_export_table.py @@ -0,0 +1,37 @@ +"""Peewee migrations -- 024_create_export_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "export" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "name" VARCHAR(100) NOT NULL, "date" DATETIME NOT NULL, "video_path" VARCHAR(255) NOT NULL, "thumb_path" VARCHAR(255) NOT NULL, "in_progress" INTEGER NOT NULL)' + ) + migrator.sql('CREATE INDEX IF NOT EXISTS "export_camera" ON "export" ("camera")') + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/025_create_user_table.py b/migrations/025_create_user_table.py new file mode 100644 index 0000000..dec57d6 --- /dev/null +++ b/migrations/025_create_user_table.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 025_create_user_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "user" ("username" VARCHAR(30) NOT NULL PRIMARY KEY, "password_hash" VARCHAR(120) NOT NULL)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/026_add_notification_tokens.py b/migrations/026_add_notification_tokens.py new file mode 100644 index 0000000..23860c5 --- /dev/null +++ b/migrations/026_add_notification_tokens.py @@ -0,0 +1,40 @@ +"""Peewee migrations + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw +from playhouse.sqlite_ext import JSONField + +from frigate.models import User + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + User, + notification_tokens=JSONField(default=[]), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/027_create_explore_index.py b/migrations/027_create_explore_index.py new file mode 100644 index 0000000..f08c0bb --- /dev/null +++ b/migrations/027_create_explore_index.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 027_create_explore_index.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "event_label_start_time" ON "event" ("label", "start_time" DESC)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.sql('DROP INDEX IF EXISTS "event_label_start_time"') diff --git a/migrations/028_optional_event_thumbnail.py b/migrations/028_optional_event_thumbnail.py new file mode 100644 index 0000000..5217700 --- /dev/null +++ b/migrations/028_optional_event_thumbnail.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 028_optional_event_thumbnail.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.drop_not_null(Event, "thumbnail") + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.add_not_null(Event, "thumbnail") diff --git a/migrations/029_add_user_role.py b/migrations/029_add_user_role.py new file mode 100644 index 0000000..e0fb1bb --- /dev/null +++ b/migrations/029_add_user_role.py @@ -0,0 +1,37 @@ +"""Peewee migrations -- 029_add_user_role.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'ALTER TABLE "user" ADD COLUMN "role" VARCHAR(20) NOT NULL DEFAULT \'admin\'' + ) + migrator.sql('UPDATE "user" SET "role" = \'admin\' WHERE "role" IS NULL') + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.sql('ALTER TABLE "user" DROP COLUMN "role"') diff --git a/migrations/030_create_user_review_status.py b/migrations/030_create_user_review_status.py new file mode 100644 index 0000000..ddcf063 --- /dev/null +++ b/migrations/030_create_user_review_status.py @@ -0,0 +1,89 @@ +"""Peewee migrations -- 030_create_user_review_status.py. + +This migration creates the UserReviewStatus table to track per-user review states, +migrates existing has_been_reviewed data from ReviewSegment to all users in the user table, +and drops the has_been_reviewed column. Rollback drops UserReviewStatus and restores the column. + +Some examples (model - class or model_name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import User, UserReviewStatus + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + User._meta.database = database + UserReviewStatus._meta.database = database + + migrator.sql( + """ + CREATE TABLE IF NOT EXISTS "userreviewstatus" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "user_id" VARCHAR(30) NOT NULL, + "review_segment_id" VARCHAR(30) NOT NULL, + "has_been_reviewed" INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY ("review_segment_id") REFERENCES "reviewsegment" ("id") ON DELETE CASCADE + ) + """ + ) + + # Add unique index on (user_id, review_segment_id) + migrator.sql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "userreviewstatus_user_segment" ON "userreviewstatus" ("user_id", "review_segment_id")' + ) + + # Migrate existing has_been_reviewed data to UserReviewStatus for all users + def migrate_data(): + # Use raw SQL to avoid ORM issues with columns that don't exist yet + cursor = database.execute_sql('SELECT "username" FROM "user"') + all_users = cursor.fetchall() + if not all_users: + return + + cursor = database.execute_sql( + 'SELECT "id" FROM "reviewsegment" WHERE "has_been_reviewed" = 1' + ) + reviewed_segment_ids = [row[0] for row in cursor.fetchall()] + # also migrate for anonymous (unauthenticated users) + usernames = [user[0] for user in all_users] + ["anonymous"] + + for segment_id in reviewed_segment_ids: + for username in usernames: + UserReviewStatus.create( + user_id=username, + review_segment=segment_id, + has_been_reviewed=True, + ) + + if not fake: # Only run data migration if not faking + migrator.run(migrate_data) + + migrator.sql('ALTER TABLE "reviewsegment" DROP COLUMN "has_been_reviewed"') + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.sql('DROP TABLE IF EXISTS "userreviewstatus"') + # Restore has_been_reviewed column to reviewsegment (no data restoration) + migrator.sql( + 'ALTER TABLE "reviewsegment" ADD COLUMN "has_been_reviewed" INTEGER NOT NULL DEFAULT 0' + ) diff --git a/migrations/031_create_trigger_table.py b/migrations/031_create_trigger_table.py new file mode 100644 index 0000000..c2ac2e0 --- /dev/null +++ b/migrations/031_create_trigger_table.py @@ -0,0 +1,50 @@ +"""Peewee migrations -- 031_create_trigger_table.py. + +This migration creates the Trigger table to track semantic search triggers for cameras. + +Some examples (model - class or model_name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + """ + CREATE TABLE IF NOT EXISTS trigger ( + camera VARCHAR(20) NOT NULL, + name VARCHAR NOT NULL, + type VARCHAR(10) NOT NULL, + model VARCHAR(30) NOT NULL, + data TEXT NOT NULL, + threshold REAL, + embedding BLOB, + triggering_event_id VARCHAR(30), + last_triggered DATETIME, + PRIMARY KEY (camera, name) + ) + """ + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.sql("DROP TABLE IF EXISTS trigger") diff --git a/migrations/032_add_password_changed_at.py b/migrations/032_add_password_changed_at.py new file mode 100644 index 0000000..5382c12 --- /dev/null +++ b/migrations/032_add_password_changed_at.py @@ -0,0 +1,42 @@ +"""Peewee migrations -- 032_add_password_changed_at.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + """ + ALTER TABLE user ADD COLUMN password_changed_at DATETIME NULL + """ + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.sql( + """ + ALTER TABLE user DROP COLUMN password_changed_at + """ + ) diff --git a/migrations/033_create_export_case_table.py b/migrations/033_create_export_case_table.py new file mode 100644 index 0000000..08edcbc --- /dev/null +++ b/migrations/033_create_export_case_table.py @@ -0,0 +1,50 @@ +"""Peewee migrations -- 033_create_export_case_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.python(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + """ + CREATE TABLE IF NOT EXISTS "exportcase" ( + "id" VARCHAR(30) NOT NULL PRIMARY KEY, + "name" VARCHAR(100) NOT NULL, + "description" TEXT NULL, + "created_at" DATETIME NOT NULL, + "updated_at" DATETIME NOT NULL + ) + """ + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "exportcase_name" ON "exportcase" ("name")' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "exportcase_created_at" ON "exportcase" ("created_at")' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/034_add_export_case_to_exports.py b/migrations/034_add_export_case_to_exports.py new file mode 100644 index 0000000..da9e1d4 --- /dev/null +++ b/migrations/034_add_export_case_to_exports.py @@ -0,0 +1,40 @@ +"""Peewee migrations -- 034_add_export_case_to_exports.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.python(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + # Add nullable export_case_id column to export table + migrator.sql('ALTER TABLE "export" ADD COLUMN "export_case_id" VARCHAR(30) NULL') + + # Index for faster case-based queries + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "export_export_case_id" ON "export" ("export_case_id")' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/migrations/035_add_motion_heatmap.py b/migrations/035_add_motion_heatmap.py new file mode 100644 index 0000000..b696208 --- /dev/null +++ b/migrations/035_add_motion_heatmap.py @@ -0,0 +1,34 @@ +"""Peewee migrations -- 035_add_motion_heatmap.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.python(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql('ALTER TABLE "recordings" ADD COLUMN "motion_heatmap" TEXT NULL') + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..59362d2 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,7 @@ +[build] + base = "docs/" + publish = "build" + command = "npm run build" + environment = { NODE_VERSION = "20" } + + diff --git a/notebooks/README.md b/notebooks/README.md new file mode 100644 index 0000000..23afa6e --- /dev/null +++ b/notebooks/README.md @@ -0,0 +1,10 @@ +# Notebooks + +## YOLO-NAS Pretrained + +You can build and download a compatible model with pre-trained weights using [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb). + +> [!WARNING] +> The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html + +The input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired. By default, YOLO_NAS_S is built with YOLO_NAS_M and YOLO_NAS_L sizes also being available for export. \ No newline at end of file diff --git a/notebooks/YOLO_NAS_Pretrained_Export.ipynb b/notebooks/YOLO_NAS_Pretrained_Export.ipynb new file mode 100644 index 0000000..23c55d1 --- /dev/null +++ b/notebooks/YOLO_NAS_Pretrained_Export.ipynb @@ -0,0 +1,95 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "runtime-notice" + }, + "source": [ + "**Before running:** go to **Runtime → Change runtime type → Fallback runtime version: 2025.07** (Python 3.11). The current Colab default (Python 3.12+) is incompatible with `super-gradients`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rmuF9iKWTbdk" + }, + "outputs": [], + "source": [ + "! pip install -q \"jedi>=0.16\"\n", + "! pip install -q git+https://github.com/Deci-AI/super-gradients.git" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NiRCt917KKcL" + }, + "outputs": [], + "source": "! sed -i 's/sghub\\.deci\\.ai/d2gjn4b69gu75n.cloudfront.net/g; s/sg-hub-nv\\.s3\\.amazonaws\\.com/d2gjn4b69gu75n.cloudfront.net/g' /usr/local/lib/python*/dist-packages/super_gradients/training/pretrained_models.py\n! sed -i 's/sghub\\.deci\\.ai/d2gjn4b69gu75n.cloudfront.net/g; s/sg-hub-nv\\.s3\\.amazonaws\\.com/d2gjn4b69gu75n.cloudfront.net/g' /usr/local/lib/python*/dist-packages/super_gradients/training/utils/checkpoint_utils.py" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dTB0jy_NNSFz" + }, + "outputs": [], + "source": [ + "from super_gradients.common.object_names import Models\n", + "from super_gradients.conversion import DetectionOutputFormatMode\n", + "from super_gradients.training import models\n", + "\n", + "model = models.get(Models.YOLO_NAS_S, pretrained_weights=\"coco\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GymUghyCNXem" + }, + "outputs": [], + "source": [ + "# export the model for compatibility with Frigate\n", + "\n", + "model.export(\"yolo_nas_s.onnx\",\n", + " output_predictions_format=DetectionOutputFormatMode.FLAT_FORMAT,\n", + " max_predictions_per_image=20,\n", + " num_pre_nms_predictions=300,\n", + " confidence_threshold=0.4,\n", + " input_image_shape=(320,320),\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uBhXV5g4Nh42" + }, + "outputs": [], + "source": [ + "from google.colab import files\n", + "\n", + "files.download('yolo_nas_s.onnx')" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..13ac760 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "frigate", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..775db60 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.ruff] +target-version = "py311" + +[tool.ruff.lint] +ignore = ["E501","E711","E712","UP031","UP032","UP042","G004"] +extend-select = ["I", "UP", "G", "ASYNC210", "B904"] diff --git a/testing-scripts/analyze_recording_keyframes.py b/testing-scripts/analyze_recording_keyframes.py new file mode 100644 index 0000000..982cac8 --- /dev/null +++ b/testing-scripts/analyze_recording_keyframes.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +"""Analyze keyframe and timestamp structure of Frigate recording segments. + +This is a diagnostic tool for investigating seek precision / GOP behavior on +recorded segments. It does not modify anything. + +ffprobe is only available inside the Frigate container, at + /usr/lib/ffmpeg/$DEFAULT_FFMPEG_VERSION/bin/ffprobe +This script auto-resolves that path from the DEFAULT_FFMPEG_VERSION env var +(or falls back to scanning /usr/lib/ffmpeg/*/bin/ffprobe). Pass --ffprobe to +override if needed. + +All recording segments on the filesystem are in UTC. The --timestamp flag +expects a UTC Unix timestamp. + +Typical use: + # Inside the Frigate container (or wherever recordings are mounted) + python3 analyze_recording_keyframes.py + + # Analyze 10 most recent segments + python3 analyze_recording_keyframes.py --count 10 + + # Locate the segment that contains a specific UTC Unix timestamp and + # show it plus surrounding segments + python3 analyze_recording_keyframes.py --timestamp 1713471234.567 + + # Custom recordings directory + python3 analyze_recording_keyframes.py --recordings-dir /media/frigate/recordings + + # Override the ffprobe path explicitly + python3 analyze_recording_keyframes.py --ffprobe /usr/lib/ffmpeg/7.0/bin/ffprobe +""" + +import argparse +import datetime +import json +import os +import subprocess +import sys +from pathlib import Path +from statistics import mean, median, stdev + + +def resolve_ffprobe_path(override: str | None) -> str: + """Resolve the ffprobe binary path. + + Inside the Frigate container, ffprobe lives at + /usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffprobe — the exact version + depends on the image build and is exposed as an env var. + """ + if override: + return override + version = os.environ.get("DEFAULT_FFMPEG_VERSION", "") + if version: + path = f"/usr/lib/ffmpeg/{version}/bin/ffprobe" + if Path(path).is_file(): + return path + # Fall back to scanning the Frigate ffmpeg install root. + for candidate in sorted(Path("/usr/lib/ffmpeg").glob("*/bin/ffprobe")): + if candidate.is_file(): + return str(candidate) + print( + "Could not locate ffprobe. Pass --ffprobe or set " + "DEFAULT_FFMPEG_VERSION.", + file=sys.stderr, + ) + sys.exit(1) + + +def find_recent_segments(recordings_dir: Path, camera: str, count: int) -> list[Path]: + """Return the N most recent .mp4 segments for the given camera. + + Expected layout: ////..mp4 + """ + pattern = f"*/*/{camera}/*.mp4" + segments = sorted(recordings_dir.glob(pattern)) + return segments[-count:] + + +def find_segments_near_timestamp( + recordings_dir: Path, camera: str, target_ts: float, count: int +) -> tuple[list[Path], Path | None]: + """Return `count` segments centered on the one containing `target_ts`. + + Also returns the specific segment that should contain the timestamp, so + callers can highlight it in output. + """ + pattern = f"*/*/{camera}/*.mp4" + with_ts: list[tuple[float, Path]] = [] + for seg in sorted(recordings_dir.glob(pattern)): + ts = filename_to_timestamp(seg) + if ts is not None: + with_ts.append((ts, seg)) + + if not with_ts: + return [], None + + # Largest filename_ts that is <= target_ts — that's the segment that + # should contain the timestamp (Frigate catalogs segments by filename). + target_idx = -1 + for i, (ts, _) in enumerate(with_ts): + if ts <= target_ts: + target_idx = i + else: + break + + if target_idx < 0: + # target_ts is before the earliest segment we have — just return the + # first `count` segments so the user can see what's available. + window = with_ts[:count] + return [seg for _, seg in window], None + + half = count // 2 + start = max(0, target_idx - half) + end = min(len(with_ts), start + count) + start = max(0, end - count) + + window = with_ts[start:end] + return [seg for _, seg in window], with_ts[target_idx][1] + + +def filename_to_timestamp(segment: Path) -> float | None: + """Parse the wall-clock time from Frigate's segment path layout.""" + try: + date = segment.parent.parent.parent.name # YYYY-MM-DD + hour = segment.parent.parent.name # HH + mm_ss = segment.stem # MM.SS + minute, second = mm_ss.split(".") + dt = datetime.datetime.strptime( + f"{date} {hour}:{minute}:{second}", + "%Y-%m-%d %H:%M:%S", + ).replace(tzinfo=datetime.timezone.utc) + return dt.timestamp() + except (ValueError, IndexError): + return None + + +def run_ffprobe(ffprobe: str, args: list[str]) -> dict: + """Run ffprobe and return parsed JSON, or empty dict on failure.""" + result = subprocess.run( + [ffprobe, "-v", "error", *args, "-of", "json"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + print(f" ffprobe error: {result.stderr.strip()}", file=sys.stderr) + return {} + try: + return json.loads(result.stdout) + except json.JSONDecodeError: + return {} + + +def get_format_info(ffprobe: str, segment: Path) -> tuple[dict, dict]: + """Return (format_dict, stream_dict) for the first video stream.""" + data = run_ffprobe( + ffprobe, + [ + "-show_entries", + "format=duration,start_time", + "-show_entries", + "stream=codec_name,profile,r_frame_rate,width,height", + "-select_streams", + "v:0", + str(segment), + ], + ) + fmt = data.get("format", {}) + streams = data.get("streams") or [{}] + return fmt, streams[0] + + +def get_video_packets(ffprobe: str, segment: Path) -> list[dict]: + """Return video packets with pts_time and flags.""" + data = run_ffprobe( + ffprobe, + [ + "-select_streams", + "v", + "-show_entries", + "packet=pts_time,dts_time,flags", + str(segment), + ], + ) + return data.get("packets", []) + + +def analyze(ffprobe: str, segment: Path, highlight: bool = False) -> None: + marker = " <-- contains target timestamp" if highlight else "" + print(f"\n=== {segment} ==={marker}") + + fmt, stream = get_format_info(ffprobe, segment) + duration = float(fmt.get("duration", 0) or 0) + start_time = float(fmt.get("start_time", 0) or 0) + codec = stream.get("codec_name", "?") + profile = stream.get("profile", "?") + width = stream.get("width", "?") + height = stream.get("height", "?") + fps = stream.get("r_frame_rate", "?/1") + + filename_ts = filename_to_timestamp(segment) + filename_iso = ( + datetime.datetime.fromtimestamp( + filename_ts, tz=datetime.timezone.utc + ).isoformat() + if filename_ts is not None + else "?" + ) + + print(f" Codec: {codec} ({profile}) {width}x{height} {fps}") + print(f" Filename time: {filename_ts} ({filename_iso})") + print(f" Format duration: {duration:.3f}s") + print(f" Format start: {start_time:.3f}s (PTS offset of first packet)") + + packets = get_video_packets(ffprobe, segment) + if not packets: + print(" (no video packets)") + return + + keyframe_times: list[float] = [] + first_pts: float | None = None + last_pts: float | None = None + + for pkt in packets: + pts_str = pkt.get("pts_time") + if pts_str is None or pts_str == "N/A": + continue + pts = float(pts_str) + if first_pts is None: + first_pts = pts + last_pts = pts + if "K" in pkt.get("flags", ""): + keyframe_times.append(pts) + + total_packets = len(packets) + kf_count = len(keyframe_times) + + print(f" Video packets: {total_packets}") + print(f" Keyframes: {kf_count}") + if first_pts is not None and last_pts is not None: + print( + f" Packet PTS: first={first_pts:.3f}s last={last_pts:.3f}s " + f"span={last_pts - first_pts:.3f}s" + ) + + if keyframe_times: + print( + f" Keyframe PTS: first={keyframe_times[0]:.3f}s " + f"last={keyframe_times[-1]:.3f}s" + ) + formatted = ", ".join(f"{t:.3f}" for t in keyframe_times) + print(f" Keyframe times: [{formatted}]") + + if len(keyframe_times) >= 2: + gaps = [b - a for a, b in zip(keyframe_times, keyframe_times[1:])] + avg_fps_estimate = ( + total_packets / (last_pts - first_pts) + if last_pts and first_pts is not None and last_pts > first_pts + else 0 + ) + print( + f" GOP gaps (s): min={min(gaps):.3f} max={max(gaps):.3f} " + f"mean={mean(gaps):.3f} median={median(gaps):.3f}" + ) + if len(gaps) > 1: + print(f" stdev={stdev(gaps):.3f}") + print( + f" Est. mean GOP: ~{mean(gaps) * avg_fps_estimate:.1f} frames" + if avg_fps_estimate + else "" + ) + if max(gaps) > 5: + print( + " !! Max GOP > 5s — consistent with adaptive/smart codec " + "(even if 'Smart Codec' is off in the UI, some cameras still " + "produce irregular GOPs under specific encoder profiles)" + ) + elif kf_count == 1: + print(" !! Only one keyframe in segment — very long GOP") + + # Report how well filename time aligns with first-packet PTS. + # (Filename time is what Frigate uses as recording.start_time in the DB.) + if filename_ts is not None and first_pts is not None: + print( + f" Notes: first packet PTS is {first_pts:.3f}s into the file; " + f"Frigate treats filename time as PTS=0 for seek math." + ) + + +def main() -> None: + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("camera", help="Camera name (matches the recordings subfolder)") + parser.add_argument( + "--count", + type=int, + default=5, + help="Number of most recent segments to analyze (default: 5)", + ) + parser.add_argument( + "--recordings-dir", + default="/media/frigate/recordings", + help="Path to the recordings directory (default: /media/frigate/recordings)", + ) + parser.add_argument( + "--ffprobe", + default=None, + help=( + "Full path to the ffprobe binary. Defaults to the Frigate-bundled " + "binary at /usr/lib/ffmpeg/$DEFAULT_FFMPEG_VERSION/bin/ffprobe." + ), + ) + parser.add_argument( + "--timestamp", + type=float, + default=None, + help=( + "Unix timestamp (UTC seconds, decimals allowed) to locate. The " + "script finds the segment that should contain this time and " + "analyzes it plus surrounding segments (count controls the " + "window). All on-disk segments are stored in UTC, so pass a UTC " + "Unix timestamp." + ), + ) + args = parser.parse_args() + + ffprobe = resolve_ffprobe_path(args.ffprobe) + + recordings_dir = Path(args.recordings_dir) + if not recordings_dir.is_dir(): + print( + f"Recordings directory not found: {recordings_dir}", + file=sys.stderr, + ) + sys.exit(1) + + target_segment: Path | None = None + if args.timestamp is not None: + segments, target_segment = find_segments_near_timestamp( + recordings_dir, args.camera, args.timestamp, args.count + ) + target_iso = datetime.datetime.fromtimestamp( + args.timestamp, tz=datetime.timezone.utc + ).isoformat() + mode = f"around timestamp {args.timestamp} ({target_iso})" + else: + segments = find_recent_segments(recordings_dir, args.camera, args.count) + mode = "most recent" + + if not segments: + print( + f"No segments found for camera '{args.camera}' under {recordings_dir}", + file=sys.stderr, + ) + sys.exit(1) + + if args.timestamp is not None and target_segment is None: + print( + f"!! Target timestamp {args.timestamp} is before the earliest " + f"segment on disk; showing the earliest available segments instead.", + file=sys.stderr, + ) + + print( + f"Analyzing {len(segments)} {mode} segment(s) for camera " + f"'{args.camera}' under {recordings_dir} (ffprobe: {ffprobe})" + ) + for segment in segments: + analyze(ffprobe, segment, highlight=(segment == target_segment)) + + +if __name__ == "__main__": + main() diff --git a/testing-scripts/benchmark.py b/testing-scripts/benchmark.py new file mode 100755 index 0000000..46adc59 --- /dev/null +++ b/testing-scripts/benchmark.py @@ -0,0 +1,109 @@ +import datetime +import multiprocessing as mp +from statistics import mean + +import numpy as np + +from frigate.config import DetectorTypeEnum +from frigate.object_detection.base import ( + ObjectDetectProcess, + RemoteObjectDetector, + load_labels, +) +from frigate.util.process import FrigateProcess + +my_frame = np.expand_dims(np.full((300, 300, 3), 1, np.uint8), axis=0) +labels = load_labels("/labelmap.txt") + +###### +# Minimal same process runner +###### +# object_detector = LocalObjectDetector() +# tensor_input = np.expand_dims(np.full((300,300,3), 0, np.uint8), axis=0) + +# start = datetime.datetime.now().timestamp() + +# frame_times = [] +# for x in range(0, 1000): +# start_frame = datetime.datetime.now().timestamp() + +# tensor_input[:] = my_frame +# detections = object_detector.detect_raw(tensor_input) +# parsed_detections = [] +# for d in detections: +# if d[1] < 0.4: +# break +# parsed_detections.append(( +# labels[int(d[0])], +# float(d[1]), +# (d[2], d[3], d[4], d[5]) +# )) +# frame_times.append(datetime.datetime.now().timestamp()-start_frame) + +# duration = datetime.datetime.now().timestamp()-start +# print(f"Processed for {duration:.2f} seconds.") +# print(f"Average frame processing time: {mean(frame_times)*1000:.2f}ms") + + +def start(id, num_detections, detection_queue, event): + object_detector = RemoteObjectDetector( + str(id), "/labelmap.txt", detection_queue, event + ) + start = datetime.datetime.now().timestamp() + + frame_times = [] + for x in range(0, num_detections): + start_frame = datetime.datetime.now().timestamp() + object_detector.detect(my_frame) + frame_times.append(datetime.datetime.now().timestamp() - start_frame) + + duration = datetime.datetime.now().timestamp() - start + object_detector.cleanup() + print(f"{id} - Processed for {duration:.2f} seconds.") + print(f"{id} - FPS: {object_detector.fps.eps():.2f}") + print(f"{id} - Average frame processing time: {mean(frame_times) * 1000:.2f}ms") + + +###### +# Separate process runner +###### +# event = mp.Event() +# detection_queue = mp.Queue() +# edgetpu_process = EdgeTPUProcess(detection_queue, {'1': event}, 'usb:0') + +# start(1, 1000, edgetpu_process.detection_queue, event) +# print(f"Average raw inference speed: {edgetpu_process.avg_inference_speed.value*1000:.2f}ms") + +#### +# Multiple camera processes +#### +camera_processes = [] + +events = {} +for x in range(0, 10): + events[str(x)] = mp.Event() +detection_queue = mp.Queue() +edgetpu_process_1 = ObjectDetectProcess( + detection_queue, events, DetectorTypeEnum.edgetpu, "usb:0" +) +edgetpu_process_2 = ObjectDetectProcess( + detection_queue, events, DetectorTypeEnum.edgetpu, "usb:1" +) + +for x in range(0, 10): + camera_process = FrigateProcess( + target=start, args=(x, 300, detection_queue, events[str(x)]) + ) + camera_process.daemon = True + camera_processes.append(camera_process) + +start_time = datetime.datetime.now().timestamp() + +for p in camera_processes: + p.start() + +for p in camera_processes: + p.join() + +duration = datetime.datetime.now().timestamp() - start_time +print(f"Total - Processed for {duration:.2f} seconds.") diff --git a/testing-scripts/benchmark_motion.py b/testing-scripts/benchmark_motion.py new file mode 100644 index 0000000..431398f --- /dev/null +++ b/testing-scripts/benchmark_motion.py @@ -0,0 +1,118 @@ +import datetime +import multiprocessing as mp +import os + +import cv2 +import numpy as np + +from frigate.config import MotionConfig +from frigate.motion.improved_motion import ImprovedMotionDetector +from frigate.util import create_mask + +# get info on the video +# cap = cv2.VideoCapture("debug/front_cam_2023_05_23_08_41__2023_05_23_08_43.mp4") +# cap = cv2.VideoCapture("debug/motion_test_clips/rain_1.mp4") +cap = cv2.VideoCapture("debug/motion_test_clips/lawn_mower_night_1.mp4") +# cap = cv2.VideoCapture("airport.mp4") +width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) +height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) +fps = cap.get(cv2.CAP_PROP_FPS) +frame_shape = (height, width, 3) +# Nick back: +# "1280,0,1280,316,1170,216,1146,126,1016,127,979,82,839,0", +# "310,350,300,402,224,405,241,354", +# "378,0,375,26,0,23,0,0", +# Front door: +# "1080,0,1080,339,1010,280,1020,169,777,163,452,170,318,299,191,365,186,417,139,470,108,516,40,530,0,514,0,0", +# "336,833,438,1024,346,1093,103,1052,24,814", +# Back +# "1855,0,1851,100,1289,96,1105,161,1045,119,890,121,890,0", +# "505,95,506,138,388,153,384,114", +# "689,72,689,122,549,134,547,89", +# "261,134,264,176,169,195,167,158", +# "145,159,146,202,70,220,65,183", + +mask = create_mask( + (height, width), + [ + "1080,0,1080,339,1010,280,1020,169,777,163,452,170,318,299,191,365,186,417,139,470,108,516,40,530,0,514,0,0", + "336,833,438,1024,346,1093,103,1052,24,814", + ], +) + +# create the motion config +motion_config_1 = MotionConfig() +motion_config_1.mask = np.zeros((height, width), np.uint8) +motion_config_1.mask[:] = mask +# motion_config_1.improve_contrast = 1 +motion_config_1.frame_height = 150 +# motion_config_1.frame_alpha = 0.02 +# motion_config_1.threshold = 30 +# motion_config_1.contour_area = 10 + +motion_config_2 = MotionConfig() +motion_config_2.mask = np.zeros((height, width), np.uint8) +motion_config_2.mask[:] = mask +# motion_config_2.improve_contrast = 1 +motion_config_2.frame_height = 150 +# motion_config_2.frame_alpha = 0.01 +motion_config_2.threshold = 20 +# motion_config.contour_area = 10 + +save_images = True + +improved_motion_detector_1 = ImprovedMotionDetector( + frame_shape=frame_shape, + config=motion_config_1, + fps=fps, + improve_contrast=mp.Value("i", motion_config_1.improve_contrast), + threshold=mp.Value("i", motion_config_1.threshold), + contour_area=mp.Value("i", motion_config_1.contour_area), + name="default", +) +improved_motion_detector_1.save_images = save_images + +improved_motion_detector_2 = ImprovedMotionDetector( + frame_shape=frame_shape, + config=motion_config_2, + fps=fps, + improve_contrast=mp.Value("i", motion_config_2.improve_contrast), + threshold=mp.Value("i", motion_config_2.threshold), + contour_area=mp.Value("i", motion_config_2.contour_area), + name="compare", +) +improved_motion_detector_2.save_images = save_images + +# read and process frames +ret, frame = cap.read() +frame_counter = 1 +while ret: + yuv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420) + + start_frame = datetime.datetime.now().timestamp() + improved_motion_detector_1.detect(yuv_frame) + + start_frame = datetime.datetime.now().timestamp() + improved_motion_detector_2.detect(yuv_frame) + + default_frame = f"debug/frames/default-{frame_counter}.jpg" + compare_frame = f"debug/frames/compare-{frame_counter}.jpg" + if os.path.exists(default_frame) and os.path.exists(compare_frame): + images = [ + cv2.imread(default_frame), + cv2.imread(compare_frame), + ] + + cv2.imwrite( + f"debug/frames/all-{frame_counter}.jpg", + cv2.vconcat(images) + if frame_shape[0] > frame_shape[1] + else cv2.hconcat(images), + ) + os.unlink(default_frame) + os.unlink(compare_frame) + frame_counter += 1 + + ret, frame = cap.read() + +cap.release() diff --git a/testing-scripts/face_dataset.py b/testing-scripts/face_dataset.py new file mode 100644 index 0000000..0c9e451 --- /dev/null +++ b/testing-scripts/face_dataset.py @@ -0,0 +1,783 @@ +""" +Face recognition investigation script. + +Standalone replica of Frigate's ArcFace pipeline (see +frigate/data_processing/common/face/model.py and +frigate/embeddings/onnx/face_embedding.py) for analyzing a face collection +outside the running service. Useful for: + + - Diagnosing why a person's collection produces false positives + - Finding outlier/contaminating training images + - Inspecting the effect of the shipped vector-wise outlier filter + +Layout: + - Core pipeline: LandmarkAligner, ArcFaceEmbedder, arcface_preprocess, + similarity_to_confidence, blur_reduction — all mirroring the production + code exactly + - Default run: summarize positive and negative sets against a baseline + trim_mean class representation + - Optional diagnostics (flags): vector-outlier filter behavior, degenerate + "tiny crop" embedding clustering, and multi-identity contamination + +Usage: + python3 face_investigate.py \\ + --positive \\ + --negative \\ + [--model-cache /path/to/model_cache] \\ + [--vector-outlier] [--degenerate] [--contamination] + +The positive folder should contain training images for a single identity +(same layout as FACE_DIR//*.webp). The negative folder should contain +runtime crops to test against — a mix of true matches and misfires. +""" + +from __future__ import annotations + +import argparse +import os +import sys +from dataclasses import dataclass +from typing import Iterable + +import cv2 +import numpy as np +import onnxruntime as ort +from PIL import Image +from scipy import stats + +ARCFACE_INPUT_SIZE = 112 + + +# --------------------------------------------------------------------------- +# Replicated Frigate pipeline +# --------------------------------------------------------------------------- + + +def _process_image_frigate(image: np.ndarray) -> Image.Image: + """Mirror BaseEmbedding._process_image for an ndarray input. + + NOTE: Frigate passes the output of `cv2.imread` (BGR) directly in. PIL's + `Image.fromarray` does NOT reorder channels, so the embedder effectively + receives a BGR-ordered tensor. We replicate that faithfully here. (Tested + — swapping to RGB produces near-identical embeddings; this model is + robust to channel order.) + """ + return Image.fromarray(image) + + +def arcface_preprocess(image_bgr: np.ndarray) -> np.ndarray: + """Mirror ArcfaceEmbedding._preprocess_inputs.""" + pil = _process_image_frigate(image_bgr) + + width, height = pil.size + if width != ARCFACE_INPUT_SIZE or height != ARCFACE_INPUT_SIZE: + if width > height: + new_height = int(((height / width) * ARCFACE_INPUT_SIZE) // 4 * 4) + pil = pil.resize((ARCFACE_INPUT_SIZE, new_height)) + else: + new_width = int(((width / height) * ARCFACE_INPUT_SIZE) // 4 * 4) + pil = pil.resize((new_width, ARCFACE_INPUT_SIZE)) + + og = np.array(pil).astype(np.float32) + og_h, og_w, channels = og.shape + + frame = np.zeros( + (ARCFACE_INPUT_SIZE, ARCFACE_INPUT_SIZE, channels), dtype=np.float32 + ) + x_center = (ARCFACE_INPUT_SIZE - og_w) // 2 + y_center = (ARCFACE_INPUT_SIZE - og_h) // 2 + frame[y_center : y_center + og_h, x_center : x_center + og_w] = og + + frame = (frame / 127.5) - 1.0 + frame = np.transpose(frame, (2, 0, 1)) + frame = np.expand_dims(frame, axis=0) + return frame + + +class LandmarkAligner: + """Mirror FaceRecognizer.align_face.""" + + def __init__(self, landmark_model_path: str): + if not os.path.exists(landmark_model_path): + raise FileNotFoundError(landmark_model_path) + self.detector = cv2.face.createFacemarkLBF() + self.detector.loadModel(landmark_model_path) + + def align( + self, image: np.ndarray, out_w: int, out_h: int + ) -> tuple[np.ndarray, dict]: + land_image = ( + cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if image.ndim == 3 else image + ) + _, lands = self.detector.fit( + land_image, np.array([(0, 0, land_image.shape[1], land_image.shape[0])]) + ) + landmarks = lands[0][0] + + leftEyePts = landmarks[42:48] + rightEyePts = landmarks[36:42] + leftEyeCenter = leftEyePts.mean(axis=0).astype("int") + rightEyeCenter = rightEyePts.mean(axis=0).astype("int") + + dY = rightEyeCenter[1] - leftEyeCenter[1] + dX = rightEyeCenter[0] - leftEyeCenter[0] + angle = np.degrees(np.arctan2(dY, dX)) - 180 + dist = float(np.sqrt((dX**2) + (dY**2))) + + desiredRightEyeX = 1.0 - 0.35 + desiredDist = (desiredRightEyeX - 0.35) * out_w + scale = desiredDist / dist if dist > 0 else 1.0 + + eyesCenter = ( + int((leftEyeCenter[0] + rightEyeCenter[0]) // 2), + int((leftEyeCenter[1] + rightEyeCenter[1]) // 2), + ) + M = cv2.getRotationMatrix2D(eyesCenter, angle, scale) + tX = out_w * 0.5 + tY = out_h * 0.35 + M[0, 2] += tX - eyesCenter[0] + M[1, 2] += tY - eyesCenter[1] + + aligned = cv2.warpAffine( + image, M, (out_w, out_h), flags=cv2.INTER_CUBIC + ) + info = dict( + angle=float(angle), + eye_dist_px=dist, + scale=float(scale), + landmarks=landmarks, + ) + return aligned, info + + +class ArcFaceEmbedder: + def __init__(self, model_path: str): + self.session = ort.InferenceSession( + model_path, providers=["CPUExecutionProvider"] + ) + self.input_name = self.session.get_inputs()[0].name + + def embed(self, image_bgr: np.ndarray) -> np.ndarray: + tensor = arcface_preprocess(image_bgr) + out = self.session.run(None, {self.input_name: tensor})[0] + return out.squeeze() + + +def similarity_to_confidence( + cos_sim: float, + median: float = 0.3, + range_width: float = 0.6, + slope_factor: float = 12, +) -> float: + slope = slope_factor / range_width + return float(1.0 / (1.0 + np.exp(-slope * (cos_sim - median)))) + + +def laplacian_variance(image: np.ndarray) -> float: + return float(cv2.Laplacian(image, cv2.CV_64F).var()) + + +def blur_reduction(variance: float) -> float: + if variance < 120: + return 0.06 + elif variance < 160: + return 0.04 + elif variance < 200: + return 0.02 + elif variance < 250: + return 0.01 + return 0.0 + + +def cosine(a: np.ndarray, b: np.ndarray) -> float: + denom = np.linalg.norm(a) * np.linalg.norm(b) + if denom == 0: + return 0.0 + return float(np.dot(a, b) / denom) + + +def l2(v: np.ndarray) -> np.ndarray: + return v / (np.linalg.norm(v) + 1e-9) + + +# --------------------------------------------------------------------------- +# Sample loading +# --------------------------------------------------------------------------- + + +@dataclass +class FaceSample: + path: str + shape: tuple[int, int] + embedding: np.ndarray + blur_var: float + align_info: dict + + +def load_folder( + folder: str, aligner: LandmarkAligner, embedder: ArcFaceEmbedder +) -> list[FaceSample]: + samples: list[FaceSample] = [] + names = sorted(os.listdir(folder)) + for name in names: + if name.startswith("."): + continue + path = os.path.join(folder, name) + if not os.path.isfile(path): + continue + img = cv2.imread(path) + if img is None: + print(f" [skip unreadable] {name}") + continue + aligned, info = aligner.align(img, img.shape[1], img.shape[0]) + emb = embedder.embed(aligned) + samples.append( + FaceSample( + path=path, + shape=(img.shape[1], img.shape[0]), + embedding=emb, + blur_var=laplacian_variance(img), + align_info=info, + ) + ) + return samples + + +def trimmed_mean(embs: Iterable[np.ndarray], trim: float = 0.15) -> np.ndarray: + arr = np.stack(list(embs), axis=0) + return stats.trim_mean(arr, trim, axis=0) + + +# --------------------------------------------------------------------------- +# Baseline analyses (always run) +# --------------------------------------------------------------------------- + + +def summarize_positive(samples: list[FaceSample], mean_emb: np.ndarray) -> None: + """Summary of training set: per-sample cos to class mean, intra-class stats. + + Outliers with cos far below the rest are likely degrading the mean — + they'd be the first candidates the shipped vector-outlier filter drops. + """ + print("\n" + "=" * 78) + print(f"POSITIVE SET ANALYSIS ({len(samples)} images)") + print("=" * 78) + + rows = [] + for s in samples: + cs = cosine(s.embedding, mean_emb) + conf = similarity_to_confidence(cs) + red = blur_reduction(s.blur_var) + rows.append( + dict( + name=os.path.basename(s.path), + shape=f"{s.shape[0]}x{s.shape[1]}", + eye_px=s.align_info["eye_dist_px"], + angle=s.align_info["angle"] + 180, + blur=s.blur_var, + cos=cs, + conf=conf, + red=red, + adj_conf=max(0.0, conf - red), + ) + ) + + rows.sort(key=lambda r: r["cos"]) + sims = np.array([r["cos"] for r in rows]) + print( + f"\nCosine-to-trimmed-mean: mean={sims.mean():.3f} std={sims.std():.3f} " + f"min={sims.min():.3f} max={sims.max():.3f}" + ) + + print("\n-- Worst matches (bottom 10, most likely hurting the mean) --") + print( + f"{'cos':>6} {'conf':>6} {'blur':>7} {'eyes':>6} " + f"{'angle':>6} {'shape':>9} name" + ) + for r in rows[:10]: + print( + f"{r['cos']:6.3f} {r['conf']:6.3f} {r['blur']:7.1f} " + f"{r['eye_px']:6.1f} {r['angle']:6.1f} {r['shape']:>9} {r['name']}" + ) + + print("\n-- Best matches (top 5) --") + for r in rows[-5:][::-1]: + print( + f"{r['cos']:6.3f} {r['conf']:6.3f} {r['blur']:7.1f} " + f"{r['eye_px']:6.1f} {r['angle']:6.1f} {r['shape']:>9} {r['name']}" + ) + + # Pairwise analysis — flags embeddings poorly correlated with the rest + print("\n-- Pairwise intra-class similarity (mean cos vs. other positives) --") + embs = np.stack([s.embedding for s in samples], axis=0) + norms = embs / (np.linalg.norm(embs, axis=1, keepdims=True) + 1e-9) + sim_matrix = norms @ norms.T + np.fill_diagonal(sim_matrix, np.nan) + mean_pairwise = np.nanmean(sim_matrix, axis=1) + names = [os.path.basename(s.path) for s in samples] + ordered = sorted(zip(names, mean_pairwise), key=lambda t: t[1]) + print(f"{'mean_cos':>9} name") + for nm, mp in ordered[:10]: + print(f"{mp:9.3f} {nm}") + print(f"\n overall mean pairwise cos: {np.nanmean(sim_matrix):.3f}") + print(f" median pairwise cos: {np.nanmedian(sim_matrix):.3f}") + + +def summarize_negative( + neg_samples: list[FaceSample], + mean_emb: np.ndarray, + pos_samples: list[FaceSample], +) -> None: + """Score each negative against the class mean, then show its top-3 + nearest positives. High-scoring negatives that match specific outlier + positives hint at training-set contamination. + """ + print("\n" + "=" * 78) + print(f"NEGATIVE SET ANALYSIS ({len(neg_samples)} images)") + print("=" * 78) + print( + f"\n{'cos':>6} {'conf':>6} {'red':>5} {'adj':>5} " + f"{'blur':>7} {'eyes':>6} {'shape':>9} name" + ) + for s in neg_samples: + cs = cosine(s.embedding, mean_emb) + conf = similarity_to_confidence(cs) + red = blur_reduction(s.blur_var) + print( + f"{cs:6.3f} {conf:6.3f} {red:5.2f} {max(0, conf - red):5.2f} " + f"{s.blur_var:7.1f} {s.align_info['eye_dist_px']:6.1f} " + f"{s.shape[0]}x{s.shape[1]:<5} {os.path.basename(s.path)}" + ) + + print("\n-- For each negative, top-3 most similar positives --") + pos_embs = np.stack([p.embedding for p in pos_samples]) + pos_norm = pos_embs / (np.linalg.norm(pos_embs, axis=1, keepdims=True) + 1e-9) + for s in neg_samples: + v = s.embedding / (np.linalg.norm(s.embedding) + 1e-9) + sims = pos_norm @ v + idx = np.argsort(-sims)[:3] + print(f"\n {os.path.basename(s.path)}:") + for i in idx: + print( + f" {sims[i]:6.3f} {os.path.basename(pos_samples[i].path)} " + f"blur={pos_samples[i].blur_var:.1f} " + f"eyes={pos_samples[i].align_info['eye_dist_px']:.1f}" + ) + + +# --------------------------------------------------------------------------- +# Optional diagnostics +# --------------------------------------------------------------------------- + + +def vector_outlier_test( + pos: list[FaceSample], neg: list[FaceSample], base_trim: float = 0.15 +) -> None: + """Measure the shipped vector-wise outlier filter at various thresholds. + + The production filter at `build_class_mean` in + frigate/data_processing/common/face/model.py uses T=0.30. This test + sweeps T so you can see which images would be dropped on a new collection + and how that affects the negative scores. + + Algorithm: iteratively recompute trim_mean on the kept set, drop any + embedding with cos < T to that mean, repeat until converged. Floor at + 50% of the collection to avoid collapse. + """ + print("\n" + "=" * 78) + print("VECTOR-WISE OUTLIER PRE-FILTER — layered on trim_mean(0.15)") + print("=" * 78) + + all_embs = np.stack([s.embedding for s in pos]) + + def iterative_mean( + embs: np.ndarray, + threshold: float, + iters: int = 3, + min_keep_frac: float = 0.5, + ) -> tuple[np.ndarray, np.ndarray]: + keep = np.ones(len(embs), dtype=bool) + floor = max(5, int(np.ceil(min_keep_frac * len(embs)))) + for _ in range(iters): + m = stats.trim_mean(embs[keep], base_trim, axis=0) + m_norm = m / (np.linalg.norm(m) + 1e-9) + e_norms = embs / (np.linalg.norm(embs, axis=1, keepdims=True) + 1e-9) + cos_to_mean = e_norms @ m_norm + new_keep = cos_to_mean >= threshold + if new_keep.sum() < floor: + top_idx = np.argsort(-cos_to_mean)[:floor] + new_keep = np.zeros_like(new_keep) + new_keep[top_idx] = True + if np.array_equal(new_keep, keep): + break + keep = new_keep + final = stats.trim_mean(embs[keep], base_trim, axis=0) + return final, keep + + provisional = stats.trim_mean(all_embs, base_trim, axis=0) + p_norm = provisional / (np.linalg.norm(provisional) + 1e-9) + e_norms_all = all_embs / (np.linalg.norm(all_embs, axis=1, keepdims=True) + 1e-9) + cos_to_prov = e_norms_all @ p_norm + print("\nDistribution of cos(positive, provisional trim_mean):") + print( + f" min={cos_to_prov.min():.3f} p10={np.percentile(cos_to_prov, 10):.3f} " + f"p25={np.percentile(cos_to_prov, 25):.3f} " + f"median={np.median(cos_to_prov):.3f} " + f"p75={np.percentile(cos_to_prov, 75):.3f} max={cos_to_prov.max():.3f}" + ) + + baseline_mean = stats.trim_mean(all_embs, base_trim, axis=0) + baseline_pos = np.array([cosine(p.embedding, baseline_mean) for p in pos]) + baseline_neg = ( + np.array([cosine(n.embedding, baseline_mean) for n in neg]) + if neg + else np.array([]) + ) + baseline_conf_neg = np.array( + [similarity_to_confidence(c) for c in baseline_neg] + ) + + print( + f"\nBaseline (trim_mean only, {len(pos)} images):" + f"\n pos cos min={baseline_pos.min():.3f} " + f"mean={baseline_pos.mean():.3f} max={baseline_pos.max():.3f}" + ) + if len(neg): + print( + f" neg cos min={baseline_neg.min():.3f} " + f"mean={baseline_neg.mean():.3f} max={baseline_neg.max():.3f}" + ) + print( + f" neg conf min={baseline_conf_neg.min():.3f} " + f"mean={baseline_conf_neg.mean():.3f} max={baseline_conf_neg.max():.3f}" + ) + print( + f" margin (pos.min - neg.max): " + f"{baseline_pos.min() - baseline_neg.max():+.3f}" + ) + + print("\nIterative (refine mean → drop vectors with cos5} {'kept':>6} {'pos min':>7} {'pos mean':>8} " + f"{'neg max':>7} {'neg mean':>8} {'neg conf.max':>12} {'margin':>7}" + ) + for T in [0.15, 0.20, 0.25, 0.28, 0.30, 0.33, 0.36, 0.40]: + mean, keep = iterative_mean(all_embs, T) + pos_sims = np.array([cosine(p.embedding, mean) for p in pos]) + neg_sims = ( + np.array([cosine(n.embedding, mean) for n in neg]) + if neg + else np.array([]) + ) + neg_conf = np.array([similarity_to_confidence(c) for c in neg_sims]) + margin = pos_sims.min() - (neg_sims.max() if len(neg_sims) else 0) + print( + f"{T:5.2f} {int(keep.sum()):>3}/{len(pos):<2} " + f"{pos_sims.min():7.3f} {pos_sims.mean():8.3f} " + f"{neg_sims.max() if len(neg_sims) else float('nan'):7.3f} " + f"{neg_sims.mean() if len(neg_sims) else float('nan'):8.3f} " + f"{neg_conf.max() if len(neg_conf) else float('nan'):12.3f} " + f"{margin:+7.3f}" + ) + + # Show which images get dropped at the shipped threshold + neighbors + for T_show in (0.25, 0.30, 0.33): + _, keep = iterative_mean(all_embs, T_show) + print( + f"\nAt T={T_show}, the {int((~keep).sum())} dropped positives are:" + ) + final_mean = stats.trim_mean(all_embs[keep], base_trim, axis=0) + m_n = final_mean / (np.linalg.norm(final_mean) + 1e-9) + for i, (p, k) in enumerate(zip(pos, keep)): + if not k: + e_n = p.embedding / (np.linalg.norm(p.embedding) + 1e-9) + cos_final = float(e_n @ m_n) + print( + f" cos_to_clean_mean={cos_final:6.3f} " + f"shape={p.shape[0]}x{p.shape[1]} " + f"eyes={p.align_info['eye_dist_px']:6.1f} " + f"blur={p.blur_var:7.1f} " + f"{os.path.basename(p.path)}" + ) + + +def degenerate_embedding_test( + pos: list[FaceSample], neg: list[FaceSample] +) -> None: + """Detect whether negatives and low-quality positives share a degenerate + 'tiny/noisy face' region of the embedding space. + + Signal: if neg-to-neg cos is higher than pos-to-pos cos, the negatives + aren't really per-identity embeddings — they're dominated by upsample / + low-resolution artifacts that all map to a similar corner of embedding + space regardless of who the face belongs to. + + Also rebuilds the mean using only high-intra-similarity positives to + show whether a cleaner training set separates the negatives. + """ + print("\n" + "=" * 78) + print("DEGENERATE-EMBEDDING TEST") + print("=" * 78) + + pos_embs = np.stack([l2(s.embedding) for s in pos]) + neg_embs = np.stack([l2(s.embedding) for s in neg]) + + nn = neg_embs @ neg_embs.T + np.fill_diagonal(nn, np.nan) + pp = pos_embs @ pos_embs.T + np.fill_diagonal(pp, np.nan) + pn = pos_embs @ neg_embs.T + + print( + f"\n neg<->neg mean cos : {np.nanmean(nn):.3f} " + f"(how tightly negatives cluster together)" + ) + print( + f" pos<->pos mean cos : {np.nanmean(pp):.3f} " + f"(how tightly positives cluster)" + ) + print( + f" pos<->neg mean cos : {pn.mean():.3f} " + f"(cross-class — should be low for a clean class)" + ) + if np.nanmean(nn) > np.nanmean(pp): + print( + "\n >> neg<->neg > pos<->pos: negatives cluster more tightly than\n" + " positives. This is the degenerate-embedding signature —\n" + " upsampled tiny crops share a common 'face-like blob' region\n" + " regardless of identity." + ) + + mean_intra = np.nanmean(pp, axis=1) + for thresh in (0.30, 0.33, 0.36): + keep = mean_intra >= thresh + if keep.sum() < 5: + continue + clean_embs = [pos[i].embedding for i in range(len(pos)) if keep[i]] + clean_mean = stats.trim_mean(np.stack(clean_embs), 0.15, axis=0) + neg_scores = np.array([cosine(n.embedding, clean_mean) for n in neg]) + neg_confs = np.array([similarity_to_confidence(c) for c in neg_scores]) + pos_scores = np.array( + [ + cosine(pos[i].embedding, clean_mean) + for i in range(len(pos)) + if keep[i] + ] + ) + print( + f"\n mean_intra >= {thresh}: keeping {int(keep.sum())}/{len(pos)} positives" + ) + print( + f" pos cos vs mean : min={pos_scores.min():.3f} " + f"mean={pos_scores.mean():.3f} max={pos_scores.max():.3f}" + ) + print( + f" neg cos vs mean : min={neg_scores.min():.3f} " + f"mean={neg_scores.mean():.3f} max={neg_scores.max():.3f}" + ) + print( + f" neg conf : min={neg_confs.min():.3f} " + f"mean={neg_confs.mean():.3f} max={neg_confs.max():.3f}" + ) + print( + f" margin (pos.min - neg.max): " + f"{pos_scores.min() - neg_scores.max():+.3f}" + ) + + +def contamination_analysis( + pos: list[FaceSample], neg: list[FaceSample] +) -> None: + """Check whether the positive collection contains a second identity. + + Two signals: + (a) Per-positive: if an image is closer to at least one negative than + to the rest of the positive class, it's likely a mislabeled face. + (b) 2-means split of the positive embeddings: if one cluster center + lands close to the negative mean, that cluster is a contaminating + sub-identity that's pulling the class mean toward the negatives. + """ + print("\n" + "=" * 78) + print("CONTAMINATION ANALYSIS") + print("=" * 78) + + pos_embs = np.stack([l2(s.embedding) for s in pos]) + neg_embs = np.stack([l2(s.embedding) for s in neg]) + pos_names = [os.path.basename(s.path) for s in pos] + + pos_pos = pos_embs @ pos_embs.T + np.fill_diagonal(pos_pos, np.nan) + pos_neg = pos_embs @ neg_embs.T + + mean_intra = np.nanmean(pos_pos, axis=1) + max_to_neg = pos_neg.max(axis=1) + mean_to_neg = pos_neg.mean(axis=1) + + print( + "\nPositives closer to a negative than to their own class avg" + "\n(these are candidates for mislabeled images):" + ) + print( + f"\n{'max_neg':>7} {'mean_neg':>8} {'mean_intra':>10} " + f"{'delta':>6} name" + ) + rows = list(zip(pos_names, max_to_neg, mean_to_neg, mean_intra)) + rows.sort(key=lambda r: -(r[1] - r[3])) + for nm, mxn, mnn, mi in rows[:15]: + delta = mxn - mi + marker = " <<" if delta > 0 else "" + print(f"{mxn:7.3f} {mnn:8.3f} {mi:10.3f} {delta:6.3f} {nm}{marker}") + + # 2-means in cosine space (no sklearn dependency). + print("\n2-means split of positive embeddings (cosine space):") + rng = np.random.default_rng(0) + best = None + for _ in range(5): + idx = rng.choice(len(pos_embs), 2, replace=False) + centers = pos_embs[idx].copy() + for _ in range(50): + sims = pos_embs @ centers.T + labels = np.argmax(sims, axis=1) + new_centers = np.stack( + [ + l2(pos_embs[labels == k].mean(axis=0)) + if np.any(labels == k) + else centers[k] + for k in range(2) + ] + ) + if np.allclose(new_centers, centers): + break + centers = new_centers + tight = float(np.mean([sims[i, labels[i]] for i in range(len(labels))])) + if best is None or tight > best[0]: + best = (tight, labels.copy(), centers.copy()) + + _, labels, centers = best + sizes = [int((labels == k).sum()) for k in range(2)] + neg_mean = l2(neg_embs.mean(axis=0)) + print( + f" cluster 0: size={sizes[0]:>2} " + f"center<->other_center_cos={float(centers[0] @ centers[1]):.3f} " + f"center<->neg_mean_cos={float(centers[0] @ neg_mean):.3f}" + ) + print( + f" cluster 1: size={sizes[1]:>2} " + f"center<->neg_mean_cos={float(centers[1] @ neg_mean):.3f}" + ) + + neg_aligned = 0 if centers[0] @ neg_mean > centers[1] @ neg_mean else 1 + print( + f"\n cluster {neg_aligned} is more similar to the negatives — " + f"its members are the contamination candidates:" + ) + for i, lbl in enumerate(labels): + if lbl == neg_aligned: + print( + f" max_to_neg={max_to_neg[i]:.3f} " + f"mean_intra={mean_intra[i]:.3f} {pos_names[i]}" + ) + + keep_mask = labels != neg_aligned + if keep_mask.sum() >= 3: + clean_embs = [pos[i].embedding for i in range(len(pos)) if keep_mask[i]] + clean_mean = stats.trim_mean(np.stack(clean_embs), 0.15, axis=0) + print( + f"\n Rebuilding class mean from the OTHER cluster " + f"({keep_mask.sum()} images):" + ) + print(f" {'cos':>6} {'conf':>6} name") + for n in neg: + cs = cosine(n.embedding, clean_mean) + cf = similarity_to_confidence(cs) + print(f" {cs:6.3f} {cf:6.3f} {os.path.basename(n.path)}") + + +# --------------------------------------------------------------------------- +# main +# --------------------------------------------------------------------------- + + +def main() -> int: + ap = argparse.ArgumentParser( + description="Analyze a face recognition collection outside Frigate.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + ap.add_argument("--positive", required=True, help="Training folder for one identity") + ap.add_argument( + "--negative", + default=None, + help="Runtime-crop folder to score against (optional)", + ) + ap.add_argument( + "--model-cache", + default="/config/model_cache", + help="Directory containing facedet/arcface.onnx and facedet/landmarkdet.yaml", + ) + ap.add_argument( + "--trim", + type=float, + default=0.15, + help="trim_mean proportion (Frigate uses 0.15)", + ) + ap.add_argument( + "--vector-outlier", + action="store_true", + help="Sweep the vector-wise outlier filter threshold", + ) + ap.add_argument( + "--degenerate", + action="store_true", + help="Test whether negatives share a degenerate embedding region", + ) + ap.add_argument( + "--contamination", + action="store_true", + help="Check whether the positive folder contains a second identity", + ) + args = ap.parse_args() + + arcface_path = os.path.join(args.model_cache, "facedet", "arcface.onnx") + landmark_path = os.path.join(args.model_cache, "facedet", "landmarkdet.yaml") + for p in (arcface_path, landmark_path): + if not os.path.exists(p): + print(f"ERROR: model file not found: {p}") + return 1 + + print(f"Loading ArcFace from {arcface_path}") + embedder = ArcFaceEmbedder(arcface_path) + print(f"Loading landmark model from {landmark_path}") + aligner = LandmarkAligner(landmark_path) + + print(f"\nLoading positives from {args.positive} ...") + pos = load_folder(args.positive, aligner, embedder) + print(f" {len(pos)} positives loaded") + + neg: list[FaceSample] = [] + if args.negative: + print(f"\nLoading negatives from {args.negative} ...") + neg = load_folder(args.negative, aligner, embedder) + print(f" {len(neg)} negatives loaded") + + if not pos: + print("no positive samples — aborting") + return 1 + + mean_emb = trimmed_mean([s.embedding for s in pos], trim=args.trim) + summarize_positive(pos, mean_emb) + if neg: + summarize_negative(neg, mean_emb, pos) + + if args.vector_outlier: + vector_outlier_test(pos, neg, args.trim) + if args.degenerate and neg: + degenerate_embedding_test(pos, neg) + if args.contamination and neg: + contamination_analysis(pos, neg) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/testing-scripts/object_dataset.py b/testing-scripts/object_dataset.py new file mode 100644 index 0000000..dd4071c --- /dev/null +++ b/testing-scripts/object_dataset.py @@ -0,0 +1,1022 @@ +""" +Object classification investigation script. + +Standalone replica of Frigate's custom object classification inference pipeline +(see frigate/data_processing/real_time/custom_classification.py and +frigate/util/classification.py) for analyzing a training dataset outside the +running service. Useful for: + + - Diagnosing why a class produces false positives / misidentifications + - Finding the training images that the deployed model itself misclassifies + (these are the worst offenders — usually mislabeled or low-quality crops) + - Inspecting borderline-correct images that sit near the decision boundary + - Spotting class-pair confusion (which classes get mixed up) + +Layout: + - Core pipeline: load_tflite, preprocess_for_inference, classify_image — + all mirroring CustomObjectClassificationProcessor exactly + - Default run: scan the dataset, classify every image with the deployed + model.tflite, report misclassified + borderline images per class, and + print a confusion matrix + - Optional diagnostics (flags): image-quality breakdown, scoring an + unlabeled "negative" folder, cross-class contamination analysis (find + training images in class A that visually look like class B and pull + inference toward A), and copying worst offenders out for review + +Recommended workflow when troubleshooting misclassifications: + + 1. Run the basic scan first (no extra flags). Read top-down: + - Class balance ratio. If > 3x, balance counts before anything else. + The dominant class will absorb borderline predictions otherwise. + - Per-class accuracy. Any class < 50% needs attention. + - Confusion matrix. If multiple classes all over-predict the same + class (e.g. Buddy->Rex, Bailey->Rex, none->Rex), you have + feature collapse, not "a few bad photos." Don't bother with + contamination analysis yet — fix the collapse first. + + 2. Check for "degenerate blob" upsampling. Look at the SHAPE column on + worst-offender rows. If most misclassified crops are < 80x80, the + small originals are being stretched 3-7x to fit the 224x224 model + input. Upsampled crops collapse to a similar region of feature space + regardless of identity — the model can't tell them apart and defaults + them to whichever class has the most of them. + + Fix: quarantine every image where min(w, h) < 80 (or 100 for a + stricter cut) and retrain. This works when the named class has + plenty of non-small examples to fall back on AND the small crops + are mostly degenerate blobs (target unrecognizable at that size). + + CAVEAT — sometimes small crops ARE the signal, not the noise: if + your target naturally appears small at the camera distance (cats + indoors, distant subjects, wide-FOV setups), the small crops in + the named class ARE the typical inference-time input. Removing them + leaves the model unable to recognize the target at its natural + detection size, and accuracy on the named class collapses after + retraining. If that happens — named-class accuracy drops sharply + after size cut + retrain — restore the quarantine and switch to + visual review of just the misclassified small crops instead of + bulk size filtering. The size threshold is a tool for "tons of + accidental tiny blobs polluting a class with otherwise large + examples," not a universal cleanup. + + 3. Verify the "none" class exists and is healthy. Without a strong + "none" class, every unknown crop at inference gets forced into one of + your real classes — the model has no "I don't know" option. Aim for: + - Count similar to your other classes (don't let it be the smallest) + - Images >= 100x100, well-framed + - Visual variety: other dogs/objects, partial views, empty scenes, + not just one type of negative + + 4. Look for cross-class duplicates from the same Frigate event. If the + same timestamp prefix appears across multiple class folders (e.g. + "1772052999.x" present in Buddy AND Bailey AND Rex AND none), those + crops came from one moment in time. Either they were extracted from a + multi-object frame and labeled inconsistently, or they're near- + duplicates of one scene cropped slightly differently. Inspect them as + a group and decide together. + + 5. Only after (1)-(4) are clean, run --confuses : for + targeted contamination analysis. The "ringleaders" section at the + bottom is the actionable part: a short list of images appearing + repeatedly as nearest neighbors of the wrong class. Those are the + few photos doing most of the damage. + + 6. Stop deleting when the contamination delta column shows ALL negative + values for the source class. That means dataset images in + are already visually distinct from in fixed-backbone + embedding space — the trained model just hasn't learned to use that + separation. The fix from that point is to ADD more training data for + the underperforming class, not delete more. Aim for at least 20 well- + framed images per class. + +The dataset must be the same layout Frigate trains from: + //dataset//*.{webp,png,jpg,jpeg} + +The model must already be trained: + //model.tflite + //labelmap.txt + +Command-line examples (mirror the workflow steps above): + + One-time setup — download the ImageNet-pretrained MobileNetV2 backbone + that --confuses uses for model-independent embeddings: + + curl -L -o /config/model_cache/mobilenetv2-7.onnx \\ + https://github.com/onnx/models/raw/main/validated/vision/classification/mobilenet/model/mobilenetv2-7.onnx + + Step 1 — Basic scan. Always start here. Reads class balance, accuracy, + confusion matrix, and per-class worst offenders: + + python3 object_dataset.py --name "" --top-n 25 + + Step 2 — Same scan plus image-quality stats (blur, brightness, aspect + distortion) for correct vs misclassified rows. Use when you suspect + systematic quality issues are driving the misses: + + python3 object_dataset.py --name "" --top-n 25 --quality + + Step 2 (cleanup) — Quarantine crops below 80x80 (the upsampling-blob + fix). Mirrors the class folder structure so individual images can be + restored. Change `threshold = 80` to 64 (looser) or 100 (stricter): + + python3 - <<'EOF' + import cv2 + from pathlib import Path + dataset = Path("/media/frigate/clips//dataset") + quarantine = Path("/media/frigate/clips//quarantine_small") + threshold = 80 + moved = 0 + for cls_dir in sorted(dataset.iterdir()): + if not cls_dir.is_dir(): + continue + for img_path in sorted(cls_dir.iterdir()): + if img_path.suffix.lower() not in (".png", ".jpg", ".jpeg", ".webp"): + continue + img = cv2.imread(str(img_path)) + if img is None: + continue + h, w = img.shape[:2] + if min(h, w) < threshold: + dest_dir = quarantine / cls_dir.name + dest_dir.mkdir(parents=True, exist_ok=True) + img_path.rename(dest_dir / img_path.name) + moved += 1 + print(f"moved {moved} images") + EOF + + Revert any quarantine directory (puts everything back into dataset/): + + python3 - <<'EOF' + from pathlib import Path + quarantine = Path("/media/frigate/clips//quarantine_small") + dataset = Path("/media/frigate/clips//dataset") + for cls_dir in sorted(quarantine.iterdir()): + if not cls_dir.is_dir(): + continue + target = dataset / cls_dir.name + target.mkdir(parents=True, exist_ok=True) + for img_path in sorted(cls_dir.iterdir()): + img_path.rename(target / img_path.name) + EOF + + Step 4 — Inspect a same-timestamp cluster across all classes (replace + TIMESTAMP with the prefix you saw in worst-offenders, e.g. "1772052999"): + + mkdir -p /tmp/timestamp_cluster + cd "/media/frigate/clips//dataset" + for f in */*TIMESTAMP*; do + cls=$(dirname "$f"); fn=$(basename "$f") + cp "$f" "/tmp/timestamp_cluster/${cls}__${fn}" + done + + Step 5 — Cross-class contamination. Lists specific images that + look like , plus a ringleader summary of the few worst offenders. + Also copies all misclassified images into a flat browse-able folder + bucketed by (true_class)__as__(predicted_class): + + python3 object_dataset.py --name "" \\ + --embedding-model /config/model_cache/mobilenetv2-7.onnx \\ + --confuses Rex:Buddy --top-n 15 \\ + --save-misclassified /tmp/_offenders + + Or let the script pick the worst-confused class pair from the matrix: + + python3 object_dataset.py --name "" \\ + --embedding-model /config/model_cache/mobilenetv2-7.onnx \\ + --confuses auto + + Score an unlabeled folder of runtime crops against the trained model — + useful for analyzing why specific inference-time misfires happened. + Prints full per-class probability vectors and threshold-pass status: + + python3 object_dataset.py --name "" \\ + --negative /path/to/runtime_misfires --threshold 0.8 + +Full flag reference: + python3 object_dataset.py \\ + --name \\ + [--clips-dir /media/frigate/clips] \\ + [--model-cache /config/model_cache] \\ + [--threshold 0.8] [--top-n 15] \\ + [--quality] [--negative ] [--save-misclassified ] \\ + [--confuses :] [--embedding-model ] +""" + +from __future__ import annotations + +import argparse +import os +import shutil +import sys +from dataclasses import dataclass + +import cv2 +import numpy as np + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from ai_edge_litert.interpreter import Interpreter + +CLASSIFIER_INPUT_SIZE = 224 +IMAGE_EXTS = (".webp", ".png", ".jpg", ".jpeg") + + +# --------------------------------------------------------------------------- +# Replicated Frigate pipeline +# --------------------------------------------------------------------------- + + +def load_tflite(model_path: str) -> tuple[Interpreter, list[dict], list[dict]]: + """Mirror CustomObjectClassificationProcessor.__build_detector.""" + interpreter = Interpreter(model_path=model_path, num_threads=2) + interpreter.allocate_tensors() + return ( + interpreter, + interpreter.get_input_details(), + interpreter.get_output_details(), + ) + + +def load_labelmap(path: str) -> dict[int, str]: + """Mirror util.builtin.load_labels(prefill=0, indexed=False).""" + with open(path, "r", encoding="utf-8") as f: + lines = [line.strip() for line in f.readlines() if line.strip()] + return {idx: line for idx, line in enumerate(lines)} + + +def preprocess_for_inference(image_bgr: np.ndarray) -> np.ndarray: + """Mirror the inference preprocessing in process_frame. + + Frigate decodes the camera frame YUV->RGB, crops, then cv2.resize to + 224x224, and passes the uint8 array directly to the int8-quantized + interpreter. On disk we read BGR via cv2.imread, so we must convert + to RGB to match the channel order the model was trained on. + """ + rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) + resized = cv2.resize(rgb, (CLASSIFIER_INPUT_SIZE, CLASSIFIER_INPUT_SIZE)) + return resized + + +class MobileNetEmbedder: + """ImageNet-pretrained MobileNetV2 backbone via cv2.dnn. + + Used as a model-independent visual embedder for cross-class contamination + analysis. The user's trained classifier may have memorized contaminating + training images and place them inside the right class in its own embedding + space — a fixed external backbone keeps the analysis honest. + + Expects the standard ONNX Model Zoo MobileNetV2-7 file (PyTorch-style + preprocessing: ImageNet mean/std on /255 input). Output is 1000-d ImageNet + logits; L2-normalized for cosine-similarity comparisons. + """ + + IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32) + IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32) + + def __init__(self, model_path: str): + if not os.path.exists(model_path): + raise FileNotFoundError(model_path) + self.net = cv2.dnn.readNetFromONNX(model_path) + + def embed(self, image_bgr: np.ndarray) -> np.ndarray: + rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) + resized = cv2.resize(rgb, (224, 224)).astype(np.float32) / 255.0 + normalized = (resized - self.IMAGENET_MEAN) / self.IMAGENET_STD + # NHWC -> NCHW + blob = np.transpose(normalized, (2, 0, 1))[np.newaxis, :, :, :] + self.net.setInput(blob) + out = self.net.forward().squeeze().astype(np.float32) + norm = float(np.linalg.norm(out)) + return out / norm if norm > 0 else out + + +def classify_image( + interpreter: Interpreter, + input_details: list[dict], + output_details: list[dict], + image_bgr: np.ndarray, +) -> np.ndarray: + """Mirror _classify_object's tensor flow. + + Returns the per-class probability vector (length = num_classes) after + the exact `probs = res / res.sum(axis=0)` renormalization Frigate uses + on the int8-quantized output. + """ + resized = preprocess_for_inference(image_bgr) + tensor = np.expand_dims(resized, axis=0) + interpreter.set_tensor(input_details[0]["index"], tensor) + interpreter.invoke() + res = interpreter.get_tensor(output_details[0]["index"])[0].astype(np.float32) + total = res.sum(axis=0) + if total <= 0: + # Defensive: all zeros from a degenerate quantization step. + return np.full_like(res, 1.0 / len(res)) + return res / total + + +# --------------------------------------------------------------------------- +# Sample loading +# --------------------------------------------------------------------------- + + +@dataclass +class ImageSample: + path: str + true_label: str | None # None for the unlabeled negative folder + shape: tuple[int, int] + probs: np.ndarray + pred_idx: int + pred_label: str + pred_score: float + true_idx: int | None + true_score: float | None + + +def laplacian_variance(image_bgr: np.ndarray) -> float: + gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY) + return float(cv2.Laplacian(gray, cv2.CV_64F).var()) + + +def mean_brightness(image_bgr: np.ndarray) -> float: + return float(cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY).mean()) + + +def aspect_distortion(shape: tuple[int, int]) -> float: + """How far the crop is from square; |1 - max(w,h)/min(w,h)|. + + A wide or tall crop gets squashed to 224x224 by the inference resize, + which can be a hidden source of misclassification. + """ + w, h = shape + if w <= 0 or h <= 0: + return float("inf") + return float(max(w, h) / min(w, h) - 1.0) + + +def iter_dataset(dataset_dir: str) -> list[tuple[str, str]]: + """Yield (class_name, image_path) pairs from the dataset directory.""" + pairs: list[tuple[str, str]] = [] + if not os.path.isdir(dataset_dir): + return pairs + for cls in sorted(os.listdir(dataset_dir)): + cls_dir = os.path.join(dataset_dir, cls) + if not os.path.isdir(cls_dir) or cls.startswith("."): + continue + for name in sorted(os.listdir(cls_dir)): + if name.startswith("."): + continue + if not name.lower().endswith(IMAGE_EXTS): + continue + pairs.append((cls, os.path.join(cls_dir, name))) + return pairs + + +def classify_folder( + folder: str, + interpreter: Interpreter, + input_details: list[dict], + output_details: list[dict], + labelmap: dict[int, str], + label_to_idx: dict[str, int], + true_label: str | None = None, +) -> list[ImageSample]: + """Classify every image directly under `folder`. Used for the negative set.""" + samples: list[ImageSample] = [] + if not os.path.isdir(folder): + return samples + for name in sorted(os.listdir(folder)): + if name.startswith(".") or not name.lower().endswith(IMAGE_EXTS): + continue + path = os.path.join(folder, name) + img = cv2.imread(path) + if img is None: + print(f" [skip unreadable] {name}") + continue + probs = classify_image(interpreter, input_details, output_details, img) + pred_idx = int(np.argmax(probs)) + true_idx = label_to_idx.get(true_label) if true_label is not None else None + true_score = float(probs[true_idx]) if true_idx is not None else None + samples.append( + ImageSample( + path=path, + true_label=true_label, + shape=(img.shape[1], img.shape[0]), + probs=probs, + pred_idx=pred_idx, + pred_label=labelmap[pred_idx], + pred_score=float(probs[pred_idx]), + true_idx=true_idx, + true_score=true_score, + ) + ) + return samples + + +def classify_dataset( + dataset_dir: str, + interpreter: Interpreter, + input_details: list[dict], + output_details: list[dict], + labelmap: dict[int, str], + label_to_idx: dict[str, int], +) -> list[ImageSample]: + samples: list[ImageSample] = [] + pairs = iter_dataset(dataset_dir) + for cls, path in pairs: + img = cv2.imread(path) + if img is None: + print(f" [skip unreadable] {cls}/{os.path.basename(path)}") + continue + probs = classify_image(interpreter, input_details, output_details, img) + pred_idx = int(np.argmax(probs)) + true_idx = label_to_idx.get(cls) + true_score = float(probs[true_idx]) if true_idx is not None else None + samples.append( + ImageSample( + path=path, + true_label=cls, + shape=(img.shape[1], img.shape[0]), + probs=probs, + pred_idx=pred_idx, + pred_label=labelmap[pred_idx], + pred_score=float(probs[pred_idx]), + true_idx=true_idx, + true_score=true_score, + ) + ) + return samples + + +# --------------------------------------------------------------------------- +# Baseline analyses (always run) +# --------------------------------------------------------------------------- + + +def summarize_dataset(samples: list[ImageSample], labelmap: dict[int, str]) -> None: + """Per-class counts, accuracy, mean confidence on the true class.""" + print("\n" + "=" * 78) + print(f"DATASET OVERVIEW ({len(samples)} images)") + print("=" * 78) + + by_class: dict[str, list[ImageSample]] = {} + for s in samples: + by_class.setdefault(s.true_label or "", []).append(s) + + print( + f"\n{'class':<20} {'count':>6} {'acc':>6} {'mean_p_true':>12} " + f"{'min_p_true':>10} {'mislabeled':>11}" + ) + for cls in sorted(by_class): + rows = by_class[cls] + correct = sum(1 for r in rows if r.pred_label == cls) + mean_pt = ( + np.mean([r.true_score for r in rows if r.true_score is not None]) + if any(r.true_score is not None for r in rows) + else float("nan") + ) + min_pt = ( + np.min([r.true_score for r in rows if r.true_score is not None]) + if any(r.true_score is not None for r in rows) + else float("nan") + ) + acc = correct / len(rows) if rows else 0.0 + bad = len(rows) - correct + print( + f"{cls:<20} {len(rows):>6} {acc:>6.2%} {mean_pt:>12.3f} " + f"{min_pt:>10.3f} {bad:>11}" + ) + + # Class balance — large skew can hide poor minority-class accuracy in the totals. + counts = [len(by_class[c]) for c in by_class] + if counts: + print( + f"\nClass balance: min={min(counts)} max={max(counts)} " + f"ratio={max(counts) / max(1, min(counts)):.1f}x" + ) + + +def confusion_matrix(samples: list[ImageSample], labelmap: dict[int, str]) -> None: + print("\n" + "=" * 78) + print("CONFUSION MATRIX (rows = true class, cols = predicted class)") + print("=" * 78) + + classes = [labelmap[i] for i in sorted(labelmap)] + idx = {c: i for i, c in enumerate(classes)} + mat = np.zeros((len(classes), len(classes)), dtype=int) + for s in samples: + if s.true_label is None or s.true_label not in idx: + continue + mat[idx[s.true_label], s.pred_idx] += 1 + + col_w = max(8, max(len(c) for c in classes) + 1) + header = " " * (col_w + 2) + "".join(f"{c[: col_w - 1]:>{col_w}}" for c in classes) + print("\n" + header) + for i, cls in enumerate(classes): + row = "".join(f"{mat[i, j]:>{col_w}}" for j in range(len(classes))) + print(f" {cls[: col_w - 1]:<{col_w}}{row}") + + # Top class-pair confusions, in both directions. + pairs: list[tuple[str, str, int]] = [] + for i, src in enumerate(classes): + for j, dst in enumerate(classes): + if i != j and mat[i, j] > 0: + pairs.append((src, dst, int(mat[i, j]))) + pairs.sort(key=lambda r: -r[2]) + if pairs: + print("\nTop class-pair confusions:") + for src, dst, n in pairs[:10]: + print(f" {n:>4} {src} -> {dst}") + + +def worst_offenders( + samples: list[ImageSample], + labelmap: dict[int, str], + top_n: int, + quality: bool, +) -> list[ImageSample]: + """Print the worst-offender images grouped by class. + + Two buckets per class: + (a) Misclassified — predicted label differs from folder. Sorted by the + confidence in the WRONG class (highest first). These are the most + confidently wrong images, the strongest candidates for relabeling + or deletion. + (b) Borderline-correct — predicted label matches but p_true is low. + These sit near the decision boundary; they're not actively wrong + but they make the class harder to learn cleanly. + + Returns the union of (a) lists across classes, for optional copying. + """ + print("\n" + "=" * 78) + print(f"WORST OFFENDERS (top {top_n} per class)") + print("=" * 78) + + by_class: dict[str, list[ImageSample]] = {} + for s in samples: + if s.true_label is None: + continue + by_class.setdefault(s.true_label, []).append(s) + + all_misclassified: list[ImageSample] = [] + for cls in sorted(by_class): + rows = by_class[cls] + miscls = [r for r in rows if r.pred_label != cls] + miscls.sort(key=lambda r: -r.pred_score) + all_misclassified.extend(miscls[:top_n]) + + print(f"\n-- class '{cls}': {len(miscls)}/{len(rows)} misclassified --") + if miscls: + print( + f"{'p_pred':>7} {'pred':<18} {'p_true':>7} {'shape':>11}" + + (" blur bright aspect " if quality else " ") + + "name" + ) + for r in miscls[:top_n]: + shape = f"{r.shape[0]}x{r.shape[1]}" + extra = "" + if quality: + img = cv2.imread(r.path) + blur = laplacian_variance(img) if img is not None else float("nan") + bright = mean_brightness(img) if img is not None else float("nan") + aspect = aspect_distortion(r.shape) + extra = f" {blur:5.0f} {bright:6.1f} {aspect:6.2f} " + pt = r.true_score if r.true_score is not None else float("nan") + print( + f"{r.pred_score:7.3f} {r.pred_label:<18} " + f"{pt:7.3f} {shape:>11}{extra}{os.path.basename(r.path)}" + ) + + # Borderline-correct: labeled right but the model isn't confident. + correct = [r for r in rows if r.pred_label == cls and r.true_score is not None] + correct.sort(key=lambda r: r.true_score or 0.0) + borderline = correct[: max(5, top_n // 3)] + if borderline: + print("\n borderline-correct (lowest p_true while still labeled right):") + for r in borderline: + # Second-best class names the neighbor that's pulling on this image. + if len(r.probs) > 1: + second = int(np.argsort(-r.probs)[1]) + second_lbl = labelmap[second] + second_p = float(r.probs[second]) + else: + second_lbl = "-" + second_p = 0.0 + print( + f" p_true={r.true_score:.3f} " + f"p_2nd={second_p:.3f} ({second_lbl}) " + f"{os.path.basename(r.path)}" + ) + + return all_misclassified + + +# --------------------------------------------------------------------------- +# Optional diagnostics +# --------------------------------------------------------------------------- + + +def quality_summary(samples: list[ImageSample]) -> None: + """Compare image-quality stats for correct vs misclassified images. + + Helps answer: are the worst offenders systematically blurrier / darker / + more squashed than the rest of the class? If so, the fix is to tighten + the data-collection criteria, not just delete individual images. + """ + print("\n" + "=" * 78) + print("IMAGE QUALITY — correct vs misclassified") + print("=" * 78) + + rows: list[tuple[str, bool, float, float, float]] = [] + for s in samples: + if s.true_label is None: + continue + img = cv2.imread(s.path) + if img is None: + continue + blur = laplacian_variance(img) + bright = mean_brightness(img) + aspect = aspect_distortion(s.shape) + rows.append((s.true_label, s.pred_label == s.true_label, blur, bright, aspect)) + + if not rows: + print(" (no readable images)") + return + + correct = [r for r in rows if r[1]] + wrong = [r for r in rows if not r[1]] + + def stats(name: str, getter, group: list) -> None: + if not group: + print(f" {name:<14} (no samples)") + return + vals = np.array([getter(r) for r in group]) + print( + f" {name:<14} n={len(vals):>4} " + f"mean={vals.mean():8.2f} median={np.median(vals):8.2f} " + f"p10={np.percentile(vals, 10):8.2f} p90={np.percentile(vals, 90):8.2f}" + ) + + print("\nBlur (laplacian variance — higher = sharper):") + stats("correct", lambda r: r[2], correct) + stats("misclassified", lambda r: r[2], wrong) + print("\nBrightness (0..255):") + stats("correct", lambda r: r[3], correct) + stats("misclassified", lambda r: r[3], wrong) + print("\nAspect distortion (0 = square; higher = more squashed by 224x224):") + stats("correct", lambda r: r[4], correct) + stats("misclassified", lambda r: r[4], wrong) + + +def summarize_negative( + neg_samples: list[ImageSample], + threshold: float, + labelmap: dict[int, str], +) -> None: + """Score an unlabeled folder of runtime crops against the model. + + Equivalent to face_dataset.py's negative-set analysis: each image is + classified, and we print its full probability vector plus whether it + would clear the configured threshold. High-confidence predictions on + crops the user knows are wrong indicate the training set is leaking + a representative image into the wrong class. + """ + print("\n" + "=" * 78) + print(f"NEGATIVE SET ANALYSIS ({len(neg_samples)} images, threshold={threshold})") + print("=" * 78) + + classes = [labelmap[i] for i in sorted(labelmap)] + print(f"\n{'pass':>4} {'score':>6} {'pred':<18} full prob vector / name") + for s in neg_samples: + passes = "yes" if s.pred_score >= threshold else "no" + full = " ".join(f"{c}={float(s.probs[i]):.2f}" for i, c in enumerate(classes)) + print( + f"{passes:>4} {s.pred_score:6.3f} {s.pred_label:<18} " + f"{full} :: {os.path.basename(s.path)}" + ) + + +def pick_worst_confusion_pair( + samples: list[ImageSample], + labelmap: dict[int, str], +) -> tuple[int, str | None, str | None]: + """Return (count, source, target) for the most-confused class pair.""" + classes = [labelmap[i] for i in sorted(labelmap)] + pairs: list[tuple[int, str, str]] = [] + for src in classes: + for tgt in classes: + if src == tgt: + continue + n = sum(1 for s in samples if s.true_label == src and s.pred_label == tgt) + if n > 0: + pairs.append((n, src, tgt)) + pairs.sort(reverse=True) + return pairs[0] if pairs else (0, None, None) + + +def cross_class_contamination( + samples: list[ImageSample], + source_class: str, + target_class: str, + label_to_idx: dict[str, int], + embedder: MobileNetEmbedder, + top_n: int, +) -> None: + """Find training images in source_class that visually look like target_class. + + Generalizes face_dataset.py's contamination_analysis to N classes. Uses a + fixed ImageNet backbone (NOT the user's trained classifier) so that + contaminators which the trained model has memorized into the source class + still surface — the trained model's own embedding would hide them. + + Three sections: + 1. Source-image culprits ranked by `cos(img, target_centroid) - + cos(img, source_centroid)`. Positive delta = the image looks more + like the target class than its own class — prime relabeling + candidates. + 2. For each target image, the top-3 nearest source training images. + Shows the visual chain of confusion image-by-image. + 3. Ringleader summary: source images that appear most often as a top-3 + neighbor across the target set. These few photos are responsible + for the bulk of the confusion. + """ + src = [s for s in samples if s.true_label == source_class] + tgt = [s for s in samples if s.true_label == target_class] + + if not src or not tgt: + print( + f"\nERROR: need both classes populated; got {len(src)} " + f"'{source_class}' and {len(tgt)} '{target_class}'" + ) + return + + print("\n" + "=" * 78) + print( + f"CROSS-CLASS CONTAMINATION '{source_class}' leaning toward '{target_class}'" + ) + print(" (model-independent embeddings via ImageNet MobileNetV2)") + print("=" * 78) + + target_idx = label_to_idx.get(target_class) + source_idx = label_to_idx.get(source_class) + + print(f"\nEmbedding {len(src) + len(tgt)} images...") + src_embs = np.stack([embedder.embed(cv2.imread(s.path)) for s in src]) + tgt_embs = np.stack([embedder.embed(cv2.imread(s.path)) for s in tgt]) + + src_centroid = src_embs.mean(axis=0) + src_centroid /= np.linalg.norm(src_centroid) + 1e-9 + tgt_centroid = tgt_embs.mean(axis=0) + tgt_centroid /= np.linalg.norm(tgt_centroid) + 1e-9 + + src_to_src = src_embs @ src_centroid + src_to_tgt = src_embs @ tgt_centroid + delta = src_to_tgt - src_to_src + + print(f"\n-- '{source_class}' images sorted by '{target_class}'-likeness --") + print(f" positive delta = visually closer to '{target_class}' centroid") + print( + f" p_{target_class} = trained model's probability for '{target_class}' " + f"on this image\n" + ) + delta_label = "delta" + tgt_cos_label = f"cos_{target_class}"[:12] + src_cos_label = f"cos_{source_class}"[:12] + p_tgt_label = f"p_{target_class}"[:10] + print( + f" {delta_label:>7} {tgt_cos_label:>12} {src_cos_label:>12} " + f"{p_tgt_label:>10} name" + ) + order = np.argsort(-delta) + for i in order[:top_n]: + s = src[i] + p_tgt = float(s.probs[target_idx]) if target_idx is not None else float("nan") + print( + f" {delta[i]:+7.3f} {src_to_tgt[i]:12.3f} {src_to_src[i]:12.3f} " + f"{p_tgt:10.3f} {os.path.basename(s.path)}" + ) + + print(f"\n-- nearest '{source_class}' neighbors for each '{target_class}' image --") + neighbor_counts: dict[str, int] = {} + src_paths = [os.path.basename(s.path) for s in src] + for i, t in enumerate(tgt): + sims = src_embs @ tgt_embs[i] + top3 = np.argsort(-sims)[:3] + p_src = float(t.probs[source_idx]) if source_idx is not None else float("nan") + marker = " <5} name") + ranked = sorted(neighbor_counts.items(), key=lambda r: -r[1]) + for name, count in ranked[:top_n]: + print(f" {count:>5} {name}") + + +def save_misclassified(samples: list[ImageSample], out_dir: str) -> None: + """Copy misclassified images to /__as__/. + + Lets you browse the worst offenders in a file manager and bulk-delete or + relabel them without poking through the original dataset tree. + """ + print("\n" + "=" * 78) + print(f"SAVING MISCLASSIFIED IMAGES -> {out_dir}") + print("=" * 78) + count = 0 + for s in samples: + if s.true_label is None or s.pred_label == s.true_label: + continue + bucket = os.path.join(out_dir, f"{s.true_label}__as__{s.pred_label}") + os.makedirs(bucket, exist_ok=True) + score_tag = f"{int(round(s.pred_score * 100)):03d}" + dest = os.path.join(bucket, f"{score_tag}_{os.path.basename(s.path)}") + try: + shutil.copy2(s.path, dest) + count += 1 + except OSError as err: + print(f" [copy failed] {s.path}: {err}") + print(f" copied {count} images into {out_dir}") + + +# --------------------------------------------------------------------------- +# main +# --------------------------------------------------------------------------- + + +def main() -> int: + ap = argparse.ArgumentParser( + description=( + "Analyze a Frigate object-classification training dataset against its " + "deployed TFLite model." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + ap.add_argument( + "--name", + required=True, + help="Classification model name (matches the key in classification.custom.)", + ) + ap.add_argument( + "--clips-dir", + default="/media/frigate/clips", + help="Frigate clips directory; dataset is read from //dataset", + ) + ap.add_argument( + "--model-cache", + default="/config/model_cache", + help="Frigate model_cache; model is read from //model.tflite", + ) + ap.add_argument( + "--threshold", + type=float, + default=0.8, + help="Score threshold (matches model_config.threshold; default 0.8)", + ) + ap.add_argument( + "--top-n", + type=int, + default=15, + help="Worst-offender images to show per class", + ) + ap.add_argument( + "--quality", + action="store_true", + help="Include blur/brightness/aspect stats for correct vs misclassified", + ) + ap.add_argument( + "--negative", + default=None, + help="Score an unlabeled folder of crops against the model", + ) + ap.add_argument( + "--save-misclassified", + default=None, + help="Copy every misclassified image into this directory for review", + ) + ap.add_argument( + "--confuses", + default=None, + help=( + "Cross-class contamination analysis. Format ':', " + "e.g. 'rex:buddy' to find Rex training images that look like " + "Buddy. Use 'auto' to pick the worst pair from the confusion matrix. " + "Requires --embedding-model." + ), + ) + ap.add_argument( + "--embedding-model", + default=None, + help=( + "Path to ONNX MobileNetV2 file for model-independent embeddings " + "(required by --confuses). Download once with: curl -L -o " + "/config/model_cache/mobilenetv2-7.onnx https://github.com/onnx/" + "models/raw/main/validated/vision/classification/mobilenet/model/" + "mobilenetv2-7.onnx" + ), + ) + args = ap.parse_args() + + dataset_dir = os.path.join(args.clips_dir, args.name, "dataset") + model_path = os.path.join(args.model_cache, args.name, "model.tflite") + labelmap_path = os.path.join(args.model_cache, args.name, "labelmap.txt") + + for required in (dataset_dir, model_path, labelmap_path): + if not os.path.exists(required): + print(f"ERROR: required path not found: {required}") + return 1 + + print(f"Loading model from {model_path}") + interpreter, input_details, output_details = load_tflite(model_path) + labelmap = load_labelmap(labelmap_path) + label_to_idx = {v: k for k, v in labelmap.items()} + print(f" labels: {sorted(labelmap.values())}") + + print(f"\nScanning dataset at {dataset_dir} ...") + samples = classify_dataset( + dataset_dir, interpreter, input_details, output_details, labelmap, label_to_idx + ) + if not samples: + print("no images found — aborting") + return 1 + print(f" classified {len(samples)} images") + + summarize_dataset(samples, labelmap) + confusion_matrix(samples, labelmap) + misclassified = worst_offenders(samples, labelmap, args.top_n, args.quality) + + if args.quality: + quality_summary(samples) + + if args.negative: + print(f"\nLoading negatives from {args.negative} ...") + neg = classify_folder( + args.negative, + interpreter, + input_details, + output_details, + labelmap, + label_to_idx, + true_label=None, + ) + if neg: + summarize_negative(neg, args.threshold, labelmap) + + if args.confuses: + if not args.embedding_model: + print( + "\nERROR: --confuses requires --embedding-model (path to ONNX " + "MobileNetV2). See --help for the download command." + ) + return 1 + try: + embedder = MobileNetEmbedder(args.embedding_model) + except (FileNotFoundError, cv2.error) as err: + print(f"\nERROR: failed to load embedding model: {err}") + return 1 + + if args.confuses == "auto": + n, src, tgt = pick_worst_confusion_pair(samples, labelmap) + if src is None: + print( + "\nNo misclassifications in dataset — " + "nothing to investigate via --confuses auto" + ) + else: + print(f"\nAuto-picked worst confusion: {src} -> {tgt} ({n} cases)") + cross_class_contamination( + samples, src, tgt, label_to_idx, embedder, args.top_n + ) + else: + if ":" not in args.confuses: + print("\nERROR: --confuses expects ':' or 'auto'") + return 1 + src, tgt = args.confuses.split(":", 1) + if src not in label_to_idx or tgt not in label_to_idx: + print( + f"\nERROR: class names must be in the labelmap " + f"({sorted(label_to_idx)})" + ) + return 1 + cross_class_contamination( + samples, src, tgt, label_to_idx, embedder, args.top_n + ) + + if args.save_misclassified: + save_misclassified(misclassified, args.save_misclassified) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/testing-scripts/process_clip.py b/testing-scripts/process_clip.py new file mode 100644 index 0000000..6f474de --- /dev/null +++ b/testing-scripts/process_clip.py @@ -0,0 +1,321 @@ +import csv +import json +import logging +import multiprocessing as mp +import os +import subprocess as sp +import sys + +import click +import cv2 +import numpy as np + +sys.path.append("/workspace/frigate") + +from frigate.config import FrigateConfig # noqa: E402 +from frigate.motion import MotionDetector # noqa: E402 +from frigate.object_detection.base import LocalObjectDetector # noqa: E402 +from frigate.track.centroid_tracker import CentroidTracker # noqa: E402 +from frigate.track.object_processing import CameraState # noqa: E402 +from frigate.util import ( # noqa: E402 + EventsPerSecond, + SharedMemoryFrameManager, + draw_box_with_label, +) +from frigate.video import ( # noqa: E402 + capture_frames, + process_frames, + start_or_restart_ffmpeg, +) + +logging.basicConfig(level=logging.DEBUG) + +logger = logging.getLogger(__name__) + + +def get_frame_shape(source): + ffprobe_cmd = [ + "ffprobe", + "-v", + "panic", + "-show_error", + "-show_streams", + "-of", + "json", + source, + ] + p = sp.run(ffprobe_cmd, capture_output=True) + info = json.loads(p.stdout) + + video_info = [s for s in info["streams"] if s["codec_type"] == "video"][0] + + if video_info["height"] != 0 and video_info["width"] != 0: + return (video_info["height"], video_info["width"], 3) + + # fallback to using opencv if ffprobe didn't succeed + video = cv2.VideoCapture(source) + ret, frame = video.read() + frame_shape = frame.shape + video.release() + return frame_shape + + +class ProcessClip: + def __init__(self, clip_path, frame_shape, config: FrigateConfig): + self.clip_path = clip_path + self.camera_name = "camera" + self.config = config + self.camera_config = self.config.cameras["camera"] + self.frame_shape = self.camera_config.frame_shape + self.ffmpeg_cmd = [ + c["cmd"] for c in self.camera_config.ffmpeg_cmds if "detect" in c["roles"] + ][0] + self.frame_manager = SharedMemoryFrameManager() + self.frame_queue = mp.Queue() + self.detected_objects_queue = mp.Queue() + self.camera_state = CameraState(self.camera_name, config, self.frame_manager) + + def load_frames(self): + fps = EventsPerSecond() + skipped_fps = EventsPerSecond() + current_frame = mp.Value("d", 0.0) + frame_size = ( + self.camera_config.frame_shape_yuv[0] + * self.camera_config.frame_shape_yuv[1] + ) + ffmpeg_process = start_or_restart_ffmpeg( + self.ffmpeg_cmd, logger, sp.DEVNULL, frame_size + ) + capture_frames( + ffmpeg_process, + self.camera_name, + self.camera_config.frame_shape_yuv, + self.frame_manager, + self.frame_queue, + fps, + skipped_fps, + current_frame, + ) + ffmpeg_process.wait() + ffmpeg_process.communicate() + + def process_frames( + self, object_detector, objects_to_track=["person"], object_filters={} + ): + mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8) + mask[:] = 255 + motion_detector = MotionDetector(self.frame_shape, self.camera_config.motion) + motion_detector.save_images = False + + object_tracker = CentroidTracker(self.camera_config.detect) + process_info = { + "process_fps": mp.Value("d", 0.0), + "detection_fps": mp.Value("d", 0.0), + "detection_frame": mp.Value("d", 0.0), + } + + detection_enabled = mp.Value("d", 1) + motion_enabled = mp.Value("d", True) + stop_event = mp.Event() + + process_frames( + self.camera_name, + self.frame_queue, + self.frame_shape, + self.config.model, + self.camera_config.detect, + self.frame_manager, + motion_detector, + object_detector, + object_tracker, + self.detected_objects_queue, + process_info, + objects_to_track, + object_filters, + detection_enabled, + motion_enabled, + stop_event, + exit_on_empty=True, + ) + + def stats(self, debug_path=None): + total_regions = 0 + total_motion_boxes = 0 + object_ids = set() + total_frames = 0 + + while not self.detected_objects_queue.empty(): + ( + camera_name, + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) = self.detected_objects_queue.get() + + if debug_path: + self.save_debug_frame( + debug_path, frame_time, current_tracked_objects.values() + ) + + self.camera_state.update( + frame_time, current_tracked_objects, motion_boxes, regions + ) + total_regions += len(regions) + total_motion_boxes += len(motion_boxes) + top_score = 0 + for id, obj in self.camera_state.tracked_objects.items(): + if not obj.false_positive: + object_ids.add(id) + if obj.top_score > top_score: + top_score = obj.top_score + + total_frames += 1 + + self.frame_manager.delete(self.camera_state.previous_frame_id) + + return { + "total_regions": total_regions, + "total_motion_boxes": total_motion_boxes, + "true_positive_objects": len(object_ids), + "total_frames": total_frames, + "top_score": top_score, + } + + def save_debug_frame(self, debug_path, frame_time, tracked_objects): + current_frame = cv2.cvtColor( + self.frame_manager.get( + f"{self.camera_name}{frame_time}", self.camera_config.frame_shape_yuv + ), + cv2.COLOR_YUV2BGR_I420, + ) + # draw the bounding boxes on the frame + for obj in tracked_objects: + thickness = 2 + color = (0, 0, 175) + if obj["frame_time"] != frame_time: + thickness = 1 + color = (255, 0, 0) + else: + color = (255, 255, 0) + + # draw the bounding boxes on the frame + box = obj["box"] + draw_box_with_label( + current_frame, + box[0], + box[1], + box[2], + box[3], + obj["id"], + f"{int(obj['score'] * 100)}% {int(obj['area'])}", + thickness=thickness, + color=color, + ) + # draw the regions on the frame + region = obj["region"] + draw_box_with_label( + current_frame, + region[0], + region[1], + region[2], + region[3], + "region", + "", + thickness=1, + color=(0, 255, 0), + ) + + cv2.imwrite( + f"{os.path.join(debug_path, os.path.basename(self.clip_path))}.{int(frame_time * 1000000)}.jpg", + current_frame, + ) + + +@click.command() +@click.option("-p", "--path", required=True, help="Path to clip or directory to test.") +@click.option("-l", "--label", default="person", help="Label name to detect.") +@click.option("-o", "--output", default=None, help="File to save csv of data") +@click.option("--debug-path", default=None, help="Path to output frames for debugging.") +def process(path, label, output, debug_path): + clips = [] + if os.path.isdir(path): + files = os.listdir(path) + files.sort() + clips = [os.path.join(path, file) for file in files] + elif os.path.isfile(path): + clips.append(path) + + json_config = { + "mqtt": {"enabled": False}, + "detectors": {"coral": {"type": "edgetpu", "device": "usb"}}, + "cameras": { + "camera": { + "ffmpeg": { + "inputs": [ + { + "path": "path.mp4", + "global_args": "-hide_banner", + "input_args": "-loglevel info", + "roles": ["detect"], + } + ] + }, + "record": {"enabled": False}, + } + }, + } + + object_detector = LocalObjectDetector(labels="/labelmap.txt") + + results = [] + for c in clips: + logger.info(c) + frame_shape = get_frame_shape(c) + + json_config["cameras"]["camera"]["detect"] = { + "height": frame_shape[0], + "width": frame_shape[1], + } + json_config["cameras"]["camera"]["ffmpeg"]["inputs"][0]["path"] = c + + frigate_config = FrigateConfig(**json_config) + process_clip = ProcessClip(c, frame_shape, frigate_config) + process_clip.load_frames() + process_clip.process_frames(object_detector, objects_to_track=[label]) + + results.append((c, process_clip.stats(debug_path))) + + positive_count = sum( + 1 for result in results if result[1]["true_positive_objects"] > 0 + ) + print( + f"Objects were detected in {positive_count}/{len(results)}({positive_count / len(results) * 100:.2f}%) clip(s)." + ) + + if output: + # now we will open a file for writing + data_file = open(output, "w") + + # create the csv writer object + csv_writer = csv.writer(data_file) + + # Counter variable used for writing + # headers to the CSV file + count = 0 + + for result in results: + if count == 0: + # Writing headers of CSV file + header = ["file"] + list(result[1].keys()) + csv_writer.writerow(header) + count += 1 + + # Writing data of CSV file + csv_writer.writerow([result[0]] + list(result[1].values())) + + data_file.close() + + +if __name__ == "__main__": + process() diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs new file mode 100644 index 0000000..ab64df6 --- /dev/null +++ b/web/.eslintrc.cjs @@ -0,0 +1,74 @@ +module.exports = { + root: true, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + "plugin:vitest-globals/recommended", + "plugin:prettier/recommended", + ], + env: { browser: true, es2021: true, "vitest-globals/env": true }, + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: "latest", + sourceType: "module", + }, + settings: { + jest: { + version: 27, + }, + }, + ignorePatterns: ["*.d.ts", "/src/components/ui/*"], + plugins: ["react-hooks", "react-refresh"], + rules: { + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "comma-dangle": [ + "error", + { + objects: "always-multiline", + arrays: "always-multiline", + imports: "always-multiline", + }, + ], + "no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "no-console": "error", + "prettier/prettier": [ + "warn", + { + plugins: ["prettier-plugin-tailwindcss"], + }, + ], + }, + overrides: [ + { + files: ["**/*.{ts,tsx}"], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier", + ], + }, + ], +}; diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..ca98c7b --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Playwright +playwright-report +test-results + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.env \ No newline at end of file diff --git a/web/.prettierrc b/web/.prettierrc new file mode 100644 index 0000000..b4bfed3 --- /dev/null +++ b/web/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json new file mode 100644 index 0000000..7dea559 --- /dev/null +++ b/web/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint" + ] + } \ No newline at end of file diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..b30fa73 --- /dev/null +++ b/web/README.md @@ -0,0 +1,25 @@ +This is the Frigate frontend which connects to and provides a User Interface to the Python backend. + +# Web Development + +## Installing Web Dependencies Via NPM + +Within `/web`, run: + +```bash +npm install +``` + +## Running development frontend + +Within `/web`, run: + +```bash +PROXY_HOST= npm run dev +``` + +The Proxy Host can point to your existing Frigate instance. Otherwise defaults to `localhost:5000` if running Frigate on the same machine. + +## Extensions +Install these IDE extensions for an improved development experience: +- eslint diff --git a/web/components.json b/web/components.json new file mode 100644 index 0000000..679fbd7 --- /dev/null +++ b/web/components.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.cjs", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/web/e2e/fixtures/error-allowlist.ts b/web/e2e/fixtures/error-allowlist.ts new file mode 100644 index 0000000..4e6523b --- /dev/null +++ b/web/e2e/fixtures/error-allowlist.ts @@ -0,0 +1,116 @@ +/** + * Global allowlist of regex patterns that the error collector ignores. + * + * Each entry MUST include a comment explaining what it silences and why. + * The allowlist is filtered at collection time, so failure messages list + * only unfiltered errors. + * + * Per-spec additions go through the `expectedErrors` test fixture parameter + * (see error-collector.ts), not by editing this file. That keeps allowlist + * drift visible per-PR rather than buried in shared infrastructure. + * + * NOTE ON CONSOLE vs REQUEST ERRORS: + * When a network request returns a 5xx response, the browser emits two + * events that the error collector captures: + * [request] "500 Internal Server Error " — from onResponse (URL included) + * [console] "Failed to load resource: ..." — from onConsole (URL NOT included) + * + * The request-level message includes the URL, so those patterns are specific. + * The console-level message text (from ConsoleMessage.text()) does NOT include + * the URL — the URL is stored separately in e.url. Therefore the console + * pattern for HTTP 500s cannot be URL-discriminated, and a single pattern + * covers all such browser echoes. This is safe because every such console + * error is already caught (and specifically matched) by its paired [request] + * entry below. + */ + +export const GLOBAL_ALLOWLIST: RegExp[] = [ + // ------------------------------------------------------------------------- + // Browser echo of HTTP 5xx responses (console mirror of [request] events). + // + // Whenever the browser receives a 5xx response it emits a console error: + // "Failed to load resource: the server responded with a status of 500 + // (Internal Server Error)" + // The URL is NOT part of ConsoleMessage.text() — it is stored separately. + // Every console error of this form is therefore paired with a specific + // [request] 500 entry below that names the exact endpoint. Allowlisting + // this pattern here silences the browser echo; the request-level entries + // enforce specificity. + // ------------------------------------------------------------------------- + /Failed to load resource: the server responded with a status of 500/, + + // ------------------------------------------------------------------------- + // Mock infrastructure gaps — API endpoints not yet covered by ApiMocker. + // + // These produce 500s because Vite's preview server has no handler for them. + // Each is a TODO(real-bug): the mock should be extended so these endpoints + // return sensible fixture data in tests. + // + // Only [request] patterns are listed here; the paired [console] mirror is + // covered by the "Failed to load resource" entry above. + // ------------------------------------------------------------------------- + + // TODO(real-bug): ApiMocker registers "**/api/reviews**" (plural) but the + // app fetches /api/review (singular) for the review list and timeline. + // Affects: review.spec.ts, navigation.spec.ts, live.spec.ts, auth.spec.ts. + // Fix: add route handlers for /api/review and /api/review/** in api-mocker.ts. + /500 Internal Server Error.*\/api\/review(\?|\/|$)/, + + // TODO(real-bug): /api/stats/history is not mocked; the system page fetches + // it for the detector/process history charts. + // Fix: add route handler for /api/stats/history in api-mocker.ts. + /500 Internal Server Error.*\/api\/stats\/history/, + + // TODO(real-bug): /api/event_ids is not mocked; the explore/search page + // fetches it to resolve event IDs for display. + // Fix: add route handler for /api/event_ids in api-mocker.ts. + /500 Internal Server Error.*\/api\/event_ids/, + + // TODO(real-bug): /api/sub_labels?split_joined=1 returns 500; the mock + // registers "**/api/sub_labels" which may not match when a query string is + // present, or route registration order causes the catch-all to win first. + // Fix: change the mock route to "**/api/sub_labels**" in api-mocker.ts. + /500 Internal Server Error.*\/api\/sub_labels/, + + // TODO(real-bug): MediaMocker handles /api/*/latest.jpg but the app also + // requests /api/*/latest.webp (webp format) for camera snapshots. + // Affects: live.spec.ts, review.spec.ts, auth.spec.ts, navigation.spec.ts. + // Fix: add route handler for /api/*/latest.webp in MediaMocker.install(). + /500 Internal Server Error.*\/api\/[^/]+\/latest\.webp/, + /failed: net::ERR_ABORTED.*\/api\/[^/]+\/latest\.webp/, + + // ------------------------------------------------------------------------- + // Mock infrastructure gap — WebSocket streams. + // + // Playwright's page.route() does not intercept WebSocket connections. + // The jsmpeg live-stream WS connections to /live/jsmpeg/* always fail + // with a 500 handshake error because the Vite preview server has no WS + // handler. TODO(real-bug): add WsMocker support for jsmpeg WebSocket + // connections, or suppress the connection attempt in the test environment. + // Affects: live.spec.ts (single camera view), auth.spec.ts. + // ------------------------------------------------------------------------- + /WebSocket connection to '.*\/live\/jsmpeg\/.*' failed/, + + // ------------------------------------------------------------------------- + // Benign — lazy-loaded chunk aborts during navigation. + // + // When a test navigates away from a page while the browser is still + // fetching lazily-split JS/CSS asset chunks, the in-flight fetch is + // cancelled (net::ERR_ABORTED). This is normal browser behaviour on + // navigation and does not indicate a real error; the assets load fine + // on a stable connection. + // ------------------------------------------------------------------------- + /failed: net::ERR_ABORTED.*\/assets\//, + + // ------------------------------------------------------------------------- + // Real app bug — Radix UI DialogContent missing accessible title. + // + // TODO(real-bug): A dialog somewhere in the app renders + // without a , violating Radix UI's accessibility contract. + // The warning originates from the bundled main-*.js. Investigate which + // dialog component is missing the title and add a VisuallyHidden DialogTitle. + // Likely candidate: face-library or search-detail dialog in explore page. + // See: https://radix-ui.com/primitives/docs/components/dialog + // ------------------------------------------------------------------------- + /`DialogContent` requires a `DialogTitle`/, +]; diff --git a/web/e2e/fixtures/error-collector.ts b/web/e2e/fixtures/error-collector.ts new file mode 100644 index 0000000..7cba526 --- /dev/null +++ b/web/e2e/fixtures/error-collector.ts @@ -0,0 +1,122 @@ +/** + * Collects console errors, page errors, and failed network requests + * during a Playwright test, with regex-based allowlist filtering. + * + * Usage: + * const collector = installErrorCollector(page, [...GLOBAL_ALLOWLIST]); + * // ... run test ... + * collector.assertClean(); // throws if any non-allowlisted error + * + * The collector is wired into the `frigateApp` fixture so every test + * gets it for free. Tests that intentionally trigger an error pass + * additional regexes via the `expectedErrors` fixture parameter. + */ + +import type { Page, Request, Response, ConsoleMessage } from "@playwright/test"; + +export type CollectedError = { + kind: "console" | "pageerror" | "request"; + message: string; + url?: string; + stack?: string; +}; + +export type ErrorCollector = { + errors: CollectedError[]; + assertClean(): void; +}; + +function isAllowlisted(message: string, allowlist: RegExp[]): boolean { + return allowlist.some((pattern) => pattern.test(message)); +} + +function firstStackFrame(stack: string | undefined): string | undefined { + if (!stack) return undefined; + const lines = stack + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); + // Skip the error message line (line 0); return the first "at ..." frame + return lines.find((l) => l.startsWith("at ")); +} + +function isSameOrigin(url: string, baseURL: string | undefined): boolean { + if (!baseURL) return true; + try { + return new URL(url).origin === new URL(baseURL).origin; + } catch { + return false; + } +} + +export function installErrorCollector( + page: Page, + allowlist: RegExp[], +): ErrorCollector { + const errors: CollectedError[] = []; + const baseURL = ( + page.context() as unknown as { _options?: { baseURL?: string } } + )._options?.baseURL; + + const onConsole = (msg: ConsoleMessage) => { + if (msg.type() !== "error") return; + const text = msg.text(); + if (isAllowlisted(text, allowlist)) return; + errors.push({ + kind: "console", + message: text, + url: msg.location().url, + }); + }; + + const onPageError = (err: Error) => { + const text = err.message; + if (isAllowlisted(text, allowlist)) return; + errors.push({ + kind: "pageerror", + message: text, + stack: firstStackFrame(err.stack), + }); + }; + + const onResponse = (response: Response) => { + const status = response.status(); + if (status < 500) return; + const url = response.url(); + if (!isSameOrigin(url, baseURL)) return; + const text = `${status} ${response.statusText()} ${url}`; + if (isAllowlisted(text, allowlist)) return; + errors.push({ kind: "request", message: text, url }); + }; + + const onRequestFailed = (request: Request) => { + const url = request.url(); + if (!isSameOrigin(url, baseURL)) return; + const failure = request.failure(); + const text = `failed: ${failure?.errorText ?? "unknown"} ${url}`; + if (isAllowlisted(text, allowlist)) return; + errors.push({ kind: "request", message: text, url }); + }; + + page.on("console", onConsole); + page.on("pageerror", onPageError); + page.on("response", onResponse); + page.on("requestfailed", onRequestFailed); + + return { + errors, + assertClean() { + if (errors.length === 0) return; + const formatted = errors + .map((e, i) => { + const stack = e.stack ? `\n ${e.stack}` : ""; + const url = e.url && e.url !== e.message ? ` (${e.url})` : ""; + return ` ${i + 1}. [${e.kind}] ${e.message}${url}${stack}`; + }) + .join("\n"); + throw new Error( + `Page emitted ${errors.length} unexpected error${errors.length === 1 ? "" : "s"}:\n${formatted}`, + ); + }, + }; +} diff --git a/web/e2e/fixtures/frigate-test.ts b/web/e2e/fixtures/frigate-test.ts new file mode 100644 index 0000000..bc28ab5 --- /dev/null +++ b/web/e2e/fixtures/frigate-test.ts @@ -0,0 +1,120 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +/** + * Extended Playwright test fixture with FrigateApp. + * + * Every test imports `test` and `expect` from this file instead of + * @playwright/test directly. The `frigateApp` fixture provides a + * fully mocked Frigate frontend ready for interaction. + * + * The fixture also installs the error collector (see error-collector.ts). + * Any console error, page error, or same-origin failed request that is + * not on the global allowlist or the test's `expectedErrors` list will + * fail the test in the fixture's teardown. + * + * CRITICAL: All route/WS handlers are registered before page.goto() + * to prevent AuthProvider from redirecting to login.html. + */ + +import { test as base, expect, type Page } from "@playwright/test"; +import { + ApiMocker, + MediaMocker, + type ApiMockOverrides, +} from "../helpers/api-mocker"; +import { WsMocker } from "../helpers/ws-mocker"; +import { installErrorCollector, type ErrorCollector } from "./error-collector"; +import { GLOBAL_ALLOWLIST } from "./error-allowlist"; + +export class FrigateApp { + public api: ApiMocker; + public media: MediaMocker; + public ws: WsMocker; + public page: Page; + + private isDesktop: boolean; + + constructor(page: Page, projectName: string) { + this.page = page; + this.api = new ApiMocker(page); + this.media = new MediaMocker(page); + this.ws = new WsMocker(); + this.isDesktop = projectName === "desktop"; + } + + get isMobile() { + return !this.isDesktop; + } + + /** Install all mocks with default data. Call before goto(). */ + async installDefaults(overrides?: ApiMockOverrides) { + // Mock i18n locale files to prevent 404s + await this.page.route("**/locales/**", async (route) => { + // Let the request through to the built files + return route.fallback(); + }); + + await this.ws.install(this.page); + await this.media.install(); + await this.api.install(overrides); + } + + /** Navigate to a page. Always call installDefaults() first. */ + async goto(path: string) { + await this.page.goto(path); + // Wait for the app to render past the loading indicator + await this.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + } + + /** Navigate to a page that may show a loading indicator */ + async gotoAndWait(path: string, selector: string) { + await this.page.goto(path); + await this.page.waitForSelector(selector, { timeout: 10_000 }); + } +} + +type FrigateFixtures = { + frigateApp: FrigateApp; + /** + * Per-test additional allowlist regex patterns. Tests that intentionally + * trigger errors (e.g. error-state tests that hit a mocked 500) declare + * their expected errors here so the collector ignores them. + * + * Default is `[]` — most tests should not need this. + */ + expectedErrors: RegExp[]; + errorCollector: ErrorCollector; +}; + +export const test = base.extend({ + expectedErrors: [[], { option: true }], + + errorCollector: async ({ page, expectedErrors }, use, testInfo) => { + const collector = installErrorCollector(page, [ + ...GLOBAL_ALLOWLIST, + ...expectedErrors, + ]); + await use(collector); + if (process.env.E2E_STRICT_ERRORS === "1") { + collector.assertClean(); + } else if (collector.errors.length > 0) { + // Soft mode: attach errors to the test report so they're visible + // without failing the run. + await testInfo.attach("collected-errors.txt", { + body: collector.errors + .map((e) => `[${e.kind}] ${e.message}${e.url ? ` (${e.url})` : ""}`) + .join("\n"), + contentType: "text/plain", + }); + } + }, + + frigateApp: async ({ page, errorCollector }, use, testInfo) => { + // Reference the collector so its `use()` runs and teardown fires + void errorCollector; + const app = new FrigateApp(page, testInfo.project.name); + await app.installDefaults(); + await use(app); + }, +}); + +export { expect }; diff --git a/web/e2e/fixtures/mock-data/camera-activity.ts b/web/e2e/fixtures/mock-data/camera-activity.ts new file mode 100644 index 0000000..425e931 --- /dev/null +++ b/web/e2e/fixtures/mock-data/camera-activity.ts @@ -0,0 +1,77 @@ +/** + * Camera activity WebSocket payload factory. + * + * The camera_activity topic payload is double-serialized: + * the WS message contains { topic: "camera_activity", payload: JSON.stringify(activityMap) } + */ + +export interface CameraActivityState { + config: { + enabled: boolean; + detect: boolean; + record: boolean; + snapshots: boolean; + audio: boolean; + audio_transcription: boolean; + notifications: boolean; + notifications_suspended: number; + autotracking: boolean; + alerts: boolean; + detections: boolean; + object_descriptions: boolean; + review_descriptions: boolean; + }; + motion: boolean; + objects: Array<{ + label: string; + score: number; + box: [number, number, number, number]; + area: number; + ratio: number; + region: [number, number, number, number]; + current_zones: string[]; + id: string; + }>; + audio_detections: Array<{ + label: string; + score: number; + }>; +} + +function defaultCameraActivity(): CameraActivityState { + return { + config: { + enabled: true, + detect: true, + record: true, + snapshots: true, + audio: false, + audio_transcription: false, + notifications: false, + notifications_suspended: 0, + autotracking: false, + alerts: true, + detections: true, + object_descriptions: false, + review_descriptions: false, + }, + motion: false, + objects: [], + audio_detections: [], + }; +} + +export function cameraActivityPayload( + cameras: string[], + overrides?: Partial>>, +): string { + const activity: Record = {}; + for (const name of cameras) { + activity[name] = { + ...defaultCameraActivity(), + ...overrides?.[name], + } as CameraActivityState; + } + // Double-serialize: the WS payload is a JSON string + return JSON.stringify(activity); +} diff --git a/web/e2e/fixtures/mock-data/cases.json b/web/e2e/fixtures/mock-data/cases.json new file mode 100644 index 0000000..6174cde --- /dev/null +++ b/web/e2e/fixtures/mock-data/cases.json @@ -0,0 +1 @@ +[{"id": "case-001", "name": "Package Theft Investigation", "description": "Review of suspicious activity near the front porch", "created_at": 1780597809.365581, "updated_at": 1780673409.365581}] \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/config-schema.json b/web/e2e/fixtures/mock-data/config-schema.json new file mode 100644 index 0000000..04d72b1 --- /dev/null +++ b/web/e2e/fixtures/mock-data/config-schema.json @@ -0,0 +1 @@ +{"$defs": {"AlertsConfig": {"additionalProperties": false, "description": "Configure alerts", "properties": {"enabled": {"default": true, "description": "Enable or disable alert generation for all cameras; can be overridden per-camera.", "title": "Enable alerts", "type": "boolean"}, "labels": {"default": ["person", "car"], "description": "List of object labels that qualify as alerts (for example: car, person).", "items": {"type": "string"}, "title": "Alert labels", "type": "array"}, "required_zones": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Zones that an object must enter to be considered an alert; leave empty to allow any zone.", "title": "Required zones"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Tracks whether alerts were originally enabled in the static configuration.", "title": "Original alerts state"}, "cutoff_time": {"default": 40, "description": "Seconds to wait after no alert-causing activity before cutting off an alert.", "title": "Alerts cutoff time", "type": "integer"}}, "title": "AlertsConfig", "type": "object"}, "AudioConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable audio event detection for all cameras; can be overridden per-camera.", "title": "Enable audio detection", "type": "boolean"}, "max_not_heard": {"default": 30, "description": "Amount of seconds without the configured audio type before the audio event is ended.", "title": "End timeout", "type": "integer"}, "min_volume": {"default": 500, "description": "Minimum RMS volume threshold required to run audio detection; lower values increase sensitivity (e.g., 200 high, 500 medium, 1000 low).", "title": "Minimum volume", "type": "integer"}, "listen": {"default": ["bark", "fire_alarm", "speech", "yell"], "description": "List of audio event types to detect (for example: bark, fire_alarm, speech, yell).", "items": {"type": "string"}, "title": "Listen types", "type": "array"}, "filters": {"anyOf": [{"additionalProperties": {"$ref": "#/$defs/AudioFilterConfig"}, "type": "object"}, {"type": "null"}], "default": null, "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.", "title": "Audio filters"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether audio detection was originally enabled in the static config file.", "title": "Original audio state"}, "num_threads": {"default": 2, "description": "Number of threads to use for audio detection processing.", "minimum": 1, "title": "Detection threads", "type": "integer"}}, "title": "AudioConfig", "type": "object"}, "AudioFilterConfig": {"additionalProperties": false, "properties": {"threshold": {"default": 0.8, "description": "Minimum confidence threshold for the audio event to be counted.", "exclusiveMaximum": 1.0, "minimum": 0.5, "title": "Minimum audio confidence", "type": "number"}}, "title": "AudioFilterConfig", "type": "object"}, "AudioTranscriptionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable automatic audio transcription for all cameras; can be overridden per-camera.", "title": "Enable audio transcription", "type": "boolean"}, "language": {"default": "en", "description": "Language code used for transcription/translation (for example 'en' for English). See https://whisper-api.com/docs/languages/ for supported language codes.", "title": "Transcription language", "type": "string"}, "device": {"$ref": "#/$defs/EnrichmentsDeviceEnum", "default": "CPU", "description": "Device key (CPU/GPU) to run the transcription model on. Only NVIDIA CUDA GPUs are currently supported for transcription.", "title": "Transcription device"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Model size to use for offline audio event transcription.", "title": "Model size"}, "live_enabled": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "Enable streaming live transcription for audio as it is received.", "title": "Live transcription"}}, "title": "AudioTranscriptionConfig", "type": "object"}, "AuthConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable native authentication for the Frigate UI.", "title": "Enable authentication", "type": "boolean"}, "reset_admin_password": {"default": false, "description": "If true, reset the admin user's password on startup and print the new password in logs.", "title": "Reset admin password", "type": "boolean"}, "cookie_name": {"default": "frigate_token", "description": "Name of the cookie used to store the JWT token for native authentication.", "pattern": "^[a-z_]+$", "title": "JWT cookie name", "type": "string"}, "cookie_secure": {"default": false, "description": "Set the secure flag on the auth cookie; should be true when using TLS.", "title": "Secure cookie flag", "type": "boolean"}, "session_length": {"default": 86400, "description": "Session duration in seconds for JWT-based sessions.", "minimum": 60, "title": "Session length", "type": "integer"}, "refresh_time": {"default": 1800, "description": "When a session is within this many seconds of expiring, refresh it back to full length.", "minimum": 30, "title": "Session refresh window", "type": "integer"}, "failed_login_rate_limit": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Rate limiting rules for failed login attempts to reduce brute-force attacks.", "title": "Failed login limits"}, "trusted_proxies": {"default": [], "description": "List of trusted proxy IPs used when determining client IP for rate limiting.", "items": {"type": "string"}, "title": "Trusted proxies", "type": "array"}, "hash_iterations": {"default": 600000, "description": "Number of PBKDF2-SHA256 iterations to use when hashing user passwords.", "title": "Hash iterations", "type": "integer"}, "roles": {"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "description": "Map roles to camera lists. An empty list grants access to all cameras for the role.", "title": "Role mappings", "type": "object"}, "admin_first_time_login": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "When true the UI may show a help link on the login page informing users how to sign in after an admin password reset. ", "title": "First-time admin flag"}}, "title": "AuthConfig", "type": "object"}, "BaseDetectorConfig": {"additionalProperties": true, "properties": {"type": {"default": "cpu", "description": "Type of detector to use for object detection (for example 'cpu', 'edgetpu', 'openvino').", "title": "Detector Type", "type": "string"}, "model": {"anyOf": [{"$ref": "#/$defs/ModelConfig"}, {"type": "null"}], "default": null, "description": "Detector-specific model configuration options (path, input size, etc.).", "title": "Detector specific model configuration"}, "model_path": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "File path to the detector model binary if required by the chosen detector.", "title": "Detector specific model path"}}, "title": "BaseDetectorConfig", "type": "object"}, "BirdClassificationConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable bird classification.", "title": "Bird classification", "type": "boolean"}, "threshold": {"default": 0.9, "description": "Minimum classification score required to accept a bird classification.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Minimum score", "type": "number"}}, "title": "BirdClassificationConfig", "type": "object"}, "BirdseyeCameraConfig": {"properties": {"enabled": {"default": true, "description": "Enable or disable the Birdseye view feature.", "title": "Enable Birdseye", "type": "boolean"}, "mode": {"$ref": "#/$defs/BirdseyeModeEnum", "default": "objects", "description": "Mode for including cameras in Birdseye: 'objects', 'motion', or 'continuous'.", "title": "Tracking mode"}, "order": {"default": 0, "description": "Numeric position controlling the camera's ordering in the Birdseye layout.", "title": "Position", "type": "integer"}}, "title": "BirdseyeCameraConfig", "type": "object"}, "BirdseyeConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable the Birdseye view feature.", "title": "Enable Birdseye", "type": "boolean"}, "mode": {"$ref": "#/$defs/BirdseyeModeEnum", "default": "objects", "description": "Mode for including cameras in Birdseye: 'objects', 'motion', or 'continuous'.", "title": "Tracking mode"}, "restream": {"default": false, "description": "Re-stream the Birdseye output as an RTSP feed; enabling this will keep Birdseye running continuously.", "title": "Restream RTSP", "type": "boolean"}, "width": {"default": 1280, "description": "Output width (pixels) of the composed Birdseye frame.", "title": "Width", "type": "integer"}, "height": {"default": 720, "description": "Output height (pixels) of the composed Birdseye frame.", "title": "Height", "type": "integer"}, "quality": {"default": 8, "description": "Encoding quality for the Birdseye mpeg1 feed (1 highest quality, 31 lowest).", "maximum": 31, "minimum": 1, "title": "Encoding quality", "type": "integer"}, "inactivity_threshold": {"default": 30, "description": "Seconds of inactivity after which a camera will stop being shown in Birdseye.", "exclusiveMinimum": 0, "title": "Inactivity threshold", "type": "integer"}, "layout": {"$ref": "#/$defs/BirdseyeLayoutConfig", "description": "Layout options for the Birdseye composition.", "title": "Layout"}, "idle_heartbeat_fps": {"default": 0.0, "description": "Frames-per-second to resend the last composed Birdseye frame when idle; set to 0 to disable.", "maximum": 10.0, "minimum": 0.0, "title": "Idle heartbeat FPS", "type": "number"}}, "title": "BirdseyeConfig", "type": "object"}, "BirdseyeLayoutConfig": {"additionalProperties": false, "properties": {"scaling_factor": {"default": 2.0, "description": "Scaling factor used by the layout calculator (range 1.0 to 5.0).", "maximum": 5.0, "minimum": 1.0, "title": "Scaling factor", "type": "number"}, "max_cameras": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Maximum number of cameras to display at once in Birdseye; shows the most recent cameras.", "title": "Max cameras"}}, "title": "BirdseyeLayoutConfig", "type": "object"}, "BirdseyeModeEnum": {"enum": ["objects", "motion", "continuous"], "title": "BirdseyeModeEnum", "type": "string"}, "CameraAudioTranscriptionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable manually triggered audio event transcription.", "title": "Enable transcription", "type": "boolean"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Original transcription state"}, "live_enabled": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "Enable streaming live transcription for audio as it is received.", "title": "Live transcription"}}, "title": "CameraAudioTranscriptionConfig", "type": "object"}, "CameraConfig": {"additionalProperties": false, "properties": {"name": {"anyOf": [{"pattern": "^[a-zA-Z0-9_-]+$", "type": "string"}, {"type": "null"}], "default": null, "description": "Camera name is required", "title": "Camera name"}, "friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Camera friendly name used in the Frigate UI", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enabled", "title": "Enabled", "type": "boolean"}, "audio": {"$ref": "#/$defs/AudioConfig", "description": "Settings for audio-based event detection for this camera.", "title": "Audio detection"}, "audio_transcription": {"$ref": "#/$defs/CameraAudioTranscriptionConfig", "description": "Settings for live and speech audio transcription used for events and live captions.", "title": "Audio transcription"}, "birdseye": {"$ref": "#/$defs/BirdseyeCameraConfig", "description": "Settings for the Birdseye composite view that composes multiple camera feeds into a single layout.", "title": "Birdseye"}, "detect": {"$ref": "#/$defs/DetectConfig", "description": "Settings for the detection/detect role used to run object detection and initialize trackers.", "title": "Object Detection"}, "face_recognition": {"$ref": "#/$defs/CameraFaceRecognitionConfig", "description": "Settings for face detection and recognition for this camera.", "title": "Face recognition"}, "ffmpeg": {"$ref": "#/$defs/CameraFfmpegConfig", "description": "Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.", "title": "Streams (FFmpeg)"}, "live": {"$ref": "#/$defs/CameraLiveConfig", "description": "Settings used by the Web UI to control live stream selection, resolution and quality.", "title": "Live playback"}, "lpr": {"$ref": "#/$defs/CameraLicensePlateRecognitionConfig", "description": "License plate recognition settings including detection thresholds, formatting, and known plates.", "title": "License Plate Recognition"}, "motion": {"$ref": "#/$defs/MotionConfig", "default": null, "description": "Default motion detection settings for this camera.", "title": "Motion detection"}, "objects": {"$ref": "#/$defs/ObjectConfig", "description": "Object tracking defaults including which labels to track and per-object filters.", "title": "Objects"}, "record": {"$ref": "#/$defs/RecordConfig", "description": "Recording and retention settings for this camera.", "title": "Recording"}, "review": {"$ref": "#/$defs/ReviewConfig", "description": "Settings that control alerts, detections, and GenAI review summaries used by the UI and storage for this camera.", "title": "Review"}, "semantic_search": {"$ref": "#/$defs/CameraSemanticSearchConfig", "description": "Settings for semantic search which builds and queries object embeddings to find similar items.", "title": "Semantic Search"}, "snapshots": {"$ref": "#/$defs/SnapshotsConfig", "description": "Settings for API-generated snapshots of tracked objects for this camera.", "title": "Snapshots"}, "timestamp_style": {"$ref": "#/$defs/TimestampStyleConfig", "description": "Styling options for timestamps applied to snapshots and Debug view.", "title": "Timestamp style"}, "best_image_timeout": {"default": 60, "description": "How long to wait for the image with the highest confidence score.", "title": "Best image timeout", "type": "integer"}, "mqtt": {"$ref": "#/$defs/CameraMqttConfig", "description": "MQTT image publishing settings.", "title": "MQTT"}, "notifications": {"$ref": "#/$defs/NotificationConfig", "description": "Settings to enable and control notifications for this camera.", "title": "Notifications"}, "onvif": {"$ref": "#/$defs/OnvifConfig", "description": "ONVIF connection and PTZ autotracking settings for this camera.", "title": "ONVIF"}, "type": {"$ref": "#/$defs/CameraTypeEnum", "default": "generic", "description": "Camera Type", "title": "Camera type"}, "ui": {"$ref": "#/$defs/CameraUiConfig", "description": "Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.", "title": "Camera UI"}, "webui_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "URL to visit the camera directly from system page", "title": "Camera URL"}, "profiles": {"additionalProperties": {"$ref": "#/$defs/CameraProfileConfig"}, "description": "Named config profiles with partial overrides that can be activated at runtime.", "title": "Profiles", "type": "object"}, "zones": {"additionalProperties": {"$ref": "#/$defs/ZoneConfig"}, "description": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.", "title": "Zones", "type": "object"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Keep track of original state of camera.", "title": "Original camera state"}}, "required": ["ffmpeg"], "title": "CameraConfig", "type": "object"}, "CameraFaceRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable face recognition.", "title": "Enable face recognition", "type": "boolean"}, "min_area": {"default": 750, "description": "Minimum area (pixels) of a detected face box required to attempt recognition.", "title": "Minimum face area", "type": "integer"}}, "title": "CameraFaceRecognitionConfig", "type": "object"}, "CameraFfmpegConfig": {"additionalProperties": false, "properties": {"path": {"default": "default", "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\").", "title": "FFmpeg path", "type": "string"}, "global_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "description": "Global arguments passed to FFmpeg processes.", "title": "FFmpeg global arguments"}, "hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "auto", "description": "Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.", "title": "Hardware acceleration arguments"}, "input_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "preset-rtsp-generic", "description": "Input arguments applied to FFmpeg input streams.", "title": "Input arguments"}, "output_args": {"$ref": "#/$defs/FfmpegOutputArgsConfig", "description": "Default output arguments used for different FFmpeg roles such as detect and record.", "title": "Output arguments"}, "retry_interval": {"default": 10.0, "description": "Seconds to wait before attempting to reconnect a camera stream after failure. Default is 10.", "exclusiveMinimum": 0.0, "title": "FFmpeg retry time", "type": "number"}, "apple_compatibility": {"default": false, "description": "Enable HEVC tagging for better Apple player compatibility when recording H.265.", "title": "Apple compatibility", "type": "boolean"}, "gpu": {"default": 0, "description": "Default GPU index used for hardware acceleration if available.", "title": "GPU index", "type": "integer"}, "inputs": {"description": "List of input stream definitions (paths and roles) for this camera.", "items": {"$ref": "#/$defs/CameraInput"}, "title": "Camera inputs", "type": "array"}}, "required": ["inputs"], "title": "CameraFfmpegConfig", "type": "object"}, "CameraGroupConfig": {"additionalProperties": false, "properties": {"cameras": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Array of camera names included in this group.", "title": "Camera list"}, "icon": {"default": "generic", "description": "Icon used to represent the camera group in the UI.", "title": "Group icon", "type": "string"}, "order": {"default": 0, "description": "Numeric order used to sort camera groups in the UI; larger numbers appear later.", "title": "Sort order", "type": "integer"}}, "title": "CameraGroupConfig", "type": "object"}, "CameraInput": {"additionalProperties": false, "properties": {"path": {"description": "Camera input stream URL or path.", "title": "Input path", "type": "string"}, "roles": {"description": "Roles for this input stream.", "items": {"$ref": "#/$defs/CameraRoleEnum"}, "title": "Input roles", "type": "array"}, "global_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "FFmpeg global arguments for this input stream.", "title": "FFmpeg global arguments"}, "hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Hardware acceleration arguments for this input stream.", "title": "Hardware acceleration arguments"}, "input_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Input arguments specific to this stream.", "title": "Input arguments"}}, "required": ["path", "roles"], "title": "CameraInput", "type": "object"}, "CameraLicensePlateRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable LPR on this camera.", "title": "Enable LPR", "type": "boolean"}, "expire_time": {"default": 3, "description": "Time in seconds after which an unseen plate is expired from the tracker (for dedicated LPR cameras only).", "exclusiveMinimum": 0, "title": "Expire seconds", "type": "integer"}, "min_area": {"default": 1000, "description": "Minimum plate area (pixels) required to attempt recognition.", "title": "Minimum plate area", "type": "integer"}, "enhancement": {"default": 0, "description": "Enhancement level (0-10) to apply to plate crops prior to OCR; higher values may not always improve results, levels above 5 may only work with night time plates and should be used with caution.", "maximum": 10, "minimum": 0, "title": "Enhancement level", "type": "integer"}}, "title": "CameraLicensePlateRecognitionConfig", "type": "object"}, "CameraLiveConfig": {"additionalProperties": false, "properties": {"streams": {"additionalProperties": {"type": "string"}, "description": "Mapping of configured stream names to restream/go2rtc names used for live playback.", "title": "Live stream names", "type": "object"}, "height": {"default": 720, "description": "Height (pixels) to render the jsmpeg live stream in the Web UI; must be <= detect stream height.", "title": "Live height", "type": "integer"}, "quality": {"default": 8, "description": "Encoding quality for the jsmpeg stream (1 highest, 31 lowest).", "maximum": 31, "minimum": 1, "title": "Live quality", "type": "integer"}}, "title": "CameraLiveConfig", "type": "object"}, "CameraMqttConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable publishing image snapshots for objects to MQTT topics for this camera.", "title": "Send image", "type": "boolean"}, "timestamp": {"default": true, "description": "Overlay a timestamp on images published to MQTT.", "title": "Add timestamp", "type": "boolean"}, "bounding_box": {"default": true, "description": "Draw bounding boxes on images published over MQTT.", "title": "Add bounding box", "type": "boolean"}, "crop": {"default": true, "description": "Crop images published to MQTT to the detected object's bounding box.", "title": "Crop image", "type": "boolean"}, "height": {"default": 270, "description": "Height (pixels) to resize images published over MQTT.", "title": "Image height", "type": "integer"}, "required_zones": {"description": "Zones that an object must enter for an MQTT image to be published.", "items": {"type": "string"}, "title": "Required zones", "type": "array"}, "quality": {"default": 70, "description": "JPEG quality for images published to MQTT (0-100).", "maximum": 100, "minimum": 0, "title": "JPEG quality", "type": "integer"}}, "title": "CameraMqttConfig", "type": "object"}, "CameraProfileConfig": {"additionalProperties": false, "description": "A named profile containing partial camera config overrides.\n\nSections set to None inherit from the camera's base config.\nSections that are defined get Pydantic-validated, then only\nexplicitly-set fields are used as overrides via exclude_unset.", "properties": {"enabled": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Enabled"}, "audio": {"anyOf": [{"$ref": "#/$defs/AudioConfig"}, {"type": "null"}], "default": null}, "birdseye": {"anyOf": [{"$ref": "#/$defs/BirdseyeCameraConfig"}, {"type": "null"}], "default": null}, "detect": {"anyOf": [{"$ref": "#/$defs/DetectConfig"}, {"type": "null"}], "default": null}, "face_recognition": {"anyOf": [{"$ref": "#/$defs/CameraFaceRecognitionConfig"}, {"type": "null"}], "default": null}, "lpr": {"anyOf": [{"$ref": "#/$defs/CameraLicensePlateRecognitionConfig"}, {"type": "null"}], "default": null}, "motion": {"anyOf": [{"$ref": "#/$defs/MotionConfig"}, {"type": "null"}], "default": null}, "notifications": {"anyOf": [{"$ref": "#/$defs/NotificationConfig"}, {"type": "null"}], "default": null}, "objects": {"anyOf": [{"$ref": "#/$defs/ObjectConfig"}, {"type": "null"}], "default": null}, "record": {"anyOf": [{"$ref": "#/$defs/RecordConfig"}, {"type": "null"}], "default": null}, "review": {"anyOf": [{"$ref": "#/$defs/ReviewConfig"}, {"type": "null"}], "default": null}, "snapshots": {"anyOf": [{"$ref": "#/$defs/SnapshotsConfig"}, {"type": "null"}], "default": null}, "zones": {"anyOf": [{"additionalProperties": {"$ref": "#/$defs/ZoneConfig"}, "type": "object"}, {"type": "null"}], "default": null, "title": "Zones"}}, "title": "CameraProfileConfig", "type": "object"}, "CameraRoleEnum": {"enum": ["audio", "record", "detect"], "title": "CameraRoleEnum", "type": "string"}, "CameraSemanticSearchConfig": {"additionalProperties": false, "properties": {"triggers": {"additionalProperties": {"$ref": "#/$defs/TriggerConfig"}, "default": {}, "description": "Actions and matching criteria for camera-specific semantic search triggers.", "title": "Triggers", "type": "object"}}, "title": "CameraSemanticSearchConfig", "type": "object"}, "CameraTypeEnum": {"enum": ["generic", "lpr"], "title": "CameraTypeEnum", "type": "string"}, "CameraUiConfig": {"additionalProperties": false, "properties": {"order": {"default": 0, "description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later.", "title": "UI order", "type": "integer"}, "dashboard": {"default": true, "description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again.", "title": "Show in UI", "type": "boolean"}, "review": {"default": true, "description": "Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view).", "title": "Show in review", "type": "boolean"}}, "title": "CameraUiConfig", "type": "object"}, "ClassificationConfig": {"additionalProperties": false, "properties": {"bird": {"$ref": "#/$defs/BirdClassificationConfig", "description": "Settings specific to bird classification models.", "title": "Bird classification config"}, "custom": {"additionalProperties": {"$ref": "#/$defs/CustomClassificationConfig"}, "default": {}, "description": "Configuration for custom classification models used for objects or state detection.", "title": "Custom Classification Models", "type": "object"}}, "title": "ClassificationConfig", "type": "object"}, "ColorConfig": {"additionalProperties": false, "properties": {"red": {"default": 255, "description": "Red component (0-255) for timestamp color.", "maximum": 255, "minimum": 0, "title": "Red", "type": "integer"}, "green": {"default": 255, "description": "Green component (0-255) for timestamp color.", "maximum": 255, "minimum": 0, "title": "Green", "type": "integer"}, "blue": {"default": 255, "description": "Blue component (0-255) for timestamp color.", "maximum": 255, "minimum": 0, "title": "Blue", "type": "integer"}}, "title": "ColorConfig", "type": "object"}, "CustomClassificationConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable the custom classification model.", "title": "Enable model", "type": "boolean"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Identifier for the custom classification model to use.", "title": "Model name"}, "threshold": {"default": 0.8, "description": "Score threshold used to change the classification state.", "title": "Score threshold", "type": "number"}, "save_attempts": {"anyOf": [{"minimum": 0, "type": "integer"}, {"type": "null"}], "default": null, "description": "How many classification attempts to save for recent classifications UI.", "title": "Save attempts"}, "object_config": {"anyOf": [{"$ref": "#/$defs/CustomClassificationObjectConfig"}, {"type": "null"}], "default": null}, "state_config": {"anyOf": [{"$ref": "#/$defs/CustomClassificationStateConfig"}, {"type": "null"}], "default": null}}, "title": "CustomClassificationConfig", "type": "object"}, "CustomClassificationObjectConfig": {"additionalProperties": false, "properties": {"objects": {"description": "List of object types to run object classification on.", "items": {"type": "string"}, "title": "Classify objects", "type": "array"}, "classification_type": {"$ref": "#/$defs/ObjectClassificationType", "default": "sub_label", "description": "Classification type applied: 'sub_label' (adds sub_label) or other supported types.", "title": "Classification type"}}, "title": "CustomClassificationObjectConfig", "type": "object"}, "CustomClassificationStateCameraConfig": {"additionalProperties": false, "properties": {"crop": {"description": "Crop coordinates to use for running classification on this camera.", "items": {"type": "number"}, "title": "Classification crop", "type": "array"}}, "required": ["crop"], "title": "CustomClassificationStateCameraConfig", "type": "object"}, "CustomClassificationStateConfig": {"additionalProperties": false, "properties": {"cameras": {"additionalProperties": {"$ref": "#/$defs/CustomClassificationStateCameraConfig"}, "description": "Per-camera crop and settings for running state classification.", "title": "Classification cameras", "type": "object"}, "motion": {"default": false, "description": "If true, run classification when motion is detected within the specified crop.", "title": "Run on motion", "type": "boolean"}, "interval": {"anyOf": [{"exclusiveMinimum": 0, "type": "integer"}, {"type": "null"}], "default": null, "description": "Interval (seconds) between periodic classification runs for state classification.", "title": "Classification interval"}}, "required": ["cameras"], "title": "CustomClassificationStateConfig", "type": "object"}, "DatabaseConfig": {"additionalProperties": false, "properties": {"path": {"default": "/config/frigate.db", "description": "Filesystem path where the Frigate SQLite database file will be stored.", "title": "Database path", "type": "string"}}, "title": "DatabaseConfig", "type": "object"}, "DetectConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable object detection for all cameras; can be overridden per-camera.", "title": "Enable object detection", "type": "boolean"}, "height": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", "title": "Detect height"}, "width": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", "title": "Detect width"}, "fps": {"default": 5, "description": "Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects).", "title": "Detect FPS", "type": "integer"}, "min_initialized": {"anyOf": [{"minimum": 2, "type": "integer"}, {"type": "null"}], "default": null, "description": "Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2.", "title": "Minimum initialization frames"}, "max_disappeared": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Number of frames without a detection before a tracked object is considered gone.", "title": "Maximum disappeared frames"}, "stationary": {"$ref": "#/$defs/StationaryConfig", "description": "Settings to detect and manage objects that remain stationary for a period of time.", "title": "Stationary objects config"}, "annotation_offset": {"default": 0, "description": "Milliseconds to shift detect annotations to better align timeline bounding boxes with recordings; can be positive or negative.", "title": "Annotation offset", "type": "integer"}}, "title": "DetectConfig", "type": "object"}, "DetectionsConfig": {"additionalProperties": false, "description": "Configure detections", "properties": {"enabled": {"default": true, "description": "Enable or disable detection events for all cameras; can be overridden per-camera.", "title": "Enable detections", "type": "boolean"}, "labels": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "default": null, "description": "List of object labels that qualify as detection events.", "title": "Detection labels"}, "required_zones": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Zones that an object must enter to be considered a detection; leave empty to allow any zone.", "title": "Required zones"}, "cutoff_time": {"default": 30, "description": "Seconds to wait after no detection-causing activity before cutting off a detection.", "title": "Detections cutoff time", "type": "integer"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Tracks whether detections were originally enabled in the static configuration.", "title": "Original detections state"}}, "title": "DetectionsConfig", "type": "object"}, "EnrichmentsDeviceEnum": {"enum": ["GPU", "CPU"], "title": "EnrichmentsDeviceEnum", "type": "string"}, "EventsConfig": {"additionalProperties": false, "properties": {"pre_capture": {"default": 5, "description": "Number of seconds before the detection event to include in the recording.", "maximum": 60, "minimum": 0, "title": "Pre-capture seconds", "type": "integer"}, "post_capture": {"default": 5, "description": "Number of seconds after the detection event to include in the recording.", "minimum": 0, "title": "Post-capture seconds", "type": "integer"}, "retain": {"$ref": "#/$defs/ReviewRetainConfig", "description": "Retention settings for recordings of detection events.", "title": "Event retention"}}, "title": "EventsConfig", "type": "object"}, "FaceRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable face recognition for all cameras; can be overridden per-camera.", "title": "Enable face recognition", "type": "boolean"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Model size to use for face embeddings (small/large); larger may require GPU.", "title": "Model size"}, "unknown_score": {"default": 0.8, "description": "Distance threshold below which a face is considered a potential match (higher = stricter).", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Unknown score threshold", "type": "number"}, "detection_threshold": {"default": 0.7, "description": "Minimum detection confidence required to consider a face detection valid.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Detection threshold", "type": "number"}, "recognition_threshold": {"default": 0.9, "description": "Face embedding distance threshold to consider two faces a match.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Recognition threshold", "type": "number"}, "min_area": {"default": 750, "description": "Minimum area (pixels) of a detected face box required to attempt recognition.", "title": "Minimum face area", "type": "integer"}, "min_faces": {"default": 1, "description": "Minimum number of face recognitions required before applying a recognized sub-label to a person.", "exclusiveMinimum": 0, "maximum": 6, "title": "Minimum faces", "type": "integer"}, "save_attempts": {"default": 200, "description": "Number of face recognition attempts to retain for recent recognition UI.", "minimum": 0, "title": "Save attempts", "type": "integer"}, "blur_confidence_filter": {"default": true, "description": "Adjust confidence scores based on image blur to reduce false positives for poor quality faces.", "title": "Blur confidence filter", "type": "boolean"}, "device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", "title": "Device"}}, "title": "FaceRecognitionConfig", "type": "object"}, "FfmpegConfig": {"additionalProperties": false, "properties": {"path": {"default": "default", "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\").", "title": "FFmpeg path", "type": "string"}, "global_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "description": "Global arguments passed to FFmpeg processes.", "title": "FFmpeg global arguments"}, "hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "auto", "description": "Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.", "title": "Hardware acceleration arguments"}, "input_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "preset-rtsp-generic", "description": "Input arguments applied to FFmpeg input streams.", "title": "Input arguments"}, "output_args": {"$ref": "#/$defs/FfmpegOutputArgsConfig", "description": "Default output arguments used for different FFmpeg roles such as detect and record.", "title": "Output arguments"}, "retry_interval": {"default": 10.0, "description": "Seconds to wait before attempting to reconnect a camera stream after failure. Default is 10.", "exclusiveMinimum": 0.0, "title": "FFmpeg retry time", "type": "number"}, "apple_compatibility": {"default": false, "description": "Enable HEVC tagging for better Apple player compatibility when recording H.265.", "title": "Apple compatibility", "type": "boolean"}, "gpu": {"default": 0, "description": "Default GPU index used for hardware acceleration if available.", "title": "GPU index", "type": "integer"}}, "title": "FfmpegConfig", "type": "object"}, "FfmpegOutputArgsConfig": {"additionalProperties": false, "properties": {"detect": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "description": "Default output arguments for detect role streams.", "title": "Detect output arguments"}, "record": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "preset-record-generic-audio-aac", "description": "Default output arguments for record role streams.", "title": "Record output arguments"}}, "title": "FfmpegOutputArgsConfig", "type": "object"}, "FilterConfig": {"additionalProperties": false, "properties": {"min_area": {"anyOf": [{"type": "integer"}, {"type": "number"}], "default": 0, "description": "Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", "title": "Minimum object area"}, "max_area": {"anyOf": [{"type": "integer"}, {"type": "number"}], "default": 24000000, "description": "Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", "title": "Maximum object area"}, "min_ratio": {"default": 0, "description": "Minimum width/height ratio required for the bounding box to qualify.", "title": "Minimum aspect ratio", "type": "number"}, "max_ratio": {"default": 24000000, "description": "Maximum width/height ratio allowed for the bounding box to qualify.", "title": "Maximum aspect ratio", "type": "number"}, "threshold": {"default": 0.7, "description": "Average detection confidence threshold required for the object to be considered a true positive.", "title": "Confidence threshold", "type": "number"}, "min_score": {"default": 0.5, "description": "Minimum single-frame detection confidence required for the object to be counted.", "title": "Minimum confidence", "type": "number"}, "mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "description": "Polygon coordinates defining where this filter applies within the frame.", "title": "Filter mask", "type": "object"}, "raw_mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "title": "Raw Mask", "type": "object"}}, "title": "FilterConfig", "type": "object"}, "GenAIConfig": {"additionalProperties": false, "description": "Primary GenAI Config to define GenAI Provider.", "properties": {"api_key": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "API key required by some providers (can also be set via environment variables).", "title": "API key"}, "base_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Base URL for self-hosted or compatible providers (for example an Ollama instance).", "title": "Base URL"}, "model": {"default": "", "description": "The model to use from the provider for generating descriptions or summaries.", "title": "Model", "type": "string"}, "provider": {"$ref": "#/$defs/GenAIProviderEnum", "description": "The GenAI provider to use (for example: ollama, gemini, openai).", "title": "Provider"}, "roles": {"description": "GenAI roles (chat, descriptions, embeddings); one provider per role.", "items": {"$ref": "#/$defs/GenAIRoleEnum"}, "title": "Roles", "type": "array"}, "provider_options": {"additionalProperties": {}, "default": {}, "description": "Additional provider-specific options to pass to the GenAI client.", "title": "Provider options", "type": "object"}, "runtime_options": {"additionalProperties": {}, "default": {}, "description": "Runtime options passed to the provider for each inference call.", "title": "Runtime options", "type": "object"}}, "required": ["provider"], "title": "GenAIConfig", "type": "object"}, "GenAIObjectConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable GenAI generation of descriptions for tracked objects by default.", "title": "Enable GenAI", "type": "boolean"}, "use_snapshot": {"default": false, "description": "Use object snapshots instead of thumbnails for GenAI description generation.", "title": "Use snapshots", "type": "boolean"}, "prompt": {"default": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "description": "Default prompt template used when generating descriptions with GenAI.", "title": "Caption prompt", "type": "string"}, "object_prompts": {"additionalProperties": {"type": "string"}, "description": "Per-object prompts to customize GenAI outputs for specific labels.", "title": "Object prompts", "type": "object"}, "objects": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "List of object labels to send to GenAI by default.", "title": "GenAI objects"}, "required_zones": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Zones that must be entered for objects to qualify for GenAI description generation.", "title": "Required zones"}, "debug_save_thumbnails": {"default": false, "description": "Save thumbnails sent to GenAI for debugging and review.", "title": "Save thumbnails", "type": "boolean"}, "send_triggers": {"$ref": "#/$defs/GenAIObjectTriggerConfig", "description": "Defines when frames should be sent to GenAI (on end, after updates, etc.).", "title": "GenAI triggers"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether GenAI was enabled in the original static config.", "title": "Original GenAI state"}}, "title": "GenAIObjectConfig", "type": "object"}, "GenAIObjectTriggerConfig": {"additionalProperties": false, "properties": {"tracked_object_end": {"default": true, "description": "Send a request to GenAI when the tracked object ends.", "title": "Send on end", "type": "boolean"}, "after_significant_updates": {"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}], "default": null, "description": "Send a request to GenAI after a specified number of significant updates for the tracked object.", "title": "Early GenAI trigger"}}, "title": "GenAIObjectTriggerConfig", "type": "object"}, "GenAIProviderEnum": {"enum": ["openai", "azure_openai", "gemini", "ollama", "llamacpp"], "title": "GenAIProviderEnum", "type": "string"}, "GenAIReviewConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable GenAI-generated descriptions and summaries for review items.", "title": "Enable GenAI descriptions", "type": "boolean"}, "alerts": {"default": true, "description": "Use GenAI to generate descriptions for alert items.", "title": "Enable GenAI for alerts", "type": "boolean"}, "detections": {"default": false, "description": "Use GenAI to generate descriptions for detection items.", "title": "Enable GenAI for detections", "type": "boolean"}, "image_source": {"$ref": "#/$defs/ImageSourceEnum", "default": "preview", "description": "Source of images sent to GenAI ('preview' or 'recordings'); 'recordings' uses higher quality frames but more tokens.", "title": "Review image source"}, "additional_concerns": {"default": [], "description": "A list of additional concerns or notes the GenAI should consider when evaluating activity on this camera.", "items": {"type": "string"}, "title": "Additional concerns", "type": "array"}, "debug_save_thumbnails": {"default": false, "description": "Save thumbnails that are sent to the GenAI provider for debugging and review.", "title": "Save thumbnails", "type": "boolean"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Tracks whether GenAI review was originally enabled in the static configuration.", "title": "Original GenAI state"}, "preferred_language": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Preferred language to request from the GenAI provider for generated responses.", "title": "Preferred language"}, "activity_context_prompt": {"default": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is.", "description": "Custom prompt describing what is and is not suspicious activity to provide context for GenAI summaries.", "title": "Activity context prompt", "type": "string"}}, "title": "GenAIReviewConfig", "type": "object"}, "GenAIRoleEnum": {"enum": ["chat", "descriptions", "embeddings"], "title": "GenAIRoleEnum", "type": "string"}, "HeaderMappingConfig": {"additionalProperties": false, "properties": {"user": {"default": null, "description": "Header containing the authenticated username provided by the upstream proxy.", "title": "User header", "type": "string"}, "role": {"default": null, "description": "Header containing the authenticated user's role or groups from the upstream proxy.", "title": "Role header", "type": "string"}, "role_map": {"anyOf": [{"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "type": "object"}, {"type": "null"}], "description": "Map upstream group values to Frigate roles (for example map admin groups to the admin role).", "title": "Role mapping"}}, "title": "HeaderMappingConfig", "type": "object"}, "IPv6Config": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable IPv6 support for Frigate services (API and UI) where applicable.", "title": "Enable IPv6", "type": "boolean"}}, "title": "IPv6Config", "type": "object"}, "ImageSourceEnum": {"description": "Image source options for GenAI Review.", "enum": ["preview", "recordings"], "title": "ImageSourceEnum", "type": "string"}, "InputDTypeEnum": {"enum": ["float", "float_denorm", "int"], "title": "InputDTypeEnum", "type": "string"}, "InputTensorEnum": {"enum": ["nchw", "nhwc", "hwnc", "hwcn"], "title": "InputTensorEnum", "type": "string"}, "LicensePlateRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable license plate recognition for all cameras; can be overridden per-camera.", "title": "Enable LPR", "type": "boolean"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Model size used for text detection/recognition. Most users should use 'small'.", "title": "Model size"}, "detection_threshold": {"default": 0.7, "description": "Detection confidence threshold to begin running OCR on a suspected plate.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Detection threshold", "type": "number"}, "min_area": {"default": 1000, "description": "Minimum plate area (pixels) required to attempt recognition.", "title": "Minimum plate area", "type": "integer"}, "recognition_threshold": {"default": 0.9, "description": "Confidence threshold required for recognized plate text to be attached as a sub-label.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Recognition threshold", "type": "number"}, "min_plate_length": {"default": 4, "description": "Minimum number of characters a recognized plate must contain to be considered valid.", "title": "Min plate length", "type": "integer"}, "format": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional regex to validate recognized plate strings against an expected format.", "title": "Plate format regex"}, "match_distance": {"default": 1, "description": "Number of character mismatches allowed when comparing detected plates to known plates.", "minimum": 0, "title": "Match distance", "type": "integer"}, "known_plates": {"anyOf": [{"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "type": "object"}, {"type": "null"}], "default": {}, "description": "List of plates or regexes to specially track or alert on.", "title": "Known plates"}, "enhancement": {"default": 0, "description": "Enhancement level (0-10) to apply to plate crops prior to OCR; higher values may not always improve results, levels above 5 may only work with night time plates and should be used with caution.", "maximum": 10, "minimum": 0, "title": "Enhancement level", "type": "integer"}, "debug_save_plates": {"default": false, "description": "Save plate crop images for debugging LPR performance.", "title": "Save debug plates", "type": "boolean"}, "device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", "title": "Device"}, "replace_rules": {"description": "Regex replacement rules used to normalize detected plate strings before matching.", "items": {"$ref": "#/$defs/ReplaceRule"}, "title": "Replacement rules", "type": "array"}}, "title": "LicensePlateRecognitionConfig", "type": "object"}, "ListenConfig": {"additionalProperties": false, "properties": {"internal": {"anyOf": [{"type": "integer"}, {"type": "string"}], "default": 5000, "description": "Internal listening port for Frigate (default 5000).", "title": "Internal port"}, "external": {"anyOf": [{"type": "integer"}, {"type": "string"}], "default": 8971, "description": "External listening port for Frigate (default 8971).", "title": "External port"}}, "title": "ListenConfig", "type": "object"}, "LogLevel": {"enum": ["debug", "info", "warning", "error", "critical"], "title": "LogLevel", "type": "string"}, "LoggerConfig": {"additionalProperties": false, "properties": {"default": {"$ref": "#/$defs/LogLevel", "default": "info", "title": "Logging level", "description": "Default global log verbosity (debug, info, warning, error)."}, "logs": {"additionalProperties": {"$ref": "#/$defs/LogLevel"}, "description": "Per-component log level overrides to increase or decrease verbosity for specific modules.", "title": "Per-process log level", "type": "object"}}, "title": "LoggerConfig", "type": "object"}, "ModelConfig": {"additionalProperties": false, "properties": {"path": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Path to a custom detection model file (or plus:// for Frigate+ models).", "title": "Custom object detector model path"}, "labelmap_path": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Path to a labelmap file that maps numeric classes to string labels for the detector.", "title": "Label map for custom object detector"}, "width": {"default": 320, "description": "Width of the model input tensor in pixels.", "title": "Object detection model input width", "type": "integer"}, "height": {"default": 320, "description": "Height of the model input tensor in pixels.", "title": "Object detection model input height", "type": "integer"}, "labelmap": {"additionalProperties": {"type": "string"}, "description": "Overrides or remapping entries to merge into the standard labelmap.", "title": "Labelmap customization", "type": "object"}, "attributes_map": {"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "default": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "description": "Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate']).", "title": "Map of object labels to their attribute labels", "type": "object"}, "input_tensor": {"$ref": "#/$defs/InputTensorEnum", "default": "nhwc", "description": "Tensor format expected by the model: 'nhwc' or 'nchw'.", "title": "Model Input Tensor Shape"}, "input_pixel_format": {"$ref": "#/$defs/PixelFormatEnum", "default": "rgb", "description": "Pixel colorspace expected by the model: 'rgb', 'bgr', or 'yuv'.", "title": "Model Input Pixel Color Format"}, "input_dtype": {"$ref": "#/$defs/InputDTypeEnum", "default": "int", "description": "Data type of the model input tensor (for example 'float32').", "title": "Model Input D Type"}, "model_type": {"$ref": "#/$defs/ModelTypeEnum", "default": "ssd", "description": "Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization.", "title": "Object Detection Model Type"}}, "title": "ModelConfig", "type": "object"}, "ModelSizeEnum": {"enum": ["small", "large"], "title": "ModelSizeEnum", "type": "string"}, "ModelTypeEnum": {"enum": ["dfine", "rfdetr", "ssd", "yolox", "yolonas", "yolo-generic"], "title": "ModelTypeEnum", "type": "string"}, "MotionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable motion detection for all cameras; can be overridden per-camera.", "title": "Enable motion detection", "type": "boolean"}, "threshold": {"default": 30, "description": "Pixel difference threshold used by the motion detector; higher values reduce sensitivity (range 1-255).", "maximum": 255, "minimum": 1, "title": "Motion threshold", "type": "integer"}, "lightning_threshold": {"default": 0.8, "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events.", "maximum": 1.0, "minimum": 0.3, "title": "Lightning threshold", "type": "number"}, "skip_motion_threshold": {"anyOf": [{"maximum": 1.0, "minimum": 0.0, "type": "number"}, {"type": "null"}], "default": null, "description": "If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto\u2011tracking an object. The trade\u2011off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.", "title": "Skip motion threshold"}, "improve_contrast": {"default": true, "description": "Apply contrast improvement to frames before motion analysis to help detection.", "title": "Improve contrast", "type": "boolean"}, "contour_area": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": 10, "description": "Minimum contour area in pixels required for a motion contour to be counted.", "title": "Contour area"}, "delta_alpha": {"default": 0.2, "description": "Alpha blending factor used in frame differencing for motion calculation.", "title": "Delta alpha", "type": "number"}, "frame_alpha": {"default": 0.01, "description": "Alpha value used when blending frames for motion preprocessing.", "title": "Frame alpha", "type": "number"}, "frame_height": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": 100, "description": "Height in pixels to scale frames to when computing motion.", "title": "Frame height"}, "mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/MotionMaskConfig"}, {"type": "null"}]}, "description": "Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", "title": "Mask coordinates", "type": "object"}, "mqtt_off_delay": {"default": 30, "description": "Seconds to wait after last motion before publishing an MQTT 'off' state.", "title": "MQTT off delay", "type": "integer"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether motion detection was enabled in the original static configuration.", "title": "Original motion state"}, "raw_mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/MotionMaskConfig"}, {"type": "null"}]}, "title": "Raw Mask", "type": "object"}}, "title": "MotionConfig", "type": "object"}, "MotionMaskConfig": {"additionalProperties": false, "description": "Configuration for a single motion mask.", "properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "A friendly name for this motion mask used in the Frigate UI", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enable or disable this motion mask", "title": "Enabled", "type": "boolean"}, "coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "description": "Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", "title": "Coordinates"}, "raw_coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "title": "Raw Coordinates"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Keep track of original state of motion mask."}}, "title": "MotionMaskConfig", "type": "object"}, "MqttConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable MQTT integration for state, events, and snapshots.", "title": "Enable MQTT", "type": "boolean"}, "host": {"default": "", "description": "Hostname or IP address of the MQTT broker.", "title": "MQTT host", "type": "string"}, "port": {"default": 1883, "description": "Port of the MQTT broker (usually 1883 for plain MQTT).", "title": "MQTT port", "type": "integer"}, "topic_prefix": {"default": "frigate", "description": "MQTT topic prefix for all Frigate topics; must be unique if running multiple instances.", "title": "Topic prefix", "type": "string"}, "client_id": {"default": "frigate", "description": "Client identifier used when connecting to the MQTT broker; should be unique per instance.", "title": "Client ID", "type": "string"}, "stats_interval": {"default": 60, "description": "Interval in seconds for publishing system and camera stats to MQTT.", "minimum": 15, "title": "Stats interval", "type": "integer"}, "user": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional MQTT username; can be provided via environment variables or secrets.", "title": "MQTT username"}, "password": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional MQTT password; can be provided via environment variables or secrets.", "title": "MQTT password"}, "tls_ca_certs": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Path to CA certificate for TLS connections to the broker (for self-signed certs).", "title": "TLS CA certs"}, "tls_client_cert": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Client certificate path for TLS mutual authentication; do not set user/password when using client certs.", "title": "Client cert"}, "tls_client_key": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Private key path for the client certificate.", "title": "Client key"}, "tls_insecure": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Allow insecure TLS connections by skipping hostname verification (not recommended).", "title": "TLS insecure"}, "qos": {"default": 0, "description": "Quality of Service level for MQTT publishes/subscriptions (0, 1, or 2).", "title": "MQTT QoS", "type": "integer"}}, "title": "MqttConfig", "type": "object"}, "NetworkingConfig": {"additionalProperties": false, "properties": {"ipv6": {"$ref": "#/$defs/IPv6Config", "description": "IPv6-specific settings for Frigate network services.", "title": "IPv6 configuration"}, "listen": {"$ref": "#/$defs/ListenConfig", "description": "Configuration for internal and external listening ports. This is for advanced users. For the majority of use cases it's recommended to change the ports section of your Docker compose file.", "title": "Listening ports configuration"}}, "title": "NetworkingConfig", "type": "object"}, "NotificationConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable notifications for all cameras; can be overridden per-camera.", "title": "Enable notifications", "type": "boolean"}, "email": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Email address used for push notifications or required by certain notification providers.", "title": "Notification email"}, "cooldown": {"default": 0, "description": "Cooldown (seconds) between notifications to avoid spamming recipients.", "minimum": 0, "title": "Cooldown period", "type": "integer"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether notifications were enabled in the original static configuration.", "title": "Original notifications state"}}, "title": "NotificationConfig", "type": "object"}, "ObjectClassificationType": {"enum": ["sub_label", "attribute"], "title": "ObjectClassificationType", "type": "string"}, "ObjectConfig": {"additionalProperties": false, "properties": {"track": {"default": ["person"], "description": "List of object labels to track for all cameras; can be overridden per-camera.", "items": {"type": "string"}, "title": "Objects to track", "type": "array"}, "filters": {"additionalProperties": {"$ref": "#/$defs/FilterConfig"}, "description": "Filters applied to detected objects to reduce false positives (area, ratio, confidence).", "title": "Object filters", "type": "object"}, "mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "description": "Mask polygon used to prevent object detection in specified areas.", "title": "Object mask", "type": "object"}, "raw_mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "title": "Raw Mask", "type": "object"}, "genai": {"$ref": "#/$defs/GenAIObjectConfig", "description": "GenAI options for describing tracked objects and sending frames for generation.", "title": "GenAI object config"}}, "title": "ObjectConfig", "type": "object"}, "ObjectMaskConfig": {"additionalProperties": false, "description": "Configuration for a single object mask.", "properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "A friendly name for this object mask used in the Frigate UI", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enable or disable this object mask", "title": "Enabled", "type": "boolean"}, "coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "description": "Ordered x,y coordinates defining the object mask polygon used to include/exclude areas.", "title": "Coordinates"}, "raw_coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "title": "Raw Coordinates"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Keep track of original state of object mask."}}, "title": "ObjectMaskConfig", "type": "object"}, "OnvifConfig": {"additionalProperties": false, "properties": {"host": {"default": "", "description": "Host (and optional scheme) for the ONVIF service for this camera.", "title": "ONVIF host", "type": "string"}, "port": {"default": 8000, "description": "Port number for the ONVIF service.", "title": "ONVIF port", "type": "integer"}, "user": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Username for ONVIF authentication; some devices require admin user for ONVIF.", "title": "ONVIF username"}, "password": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Password for ONVIF authentication.", "title": "ONVIF password"}, "tls_insecure": {"default": false, "description": "Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).", "title": "Disable TLS verify", "type": "boolean"}, "profile": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically.", "title": "ONVIF profile"}, "autotracking": {"$ref": "#/$defs/PtzAutotrackConfig", "description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.", "title": "Autotracking"}, "ignore_time_mismatch": {"default": false, "description": "Ignore time synchronization differences between camera and Frigate server for ONVIF communication.", "title": "Ignore time mismatch", "type": "boolean"}}, "title": "OnvifConfig", "type": "object"}, "PixelFormatEnum": {"enum": ["rgb", "bgr", "yuv"], "title": "PixelFormatEnum", "type": "string"}, "ProfileDefinitionConfig": {"additionalProperties": false, "description": "Defines a named profile with a human-readable display name.\n\nThe dict key is the machine name used internally; friendly_name\nis the label shown in the UI and API responses.", "properties": {"friendly_name": {"description": "Display name for this profile shown in the UI.", "title": "Friendly name", "type": "string"}}, "required": ["friendly_name"], "title": "ProfileDefinitionConfig", "type": "object"}, "ProxyConfig": {"additionalProperties": false, "properties": {"header_map": {"$ref": "#/$defs/HeaderMappingConfig", "description": "Map incoming proxy headers to Frigate user and role fields for proxy-based auth.", "title": "Header mapping"}, "logout_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "URL to redirect users to when logging out via the proxy.", "title": "Logout URL"}, "auth_secret": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional secret checked against the X-Proxy-Secret header to verify trusted proxies.", "title": "Proxy secret"}, "default_role": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": "viewer", "description": "Default role assigned to proxy-authenticated users when no role mapping applies.", "title": "Default role"}, "separator": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": ",", "description": "Character used to split multiple values provided in proxy headers.", "title": "Separator character"}}, "title": "ProxyConfig", "type": "object"}, "PtzAutotrackConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable automatic PTZ camera tracking of detected objects.", "title": "Enable Autotracking", "type": "boolean"}, "calibrate_on_startup": {"default": false, "description": "Measure PTZ motor speeds on startup to improve tracking accuracy. Frigate will update config with movement_weights after calibration.", "title": "Calibrate on start", "type": "boolean"}, "zooming": {"$ref": "#/$defs/ZoomingModeEnum", "default": "disabled", "description": "Control zoom behavior: disabled (pan/tilt only), absolute (most compatible), or relative (concurrent pan/tilt/zoom).", "title": "Zoom mode"}, "zoom_factor": {"default": 0.3, "description": "Control zoom level on tracked objects. Lower values keep more scene in view; higher values zoom in closer but may lose tracking. Values between 0.1 and 0.75.", "maximum": 0.75, "minimum": 0.1, "title": "Zoom factor", "type": "number"}, "track": {"default": ["person"], "description": "List of object types that should trigger autotracking.", "items": {"type": "string"}, "title": "Tracked objects", "type": "array"}, "required_zones": {"description": "Objects must enter one of these zones before autotracking begins.", "items": {"type": "string"}, "title": "Required zones", "type": "array"}, "return_preset": {"default": "home", "description": "ONVIF preset name configured in camera firmware to return to after tracking ends.", "title": "Return preset", "type": "string"}, "timeout": {"default": 10, "description": "Wait this many seconds after losing tracking before returning camera to preset position.", "title": "Return timeout", "type": "integer"}, "movement_weights": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "description": "Calibration values automatically generated by camera calibration. Do not modify manually.", "title": "Movement weights"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Internal field to track whether autotracking was enabled in configuration.", "title": "Original autotrack state"}}, "title": "PtzAutotrackConfig", "type": "object"}, "RecordConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable recording for all cameras; can be overridden per-camera.", "title": "Enable recording", "type": "boolean"}, "expire_interval": {"default": 60, "description": "Minutes between cleanup passes that remove expired recording segments.", "title": "Record cleanup interval", "type": "integer"}, "continuous": {"$ref": "#/$defs/RecordRetainConfig", "description": "Number of days to retain recordings regardless of tracked objects or motion. Set to 0 if you only want to retain recordings of alerts and detections.", "title": "Continuous retention"}, "motion": {"$ref": "#/$defs/RecordRetainConfig", "description": "Number of days to retain recordings triggered by motion regardless of tracked objects. Set to 0 if you only want to retain recordings of alerts and detections.", "title": "Motion retention"}, "detections": {"$ref": "#/$defs/EventsConfig", "description": "Recording retention settings for detection events including pre/post capture durations.", "title": "Detection retention"}, "alerts": {"$ref": "#/$defs/EventsConfig", "description": "Recording retention settings for alert events including pre/post capture durations.", "title": "Alert retention"}, "export": {"$ref": "#/$defs/RecordExportConfig", "description": "Settings used when exporting recordings such as timelapse and hardware acceleration.", "title": "Export config"}, "preview": {"$ref": "#/$defs/RecordPreviewConfig", "description": "Settings controlling the quality of recording previews shown in the UI.", "title": "Preview config"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether recording was enabled in the original static configuration.", "title": "Original recording state"}}, "title": "RecordConfig", "type": "object"}, "RecordExportConfig": {"additionalProperties": false, "properties": {"hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "auto", "description": "Hardware acceleration args to use for export/transcode operations.", "title": "Export hwaccel args"}, "max_concurrent": {"default": 3, "description": "Maximum number of export jobs to process at the same time.", "minimum": 1, "title": "Maximum concurrent exports", "type": "integer"}}, "title": "RecordExportConfig", "type": "object"}, "RecordPreviewConfig": {"additionalProperties": false, "properties": {"quality": {"$ref": "#/$defs/RecordQualityEnum", "default": "medium", "description": "Preview quality level (very_low, low, medium, high, very_high).", "title": "Preview quality"}}, "title": "RecordPreviewConfig", "type": "object"}, "RecordQualityEnum": {"enum": ["very_low", "low", "medium", "high", "very_high"], "title": "RecordQualityEnum", "type": "string"}, "RecordRetainConfig": {"additionalProperties": false, "properties": {"days": {"default": 0, "description": "Days to retain recordings.", "minimum": 0.0, "title": "Retention days", "type": "number"}}, "title": "RecordRetainConfig", "type": "object"}, "ReplaceRule": {"additionalProperties": false, "properties": {"pattern": {"title": "Regex pattern", "type": "string"}, "replacement": {"title": "Replacement string", "type": "string"}}, "required": ["pattern", "replacement"], "title": "ReplaceRule", "type": "object"}, "RestreamConfig": {"additionalProperties": true, "properties": {}, "title": "RestreamConfig", "type": "object"}, "RetainConfig": {"additionalProperties": false, "properties": {"default": {"type": "number", "default": 10, "title": "Default retention", "description": "Default number of days to retain snapshots."}, "objects": {"additionalProperties": {"type": "number"}, "description": "Per-object overrides for snapshot retention days.", "title": "Object retention", "type": "object"}}, "title": "RetainConfig", "type": "object"}, "RetainModeEnum": {"enum": ["all", "motion", "active_objects"], "title": "RetainModeEnum", "type": "string"}, "ReviewConfig": {"additionalProperties": false, "properties": {"alerts": {"$ref": "#/$defs/AlertsConfig", "description": "Settings for which tracked objects generate alerts and how alerts are retained.", "title": "Alerts config"}, "detections": {"$ref": "#/$defs/DetectionsConfig", "description": "Settings for which tracked objects generate detections (non-alert) and how detections are retained.", "title": "Detections config"}, "genai": {"$ref": "#/$defs/GenAIReviewConfig", "description": "Controls use of generative AI for producing descriptions and summaries of review items.", "title": "GenAI config"}}, "title": "ReviewConfig", "type": "object"}, "ReviewRetainConfig": {"additionalProperties": false, "properties": {"days": {"default": 10, "description": "Number of days to retain recordings of detection events.", "minimum": 0.0, "title": "Retention days", "type": "number"}, "mode": {"$ref": "#/$defs/RetainModeEnum", "default": "motion", "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects).", "title": "Retention mode"}}, "title": "ReviewRetainConfig", "type": "object"}, "SemanticSearchConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable the semantic search feature.", "title": "Enable semantic search", "type": "boolean"}, "reindex": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "Trigger a full reindex of historical tracked objects into the embeddings database.", "title": "Reindex on startup"}, "model": {"anyOf": [{"$ref": "#/$defs/SemanticSearchModelEnum"}, {"type": "string"}, {"type": "null"}], "default": "jinav1", "description": "The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role.", "title": "Semantic search model or GenAI provider name"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Select model size; 'small' runs on CPU and 'large' typically requires GPU.", "title": "Model size"}, "device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", "title": "Device"}}, "title": "SemanticSearchConfig", "type": "object"}, "SemanticSearchModelEnum": {"enum": ["jinav1", "jinav2"], "title": "SemanticSearchModelEnum", "type": "string"}, "SnapshotsConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable saving snapshots for all cameras; can be overridden per-camera.", "title": "Enable snapshots", "type": "boolean"}, "timestamp": {"default": false, "description": "Overlay a timestamp on snapshots from API.", "title": "Timestamp overlay", "type": "boolean"}, "bounding_box": {"default": true, "description": "Draw bounding boxes for tracked objects on snapshots from API.", "title": "Bounding box overlay", "type": "boolean"}, "crop": {"default": false, "description": "Crop snapshots from API to the detected object's bounding box.", "title": "Crop snapshot", "type": "boolean"}, "required_zones": {"description": "Zones an object must enter for a snapshot to be saved.", "items": {"type": "string"}, "title": "Required zones", "type": "array"}, "height": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Height (pixels) to resize snapshots from API to; leave empty to preserve original size.", "title": "Snapshot height"}, "retain": {"$ref": "#/$defs/RetainConfig", "description": "Retention settings for snapshots including default days and per-object overrides.", "title": "Snapshot retention"}, "quality": {"default": 60, "description": "Encode quality for saved snapshots (0-100).", "maximum": 100, "minimum": 0, "title": "Snapshot quality", "type": "integer"}}, "title": "SnapshotsConfig", "type": "object"}, "StationaryConfig": {"additionalProperties": false, "properties": {"interval": {"anyOf": [{"exclusiveMinimum": 0, "type": "integer"}, {"type": "null"}], "default": null, "description": "How often (in frames) to run a detection check to confirm a stationary object.", "title": "Stationary interval"}, "threshold": {"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}], "default": null, "description": "Number of frames with no position change required to mark an object as stationary.", "title": "Stationary threshold"}, "max_frames": {"$ref": "#/$defs/StationaryMaxFramesConfig", "description": "Limits how long stationary objects are tracked before being discarded.", "title": "Max frames"}, "classifier": {"default": true, "description": "Use a visual classifier to detect truly stationary objects even when bounding boxes jitter.", "title": "Enable visual classifier", "type": "boolean"}}, "title": "StationaryConfig", "type": "object"}, "StationaryMaxFramesConfig": {"additionalProperties": false, "properties": {"default": {"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}], "default": null, "title": "Default max frames", "description": "Default maximum frames to track a stationary object before stopping."}, "objects": {"additionalProperties": {"type": "integer"}, "description": "Per-object overrides for maximum frames to track stationary objects.", "title": "Object max frames", "type": "object"}}, "title": "StationaryMaxFramesConfig", "type": "object"}, "StatsConfig": {"additionalProperties": false, "properties": {"amd_gpu_stats": {"default": true, "description": "Enable collection of AMD GPU statistics if an AMD GPU is present.", "title": "AMD GPU stats", "type": "boolean"}, "intel_gpu_stats": {"default": true, "description": "Enable collection of Intel GPU statistics if an Intel GPU is present.", "title": "Intel GPU stats", "type": "boolean"}, "network_bandwidth": {"default": false, "description": "Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities).", "title": "Network bandwidth", "type": "boolean"}, "intel_gpu_device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present.", "title": "Intel GPU device"}}, "title": "StatsConfig", "type": "object"}, "TelemetryConfig": {"additionalProperties": false, "properties": {"network_interfaces": {"default": [], "description": "List of network interface name prefixes to monitor for bandwidth statistics.", "items": {"type": "string"}, "title": "Network interfaces", "type": "array"}, "stats": {"$ref": "#/$defs/StatsConfig", "description": "Options to enable/disable collection of various system and GPU statistics.", "title": "System stats"}, "version_check": {"default": true, "description": "Enable an outbound check to detect if a newer Frigate version is available.", "title": "Version check", "type": "boolean"}}, "title": "TelemetryConfig", "type": "object"}, "TimeFormatEnum": {"enum": ["browser", "12hour", "24hour"], "title": "TimeFormatEnum", "type": "string"}, "TimestampEffectEnum": {"enum": ["solid", "shadow"], "title": "TimestampEffectEnum", "type": "string"}, "TimestampPositionEnum": {"enum": ["tl", "tr", "bl", "br"], "title": "TimestampPositionEnum", "type": "string"}, "TimestampStyleConfig": {"additionalProperties": false, "properties": {"position": {"$ref": "#/$defs/TimestampPositionEnum", "default": "tl", "description": "Position of the timestamp on the image (tl/tr/bl/br).", "title": "Timestamp position"}, "format": {"default": "%m/%d/%Y %H:%M:%S", "description": "Datetime format string used for timestamps (Python datetime format codes).", "title": "Timestamp format", "type": "string"}, "color": {"$ref": "#/$defs/ColorConfig", "description": "RGB color values for the timestamp text (all values 0-255).", "title": "Timestamp color"}, "thickness": {"default": 2, "description": "Line thickness of the timestamp text.", "title": "Timestamp thickness", "type": "integer"}, "effect": {"anyOf": [{"$ref": "#/$defs/TimestampEffectEnum"}, {"type": "null"}], "default": null, "description": "Visual effect for the timestamp text (none, solid, shadow).", "title": "Timestamp effect"}}, "title": "TimestampStyleConfig", "type": "object"}, "TlsConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable TLS for Frigate's web UI and API on the configured TLS port.", "title": "Enable TLS", "type": "boolean"}}, "title": "TlsConfig", "type": "object"}, "TriggerAction": {"enum": ["notification", "sub_label", "attribute"], "title": "TriggerAction", "type": "string"}, "TriggerConfig": {"additionalProperties": false, "properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional friendly name displayed in the UI for this trigger.", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enable or disable this semantic search trigger.", "title": "Enable this trigger", "type": "boolean"}, "type": {"$ref": "#/$defs/TriggerType", "default": "description", "description": "Type of trigger: 'thumbnail' (match against image) or 'description' (match against text).", "title": "Trigger type"}, "data": {"description": "Text phrase or thumbnail ID to match against tracked objects.", "title": "Trigger content", "type": "string"}, "threshold": {"default": 0.8, "description": "Minimum similarity score (0-1) required to activate this trigger.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Trigger threshold", "type": "number"}, "actions": {"default": [], "description": "List of actions to execute when trigger matches (notification, sub_label, attribute).", "items": {"$ref": "#/$defs/TriggerAction"}, "title": "Trigger actions", "type": "array"}}, "required": ["data"], "title": "TriggerConfig", "type": "object"}, "TriggerType": {"enum": ["thumbnail", "description"], "title": "TriggerType", "type": "string"}, "UIConfig": {"additionalProperties": false, "properties": {"timezone": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional timezone to display across the UI (defaults to browser local time if unset).", "title": "Timezone"}, "time_format": {"$ref": "#/$defs/TimeFormatEnum", "default": "browser", "description": "Time format to use in the UI (browser, 12hour, or 24hour).", "title": "Time format"}, "unit_system": {"$ref": "#/$defs/UnitSystemEnum", "default": "metric", "description": "Unit system for display (metric or imperial) used in the UI and MQTT.", "title": "Unit system"}}, "title": "UIConfig", "type": "object"}, "UnitSystemEnum": {"enum": ["imperial", "metric"], "title": "UnitSystemEnum", "type": "string"}, "ZoneConfig": {"properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used.", "title": "Zone name"}, "enabled": {"default": true, "description": "Enable or disable this zone. Disabled zones are ignored at runtime.", "title": "Enabled", "type": "boolean"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Keep track of original state of zone."}, "filters": {"additionalProperties": {"$ref": "#/$defs/FilterConfig"}, "description": "Filters to apply to objects within this zone. Used to reduce false positives or restrict which objects are considered present in the zone.", "title": "Zone filters", "type": "object"}, "coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Polygon coordinates that define the zone area. Can be a comma-separated string or a list of coordinate strings. Coordinates should be relative (0-1) or absolute (legacy).", "title": "Coordinates"}, "distances": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "description": "Optional real-world distances for each side of the zone quadrilateral, used for speed or distance calculations. Must have exactly 4 values if set.", "title": "Real-world distances"}, "inertia": {"default": 3, "description": "Number of consecutive frames an object must be detected in the zone before it is considered present. Helps filter out transient detections.", "exclusiveMinimum": 0, "title": "Inertia frames", "type": "integer"}, "loitering_time": {"default": 0, "description": "Number of seconds an object must remain in the zone to be considered as loitering. Set to 0 to disable loitering detection.", "minimum": 0, "title": "Loitering seconds", "type": "integer"}, "speed_threshold": {"anyOf": [{"minimum": 0.1, "type": "number"}, {"type": "null"}], "default": null, "description": "Minimum speed (in real-world units if distances are set) required for an object to be considered present in the zone. Used for speed-based zone triggers.", "title": "Minimum speed"}, "objects": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "List of object types (from labelmap) that can trigger this zone. Can be a string or a list of strings. If empty, all objects are considered.", "title": "Trigger objects"}}, "required": ["coordinates"], "title": "ZoneConfig", "type": "object"}, "ZoomingModeEnum": {"enum": ["disabled", "absolute", "relative"], "title": "ZoomingModeEnum", "type": "string"}}, "additionalProperties": false, "properties": {"version": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Numeric or string version of the active configuration to help detect migrations or format changes.", "title": "Current config version"}, "safe_mode": {"default": false, "description": "When enabled, start Frigate in safe mode with reduced features for troubleshooting.", "title": "Safe mode", "type": "boolean"}, "environment_vars": {"additionalProperties": {"type": "string"}, "description": "Key/value pairs of environment variables to set for the Frigate process in Home Assistant OS. Non-HAOS users must use Docker environment variable configuration instead.", "title": "Environment variables", "type": "object"}, "logger": {"$ref": "#/$defs/LoggerConfig", "description": "Controls default log verbosity and per-component log level overrides.", "title": "Logging"}, "auth": {"$ref": "#/$defs/AuthConfig", "description": "Authentication and session-related settings including cookie and rate limit options.", "title": "Authentication"}, "database": {"$ref": "#/$defs/DatabaseConfig", "description": "Settings for the SQLite database used by Frigate to store tracked object and recording metadata.", "title": "Database"}, "go2rtc": {"$ref": "#/$defs/RestreamConfig", "description": "Settings for the integrated go2rtc restreaming service used for live stream relaying and translation.", "title": "go2rtc"}, "mqtt": {"$ref": "#/$defs/MqttConfig", "description": "Settings for connecting and publishing telemetry, snapshots, and event details to an MQTT broker.", "title": "MQTT"}, "notifications": {"$ref": "#/$defs/NotificationConfig", "description": "Settings to enable and control notifications for all cameras; can be overridden per-camera.", "title": "Notifications"}, "networking": {"$ref": "#/$defs/NetworkingConfig", "description": "Network-related settings such as IPv6 enablement for Frigate endpoints.", "title": "Networking"}, "proxy": {"$ref": "#/$defs/ProxyConfig", "description": "Settings for integrating Frigate behind a reverse proxy that passes authenticated user headers.", "title": "Proxy"}, "telemetry": {"$ref": "#/$defs/TelemetryConfig", "description": "System telemetry and stats options including GPU and network bandwidth monitoring.", "title": "Telemetry"}, "tls": {"$ref": "#/$defs/TlsConfig", "description": "TLS settings for Frigate's web endpoints (port 8971).", "title": "TLS"}, "ui": {"$ref": "#/$defs/UIConfig", "description": "User interface preferences such as timezone, time/date formatting, and units.", "title": "UI"}, "detectors": {"additionalProperties": {"$ref": "#/$defs/BaseDetectorConfig"}, "default": {"cpu": {"type": "cpu"}}, "description": "Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.", "title": "Detector hardware", "type": "object"}, "model": {"$ref": "#/$defs/ModelConfig", "description": "Settings to configure a custom object detection model and its input shape.", "title": "Detection model"}, "genai": {"additionalProperties": {"$ref": "#/$defs/GenAIConfig"}, "description": "Settings for integrated generative AI providers used to generate object descriptions and review summaries.", "title": "Generative AI configuration", "type": "object"}, "cameras": {"additionalProperties": {"$ref": "#/$defs/CameraConfig"}, "description": "Cameras", "title": "Cameras", "type": "object"}, "audio": {"$ref": "#/$defs/AudioConfig", "description": "Settings for audio-based event detection for all cameras; can be overridden per-camera.", "title": "Audio detection"}, "birdseye": {"$ref": "#/$defs/BirdseyeConfig", "description": "Settings for the Birdseye composite view that composes multiple camera feeds into a single layout.", "title": "Birdseye"}, "detect": {"$ref": "#/$defs/DetectConfig", "description": "Settings for the detection/detect role used to run object detection and initialize trackers.", "title": "Object Detection"}, "ffmpeg": {"$ref": "#/$defs/FfmpegConfig", "description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.", "title": "FFmpeg"}, "live": {"$ref": "#/$defs/CameraLiveConfig", "description": "Settings to control the jsmpeg live stream resolution and quality. This does not affect restreamed cameras that use go2rtc for live view.", "title": "Live playback"}, "motion": {"anyOf": [{"$ref": "#/$defs/MotionConfig"}, {"type": "null"}], "default": null, "description": "Default motion detection settings applied to cameras unless overridden per-camera.", "title": "Motion detection"}, "objects": {"$ref": "#/$defs/ObjectConfig", "description": "Object tracking defaults including which labels to track and per-object filters.", "title": "Objects"}, "record": {"$ref": "#/$defs/RecordConfig", "description": "Recording and retention settings applied to cameras unless overridden per-camera.", "title": "Recording"}, "review": {"$ref": "#/$defs/ReviewConfig", "description": "Settings that control alerts, detections, and GenAI review summaries used by the UI and storage.", "title": "Review"}, "snapshots": {"$ref": "#/$defs/SnapshotsConfig", "description": "Settings for API-generated snapshots of tracked objects for all cameras; can be overridden per-camera.", "title": "Snapshots"}, "timestamp_style": {"$ref": "#/$defs/TimestampStyleConfig", "description": "Styling options for in-feed timestamps applied to debug view and snapshots.", "title": "Timestamp style"}, "audio_transcription": {"$ref": "#/$defs/AudioTranscriptionConfig", "description": "Settings for live and speech audio transcription used for events and live captions.", "title": "Audio transcription"}, "classification": {"$ref": "#/$defs/ClassificationConfig", "description": "Settings for classification models used to refine object labels or state classification.", "title": "Object classification"}, "semantic_search": {"$ref": "#/$defs/SemanticSearchConfig", "description": "Settings for Semantic Search which builds and queries object embeddings to find similar items.", "title": "Semantic Search"}, "face_recognition": {"$ref": "#/$defs/FaceRecognitionConfig", "description": "Settings for face detection and recognition for all cameras; can be overridden per-camera.", "title": "Face recognition"}, "lpr": {"$ref": "#/$defs/LicensePlateRecognitionConfig", "description": "License plate recognition settings including detection thresholds, formatting, and known plates.", "title": "License Plate Recognition"}, "camera_groups": {"additionalProperties": {"$ref": "#/$defs/CameraGroupConfig"}, "description": "Configuration for named camera groups used to organize cameras in the UI.", "title": "Camera groups", "type": "object"}, "profiles": {"additionalProperties": {"$ref": "#/$defs/ProfileDefinitionConfig"}, "description": "Named profile definitions with friendly names. Camera profiles must reference names defined here.", "title": "Profiles", "type": "object"}, "active_profile": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Currently active profile name. Runtime-only, not persisted in YAML.", "title": "Active profile"}}, "required": ["mqtt", "cameras"], "title": "FrigateConfig", "type": "object"} \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/config-snapshot.json b/web/e2e/fixtures/mock-data/config-snapshot.json new file mode 100644 index 0000000..2df402b --- /dev/null +++ b/web/e2e/fixtures/mock-data/config-snapshot.json @@ -0,0 +1 @@ +{"version": null, "safe_mode": false, "environment_vars": {}, "logger": {"default": "info", "logs": {}}, "auth": {"enabled": true, "reset_admin_password": false, "cookie_name": "frigate_token", "cookie_secure": false, "session_length": 86400, "refresh_time": 1800, "failed_login_rate_limit": null, "trusted_proxies": [], "hash_iterations": 600000, "roles": {"admin": [], "viewer": []}, "admin_first_time_login": false}, "database": {"path": "/config/frigate.db"}, "go2rtc": {}, "mqtt": {"enabled": true, "host": "mqtt", "port": 1883, "topic_prefix": "frigate", "client_id": "frigate", "stats_interval": 60, "user": null, "password": null, "tls_ca_certs": null, "tls_client_cert": null, "tls_client_key": null, "tls_insecure": null, "qos": 0}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "networking": {"ipv6": {"enabled": false}, "listen": {"internal": 5000, "external": 8971}}, "proxy": {"header_map": {"user": null, "role": null, "role_map": {}}, "logout_url": null, "auth_secret": null, "default_role": "viewer", "separator": ","}, "telemetry": {"network_interfaces": [], "stats": {"amd_gpu_stats": true, "intel_gpu_stats": true, "network_bandwidth": false, "intel_gpu_device": null}, "version_check": true}, "tls": {"enabled": true}, "ui": {"timezone": null, "time_format": "browser", "unit_system": "metric"}, "detectors": {"cpu": {"type": "cpu", "model": {"path": "/cpu_model.tflite", "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd"}, "model_path": null}}, "model": {"path": null, "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd", "all_attributes": ["amazon", "an_post", "canada_post", "dhl", "dpd", "face", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "colormap": {}}, "genai": {}, "cameras": {"front_door": {"name": "front_door", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"front_door": "front_door"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true, "review": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "backyard": {"name": "backyard", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"backyard": "backyard"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true, "review": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "garage": {"name": "garage", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"garage": "garage"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true, "review": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}}, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": null, "num_threads": 2}, "birdseye": {"enabled": true, "mode": "objects", "restream": false, "width": 1280, "height": 720, "quality": 8, "inactivity_threshold": 30, "layout": {"scaling_factor": 2.0, "max_cameras": null}, "idle_heartbeat_fps": 0.0}, "detect": {"enabled": false, "height": null, "width": null, "fps": 5, "min_initialized": null, "max_disappeared": null, "stationary": {"interval": null, "threshold": null, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0}, "live": {"streams": [], "height": 720, "quality": 8}, "motion": null, "objects": {"track": ["person"], "filters": {"royal_mail": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "an_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "ups": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnord": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dhl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "usps": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "face": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "license_plate": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dpd": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "amazon": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "fedex": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "canada_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "nzpost": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "gls": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "purolator": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": null}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": null}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": null, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": null}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": null, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "audio_transcription": {"enabled": false, "language": "en", "device": "CPU", "model_size": "small", "live_enabled": false}, "classification": {"bird": {"enabled": false, "threshold": 0.9}, "custom": {}}, "semantic_search": {"enabled": false, "reindex": false, "model": "jinav1", "model_size": "small", "device": null}, "face_recognition": {"enabled": false, "model_size": "small", "unknown_score": 0.8, "detection_threshold": 0.7, "recognition_threshold": 0.9, "min_area": 750, "min_faces": 1, "save_attempts": 200, "blur_confidence_filter": true, "device": null}, "lpr": {"enabled": false, "model_size": "small", "detection_threshold": 0.7, "min_area": 1000, "recognition_threshold": 0.9, "min_plate_length": 4, "format": null, "match_distance": 1, "known_plates": {}, "enhancement": 0, "debug_save_plates": false, "device": null, "replace_rules": []}, "camera_groups": {"default": {"cameras": ["front_door", "backyard", "garage"], "icon": "generic", "order": 0}, "outdoor": {"cameras": ["front_door", "backyard"], "icon": "generic", "order": 1}}, "profiles": {}} \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/config.ts b/web/e2e/fixtures/mock-data/config.ts new file mode 100644 index 0000000..ba86425 --- /dev/null +++ b/web/e2e/fixtures/mock-data/config.ts @@ -0,0 +1,76 @@ +/** + * FrigateConfig factory for E2E tests. + * + * Uses a real config snapshot generated from the Python backend's FrigateConfig + * model. This guarantees all fields are present and match what the app expects. + * Tests override specific fields via DeepPartial. + */ + +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const configSnapshot = JSON.parse( + readFileSync(resolve(__dirname, "config-snapshot.json"), "utf-8"), +); + +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; + +function deepMerge>( + base: T, + overrides?: DeepPartial, +): T { + if (!overrides) return base; + const result = { ...base }; + for (const key of Object.keys(overrides) as (keyof T)[]) { + const val = overrides[key]; + if ( + val !== undefined && + typeof val === "object" && + val !== null && + !Array.isArray(val) && + typeof base[key] === "object" && + base[key] !== null && + !Array.isArray(base[key]) + ) { + result[key] = deepMerge( + base[key] as Record, + val as DeepPartial>, + ) as T[keyof T]; + } else if (val !== undefined) { + result[key] = val as T[keyof T]; + } + } + return result; +} + +// The base config is a real snapshot from the Python backend. +// Apply test-specific overrides: friendly names, camera groups, version. +export const BASE_CONFIG = { + ...configSnapshot, + version: "0.15.0-test", + cameras: { + ...configSnapshot.cameras, + front_door: { + ...configSnapshot.cameras.front_door, + friendly_name: "Front Door", + }, + backyard: { + ...configSnapshot.cameras.backyard, + friendly_name: "Backyard", + }, + garage: { + ...configSnapshot.cameras.garage, + friendly_name: "Garage", + }, + }, +}; + +export function configFactory( + overrides?: DeepPartial, +): typeof BASE_CONFIG { + return deepMerge(BASE_CONFIG, overrides); +} diff --git a/web/e2e/fixtures/mock-data/debug-replay.ts b/web/e2e/fixtures/mock-data/debug-replay.ts new file mode 100644 index 0000000..9b9d2c6 --- /dev/null +++ b/web/e2e/fixtures/mock-data/debug-replay.ts @@ -0,0 +1,54 @@ +/** + * Debug replay status factory. + * + * The Replay page polls /api/debug_replay/status every 1s via SWR. + * The no-session state shows an empty state; the active state + * renders the live camera image + debug toggles + objects/messages + * tabs. Used by replay.spec.ts. + */ + +export type DebugReplayStatus = { + active: boolean; + replay_camera: string | null; + source_camera: string | null; + start_time: number | null; + end_time: number | null; + live_ready: boolean; +}; + +export function noSessionStatus(): DebugReplayStatus { + return { + active: false, + replay_camera: null, + source_camera: null, + start_time: null, + end_time: null, + live_ready: false, + }; +} + +export function activeSessionStatus( + opts: { + camera?: string; + sourceCamera?: string; + startTime?: number; + endTime?: number; + liveReady?: boolean; + } = {}, +): DebugReplayStatus { + const { + camera = "front_door", + sourceCamera = "front_door", + startTime = Date.now() / 1000 - 3600, + endTime = Date.now() / 1000 - 1800, + liveReady = true, + } = opts; + return { + active: true, + replay_camera: camera, + source_camera: sourceCamera, + start_time: startTime, + end_time: endTime, + live_ready: liveReady, + }; +} diff --git a/web/e2e/fixtures/mock-data/events.json b/web/e2e/fixtures/mock-data/events.json new file mode 100644 index 0000000..fa698a9 --- /dev/null +++ b/web/e2e/fixtures/mock-data/events.json @@ -0,0 +1 @@ +[{"id": "event-person-001", "label": "person", "sub_label": null, "camera": "front_door", "start_time": 1780677009.365581, "end_time": 1780677039.365581, "false_positive": false, "zones": ["front_yard"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "abc123", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.92, "score": 0.92, "region": [0.1, 0.1, 0.5, 0.8], "box": [0.2, 0.15, 0.45, 0.75], "area": 0.18, "ratio": 0.6, "type": "object", "description": "A person walking toward the front door", "average_estimated_speed": 1.2, "velocity_angle": 45.0, "path_data": [[[0.2, 0.5], 0.0], [[0.3, 0.5], 1.0]]}}, {"id": "event-car-001", "label": "car", "sub_label": null, "camera": "backyard", "start_time": 1780673409.365581, "end_time": 1780673454.365581, "false_positive": false, "zones": ["driveway"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "def456", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.87, "score": 0.87, "region": [0.3, 0.2, 0.9, 0.7], "box": [0.35, 0.25, 0.85, 0.65], "area": 0.2, "ratio": 1.25, "type": "object", "description": "A car parked in the driveway", "average_estimated_speed": 0.0, "velocity_angle": 0.0, "path_data": []}}, {"id": "event-person-002", "label": "person", "sub_label": null, "camera": "garage", "start_time": 1780669809.365581, "end_time": 1780669829.365581, "false_positive": false, "zones": [], "thumbnail": null, "has_clip": false, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "ghi789", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.78, "score": 0.78, "region": [0.0, 0.0, 0.6, 0.9], "box": [0.1, 0.05, 0.5, 0.85], "area": 0.32, "ratio": 0.5, "type": "object", "description": null, "average_estimated_speed": 0.5, "velocity_angle": 90.0, "path_data": [[[0.1, 0.4], 0.0]]}}] \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/exports.json b/web/e2e/fixtures/mock-data/exports.json new file mode 100644 index 0000000..7e6c787 --- /dev/null +++ b/web/e2e/fixtures/mock-data/exports.json @@ -0,0 +1 @@ +[{"id": "export-001", "camera": "front_door", "name": "Front Door - Person Alert", "date": 1780680609.365581, "video_path": "/exports/export-001.mp4", "thumb_path": "/exports/export-001-thumb.jpg", "in_progress": false, "export_case_id": null}, {"id": "export-002", "camera": "backyard", "name": "Backyard - Car Detection", "date": 1780673409.365581, "video_path": "/exports/export-002.mp4", "thumb_path": "/exports/export-002-thumb.jpg", "in_progress": false, "export_case_id": "case-001"}, {"id": "export-003", "camera": "garage", "name": "Garage - In Progress", "date": 1780682409.365581, "video_path": "/exports/export-003.mp4", "thumb_path": "/exports/export-003-thumb.jpg", "in_progress": true, "export_case_id": null}] \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/faces.ts b/web/e2e/fixtures/mock-data/faces.ts new file mode 100644 index 0000000..ed2f944 --- /dev/null +++ b/web/e2e/fixtures/mock-data/faces.ts @@ -0,0 +1,45 @@ +/** + * Face library factories. + * + * The /api/faces endpoint returns a record keyed by collection name + * with the list of face image filenames. Grouped training attempts + * live under the "train" key with filenames of the form + * `${event_id}-${timestamp}-${label}-${score}.webp`. + * + * Used by face-library.spec.ts and chat.spec.ts (attachment chip). + */ + +export type FacesMock = Record; + +export function basicFacesMock(): FacesMock { + return { + alice: ["alice-1.webp", "alice-2.webp"], + bob: ["bob-1.webp"], + charlie: ["charlie-1.webp"], + }; +} + +export function emptyFacesMock(): FacesMock { + return {}; +} + +/** + * Adds a grouped recent-recognition training attempt to an existing + * faces mock. The grouping key on the backend is the event id — so + * images with the same event-id prefix render as one dialog-able card. + */ +export function withGroupedTrainingAttempt( + base: FacesMock, + opts: { + eventId: string; + attempts: Array<{ timestamp: number; label: string; score: number }>; + }, +): FacesMock { + const trainImages = opts.attempts.map( + (a) => `${opts.eventId}-${a.timestamp}-${a.label}-${a.score}.webp`, + ); + return { + ...base, + train: [...(base.train ?? []), ...trainImages], + }; +} diff --git a/web/e2e/fixtures/mock-data/generate-mock-data.py b/web/e2e/fixtures/mock-data/generate-mock-data.py new file mode 100644 index 0000000..bba488d --- /dev/null +++ b/web/e2e/fixtures/mock-data/generate-mock-data.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 +"""Generate E2E mock data from backend Pydantic and Peewee models. + +Run from the repo root: + PYTHONPATH=/workspace/frigate python3 web/e2e/fixtures/mock-data/generate-mock-data.py + +Strategy: + - FrigateConfig: instantiate the Pydantic config model, then model_dump() + - API responses: instantiate Pydantic response models (ReviewSegmentResponse, + EventResponse, ExportModel, ExportCaseModel) to validate all required fields + - If the backend adds a required field, this script fails at instantiation time + - The Peewee model field list is checked to detect new columns that would + appear in .dicts() API responses but aren't in our mock data +""" + +import json +import sys +import time +import warnings +from datetime import datetime, timedelta +from pathlib import Path + +warnings.filterwarnings("ignore") + +OUTPUT_DIR = Path(__file__).parent +NOW = time.time() +HOUR = 3600 + +CAMERAS = ["front_door", "backyard", "garage"] + + +def check_pydantic_fields(pydantic_class, mock_keys, model_name): + """Verify mock data covers all fields declared in the Pydantic response model. + + The Pydantic response model is what the frontend actually receives. + Peewee models may have extra legacy columns that are filtered out by + FastAPI's response_model validation. + """ + required_fields = set() + for name, field_info in pydantic_class.model_fields.items(): + required_fields.add(name) + + missing = required_fields - mock_keys + if missing: + print( + f" ERROR: {model_name} response model has fields not in mock data: {missing}", + file=sys.stderr, + ) + print( + f" Add these fields to the mock data in this script.", + file=sys.stderr, + ) + sys.exit(1) + + extra = mock_keys - required_fields + if extra: + print( + f" NOTE: {model_name} mock data has extra fields (not in response model): {extra}", + ) + + +def generate_config(): + """Generate FrigateConfig from the Python backend model.""" + from frigate.config import FrigateConfig + + config = FrigateConfig.model_validate_json( + json.dumps( + { + "mqtt": {"host": "mqtt"}, + "cameras": { + cam: { + "ffmpeg": { + "inputs": [ + { + "path": f"rtsp://10.0.0.{i+1}:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 720, "width": 1280, "fps": 5}, + } + for i, cam in enumerate(CAMERAS) + }, + "camera_groups": { + "default": { + "cameras": CAMERAS, + "icon": "generic", + "order": 0, + }, + "outdoor": { + "cameras": ["front_door", "backyard"], + "icon": "generic", + "order": 1, + }, + }, + } + ) + ) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + snapshot = config.model_dump() + + # Runtime-computed fields not in the Pydantic dump + all_attrs = set() + for attrs in snapshot.get("model", {}).get("attributes_map", {}).values(): + all_attrs.update(attrs) + snapshot["model"]["all_attributes"] = sorted(all_attrs) + snapshot["model"]["colormap"] = {} + + return snapshot + + +def generate_config_schema(): + """Generate the JSON Schema for FrigateConfig from the backend model. + + This is what the app fetches from /api/config/schema.json to drive the + RJSF-based config form. Generating it here keeps the e2e fixture in sync + with the backend whenever config models change. + """ + from frigate.config import FrigateConfig + + return FrigateConfig.model_json_schema() + + +def generate_reviews(): + """Generate ReviewSegmentResponse[] validated against Pydantic + Peewee.""" + from frigate.api.defs.response.review_response import ReviewSegmentResponse + + reviews = [ + ReviewSegmentResponse( + id="review-alert-001", + camera="front_door", + severity="alert", + start_time=datetime.fromtimestamp(NOW - 2 * HOUR), + end_time=datetime.fromtimestamp(NOW - 2 * HOUR + 30), + has_been_reviewed=False, + thumb_path="/clips/front_door/review-alert-001-thumb.jpg", + data=json.dumps( + { + "audio": [], + "detections": ["person-abc123"], + "objects": ["person"], + "sub_labels": [], + "significant_motion_areas": [], + "zones": ["front_yard"], + } + ), + ), + ReviewSegmentResponse( + id="review-alert-002", + camera="backyard", + severity="alert", + start_time=datetime.fromtimestamp(NOW - 3 * HOUR), + end_time=datetime.fromtimestamp(NOW - 3 * HOUR + 45), + has_been_reviewed=True, + thumb_path="/clips/backyard/review-alert-002-thumb.jpg", + data=json.dumps( + { + "audio": [], + "detections": ["car-def456"], + "objects": ["car"], + "sub_labels": [], + "significant_motion_areas": [], + "zones": ["driveway"], + } + ), + ), + ReviewSegmentResponse( + id="review-detect-001", + camera="garage", + severity="detection", + start_time=datetime.fromtimestamp(NOW - 4 * HOUR), + end_time=datetime.fromtimestamp(NOW - 4 * HOUR + 20), + has_been_reviewed=False, + thumb_path="/clips/garage/review-detect-001-thumb.jpg", + data=json.dumps( + { + "audio": [], + "detections": ["person-ghi789"], + "objects": ["person"], + "sub_labels": [], + "significant_motion_areas": [], + "zones": [], + } + ), + ), + ReviewSegmentResponse( + id="review-detect-002", + camera="front_door", + severity="detection", + start_time=datetime.fromtimestamp(NOW - 5 * HOUR), + end_time=datetime.fromtimestamp(NOW - 5 * HOUR + 15), + has_been_reviewed=False, + thumb_path="/clips/front_door/review-detect-002-thumb.jpg", + data=json.dumps( + { + "audio": [], + "detections": ["car-jkl012"], + "objects": ["car"], + "sub_labels": [], + "significant_motion_areas": [], + "zones": ["front_yard"], + } + ), + ), + ] + + result = [r.model_dump(mode="json") for r in reviews] + + # Verify mock data covers all Pydantic response model fields + check_pydantic_fields( + ReviewSegmentResponse, set(result[0].keys()), "ReviewSegment" + ) + + return result + + +def generate_events(): + """Generate EventResponse[] validated against Pydantic + Peewee.""" + from frigate.api.defs.response.event_response import EventResponse + + events = [ + EventResponse( + id="event-person-001", + label="person", + sub_label=None, + camera="front_door", + start_time=NOW - 2 * HOUR, + end_time=NOW - 2 * HOUR + 30, + false_positive=False, + zones=["front_yard"], + thumbnail=None, + has_clip=True, + has_snapshot=True, + retain_indefinitely=False, + plus_id=None, + model_hash="abc123", + detector_type="cpu", + model_type="ssd", + data={ + "top_score": 0.92, + "score": 0.92, + "region": [0.1, 0.1, 0.5, 0.8], + "box": [0.2, 0.15, 0.45, 0.75], + "area": 0.18, + "ratio": 0.6, + "type": "object", + "description": "A person walking toward the front door", + "average_estimated_speed": 1.2, + "velocity_angle": 45.0, + "path_data": [[[0.2, 0.5], 0.0], [[0.3, 0.5], 1.0]], + }, + ), + EventResponse( + id="event-car-001", + label="car", + sub_label=None, + camera="backyard", + start_time=NOW - 3 * HOUR, + end_time=NOW - 3 * HOUR + 45, + false_positive=False, + zones=["driveway"], + thumbnail=None, + has_clip=True, + has_snapshot=True, + retain_indefinitely=False, + plus_id=None, + model_hash="def456", + detector_type="cpu", + model_type="ssd", + data={ + "top_score": 0.87, + "score": 0.87, + "region": [0.3, 0.2, 0.9, 0.7], + "box": [0.35, 0.25, 0.85, 0.65], + "area": 0.2, + "ratio": 1.25, + "type": "object", + "description": "A car parked in the driveway", + "average_estimated_speed": 0.0, + "velocity_angle": 0.0, + "path_data": [], + }, + ), + EventResponse( + id="event-person-002", + label="person", + sub_label=None, + camera="garage", + start_time=NOW - 4 * HOUR, + end_time=NOW - 4 * HOUR + 20, + false_positive=False, + zones=[], + thumbnail=None, + has_clip=False, + has_snapshot=True, + retain_indefinitely=False, + plus_id=None, + model_hash="ghi789", + detector_type="cpu", + model_type="ssd", + data={ + "top_score": 0.78, + "score": 0.78, + "region": [0.0, 0.0, 0.6, 0.9], + "box": [0.1, 0.05, 0.5, 0.85], + "area": 0.32, + "ratio": 0.5, + "type": "object", + "description": None, + "average_estimated_speed": 0.5, + "velocity_angle": 90.0, + "path_data": [[[0.1, 0.4], 0.0]], + }, + ), + ] + + result = [e.model_dump(mode="json") for e in events] + + check_pydantic_fields(EventResponse, set(result[0].keys()), "Event") + + return result + + +def generate_exports(): + """Generate ExportModel[] validated against Pydantic + Peewee.""" + from frigate.api.defs.response.export_response import ExportModel + + exports = [ + ExportModel( + id="export-001", + camera="front_door", + name="Front Door - Person Alert", + date=NOW - 1 * HOUR, + video_path="/exports/export-001.mp4", + thumb_path="/exports/export-001-thumb.jpg", + in_progress=False, + export_case_id=None, + ), + ExportModel( + id="export-002", + camera="backyard", + name="Backyard - Car Detection", + date=NOW - 3 * HOUR, + video_path="/exports/export-002.mp4", + thumb_path="/exports/export-002-thumb.jpg", + in_progress=False, + export_case_id="case-001", + ), + ExportModel( + id="export-003", + camera="garage", + name="Garage - In Progress", + date=NOW - 0.5 * HOUR, + video_path="/exports/export-003.mp4", + thumb_path="/exports/export-003-thumb.jpg", + in_progress=True, + export_case_id=None, + ), + ] + + result = [e.model_dump(mode="json") for e in exports] + + check_pydantic_fields(ExportModel, set(result[0].keys()), "Export") + + return result + + +def generate_cases(): + """Generate ExportCaseModel[] validated against Pydantic + Peewee.""" + from frigate.api.defs.response.export_case_response import ExportCaseModel + + cases = [ + ExportCaseModel( + id="case-001", + name="Package Theft Investigation", + description="Review of suspicious activity near the front porch", + created_at=NOW - 24 * HOUR, + updated_at=NOW - 3 * HOUR, + ), + ] + + result = [c.model_dump(mode="json") for c in cases] + + check_pydantic_fields(ExportCaseModel, set(result[0].keys()), "ExportCase") + + return result + + +def generate_review_summary(): + """Generate ReviewSummary for the calendar filter.""" + today = datetime.now().strftime("%Y-%m-%d") + yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") + + return { + today: { + "day": today, + "reviewed_alert": 1, + "reviewed_detection": 0, + "total_alert": 2, + "total_detection": 2, + }, + yesterday: { + "day": yesterday, + "reviewed_alert": 3, + "reviewed_detection": 2, + "total_alert": 3, + "total_detection": 4, + }, + } + + +def write_json(filename, data): + path = OUTPUT_DIR / filename + path.write_text(json.dumps(data, default=str)) + print(f" {path.name} ({path.stat().st_size} bytes)") + + +def main(): + print("Generating E2E mock data from backend models...") + print(" Validating against Pydantic response models + Peewee DB columns") + print() + + write_json("config-snapshot.json", generate_config()) + write_json("config-schema.json", generate_config_schema()) + write_json("reviews.json", generate_reviews()) + write_json("events.json", generate_events()) + write_json("exports.json", generate_exports()) + write_json("cases.json", generate_cases()) + write_json("review-summary.json", generate_review_summary()) + + print() + print("All mock data validated against backend schemas.") + print("If this script fails, update the mock data to match the new schema.") + + +if __name__ == "__main__": + main() diff --git a/web/e2e/fixtures/mock-data/profile.ts b/web/e2e/fixtures/mock-data/profile.ts new file mode 100644 index 0000000..62d70e3 --- /dev/null +++ b/web/e2e/fixtures/mock-data/profile.ts @@ -0,0 +1,39 @@ +/** + * User profile factories for E2E tests. + */ + +export interface UserProfile { + username: string; + role: string; + allowed_cameras: string[] | null; +} + +export function adminProfile(overrides?: Partial): UserProfile { + return { + username: "admin", + role: "admin", + allowed_cameras: null, + ...overrides, + }; +} + +export function viewerProfile(overrides?: Partial): UserProfile { + return { + username: "viewer", + role: "viewer", + allowed_cameras: null, + ...overrides, + }; +} + +export function restrictedProfile( + cameras: string[], + overrides?: Partial, +): UserProfile { + return { + username: "restricted", + role: "viewer", + allowed_cameras: cameras, + ...overrides, + }; +} diff --git a/web/e2e/fixtures/mock-data/review-summary.json b/web/e2e/fixtures/mock-data/review-summary.json new file mode 100644 index 0000000..bb3afc2 --- /dev/null +++ b/web/e2e/fixtures/mock-data/review-summary.json @@ -0,0 +1 @@ +{"2026-06-05": {"day": "2026-06-05", "reviewed_alert": 1, "reviewed_detection": 0, "total_alert": 2, "total_detection": 2}, "2026-06-04": {"day": "2026-06-04", "reviewed_alert": 3, "reviewed_detection": 2, "total_alert": 3, "total_detection": 4}} \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/reviews.json b/web/e2e/fixtures/mock-data/reviews.json new file mode 100644 index 0000000..0b60850 --- /dev/null +++ b/web/e2e/fixtures/mock-data/reviews.json @@ -0,0 +1 @@ +[{"id": "review-alert-001", "camera": "front_door", "start_time": "2026-06-05T11:30:09.365581", "end_time": "2026-06-05T11:30:39.365581", "has_been_reviewed": false, "severity": "alert", "thumb_path": "/clips/front_door/review-alert-001-thumb.jpg", "data": {"audio": [], "detections": ["person-abc123"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}, {"id": "review-alert-002", "camera": "backyard", "start_time": "2026-06-05T10:30:09.365581", "end_time": "2026-06-05T10:30:54.365581", "has_been_reviewed": true, "severity": "alert", "thumb_path": "/clips/backyard/review-alert-002-thumb.jpg", "data": {"audio": [], "detections": ["car-def456"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["driveway"]}}, {"id": "review-detect-001", "camera": "garage", "start_time": "2026-06-05T09:30:09.365581", "end_time": "2026-06-05T09:30:29.365581", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/garage/review-detect-001-thumb.jpg", "data": {"audio": [], "detections": ["person-ghi789"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": []}}, {"id": "review-detect-002", "camera": "front_door", "start_time": "2026-06-05T08:30:09.365581", "end_time": "2026-06-05T08:30:24.365581", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/front_door/review-detect-002-thumb.jpg", "data": {"audio": [], "detections": ["car-jkl012"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}] \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/stats.ts b/web/e2e/fixtures/mock-data/stats.ts new file mode 100644 index 0000000..d34ea25 --- /dev/null +++ b/web/e2e/fixtures/mock-data/stats.ts @@ -0,0 +1,76 @@ +/** + * FrigateStats factory for E2E tests. + */ + +import type { DeepPartial } from "./config"; + +function cameraStats(_name: string) { + return { + audio_dBFPS: 0, + audio_rms: 0, + camera_fps: 5.0, + capture_pid: 100, + detection_enabled: 1, + detection_fps: 5.0, + ffmpeg_pid: 101, + pid: 102, + process_fps: 5.0, + skipped_fps: 0, + connection_quality: "excellent" as const, + expected_fps: 5, + reconnects_last_hour: 0, + stalls_last_hour: 0, + }; +} + +export const BASE_STATS = { + cameras: { + front_door: cameraStats("front_door"), + backyard: cameraStats("backyard"), + garage: cameraStats("garage"), + }, + cpu_usages: { + "1": { cmdline: "frigate.app", cpu: "5.0", cpu_average: "4.5", mem: "2.1" }, + }, + detectors: { + cpu: { + detection_start: 0, + inference_speed: 75.5, + pid: 200, + }, + }, + gpu_usages: {}, + npu_usages: {}, + processes: {}, + service: { + last_updated: Date.now() / 1000, + storage: { + "/media/frigate/recordings": { + free: 50000000000, + total: 100000000000, + used: 50000000000, + mount_type: "ext4", + }, + "/tmp/cache": { + free: 500000000, + total: 1000000000, + used: 500000000, + mount_type: "tmpfs", + }, + }, + uptime: 86400, + latest_version: "0.15.0", + version: "0.15.0-test", + }, + camera_fps: 15.0, + process_fps: 15.0, + skipped_fps: 0, + detection_fps: 15.0, +}; + +export function statsFactory( + overrides?: DeepPartial, +): typeof BASE_STATS { + if (!overrides) return BASE_STATS; + return { ...BASE_STATS, ...overrides } as typeof BASE_STATS; +} diff --git a/web/e2e/global-setup.ts b/web/e2e/global-setup.ts new file mode 100644 index 0000000..ef8f546 --- /dev/null +++ b/web/e2e/global-setup.ts @@ -0,0 +1,7 @@ +import { execSync } from "child_process"; +import path from "path"; + +export default function globalSetup() { + const webDir = path.resolve(__dirname, ".."); + execSync("npm run e2e:build", { cwd: webDir, stdio: "inherit" }); +} diff --git a/web/e2e/helpers/api-mocker.ts b/web/e2e/helpers/api-mocker.ts new file mode 100644 index 0000000..e1a191f --- /dev/null +++ b/web/e2e/helpers/api-mocker.ts @@ -0,0 +1,284 @@ +/** + * REST API mock using Playwright's page.route(). + * + * Intercepts all /api/* requests and returns factory-generated responses. + * Must be installed BEFORE page.goto() to prevent auth redirects. + */ + +import type { Page } from "@playwright/test"; +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + BASE_CONFIG, + type DeepPartial, + configFactory, +} from "../fixtures/mock-data/config"; +import { adminProfile, type UserProfile } from "../fixtures/mock-data/profile"; +import { BASE_STATS, statsFactory } from "../fixtures/mock-data/stats"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const MOCK_DATA_DIR = resolve(__dirname, "../fixtures/mock-data"); + +function loadMockJson(filename: string): unknown { + return JSON.parse(readFileSync(resolve(MOCK_DATA_DIR, filename), "utf-8")); +} + +// 1x1 transparent PNG +const PLACEHOLDER_PNG = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "base64", +); + +export interface ApiMockOverrides { + config?: DeepPartial; + profile?: UserProfile; + stats?: DeepPartial; + reviews?: unknown[]; + events?: unknown[]; + exports?: unknown[]; + cases?: unknown[]; + faces?: Record; + configRaw?: string; + configSchema?: Record; +} + +export class ApiMocker { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + async install(overrides?: ApiMockOverrides) { + const config = configFactory(overrides?.config); + const profile = overrides?.profile ?? adminProfile(); + const stats = statsFactory(overrides?.stats); + const reviews = + overrides?.reviews ?? (loadMockJson("reviews.json") as unknown[]); + const events = + overrides?.events ?? (loadMockJson("events.json") as unknown[]); + const exports = + overrides?.exports ?? (loadMockJson("exports.json") as unknown[]); + const cases = overrides?.cases ?? (loadMockJson("cases.json") as unknown[]); + const reviewSummary = loadMockJson("review-summary.json"); + + // Config endpoint + await this.page.route("**/api/config", (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ json: config }); + } + return route.fulfill({ json: { success: true } }); + }); + + // Profile endpoint (AuthProvider fetches /profile directly via axios, + // which resolves to /api/profile due to axios.defaults.baseURL) + await this.page.route("**/profile", (route) => + route.fulfill({ json: profile }), + ); + + // Stats endpoint + await this.page.route("**/api/stats", (route) => + route.fulfill({ json: stats }), + ); + + // Reviews. The real backend exposes /review (singular) for the main + // list and /review/summary for the summary — the previous plural glob + // (**/api/reviews**) never matched either endpoint, so review-dependent + // tests silently ran without data. The POST mutations at /reviews/viewed + // and /reviews/delete (plural) still fall through to the generic + // mutation catch-all further down the file. + await this.page.route(/\/api\/review\/summary/, (route) => + route.fulfill({ json: reviewSummary }), + ); + await this.page.route(/\/api\/review(\?|$)/, (route) => + route.fulfill({ json: reviews }), + ); + + // Export jobs. The Exports page polls this every 2s while any export + // is in_progress; without a mock route it falls through to the preview + // server which returns 500 and makes the page flap between loading and + // rendered state, breaking tests that navigate to /export. + await this.page.route("**/api/jobs/export", (route) => + route.fulfill({ json: [] }), + ); + + // Recordings summary + await this.page.route("**/api/recordings/summary**", (route) => + route.fulfill({ json: {} }), + ); + + // Previews (needed for review page event cards) + await this.page.route("**/api/preview/**", (route) => + route.fulfill({ json: [] }), + ); + + // Sub-labels and attributes (for explore filters). + // Use trailing ** so query-string variants (e.g. ?split_joined=1) match. + await this.page.route("**/api/sub_labels**", (route) => + route.fulfill({ json: [] }), + ); + await this.page.route("**/api/labels**", (route) => + route.fulfill({ json: ["person", "car"] }), + ); + await this.page.route("**/api/*/attributes", (route) => + route.fulfill({ json: [] }), + ); + await this.page.route("**/api/recognized_license_plates", (route) => + route.fulfill({ json: [] }), + ); + + // Events / search + await this.page.route("**/api/events**", (route) => + route.fulfill({ json: events }), + ); + + // Exports + await this.page.route("**/api/export**", (route) => + route.fulfill({ json: exports }), + ); + + // Cases + await this.page.route("**/api/cases", (route) => + route.fulfill({ json: cases }), + ); + + // Faces + await this.page.route("**/api/faces", (route) => + route.fulfill({ json: overrides?.faces ?? {} }), + ); + + // Logs + await this.page.route("**/api/logs/**", (route) => + route.fulfill({ + contentType: "text/plain", + body: "[2026-04-06 10:00:00] INFO: Frigate started\n[2026-04-06 10:00:01] INFO: Cameras loaded\n", + }), + ); + + // Config raw + await this.page.route("**/api/config/raw", (route) => + route.fulfill({ + contentType: "text/plain", + body: + overrides?.configRaw ?? + "mqtt:\n host: mqtt\ncameras:\n front_door:\n enabled: true\n", + }), + ); + + // Config schema + await this.page.route("**/api/config/schema.json", (route) => + route.fulfill({ + json: overrides?.configSchema ?? { type: "object", properties: {} }, + }), + ); + + // Config set (mutation) + await this.page.route("**/api/config/set", (route) => + route.fulfill({ json: { success: true, require_restart: false } }), + ); + + // Go2RTC streams + await this.page.route("**/api/go2rtc/streams**", (route) => + route.fulfill({ json: {} }), + ); + + // Profiles + await this.page.route("**/api/profiles**", (route) => + route.fulfill({ + json: { profiles: [], active_profile: null, last_activated: {} }, + }), + ); + + // Motion search + await this.page.route("**/api/motion_search**", (route) => + route.fulfill({ json: { job_id: "test-job" } }), + ); + + // Region grid + await this.page.route("**/api/*/region_grid", (route) => + route.fulfill({ json: {} }), + ); + + // Debug replay + await this.page.route("**/api/debug_replay/**", (route) => + route.fulfill({ json: {} }), + ); + + // Generic mutation catch-all for remaining endpoints. + // Uses route.fallback() to defer to more specific routes registered above. + // Playwright matches routes in reverse registration order (last wins), + // so this catch-all must use fallback() to let specific routes take precedence. + await this.page.route("**/api/**", (route) => { + const method = route.request().method(); + if ( + method === "POST" || + method === "PUT" || + method === "PATCH" || + method === "DELETE" + ) { + return route.fulfill({ json: { success: true } }); + } + // Fall through to more specific routes for GET requests + return route.fallback(); + }); + } +} + +export class MediaMocker { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + async install() { + // Camera snapshots + await this.page.route("**/api/*/latest.jpg**", (route) => + route.fulfill({ + contentType: "image/png", + body: PLACEHOLDER_PNG, + }), + ); + + // Clips and thumbnails + await this.page.route("**/clips/**", (route) => + route.fulfill({ + contentType: "image/png", + body: PLACEHOLDER_PNG, + }), + ); + + // Event thumbnails + await this.page.route("**/api/events/*/thumbnail.jpg**", (route) => + route.fulfill({ + contentType: "image/png", + body: PLACEHOLDER_PNG, + }), + ); + + // Event snapshots + await this.page.route("**/api/events/*/snapshot.jpg**", (route) => + route.fulfill({ + contentType: "image/png", + body: PLACEHOLDER_PNG, + }), + ); + + // VOD / recordings + await this.page.route("**/vod/**", (route) => + route.fulfill({ + contentType: "application/vnd.apple.mpegurl", + body: "#EXTM3U\n#EXT-X-ENDLIST\n", + }), + ); + + // Live streams + await this.page.route("**/live/**", (route) => + route.fulfill({ + contentType: "application/vnd.apple.mpegurl", + body: "#EXTM3U\n#EXT-X-ENDLIST\n", + }), + ); + } +} diff --git a/web/e2e/helpers/clipboard.ts b/web/e2e/helpers/clipboard.ts new file mode 100644 index 0000000..9099073 --- /dev/null +++ b/web/e2e/helpers/clipboard.ts @@ -0,0 +1,25 @@ +/** + * Clipboard read helper for e2e tests. + * + * Clipboard API requires a browser permission in headless mode. + * grantClipboardPermissions() must be called before any readClipboard() + * attempt. Used by logs.spec.ts (Copy button) and config-editor.spec.ts + * (Copy button). + */ + +import type { BrowserContext, Page } from "@playwright/test"; + +/** + * Grant clipboard-read + clipboard-write permissions on the context. + * Call in beforeEach or at the top of a test before the Copy action. + */ +export async function grantClipboardPermissions( + context: BrowserContext, +): Promise { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); +} + +/** Read the current clipboard contents via the page's navigator.clipboard. */ +export async function readClipboard(page: Page): Promise { + return page.evaluate(async () => await navigator.clipboard.readText()); +} diff --git a/web/e2e/helpers/mock-overrides.ts b/web/e2e/helpers/mock-overrides.ts new file mode 100644 index 0000000..71ea38e --- /dev/null +++ b/web/e2e/helpers/mock-overrides.ts @@ -0,0 +1,56 @@ +/** + * Per-test mock overrides for driving empty / loading / error states. + * + * Playwright route handlers are LIFO: the most recently registered handler + * matching a URL takes precedence. The frigateApp fixture installs default + * mocks before the test body runs, so these helpers — called inside the + * test body — register AFTER the defaults and therefore win. + * + * Always call these BEFORE the navigation that triggers the request. + * + * Example: + * await mockEmpty(page, "**\/api\/exports**"); + * await frigateApp.goto("/export"); + * // Page now renders the empty state + */ + +import type { Page } from "@playwright/test"; + +/** Return an empty array for the matched endpoint. */ +export async function mockEmpty( + page: Page, + urlPattern: string | RegExp, +): Promise { + await page.route(urlPattern, (route) => route.fulfill({ json: [] })); +} + +/** Return an HTTP error for the matched endpoint. Default status 500. */ +export async function mockError( + page: Page, + urlPattern: string | RegExp, + status = 500, +): Promise { + await page.route(urlPattern, (route) => + route.fulfill({ + status, + json: { success: false, message: "Mocked error" }, + }), + ); +} + +/** + * Delay the response by `ms` milliseconds before fulfilling with the + * provided body. Use to assert loading-state UI is visible during the + * delay window. + */ +export async function mockDelay( + page: Page, + urlPattern: string | RegExp, + ms: number, + body: unknown = [], +): Promise { + await page.route(urlPattern, async (route) => { + await new Promise((resolve) => setTimeout(resolve, ms)); + await route.fulfill({ json: body }); + }); +} diff --git a/web/e2e/helpers/monaco.ts b/web/e2e/helpers/monaco.ts new file mode 100644 index 0000000..6e9bcd8 --- /dev/null +++ b/web/e2e/helpers/monaco.ts @@ -0,0 +1,58 @@ +/** + * Monaco editor DOM helpers for e2e tests. + * + * Monaco is imported as a module-local object in the app and is NOT + * exposed on window; we drive + read through the rendered DOM and + * keyboard instead. Used by config-editor.spec.ts only. + */ + +import { expect, type Page } from "@playwright/test"; + +/** + * Returns the current visible text of the first Monaco editor on the + * page. Monaco virtualizes long files — this reads only the rendered + * lines. For short configs (our mocks) that's the full content. + */ +export async function getMonacoVisibleText(page: Page): Promise { + return page.locator(".monaco-editor .view-lines").first().innerText(); +} + +/** + * Focus the editor and replace its full content with `value` via + * keyboard. Uses Ctrl+A (Cmd+A on macOS Playwright is equivalent) + * + Delete + type. Works cross-platform because Playwright normalizes. + */ +export async function replaceMonacoValue( + page: Page, + value: string, +): Promise { + const editor = page.locator(".monaco-editor").first(); + await editor.click(); + await page.keyboard.press("ControlOrMeta+A"); + await page.keyboard.press("Delete"); + // Use `type` with zero delay — Monaco handles each key. + await page.keyboard.type(value, { delay: 0 }); +} + +/** + * Returns true when the editor shows at least one error-severity + * marker. Monaco renders error underlines as `.squiggly-error` in + * the `.view-overlays` layer. + */ +export async function hasErrorMarkers(page: Page): Promise { + const count = await page.locator(".monaco-editor .squiggly-error").count(); + return count > 0; +} + +/** + * Poll until an error marker appears. Monaco schedules marker updates + * asynchronously after content changes (debounce + schema validation). + */ +export async function waitForErrorMarker( + page: Page, + timeoutMs: number = 10_000, +): Promise { + await expect + .poll(() => hasErrorMarkers(page), { timeout: timeoutMs }) + .toBe(true); +} diff --git a/web/e2e/helpers/overlay-interaction.ts b/web/e2e/helpers/overlay-interaction.ts new file mode 100644 index 0000000..81a01d8 --- /dev/null +++ b/web/e2e/helpers/overlay-interaction.ts @@ -0,0 +1,41 @@ +/** + * Overlay interaction helpers for Radix-based UI tests. + * + * These helpers exist to guard the class of bugs fixed by de-duping + * `@radix-ui/react-dismissable-layer` across the tree: body pointer-events + * getting stuck, dropdown typeahead breaking, tooltips re-popping after a + * dropdown closes, and related nested-overlay regressions. + */ + +import { expect, type Page } from "@playwright/test"; + +/** + * Assert that `` is interactive (no stuck `pointer-events: none`). + * + * Call after closing any overlay. This is the fast secondary assertion — + * test specs should also assert a user-visible behavior like "a button + * responded to a click" so the test fails on meaningful breakage rather + * than just a CSS invariant. + */ +export async function expectBodyInteractive(page: Page) { + const stuck = await page.evaluate( + () => document.body.style.pointerEvents === "none", + ); + expect(stuck, "body.style.pointer-events stuck after overlay close").toBe( + false, + ); +} + +/** + * Wait until the `` is no longer marked with `pointer-events: none`. + * + * Useful right after closing an overlay when Radix's cleanup runs in the + * next frame. Throws if the style does not clear within `timeoutMs`. + */ +export async function waitForBodyInteractive(page: Page, timeoutMs = 2000) { + await page.waitForFunction( + () => document.body.style.pointerEvents !== "none", + null, + { timeout: timeoutMs }, + ); +} diff --git a/web/e2e/helpers/ws-frames.ts b/web/e2e/helpers/ws-frames.ts new file mode 100644 index 0000000..d46376d --- /dev/null +++ b/web/e2e/helpers/ws-frames.ts @@ -0,0 +1,65 @@ +/** + * WebSocket frame capture helper. + * + * The ws-mocker intercepts the /ws route, so Playwright's page-level + * `websocket` event never fires. This helper patches client-side + * WebSocket.prototype.send before any app code runs and mirrors every + * sent frame into a window-level array the test can read back. + * + * Used by live.spec.ts (feature toggles, PTZ preset commands) and + * config-editor.spec.ts (restart command via useRestart). + */ + +import { expect, type Page } from "@playwright/test"; + +export type CapturedFrame = string; + +declare global { + interface Window { + __sentWsFrames: CapturedFrame[]; + } +} + +/** + * Patch WebSocket.prototype.send to capture every outbound frame into + * window.__sentWsFrames. Must be called BEFORE page.goto(). + */ +export async function installWsFrameCapture(page: Page): Promise { + await page.addInitScript(() => { + window.__sentWsFrames = []; + const origSend = WebSocket.prototype.send; + WebSocket.prototype.send = function (data) { + try { + window.__sentWsFrames.push( + typeof data === "string" ? data : "(binary)", + ); + } catch { + // ignore — best-effort tracing + } + return origSend.call(this, data); + }; + }); +} + +/** Read all captured frames at call time. */ +export async function readWsFrames(page: Page): Promise { + return page.evaluate(() => window.__sentWsFrames ?? []); +} + +/** + * Poll until at least one captured frame matches the predicate. + * Throws via expect if the frame never arrives within timeout. + */ +export async function waitForWsFrame( + page: Page, + matcher: (frame: CapturedFrame) => boolean, + opts: { timeout?: number; message?: string } = {}, +): Promise { + const { timeout = 2_000, message } = opts; + await expect + .poll(async () => (await readWsFrames(page)).some(matcher), { + timeout, + message, + }) + .toBe(true); +} diff --git a/web/e2e/helpers/ws-mocker.ts b/web/e2e/helpers/ws-mocker.ts new file mode 100644 index 0000000..03db9f7 --- /dev/null +++ b/web/e2e/helpers/ws-mocker.ts @@ -0,0 +1,138 @@ +/** + * WebSocket mock using Playwright's native page.routeWebSocket(). + * + * Intercepts the app's WebSocket connection and simulates the Frigate + * WS protocol: onConnect handshake, camera_activity expansion, and + * topic-based state updates. + */ + +import type { Page, WebSocketRoute } from "@playwright/test"; +import { cameraActivityPayload } from "../fixtures/mock-data/camera-activity"; + +export class WsMocker { + private mockWs: WebSocketRoute | null = null; + private cameras: string[]; + + constructor(cameras: string[] = ["front_door", "backyard", "garage"]) { + this.cameras = cameras; + } + + async install(page: Page) { + await page.routeWebSocket("**/ws", (ws) => { + this.mockWs = ws; + + ws.onMessage((msg) => { + this.handleClientMessage(msg.toString()); + }); + }); + } + + private handleClientMessage(raw: string) { + let data: { topic: string; payload?: unknown; message?: string }; + try { + data = JSON.parse(raw); + } catch { + return; + } + + if (data.topic === "onConnect") { + // Send initial camera_activity state + this.sendCameraActivity(); + + // Send initial stats + this.send( + "stats", + JSON.stringify({ + cameras: Object.fromEntries( + this.cameras.map((c) => [ + c, + { + camera_fps: 5, + detection_fps: 5, + process_fps: 5, + skipped_fps: 0, + detection_enabled: 1, + connection_quality: "excellent", + }, + ]), + ), + service: { + last_updated: Date.now() / 1000, + uptime: 86400, + version: "0.15.0-test", + latest_version: "0.15.0", + storage: {}, + }, + detectors: {}, + cpu_usages: {}, + gpu_usages: {}, + camera_fps: 15, + process_fps: 15, + skipped_fps: 0, + detection_fps: 15, + }), + ); + } + + // Echo back state commands (e.g., modelState, jobState, etc.) + if (data.topic === "modelState") { + this.send("model_state", JSON.stringify({})); + } + if (data.topic === "embeddingsReindexProgress") { + // Send a completed reindex state so Explore renders when + // semantic_search.enabled is true. A null payload leaves the page + // in a permanent loading spinner because !reindexState is truthy. + this.send( + "embeddings_reindex_progress", + JSON.stringify({ + status: "completed", + processed_objects: 0, + total_objects: 0, + thumbnails: 0, + descriptions: 0, + time_remaining: null, + }), + ); + } + if (data.topic === "birdseyeLayout") { + this.send("birdseye_layout", JSON.stringify(null)); + } + if (data.topic === "jobState") { + this.send("job_state", JSON.stringify({})); + } + if (data.topic === "audioTranscriptionState") { + this.send("audio_transcription_state", JSON.stringify("idle")); + } + + // Camera toggle commands: echo back the new state + const toggleMatch = data.topic?.match( + /^(.+)\/(detect|recordings|snapshots|audio|enabled|notifications|ptz_autotracker|review_alerts|review_detections|object_descriptions|review_descriptions|audio_transcription)\/set$/, + ); + if (toggleMatch) { + const [, camera, feature] = toggleMatch; + this.send(`${camera}/${feature}/state`, data.payload); + } + } + + /** Send a raw WS message to the app */ + send(topic: string, payload: unknown) { + if (!this.mockWs) return; + this.mockWs.send(JSON.stringify({ topic, payload })); + } + + /** Send camera_activity with default or custom state */ + sendCameraActivity(overrides?: Parameters[1]) { + const payload = cameraActivityPayload(this.cameras, overrides); + this.send("camera_activity", payload); + } + + /** Send a review update */ + sendReview(review: unknown) { + this.send("reviews", JSON.stringify(review)); + } + + /** Send an event update */ + sendEvent(event: unknown) { + this.send("events", JSON.stringify(event)); + } +} diff --git a/web/e2e/pages/base.page.ts b/web/e2e/pages/base.page.ts new file mode 100644 index 0000000..e5628cb --- /dev/null +++ b/web/e2e/pages/base.page.ts @@ -0,0 +1,135 @@ +/** + * Base page object with viewport-aware navigation helpers. + * + * Desktop: clicks sidebar NavLink elements. + * Mobile: clicks bottombar NavLink elements. + */ + +import type { Page, Locator } from "@playwright/test"; + +export class BasePage { + constructor( + protected page: Page, + public isDesktop: boolean, + ) {} + + get isMobile() { + return !this.isDesktop; + } + + /** The sidebar (desktop only) */ + get sidebar(): Locator { + return this.page.locator("aside"); + } + + /** The bottombar (mobile only) */ + get bottombar(): Locator { + return this.page + .locator('[data-bottombar="true"]') + .or(this.page.locator(".absolute.inset-x-4.bottom-0").first()); + } + + /** The main page content area */ + get pageRoot(): Locator { + return this.page.locator("#pageRoot"); + } + + /** Navigate using a NavLink by its href */ + async navigateTo(path: string) { + // Wait for any in-progress React renders to settle before clicking + await this.page.waitForLoadState("domcontentloaded"); + // Use page.click with a CSS selector to avoid stale element issues + // when React re-renders the nav during route transitions. + // force: true bypasses actionability checks that fail when React + // detaches and reattaches nav elements during re-renders. + const selector = this.isDesktop + ? `aside a[href="${path}"]` + : `a[href="${path}"]`; + // Use dispatchEvent to bypass actionability checks that fail when + // React tooltip wrappers detach/reattach nav elements during re-renders + await this.page.locator(selector).first().dispatchEvent("click"); + // React Router navigates client-side, wait for URL update + if (path !== "/") { + const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + await this.page.waitForURL(new RegExp(escaped), { timeout: 10_000 }); + } + } + + /** Navigate to Live page */ + async goToLive() { + await this.navigateTo("/"); + } + + /** Navigate to Review page */ + async goToReview() { + await this.navigateTo("/review"); + } + + /** Navigate to Explore page */ + async goToExplore() { + await this.navigateTo("/explore"); + } + + /** Navigate to Export page */ + async goToExport() { + await this.navigateTo("/export"); + } + + /** Check if the page has loaded */ + async waitForPageLoad() { + await this.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + } + + /** + * Open the mobile-only export pane / sheet that slides up from the + * bottom on the export page. No-op on desktop. Returns the pane locator + * so the caller can assert against its contents. + */ + async openMobilePane(): Promise { + if (this.isDesktop) { + // Return the desktop equivalent (the main content area itself) + return this.pageRoot; + } + // Look for any element that opens a sheet/dialog on tap. + // Specific views override this with their own selector. + const pane = this.page.locator('[role="dialog"]').first(); + return pane; + } + + /** + * Open a side drawer (e.g. mobile filter drawer). View-specific page + * objects should override this with their actual trigger selector. + * The default implementation looks for a button labelled "Open menu" + * or "Filters" and clicks it, then returns the drawer locator. + */ + async openDrawer(): Promise { + if (this.isDesktop) { + return this.pageRoot; + } + const trigger = this.page + .getByRole("button", { name: /menu|filter/i }) + .first(); + if (await trigger.count()) { + await trigger.click(); + } + return this.page.locator('[role="dialog"], [data-state="open"]').first(); + } + + /** + * Open a bottom sheet (vaul). View-specific page objects should + * override this with their actual trigger selector. + */ + async openBottomSheet(): Promise { + if (this.isDesktop) { + return this.pageRoot; + } + return this.page.locator("[vaul-drawer]").first(); + } + + /** Close any currently-open mobile overlay (drawer, sheet, dialog). */ + async closeMobileOverlay(): Promise { + if (this.isDesktop) return; + // Press Escape — Radix dialogs and vaul both close on Escape + await this.page.keyboard.press("Escape"); + } +} diff --git a/web/e2e/pages/live.page.ts b/web/e2e/pages/live.page.ts new file mode 100644 index 0000000..8140649 --- /dev/null +++ b/web/e2e/pages/live.page.ts @@ -0,0 +1,55 @@ +/** + * Live dashboard + single-camera page object. + * + * Encapsulates selectors and viewport-conditional openers for the + * Live route. Does NOT own assertions — specs call expect on the + * locators returned from these getters. + */ + +import type { Locator, Page } from "@playwright/test"; +import { BasePage } from "./base.page"; + +export class LivePage extends BasePage { + constructor(page: Page, isDesktop: boolean) { + super(page, isDesktop); + } + + /** The camera card wrapper on the dashboard, keyed by camera name. */ + cameraCard(name: string): Locator { + return this.page.locator(`[data-camera='${name}']`); + } + + /** Back button on the single-camera view header (desktop text). */ + get backButton(): Locator { + return this.page.getByText("Back", { exact: true }); + } + + /** History button on the single-camera view header (desktop text). */ + get historyButton(): Locator { + return this.page.getByText("History", { exact: true }); + } + + /** All CameraFeatureToggle elements (active + inactive). */ + get featureToggles(): Locator { + // Use div selector to exclude NavItem anchor elements that share the same classes. + return this.page.locator( + "div.flex.flex-col.items-center.justify-center.bg-selected, div.flex.flex-col.items-center.justify-center.bg-secondary", + ); + } + + /** Only the active (bg-selected) feature toggles. */ + get activeFeatureToggles(): Locator { + // Use div selector to exclude NavItem anchor elements that share the same classes. + return this.page.locator( + "div.flex.flex-col.items-center.justify-center.bg-selected", + ); + } + + /** Open the right-click context menu on a camera card (desktop only). */ + async openContextMenuOn(cameraName: string): Promise { + await this.cameraCard(cameraName).first().click({ button: "right" }); + return this.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + } +} diff --git a/web/e2e/pages/review.page.ts b/web/e2e/pages/review.page.ts new file mode 100644 index 0000000..6d9cfb7 --- /dev/null +++ b/web/e2e/pages/review.page.ts @@ -0,0 +1,52 @@ +/** + * Review/events page object. + * + * Encapsulates severity tab, filter bar, calendar, and mobile filter + * drawer selectors. Does NOT own assertions. + */ + +import type { Locator, Page } from "@playwright/test"; +import { BasePage } from "./base.page"; + +export class ReviewPage extends BasePage { + constructor(page: Page, isDesktop: boolean) { + super(page, isDesktop); + } + + get alertsTab(): Locator { + return this.page.getByLabel("Alerts"); + } + + get detectionsTab(): Locator { + return this.page.getByLabel("Detections"); + } + + get motionTab(): Locator { + return this.page.getByRole("radio", { name: "Motion" }); + } + + get camerasFilterTrigger(): Locator { + return this.page.getByRole("button", { name: /cameras/i }).first(); + } + + get calendarTrigger(): Locator { + return this.page.getByRole("button", { name: /24 hours|calendar|date/i }); + } + + get showReviewedToggle(): Locator { + return this.page.getByRole("button", { name: /reviewed/i }); + } + + get reviewItems(): Locator { + return this.page.locator(".review-item"); + } + + /** The filter popover content (desktop) or drawer (mobile). */ + get filterOverlay(): Locator { + return this.page + .locator( + '[data-radix-popper-content-wrapper], [role="dialog"], [data-vaul-drawer]', + ) + .first(); + } +} diff --git a/web/e2e/playwright.config.ts b/web/e2e/playwright.config.ts new file mode 100644 index 0000000..4b21258 --- /dev/null +++ b/web/e2e/playwright.config.ts @@ -0,0 +1,56 @@ +import { defineConfig, devices } from "@playwright/test"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const webRoot = resolve(__dirname, ".."); + +const DESKTOP_UA = + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; +const MOBILE_UA = + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"; + +export default defineConfig({ + testDir: "./specs", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 4, + reporter: process.env.CI ? [["json"], ["html"]] : [["html"]], + timeout: 30_000, + expect: { timeout: 5_000 }, + + use: { + baseURL: "http://localhost:4173", + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + + webServer: { + command: "npx vite preview --port 4173", + port: 4173, + cwd: webRoot, + reuseExistingServer: !process.env.CI, + }, + + projects: [ + { + name: "desktop", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 1920, height: 1080 }, + userAgent: DESKTOP_UA, + }, + }, + { + name: "mobile", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 390, height: 844 }, + userAgent: MOBILE_UA, + isMobile: true, + hasTouch: true, + }, + }, + ], +}); diff --git a/web/e2e/scripts/lint-specs.mjs b/web/e2e/scripts/lint-specs.mjs new file mode 100644 index 0000000..e404686 --- /dev/null +++ b/web/e2e/scripts/lint-specs.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node +/** + * Lint script for e2e specs. Bans lenient test patterns and requires + * a @mobile-tagged test in every spec under specs/ (excluding _meta/). + * + * Banned patterns: + * - page.waitForTimeout( — use expect().toPass() or waitFor instead + * - if (await ... .isVisible()) — assertions must be unconditional + * - if ((await ... .count()) > 0) — same as above + * - expect(... .length).toBeGreaterThan(0) on textContent results + * + * Escape hatch: append `// e2e-lint-allow` on any line to silence the + * check for that line. Use sparingly and explain why in a comment above. + * + * @mobile rule: every .spec.ts under specs/ (not specs/_meta/) must + * contain at least one test title or describe with the substring "@mobile". + */ + +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join, relative, resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SPECS_DIR = resolve(__dirname, "..", "specs"); +const META_PREFIX = resolve(SPECS_DIR, "_meta"); + +const BANNED_PATTERNS = [ + { + name: "page.waitForTimeout", + regex: /\bwaitForTimeout\s*\(/, + advice: + "Use expect.poll(), expect(...).toPass(), or waitFor() with a real condition.", + }, + { + name: "conditional isVisible() assertion", + regex: /\bif\s*\(\s*await\s+[^)]*\.isVisible\s*\(/, + advice: + "Assertions must be unconditional. Use expect(...).toBeVisible() instead.", + }, + { + name: "conditional count() assertion", + regex: /\bif\s*\(\s*\(?\s*await\s+[^)]*\.count\s*\(\s*\)\s*\)?\s*[><=!]/, + advice: "Assertions must be unconditional. Use expect(...).toHaveCount(n).", + }, + { + name: "vacuous textContent length assertion", + regex: /expect\([^)]*\.length\)\.toBeGreaterThan\(0\)/, + advice: "Assert specific content, not that some text exists.", + }, +]; + +function walk(dir) { + const entries = readdirSync(dir); + const out = []; + for (const entry of entries) { + const full = join(dir, entry); + const st = statSync(full); + if (st.isDirectory()) { + out.push(...walk(full)); + } else if (entry.endsWith(".spec.ts")) { + out.push(full); + } + } + return out; +} + +function lintFile(file) { + if (file.includes("/specs/settings/")) return []; + + const errors = []; + const text = readFileSync(file, "utf8"); + const lines = text.split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.includes("e2e-lint-allow")) continue; + for (const pat of BANNED_PATTERNS) { + if (pat.regex.test(line)) { + errors.push({ + file, + line: i + 1, + col: 1, + rule: pat.name, + message: `${pat.name}: ${pat.advice}`, + source: line.trim(), + }); + } + } + } + + // @mobile rule: skip _meta + const isMeta = file.startsWith(META_PREFIX); + if (!isMeta) { + if (!/@mobile\b/.test(text)) { + errors.push({ + file, + line: 1, + col: 1, + rule: "missing @mobile test", + message: + 'Spec must contain at least one test or describe tagged with "@mobile".', + source: "", + }); + } + } + + return errors; +} + +function main() { + const files = walk(SPECS_DIR); + const allErrors = []; + for (const f of files) { + allErrors.push(...lintFile(f)); + } + + if (allErrors.length === 0) { + console.log(`e2e:lint: ${files.length} spec files OK`); + process.exit(0); + } + + for (const err of allErrors) { + const rel = relative(process.cwd(), err.file); + console.error(`${rel}:${err.line}:${err.col} ${err.rule}`); + console.error(` ${err.message}`); + if (err.source) console.error(` > ${err.source}`); + } + console.error( + `\ne2e:lint: ${allErrors.length} error${allErrors.length === 1 ? "" : "s"} in ${files.length} files`, + ); + process.exit(1); +} + +main(); diff --git a/web/e2e/specs/_meta/error-collector.spec.ts b/web/e2e/specs/_meta/error-collector.spec.ts new file mode 100644 index 0000000..7a888d4 --- /dev/null +++ b/web/e2e/specs/_meta/error-collector.spec.ts @@ -0,0 +1,112 @@ +/** + * Self-tests for the error collector fixture itself. + * + * These guard against future regressions in the safety net. Each test + * deliberately triggers (or avoids triggering) an error to verify the + * collector behaves correctly. Tests that expect to fail use the + * `expectedErrors` fixture parameter to allowlist their own errors. + */ + +import { test, expect } from "../../fixtures/frigate-test"; + +// test.use applies to a whole describe block in Playwright, so each test +// that needs a custom allowlist gets its own describe. + +test.describe("Error Collector — clean @meta", () => { + test("clean page passes", async ({ frigateApp }) => { + await frigateApp.goto("/"); + // No errors triggered. The fixture teardown should not throw. + }); +}); + +test.describe("Error Collector — unallowlisted console error fails @meta", () => { + test("console.error fails the test when not allowlisted", async ({ + page, + frigateApp, + }) => { + test.skip( + process.env.E2E_STRICT_ERRORS !== "1", + "Requires E2E_STRICT_ERRORS=1 to assert failure", + ); + test.fail(); // We expect the fixture teardown to throw + await frigateApp.goto("/"); + await page.evaluate(() => { + // eslint-disable-next-line no-console + console.error("UNEXPECTED_DELIBERATE_TEST_ERROR_xyz123"); + }); + }); +}); + +test.describe("Error Collector — allowlisted console error passes @meta", () => { + test.use({ expectedErrors: [/ALLOWED_DELIBERATE_TEST_ERROR_xyz123/] }); + + test("console.error is silenced when allowlisted via expectedErrors", async ({ + page, + frigateApp, + }) => { + await frigateApp.goto("/"); + await page.evaluate(() => { + // eslint-disable-next-line no-console + console.error("ALLOWED_DELIBERATE_TEST_ERROR_xyz123"); + }); + }); +}); + +test.describe("Error Collector — uncaught pageerror fails @meta", () => { + test("uncaught pageerror fails the test", async ({ page, frigateApp }) => { + test.skip( + process.env.E2E_STRICT_ERRORS !== "1", + "Requires E2E_STRICT_ERRORS=1 to assert failure", + ); + test.fail(); + await frigateApp.goto("/"); + await page.evaluate(() => { + setTimeout(() => { + throw new Error("UNCAUGHT_DELIBERATE_TEST_ERROR_xyz789"); + }, 0); + }); + // Wait a frame to let the throw propagate before fixture teardown. + // The marker below silences the e2e:lint banned-pattern check on this line. + await page.waitForTimeout(100); // e2e-lint-allow: deliberate; need to await async throw + }); +}); + +test.describe("Error Collector — 5xx fails @meta", () => { + test("same-origin 5xx response fails the test", async ({ + page, + frigateApp, + }) => { + test.skip( + process.env.E2E_STRICT_ERRORS !== "1", + "Requires E2E_STRICT_ERRORS=1 to assert failure", + ); + test.fail(); + await page.route("**/api/version", (route) => + route.fulfill({ status: 500, body: "boom" }), + ); + await frigateApp.goto("/"); + await page.evaluate(() => fetch("/api/version").catch(() => {})); + // Give the response listener a microtask to fire + await expect.poll(async () => true).toBe(true); + }); +}); + +test.describe("Error Collector — allowlisted 5xx passes @meta", () => { + // Use a single alternation regex so test.use() receives a 1-element array. + // Playwright's isFixtureTuple() treats any [value, object] pair as a fixture + // tuple, so a 2-element array whose second item is a RegExp would be + // misinterpreted as [defaultValue, options]. Both the request collector + // error ("500 … /api/version") and the browser console error + // ("Failed to load resource … 500") are matched by the alternation below. + test.use({ + expectedErrors: [/500.*\/api\/version|Failed to load resource.*500/], + }); + + test("allowlisted 5xx passes", async ({ page, frigateApp }) => { + await page.route("**/api/version", (route) => + route.fulfill({ status: 500, body: "boom" }), + ); + await frigateApp.goto("/"); + await page.evaluate(() => fetch("/api/version").catch(() => {})); + }); +}); diff --git a/web/e2e/specs/_meta/mock-overrides.spec.ts b/web/e2e/specs/_meta/mock-overrides.spec.ts new file mode 100644 index 0000000..f3c1ae3 --- /dev/null +++ b/web/e2e/specs/_meta/mock-overrides.spec.ts @@ -0,0 +1,73 @@ +/** + * Self-tests for the mock override helpers. Verifies each helper + * intercepts the matched URL and returns the expected payload/status. + */ + +import { test, expect } from "../../fixtures/frigate-test"; +import { mockEmpty, mockError, mockDelay } from "../../helpers/mock-overrides"; + +test.describe("Mock Overrides — empty @meta", () => { + test("mockEmpty returns []", async ({ page, frigateApp }) => { + await mockEmpty(page, "**/api/__meta_test__"); + await frigateApp.goto("/"); + const result = await page.evaluate(async () => { + const r = await fetch("/api/__meta_test__"); + return { status: r.status, body: await r.json() }; + }); + expect(result.status).toBe(200); + expect(result.body).toEqual([]); + }); +}); + +test.describe("Mock Overrides — error default @meta", () => { + // Match both the collected request error and the browser's console echo. + // Using a single alternation regex avoids Playwright's isFixtureTuple + // collision with multi-element RegExp arrays. + test.use({ + expectedErrors: [/500.*__meta_test__|Failed to load resource.*500/], + }); + + test("mockError returns 500 by default", async ({ page, frigateApp }) => { + await mockError(page, "**/api/__meta_test__"); + await frigateApp.goto("/"); + const status = await page.evaluate(async () => { + const r = await fetch("/api/__meta_test__"); + return r.status; + }); + expect(status).toBe(500); + }); +}); + +test.describe("Mock Overrides — error custom status @meta", () => { + // The browser emits a "Failed to load resource" console.error for 404s, + // which the error collector catches even though 404 is not a 5xx. + test.use({ + expectedErrors: [/Failed to load resource.*404|404.*__meta_test_404__/], + }); + + test("mockError accepts a custom status", async ({ page, frigateApp }) => { + await mockError(page, "**/api/__meta_test_404__", 404); + await frigateApp.goto("/"); + const status = await page.evaluate(async () => { + const r = await fetch("/api/__meta_test_404__"); + return r.status; + }); + expect(status).toBe(404); + }); +}); + +test.describe("Mock Overrides — delay @meta", () => { + test("mockDelay delays response by the requested ms", async ({ + page, + frigateApp, + }) => { + await mockDelay(page, "**/api/__meta_test_delay__", 300, ["delayed"]); + await frigateApp.goto("/"); + const elapsed = await page.evaluate(async () => { + const start = performance.now(); + await fetch("/api/__meta_test_delay__"); + return performance.now() - start; + }); + expect(elapsed).toBeGreaterThanOrEqual(250); + }); +}); diff --git a/web/e2e/specs/auth.spec.ts b/web/e2e/specs/auth.spec.ts new file mode 100644 index 0000000..f0326a5 --- /dev/null +++ b/web/e2e/specs/auth.spec.ts @@ -0,0 +1,110 @@ +/** + * Auth and role tests -- HIGH tier. + * + * Admin access to /system, /config, /logs; viewer access denied + * markers (via i18n heading, not a data-testid we don't own); + * viewer nav restrictions; all-routes smoke. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { viewerProfile } from "../fixtures/mock-data/profile"; + +test.describe("Auth — admin access @high", () => { + test("admin /system renders general tab", async ({ frigateApp }) => { + await frigateApp.goto("/system"); + await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({ + timeout: 15_000, + }); + }); + + test("admin /config renders Monaco editor", async ({ frigateApp }) => { + await frigateApp.goto("/config"); + await expect( + frigateApp.page + .locator(".monaco-editor, [data-keybinding-context]") + .first(), + ).toBeVisible({ timeout: 15_000 }); + }); + + test("admin /logs renders frigate tab", async ({ frigateApp }) => { + await frigateApp.goto("/logs"); + await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({ + timeout: 5_000, + }); + }); +}); + +test.describe("Auth — viewer restrictions @high", () => { + for (const path of ["/system", "/config", "/logs"]) { + test(`viewer on ${path} sees AccessDenied`, async ({ frigateApp }) => { + await frigateApp.installDefaults({ profile: viewerProfile() }); + await frigateApp.page.goto(path); + await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + await expect( + frigateApp.page.getByRole("heading", { + level: 2, + name: /access denied/i, + }), + ).toBeVisible({ timeout: 10_000 }); + }); + } + + test("viewer sees cameras on /", async ({ frigateApp }) => { + await frigateApp.installDefaults({ profile: viewerProfile() }); + await frigateApp.page.goto("/"); + await expect( + frigateApp.page.locator("[data-camera='front_door']"), + ).toBeVisible({ timeout: 10_000 }); + }); + + test("viewer sees severity tabs on /review", async ({ frigateApp }) => { + await frigateApp.installDefaults({ profile: viewerProfile() }); + await frigateApp.page.goto("/review"); + await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({ + timeout: 10_000, + }); + }); + + test("viewer can access all non-admin routes without AccessDenied", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ profile: viewerProfile() }); + const routes = ["/", "/review", "/explore", "/export", "/settings"]; + for (const route of routes) { + await frigateApp.page.goto(route); + await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + await expect( + frigateApp.page.getByRole("heading", { + level: 2, + name: /access denied/i, + }), + ).toHaveCount(0); + } + }); +}); + +test.describe("Auth — viewer nav restrictions (desktop) @high", () => { + test.skip(({ frigateApp }) => frigateApp.isMobile, "Sidebar only on desktop"); + + test("viewer sidebar hides admin routes", async ({ frigateApp }) => { + await frigateApp.installDefaults({ profile: viewerProfile() }); + await frigateApp.page.goto("/"); + await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + for (const href of ["/system", "/config", "/logs"]) { + await expect( + frigateApp.page.locator(`aside a[href='${href}']`), + ).toHaveCount(0); + } + }); +}); + +test.describe("Auth — all routes smoke @high @mobile", () => { + test("every common route renders #pageRoot", async ({ frigateApp }) => { + for (const route of ["/", "/review", "/explore", "/export", "/settings"]) { + await frigateApp.goto(route); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({ + timeout: 10_000, + }); + } + }); +}); diff --git a/web/e2e/specs/chat.spec.ts b/web/e2e/specs/chat.spec.ts new file mode 100644 index 0000000..cab4982 --- /dev/null +++ b/web/e2e/specs/chat.spec.ts @@ -0,0 +1,358 @@ +/** + * Chat page tests -- MEDIUM tier. + * + * Starting state, NDJSON streaming contract (not SSE), assistant + * bubble grows as chunks arrive, error path, and mobile viewport. + */ + +import { test, expect, type FrigateApp } from "../fixtures/frigate-test"; + +/** + * Install a window.fetch override on the page so that POSTs to + * chat/completion resolve with a real ReadableStream that emits the + * given chunks over time. This is the only way to validate + * chunk-by-chunk rendering through Playwright — page.route() does not + * support streaming responses. + * + * Must be called BEFORE frigateApp.goto(). The override also exposes + * `__chatRequests` on window so tests can assert the outgoing body. + */ +async function installChatStreamOverride( + app: FrigateApp, + chunks: Array>, + opts: { chunkDelayMs?: number; status?: number } = {}, +) { + const { chunkDelayMs = 40, status = 200 } = opts; + await app.page.addInitScript( + ({ chunks, chunkDelayMs, status }) => { + (window as unknown as { __chatRequests: unknown[] }).__chatRequests = []; + const origFetch = window.fetch; + window.fetch = async (input, init) => { + const url = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : (input as Request).url; + if (url.includes("chat/completion")) { + const body = + init?.body instanceof String || typeof init?.body === "string" + ? JSON.parse(init!.body as string) + : null; + ( + window as unknown as { __chatRequests: unknown[] } + ).__chatRequests.push({ url, body }); + if (status !== 200) { + return new Response(JSON.stringify({ error: "boom" }), { + status, + }); + } + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + async start(controller) { + for (const chunk of chunks) { + await new Promise((r) => setTimeout(r, chunkDelayMs)); + controller.enqueue( + encoder.encode(JSON.stringify(chunk) + "\n"), + ); + } + controller.close(); + }, + }); + return new Response(stream, { status: 200 }); + } + return origFetch.call(window, input as RequestInfo, init); + }; + }, + { chunks, chunkDelayMs, status }, + ); +} + +test.describe("Chat — starting state @medium", () => { + test("empty message list renders ChatStartingState with title and input", async ({ + frigateApp, + }) => { + await frigateApp.goto("/chat"); + await expect( + frigateApp.page.getByRole("heading", { level: 1 }), + ).toBeVisible({ timeout: 10_000 }); + await expect(frigateApp.page.getByPlaceholder(/ask/i)).toBeVisible(); + // Four quick-reply buttons from starting_requests.* + const quickReplies = frigateApp.page.locator( + "button:has-text('Show recent events'), button:has-text('Show camera status'), button:has-text('What happened'), button:has-text('Watch')", + ); + await expect(quickReplies.first()).toBeVisible({ timeout: 5_000 }); + }); +}); + +test.describe("Chat — streaming @medium", () => { + test("submission POSTs to chat/completion with stream: true", async ({ + frigateApp, + }) => { + await installChatStreamOverride(frigateApp, [ + { type: "content", delta: "Hel" }, + { type: "content", delta: "lo" }, + { + type: "messages", + messages: [ + { role: "system", content: "sys" }, + { role: "user", content: "hello chat" }, + { role: "assistant", content: "Hello" }, + ], + }, + { type: "done" }, + ]); + await frigateApp.goto("/chat"); + const input = frigateApp.page.getByPlaceholder(/ask/i); + await expect(input).toBeVisible({ timeout: 10_000 }); + await input.fill("hello chat"); + await input.press("Enter"); + + await expect + .poll( + async () => + frigateApp.page.evaluate( + () => + (window as unknown as { __chatRequests: unknown[] }) + .__chatRequests?.length ?? 0, + ), + { timeout: 5_000 }, + ) + .toBeGreaterThan(0); + + const request = await frigateApp.page.evaluate( + () => + ( + window as unknown as { + __chatRequests: Array<{ + url: string; + body: { stream: boolean; messages: Array<{ content: string }> }; + }>; + } + ).__chatRequests[0], + ); + expect(request.body.stream).toBe(true); + expect( + request.body.messages[request.body.messages.length - 1].content, + ).toBe("hello chat"); + }); + + test("NDJSON content chunks accumulate in the assistant bubble", async ({ + frigateApp, + }) => { + await installChatStreamOverride( + frigateApp, + [ + { type: "content", delta: "Hel" }, + { type: "content", delta: "lo, " }, + { type: "content", delta: "world!" }, + { + type: "messages", + messages: [ + { role: "system", content: "sys" }, + { role: "user", content: "greet me" }, + { role: "assistant", content: "Hello, world!" }, + ], + }, + { type: "done" }, + ], + { chunkDelayMs: 50 }, + ); + await frigateApp.goto("/chat"); + const input = frigateApp.page.getByPlaceholder(/ask/i); + await expect(input).toBeVisible({ timeout: 10_000 }); + await input.fill("greet me"); + await input.press("Enter"); + + await expect(frigateApp.page.getByText(/Hello, world!/i)).toBeVisible({ + timeout: 10_000, + }); + }); + + test("tool calls in the chain render a ToolCallsGroup", async ({ + frigateApp, + }) => { + const toolTurn = [ + { role: "system", content: "sys" }, + { role: "user", content: "find people" }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_1", + type: "function", + function: { + name: "search_objects", + arguments: '{"label":"person"}', + }, + }, + ], + }, + { role: "tool", tool_call_id: "call_1", content: "[]" }, + ]; + await installChatStreamOverride(frigateApp, [ + { type: "messages", messages: toolTurn }, + { type: "content", delta: "Searching for people." }, + { + type: "messages", + messages: [ + ...toolTurn, + { role: "assistant", content: "Searching for people." }, + ], + }, + { type: "done" }, + ]); + await frigateApp.goto("/chat"); + const input = frigateApp.page.getByPlaceholder(/ask/i); + await expect(input).toBeVisible({ timeout: 10_000 }); + await input.fill("find people"); + await input.press("Enter"); + + // ToolCallsGroup normalizes "search_objects" → "Search Objects" via + // normalizeName(). Match the rendered display label instead. + await expect(frigateApp.page.getByText(/search objects/i)).toBeVisible({ + timeout: 10_000, + }); + await expect( + frigateApp.page.getByText(/searching for people/i), + ).toBeVisible({ timeout: 5_000 }); + }); +}); + +test.describe("Chat — stop @medium", () => { + test("Stop button aborts an in-flight stream and freezes the partial message", async ({ + frigateApp, + }) => { + // A long chunk sequence with big delays gives us time to hit Stop. + await installChatStreamOverride( + frigateApp, + [ + { type: "content", delta: "First chunk. " }, + { type: "content", delta: "Second chunk. " }, + { type: "content", delta: "Third chunk. " }, + ], + { chunkDelayMs: 300 }, + ); + await frigateApp.goto("/chat"); + const input = frigateApp.page.getByPlaceholder(/ask/i); + await expect(input).toBeVisible({ timeout: 10_000 }); + await input.fill("slow response please"); + await input.press("Enter"); + + // Wait for the first chunk to render + await expect(frigateApp.page.getByText(/First chunk\./)).toBeVisible({ + timeout: 10_000, + }); + + // The Stop button is a destructive rounded button shown while isLoading. + // It contains only an FaStop SVG icon (no visible text). Find it by the + // destructive variant class or fall back to aria-label. + const stopBtn = frigateApp.page + .locator("button.bg-destructive, button[class*='destructive']") + .first(); + await stopBtn.click({ timeout: 3_000 }).catch(async () => { + await frigateApp.page + .getByRole("button", { name: /stop|cancel/i }) + .first() + .click(); + }); + + // Third chunk should never appear. + await expect(frigateApp.page.getByText(/Third chunk\./)).toHaveCount(0); + }); +}); + +test.describe("Chat — error @medium", () => { + test("non-OK response renders an error banner", async ({ frigateApp }) => { + await installChatStreamOverride(frigateApp, [], { status: 500 }); + await frigateApp.goto("/chat"); + const input = frigateApp.page.getByPlaceholder(/ask/i); + await expect(input).toBeVisible({ timeout: 10_000 }); + await input.fill("trigger error"); + await input.press("Enter"); + // The error banner is a role="alert" paragraph; target by role so we + // don't collide with the user-message bubble that contains "trigger + // error" (which would match /error/ in strict mode). + await expect( + frigateApp.page.getByRole("alert").filter({ + hasText: /boom|something went wrong/i, + }), + ).toBeVisible({ timeout: 5_000 }); + }); +}); + +test.describe("Chat — attachment chip @medium", () => { + test("attaching an event renders a ChatAttachmentChip", async ({ + frigateApp, + }) => { + // The chat starts with an empty message list (ChatStartingState). + // After sending a message, ChatEntry with the paperclip button appears. + // We use the stream override so the first message completes quickly. + await installChatStreamOverride(frigateApp, [ + { type: "content", delta: "Done." }, + { + type: "messages", + messages: [ + { role: "system", content: "sys" }, + { role: "user", content: "hello" }, + { role: "assistant", content: "Done." }, + ], + }, + { type: "done" }, + ]); + await frigateApp.goto("/chat"); + + // Send a first message to transition out of ChatStartingState so the + // full ChatEntry (with the paperclip) is visible. + const input = frigateApp.page.getByPlaceholder(/ask/i); + await expect(input).toBeVisible({ timeout: 10_000 }); + await input.fill("hello"); + await input.press("Enter"); + // Wait for the assistant response to complete so isLoading becomes false + // and the paperclip button is re-enabled. + await expect(frigateApp.page.getByText(/Done\./i)).toBeVisible({ + timeout: 10_000, + }); + + // The paperclip button has aria-label from t("attachment_picker_placeholder") + // = "Attach an event". + const paperclip = frigateApp.page + .getByRole("button", { name: /attach an event/i }) + .first(); + await expect(paperclip).toBeVisible({ timeout: 5_000 }); + await paperclip.click(); + + // The popover shows a paste input with placeholder "Or paste event ID". + const idInput = frigateApp.page + .locator('input[placeholder*="event" i], input[aria-label*="attach" i]') + .first(); + await expect(idInput).toBeVisible({ timeout: 3_000 }); + await idInput.fill("test-event-1"); + await frigateApp.page + .getByRole("button", { name: /^attach$/i }) + .first() + .click(); + + // The ChatAttachmentChip renders in the composer area. It shows an + // activity indicator while loading event data (event_ids API not mocked), + // so assert on the chip container being present in the composer. + await expect( + frigateApp.page.locator( + "[class*='inline-flex'][class*='rounded-lg'][class*='border']", + ), + ).toBeVisible({ timeout: 5_000 }); + }); +}); + +test.describe("Chat — mobile @medium @mobile", () => { + test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only"); + + test("chat input is focusable at mobile viewport", async ({ frigateApp }) => { + await frigateApp.goto("/chat"); + const input = frigateApp.page.getByPlaceholder(/ask/i); + await expect(input).toBeVisible({ timeout: 10_000 }); + await input.focus(); + await expect(input).toBeFocused(); + }); +}); diff --git a/web/e2e/specs/classification.spec.ts b/web/e2e/specs/classification.spec.ts new file mode 100644 index 0000000..83a33a8 --- /dev/null +++ b/web/e2e/specs/classification.spec.ts @@ -0,0 +1,228 @@ +/** + * Classification page tests -- MEDIUM tier. + * + * Model list driven by config.classification.custom + per-model + * dataset fetches. Admin-only access. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { viewerProfile } from "../fixtures/mock-data/profile"; + +const CUSTOM_MODELS = { + object_classifier: { + name: "object_classifier", + object_config: { objects: ["person"], classification_type: "sub_label" }, + }, + state_classifier: { + name: "state_classifier", + state_config: { cameras: { front_door: { crop: [0, 0, 1, 1] } } }, + }, +}; + +async function installDatasetRoute( + app: { page: import("@playwright/test").Page }, + name: string, + body: Record = { categories: {} }, +) { + await app.page.route( + new RegExp(`/api/classification/${name}/dataset`), + (route) => route.fulfill({ json: body }), + ); +} + +async function installTrainRoute( + app: { page: import("@playwright/test").Page }, + name: string, +) { + await app.page.route( + new RegExp(`/api/classification/${name}/train`), + (route) => route.fulfill({ json: [] }), + ); +} + +test.describe("Classification — model list @medium", () => { + test("custom models render by name", async ({ frigateApp }) => { + await frigateApp.installDefaults({ + config: { classification: { custom: CUSTOM_MODELS } }, + }); + await installDatasetRoute(frigateApp, "object_classifier"); + await installDatasetRoute(frigateApp, "state_classifier"); + await frigateApp.goto("/classification"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({ + timeout: 10_000, + }); + }); + + test("empty custom map renders without crash", async ({ frigateApp }) => { + await frigateApp.installDefaults({ + config: { classification: { custom: {} } }, + }); + await frigateApp.goto("/classification"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({ + timeout: 10_000, + }); + }); + + test("toggling to states view switches the rendered card set", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ + config: { classification: { custom: CUSTOM_MODELS } }, + }); + await installDatasetRoute(frigateApp, "object_classifier"); + await installDatasetRoute(frigateApp, "state_classifier"); + await frigateApp.goto("/classification"); + // Objects is default — object_classifier visible, state_classifier hidden. + await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({ + timeout: 10_000, + }); + await expect(frigateApp.page.getByText("state_classifier")).toHaveCount(0); + + // Click the "states" toggle. Radix ToggleGroup type="single" uses role="radio". + const statesToggle = frigateApp.page + .getByRole("radio", { name: /state/i }) + .first(); + await expect(statesToggle).toBeVisible({ timeout: 5_000 }); + await statesToggle.click(); + + await expect(frigateApp.page.getByText("state_classifier")).toBeVisible({ + timeout: 5_000, + }); + await expect(frigateApp.page.getByText("object_classifier")).toHaveCount(0); + }); +}); + +test.describe("Classification — model detail navigation @medium", () => { + test("clicking a model card opens ModelTrainingView", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ + config: { classification: { custom: CUSTOM_MODELS } }, + }); + await installDatasetRoute(frigateApp, "object_classifier"); + await installDatasetRoute(frigateApp, "state_classifier"); + await installTrainRoute(frigateApp, "object_classifier"); + await frigateApp.goto("/classification"); + + const objectCard = frigateApp.page.getByText("object_classifier").first(); + await expect(objectCard).toBeVisible({ timeout: 10_000 }); + await objectCard.click(); + + // ModelTrainingView renders a Back button (aria-label "Back"). + // useOverlayState stores the selected model in window.history.state + // (not the URL), so we verify the state transition via the DOM. + await expect( + frigateApp.page.getByRole("button", { name: /back/i }), + ).toBeVisible({ timeout: 5_000 }); + + // The model grid is no longer shown; state_classifier card is gone. + await expect(frigateApp.page.getByText("state_classifier")).toHaveCount(0); + }); +}); + +test.describe("Classification — delete model (desktop) @medium", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Delete action menu is desktop-focused", + ); + + test("deleting a model fires DELETE + PUT /config/set", async ({ + frigateApp, + }) => { + let deleteCalled = false; + let configSetCalled = false; + + // installDefaults must run first because Playwright matches routes in + // LIFO order — routes registered after installDefaults take precedence + // over the generic catch-all registered inside it. + await frigateApp.installDefaults({ + config: { classification: { custom: CUSTOM_MODELS } }, + }); + await installDatasetRoute(frigateApp, "object_classifier"); + await installDatasetRoute(frigateApp, "state_classifier"); + + // Register spy routes after installDefaults so they win over the catch-all. + await frigateApp.page.route( + /\/api\/classification\/object_classifier$/, + async (route) => { + if (route.request().method() === "DELETE") { + deleteCalled = true; + await route.fulfill({ json: { success: true } }); + return; + } + return route.fallback(); + }, + ); + await frigateApp.page.route("**/api/config/set", async (route) => { + if (route.request().method() === "PUT") configSetCalled = true; + await route.fulfill({ json: { success: true, require_restart: false } }); + }); + await frigateApp.goto("/classification"); + await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({ + timeout: 10_000, + }); + + // The card-level actions menu (FiMoreVertical three-dot icon) is a + // DropdownMenuTrigger with asChild on a BlurredIconButton div. + // Radix forwards aria-haspopup="menu" to the child element. + // Scope the selector to the model card grid to avoid hitting the + // settings sidebar trigger. + const cardGrid = frigateApp.page.locator(".grid.auto-rows-max"); + await expect(cardGrid).toBeVisible({ timeout: 5_000 }); + const trigger = cardGrid.locator('[aria-haspopup="menu"]').first(); + await expect(trigger).toBeVisible({ timeout: 5_000 }); + await trigger.click(); + const deleteItem = frigateApp.page + .getByRole("menuitem", { name: /delete/i }) + .first(); + await expect(deleteItem).toBeVisible({ timeout: 5_000 }); + await deleteItem.click(); + + // Confirm the AlertDialog. + const alert = frigateApp.page.getByRole("alertdialog"); + await expect(alert).toBeVisible({ timeout: 5_000 }); + await alert + .getByRole("button", { name: /delete|confirm/i }) + .first() + .click(); + + await expect.poll(() => deleteCalled, { timeout: 5_000 }).toBe(true); + await expect.poll(() => configSetCalled, { timeout: 5_000 }).toBe(true); + }); +}); + +test.describe("Classification — admin only @medium", () => { + test("viewer navigating to /classification is redirected to access-denied", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ profile: viewerProfile() }); + await frigateApp.page.goto("/classification"); + await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + await expect(frigateApp.page).toHaveURL(/\/unauthorized/, { + timeout: 10_000, + }); + await expect( + frigateApp.page.getByRole("heading", { + level: 2, + name: /access denied/i, + }), + ).toBeVisible({ timeout: 10_000 }); + }); +}); + +test.describe("Classification — mobile @medium @mobile", () => { + test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only"); + + test("page renders at mobile viewport", async ({ frigateApp }) => { + await frigateApp.installDefaults({ + config: { classification: { custom: CUSTOM_MODELS } }, + }); + await installDatasetRoute(frigateApp, "object_classifier"); + await installDatasetRoute(frigateApp, "state_classifier"); + await frigateApp.goto("/classification"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({ + timeout: 10_000, + }); + }); +}); diff --git a/web/e2e/specs/clone-camera.spec.ts b/web/e2e/specs/clone-camera.spec.ts new file mode 100644 index 0000000..1c75e71 --- /dev/null +++ b/web/e2e/specs/clone-camera.spec.ts @@ -0,0 +1,181 @@ +/** + * Camera clone dialog E2E tests. + * + * Covers the design invariants that don't depend on per-camera resolution + * differences in the mock fixture: + * 1. Dialog opens from the "Clone settings" button below Add/Delete. + * 2. A source camera must be chosen inside the dialog before cloning. + * 3. "Stream URLs and roles" is forced on and disabled for new-camera target. + * 4. Cloning to a new camera issues a single add PUT and shows a restart prompt. + * 5. The existing-camera target selects multiple destinations via a switch + * popover (with an "All cameras" toggle and source exclusion); the closed + * trigger summarizes the selection by name or as "All cameras". + * + * The spatial-mismatch warning path is exercised in unit-level review and via + * manual QA — the shared mock fixture ships every camera at 1280×720. The + * existing-camera PUT fan-out is likewise not asserted here: the mock cameras + * are identical apart from stream URLs (which existing-camera clones never + * copy) and the schema mock is empty, so a clone onto them produces no diff + * and no PUT. That path is covered by unit-level review and manual QA. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +async function openCloneDialog(frigateApp: { + page: import("@playwright/test").Page; +}) { + await frigateApp.page + .getByRole("button", { name: /^Clone settings$/i }) + .click(); + await expect(frigateApp.page.getByRole("dialog")).toBeVisible(); +} + +async function selectSource( + frigateApp: { page: import("@playwright/test").Page }, + source: string, +) { + await frigateApp.page.getByRole("dialog").getByRole("combobox").click(); + await frigateApp.page + .getByRole("option", { name: source, exact: true }) + .click(); +} + +test.describe("Camera clone dialog @medium @mobile", () => { + test.beforeEach(async ({ frigateApp }) => { + await frigateApp.goto("/settings?page=cameraManagement"); + await expect( + frigateApp.page.getByRole("heading", { name: /Manage Cameras/i }), + ).toBeVisible(); + }); + + test("opens the dialog from the Clone settings button", async ({ + frigateApp, + }) => { + await openCloneDialog(frigateApp); + + await expect( + frigateApp.page.getByRole("dialog").getByText(/Clone camera settings/i), + ).toBeVisible(); + + // The Clone button is disabled until a source (and target) is chosen. + await expect( + frigateApp.page.getByRole("button", { name: /^Clone$/i }), + ).toBeDisabled(); + }); + + test("forces Stream URLs and roles on for new-camera target", async ({ + frigateApp, + }) => { + await openCloneDialog(frigateApp); + await selectSource(frigateApp, "Front Door"); + + // The "New camera" radio is selected by default; the Streams group renders + // the ffmpeg_live checkbox as forced-checked and disabled. + const streamsLabel = frigateApp.page + .locator("label") + .filter({ hasText: /Stream URLs and roles/i }); + await expect(streamsLabel).toBeVisible(); + + const streamsCheckbox = streamsLabel.getByRole("checkbox"); + await expect(streamsCheckbox).toBeChecked(); + await expect(streamsCheckbox).toBeDisabled(); + }); + + test("issues a single add PUT and shows restart toast for new-camera target", async ({ + frigateApp, + }) => { + const requests: { body: unknown }[] = []; + + await frigateApp.page.route("**/api/config/set", async (route) => { + const body = route.request().postDataJSON(); + requests.push({ body }); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ success: true, require_restart: false }), + }); + }); + + await frigateApp.goto("/settings?page=cameraManagement"); + await expect( + frigateApp.page.getByRole("heading", { name: /Manage Cameras/i }), + ).toBeVisible(); + + await openCloneDialog(frigateApp); + await selectSource(frigateApp, "Front Door"); + + const nameInput = frigateApp.page.getByPlaceholder( + /e\.g\., back_door or Back Door/i, + ); + await nameInput.fill("clone_target_one"); + + // With a source picked and a valid name, changeCount > 0 enables Clone. + await expect( + frigateApp.page.getByRole("button", { name: /^Clone$/i }), + ).toBeEnabled({ timeout: 5_000 }); + + await frigateApp.page.getByRole("button", { name: /^Clone$/i }).click(); + + // New-camera clones bundle into a single atomic add PUT (avoids + // per-section validation ordering issues). + await expect.poll(() => requests.length, { timeout: 10_000 }).toBe(1); + + const firstBody = requests[0].body as { + requires_restart?: number; + update_topic?: string; + }; + expect(firstBody.update_topic).toMatch( + /config\/cameras\/clone_target_one\/add/, + ); + expect(firstBody.requires_restart).toBe(1); + + // The toast offers a Restart action because new-camera always needs restart. + // .first() avoids strict-mode rejection when both the toast action and the + // RestartDialog trigger render concurrently. + await expect( + frigateApp.page.getByRole("button", { name: /Restart/i }).first(), + ).toBeVisible({ timeout: 8_000 }); + }); + + test("selects multiple existing destination cameras via a switch popover", async ({ + frigateApp, + }) => { + await openCloneDialog(frigateApp); + await selectSource(frigateApp, "Front Door"); + + await frigateApp.page + .getByRole("radio", { name: /Existing cameras/i }) + .click(); + + const dialog = frigateApp.page.getByRole("dialog"); + + // The destination trigger starts with the empty-selection placeholder. + await dialog + .getByRole("button", { name: /Select at least one camera/i }) + .click(); + + // The chosen source is excluded from the destination switch list. + await expect( + dialog.getByRole("switch", { name: /Backyard/i }), + ).toBeVisible(); + await expect(dialog.getByRole("switch", { name: /Garage/i })).toBeVisible(); + await expect( + dialog.getByRole("switch", { name: /^Front Door$/i }), + ).toHaveCount(0); + + // Selecting a single camera summarizes by name once the popover closes. + await dialog.getByRole("switch", { name: /Backyard/i }).click(); + await frigateApp.page.keyboard.press("Escape"); + await expect( + dialog.getByRole("button", { name: /^Backyard$/i }), + ).toBeVisible(); + + // Reopen and select everything; the trigger collapses to "All cameras". + await dialog.getByRole("button", { name: /^Backyard$/i }).click(); + await dialog.getByRole("switch", { name: /^All cameras$/i }).click(); + await frigateApp.page.keyboard.press("Escape"); + await expect( + dialog.getByRole("button", { name: /^All cameras$/i }), + ).toBeVisible(); + }); +}); diff --git a/web/e2e/specs/config-editor.spec.ts b/web/e2e/specs/config-editor.spec.ts new file mode 100644 index 0000000..2b51a93 --- /dev/null +++ b/web/e2e/specs/config-editor.spec.ts @@ -0,0 +1,276 @@ +/** + * Config Editor tests -- MEDIUM tier. + * + * Monaco load + value, Save (config/save?save_option=saveonly), + * Save error path, Save and Restart (WS frame via useRestart), + * Copy (clipboard), schema markers. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { installWsFrameCapture, waitForWsFrame } from "../helpers/ws-frames"; +import { grantClipboardPermissions, readClipboard } from "../helpers/clipboard"; +import { + getMonacoVisibleText, + replaceMonacoValue, + waitForErrorMarker, +} from "../helpers/monaco"; + +const SAMPLE_CONFIG = + "mqtt:\n host: mqtt\ncameras:\n front_door:\n enabled: true\n"; + +async function installSaveRoute( + app: { page: import("@playwright/test").Page }, + status: number, + body: Record, +): Promise<{ + capturedUrl: () => string | null; + capturedBody: () => string | null; +}> { + let lastUrl: string | null = null; + let lastBody: string | null = null; + await app.page.route("**/api/config/save**", async (route) => { + lastUrl = route.request().url(); + lastBody = route.request().postData(); + await route.fulfill({ status, json: body }); + }); + return { + capturedUrl: () => lastUrl, + capturedBody: () => lastBody, + }; +} + +test.describe("Config Editor — Monaco @medium", () => { + test("editor loads with mocked configRaw content", async ({ frigateApp }) => { + await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); + await frigateApp.goto("/config"); + await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible( + { timeout: 15_000 }, + ); + // Assert via DOM-rendered visible text (Monaco virtualizes — works + // for short configs which covers our mocked content). + await expect + .poll(() => getMonacoVisibleText(frigateApp.page), { timeout: 10_000 }) + .toContain("front_door"); + }); +}); + +test.describe("Config Editor — Save @medium", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Save button copy is desktop-visible (hidden md:block)", + ); + + test("clicking Save Only POSTs config/save?save_option=saveonly", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); + const capture = await installSaveRoute(frigateApp, 200, { + message: "Config saved", + }); + await frigateApp.goto("/config"); + await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible( + { timeout: 15_000 }, + ); + await frigateApp.page.getByLabel("Save Only").click(); + await expect + .poll(() => capture.capturedUrl(), { timeout: 5_000 }) + .toMatch(/config\/save\?save_option=saveonly/); + // Body is the raw YAML as text/plain + await expect + .poll(() => capture.capturedBody(), { timeout: 5_000 }) + .toContain("front_door"); + }); + + test("Save error shows the server message in the error area", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); + await installSaveRoute(frigateApp, 400, { + message: "Invalid field `cameras.front_door`", + }); + await frigateApp.goto("/config"); + await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible( + { timeout: 15_000 }, + ); + await frigateApp.page.getByLabel("Save Only").click(); + await expect(frigateApp.page.getByText(/Invalid field/i)).toBeVisible({ + timeout: 5_000, + }); + }); +}); + +test.describe("Config Editor — Save and Restart @medium", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Save and Restart button copy is desktop-visible", + ); + + test("Save and Restart opens dialog; confirm sends WS restart frame", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); + await installSaveRoute(frigateApp, 200, { message: "Saved" }); + await installWsFrameCapture(frigateApp.page); + + await frigateApp.goto("/config"); + await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible( + { timeout: 15_000 }, + ); + + await frigateApp.page.getByLabel("Save & Restart").click(); + const dialog = frigateApp.page.getByRole("alertdialog"); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + + await dialog.getByRole("button", { name: /restart/i }).click(); + await waitForWsFrame( + frigateApp.page, + (frame) => frame.includes('"restart"') || frame.includes("restart"), + { message: "useRestart should send a WS frame on the restart topic" }, + ); + }); + + test("cancelling the restart dialog leaves body interactive", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); + await installSaveRoute(frigateApp, 200, { message: "Saved" }); + + await frigateApp.goto("/config"); + await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible( + { timeout: 15_000 }, + ); + + await frigateApp.page.getByLabel("Save & Restart").click(); + const dialog = frigateApp.page.getByRole("alertdialog"); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + await dialog.getByRole("button", { name: /cancel/i }).click(); + await expect(dialog).not.toBeVisible({ timeout: 3_000 }); + await expect( + frigateApp.page.locator(".monaco-editor").first(), + ).toBeVisible(); + }); +}); + +test.describe("Config Editor — Copy @medium", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Copy button copy is desktop-visible", + ); + + test("Copy places the editor value in the clipboard", async ({ + frigateApp, + context, + }) => { + await grantClipboardPermissions(context); + await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); + await frigateApp.goto("/config"); + await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible( + { timeout: 15_000 }, + ); + + await frigateApp.page.getByLabel("Copy Config").click(); + await expect + .poll(() => readClipboard(frigateApp.page), { timeout: 5_000 }) + .toContain("front_door"); + }); +}); + +test.describe("Config Editor — schema markers @medium", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Schema validation assumes focused desktop editing", + ); + + test("invalid YAML renders at least one error marker in the DOM", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); + await frigateApp.goto("/config"); + await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible( + { timeout: 15_000 }, + ); + + // Replace editor contents with clearly invalid YAML via keyboard. + await replaceMonacoValue( + frigateApp.page, + "this is not: [yaml: and has {unbalanced", + ); + // Monaco debounces marker evaluation; the .squiggly-error decoration + // appears asynchronously in the .view-overlays layer. + await waitForErrorMarker(frigateApp.page); + }); +}); + +test.describe("Config Editor — Cmd+S keyboard shortcut @medium", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Keyboard save shortcut is desktop-only", + ); + + test("Cmd/Ctrl+S fires the same config/save POST as the Save button", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); + const capture = await installSaveRoute(frigateApp, 200, { + message: "Saved", + }); + await frigateApp.goto("/config"); + await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible( + { timeout: 15_000 }, + ); + + // Focus the editor so Monaco's keybinding receives the shortcut. + await frigateApp.page.locator(".monaco-editor").first().click(); + await frigateApp.page.keyboard.press("ControlOrMeta+s"); + + await expect + .poll(() => capture.capturedUrl(), { timeout: 5_000 }) + .toMatch(/config\/save\?save_option=saveonly/); + }); +}); + +test.describe("Config Editor — Safe Mode auto-validation @medium", () => { + test("safe-mode config auto-posts on mount and shows the inline error", async ({ + frigateApp, + }) => { + // Thread safe_mode: true through the config override, then stub + // config/save to return a validation error. The page's + // initialValidationRef effect runs on mount and POSTs + // config/save?save_option=saveonly with the raw config; the 400 + // surfaces through setError. + // installDefaults must come first so our specific route wins (LIFO). + await frigateApp.installDefaults({ + config: { safe_mode: true } as unknown as Record, + configRaw: "cameras:\n front_door:\n ffmpeg: {}\n", + }); + let autoSaveCalled = false; + await frigateApp.page.route("**/api/config/save**", async (route) => { + autoSaveCalled = true; + await route.fulfill({ + status: 400, + json: { message: "safe-mode validation failure" }, + }); + }); + + await frigateApp.goto("/config"); + await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible( + { timeout: 15_000 }, + ); + await expect.poll(() => autoSaveCalled, { timeout: 10_000 }).toBe(true); + await expect( + frigateApp.page.getByText(/safe-mode validation failure/i), + ).toBeVisible({ timeout: 5_000 }); + }); +}); + +test.describe("Config Editor — mobile @medium @mobile", () => { + test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only"); + + test("editor renders at narrow viewport", async ({ frigateApp }) => { + await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); + await frigateApp.goto("/config"); + await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible( + { timeout: 15_000 }, + ); + }); +}); diff --git a/web/e2e/specs/explore.spec.ts b/web/e2e/specs/explore.spec.ts new file mode 100644 index 0000000..9d239a3 --- /dev/null +++ b/web/e2e/specs/explore.spec.ts @@ -0,0 +1,265 @@ +/** + * Explore page tests -- HIGH tier. + * + * Search input, Enter submission, camera filter popover (desktop), + * event grid rendering with mocked events, mobile filter drawer. + * + * DEVIATION NOTES (from original plan): + * + * 1. Search input: InputWithTags is only rendered when + * config.semantic_search.enabled is true. Tests that exercise the search + * input override the config accordingly, using model:"genai" (not in the + * JINA_EMBEDDING_MODELS list) so the page skips local model-state checks + * and renders without waiting for model-download WS messages. + * + * 2. Filter buttons (Cameras, Labels, More Filters): SearchFilterGroup is + * only rendered when hasExistingSearch is true. Tests navigate with a URL + * param (?labels=person) to surface the filter bar. + * + * 3. Cameras button: accessible name is "Cameras Filter" (aria-label), not + * "All Cameras" (inner text). Use getByLabel("Cameras Filter"). + * + * 4. Labels: button accessible name is "Labels" (aria-label). With + * ?labels=person, the text shows "Person" rather than "All Labels". + * Use getByLabel("Labels"). + * + * 5. Sub-labels / Zones: These live inside the "More Filters" dialog + * (SearchFilterDialog), not as standalone top-level buttons. The Zones + * test opens "More Filters" and asserts zone content from config. + * + * 6. similarity_search_id URL param: This param does not exist in the app. + * The correct entrypoint for similarity search is + * ?search_type=similarity&event_id=. The test uses this URL and + * polls for the resulting API request. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +// Semantic search config override used by multiple tests. Using model: +// "genai" (not in JINA_EMBEDDING_MODELS) sets isGenaiEmbeddings=true, which +// skips local model-state checks and lets the page render without waiting for +// individual model download WS messages. The WS mocker returns a completed +// reindexState so !reindexState is false and the loading gate clears. +const SEMANTIC_SEARCH_CONFIG = { + semantic_search: { enabled: true, model: "genai" }, +} as const; + +// --------------------------------------------------------------------------- +// Search input (semantic_search must be enabled) +// --------------------------------------------------------------------------- + +test.describe("Explore — search @high", () => { + test("search input accepts text and clears", async ({ frigateApp }) => { + // Enable semantic search so InputWithTags renders. + await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG }); + await frigateApp.goto("/explore"); + const searchInput = frigateApp.page.locator("input").first(); + await expect(searchInput).toBeVisible({ timeout: 10_000 }); + await searchInput.fill("person"); + await expect(searchInput).toHaveValue("person"); + await searchInput.fill(""); + await expect(searchInput).toHaveValue(""); + }); + + test("Enter submission does not crash the page", async ({ frigateApp }) => { + await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG }); + await frigateApp.goto("/explore"); + const searchInput = frigateApp.page.locator("input").first(); + await expect(searchInput).toBeVisible({ timeout: 10_000 }); + await searchInput.fill("car in driveway"); + await searchInput.press("Enter"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); +}); + +// --------------------------------------------------------------------------- +// Filter bar — desktop only +// Filter buttons appear once hasExistingSearch is true (URL params present). +// --------------------------------------------------------------------------- + +test.describe("Explore — filters (desktop) @high", () => { + test.skip(({ frigateApp }) => frigateApp.isMobile, "Desktop popovers"); + + test("Cameras popover lists configured cameras", async ({ frigateApp }) => { + // Navigate with a labels filter param so the filter bar renders. + await frigateApp.goto("/explore?labels=person"); + // CamerasFilterButton has aria-label="Cameras Filter". Use getByLabel to + // match against the accessible name (not the inner "All Cameras" text). + const camerasBtn = frigateApp.page.getByLabel("Cameras Filter").first(); + await expect(camerasBtn).toBeVisible({ timeout: 10_000 }); + await camerasBtn.click(); + // DropdownMenu on desktop wraps content in data-radix-popper-content-wrapper. + const popover = frigateApp.page.locator( + "[data-radix-popper-content-wrapper]", + ); + await expect(popover.first()).toBeVisible({ timeout: 3_000 }); + await expect(frigateApp.page.getByText("Front Door")).toBeVisible(); + }); + + test("Labels filter lists labels from config", async ({ frigateApp }) => { + // Navigate with an existing search so the filter bar renders. + await frigateApp.goto("/explore?labels=person"); + // GeneralFilterButton has aria-label="Labels". With ?labels=person the + // button text shows "Person" (the selected label), but the aria-label + // remains "Labels". + const labelsBtn = frigateApp.page.getByLabel("Labels").first(); + await expect(labelsBtn).toBeVisible({ timeout: 10_000 }); + await labelsBtn.click(); + // PlatformAwareDialog renders on desktop as a dropdown/popover overlay. + const overlay = frigateApp.page.locator( + "[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']", + ); + await expect(overlay.first()).toBeVisible({ timeout: 3_000 }); + // "person" is already selected (it's in the URL); assert it appears in + // the overlay content. + await expect(overlay.first().getByText(/person/i)).toBeVisible(); + await frigateApp.page.keyboard.press("Escape"); + }); + + test("Sub-labels filter renders inside More Filters dialog", async ({ + frigateApp, + }) => { + // Sub-labels live inside SearchFilterDialog ("More Filters" button). + // With sub_labels mocked as [], the section still renders its heading. + await frigateApp.page.route("**/api/sub_labels**", (route) => + route.fulfill({ json: [] }), + ); + await frigateApp.goto("/explore?labels=person"); + const moreBtn = frigateApp.page.getByLabel("More Filters").first(); + await expect(moreBtn).toBeVisible({ timeout: 10_000 }); + await moreBtn.click(); + const overlay = frigateApp.page.locator( + "[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']", + ); + await expect(overlay.first()).toBeVisible({ timeout: 3_000 }); + // "Sub Labels" section heading always renders inside the dialog. + await expect( + frigateApp.page.getByText(/sub.?label/i).first(), + ).toBeVisible(); + await frigateApp.page.keyboard.press("Escape"); + }); + + test("Zones filter lists configured zones inside More Filters dialog", async ({ + frigateApp, + }) => { + // Override config to guarantee a known zone on front_door. + await frigateApp.installDefaults({ + config: { + cameras: { + front_door: { + zones: { + front_yard: { coordinates: "0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9" }, + }, + }, + }, + }, + }); + await frigateApp.goto("/explore?labels=person"); + const moreBtn = frigateApp.page.getByLabel("More Filters").first(); + await expect(moreBtn).toBeVisible({ timeout: 10_000 }); + await moreBtn.click(); + const overlay = frigateApp.page.locator( + "[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']", + ); + await expect(overlay.first()).toBeVisible({ timeout: 3_000 }); + await expect(frigateApp.page.getByText(/front.?yard/i)).toBeVisible(); + await frigateApp.page.keyboard.press("Escape"); + }); +}); + +// --------------------------------------------------------------------------- +// Content +// --------------------------------------------------------------------------- + +test.describe("Explore — content @high", () => { + test("page renders with mock events", async ({ frigateApp }) => { + await frigateApp.goto("/explore"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({ + timeout: 10_000, + }); + await expect( + frigateApp.page.locator("#pageRoot button").first(), + ).toBeVisible({ timeout: 10_000 }); + }); + + test("empty events renders without crash", async ({ frigateApp }) => { + await frigateApp.installDefaults({ events: [] }); + await frigateApp.goto("/explore"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({ + timeout: 10_000, + }); + }); + + test("search fires a /api/events request with the query", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG }); + const eventsRequests: string[] = []; + frigateApp.page.on("request", (req) => { + const url = req.url(); + if (/\/api\/events/.test(url)) eventsRequests.push(url); + }); + await frigateApp.goto("/explore"); + const searchInput = frigateApp.page.locator("input").first(); + await expect(searchInput).toBeVisible({ timeout: 10_000 }); + + const before = eventsRequests.length; + await searchInput.fill("person in driveway"); + await searchInput.press("Enter"); + await expect + .poll(() => eventsRequests.length > before, { timeout: 5_000 }) + .toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// Similarity search URL param +// --------------------------------------------------------------------------- + +test.describe("Explore — similarity search (desktop) @high", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Similarity trigger is hover-based; desktop-focused", + ); + + test("URL similarity search params fetch events", async ({ frigateApp }) => { + const eventsRequests: string[] = []; + frigateApp.page.on("request", (req) => { + const url = req.url(); + if (/\/api\/events/.test(url)) eventsRequests.push(url); + }); + // The app uses search_type=similarity&event_id= (not + // similarity_search_id). This exercises the same similarity search code + // path as clicking "Find Similar" on a thumbnail. + // Use a valid event-id format (timestamp.fractional-alphanumeric). + await frigateApp.goto( + "/explore?search_type=similarity&event_id=1712412000.000000-abc123", + ); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({ + timeout: 10_000, + }); + // Poll to allow any pending SWR fetch to complete and be captured. + await expect + .poll(() => eventsRequests.length, { timeout: 5_000 }) + .toBeGreaterThan(0); + }); +}); + +// --------------------------------------------------------------------------- +// Mobile +// --------------------------------------------------------------------------- + +test.describe("Explore — mobile @high @mobile", () => { + test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only"); + + test("search input is focusable at mobile viewport", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG }); + await frigateApp.goto("/explore"); + const searchInput = frigateApp.page.locator("input").first(); + await expect(searchInput).toBeVisible({ timeout: 10_000 }); + await searchInput.focus(); + await expect(searchInput).toBeFocused(); + }); +}); diff --git a/web/e2e/specs/export.spec.ts b/web/e2e/specs/export.spec.ts new file mode 100644 index 0000000..5b7e9f0 --- /dev/null +++ b/web/e2e/specs/export.spec.ts @@ -0,0 +1,1041 @@ +import { test, expect } from "../fixtures/frigate-test"; +import { + expectBodyInteractive, + waitForBodyInteractive, +} from "../helpers/overlay-interaction"; + +test.describe("Export Page - Delete race @high", () => { + // Empirical guard for radix-ui/primitives#3445: when a modal DropdownMenu + // opens an AlertDialog and the AlertDialog's confirm action causes the + // parent's optimistic cache update to unmount the card, we want to know + // whether the deduped react-dismissable-layer (1.1.11) handles the + // pointer-events stack cleanup or whether `modal={false}` is still + // required on the DropdownMenu. The classic "canonical" pattern, distinct + // from the FaceSelectionDialog auto-unmount race already covered by + // face-library.spec.ts. + test("deleting an export via dropdown→alert→confirm leaves body interactive", async ({ + frigateApp, + }) => { + if (frigateApp.isMobile) { + test.skip(); + return; + } + + const initialExports = [ + { + id: "export-race-001", + camera: "front_door", + name: "Race - Test Export", + date: 1775490731.3863528, + video_path: "/exports/export-race-001.mp4", + thumb_path: "/exports/export-race-001-thumb.jpg", + in_progress: false, + export_case_id: null, + }, + ]; + let deleted = false; + + await frigateApp.installDefaults({ + exports: initialExports, + }); + + // Flip /api/export to empty after the delete POST is observed so the + // page's SWR mutate sees the export gone. + await frigateApp.page.route("**/api/export**", async (route) => { + const payload = deleted ? [] : initialExports; + await route.fulfill({ json: payload }); + }); + await frigateApp.page.route("**/api/exports/delete", async (route) => { + deleted = true; + const delayMs = Number( + (globalThis as { process?: { env?: Record } }).process + ?.env?.DELETE_DELAY_MS ?? "100", + ); + if (delayMs > 0) { + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + await route.fulfill({ json: { success: true } }); + }); + + await frigateApp.goto("/export"); + await expect(frigateApp.page.getByText("Race - Test Export")).toBeVisible({ + timeout: 5_000, + }); + + // Open the kebab menu on the export card. The kebab uses the + // (misleading) aria-label "Edit name" from ExportCard's source — it + // wraps the FiMoreVertical icon. There is exactly one such button on + // the page once we have a single export rendered. + const kebab = frigateApp.page + .getByRole("button", { name: /edit name/i }) + .first(); + await expect(kebab).toBeVisible({ timeout: 5_000 }); + await kebab.click(); + + const menu = frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + await expect(menu).toBeVisible({ timeout: 3_000 }); + + // Delete Export + await menu + .getByRole("menuitem", { name: /delete export/i }) + .first() + .click(); + + // AlertDialog at page level. The confirm button's accessible name is + // "Delete Export" (its aria-label), the visible text is just "Delete". + const confirm = frigateApp.page.getByRole("alertdialog"); + await expect(confirm).toBeVisible({ timeout: 3_000 }); + await confirm + .getByRole("button", { name: /^delete export$/i }) + .first() + .click(); + + // The card optimistically disappears, the dialog closes, and body + // pointer-events must come unstuck. + await expect( + frigateApp.page.getByText("Race - Test Export"), + ).not.toBeVisible({ timeout: 5_000 }); + await waitForBodyInteractive(frigateApp.page, 5_000); + await expectBodyInteractive(frigateApp.page); + + // Sanity: another page-level button still responds. + const newCase = frigateApp.page.getByRole("button", { name: /new case/i }); + await expect(newCase).toBeVisible({ timeout: 3_000 }); + await newCase.click(); + await expect( + frigateApp.page.getByRole("dialog").filter({ hasText: /create case/i }), + ).toBeVisible({ timeout: 3_000 }); + }); +}); + +test.describe("Export Page - Overview @high", () => { + test("renders uncategorized exports and case cards from mock data", async ({ + frigateApp, + }) => { + await frigateApp.goto("/export"); + + await expect( + frigateApp.page.getByText("Front Door - Person Alert"), + ).toBeVisible(); + await expect( + frigateApp.page.getByText("Garage - In Progress"), + ).toBeVisible(); + await expect( + frigateApp.page.getByText("Package Theft Investigation"), + ).toBeVisible(); + }); + + test("search filters uncategorized exports", async ({ frigateApp }) => { + await frigateApp.goto("/export"); + + const searchInput = frigateApp.page.getByPlaceholder(/search/i).first(); + await searchInput.fill("Front Door"); + + await expect( + frigateApp.page.getByText("Front Door - Person Alert"), + ).toBeVisible(); + await expect( + frigateApp.page.getByText("Backyard - Car Detection"), + ).toBeHidden(); + await expect( + frigateApp.page.getByText("Garage - In Progress"), + ).toBeHidden(); + }); + + test("new case button opens the create case dialog", async ({ + frigateApp, + }) => { + await frigateApp.goto("/export"); + + await frigateApp.page.getByRole("button", { name: "New Case" }).click(); + + await expect( + frigateApp.page.getByRole("dialog").filter({ hasText: "Create Case" }), + ).toBeVisible(); + await expect(frigateApp.page.getByPlaceholder("Case name")).toBeVisible(); + }); +}); + +test.describe("Export Page - Case Detail @high", () => { + test("opening a case shows its detail view and associated export", async ({ + frigateApp, + }) => { + await frigateApp.goto("/export"); + + await frigateApp.page + .getByText("Package Theft Investigation") + .first() + .click(); + + await expect( + frigateApp.page.getByRole("heading", { + name: "Package Theft Investigation", + }), + ).toBeVisible(); + await expect( + frigateApp.page.getByText("Backyard - Car Detection"), + ).toBeVisible(); + await expect( + frigateApp.page.getByRole("button", { name: "Add Export" }), + ).toBeVisible(); + await expect( + frigateApp.page.getByRole("button", { name: "Edit Case" }), + ).toBeVisible(); + await expect( + frigateApp.page.getByRole("button", { name: "Delete Case" }), + ).toBeVisible(); + }); + + test("edit case opens a prefilled dialog", async ({ frigateApp }) => { + await frigateApp.goto("/export"); + + await frigateApp.page + .getByText("Package Theft Investigation") + .first() + .click(); + await frigateApp.page.getByRole("button", { name: "Edit Case" }).click(); + + const dialog = frigateApp.page + .getByRole("dialog") + .filter({ hasText: "Edit Case" }); + await expect(dialog).toBeVisible(); + await expect(dialog.locator("input")).toHaveValue( + "Package Theft Investigation", + ); + await expect(dialog.locator("textarea")).toHaveValue( + "Review of suspicious activity near the front porch", + ); + }); + + test("add export shows completed uncategorized exports for assignment", async ({ + frigateApp, + }) => { + await frigateApp.goto("/export"); + + await frigateApp.page + .getByText("Package Theft Investigation") + .first() + .click(); + await frigateApp.page.getByRole("button", { name: "Add Export" }).click(); + + const dialog = frigateApp.page + .getByRole("dialog") + .filter({ hasText: "Add Export to Package Theft Investigation" }); + await expect(dialog).toBeVisible(); + // Completed, uncategorized exports are selectable + await expect(dialog.getByText("Front Door - Person Alert")).toBeVisible(); + // In-progress exports are intentionally hidden by AssignExportDialog + // (see Exports.tsx filteredExports) — they can't be assigned until + // they finish, so they should not show in the picker. + await expect(dialog.getByText("Garage - In Progress")).toBeHidden(); + }); + + test("delete case opens a confirmation dialog", async ({ frigateApp }) => { + await frigateApp.goto("/export"); + + await frigateApp.page + .getByText("Package Theft Investigation") + .first() + .click(); + await frigateApp.page.getByRole("button", { name: "Delete Case" }).click(); + + const dialog = frigateApp.page + .getByRole("alertdialog") + .filter({ hasText: "Delete Case" }); + await expect(dialog).toBeVisible(); + await expect(dialog.getByText(/Package Theft Investigation/)).toBeVisible(); + }); + + test("delete case can also delete its exports", async ({ frigateApp }) => { + let deleteRequestUrl: string | null = null; + let deleteCaseCompleted = false; + + const initialCases = [ + { + id: "case-001", + name: "Package Theft Investigation", + description: "Review of suspicious activity near the front porch", + created_at: 1775407931.3863528, + updated_at: 1775483531.3863528, + }, + ]; + + const initialExports = [ + { + id: "export-001", + camera: "front_door", + name: "Front Door - Person Alert", + date: 1775490731.3863528, + video_path: "/exports/export-001.mp4", + thumb_path: "/exports/export-001-thumb.jpg", + in_progress: false, + export_case_id: null, + }, + { + id: "export-002", + camera: "backyard", + name: "Backyard - Car Detection", + date: 1775483531.3863528, + video_path: "/exports/export-002.mp4", + thumb_path: "/exports/export-002-thumb.jpg", + in_progress: false, + export_case_id: "case-001", + }, + { + id: "export-003", + camera: "garage", + name: "Garage - In Progress", + date: 1775492531.3863528, + video_path: "/exports/export-003.mp4", + thumb_path: "/exports/export-003-thumb.jpg", + in_progress: true, + export_case_id: null, + }, + ]; + + await frigateApp.page.route(/\/api\/cases(?:$|\?|\/)/, async (route) => { + const request = route.request(); + + if (request.method() === "DELETE") { + deleteRequestUrl = request.url(); + deleteCaseCompleted = true; + return route.fulfill({ json: { success: true } }); + } + + if (request.method() === "GET") { + return route.fulfill({ + json: deleteCaseCompleted ? [] : initialCases, + }); + } + + return route.fallback(); + }); + + await frigateApp.page.route("**/api/exports**", async (route) => { + if (route.request().method() !== "GET") { + return route.fallback(); + } + + return route.fulfill({ + json: deleteCaseCompleted + ? initialExports.filter((exp) => exp.export_case_id !== "case-001") + : initialExports, + }); + }); + + await frigateApp.goto("/export"); + + await frigateApp.page + .getByText("Package Theft Investigation") + .first() + .click(); + await frigateApp.page.getByRole("button", { name: "Delete Case" }).click(); + + const dialog = frigateApp.page + .getByRole("alertdialog") + .filter({ hasText: "Delete Case" }); + await expect(dialog).toBeVisible(); + + const deleteExportsSwitch = dialog.getByRole("switch", { + name: "Also delete exports", + }); + await expect(deleteExportsSwitch).toHaveAttribute("aria-checked", "false"); + await expect( + dialog.getByText( + "Exports will remain available as uncategorized exports.", + ), + ).toBeVisible(); + + await deleteExportsSwitch.click(); + + await expect(deleteExportsSwitch).toHaveAttribute("aria-checked", "true"); + await expect( + dialog.getByText("All exports in this case will be permanently deleted."), + ).toBeVisible(); + + await dialog.getByRole("button", { name: /^delete$/i }).click(); + + await expect + .poll(() => deleteRequestUrl) + .toContain("/api/cases/case-001?delete_exports=true"); + + await expect(dialog).toBeHidden(); + await expect( + frigateApp.page.getByRole("heading", { + name: "Package Theft Investigation", + }), + ).toBeHidden(); + await expect( + frigateApp.page.getByText("Backyard - Car Detection"), + ).toBeHidden(); + await expect( + frigateApp.page.getByText("Front Door - Person Alert"), + ).toBeVisible(); + }); +}); + +test.describe("Export Page - Empty State @high", () => { + test("renders the empty state when there are no exports or cases", async ({ + frigateApp, + }) => { + await frigateApp.page.route("**/api/export**", (route) => + route.fulfill({ json: [] }), + ); + await frigateApp.page.route("**/api/exports**", (route) => + route.fulfill({ json: [] }), + ); + await frigateApp.page.route("**/api/cases", (route) => + route.fulfill({ json: [] }), + ); + await frigateApp.page.route("**/api/cases**", (route) => + route.fulfill({ json: [] }), + ); + + await frigateApp.goto("/export"); + + await expect(frigateApp.page.getByText("No exports found")).toBeVisible(); + }); +}); + +test.describe("Export Page - Mobile @high @mobile", () => { + test("mobile can open an export preview dialog", async ({ frigateApp }) => { + test.skip(!frigateApp.isMobile, "Mobile-only assertion"); + + await frigateApp.goto("/export"); + + await frigateApp.page + .getByText("Front Door - Person Alert") + .first() + .click(); + + const dialog = frigateApp.page + .getByRole("dialog") + .filter({ hasText: "Front Door - Person Alert" }); + await expect(dialog).toBeVisible(); + await expect(dialog.locator("video")).toBeVisible(); + }); +}); + +test.describe("Multi-Review Export @high", () => { + // Two alert reviews close enough to "now" to fall within the + // default last-24-hours review window. Using numeric timestamps + // because the TS ReviewSegment type expects numbers even though + // the backend pydantic model serializes datetime as ISO strings — + // the app reads these as numbers for display math. + const now = Date.now() / 1000; + const mockReviews = [ + { + id: "mex-review-001", + camera: "front_door", + start_time: now - 600, + end_time: now - 580, + has_been_reviewed: false, + severity: "alert", + thumb_path: "/clips/front_door/mex-review-001-thumb.jpg", + data: { + audio: [], + detections: ["person-001"], + objects: ["person"], + sub_labels: [], + significant_motion_areas: [], + zones: ["front_yard"], + }, + }, + { + id: "mex-review-002", + camera: "backyard", + start_time: now - 1200, + end_time: now - 1170, + has_been_reviewed: false, + severity: "alert", + thumb_path: "/clips/backyard/mex-review-002-thumb.jpg", + data: { + audio: [], + detections: ["car-002"], + objects: ["car"], + sub_labels: [], + significant_motion_areas: [], + zones: ["driveway"], + }, + }, + ]; + + // 51 alert reviews, all front_door, spaced 5 minutes apart. Used by the + // over-limit test to trigger Ctrl+A select-all and verify the Export + // button is hidden at 51 selected. + const oversizedReviews = Array.from({ length: 51 }, (_, i) => ({ + id: `mex-oversized-${i.toString().padStart(3, "0")}`, + camera: "front_door", + start_time: now - 60 * 60 - i * 300, + end_time: now - 60 * 60 - i * 300 + 20, + has_been_reviewed: false, + severity: "alert", + thumb_path: `/clips/front_door/mex-oversized-${i}-thumb.jpg`, + data: { + audio: [], + detections: [`person-${i}`], + objects: ["person"], + sub_labels: [], + significant_motion_areas: [], + zones: ["front_yard"], + }, + })); + + const mockSummary = { + last24Hours: { + reviewed_alert: 0, + reviewed_detection: 0, + total_alert: 2, + total_detection: 0, + }, + }; + + async function routeReviews( + page: import("@playwright/test").Page, + reviews: unknown[], + ) { + // Intercept the actual `/api/review` endpoint (singular — the + // default api-mocker only registers `/api/reviews**` (plural) + // which does not match the real request URL). + await page.route(/\/api\/review(\?|$)/, (route) => + route.fulfill({ json: reviews }), + ); + await page.route(/\/api\/review\/summary/, (route) => + route.fulfill({ json: mockSummary }), + ); + } + + test.beforeEach(async ({ frigateApp }) => { + await routeReviews(frigateApp.page, mockReviews); + // Empty cases list by default so the dialog defaults to "new case". + // Individual tests override this to populate existing cases. + await frigateApp.page.route("**/api/cases", (route) => + route.fulfill({ json: [] }), + ); + }); + + async function selectTwoReviews(frigateApp: { + page: import("@playwright/test").Page; + }) { + // Every review card has className `review-item` on its wrapper + // (see EventView.tsx). Cards also have data-start attributes that + // we can key off if needed. + const reviewItems = frigateApp.page.locator(".review-item"); + await reviewItems.first().waitFor({ state: "visible", timeout: 10_000 }); + + // Meta-click the first two items to enter multi-select mode. + // PreviewThumbnailPlayer reads e.metaKey to decide multi-select. + await reviewItems.nth(0).click({ modifiers: ["Meta"] }); + await reviewItems.nth(1).click(); + } + + test("selecting two reviews reveals the export button", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop multi-select flow"); + + await frigateApp.goto("/review"); + + await selectTwoReviews(frigateApp); + + // Action group replaces the filter bar once items are selected + await expect(frigateApp.page.getByText(/2.*selected/i)).toBeVisible({ + timeout: 5_000, + }); + + const exportButton = frigateApp.page.getByRole("button", { + name: /export/i, + }); + await expect(exportButton).toBeVisible(); + }); + + test("clicking export opens the multi-review dialog with correct title", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop multi-select flow"); + + await frigateApp.goto("/review"); + + await selectTwoReviews(frigateApp); + + await frigateApp.page + .getByRole("button", { name: /export/i }) + .first() + .click(); + + const dialog = frigateApp.page + .getByRole("dialog") + .filter({ hasText: /Export 2 reviews/i }); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + // The dialog uses a Select trigger for case selection (admins). The + // default "None" value is shown on the trigger. + await expect(dialog.locator("button[role='combobox']")).toBeVisible(); + await expect(dialog.getByText(/None/)).toBeVisible(); + }); + + test("starting an export posts the expected payload and navigates to the case", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop multi-select flow"); + + let capturedPayload: unknown = null; + await frigateApp.page.route("**/api/exports/batch", async (route) => { + capturedPayload = route.request().postDataJSON(); + await route.fulfill({ + status: 202, + json: { + export_case_id: "new-case-xyz", + export_ids: ["front_door_a", "backyard_b"], + results: [ + { + camera: "front_door", + export_id: "front_door_a", + success: true, + status: "queued", + error: null, + item_index: 0, + }, + { + camera: "backyard", + export_id: "backyard_b", + success: true, + status: "queued", + error: null, + item_index: 1, + }, + ], + }, + }); + }); + + await frigateApp.goto("/review"); + await selectTwoReviews(frigateApp); + await frigateApp.page + .getByRole("button", { name: /export/i }) + .first() + .click(); + + const dialog = frigateApp.page + .getByRole("dialog") + .filter({ hasText: /Export 2 reviews/i }); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + + // Select "Create new case" from the case dropdown (default is "None") + await dialog.locator("button[role='combobox']").click(); + await frigateApp.page + .getByRole("option", { name: /Create new case/i }) + .click(); + + const nameInput = dialog.locator("input").first(); + await nameInput.fill("E2E Incident"); + + await dialog.getByRole("button", { name: /export 2 reviews/i }).click(); + + // Wait for the POST to fire + await expect.poll(() => capturedPayload, { timeout: 5_000 }).not.toBeNull(); + + const payload = capturedPayload as { + items: Array<{ + camera: string; + start_time: number; + end_time: number; + image_path?: string; + client_item_id?: string; + }>; + new_case_name?: string; + export_case_id?: string; + }; + expect(payload.items).toHaveLength(2); + expect(payload.new_case_name).toBe("E2E Incident"); + // When creating a new case, we must NOT also send export_case_id — + // the two fields are mutually exclusive on the backend. + expect(payload.export_case_id).toBeUndefined(); + expect(payload.items.map((i) => i.camera).sort()).toEqual([ + "backyard", + "front_door", + ]); + // Each item must preserve REVIEW_PADDING (4s) on the edges — + // i.e. the padded window is 8s longer than the original review. + // The mock reviews above have 20s and 30s raw durations, so the + // expected padded durations are 28s and 38s. + const paddedDurations = payload.items + .map((i) => i.end_time - i.start_time) + .sort((a, b) => a - b); + expect(paddedDurations).toEqual([28, 38]); + // Thumbnails should be passed through per item + for (const item of payload.items) { + expect(item.image_path).toMatch(/mex-review-\d+-thumb\.jpg$/); + } + expect(payload.items.map((item) => item.client_item_id)).toEqual([ + "mex-review-001", + "mex-review-002", + ]); + + await expect(frigateApp.page).toHaveURL(/caseId=new-case-xyz/, { + timeout: 5_000, + }); + }); + + test("mobile opens a drawer (not a dialog) for the multi-review export flow", async ({ + frigateApp, + }) => { + test.skip(!frigateApp.isMobile, "Mobile-only Drawer assertion"); + + await frigateApp.goto("/review"); + await selectTwoReviews(frigateApp); + + await frigateApp.page + .getByRole("button", { name: /export/i }) + .first() + .click(); + + // On mobile the component renders a shadcn Drawer, which uses + // role="dialog" but sets data-vaul-drawer. Desktop renders a + // shadcn Dialog with role="dialog" but no data-vaul-drawer. + // The title and submit button both contain "Export 2 reviews", so + // assert each element distinctly: the title is a heading and the + // submit button has role="button". + const drawer = frigateApp.page.locator("[data-vaul-drawer]"); + await expect(drawer).toBeVisible({ timeout: 5_000 }); + await expect( + drawer.getByRole("heading", { name: /Export 2 reviews/i }), + ).toBeVisible(); + await expect( + drawer.getByRole("button", { name: /export 2 reviews/i }), + ).toBeVisible(); + }); + + test("hides export button when more than 50 reviews are selected", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop select-all keyboard flow"); + + // Override the default 2-review mock with 51 reviews before + // navigation. Playwright matches routes last-registered-first so + // this takes precedence over the beforeEach. + await routeReviews(frigateApp.page, oversizedReviews); + + await frigateApp.goto("/review"); + + // Wait for any review item to render before firing the shortcut + await frigateApp.page + .locator(".review-item") + .first() + .waitFor({ state: "visible", timeout: 10_000 }); + + // Ctrl+A triggers onSelectAllReviews (see EventView.tsx useKeyboardListener) + await frigateApp.page.keyboard.press("Control+a"); + + // The action group should show "51 selected" but no Export button. + // Mark-as-reviewed is still there so the action bar is rendered. + // Scope the "Mark as reviewed" lookup to its exact aria-label because + // the page can render other "mark as reviewed" controls elsewhere + // (e.g. on individual cards) that would trip strict-mode matching. + await expect(frigateApp.page.getByText(/51.*selected/i)).toBeVisible({ + timeout: 5_000, + }); + await expect( + frigateApp.page.getByRole("button", { name: "Mark as reviewed" }), + ).toBeVisible(); + await expect( + frigateApp.page.getByRole("button", { name: /^export$/i }), + ).toHaveCount(0); + }); + + test("attaching to an existing case sends export_case_id without new_case_name", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop multi-select flow"); + + // Seed one existing case so the dialog can offer the "existing" branch. + // The fixture mocks the user as admin (adminProfile()), so useIsAdmin() + // is true and the dialog renders the "Existing case" radio. + await frigateApp.page.route("**/api/cases", (route) => + route.fulfill({ + json: [ + { + id: "existing-case-abc", + name: "Incident #42", + description: "", + created_at: now - 3600, + updated_at: now - 3600, + }, + ], + }), + ); + + let capturedPayload: unknown = null; + await frigateApp.page.route("**/api/exports/batch", async (route) => { + capturedPayload = route.request().postDataJSON(); + await route.fulfill({ + status: 202, + json: { + export_case_id: "existing-case-abc", + export_ids: ["front_door_a", "backyard_b"], + results: [ + { + camera: "front_door", + export_id: "front_door_a", + success: true, + status: "queued", + error: null, + item_index: 0, + }, + { + camera: "backyard", + export_id: "backyard_b", + success: true, + status: "queued", + error: null, + item_index: 1, + }, + ], + }, + }); + }); + + await frigateApp.goto("/review"); + await selectTwoReviews(frigateApp); + + await frigateApp.page + .getByRole("button", { name: /export/i }) + .first() + .click(); + + const dialog = frigateApp.page + .getByRole("dialog") + .filter({ hasText: /Export 2 reviews/i }); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + + // Open the Case Select dropdown and pick the seeded case directly. + // The dialog now uses a single Select listing existing cases above + // the "Create new case" option — no radio toggle needed. + const selectTrigger = dialog.locator("button[role='combobox']").first(); + await selectTrigger.waitFor({ state: "visible", timeout: 5_000 }); + await selectTrigger.click(); + + // The dropdown portal renders outside the dialog + await frigateApp.page.getByRole("option", { name: /Incident #42/ }).click(); + + await dialog.getByRole("button", { name: /export 2 reviews/i }).click(); + + await expect.poll(() => capturedPayload, { timeout: 5_000 }).not.toBeNull(); + + const payload = capturedPayload as { + items: unknown[]; + new_case_name?: string; + new_case_description?: string; + export_case_id?: string; + }; + expect(payload.export_case_id).toBe("existing-case-abc"); + expect(payload.new_case_name).toBeUndefined(); + expect(payload.new_case_description).toBeUndefined(); + expect(payload.items).toHaveLength(2); + + // Navigate should hit /export. useSearchEffect consumes the caseId + // query param and strips it once the case is found in the cases list, + // so we assert on the path, not the query string. + await expect(frigateApp.page).toHaveURL(/\/export(\?|$)/, { + timeout: 5_000, + }); + }); +}); + +test.describe("Export Page - Active Job Progress @medium", () => { + test("encoding job renders percent label and progress bar", async ({ + frigateApp, + }) => { + // Override the default empty mock with an encoding job. Per-test + // page.route registrations win over those set by the api-mocker. + await frigateApp.page.route("**/api/jobs/export", (route) => + route.fulfill({ + json: [ + { + id: "job-encoding", + job_type: "export", + status: "running", + camera: "front_door", + name: "Encoding Sample", + export_case_id: null, + request_start_time: 1775407931, + request_end_time: 1775408531, + start_time: 1775407932, + end_time: null, + error_message: null, + results: null, + current_step: "encoding", + progress_percent: 42, + }, + ], + }), + ); + + await frigateApp.goto("/export"); + + await expect(frigateApp.page.getByText("Encoding Sample")).toBeVisible(); + // Step label and percent are rendered together as text near the + // progress bar (separated by a middle dot), not in a corner badge. + await expect(frigateApp.page.getByText(/Encoding\s*·\s*42%/)).toBeVisible(); + }); + + test("queued job shows queued badge", async ({ frigateApp }) => { + await frigateApp.page.route("**/api/jobs/export", (route) => + route.fulfill({ + json: [ + { + id: "job-queued", + job_type: "export", + status: "queued", + camera: "front_door", + name: "Queued Sample", + export_case_id: null, + request_start_time: 1775407931, + request_end_time: 1775408531, + start_time: null, + end_time: null, + error_message: null, + results: null, + current_step: "queued", + progress_percent: 0, + }, + ], + }), + ); + + await frigateApp.goto("/export"); + + await expect(frigateApp.page.getByText("Queued Sample")).toBeVisible(); + await expect( + frigateApp.page.getByText("Queued", { exact: true }), + ).toBeVisible(); + }); + + test("active job hides matching in_progress export row", async ({ + frigateApp, + }) => { + // The backend inserts the Export row with in_progress=True before + // FFmpeg starts encoding, so the same id appears in BOTH /jobs/export + // and /exports during the run. The page must show the rich progress + // card from the active jobs feed and suppress the binary-spinner + // ExportCard from the exports feed; otherwise the older binary + // spinner replaces the percent label as soon as SWR re-polls. + await frigateApp.page.route("**/api/jobs/export", (route) => + route.fulfill({ + json: [ + { + id: "shared-id", + job_type: "export", + status: "running", + camera: "front_door", + name: "Shared Id Encoding", + export_case_id: null, + request_start_time: 1775407931, + request_end_time: 1775408531, + start_time: 1775407932, + end_time: null, + error_message: null, + results: null, + current_step: "encoding", + progress_percent: 67, + }, + ], + }), + ); + + await frigateApp.page.route("**/api/exports**", (route) => { + if (route.request().method() !== "GET") { + return route.fallback(); + } + return route.fulfill({ + json: [ + { + id: "shared-id", + camera: "front_door", + name: "Shared Id Encoding", + date: 1775407931, + video_path: "/exports/shared-id.mp4", + thumb_path: "/exports/shared-id-thumb.jpg", + in_progress: true, + export_case_id: null, + }, + ], + }); + }); + + await frigateApp.goto("/export"); + + // The progress label must be present — proving the rich card won. + await expect(frigateApp.page.getByText(/Encoding\s*·\s*67%/)).toBeVisible(); + + // And only ONE card should be visible for that id, not two. + const titles = frigateApp.page.getByText("Shared Id Encoding"); + await expect(titles).toHaveCount(1); + }); + + test("stream copy job shows copying label", async ({ frigateApp }) => { + // Default (non-custom) exports use `-c copy`, which is a remux, not + // a real encode. The step label should read "Copying" so users + // aren't misled into thinking re-encoding is happening. + await frigateApp.page.route("**/api/jobs/export", (route) => + route.fulfill({ + json: [ + { + id: "job-copying", + job_type: "export", + status: "running", + camera: "front_door", + name: "Copy Sample", + export_case_id: null, + request_start_time: 1775407931, + request_end_time: 1775408531, + start_time: 1775407932, + end_time: null, + error_message: null, + results: null, + current_step: "copying", + progress_percent: 80, + }, + ], + }), + ); + + await frigateApp.goto("/export"); + + await expect(frigateApp.page.getByText("Copy Sample")).toBeVisible(); + await expect(frigateApp.page.getByText(/Copying\s*·\s*80%/)).toBeVisible(); + }); + + test("encoding retry job shows retry label", async ({ frigateApp }) => { + await frigateApp.page.route("**/api/jobs/export", (route) => + route.fulfill({ + json: [ + { + id: "job-retry", + job_type: "export", + status: "running", + camera: "front_door", + name: "Retry Sample", + export_case_id: null, + request_start_time: 1775407931, + request_end_time: 1775408531, + start_time: 1775407932, + end_time: null, + error_message: null, + results: null, + current_step: "encoding_retry", + progress_percent: 12, + }, + ], + }), + ); + + await frigateApp.goto("/export"); + + await expect(frigateApp.page.getByText("Retry Sample")).toBeVisible(); + await expect( + frigateApp.page.getByText(/Encoding \(retry\)\s*·\s*12%/), + ).toBeVisible(); + }); +}); diff --git a/web/e2e/specs/face-library.spec.ts b/web/e2e/specs/face-library.spec.ts new file mode 100644 index 0000000..e499178 --- /dev/null +++ b/web/e2e/specs/face-library.spec.ts @@ -0,0 +1,542 @@ +/** + * Face Library page tests -- HIGH tier. + * + * Collection selector, face tiles, grouped recent-recognition dialog + * (migrated from radix-overlay-regressions.spec.ts), and mobile + * library selector. + */ + +import { type Locator } from "@playwright/test"; +import { test, expect, type FrigateApp } from "../fixtures/frigate-test"; +import { + basicFacesMock, + emptyFacesMock, + withGroupedTrainingAttempt, +} from "../fixtures/mock-data/faces"; +import { + expectBodyInteractive, + waitForBodyInteractive, +} from "../helpers/overlay-interaction"; + +const GROUPED_EVENT_ID = "1775487131.3863528-abc123"; + +function groupedFacesMock() { + return withGroupedTrainingAttempt(basicFacesMock(), { + eventId: GROUPED_EVENT_ID, + attempts: [ + { timestamp: 1775487131.3863528, label: "unknown", score: 0.95 }, + { timestamp: 1775487132.3863528, label: "unknown", score: 0.91 }, + ], + }); +} + +async function installGroupedFaces(app: FrigateApp) { + await app.api.install({ + events: [ + { + id: GROUPED_EVENT_ID, + label: "person", + sub_label: null, + camera: "front_door", + start_time: 1775487131.3863528, + end_time: 1775487161.3863528, + false_positive: false, + zones: ["front_yard"], + thumbnail: null, + has_clip: true, + has_snapshot: true, + retain_indefinitely: false, + plus_id: null, + model_hash: "abc123", + detector_type: "cpu", + model_type: "ssd", + data: { + top_score: 0.92, + score: 0.92, + region: [0.1, 0.1, 0.5, 0.8], + box: [0.2, 0.15, 0.45, 0.75], + area: 0.18, + ratio: 0.6, + type: "object", + path_data: [], + }, + }, + ], + faces: groupedFacesMock(), + }); +} + +async function openGroupedFaceDialog(app: FrigateApp): Promise { + await installGroupedFaces(app); + await app.goto("/faces"); + const groupedImage = app.page + .locator('img[src*="clips/faces/train/"]') + .first(); + const groupedCard = groupedImage.locator("xpath=.."); + await expect(groupedImage).toBeVisible({ timeout: 5_000 }); + await groupedCard.click(); + const dialog = app.page + .getByRole("dialog") + .filter({ has: app.page.locator('img[src*="clips/faces/train/"]') }) + .first(); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + await expect(dialog.locator('img[src*="clips/faces/train/"]')).toHaveCount(2); + return dialog; +} + +/** + * Opens the LibrarySelector dropdown (the single button at the top-left of + * the Face Library page) and returns the dropdown menu locator. + * + * The LibrarySelector is a single DropdownMenu whose trigger shows the + * current tab name + count (e.g. "Recent Recognitions (0)"). Named face + * collections (alice, bob, charlie) are items inside this dropdown. + */ +async function openLibraryDropdown(app: FrigateApp): Promise { + // The trigger is the first button on the page with a parenthesised count. + const trigger = app.page + .getByRole("button") + .filter({ hasText: /\(\d+\)/ }) + .first(); + await expect(trigger).toBeVisible({ timeout: 10_000 }); + await trigger.click(); + const menu = app.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + await expect(menu).toBeVisible({ timeout: 5_000 }); + return menu; +} + +test.describe("Face Library — collection selector @high", () => { + test("selector shows named face collections", async ({ frigateApp }) => { + await frigateApp.installDefaults({ faces: basicFacesMock() }); + await frigateApp.goto("/faces"); + // Named collections appear in the LibrarySelector dropdown. + const menu = await openLibraryDropdown(frigateApp); + await expect(menu.getByText(/alice/i).first()).toBeVisible({ + timeout: 5_000, + }); + }); + + test("empty state renders when no faces exist", async ({ frigateApp }) => { + await frigateApp.installDefaults({ faces: emptyFacesMock() }); + await frigateApp.goto("/faces"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + await expect( + frigateApp.page.locator('img[src*="/clips/faces/"]'), + ).toHaveCount(0); + }); + + test("tiles render for each named collection", async ({ frigateApp }) => { + await frigateApp.installDefaults({ faces: basicFacesMock() }); + await frigateApp.goto("/faces"); + // Open the dropdown — collections list shows "alice (2)" and "bob (1)". + const menu = await openLibraryDropdown(frigateApp); + await expect( + menu.locator('[role="menuitem"]').filter({ hasText: /alice/i }).first(), + ).toBeVisible({ timeout: 5_000 }); + await expect( + menu.locator('[role="menuitem"]').filter({ hasText: /bob/i }).first(), + ).toBeVisible(); + }); +}); + +test.describe("Face Library — delete flow (desktop) @high", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Delete action menu is desktop-focused", + ); + + test("deleting a collection fires POST /faces//delete", async ({ + frigateApp, + }) => { + let deleteUrl: string | null = null; + let deleteBody: unknown = null; + // Install base mocks first, then register our more-specific route AFTER + // so it takes priority over the ApiMocker catch-all (Playwright LIFO order). + await frigateApp.installDefaults({ faces: basicFacesMock() }); + await frigateApp.page.route( + /\/api\/faces\/[^/]+\/delete/, + async (route) => { + deleteUrl = route.request().url(); + deleteBody = route.request().postDataJSON(); + await route.fulfill({ json: { success: true } }); + }, + ); + await frigateApp.goto("/faces"); + + // Open the LibrarySelector dropdown and click the trash icon next + // to the alice row. The trash icon is a ghost-variant Button inside + // the DropdownMenuItem — it becomes visible on hover/focus. + const menu = await openLibraryDropdown(frigateApp); + const aliceRow = menu + .locator('[role="menuitem"]') + .filter({ hasText: /alice/i }) + .first(); + await expect(aliceRow).toBeVisible({ timeout: 5_000 }); + // Hover first to make hover-only opacity-0 buttons visible. + await aliceRow.hover(); + // The icon buttons have no aria-label or title. The row renders exactly + // two buttons in fixed source order: [0] LuPencil (rename), [1] LuTrash2 + // (delete). This order is determined by FaceLibrary.tsx and is stable. + const trashBtn = aliceRow.locator("button").nth(1); + await trashBtn.click(); + + // The delete confirmation is a Dialog (not AlertDialog) in this flow. + const dialog = frigateApp.page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + await dialog + .getByRole("button", { name: /delete/i }) + .first() + .click(); + + await expect + .poll(() => deleteUrl, { timeout: 5_000 }) + .toMatch(/\/faces\/alice\/delete/); + expect(deleteBody).toMatchObject({ ids: expect.any(Array) }); + }); +}); + +test.describe("Face Library — rename flow (desktop) @high", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Rename action menu is desktop-focused", + ); + + test("renaming a collection fires PUT /faces//rename", async ({ + frigateApp, + }) => { + let renameUrl: string | null = null; + let renameBody: unknown = null; + // Install base mocks first, then register our more-specific route AFTER + // so it takes priority over the ApiMocker catch-all (Playwright LIFO order). + await frigateApp.installDefaults({ faces: basicFacesMock() }); + await frigateApp.page.route( + /\/api\/faces\/[^/]+\/rename/, + async (route) => { + renameUrl = route.request().url(); + renameBody = route.request().postDataJSON(); + await route.fulfill({ json: { success: true } }); + }, + ); + await frigateApp.goto("/faces"); + + // Open the LibrarySelector dropdown and click the pencil (rename) icon + // next to alice. The icon is a ghost Button inside the DropdownMenuItem. + const menu = await openLibraryDropdown(frigateApp); + const aliceRow = menu + .locator('[role="menuitem"]') + .filter({ hasText: /alice/i }) + .first(); + await expect(aliceRow).toBeVisible({ timeout: 5_000 }); + await aliceRow.hover(); + // The icon buttons have no aria-label or title. The row renders exactly + // two buttons in fixed source order: [0] LuPencil (rename), [1] LuTrash2 + // (delete). This order is determined by FaceLibrary.tsx and is stable. + const pencilBtn = aliceRow.locator("button").nth(0); + await pencilBtn.click(); + + // TextEntryDialog — fill the input and confirm. + const dialog = frigateApp.page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + await dialog.locator("input").first().fill("alice_renamed"); + await dialog + .getByRole("button", { name: /save|rename|confirm/i }) + .first() + .click(); + + await expect + .poll(() => renameUrl, { timeout: 5_000 }) + .toMatch(/\/faces\/alice\/rename/); + expect(renameBody).toEqual({ new_name: "alice_renamed" }); + }); +}); + +test.describe("Face Library — upload flow @high", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Upload button has no accessible text on mobile — icon-only on narrow viewports", + ); + + test("Upload button opens the upload dialog", async ({ frigateApp }) => { + await frigateApp.installDefaults({ faces: basicFacesMock() }); + await frigateApp.goto("/faces"); + + // Navigate to the alice tab by opening the dropdown and clicking alice. + const menu = await openLibraryDropdown(frigateApp); + await menu + .locator('[role="menuitem"]') + .filter({ hasText: /alice/i }) + .first() + .click(); + + // After switching to alice, the Upload Image button appears in the toolbar. + const uploadBtn = frigateApp.page + .getByRole("button") + .filter({ hasText: /upload/i }) + .first(); + await expect(uploadBtn).toBeVisible({ timeout: 5_000 }); + await uploadBtn.click(); + + // UploadImageDialog renders a file input + confirm button. + const dialog = frigateApp.page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + await expect(dialog.locator('input[type="file"]')).toHaveCount(1); + }); +}); + +test.describe("FaceSelectionDialog @high", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Grouped dropdown flow is desktop-only", + ); + + test("reclassify dropdown selects a name and closes cleanly", async ({ + frigateApp, + }) => { + // Migrated from radix-overlay-regressions.spec.ts. + const dialog = await openGroupedFaceDialog(frigateApp); + const triggers = dialog.locator('[aria-haspopup="menu"]'); + await expect(triggers).toHaveCount(2); + + await triggers.first().click(); + const menu = frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + await expect(menu).toBeVisible({ timeout: 5_000 }); + + await menu.getByRole("menuitem", { name: /^bob$/i }).click(); + + await expect(menu).not.toBeVisible({ timeout: 3_000 }); + await expect(dialog).toBeVisible(); + + const tooltipVisible = await frigateApp.page + .locator('[role="tooltip"]') + .filter({ hasText: /train face/i }) + .isVisible() + .catch(() => false); + expect( + tooltipVisible, + "Train Face tooltip popped after dropdown closed — focus-restore regression", + ).toBe(false); + }); + + test("second dropdown open accepts typeahead keyboard input", async ({ + frigateApp, + }) => { + // Migrated from radix-overlay-regressions.spec.ts. + const dialog = await openGroupedFaceDialog(frigateApp); + const triggers = dialog.locator('[aria-haspopup="menu"]'); + await expect(triggers).toHaveCount(2); + + await triggers.first().click(); + let menu = frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + await expect(menu).toBeVisible({ timeout: 5_000 }); + await menu.getByRole("menuitem", { name: /^bob$/i }).click(); + await expect(menu).not.toBeVisible({ timeout: 3_000 }); + + await triggers.nth(1).click(); + menu = frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + await expect(menu).toBeVisible({ timeout: 5_000 }); + + await frigateApp.page.keyboard.press("c"); + await expect + .poll( + async () => + frigateApp.page.evaluate( + () => + document.activeElement?.textContent?.trim().toLowerCase() ?? "", + ), + { timeout: 2_000 }, + ) + .toMatch(/^charlie/); + + await frigateApp.page.keyboard.press("Escape"); + await expect(menu).not.toBeVisible({ timeout: 3_000 }); + }); + + test("classifying the last image in a group leaves body interactive", async ({ + frigateApp, + }) => { + // Regression guard for the stuck body pointer-events bug when the + // last image in a grouped-recognition detail Dialog is classified. + // Tracked upstream at radix-ui/primitives#3445. + // + // Root cause: when the user clicks a FaceSelectionDialog menu item, + // the modal DropdownMenu enters its exit animation (Radix's Presence + // keeps it in the DOM with data-state="closed" until animationend). + // While that is in flight the classify axios resolves, SWR removes + // the image from /api/faces, the parent's map no longer renders the + // grouped card, and React unmounts the subtree — including the still- + // animating DropdownMenu's Presence container. DismissableLayer's + // shared modal-layer stack can't reconcile the interrupted exit, so + // the `body { pointer-events: none }` entry it put on mount is never + // popped and the rest of the UI becomes unclickable. + // + // The fix is `modal={false}` on the FaceSelectionDialog's + // DropdownMenu (desktop path only). With modal=false the DropdownMenu + // never puts an entry on DismissableLayer's body-pointer-events stack + // in the first place, so there's nothing to leak when its Presence is + // torn down mid-animation. The Radix-community-documented workaround + // for #3445. + // + // The bug only reproduces when the mock resolves fast enough that + // the parent unmounts before the dropdown's exit animation finishes. + // Measured window via a 3x sweep on the pre-fix build: 0–200 ms + // triggers it; 300 ms+ no longer reproduces. Production LAN networks + // sit comfortably inside the bad window, while `npm run dev` seems + // to mask it via React StrictMode's double-effect scheduling. + const EVENT_ID = "1775487131.3863528-race"; + const initialFaces = withGroupedTrainingAttempt(basicFacesMock(), { + eventId: EVENT_ID, + attempts: [ + { timestamp: 1775487131.3863528, label: "unknown", score: 0.95 }, + ], + }); + + let classified = false; + + await frigateApp.installDefaults({ + faces: initialFaces, + events: [ + { + id: EVENT_ID, + label: "person", + sub_label: null, + camera: "front_door", + start_time: 1775487131.3863528, + end_time: 1775487161.3863528, + false_positive: false, + zones: ["front_yard"], + thumbnail: null, + has_clip: true, + has_snapshot: true, + retain_indefinitely: false, + plus_id: null, + model_hash: "abc123", + detector_type: "cpu", + model_type: "ssd", + data: { + top_score: 0.92, + score: 0.92, + region: [0.1, 0.1, 0.5, 0.8], + box: [0.2, 0.15, 0.45, 0.75], + area: 0.18, + ratio: 0.6, + type: "object", + path_data: [], + }, + }, + ], + }); + + // Re-route /api/faces to flip to the "train empty" payload once the + // classify POST has been received. Registered AFTER installDefaults so + // Playwright's LIFO route matching hits this handler first. + await frigateApp.page.route("**/api/faces", async (route) => { + const payload = classified ? basicFacesMock() : initialFaces; + await route.fulfill({ json: payload }); + }); + + // Hold the classify POST briefly. The race opens when the parent + // unmounts before the dropdown's exit animation finishes (~200ms + // in Radix). 100ms keeps us comfortably inside that window and + // reliably triggered the bug in a 3x sweep across 0/50/100/200ms + // on the pre-fix build. CLASSIFY_DELAY_MS overrides for local sweeps. + const delayMs = Number( + (globalThis as { process?: { env?: Record } }).process + ?.env?.CLASSIFY_DELAY_MS ?? "100", + ); + await frigateApp.page.route( + "**/api/faces/train/*/classify", + async (route) => { + classified = true; + if (delayMs > 0) { + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + await route.fulfill({ json: { success: true } }); + }, + ); + + await frigateApp.goto("/faces"); + + // Open the grouped detail Dialog. + const groupedImage = frigateApp.page + .locator('img[src*="clips/faces/train/"]') + .first(); + await expect(groupedImage).toBeVisible({ timeout: 5_000 }); + await groupedImage.locator("xpath=..").click(); + const dialog = frigateApp.page + .getByRole("dialog") + .filter({ + has: frigateApp.page.locator('img[src*="clips/faces/train/"]'), + }) + .first(); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + + // Single attempt → single `+` trigger. + const triggers = dialog.locator('[aria-haspopup="menu"]'); + await expect(triggers).toHaveCount(1); + await triggers.first().click(); + + const menu = frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + await expect(menu).toBeVisible({ timeout: 5_000 }); + await menu.getByRole("menuitem", { name: /^alice$/i }).click(); + + // The Dialog must leave the tree cleanly, and body must recover. + await expect(dialog).not.toBeVisible({ timeout: 5_000 }); + + // Give Radix's exit animation + cleanup a comfortable margin on top of + // the ~300ms simulated network delay. + await waitForBodyInteractive(frigateApp.page, 5_000); + await expectBodyInteractive(frigateApp.page); + + // User-visible confirmation: click something outside the dialog + // and assert it actually responds. + const librarySelector = frigateApp.page + .getByRole("button") + .filter({ hasText: /\(\d+\)/ }) + .first(); + await librarySelector.click(); + await expect( + frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(), + ).toBeVisible({ timeout: 3_000 }); + }); +}); + +test.describe("Face Library — mobile @high @mobile", () => { + test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only"); + + test("mobile library selector dropdown closes cleanly on Escape", async ({ + frigateApp, + }) => { + // Migrated from radix-overlay-regressions.spec.ts. + await installGroupedFaces(frigateApp); + await frigateApp.goto("/faces"); + + const selector = frigateApp.page + .getByRole("button") + .filter({ hasText: /\(\d+\)/ }) + .first(); + await expect(selector).toBeVisible({ timeout: 5_000 }); + await selector.click(); + + const menu = frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + await expect(menu).toBeVisible({ timeout: 3_000 }); + + await frigateApp.page.keyboard.press("Escape"); + await expect(menu).not.toBeVisible({ timeout: 3_000 }); + await waitForBodyInteractive(frigateApp.page); + await expectBodyInteractive(frigateApp.page); + }); +}); diff --git a/web/e2e/specs/live.spec.ts b/web/e2e/specs/live.spec.ts new file mode 100644 index 0000000..af28f00 --- /dev/null +++ b/web/e2e/specs/live.spec.ts @@ -0,0 +1,289 @@ +/** + * Live page tests -- CRITICAL tier. + * + * Dashboard grid, single-camera controls, feature toggles (with WS + * frame assertions), context menu, birdseye, and mobile layout. + * Also absorbs the PTZ preset-dropdown regression tests from the + * now-deleted ptz-overlay.spec.ts. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { LivePage } from "../pages/live.page"; +import { installWsFrameCapture, waitForWsFrame } from "../helpers/ws-frames"; +import { + expectBodyInteractive, + waitForBodyInteractive, +} from "../helpers/overlay-interaction"; + +const PTZ_CAMERA = "front_door"; +const PRESET_NAMES = ["home", "driveway", "front_porch"]; + +test.describe("Live Dashboard @critical", () => { + test("every configured camera renders on the dashboard", async ({ + frigateApp, + }) => { + await frigateApp.goto("/"); + const live = new LivePage(frigateApp.page, !frigateApp.isMobile); + for (const cam of ["front_door", "backyard", "garage"]) { + await expect(live.cameraCard(cam)).toBeVisible({ timeout: 10_000 }); + } + }); + + test("clicking a camera card opens the single-camera view via hash", async ({ + frigateApp, + }) => { + await frigateApp.goto("/"); + const live = new LivePage(frigateApp.page, !frigateApp.isMobile); + await live.cameraCard("front_door").first().click({ timeout: 10_000 }); + await expect(frigateApp.page).toHaveURL(/#front_door/); + }); + + test("birdseye route renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/#birdseye"); + await expect(frigateApp.page.locator("body")).toBeVisible(); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("empty group shows fallback content", async ({ frigateApp }) => { + await frigateApp.page.goto("/?group=nonexistent"); + await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); +}); + +test.describe("Live Single Camera — desktop controls @critical", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Desktop-only header controls", + ); + + test("single-camera view shows Back and History buttons", async ({ + frigateApp, + }) => { + await frigateApp.goto("/#front_door"); + const live = new LivePage(frigateApp.page, true); + await expect(live.backButton).toBeVisible({ timeout: 5_000 }); + await expect(live.historyButton).toBeVisible(); + }); + + test("feature toggles render (at least 3)", async ({ frigateApp }) => { + await frigateApp.goto("/#front_door"); + const live = new LivePage(frigateApp.page, true); + // Wait for the single-camera header to render before counting toggles. + await expect(live.backButton).toBeVisible({ timeout: 5_000 }); + await expect(live.featureToggles.first()).toBeVisible({ timeout: 5_000 }); + const count = await live.featureToggles.count(); + expect(count).toBeGreaterThanOrEqual(3); + }); + + test("clicking a feature toggle sends the matching WS frame", async ({ + frigateApp, + }) => { + await installWsFrameCapture(frigateApp.page); + await frigateApp.goto("/#front_door"); + const live = new LivePage(frigateApp.page, true); + // Wait for feature toggles to render (WS camera_activity must arrive first). + await expect(live.activeFeatureToggles.first()).toBeVisible({ + timeout: 5_000, + }); + const activeBefore = await live.activeFeatureToggles.count(); + expect(activeBefore).toBeGreaterThan(0); + + await live.activeFeatureToggles.first().click(); + + // The toggle dispatches a frame on //set — match on + // front_door/ prefix + /set suffix (any feature). + await waitForWsFrame( + frigateApp.page, + (frame) => frame.includes("front_door/") && frame.includes("/set"), + { + message: + "feature toggle should dispatch a //set frame", + }, + ); + }); + + test("keyboard shortcut f does not crash", async ({ frigateApp }) => { + await frigateApp.goto("/"); + await frigateApp.page.keyboard.press("f"); + await expect(frigateApp.page.locator("body")).toBeVisible(); + // Note: headless Chromium rejects fullscreen requests without a user + // gesture, so document.fullscreenElement cannot be asserted reliably + // in e2e. We assert the keypress doesn't crash the app; real + // fullscreen behavior is covered by manual testing. + }); + + test("settings gear opens a dropdown with Stream/Play menu items", async ({ + frigateApp, + }) => { + await frigateApp.goto("/#front_door"); + // Wait for the single-camera view to render — use the Back button + // as a deterministic marker. + const live = new LivePage(frigateApp.page, true); + await expect(live.backButton).toBeVisible({ timeout: 10_000 }); + + // The gear icon button is the last button-like element in the + // single-camera header. Clicking it opens a Radix dropdown. + const gearButtons = frigateApp.page.locator("button:has(svg)"); + const count = await gearButtons.count(); + expect(count).toBeGreaterThan(0); + await gearButtons.last().click(); + + const menu = frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + await expect(menu).toBeVisible({ timeout: 3_000 }); + await frigateApp.page.keyboard.press("Escape"); + await expect(menu).not.toBeVisible({ timeout: 3_000 }); + }); +}); + +test.describe("Live Context Menu (desktop) @critical", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Right-click is desktop-only", + ); + + test("right-click opens the context menu", async ({ frigateApp }) => { + await frigateApp.goto("/"); + const live = new LivePage(frigateApp.page, true); + const menu = await live.openContextMenuOn("front_door"); + await expect(menu).toBeVisible({ timeout: 5_000 }); + }); + + test("context menu closes on Escape and leaves body interactive", async ({ + frigateApp, + }) => { + await frigateApp.goto("/"); + const live = new LivePage(frigateApp.page, true); + const menu = await live.openContextMenuOn("front_door"); + await expect(menu).toBeVisible({ timeout: 5_000 }); + await frigateApp.page.keyboard.press("Escape"); + await expect(menu).not.toBeVisible(); + await waitForBodyInteractive(frigateApp.page); + await expectBodyInteractive(frigateApp.page); + }); +}); + +test.describe("Live PTZ preset dropdown @critical", () => { + // Migrated from ptz-overlay.spec.ts. Guards: + // 1. After selecting a preset, the "Presets" tooltip must not re-pop. + // 2. Keyboard shortcuts after close should not re-open the dropdown. + + test("selecting a preset closes menu cleanly and does not re-open on keyboard", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "PTZ preset dropdown is desktop-only"); + + await frigateApp.api.install({ + config: { + cameras: { + [PTZ_CAMERA]: { onvif: { host: "10.0.0.50" } }, + }, + }, + }); + await frigateApp.page.route(`**/api/${PTZ_CAMERA}/ptz/info`, (route) => + route.fulfill({ + json: { + name: PTZ_CAMERA, + features: ["pt", "zoom"], + presets: PRESET_NAMES, + profiles: [], + }, + }), + ); + + await installWsFrameCapture(frigateApp.page); + await frigateApp.goto(`/#${PTZ_CAMERA}`); + + const presetTrigger = frigateApp.page.getByRole("button", { + name: /presets/i, + }); + await expect(presetTrigger.first()).toBeVisible({ timeout: 5_000 }); + await presetTrigger.first().click(); + + const menu = frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + await expect(menu).toBeVisible({ timeout: 3_000 }); + + await menu.getByRole("menuitem", { name: PRESET_NAMES[0] }).first().click(); + await expect(menu).not.toBeVisible({ timeout: 3_000 }); + + await waitForWsFrame( + frigateApp.page, + (frame) => + frame.includes(`"${PTZ_CAMERA}/ptz"`) && + frame.includes(`preset_${PRESET_NAMES[0]}`), + ); + + await waitForBodyInteractive(frigateApp.page); + await expectBodyInteractive(frigateApp.page); + + await expect + .poll( + async () => + frigateApp.page + .locator('[role="tooltip"]') + .filter({ hasText: /presets/i }) + .isVisible() + .catch(() => false), + { timeout: 1_000 }, + ) + .toBe(false); + + await frigateApp.page.keyboard.press("ArrowUp"); + await frigateApp.page.keyboard.press("Space"); + await frigateApp.page.keyboard.press("Enter"); + await expect + .poll(() => menu.isVisible().catch(() => false), { timeout: 1_000 }) + .toBe(false); + }); +}); + +test.describe("Live mobile layout @critical @mobile", () => { + test("mobile dashboard has no sidebar and renders cameras", async ({ + frigateApp, + }) => { + test.skip(!frigateApp.isMobile, "Mobile-only"); + await frigateApp.goto("/"); + await expect(frigateApp.page.locator("aside")).toHaveCount(0); + const live = new LivePage(frigateApp.page, false); + await expect(live.cameraCard("front_door")).toBeVisible({ + timeout: 10_000, + }); + }); + + test("mobile camera tap opens single view", async ({ frigateApp }) => { + test.skip(!frigateApp.isMobile, "Mobile-only"); + await frigateApp.goto("/"); + const live = new LivePage(frigateApp.page, false); + await live.cameraCard("front_door").first().click({ timeout: 10_000 }); + await expect(frigateApp.page).toHaveURL(/#front_door/); + }); + + test("mobile onvif single-camera view loads without freezing body", async ({ + frigateApp, + }) => { + test.skip(!frigateApp.isMobile, "Mobile-only"); + // Migrated from ptz-overlay.spec.ts — dismissable-layer dedupe smoke test. + await frigateApp.api.install({ + config: { + cameras: { [PTZ_CAMERA]: { onvif: { host: "10.0.0.50" } } }, + }, + }); + await frigateApp.page.route(`**/api/${PTZ_CAMERA}/ptz/info`, (route) => + route.fulfill({ + json: { + name: PTZ_CAMERA, + features: ["pt", "zoom"], + presets: PRESET_NAMES, + profiles: [], + }, + }), + ); + await frigateApp.goto(`/#${PTZ_CAMERA}`); + await expectBodyInteractive(frigateApp.page); + await expect(frigateApp.page.locator("body")).toBeVisible(); + }); +}); diff --git a/web/e2e/specs/logs.spec.ts b/web/e2e/specs/logs.spec.ts new file mode 100644 index 0000000..0b74acf --- /dev/null +++ b/web/e2e/specs/logs.spec.ts @@ -0,0 +1,222 @@ +/** + * Logs page tests -- MEDIUM tier. + * + * Service tabs (with real /logs/ JSON contract), + * log content render, Copy (clipboard), Download (assert + * ?download=true request fired), mobile tab selector. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { grantClipboardPermissions, readClipboard } from "../helpers/clipboard"; + +function logsJsonBody(lines: string[]) { + return { lines, totalLines: lines.length }; +} + +test.describe("Logs — service tabs @medium", () => { + test("frigate tab renders by default with mocked log lines", async ({ + frigateApp, + }) => { + await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => + route.fulfill({ + json: logsJsonBody([ + "[2026-04-06 10:00:00] INFO: Frigate started", + "[2026-04-06 10:00:01] INFO: Cameras loaded", + ]), + }), + ); + // Silence the streaming fetch so it doesn't hang the test. + await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) => + route.fulfill({ status: 200, body: "" }), + ); + await frigateApp.goto("/logs"); + await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({ + timeout: 5_000, + }); + await expect(frigateApp.page.getByText(/Frigate started/)).toBeVisible({ + timeout: 10_000, + }); + }); + + test("switching to go2rtc fires a GET to /logs/go2rtc", async ({ + frigateApp, + }) => { + let go2rtcCalled = false; + await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => + route.fulfill({ json: logsJsonBody(["frigate line"]) }), + ); + await frigateApp.page.route(/\/api\/logs\/go2rtc(\?|$)/, (route) => { + if (!route.request().url().includes("stream=true")) { + go2rtcCalled = true; + } + return route.fulfill({ json: logsJsonBody(["go2rtc line"]) }); + }); + await frigateApp.page.route(/\/api\/logs\/.*\?stream=true/, (route) => + route.fulfill({ status: 200, body: "" }), + ); + + await frigateApp.goto("/logs"); + await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({ + timeout: 5_000, + }); + const go2rtcTab = frigateApp.page.getByLabel("Select go2rtc"); + await expect(go2rtcTab).toBeVisible(); + await go2rtcTab.click(); + await expect.poll(() => go2rtcCalled, { timeout: 5_000 }).toBe(true); + await expect(go2rtcTab).toHaveAttribute("data-state", "on"); + }); +}); + +test.describe("Logs — actions @medium", () => { + test("Copy button writes current logs to clipboard", async ({ + frigateApp, + context, + }) => { + await grantClipboardPermissions(context); + await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => + route.fulfill({ + json: logsJsonBody([ + "[2026-04-06 10:00:00] INFO: Frigate started", + "[2026-04-06 10:00:01] INFO: Cameras loaded", + ]), + }), + ); + await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) => + route.fulfill({ status: 200, body: "" }), + ); + await frigateApp.goto("/logs"); + await expect(frigateApp.page.getByText(/Frigate started/)).toBeVisible({ + timeout: 10_000, + }); + + const copyBtn = frigateApp.page.getByLabel("Copy to Clipboard"); + await expect(copyBtn).toBeVisible({ timeout: 5_000 }); + await copyBtn.click(); + await expect + .poll(() => readClipboard(frigateApp.page), { timeout: 5_000 }) + .toContain("Frigate started"); + }); + + test("Download button fires GET /logs/?download=true", async ({ + frigateApp, + }) => { + let downloadCalled = false; + await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => { + if (route.request().url().includes("download=true")) { + downloadCalled = true; + } + return route.fulfill({ json: logsJsonBody(["frigate line"]) }); + }); + await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) => + route.fulfill({ status: 200, body: "" }), + ); + + await frigateApp.goto("/logs"); + const downloadBtn = frigateApp.page.getByLabel("Download Logs"); + await expect(downloadBtn).toBeVisible({ timeout: 5_000 }); + await downloadBtn.click(); + await expect.poll(() => downloadCalled, { timeout: 5_000 }).toBe(true); + }); +}); + +test.describe("Logs — websocket tab @medium", () => { + test("switching to websocket tab renders WsMessageFeed container", async ({ + frigateApp, + }) => { + await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => + route.fulfill({ json: logsJsonBody(["frigate line"]) }), + ); + await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) => + route.fulfill({ status: 200, body: "" }), + ); + await frigateApp.goto("/logs"); + const wsTab = frigateApp.page.getByLabel("Select websocket"); + await expect(wsTab).toBeVisible({ timeout: 5_000 }); + await wsTab.click(); + await expect(wsTab).toHaveAttribute("data-state", "on", { timeout: 5_000 }); + }); +}); + +test.describe("Logs — streaming @medium", () => { + test("streamed log lines appear in the viewport", async ({ frigateApp }) => { + await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => { + if (route.request().url().includes("stream=true")) { + // Intercepted below via addInitScript fetch override. + return route.fallback(); + } + return route.fulfill({ + json: logsJsonBody(["[2026-04-06 10:00:00] INFO: initial batch line"]), + }); + }); + + // Override window.fetch so the /api/logs/frigate?stream=true request + // resolves with a real ReadableStream that emits chunks over time. + // This is the only way to validate streaming-append behavior through + // Playwright — route.fulfill() cannot return a stream. + // NOTE: The app calls fetch('api/logs/...') with a relative URL (no + // leading slash), so we match both relative and absolute forms. + await frigateApp.page.addInitScript(() => { + const origFetch = window.fetch; + window.fetch = async (input, init) => { + const url = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : (input as Request).url; + if (url.includes("api/logs/frigate") && url.includes("stream=true")) { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + async start(controller) { + await new Promise((r) => setTimeout(r, 30)); + controller.enqueue( + encoder.encode( + "[2026-04-06 10:00:02] INFO: streamed line one\n", + ), + ); + await new Promise((r) => setTimeout(r, 30)); + controller.enqueue( + encoder.encode( + "[2026-04-06 10:00:03] INFO: streamed line two\n", + ), + ); + controller.close(); + }, + }); + return new Response(stream, { status: 200 }); + } + return origFetch.call(window, input as RequestInfo, init); + }; + }); + + await frigateApp.goto("/logs"); + // The initial batch line is parsed by LogLineData and its content is + // rendered in a .log-content cell — assert against that element. + await expect(frigateApp.page.getByText("initial batch line")).toBeVisible({ + timeout: 10_000, + }); + await expect(frigateApp.page.getByText(/streamed line one/)).toBeVisible({ + timeout: 10_000, + }); + await expect(frigateApp.page.getByText(/streamed line two/)).toBeVisible({ + timeout: 10_000, + }); + }); +}); + +test.describe("Logs — mobile @medium @mobile", () => { + test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only"); + + test("service tabs render at mobile viewport", async ({ frigateApp }) => { + await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => + route.fulfill({ json: logsJsonBody(["frigate line"]) }), + ); + await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) => + route.fulfill({ status: 200, body: "" }), + ); + await frigateApp.goto("/logs"); + await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({ + timeout: 5_000, + }); + }); +}); diff --git a/web/e2e/specs/navigation.spec.ts b/web/e2e/specs/navigation.spec.ts new file mode 100644 index 0000000..e14887c --- /dev/null +++ b/web/e2e/specs/navigation.spec.ts @@ -0,0 +1,175 @@ +/** + * Navigation tests -- CRITICAL tier. + * + * Covers sidebar (desktop) / bottombar (mobile) link set, conditional + * nav items (faces, chat, classification), settings menu navigation, + * unknown-route redirect to /, and mobile-specific nav behaviors. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { BasePage } from "../pages/base.page"; + +const PRIMARY_ROUTES = ["/review", "/explore", "/export"] as const; + +test.describe("Navigation — primary links @critical", () => { + test("every primary link is visible and navigates", async ({ + frigateApp, + }) => { + await frigateApp.goto("/"); + for (const route of PRIMARY_ROUTES) { + await expect( + frigateApp.page.locator(`a[href="${route}"]`).first(), + ).toBeVisible(); + } + const base = new BasePage(frigateApp.page, !frigateApp.isMobile); + for (const route of PRIMARY_ROUTES) { + await base.navigateTo(route); + await expect(frigateApp.page).toHaveURL(new RegExp(route)); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + } + }); + + test("logo links home on desktop", async ({ frigateApp }) => { + test.skip(frigateApp.isMobile, "Sidebar logo is desktop-only"); + await frigateApp.goto("/review"); + await frigateApp.page.locator("aside a[href='/']").first().click(); + await expect(frigateApp.page).toHaveURL(/\/$/); + }); + + test("unknown route redirects to /", async ({ frigateApp }) => { + await frigateApp.page.goto("/nonexistent-route"); + await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + await expect(frigateApp.page).toHaveURL(/\/$/); + await expect( + frigateApp.page.locator("[data-camera='front_door']"), + ).toBeVisible({ timeout: 10_000 }); + }); +}); + +test.describe("Navigation — conditional items @critical", () => { + test("/faces is hidden when face_recognition.enabled is false", async ({ + frigateApp, + }) => { + await frigateApp.goto("/"); + await expect( + frigateApp.page.locator('a[href="/faces"]').first(), + ).toHaveCount(0); + }); + + test("/faces is visible when face_recognition.enabled is true (desktop)", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop sidebar"); + await frigateApp.installDefaults({ + config: { face_recognition: { enabled: true } }, + }); + await frigateApp.goto("/"); + await expect( + frigateApp.page.locator('a[href="/faces"]').first(), + ).toBeVisible(); + }); + + test("/chat is hidden when no agent has the chat role (desktop)", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop sidebar"); + await frigateApp.installDefaults({ + config: { + genai: { + descriptions_only: { + provider: "ollama", + model: "llava", + roles: ["descriptions"], + }, + }, + }, + }); + await frigateApp.goto("/"); + await expect( + frigateApp.page.locator('a[href="/chat"]').first(), + ).toHaveCount(0); + }); + + test("/chat is visible when an agent has the chat role (desktop)", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop sidebar"); + await frigateApp.installDefaults({ + config: { + genai: { + chat_agent: { + provider: "ollama", + model: "llava", + roles: ["chat"], + }, + }, + }, + }); + await frigateApp.goto("/"); + await expect( + frigateApp.page.locator('a[href="/chat"]').first(), + ).toBeVisible(); + }); + + test("/classification is visible for admin on desktop", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop sidebar"); + await frigateApp.goto("/"); + await expect( + frigateApp.page.locator('a[href="/classification"]').first(), + ).toBeVisible(); + }); +}); + +test.describe("Navigation — settings menu (desktop) @critical", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Sidebar settings menu is desktop-only", + ); + + const TARGETS = [ + { label: "Settings", url: /\/settings/ }, + { label: "System metrics", url: /\/system/ }, + { label: "System logs", url: /\/logs/ }, + { label: "Configuration Editor", url: /\/config/ }, + ]; + + for (const target of TARGETS) { + test(`menu → ${target.label} navigates`, async ({ frigateApp }) => { + await frigateApp.goto("/"); + const gear = frigateApp.page + .locator("aside .mb-8 div[class*='cursor-pointer']") + .first(); + await gear.click(); + await frigateApp.page.getByLabel(target.label).click(); + await expect(frigateApp.page).toHaveURL(target.url); + }); + } +}); + +test.describe("Navigation — mobile @critical @mobile", () => { + test("mobile bottombar visible, sidebar not rendered", async ({ + frigateApp, + }) => { + test.skip(!frigateApp.isMobile, "Mobile-only"); + await frigateApp.goto("/"); + await expect(frigateApp.page.locator("aside")).toHaveCount(0); + for (const route of PRIMARY_ROUTES) { + await expect( + frigateApp.page.locator(`a[href="${route}"]`).first(), + ).toBeVisible(); + } + }); + + test("mobile nav survives route change", async ({ frigateApp }) => { + test.skip(!frigateApp.isMobile, "Mobile-only"); + await frigateApp.goto("/"); + const reviewLink = frigateApp.page.locator('a[href="/review"]').first(); + await reviewLink.click(); + await expect(frigateApp.page).toHaveURL(/\/review/); + await expect( + frigateApp.page.locator('a[href="/review"]').first(), + ).toBeVisible(); + }); +}); diff --git a/web/e2e/specs/replay.spec.ts b/web/e2e/specs/replay.spec.ts new file mode 100644 index 0000000..51a4273 --- /dev/null +++ b/web/e2e/specs/replay.spec.ts @@ -0,0 +1,310 @@ +/** + * Replay page tests -- MEDIUM tier. + * + * /replay is the admin debug replay page (not a recordings player). + * Polls /api/debug_replay/status, renders a no-session state when + * inactive, and a live camera image + debug toggles + Stop controls + * when active. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { + activeSessionStatus, + noSessionStatus, +} from "../fixtures/mock-data/debug-replay"; + +async function installStatusRoute( + app: { page: import("@playwright/test").Page }, + body: unknown, +) { + await app.page.route("**/api/debug_replay/status", (route) => + route.fulfill({ json: body }), + ); +} + +test.describe("Replay — no active session @medium", () => { + test("empty state renders heading + Go to History button", async ({ + frigateApp, + }) => { + await installStatusRoute(frigateApp, noSessionStatus()); + await frigateApp.goto("/replay"); + await expect( + frigateApp.page.getByRole("heading", { + level: 2, + name: /No Active Debug Replay Session/i, + }), + ).toBeVisible({ timeout: 10_000 }); + const goButton = frigateApp.page.getByRole("button", { + name: /Go to History|Go to Recordings/i, + }); + await expect(goButton).toBeVisible(); + }); + + test("clicking Go to History navigates to /review", async ({ + frigateApp, + }) => { + await installStatusRoute(frigateApp, noSessionStatus()); + await frigateApp.goto("/replay"); + await expect( + frigateApp.page.getByRole("heading", { + level: 2, + name: /No Active Debug Replay Session/i, + }), + ).toBeVisible({ timeout: 10_000 }); + await frigateApp.page + .getByRole("button", { name: /Go to History|Go to Recordings/i }) + .click(); + await expect(frigateApp.page).toHaveURL(/\/review/); + }); +}); + +test.describe("Replay — active session @medium", () => { + test("active status renders the Debug Replay side panel", async ({ + frigateApp, + }) => { + await installStatusRoute(frigateApp, activeSessionStatus()); + await frigateApp.goto("/replay"); + await expect( + frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }), + ).toBeVisible({ timeout: 10_000 }); + // Three tabs (Debug / Objects / Messages) in TabsList + await expect(frigateApp.page.locator('[role="tab"]')).toHaveCount(3); + }); + + test("debug toggles render with bbox ON by default", async ({ + frigateApp, + }) => { + await installStatusRoute(frigateApp, activeSessionStatus()); + await frigateApp.goto("/replay"); + const bbox = frigateApp.page.locator("#debug-bbox"); + await expect(bbox).toBeVisible({ timeout: 10_000 }); + await expect(bbox).toHaveAttribute("aria-checked", "true"); + }); + + test("clicking bbox toggle flips aria-checked", async ({ frigateApp }) => { + await installStatusRoute(frigateApp, activeSessionStatus()); + await frigateApp.goto("/replay"); + const bbox = frigateApp.page.locator("#debug-bbox"); + await expect(bbox).toBeVisible({ timeout: 10_000 }); + await expect(bbox).toHaveAttribute("aria-checked", "true"); + await bbox.click(); + await expect(bbox).toHaveAttribute("aria-checked", "false"); + }); + + test("Configuration button opens the configuration dialog (desktop)", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop: button has visible text label"); + await installStatusRoute(frigateApp, activeSessionStatus()); + await frigateApp.goto("/replay"); + await expect( + frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }), + ).toBeVisible({ timeout: 10_000 }); + + // On desktop the span is visible and gives the button an accessible name. + await frigateApp.page + .getByRole("button", { name: /configuration/i }) + .first() + .click(); + + const dialog = frigateApp.page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + }); + + test("Configuration button opens the configuration dialog (mobile)", async ({ + frigateApp, + }) => { + test.skip(!frigateApp.isMobile, "Mobile: button is icon-only"); + await installStatusRoute(frigateApp, activeSessionStatus()); + await frigateApp.goto("/replay"); + await expect( + frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }), + ).toBeVisible({ timeout: 10_000 }); + + // On mobile the Configuration button text span is hidden (md:inline). + // It is the first button inside the right-side action group div + // (the flex container that holds Config + Stop, sibling of the Back button). + const actionGroup = frigateApp.page.locator( + ".flex.items-center.gap-2 button", + ); + await actionGroup.first().click(); + + // On mobile PlatformAwareSheet renders a MobilePage (full-screen panel) + // instead of a Radix Dialog, so assert the panel title heading is visible. + await expect( + frigateApp.page.getByRole("heading", { + level: 2, + name: /^Configuration$/i, + }), + ).toBeVisible({ timeout: 5_000 }); + }); + + test("Objects tab renders with the camera_activity objects list", async ({ + frigateApp, + }) => { + await installStatusRoute(frigateApp, activeSessionStatus()); + await frigateApp.goto("/replay"); + + await expect( + frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }), + ).toBeVisible({ timeout: 10_000 }); + + // Send an activity payload with a person object on front_door. + // Must be called after goto() so the WS connection is established. + await frigateApp.ws.sendCameraActivity({ + front_door: { + objects: [ + { + label: "person", + score: 0.95, + box: [0.1, 0.1, 0.5, 0.8], + area: 0.2, + ratio: 0.6, + region: [0.05, 0.05, 0.6, 0.85], + current_zones: [], + id: "obj-person-1", + }, + ], + }, + }); + + // Switch to Objects tab (labelled "Object List" in i18n). + const objectsTab = frigateApp.page.getByRole("tab", { + name: /object/i, + }); + await objectsTab.click(); + await expect(objectsTab).toHaveAttribute("data-state", "active", { + timeout: 3_000, + }); + + // The object row renders the label. + await expect(frigateApp.page.getByText(/person/i).first()).toBeVisible({ + timeout: 5_000, + }); + }); + + test("Messages tab renders WsMessageFeed container", async ({ + frigateApp, + }) => { + await installStatusRoute(frigateApp, activeSessionStatus()); + await frigateApp.goto("/replay"); + await expect( + frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }), + ).toBeVisible({ timeout: 10_000 }); + + const messagesTab = frigateApp.page.getByRole("tab", { + name: /messages/i, + }); + await messagesTab.click(); + await expect(messagesTab).toHaveAttribute("data-state", "active", { + timeout: 3_000, + }); + }); + + test("bbox info popover opens and closes cleanly", async ({ frigateApp }) => { + await installStatusRoute(frigateApp, activeSessionStatus()); + await frigateApp.goto("/replay"); + // The bbox row has an info icon popover trigger next to its label. + // The trigger is a div (not button) wrapping LuInfo with an sr-only + // "Info" span. Target it by the sr-only text content. + const infoTrigger = frigateApp.page + .locator("span.sr-only", { hasText: /info/i }) + .first(); + await expect(infoTrigger).toBeVisible({ timeout: 10_000 }); + // Click the parent div (the actual trigger) + await infoTrigger.locator("..").click(); + + const popover = frigateApp.page.locator( + "[data-radix-popper-content-wrapper]", + ); + await expect(popover.first()).toBeVisible({ timeout: 3_000 }); + await frigateApp.page.keyboard.press("Escape"); + await expect(popover.first()).not.toBeVisible({ timeout: 3_000 }); + }); +}); + +test.describe("Replay — stop flow (desktop) @medium", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Desktop button has accessible 'Stop Replay' name", + ); + + test("Stop Replay opens confirm dialog; confirm POSTs debug_replay/stop", async ({ + frigateApp, + }) => { + await installStatusRoute(frigateApp, activeSessionStatus()); + let stopCalled = false; + await frigateApp.page.route("**/api/debug_replay/stop", async (route) => { + if (route.request().method() === "POST") stopCalled = true; + await route.fulfill({ json: { success: true } }); + }); + + await frigateApp.goto("/replay"); + await expect( + frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }), + ).toBeVisible({ timeout: 10_000 }); + + await frigateApp.page + .getByRole("button", { name: /stop replay/i }) + .first() + .click(); + + const dialog = frigateApp.page.getByRole("alertdialog"); + await expect(dialog).toBeVisible({ timeout: 3_000 }); + await dialog + .getByRole("button", { name: /stop|confirm/i }) + .first() + .click(); + await expect.poll(() => stopCalled, { timeout: 5_000 }).toBe(true); + }); +}); + +test.describe("Replay — stop button (mobile) @medium @mobile", () => { + test.skip( + ({ frigateApp }) => !frigateApp.isMobile, + "Mobile-only icon-button variant", + ); + + test("tapping the icon-only stop button opens the confirm dialog", async ({ + frigateApp, + }) => { + await installStatusRoute(frigateApp, activeSessionStatus()); + await frigateApp.goto("/replay"); + await expect( + frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }), + ).toBeVisible({ timeout: 10_000 }); + + // On mobile the Stop button is an icon (LuSquare) inside an + // AlertDialogTrigger. It's the last button in the top bar's + // right-side action group (Back is on the left). Target by + // position within the top-bar flex container. + const topRightButtons = frigateApp.page + .locator(".min-h-12 button, .md\\:min-h-16 button") + .filter({ hasNot: frigateApp.page.getByLabel("Back") }); + const lastButton = topRightButtons.last(); + await expect(lastButton).toBeVisible({ timeout: 10_000 }); + await lastButton.click(); + + const dialog = frigateApp.page.getByRole("alertdialog"); + await expect(dialog).toBeVisible({ timeout: 3_000 }); + await dialog.getByRole("button", { name: /cancel/i }).click(); + await expect(dialog).not.toBeVisible({ timeout: 3_000 }); + }); +}); + +test.describe("Replay — mobile @medium @mobile", () => { + test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only"); + + test("no-session state renders at mobile viewport", async ({ + frigateApp, + }) => { + await installStatusRoute(frigateApp, noSessionStatus()); + await frigateApp.goto("/replay"); + await expect( + frigateApp.page.getByRole("heading", { + level: 2, + name: /No Active Debug Replay Session/i, + }), + ).toBeVisible({ timeout: 10_000 }); + }); +}); diff --git a/web/e2e/specs/review.spec.ts b/web/e2e/specs/review.spec.ts new file mode 100644 index 0000000..8d52e08 --- /dev/null +++ b/web/e2e/specs/review.spec.ts @@ -0,0 +1,230 @@ +/** + * Review/Events page tests -- CRITICAL tier. + * + * Severity tabs, filter popovers, calendar, show-reviewed toggle, + * timeline, and the nested-overlay regression migrated from + * radix-overlay-regressions.spec.ts. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { BasePage } from "../pages/base.page"; +import { ReviewPage } from "../pages/review.page"; +import { + expectBodyInteractive, + waitForBodyInteractive, +} from "../helpers/overlay-interaction"; + +test.describe("Review — severity tabs @critical", () => { + test("tabs render with Alerts default-on", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile); + await expect(review.alertsTab).toBeVisible({ timeout: 10_000 }); + await expect(review.detectionsTab).toBeVisible(); + await expect(review.motionTab).toBeVisible(); + await expect(review.alertsTab).toHaveAttribute("data-state", "on"); + }); + + test("clicking Detections flips data-state", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile); + await expect(review.alertsTab).toBeVisible({ timeout: 10_000 }); + await review.detectionsTab.click(); + await expect(review.detectionsTab).toHaveAttribute("data-state", "on"); + await expect(review.alertsTab).toHaveAttribute("data-state", "off"); + }); + + test("clicking Motion flips data-state", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile); + await expect(review.alertsTab).toBeVisible({ timeout: 10_000 }); + await review.motionTab.click(); + await expect(review.motionTab).toHaveAttribute("data-state", "on"); + }); + + test("switching back to Alerts works", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile); + await review.detectionsTab.click(); + await expect(review.detectionsTab).toHaveAttribute("data-state", "on"); + await review.alertsTab.click(); + await expect(review.alertsTab).toHaveAttribute("data-state", "on"); + }); + + test("switching tabs updates active data-state (client-side filter)", async ({ + frigateApp, + }) => { + // The severity tabs filter the already-fetched review data client-side; + // they do not trigger a new /api/review network request. This test + // verifies the state-change assertion that the tab switch takes effect. + await frigateApp.goto("/review"); + const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile); + await expect(review.alertsTab).toBeVisible({ timeout: 10_000 }); + await expect(review.alertsTab).toHaveAttribute("data-state", "on"); + await review.detectionsTab.click(); + await expect(review.detectionsTab).toHaveAttribute("data-state", "on"); + await expect(review.alertsTab).toHaveAttribute("data-state", "off"); + }); +}); + +test.describe("Review — filters (desktop) @critical", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Filter bar differs on mobile", + ); + + test("Cameras popover lists configured camera names", async ({ + frigateApp, + }) => { + await frigateApp.goto("/review"); + const review = new ReviewPage(frigateApp.page, true); + await expect(review.camerasFilterTrigger).toBeVisible({ timeout: 5_000 }); + await review.camerasFilterTrigger.click(); + await expect(review.filterOverlay).toBeVisible({ timeout: 3_000 }); + await expect(frigateApp.page.getByText("Front Door")).toBeVisible(); + }); + + test("closing the Cameras popover with Escape leaves body interactive", async ({ + frigateApp, + }) => { + // Migrated from radix-overlay-regressions.spec.ts. + await frigateApp.goto("/review"); + const review = new ReviewPage(frigateApp.page, true); + await review.camerasFilterTrigger.click(); + await expect(review.filterOverlay).toBeVisible({ timeout: 3_000 }); + await frigateApp.page.keyboard.press("Escape"); + await expect(review.filterOverlay).not.toBeVisible({ timeout: 3_000 }); + await waitForBodyInteractive(frigateApp.page); + await expectBodyInteractive(frigateApp.page); + }); + + test("Labels are shown inside the General Filter dialog", async ({ + frigateApp, + }) => { + // Labels are surfaced inside the "Filter" button's GeneralFilterContent + // dialog, not as a standalone top-level button. We open that dialog and + // confirm labels from the camera config are listed there. + await frigateApp.goto("/review"); + const filterBtn = frigateApp.page + .getByRole("button", { name: /^filter$/i }) + .first(); + await expect(filterBtn).toBeVisible({ timeout: 5_000 }); + await filterBtn.click(); + + const overlay = frigateApp.page.locator( + "[data-radix-popper-content-wrapper], [role='dialog']", + ); + await expect(overlay.first()).toBeVisible({ timeout: 3_000 }); + // The default mock config for front_door tracks "person" + await expect(overlay.first().getByText(/person/i)).toBeVisible(); + await frigateApp.page.keyboard.press("Escape"); + }); + + test("Zones popover lists configured zones inside the General Filter dialog", async ({ + frigateApp, + }) => { + // Override config to guarantee a known zone on front_door. + await frigateApp.installDefaults({ + config: { + cameras: { + front_door: { + zones: { + front_yard: { coordinates: "0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9" }, + }, + }, + }, + }, + }); + await frigateApp.goto("/review"); + const filterBtn = frigateApp.page + .getByRole("button", { name: /^filter$/i }) + .first(); + await expect(filterBtn).toBeVisible({ timeout: 5_000 }); + await filterBtn.click(); + + const overlay = frigateApp.page.locator( + "[data-radix-popper-content-wrapper], [role='dialog']", + ); + await expect(overlay.first()).toBeVisible({ timeout: 3_000 }); + await expect(overlay.first().getByText(/front.?yard/i)).toBeVisible(); + await frigateApp.page.keyboard.press("Escape"); + }); + + test("Calendar trigger opens a date picker popover", async ({ + frigateApp, + }) => { + await frigateApp.goto("/review"); + const review = new ReviewPage(frigateApp.page, true); + await expect(review.calendarTrigger).toBeVisible({ timeout: 5_000 }); + await review.calendarTrigger.click(); + + // react-day-picker v9 renders a role="grid" calendar with day cells + // as buttons inside gridcells (e.g. "Wednesday, April 1st, 2026"). + // The calendar is placed directly in the DOM (not always inside a + // Radix popper wrapper), so scope by the grid role instead. + const calendarGrid = frigateApp.page.locator('[role="grid"]').first(); + await expect(calendarGrid).toBeVisible({ timeout: 3_000 }); + const dayButton = calendarGrid.locator('[role="gridcell"] button').first(); + await expect(dayButton).toBeVisible({ timeout: 3_000 }); + await frigateApp.page.keyboard.press("Escape"); + }); + + test("Show Reviewed switch flips its checked state", async ({ + frigateApp, + }) => { + // "Show Reviewed" is a Radix Switch (role=switch), not a button. + // It filters review data client-side; it does not trigger a new + // /api/review network request. Verify the switch state toggles. + await frigateApp.goto("/review"); + const showReviewedSwitch = frigateApp.page.getByRole("switch", { + name: /show reviewed/i, + }); + await expect(showReviewedSwitch).toBeVisible({ timeout: 5_000 }); + + // Record initial checked state and click to toggle + const initialChecked = + await showReviewedSwitch.getAttribute("aria-checked"); + await showReviewedSwitch.click(); + const flippedChecked = initialChecked === "true" ? "false" : "true"; + await expect(showReviewedSwitch).toHaveAttribute( + "aria-checked", + flippedChecked, + ); + }); +}); + +test.describe("Review — timeline (desktop) @critical", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Timeline not shown on mobile", + ); + + test("timeline renders time markers", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + await expect + .poll( + async () => (await frigateApp.page.textContent("#pageRoot")) ?? "", + { timeout: 10_000 }, + ) + .toMatch(/[AP]M|\d+:\d+/); + }); +}); + +test.describe("Review — mobile @critical @mobile", () => { + test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only"); + + test("severity tabs render on mobile", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + const review = new ReviewPage(frigateApp.page, false); + await expect(review.alertsTab).toBeVisible({ timeout: 10_000 }); + await expect(review.detectionsTab).toBeVisible(); + }); + + test("back navigation returns to Live", async ({ frigateApp }) => { + await frigateApp.goto("/"); + const base = new BasePage(frigateApp.page, false); + await base.navigateTo("/review"); + await expect(frigateApp.page).toHaveURL(/\/review/); + await base.navigateTo("/"); + await expect(frigateApp.page).toHaveURL(/\/$/); + }); +}); diff --git a/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts b/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts new file mode 100644 index 0000000..38b0f5c --- /dev/null +++ b/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts @@ -0,0 +1,203 @@ +/** + * Camera ffmpeg streams settings tests -- MEDIUM tier. + * + * Covers the input-path source toggle: each ffmpeg input can either point at a + * go2rtc restream (picked from a dropdown, which writes the rtsp://127.0.0.1:8554 + * path plus the preset-rtsp-restream input_args) or use a manually typed path. + */ + +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { test, expect } from "../../fixtures/frigate-test"; +import type { Page } from "@playwright/test"; +import { configFactory } from "../../fixtures/mock-data/config"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CONFIG_SCHEMA = JSON.parse( + readFileSync( + resolve(__dirname, "../../fixtures/mock-data/config-schema.json"), + "utf-8", + ), +); + +const GO2RTC_STREAMS = { + dome_main: ["rtsp://user:pass@192.168.0.20:554/Stream1"], + dome_sub: ["rtsp://user:pass@192.168.0.20:554/Stream2"], +}; + +type CameraInput = { + path: string; + roles: string[]; + input_args?: string; +}; + +async function installRoutes(page: Page, frontDoorInputs: CameraInput[]) { + const config = configFactory({ + go2rtc: { streams: GO2RTC_STREAMS }, + cameras: { + front_door: { + ffmpeg: { inputs: frontDoorInputs }, + }, + }, + }); + + let lastSavedConfig: unknown = null; + + await page.route("**/api/config/schema.json", (route) => + route.fulfill({ json: CONFIG_SCHEMA }), + ); + await page.route("**/api/config", (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ json: config }); + } + return route.fulfill({ json: { success: true } }); + }); + await page.route("**/api/config/raw_paths", (route) => + route.fulfill({ + json: { + cameras: { front_door: { ffmpeg: { inputs: frontDoorInputs } } }, + go2rtc: { streams: GO2RTC_STREAMS }, + }, + }), + ); + await page.route("**/api/config/set", async (route) => { + lastSavedConfig = route.request().postDataJSON(); + await route.fulfill({ json: { success: true, require_restart: false } }); + }); + await page.route("**/api/ffmpeg/presets", (route) => + route.fulfill({ + json: { + hwaccel_args: [], + input_args: ["preset-rtsp-restream", "preset-rtsp-generic"], + output_args: { record: [], detect: [] }, + }, + }), + ); + + return { capturedConfig: () => lastSavedConfig }; +} + +const RESTREAM_RADIO = "Restream (go2rtc)"; +const MANUAL_RADIO = "Manual input path"; + +test.describe("camera ffmpeg input source toggle @medium", () => { + test("manual input defaults to the manual text field", async ({ + frigateApp, + }) => { + await installRoutes(frigateApp.page, [ + { path: "rtsp://10.0.0.1:554/video", roles: ["detect"] }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await expect( + frigateApp.page.getByRole("radio", { name: MANUAL_RADIO }), + ).toBeChecked(); + await expect( + frigateApp.page.getByRole("textbox", { name: "Input path" }), + ).toHaveValue("rtsp://10.0.0.1:554/video"); + }); + + test("an existing restream path auto-detects into restream mode", async ({ + frigateApp, + }) => { + await installRoutes(frigateApp.page, [ + { + path: "rtsp://127.0.0.1:8554/dome_main", + roles: ["detect"], + input_args: "preset-rtsp-restream", + }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await expect( + frigateApp.page.getByRole("radio", { name: RESTREAM_RADIO }), + ).toBeChecked(); + // The dropdown is preselected to the matching go2rtc stream. + await expect( + frigateApp.page.getByRole("combobox", { name: /go2rtc stream/i }), + ).toContainText("dome_main"); + }); + + test("selecting a restream writes the path and preset", async ({ + frigateApp, + }) => { + const capture = await installRoutes(frigateApp.page, [ + { path: "rtsp://10.0.0.1:554/video", roles: ["detect"] }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await frigateApp.page.getByRole("radio", { name: RESTREAM_RADIO }).click(); + await frigateApp.page + .getByRole("combobox", { name: /go2rtc stream/i }) + .click(); + + // The dropdown is searchable: typing narrows the list to matches only, + // with no option to enter a custom stream name. + await frigateApp.page.getByPlaceholder("Search streams...").fill("sub"); + await expect( + frigateApp.page.getByRole("option", { name: "dome_main" }), + ).toBeHidden(); + await frigateApp.page.getByRole("option", { name: "dome_sub" }).click(); + + await frigateApp.page.getByRole("button", { name: "Save" }).click(); + + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .toMatchObject({ + config_data: { + cameras: { + front_door: { + ffmpeg: { + inputs: [ + { + path: "rtsp://127.0.0.1:8554/dome_sub", + input_args: "preset-rtsp-restream", + }, + ], + }, + }, + }, + }, + }); + }); + + test("switching a restream back to manual reverts the preset", async ({ + frigateApp, + }) => { + const capture = await installRoutes(frigateApp.page, [ + { + path: "rtsp://127.0.0.1:8554/dome_main", + roles: ["detect"], + input_args: "preset-rtsp-restream", + }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await frigateApp.page.getByRole("radio", { name: MANUAL_RADIO }).click(); + + // The restream path stays editable in the manual text field. + await expect( + frigateApp.page.getByRole("textbox", { name: "Input path" }), + ).toHaveValue("rtsp://127.0.0.1:8554/dome_main"); + + await frigateApp.page.getByRole("button", { name: "Save" }).click(); + + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .not.toBeNull(); + + const payload = capture.capturedConfig() as { + config_data?: { + cameras?: { + front_door?: { + ffmpeg?: { inputs?: Array<{ input_args?: unknown }> }; + }; + }; + }; + }; + const input = + payload?.config_data?.cameras?.front_door?.ffmpeg?.inputs?.[0]; + expect(input?.input_args).not.toBe("preset-rtsp-restream"); + }); +}); diff --git a/web/e2e/specs/settings/detectors-and-model.spec.ts b/web/e2e/specs/settings/detectors-and-model.spec.ts new file mode 100644 index 0000000..f697b2b --- /dev/null +++ b/web/e2e/specs/settings/detectors-and-model.spec.ts @@ -0,0 +1,55 @@ +/** + * Detectors and model settings page tests -- HIGH tier. + * + * Tests rendering of the merged page and navigation from the Frigate+ page. + */ + +import { test, expect } from "../../fixtures/frigate-test"; + +test.describe("Detectors and model Settings @high", () => { + test("page renders with detector and model cards", async ({ frigateApp }) => { + await frigateApp.goto("/settings?page=systemDetectorsAndModel"); + await frigateApp.page.waitForTimeout(2000); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + + const text = await frigateApp.page.textContent("#pageRoot"); + expect(text).toContain("Detectors and model"); + expect(text?.toLowerCase()).toContain("detector hardware"); + expect(text?.toLowerCase()).toContain("detection model"); + }); + + test("Frigate+ page links to the merged page", async ({ frigateApp }) => { + await frigateApp.goto("/settings?page=frigateplus"); + await frigateApp.page.waitForTimeout(2000); + + const button = frigateApp.page.getByRole("button", { + name: /Change in Detectors and model/, + }); + + // Button only appears when Frigate+ is enabled in the test config; skip + // the click assertion if it's not present. + if ((await button.count()) > 0) { + await button.first().click(); + await frigateApp.page.waitForURL(/page=systemDetectorsAndModel/); + await expect(frigateApp.page.locator("#pageRoot")).toContainText( + "Detectors and model", + ); + } else { + test.skip( + true, + "Frigate+ not enabled in this test config; skipping link assertion", + ); + } + }); + + test("old systemDetectionModel deep-link no longer routes here", async ({ + frigateApp, + }) => { + await frigateApp.goto("/settings?page=systemDetectionModel"); + await frigateApp.page.waitForTimeout(2000); + // The old page key is no longer in allSettingsViews; the router + // falls back to its default settings page (uiSettings). + const text = await frigateApp.page.textContent("#pageRoot"); + expect(text).not.toContain("Detection model"); + }); +}); diff --git a/web/e2e/specs/settings/go2rtc-streams.spec.ts b/web/e2e/specs/settings/go2rtc-streams.spec.ts new file mode 100644 index 0000000..223a261 --- /dev/null +++ b/web/e2e/specs/settings/go2rtc-streams.spec.ts @@ -0,0 +1,235 @@ +/** + * go2rtc streams settings page tests -- MEDIUM tier. + * + * Regression coverage for the compat-mode (ffmpeg:) URL editor: unknown + * fragments like #timeout=10 must remain visible and editable when the + * stream is using compatibility mode. + */ + +import { test, expect } from "../../fixtures/frigate-test"; +import type { Page } from "@playwright/test"; + +const STREAM_NAME = "dome_sub"; +const FFMPEG_URL_WITH_TIMEOUT = + "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#timeout=10"; + +async function installRawPathsRoute(page: Page, streamUrl: string) { + let lastSavedConfig: unknown = null; + await page.route("**/api/config/raw_paths", (route) => + route.fulfill({ + json: { + cameras: {}, + go2rtc: { streams: { [STREAM_NAME]: [streamUrl] } }, + }, + }), + ); + await page.route("**/api/config/set", async (route) => { + lastSavedConfig = route.request().postDataJSON(); + await route.fulfill({ json: { success: true, require_restart: false } }); + }); + return { + capturedConfig: () => lastSavedConfig, + }; +} + +async function expandStream(page: Page, streamName: string) { + // Each StreamCard renders the stream name as an h4 next to a rename + // button, with the chevron toggle as the last button in the header row. + // Scope to the header row (h4's grandparent) and click that last button. + const headerRow = page + .locator(`h4:text-is("${streamName}")`) + .locator("xpath=../.."); + await headerRow.getByRole("button").last().click(); +} + +test.describe("go2rtc streams settings — ffmpeg compat mode @medium", () => { + test("preserves unknown fragments like #timeout= in the URL input", async ({ + frigateApp, + }) => { + await installRawPathsRoute(frigateApp.page, FFMPEG_URL_WITH_TIMEOUT); + await frigateApp.goto("/settings?page=systemGo2rtcStreams"); + + await expect( + frigateApp.page.getByRole("heading", { name: STREAM_NAME }), + ).toBeVisible(); + + await expandStream(frigateApp.page, STREAM_NAME); + + const urlInput = frigateApp.page.getByPlaceholder( + "e.g., rtsp://user:pass@192.168.1.100/stream", + ); + await expect(urlInput).toBeVisible(); + + // Focus the input so credential masking is bypassed and the raw value + // is rendered — this matches how a user would inspect the URL before + // editing it. + await urlInput.focus(); + await expect(urlInput).toHaveValue( + "rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10", + ); + }); + + test("lets the user add an extra fragment in compat mode", async ({ + frigateApp, + }) => { + const capture = await installRawPathsRoute( + frigateApp.page, + FFMPEG_URL_WITH_TIMEOUT, + ); + await frigateApp.goto("/settings?page=systemGo2rtcStreams"); + await expandStream(frigateApp.page, STREAM_NAME); + + const urlInput = frigateApp.page.getByPlaceholder( + "e.g., rtsp://user:pass@192.168.1.100/stream", + ); + await urlInput.focus(); + await urlInput.fill( + "rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10#backchannel=0", + ); + await urlInput.blur(); + + // Reopen and re-focus to assert the new value round-tripped through + // parseFfmpegBaseAndExtras + buildFfmpegUrl back into the displayed text. + await urlInput.focus(); + await expect(urlInput).toHaveValue( + "rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10#backchannel=0", + ); + + // Save and verify the persisted URL includes both extras after the + // recognized video/audio directives. + await frigateApp.page.getByRole("button", { name: "Save" }).click(); + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .toMatchObject({ + config_data: { + go2rtc: { + streams: { + [STREAM_NAME]: [ + "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#timeout=10#backchannel=0", + ], + }, + }, + }, + }); + }); + + test("preserves repeatable #audio= fallback chain and lets the user add another codec", async ({ + frigateApp, + }) => { + const capture = await installRawPathsRoute( + frigateApp.page, + // Idiomatic go2rtc fallback: copy if source has the codec, else transcode + "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus", + ); + await frigateApp.goto("/settings?page=systemGo2rtcStreams"); + await expandStream(frigateApp.page, STREAM_NAME); + + // Two pre-populated audio rows — one per #audio= fragment. + const audioLabel = frigateApp.page.locator(`label:text-is("Audio")`); + const audioRowsContainer = audioLabel.locator("xpath=../.."); + await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(2); + await expect(audioRowsContainer.getByRole("combobox").first()).toHaveText( + "Copy", + ); + await expect(audioRowsContainer.getByRole("combobox").nth(1)).toHaveText( + "Transcode to Opus", + ); + + // Add a third audio codec via the LuPlus next to the "Audio" label. + await audioRowsContainer + .getByRole("button", { name: "Add audio codec" }) + .click(); + await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(3); + + // Change the newly-added entry to AAC. + await audioRowsContainer.getByRole("combobox").nth(2).click(); + await frigateApp.page + .getByRole("option", { name: "Transcode to AAC" }) + .click(); + + await frigateApp.page.getByRole("button", { name: "Save" }).click(); + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .toMatchObject({ + config_data: { + go2rtc: { + streams: { + [STREAM_NAME]: [ + "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus#audio=aac", + ], + }, + }, + }, + }); + }); + + test("LuX is only shown on fallback rows and removes only that codec", async ({ + frigateApp, + }) => { + const capture = await installRawPathsRoute( + frigateApp.page, + "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus", + ); + await frigateApp.goto("/settings?page=systemGo2rtcStreams"); + await expandStream(frigateApp.page, STREAM_NAME); + + const audioLabel = frigateApp.page.locator(`label:text-is("Audio")`); + const audioRowsContainer = audioLabel.locator("xpath=../.."); + const removeButtons = audioRowsContainer.getByRole("button", { + name: "Remove codec", + }); + // Primary (audio=copy) row is permanent and has no X; only the audio=opus + // fallback exposes a remove button. + await expect(removeButtons).toHaveCount(1); + + await removeButtons.first().click(); + await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(1); + await expect(audioRowsContainer.getByRole("combobox")).toHaveText("Copy"); + + await frigateApp.page.getByRole("button", { name: "Save" }).click(); + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .toMatchObject({ + config_data: { + go2rtc: { + streams: { + [STREAM_NAME]: [ + "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy", + ], + }, + }, + }, + }); + }); + + test("picking Exclude on the primary row drops the #video= fragment entirely", async ({ + frigateApp, + }) => { + const capture = await installRawPathsRoute( + frigateApp.page, + "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy", + ); + await frigateApp.goto("/settings?page=systemGo2rtcStreams"); + await expandStream(frigateApp.page, STREAM_NAME); + + const videoLabel = frigateApp.page.locator(`label:text-is("Video")`); + const videoRowsContainer = videoLabel.locator("xpath=../.."); + await videoRowsContainer.getByRole("combobox").first().click(); + await frigateApp.page.getByRole("option", { name: "Exclude" }).click(); + + await frigateApp.page.getByRole("button", { name: "Save" }).click(); + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .toMatchObject({ + config_data: { + go2rtc: { + streams: { + [STREAM_NAME]: [ + "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#audio=copy", + ], + }, + }, + }, + }); + }); +}); diff --git a/web/e2e/specs/settings/ui-settings.spec.ts b/web/e2e/specs/settings/ui-settings.spec.ts new file mode 100644 index 0000000..656ce7f --- /dev/null +++ b/web/e2e/specs/settings/ui-settings.spec.ts @@ -0,0 +1,40 @@ +/** + * Settings page tests -- HIGH tier. + * + * Tests settings page rendering with content, form controls, + * and section navigation. + */ + +import { test, expect } from "../../fixtures/frigate-test"; + +test.describe("Settings Page @high", () => { + test("settings page renders with content", async ({ frigateApp }) => { + await frigateApp.goto("/settings"); + await frigateApp.page.waitForTimeout(2000); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + const text = await frigateApp.page.textContent("#pageRoot"); + expect(text?.length).toBeGreaterThan(0); + }); + + test("settings page has clickable navigation items", async ({ + frigateApp, + }) => { + await frigateApp.goto("/settings"); + await frigateApp.page.waitForTimeout(2000); + const navItems = frigateApp.page.locator( + "#pageRoot button, #pageRoot [role='button'], #pageRoot a", + ); + const count = await navItems.count(); + expect(count).toBeGreaterThan(0); + }); + + test("settings page has form controls", async ({ frigateApp }) => { + await frigateApp.goto("/settings"); + await frigateApp.page.waitForTimeout(2000); + const formElements = frigateApp.page.locator( + '#pageRoot input, #pageRoot button[role="switch"], #pageRoot button[role="combobox"]', + ); + const count = await formElements.count(); + expect(count).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/web/e2e/specs/system.spec.ts b/web/e2e/specs/system.spec.ts new file mode 100644 index 0000000..87e4db5 --- /dev/null +++ b/web/e2e/specs/system.spec.ts @@ -0,0 +1,237 @@ +/** + * System page tests -- MEDIUM tier (promoted to cover migrated + * RestartDialog test from radix-overlay-regressions.spec.ts). + * + * Tab switching, version + last-refreshed display, and the + * RestartDialog cancel flow. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { + expectBodyInteractive, + waitForBodyInteractive, +} from "../helpers/overlay-interaction"; + +test.describe("System — tabs @medium", () => { + test("general tab is active by default via #general hash", async ({ + frigateApp, + }) => { + await frigateApp.goto("/system#general"); + await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute( + "data-state", + "on", + { timeout: 15_000 }, + ); + await expect(frigateApp.page.getByLabel("Select storage")).toBeVisible(); + await expect(frigateApp.page.getByLabel("Select cameras")).toBeVisible(); + }); + + test("Storage tab activates and deactivates General", async ({ + frigateApp, + }) => { + await frigateApp.goto("/system#general"); + await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute( + "data-state", + "on", + { timeout: 15_000 }, + ); + await frigateApp.page.getByLabel("Select storage").click(); + await expect(frigateApp.page.getByLabel("Select storage")).toHaveAttribute( + "data-state", + "on", + { timeout: 5_000 }, + ); + await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute( + "data-state", + "off", + ); + }); + + test("Cameras tab activates", async ({ frigateApp }) => { + await frigateApp.goto("/system#general"); + await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute( + "data-state", + "on", + { timeout: 15_000 }, + ); + await frigateApp.page.getByLabel("Select cameras").click(); + await expect(frigateApp.page.getByLabel("Select cameras")).toHaveAttribute( + "data-state", + "on", + { timeout: 5_000 }, + ); + }); + + test("general tab shows version and last-refreshed", async ({ + frigateApp, + }) => { + await frigateApp.goto("/system#general"); + await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute( + "data-state", + "on", + { timeout: 15_000 }, + ); + await expect(frigateApp.page.getByText("0.15.0-test")).toBeVisible(); + await expect(frigateApp.page.getByText(/Last refreshed/)).toBeVisible(); + }); + + test("storage tab renders content after switching", async ({ + frigateApp, + }) => { + await frigateApp.goto("/system#general"); + await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute( + "data-state", + "on", + { timeout: 15_000 }, + ); + await frigateApp.page.getByLabel("Select storage").click(); + await expect(frigateApp.page.getByLabel("Select storage")).toHaveAttribute( + "data-state", + "on", + { timeout: 5_000 }, + ); + // On desktop, tab buttons render text labels so the word "storage" + // always appears in #pageRoot after switching. On mobile, tabs are + // icon-only, so we verify the general-tab content disappears instead + // (the storage tab's metrics section is hidden but general is gone). + if (!frigateApp.isMobile) { + await expect + .poll( + async () => (await frigateApp.page.textContent("#pageRoot")) ?? "", + { timeout: 10_000 }, + ) + .toMatch(/storage|mount|disk|used|free/i); + } else { + // Mobile: tab activation (data-state "on") already asserted above. + // Additionally confirm general tab is no longer the active tab. + await expect( + frigateApp.page.getByLabel("Select general"), + ).toHaveAttribute("data-state", "off", { timeout: 5_000 }); + } + }); + + test("cameras tab renders each configured camera", async ({ frigateApp }) => { + await frigateApp.goto("/system#general"); + await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute( + "data-state", + "on", + { timeout: 15_000 }, + ); + await frigateApp.page.getByLabel("Select cameras").click(); + await expect(frigateApp.page.getByLabel("Select cameras")).toHaveAttribute( + "data-state", + "on", + { timeout: 5_000 }, + ); + // Cameras tab lists every camera from config/stats. The default + // mock has front_door, backyard, garage. + for (const cam of ["front_door", "backyard", "garage"]) { + await expect( + frigateApp.page + .getByText(new RegExp(cam.replace("_", ".?"), "i")) + .first(), + ).toBeVisible({ timeout: 10_000 }); + } + }); + + test("enrichments tab renders when semantic search is enabled", async ({ + frigateApp, + }) => { + // Override config to guarantee the enrichments tab is present. + // System.tsx shows the tab when semantic_search.enabled === true. + await frigateApp.installDefaults({ + config: { semantic_search: { enabled: true } }, + }); + await frigateApp.goto("/system#general"); + await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute( + "data-state", + "on", + { timeout: 15_000 }, + ); + const enrichTab = frigateApp.page.getByLabel(/select enrichments/i).first(); + await expect(enrichTab).toBeVisible({ timeout: 5_000 }); + await enrichTab.click(); + await expect(enrichTab).toHaveAttribute("data-state", "on", { + timeout: 5_000, + }); + }); +}); + +test.describe("System — RestartDialog @medium", () => { + test.skip( + ({ frigateApp }) => frigateApp.isMobile, + "Sidebar menu is desktop-only", + ); + + test("cancelling restart leaves body interactive", async ({ frigateApp }) => { + // Migrated from radix-overlay-regressions.spec.ts. + await frigateApp.goto("/"); + + const sidebarTriggers = frigateApp.page + .locator('[role="complementary"] [aria-haspopup="menu"]') + .or(frigateApp.page.locator('aside [aria-haspopup="menu"]')); + const triggerCount = await sidebarTriggers.count(); + expect(triggerCount).toBeGreaterThan(0); + + let opened = false; + for (let i = 0; i < triggerCount; i++) { + const trigger = sidebarTriggers.nth(i); + await trigger.click().catch(() => {}); + const restartItem = frigateApp.page + .getByRole("menuitem", { name: /restart/i }) + .first(); + const visible = await expect(restartItem) + .toBeVisible({ timeout: 300 }) + .then(() => true) + .catch(() => false); + if (visible) { + await restartItem.click(); + opened = true; + break; + } + await frigateApp.page.keyboard.press("Escape").catch(() => {}); + } + expect(opened).toBe(true); + + const cancel = frigateApp.page.getByRole("button", { name: /cancel/i }); + await expect(cancel).toBeVisible({ timeout: 3_000 }); + await cancel.click(); + + await waitForBodyInteractive(frigateApp.page); + await expectBodyInteractive(frigateApp.page); + + const postCancelTrigger = sidebarTriggers.first(); + await postCancelTrigger.click(); + await expect( + frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(), + ).toBeVisible({ timeout: 3_000 }); + }); +}); + +test.describe("System — mobile @medium @mobile", () => { + test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only"); + + test("tabs render at mobile viewport", async ({ frigateApp }) => { + await frigateApp.goto("/system#general"); + await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({ + timeout: 15_000, + }); + }); + + test("switching tabs works at mobile viewport", async ({ frigateApp }) => { + await frigateApp.goto("/system#general"); + await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute( + "data-state", + "on", + { timeout: 15_000 }, + ); + await frigateApp.page.getByLabel("Select storage").click(); + await expect(frigateApp.page.getByLabel("Select storage")).toHaveAttribute( + "data-state", + "on", + { timeout: 5_000 }, + ); + }); +}); diff --git a/web/i18next.config.ts b/web/i18next.config.ts new file mode 100644 index 0000000..903cb9c --- /dev/null +++ b/web/i18next.config.ts @@ -0,0 +1,51 @@ +import { defineConfig, type Plugin } from "i18next-cli"; + +/** + * Plugin to remove false positive keys generated by dynamic namespace patterns + * like useTranslation([i18nLibrary]) and t("key", { ns: configNamespace }). + * These keys already exist in their correct runtime namespaces. + */ +function ignoreDynamicNamespaceKeys(): Plugin { + // Keys that the extractor misattributes to the wrong namespace + // because it can't resolve dynamic ns values at build time. + const falsePositiveKeys = new Set([ + // From useTranslation([i18nLibrary]) in ClassificationCard.tsx + // Already in views/classificationModel and views/faceLibrary + "details.unknown", + "details.none", + // From t("key", { ns: configNamespace }) in DetectorHardwareField.tsx + // Already in config/global + "detectors.type.label", + // From t(`${prefix}`) template literals producing empty/partial keys + "", + "_one", + "_other", + ]); + + return { + name: "ignore-dynamic-namespace-keys", + onEnd: async (keys) => { + for (const key of keys.keys()) { + // Each map key is "ns:actualKey" format + const separatorIndex = key.indexOf(":"); + const actualKey = + separatorIndex >= 0 ? key.slice(separatorIndex + 1) : key; + if (falsePositiveKeys.has(actualKey)) { + keys.delete(key); + } + } + }, + }; +} + +export default defineConfig({ + locales: ["en"], + extract: { + input: ["src/**/*.{ts,tsx}"], + output: "public/locales/{{language}}/{{namespace}}.json", + defaultNS: "common", + removeUnusedKeys: false, + sort: false, + }, + plugins: [ignoreDynamicNamespaceKeys()], +}); diff --git a/web/images/branding/LICENSE b/web/images/branding/LICENSE new file mode 100644 index 0000000..6dbfbe6 --- /dev/null +++ b/web/images/branding/LICENSE @@ -0,0 +1,33 @@ +# COPYRIGHT AND TRADEMARK NOTICE + +The images, logos, and icons contained in this directory (the "Brand Assets") are +proprietary to Frigate, Inc. and are NOT covered by the MIT License governing the +rest of this repository. + +1. TRADEMARK STATUS + The "Frigate" name and the accompanying logo are common law trademarks™ of + Frigate, Inc. Frigate, Inc. reserves all rights to these marks. + +2. LIMITED PERMISSION FOR USE + Permission is hereby granted to display these Brand Assets strictly for the + following purposes: + a. To execute the software interface on a local machine. + b. To identify the software in documentation or reviews (nominative use). + +3. RESTRICTIONS + You may NOT: + a. Use these Brand Assets to represent a derivative work (fork) as an official + product of Frigate, Inc. + b. Use these Brand Assets in a way that implies endorsement, sponsorship, or + commercial affiliation with Frigate, Inc. + c. Modify or alter the Brand Assets. + +If you fork this repository with the intent to distribute a modified or competing +version of the software, you must replace these Brand Assets with your own +original content. + +For full usage guidelines, strictly see the TRADEMARK.md file in the +repository root. + +ALL RIGHTS RESERVED. +Copyright (c) 2026 Frigate, Inc. diff --git a/web/images/branding/apple-touch-icon.png b/web/images/branding/apple-touch-icon.png new file mode 100644 index 0000000..a0ca9e8 Binary files /dev/null and b/web/images/branding/apple-touch-icon.png differ diff --git a/web/images/branding/favicon-16x16.png b/web/images/branding/favicon-16x16.png new file mode 100644 index 0000000..bbe3207 Binary files /dev/null and b/web/images/branding/favicon-16x16.png differ diff --git a/web/images/branding/favicon-32x32.png b/web/images/branding/favicon-32x32.png new file mode 100644 index 0000000..20e64b2 Binary files /dev/null and b/web/images/branding/favicon-32x32.png differ diff --git a/web/images/branding/favicon.ico b/web/images/branding/favicon.ico new file mode 100644 index 0000000..1de8ec8 Binary files /dev/null and b/web/images/branding/favicon.ico differ diff --git a/web/images/branding/favicon.png b/web/images/branding/favicon.png new file mode 100644 index 0000000..60bf469 Binary files /dev/null and b/web/images/branding/favicon.png differ diff --git a/web/images/branding/favicon.svg b/web/images/branding/favicon.svg new file mode 100644 index 0000000..066268a --- /dev/null +++ b/web/images/branding/favicon.svg @@ -0,0 +1,46 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/web/images/branding/mstile-150x150.png b/web/images/branding/mstile-150x150.png new file mode 100644 index 0000000..63ecc61 Binary files /dev/null and b/web/images/branding/mstile-150x150.png differ diff --git a/web/images/marker.png b/web/images/marker.png new file mode 100644 index 0000000..3591e0a Binary files /dev/null and b/web/images/marker.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..bb019d4 --- /dev/null +++ b/web/index.html @@ -0,0 +1,36 @@ + + + + + + + Frigate + + + + + + + + + + +
+ + + + diff --git a/web/login.html b/web/login.html new file mode 100644 index 0000000..f83243d --- /dev/null +++ b/web/login.html @@ -0,0 +1,36 @@ + + + + + + + Frigate + + + + + + + + + + +
+ + + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..cd09b79 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,14625 @@ +{ + "name": "web-new", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web-new", + "version": "0.0.0", + "hasInstallScript": true, + "dependencies": { + "@cycjimmy/jsmpeg-player": "^6.1.2", + "@hookform/resolvers": "^3.10.0", + "@melloware/react-logviewer": "^6.1.2", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.2", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.2.3", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.2.3", + "@radix-ui/react-slot": "1.2.4", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-toggle": "^1.1.2", + "@radix-ui/react-toggle-group": "^1.1.2", + "@radix-ui/react-tooltip": "^1.2.8", + "@rjsf/core": "^6.4.1", + "@rjsf/shadcn": "^6.4.1", + "@rjsf/utils": "^6.4.1", + "@rjsf/validator-ajv8": "^6.4.1", + "apexcharts": "^3.52.0", + "axios": "^1.13.6", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "copy-to-clipboard": "^3.3.3", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.2.0", + "framer-motion": "^12.38.0", + "hls.js": "^1.6.15", + "i18next": "^24.2.0", + "i18next-http-backend": "^3.0.1", + "idb-keyval": "^6.2.1", + "immer": "^10.1.1", + "js-yaml": "^4.1.1", + "konva": "^10.2.3", + "lodash": "^4.18.1", + "lucide-react": "^0.577.0", + "monaco-yaml": "^5.4.1", + "next-themes": "^0.4.6", + "nosleep.js": "^0.12.0", + "react": "^19.2.4", + "react-apexcharts": "^1.4.1", + "react-day-picker": "^9.14.0", + "react-device-detect": "^2.2.3", + "react-dom": "^19.2.4", + "react-dropzone": "^14.3.8", + "react-grid-layout": "^2.2.2", + "react-hook-form": "^7.72.0", + "react-i18next": "^15.2.0", + "react-icons": "^5.5.0", + "react-konva": "^19.2.3", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.30.3", + "react-swipeable": "^7.0.2", + "react-zoom-pan-pinch": "^3.7.0", + "remark-gfm": "^4.0.0", + "scroll-into-view-if-needed": "^3.1.0", + "sonner": "^2.0.7", + "sort-by": "^1.2.0", + "strftime": "^0.10.3", + "swr": "^2.4.1", + "tailwind-merge": "^2.4.0", + "tailwind-scrollbar": "^3.1.0", + "tailwindcss-animate": "^1.0.7", + "use-long-press": "^3.2.0", + "vaul": "^1.1.2", + "vite-plugin-monaco-editor": "^1.1.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@playwright/test": "^1.59.1", + "@tailwindcss/forms": "^0.5.9", + "@testing-library/jest-dom": "^6.6.2", + "@types/js-yaml": "^4.0.9", + "@types/lodash": "^4.17.12", + "@types/node": "^20.14.10", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/strftime": "^0.9.8", + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.5.0", + "@vitejs/plugin-react-swc": "^3.8.0", + "@vitest/coverage-v8": "^3.0.7", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jest": "^28.2.0", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.8", + "eslint-plugin-vitest-globals": "^1.5.0", + "fake-indexeddb": "^6.0.0", + "i18next-cli": "^1.5.11", + "jest-websocket-mock": "^2.5.0", + "jsdom": "^24.1.1", + "monaco-editor": "^0.52.2", + "msw": "^2.3.5", + "patch-package": "^8.0.1", + "postcss": "^8.5.8", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.5", + "tailwindcss": "^3.4.9", + "typescript": "^5.9.3", + "vite": "^6.4.2", + "vitest": "^3.0.7" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@cycjimmy/jsmpeg-player": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.1.2.tgz", + "integrity": "sha512-U9DBDe5fxHmbwQww9rFxMLNI2Wlg7DhPzI7AVFpq8GehiUP7+NwuMPXpP4zAd52sgkxtOqOeMjgE5g0ZLnQZ0w==", + "license": "MIT" + }, + "node_modules/@date-fns/tz": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", + "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@melloware/react-logviewer": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@melloware/react-logviewer/-/react-logviewer-6.1.2.tgz", + "integrity": "sha512-WDw3VIGqhoXxDn93HFDicwRhi4+FQyaKiVTB07bWerT82gTgyWV7bOciVV33z25N3WJrz62j5FKVzvFZCu17/A==", + "license": "MPL-2.0", + "dependencies": { + "hotkeys-js": "3.13.9", + "mitt": "3.0.1", + "react-string-replace": "1.1.1", + "virtua": "0.39.3" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.37.6", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", + "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.2.tgz", + "integrity": "sha512-TaJxYoCpxJ7vfEkv2PTNox/6zzmpKXT6ewvCuf2tTOIVN45/Jahhlld29Yw4pciOXS2Xq91/rSGEdmEnUWZCqA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz", + "integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz", + "integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.2.tgz", + "integrity": "sha512-JBm6s6aVG/nwuY5eadhU2zDi/IwYS0sDM5ZWb4nymv/hn3hZdkw+gENn0LP4iY1yCd7+bgJaCwueMYJIU3vk4A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-toggle": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-toggle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz", + "integrity": "sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@react-icons/all-files": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@react-icons/all-files/-/all-files-4.1.0.tgz", + "integrity": "sha512-hxBI2UOuVaI3O/BhQfhtb4kcGn9ft12RWAFVMUeNjqqhLsHvFtzIkFaptBJpFDANTKoDfdVoHTKZDlwKCACbMQ==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rjsf/core": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.4.1.tgz", + "integrity": "sha512-+QaiSgQnOuO6ghIsohH2u/QcylkN+Da2968a75g/i4oARYJRYVxXDm2u3JR5aXndpMb4t4jTFrYyG8cNIv6oEg==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "markdown-to-jsx": "^8.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@rjsf/utils": "^6.4.x", + "react": ">=18" + } + }, + "node_modules/@rjsf/shadcn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@rjsf/shadcn/-/shadcn-6.4.1.tgz", + "integrity": "sha512-WzwXW3XY7K1jo9XrBv6M41ScdHrnQDKpSxip5i1N6xCgEE6hiyX+wn7pDO689OoidvL3lWQmtnoqMdcoJvEWjw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.0", + "@react-icons/all-files": "^4.1.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lucide-react": "^0.548.0", + "tailwind-merge": "^3.4.0", + "tailwindcss-animate": "^1.0.7", + "uuid": "^13.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@rjsf/core": "^6.4.x", + "@rjsf/utils": "^6.4.x", + "react": ">=18" + } + }, + "node_modules/@rjsf/shadcn/node_modules/lucide-react": { + "version": "0.548.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz", + "integrity": "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@rjsf/shadcn/node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/@rjsf/utils": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.4.1.tgz", + "integrity": "sha512-5NL3jwt3rIS5/WRTrKt++y40FS/ScKGVwYJ3jIrHSQHSwBdLnd4cHf2zcnA97L1Klj8I6tvS/ugh+blf/Diwuw==", + "license": "Apache-2.0", + "dependencies": { + "@x0k/json-schema-merge": "^1.0.2", + "fast-uri": "^3.1.0", + "jsonpointer": "^5.0.1", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@rjsf/validator-ajv8": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.4.1.tgz", + "integrity": "sha512-Gx28sRIV7E4CYs2c7BxOGLX44p5IlJE+IaD7GbVk1S+6TxDATqFBSYYZukLB+/vNk3urpndQMreQLKW3W7POHQ==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^2.1.1", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@rjsf/utils": "^6.4.x" + } + }, + "node_modules/@rjsf/validator-ajv8/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@rjsf/validator-ajv8/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@swc/core": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.19.tgz", + "integrity": "sha512-V1r4wFdjaZIUIZZrV2Mb/prEeu03xvSm6oatPxsvnXKF9lNh5Jtk9QvUdiVfD9rrvi7bXrAVhg9Wpbmv/2Fl1g==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.19", + "@swc/core-darwin-x64": "1.13.19", + "@swc/core-linux-arm-gnueabihf": "1.13.19", + "@swc/core-linux-arm64-gnu": "1.13.19", + "@swc/core-linux-arm64-musl": "1.13.19", + "@swc/core-linux-x64-gnu": "1.13.19", + "@swc/core-linux-x64-musl": "1.13.19", + "@swc/core-win32-arm64-msvc": "1.13.19", + "@swc/core-win32-ia32-msvc": "1.13.19", + "@swc/core-win32-x64-msvc": "1.13.19" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.19.tgz", + "integrity": "sha512-NxDyte9tCJSJ8+R62WDtqwg8eI57lubD52sHyGOfezpJBOPr36bUSGGLyO3Vod9zTGlOu2CpkuzA/2iVw92u1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.19.tgz", + "integrity": "sha512-+w5DYrJndSygFFRDcuPYmx5BljD6oYnAohZ15K1L6SfORHp/BTSIbgSFRKPoyhjuIkDiq3W0um8RoMTOBAcQjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.19.tgz", + "integrity": "sha512-7LlfgpdwwYq2q7himNkAAFo4q6jysMLFNoBH6GRP7WL29NcSsl5mPMJjmYZymK+sYq/9MTVieDTQvChzYDsapw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.19.tgz", + "integrity": "sha512-ml3I6Lm2marAQ3UC/TS9t/yILBh/eDSVHAdPpikp652xouWAVW1znUeV6bBSxe1sSZIenv+p55ubKAWq/u84sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.19.tgz", + "integrity": "sha512-M/otFc3/rWWkbF6VgbOXVzUKVoE7MFcphTaStxJp4bwb7oP5slYlxMZN51Dk/OTOfvCDo9pTAFDKNyixbkXMDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.19.tgz", + "integrity": "sha512-NoMUKaOJEdouU4tKF88ggdDHFiRRING+gYLxDqnTfm+sUXaizB5OGBRzvSVDYSXQb1SuUuChnXFPFzwTWbt3ZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.19.tgz", + "integrity": "sha512-r6krlZwyu8SBaw24QuS1lau2I9q8M+eJV6ITz0rpb6P1Bx0elf9ii5Bhh8ddmIqXXH8kOGSjC/dwcdHbZqAhgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.19.tgz", + "integrity": "sha512-awcZSIuxyVn0Dw28VjMvgk1qiDJ6CeQwHkZNUjg2UxVlq23zE01NMMp+zkoGFypmLG9gaGmJSzuoqvk/WCQ5tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.19.tgz", + "integrity": "sha512-H5d+KO7ISoLNgYvTbOcCQjJZNM3R7yaYlrMAF13lUr6GSiOUX+92xtM31B+HvzAWI7HtvVe74d29aC1b1TpXFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.19.tgz", + "integrity": "sha512-qNoyCpXvv2O3JqXKanRIeoMn03Fho/As+N4Fhe7u0FsYh4VYqGQah4DGDzEP/yjl4Gx1IElhqLGDhCCGMwWaDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tabby_ai/hijri-converter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", + "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz", + "integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz", + "integrity": "sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz", + "integrity": "sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.4.tgz", + "integrity": "sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==", + "dev": true + }, + "node_modules/@types/strftime": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@types/strftime/-/strftime-0.9.8.tgz", + "integrity": "sha512-QIvDlGAKyF3YJbT3QZnfC+RIvV5noyDbi+ZJ5rkaSRqxCGrYJefgXm3leZAjtoQOutZe1hCXbAg+p89/Vj4HlQ==", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz", + "integrity": "sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/type-utils": "7.12.0", + "@typescript-eslint/utils": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz", + "integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.12.0", + "@typescript-eslint/utils": "7.12.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", + "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/typescript-estree": "7.12.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz", + "integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/typescript-estree": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz", + "integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz", + "integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz", + "integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz", + "integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.8.0.tgz", + "integrity": "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/core": "^1.10.15" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.7.tgz", + "integrity": "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "debug": "^4.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.0.7", + "vitest": "3.0.7" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.0.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@x0k/json-schema-merge": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@x0k/json-schema-merge/-/json-schema-merge-1.0.2.tgz", + "integrity": "sha512-1734qiJHNX3+cJGDMMw2yz7R+7kpbAtl5NdPs1c/0gO5kYT6s4dMbLXiIfpZNsOYhGZI3aH7FWrj4Zxz7epXNg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15" + } + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apexcharts": { + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz", + "integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==", + "license": "MIT", + "dependencies": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-browser/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/default-browser/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", + "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "28.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.6.0.tgz", + "integrity": "sha512-YG28E1/MIKwnz+e2H7VwYPzHUYU4aMa19w0yGcwXnnmJH6EfgHahTJ2un3IyraUxNfnz/KUhJAFXNNwWPo12tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", + "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/typescript-estree": "7.12.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.8.tgz", + "integrity": "sha512-MIKAclwaDFIiYtVBLzDdm16E+Ty4GwhB6wZlCAG1R3Ur+F9Qbo6PRxpA5DK7XtDgm+WlCoAY2WxAwqhmIDHg6Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-vitest-globals": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.5.0.tgz", + "integrity": "sha512-ZSsVOaOIig0oVLzRTyk8lUfBfqzWxr/J3/NFMfGGRIkGQPejJYmDH3gXmSJxAojts77uzAGB/UmVrwi2DC4LYA==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expect-type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.0.tgz", + "integrity": "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fake-indexeddb": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.0.0.tgz", + "integrity": "sha512-YEboHE5VfopUclOck7LncgIqskAqnv4q0EWbYCaxKKjAvO93c+TJIaBuGy8CBFdbg9nKdpN3AuPRwVBJ4k7NrQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/graphql": { + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz", + "integrity": "sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw==", + "dev": true + }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "license": "Apache-2.0" + }, + "node_modules/hotkeys-js": { + "version": "3.13.9", + "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.9.tgz", + "integrity": "sha512-3TRCj9u9KUH6cKo25w4KIdBfdBfNRjfUwrljCLDC2XhmPDG0SjAZFcFZekpUZFmXzfYoGhFDcdx2gX/vUVtztQ==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/i18next": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz", + "integrity": "sha512-ArJJTS1lV6lgKH7yEf4EpgNZ7+THl7bsGxxougPYiXRTJ/Fe1j08/TBpV9QsXCIYVfdE/HWG/xLezJ5DOlfBOA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-cli": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/i18next-cli/-/i18next-cli-1.5.11.tgz", + "integrity": "sha512-FnLWn+liCoXsrVspv/ysHEzNimzqu5pq6K9tInfWgqSLoYLYPLWMup9UYohp04TgOMAjZd6N4JYIwHcQK4VwIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/core": "1.13.19", + "chalk": "5.6.2", + "chokidar": "4.0.3", + "commander": "14.0.1", + "execa": "9.6.0", + "glob": "11.0.3", + "i18next-resources-for-ts": "1.7.4", + "inquirer": "12.9.6", + "jiti": "2.6.1", + "jsonc-parser": "3.3.1", + "ora": "9.0.0", + "swc-walk": "1.0.0" + }, + "bin": { + "i18next-cli": "dist/esm/cli.js" + } + }, + "node_modules/i18next-cli/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/i18next-cli/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/i18next-cli/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/i18next-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/i18next-cli/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/i18next-cli/node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/i18next-cli/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/i18next-cli/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/i18next-cli/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/i18next-cli/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/i18next-cli/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/i18next-cli/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/i18next-cli/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.1.tgz", + "integrity": "sha512-XT2lYSkbAtDE55c6m7CtKxxrsfuRQO3rUfHzj8ZyRtY9CkIX3aRGwXGTkUhpGWce+J8n7sfu3J0f2wTzo7Lw0A==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, + "node_modules/i18next-resources-for-ts": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/i18next-resources-for-ts/-/i18next-resources-for-ts-1.7.4.tgz", + "integrity": "sha512-3NpN2zasOWYR5zWA4JIdFhxrHxRJV8HEsbR7/GHSnotfjArjZzKvOzQnLFZ911QFmmcwq80saw8rccpHH+MYVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0", + "yaml": "^2.7.1" + }, + "bin": { + "i18next-resources-for-ts": "bin/i18next-resources-for-ts.js" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==" + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/inquirer": { + "version": "12.9.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.9.6.tgz", + "integrity": "sha512-603xXOgyfxhuis4nfnWaZrMaotNT0Km9XwwBNWUKbIDqeCY89jGr2F9YPEMiNhU6XjIP4VoWISMBFfcc5NgrTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.0", + "@inquirer/core": "^10.2.2", + "@inquirer/prompts": "^7.8.6", + "@inquirer/type": "^3.0.8", + "mute-stream": "^2.0.0", + "run-async": "^4.0.5", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-websocket-mock": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/jest-websocket-mock/-/jest-websocket-mock-2.5.0.tgz", + "integrity": "sha512-a+UJGfowNIWvtIKIQBHoEWIUqRxxQHFx4CXT+R5KxxKBtEQ5rS3pPOV/5299sHzqbmeCzxxY5qE4+yfXePePig==", + "dev": true, + "dependencies": { + "jest-diff": "^29.2.0", + "mock-socket": "^9.3.0" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "24.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.1.tgz", + "integrity": "sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, + "node_modules/konva": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/konva/-/konva-10.2.3.tgz", + "integrity": "sha512-NDGeIxm2nsQcp6oqZKS9T764JEi53RpQvpUxV2EK7Awm49fwdd1+EB1Nq1nyspRc0hOAKyKssoTFvPaKwiSUog==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/markdown-to-jsx": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-8.0.0.tgz", + "integrity": "sha512-hWEaRxeCDjes1CVUQqU+Ov0mCqBqkGhLKjL98KdbwHSgEWZZSJQeGlJQatVfeZ3RaxrfTrZZ3eczl2dhp5c/pA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT" + }, + "node_modules/monaco-languageserver-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/monaco-languageserver-types/-/monaco-languageserver-types-0.4.0.tgz", + "integrity": "sha512-QQ3BZiU5LYkJElGncSNb5AKoJ/LCs6YBMCJMAz9EA7v+JaOdn3kx2cXpPTcZfKA5AEsR0vc97sAw+5mdNhVBmw==", + "license": "MIT", + "dependencies": { + "monaco-types": "^0.1.0", + "vscode-languageserver-protocol": "^3.0.0", + "vscode-uri": "^3.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/monaco-marker-data-provider": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/monaco-marker-data-provider/-/monaco-marker-data-provider-1.1.1.tgz", + "integrity": "sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==", + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "monaco-editor": ">=0.30.0" + } + }, + "node_modules/monaco-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/monaco-types/-/monaco-types-0.1.0.tgz", + "integrity": "sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/monaco-worker-manager": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz", + "integrity": "sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==", + "peerDependencies": { + "monaco-editor": ">=0.30.0" + } + }, + "node_modules/monaco-yaml": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-5.4.1.tgz", + "integrity": "sha512-YQ6d/Ei98Uk073SJLFbwuSi95qhnl8F8NNmIUqN2XhDt9psZN2LqQ1T7pPQ866NJb2wFj44IrjnANgpa2jTfag==", + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "jsonc-parser": "^3.0.0", + "monaco-languageserver-types": "^0.4.0", + "monaco-marker-data-provider": "^1.0.0", + "monaco-types": "^0.1.0", + "monaco-worker-manager": "^2.0.0", + "path-browserify": "^1.0.0", + "prettier": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.0", + "vscode-languageserver-types": "^3.0.0", + "vscode-uri": "^3.0.0", + "yaml": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "monaco-editor": ">=0.36" + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.3.tgz", + "integrity": "sha512-+mycXv8l2fEAjFZ5sjrtjJDmm2ceKGjrNbBr1durRg6VkU9fNUE/gsmQ51hWbHqs+l35W1iM+ZsmOD9Fd6lspw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.37.0", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz", + "integrity": "sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nosleep.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", + "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==" + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-path": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.6.0.tgz", + "integrity": "sha512-fxrwsCFi3/p+LeLOAwo/wyRMODZxdGBtUlWRzsEpsUVrisZbEfZ21arxLGfaWfcnqb8oHPNihIb4XPE8CQPN5A==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.2.2", + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "engines": { + "node": ">=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz", + "integrity": "sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig-melody": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig-melody": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-apexcharts": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.1.tgz", + "integrity": "sha512-G14nVaD64Bnbgy8tYxkjuXEUp/7h30Q0U33xc3AwtGFijJB9nHqOt1a6eG0WBn055RgRg+NwqbKGtqPxy15d0Q==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "apexcharts": "^3.41.0", + "react": ">=0.13" + } + }, + "node_modules/react-day-picker": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz", + "integrity": "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "@tabby_ai/hijri-converter": "1.0.5", + "date-fns": "^4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-day-picker/node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/react-device-detect": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.2.3.tgz", + "integrity": "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==", + "dependencies": { + "ua-parser-js": "^1.0.33" + }, + "peerDependencies": { + "react": ">= 0.14.0", + "react-dom": ">= 0.14.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-draggable": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-dropzone": { + "version": "14.3.8", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", + "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-grid-layout": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz", + "integrity": "sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "fast-equals": "^4.0.3", + "prop-types": "^15.8.1", + "react-draggable": "^4.4.6", + "react-resizable": "^3.0.5", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.72.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.0.tgz", + "integrity": "sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-i18next": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz", + "integrity": "sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-konva": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-19.2.3.tgz", + "integrity": "sha512-VsO5CJZwUo12xFa33UEIDOQn6ZZBeE6jlkStGFvpR/3NiDA/9RPQTzw6Ri++C0Pnh3Arco1AehB8qJNv9YCRwg==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.33.0", + "its-fine": "^2.0.0", + "react-reconciler": "0.33.0", + "scheduler": "0.27.0" + }, + "peerDependencies": { + "konva": "^8.0.1 || ^7.2.5 || ^9.0.0 || ^10.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + } + }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz", + "integrity": "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==", + "license": "MIT", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.5.0" + }, + "peerDependencies": { + "react": ">= 16.3", + "react-dom": ">= 16.3" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-string-replace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.1.tgz", + "integrity": "sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-swipeable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz", + "integrity": "sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-zoom-pan-pinch": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz", + "integrity": "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==", + "license": "MIT", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/run-applescript/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/run-applescript/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-applescript/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-applescript/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/run-applescript/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/sort-by": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sort-by/-/sort-by-1.2.0.tgz", + "integrity": "sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==", + "dependencies": { + "object-path": "0.6.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", + "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strftime": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/strftime/-/strftime-0.10.3.tgz", + "integrity": "sha512-DZrDUeIF73eKJ4/GgGuv8UHWcUQPYDYfDeQFj3jrx+JZl6GQE656MbHIpvbo4mEG9a5DgS8GRCc5DxJXD2udDQ==", + "license": "MIT", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/swc-walk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/swc-walk/-/swc-walk-1.0.0.tgz", + "integrity": "sha512-QnEvBZ/ZRsUrXCz/Z3Kto06xUsoqUTo3doj/UvOD0RfamEgqlhpgpyCykFAwiUcuDrODShzlxuDqDPf2Wc+DvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-walk": "^8.3.4" + }, + "engines": { + "node": ">=20.2.0" + } + }, + "node_modules/swr": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/synckit": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.6.tgz", + "integrity": "sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwind-merge": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.4.0.tgz", + "integrity": "sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-scrollbar": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.1.0.tgz", + "integrity": "sha512-pmrtDIZeHyu2idTejfV59SbaJyvp1VRjYxAjZBH0jnyrPRo6HL1kD5Glz8VPagasqr6oAx6M05+Tuw429Z8jxg==", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "tailwindcss": "3.x" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz", + "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-long-press": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.2.0.tgz", + "integrity": "sha512-uq5o2qFR1VRjHn8Of7Fl344/AGvgk7C5Mcb4aSb1ZRVp6PkgdXJJLdRrlSTJQVkkQcDuqFbFc3mDX4COg7mRTA==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/virtua": { + "version": "0.39.3", + "resolved": "https://registry.npmjs.org/virtua/-/virtua-0.39.3.tgz", + "integrity": "sha512-Ep3aiJXSGPm1UUniThr5mGDfG0upAleP7pqQs5mvvCgM1wPhII1ZKa7eNCWAJRLkC+InpXKokKozyaaj/aMYOQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0", + "solid-js": ">=1.0", + "svelte": ">=5.0", + "vue": ">=3.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-monaco-editor": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz", + "integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==", + "peerDependencies": { + "monaco-editor": ">=0.33.0" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.7", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", + "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..7d22dc7 --- /dev/null +++ b/web/package.json @@ -0,0 +1,147 @@ +{ + "name": "web-new", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host", + "postinstall": "patch-package", + "build": "tsc && vite build --base=/BASE_PATH/", + "lint": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore . && npm run e2e:lint", + "e2e:lint": "node e2e/scripts/lint-specs.mjs", + "lint:fix": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore --fix .", + "preview": "vite preview", + "prettier:write": "prettier -u -w --ignore-path .gitignore \"*.{ts,tsx,js,jsx,css,html}\"", + "test": "vitest", + "coverage": "vitest run --coverage", + "e2e:build": "tsc && vite build --base=/", + "e2e": "playwright test --config e2e/playwright.config.ts", + "e2e:ui": "playwright test --config e2e/playwright.config.ts --ui", + "e2e:headed": "playwright test --config e2e/playwright.config.ts --headed", + "i18n:extract": "i18next-cli extract", + "i18n:extract:ci": "i18next-cli extract --ci", + "i18n:status": "i18next-cli status" + }, + "dependencies": { + "@cycjimmy/jsmpeg-player": "^6.1.2", + "@hookform/resolvers": "^3.10.0", + "@melloware/react-logviewer": "^6.1.2", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.2", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.2.3", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.2.3", + "@radix-ui/react-slot": "1.2.4", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-toggle": "^1.1.2", + "@radix-ui/react-toggle-group": "^1.1.2", + "@radix-ui/react-tooltip": "^1.2.8", + "@rjsf/core": "^6.4.1", + "@rjsf/shadcn": "^6.4.1", + "@rjsf/utils": "^6.4.1", + "@rjsf/validator-ajv8": "^6.4.1", + "apexcharts": "^3.52.0", + "axios": "^1.13.6", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "copy-to-clipboard": "^3.3.3", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.2.0", + "framer-motion": "^12.38.0", + "hls.js": "^1.6.15", + "i18next": "^24.2.0", + "i18next-http-backend": "^3.0.1", + "idb-keyval": "^6.2.1", + "immer": "^10.1.1", + "js-yaml": "^4.1.1", + "konva": "^10.2.3", + "lodash": "^4.18.1", + "lucide-react": "^0.577.0", + "monaco-yaml": "^5.4.1", + "next-themes": "^0.4.6", + "nosleep.js": "^0.12.0", + "react": "^19.2.4", + "react-apexcharts": "^1.4.1", + "react-day-picker": "^9.14.0", + "react-device-detect": "^2.2.3", + "react-dom": "^19.2.4", + "react-dropzone": "^14.3.8", + "react-grid-layout": "^2.2.2", + "react-hook-form": "^7.72.0", + "react-i18next": "^15.2.0", + "react-icons": "^5.5.0", + "react-konva": "^19.2.3", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.30.3", + "react-swipeable": "^7.0.2", + "react-zoom-pan-pinch": "^3.7.0", + "remark-gfm": "^4.0.0", + "scroll-into-view-if-needed": "^3.1.0", + "sonner": "^2.0.7", + "sort-by": "^1.2.0", + "strftime": "^0.10.3", + "swr": "^2.4.1", + "tailwind-merge": "^2.4.0", + "tailwind-scrollbar": "^3.1.0", + "tailwindcss-animate": "^1.0.7", + "use-long-press": "^3.2.0", + "vaul": "^1.1.2", + "vite-plugin-monaco-editor": "^1.1.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@playwright/test": "^1.59.1", + "@tailwindcss/forms": "^0.5.9", + "@testing-library/jest-dom": "^6.6.2", + "@types/js-yaml": "^4.0.9", + "@types/lodash": "^4.17.12", + "@types/node": "^20.14.10", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/strftime": "^0.9.8", + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.5.0", + "@vitejs/plugin-react-swc": "^3.8.0", + "@vitest/coverage-v8": "^3.0.7", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jest": "^28.2.0", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.8", + "eslint-plugin-vitest-globals": "^1.5.0", + "fake-indexeddb": "^6.0.0", + "i18next-cli": "^1.5.11", + "jest-websocket-mock": "^2.5.0", + "jsdom": "^24.1.1", + "monaco-editor": "^0.52.2", + "msw": "^2.3.5", + "patch-package": "^8.0.1", + "postcss": "^8.5.8", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.5", + "tailwindcss": "^3.4.9", + "typescript": "^5.9.3", + "vite": "^6.4.2", + "vitest": "^3.0.7" + }, + "overrides": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-slot": "1.2.4" + } +} diff --git a/web/patches/@radix-ui+react-compose-refs+1.1.2.patch b/web/patches/@radix-ui+react-compose-refs+1.1.2.patch new file mode 100644 index 0000000..0cb022b --- /dev/null +++ b/web/patches/@radix-ui+react-compose-refs+1.1.2.patch @@ -0,0 +1,75 @@ +diff --git a/node_modules/@radix-ui/react-compose-refs/dist/index.js b/node_modules/@radix-ui/react-compose-refs/dist/index.js +index 5ba7a95..65aa7be 100644 +--- a/node_modules/@radix-ui/react-compose-refs/dist/index.js ++++ b/node_modules/@radix-ui/react-compose-refs/dist/index.js +@@ -69,6 +69,31 @@ function composeRefs(...refs) { + }; + } + function useComposedRefs(...refs) { +- return React.useCallback(composeRefs(...refs), refs); ++ const refsRef = React.useRef(refs); ++ React.useLayoutEffect(() => { ++ refsRef.current = refs; ++ }); ++ return React.useCallback((node) => { ++ let hasCleanup = false; ++ const cleanups = refsRef.current.map((ref) => { ++ const cleanup = setRef(ref, node); ++ if (!hasCleanup && typeof cleanup === "function") { ++ hasCleanup = true; ++ } ++ return cleanup; ++ }); ++ if (hasCleanup) { ++ return () => { ++ for (let i = 0; i < cleanups.length; i++) { ++ const cleanup = cleanups[i]; ++ if (typeof cleanup === "function") { ++ cleanup(); ++ } else { ++ setRef(refsRef.current[i], null); ++ } ++ } ++ }; ++ } ++ }, []); + } + //# sourceMappingURL=index.js.map +diff --git a/node_modules/@radix-ui/react-compose-refs/dist/index.mjs b/node_modules/@radix-ui/react-compose-refs/dist/index.mjs +index 7dd9172..d1b53a5 100644 +--- a/node_modules/@radix-ui/react-compose-refs/dist/index.mjs ++++ b/node_modules/@radix-ui/react-compose-refs/dist/index.mjs +@@ -32,7 +32,32 @@ function composeRefs(...refs) { + }; + } + function useComposedRefs(...refs) { +- return React.useCallback(composeRefs(...refs), refs); ++ const refsRef = React.useRef(refs); ++ React.useLayoutEffect(() => { ++ refsRef.current = refs; ++ }); ++ return React.useCallback((node) => { ++ let hasCleanup = false; ++ const cleanups = refsRef.current.map((ref) => { ++ const cleanup = setRef(ref, node); ++ if (!hasCleanup && typeof cleanup === "function") { ++ hasCleanup = true; ++ } ++ return cleanup; ++ }); ++ if (hasCleanup) { ++ return () => { ++ for (let i = 0; i < cleanups.length; i++) { ++ const cleanup = cleanups[i]; ++ if (typeof cleanup === "function") { ++ cleanup(); ++ } else { ++ setRef(refsRef.current[i], null); ++ } ++ } ++ }; ++ } ++ }, []); + } + export { + composeRefs, diff --git a/web/patches/@radix-ui+react-slot+1.2.4.patch b/web/patches/@radix-ui+react-slot+1.2.4.patch new file mode 100644 index 0000000..62c2467 --- /dev/null +++ b/web/patches/@radix-ui+react-slot+1.2.4.patch @@ -0,0 +1,46 @@ +diff --git a/node_modules/@radix-ui/react-slot/dist/index.js b/node_modules/@radix-ui/react-slot/dist/index.js +index 3691205..3b62ea8 100644 +--- a/node_modules/@radix-ui/react-slot/dist/index.js ++++ b/node_modules/@radix-ui/react-slot/dist/index.js +@@ -85,11 +85,12 @@ function createSlotClone(ownerName) { + if (isLazyComponent(children) && typeof use === "function") { + children = use(children._payload); + } ++ const childrenRef = React.isValidElement(children) ? getElementRef(children) : null; ++ const composedRef = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, childrenRef); + if (React.isValidElement(children)) { +- const childrenRef = getElementRef(children); + const props2 = mergeProps(slotProps, children.props); + if (children.type !== React.Fragment) { +- props2.ref = forwardedRef ? (0, import_react_compose_refs.composeRefs)(forwardedRef, childrenRef) : childrenRef; ++ props2.ref = forwardedRef ? composedRef : childrenRef; + } + return React.cloneElement(children, props2); + } +diff --git a/node_modules/@radix-ui/react-slot/dist/index.mjs b/node_modules/@radix-ui/react-slot/dist/index.mjs +index d7ea374..a990150 100644 +--- a/node_modules/@radix-ui/react-slot/dist/index.mjs ++++ b/node_modules/@radix-ui/react-slot/dist/index.mjs +@@ -1,6 +1,6 @@ + // src/slot.tsx + import * as React from "react"; +-import { composeRefs } from "@radix-ui/react-compose-refs"; ++import { composeRefs, useComposedRefs } from "@radix-ui/react-compose-refs"; + import { Fragment as Fragment2, jsx } from "react/jsx-runtime"; + var REACT_LAZY_TYPE = Symbol.for("react.lazy"); + var use = React[" use ".trim().toString()]; +@@ -45,11 +45,12 @@ function createSlotClone(ownerName) { + if (isLazyComponent(children) && typeof use === "function") { + children = use(children._payload); + } ++ const childrenRef = React.isValidElement(children) ? getElementRef(children) : null; ++ const composedRef = useComposedRefs(forwardedRef, childrenRef); + if (React.isValidElement(children)) { +- const childrenRef = getElementRef(children); + const props2 = mergeProps(slotProps, children.props); + if (children.type !== React.Fragment) { +- props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef; ++ props2.ref = forwardedRef ? composedRef : childrenRef; + } + return React.cloneElement(children, props2); + } diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/web/public/fonts/Inter-Black.woff2 b/web/public/fonts/Inter-Black.woff2 new file mode 100644 index 0000000..18b35db Binary files /dev/null and b/web/public/fonts/Inter-Black.woff2 differ diff --git a/web/public/fonts/Inter-BlackItalic.woff2 b/web/public/fonts/Inter-BlackItalic.woff2 new file mode 100644 index 0000000..02c9d8e Binary files /dev/null and b/web/public/fonts/Inter-BlackItalic.woff2 differ diff --git a/web/public/fonts/Inter-Bold.woff2 b/web/public/fonts/Inter-Bold.woff2 new file mode 100644 index 0000000..0f1b157 Binary files /dev/null and b/web/public/fonts/Inter-Bold.woff2 differ diff --git a/web/public/fonts/Inter-BoldItalic.woff2 b/web/public/fonts/Inter-BoldItalic.woff2 new file mode 100644 index 0000000..bc50f24 Binary files /dev/null and b/web/public/fonts/Inter-BoldItalic.woff2 differ diff --git a/web/public/fonts/Inter-ExtraBold.woff2 b/web/public/fonts/Inter-ExtraBold.woff2 new file mode 100644 index 0000000..b113368 Binary files /dev/null and b/web/public/fonts/Inter-ExtraBold.woff2 differ diff --git a/web/public/fonts/Inter-ExtraBoldItalic.woff2 b/web/public/fonts/Inter-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..a5b76ca Binary files /dev/null and b/web/public/fonts/Inter-ExtraBoldItalic.woff2 differ diff --git a/web/public/fonts/Inter-ExtraLight.woff2 b/web/public/fonts/Inter-ExtraLight.woff2 new file mode 100644 index 0000000..1d77ae8 Binary files /dev/null and b/web/public/fonts/Inter-ExtraLight.woff2 differ diff --git a/web/public/fonts/Inter-ExtraLightItalic.woff2 b/web/public/fonts/Inter-ExtraLightItalic.woff2 new file mode 100644 index 0000000..8c68492 Binary files /dev/null and b/web/public/fonts/Inter-ExtraLightItalic.woff2 differ diff --git a/web/public/fonts/Inter-Italic.woff2 b/web/public/fonts/Inter-Italic.woff2 new file mode 100644 index 0000000..4c24ce2 Binary files /dev/null and b/web/public/fonts/Inter-Italic.woff2 differ diff --git a/web/public/fonts/Inter-Light.woff2 b/web/public/fonts/Inter-Light.woff2 new file mode 100644 index 0000000..dbe6143 Binary files /dev/null and b/web/public/fonts/Inter-Light.woff2 differ diff --git a/web/public/fonts/Inter-LightItalic.woff2 b/web/public/fonts/Inter-LightItalic.woff2 new file mode 100644 index 0000000..a40d042 Binary files /dev/null and b/web/public/fonts/Inter-LightItalic.woff2 differ diff --git a/web/public/fonts/Inter-Medium.woff2 b/web/public/fonts/Inter-Medium.woff2 new file mode 100644 index 0000000..0fd2ee7 Binary files /dev/null and b/web/public/fonts/Inter-Medium.woff2 differ diff --git a/web/public/fonts/Inter-MediumItalic.woff2 b/web/public/fonts/Inter-MediumItalic.woff2 new file mode 100644 index 0000000..9676715 Binary files /dev/null and b/web/public/fonts/Inter-MediumItalic.woff2 differ diff --git a/web/public/fonts/Inter-Regular.woff2 b/web/public/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000..b8699af Binary files /dev/null and b/web/public/fonts/Inter-Regular.woff2 differ diff --git a/web/public/fonts/Inter-SemiBold.woff2 b/web/public/fonts/Inter-SemiBold.woff2 new file mode 100644 index 0000000..95c48b1 Binary files /dev/null and b/web/public/fonts/Inter-SemiBold.woff2 differ diff --git a/web/public/fonts/Inter-SemiBoldItalic.woff2 b/web/public/fonts/Inter-SemiBoldItalic.woff2 new file mode 100644 index 0000000..ddfe19e Binary files /dev/null and b/web/public/fonts/Inter-SemiBoldItalic.woff2 differ diff --git a/web/public/fonts/Inter-Thin.woff2 b/web/public/fonts/Inter-Thin.woff2 new file mode 100644 index 0000000..0790960 Binary files /dev/null and b/web/public/fonts/Inter-Thin.woff2 differ diff --git a/web/public/fonts/Inter-ThinItalic.woff2 b/web/public/fonts/Inter-ThinItalic.woff2 new file mode 100644 index 0000000..a7bf213 Binary files /dev/null and b/web/public/fonts/Inter-ThinItalic.woff2 differ diff --git a/web/public/images/android-chrome-192x192.png b/web/public/images/android-chrome-192x192.png new file mode 100644 index 0000000..80c3820 Binary files /dev/null and b/web/public/images/android-chrome-192x192.png differ diff --git a/web/public/images/android-chrome-512x512.png b/web/public/images/android-chrome-512x512.png new file mode 100644 index 0000000..a23e25c Binary files /dev/null and b/web/public/images/android-chrome-512x512.png differ diff --git a/web/public/images/apple-touch-icon.png b/web/public/images/apple-touch-icon.png new file mode 100644 index 0000000..a0ca9e8 Binary files /dev/null and b/web/public/images/apple-touch-icon.png differ diff --git a/web/public/images/maskable-badge.png b/web/public/images/maskable-badge.png new file mode 100644 index 0000000..856f142 Binary files /dev/null and b/web/public/images/maskable-badge.png differ diff --git a/web/public/images/maskable-icon.png b/web/public/images/maskable-icon.png new file mode 100644 index 0000000..eb77fe2 Binary files /dev/null and b/web/public/images/maskable-icon.png differ diff --git a/web/public/locales/ar/audio.json b/web/public/locales/ar/audio.json new file mode 100644 index 0000000..350d644 --- /dev/null +++ b/web/public/locales/ar/audio.json @@ -0,0 +1,165 @@ +{ + "bark": "نُبَاح", + "snort": "نَفْخَة", + "heartbeat": "نَبْض القَلْب", + "pets": "حَيَوَانَات أَلِيفَة", + "whoop": "هتاف", + "humming": "هَمْهَمَة", + "chewing": "مَضْغ", + "yodeling": "غناء متقلب", + "howl": "عُوَاء", + "speech": "تحدث", + "hiccup": "فُوَاق", + "dog": "كَلْب", + "yip": "نُبَيْحَة", + "babbling": "ثرثرة", + "yell": "صراخ", + "bellow": "زمجرة", + "whispering": "همس", + "laughter": "ضحك", + "snicker": "ضحكة خفيفه", + "crying": "بكاء", + "sigh": "تنهد", + "singing": "غناء", + "choir": "فرقة غناء", + "chant": "تَرْنِيم", + "mantra": "تعويذة", + "child_singing": "غِنَاء طِفْل", + "synthetic_singing": "غِنَاء اِصْطِنَاعِيّ", + "rapping": "رَاب", + "groan": "أَنِين", + "grunt": "خَنِين", + "whistling": "صَفِير", + "breathing": "تَنَفُّس", + "wheeze": "أَزِيز", + "snoring": "شَخِير", + "gasp": "شَهْقَة", + "pant": "لَهَث", + "cough": "سُعَال", + "throat_clearing": "تَنْحِيم", + "sneeze": "عُطَاس", + "sniff": "شَمَّ", + "run": "رَكْض", + "shuffle": "خَلْط", + "footsteps": "خُطُوَات", + "biting": "عَضّ", + "gargling": "غَرْغَرَة", + "stomach_rumble": "قَرْقَرَة المَعِدَة", + "burping": "تَجَشُّؤ", + "fart": "ضُرَاط", + "hands": "أَيْدِي", + "finger_snapping": "طَقْطَقَة الأَصَابِع", + "clapping": "تَصْفِيق", + "heart_murmur": "نفخة القَلْب", + "cheering": "صِيَاح", + "applause": "تَصْفِيق", + "chatter": "حَدِيث", + "crowd": "جُمْهُور", + "children_playing": "لَعِب الأَطْفَال", + "animal": "حَيَوَان", + "bow_wow": "نُبَاح الكَلْب", + "growling": "زَمْجَرَ", + "whimper_dog": "أَنِين الكَلْب", + "cat": "قِطّ", + "purr": "خَرْخَرَة", + "meow": "مُوَاء", + "hiss": "فَحِيح", + "caterwaul": "صُرَاخ مُتَوَاصِل", + "livestock": "مَاشِيَة", + "horse": "حِصَان", + "clip_clop": "حَوَافِر الخَيْل", + "car": "سيارة", + "motorcycle": "دراجة نارية", + "bicycle": "دراجة هوائية", + "bus": "حافلة", + "train": "قطار", + "boat": "زورق", + "bird": "طائر", + "sine_wave": "موجة الإشارة", + "harmonic": "أوزة", + "caw": "نُعَاقُ الغراب", + "owl": "بومة", + "hoot": "صاح", + "flapping_wings": "أجنحة ترفرف", + "dogs": "كلاب", + "rats": "فئران", + "mouse": "فأر", + "patter": "طقطق", + "insect": "حشرة", + "cricket": "كريكيت", + "mosquito": "بعوضة", + "fly": "سافر", + "buzz": "طنين", + "frog": "ضفدع", + "croak": "نق الضفدع", + "snake": "ثعبان", + "rattle": "جلجلية", + "whale_vocalization": "أصوات الحيتان", + "music": "موسيقى", + "musical_instrument": "آلة موسيقية", + "plucked_string_instrument": "آلة وترية", + "guitar": "غيتار", + "electric_guitar": "غيتار كهربائي", + "bass_guitar": "غيتار البيس", + "acoustic_guitar": "غيتار صوتي", + "steel_guitar": "غيتار فولاذي", + "tapping": "نقر", + "strum": "داعب الأ وتار", + "banjo": "البانجو", + "sitar": "سيتار", + "mandolin": "الماندولين", + "zither": "زيثارة", + "ukulele": "أوكوليلي", + "keyboard": "لوحة المفاتيح", + "piano": "بيانو", + "electric_piano": "بيانو كهربائي", + "organ": "أرغن", + "electronic_organ": "الأورغن الإلكتروني", + "hammond_organ": "أورغن هاموند", + "synthesizer": "مُركِّب صوتي", + "sampler": "عينة", + "harpsichord": "بيان القيثاري", + "percussion": "آلات الإيقاع", + "drum_kit": "طقم طبول", + "drum_machine": "آلة الطبول", + "drum": "طبل", + "snare_drum": "طبلة جانبية", + "rimshot": "طقطة", + "drum_roll": "قرع الطبول", + "bass_drum": "طبلة الباس", + "timpani": "الطبول", + "tabla": "طبلة", + "cymbal": "الصنج", + "hi_hat": "هاي-هات", + "wood_block": "كتلة خشبية", + "tambourine": "دف", + "maraca": "ماراكا", + "gong": "غونغ", + "tubular_bells": "أجراس أنبوبية", + "cattle": "ماشية", + "moo": "خوار", + "cowbell": "جرس البقر", + "pig": "خنزير", + "oink": "أوينك", + "goat": "معزة", + "bleat": "ثغاء", + "sheep": "غنم", + "fowl": "الدواجن", + "chicken": "دجاجة", + "cluck": "قرقرة", + "cock_a_doodle_doo": "كوكو-كو-كوووووو", + "turkey": "ديك رومى", + "gobble": "كركرة", + "tearing": "يمزق", + "ping": "طنّة", + "clang": "صوت رنين", + "squeal": "يصرخ", + "creak": "صرير", + "sizzle": "صوت الأزيز", + "clicking": "النقر", + "clickety_clack": "طقطقة", + "rumble": "الحلبة", + "skateboard": "لوح تزلج", + "echo": "صدى الصوت", + "noise": "ازعاج" +} diff --git a/web/public/locales/ar/common.json b/web/public/locales/ar/common.json new file mode 100644 index 0000000..92390a7 --- /dev/null +++ b/web/public/locales/ar/common.json @@ -0,0 +1,20 @@ +{ + "time": { + "untilForTime": "حتى {{time}}", + "untilForRestart": "حتى يعاد تشغيل فرايجيت.", + "untilRestart": "حتى إعادة التشغيل", + "ago": "منذ {{timeAgo}}", + "justNow": "في التو", + "today": "اليوم", + "last14": "آخر 14 يومًا", + "last30": "آخر 30 يومًا", + "thisWeek": "هذا الأسبوع", + "lastWeek": "الأسبوع الماضي", + "thisMonth": "هذا الشهر", + "yesterday": "بالأمس", + "last7": "آخر 7 أيام", + "lastMonth": "الشهر المنصرم", + "5minutes": "5 دقائق", + "10minutes": "10 دقائق" + } +} diff --git a/web/public/locales/ar/components/auth.json b/web/public/locales/ar/components/auth.json new file mode 100644 index 0000000..1c8eabf --- /dev/null +++ b/web/public/locales/ar/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "password": "كلمة السر", + "user": "أسم المستخدم", + "login": "تسجيل الدخول", + "errors": { + "usernameRequired": "اسم المستخدم مطلوب", + "passwordRequired": "كلمة المرور مطلوبة", + "rateLimit": "تجاوز الحد الأقصى للمعدل. حاول مرة أخرى في وقت لاحق.", + "webUnknownError": "خطأ غير معروف. تحقق من سجلات وحدة التحكم.", + "loginFailed": "فشل تسجيل الدخول", + "unknownError": "خطأ غير معروف. تحقق من السجلات." + } + } +} diff --git a/web/public/locales/ar/components/camera.json b/web/public/locales/ar/components/camera.json new file mode 100644 index 0000000..9bc19f1 --- /dev/null +++ b/web/public/locales/ar/components/camera.json @@ -0,0 +1,51 @@ +{ + "group": { + "label": "مجموعات الكاميرات", + "add": "إضافة مجموعة الكاميرات", + "edit": "تعديل مجموعة الكاميرات", + "delete": { + "label": "حذف مجموعة الكاميرات", + "confirm": { + "title": "تأكيد الحذف", + "desc": "هل أنت متأكد أنك تريد حذف مجموعة الكاميرات {{name}}؟" + } + }, + "name": { + "errorMessage": { + "mustLeastCharacters": "يجب أن يتكون اسم مجموعة الكاميرا من حرفين على الأقل.", + "exists": "اسم مجموعة الكاميرا موجود بالفعل.", + "nameMustNotPeriod": "يجب ألا يحتوي اسم مجموعة الكاميرا على نقطة.", + "invalid": "اسم مجموعة الكاميرا غير صالح." + }, + "label": "الاسم", + "placeholder": "أدخل اسمًا…" + }, + "cameras": { + "label": "الكاميرات", + "desc": "اختر الكاميرات لهذه المجموعة." + }, + "icon": "أيقونة", + "camera": { + "setting": { + "streamMethod": { + "placeholder": "إختيار طريقة البث", + "method": { + "noStreaming": { + "label": "لايوجد بث", + "desc": "صور الكاميرا سيتم تحديثها مرة واحدة فقط كل دقيقة من دون بث حي." + }, + "smartStreaming": { + "label": "البث الذكي (ينصح به)" + }, + "continuousStreaming": { + "label": "بث متواصل" + } + } + } + } + } + }, + "debug": { + "timestamp": "الختم الزمني" + } +} diff --git a/web/public/locales/ar/components/dialog.json b/web/public/locales/ar/components/dialog.json new file mode 100644 index 0000000..acc4c8b --- /dev/null +++ b/web/public/locales/ar/components/dialog.json @@ -0,0 +1,39 @@ +{ + "restart": { + "title": "هل أنت متأكد أنك تريد إعادة تشغيل فرايجيت؟", + "button": "إعادة التشغيل", + "restarting": { + "title": "يتم إعادة تشغيل فرايجيت", + "content": "العد التنازلي", + "button": "فرض إعادة التحميل الآن" + }, + "description": "هذا سيؤدي لإيقاف Frigate مؤقتا أثناء إعادة تشغيلها" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "التقديم إلى Frigate+", + "desc": "الكائنات الموجودة في الأماكن التي تريد تجنبها ليست ضمن النتائج الإيجابية الخاطئة. إرسالها كنتائج إيجابية خاطئة سيؤدي إلى إرباك النموذج." + }, + "review": { + "state": { + "submitted": "تم تقديمه" + }, + "question": { + "label": "تأكد من صحة هذه التسمية لـ Frigate Plus", + "ask_a": "هل هذا الكائن هو {{label}}؟", + "ask_an": "هل هذا الكائن هو {{label}}؟", + "ask_full": "هل هذا الكائن هو {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "عرض في التاريخ" + } + }, + "export": { + "time": { + "fromTimeline": "اختر من التسلسل الزمني" + } + } +} diff --git a/web/public/locales/ar/components/filter.json b/web/public/locales/ar/components/filter.json new file mode 100644 index 0000000..954d69f --- /dev/null +++ b/web/public/locales/ar/components/filter.json @@ -0,0 +1,31 @@ +{ + "filter": "ترشيح", + "labels": { + "label": "التسميات", + "all": { + "title": "كل التسميات", + "short": "المسمّيات" + } + }, + "classes": { + "label": "فئات", + "all": { + "title": "جميع الفئات" + }, + "count_one": "{{عدد}} الفئة", + "count_other": "{{count}} الفئات" + }, + "zones": { + "label": "المناطق", + "all": { + "title": "جميع المناطق", + "short": "المناطق" + } + }, + "dates": { + "selectPreset": "اختر إعدادًا مسبقًا…", + "all": { + "title": "جميع التواريخ" + } + } +} diff --git a/web/public/locales/ar/components/icons.json b/web/public/locales/ar/components/icons.json new file mode 100644 index 0000000..6002ba6 --- /dev/null +++ b/web/public/locales/ar/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "اختر أيقونة", + "search": { + "placeholder": "ابحث عن أيقونة…" + } + } +} diff --git a/web/public/locales/ar/components/input.json b/web/public/locales/ar/components/input.json new file mode 100644 index 0000000..f58f8be --- /dev/null +++ b/web/public/locales/ar/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "تحميل الفيديو", + "toast": { + "success": "تم بدأ تحميل فيديو عنصر المراجعة." + } + } + } +} diff --git a/web/public/locales/ar/components/player.json b/web/public/locales/ar/components/player.json new file mode 100644 index 0000000..5a3e87d --- /dev/null +++ b/web/public/locales/ar/components/player.json @@ -0,0 +1,29 @@ +{ + "noRecordingsFoundForThisTime": "لا يوجد تسجيلات في هذا التوقيت", + "noPreviewFound": "لا يوجد معاينة", + "noPreviewFoundFor": "لا يوجد معاينة لـ{{cameraName}}", + "submitFrigatePlus": { + "title": "هل ترغب بإرسال هذه الصوره الى Frigate+؟", + "submit": "تقديم" + }, + "livePlayerRequiredIOSVersion": "مطلوب نظام iOS 17.1 أو أكبر لهذا النوع من البث المباشر.", + "cameraDisabled": "الكاميرا معطلة", + "stats": { + "streamType": { + "title": "نوع الدفق:", + "short": "النوع" + }, + "bandwidth": { + "title": "العرض الترددي:", + "short": "العرض الترددي" + }, + "latency": { + "title": "التأخير:", + "value": "{{seconds}} ثانية" + } + }, + "streamOffline": { + "title": "البث دون اتصال بالإنترنت", + "desc": "لم يتم استلام أي إطارات على دفق {{cameraName}} detect، تحقق من سجلات الأخطاء" + } +} diff --git a/web/public/locales/ar/config/cameras.json b/web/public/locales/ar/config/cameras.json new file mode 100644 index 0000000..8cf9fb0 --- /dev/null +++ b/web/public/locales/ar/config/cameras.json @@ -0,0 +1,6 @@ +{ + "label": "اعدادات الكاميرا", + "name": { + "label": "إسم الكاميرا" + } +} diff --git a/web/public/locales/ar/config/global.json b/web/public/locales/ar/config/global.json new file mode 100644 index 0000000..a663ff6 --- /dev/null +++ b/web/public/locales/ar/config/global.json @@ -0,0 +1,6 @@ +{ + "version": { + "label": "إصدار الإعدادات الحالية", + "description": "نسحة عددية أو نصية من الإعدادات الحالية الفعالة للمساعدة على اكتشاف الانتقال أو التغير في الصِّيَغ" + } +} diff --git a/web/public/locales/ar/config/groups.json b/web/public/locales/ar/config/groups.json new file mode 100644 index 0000000..ff73d6a --- /dev/null +++ b/web/public/locales/ar/config/groups.json @@ -0,0 +1,8 @@ +{ + "audio": { + "global": { + "detection": "التحري العام", + "sensitivity": "الحساسية العامة" + } + } +} diff --git a/web/public/locales/ar/config/validation.json b/web/public/locales/ar/config/validation.json new file mode 100644 index 0000000..fef7af7 --- /dev/null +++ b/web/public/locales/ar/config/validation.json @@ -0,0 +1,4 @@ +{ + "minimum": "يجب أن تكون {{limit}} على الأقل", + "maximum": "يجب أن تكون {{limit}} كحد أقصى" +} diff --git a/web/public/locales/ar/objects.json b/web/public/locales/ar/objects.json new file mode 100644 index 0000000..50d19ad --- /dev/null +++ b/web/public/locales/ar/objects.json @@ -0,0 +1,27 @@ +{ + "dog": "كَلْب", + "cat": "قِطّ", + "horse": "حِصَان", + "animal": "حَيَوَان", + "bark": "نُبَاح", + "person": "شخص", + "bicycle": "دراجة هوائية", + "car": "سيارة", + "motorcycle": "دراجة نارية", + "airplane": "طائرة", + "bus": "حافلة", + "traffic_light": "إشارة المرور", + "fire_hydrant": "حنفية إطفاء الحريق", + "street_sign": "لافتة شارع", + "stop_sign": "إشارة توقف", + "parking_meter": "عداد موقف سيارات", + "train": "قطار", + "boat": "زورق", + "bench": "مقعدة", + "bird": "طائر", + "mouse": "فأر", + "keyboard": "لوحة المفاتيح", + "goat": "معزة", + "sheep": "غنم", + "skateboard": "لوح تزلج" +} diff --git a/web/public/locales/ar/views/chat.json b/web/public/locales/ar/views/chat.json new file mode 100644 index 0000000..961cd84 --- /dev/null +++ b/web/public/locales/ar/views/chat.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "المحادثات - Frigate" +} diff --git a/web/public/locales/ar/views/classificationModel.json b/web/public/locales/ar/views/classificationModel.json new file mode 100644 index 0000000..09af345 --- /dev/null +++ b/web/public/locales/ar/views/classificationModel.json @@ -0,0 +1,6 @@ +{ + "train": { + "titleShort": "الأخيرة" + }, + "documentTitle": "تصنيف النماذج - Frigate" +} diff --git a/web/public/locales/ar/views/configEditor.json b/web/public/locales/ar/views/configEditor.json new file mode 100644 index 0000000..6387006 --- /dev/null +++ b/web/public/locales/ar/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "محرر الإعدادات - فرايجيت", + "configEditor": "محرر الإعدادات", + "copyConfig": "نسخ الإعدادات", + "saveAndRestart": "حفظ وإعادة تشغيل", + "safeConfigEditor": "محرر التكوين في ( الوضع الامن )", + "safeModeDescription": "أصبح Frigate في الوضع الآمن بسبب خطأ في التحقق من صحة التكوين.", + "toast": { + "success": { + "copyToClipboard": "تم نسخ التكوين إلى الحافظة." + }, + "error": { + "savingError": "خطأ في حفظ التكوين" + } + }, + "saveOnly": "احفظ فقط", + "confirm": "أتود الخروج دون حفظ؟" +} diff --git a/web/public/locales/ar/views/events.json b/web/public/locales/ar/views/events.json new file mode 100644 index 0000000..41312c9 --- /dev/null +++ b/web/public/locales/ar/views/events.json @@ -0,0 +1,25 @@ +{ + "detections": "الإكتشافات", + "alerts": "الإنذارات", + "motion": { + "label": "الحركة", + "only": "حركة فقط" + }, + "allCameras": "كافة الكاميرات", + "empty": { + "alert": "لا توجد تنبيهات لمراجعتها", + "detection": "لا توجد عمليات كشف لمراجعتها", + "motion": "لم يتم العثور على بيانات الحركة" + }, + "timeline": "التسلسل الزمني", + "timeline.aria": "اختر التسلسل الزمني", + "events": { + "label": "اﻷحداث", + "aria": "اختر الأحداث", + "noFoundForTimePeriod": "لم يتم العثور على أي أحداث لهذه الفترة الزمنية." + }, + "documentTitle": "مراجعة - Frigate", + "recordings": { + "documentTitle": "التسجيلات - Frigate" + } +} diff --git a/web/public/locales/ar/views/explore.json b/web/public/locales/ar/views/explore.json new file mode 100644 index 0000000..4b54ed1 --- /dev/null +++ b/web/public/locales/ar/views/explore.json @@ -0,0 +1,30 @@ +{ + "exploreMore": "اكتشف المزيد من أجسام {{label}}", + "documentTitle": "اكتشف - فرايجيت", + "generativeAI": "ذكاء اصطناعي مولد", + "exploreIsUnavailable": { + "title": "المتصفح غير متاح", + "embeddingsReindexing": { + "context": "يمكن استخدام الاستكشاف بعد انتهاء تضمين الكائنات المتعقبة من إعادة الفهرسة.", + "startingUp": "إبتدا التشغيل…", + "step": { + "thumbnailsEmbedded": "الصور المصغرة المضمنة: ", + "descriptionsEmbedded": "الأوصاف المضمنة: ", + "trackedObjectsProcessed": "الأشياء المتعقبة التي تمت معالجتها: " + }, + "estimatedTime": "الزمن المتبقي المقدر:", + "finishingShortly": "سينتهي قريبًا" + }, + "downloadingModels": { + "context": "تقوم Frigate بتنزيل نماذج التضمين اللازمة لدعم ميزة البحث الدلالي. قد يستغرق ذلك عدة دقائق حسب سرعة اتصالك بالإنترنت.", + "setup": { + "visionModel": "نموذج الرؤية", + "visionModelFeatureExtractor": "مستخرج ميزات نموذج الرؤية", + "textModel": "نموذج النص" + } + } + }, + "details": { + "timestamp": "الطابع الزمني" + } +} diff --git a/web/public/locales/ar/views/exports.json b/web/public/locales/ar/views/exports.json new file mode 100644 index 0000000..318ec2f --- /dev/null +++ b/web/public/locales/ar/views/exports.json @@ -0,0 +1,17 @@ +{ + "search": "بحث", + "noExports": "لا يوجد تصديرات", + "documentTitle": "التصدير - فرايجيت", + "deleteExport": "حذف التصدير", + "deleteExport.desc": "هل أنت متأكد من رغبتك في حذف{{exportName}}؟", + "editExport": { + "title": "إعادة تسمية التصدير", + "desc": "قم بإدخال اسم جديد لهذا التصدير.", + "saveExport": "حفظ التصدير" + }, + "toast": { + "error": { + "renameExportFailed": "فشل إعادة تسمية التصدير: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/ar/views/faceLibrary.json b/web/public/locales/ar/views/faceLibrary.json new file mode 100644 index 0000000..5a40c8c --- /dev/null +++ b/web/public/locales/ar/views/faceLibrary.json @@ -0,0 +1,108 @@ +{ + "description": { + "addFace": "أضف مجموعة جديدة إلى مكتبة الوجوه عن طريق رفع صورتك الأولى.", + "invalidName": "أسم غير صالح. يجب أن يشمل الأسم فقط على الحروف، الأرقام، المسافات، الفاصلة العليا، الشرطة التحتية، والشرطة الواصلة.", + "placeholder": "أدخل أسم لهذه المجموعة" + }, + "details": { + "person": "شخص", + "subLabelScore": "نتيجة العلامة الفرعية", + "timestamp": "الطابع الزمني", + "unknown": "غير معروف", + "scoreInfo": "النتيجة الفرعية هي النتيجة المرجحة لجميع درجات الثقة المعترف بها للوجه، لذلك قد تختلف عن النتيجة الموضحة في اللقطة.", + "face": "تفاصيل الوجه", + "faceDesc": "تفاصيل الكائن المتتبع الذي أنشأ هذا الوجه" + }, + "documentTitle": "مكتبة الوجوه - Frigate", + "uploadFaceImage": { + "title": "رفع صورة الوجه", + "desc": "قم بتحميل صورة لمسح الوجوه وإدراجها في {{pageToggle}}" + }, + "collections": "المجموعات", + "createFaceLibrary": { + "title": "إنشاء المجاميع", + "desc": "إنشاء مجموعة جديدة", + "new": "إضافة وجه جديد", + "nextSteps": "لبناء أساس قوي:
  • استخدم علامة التبويب \"التعرّفات الأخيرة\" لاختيار الصور والتدريب عليها لكل شخص تم اكتشافه.
  • ركّز على الصور الأمامية المباشرة للحصول على أفضل النتائج؛ وتجنّب صور التدريب التي تُظهر الوجوه بزاوية.
  • " + }, + "steps": { + "faceName": "ادخل اسم للوجه", + "uploadFace": "ارفع صورة للوجه", + "nextSteps": "الخطوة التالية", + "description": { + "uploadFace": "قم برفع صورة لـ {{name}} تُظهر وجهه من زاوية أمامية مباشرة. لا يلزم أن تكون الصورة مقتصرة على الوجه فقط." + } + }, + "train": { + "title": "التعرّفات الأخيرة", + "titleShort": "الأخيرة", + "aria": "اختر التعرّفات الأخيرة", + "empty": "لا توجد أي محاولات حديثة للتعرّف على الوجوه" + }, + "deleteFaceLibrary": { + "title": "احذف الاسم", + "desc": "هل أنت متأكد أنك تريد حذف المجموعة {{name}}؟ سيؤدي هذا إلى حذف جميع الوجوه المرتبطة بها نهائيًا." + }, + "deleteFaceAttempts": { + "title": "احذف الوجوه", + "desc_zero": "وجه", + "desc_one": "وجه", + "desc_two": "وجهان", + "desc_few": "وجوه", + "desc_many": "وجهًا", + "desc_other": "وجه" + }, + "renameFace": { + "title": "اعادة تسمية الوجه", + "desc": "ادخل اسم جديد لـ{{name}}" + }, + "button": { + "deleteFaceAttempts": "احذف الوجوه", + "addFace": "اظف وجهًا", + "renameFace": "اعد تسمية وجه", + "deleteFace": "احذف وجهًا", + "uploadImage": "ارفع صورة", + "reprocessFace": "إعادة معالجة الوجه" + }, + "imageEntry": { + "validation": { + "selectImage": "يرجى اختيار ملف صورة." + }, + "dropActive": "اسحب الصورة إلى هنا…", + "dropInstructions": "اسحب وأفلت أو الصق صورة هنا، أو انقر للاختيار", + "maxSize": "الحجم الأقصى: {{size}} ميغابايت" + }, + "nofaces": "لا توجد وجوه متاحة", + "trainFaceAs": "درّب الوجه كـ:", + "trainFace": "درّب الوجه", + "toast": { + "success": { + "uploadedImage": "تم رفع الصورة بنجاح.", + "addFaceLibrary": "تمت إضافة {{name}} بنجاح إلى مكتبة الوجوه!", + "deletedFace_zero": "وجه", + "deletedFace_one": "وجه", + "deletedFace_two": "وجهين", + "deletedFace_few": "وجوه", + "deletedFace_many": "وجهًا", + "deletedFace_other": "وجه", + "deletedName_zero": "وجه", + "deletedName_one": "وجه", + "deletedName_two": "وجهين", + "deletedName_few": "وجوه", + "deletedName_many": "وجهًا", + "deletedName_other": "وجه", + "renamedFace": "تمت إعادة تسمية الوجه بنجاح إلى {{name}}", + "trainedFace": "تم تدريب الوجه بنجاح.", + "updatedFaceScore": "تم تحديث درجة الوجه بنجاح إلى {{name}} ({{score}})." + }, + "error": { + "uploadingImageFailed": "فشل في رفع الصورة: {{errorMessage}}", + "addFaceLibraryFailed": "فشل في تعيين اسم الوجه: {{errorMessage}}", + "deleteFaceFailed": "فشل الحذف: {{errorMessage}}", + "deleteNameFailed": "فشل في حذف الاسم: {{errorMessage}}", + "renameFaceFailed": "فشل في إعادة تسمية الوجه: {{errorMessage}}", + "trainFailed": "فشل التدريب: {{errorMessage}}", + "updateFaceScoreFailed": "فشل في تحديث درجة الوجه: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/ar/views/live.json b/web/public/locales/ar/views/live.json new file mode 100644 index 0000000..6e4f32d --- /dev/null +++ b/web/public/locales/ar/views/live.json @@ -0,0 +1,39 @@ +{ + "documentTitle": "بث حي - فرايجيت", + "documentTitle.withCamera": "{{camera}} - بث حي - فرايجيت", + "lowBandwidthMode": "وضع موفر للبيانات", + "twoWayTalk": { + "enable": "تفعيل المكالمات ثنائية الاتجاه", + "disable": "تعطيل المحادثة ثنائية الاتجاه" + }, + "cameraAudio": { + "enable": "تمكين صوت الكاميرا", + "disable": "تعطيل صوت الكاميرا" + }, + "ptz": { + "move": { + "clickMove": { + "enable": "تمكين النقر للتحريك", + "disable": "تعطيل النقر للتحريك", + "label": "سينتهي قريبًا" + }, + "left": { + "label": "حرك الكاميرا PTZ إلى اليسار" + }, + "up": { + "label": "حرك كاميرا PTZ لأعلى" + }, + "down": { + "label": "حرك كاميرا PTZ لأسفل" + }, + "right": { + "label": "حرك الكاميرا PTZ إلى اليمين" + } + }, + "zoom": { + "in": { + "label": "تقريب كاميرا PTZ" + } + } + } +} diff --git a/web/public/locales/ar/views/motionSearch.json b/web/public/locales/ar/views/motionSearch.json new file mode 100644 index 0000000..119c06e --- /dev/null +++ b/web/public/locales/ar/views/motionSearch.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "البحث عن الحركة - Frigate" +} diff --git a/web/public/locales/ar/views/recording.json b/web/public/locales/ar/views/recording.json new file mode 100644 index 0000000..c12dfda --- /dev/null +++ b/web/public/locales/ar/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "ترشيح", + "export": "إرسال", + "calendar": "التقويم", + "filters": "المنقيات", + "toast": { + "error": { + "noValidTimeSelected": "لم يتم تحديد نطاق زمني صحيح", + "endTimeMustAfterStartTime": "يجب أن يكون وقت الانتهاء بعد وقت بدء التشغيل" + } + } +} diff --git a/web/public/locales/ar/views/replay.json b/web/public/locales/ar/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ar/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ar/views/search.json b/web/public/locales/ar/views/search.json new file mode 100644 index 0000000..7964a0f --- /dev/null +++ b/web/public/locales/ar/views/search.json @@ -0,0 +1,23 @@ +{ + "search": "بحث", + "savedSearches": "عمليات البحث المحفوظة", + "searchFor": "البحث عن {{inputValue}}", + "button": { + "clear": "محو البحث", + "save": "احفظ البحث", + "delete": "حذف البحث المحفوظ", + "filterInformation": "تصفية المعلومات", + "filterActive": "الفلتر النشط" + }, + "trackedObjectId": "مُعرف الكائن المتعقّب", + "filter": { + "label": { + "cameras": "الكاميرات", + "labels": "الملصقات", + "zones": "مناطق", + "search_type": "نوع البحث", + "sub_labels": "العلامات الفرعية", + "time_range": "النطاق الزمني" + } + } +} diff --git a/web/public/locales/ar/views/settings.json b/web/public/locales/ar/views/settings.json new file mode 100644 index 0000000..6a40658 --- /dev/null +++ b/web/public/locales/ar/views/settings.json @@ -0,0 +1,35 @@ +{ + "documentTitle": { + "camera": "إعدادات الكاميرا - فرايجيت", + "default": "الإعدادات - فرايجيت", + "authentication": "إعدادات المصادقة - فرايجيت", + "enrichments": "إحصاء الاعدادات", + "masksAndZones": "القناع ومحرر المنطقة - Frigate", + "motionTuner": "مضبط الحركة - Firgate", + "object": "تصحيح الأخطاء - Frigate", + "general": "الإعدادات العامة - Frigate", + "notifications": "إعدادات الإشعارات - Frigate" + }, + "menu": { + "ui": "واجهة المستخدم", + "enrichments": "التحسينات", + "cameras": "إعدادات الكاميرا", + "masksAndZones": "أقنعة / مناطق", + "motionTuner": "مضبط الحركة", + "debug": "تصحيح", + "users": "المستخدمون", + "notifications": "إشعارات" + }, + "dialog": { + "unsavedChanges": { + "title": "لديك تغييرات غير محفوظة.", + "desc": "هل تريد حفظ تغييراتك قبل المتابعة؟" + } + }, + "cameraSetting": { + "camera": "كاميرا" + }, + "general": { + "title": "الإعدادات العامة" + } +} diff --git a/web/public/locales/ar/views/system.json b/web/public/locales/ar/views/system.json new file mode 100644 index 0000000..261b7e9 --- /dev/null +++ b/web/public/locales/ar/views/system.json @@ -0,0 +1,149 @@ +{ + "documentTitle": { + "cameras": "إحصاءات الكاميرات - فرايجيت", + "storage": "إحصاءات التخزين - فرايجيت", + "general": "إحصاءات عامة - فرايجيت", + "enrichments": "إحصاء العمليات", + "logs": { + "frigate": "سجلات Frigate - Frigate", + "go2rtc": "Go2RTC سجلات - Frigate", + "nginx": "سجلات إنجنإكس - Frigate", + "websocket": "سجلات الرسائل" + } + }, + "metrics": "مقاييس النظام", + "logs": { + "download": { + "label": "تنزيل السجلات" + }, + "copy": { + "label": "نسخ إلى الحافظة", + "success": "نسخ السجلات إلى الحافظة", + "error": "تعذر نسخ السجلات إلى الحافظة" + }, + "type": { + "label": "النوع", + "timestamp": "الختم الزمني", + "message": "رسالة" + }, + "tips": "يتم الآن جلب السجلات من الخادم", + "websocket": { + "label": "الرسائل", + "pause": "إيقاف مؤقت", + "resume": "استئناف", + "filter": { + "all": "كافة المواضيع", + "topics": "المسارات", + "events": "الأحداث", + "reviews": "المراجعات", + "classification": "التصنيف", + "face_recognition": "التعرف على الوجه", + "camera_activity": "نشاط الكاميرا", + "system": "النظام", + "camera": "الكاميرا", + "all_cameras": "كافة الكاميرات" + } + }, + "toast": { + "error": { + "fetchingLogsFailed": "خطأ أثناء جلب السجلات: {{errorMessage}}", + "whileStreamingLogs": "خطأ أثناء تدفق السجلات: {{errorMessage}}" + } + } + }, + "title": "النظام", + "general": { + "hardwareInfo": { + "gpuEncoder": "مشفر ترميز GPU", + "gpuDecoder": "مفكك ترميز GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "مخرجات Vainfo", + "processOutput": "مخرجات العملية :", + "processError": "خطأ في العملية:", + "returnCode": "كود الاستجابة: {{code}}" + }, + "nvidiaSMIOutput": { + "title": "مخرجات Nvidia SMI", + "name": "الاسم: {{name}}", + "driver": "برنامج التشغيل: {{driver}}", + "cudaComputerCapability": "قدرة الحوسبة CUDA: {{cuda_compute}}", + "vbios": "" + }, + "closeInfo": { + "label": "إغلاق معلومات المعالج الرسومي" + }, + "copyInfo": { + "label": "نسخ معلومات المعالج الرسومي" + }, + "toast": { + "success": "تم نسخ معلومات المعالج الرسومي إلى الحافظة" + } + }, + "title": "معلومات الاجهزة المادية", + "gpuUsage": "مقدار استخدام GPU", + "gpuMemory": "ذاكرة GPU", + "gpuTemperature": "درجة حرارة الـ GPU", + "npuUsage": "معلومات وحدة معالجة الشبكة", + "npuMemory": "استخدام وحدة المعالجة العصبية", + "npuTemperature": "درجة حرارة الـ NPU", + "intelGpuWarning": { + "title": "تحذير إحصائيات معالج Intel الرسومي", + "description": "هذا خطأ برمي معروف في أدوات تقارير إحصائيات معالجات Intel الرسومية (intel_gpu_top)، حيث تتوقف الأداة عن العمل وتُظهر استهلاك المعالج الرسومي (GPU) بنسبة 0% بشكل متكرر، حتى في الحالات التي يعمل فيها تسريع العتاد وكشف الكائنات بشكل صحيح على المعالج الرسومي المدمج (iGPU). هذا ليس خطأً في برنامج فرايجيت (Frigate). يمكنك إعادة تشغيل الجهاز المضيف لحل المشكلة مؤقتاً والتأكد من أن المعالج الرسومي يعمل بشكل صحيح. علماً بأن هذا الخلل لا يؤثر على الأداء." + } + }, + "title": "لمحة عامة", + "detector": { + "title": "أجهزة الكشف", + "inferenceSpeed": "سرعة استنتاج الكاشف", + "temperature": "درجة حرارة الكاشف", + "cpuUsage": "كشف استخدام CPU", + "memoryUsage": "كشف استخدام الذاكرة", + "cpuUsageInformation": "المعالج المستخدم في تجهيز بيانات الإدخال والإخراج من وإلى نماذج الكشف. هذه القيمة لا تقيس استهلاك الاستنتاج (Inference)، حتى عند استخدام معالج رسومي (GPU) أو مسرع." + }, + "otherProcesses": { + "title": "عمليات أخرى", + "processCpuUsage": "استخدام وحدة المعالجة المركزية (CPU)", + "processMemoryUsage": "استخدام ذاكرة العملية" + } + }, + "storage": { + "title": "التخزين", + "overview": "نظرة عامة", + "recordings": { + "title": "التسجيلات", + "tips": "تمثل هذه القيمة إجمالي مساحة التخزين المستخدمة للتسجيلات في قاعدة بيانات Frigate. لا يتتبع Frigate استخدام مساحة التخزين لجميع الملفات الموجودة على القرص.", + "earliestRecording": "أقدم تسجيل متاح:" + }, + "shm": { + "warning": "حجم ذاكرة SHM الحالي البالغ {{total}} ميجابايت صغير جداً. يرجى زيادته إلى {{min_shm}} ميجابايت على الأقل.", + "frameLifetime": { + "description": "تمتلك كل كاميرا {{frames}} خانة (slots) للإطارات في الذاكرة المشتركة. عند أعلى معدل إطارات للكاميرا، يكون كل إطار متاحاً لمدة {{lifetime}} ثانية تقريباً قبل أن يتم الكتابة فوقه." + } + }, + "cameraStorage": { + "unused": { + "tips": "قد لا تمثل هذه القيمة بدقة المساحة الخالية المتاحة لبرنامج فرايجيت (Frigate) إذا كان لديك ملفات أخرى مخزنة على القرص بخلاف تسجيلات البرنامج نفسه. لا يقوم فرايجيت بتتبع استهلاك التخزين خارج نطاق تسجيلاته الخاصة." + } + } + }, + "cameras": { + "overview": "نظرة عامة", + "info": { + "unknown": "غير معروف" + }, + "connectionQuality": { + "fair": "متوسط", + "poor": "ضعيف", + "unusable": "غير قابل للاستخدام", + "fps": "معدل الإطارات", + "expectedFps": "معدل الإطارات المتوقع", + "reconnectsLastHour": "إعادات الاتصال (خلال الساعة الماضية)", + "stallsLastHour": "توقفات البث (خلال الساعة الماضية)" + } + }, + "stats": { + "detectIsSlow": "عملية الكشف {{detect}} بطيئة ({{speed}} مللي ثانية)", + "detectIsVerySlow": "عملية الكشف {{detect}} بطيئة جداً ({{speed}} مللي ثانية)" + } +} diff --git a/web/public/locales/bg/audio.json b/web/public/locales/bg/audio.json new file mode 100644 index 0000000..0380caf --- /dev/null +++ b/web/public/locales/bg/audio.json @@ -0,0 +1,397 @@ +{ + "babbling": "Бърборене", + "whispering": "Шепнене", + "laughter": "Смях", + "crying": "Плач", + "sigh": "Въздишка", + "singing": "Пеене", + "choir": "Хор", + "yodeling": "Йоделинг", + "mantra": "Мантра", + "child_singing": "Дете пее", + "rapping": "Рапиране", + "humming": "Тананикане", + "groan": "Пъшкане", + "whistling": "Подсвиркване", + "breathing": "Дишане", + "snoring": "Хъркане", + "cough": "Кашлица", + "throat_clearing": "Прокашляне", + "sneeze": "Кихане", + "sniff": "Подсмърчане", + "run": "Бяг", + "shuffle": "Разбъркване", + "footsteps": "Стъпки", + "chewing": "Дъвчене", + "biting": "Хапане", + "gargling": "Гаргара", + "burping": "Оригване", + "hiccup": "Хълцане", + "fart": "Пръдня", + "hands": "Ръце", + "finger_snapping": "Щтракане с пръсти", + "clapping": "Ръкопляскане", + "applause": "Овации", + "chatter": "Говорене", + "crowd": "Тълпа", + "children_playing": "Деца си играят", + "animal": "Животно", + "pets": "Домашен любимец", + "dog": "Куче", + "bark": "Лай", + "cat": "Котка", + "purr": "Мър", + "meow": "Мял", + "hiss": "Съскане", + "livestock": "Добитък", + "horse": "Кон", + "neigh": "Иииааа", + "moo": "Муу", + "cowbell": "Хлопка", + "pig": "Прасе", + "oink": "Грух", + "goat": "Коза", + "sheep": "Овца", + "chicken": "Пиле", + "cluck": "Ко-ко", + "cock_a_doodle_doo": "Кукуригу", + "turkey": "Пуйка", + "gobble": "Пулюпулю", + "duck": "Патка", + "quack": "Ква", + "goose": "Гъска", + "wild_animals": "Диви животни", + "roaring_cats": "Ревящи котки", + "roar": "Рев", + "bird": "Птица", + "pigeon": "Гълъб", + "coo": "Гуу", + "crow": "Гарван", + "caw": "Га", + "owl": "Сова", + "hoot": "Бухуу", + "flapping_wings": "Плясък на крила", + "dogs": "Кучета", + "rats": "Плъхове", + "mouse": "Мишка", + "insect": "Насекомо", + "cricket": "Щурец", + "mosquito": "Комар", + "fly": "Муха", + "buzz": "Бръм", + "frog": "Жаба", + "croak": "Квак", + "snake": "Змия", + "whale_vocalization": "Вик на кит", + "music": "Музика", + "musical_instrument": "Музикален инструмент", + "plucked_string_instrument": "Струнен инструмент", + "guitar": "Китара", + "electric_guitar": "Електрическа китара", + "bass_guitar": "Бас китара", + "acoustic_guitar": "Акустична китара", + "steel_guitar": "Метална китара", + "banjo": "Банджо", + "sitar": "Ситар", + "mandolin": "Мандолина", + "ukulele": "Укулеле", + "keyboard": "Клавир", + "piano": "Пиано", + "electric_piano": "Електрическо пиано", + "organ": "Орган", + "electronic_organ": "Електрически орган", + "hammond_organ": "Хамонд орган", + "synthesizer": "Синтезатор", + "sampler": "Семплър", + "percussion": "Перкуции", + "drum_kit": "Сет барабани", + "drum_machine": "Дръм машина", + "drum": "Барабан", + "drum_roll": "Туш", + "timpani": "Тимпани", + "tabla": "Табла", + "cymbal": "Цимбал", + "tambourine": "Тамбура", + "maraca": "Маракас", + "gong": "Гонг", + "vibraphone": "Вибрафон", + "orchestra": "Оркестър", + "brass_instrument": "Брас инструмент", + "french_horn": "Валдхорна", + "trumpet": "Тромпет", + "trombone": "Тромбон", + "bowed_string_instrument": "Струнен инструмент с лък", + "violin": "Цигулка", + "pizzicato": "Пицикато", + "cello": "Чело", + "double_bass": "Контрабас", + "wind_instrument": "Духов инструмент", + "flute": "Флейта", + "saxophone": "Саксофон", + "clarinet": "Кларинет", + "harp": "Арфа", + "bell": "Камбана", + "church_bell": "Църковна камбана", + "bicycle_bell": "Вело звънец", + "tuning_fork": "Камертон", + "harmonica": "Хармоника", + "accordion": "Акордеон", + "bagpipes": "Гайда", + "didgeridoo": "Диджириду", + "theremin": "Теремин", + "scratching": "Чесане", + "pop_music": "Поп музика", + "hip_hop_music": "Хип-хоп музика", + "beatboxing": "Бийтбокс", + "rock_music": "Рок музика", + "heavy_metal": "Хеви метъл", + "punk_rock": "Пънк рок", + "grunge": "Гръндж", + "progressive_rock": "Прогресивен рок", + "rock_and_roll": "Рок енд рол", + "psychedelic_rock": "Психаделичен рок", + "rhythm_and_blues": "Ритъм и блуз", + "soul_music": "Соул музика", + "reggae": "Реге", + "country": "Кънтри", + "swing_music": "Суинг музика", + "bluegrass": "Блуграс", + "funk": "Фънк", + "folk_music": "Фолк музика", + "middle_eastern_music": "Маанета", + "jazz": "Джаз", + "disco": "Диско", + "classical_music": "Класическа музика", + "opera": "Опера", + "electronic_music": "Електронна музика", + "house_music": "Хаус музика", + "techno": "Техно", + "dubstep": "Дъбстеп", + "drum_and_bass": "Дръм и бас", + "electronica": "Електроника", + "trance_music": "Транс музика", + "music_of_latin_america": "Латино музика", + "salsa_music": "Салса музика", + "flamenco": "Фламенко", + "blues": "Блус", + "music_for_children": "Детска музика", + "a_capella": "Акапела", + "music_of_africa": "Африканска музика", + "afrobeat": "Афроритъм", + "gospel_music": "Госпел", + "music_of_asia": "Азиатска музика", + "ska": "Ска", + "song": "Песен", + "background_music": "Фонова музика", + "jingle": "Джингъл", + "thunderstorm": "Гръмотевична буря", + "thunder": "Гръмотевица", + "water": "Вода", + "rain": "Дъжд", + "raindrop": "Дъждовна капка", + "stream": "Поток", + "waterfall": "Водопад", + "ocean": "Океан", + "waves": "Вълни", + "steam": "Пара", + "fire": "Огън", + "vehicle": "Превозно средство", + "boat": "Лодка", + "sailboat": "Ветроходна лодка", + "rowboat": "Гребна лодка", + "motorboat": "Моторна лодка", + "ship": "Кораб", + "motor_vehicle": "МПС", + "car": "Кола", + "car_alarm": "Аларма на кола", + "skidding": "Поднасяне", + "tire_squeal": "Скърцане на гуми", + "car_passing_by": "Преминаваща кола", + "race_car": "Състезателна кола", + "truck": "Камион", + "air_brake": "Въздушна спирачка", + "air_horn": "Тромба", + "reversing_beeps": "Звуков сигнал за задна скорост", + "ice_cream_truck": "Камион за сладолед", + "bus": "Автобус", + "police_car": "Полицейска кола", + "ambulance": "Линейка", + "fire_engine": "Пожарна кола", + "motorcycle": "Мотоциклет", + "traffic_noise": "Шум от трафик", + "rail_transport": "Железопътен транспорт", + "train": "Влак", + "train_whistle": "Влакова свирка", + "train_horn": "Влаков клаксон", + "railroad_car": "Вагон", + "train_wheels_squealing": "Скърцане на ЖП спирачки", + "subway": "Метро", + "aircraft": "Самолет", + "aircraft_engine": "Самолетен двигател", + "jet_engine": "Реактивен двигател", + "propeller": "Витло", + "helicopter": "Хеликоптер", + "fixed-wing_aircraft": "Самолет с твърди крила", + "bicycle": "Велосипед", + "skateboard": "Скейтборд", + "engine": "Двигател", + "dental_drill's_drill": "Зълболекарско борче", + "lawn_mower": "Косачка", + "chainsaw": "Моторен трион", + "engine_starting": "Стартиране на двигател", + "idling": "Празен ход", + "accelerating": "Ускорение", + "door": "Врата", + "doorbell": "Звънец", + "ding-dong": "Динг-донг", + "sliding_door": "Плъзгаща врата", + "slam": "Затръшване", + "knock": "Чук", + "tap": "Почукване", + "squeak": "Скръц", + "drawer_open_or_close": "Чекмедже отвори или затвори", + "dishes": "Чинии", + "cutlery": "Прибори за хранене", + "chopping": "Рязане", + "frying": "Пържене", + "microwave_oven": "Микровълнова фурна", + "blender": "Блендер", + "water_tap": "Кран за вода", + "speech": "Реч", + "yell": "Викане", + "bellow": "Под", + "whoop": "Уупс", + "pant": "Здъхване", + "stomach_rumble": "Къркорене на стомах", + "heartbeat": "Сърцебиене", + "scream": "Вик", + "snicker": "Хихикане", + "chant": "Скандиране", + "synthetic_singing": "Синтетично Пеене", + "grunt": "Грухтене", + "wheeze": "Хриптене", + "gasp": "Издихание", + "snort": "Смъркане", + "heart_murmur": "Сърдечен Шум", + "cheering": "Радостни Викове", + "yip": "Джавкане", + "howl": "Вой", + "bow_wow": "Кучешки Вой", + "growling": "Ръмжене", + "whimper_dog": "Кучешко Скимтене", + "caterwaul": "Мяукане", + "clip_clop": "Копита", + "cattle": "Добитък", + "bleat": "Блеене", + "fowl": "Домашни Птици", + "honk": "Бибиткане", + "chirp": "Пиукане", + "squawk": "Кряскане/Грачене", + "patter": "Ромолене/Потупване", + "rattle": "Тракане", + "tapping": "Потупване", + "strum": "Звук от струни", + "zither": "Цитра", + "harpsichord": "Клавесин", + "snare_drum": "Малко барабанче", + "rimshot": "Римшот", + "bass_drum": "Голям барабан", + "hi_hat": "Фус", + "wood_block": "Парче дърво", + "electronic_dance_music": "Електронна денс музика", + "music_of_bollywood": "Музика от Боливут", + "traditional_music": "Традиционна Музика", + "soundtrack_music": "Саундтрак музика", + "lullaby": "Приспивна песен", + "video_game_music": "Музика от компютърна игра", + "christmas_music": "Коледна музика", + "dance_music": "Денс музика", + "wedding_music": "Сватбена музика", + "happy_music": "Радостна музика", + "sad_music": "Тъжна музика", + "tender_music": "Нежна музика", + "exciting_music": "Вълнуваща музика", + "angry_music": "Яростна музика", + "scary_music": "Страшна музика", + "wind": "Вятър", + "rustling_leaves": "Шумолящи листа", + "wind_noise": "Шум от вятър", + "rain_on_surface": "Дъжд на повърхност", + "crackle": "Пукане", + "emergency_vehicle": "Кола на спешна помощ", + "engine_knocking": "Чукане от двигател", + "cupboard_open_or_close": "Отваряне или затваряне на шкаф", + "sink": "Мивка", + "bathtub": "Вана", + "hair_dryer": "Сешоар", + "toilet_flush": "Пускане на вода в тоалетна", + "toothbrush": "Четка за зъби", + "electric_toothbrush": "Електрическа четка за зъби", + "vacuum_cleaner": "Прахосмукачка", + "zipper": "Цип", + "keys_jangling": "Дрънкане на ключове", + "coin": "Монета", + "scissors": "Ножица", + "electric_shaver": "Електрическа самобръсначка", + "shuffling_cards": "Разбъркване на карти", + "typing": "Пишене", + "typewriter": "Пишеща машина", + "computer_keyboard": "Клавиатура", + "writing": "Писане на ръка", + "alarm": "Аларма", + "telephone": "Телефон", + "telephone_bell_ringing": "Камбанен звук от телефон", + "ringtone": "Мелодия за звънене", + "telephone_dialing": "Набиране на телефон", + "dial_tone": "Набиране на цифра на телефон", + "busy_signal": "Сигнал заето", + "alarm_clock": "Алармен часовник", + "siren": "Сирена", + "civil_defense_siren": "Сирена на гражданска защита", + "buzzer": "Бъзър", + "smoke_detector": "Детектор за пушек", + "fire_alarm": "Пожарна аларма", + "whistle": "Свиркане", + "steam_whistle": "Парна свирка", + "mechanisms": "Механизми", + "clock": "Часовник", + "tick": "Тик", + "tick-tock": "Тиктакане", + "gears": "Зъбни колела", + "sewing_machine": "Шиеща машина", + "sound_effect": "Звуков ефект", + "tubular_bells": "Тръбни камбани", + "mallet_percussion": "Ударни чукчета", + "marimba": "Маримба", + "glockenspiel": "Металлофон", + "steelpan": "Пан барабан", + "string_section": "Струнна група", + "jingle_bell": "Звънче", + "chime": "Звънече", + "wind_chime": "Вятърен звън", + "singing_bowl": "Пееща купа", + "ambient_music": "Амбиентна музика", + "new-age_music": "Музика от ново поколение", + "vocal_music": "Вокална музика", + "gurgling": "Бълбукане", + "light_engine": "Лек двигател", + "medium_engine": "Среден двигател", + "heavy_engine": "Тежък двигател", + "ratchet": "Тресчотка", + "hammer": "Ръчен чук", + "pulleys": "Макари", + "mechanical_fan": "Механичен вентилатор", + "air_conditioning": "Климатик", + "cash_register": "Каса", + "printer": "Принтер", + "camera": "Камера", + "tools": "Инструменти", + "artillery_fire": "Артилерийски огън", + "boom": "Гръм", + "glass": "Стъкло", + "crack": "Пукнатина", + "wood": "Дърво", + "silence": "Тишина", + "liquid": "Течност", + "splash": "Разливам" +} diff --git a/web/public/locales/bg/common.json b/web/public/locales/bg/common.json new file mode 100644 index 0000000..fcecbc2 --- /dev/null +++ b/web/public/locales/bg/common.json @@ -0,0 +1,123 @@ +{ + "time": { + "today": "Днес", + "yesterday": "Вчера", + "month_one": "{{time}} месец", + "month_other": "{{time}} месеца", + "day_one": "{{time}} ден", + "day_other": "{{time}} дни", + "hour_one": "{{time}} час", + "hour_other": "{{time}} часа", + "minute_one": "{{time}} минута", + "minute_other": "{{time}} минути", + "second_one": "{{time}} секунда", + "second_other": "{{time}} секунди", + "year_one": "{{time}} година", + "year_other": "{{time}} години", + "justNow": "Сега", + "last7": "Изминалите 7 дни", + "last14": "Изминалите 14 дни", + "last30": "Изминалите 30 дни", + "thisWeek": "Тази седмица", + "lastWeek": "Предходната седмица", + "thisMonth": "Този месец", + "lastMonth": "Предходния месец", + "5minutes": "5 минути", + "10minutes": "10 минути", + "30minutes": "30 минути", + "1hour": "1 час", + "12hours": "12 часа", + "24hours": "24 часа", + "pm": "pm", + "am": "am", + "yr": "г", + "d": "{{time}}д", + "h": "{{time}}ч", + "formattedTimestamp": { + "12hour": "МММ д, ч:мм:сс ааа", + "24hour": "МММ д, ЧЧ:мм:сс" + }, + "formattedTimestamp2": { + "12hour": "ММ/дд ч:мм:сса", + "24hour": "д МММ ЧЧ:мм:сс" + }, + "formattedTimestampHourMinute": { + "12hour": "ч:мм ааа", + "24hour": "ЧЧ:мм" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "ч:мм:сс ааа", + "24hour": "ЧЧ:мм:сс" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "МММ д, ч:мм ааа", + "24hour": "МММ д, ЧЧ:мм" + }, + "formattedTimestampMonthDayYear": { + "12hour": "МММ д, гггг", + "24hour": "МММ д, гггг" + }, + "ago": "Преди {{timeAgo}}", + "untilForTime": "До {{time}}", + "untilForRestart": "Докато Frigate рестартира.", + "untilRestart": "До рестарт", + "mo": "{{time}}мес", + "m": "{{time}}м", + "s": "{{time}}с", + "never": "Никога", + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa" + } + }, + "button": { + "apply": "Приложи", + "reset": "Нулиране", + "done": "Готово", + "disabled": "Изключено", + "save": "Запази", + "saving": "Запазване…", + "cancel": "Отказ", + "close": "Затвори", + "copy": "Копирай", + "edit": "Редактирай", + "copyCoordinates": "Копирай координати", + "delete": "Изтриване", + "yes": "Да", + "download": "Изтегляне", + "enabled": "Включено", + "history": "История", + "back": "Назад", + "fullscreen": "Цял екран", + "exitFullscreen": "Излез от цял екран", + "pictureInPicture": "Картина в картина", + "twoWayTalk": "Двупосочни разговори", + "cameraAudio": "Аудио на камерата", + "on": "Включено", + "off": "Изключено", + "no": "Не", + "info": "Информация", + "suspended": "Спряно", + "unsuspended": "Възобновяване", + "play": "Пускане", + "unselect": "Демаркиране", + "export": "Експортиране", + "deleteNow": "Изтрии сега", + "next": "Следващ", + "disable": "Изключи", + "enable": "Включи" + }, + "menu": { + "live": { + "title": "Наживо", + "cameras": { + "count_one": "{{count}} камера", + "count_other": "{{count}} камери" + } + } + }, + "label": { + "back": "Върни се" + }, + "selectItem": "Избери {{item}}", + "readTheDocumentation": "Прочетете документацията" +} diff --git a/web/public/locales/bg/components/auth.json b/web/public/locales/bg/components/auth.json new file mode 100644 index 0000000..094cd71 --- /dev/null +++ b/web/public/locales/bg/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Потребителско име", + "password": "Парола", + "login": "Вход", + "firstTimeLogin": "Опитвате да влезете за първи път? Данните за вход са разпечатани в логовете на Frigate.", + "errors": { + "usernameRequired": "Потребителското име е задължително", + "passwordRequired": "Паролата е задължителна", + "rateLimit": "Надхвърлен брой опити. Моля Опитайте по-късно.", + "loginFailed": "Неуспешен вход", + "unknownError": "Неизвестна грешка. Поля проверете логовете.", + "webUnknownError": "Неизвестна грешка. Поля проверете изхода в конзолата." + } + } +} diff --git a/web/public/locales/bg/components/camera.json b/web/public/locales/bg/components/camera.json new file mode 100644 index 0000000..cad1127 --- /dev/null +++ b/web/public/locales/bg/components/camera.json @@ -0,0 +1,45 @@ +{ + "group": { + "add": "Добави група за камери", + "label": "Групи камери", + "edit": "Редактирай група за камери", + "delete": { + "label": "Изтрий група за камери", + "confirm": { + "title": "Потвърди изтриването", + "desc": "Сигурни ли сте, че искате да изтриете група {{name}}?" + } + }, + "name": { + "label": "Име", + "placeholder": "Въведете име…", + "errorMessage": { + "mustLeastCharacters": "Името на групата камери трябва да е поне 2 символа.", + "exists": "Групата камери вече съществува.", + "nameMustNotPeriod": "Името на групата камери не трябва да съръжа точка.", + "invalid": "Невалидно име за група камери." + } + }, + "cameras": { + "label": "Камери", + "desc": "Изберете камери за тази група." + }, + "icon": "Икона", + "success": "Група камери ({{name}}) беше записана.", + "camera": { + "setting": { + "stream": "Поток", + "placeholder": "Изберете поток", + "streamMethod": { + "label": "Метод на стийминг", + "placeholder": "Избери метод на стрийминг", + "method": { + "noStreaming": { + "label": "Без стрийминг" + } + } + } + } + } + } +} diff --git a/web/public/locales/bg/components/dialog.json b/web/public/locales/bg/components/dialog.json new file mode 100644 index 0000000..6a2d356 --- /dev/null +++ b/web/public/locales/bg/components/dialog.json @@ -0,0 +1,19 @@ +{ + "export": { + "name": { + "placeholder": "Име на експорта" + }, + "time": { + "lastHour_one": "Последният час", + "lastHour_other": "Последните {{count}} часа" + }, + "select": "Избери" + }, + "restart": { + "title": "Сигурен ли сте, че искате да рестартирате Frigate?", + "button": "Рестартирай", + "restarting": { + "title": "Frigare се рестартира" + } + } +} diff --git a/web/public/locales/bg/components/filter.json b/web/public/locales/bg/components/filter.json new file mode 100644 index 0000000..afb81bb --- /dev/null +++ b/web/public/locales/bg/components/filter.json @@ -0,0 +1,39 @@ +{ + "filter": "Филтър", + "cameras": { + "all": { + "title": "Всички камери", + "short": "Камери" + } + }, + "logSettings": { + "allLogs": "Всички логове" + }, + "subLabels": { + "all": "Всички подетикети", + "label": "Подетикети" + }, + "labels": { + "all": { + "title": "Всички етикети", + "short": "Етикети" + } + }, + "zones": { + "all": { + "title": "Всички зони", + "short": "Зони" + } + }, + "dates": { + "all": { + "title": "Всички дати", + "short": "Дати" + } + }, + "more": "Още филтри", + "reset": { + "label": "Рестартирай филтрите по подразбиране" + }, + "timeRange": "Времеви диапазон" +} diff --git a/web/public/locales/bg/components/icons.json b/web/public/locales/bg/components/icons.json new file mode 100644 index 0000000..a978fa3 --- /dev/null +++ b/web/public/locales/bg/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Изберете иконка", + "search": { + "placeholder": "Потърси за икона…" + } + } +} diff --git a/web/public/locales/bg/components/input.json b/web/public/locales/bg/components/input.json new file mode 100644 index 0000000..9bd41d6 --- /dev/null +++ b/web/public/locales/bg/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Свали видео", + "toast": { + "success": "Вашето видео за преглеждане почна да се изтегля." + } + } + } +} diff --git a/web/public/locales/bg/components/player.json b/web/public/locales/bg/components/player.json new file mode 100644 index 0000000..706f819 --- /dev/null +++ b/web/public/locales/bg/components/player.json @@ -0,0 +1,53 @@ +{ + "noPreviewFoundFor": "Не е намерен предварителен преглед за {{cameraName}}", + "stats": { + "latency": { + "short": { + "value": "{{seconds}} сек", + "title": "Закъснение" + }, + "title": "Закъснение:", + "value": "{{seconds}} секунди" + }, + "streamType": { + "title": "Тип поток:", + "short": "Тип" + }, + "bandwidth": { + "title": "Трафик:", + "short": "Трафик" + }, + "totalFrames": "Общо кадри:", + "droppedFrames": { + "title": "Пропуснати кадри:", + "short": { + "title": "Пропуснати", + "value": "{{droppedFrames}} кадри" + } + }, + "decodedFrames": "Декодирани кадри:", + "droppedFrameRate": "Честота на пропуснатати кадри:" + }, + "streamOffline": { + "desc": "Не са получени кадри в потока за разпознаване на {{cameraName}}, проверете лог файловете за грешки", + "title": "Потокът е офлайн" + }, + "submitFrigatePlus": { + "title": "Да се изпрати ли този кадър към Frigate+?", + "submit": "Изпрати", + "previewError": "Не можe да се зареди предварителен преглед на моментната снимка. Записът може да не е наличен в момента." + }, + "noPreviewFound": "Не е намерен предварителен преглед", + "noRecordingsFoundForThisTime": "За това време не са намерени записи", + "livePlayerRequiredIOSVersion": "За този тип поток на живо се изисква iOS 17.1 или по-нова версия.", + "cameraDisabled": "Камерата е изключена", + "toast": { + "success": { + "submittedFrigatePlus": "Успешно изпратен кадър към Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Неуспешно изпратен кадър към Frigate+" + } + }, + "cameraOff": "Камерата е изключена" +} diff --git a/web/public/locales/bg/config/cameras.json b/web/public/locales/bg/config/cameras.json new file mode 100644 index 0000000..bdace6a --- /dev/null +++ b/web/public/locales/bg/config/cameras.json @@ -0,0 +1,69 @@ +{ + "label": "Конфигурация на камерата", + "timestamp_style": { + "label": "Стил на времева щампа", + "format": { + "label": "Формат на времева щампа" + }, + "color": { + "red": { + "label": "Червено", + "description": "Червен компонент (0-255) за времева щампа." + }, + "green": { + "description": "Зелен компонент (0-255) за времева щампа.", + "label": "Зелено" + }, + "blue": { + "description": "Син компонент (0-255) за времева щампа.", + "label": "Синьо" + }, + "label": "Цвят на времева щампа", + "description": "RGB стойности за текста на времева щампа (всички стойности 0-255)." + }, + "thickness": { + "description": "Дебелина на текстовата линия за времева щампа.", + "label": "Дебелина на времева щампа" + }, + "effect": { + "label": "Ефект на времева щампа", + "description": "Визуален ефект на времева щампа (без, плътен, сянка)." + }, + "position": { + "label": "Позиция на времева щампа", + "description": "Позиция за времева щампа на снимката (гл/гд/дл/дд)." + } + }, + "audio_transcription": { + "label": "Аудио транскрипция", + "live_enabled": { + "label": "Транскрипция на живо" + } + }, + "snapshots": { + "quality": { + "label": "Качество на моментната снимка" + }, + "required_zones": { + "label": "Задължителни зони", + "description": "Зони в които обект трябва да влезе за да се запази моментна снимка." + }, + "height": { + "label": "Височина на моментната снимка" + }, + "retain": { + "default": { + "description": "Дни по подразбиране за задържане на моментните снимки." + } + } + }, + "semantic_search": { + "label": "Семантично търсене", + "triggers": { + "label": "Спусък", + "friendly_name": { + "label": "Удобно име" + } + } + } +} diff --git a/web/public/locales/bg/config/global.json b/web/public/locales/bg/config/global.json new file mode 100644 index 0000000..85376b2 --- /dev/null +++ b/web/public/locales/bg/config/global.json @@ -0,0 +1,151 @@ +{ + "auth": { + "label": "Автентикация", + "session_length": { + "label": "Продължителност на сесията" + } + }, + "version": { + "label": "Текуща версия на конфигурацията", + "description": "Версия на активната конфигурация. Помага за проследяване на промени от миграция или форматиране." + }, + "safe_mode": { + "label": "Безопасен режим", + "description": "При избор, Frigate ще стартира в безопасен режим за отстраняване на неизправности." + }, + "environment_vars": { + "label": "Променливи", + "description": "Параметри за стартиране на Frigate в Home Assistant OS. Non-HAOS потребителите трябва да използват Docker конфигурация." + }, + "logger": { + "label": "Логове", + "default": { + "label": "Ниво на логовете" + } + }, + "timestamp_style": { + "label": "Стил на времева щампа", + "format": { + "label": "Формат на времева щампа" + }, + "color": { + "red": { + "label": "Червено", + "description": "Червен компонент (0-255) за времева щампа." + }, + "green": { + "description": "Зелен компонент (0-255) за времева щампа.", + "label": "Зелено" + }, + "blue": { + "description": "Син компонент (0-255) за времева щампа.", + "label": "Синьо" + }, + "label": "Цвят на времева щампа", + "description": "RGB стойности за текста на времева щампа (всички стойности 0-255)." + }, + "thickness": { + "description": "Дебелина на текстовата линия за времева щампа.", + "label": "Дебелина на времева щампа" + }, + "effect": { + "label": "Ефект на времева щампа", + "description": "Визуален ефект на времева щампа (без, плътен, сянка)." + }, + "position": { + "label": "Позиция на времева щампа", + "description": "Позиция за времева щампа на снимката (гл/гд/дл/дд)." + } + }, + "audio_transcription": { + "label": "Аудио транскрипция", + "enabled": { + "label": "Включи аудио транскрипцията" + }, + "language": { + "label": "Език на транскрипция" + }, + "device": { + "label": "Устройство за транскрипция" + }, + "model_size": { + "label": "Големина на модела" + }, + "live_enabled": { + "label": "Транскрипция на живо" + } + }, + "snapshots": { + "quality": { + "label": "Качество на моментната снимка" + }, + "required_zones": { + "label": "Задължителни зони", + "description": "Зони в които обект трябва да влезе за да се запази моментна снимка." + }, + "height": { + "label": "Височина на моментната снимка" + }, + "retain": { + "default": { + "description": "Дни по подразбиране за задържане на моментните снимки." + } + } + }, + "classification": { + "label": "Класификация на обекти", + "bird": { + "label": "Конфигурация за класификация на птици", + "enabled": { + "label": "Класификация на птици", + "description": "Клацификация на птици (Вкл./Изкл.)." + }, + "threshold": { + "label": "Минимален резултат" + } + }, + "custom": { + "label": "Потребителски класификационни модели", + "enabled": { + "label": "Включи модел", + "description": "Потребителски класификационнен модел (Вкл./Изкл.)." + }, + "name": { + "label": "Име на модел" + }, + "save_attempts": { + "label": "Опити за запазване" + }, + "state_config": { + "motion": { + "label": "Изпълни при движение" + }, + "interval": { + "label": "Интервал за класификация" + } + } + } + }, + "semantic_search": { + "label": "Семантично търсене", + "enabled": { + "label": "Включи семантично търсене", + "description": "Семантично търсене (Вкл./Изкл.)." + }, + "reindex": { + "label": "Реиндексирай при стартиране" + }, + "model_size": { + "label": "Големина на модел" + }, + "device": { + "label": "Устройство" + }, + "triggers": { + "label": "Спусък", + "friendly_name": { + "label": "Удобно име" + } + } + } +} diff --git a/web/public/locales/bg/config/groups.json b/web/public/locales/bg/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/bg/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/bg/config/validation.json b/web/public/locales/bg/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/bg/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/bg/objects.json b/web/public/locales/bg/objects.json new file mode 100644 index 0000000..79485a7 --- /dev/null +++ b/web/public/locales/bg/objects.json @@ -0,0 +1,29 @@ +{ + "animal": "Животно", + "dog": "Куче", + "bark": "Лай", + "cat": "Котка", + "horse": "Кон", + "goat": "Коза", + "sheep": "Овца", + "bird": "Птица", + "mouse": "Мишка", + "keyboard": "Клавир", + "vehicle": "Превозно средство", + "boat": "Лодка", + "car": "Кола", + "bus": "Автобус", + "motorcycle": "Мотоциклет", + "train": "Влак", + "bicycle": "Велосипед", + "skateboard": "Скейтборд", + "door": "Врата", + "blender": "Блендер", + "person": "Човек", + "sink": "Мивка", + "hair_dryer": "Сешоар", + "toothbrush": "Четка за зъби", + "scissors": "Ножица", + "clock": "Часовник", + "airplane": "Самолет" +} diff --git a/web/public/locales/bg/views/chat.json b/web/public/locales/bg/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/bg/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/bg/views/classificationModel.json b/web/public/locales/bg/views/classificationModel.json new file mode 100644 index 0000000..493cf18 --- /dev/null +++ b/web/public/locales/bg/views/classificationModel.json @@ -0,0 +1,14 @@ +{ + "documentTitle": "Модели за класификация - Frigate", + "description": { + "invalidName": "Невалидно име. Имената могат да съдържат единствено: букви, числа, празни места, долни черти и тирета." + }, + "details": { + "scoreInfo": "Резултатът представлява средната степен на увереност в класификацията при всички засечки на този обект." + }, + "wizard": { + "step1": { + "classificationAttribute": "Атрибут" + } + } +} diff --git a/web/public/locales/bg/views/configEditor.json b/web/public/locales/bg/views/configEditor.json new file mode 100644 index 0000000..955fb99 --- /dev/null +++ b/web/public/locales/bg/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Настройки на конфигурацията - Frigate", + "configEditor": "Конфигуратор", + "safeConfigEditor": "Конфигуратор (Safe Mode)", + "safeModeDescription": "Frigate е в режим \"Safe Mode\" тъй като конфигурацията не минава проверките за валидност.", + "copyConfig": "Копирай Конфигурацията", + "saveAndRestart": "Запази и Рестартирай", + "saveOnly": "Запази", + "confirm": "Изход без запис?", + "toast": { + "success": { + "copyToClipboard": "Конфигурацията е копирана." + }, + "error": { + "savingError": "Грешка при запис на конфигурацията" + } + } +} diff --git a/web/public/locales/bg/views/events.json b/web/public/locales/bg/views/events.json new file mode 100644 index 0000000..bf0c0a7 --- /dev/null +++ b/web/public/locales/bg/views/events.json @@ -0,0 +1,19 @@ +{ + "timeline.aria": "Избери хронология", + "timeline": "Хронология", + "calendarFilter": { + "last24Hours": "Последните 24 часа" + }, + "events": { + "label": "Събития", + "aria": "Избери събития", + "noFoundForTimePeriod": "Няма намерени събития за този времеви период." + }, + "allCameras": "Всички камери", + "alerts": "Известия", + "detections": "Засичания", + "motion": { + "label": "Движение" + }, + "camera": "Камера" +} diff --git a/web/public/locales/bg/views/explore.json b/web/public/locales/bg/views/explore.json new file mode 100644 index 0000000..84a8060 --- /dev/null +++ b/web/public/locales/bg/views/explore.json @@ -0,0 +1,73 @@ +{ + "details": { + "item": { + "tips": { + "mismatch_one": "{{count}} недостъпен обект беше открит и включен в този елемент за преглед. Тези обекти или не са квалифицирани като предупреждение или откриване, или вече са били изчистени/изтрити.", + "mismatch_other": "{{count}} недостъпни обекта бяха открити и включени в този елемент за преглед. Тези обекти или не са квалифицирани като предупреждение или откриване, или вече са били изчистени/изтрити." + } + }, + "editLPR": { + "title": "Редактиране на регистрационния номер" + }, + "editAttributes": { + "title": "Редактиране на атрибутите" + }, + "topScore": { + "label": "Най-силен резултат" + }, + "estimatedSpeed": "Естимирана скорост", + "objects": "Обекти", + "camera": "Камера", + "zones": "Зони", + "timestamp": "Времева щампа", + "button": { + "findSimilar": "Намери подобни", + "regenerate": { + "title": "Регенерирай", + "label": "Регенерирай описанието на следените обекти" + } + }, + "description": { + "label": "Описание", + "placeholder": "Описание на следените обекти" + } + }, + "trackedObjectsCount_one": "{{count}} проследен обект ", + "trackedObjectsCount_other": "{{count}} проследени обекта ", + "documentTitle": "Разгледай - Фригейт", + "generativeAI": "Генеративен Изкъствен Интелект", + "itemMenu": { + "downloadSnapshot": { + "aria": "Сваляне на моментна снимка/кадър" + }, + "viewTrackingDetails": { + "label": "Виж детайли за следенето", + "aria": "Покажи детайли за следенето" + }, + "findSimilar": { + "label": "Намери подобни" + }, + "submitToPlus": { + "label": "Изпрати към Frigate+", + "aria": "Изпрати към Frigate Plus" + }, + "viewInHistory": { + "label": "Виж в история", + "aria": "Виж в история" + }, + "more": { + "aria": "Повече" + } + }, + "dialog": { + "confirmDelete": { + "title": "Потвърди изтриване" + } + }, + "aiAnalysis": { + "title": "AI Анализ" + }, + "concerns": { + "label": "Притеснения" + } +} diff --git a/web/public/locales/bg/views/exports.json b/web/public/locales/bg/views/exports.json new file mode 100644 index 0000000..344695c --- /dev/null +++ b/web/public/locales/bg/views/exports.json @@ -0,0 +1,74 @@ +{ + "documentTitle": "Експорт - Frigate", + "search": "Търси", + "noExports": "Няма намерени експорти", + "deleteExport": "Изтрий експорт", + "deleteExport.desc": "Сигурни ли сте, че искате да изтриете {{exportName}}?", + "editExport": { + "title": "Преименувай експорт", + "desc": "Въведете ново име за този експорт.", + "saveExport": "Запази експорт" + }, + "tooltip": { + "shareExport": "Сподели експорт", + "downloadVideo": "Свали видео", + "editName": "Редактирай име", + "deleteExport": "Изтрий експорт" + }, + "toast": { + "error": { + "renameExportFailed": "Неуспешно преименуване на експорт: {{errorMessage}}", + "assignCaseFailed": "Неуспешно обновяване на възложен случай: {{errorMessage}}", + "caseSaveFailed": "Неуспешно запазен случай: {{errorMessage}}", + "caseDeleteFailed": "Неуспешно изтрит случай: {{errorMessage}}" + } + }, + "deleteCase": { + "desc": "Сигурен ли сте, че искате да изтриете {{caseName}}?", + "label": "Изтрии случай" + }, + "caseDialog": { + "nameLabel": "Име на случай", + "descriptionLabel": "Описание" + }, + "toolbar": { + "editCase": "Редактирай случай", + "deleteCase": "Изтрии случай" + }, + "caseCard": { + "emptyCase": "Все още няма експорти" + }, + "jobCard": { + "defaultName": "{{camera}} експортиране", + "queued": "На опашката", + "running": "В ход", + "preparing": "Подготвяне", + "copying": "Копиране", + "encoding": "Енкодиране", + "encodingRetry": "Енкодиране (повторно)", + "finalizing": "Финализиране" + }, + "caseView": { + "noDescription": "Без описание", + "createdAt": "Създаден {{value}}", + "exportCount_one": "1 експорт", + "exportCount_other": "{{count}} експорти", + "cameraCount_one": "1 камера", + "cameraCount_other": "{{count}} камери", + "showMore": "Покажи повече", + "showLess": "Покажи по-малко" + }, + "bulkActions": { + "delete": "Изтрии", + "deleteNow": "Изтрии сега" + }, + "bulkDelete": { + "title": "Изтрии експорти", + "desc_one": "Сигурни ли сте , че искате да изтриете {{count}} експорта?" + }, + "bulkToast": { + "success": { + "delete": "Успешно изтрити експорти" + } + } +} diff --git a/web/public/locales/bg/views/faceLibrary.json b/web/public/locales/bg/views/faceLibrary.json new file mode 100644 index 0000000..d00942b --- /dev/null +++ b/web/public/locales/bg/views/faceLibrary.json @@ -0,0 +1,22 @@ +{ + "deleteFaceAttempts": { + "desc_one": "Сигурни ли сте, че искате да изтриете {{count}} лице? Това действие не може да бъде отменено.", + "desc_other": "Сигурни ли сте, че искате да изтриете {{count}} лица? Това действие не може да бъде отменено." + }, + "toast": { + "success": { + "deletedFace_one": "Успешно изтрито {{count}} лице.", + "deletedFace_other": "Успешно изтрити {{count}} лица.", + "deletedName_one": "{{count}} лице бе изтрито успешно.", + "deletedName_other": "{{count}} лица бяха изтрити успешно." + } + }, + "description": { + "addFace": "Добавете нова колекция във библиотеката за лица при качването на първата ви снимка.", + "placeholder": "Напишете име за тази колекция", + "invalidName": "Невалидно име. Имената могат да съдържат единствено: букви, числа, празни места, долни черти и тирета." + }, + "details": { + "timestamp": "Времева щампа" + } +} diff --git a/web/public/locales/bg/views/live.json b/web/public/locales/bg/views/live.json new file mode 100644 index 0000000..c10906c --- /dev/null +++ b/web/public/locales/bg/views/live.json @@ -0,0 +1,84 @@ +{ + "stream": { + "playInBackground": { + "tips": "Активирайте тази опция, за да продължите поточното предаване, когато плейърът е скрит." + } + }, + "cameraAudio": { + "enable": "Включи звука на камерата", + "disable": "Изключи звука на камерата" + }, + "twoWayTalk": { + "enable": "Включи двупосочен разговор", + "disable": "Изключи двупосочен разговор" + }, + "ptz": { + "move": { + "clickMove": { + "enable": "Включи кликване за преместване", + "disable": "Изключи кликване за преместване", + "label": "Кликнете в центъра на кадъра за да центрирате камерата" + } + } + }, + "muteCameras": { + "enable": "Заглушаване на всички камери", + "disable": "Включване на звука на всички камери" + }, + "recording": { + "enable": "Включи запис", + "disable": "Изключи запис", + "disabledInConfig": "\"Записване\" трябва първо да се вкючи през настройките за тази камера." + }, + "snapshots": { + "enable": "Включи моментни снимки", + "disable": "Изключи моментни снимки" + }, + "audioDetect": { + "enable": "Включи аудио разпознаване", + "disable": "Изключи аудио разпознаване" + }, + "camera": { + "enable": "Включи камера", + "disable": "Изключи камера", + "turnOn": "Включване на камера", + "turnOff": "Изключване на камера" + }, + "detect": { + "enable": "Включи разпознаване", + "disable": "Изключи разпознаване" + }, + "autotracking": { + "enable": "Включи автоматично проследяване", + "disable": "Изключи автоматично проследяване" + }, + "streamStats": { + "enable": "Показване на статистика на потока", + "disable": "Скриване на статистиката на потока" + }, + "manualRecording": { + "playInBackground": { + "desc": "Активирайте тази опция, за да продължите поточното предаване, когато плейърът е скрит." + }, + "showStats": { + "desc": "Активирайте тази опция, за да покажете статистиката на потока като наслагване върху канала на камерата." + }, + "recordDisabledTips": "Тъй като записът е изключен или ограничен в конфигурацията за тази камера, ще бъде запазена само моментна снимка." + }, + "cameraSettings": { + "cameraEnabled": "Камерата е включена" + }, + "documentTitle": "Наживо - Frigate", + "documentTitle.withCamera": "{{camera}} - На живо - Фригейт", + "noCameras": { + "default": { + "buttonText": "Добави камера" + }, + "group": { + "title": "Няма камери в групата", + "description": "Тази група няма добавени или включени камери.", + "buttonText": "Управление на групите" + } + }, + "lowBandwidthMode": "Режим ограничена/бавна връзка" +} diff --git a/web/public/locales/bg/views/motionSearch.json b/web/public/locales/bg/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/bg/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/bg/views/recording.json b/web/public/locales/bg/views/recording.json new file mode 100644 index 0000000..e600636 --- /dev/null +++ b/web/public/locales/bg/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Филтър", + "export": "Експорт", + "calendar": "Календар", + "filters": "Филтри", + "toast": { + "error": { + "noValidTimeSelected": "Не е избран валиден времеви диапазон", + "endTimeMustAfterStartTime": "Крайното време трябва да бъде след началеният час" + } + } +} diff --git a/web/public/locales/bg/views/replay.json b/web/public/locales/bg/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/bg/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/bg/views/search.json b/web/public/locales/bg/views/search.json new file mode 100644 index 0000000..9246823 --- /dev/null +++ b/web/public/locales/bg/views/search.json @@ -0,0 +1,8 @@ +{ + "button": { + "save": "Запазване на търсенето" + }, + "search": "Търси", + "savedSearches": "Запазени търсения", + "searchFor": "Търсене за {{inputValue}}" +} diff --git a/web/public/locales/bg/views/settings.json b/web/public/locales/bg/views/settings.json new file mode 100644 index 0000000..08395e4 --- /dev/null +++ b/web/public/locales/bg/views/settings.json @@ -0,0 +1,20 @@ +{ + "masksAndZones": { + "motionMasks": { + "point_one": "{{count}} точка", + "point_other": "{{count}} точки" + }, + "objectMasks": { + "point_one": "{{count}} точка", + "point_other": "{{count}} точки" + }, + "zones": { + "point_one": "{{count}} точка", + "point_other": "{{count}} точки" + } + }, + "documentTitle": { + "default": "Настройки - Фригейт", + "authentication": "Настройки за сигурността - Фругейт" + } +} diff --git a/web/public/locales/bg/views/system.json b/web/public/locales/bg/views/system.json new file mode 100644 index 0000000..be1e23d --- /dev/null +++ b/web/public/locales/bg/views/system.json @@ -0,0 +1,10 @@ +{ + "stats": { + "healthy": "Системата е изправна" + }, + "documentTitle": { + "cameras": "Статистики за Камери - Фригейт", + "storage": "Статистика за паметта - Фригейт", + "general": "Обща Статистика - Frigate" + } +} diff --git a/web/public/locales/bs/audio.json b/web/public/locales/bs/audio.json new file mode 100644 index 0000000..4ee2ca9 --- /dev/null +++ b/web/public/locales/bs/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Govor", + "babbling": "Babavljanje", + "bicycle": "Kolo", + "yell": "Vik", + "bellow": "Bubanj", + "whoop": "Vrisak", + "whispering": "Šaputanje", + "laughter": "Smijeh", + "snicker": "Prijem", + "crying": "Plač", + "sigh": "Usklik", + "singing": "Pjevanje", + "choir": "Hors", + "yodeling": "Jodelanje", + "chant": "Pjevanje", + "mantra": "Mantra", + "child_singing": "Dječje pjevanje", + "synthetic_singing": "Sintetičko pjevanje", + "rapping": "Rap", + "humming": "Hum", + "groan": "Grokot", + "grunt": "Groktanje", + "whistling": "Pucanje", + "breathing": "Disanje", + "wheeze": "Pijuckanje", + "snoring": "Kicanje", + "gasp": "Udah", + "pant": "Pantanje", + "snort": "Snortanje", + "cough": "Kašljanje", + "throat_clearing": "Očišćavanje grla", + "sneeze": "Prašanje", + "sniff": "Njuhanje", + "run": "Trčanje", + "shuffle": "Prelazak", + "footsteps": "Koraci", + "chewing": "Zubljanje", + "biting": "Gubitak", + "gargling": "Peranje grla", + "stomach_rumble": "Grušenje", + "burping": "Puknutje", + "hiccup": "Kikot", + "fart": "Pucanje", + "hands": "Ruke", + "finger_snapping": "Prašanje prstiju", + "clapping": "Ključanje", + "heartbeat": "Taktilno", + "heart_murmur": "Šum srca", + "cheering": "Pozdrav", + "applause": "Pozdravljati", + "chatter": "Šaputanje", + "crowd": "Gomila", + "children_playing": "Dječja igra", + "animal": "Životinja", + "pets": "Hrana", + "dog": "Pas", + "bark": "Glavu", + "yip": "Jauk", + "howl": "Vijuk", + "bow_wow": "Vau vau", + "growling": "Gručenje", + "whimper_dog": "Pijuckanje psa", + "cat": "Mačka", + "purr": "Mrmor", + "meow": "Mjau", + "hiss": "Zujanje", + "caterwaul": "Krik", + "livestock": "Stoke", + "horse": "Konj", + "clip_clop": "Klik klok", + "neigh": "Kijanje", + "cattle": "Stoke", + "moo": "Muu", + "cowbell": "Kovčeg", + "pig": "Svinja", + "oink": "Roktanje", + "goat": "Koza", + "bleat": "Blejkanje", + "sheep": "Ovca", + "fowl": "Ptica", + "chicken": "Pilica", + "cluck": "Kukanje", + "cock_a_doodle_doo": "Kukavica", + "turkey": "Gusa", + "gobble": "Gubljanje", + "duck": "Kuja", + "quack": "Kvaka", + "goose": "Guska", + "honk": "Trubljenje", + "wild_animals": "Divlja životinja", + "roaring_cats": "Vrišćeći mački", + "roar": "Vrištanje", + "bird": "Ptica", + "chirp": "Pijuckanje", + "squawk": "Krik", + "pigeon": "Papiga", + "coo": "Kukanje", + "crow": "Vran", + "caw": "Vranje", + "owl": "Kukavica", + "hoot": "Kukavica", + "flapping_wings": "Mahanje krilima", + "dogs": "Psi", + "rats": "Štakori", + "mouse": "Miš", + "patter": "Topotanje", + "insect": "Insekt", + "cricket": "Cvrčak", + "mosquito": "Komarac", + "fly": "Muha", + "buzz": "Zujanje", + "frog": "Žaba", + "croak": "Kreketanje", + "snake": "Zmija", + "rattle": "Zveckanje", + "whale_vocalization": "Glasanje kita", + "music": "Muzika", + "musical_instrument": "Muzički instrument", + "plucked_string_instrument": "Plucked String Instrument", + "guitar": "Gitara", + "electric_guitar": "Električna gitara", + "bass_guitar": "Bas gitara", + "acoustic_guitar": "Akustična gitara", + "steel_guitar": "Steel gitara", + "tapping": "Tapping", + "strum": "Strum", + "banjo": "Bendžo", + "sitar": "Sitar", + "mandolin": "Mandolina", + "zither": "Citra", + "ukulele": "Ukulele", + "keyboard": "Klaviatura", + "piano": "Klavir", + "electric_piano": "Električni piano", + "organ": "Orgulje", + "electronic_organ": "Elektronski organ", + "hammond_organ": "Hammond organ", + "synthesizer": "Sintetizator", + "sampler": "Sampler", + "harpsichord": "Harfura", + "percussion": "Percuzija", + "drum_kit": "Set bubnjeva", + "drum_machine": "Mašina za bubnjeve", + "drum": "Bubanj", + "snare_drum": "Bubanj sa zavojima", + "rimshot": "Rimshot", + "drum_roll": "Bubanj za roliranje", + "bass_drum": "Bubanj za bas", + "timpani": "Timpani", + "tabla": "Tabla", + "cymbal": "Cimbale", + "hi_hat": "Hi-Hat", + "wood_block": "Drveni blok", + "tambourine": "Tamburina", + "maraca": "Maraka", + "gong": "Gong", + "tubular_bells": "Cijevasti zvoni", + "mallet_percussion": "Percusija s mljevima", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibrafon", + "steelpan": "Stelpan", + "orchestra": "Orkestar", + "brass_instrument": "Bronski instrument", + "french_horn": "Francuski rog", + "trumpet": "Truba", + "trombone": "Trombon", + "bowed_string_instrument": "Užadno strunski instrument", + "string_section": "Strunski sekcija", + "violin": "Violina", + "pizzicato": "Pizzicato", + "cello": "Celula", + "double_bass": "Dvostruki bas", + "wind_instrument": "Vjetreni instrument", + "flute": "Flauta", + "saxophone": "Saksafon", + "clarinet": "Klarinet", + "harp": "Harfa", + "bell": "Zvono", + "church_bell": "Crkveno zvono", + "jingle_bell": "Zvono za igračke", + "bicycle_bell": "Zvono za bicikl", + "tuning_fork": "Zvučnik", + "chime": "Zvono", + "wind_chime": "Vjetrenjac", + "harmonica": "Harmonika", + "accordion": "Akkordon", + "bagpipes": "Bogovina", + "didgeridoo": "Didgeridoo", + "theremin": "Teremin", + "singing_bowl": "Pjevni čaša", + "scratching": "Skrečing", + "pop_music": "Pop muzika", + "hip_hop_music": "Hip-Hop muzika", + "beatboxing": "Bitboksing", + "rock_music": "Rock muzika", + "heavy_metal": "Heavy metal", + "punk_rock": "Punk rock", + "grunge": "Grandž", + "progressive_rock": "Progressivni rock", + "rock_and_roll": "Rock and roll", + "psychedelic_rock": "Psihederički rock", + "rhythm_and_blues": "Ritam i blues", + "soul_music": "Soul glazba", + "reggae": "Rege", + "country": "Kantri", + "swing_music": "Swing glazba", + "bluegrass": "Bluegrass", + "funk": "Fank", + "folk_music": "Folklorno glazba", + "middle_eastern_music": "Glazba Bliskog istoka", + "jazz": "Džez", + "disco": "Disko", + "classical_music": "Klasična glazba", + "opera": "Opera", + "electronic_music": "Elektronska glazba", + "house_music": "House glazba", + "techno": "Tehno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum i bass", + "electronica": "Elektronika", + "electronic_dance_music": "Elektronska plesna glazba", + "ambient_music": "Ambient glazba", + "trance_music": "Trance glazba", + "music_of_latin_america": "Glazba Latinske Amerike", + "salsa_music": "Salsa glazba", + "flamenco": "Flamenko", + "blues": "Bluz", + "music_for_children": "Muzika za djecu", + "new-age_music": "Muzika novog doba", + "vocal_music": "Vokalna muzika", + "a_capella": "A Capella", + "music_of_africa": "Afrička muzika", + "afrobeat": "Afrobeat", + "christian_music": "Kršćanska muzika", + "gospel_music": "Gospel muzika", + "music_of_asia": "Azijatska muzika", + "carnatic_music": "Karnatička muzika", + "music_of_bollywood": "Bollywood muzika", + "ska": "Ska", + "traditional_music": "Tradicionalna muzika", + "independent_music": "Nezavisna muzika", + "song": "Pjesma", + "background_music": "Pozadinska muzika", + "theme_music": "Tema muzika", + "jingle": "Jingle", + "soundtrack_music": "Soundtrack muzika", + "lullaby": "Pjesma za uspavanje", + "video_game_music": "Muzika za video igre", + "christmas_music": "Božićna muzika", + "dance_music": "Dance muzika", + "wedding_music": "Venčanska glazba", + "happy_music": "Sretna glazba", + "sad_music": "Tužna glazba", + "tender_music": "Tenderna glazba", + "exciting_music": "Uzbudljiva glazba", + "angry_music": "Zlobna glazba", + "scary_music": "Strašna glazba", + "wind": "Vjetar", + "rustling_leaves": "Šum listova", + "wind_noise": "Šum vjetra", + "thunderstorm": "Grmljavina", + "thunder": "Grmljavac", + "water": "Voda", + "rain": "Kisa", + "raindrop": "Kap kise", + "rain_on_surface": "Kisa na površini", + "stream": "Tok", + "waterfall": "Padina", + "ocean": "Okean", + "waves": "Valovi", + "steam": "Par", + "gurgling": "Gurkanje", + "fire": "Vatra", + "crackle": "Krik", + "vehicle": "Vozilo", + "boat": "Brod", + "sailboat": "Jedrilica", + "rowboat": "Čamac", + "motorboat": "Motorni čamac", + "ship": "Brod", + "motor_vehicle": "Motorno vozilo", + "car": "Automobil", + "toot": "Zvuk klaksona", + "car_alarm": "Automobilski alarm", + "power_windows": "Električna prozora", + "skidding": "Klizanje", + "tire_squeal": "Krik kotača", + "car_passing_by": "Automobil prolazi", + "race_car": "Racing automobil", + "truck": "Kamion", + "air_brake": "Vazdušni kočnici", + "air_horn": "Vazdušni signal", + "reversing_beeps": "Zvukovi za odlazak unazad", + "ice_cream_truck": "Kamion za sladoled", + "bus": "Autobus", + "emergency_vehicle": "Hitni vozilo", + "police_car": "Policijski automobil", + "ambulance": "Ambulansa", + "fire_engine": "Pogonski automobil", + "motorcycle": "Motocikl", + "traffic_noise": "Prometni šum", + "rail_transport": "Željeznički transport", + "train": "Vlak", + "train_whistle": "Vlakovni svirac", + "train_horn": "Vlakovni rohorn", + "railroad_car": "Željeznički vagon", + "train_wheels_squealing": "Vlakove točkove koje zavijaju", + "subway": "Metropolitena", + "aircraft": "Avion", + "aircraft_engine": "Avionski motor", + "jet_engine": "Reaktivni motor", + "propeller": "Vijak", + "helicopter": "Heličopter", + "fixed-wing_aircraft": "Avion s krilima", + "skateboard": "Skejtbord", + "engine": "Motor", + "light_engine": "Lagani motor", + "dental_drill's_drill": "Stomatološki bušilica", + "lawn_mower": "Kosilica", + "chainsaw": "Pilica", + "medium_engine": "Srednji motor", + "heavy_engine": "Teški motor", + "engine_knocking": "Kloping motora", + "engine_starting": "Pokretanje motora", + "idling": "Miris", + "accelerating": "Ubrzavanje", + "door": "Vrata", + "doorbell": "Zvonce", + "ding-dong": "Ding-dong", + "sliding_door": "Klizna vrata", + "slam": "Zatvaranje", + "knock": "Kucanje", + "tap": "Kucanje", + "squeak": "Krik", + "cupboard_open_or_close": "Otvorenje ili zatvaranje police", + "drawer_open_or_close": "Otvorenje ili zatvaranje vunca", + "dishes": "Posuđe", + "cutlery": "Posuđe za jelo", + "chopping": "Rezanje", + "frying": "Praženje", + "microwave_oven": "Mikrotalasna pećnica", + "blender": "Miksere", + "water_tap": "Kran", + "sink": "Lavabo", + "bathtub": "Kupatilo", + "hair_dryer": "Sušilac za kosu", + "toilet_flush": "Očišćavanje toaleta", + "toothbrush": "Šetka za zube", + "electric_toothbrush": "Električna šetka za zube", + "vacuum_cleaner": "Praškoljac", + "zipper": "Zatvarac", + "keys_jangling": "Ključevi koji se škripi", + "coin": "Novčanik", + "scissors": "Škare", + "electric_shaver": "Električni šavac", + "shuffling_cards": "Premještanje karata", + "typing": "Kucanje", + "typewriter": "Tipkovnica", + "computer_keyboard": "Računalna tipkovnica", + "writing": "Pisanje", + "alarm": "Alarm", + "telephone": "Telefon", + "telephone_bell_ringing": "Zvono telefona", + "ringtone": "Ton za poziv", + "telephone_dialing": "Pozivanje telefona", + "dial_tone": "Ton za poziv", + "busy_signal": "Signal zauzetosti", + "alarm_clock": "Budilica", + "siren": "Sirena", + "civil_defense_siren": "Sirena za civilnu zaštitu", + "buzzer": "Buzer", + "smoke_detector": "Detektor dima", + "fire_alarm": "Pozar alarm", + "foghorn": "Mlazni svirac", + "whistle": "Štiklja", + "steam_whistle": "Parni zvono", + "mechanisms": "Mehanizmi", + "ratchet": "Ratchet", + "clock": "Sat", + "tick": "Tik", + "tick-tock": "Tik-tak", + "gears": "Zupčanici", + "pulleys": "Koturači", + "sewing_machine": "Šitna mašina", + "mechanical_fan": "Mehanički ventilator", + "air_conditioning": "Klima uređaj", + "cash_register": "Gotovinska kasica", + "printer": "Štampač", + "camera": "Kamera", + "single-lens_reflex_camera": "Kamera s jednim objektivom", + "tools": "Alati", + "hammer": "Klubica", + "jackhammer": "Betonomijak", + "sawing": "Sečenje", + "filing": "Flešanje", + "sanding": "Šljokanje", + "power_tool": "Električni alat", + "drill": "Bušilica", + "explosion": "Eksplozija", + "gunshot": "Pucanj", + "machine_gun": "Automatska puška", + "fusillade": "Fusiladža", + "artillery_fire": "Pucanj topovima", + "cap_gun": "Pistolj za pucanje", + "fireworks": "Pucanje svjetiljki", + "firecracker": "Svjetiljka", + "burst": "Izbič", + "eruption": "Eruptija", + "boom": "Tutnjava", + "wood": "Drvo", + "chop": "Rezanje", + "splinter": "Razlomak", + "crack": "Klackanje", + "glass": "Staklo", + "chink": "Prozor", + "shatter": "Razbijanje", + "silence": "Tišina", + "sound_effect": "Zvučni efekt", + "environmental_noise": "Okolišni šum", + "static": "Statički šum", + "white_noise": "Bijeli šum", + "pink_noise": "Rumeni šum", + "television": "Televizija", + "radio": "Radio", + "field_recording": "Snimka na terenu", + "scream": "Vrisak", + "sodeling": "Sodeling", + "chird": "Chird", + "change_ringing": "Promjena zvona", + "shofar": "Šofar", + "liquid": "Tekućina", + "splash": "Pljuskanje", + "slosh": "Sloš", + "squish": "Škripanje", + "drip": "Kapanje", + "pour": "Prelivanje", + "trickle": "Tijek", + "gush": "Gusenje", + "fill": "Popunjavanje", + "spray": "Sprajanje", + "pump": "Pumpa", + "stir": "Miješanje", + "boiling": "Vrećenje", + "sonar": "Sonar", + "arrow": "Strela", + "whoosh": "Šum", + "thump": "Tupanje", + "thunk": "Tunk", + "electronic_tuner": "Elektronski tuner", + "effects_unit": "Jedinica efekata", + "chorus_effect": "Efekt korusa", + "basketball_bounce": "Košarkaški skok", + "bang": "Bum", + "slap": "Pljeska", + "whack": "Perc", + "smash": "Sprem", + "breaking": "Raskidanje", + "bouncing": "Skakanje", + "whip": "Škripanje", + "flap": "Klizanje", + "scratch": "Oštećenje", + "scrape": "Prašenje", + "rub": "Trenje", + "roll": "Kotrljanje", + "crushing": "Stiskanje", + "crumpling": "Sklapanje", + "tearing": "Raskidanje", + "beep": "Bip", + "ping": "Poziv", + "ding": "Ding", + "clang": "Zveket", + "squeal": "Cika", + "creak": "Škripa", + "rustle": "Šuškanje", + "whir": "Brujanje", + "clatter": "Tropot", + "sizzle": "Šištanje", + "clicking": "Klikanje", + "clickety_clack": "Klik-tak", + "rumble": "Rumbljanje", + "plop": "Pljus", + "hum": "Pjevušenje", + "zing": "Zing", + "boing": "Boing", + "crunch": "Crunch", + "sine_wave": "Sinusna valna", + "harmonic": "Harmonični", + "chirp_tone": "Tanjirasti ton", + "pulse": "Impuls", + "inside": "Unutra", + "outside": "Van", + "reverberation": "Reverberacija", + "echo": "Odjek", + "noise": "Šum", + "mains_hum": "Glavni šum", + "distortion": "Distorzija", + "sidetone": "Sidetone", + "cacophony": "Kacofonija", + "throbbing": "Tremor", + "vibration": "Vibracija" +} diff --git a/web/public/locales/bs/common.json b/web/public/locales/bs/common.json new file mode 100644 index 0000000..dba59d2 --- /dev/null +++ b/web/public/locales/bs/common.json @@ -0,0 +1,326 @@ +{ + "time": { + "untilForTime": "Do {{time}}", + "untilForRestart": "Do ponovnog pokretanja Frigate.", + "untilRestart": "Do ponovnog pokretanja", + "never": "Nikad", + "ago": "{{timeAgo}} prije", + "justNow": "Sada", + "today": "Danas", + "yesterday": "Jučer", + "last7": "Prošlih 7 dana", + "last14": "Prošlih 14 dana", + "last30": "Prošlih 30 dana", + "thisWeek": "Ova sedmica", + "lastWeek": "Prošla sedmica", + "thisMonth": "Ovaj mjesec", + "lastMonth": "Prošli mjesec", + "5minutes": "5 minuta", + "10minutes": "10 minuta", + "30minutes": "30 minuta", + "1hour": "1 sat", + "12hours": "12 sati", + "24hours": "24 sata", + "pm": "posle podne", + "am": "pre podne", + "yr": "{{time}} god", + "year_one": "{{time}} godina", + "year_few": "{{time}} godine", + "year_other": "{{time}} godina", + "mo": "{{time}} mjes", + "month_one": "{{time}} mjesec", + "month_few": "{{time}} mjeseca", + "month_other": "{{time}} mjeseci", + "d": "{{time}}d", + "day_one": "{{time}} dan", + "day_few": "{{time}} dana", + "day_other": "{{time}} dana", + "h": "{{time}}h", + "hour_one": "{{time}} sat", + "hour_few": "{{time}} sata", + "hour_other": "{{time}} sati", + "m": "{{time}}m", + "minute_one": "{{time}} minuta", + "minute_few": "{{time}} minute", + "minute_other": "{{time}} minuta", + "s": "{{time}}s", + "second_one": "{{time}} sekunda", + "second_few": "{{time}} sekunde", + "second_other": "{{time}} sekundi", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "inProgress": "U toku", + "invalidStartTime": "Neispravno početno vrijeme", + "invalidEndTime": "Neispravno krajnje vrijeme" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "kph" + }, + "length": { + "feet": "fut", + "meters": "metar" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hour", + "mbph": "MB/hour", + "gbph": "GB/hour" + } + }, + "label": { + "back": "Povratak", + "hide": "Sakrij {{item}}", + "show": "Prikaži {{item}}", + "ID": "ID", + "none": "Nijedan", + "all": "Sve", + "other": "Ostalo" + }, + "list": { + "two": "{{0}} i {{1}}", + "many": "{{items}}, i {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opcionalno", + "internalID": "Unutarnji ID koji Frigate koristi u konfiguraciji i bazi podataka" + }, + "button": { + "add": "Dodaj", + "apply": "Primijeni", + "applying": "Primjenjuje se…", + "reset": "Resetuj", + "undo": "Poništi", + "done": "Gotovo", + "enabled": "Omogućeno", + "enable": "Omogući", + "disabled": "Onemogućeno", + "disable": "Onemogući", + "save": "Sačuvaj", + "saving": "Sačuvanje…", + "cancel": "Otkaži", + "close": "Zatvori", + "copy": "Kopiraj", + "copiedToClipboard": "Kopirano u međuspremnik", + "back": "Nazad", + "history": "Historija", + "fullscreen": "Pun ekran", + "exitFullscreen": "Napusti pun ekran", + "pictureInPicture": "Slika u slici", + "twoWayTalk": "Dvostrani razgovor", + "cameraAudio": "Zvuk kamere", + "on": "Uključeno", + "off": "Isključeno", + "edit": "Uredi", + "copyCoordinates": "Kopiraj koordinate", + "delete": "Obriši", + "yes": "Da", + "no": "Ne", + "download": "Preuzmi", + "info": "Informacija", + "suspended": "Otkazano", + "unsuspended": "Ponovi", + "play": "Reproduciraj", + "unselect": "Odznači", + "export": "Izvoz", + "deleteNow": "Obriši sada", + "next": "Sljedeće", + "continue": "Nastavi", + "modified": "Izmijenjeno", + "overridden": "Preklopljeno", + "resetToGlobal": "Vrati na globalno", + "resetToDefault": "Vrati na podrazumijevano", + "saveAll": "Sačuvaj sve", + "savingAll": "Sačuvanje svih…", + "undoAll": "Poništi sve", + "retry": "Pokušaj ponovno" + }, + "menu": { + "system": "Sistem", + "systemMetrics": "Sistem metrike", + "configuration": "Konfiguracija", + "systemLogs": "Sistemski zapisi", + "profiles": "Profili", + "settings": "Postavke", + "configurationEditor": "Uređivač konfiguracije", + "languages": "Jezici", + "language": { + "en": "Engleski (English)", + "es": "Španjolski (Spanish)", + "zhCN": "Jednostavni kineski (Simplified Chinese)", + "hi": "Hindi (Hindi)", + "fr": "Francuski (French)", + "ar": "Arapski (Arabic)", + "pt": "Portugalski (Portuguese)", + "ptBR": "Portugalski brazilski (Brazilian Portuguese)", + "ru": "Ruski (Russian)", + "de": "Nemački (German)", + "ja": "Japanski (Japanese)", + "tr": "Turski (Turkish)", + "it": "Talijanski (Italian)", + "nl": "Nizozemski (Dutch)", + "sv": "Švedski (Swedish)", + "cs": "Češki (Czech)", + "nb": "Norveški bokmål (Norwegian Bokmål)", + "ko": "Koreanski (Korean)", + "vi": "Vietnamski (Vietnamese)", + "fa": "Perzijski (Persian)", + "pl": "Polski (Poljski)", + "uk": "Українська (Ukrajinski)", + "he": "עברית (Hebrejski)", + "el": "Ελληνικά (Grčki)", + "ro": "Română (Romunski)", + "hu": "Magyar (Mađarski)", + "fi": "Suomi (Finski)", + "da": "Dansk (Danski)", + "sk": "Slovenčina (Slovački)", + "yue": "粵語 (Kantonski)", + "th": "ไทย (Tajski)", + "ca": "Català (Katalonski)", + "hr": "Hrvatski (Hrvatski)", + "sr": "Српски (Srpski)", + "sl": "Slovenščina (Slovenski)", + "lt": "Lietuvių (Lietuvių)", + "bg": "Български (Bugarinski)", + "gl": "Galego (Galicijski)", + "id": "Bahasa Indonesia (Indoneziski)", + "ur": "اردو (Urdu)", + "withSystem": { + "label": "Koristite postavke sistema za jezik" + } + }, + "appearance": "Izgled", + "darkMode": { + "label": "Tamni režim", + "light": "Svijetla", + "dark": "Tamna", + "withSystem": { + "label": "Koristite postavke sistema za svjetlosni ili tamni režim" + } + }, + "withSystem": "Sistem", + "theme": { + "label": "Tema", + "blue": "Plava", + "green": "Zelena", + "nord": "Nord", + "red": "Crvena", + "highcontrast": "Visok kontrast", + "default": "Zadano" + }, + "help": "Pomoć", + "documentation": { + "title": "Dokumentacija", + "label": "Dokumentacija za Frigate" + }, + "restart": "Ponovno pokreni Frigate", + "live": { + "title": "Uživo", + "allCameras": "Sve Kamere", + "cameras": { + "title": "Kamere", + "count_one": "{{count}} Kamera", + "count_few": "{{count}} Kamere", + "count_other": "{{count}} Kamere" + } + }, + "review": "Pregled", + "explore": "Istraži", + "export": "Izvoz", + "actions": "Akcije", + "uiPlayground": "UI Playground", + "features": "Funkcije", + "faceLibrary": "Biblioteka lica", + "classification": "Klasifikacija", + "chat": "Razgovor", + "user": { + "title": "Korisnik", + "account": "Račun", + "current": "Trenutni korisnik: {{user}}", + "anonymous": "anons", + "logout": "Odjava", + "setPassword": "Postavi lozinku" + } + }, + "toast": { + "copyUrlToClipboard": "URL kopiran u međuspremnik.", + "save": { + "title": "Sačuvaj", + "error": { + "title": "Nije uspješno sačuvana promjena konfiguracije: {{errorMessage}}", + "noMessage": "Nije uspješno sačuvana promjena konfiguracije" + }, + "success": "Uspješno sačuvana promjena konfiguracije." + } + }, + "role": { + "title": "Uloga", + "admin": "Administrator", + "viewer": "Pregledač", + "desc": "Admini imaju pun pristup svim funkcijama u korisničkom sučelju Frigate. Pregledači su ograničeni na pregled kamere, pregled stavki i povijesne snimke u korisničkom sučelju." + }, + "pagination": { + "label": "paginacija", + "previous": { + "title": "Prethodno", + "label": "Idi na prethodnu stranicu" + }, + "next": { + "title": "Sljedeće", + "label": "Idi na sljedeću stranicu" + }, + "more": "Više stranica" + }, + "accessDenied": { + "documentTitle": "Pristup odbijen - Frigate", + "title": "Pristup odbijen", + "desc": "Nemate dozvolu za pregled ove stranice." + }, + "notFound": { + "documentTitle": "Nije pronađeno - Frigate", + "title": "404", + "desc": "Stranica nije pronađena" + }, + "selectItem": "Odaberite {{item}}", + "readTheDocumentation": "Pročitajte dokumentaciju", + "information": { + "pixels": "{{area}}px" + }, + "no_items": "Nema stavki", + "validation_errors": "Greške validacije" +} diff --git a/web/public/locales/bs/components/auth.json b/web/public/locales/bs/components/auth.json new file mode 100644 index 0000000..42bac9b --- /dev/null +++ b/web/public/locales/bs/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Korisničko ime", + "password": "Lozinka", + "login": "Prijava", + "firstTimeLogin": "Pokušavate se prijaviti prvi put? Vjerodajnice su ispisane u logovima Frigate.", + "errors": { + "usernameRequired": "Korisničko ime je obavezno", + "passwordRequired": "Lozinka je obavezna", + "rateLimit": "Premašen je limit brzine. Pokušajte kasnije.", + "loginFailed": "Prijava nije uspješna", + "unknownError": "Nepoznata greška. Provjerite zapise.", + "webUnknownError": "Nepoznata greška. Provjerite konzolne zapise." + } + } +} diff --git a/web/public/locales/bs/components/camera.json b/web/public/locales/bs/components/camera.json new file mode 100644 index 0000000..68c8fdf --- /dev/null +++ b/web/public/locales/bs/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Grupe kamere", + "add": "Dodaj grupu kamere", + "edit": "Uredi grupu kamera", + "delete": { + "label": "Obriši grupu kamere", + "confirm": { + "title": "Potvrdi brisanje", + "desc": "Sigurno li želite da obrišete grupu kamere {{name}}?" + } + }, + "name": { + "label": "Ime", + "placeholder": "Unesite ime…", + "errorMessage": { + "mustLeastCharacters": "Ime grupe kamere mora imati najmanje 2 karaktera.", + "exists": "Ime grupe kamere već postoji.", + "nameMustNotPeriod": "Ime grupe kamere ne smije sadržavati tačku.", + "invalid": "Neispravno ime grupe kamere." + } + }, + "cameras": { + "label": "Kamere", + "desc": "Odaberite kamere za ovu grupu." + }, + "icon": "Ikona", + "success": "Grupa kamere ({{name}}) je sačuvana.", + "camera": { + "birdseye": "Birdseye", + "setting": { + "label": "Postavke prenošenja kamere", + "title": "Postavke prenošenja {{cameraName}}", + "desc": "Promijenite opcije uživo prenošenja za tablicu upravljanja ove grupe kamere. Ove postavke su specifične za uređaj/pretvarač.", + "audioIsAvailable": "Audio je dostupan za ovaj stream", + "audioIsUnavailable": "Zvuk nije dostupan za ovaj tok", + "audio": { + "tips": { + "title": "Audio mora biti izlaz iz vaše kamere i konfiguriran u go2rtc za ovaj stream." + } + }, + "stream": "Tok", + "placeholder": "Odaberite tok", + "streamMethod": { + "label": "Način prenošenja", + "placeholder": "Odaberite način prenošenja", + "method": { + "noStreaming": { + "label": "Bez prenošenja", + "desc": "Slike kamere će se ažurirati samo jednom na minut i neće se dogoditi uživo prenošenje." + }, + "smartStreaming": { + "label": "Pametno prenošenje (preporučeno)", + "desc": "Pametno prenošenje će ažurirati sliku kamere jednom na minut kada se ne događa detektovana aktivnost kako bi se uštedjelo na širovini i resursima. Kada se detektuje aktivnost, slika se glatko prebacuje u uživo prenošenje." + }, + "continuousStreaming": { + "label": "Neprekidno prenošenje", + "desc": { + "title": "Slika kamere uvijek će biti živo prenošenje kada je vidljiva na ploči, čak i ako se ne detektira aktivnost.", + "warning": "Neprekidno prenošenje može uzrokovati visoku upotrebu širine pojasa i probleme s performansama. Koristite s oprezom." + } + } + } + }, + "compatibilityMode": { + "label": "Režim kompatibilnosti", + "desc": "Omogućite ovu opciju samo ako se živo prenošenje vaše kamere prikazuje s bojnim artefaktima i dijagonalnom linijom na desnoj strani slike." + } + } + } + }, + "debug": { + "options": { + "label": "Postavke", + "title": "Opcije", + "showOptions": "Prikaži opcije", + "hideOptions": "Sakrij opcije" + }, + "boundingBox": "Okvir", + "timestamp": "Vremenski pečat", + "zones": "Zone", + "mask": "Maska", + "motion": "Kretanje", + "regions": "Regije", + "paths": "Putanje" + } +} diff --git a/web/public/locales/bs/components/dialog.json b/web/public/locales/bs/components/dialog.json new file mode 100644 index 0000000..95f4ada --- /dev/null +++ b/web/public/locales/bs/components/dialog.json @@ -0,0 +1,197 @@ +{ + "restart": { + "title": "Sigurni li ste da želite ponovno pokrenuti Frigate?", + "description": "Ovo privremeno zaustavi Frigate dok se ponovno pokreće.", + "button": "Ponovno pokretanje", + "restarting": { + "title": "Frigate se ponovo pokreće", + "content": "Ova stranica će se ponovno učitati za {{countdown}} sekundi.", + "button": "Silovito ponovno učitavanje sada" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Pošalji na Frigate+", + "desc": "Predmeti u lokacijama koje želite izbjeći nisu lažni pozitivi. Pošiljanje ih kao lažne pozitive zbunjuje model." + }, + "review": { + "question": { + "label": "Potvrdite ovu oznaku za Frigate Plus", + "ask_a": "Je li ovaj objekt {{label}}?", + "ask_an": "Je li ovaj objekt {{label}}?", + "ask_full": "Je li ovaj objekt {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Pošlato" + } + } + }, + "video": { + "viewInHistory": "Pregledajte u povijesti" + } + }, + "export": { + "time": { + "fromTimeline": "Odaberite iz vremenske linije", + "lastHour_one": "Prošli sat", + "lastHour_few": "Prošla {{count}} sata", + "lastHour_other": "Prošlih {{count}} sati", + "custom": "Prilagođeno", + "start": { + "title": "Vrijeme početka", + "label": "Odaberite vrijeme početka" + }, + "end": { + "title": "Vrijeme kraja", + "label": "Odaberite vrijeme kraja" + } + }, + "name": { + "placeholder": "Nazovite izvoz" + }, + "case": { + "newCaseOption": "Napravite novi slučaj", + "newCaseNamePlaceholder": "Novo ime slučaja", + "newCaseDescriptionPlaceholder": "Opis slučaja", + "label": "Slučaj", + "nonAdminHelp": "Za ove izvoze će se stvoriti novi slučaj.", + "placeholder": "Odaberite slučaj" + }, + "select": "Odaberite", + "export": "Izvoz", + "queueing": "Stavljanje izvoza u red...", + "selectOrExport": "Odaberite ili izvozite", + "tabs": { + "export": "Jedna kamera", + "multiCamera": "Više kamera" + }, + "multiCamera": { + "timeRange": "Vremenski opseg", + "selectFromTimeline": "Odaberite iz vremenske linije", + "cameraSelection": "Kamere", + "cameraSelectionHelp": "Kamere s praćenim objektima u ovom vremenskom opsegu su preselektirane", + "checkingActivity": "Provjeravamo aktivnost kamere...", + "noCameras": "Nema dostupnih kamera", + "detectionCount_one": "1 praćen objekt", + "detectionCount_few": "{{count}} praćena objekta", + "detectionCount_other": "{{count}} praćenih objekata", + "nameLabel": "Ime izvoza", + "namePlaceholder": "Nepovlačenje baznog imena za ove izvoze", + "queueingButton": "Stavljanje izvoza u red...", + "exportButton_one": "Izvoz 1 kamere", + "exportButton_few": "Izvoz {{count}} kamere", + "exportButton_other": "Izvoz {{count}} kamera" + }, + "multi": { + "title_one": "Izvoz 1 pregleda", + "title_few": "Izvoz {{count}} pregleda", + "title_other": "Izvoz {{count}} pregleda", + "description": "Izvoz svakog odabranih pregleda. Svi izvozi bit će grupirani pod jedan slučaj.", + "descriptionNoCase": "Izvoz svakog odabranih pregleda.", + "caseNamePlaceholder": "Pregled izvoza - {{date}}", + "exportButton_one": "Izvoz 1 pregleda", + "exportButton_few": "Izvoz {{count}} pregleda", + "exportButton_other": "Izvoz {{count}} pregleda", + "exportingButton": "Izvoz...", + "toast": { + "started_one": "Pokrenut 1 izvoz. Otvaranje slučaja sada.", + "started_few": "Pokrenuta {{count}} izvoza. Otvaranje slučaja sada.", + "started_other": "Pokrenuto {{count}} izvoza. Otvaranje slučaja sada.", + "startedNoCase_one": "Pokrenut 1 izvoz.", + "startedNoCase_few": "Pokrenuta {{count}} izvoza.", + "startedNoCase_other": "Pokrenuto {{count}} izvoza.", + "partial": "Pokrenuto {{successful}} od {{total}} izvoza. Neuspješno: {{failedItems}}", + "failed": "Neuspješno pokretanje {{total}} izvoza. Neuspješno: {{failedItems}}" + } + }, + "toast": { + "success": "Uspješno pokrenut izvoz. Pregledajte datoteku na stranici izvoza.", + "queued": "Izvoz u redu. Pregledajte napredak na stranici izvoza.", + "view": "Pregled", + "batchSuccess_one": "Pokrenut 1 izvoz. Otvaranje slučaja sada.", + "batchSuccess_few": "Pokrenuta {{count}} izvoza. Otvaranje slučaja sada.", + "batchSuccess_other": "Pokrenuto {{count}} izvoza. Otvaranje slučaja sada.", + "batchPartial": "Pokrenuto {{successful}} od {{total}} izvoza. Neuspješne kamere: {{failedCameras}}", + "batchFailed": "Neuspješno pokretanje {{total}} izvoza. Neuspješne kamere: {{failedCameras}}", + "batchQueuedSuccess_one": "U red stavljen 1 izvoz. Otvaranje slučaja sada.", + "batchQueuedSuccess_few": "U red stavljena {{count}} izvoza. Otvaranje slučaja sada.", + "batchQueuedSuccess_other": "U red stavljeno {{count}} izvoza. Otvaranje slučaja sada.", + "batchQueuedPartial": "U redu {{successful}} od {{total}} izvoza. Neuspješne kamere: {{failedCameras}}", + "batchQueueFailed": "Neuspješno dodavanje {{total}} izvoza. Neuspješne kamere: {{failedCameras}}", + "error": { + "failed": "Neuspješno dodavanje izvoza: {{error}}", + "endTimeMustAfterStartTime": "Krajnje vrijeme mora biti nakon početnog vremena", + "noVaildTimeSelected": "Nije odabran valjan vremenski opseg" + } + }, + "fromTimeline": { + "saveExport": "Sačuvaj izvoz", + "queueingExport": "Kopiranje izvoza...", + "previewExport": "Pregled izvoza", + "useThisRange": "Koristi ovaj opseg" + } + }, + "streaming": { + "label": "Tok", + "restreaming": { + "disabled": "Restreaming nije omogućeno za ovu kameru.", + "desc": { + "title": "Postavite go2rtc za dodatne opcije uživog pregleda i zvuk za ovu kameru." + } + }, + "showStats": { + "label": "Prikaži statistiku strima", + "desc": "Omogući ovu opciju da prikaže statistiku prijenosa kao preklapanje na toku kamere." + }, + "debugView": "Pregled za otklanjanje grešaka" + }, + "search": { + "saveSearch": { + "label": "Sačuvaj pretragu", + "desc": "Navedite ime za ovu sačuvanu pretragu.", + "placeholder": "Unesite ime za svoju pretragu", + "overwrite": "{{searchName}} već postoji. Sačuvavanje će prebrisati postojet će vrijednost.", + "success": "Pretraga ({{searchName}}) je sačuvana.", + "button": { + "save": { + "label": "Sačuvaj ovu pretragu" + } + } + } + }, + "recording": { + "shareTimestamp": { + "label": "Dijeli vremensku oznaku", + "title": "Dijeli vremensku oznaku", + "description": "Dijelite URL označen vremenom trenutne pozicije igrača ili odaberite prilagođenu vremensku oznaku. Napomena: ovo nije javni URL za dijeljenje i dostupan je samo korisnicima koji imaju pristup Frigate i ovoj kameri.", + "custom": "Prilagođena vremenska oznaka", + "button": "URL za dijeljenje vremenske oznake", + "shareTitle": "Vremenska oznaka pregleda Frigate: {{camera}}" + }, + "confirmDelete": { + "title": "Potvrdi brisanje", + "desc": { + "selected": "Sigurni li ste da želite izbrisati sve snimljeno video povezano s ovim preglednim stavkom?

    Zadržite tipku Shift da biste preskočili ovaj dijalog u budućnosti." + }, + "toast": { + "success": "Video snimke povezane s odabranim preglednim stavcima uspješno su izbrisane.", + "error": "Neuspješno brisanje: {{error}}" + } + }, + "button": { + "export": "Izvoz", + "markAsReviewed": "Označi kao pregledano", + "markAsUnreviewed": "Označi kao nepregledano", + "deleteNow": "Obriši sada" + } + }, + "imagePicker": { + "selectImage": "Odaberite minijaturu praćenog objekta", + "unknownLabel": "Sačuvana slika izazivača", + "search": { + "placeholder": "Pretraga po oznaci ili podoznaci..." + }, + "noImages": "Nema mini prikaza za ovu kameru" + } +} diff --git a/web/public/locales/bs/components/filter.json b/web/public/locales/bs/components/filter.json new file mode 100644 index 0000000..7578235 --- /dev/null +++ b/web/public/locales/bs/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filtar", + "classes": { + "label": "Klase", + "all": { + "title": "Sve klase" + }, + "count_one": "{{count}} Klasa", + "count_other": "{{count}} Klase" + }, + "labels": { + "label": "Oznake", + "all": { + "title": "Sve oznake", + "short": "Oznake" + }, + "count_one": "{{count}} Oznaka", + "count_other": "{{count}} Oznake" + }, + "zones": { + "label": "Zone", + "all": { + "title": "Sve zone", + "short": "Zone" + } + }, + "dates": { + "selectPreset": "Odaberite predpostavku…", + "all": { + "title": "Svi datumi", + "short": "Datumi" + } + }, + "more": "Više filtera", + "reset": { + "label": "Poništi filtere na zadane vrijednosti" + }, + "timeRange": "Vremenski opseg", + "subLabels": { + "label": "Podoznake", + "all": "Sve podoznake" + }, + "attributes": { + "label": "Atributi klasifikacije", + "all": "Svi atributi" + }, + "score": "Rezultat", + "estimatedSpeed": "Procijenjena brzina ({{unit}})", + "features": { + "label": "Funkcije", + "hasSnapshot": "Ima snimak", + "hasVideoClip": "Ima video zapis", + "submittedToFrigatePlus": { + "label": "Predano Frigate+", + "tips": "Prvo morate filtrirati prateće objekte koji imaju snimak.

    Prateći objekti bez snimka ne mogu se poslati na Frigate+." + } + }, + "sort": { + "label": "Sortiraj", + "dateAsc": "Datum (Uzlazno)", + "dateDesc": "Datum (Silazno)", + "scoreAsc": "Ocjena objekta (Uzlazno)", + "scoreDesc": "Ocjena objekta (Silazno)", + "speedAsc": "Procijenjena brzina (Uzlazno)", + "speedDesc": "Procijenjena brzina (Silazno)", + "relevance": "Relevantnost" + }, + "cameras": { + "label": "Filter kamere", + "all": { + "title": "Sve Kamere", + "short": "Kamere" + } + }, + "review": { + "showReviewed": "Prikaži pregledane" + }, + "motion": { + "showMotionOnly": "Prikaži samo pokret" + }, + "explore": { + "settings": { + "title": "Postavke", + "defaultView": { + "title": "Zadani prikaz", + "desc": "Kada nisu odabrani filteri, prikazuje se sažetak najnovijih pratećih objekata po oznaci, ili prikazuje se mreža bez filtriranja.", + "summary": "Sažetak", + "unfilteredGrid": "Mreža bez filtriranja" + }, + "gridColumns": { + "title": "Kolone mreže", + "desc": "Odaberite broj kolona u prikazu mreže." + }, + "searchSource": { + "label": "Izvor pretrage", + "desc": "Odaberite da li ćete pretraživati miniaturne slike ili opise vaših praćenih objekata.", + "options": { + "thumbnailImage": "Miniaturna slika", + "description": "Opis" + } + } + }, + "date": { + "selectDateBy": { + "label": "Odaberite datum za filtriranje" + } + } + }, + "logSettings": { + "label": "Filtrirajte nivo zapisa", + "filterBySeverity": "Filtrirajte zapise prema ozbiljnosti", + "loading": { + "title": "Učitavanje", + "desc": "Kada se panel zapisa pomakne do dna, novi zapisi automatski se prikazuju kada se dodaju." + }, + "disableLogStreaming": "Onemogući praćenje zapisa", + "allLogs": "Svi zapisi" + }, + "trackedObjectDelete": { + "title": "Potvrdi brisanje", + "desc": "Brisanje ovih {{objectLength}} praćenih objekata uklanja snimku, bilo koje sačuvane ugradnje, i sve povezane uloge objekata. Snimljeni materijal ovih praćenih objekata u pogledu Historija NEĆE biti obrisan.

    Sigurni ste da želite nastaviti?

    Zadržite tipku Shift da biste preskočili ovaj dijalog u budućnosti.", + "toast": { + "success": "Praćeni objekti uspješno obrisani.", + "error": "Neuspješno brisanje praćenih objekata: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtriraj po maski zone" + }, + "recognizedLicensePlates": { + "title": "Prepoznate tablice", + "loadFailed": "Neuspješno učitavanje prepoznatih tablica.", + "loading": "Učitavanje prepoznatih tablica…", + "placeholder": "Unesite za pretragu tablica…", + "noLicensePlatesFound": "Nema pronađenih tablica.", + "selectPlatesFromList": "Odaberite jednu ili više tablica iz liste.", + "selectAll": "Odaberite sve", + "clearAll": "Očistite sve" + } +} diff --git a/web/public/locales/bs/components/icons.json b/web/public/locales/bs/components/icons.json new file mode 100644 index 0000000..807c1d2 --- /dev/null +++ b/web/public/locales/bs/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Odaberite ikonu", + "search": { + "placeholder": "Pretražite ikonu…" + } + } +} diff --git a/web/public/locales/bs/components/input.json b/web/public/locales/bs/components/input.json new file mode 100644 index 0000000..03e33fb --- /dev/null +++ b/web/public/locales/bs/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Preuzimanje videa", + "toast": { + "success": "Vaš video stavke pregleda je započelo preuzimanje." + } + } + } +} diff --git a/web/public/locales/bs/components/player.json b/web/public/locales/bs/components/player.json new file mode 100644 index 0000000..3056608 --- /dev/null +++ b/web/public/locales/bs/components/player.json @@ -0,0 +1,52 @@ +{ + "noRecordingsFoundForThisTime": "Nisu pronađeni snimci za ovo vrijeme", + "noPreviewFound": "Nije pronađen pregled", + "noPreviewFoundFor": "Nije pronađen pregled za {{cameraName}}", + "submitFrigatePlus": { + "title": "Pošalji ovaj okvir Frigate+?", + "submit": "Pošalji", + "previewError": "Nije moguće učitati prikaz snimke. Snimka možda trenutno nije dostupna." + }, + "livePlayerRequiredIOSVersion": "Za ovaj tip uživo prijenosa potreban je iOS 17.1 ili noviji.", + "streamOffline": { + "title": "Prijenos je offline", + "desc": "Nisu primljeni okviri na {{cameraName}} detect prijenos, provjerite zapise o greškama" + }, + "cameraDisabled": "Kamera je onemogućena", + "stats": { + "streamType": { + "title": "Tip prijenosa:", + "short": "Tip" + }, + "bandwidth": { + "title": "Širina pojasa:", + "short": "Širina pojasa" + }, + "latency": { + "title": "Kasnjenje:", + "value": "{{seconds}} sekundi", + "short": { + "title": "Kasnjenje", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Ukupno okvira:", + "droppedFrames": { + "title": "Izgubljeni okviri:", + "short": { + "title": "Izgubljeni", + "value": "{{droppedFrames}} okvira" + } + }, + "decodedFrames": "Dekodirani okviri:", + "droppedFrameRate": "Stopa izgubljenih okvira:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Uspješno je poslano okvir Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Neuspješno slanje okvira Frigate+" + } + } +} diff --git a/web/public/locales/bs/config/cameras.json b/web/public/locales/bs/config/cameras.json new file mode 100644 index 0000000..97da5a3 --- /dev/null +++ b/web/public/locales/bs/config/cameras.json @@ -0,0 +1,949 @@ +{ + "label": "KameraKonfig", + "zones": { + "label": "Zone", + "description": "Zona omogućava da definirate specifičnu područje okvira da biste odredili je li objekt unutar određenog područja.", + "friendly_name": { + "label": "Ime zone", + "description": "Korisničko ime za zonu, prikazano u UI Frigate. Ako nije postavljeno, koristi se oblikovana verzija imena zone." + }, + "enabled": { + "label": "Omogućeno", + "description": "Omogući ili onemogući ovu zonu. Onemogućene zone zanemaruju se tijekom izvršavanja." + }, + "enabled_in_config": { + "label": "Zapamti originalno stanje zone." + }, + "filters": { + "label": "Filtri zone", + "description": "Filtri za primjenu na objekte unutar ove zone. Koriste se za smanjenje lažnih pozitiva ili ograničavanje kojih objekata se smatraju prisutnim u zoni.", + "min_area": { + "label": "Minimalna površina objekta", + "description": "Minimalna površina okvira (pikseli ili postotak) potrebna za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)." + }, + "max_area": { + "label": "Maksimalna površina objekta", + "description": "Maksimalna površina okvira (pikseli ili postotak) dozvoljena za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)." + }, + "min_ratio": { + "label": "Minimalni omjer visine/širine", + "description": "Minimalni omjer širine/visine potreban da bi okvir bio prihvaćen." + }, + "max_ratio": { + "label": "Maksimalni omjer visine/širine", + "description": "Maksimalni omjer širine/visine dozvoljen da bi okvir bio prihvaćen." + }, + "threshold": { + "label": "Prag pouzdanosti", + "description": "Prosjek pragova pouzdanosti detekcije potreban da bi objekt bio smatravan pravim pozitivom." + }, + "min_score": { + "label": "Minimalna pouzdanost", + "description": "Minimalna pouzdanost detekcije po okviru potrebna da bi objekt bio brojan." + }, + "mask": { + "label": "Maska filtriranja", + "description": "Koordinate poligona koje definiraju područje na kojem se ovaj filter primjenjuje unutar okvira." + }, + "raw_mask": { + "label": "Ručna maska" + } + }, + "coordinates": { + "label": "Koordinate", + "description": "Koordinate poligona koje definiraju područje zone. Može biti niz razdvojen zarezom ili lista nizova koordinata. Koordinate trebaju biti relativne (0-1) ili apsolutne (stariji format)." + }, + "distances": { + "label": "Stvarne udaljenosti", + "description": "Nepovlačni stvarne udaljenosti za svaku stranu kvadrilateralne zone, koristi se za izračun brzine ili udaljenosti. Moraju imati tačno 4 vrijednosti ako su postavljene." + }, + "inertia": { + "label": "Okviri inertnosti", + "description": "Broj uzastopnih okvira u kojima mora biti detektovan objekt u zoni da bi bio smatravan prisutnim. Pomaže u filtriranju privremenih detekcija." + }, + "loitering_time": { + "label": "Sekunde loiteranja", + "description": "Broj sekundi koje objekt mora ostati u zoni da bi bio smatravan loiteranjem. Postaviti na 0 za onemogućavanje detekcije loiteranja." + }, + "speed_threshold": { + "label": "Minimalna brzina", + "description": "Minimalna brzina (u stvarnim jedinicama ako su udaljenosti postavljene) potrebna da bi objekt bio smatravan prisutnim u zoni. Koristi se za zone koje se aktiviraju na osnovu brzine." + }, + "objects": { + "label": "Objekti koji izazivaju", + "description": "Lista tipova objekata (iz labelmapa) koji mogu izazvati ovu zonu. Može biti niz ili lista nizova. Ako je prazna, svi objekti se uzimaju u obzir." + } + }, + "name": { + "label": "Ime kamere", + "description": "Ime kamere je obavezno" + }, + "friendly_name": { + "label": "Prijateljsko ime", + "description": "Prijateljsko ime kamere korišteno u korisničkom sučelju Frigate" + }, + "enabled": { + "label": "Omogućeno", + "description": "Omogućeno" + }, + "audio": { + "label": "Audio događaji", + "description": "Postavke za detekciju događaja temeljene na audio.", + "enabled": { + "label": "Omogući detekciju zvuka", + "description": "Omogući ili onemogući detekciju događaja temeljenu na audio za ovu kameru." + }, + "max_not_heard": { + "label": "Vrijeme trajanja do kraja", + "description": "Količina sekundi bez konfiguriranog tipa zvuka prije nego što se audio događaj završi." + }, + "min_volume": { + "label": "Minimalna zapremina", + "description": "Minimalni prag RMS zapremine potreban za pokretanje detekcije zvuka; niže vrijednosti povećavaju osjetljivost (npr. 200 visoko, 500 srednje, 1000 nisko)." + }, + "listen": { + "label": "Tipovi slušanja", + "description": "Popis tipova audio događaja za detekciju (npr. zavijanje, požarne zvona, vrisak, govorenje, vikanje)." + }, + "filters": { + "label": "Audio filteri", + "description": "Postavke filtera po tipu zvuka kao što su pragovi pouzdanosti za smanjenje lažnih pozitiva." + }, + "enabled_in_config": { + "label": "Originalno stanje zvuka", + "description": "Indikuje je li detekcija zvuka izvorno omogućena u statičkoj konfiguracijskoj datoteci." + }, + "num_threads": { + "label": "Dretve detekcije", + "description": "Broj dretvi za korištenje za obradu detekcije zvuka." + } + }, + "audio_transcription": { + "label": "Transkripcija zvuka", + "description": "Postavke za transkripciju živog i govornog zvuka korištenih za događaje i žive podnaslove.", + "enabled": { + "label": "Omogući transkripciju", + "description": "Omogući ili onemogući transkripciju audio događaja pokrenutu ručno." + }, + "enabled_in_config": { + "label": "Originalni stanje transkripcije" + }, + "live_enabled": { + "label": "Uživo transkripcija", + "description": "Omogući streaming uživo transkripcije za audio dok se prima." + } + }, + "birdseye": { + "label": "Birdseye", + "description": "Postavke za sastavni prikaz Birdseye koji kombinuje više snimke kamere u jedinstveni raspored.", + "enabled": { + "label": "Omogući Birdseye", + "description": "Omogući ili onemogući funkciju prikaza Birdseye." + }, + "mode": { + "label": "Način praćenja", + "description": "Način uključivanja kamera u Birdseye: 'objekti', 'kretanje' ili 'kontinuirano'." + }, + "order": { + "label": "Pozicija", + "description": "Numerička pozicija koja kontroliše redoslijed kamera u rasporedu Birdseye." + } + }, + "detect": { + "label": "Detekcija objekata", + "description": "Postavke za ulogu detekcije/detekcija koja se koristi za pokretanje detekcije objekata i inicijalizaciju praćenja.", + "enabled": { + "label": "Omogući detekciju objekata", + "description": "Omogući ili onemogući detekciju objekata za ovu kameru." + }, + "height": { + "label": "Visina detekcije", + "description": "Visina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a." + }, + "width": { + "label": "Širina detekcije", + "description": "Širina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a." + }, + "fps": { + "label": "Detekcija FPS", + "description": "Željeni broj okvira po sekundi za pokretanje detekcije; niže vrijednosti smanjuju upotrebu CPU-a (preporučena vrijednost je 5, postavite više - najviše 10 - samo ako praćite vrlo brze objekte)." + }, + "min_initialized": { + "label": "Minimalni broj okvira inicijalizacije", + "description": "Broj uzastopnih detekcija potreban prije stvaranja praćenog objekta. Povećajte da biste smanjili lažne inicijalizacije. Zadana vrijednost je fps podijeljeno sa 2." + }, + "max_disappeared": { + "label": "Maksimalni broj okvira koji su nestali", + "description": "Broj okvira bez detekcije prije nego što se praćeni objekt smatra izgubljenim." + }, + "stationary": { + "label": "Konfiguracija stacionarnih objekata", + "description": "Postavke za detekciju i upravljanje objektima koji ostaju stacionarni tokom određenog vremena.", + "interval": { + "label": "Stacionarni interval", + "description": "Kako često (u snimcima) pokretati provjeru detekcije da biste potvrdili stacionarni objekt." + }, + "threshold": { + "label": "Stacionarni prag", + "description": "Broj snimaka bez promjene pozicije potreban da bi objekt bio označen kao stacionarni." + }, + "max_frames": { + "label": "Maksimalni snimci", + "description": "Ograničava koliko dugo se stacionarni objekti praćaju prije nego što se odbacuju.", + "default": { + "label": "Zadani maksimalni snimci", + "description": "Zadani maksimalni broj snimaka za praćenje stacionarnog objekta prije prestanka." + }, + "objects": { + "label": "Maksimalni snimci po objektu", + "description": "Podešavanja po objektu za maksimalni broj snimaka za praćenje stacionarnih objekata." + } + }, + "classifier": { + "label": "Omogući vizualni klasifikator", + "description": "Koristi vizualni klasifikator za detekciju pravozadanih stacionarnih objekata čak i kada se okviri tresu." + } + }, + "annotation_offset": { + "label": "Pomak oznake", + "description": "Milisekunde za pomak detektiranih oznaka kako bi se bolje poravnali vremenski okviri s snimcima; može biti pozitivan ili negativan." + } + }, + "face_recognition": { + "label": "Prepoznavanje lica", + "description": "Postavke za detekciju i prepoznavanje lica za ovu kameru.", + "enabled": { + "label": "Omogući prepoznavanje lica", + "description": "Omogući ili onemogući prepoznavanje lica." + }, + "min_area": { + "label": "Minimalna površina lica", + "description": "Minimalna površina (pikseli) detektiranog okvira lica potrebna za pokušaj prepoznavanja." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "Postavke FFmpeg uključuju putanju binarne datoteke, argumente, opcije hwaccel i izlazne argumente po ulozi.", + "path": { + "label": "Putanja do FFmpeg binarne datoteke", + "description": "Putanja do FFmpeg binarne datoteke ili verzija alias (\"5.0\" ili \"7.0\")." + }, + "global_args": { + "label": "Globalni argumenti FFmpeg-a", + "description": "Globalni argumenti prebačeni na procese FFmpeg." + }, + "hwaccel_args": { + "label": "Argumenti za ubrzanje hardvera", + "description": "Argumenti za ubrzanje hardvera za FFmpeg. Preporučuju se predložci specifični za dobavljača." + }, + "input_args": { + "label": "Unos argumenata", + "description": "Ulazni argumenti primjenjeni na ulazne snimke FFmpeg." + }, + "output_args": { + "label": "Izlazni argumenti", + "description": "Zadani izlazni argumenti korišteni za različite uloge FFmpeg-a poput detekcije i snimanja.", + "detect": { + "label": "Izlazni argumenti za detekciju", + "description": "Zadani izlazni argumenti za snimke uloga detekcije." + }, + "record": { + "label": "Izlazni argumenti za snimanje", + "description": "Zadani izlazni argumenti za snimke uloga snimanja." + } + }, + "retry_interval": { + "label": "Vrijeme ponovnog pokušaja FFmpeg-a", + "description": "Sekunde koje treba čekati prije nego što se pokuša ponovno uspostaviti veza s tokom kamere nakon neuspjeha. Zadano je 10." + }, + "apple_compatibility": { + "label": "Kompatibilnost s Apple-om", + "description": "Omogući označavanje HEVC za bolju kompatibilnost s igračima Apple-a prilikom snimanja H.265." + }, + "gpu": { + "label": "Indeks GPU-a", + "description": "Zadani indeks GPU-a korišten za ubrzanje hardvera ako je dostupan." + }, + "inputs": { + "label": "Ulazni podaci kamere", + "description": "Popis definicija ulaznih tokova (putanje i uloge) za ovu kameru.", + "path": { + "label": "Putanja ulaza", + "description": "URL ili putanja ulaznog toka kamere." + }, + "roles": { + "label": "Uloge ulaza", + "description": "Uloge za ovaj ulazni tok." + }, + "global_args": { + "label": "Globalni argumenti FFmpeg-a", + "description": "Globalni argumenti FFmpeg-a za ovaj ulazni tok." + }, + "hwaccel_args": { + "label": "Argumenti za ubrzanje hardvera", + "description": "Argumenti za ubrzanje hardvera za ovaj ulazni stream." + }, + "input_args": { + "label": "Unos argumenata", + "description": "Argumeti unosa specifični za ovaj stream." + } + } + }, + "live": { + "label": "Uživo prikaz", + "description": "Postavke korištenje Web UI za kontrolu izbora živog streama, rezolucije i kvalitete.", + "streams": { + "label": "Imena živih streamova", + "description": "Mapiranje konfiguriranih imena streamova na imena restream/go2rtc korишtena za uživo prikaz." + }, + "height": { + "label": "Visina uživo", + "description": "Visina (piksela) za prikaz jsmpeg živog streama u Web UI; mora biti <= visina detektiranog streama." + }, + "quality": { + "label": "Kvalitet uživo", + "description": "Kvalitet kodiranja za jsmpeg stream (1 najviši, 31 najniži)." + } + }, + "lpr": { + "label": "Prepoznavanje tablice vozila", + "description": "Postavke prepoznavanja tablice vozila uključujući pragovi detekcije, formatiranje i poznate tablice.", + "enabled": { + "label": "Omogući LPR", + "description": "Omogući ili onemogući LPR na ovoj kameri." + }, + "expire_time": { + "label": "Sekunde isteka", + "description": "Vrijeme u sekundama nakon kojeg nevidljiva tablica istječe iz praćenja (samo za dedikovane LPR kamere)." + }, + "min_area": { + "label": "Minimalna površina tablice", + "description": "Minimalna površina tablice (piksela) potrebna za pokušaj prepoznavanja." + }, + "enhancement": { + "label": "Nivo poboljšanja", + "description": "Nivo poboljšanja (0-10) za primjenu na isječke tablice prije OCR-a; veće vrijednosti ne moraju uvijek poboljšati rezultate, nivoi iznad 5 mogu raditi samo s tablicama u noćnom vremenu i trebaju se koristiti s oprezom." + } + }, + "motion": { + "label": "Detekcija pokreta", + "description": "Zadane postavke detekcije pokreta za ovu kameru.", + "enabled": { + "label": "Omogući detekciju pokreta", + "description": "Omogući ili onemogući detekciju pokreta za ovu kameru." + }, + "threshold": { + "label": "Prag pokreta", + "description": "Prag razlike piksela korišten za detektor pokreta; veće vrijednosti smanjuju osjetljivost (opseg 1-255)." + }, + "lightning_threshold": { + "label": "Prag munje", + "description": "Prag za detekciju i zanemarivanje kratkih iskri svjetlosti (niže vrijednosti povećavaju osjetljivost, vrijednosti između 0.3 i 1.0). Ovo ne spriječava detekciju pokreta u potpunosti; jednostavno zaustavlja detektor da analizira dodatne okvire nakon što se prag premaši. Snimci temeljeni na pokretima i dalje se stvaraju tijekom ovih događaja." + }, + "skip_motion_threshold": { + "label": "Preskoči prag pokreta", + "description": "Ako se postavi na vrijednost između 0.0 i 1.0, i ako se više od ovog udjela slike promijeni u jednom okviru, detektor neće vratiti kutije pokreta i odmah će se ponovno kalibrirati. Ovo može uštedjeti CPU i smanjiti lažne pozitive tijekom munje, oluje itd., ali može propustiti stvarne događaje kao što je automatsko praćenje objekta PTZ kamerom. Tržište je između izgube nekoliko megabajta snimaka i pregleda nekoliko kratkih zapisnika. Ostavite nepostavljeno (Nijedno) za onemogućavanje ove funkcije." + }, + "improve_contrast": { + "label": "Poboljšaj kontrast", + "description": "Primijeni poboljšanje kontrasta na okvire prije analize pokreta kako bi pomoću detekcije." + }, + "contour_area": { + "label": "Površina kontura", + "description": "Minimalna površina kontura u pikselima potrebna za brojanje kontura pokreta." + }, + "delta_alpha": { + "label": "Delta alfa", + "description": "Faktor alfa spajanja korišten za razliku okvira za izračun pokreta." + }, + "frame_alpha": { + "label": "Alfa okvira", + "description": "Vrijednost alfa korištena prilikom spajanja okvira za predobradbu pokreta." + }, + "frame_height": { + "label": "Visina okvira", + "description": "Visina u pikselima na koju se skaliraju okviri prilikom izračuna pokreta." + }, + "mask": { + "label": "Koordinate maska", + "description": "Uredno x,y koordinate koje definiraju poligon maska pokreta za uključivanje/isključivanje područja." + }, + "mqtt_off_delay": { + "label": "MQTT zakasnjenje isključivanja", + "description": "Sekunde koje se čekaju nakon posljednjeg pokreta prije objave MQTT 'isključeno' stanje." + }, + "enabled_in_config": { + "label": "Originalno stanje pokreta", + "description": "Indikira je li detekcija pokreta bila omogućena u originalnoj statičkoj konfiguraciji." + }, + "raw_mask": { + "label": "Ručna maska" + } + }, + "objects": { + "label": "Objekti", + "description": "Zadani parametri praćenja objekata uključujući koje oznake praćenja i filtre po objektu.", + "track": { + "label": "Objekti za praćenje", + "description": "Popis oznaka objekata za praćenje za ovu kameru." + }, + "filters": { + "label": "Filtar objekata", + "description": "Filtar primijenjen na detektirane objekte kako bi se smanjila broj lažnih pozitiva (površina, omjer, pouzdanost).", + "min_area": { + "label": "Minimalna površina objekta", + "description": "Minimalna površina okvira (pikseli ili postotak) potrebna za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)." + }, + "max_area": { + "label": "Maksimalna površina objekta", + "description": "Maksimalna površina okvira (pikseli ili postotak) dozvoljena za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)." + }, + "min_ratio": { + "label": "Minimalni omjer visine/širine", + "description": "Minimalni omjer širine/visine potreban da bi okvir bio prihvaćen." + }, + "max_ratio": { + "label": "Maksimalni omjer visine/širine", + "description": "Maksimalni omjer širine/visine dozvoljen da bi okvir bio prihvaćen." + }, + "threshold": { + "label": "Prag pouzdanosti", + "description": "Prosjek pragova pouzdanosti detekcije potreban da bi objekt bio smatravan pravim pozitivom." + }, + "min_score": { + "label": "Minimalna pouzdanost", + "description": "Minimalna pouzdanost detekcije po okviru potrebna da bi objekt bio brojan." + }, + "mask": { + "label": "Maska filtriranja", + "description": "Koordinate poligona koje definiraju područje na kojem se ovaj filter primjenjuje unutar okvira." + }, + "raw_mask": { + "label": "Ručna maska" + } + }, + "mask": { + "label": "Maska objekta", + "description": "Poligonalna maska korištena za spriječavanje detekcije objekta u određenim područjima." + }, + "raw_mask": { + "label": "Ručna maska" + }, + "genai": { + "label": "Konfiguracija GenAI objekta", + "description": "Opcije GenAI za opisivanje praćenih objekata i slanje okvira za generisanje.", + "enabled": { + "label": "Omogući GenAI", + "description": "Omogući generisanje opisa za praćene objekte po zadanim postavkama." + }, + "use_snapshot": { + "label": "Koristi snimke", + "description": "Koristi snimke objekata umjesto miniaturnih slika za generisanje opisa GenAI." + }, + "prompt": { + "label": "Naslovni prompt", + "description": "Zadani šablon upita korišten za generisanje opisa pomoću GenAI." + }, + "object_prompts": { + "label": "Prompti za objekte", + "description": "Prompti po objektu za prilagođavanje izlaza GenAI za specifične oznake." + }, + "objects": { + "label": "GenAI objekti", + "description": "Popis oznaka objekata koje se po defaultu šalju GenAI." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Zone koje moraju biti unesene za objekte da bi se kvalifikovali za generisanje opisa GenAI." + }, + "debug_save_thumbnails": { + "label": "Sačuvajte miniaturne slike", + "description": "Sačuvaj miniaturne slike koje se šalju GenAI za ispravljanje i pregled." + }, + "send_triggers": { + "label": "GenAI izazivači", + "description": "Definiše kada bi se trebale slati okvir za GenAI (na kraju, nakon ažuriranja, itd.).", + "tracked_object_end": { + "label": "Pošalji na kraju", + "description": "Pošalji zahtjev GenAI kada praćeni objekt završi." + }, + "after_significant_updates": { + "label": "Raniji GenAI izazivač", + "description": "Pošalji zahtjev GenAI nakon određenog broja značajnih ažuriranja za praćeni objekt." + } + }, + "enabled_in_config": { + "label": "Originalno stanje GenAI", + "description": "Pokazuje je li GenAI bio omogućen u originalnoj statičkoj konfiguraciji." + } + } + }, + "record": { + "label": "Snimanje", + "description": "Postavke snimanja i zadržavanja za ovu kameru.", + "enabled": { + "label": "Omogući snimanje", + "description": "Omogući ili onemogući snimanje za ovu kameru." + }, + "expire_interval": { + "label": "Interval čišćenja snimanja", + "description": "Minute između čišćenja koja uklanjaju istekle segmente snimaka." + }, + "continuous": { + "label": "Neprekidna retencija", + "description": "Broj dana za čuvanje snimaka bez obzira na praćene objekte ili pokret. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.", + "days": { + "label": "Dane zadržavanja", + "description": "Dana za čuvanje snimaka." + } + }, + "motion": { + "label": "Retencija pokreta", + "description": "Broj dana za čuvanje snimaka izazvanih pokretom bez obzira na praćene objekte. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.", + "days": { + "label": "Dane zadržavanja", + "description": "Dana za čuvanje snimaka." + } + }, + "detections": { + "label": "Retencija detekcije", + "description": "Postavke retencije snimaka za događaje detekcije uključujući trajanje pre/post snimanja.", + "pre_capture": { + "label": "Sekundi pre snimanja", + "description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak." + }, + "post_capture": { + "label": "Sekunde nakon snimanja", + "description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje." + }, + "retain": { + "label": "Zadržavanje događaja", + "description": "Postavke zadržavanja za snimke događaja detekcije.", + "days": { + "label": "Dane zadržavanja", + "description": "Broj dana za koje se zadržavaju snimke događaja detekcije." + }, + "mode": { + "label": "Način zadržavanja", + "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)." + } + } + }, + "alerts": { + "label": "Retencija upozorenja", + "description": "Postavke retencije snimaka za događaje upozorenja uključujući trajanje pre/post snimanja.", + "pre_capture": { + "label": "Sekundi pre snimanja", + "description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak." + }, + "post_capture": { + "label": "Sekunde nakon snimanja", + "description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje." + }, + "retain": { + "label": "Zadržavanje događaja", + "description": "Postavke zadržavanja za snimke događaja detekcije.", + "days": { + "label": "Dane zadržavanja", + "description": "Broj dana za koje se zadržavaju snimke događaja detekcije." + }, + "mode": { + "label": "Način zadržavanja", + "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)." + } + } + }, + "export": { + "label": "Konfiguracija izvoza", + "description": "Postavke koje se koriste prilikom izvoza snimaka kao što su timelapse i ubrzavanje dretve.", + "hwaccel_args": { + "label": "Argumeti ubrzavanja dretve za izvoz", + "description": "Argumeti ubrzavanja dretve za operacije izvoza/prenosa." + }, + "max_concurrent": { + "label": "Maksimalan broj istovremenih izvoza", + "description": "Maksimalan broj poslova izvoza koji se obrađuju istovremeno." + } + }, + "preview": { + "label": "Konfiguracija pregleda", + "description": "Postavke koje kontrolišu kvalitet pregleda snimanja prikazanih u UI.", + "quality": { + "label": "Kvaliteta pregleda", + "description": "Nivo kvalitete pregleda (vrlo_nizak, nizak, srednji, visok, vrlo_visok)." + } + }, + "enabled_in_config": { + "label": "Originalno stanje snimanja", + "description": "Pokazuje je li snimanje bilo omogućeno u originalnoj statičkoj konfiguraciji." + } + }, + "review": { + "label": "Pregled", + "description": "Postavke koje kontrolišu upozorenja, detekcije i sažetke pregleda GenAI korišteni od strane UI i skladišta za ovu kameru.", + "alerts": { + "label": "Konfiguracija upozorenja", + "description": "Postavke za koje objekti praćeni generišu upozorenja i kako se upozorenja zadržavaju.", + "enabled": { + "label": "Omogući upozorenja", + "description": "Omogući ili onemogući generisanje upozorenja za ovu kameru." + }, + "labels": { + "label": "Oznake upozorenja", + "description": "Lista oznaka objekata koje se smatraju upozorenjima (npr. automobil, osoba)." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Zone koje objekt mora ući da bi se smatrao upozorenjem; ostavite prazno da omogućite bilo koju zonu." + }, + "enabled_in_config": { + "label": "Originalno stanje upozorenja", + "description": "Pratiti je li upozorenja izvorno omogućena u statičkoj konfiguraciji." + }, + "cutoff_time": { + "label": "Vrijeme prekida upozorenja", + "description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje upozorenje prije nego se prekine upozorenje." + } + }, + "detections": { + "label": "Konfiguracija detekcija", + "description": "Postavke koje objekti koje se praćenje generišu detekcije (nepozornja) i kako se detekcije čuvaju.", + "enabled": { + "label": "Omogući detekcije", + "description": "Omogući ili onemogući događaje detekcije za ovu kameru." + }, + "labels": { + "label": "Oznake detekcije", + "description": "Popis oznaka objekata koje kvalifikuju kao događaji detekcije." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Zone koje objekt mora ući da bi se smatrao detekcijom; ostavite prazno da omogućite bilo koju zonu." + }, + "cutoff_time": { + "label": "Vrijeme prekida detekcija", + "description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje detekciju prije nego se prekine detekcija." + }, + "enabled_in_config": { + "label": "Originalno stanje detekcija", + "description": "Pratiti je li detekcije izvorno omogućene u statičkoj konfiguraciji." + } + }, + "genai": { + "label": "Konfiguracija GenAI", + "description": "Kontrolira korištenje generativne AI za proizvodnju opisa i sažetaka stavki za pregled.", + "enabled": { + "label": "Omogući opise GenAI", + "description": "Omogući ili onemogući opise i sažetke generirane GenAI za stavke za pregled." + }, + "alerts": { + "label": "Omogući GenAI za upozorenja", + "description": "Koristi GenAI za generiranje opisa stavki upozorenja." + }, + "detections": { + "label": "Omogući GenAI za detekcije", + "description": "Koristite GenAI za generiranje opisa predmeta detekcije." + }, + "image_source": { + "label": "Pregledajte izvor slike", + "description": "Izvor slika poslatih GenAIJ-u ('preview' ili 'recordings'); 'recordings' koristi kvalitetnije okvire, ali više tokena." + }, + "additional_concerns": { + "label": "Dodatne brige", + "description": "Popis dodatnih briga ili napomena koje GenAI treba uzeti u obzir prilikom procjene aktivnosti na ovoj kameri." + }, + "debug_save_thumbnails": { + "label": "Sačuvajte miniaturne slike", + "description": "Sačuvajte miniaturne slike koje se šalju GenAI provajderu za ispravljanje grešaka i pregled." + }, + "enabled_in_config": { + "label": "Originalno stanje GenAI", + "description": "Pratiti je li pregled GenAI izvorno omogućen u statičkoj konfiguraciji." + }, + "preferred_language": { + "label": "Preferirani jezik", + "description": "Preferirani jezik za zahtijevanje od GenAI provajdera za generirane odgovore." + }, + "activity_context_prompt": { + "label": "Prompt konteksta aktivnosti", + "description": "Prilagođeni prompt koji opisuje što je i što nije sumnjivo ponašanje kako bi pružio kontekst za sažetke GenAI." + } + } + }, + "semantic_search": { + "label": "Semantička pretraga", + "description": "Postavke za semantičku pretragu koja konstruira i upita uključivanje objekata kako bi pronašla slične stavke.", + "triggers": { + "label": "Pokretači", + "description": "Akcije i kriteriji za usklađivanje za pokretače semantičke pretrage specifične za kameru.", + "friendly_name": { + "label": "Prijateljsko ime", + "description": "Nepovlačno prijateljsko ime prikazano u korisničkom sučelju za ovaj pokretač." + }, + "enabled": { + "label": "Omogući ovaj pokretač", + "description": "Omogući ili onemogući ovaj pokretač semantičke pretrage." + }, + "type": { + "label": "Tip pokretača", + "description": "Tip pokretača: 'thumbnail' (uspoređivanje slikom) ili 'description' (uspoređivanje teksta)." + }, + "data": { + "label": "Sadržaj pokretača", + "description": "Tekstualni izraz ili ID miniaturne slike za uspoređivanje s praćenim objektima." + }, + "threshold": { + "label": "Prag aktivacije", + "description": "Minimalna ocjena sličnosti (0-1) potrebna za aktivaciju ovog izazivača." + }, + "actions": { + "label": "Akcije izazivača", + "description": "Popis akcija koje se izvršavaju kada izazivač odgovara (obavijest, pod_naziv, atribute)." + } + } + }, + "snapshots": { + "label": "Snimci", + "description": "Postavke za snimke generirane preko API-ja za praćene objekte za ovu kameru.", + "enabled": { + "label": "Omogući snimke", + "description": "Omogući ili onemogući snimanje snimaka za ovu kameru." + }, + "timestamp": { + "label": "Preklapanje vremenske oznake", + "description": "Preklopiti vremensku oznaku na snimke iz API-ja." + }, + "bounding_box": { + "label": "Preklapanje okvira", + "description": "Crtanje okvira za praćene objekte na snimke iz API-ja." + }, + "crop": { + "label": "Izrezivanje snimke", + "description": "Izrezivanje snimki iz API-ja do okvira detektiranog objekta." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Zone koje objekt mora ući da bi snimka bila sačuvana." + }, + "height": { + "label": "Visina snimke", + "description": "Visina (pikseli) za promjenu veličine snimki iz API-ja; ostavite prazno da biste sačuvali originalnu veličinu." + }, + "retain": { + "label": "Zadržavanje snimki", + "description": "Postavke zadržavanja snimki uključujući zadane dane i prekriženja po objektu.", + "default": { + "label": "Zadano zadržavanje", + "description": "Zadani broj dana za zadržavanje snimki." + }, + "mode": { + "label": "Način zadržavanja", + "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)." + }, + "objects": { + "label": "Zadržavanje objekata", + "description": "Prekriženja po objektu za dane zadržavanja snimki." + } + }, + "quality": { + "label": "Kvaliteta snimka", + "description": "Kvaliteta kodiranja za sačuvane snimke (0-100)." + } + }, + "timestamp_style": { + "label": "Stil vremenske oznake", + "description": "Opcije stilizacije za vremenske oznake u snimcima i snimcima.", + "position": { + "label": "Pozicija vremenske oznake", + "description": "Pozicija vremenske oznake na slici (tl/tr/bl/br)." + }, + "format": { + "label": "Format vremenske oznake", + "description": "String formata datuma i vremena korišten za vremenske oznake (Python format koda za datum i vrijeme)." + }, + "color": { + "label": "Boja vremenske oznake", + "description": "RGB vrijednosti boja za tekst vremenske oznake (sve vrijednosti 0-255).", + "red": { + "label": "Crvena", + "description": "Crveni komponent (0-255) za boju vremenske oznake." + }, + "green": { + "label": "Zelena", + "description": "Zeleni komponent (0-255) za boju vremenske oznake." + }, + "blue": { + "label": "Plava", + "description": "Plavi komponent (0-255) za boju vremenske oznake." + } + }, + "thickness": { + "label": "Debljina vremenske oznake", + "description": "Debljina linije teksta vremenske oznake." + }, + "effect": { + "label": "Efekt vremenske oznake", + "description": "Vizualni efekt za tekst vremenske oznake (none, solid, shadow)." + } + }, + "best_image_timeout": { + "label": "Vrijeme čekanja za najbolju sliku", + "description": "Koliko dugo čekati na sliku s najvišim stupnjem pouzdanosti." + }, + "mqtt": { + "label": "MQTT", + "description": "Postavke objave slika preko MQTT.", + "enabled": { + "label": "Pošalji sliku", + "description": "Omogući objavljivanje snimaka slika za objekte na MQTT teme za ovu kameru." + }, + "timestamp": { + "label": "Dodaj vremensku oznaku", + "description": "Preklopiti vremensku oznaku na slike objavljene preko MQTT." + }, + "bounding_box": { + "label": "Dodaj okvir", + "description": "Crtaj okvire na slikama objavljenim preko MQTT." + }, + "crop": { + "label": "Iscijepi sliku", + "description": "Iscijepi slike objavljene preko MQTT na okvir detektiranog objekta." + }, + "height": { + "label": "Visina slike", + "description": "Visina (piksela) za promjenu veličine slika objavljenih preko MQTT." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Zone koje objekt mora ući da bi se slika preko MQTT objavila." + }, + "quality": { + "label": "Kvaliteta JPEG", + "description": "Kvaliteta JPEG za slike objavljene preko MQTT (0-100)." + } + }, + "notifications": { + "label": "Obavještenja", + "description": "Postavke za omogućavanje i kontrolu obavijesti za ovu kameru.", + "enabled": { + "label": "Omogući obavijesti", + "description": "Omogući ili onemogući obavijesti za ovu kameru." + }, + "email": { + "label": "E-mail za obavijesti", + "description": "Adresa e-maila koja se koristi za obavijesti putem push-a ili je potrebna određenim dobavljačima obavijesti." + }, + "cooldown": { + "label": "Period hlađenja", + "description": "Period hlađenja (sekunde) između obavijesti kako bi se izbjeglo spaming primateljima." + }, + "enabled_in_config": { + "label": "Originalno stanje obavijesti", + "description": "Pokazuje je li obavijesti bile omogućene u originalnoj statičkoj konfiguraciji." + } + }, + "onvif": { + "label": "ONVIF", + "description": "Postavke povezivanja preko ONVIF i automatskog praćenja PTZ za ovu kameru.", + "host": { + "label": "Gost ONVIF", + "description": "Gost (i opcionalni shema) za uslugu ONVIF za ovu kameru." + }, + "port": { + "label": "Port ONVIF", + "description": "Broj porta za uslugu ONVIF." + }, + "user": { + "label": "Korisničko ime za ONVIF", + "description": "Korisničko ime za autentifikaciju ONVIF; neki uređaji zahtijevaju korisnika admin za ONVIF." + }, + "password": { + "label": "Lozinka za ONVIF", + "description": "Lozinka za autentifikaciju ONVIF." + }, + "tls_insecure": { + "label": "Onemogući provjeru TLS", + "description": "Preskoči provjeru TLS i onemogući digest autentifikaciju za ONVIF (nebezbedno; koristiti samo u sigurnim mrežama)." + }, + "profile": { + "label": "ONVIF profil", + "description": "Specifičan ONVIF medij profil za korištenje za kontrolu PTZ, prilagođen tokenom ili imenom. Ako nije postavljen, prvi profil s važećom konfiguracijom PTZ automatski se odabire." + }, + "autotracking": { + "label": "Autotračenje", + "description": "Automatski praćenje pokretanja objekata i držanje ih u sredini okvira korištenjem pokreta kamere PTZ.", + "enabled": { + "label": "Omogući automatsko praćenje", + "description": "Omogući ili onemogući automatsko praćenje kamere PTZ detektiranih objekata." + }, + "calibrate_on_startup": { + "label": "Kalibriraj na početku", + "description": "Mjeri brzine motora PTZ pri pokretanju kako bi poboljšao preciznost praćenja. Frigate će ažurirati konfiguraciju s težinama pokreta nakon kalibracije." + }, + "zooming": { + "label": "Režim zumiranja", + "description": "Kontrola ponašanja zumiranja: onemogućeno (samo pan/tilt), apsolutno (najkompatibilnije) ili relativno (konkurentno pan/tilt/zum)." + }, + "zoom_factor": { + "label": "Faktor zumiranja", + "description": "Kontrola razine zumiranja na praćenim objektima. Niže vrijednosti drže više scene u pogledu; više vrijednosti zumiraju bliže, ali mogu izgubiti praćenje. Vrijednosti između 0.1 i 0.75." + }, + "track": { + "label": "Praćeni objekti", + "description": "Popis vrsta objekata koji trebaju pokrenuti automatsko praćenje." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Objekti moraju ući u jednu od ovih zona prije nego što započne automatsko praćenje." + }, + "return_preset": { + "label": "Povratak na predpostavku", + "description": "Ime predpostavke konfigurirano u firmware kamere za povratak nakon završetka praćenja." + }, + "timeout": { + "label": "Vrijeme čekanja povratka", + "description": "Čekajte ovaj broj sekundi nakon gubitka praćenja prije povratka kamere na predpostavljeno mjesto." + }, + "movement_weights": { + "label": "Težine pokreta", + "description": "Vrijednosti kalibracije automatski generirane kroz kalibraciju kamere. Ne mijenjajte ručno." + }, + "enabled_in_config": { + "label": "Originalni stanje autotračenja", + "description": "Unutarnje polje za praćenje je li autotračenje bilo omogućeno u konfiguraciji." + } + }, + "ignore_time_mismatch": { + "label": "Zanemari razliku u vremenu", + "description": "Zanemari razlike u sinhronizaciji vremena između kamere i Frigate servera za komunikaciju ONVIF." + } + }, + "type": { + "label": "Tip kamere", + "description": "Tip kamere" + }, + "ui": { + "label": "Korisnički interfejs kamere", + "description": "Prikaz redoslijeda i vidljivosti za ovu kameru u UI. Redoslijed utječe na zadani nadzorno pločo. Za detaljniju kontrolu koristite grupe kamere.", + "order": { + "label": "Redoslijed UI", + "description": "Numerički redoslijed koristi se za sortiranje kamere u UI (zadani nadzorno pločo i popisi); veći brojevi pojavljuju se kasnije." + }, + "dashboard": { + "label": "Prikaži u UI", + "description": "Prekidač je li ova kamera vidljiva svuda u UI Frigate. Onemogućavanje ovoga zahtijeva ručno uređivanje konfiguracije za ponovno prikazivanje ove kamere u UI." + } + }, + "webui_url": { + "label": "URL kamere", + "description": "URL za pristup kamere izravno iz stranice sustava" + }, + "profiles": { + "label": "Profili", + "description": "Imenovane konfiguracijske profile s parcijalnim preklopima koji se mogu aktivirati tijekom izvršavanja." + }, + "enabled_in_config": { + "label": "Originalno stanje kamere", + "description": "Pratite originalno stanje kamere." + } +} diff --git a/web/public/locales/bs/config/global.json b/web/public/locales/bs/config/global.json new file mode 100644 index 0000000..84e370a --- /dev/null +++ b/web/public/locales/bs/config/global.json @@ -0,0 +1,1596 @@ +{ + "version": { + "label": "Trenutna verzija konfiguracije", + "description": "Numerička ili string verzija aktivne konfiguracije za pomoć pri otkrivanju migracija ili promjena formata." + }, + "audio": { + "label": "Audio događaji", + "enabled": { + "label": "Omogući detekciju zvuka", + "description": "Omogući ili onemogući detekciju zvučnih događaja za sve kamere; mogu se prekrivati po kameri." + }, + "max_not_heard": { + "label": "Vrijeme trajanja do kraja", + "description": "Količina sekundi bez konfiguriranog tipa zvuka prije nego što se audio događaj završi." + }, + "min_volume": { + "label": "Minimalna zapremina", + "description": "Minimalni prag RMS zapremine potreban za pokretanje detekcije zvuka; niže vrijednosti povećavaju osjetljivost (npr. 200 visoko, 500 srednje, 1000 nisko)." + }, + "listen": { + "label": "Tipovi slušanja", + "description": "Popis tipova audio događaja za detekciju (npr. zavijanje, požarne zvona, vrisak, govorenje, vikanje)." + }, + "filters": { + "label": "Audio filteri", + "description": "Postavke filtera po tipu zvuka kao što su pragovi pouzdanosti za smanjenje lažnih pozitiva." + }, + "enabled_in_config": { + "label": "Originalno stanje zvuka", + "description": "Indikuje je li detekcija zvuka izvorno omogućena u statičkoj konfiguracijskoj datoteci." + }, + "num_threads": { + "label": "Dretve detekcije", + "description": "Broj dretvi za korištenje za obradu detekcije zvuka." + }, + "description": "Postavke za detekciju događaja na osnovu zvuka za sve kamere; mogu se prekrivati po kameri." + }, + "audio_transcription": { + "label": "Transkripcija zvuka", + "description": "Postavke za transkripciju živog i govornog zvuka korištenih za događaje i žive podnaslove.", + "live_enabled": { + "label": "Uživo transkripcija", + "description": "Omogući streaming uživo transkripcije za audio dok se prima." + }, + "enabled": { + "label": "Omogući transkripciju zvuka", + "description": "Omogući ili onemogući automatsku transkripciju zvuka za sve kamere; može se prekrimiti po kamere." + }, + "language": { + "label": "Jezik za transkripciju", + "description": "Kod jezika korišten za transkripciju/prevod (npr. 'en' za engleski). Pogledajte https://whisper-api.com/docs/languages/ za podržane kodove jezika." + }, + "device": { + "label": "Uređaj za transkripciju", + "description": "Ključ uređaja (CPU/GPU) za izvršavanje modela transkripcije. Trenutno se podržavaju samo NVIDIA CUDA GPU-ovi za transkripciju." + }, + "model_size": { + "label": "Veličina modela", + "description": "Veličina modela za korištenje za offline transkripciju zvučnih događaja." + } + }, + "birdseye": { + "label": "Birdseye", + "description": "Postavke za sastavni prikaz Birdseye koji kombinuje više snimke kamere u jedinstveni raspored.", + "enabled": { + "label": "Omogući Birdseye", + "description": "Omogući ili onemogući funkciju prikaza Birdseye." + }, + "mode": { + "label": "Način praćenja", + "description": "Način uključivanja kamera u Birdseye: 'objekti', 'kretanje' ili 'kontinuirano'." + }, + "order": { + "label": "Pozicija", + "description": "Numerička pozicija koja kontroliše redoslijed kamera u rasporedu Birdseye." + }, + "restream": { + "label": "Ponovno prenos RTSP", + "description": "Ponovno prenos izlaza Birdseye kao RTSP tok; uključivanje ovoga će održavati Birdseye u neprekidnom radu." + }, + "width": { + "label": "Širina", + "description": "Širina izlaza (piksela) sastavljenog okvira Birdseye." + }, + "height": { + "label": "Visina", + "description": "Visina izlaza (piksela) sastavljenog okvira Birdseye." + }, + "quality": { + "label": "Kvalitet kodiranja", + "description": "Kvalitet kodiranja za Birdseye mpeg1 tok (1 najviši kvalitet, 31 najniži)." + }, + "inactivity_threshold": { + "label": "Prag neaktivnosti", + "description": "Sekunde neaktivnosti nakon kojih će kamera prestati da se prikazuje u Birdseye." + }, + "layout": { + "label": "Razmještaj", + "description": "Opcije razmještaja za sastavljanje Birdseye.", + "scaling_factor": { + "label": "Faktor skaliranja", + "description": "Faktor skaliranja korišten od strane računala za razmještaj (opseg 1.0 do 5.0)." + }, + "max_cameras": { + "label": "Maksimalan broj kamera", + "description": "Maksimalan broj kamera koje se mogu prikazati istovremeno u Birdseye; prikazuje najnovije kamere." + } + }, + "idle_heartbeat_fps": { + "label": "Neaktivno srčanog udaraca FPS", + "description": "Broj okvira po sekundi za ponovno slanje posljednjeg sastavljenog Birdseye okvira kada je neaktivno; postavite na 0 za onemogućavanje." + } + }, + "detect": { + "label": "Detekcija objekata", + "description": "Postavke za ulogu detekcije/detekcija koja se koristi za pokretanje detekcije objekata i inicijalizaciju praćenja.", + "enabled": { + "label": "Omogući detekciju objekata", + "description": "Omogući ili onemogući detekciju objekata za sve kamere; može se prekrimiti po kamere." + }, + "height": { + "label": "Visina detekcije", + "description": "Visina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a." + }, + "width": { + "label": "Širina detekcije", + "description": "Širina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a." + }, + "fps": { + "label": "Detekcija FPS", + "description": "Željeni broj okvira po sekundi za pokretanje detekcije; niže vrijednosti smanjuju upotrebu CPU-a (preporučena vrijednost je 5, postavite više - najviše 10 - samo ako praćite vrlo brze objekte)." + }, + "min_initialized": { + "label": "Minimalni broj okvira inicijalizacije", + "description": "Broj uzastopnih detekcija potreban prije stvaranja praćenog objekta. Povećajte da biste smanjili lažne inicijalizacije. Zadana vrijednost je fps podijeljeno sa 2." + }, + "max_disappeared": { + "label": "Maksimalni broj okvira koji su nestali", + "description": "Broj okvira bez detekcije prije nego što se praćeni objekt smatra izgubljenim." + }, + "stationary": { + "label": "Konfiguracija stacionarnih objekata", + "description": "Postavke za detekciju i upravljanje objektima koji ostaju stacionarni tokom određenog vremena.", + "interval": { + "label": "Stacionarni interval", + "description": "Kako često (u snimcima) pokretati provjeru detekcije da biste potvrdili stacionarni objekt." + }, + "threshold": { + "label": "Stacionarni prag", + "description": "Broj snimaka bez promjene pozicije potreban da bi objekt bio označen kao stacionarni." + }, + "max_frames": { + "label": "Maksimalni snimci", + "description": "Ograničava koliko dugo se stacionarni objekti praćaju prije nego što se odbacuju.", + "default": { + "label": "Zadani maksimalni snimci", + "description": "Zadani maksimalni broj snimaka za praćenje stacionarnog objekta prije prestanka." + }, + "objects": { + "label": "Maksimalni snimci po objektu", + "description": "Podešavanja po objektu za maksimalni broj snimaka za praćenje stacionarnih objekata." + } + }, + "classifier": { + "label": "Omogući vizualni klasifikator", + "description": "Koristi vizualni klasifikator za detekciju pravozadanih stacionarnih objekata čak i kada se okviri tresu." + } + }, + "annotation_offset": { + "label": "Pomak oznake", + "description": "Milisekunde za pomak detektiranih oznaka kako bi se bolje poravnali vremenski okviri s snimcima; može biti pozitivan ili negativan." + } + }, + "face_recognition": { + "label": "Prepoznavanje lica", + "enabled": { + "label": "Omogući prepoznavanje lica", + "description": "Omogući ili onemogući prepoznavanje lica za sve kamere; mogu se preklopiti po kameri." + }, + "min_area": { + "label": "Minimalna površina lica", + "description": "Minimalna površina (pikseli) detektiranog okvira lica potrebna za pokušaj prepoznavanja." + }, + "description": "Postavke za detekciju i prepoznavanje lica za sve kamere; mogu se preklopiti po kameri.", + "model_size": { + "label": "Veličina modela", + "description": "Veličina modela za korištenje za ugradnje lica (small/large); veće može zahtijevati GPU." + }, + "unknown_score": { + "label": "Prag neznatnog rezultata", + "description": "Prag udaljenosti ispod kojeg se lice smatra potencijalnim odgovarajućim (viši = stroži)." + }, + "detection_threshold": { + "label": "Prag detekcije", + "description": "Minimalni prag pouzdanosti potreban za razmatranje detekcije lica kao važeće." + }, + "recognition_threshold": { + "label": "Prag prepoznavanja", + "description": "Prag udaljenosti ugradnje lica za razmatranje dva lica kao odgovarajuća." + }, + "min_faces": { + "label": "Minimalan broj lica", + "description": "Minimalan broj prepoznavanja lica potreban prije nego što se primijeni prepoznati podnaziv za osobu." + }, + "save_attempts": { + "label": "Pokušaji sačuvanja", + "description": "Broj pokušaja prepoznavanja lica koje se treba sačuvati za korisnički sučelje najnovijih prepoznavanja." + }, + "blur_confidence_filter": { + "label": "Filter pouzdanosti za zamagljenost", + "description": "Prilagodite ocjene pouzdanosti na temelju zamagljenosti slike kako biste smanjili lažne pozitive za loše kvalitete lica." + }, + "device": { + "label": "Uređaj", + "description": "Ovo je prekršaj, da biste ciljali specifičan uređaj. Pogledajte https://onnxruntime.ai/docs/execution-providers/ za više informacija" + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "Postavke FFmpeg uključuju putanju binarne datoteke, argumente, opcije hwaccel i izlazne argumente po ulozi.", + "path": { + "label": "Putanja do FFmpeg binarne datoteke", + "description": "Putanja do FFmpeg binarne datoteke ili verzija alias (\"5.0\" ili \"7.0\")." + }, + "global_args": { + "label": "Globalni argumenti FFmpeg-a", + "description": "Globalni argumenti prebačeni na procese FFmpeg." + }, + "hwaccel_args": { + "label": "Argumenti za ubrzanje hardvera", + "description": "Argumenti za ubrzanje hardvera za FFmpeg. Preporučuju se predložci specifični za dobavljača." + }, + "input_args": { + "label": "Unos argumenata", + "description": "Ulazni argumenti primjenjeni na ulazne snimke FFmpeg." + }, + "output_args": { + "label": "Izlazni argumenti", + "description": "Zadani izlazni argumenti korišteni za različite uloge FFmpeg-a poput detekcije i snimanja.", + "detect": { + "label": "Izlazni argumenti za detekciju", + "description": "Zadani izlazni argumenti za snimke uloga detekcije." + }, + "record": { + "label": "Izlazni argumenti za snimanje", + "description": "Zadani izlazni argumenti za snimke uloga snimanja." + } + }, + "retry_interval": { + "label": "Vrijeme ponovnog pokušaja FFmpeg-a", + "description": "Sekunde koje treba čekati prije nego što se pokuša ponovno uspostaviti veza s tokom kamere nakon neuspjeha. Zadano je 10." + }, + "apple_compatibility": { + "label": "Kompatibilnost s Apple-om", + "description": "Omogući označavanje HEVC za bolju kompatibilnost s igračima Apple-a prilikom snimanja H.265." + }, + "gpu": { + "label": "Indeks GPU-a", + "description": "Zadani indeks GPU-a korišten za ubrzanje hardvera ako je dostupan." + }, + "inputs": { + "label": "Ulazni podaci kamere", + "description": "Popis definicija ulaznih tokova (putanje i uloge) za ovu kameru.", + "path": { + "label": "Putanja ulaza", + "description": "URL ili putanja ulaznog toka kamere." + }, + "roles": { + "label": "Uloge ulaza", + "description": "Uloge za ovaj ulazni tok." + }, + "global_args": { + "label": "Globalni argumenti FFmpeg-a", + "description": "Globalni argumenti FFmpeg-a za ovaj ulazni tok." + }, + "hwaccel_args": { + "label": "Argumenti za ubrzanje hardvera", + "description": "Argumenti za ubrzanje hardvera za ovaj ulazni stream." + }, + "input_args": { + "label": "Unos argumenata", + "description": "Argumeti unosa specifični za ovaj stream." + } + } + }, + "live": { + "label": "Uživo prikaz", + "streams": { + "label": "Imena živih streamova", + "description": "Mapiranje konfiguriranih imena streamova na imena restream/go2rtc korишtena za uživo prikaz." + }, + "height": { + "label": "Visina uživo", + "description": "Visina (piksela) za prikaz jsmpeg živog streama u Web UI; mora biti <= visina detektiranog streama." + }, + "quality": { + "label": "Kvalitet uživo", + "description": "Kvalitet kodiranja za jsmpeg stream (1 najviši, 31 najniži)." + }, + "description": "Postavke za kontrolu rezolucije i kvalitete žive struje jsmpeg. Ovo ne utiče na kamere koje koriste go2rtc za živi pregled." + }, + "lpr": { + "label": "Prepoznavanje tablice vozila", + "description": "Postavke prepoznavanja tablice vozila uključujući pragovi detekcije, formatiranje i poznate tablice.", + "enabled": { + "label": "Omogući LPR", + "description": "Omogući ili onemogući prepoznavanje tablice za sve kamere; može se prekršiti po kamere." + }, + "expire_time": { + "label": "Sekunde isteka", + "description": "Vrijeme u sekundama nakon kojeg nevidljiva tablica istječe iz praćenja (samo za dedikovane LPR kamere)." + }, + "min_area": { + "label": "Minimalna površina tablice", + "description": "Minimalna površina tablice (piksela) potrebna za pokušaj prepoznavanja." + }, + "enhancement": { + "label": "Nivo poboljšanja", + "description": "Nivo poboljšanja (0-10) za primjenu na isječke tablice prije OCR-a; veće vrijednosti ne moraju uvijek poboljšati rezultate, nivoi iznad 5 mogu raditi samo s tablicama u noćnom vremenu i trebaju se koristiti s oprezom." + }, + "model_size": { + "label": "Veličina modela", + "description": "Veličina modela korištena za detekciju/pretvorbu teksta. Većina korisnika treba koristiti 'small'." + }, + "detection_threshold": { + "label": "Prag detekcije", + "description": "Prag pouzdanosti detekcije za početak izvršavanja OCR na sumnjivim pločama." + }, + "recognition_threshold": { + "label": "Prag prepoznavanja", + "description": "Prag pouzdanosti potreban za prepoznati tekst ploče da bi se priložio kao podnaziv." + }, + "min_plate_length": { + "label": "Minimalna dužina ploče", + "description": "Minimalan broj znakova koje prepoznata ploča mora sadržavati da bi se smatrala važećom." + }, + "format": { + "label": "Regex formata ploče", + "description": "Nepovlačen regex za provjeru prepoznatih nizova ploča protiv očekivanog formata." + }, + "match_distance": { + "label": "Razlika u odgovaranju", + "description": "Broj nepravilnih znakova dopuštenih pri uspoređivanju detektiranih ploča s poznatim pločama." + }, + "known_plates": { + "label": "Poznate ploče", + "description": "Popis ploča ili regexa za posebno praćenje ili upozorenje." + }, + "debug_save_plates": { + "label": "Sačuvaj tablice za debagovanje", + "description": "Sačuvaj slike izrezaka tablica za debagovanje performansi LPR." + }, + "device": { + "label": "Uređaj", + "description": "Ovo je preklop za ciljanje specifičnog uređaja. Vidi https://onnxruntime.ai/docs/execution-providers/ za više informacija" + }, + "replace_rules": { + "label": "Pravila zamjene", + "description": "Pravila zamjene regex korишtena za normalizaciju detektiranih stringova ploča prije uspoređivanja.", + "pattern": { + "label": "Regex uzorak" + }, + "replacement": { + "label": "Zamjenski string" + } + } + }, + "motion": { + "label": "Detekcija pokreta", + "enabled": { + "label": "Omogući detekciju pokreta", + "description": "Omogući ili onemogući detekciju pokreta za sve kamere; može se prekrimiti po kamere." + }, + "threshold": { + "label": "Prag pokreta", + "description": "Prag razlike piksela korišten za detektor pokreta; veće vrijednosti smanjuju osjetljivost (opseg 1-255)." + }, + "lightning_threshold": { + "label": "Prag munje", + "description": "Prag za detekciju i zanemarivanje kratkih iskri svjetlosti (niže vrijednosti povećavaju osjetljivost, vrijednosti između 0.3 i 1.0). Ovo ne spriječava detekciju pokreta u potpunosti; jednostavno zaustavlja detektor da analizira dodatne okvire nakon što se prag premaši. Snimci temeljeni na pokretima i dalje se stvaraju tijekom ovih događaja." + }, + "skip_motion_threshold": { + "label": "Preskoči prag pokreta", + "description": "Ako se postavi na vrijednost između 0.0 i 1.0, i ako se više od ovog udjela slike promijeni u jednom okviru, detektor neće vratiti kutije pokreta i odmah će se ponovno kalibrirati. Ovo može uštedjeti CPU i smanjiti lažne pozitive tijekom munje, oluje itd., ali može propustiti stvarne događaje kao što je automatsko praćenje objekta PTZ kamerom. Tržište je između izgube nekoliko megabajta snimaka i pregleda nekoliko kratkih zapisnika. Ostavite nepostavljeno (Nijedno) za onemogućavanje ove funkcije." + }, + "improve_contrast": { + "label": "Poboljšaj kontrast", + "description": "Primijeni poboljšanje kontrasta na okvire prije analize pokreta kako bi pomoću detekcije." + }, + "contour_area": { + "label": "Površina kontura", + "description": "Minimalna površina kontura u pikselima potrebna za brojanje kontura pokreta." + }, + "delta_alpha": { + "label": "Delta alfa", + "description": "Faktor alfa spajanja korišten za razliku okvira za izračun pokreta." + }, + "frame_alpha": { + "label": "Alfa okvira", + "description": "Vrijednost alfa korištena prilikom spajanja okvira za predobradbu pokreta." + }, + "frame_height": { + "label": "Visina okvira", + "description": "Visina u pikselima na koju se skaliraju okviri prilikom izračuna pokreta." + }, + "mask": { + "label": "Koordinate maska", + "description": "Uredno x,y koordinate koje definiraju poligon maska pokreta za uključivanje/isključivanje područja." + }, + "mqtt_off_delay": { + "label": "MQTT zakasnjenje isključivanja", + "description": "Sekunde koje se čekaju nakon posljednjeg pokreta prije objave MQTT 'isključeno' stanje." + }, + "enabled_in_config": { + "label": "Originalno stanje pokreta", + "description": "Indikira je li detekcija pokreta bila omogućena u originalnoj statičkoj konfiguraciji." + }, + "raw_mask": { + "label": "Ručna maska" + }, + "description": "Zadane postavke detekcije pokreta primjenjene na kamere osim ako se prekrivaju po kamere." + }, + "objects": { + "label": "Objekti", + "description": "Zadani parametri praćenja objekata uključujući koje oznake praćenja i filtre po objektu.", + "track": { + "label": "Objekti za praćenje", + "description": "Popis oznaka objekata za praćenje za sve kamere; može se prekrimiti po kamere." + }, + "filters": { + "label": "Filtar objekata", + "description": "Filtar primijenjen na detektirane objekte kako bi se smanjila broj lažnih pozitiva (površina, omjer, pouzdanost).", + "min_area": { + "label": "Minimalna površina objekta", + "description": "Minimalna površina okvira (pikseli ili postotak) potrebna za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)." + }, + "max_area": { + "label": "Maksimalna površina objekta", + "description": "Maksimalna površina okvira (pikseli ili postotak) dozvoljena za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)." + }, + "min_ratio": { + "label": "Minimalni omjer visine/širine", + "description": "Minimalni omjer širine/visine potreban da bi okvir bio prihvaćen." + }, + "max_ratio": { + "label": "Maksimalni omjer visine/širine", + "description": "Maksimalni omjer širine/visine dozvoljen da bi okvir bio prihvaćen." + }, + "threshold": { + "label": "Prag pouzdanosti", + "description": "Prosjek pragova pouzdanosti detekcije potreban da bi objekt bio smatravan pravim pozitivom." + }, + "min_score": { + "label": "Minimalna pouzdanost", + "description": "Minimalna pouzdanost detekcije po okviru potrebna da bi objekt bio brojan." + }, + "mask": { + "label": "Maska filtriranja", + "description": "Koordinate poligona koje definiraju područje na kojem se ovaj filter primjenjuje unutar okvira." + }, + "raw_mask": { + "label": "Ručna maska" + } + }, + "mask": { + "label": "Maska objekta", + "description": "Poligonalna maska korištena za spriječavanje detekcije objekta u određenim područjima." + }, + "raw_mask": { + "label": "Ručna maska" + }, + "genai": { + "label": "Konfiguracija GenAI objekta", + "description": "Opcije GenAI za opisivanje praćenih objekata i slanje okvira za generisanje.", + "enabled": { + "label": "Omogući GenAI", + "description": "Omogući generisanje opisa za praćene objekte po zadanim postavkama." + }, + "use_snapshot": { + "label": "Koristi snimke", + "description": "Koristi snimke objekata umjesto miniaturnih slika za generisanje opisa GenAI." + }, + "prompt": { + "label": "Naslovni prompt", + "description": "Zadani šablon upita korišten za generisanje opisa pomoću GenAI." + }, + "object_prompts": { + "label": "Prompti za objekte", + "description": "Prompti po objektu za prilagođavanje izlaza GenAI za specifične oznake." + }, + "objects": { + "label": "GenAI objekti", + "description": "Popis oznaka objekata koje se po defaultu šalju GenAI." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Zone koje moraju biti unesene za objekte da bi se kvalifikovali za generisanje opisa GenAI." + }, + "debug_save_thumbnails": { + "label": "Sačuvajte miniaturne slike", + "description": "Sačuvaj miniaturne slike koje se šalju GenAI za ispravljanje i pregled." + }, + "send_triggers": { + "label": "GenAI izazivači", + "description": "Definiše kada bi se trebale slati okvir za GenAI (na kraju, nakon ažuriranja, itd.).", + "tracked_object_end": { + "label": "Pošalji na kraju", + "description": "Pošalji zahtjev GenAI kada praćeni objekt završi." + }, + "after_significant_updates": { + "label": "Raniji GenAI izazivač", + "description": "Pošalji zahtjev GenAI nakon određenog broja značajnih ažuriranja za praćeni objekt." + } + }, + "enabled_in_config": { + "label": "Originalno stanje GenAI", + "description": "Pokazuje je li GenAI bio omogućen u originalnoj statičkoj konfiguraciji." + } + } + }, + "record": { + "label": "Snimanje", + "enabled": { + "label": "Omogući snimanje", + "description": "Omogući ili onemogući snimanje za sve kamere; može se prekrimiti po kamere." + }, + "expire_interval": { + "label": "Interval čišćenja snimanja", + "description": "Minute između čišćenja koja uklanjaju istekle segmente snimaka." + }, + "continuous": { + "label": "Neprekidna retencija", + "description": "Broj dana za čuvanje snimaka bez obzira na praćene objekte ili pokret. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.", + "days": { + "label": "Dane zadržavanja", + "description": "Dana za čuvanje snimaka." + } + }, + "motion": { + "label": "Retencija pokreta", + "description": "Broj dana za čuvanje snimaka izazvanih pokretom bez obzira na praćene objekte. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.", + "days": { + "label": "Dane zadržavanja", + "description": "Dana za čuvanje snimaka." + } + }, + "detections": { + "label": "Retencija detekcije", + "description": "Postavke retencije snimaka za događaje detekcije uključujući trajanje pre/post snimanja.", + "pre_capture": { + "label": "Sekundi pre snimanja", + "description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak." + }, + "post_capture": { + "label": "Sekunde nakon snimanja", + "description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje." + }, + "retain": { + "label": "Zadržavanje događaja", + "description": "Postavke zadržavanja za snimke događaja detekcije.", + "days": { + "label": "Dane zadržavanja", + "description": "Broj dana za koje se zadržavaju snimke događaja detekcije." + }, + "mode": { + "label": "Način zadržavanja", + "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)." + } + } + }, + "alerts": { + "label": "Retencija upozorenja", + "description": "Postavke retencije snimaka za događaje upozorenja uključujući trajanje pre/post snimanja.", + "pre_capture": { + "label": "Sekundi pre snimanja", + "description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak." + }, + "post_capture": { + "label": "Sekunde nakon snimanja", + "description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje." + }, + "retain": { + "label": "Zadržavanje događaja", + "description": "Postavke zadržavanja za snimke događaja detekcije.", + "days": { + "label": "Dane zadržavanja", + "description": "Broj dana za koje se zadržavaju snimke događaja detekcije." + }, + "mode": { + "label": "Način zadržavanja", + "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)." + } + } + }, + "export": { + "label": "Konfiguracija izvoza", + "description": "Postavke koje se koriste prilikom izvoza snimaka kao što su timelapse i ubrzavanje dretve.", + "hwaccel_args": { + "label": "Argumeti ubrzavanja dretve za izvoz", + "description": "Argumeti ubrzavanja dretve za operacije izvoza/prenosa." + }, + "max_concurrent": { + "label": "Maksimalan broj istovremenih izvoza", + "description": "Maksimalan broj poslova izvoza koji se obrađuju istovremeno." + } + }, + "preview": { + "label": "Konfiguracija pregleda", + "description": "Postavke koje kontrolišu kvalitet pregleda snimanja prikazanih u UI.", + "quality": { + "label": "Kvaliteta pregleda", + "description": "Nivo kvalitete pregleda (vrlo_nizak, nizak, srednji, visok, vrlo_visok)." + } + }, + "enabled_in_config": { + "label": "Originalno stanje snimanja", + "description": "Pokazuje je li snimanje bilo omogućeno u originalnoj statičkoj konfiguraciji." + }, + "description": "Postavke za snimanje i zadržavanje primjenjene na kamere osim ako se prekrivaju po kamere." + }, + "review": { + "label": "Pregled", + "alerts": { + "label": "Konfiguracija upozorenja", + "description": "Postavke za koje objekti praćeni generišu upozorenja i kako se upozorenja zadržavaju.", + "enabled": { + "label": "Omogući upozorenja", + "description": "Omogući ili onemogući generisanje upozorenja za sve kamere; može se prekrimiti po kamere." + }, + "labels": { + "label": "Oznake upozorenja", + "description": "Lista oznaka objekata koje se smatraju upozorenjima (npr. automobil, osoba)." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Zone koje objekt mora ući da bi se smatrao upozorenjem; ostavite prazno da omogućite bilo koju zonu." + }, + "enabled_in_config": { + "label": "Originalno stanje upozorenja", + "description": "Pratiti je li upozorenja izvorno omogućena u statičkoj konfiguraciji." + }, + "cutoff_time": { + "label": "Vrijeme prekida upozorenja", + "description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje upozorenje prije nego se prekine upozorenje." + } + }, + "detections": { + "label": "Konfiguracija detekcija", + "description": "Postavke koje objekti koje se praćenje generišu detekcije (nepozornja) i kako se detekcije čuvaju.", + "enabled": { + "label": "Omogući detekcije", + "description": "Omogući ili onemogući događaje detekcije za sve kamere; može se prekrimiti po kamere." + }, + "labels": { + "label": "Oznake detekcije", + "description": "Popis oznaka objekata koje kvalifikuju kao događaji detekcije." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Zone koje objekt mora ući da bi se smatrao detekcijom; ostavite prazno da omogućite bilo koju zonu." + }, + "cutoff_time": { + "label": "Vrijeme prekida detekcija", + "description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje detekciju prije nego se prekine detekcija." + }, + "enabled_in_config": { + "label": "Originalno stanje detekcija", + "description": "Pratiti je li detekcije izvorno omogućene u statičkoj konfiguraciji." + } + }, + "genai": { + "label": "Konfiguracija GenAI", + "description": "Kontrolira korištenje generativne AI za proizvodnju opisa i sažetaka stavki za pregled.", + "enabled": { + "label": "Omogući opise GenAI", + "description": "Omogući ili onemogući opise i sažetke generirane GenAI za stavke za pregled." + }, + "alerts": { + "label": "Omogući GenAI za upozorenja", + "description": "Koristi GenAI za generiranje opisa stavki upozorenja." + }, + "detections": { + "label": "Omogući GenAI za detekcije", + "description": "Koristite GenAI za generiranje opisa predmeta detekcije." + }, + "image_source": { + "label": "Pregledajte izvor slike", + "description": "Izvor slika poslatih GenAIJ-u ('preview' ili 'recordings'); 'recordings' koristi kvalitetnije okvire, ali više tokena." + }, + "additional_concerns": { + "label": "Dodatne brige", + "description": "Popis dodatnih briga ili napomena koje GenAI treba uzeti u obzir prilikom procjene aktivnosti na ovoj kameri." + }, + "debug_save_thumbnails": { + "label": "Sačuvajte miniaturne slike", + "description": "Sačuvajte miniaturne slike koje se šalju GenAI provajderu za ispravljanje grešaka i pregled." + }, + "enabled_in_config": { + "label": "Originalno stanje GenAI", + "description": "Pratiti je li pregled GenAI izvorno omogućen u statičkoj konfiguraciji." + }, + "preferred_language": { + "label": "Preferirani jezik", + "description": "Preferirani jezik za zahtijevanje od GenAI provajdera za generirane odgovore." + }, + "activity_context_prompt": { + "label": "Prompt konteksta aktivnosti", + "description": "Prilagođeni prompt koji opisuje što je i što nije sumnjivo ponašanje kako bi pružio kontekst za sažetke GenAI." + } + }, + "description": "Postavke koje kontrolišu upozorenja, detekcije i GenAI pregledne sažetke korišteni od strane UI i skladišta." + }, + "semantic_search": { + "label": "Semantička pretraga", + "triggers": { + "label": "Pokretači", + "description": "Akcije i kriteriji za usklađivanje za pokretače semantičke pretrage specifične za kameru.", + "friendly_name": { + "label": "Prijateljsko ime", + "description": "Nepovlačno prijateljsko ime prikazano u korisničkom sučelju za ovaj pokretač." + }, + "enabled": { + "label": "Omogući ovaj pokretač", + "description": "Omogući ili onemogući ovaj pokretač semantičke pretrage." + }, + "type": { + "label": "Tip pokretača", + "description": "Tip pokretača: 'thumbnail' (uspoređivanje slikom) ili 'description' (uspoređivanje teksta)." + }, + "data": { + "label": "Sadržaj pokretača", + "description": "Tekstualni izraz ili ID miniaturne slike za uspoređivanje s praćenim objektima." + }, + "threshold": { + "label": "Prag aktivacije", + "description": "Minimalna ocjena sličnosti (0-1) potrebna za aktivaciju ovog izazivača." + }, + "actions": { + "label": "Akcije izazivača", + "description": "Popis akcija koje se izvršavaju kada izazivač odgovara (obavijest, pod_naziv, atribute)." + } + }, + "description": "Postavke za semantičku pretragu koja građi i upita objektne ugradnje da bi pronašla slične stavke.", + "enabled": { + "label": "Omogući semantičku pretragu", + "description": "Omogući ili onemogući funkciju semantičke pretrage." + }, + "reindex": { + "label": "Ponovno indeksiranje pri pokretanju", + "description": "Pokrenite puno ponovno indeksiranje povijesnih praćenih objekata u bazu ugradnji." + }, + "model": { + "label": "Ime modela za semantičku pretragu ili dobavljača GenAI", + "description": "Model ugradnje koji se koristi za semantičku pretragu (npr. 'jinav1'), ili ime dobavljača GenAI s ulogom ugradnje." + }, + "model_size": { + "label": "Veličina modela", + "description": "Izaberite veličinu modela; 'small' radi na CPU i 'large' obično zahtijeva GPU." + }, + "device": { + "label": "Uređaj", + "description": "Ovo je preklop za ciljanje specifičnog uređaja. Vidi https://onnxruntime.ai/docs/execution-providers/ za više informacija" + } + }, + "snapshots": { + "label": "Snimci", + "enabled": { + "label": "Omogući snimke", + "description": "Omogući ili onemogući sačuvanje snimaka za sve kamere; može se prekrimiti po kamere." + }, + "timestamp": { + "label": "Preklapanje vremenske oznake", + "description": "Preklopiti vremensku oznaku na snimke iz API-ja." + }, + "bounding_box": { + "label": "Preklapanje okvira", + "description": "Crtanje okvira za praćene objekte na snimke iz API-ja." + }, + "crop": { + "label": "Izrezivanje snimke", + "description": "Izrezivanje snimki iz API-ja do okvira detektiranog objekta." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Zone koje objekt mora ući da bi snimka bila sačuvana." + }, + "height": { + "label": "Visina snimke", + "description": "Visina (pikseli) za promjenu veličine snimki iz API-ja; ostavite prazno da biste sačuvali originalnu veličinu." + }, + "retain": { + "label": "Zadržavanje snimki", + "description": "Postavke zadržavanja snimki uključujući zadane dane i prekriženja po objektu.", + "default": { + "label": "Zadano zadržavanje", + "description": "Zadani broj dana za zadržavanje snimki." + }, + "mode": { + "label": "Način zadržavanja", + "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)." + }, + "objects": { + "label": "Zadržavanje objekata", + "description": "Prekriženja po objektu za dane zadržavanja snimki." + } + }, + "quality": { + "label": "Kvaliteta snimka", + "description": "Kvaliteta kodiranja za sačuvane snimke (0-100)." + }, + "description": "Postavke za API generisane snimke praćenih objekata za sve kamere; može se prekrimiti po kamere." + }, + "timestamp_style": { + "label": "Stil vremenske oznake", + "position": { + "label": "Pozicija vremenske oznake", + "description": "Pozicija vremenske oznake na slici (tl/tr/bl/br)." + }, + "format": { + "label": "Format vremenske oznake", + "description": "String formata datuma i vremena korišten za vremenske oznake (Python format koda za datum i vrijeme)." + }, + "color": { + "label": "Boja vremenske oznake", + "description": "RGB vrijednosti boja za tekst vremenske oznake (sve vrijednosti 0-255).", + "red": { + "label": "Crvena", + "description": "Crveni komponent (0-255) za boju vremenske oznake." + }, + "green": { + "label": "Zelena", + "description": "Zeleni komponent (0-255) za boju vremenske oznake." + }, + "blue": { + "label": "Plava", + "description": "Plavi komponent (0-255) za boju vremenske oznake." + } + }, + "thickness": { + "label": "Debljina vremenske oznake", + "description": "Debljina linije teksta vremenske oznake." + }, + "effect": { + "label": "Efekt vremenske oznake", + "description": "Vizualni efekt za tekst vremenske oznake (none, solid, shadow)." + }, + "description": "Opcije stilizacije vremenskih oznaka u toku prikaza primjenjene na debug prikaz i snimke." + }, + "mqtt": { + "label": "MQTT", + "description": "Postavke za povezivanje i objavljivanje telemetrije, snimaka i detalja događaja na MQTT brokera.", + "enabled": { + "label": "Omogući MQTT", + "description": "Omogući ili onemogući integraciju MQTT za stanje, događaje i snimke." + }, + "host": { + "label": "Gospodar MQTT", + "description": "Ime domene ili IP adresa MQTT brokera." + }, + "port": { + "label": "Port MQTT", + "description": "Port MQTT brokera (obično 1883 za običan MQTT)." + }, + "topic_prefix": { + "label": "Predfiks teme", + "description": "Predložak teme MQTT za sve teme Frigate; mora biti jedinstven ako pokrećete više instanci." + }, + "client_id": { + "label": "ID klijenta", + "description": "Identifikator klijenta korišten pri povezivanju s MQTT brokerom; trebao bi biti jedinstven po instanci." + }, + "stats_interval": { + "label": "Interval statistika", + "description": "Interval u sekundama za objavljivanje sustavnih i kamera statistika na MQTT." + }, + "user": { + "label": "Korisničko ime MQTT", + "description": "Nepovlačno korisničko ime MQTT; može se pružiti putem varijabli okoline ili vjerodajnica." + }, + "password": { + "label": "Lozinka MQTT", + "description": "Nepovlačna lozinka MQTT; može se pružiti putem varijabli okoline ili vjerodajnica." + }, + "tls_ca_certs": { + "label": "TLS CA sertifikati", + "description": "Putanja do sertifikata CA za TLS povezivanje s brokerom (za samopotpisane sertifikate)." + }, + "tls_client_cert": { + "label": "Klijent sertifikat", + "description": "Putanja do sertifikata klijenta za TLS međusobnu autentifikaciju; ne postavljajte korisničko ime/lozinku kada koristite sertifikate klijenta." + }, + "tls_client_key": { + "label": "Klijent ključ", + "description": "Putanja do privatnog ključa za klijent sertifikat." + }, + "tls_insecure": { + "label": "TLS nebezbedan", + "description": "Dozvoli nebezbedne TLS povezivanja preskačući provjeru imena domene (nije preporučeno)." + }, + "qos": { + "label": "MQTT QoS", + "description": "Nivo kvaliteta usluge za MQTT objave/pretplate (0, 1 ili 2)." + } + }, + "notifications": { + "label": "Obavještenja", + "enabled": { + "label": "Omogući obavijesti", + "description": "Omogući ili onemogući obavijesti za sve kamere; mogu se prekrivati po kamere." + }, + "email": { + "label": "E-mail za obavijesti", + "description": "Adresa e-maila koja se koristi za obavijesti putem push-a ili je potrebna određenim dobavljačima obavijesti." + }, + "cooldown": { + "label": "Period hlađenja", + "description": "Period hlađenja (sekunde) između obavijesti kako bi se izbjeglo spaming primateljima." + }, + "enabled_in_config": { + "label": "Originalno stanje obavijesti", + "description": "Pokazuje je li obavijesti bile omogućene u originalnoj statičkoj konfiguraciji." + }, + "description": "Postavke za omogućavanje i kontrolu obavijesti za sve kamere; mogu se prekrivati po kamere." + }, + "onvif": { + "label": "ONVIF", + "description": "Postavke povezivanja preko ONVIF i automatskog praćenja PTZ za ovu kameru.", + "host": { + "label": "Gost ONVIF", + "description": "Gost (i opcionalni shema) za uslugu ONVIF za ovu kameru." + }, + "port": { + "label": "Port ONVIF", + "description": "Broj porta za uslugu ONVIF." + }, + "user": { + "label": "Korisničko ime za ONVIF", + "description": "Korisničko ime za autentifikaciju ONVIF; neki uređaji zahtijevaju korisnika admin za ONVIF." + }, + "password": { + "label": "Lozinka za ONVIF", + "description": "Lozinka za autentifikaciju ONVIF." + }, + "tls_insecure": { + "label": "Onemogući provjeru TLS", + "description": "Preskoči provjeru TLS i onemogući digest autentifikaciju za ONVIF (nebezbedno; koristiti samo u sigurnim mrežama)." + }, + "profile": { + "label": "ONVIF profil", + "description": "Specifičan ONVIF medij profil za korištenje za kontrolu PTZ, prilagođen tokenom ili imenom. Ako nije postavljen, prvi profil s važećom konfiguracijom PTZ automatski se odabire." + }, + "autotracking": { + "label": "Autotračenje", + "description": "Automatski praćenje pokretanja objekata i držanje ih u sredini okvira korištenjem pokreta kamere PTZ.", + "enabled": { + "label": "Omogući automatsko praćenje", + "description": "Omogući ili onemogući automatsko praćenje kamere PTZ detektiranih objekata." + }, + "calibrate_on_startup": { + "label": "Kalibriraj na početku", + "description": "Mjeri brzine motora PTZ pri pokretanju kako bi poboljšao preciznost praćenja. Frigate će ažurirati konfiguraciju s težinama pokreta nakon kalibracije." + }, + "zooming": { + "label": "Režim zumiranja", + "description": "Kontrola ponašanja zumiranja: onemogućeno (samo pan/tilt), apsolutno (najkompatibilnije) ili relativno (konkurentno pan/tilt/zum)." + }, + "zoom_factor": { + "label": "Faktor zumiranja", + "description": "Kontrola razine zumiranja na praćenim objektima. Niže vrijednosti drže više scene u pogledu; više vrijednosti zumiraju bliže, ali mogu izgubiti praćenje. Vrijednosti između 0.1 i 0.75." + }, + "track": { + "label": "Praćeni objekti", + "description": "Popis vrsta objekata koji trebaju pokrenuti automatsko praćenje." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Objekti moraju ući u jednu od ovih zona prije nego što započne automatsko praćenje." + }, + "return_preset": { + "label": "Povratak na predpostavku", + "description": "Ime predpostavke konfigurirano u firmware kamere za povratak nakon završetka praćenja." + }, + "timeout": { + "label": "Vrijeme čekanja povratka", + "description": "Čekajte ovaj broj sekundi nakon gubitka praćenja prije povratka kamere na predpostavljeno mjesto." + }, + "movement_weights": { + "label": "Težine pokreta", + "description": "Vrijednosti kalibracije automatski generirane kroz kalibraciju kamere. Ne mijenjajte ručno." + }, + "enabled_in_config": { + "label": "Originalni stanje autotračenja", + "description": "Unutarnje polje za praćenje je li autotračenje bilo omogućeno u konfiguraciji." + } + }, + "ignore_time_mismatch": { + "label": "Zanemari razliku u vremenu", + "description": "Zanemari razlike u sinhronizaciji vremena između kamere i Frigate servera za komunikaciju ONVIF." + } + }, + "profiles": { + "label": "Profili", + "description": "Imenovane definicije profila s prijateljivim imenima. Profili kamera moraju se referirati na imena definirana ovdje.", + "friendly_name": { + "label": "Prijateljsko ime", + "description": "Prikazano ime za ovaj profil prikazano u UI-u." + } + }, + "safe_mode": { + "label": "Sigurnosni režim", + "description": "Kada je omogućeno, pokrenite Frigate u sigurnosnom režimu s smanjenim funkcijama za uklanjanje problema." + }, + "environment_vars": { + "label": "Okolinski varijable", + "description": "Parovi ključ/vrijednost okolinskih varijabli za postavljanje za proces Frigate u Home Assistant OS. Korisnici koji nisu HAOS moraju koristiti konfiguraciju okolinskih varijabli Docker umjesto toga." + }, + "logger": { + "label": "Zapisi", + "description": "Kontrolira podrazumijevanu razinu detaljnosti zapisa i prekriženja razina detaljnosti po komponenti.", + "default": { + "label": "Razina zapisa", + "description": "Podrazumijevana globalna razina detaljnosti (debug, info, warning, error)." + }, + "logs": { + "label": "Razina zapisa po procesu", + "description": "Prekriženja razina detaljnosti po komponenti za povećanje ili smanjenje detaljnosti za određene module." + } + }, + "auth": { + "label": "Autentifikacija", + "description": "Postavke povezane s autentifikacijom i sesijama uključujući opcije kolačića i ograničenja brzine.", + "enabled": { + "label": "Omogući autentifikaciju", + "description": "Omogući nativnu autentifikaciju za korisnički sučelje Frigate." + }, + "reset_admin_password": { + "label": "Ponovno postavljanje lozinke administratora", + "description": "Ako je tačno, ponovno postavite lozinku korisnika administratora pri pokretanju i ispišite novu lozinku u zapisima." + }, + "cookie_name": { + "label": "Ime kolačića JWT", + "description": "Ime kolačića koji se koristi za pohranjivanje JWT tokena za nativnu autentifikaciju." + }, + "cookie_secure": { + "label": "Sigurnosni flag kolačića", + "description": "Postavite sigurnosni flag na kolačić autentifikacije; trebalo bi biti tačno kada se koristi TLS." + }, + "session_length": { + "label": "Trajanje sesije", + "description": "Trajanje sesije u sekundama za sesije temeljene na JWT." + }, + "refresh_time": { + "label": "Prozor osvežavanja sesije", + "description": "Kada se sesija nalazi unutar ovih sekundi do isteka, ponovo je ažurirati na punu dužinu." + }, + "failed_login_rate_limit": { + "label": "Ograničenja za neuspješne prijave", + "description": "Pravila ograničavanja brzine za neuspješne pokušaje prijave kako bi se smanjila napada silom." + }, + "trusted_proxies": { + "label": "Povereni proxy-ovi", + "description": "Lista IP adresa poverenih proxy-ova korištena prilikom određivanja IP adrese klijenta za ograničavanje brzine." + }, + "hash_iterations": { + "label": "Iteracije haširanja", + "description": "Broj iteracija PBKDF2-SHA256 koje se koriste za kriptiranje lozinki korisnika." + }, + "roles": { + "label": "Mapiranja uloga", + "description": "Pridružiti uloge listama kamera. Prazna lista omogućava pristup svim kamerama za ulogu." + }, + "admin_first_time_login": { + "label": "Zastavica za prvi put administrator", + "description": "Kada je istina, UI može prikazati poveznicu za pomoć na stranici prijave koja obavješćuje korisnike kako se prijaviti nakon ponovnog postavljanja lozinke administratora. " + } + }, + "database": { + "label": "Baza podataka", + "description": "Postavke SQLite baze podataka korištene od strane Frigate za pohranjivanje metapodataka praćenih objekata i metapodataka snimaka.", + "path": { + "label": "Putanja do baze podataka", + "description": "Putanja datotečnog sustava gdje će se datoteka SQLite baze podataka Frigate pohraniti." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "Postavke integrirane usluge go2rtc ponovnog prenošenja korištene za prenošenje živih streamova i prevodjenje." + }, + "networking": { + "label": "Mrežno", + "description": "Postavke povezane s mrežom, kao što je omogućavanje IPv6 za Frigate krajeve.", + "ipv6": { + "label": "Konfiguracija IPv6", + "description": "IPv6-specifične postavke za mrežne usluge Frigate.", + "enabled": { + "label": "Omogući IPv6", + "description": "Omogući podršku za IPv6 za usluge Frigate (API i UI) gdje je primjenjivo." + } + }, + "listen": { + "label": "Konfiguracija slušajućih porta", + "description": "Konfiguracija unutarnjih i vanjskih slušajućih porta. Ovo je za napredne korisnike. Za većinu slučajeva preporučuje se promijeniti sekciju porta u svojoj Docker compose datoteci.", + "internal": { + "label": "Unutarnji port", + "description": "Unutarnji slušajući port za Frigate (zadano 5000)." + }, + "external": { + "label": "Vanjski port", + "description": "Vanjski slušajući port za Frigate (zadano 8971)." + } + } + }, + "proxy": { + "label": "Proxy", + "description": "Postavke za integraciju Frigate iza obrnute proxy posrednike koji prenose zaglavlja autentificiranih korisnika.", + "header_map": { + "label": "Mapiranje zaglavlja", + "description": "Mapiraj dolazna zaglavlja proxy-a na polja korisnika i uloge Frigate za autentifikaciju baziranu na proxy-u.", + "user": { + "label": "Zaglavlje korisnika", + "description": "Zaglavlje koje sadrži autentificirano korisničko ime pruženo od strane nadolazećeg proxy-a." + }, + "role": { + "label": "Zaglavlje uloge", + "description": "Zaglavlje koje sadrži ulogu ili grupe autentificiranog korisnika od strane nadolazećeg proxy-a." + }, + "role_map": { + "label": "Mapiranje uloga", + "description": "Mapiraj vrijednosti grupe iznad na uloge Frigate (npr. mapiraj grupe administratora na ulogu administratora)." + } + }, + "logout_url": { + "label": "URL za odjavu", + "description": "URL na koji će korisnici biti preusmjereni kada se odjave putem proxy-a." + }, + "auth_secret": { + "label": "Tajna proxy", + "description": "Nepovlačena tajna provjeravana protiv zaglavlja X-Proxy-Secret za potvrdu pouzdanih proxy-a." + }, + "default_role": { + "label": "Zadana uloga", + "description": "Zadana uloga dodijeljena korisnicima autentificiranim putem proxy-a kada neka mapiranja uloga ne vrijede (administrator ili pregledač)." + }, + "separator": { + "label": "Znak separatora", + "description": "Karakter koristen za razdvajanje više vrijednosti navedenih u zaglavju proksi." + } + }, + "telemetry": { + "label": "Telemetrija", + "description": "Opcije sistem telemetrije i statistika uključujući praćenje širine pojasa mreže i GPU.", + "network_interfaces": { + "label": "Mrežni sučelja", + "description": "Popis prefiksa imena mrežnih sučelja za praćenje statistika širine pojasa." + }, + "stats": { + "label": "Sistem statistika", + "description": "Opcije za omogućavanje/onemogućavanje prikupljanja različitih sistem i GPU statistika.", + "amd_gpu_stats": { + "label": "AMD GPU statistika", + "description": "Omogući prikupljanje AMD GPU statistika ako je prisutan AMD GPU." + }, + "intel_gpu_stats": { + "label": "Intel GPU statistika", + "description": "Omogući prikupljanje Intel GPU statistika ako je prisutan Intel GPU." + }, + "network_bandwidth": { + "label": "Širina pojasa mreže", + "description": "Omogući praćenje širine pojasa mreže po procesu za procese kamere ffmpeg i detektore (zahtijeva mogućnosti)." + }, + "intel_gpu_device": { + "label": "Intel GPU uređaj", + "description": "PCI adresa magistrale ili DRM putanja uređaja (npr. /dev/dri/card1) koja se koristi za vezivanje Intel GPU statistika za određeni uređaj kada je prisutno više njih." + } + }, + "version_check": { + "label": "Provjera verzije", + "description": "Omogući ishodnu provjeru za otkrivanje ako je dostupnija verzija Frigate." + } + }, + "tls": { + "label": "TLS", + "description": "Postavke TLS za web krajnje točke Frigate (port 8971).", + "enabled": { + "label": "Omogući TLS", + "description": "Omogući TLS za web UI i API Frigate na konfiguriranom TLS portu." + } + }, + "ui": { + "label": "UI", + "description": "Postavke korisničkog sučelja poput vremenske zone, oblikovanja vremena/datuma i jedinica.", + "timezone": { + "label": "Vremenska zona", + "description": "Nepovlačena vremenska zona za prikaz kroz UI (podrazumijevano je lokalno vrijeme preglednika ako nije postavljeno)." + }, + "time_format": { + "label": "Oblik vremena", + "description": "Oblik vremena za korištenje u UI (browser, 12hour, ili 24hour)." + }, + "date_style": { + "label": "Oblik datuma", + "description": "Oblik datuma za korištenje u UI (full, long, medium, short)." + }, + "time_style": { + "label": "Oblik vremena", + "description": "Oblik vremena za korištenje u UI (full, long, medium, short)." + }, + "unit_system": { + "label": "Sustav jedinica", + "description": "Sustav jedinica za prikaz (metric ili imperial) korišten u UI i MQTT." + } + }, + "detectors": { + "label": "Hardver detektora", + "description": "Konfiguracija za detektore objekata (CPU, GPU, ONNX backends) i bilo koje postavke modela specifične za detektor.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Konfiguracija modela specifične za detektor", + "description": "Opcije konfiguracije modela specifične za detektor (putanja, veličina ulaza, itd.).", + "path": { + "label": "Putanja za prilagođeni model detektora objekata", + "description": "Putanja do datoteke prilagođenog modela detekcije (ili plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Mapa oznaka za prilagođeni detektor objekata", + "description": "Putanja do datoteke mape oznaka koja mapira numeričke klase na string oznake za detektor." + }, + "width": { + "label": "Širina ulaznog tenzora modela detekcije objekata", + "description": "Širina ulaznog tenzora modela u pikselima." + }, + "height": { + "label": "Visina ulaznog tenzora modela detekcije objekata", + "description": "Visina ulaznog tenzora modela u pikselima." + }, + "labelmap": { + "label": "Prilagodba mape oznaka", + "description": "Preklop ili preslikavanje unosa za uključivanje u standardnu mapu oznaka." + }, + "attributes_map": { + "label": "Mapa oznaka objekata na njihove atribute", + "description": "Preslikavanje iz oznaka objekata na atribute oznaka koje se koriste za dodavanje metapodataka (npr. 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Oblik tenzora ulaza modela", + "description": "Format tenzora očekivan od strane modela: 'nhwc' ili 'nchw'." + }, + "input_pixel_format": { + "label": "Format boje piksela ulaza modela", + "description": "Boja piksela očekivana od strane modela: 'rgb', 'bgr' ili 'yuv'." + }, + "input_dtype": { + "label": "Tip D ulaza modela", + "description": "Tip podataka modela ulaznog tenzora (npr. 'float32')." + }, + "model_type": { + "label": "Tip modela detekcije objekata", + "description": "Tip arhitekture modela detektora (ssd, yolox, yolonas) korišten od strane nekih detektora za optimizaciju." + } + }, + "model_path": { + "label": "Putanja modela specifična za detektor", + "description": "Putanja datoteke do binarne datoteke modela detektora ako je potrebna odabranim detektorom." + }, + "axengine": { + "label": "AXEngine NPU", + "description": "Detektor AXERA AX650N/AX8850N NPU koji pokreće prevedene .axmodel datoteke putem AXEngine runtime-a." + }, + "cpu": { + "label": "CPU", + "description": "Detektor CPU TFLite koji pokreće modele TensorFlow Lite na domaćem CPU bez hardverske akceleracije. Nije preporučeno.", + "num_threads": { + "label": "Broj nitova detekcije", + "description": "Broj nitova korištenih za inferenciju na CPU." + } + }, + "deepstack": { + "label": "DeepStack", + "description": "Detektor DeepStack/CodeProject.AI koji šalje slike na udaljenu DeepStack HTTP API za inferenciju. Nije preporučeno.", + "api_url": { + "label": "URL API-ja DeepStack", + "description": "URL API-ja DeepStack." + }, + "api_timeout": { + "label": "Vrijeme čekanja API-ja DeepStack (u sekundama)", + "description": "Maksimalno dozvoljeno vrijeme za zahtjev API-ja DeepStack." + }, + "api_key": { + "label": "Ključ API-ja DeepStack (ako je potreban)", + "description": "Nepovlađeni ključ API-ja za autentificirane usluge DeepStack." + } + }, + "degirum": { + "label": "DeGirum", + "description": "Detektor DeGirum za pokretanje modela putem DeGirum oblaka ili lokalnih usluga inferencije.", + "location": { + "label": "Lokacija inferencije", + "description": "Lokacija DeGirim inferencije (npr. '@cloud', '127.0.0.1')." + }, + "zoo": { + "label": "Model Zoo", + "description": "Putanja ili URL do DeGirum model zoo." + }, + "token": { + "label": "Token za DeGirum Cloud", + "description": "Token za pristup DeGirum Cloud." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "Detektor EdgeTPU koji pokreće modele TensorFlow Lite kompilirane za Coral EdgeTPU pomoću EdgeTPU delegata.", + "device": { + "label": "Tip uređaja", + "description": "Uređaj za korištenje EdgeTPU inferencije (npr. 'usb', 'pci')." + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "Detektor Hailo-8/Hailo-8L koji koristi HEF modele i HailoRT SDK za inferenciju na Hailo uređaju.", + "device": { + "label": "Tip uređaja", + "description": "Uređaj za korištenje Hailo inferencije (npr. 'PCIe', 'M.2')." + } + }, + "memryx": { + "label": "MemryX", + "description": "Detektor MemryX MX3 koji pokreće kompilirane modele DFP na MemryX akceleratorima.", + "device": { + "label": "Putanja uređaja", + "description": "Uređaj za korištenje MemryX inferencije (npr. 'PCIe')." + } + }, + "onnx": { + "label": "ONNX", + "description": "Detektor ONNX za pokretanje ONNX modela; koristi dostupne akceleracijske backendove (CUDA/ROCm/OpenVINO) kada su dostupni.", + "device": { + "label": "Tip uređaja", + "description": "Uređaj za korištenje ONNX inferencije (npr. 'AUTO', 'CPU', 'GPU')." + } + }, + "openvino": { + "label": "OpenVINO", + "description": "Detektor OpenVINO za AMD i Intel CPU-e, Intel GPU-e i Intel VPU uređaje.", + "device": { + "label": "Tip uređaja", + "description": "Uređaj za korištenje za inferenciju OpenVINO (npr. 'CPU', 'GPU', 'NPU')." + } + }, + "rknn": { + "label": "RKNN", + "description": "Detektor RKNN za NPUs Rockchipa; izvršava preveđene modele RKNN na Rockchip uređaju.", + "num_cores": { + "label": "Broj jezgri NPU koje se koriste.", + "description": "Broj jezgri NPU koje se koriste (0 za automatsko)." + } + }, + "synaptics": { + "label": "Synaptics", + "description": "Detektor NPU Synaptics za modele u formatu .synap pomoću SDK-a Synap na uređaju Synaptics." + }, + "teflon_tfl": { + "label": "Teflon", + "description": "Detektor delegata Teflon za TFLite pomoću biblioteke Mesa Teflon delegata za ubrzanje inferencije na podržanim GPU-ima." + }, + "tensorrt": { + "label": "TensorRT", + "description": "Detektor TensorRT za uređaje Nvidia Jetson koji koristi serijalizirane TensorRT motore za ubrzanu inferenciju.", + "device": { + "label": "Indeks GPU uređaja", + "description": "Indeks GPU uređaja za korištenje." + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "Detektor ZMQ IPC koji prenosi inferenciju vanjskom procesu putem ZMQ IPC kraja.", + "endpoint": { + "label": "ZMQ IPC kraja", + "description": "Kraj ZMQ-a na koji se povezati." + }, + "request_timeout_ms": { + "label": "ZMQ zahtjev timeout u milisekundama", + "description": "Timeout za ZMQ zahtjeve u milisekundama." + }, + "linger_ms": { + "label": "ZMQ socket linger u milisekundama", + "description": "Period linger socketa u milisekundama." + } + } + }, + "model": { + "label": "Model detekcije", + "description": "Postavke za konfiguraciju prilagođenog modela detekcije objekata i njegove ulazne oblike.", + "path": { + "label": "Put do prilagođenog modela detekcije", + "description": "Put do datoteke prilagođenog modela detekcije (ili plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Mapa oznaka za prilagođeni detektor objekata", + "description": "Putanja do datoteke labelmap koja preslikava numeričke klase u string oznake za detektor." + }, + "width": { + "label": "Širina ulaznog tenzora modela detekcije objekata", + "description": "Širina ulaznog tenzora modela u pikselima." + }, + "height": { + "label": "Visina ulaznog tenzora modela detekcije objekata", + "description": "Visina ulaznog tenzora modela u pikselima." + }, + "labelmap": { + "label": "Prilagodba labelmap", + "description": "Preklop ili preslikavanje unosa za spajanje u standardnu labelmap." + }, + "attributes_map": { + "label": "Mapa oznaka objekata na njihove atribute oznake", + "description": "Preslikavanje iz oznaka objekata na atribute oznake koje se koriste za dodavanje metapodataka (npr. 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Oblik ulaznog tenzora modela", + "description": "Format tenzora očekivan od strane modela: 'nhwc' ili 'nchw'." + }, + "input_pixel_format": { + "label": "Format boje piksela ulaznog modela", + "description": "Boja prostor očekivan od strane modela: 'rgb', 'bgr' ili 'yuv'." + }, + "input_dtype": { + "label": "D tip ulaza modela", + "description": "Tip podataka ulaznog tenzora modela (npr. 'float32')." + }, + "model_type": { + "label": "Tip modela detekcije objekata", + "description": "Tip arhitekture modela detektora (ssd, yolox, yolonas) korišten od strane nekih detektora za optimizaciju." + } + }, + "genai": { + "label": "Konfiguracija generativne AI", + "description": "Postavke za integrirane generativne AI provajdere korišteni za generisanje opisa objekata i pregled sažetaka.", + "api_key": { + "label": "Ključ API", + "description": "Ključ API potreban nekim provajderima (takođe može biti postavljen putem okruženja)." + }, + "base_url": { + "label": "Osnovna URL", + "description": "Osnovna URL za samohostovane ili kompatibilne provajdere (npr. instanca Ollama)." + }, + "model": { + "label": "Model", + "description": "Model koji se koristi iz ponuđača za generisanje opisa ili sažetaka." + }, + "provider": { + "label": "Ponuđač", + "description": "GenAI ponuđač koji se koristi (npr. ollama, gemini, openai)." + }, + "roles": { + "label": "Uloge", + "description": "GenAI uloge (razgovor, opisi, ugradnje); jedan ponuđač po ulozi." + }, + "provider_options": { + "label": "Opcije ponuđača", + "description": "Dodatne opcije specifične za ponuđača koje se prosleđuju klijentu GenAI." + }, + "runtime_options": { + "label": "Opcije izvršavanja", + "description": "Opcije izvršavanja koje se prosleđuju ponuđaču za svaku poziv izvođenja." + } + }, + "classification": { + "label": "Klasifikacija objekata", + "description": "Postavke modela klasifikacije koji se koriste za poboljšanje oznaka objekata ili klasifikaciju stanja.", + "bird": { + "label": "Konfiguracija klasifikacije ptica", + "description": "Postavke specifične za modele klasifikacije ptica.", + "enabled": { + "label": "Klasifikacija ptica", + "description": "Omogući ili onemogući klasifikaciju ptica." + }, + "threshold": { + "label": "Minimalna ocjena", + "description": "Minimalna ocjena klasifikacije potrebna za prihvaćanje klasifikacije ptica." + } + }, + "custom": { + "label": "Prilagođeni modeli klasifikacije", + "description": "Konfiguracija prilagođenih modela klasifikacije korištenih za objekte ili detekciju stanja.", + "enabled": { + "label": "Omogući model", + "description": "Omogući ili onemogući prilagođeni model klasifikacije." + }, + "name": { + "label": "Ime modela", + "description": "Identifikator za prilagođeni model klasifikacije koji se koristi." + }, + "threshold": { + "label": "Prag ocjene", + "description": "Prag ocjene korišten za promjenu stanja klasifikacije." + }, + "save_attempts": { + "label": "Snimi pokušaje", + "description": "Koliko pokušaja klasifikacije sačuvati za korisnički sučelje nedavnih klasifikacija." + }, + "object_config": { + "objects": { + "label": "Klasificiraj objekte", + "description": "Popis vrsta objekata za koje se izvršava klasifikacija objekata." + }, + "classification_type": { + "label": "Vrsta klasifikacije", + "description": "Vrsta klasifikacije primijenjena: 'sub_label' (dodaje sub_label) ili druge podržane vrste." + } + }, + "state_config": { + "cameras": { + "label": "Kamere za klasifikaciju", + "description": "Postavke za rezanje i podešavanja po kameri za izvršavanje klasifikacije stanja.", + "crop": { + "label": "Rezanje za klasifikaciju", + "description": "Koordinate rezanja koje se koriste za izvršavanje klasifikacije na ovoj kameri." + } + }, + "motion": { + "label": "Pokreni na pokret", + "description": "Ako je tačno, pokrenite klasifikaciju kada se detektuje pokret unutar navedenog krova." + }, + "interval": { + "label": "Interval klasifikacije", + "description": "Interval (sekunde) između periodičnih pokretanja klasifikacije za klasifikaciju stanja." + } + } + } + }, + "camera_groups": { + "label": "Grupe kamera", + "description": "Konfiguracija imenovanih grupa kamera koje se koriste za organizaciju kamera u UI-u.", + "cameras": { + "label": "Popis kamera", + "description": "Niz imena kamera uključenih u ovu grupu." + }, + "icon": { + "label": "Ikona grupe", + "description": "Ikona korištena za prikaz grupe kamera u UI-u." + }, + "order": { + "label": "Redoslijed sortiranja", + "description": "Numerički redoslijed korišten za sortiranje grupa kamera u UI-u; veći brojevi se pojavljuju kasnije." + } + }, + "active_profile": { + "label": "Aktivni profil", + "description": "Trenutno aktivno ime profila. Samo za runtime, ne čuva se u YAML-u." + }, + "camera_mqtt": { + "label": "MQTT", + "description": "Postavke objave slika preko MQTT.", + "enabled": { + "label": "Pošalji sliku", + "description": "Omogući objavljivanje snimaka slika za objekte na MQTT teme za ovu kameru." + }, + "timestamp": { + "label": "Dodaj vremensku oznaku", + "description": "Preklopiti vremensku oznaku na slike objavljene preko MQTT." + }, + "bounding_box": { + "label": "Dodaj okvir", + "description": "Crtaj okvire na slikama objavljenim preko MQTT." + }, + "crop": { + "label": "Iscijepi sliku", + "description": "Iscijepi slike objavljene preko MQTT na okvir detektiranog objekta." + }, + "height": { + "label": "Visina slike", + "description": "Visina (piksela) za promjenu veličine slika objavljenih preko MQTT." + }, + "required_zones": { + "label": "Potrebne zone", + "description": "Zone koje objekt mora ući da bi se slika preko MQTT objavila." + }, + "quality": { + "label": "Kvaliteta JPEG", + "description": "Kvaliteta JPEG za slike objavljene preko MQTT (0-100)." + } + }, + "camera_ui": { + "label": "Kamera UI", + "description": "Prikaz redoslijeda i vidljivosti za ovu kameru u UI. Redoslijed utječe na zadani nadzorno pločo. Za detaljniju kontrolu koristite grupe kamere.", + "order": { + "label": "Redoslijed UI", + "description": "Numerički redoslijed koristi se za sortiranje kamere u UI (zadani nadzorno pločo i popisi); veći brojevi pojavljuju se kasnije." + }, + "dashboard": { + "label": "Prikaži u UI", + "description": "Prekidač je li ova kamera vidljiva svuda u UI Frigate. Onemogućavanje ovoga zahtijeva ručno uređivanje konfiguracije za ponovno prikazivanje ove kamere u UI." + } + } +} diff --git a/web/public/locales/bs/config/groups.json b/web/public/locales/bs/config/groups.json new file mode 100644 index 0000000..44f91e7 --- /dev/null +++ b/web/public/locales/bs/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Globalna detekcija", + "sensitivity": "Globalna osjetljivost" + }, + "cameras": { + "detection": "Detekcija", + "sensitivity": "Osjetljivost" + } + }, + "timestamp_style": { + "global": { + "appearance": "Globalno izgled" + }, + "cameras": { + "appearance": "Izgled" + } + }, + "motion": { + "global": { + "sensitivity": "Globalna osjetljivost", + "algorithm": "Globalni algoritam" + }, + "cameras": { + "sensitivity": "Osjetljivost", + "algorithm": "Algoritam" + } + }, + "snapshots": { + "global": { + "display": "Globalno prikazivanje" + }, + "cameras": { + "display": "Prikazivanje" + } + }, + "detect": { + "global": { + "resolution": "Globalna rezolucija", + "tracking": "Globalno praćenje" + }, + "cameras": { + "resolution": "Rezolucija", + "tracking": "Praćenje" + } + }, + "objects": { + "global": { + "tracking": "Globalno praćenje", + "filtering": "Globalno filtriranje" + }, + "cameras": { + "tracking": "Praćenje", + "filtering": "Filtriranje" + } + }, + "record": { + "global": { + "retention": "Globalno zadržavanje", + "events": "Globalni događaji" + }, + "cameras": { + "retention": "Zadržavanje", + "events": "Događaji" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Argumeni FFmpeg specifični za kameru" + } + } +} diff --git a/web/public/locales/bs/config/validation.json b/web/public/locales/bs/config/validation.json new file mode 100644 index 0000000..1ed1dd7 --- /dev/null +++ b/web/public/locales/bs/config/validation.json @@ -0,0 +1,32 @@ +{ + "minimum": "Bar {{limit}}", + "maximum": "Mora biti najviše {{limit}}", + "exclusiveMinimum": "Mora biti veći od {{limit}}", + "exclusiveMaximum": "Mora biti manje od {{limit}}", + "minLength": "Bar {{limit}} znak(ovi)", + "maxLength": "Mora biti najviše {{limit}} znak(ovi)", + "minItems": "Mora imati bar {{limit}} stavke", + "maxItems": "Mora imati najviše {{limit}} stavke", + "pattern": "Neispravan format", + "required": "Ovo polje je obavezno", + "type": "Neispravan tip vrijednosti", + "enum": "Mora biti jedan od dopuštenih vrijednosti", + "const": "Vrijednost se ne podudara s očekivanom konstantom", + "uniqueItems": "Sve stavke moraju biti jedinstvene", + "format": "Neispravan format", + "additionalProperties": "Nepoznato svojstvo nije dozvoljeno", + "oneOf": "Mora se podudarati s točno jednim od dopuštenih shema", + "anyOf": "Mora se podudarati s bar jednim od dopuštenih shema", + "proxy": { + "header_map": { + "roleHeaderRequired": "Zaglavlje uloge je obavezno kada su konfigurirane mapiranja uloga." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "Svaka uloga može biti dodijeljena samo jednom ulaznom toku.", + "detectRequired": "Bar jedan ulazni tok mora biti dodijeljen ulozi 'detektirati'.", + "hwaccelDetectOnly": "Samo ulazni tok s ulogom detektiranja može definirati argumente ubrzavanja hardvera." + } + } +} diff --git a/web/public/locales/bs/objects.json b/web/public/locales/bs/objects.json new file mode 100644 index 0000000..cbeaacd --- /dev/null +++ b/web/public/locales/bs/objects.json @@ -0,0 +1,125 @@ +{ + "person": "Ljudsko bit će", + "bicycle": "Kolo", + "animal": "Životinja", + "dog": "Pas", + "bark": "Glavu", + "cat": "Mačka", + "horse": "Konj", + "goat": "Koza", + "sheep": "Ovca", + "bird": "Ptica", + "mouse": "Miš", + "keyboard": "Klaviatura", + "vehicle": "Vozilo", + "boat": "Brod", + "car": "Automobil", + "bus": "Autobus", + "motorcycle": "Motocikl", + "train": "Vlak", + "skateboard": "Skejtbord", + "door": "Vrata", + "blender": "Miksere", + "sink": "Lavabo", + "hair_dryer": "Sušilac za kosu", + "toothbrush": "Šetka za zube", + "scissors": "Škare", + "clock": "Sat", + "airplane": "Avion", + "traffic_light": "Svetofor", + "fire_hydrant": "Vatrostaničar", + "street_sign": "Ulični znak", + "stop_sign": "Znak zaustavljanja", + "parking_meter": "Parkirni metar", + "bench": "Banko", + "cow": "Korova", + "elephant": "Slon", + "bear": "Medvjed", + "zebra": "Zebra", + "giraffe": "Žirafa", + "hat": "Kaputa", + "backpack": "Torba", + "umbrella": "Kreveta", + "shoe": "Cizma", + "eye_glasses": "Očna stakla", + "handbag": "Ručna torba", + "tie": "Kremplj", + "suitcase": "Kufer", + "frisbee": "Frizbi", + "skis": "Ski", + "snowboard": "Snjegobord", + "sports_ball": "Sportska lopta", + "kite": "Let", + "baseball_bat": "Batsa za baseball", + "baseball_glove": "Rukavica za baseball", + "surfboard": "Surfbord", + "tennis_racket": "Teniski raketa", + "bottle": "Bocica", + "plate": "Ploča", + "wine_glass": "Vinsko čaša", + "cup": "Kupa", + "fork": "Škarpe", + "knife": "Nož", + "spoon": "Lajna", + "bowl": "Tanjir", + "banana": "Banana", + "apple": "Jabuka", + "sandwich": "Sandučić", + "orange": "Portakal", + "broccoli": "Brobkoli", + "carrot": "Mahunika", + "hot_dog": "Hot dog", + "pizza": "Pica", + "donut": "Krofna", + "cake": "Torta", + "chair": "Stolica", + "couch": "Divan", + "potted_plant": "Ukrasna biljka", + "bed": "Krevet", + "mirror": "Zrcalo", + "dining_table": "Stol za ručak", + "window": "Prozor", + "desk": "Radni stol", + "toilet": "Toalet", + "tv": "TV", + "laptop": "Laptop", + "remote": "Udaljeno upravljanje", + "cell_phone": "Mobilni telefon", + "microwave": "Mikrotalasna pećnica", + "oven": "Pećnica", + "toaster": "Tostera", + "refrigerator": "Hladnjak", + "book": "Knjiga", + "vase": "Vaza", + "teddy_bear": "Biberon", + "hair_brush": "Kosmetička četka", + "squirrel": "Šumski pas", + "deer": "Jelen", + "fox": "Lisica", + "rabbit": "Zajac", + "raccoon": "Rakun", + "robot_lawnmower": "Robotska kosilica", + "waste_bin": "Kanta za otpad", + "on_demand": "Na zahtjev", + "face": "Lice", + "license_plate": "Tablica", + "package": "Paket", + "bbq_grill": "Grill za BBQ", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD", + "canada_post": "Canada Post", + "royal_mail": "Royal Mail", + "school_bus": "Školski autobus", + "skunk": "Mrdka", + "kangaroo": "Kanguru" +} diff --git a/web/public/locales/bs/views/chat.json b/web/public/locales/bs/views/chat.json new file mode 100644 index 0000000..643825a --- /dev/null +++ b/web/public/locales/bs/views/chat.json @@ -0,0 +1,46 @@ +{ + "documentTitle": "Razgovor - Frigate", + "title": "Frigate razgovor", + "subtitle": "Vaš AI asistent za upravljanje kamerama i insighti", + "placeholder": "Pitajte bilo što...", + "error": "Nešto je pošlo po zlu. Molimo pokušajte ponovo.", + "processing": "Obrađivanje...", + "toolsUsed": "Korišteno: {{tools}}", + "showTools": "Prikaži alate ({{count}})", + "hideTools": "Sakrij alate", + "call": "Poziv", + "result": "Rezultat", + "arguments": "Argumenti:", + "response": "Odgovor:", + "attachment_chip_label": "{{label}} na {{camera}}", + "attachment_chip_remove": "Ukloni privitak", + "open_in_explore": "Otvori u Explore", + "attach_event_aria": "Prikači događaj {{eventId}}", + "attachment_picker_paste_label": "Ili zalijepite ID događaja", + "attachment_picker_attach": "Prikači", + "attachment_picker_placeholder": "Prikači događaj", + "quick_reply_find_similar": "Pronađi slične susreti", + "quick_reply_tell_me_more": "Recite mi više o ovome", + "quick_reply_when_else": "Kada je još puta vidjeno?", + "quick_reply_find_similar_text": "Pronađi slične susreti za ovaj.", + "quick_reply_tell_me_more_text": "Recite mi više o ovom.", + "quick_reply_when_else_text": "Kada je to još puta vidjeno?", + "anchor": "Referenca", + "similarity_score": "Sličnost", + "no_similar_objects_found": "Nisu pronađeni slični objekti.", + "semantic_search_required": "Semantička pretraga mora biti omogućena da bi se pronašli slični objekti.", + "send": "Pošalji", + "suggested_requests": "Pokušaj pitati:", + "starting_requests": { + "show_recent_events": "Prikaži nedavne događaje", + "show_camera_status": "Prikaži status kamere", + "recap": "Što se desilo dok sam bio odsutan?", + "watch_camera": "Pratite kameru za aktivnost" + }, + "starting_requests_prompts": { + "show_recent_events": "Prikaži mi nedavne događaje iz posljednjeg sata", + "show_camera_status": "Koji je trenutni status mojih kamera?", + "recap": "Što se desilo dok sam bio odsutan?", + "watch_camera": "Pratite ulazna vrata i obavijestite me ako netko dođe" + } +} diff --git a/web/public/locales/bs/views/classificationModel.json b/web/public/locales/bs/views/classificationModel.json new file mode 100644 index 0000000..aa18d5a --- /dev/null +++ b/web/public/locales/bs/views/classificationModel.json @@ -0,0 +1,205 @@ +{ + "documentTitle": "Modeli klasifikacije - Frigate", + "details": { + "scoreInfo": "Ocjena predstavlja prosjek pouzdanosti klasifikacije kroz sve detekcije ovog objekta.", + "none": "Nijedan", + "unknown": "Nepoznato" + }, + "button": { + "deleteClassificationAttempts": "Obriši slike klasifikacije", + "renameCategory": "Preimenuj klasu", + "deleteCategory": "Obriši klasu", + "deleteImages": "Obriši slike", + "trainModel": "Obuci model", + "addClassification": "Dodaj klasifikaciju", + "deleteModels": "Obriši modele", + "editModel": "Uredi model" + }, + "tooltip": { + "trainingInProgress": "Model trenutno obučava", + "noNewImages": "Nema novih slika za obuku. Prvo klasificirajte više slika u skupu podataka.", + "noChanges": "Nema promjena u skupu podataka od posljednje obuke.", + "modelNotReady": "Model nije spreman za obuku" + }, + "toast": { + "success": { + "deletedModel_one": "Uspješno obrisan {{count}} model", + "deletedModel_few": "Uspješno obrisani {{count}} modeli", + "deletedModel_other": "Uspješno obrisani {{count}} modeli", + "categorizedImage": "Uspješno klasificirana slika", + "reclassifiedImage": "Uspješno ponovno klasificirana slika", + "trainedModel": "Uspješno obučen model.", + "trainingModel": "Uspješno pokrenuta obuka modela.", + "updatedModel": "Uspješno ažurirana konfiguracija modela", + "renamedCategory": "Uspješno preimenovana klasa u {{name}}", + "deletedCategory_one": "Obrisana {{count}} klasa", + "deletedCategory_few": "Obrisane {{count}} klase", + "deletedCategory_other": "Obrisane {{count}} klase", + "deletedImage_one": "Izbrisana {{count}} slika", + "deletedImage_few": "Izbrisane {{count}} slike", + "deletedImage_other": "Izbrisane {{count}} slike" + }, + "error": { + "deleteImageFailed": "Neuspješno brisanje: {{errorMessage}}", + "deleteCategoryFailed": "Neuspješno brisanje klase: {{errorMessage}}", + "deleteModelFailed": "Neuspješno brisanje modela: {{errorMessage}}", + "categorizeFailed": "Neuspješno kategoriziranje slike: {{errorMessage}}", + "trainingFailed": "Obuka modela nije uspješna. Provjerite zapise Frigate za detalje.", + "trainingFailedToStart": "Neuspješno pokretanje obuke modela: {{errorMessage}}", + "updateModelFailed": "Neuspješno ažuriranje modela: {{errorMessage}}", + "renameCategoryFailed": "Neuspješno preimenovanje klase: {{errorMessage}}", + "reclassifyFailed": "Neuspješno ponovno klasifikovanje slike: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Izbriši klasu", + "desc": "Sigurni li ste da želite izbrisati klasu {{name}}? Ovo će trajno izbrisati sve povezane slike i zahtijevati ponovnu obuku modela.", + "minClassesTitle": "Nemoguće izbrisati klasu", + "minClassesDesc": "Model klasifikacije mora imati najmanje 2 klase. Dodajte još jednu klasu prije brisanja ove." + }, + "deleteModel": { + "title": "Izbriši model klasifikacije", + "single": "Sigurni li ste da želite izbrisati {{name}}? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.", + "desc_one": "Sigurni li ste da želite izbrisati {{count}} model? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.", + "desc_few": "Sigurni li ste da želite izbrisati {{count}} modele? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.", + "desc_other": "Sigurni li ste da želite izbrisati {{count}} modele? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena." + }, + "edit": { + "title": "Uredi model klasifikacije", + "descriptionState": "Uredi klase za ovaj model klasifikacije stanja. Promjene će zahtijevati ponovnu obuku modela.", + "descriptionObject": "Uredi vrstu objekta i vrstu klasifikacije za ovaj model klasifikacije objekta.", + "stateClassesInfo": "Napomena: Promjene klasa stanja zahtijevaju ponovnu obuku modela sa ažuriranim klasama." + }, + "deleteDatasetImages": { + "title": "Izbriši slike skupa podataka", + "desc_one": "Sigurni li ste da želite izbrisati {{count}} sliku iz {{dataset}}? Ova akcija ne može biti poništena i zahtijevati će ponovnu obuku modela.", + "desc_few": "Sigurni li ste da želite obrisati {{count}} slike iz {{dataset}}? Ova akcija ne može se poništiti i zahtijevat će ponovno treniranje modela.", + "desc_other": "Sigurni li ste da želite obrisati {{count}} slike iz {{dataset}}? Ova akcija ne može se poništiti i zahtijevat će ponovno treniranje modela." + }, + "deleteTrainImages": { + "title": "Obriši slike za treniranje", + "desc_one": "Sigurni li ste da želite obrisati {{count}} sliku? Ova akcija ne može se poništiti.", + "desc_few": "Sigurni li ste da želite obrisati {{count}} slike? Ova akcija ne može se poništiti.", + "desc_other": "Sigurni li ste da želite obrisati {{count}} slike? Ova akcija ne može se poništiti." + }, + "renameCategory": { + "title": "Preimenuj klasu", + "desc": "Unesite novo ime za {{name}}. Za promjenu imena će vam se zahtijevati ponovno treniranje modela." + }, + "description": { + "invalidName": "Neprihvatljivo ime. Imena mogu sadržavati samo slova, brojeve, razmake, aposrofe, donje crte i crte." + }, + "train": { + "title": "Nedavne klasifikacije", + "titleShort": "Nedavno", + "aria": "Odaberite nedavne klasifikacije" + }, + "categories": "Klase", + "createCategory": { + "new": "Stvori novu klasu" + }, + "categorizeImageAs": "Klasificiraj sliku kao:", + "categorizeImage": "Klasificiraj sliku", + "reclassifyImageAs": "Ponovno klasificiraj sliku kao:", + "reclassifyImage": "Ponovno klasificiraj sliku", + "menu": { + "objects": "Objekti", + "states": "Stanja" + }, + "noModels": { + "object": { + "title": "Nema modela za klasifikaciju objekata", + "description": "Stvorite prilagođeni model za klasifikaciju detektiranih objekata.", + "buttonText": "Stvori model objekta" + }, + "state": { + "title": "Nema modela za klasifikaciju stanja", + "description": "Stvorite prilagođeni model za praćenje i klasifikaciju promjena stanja u određenim područjima kamere.", + "buttonText": "Stvori model stanja" + } + }, + "wizard": { + "title": "Stvori novu klasifikaciju", + "steps": { + "nameAndDefine": "Ime i definicija", + "stateArea": "Područje stanja", + "chooseExamples": "Odaberite primjere" + }, + "step1": { + "description": "Modeli stanja nadziraju fiksne područja kamere za promjene (npr. vrata otvorena/zatvorena). Modeli objekata dodaju klasifikacije detektiranim objektima (npr. poznati životinje, dostavljači, itd.).", + "name": "Ime", + "namePlaceholder": "Unesite ime modela...", + "type": "Tip", + "typeState": "Stanje", + "typeObject": "Objekt", + "objectLabel": "Oznaka objekta", + "objectLabelPlaceholder": "Odaberite vrstu objekta...", + "classificationType": "Vrsta klasifikacije", + "classificationTypeTip": "Učite više o vrstama klasifikacije", + "classificationTypeDesc": "Podoznake dodaju dodatni tekst oznaci objekta (npr. 'Ljudsko bit će: UPS'). Atributi su pretraživi metapodaci pohranjeni zasebno u metapodacima objekta.", + "classificationSubLabel": "Podoznaka", + "classificationAttribute": "Atribut", + "classes": "Klase", + "states": "Stanja", + "classesTip": "Učite više o klasama", + "classesStateDesc": "Definirajte različita stanja koja može imati područje kamere. Na primjer: 'otvoreno' i 'zatvoreno' za vrata garaže.", + "classesObjectDesc": "Definirajte različite kategorije u koje ćete klasificirati detektirane objekte. Na primjer: 'dostavljac', 'stanovnik', 'stranac' za klasifikaciju ljudi.", + "classPlaceholder": "Unesite ime klase...", + "errors": { + "nameRequired": "Ime modela je obavezno", + "nameLength": "Ime modela mora imati 64 znaka ili manje", + "nameOnlyNumbers": "Ime modela ne može sadržavati samo brojeve", + "classRequired": "Potrebna je bar jedna klasa", + "classesUnique": "Imena klasa moraju biti jedinstvena", + "noneNotAllowed": "Klasa 'none' nije dozvoljena", + "stateRequiresTwoClasses": "Modeli stanja zahtijevaju bar dvije klase", + "objectLabelRequired": "Molimo odaberite oznaku objekta", + "objectTypeRequired": "Molimo odaberite vrstu klasifikacije" + } + }, + "step2": { + "description": "Odaberite kamere i definirajte područje koje ćete nadzirati za svaku kameru. Model će klasificirati stanje ovih područja.", + "cameras": "Kamere", + "selectCamera": "Odaberite Kameru", + "noCameras": "Kliknite + za dodavanje kamera", + "selectCameraPrompt": "Odaberite kameru iz popisa da biste definirali njezino područje nadzora" + }, + "step3": { + "selectImagesPrompt": "Odaberite sve slike s: {{className}}", + "selectImagesDescription": "Kliknite na slike da biste ih odabrali. Kliknite Nadalje kada završite s ovom klasifikacijom.", + "allImagesRequired_one": "Molimo klasificirajte sve slike. Preostala je {{count}} slika.", + "allImagesRequired_few": "Molimo klasificirajte sve slike. Preostale su {{count}} slike.", + "allImagesRequired_other": "Molimo klasificirajte sve slike. Preostale su {{count}} slike.", + "generating": { + "title": "Generisanje uzoraka slika", + "description": "Frigate učitava reprezentativne slike iz vaših snimaka. Ovo može trajati trenutak..." + }, + "training": { + "title": "Obučavanje modela", + "description": "Vaš model se trenutno obučava u pozadini. Zatvorite ovaj dijalog, a vaš model će započeti raditi odmah kada se obuka završi." + }, + "retryGenerate": "Ponovno generisanje", + "noImages": "Nema generisanih uzoraka slika", + "classifying": "Klasifikacija i obuka...", + "trainingStarted": "Obuka je uspješno pokrenuta", + "modelCreated": "Model je uspješno stvoren. Koristite pogled Najnovije klasifikacije da dodate slike za nedostajuće stanja, a zatim obučite model.", + "errors": { + "noCameras": "Nema konfigurisanih kamera", + "noObjectLabel": "Nije odabrana oznaka objekta", + "generateFailed": "Neuspješno generisanje primera: {{error}}", + "generationFailed": "Generisanje nije uspješno. Molimo pokušajte ponovo.", + "classifyFailed": "Neuspješna klasifikacija slika: {{error}}" + }, + "generateSuccess": "Uspješno generisane uzorak slike", + "refreshExamples": "Generiši nove primjere", + "refreshConfirm": { + "title": "Generiši nove primjere?", + "description": "Ovo će generisati novi skup slika i obrisati sve odabire, uključujući prethodne klase. Trebat će vam ponovno odabrati primjere za sve klase." + }, + "missingStatesWarning": { + "title": "Primjeri nedostajućih klasa", + "description": "Nisu sve klase imale primjere. Pokušajte generisanje novih primjera da biste pronašli nedostajuću klasu, ili nastavite i koristite pogled Najnovije klasifikacije da biste kasnije dodali slike." + } + } + } +} diff --git a/web/public/locales/bs/views/configEditor.json b/web/public/locales/bs/views/configEditor.json new file mode 100644 index 0000000..7ce0100 --- /dev/null +++ b/web/public/locales/bs/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Uređivač konfiguracije - Frigate", + "configEditor": "Uređivač konfiguracije", + "safeConfigEditor": "Uređivač konfiguracije (Sigurnosni režim)", + "safeModeDescription": "Frigate je u sigurnosnom režimu zbog greške u validaciji konfiguracije.", + "copyConfig": "Kopiraj konfiguraciju", + "saveAndRestart": "Sačuvaj i ponovo pokreni", + "saveOnly": "Sačuvaj samo", + "confirm": "Napusti bez čuvanja?", + "toast": { + "success": { + "copyToClipboard": "Konfiguracija kopirana u međuspremnik." + }, + "error": { + "savingError": "Greška prilikom čuvanja konfiguracije" + } + } +} diff --git a/web/public/locales/bs/views/events.json b/web/public/locales/bs/views/events.json new file mode 100644 index 0000000..19869a8 --- /dev/null +++ b/web/public/locales/bs/views/events.json @@ -0,0 +1,92 @@ +{ + "alerts": "Upozorenja", + "detections": "Detekcije", + "camera": "Kamera", + "motion": { + "label": "Kretanje", + "only": "Samo pokret" + }, + "allCameras": "Sve Kamere", + "empty": { + "alert": "Nema upozorenja za pregled", + "detection": "Nema detekcija za pregled", + "motion": "Nema podataka o pokretu", + "recordingsDisabled": { + "title": "Snimci moraju biti omogućeni", + "description": "Pregledni stavci mogu se stvarati samo za kameru kada su snimci omogućeni za tu kameru." + } + }, + "timeline": { + "label": "vremenska linija", + "aria": "Odaberite vremensku liniju" + }, + "zoomIn": "Uvećajte", + "zoomOut": "Umanjite", + "events": { + "label": "Događaji", + "aria": "Odaberite događaje", + "noFoundForTimePeriod": "Nema događaja za ovaj vremenski period." + }, + "detail": { + "label": "Detalj", + "noDataFound": "Nema detaljnih podataka za pregled", + "aria": "Prekidač pregleda detalja", + "trackedObject_one": "{{count}} objekt", + "trackedObject_other": "{{count}} objekta", + "noObjectDetailData": "Nema dostupnih detaljnih podataka o objektu.", + "settings": "Postavke pregleda detalja", + "alwaysExpandActive": { + "title": "Uvijek proširujte aktivno", + "desc": "Uvijek proširite detalje objekta aktivnog stavka pregleda kada su dostupni." + } + }, + "objectTrack": { + "trackedPoint": "Praćeni točka", + "clickToSeek": "Kliknite da biste prešli na ovo vrijeme" + }, + "documentTitle": "Pregled - Frigate", + "recordings": { + "documentTitle": "Snimci - Frigate", + "invalidSharedLink": "Nemoguće otvoriti vezu za snimak sa vremenskom oznakom zbog greške u parsiranju.", + "invalidSharedCamera": "Nemoguće otvoriti vezu za snimak sa vremenskom oznakom zbog nepoznate ili neovlašćene kamere." + }, + "calendarFilter": { + "last24Hours": "Poslednje 24 sata" + }, + "markAsReviewed": "Označi kao pregledano", + "markTheseItemsAsReviewed": "Označi ove stavke kao pregledane", + "newReviewItems": { + "label": "Pregledaj nove stavke za pregled", + "button": "Nove stavke za pregled" + }, + "selected_one": "{{count}} odabrano", + "selected_other": "{{count}} odabrano", + "select_all": "Sve", + "detected": "detektovano", + "normalActivity": "Normal", + "needsReview": "Treba pregledati", + "securityConcern": "Sigurnosna zabrinutost", + "motionSearch": { + "menuItem": "Pretraga kretanja", + "openMenu": "Opcije kamere" + }, + "motionPreviews": { + "menuItem": "Pregledaj preglednike kretanja", + "title": "Preglednici kretanja: {{camera}}", + "mobileSettingsTitle": "Postavke preglednika kretanja", + "mobileSettingsDesc": "Prilagodite brzinu reprodukcije i osvetljavanje, i odaberite datum za pregled snimaka samo sa kretanjem.", + "dim": "Osvetljavanje", + "dimAria": "Prilagodite intenzitet osvetljavanja", + "dimDesc": "Povećajte osvetljavanje da biste povećali vidljivost područja kretanja.", + "speed": "Brzina", + "speedAria": "Odaberite brzinu reprodukcije preglednika", + "speedDesc": "Odaberite koliko brzo će se pregledni snimci reproducirati.", + "back": "Nazad", + "empty": "Nema pregleda dostupnih", + "noPreview": "Pregled nije dostupan", + "seekAria": "Pretражuj {{camera}} igraču do {{time}}", + "filter": "Filtar", + "filterDesc": "Odaberite područja da biste prikazali samo klipove sa kretanjem u tim područjima.", + "filterClear": "Očisti" + } +} diff --git a/web/public/locales/bs/views/explore.json b/web/public/locales/bs/views/explore.json new file mode 100644 index 0000000..8049fd8 --- /dev/null +++ b/web/public/locales/bs/views/explore.json @@ -0,0 +1,267 @@ +{ + "documentTitle": "Istraživanje - Frigate", + "generativeAI": "Generativna AI", + "exploreMore": "Istražite više {{label}} objekata", + "exploreIsUnavailable": { + "title": "Istraživanje nije dostupno", + "embeddingsReindexing": { + "context": "Istraživanje može se koristiti nakon što se reindeksiranje ugrađenih objekata završi.", + "startingUp": "Pokretanje…", + "estimatedTime": "Procijenjeno preostalo vrijeme:", + "finishingShortly": "Završetak uskoro", + "step": { + "thumbnailsEmbedded": "Ugrađene miniaturne slike: ", + "descriptionsEmbedded": "Ugrađene opise: ", + "trackedObjectsProcessed": "Obrađeni praćeni objekti: " + } + }, + "downloadingModels": { + "context": "Frigate preuzima potrebne modele ugrađenih objekata kako bi podržao funkciju Semantičke pretrage. Ovo može trajati nekoliko minuta ovisno o brzini vaše mreže.", + "setup": { + "visionModel": "Model vida", + "visionModelFeatureExtractor": "Izvođač značajki modela vida", + "textModel": "Model teksta", + "textTokenizer": "Tokenizator teksta" + }, + "tips": { + "context": "Moguće je da želite ponovno indeksirati ugrađene objekte koji se prate nakon što se modele preuzmu." + }, + "error": "Dogodila se greška. Provjerite zapise Frigate." + } + }, + "trackedObjectDetails": "Detalji praćenih objekata", + "type": { + "details": "Detalji", + "snapshot": "Snimak", + "thumbnail": "miniaturna slika", + "video": "Video", + "tracking_details": "detalji praćenja" + }, + "trackingDetails": { + "title": "Detalji praćenja", + "noImageFound": "Nije pronađena slika za ovaj vremenski moment.", + "createObjectMask": "Kreirajte masku objekta", + "adjustAnnotationSettings": "Prilagodite postavke oznaka", + "scrollViewTips": "Kliknite da biste vidjeli važne trenutke životnog ciklusa ovog objekta.", + "autoTrackingTips": "Pozicije okvirnih kutija neće biti tačne za autotracking kamere.", + "count": "{{first}} od {{second}}", + "trackedPoint": "Praćena tačka", + "lifecycleItemDesc": { + "visible": "{{label}} detektovan", + "entered_zone": "{{label}} ušao u {{zones}}", + "active": "{{label}} postao aktivno", + "stationary": "{{label}} postao stacionarno", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detektovan za {{label}}", + "other": "{{label}} prepoznat kao {{attribute}}" + }, + "gone": "{{label}} otišao", + "heard": "{{label}} čujeo", + "external": "{{label}} detektovan", + "header": { + "zones": "Zone", + "ratio": "Omjer", + "area": "Površina", + "score": "Rezultat", + "computedScore": "Izračunata ocjena", + "topScore": "Najbolja ocjena", + "toggleAdvancedScores": "Prekidač naprednih ocjena" + } + }, + "annotationSettings": { + "title": "Postavke oznaka", + "showAllZones": { + "title": "Prikaži sve zone", + "desc": "Uvijek prikazujte zone na okvirima gdje su objekti ušli u zonu." + }, + "offset": { + "label": "Pomak oznaka", + "desc": "Ova podatka dolaze iz vaše kamere detektovane snimke, ali se preklapaju na slikama iz snimke snimke. Vjerojatno nije moguće da su dva toka savršeno sinhronizirana. Kao rezultat, okvirni kutiji i snimke neće se savršeno poklopiti. Možete koristiti ovu postavku da pomaknete oznake unaprijed ili unazad u vremenu da bi ih bolje uskladili s snimljenim snimkom.", + "millisecondsToOffset": "Milisekunde za pomak detektovanih oznaka. Podrazumevano: 0", + "tips": "Smanjite vrijednost ako je reprodukcija videa ispred kutija i tačaka putanje, a povećajte vrijednost ako je reprodukcija videa iza njih. Ova vrijednost može biti negativna.", + "toast": { + "success": "Pomak anotacije za {{camera}} je sačuvan u konfiguracionu datoteku." + } + } + }, + "carousel": { + "previous": "Prethodni slajd", + "next": "Sljedeći slajd" + } + }, + "details": { + "item": { + "title": "Pregled detalja stavke", + "desc": "Detalji stavke za pregled", + "button": { + "share": "Dijelite ovu stavku za pregled", + "viewInExplore": "Pregledajte u Explore" + }, + "tips": { + "mismatch_one": "{{count}} nedostupan objekat je detektovan i uključen u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekcija, ili su već očišćeni/obrisani.", + "mismatch_few": "{{count}} nedostupnih objekata je detektovano i uključeno u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekciju, ili su već očišćeni/obrisani.", + "mismatch_other": "{{count}} nedostupnih objekata je detektovano i uključeno u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekciju, ili su već očišćeni/obrisani.", + "hasMissingObjects": "Prilagodite svoju konfiguraciju ako želite da Frigate sačuva pratiti objekte za sljedeće oznake: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "Zahtjev za novi opis je poslat {{provider}}. Ovisno o brzini vašeg provajdera, novi opis može potrajati neko vrijeme da se ponovno generira.", + "updatedSublabel": "Uspješno ažurirana podjezika.", + "updatedLPR": "Uspješno ažurirana tablica.", + "updatedAttributes": "Uspješno ažurirana atribute.", + "audioTranscription": "Uspješno zahtjev za audio transkripciju. Ovisno o brzini vašeg Frigate servera, transkripcija može potrajati neko vrijeme da se završi." + }, + "error": { + "regenerate": "Neuspješno poziv {{provider}} za novi opis: {{errorMessage}}", + "updatedSublabelFailed": "Neuspješno ažuriranje podjezika: {{errorMessage}}", + "updatedLPRFailed": "Neuspješno ažuriranje tablice: {{errorMessage}}", + "updatedAttributesFailed": "Neuspješno ažuriranje atribute: {{errorMessage}}", + "audioTranscription": "Neuspješno zahtjev za audio transkripciju: {{errorMessage}}" + } + } + }, + "label": "Oznaka", + "editSubLabel": { + "title": "Uredi podjeziku", + "desc": "Unesite novu podjeziku za ovaj {{label}}", + "descNoLabel": "Unesite novu podjeziku za ovaj pratiti objekt" + }, + "editLPR": { + "title": "Uredi tablica", + "desc": "Unesite novu vrijednost tablice za ovaj {{label}}", + "descNoLabel": "Unesite novu vrijednost tablice za ovaj praćeni objekt" + }, + "editAttributes": { + "title": "Uredi atribute", + "desc": "Odaberite atribute klasifikacije za ovaj {{label}}" + }, + "snapshotScore": { + "label": "Snimak Rezultat" + }, + "topScore": { + "label": "Najbolji Rezultat", + "info": "Najbolji rezultat je najviši srednji rezultat za praćeni objekt, pa se može razlikovati od rezultata prikazanog na minijaturi rezultata pretrage." + }, + "score": { + "label": "Rezultat" + }, + "recognizedLicensePlate": "Prepoznata tablica", + "attributes": "Atributi klasifikacije", + "estimatedSpeed": "Procijenjena brzina", + "objects": "Objekti", + "camera": "Kamera", + "zones": "Zone", + "timestamp": "Vremenski pečat", + "button": { + "findSimilar": "Pronađi slične", + "regenerate": { + "title": "Regeneriraj", + "label": "Regeneriraj opis praćenog objekta" + } + }, + "description": { + "label": "Opis", + "placeholder": "Opis praćenog objekta", + "aiTips": "Frigate neće tražiti opis od vašeg generativnog AI provajdera dok se životni vijek praćenog objekta ne završi." + }, + "expandRegenerationMenu": "Proširi izbornik regeneracije", + "regenerateFromSnapshot": "Regeneriraj iz snimka", + "regenerateFromThumbnails": "Regeneriraj iz minijatura", + "tips": { + "descriptionSaved": "Uspješno sačuvan opis", + "saveDescriptionFailed": "Neuspješno ažuriranje opisa: {{errorMessage}}" + }, + "title": { + "label": "Naslov" + }, + "scoreInfo": "Informacije o rezultatu" + }, + "itemMenu": { + "downloadVideo": { + "label": "Preuzmi video", + "aria": "Preuzmi video" + }, + "downloadSnapshot": { + "label": "Preuzmi snimak", + "aria": "Preuzmi snimak" + }, + "downloadCleanSnapshot": { + "label": "Preuzmi čist snimak", + "aria": "Preuzmi čist snimak" + }, + "viewTrackingDetails": { + "label": "Pregledaj detalje praćenja", + "aria": "Prikaži detalje praćenja" + }, + "findSimilar": { + "label": "Pronađi slične", + "aria": "Pronađi slične praćene objekte" + }, + "addTrigger": { + "label": "Dodaj izazov", + "aria": "Dodaj izazov za ovaj praćeni objekt" + }, + "audioTranscription": { + "label": "Transkriptiraj", + "aria": "Zatraži transkripciju zvuka" + }, + "submitToPlus": { + "label": "Pošalji na Frigate+", + "aria": "Pošalji na Frigate Plus" + }, + "viewInHistory": { + "label": "Pregledajte u povijesti", + "aria": "Pregledajte u povijesti" + }, + "deleteTrackedObject": { + "label": "Obriši ovaj praćeni objekt" + }, + "showObjectDetails": { + "label": "Prikaži put objekta" + }, + "hideObjectDetails": { + "label": "Sakrij put objekta" + }, + "debugReplay": { + "label": "Debug ponovno snimanje", + "aria": "Pregledaj ovaj praćeni objekt u pogledu debug ponovnog snimanja" + }, + "more": { + "aria": "Više" + } + }, + "dialog": { + "confirmDelete": { + "title": "Potvrdi brisanje", + "desc": "Brisanje ovog praćenog objekta uklanja snimak, bilo kakve sačuvane ugradnje, i sve povezane unose detalja praćenja. Snimljeni materijal ovog praćenog objekta u pogledu povijesti NEĆE biti obrisan.

    Sigurno li želite nastaviti?" + }, + "toast": { + "error": "Greška prilikom brisanja ovog praćenog objekta: {{errorMessage}}" + } + }, + "noTrackedObjects": "Nijedan praćeni objekt nije pronađen", + "fetchingTrackedObjectsFailed": "Greška prilikom dohvaćanja praćenih objekata: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} praćeni objekt ", + "trackedObjectsCount_few": "{{count}} praćena objekta ", + "trackedObjectsCount_other": "{{count}} praćena objekta ", + "searchResult": { + "tooltip": "Pronađeno {{type}} na {{confidence}}%", + "previousTrackedObject": "Prethodni praćeni objekt", + "nextTrackedObject": "Sljedeći praćeni objekt", + "deleteTrackedObject": { + "toast": { + "success": "Praćeni objekt je uspješno obrisan.", + "error": "Neuspješno brisanje praćenog objekta: {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "Analiza AI" + }, + "concerns": { + "label": "Pitanja" + }, + "objectLifecycle": { + "noImageFound": "Nije pronađena slika za ovaj praćeni objekt." + } +} diff --git a/web/public/locales/bs/views/exports.json b/web/public/locales/bs/views/exports.json new file mode 100644 index 0000000..b04ece1 --- /dev/null +++ b/web/public/locales/bs/views/exports.json @@ -0,0 +1,128 @@ +{ + "search": "Pretraga", + "documentTitle": "Izvoz - Frigate", + "selected_one": "{{count}} odabrano", + "selected_other": "{{count}} odabrano", + "noExports": "Nijedan izvoz nije pronađen", + "headings": { + "cases": "Slučajevi", + "uncategorizedExports": "Nekategorizirani izvozi" + }, + "deleteExport": { + "label": "Obriši izvoz", + "desc": "Da li ste sigurni da želite da obrišete {{exportName}}?" + }, + "editExport": { + "title": "Preimenuj izvoz", + "desc": "Unesite novi naziv za ovaj izvoz.", + "saveExport": "Sačuvaj izvoz" + }, + "tooltip": { + "shareExport": "Dijeli izvoz", + "downloadVideo": "Preuzmi video", + "editName": "Uredi naziv", + "deleteExport": "Obriši izvoz", + "assignToCase": "Dodaj u slučaj", + "removeFromCase": "Ukloni iz slučaja" + }, + "toolbar": { + "newCase": "Novi slučaj", + "addExport": "Dodaj izvoz", + "editCase": "Uredi slučaj", + "deleteCase": "Obriši slučaj" + }, + "toast": { + "error": { + "renameExportFailed": "Neuspješno preimenovanje izvoza: {{errorMessage}}", + "assignCaseFailed": "Neuspješno ažuriranje dodjele slučaja: {{errorMessage}}", + "caseSaveFailed": "Neuspješno čuvanje slučaja: {{errorMessage}}", + "caseDeleteFailed": "Neuspješno brisanje slučaja: {{errorMessage}}" + } + }, + "deleteCase": { + "label": "Obriši slučaj", + "desc": "Da li ste sigurni da želite da obrišete {{caseName}}?", + "descKeepExports": "Izvozi će ostati dostupni kao nekategorizirani izvozi.", + "descDeleteExports": "Svi izvozi u ovom slučaju trajno će biti obrisani.", + "deleteExports": "Takođe izbriši izvoze" + }, + "caseDialog": { + "title": "Dodaj u slučaj", + "description": "Odaberite postojeći slučaj ili napravite novi.", + "selectLabel": "Slučaj", + "newCaseOption": "Napravite novi slučaj", + "nameLabel": "Ime slučaja", + "descriptionLabel": "Opis" + }, + "caseCard": { + "emptyCase": "Nema još izvoza" + }, + "jobCard": { + "defaultName": "{{camera}} izvoz", + "queued": "U redu", + "running": "Pokretanje", + "preparing": "Priprema", + "copying": "Kopiranje", + "encoding": "Kodiranje", + "encodingRetry": "Kodiranje (ponovi)", + "finalizing": "Završavanje" + }, + "caseView": { + "noDescription": "Nema opisa", + "createdAt": "Kreirano {{value}}", + "exportCount_one": "1 izvoz", + "exportCount_other": "{{count}} izvozi", + "cameraCount_one": "1 kamera", + "cameraCount_other": "{{count}} kamere", + "showMore": "Prikaži više", + "showLess": "Prikaži manje", + "emptyTitle": "Ovaj slučaj je prazan", + "emptyDescription": "Dodaj postojet će nekategorizirane izvoze kako bi slučaj ostao organizovan.", + "emptyDescriptionNoExports": "Nema dostupnih nekategoriziranih izvoza koje je moguće dodati još." + }, + "caseEditor": { + "createTitle": "Kreiraj slučaj", + "editTitle": "Uredi slučaj", + "namePlaceholder": "Ime slučaja", + "descriptionPlaceholder": "Dodaj napomene ili kontekst za ovaj slučaj" + }, + "addExportDialog": { + "title": "Dodaj izvoz u {{caseName}}", + "searchPlaceholder": "Pretraga nekategoriziranih izvoza", + "empty": "Nema nekategoriziranih izvoza koji odgovaraju ovoj pretrazi.", + "addButton_one": "Dodaj 1 izvoz", + "addButton_other": "Dodaj {{count}} izvoza", + "adding": "Dodavanje..." + }, + "bulkActions": { + "addToCase": "Dodaj u slučaj", + "moveToCase": "Premjesti u slučaj", + "removeFromCase": "Ukloni iz slučaja", + "delete": "Obriši", + "deleteNow": "Obriši sada" + }, + "bulkDelete": { + "title": "Obriši izvoze", + "desc_one": "Sigurni li ste da želite obrisati {{count}} izvoz?", + "desc_other": "Sigurni li ste da želite obrisati {{count}} izvoze?" + }, + "bulkRemoveFromCase": { + "title": "Ukloni iz slučaja", + "desc_one": "Ukloni {{count}} izvoz iz ovog slučaja?", + "desc_other": "Ukloni {{count}} izvoze iz ovog slučaja?", + "descKeepExports": "Izvozi će biti premješteni u nekategorizirane.", + "descDeleteExports": "Izvozi će biti trajno obrisani.", + "deleteExports": "Umjesto toga, obriši izvoze" + }, + "bulkToast": { + "success": { + "delete": "Uspješno obrisani izvozi", + "reassign": "Uspješno ažurirana dodjela slučaja", + "remove": "Uspješno uklonjeni izvozi iz slučaja" + }, + "error": { + "deleteFailed": "Neuspješno brisanje izvoza: {{errorMessage}}", + "reassignFailed": "Neuspješno ažuriranje dodjele slučaja: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/bs/views/faceLibrary.json b/web/public/locales/bs/views/faceLibrary.json new file mode 100644 index 0000000..db74fe1 --- /dev/null +++ b/web/public/locales/bs/views/faceLibrary.json @@ -0,0 +1,98 @@ +{ + "description": { + "addFace": "Dodajte novu kolekciju u Biblioteku lica prema učitavanju svoje prve slike.", + "placeholder": "Unesite ime za ovu kolekciju", + "invalidName": "Neprihvatljivo ime. Imena mogu sadržavati samo slova, brojeve, razmake, aposrofe, donje crte i crte.", + "nameCannotContainHash": "Ime ne može sadržavati #." + }, + "details": { + "unknown": "Nepoznato", + "timestamp": "Vremenski pečat", + "scoreInfo": "Ocjena je težinski prosjek svih ocjena lica, težinski određen prema veličini lica u svakoj slici." + }, + "train": { + "titleShort": "Nedavno", + "title": "Najnovije prepoznavanja", + "aria": "Odaberite nedavna prepoznavanja", + "empty": "Nema nedavnih pokušaja prepoznavanja lica" + }, + "documentTitle": "Biblioteka lica - Frigate", + "uploadFaceImage": { + "title": "Učitajte sliku lica", + "desc": "Učitajte sliku za skeniranje lica i uključite za {{pageToggle}}" + }, + "collections": "Kolekcije", + "createFaceLibrary": { + "new": "Stvori novo lice", + "nextSteps": "Da biste izgradili čvrstu osnovu:
  • Koristite karticu Najnovije prepoznavanja da biste odabrali i trenirali se na slikama za svaku detektiranu osobu.
  • Fokusirajte se na slike iz pravog ugla za najbolje rezultate; izbjegavajte slike za treniranje koje prikazuju lica pod uglom.
  • " + }, + "steps": { + "faceName": "Unesite ime lica", + "uploadFace": "Učitajte sliku lica", + "nextSteps": "Sljedeći koraci", + "description": { + "uploadFace": "Učitajte sliku od {{name}} koja prikazuje njihovo lice iz pravog ugla. Slika ne mora biti izrezana samo na njihovo lice." + } + }, + "deleteFaceLibrary": { + "title": "Izbrišite ime", + "desc": "Da li ste sigurni da želite izbrisati kolekciju {{name}}? Ovo će trajno izbrisati sva povezana lica." + }, + "deleteFaceAttempts": { + "title": "Izbrišite lica", + "desc_one": "Da li ste sigurni da želite izbrisati {{count}} lice? Ova akcija ne može se poništiti.", + "desc_few": "Da li ste sigurni da želite izbrisati {{count}} lica? Ova akcija ne može se poništiti.", + "desc_other": "Da li ste sigurni da želite izbrisati {{count}} lica? Ova akcija ne može se poništiti." + }, + "renameFace": { + "title": "Preimenujte lice", + "desc": "Unesite novo ime za {{name}}" + }, + "button": { + "deleteFaceAttempts": "Izbrišite lica", + "addFace": "Dodaj lice", + "renameFace": "Preimenuj lice", + "deleteFace": "Obriši lice", + "uploadImage": "Prenesi sliku", + "reprocessFace": "Ponovno obradi lice" + }, + "imageEntry": { + "validation": { + "selectImage": "Molimo izaberite datoteku slike." + }, + "dropActive": "Pustite sliku ovdje…", + "dropInstructions": "Povucite i ispišite, zalijepite sliku ovdje ili kliknite za odabir", + "maxSize": "Maksimalna veličina: {{size}}MB" + }, + "nofaces": "Nema dostupnih lica", + "trainFaceAs": "Obuči lice kao:", + "trainFace": "Obuči lice", + "reclassifyFaceAs": "Ponovno klasificiraj lice kao:", + "reclassifyFace": "Ponovno klasificiraj lice", + "toast": { + "success": { + "uploadedImage": "Uspješno prenesena slika.", + "addFaceLibrary": "{{name}} je uspješno dodan u biblioteku lica!", + "deletedFace_one": "Uspješno obrisano {{count}} lice.", + "deletedFace_few": "Uspješno obrisana {{count}} lica.", + "deletedFace_other": "Uspješno obrisana {{count}} lica.", + "deletedName_one": "{{count}} lice je uspješno obrisano.", + "deletedName_few": "{{count}} lica su uspješno obrisana.", + "deletedName_other": "{{count}} lica su uspješno obrisana.", + "renamedFace": "Uspješno preimenovan lice na {{name}}", + "trainedFace": "Uspješno obučeno lice.", + "reclassifiedFace": "Uspješno ponovno klasificirano lice.", + "updatedFaceScore": "Uspješno ažurirana ocjena lica na {{name}} ({{score}})." + }, + "error": { + "uploadingImageFailed": "Nije uspješno prenijeti sliku: {{errorMessage}}", + "addFaceLibraryFailed": "Nije uspješno postaviti ime lica: {{errorMessage}}", + "deleteFaceFailed": "Neuspješno brisanje: {{errorMessage}}", + "deleteNameFailed": "Nije uspješno obrisati ime: {{errorMessage}}", + "renameFaceFailed": "Nije uspješno preimenovati lice: {{errorMessage}}", + "trainFailed": "Nije uspješno trenirati: {{errorMessage}}", + "reclassifyFailed": "Nije uspješno ponovno klasifikovati lice: {{errorMessage}}", + "updateFaceScoreFailed": "Nije uspješno ažurirati bodove lica: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/bs/views/live.json b/web/public/locales/bs/views/live.json new file mode 100644 index 0000000..e4fa673 --- /dev/null +++ b/web/public/locales/bs/views/live.json @@ -0,0 +1,199 @@ +{ + "documentTitle": { + "default": "Uživo - Frigate", + "withCamera": "{{camera}} - Uživo - Frigate" + }, + "lowBandwidthMode": "Nizopojasni režim", + "twoWayTalk": { + "enable": "Omogući dvostrani razgovor", + "disable": "Onemogući dvostrani razgovor" + }, + "cameraAudio": { + "enable": "Omogući zvuk kamere", + "disable": "Onemogući zvuk kamere" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kliknite unutar okvira da biste centrirali kameru", + "enable": "Omogući klik za pomak", + "enableWithZoom": "Omogući klik za pomak / povucite za uvećanje", + "disable": "Onemogući klik za pomak" + }, + "left": { + "label": "Pomaknite PTZ kameru ulevo" + }, + "up": { + "label": "Pomaknite PTZ kameru gore" + }, + "down": { + "label": "Pomaknite PTZ kameru dolje" + }, + "right": { + "label": "Pomaknite PTZ kameru udesno" + } + }, + "zoom": { + "in": { + "label": "Uvećajte PTZ kameru" + }, + "out": { + "label": "Umanjite PTZ kameru" + } + }, + "focus": { + "in": { + "label": "Fokusirajte PTZ kameru unapred" + }, + "out": { + "label": "Fokusirajte PTZ kameru unazad" + } + }, + "frame": { + "center": { + "label": "Kliknite unutar okvira da biste centrirali PTZ kameru" + } + }, + "presets": "Preseti PTZ kamere" + }, + "camera": { + "enable": "Omogući kameru", + "disable": "Onemogući kameru" + }, + "muteCameras": { + "enable": "Utišajte sve kamere", + "disable": "Ponovo uključite zvuk za sve kamere" + }, + "detect": { + "enable": "Omogući detekciju", + "disable": "Onemogući detekciju" + }, + "recording": { + "enable": "Omogući snimanje", + "disable": "Onemogući snimanje" + }, + "snapshots": { + "enable": "Omogući snimke", + "disable": "Onemogući snimke" + }, + "snapshot": { + "takeSnapshot": "Preuzmi trenutni snimak", + "noVideoSource": "Nema dostupnog video izvora za snimak.", + "captureFailed": "Neuspješno snimanje trenutnog snimka.", + "downloadStarted": "Preuzimanje trenutnog snimka započeto." + }, + "audioDetect": { + "enable": "Omogući detekciju zvuka", + "disable": "Onemogući detekciju zvuka" + }, + "transcription": { + "enable": "Omogući prepoznavanje zvuka uživo", + "disable": "Onemogući prepoznavanje zvuka uživo" + }, + "autotracking": { + "enable": "Omogući automatsko praćenje", + "disable": "Onemogući automatsko praćenje" + }, + "streamStats": { + "enable": "Prikaži statistiku prijenosa", + "disable": "Sakrij statistiku prijenosa" + }, + "manualRecording": { + "title": "Na zahtjev", + "tips": "Preuzmi trenutni snimak ili pokreni ručni događaj na temelju postavki trajanja snimanja ove kamere.", + "playInBackground": { + "label": "Ponovno postavi stream", + "desc": "Omogući ovu opciju da nastavi streamanje kada je pokazivač sakriven." + }, + "showStats": { + "label": "Prikaži statistiku", + "desc": "Omogući ovu opciju da prikaže statistiku prijenosa kao preklapanje na toku kamere." + }, + "debugView": "Pregled za otklanjanje grešaka", + "start": "Počni snimanje na zahtjev", + "started": "Pokrenuto ručno snimanje na zahtjev.", + "failedToStart": "Neuspješno pokretanje ručnog snimanja na zahtjev.", + "recordDisabledTips": "Kako je snimanje onemogućeno ili ograničeno u konfiguraciji za ovu kameru, spremat će se samo snimak.", + "end": "Završi snimanje na zahtjev", + "ended": "Završeno ručno snimanje na zahtjev.", + "failedToEnd": "Neuspješno završavanje ručnog snimanja na zahtjev." + }, + "streamingSettings": "Postavke streamanja", + "notifications": "Obavještenja", + "audio": "Audio", + "suspend": { + "forTime": "Pauziraj za: " + }, + "stream": { + "title": "Tok", + "audio": { + "tips": { + "title": "Audio mora biti izlaz iz vaše kamere i konfiguriran u go2rtc za ovaj stream." + }, + "available": "Audio je dostupan za ovaj stream", + "unavailable": "Audio nije dostupan za ovaj stream" + }, + "debug": { + "picker": "Izbor streama nije dostupan u režimu debuga. Pregled debuga uvijek koristi stream dodeljen ulozi detekcije." + }, + "twoWayTalk": { + "tips": "Vaš uređaj mora podržavati funkciju, a WebRTC mora biti konfiguriran za dvosmernu komunikaciju.", + "available": "Dvosmerna komunikacija je dostupna za ovaj stream", + "unavailable": "Dvosmerna komunikacija nije dostupna za ovaj stream" + }, + "lowBandwidth": { + "tips": "Živo prikazivanje je u režimu niske propusnosti zbog buferiranja ili grešaka u streamu.", + "resetStream": "Ponovno postavi stream" + }, + "playInBackground": { + "label": "Ponovno postavi stream", + "tips": "Omogući ovu opciju da nastavi streamanje kada je pokazivač sakriven." + } + }, + "cameraSettings": { + "title": "{{camera}} Postavke", + "cameraEnabled": "Kamera omogućena", + "objectDetection": "Detekcija objekata", + "recording": "Snimanje", + "snapshots": "Snimci", + "audioDetection": "Detekcija zvuka", + "transcription": "Transkripcija zvuka", + "autotracking": "Autotračenje" + }, + "history": { + "label": "Prikaži povijesne snimke" + }, + "effectiveRetainMode": { + "modes": { + "all": "Sve", + "motion": "Kretanje", + "active_objects": "Aktivni objekti" + } + }, + "editLayout": { + "label": "Uredi raspored", + "group": { + "label": "Uredi grupu kamera" + }, + "exitEdit": "Izađi iz uređivanja" + }, + "noCameras": { + "title": "Nema konfiguriranih kamera", + "description": "Počnite tako što ćete povezati kameru s Frigate.", + "buttonText": "Dodaj kameru", + "restricted": { + "title": "Nema dostupnih kamera", + "description": "Nemate dozvolu za pregled bilo koje kamere u ovoj grupi." + }, + "default": { + "title": "Nema konfiguriranih kamera", + "description": "Počnite tako što ćete povezati kameru s Frigate.", + "buttonText": "Dodaj kameru" + }, + "group": { + "title": "Nema kamera u grupi", + "description": "Ova grupa kamera nema dodeljene ili omogućene kamere.", + "buttonText": "Upravljajte grupama" + } + } +} diff --git a/web/public/locales/bs/views/motionSearch.json b/web/public/locales/bs/views/motionSearch.json new file mode 100644 index 0000000..f9e417f --- /dev/null +++ b/web/public/locales/bs/views/motionSearch.json @@ -0,0 +1,77 @@ +{ + "documentTitle": "Pretraga pokreta - Frigate", + "title": "Pretraga pokreta", + "description": "Nacrtaj poligon da biste definirali regiju interesa, a zatim navedite vremenski raspon za pretragu promjena pokreta unutar te regije.", + "selectCamera": "Pretraga pokreta učitava se", + "startSearch": "Počni pretragu", + "searchStarted": "Pretraga započeta", + "searchCancelled": "Pretraga otkazana", + "cancelSearch": "Otkaži", + "searching": "Pretraga u toku.", + "searchComplete": "Pretraga završena", + "noResultsYet": "Pokrenite pretragu da biste pronašli promjene pokreta u odabranoj regiji", + "noChangesFound": "Nisu otkrivene promjene piksela u odabranoj regiji", + "changesFound_one": "Pronađeno {{count}} promjena pokreta", + "changesFound_few": "Pronađeno {{count}} nekoliko pokreta", + "changesFound_other": "Pronađeno {{count}} promjene pokreta", + "framesProcessed": "{{count}} okvir procesiran", + "jumpToTime": "Preskoči na ovo vrijeme", + "results": "Rezultati", + "showSegmentHeatmap": "Top mapa", + "newSearch": "Nova pretraga", + "clearResults": "Očisti rezultate", + "clearROI": "Očisti poligon", + "polygonControls": { + "points_one": "{{count}} tačka", + "points_few": "{{count}} tačke", + "points_other": "{{count}} tačke", + "undo": "Poništi posljednju tačku", + "reset": "Ponovi poligon" + }, + "motionHeatmapLabel": "Top mapa pokreta", + "dialog": { + "title": "Pretraga pokreta", + "cameraLabel": "Kamera", + "previewAlt": "Pregled kamere za {{camera}}" + }, + "timeRange": { + "title": "Opseg pretrage", + "start": "Početno vrijeme", + "end": "Krajnje vrijeme" + }, + "settings": { + "title": "Postavke pretrage", + "parallelMode": "Paralelni način", + "parallelModeDesc": "Skeniranje više segmenata snimaka istovremeno (brže, ali značajno intenzivnije za CPU)", + "threshold": "Praga osjetljivosti", + "thresholdDesc": "Niže vrijednosti detektiraju manje promjene (1-255)", + "minArea": "Minimalna površina promjene", + "minAreaDesc": "Minimalni postotak područja interesa koji mora promijeniti da bi se smatrao značajnim", + "frameSkip": "Preskoči okvir", + "frameSkipDesc": "Obrađujte svaki N-ti okvir. Postavite ovo na brzinu okvira vaše kamere da biste obradili jedan okvir po sekundi (npr. 5 za 5 FPS kameru, 30 za 30 FPS kameru). Više vrijednosti će biti brže, ali mogu propustiti kratke događaje pokreta.", + "maxResults": "Maksimalni rezultati", + "maxResultsDesc": "Zaustavi nakon ovog broja odgovarajućih vremenskih oznaka" + }, + "errors": { + "noCamera": "Molimo odaberite kameru", + "noROI": "Molimo nacrtajte područje interesa", + "noTimeRange": "Molimo odaberite vremenski opseg", + "invalidTimeRange": "Krajnje vrijeme mora biti nakon početnog vremena", + "searchFailed": "Pretraga neuspješna: {{message}}", + "polygonTooSmall": "Poligon mora imati najmanje 3 točke", + "unknown": "Nepoznata greška" + }, + "changePercentage": "{{percentage}}% promijenjeno", + "metrics": { + "title": "Metrike pretrage", + "segmentsScanned": "Skenirani segmenti", + "segmentsProcessed": "Obrađeno", + "segmentsSkippedInactive": "Preskočeno (bez aktivnosti)", + "segmentsSkippedHeatmap": "Preskočeno (bez preklapanja ROI)", + "fallbackFullRange": "Povratni put skeniranje cijelog opsega", + "framesDecoded": "Dekodirani okviri", + "wallTime": "Vrijeme pretrage", + "segmentErrors": "Greške segmenta", + "seconds": "{{seconds}}s" + } +} diff --git a/web/public/locales/bs/views/recording.json b/web/public/locales/bs/views/recording.json new file mode 100644 index 0000000..1810fad --- /dev/null +++ b/web/public/locales/bs/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtar", + "export": "Izvoz", + "calendar": "Kalendar", + "filters": "Filtari", + "toast": { + "error": { + "noValidTimeSelected": "Nije odabran valjan vremenski opseg", + "endTimeMustAfterStartTime": "Krajnje vrijeme mora biti nakon početnog vremena" + } + } +} diff --git a/web/public/locales/bs/views/replay.json b/web/public/locales/bs/views/replay.json new file mode 100644 index 0000000..0b102b5 --- /dev/null +++ b/web/public/locales/bs/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "Debug ponavljanje", + "description": "Ponovno prikazivanje snimaka kamere za ispitivanje. Lista objekata prikazuje zakasnjelje sažetak detektiranih objekata, a kartica Zapisi prikazuje tok unutrašnjih poruka Frigate iz snimaka ponavljanja.", + "websocket_messages": "Poruke", + "dialog": { + "title": "Počni debug ponavljanje", + "description": "Kreiraj privremenu kameru za ponavljanje koja ponavlja povijesne snimke za ispitivanje problema detekcije i praćenja objekata. Kamera za ponavljanje će imati istu konfiguraciju detekcije kao i izvorna kamera. Odaberite vremenski raspon za početak.", + "camera": "Izvorna kamera", + "timeRange": "Vremenski opseg", + "preset": { + "1m": "Posljednja 1 minuta", + "5m": "Posljednje 5 minuta", + "timeline": "Iz vremenske linije", + "custom": "Prilagođeno" + }, + "startButton": "Počni ponavljanje", + "selectFromTimeline": "Odaberite", + "starting": "Pokretanje ponavljanja...", + "startLabel": "Početak", + "endLabel": "Kraj", + "toast": { + "error": "Neuspješno pokretanje debug ponavljanja: {{error}}", + "alreadyActive": "Već postoji aktivna sesija ponavljanja", + "stopError": "Neuspješno zaustavljanje debug ponavljanja: {{error}}", + "goToReplay": "Idi na ponavljanje" + } + }, + "page": { + "noSession": "Nema aktivne sesije ponavljanja", + "noSessionDesc": "Pokrenite debug ponavljanje iz pogleda Povijest klikom na dugme Debug Replay u alatnoj traci.", + "goToRecordings": "Idi na povijest", + "sourceCamera": "Izvorna kamera", + "replayCamera": "Kamera za ponavljanje", + "initializingReplay": "Inicijalizacija ponavljanja...", + "stoppingReplay": "Zaustavljanje ponavljanja...", + "stopReplay": "Zaustavi ponavljanje", + "confirmStop": { + "title": "Zaustavi režim ponavljanja za debagovanje?", + "description": "Ovo će zaustaviti sesiju ponavljanja i očistiti sve privremene podatke. Sigurni li?", + "confirm": "Zaustavi ponavljanje", + "cancel": "Otkaži" + }, + "activity": "Aktivnost", + "objects": "Popis objekata", + "audioDetections": "Audio detekcije", + "noActivity": "Nema detektovane aktivnosti", + "activeTracking": "Aktivno praćenje", + "noActiveTracking": "Nema aktivnog praćenja", + "configuration": "Konfiguracija", + "configurationDesc": "Podesiti precizno detekciju pokreta i praćenje objekata za kameru za debagovanje ponavljanja. Promjene se ne čuvaju u datoteci konfiguracije Frigate.", + "preparingClip": "Pripremam klip…", + "preparingClipDesc": "Frigate spaja snimke za odabrani vremenski raspon. Ovo može potrajati minut za duže raspone.", + "startingCamera": "Pokretanje ponovnog pokretanja otklanjanja grešaka…", + "startError": { + "title": "Neuspjelo pokretanje ponovnog prikaza otklanjanja grešaka", + "back": "Povratak na historiju" + } + } +} diff --git a/web/public/locales/bs/views/search.json b/web/public/locales/bs/views/search.json new file mode 100644 index 0000000..f33ff10 --- /dev/null +++ b/web/public/locales/bs/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Pretraga", + "button": { + "save": "Sačuvaj pretragu", + "clear": "Očisti pretragu", + "delete": "Obriši sačuvanu pretragu", + "filterInformation": "Filtrirajte informacije", + "filterActive": "Filtari aktivni" + }, + "savedSearches": "Sačuvane pretrage", + "searchFor": "Pretraga za {{inputValue}}", + "trackedObjectId": "ID praćenog objekta", + "filter": { + "label": { + "cameras": "Kamere", + "labels": "Oznake", + "zones": "Zone", + "sub_labels": "Podoznake", + "attributes": "Atributi", + "search_type": "Tip pretrage", + "time_range": "Vremenski opseg", + "before": "Prije", + "after": "Nakon", + "min_score": "Min. bodovi", + "max_score": "Max. bodovi", + "min_speed": "Min. brzina", + "max_speed": "Max. brzina", + "recognized_license_plate": "Prepoznata tablica", + "has_clip": "Ima klip", + "has_snapshot": "Ima snimak" + }, + "searchType": { + "thumbnail": "Minijatura", + "description": "Opis" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Datum 'before' mora biti kasniji od datuma 'after'.", + "afterDatebeEarlierBefore": "Datum 'after' mora biti raniji od datuma 'before'.", + "minScoreMustBeLessOrEqualMaxScore": "Vrijednost 'min_score' mora biti manja ili jednaka vrijednosti 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Vrijednost 'max_score' mora biti veća ili jednaka vrijednosti 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Vrijednost 'min_speed' mora biti manja ili jednaka vrijednosti 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Vrijednost 'max_speed' mora biti veća ili jednaka vrijednosti 'min_speed'." + } + }, + "tips": { + "title": "Kako koristiti tekstualne filtere", + "desc": { + "text": "Filteri vam pomažu da sužite rezultate pretrage. Evo kako ih koristiti u polju za unos:", + "step1": "Unesite ime ključa filtera, zatim dvojtočku (npr. \"kamere:\").", + "step2": "Izaberite vrijednost iz predloga ili unesite vlastitu.", + "step3": "Koristite više filtera dodavanjem jednog za drugim s razmakom između.", + "step4": "Filteri datuma (pre: i nakon:) koriste {{DateFormat}} format.", + "step5": "Filter raspona vremena koristi format {{exampleTime}}.", + "step6": "Uklonite filtre klikom na 'x' pored njih.", + "exampleLabel": "Primjer:" + } + }, + "header": { + "currentFilterType": "Vrijednosti filtera", + "noFilters": "Filtari", + "activeFilters": "Aktivni filteri" + } + }, + "similaritySearch": { + "title": "Pretraga sličnosti", + "active": "Pretraga sličnosti aktivna", + "clear": "Očisti pretragu sličnosti" + }, + "placeholder": { + "search": "Pretraži…" + } +} diff --git a/web/public/locales/bs/views/settings.json b/web/public/locales/bs/views/settings.json new file mode 100644 index 0000000..10126fe --- /dev/null +++ b/web/public/locales/bs/views/settings.json @@ -0,0 +1,1698 @@ +{ + "documentTitle": { + "default": "Postavke - Frigate", + "authentication": "Postavke autentifikacije - Frigate", + "cameraManagement": "Upravljanje kamerama - Frigate", + "cameraReview": "Postavke pregleda kamera - Frigate", + "enrichments": "Postavke bogatstva - Frigate", + "masksAndZones": "Uređivač maski i zona - Frigate", + "motionTuner": "Podešavanje pokreta - Frigate", + "object": "Debug - Frigate", + "general": "Postavke korisničkog sučelja - Frigate", + "globalConfig": "Globalna konfiguracija - Frigate", + "cameraConfig": "Konfiguracija kamere - Frigate", + "frigatePlus": "Postavke Frigate+ - Frigate", + "notifications": "Postavke obavijesti - Frigate", + "maintenance": "Održavanje - Frigate", + "profiles": "Profili - Frigate" + }, + "menu": { + "system": "Sistem", + "profiles": "Profili", + "general": "Općenito", + "globalConfig": "Globalna konfiguracija", + "integrations": "Integracije", + "cameras": "Konfiguracija kamere", + "ui": "UI", + "uiSettings": "Postavke korisničkog sučelja", + "globalDetect": "Detekcija objekata", + "globalRecording": "Snimanje", + "globalSnapshots": "Snimci", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Detekcija pokreta", + "globalObjects": "Objekti", + "globalReview": "Pregled", + "globalAudioEvents": "Audio događaji", + "globalLivePlayback": "Uživo prikaz", + "globalTimestampStyle": "Stil vremenske oznake", + "systemDatabase": "Baza podataka", + "systemTls": "TLS", + "systemAuthentication": "Autentifikacija", + "systemNetworking": "Mrežno", + "systemProxy": "Proxy", + "systemUi": "UI", + "systemLogging": "Zapisi", + "systemEnvironmentVariables": "Okolinski varijable", + "systemTelemetry": "Telemetrija", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "Hardver detektora", + "systemDetectionModel": "Model detekcije", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "go2rtc streams", + "integrationSemanticSearch": "Semantička pretraga", + "integrationGenerativeAi": "Generativna AI", + "integrationFaceRecognition": "Prepoznavanje lica", + "integrationLpr": "Prepoznavanje tablice za registraciju", + "integrationObjectClassification": "Klasifikacija objekata", + "integrationAudioTranscription": "Transkripcija zvuka", + "cameraDetect": "Detekcija objekata", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "Snimanje", + "cameraSnapshots": "Snimci", + "cameraMotion": "Detekcija pokreta", + "cameraObjects": "Objekti", + "cameraConfigReview": "Pregled", + "cameraAudioEvents": "Audio događaji", + "cameraAudioTranscription": "Transkripcija zvuka", + "cameraNotifications": "Obavještenja", + "cameraLivePlayback": "Uživo prikaz", + "cameraBirdseye": "Birdseye", + "cameraFaceRecognition": "Prepoznavanje lica", + "cameraLpr": "Prepoznavanje tablice za registraciju", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "Kamera UI", + "cameraTimestampStyle": "Stil vremenske oznake", + "cameraMqtt": "Kamera MQTT", + "cameraManagement": "Upravljanje", + "cameraReview": "Pregled", + "masksAndZones": "Maska / Zone", + "motionTuner": "Regulator kretanja", + "enrichments": "Poboljšanja", + "users": "Korisnici", + "roles": "Uloge", + "notifications": "Obavještenja", + "triggers": "Pokretači", + "debug": "Debug", + "frigateplus": "Frigate+", + "maintenance": "Održavanje", + "mediaSync": "Sinkronizacija medija", + "regionGrid": "Mreža regija" + }, + "button": { + "overriddenGlobal": "Prekriveno (Globalno)", + "overriddenGlobalTooltip": "Ova kamera prekriva globalne postavke konfiguracije u ovom odjeljku", + "overriddenBaseConfig": "Prekriveno (Bazna konfiguracija)", + "overriddenBaseConfigTooltip": "Profil {{profile}} prekriva postavke konfiguracije u ovom odjeljku", + "overriddenInCameras": { + "label_one": "Nadjačano u {{count}} kameri", + "label_few": "Nadjačano u {{count}} kamere", + "label_other": "Nadjačano u {{count}} kamera", + "tooltip_one": "{{count}} kamera nadjačava vrijednosti u ovom odjeljku. Kliknite za detalje.", + "tooltip_few": "{{count}} kamere nadjačavaju vrijednosti u ovom odjeljku. Kliknite za detalje.", + "tooltip_other": "{{count}} kamera nadjačava vrijednosti u ovom odjeljku. Kliknite za detalje.", + "heading_one": "Ovaj globalni odjeljak ima polja koja su nadjačana u {{count}} kameri.", + "heading_few": "Ovaj globalni odjeljak ima polja koja su nadjačana u {{count}} kamere.", + "heading_other": "Ovaj globalni odjeljak ima polja koja su nadjačana u {{count}} kamera.", + "othersField_one": "{{count}} drugo", + "othersField_few": "{{count}} druga", + "othersField_other": "{{count}} drugih", + "profilePrefix": "{{profile}} profil: {{fields}}" + } + }, + "dialog": { + "unsavedChanges": { + "title": "Imate nečuvane promjene.", + "desc": "Želite li da sačuvate promjene prije nego što nastavite?" + } + }, + "saveAllPreview": { + "title": "Promjene koje treba sačuvati", + "triggerLabel": "Pregledajte čekajuće promjene", + "empty": "Nema čekajućih promjena.", + "scope": { + "label": "Opseg", + "global": "Globalni", + "camera": "Kamera: {{cameraName}}" + }, + "profile": { + "label": "Profil" + }, + "field": { + "label": "Polje" + }, + "value": { + "label": "Nova vrijednost", + "reset": "Resetuj" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Nema kamere" + }, + "general": { + "title": "Postavke korisničkog sučelja", + "liveDashboard": { + "title": "Uživo upravljačko sučelje", + "automaticLiveView": { + "label": "Automatski pregled uživo", + "desc": "Automatski pređite na pregled uživo kamere kada se detektira aktivnost. Onemogućavanje ove opcije uzrokuje da statične slike kamere na upravljačkom sučelju uživo ažuriraju se jednom na minut." + }, + "playAlertVideos": { + "label": "Pregledajte videozapise upozorenja", + "desc": "Zaduženo, nedavna upozorenja na upravljačkom sučelju uživo se prikazuju kao mala petlja videa. Onemogućavanje ove opcije omogućava prikaz samo statične slike nedavnih upozorenja na ovom uređaju/pregledniku." + }, + "displayCameraNames": { + "label": "Uvijek prikaži imena kamere", + "desc": "Uvijek prikaži imena kamere u čipu u pregledu uživo više kamera." + }, + "liveFallbackTimeout": { + "label": "Vrijeme čekanja za povratno prelazak igrača uživo", + "desc": "Kada je visokokvalitetni tok uživo za kameru nedostupan, pređite na način s niskom širinom pojasa nakon ovog broja sekundi. Zadano: 3." + } + }, + "storedLayouts": { + "title": "Pohranjene izgleda", + "desc": "Izgled kamere u grupi kamera može se povući/ponoviti. Položaji se pohranjuju u lokalno skladište vašeg preglednika.", + "clearAll": "Izbriši sve izgleda" + }, + "cameraGroupStreaming": { + "title": "Postavke streaminga za grupu kamera", + "desc": "Postavke streaminga za svaku grupu kamera pohranjene su u lokalno skladište vašeg preglednika.", + "clearAll": "Izbriši sve postavke streaminga" + }, + "recordingsViewer": { + "title": "Pregledač snimaka", + "defaultPlaybackRate": { + "label": "Zadani brzina reprodukcije", + "desc": "Zadani brzina reprodukcije za prikazivanje snimaka." + } + }, + "calendar": { + "title": "Kalendar", + "firstWeekday": { + "label": "Prvi radni dan", + "desc": "Dan na kojem počinju tjedni pregled kalendar.", + "sunday": "Nedjelja", + "monday": "Ponedjeljak" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Izbrisana pohranjena izgled za {{cameraName}}", + "clearStreamingSettings": "Izbrisane postavke streaminga za sve grupe kamera." + }, + "error": { + "clearStoredLayoutFailed": "Nije uspješno obrisano pohranjeno raspoređenje: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nije uspješno obrisano pohranjene postavke streaminga: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Postavke obogaćivanja", + "unsavedChanges": "Nespremljene promjene postavki bogatstva", + "birdClassification": { + "title": "Klasifikacija ptica", + "desc": "Klasifikacija ptica identifikuje poznate ptice pomoću kvantiziranog Tensorflow modela. Kada se prepozna poznata ptica, njezino uobičajeno ime bit će dodato kao sub_label. Ova informacija uključena je u UI, filtere, kao i u obavijesti." + }, + "semanticSearch": { + "title": "Semantička pretraga", + "desc": "Semantička pretraga u Frigate omogućava vam da pronađete praćene objekte unutar vaših stavki za pregled pomoću same slike, korisnički definiranog tekstualnog opisa ili automatski generiranog.", + "reindexNow": { + "label": "Ponovno indeksiraj sada", + "desc": "Ponovno indeksiranje će ponovno generirati ugrađivanja za sve praćene objekte. Ovaj proces se izvršava u pozadini i može iskoristiti punu snagu vašeg CPU-a i može trajati značajno vrijeme ovisno o broju praćenih objekata koje imate.", + "confirmTitle": "Potvrdi ponovno indeksiranje", + "confirmDesc": "Sigurni li ste da želite ponovno indeksirati ugrađivanja svih praćenih objekata? Ovaj proces će se izvršavati u pozadini, ali može iskoristiti punu snagu vašeg CPU-a i može trajati značajno vrijeme. Možete pratiti napredak na stranici Istraživanje.", + "confirmButton": "Ponovno indeksiraj", + "success": "Ponovno indeksiranje je uspješno pokrenuto.", + "alreadyInProgress": "Ponovno indeksiranje već je u toku.", + "error": "Nije uspješno pokrenuto ponovno indeksiranje: {{errorMessage}}" + }, + "modelSize": { + "label": "Veličina modela", + "desc": "Veličina modela korištenog za semantičke pretrage ugrađivanja.", + "small": { + "title": "mali", + "desc": "Korištenje mali koristi kvantiziranu verziju modela koja koristi manje RAM-a i brže se izvršava na CPU-u s vrlo zanemarivim razlikama u kvaliteti ugrađivanja." + }, + "large": { + "title": "veliki", + "desc": "Korištenje veliki koristi puni Jina model i automatski će se izvršavati na GPU-u ako je primjenjivo." + } + } + }, + "faceRecognition": { + "title": "Prepoznavanje lica", + "desc": "Prepoznavanje lica omogućava ljudima da se dodele imena, a kada se prepozna lice, Frigate će dodijeliti ime osobe kao sub_label. Ova informacija uključena je u UI, filtere, kao i u obavijesti.", + "modelSize": { + "label": "Veličina modela", + "desc": "Veličina modela korištenog za prepoznavanje lica.", + "small": { + "title": "mali", + "desc": "Korišćenje mali koristi model za ugradnju lica FaceNet koji učinkovito radi na većini CPU-ova." + }, + "large": { + "title": "veliki", + "desc": "Korišćenje veliki koristi model za ugradnju lica ArcFace i automatski će se pokrenuti na GPU-u ako je primjenjivo." + } + } + }, + "licensePlateRecognition": { + "title": "Prepoznavanje tablice vozila", + "desc": "Frigate može prepoznati registracijske tablice na vozilima i automatski dodati detektirane znakove u polje recognized_license_plate ili poznato ime kao sub_label objektima koji su tipa automobil. Često korišćeni slučaj može biti čitanje registracijskih tablica automobila koji se voze u dvorište ili automobila koji prolaze ulicom." + }, + "restart_required": "Potrebno je ponovno pokretanje (promijenjene postavke bogatstva)", + "toast": { + "success": "Postavke bogatstva su sačuvane. Ponovno pokrenite Frigate da biste primijenili svoje promjene.", + "error": "Nije uspješno sačuvana promjena konfiguracije: {{errorMessage}}" + } + }, + "cameraWizard": { + "title": "Dodaj kameru", + "description": "Slijedite korake ispod da biste dodali novu kameru u svoju instalaciju Frigate.", + "steps": { + "nameAndConnection": "Ime i Povezivanje", + "probeOrSnapshot": "Testiranje ili Snimak", + "streamConfiguration": "Konfiguracija strujanja", + "validationAndTesting": "Validacija i Testiranje" + }, + "save": { + "success": "Uspješno sačuvana nova kamera {{cameraName}}.", + "failure": "Greška prilikom sačuvavanja {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Rezolucija", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Molimo unesite važeću URL adresu za strujanje", + "testFailed": "Test strujanja nije uspio: {{error}}" + }, + "step1": { + "description": "Unesite detalje o svojoj kameri i odaberite testiranje kamere ili ručno odaberite proizvođača.", + "cameraName": "Ime kamere", + "cameraNamePlaceholder": "npr. front_door ili Pregled zadnjeg dvorišta", + "host": "Host/IP adresa", + "port": "Port", + "username": "Korisničko ime", + "usernamePlaceholder": "Opcionalno", + "password": "Lozinka", + "passwordPlaceholder": "Opcionalno", + "selectTransport": "Odaberite protokol prenosa", + "cameraBrand": "Marka kamere", + "selectBrand": "Odaberite marku kamere za URL predložak", + "customUrl": "Prilagođeni URL tokova", + "brandInformation": "Informacije o brendu", + "brandUrlFormat": "Za kamere sa formatom RTSP URL-a kao: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "connectionSettings": "Postavke veze", + "detectionMethod": "Metoda detekcije tokova", + "onvifPort": "Port ONVIF", + "probeMode": "Pregledaj kameru", + "manualMode": "Ručna selekcija", + "detectionMethodDescription": "Pregledaj kameru pomoću ONVIF (ako je podržano) kako bi pronašao URL-ove tokova kamere, ili ručno odaberi brend kamere za korištenje preddefiniranih URL-ova. Za unos prilagođenog RTSP URL-a, odaberi ručni metod i odaberi \"Ostalo\".", + "onvifPortDescription": "Za kamere koje podržavaju ONVIF, ovo je obično 80 ili 8080.", + "useDigestAuth": "Koristi digest autentifikaciju", + "useDigestAuthDescription": "Koristi HTTP digest autentifikaciju za ONVIF. Neke kamere mogu zahtijevati dedikovane korisničko ime/lozinku ONVIF umjesto standardnog korisnika admin.", + "errors": { + "brandOrCustomUrlRequired": "Odaberite brend kamere s host/IP ili odaberite 'Ostalo' s prilagođenim URL-om", + "nameRequired": "Ime kamere je obavezno", + "nameLength": "Ime kamere mora imati 64 znaka ili manje", + "invalidCharacters": "Ime kamere sadrži nevažeće znakove", + "nameExists": "Ime kamere već postoji", + "customUrlRtspRequired": "Prilagođeni URL-ovi moraju početi s \"rtsp://\". Za tokove kamere koji nisu RTSP potrebna je ručna konfiguracija." + } + }, + "step2": { + "description": "Pregledajte kameru za dostupne tokove ili konfigurirajte ručne postavke na temelju odabrane metode detekcije.", + "testSuccess": "Test veze uspješan!", + "testFailed": "Test veze nije uspio. Molimo provjerite svoj unos i pokušajte ponovno.", + "testFailedTitle": "Test nije uspio", + "streamDetails": "Detalji tokova", + "probing": "Pregledavanje kamere...", + "retry": "Pokušaj ponovno", + "testing": { + "probingMetadata": "Pregledavanje metapodataka kamere...", + "fetchingSnapshot": "Učitavanje snimka kamere..." + }, + "probeFailed": "Neuspješno ispitivanje kamere: {{error}}", + "probingDevice": "Ispitivanje uređaja...", + "probeSuccessful": "Uspješno ispitivanje", + "probeError": "Greška ispitivanja", + "probeNoSuccess": "Neuspješno ispitivanje", + "deviceInfo": "Informacije o uređaju", + "manufacturer": "Proizvođač", + "model": "Model", + "firmware": "Firmver", + "profiles": "Profili", + "ptzSupport": "Podrška PTZ", + "autotrackingSupport": "Podrška za autotračenje", + "presets": "Podešavanja", + "rtspCandidates": "RTSP kandidati", + "rtspCandidatesDescription": "Sljedeće RTSP URL-ovi su pronađeni iz ispitivanja kamere. Testirajte vezu za pregled metapodataka o streamu.", + "noRtspCandidates": "Nisu pronađeni RTSP URL-ovi iz kamere. Vaše vjerodajnice mogu biti netočne, ili kamera možda ne podržava ONVIF ili metod za dobivanje RTSP URL-ova. Vrati se i ručno unesi RTSP URL.", + "candidateStreamTitle": "Kandidat {{number}}", + "useCandidate": "Koristi", + "uriCopy": "Kopiraj", + "uriCopied": "URI kopiran na međuspremnik", + "testConnection": "Testiraj vezu", + "toggleUriView": "Kliknite za prebacivanje u puni prikaz URI-ja", + "connected": "Povezani", + "notConnected": "Nije povezan", + "errors": { + "hostRequired": "Potrebna je adresa hosta/IP" + } + }, + "step3": { + "description": "Konfigurirajte uloge streamova i dodajte dodatne streamove za vašu kameru.", + "streamsTitle": "Streamovi kamere", + "addStream": "Dodaj snimak", + "addAnotherStream": "Dodaj još jedan snimak", + "streamTitle": "Snimak {{number}}", + "streamUrl": "URL snimka", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "selectStream": "Odaberite snimak", + "searchCandidates": "Pretražite kandidate...", + "noStreamFound": "Nijedan snimak nije pronađen", + "url": "URL", + "resolution": "Rezolucija", + "selectResolution": "Odaberite rezoluciju", + "quality": "Kvalitet", + "selectQuality": "Odaberite kvalitet", + "roles": "Uloge", + "roleLabels": { + "detect": "Detekcija objekata", + "record": "Snimanje", + "audio": "Audio" + }, + "testStream": "Testirajte vezu", + "testSuccess": "Test snimka uspješan!", + "testFailed": "Test snimka neuspješan", + "testFailedTitle": "Test neuspješan", + "connected": "Povezani", + "notConnected": "Nije povezan", + "featuresTitle": "Funkcije", + "go2rtc": "Smanjite povezivanje s kamerom", + "detectRoleWarning": "Bar jedan snimak mora imati ulogu \"detektirati\" da biste nastavili.", + "rolesPopover": { + "title": "Uloge snimka", + "detect": "Glavni tok za detekciju objekata.", + "record": "Sprema segmente video toka na temelju postavki konfiguracije.", + "audio": "Tok za detekciju na temelju zvuka." + }, + "featuresPopover": { + "title": "Značajke snimka", + "description": "Koristite go2rtc restreaming za smanjenje povezivanja s vašom kamerom." + } + }, + "step4": { + "description": "Konačna validacija i analiza prije spašavanja vaše nove kamere. Povežite svaki tok prije spašavanja.", + "validationTitle": "Validacija toka", + "connectAllStreams": "Poveži sve toke", + "reconnectionSuccess": "Ponovno povezivanje uspješno.", + "reconnectionPartial": "Neki tokovi su neuspješno ponovno povezani.", + "streamUnavailable": "Pregled toka nije dostupan", + "reload": "Ponovno učitaj", + "connecting": "Povezivanje...", + "streamTitle": "Tok {{number}}", + "valid": "Važeći", + "failed": "Neuspješno", + "notTested": "Nije testirano", + "connectStream": "Poveži", + "connectingStream": "Povezivanje", + "disconnectStream": "Odspoji", + "estimatedBandwidth": "Procijenjena širina pojasa", + "roles": "Uloge", + "ffmpegModule": "Koristi režim kompatibilnosti toka", + "ffmpegModuleDescription": "Ako tok ne učita nakon nekoliko pokušaja, pokušajte omogućavanje ovoga. Kada je omogućeno, Frigate će koristiti modul ffmpeg sa go2rtc. Ovo može pružiti bolju kompatibilnost sa nekim tokovima kamera.", + "none": "Nijedan", + "error": "Greška", + "streamValidated": "Tok {{number}} uspješno validiran", + "streamValidationFailed": "Tok {{number}} validacija neuspješna", + "saveAndApply": "Sačuvaj novu kameru", + "saveError": "Neispravna konfiguracija. Molimo provjerite svoje postavke.", + "issues": { + "title": "Validacija toka", + "videoCodecGood": "Video kodak je {{codec}}.", + "audioCodecGood": "Audio kodak je {{codec}}.", + "resolutionHigh": "Rezolucija od {{resolution}} može uzrokovati povećanu upotrebu resursa.", + "resolutionLow": "Rezolucija od {{resolution}} može biti preniska za pouzdanu detekciju malih objekata.", + "resolutionUnknown": "Rezolucija ovog tokova nije moguća za ispitivanje. Trebalo bi ručno postaviti detekciju rezolucije u Postavkama ili vašoj konfiguraciji.", + "noAudioWarning": "Nije detektovan zvuk za ovaj tok, snimci neće imati zvuk.", + "audioCodecRecordError": "Potreban je AAC audio kodac za podršku zvuku u snimcima.", + "audioCodecRequired": "Potreban je audio tok za podržavanje detekcije zvuka.", + "restreamingWarning": "Smanjenje povezivanja sa kamerom za snimljeni tok može lagano povećati upotrebu CPU.", + "brands": { + "reolink-rtsp": "Reolink RTSP nije preporučen. Omogući HTTP u postavkama firmvera kamere i ponovo pokreni čarobnjaka.", + "reolink-http": "Reolink HTTP tokovi trebaju koristiti FFmpeg za bolju kompatibilnost. Omogući 'Korištenje režima kompatibilnosti tokova' za ovaj tok." + }, + "dahua": { + "substreamWarning": "Podstrujak 1 je zaključan na nisku rezoluciju. Mnoge kamere Dahua / Amcrest / EmpireTech podržavaju dodatne podstrujke koje treba omogućiti u postavkama kamere. Preporučuje se da ih provjerite i iskoristite ako su dostupne." + }, + "hikvision": { + "substreamWarning": "Podstrujak 1 je zaključan na nisku rezoluciju. Mnoge kamere Hikvision podržavaju dodatne podstrujke koje treba omogućiti u postavkama kamere. Preporučuje se da ih provjerite i iskoristite ako su dostupne." + } + } + } + }, + "cameraManagement": { + "title": "Upravljanje kamerama", + "addCamera": "Dodaj novu kameru", + "deleteCamera": "Obriši kameru", + "deleteCameraDialog": { + "title": "Obriši kameru", + "description": "Brisanje kamere trajno uklanja sve snimke, praćene objekte i konfiguraciju za tu kameru. Bilo bi potrebno ručno ukloniti sve go2rtc tokove povezane s ovom kamerom.", + "selectPlaceholder": "Izaberite kameru...", + "confirmTitle": "Sigurni ste?", + "confirmWarning": "Brisanje {{cameraName}} ne može se povući.", + "deleteExports": "Takođe obriši izvoze za ovu kameru", + "confirmButton": "Obriši trajno", + "success": "Kamera {{cameraName}} uspješno obrisana", + "error": "Neuspješno brisanje kamere {{cameraName}}" + }, + "editCamera": "Uredi kameru:", + "selectCamera": "Izaberite kameru", + "backToSettings": "Povratak na postavke kamere", + "streams": { + "title": "Omogući / Onemogući kamere", + "enableLabel": "Omogućene kamere", + "enableDesc": "Privremeno onemogući omogućenu kameru dok Frigate ne ponovo započne. Onemogućavanje kamere potpuno zaustavlja obradu tokova ove kamere od strane Frigate. Detekcija, snimanje i praćenje nedostat će.
    Napomena: Ovo ne onemogućava restreamove go2rtc.", + "disableLabel": "Onemogućene kamere", + "disableDesc": "Omogući kameru koja trenutno nije vidljiva u UI-ju i onemogućena u konfiguraciji. Potrebno je ponovno pokrenuti Frigate nakon omogućavanja.", + "enableSuccess": "Omogućena {{cameraName}} u konfiguraciji. Ponovno pokrenite Frigate da biste primijenili promjene.", + "friendlyName": { + "edit": "Uredi prikazano ime kamere", + "title": "Uredi prikazano ime", + "description": "Postavite prijateljsko ime koje će se prikazivati za ovu kameru kroz cijeli Frigate UI. Ostavite prazno da biste koristili ID kamere.", + "rename": "Preimenuj" + } + }, + "cameraConfig": { + "add": "Dodaj kameru", + "edit": "Uredi kameru", + "description": "Konfiguriraj postavke kamere uključujući ulazne tokove i uloge.", + "name": "Ime kamere", + "nameRequired": "Ime kamere je obavezno", + "nameLength": "Ime kamere mora imati manje od 64 znaka.", + "namePlaceholder": "npr. front_door ili Pregled zadnjeg dvorišta", + "enabled": "Omogućeno", + "ffmpeg": { + "inputs": "Ulazni tokovi", + "path": "Putanja toka", + "pathRequired": "Putanja toka je obavezna", + "pathPlaceholder": "rtsp://...", + "roles": "Uloge", + "rolesRequired": "Potrebna je bar jedna uloga", + "rolesUnique": "Svaka uloga (audio, detekcija, snimanje) može se dodijeliti samo jednom toku", + "addInput": "Dodaj ulazni tok", + "removeInput": "Ukloni ulazni tok", + "inputsRequired": "Potrebno je bar jedan ulazni tok" + }, + "go2rtcStreams": "go2rtc Tokovi", + "streamUrls": "URL-ovi tokova", + "addUrl": "Dodaj URL", + "addGo2rtcStream": "Dodaj go2rtc tok", + "toast": { + "success": "Kamera {{cameraName}} je uspješno sačuvana" + } + }, + "profiles": { + "title": "Prekrižavanja kamere profila", + "selectLabel": "Odaberi profil", + "description": "Konfigurirajte koje kamere su omogućene ili onemogućene kada je profil aktiviran. Kamere postavljene na \"Naslijeđivanje\" očuvaju svoje osnovno stanje omogućeno.", + "inherit": "Naslijeđivanje", + "enabled": "Omogućeno", + "disabled": "Onemogućeno" + }, + "cameraType": { + "title": "Tip kamere", + "label": "Tip kamere", + "description": "Postavite tip za svaku kameru. Posvećene LPR kamere su jednonamjenske kamere sa snažnim optičkim zumom za hvatanje registarskih tablica na udaljenim vozilima. Većina kamera treba koristiti normalan tip kamere, osim ako je kamera namjenski za LPR i ima usko fokusiran pogled na registarske tablice.", + "normal": "Normalna", + "dedicatedLpr": "Posvećena LPR", + "saveSuccess": "Tip kamere ažuriran za {{cameraName}}. Ponovo pokrenite Frigate da bi se promjene primijenile." + } + }, + "cameraReview": { + "title": "Postavke pregleda kamere", + "object_descriptions": { + "title": "Opisi objekata generativne AI", + "desc": "Privremeno omogući/onemogući opise objekata generativne AI za ovu kameru dok Frigate ne ponovo pokrene. Kada je onemogućeno, opisi generirani AI-om neće se tražiti za praćene objekte na ovoj kameri." + }, + "review_descriptions": { + "title": "Opisi pregleda generativne AI", + "desc": "Privremeno omogući/onemogući opise pregleda generativne AI za ovu kameru dok Frigate ne ponovo pokrene. Kada je onemogućeno, opisi generirani AI-om neće se tražiti za stavke pregleda na ovoj kameri." + }, + "review": { + "title": "Pregled", + "desc": "Privremeno omogući/onemogući upozorenja i detekcije za ovu kameru dok Frigate ne ponovo pokrene. Kada je onemogućeno, neće se generirati nove stavke pregleda. ", + "alerts": "Upozorenja ", + "detections": "Detekcije " + }, + "reviewClassification": { + "title": "Klasifikacija pregleda", + "desc": "Frigate klasificira stavke pregleda kao Upozorenja i Detekcije. Po defaultu, svi osobe i automobili objekti se smatraju Upozorenjima. Možete usavršiti klasifikaciju svojih stavki pregleda konfiguriranjem potrebnih zona za njih.", + "noDefinedZones": "Nema definiranih zona za ovu kameru.", + "objectAlertsTips": "Svi {{alertsLabels}} objekti na {{cameraName}} bit će prikazani kao Upozorenja.", + "zoneObjectAlertsTips": "Svi {{alertsLabels}} objekti detektirani u {{zone}} na {{cameraName}} bit će prikazani kao Upozorenja.", + "objectDetectionsTips": "Svi {{detectionsLabels}} objekti koji nisu kategorizirani na {{cameraName}} bit će prikazani kao Detekcije bez obzira na kojoj se zoni nalaze.", + "zoneObjectDetectionsTips": { + "text": "Svi {{detectionsLabels}} objekti koji nisu kategorizirani u {{zone}} na {{cameraName}} bit će prikazani kao Detekcije.", + "notSelectDetections": "Svi {{detectionsLabels}} objekti detektirani u {{zone}} na {{cameraName}} koji nisu kategorizirani kao Upozorenja bit će prikazani kao Detekcije bez obzira na kojoj se zoni nalaze.", + "regardlessOfZoneObjectDetectionsTips": "Svi {{detectionsLabels}} objekti koji nisu kategorizirani na {{cameraName}} bit će prikazani kao Detekcije bez obzira na koju zonu se nalaze." + }, + "unsavedChanges": "Nespremljene postavke klasifikacije pregleda za {{camera}}", + "selectAlertsZones": "Odaberite zone za Upozorenja", + "selectDetectionsZones": "Odaberite zone za Detekcije", + "limitDetections": "Ograničite detekcije na specifične zone", + "toast": { + "success": "Konfiguracija klasifikacije pregleda je sačuvana. Ponovo pokrenite Frigate da biste primijenili promjene." + } + } + }, + "masksAndZones": { + "filter": { + "all": "Svi Maski i Zone" + }, + "restart_required": "Potrebno je ponovo pokrenuti (maska/zone promijenjene)", + "disabledInConfig": "Stavka je onemogućena u datoteci konfiguracije", + "addDisabledProfile": "Prvo dodajte u osnovnu konfiguraciju, zatim prekrijte u profilu", + "profileBase": "(osnovna)", + "profileOverride": "(preklop)", + "toast": { + "success": { + "copyCoordinates": "Koordinate za {{polyName}} su kopirane u međuspremnik." + }, + "error": { + "copyCoordinatesFailed": "Nemoguće kopirati koordinate u međuspremnik." + } + }, + "motionMaskLabel": "Maska za pokret {{number}}", + "objectMaskLabel": "Maska za objekt {{number}}", + "form": { + "id": { + "error": { + "mustNotBeEmpty": "ID ne smije biti prazan.", + "alreadyExists": "Postoji maska s ovim ID-om za ovu kameru." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Ime ne smije biti prazno." + } + }, + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Ime zone mora imati najmanje 2 karaktera.", + "mustNotBeSameWithCamera": "Ime zone ne smije biti isto kao ime kamere.", + "alreadyExists": "Postoji zona s ovim imenom za ovu kameru.", + "mustNotContainPeriod": "Ime zone ne smije sadržavati tačke.", + "hasIllegalCharacter": "Ime zone sadrži nedozvoljene znakove.", + "mustHaveAtLeastOneLetter": "Ime zone mora imati bar jedan slovo." + } + }, + "distance": { + "error": { + "text": "Rastojanje mora biti veće ili jednako 0.1.", + "mustBeFilled": "Sva polja za rastojanje moraju biti popunjena da biste koristili procjenu brzine." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Inercija mora biti iznad 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Vrijeme loiteringa mora biti veće ili jednako 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Prag brzine mora biti veći ili jednak 0.1." + } + }, + "polygonDrawing": { + "type": { + "zone": "Zona", + "motion_mask": "maska pokreta", + "object_mask": "maska objekta" + }, + "removeLastPoint": "Ukloni posljednju tačku", + "reset": { + "label": "Obriši sve tačke" + }, + "snapPoints": { + "true": "Prilagodi tačke", + "false": "Ne prilagođavaj tačke" + }, + "delete": { + "title": "Potvrdi brisanje", + "desc": "Da li ste sigurni da želite izbrisati {{type}} {{name}}?", + "success": "{{name}} je izbrisan." + }, + "revertOverride": { + "title": "Povrati se na osnovnu konfiguraciju", + "desc": "Ovo će ukloniti preklop profila za {{type}} {{name}} i povrati se na osnovnu konfiguraciju." + }, + "error": { + "mustBeFinished": "Crtanje poligona mora se završiti prije spašavanja." + } + } + }, + "zones": { + "label": "Zone", + "documentTitle": "Uredi zonu - Frigate", + "desc": { + "title": "Zona omogućava da definirate specifičnu područje okvira da biste odredili je li objekt unutar određenog područja.", + "documentation": "Dokumentacija" + }, + "add": "Dodaj zonu", + "edit": "Uredi zonu", + "point_one": "{{count}} tačka", + "point_few": "{{count}} tačke", + "point_other": "{{count}} tačke", + "clickDrawPolygon": "Kliknite da nacrtate poligon na slici.", + "name": { + "title": "Ime", + "inputPlaceHolder": "Unesite ime…", + "tips": "Ime mora imati najmanje 2 karaktera, mora imati bar jedan slovo, i ne smije biti ime kamere ili druge zone na ovoj kameri." + }, + "enabled": { + "title": "Omogućeno", + "description": "Da li je ova zona aktivna i omogućena u konfiguracionoj datoteci. Ako je onemogućena, ne može se omogućiti putem MQTT. Onemogućene zone se zanemaruju u vremenu izvršavanja." + }, + "inertia": { + "title": "Inercija", + "desc": "Određuje koliko okvira mora objekt biti u zoni prije nego što se uzima u obzir u zoni. Podrazumevano: 3" + }, + "loiteringTime": { + "title": "Vrijeme loiteringa", + "desc": "Postavlja minimalno vrijeme u sekundama koje objekt mora biti u zoni da bi aktivirao. Zadano: 0" + }, + "objects": { + "title": "Objekti", + "desc": "Popis objekata koji se odnose na ovu zonu." + }, + "allObjects": "Svi objekti", + "speedEstimation": { + "title": "Procjena brzine", + "desc": "Omogući procjenu brzine za objekte u ovoj zoni. Zona mora imati točno 4 točke.", + "lineADistance": "Udaljenost linije A ({{unit}})", + "lineBDistance": "Udaljenost linije B ({{unit}})", + "lineCDistance": "Udaljenost linije C ({{unit}})", + "lineDDistance": "Udaljenost linije D ({{unit}})" + }, + "speedThreshold": { + "title": "Prag brzine ({{unit}})", + "desc": "Određuje minimalnu brzinu za objekte da bi se smatrali u ovoj zoni.", + "toast": { + "error": { + "pointLengthError": "Procjena brzine je onemogućena za ovu zonu. Zone s procjenom brzine moraju imati točno 4 točke.", + "loiteringTimeError": "Zone s vremenima trajanja većim od 0 ne bi trebale se koristiti s procjenom brzine." + } + } + }, + "toast": { + "success": "Zona ({{zoneName}}) je sačuvana." + } + }, + "motionMasks": { + "label": "Maska pokreta", + "documentTitle": "Uredi masku pokreta - Frigate", + "desc": { + "title": "Maska pokreta koristi se za spriječavanje neželjenih vrsta pokreta da aktiviraju detekciju. Prekrižavanje će napraviti teško praćenje objekata.", + "documentation": "Dokumentacija" + }, + "add": "Nova maska pokreta", + "edit": "Uredi masku pokreta", + "defaultName": "Maska pokreta {{number}}", + "context": { + "title": "Maska pokreta koristi se za spriječavanje neželjenih vrsta pokreta da aktiviraju detekciju (primjer: grančice stabala, vremenske oznake kamere). Maska pokreta treba se koristiti vrlo retko, prekrižavanje će napraviti teško praćenje objekata." + }, + "point_one": "{{count}} tačka", + "point_few": "{{count}} tačke", + "point_other": "{{count}} tačke", + "clickDrawPolygon": "Kliknite da nacrtate poligon na slici.", + "name": { + "title": "Ime", + "description": "Nepovlačni prijateljski naziv za ovu masku pokreta.", + "placeholder": "Unesite ime..." + }, + "polygonAreaTooLarge": { + "title": "Maska pokreta pokriva {{polygonArea}}% okvirnog slike kamere. Velike maske pokreta nisu preporučene.", + "tips": "Maska za pokret ne sprječava detekciju objekata. Umjesto toga, trebalo bi koristiti obaveznu zonu." + }, + "toast": { + "success": { + "title": "{{polygonName}} je sačuvan.", + "noName": "Maska za pokret je sačuvana." + } + } + }, + "objectMasks": { + "label": "Maska za objekte", + "documentTitle": "Uredi masku za objekte - Frigate", + "desc": { + "title": "Maska za filtriranje objekata koristi se za uklanjanje lažnih pozitivnih rezultata za određeni tip objekta na temelju lokacije.", + "documentation": "Dokumentacija" + }, + "add": "Dodaj masku za objekte", + "edit": "Uredi masku za objekte", + "context": "Maska za filtriranje objekata koristi se za uklanjanje lažnih pozitivnih rezultata za određeni tip objekta na temelju lokacije.", + "point_one": "{{count}} tačka", + "point_few": "{{count}} tačke", + "point_other": "{{count}} tačke", + "clickDrawPolygon": "Kliknite da nacrtate poligon na slici.", + "name": { + "title": "Ime", + "description": "Nepovlaženo prijateljivo ime za ovu masku za objekte.", + "placeholder": "Unesite ime..." + }, + "objects": { + "title": "Objekti", + "desc": "Tip objekta koji se odnosi na ovu masku za objekte.", + "allObjectTypes": "Svi tipovi objekata" + }, + "toast": { + "success": { + "title": "{{polygonName}} je sačuvan.", + "noName": "Maska za objekte je sačuvana." + } + } + }, + "masks": { + "enabled": { + "title": "Omogućeno", + "description": "Da li je ova maska omogućena u konfiguracijskoj datoteci. Ako je onemogućena, ne može se omogućiti putem MQTT. Onemogućene maske se zanemaruju tijekom izvršavanja." + } + } + }, + "motionDetectionTuner": { + "title": "Podešavač detekcije pokreta", + "unsavedChanges": "Nespremljene promjene podešavača detekcije pokreta ({{camera}})", + "desc": { + "title": "Frigate koristi detekciju pokreta kao prvi korak provjere da li se nešto događa u okviru vrijedno provjere pomoću detekcije objekata.", + "documentation": "Pročitajte vodič za podešavanje detekcije pokreta" + }, + "Threshold": { + "title": "Prag", + "desc": "Vrijednost pragodiktira koliko promjene u svjetlosnosti piksela je potrebno za razmatranje kao pokret. Default: 30" + }, + "contourArea": { + "title": "Površina kontura", + "desc": "Vrijednost površine kontura koristi se za odluku koja skupine promijenjenih piksela kvalificiraju kao pokret. Default: 10" + }, + "improveContrast": { + "title": "Poboljšaj kontrast", + "desc": "Poboljšaj kontrast za tamnije scene. Zadano: UKLJUČENO" + }, + "toast": { + "success": "Postavke pokreta su sačuvane." + } + }, + "debug": { + "title": "Uklanjanje grešaka", + "detectorDesc": "Frigate koristi vaše detektore ({{detectors}}) za detekciju objekata u vašem video toku kamere.", + "desc": "Pregled u režimu uklanjanja grešaka prikazuje stvarni pregled praćenih objekata i njihovih statistika. Lista objekata prikazuje zakasnjeni pregled detektovanih objekata.", + "openCameraWebUI": "Otvori Web UI {{camera}}", + "debugging": "Uklanjanje grešaka", + "objectList": "Popis objekata", + "noObjects": "Nema objekata", + "audio": { + "title": "Audio", + "noAudioDetections": "Nema detekcija zvuka", + "score": "poena", + "currentRMS": "Trenutni RMS", + "currentdbFS": "Trenutni dbFS" + }, + "boundingBoxes": { + "title": "Okvirne kutije", + "desc": "Prikaži okvirne kutije oko praćenih objekata", + "colors": { + "label": "Boje okvirnih kutija objekata", + "info": "
  • Na početku, različite boje će biti dodijeljene svakom oznaci objekta
  • Tanjira crna linija označava da objekt nije detektovan u ovom trenutku
  • Tanjira siva linija označava da objekt detektovan kao stacionaran
  • Deblja linija označava da je objekt subjekt automatskog praćenja (kada je omogućeno)
  • " + } + }, + "timestamp": { + "title": "Vremenski pečat", + "desc": "Prikazati vremenski pečat na slici" + }, + "zones": { + "title": "Zone", + "desc": "Prikaži konturu definisanih zona" + }, + "mask": { + "title": "Maska za pokret", + "desc": "Prikaži poligone maski za pokret" + }, + "motion": { + "title": "Kutije za pokret", + "desc": "Prikaži kutije oko područja gdje je detektovan pokret", + "tips": "

    Kutije za pokret


    Crvene kutije će biti prikazane na područjima okvira gdje se trenutno detektuje pokret

    " + }, + "regions": { + "title": "Regije", + "desc": "Prikaži kutiju područja interesa poslatog objektu detektora", + "tips": "

    Kutije regija


    Sjajno zelene kutije bit će preklopljene na područjima zanimanja u okviru koji se šalju detektoru objekata.

    " + }, + "paths": { + "title": "Putanje", + "desc": "Prikaži značajne točke putanje praćenog objekta", + "tips": "

    Putanje


    Linije i krugovi će pokazati značajne točke koje je praćeni objekt prešao tokom svojeg života.

    " + }, + "objectShapeFilterDrawing": { + "title": "Crtanje filtera oblika objekta", + "desc": "Nacrtaj pravokutnik na slici da bi pogledao detalje površine i omjera", + "tips": "Omogući ovu opciju da nacrtate pravokutnik na slici kamere da biste prikazali njegovu površinu i omjer. Ove vrijednosti zatim mogu se koristiti za postavljanje parametara filtera oblika objekta u vašoj konfiguraciji.", + "score": "Rezultat", + "ratio": "Omjer", + "area": "Površina" + } + }, + "timestampPosition": { + "tl": "Gornji lijevo", + "tr": "Gornji desno", + "bl": "Donji lijevo", + "br": "Donji desno" + }, + "users": { + "title": "Korisnici", + "management": { + "title": "Upravljanje korisnicima", + "desc": "Upravljajte računima korisnika ove instance Frigate." + }, + "addUser": "Dodaj korisnika", + "updatePassword": "Ponovno postavi lozinku", + "toast": { + "success": { + "createUser": "Korisnik {{user}} uspješno stvoren", + "deleteUser": "Korisnik {{user}} uspješno obrisan", + "updatePassword": "Lozinka uspješno ažurirana.", + "roleUpdated": "Uloga ažurirana za {{user}}" + }, + "error": { + "setPasswordFailed": "Neuspješno spremanje lozinke: {{errorMessage}}", + "createUserFailed": "Neuspješno stvaranje korisnika: {{errorMessage}}", + "deleteUserFailed": "Neuspješno brisanje korisnika: {{errorMessage}}", + "roleUpdateFailed": "Neuspješno ažuriranje uloge: {{errorMessage}}" + } + }, + "table": { + "username": "Korisničko ime", + "actions": "Akcije", + "role": "Uloga", + "noUsers": "Nema pronađenih korisnika.", + "changeRole": "Promijeni ulogu korisnika", + "password": "Ponovno postavi lozinku", + "deleteUser": "Obriši korisnika" + }, + "dialog": { + "form": { + "user": { + "title": "Korisničko ime", + "desc": "Dozvoljeno su samo slova, brojevi, tačke i donje crte.", + "placeholder": "Unesite korisničko ime" + }, + "password": { + "title": "Lozinka", + "placeholder": "Unesite lozinku", + "show": "Prikaži lozinku", + "hide": "Sakrij lozinku", + "confirm": { + "title": "Potvrdite lozinku", + "placeholder": "Potvrdite lozinku" + }, + "strength": { + "title": "Jakoća lozinke: ", + "weak": "Slaba", + "medium": "Srednja", + "strong": "Jaka", + "veryStrong": "Veoma jaka" + }, + "requirements": { + "title": "Zahtjevi za lozinku:", + "length": "Bar 12 karaktera" + }, + "match": "Lozinke se poklapaju", + "notMatch": "Lozinke se ne poklapaju" + }, + "newPassword": { + "title": "Nova lozinka", + "placeholder": "Unesite novu lozinku", + "confirm": { + "placeholder": "Ponovite novu lozinku" + } + }, + "currentPassword": { + "title": "Trenutna lozinka", + "placeholder": "Unesite svoju trenutnu lozinku" + }, + "usernameIsRequired": "Korisničko ime je obavezno", + "passwordIsRequired": "Lozinka je obavezna" + }, + "createUser": { + "title": "Kreirajte novog korisnika", + "desc": "Dodajte novi korisnički račun i odredite ulogu za pristup područjima sučelja Frigate.", + "usernameOnlyInclude": "Korisničko ime može sadržavati samo slova, brojeve, . ili _", + "confirmPassword": "Molimo potvrdite svoju lozinku" + }, + "deleteUser": { + "title": "Obriši korisnika", + "desc": "Ova akcija ne može se poništiti. Ovo će trajno izbrisati korisnički račun i ukloniti sve povezane podatke.", + "warn": "Sigurni ste da želite izbrisati {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "Lozinka ne može biti prazna", + "doNotMatch": "Lozinke se ne podudaraju", + "currentPasswordRequired": "Trenutna lozinka je obavezna", + "incorrectCurrentPassword": "Trenutna lozinka je netočna", + "passwordVerificationFailed": "Neuspješno provjeravanje lozinke", + "updatePassword": "Ažurirajte lozinku za {{username}}", + "setPassword": "Postavi lozinku", + "desc": "Napravite jaku lozinku za sigurnost ovog računa.", + "multiDeviceWarning": "Bilo koje druge uređaje na kojima ste prijavljeni bit će potrebno ponovno se prijaviti unutar {{refresh_time}}.", + "multiDeviceAdmin": "Takođe možete obavezati sve korisnike da se odmah ponovno autentificiraju rotiranjem vaše tajne JWT." + }, + "changeRole": { + "title": "Promijenite ulogu korisnika", + "select": "Odaberite ulogu", + "desc": "Ažurirajte dozvole za {{username}}", + "roleInfo": { + "intro": "Odaberite odgovarajuću ulogu za ovog korisnika:", + "admin": "Administrator", + "adminDesc": "Pun pristup svim funkcijama.", + "viewer": "Pregledač", + "viewerDesc": "Ograničeno na Uživo tablo, pregled, istraživanje i izvoze.", + "customDesc": "Prilagođena uloga s određenim pristupom kamerama." + } + } + } + }, + "roles": { + "management": { + "title": "Upravljanje ulogama gledatelja", + "desc": "Upravljajte prilagođenim ulogama gledatelja i njihovim dozvolama za pristup kamerama za ovu instancu Frigate." + }, + "addRole": "Dodaj ulogu", + "table": { + "role": "Uloga", + "cameras": "Kamere", + "actions": "Akcije", + "noRoles": "Nisu pronađene prilagođene uloge.", + "editCameras": "Uredi Kamere", + "deleteRole": "Obriši ulogu" + }, + "toast": { + "success": { + "createRole": "Uloga {{role}} uspješno stvorena", + "updateCameras": "Kamere ažurirane za ulogu {{role}}", + "deleteRole": "Uloga {{role}} uspješno obrisana", + "userRolesUpdated_one": "{{count}} korisnik dodeljen ovoj ulogi je ažuriran na 'viewer', koji ima pristup svim kamerama.", + "userRolesUpdated_few": "{{count}} korisnici dodeljeni ovoj ulogi su ažurirani na 'viewer', koji ima pristup svim kamerama.", + "userRolesUpdated_other": "{{count}} korisnici dodeljeni ovoj ulogi su ažurirani na 'viewer', koji ima pristup svim kamerama." + }, + "error": { + "createRoleFailed": "Neuspješno stvaranje uloge: {{errorMessage}}", + "updateCamerasFailed": "Neuspješno ažuriranje kamera: {{errorMessage}}", + "deleteRoleFailed": "Neuspješno brisanje uloge: {{errorMessage}}", + "userUpdateFailed": "Neuspješno ažuriranje uloga korisnika: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Stvori novu ulogu", + "desc": "Dodaj novu ulogu i specifično odredi dozvole za pristup kamerama." + }, + "editCameras": { + "title": "Uredi kamere uloge", + "desc": "Ažuriraj pristup kamerama za ulogu {{role}}." + }, + "deleteRole": { + "title": "Obriši ulogu", + "desc": "Ova akcija ne može biti poništena. Ovo će trajno izbrisati ulogu i dodeliti sve korisnike s ovom ulogom ulogi 'viewer', što će im dati pristup svim kamerama.", + "warn": "Da li ste sigurni da želite izbrisati {{role}}?", + "deleting": "Brisanje..." + }, + "form": { + "role": { + "title": "Ime uloge", + "placeholder": "Unesite ime uloge", + "desc": "Dozvoljeno su samo slova, brojevi, tačke i donje crte.", + "roleIsRequired": "Ime uloge je obavezno", + "roleOnlyInclude": "Ime uloge može sadržavati samo slova, brojeve, . ili _", + "roleExists": "Uloga s ovim imenom već postoji." + }, + "cameras": { + "title": "Kamere", + "desc": "Odaberite kamere kojima ova uloga ima pristup. Potreban je bar jedan pristup.", + "required": "Mora biti odabrana bar jedna kamera." + } + } + } + }, + "notification": { + "title": "Obavještenja", + "notificationSettings": { + "title": "Postavke obavijesti", + "desc": "Frigate može nativno slati obavijesti na vaš uređaj kada radi u pregledaču ili je instalirana kao PWA." + }, + "notificationUnavailable": { + "title": "Obavijesti nedostupne", + "desc": "Web obavijesti zahtijevaju sigurni kontekst (https://…). Ovo je ograničenje pregledača. Pristupite Frigate sigurno da biste koristili obavijesti." + }, + "globalSettings": { + "title": "Globalne postavke", + "desc": "Privremeno zaustavi obavijesti za određene kamere na svim registrovanim uređajima." + }, + "email": { + "title": "E-mail", + "placeholder": "npr. example@email.com", + "desc": "Potrebna je važeća e-mail adresa i koristit će se za obavijestavanje ako dođe do problema sa uslugom slanja obavijesti." + }, + "cameras": { + "title": "Kamere", + "noCameras": "Nema dostupnih kamera", + "desc": "Odaberite koje kamere omogućiti za obavijesti." + }, + "deviceSpecific": "Postavke specifične za uređaj", + "registerDevice": "Registrujte ovaj uređaj", + "unregisterDevice": "Deregistrujte ovaj uređaj", + "sendTestNotification": "Pošaljite test obavijest", + "unsavedRegistrations": "Nečuvane registracije obavijesti", + "unsavedChanges": "Nečuvane promjene obavijesti", + "active": "Obavijesti aktivne", + "suspended": "Obavijesti zaustavljene {{time}}", + "suspendTime": { + "suspend": "Zaustavi", + "5minutes": "Zaustavi za 5 minuta", + "10minutes": "Zaustavi za 10 minuta", + "30minutes": "Zaustavi za 30 minuta", + "1hour": "Zaustavi za 1 sat", + "12hours": "Zaustavi za 12 sati", + "24hours": "Odložiti za 24 sata", + "untilRestart": "Odložiti do ponovnog pokretanja" + }, + "cancelSuspension": "Otkaži odloženje", + "toast": { + "success": { + "registered": "Uspješno registrovan za obaveštenja. Potrebno je ponovno pokrenuti Frigate prije nego što se mogu slati obaveštenja (uključujući test obaveštenje).", + "settingSaved": "Postavke obaveštenja su sačuvane." + }, + "error": { + "registerFailed": "Neuspješno sačuvana registracija obaveštenja." + } + } + }, + "frigatePlus": { + "title": "Postavke Frigate+", + "description": "Frigate+ je usluga pretplate koja pruža pristup dodatnim funkcijama i mogućnostima za vašu instancu Frigate, uključujući mogućnost korištenja prilagođenih modela detekcije objekata treniranih na vašim podacima. Ovdje možete upravljati postavkama modela Frigate+.", + "cardTitles": { + "api": "API", + "currentModel": "Trenutni model", + "otherModels": "Drugi modeli", + "configuration": "Konfiguracija" + }, + "apiKey": { + "title": "Frigate+ API ključ", + "validated": "Frigate+ API ključ je detektovan i validiran", + "notValidated": "Frigate+ API ključ nije detektovan ili nije validiran", + "desc": "Frigate+ API ključ omogućava integraciju sa uslugom Frigate+.", + "plusLink": "Pročitajte više o Frigate+" + }, + "snapshotConfig": { + "title": "Konfiguracija snimaka", + "desc": "Slanje na Frigate+ zahtijeva da su snimci omogućeni u vašoj konfiguraciji.", + "cleanCopyWarning": "Neki uređaji imaju isključene snimke", + "table": { + "camera": "Kamera", + "snapshots": "Snimci" + } + }, + "modelInfo": { + "title": "Informacije o modelu", + "modelType": "Tip modela", + "trainDate": "Datum treniranja", + "baseModel": "Osnovni model", + "plusModelType": { + "baseModel": "Osnovni model", + "userModel": "Podeseno" + }, + "supportedDetectors": "Podržani detektori", + "cameras": "Kamere", + "loading": "Učitavanje informacija o modelu…", + "error": "Neuspješno učitavanje informacija o modelu", + "availableModels": "Dostupni modeli", + "loadingAvailableModels": "Učitavanje dostupnih modela…", + "modelSelect": "Vaši dostupni modeli na Frigate+ mogu se odabrati ovdje. Napomena: samo modeli kompatibilni s vašom trenutnom konfiguracijom detektora mogu se odabrati." + }, + "unsavedChanges": "Nespremljene promjene postavki Frigate+", + "restart_required": "Potrebno je ponovno pokretanje (model Frigate+ promijenjen)", + "toast": { + "success": "Postavke Frigate+ su spremljene. Ponovno pokrenite Frigate da biste primijenili promjene.", + "error": "Nije uspješno sačuvana promjena konfiguracije: {{errorMessage}}" + } + }, + "detectionModel": { + "plusActive": { + "title": "Upravljanje modelima Frigate+", + "label": "Trenutni izvor modela", + "description": "Ova instanca pokreće model Frigate+. Odaberite ili promijenite svoj model u postavkama Frigate+.", + "goToFrigatePlus": "Idi na postavke Frigate+", + "showModelForm": "Ručno konfigurirajte model" + } + }, + "triggers": { + "documentTitle": "Pokretači", + "semanticSearch": { + "title": "Semantička pretraga je onemogućena", + "desc": "Semantička pretraga mora biti omogućena da biste koristili izazivače." + }, + "management": { + "title": "Pokretači", + "desc": "Upravljanje izazivačima za {{camera}}. Korištenjem tipa prikaznog slika, izazivači se mogu aktivirati za slične prikazne slike odabranom praćenom objektu, a tipom opisa za slične opise teksta koji navodite." + }, + "addTrigger": "Dodaj izazivač", + "table": { + "name": "Ime", + "type": "Tip", + "content": "Sadržaj", + "threshold": "Prag", + "actions": "Akcije", + "noTriggers": "Nema konfiguriranih izazivača za ovu kameru.", + "edit": "Uredi", + "deleteTrigger": "Obriši izazivač", + "lastTriggered": "Zadnji put izazvan" + }, + "type": { + "thumbnail": "Minijatura", + "description": "Opis" + }, + "actions": { + "notification": "Pošalji obavijest", + "sub_label": "Dodaj podnaziv", + "attribute": "Dodaj atribut" + }, + "dialog": { + "createTrigger": { + "title": "Kreiraj izazov", + "desc": "Kreiraj izazov za kameru {{camera}}" + }, + "editTrigger": { + "title": "Uredi izazov", + "desc": "Uredi postavke za izazov na kameri {{camera}}" + }, + "deleteTrigger": { + "title": "Obriši izazov", + "desc": "Da li ste sigurni da želite obrisati izazov {{triggerName}}? Ova akcija ne može biti poništena." + }, + "form": { + "name": { + "title": "Ime", + "placeholder": "Daj ime ovom izazovu", + "description": "Unesite jedinstveno ime ili opis da biste identifikovali ovaj izazov", + "error": { + "minLength": "Polje mora imati najmanje 2 karaktera.", + "invalidCharacters": "Polje može sadržavati samo slova, brojeve, donje crte i crte.", + "alreadyExists": "Izazov sa ovim imenom već postoji za ovu kameru." + } + }, + "enabled": { + "description": "Omogući ili onemogući ovaj izazov" + }, + "type": { + "title": "Tip", + "placeholder": "Odaberite vrstu izazova", + "description": "Izazov kada se detektuje opis sličnog praćenog objekta", + "thumbnail": "Izazov kada se detektuje minijaturna slika sličnog praćenog objekta" + }, + "content": { + "title": "Sadržaj", + "imagePlaceholder": "Odaberite minijaturnu sliku", + "textPlaceholder": "Unesite tekstualni sadržaj", + "imageDesc": "Prikazivaju se samo najnovije 100 minijaturnih slika. Ako ne možete pronaći željenu minijaturnu sliku, pregledajte ranije objekte u Pretraživanju i postavite izazov iz menija tamo.", + "textDesc": "Unesite tekst za izazivanje ove akcije kada se detektuje opis sličnog praćenog objekta.", + "error": { + "required": "Sadržaj je obavezan." + } + }, + "threshold": { + "title": "Prag", + "desc": "Postavite prag sličnosti za ovaj izazov. Viši prag znači da je potrebno bliže podudaranje da bi se izazov aktivirao.", + "error": { + "min": "Prag mora biti bar 0", + "max": "Prag mora biti najviše 1" + } + }, + "actions": { + "title": "Akcije", + "desc": "Po defaultu, Frigate šalje poruku MQTT za sve izazovnike. Podnošnici dodaju ime izazovnog događaja u oznaku objekta. Atributi su pretraživi metapodaci pohranjeni zasebno u metapodacima praćenih objekata.", + "error": { + "min": "Mora se odabrati bar jedna akcija." + } + } + } + }, + "wizard": { + "title": "Kreiraj izazov", + "step1": { + "description": "Konfiguriraj osnovne postavke za tvoj izazov." + }, + "step2": { + "description": "Postavi sadržaj koji će izazvati ovu akciju." + }, + "step3": { + "description": "Konfiguriraj prag i akcije za ovaj izazov." + }, + "steps": { + "nameAndType": "Ime i Tip", + "configureData": "Konfiguriraj podatke", + "thresholdAndActions": "Prag i Akcije" + } + }, + "toast": { + "success": { + "createTrigger": "Izazov {{name}} uspješno kreiran.", + "updateTrigger": "Izazov {{name}} uspješno ažuriran.", + "deleteTrigger": "Izazov {{name}} uspješno obrisan." + }, + "error": { + "createTriggerFailed": "Neuspješno kreiranje izazova: {{errorMessage}}", + "updateTriggerFailed": "Neuspješno ažuriranje izazova: {{errorMessage}}", + "deleteTriggerFailed": "Neuspješno brisanje izazova: {{errorMessage}}" + } + } + }, + "maintenance": { + "title": "Održavanje", + "sync": { + "title": "Sinkronizacija medija", + "desc": "Frigate će periodično čistiti medije prema regularnom rasporedu u skladu s vašom konfiguracijom retencije. Normalno je da se vidi nekoliko orfaniranih datoteka dok Frigate radi. Koristite ovu funkciju za uklanjanje orfaniranih datoteka medija s diska koje više nisu referencirane u bazi podataka.", + "started": "Sinkronizacija započeta.", + "alreadyRunning": "Postoji već pokrenuta poslovna jedinica", + "error": "Neuspješno pokretanje sinkronizacije", + "currentStatus": "Status", + "jobId": "ID posla", + "startTime": "Vrijeme početka", + "endTime": "Vrijeme kraja", + "statusLabel": "Status", + "results": "Rezultati", + "errorLabel": "Greška", + "mediaTypes": "Tipovi medija", + "allMedia": "Svi mediji", + "dryRun": "Sušenje", + "dryRunEnabled": "Nijedna datoteka neće biti obrisana", + "dryRunDisabled": "Datoteke će biti obrisane", + "force": "Silovito", + "forceDesc": "Preskočiti prag sigurnosti i završiti sinkronizaciju čak i ako bi više od 50% datoteka bilo obrisano.", + "verbose": "Detaljan", + "verboseDesc": "Napisati pun popis siročića na disk za pregled.", + "running": "Sinkronizacija u toku...", + "start": "Pokreni sinkronizaciju", + "inProgress": "Sinkronizacija je u toku. Ova stranica je onemogućena.", + "status": { + "queued": "U redu", + "running": "Pokretanje", + "completed": "Završeno", + "failed": "Neuspešno", + "notRunning": "Nije u toku" + }, + "resultsFields": { + "filesChecked": "Provjerene datoteke", + "orphansFound": "Nađeni siročići", + "orphansDeleted": "Obrisani siročići", + "aborted": "Prekinuto. Brisanje bi premašilo prag sigurnosti.", + "error": "Greška", + "totals": "Ukupno" + }, + "event_snapshots": "Snimci praćenih objekata", + "event_thumbnails": "Minijature praćenih objekata", + "review_thumbnails": "Pregled minijatura", + "previews": "Pregledi", + "exports": "Izvozi", + "recordings": "Snimci" + }, + "regionGrid": { + "title": "Mreža regija", + "desc": "Mreža regija je optimizacija koja uči gdje se objekti različitih veličina obično pojavljuju u svakoj kamere polju pogleda. Frigate koristi ove podatke da učinkovito postavi regije detekcije. Mreža se automatski gradi tokom vremena iz podataka o praćenim objektima.", + "clear": "Očisti rešetku područja", + "clearConfirmTitle": "Očisti Rešetku Područja", + "clearConfirmDesc": "Očišćavanje rešetke područja nije preporučeno osim ako ste nedavno promijenili veličinu modela detektora ili promijenili fizičku poziciju kamere i imate probleme s praćenjem objekata. Rešetka će se automatski ponovno izgraditi tokom vremena kada se objekti praćuju. Potreban je ponovni pokretanje Frigate-a za primjenu promjena.", + "clearSuccess": "Rešetka područja uspješno očišćena", + "clearError": "Neuspješno očišćavanje rešetke područja", + "restartRequired": "Potreban je ponovni pokretanje za primjenu promjena rešetke područja" + } + }, + "configForm": { + "global": { + "title": "Globalne postavke", + "description": "Ove postavke se primjenjuju na sve kamere osim ako nisu prekrivene u postavkama specifičnim za kameru." + }, + "camera": { + "title": "Postavke kamere", + "description": "Ove postavke se primjenjuju samo na ovu kameru i prekrivaju globalne postavke.", + "noCameras": "Nema dostupnih kamera" + }, + "advancedSettingsCount": "Napredne postavke ({{count}})", + "advancedCount": "Napredno ({{count}})", + "showAdvanced": "Prikaži napredne postavke", + "tabs": { + "sharedDefaults": "Dijeljene zadane vrijednosti", + "system": "Sistem", + "integrations": "Integracije" + }, + "additionalProperties": { + "keyLabel": "Ključ", + "valueLabel": "Vrijednost", + "keyPlaceholder": "Novi ključ", + "remove": "Ukloni" + }, + "knownPlates": { + "namePlaceholder": "npr. Automobil supružnice", + "platePlaceholder": "Broj ploče ili regex" + }, + "timezone": { + "defaultOption": "Koristi vremensku zonu pregledača" + }, + "roleMap": { + "empty": "Nema mapiranja uloga", + "roleLabel": "Uloga", + "groupsLabel": "Grupe", + "addMapping": "Dodaj mapiranje uloga", + "remove": "Ukloni" + }, + "ffmpegArgs": { + "preset": "Predefinisana postavka", + "manual": "Ručni argumenti", + "inherit": "Naslijeđuj iz postavke kamere", + "none": "Nijedan", + "useGlobalSetting": "Naslijeđuj iz globalne postavke", + "selectPreset": "Odaberite predpostavljeno", + "manualPlaceholder": "Unesite argumente FFmpeg", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (Općenito)", + "preset-http-mjpeg-generic": "HTTP MJPEG (Općenito)", + "preset-http-reolink": "HTTP - Kamere Reolink", + "preset-rtmp-generic": "RTMP (Općenito)", + "preset-rtsp-generic": "RTSP (Općenito)", + "preset-rtsp-restream": "RTSP - Ponovno preusmjeravanje iz go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - Ponovno preusmjeravanje iz go2rtc (Niska kašnjenja)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Snimanje (Općenito, bez zvuka)", + "preset-record-generic-audio-copy": "Snimanje (Općenito + Kopiraj zvuk)", + "preset-record-generic-audio-aac": "Snimanje (Općenito + Zvuk u AAC)", + "preset-record-mjpeg": "Snimanje - Kamere MJPEG", + "preset-record-jpeg": "Snimanje - JPEG Kamere", + "preset-record-ubiquiti": "Snimanje - Ubiquiti Kamere" + } + }, + "cameraInputs": { + "itemTitle": "Prijenos {{index}}" + }, + "restartRequiredField": "Potrebno je ponovno pokretanje", + "restartRequiredFooter": "Konfiguracija promijenjena - Potrebno je ponovno pokretanje", + "sections": { + "detect": "Detekcija", + "record": "Snimanje", + "snapshots": "Snimci", + "motion": "Kretanje", + "objects": "Objekti", + "review": "Pregled", + "audio": "Audio", + "notifications": "Obavještenja", + "live": "Pregled uživo", + "timestamp_style": "Vremenske oznake", + "mqtt": "MQTT", + "database": "Baza podataka", + "telemetry": "Telemetrija", + "auth": "Autentifikacija", + "tls": "TLS", + "proxy": "Proxy", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg", + "detectors": "Detektori", + "model": "Model", + "semantic_search": "Semantička pretraga", + "genai": "GenAI", + "face_recognition": "Prepoznavanje lica", + "lpr": "Prepoznavanje tablice vozila", + "birdseye": "Birdseye", + "masksAndZones": "Maskice / Zone" + }, + "detect": { + "title": "Postavke detekcije" + }, + "detectors": { + "title": "Postavke detektora", + "singleType": "Dozvoljen je samo jedan {{type}} detektor.", + "keyRequired": "Ime detektora je obavezno.", + "keyDuplicate": "Ime detektora već postoji.", + "noSchema": "Nema dostupnih šema detektora.", + "none": "Nema konfiguriranih instanci detektora.", + "add": "Dodaj detektor", + "addCustomKey": "Dodaj prilagođeni ključ" + }, + "record": { + "title": "Postavke snimanja" + }, + "snapshots": { + "title": "Postavke snimka" + }, + "motion": { + "title": "Postavke pokreta" + }, + "objects": { + "title": "Postavke objekta" + }, + "audioLabels": { + "summary": "Odabrano {{count}} audio oznake", + "empty": "Nema dostupnih audio oznaka" + }, + "objectLabels": { + "summary": "Odabrano {{count}} tipova objekata", + "empty": "Nema dostupnih oznaka objekata" + }, + "reviewLabels": { + "summary": "Odabrano {{count}} oznaka", + "empty": "Nema dostupnih oznaka" + }, + "filters": { + "objectFieldLabel": "{{field}} za {{label}}" + }, + "zoneNames": { + "summary": "{{count}} odabrano", + "empty": "Nema dostupnih zona" + }, + "inputRoles": { + "summary": "Odabrano {{count}} uloga", + "empty": "Nema dostupnih uloga", + "options": { + "detect": "Detektiraj", + "record": "Snimi", + "audio": "Audio" + } + }, + "genaiRoles": { + "options": { + "embeddings": "Ugrađivanje", + "vision": "Vizija", + "tools": "Alati" + } + }, + "semanticSearchModel": { + "placeholder": "Odaberi model…", + "builtIn": "Ugrađeni modeli", + "genaiProviders": "Dostavljatelji GenAI" + }, + "review": { + "title": "Pregled postavki" + }, + "audio": { + "title": "Postavke audija" + }, + "notifications": { + "title": "Postavke obavijesti" + }, + "live": { + "title": "Postavke pregleda uživo" + }, + "timestamp_style": { + "title": "Postavke vremenske oznake" + }, + "searchPlaceholder": "Pretraži...", + "addCustomLabel": "Dodaj prilagođenu oznaku...", + "genaiModel": { + "placeholder": "Odaberi model…", + "search": "Pretraži modele…", + "noModels": "Nema dostupnih modela" + } + }, + "globalConfig": { + "title": "Globalna konfiguracija", + "description": "Konfigurirajte globalne postavke koje se primjenjuju na sve kamere osim ako nisu prekriveni.", + "toast": { + "success": "Globalne postavke uspješno sačuvane", + "error": "Neuspješno spremanje globalnih postavki", + "validationError": "Validacija neuspješna" + } + }, + "cameraConfig": { + "title": "Konfiguracija kamere", + "description": "Konfigurirajte postavke za pojedinačne kamere. Postavke prekrivaju globalne podrazumijevane vrijednosti.", + "overriddenBadge": "Preklopljeno", + "resetToGlobal": "Vrati na globalno", + "toast": { + "success": "Postavke kamere uspješno sačuvane", + "error": "Neuspješno spremanje postavki kamere" + } + }, + "toast": { + "success": "Postavke uspješno sačuvane", + "applied": "Postavke uspješno primijenjene", + "successRestartRequired": "Postavke uspješno sačuvane. Ponovo pokrenite Frigate da biste primijenili svoje promjene.", + "error": "Neuspješno spremanje postavki", + "validationError": "Validacija neuspješna: {{message}}", + "resetSuccess": "Poništi i vratiti se na globalne podrazumijevane vrijednosti", + "resetError": "Neuspješno poništavanje postavki", + "saveAllSuccess_one": "Uspješno sačuvan odjeljak {{count}}.", + "saveAllSuccess_few": "Svi odjeljci {{count}} uspješno sačuvani.", + "saveAllSuccess_other": "Svi odjeljci {{count}} uspješno sačuvani.", + "saveAllPartial_one": "{{successCount}} od {{totalCount}} odjeljka sačuvan. {{failCount}} neuspješno.", + "saveAllPartial_few": "{{successCount}} od {{totalCount}} odjeljaka sačuvanih. {{failCount}} neuspješno.", + "saveAllPartial_other": "{{successCount}} od {{totalCount}} odjeljaka sačuvanih. {{failCount}} neuspješno.", + "saveAllFailure": "Neuspješno spremanje svih odjeljaka." + }, + "profiles": { + "title": "Profili", + "activeProfile": "Aktivni profil", + "noActiveProfile": "Nema aktivnog profila", + "active": "Aktivno", + "activated": "Profil '{{profile}}' aktiviran", + "activateFailed": "Neuspješno postavljanje profila", + "deactivated": "Profil deaktiviran", + "noProfiles": "Nema definisanih profila.", + "noOverrides": "Nema prekriženja", + "cameraCount_one": "kamera {{count}}", + "cameraCount_few": "{{count}} kamere", + "cameraCount_other": "{{count}} kamere", + "columnCamera": "Kamera", + "columnOverrides": "Prekriženja profila", + "baseConfig": "Bazna konfiguracija", + "addProfile": "Dodaj profil", + "newProfile": "Novi profil", + "profileNamePlaceholder": "npr. Opremljen, Odsutan, Noćni režim", + "friendlyNameLabel": "Ime profila", + "profileIdLabel": "ID profila", + "profileIdDescription": "Unutarnji identifikator korišten u konfiguraciji i automatizacijama", + "nameInvalid": "Dozvoljena su samo mala slova, brojevi i donje crte", + "nameDuplicate": "Profil s ovim imenom već postoji", + "error": { + "mustBeAtLeastTwoCharacters": "Mora imati najmanje 2 karaktera", + "mustNotContainPeriod": "Ne smije sadržavati tačke", + "alreadyExists": "Profil s ovim ID-om već postoji" + }, + "renameProfile": "Preimenuj profil", + "renameSuccess": "Profil preimenovan u '{{profile}}'", + "deleteProfile": "Obriši profil", + "deleteProfileConfirm": "Obriši profil \"{{profile}}\" sa svih kamera? Ovo ne može biti poništeno.", + "deleteSuccess": "Profil '{{profile}}' obrisan", + "createSuccess": "Profil '{{profile}}' kreiran", + "removeOverride": "Ukloni prekrivanje profila", + "deleteSection": "Izbriši prekrivanja sekcije", + "deleteSectionConfirm": "Ukloni prekrivanja {{section}} za profil {{profile}} na {{camera}}?", + "deleteSectionSuccess": "Uklonjena prekrivanja {{section}} za {{profile}}", + "enableSwitch": "Omogući profile", + "enabledDescription": "Profilei su omogućeni. Napravite novi profil ispod, pređite na sekciju konfiguracije kamere da biste napravili promjene i sačuvajte da bi promjene bile primijenjene.", + "disabledDescription": "Profilei vam omogućavaju da definirate imenovane skupove prekrivanja konfiguracije kamere (npr., opremljen, odsutan, noć) koji se mogu aktivirati na zahtjev." + }, + "unsavedChanges": "Imate nepohranjene promjene", + "confirmReset": "Potvrdi ponovno postavljanje", + "resetToDefaultDescription": "Ovo će ponovno postaviti sve postavke u ovoj sekciji na svoje zadane vrijednosti. Ova akcija ne može se povući.", + "resetToGlobalDescription": "Ovo će ponovno postaviti postavke u ovoj sekciji na globalne zadane vrijednosti. Ova akcija ne može se povući.", + "go2rtcStreams": { + "title": "go2rtc streamovi", + "description": "Upravljajte konfiguracijama go2rtc streamova za ponovno praćenje kamere. Svaki stream ima ime i jednu ili više izvornih URL-ova.", + "addStream": "Dodaj stream", + "addStreamDesc": "Unesite ime za novi stream. Ovo ime će se koristiti za referenciranje streama u vašoj konfiguraciji kamere.", + "addUrl": "Dodaj URL", + "streamName": "Ime streama", + "streamNamePlaceholder": "npr., front_door", + "streamUrlPlaceholder": "npr., rtsp://user:pass@192.168.1.100/stream", + "deleteStream": "Izbriši stream", + "deleteStreamConfirm": "Sigurni ste da želite izbrisati stream \"{{streamName}}\"? Kamere koje se referiraju na ovaj stream mogu prestati da rade.", + "noStreams": "Nema konfiguriranih go2rtc streamova. Dodajte stream da biste započeli.", + "validation": { + "nameRequired": "Ime streama je obavezno", + "nameDuplicate": "Stream s ovim imenom već postoji", + "nameInvalid": "Ime streama može sadržavati samo slova, brojeve, donje crte i crte za odvajanje", + "urlRequired": "Potrebna je bar jedna URL adresa" + }, + "renameStream": "Preimenuj tok", + "renameStreamDesc": "Unesite novi naziv za ovaj tok. Preimenovanje toka može oštetiti kamere ili druge toke koji se reference na njega po nazivu.", + "newStreamName": "Novi naziv toka", + "ffmpeg": { + "useFfmpegModule": "Koristi režim kompatibilnosti (ffmpeg)", + "video": "Video", + "audio": "Audio", + "hardware": "Hardverska ubrzanja", + "videoCopy": "Kopiraj", + "videoH264": "Prevedi na H.264", + "videoH265": "Prevedi na H.265", + "videoExclude": "Izuzmi", + "audioCopy": "Kopiraj", + "audioAac": "Prevedi na AAC", + "audioOpus": "Prevedi na Opus", + "audioPcmu": "Prevedi na PCM μ-law", + "audioPcma": "Prevedi na PCM A-law", + "audioPcm": "Prevedi na PCM", + "audioMp3": "Prevedi na MP3", + "audioExclude": "Izuzmi", + "hardwareNone": "Bez hardverske ubrzanja", + "hardwareAuto": "Automatska hardverska ubrzanja" + } + }, + "onvif": { + "profileAuto": "Automatski", + "profileLoading": "Učitavanje profila..." + }, + "configMessages": { + "review": { + "recordDisabled": "Snimanje je onemogućeno, stavke za pregled neće biti generisane.", + "detectDisabled": "Detekcija objekata je onemogućena. Stavke za pregled zahtijevaju detektovane objekte za kategorizaciju upozorenja i detekcija.", + "allNonAlertDetections": "Sve aktivnosti koje nisu upozorenja bit će uključene kao detekcije." + }, + "audio": { + "noAudioRole": "Nijedan tok nema definisan ulogu zvuka. Morate omogućiti ulogu zvuka da bi detekcija zvuka mogla da funkcioniše." + }, + "audioTranscription": { + "audioDetectionDisabled": "Detekcija zvuka nije omogućena za ovu kameru. Transkripcija zvuka zahtijeva da detekcija zvuka bude aktivna." + }, + "detect": { + "fpsGreaterThanFive": "Postavljanje vrijednosti detect FPS veće od 5 nije preporučljivo. Veće vrijednosti mogu uzrokovati probleme s performansama i neće pružiti nikakvu korist." + }, + "faceRecognition": { + "globalDisabled": "Prepoznavanje lica nije omogućeno na globalnom nivou. Omogućite ga u Obogaćivanjima da bi prepoznavanje lica na nivou kamere funkcioniralo.", + "personNotTracked": "Prepoznavanje lica zahtijeva da se objekat 'osoba' praći. Osigurajte da je 'osoba' u listi praćenja objekata." + }, + "lpr": { + "globalDisabled": "Prepoznavanje registarskih tablica nije omogućeno na globalnom nivou. Omogućite ga u Obogaćivanjima da bi LPR na nivou kamere funkcionirao.", + "vehicleNotTracked": "Prepoznavanje tablice zahtijeva da se praći 'automobil' ili 'motocikl'." + }, + "record": { + "noRecordRole": "Nema streamova koji imaju definisanu ulogu snimanja. Snimanje neće funkcionišati." + }, + "birdseye": { + "objectsModeDetectDisabled": "Birdseye je postavljen na režim 'objekti', ali je detekcija objekata onemogućena za ovu kameru. Kamera neće biti prikazana u Birdseye." + }, + "snapshots": { + "detectDisabled": "Detekcija objekata je onemogućena. Snimci se generišu iz praćenih objekata i neće biti kreirani." + }, + "detectors": { + "mixedTypes": "Svi detektori moraju koristiti isti tip. Uklonite postojet će detektore da biste koristili drugi tip.", + "mixedTypesSuggestion": "Svi detektori moraju koristiti isti tip. Uklonite postojet će detektore ili izaberite {{type}}." + } + } +} diff --git a/web/public/locales/bs/views/system.json b/web/public/locales/bs/views/system.json new file mode 100644 index 0000000..b36221e --- /dev/null +++ b/web/public/locales/bs/views/system.json @@ -0,0 +1,256 @@ +{ + "documentTitle": { + "cameras": "Statistika kamere - Frigate", + "storage": "Statistika skladišta - Frigate", + "general": "Opća statistika - Frigate", + "enrichments": "Statistika bogatstva - Frigate", + "logs": { + "frigate": "Zapisi Frigate - Frigate", + "go2rtc": "Zapisi Go2RTC - Frigate", + "nginx": "Zapisi Nginx - Frigate", + "websocket": "Zapisi poruka - Frigate" + } + }, + "title": "Sistem", + "metrics": "Sistem metrike", + "logs": { + "websocket": { + "label": "Zapisi", + "pause": "Pauziraj", + "resume": "Nastavi", + "clear": "Očisti", + "filter": { + "all": "Svi temi", + "topics": "Teme", + "events": "Događaji", + "reviews": "Pregledi", + "classification": "Klasifikacija", + "face_recognition": "Prepoznavanje lica", + "lpr": "LPR", + "camera_activity": "Aktivnost kamere", + "system": "Sistem", + "camera": "Kamera", + "all_cameras": "Sve kamere", + "cameras_count_one": "{{count}} Kamera", + "cameras_count_other": "{{count}} Kamere" + }, + "empty": "Nema još prihvaćenih poruka", + "count_one": "{{count}} poruka", + "count_other": "{{count}} poruke", + "expanded": { + "payload": "Opterećenje" + } + }, + "download": { + "label": "Preuzimanje zapisa" + }, + "copy": { + "label": "Kopiraj u clipboard", + "success": "Zapisi su kopirani u clipboard", + "error": "Nije moguće kopirati zapise u clipboard" + }, + "type": { + "label": "Tip", + "timestamp": "Vremenski pečat", + "tag": "Oznaka", + "message": "Poruka" + }, + "tips": "Zapisi se prenose sa servera", + "toast": { + "error": { + "fetchingLogsFailed": "Greška prilikom preuzimanja zapisa: {{errorMessage}}", + "whileStreamingLogs": "Greška prilikom prijenosa protokola: {{errorMessage}}" + } + } + }, + "general": { + "title": "Općenito", + "detector": { + "title": "Detektori", + "inferenceSpeed": "Brzina zaključivanja detektora", + "temperature": "Temperatura detektora", + "cpuUsage": "Korištenje CPU detektora", + "cpuUsageInformation": "CPU korištena za pripremu ulaznih i izlaznih podataka za/iz modela detekcije. Ova vrijednost ne mjeri korištenje zaključivanja, čak i ako se koristi GPU ili ubrzivač.", + "memoryUsage": "Korištenje memorije detektora" + }, + "hardwareInfo": { + "title": "Hardverske informacije", + "gpuUsage": "Korištenje GPU", + "gpuMemory": "Memorija GPU", + "gpuEncoder": "Kodiralo GPU", + "gpuCompute": "GPU Izračunavanje / Kodiranje", + "gpuDecoder": "Dekodiranje GPU", + "gpuTemperature": "Temperatura GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo Izlaz", + "returnCode": "Kod povratka: {{code}}", + "processOutput": "Izlaz procesa:", + "processError": "Greška procesa:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI Izlaz", + "name": "Ime: {{name}}", + "driver": "Vozač: {{driver}}", + "cudaComputerCapability": "CUDA sposobnost izračunavanja: {{cuda_compute}}", + "vbios": "VBios informacije: {{vbios}}" + }, + "closeInfo": { + "label": "Zatvori informacije GPU" + }, + "copyInfo": { + "label": "Kopiraj informacije GPU" + }, + "toast": { + "success": "Kopirano informacije GPU u međuspremnik" + } + }, + "npuUsage": "Korišćenje NPU", + "npuMemory": "Memorija NPU", + "npuTemperature": "Temperatura NPU", + "intelGpuWarning": { + "title": "Upozorenje o statistikama Intel GPU", + "message": "Statistike GPU nedostupne", + "description": "Ovo je poznati bug u alatima za prikaz statistika Intel GPU (intel_gpu_top) gdje će se prekiniti i ponovo vratiti GPU korišćenje od 0% čak i u slučajevima kada se hardverska akceleracija i detekcija objekata ispravno izvršavaju na (i)GPU. Ovo nije bug Frigate. Možete ponovo pokrenuti host kako biste privremeno popravili problem i potvrdili da GPU radi ispravno. Ovo ne utiče na performanse." + } + }, + "otherProcesses": { + "title": "Drugi procesi", + "processCpuUsage": "Korišćenje CPU procesa", + "processMemoryUsage": "Korišćenje memorije procesa", + "series": { + "go2rtc": "go2rtc", + "recording": "Snimanje", + "review_segment": "pregled segmenta", + "embeddings": "Ugrađivanja", + "audio_detector": "audio detektor" + } + } + }, + "storage": { + "title": "Skladište", + "overview": "Pregled", + "recordings": { + "title": "Snimci", + "tips": "Ova vrijednost predstavlja ukupno skladište koje se koristi za snimke u bazi podataka Frigate. Frigate ne praćenje korišćenje skladišta za sve datoteke na vašem disku.", + "earliestRecording": "Najstariji dostupni snimak:" + }, + "shm": { + "title": "Alokacija SHM (deljenja memorije)", + "warning": "Trenutna veličina SHM od {{total}}MB je prevelika. Povećajte je na najmanje {{min_shm}}MB.", + "frameLifetime": { + "title": "Vijek trajanja okvira", + "description": "Svaka kamera ima {{frames}} slotova za okvire u deljenoj memoriji. Na najbržoj brzini okvira kamere, svaki okvir je dostupan za približno {{lifetime}}s prije nego što se prepiše." + } + }, + "cameraStorage": { + "title": "Skladište kamere", + "camera": "Kamera", + "unusedStorageInformation": "Informacije o neiskorišćenom skladištu", + "storageUsed": "Skladište", + "percentageOfTotalUsed": "Postotak ukupno", + "bandwidth": "Širina pojasa", + "unused": { + "title": "Neiskorišćeno", + "tips": "Ova vrijednost može nepravilno predstavljati slobodno prostor dostupan Frigate ako imate druge datoteke pohranjene na vašem disku izvan snimaka Frigate. Frigate ne praćenje korišćenje skladišta izvan svojih snimaka." + } + } + }, + "cameras": { + "title": "Kamere", + "overview": "Pregled", + "info": { + "aspectRatio": "odnos stranica", + "cameraProbeInfo": "{{camera}} Informacije o ispitivanju kamere", + "streamDataFromFFPROBE": "Podaci o prijenosu se dobijaju pomoću ffprobe.", + "fetching": "Prenošenje podataka o kameri", + "stream": "Prijenos {{idx}}", + "video": "Video:", + "codec": "Kodek:", + "resolution": "Rješenje:", + "fps": "FPS:", + "unknown": "Nepoznato", + "audio": "Zvuk:", + "error": "Greška: {{error}}", + "tips": { + "title": "Informacije o ispitivanju kamere" + } + }, + "framesAndDetections": "Okviri / Detekcije", + "label": { + "camera": "Kamera", + "detect": "detektirati", + "skipped": "preskočeno", + "ffmpeg": "FFmpeg", + "capture": "snimiti", + "overallFramesPerSecond": "ukupni okviri po sekundi", + "overallDetectionsPerSecond": "ukupne detekcije po sekundi", + "overallSkippedDetectionsPerSecond": "ukupno preskočene detekcije po sekundi", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} snimiti", + "cameraDetect": "{{camName}} detektirati", + "cameraGpu": "{{camName}} GPU", + "cameraFramesPerSecond": "{{camName}} okviri po sekundi", + "cameraDetectionsPerSecond": "{{camName}} detekcije po sekundi", + "cameraSkippedDetectionsPerSecond": "{{camName}} preskočenih detekcija u sekundi" + }, + "connectionQuality": { + "title": "Kvaliteta veze", + "excellent": "Izuzetno dobra", + "fair": "Uredna", + "poor": "Loša", + "unusable": "Nepogodna", + "fps": "FPS", + "expectedFps": "Očekivani FPS", + "reconnectsLastHour": "Ponovne povezivanja (posljednje satu)", + "stallsLastHour": "Pauze (posljednje satu)" + }, + "toast": { + "success": { + "copyToClipboard": "Podaci o testiranju kopirani u clipboard." + }, + "error": { + "unableToProbeCamera": "Nemoguće testiranje kamere: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Posljednje ažuriranje: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} ima visoku upotrebu CPU za FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} ima visoku upotrebu CPU za detekciju ({{detectAvg}}%)", + "healthy": "Sistem je zdrav", + "reindexingEmbeddings": "Ponovno indeksiranje ugrađenih vjerodajnica ({{processed}}% završeno)", + "cameraIsOffline": "{{camera}} je offline", + "detectIsSlow": "{{detect}} je spor ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} je vrlo spor ({{speed}} ms)", + "shmTooLow": "/dev/shm alokacija ({{total}} MB) treba povećati na najmanje {{min}} MB.", + "debugReplayActive": "Debug ponavljanje sesije je aktivno" + }, + "enrichments": { + "title": "Obogaćivanja", + "infPerSecond": "Inferencije po sekundi", + "averageInf": "Prosjek vremena inferencije", + "embeddings": { + "image_embedding": "Slika ugrađenih vjerodajnica", + "text_embedding": "Tekst ugrađenih vjerodajnica", + "face_recognition": "Prepoznavanje lica", + "plate_recognition": "Prepoznavanje ploča", + "image_embedding_speed": "Brzina ugradnje slika", + "face_embedding_speed": "Brzina ugradnje lica", + "face_recognition_speed": "Brzina prepoznavanja lica", + "plate_recognition_speed": "Brzina prepoznavanja ploča", + "text_embedding_speed": "Brzina ugradnje teksta", + "yolov9_plate_detection_speed": "Brzina detekcije ploča YOLOv9", + "yolov9_plate_detection": "Detekcija ploča YOLOv9", + "review_description": "Pregled opisa", + "review_description_speed": "Brzina pregleda opisa", + "review_description_events_per_second": "Pregled opisa", + "object_description": "Opis objekta", + "object_description_speed": "Brzina opisa objekta", + "object_description_events_per_second": "Opis objekta", + "classification": "{{name}} Klasifikacija", + "classification_speed": "{{name}} Brzina klasifikacije", + "classification_events_per_second": "{{name}} Događaji klasifikacije po sekundi" + } + } +} diff --git a/web/public/locales/ca/audio.json b/web/public/locales/ca/audio.json new file mode 100644 index 0000000..0cd1959 --- /dev/null +++ b/web/public/locales/ca/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Parla", + "babbling": "Balbuceig", + "yell": "Crit", + "whispering": "Xiuxiuejant", + "laughter": "Riure", + "snicker": "Rialleta", + "crying": "Plor", + "bellow": "Bram", + "sigh": "Suspir", + "singing": "Cant", + "choir": "Cor", + "yodeling": "Cant Tirolès", + "chant": "Càntic", + "mantra": "Mantra", + "child_singing": "Cant Infantil", + "synthetic_singing": "Cant Sintètic", + "rapping": "Rap", + "humming": "Taral·leig", + "groan": "Gemec", + "grunt": "Grunyo", + "whistling": "Xiulet", + "wheeze": "Sibilància", + "snoring": "Ronc", + "gasp": "Jadeig", + "cat": "Gat", + "dog": "Gos", + "animal": "Animal", + "bark": "Escorça", + "horse": "Cavall", + "sheep": "Ovella", + "goat": "Cabra", + "bird": "Ocell", + "mouse": "Ratolí", + "keyboard": "Teclat", + "vehicle": "Vehicle", + "boat": "Vaixell", + "car": "Cotxe", + "motorcycle": "Motocicleta", + "bus": "Autobús", + "bicycle": "Bicicleta", + "train": "Tren", + "skateboard": "Monopatí", + "door": "Porta", + "hair_dryer": "Assecador de cabell", + "sink": "Aigüera", + "blender": "Batedora", + "toothbrush": "Raspall de dents", + "scissors": "Tisores", + "clock": "Rellotge", + "breathing": "Respiració", + "fart": "Pet", + "stomach_rumble": "Barbolló d'estómac", + "hands": "Mans", + "burping": "Eructe", + "hiccup": "Singlot", + "whoop": "Crit d'alegria", + "pant": "Esbufec", + "snort": "Esbufec nasal", + "cough": "Tos", + "throat_clearing": "Carraspeig", + "sneeze": "Esternut", + "sniff": "olorar", + "run": "Córrer", + "shuffle": "Passos arrossegats", + "footsteps": "Passos", + "chewing": "Masticació", + "biting": "Mossegada", + "gargling": "Gàrgares", + "finger_snapping": "Claqueig de dits", + "heartbeat": "Batec del cor", + "heart_murmur": "Sospit cardíac", + "cheering": "Ovacions", + "applause": "Aplaudiments", + "clapping": "Cop de mans", + "chatter": "Xerrameca", + "crowd": "Multitud", + "children_playing": "Nens jugant", + "pets": "Animals de companyia", + "camera": "Càmera", + "wild_animals": "Animals salvatges", + "heavy_engine": "Motor pesat", + "wedding_music": "Música de casament", + "yip": "Crit agut", + "howl": "Udol", + "bow_wow": "Lladruc", + "growling": "Grunyit", + "whimper_dog": "Gemec de gos", + "purr": "Ronroneig", + "meow": "Miol", + "hiss": "Siseig", + "caterwaul": "Udol estrident", + "livestock": "Bestiar", + "clip_clop": "Clip-clop", + "neigh": "Relinxo", + "cattle": "Bestiar boví", + "moo": "Mugir", + "cowbell": "Esquellot", + "pig": "Porc", + "oink": "Oinc", + "bleat": "Brama", + "fowl": "Au de corral", + "chicken": "Pollastre", + "cluck": "Cloqueig", + "cock_a_doodle_doo": "Quiquiriquí", + "turkey": "Gall dindi", + "gobble": "Gorgoriteig", + "duck": "Ànec", + "quack": "Quac", + "goose": "Oca", + "honk": "Cluc-cluc", + "roaring_cats": "Gats que rugen", + "roar": "Rugit", + "chirp": "Piulet", + "squawk": "Xerric", + "pigeon": "Colom", + "coo": "Arruix", + "crow": "Corb", + "caw": "Cric", + "owl": "Mussol", + "hoot": "Ulul", + "flapping_wings": "Batuda d’ales", + "dogs": "Gossos", + "rats": "Rates", + "patter": "Repic", + "insect": "Insecte", + "cricket": "Grill", + "mosquito": "Mosquit", + "fly": "Mosca", + "buzz": "Brunzit", + "frog": "Granota", + "croak": "Grall", + "snake": "Serp", + "rattle": "Cascavell", + "whale_vocalization": "Vocalització de balena", + "music": "Música", + "musical_instrument": "Instrument musical", + "plucked_string_instrument": "Instrument de corda pinçada", + "guitar": "Guitarra", + "electric_guitar": "Guitarra elèctrica", + "bass_guitar": "Guitarra baixa", + "acoustic_guitar": "Guitarra acústica", + "steel_guitar": "Guitarra steel", + "tapping": "Tapping", + "strum": "Rasgueig", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolina", + "zither": "Cítara", + "ukulele": "Ukulele", + "piano": "Piano", + "electric_piano": "Piano elèctric", + "organ": "Orgue", + "electronic_organ": "Orgue electrònic", + "hammond_organ": "Orgue Hammond", + "synthesizer": "Sintetitzador", + "sampler": "Sampler", + "harpsichord": "Clavicèmbal", + "percussion": "Percussió", + "drum_kit": "Bateria", + "drum_machine": "Caixa de ritmes", + "drum": "Tambor", + "snare_drum": "Caixa", + "rimshot": "Rimshot", + "drum_roll": "Rul·lat de tambor", + "bass_drum": "Bombo", + "timpani": "Timpà", + "tabla": "Tabla", + "cymbal": "Plat", + "hi_hat": "Charles", + "wood_block": "Bloc de fusta", + "tambourine": "Pandereta", + "maraca": "Maraca", + "gong": "Gong", + "tubular_bells": "Campanes tubulars", + "mallet_percussion": "Percussió amb baquetes", + "marimba": "Marimba", + "glockenspiel": "Carilló", + "vibraphone": "Vibràfon", + "steelpan": "Steelpan", + "orchestra": "Orquestra", + "brass_instrument": "Instrument de metall", + "french_horn": "Corn francès", + "trumpet": "Trompeta", + "trombone": "Trombó", + "bowed_string_instrument": "Instrument de corda fregada", + "string_section": "Secció de corda", + "violin": "Violí", + "pizzicato": "Pizzicato", + "cello": "Violoncel", + "double_bass": "Contrabaix", + "wind_instrument": "Instrument de vent", + "flute": "Flauta", + "saxophone": "Saxòfon", + "clarinet": "Clarinet", + "harp": "Arpa", + "bell": "Campana", + "church_bell": "Campana d'església", + "jingle_bell": "Campaneta", + "bicycle_bell": "Timbre de bicicleta", + "tuning_fork": "Diapasó", + "chime": "Timbre", + "wind_chime": "Campanes de vent", + "harmonica": "Harmònica", + "accordion": "Acordió", + "bagpipes": "Gaites", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Bol tibetà", + "scratching": "Esgarrapar", + "pop_music": "Música pop", + "hip_hop_music": "Música Hip-Hop", + "beatboxing": "Beatboxing", + "rock_music": "Música rock", + "heavy_metal": "Heavy Metal", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "progressive_rock": "Rock progressiu", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Rock psicodèlic", + "rhythm_and_blues": "Rhythm and blues", + "soul_music": "Música soul", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Música swing", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Música folk", + "middle_eastern_music": "Música d'Orient Mitjà", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Música clàssica", + "opera": "Òpera", + "electronic_music": "Música electrònica", + "house_music": "Música house", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Electrònica", + "electronic_dance_music": "Música electrònica de ball", + "ambient_music": "Música ambient", + "trance_music": "Música trance", + "music_of_latin_america": "Música d'Amèrica Llatina", + "salsa_music": "Música salsa", + "flamenco": "Flamenc", + "blues": "Blues", + "music_for_children": "Música per a nens", + "new-age_music": "Música new age", + "vocal_music": "Música vocal", + "a_capella": "A capella", + "music_of_africa": "Música d'Àfrica", + "afrobeat": "Afrobeat", + "christian_music": "Música cristiana", + "gospel_music": "Música gospel", + "music_of_asia": "Música d'Àsia", + "carnatic_music": "Música carnàtica", + "music_of_bollywood": "Música de bollywood", + "ska": "Ska", + "traditional_music": "Música tradicional", + "independent_music": "Música independent", + "song": "Cançó", + "background_music": "Música de fons", + "theme_music": "Música temàtica", + "jingle": "Jingle", + "soundtrack_music": "Música de banda sonora", + "lullaby": "Lullaby", + "video_game_music": "Música de videojocs", + "christmas_music": "Música nadalenca", + "dance_music": "Música dance", + "happy_music": "Música alegre", + "sad_music": "Música trista", + "tender_music": "Música tendra", + "exciting_music": "Música emocionant", + "angry_music": "Música enfadada", + "scary_music": "Música de por", + "wind": "Vent", + "wind_noise": "Soroll del vent", + "thunderstorm": "Tempesta", + "thunder": "Tro", + "water": "Aigua", + "rain": "Pluja", + "raindrop": "Gota de pluja", + "rain_on_surface": "Pluja en superfície", + "stream": "Rierol", + "waterfall": "Cascada", + "ocean": "Oceà", + "waves": "Ones", + "steam": "Vapor", + "fire": "Foc", + "sailboat": "Veler", + "rowboat": "Barca de rems", + "ship": "Vaixell", + "motor_vehicle": "Vehicle de motor", + "car_alarm": "Alarma del cotxe", + "car_passing_by": "Cotxe passant", + "race_car": "Cotxe de curses", + "truck": "Camió", + "air_brake": "Fre d'aire", + "air_horn": "Bocina d'aire", + "ice_cream_truck": "Camió de gelats", + "emergency_vehicle": "Vehicle d'emergència", + "police_car": "Cotxe de policia", + "ambulance": "Ambulància", + "fire_engine": "Camió de bombers", + "traffic_noise": "Soroll de trànsit", + "rail_transport": "Transport ferroviari", + "train_whistle": "Xiulet de tren", + "train_horn": "Bocina de tren", + "railroad_car": "Vagó de tren", + "subway": "Metro", + "aircraft": "Aeronau", + "aircraft_engine": "Motor d'aeronau", + "propeller": "Hèlix", + "helicopter": "Helicòpter", + "fixed-wing_aircraft": "Aeronau d'Ala Fixa", + "engine": "Motor", + "light_engine": "Motor lleuger", + "dental_drill's_drill": "Trepant dental", + "lawn_mower": "Talla-gespa", + "chainsaw": "Motoserra", + "medium_engine": "Motor mitjà", + "engine_starting": "Arranc del motor", + "idling": "Ralentí", + "accelerating": "Accelerant", + "doorbell": "Timbre", + "ding-dong": "Ding-dong", + "sliding_door": "Porta corredissa", + "slam": "Cop de porta", + "knock": "Toc", + "tap": "Toc suau", + "squeak": "Grinyol", + "cupboard_open_or_close": "Obertura o tancament d'armari", + "drawer_open_or_close": "Obertura o tancament de calaix", + "dishes": "Plats", + "cutlery": "Coberteria", + "rustling_leaves": "Sons de fulles", + "gurgling": "Borbolleig", + "crackle": "Cremoreig", + "motorboat": "Llanxa a motor", + "toot": "Botzinada", + "skidding": "Derrapada", + "reversing_beeps": "Bips de marxa enrere", + "jet_engine": "Motor a reacció", + "train_wheels_squealing": "Xiulet de rodes de tren", + "engine_knocking": "Cop de motor", + "chopping": "Tallant", + "frying": "Fregint", + "electric_shaver": "Afeitadora elèctrica", + "shuffling_cards": "Barrejar cartes", + "alarm": "Alarma", + "alarm_clock": "Despertador", + "siren": "Sirena", + "buzzer": "Brunzidor", + "gears": "Engranatges", + "pulleys": "Politges", + "glass": "Vidre", + "chop": "Tall", + "splinter": "Astella", + "scream": "Crit", + "field_recording": "Enregistrament de camp", + "tire_squeal": "Xiulet de rodes", + "explosion": "Explosió", + "wood": "Fusta", + "crack": "Esquerda", + "air_conditioning": "Aire condicionat", + "tick-tock": "Tic-tac", + "sewing_machine": "Màquina de cosir", + "writing": "Escrivint", + "telephone": "Telèfon", + "environmental_noise": "Soroll ambiental", + "zipper": "Cremallera", + "smoke_detector": "Detector de fums", + "sound_effect": "Efecte sonor", + "microwave_oven": "Forn microones", + "water_tap": "Aixeta d'aigua", + "toilet_flush": "Cisterna del vàter", + "electric_toothbrush": "Raspall de dents elèctric", + "vacuum_cleaner": "Aspiradora", + "keys_jangling": "Claus repicant", + "bathtub": "Banyera", + "coin": "Moneda", + "typing": "Mecanografia", + "computer_keyboard": "Teclat d'ordinador", + "telephone_dialing": "Marcatge telefònic", + "dial_tone": "To de marcatge", + "telephone_bell_ringing": "Timbre del telèfon sonant", + "typewriter": "Màquina d'escriure", + "ringtone": "To de trucada", + "busy_signal": "Senyal d'ocupat", + "fire_alarm": "Alarma d'incendis", + "civil_defense_siren": "Sirena de defensa civil", + "foghorn": "Bocina de boira", + "whistle": "Xiulet", + "steam_whistle": "Xiulet de vapor", + "mechanical_fan": "Ventall mecànic", + "cash_register": "Caixa registradora", + "single-lens_reflex_camera": "Càmera reflex de lent fixa", + "mechanisms": "Mecanismes", + "ratchet": "Trinquet", + "tick": "Tic", + "printer": "Impressora", + "tools": "Eines", + "hammer": "Martell", + "jackhammer": "Martell neumàtic", + "sawing": "Serratge", + "filing": "Llimar", + "sanding": "Poliment", + "power_tool": "Eina elèctrica", + "machine_gun": "Ametralladora", + "cap_gun": "Pistola de joguina", + "drill": "Trepant", + "gunshot": "Tret", + "fusillade": "Ràfega de trets", + "fireworks": "Focs artificials", + "firecracker": "Petard", + "chink": "Clinc", + "shatter": "Trencar", + "silence": "Silenci", + "static": "Estàtic", + "white_noise": "Soroll blanc", + "burst": "Explosió", + "eruption": "Erupció", + "boom": "Boom", + "television": "Televisió", + "radio": "Ràdio", + "pink_noise": "Soroll rosa", + "power_windows": "Finestres elèctriques", + "artillery_fire": "Foc d'artilleria", + "sodeling": "Cant a la tirolesa", + "vibration": "Vibració", + "throbbing": "Palpitant", + "cacophony": "Cacofonia", + "sidetone": "To local", + "distortion": "Distorsió", + "mains_hum": "brunzit", + "noise": "Soroll", + "echo": "Echo", + "reverberation": "Reverberació", + "inside": "Interior", + "pulse": "Pols", + "outside": "Fora", + "chirp_tone": "Gisclada", + "harmonic": "Harmònic", + "sine_wave": "Ona sinus", + "crunch": "Cruixit", + "hum": "Zunzum", + "plop": "Xip-xap", + "clickety_clack": "Clic-Clac", + "clicking": "Clicant", + "clatter": "Rebombori", + "chird": "Piular", + "liquid": "Líquid", + "splash": "Esquitx", + "slosh": "Xipolleig", + "boing": "Rebot", + "zing": "Zunzum agut", + "rumble": "Retombori", + "sizzle": "Crepitació", + "whir": "Brrrm", + "rustle": "Frec", + "creak": "Rascada", + "clang": "Soroll metàl·lic", + "squish": "Xaf", + "drip": "Goteig", + "pour": "Abocament", + "trickle": "Raig fi", + "gush": "Raig fort", + "fill": "Ompliment", + "ding": "Ting", + "ping": "Ressò", + "beep": "Pitit", + "squeal": "Chirrit", + "crumpling": "Arrugant-se", + "rub": "Fregar", + "scrape": "Raspar", + "scratch": "Rasca", + "whip": "Fuet", + "bouncing": "Rebotant", + "breaking": "Trencant", + "smash": "Aixafar", + "whack": "Cop", + "slap": "Bufetada", + "bang": "Cop fort", + "basketball_bounce": "Rebot de bàsquet", + "chorus_effect": "Efecte de cor", + "effects_unit": "Unitat d'Efectes", + "electronic_tuner": "Afinador electrònic", + "thunk": "Bruix", + "thump": "Soroll sord", + "whoosh": "Xiuxiueig", + "arrow": "Fletxa", + "sonar": "Sonar", + "boiling": "Bullint", + "stir": "Remenar", + "pump": "Bomba", + "spray": "Esprai", + "shofar": "Xofar", + "crushing": "Aixafament", + "change_ringing": "Toc de campanes", + "flap": "Cop de peu", + "roll": "Rodament", + "tearing": "Esquinçat" +} diff --git a/web/public/locales/ca/common.json b/web/public/locales/ca/common.json new file mode 100644 index 0000000..219c044 --- /dev/null +++ b/web/public/locales/ca/common.json @@ -0,0 +1,331 @@ +{ + "role": { + "title": "Rol", + "viewer": "Visualitzador", + "admin": "Administrador", + "desc": "Els administradors tenen accés complet a totes les característiques de la interfície d'usuari de Frigate. Els visualitzadors es limiten a visualitzar càmeres, articles de revisió i imatges històriques a la interfície d'usuari." + }, + "menu": { + "language": { + "yue": "粵語 (Cantonès)", + "zhCN": "简体中文 (Xinès simplificat)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Francès)", + "ar": "العربية (Àrab)", + "de": "Deutsch (Alemany)", + "sv": "Svenska (Suec)", + "cs": "Čeština (Txec)", + "nb": "Norsk Bokmål (Noruec Bokmål)", + "ko": "한국어 (Coreà)", + "vi": "Tiếng Việt (Vietnamita)", + "fa": "فارسی (Persa)", + "hu": "Magyar (Hongarès)", + "fi": "Suomi (Finlandès)", + "en": "English (Anglès)", + "pt": "Português (Portuguès)", + "ja": "日本語 (Japonès)", + "es": "Español (Espanyol)", + "withSystem": { + "label": "Utilitzeu la configuració del sistema per a l'idioma" + }, + "tr": "Türkçe (Turc)", + "it": "Italiano (Italià)", + "he": "עברית (Hebreu)", + "el": "Ελληνικά (Grec)", + "ro": "Română (Romanès)", + "nl": "Nederlands (Holandès)", + "pl": "Polski (Polonès)", + "uk": "Українська (Ucraïnès)", + "da": "Dansk (Danès)", + "sk": "Slovenčina (Eslovac)", + "ru": "Русский (Rus)", + "th": "ไทย (Tailandès)", + "ca": "Català (Catalan)", + "ptBR": "Português brasileiro (Portuguès Brasiler)", + "sr": "Српски (Serbi)", + "sl": "Slovenščina (Sloveni)", + "lt": "Lietuvių (Lituà)", + "bg": "Български (Búlgar)", + "gl": "Galego (Gallec)", + "id": "Bahasa Indonesia (Indonesi)", + "ur": "اردو (Urdú)", + "hr": "Hrvatski (croat)", + "bs": "Bosanski (Bosni)", + "zhHant": "繁體中文 (Xinès Tradicional)" + }, + "system": "Sistema", + "systemMetrics": "Mètriques del sistema", + "configuration": "Configuració", + "systemLogs": "Registres del sistema", + "configurationEditor": "Editor de configuració", + "languages": "Idiomes", + "settings": "Opcions", + "darkMode": { + "light": "Clar", + "dark": "Fosc", + "withSystem": { + "label": "Utilitzeu la configuració del sistema per al mode clar o fosc" + }, + "label": "Mode fosc" + }, + "withSystem": "Sistema", + "appearance": "Aspecte", + "theme": { + "blue": "Blau", + "green": "Verd", + "nord": "Nord", + "red": "Vermell", + "default": "Per defecte", + "highcontrast": "Contrast Alt", + "label": "Tema" + }, + "help": "Ajuda", + "documentation": { + "title": "Documentació", + "label": "Documentació de Frigate" + }, + "restart": "Reinicia Frigate", + "live": { + "title": "Directe", + "allCameras": "Totes les càmeres", + "cameras": { + "title": "Càmeres", + "count_one": "{{count}} Càmera", + "count_many": "{{count}} Càmeres", + "count_other": "{{count}} Càmeres" + } + }, + "review": "Revisió", + "explore": "Explora", + "export": "Exportar", + "uiPlayground": "Zona de proves de la interfície d'usuari", + "faceLibrary": "Biblioteca de cares", + "user": { + "title": "Usuari", + "setPassword": "Estableix Contrasenya", + "account": "Compte", + "anonymous": "Anònim", + "logout": "Tanca la sessió", + "current": "Usuari actual: {{user}}" + }, + "classification": "Classificació", + "chat": "Xat", + "actions": "Accions", + "profiles": "Perfils", + "features": "Característiques" + }, + "pagination": { + "previous": { + "label": "Ves a pàgina anterior", + "title": "Anterior" + }, + "next": { + "label": "Ves a pàgina següent", + "title": "Següent" + }, + "more": "Més pàgines", + "label": "paginació" + }, + "time": { + "untilForTime": "Fins les {{time}}", + "untilForRestart": "Fins que Frigate es reiniciï.", + "untilRestart": "Fins que es reiniciï", + "ago": "Fa {{timeAgo}}", + "justNow": "Ara mateix", + "today": "Avui", + "yesterday": "Ahir", + "last7": "Últims 7 dies", + "last14": "Últims 14 dies", + "last30": "Últims 30 dies", + "thisWeek": "Aquesta setmana", + "lastWeek": "La setmana passada", + "thisMonth": "Aquest mes", + "lastMonth": "El mes passat", + "5minutes": "5 minuts", + "10minutes": "10 minuts", + "30minutes": "30 minuts", + "1hour": "1 hora", + "12hours": "12 hores", + "24hours": "24 hores", + "pm": "pm", + "am": "am", + "yr": "{{time}}any", + "year_one": "{{time}} any", + "year_many": "{{time}} anys", + "year_other": "{{time}} anys", + "mo": "{{time}}mes", + "month_one": "{{time}} mes", + "month_many": "{{time}} mesos", + "month_other": "{{time}} mesos", + "h": "{{time}}h", + "d": "{{time}}d", + "day_one": "{{time}} dia", + "day_many": "{{time}} dies", + "day_other": "{{time}} dies", + "hour_one": "{{time}} hora", + "hour_many": "{{time}} hores", + "hour_other": "{{time}} hores", + "m": "{{time}} m", + "minute_one": "{{time}} minut", + "minute_many": "{{time}} minuts", + "minute_other": "{{time}} minuts", + "s": "{{time}}s", + "second_one": "{{time}} segon", + "second_many": "{{time}} segons", + "second_other": "{{time}} segons", + "formattedTimestamp": { + "12hour": "MMM d, h::mm::ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestampMonthDayYear": { + "24hour": "MMM d, yyyy", + "12hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "h:mm:ss aaa" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "inProgress": "En curs", + "invalidStartTime": "Hora d'inici no vàlida", + "invalidEndTime": "Hora de finalització no vàlida", + "never": "Mai" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "peus", + "meters": "metres" + }, + "data": { + "kbps": "Kb/s", + "mbps": "Mb/s", + "gbps": "Gb/s", + "kbph": "kB/hora", + "mbph": "MB/hora", + "gbph": "GB/hora" + } + }, + "label": { + "back": "Torna enrere", + "hide": "Oculta {{item}}", + "show": "Mostra {{item}}", + "ID": "ID", + "none": "Cap", + "all": "Tots", + "other": "Altres" + }, + "button": { + "apply": "Aplicar", + "reset": "Restablir", + "done": "Fet", + "disabled": "Deshabilitat", + "disable": "Deshabilitar", + "save": "Desa", + "copy": "Copiar", + "back": "Enrere", + "pictureInPicture": "Imatge en Imatge", + "twoWayTalk": "Xerrada bidireccional", + "cameraAudio": "Àudio de la càmera", + "no": "No", + "yes": "Sí", + "download": "Descarregar", + "info": "Informació", + "suspended": "Suspès", + "export": "Exportar", + "deleteNow": "Eliminar ara", + "next": "Següent", + "saving": "Guardant…", + "cancel": "Cancelar", + "edit": "Editar", + "copyCoordinates": "Copiar coordenades", + "delete": "Elimina", + "unsuspended": "Reactivar", + "play": "Reproduir", + "close": "Tancar", + "history": "Historial", + "fullscreen": "Pantalla completa", + "exitFullscreen": "Sortir de pantalla completa", + "on": "ENCÈS", + "off": "APAGAT", + "unselect": "Desseleccionar", + "enable": "Habilitar", + "enabled": "Habilitat", + "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…", + "retry": "Torna a intentar" + }, + "toast": { + "copyUrlToClipboard": "URL copiada al porta-retalls.", + "save": { + "title": "Guardar", + "error": { + "title": "No s'han pogut guardar els canvis de configuració: {{errorMessage}}", + "noMessage": "No s'han pogut guardar els canvis de configuració" + }, + "success": "S'han desat correctament els canvis de configuració." + } + }, + "accessDenied": { + "desc": "No teniu permís per veure aquesta pàgina.", + "documentTitle": "Accés Denegat - Frigate", + "title": "Accés Denegat" + }, + "notFound": { + "documentTitle": "No s'ha trobat - Frigate", + "title": "404", + "desc": "Pàgina no trobada" + }, + "selectItem": "Selecciona {{item}}", + "readTheDocumentation": "Llegir la documentació", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} i {{1}}", + "many": "{{items}}, i {{last}}", + "separatorWithSpace": ",· " + }, + "field": { + "optional": "Opcional", + "internalID": "L'ID intern que Frigate s'utilitza a la configuració i a la base de dades" + }, + "no_items": "Sense elements", + "validation_errors": "Errors de validació", + "credentialField": { + "savedPlaceholder": "Desat — deixa en blanc per mantenir l'actual" + } +} diff --git a/web/public/locales/ca/components/auth.json b/web/public/locales/ca/components/auth.json new file mode 100644 index 0000000..1ca91ee --- /dev/null +++ b/web/public/locales/ca/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Usuari", + "password": "Contrasenya", + "login": "Iniciar sessió", + "errors": { + "usernameRequired": "El nom d'usuari és obligatori", + "passwordRequired": "La contrasenya és obligatoria", + "rateLimit": "S'ha superat el límit d'intents. Torna-ho a provar més tard.", + "loginFailed": "Error en l'inici de sessió", + "unknownError": "Error desconegut. Comproveu els registres.", + "webUnknownError": "Error desconegut. Comproveu els registres de la consola." + }, + "firstTimeLogin": "Intentar iniciar sessió per primera vegada? Les credencials s'imprimeixen als registres de Frigate." + } +} diff --git a/web/public/locales/ca/components/camera.json b/web/public/locales/ca/components/camera.json new file mode 100644 index 0000000..b95cd13 --- /dev/null +++ b/web/public/locales/ca/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "add": "Afegir grup de càmeres", + "edit": "Editar grup de càmeres", + "delete": { + "label": "Eliminar grup de càmeres", + "confirm": { + "title": "Confirmar eliminació", + "desc": "Estàs segur que vols eliminar el grup de càmeres {{name}}?" + } + }, + "name": { + "placeholder": "Introduïu un nom…", + "errorMessage": { + "mustLeastCharacters": "El nom del grup de càmeres ha de ser de com a mínim 2 caràcters.", + "exists": "El nom del grup de càmeres ja existeix.", + "nameMustNotPeriod": "El nom del grup de càmeres no pot contenir un punt.", + "invalid": "Nom del grup de càmeres no vàlid." + }, + "label": "Nom" + }, + "cameras": { + "label": "Càmeres", + "desc": "Seleccioneu càmeres per a aquest grup." + }, + "camera": { + "setting": { + "label": "Paràmetres de transmissió de la càmera", + "title": "Paràmetres de transmissió de {{cameraName}}", + "audioIsAvailable": "L'àudio està disponible per a aquesta transmissió", + "audioIsUnavailable": "L'audio no està disponible per a aquesta transmissió", + "audio": { + "tips": { + "document": "Llegir la documentació · ", + "title": "L'audio ha de venir de la càmera i estar configurat a go2rtc per a aquesta transmissió." + } + }, + "streamMethod": { + "label": "Mètode de transmissió", + "method": { + "noStreaming": { + "label": "Sense transmissió", + "desc": "Les imatges de la càmera només s'actualitzaran una vegada per minut i no hi haurà transmissió en viu." + }, + "smartStreaming": { + "label": "Transmissió intel·ligent (recomanat)", + "desc": "La transmissió intel·ligent actualitzarà la imatge de la teva càmera una vegada per minut quan no es detecti activitat per a conservar amplada de banda i recursos. Quan es detecti activitat, la imatge canviarà automàticament a una transmissió en directe." + }, + "continuousStreaming": { + "label": "Transmissió contínua", + "desc": { + "title": "La imatge de la càmera sempre serà una transmissió en directe quan estigui visible al panell de control, tot i que no hi hagi cap activitat detectada.", + "warning": "La transmissió contínua pot provocar problemes d'ús elevat d'amplada de banda i rendiment. Feu servir amb precaució." + } + } + }, + "placeholder": "Tria un mètode de transmissió" + }, + "compatibilityMode": { + "label": "Mode de compatibilitat", + "desc": "Activeu aquesta opció només si la transmissió en directe de la càmera mostra artefactes de color i té una línia diagonal a la part dreta de la imatge." + }, + "desc": "Cambia les opcions de transmissió en viu del panell de control d'aquest grup de càmeres. Aquest paràmetres son específics del dispositiu/navegador.", + "stream": "Transmissió", + "placeholder": "Seleccionar una transmissió" + }, + "birdseye": "Ull d'ocell" + }, + "success": "El grup de càmeres ({{name}}) ha estat guardat.", + "icon": "Icona", + "label": "Grups de Càmeres", + "showAll": "Mostra tots els grups de càmeres", + "showLess": "Mostra menys", + "editGroups": "Edita els grups de la càmera" + }, + "debug": { + "options": { + "title": "Opcions", + "showOptions": "Mostra opcions", + "hideOptions": "Amaga opcions", + "label": "Paràmetres" + }, + "boundingBox": "Caixa delimitadora", + "timestamp": "Marca temporal", + "zones": "Zones", + "mask": "Màscara", + "motion": "Moviment", + "regions": "Regions", + "paths": "Rutes" + } +} diff --git a/web/public/locales/ca/components/dialog.json b/web/public/locales/ca/components/dialog.json new file mode 100644 index 0000000..f63d47f --- /dev/null +++ b/web/public/locales/ca/components/dialog.json @@ -0,0 +1,208 @@ +{ + "restart": { + "title": "Estàs segur que vols reiniciar Frigate?", + "button": "Reiniciar", + "restarting": { + "title": "Frigate s'està reiniciant", + "content": "Aquesta pàgina es tornarà a carregar d'aquí a {{countdown}} segons.", + "button": "Forçar la recàrrega ara" + }, + "description": "Això aturarà breument Frigate mentre es reinicia." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Enviar a Frigate+", + "desc": "Els objectes a les ubicacions que voleu evitar no són falsos positius. Enviar-los com a falsos positius confondrà el model." + }, + "review": { + "question": { + "label": "Confirmar aquesta etiqueta per a Frigate Plus", + "ask_a": "Aquest objecte és un {{label}}?", + "ask_an": "Aquest objecte és un {{label}}?", + "ask_full": "Aquest objecte és un {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Enviat" + }, + "toast": { + "error": "No s'ha pogut enviar a Frigate+. Si us plau, comproveu la vostra connexió de xarxa i torneu-ho a provar." + } + } + }, + "video": { + "viewInHistory": "Veure a l'historial" + } + }, + "export": { + "time": { + "custom": "Personalitzat", + "fromTimeline": "Seleccionar a la línia de temps", + "lastHour_one": "L'última hora", + "lastHour_many": "Les últimes {{count}} hores", + "lastHour_other": "Les últimes {{count}} hores", + "start": { + "title": "Hora d'inci", + "label": "Seleccionar una hora d'inici" + }, + "end": { + "title": "Hora de finalització", + "label": "Seleccionar una hora de finalització" + } + }, + "name": { + "placeholder": "Nom de l'exportació" + }, + "select": "Seleccionar", + "export": "Exportar", + "selectOrExport": "Seleccionar o exportar", + "toast": { + "success": "Exportació inciada amb èxit. Pots veure l'arxiu a la pàgina d'exportacions.", + "error": { + "endTimeMustAfterStartTime": "L'hora de finalització ha de ser posterior a l'hora d'inici", + "noVaildTimeSelected": "No s'ha seleccionat un rang de temps vàlid", + "failed": "No s'ha pogut inciar l'exportació: {{error}}" + }, + "view": "Vista", + "queued": "Exporta a la cua. Mostra el progrés a la pàgina d'exportacions.", + "batchSuccess_one": "S'ha iniciat l'exportació 1. Obrint el cas ara.", + "batchSuccess_many": "S'han iniciat {{count}} exportacions. Obrint el cas ara.", + "batchSuccess_other": "S'han iniciat {{count}} exportacions. Obrint el cas ara.", + "batchPartial": "S'han iniciat {{successful}} de {{total}} exportacions. Càmeres fallides: {{failedCameras}}", + "batchFailed": "No s'han pogut iniciar {{total}} exportacions. Càmeres fallides: {{failedCameras}}", + "batchQueuedSuccess_one": "Exporta a la cua 1. Obrint el cas ara.", + "batchQueuedSuccess_many": "{{count}} exportacions a la cua. Obrint el cas ara.", + "batchQueuedSuccess_other": "{{count}} exportacions a la cua. Obrint el cas ara.", + "batchQueuedPartial": "{{successful}} de {{total}} exportacions a la cua. Càmeres fallides: {{failedCameras}}", + "batchQueueFailed": "No s'han pogut posar a la cua {{total}} exportacions. Càmeres fallides: {{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "Guardar exportació", + "previewExport": "Previsualitzar exportació", + "queueingExport": "S'està fent la cua de l'exportació...", + "useThisRange": "Utilitza aquest interval" + }, + "case": { + "label": "Cas", + "placeholder": "Selecciona un cas", + "newCaseOption": "Crea un cas no", + "newCaseNamePlaceholder": "Nom de cas nou", + "newCaseDescriptionPlaceholder": "Descripció del cas", + "nonAdminHelp": "Es crearà un nou cas per a aquestes exportacions." + }, + "queueing": "S'està fent la cua de l'exportació...", + "tabs": { + "export": "Càmera única", + "multiCamera": "Multicàmera" + }, + "multiCamera": { + "timeRange": "Interval de temps", + "selectFromTimeline": "Selecciona des de la línia de temps", + "cameraSelection": "Càmeres", + "cameraSelectionHelp": "Les càmeres amb objectes rastrejats en aquest interval de temps estan preseleccionades", + "checkingActivity": "Comprovant l'activitat de la càmera...", + "noCameras": "No hi ha càmeres disponibles", + "detectionCount_one": "1 objecte rastrejat", + "detectionCount_many": "{{count}} objectes rastrejats", + "detectionCount_other": "{{count}} objectes rastrejats", + "nameLabel": "Nom de l'exportació", + "namePlaceholder": "Nom base opcional per a aquestes exportacions", + "queueingButton": "S'estan posant a la cua les exportacions...", + "exportButton_one": "Exporta 1 càmera", + "exportButton_many": "Exporta {{count}} càmeres", + "exportButton_other": "Exporta {{count}} càmeres", + "searchOrSelectGroup": "Cerca, o selecciona un grup de càmeres...", + "selectAll": "Selecciona totes les càmeres", + "clearSelection": "Neteja la selecció", + "selectWithActivity": "Càmeres amb objectes rastrejats", + "selectGroup": "Selecciona un grup", + "noMatchingCameras": "No hi ha càmeres que coincideixin amb la cerca", + "selectedCount": "{{selected}} / {{total}} seleccionats" + }, + "multi": { + "title_one": "Exporta {{count}} ressenyes", + "title_many": "Exporta {{count}} ressenyes", + "title_other": "Exporta {{count}} ressenyes", + "description": "Exporta cada revisió seleccionada. Totes les exportacions s'agruparan en un sol cas.", + "descriptionNoCase": "Exporta cada revisió seleccionada.", + "caseNamePlaceholder": "Exporta la revisió - {{date}}", + "exportButton_one": "Exporta {{count}} ressenyes", + "exportButton_many": "Exporta {{count}} ressenyes", + "exportButton_other": "Exporta {{count}} ressenyes", + "exportingButton": "S'està exportant...", + "toast": { + "started_one": "S'ha iniciat l'exportació 1. Obrint el cas ara.", + "started_many": "S'han iniciat {{count}} exportacions. Obrint el cas ara.", + "started_other": "S'han iniciat {{count}} exportacions. Obrint el cas ara.", + "startedNoCase_one": "S'ha iniciat l'exportació 1.", + "startedNoCase_many": "S'han iniciat {{count}} exportacions.", + "startedNoCase_other": "S'han iniciat {{count}} exportacions.", + "partial": "S'han iniciat {{successful}} de {{total}} exportacions. Ha fallat: {{failedItems}}", + "failed": "No s'han pogut iniciar {{total}} exportacions. Ha fallat: {{failedItems}}" + } + } + }, + "streaming": { + "label": "Transmissió", + "restreaming": { + "disabled": "La retransmissió no està habilitada per a aquesta càmera.", + "desc": { + "title": "Configurar go2rtc per a àudio i opcions addicionals de visualització en directe per a aquesta càmera.", + "readTheDocumentation": "Llegir la documentació" + } + }, + "showStats": { + "label": "Mostrar les estadístiques de la transmissió", + "desc": "Activa aquesta opció per a mostrar les estadístiques de la transmissió superposades a la imatge de la càmera." + }, + "debugView": "Vista de depuració" + }, + "search": { + "saveSearch": { + "label": "Desar la cerca", + "desc": "Propocioneu un nom per a aquesta cerca desada.", + "placeholder": "Introduïu un nom per a la vostra cerca", + "success": "La cerca {{searchName}} ha sigut desada.", + "overwrite": "{{searchName}} ja existeix. Si deseu, es sobreescriurà el valor existent.", + "button": { + "save": { + "label": "Desar aquesta cerca" + } + } + } + }, + "recording": { + "button": { + "deleteNow": "Suprimir ara", + "export": "Exportar", + "markAsReviewed": "Marcar com a revisat", + "markAsUnreviewed": "Marcar com no revisat" + }, + "confirmDelete": { + "title": "Confirmar la supressió", + "desc": { + "selected": "Esteu segurs que voleu suprimir tots els vídeos enregistrats associats a aquest element de revisió?

    Manteniu premuda la tecla Maj per ometre aquest diàleg en el futur." + }, + "toast": { + "success": "Els enregistraments de vídeo associats als elements de revisió seleccionats s’han suprimit correctament.", + "error": "No s'ha pogut suprimir: {{error}}" + } + }, + "shareTimestamp": { + "label": "Comparteix la marca horària", + "title": "Comparteix la marca horària", + "description": "Comparteix un URL amb marca horària de la posició actual del jugador o tria una marca horària personalitzada. Tingueu en compte que aquest no és un URL de compartició pública i només és accessible per als usuaris amb accés a Frigate i aquesta càmera.", + "custom": "Marca horària personalitzada", + "button": "Comparteix l'URL de la marca horària", + "shareTitle": "Marca de temps de revisió de Frigate: {{camera}}" + } + }, + "imagePicker": { + "selectImage": "Selecciona la miniatura d'un objecte rastrejat", + "search": { + "placeholder": "Cerca per etiqueta o subetiqueta..." + }, + "noImages": "No s'han trobat miniatures per a aquesta càmera", + "unknownLabel": "Imatge activadora desada" + } +} diff --git a/web/public/locales/ca/components/filter.json b/web/public/locales/ca/components/filter.json new file mode 100644 index 0000000..5a0a7b0 --- /dev/null +++ b/web/public/locales/ca/components/filter.json @@ -0,0 +1,140 @@ +{ + "labels": { + "all": { + "short": "Etiquetes", + "title": "Totes les etiquetes" + }, + "label": "Etiquetes", + "count_one": "{{count}} Etiqueta", + "count_other": "{{count}} Etiquetes" + }, + "filter": "Filtre", + "zones": { + "label": "Zones", + "all": { + "title": "Totes les zones", + "short": "Zones" + } + }, + "dates": { + "all": { + "title": "Totes les dates", + "short": "Dates" + }, + "selectPreset": "Selecciona un preajust…" + }, + "more": "Més filtres", + "reset": { + "label": "Restablir filtres als valors predeterminats" + }, + "timeRange": "Rang de temps", + "subLabels": { + "label": "Subetiquetes", + "all": "Totes les subetiquetes" + }, + "score": "Puntuació", + "estimatedSpeed": "Velocitat estimada ({{unit}})", + "features": { + "label": "Característiques", + "hasSnapshot": "Té una instantània", + "hasVideoClip": "Té un clip de vídeo", + "submittedToFrigatePlus": { + "label": "Enviat a Frigate+", + "tips": "Primer heu de filtrar els objectes de seguiment que tenen una instantània.

    Els objectes de seguiment sense una instantània no es poden enviar a Frigate+." + } + }, + "sort": { + "label": "Ordenar", + "dateAsc": "Data (Ascendent)", + "dateDesc": "Data (Descendent)", + "scoreAsc": "Puntuació de l'objecte (Ascendent)", + "scoreDesc": "Puntuació de l'objecte (Descendent)", + "speedAsc": "Velocitat estimada (Ascendent)", + "speedDesc": "Velocitat estimada (descendent)", + "relevance": "Rellevància" + }, + "explore": { + "settings": { + "defaultView": { + "summary": "Resum", + "unfilteredGrid": "Quadrícula sense filtrar", + "title": "Vista per defecte", + "desc": "Quan no s'ha seleccionat cap filtre, mostreu un resum dels objectes de seguiment més recents per etiqueta, o visualitzeu una quadrícula sense filtrar." + }, + "gridColumns": { + "title": "Columnes de la quadrícula", + "desc": "Seleccionar el nombre de columnes a la vista de quadrícula." + }, + "searchSource": { + "label": "Font de cerca", + "options": { + "description": "Descripció", + "thumbnailImage": "Imatge en miniatura" + }, + "desc": "Trieu si voleu cercar les miniatures o les descripcions dels objectes de seguiment." + }, + "title": "Configuració" + }, + "date": { + "selectDateBy": { + "label": "Seleccionr a una data per filtrar" + } + } + }, + "logSettings": { + "disableLogStreaming": "Deshabilitar la transmissió de registres", + "allLogs": "Tots els registres", + "loading": { + "desc": "Quan el panell de registre es desplaça cap a la part inferior, els registres nous apareixen automàticament a mesura que s'afegeixen.", + "title": "Carregant" + }, + "label": "Filtrar nivell de registre", + "filterBySeverity": "Filtrar registre per gravetat" + }, + "trackedObjectDelete": { + "title": "Confirmar la supressió", + "desc": "En suprimir aquests {{objectLength}} objectes de seguiment, s'elimina la instatània, les incrustacions desades, i els registres de temps de vida. Les imatges gravades d'aquests objectes NO es suprimiran de l'historial.

    Està segur que vol continuar?

    Manteniu la tecla Shift per ometre aquest diàleg en el futur.", + "toast": { + "success": "Els objectes amb seguiment s'han suprimit correctament.", + "error": "No s'han pogut suprimir els objectes de seguiment: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrar per màscara de zona" + }, + "recognizedLicensePlates": { + "title": "Matrícules reconegudes", + "loadFailed": "No s'han pogut carregar les matrícules reconegudes.", + "loading": "Carregant les matrícules reconegudes…", + "placeholder": "Escriu per a buscar matrícules…", + "noLicensePlatesFound": "No s'han trobat matrícules.", + "selectPlatesFromList": "Seleccioni una o més matrícules de la llista.", + "selectAll": "Seleccionar tots", + "clearAll": "Netejar tot" + }, + "cameras": { + "label": "Filtre de càmeres", + "all": { + "title": "Totes les càmeres", + "short": "Càmeres" + } + }, + "review": { + "showReviewed": "Mostrar els revisats" + }, + "motion": { + "showMotionOnly": "Mostar només el moviment" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Totes les classes" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classes" + }, + "attributes": { + "label": "Atributs de classificació", + "all": "Tots els atributs" + } +} diff --git a/web/public/locales/ca/components/icons.json b/web/public/locales/ca/components/icons.json new file mode 100644 index 0000000..88fba47 --- /dev/null +++ b/web/public/locales/ca/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecciona una icona", + "search": { + "placeholder": "Buscar icona…" + } + } +} diff --git a/web/public/locales/ca/components/input.json b/web/public/locales/ca/components/input.json new file mode 100644 index 0000000..7b36418 --- /dev/null +++ b/web/public/locales/ca/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Descarregar vídeo", + "toast": { + "success": "S’ha començat a descarregar el vídeo de l’element de revisió." + } + } + } +} diff --git a/web/public/locales/ca/components/player.json b/web/public/locales/ca/components/player.json new file mode 100644 index 0000000..1617279 --- /dev/null +++ b/web/public/locales/ca/components/player.json @@ -0,0 +1,53 @@ +{ + "stats": { + "latency": { + "short": { + "title": "Latència", + "value": "{{seconds}} s" + }, + "title": "Latència:", + "value": "{{seconds}} segons" + }, + "streamType": { + "title": "Tipus de transmissió:", + "short": "Tipus" + }, + "bandwidth": { + "title": "Ample de banda:", + "short": "Ample de banda" + }, + "totalFrames": "Fotogrames totals:", + "droppedFrames": { + "title": "Fotogrames perduts:", + "short": { + "title": "Perduts", + "value": "{{droppedFrames}} fotogrames" + } + }, + "decodedFrames": "Fotogrames decodificats:", + "droppedFrameRate": "Taxa de fotogrames perduts:" + }, + "noRecordingsFoundForThisTime": "No s'han trobat enregistraments en aquesta hora", + "noPreviewFound": "No s'ha trobat previsualització", + "noPreviewFoundFor": "No s'ha trobat cap previsualització per a {{cameraName}}", + "submitFrigatePlus": { + "title": "Enviar aquesta imatge a Frigate+?", + "submit": "Enviar", + "previewError": "No s'ha pogut carregar la vista prèvia de la instantània. És possible que l'enregistrament no estigui disponible en aquest moment." + }, + "livePlayerRequiredIOSVersion": "Es requereix iOS 17.1 o superior per a aquest tipus de reproducció en directe.", + "streamOffline": { + "title": "Transmissió desconnectada", + "desc": "No s’han rebut imatges a la transmissió detect de la càmera {{cameraName}}. Comprova els registres d’errors" + }, + "cameraDisabled": "La càmera està desactivada", + "toast": { + "success": { + "submittedFrigatePlus": "Fotograma enviat correctament a Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Error al enviar fotograma a Frigate+" + } + }, + "cameraOff": "La càmera està apagada" +} diff --git a/web/public/locales/ca/config/cameras.json b/web/public/locales/ca/config/cameras.json new file mode 100644 index 0000000..1ba68b1 --- /dev/null +++ b/web/public/locales/ca/config/cameras.json @@ -0,0 +1,961 @@ +{ + "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": "Detecció 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, 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.", + "threshold": { + "label": "Confiança mínima de l'àudio", + "description": "Llindar mínim de confiança per a l'esdeveniment d'àudio a comptar." + } + }, + "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": "Habilita la detecció d'objectes", + "description": "Activa o desactiva la detecció d'objectes per a aquesta càmera." + }, + "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": "Fluxos (FFmpeg)", + "description": "Les entrades de flux de càmera i les opcions del FFmpeg, incloent-hi el camí binari, els arguments, l'hwaccel i els arguments de sortida per rol.", + "path": { + "label": "Ruta FFmpeg", + "description": "Ruta al binari FFmpeg a usar o un àlies de versió («7.0» o «8.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 (rutes 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 s'estableix a un valor entre 0.0 i 1.0, i 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. Deixa sense establir (Cap) per desactivar aquesta característica." + } + }, + "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ó." + }, + "max_concurrent": { + "label": "Màxim d'exportacions concurrents", + "description": "Nombre màxim de treballs d'exportació a processar al mateix temps." + } + }, + "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 als quals els objectes rastrejats generen deteccions (sense-alerta) i com es mantenen les deteccions.", + "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 API-generades dels objectes seguits per a aquesta càmera.", + "enabled": { + "label": "Habilita les instantànies", + "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 de l'API." + }, + "bounding_box": { + "label": "Superposició de la caixa contenidora", + "description": "Dibuixa caixes contenidores per als objectes seguits en les instantànies de l'API." + }, + "crop": { + "label": "Retalla la instantània", + "description": "Retalla les instantànies de l'API 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 de l'API; 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, 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 captura", + "description": "Qualitat 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 aplicades instantànies i la vista de depuració.", + "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." + }, + "profile": { + "label": "Perfil ONVIF", + "description": "Perfil multimèdia ONVIF específic a utilitzar per al control PTZ, que coincideix amb el token o el nom. Si no s'estableix, el primer perfil amb configuració PTZ vàlida se selecciona automàticament." + } + }, + "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." + }, + "review": { + "label": "Mostra en la revisió", + "description": "Alterna si aquesta càmera és visible a la revisió (la pàgina de revisió i el seu filtre de càmera, la revisió de moviment i la vista de l'historial)." + } + }, + "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 Friagte. 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." + }, + "profiles": { + "label": "Perfils", + "description": "Perfils de configuració amb nom amb anul·lacions parcials que es poden activar en temps d'execució." + } +} diff --git a/web/public/locales/ca/config/global.json b/web/public/locales/ca/config/global.json new file mode 100644 index 0000000..038deb4 --- /dev/null +++ b/web/public/locales/ca/config/global.json @@ -0,0 +1,2358 @@ +{ + "ffmpeg": { + "apple_compatibility": { + "description": "Activa l'etiquetatge HEVC per a una millor compatibilitat amb el reproductor d'Apple en gravar H.265.", + "label": "Compatibilitat d'Apple" + }, + "description": "Paràmetres del FFmpeg que inclouen la ruta dels binaris, args, opcions de hwaccel i args de sortida per rol.", + "path": { + "label": "Ruta FFmpeg", + "description": "Ruta al binari FFmpeg a usar o un àlies de versió («7.0» o «8.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." + }, + "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 (rutes 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." + } + }, + "label": "FFmpeg" + }, + "live": { + "height": { + "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.", + "label": "Alçada del directe" + }, + "label": "Reproducció en directe", + "description": "Configuració per a controlar la resolució i la qualitat del flux en viu del jsmpeg. Això no afecta les càmeres restreamed que utilitzen go2rtc per a la vista en directe.", + "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." + }, + "quality": { + "label": "Qualitat del directe", + "description": "Qualitat de codificació per al flux jsmpeg (1 més alt, 31 més baix)." + } + }, + "ui": { + "label": "Interfície", + "description": "Preferències de la interfície d'usuari com ara la zona horària, el format de l'hora/data i les unitats.", + "timezone": { + "label": "Zona horària", + "description": "Zona horària opcional que es mostrarà a través de la interfície d'usuari (per defecte al navegador hora local si no s'estableix)." + }, + "time_format": { + "label": "Format de l'hora", + "description": "Format d'hora a utilitzar a la interfície d'usuari (navegador, 12 hores o 24 hores)." + }, + "date_style": { + "label": "Estil de data", + "description": "Estil de data a utilitzar a la interfície d'usuari (complet, llarg, mitjà, curt)." + }, + "time_style": { + "label": "Estil de temps", + "description": "Estil de temps a utilitzar a la interfície d'usuari (complet, llarg, mitjà, curt)." + }, + "unit_system": { + "label": "Sistema d'unitat", + "description": "Sistema d'unitats per a la visualització (mètrica o imperial) utilitzat en la IU i MQTT." + } + }, + "motion": { + "improve_contrast": { + "description": "Aplicar la millora del contrast als fotogrames abans de l'anàlisi del moviment per ajudar a la detecció.", + "label": "Millora el contrast" + }, + "label": "Detecció de moviment", + "description": "Paràmetres de detecció de moviment per defecte aplicats a les càmeres llevat que se substitueixin per càmera.", + "enabled": { + "label": "Habilita la detecció de moviment", + "description": "Activa o desactiva la detecció de moviment per a totes les càmeres; es pot sobreescriure per 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." + }, + "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 s'estableix a un valor entre 0.0 i 1.0, i 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. Deixa sense establir (Cap) per desactivar aquesta característica." + } + }, + "objects": { + "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": { + "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" + } + }, + "genai": { + "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." + }, + "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." + }, + "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." + } + }, + "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 rastrejar per a totes les càmeres; es poden sobreescriure per càmera." + }, + "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" + }, + "filters_attribute": { + "label": "Filtres d'atribut", + "description": "Filtres aplicats als atributs detectats per reduir falsos positius (àrea, relació, confiança).", + "min_area": { + "label": "Àrea mínima de l'atribut", + "description": "Es requereix una àrea de caixa contenidora mínima (píxels o percentatge) per a aquest atribut. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)." + }, + "max_area": { + "label": "Àrea màxima de l'atribut", + "description": "Es permet l'àrea màxima del contenidor (píxels o percentatge) per a aquest atribut. 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'atribut 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 associar aquest atribut amb el seu objecte pare." + }, + "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" + } + } + }, + "record": { + "detections": { + "retain": { + "days": { + "label": "Dies de retenció", + "description": "Nombre de dies per a retenir enregistraments d'esdeveniments de detecció." + }, + "label": "Retenció d'esdeveniments", + "description": "Configuració de retenció per a 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)." + } + }, + "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." + } + }, + "alerts": { + "post_capture": { + "label": "Segons de postcaptura", + "description": "Nombre de segons després de l'esdeveniment de detecció que s'inclourà a l'enregistrament." + }, + "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." + }, + "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)." + } + } + }, + "label": "Enregistrament", + "description": "Configuració d'enregistrament i retenció aplicada a les càmeres llevat que se substitueixi per càmera.", + "enabled": { + "label": "Habilita l'enregistrament", + "description": "Activa o desactiva l'enregistrament per a totes les càmeres; es pot substituir per 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." + } + }, + "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ó." + }, + "max_concurrent": { + "label": "Màxim d'exportacions concurrents", + "description": "Nombre màxim de treballs d'exportació a processar al mateix temps." + } + }, + "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": { + "detections": { + "required_zones": { + "description": "Zones que un objecte ha d'introduir per a ser considerat una detecció; deixeu-ho buit per a permetre qualsevol zona.", + "label": "Zones requerides" + }, + "label": "Configuració de les deteccions", + "description": "Paràmetres per als quals els objectes rastrejats generen deteccions (sense-alerta) i com es mantenen les deteccions.", + "enabled": { + "label": "Habilita les deteccions", + "description": "Habilita o inhabilita els esdeveniments de detecció per a totes les càmeres; es poden sobreescriure per càmera." + }, + "labels": { + "label": "Etiquetes de detecció", + "description": "Llista d'etiquetes d'objectes que es qualifiquen com a esdeveniments de detecció." + }, + "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": { + "image_source": { + "description": "Font d'imatges enviades a GenAI ('previsualització' o 'enregistraments'); 'enregistraments' utilitza Fotogrames de més qualitat però més tokens.", + "label": "Revisa l'origen de la imatge" + }, + "enabled_in_config": { + "description": "Fa un seguiment de si la revisió de GenAI es va habilitar originalment a la configuració estàtica.", + "label": "Estat original de 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ó." + }, + "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ó." + }, + "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." + } + }, + "label": "Revisió", + "description": "Configuració que controla les alertes, les deteccions i els resums de revisió de GenAI utilitzats per la IU i l'emmagatzematge.", + "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 totes les càmeres; es pot substituir per 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." + } + } + }, + "semantic_search": { + "triggers": { + "enabled": { + "description": "Activa o desactiva aquest activador de cerca semàntica.", + "label": "Habilita aquest activador" + }, + "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." + }, + "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)." + } + }, + "label": "Cerca semàntica", + "description": "Configuració de la cerca semàntica que construeix i consulta incrustacions d'objectes per trobar elements similars.", + "enabled": { + "label": "Habilita la cerca semàntica", + "description": "Activa o desactiva la funció de cerca semàntica." + }, + "reindex": { + "label": "Reindexa en iniciar", + "description": "Activa un reindexat complet d'objectes rastrejats històrics a la base de dades d'incrustacions." + }, + "model": { + "label": "Model de cerca semàntica o nom del proveïdor GenAI", + "description": "El model de vectors a utilitzar per a la cerca semàntica (per exemple 'jinav1'), o el nom d'un proveïdor de GenAI amb el rol de vectors." + }, + "model_size": { + "label": "Mida del model", + "description": "Seleccioneu la mida del model; «petit» s'executa a la CPU i «gran» normalment requereix GPU." + }, + "device": { + "label": "Dispositiu", + "description": "Això és una sobreescriptura, per dirigir-se a un dispositiu específic. Vegeu https://onnxruntime.ai/docs/execution-providers/ per a més informació" + } + }, + "snapshots": { + "label": "Instantànies", + "description": "Arranjament per a les instantànies de l'API dels objectes rastrejats per a totes les càmeres; es pot sobreescriure per càmera.", + "enabled": { + "label": "Habilita les instantànies", + "description": "Habilita o inhabilita les instantànies de desament per a totes les càmeres; es pot sobreescriure per 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 de l'API." + }, + "bounding_box": { + "label": "Superposició de la caixa contenidora", + "description": "Dibuixa caixes contenidores per als objectes seguits en les instantànies de l'API." + }, + "crop": { + "label": "Retalla la instantània", + "description": "Retalla les instantànies de l'API 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 de l'API; 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, 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 captura", + "description": "Qualitat per a les instantànies desades (0-100)." + } + }, + "timestamp_style": { + "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": { + "blue": { + "label": "Blau", + "description": "Component blau (0-255) per al color de la marca horària." + }, + "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." + } + }, + "label": "Estil de la marca horària", + "description": "Opcions d'estilització per a marques horàries d'alimentació aplicades a la vista de depuració 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)." + }, + "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)." + } + }, + "onvif": { + "autotracking": { + "enabled_in_config": { + "description": "Camp intern per a fer el seguiment de si s'ha habilitat el seguiment automàtic a la configuració.", + "label": "Estat de la pista automàtica original" + }, + "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." + } + }, + "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)." + }, + "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." + }, + "profile": { + "label": "Perfil ONVIF", + "description": "Perfil multimèdia ONVIF específic a utilitzar per al control PTZ, que coincideix amb el token o el nom. Si no s'estableix, el primer perfil amb configuració PTZ vàlida se selecciona automàticament." + } + }, + "audio_transcription": { + "enabled": { + "label": "Habilita la transcripció d'àudio", + "description": "Activa o desactiva la transcripció automàtica d'àudio per a totes les càmeres; es pot substituir per càmera." + }, + "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.", + "language": { + "label": "Idioma de transcripció", + "description": "Codi d'idioma utilitzat per a la transcripció/traducció (per exemple 'en' per a l'anglès). Vegeu https://whisper-api.com/docs/languages/ per als codis de llengua compatibles." + }, + "device": { + "label": "Dispositiu de transcripció", + "description": "Tecla de dispositiu (CPU/GPU) per a executar el model de transcripció. Actualment només les GPU CUDA NVIDIA estan admeses per a la transcripció." + }, + "model_size": { + "label": "Mida del model", + "description": "Mida del model a utilitzar per a la transcripció d'esdeveniments d'àudio fora de línia." + }, + "live_enabled": { + "label": "Transcripció en viu", + "description": "Habilita la transcripció en directe per a l'àudio a mesura que es rep." + } + }, + "version": { + "label": "Versió de configuració actual", + "description": "Versió numèrica o de cadena de la configuració activa per ajudar a detectar migracions o canvis de format." + }, + "safe_mode": { + "label": "Mode segur", + "description": "Quan està activat, arrenca la Frigate en mode segur amb funcions reduïdes per a la resolució de problemes." + }, + "environment_vars": { + "label": "Variables d'entorn", + "description": "Parells clau/valor de les variables d'entorn a establir per al procés Frigate al sistema operatiu Home Assistant. Els usuaris que no són usuaris de HAOS han d'utilitzar la configuració de la variable d'entorn Docker." + }, + "logger": { + "label": "Registre", + "description": "Controla la verbositat predeterminada del registre i el nivell de registre per component.", + "default": { + "label": "Nivell de registre", + "description": "Veritat predeterminada del registre global (depuració, informació, avís, error)." + }, + "logs": { + "label": "Nivell de registre per procés", + "description": "El nivell de registre per component substitueix per augmentar o disminuir la verbositat per a mòduls específics." + } + }, + "auth": { + "label": "Autenticació", + "description": "Configuració relacionada amb l'autenticació i la sessió, incloses les opcions de cookies i límit de velocitat.", + "enabled": { + "label": "Habilita l'autenticació", + "description": "Habilita l'autenticació nativa per a la interfície d'usuari de la Frigate." + }, + "reset_admin_password": { + "label": "Restableix la contrasenya d'administrador", + "description": "Si és cert, restableix la contrasenya de l'usuari administrador a l'inici i imprimeix la nova contrasenya als registres." + }, + "cookie_name": { + "label": "Nom de la cookie JWT", + "description": "Nom de la cookie utilitzada per emmagatzemar el testimoni JWT per a l'autenticació nativa." + }, + "cookie_secure": { + "label": "Atribut de segura de la cookie", + "description": "Establiu l'indicador segur a l'auth cookie; hauria de ser cert quan s'utilitza TLS." + }, + "session_length": { + "label": "Longitud de la sessió", + "description": "Durada de la sessió en segons per a les sessions basades en JWT." + }, + "refresh_time": { + "label": "Finestra d'actualització de sessió", + "description": "Quan una sessió estigui dins d'aquests segons d'expiració, refresca-la de nou a tota la durada." + }, + "failed_login_rate_limit": { + "label": "Límits d'inici de sessió fallits", + "description": "Regles de limitació de la taxa per als intents d'inici de sessió fallits per reduir els atacs de força bruta." + }, + "trusted_proxies": { + "label": "Intermediaris de confiança", + "description": "Llista d'IP de confiança usades quan es determina la IP del client per limitar la taxa." + }, + "hash_iterations": { + "label": "Iteracions de resum", + "description": "Nombre d'iteracions PBKDF2-SHA256 a utilitzar quan es fan servir contrasenyes d'usuari." + }, + "roles": { + "label": "Mapatge de rols", + "description": "Mapa els rols a les llistes de càmeres. Una llista buida permet l'accés a totes les càmeres per al rol." + }, + "admin_first_time_login": { + "label": "Bandera d'administració per primera vegada", + "description": "Quan sigui cert, la interfície d'usuari pot mostrar un enllaç d'ajuda a la pàgina d'inici de sessió informant els usuaris de com iniciar la sessió després d'un restabliment de contrasenya d'administrador. " + } + }, + "face_recognition": { + "label": "Reconeixement de cares", + "description": "Configuració per a la detecció de la cara i el reconeixement de totes les càmeres; es pot substituir per càmera.", + "enabled": { + "label": "Habilita el reconeixement facial", + "description": "Activa o desactiva el reconeixement facial de totes les càmeres; es pot substituir per càmera." + }, + "model_size": { + "label": "Mida del model", + "description": "Mida del model a utilitzar per als vectors facials (petit/gran); més gran pot requerir GPU." + }, + "unknown_score": { + "label": "Llindar de puntuació desconegut", + "description": "Llindar de distància per sota del qual una cara es considera una coincidència potencial (més alta ). més estricta)." + }, + "detection_threshold": { + "label": "Llindar de detecció", + "description": "Confiança mínima de detecció necessària per considerar vàlida una detecció facial." + }, + "recognition_threshold": { + "label": "Llindar de reconeixement", + "description": "Llindar de distància d'incrustació de cares per considerar dues cares una coincidència." + }, + "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." + }, + "min_faces": { + "label": "Cares mínimes", + "description": "Nombre mínim de reconeixements facials necessaris abans d'aplicar una subetiqueta reconeguda a una persona." + }, + "save_attempts": { + "label": "Desa els intents", + "description": "Nombre d'intents de reconeixement facial que s'han de conservar per a la interfície d'usuari de reconeixement recent." + }, + "blur_confidence_filter": { + "label": "Filtre de confiança del difuminat", + "description": "Ajusta les puntuacions de confiança basades en el difuminat de la imatge per reduir falsos positius per a cares de mala qualitat." + }, + "device": { + "label": "Dispositiu", + "description": "Això és una sobreescriptura, per dirigir-se a un dispositiu específic. Vegeu https://onnxruntime.ai/docs/execution-providers/ per a més informació" + } + }, + "database": { + "label": "Base de dades", + "description": "Configuració de la base de dades SQLite utilitzada per Frigate per emmagatzemar objectes rastrejats i enregistrar metadades.", + "path": { + "label": "Ruta a la base de dades", + "description": "Ruta del sistema de fitxers on s'emmagatzemarà el fitxer de base de dades SQLite de Frigate." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "Paràmetres per al servei de restreaming go2rtc integrat utilitzat per a la retransmissió i traducció de flux en viu." + }, + "mqtt": { + "label": "MQTT", + "description": "Configuració per a connectar i publicar telemetria, instantànies i detalls d'esdeveniment a un corredor MQTT.", + "enabled": { + "label": "Habilita MQTT", + "description": "Activa o desactiva la integració MQTT per a l'estat, els esdeveniments i les instantànies." + }, + "host": { + "label": "Amfitrió MQTT", + "description": "Nom d'amfitrió o adreça IP del corredor MQTT." + }, + "port": { + "label": "Port MQTT", + "description": "Port del corredor MQTT (normalment 1883 per a MQTT pla)." + }, + "topic_prefix": { + "label": "Prefix del tema", + "description": "El prefix del tema MQTT per a tots els temes de Frigate; ha de ser únic si s'executen diverses instàncies." + }, + "client_id": { + "label": "ID del client", + "description": "L'identificador de client utilitzat quan es connecta al broker MQTT; hauria de ser únic per instància." + }, + "stats_interval": { + "label": "Interval d'estadístiques", + "description": "Interval en segons per a la publicació de les estadístiques del sistema i de la càmera a MQTT." + }, + "user": { + "label": "Nom d'usuari MQTT", + "description": "El nom d'usuari opcional del MQTT; es pot proporcionar a través de variables d'entorn o secrets." + }, + "password": { + "label": "Contrasenya MQTT", + "description": "Contrasenya opcional MQTT; es pot proporcionar a través de variables d'entorn o secrets." + }, + "tls_ca_certs": { + "label": "TLS CA certs", + "description": "Ruta al certificat de CA per a les connexions TLS al corredor (per a autosignats certs)." + }, + "tls_client_cert": { + "label": "Cert del client", + "description": "Ruta del certificat del client per a l'autenticació mútua TLS; no estableixis l'usuari/contrasenya quan utilitzis el client CERT." + }, + "tls_client_key": { + "label": "Clau del client", + "description": "Camí de clau privada per al certificat de client." + }, + "tls_insecure": { + "label": "TLS insegur", + "description": "Permet connexions TLS insegures saltant la verificació del nom d'amfitrió (no recomanat)" + }, + "qos": { + "label": "MQTT QoS", + "description": "Nivell de qualitat del servei per a publicacions/subscripcions de MQTT (0, 1, o 2)." + } + }, + "notifications": { + "label": "Notificacions", + "description": "La configuració per a habilitar i controlar les notificacions de totes les càmeres; es pot substituir per la càmera.", + "enabled": { + "label": "Habilita les notificacions", + "description": "Activa o desactiva les notificacions per a totes les càmeres; es pot sobreescriure per 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." + } + }, + "networking": { + "label": "Xarxa", + "description": "Paràmetres relacionats amb la xarxa com l'habilitació IPv6 per als punts finals de Frigate.", + "ipv6": { + "label": "Configuració IPv6", + "description": "Configuració específica d'IPv6 per als serveis de xarxa de fragate.", + "enabled": { + "label": "Habilita IPv6", + "description": "Activa el suport IPv6 per als serveis de Frigate (API i UI) quan sigui aplicable" + } + }, + "listen": { + "label": "S'està escoltant la configuració dels ports", + "description": "Configuració per a ports d'escolta interns i externs. Això és per a usuaris avançats. Per a la majoria de casos d'ús es recomana canviar la secció de ports del fitxer Docker.", + "internal": { + "label": "Port intern", + "description": "Port d'escolta intern per a la Frigate (predeterminat 5000)." + }, + "external": { + "label": "Port extern", + "description": "Port d'escolta extern per a la Frigate (predeterminat 8971)." + } + } + }, + "proxy": { + "label": "Proxy", + "description": "Paràmetres per a integrar Frigate darrere d'un servidor intermediari invers que passa les capçaleres d'usuari autenticades.", + "header_map": { + "label": "Mapeig de capçaleres", + "description": "Mapa les capçaleres del servidor intermediari entrant a l'usuari de Frigate i als camps de rol per a l'autenticació basada en el servidor intermediari.", + "user": { + "label": "Capçalera d'usuari", + "description": "Capçalera que conté el nom d'usuari autenticat proporcionat pel servidor intermediari de la font." + }, + "role": { + "label": "Capçalera del rol", + "description": "Capçalera que conté el rol o els grups de l'usuari autenticat des del servidor intermediari de flux ascendent." + }, + "role_map": { + "label": "Mapatge del rol", + "description": "Mapa els valors de grup de la font als rols de Frigate (per exemple, assigna els grups d'administració al rol d'administrador)." + } + }, + "logout_url": { + "label": "URL de sortida", + "description": "URL a la qual redirigir els usuaris quan es tanqui la sessió a través del servidor intermediari." + }, + "auth_secret": { + "label": "Secret proxy", + "description": "S'ha comprovat el secret opcional contra la capçalera X-Proxy-Secret per verificar els servidors intermediaris de confiança." + }, + "default_role": { + "label": "Rol predeterminat", + "description": "Rol predeterminat assignat als usuaris intermediaris autenticats quan no s'aplica cap mapatge de rols." + }, + "separator": { + "label": "Caràcter separador", + "description": "Caràcter utilitzat per a dividir múltiples valors proporcionats a les capçaleres del servidor intermediari." + } + }, + "telemetry": { + "label": "Telemetria", + "description": "Opcions de telemetria del sistema i estadístiques, incloent-hi la GPU i el monitoratge de l'amplada de banda de la xarxa.", + "network_interfaces": { + "label": "Interfícies de xarxa", + "description": "Llista de prefixos de nom d'interfície de xarxa que s'han de controlar per a les estadístiques d'amplada de banda." + }, + "stats": { + "label": "Estadístiques del sistema", + "description": "Opcions per a habilitar/desactivar la col·lecció de diverses estadístiques de sistemes i GPU.", + "amd_gpu_stats": { + "label": "Estadístiques de GPU AMD", + "description": "Habilita la col·lecció d'estadístiques de GPU AMD si hi ha una GPU AMD present." + }, + "intel_gpu_stats": { + "label": "Estadístiques de la GPU d'Intel", + "description": "Habilita la col·lecció d'estadístiques de GPU d'Intel si hi ha una GPU d'Intel." + }, + "network_bandwidth": { + "label": "Amplada de banda de la xarxa", + "description": "Habilita el monitoratge d'amplada de banda per procés per als processos i detectors de ffmpeg de càmera (requereix capacitats)." + }, + "intel_gpu_device": { + "label": "Dispositiu GPU d'Intel", + "description": "Adreça de bus PCI o camí del dispositiu DRM (p. ex. /dev/dri/card1) utilitzat per fixar les estadístiques de GPU d'Intel a un dispositiu específic quan hi ha múltiples." + } + }, + "version_check": { + "label": "Comprovació de versió", + "description": "Activa una comprovació de sortida per detectar si hi ha disponible una versió de Frigate més nova." + } + }, + "tls": { + "label": "TLS", + "description": "Configuració de TLS per als punts finals web de Frigate (port 8971).", + "enabled": { + "label": "Habilita TLS", + "description": "Activa TLS per a la interfície d'usuari web i l'API de Frigate al port TLS configurat." + } + }, + "detectors": { + "label": "Detector de hardware", + "description": "Configuració per a detectors d'objectes (CPU, GPU, dorsals ONNX) i qualsevol configuració de model específica per a detectors.", + "type": { + "label": "Tipus", + "description": "Tipus de detector a utilitzar per a la detecció d'objectes (per exemple 'cpu', 'edgetpu', 'openvino')." + }, + "cpu": { + "label": "CPU", + "description": "Detector TFLite de CPU que executa els models TensorFlow Lite a la CPU de l'amfitrió sense acceleració de maquinari. No recomanat.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Mapeig d'etiquetes d'objecte a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple, 'cotxe' -> ['matrícula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "num_threads": { + "label": "Nombre de fils de detecció", + "description": "El nombre de fils utilitzats per a la inferència basada en CPU." + } + }, + "deepstack": { + "label": "DeepStack", + "description": "Detector DeepStack/CodeProject.AI que envia imatges a una API HTTP de DeepStack remota per a la inferència. No recomanat.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Camí personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Camí a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple, 'cotxe' -> ['matrícula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "api_url": { + "label": "URL de l'API del DeepStack", + "description": "L'URL de l'API de DeepStack." + }, + "api_timeout": { + "label": "Temps d'espera de l'API DeepStack (en segons)", + "description": "Temps màxim permès per a una sol·licitud de l'API DeepStack." + }, + "api_key": { + "label": "Clau API del DeepStack (si es requereix)", + "description": "Clau API opcional per als serveis DeepStack autenticats." + } + }, + "degirum": { + "label": "DeGirum", + "description": "Detector DeGirum per a l'execució de models a través del núvol DeGirum o serveis d'inferència locals.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple, 'cotxe' -> ['matrícula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "location": { + "label": "Ubicació de la referència", + "description": "Ubicació del motor d'inferència DeGirim (p. ex. ',cloud', '127.0.0.1')." + }, + "zoo": { + "label": "Model Zoo", + "description": "Camí o URL al zoològic del model zoo." + }, + "token": { + "label": "Token del cloud de DeGirum", + "description": "Token d'accés al cloud de DeGirum." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "Detector EdgeTPU que executa models TensorFlow Lite compilats per a Coral EdgeTPU utilitzant el delegat EdgeTPU.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' -> ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "device": { + "label": "Tipus de dispositiu", + "description": "El dispositiu a utilitzar per a la inferència EdgeTPU (p. ex. «usb», «pci»)." + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "Detector Hailo-8/Hailo-8L utilitzant models HEF i el HailoRT SDK per inferència en maquinari Hailo.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' -> ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "device": { + "label": "Tipus de dispositiu", + "description": "El dispositiu a utilitzar per a la inferència Hailo (p. ex. 'PCIe', 'M.2')." + } + }, + "memryx": { + "label": "MemryX", + "description": "Detector MemryX MX3 que executa models DFP compilats en acceleradors MemryX.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' -> ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Camí del model específic del detector", + "description": "Camí de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "device": { + "label": "Camí del dispositiu", + "description": "El dispositiu a utilitzar per a la inferència MemryX (p. ex. «PCIe»)." + } + }, + "onnx": { + "description": "Detector ONNX per executar models ONNX; utilitzarà els dorsals d'acceleració disponibles (CUDA/ROCm/OpenVINO) quan estigui disponible.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' -> ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple 'float32')." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "device": { + "label": "Tipus de dispositiu", + "description": "El dispositiu a utilitzar per a la inferència ONNX (p. ex. «AUTO», «CPU», «GPU»)." + }, + "label": "ONNX" + }, + "openvino": { + "description": "Detector OpenVINO per a CPU AMD i Intel, GPUs Intel i maquinari Intel VPU.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Rutaa un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' ->. ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "device": { + "label": "Tipus de dispositiu", + "description": "El dispositiu a utilitzar per a la inferència OpenVINO (p. ex. 'CPU', 'GPU', 'NPU')." + }, + "label": "OpenVINO" + }, + "rknn": { + "description": "El detector RKNN per a Rockchip NPUs; executa models RKNN compilats en maquinari Rockchip.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' ->. ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "num_cores": { + "label": "Nombre de nuclis NPU a utilitzar.", + "description": "El nombre de nuclis NPU a usar (0 per a l'automàtic)." + }, + "label": "RKNN" + }, + "synaptics": { + "label": "Sinapsi", + "description": "Detector NPU Synaptics per a models en format .synap utilitzant el Synap SDK en maquinari Synaptics.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)" + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' ->. ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + } + }, + "teflon_tfl": { + "description": "Detector delegat de Teflon per a TFLite utilitzant la biblioteca delegat de Mesa Teflon per accelerar la inferència en GPU compatibles.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' ->. ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Cami de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "label": "Teflon" + }, + "tensorrt": { + "description": "Detector TensorRT per a dispositius Nvidia Jetson utilitzant motors TensorRT serialitzats per a la inferència accelerada.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' ->. ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "device": { + "label": "Índex del dispositiu GPU", + "description": "L'índex del dispositiu GPU a utilitzar." + }, + "label": "TensorRT" + }, + "zmq": { + "description": "Detector ZMQ IPC que descarrega la inferència a un procés extern a través d'un extrem IPC ZeroMQ.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Ruta personalitzat del model de detecció d'objectes", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' ->. ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Ruta del model específic del detector", + "description": "Ruta de fitxer al binari del model de detector si el detector escollit ho requereix." + }, + "endpoint": { + "label": "Final ZMQ IPC", + "description": "L'extrem ZMQ al qual connectar-se." + }, + "request_timeout_ms": { + "label": "Temps d'espera de la sol·licitud ZMQ en mil·lisegons", + "description": "Temps d'espera per a les sol·licituds ZMQ en mil·lisegons." + }, + "linger_ms": { + "label": "Socket ZMQ roman en mil·lisegons", + "description": "Període de permanència del socket en mil·lisegons." + }, + "label": "ZMQ IPC" + }, + "axengine": { + "label": "AXEngine NPU", + "description": "Detector AXERA AX650N/AX8850N NPU executant fitxers .axmodel compilats a través del temps d'execució d'AXEngine.", + "type": { + "label": "Tipus" + }, + "model": { + "label": "Configuració del model específic del detector", + "description": "Opcions de configuració del model específic del detector (camí, mida d'entrada, etc.).", + "path": { + "label": "Camí personalitzat del model de detecció d'objectes", + "description": "Camí a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Camí a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'cotxe' -). ['matrícula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple 'float32')." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "model_path": { + "label": "Camí del model específic del detector", + "description": "Camí de fitxer al binari del model de detector si el detector escollit ho requereix." + } + }, + "model": { + "label": "Configuració de model de detector específic", + "description": "Opcions de configuració de model de detector específic (ruta, tamany entrada, etc.).", + "path": { + "label": "Ruta del model de detector d'objectes personalitzat", + "description": "Ruta a l'arxiu del model de detecció personalitzat ( o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Etiqueta per a detector d'objectes personalitzat", + "description": "Ruta a l'arxiu d'etiqueta que mapeja les classes numériques a etiquetes per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objecte", + "description": "Amplada de l'entrada del model en píxels." + }, + "height": { + "label": "Entrada de l'altura del model de detecció d'objecte", + "description": "Altura de l'entrada del model en píxels." + }, + "labelmap": { + "label": "Personlització d'etiquetes", + "description": "Sobreescriu o remapeja entrades per fusionar a l'estandar d'etiquetes." + }, + "attributes_map": { + "label": "Mapeja d'etiquetes d'objecte a la seva etiqueta", + "description": "Mapeja des de les etiquetes d'objectes als seus atributs usats per anexar metadades (per exemple 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Model d'entrada de forma de tensor", + "description": "El format del tensor experat per el model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Entrada del format de píxel del model", + "description": "Espai-color del píxel experat per el model: 'rgb', 'bgr', o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D entrada del model", + "description": "tipus de dada per al model de tensor (per exemple 'float32')." + }, + "model_type": { + "label": "Tipus de Model de detecció d'objecte", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) usat per l'optimització d'alguns detectors." + } + }, + "model_path": { + "label": "Ruta a model de detector específic", + "description": "Ruta a l'arxiu al model binari de detector si es requerit per al detector seleccionat." + } + }, + "model": { + "label": "Model de detecció", + "description": "Configuració per a configurar un model de detecció d'objectes personalitzat i la seva forma d'entrada.", + "path": { + "label": "Ruta del model de detector d'objectes personalitzat", + "description": "Ruta a un fitxer de model de detecció personalitzat (o plus:// per a models Frigate+)." + }, + "labelmap_path": { + "label": "Mapa d'etiquetes per al detector d'objectes personalitzat", + "description": "Ruta a un fitxer de mapa d'etiquetes que assigna classes numèriques a etiquetes de cadena per al detector." + }, + "width": { + "label": "Amplada d'entrada del model de detecció d'objectes", + "description": "Amplada del tensor d'entrada del model en píxels." + }, + "height": { + "label": "Alçada d'entrada del model de detecció d'objectes", + "description": "Alçada del tensor d'entrada del model en píxels." + }, + "labelmap": { + "label": "Personalització del mapa d'etiquetes", + "description": "Sobreescriu o reassigna les entrades per a fusionar-se en el mapa d'etiquetes estàndard." + }, + "attributes_map": { + "label": "Mapa d'etiquetes d'objectes a les seves etiquetes d'atribut", + "description": "Assignació des d'etiquetes d'objectes a etiquetes d'atribut utilitzades per adjuntar metadades (per exemple 'car' ->. ['matricula'])." + }, + "input_tensor": { + "label": "Forma del sensor d'entrada del model", + "description": "Format del sensor esperat pel model: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Format de color del píxel d'entrada del model", + "description": "Espai de color del píxel esperat pel model: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipus D d'entrada del model", + "description": "Tipus de dades del tensor d'entrada del model (per exemple «float32»)." + }, + "model_type": { + "label": "Tipus de model de detecció d'objectes", + "description": "Tipus d'arquitectura del model de detector (ssd, yolox, yolonas) utilitzat per alguns detectors per a l'optimització." + } + }, + "genai": { + "label": "Configuració de la IA generada", + "description": "Paràmetres per als proveïdors integrats generatius d'IA utilitzats per generar descripcions d'objectes i resums de revisions.", + "api_key": { + "label": "Clau API", + "description": "Clau API requerida per alguns proveïdors (també es pot establir a través de variables d'entorn)." + }, + "base_url": { + "label": "URL base", + "description": "URL base per a proveïdors allotjats o compatibles (per exemple, una instància d'Ollama)." + }, + "model": { + "description": "El model a utilitzar del proveïdor per generar descripcions o resums.", + "label": "Model" + }, + "provider": { + "label": "Proveïdor", + "description": "El proveïdor GenAI a utilitzar (per exemple: ollama, gemini, openai)." + }, + "roles": { + "label": "Rols", + "description": "Rols de GenAI (xat, descripcions, incrustacions); un proveïdor per rol." + }, + "provider_options": { + "label": "Opcions del proveïdor", + "description": "Opcions addicionals específiques del proveïdor per passar al client GenAI." + }, + "runtime_options": { + "label": "Opcions de temps d'execució", + "description": "Les opcions d'execució passades al proveïdor per a cada crida d'inferència." + } + }, + "audio": { + "label": "Detecció d'àudio", + "description": "Configuració per a la detecció d'esdeveniments basats en àudio per a totes les càmeres; es pot substituir per càmera.", + "enabled": { + "label": "Habilita la detecció d'àudio", + "description": "Activa o desactiva la detecció d'esdeveniments d'àudio per a totes les càmeres; es pot substituir per 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, 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.", + "threshold": { + "label": "Confiança mínima de l'àudio", + "description": "Llindar mínim de confiança per a l'esdeveniment d'àudio a comptar." + } + }, + "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." + } + }, + "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'." + }, + "restream": { + "label": "Restream RTSP", + "description": "Torna a transmetre la sortida Birdseye com a font RTSP; habilitant això es mantindrà Birdseye funcionant contínuament." + }, + "width": { + "label": "Amplada", + "description": "Amplada de sortida (píxels) del marc Birdseye compost." + }, + "height": { + "label": "Alçada", + "description": "Alçada de sortida (píxels) del marc Birdseye compost." + }, + "quality": { + "label": "Qualitat de la codificació", + "description": "Qualitat de codificació per a l'alimentació Birdseye mpeg1 (1 qualitat més alta, 31 més baixa)." + }, + "inactivity_threshold": { + "label": "Llindar d'inactivitat", + "description": "Segons d'inactivitat després de la qual una càmera deixarà de mostrar-se a Birdseye." + }, + "layout": { + "label": "Disposició", + "description": "Opcions de disposició per a la composició Birdseye.", + "scaling_factor": { + "label": "Factor d'escalat", + "description": "Factor d'escalat utilitzat per la calculadora de disposició (interval 1.0 a 5.0)." + }, + "max_cameras": { + "label": "Màxim de càmeres", + "description": "Nombre màxim de càmeres a mostrar alhora a Birdseye; mostra les càmeres més recents." + } + }, + "idle_heartbeat_fps": { + "label": "FPS de batec cardíac inactiu", + "description": "Fotogrames per segon per a tornar a enviar l'últim fotograma compost Birdseye quan estigui inactiu; establert a 0 per a desactivar." + }, + "order": { + "label": "Posició", + "description": "Posició numèrica que controla l'ordenació de la càmera en la disposició Birdseye." + }, + "label": "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": "Habilita la detecció d'objectes", + "description": "Activa o desactiva la detecció d'objectes per a totes les càmeres; es pot sobreescriure per càmera." + }, + "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." + } + }, + "classification": { + "label": "Classificació de l'objecte", + "description": "Paràmetres per als models de classificació utilitzats per refinar les etiquetes dels objectes o la classificació de l'estat.", + "bird": { + "label": "Configuració de la classificació dels ocells", + "description": "Paràmetres específics dels models de classificació d'aus.", + "enabled": { + "label": "Classificació dels ocells", + "description": "Activa o desactiva la classificació d'ocells." + }, + "threshold": { + "label": "Puntuació mínima", + "description": "Puntuació mínima requerida per acceptar una classificació d'ocells." + } + }, + "custom": { + "label": "Models de classificació personalitzats", + "description": "Configuració per a models de classificació personalitzats utilitzats per a objectes o detecció d'estats.", + "enabled": { + "label": "Habilita el model", + "description": "Activa o desactiva el model de classificació personalitzat." + }, + "name": { + "label": "Nom del model", + "description": "Identificador per al model de classificació personalitzat a utilitzar." + }, + "threshold": { + "label": "Llindar de puntuació", + "description": "Llindar de puntuació utilitzat per a canviar l'estat de classificació." + }, + "save_attempts": { + "label": "Desa els intents", + "description": "Quants intents de classificació s'han de desar per a les classificacions recents de la interfície d'usuari." + }, + "object_config": { + "objects": { + "label": "Classifica els objectes", + "description": "Llista de tipus d'objectes on executar la classificació d'objectes." + }, + "classification_type": { + "label": "Tipus de classificació", + "description": "Tipus de classificació aplicat: 'sub.label' (afegeix sub.label) o altres tipus admesos." + } + }, + "state_config": { + "cameras": { + "label": "Càmeres de classificació", + "description": "Retalla per càmera i configuració per executar la classificació d'estat.", + "crop": { + "label": "Retalla la classificació", + "description": "Retalla les coordenades a usar per a executar la classificació en aquesta càmera." + } + }, + "motion": { + "label": "Executa en moviment", + "description": "Si és cert, executeu la classificació quan es detecti el moviment dins del retall especificat." + }, + "interval": { + "label": "Interval de classificació", + "description": "Interval (segons) entre les classificacions periòdiques per a la classificació estatal." + } + } + } + }, + "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 el reconeixement de la matrícula per a totes les càmeres; es pot substituir per la càmera." + }, + "model_size": { + "label": "Mida del model", + "description": "Mida del model utilitzat per a la detecció/reconeixement del text. La majoria d'usuaris haurien d'utilitzar 'petits'." + }, + "detection_threshold": { + "label": "Llindar de detecció", + "description": "Llindar de confiança de detecció per començar a executar OCR en una placa sospitosa." + }, + "min_area": { + "label": "Àrea mínima de la placa", + "description": "Àrea mínima de placa (píxels) necessària per intentar el reconeixement." + }, + "recognition_threshold": { + "label": "Llindar de reconeixement", + "description": "Es requereix un llindar de confiança perquè el text de la placa reconeguda s'adjunti com a subetiqueta." + }, + "min_plate_length": { + "label": "Longitud mínima de la placa", + "description": "El nombre mínim de caràcters que ha de contenir una placa reconeguda ha de ser considerat vàlid." + }, + "format": { + "label": "Format regex de la matrícula", + "description": "Expressió regular opcional per a validar les cadenes de placa reconegudes contra un format esperat." + }, + "match_distance": { + "label": "Distància de la coincidència", + "description": "Nombre de discrepàncies de caràcters permesos en comparar les plaques detectades amb les plaques conegudes." + }, + "known_plates": { + "label": "Matricules conegudes", + "description": "Llista de plaques o expressions regulars per fer un seguiment o una alerta especialment activades." + }, + "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ó." + }, + "debug_save_plates": { + "label": "Desa les plaques de depuració", + "description": "Desa les imatges retallades de la matrícula per a depurar el rendiment LPR." + }, + "device": { + "label": "Dispositiu", + "description": "Això és una sobreescriptura, per dirigir-se a un dispositiu específic. Vegeu https://onnxruntime.ai/docs/execution-providers/ per a més informació" + }, + "replace_rules": { + "label": "Regles de reemplaçament", + "description": "Regex regles de reemplaçament usades per a normalitzar les cadenes de placa detectades abans de coincidir.", + "pattern": { + "label": "Patró Regex" + }, + "replacement": { + "label": "Cadena de reemplaçament" + } + }, + "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)." + } + }, + "camera_groups": { + "label": "Grups de càmera", + "description": "Configuració dels grups de càmeres amb nom utilitzats per a organitzar càmeres a la interfície d'usuari.", + "cameras": { + "label": "Llista de càmeres", + "description": "Matriu de noms de càmera inclosos en aquest grup." + }, + "icon": { + "label": "Icona de grup", + "description": "Icona utilitzada per a representar el grup de càmeres a la interfície d'usuari." + }, + "order": { + "label": "Ordre d'ordenació", + "description": "Ordre numèric utilitzat per ordenar els grups de càmera a la interfície d'usuari; els nombres més grans apareixen més tard." + } + }, + "camera_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 un contenidor", + "description": "Dibuixa caixes contenidores en imatges publicades sobre MQTT." + }, + "crop": { + "label": "Retalla la imatge", + "description": "Retalla les imatges publicades a MQTT segons el quadre de delimitació 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)." + } + }, + "camera_ui": { + "label": "Interfície 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 la interfície", + "description": "Estableix si aquesta càmera és visible a tot arreu a la interfície d'usuari de Frigate. Desactivar això requerirà editar manualment la configuració per tornar a veure aquesta càmera a la interfície d'usuari." + }, + "review": { + "label": "Mostra en la revisió", + "description": "Alterna si aquesta càmera és visible a la revisió (la pàgina de revisió i el seu filtre de càmera, la revisió de moviment i la vista de l'historial)." + } + }, + "profiles": { + "label": "Perfils", + "description": "Definicions de perfil amb nom amigable. Els perfils de la càmera han de fer referència als noms definits aquí.", + "friendly_name": { + "label": "Nom amistós", + "description": "Mostra el nom d'aquest perfil que es mostra a la interfície d'usuari." + } + }, + "active_profile": { + "label": "Perfil actiu", + "description": "Nom de perfil actualment actiu. Només en temps d'execució, no ha persistit en YAML." + } +} diff --git a/web/public/locales/ca/config/groups.json b/web/public/locales/ca/config/groups.json new file mode 100644 index 0000000..a8282ec --- /dev/null +++ b/web/public/locales/ca/config/groups.json @@ -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" + } + } +} diff --git a/web/public/locales/ca/config/validation.json b/web/public/locales/ca/config/validation.json new file mode 100644 index 0000000..b087d5a --- /dev/null +++ b/web/public/locales/ca/config/validation.json @@ -0,0 +1,35 @@ +{ + "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." + } + }, + "detect": { + "dimensionMustBeEven": "Ha de ser un nombre parell." + } +} diff --git a/web/public/locales/ca/objects.json b/web/public/locales/ca/objects.json new file mode 100644 index 0000000..17378df --- /dev/null +++ b/web/public/locales/ca/objects.json @@ -0,0 +1,130 @@ +{ + "person": "Persona", + "bicycle": "Bicicleta", + "car": "Cotxe", + "motorcycle": "Motocicleta", + "airplane": "Avió", + "boat": "Vaixell", + "traffic_light": "Llum del trànsit", + "fire_hydrant": "Boca d'incendi", + "street_sign": "Senyal de trànsit", + "stop_sign": "Senyal de stop", + "parking_meter": "Parquímetre", + "bench": "Banc", + "bird": "Ocell", + "cat": "Gat", + "dog": "Gos", + "horse": "Cavall", + "sheep": "Ovella", + "cow": "Vaca", + "elephant": "Elefant", + "bear": "Ós", + "zebra": "Zebra", + "giraffe": "Girafa", + "hat": "Barret", + "backpack": "Motxilla", + "umbrella": "Paraigües", + "shoe": "Sabata", + "eye_glasses": "Ulleres", + "tie": "Corbata", + "suitcase": "Maleta", + "frisbee": "Frisbee", + "skis": "Esquís", + "snowboard": "Snowboard", + "sports_ball": "Pilota d'esports", + "kite": "Estel", + "baseball_bat": "Bat de beisbol", + "baseball_glove": "Guant de beisbol", + "skateboard": "Monopatí", + "surfboard": "Taula de surf", + "tennis_racket": "Raqueta de tenis", + "bottle": "Ampolla", + "plate": "Matrícula", + "wine_glass": "Got de vi", + "cup": "Copa", + "fork": "Forquilla", + "knife": "Ganivet", + "spoon": "Cullera", + "bowl": "Bol", + "apple": "Poma", + "sandwich": "Sandvitx", + "orange": "Taronja", + "broccoli": "Bròquil", + "carrot": "Pastanaga", + "hot_dog": "Frankfurt", + "pizza": "Pizza", + "donut": "Dònut", + "cake": "Pastís", + "chair": "Cadira", + "couch": "Sofà", + "potted_plant": "Planta en test", + "bed": "Llit", + "mirror": "Mirall", + "dining_table": "Taula de menjador", + "window": "Finestra", + "desk": "Escriptori", + "toilet": "Bany", + "door": "Porta", + "tv": "TV", + "mouse": "Ratolí", + "remote": "Comandament", + "keyboard": "Teclat", + "cell_phone": "Telèfon mòbil", + "microwave": "Microones", + "oven": "Forn", + "toaster": "Torradora", + "sink": "Aigüera", + "refrigerator": "Nevera", + "blender": "Batedora", + "book": "Llibre", + "clock": "Rellotge", + "vase": "Gerro", + "scissors": "Tisores", + "teddy_bear": "Ós de peluix", + "hair_dryer": "Assecador de cabell", + "toothbrush": "Raspall de dents", + "hair_brush": "Raspall de cabell", + "vehicle": "Vehicle", + "squirrel": "Esquirol", + "deer": "Cérvol", + "animal": "Animal", + "bark": "Escorça", + "fox": "Guineu", + "goat": "Cabra", + "rabbit": "Conill", + "raccoon": "Ós rentador", + "robot_lawnmower": "Robot tallagespa", + "handbag": "Bossa de mà", + "banana": "Plàtan", + "train": "Tren", + "bus": "Autobús", + "laptop": "Portàtil", + "waste_bin": "Paperera", + "face": "Cara", + "on_demand": "Sota demanda", + "license_plate": "Matrícula", + "package": "Paquet", + "bbq_grill": "Barbacoa", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "dpd": "DPD", + "gls": "GLS", + "canada_post": "Canada Post", + "royal_mail": "Royal Mail", + "school_bus": "Bus escolar", + "skunk": "Mofeta", + "kangaroo": "Cangur", + "baby": "Nadó", + "baby_stroller": "Cotxet", + "rickshaw": "Ricksaw", + "Rodent": "Rosegador", + "rodent": "Rosegador" +} diff --git a/web/public/locales/ca/views/chat.json b/web/public/locales/ca/views/chat.json new file mode 100644 index 0000000..cb618ce --- /dev/null +++ b/web/public/locales/ca/views/chat.json @@ -0,0 +1,72 @@ +{ + "documentTitle": "Xat - Frigate", + "title": "Xat Frigate", + "subtitle": "El teu assistent d'AI per a gestionar càmeres i coneixements", + "placeholder": "Pregunta qualsevol cosa...", + "error": "Alguna cosa ha fallat. Torna-ho a provar.", + "processing": "Processant...", + "toolsUsed": "Usades: {{tools}}", + "showTools": "Mostra eines ({{count}})", + "hideTools": "Amaga eines", + "call": "Truca", + "result": "Resultat", + "arguments": "Variables:", + "response": "Resposta:", + "attachment_chip_label": "{{label}} a {{camera}}", + "attachment_chip_remove": "Elimina l'adjunt", + "open_in_explore": "Obre en l'explorador", + "attach_event_aria": "Adjunta l'esdeveniment {{eventId}}", + "attachment_picker_paste_label": "O enganxa l'ID de l'esdeveniment", + "attachment_picker_attach": "Adjunta", + "attachment_picker_placeholder": "Adjunta un esdeveniment", + "quick_reply_find_similar": "Troba albiraments similars", + "quick_reply_tell_me_more": "Explica'm més sobre això", + "quick_reply_when_else": "Quan més es va veure?", + "quick_reply_find_similar_text": "Troba albiraments similars a això.", + "quick_reply_tell_me_more_text": "Parla'm més d'aquest.", + "quick_reply_when_else_text": "Quan més es va veure això?", + "anchor": "Referència", + "similarity_score": "Similitud", + "no_similar_objects_found": "No s'ha trobat cap objecte similar.", + "semantic_search_required": "La cerca semàntica ha d'estar habilitada per trobar objectes similars.", + "send": "Envia", + "suggested_requests": "Proveu de preguntar:", + "starting_requests": { + "show_recent_events": "Mostra els esdeveniments recents", + "show_camera_status": "Mostra l'estat de la càmera", + "recap": "Què va passar mentre jo era fora?", + "watch_camera": "Observa una càmera per a l'activitat" + }, + "starting_requests_prompts": { + "show_recent_events": "Mostra'm els esdeveniments recents de l'última hora", + "show_camera_status": "Quin és l'estat actual de les meves càmeres?", + "recap": "Què va passar mentre jo era fora?", + "watch_camera": "Vigila la porta d'entrada i fes-me saber si algú apareix" + }, + "new_chat": "Xat nou", + "settings": { + "title": "Configuració del xat", + "show_stats": { + "title": "Mostra les estadístiques", + "desc": "Mostra la velocitat de generació i la mida del context per a les respostes del xat.", + "while_generating": "En generar", + "always": "Sempre" + }, + "auto_scroll": { + "title": "Desplaçament automàtic", + "desc": "Segueix els missatges nous a mesura que arriben." + } + }, + "stats": { + "context": "{{tokens}} tokens", + "tokens_per_second": "{{rate}} t/s" + }, + "reasoning": { + "active": "Raonant…", + "show": "Mostra el raonament", + "hide": "Amaga el raonament" + }, + "thinking": { + "toggle": "Commuta el pensament" + } +} diff --git a/web/public/locales/ca/views/classificationModel.json b/web/public/locales/ca/views/classificationModel.json new file mode 100644 index 0000000..e683939 --- /dev/null +++ b/web/public/locales/ca/views/classificationModel.json @@ -0,0 +1,206 @@ +{ + "documentTitle": "Models de classificació - Frigate", + "button": { + "deleteClassificationAttempts": "Suprimeix les imatges de classificació", + "renameCategory": "Reanomena la classe", + "deleteCategory": "Suprimeix la classe", + "deleteImages": "Suprimeix les imatges", + "trainModel": "Model de tren", + "addClassification": "Afegeix una classificació", + "deleteModels": "Suprimeix els models", + "editModel": "Edita el model" + }, + "toast": { + "success": { + "deletedCategory_one": "S'ha suprimit la classe {{count}}", + "deletedCategory_many": "S'han suprimit {{count}} classes", + "deletedCategory_other": "S'han suprimit {{count}} classes", + "deletedImage_one": "Imatge eliminada {{count}}", + "deletedImage_many": "S'han suprimit {{count}} imatges", + "deletedImage_other": "S'han suprimit {{count}} imatges", + "categorizedImage": "Imatge classificada amb èxit", + "trainedModel": "Model entrenat amb èxit.", + "trainingModel": "S'ha iniciat amb èxit la formació de models.", + "deletedModel_one": "S'ha suprimit correctament {{count}} model", + "deletedModel_many": "S'han suprimit correctament els {{count}} models", + "deletedModel_other": "S'han suprimit correctament els {{count}} models", + "updatedModel": "S'ha actualitzat correctament la configuració del model", + "renamedCategory": "S'ha canviat el nom de la classe a {{name}}", + "reclassifiedImage": "Imatge reclassificada amb èxit" + }, + "error": { + "deleteImageFailed": "No s'ha pogut suprimir: {{errorMessage}}", + "deleteCategoryFailed": "No s'ha pogut suprimir la classe: {{errorMessage}}", + "categorizeFailed": "No s'ha pogut categoritzar la imatge: {{errorMessage}}", + "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}}", + "updateModelFailed": "No s'ha pogut actualitzar el model: {{errorMessage}}", + "renameCategoryFailed": "No s'ha pogut canviar el nom de la classe: {{errorMessage}}", + "trainingFailedToStart": "Errar en arrencar l'entrenament del model: {{errorMessage}}", + "reclassifyFailed": "No s'ha pogut reclassificar la imatge: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Suprimeix la classe", + "desc": "Esteu segur que voleu suprimir la classe {{name}}? Això suprimirà permanentment totes les imatges associades i requerirà tornar a entrenar el model.", + "minClassesTitle": "No es pot suprimir la classe", + "minClassesDesc": "Un model de classificació ha de tenir almenys 2 classes. Afegeix una altra classe abans d'eliminar aquesta." + }, + "deleteDatasetImages": { + "title": "Suprimeix les imatges del conjunt de dades", + "desc_one": "Esteu segur que voleu suprimir {{count}} imatge de {{dataset}}? Aquesta acció no es pot desfer i requerirà tornar a entrenar el model.", + "desc_many": "Esteu segur que voleu suprimir {{count}} imatges de {{dataset}}? Aquesta acció no es pot desfer i requerirà tornar a entrenar el model.", + "desc_other": "Esteu segur que voleu suprimir {{count}} imatges de {{dataset}}? Aquesta acció no es pot desfer i requerirà tornar a entrenar el model." + }, + "deleteTrainImages": { + "title": "Suprimeix les imatges del tren", + "desc_one": "Esteu segur que voleu suprimir {{count}} imatge? Aquesta acció no es pot desfer.", + "desc_many": "Esteu segur que voleu suprimir {{count}} imatges? Aquesta acció no es pot desfer.", + "desc_other": "Esteu segur que voleu suprimir {{count}} imatges? Aquesta acció no es pot desfer." + }, + "renameCategory": { + "title": "Reanomena la classe", + "desc": "Introduïu un nom nou per {{name}}. Se us requerirà que torneu a entrenar el model per al canvi de nom afectar." + }, + "description": { + "invalidName": "Nom no vàlid. Els noms només poden incloure lletres, números, espais, apòstrofs, guions baixos i guions." + }, + "train": { + "title": "Classificacions recents", + "aria": "Selecciona les classificacions recents", + "titleShort": "Recent" + }, + "categories": "Classes", + "createCategory": { + "new": "Crea una classe nova" + }, + "categorizeImageAs": "Classifica la imatge com a:", + "categorizeImage": "Classifica la imatge", + "noModels": { + "object": { + "title": "No hi ha models de classificació d'objectes", + "description": "Crea un model personalitzat per classificar els objectes detectats.", + "buttonText": "Crea un model d'objecte" + }, + "state": { + "title": "Cap model de classificació d'estat", + "description": "Crea un model personalitzat per a monitoritzar i classificar els canvis d'estat en àrees de càmera específiques.", + "buttonText": "Crea un model d'estat" + } + }, + "wizard": { + "title": "Crea una classificació nova", + "steps": { + "nameAndDefine": "Nom i definició", + "stateArea": "Àrea estatal", + "chooseExamples": "Trieu exemples" + }, + "step1": { + "description": "Els models estatals monitoritzen àrees de càmera fixes per als canvis (p. ex., porta oberta/tancada). Els models d'objectes afegeixen classificacions als objectes detectats (per exemple, animals coneguts, persones de lliurament, etc.).", + "name": "Nom", + "namePlaceholder": "Introduïu el nom del model...", + "type": "Tipus", + "typeState": "Estat", + "typeObject": "Objecte", + "objectLabel": "Etiqueta de l'objecte", + "objectLabelPlaceholder": "Selecciona el tipus d'objecte...", + "classificationType": "Tipus de classificació", + "classificationTypeTip": "Apreneu sobre els tipus de classificació", + "classificationTypeDesc": "Les subetiquetes afegeixen text addicional a l'etiqueta de l'objecte (p. ex., 'Person: UPS'). Els atributs són metadades cercables emmagatzemades per separat a les metadades de l'objecte.", + "classificationSubLabel": "Subetiqueta", + "classificationAttribute": "Atribut", + "classes": "Classes", + "classesTip": "Aprèn sobre les classes", + "classesStateDesc": "Defineix els diferents estats en què pot estar la teva àrea de càmera. Per exemple: \"obert\" i \"tancat\" per a una porta de garatge.", + "classesObjectDesc": "Defineix les diferents categories en què classificar els objectes detectats. Per exemple: 'lliuramentpersonpersona', 'resident', 'amenaça' per a la classificació de persones.", + "classPlaceholder": "Introduïu el nom de la classe...", + "errors": { + "nameRequired": "Es requereix el nom del model", + "nameLength": "El nom del model ha de tenir 64 caràcters o menys", + "nameOnlyNumbers": "El nom del model no pot contenir només números", + "classRequired": "Es requereix com a mínim 1 classe", + "classesUnique": "Els noms de classe han de ser únics", + "stateRequiresTwoClasses": "Els models d'estat requereixen almenys 2 classes", + "objectLabelRequired": "Seleccioneu una etiqueta d'objecte", + "objectTypeRequired": "Seleccioneu un tipus de classificació", + "noneNotAllowed": "La classe 'none' no està permesa" + }, + "states": "Estats" + }, + "step2": { + "description": "Seleccioneu les càmeres i definiu l'àrea a monitoritzar per a cada càmera. El model classificarà l'estat d'aquestes àrees.", + "cameras": "Càmeres", + "selectCamera": "Selecciona la càmera", + "noCameras": "Feu clic a + per a afegir càmeres", + "selectCameraPrompt": "Seleccioneu una càmera de la llista per definir la seva àrea de monitoratge" + }, + "step3": { + "selectImagesPrompt": "Selecciona totes les imatges amb: {{className}}", + "selectImagesDescription": "Feu clic a les imatges per a seleccionar-les. Feu clic a Continua quan hàgiu acabat amb aquesta classe.", + "generating": { + "title": "S'estan generant imatges de mostra", + "description": "Frigate està traient imatges representatives dels vostres enregistraments. Això pot trigar un moment..." + }, + "training": { + "title": "Model d'entrenament", + "description": "El teu model s'està entrenant en segon pla. Tanqueu aquest diàleg i el vostre model començarà a funcionar tan aviat com s'hagi completat l'entrenament." + }, + "retryGenerate": "Torna a provar la generació", + "noImages": "No s'ha generat cap imatge de mostra", + "classifying": "Classificació i formació...", + "trainingStarted": "L'entrenament s'ha iniciat amb èxit", + "errors": { + "noCameras": "No s'ha configurat cap càmera", + "noObjectLabel": "No s'ha seleccionat cap etiqueta d'objecte", + "generateFailed": "No s'han pogut generar exemples: {{error}}", + "generationFailed": "Ha fallat la generació. Torneu-ho a provar.", + "classifyFailed": "No s'han pogut classificar les imatges: {{error}}" + }, + "generateSuccess": "Imatges de mostra generades amb èxit", + "allImagesRequired_one": "Classifiqueu totes les imatges. Queda {{count}} imatge.", + "allImagesRequired_many": "Classifiqueu totes les imatges. Queden {{count}} imatges.", + "allImagesRequired_other": "Classifiqueu totes les imatges. Queden {{count}} imatges.", + "modelCreated": "El model s'ha creat correctament. Utilitzeu la vista Classificacions recents per a afegir imatges per als estats que falten i, a continuació, entrenar el model.", + "missingStatesWarning": { + "title": "Falten exemples de classe", + "description": "No totes les classes tenen exemples. Proveu de generar nous exemples per a trobar la classe que falta, o continueu i utilitzeu la vista Classificacions recents per a afegir imatges més tard." + }, + "refreshExamples": "Genera nous exemples", + "refreshConfirm": { + "title": "Voleu generar exemples nous?", + "description": "Això generarà un nou conjunt d'imatges i netejarà totes les seleccions, incloses les classes anteriors. Haureu de tornar a seleccionar exemples per a totes les classes." + } + } + }, + "deleteModel": { + "title": "Suprimeix el model de classificació", + "single": "Esteu segur que voleu suprimir {{name}}? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer.", + "desc_one": "Esteu segur que voleu suprimir el model {{count}}? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer.", + "desc_many": "Esteu segur que voleu suprimir {{count}} models? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer.", + "desc_other": "Esteu segur que voleu suprimir {{count}} models? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer." + }, + "menu": { + "objects": "Objectes", + "states": "Estats" + }, + "details": { + "scoreInfo": "La puntuació representa la confiança mitjana de la classificació en totes les deteccions d'aquest objecte.", + "none": "Cap", + "unknown": "Desconegut" + }, + "edit": { + "title": "Edita el model de classificació", + "descriptionState": "Edita les classes per a aquest model de classificació d'estats. Els canvis requeriran tornar a entrenar el model.", + "descriptionObject": "Edita el tipus d'objecte i el tipus de classificació per a aquest model de classificació d'objectes.", + "stateClassesInfo": "Nota: Canviar les classes d'estat requereix tornar a entrenar el model amb les classes actualitzades." + }, + "tooltip": { + "trainingInProgress": "El model s'està entrenant actualment", + "noNewImages": "Sense noves imatges per entrenar. Classifica més imatges primer.", + "modelNotReady": "El model no está preparat per entrenar", + "noChanges": "No hi ha canvis al conjunt de dades des de l'última formació." + }, + "none": "Cap", + "reclassifyImageAs": "Reclassifica la imatge com a:", + "reclassifyImage": "Reclassifica la imatge" +} diff --git a/web/public/locales/ca/views/configEditor.json b/web/public/locales/ca/views/configEditor.json new file mode 100644 index 0000000..bd3149a --- /dev/null +++ b/web/public/locales/ca/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor de Configuració - Frigate", + "configEditor": "Editor de configuració", + "copyConfig": "Copiar configuració", + "saveAndRestart": "Desa i reinicia", + "saveOnly": "Només desar", + "toast": { + "success": { + "copyToClipboard": "Configuració copiada al porta-retalls." + }, + "error": { + "savingError": "Error al desar la configuració" + } + }, + "confirm": "Sortir sense desar?", + "safeConfigEditor": "Editor de Configuració (Mode Segur)", + "safeModeDescription": "Frigate està en mode segur a causa d'un error de validació de la configuració." +} diff --git a/web/public/locales/ca/views/events.json b/web/public/locales/ca/views/events.json new file mode 100644 index 0000000..a0563a9 --- /dev/null +++ b/web/public/locales/ca/views/events.json @@ -0,0 +1,94 @@ +{ + "detected": "detectat", + "alerts": "Alertes", + "detections": "Deteccions", + "motion": { + "label": "Moviment", + "only": "Només moviment" + }, + "allCameras": "Totes les càmeres", + "empty": { + "alert": "Hi ha cap alerta per revisar", + "detection": "Hi ha cap detecció per revisar", + "motion": "No s'haan trobat dades de moviment", + "recordingsDisabled": { + "title": "S'han d'activar les gravacions", + "description": "Només es poden revisar temes quan s'han activat les gravacions de la càmera." + } + }, + "timeline": { + "label": "Línia de temps" + }, + "timeline.aria": "Seleccionar línia de temps", + "events": { + "label": "Esdeveniments", + "aria": "Seleccionar esdeveniments", + "noFoundForTimePeriod": "No s'han trobat esdeveniments per aquest període de temps." + }, + "documentTitle": "Revisió - Frigate", + "recordings": { + "documentTitle": "Enregistraments - Frigate", + "invalidSharedLink": "No s'ha pogut obrir l'enllaç d'enregistrament amb marques de temps a causa d'un error d'anàlisi.", + "invalidSharedCamera": "No s'ha pogut obrir l'enllaç d'enregistrament amb marques de temps a causa d'una càmera desconeguda o no autoritzada." + }, + "calendarFilter": { + "last24Hours": "Últimes 24 hores" + }, + "markAsReviewed": "Marcar com a revisat", + "markTheseItemsAsReviewed": "Marca aquests elements com a revisats", + "newReviewItems": { + "label": "Veure nous elements de revisió", + "button": "Nous elements per revisar" + }, + "camera": "Càmera", + "selected_one": "{{count}} seleccionats", + "selected_other": "{{count}} seleccionats", + "suspiciousActivity": "Activitat sospitosa", + "threateningActivity": "Activitat amenaçadora", + "detail": { + "noDataFound": "No hi ha dades detallades a revisar", + "trackedObject_one": "{{count}} objecte", + "aria": "Canvia la vista de detall", + "trackedObject_other": "{{count}} objectes", + "noObjectDetailData": "No hi ha dades de detall d'objecte disponibles.", + "label": "Detall", + "settings": "Configuració de la vista detallada", + "alwaysExpandActive": { + "title": "Expandeix sempre actiu", + "desc": "Expandeix sempre els detalls de l'objecte de la revisió activa quan estigui disponible." + } + }, + "objectTrack": { + "clickToSeek": "Feu clic per cercar aquesta hora", + "trackedPoint": "Punt de seguiment" + }, + "zoomIn": "Amplia", + "zoomOut": "Redueix", + "normalActivity": "Normal", + "needsReview": "Necessita revisió", + "securityConcern": "Preocupació per la seguretat", + "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}}", + "filter": "Filtre", + "filterDesc": "Seleccioneu àrees per a mostrar només clips amb moviment en aquestes regions.", + "filterClear": "Neteja" + } +} diff --git a/web/public/locales/ca/views/explore.json b/web/public/locales/ca/views/explore.json new file mode 100644 index 0000000..a6a2844 --- /dev/null +++ b/web/public/locales/ca/views/explore.json @@ -0,0 +1,319 @@ +{ + "exploreIsUnavailable": { + "downloadingModels": { + "tips": { + "context": "Potser voldreu reindexar els vectors dels objectes seguits un cop s'hagin descarregat els models.", + "documentation": "Llegir la documentació" + }, + "context": "El Frigate està baixant els models de vectors necessaris per a admetre la funció de Cerca Semàntica. Això pot trigar uns quants minuts depenent de la velocitat de la vostra connexió de xarxa.", + "setup": { + "visionModel": "Model de visió", + "visionModelFeatureExtractor": "Extractor de característiques del model de visió", + "textModel": "Model de text", + "textTokenizer": "Tokenitzador de text" + }, + "error": "S'ha produït un error. Comproveu els registres de Frigate." + }, + "embeddingsReindexing": { + "context": "Explorar pot ser utilitzat després d’haver completat la reindexació d’objectes rastrejats.", + "startingUp": "Iniciant…", + "finishingShortly": "Finalitzant en breus", + "step": { + "thumbnailsEmbedded": "Miniatures integrades: ", + "descriptionsEmbedded": "Descripcions integrades: ", + "trackedObjectsProcessed": "Objectes processats: " + }, + "estimatedTime": "Temps restant estimat:" + }, + "title": "Explorar no està disponible" + }, + "documentTitle": "Explora - Frigate", + "generativeAI": "IA Generativa", + "objectLifecycle": { + "createObjectMask": "Crear màscara per a l'objecte", + "title": "Cicle de vida de l'objecte", + "noImageFound": "No s'ha trobat cap imatge per a aquesta marca temporal.", + "adjustAnnotationSettings": "Ajustar els paràmetres de les anotacions", + "scrollViewTips": "Desplaça't per veure els moments significatius del cicle de vida d'aquest objecte.", + "lifecycleItemDesc": { + "entered_zone": "{{label}} ha entrat a {{zones}}", + "active": "{{label}} s'ha activat", + "stationary": "{{label}} ha esdevingut estacionari", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectat per a {{label}}", + "other": "{{label}} reconegut com a {{attribute}}" + }, + "header": { + "zones": "Zones", + "ratio": "Proporció", + "area": "Àrea" + }, + "heard": "{{label}} escoltat", + "external": "{{label}} detectat", + "gone": "{{label}} ha marxat", + "visible": "{{label}} detectat" + }, + "annotationSettings": { + "offset": { + "documentation": "Llegir la documentació ", + "label": "Desplaçament de l'anotació", + "desc": "Aquestes dades provenen de la detecció d'objectes, però se superposen a les imatges d’enregistrament. És poc probable que les dues transmissions estiguin perfectament sincronitzades. Per aquest motiu, la capsa delimitadora i les imatges poden no coincidir exactament. Tanmateix, es pot utilitzar el camp annotation_offset per ajustar-ho.", + "tips": "CONSELL: Imagina que hi ha la captura d'un esdeveniment on una persona camina d'esquerra a dreta. Si la caixa delimitadora de l'objecte està constantment a l'esquerra de la persona, llavors el valor s'hauria de disminuir. Si, per contra, la caixa delimitadora està constantment per davant de la persona (a la seva dreta en aquest exemple), llavors el valor s'hauria d'augmentar.", + "toast": { + "success": "El desplaçament d'anotació per {{camera}} s'ha guardat al fitxer de configuració. Reinicia Frigate per aplicar els canvis." + }, + "millisecondsToOffset": "Mil·lisegons a desplaçar les anotacions de detecció: Per Defecte: 0" + }, + "title": "Paràmetres de les anotacions", + "showAllZones": { + "title": "Mostra totes les zones", + "desc": "Mostra sempre les zones en fotogrames on hi hagin aparegut objectes." + } + }, + "carousel": { + "next": "Diapositiva següent", + "previous": "Diapositiva anterior" + }, + "autoTrackingTips": "Les posicions dels recuadres delimitadors seràn inexactes per a càmeres amb seguiment automàtic.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Punt seguit" + }, + "exploreMore": "Explora més {{label}} objectes", + "trackedObjectDetails": "Detalls de l'objecte rastrejat", + "type": { + "details": "detalls", + "snapshot": "instantània", + "video": "vídeo", + "object_lifecycle": "cicle de vida de l'objecte", + "thumbnail": "miniatura", + "tracking_details": "detalls del seguiment" + }, + "details": { + "timestamp": "Marca temporal", + "item": { + "button": { + "viewInExplore": "Veure a Explorar", + "share": "Comparteix aquest element de revisió" + }, + "toast": { + "success": { + "updatedSublabel": "Subetiqueta actualitzada amb èxit.", + "updatedLPR": "Matrícula actualitzada amb èxit.", + "regenerate": "El {{provider}} ha sol·licitat una nova descripció. En funció de la velocitat del vostre proveïdor, la nova descripció pot trigar un temps a regenerar-se.", + "audioTranscription": "S'ha sol·licitat correctament la transcripció d'àudio. Depenent de la velocitat del vostre servidor Frigate, la transcripció pot trigar una estona a completar-se.", + "updatedAttributes": "Els atributs s'han actualitzat correctament." + }, + "error": { + "regenerate": "No s'ha pogut contactar amb {{provider}} per obtenir una nova descripció: {{errorMessage}}", + "updatedSublabelFailed": "No s'ha pogut actualitzar la subetiqueta: {{errorMessage}}", + "updatedLPRFailed": "No s'ha pogut actualitzar la matrícula: {{errorMessage}}", + "audioTranscription": "Error en demanar la transcripció d'audio {{errorMessage}}", + "updatedAttributesFailed": "No s'han pogut actualitzar els atributs: {{errorMessage}}" + } + }, + "title": "Revisar detalls de l'element", + "desc": "Revisar detalls de l'element", + "tips": { + "hasMissingObjects": "Ajusta la configuració si vols que Frigate guardi els objectes rastrejat de les seguents etiquetes: {{objects}}", + "mismatch_one": "{{count}} objecte no disponible ha estat detectat i inclòs en aquest element de revisió. Aquest objecte tampoc no s'han calificat com una alerta o detecció o ja ha estat netejat mes amunt/eliminat.", + "mismatch_many": "{{count}} objectes no disponibles han estat detectats i inclosos en aquest element de revisió. Aquests objectes tampoc no s'han calificat com una alerta o detecció o ja han estat netejats mes amunt/eliminats.", + "mismatch_other": "{{count}} objectes no disponibles han estat detectats i inclosos en aquest element de revisió. Aquests objectes tampoc no s'han calificat com una alerta o detecció o ja han estat netejats mes amunt/eliminats." + } + }, + "label": "Etiqueta", + "topScore": { + "label": "Puntuació màxima", + "info": "El resultat superior és la mediana més alta per l'objecte seguit, així que pot diferir des del resultat mostrat en thumbnail de la búsqueda de recerca." + }, + "estimatedSpeed": "Velocitat estimada", + "button": { + "regenerate": { + "title": "Regenerar", + "label": "Regenerar descripció d'objecte rastrejat" + }, + "findSimilar": "Cercar similars" + }, + "expandRegenerationMenu": "Amplia el menú de regeneració", + "regenerateFromSnapshot": "Regenerar desde instantània", + "regenerateFromThumbnails": "Regenerar desde miniatures", + "tips": { + "descriptionSaved": "Descripció desada amb èxit", + "saveDescriptionFailed": "No s'ha pogut actualitzar la descripció: {{errorMessage}}" + }, + "description": { + "placeholder": "Descripció de l'objecte rastrejat", + "label": "Descripció", + "aiTips": "Frigate no sol·licitarà una descripció al teu proveïdor d'intel·ligència artificial generativa fins que el cicle de vida de l'objecte rastrejat hagi acabat." + }, + "objects": "Objectes", + "camera": "Càmera", + "editSubLabel": { + "title": "Editar subetiqueta", + "descNoLabel": "Introdueix una nova subetiqueta per a aquest objecte rastrejat", + "desc": "Introdueix una nova subetiqueta per a aquesta {{label}}" + }, + "zones": "Zones", + "recognizedLicensePlate": "Matrícula reconeguda", + "snapshotScore": { + "label": "Puntuació d'instantània" + }, + "editLPR": { + "title": "Editar matrícula", + "descNoLabel": "Introdueix un nou valor de matrícula per a aquest objecte rastrejat", + "desc": "Introdueix un nou valor per a la matrícula per aquesta {{label}}" + }, + "score": { + "label": "Puntuació" + }, + "editAttributes": { + "title": "Edita els atributs", + "desc": "Seleccioneu els atributs de classificació per a aquesta {{label}}" + }, + "attributes": "Atributs de classificació", + "title": { + "label": "Títol" + }, + "scoreInfo": "Informació de la partitura" + }, + "searchResult": { + "tooltip": "S'ha identificat {{type}} amb una confiança del {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "success": "L'objectes amb seguiment s'ha suprimit correctament.", + "error": "No s'ha pogut suprimir l'objecte rastrejat: {{errorMessage}}" + } + }, + "nextTrackedObject": "Següent objecte rastrejat", + "previousTrackedObject": "Objecte rastrejat anterior" + }, + "itemMenu": { + "downloadVideo": { + "aria": "Descarregar vídeo", + "label": "Descarregar vídeo" + }, + "submitToPlus": { + "aria": "Enviar a Frigate Plus", + "label": "Enviar a Frigate+" + }, + "downloadSnapshot": { + "label": "Descarregar instantània", + "aria": "Descarregar instantània" + }, + "findSimilar": { + "label": "Cercar similars", + "aria": "Trobar objectes de seguiment similars" + }, + "viewObjectLifecycle": { + "label": "Veure el cicle de vida de l'objecte", + "aria": "Mostrar el cicle de vida de l'objecte" + }, + "viewInHistory": { + "label": "Veure a l'historial", + "aria": "Veure a l'historial" + }, + "deleteTrackedObject": { + "label": "Suprimeix aquest objecte rastrejat" + }, + "addTrigger": { + "label": "Afegir disparador", + "aria": "Afegir disparador per aquest objecte" + }, + "audioTranscription": { + "label": "Transcriu", + "aria": "Demanar una transcripció d'audio" + }, + "showObjectDetails": { + "label": "Mostra la ruta de l'objecte" + }, + "hideObjectDetails": { + "label": "Amaga la ruta de l'objecte" + }, + "viewTrackingDetails": { + "label": "Veure detalls de seguiment", + "aria": "Mostra els detalls de seguiment" + }, + "downloadCleanSnapshot": { + "label": "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ó" + }, + "more": { + "aria": "Més" + } + }, + "noTrackedObjects": "No s'han trobat objectes rastrejats", + "dialog": { + "confirmDelete": { + "title": "Confirmar la supressió", + "desc": "En eliminar aquest objecte detectat, s'esborrarà la instantània, els vectors desats i qualsevol entrada associada als detalls de seguiment d'aquest objecte. El metratge enregistrat d'aquest objecte detectat a la vista de l'Historial NO s'esborrarà.

    Segur que voleu continuar?" + }, + "toast": { + "error": "S'ha produït un error en suprimir aquest objecte rastrejat: {{errorMessage}}" + } + }, + "fetchingTrackedObjectsFailed": "Error al obtenir objectes rastrejats: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} objecte rastrejat ", + "trackedObjectsCount_many": "{{count}} objectes rastrejats ", + "trackedObjectsCount_other": "{{count}} objectes rastrejats ", + "aiAnalysis": { + "title": "Anàlisi d'IA" + }, + "concerns": { + "label": "Preocupacions" + }, + "trackingDetails": { + "title": "Detalls de seguiment", + "noImageFound": "No s'ha trobat cap imatge amb aquesta hora.", + "createObjectMask": "Crear màscara d'objecte", + "adjustAnnotationSettings": "Ajustar configuració d'anotacions", + "scrollViewTips": "Feu clic per veure els moments significatius del cicle de vida d'aquest objecte.", + "autoTrackingTips": "Limitar les posicións de la caixa serà inacurat per càmeras de seguiment automàtic.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Punt Seguit", + "lifecycleItemDesc": { + "visible": "{{label}} detectat", + "entered_zone": "{{label}} ha entrat a {{zones}}", + "active": "{{label}} ha esdevingut actiu", + "stationary": "{{label}} ha esdevingut estacionari", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectat per {{label}}", + "other": "{{label}} reconegut com a {{attribute}}" + }, + "gone": "{{label}} ha sortit", + "heard": "{{label}} sentit", + "external": "{{label}} detectat", + "header": { + "zones": "Zones", + "ratio": "Ràtio", + "area": "Àrea", + "score": "Puntuació", + "computedScore": "Puntuació calculada", + "topScore": "Puntuació superior", + "toggleAdvancedScores": "Commuta les puntuacions avançades" + } + }, + "annotationSettings": { + "title": "Configuració d'anotacions", + "showAllZones": { + "title": "Mostra totes les Zones", + "desc": "Mostra sempre les zones amb fotogrames on els objectes hagin entrat a la zona." + }, + "offset": { + "label": "Òfset d'Anotació", + "desc": "Aquestes dades provenen del flux de detecció de la càmera, però se superposen a les imatges del flux de gravació. És poc probable que els dos fluxos estiguin perfectament sincronitzats. Com a resultat, el quadre delimitador i les imatges no s'alinearan perfectament. Tanmateix, es pot utilitzar el camp annotation_offset per ajustar-ho.", + "millisecondsToOffset": "Millisegons per l'òfset de detecció d'anotacions per. Per defecte: 0", + "tips": "Reduïu el valor si la reproducció del vídeo es troba per davant dels quadres i els punts de ruta, i augmenteu-lo si es troba per darrere. Aquest valor pot ser negatiu.", + "toast": { + "success": "El desplaçament de l'anotació per {{camera}} s'ha desat al fitxer de configuració." + } + } + }, + "carousel": { + "previous": "Diapositiva anterior", + "next": "Dispositiva posterior" + } + } +} diff --git a/web/public/locales/ca/views/exports.json b/web/public/locales/ca/views/exports.json new file mode 100644 index 0000000..194d87a --- /dev/null +++ b/web/public/locales/ca/views/exports.json @@ -0,0 +1,128 @@ +{ + "documentTitle": "Exportar - Frigate", + "search": "Buscar", + "noExports": "No s'han trobat exportacions", + "deleteExport": { + "label": "Suprimeix l'exportació" + }, + "deleteExport.desc": "Estàs segur que vols eliminar {{exportName}}?", + "editExport": { + "title": "Renombrar exportació", + "desc": "Introdueix un nou nom per a aquesta exportació.", + "saveExport": "Desar exportació" + }, + "toast": { + "error": { + "renameExportFailed": "Error al canviar el nom de l’exportació: {{errorMessage}}", + "assignCaseFailed": "No s'ha pogut actualitzar l'assignació de cas:{{errorMessage}}", + "caseSaveFailed": "No s'ha pogut desar el cas: {{errorMessage}}", + "caseDeleteFailed": "No s'ha pogut suprimir el cas: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Comparteix l'exportació", + "downloadVideo": "Baixa el vídeo", + "editName": "Edita el nom", + "deleteExport": "Suprimeix l'exportació", + "assignToCase": "Afegeix al cas", + "removeFromCase": "Elimina del 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ó" + }, + "toolbar": { + "newCase": "Cas nou", + "addExport": "Afegeix una exportació", + "editCase": "Edita el cas", + "deleteCase": "Suprimeix el cas" + }, + "deleteCase": { + "label": "Suprimeix el cas", + "desc": "Esteu segur que voleu suprimir {{caseName}}?", + "descKeepExports": "Les exportacions continuaran estant disponibles com a exportacions sense categoria.", + "descDeleteExports": "Totes les exportacions en aquest cas s'eliminaran permanentment.", + "deleteExports": "Elimina també les exportacions" + }, + "caseCard": { + "emptyCase": "Encara no hi ha exportacions" + }, + "jobCard": { + "defaultName": "Exportació de {{camera}}", + "queued": "En cua", + "running": "En execució", + "preparing": "Preparant", + "copying": "Copiant", + "encoding": "Codificant", + "encodingRetry": "Codificant (reintent)", + "finalizing": "Finalitzant" + }, + "caseView": { + "noDescription": "Sense descripció", + "createdAt": "{{value}} creat", + "exportCount_one": "1 exportació", + "exportCount_other": "{{count}} exportacions", + "cameraCount_one": "1 càmera", + "cameraCount_other": "{{count}} càmeres", + "showMore": "Mostra'n més", + "showLess": "Mostra menys", + "emptyTitle": "Aquest cas és buit", + "emptyDescription": "Afegeix les exportacions no categoritzades existents per mantenir el cas organitzat.", + "emptyDescriptionNoExports": "Encara no hi ha exportacions sense categoria per afegir." + }, + "caseEditor": { + "createTitle": "Crea un cas", + "editTitle": "Edita el cas", + "namePlaceholder": "Nom del cas", + "descriptionPlaceholder": "Afegeix notes o context per a aquest cas" + }, + "addExportDialog": { + "title": "Afegeix l'exportació a {{caseName}}", + "searchPlaceholder": "Cerca exportacions sense categoria", + "empty": "No hi ha exportacions sense categoria que coincideixin amb aquesta cerca.", + "addButton_one": "Afegeix 1 exportació", + "addButton_other": "Afegeix {{count}} exportacions", + "adding": "S'està afegint..." + }, + "selected_one": "{{count}} seleccionats", + "selected_other": "{{count}} seleccionats", + "bulkActions": { + "addToCase": "Afegeix al cas", + "moveToCase": "Mou al cas", + "removeFromCase": "Elimina del cas", + "delete": "Suprimeix", + "deleteNow": "Suprimeix ara" + }, + "bulkDelete": { + "title": "Suprimeix les exportacions", + "desc_one": "Esteu segur que voleu suprimir {{count}} l'exportació?", + "desc_other": "steu segur que voleu suprimir {{count}} exportacions?" + }, + "bulkRemoveFromCase": { + "title": "Elimina del cas", + "desc_one": "Voleu suprimir {{count}} d'aquest cas?", + "desc_other": "Voleu eliminar {{count}} exportacions d'aquest cas?", + "descKeepExports": "Les exportacions es mouran a sense categoria.", + "descDeleteExports": "Les exportacions s'eliminaran permanentment.", + "deleteExports": "Suprimeix les exportacions" + }, + "bulkToast": { + "success": { + "delete": "Exportacions suprimides amb èxit", + "reassign": "Assignació de cas actualitzada amb èxit", + "remove": "S'han eliminat les exportacions del cas" + }, + "error": { + "deleteFailed": "No s'han pogut suprimir les exportacions: {{errorMessage}}", + "reassignFailed": "No s'ha pogut actualitzar l'assignació de cas: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/ca/views/faceLibrary.json b/web/public/locales/ca/views/faceLibrary.json new file mode 100644 index 0000000..5f0546e --- /dev/null +++ b/web/public/locales/ca/views/faceLibrary.json @@ -0,0 +1,112 @@ +{ + "selectItem": "Selecciona {{item}}", + "details": { + "subLabelScore": "Puntuació de la subetiqueta", + "scoreInfo": "La puntuació de la subetiqueta és la puntuació ponderada de totes la confidència dels rostres reconeguts, de manera que pot ser diferent de la puntuació que es mostra a la instantània.", + "unknown": "Desconegut", + "person": "Persona", + "faceDesc": "Detalls de l'objecte que ha generat aquest rostre", + "timestamp": "Marca temporal", + "face": "Detalls del rostre" + }, + "collections": "Col·leccions", + "train": { + "empty": "No hi ha intents recents de reconeixement de rostres", + "title": "Reconeixements recents", + "aria": "Selecciona els reconeixements recents", + "titleShort": "Recent", + "emptyNoLibrary": { + "title": "Puja una cara", + "description": "Heu d'afegir com a mínim una cara a la biblioteca perquè el reconeixement de la cara funcioni." + } + }, + "description": { + "addFace": "Afegiu una col·lecció nova a la biblioteca de cares pujant la vostra primera imatge.", + "placeholder": "Introduïu un nom per a aquesta col·lecció", + "invalidName": "Nom no vàlid. Els noms només poden incloure lletres, números, espais, apòstrofs, guions baixos i guions.", + "nameCannotContainHash": "El nom no pot contenir #." + }, + "documentTitle": "Biblioteca de rostres - Frigate", + "uploadFaceImage": { + "title": "Puja una imatge del rostre", + "desc": "Carregar una imatge per escanejar els rostres i incloure per a {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Crear Col·lecció", + "desc": "Crear una nova col·lecció", + "new": "Crear un nou rostre", + "nextSteps": "Per establir una base sòlida:
  • Utilitza la pestanya Entrenament per seleccionar i entrenar imatges de cada persona detectada.
  • Centra’t en imatges frontals per obtenir millors resultats; evita imatges d’entrenament amb rostres en angle.
  • " + }, + "steps": { + "faceName": "Introduir el nom del rostre", + "uploadFace": "Puja una imatge del rostre", + "nextSteps": "Següents passos", + "description": { + "uploadFace": "Pugeu una imatge de {{name}} que mostra la seva cara des d'un angle frontal. La imatge no necessita ser retallada a la seva cara." + } + }, + "selectFace": "Seleccionar rostre", + "deleteFaceLibrary": { + "desc": "Estàs segur que vols eliminar la col·lecció {{name}}? Això eliminarà permanentment tots els rostres associats.", + "title": "Suprimir nom" + }, + "renameFace": { + "desc": "Introduïu un nou nom per a {{name}}", + "title": "Canviar nom del rostre" + }, + "imageEntry": { + "dropActive": "Arrossegueu la imatge aquí…", + "validation": { + "selectImage": "Siusplau, selecciona un fixer d'imatge." + }, + "maxSize": "Mida màxima: {{size}}MB", + "dropInstructions": "Arrossegueu i deixeu anar o enganxeu una imatge aquí, o feu clic per seleccionar" + }, + "button": { + "uploadImage": "Pujar imatge", + "addFace": "Afegir rostre", + "deleteFaceAttempts": "Suprimir rostres", + "renameFace": "Renombrar rostre", + "deleteFace": "Suprimeix rostre", + "reprocessFace": "Reprocessar rostre" + }, + "toast": { + "success": { + "trainedFace": "Rostre entrenat amb èxit.", + "updatedFaceScore": "S'ha actualitzat correctament la puntuació de la cara a {{name}} ({{score}}).", + "uploadedImage": "Imatge pujada amb èxit.", + "addFaceLibrary": "{{name}} s'ha afegit amb èxit a la biblioteca de rostres!", + "deletedName_one": "{{count}} rostre s'ha suprimit amb èxit.", + "deletedName_many": "{{count}} rostres s'han suprimit amb èxit.", + "deletedName_other": "{{count}} rostres s'han suprimit amb èxit.", + "deletedFace_one": "{{count}} rostre suprimit amb èxit.", + "deletedFace_many": "{{count}} rostres suprimits amb èxit.", + "deletedFace_other": "{{count}} rostres suprimits amb èxit.", + "renamedFace": "Rostre renombrat amb èxit a {{name}}", + "reclassifiedFace": "Cara reclassificada amb èxit." + }, + "error": { + "uploadingImageFailed": "No s'ha pogut penjar la imatge: {{errorMessage}}", + "trainFailed": "No s'ha pogut entrenar: {{errorMessage}}", + "deleteFaceFailed": "No s'ha pogut suprimir: {{errorMessage}}", + "deleteNameFailed": "No s'ha pogut suprimir el nom: {{errorMessage}}", + "updateFaceScoreFailed": "No s'ha pogut actualitzar la puntuació de rostre: {{errorMessage}}", + "addFaceLibraryFailed": "No s'ha pogut establir el nom del rostre: {{errorMessage}}", + "renameFaceFailed": "No s'ha pogut renombrar el rostre: {{errorMessage}}", + "reclassifyFailed": "No s'ha pogut reclassificar la cara: {{errorMessage}}" + } + }, + "nofaces": "No hi han rostres disponibles", + "deleteFaceAttempts": { + "title": "Suprimir rostres", + "desc_one": "Estàs segur que vols suprimir {{count}} rostre? Aquesta acció no es pot desfer.", + "desc_many": "Estàs segur que vols suprimir {{count}} rostres? Aquesta acció no es pot desfer.", + "desc_other": "Estàs segur que vols suprimir {{count}} rostres? Aquesta acció no es pot desfer." + }, + "pixels": "{{area}}px", + "trainFace": "Entrenar rostre", + "readTheDocs": "Llegir la documentació", + "trainFaceAs": "Entrenar rostre com a:", + "reclassifyFaceAs": "Reclassifica la cara com a:", + "reclassifyFace": "Reclassifica la cara" +} diff --git a/web/public/locales/ca/views/live.json b/web/public/locales/ca/views/live.json new file mode 100644 index 0000000..b2f7fda --- /dev/null +++ b/web/public/locales/ca/views/live.json @@ -0,0 +1,206 @@ +{ + "ptz": { + "zoom": { + "in": { + "label": "Apropar la càmera PTZ" + }, + "out": { + "label": "Allunyar la càmera PTZ" + } + }, + "move": { + "clickMove": { + "label": "Fes clic a la imatge per centrar la càmera", + "enable": "Habilita clic per moure", + "disable": "Deshabilita clic per moure", + "enableWithZoom": "Activa el clic per moure / arrossegar per ampliar" + }, + "left": { + "label": "Moure la càmera PTZ a l'esquerra" + }, + "up": { + "label": "Moure la càmera PTZ cap amunt" + }, + "down": { + "label": "Moure la càmera PTZ cap avall" + }, + "right": { + "label": "Moure la càmera PTZ a la dreta" + } + }, + "frame": { + "center": { + "label": "Fer clic a la imatge per centrar la càmera PTZ" + } + }, + "presets": "Predefinits de la càmera PTZ", + "focus": { + "in": { + "label": "Enfoca la càmera PTZ aprop" + }, + "out": { + "label": "Enfoca la càmera PTZ lluny" + } + } + }, + "documentTitle": { + "default": "Live - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Directe - Frigate", + "lowBandwidthMode": "Mode de baix ample de banda", + "twoWayTalk": { + "enable": "Activa la comunicació bidireccional", + "disable": "Desactiva la comunicació bidireccional" + }, + "cameraAudio": { + "enable": "Habilitar l'àudio de la càmera", + "disable": "Deshabilita l'àudio de la càmera" + }, + "camera": { + "enable": "Habilitar la càmera", + "disable": "Deshabilita la càmera", + "turnOn": "Activa la càmera", + "turnOff": "Apaga la càmera" + }, + "muteCameras": { + "enable": "Silencia totes les càmeres", + "disable": "Activar el so de totes les càmeres" + }, + "detect": { + "enable": "Habilita la detecció", + "disable": "Deshabilitar detecció" + }, + "recording": { + "enable": "Habilitar gravació", + "disable": "Deshabilita l'enregistrament", + "disabledInConfig": "L'enregistrament primer s'ha d'habilitar a la configuració d'aquesta càmera." + }, + "snapshots": { + "enable": "Habilita captura d'instantània", + "disable": "Deshabilitar instantànies" + }, + "audioDetect": { + "enable": "Habilita la detecció d'àudio", + "disable": "Deshabilitar la detecció d'àudio" + }, + "autotracking": { + "enable": "Habilitar seguiment automàtic", + "disable": "Deshabilitar seguiment automàtic" + }, + "streamStats": { + "enable": "Mostrar les estadístiques de la transmissió", + "disable": "Amaga estadístiques de la transmissió" + }, + "manualRecording": { + "title": "Sota demanda", + "tips": "Baixeu una instantània o inicieu un esdeveniment manual basat en la configuració de retenció d'enregistrament d'aquesta càmera.", + "playInBackground": { + "label": "Reproduir en segon pla", + "desc": "Habilita aquesta opció per a continuar la transmissió quan el reproductor està amagat." + }, + "showStats": { + "label": "Mostrar les estadístiques", + "desc": "Habilita aquesta opció per mostrar les estadístiques de transmissió com una superposició de la transmissió de la càmera." + }, + "start": "Iniciar enregistrament sota demanda", + "started": "Gravació sota demanda manual inciada.", + "ended": "Gravació sota demanda manual finalitzada.", + "debugView": "Vista de depuració", + "end": "Finalitzar gravació sota demanda", + "failedToStart": "No s'ha pogut iniciar la gravació manual sota demanda.", + "recordDisabledTips": "Com que la gravació està deshabilitada o restringida a la configuració d'aquesta càmera, només es guardarà una instantània.", + "failedToEnd": "No s'ha pogut acabar la gravació manual sota demanda." + }, + "notifications": "Notificacions", + "audio": "Àudio", + "stream": { + "title": "Transmissió", + "audio": { + "tips": { + "documentation": "Llegir la documentació ", + "title": "L'àudio ha de provenir de la càmera i estar configurat amb go2rtc per a aquesta transmissió." + }, + "available": "L'àudio està disponible per a aquesta transmissió", + "unavailable": "L'audio no està disponible per a aquesta transmissió" + }, + "twoWayTalk": { + "tips.documentation": "Llegir la documentació ", + "tips": "El teu dispositiu ha de suportar la funció i WebRTC ha d'estar configurat per a conversa bidireccional.", + "available": "La conversa bidireccional està disponible per a aquesta transmissió", + "unavailable": "La conversa bidireccional no està disponible per a aquesta transmissió" + }, + "lowBandwidth": { + "resetStream": "Restablir transmissió", + "tips": "La vista en directe està en mode de baix ample de banda a causa d'errors de transmissió o de buffering." + }, + "playInBackground": { + "label": "Reproduir en segon pla", + "tips": "Habilita aquesta opció per a contiuar la transmissió tot i que el reproductor estigui ocult." + }, + "debug": { + "picker": "Selecció de stream no disponible en mode debug. La vista debug sempre fa servir el stream assignat pel rol de detecció." + } + }, + "streamingSettings": "Paràmetres de transmissió", + "suspend": { + "forTime": "Suspèn per: " + }, + "cameraSettings": { + "title": "{{camera}} Paràmetres", + "cameraEnabled": "Càmera habilitada", + "recording": "Gravació", + "snapshots": "Instantànies", + "autotracking": "Seguiment automàtic", + "objectDetection": "Detecció d'objectes", + "audioDetection": "Detecció d'àudio", + "transcription": "Transcripció d'audio", + "camera": "Càmera" + }, + "history": { + "label": "Mostrar gravacions històriques" + }, + "effectiveRetainMode": { + "modes": { + "all": "Tot", + "motion": "Moviment", + "active_objects": "Objectes actius" + }, + "notAllTips": "El vostre {{source}} registre de configuració de retenció s'ha posat en el mode : {{effectiveRetainMode}}, així que la gravaciò a demanda només seguirà segments amb {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Editar el disseny", + "group": { + "label": "Editar grup de càmeres" + }, + "exitEdit": "Sortir de l'edició" + }, + "transcription": { + "enable": "Habilita la transcripció d'àudio en temps real", + "disable": "Deshabilita la transcripció d'àudio en temps real" + }, + "snapshot": { + "takeSnapshot": "Descarregar una instantània", + "noVideoSource": "No hi ha cap font de video per fer una instantània.", + "captureFailed": "Error capturant una instantània.", + "downloadStarted": "Inici de baixada d'instantània." + }, + "noCameras": { + "title": "No s'ha configurat cap càmera", + "description": "Comenceu connectant una càmera a Frigate.", + "buttonText": "Afegeix una càmera", + "restricted": { + "title": "No hi ha càmeres disponibles", + "description": "No teniu permís per veure cap càmera en aquest grup." + }, + "default": { + "title": "No s'ha configurat cap càmera", + "description": "Comenceu connectant una càmera a Frigate.", + "buttonText": "Afegeix una càmera" + }, + "group": { + "title": "No hi ha càmeres al grup", + "description": "Aquest grup de càmeres no té càmeres assignades o habilitades.", + "buttonText": "Gestiona els grups" + } + } +} diff --git a/web/public/locales/ca/views/motionSearch.json b/web/public/locales/ca/views/motionSearch.json new file mode 100644 index 0000000..585f6cb --- /dev/null +++ b/web/public/locales/ca/views/motionSearch.json @@ -0,0 +1,82 @@ +{ + "documentTitle": "Busca Deteccións - Frigate", + "title": "Búsqueda de Deteccions", + "selectCamera": "Búsqueda de Deteccions s'esta carregant", + "startSearch": "Començar Búsqueda", + "searchStarted": "Búsqueda inicada", + "searchCancelled": "Búsqueda cancel·lada", + "cancelSearch": "Cancel·lar", + "searching": "Búsqueda en progrés.", + "searchComplete": "Búsqueda completa", + "description": "Dibuixa un polígon per definir la regió d'interès, i especifica un interval de temps per cercar canvis de moviment dins d'aquesta regió.", + "noResultsYet": "Executa una cerca per a trobar canvis de moviment a la regió seleccionada", + "noChangesFound": "No s'ha detectat cap canvi de píxel a la regió seleccionada", + "changesFound_one": "S'ha trobat el canvi de moviment {{count}}", + "changesFound_many": "S'han trobat {{count}} canvis de moviment", + "changesFound_other": "S'han trobat {{count}} canvis de moviment", + "framesProcessed": "{{count}} fotogrames processats", + "jumpToTime": "Salta a aquesta hora", + "results": "Resultats", + "showSegmentHeatmap": "Mapa de calor", + "newSearch": "Cerca nova", + "clearResults": "Neteja els resultats", + "clearROI": "Neteja el polígon", + "polygonControls": { + "points_one": "{{count}} punt", + "points_many": "{{count}} punts", + "points_other": "{{count}} punts", + "undo": "Desfés l'últim punt", + "reset": "Restableix el polígon", + "drawMode": "Dibuxa", + "moveMode": "Moure" + }, + "motionHeatmapLabel": "Mapa de calor del moviment", + "dialog": { + "title": "Cerca de moviment", + "cameraLabel": "Càmara", + "previewAlt": "Vista prèvia de la càmera per a {{camera}}" + }, + "timeRange": { + "title": "Interval de cerca", + "start": "Hora d'inici", + "end": "Hora final" + }, + "settings": { + "title": "Configuració de la cerca", + "parallelMode": "Mode paral·lel", + "parallelModeDesc": "Escaneja múltiples intervals d'enregistrament al mateix temps (més ràpid; utilitza més recursos de descodificació)", + "threshold": "Llindar de la sensibilitat", + "thresholdDesc": "Els valors més baixos detecten canvis més petits (1-255)", + "minArea": "Àrea de canvi mínim", + "minAreaDesc": "Mida mínima d'una sola regió en moviment, com a percentatge de la regió d'interès", + "frameSkip": "Omet el fotograma", + "frameSkipDesc": "Processa cada N fotograma. Establiu això a la velocitat de fotogrames de la càmera per processar un fotograma per segon (p. ex. 5 per a una càmera de 5 FPS, 30 per a una càmera de 30 FPS). Els valors més alts seran més ràpids, però poden perdre els esdeveniments de curt moviment.", + "maxResults": "Resultats màxims", + "maxResultsDesc": "Atura després d'aquestes quantes marques horàries coincidents" + }, + "errors": { + "noCamera": "Seleccioneu una càmera", + "noROI": "Dibuixeu una regió d'interès", + "noTimeRange": "Seleccioneu un interval de temps", + "invalidTimeRange": "L'hora de finalització ha de ser posterior a l'hora d'inici", + "searchFailed": "Ha fallat la cerca: {{message}}", + "polygonTooSmall": "El polígon ha de tenir almenys 3 punts", + "unknown": "Error desconegut" + }, + "changePercentage": "{{percentage}}% canviat", + "metrics": { + "title": "Cerca les mètriques", + "segmentsScanned": "Segments escanejats", + "segmentsProcessed": "Processat", + "segmentsSkippedInactive": "S'ha omès (sense activitat)", + "segmentsSkippedHeatmap": "S'ha omès (sense superposició ROI)", + "fallbackFullRange": "Escaneig de rang complet alternatiu", + "framesDecoded": "Fotogrames descodificats", + "wallTime": "Temps de cerca", + "segmentErrors": "Errors del segment", + "seconds": "{{seconds}}s", + "scanSummary": "{{segments}} segments · {{time}}", + "minutesSeconds": "{{minutes}}m {{seconds}}s" + }, + "scanning": "S'està analitzant {{time}}" +} diff --git a/web/public/locales/ca/views/recording.json b/web/public/locales/ca/views/recording.json new file mode 100644 index 0000000..e78f5ef --- /dev/null +++ b/web/public/locales/ca/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtre", + "toast": { + "error": { + "noValidTimeSelected": "No s'ha seleccionat un rang de temps vàlid", + "endTimeMustAfterStartTime": "L'hora de finalització ha de ser posterior a l'hora d'inici" + } + }, + "export": "Exportar", + "calendar": "Calendari", + "filters": "Filtres" +} diff --git a/web/public/locales/ca/views/replay.json b/web/public/locales/ca/views/replay.json new file mode 100644 index 0000000..8dc6730 --- /dev/null +++ b/web/public/locales/ca/views/replay.json @@ -0,0 +1,59 @@ +{ + "page": { + "startError": { + "back": "Torna a l'Historial", + "title": "No s'ha pogut iniciar la repetició de la depuració" + }, + "sourceCamera": "Camera d'origen", + "replayCamera": "Reproduïr Càmera", + "initializingReplay": "Inicialitzant depurar repetició...", + "stoppingReplay": "Parant depurar repetició...", + "stopReplay": "Parar Repetició", + "confirmStop": { + "title": "Parar Depurar Repetició?", + "description": "Aixó pararà la sessió i netejarà les dades temporals. Estás segur?", + "confirm": "Parar Repetició", + "cancel": "Cancel·lar" + }, + "activity": "Activitat", + "objects": "Llista d'Objectes", + "audioDetections": "Deteccions d'Audio", + "noActivity": "Sense activitat detectada", + "activeTracking": "Tracking Actiu", + "noActiveTracking": "Sense tracking actiu", + "configuration": "Configuració", + "configurationDesc": "Configuració d'ajust fi de detecció de moviment i tracking d'objectes per a la depuració de reproducció de càmera. Cap canvi es graba en el teu arxiu de configuració de Frigate.", + "noSession": "No hi ha una sessió activa de reproducció de depuració", + "noSessionDesc": "Inicia una reproducció de depuració des de la vista Historial fent clic al botó Accions a la barra d'eines i escollint Depura Repeteix.", + "goToRecordings": "Ves a l'historial", + "preparingClip": "S'està preparant el clip…", + "preparingClipDesc": "Frigate està cosint enregistraments per a l'interval de temps seleccionat. Això pot trigar un minut en intervals més llargs.", + "startingCamera": "S'està iniciant la repetició de la depuració…" + }, + "title": "Repetició de depuració", + "websocket_messages": "Missatges", + "dialog": { + "title": "Iniciar Depuració de Repeticions", + "camera": "Càmera Font", + "timeRange": "Rang de Temps", + "preset": { + "1m": "Últim 1 Minut", + "5m": "Últims 5 Minuts", + "timeline": "Desde la Línia de Temps", + "custom": "Personalitzat" + }, + "description": "Crea una càmera de reproducció temporal que fa bucles de metratge històric per depurar la detecció d'objectes i els problemes de seguiment. La càmera de reproducció tindrà la mateixa configuració de detecció que la càmera d'origen. Trieu un interval de temps per començar.", + "startButton": "Inicia la repetició", + "selectFromTimeline": "Selecciona", + "starting": "S'està iniciant la repetició...", + "startLabel": "Inici", + "endLabel": "Final", + "toast": { + "error": "No s'ha pogut iniciar la repetició de depuració: {{error}}", + "alreadyActive": "Ja hi ha activada una sessió de reproducció", + "stopError": "No s'ha pogut aturar la repetició de depuració: {{error}}", + "goToReplay": "Ves a la repetició" + } + }, + "description": "Reprodueix els enregistraments de la càmera per a la depuració. La llista d'objectes mostra un resum retardat en el temps dels objectes detectats i la pestanya Missatges mostra un flux de missatges interns de frigate a partir del metratge de reproducció." +} diff --git a/web/public/locales/ca/views/search.json b/web/public/locales/ca/views/search.json new file mode 100644 index 0000000..71f3331 --- /dev/null +++ b/web/public/locales/ca/views/search.json @@ -0,0 +1,73 @@ +{ + "filter": { + "label": { + "time_range": "Rang de temps", + "cameras": "Càmeres", + "search_type": "Tipus de cerca", + "labels": "Etiquetes", + "zones": "Zones", + "sub_labels": "Subetiquetes", + "before": "Abans", + "after": "Després", + "min_score": "Puntuació mínima", + "max_score": "Puntuació màxima", + "min_speed": "Velocitat mínima", + "max_speed": "Velocitat màxima", + "recognized_license_plate": "Matrícula reconeguda", + "has_clip": "Té Clip", + "has_snapshot": "Té instantània", + "attributes": "Atributs" + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Descripció" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "La data 'abans' ha de ser posterior a la data 'després'.", + "afterDatebeEarlierBefore": "La data 'després' ha de ser anterior a la data 'abans'.", + "minScoreMustBeLessOrEqualMaxScore": "La \"puntuació mínima\" ha de ser menor o igual que la \"puntuació màxima\".", + "maxScoreMustBeGreaterOrEqualMinScore": "La \"puntuació màxima\" ha de ser major o igual que la \"puntuació mínima\".", + "minSpeedMustBeLessOrEqualMaxSpeed": "La \"velocitat mínima\" ha de ser menor o igual que la \"velocitat màxima\".", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "La \"velocitat màxima\" ha de ser major o igual que la \"velocitat mínima\"." + } + }, + "tips": { + "title": "Com utilitzar filtres de text", + "desc": { + "step2": "Selecciona un valor de les suggerències o escriu-ne un de propi.", + "step3": "Utilitza múltiples filtres afegint-los un rere l'altre amb un espai entremig.", + "exampleLabel": "Exemple:", + "step1": "Escriviu un nom de clau de filtre seguit de dos punts (p. ex., \"càmeres:\").", + "text": "Els filtres t'ajuden a acotar els resultats de cerca. Aquí tens com utilitzar-los al camp d’entrada:", + "step4": "Els filtres de data {abans: i després:) fan servir el format {{DateFormat}}.", + "step5": "El filtre de rang de temps fa servir el format {{exampleTime}}.", + "step6": "Suprimeix els filtres fent clic a la 'x' que tenen al costat." + } + }, + "header": { + "noFilters": "Filtres", + "currentFilterType": "Valors del filtre", + "activeFilters": "Filtres actius" + } + }, + "search": "Buscar", + "savedSearches": "Cerques desades", + "searchFor": "Buscar {{inputValue}}", + "button": { + "clear": "Netejar cerca", + "save": "Desa la cerca", + "delete": "Elimina la recerca desada", + "filterInformation": "Informació del filtre", + "filterActive": "Filtres actius" + }, + "trackedObjectId": "ID de l'objecte rastrejat", + "placeholder": { + "search": "Cercar…" + }, + "similaritySearch": { + "title": "Cerca per similitud", + "active": "Cerca per similitud habilitada", + "clear": "Netejar cerca per similitud" + } +} diff --git a/web/public/locales/ca/views/settings.json b/web/public/locales/ca/views/settings.json new file mode 100644 index 0000000..97905e7 --- /dev/null +++ b/web/public/locales/ca/views/settings.json @@ -0,0 +1,2205 @@ +{ + "documentTitle": { + "enrichments": "Parmàmetres complementaris - Frigate", + "motionTuner": "Ajust de moviment - Frigate", + "object": "Depurar - Frigate", + "default": "Paràmetres - Frigate", + "authentication": "Configuració d'autenticació - Frigate", + "camera": "Paràmetres de càmera - Frigate", + "masksAndZones": "Editor de màscares i zones - Frigate", + "general": "Configuració del perfil - Frigate", + "frigatePlus": "Paràmetres de Frigate+ - Frigate", + "notifications": "Paràmetres de notificació - Frigate", + "cameraManagement": "Gestionar càmeres - Frigate", + "cameraReview": "Configuració Revisió de Càmeres - Frigate", + "globalConfig": "Configuració global - Frigate", + "cameraConfig": "Configuració de la càmera - Frigate", + "maintenance": "Manteniment - Frigate", + "profiles": "Perfils - Frigate", + "detectorsAndModel": "Detectors i model - Frigate" + }, + "menu": { + "ui": "Interfície d'usuari", + "cameras": "Paràmetres de la càmera", + "masksAndZones": "Màscares / Zones", + "motionTuner": "Afinador de moviment", + "users": "Usuaris", + "notifications": "Notificacions", + "debug": "Depuració", + "frigateplus": "Frigate+", + "enrichments": "Enriquiments", + "triggers": "Disparadors", + "cameraManagement": "Gestió de la càmera", + "cameraReview": "Revisió", + "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": "Detecció 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": "Fluxos (FFmpeg)", + "cameraRecording": "Enregistrament", + "cameraSnapshots": "Instantànies", + "cameraMotion": "Detecció de moviment", + "cameraObjects": "Objectes", + "cameraConfigReview": "Revisió", + "cameraAudioEvents": "Detecció 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ó", + "uiSettings": "Paràmetres de la IU", + "profiles": "Perfils", + "systemGo2rtcStreams": "go2rtc streams", + "systemDetectorsAndModel": "Detectors i model" + }, + "dialog": { + "unsavedChanges": { + "title": "Hi ha canvis no guardats.", + "desc": "Desar els canvis abans de continuar?" + } + }, + "cameraSetting": { + "camera": "Càmera", + "noCamera": "Cap càmera" + }, + "general": { + "title": "Paràmetres de la IU", + "liveDashboard": { + "title": "Panell en directe", + "automaticLiveView": { + "label": "Vista en directe automàtica", + "desc": "Canvia automàticament a la vista en directe d’una càmera quan es detecta activitat. Desactivar aquesta opció fa que les imatges estàtiques de la càmera al panell en directe s’actualitzin només un cop per minut." + }, + "playAlertVideos": { + "label": "Reproduir vídeos d’alerta", + "desc": "Per defecte, les alertes recents al tauler en directe es reprodueixen com a vídeos petits en bucle. Desactiva aquesta opció per mostrar només una imatge estàtica de les alertes recents en aquest dispositiu/navegador." + }, + "displayCameraNames": { + "label": "Mostra sempre els noms de la càmera", + "desc": "Mostra sempre els noms de les càmeres en un xip al tauler de visualització en directe multicàmera." + }, + "liveFallbackTimeout": { + "label": "Temps d'espera per a la reserva del jugador en directe", + "desc": "Quan el flux en viu d'alta qualitat d'una càmera no està disponible, torneu al mode d'amplada de banda baixa després d'aquests molts segons. Per defecte: 3." + } + }, + "storedLayouts": { + "title": "Disposicions desades", + "desc": "La disposició de les càmeres en un grup es pot arrossegar i redimensionar. Les posicions es guarden a l’emmagatzematge local del teu navegador.", + "clearAll": "Esborra tots les disposicions" + }, + "cameraGroupStreaming": { + "desc": "La configuració de la transmissió per a cada grup de càmeres s'emmagatzema de manera local al vostre navegador.", + "title": "Parmàmetres de transmissió del grup de càmeres", + "clearAll": "Esborra tots els paràmetres de transmissió" + }, + "recordingsViewer": { + "title": "Visualitzador d'enregistraments", + "defaultPlaybackRate": { + "label": "Velocitat de reproducció predeterminada", + "desc": "Velocitat de reproducció predeterminada per a la reproducció de gravacions." + } + }, + "calendar": { + "firstWeekday": { + "monday": "Dilluns", + "label": "Primer dia de la setmana", + "sunday": "Diumenge", + "desc": "El dia en que comencen les setmanes del calendari de revisions." + }, + "title": "Calendari" + }, + "toast": { + "success": { + "clearStoredLayout": "Disposició emmagatzemada esborrada per {{cameraName}}", + "clearStreamingSettings": "S'han suprimit els paràmetres de la transmissió per tots els grups de càmeres." + }, + "error": { + "clearStoredLayoutFailed": "Error en suprimir la disposició desada: {{errorMessage}}", + "clearStreamingSettingsFailed": "Error en esborrar els paràmetres de la transmissió: {{errorMessage}}" + } + } + }, + "masksAndZones": { + "form": { + "polygonDrawing": { + "snapPoints": { + "false": "No ajustar punts", + "true": "Punts d'ajust" + }, + "delete": { + "success": "{{name}} s'ha suprimit.", + "title": "Confirmar la supressió", + "desc": "Estas segur que vols suprimir el {{type}} {{name}}?" + }, + "removeLastPoint": "Eliminar l'últim punt", + "reset": { + "label": "Neteja tots els punts" + }, + "error": { + "mustBeFinished": "El dibuix del polígon s'ha d'acabar abans de desar." + }, + "type": { + "zone": "zona", + "motion_mask": "màscara de moviment", + "object_mask": "màscara d'objecte" + }, + "revertOverride": { + "title": "Reverteix a la configuració base", + "desc": "Això eliminarà la substitució de perfil per {{type}} {{name}} i tornarà a la configuració base." + } + }, + "zoneName": { + "error": { + "hasIllegalCharacter": "El nom de la zona conté caràcters il·legals.", + "mustBeAtLeastTwoCharacters": "El nom de la zona ha de contenir com a mínim 2 caràcters.", + "mustNotContainPeriod": "El nom de la zona no pot contenir punts.", + "alreadyExists": "Ja existeix una zona amb aquest nom per a aquesta càmera.", + "mustNotBeSameWithCamera": "El nom de la zona no pot ser el mateix que el nom de la càmera.", + "mustHaveAtLeastOneLetter": "El nom de la zona ha de tenir almenys una lletra." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "L'inèrcia ha de ser superior a 0." + } + }, + "distance": { + "error": { + "text": "La distància ha de ser major o igual a 0.1.", + "mustBeFilled": "Cal omplir tots els camps de distància per poder utilitzar l’estimació de la velocitat." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "El llindar de velocitat ha de ser major o igual a 0.1." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "El temps de merodeig ha de ser mes gran o igual a 0." + } + }, + "id": { + "error": { + "mustNotBeEmpty": "L'ID no pot estar buit.", + "alreadyExists": "Ja existeix una màscara amb aquest ID per a aquesta càmera." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "El nom no pot estar buit." + } + } + }, + "zones": { + "objects": { + "title": "Objectes", + "desc": "Llista d'objectes que apliquen per aquesta zona." + }, + "speedEstimation": { + "lineDDistance": "Distància de la línia D ({{unit}})", + "title": "Estimació de velocitat", + "docs": "Llegir la documentació", + "lineADistance": "Distància de la línia A ({{unit}})", + "lineBDistance": "Distància de la línia B ({{unit}})", + "lineCDistance": "Distància de la línia C ({{unit}})", + "desc": "Habilita l'estimació de velocitat per a objectes dins d'aquesta zona. La zona ha de tenir exactament 4 punts." + }, + "inertia": { + "title": "Inèrcia", + "desc": "Especifica quants fotogrames ha d’estar un objecte dins d’una zona abans de considerar-se que hi és. Per defecte: 3" + }, + "point_one": "{{count}} punt", + "point_many": "{{count}} punts", + "point_other": "{{count}} punts", + "name": { + "inputPlaceHolder": "Introduïu un nom…", + "title": "Nom", + "tips": "El nom ha de tenir almenys 2 caràcters, ha de tenir almenys una lletra, i no ha de ser el nom d'una càmera o una altra zona en aquesta càmera." + }, + "label": "Zones", + "desc": { + "documentation": "Documentació", + "title": "Les zones permeten definir una àrea específica de la imatge per tal de determinar si un objecte es troba dins d'una àrea concreta o no." + }, + "add": "Afegir Zona", + "edit": "Editar zona", + "loiteringTime": { + "title": "Temps de merodeig", + "desc": "Estableix el temps mínim, en segons, que l'objecte ha d'estar dins la zona perquè s'activi. Per defecte: 0" + }, + "allObjects": "Tots els objectes", + "documentTitle": "Edita zona - Frigate", + "speedThreshold": { + "title": "Llindar de velocitat ({{unit}})", + "toast": { + "error": { + "loiteringTimeError": "Zones amb temps de merodeig superior a 0 no s'han d'utilitzar per a l'estimació de velocitat.", + "pointLengthError": "L'estimació de velocitat s'ha desactivat per a aquesta zona. Les zones amb estimació de velocitat han de tenir exactament 4 points." + } + }, + "desc": "Especifica una velocitat mínima dels objectes per ser considerats dins d'aquesta zona." + }, + "clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.", + "toast": { + "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": { + "all": "Totes les màscares i zones" + }, + "motionMasks": { + "desc": { + "documentation": "Documentació", + "title": "Les màscares de moviment s’utilitzen per evitar que certs tipus de moviment no desitjats activin la detecció. Si s’aplica una màscara excessiva, es dificultarà el seguiment dels objectes." + }, + "context": { + "documentation": "Llegir la documentació", + "title": "Les màscares de moviment s’utilitzen per evitar que certs tipus de moviment no desitjats activin la detecció (per exemple: branques d’arbres, marques temporals). Les màscares de moviment s’han d’utilitzar amb molta moderació, un excés de màscares dificultarà el seguiment dels objectes." + }, + "polygonAreaTooLarge": { + "documentation": "Llegir la documentació", + "tips": "Les màscares de moviment no impedeixen la detecció d'objectes. Hauries de fer servir una zona requerida en el seu lloc.", + "title": "La màscara de moviment cobreix el {{polygonArea}}% del camp de visió de la càmera. Les màscares de moviment molt grans no son recomanables." + }, + "point_one": "{{count}} punt", + "point_many": "{{count}} punts", + "point_other": "{{count}} punts", + "label": "Màscara de moviment", + "add": "Nova màscara de moviment", + "edit": "Edita la màscara de moviment", + "documentTitle": "Editar la màscara de moviment - Frigate", + "clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.", + "toast": { + "success": { + "title": "{{polygonName}} s'ha desat.", + "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": { + "documentTitle": "Editar la màscara d'objecte - Frigate", + "label": "Màscares d'objecte", + "edit": "Editar la màscara d'objecte", + "point_one": "{{count}} punt", + "point_many": "{{count}} punts", + "point_other": "{{count}} punts", + "objects": { + "allObjectTypes": "Tots els tipus d’objecte", + "title": "Objectes", + "desc": "El tipus d'objecte que s'aplica a la màscara d'objectes." + }, + "add": "Afegir màscara d'objecte", + "desc": { + "documentation": "Documentació", + "title": "Les màscares de filtratge d’objectes s’utilitzen per descartar falsos positius d’un tipus d’objecte concret segons la seva ubicació." + }, + "clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.", + "toast": { + "success": { + "title": "{{polygonName}} s'ha desat.", + "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ó.", + "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)", + "motionMaskLabel": "Màscara de moviment {{number}}", + "objectMaskLabel": "Màscara d'objecte {{number}}", + "toast": { + "success": { + "copyCoordinates": "S'han copiat les coordenades per a {{polyName}} al porta-retalls." + }, + "error": { + "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ó." + } + }, + "profileBase": "(base)", + "profileOverride": "(sobreescriu)", + "addDisabledProfile": "Afegiu primer a la configuració base i després sobreescriviu-ho al perfil" + }, + "notification": { + "email": { + "title": "Correu electrònic", + "placeholder": "p. ex. exemple@email.com", + "desc": "Es requereix un correu electrònic vàlid que s’utilitzarà per notificar-te si hi ha algun problema amb el servei de notificacions push." + }, + "notificationSettings": { + "documentation": "Llegir la documentació", + "title": "Paràmetres de notificació", + "desc": "Frigate pot enviar notificacions push directament al teu dispositiu quan s’executa des del navegador o està instal·lat com a PWA (aplicació web progressiva)." + }, + "deviceSpecific": "Paràmetres específics del dispositiu", + "registerDevice": "Registrar aquest dispositiu", + "unregisterDevice": "Desegistrar aquest dispositiu", + "cancelSuspension": "Cancel·la la suspensió", + "suspendTime": { + "untilRestart": "Suspendre fins al reinici", + "30minutes": "Suspèn durant 30 minuts", + "1hour": "Suspèn durant 1 hora", + "12hours": "Suspèn durant 12 hores", + "suspend": "Suspendre", + "24hours": "Suspèn durant 24 hores", + "10minutes": "Suspèn durant 10 minuts", + "5minutes": "Suspèn durant 5 minuts" + }, + "toast": { + "success": { + "settingSaved": "Els paràmetres de notificacions s'han desat.", + "registered": "Registre de notificacions realitzat amb èxit. Cal reiniciar Frigate perquè es puguin enviar notificacions (inclosa una notificació de prova)." + }, + "error": { + "registerFailed": "No s'ha pogut desar el registre de notificacions." + } + }, + "cameras": { + "title": "Càmeres", + "noCameras": "Sense càmeres disponibles", + "desc": "Selecciona per a quines càmeres s'han d'habilitar les notificacions." + }, + "title": "Notificacions", + "notificationUnavailable": { + "title": "Notificacions no disponibles", + "documentation": "Llegir la documentació", + "desc": "Les notificacions push web requereixen un context segur (https://…). Aquesta és una limitació del navegador. Accedeix a Frigate de manera segura per utilitzar les notificacions.", + "descPwa": "A iOS, les notificacions push web només estàn disponibles quan Frigate està instalat a la pantalla principal. Obre el menú Compartir , selecciona Afegir a la pantalla, i obre Frigate des del nou icona per registrar les notificacions en aquest dispositiu." + }, + "unsavedChanges": "Canvis de notificació no desats", + "globalSettings": { + "title": "Paràmetres globals", + "desc": "Suspendre temporalment les notificacions per a certes càmeres per tots els dispositius registrats." + }, + "active": "Notificacions actives", + "suspended": "Notificacions suspeses {{time}}", + "unsavedRegistrations": "Registres de notificació no desats", + "sendTestNotification": "Enviar una notificació de prova" + }, + "camera": { + "streams": { + "title": "Transmissions", + "desc": "Desactiva temporalment una càmera fins que es reiniciï Frigate. La desactivació d'una càmera atura completament el processament de les transmissions d'aquesta càmera per part de Frigate. La detecció, gravació i depuració no estaran disponibles.
    Nota: Això no desactiva les retransmissions de go2rtc." + }, + "title": "Paràmetres de la càmera", + "reviewClassification": { + "title": "Revisar la classificació", + "readTheDocumentation": "Llegir la documentació", + "selectAlertsZones": "Seleccionar zones per alertes", + "limitDetections": "Limitar deteccions a zones específiques", + "selectDetectionsZones": "Seleccionar zones per deteccions", + "unsavedChanges": "Paràmetres de la revisió de classificació no guardats per a {{camera}}", + "noDefinedZones": "No s'han definit zones per a aquesta càmera.", + "objectAlertsTips": "Tots els objectes {{alertsLabels}} a {{cameraName}} es mostraràn com a Alertes.", + "zoneObjectAlertsTips": "Tots els objectes {{alertsLabels}} detectats a la {{zone}} de {{cameraName}} es mostraràn com a Alertes.", + "toast": { + "success": "S'ha desat la configuració de la classificació de revisió. Reinicia Frigate per aplicar els canvis." + }, + "zoneObjectDetectionsTips": { + "text": "Tots els objectes {{detectionsLabels}} no classificats a la {{zone}} de {{cameraName}} es mostraràn com a Deteccions.", + "notSelectDetections": "Tots els objectes {{detectionsLabels}} detectats a {{zone}} de la càmera {{cameraName}} que no estiguin categoritzats com a Alertes es mostraran com a Deteccions, independentment de la zona en què es trobin.", + "regardlessOfZoneObjectDetectionsTips": "Tots els objectes {{detectionsLabels}} no categoritzats a {{cameraName}} es mostraran com a Deteccions independentment de la zona en què es trobin." + }, + "objectDetectionsTips": "Tots els objectes {{detectionsLabels}} no categoritzats a {{cameraName}} es mostraran com a Deteccions independentment de la zona en què es trobin.", + "desc": "Frigate categoritza els elements de revisió com a Alertes i Deteccions. Per defecte, tots els objectes de tipus persona i cotxe es consideren Alertes. Pots afinar la categorització dels teus elements de revisió configurant zones requerides per a aquests." + }, + "review": { + "alerts": "Alertes ", + "detections": "Deteccions ", + "title": "Revisar", + "desc": "Habilita o deshabilita temporalment les alertes i deteccions per a aquesta càmera fins que es reiniciï Frigate. Quan estigui desactivat, no es generaran nous elements de revisió. " + }, + "object_descriptions": { + "title": "Descripció d'objectes per IA generativa", + "desc": "Activar/desactivar temporalment la IA generativa de descripcions per aquesta càmera. Quan està desactivat, les descripcions d'IA generativa no seran requerides per als objectes seguits per aquesta càmera." + }, + "review_descriptions": { + "title": "Revisar las descripcions d'IA generativa", + "desc": "Activar/desactivals temporalment les descripcions d'IA generativa per aquesta càmera. Quan estan desactivades, les descripcions d'IA generativa no serán requerides per revisar els items en aquesta càmera." + }, + "addCamera": "Afegir Nova Càmera", + "editCamera": "Editar Càmera:", + "selectCamera": "Seleccionar Càmera", + "backToSettings": "Tornar a la Configuració de Càmera", + "cameraConfig": { + "add": "Afegir Càmera", + "edit": "Editar Càmera", + "description": "Configurar la càmera incloent les entrades y rols.", + "name": "Nom de Càmera", + "nameRequired": "El nom de càmera es necesari", + "nameLength": "El nom de la càmera ha de ser com a mínim de 24 caràcters.", + "namePlaceholder": "e.x., porta_entrada", + "enabled": "Activat", + "ffmpeg": { + "inputs": "Entrades", + "path": "Direcció d'entrada", + "pathRequired": "Direcció d'entrada necesaria", + "pathPlaceholder": "rtsp://...", + "roles": "Rols", + "rolesRequired": "Com a mínin un rol es necesari", + "rolesUnique": "Cada rol (audio, detecció, gravació) pot ser assiganda a una entrada", + "addInput": "Afegir una entrada", + "removeInput": "Esborrar una entrada", + "inputsRequired": "Com a mínim una entrada es necesaria" + }, + "toast": { + "success": "La càmera {{cameraName}} s'ha guardat correctament" + } + } + }, + "motionDetectionTuner": { + "Threshold": { + "title": "Llindar", + "desc": "El valor del llindar determina quanta variació en la luminància d’un píxel cal perquè es consideri moviment. Per defecte: 30" + }, + "contourArea": { + "title": "Àrea de contorn", + "desc": "El valor de l’àrea del contorn s’utilitza per decidir quins grups de píxels canviats es consideren moviment. Valor per defecte: 10" + }, + "desc": { + "documentation": "Llegeix la guia d'ajust de detecció de moviment", + "title": "Frigate utilitza la detecció de moviment com a primer filtre per comprovar si hi ha alguna activitat a la imatge que valgui la pena analitzar amb la detecció d’objectes." + }, + "improveContrast": { + "title": "Millorar contrast", + "desc": "Millora el contrast per les escenes fosques. Predeterminat: ACTIVAT" + }, + "title": "Afinador de detecció de moviment", + "toast": { + "success": "S'han desat els paràmetres del moviment." + }, + "unsavedChanges": "Canvis no desats en l'ajust de moviment {{camera}}" + }, + "debug": { + "title": "Depuració", + "objectList": "Llista d'objectes", + "noObjects": "Cap objecte", + "debugging": "Depurant", + "mask": { + "title": "Màscares de moviment", + "desc": "Mostra els polígons de la màscara de moviment" + }, + "regions": { + "title": "Regions", + "desc": "Mostre un requadre de la regió d'interés enviat al detector d'objectes", + "tips": "

    Requadres de Regió


    Requadres verds es sobreposaran a les àrees d’interès de la imatge que s'envien al detector d’objectes.

    " + }, + "objectShapeFilterDrawing": { + "score": "Puntuació", + "document": "Llegir la documentació ", + "ratio": "Proporció", + "area": "Àrea", + "title": "Dibuix del filtre de forma de l'objecte", + "desc": "Dibuixa un rectangle a la imatge per veure detalls d'àrea i proporció", + "tips": "Habilita aquesta opció per dibuixar un rectangle a la imatge de la càmera que mostri la seva àrea i proporció. Aquests valors es poden utilitzar després per configurar els paràmetres del filtre de forma d’objecte a la teva configuració." + }, + "zones": { + "title": "Zones", + "desc": "Mostra el contorn per a qualsevol zona definida" + }, + "timestamp": { + "title": "Marca temporal", + "desc": "Superposa una marca temporal a la imatge" + }, + "boundingBoxes": { + "title": "Caixes delimitadores", + "colors": { + "label": "Colors de la caixa delimitadora de l'objecte", + "info": "
  • En iniciar, s'assignarà un color diferent a cada etiqueta d’objecte
  • Una línia fina de color blau fosc indica que l’objecte no està detectat en aquest moment
  • Una línia fina de color gris indica que l’objecte està detectat com a estacionari
  • Una línia gruixuda indica que l’objecte és el subjecte de l’autoseguiment (quan està activat)
  • " + }, + "desc": "Mostra les caixes delimitadores al voltant dels objectes rastrejats" + }, + "motion": { + "title": "Caixes de moviment", + "desc": "Mostra requadres al voltant de les àrees on s'ha detectat moviment", + "tips": "

    Caixes de moviment


    Es sobreposaran requadres vermells a les àrees del fotograma on actualment s’estigui detectant moviment.

    " + }, + "detectorDesc": "Frigate fa servir els teus detectors ({{detectors}}) per a detectar objectes a les imatges de la teva càmera.", + "desc": "La vista de depuració mostra en temps real els objectes rastrejats i les seves estadístiques. La llista d’objectes mostra un resum amb retard temporal dels objectes detectats.", + "openCameraWebUI": "Obrir la interficie d'usuari de {{camera}}", + "audio": { + "title": "Audio", + "noAudioDetections": "No hi ha deteccions d'audio", + "score": "puntuació", + "currentRMS": "RMS Actual", + "currentdbFS": "dbFS Actual" + }, + "paths": { + "title": "Rutes", + "desc": "Mostrar els punts significatius de la ruta dels objectes seguits", + "tips": "

    Rutes


    Les línies i cercles indicarán els punts significatius dels objectes seguits durant el seu cicle de vida.

    " + } + }, + "users": { + "table": { + "username": "Usuari", + "password": "Restableix la contrasenya", + "deleteUser": "Suprimir usuari", + "noUsers": "No s'han trobat usuaris.", + "changeRole": "Canviar la funció d’usuari", + "actions": "Accions", + "role": "Rol" + }, + "toast": { + "error": { + "deleteUserFailed": "No s'ha pogut eliminar l'usuari: {{errorMessage}}", + "roleUpdateFailed": "No s'ha pogut actualitzar la funció: {{errorMessage}}", + "setPasswordFailed": "Error en guardar la contrasenya: {{errorMessage}}", + "createUserFailed": "No s'ha pogut crear l'usuari: {{errorMessage}}" + }, + "success": { + "deleteUser": "L'usuari {{user}} s'ha suprimit amb èxit", + "createUser": "L'Usuari {{user}} s'ha creat amb èxit", + "updatePassword": "Contrasenya actualitzada amb èxit.", + "roleUpdated": "Funció actualitzada per {{user}}" + } + }, + "dialog": { + "form": { + "user": { + "title": "Nom d'usuari", + "placeholder": "Introdueix el nom d'usuari", + "desc": "Només es permeten lletres, números, punts i guions baixos." + }, + "password": { + "confirm": { + "placeholder": "Confirma contrasenya", + "title": "Confirma contrasenya" + }, + "strength": { + "title": "Seguretat de la contrasenya: ", + "weak": "Dèbil", + "strong": "Fort", + "veryStrong": "Molt forta", + "medium": "Mitjana" + }, + "notMatch": "Les contrasenyes no coincideixen", + "match": "Les contrasenyes coincideixen", + "placeholder": "Introdueix la contrasenya", + "title": "Contrasenya", + "show": "Mostra contrasenya", + "hide": "Amaga contrasenya", + "requirements": { + "title": "Requisits contrasenya:", + "length": "Com a mínim 12 carácters", + "uppercase": "Com a mínim una majúscula", + "digit": "Com a mínim un digit", + "special": "Com a mínim un carácter especial (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nova contrasenya", + "placeholder": "Introduïu una nova contrasenya", + "confirm": { + "placeholder": "Re-entrar contrasenya nova" + } + }, + "usernameIsRequired": "El nom d'usuari és obligatori", + "passwordIsRequired": "La contrasenya és obligatoria", + "currentPassword": { + "title": "Constrasenya actual", + "placeholder": "Entra l'actual contrasenya" + } + }, + "passwordSetting": { + "updatePassword": "Contrasenya actualitzada per {{username}}", + "setPassword": "Estableix Contrasenya", + "cannotBeEmpty": "La contrasenya no pot ser buida", + "doNotMatch": "Les contrasenyes no coincideixen", + "desc": "Crea un nova contrasenya segura per protegir aquest compte.", + "currentPasswordRequired": "L'actual contrasenya es requerida", + "incorrectCurrentPassword": "L'actual contrasenya es incorrecte", + "passwordVerificationFailed": "Falla en la verificació de la contrasenya", + "multiDeviceWarning": "Serà necesari loguejarte en qualsevol altre dispositiu en que estiguis loguejat en {{refresh_time}}.", + "multiDeviceAdmin": "També pots forçar a tots els usuaris a tornar a autenticar-se immediatament rotant el teu secret JWT." + }, + "deleteUser": { + "title": "Suprimir usuari", + "warn": "Estàs segur que vols suprimir {{username}}?", + "desc": "Aquesta acció no es pot desfer. Això eliminarà permanentment el compte d'usuari i suprimirà totes les dades associades." + }, + "changeRole": { + "roleInfo": { + "viewer": "Visualitzador", + "admin": "Administrador", + "adminDesc": "Accés complet a totes les funcionalitats.", + "intro": "Selecciona el rol adequat per a aquest usuari:", + "viewerDesc": "Limitat només a panells en directe, revisió, exporació i exportació.", + "customDesc": "Rol personalitzat per accés específic a una cámera." + }, + "title": "Canviar la funció d’usuari", + "desc": "Actualitzar permisos per a {{username}}", + "select": "Seleccioneu un paper" + }, + "createUser": { + "title": "Crear un nou usuari", + "confirmPassword": "Siusplau, confirma la contrasenya", + "usernameOnlyInclude": "El nom d'usuari només pot contenir lletres, números, . o _", + "desc": "Afegeix un nou compte d'usuari i especifica un rol per accedir a àrees de la interfície de Frigate." + } + }, + "title": "Usuaris", + "addUser": "Afegir usuari", + "management": { + "title": "Gestió d'usuaris", + "desc": "Gestioneu els comptes d'usuari d'aquesta instància de Frigate." + }, + "updatePassword": "Restableix la contrasenya" + }, + "frigatePlus": { + "snapshotConfig": { + "table": { + "camera": "Càmera", + "snapshots": "Instantànies", + "cleanCopySnapshots": "clean_copy Instantànies" + }, + "title": "Configuració d'instantànies", + "documentation": "Llegir la documentació", + "desc": "Per a enviar a Frigate+ fa falta que la instantània estigui habilitada a la configuració.", + "cleanCopyWarning": "Algunes càmeres tenen la captura desactivada" + }, + "modelInfo": { + "baseModel": "Model base", + "modelType": "Tipus de model", + "trainDate": "Data d'entrenament", + "title": "Informació del model", + "supportedDetectors": "Detectors compatibles", + "availableModels": "Models Frigate+ disponibles", + "cameras": "Càmeres", + "plusModelType": { + "userModel": "Afinat", + "baseModel": "Model base" + }, + "loadingAvailableModels": "Carregant models disponibles…", + "loading": "Carregant informació del model…", + "error": "No s'ha pogut carregar la informació del model", + "modelSelect": "Els models disponibles a Frigate+ es poden seleccionar aquí. Tingues en compte que només es poden triar els models compatibles amb la configuració actual del detector.", + "noModelLoaded": "Actualment no s'ha carregat cap model Frigate+.", + "selectModel": "Selecciona un model", + "noModelsAvailable": "No hi ha models disponibles", + "filter": { + "ariaLabel": "Filtra els models per tipus", + "baseModels": "Models de base", + "fineTunedModels": "Models ajustats" + } + }, + "apiKey": { + "plusLink": "Llegeix més sobre Frigate+", + "title": "Clau API de Frigate+", + "validated": "La clau API de Frigate+ ha estat detectada i validada", + "notValidated": "La clau API de Frigate+ no ha estat detectada o no ha estat validada", + "desc": "La clau API de Frigate+ habilita la integració amb el servei Frigate+." + }, + "unsavedChanges": "Canvis dels paràmetres de Frigate+ sense desar", + "title": "Paràmetres de Frigate+", + "toast": { + "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." + }, + "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ó" + }, + "changeInDetectorsAndModel": "Canviar model" + }, + "enrichments": { + "semanticSearch": { + "modelSize": { + "small": { + "title": "petit", + "desc": "Si s'utilitza small, s'empra una versió quantitzada del model que consumeix menys memòria RAM i s'executa més ràpidament a la CPU, amb una diferència inapreciable en la qualitat dels vectors." + }, + "label": "Mida del model", + "large": { + "title": "gran", + "desc": "L’opció large fa servir el model complet de Jina i s’executarà automàticament a la GPU si està disponible." + }, + "desc": "La mida del model utilitzat per als vectors de la cerca semàntica." + }, + "reindexNow": { + "confirmButton": "Reindexar", + "success": "La reindexació ha començat amb èxit.", + "label": "Reindexar ara", + "confirmTitle": "Confirmar la reindexació", + "desc": "La reindexació tornarà a generar els vectors de tots els objectes detectats. Aquest procés s'executa en segon pla, pot posar la CPU al màxim i trigar una bona estona segons el nombre d'objectes detectats que tingueu.", + "confirmDesc": "Segur que voleu tornar a indexar els vectors de tots els objectes detectats? Aquest procés s'executa en segon pla, però pot posar la CPU al màxim i trigar una bona estona. En podeu veure el progrés a la pàgina Explora.", + "alreadyInProgress": "La reindexació ja està en curs.", + "error": "Error en iniciar la reindexació: {{errorMessage}}" + }, + "readTheDocumentation": "Llegir la documentació", + "title": "Cerca semàntica", + "desc": "La cerca semàntica a Frigate permet trobar objectes rastrejats dins dels elements de revisió utilitzant la pròpia imatge, una descripció de text definida per l'usuari o una de generada automàticament." + }, + "faceRecognition": { + "modelSize": { + "small": { + "title": "petit", + "desc": "Fer servir la opció petit fa servir un model d'embedding de rostre de FaceNet que d'executa de manera eficient a la majoria de les CPUs." + }, + "large": { + "title": "gran", + "desc": "L’opció large fa servir el model d'embedding de rostres d'ArcFace i s’executarà automàticament a la GPU si està disponible." + }, + "label": "Mida del model", + "desc": "La mida del model utilitzat per al reconeixement facial." + }, + "readTheDocumentation": "Llegir la documentació", + "title": "Reconeixement de rostres", + "desc": "El reconeixement facial permet a les persones assignar noms i quan es reconeix la seva cara Frigate assignarà el nom de la persona com a subetiqueta. Aquesta informació s'inclou en la interfície d'usuari, filtres, així com en les notificacions." + }, + "unsavedChanges": "Canvis dels paràmetres complementaris sense desar", + "licensePlateRecognition": { + "readTheDocumentation": "Llegir la documentació", + "title": "Reconeixement de matrícules", + "desc": "Frigate pot reconèixer les plaques de matrícula en vehicles i afegir automàticament els caràcters detectats al camp de la placa reconeguda o un nom conegut com a sub_etiqueta en objectes que són de tipus cotxe. Un cas d'ús comú pot ser llegir les plaques de matrícula dels cotxes que entren en un lloc o els cotxes que passen per un carrer." + }, + "birdClassification": { + "title": "Classificació d'ocells", + "desc": "La classificació d’ocells identifica ocells coneguts mitjançant un model TensorFlow quantitzat. Quan es reconeix un ocell conegut, el seu nom comú s’afegeix com a subetiqueta. Aquesta informació es mostra a la interfície d’usuari, als filtres i també a les notificacions." + }, + "title": "Configuració dels enriquiments", + "toast": { + "error": "No s'han pogut guardar els canvis de configuració: {{errorMessage}}", + "success": "Els paràmetres complementaris s'han desat. Reinicia Frigate per aplicar els canvis." + }, + "restart_required": "És necessari reiniciar (Han cambiat paràmetres complementaris)" + }, + "triggers": { + "table": { + "actions": "Accions", + "noTriggers": "No hi ha disparadors configurats en aquesta càmera.", + "edit": "Editar", + "deleteTrigger": "Esborrar Disparador", + "lastTriggered": "Últim Disparo", + "name": "Nom", + "type": "Tipus", + "content": "Contingut", + "threshold": "Llindar" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descripció" + }, + "actions": { + "alert": "Marcar com Alerta", + "notification": "Enviar Notificació", + "sub_label": "Afegeix una subetiqueta", + "attribute": "Afegeix un atribut" + }, + "dialog": { + "createTrigger": { + "title": "Crear Disparador", + "desc": "Crear disparador per una càmera {{camera}}" + }, + "editTrigger": { + "title": "Editar Disparador", + "desc": "Editar la configuració per al disparador de càmera {{camera}}" + }, + "deleteTrigger": { + "title": "Esborrar Disparador", + "desc": "Estas segur que vols esborrar el disparador {{triggerName}}? Aquesta acció no es pot desfer." + }, + "form": { + "name": { + "title": "Nom", + "placeholder": "Anomena aquest activador", + "error": { + "minLength": "El camp ha de tenir almenys 2 caràcters.", + "invalidCharacters": "El camp només pot contenir lletres, números, guions baixos i guions.", + "alreadyExists": "El disparador amb aquest nom ja existeix per aquesta càmera." + }, + "description": "Introduïu un nom o una descripció únics per a identificar aquest activador" + }, + "enabled": { + "description": "Activar o desactivar aquest disparador" + }, + "type": { + "title": "Tipus", + "placeholder": "Selecciona un tipus de disparador", + "description": "Activa quan es detecta una descripció similar d'un objecte rastrejat", + "thumbnail": "Activa quan es detecti una miniatura d'objecte rastrejada similar" + }, + "content": { + "title": "Contingut", + "imagePlaceholder": "Selecciona una miniatura", + "textPlaceholder": "Entra el contingut de text", + "imageDesc": "Només es mostren les 100 miniatures més recents. Si no podeu trobar la miniatura desitjada, reviseu els objectes anteriors a Explora i configureu un activador des del menú.", + "textDesc": "Entra el text per disparar aquesta acció quan es detecti una descripció d'objecte a rastrejar similar.", + "error": { + "required": "Contigunt requerit." + } + }, + "threshold": { + "title": "Llindar", + "error": { + "min": "El llindar ha de ser mínim 0", + "max": "El llindar ha de ser máxim 1" + }, + "desc": "Estableix el llindar de similitud per a aquest activador. Un llindar més alt significa que es requereix una coincidència més propera per disparar el disparador." + }, + "actions": { + "title": "Accions", + "desc": "Per defecte, Frigate dispara un missatge MQTT per a tots els activadors. Subetiquetes afegeix el nom de l'activador a l'etiqueta de l'objecte. Els atributs són metadades cercables emmagatzemades per separat a les metadades de l'objecte rastrejat.", + "error": { + "min": "S'ha de seleccionar una acció com a mínim." + } + }, + "friendly_name": { + "title": "Nom amistós", + "placeholder": "Nom o descripció d'aquest disparador", + "description": "Un nom opcional amistós o text descriptiu per a aquest activador." + } + } + }, + "toast": { + "success": { + "createTrigger": "El disparador {{name}} s'ha creat existosament.", + "updateTrigger": "El disparador {{name}} s'ha actualitzat correctament.", + "deleteTrigger": "El disparador {{name}} s'ha borrat correctament." + }, + "error": { + "createTriggerFailed": "Error al crear el disparador: {{errorMessage}}", + "updateTriggerFailed": "Error a l'actualitzar el disparador: {{errorMessage}}", + "deleteTriggerFailed": "Error a l'esborrar el disparador: {{errorMessage}}" + } + }, + "documentTitle": "Disparadors", + "management": { + "title": "Activadors", + "desc": "Gestionar els disparadors de {{camera}}. Usa els tipus de miniatures per disparar miniatures similars a l'objecte a seguir seleccionat, i el tipus de descripció per disparar en cas de descripcions similars a l'especificada." + }, + "addTrigger": "Afegir disaprador", + "semanticSearch": { + "desc": "La cerca semàntica ha d'estar activada per a utilitzar els activadors.", + "title": "La cerca semàntica està desactivada" + }, + "wizard": { + "title": "Crea un activador", + "step1": { + "description": "Configura la configuració bàsica per al vostre activador." + }, + "step2": { + "description": "Configura el contingut que activarà aquesta acció." + }, + "step3": { + "description": "Configura el llindar i les accions d'aquest activador." + }, + "steps": { + "nameAndType": "Nom i tipus", + "configureData": "Configura les dades", + "thresholdAndActions": "Llindar i accions" + } + } + }, + "roles": { + "dialog": { + "form": { + "cameras": { + "required": "Almenys has de seleccionar una càmera.", + "title": "Càmeres", + "desc": "Selecciona les càmeres que tingui accés aquest rol. Com a mínim s'ha de seleccionar una càmera." + }, + "role": { + "title": "Nom del Rol", + "placeholder": "Entra el nom del rol", + "desc": "Només lletres, números, els punts i subrallats están permesos.", + "roleIsRequired": "Nom del Rol requerit", + "roleOnlyInclude": "El nom de Rol només pot incloure lletres, nombres, . o _", + "roleExists": "Ja existeis un rol amb aquest nom." + } + }, + "createRole": { + "title": "Crear nou Rol", + "desc": "Afegir nou rol y especificar permisos d'accés." + }, + "editCameras": { + "title": "Editar Càmeres Rol", + "desc": "Actualitza l'acces a les càmeres per al rol {{role}}." + }, + "deleteRole": { + "title": "Eliminar Rol", + "desc": "Aquesta acció no pot ser restablerta. S'esborrarà permenentment el rol y els usuaris asignats amb aquest rol de 'visor', que els dona accés a totes les càmeres.", + "warn": "Estas segur que vols eliminar {{role}}?", + "deleting": "Eliminant..." + } + }, + "management": { + "title": "Gestió del Rols de Visors", + "desc": "Gestiona els rols visors personalitzats y els seus permisos d'accés per aquesta instancia de Frigate." + }, + "addRole": "Afegir Rol", + "table": { + "role": "Rol", + "cameras": "Càmeres", + "actions": "Accions", + "noRoles": "No s'han trobat rols personalitzats.", + "editCameras": "Editar Càmeres", + "deleteRole": "Eliminar Rol" + }, + "toast": { + "success": { + "createRole": "Rol {{role}} creat exitosament", + "updateCameras": "Càmeres actualitzades per al rol {{role}}", + "deleteRole": "Rol {{role}} eliminat exitosament", + "userRolesUpdated_one": "{{count}} l'usuari assignat a aquest rol s'ha actualitzat a 'visor', que té accés a totes les càmeres.", + "userRolesUpdated_many": "{{count}} usuaris assignats a aquest rol s'han actualitzat a 'visor', que té accés a totes les càmeres.", + "userRolesUpdated_other": "{{count}} usuaris assignats a aquest rol s'han actualitzat a 'visor', que té accés a totes les càmeres." + }, + "error": { + "createRoleFailed": "Error al crear el rol: {{errorMessage}}", + "updateCamerasFailed": "Error a l'actualitzar les càmeres: {{errorMessage}}", + "deleteRoleFailed": "Error a l'eliminar el rol: {{errorMessage}}", + "userUpdateFailed": "Error a l'actualitzar els ros d'usuari: {{errorMessage}}" + } + } + }, + "cameraWizard": { + "title": "Afegir Càmera", + "description": "Seguiu els passos de sota per afegir una nova càmera a la instal·lació.", + "steps": { + "nameAndConnection": "Nom i connexió", + "streamConfiguration": "Configuració de stream", + "validationAndTesting": "Validació i proves", + "probeOrSnapshot": "Prova o instantània" + }, + "step1": { + "cameraBrand": "Marca de la càmera", + "description": "Introduïu els detalls de la càmera i trieu provar la càmera o seleccionar manualment la marca.", + "cameraName": "Nom de la càmera", + "cameraNamePlaceholder": "p. ex., vista general de la porta davantera o de la barra posterior", + "host": "Adreça de l'amfitrió/IP", + "port": "Port", + "username": "Nom d'usuari", + "usernamePlaceholder": "Opcional", + "password": "Contrasenya", + "passwordPlaceholder": "Opcional", + "selectTransport": "Selecciona el protocol de transport", + "brandInformation": "Informació de marca", + "brandUrlFormat": "Per a càmeres amb el format d'URL RTSP com: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://usuari:contrasenya@host:port/ruta", + "testConnection": "Prova la connexió", + "testSuccess": "Prova de connexió correcta!", + "testFailed": "Ha fallat la prova de connexió. Si us plau, comproveu la vostra entrada i torneu-ho a provar.", + "streamDetails": "Detalls del flux", + "warnings": { + "noSnapshot": "No s'ha pogut obtenir una instantània del flux configurat." + }, + "errors": { + "brandOrCustomUrlRequired": "Seleccioneu una marca de càmera amb host/IP o trieu 'Altres' amb un URL personalitzat", + "nameRequired": "Es requereix el nom de la càmera", + "nameLength": "El nom de la càmera ha de tenir 64 caràcters o menys", + "invalidCharacters": "El nom de la càmera conté caràcters no vàlids", + "nameExists": "El nom de la càmera ja existeix", + "brands": { + "reolink-rtsp": "No es recomana Reolink RST. Es recomana habilitar HTTP a la configuració de la càmera i reiniciar l'assistent de la càmera." + }, + "customUrlRtspRequired": "Els URL personalitzats han de començar amb \"rtsp://\" o \"rtsps://\". Es requereix configuració manual per a fluxos de càmera no RTSP." + }, + "selectBrand": "Seleccioneu la marca de la càmera per a la plantilla d'URL", + "customUrl": "URL de flux personalitzat", + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "S'estan provant les metadades de la càmera...", + "fetchingSnapshot": "S'està recuperant la instantània de la càmera..." + }, + "connectionSettings": "Configuració de la connexió", + "detectionMethod": "Mètode de detecció de flux", + "onvifPort": "ONVIF Port", + "probeMode": "Càmera de prova", + "manualMode": "Selecció manual", + "detectionMethodDescription": "Proveu la càmera amb ONVIF (si és compatible) per trobar URL de flux de càmera, o seleccioneu manualment la marca de càmera per utilitzar URL predefinits. Per a introduir un URL RTSP personalitzat, trieu el mètode manual i seleccioneu \"Altres\".", + "onvifPortDescription": "Per a les càmeres que suporten ONVIF, això sol ser 80 o 8080.", + "useDigestAuth": "Utilitza l'autenticació digest", + "useDigestAuthDescription": "Usa l'autenticació de resum HTTP per a ONVIF. Algunes càmeres poden requerir un nom d'usuari/contrasenya ONVIF dedicat en lloc de l'usuari administrador estàndard." + }, + "save": { + "failure": "S'ha produït un error en desar {{cameraName}}.", + "success": "S'ha desat correctament la càmera nova {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolució", + "video": "Vídeo", + "audio": "Àudio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Proporcioneu un URL de flux vàlid", + "testFailed": "Ha fallat la prova de flux: {{error}}" + }, + "step2": { + "description": "Proveu la càmera per als fluxos disponibles o configureu la configuració manual basada en el mètode de detecció seleccionat.", + "streamsTitle": "Fluxos de la càmera", + "addStream": "Afegeix un flux", + "addAnotherStream": "Afegeix un altre flux", + "streamTitle": "Flux {{number}}", + "streamUrl": "URL del flux", + "url": "URL", + "resolution": "Resolució", + "selectResolution": "Selecciona la resolució", + "quality": "Qualitat", + "selectQuality": "Selecciona la qualitat", + "roleLabels": { + "detect": "Detecció d'objectes", + "record": "Enregistrament", + "audio": "Àudio" + }, + "testStream": "Prova la connexió", + "testSuccess": "Prova de connexió correcta!", + "testFailed": "Ha fallat la prova de connexió. Si us plau, comproveu la vostra entrada i torneu-ho a provar.", + "testFailedTitle": "Ha fallat la prova", + "connected": "Connectat", + "notConnected": "No connectat", + "featuresTitle": "Característiques", + "go2rtc": "Redueix les connexions a la càmera", + "detectRoleWarning": "Almenys un flux ha de tenir el rol de \"detecte\" per continuar.", + "rolesPopover": { + "title": "Rols de flux", + "detect": "Canal principal per a la detecció d'objectes.", + "record": "Desa els segments del canal de vídeo basats en la configuració.", + "audio": "Canal per a la detecció basada en àudio." + }, + "featuresPopover": { + "title": "Característiques del flux", + "description": "Utilitzeu el restreaming go2rtc per reduir les connexions a la càmera." + }, + "roles": "Rols", + "streamUrlPlaceholder": "rtsp://usuari:contrasenya@host:port/ruta", + "streamDetails": "Detalls del flux", + "probing": "Provant càmera...", + "retry": "Intentar de nou", + "testing": { + "probingMetadata": "S'estan provant les metadades de la càmera...", + "fetchingSnapshot": "S'està recuperant la instantània de la càmera..." + }, + "probeFailed": "No s'ha pogut provar la càmera: {{error}}", + "probingDevice": "Provant dispositiu...", + "probeSuccessful": "Prova exitosa", + "probeError": "Error de prova", + "probeNoSuccess": "La prova no ha tingut èxit", + "deviceInfo": "Informació del dispositiu", + "manufacturer": "Fabricant", + "model": "Model", + "firmware": "Firmware", + "profiles": "Perfils", + "ptzSupport": "Suport PTZ", + "autotrackingSupport": "Implementació de seguiment automàtic", + "presets": "Predefinits", + "rtspCandidates": "Candidats RTSP", + "rtspCandidatesDescription": "S'han trobat els següents URL RTSP de la sonda de la càmera. Proveu la connexió per a veure les metadades del flux.", + "noRtspCandidates": "No s'ha trobat cap URL RTSP a la càmera. Les vostres credencials poden ser incorrectes, o la càmera pot no admetre ONVIF o el mètode utilitzat per recuperar els URL RTSP. Torneu enrere i introduïu l'URL RTSP manualment.", + "candidateStreamTitle": "Candidat {{number}}", + "useCandidate": "Utilitza", + "uriCopy": "Copia", + "uriCopied": "URI copiat al porta-retalls", + "testConnection": "Prova la connexió", + "toggleUriView": "Feu clic per a commutar la vista completa de l'URI", + "errors": { + "hostRequired": "Es requereix l'adreça de l'amfitrió/IP" + } + }, + "step3": { + "none": "Cap", + "error": "Error", + "saveAndApply": "Desa una càmera nova", + "saveError": "Configuració no vàlida. Si us plau, comproveu la configuració.", + "issues": { + "title": "Validació del flux", + "videoCodecGood": "El còdec de vídeo és {{codec}}.", + "audioCodecGood": "El còdec d'àudio és {{codec}}.", + "noAudioWarning": "No s'ha detectat cap àudio per a aquest flux, els enregistraments no tindran àudio.", + "audioCodecRecordError": "El còdec d'àudio AAC és necessari per a suportar l'àudio en els enregistraments.", + "audioCodecRequired": "Es requereix un flux d'àudio per admetre la detecció d'àudio.", + "restreamingWarning": "Reduir les connexions a la càmera per al flux de registre pot augmentar lleugerament l'ús de la CPU.", + "dahua": { + "substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Dahua / Amcrest / EmpireTech suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles." + }, + "hikvision": { + "substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Hikvision suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles." + }, + "resolutionHigh": "Una resolució de {{resolution}} pot causar un ús més gran dels recursos.", + "resolutionLow": "Una resolució de {{resolution}} pot ser massa baixa per a la detecció fiable d'objectes petits." + }, + "description": "Configura els rols de flux i afegeix fluxos addicionals per a la càmera.", + "validationTitle": "Validació del flux", + "connectAllStreams": "Connecta tots els fluxos", + "reconnectionSuccess": "S'ha reconnectat correctament.", + "reconnectionPartial": "Alguns fluxos no s'han pogut tornar a connectar.", + "streamUnavailable": "La vista prèvia del flux no està disponible", + "reload": "Torna a carregar", + "connecting": "Connectant...", + "streamTitle": "Flux {{number}}", + "valid": "Vàlid", + "failed": "Ha fallat", + "notTested": "No provat", + "connectStream": "Connecta", + "connectingStream": "Connectant", + "disconnectStream": "Desconnecta", + "estimatedBandwidth": "Amplada de banda estimad", + "roles": "Rols", + "streamValidated": "El flux {{number}} s'ha validat correctament", + "streamValidationFailed": "Ha fallat la validació del flux {{number}}", + "ffmpegModule": "Usa el mode de compatibilitat del flux", + "ffmpegModuleDescription": "Si el flux no es carrega després de diversos intents, proveu d'activar-ho. Quan està activat, Frigate utilitzarà el mòdul ffmpeg amb go2rtc. Això pot proporcionar una millor compatibilitat amb alguns fluxos de càmera.", + "streamsTitle": "Fluxos de la càmera", + "addStream": "Afegeix un flux", + "addAnotherStream": "Afegeix un altre flux", + "streamUrl": "URL del flux", + "streamUrlPlaceholder": "rtsp://usuari:contrasenya@host:port/ruta", + "selectStream": "Selecciona un flux", + "searchCandidates": "Cerca candidats...", + "noStreamFound": "No s'ha trobat cap flux", + "url": "URL", + "resolution": "Resolució", + "selectResolution": "Selecciona la resolució", + "quality": "Qualitat", + "selectQuality": "Selecciona la qualitat", + "roleLabels": { + "detect": "Detecció d'objectes", + "record": "Enregistrament", + "audio": "Àudio" + }, + "testStream": "Prova la connexió", + "testSuccess": "Prova de flux amb èxit!", + "testFailed": "Ha fallat la prova del flux", + "testFailedTitle": "Ha fallat la prova", + "connected": "Connectat", + "notConnected": "No connectat", + "featuresTitle": "Característiques", + "go2rtc": "Redueix les connexions a la càmera", + "detectRoleWarning": "Almenys un flux ha de tenir el rol de \"detecte\" per continuar.", + "rolesPopover": { + "title": "Roles de flux", + "detect": "Canal principal per a la detecció d'objectes.", + "record": "Desa els segments del canal de vídeo basats en la configuració.", + "audio": "Canal per a la detecció basada en àudio." + }, + "featuresPopover": { + "title": "Característiques del flux", + "description": "Utilitzeu el restreaming go2rtc per reduir les connexions a la càmera." + } + }, + "step4": { + "description": "Validació i anàlisi final abans de desar la nova càmera. Connecta cada flux abans de desar-lo.", + "validationTitle": "Validació del flux", + "connectAllStreams": "Connecta tots els fluxos", + "reconnectionSuccess": "S'ha reconnectat correctament.", + "reconnectionPartial": "Alguns fluxos no s'han pogut tornar a connecta.", + "streamUnavailable": "La vista prèvia del flux no està disponible", + "reload": "Torna a carregar", + "connecting": "S'està connectant...", + "streamTitle": "Flux {{number}}", + "valid": "Vàlid", + "failed": "Ha fallat", + "notTested": "No provat", + "connectStream": "Connecta", + "connectingStream": "Connectant", + "disconnectStream": "Desconnecta", + "estimatedBandwidth": "Amplada de banda estimada", + "roles": "Roles", + "ffmpegModule": "Usa el mode de compatibilitat del flux", + "ffmpegModuleDescription": "Si el flux no es carrega després de diversos intents, proveu d'activar-ho. Quan està activat, Frigate utilitzarà el mòdul ffmpeg amb go2rtc. Això pot proporcionar una millor compatibilitat amb alguns fluxos de càmera.", + "none": "Cap", + "error": "Error", + "streamValidated": "El flux {{number}} s'ha validat correctament", + "streamValidationFailed": "Ha fallat la validació del flux {{number}}", + "saveAndApply": "Desa una càmera nova", + "saveError": "Configuració no vàlida. Si us plau, comproveu la configuració.", + "issues": { + "title": "Validació del flux", + "videoCodecGood": "El còdec de vídeo és {{codec}}.", + "audioCodecGood": "El còdec d'àudio és {{codec}}.", + "resolutionHigh": "Una resolució de {{resolution}} pot causar un ús més gran dels recursos.", + "resolutionLow": "Una resolució de {{resolution}} pot ser massa baixa per a la detecció fiable d'objectes petits.", + "noAudioWarning": "No s'ha detectat cap àudio per a aquest flux, els enregistraments no tindran àudio.", + "audioCodecRecordError": "El còdec d'àudio AAC és necessari per a suportar l'àudio en els enregistraments.", + "audioCodecRequired": "Es requereix un flux d'àudio per admetre la detecció d'àudio.", + "restreamingWarning": "Reduir les connexions a la càmera per al flux de registre pot augmentar lleugerament l'ús de la CPU.", + "brands": { + "reolink-rtsp": "No és racomana utilitzar Reolink RSTP. Activeu HTTP a la configuració del microprogramari de la càmera i reinicieu l'assistent.", + "reolink-http": "Els fluxos HTTP de reenllaç haurien d'utilitzar FFmpeg per a una millor compatibilitat. Habilita «Utilitza el mode de compatibilitat del flux» per a aquest flux." + }, + "dahua": { + "substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Dahua / Amcrest / EmpireTech suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles." + }, + "hikvision": { + "substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Hikvision suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles." + }, + "resolutionUnknown": "La resolució d'aquest flux no s'ha pogut investigar. Heu d'establir manualment la resolució de detecció a Configuració o a la configuració." + } + } + }, + "cameraManagement": { + "title": "Gestiona les càmeres", + "addCamera": "Afegeix una càmera nova", + "editCamera": "Edita la càmera:", + "selectCamera": "Selecciona una càmera", + "backToSettings": "Torna a la configuració de la càmera", + "streams": { + "title": "Estat i detalls de la càmera", + "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.
    Nota: això no desactiva les retransmissions de go2rtc.", + "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.
    Nota: això no inhabilita els restreams go2rtc.

    Drag el handle per reordenar les càmeres tal com apareixen a la interfície d'usuari. L'ordre de les càmeres habilitades es reflectirà en tota la interfície d'usuari, incloent el tauler en viu i els desplegables de selecció de càmeres.", + "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. Reinicia Frigate a aplicar.", + "friendlyName": { + "edit": "Edita el nom de la pantalla de la càmera", + "title": "Edita el nom de la pantalla", + "description": "Estableix el nom amigable que es mostra per a aquesta càmera a tota la interfície d'usuari de la Fragata. Deixeu-ho en blanc per utilitzar l'ID de la càmera.", + "rename": "Canvia el nom" + }, + "reorderHandle": "Arrossega per reordenar", + "saving": "S'està desant…", + "saved": "Desat", + "details": { + "edit": "Edita els detalls de la càmera", + "title": "Edita els detalls de la càmera", + "description": "Actualitza el nom de visualització, l'URL extern i la visibilitat utilitzada per a aquesta càmera a tota la interfície d'usuari de la Fragata.", + "friendlyNameLabel": "Nom a mostrar", + "friendlyNameHelp": "Nom amistós que es mostra per a aquesta càmera a tota la interfície d'usuari de Frigate. Deixeu-ho en blanc per utilitzar l'ID de la càmera.", + "webuiUrlLabel": "URL de la interfície web de la càmera", + "webuiUrlHelp": "URL per a visitar la interfície d'usuari web de la càmera directament des de la vista de depuració. Deixeu-ho en blanc per desactivar l'enllaç.", + "webuiUrlInvalid": "Ha de ser un URL vàlid (p. ex., https://example.com).", + "dashboardLabel": "Mostra al tauler en directe", + "dashboardHelp": "Mostra aquesta càmera al Tauler en viu.", + "reviewLabel": "Mostra a la ressenya", + "reviewHelp": "Mostra aquesta càmera a Revisió, incloent el filtre de càmera, la revisió de moviment i la vista de l'historial." + }, + "label": "Estat de la càmera", + "description": "Estableix l'estat operatiu de cada càmera.

    A: els fluxos es processen normalment.
    Off: pausa temporalment el processament. No persisteix a través de reinicis de Frigate.
    Inhabilitat: deixa de processar i desa el canvi a la configuració. Es requereix un reinici per a tornar a habilitar una càmera inhabilitada.

    Nota: La inhabilitació no afecta els restreams de go2rtc.

    Arrossegueu l'ansa per a reordenar les càmeres actives a mesura que apareguin a tota la interfície d'usuari, inclosos els desplegables de selecció de quadres en viu i de càmera.", + "disabledSubheading": "Desactivat en la configuració", + "status": { + "on": "Engegat", + "off": "Apagat", + "disabled": "Desactivat" + }, + "disableSuccess": "{{cameraName}} desactivat i desat a la configuració." + }, + "cameraConfig": { + "add": "Afegeix una càmera", + "edit": "Edita la càmera", + "description": "Configura la configuració de la càmera, incloses les entrades i els rols de flux.", + "name": "Nom de la càmera", + "nameRequired": "Es requereix el nom de la càmera", + "nameLength": "El nom de la càmera ha de tenir menys de 64 caràcters.", + "namePlaceholder": "p. ex., vista general de la porta davantera o de la barra posterior", + "enabled": "Habilitat", + "ffmpeg": { + "inputs": "Fluxos d'entrada", + "path": "Camí del flux", + "pathRequired": "Es requereix un camí de flux", + "pathPlaceholder": "rtsp://...", + "rolesRequired": "Es requereix almenys un rol", + "rolesUnique": "Cada rol (àudio, detecta, registra) només es pot assignar a un flux", + "addInput": "Afegeix un flux d'entrada", + "removeInput": "Elimina el flux d'entrada", + "inputsRequired": "Es requereix com a mínim un flux d'entrada", + "roles": "Rols" + }, + "go2rtcStreams": "go2rtc Fluxos", + "streamUrls": "URL de flux", + "addUrl": "Afegeix un URL", + "addGo2rtcStream": "Afegeix go2rtc flux", + "toast": { + "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 {{cameraName}} 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}}" + }, + "profiles": { + "title": "Sobreescriu la càmera de perfil", + "selectLabel": "Seleccioneu el perfil", + "description": "Configura quines càmeres estan activades o desactivades quan s'activa un perfil. Les càmeres establertes a «herit» mantenen el seu estat per defecte.", + "inherit": "Hereta", + "enabled": "Habilitat", + "disabled": "Desactivat", + "on": "Engegat", + "off": "Apagat" + }, + "cameraType": { + "title": "Tipus de càmera", + "label": "Tipus de càmera", + "description": "Estableix el tipus per a cada càmera. Les càmeres LPR dedicades són càmeres d'un sol ús amb un potent zoom òptic per capturar matrícules en vehicles distants. La majoria de les càmeres haurien d'utilitzar el tipus de càmera normal llevat que la càmera sigui específicament per a LPR i tingui una vista molt centrada en les matrícules.", + "dedicatedLpr": "LPR dedicat", + "saveSuccess": "Tipus de càmera actualitzat per {{cameraName}}. Reinicia Frigate per aplicar els canvis.", + "normal": "Normal" + }, + "description": "Afegiu, editeu i suprimiu les càmeres, controleu l'estat de cada càmera, i configureu les superposicions per perfil i tipus de càmera. Per a configurar fluxos, detecció, moviment i altres paràmetres específics de la càmera, trieu la secció específica a Configuració de la càmera.", + "clone": { + "sectionTitle": "Clona la configuració", + "sectionDescription": "Copia la configuració d'una càmera a una altra càmera o una de nova.", + "button": "Clona la configuració", + "title": "Clona la configuració de la càmera", + "description": "Copia la configuració d'una càmera a una o més càmeres o a una càmera nova. La identitat (nom, nom amigable, URL de la interfície d'usuari web, ordre de visualització) no es copia mai.", + "source": { + "label": "Càmera d'origen", + "placeholder": "Seleccioneu una càmera d'origen", + "required": "Seleccioneu una càmera d'origen" + }, + "target": { + "legend": "Objectiu", + "newRadio": "Càmara nova", + "newNameLabel": "Nom de la càmera", + "newNamePlaceholder": "p. ex., porta enrere orporta o porta posterior", + "newNameInvalid": "Es requereix el nom de la càmera", + "newNameCollision": "Ja existeix una càmera amb aquest nom", + "newStreamsForced": "Els fluxos sempre es copien per a una càmera nova.", + "existingCamerasRadio": "Càmeres existents", + "allCameras": "Totes les càmeres", + "existingPlaceholder": "Selecciona almenys una càmera", + "existingDisabled": "No hi ha cap altra càmera a la qual copiar", + "newNameRequired": "Es requereix el nom de la càmera" + }, + "categories": { + "legend": "Configuració per clonar", + "description": "Trieu quina configuració voleu copiar de la càmera d'origen.", + "selectAll": "Selecciona-ho tot", + "selectNone": "No en seleccioneu cap", + "resetDefaults": "Restableix als valors predeterminats", + "general": "General", + "spatial": "Paràmetres espacials", + "streams": "Fluxos", + "spatialWarningTitle": "La resolució no coincideix", + "spatialWarning": "La càmera d'origen {{srcCamera}} detecta la resolució ({{srcWidth}}.{{srcHeight}}) difereix de: {{cameras}}. És possible que els polígons no s'alineïn en aquestes càmeres. Aquests valors predeterminats estan desactivats; habiliteu-ho per a copiar tal qual.", + "restartHint": "Reinicia requerit", + "items": { + "record": "Enregistrament", + "snapshots": "Instantànies", + "review": "Revisió", + "motion": "Detecció de moviment", + "objects": "Objectes", + "audio": "Detecció d'àudio", + "audio_transcription": "Transcripció d'àudio", + "notifications": "Notificacions", + "birdseye": "Birdseye", + "timestamp_style": "Estil de la marca horària", + "lpr": "Reconeixement de la matrícula", + "face_recognition": "Reconeixement de cares", + "semantic_search": "Cerca semàntica", + "genai": "IA generativa", + "type": "Tipus de càmera (LPR normal / dedicat)", + "profiles": "Perfils", + "detect": "Detecta les dimensions", + "zones": "Zones", + "motion_mask": "Màscares de moviment", + "object_masks": "Màscares d'objecte", + "ffmpeg_live": "URL i rols de flux", + "mqtt": "MQTT", + "onvif": "ONVIF" + } + }, + "footer": { + "changeCount_one": "{{count}} s'aplicarà el canvi", + "changeCount_many": "{{count}} canvis s'aplicaran", + "changeCount_other": "{{count}} canvis s'aplicaran", + "restartNeeded": "Es requerirà reiniciar per a alguns canvis.", + "liveOnly": "Tots els canvis s'aplicaran en viu sense reiniciar.", + "submit": "Clona", + "submitting": "S'està clonant…" + }, + "toast": { + "success": "Configuració copiada a {{cameraName}}", + "successWithRestart": "Configuració copiada a {{cameraName}}. Reinicia Frigate per aplicar tots els canvis.", + "successMulti_one": "Configuració copiada a la càmera {{count}}", + "successMulti_many": "Configuració copiada a {{count}} càmeres", + "successMulti_other": "Configuració copiada a {{count}} càmeres", + "successMultiWithRestart_one": "Configuració copiada a la càmera {{count}}. Reinicia Frigate per aplicar tots els canvis.", + "successMultiWithRestart_many": "Configuració copiada a {{count}} càmeres. Reinicia Frigate per aplicar tots els canvis.", + "successMultiWithRestart_other": "Configuració copiada a {{count}} càmeres. Reinicia la fragata per aplicar tots els canvis.", + "partialFailure": "{{successCount}} seccions aplicades; «{{failedSection}}» ha fallat: {{errorMessage}}", + "partialFailureMulti": "S'ha copiat a {{successCount}} càmera(es); ha fallat {{failed}}: {{errorMessage}}", + "newCameraPartialFailure": "S'ha creat la càmera {{cameraName}} però no s'han pogut copiar alguns paràmetres: {{errorMessage}}", + "sourceMissing": "La càmera d'origen ja no existeix", + "submitError": "No s'ha pogut clonar la càmera: {{errorMessage}}" + } + } + }, + "cameraReview": { + "object_descriptions": { + "title": "Descripcions d'objectes generadors d'IA", + "desc": "Activa/desactiva temporalment les descripcions d'objectes generatius d'IA per a aquesta càmera fins que es reiniciï Frigate. Quan està desactivat, les descripcions generades per IA no se sol·licitaran per als objectes rastrejats en aquesta càmera." + }, + "review_descriptions": { + "title": "Descripcions de la IA generativa", + "desc": "Activa/desactiva temporalment les descripcions de la IA Generativa per a aquesta càmera fins que es reiniciï Frigate. Quan està desactivat, les descripcions generades per IA no se sol·licitaran per als elements de revisió d'aquesta càmera." + }, + "review": { + "title": "Revisió", + "desc": "Activa/desactiva temporalment les alertes i deteccions d'aquesta càmera fins que es reiniciï Frigate. Si està desactivat, no es generaran nous elements de revisió. ", + "alerts": "Alertes. ", + "detections": "Deteccions. " + }, + "reviewClassification": { + "title": "Revisió de la classificació", + "desc": "Frigate categoritza els articles de revisió com Alertes i Deteccions. Per defecte, tots els objectes persona i cotxe es consideren Alertes. Podeu refinar la categorització dels elements de revisió configurant-los les zones requerides.", + "noDefinedZones": "No hi ha zones definides per a aquesta càmera.", + "selectAlertsZones": "Selecciona zones per a les alertes", + "selectDetectionsZones": "Selecció de zones per a les deteccions", + "limitDetections": "Limita les deteccions a zones específiques", + "toast": { + "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}}", + "objectAlertsTips": "Totes els objectes {{alertsLabels}} de {{cameraName}} es mostraran com avisos.", + "zoneObjectAlertsTips": "Tots els objectes{{alertsLabels}} detectats en {{zone}} de {{cameraName}} es mostraran com a avisos.", + "objectDetectionsTips": "Tots els objectes {{detectionsLabels}} que no estiguin categoritzats de {{cameraName}} es mostraran com a Deteccions independentment de la zona on es trobin.", + "zoneObjectDetectionsTips": { + "text": "Tots els objectes {{detectionsLabels}} no categoritzats a {{zone}} de {{cameraName}} es mostraran com a Deteccions.", + "notSelectDetections": "Tots els objectes {{detectionsLabels}} detectats a {{zone}} de{{cameraName}} no categoritzats com a alertes es mostraran com a Deteccions independentment de la zona on es trobin.", + "regardlessOfZoneObjectDetectionsTips": "Tots els objectes {{detectionsLabels}} que no estiguin categoritzats de {{cameraName}} es mostraran com a Deteccions independentment de la zona on es trobin." + } + }, + "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" + }, + "profile": { + "label": "Perfil" + } + }, + "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", + "verbose": "Verbose", + "verboseDesc": "Escriu una llista completa de fitxers orfes al disc per revisar-los." + }, + "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 Frigate 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.", + "noCameras": "No hi ha càmeres disponibles" + }, + "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", + "none": "Cap", + "useGlobalSetting": "Hereta de l'entorn global", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "JPEG HTTP (Genèric)", + "preset-http-mjpeg-generic": "HTTP MJPEG (Generic)", + "preset-http-reolink": "HTTP - Reolink càmeres", + "preset-rtmp-generic": "RTMP (Generic)", + "preset-rtsp-generic": "RTSP (Generic)", + "preset-rtsp-restream": "RTSP - Restream de go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - Restream de go2rtc (Latència baixa)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Enregistra (Genèric, sense àudio)", + "preset-record-generic-audio-copy": "Enregistra (Genèric + Copia l'àudio)", + "preset-record-generic-audio-aac": "Enregistra (Genèric + Àudio a AAC)", + "preset-record-mjpeg": "Registre - Càmeres MJPEG", + "preset-record-jpeg": "Registre - Càmeres JPEG", + "preset-record-ubiquiti": "Registre - Càmeres Ubiquiti" + } + }, + "cameraInputs": { + "itemTitle": "Flux {{index}}", + "sourceMode": { + "restream": "Restream (go2rtc)", + "manual": "Camí d'entrada manual", + "go2rtcStreamLabel": "flux go2rtc", + "go2rtcStreamPlaceholder": "Selecciona un flux go2rtc", + "noGo2rtcStreams": "No s'ha configurat cap flux go2rtc", + "go2rtcStreamSearch": "Cerca fluxos...", + "availableStreams": "Fluxos disponibles", + "noMatchingStreams": "No hi ha fluxos coincidents" + } + }, + "restartRequiredField": "Reinicia requerit", + "restartRequiredFooter": "S'ha canviat la configuració - es requereix reiniciar", + "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", + "masksAndZones": "Màscares / Zones" + }, + "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", + "addCustomKey": "Afegeix una clau personalitzada" + }, + "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...", + "genaiRoles": { + "options": { + "embeddings": "Vectors", + "vision": "Visió", + "tools": "Eines", + "descriptions": "Descripcions", + "chat": "Xat" + } + }, + "semanticSearchModel": { + "placeholder": "Selecciona el model…", + "builtIn": "Models integrats", + "genaiProviders": "Proveïdors de GenAI" + }, + "reviewLabels": { + "summary": "{{count}} etiquetes seleccionades", + "empty": "No hi ha etiquetes disponibles", + "allNonAlertDetections": "Totes les activitats no alertes s'inclouran com a deteccions." + }, + "addCustomLabel": "Afegeix una etiqueta personalitzada...", + "genaiModel": { + "placeholder": "Seleccioneu o introduïu un model…", + "search": "Cerca o introdueix un model…", + "noModels": "No hi ha models disponibles", + "available": "Models disponibles", + "useCustom": "Utilitza \"{{value}}\"", + "refresh": "Actualitza els models", + "probeFailed": "No s'han pogut investigar els models", + "fetchedModels": "S'ha obtingut correctament la llista de models" + }, + "knownPlates": { + "namePlaceholder": "per exemple. Cotxe de la parella", + "platePlaceholder": "Matricula o regex" + }, + "semanticSearchModelSize": { + "notApplicable": "No aplicable als proveïdors de GenAI" + }, + "liveStreams": { + "streamNameLabel": "Nom del flux", + "streamNamePlaceholder": "p. ex., corrent HD principal", + "go2rtcStreamLabel": "flux go2rtc", + "go2rtcStreamPlaceholder": "Selecciona un flux go2rtc", + "go2rtcStreamSearch": "Cerca o introdueix un nom de flux…", + "noGo2rtcStreams": "No s'ha configurat cap flux go2rtc", + "availableStreams": "Fluxos disponibles", + "useCustom": "Utilitza \"{{value}}\"", + "addStream": "Afegeix un flux" + }, + "ptzPresets": { + "placeholder": "Selecciona o entra una configuració...", + "search": "Busca o entra una configuració...", + "noPresets": "No hi ha configuracions disponibles", + "available": "Parámetres de Cámera", + "useCustom": "Usa \"{{value}}\"" + }, + "defaultRole": { + "admin": "Administrar", + "viewer": "Visor" + } + }, + "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", + "saveAllSuccessRestartRequired_one": "S'ha desat la secció {{count}} correctament. Reinicia Frigate per aplicar els canvis.", + "saveAllSuccessRestartRequired_many": "Totes les {{count}} seccions s'han desat correctament. Reinicia Frigate per aplicar els canvis.", + "saveAllSuccessRestartRequired_other": "Totes les {{count}} seccions s'han desat correctament. Reinicia Frigate per aplicar els canvis." + }, + "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.", + "button": { + "overriddenGlobal": "Sobreescrit (Global)", + "overriddenGlobalTooltip": "Aquesta càmera anul·la la configuració global d'aquesta secció", + "overriddenBaseConfig": "Sobreescrit (Configuració base)", + "overriddenBaseConfigTooltip": "El perfil {{profile}} substitueix la configuració d'aquesta secció", + "overriddenInCameras": { + "label_one": "Sobreescrit a la càmera {{count}}", + "label_many": "Sobreescrit en {{count}} càmeres", + "label_other": "Sobreescrit en {{count}} càmeres", + "tooltip_one": "{{count}} la càmera anul·la els valors d'aquesta secció. Feu clic per veure els detalls.", + "tooltip_many": "{{count}} càmeres substitueixen els valors d'aquesta secció. Feu clic per veure els detalls.", + "tooltip_other": "{{count}} càmeres substitueixen els valors d'aquesta secció. Feu clic per veure els detalls.", + "heading_one": "Aquesta secció global té camps que estan sobreescrits a la càmera {{count}}.", + "heading_many": "Aquesta secció global té camps que estan sobreescrits en {{count}} càmeres.", + "heading_other": "Aquesta secció global té camps que estan sobreescrits en {{count}} càmeres.", + "othersField_one": "{{count}} altre", + "othersField_many": "{{count}} altres", + "othersField_other": "{{count}} altres", + "profilePrefix": "Perfil {{profile}}: {{fields}}" + }, + "overriddenGlobalHeading_one": "Aquesta càmera substitueix el camp {{count}} de la configuració global:", + "overriddenGlobalHeading_many": "Aquesta càmera anul·la {{count}} camps de la configuració global:", + "overriddenGlobalHeading_other": "Aquesta càmera anul·la {{count}} camps de la configuració global:", + "overriddenGlobalNoDeltas": "Aquesta càmera anul·la la configuració global, però no hi ha valors de camp diferents.", + "overriddenBaseConfigHeading_one": "El perfil {{profile}} substitueix el camp {{count}} de la configuració base:", + "overriddenBaseConfigHeading_many": "El perfil {{profile}} substitueix {{count}} camps de la configuració base:", + "overriddenBaseConfigHeading_other": "El perfil {{profile}} substitueix {{count}} camps de la configuració base:", + "overriddenBaseConfigNoDeltas": "El perfil {{profile}} substitueix aquesta secció, però no hi ha valors de camp diferents de la configuració base." + }, + "profiles": { + "title": "Perfils", + "activeProfile": "Perfil actiu", + "noActiveProfile": "No hi ha un perfil actiu", + "active": "Actiu", + "activated": "S'ha activat el perfil '{{profile}}'", + "activateFailed": "No s'ha pogut establir el perfil", + "deactivated": "Perfil desactivat", + "noProfiles": "No s'ha definit cap perfil.", + "noOverrides": "No hi ha excepcions", + "cameraCount_one": "{{count}} càmera", + "cameraCount_many": "{{count}} càmeres", + "cameraCount_other": "{{count}} càmeres", + "baseConfig": "Configuració base", + "addProfile": "Afegeix un perfil", + "newProfile": "Perfil nou", + "profileNamePlaceholder": "p. ex., Armat, lluny, mode nocturn", + "friendlyNameLabel": "Nom del perfil", + "profileIdLabel": "ID del perfil", + "profileIdDescription": "Identificador intern utilitzat en la configuració i les automatitzacions", + "nameInvalid": "Només es permeten lletres en minúscula, números i guions baixos", + "nameDuplicate": "Ja existeix un perfil amb aquest nom", + "error": { + "mustBeAtLeastTwoCharacters": "Ha de tenir com a mínim 2 caràcters", + "mustNotContainPeriod": "No ha de contenir períodes", + "alreadyExists": "Ja existeix un perfil amb aquest ID" + }, + "renameProfile": "Canvia el nom del perfil", + "renameSuccess": "Perfil reanomenat a '{{profile}}'", + "deleteProfile": "Suprimeix el perfil", + "deleteProfileConfirm": "Voleu suprimir el perfil \"{{profile}}\" de totes les càmeres? Això no es pot desfer.", + "deleteSuccess": "S'ha suprimit el perfil '{{profile}}'", + "createSuccess": "S'ha creat el perfil '{{profile}}'", + "removeOverride": "Elimina la sobreescriptura del perfil", + "deleteSection": "Suprimeix les excepcions de secció", + "deleteSectionConfirm": "Voleu eliminar les sobreescriptures de {{section}} del perfil {{profile}} a {{camera}}?", + "deleteSectionSuccess": "S'han suprimit {{section}} sobreescrits per {{profile}}", + "enableSwitch": "Habilita els perfils", + "enabledDescription": "Els perfils estan habilitats. Creeu un perfil nou a continuació, navegueu a una secció de configuració de la càmera per fer els vostres canvis i deseu perquè els canvis tinguin efecte.", + "disabledDescription": "Els perfils permeten definir conjunts de configuracions de càmera amb nom (p. ex., armats, fora, nit) que es poden activar sota demanda.", + "columnCamera": "Càmara", + "columnOverrides": "Sobreescriu el perfil" + }, + "go2rtcStreams": { + "title": "go2rtc Corrents", + "description": "Gestiona les configuracions de flux go2rtc per al restreaming de la càmera. Cada flux té un nom i un o més URL d'origen.", + "addStream": "Afegeix un flux", + "addStreamDesc": "Introduïu un nom per al flux nou. Aquest nom s'utilitzarà per a fer referència al flux en la configuració de la càmera.", + "addUrl": "Afegeix un URL", + "streamName": "Nom del flux", + "streamNamePlaceholder": "p. ex., porta d'entrada", + "streamUrlPlaceholder": "e.g., rtsp://usuari:contrasenya@192.168.1.100/flux", + "deleteStream": "Suprimeix el flux", + "deleteStreamConfirm": "Segur que voleu suprimir el flux \"{{streamName}}\"? Les càmeres que fan referència a aquest flux poden deixar de funcionar.", + "noStreams": "No s'ha configurat cap flux go2rtc. Afegeix un flux per començar.", + "validation": { + "nameRequired": "Es requereix el nom del flux", + "nameDuplicate": "Ja existeix un flux amb aquest nom", + "nameInvalid": "El nom del flux només pot contenir lletres, números, guions baixos i guions", + "urlRequired": "Es requereix com a mínim un URL" + }, + "renameStream": "Canvia el nom del flux", + "renameStreamDesc": "Introduïu un nom nou per a aquest flux. El canvi de nom d'un flux pot trencar les càmeres o altres fluxos que el fan referència pel seu nom.", + "newStreamName": "Nom de flux nou", + "ffmpeg": { + "useFfmpegModule": "Usa el mode de compatibilitat (ffmpeg)", + "video": "Vídeo", + "audio": "Àudio", + "hardware": "Acceleració del maquinari", + "videoCopy": "Copia", + "videoH264": "Transcodifica a H.264", + "videoH265": "Transcodifica a H.265", + "videoExclude": "Exclou", + "audioCopy": "Copia", + "audioAac": "Transcodifica a l'AAC", + "audioOpus": "Transcodifica a Opus", + "audioPcmu": "Transcodifica a PCM μ-law", + "audioPcma": "Transcodifica a PCM A-law", + "audioPcm": "Transcodifica a PCM", + "audioMp3": "Transcodifica a MP3", + "audioExclude": "Exclou", + "hardwareNone": "Sense acceleració de hardware", + "hardwareAuto": "Automàtic (recomanat)", + "addVideoCodec": "Afegeix un còdec de vídeo", + "addAudioCodec": "Afegeix un còdec d'àudio", + "removeCodec": "Elimina el còdec", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox" + }, + "streamNumber": "Flux {{index}}", + "sourceNumber": "Font {{index}}" + }, + "timestampPosition": { + "tl": "A dalt a l'esquerra", + "tr": "A dalt a la dreta", + "bl": "Baix a l'esquerra", + "br": "A baix a la dreta" + }, + "onvif": { + "profileAuto": "Automàtic", + "profileLoading": "S'estan carregant perfils...", + "autotracking": { + "zooming": { + "disabled": "Desactivat", + "absolute": "Absolut", + "relative": "Relatiu" + } + } + }, + "configMessages": { + "review": { + "recordDisabled": "L'enregistrament està desactivat, els elements de revisió no es generaran.", + "detectDisabled": "La detecció d'objectes està desactivada. Els elements de revisió requereixen objectes detectats per categoritzar alertes i deteccions.", + "allNonAlertDetections": "Totes les activitats no alertes s'inclouran com a deteccions.", + "genaiImageSourceRecordingsRecordDisabled": "La font d'imatges està configurada com a 'enregistraments', però l'enregistrament està desactivat. Frigate tornarà a la vista prèvia de les imatges." + }, + "audio": { + "noAudioRole": "Cap flux té definit el rol d'àudio. Heu d'habilitar el rol d'àudio per a la detecció d'àudio perquè funcioni." + }, + "audioTranscription": { + "audioDetectionDisabled": "La detecció d'àudio no està activada per a aquesta càmera. La transcripció d'àudio requereix que la detecció d'àudio estigui activa." + }, + "detect": { + "fpsGreaterThanFive": "No es recomana establir el detect FPS superior a 5. Els valors més alts poden causar problemes de rendiment i no proporcionaran cap benefici.", + "disabled": "La detecció d'objectes està desactivada. Les instantànies, articles de revisió i enriquiments com el reconeixement de rostres, el reconeixement de matrícules i la IA Generativa no funcionaran.", + "resolutionShouldBeMultipleOfFour": "Per obtenir els millors resultats, detectar l'amplada i l'alçada han de ser múltiples de 4. Altres valors parells poden produir artefactes visuals o una lleugera distorsió en el flux de detecció.", + "aspectRatioMismatch": "L'amplada i alçada que heu introduït no coincideixen amb la relació d'aspecte de la resolució de detecció actual. Això pot produir una imatge estirada o distorsionada.", + "maxFramesSet": "La configuració dels fotogrames màxims anul·la el comportament predeterminat i desactiva el seguiment d'objectes estacionaris. Hi ha molt poques situacions en què això sigui necessari, utilitzeu-lo amb precaució.", + "squareResolution": "Una resolució de detecció quadrada és inusual. L'amplada i l'alçada de la detecció han de coincidir amb la relació d'aspecte de la càmera (per exemple, 16:9), no amb les dimensions del model de detecció d'objectes. Una relació d'aspecte no coincident pot estirar la imatge i reduir la precisió de detecció.", + "resolutionHigh": "Aquesta resolució de detecció és més alta del recomanat i pot causar un ús més elevat dels recursos sense millorar la precisió de detecció. Es recomana una resolució de detecció a o per sota de 1080p per a la majoria de les càmeres.", + "globalResolutionMultipleCameras": "S'estableix una resolució de detecció global mentre es configuren diverses càmeres. Tret que totes les càmeres comparteixin la mateixa resolució i relació d'aspecte, l'amplada i l'alçada de la detecció s'haurien de definir per càmera perquè coincideixi amb la relació d'aspecte nativa de cada càmera." + }, + "faceRecognition": { + "globalDisabled": "L'enriquiment del reconeixement facial s'ha d'habilitar perquè les funcions de reconeixement facial funcionin en aquesta càmera.", + "personNotTracked": "El reconeixement de cares requereix que l'objecte 'persona' sigui rastrejat. Habilita «persona» en objectes per a aquesta càmera.", + "modelSizeLarge": "El model 'gran' requereix una GPU o NPU per a un rendiment raonable. Usa «petit» en sistemes només de CPU." + }, + "lpr": { + "globalDisabled": "L'enriquiment de reconeixement de matrícules ha d'estar habilitat perquè les funcions LPR funcionin en aquesta càmera.", + "vehicleNotTracked": "El reconeixement de la matrícula requereix que es faci un seguiment del 'cotxe' o de la 'motocicleta'.", + "modelSizeLarge": "El model 'gran' està optimitzat per a matrícules multilínies. El model 'petit' proporciona un millor rendiment sobre 'gran' i s'ha d'utilitzar tret que la vostra regió utilitzi formats de placa multilínia." + }, + "record": { + "noRecordRole": "Cap flux té el rol de registre definit. L'enregistrament no funcionarà." + }, + "birdseye": { + "objectsModeDetectDisabled": "Birdseye està configurat en mode 'objectes', però la detecció d'objectes està desactivada per a aquesta càmera. La càmera no apareixerà a Birdseye." + }, + "snapshots": { + "detectDisabled": "La detecció d'objectes està desactivada. Les instantànies es generen a partir d'objectes rastrejats i no es crearan." + }, + "detectors": { + "mixedTypes": "Tots els detectors han d'utilitzar el mateix tipus. Elimina els detectors existents per utilitzar un tipus diferent.", + "mixedTypesSuggestion": "Tots els detectors han d'utilitzar el mateix tipus. Suprimiu detectors existents o seleccioneu {{type}}." + }, + "objects": { + "genaiNoDescriptionsProvider": "Heu de configurar un proveïdor de GenAI amb el rol 'descripcions' per a les descripcions que es generaran." + }, + "semanticSearch": { + "jinav2SmallModelSize": "La mida 'petita' amb el model Jina V2 té un alt cost de RAM i d'inferència. Es recomana el model 'gran' amb una GPU discreta.", + "modelSizeIgnoredForProvider": "La mida del model només s'aplica als models de Jina incorporats. Aquest valor s'ignorarà quan s'utilitzi un proveïdor d'incrustació GenAI." + }, + "onvif": { + "autotrackingNoZones": "Autotraquejar requereix al menys una zona. Defineix una zona per aquesta cámera a Mascares/Zones, després usa'l com a requerit a la part inferior." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "No es recomanen arguments manuals d'acceleració de maquinari. Tret que existeixi un requisit específic, seleccioneu el predefinit que coincideixi amb el vostre maquinari." + }, + "model": { + "optimizedFor320": "Frigate està optimitzada per a un model 320x320, que és la millor opció per a la majoria de configuracions. Un model 640x640 és més lent i només ajuda en escenaris específics.", + "inputDimensionsNotDetectResolution": "L'amplada i l'alçada del model d'entrada són les dimensions d'entrada del model de detecció d'objectes, no la resolució de detecció de la càmera. Haurien de coincidir amb les dimensions del model que esteu utilitzant - típicament una mida quadrada com 320x320 o 640x640." + } + }, + "modelSize": { + "large": "Gran", + "small": "Petit" + }, + "birdseye": { + "trackingMode": { + "objects": "Objectes", + "motion": "Moviment", + "continuous": "Continu" + }, + "cameraOrder": { + "label": "Ordre de la càmera", + "description": "Arrossega les càmeres per establir el seu ordre en la disposició Birdseye.", + "reorderHandle": "Arrossega per reordenar", + "saving": "S'està desant…", + "saved": "Desat" + } + }, + "snapshot": { + "retainMode": { + "all": "Tots", + "motion": "Moviment", + "active_objects": "Objectes Actius" + } + }, + "ui": { + "timeFormat": { + "browser": "Visor", + "12hour": "12 hores", + "24hour": "24 hores" + }, + "TimeOrDateStyle": { + "full": "Complet", + "long": "Llarg", + "medium": "Mitjà", + "short": "Curt" + }, + "unitSystem": { + "metric": "Métric", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Gravacions", + "previews": "Previsualitzacions" + } + }, + "logger": { + "logLevel": { + "debug": "Depurar", + "info": "Informació", + "warning": "Avís", + "error": "Error", + "critical": "Crític" + } + }, + "retainMode": { + "all": "Tots", + "motion": "Moviment", + "active_objects": "Objectes actius" + }, + "previewQuality": { + "very_high": "Molt alta", + "high": "Alta", + "medium": "Mitja", + "low": "Baix", + "very_low": "Molt baix" + }, + "detectorsAndModel": { + "restartRequired": "Reinici requerit (canvi en detector o model)", + "title": "Detectors i model", + "description": "Configuri el detector final que corre la detecció d'objectes i el model que usa. Els canvis es gravaràn junts i així el detector i el model estan sincronitzats.", + "cardTitles": { + "detector": "Detector Hardware", + "model": "Model de detecció" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Model personalitzat" + }, + "mismatch": { + "warning": "El model actual de Frigate+ \"{{model}}\" requereix el detector {{required}}. Selecciona un model compatible a baix o canvía e model personalitzat abans de gravar." + }, + "plusModel": { + "requiresDetector": "Requereix: {{detector}}", + "noModelSelected": "Selecciona un model Frigate+" + }, + "toast": { + "saveSuccess": "Configuració de detectors i model guardats. Reinicia Frigate per aplicar els canvis.", + "saveError": "Fallo en gravar la configuració de detector i model" + }, + "unsavedChanges": "Canvis de detector i model no gravats" + }, + "menuDot": { + "overrideGlobal": "Aquesta secció substitueix la configuració global", + "overrideProfile": "Aquesta secció està substituïda pel perfil {{profile}}", + "unsaved": "Aquesta secció té canvis sense desar" + } +} diff --git a/web/public/locales/ca/views/system.json b/web/public/locales/ca/views/system.json new file mode 100644 index 0000000..c472937 --- /dev/null +++ b/web/public/locales/ca/views/system.json @@ -0,0 +1,275 @@ +{ + "documentTitle": { + "cameras": "Estadístiques de càmera - Frigate", + "storage": "Estadístiques d'emmagatzematge - Frigate", + "general": "Estadístiques generals - Frigate", + "logs": { + "frigate": "Registres de Frigate - Frigate", + "go2rtc": "Registres de Go2RTC - Frigate", + "nginx": "Registres de Nginix - Frigate", + "websocket": "Registres de missatges - Frigate" + }, + "enrichments": "Estadístiques complementàries - Frigate" + }, + "title": "Sistema", + "metrics": "Mètriques del sistema", + "logs": { + "download": { + "label": "Descarregar registres" + }, + "copy": { + "label": "Copiar al porta-retalls", + "success": "Registres copiats al porta-retalls", + "error": "No s'han pogut copiar els registres al porta-retalls" + }, + "type": { + "label": "Tipus", + "timestamp": "Marca temporal", + "tag": "Etiqueta", + "message": "Missatge" + }, + "tips": "Els registres s'estàn transmetent des del servidor", + "toast": { + "error": { + "fetchingLogsFailed": "Error al obtenir els 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" + }, + "count_one": "{{count}} missatge", + "count_other": "{{count}} missatges" + } + }, + "general": { + "detector": { + "memoryUsage": "Ús de la memòria del detector", + "title": "Detectors", + "inferenceSpeed": "Velocitat d'inferència del detector", + "cpuUsage": "Ús de la CPU del detector", + "temperature": "Temperatura del detector", + "cpuUsageInformation": "CPU usada en la preparació d'entrades i sortides desde/cap als models de detecció. Aquest valor no mesura l'utilització d'inferència, encara que usis una GPU o accelerador." + }, + "title": "General", + "hardwareInfo": { + "title": "Informació de maquinari", + "gpuUsage": "Ús de la GPU", + "gpuMemory": "Memòria de GPU", + "gpuDecoder": "Decodificador de GPU", + "gpuEncoder": "Codificador de GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Sortida de Vainfo", + "processOutput": "Sortida del procés:", + "processError": "Error de procés:", + "returnCode": "Codi de retorn: {{code}}" + }, + "nvidiaSMIOutput": { + "title": "Sortida de Nvidia SMI", + "vbios": "Informació de VBios: {{vbios}}", + "cudaComputerCapability": "Capacitat de càlcul CUDA: {{cuda_compute}}", + "name": "Nom: {{name}}", + "driver": "Controlador: {{driver}}" + }, + "closeInfo": { + "label": "Tancar informació de GPU" + }, + "copyInfo": { + "label": "Copiar informació de GPU" + }, + "toast": { + "success": "Informació de GPU copiada al porta-retalls" + } + }, + "npuUsage": "Ús de NPU", + "npuMemory": "Memòria de NPU", + "intelGpuWarning": { + "title": "Avís d'estadístiques de la GPU d'Intel", + "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 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", + "gpuCompute": "Càlcul / Codificació per GPU" + }, + "otherProcesses": { + "title": "Altres processos", + "processMemoryUsage": "Ús de memòria de procés", + "processCpuUsage": "Ús de la CPU per procés", + "series": { + "recording": "gravant", + "review_segment": "segment de revisió", + "embeddings": "Vectors", + "audio_detector": "detector d'àudio", + "go2rtc": "go2rtc" + } + } + }, + "storage": { + "title": "Emmagatzematge", + "recordings": { + "title": "Gravacions", + "earliestRecording": "Gravació més antiga disponible:", + "tips": "Aquest valor representa l'emmagatzematge total utilitzat per les gravacions a la base de dades de Frigate. Frigate no registre l'ús de tots els arxius del disc." + }, + "cameraStorage": { + "camera": "Càmera", + "unusedStorageInformation": "Informació d'emmagatzematge no utilitzat", + "bandwidth": "Ample de banda", + "storageUsed": "Emmagatzematge", + "title": "Emmagatzematge de càmera", + "unused": { + "title": "Sense utilitzar", + "tips": "Aquest valor pot no de forma exacta representar l'espai lliure disponible a Frigate si tens altres fitxers emmagatzemats en la vostra unitat més enllà dels registres de Frigate. Frigate no rastreja l'ús d'emmagatzematge extern als seus registres." + }, + "percentageOfTotalUsed": "Percentatge del total" + }, + "overview": "Visió general", + "shm": { + "title": "Ubicació de SHM (memória compartida)", + "warning": "El tamany de la SHM oh {{total}}MB es massa petita. Augmenta almenys fins a {{min_shm}}MB.", + "frameLifetime": { + "title": "Temps de vida del fotograma", + "description": "Cada càmera té {{frames}} ranures de fotogrames en memòria compartida. A la velocitat de fotogrames més ràpida de la càmera, cada fotograma està disponible per aproximadament {{lifetime}}s abans de ser sobreescrit." + } + } + }, + "cameras": { + "framesAndDetections": "Fotogrames / Deteccions", + "label": { + "capture": "captura", + "cameraDetect": "{{camName}} detectar", + "cameraCapture": "{{camName}} captura", + "camera": "càmera", + "skipped": "omès", + "ffmpeg": "FFmpeg", + "detect": "detectar", + "overallFramesPerSecond": "Fotogrames per segon globals", + "overallDetectionsPerSecond": "Deteccions per segon globals", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraFramesPerSecond": "{{camName}} fotogrames per segon", + "cameraDetectionsPerSecond": "{{camName}} deteccions per segon", + "overallSkippedDetectionsPerSecond": "Nombre total de deteccions descartades per segon", + "cameraSkippedDetectionsPerSecond": "Nombre de deteccions descartades per segon a {{camName}}", + "cameraGpu": "{{camName}} GPU" + }, + "info": { + "codec": "Còdec:", + "fps": "FPS:", + "resolution": "Resolució:", + "video": "Vídeo:", + "unknown": "Desconegut", + "stream": "Transmissió {{idx}}", + "error": "Error: {{error}}", + "fetching": "Obtenint dades de càmera", + "aspectRatio": "relació d'aspecte", + "tips": { + "title": "Informació del sondeig de la càmera" + }, + "audio": "Àudio:", + "cameraProbeInfo": "Informació del sondeig de la càmera {{camera}}", + "streamDataFromFFPROBE": "Les dades de la transmissió són obtingudes mitjançant ffprobe.", + "keyframes": { + "title": "Anàlisi de fotogrames clau", + "analyzing": "S'estan analitzant els fotogrames clau... queden {{seconds}} segons", + "stillAnalyzing": "Encara s'estan analitzant els fotogrames clau...", + "recordStream": "Registre de flux:", + "keyframeCount": "Fotogrames clau observats:", + "observedDuration": "Durada observada:", + "gap": "Espai de fotogrames clau (mín / avg / max):", + "segmentLength": "Longitud del segment d'enregistrament:", + "ok": "Fotogrames clau cada ,{{seconds}}s, bons per enregistrar i reproduir.", + "warning": "Els fotogrames clau dispersos o variables (espai més llarg .{{seconds}}s), probablement un còdec intel·ligent (H.264+/H.265+), això no és recomanable.", + "error": "El buit dels fotogrames clau ( the{{seconds}}s) excedeix la longitud del segment d'enregistrament ({{segmentTime}}s). Alguns segments poden no tenir un fotograma clau, el qual trenca la reproducció. Desactiva el còdec intel·ligent/+ a la càmera o escurça el seu interval de fotogrames clau.", + "unknown": "No s'ha pogut determinar l'espaiat dels fotogrames clau.", + "recordDisabled": "L'enregistrament està desactivat per a aquesta càmera." + } + }, + "title": "Càmeres", + "overview": "Visió general", + "toast": { + "success": { + "copyToClipboard": "S'han copiat les dades de sondeig al porta-retalls." + }, + "error": { + "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)" + }, + "noCameras": { + "title": "No s'ha trobat cap càmera" + } + }, + "lastRefreshed": "Darrera actualització: ", + "stats": { + "reindexingEmbeddings": "Reindexant vectors ({{processed}}% completat)", + "healthy": "El sistema és saludable", + "cameraIsOffline": "{{camera}} està fora de línia", + "ffmpegHighCpuUsage": "{{camera}} te un ús elevat de CPU per FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} te un ús elevat de CPU per la detecció ({{detectAvg}}%)", + "detectIsVerySlow": "{{detect}} és molt 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.", + "debugReplayActive": "La sessió de repetició de depuració està activa" + }, + "enrichments": { + "title": "Enriquiments", + "embeddings": { + "face_recognition_speed": "Velocitat de reconeixement facial", + "image_embedding": "Vectors d'imatges", + "text_embedding": "Vectors de text", + "face_recognition": "Reconeixement de rostres", + "plate_recognition": "Reconeixemnt de matrícules", + "image_embedding_speed": "Velocitat de generació de vectors", + "face_embedding_speed": "Velocitat de generació de vectors facials", + "plate_recognition_speed": "Velocitat de reconeixement de matrícules", + "text_embedding_speed": "Velocitat de generació de vectors de text", + "yolov9_plate_detection": "Detecció de matrícules YOLOv9", + "yolov9_plate_detection_speed": "Velocitat de detecció de matrícules YOLOv9", + "review_description": "Descripció de la revisió", + "review_description_speed": "Velocitat de la descripció de la revisió", + "review_description_events_per_second": "Descripció de la revisió", + "object_description": "Descripció de l'objecte", + "object_description_speed": "Velocitat de la descripció de l'objecte", + "object_description_events_per_second": "Descripció de l'objecte", + "classification": "{{name}} Classificació", + "classification_speed": "Velocitat de classificació de {{name}}", + "classification_events_per_second": "{{name}} Esdeveniments de classificació per segon" + }, + "infPerSecond": "Inferències per segon", + "averageInf": "Temps mitjà d'inferència" + } +} diff --git a/web/public/locales/cs/audio.json b/web/public/locales/cs/audio.json new file mode 100644 index 0000000..8876626 --- /dev/null +++ b/web/public/locales/cs/audio.json @@ -0,0 +1,429 @@ +{ + "yell": "Křik", + "child_singing": "Dětský zpěv", + "speech": "Řeč", + "babbling": "Blábolení", + "bellow": "Řev", + "whoop": "Výskání", + "whispering": "Šeptání", + "snicker": "Chichotání", + "crying": "Pláč", + "sigh": "Povzdech", + "singing": "Zpěv", + "choir": "Sbor", + "yodeling": "Jódlování", + "synthetic_singing": "Umělý zpěv", + "humming": "Bzukot", + "groan": "Sténání", + "whistling": "Pískot", + "breathing": "Dech", + "wheeze": "Sípání", + "snoring": "Chrapot", + "snort": "Funění", + "cough": "Kašel", + "throat_clearing": "Odkašlávání", + "sneeze": "Kýchání", + "footsteps": "Kroky", + "chewing": "Žvýkání", + "biting": "Kousání", + "burping": "Krkání", + "hiccup": "Škytání", + "fart": "Prdění", + "hands": "Ruce", + "finger_snapping": "Luskání prstem", + "clapping": "Tleskání", + "heartbeat": "Tluk srdce", + "cheering": "Jásání", + "applause": "Potlesk", + "chatter": "Klábosení", + "crowd": "Dav", + "children_playing": "Hrající si děti", + "bark": "Štěkot", + "howl": "Vytí", + "growling": "Vrkot", + "whimper_dog": "Psí kníkot", + "cat": "Kočka", + "purr": "Předení", + "meow": "Mňouk", + "hiss": "Sykot", + "livestock": "Hospodářská zvířata", + "horse": "Kůň", + "neigh": "Řehtání", + "cattle": "Dobytek", + "moo": "Bučení", + "cowbell": "Kravský zvonec", + "pig": "Prase", + "oink": "Chrochtanie", + "fowl": "Drůbež", + "chicken": "Slepice", + "cluck": "Kvokání", + "cock_a_doodle_doo": "Kykyryký", + "turkey": "Krocan", + "gobble": "Hudrování", + "duck": "Kachna", + "quack": "Kvákání", + "goose": "Husa", + "honk": "Kejhání", + "wild_animals": "Divoká zvířata", + "roaring_cats": "Řvoucí kočky", + "roar": "Řev", + "bird": "Pták", + "chirp": "Cvrlikání", + "pigeon": "Holub", + "coo": "Vrkání", + "squawk": "Skřekání", + "crow": "Vrána", + "caw": "Krákání", + "hoot": "Houkání", + "flapping_wings": "Mávání křídel", + "dogs": "Psi", + "mouse": "Myš", + "insect": "Hmyz", + "fly": "Moucha", + "buzz": "Bzučení", + "frog": "Žába", + "snake": "Had", + "croak": "Kvákání žáby", + "rattle": "Chrastění", + "whale_vocalization": "Velrybí zpěv", + "music": "Hudba", + "guitar": "Kytara", + "bass_guitar": "Basová kytara", + "steel_guitar": "Ocelová kytara", + "tapping": "Ťukání", + "banjo": "Banjo", + "sitar": "Sitár", + "mandolin": "Mandolína", + "zither": "Citera", + "ukulele": "Ukulele", + "keyboard": "Klávesnice", + "electric_piano": "Elektrický klavír", + "electronic_organ": "Elektronické varhany", + "hammond_organ": "Hammondovy varhany", + "synthesizer": "Syntezátor", + "sampler": "Sampler", + "harpsichord": "Cembalo", + "percussion": "Perkuse", + "drum_kit": "Bubny", + "drum_machine": "Bicí automat", + "drum": "Buben", + "snare_drum": "Malý buben", + "rimshot": "Rána na obruč", + "drum_roll": "Víření", + "timpani": "Tympány", + "tabla": "Tabla", + "cymbal": "Činel", + "hi_hat": "Hi-hat", + "wood_block": "Dřevěný blok", + "tambourine": "Tamburína", + "maraca": "Maraka", + "gong": "Gong", + "marimba": "Marimba", + "vibraphone": "Vibrafon", + "steelpan": "Ocelový buben", + "orchestra": "Orchestr", + "brass_instrument": "Žesťový nástroj", + "french_horn": "Lesní roh", + "trumpet": "Trubka", + "trombone": "Trombón", + "violin": "Housle", + "saxophone": "Saxofon", + "church_bell": "Kostelní zvon", + "bicycle_bell": "Cyklistický zvonek", + "tuning_fork": "Ladička", + "chime": "Zvonění", + "harmonica": "Harmonika", + "accordion": "Akordeon", + "bagpipes": "Dudy", + "didgeridoo": "Didžeridu", + "theremin": "Theremin", + "scratching": "Škrábání", + "pop_music": "Popová muzika", + "hip_hop_music": "Hip-hopová muzika", + "rock_music": "Rocková muzika", + "heavy_metal": "Heavy metal", + "music_for_children": "Hudba pro děti", + "song": "Píseň", + "thunderstorm": "Bouře", + "wind": "Vítr", + "rustling_leaves": "Šustění listů", + "wind_noise": "Zvuk větru", + "thunder": "Hrom", + "water": "Voda", + "rain": "Déšť", + "raindrop": "Dešťové kapky", + "stream": "Potok", + "waterfall": "Vodopád", + "ocean": "Moře", + "waves": "Vlny", + "steam": "Pára", + "fire": "Oheň", + "crackle": "Praskání", + "vehicle": "Vozidlo", + "sailboat": "Plachetnice", + "boat": "Člun", + "ship": "Loď", + "rowboat": "Loďka", + "motorboat": "Motorový člun", + "motor_vehicle": "Motorové vozidlo", + "car": "Auto", + "laughter": "Smích", + "sniff": "Čichání", + "stomach_rumble": "Kručení v břiše", + "gargling": "Kloktání", + "dog": "Pes", + "run": "Běh", + "cricket": "Cvrček", + "glockenspiel": "Paličková zvonkohra", + "cello": "Cello", + "pets": "Domácí mazlíčci", + "opera": "Opera", + "harp": "Harfa", + "animal": "Zvíře", + "electric_guitar": "Elektrická kytara", + "piano": "Klavír", + "goat": "Koza", + "bleat": "Mečení", + "sheep": "Ovce", + "owl": "Sova", + "musical_instrument": "Hudební nástroj", + "organ": "Varhany", + "rats": "Krysy", + "mosquito": "Komár", + "strum": "Brnkání", + "tubular_bells": "Trubicové zvony", + "acoustic_guitar": "Akustická kytara", + "bass_drum": "Basový buben", + "jazz": "Jazz", + "flute": "Flétna", + "clarinet": "Klarinet", + "bell": "Zvon", + "techno": "Techno", + "electronic_music": "Elektronická muzika", + "car_alarm": "Autoalarm", + "power_windows": "Elektrická okénka", + "skidding": "Smyk", + "tire_squeal": "Kvílení pneumatik", + "car_passing_by": "Projíždějící auto", + "air_brake": "Vzduchové brzdy", + "air_horn": "Vzduchový klakson", + "bus": "Autobus", + "police_car": "Policejní auto", + "ambulance": "Záchranka", + "fire_engine": "Hasiči", + "motorcycle": "Motorka", + "rail_transport": "Železnice", + "train": "Vlak", + "train_horn": "Troubení vlaku", + "railroad_car": "Železniční vagon", + "subway": "Metro", + "aircraft": "Letadlo", + "aircraft_engine": "Motor letadla", + "bicycle": "Cyklistické kolo", + "jet_engine": "Tryskový motor", + "propeller": "Vrtule", + "helicopter": "Helikoptéra", + "dental_drill's_drill": "Zubní vrtačka", + "lawn_mower": "Sekačka", + "chainsaw": "Motorová pila", + "idling": "Bežící motor", + "accelerating": "Přidávání plynu", + "door": "Dveře", + "doorbell": "Zvonek", + "sliding_door": "Posuvné dveře", + "slam": "Bouchnutí", + "knock": "Klepání", + "dishes": "Nádobí", + "cutlery": "Příbory", + "chopping": "Krájení", + "bathtub": "Vana", + "hair_dryer": "Fén", + "toilet_flush": "Spláchnutí záchodu", + "toothbrush": "Zubní kartáček", + "electric_toothbrush": "Elektrický zubní kartáček", + "vacuum_cleaner": "Vysavač", + "zipper": "Zip", + "keys_jangling": "Cinkání klíčů", + "coin": "Mince", + "scissors": "Nůžky", + "electric_shaver": "Elektrický holící strojek", + "typing": "Psaní na stroji nebo klávesnici", + "typewriter": "Psací stroj", + "computer_keyboard": "Počítačová klávesnice", + "writing": "Psaní", + "alarm": "Alarm", + "telephone": "Telefon", + "telephone_bell_ringing": "Zvonění telefonu", + "telephone_dialing": "Vytáčení", + "alarm_clock": "Budík", + "siren": "Siréna", + "smoke_detector": "Detektor kouře", + "fire_alarm": "Požární alarm", + "foghorn": "Mlhovka", + "whistle": "Píšťalka", + "mechanisms": "Mechanismy", + "clock": "Hodiny", + "tick-tock": "Ťikťak", + "tick": "Ťik", + "sewing_machine": "Šicí stroj", + "air_conditioning": "Klimatizace", + "cash_register": "Kasa", + "printer": "Tiskárna", + "camera": "Kamera", + "tools": "Nářadí", + "hammer": "Kladivo", + "jackhammer": "Sbíječka", + "sawing": "Řezání", + "power_tool": "Elektrické nářadí", + "drill": "Vrtačka", + "explosion": "Exploze", + "gunshot": "Výstřel", + "fireworks": "Ohňostroj", + "firecracker": "Petarda", + "eruption": "Erupce", + "boom": "Třesk", + "wood": "Dřevo", + "splinter": "Tříska", + "glass": "Sklo", + "shatter": "Roztříštění", + "silence": "Ticho", + "sound_effect": "Zvukový efekt", + "environmental_noise": "Okolní hluk", + "white_noise": "Bilý šum", + "radio": "Rádio", + "scream": "Výkřik", + "microwave_oven": "Mikrovlnka", + "race_car": "Závodní auto", + "ding-dong": "Cink", + "water_tap": "Vodovodní kohoutek", + "sink": "Dřez", + "pink_noise": "Růžový šum", + "frying": "Smažení", + "television": "Televize", + "blender": "Mixér", + "train_whistle": "Houkání vlaku", + "engine": "Motor", + "engine_starting": "Startující motor", + "truck": "Nákladní auto", + "static": "Šum", + "engine_knocking": "Klepání v motoru", + "skateboard": "Skateboard", + "chant": "Skandování", + "rapping": "Rapování", + "gasp": "Zalapání po dechu", + "heart_murmur": "Srdeční šelest", + "mantra": "Mantra (pozitivní vibrace)", + "grunt": "Zabručení", + "pant": "Oddechávání", + "shuffle": "Míchání (karet)", + "yip": "Jo", + "bow_wow": "Hlasitý protest", + "caterwaul": "Vřeštět", + "clip_clop": "Klapání kopyt", + "patter": "Plácání", + "plucked_string_instrument": "Drnkací strunný nástroj", + "mallet_percussion": "Palička perkuse", + "bowed_string_instrument": "Smyčcový nástroj", + "string_section": "Smyčcová sekce", + "pizzicato": "Pizzicato", + "double_bass": "Kontrabas", + "wind_instrument": "Dechový nástroj", + "jingle_bell": "Rolnička", + "wind_chime": "Zvonkohra", + "singing_bowl": "Singing Bowl", + "beatboxing": "Beatboxing", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "progressive_rock": "Progressive Rock", + "rock_and_roll": "Rock & Roll", + "psychedelic_rock": "Psychadelický Rock", + "rhythm_and_blues": "Rythm & Blues", + "soul_music": "Soulová hudba", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Swingová hudba", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folková hudba", + "middle_eastern_music": "Středo-východní hudba", + "disco": "Disco", + "classical_music": "Klasická hudba", + "house_music": "House hudba", + "dubstep": "Dubstep", + "drum_and_bass": "Drum & Bass", + "electronica": "Elektronická hudba", + "electronic_dance_music": "Elektronická taneční hudba", + "ambient_music": "Ambientní hudba", + "trance_music": "Trance hudba", + "music_of_latin_america": "Latinsko-americká hudba", + "flamenco": "Flamengo", + "blues": "Blues", + "new-age_music": "Novodobá hudba", + "vocal_music": "Vokální hudba", + "a_capella": "A Capella", + "music_of_africa": "Africká hudba", + "afrobeat": "Afrobeat", + "christian_music": "Křesťanská hudba", + "gospel_music": "Gospelová hudba", + "music_of_asia": "Asijská hudba", + "carnatic_music": "Karnatická hudba", + "music_of_bollywood": "Hudba z Bollywoodu", + "ska": "SKA", + "traditional_music": "Tradiční hudba", + "independent_music": "Nezávislá hudba", + "background_music": "Hudba na pozadí", + "theme_music": "Tématická hudba", + "jingle": "Jingle", + "soundtrack_music": "Soundtracková hudba", + "lullaby": "Ukolébavka", + "video_game_music": "Herní hudba", + "christmas_music": "Vánoční hudba", + "dance_music": "Taneční hudba", + "wedding_music": "Svatební hudba", + "happy_music": "Veselá hudba", + "sad_music": "Smutná hudba", + "tender_music": "Něžná hudba", + "exciting_music": "Vzrušující hudba", + "angry_music": "Naštvaná hudba", + "scary_music": "Děsivá hudba", + "rain_on_surface": "Déšť na povrch", + "gurgling": "Klokotání", + "toot": "Troubení", + "reversing_beeps": "Parkovací pípání", + "ice_cream_truck": "Auto se zmrzlinou", + "emergency_vehicle": "Záchranářské vozidlo", + "traffic_noise": "Zvuk provozu", + "train_wheels_squealing": "Skřípání kol vlaku", + "fixed-wing_aircraft": "Letadlo s pevnými křídly", + "light_engine": "Lehký motor", + "medium_engine": "Střední motor", + "heavy_engine": "Těžký motor", + "tap": "Poklepání", + "squeak": "Skřípání", + "cupboard_open_or_close": "Otvírání nebo zavírání skříně", + "drawer_open_or_close": "Otvírání nebo zavírání šuplíku", + "shuffling_cards": "Míchání karet", + "ringtone": "Vyzváněcí melodie", + "dial_tone": "Vytáčecí tón", + "busy_signal": "Tón obsazené linky", + "civil_defense_siren": "Siréna civilní obrany", + "salsa_music": "Salsa hudba", + "buzzer": "Bzučák", + "steam_whistle": "Parní píšťala", + "ratchet": "Ráčna", + "gears": "Ozubená kola", + "pulleys": "Kladky", + "mechanical_fan": "Mechanický větrák", + "single-lens_reflex_camera": "Jednooká zrcadlovka", + "filing": "Plnění", + "sanding": "Pískování", + "machine_gun": "Kulomet", + "fusillade": "Salva", + "artillery_fire": "Dělostřelecká palba", + "cap_gun": "Kapslíková pistole", + "burst": "Výbuch", + "chop": "Sekání", + "crack": "Prasknutí", + "chink": "Cinknutí", + "field_recording": "Nahrávka z terénu" +} diff --git a/web/public/locales/cs/common.json b/web/public/locales/cs/common.json new file mode 100644 index 0000000..257bb8b --- /dev/null +++ b/web/public/locales/cs/common.json @@ -0,0 +1,307 @@ +{ + "time": { + "untilForTime": "Do {{time}}", + "untilForRestart": "Do doby, než se Frigarte restartuje.", + "untilRestart": "Do restartu", + "justNow": "Teď", + "today": "Dnes", + "yesterday": "Včera", + "last7": "Posledních 7 dní", + "last14": "Posledních 14 dní", + "last30": "Posledních 30 dní", + "thisWeek": "Tento týden", + "lastWeek": "Minulý týden", + "thisMonth": "Tento měsíc", + "lastMonth": "Minulý měsíc", + "5minutes": "5 minut", + "10minutes": "10 minut", + "30minutes": "30 minut", + "1hour": "1 hodina", + "12hours": "12 hodin", + "24hours": "24 hodin", + "pm": "odpoledne", + "am": "ráno", + "year_one": "{{time}} rok", + "year_few": "{{time}} let", + "year_other": "{{time}} let", + "month_one": "{{time}} měsíc", + "month_few": "{{time}} měsíce", + "month_other": "{{time}} měsíců", + "day_one": "{{time}} den", + "day_few": "{{time}} dny", + "day_other": "{{time}} dní", + "hour_one": "{{time}} hodina", + "hour_few": "{{time}} hodiny", + "hour_other": "{{time}} hodin", + "minute_one": "{{time}} minuta", + "minute_few": "{{time}} minuty", + "minute_other": "{{time}} minut", + "second_one": "{{time}} sekunda", + "second_few": "{{time}} sekundy", + "second_other": "{{time}} sekund", + "formattedTimestampMonthDayYear": { + "12hour": "d MMM yyyy", + "24hour": "d MMM yyyy" + }, + "ago": "před {{timeAgo}}", + "yr": "{{time}}r", + "d": "{{time}}d", + "h": "{{time}}h", + "mo": "{{time}}měs", + "formattedTimestampHourMinute": { + "24hour": "HH:mm", + "12hour": "h:mm aaa" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDay": "d MMM", + "s": "{{time}}sec", + "m": "{{time}}min", + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampMonthDayYearHourMinute": { + "24hour": "d MMM yyyy, HH:mm", + "12hour": "d MMM yyyy, h:mm aaa" + }, + "formattedTimestampFilename": { + "24hour": "dd-MM-yy-HH-mm-ss", + "12hour": "dd-MM.yy-h-mm-ss-a" + }, + "never": "Nikdy", + "inProgress": "Zpracovává se", + "invalidStartTime": "Neplatný čas začátku", + "invalidEndTime": "Neplatný čas konce" + }, + "button": { + "twoWayTalk": "Obousměrná komunikace", + "enabled": "Zapnuto", + "cameraAudio": "Zvuk kamery", + "apply": "Použij", + "reset": "Reset", + "done": "Hotovo", + "on": "Zapnuto", + "off": "Vypnuto", + "edit": "Upravit", + "enable": "Zapni", + "disabled": "Vypnuto", + "disable": "Vypni", + "save": "Uložit", + "saving": "Ukládám…", + "cancel": "Zrušit", + "close": "Zavři", + "copy": "Zkopíruj", + "back": "Zpět", + "history": "Historie", + "fullscreen": "Celá obrazovka", + "exitFullscreen": "Opustit režim celé obrazovky", + "pictureInPicture": "Obraz v obraze", + "copyCoordinates": "Zkopíruj souřadnice", + "delete": "Odstranit", + "yes": "Ano", + "no": "Ne", + "download": "Stáhnout", + "info": "Informace", + "suspended": "Pozastaveno", + "unsuspended": "Zrušit pozastavení", + "play": "Hrát", + "unselect": "Zrušit výběr", + "deleteNow": "Smazat hned", + "next": "Další", + "export": "Exportovat", + "continue": "Pokračovat" + }, + "label": { + "back": "Jdi zpět", + "hide": "Skrýt {{item}}", + "show": "Zobrazit {{item}}", + "ID": "ID", + "none": "Nic", + "all": "Vše", + "other": "Ostatní" + }, + "unit": { + "speed": { + "kph": "km/h", + "mph": "míle/h" + }, + "length": { + "feet": "stopa", + "meters": "metry" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hodinu", + "mbph": "MB/hodinu", + "gbph": "GB/hodinu" + } + }, + "selectItem": "Vybrat {{item}}", + "menu": { + "documentation": { + "label": "Dokumentace Frigate", + "title": "Dokumentace" + }, + "live": { + "allCameras": "Všechny kamery", + "title": "Živě", + "cameras": { + "count_one": "{{count}} kamera", + "count_few": "{{count}} kamery", + "count_other": "{{count}} kamer", + "title": "Kamery" + } + }, + "review": "Revize", + "explore": "Prozkoumat", + "system": "Systém", + "systemMetrics": "Systémové metriky", + "configuration": "Konfigurace", + "language": { + "yue": "粵語 (kantonština)", + "en": "English (Angličtina)", + "da": "Dansk (Dánština)", + "fi": "Suomi (Finština)", + "sk": "Slovenčina (Slovenština)", + "withSystem": { + "label": "Použít systémové nastavení jazyka" + }, + "zhCN": "简体中文 (Zjednodušená čínština)", + "es": "Español (Španělština)", + "hi": "हिन्दी (Hindština)", + "fr": "Français (Francouzština)", + "ar": "العربية (Arabština)", + "pt": "Português (Portugalština)", + "ru": "Русский (Ruština)", + "de": "Deutsch (Němčina)", + "it": "Italiano (Italština)", + "ja": "日本語 (Japonština)", + "tr": "Türkçe (Turečtina)", + "nl": "Nederlands (Holandština)", + "sv": "Svenska (Švédština)", + "cs": "Čeština", + "nb": "Norsk Bokmål (norský Bokmål)", + "uk": "Українська (Ukrainština)", + "ko": "한국어 (Korejština)", + "vi": "Tiếng Việt (Vietnamština)", + "he": "עברית (Hebrejština)", + "el": "Ελληνικά (Řečtina)", + "fa": "فارسی (Perština)", + "ro": "Română (Rumunština)", + "hu": "Magyar (Maďarština)", + "pl": "Polski (Polština)", + "th": "ไทย (Thaiština)", + "ca": "Català (Katalánština)", + "sl": "Slovinština (Slovinština)", + "ptBR": "Português brasileiro (Brazilská Portugalština)", + "sr": "Српски (Srbština)", + "lt": "Lietuvių (Litevština)", + "bg": "Български (Bulharština)", + "gl": "Galego (Galicijština)", + "id": "Bahasa Indonesia (Indonéština)", + "ur": "اردو (Urdština)", + "hr": "Hrvatski (Chorvatština)" + }, + "theme": { + "highcontrast": "Vysoký kontrast", + "default": "Výchozí", + "label": "Téma", + "blue": "Modrá", + "green": "Zelená", + "nord": "Polární", + "red": "Červená" + }, + "help": "Nápověda", + "restart": "Restartovat Frigate", + "user": { + "logout": "Odhlásit", + "setPassword": "Nastavit heslo", + "current": "Aktuální uživatel: {{user}}", + "title": "Uživatel", + "account": "Účet", + "anonymous": "anonymní" + }, + "systemLogs": "Systémový záznam", + "settings": "Nastavení", + "languages": "Jazyky", + "appearance": "Vzhled", + "darkMode": { + "label": "Tmavý režim", + "light": "Světlý", + "dark": "Tmavý", + "withSystem": { + "label": "Použít systémové nastavení pro světlý a tmavý režim" + } + }, + "export": "Exportovat", + "uiPlayground": "UI hřiště", + "faceLibrary": "Knihovna Obličejů", + "configurationEditor": "Editor Konfigurace", + "withSystem": "Systém", + "classification": "Klasifikace" + }, + "pagination": { + "previous": { + "label": "Jít na předchozí stranu", + "title": "Předchozí" + }, + "label": "stránkování", + "next": { + "label": "Jít na další stranu", + "title": "Další" + }, + "more": "Více stran" + }, + "accessDenied": { + "documentTitle": "Přístup odepřen - Frigate", + "title": "Přístup odepřen", + "desc": "Nemáte oprávnění zobrazit tuto stránku." + }, + "notFound": { + "desc": "Stránka nenalezena", + "documentTitle": "Nenalezeno - Frigate", + "title": "404" + }, + "toast": { + "copyUrlToClipboard": "Adresa URL byla zkopírována do schránky.", + "save": { + "title": "Uložit", + "error": { + "title": "Chyba při ukládání změn konfigurace: {{errorMessage}}", + "noMessage": "Chyba při ukládání změn konfigurace" + } + } + }, + "role": { + "title": "Role", + "admin": "Správce", + "viewer": "Divák", + "desc": "Správci mají plný přístup ke všem funkcím v uživatelském rozhraní Frigate. Diváci jsou omezeni na sledování kamer, položek přehledu a historických záznamů v UI." + }, + "readTheDocumentation": "Přečtěte si dokumentaci", + "list": { + "two": "{{0}} a {{1}}", + "many": "{{items}}, a {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Volitelné", + "internalID": "Interní ID Frigate používá v konfiguraci a databázi" + }, + "information": { + "pixels": "{{area}}px" + } +} diff --git a/web/public/locales/cs/components/auth.json b/web/public/locales/cs/components/auth.json new file mode 100644 index 0000000..00b0160 --- /dev/null +++ b/web/public/locales/cs/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Uživatelské jméno", + "password": "Heslo", + "login": "Přihlášení", + "errors": { + "usernameRequired": "Uživatelské jméno je povinné", + "passwordRequired": "Heslo je povinné", + "loginFailed": "Přihlášení se nezdařilo", + "unknownError": "Neznámá chyba. Zkontrolujte logy.", + "webUnknownError": "Neznámá chuba. Zkontrolujte logy konzoly.", + "rateLimit": "Limit požadavků překročen. Zkuste to znovu později." + }, + "firstTimeLogin": "Přihlašujete se poprvé? Přihlašovací údaje jsou vypsány v logu Frigate." + } +} diff --git a/web/public/locales/cs/components/camera.json b/web/public/locales/cs/components/camera.json new file mode 100644 index 0000000..ef56aa7 --- /dev/null +++ b/web/public/locales/cs/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Skupiny kamer", + "camera": { + "setting": { + "streamMethod": { + "method": { + "noStreaming": { + "label": "Žádný stream", + "desc": "Obrázky z kamery se aktualizují pouze jednou za minutu a neproběhne žádné živé vysílání." + }, + "smartStreaming": { + "label": "Smart Streaming (doporučeno)", + "desc": "Inteligentní streamování aktualizuje obraz vaší kamery jednou za minutu, když nedochází k žádné detekovatelné aktivitě, aby se šetřila šířka pásma a zdroje. Když je detekována aktivita, obraz se plynule přepne na živý přenos." + }, + "continuousStreaming": { + "label": "Kontinuální streamování", + "desc": { + "title": "Obraz z kamery bude vždy živým přenosem, když je viditelný na dashboardu, i když není detekována žádná aktivita.", + "warning": "Nepřetržité streamování může způsobit velké využití šířky pásma a problémy s výkonem. Používejte opatrně." + } + } + }, + "label": "Metoda streamování", + "placeholder": "Vyberte metodu vysílání" + }, + "label": "Nastavení streamování kamery", + "audioIsAvailable": "Audio je k dispozici pro tento stream", + "audioIsUnavailable": "Audio není k dispozici pro tento stream", + "audio": { + "tips": { + "document": "Přečtěte si dokumentaci ", + "title": "Pro tento stream musí být výstup zvuku z vaší kamery a nakonfigurován v go2rtc." + } + }, + "compatibilityMode": { + "label": "Režim kompatibility", + "desc": "Tuto možnost povolte pouze v případě, že živý přenos vaší kamery zobrazuje barevné artefakty a má na pravé straně obrazu diagonální čáru." + }, + "title": "Nastavení streamování {{cameraName}}", + "desc": "Změní možnosti živého vysílání pro dashboard této skupiny kamer. Tato nastavení jsou specifická pro zařízení/prohlížeč.", + "stream": "Proud", + "placeholder": "Vyberte proud" + }, + "birdseye": "Ptačí oko" + }, + "delete": { + "confirm": { + "title": "Potvrdit odstranění", + "desc": "Skutečně si přejete odstranit skupinu kamer {{name}}?" + }, + "label": "Odstranit skupinu kamer" + }, + "add": "Přidat skupinu kamer", + "name": { + "label": "Jméno", + "placeholder": "Zadejte jméno…", + "errorMessage": { + "exists": "Tento název skupiny kamer již existuje.", + "mustLeastCharacters": "Název skupiny kamer musí mít minimálně 2 znaky.", + "nameMustNotPeriod": "Název skupiny kamer nesmí obsahovat tečku.", + "invalid": "Špatný název skupiny kamer." + } + }, + "edit": "Upravit skupinu kamer", + "cameras": { + "label": "Kamery", + "desc": "Vyberte kamery pro tuto skupinu." + }, + "icon": "Ikona", + "success": "Skupina kamer {{name}} byla uložena." + }, + "debug": { + "options": { + "label": "Nastavení", + "title": "Možnosti", + "showOptions": "Zobrazit možnosti", + "hideOptions": "Skrýt možnosti" + }, + "zones": "Zóny", + "motion": "Pohyb", + "regions": "Kraje", + "timestamp": "Časové razítko", + "boundingBox": "Ohraničení", + "mask": "Maska" + } +} diff --git a/web/public/locales/cs/components/dialog.json b/web/public/locales/cs/components/dialog.json new file mode 100644 index 0000000..70efc69 --- /dev/null +++ b/web/public/locales/cs/components/dialog.json @@ -0,0 +1,124 @@ +{ + "restart": { + "title": "Jste si jistí, že chcete restartovat Frigate?", + "button": "Restartovat", + "restarting": { + "title": "Frigate restartuje", + "content": "Tato stránka bude obnovena za {{countdown}} sekund.", + "button": "Vynutit opětovné načtení" + } + }, + "explore": { + "plus": { + "review": { + "question": { + "label": "Potvrďte tento štítek pro Frigate Plus", + "ask_a": "Je tento objekt {{label}}?", + "ask_an": "Tento objekt je {{label}}?", + "ask_full": "Je tento objekt {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Odesláno" + } + }, + "submitToPlus": { + "label": "Odeslat do Frigate+", + "desc": "Objekty na místech, kterým se chcete vyhnout, nejsou falešné poplachy. Označení takových objektů jako falešných pozitiv může model zmást." + } + }, + "video": { + "viewInHistory": "Zobrazit historii" + } + }, + "recording": { + "confirmDelete": { + "toast": { + "success": "Videozáznam spojený s vybranými položkami přehledu byl úspěšně smazán.", + "error": "Chyba mazání: {{error}}" + }, + "title": "Potvrdit odstranění", + "desc": { + "selected": "Opravdu chcete smazat všechna nahraná videa spojená s touto položkou přehledu?

    Chcete-li tento dialog v budoucnu obejít, podržte klávesu Shift." + } + }, + "button": { + "markAsReviewed": "Označit jako zkontrolované", + "deleteNow": "Smazat hned", + "export": "Exportovat", + "markAsUnreviewed": "Označit jako nezkontrolované" + } + }, + "export": { + "time": { + "fromTimeline": "Vybrat z Časové osy", + "custom": "Vlastní", + "lastHour_one": "Minulou hodinu", + "lastHour_few": "Minulé {{count}} hodiny", + "lastHour_other": "Minulých {{count}} hodin", + "start": { + "title": "Čas začátku", + "label": "Vybrat čas začátku" + }, + "end": { + "title": "Čas konce", + "label": "Vybrat čas konce" + } + }, + "select": "Vybrat", + "export": "Exportovat", + "selectOrExport": "Vybrat pro Export", + "toast": { + "success": "Export úspěšně spuštěn. Soubor najdete na stránce exportů.", + "error": { + "failed": "Chyba spuštění exportu: {{error}}", + "endTimeMustAfterStartTime": "Čas konce musí být po čase začátku", + "noVaildTimeSelected": "Není vybráno žádné platné časové období" + }, + "view": "Zobrazení" + }, + "fromTimeline": { + "saveExport": "Uložit export", + "previewExport": "Prohlížet export" + }, + "name": { + "placeholder": "Jméno exportu" + } + }, + "streaming": { + "label": "Stream", + "restreaming": { + "disabled": "Restreaming pro tuto kameru není povolen.", + "desc": { + "title": "Nastavte go2rtc pro rozšiřující živé zobrazení a pro zvuk pro tuto kameru.", + "readTheDocumentation": "Přečtěte si dokumentaci" + } + }, + "showStats": { + "label": "Ukázat statistiky streamu", + "desc": "Povolte tuto možnost pro zobrazení překryvných statistik v obraze streamu." + }, + "debugView": "Náhled ladění" + }, + "search": { + "saveSearch": { + "desc": "Zadejte název tohoto uloženého vyhledávání.", + "placeholder": "Zadejte název pro vaše vyhledávání", + "success": "Hledání {{searchName}} bylo uloženo.", + "button": { + "save": { + "label": "Uložit toto hledání" + } + }, + "label": "Uložit vyhledávání", + "overwrite": "{{searchName}} už existuje. Uložení přepíše existující hodnotu." + } + }, + "imagePicker": { + "selectImage": "Vyber náhled sledovaného objektu", + "search": { + "placeholder": "Hledej pomocí štítku nebo podštítku..." + }, + "noImages": "Nebyly nalezeny žádné náhledy pro tuto kameru", + "unknownLabel": "Uložený obrázek Spouštěče" + } +} diff --git a/web/public/locales/cs/components/filter.json b/web/public/locales/cs/components/filter.json new file mode 100644 index 0000000..16d2dd3 --- /dev/null +++ b/web/public/locales/cs/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filtrovat", + "labels": { + "label": "Štítky", + "all": { + "title": "Všechny popisky", + "short": "Popisky" + }, + "count_one": "{{count}} Štítek", + "count_other": "{{count}} Štítků" + }, + "dates": { + "selectPreset": "Vyberte předvolbu…", + "all": { + "title": "Všechny datumy", + "short": "Datumy" + } + }, + "more": "Víc filtrů", + "reset": { + "label": "Resetovat filtry do výchozích hodnot" + }, + "timeRange": "Časový rozsah", + "score": "Skóre", + "subLabels": { + "all": "Všechny podružné štítky", + "label": "Podružné Štítky" + }, + "features": { + "label": "Funkce", + "hasSnapshot": "Má snímek", + "hasVideoClip": "Má videoklip", + "submittedToFrigatePlus": { + "label": "Odesláno do Frigate+", + "tips": "Nejprve musíte filtrovat sledované objekty, které mají snímek.

    Sledované objekty bez snímku nelze odeslat do Frigate+." + } + }, + "sort": { + "label": "Seřadit", + "relevance": "Závažnost", + "dateAsc": "Datum (Vzestupně)", + "dateDesc": "Datum (Sestupně)", + "scoreAsc": "Skóre objektu (Vzestupně)", + "scoreDesc": "Skóre objektu (Sestupně)", + "speedAsc": "Odhadovaná rychlost (Vzestupně)", + "speedDesc": "Odhadovaná rychlost (Sestupně)" + }, + "motion": { + "showMotionOnly": "Pouze zpomalené záběry" + }, + "explore": { + "settings": { + "title": "Nastavení", + "searchSource": { + "label": "Hledat zdroj", + "options": { + "thumbnailImage": "Obrázek náhledu", + "description": "Popis" + }, + "desc": "Zvolte, zda chcete prohledávat miniatury nebo popisy sledovaných objektů." + }, + "gridColumns": { + "desc": "Zvolte počet sloupců mřížky náhledu.", + "title": "Sloupce mřížky" + }, + "defaultView": { + "title": "Základní pohled", + "desc": "Pokud nejsou vybrány žádné filtry, zobrazí se přehled nejnovějších sledovaných objektů podle štítků, nebo se zobrazí nefiltrovaná mřížka.", + "summary": "Souhrn", + "unfilteredGrid": "Nefiltrovaná mřížka" + } + }, + "date": { + "selectDateBy": { + "label": "Vyberte datum k filtrování" + } + } + }, + "logSettings": { + "label": "Filtrovat úroveň protokolu", + "filterBySeverity": "Filtrovat logy podle závažnosti", + "loading": { + "title": "Načítání", + "desc": "Když se podokno protokolu posune dolů, nové protokoly se automaticky zobrazují, jakmile jsou přidány." + }, + "disableLogStreaming": "Zakázat živé zobrazování logu", + "allLogs": "Všechny protokoly" + }, + "recognizedLicensePlates": { + "title": "Rozeznané SPZ", + "loadFailed": "Chyba načítání rozeznaných SPZ.", + "loading": "Načítám rozeznané SPZ…", + "placeholder": "Zadejte text pro hledání SPZ…", + "selectPlatesFromList": "Vyberte jednu, nebo více SPZ ze seznamu.", + "noLicensePlatesFound": "Žádné SPZ nebyly nalezeny.", + "selectAll": "Označit vše", + "clearAll": "Vymazat vše" + }, + "zones": { + "all": { + "title": "Všechny zóny", + "short": "Zóny" + }, + "label": "Zóny" + }, + "trackedObjectDelete": { + "toast": { + "success": "Sledované objekty úspěšně vymazány.", + "error": "Chyba při mazání sledovaných objektů: {{errorMessage}}" + }, + "title": "Potvrdit odstranění", + "desc": "Smazáním těchto {{objectLength}} sledovaných objektů dojde k odstranění snímku, všech uložených vektorových reprezentací (embeddingů) a souvisejících záznamů o životním cyklu objektu. Záznamy z kamery v náhledu historie NEBUDOU smazány.

    Přejete si to skutečně udělat?

    Podržte Shift pro přeskočení tohoto dialogu v budoucnosti." + }, + "zoneMask": { + "filterBy": "Filtrovat podle masky zóny" + }, + "estimatedSpeed": "Odhadovaná rychlost ({{unit}})", + "cameras": { + "label": "Filtr kamer", + "all": { + "title": "Všechny kamery", + "short": "Kamery" + } + }, + "review": { + "showReviewed": "Zobrazit zkontrolované" + }, + "classes": { + "label": "Třídy", + "all": { + "title": "Všechny třídy" + }, + "count_one": "Třída {{count}}", + "count_other": "Třídy {{count}}" + }, + "attributes": { + "label": "Atributy Klasifikace", + "all": "Všechny Atributy" + } +} diff --git a/web/public/locales/cs/components/icons.json b/web/public/locales/cs/components/icons.json new file mode 100644 index 0000000..a98f6d6 --- /dev/null +++ b/web/public/locales/cs/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Zvolte ikonu", + "search": { + "placeholder": "Hledejte ikonu…" + } + } +} diff --git a/web/public/locales/cs/components/input.json b/web/public/locales/cs/components/input.json new file mode 100644 index 0000000..19574eb --- /dev/null +++ b/web/public/locales/cs/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Stáhnout video", + "toast": { + "success": "Vaše video se stahuje." + } + } + } +} diff --git a/web/public/locales/cs/components/player.json b/web/public/locales/cs/components/player.json new file mode 100644 index 0000000..6f32e40 --- /dev/null +++ b/web/public/locales/cs/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "V tomto období nebyly nalezeny žádné záznamy", + "noPreviewFound": "Náhled nenalezen", + "noPreviewFoundFor": "Náhled nenalezen pro {{cameraName}}", + "submitFrigatePlus": { + "title": "Odeslat tento snímek do služby Frigate+?", + "submit": "Odeslat" + }, + "livePlayerRequiredIOSVersion": "Pro tento typ živého přenosu je vyžadován systém iOS 17.1 nebo novější.", + "streamOffline": { + "title": "Přenos je offline", + "desc": "Žádné snímky nebyly zaznamenány na {{cameraName}} detectpřenosu, zkontrolujte logy chyb" + }, + "cameraDisabled": "Kamera je zakázaná", + "stats": { + "streamType": { + "title": "Typ přenosu:", + "short": "Typ" + }, + "bandwidth": { + "title": "Šířka pásma:", + "short": "Šířka pásma" + }, + "latency": { + "title": "Latence:", + "value": "{{seconds}} sekund", + "short": { + "title": "Latence", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Celkový počet snímků:", + "droppedFrames": { + "title": "Ztracené snímky:", + "short": { + "title": "Ztracené", + "value": "{{droppedFrames}} snímků" + } + }, + "decodedFrames": "Dekódované snímky:", + "droppedFrameRate": "Frekvence ztracených snímků:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Snimek byl úspěšně odeslán službě Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Nepodařilo se odeslat snímek službě Frigate+" + } + } +} diff --git a/web/public/locales/cs/config/cameras.json b/web/public/locales/cs/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/cs/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/cs/config/global.json b/web/public/locales/cs/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/cs/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/cs/config/groups.json b/web/public/locales/cs/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/cs/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/cs/config/validation.json b/web/public/locales/cs/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/cs/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/cs/objects.json b/web/public/locales/cs/objects.json new file mode 100644 index 0000000..ca20920 --- /dev/null +++ b/web/public/locales/cs/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Osoba", + "dog": "Pes", + "cat": "Kočka", + "horse": "Kůň", + "bird": "Pták", + "boat": "Člun", + "car": "Auto", + "sheep": "Ovce", + "mouse": "Myš", + "keyboard": "Klávesnice", + "animal": "Zvíře", + "vehicle": "Vozidlo", + "bark": "Štěkot", + "goat": "Koza", + "bus": "Autobus", + "motorcycle": "Motorka", + "train": "Vlak", + "bicycle": "Cyklistické kolo", + "door": "Dveře", + "blender": "Mixér", + "sink": "Dřez", + "scissors": "Nůžky", + "clock": "Hodiny", + "toothbrush": "Zubní kartáček", + "hair_dryer": "Fén", + "skateboard": "Skateboard", + "airplane": "Letadlo", + "traffic_light": "Semafor", + "fire_hydrant": "Požární hydrant", + "street_sign": "Uliční cedule", + "stop_sign": "Stopka", + "parking_meter": "Parkovací hodiny", + "bench": "Lavička", + "cow": "Kráva", + "elephant": "Slon", + "bear": "Medvěd", + "zebra": "Zebra", + "giraffe": "Žirafa", + "hat": "Čepice", + "backpack": "Batoh", + "umbrella": "Deštník", + "shoe": "Bota", + "eye_glasses": "Brýle", + "handbag": "Kabelka", + "tie": "Kravata", + "suitcase": "Oblečení", + "frisbee": "Frisbee", + "skis": "Lyže", + "snowboard": "Snowboard", + "sports_ball": "Sportovní míč", + "kite": "Drak", + "baseball_bat": "Baseballová pálka", + "baseball_glove": "Baseballová rukavice", + "surfboard": "Surfovací prkno", + "tennis_racket": "Tenisová raketa", + "bottle": "Lahev", + "plate": "Talíř", + "wine_glass": "Sklenice na víno", + "cup": "Šálek", + "fork": "Vidlička", + "knife": "Nůž", + "spoon": "Lžíce", + "bowl": "Mísa", + "banana": "Banán", + "apple": "Jablko", + "sandwich": "Sendvič", + "orange": "Pomeranč", + "broccoli": "Brokolice", + "carrot": "Mrkev", + "hot_dog": "Párek v rohlíku", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Koláč", + "chair": "Židle", + "couch": "Gauč", + "potted_plant": "Hrnková rostlina", + "bed": "Postel", + "mirror": "Zrcadlo", + "dining_table": "Jídelní stůl", + "window": "Okno", + "desk": "Stůl", + "toilet": "Toaleta", + "tv": "TV", + "laptop": "Laptop", + "remote": "Dálkový ovladač", + "cell_phone": "Mobilní telefon", + "microwave": "Mikrovlnná trouba", + "oven": "Trouba", + "toaster": "Toustovač", + "refrigerator": "Lednice", + "book": "Kniha", + "vase": "Váza", + "teddy_bear": "Medvídek", + "hair_brush": "Hřeben", + "squirrel": "Veverka", + "deer": "Jelen", + "fox": "Liška", + "rabbit": "Králík", + "raccoon": "Mýval", + "robot_lawnmower": "Robotická sekačka na trávu", + "waste_bin": "Odpadkový koš", + "on_demand": "Na požádání", + "face": "Obličej", + "license_plate": "SPZ", + "package": "Balík", + "bbq_grill": "Gril", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Čistič", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/web/public/locales/cs/views/chat.json b/web/public/locales/cs/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/cs/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/cs/views/classificationModel.json b/web/public/locales/cs/views/classificationModel.json new file mode 100644 index 0000000..e770a1b --- /dev/null +++ b/web/public/locales/cs/views/classificationModel.json @@ -0,0 +1,106 @@ +{ + "documentTitle": "Klasifikační modely - Frigate", + "button": { + "deleteClassificationAttempts": "Odstrániť Klasifikačné obrazy", + "renameCategory": "Přejmenovat třídu", + "deleteCategory": "Smazat třídu", + "deleteImages": "Smazat obrázek", + "trainModel": "Trénovat model", + "addClassification": "Přidat klasifikaci", + "deleteModels": "Smazat modely", + "editModel": "Upravit model" + }, + "details": { + "scoreInfo": "Skóre predstavuje priemernú istotu klasifikácie naprieč detekciami tohoto objektu.", + "none": "Nic", + "unknown": "Neznámý" + }, + "tooltip": { + "trainingInProgress": "Model se právě trénuje", + "noNewImages": "Žádné obrázky pro trénování. Nejdříve klasifikujte obrázky pro dataset.", + "noChanges": "Od posledního trénování nedošlo k žádné změně.", + "modelNotReady": "Model není připravený na trénování." + }, + "toast": { + "success": { + "deletedImage_one": "Smazat obrázky", + "deletedImage_few": "", + "deletedImage_other": "", + "deletedModel_one": "Úspěšně odstraněný {{count}} model", + "deletedModel_few": "Úspěšně odstraněné {{count}} modely", + "deletedModel_other": "Úspěšně odstraněných {{count}} modelů", + "deletedCategory_one": "Smazat třídu", + "deletedCategory_few": "", + "deletedCategory_other": "", + "categorizedImage": "Obrázek úspěšně klasifikován", + "trainedModel": "Úspěšně vytrénovaný model.", + "trainingModel": "Trénování modelu bylo úspěšně zahájeno.", + "updatedModel": "Konfigurace modelu úspěšně aktualizována.", + "renamedCategory": "Třída úspěšně přejmenována na {{name}}" + }, + "error": { + "deleteImageFailed": "Chyba při mazání: {{errorMessage}}", + "deleteCategoryFailed": "Chyba při mazání třídy: {{errorMessage}}", + "deleteModelFailed": "Chyba při mazání modelu: {{errorMessage}}", + "categorizeFailed": "Chyba při mazání obrázku: {{errorMessage}}", + "trainingFailed": "Trénování modelu selhalo. Zkontrolujte logy Frigate pro zjištění detailů.", + "trainingFailedToStart": "Chyba spuštění trénování modelu: {{errorMessage}}", + "updateModelFailed": "Chyba aktualizace modelu: {{errorMessage}}", + "renameCategoryFailed": "Chyba přejmenování třídy: {{errorMessage}}" + } + }, + "train": { + "titleShort": "Nedávný", + "title": "Předchozí klasifikace", + "aria": "Vybrat předchozí Klasifikace" + }, + "deleteModel": { + "desc_one": "Jste si jistí, že chcete odstranit {{count}} model? Tím trvale odstraníte všechny související data včetně obrázků a tréninkových dat. Tato akce je nevratná.", + "desc_few": "Jste si jistí, že chcete odstranit {{count}} modely? Tím trvale odstraníte všechny související data včetně obrázků a tréninkových dat. Tato akce je nevratná.", + "desc_other": "Jste si jistí, že chcete odstranit {{count}} modelů? Tím trvale odstraníte všechny související data včetně obrázků a tréninkových dat. Tato akce je nevratná." + }, + "deleteDatasetImages": { + "desc_one": "Opravdu chcete odstranit {{count}} obrázek z {{dataset}}? Tato akce je nevratná a vyžaduje přetrénování modelu.", + "desc_few": "Opravdu chcete odstranit {{count}} obrázky z {{dataset}}? Tato akce je nevratná a vyžaduje přetrénování modelu.", + "desc_other": "Opravdu chcete odstranit {{count}} obrázků z {{dataset}}? Tato akce je nevratná a vyžaduje přetrénování modelu.", + "title": "Smazat obrázky datové sady" + }, + "deleteTrainImages": { + "desc_one": "Opravdu chcete odstranit {{count}} obrázek? Tato akce je nevratná.", + "desc_few": "Opravdu chcete odstranit {{count}} obrázky? Tato akce je nevratná.", + "desc_other": "Opravdu chcete odstranit {{count}} obrázků? Tato akce je nevratná.", + "title": "Odstranit tréninkové obrázky" + }, + "wizard": { + "step3": { + "allImagesRequired_one": "Prosím, zařaďte všechny obrázky. Zbývá {{count}} obrázek.", + "allImagesRequired_few": "Prosím, zařaďte všechny obrázky. Zbývají {{count}} obrázky.", + "allImagesRequired_other": "Prosím, zařaďte všechny obrázky. Zbývá {{count}} obrázků.", + "trainingStarted": "Trénování úspěšně spuštěno", + "generateSuccess": "Vzorové obrázky byly úspěšně vytvořeny" + } + }, + "deleteCategory": { + "title": "Smazat Třídu", + "desc": "Opravdu chcete odstranit třídu {{name}}? Tím se na trvalo odstraní všechny související obrázky a bude potřeba přetrénovat model.", + "minClassesTitle": "Nemůžete smazat třídu", + "minClassesDesc": "Klasifikační model musí mít alespoň 2 třídy. Než tuto třídu odstraníte přidejte další třídu." + }, + "edit": { + "descriptionObject": "Upravte typ objektu a typ klasifikace pro tento model klasifikace.", + "stateClassesInfo": "Poznámka: Změna tříd stavů vyžaduje přetrénování modelu s aktualizovanými třídami." + }, + "renameCategory": { + "title": "Přejmenovat třídu", + "desc": "Vložte nové jméno pro {{name}}. Aby se změna názvu projevila, bude nutné model znovu natrénovat." + }, + "description": { + "invalidName": "Neplatné jméno. Jméno můžou obsahovat pouze písmena, čísla, mezery, apostrofy, podtržítka a spojovníky." + }, + "categories": "Třídy", + "createCategory": { + "new": "Vytvořit novou Třídu" + }, + "categorizeImageAs": "Klasifikovat obrázek jako:", + "categorizeImage": "Klasifikovat obrázek" +} diff --git a/web/public/locales/cs/views/configEditor.json b/web/public/locales/cs/views/configEditor.json new file mode 100644 index 0000000..19982f2 --- /dev/null +++ b/web/public/locales/cs/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor konfigurace - Frigate", + "configEditor": "Editor konfigurace", + "copyConfig": "Kopírovat konfiguraci", + "saveAndRestart": "Uložit a restartovat", + "saveOnly": "Jen uložit", + "toast": { + "success": { + "copyToClipboard": "Konfigurace byla zkopírovaná do schránky." + }, + "error": { + "savingError": "Chyba ukládání konfigurace" + } + }, + "confirm": "Opustit bez uložení?", + "safeConfigEditor": "Editor konfigurace (Nouzový režim)", + "safeModeDescription": "Frigate je v nouzovém režimu kvůli chybě při ověřování konfigurace." +} diff --git a/web/public/locales/cs/views/events.json b/web/public/locales/cs/views/events.json new file mode 100644 index 0000000..868e871 --- /dev/null +++ b/web/public/locales/cs/views/events.json @@ -0,0 +1,67 @@ +{ + "alerts": "Výstrahy", + "detections": "Detekce", + "motion": { + "label": "Pohyb", + "only": "Jen pohyb" + }, + "allCameras": "Všechny kamery", + "empty": { + "alert": "Nejsou žádné výstrahy na kontrolu", + "detection": "Nejsou žádné detekce na kontrolu", + "motion": "Nenalezena žádná data o pohybu", + "recordingsDisabled": { + "title": "Nahrávání musí být povoleno", + "description": "Položky revize lze pro kameru vytvořit pouze tehdy, je-li pro ni povoleno nahrávání." + } + }, + "timeline": "Časová osa", + "timeline.aria": "Zvolit časovou osu", + "events": { + "label": "Události", + "aria": "Zvolit události", + "noFoundForTimePeriod": "Pro toto časové období nebyly nalezeny žádné události." + }, + "documentTitle": "Revize - Frigate", + "camera": "Kamera", + "calendarFilter": { + "last24Hours": "Posledních 24 hodin" + }, + "markAsReviewed": "Označit jako zkontrolované", + "markTheseItemsAsReviewed": "Označit tyto položky jako zkontrolované", + "newReviewItems": { + "label": "Zobrazit nové položky revize", + "button": "Nové položky revize" + }, + "recordings": { + "documentTitle": "Záznamy - Frigate" + }, + "detected": "Detekováno", + "selected_one": "{{count}} vybráno", + "selected_other": "{{count}} vybráno", + "suspiciousActivity": "Podezřelá aktivita", + "threateningActivity": "Ohrožující činnost", + "zoomIn": "Přiblížit", + "zoomOut": "Oddálit", + "detail": { + "label": "Detail", + "noDataFound": "Žádná detailní data k prohlédnutí", + "aria": "Přepnout zobrazení detailů", + "trackedObject_other": "{{count}} objektů", + "trackedObject_one": "{{count}} objekt", + "noObjectDetailData": "Nejsou k dispozici žádné podrobné údaje o objektu.", + "settings": "Nastavení Detailního Zobrazení", + "alwaysExpandActive": { + "title": "Vždy rozbalit aktivní", + "desc": "Vždy zobrazit podrobnosti objektu aktivní položky revize, pokud jsou k dispozici." + } + }, + "objectTrack": { + "trackedPoint": "Sledovaný bod", + "clickToSeek": "Kliknutím přeskočte na tento čas" + }, + "select_all": "Vše", + "normalActivity": "Normální", + "needsReview": "Potřebuje revizi", + "securityConcern": "Obava o bezpečnost" +} diff --git a/web/public/locales/cs/views/explore.json b/web/public/locales/cs/views/explore.json new file mode 100644 index 0000000..2e2532f --- /dev/null +++ b/web/public/locales/cs/views/explore.json @@ -0,0 +1,283 @@ +{ + "generativeAI": "Generativní AI", + "documentTitle": "Prozkoumat – Frigate", + "details": { + "timestamp": "Časové razítko", + "snapshotScore": { + "label": "Skóre snímku" + }, + "item": { + "title": "Zkontrolovat detaily položky", + "desc": "Zkontrolovat detaily položky", + "button": { + "share": "Sdílet tuto položku kontroly", + "viewInExplore": "Zobrazit v Průzkumu" + }, + "tips": { + "hasMissingObjects": "Upravte svou konfiguraci, pokud chcete, aby Frigate ukládal sledované objekty pro následující štítky: {{objects}}", + "mismatch_one": "{{count}} nedostupný objekt byl detekován a vložen do této položky kontroly. Tento objekt se buď nekvalifikoval jako výstraha nebo detekce, nebo již byl vyčištěn/smazán.", + "mismatch_few": "{{count}} nedostupné objekty byly detekovány a vloženy do této položky kontroly. Tyto objekty se buď nekvalifikovaly jako výstraha nebo detekce, nebo již byly vyčištěny/smazány.", + "mismatch_other": "{{count}} nedostupných objektů bylo detekováno a vloženo do této položky kontroly. Tyto objekty se buď nekvalifikovaly jako výstraha nebo detekce, nebo již byly vyčištěny/smazány." + }, + "toast": { + "success": { + "regenerate": "Od {{provider}} byl vyžádán nový popis. V závislosti na rychlosti vašeho poskytovatele může obnovení nového popisu nějakou dobu trvat.", + "updatedSublabel": "Úspěšně aktualizovaný podružný štítek.", + "updatedLPR": "Úspěšně aktualizovaná SPZ.", + "audioTranscription": "Požádání o přepis zvuku bylo úspěšné. V závislosti na rychlosti Vašeho Frigate serveru může přepis trvat nějaký čas než bude dokončen.", + "updatedAttributes": "Atributy byly úspěšně aktualizovány." + }, + "error": { + "regenerate": "Chyba volání {{provider}} pro nový popis: {{errorMessage}}", + "updatedSublabelFailed": "Chyba obnovení podružného štítku: {{errorMessage}}", + "updatedLPRFailed": "Chyba obnovení SPZ: {{errorMessage}}", + "audioTranscription": "Požádání o přepis zvuku bylo neúspěšné: {{errorMessage}}" + } + } + }, + "editSubLabel": { + "descNoLabel": "Vložit nový podružný štítek pro tento sledovaný objekt", + "desc": "Vložit nový podružný štítek pro tento {{label}}", + "title": "Upravit podružný štítek" + }, + "editLPR": { + "title": "Upravit SPZ", + "descNoLabel": "Vložit novou SPZ pro tento sledovaný objekt", + "desc": "Vložit novou SPZ pro tento {{label}}" + }, + "estimatedSpeed": "Odhadovaná rychlost", + "objects": "Objekty", + "camera": "Kamera", + "zones": "Zóny", + "button": { + "regenerate": { + "label": "Regenerovat popis sledovaného objektu", + "title": "Regenerovat" + }, + "findSimilar": "Najít podobné" + }, + "description": { + "label": "Popis", + "placeholder": "Popis sledovaného objektu", + "aiTips": "Frigate nebude vyžadovat popis od vašeho poskytovatele generativní AI, dokud neskončí životní cyklus sledovaného objektu." + }, + "expandRegenerationMenu": "Rozbalte nabídku regenerace", + "regenerateFromSnapshot": "Regenerovat ze snímku", + "regenerateFromThumbnails": "Regenerovat z náhledu", + "tips": { + "descriptionSaved": "Popis úspěšně uložen", + "saveDescriptionFailed": "Aktualizace popisu se nezdařila: {{errorMessage}}" + }, + "topScore": { + "info": "Nejvyšší skóre je nejvyšší střední skóre pro sledovaný objekt, takže se může lišit od skóre zobrazeného na miniatuře výsledku vyhledávání.", + "label": "Nejvyšší skóre" + }, + "label": "Štítek", + "recognizedLicensePlate": "Rozpoznaná SPZ", + "score": { + "label": "Skóre" + } + }, + "exploreIsUnavailable": { + "title": "Prozkoumat je nedostupné", + "embeddingsReindexing": { + "context": "Prozkoumat může být použito až vložené sledované objekty dokončí přeindexování.", + "startingUp": "Spouštění…", + "estimatedTime": "Odhadovaný zbývající čas:", + "finishingShortly": "Brzy bude dokončeno", + "step": { + "thumbnailsEmbedded": "Vložené náhledy: ", + "descriptionsEmbedded": "Vložené popisy: ", + "trackedObjectsProcessed": "Zpracované sledované objekty: " + } + }, + "downloadingModels": { + "context": "Frigate stahuje potřebné modely vložení pro podporu funkce sémantického vyhledávání. To může trvat několik minut v závislosti na rychlosti vašeho síťového připojení.", + "setup": { + "visionModelFeatureExtractor": "Extraktor funkcí Vision modelu", + "visionModel": "Vision model", + "textModel": "Textový model", + "textTokenizer": "Textový tokenizér" + }, + "tips": { + "documentation": "Přečtěte si dokumentaci", + "context": "Můžete reindexovat vložení vašich sledovaných oběktů hned jak budou modely staženy." + }, + "error": "Nastala chyba. Zkontrolujte protokoly Frigate." + } + }, + "trackedObjectsCount_one": "{{count}} sledovaný objekt ", + "trackedObjectsCount_few": "{{count}} sledované objekty ", + "trackedObjectsCount_other": "{{count}} sledovaných objektů ", + "searchResult": { + "tooltip": "Shoda s {{type}} na {{confidence}} %", + "deleteTrackedObject": { + "toast": { + "error": "Sledovaný objekt se nepodařilo smazat: {{errorMessage}}", + "success": "Sledovaný objekt úspěšně smazán." + } + }, + "previousTrackedObject": "Předchozí sledovaný objekt", + "nextTrackedObject": "Následující sledovaný objekt" + }, + "objectLifecycle": { + "count": "{{first}} z {{second}}", + "trackedPoint": "Sledovaný bod", + "lifecycleItemDesc": { + "external": "{{label}} detekován", + "header": { + "zones": "Zóny", + "ratio": "Poměr", + "area": "Oblast" + }, + "entered_zone": "{{label}} vstoupil do {{zones}}", + "active": "{{label}} začal být aktivní", + "stationary": "{{label}} se stal statickým", + "visible": "{{label}} detekován", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detekován pro {{label}}", + "other": "{{label}} rozpoznán jako {{attribute}}" + }, + "heard": "{{label}} slyšen", + "gone": "{{label}} opuštěn" + }, + "annotationSettings": { + "showAllZones": { + "title": "Zobrazit všechny zóny", + "desc": "Vždy zobrazit zóny na snímcích, kde objekty vstoupily do zóny." + }, + "offset": { + "label": "Odsazení popisku", + "documentation": "Přečtěte si dokumentaci ", + "millisecondsToOffset": "Milisekundy pro posun zobrazení popisku detekce. Výchozí: 0", + "desc": "Tato data pocházejí z detekčního zdroje vaší kamery, ale jsou překryta na snímcích ze záznamu. Je nepravděpodobné, že by tyto dva proudy byly dokonale synchronizované. Výsledkem je, že ohraničující rámeček a stopáž nebudou dokonale zarovnány. K nastavení však lze použít pole annotation_offset.", + "tips": "TIP: Představte si, že existuje klip události s osobou, která jde zleva doprava. Pokud je ohraničovací rámeček na časové ose události konzistentně vlevo od osoby, měla by být hodnota snížena. Podobně, pokud osoba chodí zleva doprava a ohraničující rámeček je trvale před osobou, měla by být hodnota zvýšena.", + "toast": { + "success": "Posun anotace pro {{camera}} byl uložen do konfiguračního souboru. Restartujte Frigate, aby se změny projevily." + } + }, + "title": "Nastavení popisků" + }, + "carousel": { + "previous": "Předchozí snímek", + "next": "Následující snímek" + }, + "title": "Životní cyklus Objektu", + "noImageFound": "Žádný obrázek pro toto časové razítko.", + "createObjectMask": "Vytvořit masku objektu", + "adjustAnnotationSettings": "Upravit nastavení poznámek", + "scrollViewTips": "Posouváním zobrazíte významné okamžiky životního cyklu tohoto objektu.", + "autoTrackingTips": "Pozice ohraničení bude nepřesná pro kamery s automatickým sledováním." + }, + "itemMenu": { + "downloadSnapshot": { + "label": "Stáhnout snímek", + "aria": "Stáhnout snímek" + }, + "viewInHistory": { + "aria": "Zobrazit v historii", + "label": "Zobrazit historii" + }, + "deleteTrackedObject": { + "label": "Smazat tento sledovaný objekt" + }, + "downloadVideo": { + "label": "Stáhnout video", + "aria": "Stáhnout video" + }, + "findSimilar": { + "aria": "Najít podobný sledovaný objekt", + "label": "Najít podobné" + }, + "submitToPlus": { + "aria": "Odeslat do Frigate Plus", + "label": "Odeslat do Frigate+" + }, + "viewObjectLifecycle": { + "label": "Zobrazit životní cyklus objektu", + "aria": "Ukázat životní cyklus objektu" + }, + "addTrigger": { + "label": "Přidat spouštěč", + "aria": "Přidat spouštěč pro tento sledovaný objekt" + }, + "audioTranscription": { + "label": "Přepsat", + "aria": "Požádat o přepis zvukového záznamu" + }, + "showObjectDetails": { + "label": "Zobrazit trasu objektu" + }, + "hideObjectDetails": { + "label": "Skrýt trasu objektu" + } + }, + "dialog": { + "confirmDelete": { + "title": "Potvrdit smazání", + "desc": "Odstraněním tohoto sledovaného objektu se odstraní snímek, všechna uložená vložení a všechny související položky s podrobnostmi o sledování. Zaznamenaný záznam tohoto sledovaného objektu v zobrazení Historie NEBUDE smazán.

    Opravdu chcete pokračovat?" + } + }, + "trackedObjectDetails": "Detaily sledovaných objektů", + "type": { + "details": "detaily", + "snapshot": "snímek", + "video": "video", + "object_lifecycle": "životní cyklus objektu", + "thumbnail": "Náhled", + "tracking_details": "detaily sledování" + }, + "noTrackedObjects": "Žádné sledované objekty nebyly nalezeny", + "fetchingTrackedObjectsFailed": "Chyba při načítání sledovaných objektů: {{errorMessage}}", + "exploreMore": "Prozkoumat více {{label}} objektů", + "aiAnalysis": { + "title": "Analýza AI" + }, + "concerns": { + "label": "Obavy" + }, + "trackingDetails": { + "title": "Detaily Sledování", + "noImageFound": "Nebyl nalezen obrázek pro tuto časovou značku.", + "createObjectMask": "Vytvořit Masku Objektu", + "adjustAnnotationSettings": "Upravte nastavení poznámek", + "scrollViewTips": "Klikněte pro zobrazení významných okamžiků z životního cyklu tohoto objektu.", + "autoTrackingTips": "Pozice ohraničujících rámečků budou nepřesné pro kamery s automatickým sledováním.", + "count": "{{first}} z {{second}}", + "trackedPoint": "Sledovaný Bod", + "lifecycleItemDesc": { + "visible": "Detekován {{label}}", + "entered_zone": "{{label}} vstoupil do {{zones}}", + "active": "{{label}} se stal aktivním", + "stationary": "{{label}} se zastavil", + "attribute": { + "faceOrLicense_plate": "Pro {{label}} zjištěn {{attribute}}" + }, + "header": { + "ratio": "Poměr", + "area": "Oblast", + "score": "Skóre" + } + }, + "annotationSettings": { + "title": "Nastavení anotací", + "showAllZones": { + "title": "Zobrazit všechny zóny", + "desc": "Vždy zobrazovat zóny na snímcích, na kterých objekty vstoupili do zóny." + }, + "offset": { + "label": "Odsazení anotace", + "desc": "Tato data pocházejí z detekčního kanálu vaší kamery, ale překrývají se s obrázky ze záznamového kanálu. Je nepravděpodobné, že by oba streamy byly dokonale synchronizované. V důsledku toho se ohraničovací rámeček a záznam nebudou dokonale srovnávat. Toto nastavení můžete použít k časovému posunutí anotací dopředu nebo dozadu, abyste je lépe zarovnali se zaznamenaným záznamem.", + "millisecondsToOffset": "Milisekundy na posunutí detekce anotací. Výchozí: 0", + "tips": "Snižte hodnotu, pokud je přehrávané video před ohraničením a body cesty, nebo zvyšte hodnotu, pokud je přehrávané video za nimi. Hodnota může být i záporná.", + "toast": { + "success": "Odsazení anotací pro {{camera}} bylo uloženo do konfiguračního souboru." + } + } + }, + "carousel": { + "previous": "Předcházející snímek", + "next": "Další snímek" + } + } +} diff --git a/web/public/locales/cs/views/exports.json b/web/public/locales/cs/views/exports.json new file mode 100644 index 0000000..5fb25d6 --- /dev/null +++ b/web/public/locales/cs/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "Hledat", + "documentTitle": "Exportovat - Frigate", + "noExports": "Žádné exporty nenalezeny", + "deleteExport": "Smazat export", + "deleteExport.desc": "Opravdu chcete smazat {{exportName}}?", + "editExport": { + "title": "Přejmenovat export", + "desc": "Zadejte nové jméno pro tento export.", + "saveExport": "Uložit export" + }, + "toast": { + "error": { + "renameExportFailed": "Nepodařilo se přejmenovat export: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Sdílet export", + "downloadVideo": "Stáhnout video", + "deleteExport": "Smazat export", + "editName": "Upravit jméno" + } +} diff --git a/web/public/locales/cs/views/faceLibrary.json b/web/public/locales/cs/views/faceLibrary.json new file mode 100644 index 0000000..2d163b1 --- /dev/null +++ b/web/public/locales/cs/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "imageEntry": { + "dropInstructions": "Přetáhněte obrázek sem, nebo klikněte na výběr", + "maxSize": "Maximální velikost: {{size}}MB", + "dropActive": "Přetáhněte obrázek zde…", + "validation": { + "selectImage": "Prosím vyberte soubor obrázků." + } + }, + "createFaceLibrary": { + "new": "Vytvořit nový obličej", + "desc": "Vytvořit novou kolekci", + "nextSteps": "Chcete-li vybudovat pevný základ:
  • Použijte kartu Nedávná Rozpoznání k výběru a trénování na snímcích pro každou detekovanou osobu.
  • Pro nejlepší výsledky se zaměřte na přímé snímky; vyhněte se trénování snímků, které zachycují obličeje pod úhlem.
  • ", + "title": "Vytvořit kolekci" + }, + "details": { + "unknown": "Neznámý", + "person": "Osoba", + "face": "Detaily tváře", + "subLabelScore": "Skóre dílčího popisku", + "scoreInfo": "Skóre dílčího popisku je vážené skóre všech jistot rozpoznání obličejů, ale to může být odlišné od skóre zobrazeného na snímku.", + "faceDesc": "Podrobnosti o sledovaném objektu, který vygeneroval tuto tvář", + "timestamp": "Časové razítko" + }, + "selectFace": "Vyberte tvář", + "deleteFaceAttempts": { + "title": "Odstranit obličeje", + "desc_one": "Skutečně chcete vymazat {{count}} obličej? Tato akce je nevratná.", + "desc_few": "Skutečně chcete vymazat {{count}} obličeje? Tato akce je nevratná.", + "desc_other": "Skutečně chcete vymazat {{count}} obličejů? Tato akce je nevratná." + }, + "nofaces": "Žádné tváře", + "pixels": "{{area}}px", + "deleteFaceLibrary": { + "title": "Odstranit jméno", + "desc": "Skutečně chcete vymazat kolekci {{name}}? Toto trvale vymaže všechny přiřazené obličeje." + }, + "train": { + "title": "Nedávná rozpoznání", + "empty": "Nejsou zde žádné předchozí pokusy o rozpoznání obličeje", + "aria": "Vybrat poslední rozpoznávání", + "titleShort": "Nedávný" + }, + "description": { + "addFace": "Přidejte novou kolekci do Knihovny obličejů nahráním prvního obrázku.", + "placeholder": "Zadejte název pro tuto kolekci", + "invalidName": "Neplatné jméno. Jméno můžou obsahovat pouze písmena, čísla, mezery, apostrofy, podtržítka a spojovníky." + }, + "documentTitle": "Knihovna obličejů - Frigate", + "uploadFaceImage": { + "title": "Nahrát obrázek obličeje", + "desc": "Nahrajte obrázek pro skenování tváří a zahrňte jej pro {{pageToggle}}" + }, + "button": { + "deleteFaceAttempts": "Odstranění obličeje", + "addFace": "Přidat obličej", + "renameFace": "Přejmenovat obličej", + "deleteFace": "Odstranit obličej", + "uploadImage": "Nahrát obrázek", + "reprocessFace": "Přepracovat Obličej" + }, + "trainFace": "Trénovat obličej", + "selectItem": "Vyberte {{item}}", + "renameFace": { + "title": "Přejmenovat obličej", + "desc": "Zadejte nový název pro {{name}}" + }, + "readTheDocs": "Přečtěte si dokumentaci", + "toast": { + "success": { + "renamedFace": "Úspěšně přejmenovaný obličej na {{name}}", + "trainedFace": "Úspěšně vytrénovaný obličej.", + "uploadedImage": "Úspěšně nahraný obrázek.", + "deletedFace_one": "Úspěšně odstraněna {{count}} tvář.", + "deletedFace_few": "Úspěšně odstraněny {{count}} tváře.", + "deletedFace_other": "Úspěšně odstraněny {{count}} tváře.", + "deletedName_one": "{{count}} obličej byl úspěšně odstraněn.", + "deletedName_few": "{{count}} tváře byly úspěšně odstraněny.", + "deletedName_other": "{{count}} tváře byly úspěšně odstraněny.", + "updatedFaceScore": "Úspěšně aktualizováno skóre obličeje na {{name}} ({{score}}).", + "addFaceLibrary": "{{name}} byl(a) úspěšně přidán(a) do Knihovny obličejů!" + }, + "error": { + "renameFaceFailed": "Chyba při přejmenování obličeje: {{errorMessage}}", + "trainFailed": "Chyba trénování: {{errorMessage}}", + "updateFaceScoreFailed": "Chyba aktualizace skóre obličeje: {{errorMessage}}", + "deleteFaceFailed": "Chyba při mazání: {{errorMessage}}", + "uploadingImageFailed": "Chyba při nahrávání obrázku: {{errorMessage}}", + "addFaceLibraryFailed": "Nepodařilo se nastavit jméno obličeje: {{errorMessage}}", + "deleteNameFailed": "Chyba při mazání jména: {{errorMessage}}" + } + }, + "steps": { + "nextSteps": "Další kroky", + "faceName": "Zadejte název obličeje", + "uploadFace": "Nahrát obrázek obličeje", + "description": { + "uploadFace": "Pro {{name}} nahrajte obrázek, který zobrazuje jeho obličej zepředu. Obrázek nemusí být oříznut pouze na jeho obličej." + } + }, + "collections": "Kolekce", + "trainFaceAs": "Trénovat Obličej jako:" +} diff --git a/web/public/locales/cs/views/live.json b/web/public/locales/cs/views/live.json new file mode 100644 index 0000000..0985ba2 --- /dev/null +++ b/web/public/locales/cs/views/live.json @@ -0,0 +1,177 @@ +{ + "documentTitle": "Živě - Frigate", + "documentTitle.withCamera": "{{camera}}-Živě-Frigate", + "lowBandwidthMode": "Režim nízké šířky pásma", + "twoWayTalk": { + "enable": "Povolit obousměrný hovor", + "disable": "Zakázat obousměrný hovor" + }, + "cameraAudio": { + "enable": "Povolit zvuk kamery", + "disable": "Zakázat zvuk kamery" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Klikněte do snímku pro vycentrování kamery", + "enable": "Povolit pohyb kliknutím", + "disable": "Zakázat pohyb kliknutím" + }, + "left": { + "label": "Posunot PTZ kameru do leva" + }, + "up": { + "label": "Posunot PTZ kameru nahoru" + }, + "down": { + "label": "Posunot PTZ kameru dolů" + }, + "right": { + "label": "Posunot PTZ kameru do prava" + } + }, + "zoom": { + "in": { + "label": "Přiblížit PTZ kameru" + }, + "out": { + "label": "Oddálit PTZ kameru" + } + }, + "frame": { + "center": { + "label": "Klikněte do snímku pro vycentrování PTZ kamery" + } + }, + "presets": "Předvolby PTZ kamery", + "focus": { + "in": { + "label": "Zaostření PTZ kamery" + }, + "out": { + "label": "Rozostření PTZ kamery" + } + } + }, + "camera": { + "enable": "Povolit kameru", + "disable": "Zakázat kameru" + }, + "muteCameras": { + "enable": "Ztlumit všechny kamery", + "disable": "Zrušit ztlumení u všech kamer" + }, + "detect": { + "enable": "Povolit detekci", + "disable": "Zakázat detekci" + }, + "recording": { + "enable": "Povolit nahrávání", + "disable": "Zakázat nahrávání" + }, + "snapshots": { + "enable": "Povolit vytváření snímků", + "disable": "Zakázat vytváření snímků" + }, + "audioDetect": { + "enable": "Povolit detekci zvuků", + "disable": "Zakázat detekci zvuků" + }, + "autotracking": { + "enable": "Povolit automatické sledování", + "disable": "Zakázat automatické sledování" + }, + "streamStats": { + "disable": "Skrýt statistiky streamu", + "enable": "Ukázat statistiky streamu" + }, + "manualRecording": { + "title": "Na požádání", + "playInBackground": { + "label": "Přehrát na pozadí", + "desc": "Povolte tuto volbu pro pokračování streamování i když je přehrávač skrytý." + }, + "showStats": { + "label": "Ukázat statistiky", + "desc": "Povolte tuto možnost pro zobrazení překryvných statistik v obraze streamu." + }, + "debugView": "Náhled ladění", + "start": "Spustit nahrávání na vyžádání", + "failedToStart": "Chyba manuálního spuštění nahrávání na požádání.", + "end": "Konec nahrávání na vyžádání", + "failedToEnd": "Chyba ukončení manuálního nahrávání na vyžádání.", + "started": "Manuálně spuštěno nahrávání na požádání.", + "ended": "Ukončeno manuální nahrávání na vyžádání.", + "recordDisabledTips": "Protože je v konfiguraci této kamery nahrávání zakázáno nebo omezeno, bude uložen pouze snímek.", + "tips": "Stáhněte si aktuální snímek nebo spusťte ruční událost na základě nastavení uchování záznamu této kamery." + }, + "streamingSettings": "Nastavení Streamování", + "audio": "Zvuk", + "suspend": { + "forTime": "Pozastavení na: " + }, + "stream": { + "title": "Proud", + "audio": { + "tips": { + "title": "Zvuk musí být kamerou vysílán a nakonfigurován v go2rtc pro tento stream.", + "documentation": "Přečtěte si dokumentaci " + }, + "available": "Zvuk je dostupný pro tento stream", + "unavailable": "Zvuk není dostupný pro tento stream" + }, + "twoWayTalk": { + "tips.documentation": "Přečtěte si dokumentaci ", + "available": "Obousměrný hovor je dostupný pro tento stream", + "unavailable": "Obousměrný hovor není dostupný pro tento stream", + "tips": "Vaše zařízení musí tuto funkci podporovat a WebRTC musí být nakonfigurováno pro obousměrnou komunikaci." + }, + "lowBandwidth": { + "resetStream": "Resetovat stream", + "tips": "Živý náhled je v režimu nízké přenosové rychlosti kvůli vyrovnávací paměti nebo chybám ve streamu." + }, + "playInBackground": { + "label": "Přehrát na pozadí", + "tips": "Povolte tuto volbu pro pokračování streamování i když je přehrávač skrytý." + } + }, + "cameraSettings": { + "title": "{{camera}} Nastavení", + "cameraEnabled": "Kamera Povolena", + "objectDetection": "Detekce Objektu", + "snapshots": "Snímky", + "audioDetection": "Detekce Zvuku", + "autotracking": "Automatické sledování", + "recording": "Nahrávání", + "transcription": "Zvukový přepis" + }, + "history": { + "label": "Zobrazit historické záznamy" + }, + "effectiveRetainMode": { + "modes": { + "all": "Vše", + "motion": "Pohyb", + "active_objects": "Aktivní Objekty" + }, + "notAllTips": "Vaše nastavení uchovávání záznamů pro zdroj {{source}} je nastaveno na režim: {{effectiveRetainMode}}, takže tento záznam na vyžádání bude uchovávat pouze segmenty s režimem {{effectiveRetainModeName}}." + }, + "editLayout": { + "exitEdit": "Ukončit Úpravu", + "label": "Upravit Rozložení", + "group": { + "label": "Upravit Skupinu Kamer" + } + }, + "notifications": "Notifikace", + "transcription": { + "enable": "Povolit živý přepis zvuku", + "disable": "Zakázat živý přepis zvuku" + }, + "snapshot": { + "takeSnapshot": "Stáhnout aktuální snímek", + "noVideoSource": "Pro snímek není k dispozici žádné video.", + "captureFailed": "Zachycení snímku selhalo.", + "downloadStarted": "Stažení snímku spuštěno." + } +} diff --git a/web/public/locales/cs/views/motionSearch.json b/web/public/locales/cs/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/cs/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/cs/views/recording.json b/web/public/locales/cs/views/recording.json new file mode 100644 index 0000000..80fa1f1 --- /dev/null +++ b/web/public/locales/cs/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Exportovat", + "calendar": "Kalendář", + "filter": "Filtrovat", + "filters": "Filtry", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Čas konce musí být po čase začátku", + "noValidTimeSelected": "Nebylo zvoleno platné časové období" + } + } +} diff --git a/web/public/locales/cs/views/replay.json b/web/public/locales/cs/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/cs/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/cs/views/search.json b/web/public/locales/cs/views/search.json new file mode 100644 index 0000000..2d69979 --- /dev/null +++ b/web/public/locales/cs/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Hledat", + "savedSearches": "Uložená vyhledávání", + "searchFor": "Hledat {{inputValue}}", + "button": { + "clear": "Vymazat vyhledávaní", + "save": "Uložit vyhledávání", + "delete": "Vymazat uložená vyhledávání", + "filterInformation": "Filtrovat informace", + "filterActive": "Aktivní filtry" + }, + "trackedObjectId": "ID sledovaného objektu", + "filter": { + "label": { + "cameras": "Kamery", + "labels": "Štítky", + "zones": "Zóny", + "sub_labels": "Podružné Štítky", + "max_speed": "Max rychlost", + "min_speed": "Min rychlost", + "search_type": "Typ Hledání", + "time_range": "Časový Rozsah", + "before": "Před", + "after": "Po", + "max_score": "Maximální Skóre", + "min_score": "Minimální Skóre", + "recognized_license_plate": "Rozpoznaná SPZ", + "has_clip": "Má Klip", + "has_snapshot": "Má Snímek", + "attributes": "Atributy" + }, + "tips": { + "desc": { + "step1": "Zadejte název filtru následovaný dvojtečkou (např. „kamery:“).", + "step2": "Vyberte hodnotu z nabízených možností nebo zadejte vlastní.", + "step3": "Použijte více filtrů tak, že je přidáte jeden po druhém s mezerou mezi nimi.", + "step4": "Datumové filtry (before: a after:) používají formát {{DateFormat}}.", + "step6": "Odstraňte filtr kliknutím na 'x' vedle něj.", + "exampleLabel": "Příklad:", + "text": "Filtry vám pomohou zúžit výsledky hledání. Zde je návod, jak je používat ve vstupním poli:", + "step5": "Pro časový rozsah použijte formát {{exampleTime}}." + }, + "title": "Jak používat textové filtry" + }, + "searchType": { + "thumbnail": "Náhled", + "description": "Popis" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Datum 'před' musí být pozdější než datum 'po'.", + "afterDatebeEarlierBefore": "Datum 'po' musí být dřívější než datum 'před'.", + "minScoreMustBeLessOrEqualMaxScore": "'min_score' musí být menší nebo rovno 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'max_score' musí být větší nebo rovno 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'min_speed' musí být menší nebo rovno 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'max_speed' musí být větší nebo rovno 'min_speed'." + } + }, + "header": { + "currentFilterType": "Hodnoty Filtru", + "noFilters": "Filtry", + "activeFilters": "Aktivní Filtry" + } + }, + "similaritySearch": { + "title": "Hledání Podle Podobnosti", + "active": "Hledání podobností aktivní", + "clear": "Vymazat hledání podobností" + }, + "placeholder": { + "search": "Hledat…" + } +} diff --git a/web/public/locales/cs/views/settings.json b/web/public/locales/cs/views/settings.json new file mode 100644 index 0000000..e784a1c --- /dev/null +++ b/web/public/locales/cs/views/settings.json @@ -0,0 +1,938 @@ +{ + "documentTitle": { + "default": "Nastavení - Frigate", + "authentication": "Nastavení ověřování - Frigate", + "camera": "Nastavení kamery - Frigate", + "classification": "Nastavení klasifikace - Frigate", + "notifications": "Nastavení notifikací - Frigate", + "masksAndZones": "Editor masky a zón - Frigate", + "motionTuner": "Ladění detekce pohybu - Frigate", + "object": "Ladění - Frigate", + "general": "Nastavení rozhraní - Frigate", + "frigatePlus": "Frigate+ nastavení - Frigate", + "enrichments": "Nastavení obohacení - Frigate", + "cameraManagement": "Správa kamer - Frigate", + "cameraReview": "Nastavení kontroly kamery - Frigate" + }, + "frigatePlus": { + "toast": { + "error": "Chyba při ukládání změn konfigurace: {{errorMessage}}", + "success": "Nastavení Frigate+ byla uložena. Restartujte Frigate+ pro aplikování změn." + }, + "modelInfo": { + "cameras": "Kamery", + "modelSelect": "Zde můžete vybrat dostupné modely ze služby Frigate+. Upozorňujeme, že lze zvolit pouze modely kompatibilní s aktuální konfigurací detektoru.", + "loadingAvailableModels": "Načítám dostupné modely…", + "plusModelType": { + "baseModel": "Základní Model", + "userModel": "Doladěno" + }, + "loading": "Načítám informace o modelu…", + "error": "Chyba načítání informací o modelu", + "availableModels": "Dostupné Moduly", + "supportedDetectors": "Podporované Detektory", + "title": "Informace o Modelu", + "modelType": "Typ Modelu", + "trainDate": "Datum Tréninku", + "baseModel": "Základní Model" + }, + "snapshotConfig": { + "documentation": "Přečtěte si dokumentaci", + "desc": "Odesílání do Frigate+ vyžaduje, aby byly ve vaší konfiguraci povoleny jak běžné snímky, tak snímky typu clean_copy.", + "title": "Nastavení Snímku", + "table": { + "cleanCopySnapshots": "clean_copy Snímky", + "snapshots": "Snímky", + "camera": "Kamera" + }, + "cleanCopyWarning": "Některé kamery mají povolené snímky, ale volba clean_copy je zakázaná. Pro možnost odesílání snímků z těchto kamer do služby Frigate+ je nutné tuto volbu povolit v konfiguraci snímků." + }, + "apiKey": { + "notValidated": "API klíč Frigate+ nebyl rozpoznán nebo nebyl ověřen", + "plusLink": "Přečtěte si více o Frigate+", + "validated": "API klíč Frigate+ byl rozpoznán a ověřen", + "desc": "API klíč Frigate+ umožňuje integraci se službou Frigate+.", + "title": "Frigate+ API Klíč" + }, + "unsavedChanges": "Neuložené změny nastavení Frigate+", + "title": "Nastavení Frigate+", + "restart_required": "Vyžadován restart (model Frigate+ změněn)" + }, + "classification": { + "unsavedChanges": "Neuložené změny nastavení klasifikací", + "semanticSearch": { + "readTheDocumentation": "Přečtěte si dokumentaci", + "reindexNow": { + "alreadyInProgress": "Reindexování je už spuštěno.", + "label": "Reindexovat Teď", + "confirmTitle": "Potvrdit Reindexaci", + "error": "Chyba spouštění reindexování: {{errorMessage}}", + "desc": "Přeindexování znovu vygeneruje vektorové reprezentace (embeddingy) pro všechny sledované objekty. Tento proces probíhá na pozadí, může plně zatížit váš procesor a v závislosti na počtu sledovaných objektů může trvat delší dobu.", + "confirmDesc": "Opravdu chcete přeindexovat embeddingy všech sledovaných objektů? Tento proces poběží na pozadí, ale může plně vytížit procesor a trvat delší dobu. Průběh můžete sledovat na stránce Prozkoumat.", + "confirmButton": "Reindexovat", + "success": "Reindexování úspěšně spuštěno." + }, + "title": "Sémantické Hledání", + "desc": "Sémantické vyhledávání ve Frigate umožňuje najít sledované objekty ve vašich záznamech pomocí samotného obrázku, uživatelem zadaného textového popisu nebo automaticky generovaného popisu.", + "modelSize": { + "small": { + "title": "malý", + "desc": "Použití malý znamená využití kvantizované verze modelu, která spotřebovává méně paměti RAM a běží rychleji na procesoru, přičemž rozdíl v kvalitě embeddingů je zanedbatelný." + }, + "large": { + "title": "velký", + "desc": "Volba velký využívá plný model Jina a automaticky se provádí na GPU, pokud je k dispozici." + }, + "label": "Velikost Modelu", + "desc": "Velikost modelu pro sémantické vyhledávání pomocí embeddingů." + } + }, + "title": "Nastavení Klasifikací", + "birdClassification": { + "title": "Klasifikace Ptáka", + "desc": "Klasifikace ptáků rozpoznává známé druhy pomocí kvantizovaného modelu TensorFlow. Když je rozpoznán známý pták, jeho běžný název bude přidán jako podružný štítek (sub_label). Tato informace se zobrazí v uživatelském rozhraní, je dostupná ve filtrech a zahrnuta i v notifikacích." + }, + "toast": { + "error": "Chyba při ukládání změn konfigurace: {{errorMessage}}", + "success": "Nastavení klasifikací uloženo. Restartujte Frigate pro aplikování změn." + }, + "restart_required": "Vyžadován restart (Nastavení klasifikací se změnilo)", + "licensePlateRecognition": { + "title": "Rozpoznávání SPZ", + "desc": "Frigate dokáže rozpoznávat SPZ na vozidlech a automaticky přidávat rozpoznané znaky do pole recognized_license_plate nebo známý název jako podružný štítek k objektům typu auto. Běžným případem použití je čtení SPZ aut vjíždějících na příjezdovou cestu nebo projíždějících kolem na ulici.", + "readTheDocumentation": "Přečtěte si dokumentaci" + }, + "faceRecognition": { + "title": "Rozpoznávání obličeje", + "readTheDocumentation": "Přečtěte si dokumentaci", + "desc": "Rozpoznávání obličeje umožňuje přiřadit lidem jména, a když je jejich obličej rozpoznán, Frigate přiřadí dané jméno jako podružný štítek (sub_label). Tato informace se zobrazuje v uživatelském rozhraní, je dostupná ve filtrech a je také součástí notifikací.", + "modelSize": { + "label": "Velikost Modelu", + "desc": "Velikost modelu použitého pro rozpoznávání obličeje.", + "small": { + "title": "malý", + "desc": "Použití malý znamená využití FaceNet modelu pro embedding obličejů, který běží efektivně na většině procesorů (CPU)." + }, + "large": { + "title": "velký", + "desc": "Použití velký znamená využití modelu ArcFace pro embedding obličejů, který se v případě dostupnosti automaticky spustí na GPU." + } + } + } + }, + "masksAndZones": { + "zones": { + "speedEstimation": { + "docs": "Přečtěte si dokumentaci", + "title": "Odhad rychlosti", + "desc": "Povolit odhad rychlosti pro objekty v této zóně. Zóna musí mít přesně 4 body.", + "lineADistance": "Vzdálenost linky A ({{unit}})", + "lineBDistance": "Vzdálenost linky B ({{unit}})", + "lineCDistance": "Vzdálenost linky C ({{unit}})", + "lineDDistance": "Vzdálenost linky D ({{unit}})" + }, + "name": { + "inputPlaceHolder": "Zadejte jméno…", + "title": "Jméno", + "tips": "Název musí mít alespoň 2 znaky, musí obsahovat alespoň jedno písmeno a nesmí být shodný s názvem kamery nebo jiné zóny této kamery." + }, + "inertia": { + "title": "Setrvačnost", + "desc": "Určuje, po kolika snímcích strávených v zóně je objekt považován za přítomný v této zóně.Výchozí hodnota: 3" + }, + "loiteringTime": { + "title": "Doba setrvání", + "desc": "Nastavuje minimální dobu v sekundách, po kterou musí být objekt v zóně, aby došlo k aktivaci.Výchozí hodnota: 0" + }, + "objects": { + "title": "Objekty", + "desc": "Seznam objektů, na které se tato zóna vztahuje." + }, + "allObjects": "Všechny Objekty", + "speedThreshold": { + "title": "Práh rychlosti ({{unit}})", + "desc": "Určuje minimální rychlost, při které jsou objekty v této zóně zohledněny.", + "toast": { + "error": { + "pointLengthError": "Odhad rychlosti byl pro tuto zónu deaktivován. Zóny s odhadem rychlosti musí mít přesně 4 body.", + "loiteringTimeError": "Pokud má zóna nastavenou dobu setrvání větší než 0, nedoporučuje se používat odhad rychlosti." + } + } + }, + "toast": { + "success": "Zóna {{zoneName}} byla uložena." + }, + "label": "Zóny", + "desc": { + "title": "Zóny umožňují definovat konkrétní oblast v záběru, díky čemuž lze určit, zda se objekt nachází v dané oblasti či nikoliv.", + "documentation": "Dokumentace" + }, + "add": "Přidat Zónu", + "edit": "Upravit Zónu", + "documentTitle": "Upravit Zónu - Frigate", + "clickDrawPolygon": "Klikněte pro kreslení polygonu na obrázku.", + "point_one": "{{count}} bod", + "point_few": "{{count}} body", + "point_other": "{{count}} bodů" + }, + "motionMasks": { + "context": { + "documentation": "Přečtěte si dokumentaci", + "title": "Masky detekce pohybu slouží k zabránění tomu, aby nežádoucí typy pohybu spouštěly detekci (například větve stromů nebo časové značky kamery). Masky detekce pohybu by se měly používat velmi střídmě – příliš rozsáhlé maskování může ztížit sledování objektů." + }, + "polygonAreaTooLarge": { + "documentation": "Přečtěte si dokumentaci", + "title": "Maska detekce pohybu pokrývá {{polygonArea}}% záběru kamery. Příliš velké masky detekce pohybu nejsou doporučovány.", + "tips": "Masky detekce pohybu nebrání detekci objektů. Místo toho byste měli použít požadovanou zónu." + }, + "documentTitle": "Editovat Masku Detekce pohybu - Frigate", + "desc": { + "title": "Masky detekce pohybu slouží k zabránění nežádoucím typům pohybu ve spuštění detekce. Příliš rozsáhlé maskování však může ztížit sledování objektů.", + "documentation": "Dokumentace" + }, + "label": "Maska Detekce pohybu", + "add": "Nová Maska Detekce pohybu", + "edit": "Upravit Masku Detekce pohybu", + "point_one": "{{count}} bod", + "point_few": "{{count}} body", + "point_other": "{{count}} bodů", + "clickDrawPolygon": "Kliknutím nakreslíte polygon do obrázku.", + "toast": { + "success": { + "title": "{{polygonName}} byl uložen.", + "noName": "Maska Detekce pohybu byla uložena." + } + } + }, + "filter": { + "all": "Všechny Masky a Zóny" + }, + "restart_required": "Vyžadován restart (masky/zóny byly změněny)", + "toast": { + "error": { + "copyCoordinatesFailed": "Nemohu zkopírovat souřadnice do schránky." + }, + "success": { + "copyCoordinates": "Souřadnice pro {{polyName}} zkopírovány do schránky." + } + }, + "form": { + "zoneName": { + "error": { + "hasIllegalCharacter": "Název zóny obsahuje zakázané znaky.", + "mustNotBeSameWithCamera": "Název Zóny nesmí být stejný jako název kamery.", + "mustNotContainPeriod": "Název zóny nesmí obsahovat tečky.", + "alreadyExists": "Zóna se stejným názvem u této kamery již existuje.", + "mustBeAtLeastTwoCharacters": "Název Zóny musí mít minimálně 2 znaky." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Setrvačnost musí být větší než 0." + } + }, + "polygonDrawing": { + "snapPoints": { + "true": "Přichytávat body", + "false": "Nepřichytávat body" + }, + "delete": { + "title": "Potvrdit Smazání", + "desc": "Opravdu chcete smazat {{type}}{{name}}?", + "success": "{{name}} bylo smazáno." + }, + "error": { + "mustBeFinished": "Kreslení polygonu musí být před uložením dokončeno." + }, + "reset": { + "label": "Vymazat všechny body" + }, + "removeLastPoint": "Odebrat poslední bod" + }, + "distance": { + "error": { + "mustBeFilled": "Pro použití odhadu rychlosti musí být vyplněna všechna pole pro vzdálenost.", + "text": "Vzdálenost musí být větší nebo rovna 0.1." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Doba setrvání musí být větší nebo rovna nule." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Prahová hodnota rychlosti musí být větší nebo rovna 0,1." + } + } + }, + "objectMasks": { + "label": "Masky Objektu", + "documentTitle": "Upravit Masku Objektu - Frigate", + "desc": { + "documentation": "Dokumentace", + "title": "Masky filtrování objektů slouží k odfiltrování falešných detekcí daného typu objektu na základě jeho umístění." + }, + "add": "Přidat Masku Objektu", + "edit": "Upravit Masku Objektu", + "objects": { + "title": "Objekty", + "allObjectTypes": "Všechny typy objektů", + "desc": "Typ objektu, na který se tato maska objektu vztahuje." + }, + "context": "Masky filtrování objektů slouží k odfiltrování falešných poplachů konkrétního typu objektu na základě jeho umístění.", + "clickDrawPolygon": "Kliknutím nakreslete polygon do obrázku.", + "toast": { + "success": { + "title": "{{polygonName}} byl uložen.", + "noName": "Maska Objektu byla uložena." + } + }, + "point_one": "{{count}} bod", + "point_few": "{{count}} body", + "point_other": "{{count}} bodů" + }, + "motionMaskLabel": "Maska Detekce pohybu {{number}}", + "objectMaskLabel": "Maska Objektu {{number}} {{label}}" + }, + "menu": { + "ui": "Uživatelské rozhraní", + "classification": "Klasifikace", + "cameras": "Nastavení kamery", + "masksAndZones": "Masky / Zóny", + "motionTuner": "Ladění detekce pohybu", + "debug": "Ladění", + "users": "Uživatelé", + "notifications": "Notifikace", + "frigateplus": "Frigate+", + "enrichments": "Obohacení", + "triggers": "Spouštěče", + "cameraManagement": "Správa", + "cameraReview": "Kontrola", + "roles": "Role" + }, + "dialog": { + "unsavedChanges": { + "title": "Máte neuložené změny.", + "desc": "Přejete si uložit změny před pokračováním?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Žádná Kamera" + }, + "general": { + "title": "Nastavení rozhraní", + "liveDashboard": { + "title": "Živý dashboard", + "automaticLiveView": { + "desc": "Při detekci aktivity se automaticky přepne na živý náhled kamery. Vypnutí této možnosti způsobí, že se statické snímky z kamery na ovládacím panelu Live aktualizují pouze jednou za minutu.", + "label": "Automatický živý náhled" + }, + "playAlertVideos": { + "label": "Přehrát videa s výstrahou", + "desc": "Ve výchozím nastavení se nedávná upozornění na ovládacím panelu Živě přehrávají jako malá opakující se videa. Vypněte tuto možnost, chcete-li na tomto zařízení/prohlížeči zobrazovat pouze statický obrázek nedávných výstrah." + }, + "displayCameraNames": { + "label": "Vždy zobrazit názvy kamer", + "desc": "Vždy zobrazit názvy kamer v čipu na ovládacím panelu živého náhledu s více kamerami." + }, + "liveFallbackTimeout": { + "label": "Časový limit pádu živého přehrávání" + } + }, + "storedLayouts": { + "title": "Uložené rozložení", + "desc": "Rozložení kamer ve skupině kamer lze přetáhnout nebo jim změnit velikost. Pozice jsou uloženy v místním úložišti vašeho prohlížeče.", + "clearAll": "Smazat všechna rozložení" + }, + "cameraGroupStreaming": { + "title": "Nastavení streamování skupiny kamer", + "desc": "Nastavení streamování pro každou kameru je uloženo v místním uložišti vašeho prohlížeče.", + "clearAll": "Vymazat všechna nastavení streamování" + }, + "recordingsViewer": { + "title": "Prohlížeč nahrávek", + "defaultPlaybackRate": { + "label": "Výchozí rychlost přehrávání", + "desc": "Výchozí rychlost přehrávání pro nahrávky." + } + }, + "calendar": { + "title": "Kalendář", + "firstWeekday": { + "label": "První den týdne", + "desc": "Den, kterým bude začínat týden v kalendáři kontrol.", + "sunday": "Neděle", + "monday": "Pondělí" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Vymazáno uložené rozvržení pro kameru {{cameraName}}", + "clearStreamingSettings": "Vymazány nastavení streamování pro všechny skupiny kamer." + }, + "error": { + "clearStoredLayoutFailed": "Chyba mazání uloženého rozvržení: {{errorMessage}}", + "clearStreamingSettingsFailed": "Chyba mazání nastavení streamování: {{errorMessage}}" + } + } + }, + "debug": { + "timestamp": { + "title": "Časové razítko", + "desc": "Překrýt obrázek časovým razítkem" + }, + "regions": { + "title": "Regiony", + "desc": "Zobrazit rámeček oblasti zájmu odesílané detektoru objektů", + "tips": "

    Boxy oblastí zájmu


    Jasně zelené boxy budou překryty na oblastech zájmu ve snímku, které jsou odesílány detektoru objektů.

    " + }, + "title": "Ladění", + "detectorDesc": "Frigate používá vaše detektory {{detectors}} k detekci objektů ve streamu vašich kamer.", + "objectList": "Seznam objektů", + "boundingBoxes": { + "title": "Ohraničující rámečky", + "desc": "Zobrazit ohraničující rámečky okolo sledovaných objektů", + "colors": { + "label": "Barvy Ohraničujících Rámečků Objektů", + "info": "
  • Při spuštění bude každému objektovému štítku přiřazena jiná barva.
  • Tenká tmavě modrá čára označuje, že objekt není v daném okamžiku detekován.
  • Tenká šedá čára znamená, že objekt je detekován jako nehybný.
  • Silná čára označuje, že objekt je aktuálně sledován pomocí automatického sledování (pokud je aktivováno).
  • " + } + }, + "zones": { + "title": "Zóny", + "desc": "Zobrazit obrys všech definovaných zón" + }, + "mask": { + "title": "Masky detekce pohybu", + "desc": "Zobrazit polygony masek detekce pohybu" + }, + "debugging": "Ladění", + "desc": "Ladicí zobrazení ukazuje sledované objekty a jejich statistiky v reálném čase. Seznam objektů zobrazuje časově zpožděný přehled detekovaných objektů.", + "motion": { + "title": "Rámečky detekce pohybu", + "desc": "Zobrazit rámečky okolo oblastí, kde byl detekován pohyb", + "tips": "

    Boxy pohybu


    Červené boxy budou překryty na místech snímku, kde je právě detekován pohyb.

    " + }, + "noObjects": "Žádné objekty", + "objectShapeFilterDrawing": { + "title": "Vykreslení filtru tvaru objektu", + "desc": "Nakreslete na obrázek obdélník pro zobrazení informací o ploše a poměru stran", + "tips": "Povolte tuto možnost pro nakreslení obdélníku na obraz kamery, který zobrazí jeho plochu a poměr stran. Tyto hodnoty pak můžete použít pro nastavení parametrů tvarového filtru objektu ve vaší konfiguraci.", + "document": "Přečtěte si dokumentaci ", + "score": "Skóre", + "ratio": "Poměr", + "area": "Oblast" + }, + "openCameraWebUI": "Otevřít webové rozhraní {{camera}}", + "audio": { + "title": "Zvuk", + "noAudioDetections": "Žádné detekce zvuku", + "score": "skóre", + "currentRMS": "Aktuální RMS", + "currentdbFS": "Aktuální dbFS" + }, + "paths": { + "title": "Cesty", + "desc": "Zobrazit významné body trasy sledovaného objektu", + "tips": "

    Cesty


    Čáry a kruhy označují významné body, kterými se sledovaný objekt během svého životního cyklu pohyboval.

    " + } + }, + "camera": { + "streams": { + "title": "Streamy", + "desc": "Dočasně zakáže kameru dokud Frigate nerestartuje. Deaktivace kamery zcela zastaví zpracování jejích streamů ve Frigate. Detekce, nahrávání ani ladění nebudou dostupné.
    Poznámka: Tímto nedojde k deaktivaci restreamů v go2rtc." + }, + "review": { + "desc": "Dočasně povolte nebo zakažte upozornění a detekce pro tuto kameru dokud Frigate nerestartuje. Pokud jsou vypnuté, nebudou se vytvářet žádné nové položky k přezkoumání. ", + "detections": "Detekce ", + "title": "Revize", + "alerts": "Výstrahy " + }, + "reviewClassification": { + "readTheDocumentation": "Přečtěte si dokumentaci", + "desc": "Frigate rozděluje položky k přezkoumání na Upozornění a Detekce. Ve výchozím nastavení jsou všechny objekty typu osoba a auto považovány za Upozornění. Kategorizaci těchto položek můžete upřesnit nastavením požadovaných zón.", + "title": "Přehled klasifikace", + "objectAlertsTips": "Všechny {{alertsLabels}} objekty na {{cameraName}} budou zobrazeny ve Výstrahách.", + "objectDetectionsTips": "Všechny objekty typu {{detectionsLabels}}, které nejsou na kameře {{cameraName}} zařazeny do kategorie, budou zobrazeny jako Detekce bez ohledu na to, ve které zóně se nacházejí.", + "zoneObjectDetectionsTips": { + "notSelectDetections": "Všechny objekty typu {{detectionsLabels}} detekované v zóně {{zone}} na kameře {{cameraName}}, které nejsou zařazeny jako Upozornění, budou zobrazeny jako Detekce – bez ohledu na to, ve které zóně se nacházejí.", + "text": "Všechny objekty typu {{detectionsLabels}}, které nejsou v zóně {{zone}} na kameře {{cameraName}} zařazeny do kategorie, budou zobrazeny jako Detekce.", + "regardlessOfZoneObjectDetectionsTips": "Všechny objekty {{detectionsLabels}}, které nejsou na kameře {{cameraName}} zařazeny do žádné kategorie, budou zobrazeny jako Detekce – bez ohledu na zónu, ve které se nacházejí." + }, + "noDefinedZones": "Nejsou nastaveny žádné zóny pro tuto kameru.", + "zoneObjectAlertsTips": "Všechny {{alertsLabels}} objekty detekované v {{zone}} na {{cameraName}} budou zobrazeny ve Výstrahách.", + "unsavedChanges": "Neuložené nastavení revize klasifikace pro {{camera}}", + "selectAlertsZones": "Vybrat zóny pro Výstrahy", + "selectDetectionsZones": "Vybrat zóny pro Detekce", + "toast": { + "success": "Konfigurace Revizí Klasifikací byla uložena. Restartujte Frigate pro aplikování změn." + }, + "limitDetections": "Omezit detekce pro specifické zóny" + }, + "title": "Nastavení Kamery", + "object_descriptions": { + "title": "AI generované popisy objektů", + "desc": "Dočasně povolit/zakázat generativní popisy objektů AI pro tuto kameru. Pokud je tato funkce zakázána, nebudou pro sledované objekty na této kameře vyžadovány popisy generované AI." + }, + "review_descriptions": { + "title": "Popisy generativní AI", + "desc": "Dočasně povolit/zakázat generativní AI recenze popisů pro tuto kameru. Pokud je tato funkce zakázána, nebudou pro položky recenzí na této kameře vyžadovány popisy generované AI." + }, + "addCamera": "Přidat novou kameru", + "editCamera": "Upravit kameru:", + "selectCamera": "Vybrat kameru", + "backToSettings": "Zpět k nastavení kamery", + "cameraConfig": { + "add": "Přidat kameru", + "edit": "Upravit kameru", + "description": "Konfigurovat nastavení kamery, včetně vstupů streamu a rolí.", + "name": "Název kamery", + "nameRequired": "Název kamery je povinný", + "nameLength": "Název kamery musí mít méně než 24 znaků.", + "namePlaceholder": "např. přední dveře", + "enabled": "Povolit", + "ffmpeg": { + "inputs": "Vstupní streamy", + "path": "Cesta streamu", + "pathRequired": "Cesta ke streamu je povinná", + "pathPlaceholder": "rtsp://...", + "roles": "Role", + "rolesRequired": "Je vyžadována alespoň jedna role", + "rolesUnique": "Každá role (audio, detekce, záznam) může být přiřazena pouze k jednomu streamu", + "addInput": "Přidat vstupní stream", + "removeInput": "Odebrat vstupní stream", + "inputsRequired": "Je vyžadován alespoň jeden vstupní stream" + }, + "toast": { + "success": "Kamera {{cameraName}} byla úspěšně uložena" + } + } + }, + "notification": { + "notificationSettings": { + "documentation": "Přečtěte si dokumentaci", + "title": "Nastavení notifikací", + "desc": "Frigate může nativně odesílat push notifikace do vašeho zařízení, pokud běží v prohlížeči nebo je nainstalován jako PWA (progresivní webová aplikace)." + }, + "notificationUnavailable": { + "documentation": "Přečtěte si dokumentaci", + "title": "Notifikace Nedostupné", + "desc": "Webové push notifikace vyžadují zabezpečený kontext (https://…). Jedná se o omezení prohlížeče. Pro použití notifikací přistupujte k Frigate přes zabezpečené připojení." + }, + "cameras": { + "title": "Kamery", + "desc": "Vyberte kamery, pro které chcete povolit notifikace.", + "noCameras": "Žádné dostupné kamery" + }, + "title": "Notifikace", + "email": { + "placeholder": "např. example@email.com", + "title": "Email", + "desc": "Je vyžadována platná e-mailová adresa, která bude použita k upozornění v případě problémů se službou push notifikací." + }, + "registerDevice": "Registrovat Toto Zařízení", + "deviceSpecific": "Nastavení specifická pro zařízení", + "unregisterDevice": "Odregistrovat Toto Zařízení", + "sendTestNotification": "Poslat testovací notifikaci", + "unsavedRegistrations": "Neuložené přihlášky k Notifikacím", + "unsavedChanges": "Neuložené změny Notifikací", + "globalSettings": { + "desc": "Dočasně pozastavit notifikace pro vybrané kamery na všech registrovaných zařízeních.", + "title": "Globální nastavení" + }, + "active": "Notifikace Aktivní", + "suspendTime": { + "suspend": "Pozastavit", + "12hours": "Pozastavit na 12 hodin", + "24hours": "Pozastavit na 24 hodin", + "untilRestart": "Pozastavit do restartu", + "5minutes": "Pozastavit na 5 minut", + "10minutes": "Pozastavit na 10 minut", + "30minutes": "Pozastavit na 30 minut", + "1hour": "Pozastavit na 1 hodinu" + }, + "toast": { + "error": { + "registerFailed": "Nepodařilo se uložit registraci notifikací." + }, + "success": { + "registered": "Notifikace byly úspěšně zaregistrovány. Pro odesílání notifikací (včetně testovací) je nutné Frigate restartovat.", + "settingSaved": "Nastavení notifikací bylo uloženo." + } + }, + "cancelSuspension": "Zrušit Pozastavení", + "suspended": "Notifikace pozastaveny {{time}}" + }, + "users": { + "dialog": { + "form": { + "password": { + "strength": { + "weak": "Slabé", + "medium": "Střední", + "strong": "Silné", + "veryStrong": "Velmi Silné", + "title": "Síla hesla: " + }, + "match": "Hesla souhlasí", + "notMatch": "Hesla nesouhlasí", + "title": "Heslo", + "placeholder": "Vložit heslo", + "confirm": { + "title": "Potvrdit heslo", + "placeholder": "Potvrdit heslo" + } + }, + "newPassword": { + "placeholder": "Vložte nové heslo", + "title": "Nové Heslo", + "confirm": { + "placeholder": "Zopakujte nové heslo" + } + }, + "usernameIsRequired": "Uživatelské jméno je nutné", + "user": { + "title": "Uživatelské jméno", + "desc": "Povolena jsou pouze písmena, čísla, tečky a podtržítka.", + "placeholder": "Vložte uživatelské jméno" + }, + "passwordIsRequired": "Je vyžadováno heslo" + }, + "createUser": { + "title": "Vytvořit nového uživatele", + "desc": "Přidejte nový uživatelský účet a zadejte roli pro určení přístupu k jednotlivým částem uživatelského rozhraní Frigate.", + "usernameOnlyInclude": "Uživatelské jméno smí obsahovat pouze písmena, čísla, . nebo _", + "confirmPassword": "Potvrďte prosím heslo" + }, + "deleteUser": { + "title": "Smazat Uživatele", + "desc": "Tuto akci nelze vrátit zpět. Uživatelský účet bude trvale smazán a veškerá s ním spojená data budou odstraněna.", + "warn": "Opravdu chcete smazat {{username}}?" + }, + "changeRole": { + "roleInfo": { + "intro": "Vyberte odpovídající roli pro tohoto uživatele:", + "admin": "Správce", + "adminDesc": "Plný přístup ke všem funkcím.", + "viewer": "Divák", + "viewerDesc": "Omezení pouze na Živé dashboardy, Revize, Průzkumníka a Exporty.", + "customDesc": "Vlastní role s konkrétním přístupem ke kameře." + }, + "title": "Změnit Roli Uživatele", + "desc": "Aktualizovat oprávnění pro {{username}}", + "select": "Vyberte roli" + }, + "passwordSetting": { + "updatePassword": "Aktualizovat heslo pro uživatele {{username}}", + "setPassword": "Nastavit Heslo", + "desc": "Vytvořte silné heslo pro zabezpečení tohoto účtu.", + "cannotBeEmpty": "Heslo nemůže být prázdné", + "doNotMatch": "Hesla nesouhlasí" + } + }, + "table": { + "username": "Uživatelské jméno", + "actions": "Akce", + "noUsers": "Žádní uživatelé nebyli nalezeni.", + "changeRole": "Změnit roli uživatele", + "password": "Resetovat Heslo", + "deleteUser": "Smazat uživatele", + "role": "Role" + }, + "updatePassword": "Resetovat heslo", + "toast": { + "success": { + "createUser": "Uživatel {{user}} úspěšně vytvořen", + "deleteUser": "Uživatel {{user}} úspěšně odebrán", + "updatePassword": "Heslo úspěšně aktualizováno.", + "roleUpdated": "Role pro {{user}} aktualizována" + }, + "error": { + "setPasswordFailed": "Chyba uložení hesla: {{errorMessage}}", + "createUserFailed": "Chyba vytvoření uživatele: {{errorMessage}}", + "deleteUserFailed": "Chyba při mazání uživatele: {{errorMessage}}", + "roleUpdateFailed": "Chyba při aktualizaci role: {{errorMessage}}" + } + }, + "management": { + "desc": "Spravujte uživatelské účty této instance Frigate.", + "title": "Správa uživatelů" + }, + "addUser": "Přidat uživatele", + "title": "Uživatelé" + }, + "motionDetectionTuner": { + "unsavedChanges": "Neuložené změny ladění detekce pohybu {{camera}}", + "improveContrast": { + "title": "Zlepšit Kontrast", + "desc": "Zlepšit kontrast pro tmavé scény Výchozí: ON" + }, + "toast": { + "success": "Nastavení detekce pohybu bylo uloženo." + }, + "title": "Ladění detekce pohybu", + "desc": { + "documentation": "Přečtěte si příručku Ladění detekce pohybu", + "title": "Frigate používá detekci pohybu jako první kontrolu k ověření, zda se ve snímku děje něco, co stojí za další analýzu pomocí detekce objektů." + }, + "Threshold": { + "title": "Práh", + "desc": "Prahová hodnota určuje, jak velká změna jasu pixelu je nutná, aby byl považován za pohyb. Výchozí: 30" + }, + "contourArea": { + "title": "Obrysová Oblast", + "desc": "Hodnota plochy obrysu se používá k rozhodnutí, které skupiny změněných pixelů se kvalifikují jako pohyb. Výchozí: 10" + } + }, + "enrichments": { + "title": "Nastavení obohacení", + "faceRecognition": { + "title": "Rozpoznání obličeje", + "desc": "Rozpoznávání obličeje umožňuje přiřadit lidem jména a po rozpoznání jejich obličeje. Frigate přiřadí jméno osoby jako podštítek. Tyto informace jsou zahrnuty v uživatelském rozhraní, filtrech a také v oznámeních.", + "readTheDocumentation": "Přečtěte si Dokumentaci", + "modelSize": { + "label": "Velikost Modelu", + "desc": "Velikost modelu použitého pro rozpoznání obličeje.", + "small": { + "title": "malý", + "desc": "Použití metody malý využívá model vkládání obličejů FaceNet, který efektivně běží na většině procesorů." + }, + "large": { + "title": "velký", + "desc": "Použití metody velký využívá model vkládání obličejů ArcFace a v případě potřeby se automaticky spustí na GPU." + } + } + }, + "semanticSearch": { + "reindexNow": { + "confirmDesc": "Jste si jisti, že chcete znovu indexovat všechny vložené sledované objekty? Tento proces poběží na pozadí, ale může maximálně zatížit váš procesor a trvat poměrně dlouho. Průběh můžete sledovat na stránce Prozkoumat.", + "confirmTitle": "Potvrdit Reindexování", + "label": "Přeindexovat nyní", + "desc": "Reindexování regeneruje vložení pro všechny sledované objekty. Tento proces běží na pozadí a může maximálně zatížit váš procesor a trvat poměrně dlouho v závislosti na počtu sledovaných objektů.", + "confirmButton": "Přeindexovat", + "success": "Přeindexování úspěšně spuštěno.", + "alreadyInProgress": "Přeindexování je již spuštěno.", + "error": "Chyba spuštění přeindexování: {{errorMessage}}" + }, + "title": "Sémantické vyhledávání", + "desc": "Sémantické vyhledávání ve Frigate umožňuje najít sledované objekty v rámci vašich zkontrolovaných položek pomocí samotného obrázku, uživatelem definovaného textového popisu nebo automaticky generovaného popisu.", + "readTheDocumentation": "Přečtěte si Dokumentaci", + "modelSize": { + "label": "Velikost modelu", + "desc": "Velikost modelu použitého pro vkládání sémantického vyhledávání.", + "small": { + "title": "malý", + "desc": "Použitím malého modelu se využívá kvantizovaná verze modelu, která spotřebovává méně RAM a běží rychleji na CPU s velmi zanedbatelným rozdílem v kvalitě vkládání." + }, + "large": { + "title": "velký", + "desc": "Použití parametru velký využívá celý model Jina a v případě potřeby se automaticky spustí na GPU." + } + } + }, + "birdClassification": { + "desc": "Klasifikace ptáků identifikuje známé ptáky pomocí kvantovaného modelu Tensorflow. Po rozpoznání známého ptáka se jeho běžný název přidá jako sub_label. Tato informace je zahrnuta v uživatelském rozhraní, filtrech a také v oznámeních.", + "title": "Klasifikace ptáků" + }, + "unsavedChanges": "Neuložené změny nastavení Obohacení", + "licensePlateRecognition": { + "title": "Rozpoznání SPZ", + "desc": "Frigate dokáže rozpoznávat SPZ vozidel a automaticky přidávat detekované znaky do pole recognized_license_plate nebo název známé SPZ jako sub_label k objektům typu auto. Běžným případem použití může být čtení SPZ aut vjíždějících na příjezdovou cestu nebo aut projíždějících po ulici.", + "readTheDocumentation": "Přečtěte si Dokumentaci" + }, + "restart_required": "Nutný restart (nastavení Obohacení změněno)", + "toast": { + "success": "Nastavení Obohacení uloženo. Restartujte Frigate aby se změny aplikovaly.", + "error": "Chyba ukládání změn konfigurace: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Spouštěče", + "management": { + "title": "Spouštěče", + "desc": "Spravovat spouštěče pro {{camera}}. Použít typ miniatury ke spuštění u miniatur podobných vybranému sledovanému objektu a typ popisu ke spuštění u popisů podobných zadanému textu." + }, + "addTrigger": "Přidat spouštěč", + "table": { + "name": "Jméno", + "type": "Typ", + "content": "Obsah", + "threshold": "Prahová hodnota", + "actions": "Akce", + "noTriggers": "Pro tuto kameru nejsou nakonfigurovány žádné spouštěče.", + "edit": "Upravit", + "deleteTrigger": "Smazat spouštěč", + "lastTriggered": "Naposledy spuštěno" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Popis" + }, + "actions": { + "alert": "Označit jako upozornění", + "notification": "Odeslat oznámení" + }, + "dialog": { + "createTrigger": { + "title": "Vytvořit spouštěč", + "desc": "Vytvořit spouštěč pro kameru {{camera}}" + }, + "editTrigger": { + "title": "Upravit spouštěč", + "desc": "Upravit nastavení spouštěče na kameře {{camera}}" + }, + "deleteTrigger": { + "title": "Odstranit spouštěč", + "desc": "Opravdu chcete odstranit spouštěč {{triggerName}}? Tuto akci nelze vrátit zpět." + }, + "form": { + "name": { + "title": "Název", + "placeholder": "Pojmenujte tento spouštěč", + "error": { + "minLength": "Pole musí mít alespoň 2 znaky.", + "invalidCharacters": "Pole může obsahovat pouze písmena, číslice, podtržítka a pomlčky.", + "alreadyExists": "Spouštěč s tímto názvem již pro tuto kameru existuje." + } + }, + "enabled": { + "description": "Povolit nebo zakázat tento spouštěč" + }, + "type": { + "title": "Typ", + "placeholder": "Vybrat typ spouštěče" + }, + "content": { + "title": "Obsah", + "imagePlaceholder": "Vyberte miniaturu", + "textPlaceholder": "Zadat textový obsah", + "imageDesc": "Je zobrazeno pouze posledních 100 miniatur. Pokud nemůžete najít požadovanou miniaturu, prosím zkontrolujte dřívější objekty v Prozkoumat a nastavte spouštěč ze tamějšího menu.", + "textDesc": "Zadejte text, který spustí tuto akci, když bude zjištěn podobný popis sledovaného objektu.", + "error": { + "required": "Obsah je povinný." + } + }, + "actions": { + "title": "Akce", + "desc": "Ve výchozím nastavení Frigate odesílá MQTT zprávu pro všechny spouštěče. Podřazené popisky přidávají název spouštěče k popisku objektu. Atributy jsou prohledávatelná metadata uložená samostatně v metadatech sledovaného objektu.", + "error": { + "min": "Musí být vybrána alespoň jedna akce." + } + }, + "threshold": { + "title": "Práh", + "error": { + "min": "Práh musí být alespoň 0", + "max": "Práh musí být nanejvýš 1" + } + } + } + }, + "toast": { + "success": { + "createTrigger": "Spouštěč {{name}} byl úspěšně vytvořen.", + "updateTrigger": "Spouštěč {{name}} byl úspěšně aktualizován.", + "deleteTrigger": "Spouštěč {{name}} byl úspěšně smazán." + }, + "error": { + "createTriggerFailed": "Nepodařilo se vytvořit spouštěč: {{errorMessage}}", + "updateTriggerFailed": "Nepodařilo se aktualizovat spouštěč: {{errorMessage}}", + "deleteTriggerFailed": "Nepodařilo se smazat spouštěč: {{errorMessage}}" + } + } + }, + "roles": { + "addRole": "Přidat roli", + "table": { + "role": "Role", + "cameras": "Kamery", + "actions": "Akce", + "noRoles": "Nebyly nalezeny žádné vlastní role.", + "editCameras": "Upravit kamery", + "deleteRole": "Smazat roli" + }, + "toast": { + "success": { + "createRole": "Role {{role}} byla úspěšně vytvořena", + "updateCameras": "Kamery byly aktualizovány pro roli {{role}}", + "deleteRole": "Role {{role}} byla úspěšně smazána", + "userRolesUpdated_one": "{{count}} uživatel přiřazený k této roli byl aktualizován na „diváka“, který má přístup ke všem kamerám.", + "userRolesUpdated_few": "{{count}} uživatelé přiřazení k této roli bylo aktualizováno na „diváky“, kteří mají přístup ke všem kamerám.", + "userRolesUpdated_other": "{{count}} uživatelů přiřazených k této roli bylo aktualizováno na „diváky“, kteří mají přístup ke všem kamerám." + }, + "error": { + "createRoleFailed": "Nepodařilo se vytvořit roli: {{errorMessage}}", + "updateCamerasFailed": "Nepodařilo se aktualizovat kamery: {{errorMessage}}", + "deleteRoleFailed": "Nepodařilo se smazat roli: {{errorMessage}}", + "userUpdateFailed": "Nepodařilo se aktualizovat role uživatele: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Vytvořit novou roli", + "desc": "Přidejte novou roli a určete oprávnění k přístupu ke kamerám." + }, + "deleteRole": { + "title": "Smazat roli", + "warn": "Opravdu chcete smazat roli {{role}}?", + "deleting": "Mazání...", + "desc": "Tuto akci nelze vrátit zpět. Role bude trvale smazána a všichni uživatelé s touto rolí budou přeřazeni do role „Divák“, která poskytne přístup ke všem kamerám." + }, + "form": { + "role": { + "title": "Název role", + "placeholder": "Zadejte název role", + "desc": "Povolena jsou pouze písmena, čísla, tečky a podtržítka.", + "roleIsRequired": "Název role je povinný", + "roleOnlyInclude": "Název role smí obsahovat pouze písmena, čísla, . nebo _", + "roleExists": "Role s tímto názvem již existuje." + }, + "cameras": { + "title": "Kamery", + "desc": "Vyberte kamery, ke kterým má tato role přístup. Je vyžadována alespoň jedna kamera.", + "required": "Musí být vybrána alespoň jedna kamera." + } + }, + "editCameras": { + "desc": "Aktualizujte přístup ke kamerám pro roli {{role}}.", + "title": "Upravit kamery role" + } + }, + "management": { + "title": "Správa role diváka", + "desc": "Spravujte vlastní role diváků a jejich oprávnění k přístupu ke kamerám pro tuto instanci Frigate." + } + }, + "cameraWizard": { + "save": { + "success": "Nová kamera {{cameraName}} úspěšně uložena." + }, + "step2": { + "testSuccess": "Test připojení v pořádku!", + "probeSuccessful": "Sonda úspěšná", + "probeNoSuccess": "Sonda neúspěšná" + }, + "step3": { + "testSuccess": "Test streamu v pořádku!" + }, + "step4": { + "reconnectionSuccess": "Opakované připojení úspěšné.", + "streamValidated": "Stream {{number}} úspěšně ověřený" + } + }, + "cameraManagement": { + "cameraConfig": { + "toast": { + "success": "Kamera {{cameraName}} úspěšně uložena" + } + } + }, + "cameraReview": { + "reviewClassification": { + "toast": { + "success": "Konfigurace Klasifikací Revizí byla uložena. Restartujte Frigate pro aplikování změn." + } + } + } +} diff --git a/web/public/locales/cs/views/system.json b/web/public/locales/cs/views/system.json new file mode 100644 index 0000000..b2aa71a --- /dev/null +++ b/web/public/locales/cs/views/system.json @@ -0,0 +1,197 @@ +{ + "cameras": { + "info": { + "stream": "Stream {{idx}}", + "resolution": "Rozlišení:", + "error": "Chyba: {{error}}", + "unknown": "Neznámý", + "fps": "FPS:", + "audio": "Audio:", + "video": "Video:", + "fetching": "Získávám data o kameře", + "codec": "Kodek:", + "tips": { + "title": "Informace o sondování kamery" + }, + "streamDataFromFFPROBE": "Data ze streamu jsou získávána pomocí ffprobe.", + "cameraProbeInfo": "{{camera}} Informace o sondování kamery", + "aspectRatio": "zachovat poměr stran" + }, + "label": { + "camera": "kamera", + "ffmpeg": "FFmpeg", + "cameraFfmpeg": "{{camName}} FFmpeg", + "detect": "detekováno", + "skipped": "přeskočeno", + "capture": "zachyceno", + "overallFramesPerSecond": "celkový počet snímků za sekundu", + "overallDetectionsPerSecond": "celkový počet detekcí za sekundu", + "overallSkippedDetectionsPerSecond": "celkový počet přeskočených detekcí za sekundu", + "cameraCapture": "záznam {{camName}}", + "cameraDetect": "detekce {{camName}}", + "cameraFramesPerSecond": "{{camName}} snímků za sekundu", + "cameraDetectionsPerSecond": "{{camName}} detekcí za sekundu", + "cameraSkippedDetectionsPerSecond": "{{camName}} přeskočených detekcí za sekundu" + }, + "title": "Kamery", + "overview": "Přehled", + "framesAndDetections": "Snímky / Detekce", + "toast": { + "success": { + "copyToClipboard": "Sondovaná data uložena do schránky." + }, + "error": { + "unableToProbeCamera": "Nemohu sondovat kameru: {{errorMessage}}" + } + } + }, + "stats": { + "cameraIsOffline": "{{camera}} je offline", + "healthy": "Systém je zdravý", + "reindexingEmbeddings": "Přeindexování vektorů ({{processed}} % dokončeno)", + "detectIsSlow": "{{detect}} je pomalé ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} je velmi pomalé ({{speed}} ms)", + "detectHighCpuUsage": "{{camera}} má vysoké využití CPU detekcemi ({{detectAvg}} %)", + "ffmpegHighCpuUsage": "{{camera}} má vyské využití CPU FFmpegem ({{ffmpegAvg}}%)", + "shmTooLow": "Alokace /dev/shm ({{total}} MB) by měla být zvýšena alespoň na {{min}} MB." + }, + "enrichments": { + "embeddings": { + "face_recognition_speed": "Rychlost rozpoznávání obličeje", + "plate_recognition_speed": "Rychlost rozpoznávání SPZ", + "plate_recognition": "Rozpoznávání SPZ", + "face_recognition": "Rozpoznávání obličeje", + "image_embedding_speed": "Rychlost vkládání obrázku", + "text_embedding_speed": "Rychlost vkládání textu", + "text_embedding": "Vkládání textu", + "image_embedding": "Vkládání obrázku", + "face_embedding_speed": "Rychlost vkládání obličeje", + "yolov9_plate_detection_speed": "YOLOv9 rychlost detekce SPZ", + "yolov9_plate_detection": "YOLOv9 Detekce SPZ" + }, + "infPerSecond": "Inferencí za sekundu", + "title": "Obohacení" + }, + "general": { + "detector": { + "temperature": "Detekční teplota", + "title": "Detektory", + "inferenceSpeed": "Detekční rychlost", + "memoryUsage": "Detektor využití paměti", + "cpuUsage": "Detektor využití CPU", + "cpuUsageInformation": "CPU používané při přípravě vstupních a výstupních dat do/z detekčních modelů. Tato hodnota neměří využití inferenčních operací, ani v případě použití GPU nebo akcelerátoru." + }, + "hardwareInfo": { + "title": "Informace o hardware", + "gpuInfo": { + "vainfoOutput": { + "processError": "Chyba procesu:", + "returnCode": "Návratový kód: {{code}}", + "processOutput": "Výstup procesu:", + "title": "Výstup Vainfo" + }, + "nvidiaSMIOutput": { + "name": "Jméno: {{name}}", + "title": "Výstup Nvidia SMI", + "driver": "Ovladač: {{driver}}", + "cudaComputerCapability": "Výpočetní schopnost CUDA: {{cuda_compute}}", + "vbios": "Informace o VBios: {{vbios}}" + }, + "copyInfo": { + "label": "Kopírovat informace o GPU" + }, + "toast": { + "success": "Informace o GPU zkopírovány do schránky" + }, + "closeInfo": { + "label": "Zavřít informace o GPU" + } + }, + "npuUsage": "Využití NPU", + "npuMemory": "Paměť NPU", + "gpuUsage": "Využití CPU", + "gpuMemory": "Paměť GPU", + "gpuEncoder": "GPU kodér", + "gpuDecoder": "GPU Dekodér", + "intelGpuWarning": { + "title": "Upozornění Intel GPU Stats", + "message": "Statistiky GPU nedostupné", + "description": "Toto je známá chyba v nástrojích Intel pro hlášení statistik GPU (intel_gpu_top), která selhává a opakovaně vrací využití GPU 0 %, a to i v případech, kdy na (i)GPU správně běží hardwarová akcelerace a detekce objektů. Nejedná se o chybu Frigate. Můžete restartovat hostitele, abyste problém dočasně vyřešili a potvrdili, že GPU funguje správně. Toto neovlivňuje výkon." + } + }, + "otherProcesses": { + "title": "Ostatní procesy", + "processCpuUsage": "Využití CPU procesy", + "processMemoryUsage": "Využití paměti procesy", + "series": { + "go2rtc": "go2rtc", + "recording": "nahrávání", + "review_segment": "revidovat segment", + "embeddings": "vložení" + } + }, + "title": "Hlavní" + }, + "storage": { + "cameraStorage": { + "storageUsed": "Úložiště", + "camera": "Kamera", + "title": "Úložiště kamery", + "unused": { + "title": "Nepoužité", + "tips": "Tato hodnota nemusí přesně reprezentovat volné místo dostupné pro Frigate, pokud máte na disku uloženy další soubory kromě nahrávek Frigate. Frigate nesleduje využití úložiště mimo své nahrávky." + }, + "bandwidth": "Šířka pásma", + "unusedStorageInformation": "Informace o nepoužitém úložišti", + "percentageOfTotalUsed": "Procento celkem" + }, + "recordings": { + "title": "Záznamy", + "earliestRecording": "Nejstarší dostupná nahrávka:", + "tips": "Tato hodnota uvádí celkové využití disku záznamy uloženými v databázi Frigate. Frigate nesleduje využití disku ostatními soubory na vašem disku." + }, + "title": "Úložiště", + "overview": "Přehled", + "shm": { + "title": "přiřazení SHM (sdílené paměti)", + "warning": "Nynější velikost SHM činící {{total}}MB je příliš malá. Zvyšte ji alespoň na {{min_shm}}MB." + } + }, + "lastRefreshed": "Poslední aktualizace: ", + "documentTitle": { + "cameras": "Statistiky kamer – Frigate", + "storage": "Statistiky uložiště - Frigate", + "general": "Obecné statistiky - Frigate", + "enrichments": "Statistiky obohacení - Frigate", + "logs": { + "frigate": "Protokoly Frigate - Frigate", + "go2rtc": "Protokoly Go2RTC - Frigate", + "nginx": "Protokoly Nginx - Frigate" + } + }, + "title": "Systém", + "logs": { + "copy": { + "label": "Kopírovat do schránky", + "success": "Protokoly zkopírovány do schránky", + "error": "Protokoly se nepodařilo zkopírovat do schránky" + }, + "type": { + "label": "Typ", + "message": "Zpráva", + "timestamp": "Časové razítko", + "tag": "Štítek (Tag)" + }, + "download": { + "label": "Stáhnout záznamy" + }, + "tips": "Protokoly jsou streamovány ze serveru", + "toast": { + "error": { + "fetchingLogsFailed": "Chyba při načítání protokolů: {{errorMessage}}", + "whileStreamingLogs": "Chyba při streamování protokolů: {{errorMessage}}" + } + } + }, + "metrics": "Systémové metriky" +} diff --git a/web/public/locales/da/audio.json b/web/public/locales/da/audio.json new file mode 100644 index 0000000..0d73b07 --- /dev/null +++ b/web/public/locales/da/audio.json @@ -0,0 +1,210 @@ +{ + "clip_clop": "Klepanie kopyt", + "neigh": "Revanie", + "cattle": "Hovädzí dobytok", + "moo": "Bučanie", + "cowbell": "Kravský zvonec", + "pig": "Prasa", + "speech": "Tale", + "bicycle": "Cykel", + "car": "Bil", + "bellow": "Under", + "motorcycle": "Motorcykel", + "whispering": "Hvisker", + "bus": "Bus", + "laughter": "Latter", + "train": "Tog", + "boat": "Båd", + "crying": "Græder", + "tambourine": "Tambourin", + "marimba": "Marimba", + "trumpet": "Trumpet", + "trombone": "Trombone", + "violin": "Violin", + "flute": "Fløjte", + "saxophone": "Saxofon", + "clarinet": "Klarinet", + "harp": "Harpe", + "bell": "Klokke", + "harmonica": "Harmonika", + "bagpipes": "Sækkepiber", + "didgeridoo": "Didgeridoo", + "jazz": "Jazz", + "opera": "Opera", + "dubstep": "Dubstep", + "blues": "Blues", + "song": "Sang", + "lullaby": "Vuggevise", + "wind": "Vind", + "thunderstorm": "Tordenvejr", + "thunder": "Torden", + "water": "Vand", + "rain": "Regn", + "raindrop": "Regndråbe", + "waterfall": "Vandfald", + "waves": "Bølger", + "fire": "Ild", + "vehicle": "Køretøj", + "sailboat": "Sejlbåd", + "rowboat": "Robåd", + "motorboat": "Motorbåd", + "ship": "Skib", + "ambulance": "Ambulance", + "helicopter": "Helikopter", + "skateboard": "Skateboard", + "chainsaw": "Motorsav", + "door": "Dør", + "doorbell": "Dørklokke", + "slam": "Smæk", + "knock": "Bank", + "squeak": "Knirke", + "dishes": "Tallerkener", + "cutlery": "Bestik", + "sink": "Håndvask", + "bathtub": "Badekar", + "toothbrush": "Tandbørste", + "zipper": "Lynlås", + "coin": "Mønt", + "scissors": "Saks", + "typewriter": "Skrivemaskine", + "alarm": "Alarm", + "telephone": "Telefon", + "ringtone": "Ringetone", + "siren": "Sirene", + "foghorn": "Tågehorn", + "whistle": "Fløjte", + "clock": "Ur", + "printer": "Printer", + "camera": "Kamera", + "tools": "Værktøj", + "hammer": "Hammer", + "drill": "Boremaskine", + "explosion": "Eksplosion", + "fireworks": "Nytårskrudt", + "babbling": "Pludren", + "yell": "Råb", + "whoop": "Jubel", + "snicker": "Smålatter", + "bird": "Fugl", + "cat": "Kat", + "dog": "Hund", + "horse": "Hest", + "sheep": "Får", + "mouse": "Mus", + "keyboard": "Tastatur", + "blender": "Mixer", + "hair_dryer": "Føntørrer", + "animal": "Dyr", + "bark": "Gø", + "goat": "Gæd", + "sigh": "Suk", + "singing": "Synger", + "choir": "Kor", + "yodeling": "Jodlen", + "chant": "Messe", + "mantra": "Meditationsmantra", + "child_singing": "Barn Synger", + "synthetic_singing": "Syntetisk Sang", + "rapping": "Rapper", + "humming": "Nynner", + "groan": "Støn", + "grunt": "Grynt", + "whistling": "Fløjter", + "breathing": "Vejrtrækning", + "wheeze": "Hæsende vejrtrækning", + "snoring": "Snorker", + "gasp": "Gisp", + "pant": "Anstrengende vejrtrækning", + "snort": "Fnyse", + "cough": "Hoster", + "throat_clearing": "Rømmer sig", + "sneeze": "Nyser", + "sniff": "Snøfter", + "run": "Løb", + "shuffle": "Trække fødderne", + "footsteps": "Fodtrin", + "chewing": "Tygger", + "biting": "Bider", + "gargling": "Gurgler", + "stomach_rumble": "Maverumlen", + "burping": "Bøvser", + "hiccup": "Hikke", + "fart": "Prut", + "hands": "Hænder", + "finger_snapping": "Knipse fingere", + "clapping": "Klapper", + "heartbeat": "Hjertebanken", + "heart_murmur": "Hjertemislyd", + "cheering": "Hujen", + "applause": "Bifald", + "chatter": "Snak", + "crowd": "Forsamling", + "children_playing": "Børn leger", + "pets": "Kæledyr", + "yip": "Jubel", + "howl": "Hyl", + "bow_wow": "Vov vov", + "growling": "Knurren", + "whimper_dog": "Hunde­klynk", + "purr": "Spinde", + "meow": "Meaw", + "hiss": "Hvæser", + "caterwaul": "Kattejammer", + "livestock": "Husdyr", + "oink": "Nøf", + "bleat": "Brægen", + "vibration": "Vibration", + "fowl": "Fjerkræ", + "chicken": "Kylling", + "cluck": "Kagle", + "cock_a_doodle_doo": "Kykeliky", + "turkey": "Kalkun", + "gobble": "Kalkunlyd", + "duck": "And", + "quack": "Rap", + "goose": "Gås", + "honk": "Dyt", + "wild_animals": "Vilde dyr", + "roaring_cats": "Brølende katte", + "roar": "Brøl", + "chirp": "Pip", + "squawk": "Skræppen", + "pigeon": "Due", + "coo": "Kurre", + "crow": "Krage", + "caw": "Kragelyd", + "owl": "Ugle", + "hoot": "Uglehyl", + "flapping_wings": "Vingeslag", + "dogs": "Hunde", + "rats": "Rotter", + "patter": "Dråbelyd", + "insect": "Insekt", + "cricket": "Fårekylling", + "guitar": "Guitar", + "electric_guitar": "Elektrisk Guitar", + "bass_guitar": "Basguitar", + "acoustic_guitar": "Akustisk Guitar", + "steel_guitar": "Stål Guitar", + "tapping": "Tapping på guitar", + "strum": "Slå an", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolin", + "snare_drum": "Lilletromme", + "rimshot": "Kantslag", + "drum_roll": "Trommehvirvel", + "bass_drum": "Stortromme", + "techno": "Techno", + "mosquito": "Myg", + "fly": "Flue", + "buzz": "Summen", + "frog": "Frø", + "croak": "Kvæk", + "snake": "Slange", + "rattle": "Klapren", + "whale_vocalization": "Hvallyde", + "music": "Musik", + "musical_instrument": "Musikinstrument", + "plucked_string_instrument": "Strengeinstrument" +} diff --git a/web/public/locales/da/common.json b/web/public/locales/da/common.json new file mode 100644 index 0000000..9b096ca --- /dev/null +++ b/web/public/locales/da/common.json @@ -0,0 +1,324 @@ +{ + "time": { + "untilForTime": "Indtil {{time}}", + "untilForRestart": "Indtil Frigate genstarter.", + "untilRestart": "Indtil genstart", + "ago": "{{timeAgo}} siden", + "justNow": "Lige nu", + "today": "I dag", + "yesterday": "I går", + "last7": "Sidste 7 dage", + "last14": "Sidste 14 dage", + "last30": "Sidste 30 dage", + "thisWeek": "Denne uge", + "lastWeek": "Sidste uge", + "thisMonth": "Denne måned", + "lastMonth": "Sidste måned", + "5minutes": "5 minutter", + "10minutes": "10 minutter", + "30minutes": "30 minutter", + "1hour": "1 time", + "12hours": "12 timer", + "24hours": "24 timer", + "pm": "pm", + "am": "am", + "year_one": "{{time}} år", + "year_other": "{{time}} år", + "mo": "{{time}}md.", + "month_one": "{{time}} måned", + "month_other": "{{time}} måneder", + "d": "{{time}}d", + "day_one": "{{time}} dag", + "day_other": "{{time}} dage", + "h": "{{time}}t", + "yr": "{{time}}yr", + "hour_one": "{{time}} time", + "hour_other": "{{time}} timer", + "m": "{{time}}m", + "minute_one": "{{time}} minut", + "minute_other": "{{time}} minutter", + "s": "{{time}}s", + "second_one": "{{time}} sekund", + "second_other": "{{time}} sekunder", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d. MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ss", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "never": "Aldrig", + "inProgress": "Under behandling", + "invalidStartTime": "Ugyldig starttid", + "invalidEndTime": "Ugyldig sluttid" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/t" + }, + "length": { + "feet": "fod", + "meters": "meter" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/time", + "mbph": "MB/time", + "gbph": "GB/time" + } + }, + "label": { + "back": "Gå tilbage", + "hide": "Skjul {{item}}", + "show": "Vis {{item}}", + "ID": "ID", + "none": "Ingen", + "all": "Alle", + "other": "Andet" + }, + "button": { + "apply": "Anvend", + "reset": "Nulstil", + "done": "Udført", + "enabled": "Aktiveret", + "enable": "Aktiver", + "disabled": "Deaktiveret", + "disable": "Deaktiver", + "save": "Gem", + "saving": "Gemmer…", + "cancel": "Fortryd", + "close": "Luk", + "copy": "Kopier", + "back": "Tilbage", + "history": "Historik", + "fullscreen": "Fuldskærm", + "exitFullscreen": "Afslut Fuldskærm", + "pictureInPicture": "Billede i billede", + "twoWayTalk": "Samtale", + "cameraAudio": "Kameralyd", + "on": "Til", + "off": "Fra", + "edit": "Rediger", + "copyCoordinates": "Kopier koordinater", + "delete": "Slet", + "yes": "Ja", + "no": "Nej", + "download": "Hent", + "info": "Info", + "suspended": "Sat på pause", + "unsuspended": "Genoptag", + "play": "Afspil", + "unselect": "Fravælg", + "export": "Eksporter", + "deleteNow": "Slet nu", + "next": "Næste", + "continue": "Fortsæt", + "add": "Tilføj", + "applying": "Anvender…", + "undo": "Annuler", + "copiedToClipboard": "Kopieret til udklipsholder", + "modified": "Ændret", + "overridden": "Overskrevet", + "resetToGlobal": "Gendan til global", + "resetToDefault": "Gendan standard", + "saveAll": "Gem alle", + "savingAll": "Gemmer alle…", + "undoAll": "Fortryd alle", + "retry": "Prøv igen" + }, + "menu": { + "system": "System", + "systemMetrics": "Systemstatistik", + "configuration": "Konfiguration", + "systemLogs": "Systemlogfiler", + "settings": "Indstillinger", + "configurationEditor": "Konfigurationsværktøj", + "languages": "Sprog", + "language": { + "en": "English (Engelsk)", + "es": "Español (Spansk)", + "zhCN": "简体中文 (Forsimplet Kinesisk)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Fransk)", + "ar": "العربية (Arabisk)", + "pt": "Português (Portugisisk)", + "ru": "Русский (Russisk)", + "de": "Deutsch (Tysk)", + "ja": "日本語 (Japansk)", + "tr": "Türkçe (Tyrkisk)", + "it": "Italiano (Italiensk)", + "nl": "Nederlands (Hollandsk)", + "sv": "Svenska (Svensk)", + "cs": "Čeština (Tjekkisk)", + "nb": "Norsk Bokmål (Norsk Bokmål)", + "ko": "한국어 (Koreansk)", + "vi": "Tiếng Việt (Vietnamesisk)", + "fa": "فارسی (Persisk)", + "pl": "Polski (Polsk)", + "uk": "Українська (Ukrainsk)", + "he": "עברית (Hebraisk)", + "el": "Ελληνικά (Græsk)", + "ro": "Română (Rumænsk)", + "hu": "Magyar (Ungarsk)", + "fi": "Suomi (Finsk)", + "da": "Dansk (Dansk)", + "sk": "Slovenčina (Slovakisk)", + "yue": "粵語 (Kantonesisk)", + "th": "ไทย (Thai)", + "ca": "Català (Katalansk)", + "withSystem": { + "label": "Brug systemindstillinger for sprog" + }, + "ptBR": "Português brasileiro (Brasiliansk Portugisisk)", + "sr": "Српски (Serbisk)", + "sl": "Slovenščina (Slovensk)", + "lt": "Lietuvių (Litauisk)", + "bg": "Български (Bulgarsk)", + "gl": "Galego (Galisisk)", + "id": "Bahasa Indonesia (Indonesisk)", + "ur": "اردو (Urdu)", + "hr": "Hrvatski (Kroatisk)", + "zhHant": "繁體中文 (Traditionel Kinesisk)", + "bs": "Bosanski (Bosnisk)" + }, + "appearance": "Udseende", + "darkMode": { + "label": "Mørk tilstand", + "light": "Lys", + "dark": "Mørk", + "withSystem": { + "label": "Brug system indstillinger for mørk tilstand" + } + }, + "withSystem": "System", + "theme": { + "label": "Tema", + "blue": "Blå", + "green": "Grøn", + "nord": "Nord", + "red": "Rød", + "highcontrast": "Høj Kontrast", + "default": "Standard" + }, + "help": "Hjælp", + "documentation": { + "title": "Dokumentation", + "label": "Frigate dokumentation" + }, + "restart": "Genstart Frigate", + "live": { + "title": "Direkte", + "allCameras": "Alle kameraer", + "cameras": { + "title": "Kameraer", + "count_one": "{{count}} Kamera", + "count_other": "{{count}} Kameraer" + } + }, + "review": "Gennemse", + "explore": "Udforsk", + "export": "Eksporter", + "uiPlayground": "UI sandkasse", + "faceLibrary": "Ansigtsarkiv", + "user": { + "title": "Bruger", + "account": "Konto", + "current": "Aktiv bruger: {{user}}", + "anonymous": "anonym", + "logout": "Log ud", + "setPassword": "Vælg kodeord" + }, + "classification": "Kategorisering", + "profiles": "Profiler", + "actions": "Handlinger", + "features": "Funktioner", + "chat": "Chat" + }, + "toast": { + "copyUrlToClipboard": "Kopieret URL til udklipsholder.", + "save": { + "title": "Gem", + "error": { + "title": "Ændringer kunne ikke gemmes: {{errorMessage}}", + "noMessage": "Kunne ikke gemme konfigurationsændringer" + }, + "success": "Gemte ændringerne i konfigurationen." + } + }, + "role": { + "title": "Rolle", + "admin": "Admin", + "viewer": "Seer", + "desc": "Admins har fuld adgang til Frigate UI. Viewers er begrænset til at se kameraer, gennemse items, og historik i UI." + }, + "pagination": { + "label": "sideinddeling", + "previous": { + "title": "Forrige", + "label": "Gå til forrige side" + }, + "next": { + "title": "Næste", + "label": "Gå til næste side" + }, + "more": "Flere sider" + }, + "accessDenied": { + "documentTitle": "Adgang nægtet - Frigate", + "title": "Adgang nægtet", + "desc": "Du har ikke rettigheder til at se denne side." + }, + "notFound": { + "documentTitle": "Ikke fundet - Frigate", + "title": "404", + "desc": "Siden blev ikke fundet" + }, + "selectItem": "Vælg {{item}}", + "readTheDocumentation": "Læs dokumentationen", + "list": { + "two": "{{0}} og {{1}}", + "many": "{{items}}, og {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Valgfrit", + "internalID": "Det interne ID som Frigate bruger i konfigurationen og databasen" + }, + "information": { + "pixels": "{{area}}px" + }, + "no_items": "Intet fundet", + "validation_errors": "Valideringsfejl", + "credentialField": { + "savedPlaceholder": "Gemt - efterlad blank for at bevare nuværende" + } +} diff --git a/web/public/locales/da/components/auth.json b/web/public/locales/da/components/auth.json new file mode 100644 index 0000000..9df944d --- /dev/null +++ b/web/public/locales/da/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Brugernavn", + "password": "Kodeord", + "login": "Log ind", + "errors": { + "usernameRequired": "Brugernavn kræves", + "passwordRequired": "Kodeord kræves", + "loginFailed": "Login fejlede", + "unknownError": "Ukendt fejl. Tjek logs.", + "rateLimit": "Grænsen for forespørgsler er overskredet. Prøv igen senere.", + "webUnknownError": "Ukendt fejl. Tjek konsollogs." + }, + "firstTimeLogin": "Forsøger du at logge ind for første gang? Loginoplysningerne står i Frigate-loggene." + } +} diff --git a/web/public/locales/da/components/camera.json b/web/public/locales/da/components/camera.json new file mode 100644 index 0000000..faff7a0 --- /dev/null +++ b/web/public/locales/da/components/camera.json @@ -0,0 +1,90 @@ +{ + "group": { + "label": "Kamera Grupper", + "add": "Tilføj Kameragruppe", + "edit": "Rediger Kamera Gruppe", + "delete": { + "label": "Slet kamera gruppe", + "confirm": { + "title": "Bekræft sletning", + "desc": "Er du sikker på at du vil slette kamera gruppen {{name}}?" + } + }, + "name": { + "label": "Navn", + "placeholder": "Indtast et navn…", + "errorMessage": { + "mustLeastCharacters": "Kameragruppens navn skal være mindst 2 tegn.", + "exists": "Kameragruppenavn findes allerede.", + "nameMustNotPeriod": "Kameragruppenavn må ikke indeholde en periode.", + "invalid": "Ugyldigt kamera gruppenavn." + } + }, + "cameras": { + "label": "Kameraer", + "desc": "Vælg kameraer til denne gruppe." + }, + "icon": "Ikon", + "success": "Kameragruppe ({{name}}) er blevet gemt.", + "camera": { + "birdseye": "Fugleøje", + "setting": { + "label": "Kamera Streaming Indstillinger", + "title": "{{cameraName}} Streaming Indstillinger", + "desc": "Skift indstillingerne for direkte visning af denne kameragruppes dashboard. Disse indstillinger er enheds- og browserspecifikke.", + "audioIsAvailable": "Lyd er tilgængelig for denne stream", + "audioIsUnavailable": "Lyd er ikke tilgængelig for denne strøm", + "audio": { + "tips": { + "title": "Lyd skal komme fra dit kamera og konfigureret i go2rtc til denne stream." + } + }, + "stream": "Stream", + "placeholder": "Vælg en stream", + "streamMethod": { + "label": "Streaming Metode", + "placeholder": "Vælg en streaming metode", + "method": { + "noStreaming": { + "label": "Ingen Streaming", + "desc": "Kamerabilleder vil kun opdatere én gang i minuttet og ingen live streaming vil forekomme." + }, + "smartStreaming": { + "label": "Smart Streaming (anbefalet)", + "desc": "Smart streaming vil opdatere dit kamerabillede én gang i minuttet, når der ikke sker noget, for at spare båndbredde og ressourcer. Når der registreres aktivitet, skifter billedet problemfrit til en live stream." + }, + "continuousStreaming": { + "label": "Kontinuerlig Streaming", + "desc": { + "title": "Kamerabillede vil altid være en live stream, når det er synligt på instrumentbrættet, selv om der ikke registreres nogen aktivitet.", + "warning": "Kontinuerlig streaming kan forårsage højt båndbreddeforbrug og ydelsesproblemer. Brug med omtanke." + } + } + } + }, + "compatibilityMode": { + "label": "Kompatibilitetstilstand", + "desc": "Aktivér kun denne mulighed, hvis kameraets live stream viser farve artefakter og har en diagonal linje på højre side af billedet." + } + } + }, + "showAll": "Vis alle kameragrupper", + "showLess": "Vis mindre", + "editGroups": "Rediger kameragrupper" + }, + "debug": { + "options": { + "label": "Indstillinger", + "title": "Valgmuligheder", + "showOptions": "Vis muligheder", + "hideOptions": "Skjul muligheder" + }, + "boundingBox": "Afgrænsningsfelt", + "timestamp": "Tidsstempel", + "zones": "Zoner", + "mask": "Maske", + "motion": "Bevægelse", + "regions": "Regioner", + "paths": "Stier" + } +} diff --git a/web/public/locales/da/components/dialog.json b/web/public/locales/da/components/dialog.json new file mode 100644 index 0000000..1ab0861 --- /dev/null +++ b/web/public/locales/da/components/dialog.json @@ -0,0 +1,66 @@ +{ + "restart": { + "title": "Er du sikker på, at du vil genstarte Frigate?", + "button": "Genstart", + "restarting": { + "title": "Frigate genstarter", + "button": "Gennemtving genindlæsning nu", + "content": "Denne side genindlæses om {{countdown}} sekunder." + }, + "description": "Dette vil kortvarigt stoppe Frigate under genstart." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Indsend til Frigate+", + "desc": "Objekter på steder, du ønsker at undgå, er ikke falske positiver. Hvis du indsender dem som falske positiver, vil det forvirre modellen." + }, + "review": { + "question": { + "label": "Bekræft denne etiket til Frigate Plus", + "ask_a": "Er dette objekt et {{label}}?", + "ask_an": "Er dette objekt en {{label}}?", + "ask_full": "Er dette objekt en {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Indsendt" + }, + "toast": { + "error": "Fejl under indsendelse til Frigate+. Kontroller venligst din netværksforbindelse og prøv igen." + } + } + }, + "video": { + "viewInHistory": "Se i historik" + } + }, + "export": { + "time": { + "fromTimeline": "Vælg fra tidslinje", + "lastHour_one": "Sidste time", + "lastHour_other": "Sidste {{count}} timer", + "custom": "Tidsinterval", + "start": { + "title": "Starttidspunkt", + "label": "Vælg starttidspunkt" + }, + "end": { + "title": "Sluttidspunkt", + "label": "Vælg sluttidspunkt" + } + }, + "name": { + "placeholder": "Navngiv eksport" + }, + "case": { + "newCaseOption": "Opret ny sag", + "newCaseNamePlaceholder": "Nyt sagsnavn", + "newCaseDescriptionPlaceholder": "Sagsbeskrivelse", + "label": "Sag", + "nonAdminHelp": "En ny sag oprettes til disse eksporter.", + "placeholder": "Vælg en sag" + }, + "select": "Vælg", + "export": "Eksporter" + } +} diff --git a/web/public/locales/da/components/filter.json b/web/public/locales/da/components/filter.json new file mode 100644 index 0000000..a2fbf22 --- /dev/null +++ b/web/public/locales/da/components/filter.json @@ -0,0 +1,50 @@ +{ + "filter": "Filtrer", + "classes": { + "label": "Klasser", + "all": { + "title": "Alle klasser" + }, + "count_one": "{{count}} Klasse", + "count_other": "{{count}} Klasser" + }, + "labels": { + "all": { + "short": "Labels", + "title": "Alle etiketter" + }, + "count_one": "{{count}} Label", + "label": "Etiketter" + }, + "zones": { + "label": "Zoner", + "all": { + "title": "Alle zoner", + "short": "Zoner" + } + }, + "more": "Flere filtre", + "sort": { + "label": "Sortér", + "dateAsc": "Dato (Stigende)", + "dateDesc": "Dato (Faldende)", + "speedAsc": "Anslået hastighed (Stigende)", + "speedDesc": "Anslået hastighed (Faldende)", + "relevance": "Relevans" + }, + "dates": { + "selectPreset": "Vælg en forudindstilling…", + "all": { + "title": "Alle datoer", + "short": "Datoer" + } + }, + "reset": { + "label": "Nulstille filtre til standardværdier" + }, + "timeRange": "Tidsinterval", + "estimatedSpeed": "Anslået hastighed ({{unit}})", + "features": { + "hasVideoClip": "Har et videoklip" + } +} diff --git a/web/public/locales/da/components/icons.json b/web/public/locales/da/components/icons.json new file mode 100644 index 0000000..5d7e374 --- /dev/null +++ b/web/public/locales/da/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Vælg et ikon", + "search": { + "placeholder": "Søg efter et ikon…" + } + } +} diff --git a/web/public/locales/da/components/input.json b/web/public/locales/da/components/input.json new file mode 100644 index 0000000..0a8c897 --- /dev/null +++ b/web/public/locales/da/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Download Video", + "toast": { + "success": "Din video til gennemgang er begyndt at blive downloadet." + } + } + } +} diff --git a/web/public/locales/da/components/player.json b/web/public/locales/da/components/player.json new file mode 100644 index 0000000..9f9676b --- /dev/null +++ b/web/public/locales/da/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Ingen optagelser fundet i det angivne tidsrum", + "noPreviewFound": "Ingen forhåndsvisning fundet", + "cameraDisabled": "Kamera er deaktiveret", + "noPreviewFoundFor": "Ingen forhåndsvisning fundet for {{cameraName}}", + "submitFrigatePlus": { + "title": "Indsend dette billede til Frigate+?", + "submit": "Indsend" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 eller nyere kræves for denne type livestream.", + "streamOffline": { + "title": "Stream offline", + "desc": "Der er ikke modtaget nogen billeder på {{cameraName}}-detect-streamen, tjek fejllogs." + }, + "stats": { + "streamType": { + "title": "Stream type:", + "short": "Type" + }, + "bandwidth": { + "title": "Båndbredde:", + "short": "Båndbredde" + }, + "latency": { + "title": "Latenstid:", + "value": "{{seconds}} sekunder", + "short": { + "title": "Latenstid", + "value": "{{seconds}} sek" + } + }, + "droppedFrames": { + "short": { + "title": "Tabt", + "value": "{{droppedFrames}} billeder" + }, + "title": "Tabte billeder:" + }, + "totalFrames": "Antal billeder i alt:", + "decodedFrames": "Dekodede billeder:", + "droppedFrameRate": "Rate for tabte billeder:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Billede sendt til Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Kunne ikke sende billede til Frigate+" + } + } +} diff --git a/web/public/locales/da/config/cameras.json b/web/public/locales/da/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/da/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/da/config/global.json b/web/public/locales/da/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/da/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/da/config/groups.json b/web/public/locales/da/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/da/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/da/config/validation.json b/web/public/locales/da/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/da/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/da/objects.json b/web/public/locales/da/objects.json new file mode 100644 index 0000000..99275fb --- /dev/null +++ b/web/public/locales/da/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Person", + "bicycle": "Cykel", + "car": "Bil", + "motorcycle": "Motorcykel", + "airplane": "Flyvemaskine", + "bus": "Bus", + "train": "Tog", + "boat": "Båd", + "traffic_light": "Trafiklys", + "vehicle": "Køretøj", + "skateboard": "Skateboard", + "door": "Dør", + "sink": "Håndvask", + "toothbrush": "Tandbørste", + "scissors": "Saks", + "clock": "Ur", + "fire_hydrant": "Brandhane", + "street_sign": "Gadeskilt", + "stop_sign": "Stopskilt", + "parking_meter": "Parkeringsautomat", + "bench": "Bænk", + "bird": "Fugl", + "cat": "Kat", + "dog": "Hund", + "horse": "Hest", + "sheep": "Får", + "cow": "Ko", + "elephant": "Elefant", + "bear": "Bjørn", + "zebra": "Zebra", + "giraffe": "Giraf", + "hat": "Hat", + "backpack": "Rygsæk", + "umbrella": "Paraply", + "shoe": "Sko", + "eye_glasses": "Briller", + "handbag": "Håndtaske", + "tie": "Slips", + "suitcase": "Kuffert", + "frisbee": "Frisbee", + "skis": "Ski", + "snowboard": "Snowboard", + "sports_ball": "Bold", + "kite": "Drage", + "baseball_bat": "Baseball Bat", + "baseball_glove": "Baseball hanske", + "surfboard": "Surfbræt", + "tennis_racket": "Tennis ketcher", + "bottle": "Flaske", + "plate": "Tallerken", + "wine_glass": "Vinglas", + "cup": "Kop", + "fork": "Gaffel", + "knife": "Kniv", + "spoon": "Ske", + "bowl": "Skål", + "banana": "Banan", + "apple": "Æble", + "sandwich": "Sandwich", + "orange": "Appelsin", + "broccoli": "Broccoli", + "carrot": "Gulerod", + "hot_dog": "Hotdog", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Kage", + "chair": "Stol", + "couch": "Sofa", + "potted_plant": "Potteplante", + "bed": "Seng", + "mirror": "Spejl", + "dining_table": "Spisebord", + "window": "Vindue", + "desk": "Bord", + "toilet": "Toilet", + "tv": "Fjernsyn", + "laptop": "Bærebar computer", + "mouse": "Mus", + "remote": "Fjernbetjening", + "keyboard": "Tastatur", + "cell_phone": "Mobiltelefon", + "microwave": "Mikrobølgeovn", + "oven": "Ovn", + "toaster": "Brødrister", + "refrigerator": "Køleskab", + "blender": "Mixer", + "book": "Bog", + "vase": "Vase", + "teddy_bear": "Bamse", + "hair_dryer": "Føntørrer", + "hair_brush": "Hårbørste", + "squirrel": "Egern", + "deer": "Hjort", + "animal": "Dyr", + "bark": "Gø", + "fox": "Ræv", + "goat": "Gæd", + "rabbit": "Kanin", + "raccoon": "Vaskebjørn", + "robot_lawnmower": "Robotplæneklipper", + "waste_bin": "Affaldsspand", + "on_demand": "Manuel optagelse", + "face": "Ansigt", + "license_plate": "Nummerplade", + "package": "Pakke", + "bbq_grill": "Grill", + "amazon": "Amazon levering", + "usps": "USPS levering", + "ups": "UPS levering", + "fedex": "FedEx levering", + "dhl": "DHL levering", + "an_post": "An Post levering", + "purolator": "Purolator levering", + "postnl": "PostNL levering", + "nzpost": "NZPost levering", + "postnord": "PostNord levering", + "gls": "GLS levering", + "dpd": "DPD levering" +} diff --git a/web/public/locales/da/views/chat.json b/web/public/locales/da/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/da/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/da/views/classificationModel.json b/web/public/locales/da/views/classificationModel.json new file mode 100644 index 0000000..25d1704 --- /dev/null +++ b/web/public/locales/da/views/classificationModel.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Kategoriseringsmodeller - Frigate", + "details": { + "scoreInfo": "Scoren viser den gennemsnitlige sikkerhed for kategoriseringen på tværs af alle registreringer af dette objekt.", + "unknown": "Ukendt", + "none": "Ingen" + }, + "description": { + "invalidName": "Ugyldigt navn. Navne må kun indeholde bogstaver, tal, mellemrum, apostroffer, understregninger og bindestreger." + }, + "button": { + "deleteClassificationAttempts": "Slet kategoriseringsbilleder", + "renameCategory": "Omdøb klasse", + "deleteCategory": "Slet klasse", + "deleteImages": "Slet billeder", + "trainModel": "Træn model", + "addClassification": "Tilføj Kategori", + "deleteModels": "Slet modeller", + "editModel": "Rediger model" + }, + "tooltip": { + "trainingInProgress": "Modellen er ved at blive trænet", + "noNewImages": "Der er ingen nye billeder at lære af. Kategorisér flere billeder i datasættet først.", + "noChanges": "Ingen ændringer i datasættet siden sidste træning.", + "modelNotReady": "Modellen er ikke klar til træning" + }, + "toast": { + "success": { + "deletedCategory_one": "Slettet kategori", + "deletedCategory_other": "", + "deletedImage_one": "Slettede billeder", + "deletedImage_other": "", + "deletedModel_one": "{{count}} model er nu slettet", + "deletedModel_other": "{{count}} modeller er nu slettet", + "categorizedImage": "Billedet er nu kategoriseret", + "trainedModel": "Modellen er klar.", + "trainingModel": "Modeltræning er started.", + "updatedModel": "Modellens indstillinger er opdateret", + "renamedCategory": "Kategorien er omdøbt til {{name}}" + }, + "error": { + "deleteImageFailed": "Fejl under sletning: {{errorMessage}}", + "deleteCategoryFailed": "Sletning af kategori fejlede: {{errorMessage}}", + "deleteModelFailed": "Sletning af model fejlede: {{errorMessage}}", + "categorizeFailed": "Kategorisering af billedet fejlede: {{errorMessage}}", + "trainingFailed": "Træning af modellen fejlede. Check Frigate loggen.", + "trainingFailedToStart": "Opstart af modeltræning fejlede: {{errorMessage}}", + "updateModelFailed": "Ændring af modellen fejlede: {{errorMessage}}", + "renameCategoryFailed": "Kan ikke omdøbe kategorien: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Slet kategori", + "desc": "Er du sikker på at du vil slette kategorien {{name}}? Dette kan ikke fortrydes og sletter alle tilhørende billeder samt træning af modellen.", + "minClassesTitle": "Kan ikke slette Kategori", + "minClassesDesc": "Modellen skal have mindst 2 kategorier. Tilføj en kategori, før du sletter denne." + }, + "deleteModel": { + "title": "Slet Kategoriseringsmodellen", + "desc_one": "Er du sikker på, at du vil slette {{count}} model? Dette vil permanent slette alle tilknyttede data, inkl. billeder og træningsdata. Denne handling kan ikke fortrydes.", + "desc_other": "Er du sikker på, at du vil slette {{count}} modeller? Dette vil permanent slette alle tilknyttede data, inkl. billeder og træningsdata. Denne handling kan ikke fortrydes.", + "single": "Er du sikker på, at du vil slette {{name}}? Dette vil permanent slette alle tilknyttede data, inklusive billeder og træningsdata. Denne handling kan ikke fortrydes." + }, + "train": { + "title": "Nyeste kategorier", + "titleShort": "Nyeste", + "aria": "Vælg de nyeste kategorier" + }, + "categories": "Kategorier", + "createCategory": { + "new": "Opret en ny kategori" + }, + "categorizeImageAs": "Kategoriser billedet som:", + "categorizeImage": "Kategoriser billedet", + "menu": { + "objects": "Genstande", + "states": "Statestik" + }, + "noModels": { + "object": { + "title": "Ingen kategoriseringsmodeller for genstande", + "description": "Opret en model, der kan kategorisere genstande.", + "buttonText": "Opret Genstands Model" + }, + "state": { + "title": "Ingen modeller til genstandstilstande", + "description": "Opret en brugerdefineret model til at overvåge og kategorisere tilstandsændringer i specifikke kamerområder.", + "buttonText": "Opret tilstandsmodel" + } + }, + "wizard": { + "step1": { + "type": "Type", + "typeState": "Tilstand", + "typeObject": "Genstand", + "objectLabel": "Genstands mærkat", + "objectLabelPlaceholder": "Vælg genstands type...", + "classificationType": "Kategoriseringstype", + "classificationTypeTip": "Udforsk kategoriseringstyper", + "errors": { + "nameLength": "Modellens navn må højst være 64 tegn", + "nameOnlyNumbers": "Modellens navn skal indeholde bogstaver", + "classRequired": "Der mangler en kategori", + "classesUnique": "Kategorinavne skal være unikke", + "noneNotAllowed": "Kategorinavnet 'none' er ikke tilladt", + "stateRequiresTwoClasses": "Tilstandsmodeller har brug for 2 kategorier", + "objectLabelRequired": "Vælg genstands mærkat", + "objectTypeRequired": "Vælg kategoriseringstype", + "nameRequired": "Modelnavn er påkrævet" + }, + "description": "Tilstandsmodeller overvåger faste kameraområder for ændringer (f.eks. dør åben/lukket). Genstandsmodeller tilføjer kategoriseringer til detekterede genstande (f.eks. kendte dyr, leveringspersoner osv.).", + "name": "Navn", + "namePlaceholder": "Skriv modelnavn...", + "classificationTypeDesc": "Underetiketter tilføjer ekstra tekst til genstandens etiket (f.eks. 'Person: UPS'). Attributter er søgbare metadata, der opbevares separat i genstandens metadata.", + "classificationSubLabel": "Underetiketter", + "classificationAttribute": "Attribut", + "classes": "Kategori", + "states": "Tilstande", + "classesTip": "Lær om kategorier", + "classesStateDesc": "Definér de forskellige tilstande, dit kameraområde kan være i. For eksempel: 'åben' og 'lukket' for en garageport.", + "classesObjectDesc": "Definér de forskellige kategorier, som detekterede genstande skal kategoriseres i. For eksempel: 'leveringsperson', 'beboer', 'fremmed' til kategorisering af personer.", + "classPlaceholder": "Skriv kategorinavn..." + }, + "step2": { + "description": "Vælg kameraer, og definer det område, der skal overvåges for hvert kamera. Modellen vil kategorisere tilstanden i disse områder.", + "cameras": "Kameraer", + "selectCamera": "Vælg Kamera", + "noCameras": "Klik + for at tilføje kamera", + "selectCameraPrompt": "Vælg et kamera fra listen for at definere dets overvågningsområde" + }, + "step3": { + "selectImagesPrompt": "Vælg alle billeder med: {{className}}", + "selectImagesDescription": "Klik på billederne for at vælge dem. Klik på Fortsæt, når du er færdig med denne kategori.", + "allImagesRequired_one": "Venligst kategoriser alle billeder. {{count}} billede tilbage.", + "allImagesRequired_other": "Venligst kategoriser alle billeder. {{count}} billeder tilbage.", + "generating": { + "title": "Genererer testbilleder", + "description": "Frigate henter repræsentative billeder fra dine optagelser. Det kan tage et øjeblik..." + }, + "training": { + "title": "Træningsmodel", + "description": "Din model trænes i baggrunden. Luk denne dialog, og din model vil begynde at køre, så snart træningen er færdig." + }, + "retryGenerate": "Forsøg at generere igen", + "noImages": "Ingen prøvebilleder blev genereret", + "classifying": "Kategoriserer og træner...", + "trainingStarted": "Træningen er startet", + "modelCreated": "Model er oprettet. Brug visningen af nylige kategoriseringer til at tilføje billeder for de manglende tilstande, og træn modellen derefter.", + "errors": { + "noCameras": "Ingen kamera konfigureret", + "noObjectLabel": "Ingen genstandsmærkat valgt", + "generateFailed": "Kunne ikke generere eksempler: {{error}}", + "generationFailed": "Der opstod en fejl under genereringen. Prøv igen.", + "classifyFailed": "Kunne ikke kategorisere billederne: {{error}}" + }, + "generateSuccess": "Eksempelbilleder er nu genereret", + "missingStatesWarning": { + "title": "Manglende tilstandseksempler", + "description": "Det anbefales at vælge eksempler for alle tilstande for at opnå de bedste resultater. Du kan fortsætte uden at vælge alle tilstande, men modellen bliver ikke trænet, før alle tilstande har billeder. Efter du fortsætter, kan du bruge visningen Seneste kategoriseringer til at kategorisere billeder for de manglende tilstande og derefter træne modellen." + } + }, + "title": "Opret ny kategorisering", + "steps": { + "nameAndDefine": "Navn og definition", + "stateArea": "Tilstandsområde", + "chooseExamples": "Vælg Eksempler" + } + }, + "edit": { + "title": "Rediger kategoriseringsmodel", + "descriptionState": "Rediger kategorierne for denne model til genstandstilstande. Ændringer kræver, at modellen trænes igen.", + "descriptionObject": "Rediger genstandstypen og kategoriseringstypen for denne genstandskategoriseringsmodel.", + "stateClassesInfo": "Bemærk: Ændring af tilstandskategorier kræver, at modellen trænes igen med de opdaterede kategorier." + }, + "deleteDatasetImages": { + "title": "Slet billeder i datasættet", + "desc_one": "Er du sikker på, at du vil slette {{count}} billede fra {{dataset}}? Denne handling kan ikke fortrydes og kræver, at modellen trænes igen.", + "desc_other": "Er du sikker på, at du vil slette {{count}} billeder fra {{dataset}}? Denne handling kan ikke fortrydes og kræver, at modellen trænes igen." + }, + "deleteTrainImages": { + "title": "Slet trænings billeder", + "desc_one": "Er du sikker på, at du vil slette {{count}} billede? Denne handling kan ikke fortrydes.", + "desc_other": "Er du sikker på, at du vil slette {{count}} billeder? Denne handling kan ikke fortrydes." + }, + "renameCategory": { + "title": "Omdøb Kategori", + "desc": "Indtast et nyt navn til {{name}}. Modellen skal trænes igen, før navneændringen træder i kraft." + } +} diff --git a/web/public/locales/da/views/configEditor.json b/web/public/locales/da/views/configEditor.json new file mode 100644 index 0000000..606479d --- /dev/null +++ b/web/public/locales/da/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Konfigurationsstyring - Frigate", + "copyConfig": "Kopiér konfiguration", + "saveAndRestart": "Gem & Genstart", + "saveOnly": "Kun gem", + "configEditor": "Konfigurationsværktøj", + "safeConfigEditor": "Konfigurationsværktøj (Sikker tilstand)", + "safeModeDescription": "Frigate er i sikker tilstand på grund af valideringsfejl af konfigurationen.", + "confirm": "Afslut uden at gemme?", + "toast": { + "success": { + "copyToClipboard": "Konfigurationen er kopieret." + }, + "error": { + "savingError": "Kan ikke gemme konfigurationen" + } + } +} diff --git a/web/public/locales/da/views/events.json b/web/public/locales/da/views/events.json new file mode 100644 index 0000000..6b07e52 --- /dev/null +++ b/web/public/locales/da/views/events.json @@ -0,0 +1,32 @@ +{ + "alerts": "Alarmer", + "detections": "Detekteringer", + "motion": { + "label": "Bevægelse", + "only": "Kun bevægelse" + }, + "allCameras": "Alle kameraer", + "timeline": "Tidslinje", + "camera": "Kamera", + "empty": { + "alert": "Der er ingen advarsler at gennemgå", + "detection": "Der er ingen registreringer at gennemgå", + "motion": "Ingen bevægelsesdata fundet", + "recordingsDisabled": { + "title": "Optagelser skal være aktiveret" + } + }, + "documentTitle": "Gennemse - Frigate", + "recordings": { + "documentTitle": "Optagelser - Frigate" + }, + "calendarFilter": { + "last24Hours": "Sidste 24 timer" + }, + "markAsReviewed": "Marker som gennemset", + "markTheseItemsAsReviewed": "Marker disse som gennemset", + "detail": { + "aria": "Skift til detaljevisning" + }, + "timeline.aria": "Vælg tidslinje" +} diff --git a/web/public/locales/da/views/explore.json b/web/public/locales/da/views/explore.json new file mode 100644 index 0000000..fc0a72f --- /dev/null +++ b/web/public/locales/da/views/explore.json @@ -0,0 +1,34 @@ +{ + "documentTitle": "Udforsk - Frigate", + "generativeAI": "Generativ AI", + "type": { + "details": "detaljer", + "video": "video" + }, + "objectLifecycle": { + "lifecycleItemDesc": { + "active": "{{label}} blev aktiv" + } + }, + "exploreIsUnavailable": { + "embeddingsReindexing": { + "startingUp": "Starter…", + "estimatedTime": "Estimeret tid tilbage:", + "context": "Udforsk kan bruges, når genindekseringen af de sporede objektindlejringer er fuldført.", + "finishingShortly": "Afsluttes om lidt", + "step": { + "thumbnailsEmbedded": "Miniaturer indlejret: ", + "descriptionsEmbedded": "Beskrivelser indlejrede: ", + "trackedObjectsProcessed": "Sporede objekter behandlede: " + } + }, + "title": "Udforsk er ikke tilgængelig", + "downloadingModels": { + "context": "Frigate henter de nødvendige indlejringsmodeller for at understøtte semantiske søgninger. Dette kan tage flere minutter, afhængig af hastigheden på din netværksforbindelse." + } + }, + "exploreMore": "Udforsk flere {{label}}-objekter", + "details": { + "timestamp": "Tidsstempel" + } +} diff --git a/web/public/locales/da/views/exports.json b/web/public/locales/da/views/exports.json new file mode 100644 index 0000000..6a821b3 --- /dev/null +++ b/web/public/locales/da/views/exports.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Eksporter - Frigate", + "search": "Søg", + "deleteExport.desc": "Er du sikker på at du vil slette {{exportName}}?", + "editExport": { + "title": "Omdøb Eksport", + "saveExport": "Gem Eksport", + "desc": "Indtast et nyt navn for denne eksport." + }, + "noExports": "Ingen eksporter fundet", + "deleteExport": "Slet eksport", + "tooltip": { + "shareExport": "Del eksport", + "downloadVideo": "Download video", + "editName": "Rediger navn", + "deleteExport": "Slette eksport" + } +} diff --git a/web/public/locales/da/views/faceLibrary.json b/web/public/locales/da/views/faceLibrary.json new file mode 100644 index 0000000..53644bc --- /dev/null +++ b/web/public/locales/da/views/faceLibrary.json @@ -0,0 +1,93 @@ +{ + "selectItem": "Vælg {{item}}", + "description": { + "addFace": "Tilføj en ny samling til ansigtsbiblioteket ved at uploade dit første billede.", + "placeholder": "Angiv et navn for bibliotek", + "invalidName": "Ugyldigt navn. Navne må kun indeholde bogstaver, tal, mellemrum, apostroffer, understregninger og bindestreger.", + "nameCannotContainHash": "Navet kan ikke indeholde #." + }, + "details": { + "person": "Person", + "timestamp": "Tidsstempel", + "unknown": "Ukendt", + "scoreInfo": "Scoren er et vægtet gennemsnit af alle ansigtsscorer, vægtet efter ansigtets størrelse på hvert billede." + }, + "documentTitle": "Ansigtsbibliotek - Frigate", + "uploadFaceImage": { + "title": "Upload ansigtsbillede", + "desc": "Upload et billede for at scanne efter ansigter og inkludere det for {{pageToggle}}" + }, + "train": { + "titleShort": "Nyeste", + "title": "Seneste genkendelser", + "aria": "Vælg seneste genkendelser", + "empty": "Der er ingen nylige ansigtsgenkendelser" + }, + "createFaceLibrary": { + "new": "Nyt ansigt", + "nextSteps": "
    • Brug fanen Seneste genkendelser til at udvælge og træne på billeder for hver registreret person.
    • Fokusér på billeder taget lige forfra for de bedste resultater; undgå træningsbilleder, hvor ansigter er fotograferet fra siden eller i vinkel.
    " + }, + "steps": { + "faceName": "Skriv ansigt navn", + "uploadFace": "Upload ansigt billede", + "nextSteps": "Næste skridt", + "description": { + "uploadFace": "Upload et billede af {{name}}, hvor ansigtet er set forfra. Billedet behøver ikke kun at vise ansigtet og skal ikke beskæres." + } + }, + "button": { + "deleteFace": "Slet ansigt", + "deleteFaceAttempts": "Slet ansigter", + "addFace": "Tilføj ansigt", + "renameFace": "Omdøb ansigt", + "uploadImage": "Upload billede", + "reprocessFace": "Genbehandl ansigt" + }, + "trainFace": "Lær ansigt", + "renameFace": { + "title": "Omdøb ansigt", + "desc": "Indtast et nyt navn til {{name}}" + }, + "toast": { + "success": { + "deletedFace_one": "{{count}} ansigt blev slettet", + "deletedFace_other": "{{count}} ansigter blev slettet", + "deletedName_one": "{{count}} ansigt slettet", + "deletedName_other": "{{count}} ansigter slettet", + "uploadedImage": "Billedet blev uploadet.", + "addFaceLibrary": "{{name}} er blevet tilføjet til ansigtsbiblioteket!", + "renamedFace": "Ansigtet er blevet omdøbt til {{name}}", + "trainedFace": "Ansigtet er blevet trænet.", + "updatedFaceScore": "Ansigtets score er blevet opdateret til {{score}} ({{name}})." + }, + "error": { + "uploadingImageFailed": "Kunne ikke uploade billedet: {{errorMessage}}", + "addFaceLibraryFailed": "Kunne ikke angive navn på ansigtet: {{errorMessage}}", + "deleteFaceFailed": "Kunne ikke slette: {{errorMessage}}", + "deleteNameFailed": "Kunne ikke slette navnet: {{errorMessage}}", + "renameFaceFailed": "Kunne ikke omdøbe ansigtet: {{errorMessage}}", + "trainFailed": "Kunne ikke træne: {{errorMessage}}", + "updateFaceScoreFailed": "Kunne ikke opdatere ansigtets score: {{errorMessage}}" + } + }, + "deleteFaceAttempts": { + "desc_one": "Er du sikker på, at du vil slette {{count}} ansigt? Denne handling kan ikke fortrydes.", + "desc_other": "Er du sikker på, at du vil slette {{count}} ansigter? Denne handling kan ikke fortrydes.", + "title": "Slet ansigter" + }, + "collections": "Samlinger", + "deleteFaceLibrary": { + "title": "Slet navn", + "desc": "Er du sikker på, at du vil slette samlingen {{name}}? Dette vil permanent slette alle tilknyttede ansigter." + }, + "imageEntry": { + "maxSize": "Maks. størrelse: {{size}} MB", + "validation": { + "selectImage": "Vælg venligst en billedfil." + }, + "dropActive": "Slip billedet her…", + "dropInstructions": "Træk og slip eller indsæt et billede her – eller klik for at vælge" + }, + "nofaces": "Ingen tilgængelige ansigter", + "trainFaceAs": "Træn ansigt som:" +} diff --git a/web/public/locales/da/views/live.json b/web/public/locales/da/views/live.json new file mode 100644 index 0000000..6de5619 --- /dev/null +++ b/web/public/locales/da/views/live.json @@ -0,0 +1,117 @@ +{ + "documentTitle": "Live - Frigate", + "documentTitle.withCamera": "{{camera}} - Live - Frigate", + "twoWayTalk": { + "enable": "Aktivér tovejskommunikation", + "disable": "Deaktiver tovejskommunikation" + }, + "cameraAudio": { + "enable": "Aktivér kameralyd", + "disable": "Deaktivér kamera lyd" + }, + "lowBandwidthMode": "Lavbåndbredde-tilstand", + "ptz": { + "move": { + "clickMove": { + "label": "Klik i billedrammen for at centrere kameraet", + "enable": "Aktivér klik for at flytte", + "disable": "Deaktiver klik for at flytte" + }, + "left": { + "label": "Flyt PTZ-kameraet til venstre" + }, + "up": { + "label": "Flyt PTZ kamera op" + }, + "down": { + "label": "Flyt PTZ-kameraet ned" + }, + "right": { + "label": "Flyt PTZ-kameraet til højre" + } + }, + "zoom": { + "in": { + "label": "Zoom PTZ-kamera ind" + }, + "out": { + "label": "Zoom PTZ kamera ud" + } + }, + "focus": { + "in": { + "label": "Focus PTZ kamera ind" + }, + "out": { + "label": "Focus PTZ kamera ud" + } + }, + "frame": { + "center": { + "label": "Klik på billedet for at centrere PTZ-kameraet" + } + }, + "presets": "PTZ kamera forudindstillinger" + }, + "camera": { + "enable": "Aktivér kamera", + "disable": "Deaktivér kamera" + }, + "muteCameras": { + "enable": "Slå lyd på alle kameraer fra", + "disable": "Slå lyd på alle kameraer til" + }, + "detect": { + "enable": "Aktiver detektering", + "disable": "Deaktiver detektering" + }, + "recording": { + "enable": "Aktivér optagelse", + "disable": "Deaktiver optagelse" + }, + "snapshots": { + "enable": "Aktivér Snapshots", + "disable": "Deaktivér Snapshots" + }, + "snapshot": { + "takeSnapshot": "Hent instant snapshot", + "noVideoSource": "Ingen videokilde til snapshot.", + "captureFailed": "Kunne ikke tage snapshot.", + "downloadStarted": "Hentning af snapshot startet." + }, + "audioDetect": { + "enable": "Aktiver lyddetektor", + "disable": "Deaktiver lyddetektor" + }, + "transcription": { + "enable": "Aktiver Live Audio Transkription", + "disable": "Deaktiver Live Audio Transkription" + }, + "autotracking": { + "enable": "Aktiver Autotracking", + "disable": "Deaktiver Autotracking" + }, + "streamStats": { + "enable": "Vis Stream statistik", + "disable": "Skjul Stream statistik" + }, + "manualRecording": { + "title": "Manuel optagelse", + "tips": "Hent et øjebliksbillede eller start en manuel begivenhed baseret på dette kameras indstillinger for optagelse af opbevaring.", + "playInBackground": { + "label": "Afspil i baggrunden", + "desc": "Aktiver denne mulighed for at fortsætte streaming, når afspilleren er skjult." + }, + "showStats": { + "label": "Vis statistik", + "desc": "Aktiver denne mulighed for at vise streamstatistikker som en overlejring på kameraets feed." + }, + "debugView": "Debug View", + "start": "Start on-demand optagelse", + "started": "Start manuel optagelse.", + "failedToStart": "Manuel optagelse fejlede.", + "recordDisabledTips": "Da optagelsen er deaktiveret eller begrænset i konfig for dette kamera, gemmes der kun et snapshot.", + "end": "Afslut manuel optagelse", + "ended": "Afsluttet manuel optagelse." + } +} diff --git a/web/public/locales/da/views/motionSearch.json b/web/public/locales/da/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/da/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/da/views/recording.json b/web/public/locales/da/views/recording.json new file mode 100644 index 0000000..acfdecb --- /dev/null +++ b/web/public/locales/da/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtrer", + "export": "Eksporter", + "calendar": "Kalender", + "filters": "Filtere", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Sluttidspunkt skal være efter starttidspunkt", + "noValidTimeSelected": "Intet gyldigt tidsinterval valgt" + } + } +} diff --git a/web/public/locales/da/views/replay.json b/web/public/locales/da/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/da/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/da/views/search.json b/web/public/locales/da/views/search.json new file mode 100644 index 0000000..693032c --- /dev/null +++ b/web/public/locales/da/views/search.json @@ -0,0 +1,19 @@ +{ + "search": "Søg", + "savedSearches": "Gemte Søgninger", + "searchFor": "Søg efter {{inputValue}}", + "button": { + "save": "Gem søgning", + "delete": "Slet gemt søgning", + "filterInformation": "Filter information", + "filterActive": "Filtre aktiv", + "clear": "Ryd søgning" + }, + "trackedObjectId": "Sporet genstands-ID", + "filter": { + "label": { + "cameras": "Kameraer", + "zones": "Områder" + } + } +} diff --git a/web/public/locales/da/views/settings.json b/web/public/locales/da/views/settings.json new file mode 100644 index 0000000..816d0d6 --- /dev/null +++ b/web/public/locales/da/views/settings.json @@ -0,0 +1,20 @@ +{ + "documentTitle": { + "default": "Indstillinger - Frigate", + "authentication": "Bruger Indstillinger - Frigate", + "camera": "Kamera indstillinger - Frigate", + "object": "Debug - Frigate", + "cameraManagement": "Administrér kameraer - Frigate", + "cameraReview": "Indstillinger for kameragennemgang - Frigate", + "enrichments": "Indstillinger for berigelser - Frigate", + "masksAndZones": "Maske- og zoneeditor - Frigate", + "motionTuner": "Bevægelsesjustering - Frigate", + "general": "Brugergrænsefladeindstillinger - Frigate", + "frigatePlus": "Frigate+ Indstillinger - Frigate", + "notifications": "Notifikations indstillinger - Frigate" + }, + "menu": { + "ui": "Brugergrænseflade", + "profiles": "Profiler" + } +} diff --git a/web/public/locales/da/views/system.json b/web/public/locales/da/views/system.json new file mode 100644 index 0000000..31d7ac9 --- /dev/null +++ b/web/public/locales/da/views/system.json @@ -0,0 +1,103 @@ +{ + "documentTitle": { + "cameras": "Kamera Statistik - Frigate", + "storage": "Lagrings Statistik - Frigate", + "logs": { + "frigate": "Frigate Logs - Frigate", + "go2rtc": "Go2RTC Logs - Frigate", + "nginx": "Nginx Logs - Frigate" + }, + "general": "Generelle statistikker - Frigate", + "enrichments": "Beredningsstatistikker - Frigate" + }, + "title": "System", + "logs": { + "copy": { + "label": "Kopier til udklipsholder", + "success": "Logs er kopieret til udklipsholder", + "error": "Kunne ikke kopiere logs til udklipsholder" + }, + "type": { + "label": "Type", + "timestamp": "Tidsstempel", + "message": "Besked", + "tag": "Tag" + }, + "tips": "Logs bliver streamet fra serveren", + "toast": { + "error": { + "fetchingLogsFailed": "Fejl ved indhentning af logs: {{errorMessage}}", + "whileStreamingLogs": "Fejl ved streaming af logs: {{errorMessage}}" + } + }, + "download": { + "label": "Download logs" + } + }, + "general": { + "title": "Generelt", + "hardwareInfo": { + "gpuUsage": "GPU forbrug", + "gpuMemory": "GPU hukommelse", + "gpuEncoder": "GPU indkoder", + "gpuDecoder": "GPU afkoder", + "title": "Hardware information", + "gpuInfo": { + "closeInfo": { + "label": "Luk GPU information" + }, + "copyInfo": { + "label": "Kopier GPU information" + }, + "toast": { + "success": "Kopierede GPU information til udklipsholder" + } + }, + "npuUsage": "NPU forbrug", + "npuMemory": "NPU hukommelse" + }, + "detector": { + "title": "Detektorer", + "inferenceSpeed": "Detektorinferenshastighed", + "temperature": "Detektor temperatur", + "cpuUsage": "Detektor CPU forbrug", + "cpuUsageInformation": "CPU brugt til at forberede input- og outputdata til/fra detektionsmodeller. Denne værdi måler ikke inferensforbrug, selvom der bruges en GPU eller accelerator.", + "memoryUsage": "Detektorhummelsesforbrug" + }, + "otherProcesses": { + "title": "Andre processer", + "processCpuUsage": "Proces CPU forbrug", + "processMemoryUsage": "Proceshukommelsesforbrug" + } + }, + "metrics": "System metrikker", + "storage": { + "title": "Lagring", + "overview": "Overblik", + "recordings": { + "title": "Optagelser", + "tips": "Denne værdi repræsenterer den samlede lagerplads, der bruges af optagelserne i Frigates database. Frigate sporer ikke lagerpladsforbruget for alle filer på din disk.", + "earliestRecording": "Tidligste optagelse til rådighed:" + }, + "shm": { + "title": "SHM (delt hukommelse) tildeling", + "warning": "Den nuværende SHM størrelse af {{total}}MB er for lille. Øg den til minimum {{min_shm}}MB." + }, + "cameraStorage": { + "title": "Kamera lagring", + "camera": "Kamera", + "unusedStorageInformation": "Ubrugt lagringsinformation", + "storageUsed": "Lagring", + "percentageOfTotalUsed": "Procentandel af total", + "bandwidth": "Båndbredde", + "unused": { + "title": "Ubrugt", + "tips": "Denne værdi repræsenterer muligvis ikke nøjagtigt den ledige plads, der er tilgængelig for Frigate, hvis du har andre filer gemt på dit drev ud over Frigates optagelser. Frigate sporer ikke lagerforbrug ud over sine optagelser." + } + } + }, + "cameras": { + "title": "Kameraer", + "overview": "Overblik" + } +} diff --git a/web/public/locales/de/audio.json b/web/public/locales/de/audio.json new file mode 100644 index 0000000..045beee --- /dev/null +++ b/web/public/locales/de/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Sprache", + "babbling": "Plappern", + "laughter": "Gelächter", + "bellow": "Gebrüll", + "whoop": "Jubel", + "whispering": "Flüstern", + "crying": "Weinen", + "bark": "Bellen", + "goat": "Ziege", + "car": "Auto", + "skateboard": "Skateboard", + "hair_dryer": "Haartrockner", + "animal": "Tier", + "boat": "Boot", + "blender": "Mixer", + "sink": "Waschbecken", + "scissors": "Schere", + "train": "Zug", + "clock": "Uhr", + "bird": "Vogel", + "motorcycle": "Motorrad", + "toothbrush": "Zahnbürste", + "bicycle": "Fahrrad", + "door": "Tür", + "keyboard": "Klavier", + "bus": "Bus", + "horse": "Pferd", + "cat": "Katze", + "dog": "Hund", + "sheep": "Schaf", + "mouse": "Maus", + "vehicle": "Fahrzeug", + "yell": "Schrei", + "snicker": "Gekicher", + "sigh": "Seufzer", + "choir": "Chor", + "yodeling": "Gejodel", + "chant": "Choral", + "mantra": "Mantra", + "child_singing": "Kindergesang", + "rapping": "Rappen", + "humming": "Summen", + "groan": "Stöhnen", + "grunt": "Grunzen", + "whistling": "Pfeifen", + "breathing": "Atmen", + "wheeze": "Keuchen", + "gasp": "nach Luft schnappen", + "pant": "Hecheln", + "snort": "Schnauben", + "cough": "Husten", + "sneeze": "Niesen", + "sniff": "Schnüffeln", + "run": "Laufen", + "shuffle": "Schlurfen", + "biting": "Beißen", + "gargling": "Gurgeln", + "stomach_rumble": "Magenknurren", + "burping": "Rülpsen", + "hiccup": "Schluckauf", + "fart": "Furz", + "hands": "Hände", + "finger_snapping": "Fingerschnippen", + "heartbeat": "Herzschlag", + "heart_murmur": "Herzgeräusch", + "cheering": "Gejubel", + "applause": "Beifall", + "chatter": "Geschwätz", + "crowd": "Menge", + "children_playing": "Kinderspiel", + "pets": "Haustiere", + "yip": "Aufjaulen", + "howl": "Heulen", + "growling": "Knurren", + "whimper_dog": "Hundegewimmer", + "purr": "Schnurren", + "meow": "Miauen", + "hiss": "Zischen", + "caterwaul": "Gejaule", + "livestock": "Vieh", + "clip_clop": "Klippklapp", + "neigh": "Wiehern", + "cattle": "Rinder", + "moo": "Muhen", + "cowbell": "Kuhglocke", + "oink": "Grunz", + "bleat": "Blöken", + "cluck": "Gackern", + "cock_a_doodle_doo": "Kikeriki", + "gobble": "Kollern", + "goose": "Gans", + "honk": "Hupen", + "coo": "Gurren", + "crow": "Krähe", + "dogs": "Hunde", + "rats": "Ratten", + "insect": "Insekt", + "fly": "Fliege", + "buzz": "Surren", + "frog": "Frosch", + "snake": "Schlange", + "hammond_organ": "Hammondorgel", + "synthesizer": "Synthesizer", + "sampler": "Probennehmer", + "drum_kit": "Schlagzeug", + "drum_machine": "Trommelsynthesizer", + "snare_drum": "Kleine Trommel", + "rimshot": "Rimshot", + "drum_roll": "Trommelwirbel", + "timpani": "Timpani", + "tabla": "Tabla", + "cymbal": "Becken", + "hi_hat": "Hi-Hat", + "wood_block": "Holzblock", + "tambourine": "Tamburin", + "tubular_bells": "Glockenspiel", + "camera": "Kamera", + "roar": "Brüllen", + "owl": "Eule", + "whale_vocalization": "Walgesang", + "mandolin": "Mandoline", + "chicken": "Huhn", + "sitar": "Sitar", + "ukulele": "Ukulele", + "tapping": "Tippen", + "flapping_wings": "Flügelschlagen", + "strum": "Herumklimpern", + "electronic_organ": "Elektrische Orgel", + "duck": "Ente", + "quack": "Quaken", + "wild_animals": "Wildtiere", + "rattle": "Klappern", + "music": "Musik", + "pig": "Schwein", + "chirp": "Zwitschern", + "guitar": "Gitarre", + "plucked_string_instrument": "Zupfinstrument", + "hoot": "Heulen", + "acoustic_guitar": "Akustikgitarre", + "electric_piano": "Elektrisches Klavier", + "cricket": "Grille", + "mosquito": "Mücke", + "musical_instrument": "Musikinstrument", + "steel_guitar": "Hawaiigitarre", + "organ": "Orgel", + "drum": "Trommel", + "roaring_cats": "Katzengeschrei", + "footsteps": "Schritte", + "chewing": "Kauen", + "caw": "Krächzen", + "piano": "Klavier", + "clapping": "Klatschen", + "patter": "Trippeln", + "percussion": "Percussion", + "singing": "Gesang", + "bass_guitar": "Bassgitarre", + "fowl": "Geflügel", + "squawk": "Kreischen", + "pigeon": "Taube", + "snoring": "Schnarchen", + "synthetic_singing": "Synthetischer Gesang", + "bow_wow": "Wau-Wau", + "turkey": "Truthahn", + "croak": "Krächzen", + "electric_guitar": "Elektrische Gitarre", + "throat_clearing": "Räuspern", + "gong": "Gong", + "banjo": "Banjo", + "zither": "Zitter", + "harpsichord": "Cembalo", + "bass_drum": "Basstrommel", + "maraca": "Maraca", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibrafon", + "steelpan": "Stahlpfanne", + "brass_instrument": "Blechblasinstrument", + "french_horn": "Waldhorn", + "string_section": "Streicher", + "violin": "Geige", + "pizzicato": "Pizzikato", + "saxophone": "Saxophon", + "clarinet": "Klarinette", + "jingle_bell": "Jingle Bell", + "chime": "Glockenspiel", + "bagpipes": "Dudelsack", + "theremin": "Theremin", + "pop_music": "Popmusik", + "bowed_string_instrument": "Streichinstrument", + "didgeridoo": "Didgeridoo", + "wind_chime": "Windspiel", + "flute": "Flöte", + "church_bell": "Kirchenglocke", + "bell": "Glocke", + "orchestra": "Orchester", + "wind_instrument": "Blasinstrument", + "trombone": "Posaune", + "bicycle_bell": "Fahrradklingel", + "trumpet": "Trompete", + "harmonica": "Mundharmonika", + "double_bass": "Kontrabass", + "cello": "Cello", + "harp": "Harfe", + "tuning_fork": "Stimmgabel", + "accordion": "Akkordeon", + "singing_bowl": "Klangschale", + "mallet_percussion": "Mallet-Schlagzeug", + "hip_hop_music": "Hip-Hop-Musik", + "beatboxing": "Beatboxen", + "punk_rock": "Punkrock", + "grunge": "Grunge", + "progressive_rock": "Progressiver Rock", + "psychedelic_rock": "Psychedelischer Rock", + "rhythm_and_blues": "Rythm and Blues", + "soul_music": "Soulmusik", + "country": "Country", + "swing_music": "Swingmusik", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folkmusik", + "disco": "Disco", + "classical_music": "Klassische Musik", + "opera": "Oper", + "electronic_music": "Elektronische Musik", + "house_music": "House Musik", + "dubstep": "Dubstep", + "electronica": "Elektronische Medien", + "electronic_dance_music": "Elektronische Tanzmusik", + "ambient_music": "Hintergrundmusik", + "trance_music": "Trance-Musik", + "music_of_latin_america": "Lateinamerikanische Musik", + "salsa_music": "Salsa-Musik", + "blues": "Blues", + "vocal_music": "Vokalmusik", + "a_capella": "A Capella", + "music_of_africa": "Afrikanische Musik", + "gospel_music": "Gospel-Musik", + "music_of_asia": "Asiatische Musik", + "carnatic_music": "Karnatische Musik", + "music_of_bollywood": "Bollywood-Musik", + "traditional_music": "Traditionelle Musik", + "independent_music": "Eigenständige Musik", + "song": "Lied", + "background_music": "Hintergrundmusik", + "theme_music": "Titelmusik", + "lullaby": "Schlaflied", + "christmas_music": "Weihnachtsmusik", + "dance_music": "Tanzmusik", + "happy_music": "Fröhliche Musik", + "tender_music": "Sanfte Musik", + "exciting_music": "Spannende Musik", + "scary_music": "Gruselige Musik", + "wind": "Wind", + "wind_noise": "Windrauschen", + "rain_on_surface": "Regen auf einer Oberfläche", + "stream": "Stream", + "waterfall": "Wasserfall", + "steam": "Dampf", + "fire": "Feuer", + "crackle": "Knistern", + "sailboat": "Segelboot", + "ship": "Schiff", + "motor_vehicle": "Kraftfahrzeug", + "toot": "tuten", + "car_alarm": "Autoalarm", + "power_windows": "Elektrische Fensterheber", + "tire_squeal": "Reifenquietschen", + "car_passing_by": "Vorbeifahrendes Auto", + "air_brake": "Druckluftbremse", + "air_horn": "Autohupe", + "reversing_beeps": "Rückfahrpiepser", + "ice_cream_truck": "Eiswagen", + "emergency_vehicle": "Einsatzfahrzeug", + "police_car": "Polizeiwagen", + "ambulance": "Krankenwagen", + "fire_engine": "Feuerwehrauto", + "traffic_noise": "Verkehrslärm", + "rail_transport": "Schienentransport", + "train_whistle": "Zugpfeife", + "train_horn": "Zugsignalhorn", + "train_wheels_squealing": "Quietschende Eisenbahnräder", + "aircraft": "Flugzeug", + "aircraft_engine": "Flugzeugmotor", + "jet_engine": "Strahltriebwerk", + "propeller": "Propeller", + "helicopter": "Hubschrauber", + "engine": "Motor", + "dental_drill's_drill": "Zahnbohrer", + "lawn_mower": "Rasenmäher", + "medium_engine": "Mittlerer Motor", + "heavy_engine": "Schwerer Motor", + "engine_knocking": "Motorklopfen", + "engine_starting": "Motorstart", + "idling": "Leerlauf", + "doorbell": "Türklingel", + "ding-dong": "BimBam", + "sliding_door": "Schiebetür", + "slam": "zuknallen", + "knock": "Klopfen", + "tap": "Schlag", + "squeak": "Quietschen", + "drawer_open_or_close": "Schublade Öffnen oder Schließen", + "dishes": "Geschirr", + "chopping": "Kleinhacken", + "frying": "Braten", + "microwave_oven": "Mikrowelle", + "water_tap": "Wasserhahn", + "bathtub": "Badewanne", + "toilet_flush": "Toilettenspülung", + "vacuum_cleaner": "Staubsauger", + "zipper": "Reißverschluss", + "keys_jangling": "Schlüsselanhänger", + "coin": "Münze", + "electric_shaver": "Rasierapparat", + "typing": "Tippen", + "typewriter": "Schreibmaschine", + "computer_keyboard": "Computertastatur", + "telephone": "Telefon", + "telephone_bell_ringing": "Telefonklingeln", + "telephone_dialing": "Telefonwahl", + "dial_tone": "Wählton", + "alarm_clock": "Wecker", + "siren": "Sirene", + "civil_defense_siren": "Zivilschutzsirene", + "smoke_detector": "Rauchmelder", + "foghorn": "Nebelhorn", + "whistle": "Pfeife", + "steam_whistle": "Dampfpfeife", + "mechanisms": "Mechanismen", + "ratchet": "Ratsche", + "tick": "Ticken", + "gears": "Getriebe", + "mechanical_fan": "Mechanischer Lüfter", + "printer": "Drucker", + "tools": "Werkzeuge", + "hammer": "Hammer", + "jackhammer": "Presslufthammer", + "sawing": "Sägen", + "power_tool": "Elektrowerkzeug", + "drill": "Bohrer", + "explosion": "Explosion", + "gunshot": "Schuss", + "fusillade": "Gewehrfeuer", + "artillery_fire": "Artilleriefeuer", + "cap_gun": "Maschinenpistole", + "fireworks": "Feuerwerk", + "firecracker": "Feuerwerkskörper", + "eruption": "Ausbruch", + "wood": "Holz", + "splinter": "Splittern", + "crack": "Knacken", + "glass": "Glas", + "chink": "Klirren", + "shatter": "Zerspringen", + "silence": "Stille", + "environmental_noise": "Umgebungsgeräusch", + "static": "Statisch", + "pink_noise": "Rosa Rauschen", + "television": "Fernsehgerät", + "radio": "Radio", + "scream": "Schrei", + "heavy_metal": "Heavy Metal", + "rock_music": "Rockmusik", + "techno": "Techno", + "reggae": "Reggae", + "rain": "Regen", + "gurgling": "Plätschern", + "jazz": "Jazz", + "video_game_music": "Videospielmusik", + "rock_and_roll": "Rock and Roll", + "scratching": "Kratzen", + "thunderstorm": "Gewitter", + "christian_music": "Christliche Musik", + "ska": "Ska", + "rustling_leaves": "Blätterrascheln", + "jingle": "Jingle", + "middle_eastern_music": "Orientalische Musik", + "drum_and_bass": "Trommel und Bass", + "flamenco": "Flamenco", + "music_for_children": "Kindermusik", + "new-age_music": "New-Age-Musik", + "afrobeat": "Afrobeat", + "wedding_music": "Hochzeitsmusik", + "soundtrack_music": "Soundtrack Musik", + "raindrop": "Regentropfen", + "sad_music": "Traurige Musik", + "angry_music": "Wütende Musik", + "ocean": "Ozean", + "thunder": "Donner", + "water": "Wasser", + "waves": "Wellen", + "race_car": "Rennwagen", + "rowboat": "Ruderboot", + "truck": "Lkw", + "motorboat": "Motorboot", + "chainsaw": "Kettensäge", + "railroad_car": "Eisenbahnwaggon", + "cupboard_open_or_close": "Schrank Öffnen oder Schließen", + "alarm": "Alarm", + "filing": "Feilen", + "chop": "Hacken", + "single-lens_reflex_camera": "Spiegelreflexkamera", + "light_engine": "Lichtmaschine", + "buzzer": "Summer", + "sound_effect": "Geräuscheffekt", + "accelerating": "Beschleunigen", + "electric_toothbrush": "Elektrische Zahnbürste", + "busy_signal": "Besetztzeichen", + "pulleys": "Riemenscheiben", + "sewing_machine": "Nähmaschine", + "air_conditioning": "Klimaanlage", + "burst": "Platzen", + "skidding": "Schleudern", + "subway": "U-Bahn", + "tick-tock": "Ticktack", + "shuffling_cards": "Karten mischen", + "cutlery": "Besteck", + "cash_register": "Kasse", + "ringtone": "Klingelton", + "writing": "Schreiben", + "fixed-wing_aircraft": "Starrflügler", + "fire_alarm": "Feueralarm", + "white_noise": "Weißes Rauschen", + "sanding": "Schleifen", + "machine_gun": "Maschinengewehr", + "boom": "Dröhnen", + "field_recording": "Außenaufnahme", + "liquid": "Flüssigkeit", + "splash": "Spritzer", + "slosh": "Schwenken", + "squish": "Quetschen", + "drip": "Tropfen", + "pour": "Gießen", + "trickle": "Tröpfeln", + "fill": "Füllen", + "spray": "Sprühen", + "pump": "Pumpen", + "stir": "Umrühren", + "boiling": "Köchelnd", + "arrow": "Pfeil", + "electronic_tuner": "Elektronischer Tuner", + "effects_unit": "Effekteinheit", + "chorus_effect": "Chorus-Effekt", + "sodeling": "Verfilzen", + "chird": "Akkord", + "change_ringing": "Wechsle RingRing", + "shofar": "Schofar", + "gush": "sprudeln", + "sonar": "Sonar", + "whoosh": "Rauschen", + "thump": "Ruck", + "basketball_bounce": "Basketball Abbraller", + "bang": "Knall", + "slap": "Ohrfeige", + "whack": "verhauen", + "smash": "zerschlagen", + "breaking": "zerbrechen", + "bouncing": "Abbraller", + "whip": "Peitsche", + "flap": "Lasche", + "scratch": "Kratzer", + "scrape": "Abfall", + "rub": "scheuern", + "roll": "rollen", + "crushing": "Stauchen", + "crumpling": "zerknüllen", + "tearing": "Reißen", + "beep": "Piep", + "ping": "Ping", + "ding": "klingeln", + "thunk": "dumpfes Geräusch", + "clang": "Geklirr", + "squeal": "Ausruf", + "creak": "Knarren", + "rustle": "Geknister", + "whir": "schwirren", + "clatter": "Geratter", + "sizzle": "brutzeln", + "clicking": "Klicken", + "clickety_clack": "Klappergeräuschen", + "rumble": "Grollen", + "plop": "plumpsen", + "hum": "Brummen", + "zing": "Schwung", + "boing": "ferderndes Geräusch", + "crunch": "knirschendes", + "sine_wave": "Sinus Kurve", + "harmonic": "harmonisch", + "chirp_tone": "Frequenzwobbelung", + "pulse": "Takt", + "inside": "drinnen", + "outside": "draußen", + "reverberation": "Widerhall", + "echo": "Echo", + "noise": "Lärm", + "mains_hum": "Netzbrummen", + "distortion": "Verzerrung", + "sidetone": "Nebengeräusch", + "cacophony": "Dissonanz", + "throbbing": "Pochen", + "vibration": "Vibration" +} diff --git a/web/public/locales/de/common.json b/web/public/locales/de/common.json new file mode 100644 index 0000000..1ddb048 --- /dev/null +++ b/web/public/locales/de/common.json @@ -0,0 +1,334 @@ +{ + "time": { + "untilForTime": "Bis {{time}}", + "last7": "Letzte 7 Tage", + "untilForRestart": "Bis Frigate neu startet ist.", + "today": "Heute", + "yesterday": "Gestern", + "thisWeek": "Diese Woche", + "lastMonth": "Letzter Monat", + "5minutes": "5 Minuten", + "12hours": "12 Stunden", + "24hours": "24 Stunden", + "month_one": "{{time}} Monat", + "month_other": "{{time}} Monate", + "d": "{{time}} Tg.", + "day_one": "{{time}} Tag", + "day_other": "{{time}} Tage", + "m": "{{time}} Min", + "minute_one": "{{time}} Minute", + "minute_other": "{{time}} Minuten", + "s": "{{time}}s", + "second_one": "{{time}} Sekunde", + "second_other": "{{time}} Sekunden", + "formattedTimestamp2": { + "24hour": "dd. MMM HH:mm:ss", + "12hour": "dd.MM hh:mm:ss" + }, + "last30": "Letzte 30 Tage", + "10minutes": "10 Minuten", + "thisMonth": "Dieser Monat", + "yr": "{{time}}Jahr", + "year_one": "{{time}}Jahr", + "year_other": "{{time}}Jahre", + "hour_one": "{{time}} Stunde", + "hour_other": "{{time}} Stunden", + "last14": "Letzte 14 Tage", + "30minutes": "30 Minuten", + "1hour": "1 Stunde", + "lastWeek": "Letzte Woche", + "h": "{{time}} Std.", + "ago": "vor {{timeAgo}}", + "untilRestart": "Bis zum Neustart", + "justNow": "Gerade", + "pm": "nachmittags", + "mo": "{{time}} Mon.", + "formattedTimestamp": { + "12hour": "d. MMM, hh:mm:ss aaa", + "24hour": "dd. MMM, hh:mm:ss aaa" + }, + "formattedTimestampWithYear": { + "24hour": "%-d %b %Y, %H:%M", + "12hour": "%-d %b %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%-d %b", + "am": "vormittags", + "formattedTimestampExcludeSeconds": { + "24hour": "%-d %b, %H:%M", + "12hour": "%-d %b, %H:%M" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d. MMM yyyy", + "24hour": "d. MMM yyyy" + }, + "inProgress": "Im Gange", + "invalidStartTime": "Ungültige Startzeit", + "invalidEndTime": "Ungültige Endzeit", + "never": "Nie" + }, + "button": { + "save": "Speichern", + "delete": "Entfernen", + "apply": "Anwenden", + "enabled": "Aktiviert", + "enable": "Aktivieren", + "disabled": "deaktiviert", + "disable": "deaktivieren", + "saving": "Speichere…", + "close": "Schließen", + "back": "Zurück", + "history": "Historie", + "cameraAudio": "Kamera Ton", + "yes": "Ja", + "info": "Info", + "play": "Abspielen", + "export": "Exportieren", + "deleteNow": "Jetzt löschen", + "next": "Nächster", + "fullscreen": "Vollbild", + "no": "Nein", + "off": "AUS", + "reset": "Zurücksetzen", + "copy": "Kopieren", + "twoWayTalk": "Zwei-Wege-Kommunikation", + "exitFullscreen": "Vollbild verlassen", + "unselect": "Selektion aufheben", + "copyCoordinates": "Kopiere Koordinaten", + "done": "Fertig", + "edit": "Bearbeiten", + "download": "Herunterladen", + "cancel": "Abbrechen", + "pictureInPicture": "Bild in Bild", + "on": "AN", + "suspended": "Pausierte", + "unsuspended": "fortsetzen", + "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", + "retry": "Wiederholen" + }, + "label": { + "back": "Zurück", + "hide": "Verstecke {{item}}", + "show": "Zeige {{item}}", + "ID": "ID", + "none": "Nichts", + "all": "Alle", + "other": "andere" + }, + "menu": { + "configurationEditor": "Konfigurationseditor", + "languages": "Sprachen", + "language": { + "withSystem": { + "label": "Sprache der Systemeinstellungen verwenden" + }, + "en": "Englisch", + "zhCN": "简体中文 (Vereinfachtes Chinesisch)", + "fr": "Französisch", + "es": "Spanisch", + "ar": "Arabisch", + "pt": "Portugiesisch", + "de": "Deutsch", + "it": "Italienisch", + "nl": "Niederländisch", + "sv": "Schwedisch", + "cs": "Tschechisch", + "ko": "Koreanisch", + "pl": "Polnisch", + "el": "Griechisch", + "ro": "Rumänisch", + "hu": "Ungarisch", + "fi": "Finnisch", + "ru": "Russisch", + "ja": "Japanisch", + "tr": "Türkisch", + "da": "Dänisch", + "hi": "Hindi", + "nb": "Norwegisch", + "vi": "Vietnamesisch", + "fa": "Persisch", + "uk": "Ukrainisch", + "he": "Hebräisch", + "sk": "Slowakisch", + "yue": "粵語 (Kantonesisch)", + "th": "ไทย (Thailändisch)", + "ca": "Català (Katalanisch)", + "ur": "اردو (Urdu)", + "ptBR": "Portugiesisch (Brasilianisch)", + "sr": "Српски (Serbisch)", + "sl": "Slovenščina (Slowenisch)", + "lt": "Lietuvių (Litauisch)", + "bg": "Български (bulgarisch)", + "gl": "Galego (Galicisch)", + "id": "Bahasa Indonesia (Indonesisch)", + "hr": "Hrvatski (Kroatisch)", + "bs": "Bosanski (Bosnisch)", + "zhHant": "Traditional Chinese" + }, + "appearance": "Erscheinung", + "theme": { + "label": "Design-Thema", + "blue": "Blau", + "green": "Grün", + "default": "Standard", + "nord": "Nord", + "red": "Rot", + "contrast": "Hoher Kontrast", + "highcontrast": "Hoher Kontrast" + }, + "help": "Hilfe", + "documentation": { + "title": "Dokumentation", + "label": "Frigate Dokumentation" + }, + "live": { + "allCameras": "Alle Kameras", + "cameras": { + "title": "Kameras", + "count_one": "{{count}} Kamera", + "count_other": "{{count}} Kameras" + }, + "title": "Live" + }, + "review": "Überprüfen", + "restart": "Frigate neu starten", + "darkMode": { + "light": "Hell", + "label": "Dunkler Modus", + "dark": "Dunkel", + "withSystem": { + "label": "Verwende Systemeinstellungen fuer hell oder dunkel Modus" + } + }, + "system": "System", + "configuration": "Konfigurieren", + "withSystem": "System", + "settings": "Einstellungen", + "systemLogs": "Systemprotokoll", + "systemMetrics": "Systemstatistiken", + "explore": "Erkunden", + "faceLibrary": "Gesichterbibliothek", + "user": { + "title": "Benutzer", + "account": "Benutzerkonto", + "current": "Aktueller Benutzer: {{user}}", + "setPassword": "Passwort setzen", + "anonymous": "anonym", + "logout": "Abmelden" + }, + "uiPlayground": "Testgebiet für Benutzeroberfläche", + "export": "Exportieren", + "classification": "Klassifizierung", + "actions": "Aktion", + "chat": "Chat", + "profiles": "Profile", + "features": "Funktionen" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "Fuß", + "meters": "Meter" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/Stunde", + "mbph": "MB/Stunde", + "gbph": "GB/Stunde" + } + }, + "toast": { + "copyUrlToClipboard": "URL in Zwischenablage kopiert.", + "save": { + "error": { + "title": "Speichern der Konfigurationsänderungen gescheitert: {{errorMessage}}", + "noMessage": "Speichern der Konfigurationsänderungen gescheitert" + }, + "title": "Speichern", + "success": "Die Konfigurationsänderungen wurden erfolgreich gespeichert." + } + }, + "role": { + "title": "Rolle", + "admin": "Administrator", + "viewer": "Zuschauer", + "desc": "Administratoren haben vollen Zugang zu allen Funktionen der Frigate Benutzeroberfläche. Zuschauer können nur Kameras betrachten, erkannte Objekte überprüfen und historische Aufnahmen durchsehen." + }, + "pagination": { + "previous": { + "title": "Voherige", + "label": "Zur voherigen Seite wechseln" + }, + "next": { + "title": "Nächste", + "label": "Zur nächsten Seite wechseln" + }, + "more": "Weitere Seiten", + "label": "Seitennummerierung" + }, + "notFound": { + "title": "404", + "desc": "Seite nicht gefunden", + "documentTitle": "Nicht gefunden - Frigate" + }, + "selectItem": "Wähle {{item}}", + "readTheDocumentation": "Dokumentation lesen", + "accessDenied": { + "desc": "Du hast keine Berechtigung diese Seite anzuzeigen.", + "documentTitle": "Zugang verweigert - Frigate", + "title": "Zugang verweigert" + }, + "information": { + "pixels": "{{area}}px" + }, + "field": { + "optional": "Optional", + "internalID": "Die interne ID, die Frigate in der Konfiguration und Datenbank verwendet" + }, + "list": { + "two": "{{0}} und {{1}}", + "many": "{{items}}, und {{last}}", + "separatorWithSpace": ", " + }, + "no_items": "Keine Artikel", + "validation_errors": "Validierungsfehler", + "credentialField": { + "savedPlaceholder": "Gespeichert – leer lassen, um den aktuellen Stand beizubehalten" + } +} diff --git a/web/public/locales/de/components/auth.json b/web/public/locales/de/components/auth.json new file mode 100644 index 0000000..2c48866 --- /dev/null +++ b/web/public/locales/de/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "login": "Anmeldung", + "errors": { + "passwordRequired": "Kennwort ist erforderlich", + "loginFailed": "Anmeldung gescheitert", + "webUnknownError": "Unbekannter Fehler. Prüfe Konsolenlogs.", + "usernameRequired": "Benutzername ist erforderlich", + "rateLimit": "Anmeldelimit überschritten. Bitte später erneut versuchen.", + "unknownError": "Unbekannter Fehler. Prüfe Logs." + }, + "user": "Benutzername", + "password": "Kennwort", + "firstTimeLogin": "Ist dies der erste Loginversuch? Die Zugangsdaten werden in den Frigate Logs angezeigt." + } +} diff --git a/web/public/locales/de/components/camera.json b/web/public/locales/de/components/camera.json new file mode 100644 index 0000000..ff98559 --- /dev/null +++ b/web/public/locales/de/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "delete": { + "label": "Kameragruppe löschen", + "confirm": { + "title": "Löschen bestätigen", + "desc": "Willst Du die Kameragruppe {{name}} wirklich löschen?" + } + }, + "name": { + "label": "Name", + "placeholder": "Gib einen Namen ein…", + "errorMessage": { + "exists": "Name der Kameragruppe bereits vorhanden.", + "nameMustNotPeriod": "Name einer Kameragruppe darf keinen Punkt enthalten.", + "mustLeastCharacters": "Name einer Kameragruppe muss mindestens 2 Zeichen haben.", + "invalid": "Ungültiger Name für eine Kameragruppe." + } + }, + "icon": "Icon", + "camera": { + "setting": { + "label": "Kamera Streaming Einstellungen", + "audioIsAvailable": "Audio ist für diesen Stream verfügbar", + "audioIsUnavailable": "Audio ist für diesen Stream nicht verfügbar", + "streamMethod": { + "label": "Streaming-Methode", + "method": { + "noStreaming": { + "label": "Kein Streaming", + "desc": "Kamerabilder werden nur einmal pro Minute aktualisiert und es wird kein Live Streaming geben." + }, + "smartStreaming": { + "label": "Smart Streaming (empfohlen)", + "desc": "Smart Streaming wird Deine Kamera einmal in der Minute aktualisieren, wenn sich keine erkennbare Aktivität ereignet, um Bandbreite und Ressourcen zu schonen. Sobald eine Aktivität erkannt wird, wechselt das Standbild sofort zu einem Live Stream." + }, + "continuousStreaming": { + "label": "Kontinuierliches Streaming", + "desc": { + "title": "Das auf einem Dashboard sichtbare Kamerabild ist immer ein Live Stream, selbst wenn keine Aktivität erkannt wird.", + "warning": "Kontinuierliches Streaming kann zu hoher Bandbreitenausnutzung und zu Performanceproblemen führen. Bitte behutsam nutzen." + } + } + }, + "placeholder": "Wähle eine streaming Methode" + }, + "title": "{{cameraName}} Streaming Einstellungen", + "compatibilityMode": { + "desc": "Aktiviere diese Option nur, falls der Live Stream Deiner Kamera Farbstörungen zeigt und eine Diagonale Linie auf der rechten Seite des Bildes hat.", + "label": "Kompatibilitätsmodus" + }, + "audio": { + "tips": { + "title": "Audio muss in der Kamera verfügbar und in go2rtc für diesen Stream konfiguriert sein.", + "document": "Lies die Dokumentation. " + } + }, + "desc": "Ändere die Live Stream Optionen für das Dashboard dieser Kameragruppe. Diese Einstellungen sind geräte-/browserspezifisch.", + "stream": "Stream", + "placeholder": "Wähle einen Stream" + }, + "birdseye": "Vogelperspektive" + }, + "add": "Kameragruppe hinzufügen", + "cameras": { + "label": "Kameras", + "desc": "Wähle Kameras für diese Gruppe aus." + }, + "label": "Kamera Gruppen", + "edit": "Kameragruppe bearbeiten", + "success": "Kameragruppe {{name}} wurde gespeichert.", + "showAll": "Alle Kameragruppen anzeigen", + "showLess": "Weniger anzeigen", + "editGroups": "Kameragruppen bearbeiten" + }, + "debug": { + "options": { + "title": "Optionen", + "hideOptions": "Verberge Optionen", + "label": "Einstellungen", + "showOptions": "Zeige Optionen" + }, + "timestamp": "Zeitstempel", + "zones": "Zonen", + "mask": "Maske", + "motion": "Bewegung", + "regions": "Regionen", + "boundingBox": "Begrenzungsrechteck", + "paths": "Pfad" + } +} diff --git a/web/public/locales/de/components/dialog.json b/web/public/locales/de/components/dialog.json new file mode 100644 index 0000000..fe0db22 --- /dev/null +++ b/web/public/locales/de/components/dialog.json @@ -0,0 +1,206 @@ +{ + "restart": { + "title": "Sind Sie sicher, dass Sie Frigate neustarten wollen?", + "restarting": { + "title": "Frigate startet neu", + "content": "Diese Seite wird in {{countdown}} Sekunde(n) aktualisiert.", + "button": "Neuladen erzwingen" + }, + "button": "Neustarten", + "description": "Dies wird Frigate kurz stoppen, während es neu startet." + }, + "explore": { + "plus": { + "review": { + "true": { + "label": "Bestätigen Sie das Label für Frigate Plus", + "true_one": "Das ist ein/eine {{label}}", + "true_other": "Dies sind {{label}}" + }, + "state": { + "submitted": "Übermittelt" + }, + "false": { + "false_one": "Das ist kein(e) {{label}}", + "false_other": "Das sind kein(e) {{label}}", + "label": "Bestätige dieses Label nicht für Frigate Plus" + }, + "question": { + "label": "Bestätige diese Beschriftung für Frigate Plus", + "ask_a": "Ist dieses Objekt ein {{label}}?", + "ask_an": "Ist dieses Objekt ein {{label}}?", + "ask_full": "Ist dieses Objekt ein {{untranslatedLabel}} ({{translatedLabel}})?" + } + }, + "submitToPlus": { + "label": "An Frigate+ übermitteln", + "desc": "Objekte an Orten die du vermeiden möchtest, sind keine Fehlalarme. Wenn du sie als Fehlalarme meldest, verwirrst du das Modell." + } + }, + "video": { + "viewInHistory": "Im Verlauf ansehen" + } + }, + "export": { + "time": { + "fromTimeline": "Aus der Zeitleiste auswählen", + "start": { + "title": "Startzeit", + "label": "Startzeit auswählen" + }, + "end": { + "label": "Endzeit auswählen", + "title": "Endzeit" + }, + "lastHour_one": "Letzte Stunde", + "lastHour_other": "Letzte {{count}} Stunden", + "custom": "Benutzerdefiniert" + }, + "name": { + "placeholder": "Export benennen" + }, + "select": "Auswählen", + "selectOrExport": "Auswählen oder Exportieren", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Die Endzeit darf nicht vor der Startzeit liegen", + "failed": "Fehler beim Export in die Warteschlange: {{error}}", + "noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt" + }, + "success": "Export erfolgreich gestartet. Die Datei befindet sich auf der Exportseite.", + "view": "Ansicht", + "queued": "Export in Warteschlange gestellt. Fortschritt auf der Exportseite verfolgen.", + "batchSuccess_one": "1 Export gestartet. Öffne den Fall jetzt.", + "batchSuccess_other": "{{count}} Exports gestartet. Öffne den Fall jetzt.", + "batchPartial": "{{successful}} von {{total}} Exporten gestartet. Fehlgeschlagene Kameras: {{failedCameras}}", + "batchFailed": "Fehler beim Starten der {{total}} Exporte. Fehlgeschlagene Kameras: {{failedCameras}}", + "batchQueuedSuccess_one": "1 Export in die Warteschlange gestellt. Fall wird jetzt geöffnet.", + "batchQueuedSuccess_other": "{{count}} Exporte in der Warteschlange. Fall wird jetzt geöffnet.", + "batchQueuedPartial": "{{successful}} von {{total}} Exporten in die Warteschlange gestellt. Fehlerhafte Kameras: {{failedCameras}}", + "batchQueueFailed": "Fehler beim Einreihen von {{total}} Exporten in die Warteschlange. Fehlerhafte Kameras: {{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "Export speichern", + "previewExport": "Exportvorschau", + "queueingExport": "Export wird in die Warteschlange gestellt...", + "useThisRange": "Nutzen Sie diesen Bereich" + }, + "export": "Exportieren", + "case": { + "label": "Fall", + "placeholder": "Einen Fall auswählen", + "newCaseOption": "Einen neuen Fall erstellen", + "newCaseNamePlaceholder": "Neuer Fallname", + "newCaseDescriptionPlaceholder": "Fall Beschreibung", + "nonAdminHelp": "Für diese Exporte wird ein neuer Fall angelegt." + }, + "queueing": "Export wird in die Warteschlange gestellt...", + "tabs": { + "export": "Einzelne Kamera", + "multiCamera": "Mehrere-Kameras" + }, + "multiCamera": { + "timeRange": "Zeitbereich", + "selectFromTimeline": "Wählen Sie aus der Zeitleiste aus", + "cameraSelection": "Kameras", + "cameraSelectionHelp": "Kameras, die in diesem Zeitbereich Objekte verfolgen, sind vorausgewählt", + "checkingActivity": "Kameraaktivität wird überprüft...", + "noCameras": "Keine Kameras verfügbar", + "detectionCount_one": "1 verfolgtes Objekt", + "detectionCount_other": "{{count}} verfolgte Objekte", + "nameLabel": "Exportname", + "namePlaceholder": "Optionaler Basisname für diese Exporte", + "queueingButton": "Exporte werden in die Warteschlange gestellt...", + "exportButton_one": "Export 1 Kamera", + "exportButton_other": "xport {{count}} Kameras", + "searchOrSelectGroup": "Suchen, oder Kameragruppe auswählen...", + "selectAll": "Alle Kameras auswählen", + "clearSelection": "Auswahl löschen", + "selectWithActivity": "Kameras mit verfolgten Objekten", + "selectGroup": "Gruppe auswählen", + "noMatchingCameras": "Der Suche entsprechen keine Kameras", + "selectedCount": "{{selected}} / {{total}} ausgewählt" + }, + "multi": { + "title_one": "1 Bewertung exportieren", + "title_other": "{{count}} Bewertung exportieren", + "description": "Exportieren Sie jede ausgewählte Rezension. Alle Exporte werden in einem einzigen Fall zusammengefasst.", + "descriptionNoCase": "Jede ausgewählte Bewertung exportieren.", + "caseNamePlaceholder": "Export prüfen - {{date}}", + "exportButton_one": "1 Bewertung exportieren", + "exportButton_other": "{{count}} Bewertung exportieren", + "exportingButton": "Exportieren...", + "toast": { + "started_one": "1 Export gestartet. Fall wird jetzt geöffnet.", + "started_other": "{{count}} Exporte gestartet. Fall wird jetzt geöffnet.", + "startedNoCase_one": "1 Export gestartet.", + "startedNoCase_other": "{{count}} Exports gestartet.", + "partial": "{{successful}} von {{total}} Exporten gestartet. Fehlgeschlagen: {{failedItems}}", + "failed": "Fehler beim Starten der {{total}} Exporte. Fehler: {{failedItems}}" + } + } + }, + "streaming": { + "restreaming": { + "disabled": "Für diese Kamera ist das Restreaming nicht aktiviert.", + "desc": { + "readTheDocumentation": "Weitere Informationen in der Dokumentation", + "title": "Konfiguriere go2rtc, um erweiterte Live-Ansichtsoptionen und Audio für diese Kamera zu nutzen." + } + }, + "showStats": { + "label": "Stream-Statistiken anzeigen", + "desc": "Stream-Statistiken werden bei aktivierter Option als Overlay im Kamera-Feed eingeblendet." + }, + "debugView": "Debug-Ansicht", + "label": "Stream" + }, + "search": { + "saveSearch": { + "label": "Suche speichern", + "desc": "Gib einen Namen für diese gespeicherte Suche an.", + "placeholder": "Gib einen Namen für die Suche ein", + "overwrite": "{{searchName}} existiert bereits. Beim Speichern wird der vorhandene Wert überschrieben.", + "button": { + "save": { + "label": "Diese Suche speichern" + } + }, + "success": "Die Suche {{searchName}} wurde gespeichert." + } + }, + "recording": { + "confirmDelete": { + "title": "Bestätige Löschung", + "desc": { + "selected": "Bist du sicher, dass du alle aufgezeichneten Videos, die mit diesem Beitrag verbunden sind, löschen möchtest?

    Halte Shift-Taste gedrückt, um diesen Dialog in Zukunft zu umgehen." + }, + "toast": { + "success": "Das Videomaterial des gewählten Eintrags wurde erfolgreich gelöscht.", + "error": "Löschen is fehlgeschlagen: {{error}}" + } + }, + "button": { + "export": "Exportieren", + "markAsReviewed": "Als geprüft markieren", + "deleteNow": "Jetzt löschen", + "markAsUnreviewed": "Als ungeprüft markieren" + }, + "shareTimestamp": { + "label": "Zeitstempel teilen", + "title": "Zeitstempel teilen", + "description": "Teile eine URL mit Zeitstempel, die die aktuelle Position des Players angibt, oder wähle einen benutzerdefinierten Zeitstempel aus. Beachte, dass es sich hierbei nicht um eine öffentliche Freigabe-URL handelt und dass nur Benutzer Zugriff darauf haben, die Zugriff auf Frigate und diese Kamera haben.", + "custom": "Benutzerdefinierter Zeitstempel", + "button": "URL des Zeitstempels teilen", + "shareTitle": "Zeitstempel der Fregattenbewertung: {{camera}}" + } + }, + "imagePicker": { + "selectImage": "Vorschaubild eines verfolgten Objekts selektieren", + "search": { + "placeholder": "Nach Label oder Unterlabel suchen..." + }, + "noImages": "Kein Vorschaubild für diese Kamera gefunden", + "unknownLabel": "Gespeichertes Triggerbild" + } +} diff --git a/web/public/locales/de/components/filter.json b/web/public/locales/de/components/filter.json new file mode 100644 index 0000000..3660cf5 --- /dev/null +++ b/web/public/locales/de/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filter", + "labels": { + "all": { + "short": "Labels", + "title": "Alle Labels" + }, + "label": "Labels", + "count_one": "{{count}} Label", + "count_other": "{{count}} Labels" + }, + "zones": { + "all": { + "title": "Alle Zonen", + "short": "Zonen" + }, + "label": "Zonen" + }, + "dates": { + "all": { + "title": "Alle Zeiträume", + "short": "Daten" + }, + "selectPreset": "Wähle eine Voreinstellung aus…" + }, + "reset": { + "label": "Filter auf Standardwerte zurücksetzen" + }, + "more": "Mehr Filter", + "timeRange": "Zeitraum", + "subLabels": { + "all": "Alle Unterkategorien", + "label": "Unterkategorie" + }, + "features": { + "label": "Eigenschaften", + "hasSnapshot": "Hat einen Schnappschuss", + "hasVideoClip": "Hat einen Video-Clip", + "submittedToFrigatePlus": { + "label": "Eingereicht bei Frigate+", + "tips": "Du musst zuerst nach deine erkannten Objekten, die einen Schnappschuss haben, filtern.

    Erkante Objekte ohne Schnappschuss können nicht zu Frigate+ übermittelt werden." + } + }, + "score": "Ergebnis", + "estimatedSpeed": "Geschätzte Geschwindigkeit ({{unit}})", + "sort": { + "label": "Sortieren", + "dateAsc": "Datum (Aufsteigend)", + "dateDesc": "Datum (Absteigend)", + "scoreAsc": "Objekt Wertung (Aufsteigend)", + "scoreDesc": "Objekt Wertung (Absteigend)", + "speedAsc": "Geschätzte Geschwindigkeit (Aufsteigend)", + "relevance": "Relevanz", + "speedDesc": "Geschätzte Geschwindigkeit (absteigend)" + }, + "cameras": { + "all": { + "title": "Alle Kameras", + "short": "Kameras" + }, + "label": "Kamera Filter" + }, + "motion": { + "showMotionOnly": "Zeige nur Bewegung" + }, + "review": { + "showReviewed": "Geprüfte anzeigen" + }, + "explore": { + "settings": { + "defaultView": { + "title": "Standardansicht", + "desc": "Wenn keine Filter ausgewählt sind, wird eine Zusammenfassung der zuletzt verfolgten Objekte pro Kategorie oder ein ungefiltertes Raster angezeigt.", + "summary": "Zusammenfassung", + "unfilteredGrid": "Ungefiltertes Raster" + }, + "title": "Einstellungen", + "gridColumns": { + "title": "Rasterspalten", + "desc": "Wähle die Anzahl der Spalten in der Rasteransicht." + }, + "searchSource": { + "options": { + "description": "Beschreibung", + "thumbnailImage": "Vorschaubild" + }, + "label": "Quelle der Suche", + "desc": "Wähle, ob die Miniaturansichten oder die Beschreibungen der erkannten Objekte durchsucht werden sollen." + } + }, + "date": { + "selectDateBy": { + "label": "Wähle ein Datum zum Filtern" + } + } + }, + "logSettings": { + "label": "Log-Ebene filtern", + "filterBySeverity": "Protokolle nach Schweregrad filtern", + "loading": { + "title": "Lade", + "desc": "Wenn das Protokollfenster nach unten gescrollt wird, werden neue Protokolle automatisch geladen, sobald sie hinzugefügt werden." + }, + "disableLogStreaming": "Log des Streams deaktivieren", + "allLogs": "Alle Logs" + }, + "trackedObjectDelete": { + "title": "Bestätige Löschung", + "toast": { + "success": "Erkannte Objekte erfolgreich gelöscht.", + "error": "Das Löschen von verfolgten Objekten ist fehlgeschlagen: {{errorMessage}}" + }, + "desc": "Beim Löschen dieser {{objectLength}} verfolgten Objekte werden der Schnappschuss, alle gespeicherten Einbettungen und alle zugehörigen Objektlebenszykluseinträge entfernt. Aufgezeichnetes Filmmaterial dieser verfolgten Objekte in der Verlaufsansicht wird NICHT gelöscht.

    Bist du sicher, dass du fortfahren möchtest?

    Halte die Shift-Taste gedrückt, um diesen Dialog in Zukunft zu umgehen." + }, + "zoneMask": { + "filterBy": "Nach Zonenmaskierung filtern" + }, + "recognizedLicensePlates": { + "noLicensePlatesFound": "Keine Kennzeichen gefunden.", + "title": "Bekannte Kennzeichen", + "loadFailed": "Bekannte Nummernschilder konnten nicht geladen werden.", + "loading": "Lade bekannte Nummernschilder…", + "placeholder": "Tippe, um Kennzeichen zu suchen…", + "selectPlatesFromList": "Wählen eine oder mehrere Kennzeichen aus der Liste aus.", + "selectAll": "Alle wählen", + "clearAll": "Alle löschen" + }, + "classes": { + "label": "Klassen", + "all": { + "title": "Alle Klassen" + }, + "count_one": "{{count}} Klasse", + "count_other": "{{count}} Klassen" + }, + "attributes": { + "label": "Klassifizierungsattribute", + "all": "Alle Attribute" + } +} diff --git a/web/public/locales/de/components/icons.json b/web/public/locales/de/components/icons.json new file mode 100644 index 0000000..41d608b --- /dev/null +++ b/web/public/locales/de/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "Suche nach einem Icon…" + }, + "selectIcon": "Wähle ein Icon" + } +} diff --git a/web/public/locales/de/components/input.json b/web/public/locales/de/components/input.json new file mode 100644 index 0000000..fcee21c --- /dev/null +++ b/web/public/locales/de/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Video herunterladen", + "toast": { + "success": "Das Herunterladen des überprüften Videos wurde gestartet." + } + } + } +} diff --git a/web/public/locales/de/components/player.json b/web/public/locales/de/components/player.json new file mode 100644 index 0000000..6294301 --- /dev/null +++ b/web/public/locales/de/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "Keine Aufnahmen für diesen Zeitpunkt gefunden", + "noPreviewFound": "Keine Vorschau gefunden", + "submitFrigatePlus": { + "title": "Dieses Bild an Frigate+ senden?", + "submit": "Absenden", + "previewError": "Schnappschuss Vorschau konnte nicht geladen werden. Die Aufnahme ist möglicherweise derzeit nicht verfügbar." + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 oder höher ist für diesen Typ eines Live-Streams erforderlich.", + "streamOffline": { + "title": "Stream ist offline", + "desc": "Es wurden keine Bilder vom Erkennungsstream der Kamera {{cameraName}} empfangen, bitte Logs überprüfen" + }, + "cameraDisabled": "Kamera ist deaktiviert", + "stats": { + "streamType": { + "title": "Stream Typ:", + "short": "Typ" + }, + "bandwidth": { + "title": "Bandbreite:", + "short": "Bandbreite" + }, + "latency": { + "title": "Latenz:", + "value": "{{seconds}} Sekunden", + "short": { + "title": "Latenz", + "value": "{{seconds}} s" + } + }, + "droppedFrames": { + "short": { + "title": "Ausgelassen", + "value": "{{droppedFrames}} Bilder" + }, + "title": "Ausgelassene Bilder:" + }, + "decodedFrames": "Dekodierte Bilder:", + "droppedFrameRate": "Verlorene Bildrate:", + "totalFrames": "Bilder insgesamt:" + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "Bild an Frigate+ senden gescheitert" + }, + "success": { + "submittedFrigatePlus": "Bild erfolgreich an Frigate+ gesendet" + } + }, + "noPreviewFoundFor": "Keine Vorschau für {{cameraName}} gefunden", + "cameraOff": "Kamera ist ausgeschaltet" +} diff --git a/web/public/locales/de/config/cameras.json b/web/public/locales/de/config/cameras.json new file mode 100644 index 0000000..7865c49 --- /dev/null +++ b/web/public/locales/de/config/cameras.json @@ -0,0 +1,961 @@ +{ + "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, Gespräche, Rufen).", + "label": "Hörtypen" + }, + "filters": { + "label": "Audiofilter", + "description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden.", + "threshold": { + "label": "Mindestvertrauensgrad für Audio", + "description": "Mindestschwellenwert für die Zuverlässigkeit, damit das Audioereignis gezählt wird." + } + }, + "max_not_heard": { + "label": "Ende Timeout", + "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 Reihenfolge der Kamera im Birdseye-Layout steuert." + } + }, + "detect": { + "label": "Objekterkennung", + "description": "Einstellungen für die Erkennungs-/Detektionsrolle, die zum Ausführen der Objekterkennung und zum Initialisieren von Trackern verwendet wird.", + "enabled": { + "label": "Objekterkennung aktiviert", + "description": "Aktivieren oder deaktivieren Sie die Objekterkennung für diese Kamera." + }, + "height": { + "label": "Höhe erkennen", + "description": "Höhe (Pixel) der für den Erkennungsstrom verwendeten Frames; leer lassen, um die native Stream-Auflösung zu verwenden." + }, + "width": { + "label": "Breite erkennen", + "description": "Breite (Pixel) der für den Erkennungsstrom verwendeten Frames; leer lassen, um die native Stream-Auflösung zu verwenden." + }, + "fps": { + "label": "FPS erkennen", + "description": "Gewünschte Bilder pro Sekunde für die Erkennung; niedrigere Werte reduzieren die CPU-Auslastung (empfohlener Wert ist 5, höhere Werte – maximal 10 – nur bei der Verfolgung extrem schnell bewegter Objekte einstellen)." + }, + "min_initialized": { + "label": "Mindestanzahl an Initialisierungsframes", + "description": "Anzahl der aufeinanderfolgenden Erkennungserfolge, die vor der Erstellung eines verfolgten Objekts erforderlich sind. Erhöhen Sie diesen Wert, um Fehlinitialisierungen zu reduzieren. Der Standardwert ist fps geteilt durch 2." + }, + "max_disappeared": { + "label": "Maximale Anzahl fehlender Frames", + "description": "Anzahl der Frames ohne Erkennung, bevor ein verfolgtes Objekt als verschwunden gilt." + }, + "stationary": { + "label": "Konfiguration stationärer Objekte", + "description": "Einstellungen zum Erkennen und Verwalten von Objekten, die über einen bestimmten Zeitraum hinweg unbeweglich bleiben.", + "interval": { + "label": "Stationäres Intervall", + "description": "Wie oft (in Frames) soll eine Erkennungsprüfung durchgeführt werden, um ein stationäres Objekt zu bestätigen." + }, + "threshold": { + "label": "Stationäre Schwelle", + "description": "Anzahl der Frames ohne Positionsänderung, die erforderlich sind, um ein Objekt als stationär zu markieren." + }, + "max_frames": { + "label": "Maximale Bildanzahl", + "description": "Begrenzt, wie lange stationäre Objekte verfolgt werden, bevor sie verworfen werden.", + "default": { + "label": "Standardmäßige maximale Frames", + "description": "Standardmäßige maximale Anzahl von Frames, die ein stationäres Objekt verfolgt werden sollen, bevor die Verfolgung beendet wird." + }, + "objects": { + "label": "Objekt max Rahmen", + "description": "Objektbezogene Überschreibungen für maximale Frames zur Verfolgung stationärer Objekte." + } + }, + "classifier": { + "description": "Verwenden Sie einen visuellen Klassifikator, um wirklich stationäre Objekte auch dann zu erkennen, wenn die Begrenzungsrahmen flackern.", + "label": "Visuellen Klassifikator aktivieren" + } + }, + "annotation_offset": { + "label": "Anmerkung Offset", + "description": "Millisekunden zur Verschiebung der Anmerkungen, um die Begrenzungsrahmen der Zeitleiste besser an die Aufnahmen anzupassen; kann positiv oder negativ sein." + } + }, + "mqtt": { + "label": "mqtt", + "enabled": { + "label": "Bild senden", + "description": "Aktivieren Sie für diese Kamera die Veröffentlichung von Bild-Snapshots für Objekte in MQTT-Themen." + }, + "description": "Einstellungen für die Veröffentlichung von Bildern über MQTT.", + "timestamp": { + "label": "Zeitstempel hinzufügen", + "description": "Füge einen Zeitstempel auf Bilder ein, die über MQTT veröffentlicht werden." + }, + "bounding_box": { + "label": "Begrenzungsrahmen hinzufügen", + "description": "Zeichne Begrenzungsrahmen auf Bilder, die über MQTT veröffentlicht werden." + }, + "crop": { + "label": "Bild zuschneiden", + "description": "Bilder, die über MQTT veröffentlicht werden, werden auf die Begrenzungsrahmen der erkannten Objekte zugeschnitten." + }, + "height": { + "label": "Bildhöhe", + "description": "Höhe (in Pixeln) zur Größenanpassung von über MQTT veröffentlichten Bildern." + }, + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Zonen, die ein Objekt betreten muss, damit ein MQTT-Bild veröffentlicht wird." + }, + "quality": { + "label": "JPEG-Qualität", + "description": "JPEG-Qualität für über MQTT veröffentlichte Bilder (0–100)." + } + }, + "face_recognition": { + "label": "Gesichtserkennung", + "description": "Einstellungen für die Gesichtserkennung und -identifizierung dieser Kamera.", + "enabled": { + "label": "Gesichtserkennung aktivieren", + "description": "Gesichtserkennung aktivieren oder deaktivieren." + }, + "min_area": { + "label": "Mindestfläche der Stirnseite", + "description": "Mindestfläche (Pixel) eines erkannten Gesichtsrahmens, die für einen Erkennungsversuch erforderlich ist." + } + }, + "notifications": { + "label": "Benachrichtigung", + "enabled": { + "label": "Benachrichtigungen aktivieren", + "description": "Benachrichtigungen für diese Kamera aktivieren oder deaktivieren." + }, + "email": { + "label": "Benachrichtigungs-E-Mail", + "description": "E-Mail-Adresse, die für Push-Benachrichtigungen verwendet wird oder von bestimmten Benachrichtigungsanbietern verlangt wird." + }, + "cooldown": { + "label": "Abkühlungsphase", + "description": "Abkühlungszeit (Sekunden) zwischen Benachrichtigungen, um Spam an Empfänger zu vermeiden." + }, + "enabled_in_config": { + "label": "Ursprüngliche Meldungen geben an", + "description": "Gibt an, ob Benachrichtigungen in der ursprünglichen statischen Konfiguration aktiviert waren." + }, + "description": "Einstellungen zum Aktivieren und Verwalten von Benachrichtigungen für diese Kamera." + }, + "ffmpeg": { + "label": "Streams (FFmpeg)", + "description": "Kamera-Stream-Eingaben und FFmpeg-Optionen, einschließlich Binärpfad, Argumente, hwaccel und rollenspezifische Ausgabeargumente.", + "path": { + "label": "FFmpeg-Pfad", + "description": "Pfad zur zu verwendenden FFmpeg-Binärdatei oder ein Versionsalias („7.0” oder „8.0”)." + }, + "global_args": { + "label": "Globale Argumente von FFmpeg", + "description": "An FFmpeg-Prozesse übergebene globale Argumente." + }, + "hwaccel_args": { + "label": "Argumente für Hardwarebeschleunigung", + "description": "Hardwarebeschleunigungsargumente für FFmpeg. Es werden providerspezifische Voreinstellungen empfohlen." + }, + "input_args": { + "label": "Eingabeargumente", + "description": "Eingabeargumente, die auf FFmpeg-Eingabestreams angewendet werden." + }, + "output_args": { + "label": "Ausgabeargumente", + "description": "Standardausgabeargumente, die für verschiedene FFmpeg-Rollen wie „detect“ und „record“ verwendet werden.", + "detect": { + "label": "Ausgabeargumente erkennen", + "description": "Standardausgabeargumente für das Erkennen von Rollenströmen." + }, + "record": { + "label": "Ausgabeargumente aufzeichnen", + "description": "Standardausgabeargumente für Datensatzrollen-Streams." + } + }, + "retry_interval": { + "label": "FFmpeg-Wiederholungszeit", + "description": "Sekunden, die gewartet werden sollen, bevor nach einem Fehler erneut versucht wird, eine Kamera-Übertragung herzustellen. Der Standardwert ist 10." + }, + "apple_compatibility": { + "label": "Apple-Kompatibilität", + "description": "Aktivieren Sie die HEVC-Kennzeichnung für eine bessere Kompatibilität mit Apple-Playern bei der Aufnahme von H.265." + }, + "gpu": { + "label": "GPU-Index", + "description": "Standard-GPU-Index, der für die Hardwarebeschleunigung verwendet wird, sofern verfügbar." + }, + "inputs": { + "label": "Kameraeingänge", + "description": "Liste der Eingangsstromdefinitionen (Pfade und Rollen) für diese Kamera.", + "path": { + "label": "Eingabepfad", + "description": "URL oder Pfad des Kameraeingangsstroms." + }, + "roles": { + "label": "Eingangsrollen", + "description": "Rollen für diesen Eingabestrom." + }, + "global_args": { + "label": "Globale Argumente von FFmpeg", + "description": "Globale Argumente von FFmpeg für diesen Eingabestrom." + }, + "hwaccel_args": { + "label": "Argumente für Hardwarebeschleunigung", + "description": "Hardwarebeschleunigungsargumente für diesen Eingabestrom." + }, + "input_args": { + "label": "Eingabeargumente", + "description": "Für diesen Stream spezifische Eingabeargumente." + } + } + }, + "live": { + "label": "Live-Wiedergabe", + "description": "Einstellungen, die von der Web-Benutzeroberfläche zur Steuerung der Auswahl, Auflösung und Qualität von Live-Streams verwendet werden.", + "streams": { + "label": "Live-Stream Namen", + "description": "Zuordnung der konfigurierten Stream-Namen zu den für die Live-Wiedergabe verwendeten Restream-/Go2rtc-Namen." + }, + "height": { + "label": "Live-Höhe", + "description": "Höhe (Pixel) zum Rendern des jsmpeg-Livestreams in der Web-Benutzeroberfläche; muss <= Stream-Höhe sein." + }, + "quality": { + "label": "Live Qualität", + "description": "Kodierungsqualität für den jsmpeg-Stream (1 = höchst, 31 = niedrigst)." + } + }, + "lpr": { + "label": "Kennzeichenerkennung", + "description": "Einstellungen für die Kennzeichenerkennung, einschließlich Erkennungsschwellen, Formatierung und bekannte Kennzeichen.", + "enabled": { + "label": "LPR aktivieren", + "description": "LPR auf dieser Kamera aktivieren oder deaktivieren." + }, + "expire_time": { + "label": "Sekunden bis zum Ablauf", + "description": "Zeit in Sekunden, nach der ein nicht erkanntes Kennzeichen aus dem Tracker gelöscht wird (nur für dedizierte LPR-Kameras)." + }, + "min_area": { + "label": "Mindestplattenfläche", + "description": "Mindestplattenfläche (Pixel), die für einen Erkennungsversuch erforderlich ist." + }, + "enhancement": { + "label": "Verbesserungsgrad", + "description": "Verstärkungsstufe (0-10) zur Anwendung auf Plattenaufnahmen vor der OCR; höhere Werte führen nicht immer zu besseren Ergebnissen, Stufen über 5 funktionieren möglicherweise nur bei Nachtaufnahmen und sollten mit Vorsicht verwendet werden." + } + }, + "motion": { + "label": "Bewegungserkennung", + "description": "Standardmäßige Einstellungen für die Bewegungserkennung dieser Kamera.", + "enabled": { + "label": "Bewegungserkennung aktivieren", + "description": "Aktivieren oder deaktivieren Sie die Bewegungserkennung für diese Kamera." + }, + "threshold": { + "label": "Bewegungsschwelle", + "description": "Vom Bewegungsmelder verwendeter Schwellenwert für Pixelunterschiede; höhere Werte verringern die Empfindlichkeit (Bereich 1–255)." + }, + "lightning_threshold": { + "label": "Blitzschwelle", + "description": "Schwellenwert zum Erkennen und Ignorieren kurzer Beleuchtungsspitzen (niedrigerer Wert bedeutet höhere Empfindlichkeit, Werte zwischen 0,3 und 1,0). Dadurch wird die Bewegungserkennung nicht vollständig verhindert, sondern lediglich die Analyse weiterer Bilder durch den Detektor unterbrochen, sobald der Schwellenwert überschritten wird. Bewegungsbasierte Aufzeichnungen werden während dieser Ereignisse weiterhin erstellt." + }, + "skip_motion_threshold": { + "label": "Schwellenwert für Bewegungsüberspringen", + "description": "Wenn sich mehr als dieser Anteil des Bildes in einem einzelnen Frame ändert, gibt der Detektor keine Bewegungsfelder zurück und kalibriert sich sofort neu. Dies kann CPU-Leistung sparen und Fehlalarme bei Blitzschlag, Gewittern usw. reduzieren, aber auch echte Ereignisse übersehen, wie z. B. eine PTZ-Kamera, die ein Objekt automatisch verfolgt. Der Kompromiss besteht darin, entweder einige Megabyte an Aufzeichnungen zu verlieren oder ein paar kurze Clips zu überprüfen. Leer lassen um diese Funktion zu deaktivieren." + }, + "improve_contrast": { + "label": "Kontrast verbessern", + "description": "Wenden Sie vor der Bewegungsanalyse eine Kontrastverbesserung auf die Bilder an, um die Erkennung zu erleichtern." + }, + "contour_area": { + "label": "Konturbereich", + "description": "Mindestkonturfläche in Pixeln, die erforderlich ist, damit eine Bewegungskontur gezählt wird." + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "Alpha-Blending-Faktor, der bei der Frame-Differenzierung für die Bewegungsberechnung verwendet wird." + }, + "frame_alpha": { + "label": "Rahmen Alpha", + "description": "Alpha-Wert, der beim Mischen von Frames für die Bewegungsvorverarbeitung verwendet wird." + }, + "frame_height": { + "label": "Rahmenhöhe", + "description": "Höhe in Pixeln, auf die Frames bei der Berechnung von Bewegungen skaliert werden sollen." + }, + "mask": { + "label": "Maskenkoordinaten", + "description": "Geordnete x-, y-Koordinaten, die das Bewegungsmaskenpolygon definieren, das zum Einbeziehen/Ausschließen von Bereichen verwendet wird." + }, + "mqtt_off_delay": { + "label": "MQTT-Ausschaltverzögerung", + "description": "Sekunden, die nach der letzten Bewegung gewartet werden müssen, bevor ein MQTT-„Aus”-Status veröffentlicht wird." + }, + "enabled_in_config": { + "label": "Ursprünglicher Bewegungszustand", + "description": "Gibt an, ob die Bewegungserkennung in der ursprünglichen statischen Konfiguration aktiviert war." + }, + "raw_mask": { + "label": "Maskierung" + } + }, + "objects": { + "label": "Objekte", + "description": "Standardeinstellungen für die Objektverfolgung, einschließlich der zu verfolgenden Labels und Filter pro Objekt.", + "track": { + "label": "Zu verfolgende Objekte", + "description": "Liste der Objektbeschriftungen, die für diese Kamera verfolgt werden sollen." + }, + "filters": { + "label": "Objektfilter", + "description": "Filter, die auf erkannte Objekte angewendet werden, um Fehlalarme zu reduzieren (Fläche, Verhältnis, Konfidenz).", + "min_area": { + "label": "Mindestobjektfläche", + "description": "Mindestfläche der Begrenzungsbox (Pixel oder Prozentangabe), die für diesen Objekttyp erforderlich ist. Kann in Pixel (int) oder Prozentangabe (Float zwischen 0,000001 und 0,99) angegeben werden." + }, + "max_area": { + "label": "Maximale Objektfläche", + "description": "Maximal zulässige Begrenzungsrahmenfläche (Pixel oder Prozent) für diesen Objekttyp. Kann in Pixel (int) oder Prozent (Float zwischen 0,000001 und 0,99) angegeben werden." + }, + "min_ratio": { + "label": "Mindestseitenverhältnis", + "description": "Mindestverhältnis von Breite zu Höhe, das für die Begrenzungsbox erforderlich ist, damit diese gültig ist." + }, + "max_ratio": { + "label": "Maximales Seitenverhältnis", + "description": "Maximal zulässiges Verhältnis von Breite zu Höhe für die Begrenzungsbox, damit diese gültig ist." + }, + "threshold": { + "label": "Konfidenzschwelle", + "description": "Durchschnittlicher Schwellenwert für die Erkennungssicherheit, der erforderlich ist, damit das Objekt als echt positiv eingestuft wird." + }, + "min_score": { + "label": "Mindestvertrauen", + "description": "Mindestkonfidenz für die Einzelbilderkennung, die für die Zählung des Objekts erforderlich ist." + }, + "mask": { + "label": "Filter Maske", + "description": "Polygonkoordinaten, die definieren, wo dieser Filter innerhalb des Rahmens angewendet wird." + }, + "raw_mask": { + "label": "Rohmaske" + } + }, + "mask": { + "label": "Objekt Maskierung", + "description": "Maskenpolygon, das verwendet wird, um die Objekterkennung in bestimmten Bereichen zu verhindern." + }, + "genai": { + "label": "GenAI-Objektkonfiguration", + "description": "GenAI-Optionen zum Beschreiben verfolgter Objekte und zum Senden von Frames zur Generierung.", + "enabled": { + "label": "Aktivieren GenAI", + "description": "Die Erstellung von Beschreibungen für verfolgte Objekte durch GenAI standardmäßig aktivieren." + }, + "use_snapshot": { + "label": "Verwenden Sie Momentaufnahmen", + "description": "Verwenden Sie für die Erstellung von Beschreibungen durch GenAI Objektsnapshots anstelle von Miniaturansichten." + }, + "prompt": { + "label": "Aufforderung zur Bildunterschrift", + "description": "Standardvorlage für Eingabeaufforderungen, die bei der Erstellung von Beschreibungen mit GenAI verwendet wird." + }, + "object_prompts": { + "label": "Objekt-Eingabeaufforderungen", + "description": "Objektbezogene Eingabeaufforderungen zur Anpassung der GenAI-Ausgaben an bestimmte Labels." + }, + "objects": { + "label": "GenAI-Objekte", + "description": "Liste der Objektbezeichnungen, die standardmäßig an GenAI gesendet werden sollen." + }, + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Felder, die ausgefüllt werden müssen, damit Objekte für die Erstellung einer GenAI-Beschreibung in Frage kommen." + }, + "debug_save_thumbnails": { + "label": "Miniaturansichten speichern", + "description": "Speichere die an GenAI gesendeten Miniaturansichten zur Fehlerbehebung und Überprüfung." + }, + "send_triggers": { + "label": "GenAI-Auslöser", + "description": "Legt fest, wann Frames an GenAI gesendet werden sollen (am Ende, nach Aktualisierungen usw.).", + "tracked_object_end": { + "label": "weiterleiten", + "description": "Sende eine Anfrage an GenAI, sobald das verfolgte Objekt sein Ziel erreicht hat." + }, + "after_significant_updates": { + "label": "Früher GenAI-Auslöser", + "description": "Sende eine Anfrage an GenAI, nachdem eine bestimmte Anzahl bedeutender Aktualisierungen für das verfolgte Objekt erfolgt ist." + } + }, + "enabled_in_config": { + "label": "Ursprünglicher GenAI-Zustand", + "description": "Gibt an, ob GenAI in der ursprünglichen statischen Konfiguration aktiviert war." + } + }, + "raw_mask": { + "label": "Rohmaske" + } + }, + "record": { + "label": "Aufnahme", + "description": "Aufnahme- und Speichereinstellungen für diese Kamera.", + "enabled": { + "label": "Aufnahme aktivieren", + "description": "Die Aufzeichnung für diese Kamera aktivieren oder deaktivieren." + }, + "expire_interval": { + "label": "Bereinigungsintervall festlegen", + "description": "Minuten zwischen den Bereinigungsdurchläufen, bei denen abgelaufene Aufzeichnungssegmente entfernt werden." + }, + "continuous": { + "label": "Dauerhafte Aufbewahrung", + "description": "Anzahl der Tage, für die Aufzeichnungen unabhängig von verfolgten Objekten oder Bewegungen aufbewahrt werden sollen. Setzen Sie diesen Wert auf 0, wenn Sie nur Aufzeichnungen von Warnmeldungen und Erkennungen aufbewahren möchten.", + "days": { + "label": "Aufbewahrungsfristen", + "description": "Aufbewahrungsdauer der Aufzeichnungen." + } + }, + "motion": { + "label": "Bewegungsnachlauf", + "description": "Anzahl der Tage, für die durch Bewegung ausgelöste Aufzeichnungen unabhängig von den verfolgten Objekten aufbewahrt werden sollen. Setzen Sie diesen Wert auf 0, wenn Sie nur Aufzeichnungen von Warnmeldungen und Erkennungen aufbewahren möchten.", + "days": { + "label": "Aufbewahrungsfristen", + "description": "Aufbewahrungsdauer der Aufzeichnungen." + } + }, + "detections": { + "label": "Nachweis und Aufbewahrung", + "description": "Einstellungen zur Aufbewahrungsdauer von Aufzeichnungen für Erkennungsereignisse, einschließlich der Dauer vor und nach der Aufzeichnung.", + "pre_capture": { + "label": "Sekunden vor der Aufnahme", + "description": "Anzahl der Sekunden vor dem Erkennungsereignis, die in die Aufzeichnung aufgenommen werden sollen." + }, + "post_capture": { + "label": "Sekunden nach der Aufnahme", + "description": "Anzahl der Sekunden nach dem Erkennungsereignis, die in die Aufzeichnung aufgenommen werden sollen." + }, + "retain": { + "label": "Ereignisspeicherung", + "description": "Aufbewahrungsdauer für Aufzeichnungen von Erkennungsereignissen.", + "days": { + "label": "Aufbewahrungsfristen", + "description": "Anzahl der Tage, für die Aufzeichnungen von Erkennungsereignissen aufbewahrt werden sollen." + }, + "mode": { + "label": "Speichermodus", + "description": "Speichermodus: „all“ (alle Segmente speichern), „motion“ (Segmente mit Bewegung speichern) oder „active_objects“ (Segmente mit aktiven Objekten speichern)." + } + } + }, + "alerts": { + "label": "Aufbewahrungsfrist für Benachrichtigungen", + "description": "Einstellungen zur Aufbewahrungsdauer von Aufzeichnungen für Alarmereignisse, einschließlich der Dauer vor und nach dem Aufzeichnungsstart.", + "pre_capture": { + "label": "Sekunden vor der Aufnahme", + "description": "Anzahl der Sekunden vor dem Erkennungsereignis, die in die Aufzeichnung aufgenommen werden sollen." + }, + "post_capture": { + "label": "Sekunden nach der Aufnahme", + "description": "Anzahl der Sekunden nach dem Erkennungsereignis, die in die Aufzeichnung aufgenommen werden sollen." + }, + "retain": { + "label": "Ereignisspeicherung", + "description": "Aufbewahrungsdauer für Aufzeichnungen von Erkennungsereignissen.", + "days": { + "label": "Aufbewahrungsfrist", + "description": "Anzahl der Tage, für die Aufzeichnungen von Erkennungsereignissen aufbewahrt werden sollen." + }, + "mode": { + "label": "Speichermodus", + "description": "Speichermodus: „all“ (alle Segmente speichern), „motion“ (Segmente mit Bewegung speichern) oder „active_objects“ (Segmente mit aktiven Objekten speichern)." + } + } + }, + "export": { + "label": "Konfiguration exportieren", + "description": "Einstellungen, die beim Exportieren von Aufzeichnungen wie Zeitrafferaufnahmen und bei der Hardwarebeschleunigung verwendet werden.", + "hwaccel_args": { + "label": "hwaccel-Argumente exportieren", + "description": "Argumente für die Hardwarebeschleunigung bei Export- und Transkodierungsvorgängen." + }, + "max_concurrent": { + "label": "Maximale Anzahl gleichzeitiger Exporte", + "description": "Maximale Anzahl der gleichzeitig zu verarbeitenden Exportaufträge." + } + }, + "preview": { + "label": "Konfigurationsvorschau", + "description": "Einstellungen zur Steuerung der Qualität der in der Benutzeroberfläche angezeigten Aufnahmevorschauen.", + "quality": { + "label": "Vorschauqualität", + "description": "Qualitätsstufe der Vorschau (sehr_niedrig, niedrig, mittel, hoch, sehr_hoch)." + } + }, + "enabled_in_config": { + "label": "Ursprünglicher Aufnahmestatus", + "description": "Gibt an, ob die Aufzeichnung in der ursprünglichen statischen Konfiguration aktiviert war." + } + }, + "review": { + "label": "Rezension", + "description": "Einstellungen, die Benachrichtigungen, Erkennungen und GenAI-Überprüfungszusammenfassungen steuern, die von der Benutzeroberfläche und dem Speicher dieser Kamera verwendet werden.", + "alerts": { + "label": "Benachrichtigungseinstellungen", + "description": "Einstellungen dazu, bei welchen überwachten Objekten Warnmeldungen generiert werden und wie lange diese aufbewahrt werden.", + "enabled": { + "label": "Benachrichtigungen aktivieren", + "description": "Aktivieren oder deaktivieren Sie die Benachrichtigungsfunktion für diese Kamera." + }, + "labels": { + "label": "Warnhinweise", + "description": "Liste der Objektbezeichnungen, die als Warnmeldungen gelten (zum Beispiel: Auto, Person)." + }, + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Zonen, die ein Objekt betreten muss, damit ein Alarm ausgelöst wird; lassen Sie das Feld leer, wenn alle Zonen zulässig sein sollen." + }, + "enabled_in_config": { + "label": "Ursprünglicher Alarmstatus", + "description": "Zeigt an, ob Warnmeldungen ursprünglich in der statischen Konfiguration aktiviert waren." + }, + "cutoff_time": { + "label": "Annahmeschluss für Benachrichtigungen", + "description": "Sekunden, die nach dem Ende einer alarmauslösenden Aktivität gewartet werden müssen, bevor der Alarm abgeschaltet wird." + } + }, + "detections": { + "label": "Konfiguration der Erkennungen", + "description": "Einstellungen, die festlegen, bei welchen verfolgten Objekten Erkennungen (ohne Alarm) generiert werden und wie lange diese Erkennungen gespeichert bleiben.", + "enabled": { + "label": "Erkennung aktivieren", + "description": "Erkennungsereignisse für diese Kamera aktivieren oder deaktivieren." + }, + "labels": { + "label": "Kennzeichnungen zur Erkennung", + "description": "Liste der Objektbezeichnungen, die als Erkennungsereignisse gelten." + }, + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Zonen, die ein Objekt betreten muss, damit es als Erkennung gewertet wird; lassen Sie das Feld leer, wenn alle Zonen zulässig sein sollen." + }, + "cutoff_time": { + "label": "Zeitpunkt der Erkennung", + "description": "Sekunden, die nach dem Ende einer Aktivität, die keine Erkennung auslöst, gewartet werden müssen, bevor die Erkennung unterbrochen wird." + }, + "enabled_in_config": { + "label": "Ursprünglicher Erkennungsstatus", + "description": "Zeigt an, ob die Erkennung ursprünglich in der statischen Konfiguration aktiviert war." + } + }, + "genai": { + "label": "GenAI-Konfiguration", + "description": "Steuert den Einsatz generativer KI zur Erstellung von Beschreibungen und Zusammenfassungen von Rezensionsobjekten.", + "enabled": { + "label": "GenAI-Beschreibungen aktivieren", + "description": "Aktivieren oder deaktivieren Sie von GenAI generierte Beschreibungen und Zusammenfassungen für Überprüfungselemente." + }, + "alerts": { + "label": "GenAI für Benachrichtigungen aktivieren", + "description": "Verwenden Sie GenAI, um Beschreibungen für Alarmmeldungen zu erstellen." + }, + "detections": { + "label": "GenAI für die Erkennung aktivieren", + "description": "Verwenden Sie GenAI, um Beschreibungen für Erkennungselemente zu erstellen." + }, + "image_source": { + "label": "Quelle des Bildes", + "description": "Quelle der an GenAI gesendeten Bilder („Vorschau“ oder „Aufzeichnungen“); „Aufzeichnungen“ verwenden Bilder in höherer Qualität, verbrauchen jedoch mehr Token." + }, + "additional_concerns": { + "label": "Weitere Bedenken", + "description": "Eine Liste weiterer Aspekte oder Hinweise, die GenAI bei der Auswertung der Aktivitäten dieser Kamera berücksichtigen sollte." + }, + "debug_save_thumbnails": { + "label": "Miniaturansichten speichern", + "description": "Speichern Sie Miniaturansichten, die zur Fehlerbehebung und Überprüfung an den GenAI-Anbieter gesendet werden." + }, + "enabled_in_config": { + "label": "Ursprünglicher GenAI-Zustand", + "description": "Zeigt an, ob die GenAI-Überprüfung ursprünglich in der statischen Konfiguration aktiviert war." + }, + "preferred_language": { + "label": "Bevorzugte Sprache", + "description": "Bevorzugte Sprache, in der die generierten Antworten vom GenAI-Anbieter bereitgestellt werden sollen." + }, + "activity_context_prompt": { + "label": "Aufforderung zum Aktivitätskontext", + "description": "Eine benutzerdefinierte Eingabeaufforderung, die beschreibt, was als verdächtiges Verhalten gilt und was nicht, um den Zusammenfassungen der generativen KI einen Kontext zu geben." + } + } + }, + "onvif": { + "autotracking": { + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Objekte müssen in eine dieser Zonen eintreten, bevor die automatische Verfolgung beginnt." + }, + "movement_weights": { + "description": "Diese Kalibrierungswerte werden automatisch durch die Kamerakalibrierung generiert. Bitte nicht manuell ändern.", + "label": "Bewegungsgewichte" + }, + "label": "Automatische Verfolgung", + "description": "Bewegliche Objekte automatisch verfolgen und sie mithilfe von PTZ-Kamerabewegungen im Bildausschnitt zentriert halten.", + "enabled": { + "label": "Automatische Verfolgung aktivieren", + "description": "Aktivieren oder deaktivieren Sie die automatische PTZ-Kamera-Verfolgung erkannter Objekte." + }, + "calibrate_on_startup": { + "label": "Beim Start kalibrieren", + "description": "Messen Sie die Drehzahlen der PTZ-Motoren beim Start, um die Nachführgenauigkeit zu verbessern. Frigate aktualisiert die Konfiguration nach der Kalibrierung mit den Bewegungsgewichten." + }, + "zooming": { + "label": "Zoom-Modus", + "description": "Zoomverhalten steuern: deaktiviert (nur Schwenken/Neigen), absolut (am besten kompatibel) oder relativ (gleichzeitiges Schwenken/Neigen/Zoomen)." + }, + "zoom_factor": { + "label": "Zoomfaktor", + "description": "Steuert den Zoomfaktor bei verfolgten Objekten. Bei niedrigeren Werten bleibt mehr von der Szene im Bild; bei höheren Werten wird näher herangezoomt, wobei jedoch die Verfolgung verloren gehen kann. Werte zwischen 0,1 und 0,75." + }, + "track": { + "label": "Verfolgte Objekte", + "description": "Liste der Objekttypen, die das automatische Tracking auslösen sollen." + }, + "return_preset": { + "label": "Voreinstellung setzen", + "description": "Der in der Kamera-Firmware konfigurierte ONVIF-Voreinstellungsname, zu dem nach Beendigung der Verfolgung zurückgekehrt werden soll." + }, + "timeout": { + "label": "Zeitüberschreitung bei der Rückgabe", + "description": "Warte nach dem Verlust der Verfolgung so viele Sekunden, bevor die Kamera in die voreingestellte Position zurückkehrt." + }, + "enabled_in_config": { + "label": "Ursprünglicher Autotrack-Status", + "description": "Internes Feld zur Erfassung, ob die automatische Nachführung in der Konfiguration aktiviert wurde." + } + }, + "label": "ONVIF", + "description": "ONVIF-Verbindung und Einstellungen für die automatische PTZ-Verfolgung dieser Kamera.", + "host": { + "label": "ONVIF Host", + "description": "Host (und optional Schema) für den ONVIF-Dienst dieser Kamera." + }, + "port": { + "label": "ONVIF Port", + "description": "Portnummer für den ONVIF-Dienst." + }, + "user": { + "label": "ONVIF-Benutzername", + "description": "Benutzername für die ONVIF-Authentifizierung; bei einigen Geräten ist für ONVIF ein Admin-Benutzer erforderlich." + }, + "password": { + "label": "ONVIF-Passwort", + "description": "Passwort für die ONVIF-Authentifizierung." + }, + "tls_insecure": { + "label": "TLS-Überprüfung deaktivieren", + "description": "TLS-Überprüfung überspringen und Digest-Authentifizierung für ONVIF deaktivieren (unsicher; nur in sicheren Netzwerken verwenden)." + }, + "ignore_time_mismatch": { + "label": "Zeitabweichung ignorieren", + "description": "Ignoriere Zeitunterschiede zwischen Kamera und Frigate-Server bei der ONVIF-Kommunikation." + }, + "profile": { + "label": "ONVIF Profile", + "description": "Spezifisches ONVIF-Medienprofil für die PTZ-Steuerung, das anhand eines Tokens oder Namens ausgewählt wird. Ist kein Profil festgelegt, wird automatisch das erste Profil mit gültiger PTZ-Konfiguration ausgewählt." + } + }, + "semantic_search": { + "label": "Semantische Suche", + "description": "Einstellungen für die semantische Suche, die Objekt-Embeddings erstellt und abfragt, um ähnliche Elemente zu finden.", + "triggers": { + "label": "Auslöser", + "description": "Aktionen und Übereinstimmungskriterien für kameraspezifische Auslöser der semantischen Suche.", + "friendly_name": { + "label": "Anzeigename", + "description": "Optionaler beschreibender Name, der in der Benutzeroberfläche für diesen Trigger angezeigt wird." + }, + "enabled": { + "label": "Diesen Trigger aktivieren", + "description": "Diesen Trigger für die semantische Suche aktivieren oder deaktivieren." + }, + "type": { + "label": "Auslöseart", + "description": "Auslösertyp: „thumbnail“ (Abgleich mit Bild) oder „description“ (Abgleich mit Text)." + }, + "data": { + "label": "Inhalt anzeigen", + "description": "Textphrase oder Miniaturbild-ID, die mit den verfolgten Objekten abgeglichen werden soll." + }, + "threshold": { + "label": "Auslöseschwelle", + "description": "Erforderlicher Mindestähnlichkeitswert (0–1) zur Aktivierung dieses Triggers." + }, + "actions": { + "label": "Trigger-Aktionen", + "description": "Liste der Aktionen, die ausgeführt werden sollen, wenn der Trigger ausgelöst wird (Benachrichtigung, Unterbezeichnung, Attribut)." + } + } + }, + "ui": { + "label": "Kamera UI", + "description": "Legen Sie die Reihenfolge und Sichtbarkeit dieser Kamera in der Benutzeroberfläche fest. Die Reihenfolge wirkt sich auf das Standard-Dashboard aus. Für eine detailliertere Steuerung verwenden Sie Kameragruppen.", + "order": { + "label": "UI Reihenfolge", + "description": "Numerische Reihenfolge, nach der die Kamera in der Benutzeroberfläche sortiert wird (Standard-Dashboard und Listen); höhere Zahlen erscheinen später." + }, + "dashboard": { + "label": "In der Benutzeroberfläche anzeigen", + "description": "Schalte ein, ob diese Kamera überall in der Benutzeroberfläche von „Frigate“ sichtbar ist. Wenn du diese Option deaktivierst, musst du die Konfiguration manuell bearbeiten, um diese Kamera wieder in der Benutzeroberfläche anzuzeigen." + }, + "review": { + "label": "In der Überprüfung anzeigen", + "description": "Legen Sie fest, ob diese Kamera in der Übersicht angezeigt wird (auf der Übersichtsseite sowie im Kamerafilter, in der Bewegungsübersicht und in der Verlaufsansicht)." + } + }, + "snapshots": { + "label": "Schnappschüsse", + "description": "Einstellungen für API-generierte Momentaufnahmen der erfassten Objekte für diese Kamera.", + "enabled": { + "label": "Schnappschüsse aktivieren", + "description": "Das Speichern von Momentaufnahmen für diese Kamera aktivieren oder deaktivieren." + }, + "clean_copy": { + "label": "Saubere Kopie speichern", + "description": "Save an unannotated clean copy of snapshots in addition to annotated ones." + }, + "timestamp": { + "label": "Zeitstempel-Einblendung", + "description": "Füge einen Zeitstempel auf die von der API abgerufenen Momentaufnahmen ein." + }, + "bounding_box": { + "label": "Einblendung der Begrenzungsrahmen", + "description": "Zeichne Begrenzungsrahmen für verfolgte Objekte auf Momentaufnahmen aus der API." + }, + "crop": { + "label": "Ertragsübersicht", + "description": "Schnappschüsse aus der API auf die Begrenzungsrahmen der erkannten Objekte zuschneiden." + }, + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Bereiche, die ein Objekt betreten muss, damit ein Schnappschuss gespeichert wird." + }, + "height": { + "label": "Höhe der Momentaufnahme", + "description": "Höhe (Pixel), auf die Schnappschüsse über die API skaliert werden sollen; leer lassen, um die Originalgröße beizubehalten." + }, + "retain": { + "label": "Aufbewahrungsdauer von Snapshots", + "description": "Aufbewahrungseinstellungen für Snapshots, einschließlich Standarddauer in Tagen und objektspezifischer Überschreibungen.", + "default": { + "label": "Standard-Aufbewahrungsfrist", + "description": "Standardmäßige Anzahl von Tagen, für die Snapshots aufbewahrt werden." + }, + "mode": { + "label": "Speichermodus", + "description": "Speichermodus: „all“ (alle Segmente speichern), „motion“ (Segmente mit Bewegung speichern) oder „active_objects“ (Segmente mit aktiven Objekten speichern)." + }, + "objects": { + "label": "Objektaufbewahrung", + "description": "Objektbezogene Überschreibungen für die Aufbewahrungsdauer von Snapshots." + } + }, + "quality": { + "label": "Qualität der Momentaufnahme", + "description": "Codierungsqualität für gespeicherte Momentaufnahmen (0–100)." + } + }, + "timestamp_style": { + "label": "Format für Zeitstempel", + "description": "Gestaltungsoptionen für Zeitstempel, die auf Momentaufnahmen und die Debug-Ansicht angewendet werden.", + "position": { + "label": "Position des Zeitstempels", + "description": "Position des Zeitstempels auf dem Bild (tl/tr/bl/br)." + }, + "format": { + "label": "Zeitstempelformat", + "description": "Datums- und Uhrzeitformatzeichenfolge für Zeitstempel (Python-Datums- und Uhrzeitformatcodes)." + }, + "color": { + "label": "Farbe des Zeitstempels", + "description": "RGB-Farbwerte für den Zeitstempeltext (alle Werte zwischen 0 und 255).", + "red": { + "label": "Rot", + "description": "Rotwert (0–255) für die Farbe des Zeitstempels." + }, + "green": { + "label": "Grün", + "description": "Grünanteil (0–255) für die Farbe des Zeitstempels." + }, + "blue": { + "label": "Blau", + "description": "Blauer Farbanteil (0–255) für die Farbe des Zeitstempels." + } + }, + "thickness": { + "label": "Stärke der Zeitmarke", + "description": "Linienstärke des Zeitstempeltextes." + }, + "effect": { + "label": "Zeitstempeleffekt", + "description": "Visuelle Darstellung des Zeitstempeltextes (keine, durchgehend, Schatten)." + } + }, + "best_image_timeout": { + "label": "Optimale Zeitüberschreitung für Bilder", + "description": "Wie lange soll man auf das Bild mit dem höchsten Konfidenzwert warten?" + }, + "type": { + "label": "Kameratyp", + "description": "Kameratyp" + }, + "webui_url": { + "label": "URL der Kamera", + "description": "URL, um die Kamera direkt von der Systemseite aus aufzurufen" + }, + "profiles": { + "label": "Profile", + "description": "Benannte Konfigurationsprofile mit teilweisen Überschreibungen, die zur Laufzeit aktiviert werden können." + }, + "zones": { + "label": "Zonen", + "description": "Mit Zonen können Sie einen bestimmten Bereich des Bildausschnitts festlegen, um zu bestimmen, ob sich ein Objekt innerhalb dieses Bereichs befindet oder nicht.", + "friendly_name": { + "label": "Zonen Name", + "description": "Ein benutzerfreundlicher Name für die Zone, der in der Benutzeroberfläche von Frigate angezeigt wird. Wenn kein Name festgelegt ist, wird eine formatierte Version des Zonennamens verwendet." + }, + "enabled": { + "label": "Aktiviert", + "description": "Diese Zone aktivieren oder deaktivieren. Deaktivierte Zonen werden zur Laufzeit ignoriert." + }, + "enabled_in_config": { + "label": "Behalten Sie den ursprünglichen Zustand der Zone im Blick." + }, + "filters": { + "label": "Zonenfilter", + "description": "Filter, die auf Objekte innerhalb dieser Zone angewendet werden sollen. Dienen dazu, Fehlalarme zu reduzieren oder einzuschränken, welche Objekte als in der Zone vorhanden gelten.", + "min_area": { + "label": "Mindestfläche des Objekts", + "description": "Mindestfläche der Begrenzungsbox (in Pixeln oder Prozent), die für diesen Objekttyp erforderlich ist. Kann als Pixelwert (Ganzzahl) oder als Prozentwert (Gleitkomma zwischen 0,000001 und 0,99) angegeben werden." + }, + "max_area": { + "label": "Maximale Objektfläche", + "description": "Maximal zulässige Fläche der Begrenzungsbox (in Pixeln oder Prozent) für diesen Objekttyp. Kann als Pixelwert (Ganzzahl) oder als Prozentwert (Gleitkomma zwischen 0,000001 und 0,99) angegeben werden." + }, + "min_ratio": { + "label": "Mindestseitenverhältnis", + "description": "Erforderliches Mindestverhältnis von Breite zu Höhe, damit die Begrenzungsbox die Voraussetzungen erfüllt." + }, + "max_ratio": { + "label": "Maximales Seitenverhältnis", + "description": "Maximales Seitenverhältnis: Das maximal zulässige Verhältnis von Breite zu Höhe, damit die Begrenzungsbox die Anforderungen erfüllt.Maximales Seitenverhältnis: Das maximal zulässige Verhältnis von Breite zu Höhe, damit die Begrenzungsbox die Anforderungen erfüllt." + }, + "threshold": { + "label": "Konfidenzschwelle", + "description": "Durchschnittlicher Schwellenwert für die Erkennungssicherheit, der erforderlich ist, damit das Objekt als echtes Positiv gewertet wird." + }, + "min_score": { + "label": "Mindestvertrauen", + "description": "Erforderliche Mindestzuverlässigkeit der Einzelbilderkennung, damit das Objekt gezählt wird." + }, + "mask": { + "label": "Filtermaske", + "description": "Polygonkoordinaten, die festlegen, wo dieser Filter innerhalb des Bildausschnitts angewendet wird." + }, + "raw_mask": { + "label": "Rohmaske" + } + }, + "coordinates": { + "label": "Koordinaten", + "description": "Polygonkoordinaten, die den Bereich der Zone definieren. Dies kann eine durch Kommas getrennte Zeichenfolge oder eine Liste von Koordinatenzeichenfolgen sein. Die Koordinaten sollten relativ (0–1) oder absolut (veraltet) sein." + }, + "distances": { + "label": "Entfernungen in der realen Welt", + "description": "Optionale reale Entfernungen für jede Seite des Zonenvierecks, die für Geschwindigkeits- oder Entfernungsberechnungen verwendet werden. Bei Angabe müssen genau 4 Werte angegeben werden." + }, + "inertia": { + "label": "Inertialkoordinatensysteme", + "description": "Anzahl der aufeinanderfolgenden Bilder, in denen ein Objekt in der Zone erkannt werden muss, bevor es als vorhanden gilt. Dies hilft dabei, vorübergehende Erkennungen herauszufiltern." + }, + "loitering_time": { + "label": "Verzögerungszeit in Sekunden", + "description": "Anzahl der Sekunden, die sich ein Objekt in der Zone aufhalten muss, damit es als „Herumlungern“ gewertet wird. Setzen Sie den Wert auf 0, um die Erkennung von Herumlungern zu deaktivieren." + }, + "speed_threshold": { + "label": "Mindestgeschwindigkeit", + "description": "Mindestgeschwindigkeit (in realen Einheiten, sofern Entfernungen festgelegt sind), die erforderlich ist, damit ein Objekt als in der Zone vorhanden gilt. Wird für geschwindigkeitsbasierte Zonenauslöser verwendet." + }, + "objects": { + "label": "Auslöseobjekte", + "description": "Liste der Objekttypen (aus labelmap), die diese Zone auslösen können. Kann eine Zeichenkette oder eine Liste von Zeichenketten sein. Ist das Feld leer, werden alle Objekte berücksichtigt." + } + }, + "enabled_in_config": { + "label": "Ursprünglicher Zustand der Kamera", + "description": "Behalten Sie den ursprünglichen Zustand der Kamera." + } +} diff --git a/web/public/locales/de/config/global.json b/web/public/locales/de/config/global.json new file mode 100644 index 0000000..785a503 --- /dev/null +++ b/web/public/locales/de/config/global.json @@ -0,0 +1,1943 @@ +{ + "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, startet Frigate im abgesicherten Modus mit reduzierten Features für die Fehlersuche." + }, + "audio": { + "label": "Audioereignisse", + "enabled": { + "label": "Aktivieren der Audioerkennung", + "description": "Aktivieren oder deaktivieren Sie die Erkennung von Audioereignissen für alle Kameras; diese Einstellung kann für jede Kamera individuell überschrieben werden." + }, + "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, Gespräche, Rufen).", + "label": "Hörtypen" + }, + "filters": { + "label": "Audiofilter", + "description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden.", + "threshold": { + "label": "Mindestvertrauensgrad für Audio", + "description": "Mindestschwellenwert für die Zuverlässigkeit, damit das Audioereignis gezählt wird." + } + }, + "max_not_heard": { + "label": "Ende Timeout", + "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." + }, + "description": "Einstellungen für die audiobasierte Ereigniserkennung für alle Kameras; können für jede Kamera individuell überschrieben werden." + }, + "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": "Protokollierungsstufe", + "description": "Standardmäßige globale Log-Ausführlichkeit (Debug, Info, Warnung, Fehler)." + }, + "logs": { + "label": "Prozessspezifische Log-Stufe", + "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." + }, + "enabled": { + "label": "Audio-Transkription aktivieren", + "description": "Automatische Audio-Transkription für alle Kameras aktivieren oder deaktivieren; kann für jede Kamera einzeln überschrieben werden." + }, + "language": { + "label": "Transkriptsprache", + "description": "Für die Transkription/Übersetzung verwendeter Sprachcode (z. B. „en“ für Englisch). Eine Liste der unterstützten Sprachcodes finden Sie unter https://whisper-api.com/docs/languages/." + }, + "device": { + "label": "Transkriptionsgerät", + "description": "Geräteschlüssel (CPU/GPU), auf dem das Transkriptionsmodell ausgeführt werden soll. Derzeit werden für die Transkription nur NVIDIA-CUDA-GPUs unterstützt." + }, + "model_size": { + "label": "Modellgröße", + "description": "Modellgröße für die Transkription von Audioereignissen im Offline-Modus." + } + }, + "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 Reihenfolge der Kamera im Birdseye-Layout steuert." + }, + "restream": { + "label": "Restream RTSP", + "description": "Leiten Sie den Birdseye-Ausgang als RTSP-Feed weiter; wenn Sie diese Option aktivieren, läuft Birdseye ununterbrochen weiter." + }, + "width": { + "label": "Breite", + "description": "Ausgabebreite (Pixel) des zusammengesetzten Birdseye-Bildes." + }, + "height": { + "label": "Höhe", + "description": "Ausgabehöhe (in Pixeln) des zusammengesetzten Birdseye-Bildes." + }, + "quality": { + "label": "Codierungsqualität", + "description": "Codierungsqualität für den Birdseye-MPEG-1-Feed (1 = höchste Qualität, 31 = niedrigste Qualität)." + }, + "inactivity_threshold": { + "label": "Schwellenwert für Inaktivität", + "description": "Sekunden der Inaktivität, nach denen eine Kamera nicht mehr in Birdseye angezeigt wird." + }, + "layout": { + "label": "Layout", + "description": "Layoutoptionen für die Birdseye-Komposition.", + "scaling_factor": { + "label": "Skalierungsfaktor", + "description": "Vom Layout-Rechner verwendeter Skalierungsfaktor (Bereich 1,0 bis 5,0)." + }, + "max_cameras": { + "label": "Max. Anzahl Kameras", + "description": "Maximale Anzahl der Kameras, die gleichzeitig in Birdseye angezeigt werden können; es werden die neuesten Kameras angezeigt." + } + }, + "idle_heartbeat_fps": { + "label": "FPS im Leerlauf", + "description": "Bilder pro Sekunde, um das zuletzt erstellte Birdseye-Bild im Leerlauf erneut zu senden; auf 0 setzen, um die Funktion zu deaktivieren." + } + }, + "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", + "description": "Dateisystempfad, in dem die Frigate-SQLite-Datenbankdatei gespeichert wird." + } + }, + "detect": { + "label": "Objekterkennung", + "description": "Einstellungen für die Erkennungs-/Detektionsrolle, die zum Ausführen der Objekterkennung und zum Initialisieren von Trackern verwendet wird.", + "enabled": { + "label": "Objekterkennung aktiviert", + "description": "Objekterkennung für alle Kameras aktivieren oder deaktivieren; kann für jede Kamera einzeln überschrieben werden." + }, + "height": { + "label": "Höhe erkennen", + "description": "Höhe (Pixel) der für den Erkennungsstrom verwendeten Frames; leer lassen, um die native Stream-Auflösung zu verwenden." + }, + "width": { + "label": "Breite erkennen", + "description": "Breite (Pixel) der für den Erkennungsstrom verwendeten Frames; leer lassen, um die native Stream-Auflösung zu verwenden." + }, + "fps": { + "label": "FPS erkennen", + "description": "Gewünschte Bilder pro Sekunde für die Erkennung; niedrigere Werte reduzieren die CPU-Auslastung (empfohlener Wert ist 5, höhere Werte – maximal 10 – nur bei der Verfolgung extrem schnell bewegter Objekte einstellen)." + }, + "min_initialized": { + "label": "Mindestanzahl an Initialisierungsframes", + "description": "Anzahl der aufeinanderfolgenden Erkennungserfolge, die vor der Erstellung eines verfolgten Objekts erforderlich sind. Erhöhen Sie diesen Wert, um Fehlinitialisierungen zu reduzieren. Der Standardwert ist fps geteilt durch 2." + }, + "max_disappeared": { + "label": "Maximale Anzahl fehlender Frames", + "description": "Anzahl der Frames ohne Erkennung, bevor ein verfolgtes Objekt als verschwunden gilt." + }, + "stationary": { + "label": "Konfiguration stationärer Objekte", + "description": "Einstellungen zum Erkennen und Verwalten von Objekten, die über einen bestimmten Zeitraum hinweg unbeweglich bleiben.", + "interval": { + "label": "Stationäres Intervall", + "description": "Wie oft (in Frames) soll eine Erkennungsprüfung durchgeführt werden, um ein stationäres Objekt zu bestätigen." + }, + "threshold": { + "label": "Stationäre Schwelle", + "description": "Anzahl der Frames ohne Positionsänderung, die erforderlich sind, um ein Objekt als stationär zu markieren." + }, + "max_frames": { + "label": "Maximale Bildanzahl", + "description": "Begrenzt, wie lange stationäre Objekte verfolgt werden, bevor sie verworfen werden.", + "default": { + "label": "Standardmäßige maximale Frames", + "description": "Standardmäßige maximale Anzahl von Frames, die ein stationäres Objekt verfolgt werden sollen, bevor die Verfolgung beendet wird." + }, + "objects": { + "label": "Objekt max Rahmen", + "description": "Objektbezogene Überschreibungen für maximale Frames zur Verfolgung stationärer Objekte." + } + }, + "classifier": { + "description": "Verwenden Sie einen visuellen Klassifikator, um wirklich stationäre Objekte auch dann zu erkennen, wenn die Begrenzungsrahmen flackern.", + "label": "Visuellen Klassifikator aktivieren" + } + }, + "annotation_offset": { + "label": "Anmerkung Offset", + "description": "Millisekunden zur Verschiebung der Anmerkungen, um die Begrenzungsrahmen der Zeitleiste besser an die Aufnahmen anzupassen; kann positiv oder negativ sein." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "Einstellungen für den integrierten go2rtc-Restreaming-Dienst, der für die Weiterleitung und Übersetzung von Live-Streams verwendet wird." + }, + "mqtt": { + "label": "mqtt", + "description": "Einstellungen für die Verbindung und Veröffentlichung von Telemetriedaten, Momentaufnahmen und Ereignisdetails an einen MQTT-Broker.", + "enabled": { + "label": "mqtt aktiviert", + "description": "Aktivieren oder deaktivieren Sie die MQTT-Integration für Status, Ereignisse und Momentaufnahmen." + }, + "host": { + "label": "mqtt Host", + "description": "Hostname oder IP-Adresse des MQTT-Brokers." + }, + "port": { + "label": "mqtt Port", + "description": "Port des MQTT-Brokers (normalerweise 1883 für einfaches MQTT)." + }, + "topic_prefix": { + "label": "Themenpräfix", + "description": "MQTT-Themenpräfix für alle Frigate-Themen; muss bei Ausführung mehrerer Instanzen eindeutig sein." + }, + "client_id": { + "label": "Klient ID", + "description": "Klient-Kennung, die bei der Verbindung mit dem MQTT-Broker verwendet wird; sollte pro Instanz eindeutig sein." + }, + "stats_interval": { + "label": "Statistikintervall", + "description": "Intervall in Sekunden für die Veröffentlichung von System- und Kamerastatistiken an MQTT." + }, + "user": { + "label": "mqtt Benutzername", + "description": "Optionaler MQTT-Benutzername; kann über Umgebungsvariablen oder Geheimnisse bereitgestellt werden." + }, + "password": { + "label": "mqtt Passwort", + "description": "Optionales MQTT-Passwort; kann über Umgebungsvariablen oder Geheimnisse bereitgestellt werden." + }, + "tls_ca_certs": { + "label": "TLS CA certs", + "description": "Pfad zum CA-Zertifikat für TLS-Verbindungen zum Broker (für selbstsignierte Zertifikate)." + }, + "tls_client_cert": { + "label": "Klient Zertifikat", + "description": "Client-Zertifikatpfad für die gegenseitige TLS-Authentifizierung; bei Verwendung von Client-Zertifikaten keine Benutzerdaten/Passwörter festlegen." + }, + "tls_client_key": { + "label": "Klient Schlüssel", + "description": "Pfad zum privaten Schlüssel für das Client-Zertifikat." + }, + "tls_insecure": { + "label": "TLS unsicher", + "description": "Unsichere TLS-Verbindungen zulassen, indem die Hostnamenüberprüfung übersprungen wird (nicht empfohlen)." + }, + "qos": { + "label": "mqtt Qos", + "description": "Servicequalitätsstufe für MQTT-Veröffentlichungen/Abonnements (0, 1 oder 2)." + } + }, + "face_recognition": { + "label": "Gesichtserkennung", + "enabled": { + "label": "Gesichtserkennung aktivieren", + "description": "Gesichtserkennung für alle Kameras aktivieren oder deaktivieren; kann für jede Kamera einzeln überschrieben werden." + }, + "min_area": { + "label": "Mindestfläche der Stirnseite", + "description": "Mindestfläche (Pixel) eines erkannten Gesichtsrahmens, die für einen Erkennungsversuch erforderlich ist." + }, + "description": "Einstellungen für die Gesichtserkennung und -identifizierung für alle Kameras; können für jede Kamera individuell angepasst werden.", + "model_size": { + "label": "Modellgröße", + "description": "Zu verwendende Modellgröße für Gesichts-Embeddings (klein/groß); bei größeren Modellen ist möglicherweise eine GPU erforderlich." + }, + "unknown_score": { + "label": "Unbekannter Schwellenwert", + "description": "Abstandsschwelle, unterhalb derer ein Gesicht als potenzielle Übereinstimmung angesehen wird (höher = strenger)." + }, + "detection_threshold": { + "label": "Erkennungsschwelle", + "description": "Mindestvertrauensgrad, der erforderlich ist, damit eine Gesichtserkennung als gültig angesehen wird." + }, + "recognition_threshold": { + "label": "Erkennungsschwelle", + "description": "Schwellenwert für den Abstand bei der Gesichts-Einbettung, ab dem zwei Gesichter als übereinstimmend gelten." + }, + "min_faces": { + "label": "Mindestens Gesichter", + "description": "Mindestanzahl an Gesichtserkennungen, die erforderlich sind, bevor einer Person ein erkanntes Unterlabel zugewiesen wird." + }, + "save_attempts": { + "label": "Speicherungen", + "description": "Anzahl der Gesichtserkennungsversuche, die für die Benutzeroberfläche zur aktuellen Erkennung gespeichert werden sollen." + }, + "blur_confidence_filter": { + "label": "Weichzeichnungsfilter", + "description": "Passen Sie die Konfidenzwerte anhand der Bildunschärfe an, um Fehlalarme bei Gesichtern von schlechter Qualität zu reduzieren." + }, + "device": { + "label": "Gerät", + "description": "Dies ist eine Übersteuerung, um ein bestimmtes Gerät anzusprechen. Weitere Informationen finden Sie unter https://onnxruntime.ai/docs/execution-providers/" + } + }, + "notifications": { + "label": "Benachrichtigung", + "description": "Einstellungen zum Aktivieren und Steuern von Benachrichtigungen für alle Kameras; können pro Kamera überschrieben werden.", + "enabled": { + "label": "Benachrichtigungen aktivieren", + "description": "Benachrichtigungen für alle Kameras aktivieren oder deaktivieren; kann pro Kamera überschrieben werden." + }, + "email": { + "label": "Benachrichtigungs-E-Mail", + "description": "E-Mail-Adresse, die für Push-Benachrichtigungen verwendet wird oder von bestimmten Benachrichtigungsanbietern verlangt wird." + }, + "cooldown": { + "label": "Abkühlungsphase", + "description": "Abkühlungszeit (Sekunden) zwischen Benachrichtigungen, um Spam an Empfänger zu vermeiden." + }, + "enabled_in_config": { + "label": "Ursprüngliche Meldungen geben an", + "description": "Gibt an, ob Benachrichtigungen in der ursprünglichen statischen Konfiguration aktiviert waren." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg-Einstellungen, einschließlich Binärpfad, Argumente, hwaccel-Optionen und rollenspezifische Ausgabeargumente.", + "path": { + "label": "FFmpeg-Pfad", + "description": "Pfad zur zu verwendenden FFmpeg-Binärdatei oder ein Versionsalias („7.0” oder „8.0”)." + }, + "global_args": { + "label": "Globale Argumente von FFmpeg", + "description": "An FFmpeg-Prozesse übergebene globale Argumente." + }, + "hwaccel_args": { + "label": "Argumente für Hardwarebeschleunigung", + "description": "Hardwarebeschleunigungsargumente für FFmpeg. Es werden providerspezifische Voreinstellungen empfohlen." + }, + "input_args": { + "label": "Eingabeargumente", + "description": "Eingabeargumente, die auf FFmpeg-Eingabestreams angewendet werden." + }, + "output_args": { + "label": "Ausgabeargumente", + "description": "Standardausgabeargumente, die für verschiedene FFmpeg-Rollen wie „detect“ und „record“ verwendet werden.", + "detect": { + "label": "Ausgabeargumente erkennen", + "description": "Standardausgabeargumente für das Erkennen von Rollenströmen." + }, + "record": { + "label": "Ausgabeargumente aufzeichnen", + "description": "Standardausgabeargumente für Datensatzrollen-Streams." + } + }, + "retry_interval": { + "label": "FFmpeg-Wiederholungszeit", + "description": "Sekunden, die gewartet werden sollen, bevor nach einem Fehler erneut versucht wird, eine Kamera-Übertragung herzustellen. Der Standardwert ist 10." + }, + "apple_compatibility": { + "label": "Apple-Kompatibilität", + "description": "Aktivieren Sie die HEVC-Kennzeichnung für eine bessere Kompatibilität mit Apple-Playern bei der Aufnahme von H.265." + }, + "gpu": { + "label": "GPU-Index", + "description": "Standard-GPU-Index, der für die Hardwarebeschleunigung verwendet wird, sofern verfügbar." + }, + "inputs": { + "label": "Kameraeingänge", + "description": "Liste der Eingangsstromdefinitionen (Pfade und Rollen) für diese Kamera.", + "path": { + "label": "Eingabepfad", + "description": "URL oder Pfad des Kameraeingangsstroms." + }, + "roles": { + "label": "Eingangsrollen", + "description": "Rollen für diesen Eingabestrom." + }, + "global_args": { + "label": "Globale Argumente von FFmpeg", + "description": "Globale Argumente von FFmpeg für diesen Eingabestrom." + }, + "hwaccel_args": { + "label": "Argumente für Hardwarebeschleunigung", + "description": "Hardwarebeschleunigungsargumente für diesen Eingabestrom." + }, + "input_args": { + "label": "Eingabeargumente", + "description": "Für diesen Stream spezifische Eingabeargumente." + } + } + }, + "networking": { + "label": "Vernetzung", + "description": "Netzwerkbezogene Einstellungen wie die Aktivierung von IPv6 für Frigate-Endpunkte.", + "ipv6": { + "label": "IPv6-Konfiguration", + "description": "IPv6-spezifische Einstellungen für Frigate-Netzwerkdienste.", + "enabled": { + "label": "IPv6 aktivieren", + "description": "Aktivieren Sie die IPv6-Unterstützung für Frigate-Dienste (API und Benutzeroberfläche), wo dies möglich ist." + } + }, + "listen": { + "label": "Konfiguration der Listening-Ports", + "description": "Konfiguration für interne und externe Listening-Ports. Dies ist für fortgeschrittene Benutzer gedacht. Für die meisten Anwendungsfälle wird empfohlen, den Abschnitt „Ports“ Ihrer Docker-Compose-Datei zu ändern.", + "internal": { + "label": "interne port", + "description": "Interner Listening-Port für Frigate (Standard 5000)." + }, + "external": { + "label": "Externer Anschluss", + "description": "Externer Listening-Port für Frigate (Standard 8971)." + } + } + }, + "proxy": { + "label": "Proxy", + "description": "Einstellungen für die Integration von Frigate hinter einem Reverse-Proxy, der authentifizierte Benutzer-Header weiterleitet.", + "header_map": { + "label": "Header-Zuordnung", + "description": "Ordnen Sie eingehende Proxy-Header den Frigate-Benutzer- und Rollenfeldern für die proxybasierte Authentifizierung zu.", + "user": { + "label": "Benutzerkopfzeile", + "description": "Header, der den vom Upstream-Proxy bereitgestellten authentifizierten Benutzernamen enthält." + }, + "role": { + "label": "Rollenüberschrift", + "description": "Header, der die Rolle oder Gruppen des authentifizierten Benutzers aus dem Upstream-Proxy enthält." + }, + "role_map": { + "label": "Rollenabbildung", + "description": "Ordnen Sie die Werte der Upstream-Gruppe den Frigate-Rollen zu (z. B. ordnen Sie Admin-Gruppen der Admin-Rolle zu)." + } + }, + "logout_url": { + "label": "Abmelde-URL", + "description": "URL, zu der Benutzer beim Abmelden über den Proxy weitergeleitet werden sollen." + }, + "auth_secret": { + "label": "Proxy-Geheimnis", + "description": "Optionales Geheimnis, das anhand des X-Proxy-Secret-Headers überprüft wird, um vertrauenswürdige Proxys zu verifizieren." + }, + "default_role": { + "label": "Standardrolle", + "description": "Standardrolle, die proxy-authentifizierten Benutzern zugewiesen wird, wenn keine Rollenzuordnung vorliegt." + }, + "separator": { + "label": "Trennzeichen", + "description": "Zeichen, das zum Trennen mehrerer Werte in Proxy-Headern verwendet wird." + } + }, + "live": { + "label": "Live-Wiedergabe", + "streams": { + "label": "Live-Stream Namen", + "description": "Zuordnung der konfigurierten Stream-Namen zu den für die Live-Wiedergabe verwendeten Restream-/Go2rtc-Namen." + }, + "height": { + "label": "Live-Höhe", + "description": "Höhe (Pixel) zum Rendern des jsmpeg-Livestreams in der Web-Benutzeroberfläche; muss <= Stream-Höhe sein." + }, + "quality": { + "label": "Live Qualität", + "description": "Kodierungsqualität für den jsmpeg-Stream (1 = höchst, 31 = niedrigst)." + }, + "description": "Einstellungen zur Steuerung der Auflösung und Qualität des jsmpeg-Livestreams. Dies hat keine Auswirkungen auf weitergeleitete Kameras, die go2rtc für die Live-Ansicht verwenden." + }, + "telemetry": { + "label": "Telemetrie", + "description": "Systemtelemetrie- und Statistikoptionen, einschließlich Überwachung der GPU- und Netzwerkbandbreite.", + "network_interfaces": { + "label": "Netzwerkschnittstellen", + "description": "Liste der Präfixe für Netzwerkschnittstellennamen, die für Bandbreitenstatistiken überwacht werden sollen." + }, + "stats": { + "label": "Systemstatistiken", + "description": "Optionen zum Aktivieren/Deaktivieren der Erfassung verschiedener System- und GPU-Statistiken.", + "amd_gpu_stats": { + "label": "AMD GPU Statistik", + "description": "Aktivieren Sie die Erfassung von AMD-GPU-Statistiken, wenn eine AMD-GPU vorhanden ist." + }, + "intel_gpu_stats": { + "label": "Intel GPU Statistik", + "description": "Aktivieren Sie die Erfassung von Intel-GPU-Statistiken, wenn eine Intel-GPU vorhanden ist." + }, + "network_bandwidth": { + "label": "Netzwerk Bandbreite", + "description": "Aktivieren Sie die prozessbezogene Überwachung der Netzwerkbandbreite für Kamera-FFmpeg-Prozesse und Detektoren (erfordert entsprechende Funktionen)." + }, + "intel_gpu_device": { + "label": "Intel GPU", + "description": "PCI-Bus-Adresse oder DRM-Gerätepfad (z. B. /dev/dri/card1), der verwendet wird, um die Intel-GPU-Statistiken einem bestimmten Gerät zuzuordnen, wenn mehrere vorhanden sind." + } + }, + "version_check": { + "label": "Versionscheck", + "description": "Aktivieren Sie eine Outbound-Prüfung, um festzustellen, ob eine neuere Version von Frigate verfügbar ist." + } + }, + "lpr": { + "label": "Kennzeichenerkennung", + "description": "Einstellungen für die Kennzeichenerkennung, einschließlich Erkennungsschwellen, Formatierung und bekannte Kennzeichen.", + "enabled": { + "label": "LPR aktivieren", + "description": "Die Kennzeichenerkennung für alle Kameras aktivieren oder deaktivieren; die Einstellung kann für jede Kamera individuell überschrieben werden." + }, + "expire_time": { + "label": "Sekunden bis zum Ablauf", + "description": "Zeit in Sekunden, nach der ein nicht erkanntes Kennzeichen aus dem Tracker gelöscht wird (nur für dedizierte LPR-Kameras)." + }, + "min_area": { + "label": "Mindestplattenfläche", + "description": "Mindestplattenfläche (Pixel), die für einen Erkennungsversuch erforderlich ist." + }, + "enhancement": { + "label": "Verbesserungsgrad", + "description": "Verstärkungsstufe (0-10) zur Anwendung auf Plattenaufnahmen vor der OCR; höhere Werte führen nicht immer zu besseren Ergebnissen, Stufen über 5 funktionieren möglicherweise nur bei Nachtaufnahmen und sollten mit Vorsicht verwendet werden." + }, + "model_size": { + "label": "Modellgröße", + "description": "Für die Texterkennung verwendete Modellgröße. Die meisten Benutzer sollten „klein“ wählen." + }, + "detection_threshold": { + "label": "Erkennungsschwelle", + "description": "Schwellenwert für die Erkennungssicherheit, ab dem die OCR-Erkennung für ein verdächtiges Kennzeichen gestartet wird." + }, + "recognition_threshold": { + "label": "Erkennungsschwelle", + "description": "Schwellwert für die Erkennungssicherheit, der erforderlich ist, damit der erkannte Text des Kennzeichens als Unterbezeichnung hinzugefügt wird." + }, + "min_plate_length": { + "label": "Mindestplattenlänge", + "description": "Mindestanzahl an Zeichen, die ein erkanntes Kennzeichen enthalten muss, um als gültig zu gelten." + }, + "format": { + "label": "Regulärer Ausdruck für das Plattenformat", + "description": "Optionaler regulärer Ausdruck zur Überprüfung der erkannten Kennzeichenfolgen auf Übereinstimmung mit einem erwarteten Format." + }, + "match_distance": { + "label": "Entfernung", + "description": "Anzahl der zulässigen Zeichenabweichungen beim Vergleich erkannter Kennzeichen mit bekannten Kennzeichen." + }, + "known_plates": { + "label": "Bekannte Schilder", + "description": "Liste der Kennzeichen oder regulären Ausdrücke, die besonders überwacht oder gemeldet werden sollen." + }, + "debug_save_plates": { + "label": "Debug-Platten speichern", + "description": "Speichern Sie Ausschnitte aus den Plattenbildern zur Fehlerbehebung bei der LPR-Leistung." + }, + "device": { + "label": "Gerät", + "description": "Dies ist eine Übersteuerung, um ein bestimmtes Gerät anzusprechen. Weitere Informationen finden Sie unter https://onnxruntime.ai/docs/execution-providers/" + }, + "replace_rules": { + "label": "Ersatzregeln", + "description": "Reguläre Ausdrücke, die zur Normalisierung der erkannten Kennzeichen vor dem Abgleich verwendet werden.", + "pattern": { + "label": "Regex-Muster" + }, + "replacement": { + "label": "Ersetzungs String" + } + } + }, + "motion": { + "label": "Bewegungserkennung", + "enabled": { + "label": "Bewegungserkennung aktivieren", + "description": "Bewegungserkennung für alle Kameras aktivieren oder deaktivieren; kann für jede Kamera einzeln überschrieben werden." + }, + "threshold": { + "label": "Bewegungsschwelle", + "description": "Vom Bewegungsmelder verwendeter Schwellenwert für Pixelunterschiede; höhere Werte verringern die Empfindlichkeit (Bereich 1–255)." + }, + "lightning_threshold": { + "label": "Blitzschwelle", + "description": "Schwellenwert zum Erkennen und Ignorieren kurzer Beleuchtungsspitzen (niedrigerer Wert bedeutet höhere Empfindlichkeit, Werte zwischen 0,3 und 1,0). Dadurch wird die Bewegungserkennung nicht vollständig verhindert, sondern lediglich die Analyse weiterer Bilder durch den Detektor unterbrochen, sobald der Schwellenwert überschritten wird. Bewegungsbasierte Aufzeichnungen werden während dieser Ereignisse weiterhin erstellt." + }, + "skip_motion_threshold": { + "label": "Schwellenwert für Bewegungsüberspringen", + "description": "Wenn sich mehr als dieser Anteil des Bildes in einem einzelnen Frame ändert, gibt der Detektor keine Bewegungsfelder zurück und kalibriert sich sofort neu. Dies kann CPU-Leistung sparen und Fehlalarme bei Blitzschlag, Gewittern usw. reduzieren, aber auch echte Ereignisse übersehen, wie z. B. eine PTZ-Kamera, die ein Objekt automatisch verfolgt. Der Kompromiss besteht darin, entweder einige Megabyte an Aufzeichnungen zu verlieren oder ein paar kurze Clips zu überprüfen. Leer lassen um diese Funktion zu deaktivieren." + }, + "improve_contrast": { + "label": "Kontrast verbessern", + "description": "Wenden Sie vor der Bewegungsanalyse eine Kontrastverbesserung auf die Bilder an, um die Erkennung zu erleichtern." + }, + "contour_area": { + "label": "Konturbereich", + "description": "Mindestkonturfläche in Pixeln, die erforderlich ist, damit eine Bewegungskontur gezählt wird." + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "Alpha-Blending-Faktor, der bei der Frame-Differenzierung für die Bewegungsberechnung verwendet wird." + }, + "frame_alpha": { + "label": "Rahmen Alpha", + "description": "Alpha-Wert, der beim Mischen von Frames für die Bewegungsvorverarbeitung verwendet wird." + }, + "frame_height": { + "label": "Rahmenhöhe", + "description": "Höhe in Pixeln, auf die Frames bei der Berechnung von Bewegungen skaliert werden sollen." + }, + "mask": { + "label": "Maskenkoordinaten", + "description": "Geordnete x-, y-Koordinaten, die das Bewegungsmaskenpolygon definieren, das zum Einbeziehen/Ausschließen von Bereichen verwendet wird." + }, + "mqtt_off_delay": { + "label": "MQTT-Ausschaltverzögerung", + "description": "Sekunden, die nach der letzten Bewegung gewartet werden müssen, bevor ein MQTT-„Aus”-Status veröffentlicht wird." + }, + "enabled_in_config": { + "label": "Ursprünglicher Bewegungszustand", + "description": "Gibt an, ob die Bewegungserkennung in der ursprünglichen statischen Konfiguration aktiviert war." + }, + "raw_mask": { + "label": "Maskierung" + }, + "description": "Die Standard-Einstellungen für die Bewegungserkennung gelten für alle Kameras, sofern sie nicht für einzelne Kameras überschrieben werden." + }, + "tls": { + "label": "TLS", + "description": "TLS-Einstellungen für die Website von Frigate (Port 8971).", + "enabled": { + "label": "Aktivieren TLS", + "description": "Aktivieren Sie TLS für die Web-Benutzeroberfläche und die API von Frigate auf dem konfigurierten TLS-Port." + } + }, + "ui": { + "label": "UI", + "description": "Benutzeroberflächen-Einstellungen wie Zeitzone, Zeit-/Datumsformatierung und Einheiten.", + "timezone": { + "label": "Zeitzone", + "description": "Optionale Zeitzone, die in der Benutzeroberfläche angezeigt werden soll (Standardmäßig wird die lokale Zeit des Browsers angezeigt, wenn keine Zeitzone festgelegt ist)." + }, + "time_format": { + "label": "Zeitformat", + "description": "In der Benutzeroberfläche zu verwendendes Zeitformat (Browser, 12-Stunden- oder 24-Stunden-Format)." + }, + "date_style": { + "label": "Datumsformat", + "description": "In der Benutzeroberfläche zu verwendendes Datumsformat (vollständig, lang, mittel, kurz)." + }, + "time_style": { + "label": "Zeitstil", + "description": "In der Benutzeroberfläche zu verwendender Zeitstil (vollständig, lang, mittel, kurz)." + }, + "unit_system": { + "label": "Einheitensystem", + "description": "Einheitensystem für die Anzeige (metrisch oder imperial), das in der Benutzeroberfläche und MQTT verwendet wird." + } + }, + "detectors": { + "label": "Detektor-Hardware", + "description": "Konfiguration für Objektdetektoren (CPU, GPU, ONNX-Backends) und alle detektorspezifischen Modelleinstellungen.", + "type": { + "label": "Type", + "description": "Art des für die Objekterkennung zu verwendenden Detektors (z. B. „cpu“, „edgetpu“, „openvino“)." + }, + "cpu": { + "label": "CPU", + "description": "CPU-TFLite-Detektor, der TensorFlow Lite-Modelle ohne Hardwarebeschleunigung auf der Host-CPU ausführt. Nicht empfohlen.", + "type": { + "label": "Type" + }, + "model": { + "label": "Detektorspezifische Modellkonfiguration", + "description": "Detektorspezifische Modellkonfigurationsoptionen (Pfad, Eingabegröße usw.).", + "path": { + "label": "Pfad zum benutzerdefinierten Objekterkennungsmodell", + "description": "Pfad zu einer benutzerdefinierten Erkennungsmodelldatei (oder plus:// für Frigate+-Modelle)." + }, + "labelmap_path": { + "label": "Label-Karte für benutzerdefinierten Objektdetektor", + "description": "Pfad zu einer Labelmap-Datei, die numerische Klassen für den Detektor String-Labels zuordnet." + }, + "width": { + "label": "Eingabebreite des Objekterkennungsmodells", + "description": "Breite des Modell-Eingabetensors in Pixeln." + }, + "height": { + "label": "Eingabehöhe des Objekterkennungsmodells", + "description": "Höhe des Modell-Eingabetensors in Pixeln." + }, + "labelmap": { + "label": "Anpassung der Labelmap", + "description": "Überschreibt oder ordnet Einträge neu zu, um sie in die Standard-Labelmap zu integrieren." + }, + "attributes_map": { + "label": "Zuordnung von Objektbezeichnungen zu ihren Attributbezeichnungen", + "description": "Zuordnung von Objektbezeichnungen zu Attributbezeichnungen, die zum Anhängen von Metadaten verwendet werden (z. B. „Auto“ -> „[Kennzeichen]“)." + }, + "input_tensor": { + "label": "Form des Eingabetensors des Modells", + "description": "Vom Modell erwartetes Tensorformat: „nhwc” oder „nchw”." + }, + "input_pixel_format": { + "label": "Modell-Eingabe-Pixel-Farbformat", + "description": "Vom Modell erwarteter Pixel-Farbraum: „rgb“, „bgr“ oder „yuv“." + }, + "input_dtype": { + "label": "Modell-Eingabe D Typ", + "description": "Datentyp des Modelleingabetensors (z. B. „float32“)." + }, + "model_type": { + "label": "Objekterkennungsmodelltyp", + "description": "Detektormodellarchitekturtyp (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird." + } + }, + "model_path": { + "label": "Detektorspezifischer Modellpfad", + "description": "Dateipfad zum Binärcode des Detektormodells, falls vom ausgewählten Detektor benötigt." + }, + "num_threads": { + "label": "Anzahl der Erkennungs-Threads", + "description": "Die Anzahl der Threads, die für die CPU-basierte Inferenz verwendet werden." + } + }, + "deepstack": { + "label": "DeepStack", + "description": "DeepStack/CodeProject.AI-Detektor, der Bilder zur Inferenz an eine entfernte DeepStack-HTTP-API sendet. Nicht empfohlen.", + "type": { + "label": "Type" + }, + "model": { + "label": "Detektorspezifische Modellkonfiguration", + "description": "Detektorspezifische Modellkonfigurationsoptionen (Pfad, Eingabegröße usw.).", + "path": { + "label": "Pfad zum benutzerdefinierten Objekterkennungsmodell", + "description": "Pfad zu einer benutzerdefinierten Erkennungsmodelldatei (oder plus:// für Frigate+-Modelle)." + }, + "labelmap_path": { + "label": "Label-Karte für benutzerdefinierten Objektdetektor", + "description": "Pfad zu einer Labelmap-Datei, die numerische Klassen dem Detektor als Zeichenfolgenbezeichnungen zuordnet." + }, + "width": { + "label": "Eingabebreite des Objekterkennungsmodells", + "description": "Breite des Modell-Eingabetensors in Pixeln." + }, + "height": { + "label": "Eingabehöhe für das Objekt-Erkennungsmodell", + "description": "Höhe des Modell-Eingabetensors in Pixeln." + }, + "labelmap": { + "label": "Anpassung der Labelmap", + "description": "Überschreibt oder ordnet Einträge neu zu, um sie in die Standard-Labelmap zu integrieren." + }, + "attributes_map": { + "label": "Zuordnung der Objektbezeichnungen zu ihren Attributbezeichnungen", + "description": "Zuordnung von Objektbezeichnungen zu Attributbezeichnungen, die zum Hinzufügen von Metadaten verwendet werden (zum Beispiel „Auto“ -> [„Kennzeichen“])." + }, + "input_tensor": { + "label": "Form des Modell-Eingabetensors", + "description": "Vom Modell erwartetes Tensor-Format: „nhwc“ oder „nchw“." + }, + "input_pixel_format": { + "label": "Standard-Pixel-Farbformat", + "description": "Vom Modell erwarteter Pixel-Farbraum: „rgb“, „bgr“ oder „yuv“." + }, + "input_dtype": { + "label": "Modell-Eingangs-D-Typ", + "description": "Datentyp des Modell-Eingabetensors (z. B. „float32“)." + }, + "model_type": { + "label": "Typ des Objekterkennungsmodells", + "description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird." + } + }, + "model_path": { + "label": "Detektorspezifischer Modellpfad", + "description": "Dateipfad zur Binärdatei des Detektormodells, falls dies für den ausgewählten Detektor erforderlich ist." + }, + "api_url": { + "label": "DeepStack-API-URL", + "description": "Die URL der DeepStack-API." + }, + "api_timeout": { + "label": "DeepStack-API-Zeitlimit (in Sekunden)", + "description": "Maximal zulässige Zeit für eine DeepStack-API-Anfrage." + }, + "api_key": { + "label": "DeepStack-API-Schlüssel (falls erforderlich)", + "description": "Optionaler API-Schlüssel für authentifizierte DeepStack-Dienste." + } + }, + "degirum": { + "label": "DeGirum", + "description": "DeGirum-Detektor zum Ausführen von Modellen über die DeGirum-Cloud oder lokale Inferenzdienste.", + "type": { + "label": "Type" + }, + "model": { + "label": "Detektorspezifische Modellkonfiguration", + "description": "Detektorspezifische Optionen zur Modellkonfiguration (Pfad, Eingabegröße usw.).", + "path": { + "label": "Pfad zum benutzerdefinierten Objekt-Erkennungsmodell", + "description": "Pfad zu einer benutzerdefinierten Erkennungsmodelldatei (oder plus:// für Frigate+-Modelle)." + }, + "labelmap_path": { + "label": "Label-Karte für benutzerdefinierten Objektdetektor", + "description": "Pfad zu einer Labelmap-Datei, die numerische Klassen dem Detektor als Zeichenfolgenbezeichnungen zuordnet." + }, + "width": { + "label": "Eingabebreite des Objekterkennungsmodells", + "description": "Breite des Modell-Eingabetensors in Pixeln." + }, + "height": { + "label": "Eingabehöhe des Objekterkennungsmodells", + "description": "Höhe des Modell-Eingabetensors in Pixeln." + }, + "labelmap": { + "label": "Anpassung der Labelmap", + "description": "Überschreibt oder ordnet Einträge neu zu, um sie in die Standard-Labelmap zu integrieren." + }, + "attributes_map": { + "label": "Zuordnung der Objektbezeichnungen zu ihren Attributbezeichnungen", + "description": "Zuordnung von Objektbezeichnungen zu Attributbezeichnungen, die zum Hinzufügen von Metadaten verwendet werden (zum Beispiel „Auto“ -> [„Kennzeichen“])." + }, + "input_tensor": { + "label": "Form des Modell-Eingabetensors", + "description": "Vom Modell erwartetes Tensor-Format: „nhwc“ oder „nchw“." + }, + "input_pixel_format": { + "label": "Standard-Pixel-Farbformat", + "description": "Vom Modell erwarteter Pixel-Farbraum: „rgb“, „bgr“ oder „yuv“." + }, + "input_dtype": { + "label": "Modell-Eingangs-D-Typ", + "description": "Datentyp des Modell-Eingabetensors (z. B. „float32“)." + }, + "model_type": { + "label": "Typ des Objekterkennungsmodells", + "description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird." + } + }, + "model_path": { + "label": "Detektorspezifischer Modellpfad", + "description": "Dateipfad zur Binärdatei des Detektormodells, falls dies für den ausgewählten Detektor erforderlich ist." + }, + "location": { + "label": "Ort der Schlussfolgerung", + "description": "Standort der DeGirim-Inferenzmaschine (z. B. „@cloud“, „127.0.0.1“)." + }, + "zoo": { + "label": "Modellzoo", + "description": "Pfad oder URL zum DeGirum-Modellzoo." + }, + "token": { + "label": "DeGirum Cloud Token", + "description": "Zugangs-Token für DeGirum Cloud." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "EdgeTPU-Detektor, der TensorFlow Lite-Modelle ausführt, die mithilfe des EdgeTPU-Delegates für Coral EdgeTPU kompiliert wurden.", + "type": { + "label": "Type" + }, + "model": { + "label": "Detektorspezifische Modellkonfiguration", + "description": "Detektorspezifische Optionen zur Modellkonfiguration (Pfad, Eingabegröße usw.).", + "path": { + "label": "Pfad zum benutzerdefinierten Objekt-Erkennungsmodell", + "description": "Pfad zu einer benutzerdefinierten Erkennungsmodelldatei (oder plus:// für Frigate+-Modelle)." + }, + "labelmap_path": { + "label": "Label-Karte für benutzerdefinierten Objektdetektor", + "description": "Pfad zu einer Labelmap-Datei, die numerische Klassen dem Detektor als Zeichenfolgenbezeichnungen zuordnet." + }, + "width": { + "label": "Eingabebreite des Objekterkennungsmodells", + "description": "Breite des Modell-Eingabetensors in Pixeln." + }, + "height": { + "label": "Eingabehöhe für das Objekt-Erkennungsmodell", + "description": "Höhe des Modell-Eingabetensors in Pixeln." + }, + "labelmap": { + "label": "Anpassung der Labelmap", + "description": "Überschreibt oder ordnet Einträge neu zu, um sie in die Standard-Labelmap zu integrieren." + }, + "attributes_map": { + "label": "Zuordnung der Objektbezeichnungen zu ihren Attributbezeichnungen", + "description": "Zuordnung von Objektbezeichnungen zu Attributbezeichnungen, die zum Hinzufügen von Metadaten verwendet werden (zum Beispiel „Auto“ -> [„Kennzeichen“])." + }, + "input_tensor": { + "label": "Form des Modell-Eingabetensors", + "description": "Vom Modell erwartetes Tensor-Format: „nhwc“ oder „nchw“." + }, + "input_pixel_format": { + "label": "Standard-Pixel-Farbformat", + "description": "Vom Modell erwarteter Pixel-Farbraum: „rgb“, „bgr“ oder „yuv“." + }, + "input_dtype": { + "label": "Modell-Eingabe Typ D", + "description": "Datentyp des Modell-Eingabetensors (z. B. „float32“)." + }, + "model_type": { + "label": "Typ des Objekterkennungsmodells", + "description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird." + } + }, + "model_path": { + "label": "Detektorspezifischer Modellpfad", + "description": "Dateipfad zur Binärdatei des Detektormodells, falls dies für den ausgewählten Detektor erforderlich ist." + }, + "device": { + "label": "Gerätetyp", + "description": "Das für die EdgeTPU-Inferenz zu verwendende Gerät (z. B. „usb“, „pci“)." + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "Hailo-8/Hailo-8L-Detektor unter Verwendung von HEF-Modellen und dem HailoRT SDK für die Inferenz auf Hailo-Hardware.", + "type": { + "label": "Type" + }, + "model": { + "label": "Detektorspezifische Modellkonfiguration", + "description": "Detektorspezifische Optionen zur Modellkonfiguration (Pfad, Eingabegröße usw.).", + "path": { + "label": "Pfad zum benutzerdefinierten Objekterkennungsmodell", + "description": "Pfad zu einer benutzerdefinierten Erkennungsmodelldatei (oder plus:// für Frigate+-Modelle)." + }, + "labelmap_path": { + "label": "Label-Karte für benutzerdefinierten Objektdetektor", + "description": "Pfad zu einer Labelmap-Datei, die numerische Klassen dem Detektor als Zeichenfolgenbezeichnungen zuordnet." + }, + "width": { + "label": "Eingabebreite des Objekterkennungsmodells", + "description": "Breite des Modell-Eingabetensors in Pixeln." + }, + "height": { + "label": "Eingabehöhe für das Objekt-Erkennungsmodell", + "description": "Höhe des Modell-Eingabetensors in Pixeln." + }, + "labelmap": { + "label": "Anpassung der Labelmap", + "description": "Überschreibt oder ordnet Einträge neu zu, um sie in die Standard-Labelmap zu integrieren." + }, + "attributes_map": { + "label": "Zuordnung der Objektbezeichnungen zu ihren Attributbezeichnungen", + "description": "Zuordnung von Objektbezeichnungen zu Attributbezeichnungen, die zum Hinzufügen von Metadaten verwendet werden (zum Beispiel „Auto“ -> [„Kennzeichen“])." + }, + "input_tensor": { + "label": "Form des Modell-Eingabetensors", + "description": "Vom Modell erwartetes Tensor-Format: „nhwc“ oder „nchw“." + }, + "input_pixel_format": { + "label": "Standard-Pixel-Farbformat", + "description": "Vom Modell erwarteter Pixel-Farbraum: „rgb“, „bgr“ oder „yuv“." + }, + "input_dtype": { + "label": "Modell-Eingangs-D-Typ", + "description": "Datentyp des Modell-Eingabetensors (z. B. „float32“)." + }, + "model_type": { + "label": "Typ des Objekterkennungsmodells", + "description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird." + } + }, + "model_path": { + "label": "Detektorspezifischer Modellpfad", + "description": "Dateipfad zur Binärdatei des Detektormodells, falls dies für den ausgewählten Detektor erforderlich ist." + }, + "device": { + "label": "Geräte Type", + "description": "Das für die Hailo-Verbindung zu verwendende Gerät (z. B. „PCIe“, „M.2“)." + } + }, + "rknn": { + "model": { + "input_dtype": { + "label": "Modell-Eingangs-D-Typ" + } + }, + "label": "RKNN", + "description": "RKNN-Detektor für Rockchip-NPUs; führt kompilierte RKNN-Modelle auf Rockchip-Hardware aus.", + "num_cores": { + "label": "Anzahl der zu verwendenden NPU-Kerne.", + "description": "Die Anzahl der zu verwendenden NPU-Kerne (0 für automatische Einstellung)." + } + }, + "memryx": { + "label": "MemryX", + "description": "MemryX MX3-Detektor, der kompilierte DFP-Modelle auf MemryX-Beschleunigern ausführt.", + "type": { + "label": "Type" + }, + "model": { + "label": "Detektorspezifische Modellkonfiguration", + "description": "Detektorspezifische Optionen zur Modellkonfiguration (Pfad, Eingabegröße usw.).", + "path": { + "label": "Pfad zum benutzerdefinierten Objekterkennungsmodell", + "description": "Pfad zu einer benutzerdefinierten Erkennungsmodelldatei (oder plus:// für Frigate+-Modelle)." + }, + "labelmap_path": { + "label": "Label-Karte für benutzerdefinierten Objektdetektor", + "description": "Pfad zu einer Labelmap-Datei, die numerische Klassen dem Detektor als Zeichenfolgenbezeichnungen zuordnet." + }, + "width": { + "label": "Eingabebreite des Objekterkennungsmodells", + "description": "Breite des Modell-Eingabetensors in Pixeln." + }, + "height": { + "label": "Eingabehöhe für das Objekterkennungsmodell", + "description": "Höhe des Modell-Eingabetensors in Pixeln." + }, + "labelmap": { + "label": "Anpassung der Labelmap", + "description": "Überschreibt oder ordnet Einträge neu zu, um sie in die Standard-Labelmap zu integrieren." + }, + "attributes_map": { + "label": "Zuordnung der Objektbezeichnungen zu ihren Attributbezeichnungen", + "description": "Zuordnung von Objektbezeichnungen zu Attributbezeichnungen, die zum Hinzufügen von Metadaten verwendet werden (zum Beispiel „Auto“ -> [„Kennzeichen“])." + }, + "input_tensor": { + "label": "Form des Modell-Eingabetensors", + "description": "Vom Modell erwartetes Tensor-Format: „nhwc“ oder „nchw“." + }, + "input_pixel_format": { + "label": "Standard-Pixel-Farbformat", + "description": "Vom Modell erwarteter Pixel-Farbraum: „rgb“, „bgr“ oder „yuv“." + } + }, + "device": { + "label": "Geräte Pfad", + "description": "Das für die MemryX-Inferenz zu verwendende Gerät (z. B. „PCIe“)." + } + }, + "model": { + "label": "Detektorspezifische Modellkonfiguration", + "description": "Detektorspezifische Optionen zur Modellkonfiguration (Pfad, Eingabegröße usw.). Detektorspezifische Modellkonfiguration.", + "path": { + "label": "Pfad zum benutzerdefinierten Objekterkennungsmodell", + "description": "Pfad zu einer benutzerdefinierten Erkennungsmodelldatei (oder plus:// für Frigate+-Modelle)." + }, + "labelmap_path": { + "label": "Label-Karte für benutzerdefinierten Objektdetektor", + "description": "Pfad zu einer Labelmap-Datei, die numerische Klassen dem Detektor als Zeichenfolgenbezeichnungen zuordnet." + }, + "width": { + "label": "Eingabebreite des Objekterkennungsmodells", + "description": "Breite des Modell-Eingabetensors in Pixeln." + }, + "height": { + "label": "Eingabehöhe für das Objekterkennungsmodell", + "description": "Höhe des Modell-Eingabetensors in Pixeln." + }, + "labelmap": { + "label": "Anpassung der Labelmap", + "description": "Überschreibt oder ordnet Einträge neu zu, um sie in die Standard-Labelmap zu integrieren." + }, + "attributes_map": { + "label": "Zuordnung der Objektbezeichnungen zu ihren Attributbezeichnungen", + "description": "Zuordnung von Objektbezeichnungen zu Attributbezeichnungen, die zum Hinzufügen von Metadaten verwendet werden (zum Beispiel „Auto“ -> [„Kennzeichen“])." + }, + "input_tensor": { + "label": "Form des Modell-Eingabetensors", + "description": "Vom Modell erwartetes Tensor-Format: „nhwc“ oder „nchw“." + }, + "input_pixel_format": { + "label": "Standard-Pixel-Farbformat", + "description": "Vom Modell erwarteter Pixel-Farbraum: „rgb“, „bgr“ oder „yuv“." + }, + "input_dtype": { + "label": "Modell-Eingangs-D-Typ", + "description": "Datentyp des Modell-Eingabetensors (z. B. „float32“)." + }, + "model_type": { + "label": "Typ des Objekterkennungsmodells", + "description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird." + } + }, + "model_path": { + "label": "Detektorspezifischer Modellpfad", + "description": "Dateipfad zur Binärdatei des Detektormodells, falls dies für den ausgewählten Detektor erforderlich ist." + }, + "axengine": { + "label": "AXEngine NPU", + "description": "AXERA AX650N/AX8850N NPU-Detektor, der kompilierte .axmodel-Dateien über die AXEngine-Laufzeitumgebung ausführt." + }, + "onnx": { + "label": "ONNX", + "description": "ONNX-Detektor zum Ausführen von ONNX-Modellen; nutzt verfügbare Beschleunigungs-Backends (CUDA/ROCm/OpenVINO), sofern vorhanden.", + "device": { + "label": "Gerätetyp", + "description": "Das für die ONNX-Inferenz zu verwendende Gerät (z. B. „AUTO“, „CPU“, „GPU“)." + } + }, + "openvino": { + "label": "OpenVINO", + "description": "OpenVINO-Detektor für AMD- und Intel-CPUs, Intel-GPUs und Intel-VPU-Hardware.", + "device": { + "label": "Geräte Type", + "description": "Das für die OpenVINO-Inferenz zu verwendende Gerät (z. B. „CPU“, „GPU“, „NPU“)." + } + }, + "synaptics": { + "label": "Synaptics", + "description": "Synaptics-NPU-Detektor für Modelle im .synap-Format unter Verwendung des Synap SDK auf Synaptics-Hardware." + }, + "teflon_tfl": { + "label": "Teflon", + "description": "Teflon-Delegate-Detektor für TFLite unter Verwendung der Mesa-Teflon-Delegate-Bibliothek zur Beschleunigung der Inferenz auf unterstützten GPUs." + }, + "tensorrt": { + "label": "TensorRT", + "description": "TensorRT-Detektor für Nvidia Jetson-Geräte unter Verwendung serialisierter TensorRT-Engines zur Beschleunigung der Inferenz.", + "device": { + "label": "GPU-Geräteindex", + "description": "Der zu verwendende GPU-Geräteindex." + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "ZMQ-IPC-Detektor, der die Inferenz über einen ZeroMQ-IPC-Endpunkt an einen externen Prozess auslagert.", + "endpoint": { + "label": "ZMQ IPC Endpunkt", + "description": "Der ZMQ-Endpunkt, mit dem eine Verbindung hergestellt werden soll." + }, + "request_timeout_ms": { + "label": "ZMQ-Anfrage-Timeout in Millisekunden", + "description": "Zeitlimit für ZMQ-Anfragen in Millisekunden." + }, + "linger_ms": { + "label": "Verweilzeit des ZMQ-Sockets in Millisekunden", + "description": "Verweilzeit des Sockets in Millisekunden." + } + } + }, + "objects": { + "label": "Objekte", + "description": "Standardeinstellungen für die Objektverfolgung, einschließlich der zu verfolgenden Labels und Filter pro Objekt.", + "track": { + "label": "Zu verfolgende Objekte", + "description": "Liste der Objektbezeichnungen, die von allen Kameras verfolgt werden sollen; kann für jede Kamera individuell überschrieben werden." + }, + "filters": { + "label": "Objektfilter", + "description": "Filter, die auf erkannte Objekte angewendet werden, um Fehlalarme zu reduzieren (Fläche, Verhältnis, Konfidenz).", + "min_area": { + "label": "Mindestobjektfläche", + "description": "Mindestfläche der Begrenzungsbox (Pixel oder Prozentangabe), die für diesen Objekttyp erforderlich ist. Kann in Pixel (int) oder Prozentangabe (Float zwischen 0,000001 und 0,99) angegeben werden." + }, + "max_area": { + "label": "Maximale Objektfläche", + "description": "Maximal zulässige Begrenzungsrahmenfläche (Pixel oder Prozent) für diesen Objekttyp. Kann in Pixel (int) oder Prozent (Float zwischen 0,000001 und 0,99) angegeben werden." + }, + "min_ratio": { + "label": "Mindestseitenverhältnis", + "description": "Mindestverhältnis von Breite zu Höhe, das für die Begrenzungsbox erforderlich ist, damit diese gültig ist." + }, + "max_ratio": { + "label": "Maximales Seitenverhältnis", + "description": "Maximal zulässiges Verhältnis von Breite zu Höhe für die Begrenzungsbox, damit diese gültig ist." + }, + "threshold": { + "label": "Konfidenzschwelle", + "description": "Durchschnittlicher Schwellenwert für die Erkennungssicherheit, der erforderlich ist, damit das Objekt als echt positiv eingestuft wird." + }, + "min_score": { + "label": "Mindestvertrauen", + "description": "Mindestkonfidenz für die Einzelbilderkennung, die für die Zählung des Objekts erforderlich ist." + }, + "mask": { + "label": "Filter Maske", + "description": "Polygonkoordinaten, die definieren, wo dieser Filter innerhalb des Rahmens angewendet wird." + }, + "raw_mask": { + "label": "Rohmaske" + } + }, + "mask": { + "label": "Objekt Maskierung", + "description": "Maskenpolygon, das verwendet wird, um die Objekterkennung in bestimmten Bereichen zu verhindern." + }, + "genai": { + "label": "GenAI-Objektkonfiguration", + "description": "GenAI-Optionen zum Beschreiben verfolgter Objekte und zum Senden von Frames zur Generierung.", + "enabled": { + "label": "Aktivieren GenAI", + "description": "Die Erstellung von Beschreibungen für verfolgte Objekte durch GenAI standardmäßig aktivieren." + }, + "use_snapshot": { + "label": "Verwenden Sie Momentaufnahmen", + "description": "Verwenden Sie für die Erstellung von Beschreibungen durch GenAI Objektsnapshots anstelle von Miniaturansichten." + }, + "prompt": { + "label": "Aufforderung zur Bildunterschrift", + "description": "Standardvorlage für Eingabeaufforderungen, die bei der Erstellung von Beschreibungen mit GenAI verwendet wird." + }, + "object_prompts": { + "label": "Objekt-Eingabeaufforderungen", + "description": "Objektbezogene Eingabeaufforderungen zur Anpassung der GenAI-Ausgaben an bestimmte Labels." + }, + "objects": { + "label": "GenAI-Objekte", + "description": "Liste der Objektbezeichnungen, die standardmäßig an GenAI gesendet werden sollen." + }, + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Felder, die ausgefüllt werden müssen, damit Objekte für die Erstellung einer GenAI-Beschreibung in Frage kommen." + }, + "debug_save_thumbnails": { + "label": "Miniaturansichten speichern", + "description": "Speichere die an GenAI gesendeten Miniaturansichten zur Fehlerbehebung und Überprüfung." + }, + "send_triggers": { + "label": "GenAI-Auslöser", + "description": "Legt fest, wann Frames an GenAI gesendet werden sollen (am Ende, nach Aktualisierungen usw.).", + "tracked_object_end": { + "label": "weiterleiten", + "description": "Sende eine Anfrage an GenAI, sobald das verfolgte Objekt sein Ziel erreicht hat." + }, + "after_significant_updates": { + "label": "Früher GenAI-Auslöser", + "description": "Sende eine Anfrage an GenAI, nachdem eine bestimmte Anzahl bedeutender Aktualisierungen für das verfolgte Objekt erfolgt ist." + } + }, + "enabled_in_config": { + "label": "Ursprünglicher GenAI-Zustand", + "description": "Gibt an, ob GenAI in der ursprünglichen statischen Konfiguration aktiviert war." + } + }, + "raw_mask": { + "label": "Rohmaske" + }, + "filters_attribute": { + "label": "Attributfilter", + "description": "Auf erkannte Attribute angewendete Filter zur Reduzierung von Fehlalarmen (Fläche, Verhältnis, Konfidenz).", + "min_area": { + "label": "Mindestfläche des Attributs", + "description": "Für dieses Attribut erforderliche Mindestfläche des Begrenzungsrahmens (in Pixeln oder Prozent). Kann als Pixelwert (Ganzzahl) oder als Prozentwert (Gleitkommawert zwischen 0,000001 und 0,99) angegeben werden." + }, + "max_area": { + "label": "Maximale Attributfläche", + "description": "Maximal zulässige Fläche des Begrenzungsrahmens (in Pixeln oder Prozent) für dieses Attribut. Kann als Pixelwert (Ganzzahl) oder als Prozentwert (Gleitkommawert zwischen 0,000001 und 0,99) angegeben werden." + }, + "min_ratio": { + "label": "Mindestseitenverhältnis", + "description": "Erforderliches Mindestverhältnis von Breite zu Höhe, damit die Begrenzungsbox die Anforderungen erfüllt." + }, + "max_ratio": { + "label": "Maximales Seitenverhältnis", + "description": "Maximal zulässiges Verhältnis von Breite zu Höhe für die Begrenzungsbox, damit diese die Anforderungen erfüllt." + }, + "threshold": { + "label": "Konfidenzschwelle", + "description": "Durchschnittlicher Schwellenwert für die Erkennungssicherheit, der erforderlich ist, damit das Merkmal als echtes Positiv gewertet wird." + }, + "min_score": { + "label": "Mindestvertrauen", + "description": "Mindestwert für die Erkennungssicherheit eines einzelnen Bildes, der erforderlich ist, um dieses Attribut seinem übergeordneten Objekt zuzuordnen." + }, + "mask": { + "label": "Filtermaske", + "description": "Polygonkoordinaten, die festlegen, wo dieser Filter innerhalb des Bildausschnitts angewendet wird." + }, + "raw_mask": { + "label": "Rohmaske" + } + } + }, + "record": { + "label": "Aufnahme", + "enabled": { + "label": "Aufnahme aktivieren", + "description": "Aufzeichnung für alle Kameras aktivieren oder deaktivieren; kann für jede Kamera einzeln überschrieben werden." + }, + "expire_interval": { + "label": "Bereinigungsintervall festlegen", + "description": "Minuten zwischen den Bereinigungsdurchläufen, bei denen abgelaufene Aufzeichnungssegmente entfernt werden." + }, + "continuous": { + "label": "Dauerhafte Aufbewahrung", + "description": "Anzahl der Tage, für die Aufzeichnungen unabhängig von verfolgten Objekten oder Bewegungen aufbewahrt werden sollen. Setzen Sie diesen Wert auf 0, wenn Sie nur Aufzeichnungen von Warnmeldungen und Erkennungen aufbewahren möchten.", + "days": { + "label": "Aufbewahrungsfristen", + "description": "Aufbewahrungsdauer der Aufzeichnungen." + } + }, + "motion": { + "label": "Bewegungsnachlauf", + "description": "Anzahl der Tage, für die durch Bewegung ausgelöste Aufzeichnungen unabhängig von den verfolgten Objekten aufbewahrt werden sollen. Setzen Sie diesen Wert auf 0, wenn Sie nur Aufzeichnungen von Warnmeldungen und Erkennungen aufbewahren möchten.", + "days": { + "label": "Aufbewahrungsfristen", + "description": "Aufbewahrungsdauer der Aufzeichnungen." + } + }, + "detections": { + "label": "Nachweis und Aufbewahrung", + "description": "Einstellungen zur Aufbewahrungsdauer von Aufzeichnungen für Erkennungsereignisse, einschließlich der Dauer vor und nach der Aufzeichnung.", + "pre_capture": { + "label": "Sekunden vor der Aufnahme", + "description": "Anzahl der Sekunden vor dem Erkennungsereignis, die in die Aufzeichnung aufgenommen werden sollen." + }, + "post_capture": { + "label": "Sekunden nach der Aufnahme", + "description": "Anzahl der Sekunden nach dem Erkennungsereignis, die in die Aufzeichnung aufgenommen werden sollen." + }, + "retain": { + "label": "Ereignisspeicherung", + "description": "Aufbewahrungsdauer für Aufzeichnungen von Erkennungsereignissen.", + "days": { + "label": "Aufbewahrungsfristen", + "description": "Anzahl der Tage, für die Aufzeichnungen von Erkennungsereignissen aufbewahrt werden sollen." + }, + "mode": { + "label": "Speichermodus", + "description": "Speichermodus: „all“ (alle Segmente speichern), „motion“ (Segmente mit Bewegung speichern) oder „active_objects“ (Segmente mit aktiven Objekten speichern)." + } + } + }, + "alerts": { + "label": "Aufbewahrungsfrist für Benachrichtigungen", + "description": "Einstellungen zur Aufbewahrungsdauer von Aufzeichnungen für Alarmereignisse, einschließlich der Dauer vor und nach dem Aufzeichnungsstart.", + "pre_capture": { + "label": "Sekunden vor der Aufnahme", + "description": "Anzahl der Sekunden vor dem Erkennungsereignis, die in die Aufzeichnung aufgenommen werden sollen." + }, + "post_capture": { + "label": "Sekunden nach der Aufnahme", + "description": "Anzahl der Sekunden nach dem Erkennungsereignis, die in die Aufzeichnung aufgenommen werden sollen." + }, + "retain": { + "label": "Ereignisspeicherung", + "description": "Aufbewahrungsdauer für Aufzeichnungen von Erkennungsereignissen.", + "days": { + "label": "Aufbewahrungsfrist", + "description": "Anzahl der Tage, für die Aufzeichnungen von Erkennungsereignissen aufbewahrt werden sollen." + }, + "mode": { + "label": "Speichermodus", + "description": "Speichermodus: „all“ (alle Segmente speichern), „motion“ (Segmente mit Bewegung speichern) oder „active_objects“ (Segmente mit aktiven Objekten speichern)." + } + } + }, + "export": { + "label": "Konfiguration exportieren", + "description": "Einstellungen, die beim Exportieren von Aufzeichnungen wie Zeitrafferaufnahmen und bei der Hardwarebeschleunigung verwendet werden.", + "hwaccel_args": { + "label": "hwaccel-Argumente exportieren", + "description": "Argumente für die Hardwarebeschleunigung bei Export- und Transkodierungsvorgängen." + }, + "max_concurrent": { + "label": "Maximale Anzahl gleichzeitiger Exporte", + "description": "Maximale Anzahl der gleichzeitig zu verarbeitenden Exportaufträge." + } + }, + "preview": { + "label": "Konfigurationsvorschau", + "description": "Einstellungen zur Steuerung der Qualität der in der Benutzeroberfläche angezeigten Aufnahmevorschauen.", + "quality": { + "label": "Vorschauqualität", + "description": "Qualitätsstufe der Vorschau (sehr_niedrig, niedrig, mittel, hoch, sehr_hoch)." + } + }, + "enabled_in_config": { + "label": "Ursprünglicher Aufnahmestatus", + "description": "Gibt an, ob die Aufzeichnung in der ursprünglichen statischen Konfiguration aktiviert war." + }, + "description": "Die Einstellungen für Aufzeichnung und Speicherung gelten für alle Kameras, sofern sie nicht für einzelne Kameras überschrieben werden." + }, + "review": { + "label": "Rezension", + "alerts": { + "label": "Benachrichtigungseinstellungen", + "description": "Einstellungen dazu, bei welchen überwachten Objekten Warnmeldungen generiert werden und wie lange diese aufbewahrt werden.", + "enabled": { + "label": "Benachrichtigungen aktivieren", + "description": "Die Erzeugung von Warnmeldungen für alle Kameras aktivieren oder deaktivieren; diese Einstellung kann für jede Kamera individuell überschrieben werden." + }, + "labels": { + "label": "Warnhinweise", + "description": "Liste der Objektbezeichnungen, die als Warnmeldungen gelten (zum Beispiel: Auto, Person)." + }, + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Zonen, die ein Objekt betreten muss, damit ein Alarm ausgelöst wird; lassen Sie das Feld leer, wenn alle Zonen zulässig sein sollen." + }, + "enabled_in_config": { + "label": "Ursprünglicher Alarmstatus", + "description": "Zeigt an, ob Warnmeldungen ursprünglich in der statischen Konfiguration aktiviert waren." + }, + "cutoff_time": { + "label": "Annahmeschluss für Benachrichtigungen", + "description": "Sekunden, die nach dem Ende einer alarmauslösenden Aktivität gewartet werden müssen, bevor der Alarm abgeschaltet wird." + } + }, + "detections": { + "label": "Konfiguration der Erkennungen", + "description": "Einstellungen, die festlegen, bei welchen verfolgten Objekten Erkennungen (ohne Alarm) generiert werden und wie lange diese Erkennungen gespeichert bleiben.", + "enabled": { + "label": "Erkennung aktivieren", + "description": "Erkennungsereignisse für alle Kameras aktivieren oder deaktivieren; kann für jede Kamera einzeln überschrieben werden." + }, + "labels": { + "label": "Kennzeichnungen zur Erkennung", + "description": "Liste der Objektbezeichnungen, die als Erkennungsereignisse gelten." + }, + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Zonen, die ein Objekt betreten muss, damit es als Erkennung gewertet wird; lassen Sie das Feld leer, wenn alle Zonen zulässig sein sollen." + }, + "cutoff_time": { + "label": "Zeitpunkt der Erkennung", + "description": "Sekunden, die nach dem Ende einer Aktivität, die keine Erkennung auslöst, gewartet werden müssen, bevor die Erkennung unterbrochen wird." + }, + "enabled_in_config": { + "label": "Ursprünglicher Erkennungsstatus", + "description": "Zeigt an, ob die Erkennung ursprünglich in der statischen Konfiguration aktiviert war." + } + }, + "genai": { + "label": "GenAI-Konfiguration", + "description": "Steuert den Einsatz generativer KI zur Erstellung von Beschreibungen und Zusammenfassungen von Rezensionsobjekten.", + "enabled": { + "label": "GenAI-Beschreibungen aktivieren", + "description": "Aktivieren oder deaktivieren Sie von GenAI generierte Beschreibungen und Zusammenfassungen für Überprüfungselemente." + }, + "alerts": { + "label": "GenAI für Benachrichtigungen aktivieren", + "description": "Verwenden Sie GenAI, um Beschreibungen für Alarmmeldungen zu erstellen." + }, + "detections": { + "label": "GenAI für die Erkennung aktivieren", + "description": "Verwenden Sie GenAI, um Beschreibungen für Erkennungselemente zu erstellen." + }, + "image_source": { + "label": "Quelle des Bildes", + "description": "Quelle der an GenAI gesendeten Bilder („Vorschau“ oder „Aufzeichnungen“); „Aufzeichnungen“ verwenden Bilder in höherer Qualität, verbrauchen jedoch mehr Token." + }, + "additional_concerns": { + "label": "Weitere Bedenken", + "description": "Eine Liste weiterer Aspekte oder Hinweise, die GenAI bei der Auswertung der Aktivitäten dieser Kamera berücksichtigen sollte." + }, + "debug_save_thumbnails": { + "label": "Miniaturansichten speichern", + "description": "Speichern Sie Miniaturansichten, die zur Fehlerbehebung und Überprüfung an den GenAI-Anbieter gesendet werden." + }, + "enabled_in_config": { + "label": "Ursprünglicher GenAI-Zustand", + "description": "Zeigt an, ob die GenAI-Überprüfung ursprünglich in der statischen Konfiguration aktiviert war." + }, + "preferred_language": { + "label": "Bevorzugte Sprache", + "description": "Bevorzugte Sprache, in der die generierten Antworten vom GenAI-Anbieter bereitgestellt werden sollen." + }, + "activity_context_prompt": { + "label": "Aufforderung zum Aktivitätskontext", + "description": "Eine benutzerdefinierte Eingabeaufforderung, die beschreibt, was als verdächtiges Verhalten gilt und was nicht, um den Zusammenfassungen der generativen KI einen Kontext zu geben." + } + }, + "description": "Einstellungen, die Benachrichtigungen, Erkennungen und GenAI-Zusammenfassungen steuern, die von der Benutzeroberfläche und dem Speicher verwendet werden." + }, + "onvif": { + "autotracking": { + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Objekte müssen in eine dieser Zonen eintreten, bevor die automatische Verfolgung beginnt." + }, + "movement_weights": { + "description": "Diese Kalibrierungswerte werden automatisch durch die Kamerakalibrierung generiert. Bitte nicht manuell ändern.", + "label": "Bewegungsgewichte" + }, + "label": "Automatische Verfolgung", + "description": "Bewegliche Objekte automatisch verfolgen und sie mithilfe von PTZ-Kamerabewegungen im Bildausschnitt zentriert halten.", + "enabled": { + "label": "Automatische Verfolgung aktivieren", + "description": "Aktivieren oder deaktivieren Sie die automatische PTZ-Kamera-Verfolgung erkannter Objekte." + }, + "calibrate_on_startup": { + "label": "Beim Start kalibrieren", + "description": "Messen Sie die Drehzahlen der PTZ-Motoren beim Start, um die Nachführgenauigkeit zu verbessern. Frigate aktualisiert die Konfiguration nach der Kalibrierung mit den Bewegungsgewichten." + }, + "zooming": { + "label": "Zoom-Modus", + "description": "Zoomverhalten steuern: deaktiviert (nur Schwenken/Neigen), absolut (am besten kompatibel) oder relativ (gleichzeitiges Schwenken/Neigen/Zoomen)." + }, + "zoom_factor": { + "label": "Zoomfaktor", + "description": "Steuert den Zoomfaktor bei verfolgten Objekten. Bei niedrigeren Werten bleibt mehr von der Szene im Bild; bei höheren Werten wird näher herangezoomt, wobei jedoch die Verfolgung verloren gehen kann. Werte zwischen 0,1 und 0,75." + }, + "track": { + "label": "Verfolgte Objekte", + "description": "Liste der Objekttypen, die das automatische Tracking auslösen sollen." + }, + "return_preset": { + "label": "Voreinstellung setzen", + "description": "Der in der Kamera-Firmware konfigurierte ONVIF-Voreinstellungsname, zu dem nach Beendigung der Verfolgung zurückgekehrt werden soll." + }, + "timeout": { + "label": "Zeitüberschreitung bei der Rückgabe", + "description": "Warte nach dem Verlust der Verfolgung so viele Sekunden, bevor die Kamera in die voreingestellte Position zurückkehrt." + }, + "enabled_in_config": { + "label": "Ursprünglicher Autotrack-Status", + "description": "Internes Feld zur Erfassung, ob die automatische Nachführung in der Konfiguration aktiviert wurde." + } + }, + "label": "ONVIF", + "description": "ONVIF-Verbindung und Einstellungen für die automatische PTZ-Verfolgung dieser Kamera.", + "host": { + "label": "ONVIF Host", + "description": "Host (und optional Schema) für den ONVIF-Dienst dieser Kamera." + }, + "port": { + "label": "ONVIF Port", + "description": "Portnummer für den ONVIF-Dienst." + }, + "user": { + "label": "ONVIF-Benutzername", + "description": "Benutzername für die ONVIF-Authentifizierung; bei einigen Geräten ist für ONVIF ein Admin-Benutzer erforderlich." + }, + "password": { + "label": "ONVIF-Passwort", + "description": "Passwort für die ONVIF-Authentifizierung." + }, + "tls_insecure": { + "label": "TLS-Überprüfung deaktivieren", + "description": "TLS-Überprüfung überspringen und Digest-Authentifizierung für ONVIF deaktivieren (unsicher; nur in sicheren Netzwerken verwenden)." + }, + "ignore_time_mismatch": { + "label": "Zeitabweichung ignorieren", + "description": "Ignoriere Zeitunterschiede zwischen Kamera und Frigate-Server bei der ONVIF-Kommunikation." + }, + "profile": { + "label": "ONVIF Profile", + "description": "Spezifisches ONVIF-Medienprofil für die PTZ-Steuerung, das anhand eines Tokens oder Namens ausgewählt wird. Ist kein Profil festgelegt, wird automatisch das erste Profil mit gültiger PTZ-Konfiguration ausgewählt." + } + }, + "semantic_search": { + "label": "Semantische Suche", + "triggers": { + "label": "Auslöser", + "description": "Aktionen und Übereinstimmungskriterien für kameraspezifische Auslöser der semantischen Suche.", + "friendly_name": { + "label": "Anzeigename", + "description": "Optionaler beschreibender Name, der in der Benutzeroberfläche für diesen Trigger angezeigt wird." + }, + "enabled": { + "label": "Diesen Trigger aktivieren", + "description": "Diesen Trigger für die semantische Suche aktivieren oder deaktivieren." + }, + "type": { + "label": "Auslöseart", + "description": "Auslösertyp: „thumbnail“ (Abgleich mit Bild) oder „description“ (Abgleich mit Text)." + }, + "data": { + "label": "Inhalt anzeigen", + "description": "Textphrase oder Miniaturbild-ID, die mit den verfolgten Objekten abgeglichen werden soll." + }, + "threshold": { + "label": "Auslöseschwelle", + "description": "Erforderlicher Mindestähnlichkeitswert (0–1) zur Aktivierung dieses Triggers." + }, + "actions": { + "label": "Trigger-Aktionen", + "description": "Liste der Aktionen, die ausgeführt werden sollen, wenn der Trigger ausgelöst wird (Benachrichtigung, Unterbezeichnung, Attribut)." + } + }, + "description": "Einstellungen für die semantische Suche, die Objekt-Embeddings erstellt und abfragt, um ähnliche Elemente zu finden.", + "enabled": { + "label": "Semantische Suche aktivieren", + "description": "Aktivieren oder deaktivieren Sie die semantische Suchfunktion." + }, + "reindex": { + "label": "Beim Start neu indizieren", + "description": "Lösen Sie eine vollständige Neuindizierung der historisch erfassten Objekte in der Embedding-Datenbank aus." + }, + "model": { + "label": "Semantisches Suchmodell oder Name des GenAI-Anbieters", + "description": "Das für die semantische Suche zu verwendende Einbettungsmodell (z. B. „jinav1“) oder der Name eines GenAI-Anbieters mit der Rolle „Einbettungen“." + }, + "model_size": { + "label": "Modellgröße", + "description": "Wählen Sie die Modellgröße aus; „small“ läuft auf der CPU, während „large“ in der Regel eine GPU erfordert." + }, + "device": { + "label": "Gerät", + "description": "Dies ist eine Übersteuerung, um ein bestimmtes Gerät anzusprechen. Weitere Informationen finden Sie unter https://onnxruntime.ai/docs/execution-providers/" + } + }, + "snapshots": { + "label": "Schnappschüsse", + "enabled": { + "label": "Schnappschüsse aktivieren", + "description": "Das Speichern von Momentaufnahmen für alle Kameras aktivieren oder deaktivieren; diese Einstellung kann für jede Kamera individuell überschrieben werden." + }, + "clean_copy": { + "label": "Saubere Kopie speichern", + "description": "Save an unannotated clean copy of snapshots in addition to annotated ones." + }, + "timestamp": { + "label": "Zeitstempel-Einblendung", + "description": "Füge einen Zeitstempel auf die von der API abgerufenen Momentaufnahmen ein." + }, + "bounding_box": { + "label": "Einblendung der Begrenzungsrahmen", + "description": "Zeichne Begrenzungsrahmen für verfolgte Objekte auf Momentaufnahmen aus der API." + }, + "crop": { + "label": "Ertragsübersicht", + "description": "Schnappschüsse aus der API auf die Begrenzungsrahmen der erkannten Objekte zuschneiden." + }, + "required_zones": { + "label": "Erforderliche Zonen", + "description": "Bereiche, die ein Objekt betreten muss, damit ein Schnappschuss gespeichert wird." + }, + "height": { + "label": "Höhe der Momentaufnahme", + "description": "Höhe (Pixel), auf die Schnappschüsse über die API skaliert werden sollen; leer lassen, um die Originalgröße beizubehalten." + }, + "retain": { + "label": "Aufbewahrungsdauer von Snapshots", + "description": "Aufbewahrungseinstellungen für Snapshots, einschließlich Standarddauer in Tagen und objektspezifischer Überschreibungen.", + "default": { + "label": "Standard-Aufbewahrungsfrist", + "description": "Standardmäßige Anzahl von Tagen, für die Snapshots aufbewahrt werden." + }, + "mode": { + "label": "Speichermodus", + "description": "Speichermodus: „all“ (alle Segmente speichern), „motion“ (Segmente mit Bewegung speichern) oder „active_objects“ (Segmente mit aktiven Objekten speichern)." + }, + "objects": { + "label": "Objektaufbewahrung", + "description": "Objektbezogene Überschreibungen für die Aufbewahrungsdauer von Snapshots." + } + }, + "quality": { + "label": "Qualität der Momentaufnahme", + "description": "Codierungsqualität für gespeicherte Momentaufnahmen (0–100)." + }, + "description": "Einstellungen für API-generierte Momentaufnahmen von verfolgten Objekten für alle Kameras; können für jede Kamera individuell überschrieben werden." + }, + "model": { + "label": "Erkennungsmodell", + "description": "Einstellungen zur Konfiguration eines benutzerdefinierten Objekterkennungsmodells und seiner Eingabeform.", + "path": { + "label": "Pfad zum benutzerdefinierten Objekterkennungsmodell", + "description": "Pfad zu einer benutzerdefinierten Erkennungsmodelldatei (oder plus:// für Frigate+-Modelle)." + }, + "labelmap_path": { + "label": "Label-Karte für benutzerdefinierten Objektdetektor", + "description": "Pfad zu einer Labelmap-Datei, die numerische Klassen dem Detektor als Zeichenfolgenbezeichnungen zuordnet." + }, + "width": { + "label": "Eingabebreite des Objekterkennungsmodells", + "description": "Breite des Modell-Eingabetensors in Pixeln." + }, + "height": { + "label": "Eingabehöhe für das Objekterkennungsmodell", + "description": "Höhe des Modell-Eingabetensors in Pixeln." + }, + "labelmap": { + "label": "Anpassung der Labelmap", + "description": "Überschreibt oder ordnet Einträge neu zu, um sie in die Standard-Labelmap zu integrieren." + }, + "attributes_map": { + "label": "Zuordnung der Objektbezeichnungen zu ihren Attributbezeichnungen", + "description": "Zuordnung von Objektbezeichnungen zu Attributbezeichnungen, die zum Hinzufügen von Metadaten verwendet werden (zum Beispiel „Auto“ -> [„Kennzeichen“])." + }, + "input_tensor": { + "label": "Form des Modell-Eingabetensors", + "description": "Vom Modell erwartetes Tensor-Format: „nhwc“ oder „nchw“." + }, + "input_pixel_format": { + "label": "Standard-Pixel-Farbformat", + "description": "Vom Modell erwarteter Pixel-Farbraum: „rgb“, „bgr“ oder „yuv“." + }, + "input_dtype": { + "label": "Modell-Eingangs-D-Typ", + "description": "Datentyp des Modell-Eingabetensors (z. B. „float32“)." + }, + "model_type": { + "label": "Typ des Objekterkennungsmodells", + "description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird." + } + }, + "genai": { + "label": "Konfiguration generativer KI", + "description": "Einstellungen für integrierte Anbieter generativer KI, die zur Erstellung von Objektbeschreibungen und Zusammenfassungen von Rezensionen verwendet werden.", + "api_key": { + "label": "API Schlüssel", + "description": "Von einigen Anbietern wird ein API-Schlüssel benötigt (kann auch über Umgebungsvariablen festgelegt werden)." + }, + "base_url": { + "label": "Base URL", + "description": "Basis-URL für selbst gehostete oder kompatible Anbieter (z. B. eine Ollama-Instanz)." + }, + "model": { + "label": "Model", + "description": "Das vom Anbieter bereitzustellende Modell zur Erstellung von Beschreibungen oder Zusammenfassungen." + }, + "provider": { + "label": "Anbieter", + "description": "Der zu verwendende GenAI-Anbieter (z. B.: Ollama, Gemini, OpenAI)." + }, + "roles": { + "label": "Rollen", + "description": "GenAI-Rollen (Nachrichten, Beschreibung, Einbettungen); ein Anbieter pro Rolle." + }, + "provider_options": { + "label": "Anbieter Optionen", + "description": "Zusätzliche anbieterspezifische Optionen, die an den GenAI-Client übergeben werden sollen." + }, + "runtime_options": { + "label": "Laufzeit Optinenen", + "description": "Laufzeitoptionen, die bei jedem Inferenzaufruf an den Anbieter übergeben werden." + } + }, + "timestamp_style": { + "label": "Format für Zeitstempel", + "position": { + "label": "Position des Zeitstempels", + "description": "Position des Zeitstempels auf dem Bild (tl/tr/bl/br)." + }, + "format": { + "label": "Zeitstempelformat", + "description": "Datums- und Uhrzeitformatzeichenfolge für Zeitstempel (Python-Datums- und Uhrzeitformatcodes)." + }, + "color": { + "label": "Farbe des Zeitstempels", + "description": "RGB-Farbwerte für den Zeitstempeltext (alle Werte zwischen 0 und 255).", + "red": { + "label": "Rot", + "description": "Rotwert (0–255) für die Farbe des Zeitstempels." + }, + "green": { + "label": "Grün", + "description": "Grünanteil (0–255) für die Farbe des Zeitstempels." + }, + "blue": { + "label": "Blau", + "description": "Blauer Farbanteil (0–255) für die Farbe des Zeitstempels." + } + }, + "thickness": { + "label": "Stärke der Zeitmarke", + "description": "Linienstärke des Zeitstempeltextes." + }, + "effect": { + "label": "Zeitstempeleffekt", + "description": "Visuelle Darstellung des Zeitstempeltextes (keine, durchgehend, Schatten)." + }, + "description": "Gestaltungsoptionen für Zeitstempel im Feed, die auf die Debug-Ansicht und Snapshots angewendet werden." + }, + "profiles": { + "label": "Profile", + "description": "Benannte Profildefinitionen mit aussagekräftigen Namen. Kameraprofile müssen auf die hier definierten Namen verweisen.", + "friendly_name": { + "label": "Anzeigename", + "description": "Anzeigename für dieses Profil, der in der Benutzeroberfläche angezeigt wird." + } + }, + "classification": { + "label": "Objektklassifizierung", + "description": "Einstellungen für Klassifizierungsmodelle, die zur Verfeinerung von Objektbezeichnungen oder zur Zustandsklassifizierung verwendet werden.", + "bird": { + "label": "Konfiguration der Vogelklassifizierung", + "description": "Einstellungen speziell für Modelle zur Klassifizierung von Vögeln.", + "enabled": { + "label": "Vogelklassifizierung", + "description": "Vogelklassifizierung aktivieren oder deaktivieren." + }, + "threshold": { + "label": "Mindestpunktzahl", + "description": "Mindestpunktzahl, die erforderlich ist, um eine Vogelklassifizierung zu akzeptieren." + } + }, + "custom": { + "label": "Benutzerdefinierte Klassifizierungsmodelle", + "description": "Konfiguration für benutzerdefinierte Klassifizierungsmodelle, die zur Objekt- oder Zustandserkennung verwendet werden.", + "enabled": { + "label": "Modell aktivieren", + "description": "Das benutzerdefinierte Klassifizierungsmodell aktivieren oder deaktivieren." + }, + "name": { + "label": "Modellname", + "description": "Bezeichner für das zu verwendende benutzerdefinierte Klassifizierungsmodell." + }, + "threshold": { + "label": "Punktschwelle", + "description": "Punktschwelle, die zur Änderung des Klassifizierungsstatus herangezogen wird." + }, + "save_attempts": { + "label": "Speicherungen", + "description": "Wie viele Klassifizierungsversuche sollen für die Benutzeroberfläche „Letzte Klassifizierungen“ gespeichert werden?" + }, + "object_config": { + "objects": { + "label": "Objekte klassifizieren", + "description": "Liste der Objekttypen, für die eine Objektklassifizierung durchgeführt werden soll." + }, + "classification_type": { + "label": "Klassifizierungstyp", + "description": "Verwendeter Klassifizierungstyp: „sub_label“ (fügt „sub_label“ hinzu) oder andere unterstützte Typen." + } + }, + "state_config": { + "cameras": { + "label": "Klassifizierungskameras", + "description": "Bildausschnitt und Einstellungen pro Kamera für die Klassifizierung des Laufzustands.", + "crop": { + "label": "Klassifizierungsfeld", + "description": "Zuschneidekoordinaten, die für die Klassifizierung mit dieser Kamera verwendet werden sollen." + } + }, + "motion": { + "description": "Falls zutreffend, führe die Klassifizierung durch, sobald innerhalb des angegebenen Ausschnitts eine Bewegung erkannt wird.", + "label": "Bei Bewegung ausführen" + }, + "interval": { + "label": "Klassifizierungsintervall", + "description": "Intervall (in Sekunden) zwischen den regelmäßigen Klassifizierungsläufen für die Zustandsklassifizierung." + } + } + } + }, + "camera_groups": { + "label": "Kameragruppen", + "description": "Konfiguration für benannte Kameragruppen, die zur Organisation der Kameras in der Benutzeroberfläche verwendet werden.", + "cameras": { + "label": "Kameraübersicht", + "description": "Liste der in dieser Gruppe enthaltenen Kameramodelle." + }, + "icon": { + "label": "Gruppensymbol", + "description": "Symbol, das in der Benutzeroberfläche die Kameragruppe darstellt." + }, + "order": { + "label": "Sortierreihenfolge", + "description": "Numerische Reihenfolge, nach der die Kameragruppen in der Benutzeroberfläche sortiert werden; höhere Zahlen erscheinen später." + } + }, + "active_profile": { + "label": "Aktives Profil", + "description": "Name des derzeit aktiven Profils. Nur zur Laufzeit gültig, wird nicht in YAML gespeichert." + }, + "camera_mqtt": { + "label": "MQTT", + "description": "Einstellungen für die Veröffentlichung von Bildern über MQTT.", + "enabled": { + "label": "Bild senden", + "description": "Aktivieren Sie für diese Kamera die Veröffentlichung von Bild-Snapshots für Objekte an MQTT-Themen." + }, + "timestamp": { + "label": "Zeitstempel hinzufügen", + "description": "Füge einen Zeitstempel auf Bilder ein, die über MQTT veröffentlicht werden." + }, + "bounding_box": { + "label": "Begrenzungsrahmen hinzufügen", + "description": "Zeichne Begrenzungsrahmen auf Bilder, die über MQTT veröffentlicht werden." + }, + "crop": { + "label": "Bild zuschneiden", + "description": "Bilder, die über MQTT veröffentlicht werden, werden auf die Begrenzungsrahmen der erkannten Objekte zugeschnitten." + }, + "height": { + "label": "Bildhöhe", + "description": "Höhe (in Pixeln) zur Größenanpassung von über MQTT veröffentlichten Bildern." + }, + "required_zones": { + "label": "Benötigte Zonen", + "description": "Zonen, die ein Objekt betreten muss, damit ein MQTT-Bild veröffentlicht wird." + }, + "quality": { + "label": "JPEG Qualität", + "description": "JPEG Qualität für über MQTT veröffentlichte Bilder (0–100)." + } + }, + "camera_ui": { + "label": "Kamera UI", + "description": "Die Reihenfolge und Sichtbarkeit dieser Kamera wird in der UI angezeigt. Die Reihenfolge wirkt sich auf das Standard-Dashboard aus. Für eine feinere Kontrolle verwenden Sie Kamera-Gruppen.", + "order": { + "label": "UI-Reihenfolge", + "description": "Numerische Reihenfolge, nach der die Kamera in der Benutzeroberfläche sortiert wird (Standard-Dashboard und Listen); höhere Zahlen erscheinen später." + }, + "dashboard": { + "label": "In der Benutzeroberfläche anzeigen", + "description": "Schalte ein, ob diese Kamera überall in der Benutzeroberfläche von „Frigate“ sichtbar ist. Wenn du diese Option deaktivierst, musst du die Konfiguration manuell bearbeiten, um diese Kamera wieder in der Benutzeroberfläche anzuzeigen." + }, + "review": { + "label": "In der Überprüfung anzeigen", + "description": "Legen Sie fest, ob diese Kamera in der Übersicht angezeigt wird (auf der Übersichtsseite sowie im Kamerafilter, in der Bewegungsübersicht und in der Verlaufsansicht)." + } + } +} diff --git a/web/public/locales/de/config/groups.json b/web/public/locales/de/config/groups.json new file mode 100644 index 0000000..c1b286e --- /dev/null +++ b/web/public/locales/de/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Globale Erkennung", + "sensitivity": "Globale Empfindlichkeit" + }, + "cameras": { + "detection": "Erkennung", + "sensitivity": "Empfindlichkeit" + } + }, + "timestamp_style": { + "global": { + "appearance": "Globale Erscheinung" + }, + "cameras": { + "appearance": "Erscheinungsbild" + } + }, + "motion": { + "global": { + "sensitivity": "Globale Empfindlichkeit", + "algorithm": "Globaler Algorithmus" + }, + "cameras": { + "sensitivity": "Empfindlichkeit", + "algorithm": "Algorithmus" + } + }, + "snapshots": { + "global": { + "display": "Globales Display" + }, + "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" + } + } +} diff --git a/web/public/locales/de/config/validation.json b/web/public/locales/de/config/validation.json new file mode 100644 index 0000000..db40fc0 --- /dev/null +++ b/web/public/locales/de/config/validation.json @@ -0,0 +1,35 @@ +{ + "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." + } + }, + "detect": { + "dimensionMustBeEven": "Es muss eine gerade Zahl sein." + } +} diff --git a/web/public/locales/de/objects.json b/web/public/locales/de/objects.json new file mode 100644 index 0000000..e2236fc --- /dev/null +++ b/web/public/locales/de/objects.json @@ -0,0 +1,129 @@ +{ + "boat": "Boot", + "traffic_light": "Ampel", + "fire_hydrant": "Hydrant", + "stop_sign": "Stoppschild", + "bench": "Bank", + "bird": "Vogel", + "cow": "Kuh", + "elephant": "Elefant", + "bear": "Bär", + "zebra": "Zebra", + "giraffe": "Giraffe", + "shoe": "Schuh", + "tie": "Krawatte", + "frisbee": "Frisbee", + "skis": "Skier", + "kite": "Drachen", + "skateboard": "Skateboard", + "surfboard": "Surfbrett", + "plate": "Platte", + "cup": "Tasse", + "spoon": "Löffel", + "sandwich": "Sandwich", + "broccoli": "Brokkoli", + "carrot": "Karotte", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Kuchen", + "chair": "Stuhl", + "couch": "Sofa", + "bed": "Bett", + "dining_table": "Esstisch", + "toilet": "Toilette", + "door": "Tür", + "sink": "Waschbecken", + "refrigerator": "Kühlschrank", + "book": "Buch", + "bbq_grill": "BBQ Grill", + "amazon": "Amazon", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "Post", + "postnl": "PostNL", + "nzpost": "NZPost", + "purolator": "Purolator", + "postnord": "PostNord", + "dpd": "DPD", + "snowboard": "Snowboard", + "baseball_bat": "Baseballschläger", + "knife": "Messer", + "squirrel": "Eichhörnchen", + "animal": "Tier", + "blender": "Mixer", + "vase": "Vase", + "orange": "Orange", + "teddy_bear": "Teddybär", + "on_demand": "Auf Anfrage", + "scissors": "Schere", + "ups": "UPS", + "train": "Zug", + "toaster": "Toaster", + "clock": "Uhr", + "mirror": "Spiegel", + "backpack": "Rucksack", + "motorcycle": "Motorrad", + "window": "Fenster", + "toothbrush": "Zahnbürste", + "package": "Paket", + "hair_brush": "Haarbürste", + "apple": "Apfel", + "banana": "Banane", + "parking_meter": "Parkuhr", + "oven": "Ofen", + "umbrella": "Regenschirm", + "eye_glasses": "Brillen", + "robot_lawnmower": "Mähroboter", + "potted_plant": "Topfpflanze", + "waste_bin": "Abfallbehälter", + "license_plate": "Kennzeichen", + "bottle": "Flasche", + "deer": "Reh", + "usps": "USPS", + "person": "Person", + "bowl": "Schüssel", + "microwave": "Mikrowelle", + "bicycle": "Fahrrad", + "car": "Auto", + "fork": "Gabel", + "tv": "Fernseher", + "laptop": "Laptop", + "mouse": "Maus", + "goat": "Ziege", + "keyboard": "Klavier", + "cell_phone": "Handy", + "remote": "Fernbedienung", + "airplane": "Flugzeug", + "tennis_racket": "Tennisschläger", + "bus": "Bus", + "street_sign": "Straßenschild", + "horse": "Pferd", + "bark": "Bellen", + "cat": "Katze", + "wine_glass": "Weinglas", + "dog": "Hund", + "sheep": "Schaf", + "hat": "Hut", + "hot_dog": "Hot Dog", + "baseball_glove": "Baseballhandschuh", + "suitcase": "Koffer", + "handbag": "Handtasche", + "sports_ball": "Sportball", + "hair_dryer": "Haartrockner", + "vehicle": "Fahrzeug", + "face": "Gesicht", + "fox": "Fuchs", + "desk": "Schreibtisch", + "raccoon": "Waschbär", + "rabbit": "Kaninchen", + "gls": "GLS", + "canada_post": "Kanada Post", + "royal_mail": "Royal-Mail", + "school_bus": "Schulbus", + "skunk": "Stinktier", + "kangaroo": "Känguruh", + "baby": "Baby", + "baby_stroller": "Kinderwagen", + "rickshaw": "Rikscha", + "rodent": "Nagetier" +} diff --git a/web/public/locales/de/views/chat.json b/web/public/locales/de/views/chat.json new file mode 100644 index 0000000..9f4dcb7 --- /dev/null +++ b/web/public/locales/de/views/chat.json @@ -0,0 +1,72 @@ +{ + "documentTitle": "Chat - Frigate", + "title": "Frigate Chat", + "subtitle": "Ihr KI-Assistent für die Kameraverwaltung und Analysen", + "placeholder": "Frag mich alles...", + "error": "Es ist ein Fehler aufgetreten. Bitte versuche es erneut.", + "processing": "Wird verarbeitet...", + "toolsUsed": "Verwendet: {{tools}}", + "showTools": "Werkzeuge anzeigen ({{count}})", + "hideTools": "Werkzeuge ausblenden", + "call": "Anruf", + "result": "Ergebnis", + "arguments": "Argumente:", + "response": "Antwort:", + "attachment_chip_label": "{{label}} auf der {{camera}}", + "attachment_chip_remove": "Anhang entfernen", + "open_in_explore": "In „Explore“ öffnen", + "attach_event_aria": "Ereignis {{eventId}} hinzufügen", + "attachment_picker_paste_label": "Oder fügen Sie die Ereignis-ID ein", + "attachment_picker_attach": "Anhängen", + "attachment_picker_placeholder": "Ereignis hinzufügen", + "quick_reply_find_similar": "Ähnliche Sichtungen finden", + "quick_reply_tell_me_more": "Erzähl mir mehr darüber", + "quick_reply_when_else": "Wann wurde es sonst noch gesehen?", + "quick_reply_find_similar_text": "Ähnliche Sichtungen finden.", + "quick_reply_tell_me_more_text": "Erzähl mir mehr darüber.", + "quick_reply_when_else_text": "Wann gab es das sonst noch?", + "anchor": "Referenz", + "similarity_score": "Ähnlichkeit", + "no_similar_objects_found": "Es wurden keine ähnlichen Objekte gefunden.", + "semantic_search_required": "Die semantische Suche muss aktiviert sein, um ähnliche Objekte zu finden.", + "send": "Senden", + "suggested_requests": "Versuchen Sie doch mal zu fragen:", + "starting_requests": { + "show_recent_events": "Aktuelle Ereignisse anzeigen", + "show_camera_status": "Kamerastatus anzeigen", + "recap": "Was ist passiert, während ich weg war?", + "watch_camera": "Beobachten Sie eine Kamera auf Bewegungen" + }, + "starting_requests_prompts": { + "show_recent_events": "Zeige mir die Ereignisse der letzten Stunde", + "show_camera_status": "Wie ist der aktuelle Status meiner Kameras?", + "recap": "Was ist passiert, während ich weg war?", + "watch_camera": "Pass auf die Haustür auf und sag mir Bescheid, wenn jemand kommt" + }, + "new_chat": "Neuer Chat", + "settings": { + "title": "Chat Einstellung", + "show_stats": { + "title": "Statistiken anzeigen", + "desc": "Generierungsrate und Kontextgröße für Chat-Antworten anzeigen.", + "while_generating": "Während der Erstellung", + "always": "Immer" + }, + "auto_scroll": { + "title": "Auto scrollen", + "desc": "Verfolgen Sie neue Nachrichten, sobald sie eintreffen." + } + }, + "stats": { + "context": "{{tokens}} tokens", + "tokens_per_second": "{{rate}} t/s" + }, + "reasoning": { + "active": "Begründung…", + "show": "Begründung anzeigen", + "hide": "Begründung ausblenden" + }, + "thinking": { + "toggle": "Umschalten" + } +} diff --git a/web/public/locales/de/views/classificationModel.json b/web/public/locales/de/views/classificationModel.json new file mode 100644 index 0000000..4b55ff2 --- /dev/null +++ b/web/public/locales/de/views/classificationModel.json @@ -0,0 +1,199 @@ +{ + "documentTitle": "Klassifikationsmodelle - Frigate", + "details": { + "scoreInfo": "Die Punktzahl gibt die durchschnittliche Konfidenz aller Erkennungen dieses Objekts wieder.", + "none": "Keiner", + "unknown": "Unbekannt" + }, + "button": { + "deleteClassificationAttempts": "Lösche klassifizierte Bilder", + "renameCategory": "Klasse umbenennen", + "deleteCategory": "Klasse löschen", + "deleteImages": "Bilder löschen", + "trainModel": "Modell trainieren", + "addClassification": "Klassifikationsmodell hinzufügen", + "deleteModels": "Modell löschen", + "editModel": "Modell bearbeiten" + }, + "tooltip": { + "trainingInProgress": "Modell wird gerade trainiert", + "noNewImages": "Keine weiteren Bilder zum trainieren. Bitte klassifiziere weitere Bilder im Datensatz.", + "noChanges": "Keine Veränderungen des Datensatzes seit dem letzten Training.", + "modelNotReady": "Modell ist nicht bereit für das Training" + }, + "toast": { + "success": { + "deletedCategory_one": "Klasse gelöscht", + "deletedCategory_other": "Klassen {{count}} gelöscht", + "deletedImage_one": "{{count}} Bild gelöscht", + "deletedImage_other": "{{count}} Bilder gelöscht", + "deletedModel_one": "{{count}} Modell erfolgreich gelöscht", + "deletedModel_other": "{{count}} Modelle erfolgreich gelöscht", + "categorizedImage": "Erfolgreich klassifizierte Bilder", + "trainedModel": "Modell erfolgreich trainiert.", + "trainingModel": "Modelltraining erfolgreich gestartet.", + "updatedModel": "Modellkonfiguration erfolgreich aktualisiert", + "renamedCategory": "Klasse erfolgreich in {{name}} umbenannt", + "reclassifiedImage": "Erfolgreich neu klassifiziertes Bild" + }, + "error": { + "deleteImageFailed": "Löschen fehlgeschlagen: {{errorMessage}}", + "deleteCategoryFailed": "Löschen der Klasse fehlgeschlagen: {{errorMessage}}", + "deleteModelFailed": "Model konnte nicht gelöscht werden: {{errorMessage}}", + "trainingFailedToStart": "Modelltraining konnte nicht gestartet werden: {{errorMessage}}", + "updateModelFailed": "Aktualisierung des Modells fehlgeschlagen: {{errorMessage}}", + "renameCategoryFailed": "Umbenennung der Klasse fehlgeschlagen: {{errorMessage}}", + "categorizeFailed": "Bildkategorisierung fehlgeschlagen: {{errorMessage}}", + "trainingFailed": "Modelltraining fehlgeschlagen. Details sind in den Frigate-Protokollen zu finden.", + "reclassifyFailed": "Die Neuklassifizierung des Bildes ist fehlgeschlagen: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Klasse löschen", + "desc": "Möchten Sie die Klasse {{name}} wirklich löschen? Dadurch werden alle zugehörigen Bilder dauerhaft gelöscht und das Modell muss neu trainiert werden.", + "minClassesTitle": "Klasse kann nicht gelöscht werden", + "minClassesDesc": "Ein Klassifizierungsmodell benötigt mindestens zwei Klassen. Fügen Sie eine weitere Klasse hinzu, bevor Sie diese löschen." + }, + "deleteModel": { + "title": "Klassifizierungsmodell löschen", + "single": "Möchten Sie {{name}} wirklich löschen? Dadurch werden alle zugehörigen Daten, einschließlich Bilder und Trainingsdaten, dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", + "desc_one": "Möchtest du {{count}} Modell wirklich löschen? Dadurch werden alle zugehörigen Daten, einschließlich Bilder und Trainingsdaten, dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", + "desc_other": "Möchtest du {{count}} Modelle wirklich löschen? Dadurch werden alle zugehörigen Daten, einschließlich Bilder und Trainingsdaten, dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden." + }, + "edit": { + "title": "Klassifikationsmodell bearbeiten", + "descriptionState": "Bearbeite die Klassen für dieses Zustandsklassifikationsmodell. Änderungen erfordern ein erneutes Trainieren des Modells.", + "descriptionObject": "Bearbeite den Objekttyp und Klassifizierungstyp für dieses Objektklassifikationsmodell.", + "stateClassesInfo": "Hinweis: Die Änderung der Statusklassen erfordert ein erneutes Trainieren des Modells mit den aktualisierten Klassen." + }, + "deleteDatasetImages": { + "title": "Datensatz Bilder löschen", + "desc_one": "Bist du sicher, dass {{count}} Bild von {{dataset}} gelöscht werden sollen? Diese Aktion kann nicht rückgängig gemacht werden und erfordert ein erneutes Trainieren des Modells.", + "desc_other": "Bist du sicher, dass {{count}} Bilder von {{dataset}} gelöscht werden sollen? Diese Aktion kann nicht rückgängig gemacht werden und erfordert ein erneutes Trainieren des Modells." + }, + "deleteTrainImages": { + "title": "Trainingsbilder löschen", + "desc_one": "Bist du sicher, dass du {{count}} Bild löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "desc_other": "Bist du sicher, dass du {{count}} Bilder löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden." + }, + "renameCategory": { + "title": "Klasse umbenennen", + "desc": "Neuen Namen für {{name}} eingeben. Das Modell muss neu trainiert werden, damit die Änderungen wirksam werden." + }, + "description": { + "invalidName": "Ungültiger Name. Namen dürfen nur Buchstaben, Zahlen, Leerzeichen, Apostrophe, Unterstriche und Bindestriche enthalten." + }, + "train": { + "title": "Neue Klassifizierungen", + "titleShort": "frisch", + "aria": "Neue Klassifizierungen auswählen" + }, + "categories": "Klassen", + "createCategory": { + "new": "Neue Klasse erstellen" + }, + "categorizeImageAs": "Bild klassifizieren als:", + "categorizeImage": "Bild klassifizieren", + "menu": { + "objects": "Objekte", + "states": "Zustände" + }, + "noModels": { + "object": { + "title": "Keine Objektklassifikationsmodelle", + "description": "Erstelle ein benutzerdefiniertes Objektklassifikationsmodell, um erkannte Objekte zu klassifizieren.", + "buttonText": "Objektklassifikationsmodell erstellen" + }, + "state": { + "title": "Keine Zustandsklassifikationsmodelle", + "description": "Erstellen Sie ein benutzerdefiniertes Zustandsklassifikationsmodell, um Zustandsänderungen in bestimmten Kamerabereichen zu überwachen und zu klassifizieren.", + "buttonText": "Zustandsklassifikationsmodell erstellen" + } + }, + "wizard": { + "title": "Neues Klassifikationsmodell erstellen", + "steps": { + "nameAndDefine": "Benennen und definieren", + "stateArea": "Überwachungsbereich", + "chooseExamples": "Beispiel auswählen" + }, + "step1": { + "description": "Zustandsmodelle überwachen fest definierte Kamerabereiche auf Veränderungen (z. B. Tür offen/geschlossen). Objektmodelle klassifizieren erkannte Objekte genauer (z. B. in bekannte Tiere, Lieferanten usw.).", + "name": "Name", + "namePlaceholder": "Modellname eingeben ...", + "type": "Typ", + "typeState": "Zustand", + "typeObject": "Objekt", + "objectLabel": "Objektbezeichnung", + "objectLabelPlaceholder": "Auswahl Objekt Typ...", + "classificationType": "Klassifizierungstyp", + "classificationTypeTip": "Etwas über Klassifizierungstyp lernen", + "classificationTypeDesc": "Unterbezeichnungen fügen dem Objektnamen zusätzlichen Text hinzu (z. B. „Person: UPS“). Attribute sind durchsuchbare Metadaten, die separat in den Objektmetadaten gespeichert sind.", + "classificationSubLabel": "Unterlabel", + "classificationAttribute": "Attribut", + "classes": "Klassen", + "states": "Zustände", + "classesTip": "Mehr über Klassen erfahren", + "classesStateDesc": "Definieren Sie die verschiedenen Zustände, in denen sich Ihr Kamerabereich befinden kann. Beispiel: „offen” und „geschlossen” für ein Garagentor.", + "classesObjectDesc": "Definieren Sie die verschiedenen Kategorien, in die erkannte Objekte klassifiziert werden sollen. Beispiel: „Lieferant“, „Bewohner“, „Fremder“ für die Klassifizierung von Personen.", + "classPlaceholder": "Klassenbezeichnung eingeben...", + "errors": { + "nameRequired": "Der Modellname ist erforderlich", + "nameLength": "Der Modellname darf maximal 64 Zeichen lang sein", + "nameOnlyNumbers": "Der Modellname darf nicht nur aus Zahlen bestehen", + "classRequired": "Mindestens eine Klasse ist erforderlich", + "classesUnique": "Der Klassenname muss eindeutig sein", + "stateRequiresTwoClasses": "Zustandsmodelle erfordern mindestens zwei Klassen", + "objectLabelRequired": "Bitte wähle eine Objektbeschriftung", + "objectTypeRequired": "Bitte wählen Sie einen Klassifizierungstyp aus", + "noneNotAllowed": "Die Klasse „none“ ist nicht zulässig" + } + }, + "step2": { + "description": "Wählen Sie Kameras aus und legen Sie für jede Kamera den zu überwachenden Bereich fest. Das Modell klassifiziert den Zustand dieser Bereiche.", + "cameras": "Kameras", + "selectCamera": "Kamera auswählen", + "noCameras": "Klicke + zum Hinzufügen von Kameras", + "selectCameraPrompt": "Wählen Sie eine Kamera aus der Liste aus, um ihren Überwachungsbereich festzulegen" + }, + "step3": { + "selectImagesPrompt": "Wählen Sie alle Bilder mit: {{className}}", + "selectImagesDescription": "Klicken Sie auf die Bilder, um sie auszuwählen. Klicken Sie auf „Weiter“, wenn Sie mit dieser Klasse fertig sind.", + "allImagesRequired_one": "Bitte klassifizieren Sie alle Bilder. {{count}} Bild verbleibend.", + "allImagesRequired_other": "Bitte klassifizieren Sie alle Bilder. {{count}} Bilder verbleiben.", + "generating": { + "title": "Beispielbilder generieren", + "description": "Frigate extrahiert repräsentative Bilder aus Ihren Aufnahmen. Dies kann einen Moment dauern..." + }, + "training": { + "title": "Trainiere Modell", + "description": "Ihr Modell wird im Hintergrund trainiert. Schließen Sie diesen Dialog, und Ihr Modell wird ausgeführt, sobald das Training abgeschlossen ist." + }, + "retryGenerate": "Generierung wiederholen", + "noImages": "Keine Bilder generiert", + "classifying": "Klassifizieren und Trainieren...", + "trainingStarted": "Training wurde erfolgreich gestartet", + "errors": { + "noCameras": "Keine Kameras konfiguriert", + "noObjectLabel": "Kein Objektlabel ausgewählt", + "generateFailed": "Beispiele konnten nicht generiert werden: {{error}}", + "generationFailed": "Generierung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "classifyFailed": "Bilder konnten nicht klassifiziert werden: {{error}}" + }, + "generateSuccess": "Erfolgreich generierte Beispielbilder", + "modelCreated": "Modell erfolgreich erstellt. Verwenden Sie die Ansicht „Aktuelle Klassifizierungen“, um Bilder für fehlende Zustände hinzuzufügen und trainieren Sie dann das Modell erneut.", + "missingStatesWarning": { + "title": "Beispiele für fehlende Klassen", + "description": "Nicht alle Klassen enthalten Beispiele. Versuchen Sie, neue Beispiele zu generieren, um die fehlende Klasse zu finden, oder fahren Sie fort und fügen Sie Bilder später über die Ansicht „Letzte Klassifizierungen“ hinzu." + }, + "refreshExamples": "Neue Beispiele erstellen", + "refreshConfirm": { + "title": "Neue Beispiele erstellen?", + "description": "Dadurch wird eine neue Reihe von Bildern generiert und alle Auswahlen, einschließlich aller bisherigen Klassen, werden gelöscht. Sie müssen für alle Klassen erneut Beispiele auswählen." + } + } + }, + "none": "Keiner", + "reclassifyImageAs": "Bild neu klassifizieren als:", + "reclassifyImage": "Bild neu klassifizieren" +} diff --git a/web/public/locales/de/views/configEditor.json b/web/public/locales/de/views/configEditor.json new file mode 100644 index 0000000..86959e1 --- /dev/null +++ b/web/public/locales/de/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Konfigurationseditor", + "copyConfig": "Konfiguration kopieren", + "saveAndRestart": "Sichern und Neustarten", + "saveOnly": "Nur Sichern", + "toast": { + "error": { + "savingError": "Fehler beim Speichern der Konfiguration" + }, + "success": { + "copyToClipboard": "Konfiguration in Zwischenablage kopiert." + } + }, + "documentTitle": "Konfigurationseditor – Frigate", + "confirm": "Verlassen ohne zu Speichern?", + "safeConfigEditor": "Konfiguration Editor (abgesicherter Modus)", + "safeModeDescription": "Frigate ist aufgrund eines Konfigurationsvalidierungsfehlers im abgesicherten Modus." +} diff --git a/web/public/locales/de/views/events.json b/web/public/locales/de/views/events.json new file mode 100644 index 0000000..c943bec --- /dev/null +++ b/web/public/locales/de/views/events.json @@ -0,0 +1,94 @@ +{ + "alerts": "Alarme", + "detections": "Erkennungen", + "motion": { + "label": "Bewegung", + "only": "nur Bewegung" + }, + "empty": { + "alert": "Es gibt keine zu prüfenden Alarme", + "detection": "Es gibt keine zu prüfenden Erkennungen", + "motion": "Keine Bewegungsdaten gefunden", + "recordingsDisabled": { + "title": "Aufzeichnungen müssen aktiviert sein", + "description": "Überprüfungselemente können nur für eine Kamera erstellt werden, wenn Aufzeichnungen für diese Kamera aktiviert sind." + } + }, + "timeline": { + "label": "Zeitleiste" + }, + "timeline.aria": "Zeitleiste auswählen", + "events": { + "label": "Ereignisse", + "noFoundForTimePeriod": "Keine Ereignisse für diesen Zeitraum gefunden.", + "aria": "Wähle Ereignisse aus" + }, + "documentTitle": "Überprüfung - Frigate", + "recordings": { + "documentTitle": "Aufnahmen - Frigate", + "invalidSharedLink": "Der Link zur zeitgestempelten Aufzeichnung kann aufgrund eines Parsing-Fehlers nicht geöffnet werden.", + "invalidSharedCamera": "Der Link zur zeitgestempelten Aufzeichnung kann nicht geöffnet werden, da es sich um eine unbekannte oder nicht autorisierte Kamera handelt." + }, + "calendarFilter": { + "last24Hours": "Letzte 24 Stunden" + }, + "newReviewItems": { + "label": "Neue zu prüfende Objekte anschauen", + "button": "Neue zu prüfende Objekte" + }, + "markTheseItemsAsReviewed": "Diese Objekte als geprüft kennzeichnen", + "camera": "Kamera", + "allCameras": "Alle Kameras", + "markAsReviewed": "Als geprüft kennzeichnen", + "selected_one": "{{count}} ausgewählt", + "selected_other": "{{count}} ausgewählt", + "detected": "erkannt", + "suspiciousActivity": "Verdächtige Aktivität", + "threateningActivity": "Bedrohliche Aktivität", + "zoomIn": "Hereinzoomen", + "zoomOut": "Herauszoomen", + "detail": { + "label": "Detail", + "aria": "Detailansicht umschalten", + "trackedObject_one": "{{count}} Objekt", + "trackedObject_other": "{{count}} Objekte", + "noObjectDetailData": "Keine detaillierten Daten des Objekt verfügbar.", + "noDataFound": "Keine Detaildaten zur Überprüfung", + "settings": "Detailansicht Einstellungen", + "alwaysExpandActive": { + "desc": "Immer die Objektdetails vom aktivem Überprüfungselement erweitern, sofern verfügbar.", + "title": "Immer aktiv erweitern" + } + }, + "objectTrack": { + "trackedPoint": "Verfolgter Punkt", + "clickToSeek": "Klicke, um zu dieser Zeit zu springen" + }, + "normalActivity": "normal", + "needsReview": "benötigt Überprüfung", + "securityConcern": "Sicherheitsbedenken", + "select_all": "alle", + "motionSearch": { + "menuItem": "Bewegungssuche", + "openMenu": "Kamera Optionen" + }, + "motionPreviews": { + "menuItem": "Bewegungsvorschau anzeigen", + "title": "Bewegungsvorschau: {{camera}}", + "mobileSettingsTitle": "Einstellungen für die Bewegungsvorschau", + "mobileSettingsDesc": "Passen Sie die Wiedergabegeschwindigkeit und die Dimmung an und wählen Sie ein Datum aus, um Clips mit nur Bewegungen anzusehen.", + "dim": "düster", + "dimAria": "Dimmintensität einstellen", + "dimDesc": "Erhöhen Sie die Dimmung, um die Sichtbarkeit des Bewegungsbereichs zu verbessern.", + "speed": "Geschwindigkeit", + "speedAria": "Vorschau-Wiedergabegeschwindigkeit auswählen", + "speedDesc": "Wählen Sie aus, wie schnell die Vorschau-Clips abgespielt werden sollen.", + "back": "zurück", + "empty": "Keine Vorschau verfügbar", + "noPreview": "Vorschau nicht verfügbar", + "seekAria": "{{camera}} Player suchen bis {{time}}", + "filter": "Filter", + "filterDesc": "Wählen Sie Bereiche aus, um nur Clips mit Bewegungen in diesen Regionen anzuzeigen.", + "filterClear": "Säubern" + } +} diff --git a/web/public/locales/de/views/explore.json b/web/public/locales/de/views/explore.json new file mode 100644 index 0000000..071d887 --- /dev/null +++ b/web/public/locales/de/views/explore.json @@ -0,0 +1,317 @@ +{ + "details": { + "timestamp": "Zeitstempel", + "item": { + "title": "Item-Details begutachten", + "desc": "Item-Details begutachten", + "button": { + "share": "Diese Aufnahme teilen", + "viewInExplore": "Ansicht in Erkunden" + }, + "tips": { + "hasMissingObjects": "Passe die Konfiguration an, so dass Frigate verfolgte Objekte für die folgenden Kategorien speichert: {{objects}}", + "mismatch_one": "{{count}} nicht verfügbares Objekt wurde entdeckt und in diese Überprüfung einbezogen. Dieses Objekt hat sich entweder nicht für einen Alarm oder eine Erkennung qualifiziert oder wurde bereits bereinigt/gelöscht.", + "mismatch_other": "{{count}} nicht verfügbare Objekte wurden entdeckt und in diese Überprüfung einbezogen. Diese Objekte haben sich entweder nicht für einen Alarm oder eine Erkennung qualifiziert oder wurden bereits bereinigt/gelöscht." + }, + "toast": { + "success": { + "updatedSublabel": "Unterkategorie erfolgreich aktualisiert.", + "updatedLPR": "Nummernschild erfolgreich aktualisiert.", + "regenerate": "Eine neue Beschreibung wurde von {{provider}} angefordert. Je nach Geschwindigkeit des Anbieters kann es einige Zeit dauern, bis die neue Beschreibung generiert ist.", + "audioTranscription": "Die Audio-Transkription wurde erfolgreich angefordert. Je nach Geschwindigkeit Ihres Frigate-Servers kann die Transkription einige Zeit in Anspruch nehmen.", + "updatedAttributes": "Attribute erfolgreich aktualisiert." + }, + "error": { + "regenerate": "Der Aufruf von {{provider}} für eine neue Beschreibung ist fehlgeschlagen: {{errorMessage}}", + "updatedSublabelFailed": "Untekategorie konnte nicht aktualisiert werden: {{errorMessage}}", + "updatedLPRFailed": "Aktualisierung des Kennzeichens fehlgeschlagen: {{errorMessage}}", + "audioTranscription": "Die Anforderung der Audio Transkription ist fehlgeschlagen: {{errorMessage}}", + "updatedAttributesFailed": "Attribute konnten nicht aktualisiert werden: {{errorMessage}}" + } + } + }, + "label": "Label", + "zones": "Zonen", + "editSubLabel": { + "title": "Unterkategorie bearbeiten", + "desc": "Geben Sie eine neue Unterkategorie für dieses {{label}} ein", + "descNoLabel": "Geben Sie eine neue Unterkategorie für dieses verfolgte Objekt ein" + }, + "editLPR": { + "title": "Kennzeichen bearbeiten", + "desc": "Gib einen neuen Kennzeichenwert für dieses {{label}} ein", + "descNoLabel": "Gib einen neuen Kennzeichenwert für dieses verfolgte Objekt ein" + }, + "topScore": { + "label": "Beste Ergebnisse", + "info": "Die höchste Punktzahl ist der höchste Medianwert für das verfolgte Objekt und kann daher von der auf der Miniaturansicht des Suchergebnisses angezeigten Punktzahl abweichen." + }, + "recognizedLicensePlate": "Erkanntes Kennzeichen", + "estimatedSpeed": "Geschätzte Geschwindigkeit", + "objects": "Objekte", + "camera": "Kamera", + "button": { + "findSimilar": "Finde ähnliche", + "regenerate": { + "title": "Erneuern", + "label": "Beschreibung des verfolgten Objekts neu generieren" + } + }, + "description": { + "label": "Beschreibung", + "placeholder": "Beschreibung des verfolgten Objekts", + "aiTips": "Frigate wird erst dann eine Beschreibung vom generativen KI-Anbieter anfordern, wenn der Lebenszyklus des verfolgten Objekts beendet ist." + }, + "expandRegenerationMenu": "Erneuerungsmenü erweitern", + "regenerateFromSnapshot": "Aus Snapshot neu generieren", + "regenerateFromThumbnails": "Aus Vorschaubild neu generieren", + "tips": { + "descriptionSaved": "Erfolgreich gespeicherte Beschreibung", + "saveDescriptionFailed": "Die Aktualisierung der Beschreibung ist fehlgeschlagen: {{errorMessage}}" + }, + "snapshotScore": { + "label": "Schnappschuss Bewertung" + }, + "score": { + "label": "Treffer" + }, + "editAttributes": { + "title": "Attribute bearbeiten", + "desc": "Wählen Sie Klassifizierungsattribute für dieses {{label}} aus" + }, + "attributes": "Klassifizierungsattribute", + "title": { + "label": "Titel" + }, + "scoreInfo": "Punkte Info" + }, + "documentTitle": "Erkunde - Frigate", + "generativeAI": "Generative KI", + "exploreIsUnavailable": { + "title": "Erkunden ist nicht Verfügbar", + "embeddingsReindexing": { + "context": "Erkunden kann nach der Re-Indexierung der verfolgten Objekte verwendet werden.", + "startingUp": "Startet…", + "estimatedTime": "Voraussichtlich verbleibende Zeit:", + "finishingShortly": "Bald erledigt", + "step": { + "thumbnailsEmbedded": "Vorschaubilder eingebettet: ", + "descriptionsEmbedded": "Beschreibungen eingebettet: ", + "trackedObjectsProcessed": "Verfolgte Objekte bearbeitet: " + } + }, + "downloadingModels": { + "setup": { + "visionModel": "Vision Model", + "visionModelFeatureExtractor": "Vision Model Feature Extraktor", + "textModel": "Text Model", + "textTokenizer": "Text Tokenizer" + }, + "tips": { + "context": "Sie sollten eine Re-Indexierung der verfolgten Objekte durchführen, sobald die Modelle heruntergeladen sind.", + "documentation": "Lesen Sie die Dokumentation" + }, + "error": "Ein Fehler ist aufgetreten. Bitte prüfen Sie die Frigate Logs.", + "context": "Frigate lädt derzeit benötigte Modelle für den Support des \"Semantic Search\"-Features. Je nach der Geschwindigkeit der Netzwerkverbindung kann dies einige Minuten in Anspruch nehmen." + } + }, + "trackedObjectDetails": "Details zu verfolgtem Objekt", + "objectLifecycle": { + "noImageFound": "Kein Bild für diesen Zeitstempel gefunden.", + "createObjectMask": "Objekt-Maske erstellen", + "lifecycleItemDesc": { + "entered_zone": "{{label}} hat {{zones}} betreten", + "visible": "{{label}} erkannt", + "attribute": { + "other": "{{label}} erkannt als {{attribute}}", + "faceOrLicense_plate": "{{attribute}} erkannt für {{label}}" + }, + "external": "{{label}} erkannt", + "active": "{{label}} wurde aktiv", + "gone": "{{label}} hat verlassen", + "stationary": "{{label}} wurde stationär", + "heard": "{{label}} gehört", + "header": { + "ratio": "Verhältnis", + "area": "Bereich", + "zones": "Zonen" + } + }, + "annotationSettings": { + "offset": { + "documentation": "Lesen Sie die Dokumentation ", + "label": "Anmerkungen Versatz", + "desc": "Diese Daten stammen aus dem Erkennungs-Feed der Kamera, werden aber mit Bildern aus dem Aufnahme-Feed überlagert. Es ist unwahrscheinlich, dass die beiden Streams perfekt synchronisiert sind. Daher stimmen die Bounding Box und das Filmmaterial nicht perfekt überein. Das Feld annotation_offset kann jedoch verwendet werden, um dies anzupassen.", + "millisecondsToOffset": "Millisekunden, um die Erkennungen verschoben werden soll. Standard: 0", + "tips": "TIPP: Stelle dir einen Ereignisclip vor, in dem eine Person von links nach rechts läuft. Wenn die Bounding Box der Ereigniszeitleiste durchgehend links von der Person liegt, sollte der Wert verringert werden. Ähnlich verhält es sich, wenn eine Person von links nach rechts geht und die Bounding Box durchgängig vor der Person liegt, dann sollte der Wert erhöht werden.", + "toast": { + "success": "Versatz für {{camera}} wurde in der Konfigurationsdatei gespeichert. Starten Sie Frigate neu, um Ihre Änderungen zu übernehmen." + } + }, + "showAllZones": { + "title": "Zeige alle Zonen", + "desc": "Immer Zonen auf Rahmen anzeigen, in die Objekte eingetreten sind." + }, + "title": "Anmerkungseinstellungen" + }, + "adjustAnnotationSettings": "Anmerkungseinstellungen anpassen", + "title": "Objekt-Lebenszyklus", + "carousel": { + "next": "Nächste Anzeige", + "previous": "Vorherige Anzeige" + }, + "scrollViewTips": "Scrolle um die wichtigsten Momente dieses Objekts anzuzeigen.", + "autoTrackingTips": "Die Positionen der Bounding Box sind bei Kameras mit automatischer Verfolgung ungenau.", + "count": "{{first}} von {{second}}", + "trackedPoint": "Verfolgter Punkt" + }, + "type": { + "details": "Details", + "video": "Video", + "object_lifecycle": "Objekt-Lebenszyklus", + "snapshot": "Snapshot", + "thumbnail": "Vorschaubild", + "tracking_details": "Nachverfolgungs-Details" + }, + "itemMenu": { + "downloadSnapshot": { + "label": "Schnappschuss herunterladen", + "aria": "Schnappschuss herunterladen" + }, + "downloadVideo": { + "label": "Video herunterladen", + "aria": "Video herunterladen" + }, + "viewObjectLifecycle": { + "label": "Lebenszyklus von Objekten anzeigen", + "aria": "Den Lebenszyklus des Objekts anzeigen" + }, + "findSimilar": { + "label": "Ähnliches finden", + "aria": "Ähnliche verfolgte Objekte finden" + }, + "submitToPlus": { + "label": "Bei Frigate+ einreichen", + "aria": "Bei Frigate+ einreichen" + }, + "viewInHistory": { + "label": "Ansicht im Verlauf", + "aria": "Ansicht im Verlauf" + }, + "deleteTrackedObject": { + "label": "Dieses verfolgte Objekt löschen" + }, + "audioTranscription": { + "aria": "Audio Transkription anfordern", + "label": "Transkribieren" + }, + "addTrigger": { + "aria": "Einen Trigger für dieses verfolgte Objekt hinzufügen", + "label": "Trigger hinzufügen" + }, + "viewTrackingDetails": { + "label": "Details zum Verfolgen anzeigen", + "aria": "Details zum Verfolgen anzeigen" + }, + "showObjectDetails": { + "label": "Objektpfad anzeigen" + }, + "hideObjectDetails": { + "label": "Objektpfad verbergen" + }, + "downloadCleanSnapshot": { + "label": "Bereinigte Momentaufnahme herunterladen", + "aria": "Bereinigte Momentaufnahme herunterladen" + }, + "debugReplay": { + "label": "Debug-Wiedergabe", + "aria": "Dieses verfolgte Objekt in der Debug-Wiedergabeansicht anzeigen" + }, + "more": { + "aria": "mehr" + } + }, + "dialog": { + "confirmDelete": { + "title": "Löschen bestätigen", + "desc": "Beim Löschen dieses verfolgten Objekts werden der Schnappschuss, alle gespeicherten Einbettungen und alle zugehörigen Verfolgungsdetails entfernt. Aufgezeichnetes Filmmaterial dieses verfolgten Objekts in der Verlaufsansicht wird NICHT gelöscht.

    Sind Sie sicher, dass Sie fortfahren möchten?" + }, + "toast": { + "error": "Fehler beim Löschen dieses verfolgten Objekts: {{errorMessage}}" + } + }, + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Verfolgtes Objekt erfolgreich gelöscht.", + "error": "Das verfolgte Objekt konnte nicht gelöscht werden: {{errorMessage}}" + } + }, + "tooltip": "Entspricht {{type}} bei {{confidence}}%", + "previousTrackedObject": "Vorheriges verfolgtes Objekt", + "nextTrackedObject": "Nächstes verfolgtes Objekt" + }, + "noTrackedObjects": "Keine verfolgten Objekte gefunden", + "fetchingTrackedObjectsFailed": "Fehler beim Abrufen von verfolgten Objekten: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} verfolgtes Objekt ", + "trackedObjectsCount_other": "{{count}} verfolgte Objekte ", + "exploreMore": "Erkunde mehr {{label}} Objekte", + "aiAnalysis": { + "title": "KI-Analyse" + }, + "concerns": { + "label": "Bedenken" + }, + "trackingDetails": { + "noImageFound": "Kein Bild mit diesem Zeitstempel gefunden.", + "createObjectMask": "Objekt-Maske erstellen", + "scrollViewTips": "Klicke, um die relevanten Momente aus dem Lebenszyklus dieses Objektes zu sehen.", + "lifecycleItemDesc": { + "visible": "{{label}} erkannt", + "entered_zone": "{{label}} betrat {{zones}}", + "active": "{{label}} wurde aktiv", + "stationary": "{{label}} wurde stationär", + "attribute": { + "faceOrLicense_plate": "{{attribute}} erkannt für {{label}}", + "other": "{{label}} erkannt als {{attribute}}" + }, + "gone": "{{label}} hat sich entfernt", + "heard": "{{label}} wurde gehört", + "external": "{{label}} erkannt", + "header": { + "zones": "Zonen", + "ratio": "Verhältnis", + "area": "Bereich", + "score": "Bewertung", + "computedScore": "Berechnetes Ergebnis", + "topScore": "Bester Treffer", + "toggleAdvancedScores": "Erweiterte Ergebnisse umschalten" + } + }, + "annotationSettings": { + "title": "Anmerkungseinstellungen", + "showAllZones": { + "title": "Zeige alle Zonen", + "desc": "Immer Zonen auf Rahmen anzeigen, in die Objekte eingetreten sind." + }, + "offset": { + "label": "Anmerkungen Versatz", + "desc": "Diese Daten stammen aus dem Erkennungsfeed der Kamera, werden jedoch über Bilder aus dem Aufzeichnungsfeed gelegt. Es ist unwahrscheinlich, dass beide Streams perfekt synchron sind. Daher stimmen der Begrenzungsrahmen und das Filmmaterial nicht vollständig überein. Mit dieser Einstellung lassen sich die Anmerkungen zeitlich nach vorne oder hinten verschieben, um sie besser an das aufgezeichnete Filmmaterial anzupassen.", + "millisecondsToOffset": "Millisekunden, um Erkennungs-Anmerkungen zu verschieben. Standard: 0", + "tips": "Verringere den Wert, wenn die Videowiedergabe den Boxen und Wegpunkten voraus ist, und erhöhe den Wert, wenn die Videowiedergabe hinter ihnen zurückbleibt. Dieser Wert kann negativ sein.", + "toast": { + "success": "Der Anmerkungs-Offset für {{camera}} wurde in der Konfigurationsdatei gespeichert." + } + } + }, + "carousel": { + "previous": "Vorherige Anzeige", + "next": "Nächste Anzeige" + }, + "title": "Verfolgungsdetails", + "adjustAnnotationSettings": "Anmerkungseinstellungen anpassen", + "autoTrackingTips": "Die Positionen der Begrenzungsrahmen sind bei Kameras mit automatischer Verfolgung ungenau.", + "count": "{{first}} von {{second}}", + "trackedPoint": "Verfolgter Punkt" + } +} diff --git a/web/public/locales/de/views/exports.json b/web/public/locales/de/views/exports.json new file mode 100644 index 0000000..da604de --- /dev/null +++ b/web/public/locales/de/views/exports.json @@ -0,0 +1,128 @@ +{ + "deleteExport": { + "label": "Export löschen" + }, + "editExport": { + "title": "Export umbenennen", + "desc": "Gib einen neuen Namen für diesen Export an.", + "saveExport": "Export speichern" + }, + "documentTitle": "Exportieren - Frigate", + "deleteExport.desc": "Soll {{exportName}} wirklich gelöscht werden?", + "search": "Suche", + "noExports": "Keine Exporte gefunden", + "toast": { + "error": { + "renameExportFailed": "Umbenennen des Exports fehlgeschlagen: {{errorMessage}}", + "assignCaseFailed": "Aktualisierung der Fallzuweisung fehlgeschlagen: {{errorMessage}}", + "caseSaveFailed": "Fehler beim speichern vom Fall: {{errorMessage}}", + "caseDeleteFailed": "Fehler beim löschem vom Fall: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Export teilen", + "downloadVideo": "Video herunterladen", + "editName": "Name ändern", + "deleteExport": "Export löschen", + "assignToCase": "Hinzufügen zum Fall", + "removeFromCase": "Vom Gehäuse entfernen" + }, + "headings": { + "cases": "Fälle", + "uncategorizedExports": "Nicht kategorisierte 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" + }, + "toolbar": { + "newCase": "Neuer Fall", + "addExport": "Zum expotieren hinzufügen", + "editCase": "Fall bearbeiten", + "deleteCase": "Fall löschen" + }, + "deleteCase": { + "label": "Fall löschen", + "desc": "Sind sie sich sicher löschen von{{caseName}}?", + "descKeepExports": "Exporte bleiben als nicht kategorisierte Exporte verfügbar.", + "descDeleteExports": "Alle Exporte werden in diesem Fall endgültig gelöscht.", + "deleteExports": "Exporte auch löschen" + }, + "caseCard": { + "emptyCase": "Noch keine Exporte" + }, + "jobCard": { + "defaultName": "{{camera}} export", + "queued": "In der Warteschlange", + "running": "läuft", + "preparing": "Vorbereitung", + "copying": "kopieren", + "encoding": "Codierung", + "encodingRetry": "Kodierung (Wiederholung)", + "finalizing": "Abschließen" + }, + "caseView": { + "noDescription": "keine Beschreibung", + "createdAt": "Erstellt {{value}}", + "exportCount_one": "1 Export", + "exportCount_other": "{{count}} Exports", + "cameraCount_one": "1 Kamera", + "cameraCount_other": "{{count}} Kameras", + "showMore": "Mehr anzeigen", + "showLess": "Weniger Anzeigen", + "emptyTitle": "Der Fall ist leer", + "emptyDescription": "Fügen Sie vorhandene, nicht kategorisierte Exporte hinzu, um den Fall übersichtlich zu halten.", + "emptyDescriptionNoExports": "Es sind noch keine nicht kategorisierten Exporte zum Hinzufügen verfügbar." + }, + "caseEditor": { + "createTitle": "Fall erstellen", + "editTitle": "Fall bearbeiten", + "namePlaceholder": "Fall Name", + "descriptionPlaceholder": "Fügen Sie Anmerkungen oder Kontext zu diesem Fall hinzu" + }, + "addExportDialog": { + "title": "Export zum {{caseName}} hinzufügen", + "searchPlaceholder": "Suche nach nicht kategorisierten Exporten", + "empty": "Es wurden keine nicht kategorisierten Exporte gefunden, die dieser Suche entsprechen.", + "addButton_one": "1 Export hinzufügen", + "addButton_other": "Fügen Sie {{count}} Exporte hinzu", + "adding": "Hinzufügen..." + }, + "selected_one": "{{count}} ausgewählt", + "selected_other": "{{count}} ausgewählt", + "bulkActions": { + "addToCase": "Zum Fall hinzufügen", + "moveToCase": "Zum Fall wechseln", + "removeFromCase": "Aus dem Fall nehmen", + "delete": "löschen", + "deleteNow": "jetzt löschen" + }, + "bulkDelete": { + "title": "Exporte löschen", + "desc_one": "Möchten Sie den Export {{count}} wirklich löschen?", + "desc_other": "Möchten Sie wirklich {{count}} Exporte löschen?" + }, + "bulkRemoveFromCase": { + "title": "Aus dem Fall nehmen", + "desc_one": "{{count}}-Export aus diesem Fall entfernen?", + "desc_other": "{{count}} Exporte aus diesem Fall entfernen?", + "descKeepExports": "Die Exporte werden in die Kategorie „Nicht kategorisiert“ verschoben.", + "descDeleteExports": "Exporte werden endgültig gelöscht.", + "deleteExports": "Löschen Sie stattdessen Exporte" + }, + "bulkToast": { + "success": { + "delete": "Exporte erfolgreich gelöscht", + "reassign": "Fallzuweisung erfolgreich aktualisiert", + "remove": "Exporte erfolgreich aus dem Fall entfernt" + }, + "error": { + "deleteFailed": "Fehler beim Löschen der Exporte: {{errorMessage}}", + "reassignFailed": "Fehler beim Aktualisieren der Fallzuordnung: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/de/views/faceLibrary.json b/web/public/locales/de/views/faceLibrary.json new file mode 100644 index 0000000..7ece861 --- /dev/null +++ b/web/public/locales/de/views/faceLibrary.json @@ -0,0 +1,110 @@ +{ + "description": { + "placeholder": "Gib einen Name für diese Kollektion ein", + "addFace": "Eine neue Kollektion zur Gesichtsbibliothek durch hochladen des ersten Bildes hinzufügen.", + "invalidName": "Ungültiger Name. Namen dürfen nur Buchstaben, Zahlen, Leerzeichen, Apostrophe, Unterstriche und Bindestriche enthalten.", + "nameCannotContainHash": "Der Name darf keine # enthalten." + }, + "details": { + "person": "Person", + "confidence": "Vertrauen", + "timestamp": "Zeitstempel", + "faceDesc": "Details des verfolgten Objekts, das dieses Gesicht erzeugt hat", + "face": "Gesichtsdetails", + "subLabelScore": "Sub Label Score", + "scoreInfo": "Der Sub Label Score ist der gewichtete Score für alle erkannten Gesichter und kann daher vom Score abweichen, der auf dem Schnappschuss angezeigt wird.", + "unknown": "Unbekannt" + }, + "uploadFaceImage": { + "title": "Lade Gesichtsbild hoch", + "desc": "Lade ein Bild zur Gesichtserkennung hoch und füge es für {{pageToggle}} hinzu" + }, + "createFaceLibrary": { + "title": "Kollektion erstellen", + "new": "Lege ein neues Gesicht an", + "desc": "Erstelle eine neue Kollektion", + "nextSteps": "Um eine solide Grundlage zu bilden:
  • Benutze den \"Aktuelle Erkennungen\" Tab, um Bilder für jede erkannte Person auszuwählen und zu trainieren.
  • Konzentriere dich für gute Ergebnisse auf Frontalfotos; vermeide Bilder zu Trainingszwecken, bei denen Gesichter aus einem Winkel erfasst wurden.
  • " + }, + "documentTitle": "Gesichtsbibliothek - Frigate", + "selectItem": "Wähle {{item}}", + "selectFace": "Wähle Gesicht", + "imageEntry": { + "dropActive": "Ziehe das Bild hierher…", + "dropInstructions": "Ziehe ein Bild hier her, füge es ein oder klicke um eines auszuwählen", + "maxSize": "Maximale Größe: {{size}} MB", + "validation": { + "selectImage": "Bitte wähle ein Bild aus." + } + }, + "button": { + "addFace": "Gesicht hinzufügen", + "uploadImage": "Bild hochladen", + "deleteFaceAttempts": "Lösche Gesichter", + "reprocessFace": "Gesichter erneut verarbeiten", + "renameFace": "Gesicht umbenennen", + "deleteFace": "Lösche Gesicht" + }, + "train": { + "title": "Neueste Erkennungen", + "aria": "Wähle aktuelle Erkennungen", + "empty": "Es gibt keine aktuellen Versuche zur Gesichtserkennung", + "titleShort": "frisch", + "emptyNoLibrary": { + "title": "Gesicht hinzufügen", + "description": "Sie müssen mindestens ein Gesicht zur Bibliothek hinzufügen, damit die Gesichtserkennung funktioniert." + } + }, + "deleteFaceLibrary": { + "title": "Lösche Name", + "desc": "Möchtest du die Sammlung {{name}} löschen? Alle zugehörigen Gesichter werden gelöscht." + }, + "readTheDocs": "Lies die Dokumentation", + "trainFaceAs": "Trainiere Gesicht als:", + "trainFace": "Trainiere Gesicht", + "toast": { + "success": { + "uploadedImage": "Das Bild wurde erfolgreich hochgeladen.", + "deletedFace_one": "Erfolgreich {{count}} Gesicht gelöscht.", + "deletedFace_other": "Erfolgreich {{count}} Gesichter gelöscht.", + "deletedName_one": "{{count}} Gesicht wurde erfolgreich gelöscht.", + "deletedName_other": "{{count}} Gesichter wurden erfolgreich gelöscht.", + "addFaceLibrary": "{{name}} wurde erfolgreich in die Gesichtsbibliothek aufgenommen!", + "trainedFace": "Gesicht erfolgreich trainiert.", + "updatedFaceScore": "Gesichtsbewertung erfolgreich auf {{name}} ({{score}}) aktualisiert.", + "renamedFace": "Gesicht erfolgreich in {{name}} umbenannt", + "reclassifiedFace": "Gesicht erfolgreich neu klassifiziert." + }, + "error": { + "deleteFaceFailed": "Das Löschen ist fehlgeschlagen: {{errorMessage}}", + "uploadingImageFailed": "Bild kann nicht hochgeladen werden: {{errorMessage}}", + "addFaceLibraryFailed": "Der Gesichtsname konnte nicht gesetzt werden: {{errorMessage}}", + "trainFailed": "Ausbildung fehlgeschlagen: {{errorMessage}}", + "updateFaceScoreFailed": "Aktualisierung der Gesichtsbewertung fehlgeschlagen: {{errorMessage}}", + "deleteNameFailed": "Name kann nicht gelöscht werden: {{errorMessage}}", + "renameFaceFailed": "Gesicht konnte nicht umbenannt werden: {{errorMessage}}", + "reclassifyFailed": "Die Gesichtsbewertung ist fehlgeschlagen: {{errorMessage}}" + } + }, + "steps": { + "uploadFace": "Lade Bild des Gesichts hoch", + "nextSteps": "Nächste Schritte", + "faceName": "Gebe Gesichtsname ein", + "description": { + "uploadFace": "Lade ein Bild von {{name}} hoch, das ihr/sein Gesicht aus einer frontalen Perspektive zeigt. Das Bild muss nicht auf das Gesicht zugeschnitten sein." + } + }, + "renameFace": { + "title": "Gesicht umbenennen", + "desc": "Gib den neuen Namen für {{name}} ein" + }, + "collections": "Sammlungen", + "deleteFaceAttempts": { + "title": "Lösche Gesichter", + "desc_one": "Bist du sicher, dass du {{count}} Gesicht löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", + "desc_other": "Bist du sicher, dass du {{count}} Gesichter löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden." + }, + "nofaces": "Keine Gesichter verfügbar", + "pixels": "{{area}}px", + "reclassifyFaceAs": "Gesicht neu klassifizieren als:", + "reclassifyFace": "Gesicht neu klassifizieren" +} diff --git a/web/public/locales/de/views/live.json b/web/public/locales/de/views/live.json new file mode 100644 index 0000000..cfee8b7 --- /dev/null +++ b/web/public/locales/de/views/live.json @@ -0,0 +1,206 @@ +{ + "lowBandwidthMode": "Modus für geringe Bandbreite", + "twoWayTalk": { + "enable": "Gegensprechfunktion aktivieren", + "disable": "Gegensprechfunktion ausschalten" + }, + "cameraAudio": { + "enable": "Kamera-Audio aktivieren", + "disable": "Kamera-Audio deaktivieren" + }, + "ptz": { + "move": { + "clickMove": { + "disable": "Bewegen per Klick deaktivieren", + "enable": "Bewegen per Klick aktivieren", + "label": "Zum Zentrieren der Kamera ins Bild klicken", + "enableWithZoom": "Ermögliche Bewegung durch auswählen / Vergrößern durch ziehen" + }, + "up": { + "label": "PTZ-Kamera nach oben bewegen" + }, + "left": { + "label": "PTZ-Kamera nach links bewegen" + }, + "down": { + "label": "PTZ-Kamera nach unten bewegen" + }, + "right": { + "label": "PTZ-Kamera nach rechts bewegen" + } + }, + "zoom": { + "in": { + "label": "PTZ-Kamera rein zoomen" + }, + "out": { + "label": "PTZ-Kamera heraus zoomen" + } + }, + "presets": "PTZ-Kamera Voreinstellungen", + "frame": { + "center": { + "label": "Klicke in den Rahmen, um die PTZ-Kamera zu zentrieren" + } + }, + "focus": { + "in": { + "label": "PTZ Kamera hinein fokussieren" + }, + "out": { + "label": "PTZ Kamera hinaus fokussieren" + } + } + }, + "documentTitle": { + "default": "Live - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Live - Frigate", + "muteCameras": { + "disable": "Stumm aller Kameras aufheben", + "enable": "Alle Kameras auf stumm" + }, + "recording": { + "disable": "Aufzeichnung deaktivieren", + "enable": "Aufzeichnung aktivieren", + "disabledInConfig": "Aufnahme muss erst in den Einstellung für diese Kamera aktiviert werden." + }, + "snapshots": { + "enable": "Schnappschüsse aktivieren", + "disable": "Schnappschüsse deaktivieren" + }, + "autotracking": { + "disable": "Autotracking deaktivieren", + "enable": "Autotracking aktivieren" + }, + "streamStats": { + "enable": "Stream Statistiken anzeigen", + "disable": "Stream-Statistiken ausblenden" + }, + "manualRecording": { + "title": "auf Verlangen", + "showStats": { + "label": "Statistiken anzeigen", + "desc": "Aktivieren Sie diese Option, um Stream-Statistiken als Overlay über dem Kamera-Feed anzuzeigen." + }, + "started": "Manuelle On-Demand Aufzeichnung gestartet.", + "failedToStart": "Manuelle On-Demand Aufzeichnung konnte nicht gestartet werden.", + "recordDisabledTips": "Da die Aufzeichnung in der Konfiguration für diese Kamera deaktiviert oder eingeschränkt ist, wird nur ein Schnappschuss gespeichert.", + "end": "On-Demand Aufzeichnung beenden", + "ended": "Manuelle On-Demand Aufzeichnung beendet.", + "playInBackground": { + "desc": "Aktivieren Sie diese Option, um das Streaming fortzusetzen, wenn der Player ausgeblendet ist.", + "label": "Im Hintergrund abspielen" + }, + "tips": "Lade einen Sofort-Schnappschuss herunter oder starte ein manuelles Ereignis basierend auf den Aufbewahrungseinstellungen für Aufzeichnungen dieser Kamera.", + "debugView": "Debug-Ansicht", + "start": "On-Demand Aufzeichnung starten", + "failedToEnd": "Die manuelle On-Demand Aufzeichnung konnte nicht beendet werden." + }, + "streamingSettings": "Streaming Einstellungen", + "notifications": "Benachrichtigungen", + "stream": { + "audio": { + "available": "Audio ist für diesen Stream verfügbar", + "tips": { + "title": "Audio muss von deiner Kamera ausgegeben und für diesen Stream in go2rtc konfiguriert werden.", + "documentation": "Dokumentation lesen " + }, + "unavailable": "Für diesen Stream ist kein Audio verfügbar" + }, + "twoWayTalk": { + "tips": "Ihr Gerät muss die Funktion unterstützen und WebRTC muss für die bidirektionale Kommunikation konfiguriert sein.", + "tips.documentation": "Dokumentation lesen ", + "available": "Für diesen Stream ist eine Zwei-Wege-Sprechfunktion verfügbar", + "unavailable": "Zwei-Wege-Kommunikation für diesen Stream nicht verfügbar" + }, + "lowBandwidth": { + "tips": "Die Live-Ansicht befindet sich aufgrund von Puffer- oder Stream-Fehlern im Modus mit geringer Bandbreite.", + "resetStream": "Stream zurücksetzen" + }, + "title": "Stream", + "playInBackground": { + "tips": "Aktivieren Sie diese Option, um das Streaming fortzusetzen, wenn der Player ausgeblendet ist.", + "label": "Im Hintergrund abspielen" + }, + "debug": { + "picker": "Stream Auswahl nicht verfügbar im Debug Modus. Die Debug Ansicht nutzt immer den Stream, welcher der Rolle zugewiesen ist." + } + }, + "effectiveRetainMode": { + "modes": { + "motion": "Bewegung", + "active_objects": "Aktive Objekte", + "all": "Alle" + }, + "notAllTips": "Dein Konfiguration zur Aufzeichnungsaufbewahrung von {{source}} ist eingestellt auf -Modus:{{effectiveRetainMode}} , daher werden in dieser On-Demand Aufzeichnung nur Segmente gespeichert mit{{effectiveRetainModeName}} ." + }, + "editLayout": { + "group": { + "label": "Kameragruppe bearbeiten" + }, + "exitEdit": "Bearbeitung beenden", + "label": "Layout bearbeiten" + }, + "camera": { + "enable": "Kamera aktivieren", + "disable": "Kamera deaktivieren", + "turnOn": "Schalte die Kamera ein", + "turnOff": "Schalte die Kamera aus" + }, + "audioDetect": { + "enable": "Audioerkennung aktivieren", + "disable": "Audioerkennung deaktivieren" + }, + "detect": { + "enable": "Erkennung aktivieren", + "disable": "Erkennung deaktivieren" + }, + "cameraSettings": { + "objectDetection": "Objekterkennung", + "recording": "Aufnahme", + "snapshots": "Schnappschüsse", + "cameraEnabled": "Kamera aktiviert", + "autotracking": "Autotracking", + "audioDetection": "Audioerkennung", + "title": "{{camera}} Einstellungen", + "transcription": "Audio Transkription", + "camera": "Kamera" + }, + "history": { + "label": "Historisches Filmmaterial zeigen" + }, + "audio": "Audio", + "suspend": { + "forTime": "Aussetzen für: " + }, + "transcription": { + "enable": "Live Audio Transkription einschalten", + "disable": "Live Audio Transkription ausschalten" + }, + "noCameras": { + "title": "Keine Kameras konfiguriert", + "description": "Beginne indem du eine Kamera anschließt.", + "buttonText": "Kamera hinzufügen", + "restricted": { + "title": "Keine Kamera verfügbar", + "description": "Sie haben keine Berechtigung, Kameras in dieser Gruppe anzuzeigen." + }, + "default": { + "title": "Keine Kameras konfiguriert", + "description": "Zum Start eine Kamera mit Frigate verbinden.", + "buttonText": "Kamera hinzufügen" + }, + "group": { + "title": "Keine Kameras in der Gruppe", + "description": "Diese Kameragruppe hat keine zugewiesenen oder aktiven Kameras.", + "buttonText": "Gruppen verwalten" + } + }, + "snapshot": { + "takeSnapshot": "Sofort-Schnappschuss herunterladen", + "noVideoSource": "Keine Video-Quelle für Schnappschuss verfügbar.", + "captureFailed": "Die Aufnahme des Schnappschusses ist fehlgeschlagen.", + "downloadStarted": "Schnappschuss Download gestartet." + } +} diff --git a/web/public/locales/de/views/motionSearch.json b/web/public/locales/de/views/motionSearch.json new file mode 100644 index 0000000..d6aaf98 --- /dev/null +++ b/web/public/locales/de/views/motionSearch.json @@ -0,0 +1,80 @@ +{ + "documentTitle": "Bewegungssuche - Frigate", + "title": "Bewegungssuche", + "description": "Zeichnen Sie ein Polygon, um den gewünschten Bereich zu definieren, und geben Sie einen Zeitbereich an, um innerhalb dieses Bereichs nach Bewegungsänderungen zu suchen.", + "selectCamera": "Die Bewegungssuche wird geladen", + "startSearch": "Suche starten", + "searchStarted": "Die Suche wurde gestartet", + "searchCancelled": "Suche abgebrochen", + "cancelSearch": "Abbrechen", + "searching": "Suche läuft.", + "searchComplete": "Suche abgeschlossen", + "noResultsYet": "Führen Sie eine Suche durch, um Bewegungsänderungen im ausgewählten Bereich zu finden", + "noChangesFound": "Im ausgewählten Bereich wurden keine Pixeländerungen festgestellt", + "changesFound_one": "Es wurde {{count}} Bewegungsänderungen gefunden", + "changesFound_other": "Es wurden {{count}} Bewegungsänderungen gefunden", + "framesProcessed": "{{count}} Bilder verarbeitet", + "jumpToTime": "Zu diesem Zeitpunkt springen", + "results": "Ergebnisse", + "showSegmentHeatmap": "Heatmap", + "newSearch": "Neue Suche", + "clearResults": "Eindeutige Ergebnisse", + "clearROI": "Polygon löschen", + "polygonControls": { + "points_one": "{{count}} Punkt", + "points_other": "{{count}} Punkte", + "undo": "Letzten Schritt rückgängig machen", + "reset": "Polygon zurücksetzen", + "drawMode": "ziehen", + "moveMode": "bewegen" + }, + "motionHeatmapLabel": "Bewegungs-Heatmap", + "dialog": { + "title": "Bewegungssuche", + "cameraLabel": "Kamera", + "previewAlt": "Kamera-Vorschau für {{camera}}" + }, + "timeRange": { + "title": "Suchbereich", + "start": "Startzeit", + "end": "Endzeit" + }, + "settings": { + "title": "Sucheinstellungen", + "parallelMode": "Parallelbetrieb", + "parallelModeDesc": "Mehrere Aufzeichnungssegmente gleichzeitig scannen (schneller, aber deutlich rechenintensiver)", + "threshold": "Empfindlichkeitsschwelle", + "thresholdDesc": "Niedrigere Werte erkennen geringere Veränderungen (1–255)", + "minArea": "Mindestwechselbereich", + "minAreaDesc": "Mindestgröße eines einzelnen sich bewegenden Bereichs, ausgedrückt als Prozentsatz des untersuchten Bereichs", + "frameSkip": "Bild überspringen", + "frameSkipDesc": "Verarbeite jeden N-ten Frame. Stelle diesen Wert auf die Bildrate deiner Kamera ein, um einen Frame pro Sekunde zu verarbeiten (z. B. 5 für eine Kamera mit 5 FPS, 30 für eine Kamera mit 30 FPS). Höhere Werte sorgen für eine schnellere Verarbeitung, können jedoch kurze Bewegungsabläufe übersehen.", + "maxResults": "Maximale Ergebnisse", + "maxResultsDesc": "Nach dieser Anzahl übereinstimmender Zeitstempel anhalten" + }, + "errors": { + "noCamera": "Bitte wählen Sie eine Kamera aus", + "noROI": "Bitte zeichnen Sie einen Bereich von Interesse ein", + "noTimeRange": "Bitte wählen Sie einen Zeitraum aus", + "invalidTimeRange": "Die Endzeit muss nach der Startzeit liegen", + "searchFailed": "Suche fehlgeschlagen: {{message}}", + "polygonTooSmall": "Ein Polygon muss mindestens 3 Punkte haben", + "unknown": "Unbekannter Fehler" + }, + "changePercentage": "Um {{percentage}} % verändert", + "metrics": { + "title": "Suchmetriken", + "segmentsScanned": "Durchsuchte Segmente", + "segmentsProcessed": "Bearbeitet", + "segmentsSkippedInactive": "Übersprungen (keine Aktivität)", + "segmentsSkippedHeatmap": "Übersprungen (keine Überschneidung der ROI)", + "fallbackFullRange": "Ausweich-Vollbereichsscan", + "framesDecoded": "Rahmen decodiert", + "wallTime": "Suchzeit", + "segmentErrors": "Segmentfehler", + "seconds": "{{seconds}}s", + "minutesSeconds": "{{minutes}}m {{seconds}}s", + "scanSummary": "{{segments}} Segmente · {{time}}" + }, + "scanning": "Wird gescannt {{time}}" +} diff --git a/web/public/locales/de/views/recording.json b/web/public/locales/de/views/recording.json new file mode 100644 index 0000000..354cd40 --- /dev/null +++ b/web/public/locales/de/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Export", + "calendar": "Kalender", + "filters": "Filter", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Endzeit muss nach Startzeit liegen", + "noValidTimeSelected": "Gewählter Zeitraum ist ungültig" + } + }, + "filter": "Filter" +} diff --git a/web/public/locales/de/views/replay.json b/web/public/locales/de/views/replay.json new file mode 100644 index 0000000..6c28045 --- /dev/null +++ b/web/public/locales/de/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "Debug-Wiedergabe", + "description": "Spielen Sie Kameraaufnahmen zur Fehlerbehebung ab. Die Objektliste zeigt eine zeitversetzte Übersicht der erkannten Objekte an, und auf der Registerkarte „Meldungen“ wird ein Stream der internen Meldungen von Frigate aus dem Wiedergabematerial angezeigt.", + "websocket_messages": "Nachrichten", + "dialog": { + "title": "Debug-Wiedergabe starten", + "description": "Erstellen Sie eine temporäre Wiedergabekamera, die historisches Bildmaterial in einer Schleife wiedergibt, um Probleme bei der Objekterkennung und -verfolgung zu beheben. Die Wiedergabekamera verfügt über dieselbe Erkennungskonfiguration wie die Quellkamera. Wählen Sie einen Zeitbereich aus, ab dem die Wiedergabe beginnen soll.", + "camera": "Quellkamera", + "timeRange": "Zeitraum", + "preset": { + "1m": "Letzte Minute", + "5m": "Die letzten 5 Minuten", + "timeline": "Aus der Zeitleiste", + "custom": "Benutzerdefiniert" + }, + "startButton": "Wiedergabe starten", + "selectFromTimeline": "Auswählen", + "starting": "Wiedergabe wird gestartet...", + "startLabel": "Start", + "endLabel": "Ende", + "toast": { + "error": "Fehler beim Starten der Debug-Wiedergabe: {{error}}", + "alreadyActive": "Eine Wiederholungssitzung ist bereits aktiv", + "stopError": "Die Wiedergabe der Debug-Daten konnte nicht beendet werden: {{error}}", + "goToReplay": "Zur Aufzeichnung" + } + }, + "page": { + "noSession": "Keine aktive Debug-Wiedergabesitzung", + "noSessionDesc": "Starten Sie eine Debug-Wiedergabe aus der Verlaufsansicht, indem Sie in der Symbolleiste auf die Schaltfläche „Aktionen“ klicken und „Debug-Wiedergabe“ auswählen.", + "goToRecordings": "Zur Historie", + "preparingClip": "Clip wird vorbereitet…", + "preparingClipDesc": "Frigate fasst die Aufzeichnungen für den ausgewählten Zeitraum zusammen. Bei längeren Zeiträumen kann dies eine Minute dauern.", + "startingCamera": "Debug-Wiedergabe wird gestartet…", + "startError": { + "title": "Debug Replay konnte nicht gestartet werden", + "back": "Zurück zur Übersicht" + }, + "sourceCamera": "Quell Kamera", + "replayCamera": "Wiederholungskamera", + "initializingReplay": "Debug-Wiedergabe wird initialisiert...", + "stoppingReplay": "Debug-Wiedergabe wird angehalten...", + "stopReplay": "Stopp Wiederholung", + "confirmStop": { + "title": "Debug-Wiedergabe anhalten?", + "description": "Dadurch wird die Sitzung beendet und alle temporären Daten werden gelöscht. Sind Sie sicher?", + "confirm": "Anhalten Wiederholen", + "cancel": "Abbrechen" + }, + "activity": "Aktivität", + "objects": "Objektliste", + "audioDetections": "Audioerkennungen", + "noActivity": "Es wurde keine Aktivität festgestellt", + "activeTracking": "Aktive Verfolgung", + "noActiveTracking": "Keine aktive Nachverfolgung", + "configuration": "Konfiguration", + "configurationDesc": "Passen Sie die Einstellungen für die Bewegungserkennung und die Objektverfolgung der Debug-Replay-Kamera an. Es werden keine Änderungen in Ihrer Frigate-Konfigurationsdatei gespeichert." + } +} diff --git a/web/public/locales/de/views/search.json b/web/public/locales/de/views/search.json new file mode 100644 index 0000000..0b6424f --- /dev/null +++ b/web/public/locales/de/views/search.json @@ -0,0 +1,75 @@ +{ + "savedSearches": "Gespeicherte Suchen", + "searchFor": "Suche nach {{inputValue}}", + "button": { + "save": "Suche speichern", + "filterActive": "Filter aktiv", + "delete": "Gespeicherte Suche löschen", + "filterInformation": "Information filtern", + "clear": "Suche löschen" + }, + "trackedObjectId": "ID verfolgtes Objekt", + "filter": { + "label": { + "cameras": "Kameras", + "zones": "Zonen", + "search_type": "Suchtyp", + "before": "Vor", + "after": "Nach", + "min_score": "Minimalwert", + "max_score": "Maximalwert", + "recognized_license_plate": "Erkanntes Autokennzeichen", + "has_clip": "Clip vorhanden", + "has_snapshot": "Schnappschuss vorhanden", + "min_speed": "Minimalgeschwindigkeit", + "max_speed": "Maximalgeschwindigkeit", + "time_range": "Zeitraum", + "labels": "Labels", + "sub_labels": "Unterlabels", + "attributes": "Attribute" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Das \"Vor\" Datum muss später als das \"Nach\" Datum sein.", + "minScoreMustBeLessOrEqualMaxScore": "Der \"Minimalwert\" muss kleiner oder gleich dem \"Maximalwert\" sein.", + "afterDatebeEarlierBefore": "Das \"Nach\" Datum muss früher als das \"Vor\" Datum sein.", + "maxScoreMustBeGreaterOrEqualMinScore": "Der \"Maximalwert\" muss größer oder gleich dem \"Minimalwert\" sein.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Der \"Minimalgeschwindigkeit\" muss kleiner oder gleich der \"Maximalgeschwindigkeit\" sein.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Der \"Maximalgeschwindigkeit\" muss größer oder gleich der \"Maximalgeschwindigkeit\" sein." + } + }, + "header": { + "currentFilterType": "Filterwerte", + "noFilters": "Filter", + "activeFilters": "Aktive Filter" + }, + "tips": { + "desc": { + "step": "
    • Gib einen Filternamen gefolgt von einem Doppelpunkt ein (z.B. \"Kameras:\").
    • Wähle einen Wert aus den Vorschlägen aus oder tippe einen individuellen ein.
    • Verwende mehrere Filter, indem Du sie nacheinander mit Leerzeichen getrennt eingibst.
    • Datumfilter (Vor: und Nach:) im Format {{DateFormat}}.
    • Zeitraumfilter im Format {{exampleTime}}.
    • Lösche Filter durch Drücken des \"x\" daneben.
    ", + "text": "Mit Filtern kannst Du Suchergebnisse eingrenzen. Hier erfährst Du, wie diese im Eingabefeld verwendet werden können:", + "example": "Beispiel: Kameras:Tor Label:Person Vor:01012024 Zeitraum:15:00-16:00", + "step3": "Verwende mehrere Filter, indem du sie nacheinander mit einem Leerzeichen dazwischen hinzufügst.", + "step2": "Wähle einen Wert aus den Vorschlägen aus oder gib einen eigenen ein.", + "step1": "Gib einen Filter-Schlüssel ein, gefolgt von einem Doppelpunkt (z.B. „kameras:“).", + "exampleLabel": "Beispiel:", + "step6": "Entferne Filter, indem du auf das „x“ daneben klickst.", + "step4": "Datumsfilter (bevor: und nach:) verwenden das Format {{DateFormat}}.", + "step5": "Der Zeitbereichsfilter verwendet das Format {{exampleTime}}." + }, + "title": "Wie man Textfilter verwendet" + }, + "searchType": { + "thumbnail": "Vorschaubild", + "description": "Beschreibung" + } + }, + "similaritySearch": { + "title": "Ähnlichkeitssuche", + "clear": "Ähnlichkeitssuche löschen", + "active": "Aktive Ähnlichkeitssuche" + }, + "search": "Suche", + "placeholder": { + "search": "Suchen…" + } +} diff --git a/web/public/locales/de/views/settings.json b/web/public/locales/de/views/settings.json new file mode 100644 index 0000000..a0fcfd7 --- /dev/null +++ b/web/public/locales/de/views/settings.json @@ -0,0 +1,2237 @@ +{ + "documentTitle": { + "default": "Einstellungen - Frigate", + "authentication": "Authentifizierungseinstellungen – Frigate", + "camera": "Kameraeinstellungen - Frigate", + "masksAndZones": "Masken- und Zoneneditor – Frigate", + "object": "Debug - Frigate", + "general": "Benutzeroberflächen-Einstellungen - Frigate", + "frigatePlus": "Frigate+ Einstellungen – Frigate", + "classification": "Klassifizierungseinstellungen – Frigate", + "motionTuner": "Bewegungserkennungs-Optimierer – Frigate", + "notifications": "Benachrichtigungseinstellungen", + "enrichments": "Erweiterte Statistiken - Frigate", + "cameraManagement": "Kameras verwalten - Frigate", + "cameraReview": "Kameraeinstellungen prüfen - Frigate", + "globalConfig": "Grundeinstellungen - Frigate", + "cameraConfig": "Kameraeinstellungen - Frigate", + "maintenance": "Wartung - Frigate", + "profiles": "Profile - Frigate", + "detectorsAndModel": "Sensoren und Modell – Frigate" + }, + "menu": { + "ui": "Benutzeroberfläche", + "cameras": "Kameraeinstellungen", + "classification": "Klassifizierung", + "masksAndZones": "Maskierungen / Zonen", + "motionTuner": "Bewegungserkennungs-Optimierer", + "debug": "Debug", + "frigateplus": "Frigate+", + "users": "Benutzer", + "notifications": "Benachrichtigungen", + "enrichments": "Erkennungsfunktionen", + "triggers": "Auslöser", + "roles": "Rollen", + "cameraManagement": "Kamera Verwaltung", + "cameraReview": "Überprüfung", + "system": "System", + "general": "allgemein", + "globalConfig": "Grundeinstellungen", + "integrations": "Integrationen", + "profileSettings": "Profileinstellungen", + "globalDetect": "Objekterkennung", + "globalRecording": "Aufnahme", + "globalSnapshots": "Schnappschüsse", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Bewegungserkennung", + "globalObjects": "Objekte", + "globalReview": "Überprüfung", + "globalAudioEvents": "Audioerkennung", + "globalLivePlayback": "Live-Wiedergabe", + "globalTimestampStyle": "Zeitstempelformat", + "systemDatabase": "Datenbank", + "systemTls": "TLS", + "systemAuthentication": "Authentifizierung", + "systemNetworking": "Netzwerk", + "systemProxy": "Proxy", + "systemUi": "UI", + "systemLogging": "Log", + "systemEnvironmentVariables": "Umgebungsvariablen", + "systemTelemetry": "Telemetrie", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "Erkannte Hardware", + "systemDetectionModel": "Erkennungsmodell", + "systemMqtt": "mqtt", + "integrationSemanticSearch": "Semantische Suche", + "integrationGenerativeAi": "Generative KI", + "integrationFaceRecognition": "Gesichtserkennung", + "integrationLpr": "Kennzeichenerkennung", + "integrationObjectClassification": "Objekt Klassifizierung", + "integrationAudioTranscription": "Audio-Transkription", + "cameraDetect": "Objekterkennung", + "cameraFfmpeg": "Streams (FFmpeg)", + "cameraRecording": "Aufnahme", + "cameraSnapshots": "Momentaufnahme", + "cameraMotion": "Bewegungserkennung", + "cameraObjects": "Objekte", + "cameraConfigReview": "Überprüfung", + "cameraAudioEvents": "Audioerkennung", + "cameraAudioTranscription": "Audio-Transkription", + "cameraNotifications": "Benachrichtigung", + "cameraLivePlayback": "Live-Wiedergabe", + "cameraBirdseye": "Birdseye", + "cameraFaceRecognition": "Gesichtserkennung", + "cameraLpr": "Kennzeichenerkennung", + "cameraMqttConfig": "mqtt", + "cameraOnvif": "ONVIF", + "cameraUi": "Kamera UI", + "cameraTimestampStyle": "Zeitstempel Stil", + "cameraMqtt": "Kamera mqtt", + "mediaSync": "Medien-Synchronisierung", + "regionGrid": "Regionsraster", + "uiSettings": "Benutzeroberfläche Einstellung", + "profiles": "Profile", + "systemGo2rtcStreams": "go2rtc-streams", + "maintenance": "Wartung", + "systemDetectorsAndModel": "Detektoren und Modell" + }, + "dialog": { + "unsavedChanges": { + "title": "Du hast nicht gespeicherte Änderungen.", + "desc": "Möchtest Du deine Änderungen speichern, bevor du fortfährst?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Keine Kamera" + }, + "general": { + "title": "Benutzeroberflächen Einstellungen", + "liveDashboard": { + "title": "Live Übersicht", + "playAlertVideos": { + "label": "Spiele Videos mit Alarmierung", + "desc": "Standardmäßig werden die letzten Warnmeldungen auf dem Live-Dashboard als kurze Videoschleifen abgespielt. Deaktiviere diese Option, um nur ein statisches Bild der letzten Warnungen auf diesem Gerät/Browser anzuzeigen." + }, + "automaticLiveView": { + "desc": "Wechsle automatisch zur Live Ansicht der Kamera, wenn eine Aktivität erkannt wurde. Wenn du diese Option deaktivierst, werden die statischen Kamerabilder auf der Liveübersicht nur einmal pro Minute aktualisiert.", + "label": "Automatische Live Ansicht" + }, + "displayCameraNames": { + "label": "Immer Namen der Kamera anzeigen", + "desc": "Kamerabezeichnung permanent in einem Chip im Live-View-Dashboard für alle Kameras anzeigen." + }, + "liveFallbackTimeout": { + "label": "Live Player Ausfallzeitlimit", + "desc": "Wenn der hochwertige Live-Stream einer Kamera nicht verfügbar ist, wechsle nach dieser Anzahl von Sekunden in den Modus für geringe Bandbreite. Standard: 3." + } + }, + "storedLayouts": { + "title": "Gespeicherte Ansichten", + "clearAll": "Lösche alle Ansichten", + "desc": "Das Layout der Kameras in einer Kameragruppe kann verschoben/geändert werden. Die Positionen werden im lokalen Cache des Browsers gespeichert." + }, + "cameraGroupStreaming": { + "title": "Einstellungen für Kamera-Gruppen-Streaming", + "clearAll": "Alle Streamingeinstellungen löschen", + "desc": "Die Streaming-Einstellungen für jede Kameragruppe werden im lokalen Cache des Browsers gespeichert." + }, + "recordingsViewer": { + "title": "Aufzeichnungsbetrachter", + "defaultPlaybackRate": { + "desc": "Standard-Wiedergabegeschwindigkeit für die Wiedergabe von Aufnahmen.", + "label": "Standard-Wiedergabegeschwindigkeit" + } + }, + "calendar": { + "title": "Kalender", + "firstWeekday": { + "label": "Erster Wochentag", + "desc": "Der Tag, an dem die Wochen des Überprüfungs-Kalenders beginnen.", + "sunday": "Sonntag", + "monday": "Montag" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Gespeichertes Layout für {{cameraName}} gelöscht", + "clearStreamingSettings": "Streaming Einstellungen aller Kameragruppen bereinigt." + }, + "error": { + "clearStoredLayoutFailed": "Das gespeicherte Layout konnte nicht gelöscht werden: {{errorMessage}}", + "clearStreamingSettingsFailed": "Die Streaming-Einstellungen konnten nicht gelöscht werden: {{errorMessage}}" + } + } + }, + "classification": { + "title": "Klassifizierungseinstellungen", + "semanticSearch": { + "title": "Semantische Suche", + "desc": "Die semantische Suche in Frigate ermöglicht es, verfolgte Objekte innerhalb der Überprüfungselemente zu finden, indem entweder das Bild selbst, eine benutzerdefinierte Textbeschreibung oder eine automatisch generierte Beschreibung verwendet wird.", + "readTheDocumentation": "Lesen Sie die Dokumentation", + "reindexNow": { + "alreadyInProgress": "Neu-Indizierung läufts bereits.", + "label": "Neuindizieren", + "confirmTitle": "Bestätige Neu-Indizierung", + "confirmButton": "Neu-Indizieren", + "success": "Neuindizierung erfolgreich gestartet.", + "error": "Starten der Neuindizierung fehlgeschlagen: {{errorMessage}}", + "desc": "Durch die Neuindizierung werden die Einbettungen für alle verfolgten Objekte neu generiert. Dieser Prozess läuft im Hintergrund und kann Ihre CPU überlasten. Je nach Anzahl der verfolgten Objekte kann er einige Zeit in Anspruch nehmen.", + "confirmDesc": "Möchten Sie alle verfolgten Objekteinbettungen wirklich neu indizieren? Dieser Vorgang läuft im Hintergrund, kann aber Ihre CPU überlasten und einige Zeit in Anspruch nehmen. Sie können den Fortschritt auf der Explore-Seite verfolgen." + }, + "modelSize": { + "large": { + "title": "groß", + "desc": "Bei Verwendung von large wird das vollständige Jina-Modell verwendet und ggf. automatisch auf der GPU ausgeführt." + }, + "label": "Model Größe", + "small": { + "title": "klein", + "desc": "Durch die Verwendung von small wird eine quantisierte Version des Modells eingesetzt, die weniger RAM verwendet und schneller auf der CPU läuft, wobei der Unterschied in der Einbettungsqualität sehr gering ist." + }, + "desc": "Die Größe des Modells, das für semantische Sucheinbettungen verwendet wird." + } + }, + "birdClassification": { + "desc": "Die Vogelklassifizierung identifiziert bekannte Vögel mithilfe eines quantisierten Tensorflow-Modells. Wenn ein bekannter Vogel erkannt wird, wird sein allgemeiner Name als sub_label hinzugefügt. Diese Informationen sind in der Benutzeroberfläche, in Filtern und in Benachrichtigungen enthalten.", + "title": "Vogel-Klassifizierung" + }, + "licensePlateRecognition": { + "readTheDocumentation": "Lies die Dokumentation", + "title": "Nummernschilderkennung", + "desc": "Frigate kann Nummernschilder an Fahrzeugen erkennen und die erkannten Zeichen automatisch dem Feld „recognized_license_plate“ oder einem bekannten Namen als Unterbezeichnung für Objekte vom Typ „Auto“ hinzufügen. Ein häufiger Anwendungsfall ist das Lesen der Nummernschilder von Autos, die in eine Einfahrt einfahren oder auf einer Straße vorbeifahren." + }, + "faceRecognition": { + "readTheDocumentation": "Lies die Dokumentation", + "modelSize": { + "small": { + "title": "klein", + "desc": "ei der Verwendung von small wird ein FaceNet-Gesichtseinbettungsmodell eingesetzt, das auf den meisten CPUs effizient läuft." + }, + "label": "Model Größe", + "large": { + "title": "groß", + "desc": "Bei der Verwendung von large wird ein ArcFace-Gesichtseinbettungsmodell verwendet und ggf. automatisch auf der GPU ausgeführt." + }, + "desc": "Die Größe des für die Gesichtserkennung verwendeten Modells." + }, + "title": "Gesichtserkennung", + "desc": "Mithilfe der Gesichtserkennung können Personen Namen zugewiesen werden. Sobald das Gesicht erkannt wird, weist Frigate den Namen der Person als Unterbezeichnung zu. Diese Informationen werden in die Benutzeroberfläche, Filter und Benachrichtigungen integriert." + }, + "toast": { + "error": "Sichern der Konfigurationsänderungen fehlgeschlagen: {{errorMessage}}", + "success": "Die Klassifizierungseinstellungen wurden gespeichert. Starten Sie Frigate neu, um die Änderungen zu übernehmen." + }, + "restart_required": "Neustart erforderlich (Klassifizierungseinstellungen geändert)", + "unsavedChanges": "Nicht gespeicherte Änderungen der Klassifizierungseinstellungen" + }, + "camera": { + "reviewClassification": { + "toast": { + "success": "Die Konfiguration der Klassifizierung wurde gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen." + }, + "title": "Überprüfung der Klassifikation", + "selectAlertsZones": "Zonen für Warnungen auswählen", + "limitDetections": "Begrenzung der Erkennungen auf bestimmte Zonen", + "readTheDocumentation": "Lies die Dokumentation", + "noDefinedZones": "Für diese Kamera sind keine Zonen definiert.", + "selectDetectionsZones": "Zonen für Erkennungen auswählen", + "objectAlertsTips": "Alle {{alertsLabels}} -Objekte auf {{cameraName}} werden als Warnungen angezeigt.", + "desc": "Frigate kategorisiert Überprüfungselemente als Warnungen und Erkennungen. Standardmäßig werden alle Objekte vom Typ Person und Auto als Warnungen betrachtet. Sie können die Kategorisierung Ihrer Überprüfungselemente verfeinern, indem Sie die erforderlichen Zonen dafür konfigurieren.", + "zoneObjectAlertsTips": "Alle {{alertsLabels}} Objekte, die in {{zone}} auf {{cameraName}} erkannt werden , werden als Warnungen angezeigt.", + "objectDetectionsTips": "Alle {{detectionsLabels}} Objekte, die auf {{cameraName}} nicht kategorisiert sind, werden unabhängig von der Zone, in der sie sich befinden, als Erkennungen angezeigt.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}} Objekte, die in {{zone}} auf {{cameraName}} nicht kategorisiert sind, werden als Erkennungen angezeigt.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}} Objekte, die auf {{cameraName}} nicht kategorisiert sind, werden unabhängig von der Zone, in der sie sich befinden, als Erkennungen angezeigt.", + "notSelectDetections": "Alle {{detectionsLabels}} Objekte, die in {{zone}} auf {{cameraName}} erkannt wurden und nicht als Warnungen kategorisiert sind, werden unabhängig von der Zone, in der sie sich befinden, als Erkennungen angezeigt." + }, + "unsavedChanges": "Nicht gespeicherte Überprüfung der Klassifizierungseinstellungen für {{camera}}" + }, + "streams": { + "title": "Streams", + "desc": "Deaktiviere eine Kamera vorübergehend, bis Frigate neu gestartet wird. Das Deaktivieren einer Kamera stoppt die Verarbeitung der Streams dieser Kamera durch Frigate vollständig. Erkennung, Aufzeichnung und Debugging sind dann nicht mehr möglich.
    Hinweis: Go2RTC-Restreams werden dadurch nicht deaktiviert." + }, + "review": { + "title": "Überprüfung", + "alerts": "Warnungen ", + "detections": "Erkennungen ", + "desc": "Aktiviere/deaktiviere Benachrichtigungen und Erkennungen für diese Kamera vorübergehend, bis Frigate neu gestartet wird. Wenn deaktiviert, werden keine neuen Überprüfungseinträge erstellt. " + }, + "title": "Kameraeinstellungen", + "object_descriptions": { + "title": "Generative KI-Objektbeschreibungen", + "desc": "Generativen KI-Objektbeschreibungen für diese Kamera vorübergehend aktivieren/deaktivieren. Wenn diese Funktion deaktiviert ist, werden keine KI-generierten Beschreibungen für verfolgte Objekte auf dieser Kamera angefordert." + }, + "cameraConfig": { + "ffmpeg": { + "roles": "Rollen", + "pathRequired": "Stream-Pfad ist erforderlich", + "path": "Stream-Pfad", + "inputs": "Eingabe Streams", + "pathPlaceholder": "rtsp://...", + "rolesRequired": "Mindestens eine Rolle ist erforderlich", + "rolesUnique": "Jede Rolle (Audio, Erkennung, Aufzeichnung) kann nur einem Stream zugewiesen werden", + "addInput": "Eingabe-Stream hinzufügen", + "removeInput": "Eingabe-Stream entfernen", + "inputsRequired": "Mindestens ein Eingabe-Stream ist erforderlich" + }, + "enabled": "Aktiviert", + "namePlaceholder": "z. B., Vorder_Türe", + "nameInvalid": "Der Name der Kamera darf nur Buchstaben, Zahlen, Unterstriche oder Bindestriche enthalten", + "name": "Kamera Name", + "edit": "Kamera bearbeiten", + "add": "Kamera hinzufügen", + "description": "Kameraeinstellungen einschließlich Stream-Eingänge und Rollen konfigurieren.", + "nameRequired": "Kameraname ist erforderlich", + "toast": { + "success": "Kamera {{cameraName}} erfolgreich gespeichert" + }, + "nameLength": "Der Name der Kamera darf maximal 24 Zeichen lang sein." + }, + "backToSettings": "Zurück zu den Kamera Einstellungen", + "selectCamera": "Kamera wählen", + "editCamera": "Kamera bearbeiten:", + "addCamera": "Neue Kamera hinzufügen", + "review_descriptions": { + "desc": "Generativen KI-Objektbeschreibungen für diese Kamera vorübergehend aktivieren/deaktivieren. Wenn diese Funktion deaktiviert ist, werden keine KI-generierten Beschreibungen für Überprüfungselemente auf dieser Kamera angefordert.", + "title": "Beschreibungen zur generativen KI-Überprüfung" + } + }, + "masksAndZones": { + "form": { + "zoneName": { + "error": { + "hasIllegalCharacter": "Zonenname enthält unzulässige Zeichen.", + "alreadyExists": "Für diese Kamera existiert bereits eine Zone mit diesem Namen.", + "mustBeAtLeastTwoCharacters": "Der Zonenname muss aus mindestens 2 Zeichen bestehen.", + "mustNotBeSameWithCamera": "Der Zonenname darf nicht mit dem Kameranamen identisch sein.", + "mustNotContainPeriod": "Der Zonenname darf keine Punkte enthalten.", + "mustHaveAtLeastOneLetter": "Der Name der Zone muss mindestens einen Buchstaben enthalten." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Die Verweildauer muss größer oder gleich 0 sein." + } + }, + "distance": { + "error": { + "text": "Der Abstand muss größer als oder gleich 0.1 sein.", + "mustBeFilled": "Alle Entfernungsfelder müssen ausgefüllt werden, um die Geschwindigkeitsschätzung zu verwenden." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Die Trägheit muss über 0 liegen." + } + }, + "polygonDrawing": { + "removeLastPoint": "Letzten Punkt entfernen", + "reset": { + "label": "Alle Punkte löschen" + }, + "snapPoints": { + "true": "Fangpunkte", + "false": "Punkte nicht einrasten" + }, + "delete": { + "title": "Löschen bestätigen", + "success": "{{name}} wurde gelöscht.", + "desc": "Bist du sicher, dass du die {{type}} {{name}} löschen möchtest?" + }, + "error": { + "mustBeFinished": "Polygonzeichnung muss vor dem Speichern abgeschlossen sein." + }, + "type": { + "zone": "Zone", + "motion_mask": "Bewegungsmaske", + "object_mask": "Objektmaske" + }, + "revertOverride": { + "title": "Auf Standardkonfiguration zurücksetzen", + "desc": "Dadurch wird die Profilüberschreibung für {{type}}{{name}} aufgehoben und die Grundkonfiguration wiederhergestellt." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Der Geschwindigkeitsschwellwert muss größer oder gleich 0,1 sein." + } + }, + "id": { + "error": { + "mustNotBeEmpty": "Die ID darf nicht leer sein.", + "alreadyExists": "Für diese Kamera existiert bereits eine Maske mit dieser ID." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Das Feld „Name“ darf nicht leer sein." + } + } + }, + "toast": { + "error": { + "copyCoordinatesFailed": "Die Koordinaten konnten nicht in die Zwischenablage kopiert werden." + }, + "success": { + "copyCoordinates": "Koordinaten von {{polyName}} wurden in die Zwischenablage kopiert." + } + }, + "filter": { + "all": "Alle Maskierungen und Zonen" + }, + "zones": { + "edit": "Zone bearbeiten", + "toast": { + "success": "Die Zone ({{zoneName}}) wurde gespeichert." + }, + "desc": { + "documentation": "Dokumentation", + "title": "Zonen ermöglichen es dir, einen bestimmten Bereich des Bildes festzulegen, damit du bestimmen kannst, ob sich ein Objekt in einem bestimmten Bereich befindet oder nicht." + }, + "allObjects": "Alle Objekte", + "speedEstimation": { + "title": "Geschwindigkeitsschätzung", + "desc": "Aktiviere die Geschwindigkeitsabschätzung für Objekte in diesem Bereich. Der Bereich muss genau 4 Punkte haben.", + "docs": "Bitte lies die Dokumentation", + "lineADistance": "Spur A Distanz ({{unit}})", + "lineDDistance": "Spur D Distanz ({{unit}})", + "lineBDistance": "Spur B Distanz ({{unit}})", + "lineCDistance": "Spur C Distanz ({{unit}})" + }, + "label": "Zonen", + "documentTitle": "Zone bearbeiten - Frigate", + "add": "Zone hinzufügen", + "name": { + "title": "Name", + "inputPlaceHolder": "Geben Sie einen Namen ein…", + "tips": "Die Bezeichnung muss mindestens 2 Zeichen lang sein, mindestens einen Buchstaben enthalten und darf nicht die Bezeichnung einer Kamera oder einer anderen Zone sein." + }, + "objects": { + "title": "Objekte", + "desc": "Liste der Objekte, die zu diesem Bereich gehören." + }, + "speedThreshold": { + "title": "Geschwindigkeitsschwelle ({{unit}})", + "desc": "Legt eine Mindestgeschwindigkeit für Objekte fest, damit sie als \"in diesem Bereich\" betrachtet werden können.", + "toast": { + "error": { + "loiteringTimeError": "Zonen mit einer Verweilzeit größer als 0 sollten nicht für die Geschwindigkeitsmessung verwendet werden.", + "pointLengthError": "Die Geschwindigkeitsabschätzung wurde für diesen Bereich deaktiviert. Bereiche mit Geschwindigkeitsabschätzung müssen genau 4 Punkte haben." + } + } + }, + "point_one": "{{count}} Punkt", + "point_other": "{{count}} Punkte", + "clickDrawPolygon": "Klicke, um ein Polygon auf dem Bild zu zeichnen.", + "inertia": { + "desc": "Legt fest, wie viele Bilder ein Objekt in einem Bereich sein muss, bevor es \"als in dem Bereich befindlich\" betrachtet wird. Standard: 3", + "title": "Trägheit" + }, + "loiteringTime": { + "desc": "Legt eine Mindestzeit in Sekunden fest, die das Objekt in dem Bereich sein muss, damit es aktiviert wird. Standard: 0", + "title": "Verweilzeit" + }, + "enabled": { + "title": "Aktiviert", + "description": "Ob diese Zone in der Konfigurationsdatei aktiv und aktiviert ist. Ist sie deaktiviert, kann sie nicht über MQTT aktiviert werden. Deaktivierte Zonen werden zur Laufzeit ignoriert." + } + }, + "motionMasks": { + "desc": { + "documentation": "Dokumentation", + "title": "Bewegungsmasken werden verwendet, um zu verhindern, dass unerwünschte Bewegungsarten eine Erkennung auslösen. Zu starkes Maskieren erschwert das Nachverfolgen von Objekten." + }, + "documentTitle": "Bewegungsmaske bearbeiten – Frigate", + "context": { + "documentation": "Lies die Dokumentation", + "title": "Bewegungsmasken werden verwendet, um unerwünschte Bewegungen (z. B. Baumäste, Kamerazeitstempel) daran zu hindern, eine Erkennung auszulösen. Bewegungsmasken sollten sehr sparsam eingesetzt werden – zu starkes Maskieren erschwert das Nachverfolgen von Objekten." + }, + "clickDrawPolygon": "Klicke, um ein Polygon auf dem Bild zu zeichnen.", + "toast": { + "success": { + "noName": "Bewegungsmaske wurde gespeichert.", + "title": "{{polygonName}} wurde gespeichert." + } + }, + "add": "Neue Bewegungsmaske", + "edit": "Bewegungsmaske bearbeiten", + "polygonAreaTooLarge": { + "title": "Die Bewegungsmaske deckt {{polygonArea}} % des Kamerabildes ab. Große Bewegungsmasken werden nicht empfohlen.", + "tips": "Bewegungsmasken verhindern nicht, dass Objekte erkannt werden. Du solltest stattdessen eine erforderliche Zone verwenden.", + "documentation": "Lies die Dokumentation" + }, + "point_one": "{{count}} Punkt", + "point_other": "{{count}} Punkte", + "label": "Bewegungsmaske", + "defaultName": "Bewegungsmaske {{number}}", + "name": { + "title": "Name", + "description": "Ein optionaler beschreibender Name für diese Bewegungsmaske.", + "placeholder": "Gib einen Namen ein..." + } + }, + "restart_required": "Neustart erforderlich (Maske/Zone hat sich geändert)", + "objectMasks": { + "label": "Objektmasken", + "documentTitle": "Objektmaske bearbeiten – Frigate", + "toast": { + "success": { + "noName": "Objektmaske wurde gespeichert.", + "title": "{{polygonName}} wurde gespeichert." + } + }, + "desc": { + "title": "Objekt-Filtermasken werden verwendet, um Fehlalarme für einen bestimmten Objekttyp basierend auf dem Standort herauszufiltern.", + "documentation": "Dokumentation" + }, + "add": "Objektmaske hinzufügen", + "clickDrawPolygon": "Klicken Sie, um ein Polygon auf dem Bild zu zeichnen.", + "edit": "Objektmaske bearbeiten", + "context": "Objekt-Filtermasken werden verwendet, um Fehlalarme für einen bestimmten Objekttyp anhand des Standorts herauszufiltern.", + "point_one": "{{count}} Punkt", + "point_other": "{{count}} Punkte", + "objects": { + "title": "Objekte", + "desc": "Der Objekttyp, für den diese Objektmaske gilt.", + "allObjectTypes": "Alle Objekttypen" + }, + "name": { + "title": "Name", + "description": "Ein optionaler beschreibender Name für diese Objektmaske.", + "placeholder": "Gib einen Namen ein..." + } + }, + "motionMaskLabel": "Bewegungsmaske {{number}}", + "objectMaskLabel": "Objektmaske {{number}}", + "disabledInConfig": "Der Eintrag ist in der Konfigurationsdatei deaktiviert", + "profileBase": "(Base)", + "profileOverride": "(Überschreiben)", + "masks": { + "enabled": { + "title": "Aktiviert", + "description": "Ob diese Maske in der Konfigurationsdatei aktiviert ist. Ist sie deaktiviert, kann sie nicht über MQTT aktiviert werden. Deaktivierte Masken werden zur Laufzeit ignoriert." + } + }, + "addDisabledProfile": "Fügen Sie es zuerst der Basiskonfiguration hinzu und überschreiben Sie es dann im Profil" + }, + "debug": { + "objectShapeFilterDrawing": { + "score": "Bewertung", + "document": "Lies die Dokumentation. ", + "tips": "Aktiviere diese Option, um ein Rechteck auf dem Kamerabild zu zeichnen, das dessen Bereich und Seitenverhältnis anzeigt. Diese Werte können anschließend verwendet werden, um Parameter für den Objektform-Filter in deiner Konfiguration festzulegen.", + "area": "Bereich", + "ratio": "Verhältnis", + "title": "Objektform-Filter-Zeichnung", + "desc": "Zeichne ein Rechteck auf das Bild, um Flächen- und Verhältnisdetails anzuzeigen" + }, + "motion": { + "tips": "

    Bewegungsrahmen


    Rote Rahmen werden über die Bereiche des Bildes gelegt, in denen aktuell Bewegung erkannt wird.

    ", + "title": "Bewegungsrahmen", + "desc": "Rahmen um Bereiche anzeigen, in denen Bewegung erkannt wird" + }, + "boundingBoxes": { + "title": "Begrenzungsrahmen", + "desc": "Begrenzungsrahmen um verfolgte Objekte anzeigen", + "colors": { + "info": "
  • Beim Start werden jedem Objektlabel unterschiedliche Farben zugewiesen.
  • Eine dünne dunkelblaue Linie zeigt an, dass das Objekt zum aktuellen Zeitpunkt nicht erkannt wird.
  • Eine dünne graue Linie zeigt an, dass das Objekt als stationär erkannt wurde.
  • Eine dicke Linie zeigt an, dass das Objekt aktuell vom Autotracking verfolgt wird (wenn aktiviert).
  • ", + "label": "Farben der Objekt-Begrenzungsrahmen" + } + }, + "zones": { + "title": "Zonen", + "desc": "Umrisse aller definierten Zonen anzeigen" + }, + "timestamp": { + "desc": "Einen Zeitstempel auf dem Bild einblenden", + "title": "Zeitstempel" + }, + "mask": { + "desc": "Bewegungsmasken-Polygone anzeigen", + "title": "Bewegungsmasken" + }, + "detectorDesc": "Frigate verwendet deine Detektoren ({{detectors}}), um Objekte im Videostream deiner Kamera zu erkennen.", + "debugging": "Fehlersuche", + "objectList": "Objektliste", + "noObjects": "Keine Objekte", + "regions": { + "title": "Regionen", + "tips": "

    Regionsrahmen


    Leuchtend grüne Rahmen werden über die Interessensbereiche im Bild gelegt, die an den Objektdetektor übermittelt werden.

    ", + "desc": "Einen Rahmen für den an den Objektdetektor übermittelten Interessensbereich anzeigen" + }, + "title": "Debug", + "desc": "Die Debug-Ansicht zeigt eine Echtzeitansicht der verfolgten Objekte und ihrer Statistiken. Die Objektliste zeigt eine zeitverzögerte Zusammenfassung der erkannten Objekte.", + "paths": { + "title": "Pfade", + "desc": "Wichtige Punkte des Pfads des verfolgten Objekts anzeigen", + "tips": "

    Pfade


    Linien und Kreise zeigen wichtige Punkte an, an denen sich das verfolgte Objekt während seines Lebenszyklus bewegt hat.

    " + }, + "openCameraWebUI": "Web-Benutzeroberfläche von {{camera}} öffnen", + "audio": { + "title": "Audio", + "noAudioDetections": "Keine Audioerkennungen", + "score": "Punktzahl", + "currentRMS": "Aktueller Effektivwert", + "currentdbFS": "Aktuelle dbFS" + } + }, + "motionDetectionTuner": { + "Threshold": { + "title": "Schwellenwert", + "desc": "Der Schwellenwert legt fest, wie stark sich die Helligkeit eines Pixels ändern muss, damit dies als Bewegung erkannt wird. Standard: 30" + }, + "improveContrast": { + "title": "Kontrast verbessern", + "desc": "Den Kontrast für dunklere Szenen verbessern. Standard: EIN" + }, + "toast": { + "success": "Bewegungseinstellungen wurden gespeichert." + }, + "desc": { + "documentation": "Lies die Anleitung zur Bewegungsoptimierung", + "title": "Frigate verwendet die Bewegungserkennung als erste Überprüfung, um festzustellen, ob im Bildausschnitt etwas passiert, das eine Objekterkennung rechtfertigt." + }, + "contourArea": { + "title": "Konturfläche", + "desc": "Der Wert für die Konturfläche wird verwendet, um zu bestimmen, welche Gruppen von veränderten Pixeln als Bewegung gelten. Standard: 10" + }, + "title": "Bewegungserkennungs-Optimierer", + "unsavedChanges": "Nicht gespeicherte Änderungen im Bewegungserkennungs-Optimierer ({{camera}})" + }, + "users": { + "addUser": "Benutzer hinzufügen", + "updatePassword": "Passwort zurücksetzen", + "toast": { + "success": { + "deleteUser": "Benutzer {{user}} wurde erfolgreich gelöscht", + "createUser": "Benutzer {{user}} wurde erfolgreich erstellt", + "updatePassword": "Passwort erfolgreich aktualisiert.", + "roleUpdated": "Rolle für {{user}} aktualisiert" + }, + "error": { + "setPasswordFailed": "Speichern des Passworts fehlgeschlagen: {{errorMessage}}", + "createUserFailed": "Benutzer konnte nicht erstellt werden: {{errorMessage}}", + "deleteUserFailed": "Benutzer konnte nicht gelöscht werden: {{errorMessage}}", + "roleUpdateFailed": "Aktualisierung der Rolle fehlgeschlagen: {{errorMessage}}" + } + }, + "title": "Benutzer", + "management": { + "title": "Benutzerverwaltung", + "desc": "Verwalte die Benutzerkonten dieser Frigate-Instanz." + }, + "table": { + "changeRole": "Benutzerrolle ändern", + "deleteUser": "Benutzer löschen", + "noUsers": "Keine Benutzer gefunden.", + "password": "Passwort zurücksetzen", + "username": "Benutzername", + "actions": "Aktionen", + "role": "Rolle" + }, + "dialog": { + "form": { + "user": { + "title": "Benutzername", + "desc": "Nur Buchstaben, Zahlen, Punkte und Unterstriche sind erlaubt.", + "placeholder": "Benutzernamen eingeben" + }, + "password": { + "notMatch": "Passwörter stimmen nicht überein", + "strength": { + "weak": "Schwach", + "title": "Passwortstärke: ", + "medium": "Mittel", + "strong": "Stark", + "veryStrong": "Sehr stark" + }, + "confirm": { + "placeholder": "Bestätige Passwort", + "title": "Bestätige Passwort" + }, + "match": "Passwörter stimmen überein", + "title": "Passwort", + "placeholder": "Passwort eingeben", + "requirements": { + "title": "Passwort Anforderungen:", + "length": "Mindestens 12 Zeichen", + "uppercase": "Mindestens ein Großbuchstabe", + "digit": "Mindestens eine Ziffer", + "special": "Mindestens ein Sonderzeichen (!@#$%^&*(),.?\":{}|<>)" + }, + "show": "Passwort anzeigen", + "hide": "Verstecke Passwort" + }, + "newPassword": { + "title": "Neues Passwort", + "placeholder": "Neues Passwort eingeben", + "confirm": { + "placeholder": "Neues Passwort erneut eingeben" + } + }, + "usernameIsRequired": "Benutzername ist erforderlich", + "passwordIsRequired": "Passwort benötigt", + "currentPassword": { + "title": "Aktuelles Passwort", + "placeholder": "Gib Dein aktuelles Passwort ein" + } + }, + "changeRole": { + "desc": "Berechtigungen für {{username}} aktualisieren", + "roleInfo": { + "intro": "Wähle die entsprechende Rolle für diesen Benutzer:", + "admin": "Admin", + "adminDesc": "Voller Zugang zu allen Funktionen.", + "viewer": "Betrachter", + "viewerDesc": "Nur auf Live-Dashboards, Überprüfung, Erkundung und Exporte beschränkt.", + "customDesc": "Benutzerdefinierte Rolle mit spezifischem Kamerazugriff." + }, + "title": "Benutzerrolle ändern", + "select": "Wähle eine Rolle" + }, + "deleteUser": { + "desc": "Diese Aktion kann nicht rückgängig gemacht werden. Dadurch wird das Benutzerkonto dauerhaft gelöscht und alle zugehörigen Daten werden entfernt.", + "warn": "Bist du sicher, dass du {{username}} löschen willst?", + "title": "Benutzer löschen" + }, + "createUser": { + "title": "Neuen Benutzer anlegen", + "desc": "Füge ein neues Benutzerkonto hinzu und lege eine Rolle für den Zugriff auf Bereiche der Frigate-Benutzeroberfläche fest.", + "usernameOnlyInclude": "Der Benutzername darf nur Buchstaben, Zahlen, . oder _ enthalten", + "confirmPassword": "Bitte bestätige dein Passwort" + }, + "passwordSetting": { + "updatePassword": "Passwort für {{username}} aktualisieren", + "setPassword": "Passwort festlegen", + "desc": "Erstelle ein sicheres Passwort, um dieses Konto zu schützen.", + "cannotBeEmpty": "Das Passwort darf nicht leer sein", + "doNotMatch": "Die Passwörter sind nicht identisch", + "currentPasswordRequired": "Aktuelles Passwort wird benötigt", + "incorrectCurrentPassword": "Aktuelles Passwort ist falsch", + "passwordVerificationFailed": "Passwort konnte nicht überprüft werden", + "multiDeviceWarning": "Alle anderen Geräte, auf denen Sie angemeldet sind, müssen sich innerhalb von {{refresh_time}} erneut anmelden.", + "multiDeviceAdmin": "Sie können auch alle Benutzer dazu zwingen, sich sofort erneut zu authentifizieren, indem Sie Ihr JWT-Geheimnis ändern." + } + } + }, + "notification": { + "email": { + "desc": "Eine gültige E-Mail-Adresse ist erforderlich und wird verwendet, um Sie zu benachrichtigen, falls es Probleme mit dem Push-Dienst gibt.", + "placeholder": "z. B. example@email.com", + "title": "Email" + }, + "notificationSettings": { + "title": "Einstellungen für Benachrichtigungen", + "desc": "Frigate kann von Haus aus Push-Benachrichtigungen an ein Gerät senden, wenn es im Browser läuft oder als PWA installiert ist.", + "documentation": "Lese die Dokumentation" + }, + "title": "Benachrichtigungen", + "notificationUnavailable": { + "title": "Benachrichtigungen nicht verfügbar", + "desc": "Web Push Benachrichtigungen erfordern einen sicheren Kontext (https://…). Das ist eine Vorgabe des Browsers. Greife auf Frigate gesichert zu um Benachrichtigungen zu nutzen.", + "documentation": "Dokumentation lesen", + "descPwa": "Unter iOS sind Web-Push-Benachrichtigungen nur verfügbar, wenn Frigate auf Ihrem Startbildschirm installiert ist. Öffnen Sie das Menü Teilen, wählen Sie Zum Startbildschirm hinzufügen und öffnen Sie Frigate über das neue Symbol, um dieses Gerät für Benachrichtigungen zu registrieren." + }, + "cameras": { + "desc": "Wähle aus für welche Kameras Benachrichtigungen aktiviert werden sollen.", + "noCameras": "Keine Kameras verfügbar", + "title": "Kameras" + }, + "sendTestNotification": "Test Benachrichtigung senden", + "globalSettings": { + "desc": "Benachrichtigungen für bestimmte Kameras auf allen registrierten Geräten vorübergehend aussetzen.", + "title": "Globale Einstellungen" + }, + "deviceSpecific": "Geräte spezifische Einstellungen", + "active": "Benachrichtigungen aktiv", + "registerDevice": "Dieses Gerät registrieren", + "unregisterDevice": "Dieses Gerät abmelden", + "toast": { + "error": { + "registerFailed": "Speichern der Benachrichtigungsregistrierung fehlgeschlagen." + }, + "success": { + "registered": "Erfolgreich für Benachrichtigungen registriert. Starte Frigate neu bevor Benachrichtigungen (inklusive Testbenachrichtigung) gesendet werden können.", + "settingSaved": "Benachrichtigungseinstellungen wurden gespeichert." + } + }, + "suspendTime": { + "30minutes": "für 30 Minuten pausieren", + "1hour": "für 1 Stunde pausieren", + "12hours": "für 12 Stunden pausieren", + "untilRestart": "bis Neustart pausieren", + "24hours": "für 24 Stunden pausieren", + "5minutes": "Für 5 Minuten pausieren", + "10minutes": "Für 10 Minuten pausieren", + "suspend": "Pausieren" + }, + "cancelSuspension": "Pausieren abbrechen", + "suspended": "Benachrichtigungen für {{time}} pausiert", + "unsavedChanges": "Nicht gespeicherte Änderungen an den Benachrichtigungen", + "unsavedRegistrations": "Nicht gespeicherte Benachrichtigungsanmeldungen" + }, + "frigatePlus": { + "title": "Frigate+ Einstellungen", + "apiKey": { + "title": "Frigate+ API Key", + "desc": "Der Frigate+ API Key aktiviert die Integration des Frigate+ Dienstes.", + "validated": "Frigate+ API Key erkannt und validiert", + "notValidated": "Frigate+ API Key nicht erkannt und validiert", + "plusLink": "Lese mehr zu Frigate+" + }, + "snapshotConfig": { + "desc": "Für die Übermittlung an Frigate+ müssen Snapshots in Ihrer Konfiguration aktiviert sein.", + "cleanCopyWarning": "Bei einigen Kameras ist die Schnappschussfunktion deaktiviert", + "documentation": "die Dokumentation lesen", + "table": { + "camera": "Kamera", + "snapshots": "Snapshots", + "cleanCopySnapshots": "clean_copy Snapshots" + }, + "title": "Snapshot Einstellungen" + }, + "modelInfo": { + "modelType": "Model Typ", + "trainDate": "Trainings Datum", + "supportedDetectors": "Unterstützte Detektoren", + "modelSelect": "Die verfügbaren Modelle auf Frigate+ können hier ausgewählt werden. Beachte, dass nur Modelle kompatibel mit deiner aktuellen Detektorkonfiguration zur Auswahl stehen.", + "plusModelType": { + "baseModel": "Basis Model", + "userModel": "Feinabgestimmt" + }, + "cameras": "Kameras", + "loading": "Lade Model Informationen…", + "error": "Model Informationen laden fehlgeschlagen", + "availableModels": "Verfügbare Frigate+ Modelle", + "loadingAvailableModels": "Lade verfügbare Modelle…", + "baseModel": "Basis Model", + "title": "Model Informationen", + "noModelLoaded": "Derzeit ist kein „Frigate+“-Modell geladen.", + "selectModel": "Wählen Sie ein Modell aus", + "noModelsAvailable": "Keine Modelle verfügbar", + "filter": { + "ariaLabel": "Modelle nach Typ filtern", + "baseModels": "Basismodelle", + "fineTunedModels": "Optimierte Modelle" + } + }, + "toast": { + "error": "Speichern der Konfigurationsänderungen fehlgeschlagen: {{errorMessage}}", + "success": "Frigate+ Einstellungen wurden gespeichert. Starte Frigate neu um Änderungen anzuwenden." + }, + "restart_required": "Neustart erforderlich (Frigate+ Model geändert)", + "unsavedChanges": "Nicht gespeicherte Änderungen an den Frigate+-Einstellungen", + "description": "Frigate+ ist ein Abonnementdienst, der Ihnen Zugriff auf zusätzliche Funktionen und Möglichkeiten für Ihre Frigate-Instanz bietet, darunter die Möglichkeit, benutzerdefinierte Objekterkennungsmodelle zu verwenden, die auf Ihren eigenen Daten trainiert wurden. Hier können Sie Ihre Frigate+-Modelleinstellungen verwalten.", + "cardTitles": { + "api": "API", + "currentModel": "Aktuelles Modell", + "otherModels": "Anderes Modell", + "configuration": "Konfiguration" + }, + "changeInDetectorsAndModel": "Modell wechseln" + }, + "enrichments": { + "birdClassification": { + "title": "Vogelerkennung", + "desc": "Die Vogelerkennung identifiziert Vögelarten mithilfe eines quantisierten Tensorflowmodells. Wenn eine Vogelart erkannt wird, wird ihr Name als sub_label hinzugefügt. Diese Informationen sind in der Benutzeroberfläche, in Filtern und in Benachrichtigungen enthalten." + }, + "title": "Verfeinerungseinstellungen", + "unsavedChanges": "Ungesicherte geänderte Verbesserungseinstellungen", + "semanticSearch": { + "reindexNow": { + "confirmDesc": "Sind Sie sicher, dass Sie alle verfolgten Objekteinbettungen neu indizieren wollen? Dieser Prozess läuft im Hintergrund, kann aber Ihre CPU auslasten und eine gewisse Zeit in Anspruch nehmen. Sie können den Fortschritt auf der Seite Explore verfolgen.", + "label": "Jetzt neu indizieren", + "desc": "Bei der Neuindizierung werden die Einbettungen für alle verfolgten Objekte neu generiert. Dieser Prozess läuft im Hintergrund und kann je nach Anzahl der verfolgten Objekte Ihre CPU auslasten und eine gewisse Zeit in Anspruch nehmen.", + "confirmTitle": "Neuindizierung bestätigen", + "confirmButton": "Neuindizierung", + "success": "Die Neuindizierung wurde erfolgreich gestartet.", + "alreadyInProgress": "Die Neuindizierung ist bereits im Gange.", + "error": "Die Neuindizierung konnte nicht gestartet werden: {{errorMessage}}" + }, + "modelSize": { + "small": { + "desc": "Bei der Verwendung von klein wird eine quantisierte Version des Modells verwendet, die weniger Arbeitsspeicher verbraucht und schneller auf der CPU läuft, wobei der Unterschied in der Einbettungsqualität sehr gering ist.", + "title": "klein" + }, + "label": "Modell Größe", + "desc": "Die Größe des für die Einbettung der semantischen Suche verwendeten Modells.", + "large": { + "title": "groß", + "desc": "Bei der Verwendung von groß wird das gesamte Jina-Modell verwendet und automatisch auf der GPU ausgeführt, falls möglich." + } + }, + "title": "Semantische Suche", + "desc": "Die semantische Suche in Frigate ermöglicht es Ihnen, verfolgte Objekte innerhalb Ihrer Überprüfungselemente zu finden, indem Sie entweder das Bild selbst, eine benutzerdefinierte Textbeschreibung oder eine automatisch generierte Beschreibung verwenden.", + "readTheDocumentation": "Lies die Dokumentation" + }, + "faceRecognition": { + "title": "Gesichtserkennung", + "desc": "Die Gesichtserkennung ermöglicht es, Personen Namen zuzuweisen. Wenn ein Gesicht erkannt wird, ordnet Frigate den Namen der Person als Untertitel zu. Diese Informationen sind in der Benutzeroberfläche, den Filtern und in den Benachrichtigungen enthalten.", + "readTheDocumentation": "Lies die Dokumentation", + "modelSize": { + "label": "Modellgröße", + "desc": "Die Größe des für die Gesichtserkennung verwendeten Modells.", + "small": { + "title": "klein", + "desc": "Mit klein wird ein FaceNet-Gesichtseinbettungsmodell verwendet, das auf den meisten CPUs effizient läuft." + }, + "large": { + "title": "groß", + "desc": "Die Verwendung von groß verwendet ein ArcFace-Gesichtseinbettungsmodell und läuft automatisch auf der GPU, falls zutreffend." + } + } + }, + "licensePlateRecognition": { + "title": "Kennzeichenerkennung", + "desc": "Frigate kann Kennzeichen an Fahrzeugen erkennen und die erkannten Zeichen automatisch in das Feld recognized_license_plate oder einen bekannten Namen als sub_label zu Objekten vom Typ car hinzufügen. Ein häufiger Anwendungsfall ist das Lesen der Kennzeichen von Autos, die in eine Einfahrt einfahren oder auf einer Straße vorbeifahren.", + "readTheDocumentation": "Lies die Dokumentation" + }, + "restart_required": "Neustart erforderlich (Verbesserungseinstellungen geändert)", + "toast": { + "success": "Die Einstellungen für die Verbesserungen wurden gespeichert. Starten Sie Frigate neu, um Ihre Änderungen zu übernehmen.", + "error": "Konfigurationsänderungen konnten nicht gespeichert werden: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Auslöser", + "management": { + "title": "Auslöser", + "desc": "Auslöser für {{camera}} verwalten. Verwenden Sie den Vorschaubild Typ, um ähnliche Vorschaubilder wie das ausgewählte verfolgte Objekt auszulösen, und den Beschreibungstyp, um ähnliche Beschreibungen wie den von Ihnen angegebenen Text auszulösen." + }, + "addTrigger": "Auslöser hinzufügen", + "table": { + "name": "Name", + "type": "Typ", + "content": "Inhalt", + "threshold": "Schwellenwert", + "actions": "Aktionen", + "noTriggers": "Für diese Kamera sind keine Auslöser konfiguriert.", + "edit": "Bearbeiten", + "deleteTrigger": "Auslöser löschen", + "lastTriggered": "Zuletzt ausgelöst" + }, + "type": { + "thumbnail": "Vorschaubild", + "description": "Beschreibung" + }, + "actions": { + "alert": "Als Alarm markieren", + "notification": "Benachrichtigung senden", + "sub_label": "Unterlabel hinzufügen", + "attribute": "Attribut hinzufügen" + }, + "dialog": { + "createTrigger": { + "title": "Auslöser erstellen", + "desc": "Auslöser für Kamera {{camera}} erstellen" + }, + "editTrigger": { + "title": "Auslöser bearbeiten", + "desc": "Einstellungen für Kamera {{camera}} bearbeiten" + }, + "deleteTrigger": { + "title": "Auslöser löschen", + "desc": "Sind Sie sicher, dass Sie den Auslöser {{triggerName}} löschen wollen? Dies kann nicht Rückgängig gemacht werden." + }, + "form": { + "name": { + "title": "Name", + "placeholder": "Benennen Sie diesen Auslöser", + "error": { + "minLength": "Der Name muss mindestens 2 Zeichen lang sein.", + "invalidCharacters": "Der Name darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.", + "alreadyExists": "Ein Auslöser mit diesem Namen existiert bereits für diese Kamera." + }, + "description": "Geben Sie einen eindeutigen Namen oder eine Beschreibung ein, um diesen Auslöser zu identifizieren" + }, + "enabled": { + "description": "Diesen Auslöser aktivieren oder deaktivieren" + }, + "type": { + "title": "Typ", + "placeholder": "Auslöser Typ wählen", + "description": "Auslösen, wenn eine ähnliche Beschreibung eines verfolgten Objekts erkannt wird", + "thumbnail": "Auslösen, wenn eine ähnliche Miniaturansicht eines verfolgten Objekts erkannt wird" + }, + "content": { + "title": "Inhalt", + "imagePlaceholder": "Miniaturansicht auswählen", + "textPlaceholder": "Inhaltstext eingeben", + "imageDesc": "Es werden nur die letzten 100 Miniaturansichten angezeigt. Wenn Sie die gewünschte Miniaturansicht nicht finden können, überprüfen Sie bitte frühere Objekte in „Explore“ und richten Sie dort über das Menü einen Trigger ein.", + "textDesc": "Einen Text eingeben, um diese Aktion auszulösen, wenn eine ähnliche Beschreibung eines verfolgten Objekts erkannt wird.", + "error": { + "required": "Inhalt ist erforderlich." + } + }, + "threshold": { + "title": "Schwellenwert", + "error": { + "min": "Schwellenwert muss mindestens 0 sein", + "max": "Schwellenwert darf höchstens 1 sein" + }, + "desc": "Legen Sie den Ähnlichkeitsschwellenwert für diesen Trigger fest. Ein höherer Schwellenwert bedeutet, dass eine größere Übereinstimmung erforderlich ist, um den Trigger auszulösen." + }, + "actions": { + "title": "Aktionen", + "desc": "Standardmäßig sendet Frigate für alle Trigger eine MQTT-Nachricht. Unterbezeichnungen fügen den Triggernamen zur Objektbezeichnung hinzu. Attribute sind durchsuchbare Metadaten, die separat in den Metadaten des verfolgten Objekts gespeichert werden.", + "error": { + "min": "Mindesten eine Aktion muss ausgewählt sein." + } + }, + "friendly_name": { + "title": "Nutzerfreundlicher Name", + "placeholder": "Benenne oder beschreibe diesen Auslöser", + "description": "Ein optionaler nutzerfreundlicher Name oder eine Beschreibung für diesen Auslöser." + } + } + }, + "toast": { + "success": { + "createTrigger": "Auslöser {{name}} erfolgreich erstellt.", + "updateTrigger": "Auslöser {{name}} erfolgreich aktualisiert.", + "deleteTrigger": "Auslöser {{name}} erfolgreich gelöscht." + }, + "error": { + "createTriggerFailed": "Auslöser konnte nicht erstellt werden: {{errorMessage}}", + "updateTriggerFailed": "Auslöser könnte nicht aktualisiert werden: {{errorMessage}}", + "deleteTriggerFailed": "Auslöser konnte nicht gelöscht werden: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Semantische Suche ist deaktiviert", + "desc": "Semantische Suche muss aktiviert sein um Auslöser nutzen zu können." + }, + "wizard": { + "title": "Auslöser erstellen", + "step1": { + "description": "Konfigurieren Sie die Grundeinstellungen für Ihren Auslöser." + }, + "step2": { + "description": "Legen Sie den Inhalt fest, der diese Aktion auslöst." + }, + "step3": { + "description": "Konfigurieren Sie den Schwellenwert und die Aktionen für diesen Trigger." + }, + "steps": { + "nameAndType": "Name und Typ", + "configureData": "Daten konfigurieren", + "thresholdAndActions": "Schwellenwert und Maßnahmen" + } + } + }, + "roles": { + "dialog": { + "form": { + "cameras": { + "required": "Mindestens eine Kamera muss ausgewählt werden.", + "title": "Kameras", + "desc": "Wählen Sie die Kameras aus, auf die diese Rolle Zugriff hat. Mindestens eine Kamera ist erforderlich." + }, + "role": { + "title": "Rolle Name", + "placeholder": "Rollen Name eingeben", + "desc": "Es sind nur Buchstaben, Zahlen, Punkte und Unterstriche zulässig.", + "roleIsRequired": "Rollen Name ist erforderlich", + "roleOnlyInclude": "Der Rollenname darf nur Buchstaben, Zahlen, . oder _ enthalten", + "roleExists": "Eine Rolle mit diesem Namen existiert bereits." + } + }, + "createRole": { + "title": "Neue Rolle erstellen", + "desc": "Fügen Sie eine neue Rolle hinzu und legen Sie die Berechtigungen für den Kamerazugriff fest." + }, + "editCameras": { + "title": "Rollenkameras bearbeiten", + "desc": "Aktualisieren Sie den Kamerazugriff für die Rolle {{role}}." + }, + "deleteRole": { + "title": "Rolle löschen", + "desc": "Diese Aktion kann nicht rückgängig gemacht werden. Dadurch wird die Rolle dauerhaft gelöscht und allen Benutzern mit dieser Rolle die Rolle „Betrachter“ zugewiesen, die dann Zugriff auf alle Kameras erhält.", + "warn": "Möchten Sie {{role}} wirklich löschen?", + "deleting": "Lösche..." + } + }, + "management": { + "title": "Zuschauer Rollenverwaltung", + "desc": "Verwalten Sie benutzerdefinierte Zuschauerrollen und ihre Kamerazugriffsberechtigungen für diese Frigate-Instanz." + }, + "addRole": "Rolle hinzufügen", + "table": { + "role": "Rolle", + "cameras": "Kameras", + "actions": "Aktionen", + "noRoles": "Keine benutzerdefinierten Rollen gefunden.", + "editCameras": "Kameras bearbeiten", + "deleteRole": "Rolle löschen" + }, + "toast": { + "success": { + "createRole": "Rolle {{role}} erfolgreich erstellt", + "updateCameras": "Kameras für Rolle {{role}} aktualisiert", + "deleteRole": "Rolle {{role}} erfolgreich gelöscht", + "userRolesUpdated_one": "{{count}} Benutzer, denen diese Rolle zugewiesen wurde, wurden auf „Zuschauer“ aktualisiert, der Zugriff auf alle Kameras hat.", + "userRolesUpdated_other": "{{count}} Benutzer, denen diese Rollen zugewiesen wurde, wurden auf „Zuschauer“ aktualisiert, der Zugriff auf alle Kameras habem." + }, + "error": { + "createRoleFailed": "Fehler beim Erstellen der Rolle: {{errorMessage}}", + "updateCamerasFailed": "Aktualisierung der Kameras fehlgeschlagen: {{errorMessage}}", + "deleteRoleFailed": "Rolle konnte nicht gelöscht werden: {{errorMessage}}", + "userUpdateFailed": "Aktualisierung der Benutzerrollen fehlgeschlagen: {{errorMessage}}" + } + } + }, + "cameraWizard": { + "title": "Kamera hinzufügen", + "description": "Folge den Anweisungen unten, um eine neue Kamera zu deiner Frigate-Installation hinzuzufügen.", + "steps": { + "nameAndConnection": "Name & Verbindung", + "streamConfiguration": "Stream Konfiguration", + "validationAndTesting": "Überprüfung & Testen", + "probeOrSnapshot": "Test oder Momentaufnahme" + }, + "save": { + "success": "Neue Kamera {{cameraName}} erfolgreich hinzugefügt.", + "failure": "Fehler beim Speichern von {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Auflösung", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Bitte korrekte Stream-URL eingeben", + "testFailed": "Stream Test fehlgeschlagen: {{error}}" + }, + "step1": { + "description": "Geben Sie Ihre Kameradaten ein und wählen Sie, ob Sie die Kamera automatisch erkennen lassen oder die Marke manuell auswählen möchten.", + "cameraName": "Kameraname", + "cameraNamePlaceholder": "z.B. vordere_tür oder Hof Übersicht", + "host": "Host/IP Adresse", + "port": "Port", + "username": "Nutzername", + "usernamePlaceholder": "Optional", + "password": "Passwort", + "passwordPlaceholder": "Optional", + "selectTransport": "Transport-Protokoll auswählen", + "cameraBrand": "Kamerahersteller", + "selectBrand": "Wähle die Kamerahersteller für die URL-Vorlage aus", + "customUrl": "Benutzerdefinierte Stream-URL", + "brandInformation": "Hersteller Information", + "brandUrlFormat": "Für Kameras mit RTSP URL nutze folgendes Format: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nutzername:passwort@host:port/pfad", + "testConnection": "Teste Verbindung", + "testSuccess": "Verbindungstest erfolgreich!", + "testFailed": "Verbindungstest fehlgeschlagen. Bitte prüfe deine Eingaben und versuche es erneut.", + "streamDetails": "Stream Details", + "warnings": { + "noSnapshot": "Es kann kein Snapshot aus dem konfigurierten Stream abgerufen werden." + }, + "errors": { + "brandOrCustomUrlRequired": "Wählen Sie entweder einen Kamerahersteller mit Host/IP aus oder wählen Sie „Andere“ mit einer benutzerdefinierten URL", + "nameRequired": "Der Kameraname wird benötigt", + "nameLength": "Der Kameraname darf höchsten 64 Zeichen lang sein", + "invalidCharacters": "Der Kameraname enthält ungültige Zeichen", + "nameExists": "Der Kameraname existiert bereits", + "brands": { + "reolink-rtsp": "Reolink RTSP wird nicht empfohlen. Es wird empfohlen, http in den Kameraeinstellungen zu aktivieren und den Kamera-Assistenten neu zu starten." + }, + "customUrlRtspRequired": "Benutzerdefinierte URLs müssen mit „rtsp://“ oder \"rtsps://\" beginnen. Für Nicht-RTSP-Kamerastreams ist eine manuelle Konfiguration erforderlich." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "connectionSettings": "Verbindungseinstellungen", + "detectionMethod": "Stream Erkennungsmethode", + "onvifPort": "ONVIF Port", + "probeMode": "Untersuche Kamera", + "detectionMethodDescription": "Suchen Sie die Kamera mit ONVIF (sofern unterstützt), um die URLs der Kamerastreams zu finden, oder wählen Sie manuell die Kameramarke aus, um vordefinierte URLs zu verwenden. Um eine benutzerdefinierte RTSP-URL einzugeben, wählen Sie die manuelle Methode und dann „Andere“.", + "onvifPortDescription": "Bei Kameras, die ONVIF unterstützen, ist dies in der Regel 80 oder 8080.", + "useDigestAuth": "Digest-Authentifizierung verwenden", + "useDigestAuthDescription": "Verwenden Sie die HTTP-Digest-Authentifizierung für ONVIF. Einige Kameras erfordern möglicherweise einen speziellen ONVIF-Benutzernamen/ein spezielles ONVIF-Passwort anstelle des Standard-Admin-Benutzers.", + "manualMode": "Manuelle Auswahl" + }, + "step2": { + "description": "Suchen Sie in der Kamera nach verfügbaren Streams oder konfigurieren Sie manuelle Einstellungen basierend auf der von Ihnen ausgewählten Erkennungsmethode.", + "streamsTitle": "Kamera Streams", + "addStream": "Stream hinzufügen", + "addAnotherStream": "Weiteren Stream hinzufügen", + "streamTitle": "Stream {{nummer}}", + "streamUrl": "Stream URL", + "streamUrlPlaceholder": "rtsp://nutzername:passwort@host:port/pfad", + "url": "URL", + "resolution": "Auflösung", + "selectResolution": "Auflösung auswählen", + "quality": "Qualität", + "selectQuality": "Qualität auswählen", + "roles": "Rollen", + "roleLabels": { + "detect": "Objekt-Erkennung", + "record": "Aufzeichnung", + "audio": "Audio" + }, + "testStream": "Verbindung testen", + "testSuccess": "Verbindung erfolgreich getestet!", + "testFailed": "Verbindungstest fehlgeschlagen. Bitte überprüfen Sie ihre Eingaben und versuchen Sie es erneut.", + "testFailedTitle": "Test fehlgeschlagen", + "connected": "Verbunden", + "notConnected": "Nicht verbunden", + "featuresTitle": "Funktionen", + "go2rtc": "Verbindungen zur Kamera reduzieren", + "detectRoleWarning": "Mindestens ein Stream muss die Rolle „detect“ haben, um fortfahren zu können.", + "rolesPopover": { + "title": "Stream Rollen", + "detect": "Haupt-Feed für Objekt-Erkennung.", + "record": "Speichert Segmente des Video-Feeds basierend auf den Konfigurationseinstellungen.", + "audio": "Feed für audiobasierte Erkennung." + }, + "featuresPopover": { + "title": "Stream Funktionen", + "description": "Verwende go2rtc Restreaming, um die Verbindungen zu deiner Kamera zu reduzieren." + }, + "streamDetails": "Verbindungsdetails", + "probing": "Kamera wird geprüft...", + "retry": "Wiederholen", + "testing": { + "probingMetadata": "Metadaten der Kamera werden überprüft...", + "fetchingSnapshot": "Kamera-Schnappschuss wird abgerufen..." + }, + "probeFailed": "Fehler beim Untersuchen der Kamera: {{error}}", + "probingDevice": "Untersuche Gerät...", + "probeSuccessful": "Erkennung erfolgreich", + "probeError": "Erkennungsfehler", + "probeNoSuccess": "Erkennung fehlgeschlagen", + "deviceInfo": "Geräteinformationen", + "manufacturer": "Hersteller", + "model": "Modell", + "firmware": "Firmware", + "profiles": "Profile", + "ptzSupport": "PTZ Unterstützung", + "autotrackingSupport": "Unterstützung für Autoverfolgung", + "presets": "Voreinstellung", + "rtspCandidates": "RTSP Kandidaten", + "rtspCandidatesDescription": "Die folgenden RTSP-URLs wurden bei der Kameraerkennung gefunden. Testen Sie die Verbindung, um die Stream-Metadaten anzuzeigen.", + "noRtspCandidates": "Es wurden keine RTSP-URLs von der Kamera gefunden. Möglicherweise sind Ihre Anmeldedaten falsch oder die Kamera unterstützt ONVIF oder die Methode zum Abrufen von RTSP-URLs nicht. Gehen Sie zurück und geben Sie die RTSP-URL manuell ein.", + "candidateStreamTitle": "Kandidate {{number}}", + "useCandidate": "Verwenden", + "uriCopy": "Kopieren", + "uriCopied": "URI in die Zwischenablage kopiert", + "testConnection": "Test Verbindung", + "toggleUriView": "Klicken Sie hier, um die vollständige URI zu sehen", + "errors": { + "hostRequired": "Host/IP adresse wird benötigt" + } + }, + "step3": { + "description": "Konfigurieren Sie Stream-Rollen und fügen Sie zusätzliche Streams für Ihre Kamera hinzu.", + "validationTitle": "Stream Validierung", + "connectAllStreams": "Verbinde alle Streams", + "reconnectionSuccess": "Wiederverbindung erfolgreich.", + "reconnectionPartial": "Einige Streams konnten nicht wieder verbunden werden.", + "streamUnavailable": "Stream-Vorschau nicht verfügbar", + "reload": "Neu laden", + "connecting": "Verbinde...", + "streamTitle": "Stream {{number}}", + "valid": "Gültig", + "failed": "Fehlgeschlagen", + "notTested": "Nicht getestet", + "connectStream": "Verbinden", + "connectingStream": "Verbinde", + "disconnectStream": "Trennen", + "estimatedBandwidth": "Geschätzte Bandbreite", + "roles": "Rollen", + "none": "Keine", + "error": "Fehler", + "streamValidated": "Stream {{number}} wurde erfolgreich validiert", + "streamValidationFailed": "Stream {{number}} Validierung fehlgeschlagen", + "saveAndApply": "Neue Kamera speichern", + "saveError": "Ungültige Konfiguration. Bitte prüfe die Einstellungen.", + "issues": { + "title": "Stream Validierung", + "videoCodecGood": "Video-Codec ist {{codec}}.", + "audioCodecGood": "Audio-Codec ist {{codec}}.", + "noAudioWarning": "Für diesen Stream wurde kein Ton erkannt, die Aufzeichnungen enthalten keinen Ton.", + "audioCodecRecordError": "Der AAC-Audio-Codec ist erforderlich, um Audio in Aufnahmen zu unterstützen.", + "audioCodecRequired": "Ein Audiostream ist erforderlich, um Audioerkennung zu unterstützen.", + "restreamingWarning": "Eine Reduzierung der Verbindungen zur Kamera für den Aufzeichnungsstream kann zu einer etwas höheren CPU-Auslastung führen.", + "dahua": { + "substreamWarning": "Substream 1 ist auf eine niedrige Auflösung festgelegt. Viele Kameras von Dahua / Amcrest / EmpireTech unterstützen zusätzliche Substreams, die in den Kameraeinstellungen aktiviert werden müssen. Es wird empfohlen, diese Streams zu nutzen, sofern sie verfügbar sind." + }, + "hikvision": { + "substreamWarning": "Substream 1 ist auf eine niedrige Auflösung festgelegt. Viele Hikvision-Kameras unterstützen zusätzliche Substreams, die in den Kameraeinstellungen aktiviert werden müssen. Es wird empfohlen, diese Streams zu nutzen, sofern sie verfügbar sind." + } + }, + "streamsTitle": "Kamera Stream", + "addStream": "Stream hinzufügen", + "addAnotherStream": "weiteren Stream hinzufügen", + "streamUrl": "Stream URL", + "streamUrlPlaceholder": "rtsp://benutzername:passwort@host:port/path", + "selectStream": "Auswahl Stream", + "searchCandidates": "Suche Kandidaten...", + "noStreamFound": "Kein Stream gefunden", + "url": "URL", + "resolution": "Auflösung", + "selectResolution": "Wähle Auflösung", + "quality": "Qualität", + "selectQuality": "Wähle Qualität", + "roleLabels": { + "detect": "Objekterkennung", + "record": "Aufnahme", + "audio": "Ton" + }, + "testStream": "Verbindungstest", + "testSuccess": "Verbindungstest erfolgreich!", + "testFailed": "Verbindungstest fehlgeschlagen", + "testFailedTitle": "Test fehlgeschlagen", + "connected": "Verbunden", + "notConnected": "nicht verbunden", + "featuresTitle": "Funktionen", + "go2rtc": "Verbindungen zur Kamera reduzieren", + "detectRoleWarning": "Mindestens ein Stream muss die Rolle „detect“ haben, um fortfahren zu können.", + "rolesPopover": { + "title": "Stream Rollen", + "detect": "Hauptfeed für die Objekterkennung.", + "record": "Speichert Segmente des Video-Feeds basierend auf den Konfigurationseinstellungen.", + "audio": "Feed für audiobasierte Erkennung." + }, + "featuresPopover": { + "title": "Stream Funktionen", + "description": "Verwenden Sie go2rtc-Restreaming, um die Verbindungen zu Ihrer Kamera zu reduzieren." + } + }, + "step4": { + "description": "Endgültige Validierung und Analyse vor dem Speichern Ihrer neuen Kamera. Verbinden Sie jeden Stream vor dem Speichern.", + "validationTitle": "Stream-Validierung", + "connectAllStreams": "Alle Streams verbinden", + "reconnectionSuccess": "Wiederverbindung erfolgreich.", + "reconnectionPartial": "Einige Streams konnten nicht wieder verbunden werden.", + "streamUnavailable": "Stream Vorschau nicht verfügbar", + "reload": "neu Laden", + "connecting": "Verbinden...", + "streamTitle": "Stream {{number}}", + "valid": "gültig", + "failed": "fehlgeschlagen", + "notTested": "nicht getestet", + "connectStream": "Verbinden", + "connectingStream": "Verbinden", + "disconnectStream": "getrennt", + "estimatedBandwidth": "Voraussichtliche Bandbreite", + "roles": "Rollen", + "ffmpegModule": "Stream-Kompatibilitätsmodus verwenden", + "ffmpegModuleDescription": "Wenn der Stream nach mehreren Versuchen nicht geladen wird, versuchen Sie, diese Option zu aktivieren. Wenn diese Option aktiviert ist, verwendet Frigate das ffmpeg-Modul mit go2rtc. Dies kann zu einer besseren Kompatibilität mit einigen Kamerastreams führen.", + "none": "keiner", + "error": "Fehler", + "streamValidated": "Steam {{number}} erfolgreich validiert", + "streamValidationFailed": "Stream {{number}} Validierung fehlgeschlagen", + "saveAndApply": "Neue Kamera speichern", + "saveError": "Ungültige Konfiguration. Bitte überprüfen Sie Ihre Einstellungen.", + "issues": { + "title": "Stream-Validierung", + "videoCodecGood": "Video codec ist {{codec}}.", + "audioCodecGood": "Audio codec ist {{codec}}.", + "resolutionHigh": "Eine Auflösung von {{resolution}} kann zu einem erhöhten Ressourcenverbrauch führen.", + "resolutionLow": "Eine Auflösung von {{resolution}} ist möglicherweise zu gering, um kleine Objekte zuverlässig zu erkennen.", + "noAudioWarning": "Für diesen Stream wurde kein Ton erkannt, die Aufzeichnungen enthalten keinen Ton.", + "audioCodecRecordError": "Der AAC-Audio-Codec ist erforderlich, um Audio in Aufnahmen zu unterstützen.", + "audioCodecRequired": "Ein Audiostream ist erforderlich, um die Audioerkennung zu unterstützen.", + "restreamingWarning": "Die Reduzierung der Verbindungen zur Kamera für den Aufzeichnungsstream kann zu einer geringfügigen Erhöhung der CPU-Auslastung führen.", + "brands": { + "reolink-rtsp": "Reolink RTSP wird nicht empfohlen. Aktivieren Sie HTTP in den Firmware-Einstellungen der Kamera und starten Sie den Assistenten neu.", + "reolink-http": "Für eine bessere Kompatibilität sollten Reolink HTTP-Streams FFmpeg nutzen. Aktiviere für diesen Stream 'Stream-Kompatibilitätsmodus verwenden'." + }, + "dahua": { + "substreamWarning": "Substream 1 ist auf eine niedrige Auflösung festgelegt. Viele Kameras von Dahua / Amcrest / EmpireTech unterstützen zusätzliche Substreams, die in den Kameraeinstellungen aktiviert werden müssen. Es wird empfohlen, diese Streams zu überprüfen und zu nutzen, sofern sie verfügbar sind." + }, + "hikvision": { + "substreamWarning": "Substream 1 ist auf eine niedrige Auflösung festgelegt. Viele Hikvision-Kameras unterstützen zusätzliche Substreams, die in den Kameraeinstellungen aktiviert werden müssen. Es wird empfohlen, diese Streams zu überprüfen und zu nutzen, sofern sie verfügbar sind." + }, + "resolutionUnknown": "Die Auflösung dieses Streams konnte nicht ermittelt werden. Sie sollten die Erkennungsauflösung manuell in den Einstellungen oder in Ihrer Konfiguration festlegen." + } + } + }, + "cameraManagement": { + "title": "Kameras verwalten", + "addCamera": "Neue Kamera hinzufügen", + "editCamera": "Kamera bearbeiten:", + "selectCamera": "Wähle eine Kamera", + "backToSettings": "Zurück zu Kameraeinstellungen", + "streams": { + "title": "Kamerastatus und Details", + "desc": "Deaktiviere eine Kamera vorübergehend, bis Frigate neu gestartet wird. Deaktivierung einer Kamera stoppt die Verarbeitung der Streams dieser Kamera durch Frigate vollständig. Erkennung, Aufzeichnung und Debugging sind dann nicht mehr verfügbar.
    Hinweis: Dies deaktiviert nicht die go2rtc restreams.", + "enableLabel": "Aktivierte Kameras", + "enableDesc": "Eine aktivierte Kamera vorübergehend deaktivieren, bis Frigate neu gestartet wird. Durch das Deaktivieren einer Kamera wird die Verarbeitung der Streams dieser Kamera durch Frigate vollständig unterbrochen. Erkennung, Aufzeichnung und Fehlerbehebung stehen dann nicht mehr zur Verfügung.
    Hinweis: go2rtc-Restreams werden dadurch nicht deaktiviert.", + "disableLabel": "Deaktivierte Kameras", + "disableDesc": "Aktivieren Sie eine Kamera, die derzeit in der Benutzeroberfläche nicht sichtbar und in der Konfiguration deaktiviert ist. Nach der Aktivierung ist ein Neustart von Frigate erforderlich.", + "enableSuccess": "{{cameraName}} wurde aktiviert. Starte Frigate neu, um die Änderung zu übernehmen.", + "friendlyName": { + "edit": "Anzeigenamen der Kamera bearbeiten", + "title": "Anzeigenamen bearbeiten", + "description": "Legen Sie den Anzeigenamen fest, der für diese Kamera in der gesamten Benutzeroberfläche von „Frigate“ angezeigt wird. Lassen Sie das Feld leer, um die Kamera-ID zu verwenden.", + "rename": "Umbenennen" + }, + "reorderHandle": "Zum Neuanordnen ziehen", + "saving": "Speichern…", + "saved": "gespeichert", + "details": { + "edit": "Kameradaten bearbeiten", + "title": "Kameradaten bearbeiten", + "description": "Aktualisieren Sie den Anzeigenamen, die externe URL und die Sichtbarkeit, die für diese Kamera in der gesamten Frigate-Benutzeroberfläche verwendet werden.", + "friendlyNameLabel": "Display Name", + "friendlyNameHelp": "Der in der Benutzeroberfläche von „Frigate“ für diese Kamera angezeigte Spitzname. Lassen Sie das Feld leer, um die Kamera-ID zu verwenden.", + "webuiUrlLabel": "URL der Web-Benutzeroberfläche", + "webuiUrlHelp": "URL, um die Web-Benutzeroberfläche der Kamera direkt aus der Debug-Ansicht aufzurufen. Lassen Sie das Feld leer, um den Link zu deaktivieren.", + "webuiUrlInvalid": "Es muss sich um eine gültige URL handeln (z. B. https://example.com).", + "dashboardLabel": "Im Live-Dashboard anzeigen", + "dashboardHelp": "Diese Kamera im Live-Dashboard anzeigen.", + "reviewLabel": "In der Überprüfung anzeigen", + "reviewHelp": "Zeige diese Kamera in der Übersicht an, einschließlich des Kamerafilters, der Bewegungsübersicht und der Verlaufsansicht." + }, + "label": "Kamerazustand", + "description": "Legen Sie den Betriebszustand für jede Kamera fest.

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

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

    Ziehen Sie den Griff, um die Reihenfolge der aktiven Kameras in der Benutzeroberfläche anzupassen, einschließlich des Live-Dashboards und der Dropdown-Menüs zur Kameraauswahl.", + "disabledSubheading": "In der Konfiguration deaktiviert", + "status": { + "on": "Eingeschaltet", + "off": "Ausgeschaltet", + "disabled": "Deaktiviert" + }, + "disableSuccess": "{{cameraName}} wurde deaktiviert und in der Konfiguration gespeichert." + }, + "cameraConfig": { + "add": "Kamera hinzufügen", + "edit": "Kamera bearbeiten", + "description": "Konfiguriere die Kameraeinstellungen, einschließlich Streams und Rollen.", + "name": "Kameraname", + "nameRequired": "Kameraname benötigt", + "nameLength": "Kameraname darf maximal 64 Zeichen lang sein.", + "namePlaceholder": "z.B. vordere_tür oder Hof Übersicht", + "enabled": "Aktiviert", + "ffmpeg": { + "inputs": "Eingang Streams", + "path": "Stream-Pfad", + "pathRequired": "Stream-Pfad benötigt", + "pathPlaceholder": "rtsp://...", + "roles": "Rollen", + "rolesRequired": "Mindestens eine Rolle wird benötigt", + "rolesUnique": "Jede Rolle (audio, detect, record) kann nur einem Stream zugewiesen werden", + "addInput": "Eingangs-Stream hinzufügen", + "removeInput": "Eingangs-Stream entfernen", + "inputsRequired": "Es wird mindestens ein Eingangs-Stream benötigt" + }, + "go2rtcStreams": "go2rtc Streams", + "streamUrls": "Stream URLs", + "addUrl": "URL hinzufügen", + "addGo2rtcStream": "go2rtc Stream hinzufügen", + "toast": { + "success": "Kamera {{cameraName}} erfolgreich gespeichert" + } + }, + "deleteCamera": "Kamera löschen", + "deleteCameraDialog": { + "title": "Kamera löschen", + "description": "Durch das Löschen einer Kamera werden alle Aufzeichnungen, erfassten Objekte und Konfigurationseinstellungen für diese Kamera endgültig entfernt. Alle mit dieser Kamera verbundenen go2rtc-Streams müssen möglicherweise noch manuell entfernt werden.", + "selectPlaceholder": "Kamera auswählen...", + "confirmTitle": "Bist du dir sicher?", + "confirmWarning": "Das Löschen von {{cameraName}} kann nicht rückgängig gemacht werden.", + "deleteExports": "Lösche auch die Exporte für diese Kamera", + "confirmButton": "Dauerhalft löschen", + "success": "Die Kamera {{cameraName}} wurde erfolgreich gelöscht", + "error": "Das Löschen der Kamera {{cameraName}} ist fehlgeschlagen" + }, + "profiles": { + "title": "Profilkameraumschaltungen", + "selectLabel": "Profil auswählen", + "description": "Legen Sie fest, welche Kameras bei der Aktivierung eines Profils ein- oder ausgeschaltet werden. Kameras, die auf „Übernehmen“ eingestellt sind, behalten ihren Standardzustand bei.", + "inherit": "Erben", + "enabled": "Aktiviert", + "disabled": "Deaktiviert", + "on": "Eingeschaltet", + "off": "Ausgeschaltet" + }, + "cameraType": { + "title": "Kamera Art", + "label": "Kamera Art", + "description": "Legen Sie den Kameratyp für jede Kamera fest. Spezielle LPR-Kameras sind Kameras mit leistungsstarkem optischen Zoom, um Kennzeichen von weit entfernten Fahrzeugen zu erfassen. Für die meisten Kameras sollte der normale Kameratyp verwendet werden, es sei denn, die Kamera ist speziell für LPR vorgesehen und verfügt über einen stark fokussierten Blickwinkel auf die Kennzeichen.", + "normal": "Normal", + "dedicatedLpr": "Spezielles LPR-System", + "saveSuccess": "Der Kameratyp für {{cameraName}} wurde aktualisiert. Starte Frigate neu, um die Änderungen zu übernehmen." + }, + "description": "Fügen Sie Kameras hinzu, bearbeiten und löschen Sie sie, steuern Sie den Status jeder einzelnen Kamera und konfigurieren Sie profil- und kameratypabhängige Übersteuerungen. Um Streams, Erkennung, Bewegung und andere kameraspezifische Einstellungen zu konfigurieren, wählen Sie den entsprechenden Abschnitt unter „Kamerakonfiguration“ aus.", + "clone": { + "sectionTitle": "Einstellungen klonen", + "sectionDescription": "Konfiguration von einer Kamera auf eine andere oder eine neue Kamera kopieren.", + "button": "Einstellungen klonen", + "title": "Kameraeinstellungen kopieren", + "description": "Kopieren Sie die Konfiguration einer Kamera auf eine oder mehrere andere Kameras oder auf eine neue Kamera. Die Identitätsdaten (Name, Anzeigename, URL der Web-Benutzeroberfläche, Anzeigereihenfolge) werden dabei nicht kopiert.", + "source": { + "label": "Quellkamera", + "placeholder": "Wählen Sie eine Quellkamera aus", + "required": "Wählen Sie eine Quellkamera aus" + }, + "target": { + "legend": "Ziel", + "newRadio": "Neue Kamera", + "newNameLabel": "Kamera Name", + "newNamePlaceholder": "z. B. back_door oder Back Door", + "newNameRequired": "Kamera Name ist erforderlich", + "newNameInvalid": "ungültiger Kamera Name", + "newNameCollision": "Eine Kamera mit diesem Namen gibt es bereits", + "newStreamsForced": "Streams werden bei einer neuen Kamera immer kopiert.", + "existingCamerasRadio": "Vorhandene Kameras", + "allCameras": "Alle Kameras", + "existingPlaceholder": "Wählen Sie mindestens eine Kamera aus", + "existingDisabled": "Es gibt keine weiteren Kameras, auf die kopiert werden kann" + }, + "categories": { + "legend": "Zu klonende Einstellungen", + "description": "Wählen Sie aus, welche Einstellungen von der Quellkamera kopiert werden sollen.", + "selectAll": "Alle auswählen", + "selectNone": "Keine auswählen", + "resetDefaults": "Auf Standardwerte zurücksetzen", + "general": "Allgemeines", + "spatial": "Räumliche Rahmenbedingungen", + "streams": "Streams", + "spatialWarningTitle": "Auflösungsdiskrepanz", + "spatialWarning": "Die Quellkamera {{srcCamera}} hat eine andere Auflösung ({{srcWidth}}×{{srcHeight}}) als: {{cameras}}. Die Polygone sind möglicherweise nicht auf diese Kameras ausgerichtet. Diese Standardeinstellungen sind deaktiviert; aktivieren Sie sie, um die Daten unverändert zu kopieren.", + "restartHint": "Neustart erforderlich", + "items": { + "record": "Aufnahme", + "snapshots": "Momentaufnahmen", + "motion": "Bewegungserkennung", + "objects": "Objekte", + "audio": "Tonerkennung", + "audio_transcription": "Audio-Transkription", + "notifications": "Benachrichtigungen", + "birdseye": "Birdseye", + "mqtt": "MQTT", + "timestamp_style": "Format für Zeitstempel", + "onvif": "ONVIF", + "lpr": "Kennzeichenerkennung", + "face_recognition": "Gesichtserkennung", + "semantic_search": "Semantische Suche", + "genai": "Generative AI", + "type": "Kameratyp (Standard / speziell für Kennzeichenerkennung)", + "profiles": "Profile", + "detect": "Abmessungen ermitteln", + "zones": "Zonen", + "motion_mask": "Bewegungsmaske", + "object_masks": "Objektmaske", + "ffmpeg_live": "Stream-URLs und Rollen", + "review": "Rezension" + } + }, + "footer": { + "restartNeeded": "Für einige Änderungen ist ein Neustart erforderlich.", + "liveOnly": "Alle Änderungen werden sofort wirksam, ohne dass ein Neustart erforderlich ist.", + "submit": "Klon", + "submitting": "Klonen…", + "changeCount_one": "Die Änderung von {{count}} wird übernommen", + "changeCount_other": "Die Änderungen von {{count}} werden übernommen" + }, + "toast": { + "success": "Einstellungen wurden auf {{cameraName}} kopiert", + "successWithRestart": "Die Einstellungen wurden auf {{cameraName}} kopiert. Starte Frigate neu, um alle Änderungen zu übernehmen.", + "successMulti_one": "Einstellungen wurden auf {{count}} Kamera kopiert", + "successMulti_other": "Einstellungen wurden auf {{count}} Kameras kopiert", + "successMultiWithRestart_one": "Die Einstellungen wurden auf die Kamera {{count}} kopiert. Starte Frigate neu, um alle Änderungen zu übernehmen.", + "successMultiWithRestart_other": "Die Einstellungen wurden auf {{count}} Kameras kopiert. Starten Sie Frigate neu, um alle Änderungen zu übernehmen.", + "partialFailure": "{{successCount}} Abschnitte wurden angewendet; '{{failedSection}}' ist fehlgeschlagen: {{errorMessage}}", + "partialFailureMulti": "{{successCount}} Kamera(s) wurden kopiert; bei {{failed}} ist ein Fehler aufgetreten: {{errorMessage}}", + "newCameraPartialFailure": "Die Kamera {{cameraName}} wurde erstellt, einige Einstellungen konnten jedoch nicht kopiert werden: {{errorMessage}}", + "sourceMissing": "Die Quellkamera existiert nicht mehr", + "submitError": "Das Klonen der Kamera ist fehlgeschlagen: {{errorMessage}}" + } + } + }, + "cameraReview": { + "title": "Kamera-Einstellungen überprüfen", + "object_descriptions": { + "title": "Generative KI Objektbeschreibungen", + "desc": "Aktiviere/deaktiviere vorübergehend die Objektbeschreibungen durch generative KI für diese Kamera. Wenn diese Option deaktiviert ist, werden keine KI-generierten Beschreibungen für verfolgte Objekte dieser Kamera erstellt." + }, + "review_descriptions": { + "title": "Generative KI Review Beschreibungen", + "desc": "Aktivieren/deaktivieren Sie vorübergehend die generativen KI-Überprüfungsbeschreibungen für diese Kamera, bis Frigate neu gestartet wird. Wenn diese Option deaktiviert ist, werden für Überprüfungselemente auf dieser Kamera keine KI-generierten Beschreibungen angefordert." + }, + "review": { + "title": "Überprüfung", + "desc": "Aktivieren/deaktivieren Sie vorübergehend Warnmeldungen und Erkennungen für diese Kamera, bis Frigate neu gestartet wird. Wenn diese Funktion deaktiviert ist, werden keine neuen Überprüfungselemente generiert. ", + "alerts": "Warnungen ", + "detections": "Erkennungen " + }, + "reviewClassification": { + "title": "Bewertungsklassifizierung", + "desc": "Frigate kategorisiert zu überprüfende Elemente als Warnmeldungen und Erkennungen. Standardmäßig werden alle Objekte vom Typ Person und Auto als Warnmeldungen betrachtet. Sie können die Kategorisierung der zu überprüfenden Elemente verfeinern, indem Sie die erforderlichen Zonen für sie konfigurieren.", + "noDefinedZones": "Für diese Kamera sind keine Zonen definiert.", + "objectAlertsTips": "Alle {{alertsLabels}}-Objekte auf {{cameraName}} werden als Warnmeldungen angezeigt.", + "zoneObjectAlertsTips": "Alle {{alertsLabels}}-Objekte, die in {{zone}} auf {{cameraName}} erkannt wurden, werden als Warnmeldungen angezeigt.", + "objectDetectionsTips": "Alle {{detectionsLabels}}-Objekte, die nicht unter {{cameraName}} kategorisiert sind, werden unabhängig davon, in welcher Zone sie sich befinden, als Erkennungen angezeigt.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-Objekte, die nicht in {{zone}} auf {{cameraName}} kategorisiert sind, werden als Erkennungen angezeigt.", + "notSelectDetections": "Alle {{detectionsLabels}}-Objekte, die in {{zone}} auf {{cameraName}} erkannt und nicht als Warnmeldungen kategorisiert wurden, werden unabhängig davon, in welcher Zone sie sich befinden, als Erkennungen angezeigt.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}}-Objekte, die nicht unter {{cameraName}} kategorisiert sind, werden unabhängig davon, in welcher Zone sie sich befinden, als Erkennungen angezeigt." + }, + "unsavedChanges": "Nicht gespeicherte Überprüfung der Klassifizierungseinstellungen für {{camera}}", + "selectAlertsZones": "Zonen für Warnmeldungen auswählen", + "selectDetectionsZones": "Zonen für Erkennungen auswählen", + "limitDetections": "Erkennungen auf bestimmte Zonen beschränken", + "toast": { + "success": "Die Konfiguration der Bewertungsklassifizierung wurde gespeichert. Starten Sie Frigate neu, um die Änderungen zu übernehmen." + } + } + }, + "saveAllPreview": { + "title": "Änderungen speichern", + "triggerLabel": "Änderungen überprüfen", + "empty": "Keine ausstehenden Änderungen.", + "scope": { + "label": "Umfang", + "global": "Global", + "camera": "kamera: {{cameraName}}" + }, + "field": { + "label": "Feld" + }, + "value": { + "label": "Neuer Wert", + "reset": "Zurücksetzen" + }, + "profile": { + "label": "Profil" + } + }, + "button": { + "overriddenGlobalTooltip": "Diese Kamera überschreibt globale Konfigurationseinstellungen in diesem Abschnitt", + "overriddenBaseConfig": "Überschrieben (Basiskonfiguration)", + "overriddenBaseConfigTooltip": "Das {{profile}}-Profil überschreibt Konfigurationseinstellungen in diesem Abschnitt", + "overriddenGlobal": "Überschrieben (Global)", + "overriddenInCameras": { + "label_one": "In {{count}} Kamera überschrieben", + "label_other": "In {{count}} Kameras überschrieben", + "tooltip_one": "Die Kamera mit der Nummer {{count}} überschreibt die Werte in diesem Abschnitt. Klicken Sie hier, um Details anzuzeigen.", + "tooltip_other": "Die Kamera mit der Nummer {{count}} überschreibt die Werte in diesem Abschnitt. Klicken Sie hier, um Details anzuzeigen.", + "heading_one": "Dieser globale Abschnitt enthält Felder, die in {{count}} Kamera überschrieben werden.", + "heading_other": "Dieser globale Abschnitt enthält Felder, die bei {{count}} Kameras überschrieben werden.", + "othersField_one": "{{count}} andere", + "othersField_other": "{{count}} weitere", + "profilePrefix": "{{profile}} Profile: {{fields}}" + }, + "overriddenGlobalHeading_one": "Diese Kamera überschreibt das Feld {{count}} aus der globalen Konfiguration:", + "overriddenGlobalHeading_other": "Diese Kamera überschreibt alle Felder {{count}} aus der globalen Konfiguration:", + "overriddenGlobalNoDeltas": "Diese Kamera überschreibt die globale Konfiguration, es gibt jedoch keine Abweichungen bei den Feldwerten.", + "overriddenBaseConfigHeading_one": "Das Profil {{profile}} überschreibt das Feld {{count}} aus der Basiskonfiguration:", + "overriddenBaseConfigHeading_other": "Das Profil {{profile}} überschreibt di Felder {{count}} aus der Basiskonfiguration:", + "overriddenBaseConfigNoDeltas": "Das Profil {{profile}} überschreibt diesen Abschnitt, jedoch weichen keine Feldwerte von der Basiskonfiguration ab." + }, + "timestampPosition": { + "tl": "Oben links", + "tr": "Oben rechts", + "bl": "Unten links", + "br": "Unten rechts" + }, + "detectionModel": { + "plusActive": { + "title": "Verwaltung von Frigate+-Modellen", + "label": "Aktuelle Modellquelle", + "description": "Auf diesem Rechner läuft ein Frigate+-Modell. Wählen Sie Ihr Modell in den Frigate+-Einstellungen aus oder ändern Sie es.", + "goToFrigatePlus": "Zu den Frigate+-Einstellungen gehen", + "showModelForm": "Ein Modell manuell konfigurieren" + } + }, + "maintenance": { + "title": "Wartung", + "sync": { + "title": "Medien-Synchronisierung", + "desc": "Frigate bereinigt Medien regelmäßig nach einem festgelegten Zeitplan entsprechend Ihrer Konfiguration zur Aufbewahrungsdauer. Es ist normal, dass während der Ausführung von Frigate einige verwaiste Dateien angezeigt werden. Nutzen Sie diese Funktion, um verwaiste Mediendateien von der Festplatte zu entfernen, auf die in der Datenbank nicht mehr verwiesen wird.", + "started": "Die Mediensynchronisierung wurde gestartet.", + "alreadyRunning": "Ein Synchronisierungsauftrag wird bereits ausgeführt", + "error": "Die Synchronisierung konnte nicht gestartet werden", + "currentStatus": "Status", + "jobId": "Job ID", + "startTime": "Startzeit", + "endTime": "End Zeit", + "statusLabel": "Status", + "results": "Ergebnisse", + "errorLabel": "Fehler", + "mediaTypes": "Medientypen", + "allMedia": "Alle Medien", + "dryRun": "Probelauf", + "dryRunEnabled": "Es werden keine Dateien gelöscht", + "dryRunDisabled": "Die Dateien werden gelöscht", + "force": "Zwingen", + "forceDesc": "Die Sicherheitsschwelle umgehen und die Synchronisierung abschließen, selbst wenn mehr als 50 % der Dateien gelöscht würden.", + "verbose": "Ausführlich", + "verboseDesc": "Erstelle eine vollständige Liste der verwaisten Dateien auf der Festplatte zur Überprüfung.", + "running": "Synchronisierung läuft...", + "start": "Synchronisierung starten", + "inProgress": "Die Synchronisierung läuft. Diese Seite ist deaktiviert.", + "status": { + "queued": "In der Warteschlange", + "running": "läuft", + "completed": "Abgeschlossen", + "failed": "Fehlgeschlagen", + "notRunning": "läuft nicht" + }, + "resultsFields": { + "filesChecked": "Datein geprüft", + "orphansFound": "Datenleiche gefunden", + "orphansDeleted": "Datenleiche gelöscht", + "aborted": "Abgebrochen. Die Löschung würde den Sicherheitsgrenzwert überschreiten.", + "error": "Fehler", + "totals": "Total" + }, + "event_snapshots": "Momentaufnahmen von verfolgten Objekten", + "event_thumbnails": "Miniaturansichten der verfolgten Objekte", + "review_thumbnails": "Vorschau-Miniaturansichten", + "previews": "Vorschau", + "exports": "Exporte", + "recordings": "Aufnahmen" + }, + "regionGrid": { + "title": "Regionraster", + "desc": "Das Erfassungsraster ist ein optimiertes Modell, das lernt, wo Objekte unterschiedlicher Größe typischerweise im Sichtfeld der einzelnen Kameras auftreten. Frigate nutzt diese Daten, um die Größe der Erfassungsbereiche effizient anzupassen. Das Raster wird im Laufe der Zeit automatisch aus den Daten der verfolgten Objekte erstellt.", + "clear": "Regionsraster löschen", + "clearConfirmTitle": "Raster der Region löschen", + "clearConfirmDesc": "Es wird nicht empfohlen, das Regionsraster zu löschen, es sei denn, Sie haben kürzlich die Größe Ihres Detektormodells geändert oder die physische Position Ihrer Kamera angepasst und haben Probleme bei der Objektverfolgung. Das Raster wird im Laufe der Zeit automatisch neu aufgebaut, sobald Objekte verfolgt werden. Damit die Änderungen wirksam werden, ist ein Neustart von Frigate erforderlich.", + "clearSuccess": "Das Regionsraster wurde erfolgreich gelöscht", + "clearError": "Das Löschen des Regionsrasters ist fehlgeschlagen", + "restartRequired": "Ein Neustart ist erforderlich, damit die Änderungen am regionalen Netz wirksam werden" + } + }, + "configForm": { + "global": { + "title": "Globale Einstellung", + "description": "Diese Einstellungen gelten für alle Kameras, sofern sie nicht in den kameraspezifischen Einstellungen überschrieben werden." + }, + "camera": { + "title": "Kamera Einstellung", + "description": "Diese Einstellungen gelten nur für diese Kamera und haben Vorrang vor den allgemeinen Einstellungen.", + "noCameras": "Keine Kameras verfügbar" + }, + "advancedSettingsCount": "Erweiterte Einstellungen ({{count}})", + "advancedCount": "Fortgeschritten ({{count}})", + "showAdvanced": "Erweiterte Einstellungen anzeigen", + "tabs": { + "sharedDefaults": "Gemeinsame Standardeinstellungen", + "system": "System", + "integrations": "Integrationen" + }, + "additionalProperties": { + "keyLabel": "Schlüssel", + "valueLabel": "Wert", + "keyPlaceholder": "Neuer Schlüssel", + "remove": "Entfernen" + }, + "timezone": { + "defaultOption": "Zeitzone des Browsers verwenden" + }, + "roleMap": { + "empty": "Keine Rollenzuordnungen", + "roleLabel": "Rolle", + "groupsLabel": "Gruppe", + "addMapping": "Rollenzuordnung hinzufügen", + "remove": "Entfernen" + }, + "ffmpegArgs": { + "preset": "Voreinstellung", + "manual": "Manuelle Argumente", + "inherit": "Von den Kameraeinstellungen übernehmen", + "none": "Keine", + "useGlobalSetting": "Von der globalen Einstellung übernehmen", + "selectPreset": "Voreinstellung auswählen", + "manualPlaceholder": "FFmpeg-Argumente eingeben", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (Generic)", + "preset-http-mjpeg-generic": "HTTP MJPEG (Generic)", + "preset-http-reolink": "HTTP - Reolink Cameras", + "preset-rtmp-generic": "RTMP (Generic)", + "preset-rtsp-generic": "RTSP (Generic)", + "preset-rtsp-restream": "RTSP - Restream von go2rtc", + "preset-rtsp-restream-low-latency": "FFmpeg-Argumente eingeben: RTSP – Neustreaming von go2rtc (geringe Latenz)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Aufnahme (allgemein, ohne Ton)", + "preset-record-generic-audio-copy": "Aufnahme (Allgemein + Audio kopieren)", + "preset-record-generic-audio-aac": "Aufnahme (Allgemein + Audio in AAC)", + "preset-record-mjpeg": "Aufzeichnung – MJPEG-Kameras", + "preset-record-jpeg": "Aufnahme – JPEG-Kameras", + "preset-record-ubiquiti": "Aufzeichnung – Ubiquiti-Kameras" + } + }, + "cameraInputs": { + "itemTitle": "Stream {{index}}", + "sourceMode": { + "restream": "Restream (go2rtc)", + "manual": "Pfad für die manuelle Eingabe", + "go2rtcStreamLabel": "go2rtc stream", + "go2rtcStreamPlaceholder": "Wählen Sie einen go2rtc-Stream aus", + "noGo2rtcStreams": "Es sind keine go2rtc-Streams konfiguriert", + "go2rtcStreamSearch": "Suche Streams...", + "availableStreams": "Verfügbare Streams", + "noMatchingStreams": "Keine passenden Streams" + } + }, + "restartRequiredField": "Neustart erforderlich", + "restartRequiredFooter": "Konfiguration geändert – Neustart erforderlich", + "sections": { + "detect": "Erkennung", + "record": "Aufnahme", + "snapshots": "Schnappschüsse", + "motion": "Antrag", + "objects": "Objekte", + "review": "überprüfen", + "audio": "Audio", + "notifications": "Benachrichtigungen", + "live": "Live Ansicht", + "timestamp_style": "Zeitstempel", + "mqtt": "MQTT", + "database": "Datenbank", + "telemetry": "Telemetrie", + "auth": "Authentifizierung", + "tls": "TLS", + "proxy": "Proxy", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg", + "detectors": "Detektoren", + "model": "Modell", + "semantic_search": "Semantische Suche", + "genai": "GenAI", + "face_recognition": "Gesichtserkennung", + "lpr": "Kennzeichenerkennung", + "birdseye": "Birdseye", + "masksAndZones": "Masken / Zonen" + }, + "detect": { + "title": "Erkennungseinstellungen" + }, + "detectors": { + "title": "Erkennungseinstellungen", + "singleType": "Es ist nur ein {{type}}-Detektor zulässig.", + "keyRequired": "Der Name des Detektors ist erforderlich.", + "keyDuplicate": "Der Name des Detektors ist bereits vorhanden.", + "noSchema": "Es sind keine Detektorschemata verfügbar.", + "none": "Es sind keine Detektorinstanzen konfiguriert.", + "add": "Detektor hinzufügen", + "addCustomKey": "Benutzter Schlüssel hinzufügen" + }, + "record": { + "title": "Aufnahmeeinstellungen" + }, + "snapshots": { + "title": "Einstellungen für Momentaufnahmen" + }, + "motion": { + "title": "Bewegungseinstellungen" + }, + "objects": { + "title": "Objekteinstellungen" + }, + "audioLabels": { + "summary": "{{count}} Audio-Labels ausgewählt", + "empty": "Es sind keine Audio-Bezeichnungen verfügbar" + }, + "objectLabels": { + "summary": "{{count}} Objekttypen ausgewählt", + "empty": "Es sind keine Objektbeschriftungen verfügbar" + }, + "reviewLabels": { + "summary": "{{count}} Etiketten ausgewählt", + "empty": "Keine Beschriftungen verfügbar", + "allNonAlertDetections": "Alle Aktivitäten, die keine Warnmeldungen auslösen, werden als Erkennungen erfasst." + }, + "filters": { + "objectFieldLabel": "{{field}} für {{label}}" + }, + "zoneNames": { + "summary": "{{count}} ausgewählt", + "empty": "Keine Zonen verfügbar" + }, + "inputRoles": { + "summary": "{{count}} Rollen ausgewählt", + "empty": "Es sind keine Rollen verfügbar", + "options": { + "detect": "Erkennen", + "record": "Aufnahme", + "audio": "Audio" + } + }, + "genaiRoles": { + "options": { + "embeddings": "Einbetten", + "vision": "Vision", + "tools": "Werkzeuge", + "descriptions": "Beschreibung", + "chat": "Chat" + } + }, + "semanticSearchModel": { + "placeholder": "Modell auswählen…", + "builtIn": "Vorbereitete Modelle", + "genaiProviders": "GenAI Anbieter" + }, + "review": { + "title": "Einstellungen überprüfen" + }, + "audio": { + "title": "Audioeinstellungen" + }, + "notifications": { + "title": "Benachrichtigungseinstellungen" + }, + "live": { + "title": "Einstellungen für die Live-Ansicht" + }, + "timestamp_style": { + "title": "Einstellungen für Zeitstempel" + }, + "searchPlaceholder": "Suche...", + "addCustomLabel": "Benutzerdefiniertes Etikett hinzufügen...", + "knownPlates": { + "namePlaceholder": "z.B. das Auto der Frau", + "platePlaceholder": "Kennzeichen oder regulärer Ausdruck" + }, + "genaiModel": { + "placeholder": "Modell auswählen oder eingeben…", + "search": "Modell suchen oder eingeben…", + "noModels": "Keine Modelle verfügbar", + "available": "Verfügbare Modelle", + "useCustom": "Verwende „{{value}}“", + "refresh": "Modelle aktualisieren", + "probeFailed": "Das Abrufen der Modelle ist fehlgeschlagen", + "fetchedModels": "Modellliste erfolgreich abgerufen" + }, + "semanticSearchModelSize": { + "notApplicable": "Gilt nicht für GenAI-Anbieter" + }, + "liveStreams": { + "streamNameLabel": "Streamname", + "streamNamePlaceholder": "z. B. Haupt-HD-Stream", + "go2rtcStreamLabel": "go2rtc stream", + "go2rtcStreamPlaceholder": "Wählen Sie einen go2rtc-Stream aus", + "go2rtcStreamSearch": "Suchen Sie nach einem Streamnamen oder geben Sie ihn ein…", + "noGo2rtcStreams": "Es sind keine go2rtc-Streams konfiguriert", + "availableStreams": "Verfügbare Streams", + "useCustom": "Verwende „{{value}}“", + "addStream": "Stream hinzufügen" + }, + "ptzPresets": { + "placeholder": "Wählen Sie eine Voreinstellung aus oder geben Sie eine ein...", + "search": "Suchen oder eine Voreinstellung eingeben...", + "noPresets": "Es sind keine Voreinstellungen verfügbar", + "available": "Kamera-Voreinstellungen", + "useCustom": "Verwende „{{value}}“" + }, + "defaultRole": { + "admin": "Admin", + "viewer": "Betrachter" + } + }, + "globalConfig": { + "title": "Globale Konfiguration", + "description": "Konfigurieren Sie globale Einstellungen, die für alle Kameras gelten, sofern sie nicht überschrieben werden.", + "toast": { + "success": "Die globalen Einstellungen wurden erfolgreich gespeichert", + "error": "Das Speichern der globalen Einstellungen ist fehlgeschlagen", + "validationError": "Validierung fehlgeschlagen" + } + }, + "cameraConfig": { + "title": "Kamerakonfiguration", + "description": "Konfigurieren Sie die Einstellungen für einzelne Kameras. Diese Einstellungen haben Vorrang vor den globalen Standardeinstellungen.", + "overriddenBadge": "Überschrieben", + "resetToGlobal": "Auf globale Einstellungen zurücksetzen", + "toast": { + "success": "Die Kameraeinstellungen wurden erfolgreich gespeichert", + "error": "Das Speichern der Kameraeinstellungen ist fehlgeschlagen" + } + }, + "toast": { + "success": "Einstellungen erfolgreich gespeichert", + "applied": "Einstellungen wurden erfolgreich übernommen", + "successRestartRequired": "Die Einstellungen wurden erfolgreich gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen.", + "error": "Das Speichern der Einstellungen ist fehlgeschlagen", + "validationError": "Validierung fehlgeschlagen: {{message}}", + "resetSuccess": "Auf globale Standardeinstellungen zurücksetzen", + "resetError": "Das Zurücksetzen der Einstellungen ist fehlgeschlagen", + "saveAllSuccess_one": "Der Abschnitt {{count}} wurde erfolgreich gespeichert.", + "saveAllSuccess_other": "Alle {{count}} Abschnitte wurden erfolgreich gespeichert.", + "saveAllPartial_one": "{{successCount}} von {{totalCount}} Abschnitt wurden gespeichert. {{failCount}} sind fehlgeschlagen.", + "saveAllPartial_other": "{{successCount}} von {{totalCount}} Abschnitten wurden gespeichert. {{failCount}} sind fehlgeschlagen.", + "saveAllFailure": "Es konnten nicht alle Abschnitte gespeichert werden.", + "saveAllSuccessRestartRequired_one": "Der Abschnitt {{count}} wurde erfolgreich gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen.", + "saveAllSuccessRestartRequired_other": "Alle {{count}} Abschnitte wurden erfolgreich gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen." + }, + "profiles": { + "title": "Profile", + "activeProfile": "Aktive Profile", + "noActiveProfile": "Kein aktives Profil", + "active": "Aktiv", + "activated": "Profil „{{profile}}“ aktiviert", + "activateFailed": "Das Profil konnte nicht eingerichtet werden", + "deactivated": "Profil deaktiviert", + "noProfiles": "Es sind keine Profile definiert.", + "noOverrides": "Keine Überschreibungen", + "cameraCount_one": "{{count}} Kamera", + "cameraCount_other": "{{count}} Kameras", + "columnCamera": "Kamera", + "columnOverrides": "Profilüberschreibungen", + "baseConfig": "Basis Konfiguration", + "addProfile": "Profil hinzufügen", + "newProfile": "Neues Profil", + "profileNamePlaceholder": "z. B. „Scharf“, „Abwesend“, „Nachtmodus“", + "friendlyNameLabel": "Profilname", + "profileIdLabel": "Profile-ID", + "profileIdDescription": "Interne Kennung, die in der Konfiguration und in Automatisierungen verwendet wird", + "nameInvalid": "Es sind nur Kleinbuchstaben, Zahlen und Unterstriche zulässig", + "nameDuplicate": "Ein Profil mit diesem Namen existiert bereits", + "error": { + "mustBeAtLeastTwoCharacters": "Muss mindestens 2 Zeichen lang sein", + "mustNotContainPeriod": "Darf keine Punkte enthalten", + "alreadyExists": "Ein Profil mit dieser ID existiert bereits" + }, + "renameProfile": "Profil umbenennen", + "renameSuccess": "Profil in „{{profile}}“ umbenannt", + "deleteProfile": "Profil löschen", + "deleteProfileConfirm": "Profil „{{profile}}“ von allen Kameras löschen? Dieser Vorgang kann nicht rückgängig gemacht werden.", + "deleteSuccess": "Profil „{{profile}}“ gelöscht", + "createSuccess": "Profil „{{profile}}“ erstellt", + "removeOverride": "Profil-Überschreibung aufheben", + "deleteSection": "Abschnittsüberschreibungen löschen", + "deleteSectionConfirm": "Die Überschreibungen von {{section}} für das Profil {{profile}} auf {{camera}} entfernen?", + "deleteSectionSuccess": "Die Überschreibungen von {{section}} für {{profile}} wurden entfernt", + "enableSwitch": "Profile aktivieren", + "enabledDescription": "Profile sind aktiviert. Erstellen Sie unten ein neues Profil, navigieren Sie zum Abschnitt „Kamera-Konfiguration“, um Ihre Änderungen vorzunehmen, und speichern Sie diese, damit sie wirksam werden.", + "disabledDescription": "Mit Profilen können Sie benannte Gruppen von Kamera-Konfigurationsänderungen (z. B. „aktiviert“, „abwesend“, „Nacht“) definieren, die bei Bedarf aktiviert werden können." + }, + "unsavedChanges": "Sie haben noch nicht gespeicherte Änderungen", + "confirmReset": "Zurücksetzen bestätigen", + "resetToDefaultDescription": "Dadurch werden alle Einstellungen in diesem Abschnitt auf ihre Standardwerte zurückgesetzt. Dieser Vorgang kann nicht rückgängig gemacht werden.", + "resetToGlobalDescription": "Dadurch werden die Einstellungen in diesem Abschnitt auf die globalen Standardwerte zurückgesetzt. Dieser Vorgang kann nicht rückgängig gemacht werden.", + "go2rtcStreams": { + "title": "go2rtc-Streams", + "description": "Verwalten Sie die go2rtc-Stream-Konfigurationen für das Restreaming von Kamerabildern. Jeder Stream verfügt über einen Namen und eine oder mehrere Quell-URLs.", + "addStream": "Stream hinzufügen", + "addStreamDesc": "Geben Sie einen Namen für den neuen Stream ein. Dieser Name wird verwendet, um in Ihrer Kamerakonfiguration auf den Stream zu verweisen.", + "addUrl": "URL hinzufügen", + "streamName": "Stream-Name", + "streamNamePlaceholder": "z.B., Vordertür", + "streamUrlPlaceholder": "z.B., rtsp://user:pass@192.168.1.100/stream", + "deleteStream": "Stream löschen", + "deleteStreamConfirm": "Möchten Sie den Stream „{{streamName}}“ wirklich löschen? Kameras, die auf diesen Stream verweisen, funktionieren möglicherweise nicht mehr.", + "noStreams": "Es sind keine go2rtc-Streams konfiguriert. Füge einen Stream hinzu, um loszulegen.", + "validation": { + "nameRequired": "Der Name des Streams ist erforderlich", + "nameDuplicate": "Ein Stream mit diesem Namen existiert bereits", + "nameInvalid": "Der Name des Streams darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten", + "urlRequired": "Es ist mindestens eine URL erforderlich" + }, + "renameStream": "Stream umbenennen", + "renameStreamDesc": "Geben Sie einen neuen Namen für diesen Stream ein. Das Umbenennen eines Streams kann dazu führen, dass Kameras oder andere Streams, die namentlich darauf verweisen, nicht mehr funktionieren.", + "newStreamName": "Neuer Stream-Name", + "ffmpeg": { + "useFfmpegModule": "Kompatibilitätsmodus verwenden (ffmpeg)", + "video": "Video", + "audio": "Audio", + "hardware": "Hardwarebeschleunigung", + "videoCopy": "Kopieren", + "videoH264": "Transcode zu H.264", + "videoH265": "Transcode zu H.265", + "videoExclude": "Ausschließen", + "audioCopy": "Kopieren", + "audioAac": "Transcode zu AAC", + "audioOpus": "Transcode zu Opus", + "audioPcmu": "Transcode zu PCM μ-law", + "audioPcma": "Transcode zu PCM A-law", + "audioPcm": "Transcode zu PCM", + "audioMp3": "Transcode zu MP3", + "audioExclude": "Ausschließen", + "hardwareNone": "Keine Hardwarebeschleunigung", + "hardwareAuto": "Automatisch (empfohlen)", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "Videocodec hinzufügen", + "addAudioCodec": "Audio-Codec hinzufügen", + "removeCodec": "Codec entfernen" + }, + "streamNumber": "Stream {{index}}", + "sourceNumber": "Quelle {{index}}" + }, + "onvif": { + "profileAuto": "Auto", + "profileLoading": "Profile werden geladen...", + "autotracking": { + "zooming": { + "disabled": "deaktiviert", + "absolute": "Absolut", + "relative": "Verwandter" + } + } + }, + "configMessages": { + "review": { + "recordDisabled": "Aufnahme ist deaktiviert, Überprüfungspunkt konnte nicht erstellt werden.", + "detectDisabled": "Die Objekterkennung ist deaktiviert. Für die Überprüfung von Elementen müssen Objekte erkannt werden, um Warnmeldungen und Erkennungen zu kategorisieren.", + "allNonAlertDetections": "Alle Aktivitäten, die keine Warnmeldungen auslösen, werden als Erkennungen erfasst.", + "genaiImageSourceRecordingsRecordDisabled": "Als Bildquelle ist „Aufnahmen“ eingestellt, die Aufnahmefunktion ist jedoch deaktiviert. Frigate greift in diesem Fall auf Vorschaubilder zurück." + }, + "audio": { + "noAudioRole": "Für keinen Stream ist die Audio-Rolle definiert. Sie müssen die Audio-Rolle aktivieren, damit die Audioerkennung funktioniert." + }, + "audioTranscription": { + "audioDetectionDisabled": "Die Audioerkennung ist für diese Kamera nicht aktiviert. Für die Audio-Transkription muss die Audioerkennung aktiviert sein." + }, + "detect": { + "fpsGreaterThanFive": "Es wird nicht empfohlen, den Wert für die FPS-Erkennung auf mehr als 5 zu setzen. Höhere Werte können zu Leistungseinbußen führen und bieten keinerlei Vorteile.", + "disabled": "Die Objekterkennung ist deaktiviert. Momentaufnahmen, Überprüfungselemente und Erweiterungsfunktionen wie Gesichtserkennung, Kennzeichenerkennung und generative KI funktionieren nicht.", + "resolutionShouldBeMultipleOfFour": "Um optimale Ergebnisse zu erzielen, sollten Breite und Höhe ein Vielfaches von 4 sein. Andere gerade Werte können zu visuellen Artefakten oder leichten Verzerrungen im Erkennungsstrom führen.", + "aspectRatioMismatch": "Die von Ihnen eingegebene Breite und Höhe stimmen nicht mit dem Seitenverhältnis Ihrer aktuell erkannten Auflösung überein. Dies kann zu einem gestreckten oder verzerrten Bild führen.", + "maxFramesSet": "Die Festlegung einer maximalen Bildrate überschreibt das Standardverhalten und deaktiviert die Verfolgung stationärer Objekte. Dies ist nur in sehr wenigen Fällen erforderlich; verwenden Sie diese Option daher mit Bedacht.", + "squareResolution": "Eine quadratische Erkennungsauflösung ist ungewöhnlich. Die Erkennungsbreite und -höhe sollten dem Seitenverhältnis Ihrer Kamera entsprechen (zum Beispiel 16:9) und nicht den Abmessungen des Objekterkennungsmodells. Ein nicht übereinstimmendes Seitenverhältnis kann das Bild verzerren und die Erkennungsgenauigkeit beeinträchtigen.", + "resolutionHigh": "Diese Erkennungsauflösung liegt über der empfohlenen Wert und kann zu einem erhöhten Ressourcenverbrauch führen, ohne die Erkennungsgenauigkeit zu verbessern. Für die meisten Kameras wird eine Erkennungsauflösung von maximal 1080p empfohlen.", + "globalResolutionMultipleCameras": "Bei der Konfiguration mehrerer Kameras wird eine globale Erkennungsauflösung festgelegt. Sofern nicht alle Kameras dieselbe Auflösung und dasselbe Seitenverhältnis aufweisen, sollten die Erkennungsbreite und -höhe für jede Kamera separat festgelegt werden, um dem nativen Seitenverhältnis der jeweiligen Kamera zu entsprechen." + }, + "faceRecognition": { + "globalDisabled": "Die Gesichtserkennungserweiterung muss aktiviert sein, damit die Gesichtserkennungsfunktionen bei dieser Kamera funktionieren.", + "personNotTracked": "Für die Gesichtserkennung muss das Objekt „Person“ verfolgt werden. Aktivieren Sie „Person“ unter „Objekte“ für diese Kamera.", + "modelSizeLarge": "Das „große“ Modell erfordert eine GPU oder NPU, um eine angemessene Leistung zu erzielen. Verwenden Sie auf reinen CPU-Systemen die Option „klein“." + }, + "lpr": { + "globalDisabled": "Die Erweiterung zur Kennzeichenerkennung muss aktiviert sein, damit die LPR-Funktionen bei dieser Kamera funktionieren.", + "vehicleNotTracked": "Für die Kennzeichenerkennung muss entweder „Auto“ oder „Motorrad“ erfasst werden. Aktivieren Sie „Auto“ oder „Motorrad“ unter „Objekte“ für diese Kamera.", + "modelSizeLarge": "Das „große“ Modell ist für mehrzeilige Kennzeichen optimiert. Das „kleine“ Modell bietet eine bessere Leistung als das „große“ und sollte verwendet werden, sofern in Ihrer Region keine mehrzeiligen Kennzeichenformate verwendet werden." + }, + "record": { + "noRecordRole": "Für keinen Stream ist die Rolle „Record“ definiert. Die Aufzeichnung funktioniert nicht." + }, + "birdseye": { + "objectsModeDetectDisabled": "Birdseye ist auf den Modus „Objekte“ eingestellt, doch die Objekterkennung ist für diese Kamera deaktiviert. Die Kamera wird in Birdseye nicht angezeigt." + }, + "snapshots": { + "detectDisabled": "Die Objekterkennung ist deaktiviert. Es werden keine Momentaufnahmen von verfolgten Objekten erstellt." + }, + "detectors": { + "mixedTypes": "Alle Detektoren müssen von gleichem Typ sein, Entferne bestehende Detektoren um einen anderen Typ zu benutzen.", + "mixedTypesSuggestion": "Alle Detektoren müssen vom gleichem Typ sein. Entferne bestehende oder wähle {{type}}." + }, + "objects": { + "genaiNoDescriptionsProvider": "Sie müssen einen GenAI-Anbieter mit der Rolle „Beschreibung“ konfigurieren, damit Beschreibungen generiert werden können." + }, + "semanticSearch": { + "jinav2SmallModelSize": "Die „kleine“ Variante des Jina V2-Modells verursacht hohe RAM- und Inferenzkosten. Es wird das „große“ Modell mit einer dedizierten GPU empfohlen." + }, + "onvif": { + "autotrackingNoZones": "Für die automatische Verfolgung ist mindestens eine Zone erforderlich. Definieren Sie unter „Masken / Zonen“ eine Zone für diese Kamera und legen Sie diese anschließend unten als erforderliche Zone fest." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "Explizite Definitionen der Hardware-beschleunigungs Variablen sind nicht empfohlen. Wähle die Voreinstellung die zu deiner Hardware passt, außer wenn spezifische Anforderungen eine andere Konfiguration erfordern." + }, + "model": { + "optimizedFor320": "Frigate ist für ein 320x320-Modell optimiert, was für die meisten Konfigurationen die beste Wahl ist. Ein 640x640-Modell ist langsamer und bietet nur in bestimmten Szenarien Vorteile.", + "inputDimensionsNotDetectResolution": "Die Eingangsbreite und -höhe des Modells beziehen sich auf die Abmessungen des Objekterkennungsmodells und nicht auf die Erkennungsauflösung Ihrer Kamera. Sie sollten mit den Abmessungen des von Ihnen verwendeten Modells übereinstimmen – in der Regel quadratische Abmessungen wie 320×320 oder 640×640." + } + }, + "birdseye": { + "trackingMode": { + "objects": "Objekte", + "motion": "Bewegung", + "continuous": "Fortlaufend" + }, + "cameraOrder": { + "label": "Kamerabestellung", + "description": "Ziehe die Kameras per Drag & Drop, um ihre Reihenfolge im Birdseye-Layout festzulegen.", + "reorderHandle": "Zum Neuanordnen ziehen", + "saving": "Wird gespeichert…", + "saved": "gespeichert" + } + }, + "retainMode": { + "all": "Alle", + "motion": "Bewegung", + "active_objects": "Aktive Objekte" + }, + "previewQuality": { + "very_high": "sehr hoch", + "high": "hoch", + "medium": "Mittel", + "low": "niedrig", + "very_low": "sehr niedrig" + }, + "ui": { + "timeFormat": { + "browser": "Browser", + "12hour": "12 Stunden", + "24hour": "24 Stunden" + }, + "TimeOrDateStyle": { + "full": "vollständig", + "long": "lang", + "medium": "mittel", + "short": "kurz" + }, + "unitSystem": { + "metric": "Metrik", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Aufnahmen", + "previews": "Vorschau" + } + }, + "logger": { + "logLevel": { + "debug": "Debug", + "info": "Info", + "warning": "Warnung", + "error": "Fehler", + "critical": "Kritisch" + } + }, + "modelSize": { + "small": "klein", + "large": "groß" + }, + "menuDot": { + "overrideGlobal": "Dieser Abschnitt überschreibt die globale Konfiguration", + "overrideProfile": "Dieser Abschnitt wird durch das Profil {{profile}} überschrieben", + "unsaved": "Dieser Abschnitt enthält ungespeicherte Änderungen" + }, + "detectorsAndModel": { + "title": "Detektoren und Modell", + "description": "Konfigurieren Sie das Detektor-Backend, das die Objekterkennung ausführt, sowie das dafür verwendete Modell. Änderungen werden gemeinsam gespeichert, sodass Detektor und Modell synchron bleiben.", + "cardTitles": { + "detector": "Detektor-Hardware", + "model": "Erkennungsmodell" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Benutzerdefiniertes Modell" + }, + "mismatch": { + "warning": "Das aktuelle Frigate+-Modell „{{model}}“ erfordert den {{required}}-Detektor. Wählen Sie unten ein kompatibles Modell aus oder wechseln Sie vor dem Speichern zu „Benutzerdefiniertes Modell“." + }, + "plusModel": { + "requiresDetector": "Voraussetzung: {{detector}}", + "noModelSelected": "Wählen Sie ein Modell der Frigate+ aus" + }, + "toast": { + "saveSuccess": "Detektoren und Modelleinstellungen wurden gespeichert. Starten Sie Frigate neu, um die Änderungen zu übernehmen.", + "saveError": "Das Speichern der Detektor- und Modelleinstellungen ist fehlgeschlagen" + }, + "unsavedChanges": "Nicht gespeicherte Änderungen an Detektor und Modell", + "restartRequired": "Neustart erforderlich (Detektor oder Modell geändert)" + } +} diff --git a/web/public/locales/de/views/system.json b/web/public/locales/de/views/system.json new file mode 100644 index 0000000..567c6ae --- /dev/null +++ b/web/public/locales/de/views/system.json @@ -0,0 +1,275 @@ +{ + "general": { + "hardwareInfo": { + "gpuInfo": { + "vainfoOutput": { + "title": "Ergebnis der Vainfo-Abfrage", + "returnCode": "Rückgabecode: {{code}}", + "processError": "Prozess Fehler:", + "processOutput": "Prozess-Output:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI Ausgabe", + "cudaComputerCapability": "CUDA Rechenleistung:{{cuda_compute}}", + "name": "Name: {{name}}", + "driver": "Treiber: {{driver}}", + "vbios": "VBios Info: {{vbios}}" + }, + "closeInfo": { + "label": "Schließe GPU Info" + }, + "copyInfo": { + "label": "Kopiere GPU Info" + }, + "toast": { + "success": "GPU-Infos in die Zwischenablage kopiert" + } + }, + "title": "Hardwareinformationen", + "gpuUsage": "GPU Auslastung", + "gpuMemory": "Grafikspeicher", + "gpuDecoder": "GPU Decoder", + "gpuEncoder": "GPU Encoder", + "npuUsage": "NPU Verwendung", + "npuMemory": "NPU Speicher", + "intelGpuWarning": { + "title": "Intel GPU Statistik Warnung", + "message": "GPU stats nicht verfügbar", + "description": "Dies ist ein bekannter Fehler in den GPU-Statistik-Tools von Intel (intel_gpu_top), bei dem das Tool ausfällt und wiederholt eine GPU-Auslastung von 0 % anzeigt, selbst wenn die Hardwarebeschleunigung und die Objekterkennung auf der (i)GPU korrekt funktionieren. Dies ist kein Fehler von Frigate. Du kannst den Host neu starten, um das Problem vorübergehend zu beheben und zu prüfen, ob die GPU korrekt funktioniert. Dies hat keine Auswirkungen auf die Leistung." + }, + "gpuTemperature": "GPU Temperatur", + "npuTemperature": "NPU Temperatur", + "gpuCompute": "GPU Compute / Encode" + }, + "title": "Allgemein", + "detector": { + "title": "Detektoren", + "cpuUsage": "CPU-Auslastung des Detektors", + "memoryUsage": "Arbeitsspeichernutzung des Detektors", + "inferenceSpeed": "Detektoren Inferenzgeschwindigkeit", + "temperature": "Temperatur des Detektors", + "cpuUsageInformation": "CPU, die zur Vorbereitung von Eingabe- und Ausgabedaten für/aus Erkennungsmodellen verwendet wird. Dieser Wert misst nicht die Inferenzauslastung, selbst wenn eine GPU oder ein Beschleuniger verwendet wird." + }, + "otherProcesses": { + "title": "Andere Prozesse", + "processCpuUsage": "CPU Auslastung für Prozess", + "processMemoryUsage": "Prozessspeicherauslastung", + "series": { + "go2rtc": "go2rtc", + "recording": "Aufnahme", + "audio_detector": "Geräuscherkennung", + "review_segment": "Überprüfungsteil", + "embeddings": "Einbetten" + } + } + }, + "documentTitle": { + "cameras": "Kamerastatistiken – Frigate", + "storage": "Speicherstatistiken - Frigate", + "general": "Allgemeine Statistiken - Frigate", + "logs": { + "frigate": "Frigate Protokolle – Frigate", + "go2rtc": "Go2RTC Protokolle - Frigate", + "nginx": "Nginx Protokolle - Frigate", + "websocket": "Nachrichten Protokolle - Frigate" + }, + "enrichments": "Erweiterte Statistiken - Frigate" + }, + "title": "System", + "logs": { + "download": { + "label": "Protokolldateien herunterladen" + }, + "copy": { + "success": "Protokolle in die Zwischenablage kopiert", + "label": "In die Zwischenablage kopieren", + "error": "Protokolle konnten nicht in die Zwischenablage kopiert werden" + }, + "type": { + "message": "Nachricht", + "timestamp": "Zeitstempel", + "label": "Art", + "tag": "Tag" + }, + "toast": { + "error": { + "fetchingLogsFailed": "Fehler beim Abrufen der Protokolle: {{errorMessage}}", + "whileStreamingLogs": "Beim Übertragen der Protokolle ist ein Fehler aufgetreten: {{errorMessage}}" + } + }, + "tips": "Protokolle werden in Echtzeit vom Server übertragen", + "websocket": { + "label": "Nachrichten", + "pause": "Pause", + "clear": "reinigen", + "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": "Nutzinhalt" + }, + "resume": "fortsetzen", + "count_one": "{{count}} Nachrichten", + "count_other": "{{count}} Nachrichten" + } + }, + "metrics": "Systemmetriken", + "storage": { + "recordings": { + "earliestRecording": "Älteste verfügbare Aufzeichnung:", + "title": "Aufnahmen", + "tips": "Dieser Wert gibt den Gesamtspeicherplatz an, den die Aufzeichnungen in der Datenbank von Frigate belegen. Frigate erfasst nicht die Speichernutzung für alle Dateien auf Ihrer Festplatte." + }, + "cameraStorage": { + "camera": "Kamera", + "title": "Kamera Speicher", + "unused": { + "title": "Ungenutzt", + "tips": "Dieser Wert gibt möglicherweise nicht genau den freien Speicherplatz an, der Frigate zur Verfügung steht, wenn neben den Aufzeichnungen von Frigate noch andere Dateien auf der Festplatte gespeichert sind. Frigate verfolgt die Speichernutzung außerhalb der Aufzeichnungen nicht." + }, + "unusedStorageInformation": "Info zum ungenutzten Speicher", + "storageUsed": "Speicher", + "percentageOfTotalUsed": "Prozentualer Anteil am Gesamtanteil", + "bandwidth": "Bandbreite" + }, + "title": "Speicher", + "overview": "Übersicht", + "shm": { + "title": "SHM (Shared Memory) Zuweisung", + "warning": "Die aktuelle SHM-Größe von {{total}} MB ist zu klein. Erhöhe sie auf mindestens {{min_shm}} MB.", + "frameLifetime": { + "title": "Frame Lebenszeit", + "description": "Jede Kamera verfügt über {{frames}} Bildspeicherplätze im gemeinsamen Speicher. Bei der höchsten Bildrate der Kamera steht jedes Bild etwa {{lifetime}} Sekunden lang zur Verfügung, bevor es überschrieben wird." + } + } + }, + "cameras": { + "info": { + "stream": "Stream {{idx}}", + "video": "Video:", + "codec": "Codec:", + "fetching": "Lade Kamera Daten", + "resolution": "Auflösung:", + "fps": "FPS:", + "unknown": "Unbekannt", + "audio": "Audio:", + "error": "Fehler: {{error}}", + "cameraProbeInfo": "{{camera}} Kamera-Untersuchungsinfo", + "streamDataFromFFPROBE": "Stream-Daten werden mit ffprobe erhalten.", + "tips": { + "title": "Kamera-Untersuchsungsinfo" + }, + "aspectRatio": "Seitenverhältnis", + "keyframes": { + "title": "Keyframe-Analyse", + "analyzing": "Keyframes werden analysiert... Noch {{seconds}} Sekunden", + "stillAnalyzing": "Keyframes werden noch analysiert...", + "recordStream": "Stream aufzeichnen:", + "keyframeCount": "Beobachtete Keyframes:", + "observedDuration": "Beobachtete Dauer:", + "gap": "Keyframe-Abstand (min. / durchschnittlich / max.):", + "segmentLength": "Länge des Aufzeichnungssegments:", + "ok": "Keyframes alle ~{{seconds}}s, gut geeignet für Aufzeichnung und Wiedergabe.", + "warning": "Seltene oder unregelmäßige Keyframes (längste Lücke ~{{seconds}}s), wahrscheinlich ein „Smart“-Codec (H.264+/H.265+); dies wird nicht empfohlen.", + "error": "Die Lücke zwischen den Keyframes (~{{seconds}}s) überschreitet die Länge des Aufzeichnungssegments ({{segmentTime}}s). Einige Segmente enthalten möglicherweise keinen Keyframe, was zu einer Unterbrechung der Wiedergabe führt. Deaktivieren Sie den Smart/+-Codec an der Kamera oder verkürzen Sie dessen Keyframe-Intervall.", + "unknown": "Der Abstand zwischen den Keyframes konnte nicht ermittelt werden.", + "recordDisabled": "Die Aufzeichnung ist für diese Kamera deaktiviert." + } + }, + "overview": "Übersicht", + "label": { + "detect": "erkennen", + "camera": "Kamera", + "skipped": "übersprungene", + "ffmpeg": "FFmpeg", + "capture": "aufnehmen", + "overallFramesPerSecond": "Bilder pro Sekunde", + "overallDetectionsPerSecond": "Erkennungen pro Sekunde", + "overallSkippedDetectionsPerSecond": "übersprungene Erkennungen pro Sekunde", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} Aufnahme", + "cameraDetect": "{{camName}} Erkennung", + "cameraFramesPerSecond": "{{camName}} Bilder pro Sekunde", + "cameraDetectionsPerSecond": "{{camName}} Erkennungen pro Sekunde", + "cameraSkippedDetectionsPerSecond": "{{camName}} übersprungene Erkennungen pro Sekunde", + "cameraGpu": "{{camName}} GPU" + }, + "title": "Kameras", + "framesAndDetections": "Bilder / Erkennungen", + "toast": { + "success": { + "copyToClipboard": "Kopiert Untersuchungsdaten in die Zwischenablage." + }, + "error": { + "unableToProbeCamera": "Die Kamera kann nicht getestet werden: {{errorMessage}}" + } + }, + "connectionQuality": { + "title": "Verbindungsqualität", + "excellent": "Ausgezeichnet", + "fair": "Fair", + "poor": "arm", + "unusable": "Unbrauchbar", + "fps": "FPS", + "expectedFps": "Erwartete FPS", + "reconnectsLastHour": "Wiederverbindungen (letzte Stunde)", + "stallsLastHour": "Stände (letzte Stunde)" + }, + "noCameras": { + "title": "keine Kameras gefunden" + } + }, + "enrichments": { + "embeddings": { + "image_embedding_speed": "Geschwindigkeit der Bildeinbettung", + "face_embedding_speed": "Geschwindigkeit der Gesichtseinbettung", + "plate_recognition_speed": "Geschwindigkeit der Kennzeichenerkennung", + "text_embedding_speed": "Geschwindigkeit der Texteinbettung", + "plate_recognition": "Kennzeichen Erkennung", + "face_recognition_speed": "Gesichts Erkennungs Geschwindigkeit", + "text_embedding": "Einbettung von Bildern", + "face_recognition": "Gesichts Erkennung", + "image_embedding": "Bild Embedding", + "yolov9_plate_detection_speed": "YOLOv9 Kennzeichenerkennungsgeschwindigkeit", + "yolov9_plate_detection": "YOLOv9 Kennzeichenerkennung", + "review_description": "Bewertung Beschreibung", + "review_description_speed": "Bewertungsbeschreibung Geschwindigkeit", + "review_description_events_per_second": "Bewertungsbeschreibung", + "object_description": "Objekt Beschreibung", + "object_description_speed": "Objektbeschreibung Geschwindigkeit", + "object_description_events_per_second": "Objektbeschreibung", + "classification": "{{name}} Klassifizierung", + "classification_speed": "{{name}} Klassifizierungsgeschwindigkeit", + "classification_events_per_second": "{{name}} Klassifizierungsereignisse pro Sekunde" + }, + "title": "Optimierungen", + "infPerSecond": "Rückschlüsse pro Sekunde", + "averageInf": "Durchschnittliche Inferenzzeit" + }, + "stats": { + "healthy": "Das System läuft problemlos", + "ffmpegHighCpuUsage": "{{camera}} hat eine hohe FFmpeg CPU Auslastung ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} hat eine hohe CPU Auslastung bei der Erkennung ({{detectAvg}}%)", + "reindexingEmbeddings": "Neuindizierung von Einbettungen ({{processed}}% erledigt)", + "detectIsSlow": "{{detect}} ist langsam ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} ist sehr langsam ({{speed}} ms)", + "cameraIsOffline": "{{camera}} ist offline", + "shmTooLow": "Die Zuweisung für /dev/shm ({{total}} MB) sollte auf mindestens {{min}} MB erhöht werden.", + "debugReplayActive": "Debug-Wiederholungssitzung ist aktiv" + }, + "lastRefreshed": "Zuletzt aktualisiert: " +} diff --git a/web/public/locales/el/audio.json b/web/public/locales/el/audio.json new file mode 100644 index 0000000..f3344f1 --- /dev/null +++ b/web/public/locales/el/audio.json @@ -0,0 +1,74 @@ +{ + "speech": "Διάλογος", + "babbling": "Φλυαρία", + "yell": "Φωνές", + "bellow": "Κάτω από", + "whoop": "Κραυγή", + "whispering": "Ψίθυρος", + "laughter": "Γέλια", + "snicker": "Χαχανιτά", + "crying": "Κλάμα", + "dog": "Σκύλος", + "cat": "Γάτα", + "pig": "Γουρούνι", + "oink": "Κραυγή Γουρουνιού", + "moo": "Μουγκανιτό", + "cowbell": "Κουδούνι Αγελάδας", + "horse": "Άλογο", + "goat": "Κατσίκα", + "chicken": "Πρόβατο", + "child_singing": "Τραγούδι Παιδιού", + "sneeze": "Φτέρνισμα", + "sniff": "Όσφρηση", + "run": "Τρέξιμο", + "shuffle": "Ανακάτεμα", + "footsteps": "Βήματα", + "chewing": "Μάσημα", + "biting": "Δάγκωμα", + "bicycle": "Ποδήλατο", + "car": "Αυτοκίνητο", + "motorcycle": "Μηχανή", + "breathing": "Αναπνοή", + "snoring": "Ροχαλιτό", + "honk": "Κόρνα", + "wild_animals": "Άγρια Ζώα", + "roaring_cats": "Κραυγές από Γάτες", + "roar": "Βρυχηθμός", + "bird": "Πουλί", + "pigeon": "Περιστέρι", + "crow": "Κοράκι", + "caw": "Αγελάδα", + "owl": "Κουκουβάγια", + "flapping_wings": "Φτερούγισμα", + "dogs": "Σκυλιά", + "rats": "Ποντίκια", + "guitar": "Κιθάρα", + "electric_guitar": "Ηλεκτρική Κιθάρα", + "bass_guitar": "Μπάσο", + "acoustic_guitar": "Ακουστική Κιθάρα", + "classical_music": "Κλασική Μουσική", + "opera": "Όπερα", + "electronic_music": "Ηλεκτρονική Μουσική", + "bus": "Λεωφορείο", + "train": "Εκπαίδευση", + "boat": "Βάρκα", + "sigh": "Αναστεναγμός", + "singing": "Τραγούδι", + "choir": "Χορωδία", + "whistling": "Σφύριγμα", + "camera": "Κάμερα", + "wheeze": "Ξεφύσημα", + "yodeling": "Λαρυγγισμός", + "chant": "Ύμνος", + "mantra": "Μάντρα", + "synthetic_singing": "Συνθετικό Τραγούδι", + "rapping": "Χτύπημα", + "humming": "Μουρμουρητό", + "groan": "Aναστεναγμός", + "grunt": "Μούγκρισμα", + "gasp": "Ανάσα", + "pant": "Λαχάνιασμα", + "snort": "Ρουθούνισμα", + "cough": "Βήχας", + "throat_clearing": "Ξεροβήξιμο" +} diff --git a/web/public/locales/el/common.json b/web/public/locales/el/common.json new file mode 100644 index 0000000..256fcfb --- /dev/null +++ b/web/public/locales/el/common.json @@ -0,0 +1,186 @@ +{ + "time": { + "untilForTime": "Ως {{time}}", + "untilForRestart": "Μέχρι να γίνει επανεκίννηση του Frigate.", + "untilRestart": "Μέχρι να γίνει επανεκκίνηση", + "justNow": "Μόλις τώρα", + "ago": "Πριν {{timeAgo}}", + "today": "Σήμερα", + "yesterday": "Εχθές", + "last7": "Τελευταίες 7 ημέρες", + "year_one": "{{time}} χρόνος", + "year_other": "{{time}} χρόνια", + "month_one": "{{time}} μήνας", + "month_other": "{{time}} μήνες", + "day_one": "{{time}} ημέρα", + "day_other": "{{time}} ημέρες", + "hour_one": "{{time}} ώρα", + "hour_other": "{{time}} ώρες", + "minute_one": "{{time}} λεπτό", + "minute_other": "{{time}} λεπτά", + "second_one": "{{time}} δευτερόλεπτο", + "second_other": "{{time}} δευτερόλεπτα", + "last14": "Τελευταίες 14 ημέρες", + "last30": "Τελευταίες 30 ημέρες", + "thisWeek": "Αυτή την εβδομάδα", + "lastWeek": "Προηγούμενη Εβδομάδα", + "am": "π.μ.", + "yr": "{{time}}χρ", + "mo": "{{time}}μη", + "thisMonth": "Αυτό τον Μήνα", + "lastMonth": "Τελευταίος Μήνας", + "5minutes": "5 λεπτά", + "10minutes": "10 λεπτά", + "30minutes": "30 λεπτά", + "1hour": "1 ώρα", + "12hours": "12 ώρες", + "24hours": "24 ώρες", + "pm": "μ.μ.", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM yyyy", + "24hour": "d MMM yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "d": "{{time}}η", + "h": "{{time}}ω", + "m": "{{time}}λ", + "s": "{{time}}δ", + "inProgress": "Σε εξέλιξη", + "invalidStartTime": "Μη έγκυρη ώρα έναρξης", + "invalidEndTime": "Μη έγκυρη ώρα λήξης", + "never": "Ποτέ" + }, + "menu": { + "live": { + "cameras": { + "count_one": "{{count}} Κάμερα", + "count_other": "{{count}} Κάμερες" + } + } + }, + "button": { + "save": "Αποθήκευση", + "apply": "Εφαρμογή", + "reset": "Επαναφορά", + "done": "Τέλος", + "enabled": "Ενεργοποιημένο", + "enable": "Ενεργοποίηση", + "disabled": "Απενεργοποιημένο", + "disable": "Απενεργοποίηση", + "saving": "Αποθήκευση…", + "cancel": "Ακύρωση", + "close": "Κλείσιμο", + "copy": "Αντιγραφή", + "back": "Πίσω", + "pictureInPicture": "Εικόνα σε εικόνα", + "cameraAudio": "Ήχος κάμερας", + "edit": "Επεξεργασία", + "copyCoordinates": "Αντιγραφή συντεταγμένων", + "delete": "Διαγραφή", + "yes": "Ναι", + "no": "Όχι", + "download": "Κατέβασμα", + "info": "Πληροφορίες", + "history": "Ιστορία", + "on": "Ενεργοποίηση", + "off": "Απενεργοποίηση" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "χλμ/ώρα" + }, + "length": { + "meters": "μέτρα", + "feet": "πόδια" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/ώρα", + "mbph": "MB/ώρα", + "gbph": "GB/ώρα" + } + }, + "label": { + "back": "Επιστροφή", + "hide": "Απόκρυψη {{item}}", + "show": "Εμφάνιση {{item}}", + "ID": "ID", + "none": "Κανένα", + "all": "Όλα", + "other": "άλλο" + }, + "toast": { + "save": { + "title": "Αποθήκευση", + "error": { + "title": "Αποτυχία αποθήκευσης αλλαγών διαμόρφωσης: {{errorMessage}}", + "noMessage": "Αποτυχία αποθήκευσης αλλαγών διαμόρφωσης" + } + } + }, + "role": { + "admin": "Διαχειριστής", + "desc": "Οι διαχειριστές έχουν πλήρη πρόσβαση σε όλες τις λειτουργίες του περιβάλλοντος χρήστη Frigate. Οι θεατές έχουν περιορισμένη πρόσβαση στην προβολή καμερών, στην αναθεώρηση στοιχείων και σε ιστορικό υλικό στο περιβάλλον χρήστη.", + "viewer": "Θεατής" + }, + "pagination": { + "previous": { + "title": "Προηγούμενο", + "label": "Μετάβαση στην προηγούμενη σελίδα" + }, + "next": { + "title": "Επόμενο", + "label": "Μετάβαση στην επόμενη σελίδα" + }, + "more": "Περισσότερες σελίδες" + }, + "accessDenied": { + "documentTitle": "Πρόσβαση απορρίφθηκε - Frigate", + "title": "Πρόσβαση απορρίφθηκε", + "desc": "Δεν έχετε άδεια να δείτε αυτή τη σελίδα." + }, + "notFound": { + "documentTitle": "Δεν βρέθηκε - Frigate", + "title": "404", + "desc": "Η σελίδα δεν βρέθηκε" + }, + "list": { + "two": "{{0}} και {{1}}", + "many": "{{items}} και {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "internalID": "Το εσωτερικό ID που χρησιμοποίησε η Fregate στη διαμόρφωση και τη βάση δεδομένων" + } +} diff --git a/web/public/locales/el/components/auth.json b/web/public/locales/el/components/auth.json new file mode 100644 index 0000000..c978b36 --- /dev/null +++ b/web/public/locales/el/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Όνομα χρήστη", + "password": "Κωδικός", + "login": "Σύνδεση", + "errors": { + "usernameRequired": "Απαιτείται όνομα χρήστη", + "passwordRequired": "Απαιτείται κωδικός", + "rateLimit": "Το όριο μεταφοράς έχει ξεπεραστεί. Δοκιμάστε ξανά αργότερα.", + "loginFailed": "Αποτυχία σύνδεσης", + "unknownError": "Άγνωστο σφάλμα. Ελέγξτε το αρχείο καταγραφής.", + "webUnknownError": "Άγνωστο σφάλμα. Εξετάστε το αρχείο καταγραφής κονσόλας." + }, + "firstTimeLogin": "Προσπαθείτε να συνδεθείτε για πρώτη φορά; Τα διαπιστευτήρια είναι τυπωμένα στα logs του Frigate." + } +} diff --git a/web/public/locales/el/components/camera.json b/web/public/locales/el/components/camera.json new file mode 100644 index 0000000..3de7248 --- /dev/null +++ b/web/public/locales/el/components/camera.json @@ -0,0 +1,42 @@ +{ + "group": { + "add": "Προσθήκη ομάδας καμερών", + "label": "Ομάδες καμερών", + "edit": "Επεξεργασία ομάδας καμερών", + "delete": { + "label": "Διαγραφή ομάδας κάμερας", + "confirm": { + "title": "Επιβεβαίωση Διαγραφής", + "desc": "Είστε σίγουροι για την διαγραφή της ομάδας κάμερας {{name}};" + } + }, + "name": { + "label": "Όνομα", + "placeholder": "Εισάγετε όνομα…", + "errorMessage": { + "mustLeastCharacters": "Το όνομα ομάδας κάμερας πρέπει να περιέχει τουλάχιστον 2 χαρακτήρες.", + "exists": "Το όνομα ομάδας κάμερας υπάρχει ήδη.", + "nameMustNotPeriod": "Το όνομα ομάδας κάμερας δεν μπορεί να περιλαμβάνει κενά.", + "invalid": "Άκυρο όνομα ομάδας κάμερας." + } + }, + "camera": { + "setting": { + "audioIsUnavailable": "Ο ήχος δεν είναι διαθέσιμος για αυτή την μετάδοση", + "audio": { + "tips": { + "title": "Η κάμερα πρέπει να εκπέμπει ήχο και να είναι ρυθμισμένο το go2rtc για αυτή την μετάδοση." + } + }, + "stream": "Μετάδοση", + "placeholder": "Επιλέξτε μια μετάδοση" + } + }, + "cameras": { + "label": "Κάμερες", + "desc": "Διαλέξτε κάμερες για αυτή την ομάδα." + }, + "icon": "Εικονίδιο", + "success": "Η ομάδα κάμερας {{name}} έχει αποθηκευθεί." + } +} diff --git a/web/public/locales/el/components/dialog.json b/web/public/locales/el/components/dialog.json new file mode 100644 index 0000000..40c3f05 --- /dev/null +++ b/web/public/locales/el/components/dialog.json @@ -0,0 +1,55 @@ +{ + "restart": { + "restarting": { + "content": "Αυτή η σελίδα θα φορτώσει ξανά σε {{countdown}} δευτερόλεπτα.", + "title": "Το Frigate κάνει επανεκκίνηση", + "button": "Αναγκαστική επαναφόρτωση τώρα" + }, + "title": "Είστε σίγουροι ότι θέλετε να επανεκκινήσετε το Frigate;", + "button": "Επανεκκίνηση" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Υποβολή σε Frigate+", + "desc": "Τα αντικείμενα σε τοποθεσίες που θέλετε να αποφύγετε δεν είναι ψευδώς θετικά. Η υποβολή τους ως ψευδώς θετικά θα προκαλέσει σύγχυση στο μοντέλο." + }, + "review": { + "question": { + "label": "Επιβεβαιώστε αυτήν την ετικέτα για το Frigate Plus", + "ask_a": "Είναι αυτό το αντικείμενο {{label}};", + "ask_an": "Είναι αυτό το αντικείμενο {{label}};", + "ask_full": "Είναι αυτό το αντικείμενο {{untranslatedLabel}} ({{translatedLabel}});" + }, + "state": { + "submitted": "Υποβλήθηκε" + } + } + }, + "video": { + "viewInHistory": "Προβολή στο Ιστορικό" + } + }, + "export": { + "time": { + "fromTimeline": "Επιλογή από Χρονολόγιο", + "lastHour_one": "Τελευταία ώρα", + "lastHour_other": "Τελευταίες {{count}} Ώρες", + "custom": "Προσαρμοσμένο", + "start": { + "title": "Αρχή Χρόνου" + } + }, + "select": "Επιλογή", + "export": "Εξαγωγή", + "selectOrExport": "Επιλογή ή Εξαγωγή", + "toast": { + "success": "Επιτυχής έναρξη εξαγωγής. Δείτε το αρχείο στον φάκελο /exports." + } + }, + "search": { + "saveSearch": { + "label": "Αποθήκευση αναζήτησης" + } + } +} diff --git a/web/public/locales/el/components/filter.json b/web/public/locales/el/components/filter.json new file mode 100644 index 0000000..da69d7f --- /dev/null +++ b/web/public/locales/el/components/filter.json @@ -0,0 +1,41 @@ +{ + "filter": "Φίλτρο", + "labels": { + "label": "Ετικέτες", + "all": { + "title": "Όλες οι ετικέτες", + "short": "Ετικέτες" + }, + "count_one": "{{count}} Ετικέτα", + "count_other": "{{count}} Ετικέτες" + }, + "classes": { + "all": { + "title": "Όλες οι κλάσεις" + }, + "count_one": "{{count}} Κλάση", + "count_other": "{{count}} Κλάσεις", + "label": "Κλάσεις" + }, + "zones": { + "label": "Ζώνες", + "all": { + "title": "Όλες οι ζώνες", + "short": "Ζώνες" + } + }, + "score": "Σκορ", + "estimatedSpeed": "Εκτιμώμενη Ταχύτητα {{unit}}", + "features": { + "label": "Χαρακτηριστικά", + "hasSnapshot": "Έχει ένα στιγμιότυπο" + }, + "dates": { + "selectPreset": "Διαλέξτε μια Προεπιλογή…", + "all": { + "title": "Όλες οι Ημερομηνίες", + "short": "Ημερομηνίες" + } + }, + "more": "Επιπλέον Φίλτρα" +} diff --git a/web/public/locales/el/components/icons.json b/web/public/locales/el/components/icons.json new file mode 100644 index 0000000..5be267a --- /dev/null +++ b/web/public/locales/el/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Επιλέξτε ένα εικονίδιο", + "search": { + "placeholder": "Αναζήτηση εικονιδίου…" + } + } +} diff --git a/web/public/locales/el/components/input.json b/web/public/locales/el/components/input.json new file mode 100644 index 0000000..87a9318 --- /dev/null +++ b/web/public/locales/el/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Λήψη βίντεο", + "toast": { + "success": "Το βίντεο αξιολόγησης έχει αρχίσει να κατεβαίνει." + } + } + } +} diff --git a/web/public/locales/el/components/player.json b/web/public/locales/el/components/player.json new file mode 100644 index 0000000..de23a97 --- /dev/null +++ b/web/public/locales/el/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Δεν βρέθηκαν εγγραφές για αυτήν την ώρα", + "noPreviewFound": "Δεν βρέθηκε προεπισκόπηση", + "noPreviewFoundFor": "Δεν βρέθηκε προεπισκόπηση για {{cameraName}}", + "submitFrigatePlus": { + "title": "Να υποβληθεί αυτό το καρέ στο Frigate+;", + "submit": "Υποβολή" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 ή μεγαλύτερο χρειάζεται για αυτό τον τύπο του live stream.", + "streamOffline": { + "title": "Η μετάδοση είναι εκτός λειτουργίας", + "desc": "Δεν έχουν ληφθεί καρέ στη ροή {{cameraName}} detect, ελέγξτε τα αρχεία καταγραφής σφαλμάτων" + }, + "cameraDisabled": "Η Κάμερα έχει απενεργοποιηθεί", + "stats": { + "streamType": { + "title": "Τύπος μετάδοσης:", + "short": "Τύπος" + }, + "bandwidth": { + "title": "Ταχύτητα:", + "short": "Ταχύτητα" + }, + "latency": { + "title": "Καθυστέρηση:", + "value": "{{seconds}} δευτερόλεπτα", + "short": { + "title": "Καθυστέρηση", + "value": "{{seconds}} δευτερόλεπτα" + } + }, + "totalFrames": "Συνολικός αριθμός Καρέ:", + "droppedFrames": { + "title": "Απορριφθέντα καρέ:", + "short": { + "title": "Απορριφθέντα", + "value": "{{droppedFrames}} καρέ" + } + }, + "decodedFrames": "Αποκωδικοποιημένα Καρέ:", + "droppedFrameRate": "Ρυθμός Απορριφθέντων Καρέ:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Επιτυχής αποστολή εικόνας στο Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Αποτυχία αποστολής εικόνας στο Frigate+" + } + } +} diff --git a/web/public/locales/el/config/cameras.json b/web/public/locales/el/config/cameras.json new file mode 100644 index 0000000..b9d465e --- /dev/null +++ b/web/public/locales/el/config/cameras.json @@ -0,0 +1,3 @@ +{ + "label": "Ρύθμιση της κάμερας" +} diff --git a/web/public/locales/el/config/global.json b/web/public/locales/el/config/global.json new file mode 100644 index 0000000..5b60515 --- /dev/null +++ b/web/public/locales/el/config/global.json @@ -0,0 +1,14 @@ +{ + "version": { + "label": "Τρέχουσα έκδοση διαμόρφωσης" + }, + "safe_mode": { + "label": "Ασφαλής λειτουργία" + }, + "auth": { + "reset_admin_password": { + "label": "Επανέφερε κωδικού πρόσβασης για τον διαχειριστή admin", + "description": "Άμα είναι αλήθεια, επαναφέρει τον κωδικό πρόσβασης του χρήστη διαχειριστή(admin) κατά την εκκίνηση και εκτύπωση του νέου κωδικού πρόσβασης στα αρχείο καταγραφής(logs)" + } + } +} diff --git a/web/public/locales/el/config/groups.json b/web/public/locales/el/config/groups.json new file mode 100644 index 0000000..23a118f --- /dev/null +++ b/web/public/locales/el/config/groups.json @@ -0,0 +1,7 @@ +{ + "audio": { + "global": { + "detection": "Παγκόσμια Ανίχνευση" + } + } +} diff --git a/web/public/locales/el/config/validation.json b/web/public/locales/el/config/validation.json new file mode 100644 index 0000000..d99e527 --- /dev/null +++ b/web/public/locales/el/config/validation.json @@ -0,0 +1,3 @@ +{ + "minimum": "Πρέπει να είναι τουλάχιστον {{limit}}" +} diff --git a/web/public/locales/el/objects.json b/web/public/locales/el/objects.json new file mode 100644 index 0000000..5cc7e4f --- /dev/null +++ b/web/public/locales/el/objects.json @@ -0,0 +1,24 @@ +{ + "person": "Άτομο", + "bicycle": "Ποδήλατο", + "car": "Αυτοκίνητο", + "motorcycle": "Μηχανή", + "airplane": "Αεροπλάνο", + "bird": "Πουλί", + "bus": "Λεωφορείο", + "train": "Εκπαίδευση", + "boat": "Βάρκα", + "traffic_light": "Φανάρι Κυκλοφορίας", + "fire_hydrant": "Πυροσβεστικός Κρουνός", + "horse": "Άλογο", + "street_sign": "Πινακίδα Δρόμου", + "stop_sign": "Πινακίδα Στοπ", + "bear": "Αρκούδα", + "zebra": "Ζέμπρα", + "giraffe": "Καμηλοπάρδαλη", + "hat": "Καπέλο", + "parking_meter": "Παρκόμετρο", + "bench": "Παγκάκι", + "cat": "Γάτα", + "dog": "Σκύλος" +} diff --git a/web/public/locales/el/views/chat.json b/web/public/locales/el/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/el/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/el/views/classificationModel.json b/web/public/locales/el/views/classificationModel.json new file mode 100644 index 0000000..926a5fc --- /dev/null +++ b/web/public/locales/el/views/classificationModel.json @@ -0,0 +1,14 @@ +{ + "documentTitle": "Μοντέλα Ταξινόμησης - Frigate", + "details": { + "scoreInfo": "Η βαθμολογία αντιπροσωπεύει την κατά μέσο όρο ταξινομική εμπιστοσύνη μεταξύ όλων των ανιχνεύσεων αυτού του αντικειμένου.", + "none": "Καμία", + "unknown": "Άγνωστο" + }, + "button": { + "deleteClassificationAttempts": "Διαγραφή Εικόνων Ταξινόμησης", + "deleteImages": "Διαγραφή Εικόνων", + "trainModel": "Εκπαίδευση Μοντέλου", + "addClassification": "Προσθήκη Ταξινόμησης" + } +} diff --git a/web/public/locales/el/views/configEditor.json b/web/public/locales/el/views/configEditor.json new file mode 100644 index 0000000..79917bf --- /dev/null +++ b/web/public/locales/el/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Επεξεργαστής ρυθμίσεων - Frigate", + "configEditor": "Επεξεργαστής Ρυθμίσεων", + "saveAndRestart": "Αποθήκευση και επανεκκίνηση", + "safeConfigEditor": "Επεξεργαστής ρυθμίσεων (Ασφαλής Λειτουργία)", + "safeModeDescription": "Το Frigate είναι σε ασφαλή λειτουργία λόγω λάθους εγκυρότητας ρυθμίσεων.", + "copyConfig": "Αντιγραφή Ρυθμίσεων", + "saveOnly": "Μόνο Αποθήκευση", + "confirm": "Έξοδος χωρίς αποθήκευση;", + "toast": { + "success": { + "copyToClipboard": "Οι Ρυθμίσεις αντιγράφτηκαν στο πρόχειρο." + }, + "error": { + "savingError": "Σφάλμα αποθήκευσης ρυθμίσεων" + } + } +} diff --git a/web/public/locales/el/views/events.json b/web/public/locales/el/views/events.json new file mode 100644 index 0000000..82031a8 --- /dev/null +++ b/web/public/locales/el/views/events.json @@ -0,0 +1,35 @@ +{ + "alerts": "Ειδοποιήσεις", + "detections": "Εντοπισμοί", + "motion": { + "label": "Κίνηση", + "only": "Κίνηση μόνο" + }, + "allCameras": "Όλες οι κάμερες", + "empty": { + "alert": "Δεν υπάρχουν ειδοποιήσεις για εξέταση", + "detection": "Δεν υπάρχουν εντοπισμοί για εξέταση", + "motion": "Δεν βρέθηκαν στοιχεία κίνησης", + "recordingsDisabled": { + "title": "Οι καταγραφές πρέπει να είναι ενεργοποιημένες" + } + }, + "timeline": "Χρονολόγιο", + "timeline.aria": "Επιλογή χρονοσειράς", + "events": { + "label": "Γεγονότα", + "aria": "Επιλογή γεγονότων", + "noFoundForTimePeriod": "Δεν βρέθηκαν γεγονότα για αυτή την περίοδο." + }, + "selected_other": "{{count}} επελεγμένα", + "camera": "Κάμερα", + "detected": "ανιχνέυτηκε", + "documentTitle": "Προεσκόπιση - Frigate", + "recordings": { + "documentTitle": "Καταγραφές - Frigate" + }, + "calendarFilter": { + "last24Hours": "Τελευταίες 24 Ώρες" + }, + "markAsReviewed": "Επιβεβαίωση ως Ελεγμένα" +} diff --git a/web/public/locales/el/views/explore.json b/web/public/locales/el/views/explore.json new file mode 100644 index 0000000..a33fb17 --- /dev/null +++ b/web/public/locales/el/views/explore.json @@ -0,0 +1,52 @@ +{ + "documentTitle": "Εξερευνήστε - Frigate", + "generativeAI": "Παραγωγική τεχνητή νοημοσύνη", + "exploreMore": "Εξερευνήστε περισσότερα αντικείμενα {{label}}", + "exploreIsUnavailable": { + "title": "Η εξερεύνηση δεν είναι διαθέσιμη", + "embeddingsReindexing": { + "context": "Η εξερεύνηση μπορεί να πραγματοποιηθεί μετά το πέρας της καταλογράφησης εμπλουτισμών.", + "startingUp": "Εκκίνηση…", + "estimatedTime": "Εκτιμώμενο υπόλοιπο χρόνου:", + "finishingShortly": "Ολοκλήρωση συντόμως", + "step": { + "thumbnailsEmbedded": "Ενσωματωμένες εικόνες: ", + "descriptionsEmbedded": "Ενσωματωμένες περιγραφές: ", + "trackedObjectsProcessed": "Επεξεργασία παρακολουθούμενων αντικειμένων: " + } + }, + "downloadingModels": { + "context": "Το Frigate κατεβάζει τα απαιτούμενα μοντέλα ενσωμάτωσης για να υποστηρίξει την σημασιολογική αναζήτηση. Αυτό μπορεί να διαρκέσει αρκετά λεπτά αναλόγως και της ταχύτητας σύνδεσης με το διαδύκτιο.", + "setup": { + "visionModel": "Οπτικό Μοντέλο", + "visionModelFeatureExtractor": "Εξαγωγή χαρακτηριστικών οπτικού μοντέλου", + "textModel": "Μοντέλο γραφής" + } + } + }, + "details": { + "timestamp": "Χρονοσήμανση", + "item": { + "tips": { + "mismatch_one": "{{count}} μη διαθέσιμο αντικείμενο ανιχνεύτηκε και έχει συνιπολογιστεί στην προεσκόπιση. Αυτό το αντικείμενο είτε δεν πληροί τις προϋποθέσεις ως προειδοποίηση ή ανίχνευση ή έχει ήδη καθαριστεί/διαγραφεί.", + "mismatch_other": "{{count}} μη διαθέσιμα αντικείμενα ανιχνεύτηκαν και έχουν συνιπολογιστεί στην προεσκόπιση. Αυτά τα αντικείμενα είτε δεν πληρούν τις προϋποθέσεις ως προειδοποιήσεις ή ανιχνεύσεις ή έχουν ήδη καθαριστεί/διαγραφεί." + } + } + }, + "type": { + "video": "βίντεο", + "object_lifecycle": "κύκλος ζωής αντικειμένου" + }, + "objectLifecycle": { + "title": "Κύκλος Ζωής Αντικειμένου", + "noImageFound": "Δεν βρέθηκε εικόνα για αυτό το χρονικό σημείο." + }, + "trackedObjectsCount_one": "{{count}} παρακολουθούμενο αντικείμενο ", + "trackedObjectsCount_other": "{{count}} παρακολουθούμενα αντικείμενα ", + "itemMenu": { + "downloadVideo": { + "label": "Λήψη βίντεο", + "aria": "Λήψη βίντεο" + } + } +} diff --git a/web/public/locales/el/views/exports.json b/web/public/locales/el/views/exports.json new file mode 100644 index 0000000..f6526ee --- /dev/null +++ b/web/public/locales/el/views/exports.json @@ -0,0 +1,22 @@ +{ + "documentTitle": "Εξαγωγή - Frigate", + "search": "Αναζήτηση", + "deleteExport": "Διαγραφή εξαγωγής", + "noExports": "Δεν βρέθηκαν εξαγωγές", + "deleteExport.desc": "Είστε σίγουροι οτι θέλετε να διαγράψετε {{exportName}};", + "editExport": { + "title": "Μετονομασία Εξαγωγής", + "desc": "Εισάγετε ένα νέο όνομα για την εξαγωγή.", + "saveExport": "Αποθήκευση Εξαγωγής" + }, + "toast": { + "error": { + "renameExportFailed": "Αποτυχία μετονομασίας εξαγωγής:{{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Κοινή χρήση εξαγωγής", + "downloadVideo": "Λήψη βίντεο", + "deleteExport": "Διαγραφή εξαγωγής" + } +} diff --git a/web/public/locales/el/views/faceLibrary.json b/web/public/locales/el/views/faceLibrary.json new file mode 100644 index 0000000..7bb548e --- /dev/null +++ b/web/public/locales/el/views/faceLibrary.json @@ -0,0 +1,50 @@ +{ + "description": { + "addFace": "Προσθέστε μια νέα συλλογή στη Βιβλιοθήκη Προσώπων ανεβάζοντας την πρώτη σας εικόνα.", + "placeholder": "Εισαγάγετε ένα όνομα για αυτήν τη συλλογή", + "invalidName": "Μη έγκυρο όνομα. Τα ονόματα μπορούν να περιλαμβάνουν γράμματα, αριθμούς, κενό διάστημα, απόστροφο, παύλα, κάτω παύλα." + }, + "details": { + "person": "Άτομο", + "subLabelScore": "Σκορ υποετικέτας", + "scoreInfo": "Το σκορ υποετικέτας είναι το σταθμισμένο σκορ όλων των αναγνωρισμένων προσώπων, αυτό μπορεί να διαφέρει από το σκορ που φαίνεται στο στιγμιότυπο.", + "face": "Λεπτομέρειες προσώπου", + "faceDesc": "Λεπτομέρειες του παρακολουθούμενου αντικειμένου που παρήγε αυτό το πρόσωπο", + "timestamp": "Χρονοσήμανση", + "unknown": "Άγνωστο" + }, + "deleteFaceAttempts": { + "desc_one": "Είστε σίγουροι ότι θέλετε να διαγράψετε {{count}} πρόσωπο; Αυτή η πράξη δεν επαναφέρεται.", + "desc_other": "Είστε σίγουροι ότι θέλετε να διαγράψετε {{count}} πρόσωπα; Αυτή η πράξη δεν επαναφέρεται." + }, + "toast": { + "success": { + "deletedFace_one": "Επιτυχής διαγραφή {{count}} προσώπου.", + "deletedFace_other": "Επιτυχής διαγραφή {{count}} προσώπων.", + "deletedName_one": "{{count}} πρόσωπο διεγράφη επιτυχημένα.", + "deletedName_other": "{{count}} πρόσωπα διεγράφη επιτυχημένα." + } + }, + "documentTitle": "Βιβλιοθήκη προσώπων - Frigate", + "uploadFaceImage": { + "title": "Μεταφόρτωση Εικόνας Προσώπου", + "desc": "Ανεβάστε μια εικόνα για να σαρώσετε πρόσωπα και να τα συμπεριλάβετε στο {{pageToggle}}" + }, + "steps": { + "nextSteps": "Επόμενα βήματα", + "description": { + "uploadFace": "Μεταφορτώστε μια εικόνα του/της {{name}} που δείχνει το πρόσωπο τους από μπροστινή λήψη. Η εικόνα δεν χρειάζεται να περιέχει μόνο το πρόσωπο τους." + } + }, + "train": { + "title": "Εκπαίδευση", + "aria": "Επιλογή εκπαίδευσης", + "empty": "Δεν υπάρχουν πρόσφατες προσπάθειες αναγνώρισης προσώπου" + }, + "collections": "Συλλογές", + "createFaceLibrary": { + "title": "Δημιουργία Συλλογής", + "desc": "Δημιουργία νέας συλλογής", + "new": "Δημιουργία Νέου Προσώπου" + } +} diff --git a/web/public/locales/el/views/live.json b/web/public/locales/el/views/live.json new file mode 100644 index 0000000..b242711 --- /dev/null +++ b/web/public/locales/el/views/live.json @@ -0,0 +1,69 @@ +{ + "documentTitle": "Ζωντανά - Frigate", + "twoWayTalk": { + "enable": "Ενεργοποίηση αμφίδρομης επικοινωνίας", + "disable": "Απενεργοποίηση αμφίδρομης επικοινωνίας" + }, + "documentTitle.withCamera": "{{camera}} - Ζωντανή μετάδοση - Frigate", + "lowBandwidthMode": "Λειτουργία χαμηλής ευρυζωνικότητας", + "cameraAudio": { + "enable": "Ενεργοποίηση ήχου Κάμερας", + "disable": "Απενεργοποίηση Ήχου Κάμερας" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Πατήστε στο πλαίσιο για να κεντράρετε την κάμερα", + "enable": "Ενεργοποίηση κλικ για μεταφορά", + "disable": "Απενεργοποίηση κλικ για μεταφορά" + }, + "left": { + "label": "Κίνηση κάμερας προς τα αριστερά" + }, + "up": { + "label": "Κίνηση κάμερας προς τα πάνω" + }, + "down": { + "label": "Κίνηση κάμερας προς τα κάτω" + }, + "right": { + "label": "Κίνηση κάμερας προς τα δεξιά" + } + }, + "zoom": { + "in": { + "label": "Ζουμάρισμα κάμερας προς τα μέσα" + }, + "out": { + "label": "Ζουμάρισμα κάμερας προς τα έξω" + } + } + }, + "camera": { + "enable": "Ενεργοποίηση Κάμερας", + "disable": "Απενεργοποίηση Κάμερας" + }, + "muteCameras": { + "enable": "Σίγαση Όλων των Καμερών", + "disable": "Απενεργοποίηση Σίγασης Όλων των Καμερών" + }, + "detect": { + "enable": "Ενεργοποίηση Ανίχνευσης", + "disable": "Απενεργοποίηση Ανίχνευσης" + }, + "recording": { + "enable": "Ενεργοποίηση Καταγραφής", + "disable": "Απενεργοποίηση Καταγραφής" + }, + "snapshots": { + "enable": "Ενεργοποίηση Στιγμιοτίπων", + "disable": "Απενεργοποίηση Στιγμιοτίπων" + }, + "audioDetect": { + "enable": "Ενεργοποίηση Ανίχνευσης Ήχου", + "disable": "Απενεργοποίηση Ανίχνευσης Ήχου" + }, + "noCameras": { + "buttonText": "Προσθήκη Κάμερας" + } +} diff --git a/web/public/locales/el/views/motionSearch.json b/web/public/locales/el/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/el/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/el/views/recording.json b/web/public/locales/el/views/recording.json new file mode 100644 index 0000000..9681d0e --- /dev/null +++ b/web/public/locales/el/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Φίλτρο", + "export": "Εξαγωγή", + "calendar": "Ημερολόγιο", + "filters": "Φίλτρα", + "toast": { + "error": { + "noValidTimeSelected": "Μη επιλογή έγκυρης περιόδου", + "endTimeMustAfterStartTime": "Το επιλεγμένο τέλος περιόδου πρέπει να είναι μετά την επιλεγμένη αρχή περιόδου" + } + } +} diff --git a/web/public/locales/el/views/replay.json b/web/public/locales/el/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/el/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/el/views/search.json b/web/public/locales/el/views/search.json new file mode 100644 index 0000000..1281446 --- /dev/null +++ b/web/public/locales/el/views/search.json @@ -0,0 +1,29 @@ +{ + "search": "Αναζήτηση", + "savedSearches": "Αποθηκευμένες Αναζητήσεις", + "button": { + "clear": "Εκαθάρηση αναζήτησης", + "save": "Αποθήκευση αναζήτησης", + "delete": "Διαγραφή αποθηκευμένης αναζήτησης", + "filterInformation": "Πληροφορίες φίλτρου", + "filterActive": "Φίλτρα ενεργά" + }, + "searchFor": "Αναζήτηση {{inputValue}}", + "trackedObjectId": "Σήμανση παρακολουθούμενου αντικειμένου", + "filter": { + "label": { + "cameras": "Κάμερες", + "labels": "Ετικέτες", + "zones": "Ζώνες", + "max_speed": "Ανώτατη Ταχύτητα", + "recognized_license_plate": "Αναγνωρισμένη Πινακίδα Κυκλοφορίας", + "has_clip": "Έχει Κλιπ", + "has_snapshot": "Έχει Στιγμιότυπο", + "sub_labels": "Υποετικέτες", + "search_type": "Τύπος Αναζήτησης", + "time_range": "Χρονική Περίοδος", + "before": "Πριν", + "after": "Μετά" + } + } +} diff --git a/web/public/locales/el/views/settings.json b/web/public/locales/el/views/settings.json new file mode 100644 index 0000000..75884e0 --- /dev/null +++ b/web/public/locales/el/views/settings.json @@ -0,0 +1,58 @@ +{ + "documentTitle": { + "default": "Ρυθμίσεις - Frigate", + "authentication": "Ρυθμίσεις ελέγχου ταυτοποίησης - Frigate", + "camera": "Ρυθμίσεις Κάμερας - Frigate", + "enrichments": "Ρυθμίσεις εμπλουτισμού - Frigate", + "masksAndZones": "Ρυθμίσεις Μασκών και Ζωνών - Frigate", + "motionTuner": "Ρύθμιση Κίνησης - Frigate", + "object": "Επίλυση σφαλμάτων - Frigate", + "general": "Ρυθμίσεις UI - Frigate", + "frigatePlus": "Ρυθμίσεις Frigate+ - Frigate", + "notifications": "Ρυθμίσεις Ειδοποιήσεων", + "cameraManagement": "Διαχείριση καμερών - Frigate", + "cameraReview": "Ρυθμίσεις αξιολόγησης κάμερας - Frigate" + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} σημέιο", + "point_other": "{{count}} σημεία" + }, + "motionMasks": { + "point_one": "{{count}} σημείο", + "point_other": "{{count}} σημεία" + }, + "objectMasks": { + "point_one": "{{count}} σημέιο", + "point_other": "{{count}} σημεία" + } + }, + "menu": { + "ui": "Επιφάνεια Εργασίας", + "enrichments": "Εμπλουτισμοί", + "cameras": "Ρυθμίσεις Κάμερας", + "masksAndZones": "Μάσκες / Ζώνες", + "motionTuner": "Ρυθμιστής Κίνησης", + "debug": "Επίλυση Σφαλμάτων" + }, + "dialog": { + "unsavedChanges": { + "title": "Έχετε μη αποθηκευμένες αλλαγές.", + "desc": "Θέλετε να αποθηκεύσετε τις αλλαγές σας πριν την συνέχεια;" + } + }, + "cameraSetting": { + "camera": "Κάμερα", + "noCamera": "Δεν υπάρχει Κάμερα" + }, + "triggers": { + "dialog": { + "form": { + "friendly_name": { + "placeholder": "Ονομάτισε ή περιέγραψε αυτό το εύνασμα", + "description": "Ένα προαιρετικό φιλικό όνομα, ή ένα περιγραφικό κείμενο για αυτό το εύνασμα." + } + } + } + } +} diff --git a/web/public/locales/el/views/system.json b/web/public/locales/el/views/system.json new file mode 100644 index 0000000..0ec8ff5 --- /dev/null +++ b/web/public/locales/el/views/system.json @@ -0,0 +1,39 @@ +{ + "documentTitle": { + "cameras": "Στατιστικά Καμερών - Frigate", + "storage": "Στατιστικά αποθήκευσης - Frigate", + "general": "Γενικά στατιστικά - Frigate", + "enrichments": "Στατιστικά Εμπλουτισμού - Frigate", + "logs": { + "frigate": "Frigate αρχέιο καταγραφών - Frigate", + "go2rtc": "Αρχείο καταγραφής Go2RTC - Frigate", + "nginx": "Αρχείο καταγραφών Nginx - Frigate" + } + }, + "title": "Σύστημα", + "metrics": "Μετρήσεις συστήματος", + "logs": { + "download": { + "label": "Λήψη Αρχείων Καταγραφής" + }, + "copy": { + "label": "Αντιγραφή στο πρόχειρο", + "success": "Αρχεία καταγραφής αντιγράφτηκαν στο πρόχειρο", + "error": "Αποτυχία αντιγραφής των αρχείων καταγραφής στο πρόχειρο" + }, + "type": { + "label": "Τύπος", + "timestamp": "Χρονοσήμανση", + "tag": "Λέξη Κλειδί", + "message": "Μήνυμα" + } + }, + "general": { + "title": "Γενικά", + "detector": { + "title": "Ανιχνευτές", + "inferenceSpeed": "Ταχύτητα Συμπεράσματος Ανιχνευτή", + "temperature": "Θερμοκρασία Ανιχνευτή" + } + } +} diff --git a/web/public/locales/en/audio.json b/web/public/locales/en/audio.json new file mode 100644 index 0000000..5c197e8 --- /dev/null +++ b/web/public/locales/en/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Speech", + "babbling": "Babbling", + "yell": "Yell", + "bellow": "Bellow", + "whoop": "Whoop", + "whispering": "Whispering", + "laughter": "Laughter", + "snicker": "Snicker", + "crying": "Crying", + "sigh": "Sigh", + "singing": "Singing", + "choir": "Choir", + "yodeling": "Yodeling", + "chant": "Chant", + "mantra": "Mantra", + "child_singing": "Child Singing", + "synthetic_singing": "Synthetic Singing", + "rapping": "Rapping", + "humming": "Humming", + "groan": "Groan", + "grunt": "Grunt", + "whistling": "Whistling", + "breathing": "Breathing", + "wheeze": "Wheeze", + "snoring": "Snoring", + "gasp": "Gasp", + "pant": "Pant", + "snort": "Snort", + "cough": "Cough", + "throat_clearing": "Throat Clearing", + "sneeze": "Sneeze", + "sniff": "Sniff", + "run": "Run", + "shuffle": "Shuffle", + "footsteps": "Footsteps", + "chewing": "Chewing", + "biting": "Biting", + "gargling": "Gargling", + "stomach_rumble": "Stomach Rumble", + "burping": "Burping", + "hiccup": "Hiccup", + "fart": "Fart", + "hands": "Hands", + "finger_snapping": "Finger Snapping", + "clapping": "Clapping", + "heartbeat": "Heartbeat", + "heart_murmur": "Heart Murmur", + "cheering": "Cheering", + "applause": "Applause", + "chatter": "Chatter", + "crowd": "Crowd", + "children_playing": "Children Playing", + "animal": "Animal", + "pets": "Pets", + "dog": "Dog", + "bark": "Bark", + "yip": "Yip", + "howl": "Howl", + "bow_wow": "Bow Wow", + "growling": "Growling", + "whimper_dog": "Dog Whimper", + "cat": "Cat", + "purr": "Purr", + "meow": "Meow", + "hiss": "Hiss", + "caterwaul": "Caterwaul", + "livestock": "Livestock", + "horse": "Horse", + "clip_clop": "Clip Clop", + "neigh": "Neigh", + "cattle": "Cattle", + "moo": "Moo", + "cowbell": "Cowbell", + "pig": "Pig", + "oink": "Oink", + "goat": "Goat", + "bleat": "Bleat", + "sheep": "Sheep", + "fowl": "Fowl", + "chicken": "Chicken", + "cluck": "Cluck", + "cock_a_doodle_doo": "Cock-a-Doodle-Doo", + "turkey": "Turkey", + "gobble": "Gobble", + "duck": "Duck", + "quack": "Quack", + "goose": "Goose", + "honk": "Honk", + "wild_animals": "Wild Animals", + "roaring_cats": "Roaring Cats", + "roar": "Roar", + "bird": "Bird", + "chirp": "Chirp", + "squawk": "Squawk", + "pigeon": "Pigeon", + "coo": "Coo", + "crow": "Crow", + "caw": "Caw", + "owl": "Owl", + "hoot": "Hoot", + "flapping_wings": "Flapping Wings", + "dogs": "Dogs", + "rats": "Rats", + "mouse": "Mouse", + "patter": "Patter", + "insect": "Insect", + "cricket": "Cricket", + "mosquito": "Mosquito", + "fly": "Fly", + "buzz": "Buzz", + "frog": "Frog", + "croak": "Croak", + "snake": "Snake", + "rattle": "Rattle", + "whale_vocalization": "Whale Vocalization", + "music": "Music", + "musical_instrument": "Musical Instrument", + "plucked_string_instrument": "Plucked String Instrument", + "guitar": "Guitar", + "electric_guitar": "Electric Guitar", + "bass_guitar": "Bass Guitar", + "acoustic_guitar": "Acoustic Guitar", + "steel_guitar": "Steel Guitar", + "tapping": "Tapping", + "strum": "Strum", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolin", + "zither": "Zither", + "ukulele": "Ukulele", + "keyboard": "Keyboard", + "piano": "Piano", + "electric_piano": "Electric Piano", + "organ": "Organ", + "electronic_organ": "Electronic Organ", + "hammond_organ": "Hammond Organ", + "synthesizer": "Synthesizer", + "sampler": "Sampler", + "harpsichord": "Harpsichord", + "percussion": "Percussion", + "drum_kit": "Drum Kit", + "drum_machine": "Drum Machine", + "drum": "Drum", + "snare_drum": "Snare Drum", + "rimshot": "Rimshot", + "drum_roll": "Drum Roll", + "bass_drum": "Bass Drum", + "timpani": "Timpani", + "tabla": "Tabla", + "cymbal": "Cymbal", + "hi_hat": "Hi-Hat", + "wood_block": "Wood Block", + "tambourine": "Tambourine", + "maraca": "Maraca", + "gong": "Gong", + "tubular_bells": "Tubular Bells", + "mallet_percussion": "Mallet Percussion", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibraphone", + "steelpan": "Steelpan", + "orchestra": "Orchestra", + "brass_instrument": "Brass Instrument", + "french_horn": "French Horn", + "trumpet": "Trumpet", + "trombone": "Trombone", + "bowed_string_instrument": "Bowed String Instrument", + "string_section": "String Section", + "violin": "Violin", + "pizzicato": "Pizzicato", + "cello": "Cello", + "double_bass": "Double Bass", + "wind_instrument": "Wind Instrument", + "flute": "Flute", + "saxophone": "Saxophone", + "clarinet": "Clarinet", + "harp": "Harp", + "bell": "Bell", + "church_bell": "Church Bell", + "jingle_bell": "Jingle Bell", + "bicycle_bell": "Bicycle Bell", + "tuning_fork": "Tuning Fork", + "chime": "Chime", + "wind_chime": "Wind Chime", + "harmonica": "Harmonica", + "accordion": "Accordion", + "bagpipes": "Bagpipes", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Singing Bowl", + "scratching": "Scratching", + "pop_music": "Pop Music", + "hip_hop_music": "Hip-Hop Music", + "beatboxing": "Beatboxing", + "rock_music": "Rock Music", + "heavy_metal": "Heavy Metal", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "progressive_rock": "Progressive Rock", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Psychedelic Rock", + "rhythm_and_blues": "Rhythm and Blues", + "soul_music": "Soul Music", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Swing Music", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folk Music", + "middle_eastern_music": "Middle Eastern Music", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Classical Music", + "opera": "Opera", + "electronic_music": "Electronic Music", + "house_music": "House Music", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Electronica", + "electronic_dance_music": "Electronic Dance Music", + "ambient_music": "Ambient Music", + "trance_music": "Trance Music", + "music_of_latin_america": "Music of Latin America", + "salsa_music": "Salsa Music", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Music for Children", + "new-age_music": "New Age Music", + "vocal_music": "Vocal Music", + "a_capella": "A Capella", + "music_of_africa": "Music of Africa", + "afrobeat": "Afrobeat", + "christian_music": "Christian Music", + "gospel_music": "Gospel Music", + "music_of_asia": "Music of Asia", + "carnatic_music": "Carnatic Music", + "music_of_bollywood": "Music of Bollywood", + "ska": "Ska", + "traditional_music": "Traditional Music", + "independent_music": "Independent Music", + "song": "Song", + "background_music": "Background Music", + "theme_music": "Theme Music", + "jingle": "Jingle", + "soundtrack_music": "Soundtrack Music", + "lullaby": "Lullaby", + "video_game_music": "Video Game Music", + "christmas_music": "Christmas Music", + "dance_music": "Dance Music", + "wedding_music": "Wedding Music", + "happy_music": "Happy Music", + "sad_music": "Sad Music", + "tender_music": "Tender Music", + "exciting_music": "Exciting Music", + "angry_music": "Angry Music", + "scary_music": "Scary Music", + "wind": "Wind", + "rustling_leaves": "Rustling Leaves", + "wind_noise": "Wind Noise", + "thunderstorm": "Thunderstorm", + "thunder": "Thunder", + "water": "Water", + "rain": "Rain", + "raindrop": "Raindrop", + "rain_on_surface": "Rain on Surface", + "stream": "Stream", + "waterfall": "Waterfall", + "ocean": "Ocean", + "waves": "Waves", + "steam": "Steam", + "gurgling": "Gurgling", + "fire": "Fire", + "crackle": "Crackle", + "vehicle": "Vehicle", + "boat": "Boat", + "sailboat": "Sailboat", + "rowboat": "Rowboat", + "motorboat": "Motorboat", + "ship": "Ship", + "motor_vehicle": "Motor Vehicle", + "car": "Car", + "toot": "Toot", + "car_alarm": "Car Alarm", + "power_windows": "Power Windows", + "skidding": "Skidding", + "tire_squeal": "Tire Squeal", + "car_passing_by": "Car Passing By", + "race_car": "Race Car", + "truck": "Truck", + "air_brake": "Air Brake", + "air_horn": "Air Horn", + "reversing_beeps": "Reversing Beeps", + "ice_cream_truck": "Ice Cream Truck", + "bus": "Bus", + "emergency_vehicle": "Emergency Vehicle", + "police_car": "Police Car", + "ambulance": "Ambulance", + "fire_engine": "Fire Engine", + "motorcycle": "Motorcycle", + "traffic_noise": "Traffic Noise", + "rail_transport": "Rail Transport", + "train": "Train", + "train_whistle": "Train Whistle", + "train_horn": "Train Horn", + "railroad_car": "Railroad Car", + "train_wheels_squealing": "Train Wheels Squealing", + "subway": "Subway", + "aircraft": "Aircraft", + "aircraft_engine": "Aircraft Engine", + "jet_engine": "Jet Engine", + "propeller": "Propeller", + "helicopter": "Helicopter", + "fixed-wing_aircraft": "Fixed-Wing Aircraft", + "bicycle": "Bicycle", + "skateboard": "Skateboard", + "engine": "Engine", + "light_engine": "Light Engine", + "dental_drill's_drill": "Dental Drill", + "lawn_mower": "Lawn Mower", + "chainsaw": "Chainsaw", + "medium_engine": "Medium Engine", + "heavy_engine": "Heavy Engine", + "engine_knocking": "Engine Knocking", + "engine_starting": "Engine Starting", + "idling": "Idling", + "accelerating": "Accelerating", + "door": "Door", + "doorbell": "Doorbell", + "ding-dong": "Ding-Dong", + "sliding_door": "Sliding Door", + "slam": "Slam", + "knock": "Knock", + "tap": "Tap", + "squeak": "Squeak", + "cupboard_open_or_close": "Cupboard Open or Close", + "drawer_open_or_close": "Drawer Open or Close", + "dishes": "Dishes", + "cutlery": "Cutlery", + "chopping": "Chopping", + "frying": "Frying", + "microwave_oven": "Microwave Oven", + "blender": "Blender", + "water_tap": "Water Tap", + "sink": "Sink", + "bathtub": "Bathtub", + "hair_dryer": "Hair Dryer", + "toilet_flush": "Toilet Flush", + "toothbrush": "Toothbrush", + "electric_toothbrush": "Electric Toothbrush", + "vacuum_cleaner": "Vacuum Cleaner", + "zipper": "Zipper", + "keys_jangling": "Keys Jangling", + "coin": "Coin", + "scissors": "Scissors", + "electric_shaver": "Electric Shaver", + "shuffling_cards": "Shuffling Cards", + "typing": "Typing", + "typewriter": "Typewriter", + "computer_keyboard": "Computer Keyboard", + "writing": "Writing", + "alarm": "Alarm", + "telephone": "Telephone", + "telephone_bell_ringing": "Telephone Bell Ringing", + "ringtone": "Ringtone", + "telephone_dialing": "Telephone Dialing", + "dial_tone": "Dial Tone", + "busy_signal": "Busy Signal", + "alarm_clock": "Alarm Clock", + "siren": "Siren", + "civil_defense_siren": "Civil Defense Siren", + "buzzer": "Buzzer", + "smoke_detector": "Smoke Detector", + "fire_alarm": "Fire Alarm", + "foghorn": "Foghorn", + "whistle": "Whistle", + "steam_whistle": "Steam Whistle", + "mechanisms": "Mechanisms", + "ratchet": "Ratchet", + "clock": "Clock", + "tick": "Tick", + "tick-tock": "Tick-Tock", + "gears": "Gears", + "pulleys": "Pulleys", + "sewing_machine": "Sewing Machine", + "mechanical_fan": "Mechanical Fan", + "air_conditioning": "Air Conditioning", + "cash_register": "Cash Register", + "printer": "Printer", + "camera": "Camera", + "single-lens_reflex_camera": "Single-Lens Reflex Camera", + "tools": "Tools", + "hammer": "Hammer", + "jackhammer": "Jackhammer", + "sawing": "Sawing", + "filing": "Filing", + "sanding": "Sanding", + "power_tool": "Power Tool", + "drill": "Drill", + "explosion": "Explosion", + "gunshot": "Gunshot", + "machine_gun": "Machine Gun", + "fusillade": "Fusillade", + "artillery_fire": "Artillery Fire", + "cap_gun": "Cap Gun", + "fireworks": "Fireworks", + "firecracker": "Firecracker", + "burst": "Burst", + "eruption": "Eruption", + "boom": "Boom", + "wood": "Wood", + "chop": "Chop", + "splinter": "Splinter", + "crack": "Crack", + "glass": "Glass", + "chink": "Chink", + "shatter": "Shatter", + "silence": "Silence", + "sound_effect": "Sound Effect", + "environmental_noise": "Environmental Noise", + "static": "Static", + "white_noise": "White Noise", + "pink_noise": "Pink Noise", + "television": "Television", + "radio": "Radio", + "field_recording": "Field Recording", + "scream": "Scream", + "sodeling": "Sodeling", + "chird": "Chird", + "change_ringing": "Change Ringing", + "shofar": "Shofar", + "liquid": "Liquid", + "splash": "Splash", + "slosh": "Slosh", + "squish": "Squish", + "drip": "Drip", + "pour": "Pour", + "trickle": "Trickle", + "gush": "Gush", + "fill": "Fill", + "spray": "Spray", + "pump": "Pump", + "stir": "Stir", + "boiling": "Boiling", + "sonar": "Sonar", + "arrow": "Arrow", + "whoosh": "Whoosh", + "thump": "Thump", + "thunk": "Thunk", + "electronic_tuner": "Electronic Tuner", + "effects_unit": "Effects Unit", + "chorus_effect": "Chorus Effect", + "basketball_bounce": "Basketball Bounce", + "bang": "Bang", + "slap": "Slap", + "whack": "Whack", + "smash": "Smash", + "breaking": "Breaking", + "bouncing": "Bouncing", + "whip": "Whip", + "flap": "Flap", + "scratch": "Scratch", + "scrape": "Scrape", + "rub": "Rub", + "roll": "Roll", + "crushing": "Crushing", + "crumpling": "Crumpling", + "tearing": "Tearing", + "beep": "Beep", + "ping": "Ping", + "ding": "Ding", + "clang": "Clang", + "squeal": "Squeal", + "creak": "Creak", + "rustle": "Rustle", + "whir": "Whir", + "clatter": "Clatter", + "sizzle": "Sizzle", + "clicking": "Clicking", + "clickety_clack": "Clickety Clack", + "rumble": "Rumble", + "plop": "Plop", + "hum": "Hum", + "zing": "Zing", + "boing": "Boing", + "crunch": "Crunch", + "sine_wave": "Sine Wave", + "harmonic": "Harmonic", + "chirp_tone": "Chirp Tone", + "pulse": "Pulse", + "inside": "Inside", + "outside": "Outside", + "reverberation": "Reverberation", + "echo": "Echo", + "noise": "Noise", + "mains_hum": "Mains Hum", + "distortion": "Distortion", + "sidetone": "Sidetone", + "cacophony": "Cacophony", + "throbbing": "Throbbing", + "vibration": "Vibration" +} diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json new file mode 100644 index 0000000..272e0f2 --- /dev/null +++ b/web/public/locales/en/common.json @@ -0,0 +1,324 @@ +{ + "time": { + "untilForTime": "Until {{time}}", + "untilForRestart": "Until Frigate restarts.", + "untilRestart": "Until restart", + "never": "Never", + "ago": "{{timeAgo}} ago", + "justNow": "Just now", + "today": "Today", + "yesterday": "Yesterday", + "last7": "Last 7 days", + "last14": "Last 14 days", + "last30": "Last 30 days", + "thisWeek": "This Week", + "lastWeek": "Last Week", + "thisMonth": "This Month", + "lastMonth": "Last Month", + "5minutes": "5 minutes", + "10minutes": "10 minutes", + "30minutes": "30 minutes", + "1hour": "1 hour", + "12hours": "12 hours", + "24hours": "24 hours", + "pm": "pm", + "am": "am", + "yr": "{{time}}yr", + "year_one": "{{time}} year", + "year_other": "{{time}} years", + "mo": "{{time}}mo", + "month_one": "{{time}} month", + "month_other": "{{time}} months", + "d": "{{time}}d", + "day_one": "{{time}} day", + "day_other": "{{time}} days", + "h": "{{time}}h", + "hour_one": "{{time}} hour", + "hour_other": "{{time}} hours", + "m": "{{time}}m", + "minute_one": "{{time}} minute", + "minute_other": "{{time}} minutes", + "s": "{{time}}s", + "second_one": "{{time}} second", + "second_other": "{{time}} seconds", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "inProgress": "In progress", + "invalidStartTime": "Invalid start time", + "invalidEndTime": "Invalid end time" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "kph" + }, + "length": { + "feet": "feet", + "meters": "meters" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hour", + "mbph": "MB/hour", + "gbph": "GB/hour" + } + }, + "label": { + "back": "Go back", + "hide": "Hide {{item}}", + "show": "Show {{item}}", + "ID": "ID", + "none": "None", + "all": "All", + "other": "Other" + }, + "list": { + "two": "{{0}} and {{1}}", + "many": "{{items}}, and {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Optional", + "internalID": "The Internal ID Frigate uses in the configuration and database" + }, + "button": { + "add": "Add", + "apply": "Apply", + "applying": "Applying…", + "reset": "Reset", + "undo": "Undo", + "done": "Done", + "enabled": "Enabled", + "enable": "Enable", + "disabled": "Disabled", + "disable": "Disable", + "save": "Save", + "saving": "Saving…", + "cancel": "Cancel", + "close": "Close", + "copy": "Copy", + "copiedToClipboard": "Copied to clipboard", + "back": "Back", + "history": "History", + "fullscreen": "Fullscreen", + "exitFullscreen": "Exit Fullscreen", + "pictureInPicture": "Picture in Picture", + "twoWayTalk": "Two Way Talk", + "cameraAudio": "Camera Audio", + "on": "ON", + "off": "OFF", + "edit": "Edit", + "copyCoordinates": "Copy coordinates", + "delete": "Delete", + "yes": "Yes", + "no": "No", + "download": "Download", + "info": "Info", + "suspended": "Suspended", + "unsuspended": "Unsuspend", + "play": "Play", + "unselect": "Unselect", + "export": "Export", + "deleteNow": "Delete Now", + "next": "Next", + "continue": "Continue", + "modified": "Modified", + "overridden": "Overridden", + "resetToGlobal": "Reset to Global", + "resetToDefault": "Reset to Default", + "saveAll": "Save All", + "savingAll": "Saving All…", + "undoAll": "Undo All", + "retry": "Retry" + }, + "menu": { + "system": "System", + "systemMetrics": "System metrics", + "configuration": "Configuration", + "systemLogs": "System logs", + "profiles": "Profiles", + "settings": "Settings", + "configurationEditor": "Configuration Editor", + "languages": "Languages", + "language": { + "en": "English (English)", + "es": "Español (Spanish)", + "zhCN": "简体中文 (Simplified Chinese)", + "zhHant": "繁體中文 (Traditional Chinese)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (French)", + "ar": "العربية (Arabic)", + "pt": "Português (Portuguese)", + "ptBR": "Português brasileiro (Brazilian Portuguese)", + "ru": "Русский (Russian)", + "de": "Deutsch (German)", + "ja": "日本語 (Japanese)", + "tr": "Türkçe (Turkish)", + "it": "Italiano (Italian)", + "nl": "Nederlands (Dutch)", + "sv": "Svenska (Swedish)", + "cs": "Čeština (Czech)", + "nb": "Norsk Bokmål (Norwegian Bokmål)", + "ko": "한국어 (Korean)", + "vi": "Tiếng Việt (Vietnamese)", + "fa": "فارسی (Persian)", + "pl": "Polski (Polish)", + "uk": "Українська (Ukrainian)", + "he": "עברית (Hebrew)", + "el": "Ελληνικά (Greek)", + "ro": "Română (Romanian)", + "hu": "Magyar (Hungarian)", + "fi": "Suomi (Finnish)", + "da": "Dansk (Danish)", + "sk": "Slovenčina (Slovak)", + "yue": "粵語 (Cantonese)", + "th": "ไทย (Thai)", + "ca": "Català (Catalan)", + "hr": "Hrvatski (Croatian)", + "bs": "Bosanski (Bosnian)", + "sr": "Српски (Serbian)", + "sl": "Slovenščina (Slovenian)", + "lt": "Lietuvių (Lithuanian)", + "bg": "Български (Bulgarian)", + "gl": "Galego (Galician)", + "id": "Bahasa Indonesia (Indonesian)", + "ur": "اردو (Urdu)", + "withSystem": { + "label": "Use the system settings for language" + } + }, + "appearance": "Appearance", + "darkMode": { + "label": "Dark Mode", + "light": "Light", + "dark": "Dark", + "withSystem": { + "label": "Use the system settings for light or dark mode" + } + }, + "withSystem": "System", + "theme": { + "label": "Theme", + "blue": "Blue", + "green": "Green", + "nord": "Nord", + "red": "Red", + "highcontrast": "High Contrast", + "default": "Default" + }, + "help": "Help", + "documentation": { + "title": "Documentation", + "label": "Frigate documentation" + }, + "restart": "Restart Frigate", + "live": { + "title": "Live", + "allCameras": "All Cameras", + "cameras": { + "title": "Cameras", + "count_one": "{{count}} Camera", + "count_other": "{{count}} Cameras" + } + }, + "review": "Review", + "explore": "Explore", + "export": "Export", + "actions": "Actions", + "uiPlayground": "UI Playground", + "features": "Features", + "faceLibrary": "Face Library", + "classification": "Classification", + "chat": "Chat", + "user": { + "title": "User", + "account": "Account", + "current": "Current User: {{user}}", + "anonymous": "anonymous", + "logout": "Logout", + "setPassword": "Set Password" + } + }, + "toast": { + "copyUrlToClipboard": "Copied URL to clipboard.", + "save": { + "title": "Save", + "error": { + "title": "Failed to save config changes: {{errorMessage}}", + "noMessage": "Failed to save config changes" + }, + "success": "Successfully saved config changes." + } + }, + "role": { + "title": "Role", + "admin": "Admin", + "viewer": "Viewer", + "desc": "Admins have full access to all features in the Frigate UI. Viewers are limited to viewing cameras, review items, and historical footage in the UI." + }, + "pagination": { + "label": "pagination", + "previous": { + "title": "Previous", + "label": "Go to previous page" + }, + "next": { + "title": "Next", + "label": "Go to next page" + }, + "more": "More pages" + }, + "accessDenied": { + "documentTitle": "Access Denied - Frigate", + "title": "Access Denied", + "desc": "You don't have permission to view this page." + }, + "notFound": { + "documentTitle": "Not Found - Frigate", + "title": "404", + "desc": "Page not found" + }, + "selectItem": "Select {{item}}", + "readTheDocumentation": "Read the documentation", + "information": { + "pixels": "{{area}}px" + }, + "no_items": "No items", + "validation_errors": "Validation Errors", + "credentialField": { + "savedPlaceholder": "Saved — leave blank to keep current" + } +} diff --git a/web/public/locales/en/components/auth.json b/web/public/locales/en/components/auth.json new file mode 100644 index 0000000..56b7500 --- /dev/null +++ b/web/public/locales/en/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Username", + "password": "Password", + "login": "Login", + "firstTimeLogin": "Trying to log in for the first time? Credentials are printed in the Frigate logs.", + "errors": { + "usernameRequired": "Username is required", + "passwordRequired": "Password is required", + "rateLimit": "Exceeded rate limit. Try again later.", + "loginFailed": "Login failed", + "unknownError": "Unknown error. Check logs.", + "webUnknownError": "Unknown error. Check console logs." + } + } +} diff --git a/web/public/locales/en/components/camera.json b/web/public/locales/en/components/camera.json new file mode 100644 index 0000000..3f6afc6 --- /dev/null +++ b/web/public/locales/en/components/camera.json @@ -0,0 +1,90 @@ +{ + "group": { + "label": "Camera Groups", + "add": "Add Camera Group", + "showAll": "Show all camera groups", + "showLess": "Show less", + "edit": "Edit Camera Group", + "editGroups": "Edit Camera Groups", + "delete": { + "label": "Delete Camera Group", + "confirm": { + "title": "Confirm Delete", + "desc": "Are you sure you want to delete the camera group {{name}}?" + } + }, + "name": { + "label": "Name", + "placeholder": "Enter a name…", + "errorMessage": { + "mustLeastCharacters": "Camera group name must be at least 2 characters.", + "exists": "Camera group name already exists.", + "nameMustNotPeriod": "Camera group name must not contain a period.", + "invalid": "Invalid camera group name." + } + }, + "cameras": { + "label": "Cameras", + "desc": "Select cameras for this group." + }, + "icon": "Icon", + "success": "Camera group ({{name}}) has been saved.", + "camera": { + "birdseye": "Birdseye", + "setting": { + "label": "Camera Streaming Settings", + "title": "{{cameraName}} Streaming Settings", + "desc": "Change the live streaming options for this camera group's dashboard. These settings are device/browser-specific.", + "audioIsAvailable": "Audio is available for this stream", + "audioIsUnavailable": "Audio is unavailable for this stream", + "audio": { + "tips": { + "title": "Audio must be output from your camera and configured in go2rtc for this stream." + } + }, + "stream": "Stream", + "placeholder": "Choose a stream", + "streamMethod": { + "label": "Streaming Method", + "placeholder": "Choose a streaming method", + "method": { + "noStreaming": { + "label": "No Streaming", + "desc": "Camera images will only update once per minute and no live streaming will occur." + }, + "smartStreaming": { + "label": "Smart Streaming (recommended)", + "desc": "Smart streaming will update your camera image once per minute when no detectable activity is occurring to conserve bandwidth and resources. When activity is detected, the image seamlessly switches to a live stream." + }, + "continuousStreaming": { + "label": "Continuous Streaming", + "desc": { + "title": "Camera image will always be a live stream when visible on the dashboard, even if no activity is being detected.", + "warning": "Continuous streaming may cause high bandwidth usage and performance issues. Use with caution." + } + } + } + }, + "compatibilityMode": { + "label": "Compatibility mode", + "desc": "Enable this option only if your camera's live stream is displaying color artifacts and has a diagonal line on the right side of the image." + } + } + } + }, + "debug": { + "options": { + "label": "Settings", + "title": "Options", + "showOptions": "Show Options", + "hideOptions": "Hide Options" + }, + "boundingBox": "Bounding Box", + "timestamp": "Timestamp", + "zones": "Zones", + "mask": "Mask", + "motion": "Motion", + "regions": "Regions", + "paths": "Paths" + } +} diff --git a/web/public/locales/en/components/dialog.json b/web/public/locales/en/components/dialog.json new file mode 100644 index 0000000..dcb4a23 --- /dev/null +++ b/web/public/locales/en/components/dialog.json @@ -0,0 +1,198 @@ +{ + "restart": { + "title": "Are you sure you want to restart Frigate?", + "description": "This will briefly stop Frigate while it restarts.", + "button": "Restart", + "restarting": { + "title": "Frigate is Restarting", + "content": "This page will reload in {{countdown}} seconds.", + "button": "Force Reload Now" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Submit To Frigate+", + "desc": "Objects in locations you want to avoid are not false positives. Submitting them as false positives will confuse the model." + }, + "review": { + "question": { + "label": "Confirm this label for Frigate Plus", + "ask_a": "Is this object a {{label}}?", + "ask_an": "Is this object an {{label}}?", + "ask_full": "Is this object a {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Submitted" + }, + "toast": { + "error": "Failed to submit to Frigate+. Please check your network connection and try again." + } + } + }, + "video": { + "viewInHistory": "View in History" + } + }, + "export": { + "time": { + "fromTimeline": "Select from Timeline", + "lastHour_one": "Last Hour", + "lastHour_other": "Last {{count}} Hours", + "custom": "Custom", + "start": { + "title": "Start Time", + "label": "Select Start Time" + }, + "end": { + "title": "End Time", + "label": "Select End Time" + } + }, + "name": { + "placeholder": "Name the Export" + }, + "case": { + "newCaseOption": "Create new case", + "newCaseNamePlaceholder": "New case name", + "newCaseDescriptionPlaceholder": "Case description", + "label": "Case", + "nonAdminHelp": "A new case will be created for these exports.", + "placeholder": "Select a case" + }, + "select": "Select", + "export": "Export", + "queueing": "Queueing Export...", + "selectOrExport": "Select or Export", + "tabs": { + "export": "Single Camera", + "multiCamera": "Multi-Camera" + }, + "multiCamera": { + "timeRange": "Time range", + "selectFromTimeline": "Select from Timeline", + "cameraSelection": "Cameras", + "cameraSelectionHelp": "Cameras with tracked objects in this time range are pre-selected", + "searchOrSelectGroup": "Search, or select a camera group...", + "selectAll": "Select all cameras", + "clearSelection": "Clear selection", + "selectWithActivity": "Cameras with tracked objects", + "selectGroup": "Select group", + "noMatchingCameras": "No cameras match your search", + "selectedCount": "{{selected}} / {{total}} selected", + "checkingActivity": "Checking camera activity...", + "noCameras": "No cameras available", + "detectionCount_one": "1 tracked object", + "detectionCount_other": "{{count}} tracked objects", + "nameLabel": "Export name", + "namePlaceholder": "Optional base name for these exports", + "queueingButton": "Queueing Exports...", + "exportButton_one": "Export 1 Camera", + "exportButton_other": "Export {{count}} Cameras" + }, + "multi": { + "title_one": "Export 1 review", + "title_other": "Export {{count}} reviews", + "description": "Export each selected review. All exports will be grouped under a single case.", + "descriptionNoCase": "Export each selected review.", + "caseNamePlaceholder": "Review export - {{date}}", + "exportButton_one": "Export 1 review", + "exportButton_other": "Export {{count}} reviews", + "exportingButton": "Exporting...", + "toast": { + "started_one": "Started 1 export. Opening the case now.", + "started_other": "Started {{count}} exports. Opening the case now.", + "startedNoCase_one": "Started 1 export.", + "startedNoCase_other": "Started {{count}} exports.", + "partial": "Started {{successful}} of {{total}} exports. Failed: {{failedItems}}", + "failed": "Failed to start {{total}} exports. Failed: {{failedItems}}" + } + }, + "toast": { + "success": "Successfully started export. View the file in the exports page.", + "queued": "Export queued. View progress in the exports page.", + "view": "View", + "batchSuccess_one": "Started 1 export. Opening the case now.", + "batchSuccess_other": "Started {{count}} exports. Opening the case now.", + "batchPartial": "Started {{successful}} of {{total}} exports. Failed cameras: {{failedCameras}}", + "batchFailed": "Failed to start {{total}} exports. Failed cameras: {{failedCameras}}", + "batchQueuedSuccess_one": "Queued 1 export. Opening the case now.", + "batchQueuedSuccess_other": "Queued {{count}} exports. Opening the case now.", + "batchQueuedPartial": "Queued {{successful}} of {{total}} exports. Failed cameras: {{failedCameras}}", + "batchQueueFailed": "Failed to queue {{total}} exports. Failed cameras: {{failedCameras}}", + "error": { + "failed": "Failed to queue export: {{error}}", + "endTimeMustAfterStartTime": "End time must be after start time", + "noValidTimeSelected": "No valid time range selected" + } + }, + "fromTimeline": { + "saveExport": "Save Export", + "queueingExport": "Queueing Export...", + "previewExport": "Preview Export", + "useThisRange": "Use This Range" + } + }, + "streaming": { + "label": "Stream", + "restreaming": { + "disabled": "Restreaming is not enabled for this camera.", + "desc": { + "title": "Set up go2rtc for additional live view options and audio for this camera." + } + }, + "showStats": { + "label": "Show stream stats", + "desc": "Enable this option to show stream statistics as an overlay on the camera feed." + }, + "debugView": "Debug View" + }, + "search": { + "saveSearch": { + "label": "Save Search", + "desc": "Provide a name for this saved search.", + "placeholder": "Enter a name for your search", + "overwrite": "{{searchName}} already exists. Saving will overwrite the existing value.", + "success": "Search ({{searchName}}) has been saved.", + "button": { + "save": { + "label": "Save this search" + } + } + } + }, + "recording": { + "shareTimestamp": { + "label": "Share Timestamp", + "title": "Share Timestamp", + "description": "Share a timestamped URL of current player position or choose a custom timestamp. Note that this is not a public share URL and is only accessible to users with access to Frigate and this camera.", + "custom": "Custom Timestamp", + "button": "Share Timestamp URL", + "shareTitle": "Frigate Review Timestamp: {{camera}}" + }, + "confirmDelete": { + "title": "Confirm Delete", + "desc": { + "selected": "Are you sure you want to delete all recorded video associated with this review item?

    Hold the Shift key to bypass this dialog in the future." + }, + "toast": { + "success": "Video footage associated with the selected review items has been deleted successfully.", + "error": "Failed to delete: {{error}}" + } + }, + "button": { + "export": "Export", + "markAsReviewed": "Mark as reviewed", + "markAsUnreviewed": "Mark as unreviewed", + "deleteNow": "Delete Now" + } + }, + "imagePicker": { + "selectImage": "Select a tracked object's thumbnail", + "unknownLabel": "Saved Trigger Image", + "search": { + "placeholder": "Search by label or sub label..." + }, + "noImages": "No thumbnails found for this camera" + } +} diff --git a/web/public/locales/en/components/filter.json b/web/public/locales/en/components/filter.json new file mode 100644 index 0000000..e9ae5c7 --- /dev/null +++ b/web/public/locales/en/components/filter.json @@ -0,0 +1,138 @@ +{ + "filter": "Filter", + "classes": { + "label": "Classes", + "all": { "title": "All Classes" }, + "count_one": "{{count}} Class", + "count_other": "{{count}} Classes" + }, + "labels": { + "label": "Labels", + "all": { + "title": "All Labels", + "short": "Labels" + }, + "count_one": "{{count}} Label", + "count_other": "{{count}} Labels" + }, + "zones": { + "label": "Zones", + "all": { + "title": "All Zones", + "short": "Zones" + } + }, + "dates": { + "selectPreset": "Select a Preset…", + "all": { + "title": "All Dates", + "short": "Dates" + } + }, + "more": "More Filters", + "reset": { + "label": "Reset filters to default values" + }, + "timeRange": "Time Range", + "subLabels": { + "label": "Sub Labels", + "all": "All Sub Labels" + }, + "attributes": { + "label": "Classification Attributes", + "all": "All Attributes" + }, + "score": "Score", + "estimatedSpeed": "Estimated Speed ({{unit}})", + "features": { + "label": "Features", + "hasSnapshot": "Has a snapshot", + "hasVideoClip": "Has a video clip", + "submittedToFrigatePlus": { + "label": "Submitted to Frigate+", + "tips": "You must first filter on tracked objects that have a snapshot.

    Tracked objects without a snapshot cannot be submitted to Frigate+." + } + }, + "sort": { + "label": "Sort", + "dateAsc": "Date (Ascending)", + "dateDesc": "Date (Descending)", + "scoreAsc": "Object Score (Ascending)", + "scoreDesc": "Object Score (Descending)", + "speedAsc": "Estimated Speed (Ascending)", + "speedDesc": "Estimated Speed (Descending)", + "relevance": "Relevance" + }, + "cameras": { + "label": "Cameras Filter", + "all": { + "title": "All Cameras", + "short": "Cameras" + } + }, + "review": { + "showReviewed": "Show Reviewed" + }, + "motion": { + "showMotionOnly": "Show Motion Only" + }, + "explore": { + "settings": { + "title": "Settings", + "defaultView": { + "title": "Default View", + "desc": "When no filters are selected, display a summary of the most recent tracked objects per label, or display an unfiltered grid.", + "summary": "Summary", + "unfilteredGrid": "Unfiltered Grid" + }, + "gridColumns": { + "title": "Grid Columns", + "desc": "Select the number of columns in the grid view." + }, + "searchSource": { + "label": "Search Source", + "desc": "Choose whether to search the thumbnails or descriptions of your tracked objects.", + "options": { + "thumbnailImage": "Thumbnail Image", + "description": "Description" + } + } + }, + "date": { + "selectDateBy": { + "label": "Select a date to filter by" + } + } + }, + "logSettings": { + "label": "Filter log level", + "filterBySeverity": "Filter logs by severity", + "loading": { + "title": "Loading", + "desc": "When the log pane is scrolled to the bottom, new logs automatically stream as they are added." + }, + "disableLogStreaming": "Disable log streaming", + "allLogs": "All logs" + }, + "trackedObjectDelete": { + "title": "Confirm Delete", + "desc": "Deleting these {{objectLength}} tracked objects removes the snapshot, any saved embeddings, and any associated object lifecycle entries. Recorded footage of these tracked objects in History view will NOT be deleted.

    Are you sure you want to proceed?

    Hold the Shift key to bypass this dialog in the future.", + "toast": { + "success": "Tracked objects deleted successfully.", + "error": "Failed to delete tracked objects: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filter by zone mask" + }, + "recognizedLicensePlates": { + "title": "Recognized License Plates", + "loadFailed": "Failed to load recognized license plates.", + "loading": "Loading recognized license plates…", + "placeholder": "Type to search license plates…", + "noLicensePlatesFound": "No license plates found.", + "selectPlatesFromList": "Select one or more plates from the list.", + "selectAll": "Select all", + "clearAll": "Clear all" + } +} diff --git a/web/public/locales/en/components/icons.json b/web/public/locales/en/components/icons.json new file mode 100644 index 0000000..e7a35a7 --- /dev/null +++ b/web/public/locales/en/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Select an icon", + "search": { + "placeholder": "Search for an icon…" + } + } +} diff --git a/web/public/locales/en/components/input.json b/web/public/locales/en/components/input.json new file mode 100644 index 0000000..7a9e359 --- /dev/null +++ b/web/public/locales/en/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Download Video", + "toast": { + "success": "Your review item video has started downloading." + } + } + } +} diff --git a/web/public/locales/en/components/player.json b/web/public/locales/en/components/player.json new file mode 100644 index 0000000..011baaa --- /dev/null +++ b/web/public/locales/en/components/player.json @@ -0,0 +1,52 @@ +{ + "noRecordingsFoundForThisTime": "No recordings found for this time", + "noPreviewFound": "No Preview Found", + "noPreviewFoundFor": "No Preview Found for {{cameraName}}", + "submitFrigatePlus": { + "title": "Submit this frame to Frigate+?", + "submit": "Submit", + "previewError": "Could not load snapshot preview. The recording may not be available at this time." + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 or greater is required for this live stream type.", + "streamOffline": { + "title": "Stream Offline", + "desc": "No frames have been received on the {{cameraName}} detect stream, check error logs" + }, + "cameraOff": "Camera is off", + "stats": { + "streamType": { + "title": "Stream Type:", + "short": "Type" + }, + "bandwidth": { + "title": "Bandwidth:", + "short": "Bandwidth" + }, + "latency": { + "title": "Latency:", + "value": "{{seconds}} seconds", + "short": { + "title": "Latency", + "value": "{{seconds}} sec" + } + }, + "totalFrames": "Total Frames:", + "droppedFrames": { + "title": "Dropped Frames:", + "short": { + "title": "Dropped", + "value": "{{droppedFrames}} frames" + } + }, + "decodedFrames": "Decoded Frames:", + "droppedFrameRate": "Dropped Frame Rate:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Successfully submitted frame to Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Failed to submit frame to Frigate+" + } + } +} diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json new file mode 100644 index 0000000..2ddc30b --- /dev/null +++ b/web/public/locales/en/config/cameras.json @@ -0,0 +1,956 @@ +{ + "label": "CameraConfig", + "name": { + "label": "Camera name", + "description": "Camera name is required" + }, + "friendly_name": { + "label": "Friendly name", + "description": "Camera friendly name used in the Frigate UI" + }, + "enabled": { + "label": "Enabled", + "description": "Enabled" + }, + "audio": { + "label": "Audio detection", + "description": "Settings for audio-based event detection for this camera.", + "enabled": { + "label": "Enable audio detection", + "description": "Enable or disable audio event detection for this camera." + }, + "max_not_heard": { + "label": "End timeout", + "description": "Amount of seconds without the configured audio type before the audio event is ended." + }, + "min_volume": { + "label": "Minimum volume", + "description": "Minimum RMS volume threshold required to run audio detection; lower values increase sensitivity (e.g., 200 high, 500 medium, 1000 low)." + }, + "listen": { + "label": "Listen types", + "description": "List of audio event types to detect (for example: bark, fire_alarm, speech, yell)." + }, + "filters": { + "label": "Audio filters", + "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.", + "threshold": { + "label": "Minimum audio confidence", + "description": "Minimum confidence threshold for the audio event to be counted." + } + }, + "enabled_in_config": { + "label": "Original audio state", + "description": "Indicates whether audio detection was originally enabled in the static config file." + }, + "num_threads": { + "label": "Detection threads", + "description": "Number of threads to use for audio detection processing." + } + }, + "audio_transcription": { + "label": "Audio transcription", + "description": "Settings for live and speech audio transcription used for events and live captions.", + "enabled": { + "label": "Enable transcription", + "description": "Enable or disable manually triggered audio event transcription." + }, + "enabled_in_config": { + "label": "Original transcription state" + }, + "live_enabled": { + "label": "Live transcription", + "description": "Enable streaming live transcription for audio as it is received." + } + }, + "birdseye": { + "label": "Birdseye", + "description": "Settings for the Birdseye composite view that composes multiple camera feeds into a single layout.", + "enabled": { + "label": "Enable Birdseye", + "description": "Enable or disable the Birdseye view feature." + }, + "mode": { + "label": "Tracking mode", + "description": "Mode for including cameras in Birdseye: 'objects', 'motion', or 'continuous'." + }, + "order": { + "label": "Position", + "description": "Numeric position controlling the camera's ordering in the Birdseye layout." + } + }, + "detect": { + "label": "Object Detection", + "description": "Settings for the detection/detect role used to run object detection and initialize trackers.", + "enabled": { + "label": "Enable object detection", + "description": "Enable or disable object detection for this camera." + }, + "height": { + "label": "Detect height", + "description": "Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution." + }, + "width": { + "label": "Detect width", + "description": "Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution." + }, + "fps": { + "label": "Detect FPS", + "description": "Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects)." + }, + "min_initialized": { + "label": "Minimum initialization frames", + "description": "Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2." + }, + "max_disappeared": { + "label": "Maximum disappeared frames", + "description": "Number of frames without a detection before a tracked object is considered gone." + }, + "stationary": { + "label": "Stationary objects config", + "description": "Settings to detect and manage objects that remain stationary for a period of time.", + "interval": { + "label": "Stationary interval", + "description": "How often (in frames) to run a detection check to confirm a stationary object." + }, + "threshold": { + "label": "Stationary threshold", + "description": "Number of frames with no position change required to mark an object as stationary." + }, + "max_frames": { + "label": "Max frames", + "description": "Limits how long stationary objects are tracked before being discarded.", + "default": { + "label": "Default max frames", + "description": "Default maximum frames to track a stationary object before stopping." + }, + "objects": { + "label": "Object max frames", + "description": "Per-object overrides for maximum frames to track stationary objects." + } + }, + "classifier": { + "label": "Enable visual classifier", + "description": "Use a visual classifier to detect truly stationary objects even when bounding boxes jitter." + } + }, + "annotation_offset": { + "label": "Annotation offset", + "description": "Milliseconds to shift detect annotations to better align timeline bounding boxes with recordings; can be positive or negative." + } + }, + "face_recognition": { + "label": "Face recognition", + "description": "Settings for face detection and recognition for this camera.", + "enabled": { + "label": "Enable face recognition", + "description": "Enable or disable face recognition." + }, + "min_area": { + "label": "Minimum face area", + "description": "Minimum area (pixels) of a detected face box required to attempt recognition." + } + }, + "ffmpeg": { + "label": "Streams (FFmpeg)", + "description": "Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.", + "path": { + "label": "FFmpeg path", + "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\")." + }, + "global_args": { + "label": "FFmpeg global arguments", + "description": "Global arguments passed to FFmpeg processes." + }, + "hwaccel_args": { + "label": "Hardware acceleration arguments", + "description": "Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended." + }, + "input_args": { + "label": "Input arguments", + "description": "Input arguments applied to FFmpeg input streams." + }, + "output_args": { + "label": "Output arguments", + "description": "Default output arguments used for different FFmpeg roles such as detect and record.", + "detect": { + "label": "Detect output arguments", + "description": "Default output arguments for detect role streams." + }, + "record": { + "label": "Record output arguments", + "description": "Default output arguments for record role streams." + } + }, + "retry_interval": { + "label": "FFmpeg retry time", + "description": "Seconds to wait before attempting to reconnect a camera stream after failure. Default is 10." + }, + "apple_compatibility": { + "label": "Apple compatibility", + "description": "Enable HEVC tagging for better Apple player compatibility when recording H.265." + }, + "gpu": { + "label": "GPU index", + "description": "Default GPU index used for hardware acceleration if available." + }, + "inputs": { + "label": "Camera inputs", + "description": "List of input stream definitions (paths and roles) for this camera.", + "path": { + "label": "Input path", + "description": "Camera input stream URL or path." + }, + "roles": { + "label": "Input roles", + "description": "Roles for this input stream." + }, + "global_args": { + "label": "FFmpeg global arguments", + "description": "FFmpeg global arguments for this input stream." + }, + "hwaccel_args": { + "label": "Hardware acceleration arguments", + "description": "Hardware acceleration arguments for this input stream." + }, + "input_args": { + "label": "Input arguments", + "description": "Input arguments specific to this stream." + } + } + }, + "live": { + "label": "Live playback", + "description": "Settings used by the Web UI to control live stream selection, resolution and quality.", + "streams": { + "label": "Live stream names", + "description": "Mapping of configured stream names to restream/go2rtc names used for live playback." + }, + "height": { + "label": "Live height", + "description": "Height (pixels) to render the jsmpeg live stream in the Web UI; must be <= detect stream height." + }, + "quality": { + "label": "Live quality", + "description": "Encoding quality for the jsmpeg stream (1 highest, 31 lowest)." + } + }, + "lpr": { + "label": "License Plate Recognition", + "description": "License plate recognition settings including detection thresholds, formatting, and known plates.", + "enabled": { + "label": "Enable LPR", + "description": "Enable or disable LPR on this camera." + }, + "expire_time": { + "label": "Expire seconds", + "description": "Time in seconds after which an unseen plate is expired from the tracker (for dedicated LPR cameras only)." + }, + "min_area": { + "label": "Minimum plate area", + "description": "Minimum plate area (pixels) required to attempt recognition." + }, + "enhancement": { + "label": "Enhancement level", + "description": "Enhancement level (0-10) to apply to plate crops prior to OCR; higher values may not always improve results, levels above 5 may only work with night time plates and should be used with caution." + } + }, + "motion": { + "label": "Motion detection", + "description": "Default motion detection settings for this camera.", + "enabled": { + "label": "Enable motion detection", + "description": "Enable or disable motion detection for this camera." + }, + "threshold": { + "label": "Motion threshold", + "description": "Pixel difference threshold used by the motion detector; higher values reduce sensitivity (range 1-255)." + }, + "lightning_threshold": { + "label": "Lightning threshold", + "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events." + }, + "skip_motion_threshold": { + "label": "Skip motion threshold", + "description": "If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature." + }, + "improve_contrast": { + "label": "Improve contrast", + "description": "Apply contrast improvement to frames before motion analysis to help detection." + }, + "contour_area": { + "label": "Contour area", + "description": "Minimum contour area in pixels required for a motion contour to be counted." + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "Alpha blending factor used in frame differencing for motion calculation." + }, + "frame_alpha": { + "label": "Frame alpha", + "description": "Alpha value used when blending frames for motion preprocessing." + }, + "frame_height": { + "label": "Frame height", + "description": "Height in pixels to scale frames to when computing motion." + }, + "mask": { + "label": "Mask coordinates", + "description": "Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas." + }, + "mqtt_off_delay": { + "label": "MQTT off delay", + "description": "Seconds to wait after last motion before publishing an MQTT 'off' state." + }, + "enabled_in_config": { + "label": "Original motion state", + "description": "Indicates whether motion detection was enabled in the original static configuration." + }, + "raw_mask": { + "label": "Raw Mask" + } + }, + "objects": { + "label": "Objects", + "description": "Object tracking defaults including which labels to track and per-object filters.", + "track": { + "label": "Objects to track", + "description": "List of object labels to track for this camera." + }, + "filters": { + "label": "Object filters", + "description": "Filters applied to detected objects to reduce false positives (area, ratio, confidence).", + "min_area": { + "label": "Minimum object area", + "description": "Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum object area", + "description": "Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum aspect ratio", + "description": "Minimum width/height ratio required for the bounding box to qualify." + }, + "max_ratio": { + "label": "Maximum aspect ratio", + "description": "Maximum width/height ratio allowed for the bounding box to qualify." + }, + "threshold": { + "label": "Confidence threshold", + "description": "Average detection confidence threshold required for the object to be considered a true positive." + }, + "min_score": { + "label": "Minimum confidence", + "description": "Minimum single-frame detection confidence required for the object to be counted." + }, + "mask": { + "label": "Filter mask", + "description": "Polygon coordinates defining where this filter applies within the frame." + }, + "raw_mask": { + "label": "Raw Mask" + } + }, + "mask": { + "label": "Object mask", + "description": "Mask polygon used to prevent object detection in specified areas." + }, + "raw_mask": { + "label": "Raw Mask" + }, + "genai": { + "label": "GenAI object config", + "description": "GenAI options for describing tracked objects and sending frames for generation.", + "enabled": { + "label": "Enable GenAI", + "description": "Enable GenAI generation of descriptions for tracked objects by default." + }, + "use_snapshot": { + "label": "Use snapshots", + "description": "Use object snapshots instead of thumbnails for GenAI description generation." + }, + "prompt": { + "label": "Caption prompt", + "description": "Default prompt template used when generating descriptions with GenAI." + }, + "object_prompts": { + "label": "Object prompts", + "description": "Per-object prompts to customize GenAI outputs for specific labels." + }, + "objects": { + "label": "GenAI objects", + "description": "List of object labels to send to GenAI by default." + }, + "required_zones": { + "label": "Required zones", + "description": "Zones that must be entered for objects to qualify for GenAI description generation." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails", + "description": "Save thumbnails sent to GenAI for debugging and review." + }, + "send_triggers": { + "label": "GenAI triggers", + "description": "Defines when frames should be sent to GenAI (on end, after updates, etc.).", + "tracked_object_end": { + "label": "Send on end", + "description": "Send a request to GenAI when the tracked object ends." + }, + "after_significant_updates": { + "label": "Early GenAI trigger", + "description": "Send a request to GenAI after a specified number of significant updates for the tracked object." + } + }, + "enabled_in_config": { + "label": "Original GenAI state", + "description": "Indicates whether GenAI was enabled in the original static config." + } + } + }, + "record": { + "label": "Recording", + "description": "Recording and retention settings for this camera.", + "enabled": { + "label": "Enable recording", + "description": "Enable or disable recording for this camera." + }, + "expire_interval": { + "label": "Record cleanup interval", + "description": "Minutes between cleanup passes that remove expired recording segments." + }, + "continuous": { + "label": "Continuous retention", + "description": "Number of days to retain recordings regardless of tracked objects or motion. Set to 0 if you only want to retain recordings of alerts and detections.", + "days": { + "label": "Retention days", + "description": "Days to retain recordings." + } + }, + "motion": { + "label": "Motion retention", + "description": "Number of days to retain recordings triggered by motion regardless of tracked objects. Set to 0 if you only want to retain recordings of alerts and detections.", + "days": { + "label": "Retention days", + "description": "Days to retain recordings." + } + }, + "detections": { + "label": "Detection retention", + "description": "Recording retention settings for detection events including pre/post capture durations.", + "pre_capture": { + "label": "Pre-capture seconds", + "description": "Number of seconds before the detection event to include in the recording." + }, + "post_capture": { + "label": "Post-capture seconds", + "description": "Number of seconds after the detection event to include in the recording." + }, + "retain": { + "label": "Event retention", + "description": "Retention settings for recordings of detection events.", + "days": { + "label": "Retention days", + "description": "Number of days to retain recordings of detection events." + }, + "mode": { + "label": "Retention mode", + "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects)." + } + } + }, + "alerts": { + "label": "Alert retention", + "description": "Recording retention settings for alert events including pre/post capture durations.", + "pre_capture": { + "label": "Pre-capture seconds", + "description": "Number of seconds before the detection event to include in the recording." + }, + "post_capture": { + "label": "Post-capture seconds", + "description": "Number of seconds after the detection event to include in the recording." + }, + "retain": { + "label": "Event retention", + "description": "Retention settings for recordings of detection events.", + "days": { + "label": "Retention days", + "description": "Number of days to retain recordings of detection events." + }, + "mode": { + "label": "Retention mode", + "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects)." + } + } + }, + "export": { + "label": "Export config", + "description": "Settings used when exporting recordings such as timelapse and hardware acceleration.", + "hwaccel_args": { + "label": "Export hwaccel args", + "description": "Hardware acceleration args to use for export/transcode operations." + }, + "max_concurrent": { + "label": "Maximum concurrent exports", + "description": "Maximum number of export jobs to process at the same time." + }, + "chapters": { + "label": "Chapter metadata to embed in exported recordings" + } + }, + "preview": { + "label": "Preview config", + "description": "Settings controlling the quality of recording previews shown in the UI.", + "quality": { + "label": "Preview quality", + "description": "Preview quality level (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Original recording state", + "description": "Indicates whether recording was enabled in the original static configuration." + } + }, + "review": { + "label": "Review", + "description": "Settings that control alerts, detections, and GenAI review summaries used by the UI and storage for this camera.", + "alerts": { + "label": "Alerts config", + "description": "Settings for which tracked objects generate alerts and how alerts are retained.", + "enabled": { + "label": "Enable alerts", + "description": "Enable or disable alert generation for this camera." + }, + "labels": { + "label": "Alert labels", + "description": "List of object labels that qualify as alerts (for example: car, person)." + }, + "required_zones": { + "label": "Required zones", + "description": "Zones that an object must enter to be considered an alert; leave empty to allow any zone." + }, + "enabled_in_config": { + "label": "Original alerts state", + "description": "Tracks whether alerts were originally enabled in the static configuration." + }, + "cutoff_time": { + "label": "Alerts cutoff time", + "description": "Seconds to wait after no alert-causing activity before cutting off an alert." + } + }, + "detections": { + "label": "Detections config", + "description": "Settings for which tracked objects generate detections (non-alert) and how detections are retained.", + "enabled": { + "label": "Enable detections", + "description": "Enable or disable detection events for this camera." + }, + "labels": { + "label": "Detection labels", + "description": "List of object labels that qualify as detection events." + }, + "required_zones": { + "label": "Required zones", + "description": "Zones that an object must enter to be considered a detection; leave empty to allow any zone." + }, + "cutoff_time": { + "label": "Detections cutoff time", + "description": "Seconds to wait after no detection-causing activity before cutting off a detection." + }, + "enabled_in_config": { + "label": "Original detections state", + "description": "Tracks whether detections were originally enabled in the static configuration." + } + }, + "genai": { + "label": "GenAI config", + "description": "Controls use of generative AI for producing descriptions and summaries of review items.", + "enabled": { + "label": "Enable GenAI descriptions", + "description": "Enable or disable GenAI-generated descriptions and summaries for review items." + }, + "alerts": { + "label": "Enable GenAI for alerts", + "description": "Use GenAI to generate descriptions for alert items." + }, + "detections": { + "label": "Enable GenAI for detections", + "description": "Use GenAI to generate descriptions for detection items." + }, + "image_source": { + "label": "Review image source", + "description": "Source of images sent to GenAI ('preview' or 'recordings'); 'recordings' uses higher quality frames but more tokens." + }, + "additional_concerns": { + "label": "Additional concerns", + "description": "A list of additional concerns or notes the GenAI should consider when evaluating activity on this camera." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails", + "description": "Save thumbnails that are sent to the GenAI provider for debugging and review." + }, + "enabled_in_config": { + "label": "Original GenAI state", + "description": "Tracks whether GenAI review was originally enabled in the static configuration." + }, + "preferred_language": { + "label": "Preferred language", + "description": "Preferred language to request from the GenAI provider for generated responses." + }, + "activity_context_prompt": { + "label": "Activity context prompt", + "description": "Custom prompt describing what is and is not suspicious activity to provide context for GenAI summaries." + } + } + }, + "semantic_search": { + "label": "Semantic Search", + "description": "Settings for semantic search which builds and queries object embeddings to find similar items.", + "triggers": { + "label": "Triggers", + "description": "Actions and matching criteria for camera-specific semantic search triggers.", + "friendly_name": { + "label": "Friendly name", + "description": "Optional friendly name displayed in the UI for this trigger." + }, + "enabled": { + "label": "Enable this trigger", + "description": "Enable or disable this semantic search trigger." + }, + "type": { + "label": "Trigger type", + "description": "Type of trigger: 'thumbnail' (match against image) or 'description' (match against text)." + }, + "data": { + "label": "Trigger content", + "description": "Text phrase or thumbnail ID to match against tracked objects." + }, + "threshold": { + "label": "Trigger threshold", + "description": "Minimum similarity score (0-1) required to activate this trigger." + }, + "actions": { + "label": "Trigger actions", + "description": "List of actions to execute when trigger matches (notification, sub_label, attribute)." + } + } + }, + "snapshots": { + "label": "Snapshots", + "description": "Settings for API-generated snapshots of tracked objects for this camera.", + "enabled": { + "label": "Enable snapshots", + "description": "Enable or disable saving snapshots for this camera." + }, + "timestamp": { + "label": "Timestamp overlay", + "description": "Overlay a timestamp on snapshots from API." + }, + "bounding_box": { + "label": "Bounding box overlay", + "description": "Draw bounding boxes for tracked objects on snapshots from API." + }, + "crop": { + "label": "Crop snapshot", + "description": "Crop snapshots from API to the detected object's bounding box." + }, + "required_zones": { + "label": "Required zones", + "description": "Zones an object must enter for a snapshot to be saved." + }, + "height": { + "label": "Snapshot height", + "description": "Height (pixels) to resize snapshots from API to; leave empty to preserve original size." + }, + "retain": { + "label": "Snapshot retention", + "description": "Retention settings for snapshots including default days and per-object overrides.", + "default": { + "label": "Default retention", + "description": "Default number of days to retain snapshots." + }, + "objects": { + "label": "Object retention", + "description": "Per-object overrides for snapshot retention days." + } + }, + "quality": { + "label": "Snapshot quality", + "description": "Encode quality for saved snapshots (0-100)." + } + }, + "timestamp_style": { + "label": "Timestamp style", + "description": "Styling options for timestamps applied to snapshots and Debug view.", + "position": { + "label": "Timestamp position", + "description": "Position of the timestamp on the image (tl/tr/bl/br)." + }, + "format": { + "label": "Timestamp format", + "description": "Datetime format string used for timestamps (Python datetime format codes)." + }, + "color": { + "label": "Timestamp color", + "description": "RGB color values for the timestamp text (all values 0-255).", + "red": { + "label": "Red", + "description": "Red component (0-255) for timestamp color." + }, + "green": { + "label": "Green", + "description": "Green component (0-255) for timestamp color." + }, + "blue": { + "label": "Blue", + "description": "Blue component (0-255) for timestamp color." + } + }, + "thickness": { + "label": "Timestamp thickness", + "description": "Line thickness of the timestamp text." + }, + "effect": { + "label": "Timestamp effect", + "description": "Visual effect for the timestamp text (none, solid, shadow)." + } + }, + "best_image_timeout": { + "label": "Best image timeout", + "description": "How long to wait for the image with the highest confidence score." + }, + "mqtt": { + "label": "MQTT", + "description": "MQTT image publishing settings.", + "enabled": { + "label": "Send image", + "description": "Enable publishing image snapshots for objects to MQTT topics for this camera." + }, + "timestamp": { + "label": "Add timestamp", + "description": "Overlay a timestamp on images published to MQTT." + }, + "bounding_box": { + "label": "Add bounding box", + "description": "Draw bounding boxes on images published over MQTT." + }, + "crop": { + "label": "Crop image", + "description": "Crop images published to MQTT to the detected object's bounding box." + }, + "height": { + "label": "Image height", + "description": "Height (pixels) to resize images published over MQTT." + }, + "required_zones": { + "label": "Required zones", + "description": "Zones that an object must enter for an MQTT image to be published." + }, + "quality": { + "label": "JPEG quality", + "description": "JPEG quality for images published to MQTT (0-100)." + } + }, + "notifications": { + "label": "Notifications", + "description": "Settings to enable and control notifications for this camera.", + "enabled": { + "label": "Enable notifications", + "description": "Enable or disable notifications for this camera." + }, + "email": { + "label": "Notification email", + "description": "Email address used for push notifications or required by certain notification providers." + }, + "cooldown": { + "label": "Cooldown period", + "description": "Cooldown (seconds) between notifications to avoid spamming recipients." + }, + "enabled_in_config": { + "label": "Original notifications state", + "description": "Indicates whether notifications were enabled in the original static configuration." + } + }, + "onvif": { + "label": "ONVIF", + "description": "ONVIF connection and PTZ autotracking settings for this camera.", + "host": { + "label": "ONVIF host", + "description": "Host (and optional scheme) for the ONVIF service for this camera." + }, + "port": { + "label": "ONVIF port", + "description": "Port number for the ONVIF service." + }, + "user": { + "label": "ONVIF username", + "description": "Username for ONVIF authentication; some devices require admin user for ONVIF." + }, + "password": { + "label": "ONVIF password", + "description": "Password for ONVIF authentication." + }, + "tls_insecure": { + "label": "Disable TLS verify", + "description": "Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only)." + }, + "profile": { + "label": "ONVIF profile", + "description": "Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically." + }, + "autotracking": { + "label": "Autotracking", + "description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.", + "enabled": { + "label": "Enable Autotracking", + "description": "Enable or disable automatic PTZ camera tracking of detected objects." + }, + "calibrate_on_startup": { + "label": "Calibrate on start", + "description": "Measure PTZ motor speeds on startup to improve tracking accuracy. Frigate will update config with movement_weights after calibration." + }, + "zooming": { + "label": "Zoom mode", + "description": "Control zoom behavior: disabled (pan/tilt only), absolute (most compatible), or relative (concurrent pan/tilt/zoom)." + }, + "zoom_factor": { + "label": "Zoom factor", + "description": "Control zoom level on tracked objects. Lower values keep more scene in view; higher values zoom in closer but may lose tracking. Values between 0.1 and 0.75." + }, + "track": { + "label": "Tracked objects", + "description": "List of object types that should trigger autotracking." + }, + "required_zones": { + "label": "Required zones", + "description": "Objects must enter one of these zones before autotracking begins." + }, + "return_preset": { + "label": "Return preset", + "description": "ONVIF preset name configured in camera firmware to return to after tracking ends." + }, + "timeout": { + "label": "Return timeout", + "description": "Wait this many seconds after losing tracking before returning camera to preset position." + }, + "movement_weights": { + "label": "Movement weights", + "description": "Calibration values automatically generated by camera calibration. Do not modify manually." + }, + "enabled_in_config": { + "label": "Original autotrack state", + "description": "Internal field to track whether autotracking was enabled in configuration." + } + }, + "ignore_time_mismatch": { + "label": "Ignore time mismatch", + "description": "Ignore time synchronization differences between camera and Frigate server for ONVIF communication." + } + }, + "type": { + "label": "Camera type", + "description": "Camera Type" + }, + "ui": { + "label": "Camera UI", + "description": "Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.", + "order": { + "label": "UI order", + "description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later." + }, + "dashboard": { + "label": "Show in UI", + "description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again." + }, + "review": { + "label": "Show in review", + "description": "Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view)." + } + }, + "webui_url": { + "label": "Camera URL", + "description": "URL to visit the camera directly from system page" + }, + "profiles": { + "label": "Profiles", + "description": "Named config profiles with partial overrides that can be activated at runtime." + }, + "zones": { + "label": "Zones", + "description": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.", + "friendly_name": { + "label": "Zone name", + "description": "A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used." + }, + "enabled": { + "label": "Enabled", + "description": "Enable or disable this zone. Disabled zones are ignored at runtime." + }, + "enabled_in_config": { + "label": "Keep track of original state of zone." + }, + "filters": { + "label": "Zone filters", + "description": "Filters to apply to objects within this zone. Used to reduce false positives or restrict which objects are considered present in the zone.", + "min_area": { + "label": "Minimum object area", + "description": "Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum object area", + "description": "Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum aspect ratio", + "description": "Minimum width/height ratio required for the bounding box to qualify." + }, + "max_ratio": { + "label": "Maximum aspect ratio", + "description": "Maximum width/height ratio allowed for the bounding box to qualify." + }, + "threshold": { + "label": "Confidence threshold", + "description": "Average detection confidence threshold required for the object to be considered a true positive." + }, + "min_score": { + "label": "Minimum confidence", + "description": "Minimum single-frame detection confidence required for the object to be counted." + }, + "mask": { + "label": "Filter mask", + "description": "Polygon coordinates defining where this filter applies within the frame." + }, + "raw_mask": { + "label": "Raw Mask" + } + }, + "coordinates": { + "label": "Coordinates", + "description": "Polygon coordinates that define the zone area. Can be a comma-separated string or a list of coordinate strings. Coordinates should be relative (0-1) or absolute (legacy)." + }, + "distances": { + "label": "Real-world distances", + "description": "Optional real-world distances for each side of the zone quadrilateral, used for speed or distance calculations. Must have exactly 4 values if set." + }, + "inertia": { + "label": "Inertia frames", + "description": "Number of consecutive frames an object must be detected in the zone before it is considered present. Helps filter out transient detections." + }, + "loitering_time": { + "label": "Loitering seconds", + "description": "Number of seconds an object must remain in the zone to be considered as loitering. Set to 0 to disable loitering detection." + }, + "speed_threshold": { + "label": "Minimum speed", + "description": "Minimum speed (in real-world units if distances are set) required for an object to be considered present in the zone. Used for speed-based zone triggers." + }, + "objects": { + "label": "Trigger objects", + "description": "List of object types (from labelmap) that can trigger this zone. Can be a string or a list of strings. If empty, all objects are considered." + } + }, + "enabled_in_config": { + "label": "Original camera state", + "description": "Keep track of original state of camera." + } +} diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json new file mode 100644 index 0000000..e3b7c65 --- /dev/null +++ b/web/public/locales/en/config/global.json @@ -0,0 +1,1630 @@ +{ + "version": { + "label": "Current config version", + "description": "Numeric or string version of the active configuration to help detect migrations or format changes." + }, + "safe_mode": { + "label": "Safe mode", + "description": "When enabled, start Frigate in safe mode with reduced features for troubleshooting." + }, + "environment_vars": { + "label": "Environment variables", + "description": "Key/value pairs of environment variables to set for the Frigate process in Home Assistant OS. Non-HAOS users must use Docker environment variable configuration instead." + }, + "logger": { + "label": "Logging", + "description": "Controls default log verbosity and per-component log level overrides.", + "default": { + "label": "Logging level", + "description": "Default global log verbosity (debug, info, warning, error)." + }, + "logs": { + "label": "Per-process log level", + "description": "Per-component log level overrides to increase or decrease verbosity for specific modules." + } + }, + "auth": { + "label": "Authentication", + "description": "Authentication and session-related settings including cookie and rate limit options.", + "enabled": { + "label": "Enable authentication", + "description": "Enable native authentication for the Frigate UI." + }, + "reset_admin_password": { + "label": "Reset admin password", + "description": "If true, reset the admin user's password on startup and print the new password in logs." + }, + "cookie_name": { + "label": "JWT cookie name", + "description": "Name of the cookie used to store the JWT token for native authentication." + }, + "cookie_secure": { + "label": "Secure cookie flag", + "description": "Set the secure flag on the auth cookie; should be true when using TLS." + }, + "session_length": { + "label": "Session length", + "description": "Session duration in seconds for JWT-based sessions." + }, + "refresh_time": { + "label": "Session refresh window", + "description": "When a session is within this many seconds of expiring, refresh it back to full length." + }, + "failed_login_rate_limit": { + "label": "Failed login limits", + "description": "Rate limiting rules for failed login attempts to reduce brute-force attacks." + }, + "trusted_proxies": { + "label": "Trusted proxies", + "description": "List of trusted proxy IPs used when determining client IP for rate limiting." + }, + "hash_iterations": { + "label": "Hash iterations", + "description": "Number of PBKDF2-SHA256 iterations to use when hashing user passwords." + }, + "roles": { + "label": "Role mappings", + "description": "Map roles to camera lists. An empty list grants access to all cameras for the role." + }, + "admin_first_time_login": { + "label": "First-time admin flag", + "description": "When true the UI may show a help link on the login page informing users how to sign in after an admin password reset. " + } + }, + "database": { + "label": "Database", + "description": "Settings for the SQLite database used by Frigate to store tracked object and recording metadata.", + "path": { + "label": "Database path", + "description": "Filesystem path where the Frigate SQLite database file will be stored." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "Settings for the integrated go2rtc restreaming service used for live stream relaying and translation." + }, + "mqtt": { + "label": "MQTT", + "description": "Settings for connecting and publishing telemetry, snapshots, and event details to an MQTT broker.", + "enabled": { + "label": "Enable MQTT", + "description": "Enable or disable MQTT integration for state, events, and snapshots." + }, + "host": { + "label": "MQTT host", + "description": "Hostname or IP address of the MQTT broker." + }, + "port": { + "label": "MQTT port", + "description": "Port of the MQTT broker (usually 1883 for plain MQTT)." + }, + "topic_prefix": { + "label": "Topic prefix", + "description": "MQTT topic prefix for all Frigate topics; must be unique if running multiple instances." + }, + "client_id": { + "label": "Client ID", + "description": "Client identifier used when connecting to the MQTT broker; should be unique per instance." + }, + "stats_interval": { + "label": "Stats interval", + "description": "Interval in seconds for publishing system and camera stats to MQTT." + }, + "user": { + "label": "MQTT username", + "description": "Optional MQTT username; can be provided via environment variables or secrets." + }, + "password": { + "label": "MQTT password", + "description": "Optional MQTT password; can be provided via environment variables or secrets." + }, + "tls_ca_certs": { + "label": "TLS CA certs", + "description": "Path to CA certificate for TLS connections to the broker (for self-signed certs)." + }, + "tls_client_cert": { + "label": "Client cert", + "description": "Client certificate path for TLS mutual authentication; do not set user/password when using client certs." + }, + "tls_client_key": { + "label": "Client key", + "description": "Private key path for the client certificate." + }, + "tls_insecure": { + "label": "TLS insecure", + "description": "Allow insecure TLS connections by skipping hostname verification (not recommended)." + }, + "qos": { + "label": "MQTT QoS", + "description": "Quality of Service level for MQTT publishes/subscriptions (0, 1, or 2)." + } + }, + "notifications": { + "label": "Notifications", + "description": "Settings to enable and control notifications for all cameras; can be overridden per-camera.", + "enabled": { + "label": "Enable notifications", + "description": "Enable or disable notifications for all cameras; can be overridden per-camera." + }, + "email": { + "label": "Notification email", + "description": "Email address used for push notifications or required by certain notification providers." + }, + "cooldown": { + "label": "Cooldown period", + "description": "Cooldown (seconds) between notifications to avoid spamming recipients." + }, + "enabled_in_config": { + "label": "Original notifications state", + "description": "Indicates whether notifications were enabled in the original static configuration." + } + }, + "networking": { + "label": "Networking", + "description": "Network-related settings such as IPv6 enablement for Frigate endpoints.", + "ipv6": { + "label": "IPv6 configuration", + "description": "IPv6-specific settings for Frigate network services.", + "enabled": { + "label": "Enable IPv6", + "description": "Enable IPv6 support for Frigate services (API and UI) where applicable." + } + }, + "listen": { + "label": "Listening ports configuration", + "description": "Configuration for internal and external listening ports. This is for advanced users. For the majority of use cases it's recommended to change the ports section of your Docker compose file.", + "internal": { + "label": "Internal port", + "description": "Internal listening port for Frigate (default 5000)." + }, + "external": { + "label": "External port", + "description": "External listening port for Frigate (default 8971)." + } + } + }, + "proxy": { + "label": "Proxy", + "description": "Settings for integrating Frigate behind a reverse proxy that passes authenticated user headers.", + "header_map": { + "label": "Header mapping", + "description": "Map incoming proxy headers to Frigate user and role fields for proxy-based auth.", + "user": { + "label": "User header", + "description": "Header containing the authenticated username provided by the upstream proxy." + }, + "role": { + "label": "Role header", + "description": "Header containing the authenticated user's role or groups from the upstream proxy." + }, + "role_map": { + "label": "Role mapping", + "description": "Map upstream group values to Frigate roles (for example map admin groups to the admin role)." + } + }, + "logout_url": { + "label": "Logout URL", + "description": "URL to redirect users to when logging out via the proxy." + }, + "auth_secret": { + "label": "Proxy secret", + "description": "Optional secret checked against the X-Proxy-Secret header to verify trusted proxies." + }, + "default_role": { + "label": "Default role", + "description": "Default role assigned to proxy-authenticated users when no role mapping applies." + }, + "separator": { + "label": "Separator character", + "description": "Character used to split multiple values provided in proxy headers." + } + }, + "telemetry": { + "label": "Telemetry", + "description": "System telemetry and stats options including GPU and network bandwidth monitoring.", + "network_interfaces": { + "label": "Network interfaces", + "description": "List of network interface name prefixes to monitor for bandwidth statistics." + }, + "stats": { + "label": "System stats", + "description": "Options to enable/disable collection of various system and GPU statistics.", + "amd_gpu_stats": { + "label": "AMD GPU stats", + "description": "Enable collection of AMD GPU statistics if an AMD GPU is present." + }, + "intel_gpu_stats": { + "label": "Intel GPU stats", + "description": "Enable collection of Intel GPU statistics if an Intel GPU is present." + }, + "network_bandwidth": { + "label": "Network bandwidth", + "description": "Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities)." + }, + "intel_gpu_device": { + "label": "Intel GPU device", + "description": "PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present." + } + }, + "version_check": { + "label": "Version check", + "description": "Enable an outbound check to detect if a newer Frigate version is available." + } + }, + "tls": { + "label": "TLS", + "description": "TLS settings for Frigate's web endpoints (port 8971).", + "enabled": { + "label": "Enable TLS", + "description": "Enable TLS for Frigate's web UI and API on the configured TLS port." + } + }, + "ui": { + "label": "UI", + "description": "User interface preferences such as timezone, time/date formatting, and units.", + "timezone": { + "label": "Timezone", + "description": "Optional timezone to display across the UI (defaults to browser local time if unset)." + }, + "time_format": { + "label": "Time format", + "description": "Time format to use in the UI (browser, 12hour, or 24hour)." + }, + "unit_system": { + "label": "Unit system", + "description": "Unit system for display (metric or imperial) used in the UI and MQTT." + } + }, + "detectors": { + "label": "Detector hardware", + "description": "Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.", + "type": { + "label": "Type" + }, + "model": { + "label": "Detector specific model configuration", + "description": "Detector-specific model configuration options (path, input size, etc.).", + "path": { + "label": "Custom object detector model path", + "description": "Path to a custom detection model file (or plus:// for Frigate+ models)." + }, + "labelmap_path": { + "label": "Label map for custom object detector", + "description": "Path to a labelmap file that maps numeric classes to string labels for the detector." + }, + "width": { + "label": "Object detection model input width", + "description": "Width of the model input tensor in pixels." + }, + "height": { + "label": "Object detection model input height", + "description": "Height of the model input tensor in pixels." + }, + "labelmap": { + "label": "Labelmap customization", + "description": "Overrides or remapping entries to merge into the standard labelmap." + }, + "attributes_map": { + "label": "Map of object labels to their attribute labels", + "description": "Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Model Input Tensor Shape", + "description": "Tensor format expected by the model: 'nhwc' or 'nchw'." + }, + "input_pixel_format": { + "label": "Model Input Pixel Color Format", + "description": "Pixel colorspace expected by the model: 'rgb', 'bgr', or 'yuv'." + }, + "input_dtype": { + "label": "Model Input D Type", + "description": "Data type of the model input tensor (for example 'float32')." + }, + "model_type": { + "label": "Object Detection Model Type", + "description": "Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization." + } + }, + "model_path": { + "label": "Detector specific model path", + "description": "File path to the detector model binary if required by the chosen detector." + }, + "axengine": { + "label": "AXEngine NPU", + "description": "AXERA AX650N/AX8850N NPU detector running compiled .axmodel files via the AXEngine runtime." + }, + "cpu": { + "label": "CPU", + "description": "CPU TFLite detector that runs TensorFlow Lite models on the host CPU without hardware acceleration. Not recommended.", + "num_threads": { + "label": "Number of detection threads", + "description": "The number of threads used for CPU-based inference." + } + }, + "deepstack": { + "label": "DeepStack", + "description": "DeepStack/CodeProject.AI detector that sends images to a remote DeepStack HTTP API for inference. Not recommended.", + "api_url": { + "label": "DeepStack API URL", + "description": "The URL of the DeepStack API." + }, + "api_timeout": { + "label": "DeepStack API timeout (in seconds)", + "description": "Maximum time allowed for a DeepStack API request." + }, + "api_key": { + "label": "DeepStack API key (if required)", + "description": "Optional API key for authenticated DeepStack services." + } + }, + "degirum": { + "label": "DeGirum", + "description": "DeGirum detector for running models via DeGirum cloud or local inference services.", + "location": { + "label": "Inference Location", + "description": "Location of the DeGirim inference engine (e.g. '@cloud', '127.0.0.1')." + }, + "zoo": { + "label": "Model Zoo", + "description": "Path or URL to the DeGirum model zoo." + }, + "token": { + "label": "DeGirum Cloud Token", + "description": "Token for DeGirum Cloud access." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "EdgeTPU detector that runs TensorFlow Lite models compiled for Coral EdgeTPU using the EdgeTPU delegate.", + "device": { + "label": "Device Type", + "description": "The device to use for EdgeTPU inference (e.g. 'usb', 'pci')." + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "Hailo-8/Hailo-8L detector using HEF models and the HailoRT SDK for inference on Hailo hardware.", + "device": { + "label": "Device Type", + "description": "The device to use for Hailo inference (e.g. 'PCIe', 'M.2')." + } + }, + "memryx": { + "label": "MemryX", + "description": "MemryX MX3 detector that runs compiled DFP models on MemryX accelerators.", + "device": { + "label": "Device Path", + "description": "The device to use for MemryX inference (e.g. 'PCIe')." + } + }, + "onnx": { + "label": "ONNX", + "description": "ONNX detector for running ONNX models; will use available acceleration backends (CUDA/ROCm/OpenVINO) when available.", + "device": { + "label": "Device Type", + "description": "The device to use for ONNX inference (e.g. 'AUTO', 'CPU', 'GPU')." + } + }, + "openvino": { + "label": "OpenVINO", + "description": "OpenVINO detector for AMD and Intel CPUs, Intel GPUs and Intel VPU hardware.", + "device": { + "label": "Device Type", + "description": "The device to use for OpenVINO inference (e.g. 'CPU', 'GPU', 'NPU')." + } + }, + "rknn": { + "label": "RKNN", + "description": "RKNN detector for Rockchip NPUs; runs compiled RKNN models on Rockchip hardware.", + "num_cores": { + "label": "Number of NPU cores to use.", + "description": "The number of NPU cores to use (0 for auto)." + } + }, + "synaptics": { + "label": "Synaptics", + "description": "Synaptics NPU detector for models in .synap format using the Synap SDK on Synaptics hardware." + }, + "teflon_tfl": { + "label": "Teflon", + "description": "Teflon delegate detector for TFLite using Mesa Teflon delegate library to accelerate inference on supported GPUs." + }, + "tensorrt": { + "label": "TensorRT", + "description": "TensorRT detector for Nvidia Jetson devices using serialized TensorRT engines for accelerated inference.", + "device": { + "label": "GPU Device Index", + "description": "The GPU device index to use." + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "ZMQ IPC detector that offloads inference to an external process via a ZeroMQ IPC endpoint.", + "endpoint": { + "label": "ZMQ IPC endpoint", + "description": "The ZMQ endpoint to connect to." + }, + "request_timeout_ms": { + "label": "ZMQ request timeout in milliseconds", + "description": "Timeout for ZMQ requests in milliseconds." + }, + "linger_ms": { + "label": "ZMQ socket linger in milliseconds", + "description": "Socket linger period in milliseconds." + } + } + }, + "model": { + "label": "Detection model", + "description": "Settings to configure a custom object detection model and its input shape.", + "path": { + "label": "Custom object detector model path", + "description": "Path to a custom detection model file (or plus:// for Frigate+ models)." + }, + "labelmap_path": { + "label": "Label map for custom object detector", + "description": "Path to a labelmap file that maps numeric classes to string labels for the detector." + }, + "width": { + "label": "Object detection model input width", + "description": "Width of the model input tensor in pixels." + }, + "height": { + "label": "Object detection model input height", + "description": "Height of the model input tensor in pixels." + }, + "labelmap": { + "label": "Labelmap customization", + "description": "Overrides or remapping entries to merge into the standard labelmap." + }, + "attributes_map": { + "label": "Map of object labels to their attribute labels", + "description": "Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Model Input Tensor Shape", + "description": "Tensor format expected by the model: 'nhwc' or 'nchw'." + }, + "input_pixel_format": { + "label": "Model Input Pixel Color Format", + "description": "Pixel colorspace expected by the model: 'rgb', 'bgr', or 'yuv'." + }, + "input_dtype": { + "label": "Model Input D Type", + "description": "Data type of the model input tensor (for example 'float32')." + }, + "model_type": { + "label": "Object Detection Model Type", + "description": "Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization." + } + }, + "genai": { + "label": "Generative AI configuration", + "description": "Settings for integrated generative AI providers used to generate object descriptions and review summaries.", + "api_key": { + "label": "API key", + "description": "API key required by some providers (can also be set via environment variables)." + }, + "base_url": { + "label": "Base URL", + "description": "Base URL for self-hosted or compatible providers (for example an Ollama instance)." + }, + "model": { + "label": "Model", + "description": "The model to use from the provider for generating descriptions or summaries." + }, + "provider": { + "label": "Provider", + "description": "The GenAI provider to use (for example: ollama, gemini, openai)." + }, + "roles": { + "label": "Roles", + "description": "GenAI roles (chat, descriptions, embeddings); one provider per role." + }, + "provider_options": { + "label": "Provider options", + "description": "Additional provider-specific options to pass to the GenAI client." + }, + "runtime_options": { + "label": "Runtime options", + "description": "Runtime options passed to the provider for each inference call." + } + }, + "audio": { + "label": "Audio detection", + "description": "Settings for audio-based event detection for all cameras; can be overridden per-camera.", + "enabled": { + "label": "Enable audio detection", + "description": "Enable or disable audio event detection for all cameras; can be overridden per-camera." + }, + "max_not_heard": { + "label": "End timeout", + "description": "Amount of seconds without the configured audio type before the audio event is ended." + }, + "min_volume": { + "label": "Minimum volume", + "description": "Minimum RMS volume threshold required to run audio detection; lower values increase sensitivity (e.g., 200 high, 500 medium, 1000 low)." + }, + "listen": { + "label": "Listen types", + "description": "List of audio event types to detect (for example: bark, fire_alarm, speech, yell)." + }, + "filters": { + "label": "Audio filters", + "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.", + "threshold": { + "label": "Minimum audio confidence", + "description": "Minimum confidence threshold for the audio event to be counted." + } + }, + "enabled_in_config": { + "label": "Original audio state", + "description": "Indicates whether audio detection was originally enabled in the static config file." + }, + "num_threads": { + "label": "Detection threads", + "description": "Number of threads to use for audio detection processing." + } + }, + "birdseye": { + "label": "Birdseye", + "description": "Settings for the Birdseye composite view that composes multiple camera feeds into a single layout.", + "enabled": { + "label": "Enable Birdseye", + "description": "Enable or disable the Birdseye view feature." + }, + "mode": { + "label": "Tracking mode", + "description": "Mode for including cameras in Birdseye: 'objects', 'motion', or 'continuous'." + }, + "restream": { + "label": "Restream RTSP", + "description": "Re-stream the Birdseye output as an RTSP feed; enabling this will keep Birdseye running continuously." + }, + "width": { + "label": "Width", + "description": "Output width (pixels) of the composed Birdseye frame." + }, + "height": { + "label": "Height", + "description": "Output height (pixels) of the composed Birdseye frame." + }, + "quality": { + "label": "Encoding quality", + "description": "Encoding quality for the Birdseye mpeg1 feed (1 highest quality, 31 lowest)." + }, + "inactivity_threshold": { + "label": "Inactivity threshold", + "description": "Seconds of inactivity after which a camera will stop being shown in Birdseye." + }, + "layout": { + "label": "Layout", + "description": "Layout options for the Birdseye composition.", + "scaling_factor": { + "label": "Scaling factor", + "description": "Scaling factor used by the layout calculator (range 1.0 to 5.0)." + }, + "max_cameras": { + "label": "Max cameras", + "description": "Maximum number of cameras to display at once in Birdseye; shows the most recent cameras." + } + }, + "idle_heartbeat_fps": { + "label": "Idle heartbeat FPS", + "description": "Frames-per-second to resend the last composed Birdseye frame when idle; set to 0 to disable." + }, + "order": { + "label": "Position", + "description": "Numeric position controlling the camera's ordering in the Birdseye layout." + } + }, + "detect": { + "label": "Object Detection", + "description": "Settings for the detection/detect role used to run object detection and initialize trackers.", + "enabled": { + "label": "Enable object detection", + "description": "Enable or disable object detection for all cameras; can be overridden per-camera." + }, + "height": { + "label": "Detect height", + "description": "Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution." + }, + "width": { + "label": "Detect width", + "description": "Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution." + }, + "fps": { + "label": "Detect FPS", + "description": "Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects)." + }, + "min_initialized": { + "label": "Minimum initialization frames", + "description": "Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2." + }, + "max_disappeared": { + "label": "Maximum disappeared frames", + "description": "Number of frames without a detection before a tracked object is considered gone." + }, + "stationary": { + "label": "Stationary objects config", + "description": "Settings to detect and manage objects that remain stationary for a period of time.", + "interval": { + "label": "Stationary interval", + "description": "How often (in frames) to run a detection check to confirm a stationary object." + }, + "threshold": { + "label": "Stationary threshold", + "description": "Number of frames with no position change required to mark an object as stationary." + }, + "max_frames": { + "label": "Max frames", + "description": "Limits how long stationary objects are tracked before being discarded.", + "default": { + "label": "Default max frames", + "description": "Default maximum frames to track a stationary object before stopping." + }, + "objects": { + "label": "Object max frames", + "description": "Per-object overrides for maximum frames to track stationary objects." + } + }, + "classifier": { + "label": "Enable visual classifier", + "description": "Use a visual classifier to detect truly stationary objects even when bounding boxes jitter." + } + }, + "annotation_offset": { + "label": "Annotation offset", + "description": "Milliseconds to shift detect annotations to better align timeline bounding boxes with recordings; can be positive or negative." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.", + "path": { + "label": "FFmpeg path", + "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\")." + }, + "global_args": { + "label": "FFmpeg global arguments", + "description": "Global arguments passed to FFmpeg processes." + }, + "hwaccel_args": { + "label": "Hardware acceleration arguments", + "description": "Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended." + }, + "input_args": { + "label": "Input arguments", + "description": "Input arguments applied to FFmpeg input streams." + }, + "output_args": { + "label": "Output arguments", + "description": "Default output arguments used for different FFmpeg roles such as detect and record.", + "detect": { + "label": "Detect output arguments", + "description": "Default output arguments for detect role streams." + }, + "record": { + "label": "Record output arguments", + "description": "Default output arguments for record role streams." + } + }, + "retry_interval": { + "label": "FFmpeg retry time", + "description": "Seconds to wait before attempting to reconnect a camera stream after failure. Default is 10." + }, + "apple_compatibility": { + "label": "Apple compatibility", + "description": "Enable HEVC tagging for better Apple player compatibility when recording H.265." + }, + "gpu": { + "label": "GPU index", + "description": "Default GPU index used for hardware acceleration if available." + }, + "inputs": { + "label": "Camera inputs", + "description": "List of input stream definitions (paths and roles) for this camera.", + "path": { + "label": "Input path", + "description": "Camera input stream URL or path." + }, + "roles": { + "label": "Input roles", + "description": "Roles for this input stream." + }, + "global_args": { + "label": "FFmpeg global arguments", + "description": "FFmpeg global arguments for this input stream." + }, + "hwaccel_args": { + "label": "Hardware acceleration arguments", + "description": "Hardware acceleration arguments for this input stream." + }, + "input_args": { + "label": "Input arguments", + "description": "Input arguments specific to this stream." + } + } + }, + "live": { + "label": "Live playback", + "description": "Settings to control the jsmpeg live stream resolution and quality. This does not affect restreamed cameras that use go2rtc for live view.", + "streams": { + "label": "Live stream names", + "description": "Mapping of configured stream names to restream/go2rtc names used for live playback." + }, + "height": { + "label": "Live height", + "description": "Height (pixels) to render the jsmpeg live stream in the Web UI; must be <= detect stream height." + }, + "quality": { + "label": "Live quality", + "description": "Encoding quality for the jsmpeg stream (1 highest, 31 lowest)." + } + }, + "motion": { + "label": "Motion detection", + "description": "Default motion detection settings applied to cameras unless overridden per-camera.", + "enabled": { + "label": "Enable motion detection", + "description": "Enable or disable motion detection for all cameras; can be overridden per-camera." + }, + "threshold": { + "label": "Motion threshold", + "description": "Pixel difference threshold used by the motion detector; higher values reduce sensitivity (range 1-255)." + }, + "lightning_threshold": { + "label": "Lightning threshold", + "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events." + }, + "skip_motion_threshold": { + "label": "Skip motion threshold", + "description": "If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature." + }, + "improve_contrast": { + "label": "Improve contrast", + "description": "Apply contrast improvement to frames before motion analysis to help detection." + }, + "contour_area": { + "label": "Contour area", + "description": "Minimum contour area in pixels required for a motion contour to be counted." + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "Alpha blending factor used in frame differencing for motion calculation." + }, + "frame_alpha": { + "label": "Frame alpha", + "description": "Alpha value used when blending frames for motion preprocessing." + }, + "frame_height": { + "label": "Frame height", + "description": "Height in pixels to scale frames to when computing motion." + }, + "mask": { + "label": "Mask coordinates", + "description": "Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas." + }, + "mqtt_off_delay": { + "label": "MQTT off delay", + "description": "Seconds to wait after last motion before publishing an MQTT 'off' state." + }, + "enabled_in_config": { + "label": "Original motion state", + "description": "Indicates whether motion detection was enabled in the original static configuration." + }, + "raw_mask": { + "label": "Raw Mask" + } + }, + "objects": { + "label": "Objects", + "description": "Object tracking defaults including which labels to track and per-object filters.", + "track": { + "label": "Objects to track", + "description": "List of object labels to track for all cameras; can be overridden per-camera." + }, + "filters": { + "label": "Object filters", + "description": "Filters applied to detected objects to reduce false positives (area, ratio, confidence).", + "min_area": { + "label": "Minimum object area", + "description": "Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum object area", + "description": "Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum aspect ratio", + "description": "Minimum width/height ratio required for the bounding box to qualify." + }, + "max_ratio": { + "label": "Maximum aspect ratio", + "description": "Maximum width/height ratio allowed for the bounding box to qualify." + }, + "threshold": { + "label": "Confidence threshold", + "description": "Average detection confidence threshold required for the object to be considered a true positive." + }, + "min_score": { + "label": "Minimum confidence", + "description": "Minimum single-frame detection confidence required for the object to be counted." + }, + "mask": { + "label": "Filter mask", + "description": "Polygon coordinates defining where this filter applies within the frame." + }, + "raw_mask": { + "label": "Raw Mask" + } + }, + "mask": { + "label": "Object mask", + "description": "Mask polygon used to prevent object detection in specified areas." + }, + "raw_mask": { + "label": "Raw Mask" + }, + "genai": { + "label": "GenAI object config", + "description": "GenAI options for describing tracked objects and sending frames for generation.", + "enabled": { + "label": "Enable GenAI", + "description": "Enable GenAI generation of descriptions for tracked objects by default." + }, + "use_snapshot": { + "label": "Use snapshots", + "description": "Use object snapshots instead of thumbnails for GenAI description generation." + }, + "prompt": { + "label": "Caption prompt", + "description": "Default prompt template used when generating descriptions with GenAI." + }, + "object_prompts": { + "label": "Object prompts", + "description": "Per-object prompts to customize GenAI outputs for specific labels." + }, + "objects": { + "label": "GenAI objects", + "description": "List of object labels to send to GenAI by default." + }, + "required_zones": { + "label": "Required zones", + "description": "Zones that must be entered for objects to qualify for GenAI description generation." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails", + "description": "Save thumbnails sent to GenAI for debugging and review." + }, + "send_triggers": { + "label": "GenAI triggers", + "description": "Defines when frames should be sent to GenAI (on end, after updates, etc.).", + "tracked_object_end": { + "label": "Send on end", + "description": "Send a request to GenAI when the tracked object ends." + }, + "after_significant_updates": { + "label": "Early GenAI trigger", + "description": "Send a request to GenAI after a specified number of significant updates for the tracked object." + } + }, + "enabled_in_config": { + "label": "Original GenAI state", + "description": "Indicates whether GenAI was enabled in the original static config." + } + }, + "filters_attribute": { + "label": "Attribute filters", + "description": "Filters applied to detected attributes to reduce false positives (area, ratio, confidence).", + "min_area": { + "label": "Minimum attribute area", + "description": "Minimum bounding box area (pixels or percentage) required for this attribute. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum attribute area", + "description": "Maximum bounding box area (pixels or percentage) allowed for this attribute. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum aspect ratio", + "description": "Minimum width/height ratio required for the bounding box to qualify." + }, + "max_ratio": { + "label": "Maximum aspect ratio", + "description": "Maximum width/height ratio allowed for the bounding box to qualify." + }, + "threshold": { + "label": "Confidence threshold", + "description": "Average detection confidence threshold required for the attribute to be considered a true positive." + }, + "min_score": { + "label": "Minimum confidence", + "description": "Minimum single-frame detection confidence required to associate this attribute with its parent object." + }, + "mask": { + "label": "Filter mask", + "description": "Polygon coordinates defining where this filter applies within the frame." + }, + "raw_mask": { + "label": "Raw Mask" + } + } + }, + "record": { + "label": "Recording", + "description": "Recording and retention settings applied to cameras unless overridden per-camera.", + "enabled": { + "label": "Enable recording", + "description": "Enable or disable recording for all cameras; can be overridden per-camera." + }, + "expire_interval": { + "label": "Record cleanup interval", + "description": "Minutes between cleanup passes that remove expired recording segments." + }, + "continuous": { + "label": "Continuous retention", + "description": "Number of days to retain recordings regardless of tracked objects or motion. Set to 0 if you only want to retain recordings of alerts and detections.", + "days": { + "label": "Retention days", + "description": "Days to retain recordings." + } + }, + "motion": { + "label": "Motion retention", + "description": "Number of days to retain recordings triggered by motion regardless of tracked objects. Set to 0 if you only want to retain recordings of alerts and detections.", + "days": { + "label": "Retention days", + "description": "Days to retain recordings." + } + }, + "detections": { + "label": "Detection retention", + "description": "Recording retention settings for detection events including pre/post capture durations.", + "pre_capture": { + "label": "Pre-capture seconds", + "description": "Number of seconds before the detection event to include in the recording." + }, + "post_capture": { + "label": "Post-capture seconds", + "description": "Number of seconds after the detection event to include in the recording." + }, + "retain": { + "label": "Event retention", + "description": "Retention settings for recordings of detection events.", + "days": { + "label": "Retention days", + "description": "Number of days to retain recordings of detection events." + }, + "mode": { + "label": "Retention mode", + "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects)." + } + } + }, + "alerts": { + "label": "Alert retention", + "description": "Recording retention settings for alert events including pre/post capture durations.", + "pre_capture": { + "label": "Pre-capture seconds", + "description": "Number of seconds before the detection event to include in the recording." + }, + "post_capture": { + "label": "Post-capture seconds", + "description": "Number of seconds after the detection event to include in the recording." + }, + "retain": { + "label": "Event retention", + "description": "Retention settings for recordings of detection events.", + "days": { + "label": "Retention days", + "description": "Number of days to retain recordings of detection events." + }, + "mode": { + "label": "Retention mode", + "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects)." + } + } + }, + "export": { + "label": "Export config", + "description": "Settings used when exporting recordings such as timelapse and hardware acceleration.", + "hwaccel_args": { + "label": "Export hwaccel args", + "description": "Hardware acceleration args to use for export/transcode operations." + }, + "max_concurrent": { + "label": "Maximum concurrent exports", + "description": "Maximum number of export jobs to process at the same time." + }, + "chapters": { + "label": "Chapter metadata to embed in exported recordings" + } + }, + "preview": { + "label": "Preview config", + "description": "Settings controlling the quality of recording previews shown in the UI.", + "quality": { + "label": "Preview quality", + "description": "Preview quality level (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Original recording state", + "description": "Indicates whether recording was enabled in the original static configuration." + } + }, + "review": { + "label": "Review", + "description": "Settings that control alerts, detections, and GenAI review summaries used by the UI and storage.", + "alerts": { + "label": "Alerts config", + "description": "Settings for which tracked objects generate alerts and how alerts are retained.", + "enabled": { + "label": "Enable alerts", + "description": "Enable or disable alert generation for all cameras; can be overridden per-camera." + }, + "labels": { + "label": "Alert labels", + "description": "List of object labels that qualify as alerts (for example: car, person)." + }, + "required_zones": { + "label": "Required zones", + "description": "Zones that an object must enter to be considered an alert; leave empty to allow any zone." + }, + "enabled_in_config": { + "label": "Original alerts state", + "description": "Tracks whether alerts were originally enabled in the static configuration." + }, + "cutoff_time": { + "label": "Alerts cutoff time", + "description": "Seconds to wait after no alert-causing activity before cutting off an alert." + } + }, + "detections": { + "label": "Detections config", + "description": "Settings for which tracked objects generate detections (non-alert) and how detections are retained.", + "enabled": { + "label": "Enable detections", + "description": "Enable or disable detection events for all cameras; can be overridden per-camera." + }, + "labels": { + "label": "Detection labels", + "description": "List of object labels that qualify as detection events." + }, + "required_zones": { + "label": "Required zones", + "description": "Zones that an object must enter to be considered a detection; leave empty to allow any zone." + }, + "cutoff_time": { + "label": "Detections cutoff time", + "description": "Seconds to wait after no detection-causing activity before cutting off a detection." + }, + "enabled_in_config": { + "label": "Original detections state", + "description": "Tracks whether detections were originally enabled in the static configuration." + } + }, + "genai": { + "label": "GenAI config", + "description": "Controls use of generative AI for producing descriptions and summaries of review items.", + "enabled": { + "label": "Enable GenAI descriptions", + "description": "Enable or disable GenAI-generated descriptions and summaries for review items." + }, + "alerts": { + "label": "Enable GenAI for alerts", + "description": "Use GenAI to generate descriptions for alert items." + }, + "detections": { + "label": "Enable GenAI for detections", + "description": "Use GenAI to generate descriptions for detection items." + }, + "image_source": { + "label": "Review image source", + "description": "Source of images sent to GenAI ('preview' or 'recordings'); 'recordings' uses higher quality frames but more tokens." + }, + "additional_concerns": { + "label": "Additional concerns", + "description": "A list of additional concerns or notes the GenAI should consider when evaluating activity on this camera." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails", + "description": "Save thumbnails that are sent to the GenAI provider for debugging and review." + }, + "enabled_in_config": { + "label": "Original GenAI state", + "description": "Tracks whether GenAI review was originally enabled in the static configuration." + }, + "preferred_language": { + "label": "Preferred language", + "description": "Preferred language to request from the GenAI provider for generated responses." + }, + "activity_context_prompt": { + "label": "Activity context prompt", + "description": "Custom prompt describing what is and is not suspicious activity to provide context for GenAI summaries." + } + } + }, + "snapshots": { + "label": "Snapshots", + "description": "Settings for API-generated snapshots of tracked objects for all cameras; can be overridden per-camera.", + "enabled": { + "label": "Enable snapshots", + "description": "Enable or disable saving snapshots for all cameras; can be overridden per-camera." + }, + "timestamp": { + "label": "Timestamp overlay", + "description": "Overlay a timestamp on snapshots from API." + }, + "bounding_box": { + "label": "Bounding box overlay", + "description": "Draw bounding boxes for tracked objects on snapshots from API." + }, + "crop": { + "label": "Crop snapshot", + "description": "Crop snapshots from API to the detected object's bounding box." + }, + "required_zones": { + "label": "Required zones", + "description": "Zones an object must enter for a snapshot to be saved." + }, + "height": { + "label": "Snapshot height", + "description": "Height (pixels) to resize snapshots from API to; leave empty to preserve original size." + }, + "retain": { + "label": "Snapshot retention", + "description": "Retention settings for snapshots including default days and per-object overrides.", + "default": { + "label": "Default retention", + "description": "Default number of days to retain snapshots." + }, + "objects": { + "label": "Object retention", + "description": "Per-object overrides for snapshot retention days." + } + }, + "quality": { + "label": "Snapshot quality", + "description": "Encode quality for saved snapshots (0-100)." + } + }, + "timestamp_style": { + "label": "Timestamp style", + "description": "Styling options for in-feed timestamps applied to debug view and snapshots.", + "position": { + "label": "Timestamp position", + "description": "Position of the timestamp on the image (tl/tr/bl/br)." + }, + "format": { + "label": "Timestamp format", + "description": "Datetime format string used for timestamps (Python datetime format codes)." + }, + "color": { + "label": "Timestamp color", + "description": "RGB color values for the timestamp text (all values 0-255).", + "red": { + "label": "Red", + "description": "Red component (0-255) for timestamp color." + }, + "green": { + "label": "Green", + "description": "Green component (0-255) for timestamp color." + }, + "blue": { + "label": "Blue", + "description": "Blue component (0-255) for timestamp color." + } + }, + "thickness": { + "label": "Timestamp thickness", + "description": "Line thickness of the timestamp text." + }, + "effect": { + "label": "Timestamp effect", + "description": "Visual effect for the timestamp text (none, solid, shadow)." + } + }, + "audio_transcription": { + "label": "Audio transcription", + "description": "Settings for live and speech audio transcription used for events and live captions.", + "enabled": { + "label": "Enable audio transcription", + "description": "Enable or disable automatic audio transcription for all cameras; can be overridden per-camera." + }, + "language": { + "label": "Transcription language", + "description": "Language code used for transcription/translation (for example 'en' for English). See https://whisper-api.com/docs/languages/ for supported language codes." + }, + "device": { + "label": "Transcription device", + "description": "Device key (CPU/GPU) to run the transcription model on. Only NVIDIA CUDA GPUs are currently supported for transcription." + }, + "model_size": { + "label": "Model size", + "description": "Model size to use for offline audio event transcription." + }, + "live_enabled": { + "label": "Live transcription", + "description": "Enable streaming live transcription for audio as it is received." + } + }, + "classification": { + "label": "Object classification", + "description": "Settings for classification models used to refine object labels or state classification.", + "bird": { + "label": "Bird classification config", + "description": "Settings specific to bird classification models.", + "enabled": { + "label": "Bird classification", + "description": "Enable or disable bird classification." + }, + "threshold": { + "label": "Minimum score", + "description": "Minimum classification score required to accept a bird classification." + } + }, + "custom": { + "label": "Custom Classification Models", + "description": "Configuration for custom classification models used for objects or state detection.", + "enabled": { + "label": "Enable model", + "description": "Enable or disable the custom classification model." + }, + "name": { + "label": "Model name", + "description": "Identifier for the custom classification model to use." + }, + "threshold": { + "label": "Score threshold", + "description": "Score threshold used to change the classification state." + }, + "save_attempts": { + "label": "Save attempts", + "description": "How many classification attempts to save for recent classifications UI." + }, + "object_config": { + "objects": { + "label": "Classify objects", + "description": "List of object types to run object classification on." + }, + "classification_type": { + "label": "Classification type", + "description": "Classification type applied: 'sub_label' (adds sub_label) or other supported types." + } + }, + "state_config": { + "cameras": { + "label": "Classification cameras", + "description": "Per-camera crop and settings for running state classification.", + "crop": { + "label": "Classification crop", + "description": "Crop coordinates to use for running classification on this camera." + } + }, + "motion": { + "label": "Run on motion", + "description": "If true, run classification when motion is detected within the specified crop." + }, + "interval": { + "label": "Classification interval", + "description": "Interval (seconds) between periodic classification runs for state classification." + } + } + } + }, + "semantic_search": { + "label": "Semantic Search", + "description": "Settings for Semantic Search which builds and queries object embeddings to find similar items.", + "enabled": { + "label": "Enable semantic search", + "description": "Enable or disable the semantic search feature." + }, + "reindex": { + "label": "Reindex on startup", + "description": "Trigger a full reindex of historical tracked objects into the embeddings database." + }, + "model": { + "label": "Semantic search model or GenAI provider name", + "description": "The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role." + }, + "model_size": { + "label": "Model size", + "description": "Select model size; 'small' runs on CPU and 'large' typically requires GPU." + }, + "device": { + "label": "Device", + "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information" + }, + "triggers": { + "label": "Triggers", + "description": "Actions and matching criteria for camera-specific semantic search triggers.", + "friendly_name": { + "label": "Friendly name", + "description": "Optional friendly name displayed in the UI for this trigger." + }, + "enabled": { + "label": "Enable this trigger", + "description": "Enable or disable this semantic search trigger." + }, + "type": { + "label": "Trigger type", + "description": "Type of trigger: 'thumbnail' (match against image) or 'description' (match against text)." + }, + "data": { + "label": "Trigger content", + "description": "Text phrase or thumbnail ID to match against tracked objects." + }, + "threshold": { + "label": "Trigger threshold", + "description": "Minimum similarity score (0-1) required to activate this trigger." + }, + "actions": { + "label": "Trigger actions", + "description": "List of actions to execute when trigger matches (notification, sub_label, attribute)." + } + } + }, + "face_recognition": { + "label": "Face recognition", + "description": "Settings for face detection and recognition for all cameras; can be overridden per-camera.", + "enabled": { + "label": "Enable face recognition", + "description": "Enable or disable face recognition for all cameras; can be overridden per-camera." + }, + "model_size": { + "label": "Model size", + "description": "Model size to use for face embeddings (small/large); larger may require GPU." + }, + "unknown_score": { + "label": "Unknown score threshold", + "description": "Distance threshold below which a face is considered a potential match (higher = stricter)." + }, + "detection_threshold": { + "label": "Detection threshold", + "description": "Minimum detection confidence required to consider a face detection valid." + }, + "recognition_threshold": { + "label": "Recognition threshold", + "description": "Face embedding distance threshold to consider two faces a match." + }, + "min_area": { + "label": "Minimum face area", + "description": "Minimum area (pixels) of a detected face box required to attempt recognition." + }, + "min_faces": { + "label": "Minimum faces", + "description": "Minimum number of face recognitions required before applying a recognized sub-label to a person." + }, + "save_attempts": { + "label": "Save attempts", + "description": "Number of face recognition attempts to retain for recent recognition UI." + }, + "blur_confidence_filter": { + "label": "Blur confidence filter", + "description": "Adjust confidence scores based on image blur to reduce false positives for poor quality faces." + }, + "device": { + "label": "Device", + "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information" + } + }, + "lpr": { + "label": "License Plate Recognition", + "description": "License plate recognition settings including detection thresholds, formatting, and known plates.", + "enabled": { + "label": "Enable LPR", + "description": "Enable or disable license plate recognition for all cameras; can be overridden per-camera." + }, + "model_size": { + "label": "Model size", + "description": "Model size used for text detection/recognition. Most users should use 'small'." + }, + "detection_threshold": { + "label": "Detection threshold", + "description": "Detection confidence threshold to begin running OCR on a suspected plate." + }, + "min_area": { + "label": "Minimum plate area", + "description": "Minimum plate area (pixels) required to attempt recognition." + }, + "recognition_threshold": { + "label": "Recognition threshold", + "description": "Confidence threshold required for recognized plate text to be attached as a sub-label." + }, + "min_plate_length": { + "label": "Min plate length", + "description": "Minimum number of characters a recognized plate must contain to be considered valid." + }, + "format": { + "label": "Plate format regex", + "description": "Optional regex to validate recognized plate strings against an expected format." + }, + "match_distance": { + "label": "Match distance", + "description": "Number of character mismatches allowed when comparing detected plates to known plates." + }, + "known_plates": { + "label": "Known plates", + "description": "List of plates or regexes to specially track or alert on." + }, + "enhancement": { + "label": "Enhancement level", + "description": "Enhancement level (0-10) to apply to plate crops prior to OCR; higher values may not always improve results, levels above 5 may only work with night time plates and should be used with caution." + }, + "debug_save_plates": { + "label": "Save debug plates", + "description": "Save plate crop images for debugging LPR performance." + }, + "device": { + "label": "Device", + "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information" + }, + "replace_rules": { + "label": "Replacement rules", + "description": "Regex replacement rules used to normalize detected plate strings before matching.", + "pattern": { + "label": "Regex pattern" + }, + "replacement": { + "label": "Replacement string" + } + }, + "expire_time": { + "label": "Expire seconds", + "description": "Time in seconds after which an unseen plate is expired from the tracker (for dedicated LPR cameras only)." + } + }, + "camera_groups": { + "label": "Camera groups", + "description": "Configuration for named camera groups used to organize cameras in the UI.", + "cameras": { + "label": "Camera list", + "description": "Array of camera names included in this group." + }, + "icon": { + "label": "Group icon", + "description": "Icon used to represent the camera group in the UI." + }, + "order": { + "label": "Sort order", + "description": "Numeric order used to sort camera groups in the UI; larger numbers appear later." + } + }, + "profiles": { + "label": "Profiles", + "description": "Named profile definitions with friendly names. Camera profiles must reference names defined here.", + "friendly_name": { + "label": "Friendly name", + "description": "Display name for this profile shown in the UI." + } + }, + "active_profile": { + "label": "Active profile", + "description": "Currently active profile name. Runtime-only, not persisted in YAML." + }, + "camera_mqtt": { + "label": "MQTT", + "description": "MQTT image publishing settings.", + "enabled": { + "label": "Send image", + "description": "Enable publishing image snapshots for objects to MQTT topics for this camera." + }, + "timestamp": { + "label": "Add timestamp", + "description": "Overlay a timestamp on images published to MQTT." + }, + "bounding_box": { + "label": "Add bounding box", + "description": "Draw bounding boxes on images published over MQTT." + }, + "crop": { + "label": "Crop image", + "description": "Crop images published to MQTT to the detected object's bounding box." + }, + "height": { + "label": "Image height", + "description": "Height (pixels) to resize images published over MQTT." + }, + "required_zones": { + "label": "Required zones", + "description": "Zones that an object must enter for an MQTT image to be published." + }, + "quality": { + "label": "JPEG quality", + "description": "JPEG quality for images published to MQTT (0-100)." + } + }, + "camera_ui": { + "label": "Camera UI", + "description": "Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.", + "order": { + "label": "UI order", + "description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later." + }, + "dashboard": { + "label": "Show in UI", + "description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again." + }, + "review": { + "label": "Show in review", + "description": "Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view)." + } + }, + "onvif": { + "label": "ONVIF", + "description": "ONVIF connection and PTZ autotracking settings for this camera.", + "host": { + "label": "ONVIF host", + "description": "Host (and optional scheme) for the ONVIF service for this camera." + }, + "port": { + "label": "ONVIF port", + "description": "Port number for the ONVIF service." + }, + "user": { + "label": "ONVIF username", + "description": "Username for ONVIF authentication; some devices require admin user for ONVIF." + }, + "password": { + "label": "ONVIF password", + "description": "Password for ONVIF authentication." + }, + "tls_insecure": { + "label": "Disable TLS verify", + "description": "Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only)." + }, + "profile": { + "label": "ONVIF profile", + "description": "Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically." + }, + "autotracking": { + "label": "Autotracking", + "description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.", + "enabled": { + "label": "Enable Autotracking", + "description": "Enable or disable automatic PTZ camera tracking of detected objects." + }, + "calibrate_on_startup": { + "label": "Calibrate on start", + "description": "Measure PTZ motor speeds on startup to improve tracking accuracy. Frigate will update config with movement_weights after calibration." + }, + "zooming": { + "label": "Zoom mode", + "description": "Control zoom behavior: disabled (pan/tilt only), absolute (most compatible), or relative (concurrent pan/tilt/zoom)." + }, + "zoom_factor": { + "label": "Zoom factor", + "description": "Control zoom level on tracked objects. Lower values keep more scene in view; higher values zoom in closer but may lose tracking. Values between 0.1 and 0.75." + }, + "track": { + "label": "Tracked objects", + "description": "List of object types that should trigger autotracking." + }, + "required_zones": { + "label": "Required zones", + "description": "Objects must enter one of these zones before autotracking begins." + }, + "return_preset": { + "label": "Return preset", + "description": "ONVIF preset name configured in camera firmware to return to after tracking ends." + }, + "timeout": { + "label": "Return timeout", + "description": "Wait this many seconds after losing tracking before returning camera to preset position." + }, + "movement_weights": { + "label": "Movement weights", + "description": "Calibration values automatically generated by camera calibration. Do not modify manually." + }, + "enabled_in_config": { + "label": "Original autotrack state", + "description": "Internal field to track whether autotracking was enabled in configuration." + } + }, + "ignore_time_mismatch": { + "label": "Ignore time mismatch", + "description": "Ignore time synchronization differences between camera and Frigate server for ONVIF communication." + } + } +} diff --git a/web/public/locales/en/config/groups.json b/web/public/locales/en/config/groups.json new file mode 100644 index 0000000..1663ad1 --- /dev/null +++ b/web/public/locales/en/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Global Detection", + "sensitivity": "Global Sensitivity" + }, + "cameras": { + "detection": "Detection", + "sensitivity": "Sensitivity" + } + }, + "timestamp_style": { + "global": { + "appearance": "Global Appearance" + }, + "cameras": { + "appearance": "Appearance" + } + }, + "motion": { + "global": { + "sensitivity": "Global Sensitivity", + "algorithm": "Global Algorithm" + }, + "cameras": { + "sensitivity": "Sensitivity", + "algorithm": "Algorithm" + } + }, + "snapshots": { + "global": { + "display": "Global Display" + }, + "cameras": { + "display": "Display" + } + }, + "detect": { + "global": { + "resolution": "Global Resolution", + "tracking": "Global Tracking" + }, + "cameras": { + "resolution": "Resolution", + "tracking": "Tracking" + } + }, + "objects": { + "global": { + "tracking": "Global Tracking", + "filtering": "Global Filtering" + }, + "cameras": { + "tracking": "Tracking", + "filtering": "Filtering" + } + }, + "record": { + "global": { + "retention": "Global Retention", + "events": "Global Events" + }, + "cameras": { + "retention": "Retention", + "events": "Events" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Camera-specific FFmpeg arguments" + } + } +} diff --git a/web/public/locales/en/config/validation.json b/web/public/locales/en/config/validation.json new file mode 100644 index 0000000..f3d98a6 --- /dev/null +++ b/web/public/locales/en/config/validation.json @@ -0,0 +1,35 @@ +{ + "minimum": "Must be at least {{limit}}", + "maximum": "Must be at most {{limit}}", + "exclusiveMinimum": "Must be greater than {{limit}}", + "exclusiveMaximum": "Must be less than {{limit}}", + "minLength": "Must be at least {{limit}} character(s)", + "maxLength": "Must be at most {{limit}} character(s)", + "minItems": "Must have at least {{limit}} items", + "maxItems": "Must have at most {{limit}} items", + "pattern": "Invalid format", + "required": "This field is required", + "type": "Invalid value type", + "enum": "Must be one of the allowed values", + "const": "Value does not match expected constant", + "uniqueItems": "All items must be unique", + "format": "Invalid format", + "additionalProperties": "Unknown property is not allowed", + "oneOf": "Must match exactly one of the allowed schemas", + "anyOf": "Must match at least one of the allowed schemas", + "proxy": { + "header_map": { + "roleHeaderRequired": "Role header is required when role mappings are configured." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "Each role can only be assigned to one input stream.", + "detectRequired": "At least one input stream must be assigned the 'detect' role.", + "hwaccelDetectOnly": "Only the input stream with the detect role can define hardware acceleration arguments." + } + }, + "detect": { + "dimensionMustBeEven": "Must be an even number." + } +} diff --git a/web/public/locales/en/objects.json b/web/public/locales/en/objects.json new file mode 100644 index 0000000..d48f609 --- /dev/null +++ b/web/public/locales/en/objects.json @@ -0,0 +1,129 @@ +{ + "person": "Person", + "bicycle": "Bicycle", + "car": "Car", + "motorcycle": "Motorcycle", + "airplane": "Airplane", + "bus": "Bus", + "train": "Train", + "boat": "Boat", + "traffic_light": "Traffic Light", + "fire_hydrant": "Fire Hydrant", + "street_sign": "Street Sign", + "stop_sign": "Stop Sign", + "parking_meter": "Parking Meter", + "bench": "Bench", + "bird": "Bird", + "cat": "Cat", + "dog": "Dog", + "horse": "Horse", + "sheep": "Sheep", + "cow": "Cow", + "elephant": "Elephant", + "bear": "Bear", + "zebra": "Zebra", + "giraffe": "Giraffe", + "hat": "Hat", + "backpack": "Backpack", + "umbrella": "Umbrella", + "shoe": "Shoe", + "eye_glasses": "Eye Glasses", + "handbag": "Handbag", + "tie": "Tie", + "suitcase": "Suitcase", + "frisbee": "Frisbee", + "skis": "Skis", + "snowboard": "Snowboard", + "sports_ball": "Sports Ball", + "kite": "Kite", + "baseball_bat": "Baseball Bat", + "baseball_glove": "Baseball Glove", + "skateboard": "Skateboard", + "surfboard": "Surfboard", + "tennis_racket": "Tennis Racket", + "bottle": "Bottle", + "plate": "Plate", + "wine_glass": "Wine Glass", + "cup": "Cup", + "fork": "Fork", + "knife": "Knife", + "spoon": "Spoon", + "bowl": "Bowl", + "banana": "Banana", + "apple": "Apple", + "sandwich": "Sandwich", + "orange": "Orange", + "broccoli": "Broccoli", + "carrot": "Carrot", + "hot_dog": "Hot Dog", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Cake", + "chair": "Chair", + "couch": "Couch", + "potted_plant": "Potted Plant", + "bed": "Bed", + "mirror": "Mirror", + "dining_table": "Dining Table", + "window": "Window", + "desk": "Desk", + "toilet": "Toilet", + "door": "Door", + "tv": "TV", + "laptop": "Laptop", + "mouse": "Mouse", + "remote": "Remote", + "keyboard": "Keyboard", + "cell_phone": "Cell Phone", + "microwave": "Microwave", + "oven": "Oven", + "toaster": "Toaster", + "sink": "Sink", + "refrigerator": "Refrigerator", + "blender": "Blender", + "book": "Book", + "clock": "Clock", + "vase": "Vase", + "scissors": "Scissors", + "teddy_bear": "Teddy Bear", + "hair_dryer": "Hair Dryer", + "toothbrush": "Toothbrush", + "hair_brush": "Hair Brush", + "vehicle": "Vehicle", + "squirrel": "Squirrel", + "deer": "Deer", + "animal": "Animal", + "bark": "Bark", + "fox": "Fox", + "goat": "Goat", + "rabbit": "Rabbit", + "raccoon": "Raccoon", + "robot_lawnmower": "Robot Lawnmower", + "waste_bin": "Waste Bin", + "on_demand": "On Demand", + "face": "Face", + "license_plate": "License Plate", + "package": "Package", + "bbq_grill": "BBQ Grill", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD", + "canada_post": "Canada Post", + "royal_mail": "Royal Mail", + "school_bus": "School Bus", + "skunk": "Skunk", + "kangaroo": "Kangaroo", + "baby": "Baby", + "baby_stroller": "Baby Stroller", + "rickshaw": "Rickshaw", + "rodent": "Rodent" +} diff --git a/web/public/locales/en/views/chat.json b/web/public/locales/en/views/chat.json new file mode 100644 index 0000000..363b0e6 --- /dev/null +++ b/web/public/locales/en/views/chat.json @@ -0,0 +1,72 @@ +{ + "documentTitle": "Chat - Frigate", + "title": "Frigate Chat", + "subtitle": "Your AI assistant for camera management and insights", + "placeholder": "Ask anything...", + "error": "Something went wrong. Please try again.", + "processing": "Processing...", + "toolsUsed": "Used: {{tools}}", + "showTools": "Show tools ({{count}})", + "hideTools": "Hide tools", + "call": "Call", + "result": "Result", + "arguments": "Arguments:", + "response": "Response:", + "attachment_chip_label": "{{label}} on {{camera}}", + "attachment_chip_remove": "Remove attachment", + "open_in_explore": "Open in Explore", + "attach_event_aria": "Attach event {{eventId}}", + "attachment_picker_paste_label": "Or paste event ID", + "attachment_picker_attach": "Attach", + "attachment_picker_placeholder": "Attach an event", + "quick_reply_find_similar": "Find similar sightings", + "quick_reply_tell_me_more": "Tell me more about this", + "quick_reply_when_else": "When else was it seen?", + "quick_reply_find_similar_text": "Find similar sightings to this.", + "quick_reply_tell_me_more_text": "Tell me more about this one.", + "quick_reply_when_else_text": "When else was this seen?", + "anchor": "Reference", + "similarity_score": "Similarity", + "no_similar_objects_found": "No similar objects found.", + "semantic_search_required": "Semantic search must be enabled to find similar objects.", + "send": "Send", + "suggested_requests": "Try asking:", + "starting_requests": { + "show_recent_events": "Show recent events", + "show_camera_status": "Show camera status", + "recap": "What happened while I was away?", + "watch_camera": "Watch a camera for activity" + }, + "starting_requests_prompts": { + "show_recent_events": "Show me the recent events from the last hour", + "show_camera_status": "What is the current status of my cameras?", + "recap": "What happened while I was away?", + "watch_camera": "Watch the front door and let me know if anyone shows up" + }, + "new_chat": "New chat", + "settings": { + "title": "Chat settings", + "show_stats": { + "title": "Show stats", + "desc": "Show generation rate and context size for chat responses.", + "while_generating": "While generating", + "always": "Always" + }, + "auto_scroll": { + "title": "Auto-scroll", + "desc": "Follow new messages as they arrive." + } + }, + "stats": { + "context": "{{tokens}} tokens", + "tokens_per_second": "{{rate}} t/s" + }, + "reasoning": { + "active": "Reasoning…", + "show": "Show reasoning", + "hide": "Hide reasoning" + }, + "thinking": { + "toggle": "Toggle thinking" + } +} diff --git a/web/public/locales/en/views/classificationModel.json b/web/public/locales/en/views/classificationModel.json new file mode 100644 index 0000000..3206ad0 --- /dev/null +++ b/web/public/locales/en/views/classificationModel.json @@ -0,0 +1,198 @@ +{ + "documentTitle": "Classification Models - Frigate", + "details": { + "scoreInfo": "Score represents the average classification confidence across all detections of this object.", + "none": "None", + "unknown": "Unknown" + }, + "button": { + "deleteClassificationAttempts": "Delete Classification Images", + "renameCategory": "Rename Class", + "deleteCategory": "Delete Class", + "deleteImages": "Delete Images", + "trainModel": "Train Model", + "addClassification": "Add Classification", + "deleteModels": "Delete Models", + "editModel": "Edit Model" + }, + "tooltip": { + "trainingInProgress": "Model is currently training", + "noNewImages": "No new images to train. Classify more images in the dataset first.", + "noChanges": "No changes to the dataset since last training.", + "modelNotReady": "Model is not ready for training" + }, + "toast": { + "success": { + "deletedModel_one": "Successfully deleted {{count}} model", + "deletedModel_other": "Successfully deleted {{count}} models", + "categorizedImage": "Successfully Classified Image", + "reclassifiedImage": "Successfully Reclassified Image", + "trainedModel": "Successfully trained model.", + "trainingModel": "Successfully started model training.", + "updatedModel": "Successfully updated model configuration", + "renamedCategory": "Successfully renamed class to {{name}}", + "deletedCategory_one": "Deleted {{count}} class", + "deletedCategory_other": "Deleted {{count}} classes", + "deletedImage_one": "Deleted {{count}} image", + "deletedImage_other": "Deleted {{count}} images" + }, + "error": { + "deleteImageFailed": "Failed to delete: {{errorMessage}}", + "deleteCategoryFailed": "Failed to delete class: {{errorMessage}}", + "deleteModelFailed": "Failed to delete model: {{errorMessage}}", + "categorizeFailed": "Failed to categorize image: {{errorMessage}}", + "trainingFailed": "Model training failed. Check Frigate logs for details.", + "trainingFailedToStart": "Failed to start model training: {{errorMessage}}", + "updateModelFailed": "Failed to update model: {{errorMessage}}", + "renameCategoryFailed": "Failed to rename class: {{errorMessage}}", + "reclassifyFailed": "Failed to reclassify image: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Delete Class", + "desc": "Are you sure you want to delete the class {{name}}? This will permanently delete all associated images and require re-training the model.", + "minClassesTitle": "Cannot Delete Class", + "minClassesDesc": "A classification model must have at least 2 classes. Add another class before deleting this one." + }, + "deleteModel": { + "title": "Delete Classification Model", + "single": "Are you sure you want to delete {{name}}? This will permanently delete all associated data including images and training data. This action cannot be undone.", + "desc_one": "Are you sure you want to delete {{count}} model? This will permanently delete all associated data including images and training data. This action cannot be undone.", + "desc_other": "Are you sure you want to delete {{count}} models? This will permanently delete all associated data including images and training data. This action cannot be undone." + }, + "edit": { + "title": "Edit Classification Model", + "descriptionState": "Edit the classes for this state classification model. Changes will require retraining the model.", + "descriptionObject": "Edit the object type and classification type for this object classification model.", + "stateClassesInfo": "Note: Changing state classes requires retraining the model with the updated classes." + }, + "deleteDatasetImages": { + "title": "Delete Dataset Images", + "desc_one": "Are you sure you want to delete {{count}} image from {{dataset}}? This action cannot be undone and will require re-training the model.", + "desc_other": "Are you sure you want to delete {{count}} images from {{dataset}}? This action cannot be undone and will require re-training the model." + }, + "deleteTrainImages": { + "title": "Delete Train Images", + "desc_one": "Are you sure you want to delete {{count}} image? This action cannot be undone.", + "desc_other": "Are you sure you want to delete {{count}} images? This action cannot be undone." + }, + "renameCategory": { + "title": "Rename Class", + "desc": "Enter a new name for {{name}}. You will be required to retrain the model for the name change to take effect." + }, + "description": { + "invalidName": "Invalid name. Names can only include letters, numbers, spaces, apostrophes, underscores, and hyphens." + }, + "train": { + "title": "Recent Classifications", + "titleShort": "Recent", + "aria": "Select Recent Classifications" + }, + "categories": "Classes", + "createCategory": { + "new": "Create New Class" + }, + "categorizeImageAs": "Classify Image As:", + "categorizeImage": "Classify Image", + "reclassifyImageAs": "Reclassify Image As:", + "reclassifyImage": "Reclassify Image", + "menu": { + "objects": "Objects", + "states": "States" + }, + "noModels": { + "object": { + "title": "No Object Classification Models", + "description": "Create a custom model to classify detected objects.", + "buttonText": "Create Object Model" + }, + "state": { + "title": "No State Classification Models", + "description": "Create a custom model to monitor and classify state changes in specific camera areas.", + "buttonText": "Create State Model" + } + }, + "wizard": { + "title": "Create New Classification", + "steps": { + "nameAndDefine": "Name & Define", + "stateArea": "State Area", + "chooseExamples": "Choose Examples" + }, + "step1": { + "description": "State models monitor fixed camera areas for changes (e.g., door open/closed). Object models add classifications to detected objects (e.g., known animals, delivery persons, etc.).", + "name": "Name", + "namePlaceholder": "Enter model name...", + "type": "Type", + "typeState": "State", + "typeObject": "Object", + "objectLabel": "Object Label", + "objectLabelPlaceholder": "Select object type...", + "classificationType": "Classification Type", + "classificationTypeTip": "Learn about classification types", + "classificationTypeDesc": "Sub Labels add additional text to the object label (e.g., 'Person: UPS'). Attributes are searchable metadata stored separately in the object metadata.", + "classificationSubLabel": "Sub Label", + "classificationAttribute": "Attribute", + "classes": "Classes", + "states": "States", + "classesTip": "Learn about classes", + "classesStateDesc": "Define the different states your camera area can be in. For example: 'open' and 'closed' for a garage door.", + "classesObjectDesc": "Define the different categories to classify detected objects into. For example: 'delivery_person', 'resident', 'stranger' for person classification.", + "classPlaceholder": "Enter class name...", + "errors": { + "nameRequired": "Model name is required", + "nameLength": "Model name must be 64 characters or less", + "nameOnlyNumbers": "Model name cannot contain only numbers", + "classRequired": "At least 1 class is required", + "classesUnique": "Class names must be unique", + "noneNotAllowed": "The class 'none' is not allowed", + "stateRequiresTwoClasses": "State models require at least 2 classes", + "objectLabelRequired": "Please select an object label", + "objectTypeRequired": "Please select a classification type" + } + }, + "step2": { + "description": "Select cameras and define the area to monitor for each camera. The model will classify the state of these areas.", + "cameras": "Cameras", + "selectCamera": "Select Camera", + "noCameras": "Click + to add cameras", + "selectCameraPrompt": "Select a camera from the list to define its monitoring area" + }, + "step3": { + "selectImagesPrompt": "Select all images with: {{className}}", + "selectImagesDescription": "Click on images to select them. Click Continue when you're done with this class.", + "allImagesRequired_one": "Please classify all images. {{count}} image remaining.", + "allImagesRequired_other": "Please classify all images. {{count}} images remaining.", + "generating": { + "title": "Generating Sample Images", + "description": "Frigate is pulling representative images from your recordings. This may take a moment..." + }, + "training": { + "title": "Training Model", + "description": "Your model is being trained in the background. Close this dialog, and your model will start running as soon as training is complete." + }, + "retryGenerate": "Retry Generation", + "noImages": "No sample images generated", + "classifying": "Classifying & Training...", + "trainingStarted": "Training started successfully", + "modelCreated": "Model created successfully. Use the Recent Classifications view to add images for missing states, then train the model.", + "errors": { + "noCameras": "No cameras configured", + "noObjectLabel": "No object label selected", + "generateFailed": "Failed to generate examples: {{error}}", + "generationFailed": "Generation failed. Please try again.", + "classifyFailed": "Failed to classify images: {{error}}" + }, + "generateSuccess": "Successfully generated sample images", + "refreshExamples": "Generate new examples", + "refreshConfirm": { + "title": "Generate New Examples?", + "description": "This will generate a new set of images and clear all selections, including any previous classes. You will need to re-select examples for all classes." + }, + "missingStatesWarning": { + "title": "Missing Class Examples", + "description": "Not all classes have examples. Try generating new examples to find the missing class, or continue and use the Recent Classifications view to add images later." + } + } + } +} diff --git a/web/public/locales/en/views/configEditor.json b/web/public/locales/en/views/configEditor.json new file mode 100644 index 0000000..614143c --- /dev/null +++ b/web/public/locales/en/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Config Editor - Frigate", + "configEditor": "Config Editor", + "safeConfigEditor": "Config Editor (Safe Mode)", + "safeModeDescription": "Frigate is in safe mode due to a config validation error.", + "copyConfig": "Copy Config", + "saveAndRestart": "Save & Restart", + "saveOnly": "Save Only", + "confirm": "Exit without saving?", + "toast": { + "success": { + "copyToClipboard": "Config copied to clipboard." + }, + "error": { + "savingError": "Error saving config" + } + } +} diff --git a/web/public/locales/en/views/events.json b/web/public/locales/en/views/events.json new file mode 100644 index 0000000..f25e302 --- /dev/null +++ b/web/public/locales/en/views/events.json @@ -0,0 +1,92 @@ +{ + "alerts": "Alerts", + "detections": "Detections", + "motion": { + "label": "Motion", + "only": "Motion only" + }, + "allCameras": "All Cameras", + "empty": { + "alert": "There are no alerts to review", + "detection": "There are no detections to review", + "motion": "No motion data found", + "recordingsDisabled": { + "title": "Recordings must be enabled", + "description": "Review items can only be created for a camera when recordings are enabled for that camera." + } + }, + "timeline": { + "label": "Timeline", + "aria": "Select timeline" + }, + "zoomIn": "Zoom In", + "zoomOut": "Zoom Out", + "events": { + "label": "Events", + "aria": "Select events", + "noFoundForTimePeriod": "No events found for this time period." + }, + "detail": { + "label": "Detail", + "noDataFound": "No detail data to review", + "aria": "Toggle detail view", + "trackedObject_one": "{{count}} object", + "trackedObject_other": "{{count}} objects", + "noObjectDetailData": "No object detail data available.", + "settings": "Detail View Settings", + "alwaysExpandActive": { + "title": "Always expand active", + "desc": "Always expand the active review item's object details when available." + } + }, + "objectTrack": { + "trackedPoint": "Tracked point", + "clickToSeek": "Click to seek to this time" + }, + "documentTitle": "Review - Frigate", + "recordings": { + "documentTitle": "Recordings - Frigate", + "invalidSharedLink": "Unable to open timestamped recording link due to parsing error.", + "invalidSharedCamera": "Unable to open timestamped recording link due to an unknown or unauthorized camera." + }, + "calendarFilter": { + "last24Hours": "Last 24 Hours" + }, + "markAsReviewed": "Mark as Reviewed", + "markTheseItemsAsReviewed": "Mark these items as reviewed", + "newReviewItems": { + "label": "View new review items", + "button": "New Items To Review" + }, + "selected_one": "{{count}} selected", + "selected_other": "{{count}} selected", + "select_all": "All", + "camera": "Camera", + "detected": "detected", + "normalActivity": "Normal", + "needsReview": "Needs review", + "securityConcern": "Security concern", + "motionSearch": { + "menuItem": "Motion Search", + "openMenu": "Camera options" + }, + "motionPreviews": { + "menuItem": "View motion previews", + "title": "Motion previews: {{camera}}", + "mobileSettingsTitle": "Motion Preview Settings", + "mobileSettingsDesc": "Adjust playback speed and dimming, and choose a date to review motion-only clips.", + "dim": "Dim", + "dimAria": "Adjust dimming intensity", + "dimDesc": "Increase dimming to increase motion area visibility.", + "speed": "Speed", + "speedAria": "Select preview playback speed", + "speedDesc": "Choose how quickly preview clips play.", + "back": "Back", + "empty": "No previews available", + "noPreview": "Preview unavailable", + "seekAria": "Seek {{camera}} player to {{time}}", + "filter": "Filter", + "filterDesc": "Select areas to only show clips with motion in those regions.", + "filterClear": "Clear" + } +} diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json new file mode 100644 index 0000000..c85ee90 --- /dev/null +++ b/web/public/locales/en/views/explore.json @@ -0,0 +1,265 @@ +{ + "documentTitle": "Explore - Frigate", + "generativeAI": "Generative AI", + "exploreMore": "Explore more {{label}} objects", + "exploreIsUnavailable": { + "title": "Explore is Unavailable", + "embeddingsReindexing": { + "context": "Explore can be used after tracked object embeddings have finished reindexing.", + "startingUp": "Starting up…", + "estimatedTime": "Estimated time remaining:", + "finishingShortly": "Finishing shortly", + "step": { + "thumbnailsEmbedded": "Thumbnails embedded: ", + "descriptionsEmbedded": "Descriptions embedded: ", + "trackedObjectsProcessed": "Tracked objects processed: " + } + }, + "downloadingModels": { + "context": "Frigate is downloading the necessary embeddings models to support the Semantic Search feature. This may take several minutes depending on the speed of your network connection.", + "setup": { + "visionModel": "Vision model", + "visionModelFeatureExtractor": "Vision model feature extractor", + "textModel": "Text model", + "textTokenizer": "Text tokenizer" + }, + "tips": { + "context": "You may want to reindex the embeddings of your tracked objects once the models are downloaded." + }, + "error": "An error has occurred. Check Frigate logs." + } + }, + "trackedObjectDetails": "Tracked Object Details", + "type": { + "details": "details", + "snapshot": "snapshot", + "thumbnail": "thumbnail", + "video": "video", + "tracking_details": "tracking details" + }, + "trackingDetails": { + "title": "Tracking Details", + "noImageFound": "No image found for this timestamp.", + "createObjectMask": "Create Object Mask", + "adjustAnnotationSettings": "Adjust annotation settings", + "scrollViewTips": "Click to view the significant moments of this object's lifecycle.", + "autoTrackingTips": "Bounding box positions will be inaccurate for autotracking cameras.", + "count": "{{first}} of {{second}}", + "trackedPoint": "Tracked Point", + "lifecycleItemDesc": { + "visible": "{{label}} detected", + "entered_zone": "{{label}} entered {{zones}}", + "active": "{{label}} became active", + "stationary": "{{label}} became stationary", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detected for {{label}}", + "other": "{{label}} recognized as {{attribute}}" + }, + "gone": "{{label}} left", + "heard": "{{label}} heard", + "external": "{{label}} detected", + "header": { + "zones": "Zones", + "ratio": "Ratio", + "area": "Area", + "score": "Score", + "computedScore": "Computed Score", + "topScore": "Top Score", + "toggleAdvancedScores": "Toggle advanced scores" + } + }, + "annotationSettings": { + "title": "Annotation Settings", + "showAllZones": { + "title": "Show All Zones", + "desc": "Always show zones on frames where objects have entered a zone." + }, + "offset": { + "label": "Annotation Offset", + "desc": "This data comes from your camera's detect feed but is overlaid on images from the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. You can use this setting to offset the annotations forward or backward in time to better align them with the recorded footage.", + "millisecondsToOffset": "Milliseconds to offset detect annotations by. Default: 0", + "tips": "Lower the value if the video playback is ahead of the boxes and path points, and increase the value if the video playback is behind them. This value can be negative.", + "toast": { + "success": "Annotation offset for {{camera}} has been saved to the config file." + } + } + }, + "carousel": { + "previous": "Previous slide", + "next": "Next slide" + } + }, + "details": { + "item": { + "title": "Review Item Details", + "desc": "Review item details", + "button": { + "share": "Share this review item", + "viewInExplore": "View in Explore" + }, + "tips": { + "mismatch_one": "{{count}} unavailable object was detected and included in this review item. Those objects either did not qualify as an alert or detection or have already been cleaned up/deleted.", + "mismatch_other": "{{count}} unavailable objects were detected and included in this review item. Those objects either did not qualify as an alert or detection or have already been cleaned up/deleted.", + "hasMissingObjects": "Adjust your configuration if you want Frigate to save tracked objects for the following labels: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "A new description has been requested from {{provider}}. Depending on the speed of your provider, the new description may take some time to regenerate.", + "updatedSublabel": "Successfully updated sub label.", + "updatedLPR": "Successfully updated license plate.", + "updatedAttributes": "Successfully updated attributes.", + "audioTranscription": "Successfully requested audio transcription. Depending on the speed of your Frigate server, the transcription may take some time to complete." + }, + "error": { + "regenerate": "Failed to call {{provider}} for a new description: {{errorMessage}}", + "updatedSublabelFailed": "Failed to update sub label: {{errorMessage}}", + "updatedLPRFailed": "Failed to update license plate: {{errorMessage}}", + "updatedAttributesFailed": "Failed to update attributes: {{errorMessage}}", + "audioTranscription": "Failed to request audio transcription: {{errorMessage}}" + } + } + }, + "label": "Label", + "editSubLabel": { + "title": "Edit sub label", + "desc": "Enter a new sub label for this {{label}}", + "descNoLabel": "Enter a new sub label for this tracked object" + }, + "editLPR": { + "title": "Edit license plate", + "desc": "Enter a new license plate value for this {{label}}", + "descNoLabel": "Enter a new license plate value for this tracked object" + }, + "editAttributes": { + "title": "Edit attributes", + "desc": "Select classification attributes for this {{label}}" + }, + "snapshotScore": { + "label": "Snapshot Score" + }, + "topScore": { + "label": "Top Score", + "info": "The top score is the highest median score for the tracked object, so this may differ from the score shown on the search result thumbnail." + }, + "score": { + "label": "Score" + }, + "recognizedLicensePlate": "Recognized License Plate", + "attributes": "Classification Attributes", + "estimatedSpeed": "Estimated Speed", + "objects": "Objects", + "camera": "Camera", + "zones": "Zones", + "timestamp": "Timestamp", + "button": { + "findSimilar": "Find Similar", + "regenerate": { + "title": "Regenerate", + "label": "Regenerate tracked object description" + } + }, + "description": { + "label": "Description", + "placeholder": "Description of the tracked object", + "aiTips": "Frigate will not request a description from your Generative AI provider until the tracked object's lifecycle has ended." + }, + "expandRegenerationMenu": "Expand regeneration menu", + "regenerateFromSnapshot": "Regenerate from Snapshot", + "regenerateFromThumbnails": "Regenerate from Thumbnails", + "tips": { + "descriptionSaved": "Successfully saved description", + "saveDescriptionFailed": "Failed to update the description: {{errorMessage}}" + }, + "title": { + "label": "Title" + }, + "scoreInfo": "Score Information" + }, + "itemMenu": { + "downloadVideo": { + "label": "Download video", + "aria": "Download video" + }, + "downloadSnapshot": { + "label": "Download snapshot", + "aria": "Download snapshot" + }, + "downloadCleanSnapshot": { + "label": "Download clean snapshot", + "aria": "Download clean snapshot" + }, + "viewTrackingDetails": { + "label": "View tracking details", + "aria": "Show the tracking details" + }, + "findSimilar": { + "label": "Find similar", + "aria": "Find similar tracked objects" + }, + "addTrigger": { + "label": "Add trigger", + "aria": "Add a trigger for this tracked object" + }, + "audioTranscription": { + "label": "Transcribe", + "aria": "Request audio transcription" + }, + "submitToPlus": { + "label": "Submit to Frigate+", + "aria": "Submit to Frigate Plus" + }, + "viewInHistory": { + "label": "View in History", + "aria": "View in History" + }, + "deleteTrackedObject": { + "label": "Delete this tracked object" + }, + "showObjectDetails": { + "label": "Show object path" + }, + "hideObjectDetails": { + "label": "Hide object path" + }, + "debugReplay": { + "label": "Debug Replay", + "aria": "View this tracked object in the debug replay view" + }, + "more": { + "aria": "More" + } + }, + "dialog": { + "confirmDelete": { + "title": "Confirm Delete", + "desc": "Deleting this tracked object removes the snapshot, any saved embeddings, and any associated tracking details entries. Recorded footage of this tracked object in History view will NOT be deleted.

    Are you sure you want to proceed?" + }, + "toast": { + "error": "Error deleting this tracked object: {{errorMessage}}" + } + }, + "noTrackedObjects": "No Tracked Objects Found", + "fetchingTrackedObjectsFailed": "Error fetching tracked objects: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} tracked object ", + "trackedObjectsCount_other": "{{count}} tracked objects ", + "searchResult": { + "tooltip": "Matched {{type}} at {{confidence}}%", + "previousTrackedObject": "Previous tracked object", + "nextTrackedObject": "Next tracked object", + "deleteTrackedObject": { + "toast": { + "success": "Tracked object deleted successfully.", + "error": "Failed to delete tracked object: {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "AI Analysis" + }, + "concerns": { + "label": "Concerns" + }, + "objectLifecycle": { + "noImageFound": "No image found for this tracked object." + } +} diff --git a/web/public/locales/en/views/exports.json b/web/public/locales/en/views/exports.json new file mode 100644 index 0000000..0dd47d3 --- /dev/null +++ b/web/public/locales/en/views/exports.json @@ -0,0 +1,128 @@ +{ + "documentTitle": "Export - Frigate", + "search": "Search", + "noExports": "No exports found", + "headings": { + "cases": "Cases", + "uncategorizedExports": "Uncategorized Exports" + }, + "deleteExport": { + "label": "Delete Export", + "desc": "Are you sure you want to delete {{exportName}}?" + }, + "editExport": { + "title": "Rename Export", + "desc": "Enter a new name for this export.", + "saveExport": "Save Export" + }, + "tooltip": { + "shareExport": "Share export", + "downloadVideo": "Download video", + "editName": "Edit name", + "deleteExport": "Delete export", + "assignToCase": "Add to case", + "removeFromCase": "Remove from case" + }, + "toolbar": { + "newCase": "New Case", + "addExport": "Add Export", + "editCase": "Edit Case", + "deleteCase": "Delete Case" + }, + "toast": { + "error": { + "renameExportFailed": "Failed to rename export: {{errorMessage}}", + "assignCaseFailed": "Failed to update case assignment: {{errorMessage}}", + "caseSaveFailed": "Failed to save case: {{errorMessage}}", + "caseDeleteFailed": "Failed to delete case: {{errorMessage}}" + } + }, + "deleteCase": { + "label": "Delete Case", + "desc": "Are you sure you want to delete {{caseName}}?", + "descKeepExports": "Exports will remain available as uncategorized exports.", + "descDeleteExports": "All exports in this case will be permanently deleted.", + "deleteExports": "Also delete exports" + }, + "caseDialog": { + "title": "Add to case", + "description": "Choose an existing case or create a new one.", + "selectLabel": "Case", + "newCaseOption": "Create new case", + "nameLabel": "Case name", + "descriptionLabel": "Description" + }, + "caseCard": { + "emptyCase": "No exports yet" + }, + "jobCard": { + "defaultName": "{{camera}} export", + "queued": "Queued", + "running": "Running", + "preparing": "Preparing", + "copying": "Copying", + "encoding": "Encoding", + "encodingRetry": "Encoding (retry)", + "finalizing": "Finalizing" + }, + "caseView": { + "noDescription": "No description", + "createdAt": "Created {{value}}", + "exportCount_one": "1 export", + "exportCount_other": "{{count}} exports", + "cameraCount_one": "1 camera", + "cameraCount_other": "{{count}} cameras", + "showMore": "Show more", + "showLess": "Show less", + "emptyTitle": "This case is empty", + "emptyDescription": "Add existing uncategorized exports to keep the case organized.", + "emptyDescriptionNoExports": "There are no uncategorized exports available to add yet." + }, + "caseEditor": { + "createTitle": "Create Case", + "editTitle": "Edit Case", + "namePlaceholder": "Case name", + "descriptionPlaceholder": "Add notes or context for this case" + }, + "addExportDialog": { + "title": "Add Export to {{caseName}}", + "searchPlaceholder": "Search uncategorized exports", + "empty": "No uncategorized exports match this search.", + "addButton_one": "Add 1 Export", + "addButton_other": "Add {{count}} Exports", + "adding": "Adding..." + }, + "selected_one": "{{count}} selected", + "selected_other": "{{count}} selected", + "bulkActions": { + "addToCase": "Add to Case", + "moveToCase": "Move to Case", + "removeFromCase": "Remove from Case", + "delete": "Delete", + "deleteNow": "Delete Now" + }, + "bulkDelete": { + "title": "Delete Exports", + "desc_one": "Are you sure you want to delete {{count}} export?", + "desc_other": "Are you sure you want to delete {{count}} exports?" + }, + "bulkRemoveFromCase": { + "title": "Remove from Case", + "desc_one": "Remove {{count}} export from this case?", + "desc_other": "Remove {{count}} exports from this case?", + "descKeepExports": "Exports will be moved to uncategorized.", + "descDeleteExports": "Exports will be permanently deleted.", + "deleteExports": "Delete exports instead" + }, + "bulkToast": { + "success": { + "delete": "Successfully deleted exports", + "reassign": "Successfully updated case assignment", + "remove": "Successfully removed exports from case" + }, + "error": { + "deleteFailed": "Failed to delete exports: {{errorMessage}}", + "reassignFailed": "Failed to update case assignment: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/en/views/faceLibrary.json b/web/public/locales/en/views/faceLibrary.json new file mode 100644 index 0000000..27e5454 --- /dev/null +++ b/web/public/locales/en/views/faceLibrary.json @@ -0,0 +1,100 @@ +{ + "description": { + "addFace": "Add a new collection to the Face Library by uploading your first image.", + "placeholder": "Enter a name for this collection", + "invalidName": "Invalid name. Names can only include letters, numbers, spaces, apostrophes, underscores, and hyphens.", + "nameCannotContainHash": "Name cannot contain #." + }, + "details": { + "timestamp": "Timestamp", + "unknown": "Unknown", + "scoreInfo": "Score is a weighted average of all face scores, weighted by the size of the face in each image." + }, + "documentTitle": "Face Library - Frigate", + "uploadFaceImage": { + "title": "Upload Face Image", + "desc": "Upload an image to scan for faces and include for {{pageToggle}}" + }, + "collections": "Collections", + "createFaceLibrary": { + "new": "Create New Face", + "nextSteps": "To build a strong foundation:
  • Use the Recent Recognitions tab to select and train on images for each detected person.
  • Focus on straight-on images for best results; avoid training images that capture faces at an angle.
  • " + }, + "steps": { + "faceName": "Enter Face Name", + "uploadFace": "Upload Face Image", + "nextSteps": "Next Steps", + "description": { + "uploadFace": "Upload an image of {{name}} that shows their face from a front-facing angle. The image does not need to be cropped to just their face." + } + }, + "train": { + "title": "Recent Recognitions", + "titleShort": "Recent", + "aria": "Select recent recognitions", + "empty": "There are no recent face recognition attempts", + "emptyNoLibrary": { + "title": "Upload a face", + "description": "You must add at least one face to the library for face recognition to function." + } + }, + "deleteFaceLibrary": { + "title": "Delete Name", + "desc": "Are you sure you want to delete the collection {{name}}? This will permanently delete all associated faces." + }, + "deleteFaceAttempts": { + "title": "Delete Faces", + "desc_one": "Are you sure you want to delete {{count}} face? This action cannot be undone.", + "desc_other": "Are you sure you want to delete {{count}} faces? This action cannot be undone." + }, + "renameFace": { + "title": "Rename Face", + "desc": "Enter a new name for {{name}}" + }, + "button": { + "deleteFaceAttempts": "Delete Faces", + "addFace": "Add Face", + "renameFace": "Rename Face", + "deleteFace": "Delete Face", + "uploadImage": "Upload Image", + "reprocessFace": "Reprocess Face" + }, + "imageEntry": { + "validation": { + "selectImage": "Please select an image file." + }, + "dropActive": "Drop the image here…", + "dropInstructions": "Drag and drop or paste an image here, or click to select", + "maxSize": "Max size: {{size}}MB" + }, + "nofaces": "No faces available", + "trainFaceAs": "Train Face as:", + "trainFace": "Train Face", + "reclassifyFaceAs": "Reclassify Face as:", + "reclassifyFace": "Reclassify Face", + "toast": { + "success": { + "uploadedImage": "Successfully uploaded image.", + "addFaceLibrary": "{{name}} has successfully been added to the Face Library!", + "deletedFace_one": "Successfully deleted {{count}} face.", + "deletedFace_other": "Successfully deleted {{count}} faces.", + "deletedName_zero": "Empty collection deleted successfully.", + "deletedName_one": "{{count}} face has been successfully deleted.", + "deletedName_other": "{{count}} faces have been successfully deleted.", + "renamedFace": "Successfully renamed face to {{name}}", + "trainedFace": "Successfully trained face.", + "reclassifiedFace": "Successfully reclassified face.", + "updatedFaceScore": "Successfully updated face score to {{name}} ({{score}})." + }, + "error": { + "uploadingImageFailed": "Failed to upload image: {{errorMessage}}", + "addFaceLibraryFailed": "Failed to set face name: {{errorMessage}}", + "deleteFaceFailed": "Failed to delete: {{errorMessage}}", + "deleteNameFailed": "Failed to delete name: {{errorMessage}}", + "renameFaceFailed": "Failed to rename face: {{errorMessage}}", + "trainFailed": "Failed to train: {{errorMessage}}", + "reclassifyFailed": "Failed to reclassify face: {{errorMessage}}", + "updateFaceScoreFailed": "Failed to update face score: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json new file mode 100644 index 0000000..f2ea272 --- /dev/null +++ b/web/public/locales/en/views/live.json @@ -0,0 +1,200 @@ +{ + "documentTitle": { + "default": "Live - Frigate", + "withCamera": "{{camera}} - Live - Frigate" + }, + "lowBandwidthMode": "Low-bandwidth Mode", + "twoWayTalk": { + "enable": "Enable Two Way Talk", + "disable": "Disable Two Way Talk" + }, + "cameraAudio": { + "enable": "Enable Camera Audio", + "disable": "Disable Camera Audio" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Click in the frame to center the camera", + "enable": "Enable click to move", + "enableWithZoom": "Enable click to move / drag to zoom", + "disable": "Disable click to move" + }, + "left": { + "label": "Move PTZ camera to the left" + }, + "up": { + "label": "Move PTZ camera up" + }, + "down": { + "label": "Move PTZ camera down" + }, + "right": { + "label": "Move PTZ camera to the right" + } + }, + "zoom": { + "in": { + "label": "Zoom PTZ camera in" + }, + "out": { + "label": "Zoom PTZ camera out" + } + }, + "focus": { + "in": { + "label": "Focus PTZ camera in" + }, + "out": { + "label": "Focus PTZ camera out" + } + }, + "frame": { + "center": { + "label": "Click in the frame to center the PTZ camera" + } + }, + "presets": "PTZ camera presets" + }, + "camera": { + "turnOn": "Turn Camera On", + "turnOff": "Turn Camera Off" + }, + "muteCameras": { + "enable": "Mute All Cameras", + "disable": "Unmute All Cameras" + }, + "detect": { + "enable": "Enable Detect", + "disable": "Disable Detect" + }, + "recording": { + "enable": "Enable Recording", + "disable": "Disable Recording", + "disabledInConfig": "Recording must first be enabled in Settings for this camera." + }, + "snapshots": { + "enable": "Enable Snapshots", + "disable": "Disable Snapshots" + }, + "snapshot": { + "takeSnapshot": "Download instant snapshot", + "noVideoSource": "No video source available for snapshot.", + "captureFailed": "Failed to capture snapshot.", + "downloadStarted": "Snapshot download started." + }, + "audioDetect": { + "enable": "Enable Audio Detect", + "disable": "Disable Audio Detect" + }, + "transcription": { + "enable": "Enable Live Audio Transcription", + "disable": "Disable Live Audio Transcription" + }, + "autotracking": { + "enable": "Enable Autotracking", + "disable": "Disable Autotracking" + }, + "streamStats": { + "enable": "Show Stream Stats", + "disable": "Hide Stream Stats" + }, + "manualRecording": { + "title": "On-Demand", + "tips": "Download an instant snapshot or start a manual event based on this camera's recording retention settings.", + "playInBackground": { + "label": "Play in background", + "desc": "Enable this option to continue streaming when the player is hidden." + }, + "showStats": { + "label": "Show Stats", + "desc": "Enable this option to show stream statistics as an overlay on the camera feed." + }, + "debugView": "Debug View", + "start": "Start on-demand recording", + "started": "Started manual on-demand recording.", + "failedToStart": "Failed to start manual on-demand recording.", + "recordDisabledTips": "Since recording is disabled or restricted in the config for this camera, only a snapshot will be saved.", + "end": "End on-demand recording", + "ended": "Ended manual on-demand recording.", + "failedToEnd": "Failed to end manual on-demand recording." + }, + "streamingSettings": "Streaming Settings", + "notifications": "Notifications", + "audio": "Audio", + "suspend": { + "forTime": "Suspend for: " + }, + "stream": { + "title": "Stream", + "audio": { + "tips": { + "title": "Audio must be output from your camera and configured in go2rtc for this stream." + }, + "available": "Audio is available for this stream", + "unavailable": "Audio is not available for this stream" + }, + "debug": { + "picker": "Stream selection unavailable in debug mode. Debug view always uses the stream assigned the detect role." + }, + "twoWayTalk": { + "tips": "Your device must support the feature and WebRTC must be configured for two-way talk.", + "available": "Two-way talk is available for this stream", + "unavailable": "Two-way talk is unavailable for this stream" + }, + "lowBandwidth": { + "tips": "Live view is in low-bandwidth mode due to buffering or stream errors.", + "resetStream": "Reset stream" + }, + "playInBackground": { + "label": "Play in background", + "tips": "Enable this option to continue streaming when the player is hidden." + } + }, + "cameraSettings": { + "title": "{{camera}} Settings", + "camera": "Camera", + "objectDetection": "Object Detection", + "recording": "Recording", + "snapshots": "Snapshots", + "audioDetection": "Audio Detection", + "transcription": "Audio Transcription", + "autotracking": "Autotracking" + }, + "history": { + "label": "Show historical footage" + }, + "effectiveRetainMode": { + "modes": { + "all": "All", + "motion": "Motion", + "active_objects": "Active Objects" + } + }, + "editLayout": { + "label": "Edit Layout", + "group": { + "label": "Edit Camera Group" + }, + "exitEdit": "Exit Editing" + }, + "noCameras": { + "title": "No Cameras Configured", + "description": "Get started by connecting a camera to Frigate.", + "buttonText": "Add Camera", + "restricted": { + "title": "No Cameras Available", + "description": "You don't have permission to view any cameras in this group." + }, + "default": { + "title": "No Cameras Configured", + "description": "Get started by connecting a camera to Frigate.", + "buttonText": "Add Camera" + }, + "group": { + "title": "No Cameras in Group", + "description": "This camera group has no assigned or enabled cameras.", + "buttonText": "Manage Groups" + } + } +} diff --git a/web/public/locales/en/views/motionSearch.json b/web/public/locales/en/views/motionSearch.json new file mode 100644 index 0000000..8358bfb --- /dev/null +++ b/web/public/locales/en/views/motionSearch.json @@ -0,0 +1,78 @@ +{ + "documentTitle": "Motion Search - Frigate", + "title": "Motion Search", + "description": "Draw a polygon to define the region of interest, and specify a time range to search for motion changes within that region.", + "selectCamera": "Motion Search is loading", + "startSearch": "Start Search", + "searchStarted": "Search started", + "searchCancelled": "Search cancelled", + "cancelSearch": "Cancel", + "searching": "Search in progress.", + "scanning": "Scanning {{time}}", + "searchComplete": "Search complete", + "noResultsYet": "Run a search to find motion changes in the selected region", + "noChangesFound": "No pixel changes detected in the selected region", + "changesFound_one": "Found {{count}} motion change", + "changesFound_other": "Found {{count}} motion changes", + "framesProcessed": "{{count}} frames processed", + "jumpToTime": "Jump to this time", + "results": "Results", + "showSegmentHeatmap": "Heatmap", + "newSearch": "New Search", + "clearResults": "Clear Results", + "clearROI": "Clear polygon", + "polygonControls": { + "points_one": "{{count}} point", + "points_other": "{{count}} points", + "undo": "Undo last point", + "reset": "Reset polygon", + "drawMode": "Draw", + "moveMode": "Move" + }, + "motionHeatmapLabel": "Motion Heatmap", + "dialog": { + "title": "Motion Search", + "cameraLabel": "Camera", + "previewAlt": "Camera preview for {{camera}}" + }, + "timeRange": { + "title": "Search Range", + "start": "Start time", + "end": "End time" + }, + "settings": { + "title": "Search Settings", + "parallelMode": "Parallel mode", + "parallelModeDesc": "Scan multiple recording ranges at the same time (faster; uses more decoding resources)", + "threshold": "Sensitivity Threshold", + "thresholdDesc": "Lower values detect smaller changes (1-255)", + "minArea": "Minimum Change Area", + "minAreaDesc": "Minimum size of a single moving region, as a percentage of the region of interest", + "maxResults": "Maximum Results", + "maxResultsDesc": "Stop after this many matching timestamps" + }, + "errors": { + "noCamera": "Please select a camera", + "noROI": "Please draw a region of interest", + "noTimeRange": "Please select a time range", + "invalidTimeRange": "End time must be after start time", + "searchFailed": "Search failed: {{message}}", + "polygonTooSmall": "Polygon must have at least 3 points", + "unknown": "Unknown error" + }, + "changePercentage": "{{percentage}}% changed", + "metrics": { + "title": "Search Metrics", + "segmentsScanned": "Segments scanned", + "segmentsProcessed": "Processed", + "segmentsSkippedInactive": "Skipped (no activity)", + "segmentsSkippedHeatmap": "Skipped (no ROI overlap)", + "fallbackFullRange": "Fallback full-range scan", + "framesDecoded": "Frames decoded", + "wallTime": "Search time", + "segmentErrors": "Segment errors", + "seconds": "{{seconds}}s", + "minutesSeconds": "{{minutes}}m {{seconds}}s", + "scanSummary": "{{segments}} segments · {{time}}" + } +} diff --git a/web/public/locales/en/views/recording.json b/web/public/locales/en/views/recording.json new file mode 100644 index 0000000..9ca7c43 --- /dev/null +++ b/web/public/locales/en/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Export", + "calendar": "Calendar", + "filter": "Filter", + "filters": "Filters", + "toast": { + "error": { + "noValidTimeSelected": "No valid time range selected", + "endTimeMustAfterStartTime": "End time must be after start time" + } + } +} diff --git a/web/public/locales/en/views/replay.json b/web/public/locales/en/views/replay.json new file mode 100644 index 0000000..e8f50d7 --- /dev/null +++ b/web/public/locales/en/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "Debug Replay", + "description": "Replay camera recordings for debugging. The object list shows a time-delayed summary of detected objects and the Messages tab shows a stream of Frigate's internal messages from the replay footage.", + "websocket_messages": "Messages", + "dialog": { + "title": "Start Debug Replay", + "description": "Create a temporary replay camera that loops historical footage for debugging object detection and tracking issues. The replay camera will have the same detection configuration as the source camera. Choose a time range to begin.", + "camera": "Source Camera", + "timeRange": "Time Range", + "preset": { + "1m": "Last 1 Minute", + "5m": "Last 5 Minutes", + "timeline": "From Timeline", + "custom": "Custom" + }, + "startButton": "Start Replay", + "selectFromTimeline": "Select", + "starting": "Starting replay...", + "startLabel": "Start", + "endLabel": "End", + "toast": { + "error": "Failed to start debug replay: {{error}}", + "alreadyActive": "A replay session is already active", + "stopError": "Failed to stop debug replay: {{error}}", + "goToReplay": "Go to Replay" + } + }, + "page": { + "noSession": "No Active Debug Replay Session", + "noSessionDesc": "Start a Debug Replay from History view by clicking the Actions button in the toolbar and choosing Debug Replay.", + "goToRecordings": "Go to History", + "preparingClip": "Preparing clip…", + "preparingClipDesc": "Frigate is stitching together recordings for the selected time range. This can take a minute for longer ranges.", + "startingCamera": "Starting Debug Replay…", + "startError": { + "title": "Failed to start Debug Replay", + "back": "Back to History" + }, + "sourceCamera": "Source Camera", + "replayCamera": "Replay Camera", + "initializingReplay": "Initializing Debug Replay...", + "stoppingReplay": "Stopping Debug Replay...", + "stopReplay": "Stop Replay", + "confirmStop": { + "title": "Stop Debug Replay?", + "description": "This will stop the session and clean up all temporary data. Are you sure?", + "confirm": "Stop Replay", + "cancel": "Cancel" + }, + "activity": "Activity", + "objects": "Object List", + "audioDetections": "Audio Detections", + "noActivity": "No activity detected", + "activeTracking": "Active tracking", + "noActiveTracking": "No active tracking", + "configuration": "Configuration", + "configurationDesc": "Fine tune motion detection and object tracking settings for the Debug Replay camera. No changes are saved to your Frigate configuration file." + } +} diff --git a/web/public/locales/en/views/search.json b/web/public/locales/en/views/search.json new file mode 100644 index 0000000..dae622c --- /dev/null +++ b/web/public/locales/en/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Search", + "savedSearches": "Saved Searches", + "searchFor": "Search for {{inputValue}}", + "button": { + "clear": "Clear search", + "save": "Save search", + "delete": "Delete saved search", + "filterInformation": "Filter information", + "filterActive": "Filters active" + }, + "trackedObjectId": "Tracked Object ID", + "filter": { + "label": { + "cameras": "Cameras", + "labels": "Labels", + "zones": "Zones", + "sub_labels": "Sub Labels", + "attributes": "Attributes", + "search_type": "Search Type", + "time_range": "Time Range", + "before": "Before", + "after": "After", + "min_score": "Min Score", + "max_score": "Max Score", + "min_speed": "Min Speed", + "max_speed": "Max Speed", + "recognized_license_plate": "Recognized License Plate", + "has_clip": "Has Clip", + "has_snapshot": "Has Snapshot" + }, + "searchType": { + "thumbnail": "Thumbnail", + "description": "Description" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "The 'before' date must be later than the 'after' date.", + "afterDatebeEarlierBefore": "The 'after' date must be earlier than the 'before' date.", + "minScoreMustBeLessOrEqualMaxScore": "The 'min_score' must be less than or equal to the 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "The 'max_score' must be greater than or equal to the 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "The 'min_speed' must be less than or equal to the 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "The 'max_speed' must be greater than or equal to the 'min_speed'." + } + }, + "tips": { + "title": "How to use text filters", + "desc": { + "text": "Filters help you narrow down your search results. Here's how to use them in the input field:", + "step1": "Type a filter key name followed by a colon (e.g., \"cameras:\").", + "step2": "Select a value from the suggestions or type your own.", + "step3": "Use multiple filters by adding them one after another with a space in between.", + "step4": "Date filters (before: and after:) use {{DateFormat}} format.", + "step5": "Time range filter uses {{exampleTime}} format.", + "step6": "Remove filters by clicking the 'x' next to them.", + "exampleLabel": "Example:" + } + }, + "header": { + "currentFilterType": "Filter Values", + "noFilters": "Filters", + "activeFilters": "Active Filters" + } + }, + "similaritySearch": { + "title": "Similarity Search", + "active": "Similarity search active", + "clear": "Clear similarity search" + }, + "placeholder": { + "search": "Search…" + } +} diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json new file mode 100644 index 0000000..6824824 --- /dev/null +++ b/web/public/locales/en/views/settings.json @@ -0,0 +1,1974 @@ +{ + "documentTitle": { + "default": "Settings - Frigate", + "authentication": "Authentication Settings - Frigate", + "cameraManagement": "Manage Cameras - Frigate", + "cameraReview": "Camera Review Settings - Frigate", + "enrichments": "Enrichments Settings - Frigate", + "masksAndZones": "Mask and Zone Editor - Frigate", + "motionTuner": "Motion Tuner - Frigate", + "object": "Debug - Frigate", + "general": "UI Settings - Frigate", + "globalConfig": "Global Configuration - Frigate", + "cameraConfig": "Camera Configuration - Frigate", + "frigatePlus": "Frigate+ Settings - Frigate", + "detectorsAndModel": "Detectors and model - Frigate", + "notifications": "Notification Settings - Frigate", + "maintenance": "Maintenance - Frigate", + "profiles": "Profiles - Frigate" + }, + "button": { + "overriddenGlobal": "Overridden (Global)", + "overriddenGlobalTooltip": "This camera overrides global configuration settings in this section", + "overriddenGlobalHeading_one": "This camera overrides {{count}} field from the global config:", + "overriddenGlobalHeading_other": "This camera overrides {{count}} fields from the global config:", + "overriddenGlobalNoDeltas": "This camera overrides the global config, but no field values differ.", + "overriddenBaseConfig": "Overridden (Base Config)", + "overriddenBaseConfigTooltip": "The {{profile}} profile overrides configuration settings in this section", + "overriddenBaseConfigHeading_one": "The {{profile}} profile overrides {{count}} field from the base config:", + "overriddenBaseConfigHeading_other": "The {{profile}} profile overrides {{count}} fields from the base config:", + "overriddenBaseConfigNoDeltas": "The {{profile}} profile overrides this section, but no field values differ from the base config.", + "overriddenInCameras": { + "label_one": "Overridden in {{count}} camera", + "label_other": "Overridden in {{count}} cameras", + "tooltip_one": "{{count}} camera overrides values in this section. Click to see details.", + "tooltip_other": "{{count}} cameras override values in this section. Click to see details.", + "heading_one": "This global section has fields that are overridden in {{count}} camera.", + "heading_other": "This global section has fields that are overridden in {{count}} cameras.", + "othersField_one": "{{count}} other", + "othersField_other": "{{count}} others", + "profilePrefix": "{{profile}} profile: {{fields}}" + } + }, + "menuDot": { + "overrideGlobal": "This section overrides the global configuration", + "overrideProfile": "This section is overridden by the {{profile}} profile", + "unsaved": "This section has unsaved changes" + }, + "menu": { + "general": "General", + "globalConfig": "Global configuration", + "system": "System", + "integrations": "Integrations", + "cameras": "Camera configuration", + "ui": "UI", + "uiSettings": "UI settings", + "profiles": "Profiles", + "globalDetect": "Object detection", + "globalRecording": "Recording", + "globalSnapshots": "Snapshots", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Motion detection", + "globalObjects": "Objects", + "globalReview": "Review", + "globalAudioEvents": "Audio detection", + "globalLivePlayback": "Live playback", + "globalTimestampStyle": "Timestamp style", + "systemDatabase": "Database", + "systemTls": "TLS", + "systemAuthentication": "Authentication", + "systemNetworking": "Networking", + "systemProxy": "Proxy", + "systemUi": "UI", + "systemLogging": "Logging", + "systemEnvironmentVariables": "Environment variables", + "systemTelemetry": "Telemetry", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "Detectors and model", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "go2rtc streams", + "integrationSemanticSearch": "Semantic search", + "integrationGenerativeAi": "Generative AI", + "integrationFaceRecognition": "Face recognition", + "integrationLpr": "License plate recognition", + "integrationObjectClassification": "Object classification", + "integrationAudioTranscription": "Audio transcription", + "cameraDetect": "Object detection", + "cameraFfmpeg": "Streams (FFmpeg)", + "cameraRecording": "Recording", + "cameraSnapshots": "Snapshots", + "cameraMotion": "Motion detection", + "cameraObjects": "Objects", + "cameraConfigReview": "Review", + "cameraAudioEvents": "Audio detection", + "cameraAudioTranscription": "Audio transcription", + "cameraNotifications": "Notifications", + "cameraLivePlayback": "Live playback", + "cameraBirdseye": "Birdseye", + "cameraFaceRecognition": "Face recognition", + "cameraLpr": "License plate recognition", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "Camera UI", + "cameraTimestampStyle": "Timestamp style", + "cameraMqtt": "Camera MQTT", + "cameraManagement": "Camera management", + "cameraReview": "Review", + "masksAndZones": "Masks / Zones", + "motionTuner": "Motion tuner", + "enrichments": "Enrichments", + "users": "Users", + "roles": "Roles", + "notifications": "Notifications", + "triggers": "Triggers", + "debug": "Debug", + "frigateplus": "Frigate+", + "maintenance": "Maintenance", + "mediaSync": "Media sync", + "regionGrid": "Region grid" + }, + "dialog": { + "unsavedChanges": { + "title": "You have unsaved changes.", + "desc": "Do you want to save your changes before continuing?" + } + }, + "saveAllPreview": { + "title": "Changes to be saved", + "triggerLabel": "Review pending changes", + "empty": "No pending changes.", + "scope": { + "label": "Scope", + "global": "Global", + "camera": "Camera: {{cameraName}}" + }, + "profile": { + "label": "Profile" + }, + "field": { + "label": "Field" + }, + "value": { + "label": "New value", + "reset": "Reset" + } + }, + "cameraSetting": { + "camera": "Camera", + "noCamera": "No Camera" + }, + "general": { + "title": "UI Settings", + "liveDashboard": { + "title": "Live Dashboard", + "automaticLiveView": { + "label": "Automatic Live View", + "desc": "Automatically switch to a camera's live view when activity is detected. Disabling this option causes static camera images on the Live dashboard to only update once per minute." + }, + "playAlertVideos": { + "label": "Play Alert Videos", + "desc": "By default, recent alerts on the Live dashboard play as small looping videos. Disable this option to only show a static image of recent alerts on this device/browser." + }, + "displayCameraNames": { + "label": "Always Show Camera Names", + "desc": "Always show the camera names in a chip in the multi-camera live view dashboard." + }, + "liveFallbackTimeout": { + "label": "Live Player Fallback Timeout", + "desc": "When a camera's high quality live stream is unavailable, fall back to low bandwidth mode after this many seconds. Default: 3." + } + }, + "storedLayouts": { + "title": "Stored Layouts", + "desc": "The layout of cameras in a camera group can be dragged/resized. The positions are stored in your browser's local storage.", + "clearAll": "Clear All Layouts" + }, + "cameraGroupStreaming": { + "title": "Camera Group Streaming Settings", + "desc": "Streaming settings for each camera group are stored in your browser's local storage.", + "clearAll": "Clear All Streaming Settings" + }, + "recordingsViewer": { + "title": "Recordings Viewer", + "defaultPlaybackRate": { + "label": "Default Playback Rate", + "desc": "Default playback rate for recordings playback." + } + }, + "calendar": { + "title": "Calendar", + "firstWeekday": { + "label": "First Weekday", + "desc": "The day that the weeks of the review calendar begin on.", + "sunday": "Sunday", + "monday": "Monday" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Cleared stored layout for {{cameraName}}", + "clearStreamingSettings": "Cleared streaming settings for all camera groups." + }, + "error": { + "clearStoredLayoutFailed": "Failed to clear stored layout: {{errorMessage}}", + "clearStreamingSettingsFailed": "Failed to clear streaming settings: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Enrichments Settings", + "unsavedChanges": "Unsaved Enrichments settings changes", + "birdClassification": { + "title": "Bird Classification", + "desc": "Bird classification identifies known birds using a quantized Tensorflow model. When a known bird is recognized, its common name will be added as a sub_label. This information is included in the UI, filters, as well as in notifications." + }, + "semanticSearch": { + "title": "Semantic Search", + "desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.", + "reindexNow": { + "label": "Reindex Now", + "desc": "Reindexing will regenerate embeddings for all tracked objects. This process runs in the background and may max out your CPU and take a fair amount of time depending on the number of tracked objects you have.", + "confirmTitle": "Confirm Reindexing", + "confirmDesc": "Are you sure you want to reindex all tracked object embeddings? This process will run in the background but it may max out your CPU and take a fair amount of time. You can watch the progress on the Explore page.", + "confirmButton": "Reindex", + "success": "Reindexing started successfully.", + "alreadyInProgress": "Reindexing is already in progress.", + "error": "Failed to start reindexing: {{errorMessage}}" + }, + "modelSize": { + "label": "Model Size", + "desc": "The size of the model used for semantic search embeddings.", + "small": { + "title": "small", + "desc": "Using small employs a quantized version of the model that uses less RAM and runs faster on CPU with a very negligible difference in embedding quality." + }, + "large": { + "title": "large", + "desc": "Using large employs the full Jina model and will automatically run on the GPU if applicable." + } + } + }, + "faceRecognition": { + "title": "Face Recognition", + "desc": "Face recognition allows people to be assigned names and when their face is recognized Frigate will assign the person's name as a sub label. This information is included in the UI, filters, as well as in notifications.", + "modelSize": { + "label": "Model Size", + "desc": "The size of the model used for face recognition.", + "small": { + "title": "small", + "desc": "Using small employs a FaceNet face embedding model that runs efficiently on most CPUs." + }, + "large": { + "title": "large", + "desc": "Using large employs an ArcFace face embedding model and will automatically run on the GPU if applicable." + } + } + }, + "licensePlateRecognition": { + "title": "License Plate Recognition", + "desc": "Frigate can recognize license plates on vehicles and automatically add the detected characters to the recognized_license_plate field or a known name as a sub_label to objects that are of type car. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street." + }, + "restart_required": "Restart required (Enrichments settings changed)", + "toast": { + "success": "Enrichments settings have been saved. Restart Frigate to apply your changes.", + "error": "Failed to save config changes: {{errorMessage}}" + } + }, + "cameraWizard": { + "title": "Add Camera", + "description": "Follow the steps below to add a new camera to your Frigate installation.", + "steps": { + "nameAndConnection": "Name & Connection", + "probeOrSnapshot": "Probe or Snapshot", + "streamConfiguration": "Stream Configuration", + "validationAndTesting": "Validation & Testing" + }, + "save": { + "success": "Successfully saved new camera {{cameraName}}.", + "failure": "Error saving {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolution", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Please provide a valid stream URL", + "testFailed": "Stream test failed: {{error}}" + }, + "step1": { + "description": "Enter your camera details and choose to probe the camera or manually select the brand.", + "cameraName": "Camera Name", + "cameraNamePlaceholder": "e.g., front_door or Back Yard Overview", + "host": "Host/IP Address", + "port": "Port", + "username": "Username", + "usernamePlaceholder": "Optional", + "password": "Password", + "passwordPlaceholder": "Optional", + "selectTransport": "Select transport protocol", + "cameraBrand": "Camera Brand", + "selectBrand": "Select camera brand for URL template", + "customUrl": "Custom Stream URL", + "brandInformation": "Brand information", + "brandUrlFormat": "For cameras with the RTSP URL format as: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "connectionSettings": "Connection Settings", + "detectionMethod": "Stream Detection Method", + "onvifPort": "ONVIF Port", + "probeMode": "Probe camera", + "manualMode": "Manual selection", + "detectionMethodDescription": "Probe the camera with ONVIF (if supported) to find camera stream URLs, or manually select the camera brand to use pre-defined URLs. To enter a custom RTSP URL, choose the manual method and select \"Other\".", + "onvifPortDescription": "For cameras that support ONVIF, this is usually 80 or 8080.", + "useDigestAuth": "Use digest authentication", + "useDigestAuthDescription": "Use HTTP digest authentication for ONVIF. Some cameras may require a dedicated ONVIF username/password instead of the standard admin user.", + "errors": { + "brandOrCustomUrlRequired": "Either select a camera brand with host/IP or choose 'Other' with a custom URL", + "nameRequired": "Camera name is required", + "nameLength": "Camera name must be 64 characters or less", + "invalidCharacters": "Camera name contains invalid characters", + "nameExists": "Camera name already exists", + "customUrlRtspRequired": "Custom URLs must begin with \"rtsp://\" or \"rtsps://\". Manual configuration is required for non-RTSP camera streams." + } + }, + "step2": { + "description": "Probe the camera for available streams or configure manual settings based on your selected detection method.", + "testSuccess": "Connection test successful!", + "testFailed": "Connection test failed. Please check your input and try again.", + "testFailedTitle": "Test Failed", + "streamDetails": "Stream Details", + "probing": "Probing camera...", + "retry": "Retry", + "testing": { + "probingMetadata": "Probing camera metadata...", + "fetchingSnapshot": "Fetching camera snapshot..." + }, + "probeFailed": "Failed to probe camera: {{error}}", + "probingDevice": "Probing device...", + "probeSuccessful": "Probe successful", + "probeError": "Probe Error", + "probeNoSuccess": "Probe unsuccessful", + "deviceInfo": "Device Information", + "manufacturer": "Manufacturer", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profiles", + "ptzSupport": "PTZ Support", + "autotrackingSupport": "Autotracking Support", + "presets": "Presets", + "rtspCandidates": "RTSP Candidates", + "rtspCandidatesDescription": "The following RTSP URLs were found from the camera probe. Test the connection to view stream metadata.", + "noRtspCandidates": "No RTSP URLs were found from the camera. Your credentials may be incorrect, or the camera may not support ONVIF or the method used to retrieve RTSP URLs. Go back and enter the RTSP URL manually.", + "candidateStreamTitle": "Candidate {{number}}", + "useCandidate": "Use", + "uriCopy": "Copy", + "uriCopied": "URI copied to clipboard", + "testConnection": "Test Connection", + "toggleUriView": "Click to toggle full URI view", + "connected": "Connected", + "notConnected": "Not Connected", + "errors": { + "hostRequired": "Host/IP address is required" + } + }, + "step3": { + "description": "Configure stream roles and add additional streams for your camera.", + "streamsTitle": "Camera Streams", + "addStream": "Add Stream", + "addAnotherStream": "Add Another Stream", + "streamTitle": "Stream {{number}}", + "streamUrl": "Stream URL", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "selectStream": "Select a stream", + "searchCandidates": "Search candidates...", + "noStreamFound": "No stream found", + "url": "URL", + "resolution": "Resolution", + "selectResolution": "Select resolution", + "quality": "Quality", + "selectQuality": "Select quality", + "roles": "Roles", + "roleLabels": { + "detect": "Object Detection", + "record": "Recording", + "audio": "Audio" + }, + "testStream": "Test Connection", + "testSuccess": "Stream test successful!", + "testFailed": "Stream test failed", + "testFailedTitle": "Test Failed", + "connected": "Connected", + "notConnected": "Not Connected", + "featuresTitle": "Features", + "go2rtc": "Reduce connections to camera", + "detectRoleWarning": "At least one stream must have the \"detect\" role to proceed.", + "rolesPopover": { + "title": "Stream Roles", + "detect": "Main feed for object detection.", + "record": "Saves segments of the video feed based on configuration settings.", + "audio": "Feed for audio based detection." + }, + "featuresPopover": { + "title": "Stream Features", + "description": "Use go2rtc restreaming to reduce connections to your camera." + } + }, + "step4": { + "description": "Final validation and analysis before saving your new camera. Connect each stream before saving.", + "validationTitle": "Stream Validation", + "connectAllStreams": "Connect All Streams", + "reconnectionSuccess": "Reconnection successful.", + "reconnectionPartial": "Some streams failed to reconnect.", + "streamUnavailable": "Stream preview unavailable", + "reload": "Reload", + "connecting": "Connecting...", + "streamTitle": "Stream {{number}}", + "valid": "Valid", + "failed": "Failed", + "notTested": "Not tested", + "connectStream": "Connect", + "connectingStream": "Connecting", + "disconnectStream": "Disconnect", + "estimatedBandwidth": "Estimated Bandwidth", + "roles": "Roles", + "ffmpegModule": "Use stream compatibility mode", + "ffmpegModuleDescription": "If the stream does not load after several attempts, try enabling this. When enabled, Frigate will use the ffmpeg module with go2rtc. This may provide better compatibility with some camera streams.", + "none": "None", + "error": "Error", + "streamValidated": "Stream {{number}} validated successfully", + "streamValidationFailed": "Stream {{number}} validation failed", + "saveAndApply": "Save New Camera", + "saveError": "Invalid configuration. Please check your settings.", + "issues": { + "title": "Stream Validation", + "videoCodecGood": "Video codec is {{codec}}.", + "audioCodecGood": "Audio codec is {{codec}}.", + "resolutionHigh": "A resolution of {{resolution}} may cause increased resource usage.", + "resolutionLow": "A resolution of {{resolution}} may be too low for reliable detection of small objects.", + "resolutionUnknown": "The resolution of this stream could not be probed. You should manually set the detect resolution in Settings or your config.", + "noAudioWarning": "No audio detected for this stream, recordings will not have audio.", + "audioCodecRecordError": "The AAC audio codec is required to support audio in recordings.", + "audioCodecRequired": "An audio stream is required to support audio detection.", + "restreamingWarning": "Reducing connections to the camera for the record stream may increase CPU usage slightly.", + "brands": { + "reolink-rtsp": "Reolink RTSP is not recommended. Enable HTTP in the camera's firmware settings and restart the wizard.", + "reolink-http": "Reolink HTTP streams should use FFmpeg for better compatibility. Enable 'Use stream compatibility mode' for this stream." + }, + "dahua": { + "substreamWarning": "Substream 1 is locked to a low resolution. Many Dahua / Amcrest / EmpireTech cameras support additional substreams that need to be enabled in the camera's settings. It is recommended to check and utilize those streams if available." + }, + "hikvision": { + "substreamWarning": "Substream 1 is locked to a low resolution. Many Hikvision cameras support additional substreams that need to be enabled in the camera's settings. It is recommended to check and utilize those streams if available." + } + } + } + }, + "cameraManagement": { + "title": "Manage Cameras", + "description": "Add, edit, and delete cameras, control the state of each camera, and configure per-profile and camera type overrides. To configure streams, detection, motion, and other camera-specific settings, choose the specific section under Camera Configuration.", + "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 {{cameraName}} 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:", + "selectCamera": "Select a Camera", + "backToSettings": "Back to Camera Settings", + "streams": { + "title": "Camera State and Details", + "label": "Camera state", + "description": "Set the operating state for each camera.

    On: streams are processed normally.
    Off: temporarily pauses processing. Does not persist across Frigate restarts.
    Disabled: stops processing and saves the change to your configuration. A restart is required to re-enable a disabled camera.

    Note: Disabling does not affect go2rtc restreams.

    Drag the handle to reorder active cameras as they appear throughout the UI, including the Live dashboard and camera selection dropdowns.", + "disabledSubheading": "Disabled in configuration", + "status": { + "on": "On", + "off": "Off", + "disabled": "Disabled" + }, + "enableSuccess": "Enabled {{cameraName}}. Restart Frigate to apply.", + "disableSuccess": "Disabled {{cameraName}} and saved to configuration.", + "reorderHandle": "Drag to reorder", + "saving": "Saving…", + "saved": "Saved", + "details": { + "edit": "Edit camera details", + "title": "Edit Camera Details", + "description": "Update the display name, external URL, and visibility used for this camera throughout the Frigate UI.", + "friendlyNameLabel": "Display Name", + "friendlyNameHelp": "Friendly name shown for this camera throughout the Frigate UI. Leave blank to use the camera ID.", + "webuiUrlLabel": "Camera Web UI URL", + "webuiUrlHelp": "URL to visit the camera's web UI directly from the Debug view. Leave blank to disable the link.", + "webuiUrlInvalid": "Must be a valid URL (e.g., https://example.com).", + "dashboardLabel": "Show on Live dashboard", + "dashboardHelp": "Show this camera on the Live dashboard.", + "reviewLabel": "Show in Review", + "reviewHelp": "Show this camera in Review, including the camera filter, motion review, and the history view." + } + }, + "cameraConfig": { + "add": "Add Camera", + "edit": "Edit Camera", + "description": "Configure camera settings including stream inputs and roles.", + "name": "Camera Name", + "nameRequired": "Camera name is required", + "nameLength": "Camera name must be less than 64 characters.", + "namePlaceholder": "e.g., front_door or Back Yard Overview", + "enabled": "Enabled", + "ffmpeg": { + "inputs": "Input Streams", + "path": "Stream Path", + "pathRequired": "Stream path is required", + "pathPlaceholder": "rtsp://...", + "roles": "Roles", + "rolesRequired": "At least one role is required", + "rolesUnique": "Each role (audio, detect, record) can only be assigned to one stream", + "addInput": "Add Input Stream", + "removeInput": "Remove Input Stream", + "inputsRequired": "At least one input stream is required" + }, + "go2rtcStreams": "go2rtc Streams", + "streamUrls": "Stream URLs", + "addUrl": "Add URL", + "addGo2rtcStream": "Add go2rtc Stream", + "toast": { + "success": "Camera {{cameraName}} saved successfully" + } + }, + "profiles": { + "title": "Profile Camera Overrides", + "selectLabel": "Select profile", + "description": "Configure which cameras are turned on or off when a profile is activated. Cameras set to \"Inherit\" keep their default state.", + "inherit": "Inherit", + "on": "On", + "off": "Off" + }, + "cameraType": { + "title": "Camera Type", + "label": "Camera type", + "description": "Set the type for each camera. Dedicated LPR cameras are single-purpose cameras with powerful optical zoom to capture license plates on distant vehicles. Most cameras should use the normal camera type unless the camera is specifically for LPR and has a tightly focused view on license plates.", + "normal": "Normal", + "dedicatedLpr": "Dedicated LPR", + "saveSuccess": "Updated camera type for {{cameraName}}. Restart Frigate to apply the changes." + }, + "clone": { + "sectionTitle": "Clone settings", + "sectionDescription": "Copy configuration from one camera to another camera or a new one.", + "button": "Clone settings", + "title": "Clone camera settings", + "description": "Copy a camera's configuration to one or more other cameras or a new camera. Identity (name, friendly name, web UI URL, display order) is never copied.", + "source": { + "label": "Source camera", + "placeholder": "Select a source camera", + "required": "Select a source camera" + }, + "target": { + "legend": "Target", + "newRadio": "New camera", + "newNameLabel": "Camera name", + "newNamePlaceholder": "e.g., back_door or Back Door", + "newNameRequired": "Camera name is required", + "newNameInvalid": "Invalid camera name", + "newNameCollision": "A camera with this name already exists", + "newStreamsForced": "Streams are always copied for a new camera.", + "existingCamerasRadio": "Existing cameras", + "allCameras": "All cameras", + "existingPlaceholder": "Select at least one camera", + "existingDisabled": "No other cameras to copy to" + }, + "categories": { + "legend": "Settings to clone", + "description": "Choose which settings to copy from the source camera.", + "selectAll": "Select all", + "selectNone": "Select none", + "resetDefaults": "Reset to defaults", + "general": "General", + "spatial": "Spatial settings", + "streams": "Streams", + "spatialWarningTitle": "Resolution mismatch", + "spatialWarning": "Source camera {{srcCamera}} detect resolution ({{srcWidth}}×{{srcHeight}}) differs from: {{cameras}}. Polygons may not align on those cameras. These defaults are off; enable to copy as-is.", + "restartHint": "Restart required", + "items": { + "record": "Recording", + "snapshots": "Snapshots", + "review": "Review", + "motion": "Motion detection", + "objects": "Objects", + "audio": "Audio detection", + "audio_transcription": "Audio transcription", + "notifications": "Notifications", + "birdseye": "Birdseye", + "mqtt": "MQTT", + "timestamp_style": "Timestamp style", + "onvif": "ONVIF", + "lpr": "License plate recognition", + "face_recognition": "Face recognition", + "semantic_search": "Semantic search", + "genai": "Generative AI", + "type": "Camera type (normal / dedicated LPR)", + "profiles": "Profiles", + "detect": "Detect dimensions", + "zones": "Zones", + "motion_mask": "Motion masks", + "object_masks": "Object masks", + "ffmpeg_live": "Stream URLs and roles" + } + }, + "footer": { + "changeCount_zero": "No changes selected", + "changeCount_one": "{{count}} change will be applied", + "changeCount_other": "{{count}} changes will be applied", + "restartNeeded": "Restart will be required for some changes.", + "liveOnly": "All changes will apply live without a restart.", + "submit": "Clone", + "submitting": "Cloning…" + }, + "toast": { + "success": "Settings copied to {{cameraName}}", + "successWithRestart": "Settings copied to {{cameraName}}. Restart Frigate to apply all changes.", + "successMulti_one": "Settings copied to {{count}} camera", + "successMulti_other": "Settings copied to {{count}} cameras", + "successMultiWithRestart_one": "Settings copied to {{count}} camera. Restart Frigate to apply all changes.", + "successMultiWithRestart_other": "Settings copied to {{count}} cameras. Restart Frigate to apply all changes.", + "partialFailure": "{{successCount}} sections applied; '{{failedSection}}' failed: {{errorMessage}}", + "partialFailureMulti": "Copied to {{successCount}} camera(s); failed for {{failed}}: {{errorMessage}}", + "newCameraPartialFailure": "Camera {{cameraName}} was created but some settings failed to copy: {{errorMessage}}", + "sourceMissing": "Source camera no longer exists", + "submitError": "Failed to clone camera: {{errorMessage}}" + } + } + }, + "cameraReview": { + "title": "Camera Review Settings", + "object_descriptions": { + "title": "Generative AI Object Descriptions", + "desc": "Temporarily enable/disable Generative AI object descriptions for this camera until Frigate restarts. When disabled, AI generated descriptions will not be requested for tracked objects on this camera." + }, + "review_descriptions": { + "title": "Generative AI Review Descriptions", + "desc": "Temporarily enable/disable Generative AI review descriptions for this camera until Frigate restarts. When disabled, AI generated descriptions will not be requested for review items on this camera." + }, + "review": { + "title": "Review", + "desc": "Temporarily enable/disable alerts and detections for this camera until Frigate restarts. When disabled, no new review items will be generated. ", + "alerts": "Alerts ", + "detections": "Detections " + }, + "reviewClassification": { + "title": "Review Classification", + "desc": "Frigate categorizes review items as Alerts and Detections. By default, all person and car objects are considered Alerts. You can refine categorization of your review items by configuring required zones for them.", + "noDefinedZones": "No zones are defined for this camera.", + "objectAlertsTips": "All {{alertsLabels}} objects on {{cameraName}} will be shown as Alerts.", + "zoneObjectAlertsTips": "All {{alertsLabels}} objects detected in {{zone}} on {{cameraName}} will be shown as Alerts.", + "objectDetectionsTips": "All {{detectionsLabels}} objects not categorized on {{cameraName}} will be shown as Detections regardless of which zone they are in.", + "zoneObjectDetectionsTips": { + "text": "All {{detectionsLabels}} objects not categorized in {{zone}} on {{cameraName}} will be shown as Detections.", + "notSelectDetections": "All {{detectionsLabels}} objects detected in {{zone}} on {{cameraName}} not categorized as Alerts will be shown as Detections regardless of which zone they are in.", + "regardlessOfZoneObjectDetectionsTips": "All {{detectionsLabels}} objects not categorized on {{cameraName}} will be shown as Detections regardless of which zone they are in." + }, + "unsavedChanges": "Unsaved Review Classification settings for {{camera}}", + "selectAlertsZones": "Select zones for Alerts", + "selectDetectionsZones": "Select zones for Detections", + "limitDetections": "Limit detections to specific zones", + "toast": { + "success": "Review Classification configuration has been saved. Restart Frigate to apply changes." + } + } + }, + "masksAndZones": { + "filter": { + "all": "All Masks and Zones" + }, + "restart_required": "Restart required (masks/zones changed)", + "disabledInConfig": "Item is disabled in the config file", + "addDisabledProfile": "Add to the base config first, then override in the profile", + "profileBase": "(base)", + "profileOverride": "(override)", + "toast": { + "success": { + "copyCoordinates": "Copied coordinates for {{polyName}} to clipboard." + }, + "error": { + "copyCoordinatesFailed": "Could not copy coordinates to clipboard." + } + }, + "motionMaskLabel": "Motion Mask {{number}}", + "objectMaskLabel": "Object Mask {{number}}", + "form": { + "id": { + "error": { + "mustNotBeEmpty": "ID must not be empty.", + "alreadyExists": "A mask with this ID already exists for this camera." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Name must not be empty." + } + }, + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Zone name must be at least 2 characters.", + "mustNotBeSameWithCamera": "Zone name must not be the same as camera name.", + "alreadyExists": "A zone with this name already exists for this camera.", + "mustNotContainPeriod": "Zone name must not contain periods.", + "hasIllegalCharacter": "Zone name contains illegal characters.", + "mustHaveAtLeastOneLetter": "Zone name must have at least one letter." + } + }, + "distance": { + "error": { + "text": "Distance must be greater than or equal to 0.1.", + "mustBeFilled": "All distance fields must be filled to use speed estimation." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Inertia must be above 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Loitering time must be greater than or equal to 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Speed threshold must greater than or equal to 0.1." + } + }, + "polygonDrawing": { + "type": { + "zone": "zone", + "motion_mask": "motion mask", + "object_mask": "object mask" + }, + "removeLastPoint": "Remove last point", + "reset": { + "label": "Clear all points" + }, + "snapPoints": { + "true": "Snap points", + "false": "Don't Snap points" + }, + "delete": { + "title": "Confirm Delete", + "desc": "Are you sure you want to delete the {{type}} {{name}}?", + "success": "{{name}} has been deleted." + }, + "revertOverride": { + "title": "Revert to Base Config", + "desc": "This will remove the profile override for the {{type}} {{name}} and revert to the base configuration." + }, + "error": { + "mustBeFinished": "Polygon drawing must be finished before saving." + } + } + }, + "zones": { + "label": "Zones", + "documentTitle": "Edit Zone - Frigate", + "desc": { + "title": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.", + "documentation": "Documentation" + }, + "add": "Add Zone", + "edit": "Edit Zone", + "point_one": "{{count}} point", + "point_other": "{{count}} points", + "clickDrawPolygon": "Click to draw a polygon on the image.", + "name": { + "title": "Name", + "inputPlaceHolder": "Enter a name…", + "tips": "Name must be at least 2 characters, must have at least one letter, and must not be the name of a camera or another zone on this camera." + }, + "enabled": { + "title": "Enabled", + "description": "Whether this zone is active and enabled in the config file. If disabled, it cannot be enabled by MQTT. Disabled zones are ignored at runtime." + }, + "inertia": { + "title": "Inertia", + "desc": "Specifies how many frames that an object must be in a zone before they are considered in the zone. Default: 3" + }, + "loiteringTime": { + "title": "Loitering Time", + "desc": "Sets a minimum amount of time in seconds that the object must be in the zone for it to activate. Default: 0" + }, + "objects": { + "title": "Objects", + "desc": "List of objects that apply to this zone." + }, + "allObjects": "All Objects", + "speedEstimation": { + "title": "Speed Estimation", + "desc": "Enable speed estimation for objects in this zone. The zone must have exactly 4 points.", + "lineADistance": "Line A distance ({{unit}})", + "lineBDistance": "Line B distance ({{unit}})", + "lineCDistance": "Line C distance ({{unit}})", + "lineDDistance": "Line D distance ({{unit}})" + }, + "speedThreshold": { + "title": "Speed Threshold ({{unit}})", + "desc": "Specifies a minimum speed for objects to be considered in this zone.", + "toast": { + "error": { + "pointLengthError": "Speed estimation has been disabled for this zone. Zones with speed estimation must have exactly 4 points.", + "loiteringTimeError": "Zones with loitering times greater than 0 should not be used with speed estimation." + } + } + }, + "toast": { + "success": "Zone ({{zoneName}}) has been saved." + } + }, + "motionMasks": { + "label": "Motion Mask", + "documentTitle": "Edit Motion Mask - Frigate", + "desc": { + "title": "Motion masks are used to prevent unwanted types of motion from triggering detection. Over masking will make it more difficult for objects to be tracked.", + "documentation": "Documentation" + }, + "add": "New Motion Mask", + "edit": "Edit Motion Mask", + "defaultName": "Motion Mask {{number}}", + "context": { + "title": "Motion masks are used to prevent unwanted types of motion from triggering detection (example: tree branches, camera timestamps). Motion masks should be used very sparingly, over-masking will make it more difficult for objects to be tracked." + }, + "point_one": "{{count}} point", + "point_other": "{{count}} points", + "clickDrawPolygon": "Click to draw a polygon on the image.", + "name": { + "title": "Name", + "description": "An optional friendly name for this motion mask.", + "placeholder": "Enter a name..." + }, + "polygonAreaTooLarge": { + "title": "The motion mask is covering {{polygonArea}}% of the camera frame. Large motion masks are not recommended.", + "tips": "Motion masks do not prevent objects from being detected. You should use a required zone instead." + }, + "toast": { + "success": { + "title": "{{polygonName}} has been saved.", + "noName": "Motion Mask has been saved." + } + } + }, + "objectMasks": { + "label": "Object Masks", + "documentTitle": "Edit Object Mask - Frigate", + "desc": { + "title": "Object filter masks are used to filter out false positives for a given object type based on location.", + "documentation": "Documentation" + }, + "add": "Add Object Mask", + "edit": "Edit Object Mask", + "context": "Object filter masks are used to filter out false positives for a given object type based on location.", + "point_one": "{{count}} point", + "point_other": "{{count}} points", + "clickDrawPolygon": "Click to draw a polygon on the image.", + "name": { + "title": "Name", + "description": "An optional friendly name for this object mask.", + "placeholder": "Enter a name..." + }, + "objects": { + "title": "Objects", + "desc": "The object type that applies to this object mask.", + "allObjectTypes": "All object types" + }, + "toast": { + "success": { + "title": "{{polygonName}} has been saved.", + "noName": "Object Mask has been saved." + } + } + }, + "masks": { + "enabled": { + "title": "Enabled", + "description": "Whether this mask is enabled in the config file. If disabled, it cannot be enabled by MQTT. Disabled masks are ignored at runtime." + } + } + }, + "motionDetectionTuner": { + "title": "Motion Detection Tuner", + "unsavedChanges": "Unsaved Motion Tuner changes ({{camera}})", + "desc": { + "title": "Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection.", + "documentation": "Read the Motion Tuning Guide" + }, + "Threshold": { + "title": "Threshold", + "desc": "The threshold value dictates how much of a change in a pixel's luminance is required to be considered motion. Default: 30" + }, + "contourArea": { + "title": "Contour Area", + "desc": "The contour area value is used to decide which groups of changed pixels qualify as motion. Default: 10" + }, + "improveContrast": { + "title": "Improve Contrast", + "desc": "Improve contrast for darker scenes. Default: ON" + }, + "toast": { + "success": "Motion settings have been saved." + } + }, + "debug": { + "title": "Debug", + "detectorDesc": "Frigate uses your detectors ({{detectors}}) to detect objects in your camera's video stream.", + "desc": "Debugging view shows a real-time view of tracked objects and their statistics. The object list shows a time-delayed summary of detected objects.", + "openCameraWebUI": "Open {{camera}}'s Web UI", + "debugging": "Debugging", + "objectList": "Object List", + "noObjects": "No objects", + "audio": { + "title": "Audio", + "noAudioDetections": "No audio detections", + "score": "score", + "currentRMS": "Current RMS", + "currentdbFS": "Current dbFS" + }, + "boundingBoxes": { + "title": "Bounding boxes", + "desc": "Show bounding boxes around tracked objects", + "colors": { + "label": "Object Bounding Box Colors", + "info": "
  • At startup, different colors will be assigned to each object label
  • A dark blue thin line indicates that object is not detected at this current point in time
  • A gray thin line indicates that object is detected as being stationary
  • A thick line indicates that object is the subject of autotracking (when enabled)
  • " + } + }, + "timestamp": { + "title": "Timestamp", + "desc": "Overlay a timestamp on the image" + }, + "zones": { + "title": "Zones", + "desc": "Show an outline of any defined zones" + }, + "mask": { + "title": "Motion masks", + "desc": "Show motion mask polygons" + }, + "motion": { + "title": "Motion boxes", + "desc": "Show boxes around areas where motion is detected", + "tips": "

    Motion Boxes


    Red boxes will be overlaid on areas of the frame where motion is currently being detected

    " + }, + "regions": { + "title": "Regions", + "desc": "Show a box of the region of interest sent to the object detector", + "tips": "

    Region Boxes


    Bright green boxes will be overlaid on areas of interest in the frame that are being sent to the object detector.

    " + }, + "paths": { + "title": "Paths", + "desc": "Show significant points of the tracked object's path", + "tips": "

    Paths


    Lines and circles will indicate significant points the tracked object has moved during its lifecycle.

    " + }, + "objectShapeFilterDrawing": { + "title": "Object Shape Filter Drawing", + "desc": "Draw a rectangle on the image to view area and ratio details", + "tips": "Enable this option to draw a rectangle on the camera image to show its area and ratio. These values can then be used to set object shape filter parameters in your config.", + "score": "Score", + "ratio": "Ratio", + "area": "Area" + } + }, + "timestampPosition": { + "tl": "Top left", + "tr": "Top right", + "bl": "Bottom left", + "br": "Bottom right" + }, + "users": { + "title": "Users", + "management": { + "title": "User Management", + "desc": "Manage this Frigate instance's user accounts." + }, + "addUser": "Add User", + "updatePassword": "Reset Password", + "toast": { + "success": { + "createUser": "User {{user}} created successfully", + "deleteUser": "User {{user}} deleted successfully", + "updatePassword": "Password updated successfully.", + "roleUpdated": "Role updated for {{user}}" + }, + "error": { + "setPasswordFailed": "Failed to save password: {{errorMessage}}", + "createUserFailed": "Failed to create user: {{errorMessage}}", + "deleteUserFailed": "Failed to delete user: {{errorMessage}}", + "roleUpdateFailed": "Failed to update role: {{errorMessage}}" + } + }, + "table": { + "username": "Username", + "actions": "Actions", + "role": "Role", + "noUsers": "No users found.", + "changeRole": "Change user role", + "password": "Reset Password", + "deleteUser": "Delete user" + }, + "dialog": { + "form": { + "user": { + "title": "Username", + "desc": "Only letters, numbers, periods and underscores allowed.", + "placeholder": "Enter username" + }, + "password": { + "title": "Password", + "placeholder": "Enter password", + "show": "Show password", + "hide": "Hide password", + "confirm": { + "title": "Confirm Password", + "placeholder": "Confirm Password" + }, + "strength": { + "title": "Password strength: ", + "weak": "Weak", + "medium": "Medium", + "strong": "Strong", + "veryStrong": "Very Strong" + }, + "requirements": { + "title": "Password requirements:", + "length": "At least 12 characters" + }, + "match": "Passwords match", + "notMatch": "Passwords don't match" + }, + "newPassword": { + "title": "New Password", + "placeholder": "Enter new password", + "confirm": { + "placeholder": "Re-enter new password" + } + }, + "currentPassword": { + "title": "Current Password", + "placeholder": "Enter your current password" + }, + "usernameIsRequired": "Username is required", + "passwordIsRequired": "Password is required" + }, + "createUser": { + "title": "Create New User", + "desc": "Add a new user account and specify a role for access to areas of the Frigate UI.", + "usernameOnlyInclude": "Username may only include letters, numbers, . or _", + "confirmPassword": "Please confirm your password" + }, + "deleteUser": { + "title": "Delete User", + "desc": "This action cannot be undone. This will permanently delete the user account and remove all associated data.", + "warn": "Are you sure you want to delete {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "Password cannot be empty", + "doNotMatch": "Passwords do not match", + "currentPasswordRequired": "Current password is required", + "incorrectCurrentPassword": "Current password is incorrect", + "passwordVerificationFailed": "Failed to verify password", + "updatePassword": "Update Password for {{username}}", + "setPassword": "Set Password", + "desc": "Create a strong password to secure this account.", + "multiDeviceWarning": "Any other devices where you are logged in will be required to re-login within {{refresh_time}}.", + "multiDeviceAdmin": "You can also force all users to re-authenticate immediately by rotating your JWT secret." + }, + "changeRole": { + "title": "Change User Role", + "select": "Select a role", + "desc": "Update permissions for {{username}}", + "roleInfo": { + "intro": "Select the appropriate role for this user:", + "admin": "Admin", + "adminDesc": "Full access to all features.", + "viewer": "Viewer", + "viewerDesc": "Limited to Live dashboards, Review, Explore, and Exports only.", + "customDesc": "Custom role with specific camera access." + } + } + } + }, + "roles": { + "management": { + "title": "Viewer Role Management", + "desc": "Manage custom viewer roles and their camera access permissions for this Frigate instance." + }, + "addRole": "Add Role", + "table": { + "role": "Role", + "cameras": "Cameras", + "actions": "Actions", + "noRoles": "No custom roles found.", + "editCameras": "Edit Cameras", + "deleteRole": "Delete Role" + }, + "toast": { + "success": { + "createRole": "Role {{role}} created successfully", + "updateCameras": "Cameras updated for role {{role}}", + "deleteRole": "Role {{role}} deleted successfully", + "userRolesUpdated_one": "{{count}} user assigned to this role has been updated to 'viewer', which has access to all cameras.", + "userRolesUpdated_other": "{{count}} users assigned to this role have been updated to 'viewer', which has access to all cameras." + }, + "error": { + "createRoleFailed": "Failed to create role: {{errorMessage}}", + "updateCamerasFailed": "Failed to update cameras: {{errorMessage}}", + "deleteRoleFailed": "Failed to delete role: {{errorMessage}}", + "userUpdateFailed": "Failed to update user roles: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Create New Role", + "desc": "Add a new role and specify camera access permissions." + }, + "editCameras": { + "title": "Edit Role Cameras", + "desc": "Update camera access for the role {{role}}." + }, + "deleteRole": { + "title": "Delete Role", + "desc": "This action cannot be undone. This will permanently delete the role and assign any users with this role to the 'viewer' role, which will give viewer access to all cameras.", + "warn": "Are you sure you want to delete {{role}}?", + "deleting": "Deleting..." + }, + "form": { + "role": { + "title": "Role Name", + "placeholder": "Enter role name", + "desc": "Only letters, numbers, periods and underscores allowed.", + "roleIsRequired": "Role name is required", + "roleOnlyInclude": "Role name may only include letters, numbers, . or _", + "roleExists": "A role with this name already exists." + }, + "cameras": { + "title": "Cameras", + "desc": "Select cameras this role has access to. At least one camera is required.", + "required": "At least one camera must be selected." + } + } + } + }, + "notification": { + "title": "Notifications", + "notificationSettings": { + "title": "Notification Settings", + "desc": "Frigate can natively send push notifications to your device when it is running in the browser or installed as a PWA." + }, + "notificationUnavailable": { + "title": "Notifications Unavailable", + "desc": "Web push notifications require a secure context (https://…). This is a browser limitation. Access Frigate securely to use notifications.", + "descPwa": "On iOS, web push notifications are only available when Frigate is installed to your Home Screen. Open the Share menu, choose Add to Home Screen, then open Frigate from the new icon to register this device for notifications." + }, + "globalSettings": { + "title": "Global Settings", + "desc": "Temporarily suspend notifications for specific cameras on all registered devices." + }, + "email": { + "title": "Email", + "placeholder": "e.g. example@email.com", + "desc": "A valid email is required and will be used to notify you if there are any issues with the push service." + }, + "cameras": { + "title": "Cameras", + "noCameras": "No cameras available", + "desc": "Select which cameras to enable notifications for." + }, + "deviceSpecific": "Device Specific Settings", + "registerDevice": "Register This Device", + "unregisterDevice": "Unregister This Device", + "sendTestNotification": "Send a test notification", + "unsavedRegistrations": "Unsaved Notification registrations", + "unsavedChanges": "Unsaved Notification changes", + "active": "Notifications Active", + "suspended": "Notifications suspended {{time}}", + "suspendTime": { + "suspend": "Suspend", + "5minutes": "Suspend for 5 minutes", + "10minutes": "Suspend for 10 minutes", + "30minutes": "Suspend for 30 minutes", + "1hour": "Suspend for 1 hour", + "12hours": "Suspend for 12 hours", + "24hours": "Suspend for 24 hours", + "untilRestart": "Suspend until restart" + }, + "cancelSuspension": "Cancel Suspension", + "toast": { + "success": { + "registered": "Successfully registered for notifications. Restarting Frigate is required before any notifications (including a test notification) can be sent.", + "settingSaved": "Notification settings have been saved." + }, + "error": { + "registerFailed": "Failed to save notification registration." + } + } + }, + "frigatePlus": { + "title": "Frigate+ Settings", + "description": "Frigate+ is a subscription service that provides access to additional features and capabilities for your Frigate instance, including the ability to use custom object detection models trained on your own data. You can manage your Frigate+ model settings here.", + "cardTitles": { + "api": "API", + "currentModel": "Current Model", + "otherModels": "Other Models", + "configuration": "Configuration" + }, + "apiKey": { + "title": "Frigate+ API Key", + "validated": "Frigate+ API key is detected and validated", + "notValidated": "Frigate+ API key is not detected or not validated", + "desc": "The Frigate+ API key enables integration with the Frigate+ service.", + "plusLink": "Read more about Frigate+" + }, + "snapshotConfig": { + "title": "Snapshot Configuration", + "desc": "Submitting to Frigate+ requires snapshots to be enabled in your config.", + "cleanCopyWarning": "Some cameras have snapshots disabled", + "table": { + "camera": "Camera", + "snapshots": "Snapshots" + } + }, + "modelInfo": { + "title": "Model Information", + "modelType": "Model Type", + "trainDate": "Train Date", + "baseModel": "Base Model", + "plusModelType": { + "baseModel": "Base Model", + "userModel": "Fine-Tuned" + }, + "supportedDetectors": "Supported Detectors", + "cameras": "Cameras", + "loading": "Loading model information…", + "error": "Failed to load model information", + "noModelLoaded": "No Frigate+ model is currently loaded.", + "availableModels": "Available Frigate+ models", + "loadingAvailableModels": "Loading available models…", + "selectModel": "Select a model", + "noModelsAvailable": "No models available", + "filter": { + "ariaLabel": "Filter models by type", + "baseModels": "Base Models", + "fineTunedModels": "Fine-tuned Models" + }, + "modelSelect": "Your available models on Frigate+ can be selected here. Note that only models compatible with your current detector configuration can be selected." + }, + "changeInDetectorsAndModel": "Change model", + "unsavedChanges": "Unsaved Frigate+ settings changes", + "restart_required": "Restart required (Frigate+ model changed)", + "toast": { + "success": "Frigate+ settings have been saved. Restart Frigate to apply changes.", + "error": "Failed to save config changes: {{errorMessage}}" + } + }, + "detectorsAndModel": { + "title": "Detectors and model", + "description": "Configure the detector backend that runs object detection and the model it uses. Changes are saved together so the detector and model stay in sync.", + "cardTitles": { + "detector": "Detector Hardware", + "model": "Detection Model" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Custom Model" + }, + "mismatch": { + "warning": "The current Frigate+ model \"{{model}}\" requires the {{required}} detector. Pick a compatible model below or switch to Custom Model before saving." + }, + "plusModel": { + "requiresDetector": "Requires: {{detector}}", + "noModelSelected": "Select a Frigate+ model" + }, + "toast": { + "saveSuccess": "Detectors and model settings saved. Restart Frigate to apply changes.", + "saveError": "Failed to save detector and model settings" + }, + "unsavedChanges": "Unsaved detector and model changes", + "restartRequired": "Restart required (detector or model changed)" + }, + "triggers": { + "documentTitle": "Triggers", + "semanticSearch": { + "title": "Semantic Search is disabled", + "desc": "Semantic Search must be enabled to use Triggers." + }, + "management": { + "title": "Triggers", + "desc": "Manage triggers for {{camera}}. Use the thumbnail type to trigger on similar thumbnails to your selected tracked object, and the description type to trigger on similar descriptions to text you specify." + }, + "addTrigger": "Add Trigger", + "table": { + "name": "Name", + "type": "Type", + "content": "Content", + "threshold": "Threshold", + "actions": "Actions", + "noTriggers": "No triggers configured for this camera.", + "edit": "Edit", + "deleteTrigger": "Delete Trigger", + "lastTriggered": "Last triggered" + }, + "type": { + "thumbnail": "Thumbnail", + "description": "Description" + }, + "actions": { + "notification": "Send Notification", + "sub_label": "Add Sub Label", + "attribute": "Add Attribute" + }, + "dialog": { + "createTrigger": { + "title": "Create Trigger", + "desc": "Create a trigger for camera {{camera}}" + }, + "editTrigger": { + "title": "Edit Trigger", + "desc": "Edit the settings for trigger on camera {{camera}}" + }, + "deleteTrigger": { + "title": "Delete Trigger", + "desc": "Are you sure you want to delete the trigger {{triggerName}}? This action cannot be undone." + }, + "form": { + "name": { + "title": "Name", + "placeholder": "Name this trigger", + "description": "Enter a unique name or description to identify this trigger", + "error": { + "minLength": "Field must be at least 2 characters long.", + "invalidCharacters": "Field can only contain letters, numbers, underscores, and hyphens.", + "alreadyExists": "A trigger with this name already exists for this camera." + } + }, + "enabled": { + "description": "Enable or disable this trigger" + }, + "type": { + "title": "Type", + "placeholder": "Select trigger type", + "description": "Trigger when a similar tracked object description is detected", + "thumbnail": "Trigger when a similar tracked object thumbnail is detected" + }, + "content": { + "title": "Content", + "imagePlaceholder": "Select a thumbnail", + "textPlaceholder": "Enter text content", + "imageDesc": "Only the most recent 100 thumbnails are displayed. If you can't find your desired thumbnail, please review earlier objects in Explore and set up a trigger from the menu there.", + "textDesc": "Enter text to trigger this action when a similar tracked object description is detected.", + "error": { + "required": "Content is required." + } + }, + "threshold": { + "title": "Threshold", + "desc": "Set the similarity threshold for this trigger. A higher threshold means a closer match is required to fire the trigger.", + "error": { + "min": "Threshold must be at least 0", + "max": "Threshold must be at most 1" + } + }, + "actions": { + "title": "Actions", + "desc": "By default, Frigate fires an MQTT message for all triggers. Sub labels add the trigger name to the object label. Attributes are searchable metadata stored separately in the tracked object metadata.", + "error": { + "min": "At least one action must be selected." + } + } + } + }, + "wizard": { + "title": "Create Trigger", + "step1": { + "description": "Configure the basic settings for your trigger." + }, + "step2": { + "description": "Set up the content that will trigger this action." + }, + "step3": { + "description": "Configure the threshold and actions for this trigger." + }, + "steps": { + "nameAndType": "Name and Type", + "configureData": "Configure Data", + "thresholdAndActions": "Threshold and Actions" + } + }, + "toast": { + "success": { + "createTrigger": "Trigger {{name}} created successfully.", + "updateTrigger": "Trigger {{name}} updated successfully.", + "deleteTrigger": "Trigger {{name}} deleted successfully." + }, + "error": { + "createTriggerFailed": "Failed to create trigger: {{errorMessage}}", + "updateTriggerFailed": "Failed to update trigger: {{errorMessage}}", + "deleteTriggerFailed": "Failed to delete trigger: {{errorMessage}}" + } + } + }, + "maintenance": { + "title": "Maintenance", + "sync": { + "title": "Media Sync", + "desc": "Frigate will periodically clean up media on a regular schedule according to your retention configuration. It is normal to see a few orphaned files as Frigate runs. Use this feature to remove orphaned media files from disk that are no longer referenced in the database.", + "started": "Media sync started.", + "alreadyRunning": "A sync job is already running", + "error": "Failed to start sync", + "currentStatus": "Status", + "jobId": "Job ID", + "startTime": "Start Time", + "endTime": "End Time", + "statusLabel": "Status", + "results": "Results", + "errorLabel": "Error", + "mediaTypes": "Media Types", + "allMedia": "All Media", + "dryRun": "Dry Run", + "dryRunEnabled": "No files will be deleted", + "dryRunDisabled": "Files will be deleted", + "force": "Force", + "forceDesc": "Bypass safety threshold and complete sync even if more than 50% of the files would be deleted.", + "verbose": "Verbose", + "verboseDesc": "Write a full list of orphaned files to disk for review.", + "running": "Sync Running...", + "start": "Start Sync", + "inProgress": "Sync is in progress. This page is disabled.", + "status": { + "queued": "Queued", + "running": "Running", + "completed": "Completed", + "failed": "Failed", + "notRunning": "Not Running" + }, + "resultsFields": { + "filesChecked": "Files Checked", + "orphansFound": "Orphans Found", + "orphansDeleted": "Orphans Deleted", + "aborted": "Aborted. Deletion would exceed safety threshold.", + "error": "Error", + "totals": "Totals" + }, + "event_snapshots": "Tracked Object Snapshots", + "event_thumbnails": "Tracked Object Thumbnails", + "review_thumbnails": "Review Thumbnails", + "previews": "Previews", + "exports": "Exports", + "recordings": "Recordings" + }, + "regionGrid": { + "title": "Region Grid", + "desc": "The region grid is an optimization that learns where objects of different sizes typically appear in each camera's field of view. Frigate uses this data to efficiently size detection regions. The grid is automatically built over time from tracked object data.", + "clear": "Clear region grid", + "clearConfirmTitle": "Clear Region Grid", + "clearConfirmDesc": "Clearing the region grid is not recommended unless you have recently changed your detector model size or have changed your camera's physical position and are having object tracking issues. The grid will be automatically rebuilt over time as objects are tracked. A Frigate restart is required for changes to take effect.", + "clearSuccess": "Region grid cleared successfully", + "clearError": "Failed to clear region grid", + "restartRequired": "Restart required for region grid changes to take effect" + } + }, + "configForm": { + "global": { + "title": "Global Settings", + "description": "These settings apply to all cameras unless overridden in the camera-specific settings." + }, + "camera": { + "title": "Camera Settings", + "description": "These settings apply only to this camera and override the global settings.", + "noCameras": "No cameras available" + }, + "advancedSettingsCount": "Advanced Settings ({{count}})", + "advancedCount": "Advanced ({{count}})", + "showAdvanced": "Show Advanced Settings", + "tabs": { + "sharedDefaults": "Shared Defaults", + "system": "System", + "integrations": "Integrations" + }, + "additionalProperties": { + "keyLabel": "Key", + "valueLabel": "Value", + "keyPlaceholder": "New key", + "remove": "Remove", + "providerNameLabel": "Provider name", + "providerNamePlaceholder": "e.g., openai", + "variableNameLabel": "Variable name", + "variableNamePlaceholder": "e.g., MY_VARIABLE", + "loggerNameLabel": "Logger name", + "loggerNamePlaceholder": "e.g., frigate.record", + "keyPatternError": "Use only letters, numbers, hyphens, and underscores (no spaces)" + }, + "knownPlates": { + "namePlaceholder": "e.g., Wife's Car", + "platePlaceholder": "Plate number or regex" + }, + "liveStreams": { + "streamNameLabel": "Stream name", + "streamNamePlaceholder": "e.g., Main HD Stream", + "go2rtcStreamLabel": "go2rtc stream", + "go2rtcStreamPlaceholder": "Select a go2rtc stream", + "go2rtcStreamSearch": "Search or enter a stream name…", + "noGo2rtcStreams": "No go2rtc streams configured", + "availableStreams": "Available streams", + "useCustom": "Use \"{{value}}\"", + "addStream": "Add stream" + }, + "timezone": { + "defaultOption": "Use browser timezone" + }, + "roleMap": { + "empty": "No role mappings", + "roleLabel": "Role", + "groupsLabel": "Groups", + "addMapping": "Add role mapping", + "remove": "Remove" + }, + "ffmpegArgs": { + "preset": "Preset", + "manual": "Manual arguments", + "inherit": "Inherit from camera setting", + "none": "None", + "useGlobalSetting": "Inherit from global setting", + "selectPreset": "Select preset", + "manualPlaceholder": "Enter FFmpeg arguments", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (Generic)", + "preset-http-mjpeg-generic": "HTTP MJPEG (Generic)", + "preset-http-reolink": "HTTP - Reolink Cameras", + "preset-rtmp-generic": "RTMP (Generic)", + "preset-rtsp-generic": "RTSP (Generic)", + "preset-rtsp-restream": "RTSP - Restream from go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - Restream from go2rtc (Low Latency)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Record (Generic, no audio)", + "preset-record-generic-audio-copy": "Record (Generic + Copy Audio)", + "preset-record-generic-audio-aac": "Record (Generic + Audio to AAC)", + "preset-record-mjpeg": "Record - MJPEG Cameras", + "preset-record-jpeg": "Record - JPEG Cameras", + "preset-record-ubiquiti": "Record - Ubiquiti Cameras" + } + }, + "cameraInputs": { + "itemTitle": "Stream {{index}}", + "sourceMode": { + "restream": "Restream (go2rtc)", + "manual": "Manual input path", + "go2rtcStreamLabel": "go2rtc stream", + "go2rtcStreamPlaceholder": "Select a go2rtc stream", + "noGo2rtcStreams": "No go2rtc streams configured", + "go2rtcStreamSearch": "Search streams...", + "availableStreams": "Available streams", + "noMatchingStreams": "No matching streams" + } + }, + "restartRequiredField": "Restart required", + "restartRequiredFooter": "Configuration changed - Restart required", + "sections": { + "detect": "Detection", + "record": "Recording", + "snapshots": "Snapshots", + "motion": "Motion", + "objects": "Objects", + "review": "Review", + "audio": "Audio", + "notifications": "Notifications", + "live": "Live View", + "timestamp_style": "Timestamps", + "mqtt": "MQTT", + "database": "Database", + "telemetry": "Telemetry", + "auth": "Authentication", + "tls": "TLS", + "proxy": "Proxy", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg", + "detectors": "Detectors", + "model": "Model", + "semantic_search": "Semantic Search", + "genai": "GenAI", + "face_recognition": "Face Recognition", + "lpr": "License Plate Recognition", + "birdseye": "Birdseye", + "masksAndZones": "Masks / Zones" + }, + "detect": { + "title": "Detection Settings" + }, + "detectors": { + "title": "Detector Settings", + "singleType": "Only one {{type}} detector is allowed.", + "keyRequired": "Detector name is required.", + "keyDuplicate": "Detector name already exists.", + "noSchema": "No detector schemas are available.", + "none": "No detector instances configured.", + "add": "Add detector", + "addCustomKey": "Add custom key" + }, + "record": { + "title": "Recording Settings" + }, + "snapshots": { + "title": "Snapshot Settings" + }, + "motion": { + "title": "Motion Settings" + }, + "objects": { + "title": "Object Settings" + }, + "audioLabels": { + "summary": "{{count}} audio labels selected", + "empty": "No audio labels available" + }, + "objectLabels": { + "summary": "{{count}} object types selected", + "empty": "No object labels available" + }, + "reviewLabels": { + "summary": "{{count}} labels selected", + "empty": "No labels available" + }, + "filters": { + "objectFieldLabel": "{{field}} for {{label}}" + }, + "zoneNames": { + "summary": "{{count}} selected", + "empty": "No zones available" + }, + "inputRoles": { + "summary": "{{count}} roles selected", + "empty": "No roles available", + "options": { + "detect": "Detect", + "record": "Record", + "audio": "Audio" + } + }, + "genaiRoles": { + "options": { + "embeddings": "Embedding", + "descriptions": "Descriptions", + "chat": "Chat" + } + }, + "semanticSearchModel": { + "placeholder": "Select model…", + "builtIn": "Built-in Models", + "genaiProviders": "GenAI Providers" + }, + "semanticSearchModelSize": { + "notApplicable": "Not applicable for GenAI providers" + }, + "review": { + "title": "Review Settings" + }, + "audio": { + "title": "Audio Settings" + }, + "notifications": { + "title": "Notification Settings" + }, + "live": { + "title": "Live View Settings" + }, + "timestamp_style": { + "title": "Timestamp Settings" + }, + "searchPlaceholder": "Search...", + "addCustomLabel": "Add custom label...", + "genaiModel": { + "placeholder": "Select or enter a model…", + "search": "Search or enter a model…", + "noModels": "No models available", + "available": "Available models", + "useCustom": "Use \"{{value}}\"", + "refresh": "Refresh models", + "probeFailed": "Failed to probe models", + "fetchedModels": "Successfully fetched model list" + }, + "ptzPresets": { + "placeholder": "Select or enter a preset...", + "search": "Search or enter a preset...", + "noPresets": "No presets available", + "available": "Camera presets", + "useCustom": "Use \"{{value}}\"" + }, + "defaultRole": { + "admin": "Admin", + "viewer": "Viewer" + } + }, + "globalConfig": { + "title": "Global Configuration", + "description": "Configure global settings that apply to all cameras unless overridden.", + "toast": { + "success": "Global settings saved successfully", + "error": "Failed to save global settings", + "validationError": "Validation failed" + } + }, + "cameraConfig": { + "title": "Camera Configuration", + "description": "Configure settings for individual cameras. Settings override global defaults.", + "overriddenBadge": "Overridden", + "resetToGlobal": "Reset to Global", + "toast": { + "success": "Camera settings saved successfully", + "error": "Failed to save camera settings" + } + }, + "toast": { + "success": "Settings saved successfully", + "applied": "Settings applied successfully", + "successRestartRequired": "Settings saved successfully. Restart Frigate to apply your changes.", + "error": "Failed to save settings", + "validationError": "Validation failed: {{message}}", + "resetSuccess": "Reset to global defaults", + "resetError": "Failed to reset settings", + "saveAllSuccess_one": "Saved {{count}} section successfully.", + "saveAllSuccess_other": "All {{count}} sections saved successfully.", + "saveAllSuccessRestartRequired_one": "Saved {{count}} section successfully. Restart Frigate to apply your changes.", + "saveAllSuccessRestartRequired_other": "All {{count}} sections saved successfully. Restart Frigate to apply your changes.", + "saveAllPartial_one": "{{successCount}} of {{totalCount}} section saved. {{failCount}} failed.", + "saveAllPartial_other": "{{successCount}} of {{totalCount}} sections saved. {{failCount}} failed.", + "saveAllFailure": "Failed to save all sections." + }, + "profiles": { + "title": "Profiles", + "activeProfile": "Active Profile", + "noActiveProfile": "No active profile", + "active": "Active", + "activated": "Profile '{{profile}}' activated", + "activateFailed": "Failed to set profile", + "deactivated": "Profile deactivated", + "noProfiles": "No profiles defined.", + "noOverrides": "No overrides", + "cameraCount_one": "{{count}} camera", + "cameraCount_other": "{{count}} cameras", + "columnCamera": "Camera", + "columnOverrides": "Profile Overrides", + "baseConfig": "Base Config", + "addProfile": "Add Profile", + "newProfile": "New Profile", + "profileNamePlaceholder": "e.g., Armed, Away, Night Mode", + "friendlyNameLabel": "Profile Name", + "profileIdLabel": "Profile ID", + "profileIdDescription": "Internal identifier used in config and automations", + "nameInvalid": "Only lowercase letters, numbers, and underscores allowed", + "nameDuplicate": "A profile with this name already exists", + "error": { + "mustBeAtLeastTwoCharacters": "Must be at least 2 characters", + "mustNotContainPeriod": "Must not contain periods", + "alreadyExists": "A profile with this ID already exists" + }, + "renameProfile": "Rename Profile", + "renameSuccess": "Profile renamed to '{{profile}}'", + "deleteProfile": "Delete Profile", + "deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.", + "deleteSuccess": "Profile '{{profile}}' deleted", + "createSuccess": "Profile '{{profile}}' created", + "removeOverride": "Remove Profile Override", + "deleteSection": "Delete Section Overrides", + "deleteSectionConfirm": "Remove the {{section}} overrides for profile {{profile}} on {{camera}}?", + "deleteSectionSuccess": "Removed {{section}} overrides for {{profile}}", + "enableSwitch": "Enable Profiles", + "enabledDescription": "Profiles are enabled. Create a new profile below, navigate to a camera config section to make your changes, and save for changes to take effect.", + "disabledDescription": "Profiles allow you to define named sets of camera config overrides (e.g., armed, away, night) that can be activated on demand." + }, + "unsavedChanges": "You have unsaved changes", + "confirmReset": "Confirm Reset", + "resetToDefaultDescription": "This will reset all settings in this section to their default values. This action cannot be undone.", + "resetToGlobalDescription": "This will reset the settings in this section to the global defaults. This action cannot be undone.", + "go2rtcStreams": { + "title": "go2rtc Streams", + "description": "Manage go2rtc stream configurations for camera restreaming. Each stream has a name and one or more source URLs.", + "addStream": "Add stream", + "addStreamDesc": "Enter a name for the new stream. This name will be used to reference the stream in your camera configuration.", + "addUrl": "Add URL", + "sourceNumber": "Source {{index}}", + "streamName": "Stream name", + "streamNamePlaceholder": "e.g., front_door", + "streamUrlPlaceholder": "e.g., rtsp://user:pass@192.168.1.100/stream", + "deleteStream": "Delete stream", + "deleteStreamConfirm": "Are you sure you want to delete the stream \"{{streamName}}\"? Cameras that reference this stream may stop working.", + "noStreams": "No go2rtc streams configured. Add a stream to get started.", + "validation": { + "nameRequired": "Stream name is required", + "nameDuplicate": "A stream with this name already exists", + "nameInvalid": "Stream name can only contain letters, numbers, underscores, and hyphens", + "urlRequired": "At least one URL is required" + }, + "renameStream": "Rename stream", + "renameStreamDesc": "Enter a new name for this stream. Renaming a stream may break cameras or other streams that reference it by name.", + "newStreamName": "New stream name", + "ffmpeg": { + "useFfmpegModule": "Use compatibility mode (ffmpeg)", + "video": "Video", + "audio": "Audio", + "hardware": "Hardware acceleration", + "videoCopy": "Copy", + "videoH264": "Transcode to H.264", + "videoH265": "Transcode to H.265", + "videoExclude": "Exclude", + "audioCopy": "Copy", + "audioAac": "Transcode to AAC", + "audioOpus": "Transcode to Opus", + "audioPcmu": "Transcode to PCM μ-law", + "audioPcma": "Transcode to PCM A-law", + "audioPcm": "Transcode to PCM", + "audioMp3": "Transcode to MP3", + "audioExclude": "Exclude", + "hardwareNone": "No hardware acceleration", + "hardwareAuto": "Automatic (recommended)", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "Add video codec", + "addAudioCodec": "Add audio codec", + "removeCodec": "Remove codec" + } + }, + "birdseye": { + "trackingMode": { + "objects": "Objects", + "motion": "Motion", + "continuous": "Continuous" + }, + "cameraOrder": { + "label": "Camera order", + "description": "Drag cameras to set their order in the Birdseye layout.", + "reorderHandle": "Drag to reorder", + "saving": "Saving…", + "saved": "Saved" + } + }, + "retainMode": { + "all": "All", + "motion": "Motion", + "active_objects": "Active Objects" + }, + "previewQuality": { + "very_high": "Very High", + "high": "High", + "medium": "Medium", + "low": "Low", + "very_low": "Very Low" + }, + "ui": { + "timeFormat": { + "browser": "Browser", + "12hour": "12 hour", + "24hour": "24 hour" + }, + "unitSystem": { + "metric": "Metric", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Recordings", + "previews": "Previews" + } + }, + "logger": { + "logLevel": { + "debug": "Debug", + "info": "Info", + "warning": "Warning", + "error": "Error", + "critical": "Critical" + } + }, + "onvif": { + "profileAuto": "Auto", + "profileLoading": "Loading profiles...", + "autotracking": { + "zooming": { + "disabled": "Disabled", + "absolute": "Absolute", + "relative": "Relative" + } + } + }, + "modelSize": { + "small": "Small", + "large": "Large" + }, + "configMessages": { + "review": { + "recordDisabled": "Recording is disabled, review items will not be generated.", + "detectDisabled": "Object detection is disabled. Review items require detected objects to categorize alerts and detections.", + "allNonAlertDetections": "All non-alert activity will be included as detections.", + "genaiImageSourceRecordingsRecordDisabled": "Image source is set to 'recordings', but recording is disabled. Frigate will fall back to preview images." + }, + "audio": { + "noAudioRole": "No streams have the audio role defined. You must enable the audio role for audio detection to function." + }, + "audioTranscription": { + "audioDetectionDisabled": "Audio detection is not enabled for this camera. Audio transcription requires audio detection to be active." + }, + "detect": { + "fpsGreaterThanFive": "Setting the detect FPS higher than 5 is not recommended. Higher values may cause performance issues and will not provide any benefit.", + "disabled": "Object detection is disabled. Snapshots, review items, and enrichments such as face recognition, license plate recognition, and Generative AI will not function.", + "resolutionShouldBeMultipleOfFour": "For best results, detect width and height should be multiples of 4. Other even values may produce visual artifacts or slight distortion in the detect stream.", + "aspectRatioMismatch": "The width and height you've entered don't match the aspect ratio of your current detect resolution. This may produce a stretched or distorted image.", + "maxFramesSet": "Setting max frames overrides default behavior and disables stationary object tracking. There are very few situations where this is needed, use with caution.", + "squareResolution": "A square detect resolution is unusual. The detect width and height should match your camera's aspect ratio (for example, 16:9), not the dimensions of the object detection model. A mismatched aspect ratio can stretch the image and reduce detection accuracy.", + "resolutionHigh": "This detect resolution is higher than recommended and may cause increased resource usage without improving detection accuracy. A detect resolution at or below 1080p is recommended for most cameras.", + "globalResolutionMultipleCameras": "A global detect resolution is set while multiple cameras are configured. Unless all cameras share the same resolution and aspect ratio, the detect width and height should be defined per camera to match each camera's native aspect ratio." + }, + "model": { + "optimizedFor320": "Frigate is optimized for a 320x320 model, which is the best choice for most setups. A 640x640 model is slower and only helps in specific scenarios.", + "inputDimensionsNotDetectResolution": "Model input width and height are the input dimensions of the object detection model, not your camera's detect resolution. They should match the dimensions of the model you're using — typically a square size like 320x320 or 640x640." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "Manual hardware acceleration arguments are not recommended. Unless a specific requirement exists, select the preset that matches your hardware." + }, + "objects": { + "genaiNoDescriptionsProvider": "You must configure a GenAI provider with the 'descriptions' role for descriptions to be generated." + }, + "faceRecognition": { + "globalDisabled": "The face recognition enrichment must be enabled for face recognition features to function on this camera.", + "personNotTracked": "Face recognition requires the 'person' object to be tracked. Enable 'person' in Objects for this camera.", + "modelSizeLarge": "The 'large' model requires a GPU or NPU for reasonable performance. Use 'small' on CPU-only systems." + }, + "lpr": { + "globalDisabled": "The license plate recognition enrichment must be enabled for LPR features to function on this camera.", + "vehicleNotTracked": "License plate recognition requires 'car' or 'motorcycle' to be tracked. Enable 'car' or 'motorcycle' in Objects for this camera.", + "modelSizeLarge": "The 'large' model is optimized for multi-line license plates. The 'small' model provides better performance over 'large' and should be used unless your region uses multi-line plate formats." + }, + "record": { + "noRecordRole": "No streams have the record role defined. Recording will not function." + }, + "birdseye": { + "objectsModeDetectDisabled": "Birdseye is set to 'objects' mode, but object detection is disabled for this camera. The camera will not appear in Birdseye." + }, + "snapshots": { + "detectDisabled": "Object detection is disabled. Snapshots are generated from tracked objects and will not be created." + }, + "detectors": { + "mixedTypes": "All detectors must use the same type. Remove existing detectors to use a different type.", + "mixedTypesSuggestion": "All detectors must use the same type. Remove existing detectors or select {{type}}." + }, + "semanticSearch": { + "jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended." + }, + "onvif": { + "autotrackingNoZones": "Autotracking requires at least one zone. Define a zone for this camera in Masks / Zones, then set it as a required zone below." + } + } +} diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json new file mode 100644 index 0000000..9f387a7 --- /dev/null +++ b/web/public/locales/en/views/system.json @@ -0,0 +1,274 @@ +{ + "documentTitle": { + "cameras": "Cameras Stats - Frigate", + "storage": "Storage Stats - Frigate", + "general": "General Stats - Frigate", + "enrichments": "Enrichments Stats - Frigate", + "logs": { + "frigate": "Frigate Logs - Frigate", + "go2rtc": "Go2RTC Logs - Frigate", + "nginx": "Nginx Logs - Frigate", + "websocket": "Messages Logs - Frigate" + } + }, + "title": "System", + "metrics": "System metrics", + "logs": { + "websocket": { + "label": "Messages", + "pause": "Pause", + "resume": "Resume", + "clear": "Clear", + "filter": { + "all": "All topics", + "topics": "Topics", + "events": "Events", + "reviews": "Reviews", + "classification": "Classification", + "face_recognition": "Face Recognition", + "lpr": "LPR", + "camera_activity": "Camera activity", + "system": "System", + "camera": "Camera", + "all_cameras": "All cameras", + "cameras_count_one": "{{count}} Camera", + "cameras_count_other": "{{count}} Cameras" + }, + "empty": "No messages captured yet", + "count_one": "{{count}} message", + "count_other": "{{count}} messages", + "expanded": { + "payload": "Payload" + } + }, + "download": { + "label": "Download Logs" + }, + "copy": { + "label": "Copy to Clipboard", + "success": "Copied logs to clipboard", + "error": "Could not copy logs to clipboard" + }, + "type": { + "label": "Type", + "timestamp": "Timestamp", + "tag": "Tag", + "message": "Message" + }, + "tips": "Logs are streaming from the server", + "toast": { + "error": { + "fetchingLogsFailed": "Error fetching logs: {{errorMessage}}", + "whileStreamingLogs": "Error while streaming logs: {{errorMessage}}" + } + } + }, + "general": { + "title": "General", + "detector": { + "title": "Detectors", + "inferenceSpeed": "Detector Inference Speed", + "temperature": "Detector Temperature", + "cpuUsage": "Detector CPU Usage", + "cpuUsageInformation": "CPU used in preparing input and output data to/from detection models. This value does not measure inference usage, even if using a GPU or accelerator.", + "memoryUsage": "Detector Memory Usage" + }, + "hardwareInfo": { + "title": "Hardware Info", + "gpuUsage": "GPU Usage", + "gpuMemory": "GPU Memory", + "gpuEncoder": "GPU Encoder", + "gpuCompute": "GPU Compute / Encode", + "gpuDecoder": "GPU Decoder", + "gpuTemperature": "GPU Temperature", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo Output", + "returnCode": "Return Code: {{code}}", + "processOutput": "Process Output:", + "processError": "Process Error:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI Output", + "name": "Name: {{name}}", + "driver": "Driver: {{driver}}", + "cudaComputerCapability": "CUDA Compute Capability: {{cuda_compute}}", + "vbios": "VBios Info: {{vbios}}" + }, + "closeInfo": { + "label": "Close GPU info" + }, + "copyInfo": { + "label": "Copy GPU info" + }, + "toast": { + "success": "Copied GPU info to clipboard" + } + }, + "npuUsage": "NPU Usage", + "npuMemory": "NPU Memory", + "npuTemperature": "NPU Temperature", + "intelGpuWarning": { + "title": "Intel GPU Stats Warning", + "message": "GPU stats unavailable", + "description": "This is a known bug in Intel's GPU stats reporting tools (intel_gpu_top) where it will break and repeatedly return a GPU usage of 0% even in cases where hardware acceleration and object detection are correctly running on the (i)GPU. This is not a Frigate bug. You can restart the host to temporarily fix the issue and confirm that the GPU is working correctly. This does not affect performance." + } + }, + "otherProcesses": { + "title": "Other Processes", + "processCpuUsage": "Process CPU Usage", + "processMemoryUsage": "Process Memory Usage", + "series": { + "go2rtc": "go2rtc", + "recording": "recording", + "review_segment": "review segment", + "embeddings": "embeddings", + "audio_detector": "audio detector" + } + } + }, + "storage": { + "title": "Storage", + "overview": "Overview", + "recordings": { + "title": "Recordings", + "tips": "This value represents the total storage used by the recordings in Frigate's database. Frigate does not track storage usage for all files on your disk.", + "earliestRecording": "Earliest recording available:" + }, + "shm": { + "title": "SHM (shared memory) allocation", + "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": { + "title": "Camera Storage", + "camera": "Camera", + "unusedStorageInformation": "Unused Storage Information", + "storageUsed": "Storage", + "percentageOfTotalUsed": "Percentage of Total", + "bandwidth": "Bandwidth", + "unused": { + "title": "Unused", + "tips": "This value may not accurately represent the free space available to Frigate if you have other files stored on your drive beyond Frigate's recordings. Frigate does not track storage usage outside of its recordings." + } + } + }, + "cameras": { + "title": "Cameras", + "overview": "Overview", + "info": { + "aspectRatio": "aspect ratio", + "cameraProbeInfo": "{{camera}} Camera Probe Info", + "streamDataFromFFPROBE": "Stream data is obtained with ffprobe.", + "fetching": "Fetching Camera Data", + "stream": "Stream {{idx}}", + "video": "Video:", + "codec": "Codec:", + "resolution": "Resolution:", + "fps": "FPS:", + "unknown": "Unknown", + "audio": "Audio:", + "error": "Error: {{error}}", + "tips": { + "title": "Camera Probe Info" + }, + "keyframes": { + "title": "Keyframe analysis", + "analyzing": "Analyzing keyframes... {{seconds}} seconds remaining", + "stillAnalyzing": "Still analyzing keyframes...", + "recordStream": "Record stream:", + "keyframeCount": "Keyframes observed:", + "observedDuration": "Observed duration:", + "gap": "Keyframe gap (min / avg / max):", + "segmentLength": "Recording segment length:", + "ok": "Keyframes every ~{{seconds}}s, good for recording and playback.", + "warning": "Sparse or variable keyframes (longest gap ~{{seconds}}s), likely a smart codec (H.264+/H.265+), this is not recommended.", + "error": "Keyframe gap (~{{seconds}}s) exceeds the recording segment length ({{segmentTime}}s). Some segments may have no keyframe, which breaks playback. Disable the smart/+ codec on the camera or shorten its keyframe interval.", + "unknown": "Couldn't determine keyframe spacing.", + "recordDisabled": "Recording is disabled for this camera." + } + }, + "framesAndDetections": "Frames / Detections", + "noCameras": { + "title": "No Cameras Found" + }, + "label": { + "camera": "camera", + "detect": "detect", + "skipped": "skipped", + "ffmpeg": "FFmpeg", + "capture": "capture", + "overallFramesPerSecond": "overall frames per second", + "overallDetectionsPerSecond": "overall detections per second", + "overallSkippedDetectionsPerSecond": "overall skipped detections per second", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} capture", + "cameraDetect": "{{camName}} detect", + "cameraGpu": "{{camName}} GPU", + "cameraFramesPerSecond": "{{camName}} frames per second", + "cameraDetectionsPerSecond": "{{camName}} detections per second", + "cameraSkippedDetectionsPerSecond": "{{camName}} skipped detections per second" + }, + "connectionQuality": { + "title": "Connection Quality", + "excellent": "Excellent", + "fair": "Fair", + "poor": "Poor", + "unusable": "Unusable", + "fps": "FPS", + "expectedFps": "Expected FPS", + "reconnectsLastHour": "Reconnects (last hour)", + "stallsLastHour": "Stalls (last hour)" + }, + "toast": { + "success": { + "copyToClipboard": "Copied probe data to clipboard." + }, + "error": { + "unableToProbeCamera": "Unable to probe camera: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Last refreshed: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} has high FFmpeg CPU usage ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)", + "healthy": "System is healthy", + "reindexingEmbeddings": "Reindexing embeddings ({{processed}}% complete)", + "cameraIsOffline": "{{camera}} is offline", + "detectIsSlow": "{{detect}} is slow ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} is very slow ({{speed}} ms)", + "shmTooLow": "/dev/shm allocation ({{total}} MB) should be increased to at least {{min}} MB.", + "debugReplayActive": "Debug replay session is active" + }, + "enrichments": { + "title": "Enrichments", + "infPerSecond": "Inferences Per Second", + "averageInf": "Average Inference Time", + "embeddings": { + "image_embedding": "Image Embedding", + "text_embedding": "Text Embedding", + "face_recognition": "Face Recognition", + "plate_recognition": "Plate Recognition", + "image_embedding_speed": "Image Embedding Speed", + "face_embedding_speed": "Face Embedding Speed", + "face_recognition_speed": "Face Recognition Speed", + "plate_recognition_speed": "Plate Recognition Speed", + "text_embedding_speed": "Text Embedding Speed", + "yolov9_plate_detection_speed": "YOLOv9 Plate Detection Speed", + "yolov9_plate_detection": "YOLOv9 Plate Detection", + "review_description": "Review Description", + "review_description_speed": "Review Description Speed", + "review_description_events_per_second": "Review Description", + "object_description": "Object Description", + "object_description_speed": "Object Description Speed", + "object_description_events_per_second": "Object Description", + "classification": "{{name}} Classification", + "classification_speed": "{{name}} Classification Speed", + "classification_events_per_second": "{{name}} Classification Events Per Second" + } + } +} diff --git a/web/public/locales/es/audio.json b/web/public/locales/es/audio.json new file mode 100644 index 0000000..2641cb5 --- /dev/null +++ b/web/public/locales/es/audio.json @@ -0,0 +1,503 @@ +{ + "bark": "Ladrido", + "sheep": "Oveja", + "goat": "Cabra", + "car": "Coche", + "boat": "Barco", + "bus": "Autobus", + "motorcycle": "Motocicleta", + "train": "Tren", + "skateboard": "Monopatín", + "sink": "Fregadero", + "blender": "Batidora", + "hair_dryer": "Secador de pelo", + "scissors": "Tijeras", + "clock": "Reloj", + "camera": "Cámara", + "door": "Puerta", + "dog": "Perro", + "horse": "Caballo", + "toothbrush": "Cepillo de dientes", + "bird": "Pájaro", + "vehicle": "Vehículo", + "mouse": "Ratón", + "bicycle": "Bicicleta", + "cat": "Gato", + "keyboard": "Teclado", + "animal": "Animal", + "yell": "Grito", + "bellow": "Voz de trueno", + "whoop": "Aullido", + "crying": "Llanto", + "synthetic_singing": "Canto sintético", + "rapping": "Rap", + "humming": "Zumbido leve", + "groan": "Gemido", + "grunt": "Gruñido", + "whistling": "Silbido", + "breathing": "Respiración", + "wheeze": "Sibilancia", + "snoring": "Ronquido", + "gasp": "Jadeo", + "snort": "Resoplido", + "cough": "Tos", + "sneeze": "Estornudo", + "sniff": "Oler", + "run": "Correr", + "shuffle": "Arrastrar los pies", + "footsteps": "Pasos", + "chewing": "Masticar", + "biting": "Morder", + "gargling": "Hacer gárgaras", + "stomach_rumble": "Rugido de estómago", + "burping": "Eructo", + "finger_snapping": "Chasquido de dedos", + "clapping": "Aplausos", + "heartbeat": "Latido del corazón", + "heart_murmur": "Soplo cardíaco", + "cheering": "Aclamación", + "applause": "Aplausos", + "whispering": "Susurro", + "speech": "Habla", + "mantra": "Mantra", + "fart": "Pedos", + "snicker": "Risa maliciosa", + "yodeling": "Yodeling", + "laughter": "Risa", + "child_singing": "Canto infantil", + "pant": "Jadeo", + "throat_clearing": "Despejar la garganta", + "sigh": "Suspiro", + "choir": "Coro", + "babbling": "Balbuceo", + "singing": "Canto", + "hands": "Manos", + "hiccup": "Hipido", + "chant": "Cántico", + "chatter": "Charla", + "crowd": "Multitud", + "children_playing": "Niños jugando", + "pets": "Mascotas", + "yip": "Ladrido corto", + "howl": "Aullido", + "bow_wow": "Guau", + "growling": "Gruñido", + "whimper_dog": "Gemido de perro", + "purr": "Ronroneo", + "hiss": "Siseo", + "caterwaul": "Aullido de gato", + "livestock": "Ganado", + "clip_clop": "Trote de caballo", + "neigh": "Relincho", + "cattle": "Ganado", + "moo": "Muu", + "cowbell": "Campanilla de vaca", + "pig": "Cerdo", + "oink": "Oink", + "bleat": "Balido", + "fowl": "Aves", + "cluck": "Cacareo", + "cock_a_doodle_doo": "Quiquiriquí", + "turkey": "Pavo", + "gobble": "Gluglú", + "duck": "Pato", + "goose": "Ganso", + "honk": "Bocina", + "wild_animals": "Animales salvajes", + "roar": "Rugido", + "chirp": "Canto (de insecto o pájaro)", + "pigeon": "Paloma", + "coo": "Arrullo", + "caw": "Grito de cuervo", + "owl": "Búho", + "dogs": "Perros", + "insect": "Insecto", + "cricket": "Grillo", + "mosquito": "Mosquito", + "buzz": "Zumbido", + "frog": "Rana", + "croak": "Croar", + "snake": "Serpiente", + "rattle": "Sonajero", + "whale_vocalization": "Vocalización de ballena", + "plucked_string_instrument": "Instrumento de cuerda punteada", + "guitar": "Guitarra", + "steel_guitar": "Guitarra de acero", + "tapping": "Tapping (técnica de guitarra)", + "strum": "Rasgueo", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolina", + "zither": "Cítara", + "ukulele": "Ukelele", + "piano": "Piano", + "organ": "Órgano", + "electronic_organ": "Órgano electrónico", + "hammond_organ": "Órgano Hammond", + "sampler": "Sampler", + "harpsichord": "Clavicémbalo", + "percussion": "Percusión", + "drum_kit": "Batería", + "drum_machine": "Caja de ritmos", + "drum": "Tambor", + "snare_drum": "Caja (o redoblante)", + "rimshot": "Golpe en el borde del tambor", + "tabla": "Tabla", + "cymbal": "Platillo", + "hi_hat": "Hi-Hat", + "wood_block": "Bloque de madera", + "tambourine": "Pandereta", + "maraca": "Maraca", + "gong": "Gong", + "tubular_bells": "Campanas tubulares", + "mallet_percussion": "Percusión con mazas", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "steelpan": "SarténAcero", + "orchestra": "Orquesta", + "trumpet": "Trompeta", + "string_section": "Sección de cuerdas", + "violin": "Violín", + "double_bass": "Contrabajo", + "wind_instrument": "Instrumento de viento", + "flute": "Flauta", + "saxophone": "Saxofón", + "harp": "Arpa", + "jingle_bell": "Campanilla", + "bicycle_bell": "Campana de bicicleta", + "tuning_fork": "Diapasón", + "chime": "Campanilla", + "wind_chime": "Campanario de viento", + "harmonica": "Armónica", + "accordion": "Acordeón", + "didgeridoo": "Didgeridoo", + "theremin": "Theremín", + "singing_bowl": "Cuenco tibetano", + "scratching": "Rasguñado", + "hip_hop_music": "Música hip-hop", + "rock_music": "Música rock", + "heavy_metal": "Heavy metal", + "punk_rock": "Punk rock", + "progressive_rock": "Progressive rock", + "rock_and_roll": "Rock and roll", + "psychedelic_rock": "Rock psicodélico", + "rhythm_and_blues": "Rhythm and blues", + "soul_music": "Música soul", + "country": "País", + "swing_music": "Música swing", + "disco": "Disco", + "house_music": "Música House", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Electrónica", + "electronic_dance_music": "Música Dance Electronica", + "music_of_latin_america": "Música de América Latina", + "salsa_music": "Música Salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Música para niños", + "new-age_music": "Música New Age", + "vocal_music": "Música Vocal", + "a_capella": "A capella", + "afrobeat": "Afrobeat", + "music_of_asia": "Música de Asia", + "carnatic_music": "Música Carnatic", + "music_of_bollywood": "Música de Bollywood", + "ska": "Ska", + "song": "Canción", + "background_music": "Música Background", + "soundtrack_music": "Música de Pelicula", + "lullaby": "Cancion de cuna", + "video_game_music": "Música de Videojuego", + "christmas_music": "Música Navideña", + "sad_music": "Música triste", + "tender_music": "Música suave", + "exciting_music": "Música emocionante", + "angry_music": "Música enojada", + "scary_music": "Música aterradora", + "wind": "Viento", + "rustling_leaves": "Hojas susurrantes", + "wind_noise": "Ruido del viento", + "thunderstorm": "Tormenta eléctrica", + "thunder": "Trueno", + "raindrop": "Gota de lluvia", + "stream": "Arroyo", + "waterfall": "Cascada", + "ocean": "Oceano", + "steam": "Vapor", + "gurgling": "Gorgoteo", + "sailboat": "Vela", + "rowboat": "Bote de remos", + "motorboat": "Lancha motora", + "motor_vehicle": "Vehículo a motor", + "toot": "Pitido", + "tire_squeal": "Chillido de neumáticos", + "car_passing_by": "Coche pasando", + "ambulance": "Ambulancia", + "fire_engine": "Camión de bomberos", + "traffic_noise": "Ruido de tráfico", + "rail_transport": "Transporte ferroviario", + "aircraft_engine": "Aeronave motor", + "engine": "Motor", + "chainsaw": "Motosierra", + "medium_engine": "Motor de tamaño medio", + "heavy_engine": "Motor pesado", + "engine_knocking": "Golpeteo del motor", + "engine_starting": "Arranque del motor", + "idling": "Ralentí", + "accelerating": "Acelerando", + "doorbell": "Timbre", + "ding-dong": "Ding Dong", + "sliding_door": "Puerta corredera", + "slam": "Portazo", + "knock": "Golpe", + "tap": "Golpe suave", + "cupboard_open_or_close": "Apertura o cierre del armario", + "drawer_open_or_close": "Apertura o cierre del cajón", + "dishes": "Platos", + "cutlery": "Cubertería", + "chopping": "Cortando", + "frying": "Freír", + "microwave_oven": "Horno Microondas", + "water_tap": "Grifo de Agua", + "toilet_flush": "Descarga del Inodoro", + "electric_toothbrush": "Cepillo de Dientes Eléctrico", + "vacuum_cleaner": "Aspiradora", + "zipper": "Cremallera", + "keys_jangling": "Llaves Tintineando", + "coin": "Moneda", + "electric_shaver": "Afeitadora Eléctrica", + "shuffling_cards": "Barajar Cartas", + "typing": "Teclear", + "typewriter": "Máquina de Escribir", + "computer_keyboard": "Teclado de Computadora", + "writing": "Escribir", + "alarm": "Alarma", + "telephone": "Teléfono", + "telephone_bell_ringing": "Timbre de Teléfono Sonando", + "ringtone": "Tono de Llamada", + "telephone_dialing": "Marcación de Teléfono", + "dial_tone": "Tono de Marcación", + "busy_signal": "Señal de Ocupado", + "alarm_clock": "Reloj Despertador", + "siren": "Sirena", + "civil_defense_siren": "Sirena de Defensa Civil", + "buzzer": "Zumbador", + "fire_alarm": "Alarma de Incendio", + "foghorn": "Bocina de Niebla", + "whistle": "Silbato", + "steam_whistle": "Silbato de Vapor", + "mechanisms": "Mecanismos", + "ratchet": "Trinquete", + "tick": "Tictac", + "tick-tock": "Tictoc", + "gears": "Engranajes", + "pulleys": "Poleas", + "sewing_machine": "Máquina de Coser", + "mechanical_fan": "Ventilador Mecánico", + "air_conditioning": "Aire Acondicionado", + "cash_register": "Caja Registradora", + "printer": "Impresora", + "fly": "Mosca", + "patter": "Golpeteo", + "bell": "Campana", + "meow": "Miau", + "squawk": "Chillido", + "classical_music": "Música Clásica", + "cello": "Violoncello", + "quack": "Cuac", + "hoot": "Ulular", + "synthesizer": "Sintetizador", + "happy_music": "Música Alegre", + "timpani": "Tímpano", + "bowed_string_instrument": "Instrumento de cuerda frotada", + "jazz": "Jazz", + "train_whistle": "Silbido de tren", + "car_alarm": "Alarma de coche", + "truck": "Camion", + "ice_cream_truck": "Camión de helados", + "railroad_car": "Vagón de tren", + "aircraft": "Aeronave", + "helicopter": "Helicóptero", + "light_engine": "Motor ligero", + "dental_drill's_drill": "Talonador dental", + "crow": "Cuervo", + "flapping_wings": "Aleteo de alas", + "opera": "Opera", + "funk": "Funk", + "roaring_cats": "Gatos rugiendo", + "chicken": "Pollo", + "bagpipes": "Gaita", + "rats": "Ratas", + "music": "Música", + "musical_instrument": "Música instrumental", + "electric_guitar": "Guitarra eléctrica", + "bass_drum": "Bombo", + "acoustic_guitar": "Guitarra acústica", + "pizzicato": "Pizzicato", + "beatboxing": "Beatboxing", + "bass_guitar": "Bajo eléctrico", + "bluegrass": "Bluegrass", + "folk_music": "Música Folk", + "electronic_music": "Música electrónica", + "techno": "Techno", + "french_horn": "Trompa francesa", + "ship": "Barco", + "lawn_mower": "Cortacésped", + "electric_piano": "Piano eléctrico", + "train_wheels_squealing": "Chillido de ruedas de tren", + "drum_roll": "Redoble de tambor", + "vibraphone": "Vibrafón", + "trombone": "Trombón", + "brass_instrument": "Instrumento de metal", + "church_bell": "Campana de iglesia", + "clarinet": "Clarinete", + "grunge": "Grunge", + "pop_music": "Música pop", + "jingle": "Single", + "rain_on_surface": "Lluvia sobre superficie", + "emergency_vehicle": "Vehículo de emergencias", + "ambient_music": "Música Ambiente", + "trance_music": "Música Trance", + "music_of_africa": "Música de Africa", + "christian_music": "Música Cristiana", + "gospel_music": "Música Gospel", + "traditional_music": "Música Tradicional", + "wedding_music": "Música de Boda", + "rain": "Lluvia", + "waves": "Ondas", + "fire": "Fuego", + "police_car": "Coche de policia", + "squeak": "Chirrido", + "crackle": "Crepitar", + "reggae": "Reggae", + "middle_eastern_music": "Música del Medio Oriente", + "smoke_detector": "Detector de Humo", + "race_car": "Coche de carreras", + "air_horn": "Bocina de aire", + "independent_music": "Música Independiente", + "theme_music": "Música de Película", + "dance_music": "Música Dance", + "fixed-wing_aircraft": "Aeronave de ala fija", + "water": "Agua", + "propeller": "Hélice", + "air_brake": "Freno de aire", + "jet_engine": "Motor a reacción", + "power_windows": "Ventanas eléctricas", + "skidding": "Deslizamiento", + "reversing_beeps": "Bips de marcha atras", + "bathtub": "Bañera", + "train_horn": "Bocina de tren", + "subway": "Metro", + "single-lens_reflex_camera": "Cámara Réflex de un Solo Objetivo", + "tools": "Herramientas", + "hammer": "Martillo", + "filing": "Limar", + "jackhammer": "Martillo Neumático", + "sawing": "Serrar", + "sanding": "Lijar", + "power_tool": "Herramienta Eléctrica", + "burst": "Estallido", + "eruption": "Erupción", + "boom": "Estallido (Boom)", + "firecracker": "Petardo", + "artillery_fire": "Fuego de Artillería", + "cap_gun": "Pistola de Fulminantes", + "fireworks": "Fuegos Artificiales", + "wood": "Madera", + "chop": "Cortar (Madera)", + "splinter": "Astilla", + "crack": "Crujido", + "chink": "Tintineo (de Vidrio)", + "glass": "Vidrio", + "environmental_noise": "Ruido Ambiental", + "sound_effect": "Efecto de sonido", + "shatter": "Romperse", + "static": "Estatico", + "silence": "Silencio", + "scream": "Grito", + "white_noise": "Ruido Blanco", + "drill": "Taladro", + "field_recording": "Grabación de Campo", + "explosion": "Explosión", + "machine_gun": "Ametralladora", + "television": "Televisión", + "radio": "Radio", + "gunshot": "Disparo", + "fusillade": "Descarga de Fusilería", + "pink_noise": "Ruido Rosa", + "shofar": "Shofar", + "liquid": "Líquido", + "splash": "Chapoteo", + "slosh": "líquido_en_movimiento", + "squish": "Chapotear", + "drip": "Goteo", + "pour": "Derramar", + "trickle": "Chorrito", + "gush": "Chorro", + "fill": "Llenar", + "spray": "Pulverizar", + "pump": "Bombear", + "stir": "Remover", + "boiling": "Hirviendo", + "sonar": "Sonar", + "arrow": "Flecha", + "whoosh": "Zas", + "thump": "Golpear", + "thunk": "Golpe_sordo", + "electronic_tuner": "Afinador_electrónico", + "effects_unit": "Unidades de efecto", + "chorus_effect": "Efecto Coral", + "basketball_bounce": "Bote baloncesto", + "bang": "Bang", + "slap": "Bofeteada", + "whack": "Aporreo", + "smash": "Aplastar", + "breaking": "Romper", + "bouncing": "Botar", + "whip": "Latigazo", + "flap": "Aleteo", + "scratch": "Arañazo", + "scrape": "Arañar", + "rub": "Frotar", + "roll": "Roll", + "crushing": "aplastar", + "crumpling": "Arrugar", + "tearing": "Rasgar", + "beep": "Bip", + "ping": "Ping", + "ding": "Ding", + "clang": "Sonido metálico", + "squeal": "Chillido", + "creak": "Crujido", + "rustle": "Crujir", + "whir": "Zumbido de ventilador", + "clatter": "Estrépito", + "sizzle": "Chisporroteo", + "clicking": "Click", + "clickety_clack": "Clic-clac", + "rumble": "Retumbar", + "plop": "Plaf", + "hum": "Murmullo", + "zing": "silbido", + "boing": "Bote", + "crunch": "Crujido", + "sine_wave": "Onda Sinusoidal", + "harmonic": "Harmonica", + "chirp_tone": "Tono de chirrido", + "pulse": "Pulso", + "inside": "Dentro", + "outside": "Afuera", + "reverberation": "Reverberación", + "echo": "Eco", + "noise": "Ruido", + "mains_hum": "Zumbido de red", + "distortion": "Distorsión", + "sidetone": "Tono lateral", + "cacophony": "Cacofonía", + "throbbing": "Palpitación", + "vibration": "Vibración", + "sodeling": "Sodeling", + "chird": "Chird", + "change_ringing": "Cambio timbre" +} diff --git a/web/public/locales/es/common.json b/web/public/locales/es/common.json new file mode 100644 index 0000000..a1957c0 --- /dev/null +++ b/web/public/locales/es/common.json @@ -0,0 +1,341 @@ +{ + "time": { + "yesterday": "Ayer", + "thisMonth": "Este mes", + "yr": "{{time}}año", + "formattedTimestampWithYear": { + "12hour": "%b %-d %Y, %I:%M %p", + "24hour": "%b %-d %Y, %H:%M" + }, + "second_one": "{{time}} segundo", + "second_many": "{{time}} segundos", + "second_other": "{{time}} segundos", + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "formattedTimestampExcludeSeconds": { + "24hour": "%b %-d, %H:%M", + "12hour": "%b %-d, %I:%M %p" + }, + "formattedTimestamp": { + "24hour": "MMM d, HH:mm:ss", + "12hour": "MMM d, h:mm:ss aaa" + }, + "day_one": "{{time}} día", + "day_many": "{{time}} días", + "day_other": "{{time}} días", + "untilForTime": "Hasta {{time}}", + "untilForRestart": "Hasta que Frigate se reinicie.", + "untilRestart": "Hasta que se reinicie", + "ago": "Hace {{timeAgo}}", + "justNow": "Ahora mismo", + "today": "Hoy", + "last7": "Últimos 7 días", + "last14": "Últimos 14 días", + "last30": "Últimos 30 días", + "thisWeek": "Esta semana", + "lastWeek": "Semana pasada", + "lastMonth": "Mes pasado", + "10minutes": "10 minutos", + "30minutes": "30 minutos", + "1hour": "1 hora", + "12hours": "12 horas", + "24hours": "24 horas", + "pm": "pm", + "year_one": "{{time}} año", + "year_many": "{{time}} años", + "year_other": "{{time}} años", + "mo": "{{time}}mes", + "month_one": "{{time}} mes", + "month_many": "{{time}} meses", + "month_other": "{{time}} meses", + "h": "{{time}}h", + "m": "{{time}}m", + "minute_one": "{{time}} minuto", + "minute_many": "{{time}} minutos", + "minute_other": "{{time}} minutos", + "s": "{{time}}s", + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "5minutes": "5 minutos", + "am": "am", + "d": "{{time}}d", + "hour_one": "{{time}} hora", + "hour_many": "{{time}} horas", + "hour_other": "{{time}} horas", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "inProgress": "En progreso", + "invalidStartTime": "Hora de inicio no válida", + "invalidEndTime": "Hora de finalización no válida", + "never": "Nunca" + }, + "menu": { + "settings": "Ajustes", + "export": "Exportar", + "user": { + "title": "Usuario", + "account": "Cuenta", + "anonymous": "Anónimo", + "logout": "Cerrar sesión", + "setPassword": "Establecer contraseña", + "current": "Usuario actual: {{user}}" + }, + "systemMetrics": "Métricas del sistema", + "help": "Ayuda", + "system": "Sistema", + "configuration": "Configuración", + "systemLogs": "Registros del sistema", + "configurationEditor": "Editor de configuración", + "languages": "Idiomas", + "language": { + "en": "English (Inglés)", + "zhCN": "简体中文 (Chino simplificado)", + "withSystem": { + "label": "Usar los ajustes del sistema para el idioma" + }, + "ru": "Русский (Ruso)", + "de": "Deutsch (Alemán)", + "ja": "日本語 (Japonés)", + "tr": "Türkçe (Turco)", + "sv": "Svenska (Sueco)", + "nb": "Norsk Bokmål (Noruego Bokmål)", + "ko": "한국어 (Coreano)", + "vi": "Tiếng Việt (Vietnamita)", + "fa": "فارسی (Persa)", + "pl": "Polski (Polaco)", + "uk": "Українська (Ucraniano)", + "he": "עברית (Hebreo)", + "el": "Ελληνικά (Griego)", + "ro": "Română (Rumano)", + "hu": "Magyar (Húngaro)", + "fi": "Suomi (Finlandés)", + "it": "Italian (Italiano)", + "da": "Dansk (Danés)", + "sk": "Slovenčina (Eslovaco)", + "hi": "हिन्दी (Hindi)", + "es": "Español", + "ar": "العربية (Árabe)", + "pt": "Português (Portugues)", + "cs": "Čeština (Checo)", + "nl": "Nederlands (Neerlandés)", + "fr": "Français (Frances)", + "yue": "粵語 (Cantonés)", + "th": "ไทย (Tailandés)", + "ca": "Català (Catalan)", + "ptBR": "Português brasileiro (Portugués brasileño)", + "sr": "Српски (Serbio)", + "sl": "Slovenščina (Esloveno)", + "lt": "Lietuvių (Lituano)", + "bg": "Български (Búlgaro)", + "gl": "Galego (Gallego)", + "id": "Bahasa Indonesia (Indonesio)", + "ur": "اردو (Urdu)", + "hr": "Hrvatski (Croata)", + "bs": "Bosanski (Bosnio)", + "zhHant": "繁體中文 (Chino Tradicional)" + }, + "appearance": "Apariencia", + "darkMode": { + "label": "Modo oscuro", + "light": "Claro", + "dark": "Oscuro", + "withSystem": { + "label": "Usar los ajustes del sistema para el modo claro u oscuro" + } + }, + "withSystem": "Sistema", + "theme": { + "label": "Tema", + "blue": "Azul", + "green": "Verde", + "nord": "Nord", + "red": "Rojo", + "contrast": "Alto contraste", + "default": "Predeterminado", + "highcontrast": "Alto Contraste" + }, + "documentation": { + "title": "Documentación", + "label": "Documentación de Frigate" + }, + "restart": "Reiniciar Frigate", + "live": { + "title": "Directo", + "cameras": { + "title": "Cámaras", + "count_one": "{{count}} Cámara", + "count_many": "{{count}} Cámaras", + "count_other": "{{count}} Cámaras" + }, + "allCameras": "Todas las cámaras" + }, + "review": "Revisar", + "explore": "Explorar", + "uiPlayground": "Zona de pruebas de la interfaz de usuario", + "faceLibrary": "Biblioteca de rostros", + "classification": "Clasificación", + "profiles": "Perfiles", + "actions": "Acciones", + "features": "Funciones", + "chat": "Chat" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "kph" + }, + "length": { + "meters": "Metros", + "feet": "Pies" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hora", + "mbph": "MB/hora", + "gbph": "GB/hora" + } + }, + "button": { + "off": "APAGADO", + "copyCoordinates": "Copiar coordenadas", + "fullscreen": "Pantalla completa", + "apply": "Aplicar", + "reset": "Restablecer", + "done": "Hecho", + "enable": "Habilitar", + "disabled": "Deshabilitado", + "disable": "Deshabilitar", + "save": "Guardar", + "cancel": "Cancelar", + "close": "Cerrar", + "copy": "Copiar", + "back": "Atrás", + "history": "Historial", + "pictureInPicture": "Imagen en imagen", + "twoWayTalk": "Conversación bidireccional", + "cameraAudio": "Audio de la cámara", + "delete": "Eliminar", + "yes": "Sí", + "no": "No", + "download": "Descargar", + "info": "Información", + "suspended": "Suspendido", + "unsuspended": "Reactivar", + "play": "Reproducir", + "unselect": "Deseleccionar", + "export": "Exportar", + "deleteNow": "Eliminar ahora", + "next": "Siguiente", + "edit": "Editar", + "enabled": "Habilitado", + "saving": "Guardando…", + "exitFullscreen": "Salir de pantalla completa", + "on": "ENCENDIDO", + "continue": "Continuar", + "add": "Añadir", + "applying": "Aplicando…", + "undo": "Deshacer", + "copiedToClipboard": "Copiado al portapapeles", + "modified": "Modificado", + "overridden": "Sobrescrito", + "resetToGlobal": "Restablecer a global", + "resetToDefault": "Restablecer valores predeterminados", + "saveAll": "Guardar todo", + "savingAll": "Guardando todo…", + "undoAll": "Deshacer todo", + "retry": "Reintentar" + }, + "toast": { + "save": { + "error": { + "noMessage": "No se pudieron guardar los cambios de configuración", + "title": "No se pudieron guardar los cambios de configuración: {{errorMessage}}" + }, + "title": "Guardar", + "success": "Cambios de configuración guardados correctamente." + }, + "copyUrlToClipboard": "URL copiada al portapapeles." + }, + "label": { + "back": "Volver atrás", + "hide": "Ocultar {{item}}", + "show": "Mostrar {{item}}", + "ID": "ID", + "none": "Ninguno", + "all": "Todas", + "other": "Otro" + }, + "role": { + "title": "Rol", + "admin": "Administrador", + "viewer": "Espectador", + "desc": "Los administradores tienen acceso completo a todas las funciones en la interfaz de usuario de Frigate. Los espectadores están limitados a ver cámaras, elementos de revisión y grabaciones históricas en la interfaz de usuario." + }, + "pagination": { + "label": "paginación", + "previous": { + "title": "Anterior", + "label": "Ir a la página anterior" + }, + "next": { + "title": "Siguiente", + "label": "Ir a la página siguiente" + }, + "more": "Más páginas" + }, + "accessDenied": { + "documentTitle": "Acceso denegado - Frigate", + "desc": "No tienes permiso para ver esta página.", + "title": "Acceso denegado" + }, + "notFound": { + "documentTitle": "No se ha encontrado - Frigate", + "title": "404", + "desc": "Página no encontrada" + }, + "selectItem": "Seleccionar {{item}}", + "readTheDocumentation": "Leer la documentación", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} y {{1}}", + "many": "{{items}}, y {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opcional", + "internalID": "La ID interna que usa Frigate en la configuración y en la base de datos" + }, + "no_items": "No hay elementos", + "validation_errors": "Errores de validación", + "credentialField": { + "savedPlaceholder": "Guardado — déjalo en blanco para mantener el actual" + } +} diff --git a/web/public/locales/es/components/auth.json b/web/public/locales/es/components/auth.json new file mode 100644 index 0000000..62d6c84 --- /dev/null +++ b/web/public/locales/es/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nombre de usuario", + "errors": { + "passwordRequired": "Se requiere contraseña", + "rateLimit": "Se ha superado el límite de solicitudes. Intenta de nuevo más tarde.", + "webUnknownError": "Error desconocido. Revisa los registros de la consola.", + "usernameRequired": "Se requiere nombre de usuario", + "unknownError": "Error desconocido. Revisa los registros.", + "loginFailed": "Error de inicio de sesión" + }, + "password": "Contraseña", + "login": "Iniciar sesión", + "firstTimeLogin": "¿Estás tratando de iniciar sesión por primera vez? Las credenciales están impresas en los registros de Frigate." + } +} diff --git a/web/public/locales/es/components/camera.json b/web/public/locales/es/components/camera.json new file mode 100644 index 0000000..1ce25fa --- /dev/null +++ b/web/public/locales/es/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "Grupos de cámaras", + "add": "Agregar grupo de cámaras", + "edit": "Editar grupo de cámaras", + "delete": { + "label": "Eliminar grupo de cámaras", + "confirm": { + "title": "Confirmar eliminación", + "desc": "¿Estás seguro de que quieres eliminar el grupo de cámaras {{name}}?" + } + }, + "name": { + "label": "Nombre", + "placeholder": "Introduce un nombre…", + "errorMessage": { + "mustLeastCharacters": "El nombre del grupo de cámaras debe tener al menos 2 caracteres.", + "nameMustNotPeriod": "El nombre del grupo de cámaras no debe contener un punto.", + "invalid": "Nombre de grupo de cámaras no válido.", + "exists": "El nombre del grupo de cámaras ya existe." + } + }, + "cameras": { + "desc": "Selecciona cámaras para este grupo.", + "label": "Cámaras" + }, + "icon": "Icono", + "success": "El grupo de cámaras ({{name}}) ha sido guardado.", + "camera": { + "setting": { + "title": "Ajustes de transmisión de {{cameraName}}", + "audioIsAvailable": "El audio está disponible para esta transmisión", + "audioIsUnavailable": "El audio no está disponible para esta transmisión", + "audio": { + "tips": { + "title": "El audio debe provenir de tu cámara y estar configurado en go2rtc para esta transmisión.", + "document": "Leer la documentación " + } + }, + "streamMethod": { + "method": { + "noStreaming": { + "desc": "Las imágenes de la cámara solo se actualizarán una vez por minuto y no habrá transmisión en vivo.", + "label": "Sin transmisión" + }, + "smartStreaming": { + "label": "Transmisión inteligente (recomendada)", + "desc": "La transmisión inteligente actualizará la imagen de tu cámara una vez por minuto cuando no se detecte actividad para conservar ancho de banda y recursos. Cuando se detecte actividad, la imagen cambiará sin problemas a una transmisión en vivo." + }, + "continuousStreaming": { + "label": "Transmisión continua", + "desc": { + "title": "La imagen de la cámara siempre será una transmisión en vivo cuando esté visible en el panel de control, incluso si no se detecta ninguna actividad.", + "warning": "La transmisión continua puede causar un alto uso de ancho de banda y problemas de rendimiento. Usa con precaución." + } + } + }, + "label": "Método de transmisión", + "placeholder": "Elige un método de transmisión" + }, + "compatibilityMode": { + "label": "Modo de compatibilidad", + "desc": "Habilita esta opción solo si la transmisión en vivo de tu cámara muestra artefactos de color y tiene una línea diagonal en el lado derecho de la imagen." + }, + "label": "Ajustes de transmisión de la cámara", + "desc": "Cambia las opciones de transmisión en vivo para el panel de control de este grupo de cámaras. Estos ajustes son específicos del dispositivo/navegador.", + "placeholder": "Elige una transmisión", + "stream": "Transmitir" + }, + "birdseye": "Vista Aérea" + }, + "showAll": "Mostrar todos los grupos de cámaras", + "showLess": "Mostrar menos", + "editGroups": "Editar grupos de cámaras" + }, + "debug": { + "options": { + "label": "Ajustes", + "title": "Opciones", + "showOptions": "Mostrar opciones", + "hideOptions": "Ocultar opciones" + }, + "timestamp": "Marca de tiempo", + "zones": "Zonas", + "motion": "Movimiento", + "regions": "Regiones", + "boundingBox": "Caja delimitadora", + "mask": "Máscara", + "paths": "Trayectorias" + } +} diff --git a/web/public/locales/es/components/dialog.json b/web/public/locales/es/components/dialog.json new file mode 100644 index 0000000..d86eb50 --- /dev/null +++ b/web/public/locales/es/components/dialog.json @@ -0,0 +1,217 @@ +{ + "restart": { + "restarting": { + "title": "Frigate se está reiniciando", + "button": "Forzar recarga ahora", + "content": "Esta página se recargará en {{countdown}} segundos." + }, + "title": "¿Estás seguro de que quieres reiniciar Frigate?", + "button": "Reiniciar", + "description": "Esto detendrá brevemente Frigate mientras se reinicia." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Enviar a Frigate+", + "desc": "Los objetos en ubicaciones que deseas evitar no son falsos positivos. Enviarlos como falsos positivos confundirá al modelo." + }, + "review": { + "false": { + "label": "No confirmar esta etiqueta para Frigate Plus", + "false_one": "Esto no es un {{label}}", + "false_many": "Esto no es un {{label}}", + "false_other": "Esto no es un {{label}}" + }, + "true": { + "true_one": "Esto es un {{label}}", + "true_many": "Esto es un {{label}}", + "true_other": "Esto es un {{label}}", + "label": "Confirmar esta etiqueta para Frigate+" + }, + "state": { + "submitted": "Enviado" + }, + "question": { + "label": "Confirmar esta etiqueta para Frigate Plus", + "ask_a": "¿Es este objeto un {{label}}?", + "ask_an": "¿Es este objeto un {{label}}?", + "ask_full": "¿Es este objeto un {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Ver en el historial" + } + }, + "export": { + "time": { + "fromTimeline": "Seleccionar desde la línea de tiempo", + "lastHour_one": "Última hora", + "lastHour_many": "Últimas {{count}} horas", + "lastHour_other": "Últimas {{count}} horas", + "custom": "Personalizado", + "start": { + "title": "Hora de inicio", + "label": "Seleccionar hora de inicio" + }, + "end": { + "title": "Hora de finalización", + "label": "Seleccionar hora de finalización" + } + }, + "name": { + "placeholder": "Nombrar la exportación" + }, + "select": "Seleccionar", + "export": "Exportar", + "toast": { + "error": { + "failed": "No se pudo iniciar la exportación: {{error}}", + "noVaildTimeSelected": "No se seleccionó un rango de tiempo válido", + "endTimeMustAfterStartTime": "La hora de finalización debe ser posterior a la hora de inicio" + }, + "success": "Exportación iniciada con éxito. Ver el archivo en la página exportaciones.", + "view": "Vista", + "queued": "Exportación en cola. Consulta el progreso en la página de exportaciones.", + "batchSuccess_one": "Se inició 1 exportación. Abriendo el caso ahora.", + "batchSuccess_many": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.", + "batchSuccess_other": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.", + "batchPartial": "Se iniciaron {{successful}} de {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}", + "batchFailed": "No se pudieron iniciar {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}", + "batchQueuedSuccess_one": "1 exportación en cola. Abriendo el caso ahora.", + "batchQueuedSuccess_many": "{{count}} exportaciones en cola. Abriendo el caso ahora.", + "batchQueuedSuccess_other": "{{count}} exportaciones en cola. Abriendo el caso ahora.", + "batchQueuedPartial": "{{successful}} de {{total}} exportaciones en cola. Cámaras fallidas: {{failedCameras}}", + "batchQueueFailed": "No se pudieron poner en cola {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "Guardar exportación", + "previewExport": "Vista previa de la exportación", + "queueingExport": "Poniendo exportación en cola...", + "useThisRange": "Usar este intervalo" + }, + "selectOrExport": "Seleccionar o exportar", + "case": { + "label": "Caso", + "newCaseDescriptionPlaceholder": "Descripción de caso", + "newCaseOption": "Crear nuevo caso", + "newCaseNamePlaceholder": "Nombre del nuevo caso", + "nonAdminHelp": "Se creará un nuevo caso para estas exportaciones.", + "placeholder": "Selecciona un caso" + }, + "queueing": "Poniendo la exportación en cola…", + "tabs": { + "export": "Una Cámara", + "multiCamera": "Multicámara" + }, + "multiCamera": { + "timeRange": "Intervalo de tiempo", + "selectFromTimeline": "Seleccionar desde la línea de tiempo", + "cameraSelection": "Cámaras", + "cameraSelectionHelp": "Las cámaras con objetos detectados en este intervalo de tiempo están preseleccionadas", + "checkingActivity": "Comprobando actividad de las cámaras...", + "noCameras": "No hay cámaras disponibles", + "detectionCount_one": "1 objeto detectado", + "detectionCount_many": "{{count}} objetos detectados", + "detectionCount_other": "{{count}} objetos detectados", + "nameLabel": "Nombre de la exportación", + "namePlaceholder": "Nombre base opcional para estas exportaciones", + "queueingButton": "Poniendo exportaciones en cola...", + "exportButton_one": "Exportar 1 cámara", + "exportButton_many": "Exportar {{count}} cámaras", + "exportButton_other": "Exportar {{count}} cámaras", + "searchOrSelectGroup": "Busca o selecciona un grupo de cámaras…", + "selectAll": "Seleccionar todas las cámaras", + "clearSelection": "Borrar selección", + "selectWithActivity": "Cámaras con objetos rastreados", + "selectGroup": "Seleccionar grupo", + "noMatchingCameras": "Ninguna cámara coincide con tu búsqueda", + "selectedCount": "{{selected}} / {{total}} seleccionadas" + }, + "multi": { + "title_one": "Exportar 1 revisión", + "title_many": "Exportar {{count}} revisiones", + "title_other": "Exportar {{count}} revisiones", + "description": "Exportar cada revisión seleccionada. Todas las exportaciones se agruparán en un único caso.", + "descriptionNoCase": "Exportar cada revisión seleccionada.", + "caseNamePlaceholder": "Exportación de revisión - {{date}}", + "exportButton_one": "Exportar 1 revisión", + "exportButton_many": "Exportar {{count}} revisiones", + "exportButton_other": "Exportar {{count}} revisiones", + "exportingButton": "Exportando...", + "toast": { + "started_one": "Se inició 1 exportación. Abriendo el caso ahora.", + "started_many": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.", + "started_other": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.", + "startedNoCase_one": "Se inició 1 exportación.", + "startedNoCase_many": "Se iniciaron {{count}} exportaciones.", + "startedNoCase_other": "Se iniciaron {{count}} exportaciones.", + "partial": "Se iniciaron {{successful}} de {{total}} exportaciones. Fallidas: {{failedItems}}", + "failed": "No se pudieron iniciar {{total}} exportaciones. Fallidas: {{failedItems}}" + } + } + }, + "streaming": { + "restreaming": { + "disabled": "La retransmisión no está habilitada para esta cámara.", + "desc": { + "title": "Configura go2rtc para opciones adicionales de vista en vivo y audio para esta cámara.", + "readTheDocumentation": "Leer la documentación" + } + }, + "debugView": "Vista de depuración", + "label": "Transmisión", + "showStats": { + "label": "Mostrar estadísticas de transmisión", + "desc": "Habilita esta opción para mostrar las estadísticas de transmisión como una superposición en la imagen de la cámara." + } + }, + "search": { + "saveSearch": { + "label": "Guardar búsqueda", + "desc": "Proporciona un nombre para esta búsqueda guardada.", + "overwrite": "{{searchName}} ya existe. Guardar sobrescribirá el valor existente.", + "success": "La búsqueda ({{searchName}}) ha sido guardada.", + "button": { + "save": { + "label": "Guardar esta búsqueda" + } + }, + "placeholder": "Introduce un nombre para tu búsqueda" + } + }, + "recording": { + "confirmDelete": { + "title": "Confirmar eliminación", + "desc": { + "selected": "¿Estás seguro de que quieres eliminar todo el video grabado asociado con este elemento de revisión?

    Mantén presionada la tecla Shift para omitir este diálogo en el futuro." + }, + "toast": { + "success": "El metraje de video asociado con los elementos de revisión seleccionados se ha eliminado con éxito.", + "error": "No se pudo eliminar: {{error}}" + } + }, + "button": { + "export": "Exportar", + "markAsReviewed": "Marcar como revisado", + "deleteNow": "Eliminar ahora", + "markAsUnreviewed": "Marcar como no revisado" + }, + "shareTimestamp": { + "description": "Comparta una URL con marca de tiempo de la posición actual del reproductor o elija una marca de tiempo personalizada. Tenga en cuenta que esta no es una URL pública para compartir y solo es accesible para los usuarios que tienen acceso a Frigate y a esta cámara.", + "label": "Compartir marca de tiempo", + "title": "Compartir marca de tiempo", + "custom": "Marca de tiempo personalizada", + "button": "Compartir URL de la marca de tiempo", + "shareTitle": "Marca de tiempo de revisión de Frigate: {{camera}}" + } + }, + "imagePicker": { + "selectImage": "Seleccione la miniatura de un objeto rastreado", + "search": { + "placeholder": "Búsqueda por etiqueta o sub-etiqueta..." + }, + "noImages": "No se encontraron miniaturas para esta cámara", + "unknownLabel": "Imagen de activación guardada" + } +} diff --git a/web/public/locales/es/components/filter.json b/web/public/locales/es/components/filter.json new file mode 100644 index 0000000..1d9c078 --- /dev/null +++ b/web/public/locales/es/components/filter.json @@ -0,0 +1,141 @@ +{ + "filter": "Filtro", + "labels": { + "all": { + "short": "Etiquetas", + "title": "Todas las etiquetas" + }, + "count": "{{count}} Etiquetas", + "label": "Etiquetas", + "count_one": "{{count}} Etiqueta", + "count_other": "{{count}} Etiquetas" + }, + "zones": { + "all": { + "title": "Todas las zonas", + "short": "Zonas" + }, + "label": "Zonas" + }, + "dates": { + "all": { + "title": "Todas las fechas", + "short": "Fechas" + }, + "selectPreset": "Selecciona un Preajuste…" + }, + "timeRange": "Rango de tiempo", + "subLabels": { + "all": "Todas las subetiquetas", + "label": "Subetiquetas" + }, + "score": "Puntuación", + "estimatedSpeed": "Velocidad estimada ({{unit}})", + "features": { + "label": "Características", + "submittedToFrigatePlus": { + "label": "Enviado a Frigate+", + "tips": "Primero debes filtrar por objetos rastreados que tengan una captura de pantalla.

    Los objetos rastreados sin una captura de pantalla no pueden enviarse a Frigate+." + }, + "hasSnapshot": "Tiene una captura instantánea", + "hasVideoClip": "Tiene un clip de vídeo" + }, + "sort": { + "label": "Ordenar", + "dateAsc": "Fecha (Ascendente)", + "dateDesc": "Fecha (Descendente)", + "scoreAsc": "Puntuación del objeto (Ascendente)", + "scoreDesc": "Puntuación del objeto (Descendente)", + "speedAsc": "Velocidad estimada (Ascendente)", + "speedDesc": "Velocidad estimada (Descendente)", + "relevance": "Relevancia" + }, + "cameras": { + "label": "Filtro de cámaras", + "all": { + "title": "Todas las cámaras", + "short": "Cámaras" + } + }, + "review": { + "showReviewed": "Mostrar revisados" + }, + "motion": { + "showMotionOnly": "Mostrar solo movimiento" + }, + "explore": { + "settings": { + "title": "Configuración", + "defaultView": { + "title": "Vista predeterminada", + "summary": "Resumen", + "desc": "Cuando no se seleccionen filtros, muestra un resumen de los objetos rastreados más recientes por etiqueta, o muestra una cuadrícula sin filtrar.", + "unfilteredGrid": "Cuadrícula sin filtrar" + }, + "searchSource": { + "label": "Fuente de búsqueda", + "options": { + "thumbnailImage": "Miniatura Imagen", + "description": "Descripción" + }, + "desc": "Elige si deseas buscar en las miniaturas o en las descripciones de tus objetos rastreados." + }, + "gridColumns": { + "title": "Columnas de la cuadrícula", + "desc": "Selecciona el número de columnas en la vista de cuadrícula." + } + }, + "date": { + "selectDateBy": { + "label": "Selecciona una fecha para filtrar" + } + } + }, + "reset": { + "label": "Restablecer filtros a los valores predeterminados" + }, + "more": "Más filtros", + "logSettings": { + "label": "Filtrar nivel de registro", + "loading": { + "title": "Cargando", + "desc": "Cuando el panel de registros está desplazado hasta el final, los nuevos registros se transmiten automáticamente a medida que se añaden." + }, + "disableLogStreaming": "Deshabilitar transmisión de registros", + "filterBySeverity": "Filtrar registros por gravedad", + "allLogs": "Todos los registros" + }, + "trackedObjectDelete": { + "title": "Confirmar eliminación", + "desc": "Eliminar estos {{objectLength}} objetos rastreados elimina la captura de pantalla, cualquier incrustación guardada y cualquier entrada asociada al ciclo de vida del objeto. Las grabaciones de estos objetos rastreados en la vista de Historial NO se eliminarán.

    ¿Estás seguro de que quieres proceder?

    Mantén presionada la tecla Shift para omitir este diálogo en el futuro.", + "toast": { + "success": "Objetos rastreados eliminados con éxito.", + "error": "No se pudo eliminar los objetos rastreados: {{errorMessage}}" + } + }, + "recognizedLicensePlates": { + "title": "Matrículas reconocidas", + "loadFailed": "No se pudieron cargar las matrículas reconocidas.", + "loading": "Cargando matrículas reconocidas…", + "placeholder": "Escribe para buscar matrículas…", + "noLicensePlatesFound": "No se encontraron matrículas.", + "selectPlatesFromList": "Selecciona una o más matrículas de la lista.", + "selectAll": "Seleccionar todas", + "clearAll": "Limpiar todas" + }, + "zoneMask": { + "filterBy": "Filtrar por máscara de zona" + }, + "classes": { + "label": "Clases", + "all": { + "title": "Todas las clases" + }, + "count_one": "{{count}} Clase", + "count_other": "{{count}} Clases" + }, + "attributes": { + "label": "Atributos de clasificación", + "all": "Todos los Atributos" + } +} diff --git a/web/public/locales/es/components/icons.json b/web/public/locales/es/components/icons.json new file mode 100644 index 0000000..d4112ae --- /dev/null +++ b/web/public/locales/es/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecciona un icono", + "search": { + "placeholder": "Buscar un icono…" + } + } +} diff --git a/web/public/locales/es/components/input.json b/web/public/locales/es/components/input.json new file mode 100644 index 0000000..986f0c4 --- /dev/null +++ b/web/public/locales/es/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Descargar vídeo", + "toast": { + "success": "El vídeo de tu elemento de revisión ha comenzado a descargarse." + } + } + } +} diff --git a/web/public/locales/es/components/player.json b/web/public/locales/es/components/player.json new file mode 100644 index 0000000..e650046 --- /dev/null +++ b/web/public/locales/es/components/player.json @@ -0,0 +1,53 @@ +{ + "noPreviewFound": "No se encontró vista previa", + "noPreviewFoundFor": "No se encontró vista previa para {{cameraName}}", + "submitFrigatePlus": { + "submit": "Enviar", + "title": "¿Enviar este fotograma a Frigate+?", + "previewError": "No se pudo cargar la vista previa de la instantánea. Es posible que la grabación no esté disponible en este momento." + }, + "streamOffline": { + "desc": "No se han recibido fotogramas en la transmisión detect de {{cameraName}}, revisa los registros de errores", + "title": "Transmisión desconectada" + }, + "cameraDisabled": "La cámara está deshabilitada", + "stats": { + "streamType": { + "title": "Tipo de transmisión:", + "short": "Tipo" + }, + "bandwidth": { + "title": "Ancho de banda:", + "short": "Ancho de banda" + }, + "latency": { + "title": "Latencia:", + "short": { + "title": "Latencia", + "value": "{{seconds}} seg" + }, + "value": "{{seconds}} segundos" + }, + "totalFrames": "Fotogramas totales:", + "droppedFrames": { + "title": "Fotogramas perdidos:", + "short": { + "title": "Perdidos", + "value": "{{droppedFrames}} fotogramas" + } + }, + "decodedFrames": "Fotogramas decodificados:", + "droppedFrameRate": "Tasa de fotogramas perdidos:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Fotograma enviado correctamente a Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Error al enviar el fotograma a Frigate+" + } + }, + "livePlayerRequiredIOSVersion": "Se requiere iOS 17.1 o superior para este tipo de transmisión en vivo.", + "noRecordingsFoundForThisTime": "No se encontraron grabaciones para este momento", + "cameraOff": "La cámara está apagada" +} diff --git a/web/public/locales/es/config/cameras.json b/web/public/locales/es/config/cameras.json new file mode 100644 index 0000000..95e412f --- /dev/null +++ b/web/public/locales/es/config/cameras.json @@ -0,0 +1,957 @@ +{ + "name": { + "label": "Nombre de Cámara", + "description": "El nombre de la cámara es necesario" + }, + "enabled": { + "label": "Habilitado", + "description": "Habilitado" + }, + "audio": { + "label": "Detección 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", + "description": "Umbral mínimo de volumen RMS requerido para ejecutar la detección de audio; los valores más bajos aumentan la sensibilidad (p. ej., 200 alta, 500 media, 1000 baja)." + }, + "listen": { + "label": "Tipos de escucha", + "description": "Lista de tipos de eventos de audio a detectar (por ejemplo: ladrar, alarma de incendio, habla, gritar)." + }, + "filters": { + "label": "Filtros de audio", + "description": "Ajustes de filtro por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos.", + "threshold": { + "label": "Confianza mínima de audio", + "description": "Umbral mínimo de confianza para que se cuente el evento de audio." + } + }, + "enabled_in_config": { + "description": "Indica si la detección de audio estaba habilitada originalmente en el archivo de configuración estático.", + "label": "Estado de audio original" + }, + "num_threads": { + "label": "Hilos de detección", + "description": "Número de hilos a usar para procesamiento de detección de audio." + } + }, + "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", + "onvif": { + "profile": { + "label": "Perfil ONVIF", + "description": "Perfil multimedia ONVIF específico que se utilizará para el control PTZ, identificado por token o nombre. Si no se especifica, se selecciona automáticamente el primer perfil con una configuración PTZ válida." + }, + "autotracking": { + "zoom_factor": { + "description": "Controla el nivel de zoom en los objetos rastreados. Los valores más bajos mantienen una mayor parte de la escena a la vista; los valores más altos acercan la imagen, pero pueden provocar la pérdida del rastreo. Valores entre 0.1 y 0.75.", + "label": "Factor de zoom" + }, + "calibrate_on_startup": { + "description": "Mida la velocidad de los motores PTZ al encenderlos para mejorar la precisión del seguimiento. Frigate actualizará la configuración con los `movement_weights` tras la calibración.", + "label": "Calibrar al iniciar" + }, + "description": "Realice un seguimiento automático de objetos en movimiento y manténgalos centrados en el encuadre mediante movimientos de cámara PTZ.", + "zooming": { + "description": "Control del comportamiento del zoom: deshabilitado (solo panorámica/inclinación), absoluto (mayor compatibilidad) o relativo (panorámica/inclinación/zoom simultáneos).", + "label": "Modo de zoom" + }, + "return_preset": { + "description": "Nombre del preajuste ONVIF configurado en el firmware de la cámara al que regresar una vez finalizado el seguimiento.", + "label": "Preajuste de retorno" + }, + "timeout": { + "description": "Espere esta cantidad de segundos después de perder el seguimiento antes de devolver la cámara a la posición preestablecida.", + "label": "Tiempo de espera de retorno" + }, + "label": "Seguimiento automático", + "enabled": { + "label": "Habilitar seguimiento automático", + "description": "Habilita o deshabilita el seguimiento automático con cámara PTZ de objetos detectados." + }, + "track": { + "label": "Objetos rastreados", + "description": "Lista de tipos de objetos que deben activar el seguimiento automático." + }, + "required_zones": { + "label": "Zonas requeridas", + "description": "Los objetos deben entrar en una de estas zonas antes de que comience el seguimiento automático." + }, + "movement_weights": { + "label": "Pesos de movimiento", + "description": "Valores de calibración generados automáticamente por la calibración de la cámara. No los modifiques manualmente." + }, + "enabled_in_config": { + "label": "Estado original de autoseguimiento", + "description": "Campo interno para rastrear si el seguimiento automático estaba habilitado en la configuración." + } + }, + "tls_insecure": { + "description": "Omitir la verificación TLS y deshabilitar la autenticación digest para ONVIF (no seguro; usar solo en redes seguras).", + "label": "Deshabilitar verificación TLS" + }, + "label": "ONVIF", + "description": "Ajustes de conexión ONVIF y seguimiento automático PTZ para esta cámara.", + "host": { + "label": "Host ONVIF", + "description": "Host (y esquema opcional) para el servicio ONVIF de esta cámara." + }, + "port": { + "label": "Puerto ONVIF", + "description": "Número de puerto del servicio ONVIF." + }, + "user": { + "label": "Nombre de usuario ONVIF", + "description": "Nombre de usuario para la autenticación ONVIF; algunos dispositivos requieren un usuario administrador para ONVIF." + }, + "password": { + "label": "Contraseña ONVIF", + "description": "Contraseña para la autenticación ONVIF." + }, + "ignore_time_mismatch": { + "label": "Ignorar discrepancia horaria", + "description": "Ignora las diferencias de sincronización horaria entre la cámara y el servidor Frigate para la comunicación ONVIF." + } + }, + "zones": { + "distances": { + "label": "Distancias reales", + "description": "Distancias reales opcionales para cada lado del cuadrilátero de la zona, usadas para cálculos de velocidad o distancia. Debe tener exactamente 4 valores si se establece." + }, + "coordinates": { + "description": "Coordenadas del polígono que definen el área de la zona. Puede ser una cadena separada por comas o una lista de cadenas de coordenadas. Las coordenadas deben ser relativas (0-1) o absolutas (heredadas).", + "label": "Coordenadas" + }, + "filters": { + "raw_mask": { + "label": "Máscara en bruto" + }, + "mask": { + "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma.", + "label": "Máscara de filtro" + }, + "min_score": { + "description": "Confianza mínima en un solo fotograma requerida para que el objeto sea contabilizado.", + "label": "Confianza mínima" + }, + "threshold": { + "description": "Umbral de confianza promedio requerido para que el objeto sea considerado un positivo real.", + "label": "Umbral de confianza" + }, + "max_ratio": { + "description": "Relación máxima de ancho/alto permitida para que el cuadro delimitador califique.", + "label": "Relación de aspecto máxima" + }, + "min_ratio": { + "description": "Relación mínima de ancho/alto requerida para que el cuadro delimitador califique.", + "label": "Relación de aspecto mínima" + }, + "max_area": { + "description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede expresarse en píxeles (entero) o como porcentaje (decimal entre 0,000001 y 0,99).", + "label": "Área máxima del objeto" + }, + "description": "Filtros para aplicar a los objetos dentro de esta zona. Se utilizan para reducir los falsos positivos o restringir qué objetos se consideran presentes en la zona.", + "label": "Filtros de zona", + "min_area": { + "label": "Área mínima de objeto", + "description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + } + }, + "objects": { + "description": "Lista de tipos de objetos (del mapa de etiquetas) que pueden activar esta zona. Puede ser una cadena de texto o una lista de cadenas. Si está vacío, se consideran todos los objetos.", + "label": "Objetos activadores" + }, + "description": "Las zonas le permiten definir un área específica del fotograma, de modo que pueda determinar si un objeto se encuentra o no dentro de un área determinada.", + "speed_threshold": { + "description": "Velocidad mínima (en unidades del mundo real, si se han configurado distancias) requerida para que un objeto se considere presente en la zona. Se utiliza para los disparadores de zona basados en la velocidad.", + "label": "Velocidad mínima" + }, + "friendly_name": { + "description": "Un nombre fácil de usar para la zona, que se muestra en la interfaz de usuario de Frigate. Si no se especifica, se utilizará una versión formateada del nombre de la zona.", + "label": "Nombre de zona" + }, + "inertia": { + "description": "Número de fotogramas consecutivos en los que se debe detectar un objeto dentro de la zona antes de considerarlo presente. Ayuda a filtrar las detecciones transitorias.", + "label": "Fotogramas de inercia" + }, + "loitering_time": { + "description": "Número de segundos que un objeto debe permanecer en la zona para ser considerado como merodeo. Establezca en 0 para desactivar la detección de merodeo.", + "label": "Segundos de permanencia" + }, + "label": "Zonas", + "enabled": { + "label": "Habilitado", + "description": "Habilita o deshabilita esta zona. Las zonas deshabilitadas se ignoran en tiempo de ejecución." + }, + "enabled_in_config": { + "label": "Mantiene el registro del estado original de la zona." + } + }, + "objects": { + "raw_mask": { + "label": "Máscara en bruto" + }, + "genai": { + "label": "Configuración de objetos GenAI", + "description": "Opciones de GenAI para describir objetos rastreados y enviar fotogramas para su generación.", + "enabled": { + "label": "Activar GenAI", + "description": "Activar por defecto la generación de descripciones de GenAI para los objetos rastreados." + }, + "use_snapshot": { + "label": "Usar instantáneas", + "description": "Usar instantáneas de objetos en lugar de miniaturas para la generación de descripciones de GenAI." + }, + "send_triggers": { + "after_significant_updates": { + "description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado.", + "label": "Activador temprano de GenAI" + }, + "description": "Define cuándo se deben enviar los fotogramas a GenAI (al finalizar, después de las actualizaciones, etc.).", + "label": "Activadores de GenAI", + "tracked_object_end": { + "label": "Enviar al finalizar", + "description": "Envía una solicitud a GenAI cuando finaliza el objeto rastreado." + } + }, + "required_zones": { + "description": "Zonas en las que deben ubicarse los objetos para ser elegibles para la generación de descripciones con GenAI.", + "label": "Zonas requeridas" + }, + "prompt": { + "label": "Prompt de descripción", + "description": "Plantilla de prompt predeterminada usada al generar descripciones con GenAI." + }, + "object_prompts": { + "label": "Prompts de objetos", + "description": "Prompts por objeto para personalizar las salidas de GenAI para etiquetas concretas." + }, + "objects": { + "label": "Objetos de GenAI", + "description": "Lista de etiquetas de objetos que se enviarán a GenAI de forma predeterminada." + }, + "debug_save_thumbnails": { + "label": "Guardar miniaturas", + "description": "Guarda las miniaturas enviadas a GenAI para depuración y revisión." + }, + "enabled_in_config": { + "label": "Estado original de GenAI", + "description": "Indica si GenAI estaba habilitado en la configuración estática original." + } + }, + "label": "Objetos", + "description": "Valores predeterminados de seguimiento de objetos, incluidas las etiquetas que se rastrean y los filtros por objeto.", + "track": { + "label": "Objetos a rastrear", + "description": "Lista de etiquetas de objetos a rastrear para esta cámara." + }, + "filters": { + "label": "Filtros de objetos", + "description": "Filtros aplicados a los objetos detectados para reducir falsos positivos (área, relación, confianza).", + "min_area": { + "label": "Área mínima de objeto", + "description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + }, + "max_area": { + "label": "Área máxima de objeto", + "description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + }, + "min_ratio": { + "label": "Relación de aspecto mínima", + "description": "Relación mínima anchura/altura necesaria para que el cuadro delimitador sea válido." + }, + "max_ratio": { + "label": "Relación de aspecto máxima", + "description": "Relación máxima anchura/altura permitida para que el cuadro delimitador sea válido." + }, + "threshold": { + "label": "Umbral de confianza", + "description": "Umbral medio de confianza de detección necesario para que el objeto se considere un positivo verdadero." + }, + "min_score": { + "label": "Confianza mínima", + "description": "Confianza mínima de detección en un único fotograma necesaria para que el objeto se contabilice." + }, + "mask": { + "label": "Máscara de filtro", + "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma." + }, + "raw_mask": { + "label": "Máscara sin procesar" + } + }, + "mask": { + "label": "Máscara de objeto", + "description": "Polígono de máscara usado para evitar la detección de objetos en áreas especificadas." + } + }, + "mqtt": { + "label": "MQTT", + "required_zones": { + "description": "Zonas en las que debe entrar un objeto para que se publique una imagen MQTT.", + "label": "Zonas requeridas" + }, + "description": "Ajustes de publicación de imágenes MQTT.", + "enabled": { + "label": "Enviar imagen", + "description": "Habilita la publicación de instantáneas de objetos en temas MQTT para esta cámara." + }, + "timestamp": { + "label": "Añadir marca de tiempo", + "description": "Superpone una marca de tiempo en las imágenes publicadas en MQTT." + }, + "bounding_box": { + "label": "Añadir cuadro delimitador", + "description": "Dibuja cuadros delimitadores en las imágenes publicadas mediante MQTT." + }, + "crop": { + "label": "Recortar imagen", + "description": "Recorta las imágenes publicadas en MQTT al cuadro delimitador del objeto detectado." + }, + "height": { + "label": "Altura de imagen", + "description": "Altura (píxeles) a la que redimensionar las imágenes publicadas mediante MQTT." + }, + "quality": { + "label": "Calidad JPEG", + "description": "Calidad JPEG de las imágenes publicadas en MQTT (0-100)." + } + }, + "notifications": { + "email": { + "label": "Email de notificacion", + "description": "Dirección de correo electrónico usada para notificaciones push o requerida por ciertos proveedores de notificaciones." + }, + "label": "Notificaciones", + "description": "Ajustes para habilitar y controlar las notificaciones de esta cámara.", + "enabled": { + "label": "Habilitar notificaciones", + "description": "Habilita o deshabilita las notificaciones para esta cámara." + }, + "cooldown": { + "label": "Periodo de enfriamiento", + "description": "Periodo de enfriamiento (segundos) entre notificaciones para evitar saturar a los destinatarios." + }, + "enabled_in_config": { + "label": "Estado original de notificaciones", + "description": "Indica si las notificaciones estaban habilitadas en la configuración estática original." + } + }, + "audio_transcription": { + "description": "Configuración para la transcripción de audio en directo y de voz utilizada para eventos y subtítulos en directo.", + "enabled": { + "label": "Habilitar transcripción", + "description": "Habilitar o deshabilitar la transcripción de eventos de audio activados manualmente." + }, + "label": "Transcripción de audio", + "enabled_in_config": { + "label": "Estado original de la transcripción" + }, + "live_enabled": { + "label": "Transcripción en directo", + "description": "Habilitar la transcripción en directo del audio a medida que se recibe." + } + }, + "motion": { + "skip_motion_threshold": { + "description": "Si se establece en un valor entre 0,0 y 1,0, y más de esta fracción de la imagen cambia en un solo fotograma, el detector no devolverá cuadros de movimiento y se recalibrará inmediatamente. Esto puede ahorrar recursos de CPU y reducir los falsos positivos durante tormentas eléctricas, tempestades, etc., aunque podría pasar por alto eventos reales, como el seguimiento automático de un objeto por parte de una cámara PTZ. La disyuntiva está entre descartar unos cuantos megabytes de grabaciones o revisar un par de clips cortos. Deje este parámetro sin establecer (None) para desactivar esta función.", + "label": "Omitir umbral de movimiento" + }, + "lightning_threshold": { + "description": "Umbral para detectar e ignorar breves picos de luz (un valor menor indica mayor sensibilidad; valores entre 0,3 y 1,0). Esto no impide por completo la detección de movimiento; Simplemente provoca que el detector deje de analizar fotogramas adicionales una vez que se supera el umbral. Durante estos eventos aún se realizan grabaciones basadas en el movimiento.", + "label": "Umbral de iluminación" + }, + "threshold": { + "description": "Umbral de diferencia de píxeles utilizado por el detector de movimiento; los valores más altos reducen la sensibilidad (rango 1-255).", + "label": "Umbral de movimiento" + }, + "label": "Detección de movimiento", + "description": "Ajustes predeterminados de detección de movimiento para esta cámara.", + "enabled": { + "label": "Habilitar detección de movimiento", + "description": "Habilita o deshabilita la detección de movimiento para esta cámara." + }, + "improve_contrast": { + "label": "Mejorar contraste", + "description": "Aplica una mejora de contraste a los fotogramas antes del análisis de movimiento para ayudar a la detección." + }, + "contour_area": { + "label": "Área de contorno", + "description": "Área mínima de contorno en píxeles necesaria para que se cuente un contorno de movimiento." + }, + "delta_alpha": { + "label": "Delta alfa", + "description": "Factor de mezcla alfa usado en la diferencia entre fotogramas para calcular el movimiento." + }, + "frame_alpha": { + "label": "Alfa del fotograma", + "description": "Valor alfa usado al mezclar fotogramas para el preprocesamiento de movimiento." + }, + "frame_height": { + "label": "Altura del fotograma", + "description": "Altura en píxeles a la que escalar los fotogramas al calcular el movimiento." + }, + "mask": { + "label": "Coordenadas de máscara", + "description": "Coordenadas x,y ordenadas que definen el polígono de máscara de movimiento usado para incluir/excluir áreas." + }, + "mqtt_off_delay": { + "label": "Retraso de apagado MQTT", + "description": "Segundos a esperar tras el último movimiento antes de publicar un estado MQTT 'off'." + }, + "enabled_in_config": { + "label": "Estado de movimiento original", + "description": "Indica si la detección de movimiento estaba habilitada en la configuración estática original." + }, + "raw_mask": { + "label": "Máscara sin procesar" + } + }, + "lpr": { + "enhancement": { + "description": "Nivel de mejora (0-10) que se aplicará a los recortes de matrículas antes del OCR; los valores más altos no siempre mejoran los resultados, y los niveles superiores a 5 podrían funcionar únicamente con matrículas capturadas de noche, por lo que deben utilizarse con precaución.", + "label": "Nivel de mejora" + }, + "expire_time": { + "description": "Tiempo en segundos tras el cual una matrícula no detectada caduca en el sistema de seguimiento (solo para cámaras LPR dedicadas).", + "label": "Segundos hasta caducar" + }, + "label": "Reconocimiento de matrículas", + "description": "Ajustes de reconocimiento de matrículas, incluidos umbrales de detección, formato y matrículas conocidas.", + "enabled": { + "label": "Habilitar LPR", + "description": "Habilita o deshabilita LPR en esta cámara." + }, + "min_area": { + "label": "Área mínima de matrícula", + "description": "Área mínima de matrícula (píxeles) necesaria para intentar el reconocimiento." + } + }, + "detect": { + "fps": { + "description": "Fotogramas por segundo deseados para ejecutar la detección; los valores más bajos reducen el uso de la CPU (el valor recomendado es 5; establezca un valor superior —como máximo de 10— únicamente si realiza el seguimiento de objetos que se mueven con extrema rapidez).", + "label": "FPS de detección" + }, + "min_initialized": { + "description": "Número de detecciones consecutivas requeridas antes de crear un objeto rastreado. Auméntelo para reducir las inicializaciones falsas. El valor predeterminado es los FPS divididos por 2.", + "label": "Fotogramas mínimos de inicialización" + }, + "height": { + "description": "Altura (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.", + "label": "Altura de detección" + }, + "width": { + "description": "Ancho (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.", + "label": "Anchura de detección" + }, + "stationary": { + "description": "Configuración para detectar y gestionar objetos que permanecen inmóviles durante un periodo de tiempo.", + "label": "Configuración de objetos estacionarios", + "interval": { + "label": "Intervalo estacionario", + "description": "Frecuencia (en fotogramas) con la que se ejecuta una comprobación de detección para confirmar un objeto estacionario." + }, + "threshold": { + "label": "Umbral estacionario", + "description": "Número de fotogramas sin cambio de posición necesarios para marcar un objeto como estacionario." + }, + "max_frames": { + "label": "Fotogramas máximos", + "description": "Limita durante cuánto tiempo se rastrean los objetos estacionarios antes de descartarlos.", + "default": { + "label": "Fotogramas máximos predeterminados", + "description": "Número máximo predeterminado de fotogramas para rastrear un objeto estacionario antes de detenerse." + }, + "objects": { + "label": "Fotogramas máximos por objeto", + "description": "Sobrescrituras por objeto para el número máximo de fotogramas en los que rastrear objetos estacionarios." + } + }, + "classifier": { + "label": "Habilitar clasificador visual", + "description": "Usa un clasificador visual para detectar objetos realmente estacionarios incluso cuando los cuadros delimitadores oscilan." + } + }, + "label": "Detección de objetos", + "description": "Ajustes del rol de detección/detect usado para ejecutar la detección de objetos e inicializar los rastreadores.", + "enabled": { + "label": "Habilitar detección de objetos", + "description": "Habilita o deshabilita la detección de objetos para esta cámara." + }, + "max_disappeared": { + "label": "Fotogramas máximos desaparecido", + "description": "Número de fotogramas sin detección antes de que un objeto rastreado se considere desaparecido." + }, + "annotation_offset": { + "label": "Desplazamiento de anotaciones", + "description": "Milisegundos para desplazar las anotaciones de detección y alinear mejor los cuadros delimitadores de la línea de tiempo con las grabaciones; puede ser positivo o negativo." + } + }, + "record": { + "motion": { + "description": "Número de días para conservar las grabaciones activadas por movimiento, independientemente de los objetos rastreados. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.", + "label": "Retención de movimiento", + "days": { + "label": "Días de retención", + "description": "Días durante los que conservar las grabaciones." + } + }, + "continuous": { + "description": "Número de días para conservar las grabaciones, independientemente de los objetos rastreados o del movimiento. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.", + "label": "Retención continua", + "days": { + "label": "Días de retención", + "description": "Días durante los que conservar las grabaciones." + } + }, + "detections": { + "pre_capture": { + "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura previa" + }, + "post_capture": { + "description": "Número de segundos después del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura posterior" + }, + "label": "Retención de detección", + "description": "Ajustes de retención de grabaciones para eventos de detección, incluidas las duraciones de captura previa/posterior.", + "retain": { + "label": "Retención de eventos", + "description": "Ajustes de retención para grabaciones de eventos de detección.", + "days": { + "label": "Días de retención", + "description": "Número de días durante los que conservar grabaciones de eventos de detección." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + } + } + }, + "alerts": { + "pre_capture": { + "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura previa" + }, + "post_capture": { + "description": "Número de segundos después del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura posterior" + }, + "label": "Retención de alertas", + "description": "Ajustes de retención de grabaciones para eventos de alerta, incluidas las duraciones de captura previa/posterior.", + "retain": { + "label": "Retención de eventos", + "description": "Ajustes de retención para grabaciones de eventos de detección.", + "days": { + "label": "Días de retención", + "description": "Número de días durante los que conservar grabaciones de eventos de detección." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + } + } + }, + "label": "Grabación", + "description": "Ajustes de grabación y retención para esta cámara.", + "enabled": { + "label": "Habilitar grabación", + "description": "Habilita o deshabilita la grabación para esta cámara." + }, + "expire_interval": { + "label": "Intervalo de limpieza de grabaciones", + "description": "Minutos entre pasadas de limpieza que eliminan segmentos de grabación caducados." + }, + "export": { + "label": "Configuración de exportación", + "description": "Ajustes usados al exportar grabaciones, como timelapse y aceleración por hardware.", + "hwaccel_args": { + "label": "Argumentos hwaccel de exportación", + "description": "Argumentos de aceleración por hardware que se usarán en operaciones de exportación/transcodificación." + }, + "max_concurrent": { + "label": "Exportaciones simultáneas máximas", + "description": "Número máximo de trabajos de exportación que se procesarán al mismo tiempo." + } + }, + "preview": { + "label": "Configuración de vista previa", + "description": "Ajustes que controlan la calidad de las vistas previas de grabaciones mostradas en la interfaz.", + "quality": { + "label": "Calidad de vista previa", + "description": "Nivel de calidad de vista previa (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Estado de grabación original", + "description": "Indica si la grabación estaba habilitada en la configuración estática original." + } + }, + "ui": { + "dashboard": { + "description": "Alterna si esta cámara es visible en toda la interfaz de usuario de Frigate. Desactivar esta opción requerirá editar manualmente la configuración para volver a visualizar esta cámara en la interfaz.", + "label": "Mostrar en la interfaz" + }, + "label": "Interfaz de cámara", + "description": "Orden de visualización y visibilidad de esta cámara en la interfaz. El orden afecta al panel predeterminado. Para un control más granular, usa grupos de cámaras.", + "order": { + "label": "Orden en la interfaz", + "description": "Orden numérico usado para ordenar la cámara en la interfaz (panel predeterminado y listas); los números más altos aparecen más tarde." + }, + "review": { + "label": "Mostrar en Revisión", + "description": "Activa o desactiva si esta cámara es visible en Revisión (la página de Revisión y su filtro de cámaras, la revisión de movimiento y la vista de historial)." + } + }, + "live": { + "height": { + "description": "Altura (en píxeles) para renderizar la transmisión en vivo de jsmpeg en la interfaz web; debe ser <= a la altura de la transmisión de detección.", + "label": "Altura en directo" + }, + "description": "Configuraciones utilizadas por la interfaz web para controlar la selección, la resolución y la calidad de transmisiónes en vivo.", + "label": "Reproducción en directo", + "streams": { + "label": "Nombres de flujos en directo", + "description": "Asignación de nombres de flujos configurados a nombres de restream/go2rtc usados para la reproducción en directo." + }, + "quality": { + "label": "Calidad en directo", + "description": "Calidad de codificación para el flujo jsmpeg (1 la más alta, 31 la más baja)." + } + }, + "review": { + "description": "Configuraciones que controlan las alertas, las detecciones y los resúmenes de revisión de GenAI utilizados por la interfaz de usuario y el almacenamiento de esta cámara.", + "alerts": { + "required_zones": { + "description": "Zonas en las que debe entrar un objeto para ser considerado una alerta; dejar vacío para permitir cualquier zona.", + "label": "Zonas requeridas" + }, + "labels": { + "description": "Lista de etiquetas de objetos que califican como alertas (por ejemplo: car, person).", + "label": "Etiquetas de alerta" + }, + "label": "Configuración de alertas", + "description": "Ajustes sobre qué objetos rastreados generan alertas y cómo se conservan las alertas.", + "enabled": { + "label": "Habilitar alertas", + "description": "Habilita o deshabilita la generación de alertas para esta cámara." + }, + "enabled_in_config": { + "label": "Estado original de alertas", + "description": "Rastrea si las alertas estaban habilitadas originalmente en la configuración estática." + }, + "cutoff_time": { + "label": "Tiempo de corte de alertas", + "description": "Segundos que se esperarán tras dejar de haber actividad causante de alerta antes de cortar una alerta." + } + }, + "detections": { + "required_zones": { + "description": "Zonas en las que debe entrar un objeto para ser considerado detectado; dejar vacío para permitir cualquier zona.", + "label": "Zonas requeridas" + }, + "description": "Configuración para determinar qué objetos rastreados generan detecciones (no alertas) y cómo se retienen dichas detecciones.", + "label": "Configuración de detecciones", + "enabled": { + "label": "Habilitar detecciones", + "description": "Habilita o deshabilita los eventos de detección para esta cámara." + }, + "labels": { + "label": "Etiquetas de detección", + "description": "Lista de etiquetas de objetos que cuentan como eventos de detección." + }, + "cutoff_time": { + "label": "Tiempo de corte de detecciones", + "description": "Segundos que se esperarán tras dejar de haber actividad causante de detección antes de cortar una detección." + }, + "enabled_in_config": { + "label": "Estado original de detecciones", + "description": "Rastrea si las detecciones estaban habilitadas originalmente en la configuración estática." + } + }, + "genai": { + "image_source": { + "description": "Fuente de las imágenes enviadas a GenAI ('preview' o 'recordings'); La opción 'recordings' utiliza fotogramas de mayor calidad, pero requiere más tokens.", + "label": "Origen de imagen de revisión" + }, + "additional_concerns": { + "description": "Una lista de preocupaciones o notas adicionales que GenAI debería tener en cuenta al evaluar la actividad en esta cámara.", + "label": "Consideraciones adicionales" + }, + "activity_context_prompt": { + "description": "Instrucción personalizada que describe qué constituye y qué no una actividad sospechosa, con el fin de proporcionar contexto para los resúmenes generados por GenAI.", + "label": "Prompt de contexto de actividad" + }, + "description": "Controla el uso de IA generativa (GenAI) para la elaboración de descripciones y resúmenes de elementos de revisión.", + "debug_save_thumbnails": { + "description": "Guarde las miniaturas que se envían al proveedor de GenAI para su depuración y revisión.", + "label": "Guardar miniaturas" + }, + "label": "Configuración de GenAI", + "enabled": { + "label": "Habilitar descripciones de GenAI", + "description": "Habilita o deshabilita las descripciones y resúmenes generados por GenAI para los elementos de revisión." + }, + "alerts": { + "label": "Habilitar GenAI para alertas", + "description": "Usa GenAI para generar descripciones de elementos de alerta." + }, + "detections": { + "label": "Habilitar GenAI para detecciones", + "description": "Usa GenAI para generar descripciones de elementos de detección." + }, + "enabled_in_config": { + "label": "Estado original de GenAI", + "description": "Rastrea si la revisión de GenAI estaba habilitada originalmente en la configuración estática." + }, + "preferred_language": { + "label": "Idioma preferido", + "description": "Idioma preferido que se solicitará al proveedor de GenAI para las respuestas generadas." + } + }, + "label": "Revisión" + }, + "birdseye": { + "description": "Configuración para la vista compuesta Birdseye, que combina las transmisiones de múltiples cámaras en una sola vista.", + "label": "Birdseye", + "enabled": { + "label": "Habilitar Birdseye", + "description": "Habilita o deshabilita la función de vista Birdseye." + }, + "mode": { + "label": "Modo de seguimiento", + "description": "Modo para incluir cámaras en Birdseye: 'objects', 'motion' o 'continuous'." + }, + "order": { + "label": "Posición", + "description": "Posición numérica que controla el orden de la cámara en el diseño de Birdseye." + } + }, + "ffmpeg": { + "retry_interval": { + "description": "Segundos de espera antes de intentar reconectar la transmisión de una cámara tras un fallo. El valor predeterminado es 10.", + "label": "Tiempo de reintento de FFmpeg" + }, + "path": { + "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"7.0\" o \"8.0\").", + "label": "Ruta de FFmpeg" + }, + "output_args": { + "description": "Argumentos de salida predeterminados utilizados para diferentes roles de FFmpeg, tales como detección y grabación.", + "label": "Argumentos de salida", + "detect": { + "label": "Argumentos de salida de detección", + "description": "Argumentos de salida predeterminados para los flujos con rol de detección." + }, + "record": { + "label": "Argumentos de salida de grabación", + "description": "Argumentos de salida predeterminados para los flujos con rol de grabación." + } + }, + "description": "Entradas de flujo de la cámara y opciones de FFmpeg, incluida la ruta del binario, argumentos, aceleración por hardware y argumentos de salida por rol.", + "label": "Flujos (FFmpeg)", + "global_args": { + "label": "Argumentos globales de FFmpeg", + "description": "Argumentos globales pasados a los procesos de FFmpeg." + }, + "hwaccel_args": { + "label": "Argumentos de aceleración por hardware", + "description": "Argumentos de aceleración por hardware para FFmpeg. Se recomiendan preajustes específicos del proveedor." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada aplicados a los flujos de entrada de FFmpeg." + }, + "apple_compatibility": { + "label": "Compatibilidad con Apple", + "description": "Habilita el etiquetado HEVC para mejorar la compatibilidad con reproductores de Apple al grabar H.265." + }, + "gpu": { + "label": "Índice de GPU", + "description": "Índice de GPU predeterminado usado para la aceleración por hardware si está disponible." + }, + "inputs": { + "label": "Entradas de cámara", + "description": "Lista de definiciones de flujos de entrada (rutas y roles) para esta cámara.", + "path": { + "label": "Ruta de entrada", + "description": "URL o ruta del flujo de entrada de la cámara." + }, + "roles": { + "label": "Roles de entrada", + "description": "Roles para este flujo de entrada." + }, + "global_args": { + "label": "Argumentos globales de FFmpeg", + "description": "Argumentos globales de FFmpeg para este flujo de entrada." + }, + "hwaccel_args": { + "label": "Argumentos de aceleración por hardware", + "description": "Argumentos de aceleración por hardware para este flujo de entrada." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada específicos para este flujo." + } + } + }, + "face_recognition": { + "label": "Reconocimiento facial", + "description": "Ajustes de detección y reconocimiento facial para esta cámara.", + "enabled": { + "label": "Habilitar reconocimiento facial", + "description": "Habilita o deshabilita el reconocimiento facial." + }, + "min_area": { + "label": "Área mínima de rostro", + "description": "Área mínima (píxeles) del cuadro de un rostro detectado necesaria para intentar el reconocimiento." + } + }, + "semantic_search": { + "label": "Búsqueda semántica", + "description": "Ajustes de búsqueda semántica, que crea y consulta embeddings de objetos para encontrar elementos similares.", + "triggers": { + "label": "Activadores", + "description": "Acciones y criterios de coincidencia para activadores de búsqueda semántica específicos de la cámara.", + "friendly_name": { + "label": "Nombre descriptivo", + "description": "Nombre descriptivo opcional mostrado en la interfaz para este activador." + }, + "enabled": { + "label": "Habilitar este activador", + "description": "Habilita o deshabilita este activador de búsqueda semántica." + }, + "type": { + "label": "Tipo de activador", + "description": "Tipo de activador: 'thumbnail' (coincidir con imagen) o 'description' (coincidir con texto)." + }, + "data": { + "label": "Contenido del activador", + "description": "Frase de texto o ID de miniatura que se comparará con objetos rastreados." + }, + "threshold": { + "label": "Umbral del activador", + "description": "Puntuación mínima de similitud (0-1) necesaria para activar este activador." + }, + "actions": { + "label": "Acciones del activador", + "description": "Lista de acciones que se ejecutarán cuando el activador coincida (notification, sub_label, attribute)." + } + } + }, + "snapshots": { + "label": "Instantáneas", + "description": "Ajustes de instantáneas generadas por la API de objetos rastreados para esta cámara.", + "enabled": { + "label": "Habilitar instantáneas", + "description": "Habilita o deshabilita el guardado de instantáneas para esta cámara." + }, + "timestamp": { + "label": "Superposición de marca de tiempo", + "description": "Superpone una marca de tiempo en las instantáneas de la API." + }, + "bounding_box": { + "label": "Superposición de cuadro delimitador", + "description": "Dibuja cuadros delimitadores para los objetos rastreados en las instantáneas de la API." + }, + "crop": { + "label": "Recortar instantánea", + "description": "Recorta las instantáneas de la API al cuadro delimitador del objeto detectado." + }, + "required_zones": { + "label": "Zonas requeridas", + "description": "Zonas en las que debe entrar un objeto para que se guarde una instantánea." + }, + "height": { + "label": "Altura de instantánea", + "description": "Altura (píxeles) a la que redimensionar las instantáneas de la API; déjalo vacío para conservar el tamaño original." + }, + "retain": { + "label": "Retención de instantáneas", + "description": "Ajustes de retención de instantáneas, incluidos días predeterminados y sobrescrituras por objeto.", + "default": { + "label": "Retención predeterminada", + "description": "Número predeterminado de días durante los que conservar instantáneas." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + }, + "objects": { + "label": "Retención por objeto", + "description": "Sobrescrituras por objeto para los días de retención de instantáneas." + } + }, + "quality": { + "label": "Calidad de instantánea", + "description": "Calidad de codificación de las instantáneas guardadas (0-100)." + } + }, + "timestamp_style": { + "label": "Estilo de marca de tiempo", + "description": "Opciones de estilo para las marcas de tiempo aplicadas a las instantáneas y a la vista de depuración.", + "position": { + "label": "Posición de marca de tiempo", + "description": "Posición de la marca de tiempo en la imagen (tl/tr/bl/br)." + }, + "format": { + "label": "Formato de marca de tiempo", + "description": "Cadena de formato de fecha y hora usada para las marcas de tiempo (códigos de formato datetime de Python)." + }, + "color": { + "label": "Color de marca de tiempo", + "description": "Valores de color RGB para el texto de la marca de tiempo (todos los valores 0-255).", + "red": { + "label": "Rojo", + "description": "Componente rojo (0-255) para el color de la marca de tiempo." + }, + "green": { + "label": "Verde", + "description": "Componente verde (0-255) para el color de la marca de tiempo." + }, + "blue": { + "label": "Azul", + "description": "Componente azul (0-255) para el color de la marca de tiempo." + } + }, + "thickness": { + "label": "Grosor de marca de tiempo", + "description": "Grosor de línea del texto de la marca de tiempo." + }, + "effect": { + "label": "Efecto de marca de tiempo", + "description": "Efecto visual para el texto de la marca de tiempo (none, solid, shadow)." + } + }, + "best_image_timeout": { + "label": "Tiempo de espera de mejor imagen", + "description": "Tiempo que se esperará la imagen con la puntuación de confianza más alta." + }, + "type": { + "label": "Tipo de cámara", + "description": "Tipo de cámara" + }, + "webui_url": { + "label": "URL de la cámara", + "description": "URL para visitar la cámara directamente desde la página del sistema" + }, + "profiles": { + "label": "Perfiles", + "description": "Perfiles de configuración con nombre y sobrescrituras parciales que pueden activarse en tiempo de ejecución." + }, + "enabled_in_config": { + "label": "Estado original de cámara", + "description": "Mantiene el registro del estado original de la cámara." + } +} diff --git a/web/public/locales/es/config/global.json b/web/public/locales/es/config/global.json new file mode 100644 index 0000000..7869ce4 --- /dev/null +++ b/web/public/locales/es/config/global.json @@ -0,0 +1,1639 @@ +{ + "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": "Detección de audio", + "enabled": { + "label": "Habilitar la detección de audio", + "description": "Habilita o deshabilita la detección de eventos de audio para todas las cámaras; se puede sobrescribir por 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", + "description": "Umbral mínimo de volumen RMS requerido para ejecutar la detección de audio; los valores más bajos aumentan la sensibilidad (p. ej., 200 alta, 500 media, 1000 baja)." + }, + "listen": { + "label": "Tipos de escucha", + "description": "Lista de tipos de eventos de audio a detectar (por ejemplo: ladrar, alarma de incendio, habla, gritar)." + }, + "filters": { + "label": "Filtros de audio", + "description": "Ajustes de filtro por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos.", + "threshold": { + "label": "Confianza mínima de audio", + "description": "Umbral mínimo de confianza para que se cuente el evento de audio." + } + }, + "enabled_in_config": { + "description": "Indica si la detección de audio estaba habilitada originalmente en el archivo de configuración estático.", + "label": "Estado de audio original" + }, + "num_threads": { + "label": "Hilos de detección", + "description": "Número de hilos a usar para procesamiento de detección de audio." + }, + "description": "Ajustes para la detección de eventos basada en audio en todas las cámaras; se pueden sobrescribir por cámara." + }, + "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.", + "enabled": { + "label": "Activar autenticación", + "description": "Activar la autenticación nativa para la interfaz de Frigate." + }, + "reset_admin_password": { + "label": "Restablecer contraseña de administrador", + "description": "Si se activa, restablece la contraseña del administrador al iniciar y muestra la nueva contraseña en los registros." + }, + "cookie_name": { + "description": "Nombre de la cookie utilizada para almacenar el token JWT para la autenticación nativa.", + "label": "Nombre de la cookie JWT" + }, + "cookie_secure": { + "label": "Flag de cookie segura", + "description": "Establece el flag de seguridad en la cookie de autenticación; debe ser 'true' cuando se utilice TLS." + }, + "failed_login_rate_limit": { + "label": "Limite de intento de acceso fallidos", + "description": "Reglas de limitación de intentos de inicio de sesión fallidos para reducir los ataques de fuerza bruta." + }, + "session_length": { + "description": "Duración de la sesión en segundos para sesiones de JWT.", + "label": "Duración de la sesión" + }, + "admin_first_time_login": { + "description": "Cuando se establece en true, la interfaz de usuario puede mostrar un enlace de ayuda en la página de inicio de sesión, informando a los usuarios sobre cómo iniciar sesión tras el restablecimiento de la contraseña de administrador. ", + "label": "Marca de administrador inicial" + }, + "refresh_time": { + "description": "Cuando a una sesión le queden menos de esta cantidad de segundos para expirar, actualícela para restablecer su duración completa.", + "label": "Ventana de actualización de la sesión" + }, + "trusted_proxies": { + "label": "Proxies de confianza", + "description": "Lista de IPs de proxies de confianza utilizadas para determinar la IP del cliente en la limitación de peticiones." + }, + "hash_iterations": { + "label": "Iteraciones de hash", + "description": "Número de iteraciones PBKDF2-SHA256 que se utilizarán al generar el hash de las contraseñas de los usuarios." + }, + "roles": { + "label": "Asignaciones de roles", + "description": "Asigna roles a listas de cámaras. Una lista vacía concede acceso a todas las cámaras para ese rol." + } + }, + "onvif": { + "profile": { + "label": "Perfil ONVIF", + "description": "Perfil multimedia ONVIF específico que se utilizará para el control PTZ, identificado por token o nombre. Si no se especifica, se selecciona automáticamente el primer perfil con una configuración PTZ válida." + }, + "autotracking": { + "zoom_factor": { + "description": "Controla el nivel de zoom en los objetos rastreados. Los valores más bajos mantienen una mayor parte de la escena a la vista; los valores más altos acercan la imagen, pero pueden provocar la pérdida del rastreo. Valores entre 0.1 y 0.75.", + "label": "Factor de zoom" + }, + "calibrate_on_startup": { + "description": "Mida la velocidad de los motores PTZ al encenderlos para mejorar la precisión del seguimiento. Frigate actualizará la configuración con los `movement_weights` tras la calibración.", + "label": "Calibrar al iniciar" + }, + "description": "Realice un seguimiento automático de objetos en movimiento y manténgalos centrados en el encuadre mediante movimientos de cámara PTZ.", + "zooming": { + "description": "Control del comportamiento del zoom: deshabilitado (solo panorámica/inclinación), absoluto (mayor compatibilidad) o relativo (panorámica/inclinación/zoom simultáneos).", + "label": "Modo de zoom" + }, + "return_preset": { + "description": "Nombre del preajuste ONVIF configurado en el firmware de la cámara al que regresar una vez finalizado el seguimiento.", + "label": "Preajuste de retorno" + }, + "timeout": { + "description": "Espere esta cantidad de segundos después de perder el seguimiento antes de devolver la cámara a la posición preestablecida.", + "label": "Tiempo de espera de retorno" + }, + "label": "Seguimiento automático", + "enabled": { + "label": "Habilitar seguimiento automático", + "description": "Habilita o deshabilita el seguimiento automático con cámara PTZ de objetos detectados." + }, + "track": { + "label": "Objetos rastreados", + "description": "Lista de tipos de objetos que deben activar el seguimiento automático." + }, + "required_zones": { + "label": "Zonas requeridas", + "description": "Los objetos deben entrar en una de estas zonas antes de que comience el seguimiento automático." + }, + "movement_weights": { + "label": "Pesos de movimiento", + "description": "Valores de calibración generados automáticamente por la calibración de la cámara. No los modifiques manualmente." + }, + "enabled_in_config": { + "label": "Estado original de autoseguimiento", + "description": "Campo interno para rastrear si el seguimiento automático estaba habilitado en la configuración." + } + }, + "tls_insecure": { + "description": "Omitir la verificación TLS y deshabilitar la autenticación digest para ONVIF (no seguro; usar solo en redes seguras).", + "label": "Deshabilitar verificación TLS" + }, + "label": "ONVIF", + "description": "Ajustes de conexión ONVIF y seguimiento automático PTZ para esta cámara.", + "host": { + "label": "Host ONVIF", + "description": "Host (y esquema opcional) para el servicio ONVIF de esta cámara." + }, + "port": { + "label": "Puerto ONVIF", + "description": "Número de puerto del servicio ONVIF." + }, + "user": { + "label": "Nombre de usuario ONVIF", + "description": "Nombre de usuario para la autenticación ONVIF; algunos dispositivos requieren un usuario administrador para ONVIF." + }, + "password": { + "label": "Contraseña ONVIF", + "description": "Contraseña para la autenticación ONVIF." + }, + "ignore_time_mismatch": { + "label": "Ignorar discrepancia horaria", + "description": "Ignora las diferencias de sincronización horaria entre la cámara y el servidor Frigate para la comunicación ONVIF." + } + }, + "objects": { + "raw_mask": { + "label": "Máscara en bruto" + }, + "genai": { + "label": "Configuración de objetos GenAI", + "description": "Opciones de GenAI para describir objetos rastreados y enviar fotogramas para su generación.", + "enabled": { + "label": "Activar GenAI", + "description": "Activar por defecto la generación de descripciones de GenAI para los objetos rastreados." + }, + "use_snapshot": { + "label": "Usar instantáneas", + "description": "Usar instantáneas de objetos en lugar de miniaturas para la generación de descripciones de GenAI." + }, + "send_triggers": { + "after_significant_updates": { + "description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado.", + "label": "Activador temprano de GenAI" + }, + "description": "Define cuándo se deben enviar los fotogramas a GenAI (al finalizar, después de las actualizaciones, etc.).", + "label": "Activadores de GenAI", + "tracked_object_end": { + "label": "Enviar al finalizar", + "description": "Envía una solicitud a GenAI cuando finaliza el objeto rastreado." + } + }, + "required_zones": { + "description": "Zonas en las que deben ubicarse los objetos para ser elegibles para la generación de descripciones con GenAI.", + "label": "Zonas requeridas" + }, + "prompt": { + "label": "Prompt de descripción", + "description": "Plantilla de prompt predeterminada usada al generar descripciones con GenAI." + }, + "object_prompts": { + "label": "Prompts de objetos", + "description": "Prompts por objeto para personalizar las salidas de GenAI para etiquetas concretas." + }, + "objects": { + "label": "Objetos de GenAI", + "description": "Lista de etiquetas de objetos que se enviarán a GenAI de forma predeterminada." + }, + "debug_save_thumbnails": { + "label": "Guardar miniaturas", + "description": "Guarda las miniaturas enviadas a GenAI para depuración y revisión." + }, + "enabled_in_config": { + "label": "Estado original de GenAI", + "description": "Indica si GenAI estaba habilitado en la configuración estática original." + } + }, + "track": { + "description": "Lista de etiquetas de objetos a rastrear para todas las cámaras; puede anularse por cámara.", + "label": "Objetos a rastrear" + }, + "label": "Objetos", + "description": "Valores predeterminados de seguimiento de objetos, incluidas las etiquetas que se rastrean y los filtros por objeto.", + "filters": { + "label": "Filtros de objetos", + "description": "Filtros aplicados a los objetos detectados para reducir falsos positivos (área, relación, confianza).", + "min_area": { + "label": "Área mínima de objeto", + "description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + }, + "max_area": { + "label": "Área máxima de objeto", + "description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + }, + "min_ratio": { + "label": "Relación de aspecto mínima", + "description": "Relación mínima anchura/altura necesaria para que el cuadro delimitador sea válido." + }, + "max_ratio": { + "label": "Relación de aspecto máxima", + "description": "Relación máxima anchura/altura permitida para que el cuadro delimitador sea válido." + }, + "threshold": { + "label": "Umbral de confianza", + "description": "Umbral medio de confianza de detección necesario para que el objeto se considere un positivo verdadero." + }, + "min_score": { + "label": "Confianza mínima", + "description": "Confianza mínima de detección en un único fotograma necesaria para que el objeto se contabilice." + }, + "mask": { + "label": "Máscara de filtro", + "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma." + }, + "raw_mask": { + "label": "Máscara sin procesar" + } + }, + "mask": { + "label": "Máscara de objeto", + "description": "Polígono de máscara usado para evitar la detección de objetos en áreas especificadas." + }, + "filters_attribute": { + "label": "Filtros de atributos", + "description": "Filtros aplicados a los atributos detectados para reducir falsos positivos (área, proporción y confianza).", + "min_area": { + "label": "Área mínima del atributo", + "description": "Área mínima del cuadro delimitador (en píxeles o porcentaje) necesaria para este atributo. Puede expresarse en píxeles (entero) o como porcentaje (valor decimal entre 0.000001 y 0.99)." + }, + "max_area": { + "label": "Área máxima del atributo", + "description": "Área máxima del cuadro delimitador (en píxeles o porcentaje) permitida para este atributo. Puede expresarse en píxeles (entero) o como porcentaje (valor decimal entre 0.000001 y 0.99)." + }, + "min_ratio": { + "label": "Relación de aspecto mínima", + "description": "Relación mínima entre anchura y altura necesaria para que el cuadro delimitador se considere válido." + }, + "max_ratio": { + "label": "Relación de aspecto máxima", + "description": "Relación máxima entre anchura y altura permitida para que el cuadro delimitador se considere válido." + }, + "threshold": { + "label": "Umbral de confianza", + "description": "Umbral medio de confianza de detección necesario para que el atributo se considere un verdadero positivo." + }, + "min_score": { + "label": "Confianza mínima", + "description": "Confianza mínima de detección en un único fotograma necesaria para asociar este atributo con su objeto principal." + }, + "mask": { + "label": "Máscara de filtro", + "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma." + }, + "raw_mask": { + "label": "Máscara sin procesar" + } + } + }, + "detectors": { + "deepstack": { + "description": "Detector DeepStack/CodeProject.AI que envía imágenes a una API HTTP remota de DeepStack para la inferencia. No recomendado.", + "api_url": { + "description": "La URL de la API de DeepStack.", + "label": "URL de la API de DeepStack" + }, + "api_timeout": { + "label": "Tiempo de espera de la API de DeepStack (en segundos)", + "description": "Tiempo máximo permitido para una solicitud a la API de DeepStack." + }, + "api_key": { + "label": "Clave de API de DeepStack (si es necesaria)", + "description": "Clave API opcional para servicios autenticados de DeepStack." + }, + "label": "DeepStack" + }, + "type": { + "label": "Tipo" + }, + "label": "Detector de hardware", + "cpu": { + "label": "CPU", + "num_threads": { + "label": "Número de hilos para detección", + "description": "Número de hilos usados para inferencia basada en CPU." + }, + "description": "Detector TFLite de CPU que ejecuta modelos de TensorFlow Lite en la CPU del host sin aceleración por hardware. No recomendado." + }, + "axengine": { + "label": "Motor AX NPU", + "description": "Detector NPU AXERA AX650N/AX8850N que ejecuta archivos .axmodel compilados mediante el runtime AXEngine." + }, + "teflon_tfl": { + "description": "Detector de delegados Teflon para TFLite, que utiliza la biblioteca de delegados Mesa Teflon para acelerar la inferencia en las GPU compatibles.", + "label": "Teflon" + }, + "synaptics": { + "description": "Detector NPU de Synaptics para modelos en formato .synap, utilizando el Synap SDK en hardware de Synaptics.", + "label": "Synaptics" + }, + "zmq": { + "description": "Detector ZMQ IPC que descarga la inferencia a un proceso externo a través de un punto de conexión IPC de ZeroMQ.", + "label": "IPC de ZMQ", + "endpoint": { + "label": "Endpoint IPC de ZMQ", + "description": "Endpoint ZMQ al que conectarse." + }, + "request_timeout_ms": { + "label": "Tiempo de espera de solicitud ZMQ en milisegundos", + "description": "Tiempo de espera para solicitudes ZMQ en milisegundos." + }, + "linger_ms": { + "label": "Persistencia del socket ZMQ en milisegundos", + "description": "Periodo de persistencia del socket en milisegundos." + } + }, + "hailo8l": { + "description": "Detector Hailo-8/Hailo-8L que utiliza modelos HEF y el SDK HailoRT para la inferencia en hardware Hailo.", + "label": "Hailo-8/Hailo-8L", + "device": { + "label": "Tipo de dispositivo", + "description": "Dispositivo que se usará para la inferencia Hailo (p. ej., 'PCIe', 'M.2')." + } + }, + "onnx": { + "description": "Detector ONNX para ejecutar modelos ONNX; utilizará los backends de aceleración disponibles (CUDA/ROCm/OpenVINO) cuando estén disponibles.", + "label": "ONNX", + "device": { + "label": "Tipo de dispositivo", + "description": "Dispositivo que se usará para la inferencia ONNX (p. ej., 'AUTO', 'CPU', 'GPU')." + } + }, + "description": "Configuración para detectores de objetos (backends de CPU, GPU y ONNX) y cualquier ajuste del modelo específico del detector.", + "openvino": { + "description": "Detector OpenVINO para CPU AMD e Intel, GPU Intel y hardware VPU Intel.", + "label": "OpenVINO", + "device": { + "label": "Tipo de dispositivo", + "description": "Dispositivo que se usará para la inferencia OpenVINO (p. ej., 'CPU', 'GPU', 'NPU')." + } + }, + "tensorrt": { + "description": "Detector TensorRT para dispositivos Nvidia Jetson que utiliza motores TensorRT serializados para una inferencia acelerada.", + "label": "TensorRT", + "device": { + "label": "Índice de dispositivo GPU", + "description": "Índice del dispositivo GPU que se usará." + } + }, + "degirum": { + "description": "Detector DeGirum para ejecutar modelos a través de la nube de DeGirum o servicios de inferencia local.", + "label": "DeGirum", + "location": { + "label": "Ubicación de inferencia", + "description": "Ubicación del motor de inferencia DeGirum (p. ej., '@cloud', '127.0.0.1')." + }, + "zoo": { + "label": "Repositorio de modelos", + "description": "Ruta o URL al repositorio de modelos de DeGirum." + }, + "token": { + "label": "Token de DeGirum Cloud", + "description": "Token para acceder a DeGirum Cloud." + } + }, + "rknn": { + "description": "Detector RKNN para NPUs de Rockchip; ejecuta modelos compilados para RKNN en hardware de Rockchip.", + "label": "RKNN", + "num_cores": { + "label": "Número de núcleos NPU que se usarán.", + "description": "Número de núcleos NPU que se usarán (0 para automático)." + } + }, + "model": { + "label": "Configuración de modelo específica del detector", + "description": "Opciones de configuración de modelo específicas del detector (ruta, tamaño de entrada, etc.).", + "path": { + "label": "Ruta del modelo de detector de objetos personalizado", + "description": "Ruta a un archivo de modelo de detección personalizado (o plus:// para modelos de Frigate+)." + }, + "labelmap_path": { + "label": "Mapa de etiquetas para detector de objetos personalizado", + "description": "Ruta a un archivo labelmap que asigna clases numéricas a etiquetas de texto para el detector." + }, + "width": { + "label": "Anchura de entrada del modelo de detección de objetos", + "description": "Anchura del tensor de entrada del modelo en píxeles." + }, + "height": { + "label": "Altura de entrada del modelo de detección de objetos", + "description": "Altura del tensor de entrada del modelo en píxeles." + }, + "labelmap": { + "label": "Personalización del mapa de etiquetas", + "description": "Sobrescrituras o entradas de reasignación que se fusionarán con el mapa de etiquetas estándar." + }, + "attributes_map": { + "label": "Mapa de etiquetas de objetos a sus etiquetas de atributos", + "description": "Asignación de etiquetas de objetos a etiquetas de atributos usada para adjuntar metadatos (por ejemplo, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Forma del tensor de entrada del modelo", + "description": "Formato de tensor esperado por el modelo: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Formato de color de píxeles de entrada del modelo", + "description": "Espacio de color de píxeles esperado por el modelo: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipo D de entrada del modelo", + "description": "Tipo de datos del tensor de entrada del modelo (por ejemplo, 'float32')." + }, + "model_type": { + "label": "Tipo de modelo de detección de objetos", + "description": "Tipo de arquitectura del modelo detector (ssd, yolox, yolonas) usado por algunos detectores para optimización." + } + }, + "model_path": { + "label": "Ruta de modelo específica del detector", + "description": "Ruta del archivo binario del modelo detector si lo requiere el detector elegido." + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "Detector EdgeTPU que ejecuta modelos TensorFlow Lite compilados para Coral EdgeTPU mediante el delegado EdgeTPU.", + "device": { + "label": "Tipo de dispositivo", + "description": "Dispositivo que se usará para la inferencia EdgeTPU (p. ej., 'usb', 'pci')." + } + }, + "memryx": { + "label": "MemryX", + "description": "Detector MemryX MX3 que ejecuta modelos DFP compilados en aceleradores MemryX.", + "device": { + "label": "Ruta del dispositivo", + "description": "Dispositivo que se usará para la inferencia MemryX (p. ej., 'PCIe')." + } + } + }, + "database": { + "label": "Base de datos", + "description": "Configuración de la base de datos SQLite utilizada por Frigate para almacenar los metadatos de los objetos rastreados y las grabaciones.", + "path": { + "label": "Ruta de la base de datos", + "description": "Ruta del sistema de archivos donde se almacenará el archivo de base de datos SQLite de Frigate." + } + }, + "mqtt": { + "label": "MQTT", + "port": { + "label": "Puerto MQTT", + "description": "Puerto del broker MQTT (normalmente 1883 para MQTT sin cifrar)." + }, + "tls_client_cert": { + "label": "Certificado cliente", + "description": "Ruta del certificado de cliente para autenticación TLS mutua; no configures usuario/contraseña al usar certificados de cliente." + }, + "description": "Configuración para conectar y publicar telemetría, instantáneas y detalles de eventos en un broker MQTT.", + "topic_prefix": { + "description": "Prefijo del tema MQTT para todos los temas de Frigate; debe ser único si se ejecutan múltiples instancias.", + "label": "Prefijo de tema" + }, + "client_id": { + "description": "Identificador de cliente utilizado al conectarse al broker MQTT; debe ser único para cada instancia.", + "label": "ID de cliente" + }, + "enabled": { + "label": "Habilitar MQTT", + "description": "Habilita o deshabilita la integración MQTT para estados, eventos e instantáneas." + }, + "host": { + "label": "Host MQTT", + "description": "Nombre de host o dirección IP del broker MQTT." + }, + "stats_interval": { + "label": "Intervalo de estadísticas", + "description": "Intervalo en segundos para publicar estadísticas del sistema y de las cámaras en MQTT." + }, + "user": { + "label": "Nombre de usuario MQTT", + "description": "Nombre de usuario MQTT opcional; puede proporcionarse mediante variables de entorno o secretos." + }, + "password": { + "label": "Contraseña MQTT", + "description": "Contraseña MQTT opcional; puede proporcionarse mediante variables de entorno o secretos." + }, + "tls_ca_certs": { + "label": "Certificados CA TLS", + "description": "Ruta al certificado CA para conexiones TLS con el broker (para certificados autofirmados)." + }, + "tls_client_key": { + "label": "Clave de cliente", + "description": "Ruta de la clave privada del certificado de cliente." + }, + "tls_insecure": { + "label": "TLS inseguro", + "description": "Permite conexiones TLS inseguras omitiendo la verificación del nombre de host (no recomendado)." + }, + "qos": { + "label": "QoS de MQTT", + "description": "Nivel de calidad de servicio para publicaciones/suscripciones MQTT (0, 1 o 2)." + } + }, + "notifications": { + "email": { + "label": "Email de notificacion", + "description": "Dirección de correo electrónico usada para notificaciones push o requerida por ciertos proveedores de notificaciones." + }, + "label": "Notificaciones", + "enabled": { + "label": "Habilitar notificaciones", + "description": "Habilita o deshabilita las notificaciones para todas las cámaras; se puede sobrescribir por cámara." + }, + "cooldown": { + "label": "Periodo de enfriamiento", + "description": "Periodo de enfriamiento (segundos) entre notificaciones para evitar saturar a los destinatarios." + }, + "enabled_in_config": { + "label": "Estado original de notificaciones", + "description": "Indica si las notificaciones estaban habilitadas en la configuración estática original." + }, + "description": "Ajustes para habilitar y controlar las notificaciones de todas las cámaras; se pueden sobrescribir por cámara." + }, + "networking": { + "ipv6": { + "label": "Configuración IPV6", + "description": "Ajustes específicos de IPv6 para los servicios de red de Frigate.", + "enabled": { + "label": "Habilitar IPv6", + "description": "Habilita la compatibilidad con IPv6 para los servicios de Frigate (API e interfaz) cuando corresponda." + } + }, + "listen": { + "internal": { + "label": "Puerto interno", + "description": "Puerto de escucha interno de Frigate (predeterminado 5000)." + }, + "external": { + "label": "Puerto externo", + "description": "Puerto externo de escucha para Frigate (por defecto 8791)." + }, + "description": "Configuración de los puertos de escucha internos y externos. Esto es para usuarios avanzados. Para la mayoría de los casos de uso, se recomienda modificar la sección de puertos de su configuración de Docker Compose.", + "label": "Configuración de puertos de escucha" + }, + "label": "Red", + "description": "Ajustes relacionados con la red, como la habilitación de IPv6 para los endpoints de Frigate." + }, + "proxy": { + "label": "Proxy", + "separator": { + "label": "Carácter de separación", + "description": "Carácter usado para separar varios valores proporcionados en las cabeceras del proxy." + }, + "default_role": { + "description": "Rol predeterminado asignado a los usuarios autenticados mediante proxy cuando no se aplica ninguna asignación de roles.", + "label": "Rol predeterminado" + }, + "description": "Configuración para integrar Frigate detrás de un proxy inverso que transmite encabezados de usuario autenticados.", + "header_map": { + "description": "Mapear los encabezados de proxy entrantes a los campos de usuario y rol de Frigate para la autenticación basada en proxy.", + "role": { + "description": "Encabezado que contiene el rol o los grupos del usuario autenticado provenientes del proxy ascendente.", + "label": "Cabecera de rol" + }, + "label": "Asignación de cabeceras", + "user": { + "label": "Cabecera de usuario", + "description": "Cabecera que contiene el nombre de usuario autenticado proporcionado por el proxy ascendente." + }, + "role_map": { + "label": "Asignación de roles", + "description": "Asigna valores de grupos ascendentes a roles de Frigate (por ejemplo, asignar grupos de administradores al rol de administrador)." + } + }, + "logout_url": { + "label": "URL de cierre de sesión", + "description": "URL a la que redirigir a los usuarios al cerrar sesión mediante el proxy." + }, + "auth_secret": { + "label": "Secreto del proxy", + "description": "Secreto opcional que se comprueba con la cabecera X-Proxy-Secret para verificar proxies de confianza." + } + }, + "telemetry": { + "label": "Telemetria", + "stats": { + "intel_gpu_stats": { + "label": "Estadísticas GPU Intel", + "description": "Habilitar la recopilación de estadísticas de la GPU Intel si hay una GPU Intel presente." + }, + "network_bandwidth": { + "label": "Ancho de banda", + "description": "Habilita la monitorización del ancho de banda de red por proceso para procesos ffmpeg de cámaras y detectores (requiere capacidades)." + }, + "amd_gpu_stats": { + "label": "Estadísticas GPU Amd", + "description": "Habilitar la recopilación de estadísticas de la GPU AMD si hay una GPU AMD presente." + }, + "intel_gpu_device": { + "description": "Dirección del bus PCI o ruta del dispositivo DRM (p. ej., /dev/dri/card1) usada para fijar las estadísticas de la GPU Intel a un dispositivo concreto cuando hay varios presentes.", + "label": "Dispositivo GPU Intel" + }, + "label": "Estadísticas del sistema", + "description": "Opciones para habilitar/deshabilitar la recopilación de distintas estadísticas del sistema y de la GPU." + }, + "version_check": { + "description": "Habilite una verificación saliente para detectar si hay disponible una versión más reciente de Frigate.", + "label": "Comprobación de versión" + }, + "description": "Opciones de telemetría y estadísticas del sistema, incluida la monitorización de GPU y ancho de banda de red.", + "network_interfaces": { + "label": "Interfaces de red", + "description": "Lista de prefijos de nombres de interfaces de red que se monitorizarán para estadísticas de ancho de banda." + } + }, + "ui": { + "timezone": { + "label": "Uso horario", + "description": "Zona horaria opcional que se mostrará en la interfaz de usuario (si no se especifica, se utilizará la hora local del navegador)." + }, + "unit_system": { + "label": "Unidad de sistema", + "description": "Sistema de unidades para la visualización (métrico o imperial) utilizado en la interfaz de usuario y en MQTT." + }, + "label": "Interfaz", + "description": "Preferencias de la interfaz de usuario, como zona horaria, formato de fecha/hora y unidades.", + "time_format": { + "label": "Formato de hora", + "description": "Formato de hora que se usará en la interfaz (browser, 12hour o 24hour)." + }, + "date_style": { + "label": "Estilo de fecha", + "description": "Estilo de fecha que se usará en la interfaz (full, long, medium, short)." + }, + "time_style": { + "label": "Estilo de hora", + "description": "Estilo de hora que se usará en la interfaz (full, long, medium, short)." + } + }, + "audio_transcription": { + "description": "Configuración para la transcripción de audio en directo y de voz utilizada para eventos y subtítulos en directo.", + "language": { + "description": "Código de idioma utilizado para la transcripción/traducción (por ejemplo, 'es' para Español). Consulte https://whisper-api.com/docs/languages/ para ver los códigos de idioma compatibles.", + "label": "Idioma de transcripción" + }, + "enabled": { + "description": "Habilitar o deshabilitar la transcripción automática de audio para todas las cámaras; puede anularse por cámara.", + "label": "Habilitar transcripción de audio" + }, + "label": "Transcripción de audio", + "live_enabled": { + "label": "Transcripción en directo", + "description": "Habilitar la transcripción en directo del audio a medida que se recibe." + }, + "device": { + "label": "Dispositivo de transcripción", + "description": "Clave del dispositivo (CPU/GPU) donde ejecutar el modelo de transcripción. Actualmente, solo se admiten GPU NVIDIA CUDA para la transcripción." + }, + "model_size": { + "label": "Tamaño del modelo", + "description": "Tamaño del modelo que se usará para la transcripción sin conexión de eventos de audio." + } + }, + "motion": { + "skip_motion_threshold": { + "description": "Si se establece en un valor entre 0,0 y 1,0, y más de esta fracción de la imagen cambia en un solo fotograma, el detector no devolverá cuadros de movimiento y se recalibrará inmediatamente. Esto puede ahorrar recursos de CPU y reducir los falsos positivos durante tormentas eléctricas, tempestades, etc., aunque podría pasar por alto eventos reales, como el seguimiento automático de un objeto por parte de una cámara PTZ. La disyuntiva está entre descartar unos cuantos megabytes de grabaciones o revisar un par de clips cortos. Deje este parámetro sin establecer (None) para desactivar esta función.", + "label": "Omitir umbral de movimiento" + }, + "lightning_threshold": { + "description": "Umbral para detectar e ignorar breves picos de luz (un valor menor indica mayor sensibilidad; valores entre 0,3 y 1,0). Esto no impide por completo la detección de movimiento; Simplemente provoca que el detector deje de analizar fotogramas adicionales una vez que se supera el umbral. Durante estos eventos aún se realizan grabaciones basadas en el movimiento.", + "label": "Umbral de iluminación" + }, + "threshold": { + "description": "Umbral de diferencia de píxeles utilizado por el detector de movimiento; los valores más altos reducen la sensibilidad (rango 1-255).", + "label": "Umbral de movimiento" + }, + "enabled": { + "description": "Habilitar o deshabilitar la detección de movimiento para todas las cámaras; puede anularse para cada cámara individualmente.", + "label": "Habilitar detección de movimiento" + }, + "label": "Detección de movimiento", + "improve_contrast": { + "label": "Mejorar contraste", + "description": "Aplica una mejora de contraste a los fotogramas antes del análisis de movimiento para ayudar a la detección." + }, + "contour_area": { + "label": "Área de contorno", + "description": "Área mínima de contorno en píxeles necesaria para que se cuente un contorno de movimiento." + }, + "delta_alpha": { + "label": "Delta alfa", + "description": "Factor de mezcla alfa usado en la diferencia entre fotogramas para calcular el movimiento." + }, + "frame_alpha": { + "label": "Alfa del fotograma", + "description": "Valor alfa usado al mezclar fotogramas para el preprocesamiento de movimiento." + }, + "frame_height": { + "label": "Altura del fotograma", + "description": "Altura en píxeles a la que escalar los fotogramas al calcular el movimiento." + }, + "mask": { + "label": "Coordenadas de máscara", + "description": "Coordenadas x,y ordenadas que definen el polígono de máscara de movimiento usado para incluir/excluir áreas." + }, + "mqtt_off_delay": { + "label": "Retraso de apagado MQTT", + "description": "Segundos a esperar tras el último movimiento antes de publicar un estado MQTT 'off'." + }, + "enabled_in_config": { + "label": "Estado de movimiento original", + "description": "Indica si la detección de movimiento estaba habilitada en la configuración estática original." + }, + "raw_mask": { + "label": "Máscara sin procesar" + }, + "description": "Ajustes predeterminados de detección de movimiento aplicados a las cámaras salvo que se sobrescriban por cámara." + }, + "lpr": { + "enhancement": { + "description": "Nivel de mejora (0-10) que se aplicará a los recortes de matrículas antes del OCR; los valores más altos no siempre mejoran los resultados, y los niveles superiores a 5 podrían funcionar únicamente con matrículas capturadas de noche, por lo que deben utilizarse con precaución.", + "label": "Nivel de mejora" + }, + "expire_time": { + "description": "Tiempo en segundos tras el cual una matrícula no detectada caduca en el sistema de seguimiento (solo para cámaras LPR dedicadas).", + "label": "Segundos hasta caducar" + }, + "enabled": { + "description": "Habilitar o deshabilitar el reconocimiento de matrículas para todas las cámaras; puede anularse por cámara.", + "label": "Habilitar LPR" + }, + "min_plate_length": { + "description": "Número mínimo de caracteres que debe contener una matrícula reconocida para ser considerada válida.", + "label": "Longitud mínima de matrícula" + }, + "label": "Reconocimiento de matrículas", + "description": "Ajustes de reconocimiento de matrículas, incluidos umbrales de detección, formato y matrículas conocidas.", + "min_area": { + "label": "Área mínima de matrícula", + "description": "Área mínima de matrícula (píxeles) necesaria para intentar el reconocimiento." + }, + "model_size": { + "label": "Tamaño del modelo", + "description": "Tamaño del modelo usado para detección/reconocimiento de texto. La mayoría de usuarios debería usar 'small'." + }, + "detection_threshold": { + "label": "Umbral de detección", + "description": "Umbral de confianza de detección para empezar a ejecutar OCR en una matrícula sospechosa." + }, + "recognition_threshold": { + "label": "Umbral de reconocimiento", + "description": "Umbral de confianza necesario para adjuntar el texto de matrícula reconocido como subetiqueta." + }, + "format": { + "label": "Regex de formato de matrícula", + "description": "Regex opcional para validar cadenas de matrícula reconocidas frente a un formato esperado." + }, + "match_distance": { + "label": "Distancia de coincidencia", + "description": "Número de diferencias de caracteres permitidas al comparar matrículas detectadas con matrículas conocidas." + }, + "known_plates": { + "label": "Matrículas conocidas", + "description": "Lista de matrículas o regexes que se rastrearán especialmente o sobre las que se alertará." + }, + "debug_save_plates": { + "label": "Guardar matrículas de depuración", + "description": "Guarda imágenes recortadas de matrículas para depurar el rendimiento de LPR." + }, + "device": { + "label": "Dispositivo", + "description": "Esto es una sobrescritura para apuntar a un dispositivo concreto. Consulta https://onnxruntime.ai/docs/execution-providers/ para obtener más información" + }, + "replace_rules": { + "label": "Reglas de sustitución", + "description": "Reglas de sustitución regex usadas para normalizar cadenas de matrícula detectadas antes de compararlas.", + "pattern": { + "label": "Patrón regex" + }, + "replacement": { + "label": "Cadena de sustitución" + } + } + }, + "detect": { + "fps": { + "description": "Fotogramas por segundo deseados para ejecutar la detección; los valores más bajos reducen el uso de la CPU (el valor recomendado es 5; establezca un valor superior —como máximo de 10— únicamente si realiza el seguimiento de objetos que se mueven con extrema rapidez).", + "label": "FPS de detección" + }, + "min_initialized": { + "description": "Número de detecciones consecutivas requeridas antes de crear un objeto rastreado. Auméntelo para reducir las inicializaciones falsas. El valor predeterminado es los FPS divididos por 2.", + "label": "Fotogramas mínimos de inicialización" + }, + "height": { + "description": "Altura (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.", + "label": "Altura de detección" + }, + "width": { + "description": "Ancho (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.", + "label": "Anchura de detección" + }, + "stationary": { + "description": "Configuración para detectar y gestionar objetos que permanecen inmóviles durante un periodo de tiempo.", + "label": "Configuración de objetos estacionarios", + "interval": { + "label": "Intervalo estacionario", + "description": "Frecuencia (en fotogramas) con la que se ejecuta una comprobación de detección para confirmar un objeto estacionario." + }, + "threshold": { + "label": "Umbral estacionario", + "description": "Número de fotogramas sin cambio de posición necesarios para marcar un objeto como estacionario." + }, + "max_frames": { + "label": "Fotogramas máximos", + "description": "Limita durante cuánto tiempo se rastrean los objetos estacionarios antes de descartarlos.", + "default": { + "label": "Fotogramas máximos predeterminados", + "description": "Número máximo predeterminado de fotogramas para rastrear un objeto estacionario antes de detenerse." + }, + "objects": { + "label": "Fotogramas máximos por objeto", + "description": "Sobrescrituras por objeto para el número máximo de fotogramas en los que rastrear objetos estacionarios." + } + }, + "classifier": { + "label": "Habilitar clasificador visual", + "description": "Usa un clasificador visual para detectar objetos realmente estacionarios incluso cuando los cuadros delimitadores oscilan." + } + }, + "enabled": { + "description": "Habilitar o deshabilitar la detección de objetos para todas las cámaras; puede anularse para cada cámara individualmente.", + "label": "Habilitar detección de objetos" + }, + "label": "Detección de objetos", + "description": "Ajustes del rol de detección/detect usado para ejecutar la detección de objetos e inicializar los rastreadores.", + "max_disappeared": { + "label": "Fotogramas máximos desaparecido", + "description": "Número de fotogramas sin detección antes de que un objeto rastreado se considere desaparecido." + }, + "annotation_offset": { + "label": "Desplazamiento de anotaciones", + "description": "Milisegundos para desplazar las anotaciones de detección y alinear mejor los cuadros delimitadores de la línea de tiempo con las grabaciones; puede ser positivo o negativo." + } + }, + "record": { + "motion": { + "description": "Número de días para conservar las grabaciones activadas por movimiento, independientemente de los objetos rastreados. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.", + "label": "Retención de movimiento", + "days": { + "label": "Días de retención", + "description": "Días durante los que conservar las grabaciones." + } + }, + "continuous": { + "description": "Número de días para conservar las grabaciones, independientemente de los objetos rastreados o del movimiento. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.", + "label": "Retención continua", + "days": { + "label": "Días de retención", + "description": "Días durante los que conservar las grabaciones." + } + }, + "detections": { + "pre_capture": { + "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura previa" + }, + "post_capture": { + "description": "Número de segundos después del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura posterior" + }, + "label": "Retención de detección", + "description": "Ajustes de retención de grabaciones para eventos de detección, incluidas las duraciones de captura previa/posterior.", + "retain": { + "label": "Retención de eventos", + "description": "Ajustes de retención para grabaciones de eventos de detección.", + "days": { + "label": "Días de retención", + "description": "Número de días durante los que conservar grabaciones de eventos de detección." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + } + } + }, + "alerts": { + "pre_capture": { + "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura previa" + }, + "post_capture": { + "description": "Número de segundos después del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura posterior" + }, + "label": "Retención de alertas", + "description": "Ajustes de retención de grabaciones para eventos de alerta, incluidas las duraciones de captura previa/posterior.", + "retain": { + "label": "Retención de eventos", + "description": "Ajustes de retención para grabaciones de eventos de detección.", + "days": { + "label": "Días de retención", + "description": "Número de días durante los que conservar grabaciones de eventos de detección." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + } + } + }, + "label": "Grabación", + "enabled": { + "label": "Habilitar grabación", + "description": "Habilita o deshabilita la grabación para todas las cámaras; se puede sobrescribir por cámara." + }, + "expire_interval": { + "label": "Intervalo de limpieza de grabaciones", + "description": "Minutos entre pasadas de limpieza que eliminan segmentos de grabación caducados." + }, + "export": { + "label": "Configuración de exportación", + "description": "Ajustes usados al exportar grabaciones, como timelapse y aceleración por hardware.", + "hwaccel_args": { + "label": "Argumentos hwaccel de exportación", + "description": "Argumentos de aceleración por hardware que se usarán en operaciones de exportación/transcodificación." + }, + "max_concurrent": { + "label": "Exportaciones simultáneas máximas", + "description": "Número máximo de trabajos de exportación que se procesarán al mismo tiempo." + } + }, + "preview": { + "label": "Configuración de vista previa", + "description": "Ajustes que controlan la calidad de las vistas previas de grabaciones mostradas en la interfaz.", + "quality": { + "label": "Calidad de vista previa", + "description": "Nivel de calidad de vista previa (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Estado de grabación original", + "description": "Indica si la grabación estaba habilitada en la configuración estática original." + }, + "description": "Ajustes de grabación y retención aplicados a las cámaras salvo que se sobrescriban por cámara." + }, + "camera_ui": { + "dashboard": { + "description": "Alterna si esta cámara es visible en toda la interfaz de usuario de Frigate. Desactivar esta opción requerirá editar manualmente la configuración para volver a visualizar esta cámara en la interfaz.", + "label": "Mostrar en la interfaz" + }, + "label": "Interfaz de cámara", + "description": "Orden de visualización y visibilidad de esta cámara en la interfaz. El orden afecta al panel predeterminado. Para un control más granular, usa grupos de cámaras.", + "order": { + "label": "Orden en la interfaz", + "description": "Orden numérico usado para ordenar la cámara en la interfaz (panel predeterminado y listas); los números más altos aparecen más tarde." + }, + "review": { + "label": "Mostrar en Revisión", + "description": "Activa o desactiva si esta cámara se muestra en Revisión (la página de Revisión y su filtro de cámaras, la revisión de movimiento y la vista de historial)." + } + }, + "live": { + "description": "Configuración para controlar la resolución y la calidad de la transmisión en vivo de jsmpeg. Esto no afecta a las cámaras retransmitidas que utilizan go2rtc para la visualización en vivo.", + "height": { + "description": "Altura (en píxeles) para renderizar la transmisión en vivo de jsmpeg en la interfaz web; debe ser <= a la altura de la transmisión de detección.", + "label": "Altura en directo" + }, + "label": "Reproducción en directo", + "streams": { + "label": "Nombres de flujos en directo", + "description": "Asignación de nombres de flujos configurados a nombres de restream/go2rtc usados para la reproducción en directo." + }, + "quality": { + "label": "Calidad en directo", + "description": "Calidad de codificación para el flujo jsmpeg (1 la más alta, 31 la más baja)." + } + }, + "semantic_search": { + "model": { + "description": "El modelo de embeddings a utilizar para la búsqueda semántica (por ejemplo, 'jinav1'), o el nombre de un proveedor de GenAI con el rol de embeddings.", + "label": "Modelo de búsqueda semántica o nombre del proveedor GenAI" + }, + "label": "Búsqueda semántica", + "triggers": { + "label": "Activadores", + "description": "Acciones y criterios de coincidencia para activadores de búsqueda semántica específicos de la cámara.", + "friendly_name": { + "label": "Nombre descriptivo", + "description": "Nombre descriptivo opcional mostrado en la interfaz para este activador." + }, + "enabled": { + "label": "Habilitar este activador", + "description": "Habilita o deshabilita este activador de búsqueda semántica." + }, + "type": { + "label": "Tipo de activador", + "description": "Tipo de activador: 'thumbnail' (coincidir con imagen) o 'description' (coincidir con texto)." + }, + "data": { + "label": "Contenido del activador", + "description": "Frase de texto o ID de miniatura que se comparará con objetos rastreados." + }, + "threshold": { + "label": "Umbral del activador", + "description": "Puntuación mínima de similitud (0-1) necesaria para activar este activador." + }, + "actions": { + "label": "Acciones del activador", + "description": "Lista de acciones que se ejecutarán cuando el activador coincida (notification, sub_label, attribute)." + } + }, + "description": "Ajustes de la búsqueda semántica, que crea y consulta embeddings de objetos para encontrar elementos similares.", + "enabled": { + "label": "Habilitar búsqueda semántica", + "description": "Habilita o deshabilita la función de búsqueda semántica." + }, + "reindex": { + "label": "Reindexar al iniciar", + "description": "Activa una reindexación completa de los objetos rastreados históricos en la base de datos de embeddings." + }, + "model_size": { + "label": "Tamaño del modelo", + "description": "Selecciona el tamaño del modelo; 'small' se ejecuta en CPU y 'large' normalmente requiere GPU." + }, + "device": { + "label": "Dispositivo", + "description": "Esto es una sobrescritura para apuntar a un dispositivo concreto. Consulta https://onnxruntime.ai/docs/execution-providers/ para obtener más información" + } + }, + "review": { + "alerts": { + "required_zones": { + "description": "Zonas en las que debe entrar un objeto para ser considerado una alerta; dejar vacío para permitir cualquier zona.", + "label": "Zonas requeridas" + }, + "labels": { + "description": "Lista de etiquetas de objetos que califican como alertas (por ejemplo: car, person).", + "label": "Etiquetas de alerta" + }, + "label": "Configuración de alertas", + "description": "Ajustes sobre qué objetos rastreados generan alertas y cómo se conservan las alertas.", + "enabled": { + "label": "Habilitar alertas", + "description": "Habilita o deshabilita la generación de alertas para todas las cámaras; se puede sobrescribir por cámara." + }, + "enabled_in_config": { + "label": "Estado original de alertas", + "description": "Rastrea si las alertas estaban habilitadas originalmente en la configuración estática." + }, + "cutoff_time": { + "label": "Tiempo de corte de alertas", + "description": "Segundos que se esperarán tras dejar de haber actividad causante de alerta antes de cortar una alerta." + } + }, + "detections": { + "required_zones": { + "description": "Zonas en las que debe entrar un objeto para ser considerado detectado; dejar vacío para permitir cualquier zona.", + "label": "Zonas requeridas" + }, + "description": "Configuración para determinar qué objetos rastreados generan detecciones (no alertas) y cómo se retienen dichas detecciones.", + "label": "Configuración de detecciones", + "enabled": { + "label": "Habilitar detecciones", + "description": "Habilita o deshabilita los eventos de detección para todas las cámaras; se puede sobrescribir por cámara." + }, + "labels": { + "label": "Etiquetas de detección", + "description": "Lista de etiquetas de objetos que cuentan como eventos de detección." + }, + "cutoff_time": { + "label": "Tiempo de corte de detecciones", + "description": "Segundos que se esperarán tras dejar de haber actividad causante de detección antes de cortar una detección." + }, + "enabled_in_config": { + "label": "Estado original de detecciones", + "description": "Rastrea si las detecciones estaban habilitadas originalmente en la configuración estática." + } + }, + "genai": { + "image_source": { + "description": "Fuente de las imágenes enviadas a GenAI ('preview' o 'recordings'); La opción 'recordings' utiliza fotogramas de mayor calidad, pero requiere más tokens.", + "label": "Origen de imagen de revisión" + }, + "additional_concerns": { + "description": "Una lista de preocupaciones o notas adicionales que GenAI debería tener en cuenta al evaluar la actividad en esta cámara.", + "label": "Consideraciones adicionales" + }, + "activity_context_prompt": { + "description": "Instrucción personalizada que describe qué constituye y qué no una actividad sospechosa, con el fin de proporcionar contexto para los resúmenes generados por GenAI.", + "label": "Prompt de contexto de actividad" + }, + "description": "Controla el uso de IA generativa (GenAI) para la elaboración de descripciones y resúmenes de elementos de revisión.", + "debug_save_thumbnails": { + "description": "Guarde las miniaturas que se envían al proveedor de GenAI para su depuración y revisión.", + "label": "Guardar miniaturas" + }, + "label": "Configuración de GenAI", + "enabled": { + "label": "Habilitar descripciones de GenAI", + "description": "Habilita o deshabilita las descripciones y resúmenes generados por GenAI para los elementos de revisión." + }, + "alerts": { + "label": "Habilitar GenAI para alertas", + "description": "Usa GenAI para generar descripciones de elementos de alerta." + }, + "detections": { + "label": "Habilitar GenAI para detecciones", + "description": "Usa GenAI para generar descripciones de elementos de detección." + }, + "enabled_in_config": { + "label": "Estado original de GenAI", + "description": "Rastrea si la revisión de GenAI estaba habilitada originalmente en la configuración estática." + }, + "preferred_language": { + "label": "Idioma preferido", + "description": "Idioma preferido que se solicitará al proveedor de GenAI para las respuestas generadas." + } + }, + "label": "Revisión", + "description": "Ajustes que controlan alertas, detecciones y resúmenes de revisión de GenAI usados por la interfaz y el almacenamiento." + }, + "birdseye": { + "description": "Configuración para la vista compuesta Birdseye, que combina las transmisiones de múltiples cámaras en una sola vista.", + "restream": { + "description": "Retransmita la salida de video de Birdseye como una transmisión en vivo RTSP; al habilitar esta opción, Birdseye se mantendrá en ejecución de forma continua.", + "label": "Retransmisión RTSP" + }, + "layout": { + "max_cameras": { + "description": "Número máximo de cámaras a mostrar simultáneamente en Birdseye; muestra las cámaras más recientes.", + "label": "Cámaras máximas" + }, + "label": "Diseño", + "description": "Opciones de diseño para la composición de Birdseye.", + "scaling_factor": { + "label": "Factor de escala", + "description": "Factor de escala usado por el calculador de diseño (rango de 1.0 a 5.0)." + } + }, + "label": "Birdseye", + "enabled": { + "label": "Habilitar Birdseye", + "description": "Habilita o deshabilita la función de vista Birdseye." + }, + "mode": { + "label": "Modo de seguimiento", + "description": "Modo para incluir cámaras en Birdseye: 'objects', 'motion' o 'continuous'." + }, + "order": { + "label": "Posición", + "description": "Posición numérica que controla el orden de la cámara en el diseño de Birdseye." + }, + "width": { + "label": "Anchura", + "description": "Anchura de salida (píxeles) del fotograma compuesto de Birdseye." + }, + "height": { + "label": "Altura", + "description": "Altura de salida (píxeles) del fotograma compuesto de Birdseye." + }, + "quality": { + "label": "Calidad de codificación", + "description": "Calidad de codificación para el flujo mpeg1 de Birdseye (1 la calidad más alta, 31 la más baja)." + }, + "inactivity_threshold": { + "label": "Umbral de inactividad", + "description": "Segundos de inactividad tras los cuales una cámara dejará de mostrarse en Birdseye." + }, + "idle_heartbeat_fps": { + "label": "FPS de latido en reposo", + "description": "Fotogramas por segundo para reenviar el último fotograma compuesto de Birdseye en reposo; establécelo en 0 para deshabilitarlo." + } + }, + "ffmpeg": { + "retry_interval": { + "description": "Segundos de espera antes de intentar reconectar la transmisión de una cámara tras un fallo. El valor predeterminado es 10.", + "label": "Tiempo de reintento de FFmpeg" + }, + "path": { + "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"7.0\" o \"8.0\").", + "label": "Ruta de FFmpeg" + }, + "output_args": { + "description": "Argumentos de salida predeterminados utilizados para diferentes roles de FFmpeg, tales como detección y grabación.", + "label": "Argumentos de salida", + "detect": { + "label": "Argumentos de salida de detección", + "description": "Argumentos de salida predeterminados para los flujos con rol de detección." + }, + "record": { + "label": "Argumentos de salida de grabación", + "description": "Argumentos de salida predeterminados para los flujos con rol de grabación." + } + }, + "description": "Configuración de FFmpeg, incluyendo la ruta del binario, argumentos, opciones de aceleración por hardware y argumentos de salida por rol.", + "label": "FFmpeg", + "global_args": { + "label": "Argumentos globales de FFmpeg", + "description": "Argumentos globales pasados a los procesos de FFmpeg." + }, + "hwaccel_args": { + "label": "Argumentos de aceleración por hardware", + "description": "Argumentos de aceleración por hardware para FFmpeg. Se recomiendan preajustes específicos del proveedor." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada aplicados a los flujos de entrada de FFmpeg." + }, + "apple_compatibility": { + "label": "Compatibilidad con Apple", + "description": "Habilita el etiquetado HEVC para mejorar la compatibilidad con reproductores de Apple al grabar H.265." + }, + "gpu": { + "label": "Índice de GPU", + "description": "Índice de GPU predeterminado usado para la aceleración por hardware si está disponible." + }, + "inputs": { + "label": "Entradas de cámara", + "description": "Lista de definiciones de flujos de entrada (rutas y roles) para esta cámara.", + "path": { + "label": "Ruta de entrada", + "description": "URL o ruta del flujo de entrada de la cámara." + }, + "roles": { + "label": "Roles de entrada", + "description": "Roles para este flujo de entrada." + }, + "global_args": { + "label": "Argumentos globales de FFmpeg", + "description": "Argumentos globales de FFmpeg para este flujo de entrada." + }, + "hwaccel_args": { + "label": "Argumentos de aceleración por hardware", + "description": "Argumentos de aceleración por hardware para este flujo de entrada." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada específicos para este flujo." + } + } + }, + "go2rtc": { + "description": "Configuración del servicio integrado de retransmisión go2rtc, utilizado para el relevo y la traducción de transmisiones en vivo.", + "label": "go2rtc" + }, + "genai": { + "description": "Configuración para los proveedores integrados de IA generativa (GenAI) utilizados para generar descripciones de objetos y resúmenes de reseñas.", + "api_key": { + "description": "Clave de API requerida por algunos proveedores (también puede configurarse mediante variables de entorno).", + "label": "Clave API" + }, + "base_url": { + "description": "URL base para proveedores autoalojados o compatibles (por ejemplo, una instancia de Ollama).", + "label": "URL base" + }, + "model": { + "description": "El modelo del proveedor que se utilizará para generar descripciones o resúmenes.", + "label": "Modelo" + }, + "label": "Configuración de IA generativa", + "provider": { + "label": "Proveedor", + "description": "Proveedor de GenAI que se usará (por ejemplo: ollama, gemini, openai)." + }, + "roles": { + "label": "Roles", + "description": "Roles de GenAI (chat, descriptions, embeddings); un proveedor por rol." + }, + "provider_options": { + "label": "Opciones del proveedor", + "description": "Opciones adicionales específicas del proveedor que se pasarán al cliente GenAI." + }, + "runtime_options": { + "label": "Opciones de ejecución", + "description": "Opciones de ejecución pasadas al proveedor para cada llamada de inferencia." + } + }, + "face_recognition": { + "description": "Configuración para la detección y el reconocimiento facial en todas las cámaras; puede anularse por cámara.", + "label": "Reconocimiento facial", + "enabled": { + "label": "Habilitar reconocimiento facial", + "description": "Habilita o deshabilita el reconocimiento facial para todas las cámaras; se puede sobrescribir por cámara." + }, + "min_area": { + "label": "Área mínima de rostro", + "description": "Área mínima (píxeles) del cuadro de un rostro detectado necesaria para intentar el reconocimiento." + }, + "model_size": { + "label": "Tamaño del modelo", + "description": "Tamaño del modelo que se usará para embeddings faciales (small/large); el más grande puede requerir GPU." + }, + "unknown_score": { + "label": "Umbral de puntuación desconocida", + "description": "Umbral de distancia por debajo del cual un rostro se considera una posible coincidencia (más alto = más estricto)." + }, + "detection_threshold": { + "label": "Umbral de detección", + "description": "Confianza mínima de detección necesaria para considerar válida una detección de rostro." + }, + "recognition_threshold": { + "label": "Umbral de reconocimiento", + "description": "Umbral de distancia de embedding facial para considerar que dos rostros coinciden." + }, + "min_faces": { + "label": "Rostros mínimos", + "description": "Número mínimo de reconocimientos faciales necesarios antes de aplicar una subetiqueta reconocida a una persona." + }, + "save_attempts": { + "label": "Guardar intentos", + "description": "Número de intentos de reconocimiento facial que se conservarán para la interfaz de reconocimientos recientes." + }, + "blur_confidence_filter": { + "label": "Filtro de confianza por desenfoque", + "description": "Ajusta las puntuaciones de confianza según el desenfoque de la imagen para reducir falsos positivos en rostros de baja calidad." + }, + "device": { + "label": "Dispositivo", + "description": "Esto es una sobrescritura para apuntar a un dispositivo concreto. Consulta https://onnxruntime.ai/docs/execution-providers/ para obtener más información" + } + }, + "camera_mqtt": { + "required_zones": { + "description": "Zonas en las que debe entrar un objeto para que se publique una imagen MQTT.", + "label": "Zonas requeridas" + }, + "label": "MQTT", + "description": "Ajustes de publicación de imágenes MQTT.", + "enabled": { + "label": "Enviar imagen", + "description": "Habilita la publicación de instantáneas de objetos en temas MQTT para esta cámara." + }, + "timestamp": { + "label": "Añadir marca de tiempo", + "description": "Superpone una marca de tiempo en las imágenes publicadas en MQTT." + }, + "bounding_box": { + "label": "Añadir cuadro delimitador", + "description": "Dibuja cuadros delimitadores en las imágenes publicadas mediante MQTT." + }, + "crop": { + "label": "Recortar imagen", + "description": "Recorta las imágenes publicadas en MQTT al cuadro delimitador del objeto detectado." + }, + "height": { + "label": "Altura de imagen", + "description": "Altura (píxeles) a la que redimensionar las imágenes publicadas mediante MQTT." + }, + "quality": { + "label": "Calidad JPEG", + "description": "Calidad JPEG de las imágenes publicadas en MQTT (0-100)." + } + }, + "snapshots": { + "label": "Instantáneas", + "enabled": { + "label": "Habilitar instantáneas", + "description": "Habilita o deshabilita el guardado de instantáneas para todas las cámaras; se puede sobrescribir por cámara." + }, + "timestamp": { + "label": "Superposición de marca de tiempo", + "description": "Superpone una marca de tiempo en las instantáneas de la API." + }, + "bounding_box": { + "label": "Superposición de cuadro delimitador", + "description": "Dibuja cuadros delimitadores para los objetos rastreados en las instantáneas de la API." + }, + "crop": { + "label": "Recortar instantánea", + "description": "Recorta las instantáneas de la API al cuadro delimitador del objeto detectado." + }, + "required_zones": { + "label": "Zonas requeridas", + "description": "Zonas en las que debe entrar un objeto para que se guarde una instantánea." + }, + "height": { + "label": "Altura de instantánea", + "description": "Altura (píxeles) a la que redimensionar las instantáneas de la API; déjalo vacío para conservar el tamaño original." + }, + "retain": { + "label": "Retención de instantáneas", + "description": "Ajustes de retención de instantáneas, incluidos días predeterminados y sobrescrituras por objeto.", + "default": { + "label": "Retención predeterminada", + "description": "Número predeterminado de días durante los que conservar instantáneas." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + }, + "objects": { + "label": "Retención por objeto", + "description": "Sobrescrituras por objeto para los días de retención de instantáneas." + } + }, + "quality": { + "label": "Calidad de instantánea", + "description": "Calidad de codificación de las instantáneas guardadas (0-100)." + }, + "description": "Ajustes para instantáneas generadas por la API de objetos rastreados en todas las cámaras; se pueden sobrescribir por cámara." + }, + "timestamp_style": { + "label": "Estilo de marca de tiempo", + "position": { + "label": "Posición de marca de tiempo", + "description": "Posición de la marca de tiempo en la imagen (tl/tr/bl/br)." + }, + "format": { + "label": "Formato de marca de tiempo", + "description": "Cadena de formato de fecha y hora usada para las marcas de tiempo (códigos de formato datetime de Python)." + }, + "color": { + "label": "Color de marca de tiempo", + "description": "Valores de color RGB para el texto de la marca de tiempo (todos los valores 0-255).", + "red": { + "label": "Rojo", + "description": "Componente rojo (0-255) para el color de la marca de tiempo." + }, + "green": { + "label": "Verde", + "description": "Componente verde (0-255) para el color de la marca de tiempo." + }, + "blue": { + "label": "Azul", + "description": "Componente azul (0-255) para el color de la marca de tiempo." + } + }, + "thickness": { + "label": "Grosor de marca de tiempo", + "description": "Grosor de línea del texto de la marca de tiempo." + }, + "effect": { + "label": "Efecto de marca de tiempo", + "description": "Efecto visual para el texto de la marca de tiempo (none, solid, shadow)." + }, + "description": "Opciones de estilo para marcas de tiempo integradas aplicadas a la vista de depuración y a las instantáneas." + }, + "profiles": { + "label": "Perfiles", + "description": "Definiciones de perfiles con nombre y nombres descriptivos. Los perfiles de cámara deben hacer referencia a nombres definidos aquí.", + "friendly_name": { + "label": "Nombre descriptivo", + "description": "Nombre mostrado para este perfil en la interfaz." + } + }, + "tls": { + "label": "TLS", + "description": "Ajustes TLS para los endpoints web de Frigate (puerto 8971).", + "enabled": { + "label": "Habilitar TLS", + "description": "Habilita TLS para la interfaz web y la API de Frigate en el puerto TLS configurado." + } + }, + "model": { + "label": "Modelo de detección", + "description": "Ajustes para configurar un modelo de detección de objetos personalizado y su forma de entrada.", + "path": { + "label": "Ruta del modelo de detector de objetos personalizado", + "description": "Ruta a un archivo de modelo de detección personalizado (o plus:// para modelos de Frigate+)." + }, + "labelmap_path": { + "label": "Mapa de etiquetas para detector de objetos personalizado", + "description": "Ruta a un archivo labelmap que asigna clases numéricas a etiquetas de texto para el detector." + }, + "width": { + "label": "Anchura de entrada del modelo de detección de objetos", + "description": "Anchura del tensor de entrada del modelo en píxeles." + }, + "height": { + "label": "Altura de entrada del modelo de detección de objetos", + "description": "Altura del tensor de entrada del modelo en píxeles." + }, + "labelmap": { + "label": "Personalización del mapa de etiquetas", + "description": "Sobrescrituras o entradas de reasignación que se fusionarán con el mapa de etiquetas estándar." + }, + "attributes_map": { + "label": "Mapa de etiquetas de objetos a sus etiquetas de atributos", + "description": "Asignación de etiquetas de objetos a etiquetas de atributos usada para adjuntar metadatos (por ejemplo, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Forma del tensor de entrada del modelo", + "description": "Formato de tensor esperado por el modelo: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Formato de color de píxeles de entrada del modelo", + "description": "Espacio de color de píxeles esperado por el modelo: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipo D de entrada del modelo", + "description": "Tipo de datos del tensor de entrada del modelo (por ejemplo, 'float32')." + }, + "model_type": { + "label": "Tipo de modelo de detección de objetos", + "description": "Tipo de arquitectura del modelo detector (ssd, yolox, yolonas) usado por algunos detectores para optimización." + } + }, + "classification": { + "label": "Clasificación de objetos", + "description": "Ajustes de los modelos de clasificación usados para refinar etiquetas de objetos o clasificación de estado.", + "bird": { + "label": "Configuración de clasificación de aves", + "description": "Ajustes específicos de los modelos de clasificación de aves.", + "enabled": { + "label": "Clasificación de aves", + "description": "Habilita o deshabilita la clasificación de aves." + }, + "threshold": { + "label": "Puntuación mínima", + "description": "Puntuación mínima de clasificación necesaria para aceptar una clasificación de ave." + } + }, + "custom": { + "label": "Modelos de clasificación personalizados", + "description": "Configuración de modelos de clasificación personalizados usados para objetos o detección de estado.", + "enabled": { + "label": "Habilitar modelo", + "description": "Habilita o deshabilita el modelo de clasificación personalizado." + }, + "name": { + "label": "Nombre del modelo", + "description": "Identificador del modelo de clasificación personalizado que se usará." + }, + "threshold": { + "label": "Umbral de puntuación", + "description": "Umbral de puntuación usado para cambiar el estado de clasificación." + }, + "save_attempts": { + "label": "Guardar intentos", + "description": "Cuántos intentos de clasificación se guardarán para la interfaz de clasificaciones recientes." + }, + "object_config": { + "objects": { + "label": "Clasificar objetos", + "description": "Lista de tipos de objetos sobre los que ejecutar la clasificación de objetos." + }, + "classification_type": { + "label": "Tipo de clasificación", + "description": "Tipo de clasificación aplicado: 'sub_label' (añade sub_label) u otros tipos compatibles." + } + }, + "state_config": { + "cameras": { + "label": "Cámaras de clasificación", + "description": "Recorte y ajustes por cámara para ejecutar la clasificación de estado.", + "crop": { + "label": "Recorte de clasificación", + "description": "Coordenadas de recorte que se usarán para ejecutar la clasificación en esta cámara." + } + }, + "motion": { + "label": "Ejecutar con movimiento", + "description": "Si es true, ejecuta la clasificación cuando se detecte movimiento dentro del recorte especificado." + }, + "interval": { + "label": "Intervalo de clasificación", + "description": "Intervalo (segundos) entre ejecuciones periódicas de clasificación para la clasificación de estado." + } + } + } + }, + "camera_groups": { + "label": "Grupos de cámaras", + "description": "Configuración de grupos de cámaras con nombre usados para organizar cámaras en la interfaz.", + "cameras": { + "label": "Lista de cámaras", + "description": "Array de nombres de cámaras incluidos en este grupo." + }, + "icon": { + "label": "Icono de grupo", + "description": "Icono usado para representar el grupo de cámaras en la interfaz." + }, + "order": { + "label": "Orden de clasificación", + "description": "Orden numérico usado para ordenar grupos de cámaras en la interfaz; los números más altos aparecen más tarde." + } + }, + "active_profile": { + "label": "Perfil activo", + "description": "Nombre del perfil activo actualmente. Solo en tiempo de ejecución, no se conserva en YAML." + } +} diff --git a/web/public/locales/es/config/groups.json b/web/public/locales/es/config/groups.json new file mode 100644 index 0000000..a8cb25b --- /dev/null +++ b/web/public/locales/es/config/groups.json @@ -0,0 +1,73 @@ +{ + "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" + }, + "cameras": { + "resolution": "Resolución", + "tracking": "Seguimiento" + } + }, + "objects": { + "global": { + "tracking": "Seguimiento global", + "filtering": "Filtrado global" + }, + "cameras": { + "filtering": "Filtrado", + "tracking": "Seguimiento" + } + }, + "record": { + "global": { + "retention": "Retención global", + "events": "Eventos globales" + }, + "cameras": { + "events": "Evento", + "retention": "Retención" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Argumentos de FFmpeg específicos de la cámara" + } + } +} diff --git a/web/public/locales/es/config/validation.json b/web/public/locales/es/config/validation.json new file mode 100644 index 0000000..dc35c3e --- /dev/null +++ b/web/public/locales/es/config/validation.json @@ -0,0 +1,35 @@ +{ + "minimum": "Debe ser al menos {{limit}}", + "maximum": "Debe ser como mucho {{limit}}", + "exclusiveMinimum": "Debe ser mayor que {{limit}}", + "exclusiveMaximum": "Debe ser menor que {{limit}}", + "minLength": "Debe ser al menos {{limit}} carácter(es)", + "maxLength": "Debe ser como máximo {{limit}} carácter(es)", + "minItems": "Debe tener al menos {{limit}} objetos", + "maxItems": "Debe tener como máximo {{limit}} objetos", + "pattern": "Formato no válido", + "required": "Este campo es requerido", + "type": "Tipo de valor no válido", + "enum": "Debe ser uno de los valores permitidos", + "const": "El valor no coincide con la constante esperada", + "uniqueItems": "Todos los objetos deben ser únicos", + "format": "Formato no válido", + "additionalProperties": "No se permite una propiedad desconocida", + "oneOf": "Debe coincidir exactamente con uno de los esquemas permitidos", + "ffmpeg": { + "inputs": { + "rolesUnique": "Cada rol solo puede asignarse a un flujo de entrada.", + "detectRequired": "Al menos un flujo de entrada debe tener asignado el rol 'detect'.", + "hwaccelDetectOnly": "Solo el flujo de entrada con la función \"detect\" puede definir argumentos de aceleración por hardware." + } + }, + "anyOf": "Debe coincidir con al menos uno de los esquemas permitidos", + "proxy": { + "header_map": { + "roleHeaderRequired": "Se requiere el encabezado de rol cuando hay mapeos de roles configurados." + } + }, + "detect": { + "dimensionMustBeEven": "Debe ser un número par." + } +} diff --git a/web/public/locales/es/objects.json b/web/public/locales/es/objects.json new file mode 100644 index 0000000..94adda5 --- /dev/null +++ b/web/public/locales/es/objects.json @@ -0,0 +1,130 @@ +{ + "person": "Persona", + "bicycle": "Bicicleta", + "car": "Coche", + "motorcycle": "Motocicleta", + "airplane": "Avión", + "bus": "Autobus", + "train": "Tren", + "boat": "Barco", + "traffic_light": "Semáforo", + "fire_hydrant": "Boca de incendios", + "street_sign": "Señal de tráfico", + "stop_sign": "Señal de stop", + "parking_meter": "Parquímetro", + "bench": "Banco", + "dog": "Perro", + "cow": "Vaca", + "elephant": "Elefante", + "bear": "Oso", + "zebra": "Cebra", + "giraffe": "Jirafa", + "hat": "Sombrero", + "backpack": "Mochila", + "shoe": "Zapato", + "eye_glasses": "Gafas", + "handbag": "Bolso de mano", + "tie": "Corbata", + "suitcase": "Maleta", + "frisbee": "Disco Volador", + "skis": "Esquís", + "sports_ball": "Pelota deportiva", + "kite": "Cometa", + "baseball_glove": "Guante de béisbol", + "skateboard": "Monopatín", + "surfboard": "Tabla de surf", + "tennis_racket": "Raqueta de tenis", + "bottle": "Botella", + "plate": "Plato", + "wine_glass": "Copa de vino", + "cup": "Taza", + "fork": "Tenedor", + "spoon": "Cuchara", + "bowl": "Cuenco", + "apple": "Manzana", + "orange": "Naranja", + "broccoli": "Brócoli", + "carrot": "Zanahoria", + "hot_dog": "Perrito caliente", + "pizza": "Pizza", + "donut": "Rosquilla", + "chair": "Silla", + "couch": "Sofá", + "potted_plant": "Planta en maceta", + "bed": "Cama", + "mirror": "Espejo", + "dining_table": "Mesa de comedor", + "window": "Ventana", + "desk": "Escritorio", + "toilet": "Inodoro", + "door": "Puerta", + "laptop": "Portátil", + "mouse": "Ratón", + "remote": "Mando a distancia", + "keyboard": "Teclado", + "cell_phone": "Teléfono móvil", + "microwave": "Microondas", + "toaster": "Tostadora", + "sink": "Fregadero", + "refrigerator": "Frigorífico", + "blender": "Batidora", + "clock": "Reloj", + "vase": "Jarrón", + "scissors": "Tijeras", + "teddy_bear": "Osito de peluche", + "hair_dryer": "Secador de pelo", + "vehicle": "Vehículo", + "squirrel": "Ardilla", + "deer": "Ciervo", + "bark": "Ladrido", + "rabbit": "Conejo", + "face": "Rostro", + "license_plate": "Matrícula", + "package": "Paquete", + "bbq_grill": "Parrilla de barbacoa", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "umbrella": "Paraguas", + "horse": "Caballo", + "tv": "Televisión", + "on_demand": "Bajo demanda", + "toothbrush": "Cepillo de dientes", + "hair_brush": "Cepillo de pelo", + "amazon": "Amazon", + "sheep": "Oveja", + "bird": "Pájaro", + "knife": "Cuchillo", + "cake": "Tarta", + "baseball_bat": "Bate de béisbol", + "oven": "Horno", + "waste_bin": "Papelera", + "snowboard": "Tabla de Snow", + "sandwich": "Sandwich", + "fox": "Zorro", + "nzpost": "NZPost", + "cat": "Gato", + "banana": "Plátano", + "book": "Libro", + "raccoon": "Mapache", + "dpd": "DPD", + "goat": "Cabra", + "robot_lawnmower": "Cortacésped robotizado", + "animal": "Animal", + "postnord": "PostNord", + "usps": "USPS", + "gls": "GLS", + "canada_post": "Canada Post", + "royal_mail": "Royal Mail", + "school_bus": "Autobús escolar", + "skunk": "Mofeta", + "kangaroo": "Canguro", + "baby": "Bebé", + "baby_stroller": "Cochecito de bebé", + "rickshaw": "Rickshaw", + "Rodent": "Roedor", + "rodent": "Roedor" +} diff --git a/web/public/locales/es/views/chat.json b/web/public/locales/es/views/chat.json new file mode 100644 index 0000000..7fe10c7 --- /dev/null +++ b/web/public/locales/es/views/chat.json @@ -0,0 +1,72 @@ +{ + "documentTitle": "Chat - Frigate", + "title": "Frigate Chat", + "subtitle": "Tu asistente de IA para la gestión de cámaras y análisis", + "placeholder": "Pregunta cualquier cosa...", + "error": "Algo salió mal. Por favor, inténtalo de nuevo.", + "processing": "Procesando...", + "toolsUsed": "Usado: {{tools}}", + "showTools": "Mostrar herramientas ({{count}})", + "hideTools": "Ocultar herramientas", + "call": "Llamar", + "result": "Resultado", + "arguments": "Argumentos:", + "response": "Respuesta:", + "attachment_chip_label": "{{label}} en {{camera}}", + "attachment_chip_remove": "Eliminar adjunto", + "open_in_explore": "Abrir en Explorar", + "attach_event_aria": "Adjuntar evento {{eventId}}", + "attachment_picker_paste_label": "O pega el ID del evento", + "attachment_picker_attach": "Adjuntar", + "attachment_picker_placeholder": "Adjuntar un evento", + "quick_reply_find_similar": "Buscar avistamientos similares", + "quick_reply_tell_me_more": "Cuéntame más sobre esto", + "quick_reply_when_else": "¿Cuándo más se vio?", + "quick_reply_find_similar_text": "Buscar avistamientos similares a este.", + "quick_reply_tell_me_more_text": "Cuéntame más sobre este.", + "quick_reply_when_else_text": "¿Cuándo más se vio esto?", + "anchor": "Referencia", + "similarity_score": "Similitud", + "no_similar_objects_found": "No se encontraron objetos similares.", + "semantic_search_required": "La búsqueda semántica debe estar activada para encontrar objetos similares.", + "send": "Enviar", + "suggested_requests": "Prueba preguntando:", + "starting_requests": { + "show_recent_events": "Mostrar eventos recientes", + "show_camera_status": "Mostrar estado de la cámara", + "recap": "¿Qué ha pasado mientras estaba fuera?", + "watch_camera": "Vigilar una cámara en busca de actividad" + }, + "starting_requests_prompts": { + "show_recent_events": "Muéstrame los eventos recientes de la última hora", + "show_camera_status": "¿Cuál es el estado actual de mis cámaras?", + "recap": "¿Qué ha pasado mientras estaba fuera?", + "watch_camera": "Vigila la puerta principal y avísame si aparece alguien" + }, + "new_chat": "Nuevo chat", + "settings": { + "title": "Ajustes del chat", + "show_stats": { + "title": "Mostrar estadísticas", + "desc": "Mostrar la velocidad de generación y el tamaño del contexto en las respuestas del chat.", + "while_generating": "Durante la generación", + "always": "Siempre" + }, + "auto_scroll": { + "title": "Desplazamiento automático", + "desc": "Seguir los mensajes nuevos a medida que llegan." + } + }, + "stats": { + "context": "{{tokens}} tokens", + "tokens_per_second": "{{rate}} t/s" + }, + "reasoning": { + "active": "Razonando…", + "show": "Mostrar razonamiento", + "hide": "Ocultar razonamiento" + }, + "thinking": { + "toggle": "Alternar razonamiento" + } +} diff --git a/web/public/locales/es/views/classificationModel.json b/web/public/locales/es/views/classificationModel.json new file mode 100644 index 0000000..93bc070 --- /dev/null +++ b/web/public/locales/es/views/classificationModel.json @@ -0,0 +1,205 @@ +{ + "documentTitle": "Modelos de Clasificación - Frigate", + "button": { + "deleteClassificationAttempts": "Borrar Imágenes de Clasificación", + "renameCategory": "Renombrar Clase", + "deleteCategory": "Borrar Clase", + "deleteImages": "Borrar Imágenes", + "trainModel": "Entrenar Modelo", + "addClassification": "Añadir Clasificación", + "deleteModels": "Borrar Modelos", + "editModel": "Editar Modelo" + }, + "toast": { + "success": { + "deletedCategory_one": "Se eliminó {{count}} clase", + "deletedCategory_many": "Se eliminaron {{count}} clases", + "deletedCategory_other": "Se eliminaron {{count}} clases", + "deletedImage_one": "Se eliminó {{count}} imagen", + "deletedImage_many": "Se eliminaron {{count}} imágenes", + "deletedImage_other": "Se eliminaron {{count}} imágenes", + "deletedModel_one": "Borrado con éxito {{count}} modelo", + "deletedModel_many": "Borrados con éxito {{count}} modelos", + "deletedModel_other": "Borrados con éxito {{count}} modelos", + "categorizedImage": "Imagen Clasificada Correctamente", + "trainedModel": "Modelo entrenado correctamente.", + "trainingModel": "Entrenamiento del modelo iniciado correctamente.", + "updatedModel": "Configuración del modelo actualizada correctamente", + "renamedCategory": "Clase renombrada correctamente a {{name}}", + "reclassifiedImage": "Imagen reclasificada con éxito" + }, + "error": { + "deleteImageFailed": "Fallo al borrar: {{errorMessage}}", + "deleteCategoryFailed": "Fallo al borrar clase: {{errorMessage}}", + "deleteModelFailed": "Fallo al borrar modelo: {{errorMessage}}", + "categorizeFailed": "Fallo al categorizar imagen: {{errorMessage}}", + "trainingFailed": "El entrenamiento del modelo ha fallado. Revisa los registros de Frigate para más detalles.", + "updateModelFailed": "Fallo al actualizar modelo: {{errorMessage}}", + "trainingFailedToStart": "No se pudo iniciar el entrenamiento del modelo: {{errorMessage}}", + "renameCategoryFailed": "Fallo al renombrar la clase:{{errorMessage}}", + "reclassifyFailed": "Error al reclasificar la imagen: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Borrar Clase", + "desc": "¿Esta seguro de que quiere borrar la clase {{name}}? Esto borrará permanentemente todas las imágenes asociadas y requerirá reentrenar el modelo.", + "minClassesTitle": "No se puede borrar la clase", + "minClassesDesc": "Un modelo de clasificación debe tener al menos 2 clases. Añade otra clase antes de borrar esta." + }, + "deleteModel": { + "title": "Borrar Modelo de Clasificación", + "single": "¿Está seguro de que quiere eliminar {{name}}? Esto borrar permanentemente todos los datos asociados incluidas las imágenes y los datos de entrenamiento. Esta acción no se puede deshacer.", + "desc_one": "¿Estas seguro de que quiere borrar {{count}} modelo? Esto borrara permanentemente todos los datos asociados, incluyendo imágenes y datos de entrenamiento. Esta acción no puede ser desehecha.", + "desc_many": "¿Estas seguro de que quiere borrar {{count}} modelos? Esto borrara permanentemente todos los datos asociados, incluyendo imágenes y datos de entrenamiento. Esta acción no puede ser desehecha.", + "desc_other": "¿Estas seguro de que quiere borrar {{count}} modelos? Esto borrara permanentemente todos los datos asociados, incluyendo imágenes y datos de entrenamiento. Esta acción no puede ser desehecha." + }, + "edit": { + "title": "Editar modelo de clasificación", + "descriptionState": "Edita las clases para este modelo de clasificación de estados. Los cambios requerirán un reentrenamiento de modelo.", + "descriptionObject": "Edita el tipo de objeto y el tipo de clasificación para este modelo de clasificación de objetos.", + "stateClassesInfo": "Nota: El cambio de las clases de estado requiere reentrenar el modelo con las clases actualizadas." + }, + "tooltip": { + "noChanges": "No se han realizado cambios en el conjunto de datos desde el último entrenamiento.", + "modelNotReady": "El modelo no está listo para el entrenamiento", + "trainingInProgress": "El modelo está entrenándose actualmente", + "noNewImages": "No hay imágenes nuevas para entrenar. Clasifica antes más imágenes del conjunto de datos." + }, + "details": { + "scoreInfo": "La puntuación representa la confianza promedio de la clasificación en todas las detecciones de este objeto.", + "unknown": "Desconocido", + "none": "Ninguno" + }, + "categorizeImage": "Clasificar Imagen", + "menu": { + "objects": "Objetos", + "states": "Estados" + }, + "wizard": { + "steps": { + "chooseExamples": "Seleccionar Ejemplos", + "nameAndDefine": "Nombrar y definir", + "stateArea": "Área de estado" + }, + "step1": { + "name": "Nombre", + "namePlaceholder": "Introducir nombre del modelo...", + "type": "Tipo", + "typeState": "Estado", + "typeObject": "Objeto", + "objectLabel": "Etiqueta de Objeto", + "objectLabelPlaceholder": "Seleccionar tipo de objeto...", + "classificationAttribute": "Atributo", + "classes": "Clases", + "states": "Estados", + "classPlaceholder": "Introducir nombre de la clase...", + "errors": { + "nameRequired": "Se requiere nombre del modelo", + "nameLength": "El nombre del modelo debe tener 64 caracteres o menos", + "nameOnlyNumbers": "El nombre del modelo no puede contener solo números", + "classRequired": "Al menos se requiere una clase", + "classesUnique": "Los nombres de clase deben ser únicos", + "noneNotAllowed": "La clase 'none' no esta permitida", + "stateRequiresTwoClasses": "Los modelos de estado requieren al menos 2 clases", + "objectLabelRequired": "Por favor seleccione una etiqueta de objeto", + "objectTypeRequired": "Por favor seleccione un tipo de clasificación" + }, + "description": "Los modelos de estado monitorean las áreas fijas de la cámara para detectar cambios (p. ej., puerta abierta/cerrada). Los modelos de objetos clasifican los objetos detectados (p. ej., animales conocidos, repartidores, etc.).", + "classificationType": "Tipo de clasificación", + "classificationTypeTip": "Conozca más sobre los tipos de clasificación", + "classificationTypeDesc": "Las subetiquetas añaden texto adicional a la etiqueta del objeto (p. ej., «Persona: UPS»). Los atributos son metadatos que permiten búsquedas y se almacenan por separado en los metadatos del objeto.", + "classificationSubLabel": "Sub etiqueta", + "classesTip": "Aprenda más sobre clases", + "classesStateDesc": "Define los diferentes estados en los que puede estar el área de tu cámara. Por ejemplo: \"abierta\" y \"cerrada\" para una puerta de garaje.", + "classesObjectDesc": "Define las diferentes categorías para clasificar los objetos detectados. Por ejemplo: \"persona de reparto\", \"residente\" y \"desconocido\" para la clasificación de personas." + }, + "step2": { + "description": "Seleccione las cámaras y defina el area a monitorizar por cada cámara. El modelo clasificará el estado de estas cámaras.", + "cameras": "Camaras", + "selectCamera": "Selecciones Cámara", + "noCameras": "Haga clic en + para añadir cámaras", + "selectCameraPrompt": "Seleccione una cámara de la lista para definir su área de monitorización" + }, + "step3": { + "selectImagesPrompt": "Seleccione todas las imágenes de: {{className}}", + "selectImagesDescription": "Haga clic en las imágenes para seleccionarlas. Haga clic en Continuar cuando esté listo para esta clase.", + "generating": { + "title": "Generando Imágenes de Ejemplo", + "description": "Frigate está seleccionando imágenes representativas de sus grabaciones. Esto puede llevar un tiempo..." + }, + "training": { + "title": "Modelo de Entrenamiento", + "description": "Tu modelo se está entrenando en segundo plano. Cierra este cuadro de diálogo y tu modelo comenzará a ejecutarse en cuanto finalice el entrenamiento." + }, + "retryGenerate": "Reintentar Generación", + "noImages": "No se han generado imágenes de ejemplo", + "classifying": "Clasificando y Entrenando...", + "trainingStarted": "Entrenamiento iniciado con éxito", + "modelCreated": "Modelo creado con éxito. Use la vista de Clasificaciones Recientes para añadir imágenes para los estados que falten, después entrene el modelo.", + "errors": { + "noCameras": "No hay cámaras configuradas", + "noObjectLabel": "No se ha seleccionado etiqueta de objeto", + "generateFailed": "Falló la generación de ejemplos: {{error}}", + "generationFailed": "Generación fallida. Por favor pruebe otra vez.", + "classifyFailed": "Falló la clasificación de imágenes: {{error}}" + }, + "generateSuccess": "Imágenes de ejemplo generadas correctamente", + "missingStatesWarning": { + "title": "Faltan Ejemplos de Estado", + "description": "No todas las clases tienen ejemplos. Prueba a generar nuevos ejemplos para encontrar la clase que falta, o continúa y usa la vista de Clasificaciones recientes para añadir imágenes más tarde." + }, + "allImagesRequired_one": "Por favor clasifique todas las imágenes. Queda {{count}} imagen.", + "allImagesRequired_many": "Por favor clasifique todas las imágenes. Quedan {{count}} imágenes.", + "allImagesRequired_other": "Por favor clasifique todas las imágenes. Quedan {{count}} imágenes.", + "refreshConfirm": { + "description": "Esta acción generará un nuevo conjunto de imágenes y eliminará todas las selecciones, incluidas las clases anteriores. Deberás volver a seleccionar ejemplos para todas las clases.", + "title": "¿Generar nuevos ejemplos?" + }, + "refreshExamples": "Generar nuevos ejemplos" + }, + "title": "Crear nueva Clasificación" + }, + "deleteDatasetImages": { + "title": "Borrar Conjunto de Imágenes", + "desc_one": "¿Está seguro de que quiere eliminar {{count}} imagen de {{dataset}}? Esta acción no puede ser deshecha y requerirá reentrenar el modelo.", + "desc_many": "¿Está seguro de que quiere eliminar {{count}} imágenes de {{dataset}}? Esta acción no puede ser deshecha y requerirá reentrenar el modelo.", + "desc_other": "¿Está seguro de que quiere eliminar {{count}} imágenes de {{dataset}}? Esta acción no puede ser deshecha y requerirá reentrenar el modelo." + }, + "deleteTrainImages": { + "title": "Borrar imágenes de entrenamiento", + "desc_one": "¿Está seguro de que quiere eliminar {{count}} imagen? Esta acción no puede ser deshecha.", + "desc_many": "¿Está seguro de que quiere eliminar {{count}} imágenes? Esta acción no puede ser deshecha.", + "desc_other": "¿Está seguro de que quiere eliminar {{count}} imágenes? Esta acción no puede ser deshecha." + }, + "renameCategory": { + "title": "Renombrar Clase", + "desc": "Introduzca un nuevo nombre para {{name}}. Se requerirá que reentrene el modelo para que el cambio de nombre tenga efecto." + }, + "description": { + "invalidName": "Nombre incorrecto. Los nombres solo pueden incluir letras, números, espacios, apóstrofes, guiones bajos, y guiones." + }, + "train": { + "title": "Clasificaciones Recientes", + "titleShort": "Reciente", + "aria": "Seleccione Clasificaciones Recientes" + }, + "categories": "Clases", + "createCategory": { + "new": "Crear Nueva Clase" + }, + "categorizeImageAs": "Clasificar Imagen Como:", + "noModels": { + "object": { + "title": "No hay Modelos de Clasificación de Objetos", + "description": "Crear modelo a medida para clasificar los objetos detectados.", + "buttonText": "Crear Modelo de Objetos" + }, + "state": { + "title": "No hay Modelos de Clasificación de Estados", + "description": "Cree un modelo personalizado para monitorear y clasificar los cambios de estado en áreas específicas de la cámara.", + "buttonText": "Crear modelo de estado" + } + }, + "reclassifyImage": "Reclasificar imagen", + "reclassifyImageAs": "Reclasificar imagen como:" +} diff --git a/web/public/locales/es/views/configEditor.json b/web/public/locales/es/views/configEditor.json new file mode 100644 index 0000000..8f109c2 --- /dev/null +++ b/web/public/locales/es/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Editor de Configuración", + "copyConfig": "Copiar configuración", + "saveAndRestart": "Guardar y reiniciar", + "saveOnly": "Guardar solo", + "toast": { + "success": { + "copyToClipboard": "Configuración copiada al portapapeles." + }, + "error": { + "savingError": "Error al guardar la configuración" + } + }, + "documentTitle": "Editor de Configuración - Frigate", + "confirm": "¿Salir sin guardar?", + "safeConfigEditor": "Editor de Configuración (Modo Seguro)", + "safeModeDescription": "Frigate se encuentra en modo seguro debido a un error de validación en la configuración." +} diff --git a/web/public/locales/es/views/events.json b/web/public/locales/es/views/events.json new file mode 100644 index 0000000..7c2dd8b --- /dev/null +++ b/web/public/locales/es/views/events.json @@ -0,0 +1,95 @@ +{ + "alerts": "Alertas", + "detections": "Detecciones", + "motion": { + "label": "Movimiento", + "only": "Solo movimiento" + }, + "allCameras": "Todas las cámaras", + "empty": { + "alert": "No hay alertas para revisar", + "detection": "No hay detecciones para revisar", + "motion": "No se encontraron datos de movimiento", + "recordingsDisabled": { + "title": "Las grabaciones deben estar habilitadas", + "description": "Solo se pueden crear elementos de revisión para una cámara cuando las grabaciones están habilitadas para esa cámara." + } + }, + "timeline": { + "label": "Línea de tiempo" + }, + "timeline.aria": "Seleccionar línea de tiempo", + "events": { + "label": "Eventos", + "aria": "Seleccionar eventos", + "noFoundForTimePeriod": "No se encontraron eventos para este período de tiempo." + }, + "documentTitle": "Revisión - Frigate", + "markAsReviewed": "Marcar como revisado", + "newReviewItems": { + "label": "Ver nuevos elementos de revisión", + "button": "Nuevos elementos para revisar" + }, + "camera": "Cámara", + "recordings": { + "documentTitle": "Grabaciones - Frigate", + "invalidSharedLink": "No se puede abrir el enlace de la grabación con marca de tiempo debido a un error de análisis.", + "invalidSharedCamera": "No se puede abrir el enlace de la grabación con marca de tiempo debido a una cámara desconocida o no autorizada." + }, + "calendarFilter": { + "last24Hours": "Últimas 24 horas" + }, + "markTheseItemsAsReviewed": "Marcar estos elementos como revisados", + "selected": "{{count}} seleccionados", + "selected_one": "{{count}} seleccionados", + "selected_other": "{{count}} seleccionados", + "detected": "detectado", + "suspiciousActivity": "Actividad Sospechosa", + "threateningActivity": "Actividad Amenzadora", + "zoomIn": "Agrandar", + "zoomOut": "Alejar", + "detail": { + "label": "Detalle", + "trackedObject_one": "{{count}} objeto", + "trackedObject_other": "{{count}} objetos", + "noObjectDetailData": "No hay datos detallados del objeto.", + "settings": "Configuración de la Vista Detalle", + "noDataFound": "No hay datos detallados para revisar", + "aria": "Alternar vista de detalles", + "alwaysExpandActive": { + "title": "Expandir siempre los activos", + "desc": "Expandir siempre los detalles del objeto activo cuando esten disponibles." + } + }, + "objectTrack": { + "clickToSeek": "Clic para ir a este momento", + "trackedPoint": "Puntro trazado" + }, + "select_all": "Todas", + "normalActivity": "Normal", + "needsReview": "Necesita revisión", + "securityConcern": "Aviso de seguridad", + "motionSearch": { + "menuItem": "Búsqueda de movimiento", + "openMenu": "Opciones de cámara" + }, + "motionPreviews": { + "menuItem": "Ver vistas previas de movimiento", + "title": "Vistas previas de movimiento: {{camera}}", + "mobileSettingsTitle": "Ajustes de vistas previas de movimiento", + "mobileSettingsDesc": "Ajusta la velocidad de reproducción y el atenuado, y elige una fecha para revisar clips solo de movimiento.", + "dim": "Atenuar", + "dimAria": "Ajustar intensidad de atenuado", + "dimDesc": "Aumenta el atenuado para mejorar la visibilidad de las áreas con movimiento.", + "speed": "Velocidad", + "speedAria": "Seleccionar velocidad de reproducción de las vistas previas", + "speedDesc": "Elige la velocidad a la que se reproducen los clips de vista previa.", + "back": "Atrás", + "empty": "No hay vistas previas disponibles", + "noPreview": "Vista previa no disponible", + "seekAria": "Mover el reproductor de {{camera}} a {{time}}", + "filter": "Filtrar", + "filterDesc": "Selecciona áreas para mostrar solo clips con movimiento en esas regiones.", + "filterClear": "Limpiar" + } +} diff --git a/web/public/locales/es/views/explore.json b/web/public/locales/es/views/explore.json new file mode 100644 index 0000000..2f993e6 --- /dev/null +++ b/web/public/locales/es/views/explore.json @@ -0,0 +1,319 @@ +{ + "generativeAI": "Inteligencia Artificial Generativa", + "exploreIsUnavailable": { + "title": "Explorar no está disponible", + "embeddingsReindexing": { + "startingUp": "Iniciando…", + "estimatedTime": "Tiempo estimado restante:", + "finishingShortly": "Finalizando en breve", + "step": { + "thumbnailsEmbedded": "Miniaturas incrustadas: ", + "descriptionsEmbedded": "Descripciones incrustadas: ", + "trackedObjectsProcessed": "Objetos rastreados procesados: " + }, + "context": "Explorar puede usarse después de que las incrustaciones de objetos rastreados hayan terminado de reindexarse." + }, + "downloadingModels": { + "context": "Frigate está descargando los modelos de incrustaciones necesarios para soportar la función de Búsqueda Semántica. Esto puede tomar varios minutos dependiendo de la velocidad de tu conexión de red.", + "error": "Ha ocurrido un error. Revisa los registros de Frigate.", + "setup": { + "visionModelFeatureExtractor": "Extractor de características del modelo de visión", + "visionModel": "Modelo de visión", + "textModel": "Modelo de texto", + "textTokenizer": "Tokenizador de texto" + }, + "tips": { + "context": "Es posible que desees reindexar las incrustaciones de tus objetos rastreados una vez que se hayan descargado los modelos.", + "documentation": "Leer la documentación" + } + } + }, + "details": { + "timestamp": "Marca de tiempo", + "item": { + "title": "Detalles del elemento de revisión", + "desc": "Detalles del elemento de revisión", + "button": { + "share": "Compartir este elemento de revisión", + "viewInExplore": "Ver en Explorar" + }, + "toast": { + "success": { + "updatedSublabel": "Subetiqueta actualizada con éxito.", + "regenerate": "Se ha solicitado una nueva descripción a {{provider}}. Dependiendo de la velocidad de tu proveedor, la nueva descripción puede tardar algún tiempo en regenerarse.", + "updatedLPR": "Matrícula actualizada con éxito.", + "audioTranscription": "Se solicitó correctamente la transcripción de audio. Dependiendo de la velocidad de su servidor Frigate, la transcripción puede tardar un tiempo.", + "updatedAttributes": "Atributos actualizados correctamente." + }, + "error": { + "regenerate": "No se pudo llamar a {{provider}} para una nueva descripción: {{errorMessage}}", + "updatedSublabelFailed": "No se pudo actualizar la subetiqueta: {{errorMessage}}", + "updatedLPRFailed": "No se pudo actualizar la matrícula: {{errorMessage}}", + "audioTranscription": "Transcripción de audio solicitada falló: {{errorMessage}}", + "updatedAttributesFailed": "No se pudieron actualizar los atributos: {{errorMessage}}" + } + }, + "tips": { + "mismatch_one": "Se detectó y se incluyó en este elemento de revisión un objeto {{count}} no disponible. Esos objetos no calificaron como alerta o detección o ya han sido limpiados/eliminados.", + "mismatch_many": "Se detectaron y se incluyeron en este elemento de revisión {{count}} objetos no disponibles. Esos objetos no calificaron como alerta o detección o ya han sido limpiados/eliminados.", + "mismatch_other": "Se detectaron y se incluyeron en este elemento de revisión {{count}} objetos no disponibles. Esos objetos no calificaron como alerta o detección o ya han sido limpiados/eliminados.", + "hasMissingObjects": "Ajusta tu configuración si quieres que Frigate guarde los objetos rastreados para las siguientes etiquetas: {{objects}}" + } + }, + "topScore": { + "label": "Puntuación máxima", + "info": "La puntuación máxima es la mediana más alta para el objeto rastreado, por lo que puede diferir de la puntuación mostrada en la miniatura del resultado de búsqueda." + }, + "description": { + "aiTips": "Frigate no solicitará una descripción a tu proveedor de Inteligencia Artificial Generativa hasta que el ciclo de vida del objeto rastreado haya terminado.", + "placeholder": "Descripción del objeto rastreado", + "label": "Descripción" + }, + "expandRegenerationMenu": "Expandir menú de regeneración", + "regenerateFromSnapshot": "Regenerar desde captura de pantalla", + "regenerateFromThumbnails": "Regenerar desde miniaturas", + "tips": { + "descriptionSaved": "Descripción guardada con éxito", + "saveDescriptionFailed": "No se pudo actualizar la descripción: {{errorMessage}}" + }, + "zones": "Zonas", + "label": "Etiqueta", + "editSubLabel": { + "title": "Editar subetiqueta", + "descNoLabel": "Introduce una nueva subetiqueta para este objeto rastreado", + "desc": "Introduce una nueva subetiqueta para este {{label}}" + }, + "button": { + "regenerate": { + "label": "Regenerar descripción del objeto rastreado", + "title": "Regenerar" + }, + "findSimilar": "Buscar similares" + }, + "objects": "Objetos", + "estimatedSpeed": "Velocidad estimada", + "camera": "Cámara", + "editLPR": { + "title": "Editar matrícula", + "desc": "Introduce un nuevo valor de matrícula para este {{label}}", + "descNoLabel": "Introduce un nuevo valor de matrícula para este objeto rastreado" + }, + "recognizedLicensePlate": "Matrícula Reconocida", + "snapshotScore": { + "label": "Puntuación de Instantánea" + }, + "score": { + "label": "Puntuación" + }, + "editAttributes": { + "title": "Editar atributos", + "desc": "Seleccione atributos de clasificación para esta {{label}}" + }, + "attributes": "Atributos de clasificación", + "title": { + "label": "Título" + }, + "scoreInfo": "Información de confianza" + }, + "documentTitle": "Explorar - Frigate", + "trackedObjectDetails": "Detalles del objeto rastreado", + "type": { + "snapshot": "captura instantánea", + "video": "vídeo", + "object_lifecycle": "ciclo de vida del objeto", + "details": "detalles", + "thumbnail": "miniatura", + "tracking_details": "detalles de seguimiento" + }, + "objectLifecycle": { + "title": "Ciclo de vida del objeto", + "noImageFound": "No se encontró ninguna imagen para esta marca de tiempo.", + "createObjectMask": "Crear máscara de objeto", + "adjustAnnotationSettings": "Ajustar configuración de anotaciones", + "scrollViewTips": "Desplázate para ver los momentos significativos del ciclo de vida de este objeto.", + "lifecycleItemDesc": { + "visible": "{{label}} detectado", + "entered_zone": "{{label}} entró en {{zones}}", + "attribute": { + "other": "{{label}} reconocido como {{attribute}}", + "faceOrLicense_plate": "{{attribute}} detectado para {{label}}" + }, + "gone": "{{label}} salió", + "heard": "{{label}} escuchado", + "external": "{{label}} detectado", + "active": "{{label}} se activó", + "stationary": "{{label}} se volvió estacionario", + "header": { + "zones": "Zonas", + "ratio": "Proporción", + "area": "Área" + } + }, + "annotationSettings": { + "offset": { + "label": "Desplazamiento de anotación", + "millisecondsToOffset": "Milisegundos para desplazar las anotaciones de detección. Valor por defecto: 0", + "desc": "Estos datos provienen de la transmisión de detección de tu cámara, pero se superponen en imágenes de la transmisión de grabación. Es poco probable que ambas transmisiones estén perfectamente sincronizadas. Como resultado, la caja delimitadora y la grabación no estarán perfectamente alineadas. Sin embargo, el campo annotation_offset se puede usar para ajustar esto.", + "documentation": "Leer la documentación ", + "tips": "CONSEJO: Imagina que hay un clip de evento con una persona caminando de izquierda a derecha. Si la caja delimitadora de la línea de tiempo del evento está constantemente a la izquierda de la persona, entonces se debe disminuir el valor. Del mismo modo, si una persona camina de izquierda a derecha y la caja delimitadora está constantemente por delante de la persona, entonces se debe aumentar el valor.", + "toast": { + "success": "El desplazamiento de anotación para {{camera}} se ha guardado en el archivo de configuración. Reinicia Frigate para aplicar los cambios." + } + }, + "showAllZones": { + "title": "Mostrar todas las zonas", + "desc": "Mostrar siempre las zonas en los fotogramas donde los objetos hayan entrado en una zona." + }, + "title": "Configuración de anotaciones" + }, + "carousel": { + "previous": "Diapositiva anterior", + "next": "Siguiente diapositiva" + }, + "autoTrackingTips": "Las posiciones de las cajas delimitadoras serán inexactas para cámaras con seguimiento automático.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Punto Rastreado" + }, + "itemMenu": { + "downloadVideo": { + "label": "Descargar video", + "aria": "Descargar video" + }, + "downloadSnapshot": { + "label": "Descargar captura de pantalla", + "aria": "Descargar captura de pantalla" + }, + "viewObjectLifecycle": { + "label": "Ver ciclo de vida del objeto", + "aria": "Mostrar el ciclo de vida del objeto" + }, + "findSimilar": { + "label": "Buscar similares", + "aria": "Buscar objetos rastreados similares" + }, + "submitToPlus": { + "label": "Enviar a Frigate+", + "aria": "Enviar a Frigate Plus" + }, + "viewInHistory": { + "aria": "Ver en Historial", + "label": "Ver en Historial" + }, + "deleteTrackedObject": { + "label": "Eliminar este objeto rastreado" + }, + "audioTranscription": { + "label": "Transcribir", + "aria": "Solicitar transcripción de audio" + }, + "addTrigger": { + "label": "Añadir disparador", + "aria": "Añadir disparador para este objeto rastreado" + }, + "downloadCleanSnapshot": { + "label": "Descargue instantánea limpia", + "aria": "Descargue instantánea limpia" + }, + "viewTrackingDetails": { + "label": "Ver detalles de seguimiento", + "aria": "Ver detalles de seguimiento" + }, + "showObjectDetails": { + "label": "Mostrar la ruta del objeto" + }, + "hideObjectDetails": { + "label": "Ocultar la ruta del objeto" + }, + "more": { + "aria": "Más" + }, + "debugReplay": { + "label": "Reproducción de depuración", + "aria": "Ver este objeto rastreado en la reproducción de depuración" + } + }, + "dialog": { + "confirmDelete": { + "title": "Confirmar eliminación", + "desc": "Al eliminar este objeto rastreado, se eliminan la instantánea, las incrustaciones guardadas y las entradas de detalles de seguimiento asociadas. Las grabaciones de este objeto rastreado en la vista Historial NO se eliminarán.

    ¿Seguro que desea continuar?" + }, + "toast": { + "error": "Error al eliminar este objeto rastreado: {{errorMessage}}" + } + }, + "noTrackedObjects": "No se encontraron objetos rastreados", + "fetchingTrackedObjectsFailed": "Error al obtener objetos rastreados: {{errorMessage}}", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Objeto rastreado eliminado con éxito.", + "error": "No se pudo eliminar el objeto rastreado: {{errorMessage}}" + } + }, + "tooltip": "Coincidencia con {{type}} al {{confidence}}%", + "previousTrackedObject": "Objeto rastreado previo", + "nextTrackedObject": "Objeto rastreado siguiente" + }, + "trackedObjectsCount_one": "{{count}} objeto rastreado ", + "trackedObjectsCount_many": "{{count}} objetos rastreados ", + "trackedObjectsCount_other": "{{count}} objetos rastreados ", + "exploreMore": "Explora más objetos {{label}}", + "aiAnalysis": { + "title": "Análisis AI" + }, + "concerns": { + "label": "Preocupaciones" + }, + "trackingDetails": { + "title": "Detalles del seguimiento", + "noImageFound": "No se ha encontrado imagen en este momento.", + "createObjectMask": "Crear máscara de objeto", + "adjustAnnotationSettings": "Ajustar configuración de anotaciones", + "scrollViewTips": "Haz clic para ver los momentos relevantes del ciclo de vida de este objeto.", + "count": "{{first}} de {{second}}", + "lifecycleItemDesc": { + "visible": "{{label}} detectado", + "active": "{{label}} está activo", + "stationary": "{{label}} se volvió estacionario", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectado para {{label}}", + "other": "{{label}} reconocido como {{attribute}}" + }, + "gone": "{{label}} ha salido", + "heard": "{{label}} escuchado/a", + "external": "{{label}} detectado", + "header": { + "zones": "Zonas", + "area": "Área", + "score": "Puntuación", + "ratio": "Ratio(proporción)", + "computedScore": "Puntuación calculada", + "topScore": "Puntuación más alta", + "toggleAdvancedScores": "Alternar puntuaciones avanzadas" + }, + "entered_zone": "{{label}} ha entrado en {{zones}}" + }, + "trackedPoint": "Punto rastreado", + "annotationSettings": { + "title": "Configuración de anotaciones", + "showAllZones": { + "title": "Mostrar todas las Zonas", + "desc": "Mostrar siempre zonas en los marcos donde los objetos han entrado en una zona." + }, + "offset": { + "label": "Desplazamiento de anotación", + "desc": "Estos datos provienen de la señal de detección de la cámara, pero se superponen a las imágenes de la señal de grabación. Es poco probable que ambas transmisiones estén perfectamente sincronizadas. Por lo tanto, el cuadro delimitador y el metraje no se alinearán perfectamente. Puede usar esta configuración para desplazar las anotaciones hacia adelante o hacia atrás en el tiempo para que se alineen mejor con el metraje grabado.", + "millisecondsToOffset": "Milisegundos para compensar la detección de anotaciones. Predeterminado: 0", + "tips": "Disminuya el valor si la reproducción de vídeo se produce antes de los cuadros y los puntos de ruta, y auméntelo si se produce después de ellos. Este valor puede ser negativo.", + "toast": { + "success": "El desplazamiento de anotación para {{camera}} se ha guardado en el archivo de configuración." + } + } + }, + "autoTrackingTips": "Las posiciones del cuadro delimitador serán inexactas para las cámaras con seguimiento automático.", + "carousel": { + "previous": "Vista anterior", + "next": "Vista siguiente" + } + } +} diff --git a/web/public/locales/es/views/exports.json b/web/public/locales/es/views/exports.json new file mode 100644 index 0000000..0aec0ee --- /dev/null +++ b/web/public/locales/es/views/exports.json @@ -0,0 +1,128 @@ +{ + "search": "Búsqueda", + "documentTitle": "Exportar - Frigate", + "noExports": "No se encontraron exportaciones", + "deleteExport": { + "label": "Eliminar exportación" + }, + "editExport": { + "desc": "Introduce un nuevo nombre para esta exportación.", + "saveExport": "Guardar exportación", + "title": "Renombrar exportación" + }, + "toast": { + "error": { + "renameExportFailed": "No se pudo renombrar la exportación: {{errorMessage}}", + "assignCaseFailed": "No se pudo actualizar la asignación al caso: {{errorMessage}}", + "caseSaveFailed": "No se pudo guardar el caso: {{errorMessage}}", + "caseDeleteFailed": "No se pudo eliminar el caso: {{errorMessage}}" + } + }, + "deleteExport.desc": "¿Estás seguro de que quieres eliminar {{exportName}}?", + "tooltip": { + "shareExport": "Compartir exportación", + "downloadVideo": "Descargar video", + "editName": "Editar nombre", + "deleteExport": "Eliminar exportación", + "assignToCase": "Añadir al caso", + "removeFromCase": "Eliminar del caso" + }, + "headings": { + "cases": "Casos", + "uncategorizedExports": "Exportaciones sin categorización" + }, + "caseDialog": { + "title": "Añadir al caso", + "newCaseOption": "Crear nuevo caso", + "nameLabel": "Nombre del caso", + "description": "Elige un caso existente o crea uno nuevo.", + "selectLabel": "Caso", + "descriptionLabel": "Descripción" + }, + "toolbar": { + "addExport": "Agregar exportación", + "newCase": "Nuevo caso", + "editCase": "Editar caso", + "deleteCase": "Eliminar caso" + }, + "deleteCase": { + "label": "Eliminar caso", + "desc": "¿Estás seguro de que quieres eliminar {{caseName}}?", + "descKeepExports": "Las exportaciones seguirán disponibles como exportaciones sin categoría.", + "descDeleteExports": "Todas las exportaciones de este caso se eliminarán de forma permanente.", + "deleteExports": "Eliminar también las exportaciones" + }, + "caseCard": { + "emptyCase": "Aún no hay exportaciones" + }, + "jobCard": { + "defaultName": "Exportación de {{camera}}", + "queued": "En cola", + "running": "En ejecución", + "preparing": "Preparando", + "copying": "Copiando", + "encoding": "Codificando", + "encodingRetry": "Codificando (reintento)", + "finalizing": "Finalizando" + }, + "caseView": { + "noDescription": "Sin descripción", + "createdAt": "Creado {{value}}", + "exportCount_one": "1 exportación", + "exportCount_other": "{{count}} exportaciones", + "cameraCount_one": "1 cámara", + "cameraCount_other": "{{count}} cámaras", + "showMore": "Mostrar más", + "showLess": "Mostrar menos", + "emptyTitle": "Este caso está vacío", + "emptyDescription": "Añade exportaciones existentes sin categorizar para mantener el caso organizado.", + "emptyDescriptionNoExports": "Todavía no hay exportaciones sin categorizar disponibles para añadir." + }, + "caseEditor": { + "createTitle": "Crear caso", + "editTitle": "Editar caso", + "namePlaceholder": "Nombre del caso", + "descriptionPlaceholder": "Añade notas o contexto para este caso" + }, + "addExportDialog": { + "title": "Añadir exportación a {{caseName}}", + "searchPlaceholder": "Buscar exportaciones sin categorizar", + "empty": "Ninguna exportación sin categorizar coincide con esta búsqueda.", + "addButton_one": "Añadir 1 exportación", + "addButton_other": "Añadir {{count}} exportaciones", + "adding": "Añadiendo..." + }, + "selected_one": "{{count}} seleccionados", + "selected_other": "{{count}} seleccionados", + "bulkActions": { + "addToCase": "Añadir al caso", + "moveToCase": "Mover al caso", + "removeFromCase": "Eliminar del caso", + "delete": "Eliminar", + "deleteNow": "Eliminar ahora" + }, + "bulkDelete": { + "title": "Eliminar exportaciones", + "desc_one": "¿Seguro que quieres eliminar {{count}} exportación?", + "desc_other": "¿Seguro que quieres eliminar {{count}} exportaciones?" + }, + "bulkRemoveFromCase": { + "title": "Eliminar del caso", + "desc_one": "¿Eliminar {{count}} exportación de este caso?", + "desc_other": "¿Eliminar {{count}} exportaciones de este caso?", + "descKeepExports": "Las exportaciones se moverán a sin categorizar.", + "descDeleteExports": "Las exportaciones se eliminarán permanentemente.", + "deleteExports": "Eliminar exportaciones en su lugar" + }, + "bulkToast": { + "success": { + "delete": "Exportaciones eliminadas correctamente", + "reassign": "Asignación de caso actualizada correctamente", + "remove": "Exportaciones eliminadas del caso correctamente" + }, + "error": { + "deleteFailed": "No se pudieron eliminar las exportaciones: {{errorMessage}}", + "reassignFailed": "No se pudo actualizar la asignación del caso: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/es/views/faceLibrary.json b/web/public/locales/es/views/faceLibrary.json new file mode 100644 index 0000000..8014830 --- /dev/null +++ b/web/public/locales/es/views/faceLibrary.json @@ -0,0 +1,113 @@ +{ + "description": { + "addFace": "Agregar una nueva colección a la Biblioteca de Rostros subiendo tu primera imagen.", + "placeholder": "Introduce un nombre para esta colección", + "invalidName": "Nombre incorrecto. Los nombres solo pueden incluir letras, números, espacios, apóstrofes, guiones bajos, y guiones.", + "nameCannotContainHash": "El nombre no puede contener #." + }, + "details": { + "person": "Persona", + "confidence": "Confianza", + "face": "Detalles del rostro", + "faceDesc": "Detalles del objeto rastreado que generó este rostro", + "timestamp": "Marca de tiempo", + "subLabelScore": "Puntuación de Etiqueta Secundaria", + "scoreInfo": "La puntuación de etiqueta secundaria es la puntuación ponderada de todas las confidencias de rostros reconocidos, por lo que puede diferir de la puntuación mostrada en la instantánea.", + "unknown": "Desconocido" + }, + "documentTitle": "Biblioteca de Rostros - Frigate", + "uploadFaceImage": { + "title": "Subir imagen del rostro", + "desc": "Sube una imagen para escanear rostros e incluirla en {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Crear colección", + "desc": "Crear una nueva colección", + "new": "Crear nuevo rostro", + "nextSteps": "Para construir una base sólida:
  • Usa la pestaña Reconocimientos Recientes para seleccionar y entrenar con imágenes de cada persona detectada.
  • Céntrate en imágenes frontales para obtener los mejores resultados; evita entrenar con imágenes que capturen rostros de perfil.
  • " + }, + "train": { + "title": "Reconocimientos Recientes", + "aria": "Seleccionar reconocimientos recientes", + "empty": "No hay intentos recientes de reconocimiento facial", + "titleShort": "Reciente", + "emptyNoLibrary": { + "title": "Subir una cara", + "description": "Debes añadir al menos una cara a la biblioteca para que el reconocimiento facial funcione." + } + }, + "selectItem": "Seleccionar {{item}}", + "selectFace": "Seleccionar rostro", + "deleteFaceLibrary": { + "title": "Eliminar nombre", + "desc": "¿Estás seguro de que quieres eliminar la colección {{name}}? Esto eliminará permanentemente todos los rostros asociados." + }, + "button": { + "deleteFaceAttempts": "Eliminar Rostros", + "addFace": "Agregar rostro", + "uploadImage": "Subir imagen", + "reprocessFace": "Reprocesar rostro", + "renameFace": "Renombrar Rostro", + "deleteFace": "Eliminar Rostro" + }, + "imageEntry": { + "validation": { + "selectImage": "Por favor, selecciona un archivo de imagen." + }, + "dropActive": "Suelta la imagen aquí…", + "dropInstructions": "Arrastra y suelta, o pega una imagen aquí, o haz clic para seleccionar", + "maxSize": "Tamaño máximo: {{size}}MB" + }, + "toast": { + "success": { + "addFaceLibrary": "¡{{name}} ha sido añadido con éxito a la Biblioteca de Rostros!", + "trainedFace": "Rostro entrenado con éxito.", + "deletedName_one": "{{count}} rostro ha sido eliminado con éxito.", + "deletedName_many": "{{count}} rostros han sido eliminados con éxito.", + "deletedName_other": "{{count}} rostros han sido eliminados con éxito.", + "updatedFaceScore": "Puntuación del rostro actualizada con éxito a {{name}} ({{score}}).", + "deletedFace_one": "{{count}} rostro eliminado con éxito.", + "deletedFace_many": "{{count}} rostros eliminados con éxito.", + "deletedFace_other": "{{count}} rostros eliminados con éxito.", + "uploadedImage": "Imagen subida con éxito.", + "renamedFace": "Rostro renombrado con éxito a {{name}}", + "reclassifiedFace": "Rostro reclasificado con éxito." + }, + "error": { + "uploadingImageFailed": "No se pudo subir la imagen: {{errorMessage}}", + "addFaceLibraryFailed": "No se pudo establecer el nombre del rostro: {{errorMessage}}", + "deleteFaceFailed": "No se pudo eliminar: {{errorMessage}}", + "deleteNameFailed": "No se pudo eliminar el nombre: {{errorMessage}}", + "trainFailed": "No se pudo entrenar: {{errorMessage}}", + "updateFaceScoreFailed": "No se pudo actualizar la puntuación del rostro: {{errorMessage}}", + "renameFaceFailed": "No se pudo renombrar el rostro: {{errorMessage}}", + "reclassifyFailed": "Error al reclasificar el rostro: {{errorMessage}}" + } + }, + "readTheDocs": "Leer la documentación", + "trainFaceAs": "Entrenar rostro como:", + "trainFace": "Entrenar rostro", + "steps": { + "faceName": "Introducir Nombre de Rostro", + "uploadFace": "Subir Imagen de Rostro", + "nextSteps": "Próximos Pasos", + "description": { + "uploadFace": "Sube una imagen de {{name}} que muestre su rostro desde un ángulo frontal. La imagen no necesita estar recortada solo a su rostro." + } + }, + "renameFace": { + "title": "Renombrar Rostro", + "desc": "Introduce un nuevo nombre para {{name}}" + }, + "deleteFaceAttempts": { + "title": "Eliminar Rostros", + "desc_one": "¿Estás seguro de que quieres eliminar {{count}} rostro? Esta acción no se puede deshacer.", + "desc_many": "¿Estás seguro de que quieres eliminar {{count}} rostros? Esta acción no se puede deshacer.", + "desc_other": "¿Estás seguro de que quieres eliminar {{count}} rostros? Esta acción no se puede deshacer." + }, + "collections": "Colecciones", + "nofaces": "No hay rostros disponibles", + "pixels": "{{area}}px", + "reclassifyFace": "Reclasificar rostro", + "reclassifyFaceAs": "Reclasificar rostro como:" +} diff --git a/web/public/locales/es/views/live.json b/web/public/locales/es/views/live.json new file mode 100644 index 0000000..877e3ef --- /dev/null +++ b/web/public/locales/es/views/live.json @@ -0,0 +1,206 @@ +{ + "documentTitle": { + "default": "En vivo - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Directo - Frigate", + "twoWayTalk": { + "enable": "Habilitar conversación bidireccional", + "disable": "Deshabilitar conversación bidireccional" + }, + "cameraAudio": { + "enable": "Habilitar audio de la cámara", + "disable": "Deshabilitar audio de la cámara" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Haz clic en el marco para centrar la cámara", + "enable": "Habilitar clic para mover", + "disable": "Deshabilitar clic para mover", + "enableWithZoom": "Habilitar clic para mover / arrastrar para aumentar" + }, + "up": { + "label": "Mover la cámara PTZ hacia arriba" + }, + "down": { + "label": "Mover la cámara PTZ hacia abajo" + }, + "right": { + "label": "Mover la cámara PTZ hacia la derecha" + }, + "left": { + "label": "Mover la cámara PTZ hacia la izquierda" + } + }, + "zoom": { + "in": { + "label": "Acercar la cámara PTZ" + }, + "out": { + "label": "Alejar la cámara PTZ" + } + }, + "frame": { + "center": { + "label": "Haz clic en el marco para centrar la cámara PTZ" + } + }, + "presets": "Preajustes de cámara PTZ", + "focus": { + "in": { + "label": "Enfocar camara PTZ" + }, + "out": { + "label": "Desenfocar camara PTZ" + } + } + }, + "camera": { + "enable": "Habilitar cámara", + "disable": "Deshabilitar cámara", + "turnOn": "Encender cámara", + "turnOff": "Apagar cámara" + }, + "muteCameras": { + "enable": "Silenciar todas las cámaras", + "disable": "Activar sonido de todas las cámaras" + }, + "detect": { + "enable": "Habilitar detección", + "disable": "Deshabilitar detección" + }, + "recording": { + "enable": "Habilitar grabación", + "disable": "Deshabilitar grabación", + "disabledInConfig": "La grabación debe activarse primero en Ajustes para esta cámara." + }, + "snapshots": { + "enable": "Habilitar capturas de pantalla", + "disable": "Desactivar instantáneas" + }, + "audioDetect": { + "enable": "Activar detección de audio", + "disable": "Desactivar detección de audio" + }, + "autotracking": { + "enable": "Activar seguimiento automático", + "disable": "Desactivar seguimiento automático" + }, + "streamStats": { + "enable": "Mostrar estadísticas de transmisión", + "disable": "Ocultar estadísticas de transmisión" + }, + "manualRecording": { + "title": "Bajo demanda", + "tips": "Descargar una instantánea o Iniciar un evento manual basado en la configuración de retención de grabaciones de esta cámara.", + "playInBackground": { + "label": "Reproducir en segundo plano", + "desc": "Habilitar esta opción para continuar transmitiendo cuando el reproductor esté oculto." + }, + "showStats": { + "label": "Mostrar estadísticas", + "desc": "Habilitar esta opción para mostrar estadísticas de transmisión como una superposición en la transmisión de la cámara." + }, + "debugView": "Vista de depuración", + "started": "Grabación manual bajo demanda iniciada.", + "failedToStart": "No se pudo iniciar la grabación manual bajo demanda.", + "start": "Iniciar grabación bajo demanda", + "recordDisabledTips": "Dado que la grabación está deshabilitada o restringida en la configuración de esta cámara, solo se guardará una captura de pantalla.", + "end": "Finalizar grabación bajo demanda", + "ended": "Finalizó la grabación manual bajo demanda.", + "failedToEnd": "No se pudo finalizar la grabación manual bajo demanda." + }, + "lowBandwidthMode": "Modo de bajo ancho de banda", + "streamingSettings": "Ajustes de transmisión", + "notifications": "Notificaciones", + "audio": "Audio", + "suspend": { + "forTime": "Suspender por: " + }, + "stream": { + "title": "Transmisión", + "audio": { + "tips": { + "documentation": "Leer la documentación ", + "title": "El audio debe provenir de tu cámara y estar configurado en go2rtc para esta transmisión." + }, + "available": "El audio está disponible para esta transmisión", + "unavailable": "El audio no está disponible para esta transmisión" + }, + "twoWayTalk": { + "tips.documentation": "Leer la documentación ", + "available": "La conversación bidireccional está disponible para esta transmisión", + "unavailable": "La conversación bidireccional no está disponible para esta transmisión", + "tips": "Tu dispositivo debe soportar la función y WebRTC debe estar configurado para la conversación bidireccional." + }, + "lowBandwidth": { + "tips": "La vista en vivo está en modo de bajo ancho de banda debido a problemas de almacenamiento en búfer o errores de transmisión.", + "resetStream": "Restablecer transmisión" + }, + "playInBackground": { + "label": "Reproducir en segundo plano", + "tips": "Habilita esta opción para continuar la transmisión cuando el reproductor esté oculto." + }, + "debug": { + "picker": "Selección de transmisión no disponible en mode de debug. La vista de debug siempre usa la transmisión con el rol de deteccción asignado." + } + }, + "cameraSettings": { + "title": "Ajustes de {{camera}}", + "objectDetection": "Detección de objetos", + "audioDetection": "Detección de audio", + "recording": "Grabación", + "snapshots": "Capturas de pantalla", + "autotracking": "Seguimiento automático", + "cameraEnabled": "Cámara habilitada", + "transcription": "Transcripción de Audio", + "camera": "Cámara" + }, + "history": { + "label": "Mostrar grabaciones históricas" + }, + "effectiveRetainMode": { + "modes": { + "motion": "Movimiento", + "active_objects": "Objetos activos", + "all": "Todo" + }, + "notAllTips": "Tu configuración de retención de grabación de {{source}} está establecida en modo: {{effectiveRetainMode}}, por lo que esta grabación bajo demanda solo mantendrá segmentos con {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Editar diseño", + "group": { + "label": "Editar grupo de cámaras" + }, + "exitEdit": "Salir de la edición" + }, + "transcription": { + "enable": "Habilitar transcripción de audio en tiempo real", + "disable": "Deshabilitar transcripción de audio en tiempo real" + }, + "noCameras": { + "title": "No hay cámaras configuradas", + "description": "Comienza conectando una cámara a Frigate.", + "buttonText": "Añade Cámara", + "restricted": { + "title": "No hay cámaras disponibles", + "description": "No tiene permiso para ver ninguna cámara en este grupo." + }, + "default": { + "title": "No hay Cámaras Configuradas", + "description": "Comienza conectando una cámara a Frigate.", + "buttonText": "Añadir Cámara" + }, + "group": { + "title": "No hay Cámaras en Grupo", + "description": "Estae grupo de cámaras no tiene cámaras asignadas o habilitadas.", + "buttonText": "Gestionar Grupos" + } + }, + "snapshot": { + "takeSnapshot": "Descarga captura instantánea", + "noVideoSource": "No hay ninguna fuente de video disponible para la instantánea.", + "captureFailed": "Fallo al capturar la instantánea.", + "downloadStarted": "La descarga de la instantánea ha comenzado." + } +} diff --git a/web/public/locales/es/views/motionSearch.json b/web/public/locales/es/views/motionSearch.json new file mode 100644 index 0000000..2d1103a --- /dev/null +++ b/web/public/locales/es/views/motionSearch.json @@ -0,0 +1,82 @@ +{ + "documentTitle": "Búsqueda por movimiento - Frigate", + "title": "Búsqueda por movimiento", + "description": "Dibuja un polígono para definir la región de interés y especifica un intervalo de tiempo para buscar cambios de movimiento dentro de esa región.", + "selectCamera": "Búsqueda por movimiento se está cargando", + "startSearch": "Iniciar búsqueda", + "searchStarted": "Búsqueda iniciada", + "searchCancelled": "Búsqueda cancelada", + "cancelSearch": "Cancelar", + "searching": "Búsqueda en progreso.", + "searchComplete": "Búsqueda completada", + "noResultsYet": "Ejecuta una búsqueda para encontrar cambios de movimiento en la región seleccionada", + "noChangesFound": "No se detectaron cambios de píxeles en la región seleccionada", + "changesFound_one": "Encontrado {{count}} cambio de movimiento", + "changesFound_many": "Encontrados {{count}} cambios de movimiento", + "changesFound_other": "Encontrados {{count}} cambios de movimiento", + "framesProcessed": "{{count}} fotogramas procesados", + "jumpToTime": "Saltar a este tiempo", + "results": "Resultados", + "showSegmentHeatmap": "Mapa de calor", + "newSearch": "Nueva búsqueda", + "clearResults": "Borrar resultados", + "clearROI": "Borrar polígono", + "polygonControls": { + "points_one": "{{count}} punto", + "points_many": "{{count}} puntos", + "points_other": "{{count}} puntos", + "undo": "Deshacer el último punto", + "reset": "Restablecer polígono", + "drawMode": "Dibujar", + "moveMode": "Mover" + }, + "motionHeatmapLabel": "Mapa de calor de movimiento", + "dialog": { + "title": "Búsqueda de movimiento", + "cameraLabel": "Cámara", + "previewAlt": "Vista previa de la cámara {{camera}}" + }, + "timeRange": { + "title": "Rango de búsqueda", + "start": "Hora de inicio", + "end": "Hora de finalización" + }, + "settings": { + "title": "Ajustes de búsqueda", + "parallelMode": "Modo paralelo", + "parallelModeDesc": "Analiza varios intervalos de grabación al mismo tiempo (más rápido, pero utiliza más recursos de decodificación)", + "threshold": "Umbral de sensibilidad", + "thresholdDesc": "Los valores más bajos detectan cambios más pequeños (1-255)", + "minArea": "Área mínima de cambio", + "minAreaDesc": "Tamaño mínimo de una única región en movimiento, expresado como porcentaje de la región de interés", + "frameSkip": "Salto de fotogramas", + "frameSkipDesc": "Procesa cada N fotogramas. Establécelo según la tasa de FPS de tu cámara para procesar un fotograma por segundo (p. ej., 5 para una cámara de 5 FPS, 30 para una cámara de 30 FPS). Los valores más altos serán más rápidos, pero pueden omitir eventos de movimiento breves.", + "maxResults": "Resultados máximos", + "maxResultsDesc": "Detener después de esta cantidad de marcas de tiempo coincidentes" + }, + "errors": { + "noCamera": "Selecciona una cámara", + "noROI": "Dibuja una región de interés", + "noTimeRange": "Selecciona un rango de tiempo", + "invalidTimeRange": "La hora de fin debe ser posterior a la hora de inicio", + "searchFailed": "La búsqueda falló: {{message}}", + "polygonTooSmall": "El polígono debe tener al menos 3 puntos", + "unknown": "Error desconocido" + }, + "changePercentage": "{{percentage}}% cambiado", + "metrics": { + "title": "Métricas de búsqueda", + "segmentsScanned": "Segmentos analizados", + "segmentsProcessed": "Procesado", + "segmentsSkippedInactive": "Omitido (sin actividad)", + "segmentsSkippedHeatmap": "Omitido (sin superposición de ROI)", + "fallbackFullRange": "Análisis completo de respaldo", + "framesDecoded": "Fotogramas decodificados", + "wallTime": "Tiempo de búsqueda", + "segmentErrors": "Errores de segmento", + "seconds": "{{seconds}} s", + "minutesSeconds": "{{minutes}}m {{seconds}}s", + "scanSummary": "{{segments}} segmentos · {{time}}" + }, + "scanning": "Escaneando {{time}}" +} diff --git a/web/public/locales/es/views/recording.json b/web/public/locales/es/views/recording.json new file mode 100644 index 0000000..ad362aa --- /dev/null +++ b/web/public/locales/es/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Exportar", + "calendar": "Calendario", + "filter": "Filtro", + "filters": "Filtros", + "toast": { + "error": { + "noValidTimeSelected": "No se ha seleccionado un rango de tiempo válido", + "endTimeMustAfterStartTime": "La hora de finalización debe ser posterior a la hora de inicio" + } + } +} diff --git a/web/public/locales/es/views/replay.json b/web/public/locales/es/views/replay.json new file mode 100644 index 0000000..f1b7a84 --- /dev/null +++ b/web/public/locales/es/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "Depuración de reproducción", + "description": "Reproducir grabaciones de cámara para depuración. La lista de objetos muestra un resumen con retraso temporal de los objetos detectados y la pestaña Mensajes muestra un flujo de los mensajes internos de Frigate de la grabación reproducida.", + "websocket_messages": "Mensajes", + "dialog": { + "title": "Iniciar depuración de reproducción", + "description": "Crea una cámara de reproducción temporal que reproduzca en bucle imágenes históricas para depurar problemas de detección y seguimiento de objetos. La cámara de reproducción tendrá la misma configuración de detección que la cámara de origen. Elige un intervalo de tiempo para comenzar.", + "camera": "Cámara de origen", + "timeRange": "Intervalo de tiempo", + "preset": { + "1m": "Último 1 minuto", + "5m": "Últimos 5 minutos", + "timeline": "Desde la línea de tiempo", + "custom": "Personalizado" + }, + "startButton": "Iniciar reproducción", + "selectFromTimeline": "Seleccionar", + "starting": "Iniciando reproducción...", + "startLabel": "Iniciar", + "endLabel": "Fin", + "toast": { + "error": "No se pudo iniciar la reproducción de depuración: {{error}}", + "alreadyActive": "Ya hay una sesión de reproducción activa", + "stopError": "No se pudo detener la reproducción de depuración: {{error}}", + "goToReplay": "Ir a la reproducción" + } + }, + "page": { + "noSession": "No hay ninguna sesión activa de reproducción de depuración", + "noSessionDesc": "Inicia una reproducción de depuración desde la vista Historial haciendo clic en el botón Acciones de la barra de herramientas y seleccionando Reproducción de depuración.", + "goToRecordings": "Ir al historial", + "preparingClip": "Preparando clip…", + "preparingClipDesc": "Frigate está uniendo las grabaciones del intervalo de tiempo seleccionado. Esto puede tardar un minuto en intervalos más largos.", + "startingCamera": "Iniciando reproducción de depuración…", + "startError": { + "title": "No se pudo iniciar la reproducción de depuración", + "back": "Volver al historial" + }, + "sourceCamera": "Cámara de origen", + "replayCamera": "Cámara de reproducción", + "initializingReplay": "Inicializando reproducción de depuración…", + "stoppingReplay": "Deteniendo repetición de depuración...", + "stopReplay": "Detener repetición", + "confirmStop": { + "title": "¿Detener repetición de depuración?", + "description": "Esto detendrá la sesión y eliminará todos los datos temporales. ¿Estás seguro?", + "confirm": "Detener repetición", + "cancel": "Cancelar" + }, + "activity": "Actividad", + "objects": "Lista de objetos", + "audioDetections": "Detecciones de audio", + "noActivity": "No se detectó actividad", + "activeTracking": "Seguimiento activo", + "noActiveTracking": "No hay seguimiento activo", + "configuration": "Configuración", + "configurationDesc": "Ajusta con precisión la detección de movimiento y los ajustes de seguimiento de objetos para la cámara de repetición de depuración. No se guardará ningún cambio en el archivo de configuración de Frigate." + } +} diff --git a/web/public/locales/es/views/search.json b/web/public/locales/es/views/search.json new file mode 100644 index 0000000..547b17f --- /dev/null +++ b/web/public/locales/es/views/search.json @@ -0,0 +1,75 @@ +{ + "search": "Búsqueda", + "savedSearches": "Búsquedas Guardadas", + "searchFor": "Búsqueda de {{inputValue}}", + "button": { + "save": "Guardar búsqueda", + "delete": "Eliminar búsqueda guardada", + "clear": "Borrar búsqueda", + "filterInformation": "Información de filtro", + "filterActive": "Filtros activos" + }, + "trackedObjectId": "ID de Objeto Rastreado", + "filter": { + "label": { + "cameras": "Cámaras", + "labels": "Etiquetas", + "zones": "Zonas", + "sub_labels": "Subetiquetas", + "search_type": "Tipo de Búsqueda", + "time_range": "Rango de Tiempo", + "before": "Antes", + "after": "Después", + "min_score": "Puntuación Mínima", + "max_score": "Puntuación Máxima", + "min_speed": "Velocidad Mínima", + "max_speed": "Velocidad Máxima", + "recognized_license_plate": "Matrícula Reconocida", + "has_clip": "Tiene Clip", + "has_snapshot": "Tiene Instantánea", + "attributes": "Atributos" + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Descripción" + }, + "toast": { + "error": { + "maxSpeedMustBeGreaterOrEqualMinSpeed": "La 'velocidad máxima' debe ser mayor o igual que la 'velocidad mínima'.", + "maxScoreMustBeGreaterOrEqualMinScore": "La 'puntuación máxima' debe ser mayor o igual que la 'puntuación mínima'.", + "beforeDateBeLaterAfter": "La fecha 'antes' debe ser posterior a la fecha 'después'.", + "minScoreMustBeLessOrEqualMaxScore": "La 'puntuación mínima' debe ser menor o igual que la 'puntuación máxima'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "La 'velocidad mínima' debe ser menor o igual que la 'velocidad máxima'.", + "afterDatebeEarlierBefore": "La fecha 'después' debe ser anterior a la fecha 'antes'." + } + }, + "tips": { + "title": "Cómo usar filtros de texto", + "desc": { + "text": "Los filtros te ayudan a reducir los resultados de tu búsqueda. Aquí te explicamos cómo usarlos en el campo de entrada:", + "example": "Ejemplo: cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "step": "
    • Escribe un nombre de filtro seguido de dos puntos (por ejemplo, \"cameras:\").
    • Selecciona un valor de las sugerencias o escribe el tuyo propio.
    • Usa múltiples filtros añadiéndolos uno tras otro con un espacio entre ellos.
    • Los filtros de fecha (before: y after:) usan el formato {{DateFormat}}.
    • El filtro de rango de tiempo usa el formato {{exampleTime}}.
    • Elimina filtros haciendo clic en la 'x' junto a ellos.
    ", + "step4": "Los filtros de fecha (antes: y después:) usan el formato {{DateFormat}}.", + "step6": "Elimina filtros haciendo clic en la 'x' junto a ellos.", + "exampleLabel": "Ejemplo:", + "step1": "Escribe un nombre de clave de filtro seguido de dos puntos (por ejemplo, \"cámaras:\").", + "step2": "Selecciona un valor de las sugerencias o escribe el tuyo propio.", + "step3": "Usa múltiples filtros añadiéndolos uno tras otro con un espacio entre ellos.", + "step5": "El filtro de rango de tiempo usa el formato {{exampleTime}}." + } + }, + "header": { + "currentFilterType": "Valores de Filtro", + "noFilters": "Filtros", + "activeFilters": "Filtros Activos" + } + }, + "similaritySearch": { + "title": "Búsqueda por Similitud", + "active": "Búsqueda por similitud activa", + "clear": "Borrar búsqueda por similitud" + }, + "placeholder": { + "search": "Buscar…" + } +} diff --git a/web/public/locales/es/views/settings.json b/web/public/locales/es/views/settings.json new file mode 100644 index 0000000..f5e98eb --- /dev/null +++ b/web/public/locales/es/views/settings.json @@ -0,0 +1,2176 @@ +{ + "documentTitle": { + "masksAndZones": "Editor de máscaras y zonas - Frigate", + "object": "Depurar - Frigate", + "default": "Configuración - Frigate", + "authentication": "Configuración de autenticación - Frigate", + "camera": "Configuración de cámara - Frigate", + "motionTuner": "Ajuste de movimiento - Frigate", + "classification": "Configuración de clasificación - Frigate", + "general": "Configuración de la interfaz - Frigate", + "frigatePlus": "Configuración de Frigate+ - Frigate", + "notifications": "Configuración de Notificaciones - Frigate", + "enrichments": "Configuración de Análisis Avanzado - Frigate", + "cameraManagement": "Administrar Cámaras - Frigate", + "cameraReview": "Revisar Configuración de Cámaras - Frigate", + "globalConfig": "Configuración Global - Frigate", + "cameraConfig": "Configuración de Cámara - Frigate", + "maintenance": "Mantenimiento - Frigate", + "profiles": "Perfiles - Frigate", + "detectorsAndModel": "Detectores y modelo - Frigate" + }, + "menu": { + "cameras": "Configuración de Cámara", + "debug": "Depuración", + "ui": "Interfaz de Usuario", + "classification": "Clasificación", + "motionTuner": "Ajuste de movimiento", + "masksAndZones": "Máscaras / Zonas", + "frigateplus": "Frigate+", + "users": "Usuarios", + "notifications": "Notificaciones", + "enrichments": "Análisis avanzado", + "triggers": "Disparadores", + "roles": "Rols", + "cameraManagement": "Gestión de cámaras", + "cameraReview": "Revisar", + "general": "General", + "globalConfig": "Configuración Global", + "system": "Sistema", + "integrations": "Integraciones", + "uiSettings": "Configuración de Interfaz de Usuario", + "profiles": "Perfiles", + "globalDetect": "Detección de Objetos", + "globalRecording": "Grabación", + "globalSnapshots": "Instantáneas", + "globalFfmpeg": "arguments,Introduce", + "globalMotion": "Detección de Movimiento", + "globalObjects": "Objetos", + "globalReview": "Revisión", + "globalAudioEvents": "Eventos de Audio", + "globalLivePlayback": "Reproducción en Vivo", + "globalTimestampStyle": "Estilo de Marca de Tiempo", + "systemDatabase": "Base de Datos", + "systemAuthentication": "Autenticación", + "systemTls": "TLS", + "systemNetworking": "Red", + "systemProxy": "Proxy", + "systemUi": "Interfaz", + "systemLogging": "Registro", + "systemEnvironmentVariables": "Variables de entorno", + "systemTelemetry": "Telemetría", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "Hardware del detector", + "systemDetectionModel": "Modelo de detección", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "Flujos go2rtc", + "integrationSemanticSearch": "Búsqueda semántica", + "integrationGenerativeAi": "IA generativa", + "integrationFaceRecognition": "Reconocimiento facial", + "integrationLpr": "Reconocimiento de matrículas", + "integrationObjectClassification": "Clasificación de objetos", + "integrationAudioTranscription": "Transcripción de audio", + "cameraDetect": "Detección de objetos", + "cameraFfmpeg": "Flujos (FFmpeg)", + "cameraRecording": "Grabación", + "cameraSnapshots": "Instantáneas", + "cameraMotion": "Detección de movimiento", + "cameraObjects": "Objetos", + "cameraConfigReview": "Revisión", + "cameraAudioEvents": "Detección de audio", + "cameraAudioTranscription": "Transcripción de audio", + "cameraNotifications": "Notificaciones", + "cameraLivePlayback": "Reproducción en directo", + "cameraBirdseye": "Birdseye", + "cameraFaceRecognition": "Reconocimiento facial", + "cameraLpr": "Reconocimiento de matrículas", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "Interfaz de cámara", + "cameraTimestampStyle": "Estilo de marca de tiempo", + "cameraMqtt": "MQTT de cámara", + "maintenance": "Mantenimiento", + "mediaSync": "Sincronización de medios", + "regionGrid": "Cuadrícula de regiones", + "systemDetectorsAndModel": "Detectores y modelo" + }, + "dialog": { + "unsavedChanges": { + "title": "Tienes cambios sin guardar.", + "desc": "¿Quieres guardar los cambios antes de continuar?" + } + }, + "cameraSetting": { + "camera": "Overrides,Sobrescrituras", + "noCamera": "Sin cámara" + }, + "general": { + "liveDashboard": { + "automaticLiveView": { + "label": "Vista en directo automática", + "desc": "Cambiar automáticamente a la vista en directo de una cámara cuando se detecta actividad. Si se desactiva esta opción, las imágenes de las cámaras en el panel en directo solo se actualizarán una vez por minuto." + }, + "playAlertVideos": { + "label": "Reproducir vídeos de alertas", + "desc": "De forma predeterminada, las alertas recientes en el panel en directo se reproducen como pequeños vídeos en bucle. Desactiva esta opción para mostrar solo una imagen estática de las alertas recientes en este dispositivo/navegador." + }, + "title": "Panel en directo", + "displayCameraNames": { + "label": "Siempre mostrar nombres de las Cámaras", + "desc": "Siempre mostrar nombres de cámaras en la vista en vivo multi-cámara." + }, + "liveFallbackTimeout": { + "label": "Tiempo de espera de respaldo del reproductor en vivo", + "desc": "Cuando la reproducción en vivo de alta calidad de la cámara no está disponible, se usará el modo de ancho de banda bajo después de este número de segundos. Por defecto: 3." + } + }, + "cameraGroupStreaming": { + "desc": "La configuración de transmisión de cada grupo de cámaras se guarda en el almacenamiento local de tu navegador.", + "title": "Configuración de transmisión de grupo de cámaras", + "clearAll": "Borrar toda la configuración de transmisión" + }, + "recordingsViewer": { + "defaultPlaybackRate": { + "label": "Velocidad de reproducción predeterminada", + "desc": "Velocidad de reproducción predeterminada para la reproducción de grabaciones." + }, + "title": "Visor de grabaciones" + }, + "calendar": { + "firstWeekday": { + "label": "Primer día de la semana", + "sunday": "Domingo", + "desc": "El día con el que comienzan las semanas en el calendario de revisión.", + "monday": "Lunes" + }, + "title": "Calendario" + }, + "storedLayouts": { + "desc": "El diseño de las cámaras en un grupo de cámaras se puede arrastrar y redimensionar. Las posiciones se guardan en el almacenamiento local de tu navegador.", + "title": "Diseños guardados", + "clearAll": "Borrar todos los diseños" + }, + "title": "Ajustes de Interfaz de Usuario", + "toast": { + "success": { + "clearStoredLayout": "Diseño almacenado eliminado para {{cameraName}}", + "clearStreamingSettings": "Se ha borrado la configuración de transmisión de todos los grupos de cámaras." + }, + "error": { + "clearStreamingSettingsFailed": "Error al borrar la configuración de transmisión: {{errorMessage}}", + "clearStoredLayoutFailed": "Error al borrar el diseño guardado: {{errorMessage}}" + } + } + }, + "classification": { + "semanticSearch": { + "title": "Búsqueda semántica", + "desc": "La búsqueda semántica en Frigate te permite encontrar objetos rastreados dentro de tus elementos de revisión utilizando la propia imagen, una descripción escrita por el usuario o una generada automáticamente.", + "readTheDocumentation": "Leer la documentación", + "reindexNow": { + "confirmTitle": "Confirmar reindexación", + "confirmButton": "Reindexar", + "success": "La reindexación comenzó con éxito.", + "label": "Reindexar ahora", + "desc": "La reindexación regenerará las incrustaciones para todos los objetos rastreados. Este proceso se ejecuta en segundo plano y puede maximizar el uso de tu CPU y tomar una cantidad considerable de tiempo dependiendo del número de objetos rastreados que tengas.", + "confirmDesc": "¿Estás seguro de que quieres reindexar todas las incrustaciones de objetos rastreados? Este proceso se ejecutará en segundo plano, pero puede maximizar el uso de tu CPU y tomar una cantidad considerable de tiempo. Puedes ver el progreso en la página de Explorar.", + "alreadyInProgress": "La reindexación ya está en curso.", + "error": "No se pudo iniciar la reindexación: {{errorMessage}}" + }, + "modelSize": { + "small": { + "desc": "Usar pequeño emplea una versión cuantizada del modelo que utiliza menos RAM y se ejecuta más rápido en la CPU con una diferencia muy insignificante en la calidad de las incrustaciones.", + "title": "pequeño" + }, + "large": { + "title": "grande", + "desc": "Usar grande emplea el modelo completo de Jina y se ejecutará automáticamente en la GPU si es aplicable." + }, + "label": "Tamaño del modelo", + "desc": "El tamaño del modelo utilizado para las incrustaciones de búsqueda semántica." + } + }, + "title": "Configuración de clasificación", + "faceRecognition": { + "title": "Reconocimiento facial", + "modelSize": { + "large": { + "title": "grande", + "desc": "Usar grande emplea un modelo de incrustación facial ArcFace y se ejecutará automáticamente en la GPU si es aplicable." + }, + "small": { + "desc": "Usar pequeño emplea un modelo de incrustación facial FaceNet que se ejecuta de manera eficiente en la mayoría de las CPUs.", + "title": "pequeño" + }, + "label": "Tamaño del modelo", + "desc": "El tamaño del modelo utilizado para el reconocimiento facial." + }, + "readTheDocumentation": "Leer la documentación", + "desc": "El reconocimiento facial permite asignar nombres a las personas y, cuando se reconoce su rostro, Frigate asignará el nombre de la persona como una subetiqueta. Esta información se incluye en la interfaz de usuario, los filtros y también en las notificaciones." + }, + "licensePlateRecognition": { + "title": "Reconocimiento de matrículas", + "desc": "Frigate puede reconocer matrículas en vehículos y agregar automáticamente los caracteres detectados al campo recognized_license_plate o un nombre conocido como subetiqueta a objetos que sean del tipo coche. Un caso de uso común puede ser leer las matrículas de los coches que entran en un camino de entrada o de los coches que pasan por una calle.", + "readTheDocumentation": "Leer la documentación" + }, + "toast": { + "success": "Los ajustes de clasificación han sido guardados. Reinicia Frigate para aplicar tus cambios.", + "error": "No se pudieron guardar los cambios de configuración: {{errorMessage}}" + }, + "birdClassification": { + "title": "Clasificación de Aves", + "desc": "La clasificación de aves identifica aves conocidas utilizando un modelo de TensorFlow cuantizado. Cuando se reconoce una ave conocida, su nombre común se añadirá como una subetiqueta. Esta información se incluye en la interfaz de usuario, en los filtros y en las notificaciones." + }, + "restart_required": "Es necesario reiniciar (se han cambiado las configuraciones de clasificación)", + "unsavedChanges": "Cambios en la configuración de clasificación no guardados" + }, + "camera": { + "review": { + "alerts": "Alertas ", + "desc": "Activar/desactivar temporalmente las alertas y detecciones para esta cámara hasta que Frigate se reinicie. Cuando está desactivado, no se generarán nuevos elementos de revisión. ", + "detections": "Detecciones ", + "title": "Revisar" + }, + "reviewClassification": { + "readTheDocumentation": "Leer la documentación", + "noDefinedZones": "No se han definido zonas para esta cámara.", + "selectAlertsZones": "Seleccionar zonas para Alertas", + "zoneObjectDetectionsTips": { + "regardlessOfZoneObjectDetectionsTips": "Todos los objetos {{detectionsLabels}} no categorizados en {{cameraName}} se mostrarán como Detecciones, independientemente de en qué zona se encuentren.", + "text": "Todos los objetos {{detectionsLabels}} no categorizados en {{zone}} en {{cameraName}} se mostrarán como Detecciones.", + "notSelectDetections": "Todos los objetos {{detectionsLabels}} detectados en {{zone}} en {{cameraName}} que no estén categorizados como Alertas se mostrarán como Detecciones, independientemente de en qué zona se encuentren." + }, + "desc": "Frigate clasifica los elementos de revisión como Alertas y Detecciones. Por defecto, todos los objetos persona y coche se consideran Alertas. Puedes refinar la categorización de tus elementos de revisión configurando zonas requeridas para ellos.", + "objectDetectionsTips": "Todos los objetos {{detectionsLabels}} no categorizados en {{cameraName}} se mostrarán como Detecciones, independientemente de en qué zona se encuentren.", + "zoneObjectAlertsTips": "Todos los objetos {{alertsLabels}} detectados en {{zone}} en {{cameraName}} se mostrarán como Alertas.", + "title": "Clasificación de revisión", + "objectAlertsTips": "Todos los objetos {{alertsLabels}} en {{cameraName}} se mostrarán como Alertas.", + "selectDetectionsZones": "Seleccionar zonas para Detecciones", + "limitDetections": "Limitar detecciones a zonas específicas", + "toast": { + "success": "La configuración de clasificación de revisión ha sido guardada. Reinicia Frigate para aplicar los cambios." + }, + "unsavedChanges": "Configuración de clasificación de revisión no guardada para {{camera}}" + }, + "title": "Ajustes de la cámara", + "streams": { + "title": "Transmisiones", + "desc": "Desactivar temporalmente una cámara hasta que Frigate se reinicie. Desactivar una cámara detiene por completo el procesamiento de las transmisiones de esta cámara por parte de Frigate. La detección, grabación y depuración no estarán disponibles.
    Nota: Esto no desactiva las retransmisiones de go2rtc." + }, + "object_descriptions": { + "title": "Descripciones de objetos de IA generativa", + "desc": "Habilitar/deshabilitar temporalmente las descripciones de objetos de IA generativa para esta cámara. Cuando está deshabilitado, no se solicitarán descripciones generadas por IA para los objetos rastreados en esta cámara." + }, + "review_descriptions": { + "title": "Descripciones de revisión de IA generativa", + "desc": "Habilitar/deshabilitar temporalmente las descripciones de revisión de IA generativa para esta cámara. Cuando está deshabilitado, no se solicitarán descripciones generadas por IA para los elementos de revisión en esta cámara." + }, + "addCamera": "Añadir nueva cámara", + "editCamera": "Editar cámara:", + "selectCamera": "Seleccionar una cámara", + "backToSettings": "Volver a la configuración de la cámara", + "cameraConfig": { + "add": "Añadir cámara", + "edit": "Editar cámara", + "description": "Configurar los ajustes de la cámara, incluyendo las entradas de flujo y los roles.", + "name": "Nombre de la cámara", + "nameRequired": "El nombre de la cámara es obligatorio", + "nameInvalid": "El nombre de la cámara debe contener solo letras, números, guiones bajos o guiones", + "namePlaceholder": "p. ej., puerta_principal", + "enabled": "Habilitado", + "ffmpeg": { + "inputs": "Flujos de entrada", + "path": "Ruta del flujo", + "pathRequired": "La ruta del flujo es obligatoria", + "pathPlaceholder": "rtsp://...", + "roles": "Roles", + "rolesRequired": "Se requiere al menos un rol", + "rolesUnique": "Cada rol (audio, detección, grabación) solo puede asignarse a un flujo", + "addInput": "Añadir flujo de entrada", + "removeInput": "Eliminar flujo de entrada", + "inputsRequired": "Se requiere al menos un flujo de entrada" + }, + "toast": { + "success": "Cámara {{cameraName}} guardada con éxito" + }, + "nameLength": "Nombre de cámara debe ser de mínimo 24 caracteres." + } + }, + "masksAndZones": { + "form": { + "zoneName": { + "error": { + "alreadyExists": "Ya existe una zona con este nombre para esta cámara.", + "mustNotBeSameWithCamera": "El nombre de la zona no debe ser el mismo que el nombre de la cámara.", + "hasIllegalCharacter": "El nombre de la zona contiene caracteres no permitidos.", + "mustBeAtLeastTwoCharacters": "El nombre de la zona debe tener al menos 2 caracteres.", + "mustNotContainPeriod": "El nombre de la zona no debe contener puntos.", + "mustHaveAtLeastOneLetter": "El nombre de la Zona debe contener al menos una letra." + } + }, + "distance": { + "error": { + "mustBeFilled": "Todos los campos de distancia deben estar completados para usar la estimación de velocidad.", + "text": "La distancia debe ser mayor o igual a 0.1." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "La inercia debe ser mayor a 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "El tiempo de merodeo debe ser mayor o igual a 0." + } + }, + "polygonDrawing": { + "snapPoints": { + "true": "Ajustar puntos", + "false": "No ajustar puntos" + }, + "delete": { + "desc": "¿Estás seguro de que quieres eliminar el {{type}} {{name}}?", + "success": "{{name}} ha sido eliminado.", + "title": "Confirmar eliminación" + }, + "error": { + "mustBeFinished": "El dibujo del polígono debe estar terminado antes de guardar." + }, + "reset": { + "label": "Borrar todos los puntos" + }, + "removeLastPoint": "Eliminar el último punto", + "type": { + "zone": "zona", + "motion_mask": "máscara de movimiento", + "object_mask": "máscara de objeto" + }, + "revertOverride": { + "title": "Revertir a la configuración base", + "desc": "Esto eliminará la sobrescritura del perfil para {{type}} {{name}} y revertirá a la configuración base." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "El umbral de velocidad debe ser mayor o igual a 0,1." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "El nombre no puede estar vacío." + } + }, + "id": { + "error": { + "mustNotBeEmpty": "El ID no puede estar vacío.", + "alreadyExists": "Ya existe una máscara con este ID para esta cámara." + } + } + }, + "zones": { + "label": "Zonas", + "desc": { + "title": "Las zonas te permiten definir un área específica del cuadro para que puedas determinar si un objeto está o no dentro de un área particular.", + "documentation": "Documentación" + }, + "add": "Agregar zona", + "edit": "Editar zona", + "loiteringTime": { + "title": "Tiempo de merodeo", + "desc": "Establece una cantidad mínima de tiempo en segundos que el objeto debe estar en la zona para que se active. Predeterminado: 0" + }, + "objects": { + "title": "Objetos", + "desc": "Lista de objetos que aplican a esta zona." + }, + "inertia": { + "desc": "Especifica cuántos fotogramas debe estar un objeto en una zona antes de que se considere que está en la zona. Predeterminado: 3", + "title": "Inercia" + }, + "name": { + "title": "Nombre", + "inputPlaceHolder": "Introduce un nombre…", + "tips": "El nombre debe tener al menos 2 caracteres, al menos 1 letra y no debe coincidir con el nombre de una cámara ni de otra zona." + }, + "documentTitle": "Editar Zona - Frigate", + "clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen.", + "speedEstimation": { + "desc": "Habilitar la estimación de velocidad para objetos en esta zona. La zona debe tener exactamente 4 puntos.", + "title": "Estimación de velocidad", + "docs": "Leer la documentación", + "lineBDistance": "Distancia de la línea B ({{unit}})", + "lineCDistance": "Distancia de la línea C ({{unit}})", + "lineDDistance": "Distancia de la línea D ({{unit}})", + "lineADistance": "Distancia de la línea A ({{unit}})" + }, + "speedThreshold": { + "toast": { + "error": { + "pointLengthError": "La estimación de velocidad ha sido deshabilitada para esta zona. Las zonas con estimación de velocidad deben tener exactamente 4 puntos.", + "loiteringTimeError": "Las zonas con tiempos de merodeo mayores a 0 no deberían usarse con la estimación de velocidad." + } + }, + "title": "Umbral de velocidad ({{unit}})", + "desc": "Especifica una velocidad mínima para que los objetos sean considerados en esta zona." + }, + "point_one": "{{count}} punto", + "point_many": "{{count}} puntos", + "point_other": "{{count}} puntos", + "allObjects": "Todos los objetos", + "toast": { + "success": "La zona ({{zoneName}}) ha sido guardada." + }, + "enabled": { + "description": "Indica si esta zona está activa y habilitada en la configuración. Si está deshabilitado, no puede ser habilitado por MQTT. Las zonas deshabilitadas se ignoran durante la ejecución.", + "title": "Habilitado" + } + }, + "toast": { + "error": { + "copyCoordinatesFailed": "No se pudieron copiar las coordenadas al portapapeles." + }, + "success": { + "copyCoordinates": "Coordenadas copiadas para {{polyName}} al portapapeles." + } + }, + "filter": { + "all": "Todas las máscaras y zonas" + }, + "motionMasks": { + "label": "Máscara de movimiento", + "desc": { + "documentation": "Documentación", + "title": "Las máscaras de movimiento se utilizan para evitar que tipos de movimiento no deseados activen la detección. Un exceso de enmascaramiento puede dificultar el seguimiento de objetos." + }, + "add": "Nueva Máscara de Movimiento", + "edit": "Editar Máscara de Movimiento", + "context": { + "documentation": "Leer la documentación", + "title": "Las máscaras de movimiento se utilizan para evitar que tipos de movimiento no deseados activen la detección (por ejemplo: ramas de árboles, marcas de tiempo de la cámara). Las máscaras de movimiento deben usarse con moderación, un exceso de enmascaramiento dificultará el seguimiento de objetos." + }, + "clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen.", + "polygonAreaTooLarge": { + "documentation": "Leer la documentación", + "title": "La máscara de movimiento está cubriendo el {{polygonArea}}% del marco de la cámara. No se recomiendan máscaras de movimiento grandes.", + "tips": "Las máscaras de movimiento no impiden que se detecten objetos. Deberías usar una zona requerida en su lugar." + }, + "toast": { + "success": { + "noName": "La máscara de movimiento ha sido guardada.", + "title": "{{polygonName}} ha sido guardado." + } + }, + "documentTitle": "Editar Máscara de Movimiento - Frigate", + "point_one": "{{count}} punto", + "point_many": "{{count}} puntos", + "point_other": "{{count}} puntos", + "defaultName": "Máscara de movimiento {{number}}", + "name": { + "title": "Nombre", + "description": "Un nombre descriptivo opcional para esta máscara de movimiento.", + "placeholder": "Introduce un nombre..." + } + }, + "objectMasks": { + "label": "Máscaras de Objetos", + "documentTitle": "Editar Máscara de Objetos - Frigate", + "desc": { + "documentation": "Documentación", + "title": "Las máscaras de filtro de objetos se utilizan para descartar falsos positivos de un tipo de objeto específico según su ubicación." + }, + "add": "Añadir Máscara de Objetos", + "edit": "Editar Máscara de Objetos", + "context": "Las máscaras de filtro de objetos se utilizan para descartar falsos positivos de un tipo de objeto específico según su ubicación.", + "objects": { + "title": "Objetos", + "desc": "El tipo de objeto al que se aplica esta máscara de objetos.", + "allObjectTypes": "Todos los tipos de objetos" + }, + "toast": { + "success": { + "noName": "La máscara de objetos ha sido guardada.", + "title": "{{polygonName}} ha sido guardado." + } + }, + "point_one": "{{count}} punto", + "point_many": "{{count}} puntos", + "point_other": "{{count}} puntos", + "clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen.", + "name": { + "title": "Nombre", + "description": "Un nombre descriptivo opcional para esta máscara de objeto.", + "placeholder": "Introduce un nombre..." + } + }, + "restart_required": "Es necesario reiniciar (se han cambiado las máscaras/zonas)", + "motionMaskLabel": "Máscara de movimiento {{number}}", + "objectMaskLabel": "Máscara de objeto {{number}}", + "disabledInConfig": "El elemento está deshabilitado en el archivo de configuración", + "addDisabledProfile": "Añádelo primero a la configuración base y luego sobrescríbelo en el perfil", + "profileBase": "(base)", + "profileOverride": "(sobrescritura)", + "masks": { + "enabled": { + "title": "Habilitado", + "description": "Indica si esta máscara está habilitada en el archivo de configuración. Si está deshabilitada, no se puede habilitar mediante MQTT. Las máscaras deshabilitadas se ignoran en tiempo de ejecución." + } + } + }, + "motionDetectionTuner": { + "title": "Sintonizador de Detección de Movimiento", + "desc": { + "documentation": "Lee la Guía de Ajuste de Movimiento", + "title": "Frigate utiliza la detección de movimiento como una primera verificación para determinar si hay algo ocurriendo en el marco que merezca ser analizado con la detección de objetos." + }, + "Threshold": { + "title": "Umbral", + "desc": "El valor del umbral determina cuánto cambio en la luminancia de un píxel es necesario para que se considere movimiento. Predeterminado: 30" + }, + "contourArea": { + "title": "Área de Contorno", + "desc": "El valor del área de contorno se utiliza para decidir qué grupos de píxeles cambiados califican como movimiento. Predeterminado: 10" + }, + "improveContrast": { + "title": "Mejorar Contraste", + "desc": "Mejora el contraste para escenas más oscuras. Predeterminado: ACTIVADO" + }, + "toast": { + "success": "Los ajustes de movimiento han sido guardados." + }, + "unsavedChanges": "Cambios no guardados en el sintonizador de movimiento ({{camera}})" + }, + "debug": { + "title": "Depuración", + "debugging": "Depuración", + "objectList": "Lista de Objetos", + "noObjects": "Sin objetos", + "boundingBoxes": { + "title": "Cajas delimitadoras", + "desc": "Mostrar cajas delimitadoras alrededor de los objetos rastreados", + "colors": { + "label": "Colores de las Cajas Delimitadoras de Objetos", + "info": "
  • Al iniciar, se asignarán diferentes colores a cada etiqueta de objeto
  • Una línea fina azul oscura indica que el objeto no está detectado en este momento actual
  • Una línea fina gris indica que el objeto se detecta como estacionario
  • Una línea gruesa indica que el objeto es el sujeto de seguimiento automático (cuando está habilitado)
  • " + } + }, + "timestamp": { + "title": "Marca de tiempo", + "desc": "Superponer una marca de tiempo en la imagen" + }, + "zones": { + "title": "Zonas", + "desc": "Mostrar un contorno de las zonas definidas" + }, + "detectorDesc": "Frigate utiliza tus detectores ({{detectors}}) para detectar objetos en el flujo de video de tu cámara.", + "desc": "La vista de depuración muestra una vista en tiempo real de los objetos rastreados y sus estadísticas. La lista de objetos muestra un resumen con retraso temporal de los objetos detectados.", + "mask": { + "title": "Máscaras de movimiento", + "desc": "Mostrar polígonos de máscaras de movimiento" + }, + "motion": { + "title": "Cajas de movimiento", + "desc": "Mostrar cajas alrededor de las áreas donde se detecta movimiento", + "tips": "

    Cajas de Movimiento


    Se superpondrán cajas rojas en las áreas del fotograma donde se está detectando movimiento actualmente

    " + }, + "regions": { + "title": "Regiones", + "desc": "Mostrar una caja de la región de interés enviada al detector de objetos", + "tips": "

    Cajas de Región


    Se superpondrán cajas verdes brillantes en las áreas de interés del fotograma que se envían al detector de objetos.

    " + }, + "objectShapeFilterDrawing": { + "title": "Dibujo de Filtro de Forma de Objetos", + "desc": "Dibuja un rectángulo en la imagen para ver los detalles de área y proporción", + "tips": "Habilita esta opción para dibujar un rectángulo en la imagen de la cámara y mostrar su área y proporción. Estos valores pueden usarse luego para establecer parámetros de filtro de forma de objetos en tu configuración.", + "document": "Lee la documentación ", + "score": "Puntuación", + "ratio": "Proporción", + "area": "Área" + }, + "paths": { + "title": "Rutas", + "desc": "Mostrar puntos significativos de la ruta del objeto rastreado", + "tips": "

    Rutas


    Líneas y círculos indicarán los puntos significativos por los que se ha movido el objeto rastreado durante su ciclo de vida.

    " + }, + "openCameraWebUI": "Abrir Web UI de {{camera}}", + "audio": { + "title": "Audio", + "noAudioDetections": "No hay detecciones de audio", + "score": "puntuación", + "currentRMS": "RMS actual", + "currentdbFS": "dbFS actual" + } + }, + "users": { + "title": "Usuarios", + "management": { + "title": "Gestión de Usuarios", + "desc": "Gestiona las cuentas de usuario de esta instancia de Frigate." + }, + "addUser": "Añadir usuario", + "toast": { + "success": { + "createUser": "Usuario {{user}} creado correctamente", + "deleteUser": "Usuario {{user}} eliminado correctamente", + "updatePassword": "Contraseña actualizada correctamente.", + "roleUpdated": "Rol actualizado para {{user}}" + }, + "error": { + "createUserFailed": "Error al crear el usuario: {{errorMessage}}", + "deleteUserFailed": "Error al eliminar el usuario: {{errorMessage}}", + "roleUpdateFailed": "Error al actualizar el rol: {{errorMessage}}", + "setPasswordFailed": "Error al guardar la contraseña: {{errorMessage}}" + } + }, + "table": { + "username": "Nombre de usuario", + "actions": "Acciones", + "role": "Rol", + "noUsers": "No se encontraron usuarios.", + "changeRole": "Cambiar el rol del usuario", + "password": "Restablecer Contraseña", + "deleteUser": "Eliminar usuario" + }, + "dialog": { + "form": { + "user": { + "title": "Nombre de usuario", + "placeholder": "Introduce el nombre de usuario", + "desc": "Solo se permiten letras, números, puntos y guiones bajos." + }, + "password": { + "title": "Contraseña", + "placeholder": "Introduce la contraseña", + "confirm": { + "title": "Confirma la contraseña", + "placeholder": "Confirma la contraseña" + }, + "strength": { + "title": "Fortaleza de la contraseña: ", + "weak": "Débil", + "medium": "Media", + "strong": "Fuerte", + "veryStrong": "Muy fuerte" + }, + "match": "Las contraseñas coinciden", + "notMatch": "Las contraseñas no coinciden", + "show": "Mostrar contraseña", + "hide": "Ocultar contraseña", + "requirements": { + "title": "Requisitos de contraseña:", + "length": "Al menos 12 caracteres", + "uppercase": "Al menos una mayúscula", + "digit": "Al menos un número", + "special": "Al menos un caracter especial (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nueva contraseña", + "placeholder": "Introduce la nueva contraseña", + "confirm": { + "placeholder": "Vuelve a introducir la nueva contraseña" + } + }, + "usernameIsRequired": "Se requiere el nombre de usuario", + "passwordIsRequired": "Se requiere contraseña", + "currentPassword": { + "title": "Contraseña actual", + "placeholder": "Introduzca su contraseña actual" + } + }, + "passwordSetting": { + "updatePassword": "Actualizar contraseña para {{username}}", + "setPassword": "Establecer contraseña", + "desc": "Crear una contraseña fuerte para asegurar esta cuenta.", + "cannotBeEmpty": "La contraseña no puede estar vacía", + "doNotMatch": "Las contraseñas no coinciden", + "currentPasswordRequired": "Se requiere la contraseña actual", + "incorrectCurrentPassword": "La contraseña actual es incorrecta", + "passwordVerificationFailed": "Fallo al verificar la contraseña", + "multiDeviceWarning": "Cualquier otro dispositivo en el que haya iniciado sesión deberá iniciar sesión nuevamente con {{refresh_time}}.", + "multiDeviceAdmin": "También puede obligar a todos los usuarios a volver a autenticarse inmediatamente rotando su secreto JWT." + }, + "createUser": { + "desc": "Añadir una nueva cuenta de usuario y especificar un rol para el acceso a áreas de la interfaz de usuario de Frigate.", + "title": "Crear nuevo usuario", + "usernameOnlyInclude": "El nombre de usuario solo puede incluir letras, números, . o _", + "confirmPassword": "Por favor, confirma tu contraseña" + }, + "changeRole": { + "title": "Cambiar rol de usuario", + "desc": "Actualizar permisos para {{username}}", + "roleInfo": { + "intro": "Selecciona el rol adecuado para este usuario:", + "adminDesc": "Acceso completo a todas las funciones.", + "viewerDesc": "Limitado a paneles en vivo, revisión, exploración y exportaciones únicamente.", + "viewer": "Espectador", + "admin": "Administrador", + "customDesc": "Rol personalizado con acceso a cámaras." + }, + "select": "Selecciona un rol" + }, + "deleteUser": { + "warn": "¿Estás seguro de que quieres eliminar {{username}}?", + "title": "Eliminar usuario", + "desc": "Esta acción no se puede deshacer. Esto eliminará permanentemente la cuenta de usuario y eliminará todos los datos asociados." + } + }, + "updatePassword": "Restablecer contraseña" + }, + "notification": { + "title": "Notificaciones", + "notificationSettings": { + "title": "Configuración de notificaciones", + "desc": "Frigate puede enviar notificaciones push a tu dispositivo de forma nativa cuando se ejecuta en el navegador o está instalado como una PWA.", + "documentation": "Leer la documentación" + }, + "notificationUnavailable": { + "title": "Notificaciones no disponibles", + "documentation": "Leer la documentación", + "desc": "Las notificaciones push web requieren un contexto seguro (https://…). Esto es una limitación del navegador. Accede a Frigate de forma segura para usar las notificaciones.", + "descPwa": "En iOS, las notificaciones push web solo están disponibles cuando Frigate está instalado en la pantalla de inicio. Abre el menú Compartir, selecciona Añadir a la pantalla de inicio y, a continuación, abre Frigate desde el nuevo icono para registrar este dispositivo para las notificaciones." + }, + "globalSettings": { + "title": "Configuración global", + "desc": "Suspender temporalmente las notificaciones de cámaras específicas en todos los dispositivos registrados." + }, + "email": { + "title": "Correo electrónico", + "placeholder": "p.ej. ejemplo@correo.com", + "desc": "Se requiere un correo electrónico válido y se utilizará para notificarte si hay algún problema con el servicio de notificaciones push." + }, + "cameras": { + "title": "Cámaras", + "noCameras": "No hay cámaras disponibles", + "desc": "Selecciona qué cámaras habilitar para las notificaciones." + }, + "deviceSpecific": "Configuración específica del dispositivo", + "registerDevice": "Registrar este dispositivo", + "sendTestNotification": "Enviar una notificación de prueba", + "active": "Notificaciones activas", + "suspended": "Notificaciones suspendidas {{time}}", + "suspendTime": { + "5minutes": "Suspender por 5 minutos", + "1hour": "Suspender por 1 hora", + "12hours": "Suspender por 12 horas", + "untilRestart": "Suspender hasta reiniciar", + "30minutes": "Suspender por 30 minutos", + "24hours": "Suspender por 24 horas", + "10minutes": "Suspender por 10 minutos", + "suspend": "Suspender" + }, + "cancelSuspension": "Cancelar suspensión", + "toast": { + "success": { + "settingSaved": "La configuración de notificaciones se ha guardado.", + "registered": "Registrado correctamente para las notificaciones. Es necesario reiniciar Frigate antes de que se puedan enviar notificaciones (incluida una notificación de prueba)." + }, + "error": { + "registerFailed": "Error al guardar el registro de notificaciones." + } + }, + "unregisterDevice": "Cancelar el registro de este dispositivo", + "unsavedRegistrations": "Registros de notificaciones no guardados", + "unsavedChanges": "Cambios de notificaciones no guardados" + }, + "frigatePlus": { + "title": "Configuración de Frigate+", + "apiKey": { + "title": "Clave API de Frigate+", + "notValidated": "La clave API de Frigate+ no ha sido detectada o no ha sido validada", + "plusLink": "Lee más sobre Frigate+", + "desc": "La clave API de Frigate+ permite la integración con el servicio Frigate+.", + "validated": "La clave API de Frigate+ ha sido detectada y validada" + }, + "snapshotConfig": { + "title": "Configuración de instantáneas", + "documentation": "Leer la documentación", + "table": { + "camera": "Cámara", + "snapshots": "Instantáneas", + "cleanCopySnapshots": "clean_copy Instantáneas" + }, + "desc": "Enviar a Frigate+ requiere que las instantáneas estén habilitadas en tu configuración.", + "cleanCopyWarning": "Algunas cámaras tienen las instantáneas deshabilitadas" + }, + "modelInfo": { + "title": "Información del modelo", + "modelType": "Tipo de modelo", + "baseModel": "Modelo base", + "supportedDetectors": "Detectores compatibles", + "dimensions": "Dimensiones", + "cameras": "Cámaras", + "loading": "Cargando información del modelo…", + "error": "No se pudo cargar la información del modelo", + "availableModels": "Modelos de Frigate+ disponibles", + "loadingAvailableModels": "Cargando modelos disponibles…", + "modelSelect": "Tus modelos disponibles en Frigate+ se pueden seleccionar aquí. Ten en cuenta que solo se pueden seleccionar modelos compatibles con tu configuración actual de detectores.", + "trainDate": "Fecha de entrenamiento", + "plusModelType": { + "baseModel": "Modelo Base", + "userModel": "Ajustado Finamente" + }, + "noModelLoaded": "Actualmente no hay ningún modelo de Frigate+ cargado.", + "selectModel": "Selecciona un modelo", + "noModelsAvailable": "No hay modelos disponibles", + "filter": { + "ariaLabel": "Filtrar modelos por tipo", + "baseModels": "Modelos base", + "fineTunedModels": "Modelos ajustados" + } + }, + "toast": { + "success": "La configuración de Frigate+ se ha guardado. Reinicia Frigate para aplicar los cambios.", + "error": "No se pudieron guardar los cambios en la configuración: {{errorMessage}}" + }, + "restart_required": "Es necesario reiniciar (se ha cambiado el modelo Frigate+)", + "unsavedChanges": "Cambios en la configuración de Frigate+ no guardados", + "description": "Frigate+ es un servicio de suscripción que proporciona acceso a funciones y capacidades adicionales para su instancia de Frigate, incluida la posibilidad de utilizar modelos de detección de objetos personalizados entrenados con sus propios datos. Puede gestionar la configuración de sus modelos de Frigate+ aquí.", + "cardTitles": { + "api": "API", + "currentModel": "Modelo actual", + "otherModels": "Otros modelos", + "configuration": "Configuración" + }, + "changeInDetectorsAndModel": "Cambiar modelo" + }, + "enrichments": { + "title": "Configuración de Enriquecimientos", + "unsavedChanges": "Cambios sin guardar en la configuración de Análisis Avanzado", + "birdClassification": { + "title": "Clasificación de Aves", + "desc": "La clasificación de aves identifica especies conocidas utilizando un modelo cuantizado de TensorFlow. Cuando se reconoce un ave conocida, su nombre se añade como una subetiqueta (sub_label). Esta información se incluye en la interfaz de usuario, los filtros y las notificaciones." + }, + "semanticSearch": { + "title": "Búsqueda Semántica", + "desc": "La búsqueda semántica en Frigate te permite encontrar objetos rastreados dentro de tus elementos de revisión utilizando ya sea la imagen en sí, una descripción de texto definida por el usuario o una generada automáticamente.", + "readTheDocumentation": "Leer la Documentación", + "reindexNow": { + "confirmTitle": "Confirmar Re-Indexado", + "confirmDesc": "¿Estás seguro de que quieres re-indexar todas las representaciones (embeddings) de objetos rastreados? Este proceso se ejecutará en segundo plano, pero puede usar al máximo tu CPU y tardar una cantidad considerable de tiempo, dependiendo de la cantidad de objetos registrados. Puedes seguir el progreso en la página Explorar (Explore).", + "confirmButton": "Re-Indexar", + "success": "El proceso de re-indexado ha comenzado.", + "alreadyInProgress": "El proceso de re-indexado ya se está ejecutando.", + "error": "Ha ocurrido un error al intentar iniciar el proceso de re-indexado: {{errorMessage}}", + "label": "Re-indexar Ahora", + "desc": "La re-indexación regenerará las incrustaciones para todos los objetos rastreados. Este proceso se ejecuta en segundo plano y puede utilizar al máximo tu CPU, además de tomar una cantidad considerable de tiempo dependiendo de la cantidad de objetos rastreados que tengas." + }, + "modelSize": { + "label": "Tamaño del Modelo", + "small": { + "title": "size", + "desc": "Usar la opción small emplea una versión cuantizada del modelo que consume menos memoria RAM y se ejecuta más rápido en la CPU, con una diferencia muy pequeña o casi imperceptible en la calidad de las representaciones (embeddings)." + }, + "large": { + "title": "model", + "desc": "Usar la opción large emplea el modelo completo de Jina y se ejecutará automáticamente en la GPU, si está disponible." + }, + "desc": "Tamaño del modelo usado para la búsqueda semántica." + } + }, + "faceRecognition": { + "title": "Reconocimiento Facial", + "readTheDocumentation": "Leer la Documentación", + "modelSize": { + "label": "Tamaño del Modelo", + "desc": "Tamaño del modelo a ser utilizado para el reconocimiento facial.", + "small": { + "title": "pequeño", + "desc": "Usar la opción small emplea un modelo de FaceNet para embeddings faciales que se ejecuta de manera eficiente en la mayoría de las CPUs." + }, + "large": { + "title": "grande", + "desc": "Usar la opción large emplea un modelo de embeddings faciales ArcFace y se ejecutará automáticamente en la GPU, si está disponible." + } + }, + "desc": "El reconocimiento facial permite asignar nombres a las personas y, cuando su rostro es reconocido, Frigate asignará el nombre de la persona como una subetiqueta (sub label). Esta información se incluye en la interfaz de usuario, los filtros y también en las notificaciones." + }, + "licensePlateRecognition": { + "title": "Reconocimiento de Matrículas (LPR)", + "readTheDocumentation": "Leer la Documentación", + "desc": "Frigate puede reconocer matrículas de vehículos y agregar automáticamente los caracteres detectados al campo recognized_license_plate, o bien asignar un nombre conocido como sub-etiqueta (sub_label) a los objetos de tipo coche (car). Un caso de uso común es leer las matrículas de los autos que ingresan a una cochera o que pasan por una calle." + }, + "restart_required": "Es necesario reiniciar Frigate (La configuración de Enrichments han cambiado)", + "toast": { + "success": "Los ajustes de enriquecimientos se han guardado. Reinicia Frigate para aplicar los cambios.", + "error": "No se pudieron guardar los cambios en la configuración: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Disparadores", + "management": { + "title": "Disparadores", + "desc": "Gestionar disparadores para {{camera}}. Usa el tipo de miniatura para activar en miniaturas similares al objeto rastreado seleccionado, y el tipo de descripción para activar en descripciones similares al texto que especifiques." + }, + "addTrigger": "Añadir Disparador", + "table": { + "name": "Nombre", + "type": "Tipo", + "content": "Contenido", + "threshold": "Umbral", + "actions": "Acciones", + "noTriggers": "No hay disparadores configurados para esta cámara.", + "edit": "Editar", + "deleteTrigger": "Eliminar Disparador", + "lastTriggered": "Última activación" + }, + "type": { + "description": "Descripción", + "thumbnail": "Miniatura" + }, + "actions": { + "alert": "Marcar como Alerta", + "notification": "Enviar Notificación", + "sub_label": "Añadir una subetiqueta", + "attribute": "Añadir atributo" + }, + "dialog": { + "createTrigger": { + "title": "Crear Disparador", + "desc": "Crear un disparador par la cámara {{camera}}" + }, + "editTrigger": { + "title": "Editar Disparador", + "desc": "Editar configuractión del disparador para cámara {{camera}}" + }, + "deleteTrigger": { + "title": "Eliminar Disparador", + "desc": "Está seguro de que desea eliminar el disparador {{triggerName}}? Esta acción no se puede deshacer." + }, + "form": { + "name": { + "title": "Nombre", + "placeholder": "Asigne nombre a este disparador", + "error": { + "minLength": "El campo debe tener al menos 2 caracteres.", + "invalidCharacters": "El campo sólo puede contener letras, números, guiones bajos, y guiones.", + "alreadyExists": "Un disparador con este nombre ya existe para esta cámara." + }, + "description": "Ingrese un nombre o descripción únicos para identificar este disparador" + }, + "enabled": { + "description": "Activa o desactiva este disparador" + }, + "type": { + "title": "Tipo", + "placeholder": "Seleccione tipo de disparador", + "description": "Se dispara cuando se detecta una descripción de objeto rastreado similar", + "thumbnail": "Se dispara cuando se detecta una miniatura de un objeto rastreado similar" + }, + "friendly_name": { + "title": "Nombre amigable", + "placeholder": "Nombre o describa este disparador", + "description": "Un nombre o texto descriptivo amigable (opcional) para este disparador." + }, + "content": { + "title": "Contenido", + "imagePlaceholder": "Seleccione una imagen", + "textPlaceholder": "Entre contenido de texto", + "error": { + "required": "El contenido es requrido." + }, + "imageDesc": "Solo se muestran las 100 miniaturas más recientes. Si no encuentra la miniatura que busca, revise los objetos anteriores en Explorar y configure un disparador desde el menú.", + "textDesc": "Entre texto para iniciar esta acción cuando la descripción de un objecto seguido similar es detectado." + }, + "threshold": { + "title": "Umbral", + "error": { + "min": "El umbral debe ser al menos 0", + "max": "El umbral debe ser al menos 1" + }, + "desc": "Establezca el umbral de similitud para este disparador. Un umbral más alto significa que se requiere una coincidencia más cercana para activar el disparador." + }, + "actions": { + "title": "Acciones", + "error": { + "min": "Al menos una acción debe ser seleccionada." + }, + "desc": "Por defecto, Frigate manda un mensaje MQTT para todos los disparadores. Las subetiquetas añaden el nombre del disparador a la etiqueta del objeto. Los atributos son metadatos de búsqueda que se almacenan por separado en los metadatos del objeto rastreado." + } + } + }, + "semanticSearch": { + "title": "Búsqueda semántica desactivada", + "desc": "Búsqueda semántica debe estar activada para usar Disparadores." + }, + "toast": { + "success": { + "createTrigger": "Disparador {{name}} creado exitosamente.", + "updateTrigger": "Disparador {{name}} actualizado exitosamente.", + "deleteTrigger": "Disparador {{name}} eliminado exitosamente." + }, + "error": { + "createTriggerFailed": "Fallo al crear el disparador: {{errorMessage}}", + "updateTriggerFailed": "Fallo al actualizar el disparador: {{errorMessage}}", + "deleteTriggerFailed": "Fallo al eliminar el disparador: {{errorMessage}}" + } + }, + "wizard": { + "title": "Crear disparador", + "step1": { + "description": "Configure los ajustes básicos para su disparador." + }, + "step2": { + "description": "Configure el contenido que activará esta acción." + }, + "step3": { + "description": "Configure el umbral y las acciones para este disparador." + }, + "steps": { + "nameAndType": "Nombre y tipo", + "configureData": "Configurar datos", + "thresholdAndActions": "Umbral y acciones" + } + } + }, + "roles": { + "management": { + "title": "Administración del rol de visor", + "desc": "Administra roles de visor personalizados y sus permisos de acceso a cámaras para esta instancia de Frigate." + }, + "addRole": "Añade un rol", + "table": { + "role": "Rol", + "cameras": "Cámaras", + "actions": "Acciones", + "noRoles": "No se encontraron roles personalizados.", + "editCameras": "Edita Cámaras", + "deleteRole": "Eliminar Rol" + }, + "toast": { + "success": { + "createRole": "Rol {{role}} creado exitosamente", + "updateCameras": "Cámara actualizada para el rol {{role}}", + "deleteRole": "Rol {{role}} eliminado exitosamente", + "userRolesUpdated_one": "{{count}} usuario asignado a este rol ha sido actualizado a 'revisor', que tiene acceso a todas las cámaras.", + "userRolesUpdated_many": "{{count}} usuarios asignados a este rol han sido actualizado a 'revisor', que tienen acceso a todas las cámaras.", + "userRolesUpdated_other": "{{count}} usuarios asignados a este rol han sido actualizado a 'revisor', que tienen acceso a todas las cámaras." + }, + "error": { + "createRoleFailed": "Creación de rol fallida: {{errorMessage}}", + "updateCamerasFailed": "Actualización de cámaras fallida: {{errorMessage}}", + "deleteRoleFailed": "Eliminación de rol fallida: {{errorMessage}}", + "userUpdateFailed": "Actualización de roles de usuario fallida: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Crear Nuevo Rol", + "desc": "Añadir nuevo rol y especificar permisos de acceso a cámaras." + }, + "deleteRole": { + "title": "Eliminar Rol", + "deleting": "Eliminando...", + "desc": "Esta acción no se puede deshacer. El rol va a ser eliminado permanentemente y usuarios associados serán asignados a rol de 'Visor', que les da acceso a ver todas las cámaras.", + "warn": "Estás seguro de que quieres eliminar {{role}}?" + }, + "editCameras": { + "title": "Editar cámaras de rol", + "desc": "Actualizar acceso de cámara para el rol {{role}}." + }, + "form": { + "role": { + "title": "Nombre de rol", + "placeholder": "Entre el nombre del rol", + "desc": "Solo se permiten letras, números, puntos y guión bajo.", + "roleIsRequired": "El nombre del rol es requerido", + "roleOnlyInclude": "El nombre del rol solo incluye letras, números, . o _", + "roleExists": "Un rol con este nombre ya existe." + }, + "cameras": { + "title": "Cámaras", + "desc": "Seleccione las cámaras a las que este rol tiene accceso. Al menos una cámara es requerida.", + "required": "Al menos una cámara debe ser seleccionada." + } + } + } + }, + "cameraWizard": { + "step1": { + "errors": { + "nameRequired": "El nombre de la cámara es un campo obligatorio", + "nameLength": "El nombre de la cámara debe tener 64 caracteres o menos", + "invalidCharacters": "El nombre de la cámara contiene caracteres no válidos", + "nameExists": "El nombre de la cámara ya existe", + "customUrlRtspRequired": "Las URL personalizadas deben comenzar por “rtsp://” o “rtsps://”. Se requiere configuración manual para flujos de cámara que no sean RTSP.", + "brandOrCustomUrlRequired": "Seleccione una marca de cámara con host/IP o elija \"Otro\" con una URL personalizada" + }, + "description": "Ingrese los detalles de su cámara y elija probar la cámara o seleccionar manualmente la marca.", + "cameraName": "Nombre de la Cámara", + "cameraNamePlaceholder": "Ejempo: puerta_principal o Vista del Patio trasero", + "host": "Nombre Host / Dirección IP", + "port": "Puerto", + "username": "Nombre de usuario", + "usernamePlaceholder": "Opcional", + "password": "Contraseña", + "passwordPlaceholder": "Opcional", + "selectTransport": "Seleccionar protocolo de transporte", + "cameraBrand": "Marca de la cámara", + "selectBrand": "Seleccione la marca de la cámara para la plantilla de URL", + "customUrl": "URL de transmisión personalizada", + "brandInformation": "Información de la Marca", + "brandUrlFormat": "Para cámaras con formato de URL RTSP como: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://usuario:contraseña@hostname:puerto/ruta", + "connectionSettings": "Ajustes de conexión", + "detectionMethod": "Método de detección de transmisión", + "onvifPort": "Puerto ONVIF", + "probeMode": "Cámara de sonda", + "manualMode": "Selección manual", + "detectionMethodDescription": "Pruebe la cámara con ONVIF (si es compatible) para encontrar las URL de transmisión o seleccione manualmente la marca de la cámara para usar las URL predefinidas. Para introducir una URL RTSP personalizada, elija el método manual y seleccione \"Otro\".", + "onvifPortDescription": "Para las cámaras compatibles con ONVIF, normalmente es 80 o 8080.", + "useDigestAuth": "Use autenticación digest", + "useDigestAuthDescription": "Utilice la autenticación HTTP digest para ONVIF. Algunas cámaras pueden requerir un nombre de usuario y contraseña ONVIF específicos en lugar del usuario administrador estándar." + }, + "step2": { + "description": "Pruebe la cámara para detectar transmisiones disponibles o configure ajustes manuales según el método de detección seleccionado.", + "testSuccess": "Test de conexión satisfactorio!", + "testFailed": "Test de conexión fallido. Revise la informacion proporcionada e inténtelo de nuevo.", + "testFailedTitle": "Test fallido", + "streamDetails": "Detalles de la transmisión", + "probing": "Probando la cámara...", + "retry": "Re-intentar", + "testing": { + "probingMetadata": "Probando metadatos de la cámara...", + "fetchingSnapshot": "Obteniendo una instantánea de la cámara..." + }, + "probeFailed": "No se pudo alcanzar la cámara: {{error}}", + "probingDevice": "Probando el dispositivo...", + "probeSuccessful": "Prueba satisfactoria", + "probeError": "Error durante la prueba", + "probeNoSuccess": "Prueba fallida", + "deviceInfo": "Información de Dispositivo", + "manufacturer": "Fabricante", + "model": "Modelo", + "firmware": "Firmware", + "profiles": "Perfiles", + "ptzSupport": "Soporte PTZ", + "autotrackingSupport": "Soporte auto-seguimiento", + "presets": "Preestablecidos", + "rtspCandidates": "Candidatos RTSP", + "rtspCandidatesDescription": "Se encontraron las siguientes URL RTSP durante el sondeo de la cámara. Pruebe la conexión para ver los metadatos de la transmisión.", + "noRtspCandidates": "No se encontraron URL RTSP de la cámara. Es posible que sus credenciales sean incorrectas o que la cámara no sea compatible con ONVIF o el método utilizado para obtener las URL RTSP. Vuelva atrás e introduzca la URL RTSP manualmente.", + "candidateStreamTitle": "Candidato {{number}}", + "useCandidate": "Uso", + "uriCopy": "Copiar", + "uriCopied": "URI copiada al portapapeles", + "testConnection": "Probar conexión", + "toggleUriView": "Haga clic para alternar la vista completa de URI", + "connected": "Conectada", + "notConnected": "No conectada", + "errors": { + "hostRequired": "nombre host/dirección IP requeridos" + } + }, + "step3": { + "description": "Configure los roles de transmisión y agregue transmisiones adicionales para su cámara.", + "streamsTitle": "Transmisiones de cámara", + "addStream": "Añadir ruta de transmisión", + "addAnotherStream": "Añadir otra ruta de transmisión", + "streamTitle": "Transmisión {{number}}", + "streamUrl": "URL de transmisión", + "streamUrlPlaceholder": "rtsp://usuario:contraseña@nombrehost:puerto/ruta", + "selectStream": "Seleccione una transmisión", + "searchCandidates": "Búsqueda de candidatos...", + "noStreamFound": "No se ha encontrado transmisión", + "url": "URL", + "resolution": "Resolución", + "selectResolution": "Seleccione resolución", + "quality": "Calidad", + "selectQuality": "Seleccione calidad", + "roles": "Roles", + "roleLabels": { + "detect": "Detección de objetos", + "record": "Grabando", + "audio": "Audio" + }, + "testStream": "Pruebe la conexión", + "testSuccess": "Test de transmisión satisfactorio!", + "testFailed": "Test de transmisión fallido", + "testFailedTitle": "Prueba falló", + "connected": "Conectado", + "notConnected": "No conectado", + "featuresTitle": "Características", + "go2rtc": "Reduzca conexiones hacia la cámara", + "detectRoleWarning": "al menos una transmisión debe tener el roll de detección para continuar.", + "rolesPopover": { + "title": "Roles de transmisión", + "record": "Guarda segmentos de la transmisión de video según la configuración.", + "detect": "Hilo principal para detección de objetos.", + "audio": "Hilo para detección basada en audio." + }, + "featuresPopover": { + "title": "Características de transmisión", + "description": "Utilice la retransmisión go2rtc para reducir las conexiones a su cámara." + } + }, + "step4": { + "description": "Validación y análisis finales antes de guardar la nueva cámara. Conecte cada transmisión antes de guardar.", + "validationTitle": "Validacion de transmisión", + "connectAllStreams": "Conectar todas las transmisiones", + "reconnectionSuccess": "Reconexión satisfactoria.", + "reconnectionPartial": "Algunas transmisiones no pudieron reconectarse.", + "streamUnavailable": "Vista previa de transmisión no disponible", + "reload": "Recargar", + "connecting": "Conectando...", + "streamTitle": "Transmisión {{number}}", + "valid": "Válido", + "failed": "Falló", + "notTested": "No probado", + "connectStream": "Conectar", + "connectingStream": "Conectando", + "disconnectStream": "Desconectar", + "estimatedBandwidth": "Ancho de banda estimado", + "roles": "Roles", + "ffmpegModule": "Utilice el modo de compatibilidad de transmisión", + "ffmpegModuleDescription": "Si la transmisión no carga después de varios intentos, intenta activar esta opción. Al activarla, Frigate usará el módulo ffmpeg con go2rtc. Esto puede mejorar la compatibilidad con algunas transmisiones de cámara.", + "none": "Ninguna", + "error": "Error", + "streamValidated": "Transmisión {{number}} validada correctamente", + "streamValidationFailed": "Stream {{number}} falló la validación", + "saveAndApply": "Guardar nueva cámara", + "saveError": "Configuración inválida. Revise la configuración.", + "issues": { + "title": "Validación de transmisión", + "videoCodecGood": "El codec de video es {{codec}}.", + "audioCodecGood": "El codec de audio es {{codec}}.", + "resolutionHigh": "Una resolución de {{resolution}} puede provocar un mayor uso de recursos.", + "resolutionLow": "Una resolución de {{resolution}} puede ser demasiado baja para una detección confiable de objetos pequeños.", + "noAudioWarning": "No se detectó audio para esta transmisión, las grabaciones no tendrán audio.", + "audioCodecRecordError": "El códec de audio AAC es necesario para admitir audio en grabaciones.", + "audioCodecRequired": "Se requiere una transmisión de audio para admitir la detección de audio.", + "restreamingWarning": "Reducir las conexiones a la cámara para la transmisión de grabación puede aumentar ligeramente el uso de la CPU.", + "brands": { + "reolink-rtsp": "No se recomienda usar Reolink RTSP. Active HTTP en la configuración del firmware de la cámara y reinicie el asistente.", + "reolink-http": "Las transmisiones HTTP de Reolink deberían usar FFmpeg para una mejor compatibilidad. Active \"Usar modo de compatibilidad de transmisiones\" para esta transmisión." + }, + "dahua": { + "substreamWarning": "La subtransmisión 1 está limitada a una resolución baja. Muchas cámaras Dahua/Amcrest/EmpireTech admiten subtransmisiones adicionales que deben habilitarse en la configuración de la cámara. Se recomienda comprobar y utilizar dichas transmisiones si están disponibles." + }, + "hikvision": { + "substreamWarning": "La subtransmisión 1 está limitada a una resolución baja. Muchas cámaras Hikvision admiten subtransmisiones adicionales que deben habilitarse en la configuración de la cámara. Se recomienda comprobar y utilizar dichas transmisiones si están disponibles." + }, + "resolutionUnknown": "No se pudo detectar la resolución de este flujo. Debes establecer manualmente la resolución de detección en Ajustes o en tu configuración." + } + }, + "title": "Añadir cámara", + "description": "Siga los siguientes pasos para agregar una nueva cámara a su instalación de Frigate.", + "steps": { + "nameAndConnection": "Nombre y conexión", + "probeOrSnapshot": "Sonda de prueba o hacer instantánea", + "streamConfiguration": "Configuración de transmisión", + "validationAndTesting": "Validación y pruebas" + }, + "save": { + "success": "La nueva cámara {{cameraName}} se guardó correctamente.", + "failure": "Error al guardar {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolución", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Proporcione una URL de transmisión válida", + "testFailed": "Prueba de transmisión fallida: {{error}}" + } + }, + "cameraManagement": { + "title": "Administrar cámaras", + "addCamera": "Añadir nueva cámara", + "editCamera": "Editar cámara:", + "selectCamera": "Seleccione una cámara", + "backToSettings": "Volver a configuración de la cámara", + "streams": { + "title": "Estado y detalles de la cámara", + "desc": "Desactiva temporalmente una cámara hasta que Frigate se reinicie. Desactivar una cámara detiene por completo el procesamiento de las transmisiones de Frigate. La detección, la grabación y la depuración no estarán disponibles.
    Nota: Esto no desactiva las retransmisiones de go2rtc.", + "enableDesc": "Deshabilita temporalmente una cámara habilitada hasta que Frigate se reinicie. Deshabilitar una cámara detiene completamente el procesamiento de los flujos de esa cámara por parte de Frigate. La detección, la grabación y la depuración no estarán disponibles. Nota: Esto no deshabilita las retransmisiones de go2rtc.Arrastra el controlador para reordenar las cámaras tal y como aparecen en la interfaz. El orden de las cámaras habilitadas se reflejará en toda la interfaz, incluido el panel en directo y los menús desplegables de selección de cámaras.", + "enableLabel": "Cámaras habilitadas", + "disableLabel": "Cámaras deshabilitadas", + "disableDesc": "Habilita una cámara que actualmente no está visible en la interfaz y está deshabilitada en la configuración. Es necesario reiniciar Frigate después de habilitarla.", + "enableSuccess": "{{cameraName}} habilitada. Reinicia Frigate para aplicar los cambios.", + "friendlyName": { + "edit": "Editar nombre visible de la cámara", + "title": "Editar nombre visible", + "description": "Establece el nombre descriptivo que se mostrará para esta cámara en toda la interfaz de Frigate. Déjalo en blanco para usar el ID de la cámara.", + "rename": "Renombrar" + }, + "reorderHandle": "Arrastrar para reordenar", + "saving": "Guardando…", + "saved": "Guardado", + "details": { + "edit": "Editar detalles de la cámara", + "title": "Editar detalles de la cámara", + "description": "Actualiza el nombre visible, la URL externa y la visibilidad usados para esta cámara en toda la interfaz de Frigate.", + "friendlyNameLabel": "Nombre visible", + "friendlyNameHelp": "Nombre descriptivo que se muestra para esta cámara en toda la interfaz de Frigate. Déjalo en blanco para usar el ID de la cámara.", + "webuiUrlLabel": "URL de la interfaz web de la cámara", + "webuiUrlHelp": "URL para acceder directamente a la interfaz web de la cámara desde la vista de depuración. Déjala en blanco para deshabilitar el enlace.", + "webuiUrlInvalid": "Debe ser una URL válida (p. ej., https://ejemplo.com).", + "dashboardLabel": "Mostrar en el panel En directo", + "dashboardHelp": "Mostrar esta cámara en el panel en directo.", + "reviewLabel": "Mostrar en Revisión", + "reviewHelp": "Mostrar esta cámara en Revisión, incluido el filtro de cámaras, la revisión de movimiento y la vista de historial." + }, + "label": "Estado de la cámara", + "description": "Set the operating state for each camera.

    On: las transmisiones se procesan con normalidad.
    Off: pausa temporalmente el procesamiento. No persiste tras reinicios de Frigate.
    Disabled: detiene el procesamiento y guarda el cambio en tu configuración. Es necesario reiniciar para volver a activar una cámara desactivada.

    Note: Desactivar no afecta a las retransmisiones de go2rtc.

    Arrastra el asa para reordenar las cámaras activas tal como aparecen en toda la interfaz, incluido el panel de Live y los menús desplegables de selección de cámara.", + "disabledSubheading": "Deshabilitado en la configuración", + "status": { + "on": "On", + "off": "Off", + "disabled": "Deshabilitado" + }, + "disableSuccess": "{{cameraName}} deshabilitada y guardada en la configuración." + }, + "cameraConfig": { + "add": "Añadir cámara", + "edit": "Editar cámara", + "description": "Configure los ajustes de la cámara, incluidas las entradas de transmisión y los roles.", + "name": "Nombre de la cámara", + "nameRequired": "El nombre de la cámara es obligatorio", + "nameLength": "El nombre de la cámara debe ser inferior a 64 caracteres.", + "namePlaceholder": "Ejemplo: puerta_principal o Vista general de patio trasero", + "enabled": "Habilitada", + "ffmpeg": { + "inputs": "Transmisiones entrantes", + "path": "Ruta de transmisión", + "pathRequired": "La ruta de transmisión es requerida", + "pathPlaceholder": "rtsp://...", + "roles": "Roles", + "rolesRequired": "Al menos un rol es requerido", + "rolesUnique": "Cada rol (audio, detección, grabación) puede únicamente asignarse a una transmisión", + "addInput": "Añadir transmision entrante", + "removeInput": "Elimine transmisión entrante", + "inputsRequired": "Se requiere al menos una transmisión entrante" + }, + "go2rtcStreams": "Transmisiones go2rtc", + "streamUrls": "URLs de transmisión", + "addUrl": "Añadir URL", + "addGo2rtcStream": "Añadir transmisión go2rtc", + "toast": { + "success": "Cámara {{cameraName}} guardada correctamente" + } + }, + "deleteCameraDialog": { + "description": "Eliminar una cámara borrará permanentemente todas las grabaciones, los objetos rastreados y la configuración de esa cámara. Es posible que sea necesario eliminar manualmente cualquier transmisión go2rtc asociada a esta cámara.", + "title": "Eliminar cámara", + "selectPlaceholder": "Elegir cámara...", + "confirmTitle": "¿Estás seguro?", + "confirmWarning": "Eliminar {{cameraName}} no se puede deshacer.", + "deleteExports": "Eliminar también las exportaciones de esta cámara", + "confirmButton": "Eliminar permanentemente", + "success": "La cámara {{cameraName}} se ha eliminado correctamente", + "error": "No se pudo eliminar la cámara {{cameraName}}" + }, + "deleteCamera": "Eliminar cámara", + "profiles": { + "title": "Sobrescrituras de cámaras del perfil", + "selectLabel": "Seleccionar perfil", + "description": "Configura qué cámaras se activan o desactivan cuando se activa un perfil. Las cámaras configuradas como “Heredar” conservan su estado predeterminado.", + "inherit": "Heredar", + "enabled": "Habilitado", + "disabled": "Deshabilitado", + "on": "Encendido", + "off": "Apagado" + }, + "cameraType": { + "title": "Tipo de cámara", + "label": "Tipo de cámara", + "description": "Establece el tipo de cada cámara. Las cámaras LPR dedicadas son cámaras de un solo propósito con un zoom óptico potente para capturar matrículas de vehículos lejanos. La mayoría de cámaras deberían usar el tipo de cámara normal salvo que la cámara esté específicamente destinada a LPR y tenga una vista muy enfocada a matrículas.", + "normal": "Normal", + "dedicatedLpr": "LPR dedicada", + "saveSuccess": "Se ha actualizado el tipo de cámara de {{cameraName}}. Reinicia Frigate para aplicar los cambios." + }, + "description": "Añade, edita y elimina cámaras, controla el estado de cada cámara y configura sobrescrituras por perfil y tipo de cámara. Para configurar flujos, detección, movimiento y otros ajustes específicos de cámara, selecciona la sección correspondiente dentro de Configuración de cámara.", + "clone": { + "sectionTitle": "Clonar configuración", + "sectionDescription": "Copia la configuración de una cámara a otra cámara o a una nueva.", + "button": "Clonar configuración", + "title": "Clonar configuración de la cámara", + "description": "Copia la configuración de una cámara a una o varias cámaras existentes o a una cámara nueva. La identidad de la cámara (nombre, nombre visible, URL de la interfaz web y orden de visualización) nunca se copia.", + "source": { + "label": "Cámara de origen", + "placeholder": "Selecciona una cámara de origen", + "required": "Selecciona una cámara de origen" + }, + "target": { + "legend": "Destino", + "newRadio": "Nueva cámara", + "newNameLabel": "Nombre de la cámara", + "newNamePlaceholder": "p. ej., puerta_trasera o Puerta trasera", + "newNameRequired": "El nombre de la cámara es obligatorio", + "newNameInvalid": "Nombre de cámara no válido", + "newNameCollision": "Ya existe una cámara con este nombre", + "newStreamsForced": "Los flujos siempre se copian al crear una cámara nueva.", + "existingCamerasRadio": "Cámaras existentes", + "allCameras": "Todas las cámaras", + "existingPlaceholder": "Selecciona al menos una cámara", + "existingDisabled": "No hay otras cámaras a las que copiar la configuración" + }, + "categories": { + "legend": "Configuración para clonar", + "description": "Elige qué ajustes copiar desde la cámara de origen.", + "selectAll": "Seleccionar todo", + "selectNone": "No seleccionar ninguno", + "resetDefaults": "Restablecer valores predeterminados", + "general": "General", + "spatial": "Configuración espacial", + "streams": "Flujos", + "spatialWarningTitle": "Resolución no coincidente", + "spatialWarning": "La resolución de detección de la cámara de origen {{srcCamera}} ({{srcWidth}}×{{srcHeight}}) es diferente de la de: {{cameras}}. Es posible que los polígonos no se alineen correctamente en esas cámaras. Estas opciones están desactivadas de forma predeterminada; actívalas para copiarlas tal cual.", + "restartHint": "Reinicio necesario", + "items": { + "record": "Grabación", + "snapshots": "Instantáneas", + "review": "Revisión", + "motion": "Detección de movimiento", + "objects": "Objetos", + "audio": "Detección de audio", + "audio_transcription": "Transcripción de audio", + "notifications": "Notificaciones", + "birdseye": "Birdseye", + "mqtt": "MQTT", + "timestamp_style": "Estilo de marca de tiempo", + "onvif": "ONVIF", + "lpr": "Reconocimiento de matrículas", + "face_recognition": "Reconocimiento facial", + "semantic_search": "Búsqueda semántica", + "genai": "IA generativa", + "type": "Tipo de cámara (normal / LPR dedicada)", + "profiles": "Perfiles", + "detect": "Dimensiones de detección", + "zones": "Zonas", + "motion_mask": "Máscaras de movimiento", + "object_masks": "Máscaras de objetos", + "ffmpeg_live": "URL y roles de los flujos" + } + }, + "footer": { + "changeCount_one": "Se aplicará {{count}} cambio", + "changeCount_many": "Se aplicarán {{count}} cambios", + "changeCount_other": "Se aplicarán {{count}} cambios", + "restartNeeded": "Será necesario reiniciar para aplicar algunos cambios.", + "liveOnly": "Todos los cambios se aplicarán en tiempo real sin necesidad de reiniciar.", + "submit": "Clonar", + "submitting": "Clonando…" + }, + "toast": { + "success": "Configuración copiada a {{cameraName}}", + "successWithRestart": "Configuración copiada a {{cameraName}}. Reinicia Frigate para aplicar todos los cambios.", + "successMulti_one": "Configuración copiada a {{count}} cámara", + "successMulti_many": "Configuración copiada a {{count}} cámaras", + "successMulti_other": "Configuración copiada a {{count}} cámaras", + "successMultiWithRestart_one": "Configuración copiada a {{count}} cámara. Reinicia Frigate para aplicar todos los cambios.", + "successMultiWithRestart_many": "Configuración copiada a {{count}} cámaras. Reinicia Frigate para aplicar todos los cambios.", + "successMultiWithRestart_other": "Configuración copiada a {{count}} cámaras. Reinicia Frigate para aplicar todos los cambios.", + "partialFailure": "Se aplicaron {{successCount}} secciones; ‘{{failedSection}}’ falló: {{errorMessage}}", + "partialFailureMulti": "Copiado a {{successCount}} cámara(s); error en {{failed}}: {{errorMessage}}", + "newCameraPartialFailure": "La cámara {{cameraName}} se creó, pero no se pudieron copiar algunos ajustes: {{errorMessage}}", + "sourceMissing": "La cámara de origen ya no existe", + "submitError": "No se pudo clonar la cámara: {{errorMessage}}" + } + } + }, + "cameraReview": { + "title": "Configuración de revisión de la cámara", + "object_descriptions": { + "title": "Descripciones de objetos de IA generativa", + "desc": "Habilite o deshabilite temporalmente las descripciones de objetos generadas por IA para esta cámara hasta que Frigate se reinicie. Al deshabilitarlas, no se solicitarán descripciones generadas por IA para los objetos rastreados en esta cámara." + }, + "review_descriptions": { + "title": "Revisión de descripciones de IA generativa", + "desc": "Habilita o deshabilita temporalmente las revisión de descripciones generadas por IA para esta cámara hasta que Frigate se reinicie. Al deshabilitarlas, no se solicitarán descripciones generadas por IA para los elementos de revisión de esta cámara." + }, + "review": { + "title": "Revisar", + "desc": "Habilite o deshabilite temporalmente las alertas y detecciones de esta cámara hasta que Frigate se reinicie. Al deshabilitarlas, no se generarán nuevas revisiones. ", + "alerts": "Alertas ", + "detections": "Detecciones " + }, + "reviewClassification": { + "title": "Clasificación de la revisión", + "desc": "Frigate clasifica los elementos de revisión como Alertas y Detecciones. De forma predeterminada, todos los objetos de persona y coche se consideran Alertas. Puede refinar la categorización de sus elementos de revisión configurando las zonas requeridas para ellos.", + "noDefinedZones": "No hay Zonas definidas para esta cámara.", + "objectAlertsTips": "Todos los objetos {{alertsLabels}} en {{cameraName}} se mostrarán como alertas.", + "zoneObjectAlertsTips": "Todos los objetos {{alertsLabels}} detectados en {{zone}} en {{cameraName}} se mostrarán como alertas.", + "objectDetectionsTips": "Todos los objetos {{detectionsLabels}} no categorizados en {{cameraName}} se mostrarán como Detecciones independientemente de la zona en la que se encuentren.", + "zoneObjectDetectionsTips": { + "text": "Todos los objetos {{detectionsLabels}} no categorizados en {{zone}} en {{cameraName}} se mostrarán como Detecciones.", + "notSelectDetections": "Todos los objetos {{detectionsLabels}} detectados en {{zone}} en {{cameraName}} que no estén categorizados como Alertas se mostrarán como Detecciones independientemente de la zona en la que se encuentren.", + "regardlessOfZoneObjectDetectionsTips": "Todos los objetos {{detectionsLabels}} no categorizados en {{cameraName}} se mostrarán como Detecciones independientemente de la zona en la que se encuentren." + }, + "unsavedChanges": "Configuración de clasificación de revisión no guardadas para {{camera}}", + "selectAlertsZones": "Seleccione Zonas para Alertas", + "selectDetectionsZones": "Seleccione Zonas para la Detección", + "limitDetections": "Limite la detección a zonas específicas", + "toast": { + "success": "Se ha guardado la configuración de la clasificación de revisión. Reinicie Frigate para aplicar los cambios." + } + } + }, + "button": { + "overriddenGlobal": "Sobrescrito (Global)", + "overriddenBaseConfigTooltip": "El perfil {{profile}} sobrescribe los ajustes de configuración de esta sección", + "overriddenGlobalTooltip": "Esta cámara sobrescribe los ajustes de configuración global en esta sección", + "overriddenBaseConfig": "Sobrescrito (Configuración Base)", + "overriddenInCameras": { + "label_one": "Sobrescrito en {{count}} cámara", + "label_many": "Sobrescrito en {{count}} cámaras", + "label_other": "Sobrescrito en {{count}} cámaras", + "tooltip_one": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.", + "tooltip_many": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.", + "tooltip_other": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.", + "heading_one": "This global section has fields that are overridden in {{count}} camera.", + "heading_many": "Esta sección global tiene campos que están sobrescritos en {{count}} cámaras.", + "heading_other": "Esta sección global tiene campos que están sobrescritos en {{count}} cámaras.", + "othersField_one": "{{count}} más", + "othersField_many": "{{count}} más", + "othersField_other": "{{count}} más", + "profilePrefix": "Perfil {{profile}}: {{fields}}" + }, + "overriddenGlobalHeading_one": "Esta cámara sobrescribe {{count}} campo de la configuración global:", + "overriddenGlobalHeading_many": "Esta cámara sobrescribe {{count}} campos de la configuración global:", + "overriddenGlobalHeading_other": "Esta cámara sobrescribe {{count}} campos de la configuración global:", + "overriddenGlobalNoDeltas": "Esta cámara sobrescribe la configuración global, pero no hay diferencias en los valores de los campos.", + "overriddenBaseConfigHeading_one": "El perfil {{profile}} sobrescribe {{count}} campo de la configuración base:", + "overriddenBaseConfigHeading_many": "El perfil {{profile}} sobrescribe {{count}} campos de la configuración base:", + "overriddenBaseConfigHeading_other": "El perfil {{profile}} sobrescribe {{count}} campos de la configuración base:", + "overriddenBaseConfigNoDeltas": "El perfil {{profile}} sobrescribe esta sección, pero no hay diferencias en los valores de los campos respecto a la configuración base." + }, + "onvif": { + "profileLoading": "Cargando perfiles...", + "profileAuto": "Auto", + "autotracking": { + "zooming": { + "disabled": "Deshabilitado", + "absolute": "Absoluto", + "relative": "Relativo" + } + } + }, + "maintenance": { + "sync": { + "verboseDesc": "Escribe una lista completa de archivos huérfanos en el disco para su revisión.", + "verbose": "Detallado", + "desc": "Frigate limpiará periódicamente los archivos multimedia según un cronograma regular, de acuerdo con su configuración de retención. Es normal ver algunos archivos huérfanos mientras Frigate se ejecuta. Utilice esta función para eliminar del disco los archivos multimedia huérfanos que ya no se referencian en la base de datos.", + "forceDesc": "Omitir el umbral de seguridad y completar la sincronización incluso si se eliminara más del 50% de los archivos.", + "title": "Sincronización de medios", + "started": "Sincronización de medios iniciada.", + "alreadyRunning": "Ya hay una tarea de sincronización en ejecución", + "error": "No se pudo iniciar la sincronización", + "currentStatus": "Estado", + "jobId": "ID de tarea", + "startTime": "Hora de inicio", + "endTime": "Hora de finalización", + "statusLabel": "Estado", + "results": "Resultados", + "errorLabel": "Error", + "mediaTypes": "Tipos de medios", + "allMedia": "Todos los medios", + "dryRun": "Simulación", + "dryRunEnabled": "No se eliminará ningún archivo", + "dryRunDisabled": "Se eliminarán archivos", + "force": "Forzar", + "running": "Sincronización en curso...", + "start": "Iniciar sincronización", + "inProgress": "La sincronización está en curso. Esta página está deshabilitada.", + "status": { + "queued": "En cola", + "running": "En ejecución", + "completed": "Completado", + "failed": "Fallido", + "notRunning": "No está en ejecución" + }, + "resultsFields": { + "filesChecked": "Archivos comprobados", + "orphansFound": "Huérfanos encontrados", + "orphansDeleted": "Huérfanos eliminados", + "aborted": "Abortado. La eliminación superaría el umbral de seguridad.", + "error": "Error", + "totals": "Totales" + }, + "event_snapshots": "Instantáneas de objetos rastreados", + "event_thumbnails": "Miniaturas de objetos rastreados", + "review_thumbnails": "Miniaturas de revisión", + "previews": "Vistas previas", + "exports": "Exportaciones", + "recordings": "Grabaciones" + }, + "regionGrid": { + "clearConfirmDesc": "No se recomienda borrar la cuadrícula de la región a menos que haya cambiado recientemente el tamaño del modelo de su detector o la posición física de su cámara y esté experimentando problemas de seguimiento de objetos. La cuadrícula se reconstruirá automáticamente con el tiempo a medida que se realice el seguimiento de los objetos. Es necesario reiniciar Frigate para que los cambios surtan efecto.", + "desc": "La cuadrícula de regiones es una optimización que aprende dónde suelen aparecer los objetos de diferentes tamaños en el campo de visión de cada cámara. Frigate utiliza estos datos para dimensionar de forma eficiente las regiones de detección. La cuadrícula se construye automáticamente a lo largo del tiempo a partir de los datos de los objetos rastreados.", + "title": "Cuadrícula de regiones", + "clear": "Borrar cuadrícula de regiones", + "clearConfirmTitle": "Borrar cuadrícula de regiones", + "clearSuccess": "Cuadrícula de regiones borrada correctamente", + "clearError": "No se pudo borrar la cuadrícula de regiones", + "restartRequired": "Es necesario reiniciar para que los cambios de la cuadrícula de regiones surtan efecto" + }, + "title": "Mantenimiento" + }, + "configForm": { + "camera": { + "noCameras": "No hay cámaras disponibles", + "description": "Estos ajustes se aplican únicamente a esta cámara y anulan los ajustes globales.", + "title": "Ajustes de cámara" + }, + "genaiModel": { + "noModels": "No hay modelos disponibles", + "placeholder": "Selecciona o introduce un modelo…", + "search": "Busca o introduce un modelo…", + "available": "Modelos disponibles", + "useCustom": "Usar “{{value}}”", + "refresh": "Actualizar modelos", + "probeFailed": "No se pudieron detectar los modelos", + "fetchedModels": "La lista de modelos se ha obtenido correctamente" + }, + "global": { + "description": "Estos ajustes se aplican a todas las cámaras, a menos que se anulen en los ajustes específicos de cada cámara.", + "title": "Ajustes globales" + }, + "sections": { + "go2rtc": "streams", + "detect": "Detección", + "record": "Grabación", + "snapshots": "Instantáneas", + "motion": "Movimiento", + "objects": "Objetos", + "review": "Revisión", + "audio": "Audio", + "notifications": "Notificaciones", + "live": "Vista en directo", + "timestamp_style": "Marcas de tiempo", + "mqtt": "MQTT", + "database": "Base de datos", + "telemetry": "Telemetría", + "auth": "Autenticación", + "tls": "TLS", + "proxy": "Proxy", + "ffmpeg": "FFmpeg", + "detectors": "Detectores", + "model": "Modelo", + "semantic_search": "Búsqueda semántica", + "genai": "GenAI", + "face_recognition": "Reconocimiento facial", + "lpr": "Reconocimiento de matrículas", + "birdseye": "Birdseye", + "masksAndZones": "Máscaras / zonas" + }, + "advancedSettingsCount": "Ajustes avanzados ({{count}})", + "advancedCount": "Avanzado ({{count}})", + "showAdvanced": "Mostrar ajustes avanzados", + "tabs": { + "sharedDefaults": "Valores predeterminados compartidos", + "system": "Sistema", + "integrations": "Integraciones" + }, + "additionalProperties": { + "keyLabel": "Clave", + "valueLabel": "Valor", + "keyPlaceholder": "Nueva clave", + "remove": "Eliminar" + }, + "knownPlates": { + "namePlaceholder": "p. ej., Coche de mi mujer", + "platePlaceholder": "Número de matrícula o regex" + }, + "timezone": { + "defaultOption": "Usar zona horaria del navegador" + }, + "roleMap": { + "empty": "No hay asignaciones de roles", + "roleLabel": "Rol", + "groupsLabel": "Grupos", + "addMapping": "Añadir asignación de rol", + "remove": "Eliminar" + }, + "ffmpegArgs": { + "preset": "Preajuste", + "manual": "Argumentos manuales", + "inherit": "Heredar del ajuste de cámara", + "none": "Ninguno", + "useGlobalSetting": "Heredar del ajuste global", + "selectPreset": "Seleccionar preajuste", + "manualPlaceholder": "Introduce argumentos de FFmpeg", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (GPU Intel/AMD)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "GPU NVIDIA", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (genérico)", + "preset-http-mjpeg-generic": "HTTP MJPEG (genérico)", + "preset-http-reolink": "HTTP - Cámaras Reolink", + "preset-rtmp-generic": "RTMP (genérico)", + "preset-rtsp-generic": "RTSP (genérico)", + "preset-rtsp-restream": "RTSP - Retransmisión desde go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - Retransmisión desde go2rtc (baja latencia)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Grabación (genérica, sin audio)", + "preset-record-generic-audio-copy": "Grabación (genérica + copiar audio)", + "preset-record-generic-audio-aac": "Grabación (genérica + audio a AAC)", + "preset-record-mjpeg": "Grabación - Cámaras MJPEG", + "preset-record-jpeg": "Grabación - Cámaras JPEG", + "preset-record-ubiquiti": "Grabación - Cámaras Ubiquiti" + } + }, + "cameraInputs": { + "itemTitle": "Flujo {{index}}", + "sourceMode": { + "restream": "Retransmisión (go2rtc)", + "manual": "Ruta de entrada manual", + "go2rtcStreamLabel": "Flujo go2rtc", + "go2rtcStreamPlaceholder": "Selecciona un flujo go2rtc", + "noGo2rtcStreams": "No hay flujos go2rtc configurados", + "go2rtcStreamSearch": "Buscar flujos…", + "availableStreams": "Flujos disponibles", + "noMatchingStreams": "No hay flujos coincidentes" + } + }, + "restartRequiredField": "Reinicio necesario", + "restartRequiredFooter": "Configuración modificada - reinicio necesario", + "detect": { + "title": "Ajustes de detección" + }, + "detectors": { + "title": "Ajustes de detector", + "singleType": "Solo se permite un detector {{type}}.", + "keyRequired": "El nombre del detector es obligatorio.", + "keyDuplicate": "El nombre del detector ya existe.", + "noSchema": "No hay esquemas de detector disponibles.", + "none": "No hay instancias de detector configuradas.", + "add": "Añadir detector", + "addCustomKey": "Añadir clave personalizada" + }, + "record": { + "title": "Ajustes de grabación" + }, + "snapshots": { + "title": "Ajustes de instantáneas" + }, + "motion": { + "title": "Ajustes de movimiento" + }, + "objects": { + "title": "Ajustes de objetos" + }, + "audioLabels": { + "summary": "{{count}} etiquetas de audio seleccionadas", + "empty": "No hay etiquetas de audio disponibles" + }, + "objectLabels": { + "summary": "{{count}} tipos de objeto seleccionados", + "empty": "No hay etiquetas de objeto disponibles" + }, + "reviewLabels": { + "summary": "{{count}} etiquetas seleccionadas", + "empty": "No hay etiquetas disponibles" + }, + "filters": { + "objectFieldLabel": "{{field}} para {{label}}" + }, + "zoneNames": { + "summary": "{{count}} seleccionados", + "empty": "No hay zonas disponibles" + }, + "inputRoles": { + "summary": "{{count}} roles seleccionados", + "empty": "No hay roles disponibles", + "options": { + "detect": "Detectar", + "record": "Grabar", + "audio": "Audio" + } + }, + "genaiRoles": { + "options": { + "embeddings": "Embedding", + "descriptions": "Descripciones", + "chat": "Chat" + } + }, + "semanticSearchModel": { + "placeholder": "Seleccionar modelo…", + "builtIn": "Modelos integrados", + "genaiProviders": "Proveedores de GenAI" + }, + "review": { + "title": "Ajustes de revisión" + }, + "audio": { + "title": "Ajustes de audio" + }, + "notifications": { + "title": "Ajustes de notificaciones" + }, + "live": { + "title": "Ajustes de vista en directo" + }, + "timestamp_style": { + "title": "Ajustes de marcas de tiempo" + }, + "searchPlaceholder": "Buscar...", + "addCustomLabel": "Añadir etiqueta personalizada...", + "semanticSearchModelSize": { + "notApplicable": "No aplicable a proveedores GenAI" + }, + "liveStreams": { + "streamNameLabel": "Nombre del flujo", + "streamNamePlaceholder": "p. ej., Flujo principal HD", + "go2rtcStreamLabel": "Flujo go2rtc", + "go2rtcStreamPlaceholder": "Selecciona un flujo go2rtc", + "go2rtcStreamSearch": "Busca o introduce un nombre de flujo…", + "noGo2rtcStreams": "No hay flujos go2rtc configurados", + "availableStreams": "Flujos disponibles", + "useCustom": "Usar “{{value}}”", + "addStream": "Añadir flujo" + }, + "ptzPresets": { + "placeholder": "Selecciona o introduce un preajuste…", + "search": "Busca o introduce un preajuste…", + "noPresets": "No hay preajustes disponibles", + "available": "Preajustes de cámara", + "useCustom": "Usar “{{value}}”" + }, + "defaultRole": { + "admin": "Admin", + "viewer": "Visualizador" + } + }, + "globalConfig": { + "title": "Configuración global", + "description": "Configura los ajustes globales que se aplican a todas las cámaras, a menos que se sobrescriban.", + "toast": { + "success": "Ajustes globales guardados con éxito", + "error": "Error al guardar los ajustes globales", + "validationError": "Error de validación" + } + }, + "cameraConfig": { + "title": "Configuración de cámara", + "description": "Configura los ajustes de cámaras individuales. Estos ajustes sobrescriben los valores globales predeterminados.", + "overriddenBadge": "Sobrescrito", + "resetToGlobal": "Restablecer al valor global", + "toast": { + "success": "Ajustes de cámara guardados con éxito", + "error": "Error al guardar los ajustes de cámara" + } + }, + "toast": { + "success": "Ajustes guardados con éxito", + "applied": "Ajustes aplicados con éxito", + "successRestartRequired": "Ajustes guardados con éxito. Reinicia Frigate para aplicar los cambios.", + "error": "Error al guardar los ajustes", + "validationError": "Error de validación: {{message}}", + "resetSuccess": "Restablecido a los valores globales predeterminados", + "resetError": "Error al restablecer los ajustes", + "saveAllSuccess_one": "Se ha guardado {{count}} sección con éxito.", + "saveAllSuccess_many": "Se han guardado las {{count}} secciones con éxito.", + "saveAllSuccess_other": "Se han guardado {{count}} secciones con éxito.", + "saveAllPartial_one": "Se ha guardado {{successCount}} de {{totalCount}} sección. {{failCount}} ha fallado.", + "saveAllPartial_many": "Se han guardado {{successCount}} de {{totalCount}} secciones. {{failCount}} han fallado.", + "saveAllPartial_other": "Se han guardado {{successCount}} de {{totalCount}} secciones. {{failCount}} han fallado.", + "saveAllFailure": "Error al guardar todas las secciones.", + "saveAllSuccessRestartRequired_one": "La sección {{count}} se ha guardado correctamente. Reinicia Frigate para aplicar los cambios.", + "saveAllSuccessRestartRequired_many": "Las {{count}} secciones se han guardado correctamente. Reinicia Frigate para aplicar los cambios.", + "saveAllSuccessRestartRequired_other": "Las {{count}} secciones se han guardado correctamente. Reinicia Frigate para aplicar los cambios." + }, + "profiles": { + "title": "Perfiles", + "activeProfile": "Perfil activo", + "noActiveProfile": "Sin perfil activo", + "active": "Activo", + "activated": "Perfil '{{profile}}' activado", + "activateFailed": "Error al establecer el perfil", + "deactivated": "Perfil desactivado", + "noProfiles": "No hay perfiles definidos.", + "noOverrides": "Sin sobrescripciones", + "cameraCount_one": "{{count}} cámara", + "cameraCount_many": "{{count}} de cámaras", + "cameraCount_other": "{{count}} cámaras", + "columnCamera": "Cámara", + "columnOverrides": "Sobrescripciones del perfil", + "baseConfig": "Configuración base", + "addProfile": "Añadir perfil", + "newProfile": "Nuevo perfil", + "profileNamePlaceholder": "ej. Armado, Fuera de casa, Modo noche", + "friendlyNameLabel": "Nombre del perfil", + "profileIdLabel": "ID del perfil", + "profileIdDescription": "Identificador interno utilizado en la configuración y automatizaciones", + "nameInvalid": "Solo se permiten letras minúsculas, números y guiones bajos", + "nameDuplicate": "Ya existe un perfil con este nombre", + "error": { + "mustBeAtLeastTwoCharacters": "Debe tener al menos 2 caracteres", + "mustNotContainPeriod": "No debe contener puntos", + "alreadyExists": "Ya existe un perfil con este ID" + }, + "renameProfile": "Renombrar perfil", + "renameSuccess": "Perfil renombrado a '{{profile}}'", + "enabledDescription": "Los perfiles están habilitados. Cree un nuevo perfil a continuación, navegue a una sección de configuración de cámara para realizar sus cambios y guarde para que estos surtan efecto.", + "disabledDescription": "Los perfiles le permiten definir conjuntos con nombre de anulaciones de configuración de la cámara (por ejemplo: armado, fuera, noche) que pueden activarse bajo demanda.", + "deleteProfile": "Eliminar perfil", + "deleteProfileConfirm": "¿Eliminar el perfil \"{{profile}}\" de todas las cámaras? Esta acción no se puede deshacer.", + "deleteSuccess": "Perfil '{{profile}}' eliminado", + "createSuccess": "Perfil '{{profile}}' creado", + "removeOverride": "Eliminar sobrescritura de perfil", + "deleteSection": "Eliminar sobrescrituras de sección", + "deleteSectionConfirm": "¿Eliminar las sobrescrituras de {{section}} del perfil {{profile}} en {{camera}}?", + "deleteSectionSuccess": "Sobrescrituras de {{section}} eliminadas para {{profile}}", + "enableSwitch": "Habilitar perfiles" + }, + "go2rtcStreams": { + "renameStreamDesc": "Introduce un nuevo nombre para esta transmisión. Cambiar el nombre de una transmisión puede provocar fallos en las cámaras u otras transmisiones que hagan referencia a ella por su nombre.", + "addStreamDesc": "Introduce un nombre para la nueva transmisión. Este nombre se utilizará para hacer referencia a la transmisión en la configuración de su cámara.", + "description": "Gestione las configuraciones de transmisión de go2rtc para la retransmisión de cámaras. Cada transmisión tiene un nombre y una o más URL de origen.", + "deleteStreamConfirm": "¿Está seguro de que desea eliminar la transmisión \"{{streamName}}\"? Las cámaras que hagan referencia a esta transmisión podrían dejar de funcionar.", + "title": "Flujos go2rtc", + "addStream": "Añadir flujo", + "addUrl": "Añadir URL", + "streamName": "Nombre del flujo", + "streamNamePlaceholder": "p. ej., puerta_principal", + "streamUrlPlaceholder": "p. ej., rtsp://usuario:contraseña@192.168.1.100/stream", + "deleteStream": "Eliminar flujo", + "noStreams": "No hay flujos go2rtc configurados. Añade un flujo para empezar.", + "validation": { + "nameRequired": "El nombre del flujo es obligatorio", + "nameDuplicate": "Ya existe un flujo con este nombre", + "nameInvalid": "El nombre del flujo solo puede contener letras, números, guiones bajos y guiones", + "urlRequired": "Se requiere al menos una URL" + }, + "renameStream": "Renombrar flujo", + "newStreamName": "Nuevo nombre del flujo", + "ffmpeg": { + "useFfmpegModule": "Usar modo de compatibilidad (ffmpeg)", + "video": "Vídeo", + "audio": "Audio", + "hardware": "Aceleración por hardware", + "videoCopy": "Copiar", + "videoH264": "Transcodificar a H.264", + "videoH265": "Transcodificar a H.265", + "videoExclude": "Excluir", + "audioCopy": "Copiar", + "audioAac": "Transcodificar a AAC", + "audioOpus": "Transcodificar a Opus", + "audioPcmu": "Transcodificar a PCM μ-law", + "audioPcma": "Transcodificar a PCM A-law", + "audioPcm": "Transcodificar a PCM", + "audioMp3": "Transcodificar a MP3", + "audioExclude": "Excluir", + "hardwareNone": "Sin aceleración por hardware", + "hardwareAuto": "Automático (recomendado)", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "Añadir códec de vídeo", + "addAudioCodec": "Añadir códec de audio", + "removeCodec": "Eliminar códec" + }, + "streamNumber": "Flujo {{index}}", + "sourceNumber": "Origen {{index}}" + }, + "configMessages": { + "birdseye": { + "objectsModeDetectDisabled": "Birdseye está configurado en modo 'objects', pero la detección de objetos está desactivada para esta cámara. La cámara no aparecerá en Birdseye." + }, + "lpr": { + "globalDisabled": "El reconocimiento de matrículas no está habilitado a nivel global. Habilítelo en la configuración global para que funcione el reconocimiento de matrículas a nivel de cámara.", + "vehicleNotTracked": "El reconocimiento de matrículas requiere rastrear 'car' o 'motorcycle'. Habilita 'car' o 'motorcycle' en Objetos para esta cámara.", + "modelSizeLarge": "El modelo 'large' está optimizado para matrículas de varias líneas. El modelo 'small' ofrece mejor rendimiento que 'large' y debería usarse salvo que tu región use formatos de matrícula de varias líneas." + }, + "audio": { + "noAudioRole": "Ninguna transmisión tiene definido el rol de audio. Debe habilitar el rol de audio para que funcione la detección de audio." + }, + "faceRecognition": { + "personNotTracked": "El reconocimiento facial requiere que se realice el seguimiento del objeto 'person'. Asegúrese de que 'person' se encuentre en la lista de seguimiento de objetos.", + "globalDisabled": "El enriquecimiento de reconocimiento facial debe estar habilitado para que las funciones de reconocimiento facial funcionen en esta cámara.", + "modelSizeLarge": "El modelo 'large' requiere una GPU o NPU para ofrecer un rendimiento razonable. Usa 'small' en sistemas solo con CPU." + }, + "audioTranscription": { + "audioDetectionDisabled": "La detección de audio no está habilitada para esta cámara. La transcripción de audio requiere que la detección de audio esté activa." + }, + "snapshots": { + "detectDisabled": "La detección de objetos está desactivada. Las instantáneas se generan a partir de los objetos rastreados y no se crearán." + }, + "detectors": { + "mixedTypes": "Todos los detectores deben ser del mismo tipo. Retire los detectores existentes para utilizar un tipo diferente.", + "mixedTypesSuggestion": "Todos los detectores deben usar el mismo tipo. Elimina los detectores existentes o selecciona {{type}}." + }, + "review": { + "detectDisabled": "La detección de objetos está desactivada. Los elementos de revisión requieren objetos detectados para categorizar las alertas y detecciones.", + "recordDisabled": "La grabación está deshabilitada; no se generarán elementos de revisión.", + "allNonAlertDetections": "Toda la actividad que no sea de alerta se incluirá como detecciones.", + "genaiImageSourceRecordingsRecordDisabled": "El origen de imagen está establecido en 'recordings', pero la grabación está deshabilitada. Frigate usará imágenes de vista previa como alternativa." + }, + "detect": { + "fpsGreaterThanFive": "No se recomienda establecer los FPS de detección por encima de 5. Valores más altos pueden causar problemas de rendimiento y no aportarán ningún beneficio.", + "disabled": "La detección de objetos está deshabilitada. Las instantáneas, los elementos de revisión y enriquecimientos como el reconocimiento facial, el reconocimiento de matrículas y la IA generativa no funcionarán.", + "resolutionShouldBeMultipleOfFour": "Para obtener mejores resultados, la anchura y la altura de detección deberían ser múltiplos de 4. Otros valores pares pueden producir artefactos visuales o una ligera distorsión en el flujo de detección.", + "aspectRatioMismatch": "La anchura y la altura que has introducido no coinciden con la relación de aspecto de la resolución de detección actual. Esto puede producir una imagen estirada o distorsionada.", + "maxFramesSet": "Establecer un número máximo de fotogramas (frames) reemplaza el comportamiento predeterminado y desactiva el seguimiento de objetos estáticos. Hay muy pocas situaciones en las que esto sea necesario; utilícelo con precaución.", + "squareResolution": "Una resolución de detección cuadrada es poco habitual. El ancho y la altura de detección deberían coincidir con la relación de aspecto de tu cámara (por ejemplo, 16:9), no con las dimensiones del modelo de detección de objetos. Una relación de aspecto incorrecta puede distorsionar la imagen y reducir la precisión de la detección.", + "resolutionHigh": "Esta resolución de detección es superior a la recomendada y puede provocar un mayor consumo de recursos sin mejorar la precisión de la detección. Para la mayoría de las cámaras se recomienda una resolución de detección de 1080p o inferior.", + "globalResolutionMultipleCameras": "La resolución de detección global se establece al configurar varias cámaras. A menos que todas las cámaras compartan la misma resolución y relación de aspecto, se deben definir el ancho y la altura de detección en cada cámara, de modo que se ajusten a la relación de aspecto nativa de cada una." + }, + "objects": { + "genaiNoDescriptionsProvider": "Debes configurar un proveedor GenAI con el rol 'descriptions' para que se generen descripciones." + }, + "record": { + "noRecordRole": "Ningún flujo tiene definido el rol de grabación. La grabación no funcionará." + }, + "semanticSearch": { + "jinav2SmallModelSize": "El tamaño 'small' con el modelo Jina V2 tiene un alto consumo de RAM y coste de inferencia. Se recomienda el modelo 'large' con una GPU dedicada.", + "modelSizeIgnoredForProvider": "El tamaño del modelo solo se aplica a los modelos Jina integrados. Este valor se ignorará al usar un proveedor de embeddings GenAI." + }, + "onvif": { + "autotrackingNoZones": "El seguimiento automático requiere al menos una zona. Define una zona para esta cámara en Máscaras / Zonas y, a continuación, establécela como zona obligatoria a continuación." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "No son remontados los argumentos de aceleración por hardware manual. A no ser que un requisito específico exista, seleccione la preselección que coincida con su hardware." + } + }, + "resetToDefaultDescription": "Esto restablecerá todos los ajustes de esta sección a sus valores predeterminados. Esta acción no se puede deshacer.", + "resetToGlobalDescription": "Esto restablecerá la configuración de esta sección a los valores predeterminados globales. Esta acción no se puede deshacer.", + "detectionModel": { + "plusActive": { + "description": "Esta instancia está ejecutando un modelo de Frigate+. Seleccione o cambie su modelo en la configuración de Frigate+.", + "title": "Gestión de modelos de Frigate+", + "label": "Origen del modelo actual", + "goToFrigatePlus": "Ir a los ajustes de Frigate+", + "showModelForm": "Configurar un modelo manualmente" + } + }, + "saveAllPreview": { + "profile": { + "label": "Override,Eliminar" + }, + "title": "Cambios pendientes de guardar", + "triggerLabel": "Revisar cambios pendientes", + "empty": "No hay cambios pendientes.", + "scope": { + "label": "Ámbito", + "global": "Global", + "camera": "Cámara: {{cameraName}}" + }, + "field": { + "label": "Campo" + }, + "value": { + "label": "Nuevo valor", + "reset": "Restablecer" + } + }, + "timestampPosition": { + "tl": "Arriba a la izquierda", + "tr": "Arriba a la derecha", + "bl": "Abajo a la izquierda", + "br": "Abajo a la derecha" + }, + "unsavedChanges": "Tienes cambios sin guardar", + "confirmReset": "Confirmar restablecimiento", + "birdseye": { + "trackingMode": { + "objects": "Objetos", + "motion": "Movimiento", + "continuous": "Continuo" + }, + "cameraOrder": { + "label": "Orden de cámaras", + "description": "Arrastra las cámaras para establecer su orden en el diseño de Birdseye.", + "reorderHandle": "Arrastrar para reordenar", + "saving": "Guardando…", + "saved": "Guardado" + } + }, + "snapshot": { + "retainMode": { + "all": "Todo", + "motion": "Movimiento", + "active_objects": "Objetos activos" + } + }, + "ui": { + "timeFormat": { + "browser": "Navegador", + "12hour": "12 horas", + "24hour": "24 horas" + }, + "TimeOrDateStyle": { + "full": "Completo", + "long": "Largo", + "medium": "Medio", + "short": "Corto" + }, + "unitSystem": { + "metric": "Métrico", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Grabaciones", + "previews": "Vistas previas" + } + }, + "logger": { + "logLevel": { + "debug": "Depuración", + "info": "Información", + "warning": "Advertencia", + "error": "Error", + "critical": "Crítico" + } + }, + "modelSize": { + "small": "Pequeño", + "large": "Grande" + }, + "retainMode": { + "all": "Todo", + "motion": "Movimiento", + "active_objects": "Objetos activos" + }, + "previewQuality": { + "very_high": "Muy alto", + "high": "Alto", + "medium": "Medio", + "low": "Bajo", + "very_low": "Muy bajo" + }, + "detectorsAndModel": { + "title": "Detectores y modelo", + "description": "Configura el backend del detector que ejecuta la detección de objetos y el modelo que utiliza. Los cambios se guardan juntos para que el detector y el modelo permanezcan sincronizados.", + "cardTitles": { + "detector": "Hardware del detector", + "model": "Modelo de detección" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Modelo personalizado" + }, + "mismatch": { + "warning": "El modelo actual de Frigate+ “{{model}}” requiere el detector {{required}}. Selecciona un modelo compatible a continuación o cambia a Modelo personalizado antes de guardar." + }, + "plusModel": { + "requiresDetector": "Requiere: {{detector}}", + "noModelSelected": "Selecciona un modelo de Frigate+" + }, + "toast": { + "saveSuccess": "Los ajustes de detectores y modelo se han guardado. Reinicia Frigate para aplicar los cambios.", + "saveError": "No se pudieron guardar los ajustes del detector y del modelo" + }, + "unsavedChanges": "Cambios sin guardar en el detector y el modelo", + "restartRequired": "Reinicio necesario (se ha cambiado el detector o el modelo)" + }, + "menuDot": { + "overrideGlobal": "Esta sección sobrescribe la configuración global", + "overrideProfile": "Esta sección está sobrescrita por el perfil {{profile}}", + "unsaved": "Esta sección tiene cambios sin guardar" + } +} diff --git a/web/public/locales/es/views/system.json b/web/public/locales/es/views/system.json new file mode 100644 index 0000000..09f26fe --- /dev/null +++ b/web/public/locales/es/views/system.json @@ -0,0 +1,274 @@ +{ + "documentTitle": { + "storage": "Estadísticas de almacenamiento - Frigate", + "general": "Estadísticas generales - Frigate", + "logs": { + "frigate": "Registros de Frigate - Frigate", + "go2rtc": "Registros de Go2RTC - Frigate", + "nginx": "Registros de Nginx - Frigate", + "websocket": "Mensajes Logs - Frigata" + }, + "cameras": "Estadísticas de cámaras - Frigate", + "enrichments": "Estadísticas de Enriquecimientos - Frigate" + }, + "logs": { + "copy": { + "label": "Copiar al portapapeles", + "success": "Registros copiados al portapapeles", + "error": "No se pudieron copiar los registros al portapapeles" + }, + "type": { + "label": "Tipo", + "timestamp": "Marca de tiempo", + "tag": "Etiqueta", + "message": "Mensaje" + }, + "tips": "Los registros se están transmitiendo desde el servidor", + "toast": { + "error": { + "fetchingLogsFailed": "Error al obtener los registros: {{errorMessage}}", + "whileStreamingLogs": "Error mientras se transmitían los registros: {{errorMessage}}" + } + }, + "download": { + "label": "Descargar registros" + }, + "websocket": { + "label": "Mensajes", + "pause": "Pausar", + "resume": "Continuar", + "clear": "Limpiar", + "filter": { + "all": "Todos los temas", + "topics": "Temas", + "events": "Eventos", + "reviews": "Revisiones", + "face_recognition": "Reconocimiento facial", + "camera_activity": "Actividad de cámara", + "classification": "Clasificación", + "system": "Sistema", + "camera": "Cámara", + "all_cameras": "Todas las cámaras", + "cameras_count_one": "{{count}} Cámara", + "cameras_count_other": "{{count}} Cámaras", + "lpr": "Reconocimiento de matriculas" + }, + "count_other": "{{count}} mensajes", + "count_one": "{{count}} mensaje", + "empty": "No se han capturado mensaje aún", + "expanded": { + "payload": "Carga útil" + } + } + }, + "title": "Sistema", + "metrics": "Métricas del sistema", + "general": { + "title": "General", + "detector": { + "title": "Detectores", + "inferenceSpeed": "Velocidad de inferencia del detector", + "cpuUsage": "Uso de CPU del Detector", + "memoryUsage": "Uso de Memoria del Detector", + "temperature": "Detector de Temperatura", + "cpuUsageInformation": "CPU utilizado para preparar los datos de entrada y salida desde/hacia la detección del modelo. Este valor no mide el uso de inferencia, incluso si se está utilizando una GPU o un acelerador." + }, + "hardwareInfo": { + "title": "Información de Hardware", + "gpuUsage": "Uso de GPU", + "gpuEncoder": "Codificador de GPU", + "gpuDecoder": "Decodificador de GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Salida de Vainfo", + "returnCode": "Código de retorno: {{code}}", + "processOutput": "Salida del proceso:", + "processError": "Error del proceso:" + }, + "nvidiaSMIOutput": { + "cudaComputerCapability": "Capacidad de cómputo CUDA: {{cuda_compute}}", + "title": "Salida de Nvidia SMI", + "driver": "Controlador: {{driver}}", + "name": "Nombre: {{name}}", + "vbios": "Informe de VBios: {{vbios}}" + }, + "toast": { + "success": "Información de GPU copiada al portapapeles" + }, + "copyInfo": { + "label": "Copiar información de GPU" + }, + "closeInfo": { + "label": "Cerrar información de GPU" + } + }, + "gpuMemory": "Memoria de GPU", + "npuMemory": "Memoria de NPU", + "npuUsage": "Modo de empleo de NPU", + "intelGpuWarning": { + "title": "Aviso de estadísticas Intel GPU", + "message": "Estadísticas de GPU no disponibles", + "description": "Este es un error conocido en las herramientas de informes de estadísticas de GPU de Intel (intel_gpu_top). El error se produce y muestra repetidamente un uso de GPU del 0 %, incluso cuando la aceleración de hardware y la detección de objetos se ejecutan correctamente en la (i)GPU. No se trata de un error de Frigate. Puede reiniciar el host para solucionar el problema temporalmente y confirmar que la GPU funciona correctamente. Esto no afecta al rendimiento." + }, + "npuTemperature": "Temperatura NPU", + "gpuCompute": "Cálculo GPU / Codificación", + "gpuTemperature": "Temperatura GPU" + }, + "otherProcesses": { + "title": "Otros Procesos", + "processCpuUsage": "Uso de CPU del Proceso", + "processMemoryUsage": "Uso de Memoria del Proceso", + "series": { + "go2rtc": "go2rtc", + "recording": "grabación", + "review_segment": "revisar segmento", + "embeddings": "empotrados", + "audio_detector": "detector de audio" + } + } + }, + "storage": { + "recordings": { + "title": "Grabaciones", + "tips": "Este valor representa el almacenamiento total utilizado por las grabaciones en la base de datos de Frigate. Frigate no realiza un seguimiento del uso de almacenamiento de todos los archivos en tu disco.", + "earliestRecording": "Grabación más antigua disponible:" + }, + "overview": "Resumen", + "title": "Almacenamiento", + "cameraStorage": { + "percentageOfTotalUsed": "Porcentaje del Total", + "bandwidth": "Ancho de Banda", + "camera": "Cámara", + "unused": { + "title": "No Utilizado", + "tips": "Este valor puede no representar con precisión el espacio libre disponible para Frigate si tienes otros archivos almacenados en tu disco además de las grabaciones de Frigate. Frigate no realiza un seguimiento del uso de almacenamiento fuera de sus grabaciones." + }, + "title": "Almacenamiento de la Cámara", + "storageUsed": "Almacenamiento", + "unusedStorageInformation": "Información de Almacenamiento No Utilizado" + }, + "shm": { + "title": "Asignación de SHM (memoria compartida)", + "warning": "El tamaño actual de SHM de {{total}}MB es muy pequeño. Aumente al menos a {{min_shm}}MB.", + "frameLifetime": { + "title": "Tiempo de vida del fotograma", + "description": "Cada cámara tiene espacio en la memoria compartida para {{frames}} cuadros. Si la velocidad de cuadros de la cámara es alta, cada cuadro se guarda aproximadamente {{lifetime}} antes de ser sobreescrito." + } + } + }, + "cameras": { + "title": "Cámaras", + "overview": "Resumen", + "info": { + "cameraProbeInfo": "Información de Sondeo de la Cámara {{camera}}", + "streamDataFromFFPROBE": "Los datos del flujo se obtienen con ffprobe.", + "codec": "Codec:", + "fetching": "Obteniendo Datos de la Cámara", + "stream": "Flujo {{idx}}", + "video": "Vídeo:", + "fps": "CPS:", + "resolution": "Resolución:", + "error": "Error: {{error}}", + "unknown": "Desconocido", + "audio": "Audio:", + "tips": { + "title": "Información de Sondeo de la Cámara" + }, + "aspectRatio": "Relación de aspecto", + "keyframes": { + "title": "Análisis de clave fotograma", + "recordStream": "Flujo de grabación:", + "segmentLength": "Longitud de segmento en grabación:", + "unknown": "No pudo determinar el espaciado del fotograma.", + "analyzing": "Analizando fotogramas clave… {{seconds}} segundos restantes", + "stillAnalyzing": "Todavía analizando fotogramas clave…", + "keyframeCount": "Fotogramas clave observados:", + "observedDuration": "Duración observada:", + "gap": "Brecha de fotogramas clave (mín / med / máx):", + "recordDisabled": "La grabación está desactivada para esta cámara.", + "ok": "Cuadros cada ~{{seconds}}s, bueno para grabación y reproducción.", + "warning": "Fotograma clave escaso o variable (hueco más largo ~{{seconds}}s), probablemente un códec inteligente (H.264+/H.265+), esto no es recomendado.", + "error": "El intervalo entre fotogramas clave (~{{seconds}} s) supera la duración del segmento de grabación ({{segmentTime}} s). Es posible que algunos segmentos no contengan ningún fotograma clave, lo que impedirá su reproducción. Desactiva el códec inteligente (+) de la cámara o reduce el intervalo entre fotogramas clave." + } + }, + "framesAndDetections": "Fotogramas / Detecciones", + "label": { + "camera": "cámara", + "skipped": "omitido", + "detect": "detectar", + "ffmpeg": "FFmpeg", + "capture": "captura", + "overallFramesPerSecond": "cuadros por segundo totales", + "overallDetectionsPerSecond": "detecciones por segundo totales", + "cameraSkippedDetectionsPerSecond": "{{camName}} detecciones omitidas por segundo", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} captura", + "cameraDetect": "{{camName}} detectar", + "cameraFramesPerSecond": "{{camName}} cuadros por segundo", + "cameraDetectionsPerSecond": "{{camName}} detecciones por segundo", + "overallSkippedDetectionsPerSecond": "detecciones omitidas por segundo totales", + "cameraGpu": "{{camName}} GPU" + }, + "toast": { + "success": { + "copyToClipboard": "Datos de sondeo copiados al portapapeles." + }, + "error": { + "unableToProbeCamera": "No se pudo sondear la cámara: {{errorMessage}}" + } + }, + "connectionQuality": { + "excellent": "Excelente", + "poor": "Pobre", + "title": "Calidad de Conexión", + "fps": "CPS", + "expectedFps": "Cuadros por segundo esperados", + "reconnectsLastHour": "Reconexiones (última hora)", + "unusable": "No usable", + "fair": "Normal", + "stallsLastHour": "Bloqueos (última hora)" + }, + "noCameras": { + "title": "No se han encontrado cámaras" + } + }, + "lastRefreshed": "Última actualización: ", + "enrichments": { + "infPerSecond": "Inferencias Por Segundo", + "embeddings": { + "plate_recognition_speed": "Velocidad de Reconocimiento de Matrículas", + "face_embedding_speed": "Velocidad de Empotrado Facial", + "image_embedding_speed": "Velocidad de Empotrado de Imagen", + "text_embedding_speed": "Velocidad de Empotrado de Texto", + "face_recognition_speed": "Velocidad de Reconocimiento Facial", + "text_embedding": "Empotrado de Texto", + "face_recognition": "Reconocimiento Facial", + "plate_recognition": "Reconocimiento de Matrículas", + "yolov9_plate_detection": "Detección de Matrículas YOLOv9", + "image_embedding": "Empotrado de Imagen", + "yolov9_plate_detection_speed": "Velocidad de Detección de Matrículas YOLOv9", + "review_description": "Revisión de descripción", + "review_description_speed": "Revisión de velocidad de descripción", + "review_description_events_per_second": "Revisión de Descripción", + "object_description": "Descripción de Objeto", + "object_description_speed": "Velocidad de Descripción de Objeto", + "object_description_events_per_second": "Descripción de Objeto", + "classification": "Clasificación de {{name}}", + "classification_speed": "Velocidad de Clasificación de {{name}}", + "classification_events_per_second": "Clasificación de Eventos por Segundo de {{name}}" + }, + "title": "Enriquecimientos", + "averageInf": "Tiempo promedio de inferencia" + }, + "stats": { + "ffmpegHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por detección ({{detectAvg}}%)", + "healthy": "El sistema está saludable", + "reindexingEmbeddings": "Reindexando empotrados ({{processed}}% completado)", + "detectIsSlow": "{{detect}} es lento ({{speed}} ms)", + "cameraIsOffline": "{{camera}} está desconectada", + "detectIsVerySlow": "{{detect}} es muy lento ({{speed}} ms)", + "shmTooLow": "Asignación de /dev/shm ({{total}} MB) debe aumentarse al menos a {{min}} MB.", + "debugReplayActive": "Sesión de depuración activa" + } +} diff --git a/web/public/locales/et/audio.json b/web/public/locales/et/audio.json new file mode 100644 index 0000000..541ffa8 --- /dev/null +++ b/web/public/locales/et/audio.json @@ -0,0 +1,277 @@ +{ + "bicycle": "Jalgratas", + "car": "Auto", + "motorcycle": "Mootorratas", + "bus": "Buss", + "train": "Rong", + "boat": "Väike laev", + "bird": "Lind", + "cat": "Kass", + "dog": "Koer", + "horse": "Hobune", + "sheep": "Lammas", + "skateboard": "Rula", + "breathing": "Hingamine", + "wheeze": "Kähinal hingamine", + "snoring": "Norskamine", + "pets": "Lemmikloomad", + "animal": "Loom", + "children_playing": "Laste mängimine", + "crowd": "Rahvamass", + "applause": "Plaksutamine", + "heartbeat": "Südamelöök", + "heart_murmur": "Südamekahin", + "clapping": "Käteplagin", + "finger_snapping": "Sõrmede naksutamine", + "hands": "Käed", + "camera": "Kaamera", + "speech": "Kõne", + "babbling": "Lobisemine", + "yell": "Karjumine", + "bellow": "Röökimine", + "whoop": "Kisamine", + "whispering": "Sosistamine", + "laughter": "Naermine", + "snicker": "Itsitamine", + "sigh": "Ohkamine", + "crying": "Nutmine", + "singing": "Laulmine", + "choir": "Koorilaulmine", + "yodeling": "Joodeldamine", + "chant": "Skandeerimine", + "mantra": "Mantra lugemine", + "child_singing": "Lastelaul", + "whistling": "Vilistamine", + "gasp": "Hingeldamine", + "pant": "Ähkimine", + "door": "Uks", + "mouse": "Hiir", + "keyboard": "Klahvistik", + "sink": "Kraanikauss", + "blender": "Kannmikser", + "clock": "Kell", + "scissors": "Käärid", + "hair_dryer": "Föön", + "toothbrush": "Hambahari", + "vehicle": "Sõiduk", + "bark": "Puukoor", + "goat": "Kits", + "snort": "Nuuskamine", + "cough": "Köhimine", + "throat_clearing": "Kurgu puhtaksköhatamine", + "sneeze": "Aevastamine", + "sniff": "Nuuskimine", + "run": "Jooksmine", + "cheering": "Hõiskamine", + "synthetic_singing": "Sünteesitud laulmine", + "rapping": "Räppimine", + "humming": "Ümisemine", + "groan": "Oigamine", + "grunt": "Röhatamine", + "chatter": "Jutuvada", + "shuffle": "Jalgade lohistamine", + "footsteps": "Sammumise heli", + "chewing": "Närimine", + "biting": "Hammustamine", + "gargling": "Kuristamine", + "stomach_rumble": "Kõhukorin", + "burping": "Röhitsemine", + "hiccup": "Luksumine", + "fart": "Peeretamine", + "yip": "Haukumine heleda häälega", + "howl": "Ulgumine", + "bow_wow": "Haukumise imiteerimine", + "growling": "Urisemine", + "whimper_dog": "Koera nuuksumine", + "purr": "Nurrumine", + "meow": "Näugumine", + "hiss": "Sisisemine", + "caterwaul": "Kräunumine", + "livestock": "Kariloomad", + "bleat": "Määgimine", + "dogs": "Koerad", + "rats": "Rotid", + "patter": "Pladin", + "insect": "Putukas", + "cricket": "Ritsikas", + "mosquito": "Sääsk", + "fly": "Kärbes", + "clip_clop": "Kabjaklobin", + "neigh": "Hirnumine", + "cattle": "Loomakari", + "moo": "Ammumine", + "cowbell": "Lehmakell", + "pig": "Siga", + "oink": "Röhkimine", + "fowl": "Kodulinnud", + "chicken": "Kana", + "cluck": "Kanade loksumine", + "cock_a_doodle_doo": "Kukeleegu", + "turkey": "Kalkun", + "gobble": "Kalkuni kulistamine", + "duck": "Part", + "quack": "Prääksumine", + "goose": "Hani", + "honk": "Kaagatamine", + "wild_animals": "Metsloomad", + "roaring_cats": "Möirgavad kassid", + "roar": "Möirgamine", + "chirp": "Sirisemine", + "squawk": "Prääksatamine", + "pigeon": "Tuvi", + "coo": "Kudrutamine", + "crow": "Vares", + "caw": "Kraaksumine", + "owl": "Öökull", + "hoot": "Huikamine", + "flapping_wings": "Tiibade laperdamine", + "buzz": "Sumisemine", + "frog": "Konn", + "croak": "Krooksumine", + "snake": "Madu", + "rattle": "Kõristamine/lõgistamine", + "whale_vocalization": "Vaalaskala häälitsused", + "music": "Muusika", + "musical_instrument": "Pill", + "plucked_string_instrument": "Keelpill", + "guitar": "Kitarr", + "electric_guitar": "Elektrikitarr", + "bass_guitar": "Basskitarr", + "acoustic_guitar": "Akustiline kitarr", + "sitar": "Sitar", + "mandolin": "Mandoliin", + "banjo": "Bändžo", + "zither": "Kannel/tsitter", + "ukulele": "Ukulele", + "piano": "Klaver", + "electric_piano": "Elektriklaver", + "organ": "Orel", + "electronic_organ": "Elektriorel", + "hammond_organ": "Hammond-orel", + "synthesizer": "Süntesaator", + "sampler": "Sämpler", + "harpsichord": "Klavessiin", + "percussion": "Löökriistad", + "drum_kit": "Trummikomplekt", + "bass_drum": "Basstrumm", + "tambourine": "Tamburiin", + "glockenspiel": "Ksülofon", + "vibraphone": "Vibrafon (metalltorudega ksülofon)", + "marimba": "Marimbafon", + "tubular_bells": "Torukellad", + "gong": "Gong", + "orchestra": "Orkester", + "cello": "Tšello", + "pizzicato": "Pizzicato (poogenpilli sõrmega mängimine)", + "violin": "Viiul", + "string_section": "Keelpillid", + "trombone": "Tromboon", + "trumpet": "Trompet", + "french_horn": "Metsasarv", + "brass_instrument": "Puhkpillid", + "double_bass": "Kontrabass", + "wind_instrument": "Puhkpill", + "flute": "Flööt", + "saxophone": "Saksofon", + "clarinet": "Klarnet", + "harp": "Harf", + "bell": "Kellad", + "church_bell": "Kirikukell", + "jingle_bell": "Aisakell", + "bicycle_bell": "Rattakell", + "tuning_fork": "Helihark/kammertoon", + "bagpipes": "Torupillid", + "didgeridoo": "Didžeriduu", + "pop_music": "Popmuusika", + "hip_hop_music": "Hiphop muusika", + "rock_music": "Rokkmuusika", + "beatboxing": "Beatbox", + "heavy_metal": "Hevimuusika", + "punk_rock": "Punkrokk", + "grunge": "Grunge", + "progressive_rock": "Progressiivne rokk", + "rhythm_and_blues": "Rütmibluus", + "soul_music": "Soulmuusika", + "reggae": "Reggae", + "country": "Kantrimuusika", + "funk": "Funkmuusika", + "folk_music": "Rahvamuusika", + "middle_eastern_music": "Lähis-Ida muusika", + "jazz": "Džäss", + "disco": "Disko", + "classical_music": "Klassikaline muusika", + "opera": "Ooper", + "electronic_music": "Elektrooniline muusika", + "house_music": "House-muusika", + "techno": "Tekno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronic_dance_music": "Elektrooniline tantsumuusika", + "music_of_latin_america": "Ladina-Ameerika muusika", + "salsa_music": "Salsa", + "flamenco": "Flamenko", + "blues": "Bluus", + "music_for_children": "Lastemuusika", + "new-age_music": "New Age muusika", + "vocal_music": "Laulmine", + "a_capella": "A Capella", + "music_of_africa": "Aafrika muusika", + "afrobeat": "Afrobeat", + "christian_music": "Kristlik muusika", + "gospel_music": "Gospelmuusika", + "music_of_asia": "Aasia muusika", + "music_of_bollywood": "Bollywoodi muusika", + "ska": "Ska", + "carnatic_music": "Karnataka muusika", + "trance_music": "Trance muusika", + "ambient_music": "Ambient muusika", + "electronica": "Electronica", + "swing_music": "Svingmuusika", + "bluegrass": "Bluegrass", + "psychedelic_rock": "Psühhedeelne rokk", + "rock_and_roll": "Rock'n'roll", + "scratching": "Kriipimine/kraapimine", + "theremin": "Teremin", + "accordion": "Akordion", + "harmonica": "Suupill", + "wind_chime": "Tuulekell", + "chime": "Kelluke", + "traditional_music": "Traditsiooniline muusika", + "independent_music": "Sõltumatu muusika", + "song": "Laul", + "background_music": "Taustamuusika", + "lullaby": "Hällilaul", + "christmas_music": "Jõulumuusika", + "video_game_music": "Videomängude muusika", + "dance_music": "Tantsumuusika", + "wedding_music": "Pulmamuusika", + "happy_music": "Rõõmus muusika", + "sad_music": "Kurb muusika", + "tender_music": "Tundeline muusika", + "angry_music": "Vihane muusika", + "exciting_music": "Põnev muusika", + "scary_music": "Hirmutav muusika", + "wind": "Tuul", + "thunderstorm": "Äikesetorm", + "thunder": "Kõu/äike", + "water": "Vesi", + "rain": "Vihm", + "raindrop": "Vihmapiisk", + "spray": "Pritsimine", + "pump": "Pumpamine", + "stir": "Segamine/nihelemine", + "boiling": "Keemine", + "sonar": "Kajalood", + "arrow": "Nool", + "whoosh": "Vuhh/vuhisemine", + "thump": "Potsatus/mütsatus", + "thunk": "Põmakas", + "doorbell": "Uksekell", + "traffic_noise": "Liiklusmüra", + "rail_transport": "Raudteetransport", + "train_whistle": "Rongivile", + "sailboat": "Purjekas", + "soundtrack_music": "Filmimuusika", + "jingle": "Kõlisemine/tilisemine", + "theme_music": "Tunnusmuusika" +} diff --git a/web/public/locales/et/common.json b/web/public/locales/et/common.json new file mode 100644 index 0000000..512eb6c --- /dev/null +++ b/web/public/locales/et/common.json @@ -0,0 +1,324 @@ +{ + "time": { + "untilForTime": "Kuni {{time}}", + "today": "Täna", + "untilForRestart": "Kuni Frigate käivitub uuesti.", + "untilRestart": "Kuni uuesti käivitamiseni", + "ago": "{{timeAgo}} tagasi", + "justNow": "Hetk tagasi", + "yesterday": "Eile", + "last7": "Viimase 7 päeva jooksul", + "last14": "Viimase 14 päeva jooksul", + "last30": "Viimase 30 päeva jooksul", + "thisWeek": "Sel nädalal", + "lastWeek": "Eelmisel nädalal", + "thisMonth": "Sel kuul", + "lastMonth": "Eelmisel kuul", + "5minutes": "5 minutit", + "10minutes": "10 minutit", + "30minutes": "30 minutit", + "1hour": "1 tund", + "12hours": "12 tundi", + "24hours": "24 tundi", + "pm": "pl", + "am": "el", + "yr": "{{time}} a", + "year_one": "{{time}} aasta", + "year_other": "{{time}} aastat", + "mo": "{{time}} k", + "month_one": "{{time}} kuu", + "month_other": "{{time}} kuud", + "d": "{{time}} pv", + "day_one": "{{time}} päev", + "day_other": "{{time}} päeva", + "h": "{{time}} t", + "hour_one": "{{time}} tund", + "hour_other": "{{time}} tundi", + "m": "{{time}} min", + "minute_one": "{{time}} minut", + "minute_other": "{{time}} minutit", + "s": "{{time}} sek", + "second_one": "{{time}} sekund", + "second_other": "{{time}} sekundit", + "formattedTimestampHourMinute": { + "24hour": "HH:mm", + "12hour": "hh:mm aaa" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "hh:mm:ss aaa" + }, + "formattedTimestampFilename": { + "12hour": "yy-MM-dd-hh-mm-ss-a", + "24hour": "yy-MM-dd-HH-mm-ss" + }, + "formattedTimestamp": { + "12hour": "MMM d, hh:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd.MM hh:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, hh:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "inProgress": "Töös", + "invalidStartTime": "Vigane algusaeg", + "invalidEndTime": "Vigane lõpuaeg", + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "dd. MMM yyyy, hh:mm aaa", + "24hour": "dd. MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "dd. MMM", + "never": "Mitte kunagi" + }, + "menu": { + "user": { + "setPassword": "Lisa salasõna", + "logout": "Logi välja", + "title": "Kasutaja", + "account": "Kasutajakonto", + "current": "Praegune kasutaja: {{user}}", + "anonymous": "anonüümne" + }, + "live": { + "allCameras": "Kõik kaamerad", + "title": "Otseülekanne", + "cameras": { + "title": "Kaamerad", + "count_one": "{{count}} kaamera", + "count_other": "{{count}} kaamerat" + } + }, + "settings": "Seadistused", + "language": { + "withSystem": { + "label": "Kasuta keele jaoks süsteemi seadistusi" + }, + "en": "English (inglise keel)", + "es": "Español (hispaania keel)", + "zhCN": "简体中文 (hiina keel lihtsustatud hieroglüüfidega)", + "hi": "हिन्दी (hindi keel)", + "fr": "Français (prantsuse keel)", + "ar": "العربية (araabia keel)", + "pt": "Português (portugali keel)", + "ptBR": "Português brasileiro (Brasiilia portugali keel)", + "ru": "Русский (vene keel)", + "de": "Deutsch (saksa keel)", + "ja": "日本語 (jaapani keel)", + "tr": "Türkçe (türgi keel)", + "it": "Italiano (itaalia keel)", + "nl": "Nederlands (hollandi keel)", + "sv": "Svenska (rootsi keel)", + "cs": "Čeština (tšehhi keel)", + "nb": "Norsk Bokmål (norra bokmål)", + "ko": "한국어 (korea keel)", + "vi": "Tiếng Việt (vietnami keel)", + "fa": "فارسی (pärsia keel)", + "pl": "Polski (poola keel)", + "uk": "Українська (ukraina keel)", + "he": "עברית (heebrea keel)", + "el": "Ελληνικά (kreeka keel)", + "ro": "Română (rumeenia keel)", + "hu": "Magyar (ungari keel)", + "fi": "Suomi (soome keel)", + "da": "Dansk (taani keel)", + "sk": "Slovenčina (slovaki keel)", + "yue": "粵語 (kantoni keel)", + "th": "ไทย (tai keel)", + "ca": "Català (katalaani keel)", + "sr": "Српски (serbia keel)", + "sl": "Slovenščina (sloveeni keel)", + "lt": "Lietuvių (leedu keel)", + "bg": "Български (bulgaaria keel)", + "gl": "Galego (galeegi keel)", + "id": "Bahasa Indonesia (indoneesia keel)", + "ur": "اردو (urdu keel)", + "hr": "Hrvatski (horvaadi keel)", + "bs": "Bosanski (bosnia keel)", + "zhHant": "繁體中文 (hiina keel traditsiooniliste hieroglüüfidega)" + }, + "system": "Süsteem", + "systemMetrics": "Süsteemi meetrika", + "configuration": "Seadistused", + "systemLogs": "Süsteemi logid", + "configurationEditor": "Seadistuste haldur", + "languages": "Keeled", + "appearance": "Välimus", + "darkMode": { + "label": "Tume kujundus", + "light": "Hele kujundus", + "dark": "Tume kujundus", + "withSystem": { + "label": "Kasuta süsteemi seadistusi heleda või tumeda kujunduse jaoks" + } + }, + "withSystem": "Süsteem", + "theme": { + "label": "Kujundus", + "blue": "Sinine", + "green": "Roheline", + "nord": "Põhjala", + "red": "Punane", + "highcontrast": "Väga kontrastne", + "default": "Vaikimisi kujundus" + }, + "help": "Abiteave", + "documentation": { + "title": "Dokumentatsioon", + "label": "Frigate'i dokumentatsioon" + }, + "restart": "Käivita Frigate uuesti", + "review": "Ülevaatamine", + "explore": "Uuri", + "export": "Ekspordi", + "uiPlayground": "Leht kasutajaliidese katsetamiseks", + "faceLibrary": "Näoteek", + "classification": "Klassifikatsioon", + "chat": "Vestlus", + "actions": "Tegevused", + "profiles": "Profiilid", + "features": "Funktsionaalsused" + }, + "unit": { + "speed": { + "mph": "ml/t", + "kph": "km/t" + }, + "data": { + "kbps": "kB/sek", + "mbps": "MB/sek", + "gbps": "GB/sek", + "kbph": "kB/t", + "mbph": "MB/t", + "gbph": "GB/t" + }, + "length": { + "feet": "jalga", + "meters": "meetrit" + } + }, + "button": { + "apply": "Rakenda", + "reset": "Lähtesta", + "done": "Valmis", + "enabled": "Kasutusel", + "enable": "Võta kasutusele", + "disabled": "Pole kasutusel", + "disable": "Eemalda kasutuselt", + "save": "Salvesta", + "saving": "Salvestan…", + "cancel": "Katkesta", + "close": "Sulge", + "copy": "Kopeeri", + "back": "Tagasi", + "history": "Ajalugu", + "fullscreen": "Täisekraanivaade", + "exitFullscreen": "Välju täisekraanivaatest", + "pictureInPicture": "Pilt pildis vaade", + "twoWayTalk": "Kahepoolne kõneside", + "cameraAudio": "Kaamera heli", + "on": "SEES", + "off": "VÄLJAS", + "edit": "Muuda", + "copyCoordinates": "Kopeeri koordinaadid", + "delete": "Kustuta", + "yes": "Jah", + "no": "Ei", + "download": "Laadi alla", + "info": "Teave", + "suspended": "Peata", + "unsuspended": "Lõpeta peatamine", + "play": "Esita", + "unselect": "Eemalda valik", + "export": "Ekspordi", + "deleteNow": "Kustuta kohe", + "next": "Järgmine", + "continue": "Jätka", + "add": "Lisa", + "undo": "Võta tegevus tagasi", + "copiedToClipboard": "Kopeeritud lõikelauale", + "modified": "Muudetud", + "overridden": "Sürjutatud", + "resetToDefault": "Lähtesta vaikimisi väärtusteks", + "saveAll": "Salvesta kõik", + "resetToGlobal": "Lähtesta üldiseks väärtusteks", + "savingAll": "Salvestan kõiki…", + "undoAll": "Pööra kõik tegevused tagasi", + "applying": "Võtan kasutusele…", + "retry": "Proovi uuesti" + }, + "label": { + "back": "Mine tagasi", + "hide": "Peida: {{item}}", + "show": "Näita: {{item}}", + "all": "Kõik", + "ID": "Tunnus", + "none": "Puudub", + "other": "Muu" + }, + "list": { + "two": "{{0}} ja {{1}}", + "many": "{{items}} ja {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Valikuline", + "internalID": "Seadistustes ja andmebaasis kasutatav Frigate'i sisemine tunnus" + }, + "toast": { + "copyUrlToClipboard": "Võrguaadress on kopeeritud lõikelauale.", + "save": { + "title": "Salvesta", + "error": { + "title": "Seadistuste muudatuste salvestamine ei õnnestunud: {{errorMessage}}", + "noMessage": "Seadistuste muudatuste salvestamine ei õnnestunud" + }, + "success": "Seadistuste muudatuste salvestamine õnnestus." + } + }, + "role": { + "title": "Roll", + "admin": "Peakasutaja", + "viewer": "Vaataja", + "desc": "Peakasutajatel on Frigate'i kasutajaliideses kõik õigused. Vaatajad võivad vaid kaamerate pilti vaadata, objekte ülevaadata ning otsida arhiivist vanu videoid." + }, + "pagination": { + "label": "lehenummerdus", + "previous": { + "title": "Eelmine", + "label": "Mine eelmisele lehele" + }, + "next": { + "title": "Järgmine", + "label": "Mine järgmisele lehele" + }, + "more": "Järgnevad lehed" + }, + "accessDenied": { + "documentTitle": "Ligipääs on keelatud - Frigate", + "title": "Ligipääs on keelatud", + "desc": "Sul pole õigusi selle lehe vaatamiseks." + }, + "notFound": { + "documentTitle": "Lehte ei leidu - Frigate", + "title": "404", + "desc": "Veebilehte ei leidu" + }, + "selectItem": "Vali {{item}}", + "readTheDocumentation": "Loe dokumentatsiooni ja juhendit", + "information": { + "pixels": "{{area}} px" + }, + "no_items": "Objekte pole", + "validation_errors": "Valideerimise vead", + "credentialField": { + "savedPlaceholder": "Salvestatud - senise kasutamiseks jäta tühjaks" + } +} diff --git a/web/public/locales/et/components/auth.json b/web/public/locales/et/components/auth.json new file mode 100644 index 0000000..f5588cd --- /dev/null +++ b/web/public/locales/et/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Salasõna", + "errors": { + "passwordRequired": "Salasõna on vajalik", + "usernameRequired": "Kasutajanimi on vajalik", + "rateLimit": "Lubatud päringute ülempiir on käes. Proovi hiljem uuesti.", + "loginFailed": "Sisselogimine ei õnnestunud", + "unknownError": "Tundmatu viga. Lisateavet leiad logidest.", + "webUnknownError": "Tundmatu viga. Lisateavet leiad konsooli logidest." + }, + "user": "Kasutajanimi", + "login": "Logi sisse", + "firstTimeLogin": "Kas proovid esimest korda logida sisse? Kasutajanimi ja salasõna leiduvad Frigate'i logides." + } +} diff --git a/web/public/locales/et/components/camera.json b/web/public/locales/et/components/camera.json new file mode 100644 index 0000000..27cf0a2 --- /dev/null +++ b/web/public/locales/et/components/camera.json @@ -0,0 +1,90 @@ +{ + "group": { + "label": "Kaameragrupid", + "camera": { + "setting": { + "label": "Kaamerate voogedastuse seadistused", + "title": "Voogedastuse seadistused: {{cameraName}}", + "stream": "Voogedastus", + "placeholder": "Vali videovoog", + "streamMethod": { + "label": "Voogedastuse meetod", + "placeholder": "Vali voogedastuse meetod", + "method": { + "noStreaming": { + "label": "Voogedastust pole", + "desc": "Kaamerapildid uuenevad kord minutis ja voogedastust pole." + }, + "smartStreaming": { + "label": "Nutikas voogedastus (soovituslik)", + "desc": "Nutika voogedastuse puhul ilma igasuguse tuvastatava tegevuseta kaamerapildid uuenevad kord minutis ja voogedastust pole. Sellega säästad ribalaiud ja kuid ressursse. Tegevuse tuvastamisel käivitub tavapärane voogedastus." + }, + "continuousStreaming": { + "label": "Pidev voogedastus", + "desc": { + "title": "Kaamera voogedastus toimub töölauavaates pidevalt, seda ka siis, kui seal pole mingit tegevust tuvastatud.", + "warning": "Pidev voogedastus võib põhjustada suurt andmeedastuse mahutu ja tekitada jõudlusprobleeme. Kasuta seda võimalust ettevaatlikult." + } + } + } + }, + "audioIsAvailable": "Selle voogedastuse puhul on saadaval ka heliriba", + "audioIsUnavailable": "Selle voogedastuse puhul pole heliriba saadaval", + "compatibilityMode": { + "label": "Ühilduvusrežiim", + "desc": "Kasuta seda võimalust vaid olukorras, kus kaamera voogedastuses paistab visuaalseid vigu ja pidi paremas ääres on diagonaalne joon." + }, + "desc": "Muuda selle kaamergrupi voogedastuse valikuid töölauavaates.Need seadistused on seadme- ja veebibrauserikohased.", + "audio": { + "tips": { + "title": "See kaamera peab oskama heli jäädvustada ja edastada ja go2rtc kontekstis seadistatud selle voogedastuse jaoks." + } + } + }, + "birdseye": "Vaade linnulennult" + }, + "add": "Lisa kaameragrupp", + "edit": "Muuda kaameragruppi", + "delete": { + "label": "Kustuta kaameragrupp", + "confirm": { + "title": "Kinnita kustutamine", + "desc": "Kas oled kindel, et soovid kustutada kaameragrupi: {{name}}?" + } + }, + "name": { + "label": "Nimi", + "placeholder": "Sisesta nimi…", + "errorMessage": { + "mustLeastCharacters": "Kaameragrupi nimi peab olema vähemalt 2 tähemärki pikk.", + "exists": "Sellise nimega kaameragrupp on juba olemas.", + "nameMustNotPeriod": "Kaameragrupi nimes ei tohi olla tühikuid.", + "invalid": "Vigane kaameragrupi nimi." + } + }, + "cameras": { + "label": "Kaamerad", + "desc": "Vali kaamerad selle grupi jaoks." + }, + "icon": "Ikoon", + "success": "Kaameragrupp ({{name}}) on salvestatud.", + "showAll": "Näita kõiki kaameragruppe", + "showLess": "Näita vähem", + "editGroups": "Muuda kaameragruppe" + }, + "debug": { + "options": { + "label": "Seadistused", + "title": "Valikud", + "showOptions": "Näita valikuid", + "hideOptions": "Peida valikud" + }, + "boundingBox": "Piirdekast", + "timestamp": "Ajatempel", + "zones": "Tsoonid", + "mask": "Mask", + "motion": "Liikumine", + "regions": "Alad", + "paths": "Asukohad" + } +} diff --git a/web/public/locales/et/components/dialog.json b/web/public/locales/et/components/dialog.json new file mode 100644 index 0000000..e185d01 --- /dev/null +++ b/web/public/locales/et/components/dialog.json @@ -0,0 +1,127 @@ +{ + "restart": { + "title": "Kas oled kindel, et soovid Frigate'i uuesti käivitada?", + "button": "Käivita uuesti", + "restarting": { + "title": "Frigate käivitub uuesti", + "content": "See leht laaditakse uuesti {{countdown}} sekundi pärast.", + "button": "Laadi uuesti kohe" + }, + "description": "Järgnevaga Frigate uuesti käivitamise ajaks lõpetab korraks töö." + }, + "search": { + "saveSearch": { + "label": "Salvesta otsing", + "desc": "Sisesta nimi salvestatud otsingu jaoks.", + "placeholder": "Sisesta nimi oma otsingu jaoks", + "overwrite": "„{{searchName}}“ on juba olemas. Salvestamisel kirjutad olemasoleva väärtuse üle.", + "success": "„{{searchName}}“ otsing on salvestatud.", + "button": { + "save": { + "label": "Salvesta see otsing" + } + } + } + }, + "explore": { + "video": { + "viewInHistory": "Vaata ajaloos" + }, + "plus": { + "review": { + "state": { + "submitted": "Saadetud" + }, + "question": { + "ask_a": "Kas see objekt on {{label}}?", + "ask_an": "Kas see objekt on {{label}}?", + "ask_full": "Kas see objekt on {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Kinnita see silt Frigate+ teenuse jaoks" + } + }, + "submitToPlus": { + "label": "Saada teenusesse Frigate+", + "desc": "Objektid asukohtades, mida sa tahad vältida, pole valepositiivsed. Kui sa neid sellistena saadad teenusele, siis see ainult ajab tehisaru mudeli sassi." + } + } + }, + "export": { + "time": { + "fromTimeline": "Vali ajajoonelt", + "lastHour_one": "Viimase tunni jooksul", + "lastHour_other": "Viimase {{count}} tunni jooksul", + "custom": "Sinu valitud ajavahemik", + "start": { + "title": "Algusaeg", + "label": "Vali algusaeg" + }, + "end": { + "title": "Lõpuaeg", + "label": "Vali lõpuaeg" + } + }, + "name": { + "placeholder": "Sisesta ekspordifaili nimi" + }, + "select": "Vali", + "export": "Ekspordi", + "selectOrExport": "Vali või ekspordi", + "toast": { + "success": "Eksportimise käivitamine õnnestus. Faili leiad eksportimise lehelt.", + "view": "Vaata", + "error": { + "failed": "Eksportimise käivitamine ei õnnestunud: {{error}}", + "endTimeMustAfterStartTime": "Ajavahemiku lõpp peab olema peale algust", + "noVaildTimeSelected": "Ühtegi kehtivat ajavahemikku pole valitud" + } + }, + "fromTimeline": { + "saveExport": "Salvesta eksporditud sisu", + "previewExport": "Eksporditud sisu eelvaade" + }, + "case": { + "label": "Juhtum", + "placeholder": "Vali juhtum" + } + }, + "streaming": { + "label": "Voogedastus", + "restreaming": { + "disabled": "Voogedastuse kordus pole selle kaamera puhul kasutatav.", + "desc": { + "title": "Kui tahad selle kaameraga kasutada täiendavaid otseeetri ja helivõimalusi, siis seadista go2rtc." + } + }, + "debugView": "Veaotsinguvaade", + "showStats": { + "label": "Näita voogedastuse statistikat", + "desc": "Lülita see eelistus sisse, kui soovid kaamerapildi ülekattena näha voogedastuse statistikat." + } + }, + "recording": { + "button": { + "export": "Ekspordi", + "markAsReviewed": "Märgi ülevaadatuks", + "markAsUnreviewed": "Märgi mitteülevaadatuks", + "deleteNow": "Kustuta kohe" + }, + "confirmDelete": { + "title": "Kinnita kustutamine", + "desc": { + "selected": "Kas sa oled kindel et soovid selle kõik ülevaadatava objektiga seotud kirjed kustutada?

    Vajuta alla Shift klahv ja saad sellest vaatest tulevikus mööda minna." + }, + "toast": { + "success": "Selle ülevaadatava objektiga seotud videosisu on kustutatud.", + "error": "Kustutamine ei õnnestunud: {{error}}" + } + } + }, + "imagePicker": { + "selectImage": "Vali jälgitava objekti pisipilt", + "unknownLabel": "Päästikpilt on salvestatud", + "search": { + "placeholder": "Otsi sildi või alamsildi alusel..." + }, + "noImages": "Selle kaamera kohta ei leidu pisipilte" + } +} diff --git a/web/public/locales/et/components/filter.json b/web/public/locales/et/components/filter.json new file mode 100644 index 0000000..316aeeb --- /dev/null +++ b/web/public/locales/et/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filter", + "trackedObjectDelete": { + "toast": { + "error": "Jälgitavate objektide kustutamine ei õnnestunud: {{errorMessage}}", + "success": "Jälgitavate objektide kustutamine õnnestus." + }, + "title": "Kinnita kustutamine", + "desc": "Nende {{objectLength}} jälgitava objekti kustutamine eemaldab tõmmise salvestuse, kõik seotud salvestatud sissekanded ja kõik seotud objekti elutsükli kirjed. Ajaloo vaates salvestatud videomaterjali nende jälgitavate objektide kohta EI kustutata.

    Kas soovid kindlasti jätkata?

    Hoia Shift-klahvi all, et seda teateakent tulevikus vahele jätta." + }, + "cameras": { + "all": { + "title": "Kõik kaamerad", + "short": "Kaamerad" + }, + "label": "Kaamerate filter" + }, + "labels": { + "all": { + "title": "Kõik sildid", + "short": "Sildid" + }, + "label": "Sildid", + "count_one": "{{count}} silt", + "count_other": "{{count}} silti" + }, + "subLabels": { + "all": "Kõik alamsildid", + "label": "Alamsildid" + }, + "dates": { + "all": { + "title": "Kõik kuupäevad", + "short": "Kuupäevad" + }, + "selectPreset": "Vali eelseadistus…" + }, + "explore": { + "settings": { + "title": "Seadistused", + "defaultView": { + "title": "Vaikimisi vaade", + "summary": "Kokkuvõte", + "unfilteredGrid": "Filtreerimata ruudustik", + "desc": "Kui filtreid pole valitud, näita viimaste jälgitud objektide kokkuvõtet sildi kohta või näita filtreerimata ruudustikuvaadet." + }, + "gridColumns": { + "title": "Ruudustiku veerud", + "desc": "Vali ruudustikus kuvatavate veergude arv." + }, + "searchSource": { + "options": { + "thumbnailImage": "Pisipilt", + "description": "Kirjeldus" + }, + "label": "Otsinguallikas", + "desc": "Vali, kas soovid otsida sinu jälgitavate objektide pisipilte või kirjeldusi." + } + }, + "date": { + "selectDateBy": { + "label": "Vali kuupäev, mille alusel tahad filtreerida" + } + } + }, + "logSettings": { + "loading": { + "title": "Laadin", + "desc": "Kui logipaneeli vaade on keritud lõpuni, siis kuvatakse lisanduvad logikirjed automaatselt kohe." + }, + "disableLogStreaming": "Keela logi voogedastus", + "allLogs": "Kõik logid", + "label": "Logimistase filtri jaoks", + "filterBySeverity": "Kriitilisus filtri jaoks" + }, + "classes": { + "label": "Klassid", + "all": { + "title": "Kõik klassid" + }, + "count_one": "{{count}} klass", + "count_other": "{{count}} klassi" + }, + "zones": { + "label": "Tsoonid", + "all": { + "title": "Kõik tsoonid", + "short": "Tsoonid" + } + }, + "more": "Täiendavad filtrid", + "timeRange": "Ajavahemik", + "reset": { + "label": "Lähtesta filtrid vaikimisi väärtusteks" + }, + "score": "Punktiskoor", + "estimatedSpeed": "Hinnanguline kiirus: ({{unit}})", + "features": { + "label": "Omadused", + "hasSnapshot": "Leidub hetkvõte", + "hasVideoClip": "Videoklipp on olemas", + "submittedToFrigatePlus": { + "label": "Saadetud teenusesse Frigate+", + "tips": "Sa pead filtreerima jälgitavaid objekte, millel on tõmmis.

    Kui jälgitaval objektil pole tõmmist, siis teda Frigate+ teenusesse saata ei saa." + } + }, + "attributes": { + "label": "Klassifitseerimisatribuudid", + "all": "Kõik atribuudid" + }, + "sort": { + "label": "Järjestus", + "dateAsc": "Kuupäev (kasvavalt)", + "dateDesc": "Kuupäev (kahanevalt)", + "scoreAsc": "Objekti punktiskoor (kasvavalt)", + "scoreDesc": "Objekti punktiskoor (kahanevalt)", + "speedAsc": "Hinnanguline kiirus (kasvavalt)", + "speedDesc": "Hinnanguline kiirus (kahanevalt)", + "relevance": "Teemakohasus" + }, + "review": { + "showReviewed": "Näita ülevaadatuid" + }, + "motion": { + "showMotionOnly": "Näita vaid liikumisega klippe" + }, + "zoneMask": { + "filterBy": "Tsoonimask filtri jaoks" + }, + "recognizedLicensePlates": { + "title": "Tuvastatud sõiduki numbrimärgid", + "loadFailed": "Tuvastatud sõiduki numbrimärkide laadimine ei õnnestunud.", + "loading": "Laadin tuvastatud sõiduki numbrimärke…", + "placeholder": "Sõidukite numbrimärkide otsimiseks kirjuta midagi…", + "noLicensePlatesFound": "Sõidukite numbrimärke ei leidu.", + "selectPlatesFromList": "Vali loendist üks või enam sõiduki numbrimärki.", + "selectAll": "Vali kõik", + "clearAll": "Eemalda kõik" + } +} diff --git a/web/public/locales/et/components/icons.json b/web/public/locales/et/components/icons.json new file mode 100644 index 0000000..af0569f --- /dev/null +++ b/web/public/locales/et/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Vali ikoon", + "search": { + "placeholder": "Otsi ikooni…" + } + } +} diff --git a/web/public/locales/et/components/input.json b/web/public/locales/et/components/input.json new file mode 100644 index 0000000..127c8c7 --- /dev/null +++ b/web/public/locales/et/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Laadi video alla", + "toast": { + "success": "Sinu ülevaatamisel video allalaadimine algas." + } + } + } +} diff --git a/web/public/locales/et/components/player.json b/web/public/locales/et/components/player.json new file mode 100644 index 0000000..7553e3c --- /dev/null +++ b/web/public/locales/et/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "Hetkel ei leidu ühtegi salvestust", + "noPreviewFound": "Eelvaadet ei leidu", + "noPreviewFoundFor": "{{cameraName}} kaamera eelvaadet ei leidu", + "submitFrigatePlus": { + "submit": "Saada", + "title": "Kas saadad selle kaadri Frigate+ teenusesse?", + "previewError": "Hetktõmmise eelvaate laadimine ei õnnestu. Salvestus ei pruugi olla hetkel saadaval." + }, + "cameraDisabled": "Kaamera on kasutuselt eemaldatud", + "stats": { + "streamType": { + "title": "Voogedastuse tüüp:", + "short": "Tüüp" + }, + "bandwidth": { + "title": "Ribalaius:", + "short": "Ribalaius" + }, + "latency": { + "title": "Latentsus:", + "value": "{{seconds}} sekundit", + "short": { + "title": "Latentsus", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Kaadreid kokku:", + "droppedFrames": { + "title": "Vahelejäänud kaadreid:", + "short": { + "title": "Vahelejäänud", + "value": "{{droppedFrames}} kaadrit" + } + }, + "decodedFrames": "Dekodeeritud kaadreid:", + "droppedFrameRate": "Vahelejäänud kaadrite sagedus:" + }, + "livePlayerRequiredIOSVersion": "Selle voogedastuse tüübi jaoks on vajalik iOS-i versioon 17.1 või uuem.", + "streamOffline": { + "title": "Voogedastus ei toimi", + "desc": "„{{cameraName}}“ detect-tüüpi voogedastusest pole tulnud ühtegi kaadrit. Täpsemat teavet leiad vealogidest" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Kaadri saatmine Frigate+ teenusesse õnnestus" + }, + "error": { + "submitFrigatePlusFailed": "Kaadri saatmine Frigate+ teenusesse ei õnnestunud" + } + }, + "cameraOff": "Kaamera on lülitatud välja" +} diff --git a/web/public/locales/et/config/cameras.json b/web/public/locales/et/config/cameras.json new file mode 100644 index 0000000..157dadd --- /dev/null +++ b/web/public/locales/et/config/cameras.json @@ -0,0 +1,39 @@ +{ + "name": { + "label": "Kaamera nimi", + "description": "Kaamera nimi on nõutav" + }, + "friendly_name": { + "label": "Sõbralik nimi", + "description": "Frigate UI-s kasutatud kaamerasõbralik nimi" + }, + "enabled": { + "label": "Kasutusel", + "description": "Kasutusel" + }, + "audio": { + "label": "Helisündmused" + }, + "birdseye": { + "mode": { + "label": "Jälgimisrežiim" + } + }, + "label": "Kaameraseadistus", + "semantic_search": { + "triggers": { + "threshold": { + "description": "Minimaalne sarnasuse punktiskoor (0-1), mis on vajalik selle päästiku käivitamiseks." + } + } + }, + "lpr": { + "label": "Sõidukite numbrimärkide tuvastus", + "description": "Sõidukite numbrimärkide tuvastuse seadistus sisaldab tuvastuse lävendeid, vormindust ja teadaolevaid numbrimärke." + }, + "review": { + "genai": { + "description": "Kontrollib generatiivse tehisaru kasutamist kirjelduste ja kokkuvõtete koostamiseks ülevaatamisele kuuluvate objektide jaoks." + } + } +} diff --git a/web/public/locales/et/config/global.json b/web/public/locales/et/config/global.json new file mode 100644 index 0000000..adb9777 --- /dev/null +++ b/web/public/locales/et/config/global.json @@ -0,0 +1,56 @@ +{ + "audio": { + "label": "Helisündmused" + }, + "birdseye": { + "mode": { + "label": "Jälgimisrežiim" + } + }, + "version": { + "label": "Praegune seadistuse versioon", + "description": "Aktiivse seadistuse numbriline või tekstiline versioon, mis aitab tuvastada vormingumuudatusi." + }, + "classification": { + "bird": { + "threshold": { + "label": "Minimaalne punktiskoor", + "description": "Objekti määratlemiseks linnina vajalik mlassifitseerimise minimaalne punktiskoor." + } + }, + "custom": { + "threshold": { + "label": "Punktiskoori lävend", + "description": "Punktiskoori lävend, mida kasutatakse klassifitseerimise oleku muutmiseks." + } + } + }, + "semantic_search": { + "triggers": { + "threshold": { + "description": "Minimaalne sarnasuse punktiskoor (0-1), mis on vajalik selle päästiku käivitamiseks." + } + } + }, + "face_recognition": { + "unknown_score": { + "label": "Tundmatu punktiskoori lävend" + } + }, + "lpr": { + "label": "Sõidukite numbrimärkide tuvastus", + "description": "Sõidukite numbrimärkide tuvastuse seadistus sisaldab tuvastuse lävendeid, vormindust ja teadaolevaid numbrimärke.", + "enabled": { + "description": "Lülita sõidukite numbrimärkide tuvastus kõikide kaamerate jaoks sisse; seda saad kaamerakohaselt ka sürjutada." + } + }, + "genai": { + "label": "Generatiivse tehisaru seadistus", + "description": "Seadistsued generatiivse tehisaru teenusepakkujate kasutamisel kirjelduste ja kokkuvõtete loomiseks ülevaatamisele kuuluvate objektide jaoks." + }, + "review": { + "genai": { + "description": "Kontrollib generatiivse tehisaru kasutamist kirjelduste ja kokkuvõtete koostamiseks ülevaatamisele kuuluvate objektide jaoks." + } + } +} diff --git a/web/public/locales/et/config/groups.json b/web/public/locales/et/config/groups.json new file mode 100644 index 0000000..e8c7956 --- /dev/null +++ b/web/public/locales/et/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Üldine tuvastamine", + "sensitivity": "Üldine tundlikkus" + }, + "cameras": { + "detection": "Tuvastamine", + "sensitivity": "Tundlikkus" + } + }, + "motion": { + "global": { + "sensitivity": "Üldine tundlikkus", + "algorithm": "Üldine algoritm" + }, + "cameras": { + "sensitivity": "Tundlikkus", + "algorithm": "Algoritm" + } + }, + "snapshots": { + "global": { + "display": "Üldine vaade" + }, + "cameras": { + "display": "Vaade" + } + }, + "timestamp_style": { + "global": { + "appearance": "Üldine välimus" + }, + "cameras": { + "appearance": "Välimus" + } + }, + "detect": { + "global": { + "resolution": "Üldine eraldusvõime", + "tracking": "Üldine jälgimine" + }, + "cameras": { + "resolution": "Eraldusvõime", + "tracking": "Jälgimine" + } + }, + "objects": { + "global": { + "filtering": "Üldine filtreerimine", + "tracking": "Üldine jälgimine" + }, + "cameras": { + "filtering": "Filtreerimine", + "tracking": "Jälgimine" + } + }, + "record": { + "global": { + "retention": "Üldine säilitamine", + "events": "Üldised sündmused" + }, + "cameras": { + "retention": "Säilitamine", + "events": "Sündmused" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Kaamerakohased FFmpegi argumendid" + } + } +} diff --git a/web/public/locales/et/config/validation.json b/web/public/locales/et/config/validation.json new file mode 100644 index 0000000..c9eb465 --- /dev/null +++ b/web/public/locales/et/config/validation.json @@ -0,0 +1,35 @@ +{ + "minimum": "Peab olema vähemalt {{limit}}", + "maximum": "Võib olla kuni {{limit}}", + "exclusiveMinimum": "Peab olema suurem, kui {{limit}}", + "exclusiveMaximum": "Peab olema väiksem, kui {{limit}}", + "minLength": "Peab olema vähemalt {{limit}} tähemärk(i) pikk", + "maxLength": "Võib olla kuni {{limit}} tähemärk(i) pikk", + "minItems": "Peab sisaldama vähemalt {{limit}} objekti", + "maxItems": "Võib sisaldada kuni {{limit}} objekti", + "pattern": "Vigane vorming", + "required": "See väli on kohustuslik", + "type": "Vigane väärtuse tüüp", + "enum": "Peab olema üks lubatud väärtustest", + "const": "Väärtus ei vasta eeldatud konstandile", + "uniqueItems": "Kõik väärtused peavad olema unikaalsed", + "format": "Vigane vorming", + "additionalProperties": "Tundmatu omadus pole lubatud", + "oneOf": "Peab vastama täpselt ühele lubatud skeemile", + "anyOf": "Peab vastama vähemalt ühele lubatud skeemile", + "proxy": { + "header_map": { + "roleHeaderRequired": "Kui rollide vastendused on seadistatud, siis rollide päis on nõutav." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "Iga rolli saad määrata ühele sisendvoole.", + "detectRequired": "„Tuvasta“ rollile pead määrama vähemalt ühe sisendvoo.", + "hwaccelDetectOnly": "Vaid „Tuvasta“ rolliga sisendvoog võib määratleda raudvaralise kiirenduse argumente." + } + }, + "detect": { + "dimensionMustBeEven": "Peab olema paarisarv." + } +} diff --git a/web/public/locales/et/objects.json b/web/public/locales/et/objects.json new file mode 100644 index 0000000..2e7e6c5 --- /dev/null +++ b/web/public/locales/et/objects.json @@ -0,0 +1,130 @@ +{ + "person": "Inimene", + "bicycle": "Jalgratas", + "car": "Auto", + "motorcycle": "Mootorratas", + "airplane": "Lennuk", + "bus": "Buss", + "train": "Rong", + "boat": "Väike laev", + "traffic_light": "Valgusfoor", + "fire_hydrant": "Tuletõrjehüdrant", + "street_sign": "Liiklusmärk", + "stop_sign": "Stoppmärk", + "parking_meter": "Parkimispiletite automaat", + "bench": "Istepink", + "bird": "Lind", + "cat": "Kass", + "dog": "Koer", + "horse": "Hobune", + "sheep": "Lammas", + "cow": "Lehm", + "elephant": "Elevant", + "bear": "Karu", + "zebra": "Sebra", + "giraffe": "Kaelkirjak", + "hat": "Müts", + "backpack": "Seljakott", + "umbrella": "Vihmavari", + "shoe": "King", + "eye_glasses": "Prillid", + "handbag": "Käekott", + "tie": "Lips", + "suitcase": "Kohver", + "frisbee": "Lendav taldrik", + "skis": "Suusad", + "snowboard": "Lumelaud", + "sports_ball": "Pall", + "kite": "Tuulelohe", + "baseball_bat": "Pesapallikurikas", + "baseball_glove": "Pesapallikinnas", + "skateboard": "Rula", + "surfboard": "Surfilaud", + "tennis_racket": "Tennisereket", + "animal": "Loom", + "bottle": "Pudel", + "plate": "Taldrik", + "wine_glass": "Veiniklaas", + "cup": "Kruus", + "fork": "Kahvel", + "knife": "Nuga", + "spoon": "Lusikas", + "bowl": "Kauss", + "banana": "Banaan", + "apple": "Õun", + "sandwich": "Võileib", + "orange": "Apelsin", + "broccoli": "Spargelkapsas", + "carrot": "Porgand", + "hot_dog": "Viinerisai", + "pizza": "Pitsa", + "donut": "Sõõrik", + "cake": "Kook", + "chair": "Tool", + "couch": "Kušett", + "potted_plant": "Potilill", + "bed": "Voodi", + "mirror": "Peegel", + "dining_table": "Söögilaud", + "window": "Aken", + "desk": "Kirjutuslaud", + "toilet": "Tualett", + "door": "Uks", + "tv": "Teler", + "laptop": "Sülearvuti", + "mouse": "Hiir", + "remote": "Kaugjuhtimispult", + "keyboard": "Klahvistik", + "cell_phone": "Mobiiltelefon", + "microwave": "Mikrolaineahi", + "oven": "Ahi", + "toaster": "Röster", + "sink": "Kraanikauss", + "refrigerator": "Külmkapp", + "blender": "Kannmikser", + "book": "Raamat", + "clock": "Kell", + "vase": "Vaas", + "scissors": "Käärid", + "teddy_bear": "Mängukaru", + "hair_dryer": "Föön", + "toothbrush": "Hambahari", + "hair_brush": "Juuksehari", + "vehicle": "Sõiduk", + "squirrel": "Orav", + "deer": "Hirv", + "bark": "Puukoor", + "fox": "Rebane", + "goat": "Kits", + "rabbit": "Jänes", + "raccoon": "Pesukaru", + "robot_lawnmower": "Robotmuruniiduk", + "waste_bin": "Prügikast", + "on_demand": "Nõudmisel", + "face": "Nägu", + "license_plate": "Sõiduki numbrimärk", + "package": "Pakett", + "bbq_grill": "Väligrill", + "amazon": "Amazoni sõiduk", + "usps": "USPS-i sõiduk", + "ups": "UPS-i sõiduk", + "fedex": "FedExi sõiduk", + "dhl": "DHL-i sõiduk", + "an_post": "An Posti sõiduk", + "purolator": "Purolatori sõiduk", + "postnl": "PostNL-i sõiduk", + "nzpost": "NZPost-i sõiduk", + "postnord": "PostNordi sõiduk", + "gls": "GLS-i sõiduk", + "dpd": "DPD sõiduk", + "canada_post": "Canada Post", + "royal_mail": "Royal Mail", + "school_bus": "Koolibuss", + "skunk": "Vinukloom (skunk)", + "kangaroo": "Känguru", + "baby": "Väikelaps", + "baby_stroller": "Lapsevanker", + "rickshaw": "Rikša", + "Rodent": "Näriline", + "rodent": "Näriline" +} diff --git a/web/public/locales/et/views/chat.json b/web/public/locales/et/views/chat.json new file mode 100644 index 0000000..ca3f7f6 --- /dev/null +++ b/web/public/locales/et/views/chat.json @@ -0,0 +1,10 @@ +{ + "documentTitle": "Frigate - vestlus tehisaruga", + "title": "Vestlus tehisaruga Frigate'is", + "subtitle": "Tehisaru abil töötav abiline kaamerate haldamiseks ja analüüside koostamiseks", + "placeholder": "Küsi mida iganes…", + "error": "Midagi läks valesti. Palun proovi uuesti.", + "processing": "Töötlen…", + "toolsUsed": "Kasutatud: {{tools}}", + "similarity_score": "Sarnasus" +} diff --git a/web/public/locales/et/views/classificationModel.json b/web/public/locales/et/views/classificationModel.json new file mode 100644 index 0000000..36a4ef6 --- /dev/null +++ b/web/public/locales/et/views/classificationModel.json @@ -0,0 +1,47 @@ +{ + "toast": { + "success": { + "deletedModel_one": "{{count}} mudeli kustutamine õnnestus", + "deletedModel_other": "{{count}} mudeli kustutamine õnnestus" + } + }, + "documentTitle": "Klassifitseerimise mudelid - Frigate", + "details": { + "scoreInfo": "Punktiskoor näitab selle objekti kõigi tuvastuste keskmist klassifitseerimise usaldusväärsust.", + "none": "Puudub", + "unknown": "Pole teada" + }, + "button": { + "deleteClassificationAttempts": "Kustuta klassifitseerimispildid", + "renameCategory": "Muuda klassi nimi", + "deleteCategory": "Kustuta klass", + "deleteImages": "Kustuta pildid", + "addClassification": "Lisa klassifikatsioon", + "deleteModels": "Kustuta mudelid", + "editModel": "Muuda mudelit" + }, + "description": { + "invalidName": "Vigane nimi. Nimed võivad sisaldada ainult tähti, numbreid, tühikuid, ülakomasid, alakriipse ja sidekriipse." + }, + "deleteModel": { + "desc_one": "Kas oled kindel, et soovid kustutada {{count}} mudeli? Järgnevaga kustuvad jäädavalt kõik seotud andmed, sealhulgas pildid ja koolitusandmed. Seda tegevust ei saa tagasi pöörata.", + "desc_other": "Kas oled kindel, et soovid kustutada {{count}} mudelit? Järgnevaga kustuvad jäädavalt kõik seotud andmed, sealhulgas pildid ja koolitusandmed. Seda tegevust ei saa tagasi pöörata." + }, + "deleteDatasetImages": { + "desc_one": "Kas oled kindel, et soovid kustutada {{count}} pildi {{dataset}} andmekogust? Seda tegevust ei saa tagasi pöörata ja hiljem on vaja mudelit uuesti koolitada.", + "desc_other": "Kas oled kindel, et soovid kustutada {{count}} pilti {{dataset}} andmekogust? Seda tegevust ei saa tagasi pöörata ja hiljem on vaja mudelit uuesti koolitada." + }, + "deleteTrainImages": { + "desc_one": "Kas oled kindel, et soovid kustutada {{count}} pildi? Seda tegevust ei saa tagasi pöörata.", + "desc_other": "Kas oled kindel, et soovid kustutada {{count}} pilti? Seda tegevust ei saa tagasi pöörata." + }, + "wizard": { + "step3": { + "allImagesRequired_one": "Palun klassifitseeri kõik pildid. Jäänud on veel {{count}} pilt.", + "allImagesRequired_other": "Palun klassifitseeri kõik pildid. Jäänud on veel {{count}} pilti." + } + }, + "tooltip": { + "trainingInProgress": "Mudel on parasjagu õppimas" + } +} diff --git a/web/public/locales/et/views/configEditor.json b/web/public/locales/et/views/configEditor.json new file mode 100644 index 0000000..56371cd --- /dev/null +++ b/web/public/locales/et/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "toast": { + "error": { + "savingError": "Viga seadistuse salvestamisel" + }, + "success": { + "copyToClipboard": "Seadistused on kopeeritud lõikelauale." + } + }, + "documentTitle": "Seadistuste haldus - Frigate", + "safeConfigEditor": "Seadistuste haldus (ohutusrežiim)", + "configEditor": "Seadistuste haldus", + "safeModeDescription": "Seadistuste vea tõttu on Frigate hetkel ohutusrežiimis.", + "copyConfig": "Kopeeri seadistused", + "saveAndRestart": "Salvesta ja käivita uuesti", + "saveOnly": "Vaid salvesta", + "confirm": "Kas väljud ilma salvestamata?" +} diff --git a/web/public/locales/et/views/events.json b/web/public/locales/et/views/events.json new file mode 100644 index 0000000..b8f0e3f --- /dev/null +++ b/web/public/locales/et/views/events.json @@ -0,0 +1,92 @@ +{ + "alerts": "Häired", + "allCameras": "Kõik kaamerad", + "detail": { + "settings": "Üksikasjaliku vaate seadistused", + "label": "Üksikasjad", + "noDataFound": "Ülevaatamiseks pole üksikasjalikke andmeid", + "aria": "Lülita üksikasjalik vaade sisse/välja", + "trackedObject_one": "{{count}} objekt", + "trackedObject_other": "{{count}} objekti", + "noObjectDetailData": "Objekti üksikasjalikke andmeid pole saadaval.", + "alwaysExpandActive": { + "title": "Alati laienda aktiivse kirje andmeid", + "desc": "Kui vähegi saadaval, siis alati laienda aktiivse ülevaatamisel kirje andmeid." + } + }, + "detections": "Tuvastamise tulemused", + "motion": { + "label": "Liikumine", + "only": "Vaid liikumine" + }, + "empty": { + "alert": "Ülevaatamiseks ei leidu ühtegi häiret", + "detection": "Ülevaatamiseks ei leidu ühtegi tuvastamist", + "motion": "Liikumise andmeid ei leidu", + "recordingsDisabled": { + "title": "Salvestamine peab olema sisse lülitatud", + "description": "Objekte saad määrata ülevaadatamiseks vaid siis, kui selle kaamera puhul on salvestamine lülitatud sisse." + } + }, + "select_all": "Kõik", + "camera": "Kaamera", + "detected": "tuvastatud", + "normalActivity": "Tavaline", + "needsReview": "Vajab ülevaatamist", + "securityConcern": "Võib olla turvaprobleem", + "timeline": { + "label": "Ajajoon" + }, + "timeline.aria": "Vali ajajoon", + "zoomIn": "Suumi sisse", + "zoomOut": "Suumi välja", + "events": { + "label": "Sündmused", + "aria": "Vali sündmused", + "noFoundForTimePeriod": "Selle ajavahemiku kohta ei leidu sündmusi." + }, + "selected_one": "{{count}} valitud", + "selected_other": "{{count}} valitud", + "markAsReviewed": "Märgi ülevaadatuks", + "markTheseItemsAsReviewed": "Märgi need kirjed ülevaadatuks", + "newReviewItems": { + "label": "Vaata uusi ülevaatamiseks mõeldud kirjeid", + "button": "Uued ülevaatamiseks mõeldud kirjed" + }, + "documentTitle": "Ülevaatamine - Frigate", + "recordings": { + "documentTitle": "Salvestised - Frigate", + "invalidSharedLink": "Töötlemisvea tõttu ei õnnestu avada ajatempliga salvestuse linki.", + "invalidSharedCamera": "Tundmatu või volituseta kaamera tõttu ei õnnestu avada ajatempliga salvestuse linki." + }, + "calendarFilter": { + "last24Hours": "Viimased 24 tundi" + }, + "objectTrack": { + "clickToSeek": "Klõpsa siia ajapunkti kerimiseks", + "trackedPoint": "Jälgitav punkt" + }, + "motionSearch": { + "menuItem": "Liikumise otsing", + "openMenu": "Kaamera valikud" + }, + "motionPreviews": { + "menuItem": "Vaata liikumiste eelvaateid", + "title": "Liikumiste eelvaated: {{camera}}", + "mobileSettingsTitle": "Liikumiste eelvaadete seadistused", + "mobileSettingsDesc": "Kohenda taasesituse kiirust ja heledust ning vali kuupäev, et vaadata läbi ainult liikumist kajastavaid klipid.", + "dim": "Hämarus", + "dimAria": "Muuda hämarust", + "dimDesc": "Kohenda hämarust parandamaks liikumisala nähtavust.", + "speed": "Kiirus", + "speedAria": "Vali eelvaate taasesituse kiirus", + "speedDesc": "Määratle kiirus, millega eelvaate klippe näidatakse.", + "back": "Tagasi", + "empty": "Ühtegi eelvaadet pole saadaval", + "noPreview": "Eelvaade pole saadaval", + "seekAria": "Keri „{{camera}}“ kaamera vaade ajatempli juurde: {{time}}", + "filter": "Filtreeri", + "filterDesc": "Näitamaks ainult liikumisega klippe antud aladel, vali soovitud piirkonnad.", + "filterClear": "Tühjenda" + } +} diff --git a/web/public/locales/et/views/explore.json b/web/public/locales/et/views/explore.json new file mode 100644 index 0000000..dee52f6 --- /dev/null +++ b/web/public/locales/et/views/explore.json @@ -0,0 +1,114 @@ +{ + "trackedObjectsCount_one": "{{count}} jälgitav objekt ", + "trackedObjectsCount_other": "{{count}} jälgitavat objekti ", + "fetchingTrackedObjectsFailed": "Viga jälgitavate objektide laadimisel: {{errorMessage}}", + "noTrackedObjects": "Ühtegi jälgitavat objekti ei leidunud", + "itemMenu": { + "findSimilar": { + "aria": "Otsi sarnaseid jälgitavaid objekte" + }, + "downloadSnapshot": { + "label": "Laadi hetkvõte alla", + "aria": "Laadi hetkvõte alla" + }, + "downloadCleanSnapshot": { + "label": "Laadi puhas hetkvõte alla", + "aria": "Laadi puhas hetkvõte alla" + } + }, + "trackingDetails": { + "annotationSettings": { + "showAllZones": { + "title": "Näita kõiki tsoone", + "desc": "Kui objekt on sisenenud tsooni, siis alati näida tsooni märgistust." + } + }, + "lifecycleItemDesc": { + "attribute": { + "other": "{{label}} on tuvastatud kui {{attribute}}" + }, + "stationary": "{{label}} jäi paigale", + "active": "{{label}} muutus aktiivseks", + "entered_zone": "{{label}} sisenes tsooni {{zones}}", + "visible": "{{label}} on tuvastatud", + "header": { + "zones": "Tsoonid", + "ratio": "Suhtarv", + "area": "Ala", + "score": "Punktiskoor", + "computedScore": "Arvutatud punktiskoor", + "topScore": "Suuremad punktiskoorid" + }, + "external": "{{label}} on tuvastatud", + "heard": "{{label}} on kuuldud", + "gone": "{{label}} on jäänud" + }, + "title": "Jälgimise üksikasjad", + "noImageFound": "Selle ajatempli kohta ei leidu pilti.", + "createObjectMask": "Loo objektimask", + "carousel": { + "previous": "Eelmine slaid", + "next": "Järgmine slaid" + }, + "count": "{{first}} / {{second}}" + }, + "documentTitle": "Avasta - Frigate", + "generativeAI": "Generatiivne tehisaru", + "exploreMore": "Avasta rohkem {{label}}-tüüpi objekte", + "exploreIsUnavailable": { + "embeddingsReindexing": { + "step": { + "thumbnailsEmbedded": "Pisipildid on lõimitud: ", + "descriptionsEmbedded": "Kirjeldused on lõimitud: ", + "trackedObjectsProcessed": "Jälgitud objektid on töödeldud: " + }, + "startingUp": "Käivitun…", + "estimatedTime": "Hinnanguliselt jäänud aega:", + "finishingShortly": "Lõpetan õige pea" + } + }, + "type": { + "details": "üksikasjad", + "thumbnail": "pisipilt", + "snapshot": "hetkvõte", + "video": "video", + "tracking_details": "jälgimise üksikasjad" + }, + "details": { + "item": { + "tips": { + "mismatch_one": "Tuvastasin {{count}} võõra objekti ja need on lisatud ülevaatamiseks. Need objektid kas ei ole piisavad häire või tuvastamise jaoks, aga ka võivad juba olla eemaldatud või kustutatud.", + "mismatch_other": "Tuvastasin {{count}} võõrast objekti ja need on lisatud ülevaatamiseks. Need objektid kas ei ole piisavad häire või tuvastamise jaoks, aga ka võivad juba olla eemaldatud või kustutatud." + }, + "title": "Vaata objekti üksikasju", + "desc": "Vaata objekti üksikasju", + "toast": { + "success": { + "updatedLPR": "Sõiduki numbrimärgi uuendamine õnnestus." + }, + "error": { + "updatedLPRFailed": "Sõiduki numbrimärgi uuendamine ei õnnestunud: {{errorMessage}}" + } + } + }, + "snapshotScore": { + "label": "Hetkvõtete punktiskoor" + }, + "regenerateFromSnapshot": "Loo uuesti hetkvõttest", + "timestamp": "Ajatampel", + "score": { + "label": "Punktiskoor" + }, + "scoreInfo": "Punktiskoori teave", + "editLPR": { + "title": "Muuda sõiduki numbrimärki", + "desc": "Sisesta sõiduki numbrimärgi uus väärtus: {{label}}", + "descNoLabel": "Sisesta sõiduki numbrimärgi uus väärtus selle jälgitava objekti jaoks" + }, + "recognizedLicensePlate": "Tuvastatud sõiduki numbrimärk", + "description": { + "aiTips": "Frigate ei küsi sinu generatiivse tehisaru teenusepakkujalt kirjeldust enne, kui jälgitava objekti elutsükkel on lõppenud." + } + }, + "trackedObjectDetails": "Jälgitava objekti üksikasjad" +} diff --git a/web/public/locales/et/views/exports.json b/web/public/locales/et/views/exports.json new file mode 100644 index 0000000..ed9a379 --- /dev/null +++ b/web/public/locales/et/views/exports.json @@ -0,0 +1,39 @@ +{ + "documentTitle": "Eksport Frigate'ist", + "search": "Otsi", + "noExports": "Eksporditud sisu ei leidu", + "deleteExport": { + "label": "Kustuta eksporditud sisu" + }, + "deleteExport.desc": "Kas sa oled kindel et soovid „{{exportName}}“ kustutada?", + "editExport": { + "title": "Muuda eksporditud sisu nime", + "desc": "Sisesta eksporditud sisu jaoks uus nimi.", + "saveExport": "Salvesta eksporditud sisu" + }, + "tooltip": { + "shareExport": "Jaga eksporditud sisu", + "downloadVideo": "Laadi video alla", + "editName": "Muuda nime", + "deleteExport": "Kustuta eksporditud sisu", + "assignToCase": "Lisa juhtumile" + }, + "toast": { + "error": { + "renameExportFailed": "Eksporditud sisu nime muutmine ei õnnestunud: {{errorMessage}}", + "assignCaseFailed": "Juhtumiga seose uuendamine ei õnnestunud: {{errorMessage}}" + } + }, + "headings": { + "cases": "Juhtumid", + "uncategorizedExports": "Kategooriata eksportimised" + }, + "caseDialog": { + "title": "Lisa juhtumile", + "selectLabel": "Juhtum", + "newCaseOption": "Lisa uus juhtum", + "nameLabel": "Juhtumi nimi", + "descriptionLabel": "Kirjeldus", + "description": "Vali olemasolev juhtum või lisa uus." + } +} diff --git a/web/public/locales/et/views/faceLibrary.json b/web/public/locales/et/views/faceLibrary.json new file mode 100644 index 0000000..11a3fb1 --- /dev/null +++ b/web/public/locales/et/views/faceLibrary.json @@ -0,0 +1,46 @@ +{ + "button": { + "uploadImage": "Laadi pilt üles" + }, + "collections": "Kogumikud", + "description": { + "placeholder": "Sisesta nimi selle kogumiku jaoks", + "invalidName": "Vigane nimi. Nimed võivad sisaldada ainult tähti, numbreid, tühikuid, ülakomasid, alakriipse ja sidekriipse.", + "addFace": "Laadides üles oma esimese pildi saad lisada uue kogumiku Näoteeki.", + "nameCannotContainHash": "Nimi ei saa sisaldada # märki." + }, + "documentTitle": "Näoteek - Frigate", + "createFaceLibrary": { + "new": "Lisa uus nägu" + }, + "deleteFaceLibrary": { + "title": "Kustuta nimi" + }, + "toast": { + "error": { + "addFaceLibraryFailed": "Näo sidumine nimega ei õnnestunud: {{errorMessage}}", + "updateFaceScoreFailed": "Näo punktiskoori uuendamine ei õnnestunud: {{errorMessage}}" + }, + "success": { + "addFaceLibrary": "Lisamine Näoteeki õnnestus: {{name}}!", + "deletedFace_one": "{{count}} näo kustutamine õnnestus.", + "deletedFace_other": "{{count}} näo kustutamine õnnestus.", + "deletedName_one": "{{count}} näo kustutamine õnnestus.", + "deletedName_other": "{{count}} näo kustutamine õnnestus.", + "updatedFaceScore": "Näo punktiskoori uuendamine õnnestus: {{name}} ({{score}})." + } + }, + "deleteFaceAttempts": { + "desc_one": "Kas oled kindel, et soovid kustutada {{count}} näo? Seda tegevust ei saa tagasi pöörata.", + "desc_other": "Kas oled kindel, et soovid kustutada {{count}} nägu? Seda tegevust ei saa tagasi pöörata." + }, + "details": { + "timestamp": "Ajatampel", + "unknown": "Pole teada", + "scoreInfo": "Punktiskoor on kõigi nägude hinnete kaalutud keskmine, kus kaalukoefitsiendiks on iga pildi näo suurus." + }, + "uploadFaceImage": { + "title": "Laadi näopilt üles", + "desc": "Laadi üles pilt, et otsida sellelt nägusid ja lisada see {{pageToggle}}'i jaoks" + } +} diff --git a/web/public/locales/et/views/live.json b/web/public/locales/et/views/live.json new file mode 100644 index 0000000..e7f0507 --- /dev/null +++ b/web/public/locales/et/views/live.json @@ -0,0 +1,203 @@ +{ + "muteCameras": { + "enable": "Summuta kõik kaamerad", + "disable": "Lõpeta kõikide kaamerate summutamine" + }, + "streamingSettings": "Voogedastuse seadistused", + "cameraSettings": { + "title": "Seadistused: {{camera}}", + "cameraEnabled": "Kaamera on kasutusel", + "objectDetection": "Objektide tuvastamine", + "audioDetection": "Heli tuvastus", + "transcription": "Heli üleskirjutus", + "snapshots": "Hetkvõtted", + "autotracking": "Automaatne jälgimine", + "recording": "Salvestus", + "camera": "Kaamera" + }, + "documentTitle": { + "default": "Frigate reaalajas" + }, + "documentTitle.withCamera": "{{camera}} - Otseülekanne - Frigate", + "lowBandwidthMode": "Väikese ribalaiusega režiim", + "twoWayTalk": { + "enable": "Lülita kahepoolne kõneside sisse", + "disable": "Lülita kahepoolne kõneside välja" + }, + "cameraAudio": { + "enable": "Lülita kaamera heli sisse", + "disable": "Lülita kaamera heli välja" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kaamerapildi joondamiseks keskele klõpsa kaadris", + "enable": "Kasuta klõpsamisega teisaldamist", + "disable": "Ära kasuta klõpsamisega teisaldamist", + "enableWithZoom": "Luba liigutamine klõpsuga / suumimine lohistamisega" + }, + "left": { + "label": "Pööra liigutatavat kaamerat vasakule" + }, + "up": { + "label": "Pööra liigutatavat kaamerat üles" + }, + "down": { + "label": "Pööra liigutatavat kaamerat alla" + }, + "right": { + "label": "Pööra liigutatavat kaamerat paremale" + } + }, + "zoom": { + "in": { + "label": "Suumi liigutatavat kaamerat sisse" + }, + "out": { + "label": "Suumi liigutatavat kaamerat välja" + } + }, + "focus": { + "in": { + "label": "Fookusta liigutatavat kaamerat sisse" + }, + "out": { + "label": "Fookusta liigutatavat kaamerat välja" + } + }, + "presets": "Liigutatava kaamera eelseadistused", + "frame": { + "center": { + "label": "Klõpsa kaadrit liigutatava kaamera pildi sättimiseks keskele" + } + } + }, + "camera": { + "enable": "Lülita kaamera sisse", + "disable": "Lülita kaamera välja", + "turnOn": "Lülita kaamera sisse", + "turnOff": "Lülita kaamera välja" + }, + "detect": { + "enable": "Lülita tuvastamine sisse", + "disable": "Lülita tuvastamine välja" + }, + "recording": { + "enable": "Lülita salvestamine sisse", + "disable": "Lülita salvestamine välja", + "disabledInConfig": "Pead olema selle kaamera jaoks Seadistustest määranud salvestamise." + }, + "snapshots": { + "enable": "Lülita hetkvõtted sisse", + "disable": "Lülita hetkvõtted välja" + }, + "streamStats": { + "enable": "Näita voogedastuse statistikat", + "disable": "Peida voogedastuse statistika" + }, + "stream": { + "twoWayTalk": { + "available": "Kahepoolne kõneside on selle voogedastuse puhul saadaval", + "unavailable": "Kahepoolne kõneside pole selle voogedastuse puhul saadaval", + "tips": "Sinu seadme peab seda funktsionaalsust toetama ja WebRTC peab olema kahepoolse kõneside jaoks seadistatud." + }, + "playInBackground": { + "label": "Esita taustal", + "tips": "Selle eelistusega saad määrata, et voogedastus jääb tööle ka siis, kui meesiaesitaja on suletud." + }, + "audio": { + "available": "Selles voogedastuses on heliriba saadaval", + "unavailable": "Selles voogedastuses pole heliriba saadaval", + "tips": { + "title": "Heli peab tulema sinu kaamerast ja selle voogedastuse jaoks peab see go2rtc-s olema seadistatud." + } + }, + "title": "Voogedastus", + "lowBandwidth": { + "resetStream": "Lähtesta voogedastus", + "tips": "Reaalaja pilt on puhverdamise või voogedastuse vigade tõttu madala ribalaiusega režiimis." + }, + "debug": { + "picker": "Voogedastuse osa valik pole silumisrežiimis saadaval. Silumisvaade kasutab alati voogedastust, millele on määratud tuvastamisroll." + } + }, + "notifications": "Teavitused", + "audio": "Heli", + "snapshot": { + "takeSnapshot": "Laadi hetkvõte alla", + "noVideoSource": "Hetkvõtte tegemiseks pole saadaval ühtegi videoallikat.", + "captureFailed": "Hetkvõtte jäädvustamine ei õnnestunud.", + "downloadStarted": "Hetkvõtte allalaadimine algas." + }, + "audioDetect": { + "enable": "Lülita helituvastus sisse", + "disable": "Lülita helituvastus välja" + }, + "transcription": { + "enable": "Lülita reaalajas heli üleskirjutus sisse", + "disable": "Lülita reaalajas heli üleskirjutus välja" + }, + "autotracking": { + "enable": "Lülita automaatne jälgimine sisse", + "disable": "Lülita automaatne jälgimine välja" + }, + "manualRecording": { + "title": "Nõudmisel", + "playInBackground": { + "label": "Esita taustal", + "desc": "Kasuta seda valikut, kui tahad voogedastuse jätkumist ka siis, kui pildivaade on peidetud." + }, + "debugView": "Veaotsinguvaade", + "showStats": { + "label": "Näita statistikat", + "desc": "Selle eelistuse puhul näidatakse voogedastuse statistikat kaamerapildi peal." + }, + "tips": "Laadi alla hetktõmmis või käivita käsitsi sündmus vastavalt selle kaamera salvestiste säilitamise seadistustele.", + "start": "Alusta tellimuspõhist salvestamist", + "started": "Alustasin käsitsi tellitavat salvestamist.", + "failedToStart": "Käsitsi tellitava salvestamise alustamine ei õnnestunud.", + "recordDisabledTips": "Kuna selle kaamera seadistustes on salvestamine keelatud või piiratud, siis salvestatakse ainult pilt.", + "end": "Lõpeta tellimuspõhine salvestamine", + "ended": "Lõpetasin käsitsi tellitava salvestamise.", + "failedToEnd": "Käsitsi tellitava salvestamise lõpetamine ei õnnestunud." + }, + "noCameras": { + "buttonText": "Lisa kaamera", + "restricted": { + "title": "Ühtegi kaamerat pole saadaval", + "description": "Sul pole õigust ühegi selle grupi kaamera vaatamiseks." + }, + "title": "Ühtegi kaamerat pole seadistatud", + "description": "Alustamiseks ühenda mõni kaamera Frigate'iga.", + "default": { + "title": "Ühtegi kaamerat pole seadistatud", + "description": "Alustamiseks ühenda mõni kaamera Frigate'iga.", + "buttonText": "Lisa kaamera" + }, + "group": { + "title": "Grupid pole ühtegi kaamerat", + "description": "Selles kaameragrupis pole ühtegi määratud ega kasutusel kaamerat.", + "buttonText": "Halda gruppe" + } + }, + "effectiveRetainMode": { + "modes": { + "active_objects": "Aktiivsed objektid", + "all": "Kõik", + "motion": "Liikumine" + } + }, + "editLayout": { + "label": "Muuda paigutust", + "group": { + "label": "Muuda kaameragruppi" + }, + "exitEdit": "Välju muutmisest" + }, + "history": { + "label": "Näita varasemat sisu" + }, + "suspend": { + "forTime": "Peatamise aeg: " + } +} diff --git a/web/public/locales/et/views/motionSearch.json b/web/public/locales/et/views/motionSearch.json new file mode 100644 index 0000000..72d1bad --- /dev/null +++ b/web/public/locales/et/views/motionSearch.json @@ -0,0 +1,4 @@ +{ + "documentTitle": "Liikumise tuvastus - Frigate", + "title": "Liikumise otsing" +} diff --git a/web/public/locales/et/views/recording.json b/web/public/locales/et/views/recording.json new file mode 100644 index 0000000..57ed975 --- /dev/null +++ b/web/public/locales/et/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Ekspordi", + "calendar": "Kalender", + "filter": "Filter", + "filters": "Filtrid", + "toast": { + "error": { + "noValidTimeSelected": "Ühtegi kehtivat ajavahemikku pole valitud", + "endTimeMustAfterStartTime": "Ajavahemiku lõpp peab olema peale algust" + } + } +} diff --git a/web/public/locales/et/views/replay.json b/web/public/locales/et/views/replay.json new file mode 100644 index 0000000..4ef29cd --- /dev/null +++ b/web/public/locales/et/views/replay.json @@ -0,0 +1,18 @@ +{ + "dialog": { + "camera": "Lähtekaamera", + "timeRange": "Ajavahemik", + "preset": { + "1m": "Viimase ühe minuti jooksul", + "5m": "Viimase viie minuti jooksul", + "timeline": "Ajajoonelt", + "custom": "Kohandatud" + }, + "startButton": "Käivita kordus", + "selectFromTimeline": "Vali", + "starting": "Käivitan kordust…", + "startLabel": "Algus", + "endLabel": "Lõpp" + }, + "title": "Kordus veaotsinguks" +} diff --git a/web/public/locales/et/views/search.json b/web/public/locales/et/views/search.json new file mode 100644 index 0000000..6388029 --- /dev/null +++ b/web/public/locales/et/views/search.json @@ -0,0 +1,73 @@ +{ + "placeholder": { + "search": "Otsi…" + }, + "search": "Otsi", + "savedSearches": "Salvestatud otsingud", + "searchFor": "Otsi: {{inputValue}}", + "button": { + "clear": "Tühjenda otsing", + "save": "Salvesta otsing", + "delete": "Kustuta salvestatud otsing", + "filterInformation": "Filtri teave", + "filterActive": "Filtreid valituna" + }, + "filter": { + "label": { + "has_snapshot": "Leidub hetkvõte", + "cameras": "Kaamerad", + "labels": "Sildid", + "zones": "Tsoonid", + "sub_labels": "Alamsildid", + "attributes": "Omadused", + "search_type": "Otsingutüüp", + "time_range": "Ajavahemik", + "before": "Enne", + "after": "Pärast", + "min_score": "Minimaalne punktiskoor", + "max_score": "Maksimaalne punktiskoor", + "min_speed": "Miinimumkiirus", + "max_speed": "Maksimumkiirus", + "recognized_license_plate": "Tuvastatud sõiduki numbrimärk", + "has_clip": "Klipp on olemas" + }, + "searchType": { + "thumbnail": "Pisipilt", + "description": "Kirjeldus" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "„Enne“ kuupäev peab olema varasem, kui „Pärast“ kuupäev.", + "afterDatebeEarlierBefore": "„Pärast“ kuupäev peab olema hilisem, kui „Enne“ kuupäev.", + "minScoreMustBeLessOrEqualMaxScore": "Minimaalne punktiskoor peab olema väiksem või võrdne kui maksimaalne punktiskoor.", + "maxScoreMustBeGreaterOrEqualMinScore": "Maksimaalne punktiskoor peab olema suurem või võrdne kui minimaalne punktiskoor.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Minimaalne kiirus peab olema väiksem või võrdne kui maksimaalne kiirus.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Maksimaalne kiirus peab olema suurem või võrdne kui minimaalne kiirus." + } + }, + "tips": { + "title": "Kuidas saad kasutada tekstifiltreid", + "desc": { + "text": "Filtrid aitavad sul otsingutulemusi kitsendada. Siin on juhised nende kasutamiseks sisestusväljal:", + "step1": "Sisesta filtri nimi, millele järgnev koolon (nt, „cameras:“).", + "step2": "Vali soovitatud väärtus või sisesta enda oma.", + "step3": "Kasuta mitmeid filtreid lisades neid üksteise järgi ning eraldades tühikuga.", + "step4": "Kuupäevafiltrid (before: ja after:) kasutavad {{DateFormat}} vormingut.", + "step5": "Ajavahemiku filter kasutab {{exampleTime}} vormingut.", + "step6": "Filtreid saad eemaldada klõpsates nende kõrval leiduvad märget „x“.", + "exampleLabel": "Näide:" + } + }, + "header": { + "currentFilterType": "Filtri väärtused", + "noFilters": "Filtrid", + "activeFilters": "Aktiivsed filtrid" + } + }, + "trackedObjectId": "Jälgitava objekti tunnus", + "similaritySearch": { + "title": "Sarnaste objektide otsing", + "active": "Sarnaste objektide otsing on aktiivne", + "clear": "Eemalda sarnaste objektide otsing" + } +} diff --git a/web/public/locales/et/views/settings.json b/web/public/locales/et/views/settings.json new file mode 100644 index 0000000..4c2e46c --- /dev/null +++ b/web/public/locales/et/views/settings.json @@ -0,0 +1,501 @@ +{ + "cameraWizard": { + "step1": { + "password": "Salasõna", + "passwordPlaceholder": "Valikuline", + "customUrlPlaceholder": "rtsp://kasutajanimi:salasõna@host:port/asukoht", + "connectionSettings": "Ühenduse seadistused", + "port": "Port", + "username": "Kasutajanimi", + "usernamePlaceholder": "Valikuline" + }, + "step3": { + "streamUrlPlaceholder": "rtsp://kasutajanimi:salasõna@host:port/asukoht", + "url": "Võrguaadress", + "resolution": "Resolutsioon", + "quality": "Kvaliteet", + "roles": "Rollid", + "roleLabels": { + "record": "Salvestamine", + "audio": "Heliriba" + }, + "connected": "Ühendatud", + "featuresTitle": "Funktsionaalsused" + }, + "steps": { + "probeOrSnapshot": "Võta proov või tee hetkvõte" + }, + "step2": { + "testing": { + "fetchingSnapshot": "Laadin kaamera hetkvõtet alla..." + }, + "retry": "Proovi uuesti", + "manufacturer": "Tootja", + "model": "Mudel", + "firmware": "Püsivara", + "profiles": "Profiilid", + "presets": "Eelseadistused", + "useCandidate": "Kasuta", + "uriCopy": "Kopeeri", + "connected": "Ühendatud" + }, + "testResultLabels": { + "resolution": "Resolutsioon", + "video": "Video", + "audio": "Heliriba", + "fps": "Kaadrisagedus" + }, + "step4": { + "reload": "Laadi uuesti", + "connecting": "Ühendan…", + "valid": "Kehtiv", + "failed": "Ebaõnnestunud", + "connectStream": "Ühenda", + "connectingStream": "Ühendan", + "disconnectStream": "Katkesta ühendus", + "roles": "Rollid", + "none": "Määramata", + "error": "Viga" + } + }, + "users": { + "updatePassword": "Lähtesta salasõna", + "toast": { + "success": { + "updatePassword": "Salasõna muutmine õnnestus." + }, + "error": { + "setPasswordFailed": "Salasõna salvestamine ei õnnestunud: {{errorMessage}}" + } + }, + "table": { + "password": "Lähtesta salasõna", + "username": "Kasutajanimi", + "actions": "Tegevused", + "role": "Roll" + }, + "dialog": { + "form": { + "password": { + "title": "Salasõna", + "placeholder": "Sisesta salasõna", + "confirm": { + "title": "Korda salasõna", + "placeholder": "Korda salasõna" + }, + "strength": { + "title": "Salasõna tugevus: ", + "weak": "Nõrk", + "medium": "Keskmime", + "strong": "Tugev", + "veryStrong": "Väga tugev" + }, + "match": "Salasõnad klapivad omavahel", + "notMatch": "Salasõnad ei klapi omavahel", + "show": "Näita salasõna", + "hide": "Peida salasõna", + "requirements": { + "title": "Salasõna reeglid:", + "length": "Vähemalt 12 tähemärki", + "uppercase": "Vähemalt üks suurtäht", + "digit": "Vähemalt üks number", + "special": "Vähemalt üks erimärk (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Uus salasõna", + "placeholder": "Sisesta uus salasõna", + "confirm": { + "placeholder": "Sisesta uus salasõna uuesti" + } + }, + "passwordIsRequired": "Salasõna on vajalik", + "currentPassword": { + "title": "Senine salasõna", + "placeholder": "Sisesta oma senine salasõna" + }, + "user": { + "title": "Kasutajanimi", + "desc": "Lubatud on vaid tähed, numbrid, punktid ja alakriipsud." + } + }, + "createUser": { + "confirmPassword": "Palun kinnita oma uus salasõna", + "usernameOnlyInclude": "Kasutajanimes võivad olla vaid tähed, numbrid, punkt (.) või alakriips (_)" + }, + "passwordSetting": { + "cannotBeEmpty": "Salasõna ei või jääda tühjaks", + "doNotMatch": "Salasõnad ei klapi omavahel", + "updatePassword": "Muuda kasutaja {{username}} salasõna", + "setPassword": "Sisesta salasõna", + "desc": "Selle kasutajakonto turvalisuse tagamiseks lisa tugev salasõna.", + "currentPasswordRequired": "Senine salasõna on vajalik", + "incorrectCurrentPassword": "Senine salasõna pole õige", + "passwordVerificationFailed": "Salasõna kontrollimine ei õnnestunud" + }, + "changeRole": { + "roleInfo": { + "admin": "Peakasutaja", + "viewer": "Vaataja" + } + } + }, + "title": "Kasutajad" + }, + "debug": { + "boundingBoxes": { + "desc": "Näita jälgitavate objektide ümber märgiskaste" + }, + "title": "Silumine ja veaotsing", + "debugging": "Veaotsing ja silumine", + "audio": { + "title": "Heliriba", + "score": "punktiskoor" + }, + "timestamp": { + "title": "Ajatempel" + }, + "zones": { + "title": "Tsoonid" + }, + "regions": { + "title": "Alad" + }, + "paths": { + "title": "Asukohad" + }, + "objectShapeFilterDrawing": { + "score": "Punktiskoor", + "ratio": "Suhtarv", + "area": "Ala" + } + }, + "documentTitle": { + "default": "Seadistused - Frigate", + "authentication": "Autentimise seadistused - Frigate", + "cameraReview": "Kaamerate kordusvaatuste seadistused - Frigate", + "general": "Profiili seadistused - Frigate", + "frigatePlus": "Frigate+ seadistused - Frigate", + "notifications": "Teavituste seadistused - Frigate", + "cameraManagement": "Kaamerate haldus - Frigate", + "masksAndZones": "Maskide ja tsoonide haldus - Frigate", + "object": "Silumine ja veaotsing - Frigate" + }, + "general": { + "title": "Profiili seadistused", + "cameraGroupStreaming": { + "clearAll": "Kustuta kõik voogedastuse seadistused" + }, + "liveDashboard": { + "title": "Töölaud reaalajas", + "automaticLiveView": { + "label": "Automaatne otseülekande vaade" + } + }, + "calendar": { + "title": "Kalender", + "firstWeekday": { + "sunday": "Pühapäev", + "monday": "Esmaspäev", + "label": "Esimene nädalapäev" + } + }, + "storedLayouts": { + "title": "Salvestatud paigutused" + }, + "recordingsViewer": { + "title": "Salvestuste vaataja" + } + }, + "cameraManagement": { + "backToSettings": "Tagasi kaameraseadistuste juurde", + "cameraConfig": { + "enabled": "Kasutusel", + "ffmpeg": { + "pathPlaceholder": "rtsp://...", + "roles": "Rollid" + } + }, + "clone": { + "categories": { + "items": { + "lpr": "Sõidukite numbrimärkide tuvastus", + "genai": "Generatiivne tehisaru" + } + } + } + }, + "notification": { + "notificationSettings": { + "title": "Teavituste seadistused" + }, + "globalSettings": { + "title": "Üldseadistused" + }, + "deviceSpecific": "Seadmekohased seadistused", + "toast": { + "success": { + "settingSaved": "Teavituste seadistused on salvestatud." + } + }, + "title": "Teavitused", + "email": { + "title": "E-post" + }, + "cameras": { + "title": "Kaamerad" + }, + "suspendTime": { + "suspend": "Peata arvuti töö" + } + }, + "frigatePlus": { + "title": "Frigate+ seadistused", + "unsavedChanges": "Frigate+ seadistuste muudatused on salvestamata", + "toast": { + "success": "Frigate+ seadistuste muudatused on salvestatud. Muudatuste kasutuselevõtmiseks käivita Frigate uuesti." + }, + "snapshotConfig": { + "title": "Hetkvõtte seadistused", + "table": { + "snapshots": "Hetkvõtted", + "cleanCopySnapshots": "clean_copy Hetkvõtted", + "camera": "Kaamera" + } + }, + "modelInfo": { + "plusModelType": { + "userModel": "Peenhäälestatud" + }, + "cameras": "Kaamerad" + } + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkti", + "label": "Tsoonid", + "desc": { + "documentation": "Dokumentatsioon" + }, + "name": { + "title": "Nimi" + }, + "inertia": { + "title": "Inerts" + }, + "objects": { + "title": "Objektid" + } + }, + "motionMasks": { + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkti", + "desc": { + "documentation": "Dokumentatsioon" + } + }, + "objectMasks": { + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkti", + "desc": { + "documentation": "Dokumentatsioon" + }, + "objects": { + "title": "Objektid" + } + } + }, + "roles": { + "toast": { + "success": { + "userRolesUpdated_one": "{{count}} selle rolliga kasutaja on nüüd määratud Vaatajaks, kellel on ligipääs kõikidele kaameratele.", + "userRolesUpdated_other": "{{count}} selle rolliga kasutajat on nüüd määratud Vaatajaks, kellel on ligipääs kõikidele kaameratele." + } + }, + "table": { + "role": "Roll", + "cameras": "Kaamerad", + "actions": "Tegevused" + }, + "dialog": { + "deleteRole": { + "deleting": "Kustutan..." + }, + "form": { + "cameras": { + "title": "Kaamerad" + }, + "role": { + "desc": "Lubatud on vaid tähed, numbrid, punktid ja alakriipsud.", + "roleOnlyInclude": "Rolli nimes võivad olla vaid tähed, numbrid, punkt (.) või alakriips (_)" + } + } + } + }, + "menu": { + "ui": "Kasutajaliides", + "cameraManagement": "Haldus", + "masksAndZones": "Maskid ja tsoonid", + "triggers": "Päästikud", + "debug": "Silumine ja veaotsing", + "users": "Kasutajad", + "roles": "Rollid", + "notifications": "Teavitused", + "frigateplus": "Frigate+", + "cameraReview": "Ülevaatamine", + "profiles": "Profiilid", + "integrationLpr": "Sõidukite numbrimärkide tuvastus", + "cameraLpr": "Sõidukite numbrimärkide tuvastus", + "uiSettings": "Kasutajaliidese seadistused", + "globalDetect": "Objektide tuvastamine", + "globalRecording": "Salvestamine", + "globalSnapshots": "Hetkvõtted", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Liikumise tuvastus", + "globalObjects": "Objektid", + "globalReview": "Ülevaatamine", + "globalAudioEvents": "Heli tuvastus", + "globalLivePlayback": "Reaalajas sisu taasesitus", + "globalTimestampStyle": "Ajatempli stiil", + "systemDatabase": "Andmebaas", + "systemTls": "TLS", + "systemAuthentication": "Autentimine", + "systemNetworking": "Võrgundus", + "systemProxy": "Proksiserver", + "systemUi": "Kasutajaliides", + "systemLogging": "Logimine", + "systemEnvironmentVariables": "Keskkonnamuutujad", + "systemTelemetry": "Telemeetria", + "systemBirdseye": "Vaade linnulennult", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "Tuvastamine ja mudelid", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "go2rtc voogedastus", + "integrationSemanticSearch": "Semantiline otsing", + "integrationGenerativeAi": "Generatiivne tehisaru" + }, + "dialog": { + "unsavedChanges": { + "title": "Sul on salvestamata muudatusi.", + "desc": "Kas soovid muudatused enne jätkamist salvestada?" + } + }, + "cameraSetting": { + "camera": "Kaamera", + "noCamera": "Kaamerat pole" + }, + "enrichments": { + "semanticSearch": { + "reindexNow": { + "confirmButton": "Indekseeri uuesti", + "label": "Indekseeri uuesti kohe" + }, + "modelSize": { + "small": { + "title": "väike" + }, + "large": { + "title": "suur" + } + }, + "title": "Semantiline otsing" + }, + "faceRecognition": { + "modelSize": { + "small": { + "title": "väike" + }, + "large": { + "title": "suur" + } + } + }, + "birdClassification": { + "title": "Lindude klassifikatsioon" + }, + "licensePlateRecognition": { + "title": "Sõidukite numbrimärkide tuvastus" + } + }, + "cameraReview": { + "review": { + "title": "Ülevaatamine", + "alerts": "Hoiatused ", + "detections": "Tuvastamise tulemused " + }, + "object_descriptions": { + "title": "Generatiivse tehisaru objektikirjeldused", + "desc": "Luba/keela ajutiselt selle kaamera jaoks generatiivse tehisaru objektikirjeldused kuni Frigate'i taaskäivitamiseni. Kui see on keelatud, ei küsita selle kaamera jälgitavate objektide kohta tehisintellekti poolt loodud kirjeldusi." + }, + "review_descriptions": { + "title": "Generatiivne tehisaru ülevaatamisele kuuluva sisu kirjedlused" + } + }, + "motionDetectionTuner": { + "Threshold": { + "title": "Lävi" + } + }, + "triggers": { + "documentTitle": "Päästikud", + "management": { + "title": "Päästikud" + }, + "table": { + "name": "Nimi", + "type": "Tüüp", + "content": "Sisu", + "threshold": "Lävi", + "actions": "Tegevused", + "edit": "Muuda" + }, + "type": { + "thumbnail": "Pisipilt", + "description": "Kirjeldus" + }, + "dialog": { + "form": { + "name": { + "title": "Nimi", + "error": { + "invalidCharacters": "Välja nimes võivad olla vaid tähed, numbrid, alakriipsud (_) või sidekriipsud (-)." + } + }, + "type": { + "title": "Tüüp" + }, + "content": { + "title": "Sisu" + }, + "threshold": { + "title": "Lävi" + }, + "actions": { + "title": "Tegevused" + } + } + } + }, + "profiles": { + "nameInvalid": "Lubatud on vaid väiketähed, numbrid ja alakriipsud" + }, + "go2rtcStreams": { + "validation": { + "nameInvalid": "Voogedastuse nimes on lubatud vaid tähed, numbrid alakriipsud ja sidekriipsud" + } + }, + "configForm": { + "sections": { + "lpr": "Sõidukite numbrimärkide tuvastus" + } + }, + "configMessages": { + "detect": { + "disabled": "Objektide tuvastamine on lülitatud välja. Hetkepildid, läbivaatamisele kuuluvad objektid ja täiendavad funktsioonid, nagu näotuvastus, sõidukite numbrimärkide tuvastus ja generatiivne tehisintellekt, ei tööta." + }, + "lpr": { + "vehicleNotTracked": "Sõidukite numbrimärkide tuvastus eeldab, et auto või mootorratas on jälgitav. Lülita menüüst Objektid sell kaamera jaoks sisse valikud „auto“ või „mootorratas“." + } + } +} diff --git a/web/public/locales/et/views/system.json b/web/public/locales/et/views/system.json new file mode 100644 index 0000000..8108994 --- /dev/null +++ b/web/public/locales/et/views/system.json @@ -0,0 +1,40 @@ +{ + "documentTitle": { + "general": "Üldine statistika - Frigate", + "cameras": "Kaamerate statistika - Frigate", + "storage": "Andmeruumi statistika - Frigate" + }, + "logs": { + "download": { + "label": "Laadi logid alla" + }, + "copy": { + "label": "Kopeeri lõikelauale", + "success": "Logid on kopeeritud lõikelauale", + "error": "Logide kopeerimine lõikelauale ei õnnestunud" + }, + "websocket": { + "filter": { + "cameras_count_one": "{{count}} kaamera", + "cameras_count_other": "{{count}} kaamerat" + }, + "empty": "Ühtegi sõnumit pole veel hõivatud", + "count_one": "{{count}} sõnum", + "count_other": "{{count}} sõnumit" + }, + "type": { + "label": "Tüüp", + "timestamp": "Ajatempel", + "tag": "Silt", + "message": "Sõnum" + }, + "tips": "Logid on serverist voogedastamisel", + "toast": { + "error": { + "fetchingLogsFailed": "Viga logide laadimisel: {{errorMessage}}", + "whileStreamingLogs": "Viga logide voogedastamisel: {{errorMessage}}" + } + } + }, + "title": "Süsteem" +} diff --git a/web/public/locales/fa/audio.json b/web/public/locales/fa/audio.json new file mode 100644 index 0000000..b3e5470 --- /dev/null +++ b/web/public/locales/fa/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "گفتار", + "babbling": "پر حرفی", + "yell": "فریاد", + "bellow": "صدای نعره", + "whoop": "ضجه", + "whispering": "غیبت کردن", + "laughter": "خنده", + "snicker": "پوزخند", + "crying": "گریه کردن", + "sigh": "حسرت", + "singing": "خواندن آواز", + "choir": "آواز گروهی", + "yodeling": "عیاشی", + "chant": "مناجات", + "mantra": "مانترا", + "cat": "گربه", + "dog": "سگ", + "horse": "اسب", + "bird": "پرنده", + "boat": "قایق", + "car": "ماشین", + "bus": "اتوبوس", + "motorcycle": "موتور سیکلت", + "train": "قطار", + "bicycle": "دوچرخه", + "child_singing": "آواز خواندن کودک", + "snort": "خرناس", + "cough": "سرفه", + "throat_clearing": "صاف کردن گلو", + "sneeze": "عطسه", + "sniff": "بو کشیدن", + "run": "دویدن", + "synthetic_singing": "آواز مصنوعی", + "rapping": "رپ‌خوانی", + "humming": "هوم‌خوانی", + "sheep": "گوسفند", + "groan": "ناله", + "grunt": "غرغر", + "whistling": "سوت زدن", + "breathing": "تنفس", + "wheeze": "خِس‌خِس", + "snoring": "خروپف", + "gasp": "به نفس‌نفس افتادن", + "pant": "نفس‌نفس‌زدن", + "shuffle": "پخش تصادفی", + "footsteps": "صدای قدم‌ها", + "chewing": "جویدن", + "biting": "گاز گرفتن", + "camera": "دوربین", + "gargling": "غرغره کردنغرغره کردن", + "stomach_rumble": "قاروقور شکم", + "burping": "آروغ زدن", + "skateboard": "اسکیت‌بورد", + "yip": "ییپ", + "howl": "زوزه", + "growling": "درحال غرغر", + "meow": "میو", + "caterwaul": "جیغ‌وداد", + "livestock": "دام", + "clip_clop": "تق‌تق", + "cattle": "گوساله", + "cowbell": "زنگولهٔ گاو", + "mouse": "موش", + "oink": "خِرخِر", + "keyboard": "صفحه‌کلید", + "goat": "بز", + "sink": "سینک", + "cluck": "قُدقُد", + "turkey": "بوقلمون", + "quack": "قاقا", + "scissors": "قیچی", + "honk": "بوق", + "hair_dryer": "سشوار", + "roar": "غرش", + "vehicle": "وسیلهٔ نقلیه", + "chirp": "جیک‌جیک", + "squawk": "جیغ زدن", + "coo": "قوقو", + "crow": "کلاغ", + "owl": "جغد", + "dogs": "سگ‌ها", + "patter": "شرشر", + "mosquito": "پشه", + "buzz": "وزوز", + "frog": "قورباغه", + "snake": "مار", + "rattle": "جغجغه کردن", + "music": "موسیقی", + "musical_instrument": "ساز موسیقی", + "guitar": "گیتار", + "electric_guitar": "گیتار برقی", + "acoustic_guitar": "گیتار آکوستیک", + "steel_guitar": "گیتار استیل", + "banjo": "بانجو", + "sitar": "سیتار", + "hiccup": "سکسکه", + "fart": "باد معده", + "finger_snapping": "بشکن زدن", + "clapping": "دست زدن", + "heartbeat": "ضربان قلب", + "heart_murmur": "سوفل قلبی", + "applause": "تشویق", + "chatter": "وراجی", + "crowd": "جمعیت", + "children_playing": "بازی کردن کودکان", + "animal": "حیوان", + "pets": "حیوانات خانگی", + "bark": "پارس", + "bow_wow": "هاپ‌هاپ", + "whimper_dog": "نالیدن سگ", + "purr": "خرخر", + "hiss": "هیس", + "neigh": "شیهه", + "door": "در", + "moo": "ماغ", + "pig": "خوک", + "bleat": "بع‌بع", + "fowl": "ماکیان", + "cock_a_doodle_doo": "قدقدی‌قدقد", + "blender": "مخلوط‌کن", + "chicken": "مرغ", + "gobble": "قورت دادن", + "clock": "ساعت", + "duck": "اردک", + "goose": "غاز", + "wild_animals": "حیوانات وحشی", + "toothbrush": "مسواک", + "roaring_cats": "غرش گربه‌ها", + "pigeon": "کبوتر", + "hoot": "هوهو", + "flapping_wings": "بال‌بال زدن", + "rats": "موش‌ها", + "insect": "حشره", + "cricket": "جیرجیرک", + "fly": "مگس", + "croak": "قارقار", + "whale_vocalization": "آواز نهنگ", + "plucked_string_instrument": "ساز زهی زخمه‌ای", + "bass_guitar": "گیتار باس", + "tapping": "ضربه‌زدن", + "strum": "زخمه‌زدن", + "mandolin": "ماندولین", + "zither": "زیتر", + "ukulele": "یوکللی", + "piano": "پیانو", + "electric_piano": "پیانوی الکتریکی", + "organ": "ارگ", + "electronic_organ": "ارگ الکترونیکی", + "hammond_organ": "ارگ هموند", + "synthesizer": "سینتی‌سایزر", + "sampler": "سمپلر", + "harpsichord": "هارپسیکورد", + "percussion": "سازهای کوبه‌ای", + "drum_kit": "ست درام", + "drum_machine": "درام ماشین", + "drum": "درام", + "snare_drum": "درام اسنیر", + "rimshot": "ریم‌شات", + "drum_roll": "درام رول", + "bass_drum": "درام باس", + "timpani": "تیمپانی", + "tabla": "طبلا", + "cymbal": "سنج", + "hi_hat": "های‌هت", + "wood_block": "بلوک چوبی", + "tambourine": "تامبورین", + "maraca": "ماراکا", + "gong": "گونگ", + "tubular_bells": "ناقوس‌های لوله‌ای", + "mallet_percussion": "سازهای کوبه‌ای مالت", + "marimba": "ماریمبا", + "glockenspiel": "گلوکن‌اشپیل", + "vibraphone": "ویبرافون", + "steelpan": "استیل‌پن", + "orchestra": "ارکستر", + "brass_instrument": "ساز بادی برنجی", + "french_horn": "هورن فرانسوی", + "trumpet": "ترومپت", + "trombone": "ترومبون", + "bowed_string_instrument": "ساز زهی آرشه‌ای", + "string_section": "بخش سازهای زهی", + "violin": "ویولن", + "pizzicato": "پیتزیکاتو", + "cello": "ویولنسل", + "double_bass": "کنترباس", + "wind_instrument": "ساز بادی", + "flute": "فلوت", + "saxophone": "ساکسوفون", + "clarinet": "کلارینت", + "harp": "چنگ", + "bell": "ناقوس", + "church_bell": "ناقوس کلیسا", + "jingle_bell": "زنگوله", + "bicycle_bell": "زنگ دوچرخه", + "tuning_fork": "دیاپازون", + "chime": "زنگ", + "wind_chime": "زنگ باد", + "harmonica": "سازدهنی", + "accordion": "آکاردئون", + "bagpipes": "نی‌انبان", + "didgeridoo": "دیجریدو", + "theremin": "ترمین", + "singing_bowl": "کاسهٔ آوازخوان", + "scratching": "خراشیدن", + "pop_music": "موسیقی پاپ", + "hip_hop_music": "موسیقی هیپ‌هاپ", + "beatboxing": "بیت‌باکس", + "rock_music": "موسیقی راک", + "heavy_metal": "هوی متال", + "punk_rock": "پانک راک", + "grunge": "گرانج", + "progressive_rock": "راک پراگرسیو", + "rock_and_roll": "راک اند رول", + "psychedelic_rock": "راک روان‌گردان", + "rhythm_and_blues": "ریتم اند بلوز", + "soul_music": "موسیقی سول", + "reggae": "رگی", + "country": "کانتری", + "swing_music": "موسیقی سوئینگ", + "bluegrass": "بلوگرس", + "funk": "فانک", + "folk_music": "موسیقی فولک", + "jazz": "جاز", + "disco": "دیسکو", + "classical_music": "موسیقی کلاسیک", + "opera": "اپرا", + "electronic_music": "موسیقی الکترونیک", + "house_music": "موسیقی هاوس", + "techno": "تکنو", + "dubstep": "داب‌استپ", + "drum_and_bass": "درام اند بیس", + "electronica": "الکترونیکا", + "electronic_dance_music": "موسیقی رقص الکترونیک", + "ambient_music": "موسیقی امبینت", + "trance_music": "موسیقی ترنس", + "music_of_latin_america": "موسیقی آمریکای لاتین", + "salsa_music": "موسیقی سالسا", + "flamenco": "فلامنکو", + "blues": "بلوز", + "music_for_children": "موسیقی برای کودکان", + "new-age_music": "موسیقی نیو ایج", + "vocal_music": "موسیقی آوازی", + "a_capella": "آکاپلا", + "music_of_africa": "موسیقی آفریقا", + "afrobeat": "آفروبیت", + "christian_music": "موسیقی مسیحی", + "gospel_music": "موسیقی گاسپل", + "music_of_asia": "موسیقی آسیا", + "carnatic_music": "موسیقی کارناتیک", + "music_of_bollywood": "موسیقی بالیوود", + "ska": "اسکا", + "traditional_music": "موسیقی سنتی", + "independent_music": "موسیقی مستقل", + "song": "آهنگ", + "background_music": "موسیقی پس‌زمینه", + "theme_music": "موسیقی تم", + "soundtrack_music": "موسیقی متن", + "lullaby": "لالایی", + "video_game_music": "موسیقی بازی‌های ویدیویی", + "christmas_music": "موسیقی کریسمس", + "dance_music": "موسیقی رقص", + "wedding_music": "موسیقی عروسی", + "happy_music": "موسیقی شاد", + "sad_music": "موسیقی غمگین", + "tender_music": "موسیقی لطیف", + "angry_music": "موسیقی خشمگین", + "exciting_music": "موسیقی هیجان‌انگیز", + "scary_music": "موسیقی ترسناک", + "wind": "باد", + "rustling_leaves": "خش‌خش برگ‌ها", + "wind_noise": "صدای باد", + "thunderstorm": "طوفان تندری", + "thunder": "رعد", + "water": "آب", + "rain": "باران", + "raindrop": "قطرهٔ باران", + "rain_on_surface": "باران روی سطح", + "waterfall": "آبشار", + "ocean": "اقیانوس", + "waves": "امواج", + "steam": "بخار", + "gurgling": "قل‌قل", + "motorboat": "قایق موتوری", + "ship": "کشتی", + "motor_vehicle": "وسیلهٔ نقلیهٔ موتوری", + "toot": "توت", + "car_alarm": "دزدگیر خودرو", + "truck": "کامیون", + "air_brake": "ترمز بادی", + "air_horn": "بوق بادی", + "reversing_beeps": "بوق دنده‌عقب", + "ice_cream_truck": "کامیون بستنی‌فروشی", + "traffic_noise": "صدای ترافیک", + "rail_transport": "حمل‌ونقل ریلی", + "train_whistle": "سوت قطار", + "train_horn": "بوق قطار", + "jet_engine": "موتور جت", + "propeller": "ملخ", + "helicopter": "بالگرد", + "fixed-wing_aircraft": "هواپیمای بال‌ثابت", + "medium_engine": "موتور متوسط", + "heavy_engine": "موتور سنگین", + "engine_knocking": "تق‌تق موتور", + "engine_starting": "روشن شدن موتور", + "idling": "درجا کار کردن", + "slam": "محکم کوبیدن", + "knock": "در زدن", + "tap": "ضربهٔ آرام", + "squeak": "جیرجیر", + "cupboard_open_or_close": "باز یا بسته شدن کمد", + "microwave_oven": "مایکروفر", + "water_tap": "شیر آب", + "bathtub": "وان حمام", + "toilet_flush": "سیفون توالت", + "keys_jangling": "جرینگ‌جرینگ کلیدها", + "coin": "سکه", + "electric_shaver": "ریش‌تراش برقی", + "shuffling_cards": "بر زدنِ کارت‌ها", + "telephone_bell_ringing": "زنگ خوردن تلفن", + "ringtone": "زنگ تماس", + "telephone_dialing": "شماره‌گیری تلفن", + "dial_tone": "بوق آزاد", + "busy_signal": "بوق اشغال", + "alarm_clock": "ساعت زنگ‌دار", + "fire_alarm": "هشدار آتش‌سوزی", + "foghorn": "بوق مه", + "whistle": "سوت", + "steam_whistle": "سوت بخار", + "mechanisms": "سازوکارها", + "pulleys": "قرقره‌ها", + "sewing_machine": "چرخ خیاطی", + "mechanical_fan": "پنکهٔ مکانیکی", + "air_conditioning": "تهویهٔ مطبوع", + "cash_register": "صندوق فروش", + "jackhammer": "چکش بادی", + "sawing": "اره‌کردن", + "drill": "دریل", + "sanding": "سنباده‌کاری", + "power_tool": "ابزار برقی", + "filing": "سوهان‌کاری", + "artillery_fire": "آتش توپخانه", + "cap_gun": "تفنگ ترقه‌ای", + "fireworks": "آتش‌بازی", + "firecracker": "ترقه", + "burst": "ترکیدن", + "crack": "ترک", + "glass": "شیشه", + "chink": "جرینگ", + "shatter": "خُرد شدن", + "silence": "سکوت", + "television": "تلویزیون", + "radio": "رادیو", + "field_recording": "ضبط میدانی", + "scream": "جیغ", + "chird": "جیرجیر", + "change_ringing": "زنگ خوردن پول خرد", + "shofar": "شوفار", + "liquid": "مایع", + "splash": "پاشیدن", + "gush": "فوران", + "fill": "پر کردن", + "spray": "اسپری", + "pump": "پمپ", + "stir": "هم زدن", + "thunk": "صدای افتادن", + "electronic_tuner": "تیونر الکترونیکی", + "effects_unit": "واحد افکت‌ها", + "chorus_effect": "افکت کُر", + "basketball_bounce": "پرش توپ بسکتبال", + "bouncing": "پرش", + "whip": "شلاق", + "flap": "بال‌بال زدن", + "scratch": "خراشیدن", + "scrape": "ساییدن", + "beep": "بیپ", + "ping": "پینگ", + "ding": "دینگ", + "clang": "تق", + "squeal": "جیغ", + "clicking": "کلیک‌کردن", + "clickety_clack": "تَق‌تَق", + "rumble": "غرّش", + "plop": "پَت", + "chirp_tone": "صدای جیک", + "pulse": "پالس", + "inside": "داخل", + "outside": "بیرون", + "reverberation": "پژواک", + "cacophony": "همهمه", + "throbbing": "تپش", + "vibration": "لرزش", + "hands": "دست‌ها", + "cheering": "تشویق کردن", + "caw": "قارقار", + "jingle": "جینگل", + "middle_eastern_music": "موسیقی خاورمیانه‌ای", + "stream": "جریان", + "fire": "آتش", + "crackle": "ترق‌تروق", + "sailboat": "قایق بادبانی", + "rowboat": "قایق پارویی", + "power_windows": "شیشه‌بالابر برقی", + "skidding": "سرخوردن", + "tire_squeal": "جیغ لاستیک", + "car_passing_by": "عبور خودرو", + "race_car": "خودروی مسابقه", + "emergency_vehicle": "خودروی امدادی", + "police_car": "خودروی پلیس", + "vacuum_cleaner": "جاروبرقی", + "zipper": "زیپ", + "typing": "تایپ کردن", + "typewriter": "ماشین تحریر", + "computer_keyboard": "صفحه‌کلید رایانه", + "writing": "نوشتن", + "alarm": "هشدار", + "telephone": "تلفن", + "siren": "آژیر", + "civil_defense_siren": "آژیر دفاع مدنی", + "buzzer": "بیزر", + "smoke_detector": "آشکارساز دود", + "ratchet": "جغجغه", + "tick-tock": "تیک‌تاک", + "gears": "چرخ‌دنده‌ها", + "printer": "چاپگر", + "single-lens_reflex_camera": "دوربین تک‌لنزی بازتابی", + "tools": "ابزارها", + "hammer": "چکش", + "explosion": "انفجار", + "gunshot": "شلیک", + "machine_gun": "مسلسل", + "fusillade": "رگبار", + "eruption": "فوران", + "boom": "بوم", + "wood": "چوب", + "sound_effect": "جلوهٔ صوتی", + "splinter": "تراشه", + "environmental_noise": "نویز محیطی", + "static": "ساکن", + "white_noise": "نویز سفید", + "squish": "فشردن", + "drip": "چکه", + "pour": "ریختن", + "trickle": "چکیدن", + "boiling": "جوشیدن", + "thump": "کوبیدن", + "bang": "بنگ", + "slap": "سیلی", + "whack": "ضربه", + "smash": "خرد کردن", + "roll": "غلتیدن", + "crushing": "خرد کردن", + "crumpling": "چروک شدن", + "tearing": "پاره کردن", + "creak": "جیرجیر", + "clatter": "قارقار", + "sizzle": "جوشیدن", + "hum": "زمزمه", + "zing": "زنگ", + "boing": "بویینگ", + "crunch": "خرد کردن", + "noise": "نویز", + "mains_hum": "زمزمهٔ برق", + "distortion": "اعوجاج", + "sidetone": "صدای گوشی", + "ambulance": "آمبولانس", + "fire_engine": "خودروی آتش‌نشانی", + "railroad_car": "واگن راه‌آهن", + "train_wheels_squealing": "جیرجیر چرخ‌های قطار", + "subway": "مترو", + "aircraft": "هوانورد", + "aircraft_engine": "موتور هواپیما", + "engine": "موتور", + "light_engine": "موتور سبک", + "dental_drill's_drill": "متهٔ دندانپزشکی", + "lawn_mower": "چمن‌زن", + "chainsaw": "ارهٔ زنجیری", + "accelerating": "شتاب‌گیری", + "doorbell": "زنگ در", + "ding-dong": "دینگ‌دونگ", + "sliding_door": "در کشویی", + "drawer_open_or_close": "باز یا بسته شدن کشو", + "dishes": "ظروف", + "cutlery": "قاشق و چنگال", + "chopping": "خرد کردن", + "frying": "سرخ کردن", + "electric_toothbrush": "مسواک برقی", + "tick": "تیک", + "chop": "خرد کردن", + "pink_noise": "نویز صورتی", + "sodeling": "سودلینگ", + "slosh": "پاشیدن", + "sonar": "سونار", + "arrow": "پیکان", + "whoosh": "ووش", + "breaking": "شکستن", + "rub": "مالیدن", + "rustle": "خش‌خش", + "whir": "وزوز", + "sine_wave": "موج سینوسی", + "harmonic": "هارمونیک", + "echo": "پژواک" +} diff --git a/web/public/locales/fa/common.json b/web/public/locales/fa/common.json new file mode 100644 index 0000000..5cd5a1e --- /dev/null +++ b/web/public/locales/fa/common.json @@ -0,0 +1,322 @@ +{ + "time": { + "untilForTime": "تا {{time}}", + "untilForRestart": "تا زمانی که فریگیت دوباره شروع به کار کند.", + "untilRestart": "تا زمان ری‌استارت", + "ago": "{{timeAgo}} قبل", + "justNow": "هم اکنون", + "today": "امروز", + "yesterday": "دیروز", + "last7": "۷ روز گذشته", + "last14": "۱۴ روز گذشته", + "last30": "۳۰ روز گذشته", + "thisWeek": "این هفته", + "lastWeek": "هفتهٔ گذشته", + "thisMonth": "این ماه", + "lastMonth": "ماه گذشته", + "5minutes": "۵ دقیقه", + "10minutes": "۱۰ دقیقه", + "day_one": "{{time}} روز", + "day_other": "{{time}} روزها", + "h": "{{time}}س", + "hour_one": "{{time}} ساعت", + "hour_other": "{{time}} ساعتها", + "m": "{{time}} دقیقه", + "minute_one": "{{time}} دقیقه", + "minute_other": "{{time}} دقایق", + "s": "{{time}}ث", + "30minutes": "۳۰ دقیقه", + "1hour": "۱ ساعت", + "12hours": "۱۲ ساعت", + "24hours": "۲۴ ساعت", + "pm": "ب.ظ.", + "am": "ق.ظ.", + "yr": "{{time}} سال", + "year_one": "{{time}} سال", + "year_other": "{{time}} سالها", + "mo": "{{time}} ماه", + "month_one": "{{time}} ماه", + "month_other": "{{time}} ماه ها", + "d": "{{time}} روز", + "second_one": "{{time}} ثانیه", + "second_other": "‏{{time}} ثانیه", + "formattedTimestamp": { + "12hour": "MMM d، h:mm:ss aaa", + "24hour": "MMM d، HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ssd MMM، HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "yyyy MMM d, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "inProgress": "در حال انجام", + "invalidStartTime": "زمان شروع نامعتبر است", + "invalidEndTime": "زمان پایان نامعتبر است", + "never": "هرگز" + }, + "unit": { + "length": { + "feet": "فوت", + "meters": "متر" + }, + "data": { + "kbps": "kB/s", + "gbps": "GB/s", + "mbph": "مگابایت/ساعت", + "gbph": "گیگابایت/ساعت", + "mbps": "مگابایت/ثانیه", + "kbph": "کیلوبایت/ساعت" + }, + "speed": { + "mph": "مایل/ساعت", + "kph": "کیلومتر/ساعت" + } + }, + "label": { + "hide": "پنهان کردن {{item}}", + "ID": "شناسه", + "all": "همه", + "back": "برگشت به قبل", + "show": "نمایش {{item}}", + "none": "هیچ‌کدام", + "other": "دیگر" + }, + "list": { + "many": "{{items}}، و {{last}}", + "two": "{{0}} و {{1}}", + "separatorWithSpace": ", · " + }, + "field": { + "internalID": "شناسهٔ داخلی‌ای که Frigate در پیکربندی و پایگاه‌داده استفاده می‌کند", + "optional": "اختیاری" + }, + "button": { + "apply": "اعمال", + "done": "انجام شد", + "enable": "فعال کردن", + "disabled": "غیرفعال", + "cancel": "لغو", + "close": "بستن", + "back": "بازگشت", + "fullscreen": "تمام‌صفحه", + "exitFullscreen": "خروج از حالت تمام‌صفحه", + "twoWayTalk": "مکالمهٔ دوطرفه", + "cameraAudio": "صدای دوربین", + "off": "خاموش", + "delete": "حذف", + "download": "دانلود", + "unsuspended": "برداشتن تعلیق", + "unselect": "لغو انتخاب", + "export": "خروجی گرفتن", + "next": "بعدی", + "reset": "بازنشانی", + "enabled": "فعال", + "disable": "غیرفعال کردن", + "save": "ذخیره", + "saving": "در حال ذخیره‌سازی…", + "copy": "کپی", + "history": "تاریخچه", + "pictureInPicture": "تصویر در تصویر", + "copyCoordinates": "کپی مختصات", + "yes": "بله", + "no": "خیر", + "info": "اطلاعات", + "play": "پخش", + "deleteNow": "حذف فوری", + "continue": "ادامه", + "on": "روشن", + "edit": "ویرایش", + "suspended": "تعلیق‌شده", + "add": "افزودن", + "applying": "در حال اعمال…", + "undo": "برگرداندن", + "copiedToClipboard": "در کلیپ‌بورد کپی شد", + "modified": "اصلاح شده", + "retry": "تلاش دوباره", + "resetToGlobal": "بازنشانی به حالت جهانی", + "resetToDefault": "بازنشانی به پیش‌فرض", + "saveAll": "ذخیره همه", + "savingAll": "ذخیره‌سازی همه…", + "undoAll": "بازگردانی همه" + }, + "menu": { + "systemMetrics": "شاخص‌های سیستم", + "configuration": "پیکربندی", + "settings": "تنظیمات", + "language": { + "en": "انگلیسی (English)", + "hi": "هندی (Hindi)", + "fr": "فرانسوی (French)", + "ptBR": "پرتغالیِ برزیل (Brazilian Portuguese)", + "ru": "روسی (Russian)", + "es": "اسپانیایی (زبان اسپانیایی)", + "zhCN": "چینی ساده‌شده (چینی ساده)", + "ar": "عربی (زبان عربی)", + "pt": "پرتغالی (زبان پرتغالی)", + "de": "آلمانی (زبان آلمانی)", + "ja": "ژاپنی (زبان ژاپنی)", + "tr": "ترکی (زبان ترکی)", + "it": "ایتالیایی (زبان ایتالیایی)", + "nl": "هلندی (زبان هلندی)", + "sv": "سوئدی (زبان سوئدی)", + "cs": "چکی (زبان چکی)", + "nb": "بوکمل نروژیایی (بوکمل نروژی)", + "ko": "کره‌ای (زبان کره‌ای)", + "vi": "ویتنامی (زبان ویتنامی)", + "fa": "فارسی (زبان فارسی)", + "pl": "لهستانی (زبان لهستانی)", + "uk": "اوکراینی (زبان اوکراینی)", + "he": "عبری (زبان عبری)", + "el": "یونانی (زبان یونانی)", + "ro": "رومانیایی (زبان رومانیایی)", + "hu": "مجاری (زبان مجاری)", + "fi": "فنلاندی (زبان فنلاندی)", + "da": "دانمارکی (زبان دانمارکی)", + "sk": "اسلواکی (زبان اسلواکی)", + "yue": "کانتونی (زبان کانتونی)", + "th": "تایلندی (زبان تایلندی)", + "ca": "کاتالانی (زبان کاتالانی)", + "sr": "صربی (زبان صربی)", + "sl": "اسلوونیایی (زبان اسلوونیایی)", + "lt": "لیتوانیایی (زبان لیتوانیایی)", + "bg": "بلغاری (زبان بلغاری)", + "gl": "گالیسیایی (زبان گالیسیایی)", + "id": "اندونزیایی (زبان اندونزیایی)", + "ur": "اردو (زبان اردو)", + "withSystem": { + "label": "برای زبان از تنظیمات سامانه استفاده کنید" + }, + "zhHant": "چینی سنتی", + "hr": "کرواسیایی" + }, + "system": "سامانه", + "systemLogs": "لاگ‌های سامانه", + "configurationEditor": "ویرایشگر پیکربندی", + "languages": "زبان‌ها", + "appearance": "ظاهر", + "darkMode": { + "label": "حالت تاریک", + "light": "روشنایی", + "dark": "تاریک", + "withSystem": { + "label": "برای حالت روشن یا تاریک از تنظیمات سامانه استفاده کنید" + } + }, + "withSystem": "سامانه", + "theme": { + "label": "پوسته", + "blue": "آبی", + "green": "سبز", + "nord": "نورد", + "red": "قرمز", + "highcontrast": "کنتراست بالا", + "default": "پیش‌فرض" + }, + "help": "راهنما", + "documentation": { + "title": "مستندات", + "label": "مستندات Frigate" + }, + "restart": "راه‌اندازی مجدد Frigate", + "live": { + "title": "زنده", + "allCameras": "همهٔ دوربین‌ها", + "cameras": { + "title": "دوربین‌ها", + "count_one": "{{count}} دوربین", + "count_other": "{{count}} دوربینها" + } + }, + "review": "بازبینی", + "explore": "کاوش", + "export": "خروجی گرفتن", + "uiPlayground": "محیط آزمایشی UI", + "faceLibrary": "کتابخانهٔ چهره", + "classification": "طبقه‌بندی", + "user": { + "title": "کاربر", + "account": "حساب کاربری", + "current": "کاربر فعلی: {{user}}", + "anonymous": "ناشناس", + "logout": "خروج", + "setPassword": "تنظیم گذرواژه" + }, + "profiles": "پروفایل‌ها", + "actions": "اقدام‌ها", + "features": "ویژگی‌ها", + "chat": "چت" + }, + "toast": { + "copyUrlToClipboard": "نشانی اینترنتی در کلیپ‌بورد کپی شد.", + "save": { + "title": "ذخیره", + "error": { + "title": "ذخیرهٔ تغییرات پیکربندی ناموفق بود: {{errorMessage}}", + "noMessage": "ذخیرهٔ تغییرات پیکربندی ناموفق بود" + }, + "success": "تغییرات پیکربندی با موفقیت ذخیره شد." + } + }, + "role": { + "title": "نقش", + "admin": "مدیر", + "viewer": "بیننده", + "desc": "مدیران به همهٔ ویژگی‌ها در رابط کاربری Frigate دسترسی کامل دارند. بیننده‌ها فقط می‌توانند دوربین‌ها، موارد بازبینی و ویدیوهای تاریخی را در رابط کاربری مشاهده کنند." + }, + "pagination": { + "label": "صفحه‌بندی", + "previous": { + "title": "قبلی", + "label": "رفتن به صفحهٔ قبلی" + }, + "next": { + "title": "بعدی", + "label": "رفتن به صفحهٔ بعدی" + }, + "more": "صفحه‌های بیشتر" + }, + "accessDenied": { + "documentTitle": "دسترسی ممنوع - Frigate", + "title": "دسترسی ممنوع", + "desc": "شما اجازهٔ مشاهدهٔ این صفحه را ندارید." + }, + "notFound": { + "documentTitle": "یافت نشد - Frigate", + "title": "۴۰۴", + "desc": "صفحه پیدا نشد" + }, + "selectItem": "انتخاب {{item}}", + "readTheDocumentation": "مستندات را بخوانید", + "information": { + "pixels": "{{area}}px" + }, + "no_items": "هیچ چیزی یافت نشد", + "validation_errors": "خطاهای اعتبارسنجی", + "credentialField": { + "savedPlaceholder": "ذخیره شد — برای نگه‌داشت وضعیت کنونی، خالی بگذارید" + } +} diff --git a/web/public/locales/fa/components/auth.json b/web/public/locales/fa/components/auth.json new file mode 100644 index 0000000..3c4e021 --- /dev/null +++ b/web/public/locales/fa/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "نام کاربری", + "password": "رمز عبور", + "login": "ورود", + "firstTimeLogin": "اولین باز است وارد می شود؟ اطلاعات هویتی در ثبت رخداد های فریگیت چاپ خواهد شد.", + "errors": { + "usernameRequired": "وارد کردن نام کاربری الزامی است", + "passwordRequired": "وارد کردن رمز عبور الزامی است", + "loginFailed": "ورود ناموفق بود", + "unknownError": "خطای ناشناخته. گزارش‌ها را بررسی کنید.", + "webUnknownError": "خطای ناشناخته. گزارش‌های کنسول را بررسی کنید.", + "rateLimit": "از حد مجاز درخواست‌ها فراتر رفت. بعداً دوباره تلاش کنید." + } + } +} diff --git a/web/public/locales/fa/components/camera.json b/web/public/locales/fa/components/camera.json new file mode 100644 index 0000000..a44533d --- /dev/null +++ b/web/public/locales/fa/components/camera.json @@ -0,0 +1,90 @@ +{ + "group": { + "label": "گروه‌های دوربین", + "add": "افزودن گروه دوربین", + "edit": "ویرایش گروه دوربین", + "delete": { + "label": "حذف گروه دوربین ها", + "confirm": { + "title": "تأیید حذف", + "desc": "آیا مطمئن هستید که می‌خواهید گروه دوربین «{{name}}» را حذف کنید؟" + } + }, + "name": { + "label": "نام", + "placeholder": "یک نام وارد کنید…", + "errorMessage": { + "mustLeastCharacters": "نام گروه دوربین باید حداقل ۲ کاراکتر باشد.", + "exists": "نام گروه دوربین از قبل وجود دارد.", + "nameMustNotPeriod": "نام گروه دوربین نباید شامل نقطه باشد.", + "invalid": "نام گروه دوربین نامعتبر است." + } + }, + "cameras": { + "desc": "دوربین‌های این گروه را انتخاب کنید.", + "label": "دوربین‌ها" + }, + "icon": "آیکون", + "success": "گروه دوربین ({{name}}) ذخیره شد.", + "camera": { + "setting": { + "streamMethod": { + "method": { + "noStreaming": { + "label": "بدون پخش", + "desc": "تصاویر دوربین فقط هر یک دقیقه یک‌بار به‌روزرسانی می‌شوند و هیچ پخش زنده‌ای انجام نخواهد شد." + }, + "smartStreaming": { + "label": "پخش هوشمند (پیشنهادی)", + "desc": "پخش هوشمند زمانی که فعالیت قابل تشخیصی وجود ندارد برای صرفه‌جویی در پهنای باند و منابع، تصویر دوربین شما را هر یک دقیقه یک‌بار به‌روزرسانی می‌کند. وقتی فعالیت تشخیص داده شود، تصویر به‌طور یکپارچه به پخش زنده تغییر می‌کند." + }, + "continuousStreaming": { + "label": "پخش پیوسته", + "desc": { + "title": "تصویر دوربین وقتی در داشبورد قابل مشاهده باشد همیشه پخش زنده خواهد بود، حتی اگر هیچ فعالیتی تشخیص داده نشود.", + "warning": "پخش پیوسته ممکن است باعث مصرف بالای پهنای‌باند و مشکلات عملکردی شود. با احتیاط استفاده کنید." + } + } + }, + "label": "روش پخش", + "placeholder": "یک روش پخش را انتخاب کنید" + }, + "label": "تنظیمات پخش دوربین", + "title": "تنظیمات پخش {{cameraName}}", + "audioIsAvailable": "صدا برای این پخش در دسترس است", + "audioIsUnavailable": "صدا برای این پخش در دسترس نیست", + "audio": { + "tips": { + "title": "برای این پخش، صدا باید از دوربین شما خروجی گرفته شود و در go2rtc پیکربندی شده باشد." + } + }, + "stream": "جریان", + "placeholder": "یک جریان را برگزینید", + "compatibilityMode": { + "label": "حالت سازگاری", + "desc": "این گزینه را فقط زمانی فعال کنید که پخش زندهٔ دوربین شما دچار آثار رنگی (artifact) است و در سمت راست تصویر یک خط مورب دیده می‌شود." + }, + "desc": "گزینه‌های پخش زنده را برای داشبورد این گروه دوربین تغییر دهید. این تنظیمات مخصوص دستگاه/مرورگر هستند. " + }, + "birdseye": "نمای پرنده" + }, + "showAll": "نمایش تمام گروه های دوربین", + "showLess": "نمایش کمتر", + "editGroups": "ویرایش گروه های دوربین" + }, + "debug": { + "options": { + "label": "تنظیمات", + "title": "گزینه‌ها", + "showOptions": "نمایش گزینه‌ها", + "hideOptions": "پنهان کردن گزینه‌ها" + }, + "boundingBox": "کادر محدوده", + "timestamp": "مهر زمانی", + "zones": "ناحیه‌ها", + "mask": "ماسک", + "motion": "حرکت", + "regions": "مناطق", + "paths": "مسیرها" + } +} diff --git a/web/public/locales/fa/components/dialog.json b/web/public/locales/fa/components/dialog.json new file mode 100644 index 0000000..99095fc --- /dev/null +++ b/web/public/locales/fa/components/dialog.json @@ -0,0 +1,122 @@ +{ + "restart": { + "title": "آیا برای راه اندازی مجدد Frigate مطمئن هستید؟", + "button": "ری‌استارت", + "restarting": { + "title": "فریگیت در حال ری‌استارت شدن", + "content": "صفحه تا {{countdown}} ثانیه دیگر مجددا بارگزاری خواهد شد.", + "button": "بارگزاری مجدد هم اکنون اجرا شود" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "ارسال به Frigate+", + "desc": "اشیایی که در مکان‌هایی هستند که می‌خواهید از آن‌ها اجتناب کنید، «مثبت کاذب» محسوب نمی‌شوند. ارسال آن‌ها به‌عنوان مثبت کاذب باعث می‌شود مدل دچار سردرگمی شود." + }, + "review": { + "question": { + "label": "این برچسب را برای Frigate Plus تأیید کنید", + "ask_a": "آیا این شیء {{label}} است؟", + "ask_an": "آیا این شیء یک {{label}} است؟", + "ask_full": "آیا این شیء یک {{untranslatedLabel}} ({{translatedLabel}}) است؟" + }, + "state": { + "submitted": "ارسال شد" + } + } + }, + "video": { + "viewInHistory": "مشاهده در تاریخچه" + } + }, + "export": { + "time": { + "fromTimeline": "انتخاب از خط زمانی", + "lastHour_one": "ساعت گذشته", + "lastHour_other": "آخرین {{count}} ساعت", + "custom": "سفارشی", + "start": { + "title": "زمان شروع", + "label": "زمان شروع را انتخاب کنید" + }, + "end": { + "title": "زمان پایان", + "label": "زمان پایان را انتخاب کنید" + } + }, + "toast": { + "error": { + "endTimeMustAfterStartTime": "زمان پایان باید بعد از زمان شروع باشد", + "noVaildTimeSelected": "بازهٔ زمانی معتبر انتخاب نشده است", + "failed": "شروع خروجی‌گیری ناموفق بود: {{error}}" + }, + "success": "ساخت خروجی با موفقیت آغاز شد. فایل را در صفحه خروجی‌ها مشاهده کنید.", + "view": "مشاهده" + }, + "fromTimeline": { + "saveExport": "ذخیرهٔ خروجی", + "previewExport": "پیش‌نمایش خروجی" + }, + "name": { + "placeholder": "برای خروجی نام بگذارید" + }, + "select": "انتخاب", + "export": "خروجی", + "selectOrExport": "انتخاب یا خروجی" + }, + "streaming": { + "label": "جریان", + "restreaming": { + "disabled": "بازپخش برای این دوربین فعال نیست.", + "desc": { + "title": "برای گزینه‌های بیشتر نمایش زنده و صدا برای این دوربین، go2rtc را تنظیم کنید." + } + }, + "showStats": { + "label": "نمایش آمار جریان", + "desc": "این گزینه را فعال کنید تا آمار جریان به‌صورت پوششی روی تصویر دوربین نمایش داده شود." + }, + "debugView": "نمای اشکال‌زدایی" + }, + "search": { + "saveSearch": { + "label": "ذخیره جست‌وجو", + "desc": "برای این جست‌وجوی ذخیره‌شده یک نام وارد کنید.", + "placeholder": "برای جستجوی خود یک نام وارد کنید", + "success": "جستجو ({{searchName}}) ذخیره شد.", + "button": { + "save": { + "label": "ذخیرهٔ این جستجو" + } + }, + "overwrite": "{{searchName}} موجود است. ذخیره سازی منجر به بازنویسی مقدار موجود خواهد شد." + } + }, + "recording": { + "confirmDelete": { + "title": "تأیید حذف", + "desc": { + "selected": "آیا مطمئن هستید که می‌خواهید همهٔ ویدیوهای ضبط‌شدهٔ مرتبط با این مورد بازبینی را حذف کنید؟

    برای رد کردن این پنجره در آینده، کلید Shift را نگه دارید." + }, + "toast": { + "success": "ویدیوهای مرتبط با موارد بازبینیِ انتخاب‌شده با موفقیت حذف شد.", + "error": "حذف ناموفق بود: {{error}}" + } + }, + "button": { + "export": "خروجی گرفتن", + "markAsReviewed": "علامت‌گذاری به‌عنوان بازبینی‌شده", + "markAsUnreviewed": "علامت‌گذاری به‌عنوان بازبینی‌نشده", + "deleteNow": "حذف فوری" + } + }, + "imagePicker": { + "selectImage": "یک بندانگشتیِ شیء ردیابی‌شده را انتخاب کنید", + "unknownLabel": "تصویر محرک ذخیره شد", + "search": { + "placeholder": "جستجو بر اساس برچسب یا زیر‌برچسب…" + }, + "noImages": "برای این دوربین بندانگشتی‌ای یافت نشد" + } +} diff --git a/web/public/locales/fa/components/filter.json b/web/public/locales/fa/components/filter.json new file mode 100644 index 0000000..fe60049 --- /dev/null +++ b/web/public/locales/fa/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "فیلتر", + "classes": { + "label": "کلاس‌ها", + "all": { + "title": "تمامی کلاس ها" + }, + "count_one": "{{count}} کلاس", + "count_other": "{{count}} کلاس‌ها" + }, + "labels": { + "label": "برچسب‌ها", + "all": { + "title": "همه برچسب‌ها", + "short": "برچسب‌ها" + }, + "count_one": "{{count}} برچسب", + "count_other": "{{count}} برچسب‌ها" + }, + "zones": { + "label": "مناطق", + "all": { + "title": "همهٔ ناحیه‌ها", + "short": "ناحیه‌ها" + } + }, + "dates": { + "selectPreset": "یک پیش‌تنظیم را انتخاب کنید…", + "all": { + "title": "همهٔ تاریخ‌ها", + "short": "تاریخ‌ها" + } + }, + "features": { + "hasVideoClip": "دارای کلیپ ویدئویی است", + "submittedToFrigatePlus": { + "label": "ارسال‌شده به Frigate+", + "tips": "ابتدا باید روی اشیای ردیابی‌شده‌ای که عکس فوری دارند فیلتر کنید.

    اشیای ردیابی‌شده بدون عکس فوری نمی‌توانند به Frigate+ ارسال شوند." + }, + "label": "قابلیت‌ها", + "hasSnapshot": "دارای یک عکس فوری" + }, + "sort": { + "label": "مرتب‌سازی", + "dateAsc": "تاریخ (صعودی)", + "dateDesc": "تاریخ (نزولی)", + "scoreAsc": "امتیاز شیء (صعودی)", + "scoreDesc": "امتیاز شیء (نزولی)", + "speedAsc": "سرعت تخمینی (صعودی)", + "speedDesc": "سرعت تخمینی (نزولی)", + "relevance": "آموزش چهره به‌عنوان:ارتباط" + }, + "more": "فیلترهای بیشتر", + "reset": { + "label": "بازنشانی فیلترها به مقادیر پیش‌فرض" + }, + "timeRange": "بازهٔ زمانی", + "subLabels": { + "label": "زیربرچسب‌ها", + "all": "همهٔ زیر برچسب‌ها" + }, + "attributes": { + "label": "ویژگی‌های طبقه‌بندی", + "all": "همهٔ ویژگی‌ها" + }, + "score": "امتیاز", + "estimatedSpeed": "سرعت تخمینی ( {{unit}})", + "cameras": { + "label": "فیلتر دوربین‌ها", + "all": { + "title": "همهٔ دوربین‌ها", + "short": "دوربین‌ها" + } + }, + "logSettings": { + "filterBySeverity": "فیلتر کردن لاگ‌ها بر اساس شدت", + "loading": { + "desc": "وقتی پنل لاگ تا پایین‌ترین نقطه اسکرول شود، لاگ‌های جدید هنگام اضافه‌شدن به‌صورت خودکار نمایش داده می‌شوند.", + "title": "در حال بارگذاری" + }, + "label": "فیلتر سطح لاگ", + "disableLogStreaming": "غیرفعال کردن پخش زندهٔ لاگ", + "allLogs": "همهٔ لاگ‌ها" + }, + "trackedObjectDelete": { + "title": "تأیید حذف", + "toast": { + "success": "اشیای ردیابی‌شده با موفقیت حذف شدند.", + "error": "حذف اشیای ردیابی‌شده ناموفق بود: {{errorMessage}}" + }, + "desc": "حذف این {{objectLength}} شیء ردیابی‌شده باعث حذف عکس فوری، هرگونه امبدینگِ ذخیره‌شده و همهٔ ورودی‌های مرتبط با چرخهٔ عمر شیء می‌شود. ویدیوهای ضبط‌شدهٔ این اشیای ردیابی‌شده در نمای تاریخچه حذف نخواهند شد.

    آیا مطمئن هستید که می‌خواهید ادامه دهید؟

    برای رد کردن این پنجره در آینده، کلید Shift را نگه دارید." + }, + "zoneMask": { + "filterBy": "فیلتر بر اساس ماسک ناحیه" + }, + "recognizedLicensePlates": { + "loadFailed": "بارگذاری پلاک‌های شناسایی‌شده ناموفق بود.", + "loading": "در حال بارگذاری پلاک‌های شناسایی‌شده…", + "noLicensePlatesFound": "هیچ پلاکی پیدا نشد.", + "selectAll": "انتخاب همه", + "title": "پلاک‌های شناسایی‌شده", + "placeholder": "برای جستجوی پلاک‌ها تایپ کنید…", + "selectPlatesFromList": "یک یا چند پلاک را از فهرست انتخاب کنید.", + "clearAll": "پاک کردن همه" + }, + "review": { + "showReviewed": "نمایش بازبینی‌شده‌ها" + }, + "motion": { + "showMotionOnly": "فقط نمایش حرکت" + }, + "explore": { + "settings": { + "title": "تنظیمات", + "defaultView": { + "title": "نمای پیش‌فرض", + "summary": "خلاصه", + "unfilteredGrid": "شبکهٔ بدون فیلتر", + "desc": "هنگامی که هیچ فیلتری انتخاب نشده باشد، خلاصه ای از آخرین اشیاء ردیابی شده در هر برچسب یا یک شبکه فیلتر نشده نمایش داده خواهد شد." + }, + "gridColumns": { + "title": "ستون‌های شبکه", + "desc": "تعداد ستون‌ها را در نمای شبکه انتخاب کنید." + }, + "searchSource": { + "label": "منبع جستجو", + "desc": "انتخاب کنید که در بندانگشتی‌ها جستجو شود یا در توضیحات اشیای ردیابی‌شده.", + "options": { + "thumbnailImage": "تصویر پیش‌نمایش", + "description": "توضیحات" + } + } + }, + "date": { + "selectDateBy": { + "label": "یک تاریخ را برای فیلتر کردن انتخاب کنید" + } + } + } +} diff --git a/web/public/locales/fa/components/icons.json b/web/public/locales/fa/components/icons.json new file mode 100644 index 0000000..0fa7bec --- /dev/null +++ b/web/public/locales/fa/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "انتخاب آیکون", + "search": { + "placeholder": "جستجو برای آیکون…" + } + } +} diff --git a/web/public/locales/fa/components/input.json b/web/public/locales/fa/components/input.json new file mode 100644 index 0000000..20de892 --- /dev/null +++ b/web/public/locales/fa/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "دریافت ویدیو", + "toast": { + "success": "ویدیوی مورد بررسی شما درحال دریافت می‌باشد." + } + } + } +} diff --git a/web/public/locales/fa/components/player.json b/web/public/locales/fa/components/player.json new file mode 100644 index 0000000..f28e1a1 --- /dev/null +++ b/web/public/locales/fa/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "ویدیویی برای این زمان وجود ندارد", + "noPreviewFound": "پیش‌نمایش پیدا نشد", + "noPreviewFoundFor": "هیچ پیش‌نمایشی برای {{cameraName}} پیدا نشد", + "submitFrigatePlus": { + "title": "این فریم به فریگیت+ ارسال شود؟", + "submit": "ارسال", + "previewError": "نمی توان پیش نمایش عکس فوری را بارگذاری کرد. ممکن است ضبط در این زمان در دسترس نباشد." + }, + "livePlayerRequiredIOSVersion": "برای این نوع پخش زنده، iOS 17.1 یا بالاتر لازم است.", + "streamOffline": { + "title": "جریان آفلاین", + "desc": "هیچ فریمی از جریان detect دوربین {{cameraName}} دریافت نشده است، گزارش‌های خطا را بررسی کنید" + }, + "cameraDisabled": "دوربین غیرفعال است", + "stats": { + "streamType": { + "title": "نوع جریان:", + "short": "نوع" + }, + "bandwidth": { + "title": "پهنای باند:", + "short": "پهنای باند" + }, + "latency": { + "title": "تأخیر:", + "value": "{{seconds}} ثانیه‌ها", + "short": { + "title": "تأخیر", + "value": "{{seconds}} ثانیه" + } + }, + "totalFrames": "مجموع فریم‌ها:", + "droppedFrames": { + "title": "فریم‌های از دست‌رفته:", + "short": { + "title": "از دست‌رفته", + "value": "{{droppedFrames}} فریم" + } + }, + "decodedFrames": "فریم‌های رمزگشایی‌شده:", + "droppedFrameRate": "نرخ فریم‌های از دست‌رفته:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "فریم با موفقیت به Frigate+ ارسال شد" + }, + "error": { + "submitFrigatePlusFailed": "ارسال فریم به Frigate+ ناموفق بود" + } + }, + "cameraOff": "دوربین خاموش است" +} diff --git a/web/public/locales/fa/config/cameras.json b/web/public/locales/fa/config/cameras.json new file mode 100644 index 0000000..0c123fb --- /dev/null +++ b/web/public/locales/fa/config/cameras.json @@ -0,0 +1,941 @@ +{ + "label": "پیکربندی دوربین", + "name": { + "label": "نام دوربین", + "description": "نام دوربین الزامی است" + }, + "friendly_name": { + "label": "نام دوستانه", + "description": "نام مناسب برای دوربین که در رابط کاربری Frigate استفاده شده است" + }, + "enabled": { + "label": "فعال شده", + "description": "فعال شده" + }, + "audio": { + "label": "رویدادهای صوتی", + "description": "تنظیمات تشخیص رویداد مبتنی بر صدا برای این دوربین.", + "enabled": { + "label": "فعال کردن تشخیص صدا", + "description": "تشخیص رویداد صوتی را برای این دوربین فعال یا غیرفعال کنید." + }, + "max_not_heard": { + "label": "پایان مهلت", + "description": "تعداد ثانیه‌هایی که قبل از پایان رویداد صوتی، نوع صدای پیکربندی‌شده بدون آن باقی می‌ماند." + }, + "min_volume": { + "label": "حداقل صدا", + "description": "حداقل آستانه حجم RMS مورد نیاز برای اجرای تشخیص صدا؛ مقادیر پایین‌تر حساسیت را افزایش می‌دهند (مثلاً ۲۰۰ زیاد، ۵۰۰ متوسط، ۱۰۰۰ کم)." + }, + "listen": { + "label": "انواع گوش دادن", + "description": "فهرست انواع رویدادهای صوتی برای تشخیص (به عنوان مثال: پارس کردن، آژیر آتش، جیغ، گفتار، فریاد)." + }, + "filters": { + "label": "فیلترهای صوتی", + "description": "تنظیمات فیلتر بر اساس نوع صدا مانند آستانه‌های اطمینان که برای کاهش تشخیص‌های مثبت کاذب استفاده می‌شوند." + }, + "enabled_in_config": { + "label": "وضعیت صوتی اصلی", + "description": "نشان می‌دهد که آیا تشخیص صدا در ابتدا در فایل پیکربندی استاتیک فعال بوده است یا خیر." + }, + "num_threads": { + "label": "رشته‌های تشخیص", + "description": "تعداد رشته‌های مورد استفاده برای پردازش تشخیص صدا." + } + }, + "audio_transcription": { + "label": "رونویسی صوتی", + "description": "تنظیمات مربوط به رونویسی صوتی زنده و گفتاری که برای رویدادها و زیرنویس‌های زنده استفاده می‌شود.", + "enabled": { + "label": "فعال کردن رونویسی", + "description": "فعال یا غیرفعال کردن رونویسی رویداد صوتی با فعال‌سازی دستی." + }, + "enabled_in_config": { + "label": "حالت رونویسی اولیه" + }, + "live_enabled": { + "label": "رونویسی زنده", + "description": "فعال کردن پخش زنده رونویسی برای صدا هنگام دریافت آن." + } + }, + "birdseye": { + "label": "چشم پرندگان", + "description": "تنظیمات نمای ترکیبی چشم پرنده که تصاویر چندین دوربین را در یک طرح واحد ترکیب می‌کند.", + "enabled": { + "label": "فعال کردن چشم پرندگان", + "description": "ویژگی نمای چشم پرندگان را فعال یا غیرفعال کنید." + }, + "mode": { + "label": "حالت ردیابی", + "description": "حالت گنجاندن دوربین‌ها در چشم پرنده: «اشیاء»، «حرکت» یا «پیوسته»." + }, + "order": { + "label": "موقعیت", + "description": "موقعیت عددی که ترتیب قرارگیری دوربین را در طرح چشم پرنده کنترل می‌کند." + } + }, + "detect": { + "label": "تشخیص شیء", + "description": "تنظیمات مربوط به نقش تشخیص/شناسایی که برای اجرای تشخیص شیء و مقداردهی اولیه ردیاب‌ها استفاده می‌شود.", + "enabled": { + "label": "‫تشخیص فعال شد", + "description": "فعال یا غیرفعال کردن تشخیص اشیا برای این دوربین. برای اجرای ردیابی اشیا، تشخیص باید فعال باشد." + }, + "height": { + "label": "تشخیص ارتفاع", + "description": "ارتفاع (پیکسل) فریم‌های مورد استفاده برای تشخیص جریان؛ برای استفاده از وضوح جریان اصلی، خالی بگذارید." + }, + "width": { + "label": "تشخیص عرض", + "description": "عرض (پیکسل) فریم‌های مورد استفاده برای تشخیص جریان؛ برای استفاده از وضوح جریان اصلی، خالی بگذارید." + }, + "fps": { + "label": "تشخیص فریم بر ثانیه - اف پی اس", + "description": "تعداد فریم در ثانیه مورد نظر برای اجرای تشخیص؛ مقادیر پایین‌تر، استفاده از CPU را کاهش می‌دهند (مقدار توصیه شده ۵ است، فقط در صورت ردیابی اشیاء با حرکت بسیار سریع، مقدار بالاتر - حداکثر ۱۰ - تنظیم شود)." + }, + "min_initialized": { + "label": "حداقل فریم‌های مقداردهی اولیه", + "description": "تعداد تشخیص‌های متوالی مورد نیاز قبل از ایجاد یک شیء ردیابی شده. برای کاهش مقداردهی اولیه نادرست، افزایش دهید. مقدار پیش‌فرض، fps تقسیم بر ۲ است." + }, + "max_disappeared": { + "label": "حداکثر فریم‌های ناپدید شده", + "description": "تعداد فریم‌هایی که قبل از اینکه شیء ردیابی شده ناپدید شده تلقی شود، تشخیص داده نمی‌شوند." + }, + "stationary": { + "label": "پیکربندی اشیاء ثابت", + "description": "تنظیماتی برای شناسایی و مدیریت اشیایی که برای مدتی ثابت می‌مانند.", + "interval": { + "label": "بازه ثابت", + "description": "هر چند وقت یکبار (بر حسب فریم) باید بررسی تشخیص برای تأیید یک شیء ثابت انجام شود." + }, + "threshold": { + "label": "آستانه ثابت", + "description": "تعداد فریم‌هایی که بدون تغییر موقعیت لازم هستند تا یک جسم به عنوان ثابت علامت‌گذاری شود." + }, + "max_frames": { + "label": "حداکثر فریم", + "description": "مدت زمانی که اشیاء ثابت قبل از دور انداختن ردیابی می‌شوند را محدود می‌کند.", + "default": { + "label": "حداکثر فریم‌های پیش‌فرض", + "description": "حداکثر فریم‌های پیش‌فرض برای ردیابی یک جسم ثابت قبل از توقف." + }, + "objects": { + "label": "فریم‌های حداکثر شیء", + "description": "برای ردیابی اشیاء ثابت، حداکثر فریم‌ها به ازای هر شیء لغو می‌شوند." + } + }, + "classifier": { + "label": "فعال کردن طبقه‌بندی بصری", + "description": "از یک طبقه‌بندی‌کننده بصری برای تشخیص اشیاء واقعاً ثابت حتی در مواقعی که کادرهای محصورکننده دچار لرزش می‌شوند، استفاده کنید." + } + }, + "annotation_offset": { + "label": "حاشیه‌نویسی افست", + "description": "میلی‌ثانیه برای جابجایی، تشخیص حاشیه‌نویسی‌ها برای ترازبندی بهتر کادرهای محدودکننده‌ی جدول زمانی با ضبط‌ها؛ می‌تواند مثبت یا منفی باشد." + } + }, + "face_recognition": { + "label": "تشخیص چهره", + "description": "تنظیمات تشخیص و شناسایی چهره برای این دوربین.", + "enabled": { + "label": "فعال کردن تشخیص چهره", + "description": "فعال یا غیرفعال کردن تشخیص چهره." + }, + "min_area": { + "label": "حداقل مساحت صورت", + "description": "حداقل مساحت (پیکسل) از کادر چهره شناسایی شده که برای تلاش برای شناسایی مورد نیاز است." + } + }, + "ffmpeg": { + "description": "تنظیمات FFmpeg شامل مسیر دودویی، آرگومان‌ها، گزینه‌های hwaccel و آرگومان‌های خروجی به ازای هر نقش.", + "path": { + "label": "مسیر FFmpeg", + "description": "مسیر فایل باینری FFmpeg برای استفاده یا نام مستعار نسخه (\"5.0\" یا \"7.0\")." + }, + "global_args": { + "description": "آرگومان‌های سراسری به فرآیندهای FFmpeg ارسال شدند.", + "label": "آرگومان‌های سراسری FFmpeg" + }, + "hwaccel_args": { + "label": "آرگومان‌های شتاب سخت‌افزاری", + "description": "آرگومان‌های شتاب سخت‌افزاری برای FFmpeg. تنظیمات پیش‌فرض مخصوص ارائه‌دهنده توصیه می‌شود." + }, + "input_args": { + "label": "آرگومان‌های ورودی", + "description": "آرگومان‌های ورودی اعمال شده به جریان‌های ورودی FFmpeg." + }, + "output_args": { + "label": "آرگومان‌های خروجی", + "description": "آرگومان‌های خروجی پیش‌فرض که برای نقش‌های مختلف FFmpeg مانند شناسایی و ضبط استفاده می‌شوند.", + "detect": { + "label": "تشخیص آرگومان‌های خروجی", + "description": "آرگومان‌های خروجی پیش‌فرض برای تشخیص جریان‌های نقش." + }, + "record": { + "label": "آرگومان‌های خروجی را ضبط کنید", + "description": "آرگومان‌های خروجی پیش‌فرض برای جریان‌های نقش رکورد." + } + }, + "apple_compatibility": { + "label": "سازگاری با اپل", + "description": "برای سازگاری بهتر با پخش‌کننده‌های اپل هنگام ضبط H.265، تگ‌گذاری HEVC را فعال کنید." + }, + "gpu": { + "label": "شاخص پردازنده گرافیکی", + "description": "در صورت وجود، شاخص GPU پیش‌فرض برای شتاب سخت‌افزاری استفاده می‌شود." + }, + "inputs": { + "label": "ورودی‌های دوربین", + "description": "فهرست تعاریف جریان ورودی (مسیرها و نقش‌ها) برای این دوربین.", + "path": { + "label": "مسیر ورودی", + "description": "آدرس اینترنتی یا مسیر جریان ورودی دوربین." + }, + "roles": { + "label": "نقش‌های ورودی", + "description": "نقش‌های این جریان ورودی." + }, + "global_args": { + "label": "آرگومان‌های سراسری FFmpeg", + "description": "آرگومان‌های سراسری FFmpeg برای این جریان ورودی." + }, + "hwaccel_args": { + "label": "آرگومان‌های شتاب سخت‌افزاری", + "description": "آرگومان‌های شتاب سخت‌افزاری برای این جریان ورودی." + }, + "input_args": { + "label": "آرگومان‌های ورودی", + "description": "‫آرگومان‌های ورودی مختص به این جریان." + } + }, + "label": "FFmpeg کدک", + "retry_interval": { + "label": "زمان تلاش مجدد FFmpeg", + "description": "ثانیه‌هایی برای انتظار قبل از تلاش برای اتصال مجدد جریان دوربین پس از خرابی. مقدار پیش‌فرض ۱۰ است." + } + }, + "live": { + "label": "پخش زنده", + "description": "تنظیماتی که توسط رابط کاربری وب برای کنترل انتخاب پخش زنده، وضوح و کیفیت استفاده می‌شود.", + "streams": { + "label": "نام‌های پخش زنده", + "description": "نگاشت نام‌های جریان پیکربندی‌شده به نام‌های restream/go2rtc مورد استفاده برای پخش زنده." + }, + "height": { + "label": "ارتفاع زنده", + "description": "ارتفاع (پیکسل) برای رندر کردن پخش زنده jsmpeg در رابط کاربری وب؛ باید <= تشخیص ارتفاع جریان باشد." + }, + "quality": { + "label": "کیفیت زنده", + "description": "کیفیت کدگذاری برای جریان jsmpeg (۱ بالاترین، ۳۱ پایین‌ترین)." + } + }, + "lpr": { + "label": "تشخیص پلاک خودرو", + "description": "تنظیمات تشخیص پلاک خودرو شامل آستانه‌های تشخیص، قالب‌بندی و پلاک‌های شناخته‌شده.", + "enabled": { + "label": "فعال کردن LPR", + "description": "فعال یا غیرفعال کردن LPR در این دوربین." + }, + "expire_time": { + "label": "ثانیه‌ها منقضی می‌شوند", + "description": "مدت زمان (بر حسب ثانیه) که پس از آن پلاک دیده نشده از ردیاب حذف می‌شود (فقط برای دوربین‌های اختصاصی پلاکخوان)." + }, + "min_area": { + "label": "حداقل مساحت صفحه", + "description": "حداقل مساحت پلاک (پیکسل) مورد نیاز برای شناسایی." + }, + "enhancement": { + "label": "سطح ارتقاء", + "description": "سطح بهبود (0-10) برای اعمال روی محصولات بشقابی قبل از OCR؛ مقادیر بالاتر ممکن است همیشه نتایج را بهبود ندهند، سطوح بالاتر از 5 ممکن است فقط با بشقاب‌های شبانه کار کنند و باید با احتیاط استفاده شوند." + } + }, + "motion": { + "label": "تشخیص حرکت", + "description": "تنظیمات پیش‌فرض تشخیص حرکت برای این دوربین.", + "enabled": { + "label": "فعال کردن تشخیص حرکت", + "description": "تشخیص حرکت را برای این دوربین فعال یا غیرفعال کنید." + }, + "threshold": { + "label": "آستانه حرکت", + "description": "آستانه اختلاف پیکسل مورد استفاده توسط آشکارساز حرکت؛ مقادیر بالاتر حساسیت را کاهش می‌دهند (محدوده ۱-۲۵۵)." + }, + "lightning_threshold": { + "label": "آستانه رعد و برق", + "description": "آستانه‌ای برای تشخیص و نادیده گرفتن نوسانات کوتاه مدت نور (مقادیر کمتر، حساسیت بیشتر، بین ۰.۳ تا ۱.۰). این امر به طور کامل از تشخیص حرکت جلوگیری نمی‌کند؛ بلکه صرفاً باعث می‌شود که آشکارساز پس از عبور از آستانه، تجزیه و تحلیل فریم‌های اضافی را متوقف کند. ضبط‌های مبتنی بر حرکت همچنان در طول این رویدادها ایجاد می‌شوند." + }, + "skip_motion_threshold": { + "label": "رد شدن از آستانه حرکت", + "description": "اگر بیش از این بخش از تصویر در یک فریم تغییر کند، آشکارساز هیچ کادر حرکتی را برنمی‌گرداند و بلافاصله دوباره کالیبره می‌شود. این می‌تواند در مصرف CPU صرفه‌جویی کند و تشخیص‌های کاذب را در هنگام رعد و برق، طوفان و غیره کاهش دهد، اما ممکن است رویدادهای واقعی مانند ردیابی خودکار یک شیء توسط دوربین PTZ را از دست بدهد. انتخاب بین حذف چند مگابایت از فایل‌های ضبط شده در مقابل بررسی چند کلیپ کوتاه است. محدوده 0.0 تا 1.0." + }, + "improve_contrast": { + "label": "بهبود کنتراست", + "description": "قبل از تحلیل حرکت، بهبود کنتراست را روی فریم‌ها اعمال کنید تا به تشخیص کمک کند." + }, + "contour_area": { + "label": "ناحیه کانتور", + "description": "حداقل مساحت کانتور بر حسب پیکسل که برای شمارش یک کانتور حرکت لازم است." + }, + "delta_alpha": { + "label": "دلتا آلفا", + "description": "ضریب ترکیب آلفا که در تفاضل فریم برای محاسبه حرکت استفاده می‌شود." + }, + "frame_alpha": { + "label": "قاب آلفا", + "description": "مقدار آلفا هنگام ترکیب فریم‌ها برای پیش‌پردازش حرکت استفاده می‌شود." + }, + "frame_height": { + "label": "ارتفاع قاب", + "description": "ارتفاع بر حسب پیکسل برای مقیاس‌بندی فریم‌ها هنگام محاسبه حرکت." + }, + "mask": { + "label": "مختصات ماسک", + "description": "مختصات x و y مرتب شده که چندضلعی ماسک حرکت را که برای شامل/خارج کردن نواحی استفاده می‌شود، تعریف می‌کنند." + }, + "mqtt_off_delay": { + "label": "تأخیر خاموشی MQTT", + "description": "ثانیه‌هایی برای انتظار پس از آخرین حرکت، قبل از انتشار وضعیت «خاموش» MQTT." + }, + "enabled_in_config": { + "label": "حالت حرکت اصلی", + "description": "نشان می‌دهد که آیا تشخیص حرکت در پیکربندی استاتیک اصلی فعال بوده است یا خیر." + }, + "raw_mask": { + "label": "ماسک خام" + } + }, + "objects": { + "label": "اشیاء", + "description": "پیش‌فرض‌های ردیابی اشیا شامل برچسب‌هایی که باید ردیابی شوند و فیلترهای مربوط به هر شیء.", + "track": { + "label": "اشیاء برای ردیابی", + "description": "فهرست برچسب‌های اشیاء برای ردیابی توسط این دوربین." + }, + "filters": { + "label": "فیلترهای شیء", + "description": "فیلترهایی که برای کاهش تشخیص‌های مثبت کاذب (مساحت، نسبت، اطمینان) روی اشیاء شناسایی‌شده اعمال می‌شوند.", + "min_area": { + "label": "حداقل مساحت شیء", + "description": "حداقل مساحت کادر مرزی (پیکسل یا درصد) مورد نیاز برای این نوع شیء. می‌تواند پیکسل (عدد صحیح) یا درصد (اعداد شناور بین 0.000001 و 0.99) باشد ترجمه ابی." + }, + "max_area": { + "label": "حداکثر مساحت جسم", + "description": "حداکثر مساحت کادر محصورکننده (پیکسل یا درصد) مجاز برای این نوع شیء. می‌تواند پیکسل (عدد صحیح) یا درصد (اعداد شناور بین 0.000001 و 0.99) باشد." + }, + "min_ratio": { + "label": "حداقل نسبت ابعاد", + "description": "حداقل نسبت عرض/ارتفاع مورد نیاز برای واجد شرایط بودن کادر محصورکننده." + }, + "max_ratio": { + "label": "حداکثر نسبت ابعاد", + "description": "حداکثر نسبت عرض/ارتفاع مجاز برای واجد شرایط بودن کادر محصورکننده." + }, + "threshold": { + "label": "آستانه اطمینان", + "description": "میانگین آستانه اطمینان تشخیص مورد نیاز برای اینکه شیء مثبت واقعی در نظر گرفته شود." + }, + "min_score": { + "label": "حداقل اعتماد به نفس", + "description": "حداقل ضریب اطمینان تشخیص تک فریم مورد نیاز برای شمارش شیء." + }, + "mask": { + "label": "ماسک فیلتردار", + "description": "مختصات چندضلعی که مشخص می‌کند این فیلتر در کجای فریم اعمال می‌شود." + }, + "raw_mask": { + "label": "ماسک خام" + } + }, + "mask": { + "label": "ماسک شیء", + "description": "چندضلعی ماسک برای جلوگیری از تشخیص اشیاء در نواحی مشخص شده استفاده می‌شود." + }, + "raw_mask": { + "label": "ماسک خام" + }, + "genai": { + "label": "پیکربندی شیء GenAI", + "description": "گزینه‌های GenAI برای توصیف اشیاء ردیابی شده و ارسال فریم‌ها برای تولید.", + "enabled": { + "label": "فعال کردن GenAI", + "description": "به طور پیش‌فرض، تولید توضیحات توسط GenAI را برای اشیاء ردیابی شده فعال کنید." + }, + "use_snapshot": { + "label": "از عکس‌های فوری استفاده کنید", + "description": "برای تولید توضیحات GenAI، به جای تصاویر کوچک از عکس‌های فوری اشیاء استفاده کنید." + }, + "prompt": { + "label": "درخواست زیرنویس", + "description": "الگوی پیش‌فرض اعلان که هنگام تولید توضیحات با GenAI استفاده می‌شود." + }, + "object_prompts": { + "label": "اعلان‌های شیء", + "description": "به ازای هر شیء، می‌توان خروجی‌های GenAI را برای برچسب‌های خاص سفارشی کرد." + }, + "objects": { + "label": "اشیاء GenAI", + "description": "فهرست برچسب‌های شیء که به‌طور پیش‌فرض برای GenAI ارسال می‌شوند." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "مناطقی که باید وارد شوند تا اشیاء واجد شرایط تولید توصیف GenAI شوند." + }, + "debug_save_thumbnails": { + "label": "ذخیره ریز عکس‌ها", + "description": "تصاویر کوچک ارسال شده به GenAI را برای اشکال‌زدایی و بررسی ذخیره کنید." + }, + "send_triggers": { + "label": "محرک‌های GenAI", + "description": "مشخص می‌کند که چه زمانی فریم‌ها باید به GenAI ارسال شوند (در پایان، پس از به‌روزرسانی‌ها و غیره).", + "tracked_object_end": { + "label": "ارسال در انتها", + "description": "وقتی شیء ردیابی شده به پایان رسید، درخواستی به GenAI ارسال کنید." + }, + "after_significant_updates": { + "label": "محرک اولیه GenAI", + "description": "پس از تعداد مشخصی از به‌روزرسانی‌های مهم برای شیء ردیابی‌شده، درخواستی را به GenAI ارسال کنید." + } + }, + "enabled_in_config": { + "label": "حالت اصلی GenAI", + "description": "نشان می‌دهد که آیا GenAI در پیکربندی استاتیک اصلی فعال بوده است یا خیر." + } + } + }, + "record": { + "label": "ضبط", + "description": "تنظیمات ضبط و ذخیره‌سازی برای این دوربین.", + "enabled": { + "label": "فعال کردن ضبط", + "description": "ضبط را برای این دوربین فعال یا غیرفعال کنید." + }, + "expire_interval": { + "label": "فاصله پاکسازی رکورد", + "description": "دقایق بین مراحل پاکسازی که بخش‌های ضبط‌شده‌ی منقضی‌شده را حذف می‌کنند." + }, + "continuous": { + "label": "نگهداری مداوم", + "description": "تعداد روزهایی که صرف نظر از اشیاء ردیابی شده یا حرکت، ضبط‌ها نگهداری می‌شوند. اگر فقط می‌خواهید ضبط‌های هشدارها و تشخیص‌ها را نگهداری کنید، روی ۰ تنظیم کنید.", + "days": { + "label": "روزهای نگهداری", + "description": "روزهایی که باید فایل‌های ضبط‌شده را نگه دارید." + } + }, + "motion": { + "label": "حفظ حرکت", + "description": "تعداد روزهایی که صرف نظر از اشیاء ردیابی شده، ضبط‌های ناشی از حرکت حفظ می‌شوند. اگر می‌خواهید فقط ضبط‌های هشدارها و تشخیص‌ها حفظ شوند، روی ۰ تنظیم کنید.", + "days": { + "label": "روزهای نگهداری", + "description": "روزهایی که باید فایل‌های ضبط‌شده را نگه دارید." + } + }, + "detections": { + "label": "حفظ تشخیص", + "description": "تنظیمات نگهداری ضبط برای رویدادهای تشخیص شامل مدت زمان ضبط قبل/بعد.", + "pre_capture": { + "label": "ثانیه‌های پیش از ثبت", + "description": "تعداد ثانیه‌ها قبل از رویداد تشخیص که باید در ضبط لحاظ شود." + }, + "post_capture": { + "label": "ثانیه‌های پس از ثبت", + "description": "تعداد ثانیه‌ها پس از رویداد تشخیص که باید در ضبط لحاظ شود." + }, + "retain": { + "label": "نگهداری رویداد", + "description": "تنظیمات نگهداری برای ضبط رویدادهای تشخیص.", + "days": { + "label": "روزهای نگهداری", + "description": "تعداد روزهایی که لازم است سوابق رویدادهای شناسایی‌شده نگهداری شوند." + }, + "mode": { + "label": "حالت نگهداری", + "description": "حالت نگهداری: همه (ذخیره همه بخش‌ها)، حرکت (ذخیره بخش‌های دارای حرکت) یا active_objects (ذخیره بخش‌های دارای اشیاء فعال)." + } + } + }, + "alerts": { + "label": "حفظ هشدار", + "description": "تنظیمات نگهداری ضبط برای رویدادهای هشدار شامل مدت زمان ضبط قبل/بعد از ضبط.", + "pre_capture": { + "label": "ثانیه‌های پیش از ثبت", + "description": "تعداد ثانیه‌ها قبل از رویداد تشخیص که باید در ضبط لحاظ شود." + }, + "post_capture": { + "label": "ثانیه‌های پس از ثبت", + "description": "تعداد ثانیه‌ها پس از رویداد تشخیص که باید در ضبط لحاظ شود." + }, + "retain": { + "label": "نگهداری رویداد", + "description": "تنظیمات نگهداری برای ضبط رویدادهای تشخیص.", + "days": { + "label": "روزهای نگهداری", + "description": "تعداد روزهایی که لازم است سوابق رویدادهای شناسایی‌شده نگهداری EMSebi شوند ." + }, + "mode": { + "label": "حالت نگهداری", + "description": "حالت نگهداری: همه (ذخیره همه بخش‌ها)، حرکت (ذخیره بخش‌های دارای حرکت) یا active_objects (ذخیره بخش‌های دارای اشیاء فعال)." + } + } + }, + "export": { + "label": "پیکربندی خروجی", + "description": "تنظیماتی که هنگام خروجی گرفتن از ویدیوهای ضبط شده مانند تایم‌لپس و شتاب سخت‌افزاری استفاده می‌شوند.", + "hwaccel_args": { + "label": "خروجی گرفتن از آرگومان‌های hwaccel", + "description": "آرگومان‌های شتاب سخت‌افزاری برای استفاده در عملیات صادرات/تبدیل کد." + } + }, + "preview": { + "label": "پیش‌نمایش پیکربندی", + "description": "تنظیماتی که کیفیت پیش‌نمایش‌های ضبط نمایش داده شده در رابط کاربری را کنترل می‌کنند.", + "quality": { + "label": "کیفیت پیش‌نمایش", + "description": "پیش‌نمایش سطح کیفیت (خیلی_پایین، پایین، متوسط، بالا، خیلی_بالا)." + } + }, + "enabled_in_config": { + "label": "وضعیت ضبط اولیه", + "description": "نشان می‌دهد که آیا ضبط در پیکربندی استاتیک اصلی فعال بوده است یا خیر." + } + }, + "review": { + "label": "نقد و بررسی", + "description": "تنظیماتی که هشدارها، تشخیص‌ها و خلاصه‌های بررسی GenAI مورد استفاده توسط رابط کاربری و فضای ذخیره‌سازی این دوربین را کنترل می‌کنند.", + "alerts": { + "label": "پیکربندی هشدارها", + "description": "تنظیماتی که برای اشیاء ردیابی شده هشدار ایجاد می‌کنند و نحوه‌ی حفظ هشدارها.", + "enabled": { + "label": "فعال کردن هشدارها", + "description": "فعال یا غیرفعال کردن تولید هشدار برای این دوربین." + }, + "labels": { + "label": "برچسب‌های هشدار", + "description": "فهرست برچسب‌های اشیاء که به عنوان هشدار واجد شرایط هستند (برای مثال: ماشین، شخص)." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "مناطقی که یک شیء باید وارد آنها شود تا به عنوان هشدار در نظر گرفته شود؛ برای مجاز بودن هر منطقه‌ای، آن را خالی بگذارید." + }, + "enabled_in_config": { + "label": "وضعیت هشدارهای اصلی", + "description": "پیگیری می‌کند که آیا هشدارها در ابتدا در پیکربندی استاتیک فعال بوده‌اند یا خیر." + }, + "cutoff_time": { + "label": "زمان قطع هشدارها", + "description": "ثانیه‌هایی برای انتظار پس از عدم وجود فعالیت منجر به هشدار و سپس قطع هشدار." + } + }, + "detections": { + "label": "پیکربندی تشخیص‌ها", + "description": "تنظیمات ایجاد رویدادهای تشخیص (غیر هشدار) و مدت زمان نگهداری آنها.", + "enabled": { + "label": "فعال کردن تشخیص‌ها", + "description": "فعال یا غیرفعال کردن رویدادهای تشخیص برای این دوربین." + }, + "labels": { + "label": "برچسب‌های تشخیص", + "description": "فهرست برچسب‌های شیء که به عنوان رویدادهای تشخیص واجد شرایط هستند." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "مناطقی که یک شیء باید وارد آنها شود تا تشخیص داده شود؛ برای مجاز بودن هر منطقه‌ای، خالی بگذارید." + }, + "cutoff_time": { + "label": "زمان قطع تشخیص", + "description": "ثانیه‌هایی برای انتظار پس از عدم مشاهده فعالیت منجر به تشخیص، قبل از قطع تشخیص." + }, + "enabled_in_config": { + "label": "وضعیت تشخیص‌های اولیه", + "description": "پیگیری می‌کند که آیا تشخیص‌ها در ابتدا در پیکربندی استاتیک فعال بوده‌اند یا خیر." + } + }, + "genai": { + "label": "پیکربندی GenAI", + "description": "استفاده از هوش مصنوعی مولد را برای تولید توضیحات و خلاصه موارد بررسی کنترل می‌کند.", + "enabled": { + "label": "فعال کردن توضیحات GenAI", + "description": "فعال یا غیرفعال کردن توضیحات و خلاصه‌های تولید شده توسط GenAI برای موارد بررسی." + }, + "alerts": { + "label": "فعال کردن GenAI برای هشدارها", + "description": "از GenAI برای تولید توضیحات برای موارد هشدار استفاده کنید." + }, + "detections": { + "label": "فعال کردن GenAI برای تشخیص‌ها", + "description": "از GenAI برای تولید توضیحات برای موارد تشخیص استفاده کنید." + }, + "image_source": { + "label": "منبع تصویر را بررسی کنید", + "description": "منبع تصاویر ارسال شده به GenAI («پیش‌نمایش» یا «ضبط‌ها»)؛ «ضبط‌ها» از فریم‌های با کیفیت بالاتر اما توکن‌های بیشتری استفاده می‌کنند." + }, + "additional_concerns": { + "label": "نگرانی‌های اضافی", + "description": "فهرستی از نگرانی‌ها یا نکات اضافی که GenAI باید هنگام ارزیابی فعالیت روی این دوربین در نظر بگیرد." + }, + "debug_save_thumbnails": { + "label": "ذخیره ریز عکس‌ها", + "description": "تصاویر کوچکی را که برای اشکال‌زدایی و بررسی به ارائه‌دهنده GenAI ارسال می‌شوند، ذخیره کنید." + }, + "enabled_in_config": { + "label": "حالت اصلی GenAI", + "description": "پیگیری می‌کند که آیا بررسی GenAI در ابتدا در پیکربندی استاتیک فعال بوده است یا خیر." + }, + "preferred_language": { + "label": "زبان ترجیحی", + "description": "زبان ترجیحی برای درخواست از ارائه‌دهنده GenAI برای پاسخ‌های تولید شده." + }, + "activity_context_prompt": { + "label": "اعلان زمینه فعالیت", + "description": "دستورالعمل سفارشی که فعالیت‌های مشکوک و غیرمشکوک را توصیف می‌کند تا زمینه‌ای برای خلاصه‌های GenAI فراهم کند." + } + } + }, + "semantic_search": { + "label": "جستجوی معنایی", + "description": "تنظیماتی برای جستجوی معنایی که جاسازی‌های شیء را می‌سازد و برای یافتن موارد مشابه، جستجو می‌کند.", + "triggers": { + "label": "محرک‌ها", + "description": "اقدامات و معیارهای تطبیق برای محرک‌های جستجوی معنایی خاص دوربین.", + "friendly_name": { + "label": "نام دوستانه", + "description": "نام دلخواه و کاربرپسندی که برای این تریگر در رابط کاربری نمایش داده می‌شود." + }, + "enabled": { + "label": "این تریگر را فعال کنید", + "description": "این محرک جستجوی معنایی را فعال یا غیرفعال کنید." + }, + "type": { + "label": "نوع ماشه", + "description": "نوع تریگر: «تصویر کوچک» (مطابقت با تصویر) یا «توضیحات» (مطابقت با متن)." + }, + "data": { + "label": "محتوای محرک", + "description": "عبارت متنی یا شناسه تصویر کوچک برای مطابقت با اشیاء ردیابی شده." + }, + "threshold": { + "label": "آستانه ماشه", + "description": "حداقل امتیاز شباهت (0-1) برای فعال کردن این تریگر مورد نیاز است." + }, + "actions": { + "label": "اقدامات محرک", + "description": "فهرست اقداماتی که باید هنگام تطبیق trigger اجرا شوند (اعلان، زیربرچسب، ویژگی)." + } + } + }, + "snapshots": { + "label": "عکس‌های فوری", + "description": "تنظیمات مربوط به عکس‌های JPEG ذخیره شده از اشیاء ردیابی شده برای این دوربین.", + "enabled": { + "label": "اسنپ‌شات‌ها فعال شدند", + "description": "ذخیره عکس‌های فوری برای این دوربین را فعال یا غیرفعال کنید." + }, + "clean_copy": { + "label": "ذخیره نسخه پاک", + "description": "علاوه بر عکس‌های فوری دارای حاشیه‌نویسی، یک کپی تمیز بدون حاشیه‌نویسی از عکس‌های فوری ذخیره کنید." + }, + "timestamp": { + "label": "روکش مهر زمانی", + "description": "یک مهر زمانی روی عکس‌های ذخیره شده قرار دهید." + }, + "bounding_box": { + "label": "پوشش جعبه مرزی", + "description": "برای اشیاء ردیابی شده روی عکس‌های فوری ذخیره شده، کادرهای مرزی رسم کنید." + }, + "crop": { + "label": "برش عکس فوری", + "description": "عکس‌های ذخیره‌شده را در کادر محدوده شیء شناسایی‌شده برش دهید." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "مناطقی که یک شیء باید برای ذخیره شدن یک snapshot وارد آنها شود." + }, + "height": { + "label": "ارتفاع عکس فوری", + "description": "ارتفاع (پیکسل) برای تغییر اندازه عکس‌های ذخیره شده؛ برای حفظ اندازه اصلی، آن را خالی بگذارید." + }, + "retain": { + "label": "نگهداری اسنپ‌شات", + "description": "تنظیمات نگهداری برای اسنپ‌شات‌های ذخیره‌شده شامل روزهای پیش‌فرض و لغو هر شیء.", + "default": { + "label": "نگهداری پیش‌فرض", + "description": "تعداد روزهای پیش‌فرض برای نگهداری اسنپ‌شات‌ها." + }, + "mode": { + "label": "حالت نگهداری", + "description": "حالت نگهداری: همه (ذخیره همه بخش‌ها)، حرکت (ذخیره بخش‌های دارای حرکت) یا active_objects (ذخیره بخش‌های دارای اشیاء فعال)." + }, + "objects": { + "label": "نگهداری شیء", + "description": "برای هر شیء، تعداد روزهای نگهداری اسنپ‌شات را لغو می‌کند." + } + }, + "quality": { + "label": "کیفیت JPEG", + "description": "کیفیت کدگذاری JPEG برای عکس‌های ذخیره شده (0-100)." + } + }, + "timestamp_style": { + "label": "سبک مهر زمانی", + "description": "گزینه‌های استایل‌دهی برای مهرهای زمانی درون فید که برای ضبط‌ها و اسنپ‌شات‌ها اعمال می‌شوند.", + "position": { + "label": "موقعیت مهر زمانی", + "description": "موقعیت برچسب زمانی روی تصویر (tl/tr/bl/br)." + }, + "format": { + "label": "قالب مهر زمانی", + "description": "رشته‌ی قالب تاریخ و زمان که برای مهرهای زمانی استفاده می‌شود (کدهای قالب تاریخ و زمان پایتون)." + }, + "color": { + "label": "رنگ مهر زمانی", + "description": "مقادیر رنگ RGB برای متن مهر زمان (همه مقادیر ۰-۲۵۵).", + "red": { + "label": "قرمز", + "description": "جزء قرمز (۰-۲۵۵) برای رنگ مهر زمانی." + }, + "green": { + "label": "سبز", + "description": "جزء سبز (۰-۲۵۵) برای رنگ مهر زمانی." + }, + "blue": { + "label": "آبی", + "description": "جزء آبی (۰-۲۵۵) برای رنگ مهر زمانی." + } + }, + "thickness": { + "label": "ضخامت برچسب زمانی", + "description": "ضخامت خط متن برچسب زمانی." + }, + "effect": { + "label": "اثر مهر زمانی", + "description": "جلوه بصری برای متن مهر زمانی (هیچ، پر، سایه)." + } + }, + "best_image_timeout": { + "label": "بهترین زمان انقضای تصویر", + "description": "چقدر باید منتظر تصویری با بالاترین امتیاز اطمینان ماند." + }, + "mqtt": { + "description": "تنظیمات انتشار تصویر MQTT .", + "enabled": { + "label": "ارسال تصویر", + "description": "انتشار عکس‌های فوری از اشیاء در مباحث MQTT برای این دوربین را فعال کنید." + }, + "timestamp": { + "label": "اضافه کردن برچسب زمانی", + "description": "یک مهر زمانی روی تصاویر منتشر شده در MQTT قرار دهید." + }, + "bounding_box": { + "label": "کادر محدوده را اضافه کنید", + "description": "روی تصاویر منتشر شده از طریق MQTT، کادرهای مرزی رسم کنید." + }, + "crop": { + "label": "برش تصویر", + "description": "تصاویر منتشر شده در MQTT را بر اساس کادر محدودکننده شیء شناسایی شده برش دهید." + }, + "height": { + "label": "ارتفاع تصویر", + "description": "ارتفاع (پیکسل) برای تغییر اندازه تصاویر منتشر شده در MQTT." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "مناطقی که یک شیء باید برای انتشار تصویر MQTT وارد آنها شود." + }, + "quality": { + "label": "کیفیت JPEG", + "description": "کیفیت JPEG برای تصاویر منتشر شده در MQTT (0-100)." + }, + "label": "MQTT یک پروتکل تبادل پیام سبک ." + }, + "notifications": { + "label": "اعلان‌ها", + "description": "تنظیمات برای فعال کردن و کنترل اعلان‌های این دوربین.", + "enabled": { + "label": "فعال کردن اعلان‌ها", + "description": "اعلان‌ها را برای این دوربین فعال یا غیرفعال کنید." + }, + "email": { + "label": "ایمیل اعلان", + "description": "آدرس ایمیلی که برای اعلان‌های فوری استفاده می‌شود یا توسط برخی از ارائه‌دهندگان اعلان مورد نیاز است." + }, + "cooldown": { + "label": "دوره استراحت (کول داون)", + "description": "بین اعلان‌ها (ثانیه) زمان برای خنک شدن در نظر بگیرید تا از ارسال هرزنامه به گیرندگان جلوگیری شود." + }, + "enabled_in_config": { + "label": "وضعیت اعلان‌های اصلی", + "description": "نشان می‌دهد که آیا اعلان‌ها در پیکربندی استاتیک اصلی فعال بوده‌اند یا خیر." + } + }, + "onvif": { + "description": "تنظیمات اتصال ONVIF و ردیابی خودکار PTZ برای این دوربین.", + "host": { + "label": "میزبان ONVIF", + "description": "میزبان (و طرح اختیاری) برای سرویس ONVIF برای این دوربین." + }, + "port": { + "label": "پورت ONVIF", + "description": "شماره پورت برای سرویس ONVIF." + }, + "user": { + "label": "نام کاربری ONVIF", + "description": "نام کاربری برای احراز هویت ONVIF؛ برخی از دستگاه‌ها برای ONVIF به کاربر ادمین نیاز دارند." + }, + "password": { + "label": "رمز عبور ONVIF", + "description": "رمز عبور برای احراز هویت ONVIF." + }, + "tls_insecure": { + "label": "غیرفعال کردن تأیید TLS", + "description": "از تأیید TLS صرف‌نظر کنید و مجوز خلاصه را برای ONVIF غیرفعال کنید (ناامن؛ فقط در شبکه‌های امن استفاده شود)." + }, + "autotracking": { + "label": "ردیابی خودکار", + "description": "با استفاده از حرکات دوربین PTZ، اشیاء متحرک را به طور خودکار ردیابی کرده و آنها را در مرکز قاب نگه دارید.", + "enabled": { + "label": "فعال کردن ردیابی خودکار", + "description": "فعال یا غیرفعال کردن ردیابی خودکار دوربین PTZ از اشیاء شناسایی شده." + }, + "calibrate_on_startup": { + "label": "کالیبره کردن در شروع", + "description": "سرعت موتورهای PTZ را در هنگام راه‌اندازی اندازه‌گیری کنید تا دقت ردیابی بهبود یابد. فریگیت پس از کالیبراسیون، پیکربندی را با movement_weights به‌روزرسانی می‌کند." + }, + "zooming": { + "label": "حالت بزرگنمایی", + "description": "کنترل رفتار زوم: غیرفعال (فقط حرکت افقی/عمودی)، مطلق (سازگارترین) یا نسبی (حرکت افقی/عمودی/بزرگنمایی همزمان)." + }, + "zoom_factor": { + "label": "ضریب بزرگنمایی", + "description": "سطح زوم را روی اشیاء ردیابی شده کنترل کنید. مقادیر پایین‌تر، صحنه بیشتری را در دید نگه می‌دارند؛ مقادیر بالاتر، نزدیک‌تر زوم می‌کنند اما ممکن است ردیابی را از دست بدهند. مقادیر بین ۰.۱ تا ۰.۷۵." + }, + "track": { + "label": "اشیاء ردیابی شده", + "description": "فهرست انواع اشیایی که باید ردیابی خودکار را فعال کنند." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "اشیاء باید قبل از شروع ردیابی خودکار، وارد یکی از این مناطق شوند." + }, + "return_preset": { + "label": "بازگشت از پیش تعیین شده", + "description": "نام از پیش تعیین‌شده ONVIF که در میان‌افزار دوربین پیکربندی شده است تا پس از پایان ردیابی به آن بازگردد." + }, + "timeout": { + "label": "مهلت بازگشت", + "description": "چند ثانیه صبر کن پس از از دست دادن ردیابی قبل از بازگرداندن دوربین به موقعیت از پیش تعیین شده ." + }, + "movement_weights": { + "label": "وزنه‌های حرکتی", + "description": "مقادیر کالیبراسیون به طور خودکار توسط کالیبراسیون دوربین ایجاد می‌شوند. به صورت دستی تغییر ندهید." + }, + "enabled_in_config": { + "label": "حالت اتوترک اصلی", + "description": "فیلد داخلی برای ردیابی اینکه آیا ردیابی خودکار در پیکربندی فعال شده است یا خیر." + } + }, + "ignore_time_mismatch": { + "label": "عدم تطابق زمانی را نادیده بگیرید", + "description": "برای ارتباط ONVIF، از تفاوت‌های همگام‌سازی زمانی بین دوربین و سرور Frigate صرف نظر کنید." + }, + "label": "ONVIF پروتکل استاندارد انتقال تصویر ." + }, + "type": { + "label": "نوع دوربین", + "description": "نوع دوربین" + }, + "ui": { + "label": "رابط کاربری دوربین", + "description": "ترتیب نمایش و قابلیت مشاهده این دوربین در رابط کاربری. ترتیب نمایش، داشبورد پیش‌فرض را تحت تأثیر قرار می‌دهد. برای کنترل دقیق‌تر، از گروه‌های دوربین استفاده کنید.", + "order": { + "label": "سفارش رابط کاربری", + "description": "ترتیب عددی مورد استفاده برای مرتب‌سازی دوربین در رابط کاربری (داشبورد و لیست‌های پیش‌فرض)؛ اعداد بزرگتر بعداً ظاهر می‌شوند." + }, + "dashboard": { + "label": "نمایش در رابط کاربری", + "description": "مشخص کنید که آیا این دوربین در همه جای رابط کاربری Frigate قابل مشاهده باشد یا خیر. غیرفعال کردن این گزینه مستلزم ویرایش دستی پیکربندی برای مشاهده مجدد این دوربین در رابط کاربری است." + } + }, + "webui_url": { + "label": "آدرس اینترنتی دوربین", + "description": "آدرس اینترنتی برای بازدید مستقیم از دوربین از صفحه سیستم" + }, + "zones": { + "label": "مناطق", + "description": "مناطق به شما امکان می‌دهند ناحیه خاصی از قاب را تعریف کنید تا بتوانید تعیین کنید که آیا یک شیء در یک ناحیه خاص قرار دارد یا خیر.", + "friendly_name": { + "label": "نام منطقه", + "description": "یک نام کاربرپسند برای منطقه، که در رابط کاربری Frigate نمایش داده می‌شود. در صورت عدم تنظیم، از نسخه قالب‌بندی‌شده نام منطقه استفاده خواهد شد." + }, + "enabled": { + "label": "فعال شده", + "description": "فعال یا غیرفعال کردن این منطقه. مناطق غیرفعال در زمان اجرا نادیده گرفته می‌شوند." + }, + "enabled_in_config": { + "label": "وضعیت اولیه منطقه را پیگیری کنید." + }, + "filters": { + "label": "فیلترهای منطقه‌ای", + "description": "فیلترهایی برای اعمال روی اشیاء درون این منطقه. برای کاهش تشخیص‌های مثبت کاذب یا محدود کردن اینکه کدام اشیاء در این منطقه حضور دارند، استفاده می‌شود.", + "min_area": { + "label": "حداقل مساحت شیء", + "description": "حداقل مساحت کادر مرزی (پیکسل یا درصد) مورد نیاز برای این نوع شیء. می‌تواند پیکسل (عدد صحیح) یا درصد (اعداد شناور بین 0.000001 و 0.99) باشد." + }, + "max_area": { + "label": "حداکثر مساحت جسم", + "description": "حداکثر مساحت کادر محصورکننده (پیکسل یا درصد) مجاز برای این نوع شیء. می‌تواند پیکسل (عدد صحیح) یا درصد (اعداد شناور بین 0.000001 و 0.99) باشد." + }, + "min_ratio": { + "label": "حداقل نسبت ابعاد", + "description": "حداقل نسبت عرض/ارتفاع مورد نیاز برای واجد شرایط بودن کادر محصورکننده." + }, + "max_ratio": { + "label": "حداکثر نسبت ابعاد", + "description": "حداکثر نسبت عرض/ارتفاع مجاز برای واجد شرایط بودن کادر محصورکننده." + }, + "threshold": { + "label": "آستانه اطمینان", + "description": "میانگین آستانه اطمینان تشخیص مورد نیاز برای اینکه شیء مثبت واقعی در نظر گرفته شود." + }, + "min_score": { + "label": "حداقل اعتماد به نفس", + "description": "حداقل ضریب اطمینان تشخیص تک فریم مورد نیاز برای شمارش شیء." + }, + "mask": { + "label": "ماسک فیلتردار", + "description": "مختصات چندضلعی که مشخص می‌کند این فیلتر در کجای فریم اعمال می‌شود." + }, + "raw_mask": { + "label": "ماسک خام" + } + }, + "coordinates": { + "label": "مختصات", + "description": "مختصات چندضلعی که ناحیه‌ی منطقه را تعریف می‌کنند. می‌تواند یک رشته‌ی جدا شده با کاما یا لیستی از رشته‌های مختصات باشد. مختصات باید نسبی (0-1) یا مطلق (legacy) باشند." + }, + "distances": { + "label": "فواصل دنیای واقعی", + "description": "فواصل واقعی اختیاری برای هر ضلع چهارضلعی منطقه، که برای محاسبات سرعت یا مسافت استفاده می‌شود. در صورت تنظیم، باید دقیقاً ۴ مقدار داشته باشد." + }, + "inertia": { + "label": "قاب‌های اینرسی", + "description": "تعداد فریم‌های متوالی که یک شیء باید در منطقه شناسایی شود تا وجود آن در نظر گرفته شود. به فیلتر کردن تشخیص‌های گذرا کمک می‌کند." + }, + "loitering_time": { + "label": "ثانیه‌های سرگردان", + "description": "تعداد ثانیه‌هایی که یک شیء باید در منطقه مورد نظر باقی بماند تا به عنوان پرسه‌زنی در نظر گرفته شود. برای غیرفعال کردن تشخیص پرسه‌زنی، روی ۰ تنظیم کنید." + }, + "speed_threshold": { + "label": "حداقل سرعت", + "description": "حداقل سرعت (در واحدهای دنیای واقعی در صورت تنظیم فواصل) مورد نیاز برای اینکه یک شیء در منطقه موجود در نظر گرفته شود. برای فعال‌سازی‌های منطقه مبتنی بر سرعت استفاده می‌شود." + }, + "objects": { + "label": "اشیاء را فعال کنید", + "description": "فهرست انواع اشیاء (از labelmap) که می‌توانند این منطقه را فعال کنند. می‌تواند یک رشته یا فهرستی از رشته‌ها باشد. اگر خالی باشد، همه اشیاء در نظر گرفته می‌شوند." + } + }, + "enabled_in_config": { + "label": "وضعیت دوربین اصلی", + "description": "وضعیت اولیه دوربین را پیگیری کنید." + } +} diff --git a/web/public/locales/fa/config/global.json b/web/public/locales/fa/config/global.json new file mode 100644 index 0000000..ea5ad48 --- /dev/null +++ b/web/public/locales/fa/config/global.json @@ -0,0 +1,802 @@ +{ + "audio": { + "label": "رویدادهای صوتی", + "enabled": { + "label": "فعال کردن تشخیص صدا" + }, + "max_not_heard": { + "label": "پایان مهلت", + "description": "تعداد ثانیه‌هایی که قبل از پایان رویداد صوتی، نوع صدای پیکربندی‌شده بدون آن باقی می‌ماند." + }, + "min_volume": { + "label": "حداقل صدا", + "description": "حداقل آستانه حجم RMS مورد نیاز برای اجرای تشخیص صدا؛ مقادیر پایین‌تر حساسیت را افزایش می‌دهند (مثلاً ۲۰۰ زیاد، ۵۰۰ متوسط، ۱۰۰۰ کم)." + }, + "listen": { + "label": "انواع گوش دادن", + "description": "فهرست انواع رویدادهای صوتی برای تشخیص (به عنوان مثال: پارس کردن، آژیر آتش، جیغ، گفتار، فریاد)." + }, + "filters": { + "label": "فیلترهای صوتی", + "description": "تنظیمات فیلتر بر اساس نوع صدا مانند آستانه‌های اطمینان که برای کاهش تشخیص‌های مثبت کاذب استفاده می‌شوند." + }, + "enabled_in_config": { + "label": "وضعیت صوتی اصلی", + "description": "نشان می‌دهد که آیا تشخیص صدا در ابتدا در فایل پیکربندی استاتیک فعال بوده است یا خیر." + }, + "num_threads": { + "label": "رشته‌های تشخیص", + "description": "تعداد رشته‌های مورد استفاده برای پردازش تشخیص صدا." + } + }, + "audio_transcription": { + "label": "رونویسی صوتی", + "description": "تنظیمات مربوط به رونویسی صوتی زنده و گفتاری که برای رویدادها و زیرنویس‌های زنده استفاده می‌شود.", + "live_enabled": { + "label": "رونویسی زنده", + "description": "فعال کردن پخش زنده رونویسی برای صدا هنگام دریافت آن." + }, + "enabled": { + "label": "فعال کردن رونویسی صوتی" + } + }, + "birdseye": { + "label": "چشم پرندگان", + "description": "تنظیمات نمای ترکیبی چشم پرنده که تصاویر چندین دوربین را در یک طرح واحد ترکیب می‌کند.", + "enabled": { + "label": "فعال کردن چشم پرندگان", + "description": "ویژگی نمای چشم پرندگان را فعال یا غیرفعال کنید." + }, + "mode": { + "label": "حالت ردیابی", + "description": "حالت گنجاندن دوربین‌ها در چشم پرنده: «اشیاء»، «حرکت» یا «پیوسته»." + }, + "order": { + "label": "موقعیت", + "description": "موقعیت عددی که ترتیب قرارگیری دوربین را در طرح چشم پرنده کنترل می‌کند." + } + }, + "detect": { + "label": "تشخیص شیء", + "description": "تنظیمات مربوط به نقش تشخیص/شناسایی که برای اجرای تشخیص شیء و مقداردهی اولیه ردیاب‌ها استفاده می‌شود.", + "enabled": { + "label": "‫تشخیص فعال شد" + }, + "height": { + "label": "تشخیص ارتفاع", + "description": "ارتفاع (پیکسل) فریم‌های مورد استفاده برای تشخیص جریان؛ برای استفاده از وضوح جریان اصلی، خالی بگذارید." + }, + "width": { + "label": "تشخیص عرض", + "description": "عرض (پیکسل) فریم‌های مورد استفاده برای تشخیص جریان؛ برای استفاده از وضوح جریان اصلی، خالی بگذارید." + }, + "fps": { + "label": "تشخیص فریم بر ثانیه - اف پی اس", + "description": "تعداد فریم در ثانیه مورد نظر برای اجرای تشخیص؛ مقادیر پایین‌تر، استفاده از CPU را کاهش می‌دهند (مقدار توصیه شده ۵ است، فقط در صورت ردیابی اشیاء با حرکت بسیار سریع، مقدار بالاتر - حداکثر ۱۰ - تنظیم شود)." + }, + "min_initialized": { + "label": "حداقل فریم‌های مقداردهی اولیه", + "description": "تعداد تشخیص‌های متوالی مورد نیاز قبل از ایجاد یک شیء ردیابی شده. برای کاهش مقداردهی اولیه نادرست، افزایش دهید. مقدار پیش‌فرض، fps تقسیم بر ۲ است." + }, + "max_disappeared": { + "label": "حداکثر فریم‌های ناپدید شده", + "description": "تعداد فریم‌هایی که قبل از اینکه شیء ردیابی شده ناپدید شده تلقی شود، تشخیص داده نمی‌شوند." + }, + "stationary": { + "label": "پیکربندی اشیاء ثابت", + "description": "تنظیماتی برای شناسایی و مدیریت اشیایی که برای مدتی ثابت می‌مانند.", + "interval": { + "label": "بازه ثابت", + "description": "هر چند وقت یکبار (بر حسب فریم) باید بررسی تشخیص برای تأیید یک شیء ثابت انجام شود." + }, + "threshold": { + "label": "آستانه ثابت", + "description": "تعداد فریم‌هایی که بدون تغییر موقعیت لازم هستند تا یک جسم به عنوان ثابت علامت‌گذاری شود." + }, + "max_frames": { + "label": "حداکثر فریم", + "description": "مدت زمانی که اشیاء ثابت قبل از دور انداختن ردیابی می‌شوند را محدود می‌کند.", + "default": { + "label": "حداکثر فریم‌های پیش‌فرض", + "description": "حداکثر فریم‌های پیش‌فرض برای ردیابی یک جسم ثابت قبل از توقف." + }, + "objects": { + "label": "فریم‌های حداکثر شیء", + "description": "برای ردیابی اشیاء ثابت، حداکثر فریم‌ها به ازای هر شیء لغو می‌شوند." + } + }, + "classifier": { + "label": "فعال کردن طبقه‌بندی بصری", + "description": "از یک طبقه‌بندی‌کننده بصری برای تشخیص اشیاء واقعاً ثابت حتی در مواقعی که کادرهای محصورکننده دچار لرزش می‌شوند، استفاده کنید." + } + }, + "annotation_offset": { + "label": "حاشیه‌نویسی افست", + "description": "میلی‌ثانیه برای جابجایی، تشخیص حاشیه‌نویسی‌ها برای ترازبندی بهتر کادرهای محدودکننده‌ی جدول زمانی با ضبط‌ها؛ می‌تواند مثبت یا منفی باشد." + } + }, + "face_recognition": { + "label": "تشخیص چهره", + "enabled": { + "label": "فعال کردن تشخیص چهره" + }, + "min_area": { + "label": "حداقل مساحت صورت", + "description": "حداقل مساحت (پیکسل) از کادر چهره شناسایی شده که برای تلاش برای شناسایی مورد نیاز است." + } + }, + "ffmpeg": { + "description": "تنظیمات FFmpeg شامل مسیر دودویی، آرگومان‌ها، گزینه‌های hwaccel و آرگومان‌های خروجی به ازای هر نقش.", + "path": { + "label": "مسیر FFmpeg", + "description": "مسیر فایل باینری FFmpeg برای استفاده یا نام مستعار نسخه (\"5.0\" یا \"7.0\")." + }, + "global_args": { + "description": "آرگومان‌های سراسری به فرآیندهای FFmpeg ارسال شدند.", + "label": "آرگومان‌های سراسری FFmpeg" + }, + "hwaccel_args": { + "label": "آرگومان‌های شتاب سخت‌افزاری", + "description": "آرگومان‌های شتاب سخت‌افزاری برای FFmpeg. تنظیمات پیش‌فرض مخصوص ارائه‌دهنده توصیه می‌شود." + }, + "input_args": { + "label": "آرگومان‌های ورودی", + "description": "آرگومان‌های ورودی اعمال شده به جریان‌های ورودی FFmpeg." + }, + "output_args": { + "label": "آرگومان‌های خروجی", + "description": "آرگومان‌های خروجی پیش‌فرض که برای نقش‌های مختلف FFmpeg مانند شناسایی و ضبط استفاده می‌شوند.", + "detect": { + "label": "تشخیص آرگومان‌های خروجی", + "description": "آرگومان‌های خروجی پیش‌فرض برای تشخیص جریان‌های نقش." + }, + "record": { + "label": "آرگومان‌های خروجی را ضبط کنید", + "description": "آرگومان‌های خروجی پیش‌فرض برای جریان‌های نقش رکورد." + } + }, + "apple_compatibility": { + "label": "سازگاری با اپل", + "description": "برای سازگاری بهتر با پخش‌کننده‌های اپل هنگام ضبط H.265، تگ‌گذاری HEVC را فعال کنید." + }, + "gpu": { + "label": "شاخص پردازنده گرافیکی", + "description": "در صورت وجود، شاخص GPU پیش‌فرض برای شتاب سخت‌افزاری استفاده می‌شود." + }, + "inputs": { + "label": "ورودی‌های دوربین", + "description": "فهرست تعاریف جریان ورودی (مسیرها و نقش‌ها) برای این دوربین.", + "path": { + "label": "مسیر ورودی", + "description": "آدرس اینترنتی یا مسیر جریان ورودی دوربین." + }, + "roles": { + "label": "نقش‌های ورودی", + "description": "نقش‌های این جریان ورودی." + }, + "global_args": { + "label": "آرگومان‌های سراسری FFmpeg", + "description": "آرگومان‌های سراسری FFmpeg برای این جریان ورودی." + }, + "hwaccel_args": { + "label": "آرگومان‌های شتاب سخت‌افزاری", + "description": "آرگومان‌های شتاب سخت‌افزاری برای این جریان ورودی." + }, + "input_args": { + "label": "آرگومان‌های ورودی", + "description": "‫آرگومان‌های ورودی مختص به این جریان." + } + }, + "label": "FFmpeg کدک", + "retry_interval": { + "label": "زمان تلاش مجدد FFmpeg", + "description": "ثانیه‌هایی برای انتظار قبل از تلاش برای اتصال مجدد جریان دوربین پس از خرابی. مقدار پیش‌فرض ۱۰ است." + } + }, + "live": { + "label": "پخش زنده", + "streams": { + "label": "نام‌های پخش زنده", + "description": "نگاشت نام‌های جریان پیکربندی‌شده به نام‌های restream/go2rtc مورد استفاده برای پخش زنده." + }, + "height": { + "label": "ارتفاع زنده", + "description": "ارتفاع (پیکسل) برای رندر کردن پخش زنده jsmpeg در رابط کاربری وب؛ باید <= تشخیص ارتفاع جریان باشد." + }, + "quality": { + "label": "کیفیت زنده", + "description": "کیفیت کدگذاری برای جریان jsmpeg (۱ بالاترین، ۳۱ پایین‌ترین)." + } + }, + "lpr": { + "label": "تشخیص پلاک خودرو", + "description": "تنظیمات تشخیص پلاک خودرو شامل آستانه‌های تشخیص، قالب‌بندی و پلاک‌های شناخته‌شده.", + "enabled": { + "label": "فعال کردن LPR" + }, + "expire_time": { + "label": "ثانیه‌ها منقضی می‌شوند", + "description": "مدت زمان (بر حسب ثانیه) که پس از آن پلاک دیده نشده از ردیاب حذف می‌شود (فقط برای دوربین‌های اختصاصی پلاکخوان)." + }, + "min_area": { + "label": "حداقل مساحت صفحه", + "description": "حداقل مساحت پلاک (پیکسل) مورد نیاز برای شناسایی." + }, + "enhancement": { + "label": "سطح ارتقاء", + "description": "سطح بهبود (0-10) برای اعمال روی محصولات بشقابی قبل از OCR؛ مقادیر بالاتر ممکن است همیشه نتایج را بهبود ندهند، سطوح بالاتر از 5 ممکن است فقط با بشقاب‌های شبانه کار کنند و باید با احتیاط استفاده شوند." + } + }, + "motion": { + "label": "تشخیص حرکت", + "enabled": { + "label": "فعال کردن تشخیص حرکت" + }, + "threshold": { + "label": "آستانه حرکت", + "description": "آستانه اختلاف پیکسل مورد استفاده توسط آشکارساز حرکت؛ مقادیر بالاتر حساسیت را کاهش می‌دهند (محدوده ۱-۲۵۵)." + }, + "lightning_threshold": { + "label": "آستانه رعد و برق", + "description": "آستانه‌ای برای تشخیص و نادیده گرفتن نوسانات کوتاه مدت نور (مقادیر کمتر، حساسیت بیشتر، بین ۰.۳ تا ۱.۰). این امر به طور کامل از تشخیص حرکت جلوگیری نمی‌کند؛ بلکه صرفاً باعث می‌شود که آشکارساز پس از عبور از آستانه، تجزیه و تحلیل فریم‌های اضافی را متوقف کند. ضبط‌های مبتنی بر حرکت همچنان در طول این رویدادها ایجاد می‌شوند." + }, + "skip_motion_threshold": { + "label": "رد شدن از آستانه حرکت", + "description": "اگر بیش از این بخش از تصویر در یک فریم تغییر کند، آشکارساز هیچ کادر حرکتی را برنمی‌گرداند و بلافاصله دوباره کالیبره می‌شود. این می‌تواند در مصرف CPU صرفه‌جویی کند و تشخیص‌های کاذب را در هنگام رعد و برق، طوفان و غیره کاهش دهد، اما ممکن است رویدادهای واقعی مانند ردیابی خودکار یک شیء توسط دوربین PTZ را از دست بدهد. انتخاب بین حذف چند مگابایت از فایل‌های ضبط شده در مقابل بررسی چند کلیپ کوتاه است. محدوده 0.0 تا 1.0." + }, + "improve_contrast": { + "label": "بهبود کنتراست", + "description": "قبل از تحلیل حرکت، بهبود کنتراست را روی فریم‌ها اعمال کنید تا به تشخیص کمک کند." + }, + "contour_area": { + "label": "ناحیه کانتور", + "description": "حداقل مساحت کانتور بر حسب پیکسل که برای شمارش یک کانتور حرکت لازم است." + }, + "delta_alpha": { + "label": "دلتا آلفا", + "description": "ضریب ترکیب آلفا که در تفاضل فریم برای محاسبه حرکت استفاده می‌شود." + }, + "frame_alpha": { + "label": "قاب آلفا", + "description": "مقدار آلفا هنگام ترکیب فریم‌ها برای پیش‌پردازش حرکت استفاده می‌شود." + }, + "frame_height": { + "label": "ارتفاع قاب", + "description": "ارتفاع بر حسب پیکسل برای مقیاس‌بندی فریم‌ها هنگام محاسبه حرکت." + }, + "mask": { + "label": "مختصات ماسک", + "description": "مختصات x و y مرتب شده که چندضلعی ماسک حرکت را که برای شامل/خارج کردن نواحی استفاده می‌شود، تعریف می‌کنند." + }, + "mqtt_off_delay": { + "label": "تأخیر خاموشی MQTT", + "description": "ثانیه‌هایی برای انتظار پس از آخرین حرکت، قبل از انتشار وضعیت «خاموش» MQTT." + }, + "enabled_in_config": { + "label": "حالت حرکت اصلی", + "description": "نشان می‌دهد که آیا تشخیص حرکت در پیکربندی استاتیک اصلی فعال بوده است یا خیر." + }, + "raw_mask": { + "label": "ماسک خام" + } + }, + "objects": { + "label": "اشیاء", + "description": "پیش‌فرض‌های ردیابی اشیا شامل برچسب‌هایی که باید ردیابی شوند و فیلترهای مربوط به هر شیء.", + "track": { + "label": "اشیاء برای ردیابی" + }, + "filters": { + "label": "فیلترهای شیء", + "description": "فیلترهایی که برای کاهش تشخیص‌های مثبت کاذب (مساحت، نسبت، اطمینان) روی اشیاء شناسایی‌شده اعمال می‌شوند.", + "min_area": { + "label": "حداقل مساحت شیء", + "description": "حداقل مساحت کادر مرزی (پیکسل یا درصد) مورد نیاز برای این نوع شیء. می‌تواند پیکسل (عدد صحیح) یا درصد (اعداد شناور بین 0.000001 و 0.99) باشد ترجمه ابی." + }, + "max_area": { + "label": "حداکثر مساحت جسم", + "description": "حداکثر مساحت کادر محصورکننده (پیکسل یا درصد) مجاز برای این نوع شیء. می‌تواند پیکسل (عدد صحیح) یا درصد (اعداد شناور بین 0.000001 و 0.99) باشد." + }, + "min_ratio": { + "label": "حداقل نسبت ابعاد", + "description": "حداقل نسبت عرض/ارتفاع مورد نیاز برای واجد شرایط بودن کادر محصورکننده." + }, + "max_ratio": { + "label": "حداکثر نسبت ابعاد", + "description": "حداکثر نسبت عرض/ارتفاع مجاز برای واجد شرایط بودن کادر محصورکننده." + }, + "threshold": { + "label": "آستانه اطمینان", + "description": "میانگین آستانه اطمینان تشخیص مورد نیاز برای اینکه شیء مثبت واقعی در نظر گرفته شود." + }, + "min_score": { + "label": "حداقل اعتماد به نفس", + "description": "حداقل ضریب اطمینان تشخیص تک فریم مورد نیاز برای شمارش شیء." + }, + "mask": { + "label": "ماسک فیلتردار", + "description": "مختصات چندضلعی که مشخص می‌کند این فیلتر در کجای فریم اعمال می‌شود." + }, + "raw_mask": { + "label": "ماسک خام" + } + }, + "mask": { + "label": "ماسک شیء", + "description": "چندضلعی ماسک برای جلوگیری از تشخیص اشیاء در نواحی مشخص شده استفاده می‌شود." + }, + "raw_mask": { + "label": "ماسک خام" + }, + "genai": { + "label": "پیکربندی شیء GenAI", + "description": "گزینه‌های GenAI برای توصیف اشیاء ردیابی شده و ارسال فریم‌ها برای تولید.", + "enabled": { + "label": "فعال کردن GenAI", + "description": "به طور پیش‌فرض، تولید توضیحات توسط GenAI را برای اشیاء ردیابی شده فعال کنید." + }, + "use_snapshot": { + "label": "از عکس‌های فوری استفاده کنید", + "description": "برای تولید توضیحات GenAI، به جای تصاویر کوچک از عکس‌های فوری اشیاء استفاده کنید." + }, + "prompt": { + "label": "درخواست زیرنویس", + "description": "الگوی پیش‌فرض اعلان که هنگام تولید توضیحات با GenAI استفاده می‌شود." + }, + "object_prompts": { + "label": "اعلان‌های شیء", + "description": "به ازای هر شیء، می‌توان خروجی‌های GenAI را برای برچسب‌های خاص سفارشی کرد." + }, + "objects": { + "label": "اشیاء GenAI", + "description": "فهرست برچسب‌های شیء که به‌طور پیش‌فرض برای GenAI ارسال می‌شوند." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "مناطقی که باید وارد شوند تا اشیاء واجد شرایط تولید توصیف GenAI شوند." + }, + "debug_save_thumbnails": { + "label": "ذخیره ریز عکس‌ها", + "description": "تصاویر کوچک ارسال شده به GenAI را برای اشکال‌زدایی و بررسی ذخیره کنید." + }, + "send_triggers": { + "label": "محرک‌های GenAI", + "description": "مشخص می‌کند که چه زمانی فریم‌ها باید به GenAI ارسال شوند (در پایان، پس از به‌روزرسانی‌ها و غیره).", + "tracked_object_end": { + "label": "ارسال در انتها", + "description": "وقتی شیء ردیابی شده به پایان رسید، درخواستی به GenAI ارسال کنید." + }, + "after_significant_updates": { + "label": "محرک اولیه GenAI", + "description": "پس از تعداد مشخصی از به‌روزرسانی‌های مهم برای شیء ردیابی‌شده، درخواستی را به GenAI ارسال کنید." + } + }, + "enabled_in_config": { + "label": "حالت اصلی GenAI", + "description": "نشان می‌دهد که آیا GenAI در پیکربندی استاتیک اصلی فعال بوده است یا خیر." + } + } + }, + "record": { + "label": "ضبط", + "enabled": { + "label": "فعال کردن ضبط" + }, + "expire_interval": { + "label": "فاصله پاکسازی رکورد", + "description": "دقایق بین مراحل پاکسازی که بخش‌های ضبط‌شده‌ی منقضی‌شده را حذف می‌کنند." + }, + "continuous": { + "label": "نگهداری مداوم", + "description": "تعداد روزهایی که صرف نظر از اشیاء ردیابی شده یا حرکت، ضبط‌ها نگهداری می‌شوند. اگر فقط می‌خواهید ضبط‌های هشدارها و تشخیص‌ها را نگهداری کنید، روی ۰ تنظیم کنید.", + "days": { + "label": "روزهای نگهداری", + "description": "روزهایی که باید فایل‌های ضبط‌شده را نگه دارید." + } + }, + "motion": { + "label": "حفظ حرکت", + "description": "تعداد روزهایی که صرف نظر از اشیاء ردیابی شده، ضبط‌های ناشی از حرکت حفظ می‌شوند. اگر می‌خواهید فقط ضبط‌های هشدارها و تشخیص‌ها حفظ شوند، روی ۰ تنظیم کنید.", + "days": { + "label": "روزهای نگهداری", + "description": "روزهایی که باید فایل‌های ضبط‌شده را نگه دارید." + } + }, + "detections": { + "label": "حفظ تشخیص", + "description": "تنظیمات نگهداری ضبط برای رویدادهای تشخیص شامل مدت زمان ضبط قبل/بعد.", + "pre_capture": { + "label": "ثانیه‌های پیش از ثبت", + "description": "تعداد ثانیه‌ها قبل از رویداد تشخیص که باید در ضبط لحاظ شود." + }, + "post_capture": { + "label": "ثانیه‌های پس از ثبت", + "description": "تعداد ثانیه‌ها پس از رویداد تشخیص که باید در ضبط لحاظ شود." + }, + "retain": { + "label": "نگهداری رویداد", + "description": "تنظیمات نگهداری برای ضبط رویدادهای تشخیص.", + "days": { + "label": "روزهای نگهداری", + "description": "تعداد روزهایی که لازم است سوابق رویدادهای شناسایی‌شده نگهداری شوند." + }, + "mode": { + "label": "حالت نگهداری", + "description": "حالت نگهداری: همه (ذخیره همه بخش‌ها)، حرکت (ذخیره بخش‌های دارای حرکت) یا active_objects (ذخیره بخش‌های دارای اشیاء فعال)." + } + } + }, + "alerts": { + "label": "حفظ هشدار", + "description": "تنظیمات نگهداری ضبط برای رویدادهای هشدار شامل مدت زمان ضبط قبل/بعد از ضبط.", + "pre_capture": { + "label": "ثانیه‌های پیش از ثبت", + "description": "تعداد ثانیه‌ها قبل از رویداد تشخیص که باید در ضبط لحاظ شود." + }, + "post_capture": { + "label": "ثانیه‌های پس از ثبت", + "description": "تعداد ثانیه‌ها پس از رویداد تشخیص که باید در ضبط لحاظ شود." + }, + "retain": { + "label": "نگهداری رویداد", + "description": "تنظیمات نگهداری برای ضبط رویدادهای تشخیص.", + "days": { + "label": "روزهای نگهداری", + "description": "تعداد روزهایی که لازم است سوابق رویدادهای شناسایی‌شده نگهداری EMSebi شوند ." + }, + "mode": { + "label": "حالت نگهداری", + "description": "حالت نگهداری: همه (ذخیره همه بخش‌ها)، حرکت (ذخیره بخش‌های دارای حرکت) یا active_objects (ذخیره بخش‌های دارای اشیاء فعال)." + } + } + }, + "export": { + "label": "پیکربندی خروجی", + "description": "تنظیماتی که هنگام خروجی گرفتن از ویدیوهای ضبط شده مانند تایم‌لپس و شتاب سخت‌افزاری استفاده می‌شوند.", + "hwaccel_args": { + "label": "خروجی گرفتن از آرگومان‌های hwaccel", + "description": "آرگومان‌های شتاب سخت‌افزاری برای استفاده در عملیات صادرات/تبدیل کد." + } + }, + "preview": { + "label": "پیش‌نمایش پیکربندی", + "description": "تنظیماتی که کیفیت پیش‌نمایش‌های ضبط نمایش داده شده در رابط کاربری را کنترل می‌کنند.", + "quality": { + "label": "کیفیت پیش‌نمایش", + "description": "پیش‌نمایش سطح کیفیت (خیلی_پایین، پایین، متوسط، بالا، خیلی_بالا)." + } + }, + "enabled_in_config": { + "label": "وضعیت ضبط اولیه", + "description": "نشان می‌دهد که آیا ضبط در پیکربندی استاتیک اصلی فعال بوده است یا خیر." + } + }, + "review": { + "label": "نقد و بررسی", + "alerts": { + "label": "پیکربندی هشدارها", + "description": "تنظیماتی که برای اشیاء ردیابی شده هشدار ایجاد می‌کنند و نحوه‌ی حفظ هشدارها.", + "enabled": { + "label": "فعال کردن هشدارها" + }, + "labels": { + "label": "برچسب‌های هشدار", + "description": "فهرست برچسب‌های اشیاء که به عنوان هشدار واجد شرایط هستند (برای مثال: ماشین، شخص)." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "مناطقی که یک شیء باید وارد آنها شود تا به عنوان هشدار در نظر گرفته شود؛ برای مجاز بودن هر منطقه‌ای، آن را خالی بگذارید." + }, + "enabled_in_config": { + "label": "وضعیت هشدارهای اصلی", + "description": "پیگیری می‌کند که آیا هشدارها در ابتدا در پیکربندی استاتیک فعال بوده‌اند یا خیر." + }, + "cutoff_time": { + "label": "زمان قطع هشدارها", + "description": "ثانیه‌هایی برای انتظار پس از عدم وجود فعالیت منجر به هشدار و سپس قطع هشدار." + } + }, + "detections": { + "label": "پیکربندی تشخیص‌ها", + "description": "تنظیمات ایجاد رویدادهای تشخیص (غیر هشدار) و مدت زمان نگهداری آنها.", + "enabled": { + "label": "فعال کردن تشخیص‌ها" + }, + "labels": { + "label": "برچسب‌های تشخیص", + "description": "فهرست برچسب‌های شیء که به عنوان رویدادهای تشخیص واجد شرایط هستند." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "مناطقی که یک شیء باید وارد آنها شود تا تشخیص داده شود؛ برای مجاز بودن هر منطقه‌ای، خالی بگذارید." + }, + "cutoff_time": { + "label": "زمان قطع تشخیص", + "description": "ثانیه‌هایی برای انتظار پس از عدم مشاهده فعالیت منجر به تشخیص، قبل از قطع تشخیص." + }, + "enabled_in_config": { + "label": "وضعیت تشخیص‌های اولیه", + "description": "پیگیری می‌کند که آیا تشخیص‌ها در ابتدا در پیکربندی استاتیک فعال بوده‌اند یا خیر." + } + }, + "genai": { + "label": "پیکربندی GenAI", + "description": "استفاده از هوش مصنوعی مولد را برای تولید توضیحات و خلاصه موارد بررسی کنترل می‌کند.", + "enabled": { + "label": "فعال کردن توضیحات GenAI", + "description": "فعال یا غیرفعال کردن توضیحات و خلاصه‌های تولید شده توسط GenAI برای موارد بررسی." + }, + "alerts": { + "label": "فعال کردن GenAI برای هشدارها", + "description": "از GenAI برای تولید توضیحات برای موارد هشدار استفاده کنید." + }, + "detections": { + "label": "فعال کردن GenAI برای تشخیص‌ها", + "description": "از GenAI برای تولید توضیحات برای موارد تشخیص استفاده کنید." + }, + "image_source": { + "label": "منبع تصویر را بررسی کنید", + "description": "منبع تصاویر ارسال شده به GenAI («پیش‌نمایش» یا «ضبط‌ها»)؛ «ضبط‌ها» از فریم‌های با کیفیت بالاتر اما توکن‌های بیشتری استفاده می‌کنند." + }, + "additional_concerns": { + "label": "نگرانی‌های اضافی", + "description": "فهرستی از نگرانی‌ها یا نکات اضافی که GenAI باید هنگام ارزیابی فعالیت روی این دوربین در نظر بگیرد." + }, + "debug_save_thumbnails": { + "label": "ذخیره ریز عکس‌ها", + "description": "تصاویر کوچکی را که برای اشکال‌زدایی و بررسی به ارائه‌دهنده GenAI ارسال می‌شوند، ذخیره کنید." + }, + "enabled_in_config": { + "label": "حالت اصلی GenAI", + "description": "پیگیری می‌کند که آیا بررسی GenAI در ابتدا در پیکربندی استاتیک فعال بوده است یا خیر." + }, + "preferred_language": { + "label": "زبان ترجیحی", + "description": "زبان ترجیحی برای درخواست از ارائه‌دهنده GenAI برای پاسخ‌های تولید شده." + }, + "activity_context_prompt": { + "label": "اعلان زمینه فعالیت", + "description": "دستورالعمل سفارشی که فعالیت‌های مشکوک و غیرمشکوک را توصیف می‌کند تا زمینه‌ای برای خلاصه‌های GenAI فراهم کند." + } + } + }, + "semantic_search": { + "label": "جستجوی معنایی", + "triggers": { + "label": "محرک‌ها", + "description": "اقدامات و معیارهای تطبیق برای محرک‌های جستجوی معنایی خاص دوربین.", + "friendly_name": { + "label": "نام دوستانه", + "description": "نام دلخواه و کاربرپسندی که برای این تریگر در رابط کاربری نمایش داده می‌شود." + }, + "enabled": { + "label": "این تریگر را فعال کنید", + "description": "این محرک جستجوی معنایی را فعال یا غیرفعال کنید." + }, + "type": { + "label": "نوع ماشه", + "description": "نوع تریگر: «تصویر کوچک» (مطابقت با تصویر) یا «توضیحات» (مطابقت با متن)." + }, + "data": { + "label": "محتوای محرک", + "description": "عبارت متنی یا شناسه تصویر کوچک برای مطابقت با اشیاء ردیابی شده." + }, + "threshold": { + "label": "آستانه ماشه", + "description": "حداقل امتیاز شباهت (0-1) برای فعال کردن این تریگر مورد نیاز است." + }, + "actions": { + "label": "اقدامات محرک", + "description": "فهرست اقداماتی که باید هنگام تطبیق trigger اجرا شوند (اعلان، زیربرچسب، ویژگی)." + } + } + }, + "snapshots": { + "label": "عکس‌های فوری", + "enabled": { + "label": "اسنپ‌شات‌ها فعال شدند" + }, + "clean_copy": { + "label": "ذخیره نسخه پاک", + "description": "علاوه بر عکس‌های فوری دارای حاشیه‌نویسی، یک کپی تمیز بدون حاشیه‌نویسی از عکس‌های فوری ذخیره کنید." + }, + "timestamp": { + "label": "روکش مهر زمانی", + "description": "یک مهر زمانی روی عکس‌های ذخیره شده قرار دهید." + }, + "bounding_box": { + "label": "پوشش جعبه مرزی", + "description": "برای اشیاء ردیابی شده روی عکس‌های فوری ذخیره شده، کادرهای مرزی رسم کنید." + }, + "crop": { + "label": "برش عکس فوری", + "description": "عکس‌های ذخیره‌شده را در کادر محدوده شیء شناسایی‌شده برش دهید." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "مناطقی که یک شیء باید برای ذخیره شدن یک snapshot وارد آنها شود." + }, + "height": { + "label": "ارتفاع عکس فوری", + "description": "ارتفاع (پیکسل) برای تغییر اندازه عکس‌های ذخیره شده؛ برای حفظ اندازه اصلی، آن را خالی بگذارید." + }, + "retain": { + "label": "نگهداری اسنپ‌شات", + "description": "تنظیمات نگهداری برای اسنپ‌شات‌های ذخیره‌شده شامل روزهای پیش‌فرض و لغو هر شیء.", + "default": { + "label": "نگهداری پیش‌فرض", + "description": "تعداد روزهای پیش‌فرض برای نگهداری اسنپ‌شات‌ها." + }, + "mode": { + "label": "حالت نگهداری", + "description": "حالت نگهداری: همه (ذخیره همه بخش‌ها)، حرکت (ذخیره بخش‌های دارای حرکت) یا active_objects (ذخیره بخش‌های دارای اشیاء فعال)." + }, + "objects": { + "label": "نگهداری شیء", + "description": "برای هر شیء، تعداد روزهای نگهداری اسنپ‌شات را لغو می‌کند." + } + }, + "quality": { + "label": "کیفیت JPEG", + "description": "کیفیت کدگذاری JPEG برای عکس‌های ذخیره شده (0-100)." + } + }, + "timestamp_style": { + "label": "سبک مهر زمانی", + "position": { + "label": "موقعیت مهر زمانی", + "description": "موقعیت برچسب زمانی روی تصویر (tl/tr/bl/br)." + }, + "format": { + "label": "قالب مهر زمانی", + "description": "رشته‌ی قالب تاریخ و زمان که برای مهرهای زمانی استفاده می‌شود (کدهای قالب تاریخ و زمان پایتون)." + }, + "color": { + "label": "رنگ مهر زمانی", + "description": "مقادیر رنگ RGB برای متن مهر زمان (همه مقادیر ۰-۲۵۵).", + "red": { + "label": "قرمز", + "description": "جزء قرمز (۰-۲۵۵) برای رنگ مهر زمانی." + }, + "green": { + "label": "سبز", + "description": "جزء سبز (۰-۲۵۵) برای رنگ مهر زمانی." + }, + "blue": { + "label": "آبی", + "description": "جزء آبی (۰-۲۵۵) برای رنگ مهر زمانی." + } + }, + "thickness": { + "label": "ضخامت برچسب زمانی", + "description": "ضخامت خط متن برچسب زمانی." + }, + "effect": { + "label": "اثر مهر زمانی", + "description": "جلوه بصری برای متن مهر زمانی (هیچ، پر، سایه)." + } + }, + "notifications": { + "label": "اعلان‌ها", + "enabled": { + "label": "فعال کردن اعلان‌ها" + }, + "email": { + "label": "ایمیل اعلان", + "description": "آدرس ایمیلی که برای اعلان‌های فوری استفاده می‌شود یا توسط برخی از ارائه‌دهندگان اعلان مورد نیاز است." + }, + "cooldown": { + "label": "دوره استراحت (کول داون)", + "description": "بین اعلان‌ها (ثانیه) زمان برای خنک شدن در نظر بگیرید تا از ارسال هرزنامه به گیرندگان جلوگیری شود." + }, + "enabled_in_config": { + "label": "وضعیت اعلان‌های اصلی", + "description": "نشان می‌دهد که آیا اعلان‌ها در پیکربندی استاتیک اصلی فعال بوده‌اند یا خیر." + } + }, + "onvif": { + "description": "تنظیمات اتصال ONVIF و ردیابی خودکار PTZ برای این دوربین.", + "host": { + "label": "میزبان ONVIF", + "description": "میزبان (و طرح اختیاری) برای سرویس ONVIF برای این دوربین." + }, + "port": { + "label": "پورت ONVIF", + "description": "شماره پورت برای سرویس ONVIF." + }, + "user": { + "label": "نام کاربری ONVIF", + "description": "نام کاربری برای احراز هویت ONVIF؛ برخی از دستگاه‌ها برای ONVIF به کاربر ادمین نیاز دارند." + }, + "password": { + "label": "رمز عبور ONVIF", + "description": "رمز عبور برای احراز هویت ONVIF." + }, + "tls_insecure": { + "label": "غیرفعال کردن تأیید TLS", + "description": "از تأیید TLS صرف‌نظر کنید و مجوز خلاصه را برای ONVIF غیرفعال کنید (ناامن؛ فقط در شبکه‌های امن استفاده شود)." + }, + "autotracking": { + "label": "ردیابی خودکار", + "description": "با استفاده از حرکات دوربین PTZ، اشیاء متحرک را به طور خودکار ردیابی کرده و آنها را در مرکز قاب نگه دارید.", + "enabled": { + "label": "فعال کردن ردیابی خودکار", + "description": "فعال یا غیرفعال کردن ردیابی خودکار دوربین PTZ از اشیاء شناسایی شده." + }, + "calibrate_on_startup": { + "label": "کالیبره کردن در شروع", + "description": "سرعت موتورهای PTZ را در هنگام راه‌اندازی اندازه‌گیری کنید تا دقت ردیابی بهبود یابد. فریگیت پس از کالیبراسیون، پیکربندی را با movement_weights به‌روزرسانی می‌کند." + }, + "zooming": { + "label": "حالت بزرگنمایی", + "description": "کنترل رفتار زوم: غیرفعال (فقط حرکت افقی/عمودی)، مطلق (سازگارترین) یا نسبی (حرکت افقی/عمودی/بزرگنمایی همزمان)." + }, + "zoom_factor": { + "label": "ضریب بزرگنمایی", + "description": "سطح زوم را روی اشیاء ردیابی شده کنترل کنید. مقادیر پایین‌تر، صحنه بیشتری را در دید نگه می‌دارند؛ مقادیر بالاتر، نزدیک‌تر زوم می‌کنند اما ممکن است ردیابی را از دست بدهند. مقادیر بین ۰.۱ تا ۰.۷۵." + }, + "track": { + "label": "اشیاء ردیابی شده", + "description": "فهرست انواع اشیایی که باید ردیابی خودکار را فعال کنند." + }, + "required_zones": { + "label": "مناطق مورد نیاز", + "description": "اشیاء باید قبل از شروع ردیابی خودکار، وارد یکی از این مناطق شوند." + }, + "return_preset": { + "label": "بازگشت از پیش تعیین شده", + "description": "نام از پیش تعیین‌شده ONVIF که در میان‌افزار دوربین پیکربندی شده است تا پس از پایان ردیابی به آن بازگردد." + }, + "timeout": { + "label": "مهلت بازگشت", + "description": "چند ثانیه صبر کن پس از از دست دادن ردیابی قبل از بازگرداندن دوربین به موقعیت از پیش تعیین شده ." + }, + "movement_weights": { + "label": "وزنه‌های حرکتی", + "description": "مقادیر کالیبراسیون به طور خودکار توسط کالیبراسیون دوربین ایجاد می‌شوند. به صورت دستی تغییر ندهید." + }, + "enabled_in_config": { + "label": "حالت اتوترک اصلی", + "description": "فیلد داخلی برای ردیابی اینکه آیا ردیابی خودکار در پیکربندی فعال شده است یا خیر." + } + }, + "ignore_time_mismatch": { + "label": "عدم تطابق زمانی را نادیده بگیرید", + "description": "برای ارتباط ONVIF، از تفاوت‌های همگام‌سازی زمانی بین دوربین و سرور Frigate صرف نظر کنید." + }, + "label": "ONVIF پروتکل استاندارد انتقال تصویر ." + }, + "mqtt": { + "label": "MQTT یک پروتکل تبادل پیام سبک ." + }, + "version": { + "label": "نسخه فعلی config" + }, + "safe_mode": { + "label": "حالت امن" + }, + "environment_vars": { + "label": "متغیر های محیطی" + }, + "logger": { + "label": "گزارش گیری", + "default": { + "label": "سطح گزارش گیری" + } + }, + "auth": { + "label": "احراز هویت", + "enabled": { + "label": "فعال سازی احراز هویت" + }, + "reset_admin_password": { + "label": "بازنشانی رمز ادمین" + }, + "trusted_proxies": { + "label": "پراکسی های مورد اعتماد" + } + }, + "database": { + "label": "پایگاه داده" + } +} diff --git a/web/public/locales/fa/config/groups.json b/web/public/locales/fa/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/fa/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/fa/config/validation.json b/web/public/locales/fa/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/fa/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/fa/objects.json b/web/public/locales/fa/objects.json new file mode 100644 index 0000000..c2ce4e4 --- /dev/null +++ b/web/public/locales/fa/objects.json @@ -0,0 +1,120 @@ +{ + "person": "شخص", + "bicycle": "دوچرخه", + "car": "ماشین", + "airplane": "هواپیما", + "bus": "اتوبوس", + "train": "قطار", + "boat": "قایق", + "traffic_light": "چراغ راهنمایی", + "motorcycle": "موتور سیکلت", + "fire_hydrant": "شیر آتش‌نشانی", + "street_sign": "تابلو راهنمایی رانندگی", + "stop_sign": "تابلو ایست", + "parking_meter": "پارکومتر", + "bench": "نیمکت", + "bird": "پرنده", + "cat": "گربه", + "dog": "سگ", + "horse": "اسب", + "shoe": "کفش", + "eye_glasses": "عینک", + "handbag": "کیف دستی", + "tie": "کراوات", + "suitcase": "چمدان", + "frisbee": "فریزبی", + "sheep": "گوسفند", + "cow": "گاو", + "elephant": "فیل", + "bear": "خرس", + "zebra": "گورخر", + "giraffe": "زرافه", + "hat": "کلاه", + "umbrella": "چتر", + "skis": "اسکی", + "snowboard": "اسنوبورد", + "sports_ball": "توپ ورزشی", + "kite": "بادبادک", + "baseball_bat": "برای استفاده از چند فیلتر، آن‌ها را یکی پس از دیگری با یک فاصله از هم اضافه کنید.چوب بیسبال", + "baseball_glove": "دستکش بیسبال", + "skateboard": "اسکیت‌بورد", + "hot_dog": "هات‌داگ", + "cake": "کیک", + "couch": "مبل", + "bed": "تخت", + "dining_table": "میز ناهارخوری", + "toilet": "توالت", + "tv": "تلویزیون", + "mouse": "موش", + "keyboard": "صفحه‌کلید", + "goat": "بز", + "oven": "فر", + "sink": "سینک", + "refrigerator": "یخچال", + "book": "کتاب", + "vase": "گلدان", + "scissors": "قیچی", + "hair_dryer": "سشوار", + "hair_brush": "برس مو", + "vehicle": "وسیلهٔ نقلیه", + "deer": "گوزن", + "fox": "روباه", + "raccoon": "راکون", + "on_demand": "در صورت نیاز", + "license_plate": "پلاک خودرو", + "package": "بسته", + "amazon": "آمازون", + "usps": "USPS", + "fedex": "FedEx", + "dhl": "DHL", + "purolator": "پرولاتور", + "postnord": "PostNord", + "backpack": "کوله‌پشتی", + "tennis_racket": "راکت تنیس", + "bottle": "بطری", + "plate": "پلاک", + "wine_glass": "جام شراب", + "cup": "فنجان", + "fork": "چنگال", + "knife": "چاقو", + "spoon": "قاشق", + "bowl": "کاسه", + "banana": "موز", + "apple": "سیب", + "animal": "حیوان", + "sandwich": "ساندویچ", + "orange": "پرتقال", + "broccoli": "بروکلی", + "bark": "پارس", + "carrot": "هویج", + "pizza": "پیتزا", + "donut": "دونات", + "chair": "صندلی", + "potted_plant": "گیاه گلدانی", + "mirror": "آینه", + "window": "پنجره", + "desk": "میز", + "door": "در", + "laptop": "لپ‌تاپ", + "remote": "ریموت", + "cell_phone": "گوشی موبایل", + "microwave": "مایکروویو", + "toaster": "توستر", + "blender": "مخلوط‌کن", + "clock": "ساعت", + "teddy_bear": "خرس عروسکی", + "toothbrush": "مسواک", + "squirrel": "سنجاب", + "rabbit": "خرگوش", + "robot_lawnmower": "چمن‌زن رباتی", + "waste_bin": "سطل زباله", + "face": "چهره", + "bbq_grill": "گریل کباب", + "ups": "یو‌پی‌اس", + "an_post": "آن پُست", + "postnl": "پست‌اِن‌اِل", + "nzpost": "اِن‌زد پُست", + "gls": "جی‌اِل‌اِس", + "dpd": "دی‌پی‌دی", + "surfboard": "تخته موج سواری" +} diff --git a/web/public/locales/fa/views/chat.json b/web/public/locales/fa/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/fa/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/fa/views/classificationModel.json b/web/public/locales/fa/views/classificationModel.json new file mode 100644 index 0000000..5bb59ea --- /dev/null +++ b/web/public/locales/fa/views/classificationModel.json @@ -0,0 +1,189 @@ +{ + "button": { + "deleteClassificationAttempts": "حذف تصاویر طبقه بندی", + "renameCategory": "تغییر نام کلاس", + "deleteCategory": "حذف کردن کلاس", + "deleteImages": "حذف کردن عکس ها", + "trainModel": "مدل آموزش", + "addClassification": "افزودن دسته‌بندی", + "deleteModels": "حذف مدل‌ها", + "editModel": "ویرایش مدل" + }, + "toast": { + "success": { + "deletedCategory_one": "کلاس حذف شده", + "deletedCategory_other": "", + "deletedImage_one": "عکس های حذف شده", + "deletedImage_other": "", + "categorizedImage": "تصویر طبقه بندی شده", + "trainedModel": "مدل آموزش دیده شده.", + "trainingModel": "آموزش دادن مدل با موفقیت شروع شد.", + "deletedModel_one": "{{count}} مدل با موفقیت حذف شد", + "deletedModel_other": "{{count}} مدل با موفقیت حذف شدند", + "updatedModel": "پیکربندی مدل با موفقیت به‌روزرسانی شد", + "renamedCategory": "نام کلاس با موفقیت به {{name}} تغییر یافت" + }, + "error": { + "deleteImageFailed": "حذف نشد: {{errorMessage}}", + "deleteCategoryFailed": "کلاس حذف نشد: {{errorMessage}}", + "deleteModelFailed": "حذف مدل ناموفق بود: {{errorMessage}}", + "categorizeFailed": "دسته‌بندی تصویر ناموفق بود: {{errorMessage}}", + "trainingFailed": "آموزش مدل ناموفق بود. برای جزئیات، گزارش‌های Frigate را بررسی کنید.", + "trainingFailedToStart": "شروع آموزش مدل ناموفق بود: {{errorMessage}}", + "updateModelFailed": "به‌روزرسانی مدل ناموفق بود: {{errorMessage}}", + "renameCategoryFailed": "تغییر نام کلاس ناموفق بود: {{errorMessage}}" + } + }, + "documentTitle": "دسته بندی مدل ها - فریگیت", + "description": { + "invalidName": "نام نامعتبر، نام ها فقط می توانند شامل حروف، اعداد، فاصله، آپستروف، زیرخط و خط فاصله باشند." + }, + "details": { + "none": "هیچکدام", + "scoreInfo": "امتیاز، نشان دهنده میانگین دقت در تشخیص و دسته بندی این شیء در بین تمام تشخیص‌هاست.", + "unknown": "ناشناخته" + }, + "tooltip": { + "trainingInProgress": "مدل در حال آموزش است", + "noNewImages": "هیچ تصویر جدیدی برای آموزش وجود ندارد. ابتدا تصاویر بیشتری را در مجموعه‌داده دسته‌بندی کنید.", + "noChanges": "از آخرین آموزش، هیچ تغییری در مجموعه‌داده ایجاد نشده است.", + "modelNotReady": "مدل برای آموزش آماده نیست" + }, + "deleteCategory": { + "title": "(pending)", + "desc": "آیا مطمئن هستید که می‌خواهید کلاس {{name}} را حذف کنید؟ این کار همهٔ تصاویر مرتبط را برای همیشه حذف می‌کند و نیاز به آموزش مجدد مدل دارد.", + "minClassesTitle": "امکان حذف کلاس وجود ندارد", + "minClassesDesc": "یک مدل دسته‌بندی باید دست‌کم ۲ کلاس داشته باشد. پیش از حذف این مورد، یک کلاس دیگر اضافه کنید." + }, + "train": { + "titleShort": "اخیر", + "title": "طبقه‌بندی‌های اخیر", + "aria": "انتخاب طبقه‌بندی‌های اخیر" + }, + "deleteModel": { + "title": "حذف مدل دسته‌بندی", + "single": "آیا مطمئن هستید که می‌خواهید {{name}} را حذف کنید؟ این کار همهٔ داده‌های مرتبط از جمله تصاویر و داده‌های آموزش را برای همیشه حذف می‌کند. این عمل قابل بازگشت نیست.", + "desc_one": "آیا مطمئن هستید که می‌خواهید این {{count}} مدل را حذف کنید؟ این کار همهٔ داده‌های مرتبط از جمله تصاویر و داده‌های آموزشی را برای همیشه حذف می‌کند. این عمل قابل بازگشت نیست.", + "desc_other": "آیا مطمئن هستید که می‌خواهید {{count}} مدل را حذف کنید؟ این کار همهٔ داده‌های مرتبط از جمله تصاویر و داده‌های آموزشی را برای همیشه حذف می‌کند. این عمل قابل بازگشت نیست." + }, + "categorizeImage": "طبقه‌بندی تصویر", + "menu": { + "states": "حالت‌ها", + "objects": "اشیاء" + }, + "noModels": { + "object": { + "description": "یک مدل سفارشی ایجاد کنید تا اشیای شناسایی‌شده را طبقه‌بندی کند.", + "title": "هیچ مدل طبقه‌بندی شیء وجود ندارد", + "buttonText": "ایجاد مدل شیء" + }, + "state": { + "title": "هیچ مدل طبقه‌بندی حالت وجود ندارد", + "description": "یک مدل سفارشی ایجاد کنید تا تغییرات وضعیت را در نواحی مشخصِ دوربین پایش و طبقه‌بندی کند.", + "buttonText": "ایجاد مدل وضعیت" + } + }, + "wizard": { + "title": "ایجاد طبقه‌بندی جدید", + "steps": { + "stateArea": "ناحیهٔ حالت", + "nameAndDefine": "نام‌گذاری و تعریف", + "chooseExamples": "انتخاب نمونه‌ها" + }, + "step1": { + "description": "مدل‌های حالت نواحی ثابت دوربین را برای تغییرات پایش می‌کنند (مثلاً درِ باز/بسته). مدل‌های شیء به اشیای شناسایی‌شده طبقه‌بندی اضافه می‌کنند (مثلاً حیوانات شناخته‌شده، مأموران تحویل، و غیره).", + "namePlaceholder": "نام مدل را وارد کنید...", + "type": "نوع", + "typeObject": "شیء", + "objectLabelPlaceholder": "نوع شیء را انتخاب کنید...", + "classificationTypeDesc": "زیر‌برچسب‌ها متن اضافی به برچسب شیء اضافه می‌کنند (مثلاً «Person: UPS»). ویژگی‌ها فرادادهٔ قابل جست‌وجو هستند که جداگانه در فرادادهٔ شیء ذخیره می‌شوند.", + "classificationAttribute": "ویژگی", + "classes": "کلاس‌ها", + "classesTip": "دربارهٔ کلاس‌ها بیشتر بدانید", + "classesObjectDesc": "دسته‌بندی‌های مختلف را برای طبقه‌بندی اشیای شناسایی‌شده تعریف کنید. برای نمونه: «delivery_person»، «resident»، «stranger» برای طبقه‌بندی افراد.", + "errors": { + "nameLength": "نام مدل باید ۶۴ نویسه یا کم‌تر باشد", + "classesUnique": "نام کلاس‌ها باید یکتا باشند", + "stateRequiresTwoClasses": "مدل‌های حالت دست‌کم به ۲ کلاس نیاز دارند", + "objectLabelRequired": "لطفاً یک برچسب شیء را انتخاب کنید", + "nameRequired": "نام مدل الزامی است", + "nameOnlyNumbers": "نام مدل نمی‌تواند فقط شامل عدد باشد", + "noneNotAllowed": "کلاس «none» مجاز نیست", + "classRequired": "حداقل ۱ کلاس لازم است", + "objectTypeRequired": "لطفاً یک نوع طبقه‌بندی را انتخاب کنید" + }, + "name": "نام", + "typeState": "وضعیت", + "objectLabel": "برچسب شیء", + "classificationType": "نوع طبقه‌بندی", + "classificationSubLabel": "زیر‌برچسب", + "classificationTypeTip": "دربارهٔ انواع طبقه‌بندی بیشتر بدانید", + "states": "وضعیت‌ها", + "classesStateDesc": "حالت‌های مختلفی را که ناحیهٔ دوربین شما می‌تواند در آن باشد تعریف کنید. برای مثال: «باز» و «بسته» برای یک درِ گاراژ.", + "classPlaceholder": "نام کلاس را وارد کنید…" + }, + "step2": { + "description": "دوربین‌ها را انتخاب کنید و ناحیه‌ای را که باید برای هر دوربین پایش شود تعریف کنید. مدل، وضعیت این ناحیه‌ها را طبقه‌بندی می‌کند.", + "cameras": "دوربین‌ها", + "noCameras": "برای افزودن دوربین‌ها روی + کلیک کنید", + "selectCamera": "انتخاب دوربین", + "selectCameraPrompt": "برای تعریف ناحیهٔ پایش، یک دوربین را از فهرست انتخاب کنید" + }, + "step3": { + "selectImagesDescription": "برای انتخاب، روی تصاویر کلیک کنید. وقتی کارتان با این کلاس تمام شد روی «ادامه» کلیک کنید.", + "generating": { + "description": "Frigate در حال استخراج تصاویر نماینده از ضبط‌های شماست. ممکن است کمی زمان ببرد…", + "title": "در حال تولید تصاویر نمونه" + }, + "retryGenerate": "تلاش دوباره برای تولید", + "classifying": "در حال طبقه‌بندی و آموزش…", + "trainingStarted": "آموزش با موفقیت شروع شد", + "errors": { + "noCameras": "هیچ دوربینی پیکربندی نشده است", + "noObjectLabel": "هیچ برچسب شیئی انتخاب نشده است", + "generationFailed": "تولید ناموفق بود. لطفاً دوباره تلاش کنید.", + "classifyFailed": "طبقه‌بندی تصاویر ناموفق بود: {{error}}", + "generateFailed": "تولید نمونه‌ها ناموفق بود: {{error}}" + }, + "missingStatesWarning": { + "title": "نمونه‌های وضعیتِ جاافتاده", + "description": "برای بهترین نتیجه، توصیه می‌شود برای همهٔ حالت‌ها نمونه انتخاب کنید. می‌توانید بدون انتخاب همهٔ حالت‌ها ادامه دهید، اما تا زمانی که همهٔ حالت‌ها تصویر نداشته باشند مدل آموزش داده نمی‌شود. پس از ادامه، از نمای «طبقه‌بندی‌های اخیر» برای طبقه‌بندی تصاویرِ حالت‌های جاافتاده استفاده کنید، سپس مدل را آموزش دهید." + }, + "allImagesRequired_one": "لطفاً همهٔ تصاویر را طبقه‌بندی کنید. {{count}} تصویر باقی مانده است.", + "allImagesRequired_other": "لطفاً همهٔ تصاویر را طبقه‌بندی کنید. {{count}} تصویرها باقی مانده است.", + "training": { + "title": "در حال آموزش مدل", + "description": "مدل شما در پس‌زمینه در حال آموزش است. این پنجره را ببندید؛ به‌محض تکمیل آموزش، مدل شما شروع به اجرا می‌کند." + }, + "noImages": "هیچ تصویر نمونه‌ای تولید نشد", + "modelCreated": "مدل با موفقیت ایجاد شد. از نمای «طبقه‌بندی‌های اخیر» برای افزودن تصاویرِ وضعیت‌هایِ جاافتاده استفاده کنید، سپس مدل را آموزش دهید.", + "generateSuccess": "تصاویر نمونه با موفقیت تولید شد", + "selectImagesPrompt": "همهٔ تصاویر با {{className}} را انتخاب کنید" + } + }, + "edit": { + "title": "ویرایش مدل طبقه‌بندی", + "descriptionState": "کلاس‌های این مدل طبقه‌بندی حالت را ویرایش کنید. اعمال تغییرات نیاز به بازآموزی مدل دارد.", + "descriptionObject": "نوع شیء و نوع طبقه‌بندی را برای این مدل طبقه‌بندی شیء ویرایش کنید.", + "stateClassesInfo": "توجه: تغییر کلاس‌های وضعیت نیازمند بازآموزی مدل با کلاس‌های به‌روزرسانی‌شده است." + }, + "deleteDatasetImages": { + "title": "حذف تصاویر مجموعه‌داده", + "desc_one": "آیا مطمئن هستید که می‌خواهید این {{count}} تصویر را از {{dataset}} حذف کنید؟ این عمل قابل بازگشت نیست و نیاز به بازآموزی مدل دارد.", + "desc_other": "آیا مطمئن هستید که می‌خواهید {{count}} تصویر را از {{dataset}} حذف کنید؟ این عمل قابل بازگشت نیست و نیاز به بازآموزی مدل دارد." + }, + "deleteTrainImages": { + "title": "حذف تصاویر آموزش", + "desc_one": "آیا مطمئن هستید که می‌خواهید این {{count}} تصویر را حذف کنید؟ این عمل قابل بازگشت نیست.", + "desc_other": "آیا مطمئن هستید که می‌خواهید {{count}} تصویر را حذف کنید؟ این عمل قابل بازگشت نیست." + }, + "renameCategory": { + "title": "تغییر نام کلاس", + "desc": "یک نام جدید برای {{name}} وارد کنید. برای اعمال تغییر نام، لازم است مدل را بازآموزی کنید." + }, + "categories": "کلاس‌ها", + "createCategory": { + "new": "ایجاد کلاس جدید" + }, + "categorizeImageAs": "طبقه‌بندی تصویر به‌عنوان:" +} diff --git a/web/public/locales/fa/views/configEditor.json b/web/public/locales/fa/views/configEditor.json new file mode 100644 index 0000000..c43489d --- /dev/null +++ b/web/public/locales/fa/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "ویرایشگر کانفیگ - فریگیت", + "configEditor": "ویرایشگر کانفیگ", + "safeConfigEditor": "ویرایشگر تنظیمات (حالت امن)", + "safeModeDescription": "فریگیت به دلیل خطا در صحت سنجی پیکربندی، در حالت امن می باشد.", + "copyConfig": "کپی پیکربندی", + "saveAndRestart": "ذخیره و راه‌اندازی مجدد", + "saveOnly": "فقط ذخیره", + "confirm": "بدون ذخیره خارج می‌شوید؟", + "toast": { + "success": { + "copyToClipboard": "پیکربندی در کلیپ‌بورد کپی شد." + }, + "error": { + "savingError": "خطا در ذخیره‌سازی پیکربندی" + } + } +} diff --git a/web/public/locales/fa/views/events.json b/web/public/locales/fa/views/events.json new file mode 100644 index 0000000..cf3ca78 --- /dev/null +++ b/web/public/locales/fa/views/events.json @@ -0,0 +1,65 @@ +{ + "alerts": "هشدار‌ها", + "detections": "تشخیص‌ها", + "motion": { + "label": "حرکت", + "only": "فقط حرکتی" + }, + "allCameras": "همه دوربین‌ها", + "empty": { + "alert": "هیچ هشداری برای بازبینی وجود ندارد", + "detection": "هیچ تشخیصی برای بازبینی وجود ندارد", + "motion": "هیچ داده‌ای از حرکت پیدا نشد", + "recordingsDisabled": { + "title": "ضبط‌ها بایستی فعال باشند", + "description": "موارد بازبینی برای یک دوربین تنها درصورتی امکان ساخت دارند که ضبط‌ها برای آن دورین فعال باشد." + } + }, + "timeline": "خط زمانی", + "timeline.aria": "انتخاب خط زمانی", + "zoomIn": "بزرگ‌نمایی", + "zoomOut": "کوچک‌نمایی", + "events": { + "aria": "انتخاب رویدادها", + "noFoundForTimePeriod": "برای این بازهٔ زمانی هیچ رویدادی یافت نشد.", + "label": "رویدادها" + }, + "recordings": { + "documentTitle": "ضبط‌ها - فریگیت" + }, + "calendarFilter": { + "last24Hours": "۲۴ ساعت گذشته" + }, + "markAsReviewed": "علامت‌گذاری به‌عنوان بازبینی‌شده", + "markTheseItemsAsReviewed": "این موارد را به‌عنوان بازبینی‌شده علامت‌گذاری کنید", + "newReviewItems": { + "label": "مشاهدهٔ موارد جدید برای بازبینی", + "button": "موارد جدید برای بازبینی" + }, + "detail": { + "label": "جزئیات", + "noDataFound": "داده‌ای برای بازبینیِ جزئیات وجود ندارد", + "aria": "تغییر وضعیتِ نمای جزئیات", + "trackedObject_one": "{{count}} شیء", + "trackedObject_other": "{{count}} اشیاء", + "noObjectDetailData": "دادهٔ جزئیات شیء در دسترس نیست.", + "settings": "تنظیمات نمای جزئیات", + "alwaysExpandActive": { + "title": "همیشه فعال را باز کنید", + "desc": "در صورت امکان، همیشه جزئیات شیء مربوط به موردِ بازبینیِ فعال را باز کنید." + } + }, + "objectTrack": { + "trackedPoint": "نقطهٔ ردیابی‌شده", + "clickToSeek": "برای رفتن به این زمان کلیک کنید" + }, + "documentTitle": "بازبینی - Frigate", + "selected_one": "{{count}} انتخاب شد", + "selected_other": "{{count}} انتخاب شدند", + "select_all": "همه", + "camera": "دوربین", + "detected": "گزینه‌هاشناسایی شد", + "normalActivity": "عادی", + "needsReview": "نیاز به بازبینی", + "securityConcern": "نگرانی امنیتی" +} diff --git a/web/public/locales/fa/views/explore.json b/web/public/locales/fa/views/explore.json new file mode 100644 index 0000000..d532878 --- /dev/null +++ b/web/public/locales/fa/views/explore.json @@ -0,0 +1,248 @@ +{ + "generativeAI": "هوش مصنوعی تولید کننده", + "documentTitle": "کاوش - فریگیت", + "exploreMore": "نمایش اشیا {{label}} بیشتر", + "details": { + "timestamp": "زمان دقیق", + "item": { + "desc": "بررسی جزئیات مورد", + "button": { + "viewInExplore": "مشاهده در کاوش", + "share": "اشتراک‌گذاری این مورد بازبینی" + }, + "tips": { + "hasMissingObjects": "اگر می‌خواهید Frigate اشیای ردیابی‌شده را برای برچسب‌های زیر ذخیره کند، پیکربندی خود را تنظیم کنید: {{objects}} ", + "mismatch_one": "{{count}} شیء غیرقابلدسترس شناسایی شد و در این مورد بازبینی گنجانده شد. این اشیا یا شرایط لازم برای هشدار یا تشخیص را نداشتند یا قبلاً پاکسازی/حذف شدهاند.", + "mismatch_other": "{{count}} شیء غیرقابلدسترس شناسایی شدند و در این مورد بازبینی گنجانده شدند. این اشیا یا شرایط لازم برای هشدار یا تشخیص را نداشتند یا قبلاً پاکسازی/حذف شدهاند." + }, + "toast": { + "success": { + "regenerate": "یک توضیح جدید از {{provider}} درخواست شد. بسته به سرعت ارائه‌دهندهٔ شما، بازتولیدِ توضیح جدید ممکن است کمی زمان ببرد.", + "updatedLPR": "پلاک با موفقیت به‌روزرسانی شد.", + "audioTranscription": "درخواست تبدیل گفتارِ صوت با موفقیت ثبت شد. بسته به سرعت سرور Frigate شما، تکمیل تبدیل گفتار ممکن است کمی زمان ببرد.", + "updatedSublabel": "زیر برچسب با موفقیت به‌روزرسانی شد.", + "updatedAttributes": "ویژگی‌ها با موفقیت به‌روزرسانی شد." + }, + "error": { + "updatedSublabelFailed": "به‌روزرسانی زیر‌برچسب ناموفق بود: {{errorMessage}}", + "updatedAttributesFailed": "به‌روزرسانی ویژگی‌ها ناموفق بود: {{errorMessage}}", + "regenerate": "فراخوانی {{provider}} برای توضیح جدید ناموفق بود: {{errorMessage}}", + "updatedLPRFailed": "به‌روزرسانی پلاک ناموفق بود: {{errorMessage}}", + "audioTranscription": "درخواست رونویسی صدا ناموفق بود: {{errorMessage}}" + } + }, + "title": "جزئیات مورد بازبینی" + }, + "editSubLabel": { + "title": "ویرایش زیر‌برچسب", + "descNoLabel": "برای این شیء ردیابی‌شده یک زیر‌برچسب جدید وارد کنید", + "desc": "برای این {{label}} یک زیر‌برچسب جدید وارد کنید" + }, + "editLPR": { + "desc": "برای {{label}} یک مقدار جدید برای پلاک وارد کنید", + "descNoLabel": "برای این شیء ردیابی‌شده یک مقدار جدید برای پلاک وارد کنید", + "title": "ویرایش پلاک" + }, + "editAttributes": { + "desc": "ویژگی‌های طبقه‌بندی را برای {{label}} انتخاب کنید", + "title": "ویرایش ویژگی‌ها" + }, + "topScore": { + "label": "بالاترین امتیاز", + "info": "بالاترین امتیاز، بالاترین امتیاز میانه برای شیء ردیابی‌شده است؛ بنابراین ممکن است با امتیازی که روی تصویر بندانگشتیِ نتیجهٔ جست‌وجو نمایش داده می‌شود متفاوت باشد." + }, + "recognizedLicensePlate": "پلاک شناسایی‌شده", + "estimatedSpeed": "سرعت تخمینی", + "objects": "اشیا", + "zones": "ناحیه‌ها", + "button": { + "regenerate": { + "title": "بازتولید", + "label": "بازسازی توضیح شیء ردیابی‌شده" + }, + "findSimilar": "یافتن مشابه" + }, + "description": { + "placeholder": "توضیحِ شیء ردیابی‌شده", + "label": "توضیحات", + "aiTips": "Frigate تا زمانی که چرخهٔ عمر شیء ردیابی‌شده پایان نیابد، از ارائه‌دهندهٔ هوش مصنوعی مولد شما درخواست توضیح نمی‌کند." + }, + "expandRegenerationMenu": "باز کردن منوی بازتولید", + "regenerateFromSnapshot": "بازتولید از اسنپ‌شات", + "tips": { + "descriptionSaved": "توضیح با موفقیت ذخیره شد", + "saveDescriptionFailed": "به‌روزرسانی توضیح ناموفق بود: {{errorMessage}}" + }, + "label": "برچسب", + "snapshotScore": { + "label": "امتیاز عکس فوری" + }, + "score": { + "label": "امتیاز" + }, + "attributes": "ویژگی‌های طبقه‌بندی", + "camera": "دوربین", + "regenerateFromThumbnails": "بازسازی از تصاویر بندانگشتی", + "title": { + "label": "عنوان" + } + }, + "exploreIsUnavailable": { + "title": "کاوش کردن در دسترس نیست", + "embeddingsReindexing": { + "startingUp": "درحال شروع…", + "context": "پس از اینکه جاسازی‌های شیء ردیابی‌شده، نمایه‌سازی مجدد را به پایان رساندند، می‌توان از کاوش استفاده کرد.", + "estimatedTime": "زمان تخمینی باقی‌مانده:", + "finishingShortly": "به‌زودی تمام می‌شود", + "step": { + "thumbnailsEmbedded": "تصاویر بندانگشتی جاسازی‌شده: ", + "descriptionsEmbedded": "توضیحات جاسازی‌شده: ", + "trackedObjectsProcessed": "اشیای ردیابی‌شدهٔ پردازش‌شده: " + } + }, + "downloadingModels": { + "context": "Frigate در حال دانلود مدل‌های بردارسازی لازم برای پشتیبانی از قابلیت «جست‌وجوی معنایی» است. بسته به سرعت اتصال شبکه شما، این کار ممکن است چند دقیقه طول بکشد.", + "setup": { + "visionModel": "مدل بینایی", + "visionModelFeatureExtractor": "استخراج‌کنندهٔ ویژگی‌های مدل بینایی", + "textModel": "مدل متنی", + "textTokenizer": "توکن‌ساز متن" + }, + "tips": { + "context": "ممکن است بخواهید پس از دانلود مدل‌ها، تعبیه‌های اشیای ردیابی‌شدهٔ خود را دوباره ایندکس کنید." + }, + "error": "خطایی رخ داده است. گزارش‌های Frigate را بررسی کنید." + } + }, + "trackingDetails": { + "adjustAnnotationSettings": "تنظیمات حاشیه‌نویسی را تنظیم کنید", + "scrollViewTips": "برای مشاهدهٔ لحظه‌های مهم چرخهٔ زندگی این شیء کلیک کنید.", + "autoTrackingTips": "موقعیت کادرها برای دوربین‌های ردیابی خودکار دقیق نخواهد بود.", + "count": "{{first}} از {{second}}", + "trackedPoint": "نقطهٔ ردیابی‌شده", + "lifecycleItemDesc": { + "visible": "{{label}} شناسایی شد", + "entered_zone": "{{label}} وارد {{zones}} شد", + "active": "{{label}} فعال شد", + "stationary": "{{label}} ساکن شد", + "attribute": { + "faceOrLicense_plate": "{{attribute}} برای {{label}} شناسایی شد", + "other": "{{label}} به‌عنوان {{attribute}} شناسایی شد" + }, + "gone": "{{label}} خارج شد", + "heard": "{{label}} شنیده شد", + "external": "{{label}} شناسایی شد", + "header": { + "zones": "ناحیه‌ها", + "ratio": "نسبت", + "area": "مساحت", + "score": "امتیاز" + } + }, + "title": "جزئیات ردیابی", + "noImageFound": "برای این برچسب زمانی هیچ تصویری یافت نشد.", + "createObjectMask": "ایجاد ماسک شیء", + "annotationSettings": { + "title": "تنظیمات حاشیه‌نویسی", + "showAllZones": { + "title": "نمایش همهٔ مناطق", + "desc": "همیشه مناطق را روی فریم‌هایی که اشیا وارد یک منطقه شده‌اند نمایش دهید." + }, + "offset": { + "toast": { + "success": "افست حاشیه‌نویسی برای {{camera}} در فایل پیکربندی ذخیره شد." + }, + "label": "افست حاشیه‌نویسی", + "desc": "این داده از فید تشخیص دوربین شما می‌آید، اما روی تصاویر فید ضبط‌شده قرار می‌گیرد. بعید است این دو جریان کاملاً هم‌زمان باشند. در نتیجه، کادر محدوده و ویدیو دقیقاً روی هم منطبق نخواهند بود. می‌توانید با این تنظیمات، حاشیه‌نویسی‌ها را در زمان به جلو یا عقب جابه‌جا کنید تا با ویدئوی ضبط‌شده بهتر هم‌تراز شوند.", + "millisecondsToOffset": "میلی‌ثانیه برای جابه‌جایی حاشیه‌نویسی‌های تشخیص. پیش‌فرض: 0 ", + "tips": "اگر پخش ویدیو جلوتر از کادرها و نقاط مسیر است مقدار را کمتر کنید و اگر پخش ویدیو عقب‌تر از آن‌هاست مقدار را بیشتر کنید. این مقدار می‌تواند منفی باشد." + } + }, + "carousel": { + "previous": "اسلاید قبلی", + "next": "اسلاید بعدی" + } + }, + "trackedObjectDetails": "جزئیات شیء ردیابی‌شده", + "type": { + "details": "جزئیات‌ها", + "snapshot": "عکس فوری", + "thumbnail": "پیش‌نمایش", + "video": "ویدیو", + "tracking_details": "جزئیات ردیابی" + }, + "itemMenu": { + "downloadVideo": { + "aria": "دانلود ویدئو", + "label": "دانلود ویدیو" + }, + "downloadSnapshot": { + "label": "دانلود اسنپ‌شات", + "aria": "دانلود عکس" + }, + "downloadCleanSnapshot": { + "label": "دانلود اسنپ‌شاتِ بدون کادر", + "aria": "دانلود عکس فوری بدون کادر" + }, + "viewTrackingDetails": { + "aria": "نمایش جزئیات ردیابی", + "label": "مشاهدهٔ جزئیات ردیابی" + }, + "findSimilar": { + "label": "یافتن مشابه", + "aria": "یافتن اشیای ردیابی‌شدهٔ مشابه" + }, + "addTrigger": { + "label": "افزودن تریگر", + "aria": "افزودن تریگر برای این شیء ردیابی‌شده" + }, + "audioTranscription": { + "aria": "درخواست رونویسیِ صوتی", + "label": "رونویسی" + }, + "submitToPlus": { + "aria": "ارسال به Frigate Plus", + "label": "ارسال به Frigate+" + }, + "viewInHistory": { + "label": "مشاهده در تاریخچه", + "aria": "مشاهده در تاریخچه" + }, + "showObjectDetails": { + "label": "نمایش مسیر شیء" + }, + "hideObjectDetails": { + "label": "پنهان کردن مسیر شیء" + }, + "deleteTrackedObject": { + "label": "حذف این شیء ردیابی‌شده" + } + }, + "noTrackedObjects": "هیچ شیء ردیابی‌شده‌ای پیدا نشد", + "fetchingTrackedObjectsFailed": "خطا در دریافت اشیای ردیابی‌شده: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} شیء ردیابیشده ", + "trackedObjectsCount_other": "{{count}} اشیای ردیابیشده ", + "dialog": { + "confirmDelete": { + "title": "تأیید حذف", + "desc": "حذف این شیء ردیابی‌شده عکس فوری، هرگونه امبدینگ ذخیره‌شده و هر ورودی مرتبط با جزئیات ردیابی را حذف می‌کند. فیلم ضبط‌شدهٔ این شیء ردیابی‌شده در نمای تاریخ حذف نخواهد شد.

    آیا مطمئنید می‌خواهید ادامه دهید؟" + } + }, + "searchResult": { + "tooltip": "{{type}} با {{confidence}}٪ مطابقت داشت", + "previousTrackedObject": "شیء ردیابی‌شدهٔ قبلی", + "nextTrackedObject": "شیء ردیابی‌شدهٔ بعدی", + "deleteTrackedObject": { + "toast": { + "success": "شیء ردیابی‌شده با موفقیت حذف شد.", + "error": "حذف شیء ردیابی‌شده ناموفق بود: {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "تحلیل هوش مصنوعی" + }, + "concerns": { + "label": "نگرانی‌ها" + } +} diff --git a/web/public/locales/fa/views/exports.json b/web/public/locales/fa/views/exports.json new file mode 100644 index 0000000..922569b --- /dev/null +++ b/web/public/locales/fa/views/exports.json @@ -0,0 +1,28 @@ +{ + "search": "جستجو", + "documentTitle": "گرفتن خروجی - فریگیت", + "noExports": "هیچ خروجی یافت نشد", + "deleteExport": "حذف خروجی", + "deleteExport.desc": "آیا مطمئن هستید که می‌خواهید {{exportName}} را حذف کنید؟", + "editExport": { + "title": "تغییر نام خروجی", + "desc": "یک نام جدید برای این خروجی وارد کنید.", + "saveExport": "ذخیرهٔ خروجی" + }, + "tooltip": { + "shareExport": "اشتراک‌گذاری خروجی", + "downloadVideo": "دانلود ویدئو", + "editName": "ویرایش نام", + "deleteExport": "حذف خروجی", + "assignToCase": "به مورد اضافه کنید" + }, + "toast": { + "error": { + "renameExportFailed": "تغییر نام خروجی ناموفق بود: {{errorMessage}}" + } + }, + "headings": { + "cases": "موارد", + "uncategorizedExports": "خروجی دسته‌بندی نشده" + } +} diff --git a/web/public/locales/fa/views/faceLibrary.json b/web/public/locales/fa/views/faceLibrary.json new file mode 100644 index 0000000..7326d38 --- /dev/null +++ b/web/public/locales/fa/views/faceLibrary.json @@ -0,0 +1,99 @@ +{ + "description": { + "addFace": "با بارگزاری اولین عکستان، یک مجموعه جدید به کتابخانه چهره اضافه کنید.", + "placeholder": "نامی برای این مجموعه وارد کنید", + "invalidName": "نام نامعتبر، نام ها فقط می توانند شامل حروف، اعداد، فاصله، آپستروف، زیرخط و خط فاصله باشند.", + "nameCannotContainHash": "نام نمی‌تواند شامل # باشد." + }, + "details": { + "timestamp": "زمان دقیق", + "unknown": "ناشناخته", + "scoreInfo": "امتیاز، میانگینِ وزن‌دارِ امتیاز همهٔ چهره‌هاست که وزن آن براساس اندازهٔ چهره در هر تصویر تعیین می‌شود." + }, + "documentTitle": "کتابخانه چهره - Frigate", + "uploadFaceImage": { + "title": "بارگذاری تصویر چهره", + "desc": "یک تصویر بارگذاری کنید تا چهره‌ها اسکن شوند و برای {{pageToggle}} در نظر گرفته شود" + }, + "collections": "مجموعه‌ها", + "createFaceLibrary": { + "new": "ایجاد چهرهٔ جدید", + "nextSteps": "برای ایجاد یک پایهٔ محکم:
  • از تب «تشخیص‌های اخیر» برای انتخاب و آموزش با تصاویر هر شخصِ شناسایی‌شده استفاده کنید.
  • برای بهترین نتیجه روی تصاویر روبه‌رو تمرکز کنید؛ از آموزش با تصاویری که چهره را از زاویه نشان می‌دهند خودداری کنید.
  • " + }, + "steps": { + "faceName": "نام چهره را وارد کنید", + "uploadFace": "بارگذاری تصویر چهره", + "nextSteps": "مراحل بعدی", + "description": { + "uploadFace": "تصویری از {{name}} بارگذاری کنید که چهرهٔ او را از زاویهٔ روبه‌رو نشان دهد. لازم نیست تصویر فقط به چهرهٔ او برش داده شود." + } + }, + "button": { + "addFace": "افزودن چهره", + "renameFace": "تغییر نام چهره", + "deleteFace": "حذف چهره", + "uploadImage": "بارگذاری تصویر", + "reprocessFace": "پردازش مجدد چهره", + "deleteFaceAttempts": "حذف چهره‌ها" + }, + "imageEntry": { + "validation": { + "selectImage": "لطفاً یک فایل تصویر انتخاب کنید." + }, + "dropActive": "تصویر را اینجا رها کنید…", + "dropInstructions": "یک تصویر را اینجا بکشید و رها کنید یا جای‌گذاری کنید، یا برای انتخاب کلیک کنید", + "maxSize": "حداکثر اندازه: {{size}}MB" + }, + "train": { + "title": "تشخیص‌های اخیر", + "titleShort": "اخیر", + "aria": "تشخیص‌های اخیر را انتخاب کنید", + "empty": "تلاشِ اخیر برای تشخیص چهره وجود ندارد", + "emptyNoLibrary": { + "title": "یک صورت را آپلود کنید", + "description": "شما باید حداقل یک چهره به کتابخانه اضافه کنید تا عملکرد تشخیص چهره کار کند." + } + }, + "deleteFaceLibrary": { + "title": "حذف نام", + "desc": "آیا مطمئن هستید می‌خواهید مجموعهٔ {{name}} را حذف کنید؟ این کار همهٔ چهره‌های مرتبط را برای همیشه حذف می‌کند." + }, + "deleteFaceAttempts": { + "title": "حذف چهره‌ها", + "desc_one": "آیا مطمئن هستید که می‌خواهید {{count}} چهره را حذف کنید؟ این عمل قابل بازگشت نیست.", + "desc_other": "آیا مطمئن هستید که می‌خواهید {{count}} چهره ها را حذف کنید؟ این عمل قابل بازگشت نیست." + }, + "renameFace": { + "title": "تغییر نام چهره", + "desc": "یک نام جدید برای {{name}} وارد کنید" + }, + "nofaces": "هیچ چهره‌ای موجود نیست", + "trainFaceAs": "شناسایی شدآموزش چهره به‌عنوان:", + "trainFace": "آموزش چهره", + "toast": { + "success": { + "uploadedImage": "تصویر با موفقیت بارگذاری شد.", + "addFaceLibrary": "{{name}} با موفقیت به کتابخانهٔ چهره اضافه شد!", + "deletedFace_one": "حذف این {{count}} چهره با موفقیت انجام شد.", + "deletedFace_other": "حذف {{count}} چهره با موفقیت انجام شد.", + "deletedName_one": "{{count}} چهره با موفقیت حذف شد.", + "deletedName_other": "{{count}} چهره با موفقیت حذف شدند.", + "renamedFace": "نام چهره با موفقیت به {{name}} تغییر یافت", + "trainedFace": "آموزش چهره با موفقیت انجام شد.", + "updatedFaceScore": "امتیاز چهره با موفقیت به {{name}} ( {{score}}) به‌روزرسانی شد.", + "reclassifiedFace": "کلاسه بندی مجدد چهره با موفقیت انجام شد." + }, + "error": { + "uploadingImageFailed": "آپلود تصویر ناموفق بود: {{errorMessage}}", + "addFaceLibraryFailed": "تنظیم نام چهره ناموفق بود: {{errorMessage}}", + "deleteFaceFailed": "حذف ناموفق بود: {{errorMessage}}", + "deleteNameFailed": "حذف نام ناموفق بود: {{errorMessage}}", + "renameFaceFailed": "تغییر نام چهره ناموفق بود: {{errorMessage}}", + "trainFailed": "آموزش ناموفق بود: {{errorMessage}}", + "updateFaceScoreFailed": "به‌روزرسانی امتیاز چهره ناموفق بود: {{errorMessage}}", + "reclassifyFailed": "کلاسه بندی مجدد برای چهره: {{errorMessage}} دچار مشکل شد" + } + }, + "reclassifyFaceAs": "کلاسه بندی مجدد چهره به عنوان:", + "reclassifyFace": "کلاسه بندی مجدد چهره" +} diff --git a/web/public/locales/fa/views/live.json b/web/public/locales/fa/views/live.json new file mode 100644 index 0000000..383da43 --- /dev/null +++ b/web/public/locales/fa/views/live.json @@ -0,0 +1,186 @@ +{ + "documentTitle": "زنده - فریگیت", + "documentTitle.withCamera": "{{camera}} - زنده - فریگیت", + "lowBandwidthMode": "حالت کاهش مصرف پهنای باند", + "twoWayTalk": { + "enable": "فعال سازی مکالمه دوطرفه", + "disable": "غیرفعال کردن گفتگوی دوطرفه" + }, + "cameraAudio": { + "enable": "فعالسازی صدای دوربین", + "disable": "غیرفعال کردن صدای دوربین" + }, + "ptz": { + "move": { + "clickMove": { + "label": "برای قرار دادن دوربین در مرکز، در کادر کلیک کنید", + "enable": "فعال‌سازی کلیک برای جابه‌جایی", + "disable": "غیرفعال‌سازی کلیک برای جابه‌جایی" + }, + "left": { + "label": "دوربین PTZ را به چپ حرکت دهید" + }, + "up": { + "label": "دوربین PTZ را به بالا حرکت دهید" + }, + "right": { + "label": "دوربین PTZ را به راست حرکت دهید" + }, + "down": { + "label": "دوربین PTZ را به پایین حرکت دهید" + } + }, + "zoom": { + "in": { + "label": "روی دوربین PTZ بزرگ‌نمایی کنید" + }, + "out": { + "label": "روی دوربین PTZ کوچک‌نمایی کنید" + } + }, + "focus": { + "in": { + "label": "فوکوس دوربین PTZ را به داخل ببرید" + }, + "out": { + "label": "فوکوس دوربین PTZ را به بیرون ببرید" + } + }, + "frame": { + "center": { + "label": "برای قرار دادن دوربین PTZ در مرکز، داخل کادر کلیک کنید" + } + }, + "presets": "پیش‌تنظیم‌های دوربین PTZ" + }, + "recording": { + "disable": "غیرفعال کردن ضبط", + "enable": "فعال‌سازی ضبط" + }, + "snapshots": { + "enable": "فعال کردن عکس‌های فوری", + "disable": "غیرفعال کردن عکس‌های فوری" + }, + "snapshot": { + "takeSnapshot": "دانلود عکس فوری", + "noVideoSource": "منبع ویدیویی برای عکس فوری در دسترس نیست.", + "captureFailed": "گرفتن عکس فوری ناموفق بود.", + "downloadStarted": "دانلود عکس فوری آغاز شد." + }, + "camera": { + "enable": "فعال کردن دوربین", + "disable": "غیرفعال کردن دوربین" + }, + "muteCameras": { + "enable": "بی‌صدا کردن همهٔ دوربین‌ها", + "disable": "قطع بی‌صدا برای همهٔ دوربین‌ها" + }, + "detect": { + "enable": "فعال‌سازی تشخیص", + "disable": "غیرفعال‌سازی تشخیص" + }, + "audioDetect": { + "enable": "فعال‌سازی تشخیص صدا", + "disable": "غیرفعال‌سازی تشخیص صدا" + }, + "transcription": { + "enable": "فعال‌سازی رونوشت‌برداری زندهٔ صدا", + "disable": "غیرفعال‌سازی رونوشت‌برداری زندهٔ صدا" + }, + "autotracking": { + "enable": "فعال‌سازی ردیابی خودکار", + "disable": "غیرفعال کردن ردیابی خودکار" + }, + "streamingSettings": "تنظیمات استریم", + "audio": "صدا", + "stream": { + "title": "جریان", + "audio": { + "tips": { + "title": "برای این استریم، صدا باید از دوربین شما خروجی داده شود و در go2rtc پیکربندی شده باشد." + }, + "unavailable": "صدا برای این استریم در دسترس نیست", + "available": "برای این جریان صدا در دسترس است" + }, + "twoWayTalk": { + "tips": "دستگاه شما باید از این قابلیت پشتیبانی کند و WebRTC برای مکالمهٔ دوطرفه پیکربندی شده باشد.", + "unavailable": "مکالمهٔ دوطرفه برای این استریم در دسترس نیست", + "available": "گفت‌وگوی دوطرفه برای این جریان در دسترس است" + }, + "playInBackground": { + "label": "پخش در پس‌زمینه", + "tips": "این گزینه را فعال کنید تا هنگام پنهان بودن پخش‌کننده، پخش زنده ادامه یابد." + }, + "debug": { + "picker": "انتخاب جریان در حالت اشکال‌زدایی در دسترس نیست. نمای اشکال‌زدایی همیشه از جریانی استفاده می‌کند که نقش detect به آن اختصاص داده شده است." + }, + "lowBandwidth": { + "tips": "به‌دلیل بافر شدن یا خطاهای جریان، نمای زنده در حالت کم‌پهنای‌باند است.", + "resetStream": "بازنشانی جریان" + } + }, + "cameraSettings": { + "title": "تنظیمات {{camera}}", + "objectDetection": "تشخیص شیء", + "snapshots": "اسنپ‌شات‌ها", + "audioDetection": "تشخیص صدا", + "autotracking": "ردیابی خودکار", + "cameraEnabled": "دوربین فعال", + "recording": "ضبط", + "transcription": "رونویسی صوتی" + }, + "effectiveRetainMode": { + "modes": { + "motion": "حرکت", + "all": "همه", + "active_objects": "اشیای فعال" + } + }, + "editLayout": { + "label": "ویرایش چیدمان", + "group": { + "label": "ویرایش گروه دوربین" + }, + "exitEdit": "خروج از حالت ویرایش" + }, + "noCameras": { + "title": "هیچ دوربینی پیکربندی نشده است", + "buttonText": "افزودن دوربین", + "restricted": { + "description": "شما اجازهٔ مشاهدهٔ هیچ دوربینی را در این گروه ندارید.", + "title": "هیچ دوربینی در دسترس نیست" + }, + "description": "برای شروع، یک دوربین را به Frigate متصل کنید." + }, + "streamStats": { + "enable": "نمایش آمار پخش", + "disable": "پنهان کردن آمار پخش" + }, + "manualRecording": { + "tips": "بر اساس تنظیمات نگهداری ضبطِ این دوربین، یک عکس فوری دانلود کنید یا یک رویداد دستی را شروع کنید.", + "playInBackground": { + "label": "پخش در پس‌زمینه", + "desc": "این گزینه را فعال کنید تا هنگام پنهان بودن پخش‌کننده، پخش زنده ادامه یابد." + }, + "showStats": { + "label": "نمایش آمار", + "desc": "این گزینه را فعال کنید تا آمار پخش به‌صورت هم‌پوشان روی تصویر دوربین نمایش داده شود." + }, + "debugView": "نمای اشکال‌زدایی", + "start": "شروع ضبط درخواستی", + "started": "ضبط دستیِ درخواستی شروع شد.", + "failedToStart": "شروع ضبط دستیِ درخواستی ناموفق بود.", + "recordDisabledTips": "از آن‌جا که ضبط برای این دوربین در تنظیمات غیرفعال یا محدود شده است، فقط یک عکس فوری ذخیره می‌شود.", + "end": "پایان ضبط درخواستی", + "ended": "ضبط دستیِ درخواستی پایان یافت.", + "failedToEnd": "پایان دادنِ ضبط دستیِ درخواستی ناموفق بود.", + "title": "بر حسب تقاضا" + }, + "notifications": "اعلان‌ها", + "suspend": { + "forTime": "تعلیق به مدت: " + }, + "history": { + "label": "نمایش ویدیوهای تاریخی" + } +} diff --git a/web/public/locales/fa/views/motionSearch.json b/web/public/locales/fa/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/fa/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/fa/views/recording.json b/web/public/locales/fa/views/recording.json new file mode 100644 index 0000000..a7a9a13 --- /dev/null +++ b/web/public/locales/fa/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "فیلتر", + "export": "خروجی گرفتن", + "calendar": "تقویم", + "filters": "فیلترها", + "toast": { + "error": { + "noValidTimeSelected": "بازهٔ زمانی معتبری انتخاب نشده است", + "endTimeMustAfterStartTime": "زمان پایان باید بعد از زمان شروع باشد" + } + } +} diff --git a/web/public/locales/fa/views/replay.json b/web/public/locales/fa/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/fa/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/fa/views/search.json b/web/public/locales/fa/views/search.json new file mode 100644 index 0000000..1a72fe5 --- /dev/null +++ b/web/public/locales/fa/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "جستجو", + "savedSearches": "جستجوهای ذخیره شده", + "searchFor": "جستجو برای {{inputValue}}", + "button": { + "clear": "پاک کردن جستجو", + "save": "ذخیره جستجو", + "delete": "حذف جستجوی ذخیره‌شده", + "filterInformation": "اطلاعات فیلتر", + "filterActive": "فیلترها فعال‌اند" + }, + "trackedObjectId": "شناسه‌ی شی ردیابی‌شده", + "filter": { + "label": { + "cameras": "دوربین‌ها", + "labels": "برچسب‌ها", + "sub_labels": "زیر‌برچسب‌ها", + "attributes": "صفت‌ها", + "search_type": "نوع جستجو", + "time_range": "بازه زمانی", + "zones": "ناحیه‌ها", + "before": "قبل از", + "after": "بعد از", + "min_score": "کمینه امتیاز", + "max_score": "بیشینه امتیاز", + "min_speed": "کمینه سرعت", + "max_speed": "بیشینه سرعت", + "recognized_license_plate": "پلاک شناسایی شده", + "has_clip": "دارای کلیپ", + "has_snapshot": "دارای عکس فوری" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "تاریخ 'قبل از' باید بعد از تاریخ 'بعد از' باشد.", + "afterDatebeEarlierBefore": "تاریخ 'بعد از' باید قبل از تاریخ 'قبل از' باشد.", + "minScoreMustBeLessOrEqualMaxScore": "'min_score' باید کمتر یا مساوی 'max_score' باشد.", + "maxScoreMustBeGreaterOrEqualMinScore": "'max_score' باید بزرگ‌تر یا مساوی 'min_score' باشد.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'min_speed' باید کمتر یا مساوی 'max_speed' باشد.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'max_speed' باید بزرگ‌تر یا مساوی 'min_speed' باشد." + } + }, + "searchType": { + "thumbnail": "بندانگشتی", + "description": "توضیحات" + }, + "tips": { + "title": "نحوهٔ استفاده از فیلترهای متنی", + "desc": { + "text": "فیلترها به شما کمک می‌کنند نتایج جست‌وجوی خود را محدودتر کنید. در اینجا نحوهٔ استفاده از آن‌ها در فیلد ورودی آمده است:", + "step1": "نام کلید فیلتر را بنویسید و بعد از آن دونقطه بگذارید (مثلاً \"cameras:\").", + "step2": "از پیشنهادها یک مقدار را انتخاب کنید یا مقدار دلخواه خود را تایپ کنید.", + "step3": "برای استفاده از چند فیلتر، آن‌ها را یکی پس از دیگری با یک فاصله از هم اضافه کنید.", + "step4": "فیلترهای تاریخ (before: و after:) از قالب {{DateFormat}} استفاده می‌کنند.", + "step5": "فیلتر بازهٔ زمانی از قالب {{exampleTime}} استفاده می‌کند.", + "exampleLabel": "مثال:", + "step6": "فیلترها را با کلیک بر روی 'x' کنار آنها حذف کنید." + } + }, + "header": { + "currentFilterType": "مقادیر فیلتر", + "noFilters": "فیلترها", + "activeFilters": "فیلترهای فعال" + } + }, + "similaritySearch": { + "title": "جستجوی مشابهت", + "active": "جستجوی مشابهت فعال است", + "clear": "پاک کردن جستجوی مشابهت" + }, + "placeholder": { + "search": "جستجو…" + } +} diff --git a/web/public/locales/fa/views/settings.json b/web/public/locales/fa/views/settings.json new file mode 100644 index 0000000..3465fbf --- /dev/null +++ b/web/public/locales/fa/views/settings.json @@ -0,0 +1,1070 @@ +{ + "documentTitle": { + "default": "تنظیمات - فریگیت", + "authentication": "تنظیمات احراز هویت - فریگیت", + "camera": "تنظیمات دوربین - فریگیت", + "cameraManagement": "مدیریت دوربین ها - فریگیت", + "cameraReview": "بازبینی تنظیمات دوربین - فریگیت", + "masksAndZones": "ویرایشگر ماسک و منطقه - فریگیت", + "enrichments": "تنظیمات غنی‌سازی‌ها - Frigate", + "motionTuner": "تنظیم‌کنندهٔ حرکت - Frigate", + "object": "اشکال‌زدایی - Frigate", + "general": "تنظیمات رابط کاربری - فریگیت", + "frigatePlus": "تنظیمات Frigate+ - Frigate", + "notifications": "تنظیمات اعلان‌ها - Frigate" + }, + "menu": { + "ui": "رابط کاربری", + "enrichments": "غنی‌سازی‌ها", + "cameraManagement": "مدیریت", + "cameraReview": "بازبینی", + "masksAndZones": "ماسک‌ها / ناحیه‌ها", + "motionTuner": "تنظیم‌کنندهٔ حرکت", + "triggers": "محرک‌ها", + "debug": "اشکال‌زدایی", + "users": "کاربران", + "roles": "نقش‌ها", + "notifications": "اعلان‌ها", + "frigateplus": "فریگیت+", + "profiles": "پروفایل‌ها" + }, + "general": { + "title": "تنظیمات رابط کاربری", + "liveDashboard": { + "title": "داشبورد زنده", + "automaticLiveView": { + "label": "نمای زندهٔ خودکار", + "desc": "وقتی فعالیت تشخیص داده شود، به‌طور خودکار به نمای زندهٔ دوربین جابه‌جا شوید. غیرفعال کردن این گزینه باعث می‌شود تصاویر ثابت دوربین در داشبورد زنده فقط هر یک دقیقه یک‌بار به‌روزرسانی شوند." + }, + "playAlertVideos": { + "label": "پخش ویدیوهای هشدار", + "desc": "به‌طور پیش‌فرض، هشدارهای اخیر در داشبورد زنده به‌صورت ویدیوهای کوچکِ حلقه‌ای پخش می‌شوند. این گزینه را غیرفعال کنید تا فقط یک تصویر ثابت از هشدارهای اخیر در این دستگاه/مرورگر نمایش داده شود." + }, + "displayCameraNames": { + "label": "نمایش همیشهٔ نام دوربین‌ها", + "desc": "نام دوربین‌ها را همیشه به‌صورت یک برچسب در داشبورد نمای زندهٔ چند دوربینه نشان بده." + }, + "liveFallbackTimeout": { + "label": "مهلت بازگشت پخش زنده", + "desc": "وقتی پخش زندهٔ باکیفیتِ دوربین در دسترس نیست، پس از این تعداد ثانیه به حالت کم‌پهنای‌باند برگردد. پیش‌فرض: ۳." + } + }, + "storedLayouts": { + "title": "چیدمان‌های ذخیره‌شده", + "desc": "چیدمان دوربین‌ها در یک گروه دوربین قابل کشیدن و تغییر اندازه است. موقعیت‌ها در فضای ذخیره‌سازی محلی مرورگر شما ذخیره می‌شوند.", + "clearAll": "پاک کردن همهٔ چیدمان‌ها" + }, + "cameraGroupStreaming": { + "title": "تنظیمات پخش گروه دوربین", + "desc": "تنظیمات پخش برای هر گروه دوربین در فضای ذخیره‌سازی محلی مرورگر شما ذخیره می‌شود.", + "clearAll": "پاک کردن همهٔ تنظیمات پخش" + }, + "recordingsViewer": { + "title": "نمایشگر ضبط‌ها", + "defaultPlaybackRate": { + "label": "نرخ پخش پیش‌فرض", + "desc": "نرخ پخش پیش‌فرض برای پخش ضبط‌ها." + } + }, + "calendar": { + "title": "تقویم", + "firstWeekday": { + "label": "اولین روز هفته", + "desc": "روزی که هفته‌های تقویمِ بازبینی از آن آغاز می‌شوند.", + "sunday": "یکشنبه", + "monday": "دوشنبه" + } + }, + "toast": { + "success": { + "clearStoredLayout": "چیدمان ذخیره‌شده برای {{cameraName}} پاک شد", + "clearStreamingSettings": "تنظیمات پخش برای همهٔ گروه‌های دوربین پاک شد." + }, + "error": { + "clearStoredLayoutFailed": "پاک کردن چیدمان ذخیره‌شده ناموفق بود: {{errorMessage}}", + "clearStreamingSettingsFailed": "پاک کردن تنظیمات پخش ناموفق بود: {{errorMessage}}" + } + } + }, + "dialog": { + "unsavedChanges": { + "title": "تغییرات ذخیره‌نشده دارید.", + "desc": "آیا می‌خواهید پیش از ادامه، تغییرات خود را ذخیره کنید؟" + } + }, + "cameraSetting": { + "camera": "دوربین", + "noCamera": "بدون دوربین" + }, + "enrichments": { + "unsavedChanges": "تغییرات ذخیره‌نشدهٔ تنظیمات غنی‌سازی", + "birdClassification": { + "desc": "طبقه‌بندی پرندگان با استفاده از یک مدل Tensorflow کوانتیزه‌شده، پرندگان شناخته‌شده را شناسایی می‌کند. وقتی یک پرندهٔ شناخته‌شده شناسایی شود، نام رایج آن به‌عنوان sub_label اضافه می‌شود. این اطلاعات در رابط کاربری، فیلترها و همچنین در اعلان‌ها گنجانده می‌شود.", + "title": "طبقه‌بندی پرندگان" + }, + "semanticSearch": { + "desc": "جست‌وجوی معنایی در Frigate به شما اجازه می‌دهد اشیای ردیابی‌شده را در آیتم‌های بازبینی، با استفاده از خودِ تصویر، یک توضیح متنیِ تعریف‌شده توسط کاربر، یا یک توضیحِ تولیدشدهٔ خودکار پیدا کنید.", + "reindexNow": { + "confirmTitle": "تأیید بازنمایه‌سازی", + "confirmButton": "بازنمایه‌سازی", + "alreadyInProgress": "بازنمایه‌سازی از قبل در حال انجام است.", + "label": "بازنمایه‌سازی اکنون", + "desc": "بازنمایه‌سازی، امبدینگ‌ها را برای همهٔ اشیای ردیابی‌شده دوباره تولید می‌کند. این فرایند در پس‌زمینه اجرا می‌شود و بسته به تعداد اشیای ردیابی‌شده‌ای که دارید، ممکن است CPU شما را به سقف برساند و زمان قابل‌توجهی طول بکشد.", + "confirmDesc": "آیا مطمئن هستید که می‌خواهید همهٔ امبدینگ‌های اشیای ردیابی‌شده را بازنمایه‌سازی کنید؟ این فرایند در پس‌زمینه اجرا می‌شود، اما ممکن است CPU شما را به سقف برساند و زمان قابل‌توجهی طول بکشد. می‌توانید پیشرفت را در صفحهٔ Explore مشاهده کنید.", + "success": "بازنمایه‌سازی با موفقیت شروع شد.", + "error": "شروع بازنمایه‌سازی ناموفق بود: {{errorMessage}}" + }, + "modelSize": { + "label": "اندازهٔ مدل", + "desc": "اندازهٔ مدلی که برای بردارهای جست‌وجوی معنایی استفاده می‌شود.", + "small": { + "desc": "استفاده از small از نسخهٔ کوانتیزهٔ مدل استفاده می‌کند که RAM کم‌تری مصرف می‌کند و روی CPU سریع‌تر اجرا می‌شود، با تفاوت بسیار ناچیز در کیفیت داخلی.", + "title": "کوچک" + }, + "large": { + "desc": "استفاده از large از مدل کامل Jina استفاده می‌کند و در صورت امکان به‌طور خودکار روی GPU اجرا می‌شود.", + "title": "بزرگ" + } + }, + "title": "جستجوی معنایی" + }, + "faceRecognition": { + "desc": "تشخیص چهره امکان می‌دهد برای افراد نام تعیین شود و وقتی چهرهٔ آن‌ها شناسایی شود، Frigate نام فرد را به‌عنوان زیر‌برچسب اختصاص می‌دهد. این اطلاعات در رابط کاربری، فیلترها و همچنین در اعلان‌ها گنجانده می‌شود.", + "modelSize": { + "label": "اندازهٔ مدل", + "small": { + "title": "کوچک", + "desc": "استفاده از کوچک یک مدل امبدینگ چهرهٔ FaceNet را به‌کار می‌گیرد که روی بیشتر CPUها به‌صورت بهینه اجرا می‌شود." + }, + "large": { + "title": "بزرگ", + "desc": "استفاده از large از مدل embedding چهرهٔ ArcFace استفاده می‌کند و در صورت امکان به‌طور خودکار روی GPU اجرا می‌شود." + }, + "desc": "اندازه مدل مورد استفاده برای تشخیص چهره." + }, + "title": "شناسایی چهره" + }, + "licensePlateRecognition": { + "desc": "Frigate می‌تواند پلاک خودروها را تشخیص دهد و نویسه‌های شناسایی‌شده را به‌طور خودکار به فیلد recognized_license_plate اضافه کند، یا یک نام شناخته‌شده را به‌عنوان sub_label به اشیایی که از نوع car هستند اضافه کند. یک کاربرد رایج می‌تواند خواندن پلاک خودروهایی باشد که وارد پارکینگ/حیاط می‌شوند یا خودروهایی که از خیابان عبور می‌کنند.", + "title": "شناسایی پلاک خودرو" + }, + "toast": { + "success": "تنظیمات غنی‌سازی ذخیره شد. برای اعمال تغییرات، Frigate را دوباره راه‌اندازی کنید.", + "error": "ذخیرهٔ تغییرات پیکربندی ناموفق بود: {{errorMessage}}" + }, + "title": "تنظیمات غنی‌سازی‌ها", + "restart_required": "نیاز به راه‌اندازی مجدد (تنظیمات غنی‌سازی‌ها تغییر کرد)" + }, + "cameraWizard": { + "description": "برای افزودن یک دوربین جدید به نصب Frigate خود، مراحل زیر را دنبال کنید.", + "steps": { + "streamConfiguration": "پیکربندی استریم", + "nameAndConnection": "نام و اتصال", + "probeOrSnapshot": "پروب یا اسنپ‌شات", + "validationAndTesting": "اعتبارسنجی و آزمون" + }, + "save": { + "success": "دوربین جدید {{cameraName}} با موفقیت ذخیره شد.", + "failure": "خطا در ذخیرهٔ {{cameraName}}." + }, + "testResultLabels": { + "video": "ویدئو", + "audio": "صدا", + "fps": "FPS", + "resolution": "وضوح" + }, + "commonErrors": { + "noUrl": "لطفاً یک URL معتبر برای استریم ارائه کنید", + "testFailed": "آزمون استریم ناموفق بود: {{error}}" + }, + "step1": { + "cameraName": "نام دوربین", + "port": "پورت", + "password": "گذرواژه", + "cameraBrand": "برند دوربین", + "customUrl": "URL سفارشی استریم", + "brandInformation": "اطلاعات برند", + "customUrlPlaceholder": "rtsp://نام‌کاربری:رمز@سرور:پورت/مسیر", + "connectionSettings": "تنظیمات اتصال", + "probeMode": "پروبِ دوربین", + "onvifPortDescription": "برای دوربین‌هایی که از ONVIF پشتیبانی می‌کنند، معمولاً ۸۰ یا ۸۰۸۰ است.", + "useDigestAuth": "استفاده از احراز هویت Digest", + "description": "جزئیات دوربین خود را وارد کنید و انتخاب کنید دوربین بررسی شود یا برند را به‌صورت دستی انتخاب کنید.", + "cameraNamePlaceholder": "مثلاً front_door یا Back Yard Overview", + "host": "میزبان/آدرس IP", + "username": "نام کاربری", + "usernamePlaceholder": "اختیاری", + "passwordPlaceholder": "اختیاری", + "selectTransport": "انتخاب پروتکل انتقال", + "selectBrand": "برند دوربین را برای قالب URL انتخاب کنید", + "brandUrlFormat": "برای دوربین‌هایی با قالب URL ‏RTSP به‌شکل: {{exampleUrl}}", + "detectionMethod": "روش تشخیص جریان", + "onvifPort": "پورت ONVIF", + "manualMode": "انتخاب دستی", + "detectionMethodDescription": "دوربین را با ONVIF (در صورت پشتیبانی) بررسی کنید تا URLهای جریان دوربین پیدا شوند، یا برند دوربین را به‌صورت دستی انتخاب کنید تا از URLهای ازپیش‌تعریف‌شده استفاده شود. برای وارد کردن یک URL سفارشی RTSP، روش دستی را انتخاب کنید و «Other» را برگزینید.", + "useDigestAuthDescription": "برای ONVIF از احراز هویت Digest‏ HTTP استفاده کنید. برخی دوربین‌ها ممکن است به‌جای کاربر مدیر استاندارد، به یک نام‌کاربری/گذرواژهٔ اختصاصی ONVIF نیاز داشته باشند.", + "errors": { + "brandOrCustomUrlRequired": "یا یک برند دوربین را همراه با میزبان/آدرس IP انتخاب کنید یا «Other» را با یک URL سفارشی برگزینید", + "nameRequired": "نام دوربین الزامی است", + "nameLength": "نام دوربین باید ۶۴ کاراکتر یا کمتر باشد", + "invalidCharacters": "نام دوربین شامل نویسه‌های نامعتبر است", + "nameExists": "نام دوربین از قبل وجود دارد", + "customUrlRtspRequired": "URLهای سفارشی باید با «rtsp://» شروع شوند. برای جریان‌های دوربینِ غیر RTSP پیکربندی دستی لازم است." + } + }, + "title": "افزودن دوربین", + "step2": { + "description": "دوربین را برای جریان‌های در دسترس بررسی کنید یا بر اساس روش تشخیصِ انتخاب‌شده، تنظیمات دستی را پیکربندی کنید.", + "testSuccess": "آزمون اتصال با موفقیت انجام شد!", + "testFailed": "آزمون اتصال ناموفق بود. لطفاً ورودی‌های خود را بررسی کنید و دوباره تلاش کنید.", + "testFailedTitle": "آزمون ناموفق", + "streamDetails": "جزئیات جریان", + "probing": "در حال بررسی دوربین…", + "retry": "تلاش مجدد", + "testing": { + "probingMetadata": "در حال بررسی فرادادهٔ دوربین…", + "fetchingSnapshot": "در حال دریافت عکس فوریِ دوربین…" + }, + "probeFailed": "بررسی دوربین ناموفق بود: {{error}}", + "probingDevice": "در حال بررسی دستگاه…", + "probeSuccessful": "بررسی موفق", + "probeError": "خطای بررسی", + "probeNoSuccess": "بررسی ناموفق", + "deviceInfo": "اطلاعات دستگاه", + "manufacturer": "سازنده", + "model": "مدل", + "firmware": "فرم‌ور", + "profiles": "پروفایل‌ها", + "ptzSupport": "پشتیبانی PTZ", + "autotrackingSupport": "پشتیبانی از ردیابی خودکار", + "presets": "پیش‌تنظیم‌ها", + "rtspCandidates": "کاندیداهای RTSP", + "rtspCandidatesDescription": "URLهای RTSP زیر از بررسی دوربین به‌دست آمد. برای مشاهدهٔ فرادادهٔ جریان، اتصال را آزمایش کنید.", + "noRtspCandidates": "هیچ URL ‏RTSPای از دوربین پیدا نشد. ممکن است اطلاعات کاربری شما نادرست باشد، یا دوربین از ONVIF یا روشِ استفاده‌شده برای بازیابی URLهای RTSP پشتیبانی نکند. برگردید و URL ‏RTSP را به‌صورت دستی وارد کنید.", + "candidateStreamTitle": "کاندیدا {{number}}", + "useCandidate": "استفاده", + "uriCopy": "کپی", + "uriCopied": "نشانی URI در کلیپ‌بورد کپی شد", + "testConnection": "آزمون اتصال", + "toggleUriView": "برای تغییر به نمایش کامل URI کلیک کنید", + "connected": "متصل", + "notConnected": "متصل نیست", + "errors": { + "hostRequired": "میزبان/آدرس IP الزامی است" + } + }, + "step3": { + "description": "نقش‌های جریان را پیکربندی کنید و برای دوربین خود جریان‌های بیشتری اضافه کنید.", + "streamsTitle": "جریان‌های دوربین", + "addStream": "افزودن جریان", + "addAnotherStream": "افزودن جریان دیگر", + "streamTitle": "جریان {{number}}", + "streamUrl": "نشانی جریان", + "streamUrlPlaceholder": "rtsp://نام‌کاربری:رمز@سرور:پورت/مسیر", + "selectStream": "یک جریان را انتخاب کنید", + "searchCandidates": "جستجوی گزینه‌ها…", + "noStreamFound": "هیچ جریانی پیدا نشد", + "url": "نشانی URL", + "resolution": "وضوح", + "selectResolution": "انتخاب وضوح", + "quality": "کیفیت", + "selectQuality": "انتخاب کیفیت", + "roles": "نقش‌ها", + "roleLabels": { + "detect": "تشخیص شیء", + "record": "ضبط", + "audio": "صدا" + }, + "testStream": "آزمون اتصال", + "testSuccess": "آزمون جریان با موفقیت انجام شد!", + "testFailed": "آزمون جریان ناموفق بود", + "testFailedTitle": "آزمون ناموفق بود", + "connected": "متصل", + "notConnected": "متصل نیست", + "featuresTitle": "ویژگی‌ها", + "go2rtc": "کاهش تعداد اتصال‌ها به دوربین", + "detectRoleWarning": "برای ادامه، حداقل یک جریان باید نقش «detect» داشته باشد.", + "rolesPopover": { + "title": "نقش‌های جریان", + "detect": "فید اصلی برای تشخیص شیء.", + "record": "بر اساس تنظیمات پیکربندی، بخش‌هایی از فید ویدیو را ذخیره می‌کند.", + "audio": "فید برای تشخیص مبتنی بر صدا." + }, + "featuresPopover": { + "title": "ویژگی‌های جریان", + "description": "برای کاهش تعداد اتصال‌ها به دوربین خود از بازپخش go2rtc استفاده کنید." + } + }, + "step4": { + "validationTitle": "اعتبارسنجی جریان", + "connectAllStreams": "اتصال همهٔ جریان‌ها", + "reconnectionSuccess": "اتصال مجدد با موفقیت انجام شد.", + "reconnectionPartial": "اتصال مجدد برخی جریان‌ها ناموفق بود.", + "streamUnavailable": "پیش‌نمایش جریان در دسترس نیست", + "reload": "بارگذاری مجدد", + "streamTitle": "جریان {{number}}", + "valid": "معتبر", + "failed": "ناموفق", + "notTested": "آزمون نشده", + "connectStream": "اتصال", + "connectingStream": "در حال اتصال", + "disconnectStream": "قطع اتصال", + "estimatedBandwidth": "پهنای باند تخمینی", + "roles": "نقش‌ها", + "ffmpegModule": "استفاده از حالت سازگاری جریان", + "ffmpegModuleDescription": "اگر جریان پس از چند تلاش بارگذاری نشد، فعال‌کردن این گزینه را امتحان کنید. وقتی فعال باشد، Frigate از ماژول ffmpeg همراه با go2rtc استفاده می‌کند. این کار ممکن است با برخی جریان‌های دوربین سازگاری بهتری فراهم کند.", + "none": "هیچ‌کدام", + "error": "خطا", + "streamValidated": "اعتبارسنجی جریان {{number}} با موفقیت انجام شد", + "streamValidationFailed": "اعتبارسنجی جریان {{number}} ناموفق بود", + "saveAndApply": "ذخیرهٔ دوربین جدید", + "saveError": "پیکربندی نامعتبر است. لطفاً تنظیمات خود را بررسی کنید.", + "issues": { + "title": "اعتبارسنجی جریان", + "videoCodecGood": "کدک ویدیو {{codec}} است.", + "audioCodecGood": "کدک صدا {{codec}} است.", + "resolutionHigh": "وضوح {{resolution}} ممکن است باعث افزایش مصرف منابع شود.", + "resolutionLow": "وضوح {{resolution}} ممکن است برای تشخیص قابل‌اعتماد اشیای کوچک بیش از حد پایین باشد.", + "noAudioWarning": "برای این جریان صدایی شناسایی نشد؛ ضبط‌ها صدا نخواهند داشت.", + "audioCodecRecordError": "برای پشتیبانی از صدا در ضبط‌ها، کدک صوتی AAC لازم است.", + "audioCodecRequired": "برای پشتیبانی از تشخیص صدا، یک جریان صوتی لازم است.", + "restreamingWarning": "کاهش تعداد اتصال‌ها به دوربین برای جریان ضبط ممکن است کمی مصرف CPU را افزایش دهد.", + "brands": { + "reolink-rtsp": "RTSP در Reolink توصیه نمی‌شود. در تنظیمات میان‌افزار دوربین، HTTP را فعال کنید و جادوگر را دوباره اجرا کنید.", + "reolink-http": "جریان‌های HTTP در Reolink برای سازگاری بهتر باید از FFmpeg استفاده کنند. برای این جریان، «استفاده از حالت سازگاری جریان» را فعال کنید." + }, + "dahua": { + "substreamWarning": "زیرجریان ۱ روی وضوح پایین قفل شده است. بسیاری از دوربین‌های Dahua / Amcrest / EmpireTech از زیرجریان‌های اضافی پشتیبانی می‌کنند که باید در تنظیمات دوربین فعال شوند. توصیه می‌شود در صورت وجود، آن جریان‌ها را بررسی کرده و استفاده کنید." + }, + "hikvision": { + "substreamWarning": "زیرجریان ۱ روی وضوح پایین قفل شده است. بسیاری از دوربین‌های Hikvision از زیرجریان‌های اضافی پشتیبانی می‌کنند که باید در تنظیمات دوربین فعال شوند. توصیه می‌شود در صورت وجود، آن جریان‌ها را بررسی کرده و استفاده کنید." + } + }, + "connecting": "در حال اتصال...", + "description": "پیش از ذخیره کردن دوربین جدیدتان، اعتبارسنجی و تحلیل نهایی انجام می‌شود. پیش از ذخیره، هر استریم را متصل کنید." + } + }, + "cameraManagement": { + "title": "مدیریت دوربین‌ها", + "addCamera": "افزودن دوربین جدید", + "selectCamera": "یک دوربین را انتخاب کنید", + "backToSettings": "بازگشت به تنظیمات دوربین", + "streams": { + "title": "فعال‌سازی / غیرفعال‌سازی دوربین‌ها", + "desc": "یک دوربین را تا زمانی که Frigate دوباره راه‌اندازی شود، موقتاً غیرفعال کنید. غیرفعال‌کردن یک دوربین باعث می‌شود پردازش جریان‌های این دوربین توسط Frigate کاملاً متوقف شود. تشخیص، ضبط و اشکال‌زدایی در دسترس نخواهد بود.
    نکته: این کار بازپخش‌های go2rtc را غیرفعال نمی‌کند." + }, + "cameraConfig": { + "add": "افزودن دوربین", + "edit": "ویرایش دوربین", + "description": "تنظیمات دوربین از جمله ورودی‌های جریان و نقش‌ها را پیکربندی کنید.", + "name": "نام دوربین", + "nameLength": "نام دوربین باید کمتر از ۶۴ کاراکتر باشد.", + "nameRequired": "نام دوربین الزامی است", + "namePlaceholder": "مثلاً front_door یا Back Yard Overview", + "enabled": "فعال", + "ffmpeg": { + "inputs": "جریان‌های ورودی", + "path": "مسیر جریان", + "pathRequired": "مسیر جریان الزامی است", + "pathPlaceholder": "rtsp://...", + "roles": "نقش‌ها", + "rolesRequired": "حداقل یک نقش لازم است", + "rolesUnique": "هر نقش (audio، detect، record) فقط می‌تواند به یک جریان اختصاص داده شود", + "addInput": "افزودن جریان ورودی", + "removeInput": "حذف جریان ورودی", + "inputsRequired": "حداقل یک جریان ورودی لازم است" + }, + "go2rtcStreams": "جریان‌های go2rtc", + "streamUrls": "نشانی‌های جریان", + "addGo2rtcStream": "افزودن جریان go2rtc", + "toast": { + "success": "دوربین {{cameraName}} با موفقیت ذخیره شد" + }, + "addUrl": "افزودن نشانی" + }, + "editCamera": "ویرایش دوربین:" + }, + "cameraReview": { + "title": "تنظیمات بازبینی دوربین", + "object_descriptions": { + "title": "توضیحات شیء با هوش مصنوعی مولد", + "desc": "موقتاً توضیحات اشیای هوش مصنوعی مولد را برای این دوربین فعال/غیرفعال کنید. وقتی غیرفعال باشد، برای اشیای ردیابی‌شده در این دوربین، توضیحات تولیدشده با هوش مصنوعی درخواست نخواهد شد." + }, + "reviewClassification": { + "title": "طبقه‌بندی بازبینی", + "desc": "Frigate موارد بازبینی را به‌عنوان اعلان‌ها و تشخیص‌ها دسته‌بندی می‌کند. به‌طور پیش‌فرض، همهٔ اشیای person و car به‌عنوان اعلان در نظر گرفته می‌شوند. می‌توانید با پیکربندی نواحی لازم برای آن‌ها، طبقه‌بندی موارد بازبینی خود را دقیق‌تر کنید.", + "noDefinedZones": "هیچ ناحیه‌ای برای این دوربین تعریف نشده است.", + "objectAlertsTips": "همهٔ اشیای {{alertsLabels}} در {{cameraName}} به‌صورت اعلان نمایش داده می‌شوند.", + "zoneObjectAlertsTips": "همهٔ اشیای {{alertsLabels}} که در {{zone}} روی {{cameraName}} تشخیص داده می‌شوند، به‌صورت اعلان نمایش داده خواهند شد.", + "selectAlertsZones": "ناحیه‌ها را برای اعلان‌ها انتخاب کنید", + "selectDetectionsZones": "ناحیه‌ها را برای تشخیص‌ها انتخاب کنید", + "limitDetections": "تشخیص‌ها را به نواحی مشخص محدود کنید", + "toast": { + "success": "پیکربندی طبقه‌بندی بازبینی ذخیره شد. برای اعمال تغییرات، Frigate را راه‌اندازی مجدد کنید." + }, + "objectDetectionsTips": "همهٔ اشیای {{detectionsLabels}} که در {{cameraName}} دسته‌بندی نشده‌اند، صرف‌نظر از اینکه در کدام ناحیه هستند، به‌صورت «تشخیص‌ها» نمایش داده می‌شوند.", + "zoneObjectDetectionsTips": { + "text": "همهٔ اشیای {{detectionsLabels}} که در {{zone}} برای {{cameraName}} دسته‌بندی نشده‌اند، به‌صورت «تشخیص‌ها» نمایش داده می‌شوند.", + "notSelectDetections": "همهٔ اشیای {{detectionsLabels}} که در {{zone}} روی {{cameraName}} شناسایی شده‌اند و به‌عنوان «هشدار» دسته‌بندی نشده‌اند، صرف‌نظر از اینکه در کدام ناحیه هستند، به‌صورت «تشخیص‌ها» نمایش داده می‌شوند.", + "regardlessOfZoneObjectDetectionsTips": "همهٔ اشیای {{detectionsLabels}} که در {{cameraName}} دسته‌بندی نشده‌اند، بدون توجه به این‌که در کدام ناحیه هستند، به‌صورت «تشخیص‌ها» نمایش داده خواهند شد." + }, + "unsavedChanges": "تنظیمات ذخیره‌نشدهٔ طبقه‌بندی بازبینی برای {{camera}}" + }, + "review_descriptions": { + "title": "توضیحات بازبینیِ هوش مصنوعی مولد", + "desc": "توضیحات بازبینیِ هوش مصنوعی مولد را برای این دوربین به‌طور موقت فعال/غیرفعال کنید. وقتی غیرفعال باشد، برای موارد بازبینی این دوربین، توضیحات تولیدشده توسط هوش مصنوعی درخواست نخواهد شد." + }, + "review": { + "title": "بازبینی", + "desc": "هشدارها و تشخیص‌ها را برای این دوربین تا زمان راه‌اندازی مجدد Frigate به‌طور موقت فعال/غیرفعال کنید. وقتی غیرفعال باشد، هیچ مورد بازبینی جدیدی ایجاد نخواهد شد. ", + "alerts": "هشدارها ", + "detections": "تشخیص‌ها " + } + }, + "masksAndZones": { + "filter": { + "all": "همهٔ ماسک‌ها و ناحیه‌ها" + }, + "form": { + "zoneName": { + "error": { + "mustNotBeSameWithCamera": "نام ناحیه نباید با نام دوربین یکسان باشد.", + "alreadyExists": "ناحیه‌ای با این نام از قبل برای این دوربین وجود دارد.", + "mustNotContainPeriod": "نام ناحیه نباید شامل نقطه باشد.", + "hasIllegalCharacter": "نام ناحیه شامل نویسه‌های غیرمجاز است.", + "mustHaveAtLeastOneLetter": "نام ناحیه باید حداقل یک حرف داشته باشد.", + "mustBeAtLeastTwoCharacters": "نام ناحیه باید حداقل ۲ کاراکتر باشد." + } + }, + "distance": { + "error": { + "text": "فاصله باید بزرگ‌تر یا مساوی 0.1 باشد.", + "mustBeFilled": "همهٔ فیلدهای فاصله باید پر شوند تا بتوان از تخمین سرعت استفاده کرد." + } + }, + "polygonDrawing": { + "reset": { + "label": "پاک کردن همهٔ نقاط" + }, + "snapPoints": { + "true": "چسباندن به نقاط", + "false": "چسباندن به نقاط انجام نشود" + }, + "delete": { + "title": "تأیید حذف", + "desc": "آیا مطمئن هستید که می‌خواهید {{type}} {{name}} را حذف کنید؟", + "success": "{{name}} حذف شد." + }, + "removeLastPoint": "حذف آخرین نقطه", + "error": { + "mustBeFinished": "رسم چندضلعی باید قبل از ذخیره کامل شود." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "لختی باید بیشتر از ۰ باشد." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "زمان توقف باید بیشتر از یا مساوی ۰ باشد." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "آستانهٔ سرعت باید بیشتر از یا مساوی ۰.۱ باشد." + } + } + }, + "zones": { + "add": "افزودن ناحیه", + "edit": "ویرایش ناحیه", + "point_one": "{{count}} نقطه", + "point_other": "{{count}} نقطه ها", + "clickDrawPolygon": "برای رسم یک چندضلعی روی تصویر کلیک کنید.", + "loiteringTime": { + "desc": "یک حداقل زمان (به ثانیه) تعیین می‌کند که شیء باید در ناحیه باشد تا فعال شود. پیش‌فرض: 0 ", + "title": "زمان توقف" + }, + "objects": { + "title": "اشیا", + "desc": "فهرست اشیایی که برای این ناحیه اعمال می‌شوند." + }, + "allObjects": "همهٔ اشیا", + "speedEstimation": { + "title": "تخمین سرعت", + "desc": "فعال‌سازی تخمین سرعت برای اشیا در این ناحیه. ناحیه باید دقیقاً ۴ نقطه داشته باشد.", + "lineADistance": "فاصلهٔ خط A ( {{unit}})", + "lineBDistance": "فاصلهٔ خط B ( {{unit}})", + "lineCDistance": "فاصلهٔ خط C ( {{unit}})", + "lineDDistance": "فاصلهٔ خط D ( {{unit}})" + }, + "speedThreshold": { + "title": "آستانهٔ سرعت ( {{unit}})", + "desc": "حداقل سرعتی را مشخص می‌کند تا اشیا در این ناحیه در نظر گرفته شوند.", + "toast": { + "error": { + "pointLengthError": "تخمین سرعت برای این ناحیه غیرفعال شد. ناحیه‌هایی که تخمین سرعت دارند باید دقیقاً ۴ نقطه داشته باشند.", + "loiteringTimeError": "ناحیه‌هایی با زمان پرسه‌زنیِ بیشتر از ۰ نباید با تخمین سرعت استفاده شوند." + } + } + }, + "toast": { + "success": "ناحیه ( {{zoneName}}) ذخیره شد." + }, + "label": "ناحیه‌ها", + "documentTitle": "ویرایش ناحیه - Frigate", + "desc": { + "title": "ناحیه‌ها به شما امکان تعریف یک ناحیهٔ مشخص از فریم را می‌دهند تا بتوانید تعیین کنید که آیا یک شیء در یک ناحیهٔ خاص قرار دارد یا خیر.", + "documentation": "مستندات" + }, + "name": { + "title": "نام", + "inputPlaceHolder": "یک نام وارد کنید…", + "tips": "نام باید حداقل ۲ کاراکتر باشد، باید حداقل یک حرف داشته باشد، و نباید نام یک دوربین یا ناحیهٔ دیگری در این دوربین باشد." + }, + "inertia": { + "title": "لختی", + "desc": "تعداد فریم‌هایی را مشخص می‌کند که یک شیء باید در یک ناحیه باشد تا در آن ناحیه محسوب شود. پیش‌فرض: ۳" + } + }, + "motionMasks": { + "label": "ماسک حرکت", + "context": { + "title": "ماسک‌های حرکت برای جلوگیری از این‌که انواع ناخواستهٔ حرکت باعث فعال‌شدن تشخیص شوند استفاده می‌شوند (مثلاً شاخه‌های درخت، مهر زمانیِ دوربین). ماسک‌های حرکت باید با نهایت صرفه‌جویی استفاده شوند؛ ماسک‌گذاریِ بیش‌ازحد باعث می‌شود ردیابی اشیا دشوارتر شود." + }, + "point_one": "{{count}} نقطه", + "point_other": "{{count}} نقطه ها", + "clickDrawPolygon": "برای رسم یک چندضلعی روی تصویر کلیک کنید.", + "polygonAreaTooLarge": { + "title": "ماسک حرکت {{polygonArea}}٪ از قاب دوربین را پوشش می‌دهد. ماسک‌های حرکتِ بزرگ توصیه نمی‌شوند.", + "tips": "ماسک‌های حرکت مانعِ تشخیص اشیا نمی‌شوند. به‌جای آن باید از «ناحیهٔ الزامی» استفاده کنید." + }, + "add": "ماسک حرکت جدید", + "edit": "ویرایش ماسک حرکت", + "toast": { + "success": { + "title": "{{polygonName}} ذخیره شد.", + "noName": "ماسک حرکت ذخیره شد." + } + }, + "documentTitle": "ویرایش ماسک حرکت - Frigate", + "desc": { + "title": "ماسک‌های حرکت برای جلوگیری از فعال‌سازی تشخیص توسط انواع ناخواستهٔ حرکت استفاده می‌شوند. ماسک‌گذاری بیش‌ازحد ردیابی اشیا را دشوارتر می‌کند.", + "documentation": "مستندات" + } + }, + "objectMasks": { + "desc": { + "documentation": "مستندات", + "title": "ماسک‌های فیلترِ اشیا برای فیلتر کردن مثبت‌های کاذبِ یک نوع شیء مشخص بر اساس موقعیت استفاده می‌شوند." + }, + "add": "افزودن ماسک شیء", + "edit": "ویرایش ماسک شیء", + "context": "ماسک‌های فیلترِ شیء برای فیلتر کردن مثبت‌های کاذب برای یک نوع شیء مشخص بر اساس موقعیت استفاده می‌شوند.", + "point_one": "{{count}} نقطه", + "point_other": "{{count}} نقطه ها", + "clickDrawPolygon": "برای رسم یک چندضلعی روی تصویر کلیک کنید.", + "toast": { + "success": { + "noName": "ماسک شیء ذخیره شد.", + "title": "{{polygonName}} ذخیره شد." + } + }, + "label": "ماسک‌های شیء", + "documentTitle": "ویرایش ماسک شیء - Frigate", + "objects": { + "title": "اشیا", + "desc": "نوع شیئی که به این ماسک شیء مربوط می‌شود.", + "allObjectTypes": "همهٔ انواع شیء" + } + }, + "restart_required": "نیاز به راه‌اندازی مجدد (ماسک‌ها/ناحیه‌ها تغییر کرده‌اند)", + "toast": { + "success": { + "copyCoordinates": "مختصات {{polyName}} در کلیپ‌بورد کپی شد." + }, + "error": { + "copyCoordinatesFailed": "امکان کپی کردن مختصات در کلیپ‌بورد نبود." + } + }, + "motionMaskLabel": "ماسک حرکت {{number}}", + "objectMaskLabel": "ماسک شیء {{number}}" + }, + "motionDetectionTuner": { + "title": "تنظیم‌گر تشخیص حرکت", + "unsavedChanges": "تغییرات ذخیره‌نشدهٔ تنظیم‌گر تشخیص حرکت ( {{camera}})", + "desc": { + "title": "Frigate از تشخیص حرکت به‌عنوان نخستین بررسی استفاده می‌کند تا ببیند آیا در قاب چیزی رخ می‌دهد که ارزش بررسی با تشخیص شیء را داشته باشد یا نه.", + "documentation": "راهنمای تنظیم تشخیص حرکت را بخوانید" + }, + "improveContrast": { + "desc": "بهبود کنتراست برای صحنه‌های تاریک‌تر. پیش‌فرض: روشن ", + "title": "بهبود کنتراست" + }, + "toast": { + "success": "تنظیمات حرکت ذخیره شد." + }, + "Threshold": { + "title": "آستانه", + "desc": "مقدار آستانه تعیین می‌کند برای اینکه تغییر روشناییِ یک پیکسل «حرکت» محسوب شود، چه میزان تغییر لازم است. پیش‌فرض: 30" + }, + "contourArea": { + "title": "مساحت کانتور", + "desc": "مقدار مساحت کانتور برای تعیین اینکه کدام گروه‌های پیکسل‌های تغییر‌یافته به‌عنوان حرکت محسوب می‌شوند استفاده می‌شود. پیش‌فرض: ۱۰" + } + }, + "debug": { + "title": "اشکال‌زدایی", + "detectorDesc": "Frigate از آشکارسازهای شما ( {{detectors}}) برای تشخیص اشیا در جریان ویدیوی دوربین شما استفاده می‌کند.", + "desc": "نمای اشکال‌زدایی، نمایی بلادرنگ از اشیای ردیابی‌شده و آمار آن‌ها را نشان می‌دهد. فهرست اشیا یک خلاصهٔ با تأخیر زمانی از اشیای تشخیص‌داده‌شده را نمایش می‌دهد.", + "audio": { + "score": "امتیاز", + "currentRMS": "RMS فعلی", + "currentdbFS": "dbFS فعلی", + "title": "صدا", + "noAudioDetections": "هیچ تشخیص صدایی وجود ندارد" + }, + "boundingBoxes": { + "title": "کادرهای محدوده", + "desc": "نمایش جعبه‌های مرزی دور اشیای ردیابی‌شده", + "colors": { + "label": "رنگ‌های جعبهٔ مرزی شیء", + "info": "
  • در زمان راه‌اندازی، رنگ‌های مختلف به هر برچسب شیء اختصاص داده می‌شود
  • یک خط نازک آبی تیره نشان می‌دهد که شیء در این لحظه تشخیص داده نشده است
  • یک خط نازک خاکستری نشان می‌دهد که شیء به‌عنوان ساکن تشخیص داده شده است
  • یک خط ضخیم نشان می‌دهد که شیء موضوع ردیابی خودکار است (وقتی فعال باشد)
  • " + } + }, + "zones": { + "desc": "یک طرح کلی از هر ناحیهٔ تعریف‌شده را نمایش می‌دهد", + "title": "ناحیه‌ها" + }, + "mask": { + "title": "ماسک‌های حرکت", + "desc": "چندضلعی‌های ماسک حرکت را نشان می‌دهد" + }, + "motion": { + "title": "کادرهای حرکت", + "desc": "کادرهایی را پیرامون نواحی‌ای که در آن‌ها حرکت تشخیص داده می‌شود نشان می‌دهد", + "tips": "

    جعبه‌های حرکت


    جعبه‌های قرمز روی نواحی فریمی که در حال حاضر حرکت در آن‌ها تشخیص داده می‌شود نمایش داده می‌شوند

    " + }, + "paths": { + "desc": "نقاط مهم مسیر شیء ردیابی‌شده را نشان می‌دهد", + "tips": "

    مسیرها


    خط‌ها و دایره‌ها نقاط مهمی را که شیء ردیابی‌شده در طول چرخهٔ عمر خود طی کرده است نشان می‌دهند.

    ", + "title": "مسیرها" + }, + "objectShapeFilterDrawing": { + "title": "رسم فیلتر شکل شیء", + "desc": "برای مشاهدهٔ جزئیات مساحت و نسبت، روی تصویر یک مستطیل رسم کنید", + "tips": "این گزینه را فعال کنید تا بتوانید روی تصویر دوربین یک مستطیل رسم کنید و مساحت و نسبت آن را ببینید. سپس می‌توان از این مقادیر برای تنظیم پارامترهای فیلتر شکل شیء در پیکربندی شما استفاده کرد.", + "score": "امتیاز", + "ratio": "نسبت", + "area": "مساحت" + }, + "openCameraWebUI": "رابط وبِ {{camera}} را باز کنید", + "debugging": "انجام اشکال‌زدایی", + "objectList": "فهرست اشیا", + "noObjects": "هیچ شیئی وجود ندارد", + "timestamp": { + "title": "مهر زمان", + "desc": "نمایش مهر زمان روی تصویر" + }, + "regions": { + "title": "مناطق", + "desc": "نمایش جعبهٔ ناحیهٔ مورد علاقهٔ ارسال‌شده به تشخیص‌دهندهٔ شیء", + "tips": "

    جعبه‌های ناحیه


    جعبه‌های سبز روشن روی نواحی مورد علاقه در فریم که به تشخیص‌دهندهٔ شیء ارسال می‌شوند نمایش داده می‌شوند.

    " + } + }, + "users": { + "management": { + "desc": "حساب‌های کاربری این نمونهٔ Frigate را مدیریت کنید.", + "title": "مدیریت کاربران" + }, + "addUser": "افزودن کاربر", + "updatePassword": "بازنشانی گذرواژه", + "toast": { + "success": { + "createUser": "کاربر {{user}} با موفقیت ایجاد شد", + "deleteUser": "کاربر {{user}} با موفقیت حذف شد", + "updatePassword": "گذرواژه با موفقیت به‌روزرسانی شد.", + "roleUpdated": "نقش برای {{user}} به‌روزرسانی شد" + }, + "error": { + "setPasswordFailed": "ذخیرهٔ گذرواژه ناموفق بود: {{errorMessage}}", + "createUserFailed": "ایجاد کاربر ناموفق بود: {{errorMessage}}", + "deleteUserFailed": "حذف کاربر ناموفق بود: {{errorMessage}}", + "roleUpdateFailed": "به‌روزرسانی نقش ناموفق بود: {{errorMessage}}" + } + }, + "table": { + "changeRole": "تغییر نقش کاربر", + "password": "بازنشانی گذرواژه", + "deleteUser": "حذف کاربر", + "username": "نام کاربری", + "actions": "اقدامات", + "role": "نقش", + "noUsers": "هیچ کاربری یافت نشد." + }, + "dialog": { + "form": { + "user": { + "title": "نام کاربری", + "desc": "فقط حروف، اعداد، نقطه و زیرخط مجاز هستند.", + "placeholder": "نام کاربری را وارد کنید" + }, + "password": { + "confirm": { + "title": "تأیید گذرواژه", + "placeholder": "تأیید گذرواژه" + }, + "strength": { + "title": "قدرت گذرواژه: · ", + "weak": "ضعیف", + "medium": "متوسط", + "strong": "قوی", + "veryStrong": "خیلی قوی" + }, + "requirements": { + "digit": "حداقل یک رقم", + "special": "حداقل یک نویسهٔ ویژه (!@#$%^&*(),.?\":{}|<>)", + "title": "الزامات رمز عبور:", + "length": "حداقل ۸ کاراکتر", + "uppercase": "حداقل یک حرف بزرگ" + }, + "match": "گذرواژه‌ها مطابقت دارند", + "notMatch": "گذرواژه‌ها مطابقت ندارند", + "show": "نمایش رمز عبور", + "hide": "پنهان کردن رمز عبور", + "title": "رمز عبور", + "placeholder": "رمز عبور را وارد کنید" + }, + "newPassword": { + "title": "گذرواژهٔ جدید", + "confirm": { + "placeholder": "رمز عبور جدید را دوباره وارد کنید" + }, + "placeholder": "رمز عبور جدید را وارد کنید" + }, + "passwordIsRequired": "گذرواژه الزامی است", + "currentPassword": { + "title": "رمز عبور فعلی", + "placeholder": "رمز عبور فعلی خود را وارد کنید" + }, + "usernameIsRequired": "نام کاربری الزامی است" + }, + "createUser": { + "title": "ایجاد کاربر جدید", + "desc": "یک حساب کاربری جدید اضافه کنید و یک نقش برای دسترسی به بخش‌های رابط کاربری Frigate تعیین کنید.", + "usernameOnlyInclude": "نام کاربری فقط می‌تواند شامل حروف، اعداد، . یا _ باشد", + "confirmPassword": "لطفاً گذرواژهٔ خود را تأیید کنید" + }, + "passwordSetting": { + "currentPasswordRequired": "گذرواژهٔ فعلی الزامی است", + "incorrectCurrentPassword": "گذرواژهٔ فعلی نادرست است", + "passwordVerificationFailed": "اعتبارسنجی گذرواژه ناموفق بود", + "updatePassword": "به‌روزرسانی گذرواژه برای {{username}}", + "setPassword": "تنظیم گذرواژه", + "desc": "برای ایمن‌سازی این حساب، یک گذرواژهٔ قوی بسازید.", + "doNotMatch": "رمزهای عبور مطابقت ندارند", + "multiDeviceWarning": "هر دستگاه دیگری که در آن وارد شده‌اید باید ظرف {{refresh_time}} دوباره وارد شود.", + "multiDeviceAdmin": "همچنین می‌توانید با چرخش رمز JWT خود، همهٔ کاربران را فوراً مجبور به احراز هویت مجدد کنید.", + "cannotBeEmpty": "رمز عبور نمی‌تواند خالی باشد" + }, + "changeRole": { + "desc": "به‌روزرسانی مجوزها برای {{username}} ", + "roleInfo": { + "intro": "نقش مناسب برای این کاربر را انتخاب کنید:", + "admin": "مدیر", + "adminDesc": "دسترسی کامل به همهٔ قابلیت‌ها.", + "viewer": "بیننده", + "customDesc": "نقش سفارشی با دسترسی مشخص به دوربین.", + "viewerDesc": "محدود به داشبوردهای زنده، بررسی، کاوش و خروجی‌گیری فقط." + }, + "title": "تغییر نقش کاربر", + "select": "یک نقش انتخاب کنید" + }, + "deleteUser": { + "title": "حذف کاربر", + "desc": "این عمل قابل بازگشت نیست. این کار حساب کاربری را به‌طور دائم حذف می‌کند و همهٔ داده‌های مرتبط را حذف می‌کند.", + "warn": "آیا مطمئن هستید که می‌خواهید {{username}} را حذف کنید؟" + } + }, + "title": "کاربران" + }, + "roles": { + "table": { + "role": "نقش", + "cameras": "دوربین‌ها", + "actions": "اقدام‌ها", + "noRoles": "هیچ نقش سفارشی‌ای یافت نشد.", + "editCameras": "ویرایش دوربین‌ها", + "deleteRole": "حذف نقش" + }, + "toast": { + "success": { + "createRole": "نقش {{role}} با موفقیت ایجاد شد", + "updateCameras": "دوربین‌ها برای نقش {{role}} به‌روزرسانی شدند", + "deleteRole": "نقش {{role}} با موفقیت حذف شد", + "userRolesUpdated_one": "{{count}} کاربری که به این نقش اختصاص داده شده بود به «بیننده» تغییر یافت و اکنون به همهٔ دوربین‌ها دسترسی دارد.", + "userRolesUpdated_other": "{{count}} کاربری که به این نقش اختصاص داده شده بودند به «بیننده» تغییر یافتند و اکنون به همهٔ دوربین‌ها دسترسی دارند." + }, + "error": { + "createRoleFailed": "ایجاد نقش ناموفق بود: {{errorMessage}}", + "updateCamerasFailed": "به‌روزرسانی دوربین‌ها ناموفق بود: {{errorMessage}}", + "deleteRoleFailed": "حذف نقش ناموفق بود: {{errorMessage}}", + "userUpdateFailed": "به‌روزرسانی نقش‌های کاربر ناموفق بود: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "ایجاد نقش جدید", + "desc": "یک نقش جدید اضافه کنید و سطح دسترسی به دوربین‌ها را تعیین کنید." + }, + "form": { + "role": { + "roleExists": "نقشی با این نام از قبل وجود دارد.", + "placeholder": "نام نقش را وارد کنید", + "desc": "فقط حروف، اعداد، نقطه و زیرخط مجاز است.", + "roleIsRequired": "نام نقش الزامی است", + "roleOnlyInclude": "نام نقش فقط می‌تواند شامل حروف، اعداد، . یا _ باشد", + "title": "نام نقش" + }, + "cameras": { + "title": "دوربین‌ها", + "desc": "دوربین‌هایی را که این نقش به آن‌ها دسترسی دارد انتخاب کنید. حداقل یک دوربین لازم است.", + "required": "حداقل باید یک دوربین انتخاب شود." + } + }, + "editCameras": { + "title": "ویرایش دوربین‌های نقش", + "desc": "به‌روزرسانی دسترسی به دوربین برای نقش {{role}} ." + }, + "deleteRole": { + "title": "حذف نقش", + "desc": "این عمل قابل بازگشت نیست. این کار نقش را به‌طور دائم حذف می‌کند و همهٔ کاربرانی که این نقش را دارند به نقش 'بیننده' اختصاص می‌دهد که دسترسی بیننده به همهٔ دوربین‌ها را می‌دهد.", + "warn": "آیا مطمئن هستید که می‌خواهید {{role}} را حذف کنید؟", + "deleting": "در حال حذف…" + } + }, + "management": { + "title": "مدیریت نقش بیننده", + "desc": "مدیریت نقش‌های بینندهٔ سفارشی و مجوزهای دسترسی به دوربین آن‌ها برای این نمونهٔ Frigate." + }, + "addRole": "افزودن نقش" + }, + "notification": { + "title": "اعلان‌ها", + "notificationSettings": { + "title": "تنظیمات اعلان‌ها", + "desc": "Frigate می‌تواند به‌صورت بومی وقتی در مرورگر اجرا می‌شود یا به‌عنوان PWA نصب شده است، اعلان‌های پوش را به دستگاه شما ارسال کند." + }, + "notificationUnavailable": { + "title": "اعلان‌ها در دسترس نیستند", + "desc": "اعلان‌های پوش وب نیاز به یک بستر امن دارند ( https://… ). این محدودیت مرورگر است. برای استفاده از اعلان‌ها، به‌صورت امن به Frigate دسترسی پیدا کنید." + }, + "globalSettings": { + "title": "تنظیمات عمومی", + "desc": "به‌طور موقت اعلان‌ها را برای دوربین‌های مشخص در همهٔ دستگاه‌های ثبت‌شده متوقف کنید." + }, + "sendTestNotification": "ارسال اعلان آزمایشی", + "unsavedRegistrations": "ثبت‌نام‌های اعلان ذخیره‌نشده", + "unsavedChanges": "تغییرات اعلان ذخیره‌نشده", + "active": "اعلان‌ها فعال هستند", + "suspended": "اعلان‌ها تعلیق شده‌اند {{time}}", + "suspendTime": { + "suspend": "تعلیق", + "5minutes": "تعلیق به مدت ۵ دقیقه", + "10minutes": "تعلیق به مدت ۱۰ دقیقه", + "30minutes": "تعلیق به مدت ۳۰ دقیقه", + "1hour": "تعلیق به مدت ۱ ساعت", + "24hours": "متوقف کردن به مدت ۲۴ ساعت", + "untilRestart": "متوقف کردن تا راه‌اندازی مجدد", + "12hours": "متوقف کردن به مدت ۱۲ ساعت" + }, + "email": { + "title": "ایمیل", + "placeholder": "مثلاً example@email.com", + "desc": "یک ایمیل معتبر الزامی است و در صورت بروز مشکل در سرویس push برای اطلاع‌رسانی به شما استفاده می‌شود." + }, + "cameras": { + "title": "دوربین‌ها", + "noCameras": "هیچ دوربینی در دسترس نیست", + "desc": "انتخاب کنید که برای کدام دوربین‌ها اعلان فعال شود." + }, + "cancelSuspension": "لغو توقف", + "toast": { + "success": { + "registered": "با موفقیت برای اعلان‌ها ثبت شد. راه‌اندازی مجدد Frigate قبل از ارسال هر اعلانی (از جمله اعلان آزمایشی) الزامی است.", + "settingSaved": "تنظیمات اعلان ذخیره شد." + }, + "error": { + "registerFailed": "ذخیرهٔ ثبت‌نام اعلان ناموفق بود." + } + }, + "deviceSpecific": "تنظیمات خاص دستگاه", + "registerDevice": "ثبت این دستگاه", + "unregisterDevice": "لغو ثبت این دستگاه" + }, + "frigatePlus": { + "apiKey": { + "notValidated": "کلید API ‏Frigate+ شناسایی نشده یا معتبرسازی نشده است", + "desc": "کلید API ‏Frigate+ امکان یکپارچه‌سازی با سرویس Frigate+ را فراهم می‌کند.", + "plusLink": "دربارهٔ Frigate+ بیشتر بخوانید", + "title": "کلید API فرigate+", + "validated": "کلید API فرigate+ شناسایی و تأیید شد" + }, + "snapshotConfig": { + "title": "پیکربندی عکس فوری", + "desc": "ارسال به Frigate+ نیازمند فعال بودنِ هم «عکس‌های فوری» و هم عکس‌های فوریِ clean_copy در پیکربندی شماست.", + "cleanCopyWarning": "برای برخی دوربین‌ها عکس فوری فعال است اما clean copy غیرفعال است. برای این‌که بتوانید تصاویر این دوربین‌ها را به Frigate+ ارسال کنید، باید clean_copy را در پیکربندی عکس فوری خود فعال کنید.", + "table": { + "camera": "دوربین", + "snapshots": "عکس‌های فوری", + "cleanCopySnapshots": "عکس‌های فوریِ clean_copy " + } + }, + "modelInfo": { + "title": "اطلاعات مدل", + "loadingAvailableModels": "در حال بارگذاری مدل‌های موجود…", + "modelSelect": "مدل‌های موجود شما در Frigate+ را می‌توان از اینجا انتخاب کرد. توجه داشته باشید که فقط مدل‌های سازگار با پیکربندی فعلی آشکارساز شما قابل انتخاب هستند.", + "modelType": "نوع مدل", + "cameras": "دوربین‌ها", + "loading": "در حال بارگذاری اطلاعات مدل…", + "error": "بارگذاری اطلاعات مدل ناموفق بود", + "availableModels": "مدل‌های موجود", + "trainDate": "تاریخ آموزش", + "baseModel": "مدل پایه", + "plusModelType": { + "baseModel": "مدل پایه", + "userModel": "بهینه‌شده" + }, + "supportedDetectors": "تشخیص‌دهنده‌های پشتیبانی‌شده" + }, + "unsavedChanges": "تغییرات تنظیمات Frigate+ ذخیره‌نشده", + "restart_required": "نیاز به راه‌اندازی مجدد (مدل Frigate+ تغییر کرد)", + "toast": { + "success": "تنظیمات Frigate+ ذخیره شد. برای اعمال تغییرات، Frigate را راه‌اندازی مجدد کنید.", + "error": "ذخیرهٔ تغییرات پیکربندی ناموفق بود: {{errorMessage}}" + }, + "title": "تنظیمات Frigate+" + }, + "triggers": { + "documentTitle": "تریگرها", + "semanticSearch": { + "title": "جستجوی معنایی غیرفعال است", + "desc": "برای استفاده از تریگرها باید جستجوی معنایی فعال باشد." + }, + "management": { + "title": "تریگرها", + "desc": "مدیریت محرک‌ها برای {{camera}}. از نوع بندانگشتی برای فعال‌سازی روی بندانگشتی‌های مشابه به شیء ردیابی‌شدهٔ انتخابی‌تان استفاده کنید، و از نوع توضیحات برای فعال‌سازی روی توضیحات مشابه به متنی که مشخص می‌کنید." + }, + "table": { + "lastTriggered": "آخرین بار فعال‌شده", + "noTriggers": "هیچ محرکی برای این دوربین پیکربندی نشده است.", + "edit": "ویرایش", + "deleteTrigger": "حذف محرک", + "name": "نام", + "type": "نوع", + "content": "محتوا", + "threshold": "آستانه", + "actions": "اقدامات" + }, + "type": { + "thumbnail": "پیش‌نمایش", + "description": "توضیحات" + }, + "actions": { + "notification": "ارسال اعلان", + "sub_label": "افزودن زیر‌برچسب", + "attribute": "افزودن ویژگی" + }, + "dialog": { + "createTrigger": { + "title": "ایجاد تریگر", + "desc": "برای دوربین {{camera}} یک تریگر ایجاد کنید" + }, + "editTrigger": { + "title": "ویرایش تریگر", + "desc": "تنظیمات تریگر روی دوربین {{camera}} را ویرایش کنید" + }, + "deleteTrigger": { + "title": "حذف تریگر", + "desc": "آیا مطمئن هستید که می‌خواهید تریگر {{triggerName}} را حذف کنید؟ این عمل قابل بازگشت نیست." + }, + "form": { + "name": { + "title": "نام", + "placeholder": "این تریگر را نام‌گذاری کنید", + "description": "یک نام یا توضیح یکتا وارد کنید تا این تریگر قابل شناسایی باشد", + "error": { + "minLength": "فیلد باید حداقل ۲ کاراکتر باشد.", + "invalidCharacters": "فیلد فقط می‌تواند شامل حروف، اعداد، زیرخط (_) و خط تیره (-) باشد.", + "alreadyExists": "تریگری با این نام از قبل برای این دوربین وجود دارد." + } + }, + "enabled": { + "description": "این تریگر را فعال یا غیرفعال کنید" + }, + "type": { + "title": "نوع", + "placeholder": "نوع تریگر را انتخاب کنید", + "description": "وقتی توضیحی مشابهِ شیء ردیابی‌شده تشخیص داده شود تریگر شود", + "thumbnail": "وقتی بندانگشتیِ مشابهِ شیء ردیابی‌شده تشخیص داده شود تریگر شود" + }, + "content": { + "title": "محتوا", + "imagePlaceholder": "یک بندانگشتی انتخاب کنید", + "textPlaceholder": "محتوای متنی را وارد کنید", + "imageDesc": "فقط ۱۰۰ بندانگشتیِ آخر نمایش داده می‌شوند. اگر بندانگشتیِ موردنظر خود را پیدا نمی‌کنید، لطفاً اشیای قدیمی‌تر را در Explore مرور کنید و از همان‌جا از منو یک تریگر تنظیم کنید.", + "textDesc": "متنی وارد کنید تا وقتی توضیحی مشابهِ شیء ردیابی‌شده تشخیص داده شد، این اقدام تریگر شود.", + "error": { + "required": "محتوا الزامی است." + } + }, + "threshold": { + "title": "آستانه", + "desc": "آستانهٔ شباهت را برای این تریگر تعیین کنید. آستانهٔ بالاتر یعنی برای فعال شدن تریگر، تطابق نزدیک‌تری لازم است.", + "error": { + "min": "آستانه باید حداقل ۰ باشد", + "max": "آستانه باید حداکثر ۱ باشد" + } + }, + "actions": { + "title": "اقدام‌ها", + "desc": "به‌طور پیش‌فرض، Frigate برای همهٔ تریگرها یک پیام MQTT ارسال می‌کند. زیر‌برچسب‌ها نام تریگر را به برچسب شیء اضافه می‌کنند. ویژگی‌ها فراداده‌های قابل جستجو هستند که جداگانه در فرادادهٔ شیء ردیابی‌شده ذخیره می‌شوند.", + "error": { + "min": "حداقل باید یک اقدام انتخاب شود." + } + } + } + }, + "wizard": { + "title": "ایجاد تریگر", + "step1": { + "description": "تنظیمات پایهٔ تریگر خود را پیکربندی کنید." + }, + "step2": { + "description": "محتوایی را که این اقدام را فعال می‌کند تنظیم کنید." + }, + "step3": { + "description": "آستانه و اقدام‌های این تریگر را پیکربندی کنید." + }, + "steps": { + "nameAndType": "نام و نوع", + "configureData": "پیکربندی داده‌ها", + "thresholdAndActions": "آستانه و اقدام‌ها" + } + }, + "toast": { + "success": { + "createTrigger": "تریگر {{name}} با موفقیت ایجاد شد.", + "updateTrigger": "تریگر {{name}} با موفقیت به‌روزرسانی شد.", + "deleteTrigger": "تریگر {{name}} با موفقیت حذف شد." + }, + "error": { + "createTriggerFailed": "ایجاد تریگر ناموفق بود: {{errorMessage}}", + "updateTriggerFailed": "به‌روزرسانی تریگر ناموفق بود: {{errorMessage}}", + "deleteTriggerFailed": "حذف تریگر ناموفق بود: {{errorMessage}}" + } + }, + "addTrigger": "افزودن محرک" + } +} diff --git a/web/public/locales/fa/views/system.json b/web/public/locales/fa/views/system.json new file mode 100644 index 0000000..f2ac7eb --- /dev/null +++ b/web/public/locales/fa/views/system.json @@ -0,0 +1,201 @@ +{ + "documentTitle": { + "cameras": "آمار دوربین‌ها - فریگیت", + "storage": "آمار حافظه - فریگیت", + "general": "آمار عمومی - فریگیت", + "enrichments": "آمار بهینه سازی - فریگیت", + "logs": { + "frigate": "ثبت رخدادهای فریگیت - فریگیت", + "go2rtc": "گزارش‌های Go2RTC - فریگیت", + "nginx": "گزارش‌های Nginx - فریگیت" + } + }, + "title": "سیستم", + "metrics": "شاخص‌های سیستم", + "logs": { + "download": { + "label": "دانلود گزارش‌ها" + }, + "copy": { + "label": "کپی در کلیپ‌بورد", + "success": "گزارش‌ها در کلیپ‌بورد کپی شدند", + "error": "نمی‌توان گزارش‌ها را در کلیپ‌بورد کپی کرد" + }, + "type": { + "label": "نوع", + "timestamp": "برچسب زمانی", + "tag": "تگ", + "message": "پیام" + }, + "tips": "گزارش‌ها از سرور به‌صورت زنده در حال دریافت هستند", + "toast": { + "error": { + "fetchingLogsFailed": "خطا در دریافت گزارش‌ها: {{errorMessage}}", + "whileStreamingLogs": "خطا هنگام پخش زندهٔ گزارش‌ها: {{errorMessage}}" + } + } + }, + "general": { + "hardwareInfo": { + "title": "اطلاعات سخت‌افزار", + "gpuUsage": "مصرف GPU", + "gpuMemory": "حافظهٔ GPU", + "gpuEncoder": "رمزگذار GPU", + "gpuDecoder": "رمزگشای GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "خروجی Vainfo", + "returnCode": "کد بازگشتی: {{code}}", + "processOutput": "خروجی فرایند:", + "processError": "خطای فرایند:" + }, + "nvidiaSMIOutput": { + "title": "خروجی Nvidia SMI", + "name": "ذخیرهٔ جست‌وجونام: {{name}}", + "driver": "درایور: {{driver}}", + "cudaComputerCapability": "قابلیت محاسباتی CUDA: {{cuda_compute}}", + "vbios": "اطلاعات VBios: {{vbios}}" + }, + "closeInfo": { + "label": "بستن اطلاعات GPU" + }, + "copyInfo": { + "label": "کپی اطلاعات GPU" + }, + "toast": { + "success": "اطلاعات GPU در کلیپ‌بورد کپی شد" + } + }, + "npuUsage": "میزان استفاده از NPU", + "npuMemory": "حافظهٔ NPU", + "intelGpuWarning": { + "title": "هشدار آمار GPU اینتل", + "message": "آمار GPU در دسترس نیست", + "description": "این یک باگ شناخته‌شده در ابزارهای گزارش‌دهی آمار GPU اینتل (intel_gpu_top) است که باعث می‌شود از کار بیفتد و حتی در مواردی که شتاب‌دهی سخت‌افزاری و تشخیص شیء به‌درستی روی (i)GPU اجرا می‌شوند، به‌طور مکرر میزان استفادهٔ GPU را ۰٪ برگرداند. این مشکل مربوط به Frigate نیست. می‌توانید میزبان را ری‌استارت کنید تا موقتاً مشکل برطرف شود و تأیید کنید که GPU درست کار می‌کند. این موضوع روی عملکرد تأثیری ندارد." + } + }, + "title": "عمومی", + "detector": { + "title": "آشکارسازها", + "inferenceSpeed": "سرعت استنتاج آشکارساز", + "temperature": "دمای آشکارساز", + "cpuUsage": "مصرف CPU آشکارساز", + "cpuUsageInformation": "CPU برای آماده‌سازی داده‌های ورودی و خروجی به/از مدل‌های تشخیص استفاده می‌شود. این مقدار مصرف استنتاج را اندازه‌گیری نمی‌کند، حتی اگر از GPU یا شتاب‌دهنده استفاده شود.", + "memoryUsage": "مصرف حافظهٔ آشکارساز" + }, + "otherProcesses": { + "title": "فرایندهای دیگر", + "processCpuUsage": "میزان استفادهٔ CPU فرایند", + "processMemoryUsage": "میزان استفادهٔ حافظهٔ فرایند" + } + }, + "storage": { + "recordings": { + "earliestRecording": "قدیمی‌ترین ضبط موجود:", + "title": "ضبط‌ها", + "tips": "این مقدار نشان‌دهندهٔ کل فضای ذخیره‌سازیِ استفاده‌شده توسط ضبط‌ها در پایگاه‌دادهٔ Frigate است. Frigate میزان استفاده از فضای ذخیره‌سازیِ همهٔ فایل‌های روی دیسک شما را ردیابی نمی‌کند." + }, + "shm": { + "warning": "اندازهٔ فعلی SHM برابر {{total}}MB خیلی کوچک است. آن را دست‌کم به {{min_shm}}MB افزایش دهید.", + "title": "اختصاص SHM (حافظهٔ اشتراکی)" + }, + "cameraStorage": { + "title": "ذخیره‌سازی دوربین", + "unusedStorageInformation": "اطلاعات فضای ذخیره‌سازیِ استفاده‌نشده", + "percentageOfTotalUsed": "درصد از کل", + "unused": { + "title": "استفاده‌نشده", + "tips": "اگر فایل‌های دیگری غیر از ضبط‌های Frigate روی دیسک شما ذخیره شده باشد، این مقدار ممکن است فضای آزادِ در دسترس برای Frigate را دقیق نشان ندهد. Frigate میزان استفاده از فضای ذخیره‌سازی خارج از ضبط‌های خودش را ردیابی نمی‌کند." + }, + "camera": "دوربین", + "storageUsed": "ذخیره‌سازی", + "bandwidth": "پهنای باند" + }, + "title": "ذخیره‌سازی", + "overview": "نمای کلی" + }, + "cameras": { + "overview": "نمای کلی", + "info": { + "cameraProbeInfo": "اطلاعات پروب دوربین {{camera}}", + "fetching": "در حال دریافت داده‌های دوربین", + "video": "ویدئو:", + "fps": "FPS:", + "audio": "صدا:", + "aspectRatio": "نسبت تصویر", + "streamDataFromFFPROBE": "داده‌های جریان با ffprobe به‌دست می‌آید.", + "stream": "جریان {{idx}}", + "codec": "کدک:", + "resolution": "وضوح:", + "unknown": "نامشخص", + "error": "خطا: {{error}}", + "tips": { + "title": "اطلاعات بررسی دوربین" + } + }, + "framesAndDetections": "فریم‌ها / تشخیص‌ها", + "label": { + "detect": "تشخیص", + "capture": "گرفتن", + "overallDetectionsPerSecond": "مجموع تشخیص‌ها در ثانیه", + "cameraCapture": "گرفتن {{camName}}", + "cameraDetectionsPerSecond": "تشخیص‌ها در ثانیهٔ {{camName}}", + "camera": "دوربین", + "skipped": "رد شد", + "ffmpeg": "کدک FFmpeg", + "overallFramesPerSecond": "نرخ کلی فریم بر ثانیه", + "overallSkippedDetectionsPerSecond": "نرخ کلی تشخیص‌های ردشده بر ثانیه", + "cameraDetect": "تشخیص {{camName}}", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraFramesPerSecond": "{{camName}} فریم بر ثانیه", + "cameraSkippedDetectionsPerSecond": "{{camName}} تشخیص‌های ردشده در ثانیه" + }, + "toast": { + "error": { + "unableToProbeCamera": "پروبِ دوربین ناموفق بود: {{errorMessage}}" + }, + "success": { + "copyToClipboard": "داده‌های بررسی در کلیپ‌بورد کپی شد." + } + }, + "title": "دوربین‌ها" + }, + "stats": { + "ffmpegHighCpuUsage": "{{camera}} استفادهٔ CPU بالایی برای FFmpeg دارد ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} استفادهٔ CPU بالایی برای تشخیص دارد ({{detectAvg}}%)", + "reindexingEmbeddings": "بازتولید نمایهٔ embeddingها ({{processed}}% تکمیل شده)", + "cameraIsOffline": "{{camera}} آفلاین است", + "detectIsVerySlow": "{{detect}} بسیار کند است ({{speed}} ms)", + "shmTooLow": "اختصاص /dev/shm ({{total}} MB) باید دست‌کم تا {{min}} MB افزایش یابد.", + "healthy": "سامانه سالم است", + "detectIsSlow": "{{detect}} کند است ( {{speed}} میلی‌ثانیه )" + }, + "enrichments": { + "infPerSecond": "استنتاج‌ها در ثانیه", + "embeddings": { + "text_embedding": "امبدینگ متن", + "image_embedding_speed": "سرعت امبدینگ تصویر", + "plate_recognition_speed": "سرعت تشخیص پلاک", + "yolov9_plate_detection": "تشخیص پلاک YOLOv9", + "review_description_events_per_second": "توضیح بازبینی", + "object_description": "توضیح شیء", + "image_embedding": "امبدینگ تصویر", + "face_recognition": "شناسایی چهره", + "plate_recognition": "شناسایی پلاک", + "face_embedding_speed": "سرعت امبدینگ چهره", + "face_recognition_speed": "سرعت شناسایی چهره", + "text_embedding_speed": "سرعت امبدینگ متن", + "yolov9_plate_detection_speed": "سرعت تشخیص پلاک YOLOv9", + "review_description": "توضیحات بازبینی", + "review_description_speed": "سرعت توضیحات بازبینی", + "object_description_speed": "سرعت توضیحات شیء", + "object_description_events_per_second": "توضیحات شیء", + "classification": "طبقه‌بندی {{name}}", + "classification_speed": "سرعت طبقه‌بندی {{name}}", + "classification_events_per_second": "رویدادهای طبقه‌بندی {{name}} در ثانیه" + }, + "title": "غنی‌سازی‌ها", + "averageInf": "میانگین زمان استنتاج" + }, + "lastRefreshed": "آخرین به‌روزرسانی: · " +} diff --git a/web/public/locales/fi/audio.json b/web/public/locales/fi/audio.json new file mode 100644 index 0000000..f066503 --- /dev/null +++ b/web/public/locales/fi/audio.json @@ -0,0 +1,165 @@ +{ + "speech": "Puhe", + "yell": "Huutaa", + "babbling": "Pulina", + "boat": "Vene", + "cat": "Kissa", + "dog": "Koira", + "horse": "Hevonen", + "sheep": "Lammas", + "bird": "Lintu", + "car": "Auto", + "motorcycle": "Moottoripyörä", + "bus": "Bussi", + "train": "Juna", + "bicycle": "Pyörä", + "skateboard": "Rullalauta", + "bellow": "Karjua", + "whoop": "Huutaa", + "whispering": "Kuiskaus", + "laughter": "Nauru", + "snicker": "Hihittää", + "crying": "Itku", + "sigh": "Huokaus", + "singing": "Laulu", + "choir": "Kuoro", + "yodeling": "Jodlata", + "camera": "Kamera", + "animal": "Eläin", + "goat": "Vuohi", + "mouse": "Hiiri", + "keyboard": "Näppäimistö", + "vehicle": "Ajoneuvo", + "door": "Ovi", + "sink": "Lavuaari", + "blender": "Tehosekoitin", + "scissors": "Sakset", + "hair_dryer": "Hiustenkuivaaja", + "toothbrush": "Hammasharja", + "clock": "Kello", + "bark": "Haukku", + "chant": "Laulaa", + "mantra": "Mantra", + "child_singing": "Lapsi laulaa", + "synthetic_singing": "Synteettinen laulu", + "rapping": "Räppi", + "humming": "Humina", + "groan": "Notkua", + "grunt": "Murahtaa", + "whistling": "Vihellys", + "breathing": "Hengitys", + "wheeze": "Vinkua", + "snoring": "Kuorsaus", + "gasp": "Haukkua", + "pant": "Huohottaa", + "snort": "Haukkua", + "cough": "Yskä", + "sneeze": "Niistää", + "throat_clearing": "Kurkun selvittäminen", + "sniff": "Nuuhkia", + "run": "Juokse", + "shuffle": "Sekoitus", + "hiccup": "Hikka", + "radio": "Radio", + "television": "Televisio", + "environmental_noise": "Ympäristön melu", + "sound_effect": "Äänitehoste", + "silence": "Hiljaisuus", + "glass": "Lasi", + "wood": "Puu", + "eruption": "Purkaus", + "firecracker": "Sähikäinen", + "fireworks": "Ilotulitus", + "artillery_fire": "Tykistötuli", + "machine_gun": "Konekivääri", + "explosion": "Räjähdys", + "drill": "Pora", + "sanding": "Hionta", + "sawing": "Sahaus", + "hammer": "Vasara", + "tools": "Työkalut", + "printer": "Tulostin", + "cash_register": "Kassakone", + "air_conditioning": "Ilmastointi", + "mechanical_fan": "Mekaaninen tuuletin", + "sewing_machine": "Ompelukone", + "gears": "Hammasrattaat", + "ratchet": "Räikkä", + "pigeon": "Kyyhkynen", + "crow": "Varis", + "owl": "Pöllö", + "flapping_wings": "Siipien räpyttely", + "dogs": "Koirat", + "rats": "Rotat", + "insect": "Hyönteinen", + "cricket": "Sirkka", + "mosquito": "Hyttynen", + "fly": "Kärpänen", + "footsteps": "Askelia", + "chewing": "Pureskelu", + "biting": "Pureminen", + "gargling": "Kurlaus", + "stomach_rumble": "Vatsan kurina", + "burping": "Röyhtäily", + "fart": "Pieru", + "hands": "Kädet", + "finger_snapping": "Sormien napsauttaminen", + "clapping": "Taputtaminen", + "heartbeat": "Sydämenlyönti", + "cheering": "Hurraus", + "applause": "Aplodit", + "crowd": "Väkijoukko", + "children_playing": "Lapset leikkivät", + "pets": "Lemmikit", + "whimper_dog": "Koiran vinkuminen", + "meow": "Miau", + "livestock": "Karja", + "cattle": "Nautakarja", + "cowbell": "Lehmänkello", + "pig": "Sika", + "chicken": "Kana", + "duck": "Ankka", + "frog": "Sammakko", + "snake": "Käärme", + "music": "Musiikki", + "musical_instrument": "Musiikki-instrumentti", + "guitar": "Kitara", + "electric_guitar": "Sähkökitara", + "bass_guitar": "Bassokitara", + "acoustic_guitar": "Akustinen kitara", + "tapping": "Napauttaminen", + "piano": "Piano", + "electric_piano": "Sähköpiano", + "organ": "Urku", + "synthesizer": "Syntetisaattori", + "drum_kit": "Rumpusetti", + "drum": "Rumpu", + "wood_block": "Puupalikka", + "steelpan": "Teräspannu", + "trumpet": "Trumpetti", + "violin": "Viulu", + "cello": "Sello", + "flute": "Huilu", + "saxophone": "Saksofoni", + "clarinet": "Klarinetti", + "harp": "Harppu", + "bell": "Kello", + "church_bell": "Kirkonkello", + "bicycle_bell": "Polkupyörän kello", + "tuning_fork": "Virityshaarukka", + "pop_music": "Popmusiikki", + "hip_hop_music": "Hiphop-musiikki", + "rock_music": "Rock-musiikki", + "heavy_metal": "Heavy metal", + "punk_rock": "Punkrock", + "rock_and_roll": "Rock and Roll", + "scream": "Huutaa", + "accelerating": "Kiihdyttäminen", + "air_brake": "Ilmajarru", + "aircraft": "Ilma-alus", + "aircraft_engine": "Lentokoneen moottori", + "alarm": "Hälytys", + "ambient_music": "Tunnelmamusiikki", + "ambulance": "Ambulanssi", + "angry_music": "Vihainen musiikki" +} diff --git a/web/public/locales/fi/common.json b/web/public/locales/fi/common.json new file mode 100644 index 0000000..ea16d95 --- /dev/null +++ b/web/public/locales/fi/common.json @@ -0,0 +1,177 @@ +{ + "time": { + "untilRestart": "Kunnes uudelleenkäynnistyy", + "ago": "{{timeAgo}} sitten", + "justNow": "Juuri nyt", + "today": "Tänään", + "yesterday": "Eilen", + "last14": "Viimeiset 14 päivää", + "untilForTime": "Kunnes {{time}}", + "untilForRestart": "Kunnes Frigate uudelleenkäynnistyy.", + "thisWeek": "Tämä viikko", + "lastWeek": "Viime viikko", + "last7": "Viimeiset 7 päivää", + "thisMonth": "Tämä kuu", + "lastMonth": "Viime kuu", + "last30": "Viimeiset 30 päivää", + "5minutes": "5 minuuttia", + "10minutes": "10 minuuttia", + "30minutes": "30 minuuttia", + "1hour": "1 tunti", + "12hours": "12 tuntia", + "24hours": "24 tuntia", + "pm": "ip", + "am": "ap", + "yr": "{{time}}v", + "year_one": "{{time}} vuosi", + "year_other": "{{time}} vuotta", + "mo": "{{time}}kk", + "month_one": "{{time}} kuukausi", + "month_other": "{{time}} kuukaudet", + "d": "{{time}}pv", + "day_one": "{{time}} päivä", + "day_other": "{{time}} päivät", + "h": "{{time}}t", + "hour_one": "{{time}} tunti", + "hour_other": "{{time}} tuntia", + "m": "{{time}}m", + "s": "{{time}}s", + "minute_one": "{{time}}minuutti", + "minute_other": "{{time}}minuuttia", + "second_one": "{{time}} sekunti", + "second_other": "{{time}} sekuntia", + "formattedTimestampHourMinute": { + "24hour": "HH:mm" + }, + "never": "Ei koskaan" + }, + "pagination": { + "next": { + "title": "Seuraava", + "label": "Mene seuraavalle sivulle" + }, + "more": "Lisää sivuja", + "previous": { + "title": "Edellinen", + "label": "Mene edelliselle sivulle" + }, + "label": "sivutus" + }, + "accessDenied": { + "documentTitle": "Pääsy kielletty - Frigate", + "title": "Pääsy kielletty", + "desc": "Sinulla ei ole oikeuksia tarkastella tätä sivua." + }, + "role": { + "admin": "Järjestelmänvalvoja", + "viewer": "Katselija", + "desc": "Järjestelmänvalvojalla on täysi käyttöoikeus kaikkiin Frigaten käyttöliittymän toimintoihin. Katselijoiden oikeudet on rajoitettu kameroiden katseluun, kohteiden arviointiin ja historian tarkasteluun.", + "title": "Rooli" + }, + "notFound": { + "documentTitle": "Ei löytynyt - Frigate", + "title": "404", + "desc": "Sivua ei löytynyt" + }, + "selectItem": "Valitse {{item}}", + "menu": { + "live": { + "title": "Suora", + "cameras": { + "title": "Kamerat", + "count_one": "{{count}} kamera", + "count_other": "{{count}} kameraa" + }, + "allCameras": "Kaikki kamerat" + }, + "explore": "Selaa", + "export": "Vienti", + "uiPlayground": "UI-leikkikenttä", + "user": { + "account": "Tili", + "current": "Nykyinen käyttäjä: {{user}}", + "anonymous": "anonyymi", + "title": "Käyttäjä", + "logout": "Kirjaudu ulos", + "setPassword": "Aseta salasana" + }, + "appearance": "Ulkonäkö", + "darkMode": { + "label": "Tumma tila", + "light": "Valoisa", + "dark": "Tumma", + "withSystem": { + "label": "Käytä järjestelmän asetuksia valoisalle tai tummalle tilalle" + } + }, + "faceLibrary": "Kasvokirjasto", + "language": { + "ca": "Katalaani", + "withSystem": { + "label": "Käytä järjestelmän asetuksia kielelle" + } + }, + "review": "Esikatselu", + "theme": { + "highcontrast": "Korkea resoluutio", + "blue": "Sininen", + "green": "Vihreä", + "default": "Oletus", + "nord": "Pohjoismainen", + "red": "Punainen", + "label": "Teema" + }, + "withSystem": "Järjestelmä", + "help": "Apua", + "documentation": { + "title": "Dokumentaatio", + "label": "Frigaten dokumentaatio" + }, + "restart": "Käynnistä uudelleen", + "languages": "Kielet", + "system": "Järjestelmä", + "settings": "Asetukset", + "configuration": "Konfiguraatio" + }, + "toast": { + "copyUrlToClipboard": "URL kopioitu leikepöydälle.", + "save": { + "title": "Tallenna", + "error": { + "title": "Konfiguraatiomuutosten tallennus epäonnistui: {{errorMessage}}", + "noMessage": "Konfiguraatiomuutosten tallennus epäonnistui" + } + } + }, + "button": { + "on": "ON", + "disabled": "Pois käytöstä", + "done": "Valmis", + "enabled": "Käytössä", + "enable": "Ota käyttöön", + "disable": "Poista käytöstä", + "save": "Tallenna", + "saving": "Tallennetaan…", + "cancel": "Peruuta", + "reset": "Nollaa", + "close": "Sulje", + "off": "OFF", + "edit": "Muokkaa", + "yes": "Kyllä", + "copy": "Kopioi", + "no": "Ei", + "download": "Lataa", + "back": "Takaisin", + "history": "Historia", + "play": "Toista", + "next": "Seuraava", + "delete": "Poista", + "info": "Info" + }, + "unit": { + "length": { + "feet": "jalka" + } + }, + "readTheDocumentation": "Lue dokumentaatio" +} diff --git a/web/public/locales/fi/components/auth.json b/web/public/locales/fi/components/auth.json new file mode 100644 index 0000000..167310f --- /dev/null +++ b/web/public/locales/fi/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Salasana", + "user": "Käyttäjänimi", + "login": "Kirjaudu", + "errors": { + "usernameRequired": "Käyttäjänimi vaaditaan", + "passwordRequired": "Salasana vaaditaan", + "rateLimit": "Käyttöraja ylitetty. Yritä myöhemmin uudelleen.", + "loginFailed": "Kirjautuminen epäonnistui", + "unknownError": "Tuntematon virhe. Tarkista logit.", + "webUnknownError": "Tuntematon virhe. Tarkista konsolilogi." + }, + "firstTimeLogin": "Ensimmäistä kertaa kirjautumassa sisään? Tunnukset löytyvät Frigaten lokista." + } +} diff --git a/web/public/locales/fi/components/camera.json b/web/public/locales/fi/components/camera.json new file mode 100644 index 0000000..a641ca6 --- /dev/null +++ b/web/public/locales/fi/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Kameraryhmä", + "add": "Lisää kameraryhmä", + "edit": "Muokkaa kameraryhmää", + "delete": { + "label": "Poista kameraryhmä", + "confirm": { + "title": "Varmista poisto", + "desc": "Oletko varma että haluat poistaa kameraryhmän {{name}}?" + } + }, + "name": { + "label": "Nimi", + "placeholder": "Anna nimi…", + "errorMessage": { + "mustLeastCharacters": "Kameraryhmän nimi täytyy olla vähintään 2 kirjainta.", + "exists": "Kameraryhmän nimi on jo olemassa.", + "nameMustNotPeriod": "Kameraryhmän nimi ei voi sisältää pistettä.", + "invalid": "Virheellinen kameraryhmän nimi." + } + }, + "cameras": { + "label": "Kamerat", + "desc": "Valitse ryhmän kamera." + }, + "icon": "Ikoni", + "success": "Kameraryhmä ({{name}}) on tallennettu.", + "camera": { + "setting": { + "label": "Kameran suoratoistoasetukset", + "title": "{{cameraName}} suoratoistoasetukset", + "desc": "Muuta tämän kameraryhmän kojelaudan live-suoratoistoasetuksia.Nämä asetukset ovat laite/selainkohtaisia.", + "audioIsAvailable": "Ääni on saatavilla tähän suoratoistoon", + "audioIsUnavailable": "Ääni ei ole saatavilla tähän suoratoistoon", + "audio": { + "tips": { + "title": "Äänen on oltava kytkettynä kameraan ja määritettynä go2rtc:ssä tätä suoratoistoa varten.", + "document": "Lue dokumentaatio " + } + }, + "streamMethod": { + "label": "Suoratoistomenetelmä", + "method": { + "noStreaming": { + "label": "Ei suoratoistoa", + "desc": "Kamerakuvat päivittyvät vain kerran minuutissa, eikä suoratoistoa tapahdu." + }, + "smartStreaming": { + "label": "Älykäs suoratoisto (suositus)", + "desc": "Älykäs suoratoisto päivittää kamerakuvan kerran minuutissa kun havaittavaa toimintaa ei tapahdu, säästääkseen kaistanleveyttä ja resursseja. Kun toimintaa havaitaan, kuva vaihtuu saumattomasti reaaliaikaiseksi suoratoistoksi." + }, + "continuousStreaming": { + "label": "Jatkuva suoratoisto", + "desc": { + "title": "Kamerakuva näkyy aina reaaliaikaisena suoratoistona kojelaudassa, vaikka mitään liikettä ei havaitaisi.", + "warning": "Jatkuva suoratoisto voi lisätä kaistanleveyden käyttöä ja suorituskykyongelmia. Käytä varoen." + } + } + }, + "placeholder": "Valitse toiston tyyppi" + }, + "compatibilityMode": { + "label": "Yhteensopivuustila", + "desc": "Ota tämä vaihtoehto käyttöön vain, jos kamerasi live-suoratoistossa näkyy väriartefakteja ja kuvan oikealla puolella on vinoviiva." + }, + "stream": "Kuvavirta", + "placeholder": "Valitse kuvavirta" + }, + "birdseye": "Linnun silmä" + } + }, + "debug": { + "options": { + "label": "Asetukset", + "title": "Vaihtoehdot", + "showOptions": "Näytä vaihtoehdot", + "hideOptions": "Piilota vaihtoehdot" + }, + "boundingBox": "Rajauslaatikko", + "timestamp": "Aikaleima", + "zones": "Vyöhykkeet", + "mask": "Peite", + "motion": "Liike", + "regions": "Alueet" + } +} diff --git a/web/public/locales/fi/components/dialog.json b/web/public/locales/fi/components/dialog.json new file mode 100644 index 0000000..7466d85 --- /dev/null +++ b/web/public/locales/fi/components/dialog.json @@ -0,0 +1,88 @@ +{ + "restart": { + "title": "Haluatko varmasti käynnistää Frigaten uudelleen?", + "button": "Uudelleenkäynnistys", + "restarting": { + "title": "Fregatti käynnistyy uudelleen", + "content": "Tämä sivu latautuu uudelleen {{countdown}} sekunnin kuluttua.", + "button": "Pakota uudelleenlataus nyt" + }, + "description": "Tämä sammuttaa Frigaten lyhyeksi aikaa uudelleenkäynnistyksen ajaksi." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Lähetä Frigate+:lle", + "desc": "Välttämissäsi paikoissa olevat kohteet eivät ole vääriä positiivisia. Niiden lähettäminen väärinä positiivisina sekoittaa mallia." + }, + "review": { + "question": { + "label": "Vahvista tämä nimike Frigate Plussalle", + "ask_a": "Onko kohde {{label}}?", + "ask_an": "Onko tämä kohde {{label}}?", + "ask_full": "Onko tämä kohde {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Lähetetty" + } + } + }, + "video": { + "viewInHistory": "Katso historiaa" + } + }, + "export": { + "time": { + "fromTimeline": "Valitse aikajanalta", + "lastHour_one": "Viimeinen tunti", + "lastHour_other": "Viimeiset {{count}} tuntia", + "start": { + "title": "Aloitusaika", + "label": "Valitse aloitusaika" + }, + "end": { + "title": "Lopetusaika", + "label": "Valitse lopetusaika" + }, + "custom": "Mukautettu" + }, + "name": { + "placeholder": "Nimeä vienti" + }, + "select": "Valitse", + "export": "Vie", + "selectOrExport": "Valitse tai Vie", + "toast": { + "error": { + "failed": "Viennin aloitus epäonnistui: {{error}}", + "endTimeMustAfterStartTime": "Lopetusajan pitää olla aloitusajan jälkeen", + "noVaildTimeSelected": "Sopivaa aikaikkunaa ei valittuna" + }, + "success": "Vienti käynnistettiin onnistuneesti. Katso tiedosto /export kansiossa." + }, + "fromTimeline": { + "saveExport": "Tallenna vienti", + "previewExport": "Esikatsele vientiä" + } + }, + "streaming": { + "label": "Kuvavirta", + "restreaming": { + "disabled": "Uudelleentoisto ei ole käytettävissä tällä kameralla.", + "desc": { + "title": "Määritä go2rtc saadaksesi lisäreaaliaikanäkymän vaihtoehtoja ja ääntä tälle kameralle.", + "readTheDocumentation": "Lue dokumentaatio" + } + } + }, + "search": { + "saveSearch": { + "label": "Tallenna haku" + } + }, + "imagePicker": { + "search": { + "placeholder": "Hae nimikkeen tai alinimikkeen mukaan..." + } + } +} diff --git a/web/public/locales/fi/components/filter.json b/web/public/locales/fi/components/filter.json new file mode 100644 index 0000000..c3058bd --- /dev/null +++ b/web/public/locales/fi/components/filter.json @@ -0,0 +1,91 @@ +{ + "filter": "Suodatin", + "dates": { + "selectPreset": "Valitse esiasettelu…", + "all": { + "title": "Kaikki päivämäärät", + "short": "Päivämäärät" + } + }, + "more": "Lisää suodattimia", + "reset": { + "label": "Palauta suodattimet oletusarvoihin" + }, + "labels": { + "count_one": "{{count}} nimike", + "label": "Nimikkeet", + "all": { + "title": "Kaikki nimikkeet", + "short": "Nimikkeet" + }, + "count_other": "{{count}} nimikettä" + }, + "zones": { + "label": "Alueet", + "all": { + "title": "Kaikki alueet", + "short": "Alueet" + } + }, + "timeRange": "Aikaikkuna", + "subLabels": { + "label": "Alinimikkeet", + "all": "Kaikki alinimikkeet" + }, + "score": "Piste", + "estimatedSpeed": "Arvioitu nopeus {{unit}}", + "features": { + "label": "Piirteet", + "hasVideoClip": "Videoleike löytyy", + "submittedToFrigatePlus": { + "label": "Lähetetty Frigate+:aan", + "tips": "Sinun on ensin suodatettava seuratut kohteet, joilla on tilannekuva.

    Kohteita, joilla ei ole tilannekuvaa, ei voida lähettää Frigate+:aan." + }, + "hasSnapshot": "Tilannekuva löytyy" + }, + "sort": { + "label": "Järjestä", + "dateAsc": "Päivämäärä (Nouseva)", + "dateDesc": "Päivämäärä (Laskeva)", + "scoreAsc": "Kohteen pisteet (Nouseva)", + "scoreDesc": "Kohteen pisteet (Laskeva)", + "speedAsc": "Arvioitu nopeus (Nouseva)", + "speedDesc": "Arvioitu nopeus (Laskeva)", + "relevance": "Olennaisuus" + }, + "cameras": { + "label": "Kameran suodattimet", + "all": { + "title": "Kaikki kamerat", + "short": "Kamerat" + } + }, + "classes": { + "label": "Luokat", + "all": { + "title": "Kaikki luokat" + }, + "count_one": "{{count}} Luokka", + "count_other": "{{count}} Luokkaa" + }, + "recognizedLicensePlates": { + "clearAll": "Tyhjennä kaikki", + "title": "Tunnistetut rekisterikilvet", + "loadFailed": "Tunnistettujen rekisterikilpien lataaminen epäonnistui.", + "loading": "Ladataan tunnistettuja rekisterikilpiä…", + "placeholder": "Kirjoita hakeaksesi rekisterikilpeä…", + "noLicensePlatesFound": "Rekisterikilpiä ei löytynyt.", + "selectPlatesFromList": "Valitse yksi tai useampi rekisterikilpi luettelosta.", + "selectAll": "Valitse kaikki" + }, + "logSettings": { + "allLogs": "Kaikki lokit", + "filterBySeverity": "Suodata lokit vakavuuden mukaan" + }, + "trackedObjectDelete": { + "title": "Vahvista poisto", + "toast": { + "error": "Seurattujen kohteiden poistaminen epäonnistui: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/fi/components/icons.json b/web/public/locales/fi/components/icons.json new file mode 100644 index 0000000..20fea8f --- /dev/null +++ b/web/public/locales/fi/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Valitse kuvake", + "search": { + "placeholder": "Etsi kuvaketta…" + } + } +} diff --git a/web/public/locales/fi/components/input.json b/web/public/locales/fi/components/input.json new file mode 100644 index 0000000..59083d6 --- /dev/null +++ b/web/public/locales/fi/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Lataa Video", + "toast": { + "success": "Tarkistettavan kohteen videon lataus on aloitettu." + } + } + } +} diff --git a/web/public/locales/fi/components/player.json b/web/public/locales/fi/components/player.json new file mode 100644 index 0000000..ecb35f7 --- /dev/null +++ b/web/public/locales/fi/components/player.json @@ -0,0 +1,52 @@ +{ + "noPreviewFound": "Esikatselua ei löytynyt", + "noPreviewFoundFor": "Ei esikatselua {{cameraName}}lle", + "noRecordingsFoundForThisTime": "Ei tallenteita valitulta ajalta", + "submitFrigatePlus": { + "title": "Lähetä tämä kuva Frigate+:aan?", + "submit": "Lähetä", + "previewError": "Pysäytyskuvan esikatselua ei voi ladata. Tallenne ei ole ehkä saatavissa tällä hetkellä." + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 tai uudempi vaaditaan tälle suoratoistotyypille.", + "streamOffline": { + "title": "Kamera poissa verkosta", + "desc": "Kuvaruutuja ei vastaanotettu kameran {{cameraName}} detect-kuvavirrasta, tarkista virhelogit" + }, + "cameraDisabled": "Kamera on poistettu käytöstä", + "stats": { + "streamType": { + "title": "Kuvavirran tyyppi:", + "short": "Tyyppi" + }, + "bandwidth": { + "title": "Kaistanleveys:", + "short": "Kaistanleveys" + }, + "latency": { + "title": "Latenssi:", + "value": "{{seconds}} sekuntia", + "short": { + "value": "{{seconds}} sek", + "title": "Latenssi" + } + }, + "totalFrames": "Kehyksiä yhteensä:", + "droppedFrames": { + "title": "Pudotettuja kehyksiä:", + "short": { + "title": "Pudotettu", + "value": "{{droppedFrames}} kehystä" + } + }, + "decodedFrames": "Dekoodatut kehykset:", + "droppedFrameRate": "Pudotettujen kehysten nopeus:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Onnistuneesti lähetetty Frigate+:aan" + }, + "error": { + "submitFrigatePlusFailed": "Frigate+:aan lähetys epäonnistui" + } + } +} diff --git a/web/public/locales/fi/config/cameras.json b/web/public/locales/fi/config/cameras.json new file mode 100644 index 0000000..ac6cd5a --- /dev/null +++ b/web/public/locales/fi/config/cameras.json @@ -0,0 +1,22 @@ +{ + "label": "Kamerakonfiguraatio", + "name": { + "label": "Kameran nimi", + "description": "Kameran nimi vaaditaan" + }, + "friendly_name": { + "label": "Kutsumanimi", + "description": "Kameran kutsumanimeä käytetään Frigaten käyttöliittymässä" + }, + "enabled": { + "description": "Käytössä" + }, + "audio": { + "label": "Ääni tapahtumat", + "description": "Äänipohjaisen havaitsemisen asetukset tälle kameralle.", + "enabled": { + "label": "Ota ääni havainnointi käyttöön", + "description": "Ota tai poista käytöstä ääni tapahtuman havaiseminen tälle kameralle." + } + } +} diff --git a/web/public/locales/fi/config/global.json b/web/public/locales/fi/config/global.json new file mode 100644 index 0000000..fc3e5ac --- /dev/null +++ b/web/public/locales/fi/config/global.json @@ -0,0 +1,21 @@ +{ + "version": { + "label": "Nykyinen konfigurointiversio" + }, + "safe_mode": { + "label": "Vikasietotila", + "description": "Kun käytössä, käynnistä Frigate vikasietotilassa rajoitetuilla ominaisuuksilla vianselvitystä varten." + }, + "logger": { + "label": "Lokitus", + "default": { + "label": "Lokituksen taso" + } + }, + "audio": { + "label": "Ääni tapahtumat", + "enabled": { + "label": "Ota ääni havainnointi käyttöön" + } + } +} diff --git a/web/public/locales/fi/config/groups.json b/web/public/locales/fi/config/groups.json new file mode 100644 index 0000000..4e54fa9 --- /dev/null +++ b/web/public/locales/fi/config/groups.json @@ -0,0 +1,30 @@ +{ + "audio": { + "global": { + "detection": "Globaali tunnistus", + "sensitivity": "Globaali herkkyys" + }, + "cameras": { + "detection": "Havaitseminen", + "sensitivity": "Herkkyys" + } + }, + "timestamp_style": { + "global": { + "appearance": "Globaali vaikutelma" + }, + "cameras": { + "appearance": "Vaikutelma" + } + }, + "motion": { + "global": { + "sensitivity": "Globaali herkkyys", + "algorithm": "Globaali algoritmi" + }, + "cameras": { + "sensitivity": "Herkkyys", + "algorithm": "Algoritmi" + } + } +} diff --git a/web/public/locales/fi/config/validation.json b/web/public/locales/fi/config/validation.json new file mode 100644 index 0000000..a361e49 --- /dev/null +++ b/web/public/locales/fi/config/validation.json @@ -0,0 +1,13 @@ +{ + "minimum": "Täytyy olla vähintään {{limit}}", + "maximum": "Täytyy olla korkeitaan {{limit}}", + "exclusiveMinimum": "Täytyy olla suurempi kuin {{limit}}", + "exclusiveMaximum": "Täytyy olla vähemmän kuin {{limit}}", + "minLength": "Täytyy olla vähintään {{limit}} merkkiä", + "maxLength": "Täytyy olla enintään {{limit}} merkkiä", + "minItems": "Täytyy olla vähintään {{limit}} kappaletta", + "maxItems": "Täytyy olla enintään {{limit}} kappaletta", + "pattern": "Väärä formaatti", + "required": "Tämä kenttä on pakollinen", + "type": "Väärä arvon tyyppi" +} diff --git a/web/public/locales/fi/objects.json b/web/public/locales/fi/objects.json new file mode 100644 index 0000000..524350e --- /dev/null +++ b/web/public/locales/fi/objects.json @@ -0,0 +1,120 @@ +{ + "frisbee": "Frisbee", + "knife": "Veitsi", + "umbrella": "Sateenvarjo", + "tie": "Kravatti", + "suitcase": "Matkalaukku", + "baseball_glove": "Pesäpallohanska", + "spoon": "Lusikka", + "person": "Henkilö", + "bicycle": "Pyörä", + "car": "Auto", + "motorcycle": "Moottoripyörä", + "airplane": "Lentokone", + "bus": "Bussi", + "train": "Juna", + "boat": "Vene", + "traffic_light": "Liikennevalo", + "fire_hydrant": "Paloposti", + "street_sign": "Tieviitta", + "stop_sign": "Stop merkki", + "parking_meter": "Pysäköintimittari", + "bench": "Penkki", + "bird": "Lintu", + "cat": "Kissa", + "dog": "Koira", + "horse": "Hevonen", + "sheep": "Lammas", + "cow": "Lehmä", + "elephant": "Elefantti", + "bear": "Karhu", + "zebra": "Seepra", + "giraffe": "Kirahvi", + "hat": "Hattu", + "backpack": "Reppu", + "shoe": "Kenkä", + "eye_glasses": "Silmälasit", + "handbag": "Käsilaukku", + "skis": "Sukset", + "snowboard": "Lumilauta", + "sports_ball": "Pallo", + "kite": "Leija", + "baseball_bat": "Pesäpallomaila", + "skateboard": "Rullalauta", + "surfboard": "Surffilauta", + "tennis_racket": "Tennismaila", + "bottle": "Pullo", + "plate": "Lautanen", + "wine_glass": "Viinilasi", + "cup": "Kuppi", + "fork": "Haarukka", + "bowl": "Malja", + "banana": "Banaani", + "apple": "Omena", + "couch": "Sohva", + "keyboard": "Näppäimistö", + "book": "Kirja", + "microwave": "Mikroaaltouuni", + "toaster": "Leivänpaahdin", + "refrigerator": "Jääkaappi", + "sink": "Lavuaari", + "blender": "Tehosekoitin", + "deer": "Peura", + "oven": "Uuni", + "sandwich": "Voileipä", + "orange": "Appelsiini", + "broccoli": "Parsakaali", + "carrot": "Porkkana", + "hot_dog": "Nakkisämpylä", + "pizza": "Pizza", + "donut": "Donitsi", + "cake": "Kakku", + "chair": "Tuoli", + "potted_plant": "Ruukkukasvi", + "bed": "Sänky", + "mirror": "Peili", + "dining_table": "Ruokapöytä", + "window": "Ikkuna", + "desk": "Pöytä", + "toilet": "Vessanpönttö", + "door": "Ovi", + "tv": "TV", + "mouse": "Hiiri", + "laptop": "Kannettava tietokone", + "remote": "Kaukosäädin", + "cell_phone": "Matkapuhelin", + "clock": "Kello", + "vase": "Maljakko", + "scissors": "Sakset", + "teddy_bear": "Nallekarhu", + "hair_dryer": "Hiustenkuivaaja", + "hair_brush": "Hiusharja", + "toothbrush": "Hammasharja", + "vehicle": "Ajoneuvo", + "squirrel": "Orava", + "animal": "Eläin", + "fox": "Kettu", + "goat": "Vuohi", + "bark": "Haukku", + "rabbit": "Kaniini", + "raccoon": "Pesukarhu", + "robot_lawnmower": "Robotti ruohonleikkuri", + "waste_bin": "Jäteastia", + "package": "Paketti", + "bbq_grill": "Grilli", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "gls": "GLS", + "dpd": "DPD", + "postnord": "PostNord", + "nzpost": "NZPost", + "postnl": "PostNL", + "dhl": "DHL", + "purolator": "Purolator", + "an_post": "An Post", + "license_plate": "Rekisterikilpi", + "face": "Kasvot", + "on_demand": "Pyynnöstä" +} diff --git a/web/public/locales/fi/views/chat.json b/web/public/locales/fi/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/fi/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/fi/views/classificationModel.json b/web/public/locales/fi/views/classificationModel.json new file mode 100644 index 0000000..554a664 --- /dev/null +++ b/web/public/locales/fi/views/classificationModel.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "Luokittelumallit - Frigate", + "details": { + "scoreInfo": "Pistemäärä edustaa tämän objektin kaikkien havaintojen keskimääräistä luokitteluvarmuutta.", + "none": "Ei mitään", + "unknown": "Tuntematon" + }, + "button": { + "deleteImages": "Poista kuvat", + "trainModel": "Kouluta malli", + "deleteClassificationAttempts": "Poista luokitellut kuvat", + "deleteCategory": "Poista luokka", + "addClassification": "Lisää luokitus", + "deleteModels": "Poista mallit", + "editModel": "Muokkaa mallia" + } +} diff --git a/web/public/locales/fi/views/configEditor.json b/web/public/locales/fi/views/configEditor.json new file mode 100644 index 0000000..96990e1 --- /dev/null +++ b/web/public/locales/fi/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Frigaten konfiguraatioeditori", + "confirm": "Poistu tallentamatta?", + "saveOnly": "Vain tallennus", + "toast": { + "error": { + "savingError": "Virhe tallennettaessa konfiguraatiota" + }, + "success": { + "copyToClipboard": "Konfiguraatio kopioitu leikepöydälle." + } + }, + "configEditor": "Konfiguraatioeditori", + "copyConfig": "Kopioi konfiguraatio", + "saveAndRestart": "Tallenna & uudelleenkäynnistä", + "safeConfigEditor": "Konfiguraatioeditori (vikasietotila)", + "safeModeDescription": "Frigate on vikasietotilassa konfiguraation vahvistusvirheen vuoksi." +} diff --git a/web/public/locales/fi/views/events.json b/web/public/locales/fi/views/events.json new file mode 100644 index 0000000..4be6cc1 --- /dev/null +++ b/web/public/locales/fi/views/events.json @@ -0,0 +1,45 @@ +{ + "alerts": "Hälytykset", + "empty": { + "detection": "Ei havaintoja tarkastettavaksi", + "motion": "Ei liiketietoja", + "alert": "Ei hälyytyksiä tarkastettavaksi", + "recordingsDisabled": { + "title": "Tallenteet täytyy ottaa käyttöön" + } + }, + "detections": "Havainnot", + "motion": { + "label": "Liike", + "only": "Vain liike" + }, + "allCameras": "Kaikki kamerat", + "timeline": { + "label": "Aikajana" + }, + "timeline.aria": "Valitse aikajana", + "events": { + "label": "Tapahtumat", + "aria": "Valitse tapahtumat", + "noFoundForTimePeriod": "Tapahtumia ei löydetty tältä ajanjaksolta." + }, + "documentTitle": "Tarkastelu - Frigate", + "detected": "havaittu", + "selected_one": "{{count}} valittu", + "selected_other": "{{count}} valittu", + "recordings": { + "documentTitle": "Tallenteet - Frigate" + }, + "calendarFilter": { + "last24Hours": "Viimeiset 24 tuntia" + }, + "markAsReviewed": "Merkitse katselmoiduksi", + "markTheseItemsAsReviewed": "Merkitse nämä kohteet katselmoiduksi", + "newReviewItems": { + "label": "Näytä uudet katselmoitavat kohteet", + "button": "Uudet katselmoitavat kohteet" + }, + "camera": "Kamera", + "suspiciousActivity": "Epäilyttävä toiminta", + "threateningActivity": "Uhkaava toiminta" +} diff --git a/web/public/locales/fi/views/explore.json b/web/public/locales/fi/views/explore.json new file mode 100644 index 0000000..25743e4 --- /dev/null +++ b/web/public/locales/fi/views/explore.json @@ -0,0 +1,155 @@ +{ + "documentTitle": "Etsi", + "details": { + "timestamp": "Aikaleima", + "item": { + "title": "Tarkastele kohteen tietoja", + "desc": "Tarkastele kohteen tietoja", + "button": { + "share": "Jaa tämä tarkasteltu kohde" + }, + "toast": { + "error": { + "updatedSublabelFailed": "Alatunnisteen päivitys epäonnistui", + "updatedLPRFailed": "Rekisterikilven päivitys epäonnistui" + } + } + }, + "recognizedLicensePlate": "Tunnistettu rekisterikilpi", + "estimatedSpeed": "Arvioitu nopeus", + "objects": "Objektit", + "camera": "Kamera", + "zones": "Alueet", + "label": "Tunniste", + "editSubLabel": { + "title": "Editoi alitunnistetta", + "desc": "Syötä uusi alitunniste tähän", + "descNoLabel": "Lisää uusi alatunniste tähän seurattuun kohteeseen" + }, + "editLPR": { + "title": "Muokkaa rekisterikilpeä", + "desc": "Syötä uusi rekisterikilven arvo tähän", + "descNoLabel": "Syötä uusi rekisterikilven arvo tähän seurattuun objektiin" + }, + "snapshotScore": { + "label": "Tilannekuvan arvosana" + }, + "topScore": { + "label": "Huippuarvosana", + "info": "Ylin pistemäärä on seurattavan kohteen korkein mediaani, joten tämä voi erota hakutuloksen esikatselukuvassa näkyvästä pistemäärästä." + }, + "button": { + "findSimilar": "Etsi samankaltaisia" + }, + "description": { + "label": "Kuvaus" + }, + "score": { + "label": "Pisteet" + } + }, + "exploreIsUnavailable": { + "title": "Selaus on tavoittamattomissa", + "embeddingsReindexing": { + "startingUp": "Käynnistytään…", + "estimatedTime": "Arvioitu aika jäljellä:", + "finishingShortly": "Valmista pian", + "step": { + "trackedObjectsProcessed": "Käsitellyt seuratut objektit: ", + "thumbnailsEmbedded": "Kuvakkeet sisällytetty: ", + "descriptionsEmbedded": "Kuvaukset sisällytetty: " + }, + "context": "Selausta voidaan käyttää sen jälkeen kun seurattavien kohteiden uudelleenindeksöinti on valmistunut." + }, + "downloadingModels": { + "context": "Frigate lataa semanttista hakua varten vaadittavat upotusmallit. Tämä saattaa viedä useamman minuutin, riippuen yhteytesi nopeudesta.", + "setup": { + "visionModel": "Vision-malli", + "textModel": "Tekstimalli", + "textTokenizer": "Tekstin osioija", + "visionModelFeatureExtractor": "Näkömallin piirreluokkain" + }, + "tips": { + "documentation": "Lue dokumentaatio", + "context": "Saatat haluta uudelleenindeksoida seurattavien kohteiden upotukset, kun mallit on ladattu." + }, + "error": "Tapahtui virhe. Tarkista Frigaten lokit." + } + }, + "exploreMore": "Selaa lisää {{label}}-tyyppisiä kohteita", + "generativeAI": "Generatiivinen AI", + "objectLifecycle": { + "annotationSettings": { + "offset": { + "documentation": "Lue dokumentaatio " + }, + "showAllZones": { + "title": "Näytä kaikki vyöhykkeet" + } + }, + "lifecycleItemDesc": { + "header": { + "zones": "Vyöhykkeet", + "ratio": "Suhde", + "area": "Alue" + }, + "active": "{{label}} aktivoitui", + "stationary": "{{label}} pysähtyi", + "attribute": { + "faceOrLicense_plate": "{{attribute}} havaittiin nimikkeelle {{label}}", + "other": "{{label}} tunnistettu {{attribute}}:na" + }, + "gone": "{{label}} poistui", + "entered_zone": "{{label}} ilmestyi vyöhykkeelle {{zones}}", + "visible": "{{label}} havaittu", + "heard": "{{label}} kuului", + "external": "{{label}} havaittiin" + }, + "trackedPoint": "Seurattu piste", + "carousel": { + "previous": "Edellinen", + "next": "Seuraava" + }, + "count": "{{first}} / {{second}}", + "title": "Kohteen elinkaari", + "noImageFound": "Tältä aikaleimalta ei löytynyt kuvia.", + "createObjectMask": "Luo kohdemaski", + "scrollViewTips": "Vieritä katsoaksesi merkittäviä hetkiä kohteen elinkaarelta.", + "autoTrackingTips": "Kohteen rajojen sijainti on epätarkka automaattisesti seuraaville kameroille.", + "adjustAnnotationSettings": "Säädä merkintäasetuksia" + }, + "trackedObjectDetails": "Seurattavien kohteiden tiedot", + "type": { + "details": "tiedot", + "snapshot": "kuvankaappaus", + "video": "video", + "object_lifecycle": "kohteen elinkaari" + }, + "itemMenu": { + "downloadSnapshot": { + "label": "Lataa kuvankaappaus", + "aria": "Lataa kuvankaappaus" + }, + "addTrigger": { + "label": "Lisää laukaisin", + "aria": "Lisää laukaisin tälle seurattavalle kohteelle" + }, + "submitToPlus": { + "label": "Lähetä Frigate+:lle" + }, + "downloadVideo": { + "label": "Lataa video", + "aria": "Lataa video" + }, + "viewObjectLifecycle": { + "label": "Tarkastele objektin elinkaarta", + "aria": "Näytä objektin elinkaari" + }, + "findSimilar": { + "label": "Etsi samankaltaisia" + } + }, + "aiAnalysis": { + "title": "AI-analyysi" + } +} diff --git a/web/public/locales/fi/views/exports.json b/web/public/locales/fi/views/exports.json new file mode 100644 index 0000000..7ba06ef --- /dev/null +++ b/web/public/locales/fi/views/exports.json @@ -0,0 +1,28 @@ +{ + "search": "Etsi", + "documentTitle": "Vie", + "deleteExport.desc": "Oletko varma että haluat poistaa kohteen {{exportName}}?", + "toast": { + "error": { + "renameExportFailed": "Viedyn kohteen uudelleennimeäminen epäonnistui: {{errorMessage}}" + } + }, + "noExports": "Ei vietyjä kohteita", + "deleteExport": { + "label": "Poista vienti" + }, + "editExport": { + "title": "Nimeä uudelleen", + "desc": "Anna uusi nimi viedylle kohteelle.", + "saveExport": "Tallenna vienti" + }, + "tooltip": { + "editName": "Muokkaa nimeä", + "shareExport": "Jaa vienti", + "downloadVideo": "Lataa video" + }, + "headings": { + "cases": "Tapaukset", + "uncategorizedExports": "Kategorisoimattomat viennit" + } +} diff --git a/web/public/locales/fi/views/faceLibrary.json b/web/public/locales/fi/views/faceLibrary.json new file mode 100644 index 0000000..6e1872b --- /dev/null +++ b/web/public/locales/fi/views/faceLibrary.json @@ -0,0 +1,83 @@ +{ + "description": { + "addFace": "Opastus: Uuden kokoelman lisääminen Kasvokirjastoon.", + "invalidName": "Virheellinen nimi. Nimi voi sisältää vain merkkejä, numeroita, välejä, heittomerkkejä, alaviivoja ja väliviivoja.", + "placeholder": "Anna nimi kokoelmalle", + "nameCannotContainHash": "Nimi ei voi sisältää \"#\"." + }, + "uploadFaceImage": { + "desc": "Lähetä kuva kasvojen tunnistukseen ja lisää se sivulle {{pageToggle}}", + "title": "Lähetä kasvokuva" + }, + "details": { + "unknown": "Tuntematon", + "faceDesc": "Lisätiedot kohteesta, josta tämä kasvokuva tallennettiin", + "person": "Henkilö", + "timestamp": "Aikaleima", + "subLabelScore": "Alinimikkeen pisteet", + "face": "Kasvojen yksityiskohdat", + "scoreInfo": "Alatunnisteen pistemäärä on kaikkien tunnistettujen kasvojen varmuustasojen painotettu keskiarvo, joten se voi poiketa tilannekuvassa näkyvästä pistemäärästä." + }, + "documentTitle": "Kasvokirjasto - Frigate", + "deleteFaceAttempts": { + "desc_one": "Oletko varma, että haluat poistaa {{count}} kasvon? Tätä toimintoa ei voi perua.", + "desc_other": "Oletko varma, että haluat poistaa {{count}} kasvoa? Tätä toimintoa ei voi perua.", + "title": "Poista kasvot" + }, + "toast": { + "success": { + "deletedFace_one": "{{count}} kasvo poistettu onnistuneesti.", + "deletedFace_other": "{{count}} kasvoa poistettu onnistuneesti.", + "uploadedImage": "Kuva ladattu onnistuneesti." + } + }, + "selectItem": "Valitse {{item}}", + "train": { + "empty": "Ei viimeaikaisia kasvojentunnistusyrityksiä", + "title": "Koulutus", + "aria": "Valitse kouluta" + }, + "collections": "Kokoelmat", + "steps": { + "faceName": "Anna nimi kasvoille", + "uploadFace": "Lähetä kasvokuva", + "nextSteps": "Seuraavat vaiheet", + "description": { + "uploadFace": "Lataa kuva henkilöstä {{name}}, jossa hänen kasvonsa näkyvät suoraan edestä päin. Kuvaa ei tarvitse rajata pelkkiin kasvoihin." + } + }, + "createFaceLibrary": { + "title": "Luo kokoelma", + "desc": "Luo uusi kokoelma", + "new": "Luo uusi kasvo", + "nextSteps": "Hyvän perustan luomiseksi huomioitavaa:
  • Käytä koulutus-välilehteä valitaksesi opetukseen kuvia kustakin tunnistetusta henkilöstä
  • Panosta mahdollisimman suoraan otettuihin kuviin; vältä kouluttamista kulmassa kuvatuilla kuvilla.
  • " + }, + "selectFace": "Valitse kasvo", + "deleteFaceLibrary": { + "title": "Poista nimi", + "desc": "Haluatko varmasti poistaa kokoelman {{name}}? Tämä poistaa pysyvästi kaikki liitetyt kasvot." + }, + "renameFace": { + "title": "Uudelleennimeä kasvot", + "desc": "Anna uusi nimi tälle {{name}}" + }, + "button": { + "deleteFaceAttempts": "Poista kasvot", + "addFace": "Lisää kasvot", + "renameFace": "Uudelleennimeä kasvot", + "deleteFace": "Poista kasvot", + "uploadImage": "Lataa kuva", + "reprocessFace": "Uudelleenprosessointi Kasvot" + }, + "imageEntry": { + "validation": { + "selectImage": "Valitse kuvatiedosto." + }, + "dropActive": "Pudota kuva tähän…", + "dropInstructions": "Vedä ja pudota kuva tähän tai valitse se napsauttamalla", + "maxSize": "Maksimikoko: {{size}}MB" + }, + "nofaces": "Kasvoja ei ole saatavilla", + "pixels": "{{area}}px", + "trainFace": "Kouluta kasvot" +} diff --git a/web/public/locales/fi/views/live.json b/web/public/locales/fi/views/live.json new file mode 100644 index 0000000..93984cf --- /dev/null +++ b/web/public/locales/fi/views/live.json @@ -0,0 +1,173 @@ +{ + "documentTitle": { + "default": "Suora - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Suora - Frigate", + "lowBandwidthMode": "Pienen kaistanleveyden tila", + "twoWayTalk": { + "enable": "Ota käyttöön kaksisuuntainen puhe", + "disable": "Poista kaksisuuntainen puhe käytöstä" + }, + "cameraAudio": { + "enable": "Ota kameran ääni käyttöön", + "disable": "Poista kameran ääni käytöstä" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Napsauta ruutua keskittääksesi kameran", + "enable": "Ota käyttöön napsauttamalla siirtäminen", + "disable": "Poista napsauttamalla siirtäminen" + }, + "left": { + "label": "Siirrä PTZ-kameraa vasemmalle" + }, + "up": { + "label": "Siirrä PTZ-kameraa ylös" + }, + "down": { + "label": "Siirrä PTZ-kameraa alas" + }, + "right": { + "label": "Siirrä PTZ-kameraa oikealle" + } + }, + "zoom": { + "out": { + "label": "Zoomaa PTZ-kamera ulos" + }, + "in": { + "label": "Zoomaa PTZ-kamera sisään" + } + }, + "frame": { + "center": { + "label": "Napsauta kehystä keskittääksesi PTZ-kamera" + } + }, + "presets": "PTZ-kameroiden esiasetukset", + "focus": { + "in": { + "label": "Tarkenna PTZ-kamera sisään" + }, + "out": { + "label": "Tarkenna PTZ-kamera ulos" + } + } + }, + "camera": { + "enable": "Ota kamera käyttöön", + "disable": "Poista kamera käytöstä" + }, + "muteCameras": { + "enable": "Mykistä kaikki kamerat", + "disable": "Poista kaikkien kameroiden mykistys" + }, + "detect": { + "enable": "Ota tunnistus käyttöön", + "disable": "Poista tunnistus käytöstä" + }, + "recording": { + "enable": "Ota tallennus käyttöön", + "disable": "Poista tallennus käytöstä" + }, + "snapshots": { + "enable": "Ota tilannekuva käyttöön", + "disable": "Poista tilannekuva käytöstä" + }, + "audioDetect": { + "enable": "Ota käyttöön äänen tunnistus", + "disable": "Poista äänen tunnistus käytöstä" + }, + "autotracking": { + "enable": "Ota automaattinen seuranta käyttöön", + "disable": "Poista automaattinen seuranta käytöstä" + }, + "streamStats": { + "enable": "Näytä suoratoiston tilastot", + "disable": "Piilota suoratoiston tilastot" + }, + "manualRecording": { + "title": "Tallennus pyynnöstä", + "tips": "Aloita manuaalinen tapahtuma tämän kameran tallenteen tallennusasetusten perusteella.", + "playInBackground": { + "label": "Toista taustalla", + "desc": "Ota tämä asetus käyttöön, jos haluat jatkaa suoratoistoa kun soitin on piilotettu." + }, + "showStats": { + "label": "Näytä tilastot", + "desc": "Ota tämä asetus käyttöön, jos haluat näyttää suoratoistotilastot kamerasyötteen päällä." + }, + "debugView": "Virheenkorjausnäkymä", + "start": "Aloita tallennus pyynnöstä", + "started": "Manuaalinen pyynnöstätallennus aloitettu.", + "failedToStart": "Manuaalisen pyynnöstätallennuksen aloittaminen epäonnistui.", + "recordDisabledTips": "Koska tallennus on poistettu käytöstä tai rajoitettu tämän kameran asetuksissa, vain tilannekuva tallennetaan.", + "end": "Lopeta pyynnöstätallennus", + "ended": "Manuaalinen on-demand-tallennus lopetettu.", + "failedToEnd": "Manuaalisen pyynnöstätallennuksen lopettaminen epäonnistui." + }, + "streamingSettings": "Suoratoistoasetukset", + "notifications": "Ilmoitukset", + "audio": "Ääni", + "suspend": { + "forTime": "Keskeytys: " + }, + "stream": { + "title": "Suoratoisto", + "audio": { + "tips": { + "title": "Äänen on oltava kytkettynä kameraan ja määritettynä go2rtc:ssä tätä suoratoistoa varten.", + "documentation": "Lue dokumentaatio " + }, + "available": "Ääni on saatavilla tälle suoratoistolle", + "unavailable": "Ääni ei ole saatavilla tälle suoratoistolle" + }, + "twoWayTalk": { + "tips": "Laitteesi on tuettava ominaisuutta ja WebRTC:n on oltava määritetty kaksisuuntaista ääntä varten.", + "tips.documentation": "Lue dokumentaatio ", + "available": "Kaksisuuntainen ääni on saatavilla tässä suoratoistossa", + "unavailable": "Kaksisuuntainen ääni ei ole käytettävissä tässä suoratoistossa" + }, + "lowBandwidth": { + "tips": "Live-näkymä on matalan kaistanleveyden tilassa puskuroinnin tai suoratoistovirheiden vuoksi.", + "resetStream": "Nollaa suoratoisto" + }, + "playInBackground": { + "label": "Toista taustalla", + "tips": "Ota tämä asetus käyttöön, jos haluat jatkaa suoratoistoa, kun soitin on piilotettu." + } + }, + "cameraSettings": { + "title": "{{camera}} Asetukset", + "cameraEnabled": "Kamera käytössä", + "objectDetection": "Kohteen tunnistus", + "recording": "Nauhoitus", + "snapshots": "Tilannekuvat", + "audioDetection": "Äänen tunnistus", + "autotracking": "Automaattinen seuranta", + "transcription": "Äänitranskriptio" + }, + "history": { + "label": "Näytä historiallista materiaalia" + }, + "effectiveRetainMode": { + "modes": { + "all": "Kaikki", + "motion": "Liike", + "active_objects": "Aktiiviset kohteet" + }, + "notAllTips": "{{source}}-tallenteiden säilytysmäärityksesi on asetettu tila: {{effectiveRetainMode}}, joten tämä tilattu tallenne säilyttää vain ne osat joiden tyyppi on {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Muokkaa asettelua", + "group": { + "label": "Muokkaa kameraryhmää" + }, + "exitEdit": "Poistu muokkauksesta" + }, + "transcription": { + "enable": "Ota käyttöön reaaliaikainen äänitranskriptio", + "disable": "Poista käytöstä reaaliaikainen äänitranskriptio" + } +} diff --git a/web/public/locales/fi/views/motionSearch.json b/web/public/locales/fi/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/fi/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/fi/views/recording.json b/web/public/locales/fi/views/recording.json new file mode 100644 index 0000000..84daba2 --- /dev/null +++ b/web/public/locales/fi/views/recording.json @@ -0,0 +1,12 @@ +{ + "calendar": "Kalenteri", + "filter": "Suodatin", + "filters": "Suodattimet", + "toast": { + "error": { + "noValidTimeSelected": "Sopimaton aikaväli valittu", + "endTimeMustAfterStartTime": "Loppuaika täytyy olla aloituksen jälkeen" + } + }, + "export": "Vie" +} diff --git a/web/public/locales/fi/views/replay.json b/web/public/locales/fi/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/fi/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/fi/views/search.json b/web/public/locales/fi/views/search.json new file mode 100644 index 0000000..887c9e0 --- /dev/null +++ b/web/public/locales/fi/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Etsi", + "savedSearches": "Tallennetut haut", + "searchFor": "Etsi {{inputValue}}", + "button": { + "clear": "Tyhjennä haku", + "save": "Tallenna haku", + "delete": "Poista tallennettu haku", + "filterInformation": "Suodattimen tiedot", + "filterActive": "Suodattimia valittuina" + }, + "trackedObjectId": "Seuratun kohteen ID", + "filter": { + "label": { + "cameras": "Kamerat", + "labels": "Nimikkeet", + "zones": "Alueet", + "sub_labels": "Alinimikkeet", + "search_type": "Haun tyyppi", + "time_range": "Aikaikkuna", + "before": "Ennen", + "after": "Jälkeen", + "min_score": "Minimi pisteet", + "max_score": "Maksimi pisteet", + "min_speed": "Minimi nopeus", + "max_speed": "Maksimi nopeus", + "recognized_license_plate": "Tunnistettu rekisterikilpi", + "has_clip": "Leike löytyy", + "has_snapshot": "Tilannekuva löytyy" + }, + "searchType": { + "thumbnail": "Kuvake", + "description": "Kuvaus" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "'Ennen' ajan täytyy olla myöhemmin kun 'jälkeen' aika.", + "afterDatebeEarlierBefore": "'Jälkeen' ajan täytyy olla aiemmin kun 'ennen' aika.", + "minScoreMustBeLessOrEqualMaxScore": "Arvon 'min_score' täytyy olla pienempi tai yhtäsuuri kuin 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Arvon 'max_score' täytyy olla suurempi tai yhtäsuuri kuin 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'Minimi nopeus' tulee olla pienempi tai yhtäsuuri kuin 'maksimi nopeus'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'Maksimi nopeus' tulee olla suurempi tai yhtä suuri kuin 'minimi nopeus'." + } + }, + "tips": { + "desc": { + "exampleLabel": "Esimerkki:", + "step6": "Poista suodattimet napsauttamalla niiden vieressä olevaa 'x' merkkiä.", + "text": "Suodattimien avulla voit rajata hakutuloksia. Näin käytät niitä syöttökentässä:", + "step1": "Kirjoita suodattimen avaimen nimi ja sen perään kaksoispiste (esim. ”kamerat:”).", + "step2": "Valitse arvo ehdotuksista tai kirjoita oma arvo.", + "step3": "Käytä useita suodattimia lisäämällä ne peräkkäin välilyönnillä erotettuina.", + "step4": "Päivämääräsuodattimet (ennen: ja jälkeen:) käyttävät {{DateFormat}} muotoa.", + "step5": "Aikavälin suodatin käyttää {{exampleTime}} muotoa." + }, + "title": "Tekstisuodattimien käyttö" + }, + "header": { + "currentFilterType": "Suodata arvoja", + "noFilters": "Suodattimet", + "activeFilters": "Käytössä olevat suodattimet" + } + }, + "similaritySearch": { + "title": "Samankaltaisten kohteiden haku", + "active": "Samankaltaisuushaku aktiivinen", + "clear": "Poista samankaltaisuushaku" + }, + "placeholder": { + "search": "Hae…" + } +} diff --git a/web/public/locales/fi/views/settings.json b/web/public/locales/fi/views/settings.json new file mode 100644 index 0000000..c988472 --- /dev/null +++ b/web/public/locales/fi/views/settings.json @@ -0,0 +1,582 @@ +{ + "documentTitle": { + "camera": "Kamera-asetukset - Frigate", + "classification": "Klassifiointiasetukset - Frigate", + "masksAndZones": "Peite ja vyöhykemuokkain - Frigate", + "motionTuner": "Liikesäädin - Frigate", + "default": "Asetukset - Frigate", + "general": "Yleiset asetukset - Frigate", + "frigatePlus": "Frigate+ asetukset - Frigate", + "object": "Virheenjäljitys - Frigate", + "authentication": "Autentikointiasetukset - Frigate", + "notifications": "Ilmoitusasetukset - Frigate", + "enrichments": "Laajennusasetukset – Frigate", + "cameraManagement": "Hallitse Kameroita - Frigate", + "globalConfig": "Globaali konfiguraatio - Frigate", + "cameraConfig": "Kamera konfiguraatio - Frigate" + }, + "menu": { + "ui": "Käyttöliittymä", + "cameras": "Kameroiden asetukset", + "users": "Käyttäjät", + "classification": "Klassifiointi", + "frigateplus": "Frigate+", + "masksAndZones": "Maskit / alueet", + "debug": "Debuggaus", + "motionTuner": "Liikesäädin", + "notifications": "Ilmoitukset", + "enrichments": "Rikasteet", + "triggers": "Laukaisimet" + }, + "dialog": { + "unsavedChanges": { + "desc": "Haluatko tallentaa muutokset ennen jatkamista?", + "title": "Et ole tallentanut muutoksia." + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Ei kameraa" + }, + "general": { + "title": "Yleiset asetukset", + "liveDashboard": { + "automaticLiveView": { + "label": "Automaattinen reaaliaika-näkymä", + "desc": "Vaihda automaattisesti reaaliaikaiseen kameranäkymään kun liikettä on huomattu. Mikäli asetus on kytketty pois päivittyy reaaliaikaisen kojelaudan kuva vain kerran minuutissa." + }, + "title": "Reaaliaikainen kojelauta", + "playAlertVideos": { + "label": "Näytä hälyytysvideot", + "desc": "Vakiona viimeaikaiset hälytykset pyörivät pieninä luuppaavina videoina reaaliaikaisella kojelaudalla. Ota tämä asetus pois päältä näyttääksesi vain staattisen kuvan viimeaikaisista hälytyksistä tässä laitteessa/selaimessa." + } + }, + "storedLayouts": { + "title": "Tallennetut sijoittelut", + "desc": "Kameroiden sijoittelua kameraryhmissä voidaan raahata tai niiden kokoa muuttaa. Sijainnit tallennetaan selaimen paikalliseen muistiin.", + "clearAll": "Tyhjennä kaikki sijoittelut" + }, + "cameraGroupStreaming": { + "title": "Kameraryhmän striimauksen asetukset", + "desc": "Striimauksen asetukset jokaiselle kameraryhmälle tallennetaan selaimesi paikalliseen muistiin.", + "clearAll": "Tyhjennä kaikkai striimauksen asetukset" + }, + "recordingsViewer": { + "title": "Tallennusten näyttäjä", + "defaultPlaybackRate": { + "label": "Toiston vakionopeus", + "desc": "Toiston vakionopeus tallennusten näytölle." + } + }, + "calendar": { + "title": "Kalenteri", + "firstWeekday": { + "label": "Viikon ensimmäinen päivä", + "desc": "Päivä josta kertauskalenterin viikot alkaa.", + "sunday": "sunnuntai", + "monday": "maanantai" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Tyhjennä tallennetut sijoittelut kameralle nimeltä {{cameraName}}", + "clearStreamingSettings": "Tyhjennä striimausasetukset kaikista kameraryhmistä." + }, + "error": { + "clearStoredLayoutFailed": "Sijoittelujen tyhjentäminen ei onnistunut: {{errorMessage}}", + "clearStreamingSettingsFailed": "Striimausasetusten tyhjentäminen ei onnistunut: {{errorMessage}}" + } + } + }, + "classification": { + "title": "Klassifiointiasetukset", + "semanticSearch": { + "reindexNow": { + "label": "Uudelleen indeksoi nyt", + "confirmDesc": "Oletko varma että haluat indeksoida uudelleen kaikki seurattujen kohteiden upotukset? Tämä prosessi toimii taustalla ja saattaa maksimoida prosessorin käytön sekä viedä runsaasti aikaa. Voit seurata prosessin etenemistä tarkastelu -sivulta.", + "desc": "Indeksoinnin luominen uudelleen jälleenrakentaa upotukset kaikkiin seurattuihin kohteisiin. Tämä prosessi toimii taustalla ja saattaa maksimoida prosessorin käytön sekä viedä reilusti aikaa riippuen paljonko seurattavia kohteita sinulla on.", + "confirmButton": "Indeksoi uudelleen", + "success": "Uudelleen indeksointi aloitettiin onnistuneesti.", + "alreadyInProgress": "Uudelleen indeksointi on jo käynnissä.", + "error": "Uudelleen indeksointia ei voitu aloittaa: {{errorMessage}}", + "confirmTitle": "Vahvista uudelleen indeksointi" + }, + "modelSize": { + "label": "Mallin koko", + "small": { + "desc": "Valitessa pieni käytetään kvantisoitunutta versiota mallista joka käyttää vähemmän muistia sekä prosesoria upotuksen laatueron ollessa lähes olematon.", + "title": "pieni" + }, + "large": { + "desc": "Valittaessa suuri käytettään täyttä Jina-mallia joka ajetaan automaattisesti grafiikkaytimellä mikäli mahdollista.", + "title": "suuri" + }, + "desc": "Semanttisen haun upotuksiin käytetyn mallin koko." + }, + "title": "Semanttinen haku", + "readTheDocumentation": "Lue dokumentaatio", + "desc": "Frigaten semanttisen haun kanssa voit hakea seurattuja kohteita esikatseluista joko kuvasta itsestään, käyttäjän määrittelemän teksti-kuvauksen perusteella tai automaattisesti generoidun kuvauksen kanssa." + }, + "faceRecognition": { + "title": "Kasvojentunnistus", + "readTheDocumentation": "Lue dokumentaatio", + "modelSize": { + "label": "Mallin koko", + "desc": "Kasvojentunnistukseen käytetyn mallin koko.", + "small": { + "title": "pieni", + "desc": "Valitessa pieni FaceNet käyttää kasvojen upotukseen mallia joka toimii tehokkaasti suurimmalla osalla prosessoreista." + }, + "large": { + "title": "suuri", + "desc": "Valitessa suuri käytetään ArcFace mallia kasvojen upotukseen joka ajetaan automaattisesti grafiikkaprosessorilla mikäli mahdollista." + } + }, + "desc": "Kasvojentunnistus sallii nimien antamisen ihmisille ja kun heidän kasvonsa tunnistetaan Frigate antaa henkilölle nimen ala-viittenä. Tämä tieto sisällytetään käyttöliittymään, filttereihin sekä ilmoituksiin." + }, + "licensePlateRecognition": { + "title": "Rekisterikilven tunnistus", + "desc": "Frigate voi tunnistaa ajoneuvojen rekisterikilpiä ja lisätä tunnistetut kirjaimet automaattisesti recognized_license_plate -kenttään tai tunnettu nimi sub_label kohteisiin joiden tyyppi on ajoneuvo. Yleinen käyttökohde on lukea pihatielle ajavien tai kadulla ohiajavien ajoneuvojen rekisterikilvet.", + "readTheDocumentation": "Lue dokumentaatio" + }, + "toast": { + "success": "Klassifiointiasetukset on tallennettu. Käynnistä Frigate uudelleen saadaksesi ne käyttöön.", + "error": "Konfiguraatio muutoksia ei voitu tallentaa: {{errorMessage}}" + }, + "restart_required": "Tarvitaan uudelleenkäynnistys (luokitusasetuksia muutettu)", + "birdClassification": { + "title": "Lintujen luokittelu", + "desc": "Lintujen luokittelu tunnistaa tunnetut linnut kvantisoidun Tensorflow-mallin avulla. Kun tunnettu lintu tunnistetaan, sen yleinen nimi lisätään alitunnisteena. Tämä tieto sisältyy käyttöliittymään, suodattimiin ja ilmoituksiin." + } + }, + "camera": { + "title": "Kamera-asetukset", + "streams": { + "title": "Striimit", + "desc": "Poista kamera käytöstä väliaikaisesti, kunnes Frigate uudelleenkäynnistetään. Kameran poiskytkeminen lopettaa kameran videostriimien käsittelyn. Havainnot, tallennus ja debuggaus ovat pois käytöstä.
    Huom: tämä ei poista käytöstä go2rtc uusinta striimejä." + }, + "review": { + "title": "Katselu", + "desc": "Kytke väliaikaisesti päälle/pois hälytykset ja tunnistus tälle kameralle, kunnes Frigate käynnistetään uudelleen. Kun ne ovat pois päältä, uusia katseltavia tapahtumia ei luoda. ", + "alerts": "Hälytykset ", + "detections": "Tunnistukset " + }, + "reviewClassification": { + "title": "Katseluiden klassifiointi", + "readTheDocumentation": "Lue dokumentaatio", + "noDefinedZones": "Tälle kameralle ei ole määritelty vyöhykkeitä.", + "objectAlertsTips": "Kaikki {{alertsLabels}} objektit lähteelle {{cameraName}} näytetään hälytyksinä.", + "zoneObjectAlertsTips": "Kaikki {{alertsLabels}} objektit jotka tunnistetaan alueella {{zone}} lähteessä {{cameraName}} näytetään Hälytyksinä.", + "objectDetectionsTips": "Kaikki {{detectionsLabels}} objektit joita ei ole kategorisoitu lähteessä {{cameraName}} näytetään Tunnistuksina niiden vyöhykkeestä huolimatta.", + "zoneObjectDetectionsTips": { + "text": "Kaikki {{detectionsLabels}} objektit joita ei ole kategorisoitu vyöhykkeellä {{zone}} lähteessä {{cameraName}} näytetään Tunnistuksina.", + "notSelectDetections": "Kaikki {{detectionsLabels}} objektit jotka tunnistetaan vyöhykkeellä {{zone}} lähteessä {{cameraName}}, joita ei ole kategorisoitu Hälytyksiksi näytetään Tunnistuksina niiden vyöhykkeestä huolimatta.", + "regardlessOfZoneObjectDetectionsTips": "Kaikki {{detectionsLabels}} objektit joita ei ole kategorisoitu lähteessä {{cameraName}} näytetään Tunnistuksina niiden vyöhykkeestä huolimatta." + }, + "selectAlertsZones": "Valitse vyöhykkeet Hälytystä varten", + "desc": "Frigate kategorisoi tahtumia Hälytyksiksi ja Tunnistuksiksi. Vakiona kaikki henkilö sekä ajoneuvo objektit käsitellään Hälytyksinä. Voit kategorisoida uudelleen katseltavat tapahtumat antamalla niille vaaditut alueet.", + "limitDetections": "Rajoita tunnistukset tiettyihin vyöhykkeisiin", + "selectDetectionsZones": "Valitse vyöhykkeet tunnistusta varten", + "toast": { + "success": "Luokittelumäärityksen tarkistus on tallennettu. Käynnistä Frigate uudelleen muutosten käyttöönottamiseksi." + } + }, + "cameraConfig": { + "add": "Lisää kamera", + "ffmpeg": { + "addInput": "Lisää tulovirta" + } + }, + "addCamera": "Lisää uusi kamera" + }, + "masksAndZones": { + "filter": { + "all": "Kaikki peitteet ja vyöhykkeet" + }, + "form": { + "polygonDrawing": { + "delete": { + "desc": "Oletko varma että haluat poistaa {{type}}{{name}}?", + "success": "{{name}} on poistettu.", + "title": "Varmista poistaminen" + }, + "error": { + "mustBeFinished": "Polygonien piirron pitää olla valmis ennen tallennusta." + }, + "removeLastPoint": "Poista edellinen piste", + "snapPoints": { + "true": "Napsauta pisteet", + "false": "Älä napsauta pisteitä" + }, + "reset": { + "label": "Poista kaikki pisteet" + } + }, + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Alueen nimen tulee olla vähintään 2 merkin pituinen.", + "alreadyExists": "Tämän niminen alue on jo olemassa.", + "mustNotContainPeriod": "Alueen nimessä ei saa olla pisteitä.", + "hasIllegalCharacter": "Vyöhykkeen nimessä on kiellettyjä merkkejä.", + "mustNotBeSameWithCamera": "Alueen nimi ei saa olla sama kuin kameran nimi." + } + }, + "distance": { + "error": { + "text": "Välimatkan tulee olla suurempi tai yhtä suuri kuin 0.1.", + "mustBeFilled": "Kaikki välimatka -kentät tulee olla täytetty jotta nopeusarviota voidaan käyttää." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Inertian tulee olla yli 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Oleiluaika tulee olla suurempi tai yhtä suuri kuin 0." + } + } + }, + "zones": { + "label": "Vyöhykkeet", + "documentTitle": "Muokkaa vyöhykkeitä - Frigate", + "edit": "Myokkaa vyöhykettä", + "inertia": { + "title": "Inertia", + "desc": "Määrittää, kuinka monta kehystä objektin on oltava vyöhykkeellä, ennen kuin se lasketaan vyöhykkeeksi. Oletus: 3" + }, + "loiteringTime": { + "title": "Oleskeluaika", + "desc": "Asettaa vähimmäisajan sekunteina, jonka objektin on oltava vyöhykkeellä, jotta se aktivoituu. Oletus: 0" + }, + "objects": { + "title": "Kohteet", + "desc": "Luettelo tähän vyöhykkeeseen liittyvistä kohteista." + }, + "desc": { + "title": "Vyöhykkeiden avulla voit määrittää tietyn alueen kuvassa, jotta voit selvittää, onko kohde kyseisellä alueella.", + "documentation": "Dokumentaatio" + }, + "add": "Lisää vyöhyke", + "clickDrawPolygon": "Napsauta piirtääksesi monikulmion kuvaan.", + "name": { + "title": "Nimi", + "inputPlaceHolder": "Anna nimi…", + "tips": "Nimen on oltava vähintään kaksi merkkiä pitkä, eikä se saa olla kameran tai toisen vyöhykkeen nimi." + }, + "point_one": "{{count}} piste", + "point_other": "{{count}} pisteet", + "allObjects": "Kaikki kohteet", + "speedEstimation": { + "title": "Nopeuden arviointi", + "desc": "Ota käyttöön nopeuden arviointi tällä vyöhykkeellä oleville kohteille. Sillä on oltava täsmälleen 4 pistettä." + }, + "speedThreshold": { + "title": "Nopeuskynnys ({{unit}})", + "desc": "Määrittää kohteiden vähimmäisnopeuden, joka otetaan huomioon tässä vyöhykkeessä.", + "toast": { + "error": { + "pointLengthError": "Nopeuden arviointi on poistettu käytöstä tällä vyöhykkeellä. Vyöhykkeillä joilla on nopeuden arviointi, on oltava täsmälleen 4 pistettä.", + "loiteringTimeError": "Vyöhykkeitä, joiden oleskeluajat ovat yli 0, ei tule käyttää nopeuden arvioinnissa." + } + } + }, + "toast": { + "success": "Vyöhyke ({{zoneName}}) on tallennettu. Käynnistä Frigatti uudelleen muutosten käyttöönottamiseksi." + } + }, + "toast": { + "error": { + "copyCoordinatesFailed": "Koordinaattien kopioiminen leikepöydälle epäonnistui." + }, + "success": { + "copyCoordinates": "{{polyName}} - koordinaatit kopioitu leikepöydälle." + } + }, + "restart_required": "Uudelleenkäynnistys vaaditaan (peitteitä/vyöhykeitä muutettu)", + "motionMasks": { + "point_one": "{{count}} piste", + "point_other": "{{count}} pisteet", + "clickDrawPolygon": "Napsauta piirtääksesi monikulmion kuvaan.", + "label": "Liikepeitto", + "context": { + "documentation": "Lue dokumentaatio", + "title": "Liikepeittoja käytetään estämään ei-toivottujen liiketyyppien (esimerkiksi puiden oksat, kameroiden aikaleimat) aiheuttamat tunnistukset. Liikepeittoja tulisi käyttää hyvin säästeliäästi, sillä liiallinen maskaus vaikeuttaa kohteiden seurantaa." + }, + "documentTitle": "Muokkaa liikepeittoa - Frigate", + "desc": { + "title": "Liikepeittoa käytetään estämään ei-toivottujen liiketyyppien aiheuttamat tunnistukset. Liiallinen peittäminen vaikeuttaa kohteiden seurantaa.", + "documentation": "Dokumentaatio" + }, + "add": "Uusi liikepeitto", + "edit": "Muokkaa liikepeittoa", + "polygonAreaTooLarge": { + "title": "Liikepeitto peittää {{polygonArea}}% kameran kuvasta. Suuria liikemaskeja ei suositella.", + "tips": "Liikepeitto eivät estä kohteiden havaitsemista. Sinun tulisi sen sijaan käyttää vaadittua vyöhykettä.", + "documentation": "Lue dokumentaatio" + }, + "toast": { + "success": { + "title": "{{polygonName}} on tallennettu. Käynnistä Frigatti uudelleen muutosten käyttöönottamiseksi.", + "noName": "Liikepeitto on tallennettu. Käynnistä Frigatti uudelleen muutosten käyttöönottamiseksi." + } + } + }, + "objectMasks": { + "point_one": "{{count}} piste", + "point_other": "{{count}} pisteet", + "label": "Kohdepeitot", + "context": "Objektisuodatinpeittoja käytetään suodattamaan pois väärät positiiviset tulokset tietylle kohdetyypille sijainnin perusteella.", + "objects": { + "title": "Kohteet", + "desc": "Kohdetyyppi, jota käytetään tähän kohdepeittoon.", + "allObjectTypes": "Kaikki kohdetyypit" + }, + "toast": { + "success": { + "title": "{{polygonName}} on tallennettu. Käynnistä Frigatti uudelleen muutosten käyttöönottamiseksi.", + "noName": "Kohdepeitto on tallennettu. Käynnistä Frigatti uudelleen muutosten käyttöönottamiseksi." + } + }, + "documentTitle": "Muokkaa kohdepeittoa - Frigate", + "desc": { + "title": "Objektisuodatinpeittoja käytetään suodattamaan pois väärät positiiviset tulokset tietylle kohdetyypille sijainnin perusteella.", + "documentation": "Dokumentaatio" + }, + "add": "Lisää kohdepeitto", + "edit": "Muokkaa kohdepeittoa", + "clickDrawPolygon": "Napsauta piirtääksesi monikulmion kuvaan." + } + }, + "debug": { + "regions": { + "title": "Alueet", + "desc": "Näytä kohdeilmaisimelle lähetetyn kiinnostuksen kohteena olevan alueen laatikko", + "tips": "

    Aluelaatikot


    Kirkkaanvihreät laatikot peittävät kuvassa olevat kiinnostavat alueet, jotka lähetetään objektinilmaisimelle.

    " + }, + "objectShapeFilterDrawing": { + "title": "Objektin muodon suodattimen piirtäminen", + "desc": "Piirrä kuvaan suorakulmio nähdäksesi pinta-alan ja kuvasuhteen tiedot", + "document": "Lue dokumentaatio ", + "score": "Pisteet", + "ratio": "Suhde", + "area": "Alue", + "tips": "Ota tämä asetus käyttöön piirtääksesi kamerakuvaan suorakulmion, joka näyttää sen pinta-alan ja suhteen. Näitä arvoja voidaan sitten käyttää objektin muodon suodatusparametrien asettamiseen asetuksissasi." + }, + "timestamp": { + "title": "Aikaleima", + "desc": "Lisää aikaleima kuvan päälle" + }, + "noObjects": "Ei kohteita", + "zones": { + "title": "Vyöhykkeet", + "desc": "Näytä määriteltyjen vyöhykkeiden ääriviivat" + }, + "boundingBoxes": { + "colors": { + "info": "
  • Käynnistettäessä kullekin kohteen merkinnälle määritetään eri värit
  • Tummansininen ohut viiva osoittaa, että kohdetta ei ole havaittu tällä hetkellä
  • Harmaa ohut viiva osoittaa, että kohde on havaittu paikallaan olevaksi
  • Paksu viiva osoittaa, että kohde on automaattisen seurannan kohteena (kun se on käytössä)
  • " + } + }, + "mask": { + "title": "Liikepeitot", + "desc": "Näytä liikepeiton monikulmiot" + }, + "motion": { + "title": "Liikelaatikot", + "desc": "Näytä laatikot alueiden ympärillä, joilla liikettä havaitaan", + "tips": "

    Liikelaatikot


    Punaiset laatikot peittävät ruudun alueet, joilla liikettä havaitaan parhaillaan.

    " + } + }, + "users": { + "title": "Käyttäjät", + "management": { + "title": "Käyttäjien hallinta", + "desc": "Hallinnoi tämän Frigate-instanssin käyttäjätilejä." + }, + "addUser": "Lisää käyttäjä", + "updatePassword": "Päivitä salasana", + "toast": { + "success": { + "roleUpdated": "Rooli päivitetty käyttäjälle {{user}}", + "createUser": "Käyttäjä {{user}} luotu onnistuneesti", + "deleteUser": "Käyttäjä {{user}} poistettu onnistuneesti", + "updatePassword": "Salasana päivitetty onnistuneesti." + }, + "error": { + "setPasswordFailed": "Salasanan tallentaminen epäonnistui: {{errorMessage}}", + "createUserFailed": "Käyttäjän luonti epäonnistui: {{errorMessage}}", + "roleUpdateFailed": "Roolin päivittäminen epäonnistui: {{errorMessage}}", + "deleteUserFailed": "Käyttäjän poisto epäonistui: {{errorMessage}}" + } + }, + "table": { + "username": "Käyttäjänimi", + "actions": "Toiminnot", + "noUsers": "Käyttäjiä ei löytynyt.", + "changeRole": "Vaihda käyttäjäroolia", + "password": "Salasana", + "deleteUser": "Poista tili", + "role": "Rooli" + }, + "dialog": { + "form": { + "user": { + "desc": "Vain kirjaimet, numerot, pisteet ja alaviivat sallitaan.", + "placeholder": "Syötä käyttäjätunnus", + "title": "Käyttäjätunnus" + } + }, + "changeRole": { + "roleInfo": { + "admin": "Ylläpitäjä" + } + } + } + }, + "motionDetectionTuner": { + "title": "Liiketunnistuksen säätäminen", + "desc": { + "title": "Frigate käyttää liiketunnistusta ensimmäisenä tarkistuksena nähdäkseen, tapahtuuko kuvassa jotain, mikä kannattaisi tarkistaa objektitunnistuksella.", + "documentation": "Lue liikkeensäädön opas" + }, + "Threshold": { + "title": "Kynnys" + } + }, + "triggers": { + "documentTitle": "Laukaisimet", + "management": { + "title": "Laukaisimen hallinta" + }, + "addTrigger": "Lisää laukaisin", + "table": { + "name": "Nimi", + "type": "Tyyppi", + "content": "Sisältö", + "threshold": "Kynnys", + "actions": "Toiminnot", + "noTriggers": "Tälle kameralle ei ole määritetty laukaisimia.", + "edit": "Muokkaa", + "deleteTrigger": "Poista laukaisin", + "lastTriggered": "Viimeksi laukaistu" + }, + "type": { + "thumbnail": "Kuvake", + "description": "Kuvaus" + }, + "actions": { + "notification": "Lähetä ilmoitus", + "alert": "Merkitse hälytykseksi" + }, + "dialog": { + "createTrigger": { + "title": "Luo laukaisin", + "desc": "Luo laukaisin kameralle {{camera}}" + }, + "editTrigger": { + "title": "Muokkaa laukaisinta", + "desc": "Muokkaa laukaisimen asetuksia kamerasta {{camera}}" + }, + "deleteTrigger": { + "title": "Poista laukaisin", + "desc": "Haluatko varmasti poistaa laukaisimen {{triggerName}}? Tätä toimintoa ei voi peruuttaa." + }, + "form": { + "name": { + "title": "Nimi", + "placeholder": "Syötä laukaisimen nimi", + "error": { + "minLength": "Nimen on oltava vähintään 2 merkkiä pitkä.", + "invalidCharacters": "Nimi voi sisältää vain kirjaimia, numeroita, alaviivoja ja väliviivoja.", + "alreadyExists": "Tällä nimellä oleva laukaisin on jo olemassa tälle kameralle." + } + }, + "enabled": { + "description": "Ota tämä laukaisin käyttöön tai pois käytöstä" + }, + "type": { + "title": "Tyyppi", + "placeholder": "Valitse laukaisintyyppi" + }, + "content": { + "title": "Sisältö", + "imagePlaceholder": "Valitse kuva", + "textPlaceholder": "Kirjoita tekstisisältö", + "imageDesc": "Valitse kuva, joka laukaisee tämän toiminnon, kun samankaltainen kuva havaitaan.", + "textDesc": "Syötä teksti, joka laukaisee tämän toiminnon, kun vastaava seurattavan kohteen kuvaus havaitaan.", + "error": { + "required": "Sisältö on pakollinen." + } + }, + "threshold": { + "title": "Kynnys", + "error": { + "min": "Kynnys on oltava vähintään 0", + "max": "Kynnys on oltava enintään 1" + } + }, + "actions": { + "title": "Toiminnot", + "desc": "Oletuksena Frigate lähettää MQTT-viestin kaikille laukaisimille. Valitse lisätoiminto, joka suoritetaan, kun tämä laukaisija laukeaa.", + "error": { + "min": "Vähintään yksi toiminto on valittava." + } + } + } + }, + "toast": { + "success": { + "createTrigger": "Laukaisin {{name}} luotu onnistuneesti.", + "updateTrigger": "Laukaisin {{name}} päivitetty onnistuneesti.", + "deleteTrigger": "Laukaisin {{name}} poistettu onnistuneesti." + }, + "error": { + "createTriggerFailed": "Laukaisimen luominen epäonnistui: {{errorMessage}}", + "updateTriggerFailed": "Laukaisimen päivitys epäonnistui: {{errorMessage}}", + "deleteTriggerFailed": "Laukaisimen poistaminen epäonnistui: {{errorMessage}}" + } + } + }, + "enrichments": { + "semanticSearch": { + "modelSize": { + "small": { + "title": "pieni", + "desc": "pieni käyttää kvantisoitua versiota mallista, joka käyttää vähemmän RAM-muistia ja toimii nopeammin CPU:lla, mutta ero upotuksen laadussa on hyvin vähäinen." + }, + "large": { + "title": "suuri", + "desc": "suuri käyttää koko Jina-mallia ja toimii automaattisesti GPU:lla, jos se on mahdollista." + }, + "desc": "Semanttisen haun upotuksiin käytetyn mallin koko." + }, + "title": "Semanttinen haku", + "desc": "Semanttisen haun avulla Frigatessa voit etsiä seurattavia kohteita tarkistettavista kohteista joko kuvan, käyttäjän määrittämän tekstikuvauksen tai automaattisesti luodun kuvauksen avulla.", + "reindexNow": { + "label": "Uudelleenindeksoi nyt", + "desc": "Uudelleindeksointi luo uudelleen upotukset kaikille seuratuille objekteille. Tämä prosessi suoritetaan taustalla ja voi kuormittaa prosessorin maksimiin ja viedä melko paljon aikaa riippuen seurattujen objektien määrästä.", + "confirmTitle": "Vahvista uudelleenindeksointi" + } + }, + "faceRecognition": { + "title": "Kasvojentunnistus", + "desc": "Kasvojentunnistuksen avulla ihmisille voidaan antaa nimiä, ja kun heidän kasvonsa tunnistetaan, Frigate lisää henkilön nimen alaluokaksi. Nämä tiedot näkyvät käyttöliittymässä, suodattimissa ja ilmoituksissa.", + "modelSize": { + "label": "Mallin koko", + "desc": "Kasvojentunnistuksessa käytettävän mallin koko.", + "small": { + "title": "pieni", + "desc": "pieni käyttää FaceNet-kasvojen upotusmallia, joka toimii tehokkaasti useimmilla suorittimilla." + }, + "large": { + "title": "suuri", + "desc": "suuri käyttää ArcFace-kasvojen upotusmallia ja toimii automaattisesti GPU:lla, jos se on mahdollista." + } + } + }, + "licensePlateRecognition": { + "title": "Rekisterikilven tunnistaminen" + } + } +} diff --git a/web/public/locales/fi/views/system.json b/web/public/locales/fi/views/system.json new file mode 100644 index 0000000..2bb0828 --- /dev/null +++ b/web/public/locales/fi/views/system.json @@ -0,0 +1,82 @@ +{ + "logs": { + "type": { + "timestamp": "Aikaleima", + "tag": "Tagi", + "message": "Viesti", + "label": "Tyyppi" + }, + "copy": { + "label": "Kopioi leikepöydälle", + "success": "Lokit kopioitu leikepöydälle", + "error": "Lokeja ei voitu kopioida leikepöydälle" + }, + "download": { + "label": "Lataa lokit" + }, + "tips": "Lokeja toistetaan palvelimelta", + "toast": { + "error": { + "fetchingLogsFailed": "Virhe noudettaessa lokeja: {{errorMessage}}", + "whileStreamingLogs": "Virhe toistettaessa lokeja: {{errorMessage}}" + } + }, + "websocket": { + "label": "Viestit", + "pause": "Pysäytä" + } + }, + "documentTitle": { + "cameras": "Kameroiden tilastot - Frigate", + "storage": "Tallenteiden tilastot - Fgirage", + "general": "Yleiset tilastot - Frigate", + "enrichments": "Rikastetut tilastot - Frigate", + "logs": { + "frigate": "Frigaten lokit - Frigate", + "go2rtc": "Go2RTC lokit - Frigate", + "nginx": "Nginx lokit - Frigate", + "websocket": "Viestilokit - Frigate" + } + }, + "title": "Järjestelmä", + "metrics": "Järjestelmämittarit", + "general": { + "hardwareInfo": { + "title": "Laitteiston tiedot", + "gpuUsage": "GPU:n käyttö", + "gpuMemory": "GPU:n muisti", + "gpuEncoder": "GPU-enkooderi", + "gpuDecoder": "GPU-dekooderi", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfon tulostus", + "returnCode": "Paluuarvo: {{code}}" + }, + "toast": { + "success": "Kopioi GPU:n tiedot leikepöydälle" + }, + "copyInfo": { + "label": "Kopioi GPU:n tiedot" + }, + "closeInfo": { + "label": "Sulje GPU:n tiedot" + }, + "nvidiaSMIOutput": { + "driver": "Ajuri: {{driver}}", + "title": "Nvidia SMI tuloste", + "name": "Nimi: {{name}}", + "cudaComputerCapability": "CUDA laskentakapasiteetti: {{cuda_compute}}", + "vbios": "VBios-tiedot: {{vbios}}" + } + } + }, + "detector": { + "memoryUsage": "Ilmaiseman muistinkäyttö", + "title": "Ilmaisimet", + "inferenceSpeed": "Ilmaisimen päättelynopeus", + "cpuUsage": "Ilmaisimen CPU-käyttö", + "temperature": "Ilmaisimen lämpötila" + }, + "title": "Yleinen" + } +} diff --git a/web/public/locales/fr/audio.json b/web/public/locales/fr/audio.json new file mode 100644 index 0000000..b346158 --- /dev/null +++ b/web/public/locales/fr/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Parole", + "babbling": "Babillage", + "yell": "Cri", + "bicycle": "Vélo", + "car": "Voiture", + "bellow": "Beuglement", + "whispering": "Chuchotement", + "laughter": "Rires", + "snicker": "Ricanement", + "crying": "Pleurs", + "boat": "Bateau", + "bus": "Bus", + "train": "Train", + "motorcycle": "Moto", + "whoop": "Cri strident", + "sigh": "Soupir", + "singing": "Chant", + "choir": "Chorale", + "yodeling": "Yodel", + "chant": "Chant", + "mantra": "Mantra", + "child_singing": "Chant d'enfant", + "bird": "Oiseau", + "cat": "Chat", + "synthetic_singing": "Chant synthétique", + "rapping": "Rap", + "horse": "Cheval", + "dog": "Chien", + "sheep": "Mouton", + "whistling": "Sifflement", + "breathing": "Respiration", + "snoring": "Ronflement", + "gasp": "Souffle coupé", + "pant": "halètement", + "snort": "Ébrouement", + "camera": "Caméra", + "cough": "Toux", + "groan": "Gémissement", + "grunt": "Grognement", + "throat_clearing": "Raclement de gorge", + "wheeze": "Respiration sifflante", + "sneeze": "Éternuement", + "sniff": "Reniflement", + "chewing": "Mastication", + "gargling": "Gargarisme", + "ambulance": "Ambulance", + "police_car": "Voiture de police", + "emergency_vehicle": "Véhicule d'urgence", + "subway": "Métro", + "fire_alarm": "Alarme Incendie", + "smoke_detector": "Détecteur de Fumée", + "siren": "Sirène", + "pulleys": "Poulies", + "gears": "Engrenages", + "clock": "Horloge", + "ratchet": "Cliquet", + "mechanisms": "Mécanismes", + "steam_whistle": "Sifflet à vapeur", + "whistle": "Sifflet", + "foghorn": "Corne de brume", + "tools": "Outils", + "printer": "Imprimante", + "air_conditioning": "Climatisation", + "mechanical_fan": "Ventilateur mécanique", + "sewing_machine": "Machine à coudre", + "wood": "Bois", + "fireworks": "Feux d'artifice", + "glass": "Verre", + "television": "Télévision", + "sound_effect": "Effet sonore", + "burping": "Rots", + "fart": "Pet", + "crowd": "Foule", + "children_playing": "Jeux d'enfants", + "animal": "Animal", + "bark": "Aboiement", + "pig": "Cochon", + "goat": "Chèvre", + "chicken": "Poulet", + "turkey": "Dinde", + "duck": "Canard", + "goose": "Oie", + "wild_animals": "Animaux Sauvages", + "crow": "Corbeau", + "dogs": "Chiens", + "mouse": "Souris", + "insect": "Insecte", + "cricket": "Grillon", + "mosquito": "Moustique", + "fly": "Mouche", + "frog": "Grenouille", + "snake": "Serpent", + "music": "Musique", + "guitar": "Guitare", + "electric_guitar": "Guitare électrique", + "keyboard": "Clavier", + "piano": "Piano", + "vehicle": "Véhicule", + "skateboard": "Skateboard", + "door": "Porte", + "blender": "Mixeur", + "hair_dryer": "Sèche-cheveux", + "toothbrush": "Brosse à dents", + "sink": "Évier", + "scissors": "Ciseaux", + "humming": "Bourdonnement", + "shuffle": "Pas traînants", + "footsteps": "Bruits de pas", + "hiccup": "Hoquet", + "finger_snapping": "Claquement de doigts", + "clapping": "Claquements", + "applause": "Applaudissements", + "heartbeat": "Battements de coeur", + "cheering": "Acclamations", + "electric_shaver": "Rasoir électrique", + "truck": "Camion", + "run": "Course", + "biting": "Mordre", + "stomach_rumble": "Gargouillements d'estomac", + "hands": "Mains", + "heart_murmur": "Souffle au cœur", + "chatter": "Bavardage", + "pets": "Animaux de compagnie", + "yip": "Jappement", + "howl": "Hurlement", + "growling": "Grondement", + "whimper_dog": "Gémissements de chien", + "purr": "Ronronnements", + "caterwaul": "Miaulement", + "meow": "Miaou", + "livestock": "Bétail", + "neigh": "Hennissement", + "quack": "Coin-coin", + "honk": "Cacardement", + "roaring_cats": "Rugissement de félins", + "roar": "Rugissements", + "chirp": "Gazouillis", + "squawk": "Braillement", + "pigeon": "Pigeon", + "coo": "Roucoulement", + "caw": "Croassement", + "owl": "Chouette", + "hoot": "Hululement", + "flapping_wings": "Battement d'ailes", + "rats": "Rats", + "patter": "Crépitements", + "buzz": "Bourdonnement", + "croak": "Coassement", + "rattle": "Cliquetis", + "whale_vocalization": "Chant des baleines", + "musical_instrument": "Instrument de musique", + "plucked_string_instrument": "Instrument à cordes pincées", + "bass_guitar": "Guitare basse", + "acoustic_guitar": "Guitare acoustique", + "tapping": "Tapotement", + "strum": "Grattement", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandoline", + "steel_guitar": "Steel Guitar", + "zither": "Cithare", + "ukulele": "Ukulélé", + "electric_piano": "Piano électrique", + "organ": "Orgue", + "electronic_organ": "Orgue électrique", + "hammond_organ": "Orgue Hammond", + "synthesizer": "Synthétiseur", + "sampler": "Échantillonneur", + "harpsichord": "Clavecin", + "percussion": "Percussions", + "drum_kit": "Batterie", + "drum_machine": "Boîte à rythmes", + "drum": "Tambour", + "snare_drum": "Caisse claire", + "rimshot": "Rimshot", + "drum_roll": "Roulement de tambour", + "bass_drum": "Grosse caisse", + "timpani": "Timbales", + "tabla": "Tabla", + "cymbal": "Cymbale", + "hi_hat": "Charleston", + "wood_block": "Wood Block", + "maraca": "Maraca", + "gong": "Gong", + "tubular_bells": "Carillon tubulaire", + "marimba": "Marimba", + "mallet_percussion": "Maillet de percussion", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibraphone", + "steelpan": "Pan", + "orchestra": "Orchestre", + "brass_instrument": "Cuivres", + "french_horn": "Cor d'harmonie", + "trumpet": "Trompette", + "bowed_string_instrument": "Instrument à cordes frottées", + "string_section": "Section des cordes", + "violin": "Violon", + "pizzicato": "Pizzicato", + "cello": "Violoncelle", + "double_bass": "Contrebasse", + "wind_instrument": "Instrument à vent", + "flute": "Flûte", + "saxophone": "Saxophone", + "clarinet": "Clarinette", + "harp": "Harpe", + "church_bell": "Cloche d'église", + "bell": "Cloche", + "jingle_bell": "Grelot", + "bicycle_bell": "Sonnette de vélo", + "tuning_fork": "Diapason", + "chime": "Carillon", + "wind_chime": "Carillon à vent", + "harmonica": "Harmonica", + "accordion": "Accordéon", + "bagpipes": "Cornemuse", + "didgeridoo": "Didgeridoo", + "theremin": "Thérémine", + "singing_bowl": "Bol chantant", + "scratching": "Scratch", + "pop_music": "Musique pop", + "hip_hop_music": "Musique hip-hop", + "beatboxing": "Beatboxing", + "rock_music": "Musique rock", + "punk_rock": "Punk Rock", + "soul_music": "Musique Soul", + "reggae": "Reggae", + "country": "Country", + "funk": "Funk", + "folk_music": "Musique Folk", + "jazz": "Jazz", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "traditional_music": "Musique traditionnelle", + "independent_music": "Musique indépendante", + "song": "Chanson", + "background_music": "Musique de fond", + "theme_music": "Thème musical", + "jingle": "Tintement", + "soundtrack_music": "Musique de bande originale", + "lullaby": "Berceuse", + "video_game_music": "Musique de jeux vidéo", + "dance_music": "Musique Dance", + "wedding_music": "Musique de mariage", + "happy_music": "Musique joyeuse", + "sad_music": "Musique triste", + "tender_music": "Musique tendre", + "exciting_music": "Musique stimulante", + "angry_music": "Musique agressive", + "scary_music": "Musique effrayante", + "wind": "Vent", + "rustling_leaves": "Bruissements de feuilles", + "wind_noise": "Bruit de vent", + "thunderstorm": "Orage", + "thunder": "Tonnerre", + "water": "Eau", + "rain": "Pluie", + "raindrop": "Goutte de pluie", + "rain_on_surface": "Pluie sur une surface", + "stream": "Flux", + "waterfall": "Cascade", + "ocean": "Océan", + "waves": "Vagues", + "steam": "Vapeur", + "gurgling": "Gargouillis", + "fire": "Feu", + "crackle": "Crépitement", + "sailboat": "Voilier", + "rowboat": "Chaloupe", + "motorboat": "Bateau à moteur", + "ship": "Bateau", + "motor_vehicle": "Véhicule à moteur", + "toot": "Sifflotement", + "car_alarm": "Alarme de voiture", + "power_windows": "Vitres électriques", + "skidding": "Dérapage", + "tire_squeal": "Crissements de pneu", + "car_passing_by": "Passage de voiture", + "race_car": "Voiture de course", + "air_brake": "Frein pneumatique", + "air_horn": "Klaxon à air", + "reversing_beeps": "Bips de marche arrière", + "ice_cream_truck": "Camion de glaces", + "fire_engine": "Camion de pompiers", + "traffic_noise": "Bruit de circulation", + "rail_transport": "Transport ferroviaire", + "train_whistle": "Sifflet de train", + "train_horn": "Klaxon de train", + "railroad_car": "Wagon de chemin de fer", + "train_wheels_squealing": "Crissements de roues de train", + "aircraft": "Aéronef", + "aircraft_engine": "Moteur d'avion", + "jet_engine": "Moteur à réaction", + "propeller": "Hélice", + "helicopter": "Hélicoptère", + "fixed-wing_aircraft": "Avion à voilure fixe", + "engine": "Moteur", + "light_engine": "Moteur léger", + "dental_drill's_drill": "Fraise dentaire", + "lawn_mower": "Tondeuse à gazon", + "chainsaw": "Tronçonneuse", + "heavy_engine": "Moteur lourd", + "engine_knocking": "Détonations de moteur", + "engine_starting": "Démarrage de moteur", + "accelerating": "Accélération", + "doorbell": "Sonnette", + "ding-dong": "Ding-Dong", + "knock": "Coup", + "tap": "Tapotement", + "squeak": "Grincement", + "cupboard_open_or_close": "Ouverture ou fermeture de placard", + "drawer_open_or_close": "Ouverture ou fermeture de tiroir", + "dishes": "Bruit de vaisselle", + "cutlery": "Couverts", + "chopping": "Hacher", + "frying": "Friture", + "microwave_oven": "Four à micro-ondes", + "water_tap": "Robinet d'eau", + "bathtub": "Baignoire", + "toilet_flush": "Chasse d'eau", + "electric_toothbrush": "Brosse à dents électrique", + "vacuum_cleaner": "Aspirateur", + "zipper": "Fermeture éclair", + "keys_jangling": "Tintements de clés", + "coin": "Pièce de monnaie", + "shuffling_cards": "Battement de cartes", + "typing": "Frappe au clavier", + "typewriter": "Machine à écrire", + "writing": "Écriture", + "alarm": "Alarme", + "telephone_bell_ringing": "Sonnerie de téléphone", + "ringtone": "Sonnerie", + "telephone_dialing": "Numérotation téléphonique", + "dial_tone": "Tonalité", + "busy_signal": "Tonalité occupée", + "alarm_clock": "Réveille-matin", + "civil_defense_siren": "Sirène d'alerte aux populations", + "buzzer": "Buzzer", + "tick": "Tic-tac", + "tick-tock": "Tic-Tac", + "cash_register": "Caisse enregistreuse", + "single-lens_reflex_camera": "Appareil photo reflex mono-objectif", + "hammer": "Marteau", + "jackhammer": "Marteau-piqueur", + "sawing": "Sciage", + "filing": "Limage", + "sanding": "Ponçage", + "power_tool": "Outil électrique", + "drill": "Perceuse", + "explosion": "Explosion", + "gunshot": "Coup de feu", + "machine_gun": "Mitrailleuse", + "fusillade": "Fusillade", + "artillery_fire": "Tir d'artillerie", + "cap_gun": "Pistolet à amorces", + "firecracker": "Pétard", + "eruption": "Éruption", + "boom": "Boom", + "chop": "Coup de hache", + "splinter": "Éclat", + "crack": "Fissure", + "chink": "Fente", + "shatter": "Brisure", + "silence": "Silence", + "environmental_noise": "Bruit ambiant", + "static": "Statique", + "white_noise": "Bruit blanc", + "pink_noise": "Bruit rose", + "field_recording": "Enregistrement sur le terrain", + "scream": "Cri", + "tambourine": "Tambourin", + "electronic_music": "Musique électronique", + "rock_and_roll": "Rock and Roll", + "vocal_music": "Musique vocale", + "trombone": "Trombone", + "flamenco": "Flamenco", + "carnatic_music": "Musique carnatique", + "a_capella": "A Capella", + "christmas_music": "Musique de Noël", + "afrobeat": "Afrobeat", + "sliding_door": "Porte coulissante", + "opera": "Opéra", + "music_of_africa": "Musique d'Afrique", + "music_of_latin_america": "Musique d'Amérique Latine", + "blues": "Blues", + "music_for_children": "Musique pour enfants", + "electronica": "Electronica", + "ska": "Ska", + "salsa_music": "Salsa", + "medium_engine": "Moteur moyen", + "heavy_metal": "Heavy Metal", + "disco": "Disco", + "grunge": "Grunge", + "music_of_asia": "Musique d'Asie", + "progressive_rock": "Rock progressif", + "psychedelic_rock": "Rock psychédélique", + "rhythm_and_blues": "Rhythm and Blues", + "electronic_dance_music": "Electronic Dance Music", + "trance_music": "Musique Trance", + "new-age_music": "Musique New Age", + "bluegrass": "Bluegrass", + "swing_music": "Musique Swing", + "ambient_music": "Musique d'ambiance", + "middle_eastern_music": "Musique orientale", + "house_music": "Musique House", + "christian_music": "Musique chrétienne", + "classical_music": "Musique classique", + "gospel_music": "Musique Gospel", + "slam": "Claquement", + "computer_keyboard": "Clavier d'ordinateur", + "burst": "Éclatement", + "music_of_bollywood": "Musique de Bollywood", + "idling": "Ralenti", + "radio": "Radio", + "telephone": "Téléphone", + "bow_wow": "Aboiement", + "hiss": "Sifflement", + "clip_clop": "Clic-clac", + "cattle": "Bétail", + "moo": "Meuglement", + "cowbell": "Clochette", + "oink": "Grouin-grouin", + "bleat": "Bêlement", + "fowl": "Volaille", + "cluck": "Gloussement", + "cock_a_doodle_doo": "Cocorico", + "gobble": "Glouglou", + "chird": "Accord", + "change_ringing": "Carillon de cloches", + "sodeling": "Sodèle", + "shofar": "Choffar", + "liquid": "Liquide", + "splash": "Éclaboussure", + "slosh": "Clapotis", + "squish": "Bruit de pataugeage", + "drip": "Goutte à goutte", + "trickle": "Filet", + "gush": "Jet", + "fill": "Remplir", + "spray": "Pulvérisation", + "pump": "Pompe", + "stir": "Remuer", + "boiling": "Ébullition", + "arrow": "Flèche", + "pour": "Verser", + "sonar": "Sonar", + "whoosh": "Whoosh", + "thump": "Coup sourd", + "thunk": "Bruit sourd", + "electronic_tuner": "Accordeur électronique", + "effects_unit": "Unité d'effets", + "chorus_effect": "Effet de chœur", + "basketball_bounce": "Rebond de basket-ball", + "bang": "Détonation", + "slap": "Gifle", + "whack": "Coup sec", + "smash": "Fracasser", + "breaking": "Bruit de casse", + "bouncing": "Rebondissement", + "whip": "Fouet", + "flap": "Battement", + "scratch": "Grattement", + "scrape": "Raclement", + "rub": "Frottement", + "roll": "Roulement", + "crushing": "Écrasement", + "crumpling": "Froissement", + "tearing": "Déchirure", + "beep": "Bip", + "ping": "Ping", + "ding": "Ding", + "clang": "Bruit métallique", + "squeal": "Grincement", + "creak": "Craquer", + "rustle": "Bruissement", + "whir": "Vrombissement", + "clatter": "Bruit", + "sizzle": "Grésillement", + "clicking": "Cliquetis", + "clickety_clack": "Clic-clac", + "rumble": "Grondement", + "plop": "Ploc", + "hum": "Hum", + "harmonic": "Harmonique", + "outside": "Extérieur", + "reverberation": "Réverbération", + "echo": "Écho", + "distortion": "Distorsion", + "vibration": "Vibration", + "zing": "Sifflement", + "crunch": "Croque", + "sine_wave": "Onde sinusoïdale", + "chirp_tone": "Gazouillis", + "pulse": "Impulsion", + "inside": "Intérieur", + "noise": "Bruit", + "mains_hum": "Bourdonnement du secteur", + "sidetone": "Retour de voix", + "cacophony": "Cacophonie", + "throbbing": "Pulsation", + "boing": "Boing" +} diff --git a/web/public/locales/fr/common.json b/web/public/locales/fr/common.json new file mode 100644 index 0000000..ebec9c4 --- /dev/null +++ b/web/public/locales/fr/common.json @@ -0,0 +1,341 @@ +{ + "time": { + "untilForRestart": "Jusqu'à ce que Frigate redémarre.", + "untilRestart": "Jusqu'au redémarrage", + "untilForTime": "Jusqu'à {{time}}", + "justNow": "À l'instant", + "today": "Aujourd'hui", + "last7": "7 derniers jours", + "last14": "14 derniers jours", + "ago": "Il y a {{timeAgo}}", + "yesterday": "Hier", + "last30": "30 derniers jours", + "thisWeek": "Cette semaine", + "lastWeek": "La semaine dernière", + "thisMonth": "Ce mois-ci", + "lastMonth": "Le mois dernier", + "10minutes": "10 minutes", + "5minutes": "5 minutes", + "30minutes": "30 minutes", + "12hours": "12 heures", + "h": "{{time}} h", + "pm": "PM", + "am": "AM", + "yr": "{{time}} a", + "year_one": "{{time}} an", + "year_many": "{{time}} ans", + "year_other": "{{time}} ans", + "mo": "{{time}} mois", + "month_one": "{{time}} mois", + "month_many": "{{time}} mois", + "month_other": "{{time}} mois", + "s": "{{time}} s", + "second_one": "{{time}} seconde", + "second_many": "{{time}} secondes", + "second_other": "{{time}} secondes", + "m": "{{time}} min", + "hour_one": "{{time}} heure", + "hour_many": "{{time}} heures", + "hour_other": "{{time}} heures", + "24hours": "24 heures", + "minute_one": "{{time}} minute", + "minute_many": "{{time}} minutes", + "minute_other": "{{time}} minutes", + "d": "{{time}} j", + "day_one": "{{time}} jour", + "day_many": "{{time}} jours", + "day_other": "{{time}} jours", + "1hour": "1 heure", + "formattedTimestamp": { + "12hour": "d MMM HH:mm:ss", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampWithYear": { + "24hour": "%b %-d %Y, %H:%M", + "12hour": "%b %-d %Y, %I:%M %p" + }, + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "formattedTimestampExcludeSeconds": { + "12hour": "%b %-d, %I:%M %p", + "24hour": "%b %-d, %H:%M" + }, + "formattedTimestamp2": { + "12hour": "dd/MM HH:mm:ss", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "24hour": "HH:mm", + "12hour": "HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-HH-mm-ss", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, HH:mm", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, HH:mm", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM,yyyy" + }, + "inProgress": "En cours", + "invalidStartTime": "Heure de début invalide", + "invalidEndTime": "Heure de fin invalide", + "never": "Jamais" + }, + "button": { + "apply": "Appliquer", + "reset": "Réinitialiser", + "disabled": "Désactivé", + "save": "Enregistrer", + "saving": "Enregistrement…", + "close": "Fermer", + "copy": "Copier", + "back": "Retour", + "history": "Historique", + "pictureInPicture": "Image dans l'image", + "twoWayTalk": "Conversation bidirectionnelle", + "off": "OFF", + "edit": "Modifier", + "copyCoordinates": "Copier les coordonnées", + "delete": "Supprimer", + "yes": "Oui", + "no": "Non", + "unsuspended": "Réactiver", + "play": "Lire", + "unselect": "Désélectionner", + "suspended": "Suspendu", + "enable": "Activer", + "enabled": "Activé", + "info": "Info", + "disable": "Désactiver", + "cancel": "Annuler", + "fullscreen": "Plein écran", + "next": "Suivant", + "exitFullscreen": "Sortir du mode plein écran", + "cameraAudio": "Son de la caméra", + "on": "ON", + "export": "Exporter", + "deleteNow": "Supprimer maintenant", + "download": "Télécharger", + "done": "Terminé", + "continue": "Continuer", + "add": "Ajouter", + "undo": "Annuler", + "copiedToClipboard": "Copié dans le presse-papiers", + "modified": "Modifié", + "overridden": "Surpassé", + "resetToGlobal": "Réinitialiser aux réglages globaux", + "resetToDefault": "Réinitialiser aux réglages par défaut", + "saveAll": "Tout enregistrer", + "savingAll": "Enregistrement de tout en cours…", + "undoAll": "Tout annuler", + "applying": "Enregistrement…", + "retry": "Réessayer" + }, + "menu": { + "configuration": "Configuration", + "language": { + "en": "English (Anglais)", + "withSystem": { + "label": "Utiliser les paramètres système pour la langue" + }, + "zhCN": "简体中文 (Chinois simplifié)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Français)", + "ja": "日本語 (Japonais)", + "tr": "Türkçe (Turc)", + "it": "Italiano (Italien)", + "nl": "Nederlands (Néerlandais)", + "sv": "Svenska (Suédois)", + "cs": "Čeština (Tchèque)", + "nb": "Norsk Bokmål (Norvégien Bokmål)", + "ko": "한국어 (Coréen)", + "fa": "فارسی (Persan)", + "pl": "Polski (Polonais)", + "el": "Ελληνικά (Grec)", + "ro": "Română (Roumain)", + "hu": "Magyar (Hongrois)", + "he": "עברית (Hébreu)", + "ru": "Русский (Russe)", + "de": "Deutsch (Allemand)", + "es": "Español (Espagnol)", + "ar": "العربية (Arabe)", + "da": "Dansk (Danois)", + "fi": "Suomi (Finlandais)", + "pt": "Português (Portugais)", + "sk": "Slovenčina (Slovaque)", + "uk": "Українська (Ukrainien)", + "vi": "Tiếng Việt (Vietnamien)", + "yue": "粵語 (Cantonais)", + "th": "ไทย (Thai)", + "ca": "Català (Catalan)", + "ptBR": "Português brasileiro (Portugais brésilien)", + "sr": "Српски (Serbe)", + "sl": "Slovenščina (Slovène)", + "lt": "Lietuvių (Lithuanien)", + "bg": "Български (Bulgare)", + "gl": "Galego (Galicien)", + "id": "Bahasa Indonesia (Indonésien)", + "ur": "اردو (Ourdou)", + "hr": "Hrvatski (Croate)", + "bs": "Bosanski (Bosnien)", + "zhHant": "繁體中文 (chinois traditionnel)" + }, + "appearance": "Apparence", + "darkMode": { + "light": "Clair", + "dark": "Sombre", + "withSystem": { + "label": "Utiliser les paramètres système pour le mode clair ou sombre" + }, + "label": "Mode sombre" + }, + "review": "Activités", + "explore": "Explorer", + "export": "Exporter", + "user": { + "account": "Compte", + "logout": "Se déconnecter", + "setPassword": "Configurer un mot de passe", + "current": "Utilisateur actuel : {{user}}", + "title": "Utilisateur", + "anonymous": "anonyme" + }, + "systemLogs": "Journaux système", + "documentation": { + "title": "Documentation", + "label": "Documentation de Frigate" + }, + "system": "Système", + "help": "Aide", + "configurationEditor": "Éditeur de configuration", + "theme": { + "contrast": "Contraste élevé", + "blue": "Bleu", + "green": "Vert", + "nord": "Nord", + "red": "Rouge", + "default": "Par défaut", + "label": "Thème", + "highcontrast": "Contraste élevé" + }, + "systemMetrics": "Métriques du système", + "settings": "Paramètres", + "withSystem": "Système", + "restart": "Redémarrer Frigate", + "live": { + "cameras": { + "count_one": "{{count}} caméra", + "count_many": "{{count}} caméras", + "count_other": "{{count}} caméras", + "title": "Caméras" + }, + "allCameras": "Toutes les caméras", + "title": "Direct" + }, + "uiPlayground": "Bac à sable de l'interface", + "faceLibrary": "Bibliothèque de visages", + "languages": "Langues", + "classification": "Classification", + "profiles": "Profils", + "actions": "Actions", + "features": "Fonctionnalités", + "chat": "Discuter" + }, + "toast": { + "save": { + "title": "Enregistrer", + "error": { + "noMessage": "Echec lors de l'enregistrement des changements de configuration", + "title": "Échec de l'enregistrement des changements de configuration : {{errorMessage}}" + }, + "success": "Modifications enregistrées avec succès." + }, + "copyUrlToClipboard": "URL copiée dans le presse-papiers." + }, + "role": { + "title": "Rôle", + "viewer": "Observateur", + "admin": "Administrateur", + "desc": "Les administrateurs ont un accès complet à toutes les fonctionnalités de l'interface Frigate. Les observateurs sont limités à la consultation des caméras, des activités, et à l'historique des enregistrements dans l'interface." + }, + "pagination": { + "next": { + "title": "Suivant", + "label": "Aller à la page suivante" + }, + "more": "Plus de pages", + "previous": { + "label": "Aller à la page précédente", + "title": "Précédent" + }, + "label": "pagination" + }, + "notFound": { + "title": "404", + "documentTitle": "Non trouvé - Frigate", + "desc": "Page non trouvée" + }, + "selectItem": "Sélectionner {{item}}", + "readTheDocumentation": "Lire la documentation", + "accessDenied": { + "title": "Accès refusé", + "documentTitle": "Accès refusé - Frigate", + "desc": "Vous n'avez pas l'autorisation de consulter cette page." + }, + "label": { + "back": "Retour", + "hide": "Masquer {{item}}", + "show": "Afficher {{item}}", + "ID": "ID", + "none": "Aucun", + "all": "Tous", + "other": "Autre" + }, + "unit": { + "speed": { + "kph": "km/h", + "mph": "mph" + }, + "length": { + "feet": "pieds", + "meters": "mètres" + }, + "data": { + "kbps": "ko/s", + "mbps": "Mo/s", + "gbps": "Go/s", + "kbph": "ko/heure", + "mbph": "Mo/heure", + "gbph": "Go/heure" + } + }, + "information": { + "pixels": "{{area}}px" + }, + "field": { + "optional": "Facultatif", + "internalID": "L'ID interne utilisée par Frigate dans la configuration et la base de donnêes" + }, + "list": { + "two": "{{0}} et {{1}}", + "many": "{{items}}, et {{last}}", + "separatorWithSpace": ", " + }, + "no_items": "Aucun élément", + "validation_errors": "Erreurs de validation", + "credentialField": { + "savedPlaceholder": "Enregistré — laissez vide pour conserver la version actuelle" + } +} diff --git a/web/public/locales/fr/components/auth.json b/web/public/locales/fr/components/auth.json new file mode 100644 index 0000000..3e600fb --- /dev/null +++ b/web/public/locales/fr/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Mot de passe", + "login": "Connexion", + "user": "Nom d'utilisateur", + "errors": { + "unknownError": "Erreur inconnue. Vérifiez les journaux.", + "webUnknownError": "Erreur inconnue. Vérifiez les journaux de la console.", + "passwordRequired": "Mot de passe est requis", + "loginFailed": "Échec de l'authentification", + "usernameRequired": "Nom d'utilisateur requis", + "rateLimit": "Trop de tentatives. Veuillez réessayer plus tard." + }, + "firstTimeLogin": "Première connexion ? Vos identifiants se trouvent dans les journaux de Frigate." + } +} diff --git a/web/public/locales/fr/components/camera.json b/web/public/locales/fr/components/camera.json new file mode 100644 index 0000000..8387b90 --- /dev/null +++ b/web/public/locales/fr/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "edit": "Modifier le groupe de caméras", + "label": "Groupes de caméras", + "add": "Ajouter un groupe de caméras", + "delete": { + "label": "Supprimer le groupe de caméras", + "confirm": { + "title": "Confirmez la suppression", + "desc": "Êtes-vous sûr de vouloir supprimer le groupe de caméras {{name}} ?" + } + }, + "name": { + "placeholder": "Saisissez un nom.", + "label": "Nom", + "errorMessage": { + "mustLeastCharacters": "Le nom du groupe de caméras doit comporter au moins 2 caractères.", + "exists": "Le nom du groupe de caméras existe déjà.", + "nameMustNotPeriod": "Le nom de groupe de caméras ne doit pas contenir de point.", + "invalid": "Nom de groupe de caméras invalide" + } + }, + "cameras": { + "label": "Caméras", + "desc": "Sélectionnez les caméras pour ce groupe." + }, + "success": "Le groupe de caméras ({{name}}) a été enregistré.", + "icon": "Icône", + "camera": { + "setting": { + "label": "Paramètres du flux de la caméra", + "title": "Paramètres du flux de {{cameraName}}", + "audioIsUnavailable": "L'audio n'est pas disponible pour ce flux.", + "audioIsAvailable": "L'audio est disponible pour ce flux.", + "desc": "Modifier les options du flux temps réel pour le tableau de bord de ce groupe de caméras. Ces paramètres sont spécifiques à l'appareil ou au navigateur.", + "audio": { + "tips": { + "document": "Lire la documentation ", + "title": "L'audio doit provenir de la caméra et être configuré dans go2rtc pour ce flux." + } + }, + "streamMethod": { + "label": "Méthode de diffusion", + "method": { + "noStreaming": { + "label": "Aucune diffusion", + "desc": "Les images provenant de la caméra ne seront mises à jour qu'une fois par minute et il n'y aura aucune diffusion en direct." + }, + "smartStreaming": { + "label": "Diffusion intelligente (recommandée)", + "desc": "La diffusion intelligente mettra à jour l'image de la caméra une fois par minute lorsqu'aucune activité n'est détectée, afin de préserver la bande passante et les ressources. Quand une activité est détectée, l'image bascule automatiquement en flux temps réel." + }, + "continuousStreaming": { + "label": "Diffusion en continu", + "desc": { + "title": "L'image de la caméra sera toujours un flux temps réel lorsqu'elle est visible dans le tableau de bord, même si aucune activité n'est détectée.", + "warning": "La diffusion en continu peut entraîner une consommation de bande passante élevée et des problèmes de performance. À utiliser avec prudence." + } + } + }, + "placeholder": "Choisissez une méthode de diffusion." + }, + "compatibilityMode": { + "label": "Mode de compatibilité", + "desc": "Activez cette option uniquement si votre flux temps réel affiche des artefacts chromatiques et présente une ligne diagonale sur le côté droit de l'image." + }, + "stream": "Flux", + "placeholder": "Choisissez un flux." + }, + "birdseye": "Birdseye" + }, + "showAll": "Afficher tout les groupes de caméras", + "showLess": "Réduire l'affichage", + "editGroups": "Modifier les groupes de caméras" + }, + "debug": { + "timestamp": "Horodatage", + "motion": "Mouvement", + "mask": "Masque", + "options": { + "showOptions": "Afficher les options", + "title": "Options", + "label": "Paramètres", + "hideOptions": "Masquer les options" + }, + "boundingBox": "Cadre de détection", + "zones": "Zones", + "regions": "Régions", + "paths": "Chemins" + } +} diff --git a/web/public/locales/fr/components/dialog.json b/web/public/locales/fr/components/dialog.json new file mode 100644 index 0000000..4e0fa88 --- /dev/null +++ b/web/public/locales/fr/components/dialog.json @@ -0,0 +1,208 @@ +{ + "restart": { + "title": "Êtes-vous sûr de vouloir redémarrer Frigate ?", + "restarting": { + "title": "Redémarrage de Frigate en cours", + "content": "Cette page sera rechargée dans {{countdown}} secondes.", + "button": "Forcer l'actualisation maintenant" + }, + "button": "Redémarrer", + "description": "Frigate s'arrêtera momentanément pour redémarrer." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Soumettre à Frigate+", + "desc": "Les objets situés dans des zones à ignorer ne doivent pas être signalés comme de faux positifs, car cela nuirait à la précision du modèle." + }, + "review": { + "true": { + "label": "Confirmez cette étiquette pour Frigate Plus", + "true_one": "C'est un {{label}}", + "true_many": "Ce sont des {{label}}", + "true_other": "Ce sont des {{label}}" + }, + "false": { + "false_one": "Ceci n'est pas un {{label}}", + "false_many": "Ceux-ci ne sont pas des {{label}}", + "false_other": "Ceux-ci ne sont pas des {{label}}", + "label": "Ne pas confirmer cette étiquette pour Frigate Plus" + }, + "state": { + "submitted": "Soumis" + }, + "question": { + "label": "Confirmez cette étiquette pour Frigate+.", + "ask_an": "Cet objet est-il un(e) {{label}} ?", + "ask_a": "Cet objet est-il un(e) {{label}} ?", + "ask_full": "Cet objet est-il un(e) {{translatedLabel}}  ?" + }, + "toast": { + "error": "Impossible d'envoyer à Frigate+. Merci de vérifier votre réseau puis de réessayer." + } + } + }, + "video": { + "viewInHistory": "Afficher dans la chronologie" + } + }, + "export": { + "time": { + "custom": "Personnalisé", + "fromTimeline": "Sélectionner depuis la chronologie", + "lastHour_one": "Dernière heure", + "lastHour_many": "{{count}} dernières heures", + "lastHour_other": "{{count}} dernières heures", + "end": { + "label": "Sélectionner une heure de fin", + "title": "Heure de fin" + }, + "start": { + "label": "Sélectionner une heure de début", + "title": "Heure de début" + } + }, + "selectOrExport": "Sélectionner ou exporter", + "toast": { + "error": { + "failed": "Échec du démarrage de l'exportation : {{error}}", + "endTimeMustAfterStartTime": "L'heure de fin doit être postérieure à l'heure de début.", + "noVaildTimeSelected": "La plage horaire sélectionnée n'est pas valide." + }, + "success": "Exportation démarrée avec succès. Consultez le fichier sur la page des exportations.", + "view": "Vue", + "queued": "Exportation en attente. Consultez la progression sur la page des exportations.", + "batchSuccess_one": "Un export démarré. Ouverture du dossier en cours.", + "batchSuccess_many": "{{count}} exports démarrés. Ouverture du dossier en cours.", + "batchSuccess_other": "{{count}} exports démarrés. Ouverture du dossier en cours.", + "batchPartial": "{{successful}} exports ont démarré sur un total de : {{total}}. Échecs : {{failedCameras}}", + "batchFailed": "Échec du démarrage de {{total}} exports. Échecs : {{failedCameras}}", + "batchQueuedSuccess_one": "Un export rajouté au traitement. Ouverture du dossier en cours.", + "batchQueuedSuccess_many": "{{count}} exports rajoutés au traitement. Ouverture du dossier en cours.", + "batchQueuedSuccess_other": "{{count}} exports rajoutés au traitement. Ouverture du dossier en cours." + }, + "select": "Sélectionner", + "name": { + "placeholder": "Nommer l'exportation" + }, + "export": "Exporter", + "fromTimeline": { + "saveExport": "Enregistrer l'exportation", + "previewExport": "Aperçu de l'exportation" + }, + "case": { + "label": "Dossier", + "placeholder": "Sélectionner un dossier", + "newCaseOption": "Créer un nouveau cas", + "newCaseNamePlaceholder": "Nouveau nom de cas", + "newCaseDescriptionPlaceholder": "Description de cas", + "nonAdminHelp": "Un nouveau cas sera créé pour ces exports." + }, + "queueing": "Mise en file d'attente de l'export...", + "tabs": { + "export": "Caméra unique", + "multiCamera": "Multi-caméra" + }, + "multiCamera": { + "timeRange": "Intervalle de temps", + "selectFromTimeline": "Sélectionner depuis la chronologie", + "cameraSelection": "Caméras", + "cameraSelectionHelp": "Les caméras avec des objets suivis dans cette intervalle sont pré-sélectionnées", + "checkingActivity": "Vérification de l'activité de la caméra...", + "noCameras": "Aucune caméra disponible", + "detectionCount_one": "{{count}} objet suivi", + "detectionCount_many": "{{count}} objets suivis", + "detectionCount_other": "{{count}} objets suivis", + "nameLabel": "Nom d'export", + "namePlaceholder": "Nom de base optionnel pour ces exports", + "queueingButton": "Mise en file d'attente des exports...", + "exportButton_one": "Exporter {{count}} caméra", + "exportButton_many": "Exporter {{count}} caméras", + "exportButton_other": "Exporter {{count}} caméras", + "searchOrSelectGroup": "Chercher ou sélectionner un groupe de caméras...", + "selectAll": "Sélectionner toutes les cameras", + "clearSelection": "Effacer la sélection", + "selectWithActivity": "Caméras avec des objets suivis", + "selectGroup": "Sélectionner un groupe", + "noMatchingCameras": "Aucune caméra ne correspond à votre recherche", + "selectedCount": "{{selected}} / {{total}} sélectionnées" + }, + "multi": { + "title_one": "Export {{count}} revue", + "title_many": "Export {{count}} revues", + "title_other": "Export {{count}} revues", + "description": "Exporter chaque revue sélectionnée. Tous les exports sont regroupés sous un cas unique.", + "descriptionNoCase": "Exporter chaque revue sélectionnée.", + "caseNamePlaceholder": "Vérification de l'export – {{date}}", + "exportButton_one": "Exporter {{count}} revue", + "exportButton_many": "Exporter {{count}} revues", + "exportButton_other": "Exporter {{count}} revues", + "exportingButton": "Exportation...", + "toast": { + "started_one": "Un export a démarré. Ouverture du dossier en cours.", + "started_many": "{{count}} exports ont démarré. Ouverture du dossier en cours.", + "started_other": "{{count}} exports ont démarré. Ouverture du dossier en cours.", + "startedNoCase_one": "Un export a démarré.", + "startedNoCase_many": "{{count}} exports ont démarré.", + "startedNoCase_other": "{{count}} exports ont démarré.", + "partial": "{{successful}} exportations sur {{total}} lancées. Échecs : {{failedItems}}", + "failed": "Échec du démarrage des exports {{total}}. Échec : {{failedItems}}" + } + } + }, + "search": { + "saveSearch": { + "desc": "Saisissez un nom pour cette recherche enregistrée.", + "label": "Enregistrer la recherche", + "success": "La recherche ({{searchName}}) a été enregistrée.", + "button": { + "save": { + "label": "Enregistrer cette recherche" + } + }, + "overwrite": "{{searchName}} existe déjà. L'enregistrement écrasera la recherche existante.", + "placeholder": "Saisissez un nom pour votre recherche." + } + }, + "streaming": { + "label": "Flux", + "restreaming": { + "disabled": "La rediffusion n'est pas activée pour cette caméra.", + "desc": { + "readTheDocumentation": "Lire la documentation", + "title": "Configurez go2rtc pour bénéficier d'options de visualisation en direct supplémentaires et de l'audio pour cette caméra." + } + }, + "showStats": { + "label": "Afficher les statistiques du flux", + "desc": "Activez cette option pour afficher les statistiques de diffusion en incrustation sur le flux vidéo de la caméra." + }, + "debugView": "Affichage de débogage" + }, + "recording": { + "confirmDelete": { + "desc": { + "selected": "Êtes-vous sûr(e) de vouloir supprimer toutes les vidéos enregistrées associées à cette activité ?

    Maintenez la touche Maj enfoncée pour éviter cette boîte de dialogue à l'avenir." + }, + "title": "Confirmer la suppression", + "toast": { + "success": "Les vidéos associées aux activités sélectionnées ont été supprimées.", + "error": "Échec de la suppression : {{error}}" + } + }, + "button": { + "export": "Exporter", + "markAsReviewed": "Marquer comme traité", + "deleteNow": "Supprimer maintenant", + "markAsUnreviewed": "Marquer comme non traité" + } + }, + "imagePicker": { + "selectImage": "Sélectionnez une vignette d'objet suivi.", + "search": { + "placeholder": "Rechercher par étiquette ou sous-étiquette" + }, + "noImages": "Aucune vignette trouvée pour cette caméra", + "unknownLabel": "Image de déclencheur enregistrée" + } +} diff --git a/web/public/locales/fr/components/filter.json b/web/public/locales/fr/components/filter.json new file mode 100644 index 0000000..0af924f --- /dev/null +++ b/web/public/locales/fr/components/filter.json @@ -0,0 +1,141 @@ +{ + "labels": { + "label": "Étiquettes", + "all": { + "title": "Toutes les étiquettes", + "short": "Étiquettes" + }, + "count": "{{count}} Étiquettes", + "count_one": "{{count}} étiquette", + "count_other": "{{count}} étiquettes" + }, + "filter": "Filtre", + "zones": { + "label": "Zones", + "all": { + "title": "Toutes les zones", + "short": "Zones" + } + }, + "dates": { + "all": { + "title": "Toutes les dates", + "short": "Dates" + }, + "selectPreset": "Sélectionnez un préréglage." + }, + "more": "Plus de filtres", + "reset": { + "label": "Réinitialiser les filtres aux valeurs par défaut" + }, + "timeRange": "Plage horaire", + "subLabels": { + "label": "Sous-étiquettes", + "all": "Toutes les sous-étiquettes" + }, + "score": "Score", + "estimatedSpeed": "Vitesse estimée ({{unit}})", + "sort": { + "label": "Tri", + "dateDesc": "Date (ordre chronologique inverse)", + "dateAsc": "Date (ordre chronologique)", + "scoreDesc": "Score d'objet (décroissant)", + "scoreAsc": "Score d'objet (croissant)", + "speedAsc": "Vitesse estimée (croissant)", + "speedDesc": "Vitesse estimée (décroissant)", + "relevance": "Pertinence" + }, + "features": { + "submittedToFrigatePlus": { + "tips": "Vous devez d'abord filtrer les objets suivis qui ont un instantané.

    Les objets suivis sans instantané ne peuvent pas être soumis à Frigate+.", + "label": "Soumis à Frigate+" + }, + "hasVideoClip": "Avec une séquence vidéo", + "hasSnapshot": "Avec un instantané", + "label": "Caractéristiques" + }, + "explore": { + "settings": { + "title": "Paramètres", + "defaultView": { + "title": "Vue par défaut", + "summary": "Résumé", + "unfilteredGrid": "Grille non filtrée", + "desc": "Lorsqu'aucun filtre n'est sélectionné, afficher un résumé des objets suivis les plus récents par étiquette, ou afficher une grille non filtrée" + }, + "gridColumns": { + "desc": "Sélectionner le nombre de colonnes dans la vue grille.", + "title": "Colonnes de la grille" + }, + "searchSource": { + "label": "Source de recherche", + "options": { + "thumbnailImage": "Miniature", + "description": "Description" + }, + "desc": "Choisissez si vous souhaitez rechercher les miniatures ou les descriptions de vos objets suivis." + } + }, + "date": { + "selectDateBy": { + "label": "Sélectionner une date pour filtrer" + } + } + }, + "review": { + "showReviewed": "Afficher les activités traitées" + }, + "cameras": { + "label": "Filtre des caméras", + "all": { + "short": "Caméras", + "title": "Toutes les caméras" + } + }, + "motion": { + "showMotionOnly": "Afficher uniquement le mouvement" + }, + "logSettings": { + "filterBySeverity": "Filtrer les journaux par gravité", + "loading": { + "title": "Chargement", + "desc": "Lorsque le volet de journalisation est défilé jusqu'en bas, les nouveaux enregistrements s'affichent automatiquement au fur et à mesure qu'ils sont ajoutés." + }, + "label": "Filtrer par niveau de journal", + "disableLogStreaming": "Désactiver le flux des journaux", + "allLogs": "Tous les journaux" + }, + "recognizedLicensePlates": { + "placeholder": "Tapez pour rechercher des plaques d'immatriculation.", + "noLicensePlatesFound": "Aucune plaque d'immatriculation trouvée", + "loading": "Chargement des plaques d'immatriculation reconnues…", + "title": "Plaques d'immatriculation reconnues", + "loadFailed": "Échec du chargement des plaques d'immatriculation reconnues.", + "selectPlatesFromList": "Sélectionnez une ou plusieurs plaques d'immatriculation dans la liste.", + "selectAll": "Tout sélectionner", + "clearAll": "Tout désélectionner" + }, + "trackedObjectDelete": { + "title": "Confirmer la suppression", + "toast": { + "success": "Objets suivis supprimés avec succès.", + "error": "Échec de la suppression des objets suivis : {{errorMessage}}" + }, + "desc": "La suppression de ces {{objectLength}} objets suivis retirera l'instantané, les embeddings enregistrés et les entrées du cycle de vie de l'objet associées. Les séquences enregistrées de ces objets suivis dans la vue Chronologie NE seront PAS supprimées.

    Voulez-vous vraiment continuer?

    Maintenez la touche Maj enfoncée pour ignorer cette boîte de dialogue à l'avenir." + }, + "zoneMask": { + "filterBy": "Filtrer par masque de zone" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Toutes les classes" + }, + "count_one": "{{count}} classe", + "count_other": "{{count}} classes" + }, + "attributes": { + "label": "Attributs de classification", + "all": "Tous les attributs" + } +} diff --git a/web/public/locales/fr/components/icons.json b/web/public/locales/fr/components/icons.json new file mode 100644 index 0000000..f713f2f --- /dev/null +++ b/web/public/locales/fr/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "Rechercher une icône…" + }, + "selectIcon": "Sélectionnez une icône" + } +} diff --git a/web/public/locales/fr/components/input.json b/web/public/locales/fr/components/input.json new file mode 100644 index 0000000..0d8130c --- /dev/null +++ b/web/public/locales/fr/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Télécharger la vidéo", + "toast": { + "success": "Le téléchargement de la vidéo a commencé." + } + } + } +} diff --git a/web/public/locales/fr/components/player.json b/web/public/locales/fr/components/player.json new file mode 100644 index 0000000..12e0108 --- /dev/null +++ b/web/public/locales/fr/components/player.json @@ -0,0 +1,52 @@ +{ + "noRecordingsFoundForThisTime": "Aucun enregistrement trouvé pour cette période", + "noPreviewFoundFor": "Aucun aperçu trouvé pour {{cameraName}}", + "noPreviewFound": "Aucun aperçu trouvé", + "submitFrigatePlus": { + "title": "Soumettre cette image à Frigate+ ?", + "submit": "Soumettre", + "previewError": "Impossible de télécharger le snapshot. L'enregistrement ne pas disponible sur cette période." + }, + "streamOffline": { + "title": "Flux hors ligne", + "desc": "Aucune image n'a été reçue sur le flux de détection de la caméra {{cameraName}}. Vérifiez le journal d'erreurs." + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 ou une version supérieure est requise pour ce type de flux en direct.", + "cameraDisabled": "La caméra est désactivée.", + "stats": { + "streamType": { + "title": "Type de flux :", + "short": "Type" + }, + "bandwidth": { + "title": "Bande passante :", + "short": "Bande passante" + }, + "latency": { + "title": "Latence :", + "value": "{{seconds}} secondes", + "short": { + "title": "Latence", + "value": "{{seconds}} s" + } + }, + "droppedFrames": { + "short": { + "value": "{{droppedFrames}} images", + "title": "Perdues" + }, + "title": "Images perdues :" + }, + "decodedFrames": "Images décodées :", + "droppedFrameRate": "Taux d'images perdues :", + "totalFrames": "Total images :" + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "Échec de la soumission de l'image à Frigate+" + }, + "success": { + "submittedFrigatePlus": "Image soumise avec succès à Frigate+" + } + } +} diff --git a/web/public/locales/fr/config/cameras.json b/web/public/locales/fr/config/cameras.json new file mode 100644 index 0000000..919ee6d --- /dev/null +++ b/web/public/locales/fr/config/cameras.json @@ -0,0 +1,332 @@ +{ + "name": { + "label": "Nom de la caméra", + "description": "Le nom de la caméra est requis" + }, + "friendly_name": { + "label": "Nom convivial", + "description": "Nom convivial de la caméra utilisé dans l'IU Frigate" + }, + "enabled": { + "label": "Activé", + "description": "Activé" + }, + "audio": { + "label": "Événements audio", + "description": "Réglages pour la détection des événements audio de cette caméra.", + "enabled": { + "label": "Activer la détection audio", + "description": "Activer ou désactiver la détection des événements audio pour cette caméra." + }, + "max_not_heard": { + "description": "Nombre de secondes sans le type audio configuré avant que l'événement audio se termine.", + "label": "Délai d'inactivité" + }, + "min_volume": { + "label": "Volume minimal", + "description": "Seuil minimal d'activation du volume en moyenne quadratique requis pour exécuter la détection audio. Des valeurs plus faibles augmentent la sensibilité (p. ex. 200 est élevé, 500 est moyen et 1000 est faible)." + }, + "listen": { + "label": "Types d'écoute", + "description": "Liste des types d'événements audio à détecter (p. ex. bark, fire_alarm, scream, speech, yell)." + }, + "filters": { + "label": "Filtres audio", + "description": "Réglages des filtres par type audio, tels que seuils de confiance utilisé afin de réduire les faux positifs." + }, + "enabled_in_config": { + "label": "État audio original", + "description": "Indique si la détection audio était initialement activée dans le fichier de configuration statique." + }, + "num_threads": { + "label": "Fils d'exécution pour la détection", + "description": "Nombre de fils d'éxécution à utiliser pour le traitement de la détection audio." + } + }, + "audio_transcription": { + "label": "Transcription audio", + "description": "Réglages pour la transcription audio et vocale utilisée pour les événements et les sous-titres en temps réel.", + "enabled": { + "label": "Activer la transcription", + "description": "Activer ou désactiver le déclenchement manuel de la transcription des événements audio." + }, + "enabled_in_config": { + "label": "État original de la transcription" + }, + "live_enabled": { + "label": "Transcription en temps réel", + "description": "Activer la diffusion de la transcription en temps réel pour le flux sonore dès sa réception." + } + }, + "birdseye": { + "label": "À vol d'oiseau", + "description": "Réglages pour la vue composée à vol d'oiseau qui combine plusieurs flux de caméras dans une simple disposition.", + "enabled": { + "label": "Activer la vue à vol d'oiseau", + "description": "Activer ou désactiver la fonctionalité de vue à vol d'oiseau." + }, + "mode": { + "label": "Mode de suivi", + "description": "Mode pour l'inclusion des caméras dans la vue à vol d'oiseau: 'objects', 'motion', ou 'continuous'." + }, + "order": { + "label": "Emplacement", + "description": "Emplacement numérique contrôlant l'ordre de la caméra dans la disposition en vue à vol d'oiseau." + } + }, + "detect": { + "label": "Détection d'objets", + "description": "Réglages pour la détection ou le rôle de détection utilisé pour exécuter la détection des objets et initialiser les traceurs.", + "enabled": { + "label": "Activer la détection d'objet", + "description": "Activer ou désactiver la détection des objets pour cette caméra." + }, + "height": { + "label": "Hauteur de détection", + "description": "Hauteur (en pixels) des images utilisées pour le flux de détection ; garder vide pour utiliser la résolution native du flux." + }, + "width": { + "label": "Largeur de détection", + "description": "Largeur (en pixels) des images utilisées pour le flux de détection ; garder vide pour utiliser la résolution native du flux." + }, + "fps": { + "label": "IPS de la détection", + "description": "Nombre cible d'images par seconde à utiliser pour la détection ; des valeurs plus faibles réduisent l'utilisation de l'UCT (la valeur recommandée est 5, ne la définir à une valeur supérieure - au maximum 10, uniquement lors du suivi d'objets se déplaçant extrêmement rapidement)." + }, + "min_initialized": { + "label": "Minimum d'images d'initialisation", + "description": "Nombre de détections consécutives requises avant de créer un objet suivi. Augmenter pour réduire les initialisations erronées. La valeur par défaut est fps divisé par 2." + }, + "max_disappeared": { + "label": "Nombre maximal d'images disparues", + "description": "Nombre d'images sans détection avant qu'un objet suivi est considéré comme étant disparu." + }, + "stationary": { + "label": "Configuration des objets stationnaires", + "description": "Réglages pour la détection et la gestion des objets qui restent stationnaires pendant un certain temps.", + "interval": { + "label": "Intervalle stationnaire", + "description": "À quelle fréquence (en images) effectuer une détection pour la confirmation d'un objet stationnaire." + }, + "threshold": { + "label": "Seuil d'activation stationnaire", + "description": "Nombre d'images sans changement d'emplacement requis pour marquer un objet en tant que stationnaire." + }, + "max_frames": { + "label": "Nombre max. d'images", + "description": "Limite le temps pour lequel les objets stationnaires sont suivis avant d'être supprimés.", + "default": { + "label": "Nombre max. d'images par défaut", + "description": "Nombre maximal d'images pour suivre un objet stationnaire avant d'arrêter." + }, + "objects": { + "label": "Nombre max. d'images pour l'objet", + "description": "Remplacement des réglages par défaut par objet pour le nombre maximal d'images requis pour suivre les objets stationnaires." + } + }, + "classifier": { + "label": "Activer le classificateur visuel", + "description": "Utiliser un classificateur visuel pour détecter les objets véritablement stationnaires même lorsque les boîtes englobantes tremblent." + } + }, + "annotation_offset": { + "label": "Décalage de l'annotation", + "description": "Millisecondes pour le décalage des annotations afin de mieux aligner les boîtes englobantes de la ligne du temps avec les enregistrements ; peut être positif ou négatif." + } + }, + "face_recognition": { + "label": "Reconnaissance faciale", + "description": "Réglages pour la détection et reconnaissance faciale pour cette caméra.", + "enabled": { + "label": "Activer la reconnaissance faciale", + "description": "Activer ou désactiver la reconnaissance faciale." + }, + "min_area": { + "label": "Surface minimale du visage", + "description": "Surface minimale (en pixels) d'une boîte faciale détectée requise pour tenter la reconnaissance." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "Réglages de FFmpeg incluant l'emplacement du fichier binaire, les arguments, les options pour hwaccel et les arguments de sortie par rôle.", + "path": { + "label": "Emplacement de FFmpeg", + "description": "Emplacement du fichier binaire de FFmpeg à utiliser ou un alias de version (peut être «5.0» ou «7.0»)." + }, + "global_args": { + "label": "Arguments globaux de FFmpeg", + "description": "Arguments globaux transmis aux processus de FFmpeg." + }, + "hwaccel_args": { + "label": "Arguments pour l'accélération matérielle", + "description": "Arguments de l'accélération matérielle pour FFmpeg. Les préréglages spécifiques au fournisseur sont recommandés." + }, + "input_args": { + "label": "Arguments d'entrée", + "description": "Arguments d'entrée appliqués aux flux d'entrée FFmpeg." + }, + "output_args": { + "label": "Arguments de sortie", + "description": "Arguments de sortie par défaut utilisés pour les différents rôles FFmpeg, tels que detect et record.", + "detect": { + "label": "Détecter les arguments de sortie", + "description": "Arguments de sortie par défaut pour les flux du rôle detect." + }, + "record": { + "label": "Arguments de sortie pour l'enregistrement", + "description": "Arguments de sortie par défaut pour les flux du rôle record." + } + }, + "retry_interval": { + "label": "Temps de réessai FFmpeg", + "description": "Nombre de secondes à attendre avant de tenter de reconnecter un flux de caméra après un échec. La valeur par défaut est 10." + }, + "apple_compatibility": { + "label": "Compatibilité avec Apple", + "description": "Activer l'étiquetage HEVC pour une meilleure compatibilité avec les lecteurs Apple lors de l'enregistrement H.265." + }, + "gpu": { + "label": "Index de l'UTG", + "description": "Index par défaut de l'UTG utilisé pour l'accélération matérielle si disponible." + }, + "inputs": { + "label": "Entrées des caméras", + "description": "Liste des définitions des flux entrants (emplacements et rôles) pour cette caméra.", + "path": { + "label": "Emplacement d'entrée", + "description": "URL ou emplacement du flux d'entrée de la caméra." + }, + "roles": { + "label": "Rôles d'entrée", + "description": "Rôles pour ce flux entrant." + }, + "global_args": { + "label": "Arguments globaux de FFmpeg", + "description": "Arguments globaux de FFmpeg pour ce flux entrant." + }, + "hwaccel_args": { + "label": "Arguments pour l'accélération matérielle", + "description": "Arguments de l'accélération matérielle pour ce flux entrant." + }, + "input_args": { + "label": "Arguments d'entrée", + "description": "Arguments d'entrée spéficiques à ce flux." + } + } + }, + "live": { + "label": "Lecture en direct", + "description": "Réglages utilisés par l'IU Web afin de contrôler la sélection, la résolution et la qualité des flux en direct.", + "streams": { + "label": "Nom des flux en direct", + "description": "Mappage des noms des flux configurés vers les noms de restream et go2rtc utilisés pour la lecture en direct." + }, + "height": { + "label": "Hauteur de la diffusion en direct", + "description": "Hauteur (en pixels) à laquelle afficher le flux en direct jsmpeg dans l'IU Web ; doit être inférieure ou égale à la hauteur détectée du flux." + }, + "quality": { + "label": "Qualité de la diffusion en direct", + "description": "Qualité de l'encodage pour le flux jsmpeg (1 étant la plus élevée, 31 la plus faible)." + } + }, + "lpr": { + "label": "Reconnaissance des plaques d'immatriculation", + "description": "Réglages de la reconnaissance des plaques d'immatriculation incluant les seuils de détection, le formatage et les plaques connues.", + "enabled": { + "label": "Activer la RPI", + "description": "Activer ou désactiver la RPI sur cette caméra." + }, + "expire_time": { + "label": "Expiration en secondes", + "description": "Temps en secondes après lequel une plaque non vue expire du système de suivi (seulement pour les caméras dédiées à la RPI)." + }, + "min_area": { + "label": "Surface minimale de la plaque", + "description": "Surface minimale de la plaque (en pixels) requise pour tenter la reconnaissance." + }, + "enhancement": { + "label": "Niveau de l'enrichissement", + "description": "Niveau de l'enrichissement (de 0 à 10) à appliquer aux recadrages des plaques avant la ROC. Des valeurs plus élevées n'améliorent pas nécessairement les résultats, les niveaux supérieurs à 5 peuvent ne fonctionner qu'avec des plaques la nuit et doivent être utilisés avec prudence." + } + }, + "motion": { + "label": "Détection du mouvement", + "description": "Réglages par défaut de la détection de mouvement pour cette caméra.", + "enabled": { + "label": "Activer la détection de mouvement", + "description": "Activer ou désactiver la détection de mouvement pour cette caméra." + }, + "threshold": { + "label": "Seuil de détection du mouvement", + "description": "Seuil de différence de pixels utilisé par le détecteur de mouvement ; les valeurs plus élevées réduisent la sensibilité (plage de 1 à 255)." + }, + "lightning_threshold": { + "label": "Seuil d'éclairage", + "description": "Seuil permettant de détecter et d'ignorer les brusques pointes d'éclairage (plus la valeur est faible, plus la sensibilité est élevée, valeurs comprises entre 0.3 et 1.0)." + }, + "improve_contrast": { + "label": "Améliorer le contraste", + "description": "Appliquer les amélioration du contraste aux images avant l'analyse de mouvement afin d'améliorer la détection." + }, + "contour_area": { + "label": "Zone de contour", + "description": "Aire de la zone de contour minimale en pixels requise pour qu'un contour de mouvement soit comptabilisé." + }, + "delta_alpha": { + "label": "Delta pour alpha", + "description": "Facteur de mélange alpha utilisé dans la différenciation d'images pour le calcul du mouvement." + }, + "frame_alpha": { + "label": "Alpha pour l'image", + "description": "Valeur alpha utilisée lors du mélange d'images pour le prétraitement du mouvement." + }, + "frame_height": { + "label": "Hauteur de l'image", + "description": "Hauteur en pixels à laquelle mettre à l'échelle les images lors du traitement du mouvement." + }, + "mask": { + "label": "Moordonnées du masque", + "description": "Coordonnées ordonnés x et y définissant le polygone du masque de mouvement utilisé pour inclure ou exclure des aires." + }, + "mqtt_off_delay": { + "label": "Délai de désactivation de MQTT", + "description": "Nombre de secondes à attendre après le dernier mouvement avant de publier un état « off » MQTT." + }, + "enabled_in_config": { + "label": "État original du mouvement", + "description": "Indique si la détection de mouvement a été activée dans la configuration originale statique." + }, + "raw_mask": { + "label": "Masque brut" + }, + "skip_motion_threshold": { + "label": "Ignorer le seuil de détection de mouvement", + "description": "Si une valeur entre 0,0 et 1,0 est définie, et que plus de cette fraction de l'image change en une seule trame, le détecteur ne retournera aucune zone de mouvement et se recalibrera immédiatement. Cela peut économiser du CPU et réduire les faux positifs lors d'éclairs, d'orages, etc., mais peut manquer des événements réels comme une caméra PTZ suivant automatiquement un objet. Le compromis est entre perdre quelques mégaoctets d'enregistrements ou visionner quelques courts clips. Laisser vide (None) pour désactiver cette fonctionnalité." + } + }, + "objects": { + "label": "Objets", + "description": "Réglages par défaut pour le suivi des objets incluant les étiquettes à suivre et les filtres par objets.", + "track": { + "label": "Objets à suivre", + "description": "Liste des étiquettes d'objets à suivre pour cette caméra." + }, + "filters": { + "label": "Filtres d'objets", + "description": "Filtres appliqués aux objets détectés afin de réduire les faux positifs (aire, rapport, facteur de confiance).", + "min_area": { + "label": "Aire minimal de l'objet", + "description": "Surface minimale de la boîte englobante (en pixels ou pourcentage) requise pour ce type d'objet. Peut être exprimée en pixels (entier) ou en pourcentage (flottant entre 0,000001 et 0,99)." + }, + "max_area": { + "label": "Zone d'objet maximum", + "description": "Zone de boite englobante maximum (pixels ou pourcentage) autorisée pour ce type d'objet. Peut être en pixels (entier) ou pourcentage (décimale entre 0,000001 and 0,99)." + }, + "min_ratio": { + "label": "Rapport d'aspect minimal" + } + } + }, + "label": "Configuration de la caméra" +} diff --git a/web/public/locales/fr/config/global.json b/web/public/locales/fr/config/global.json new file mode 100644 index 0000000..97093cc --- /dev/null +++ b/web/public/locales/fr/config/global.json @@ -0,0 +1,119 @@ +{ + "version": { + "label": "Version actuelle de la configuration", + "description": "Version numérique ou textuelle de la configuration active, utilisée pour aider à détecter les migrations ou les changements de format." + }, + "safe_mode": { + "label": "Mode sans échec", + "description": "Si activé, Frigate démarre en mode sans échec avec des fonctionnalités réduites pour le dépannage." + }, + "environment_vars": { + "label": "Variables d'environnement", + "description": "Paires clé/valeur des variables d'environnement à définir pour le processus Frigate sous Home Assistant OS. Les utilisateurs sans HAOS doivent utiliser la configuration des variables d'environnement de Docker à la place." + }, + "logger": { + "label": "Journalisation", + "description": "Contrôle la verbosité par défaut des journaux et les exceptions de niveau par composant.", + "default": { + "label": "Niveau de journalisation", + "description": "Verbosité de l'ensemble des journaux par défaut (débogage, information, avertissement, erreur)." + }, + "logs": { + "label": "Niveau de journalisation par processus", + "description": "Personnaliser le niveau de journalisation par composant pour augmenter ou diminuer la verbosité pour des modules spécifiques." + } + }, + "auth": { + "label": "Authentification", + "enabled": { + "label": "Activer l'authentification", + "description": "Active l'authentification native de l'interface de Frigate." + }, + "description": "Paramètres d'authentification et de session, y compris les options relatives aux cookies et à la limitation du débit.", + "reset_admin_password": { + "label": "réinitialiser le mot de passe administrateur", + "description": "Si vrai, réinitialise le mot de passe utilisateur administrateur au démarrage et écrit le nouveau mot de passe dans les journaux." + }, + "cookie_name": { + "label": "Nom du cookie JWT", + "description": "Nom du cookie utilisé pour stocker le jeton JWT pour authentification native." + }, + "cookie_secure": { + "label": "Drapeau du cookie sécurisé", + "description": "Active le drapeau sécurisé sur le cookie d'authentification ; Doit être activé quand le TLS est utilisé." + }, + "session_length": { + "label": "Longueur de session", + "description": "Durée de session en secondes pour les sessions basé sur JWT." + }, + "refresh_time": { + "label": "Fenêtre de rafraichissement de session", + "description": "Lorsqu'une session est à moins de ce nombre de secondes d'expirer, rétablissez-la à sa durée entière." + }, + "failed_login_rate_limit": { + "label": "Limite de connexions échouées", + "description": "Règles limitant la fréquence des tentatives ratées d'authentification afin de réduire les attaques de type \"brute-force\"." + }, + "trusted_proxies": { + "label": "Mandataire de confiance", + "description": "Liste des IP de mandataire de confiance quand il faut déterminer l'IP pour limiter le taux." + }, + "hash_iterations": { + "label": "Itérations de hachage", + "description": "Nombre d'itérations PBKDF2-SHA256 à utiliser quand les mots de passe utilisateur sont hachés." + }, + "roles": { + "label": "Correspondance des rôles", + "description": "Correspondance de rôles vers la liste des caméras. Une liste vide donne l'accès totale à toutes les caméras pour ce rôle." + }, + "admin_first_time_login": { + "label": "Drapeau admin première fois", + "description": "Si activé, l'interface peut afficher un lien d'aide sur la page d'identification des utilisateurs indiquant comment se connecter après une réinitialisation du mot de passe administrateur. " + } + }, + "database": { + "label": "Base de donnée", + "description": "Réglages concernant la base de donnée SQLite utilisé par Frigate pour stocker les objets suivis et enregistrer les métadonnées.", + "path": { + "label": "Chemin vers la base de donnée" + } + }, + "genai": { + "provider": { + "label": "Fournisseur" + } + }, + "birdseye": { + "quality": { + "label": "Qualité d'encodage" + } + }, + "detect": { + "enabled": { + "label": "Activer la détection d'objet" + } + }, + "motion": { + "skip_motion_threshold": { + "label": "Ignorer le seuil de détection de mouvement", + "description": "Si une valeur entre 0,0 et 1,0 est définie, et que plus de cette fraction de l'image change en une seule trame, le détecteur ne retournera aucune zone de mouvement et se recalibrera immédiatement. Cela peut économiser du CPU et réduire les faux positifs lors d'éclairs, d'orages, etc., mais peut manquer des événements réels comme une caméra PTZ suivant automatiquement un objet. Le compromis est entre perdre quelques mégaoctets d'enregistrements ou visionner quelques courts clips. Laisser vide (None) pour désactiver cette fonctionnalité." + } + }, + "objects": { + "filters": { + "min_area": { + "description": "Surface minimale de la boîte englobante (en pixels ou pourcentage) requise pour ce type d'objet. Peut être exprimée en pixels (entier) ou en pourcentage (flottant entre 0,000001 et 0,99)." + }, + "max_area": { + "label": "Zone d'objet maximum", + "description": "Zone de boite englobante maximum (pixels ou pourcentage) autorisée pour ce type d'objet. Peut être en pixels (entier) ou pourcentage (décimale entre 0,000001 and 0,99)." + }, + "min_ratio": { + "label": "Rapport d'aspect minimal" + } + } + }, + "audio_transcription": { + "label": "Transcription audio" + } +} diff --git a/web/public/locales/fr/config/groups.json b/web/public/locales/fr/config/groups.json new file mode 100644 index 0000000..2d1e6c0 --- /dev/null +++ b/web/public/locales/fr/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Détection générale", + "sensitivity": "Sensibilité globale" + }, + "cameras": { + "detection": "Détection", + "sensitivity": "Sensibilité" + } + }, + "timestamp_style": { + "global": { + "appearance": "Apparence générale" + }, + "cameras": { + "appearance": "Apparence" + } + }, + "motion": { + "global": { + "sensitivity": "Sensibilité globale", + "algorithm": "Algorithme global" + }, + "cameras": { + "sensitivity": "Sensibilité", + "algorithm": "Algorithme" + } + }, + "snapshots": { + "global": { + "display": "Affichage Global" + }, + "cameras": { + "display": "Affichage" + } + }, + "detect": { + "global": { + "resolution": "Résolution globale", + "tracking": "Suivi global" + }, + "cameras": { + "resolution": "Résolution", + "tracking": "Suivi" + } + }, + "objects": { + "global": { + "tracking": "Suivi Global", + "filtering": "Filtrage Global" + }, + "cameras": { + "tracking": "Suivi", + "filtering": "Filtrage" + } + }, + "record": { + "global": { + "retention": "Rétention Globale", + "events": "Événements globaux" + }, + "cameras": { + "retention": "Rétention", + "events": "Événements" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Arguments FFmpeg spécifiques aux caméras" + } + } +} diff --git a/web/public/locales/fr/config/validation.json b/web/public/locales/fr/config/validation.json new file mode 100644 index 0000000..ac3853b --- /dev/null +++ b/web/public/locales/fr/config/validation.json @@ -0,0 +1,32 @@ +{ + "minimum": "Doit être au moins de {{limit}}", + "maximum": "Doit être au maximum {{limit}}", + "exclusiveMinimum": "Doit être supérieur à {{limit}}", + "exclusiveMaximum": "Doit être inférieur à {{limit}}", + "minLength": "Doit contenir au moins {{limit}} caractère(s)", + "maxLength": "Doit contenir au maximum {{limit}} caractère(s)", + "minItems": "Doit contenir au moins {{limit}} élément(s)", + "maxItems": "Doit contenir au maximum {{limit}} élément(s)", + "pattern": "Format incorrect", + "required": "Ce champ est requis", + "type": "Type de valeur incorrect", + "enum": "Doit être une des valeurs autorisées", + "const": "La valeur ne correspond pas à la constante attendu", + "uniqueItems": "Tous les éléments doivent être uniques", + "format": "Format invalide", + "additionalProperties": "Une propriété inconnue est interdite", + "oneOf": "Doit correspondre exactement à un des schémas autorisés", + "anyOf": "Doit correspondre à au moins un des schémas autorisés", + "proxy": { + "header_map": { + "roleHeaderRequired": "L'entête de rôle est nécessaire quand la cartographie des rôles est configurée." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "Chaque rôle ne peut être assigné qu'à un flux d'entrée.", + "detectRequired": "Au moins un flux d'entrée doit être assigné au rôle 'detect' (détection).", + "hwaccelDetectOnly": "Seulement le flux d'entrée avec le rôle de détection peut définir des arguments pour l'accélération matérielle." + } + } +} diff --git a/web/public/locales/fr/objects.json b/web/public/locales/fr/objects.json new file mode 100644 index 0000000..fdd517c --- /dev/null +++ b/web/public/locales/fr/objects.json @@ -0,0 +1,129 @@ +{ + "bicycle": "Vélo", + "car": "Voiture", + "person": "Personne", + "motorcycle": "Moto", + "airplane": "Avion", + "bus": "Bus", + "train": "Train", + "boat": "Bateau", + "traffic_light": "Feu de circulation", + "fire_hydrant": "Bouche d'incendie", + "street_sign": "Panneau de signalisation", + "parking_meter": "Parcmètre", + "bench": "Banc", + "bird": "Oiseau", + "cat": "Chat", + "stop_sign": "Panneau de stop", + "dog": "Chien", + "horse": "Cheval", + "sheep": "Mouton", + "cow": "Vache", + "elephant": "Éléphant", + "bear": "Ours", + "zebra": "Zèbre", + "hat": "Chapeau", + "tie": "Cravate", + "suitcase": "Valise", + "frisbee": "Frisbee", + "skis": "Skis", + "snowboard": "Snowboard", + "sports_ball": "Ballon de sport", + "kite": "Cerf-volant", + "baseball_bat": "Batte de baseball", + "umbrella": "Parapluie", + "giraffe": "Girafe", + "eye_glasses": "Lunettes", + "backpack": "Sac à dos", + "handbag": "Sac à main", + "shoe": "Chaussure", + "clock": "Horloge", + "bottle": "Bouteille", + "baseball_glove": "Gant de baseball", + "skateboard": "Skateboard", + "surfboard": "Planche de surf", + "tennis_racket": "Raquette de tennis", + "plate": "Assiette", + "cup": "Tasse", + "banana": "Banane", + "apple": "Pomme", + "wine_glass": "Verre à vin", + "pizza": "Pizza", + "couch": "Canapé", + "potted_plant": "Plante en pot", + "mirror": "Miroir", + "window": "Fenêtre", + "desk": "Bureau", + "door": "Porte", + "remote": "Télécommande", + "keyboard": "Clavier", + "mouse": "Souris", + "tv": "TV", + "laptop": "Ordinateur portable", + "toaster": "Grille-pain", + "book": "Livre", + "teddy_bear": "Ours en peluche", + "blender": "Mixeur", + "toothbrush": "Brosse à dents", + "hair_brush": "Brosse à cheveux", + "vehicle": "Véhicule", + "fox": "Renard", + "deer": "Cerf", + "animal": "Animal", + "goat": "Chèvre", + "rabbit": "Lapin", + "raccoon": "Raton laveur", + "waste_bin": "Poubelle", + "robot_lawnmower": "Robot tondeuse", + "on_demand": "Sur demande", + "face": "Visage", + "license_plate": "Plaque d'immatriculation", + "bbq_grill": "Barbecue", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "package": "Colis", + "an_post": "An Post", + "gls": "GLS", + "dpd": "DPD", + "postnl": "PostNL", + "amazon": "Amazon", + "hot_dog": "Hot Dog", + "refrigerator": "Réfrigérateur", + "bark": "Aboiement", + "oven": "Four", + "scissors": "Ciseaux", + "toilet": "Toilettes", + "carrot": "Carotte", + "bed": "Lit", + "cell_phone": "Téléphone portable", + "fork": "Fourchette", + "squirrel": "Écureuil", + "microwave": "Micro-ondes", + "hair_dryer": "Sèche-cheveux", + "bowl": "Bol", + "spoon": "Cuillère", + "sandwich": "Sandwich", + "sink": "Évier", + "broccoli": "Brocoli", + "knife": "Couteau", + "nzpost": "NZPost", + "orange": "Orange", + "chair": "Chaise", + "donut": "Donut", + "usps": "USPS", + "cake": "Gâteau", + "dining_table": "Table à manger", + "vase": "Vase", + "purolator": "Purolator", + "postnord": "PostNord", + "canada_post": "Poste du Canada", + "royal_mail": "Poste du Royaume Uni", + "school_bus": "Bus scolaire", + "skunk": "Mouffette", + "kangaroo": "Kangourou", + "baby": "Bébé", + "baby_stroller": "Poussette", + "rickshaw": "Pousse-pousse", + "Rodent": "Rongeur" +} diff --git a/web/public/locales/fr/views/chat.json b/web/public/locales/fr/views/chat.json new file mode 100644 index 0000000..72736b3 --- /dev/null +++ b/web/public/locales/fr/views/chat.json @@ -0,0 +1,22 @@ +{ + "documentTitle": "Messagerie - Frigate", + "title": "Messagerie Frigate", + "subtitle": "Votre assistant IA pour la gestion des camera et des aperçus", + "placeholder": "Posez moi toutes vos questions...", + "error": "Il y a eu un problème. Merci de bien vouloir réessayer.", + "processing": "Traitement en cours...", + "hideTools": "Masquer les outils", + "call": "Appeler", + "result": "Résultat", + "arguments": "Arguments :", + "response": "Réponse :", + "attachment_chip_label": "{{label}} sur {{camera}}", + "attachment_chip_remove": "Supprimer la pièce jointe", + "open_in_explore": "Ouvrir dans l'explorateur", + "attach_event_aria": "Attacher l'événement {{eventId}}", + "attachment_picker_paste_label": "Ou coller l'event ID", + "attachment_picker_placeholder": "Attacher un événement", + "quick_reply_find_similar": "Trouver des observations similaires", + "no_similar_objects_found": "Aucun objet similaire trouvé.", + "semantic_search_required": "La recherche sémantique doit être activée afin de trouver un objet similaire." +} diff --git a/web/public/locales/fr/views/classificationModel.json b/web/public/locales/fr/views/classificationModel.json new file mode 100644 index 0000000..df090b3 --- /dev/null +++ b/web/public/locales/fr/views/classificationModel.json @@ -0,0 +1,199 @@ +{ + "documentTitle": "Modèles de classification - Frigate", + "button": { + "deleteClassificationAttempts": "Supprimer les images de classification", + "renameCategory": "Renommer la classe", + "deleteCategory": "Supprimer la classe", + "deleteImages": "Supprimer les images", + "trainModel": "Entraîner le modèle", + "addClassification": "Ajouter une classification", + "deleteModels": "Supprimer les modèles", + "editModel": "Modifier le modèle" + }, + "toast": { + "success": { + "deletedCategory_one": "{{count}} classe supprimée", + "deletedCategory_many": "{{count}} classes supprimées", + "deletedCategory_other": "{{count}} classes supprimées", + "deletedImage_one": "{{count}} image supprimée", + "deletedImage_many": "{{count}} images supprimées", + "deletedImage_other": "{{count}} images supprimées", + "categorizedImage": "Image classifiée avec succès", + "trainedModel": "Modèle entraîné avec succès.", + "trainingModel": "L'entraînement du modèle a démarré avec succès.", + "deletedModel_one": "{{count}} modèle supprimé avec succès", + "deletedModel_many": "{{count}} modèles supprimés avec succès", + "deletedModel_other": "{{count}} modèles supprimés avec succès", + "updatedModel": "Configuration du modèle mise à jour avec succès", + "renamedCategory": "Classe renommée en {{name}} avec succès", + "reclassifiedImage": "Image reclassifiée avec succès" + }, + "error": { + "deleteImageFailed": "Échec de la suppression : {{errorMessage}}", + "deleteCategoryFailed": "Échec de la suppression de la classe : {{errorMessage}}", + "categorizeFailed": "Échec de la catégorisation de l'image : {{errorMessage}}", + "trainingFailed": "L'entraînement du modèle a échoué. Consultez les journaux de Frigate pour plus de détails.", + "deleteModelFailed": "Impossible de supprimer le modèle : {{errorMessage}}", + "updateModelFailed": "Impossible de mettre à jour le modèle : {{errorMessage}}", + "renameCategoryFailed": "Impossible de renommer la classe : {{errorMessage}}", + "trainingFailedToStart": "Impossible de démarrer l'entraînement du modèle : {{errorMessage}}", + "reclassifyFailed": "Échec de reclassification de l'image : {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Supprimer la classe", + "desc": "Êtes-vous sûr de vouloir supprimer la classe {{name}} ? Cette action supprimera définitivement toutes les images associées et nécessitera un réentraînement du modèle.", + "minClassesTitle": "Impossible de supprimer la classe", + "minClassesDesc": "Un modèle de classification doit avoir au moins 2 classes. Ajoutez une autre classe avant de supprimer celle-ci." + }, + "deleteDatasetImages": { + "title": "Supprimer les images du jeu de données", + "desc_one": "Êtes-vous sûr de vouloir supprimer {{count}} image du jeu de données {{dataset}} ? Cette action est irréversible et nécessitera un réentraînement du modèle.", + "desc_many": "Êtes-vous sûr de vouloir supprimer {{count}} images du jeu de données {{dataset}} ? Cette action est irréversible et nécessitera un réentraînement du modèle.", + "desc_other": "Êtes-vous sûr de vouloir supprimer {{count}} images du jeu de données {{dataset}} ? Cette action est irréversible et nécessitera un réentraînement du modèle." + }, + "deleteTrainImages": { + "title": "Supprimer les images d'entraînement", + "desc_one": "Êtes-vous sûr de vouloir supprimer {{count}} image ? Cette action est irréversible.", + "desc_many": "Êtes-vous sûr de vouloir supprimer {{count}} images ? Cette action est irréversible.", + "desc_other": "Êtes-vous sûr de vouloir supprimer {{count}} images ? Cette action est irréversible." + }, + "renameCategory": { + "title": "Renommer la classe", + "desc": "Saisissez un nouveau nom pour {{name}}. Vous devrez réentraîner le modèle pour que le changement de nom prenne effet." + }, + "description": { + "invalidName": "Nom invalide. Les noms ne peuvent contenir que des lettres, des chiffres, des espaces, des apostrophes, des traits de soulignement et des tirets." + }, + "train": { + "title": "Classifications récentes", + "aria": "Sélectionner des classifications récentes", + "titleShort": "Récent" + }, + "categories": "Classes", + "createCategory": { + "new": "Créer une nouvelle classe" + }, + "categorizeImageAs": "Classifier comme :", + "categorizeImage": "Classifier l'image", + "noModels": { + "object": { + "title": "Aucun modèle de classification d'objets", + "description": "Créer un modèle personnalisé pour classifier les objets détectés", + "buttonText": "Créer un modèle d'objets" + }, + "state": { + "title": "Aucun modèle de classification d'états", + "description": "Créer un modèle personnalisé pour surveiller et classifier les changements d'état dans des zones de caméra spécifiques", + "buttonText": "Créer un modèle d'états" + } + }, + "wizard": { + "title": "Créer une nouvelle classification", + "steps": { + "nameAndDefine": "Nom et définition", + "stateArea": "Zone d'état", + "chooseExamples": "Choisir des exemples" + }, + "step1": { + "description": "Les modèles d'état surveillent des zones de caméra fixes pour détecter des changements (par ex., porte ouverte/fermée). Les modèles d'objets ajoutent des classifications aux objets détectés (par ex., animaux connus, livreurs, etc.).", + "name": "Nom", + "namePlaceholder": "Saisissez un nom de modèle.", + "type": "Type", + "typeState": "État", + "typeObject": "Objet", + "objectLabel": "Étiquette d'objet", + "objectLabelPlaceholder": "Sélectionnez un type d'objet.", + "classificationType": "Type de classification", + "classificationTypeTip": "En savoir plus sur les types de classification", + "classificationTypeDesc": "Les sous-étiquettes ajoutent du texte supplémentaire à l'étiquette d'objet (par ex., « Personne : UPS »). Les attributs sont des métadonnées recherchables stockées séparément dans les métadonnées de l'objet.", + "classificationSubLabel": "Sous-étiquette", + "classificationAttribute": "Attribut", + "classes": "Classes", + "classesTip": "En savoir plus sur les classes", + "classesStateDesc": "Définissez les différents états que votre zone de caméra peut avoir. Par exemple : « ouvert » et « fermé » pour une porte de garage.", + "classesObjectDesc": "Définissez les différentes catégories pour classifier les objets détectés. Par exemple : « livreur », « résident », « inconnu » pour la classification des personnes.", + "classPlaceholder": "Saisissez le nom de la classe.", + "errors": { + "nameRequired": "Le nom du modèle est requis.", + "nameLength": "Le nom du modèle ne doit pas dépasser 64 caractères.", + "nameOnlyNumbers": "Le nom du modèle ne peut pas contenir uniquement des chiffres.", + "classRequired": "Au moins une classe est requise.", + "classesUnique": "Les noms de classe doivent être uniques.", + "stateRequiresTwoClasses": "Les modèles d'état nécessitent au moins deux classes.", + "objectLabelRequired": "Veuillez sélectionner une étiquette d'objet.", + "objectTypeRequired": "Veuillez sélectionner un type de classification.", + "noneNotAllowed": "La classe 'aucun' n'est pas autorisée." + }, + "states": "États" + }, + "step2": { + "description": "Sélectionnez les caméras et définissez la zone à surveiller pour chaque caméra. Le modèle classifiera l'état de ces zones.", + "cameras": "Caméras", + "selectCamera": "Sélectionner une caméra", + "noCameras": "Cliquez sur + pour ajouter des caméras.", + "selectCameraPrompt": "Sélectionnez une caméra dans la liste pour définir sa zone de surveillance." + }, + "step3": { + "selectImagesPrompt": "Sélectionner toutes les images contenant : {{className}}", + "selectImagesDescription": "Cliquez sur les images pour les sélectionner. Cliquez sur Continuer lorsque vous avez terminé avec cette classe.", + "generating": { + "title": "Génération d'images d'exemple en cours", + "description": "Frigate récupère des images représentatives à partir de vos enregistrements. Cela peut prendre un moment..." + }, + "training": { + "title": "Entraînement du modèle", + "description": "Votre modèle est en cours d'entraînement en arrière-plan. Fermez cette boîte de dialogue. Votre modèle se lancera dès que l'entraînement sera terminé." + }, + "retryGenerate": "Réessayer la génération", + "noImages": "Aucune image d'exemple générée", + "classifying": "Classification et entraînement en cours...", + "trainingStarted": "Entraînement démarré avec succès", + "errors": { + "noCameras": "Aucune caméra n'est configurée.", + "noObjectLabel": "Aucune étiquette d'objet sélectionnée", + "generateFailed": "Échec de la génération des exemples : {{error}}", + "generationFailed": "Échec de la génération. Veuillez réessayer.", + "classifyFailed": "Échec de la classification des images : {{error}}" + }, + "generateSuccess": "Génération des images d'exemple réussie", + "allImagesRequired_one": "Veuillez classifier toutes les images. {{count}} image restante.", + "allImagesRequired_many": "Veuillez classifier toutes les images. {{count}} images restantes.", + "allImagesRequired_other": "Veuillez classifier toutes les images. {{count}} images restantes.", + "modelCreated": "Modèle créé avec succès. Utilisez la vue Classifications récentes pour ajouter des images pour les états manquants, puis entraînez le modèle.", + "missingStatesWarning": { + "title": "Exemples d'états manquants", + "description": "Pour des résultats optimaux, il est recommandé de sélectionner des exemples pour tous les états. Vous pouvez continuer sans cette étape, mais le modèle ne sera entraîné que lorsque chaque état disposera d'images. Continuez, puis utilisez la vue Classifications récentes pour classer les images manquantes et lancer l'entraînement." + } + } + }, + "deleteModel": { + "title": "Supprimer le modèle de classification", + "single": "Voulez-vous vraiment supprimer {{name}} ? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible.", + "desc_one": "Voulez-vous vraiment supprimer {{count}} modèle ? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible.", + "desc_many": "Voulez-vous vraiment supprimer {{count}} modèles ? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible.", + "desc_other": "Voulez-vous vraiment supprimer {{count}} modèles ? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible." + }, + "menu": { + "objects": "Objets", + "states": "États" + }, + "details": { + "scoreInfo": "Le score représente la moyenne de la confiance de classification pour toutes les détections de cet objet.", + "none": "Aucun", + "unknown": "Inconnu" + }, + "edit": { + "title": "Modifier le modèle de classification", + "descriptionState": "Modifier les classes pour ce modèle de classification d'état. Les modifications nécessiteront un réentraînement du modèle.", + "descriptionObject": "Modifier le type d'objet et le type de classification pour ce modèle de classification d'objet", + "stateClassesInfo": "Note : La modification des classes d'état nécessite un réentraînement du modèle avec les classes mises à jour." + }, + "tooltip": { + "trainingInProgress": "Modèle en cours d'entraînement", + "noNewImages": "Aucune nouvelle image pour l'entraînement. Veuillez d'abord classifier plus d'images dans le jeu de données.", + "modelNotReady": "Le modèle n'est pas prêt pour l'entraînement.", + "noChanges": "Aucune modification du jeu de données depuis le dernier entraînement" + }, + "none": "Aucun" +} diff --git a/web/public/locales/fr/views/configEditor.json b/web/public/locales/fr/views/configEditor.json new file mode 100644 index 0000000..0ab9b2c --- /dev/null +++ b/web/public/locales/fr/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Éditeur de configuration", + "documentTitle": "Éditeur de configuration - Frigate", + "copyConfig": "Copier la configuration", + "saveOnly": "Enregistrer uniquement", + "saveAndRestart": "Enregistrer et redémarrer", + "toast": { + "success": { + "copyToClipboard": "Configuration copiée dans le presse-papiers" + }, + "error": { + "savingError": "Erreur lors de l'enregistrement de la configuration" + } + }, + "confirm": "Quitter sans enregistrer ?", + "safeConfigEditor": "Éditeur de configuration (mode sans échec)", + "safeModeDescription": "Frigate est en mode sans échec en raison d'une erreur de validation de la configuration." +} diff --git a/web/public/locales/fr/views/events.json b/web/public/locales/fr/views/events.json new file mode 100644 index 0000000..e5fb251 --- /dev/null +++ b/web/public/locales/fr/views/events.json @@ -0,0 +1,70 @@ +{ + "detections": "Détections", + "motion": { + "label": "Mouvement", + "only": "Mouvement uniquement" + }, + "alerts": "Alertes", + "allCameras": "Toutes les caméras", + "empty": { + "alert": "Aucune alerte à traiter", + "detection": "Aucune détection à traiter", + "motion": "Aucune donnée de mouvement trouvée", + "recordingsDisabled": { + "title": "Les enregistrements doivent être activés.", + "description": "Les activités ne peuvent être générées pour une caméra que si l'enregistrement est activé pour celle-ci." + } + }, + "timeline": { + "label": "Chronologie" + }, + "events": { + "label": "Événements", + "aria": "Sélectionner les événements", + "noFoundForTimePeriod": "Aucun événement n'a été trouvé pour cette plage de temps." + }, + "documentTitle": "Activités - Frigate", + "recordings": { + "documentTitle": "Enregistrements - Frigate" + }, + "calendarFilter": { + "last24Hours": "Dernières 24 heures" + }, + "timeline.aria": "Sélectionner une chronologie", + "markAsReviewed": "Marquer comme traitê", + "newReviewItems": { + "button": "Nouvelles activités à traiter", + "label": "Afficher les nouvelles activités" + }, + "camera": "Caméra", + "markTheseItemsAsReviewed": "Marquer ces activités comme traitées", + "selected": "{{count}} sélectionné(s)", + "selected_other": "{{count}} sélectionné(s)", + "selected_one": "{{count}} sélectionné(s)", + "detected": "détecté", + "suspiciousActivity": "Activité suspecte", + "threateningActivity": "Activité menaçante", + "detail": { + "noDataFound": "Aucun détail à traiter", + "aria": "Activer/désactiver la vue détaillée", + "trackedObject_one": "{{count}} objet", + "trackedObject_other": "{{count}} objets", + "noObjectDetailData": "Aucun détail d'objet disponible", + "label": "Détail", + "settings": "Paramètres de la vue Détail", + "alwaysExpandActive": { + "title": "Toujours développer l'élément actif", + "desc": "Toujours développer les détails de l'objet pour l'activité en cours" + } + }, + "objectTrack": { + "trackedPoint": "Point suivi", + "clickToSeek": "Cliquez pour atteindre ce moment." + }, + "zoomIn": "Zoom avant", + "zoomOut": "Zoom arrière", + "normalActivity": "Normal", + "needsReview": "À traiter", + "securityConcern": "Problème de sécurité", + "select_all": "Tous" +} diff --git a/web/public/locales/fr/views/explore.json b/web/public/locales/fr/views/explore.json new file mode 100644 index 0000000..6c116ef --- /dev/null +++ b/web/public/locales/fr/views/explore.json @@ -0,0 +1,319 @@ +{ + "generativeAI": "IA générative", + "documentTitle": "Explorer - Frigate", + "exploreIsUnavailable": { + "title": "L'exploration est indisponible", + "embeddingsReindexing": { + "estimatedTime": "Temps restant estimé :", + "finishingShortly": "Bientôt fini", + "context": "L'exploration peut être utilisée une fois la réindexation des embeddings des objets suivis terminée.", + "startingUp": "Démarrage…", + "step": { + "thumbnailsEmbedded": "Embeddings des miniatures : ", + "descriptionsEmbedded": "Embeddings des descriptions  : ", + "trackedObjectsProcessed": "Objets suivis traités : " + } + }, + "downloadingModels": { + "context": "Frigate télécharge les modèles d'embeddings nécessaires pour prendre en charge la fonctionnalité de recherche sémantique. Cette opération peut prendre plusieurs minutes selon la vitesse de votre connexion réseau.", + "setup": { + "visionModelFeatureExtractor": "Extracteur de caractéristiques de modèle de vision", + "textTokenizer": "Tokeniseur de texte", + "visionModel": "Modèle de vision", + "textModel": "Modèle de texte" + }, + "tips": { + "documentation": "Lire la documentation", + "context": "Une fois les modèles téléchargés, il est conseillé de réindexer les embeddings de vos objets suivis." + }, + "error": "Une erreur est survenue. Vérifiez les journaux Frigate." + } + }, + "details": { + "timestamp": "Horodatage", + "item": { + "title": "Détails de l'activité", + "button": { + "share": "Partager cette activité", + "viewInExplore": "Afficher dans Explorer" + }, + "toast": { + "success": { + "regenerate": "Une nouvelle description a été demandée à {{provider}}. Selon la vitesse de votre fournisseur, la régénération de la nouvelle description peut prendre un certain temps.", + "updatedSublabel": "Sous-étiquette mise à jour avec succès", + "updatedLPR": "Plaque d'immatriculation mise à jour avec succès", + "audioTranscription": "Transcription audio demandée avec succès. Selon la vitesse de votre serveur Frigate, la transcription peut prendre un certain temps.", + "updatedAttributes": "Attributs mis à jour avec succès" + }, + "error": { + "regenerate": "Échec de l'appel de {{provider}} pour une nouvelle description : {{errorMessage}}", + "updatedSublabelFailed": "Échec de la mise à jour de la sous-étiquette : {{errorMessage}}", + "updatedLPRFailed": "Échec de la mise à jour de la plaque d'immatriculation : {{errorMessage}}", + "audioTranscription": "Échec de la demande de transcription audio : {{errorMessage}}", + "updatedAttributesFailed": "Échec de la mise à jour des attributs  : {{errorMessage}}" + } + }, + "tips": { + "mismatch_one": "{{count}} objet indisponible a été détecté et intégré dans cette activité. Cet objet n'a pas été qualifié comme une alerte ou une détection, ou a déjà été nettoyé / supprimé.", + "mismatch_many": "{{count}} objets indisponibles ont été détectés et intégrés dans cette activité. Ces objets n'ont pas été qualifiés comme une alerte ou une détection, ou ont déjà été nettoyés / supprimés.", + "mismatch_other": "{{count}} objets indisponibles ont été détectés et intégrés dans cette activité. Ces objets n'ont pas été qualifiés comme une alerte ou une détection, ou ont déjà été nettoyés / supprimés.", + "hasMissingObjects": "Ajustez votre configuration si vous souhaitez que Frigate enregistre les objets suivis pour les étiquettes suivantes : {{objects}}" + }, + "desc": "Détails de l'activité" + }, + "label": "Étiquette", + "editSubLabel": { + "title": "Modifier la sous-étiquette", + "desc": "Saisissez une nouvelle sous-étiquette pour {{label}}.", + "descNoLabel": "Saisissez une nouvelle sous-étiquette pour cet objet suivi." + }, + "topScore": { + "label": "Meilleur score", + "info": "Le meilleur score correspond au score médian le plus élevé de l'objet suivi, il peut donc différer du score affiché sur la miniature du résultat de recherche." + }, + "objects": "Objets", + "button": { + "regenerate": { + "label": "Générer à nouveau la description de l'objet suivi", + "title": "Générer à nouveau" + }, + "findSimilar": "Trouver des éléments similaires" + }, + "description": { + "label": "Description", + "placeholder": "Description de l'objet suivi", + "aiTips": "Frigate ne demandera pas de description à votre fournisseur d'IA générative tant que le cycle de vie de l'objet suivi ne sera pas terminé." + }, + "regenerateFromSnapshot": "Générer à nouveau à partir d'un instantané", + "regenerateFromThumbnails": "Générer à nouveau à partir des miniatures", + "editLPR": { + "title": "Modifier la plaque d'immatriculation", + "desc": "Saisissez une nouvelle valeur de plaque d'immatriculation pour {{label}}.", + "descNoLabel": "Saisissez une nouvelle valeur de plaque d'immatriculation pour cet objet suivi." + }, + "recognizedLicensePlate": "Plaque d'immatriculation reconnue", + "estimatedSpeed": "Vitesse estimée", + "zones": "Zones", + "expandRegenerationMenu": "Développer le menu de régénération", + "camera": "Caméra", + "tips": { + "descriptionSaved": "Description enregistrée avec succès", + "saveDescriptionFailed": "Échec de la mise à jour de la description : {{errorMessage}}" + }, + "snapshotScore": { + "label": "Score de l'instantané" + }, + "score": { + "label": "Score" + }, + "editAttributes": { + "title": "Modifier les attributs", + "desc": "Sélectionnez les attributs de classification pour : {{label}}" + }, + "attributes": "Attributs de classification", + "title": { + "label": "Titre" + }, + "scoreInfo": "Information score" + }, + "type": { + "details": "détails", + "video": "vidéo", + "object_lifecycle": "cycle de vie de l'objet", + "snapshot": "instantané", + "thumbnail": "Miniature", + "tracking_details": "Détails du suivi" + }, + "objectLifecycle": { + "title": "Cycle de vie de l'objet", + "noImageFound": "Aucune image trouvée pour cet horodatage", + "createObjectMask": "Créer un masque d'objet", + "scrollViewTips": "Faites défiler pour voir les moments clés du cycle de vie de cet objet.", + "adjustAnnotationSettings": "Ajuster les paramètres d'annotation", + "autoTrackingTips": "Les positions des cadres englobants seront imprécises pour les caméras à suivi automatique.", + "lifecycleItemDesc": { + "visible": "{{label}} détecté", + "entered_zone": "{{label}} est entré dans {{zones}}.", + "stationary": "{{label}} est devenu stationnaire.", + "attribute": { + "other": "{{label}} reconnu comme {{attribute}}", + "faceOrLicense_plate": "{{attribute}} détecté pour {{label}}" + }, + "gone": "{{label}} parti", + "heard": "{{label}} entendu", + "external": "{{label}} détecté", + "active": "{{label}} est devenu actif.", + "header": { + "zones": "Zones", + "area": "Aire", + "ratio": "Ratio" + } + }, + "annotationSettings": { + "title": "Paramètres d'annotation", + "showAllZones": { + "title": "Afficher toutes les zones", + "desc": "Afficher systématiquement les zones sur les images quand des objets y sont entrés" + }, + "offset": { + "label": "Décalage de l'annotation", + "documentation": "Lire la documentation ", + "desc": "Ces données, issues du flux de détection de votre caméra, sont incrustées dans les images du flux d'enregistrement. Cependant, une synchronisation parfaite entre ces deux flux est rarement garantie. Il est donc possible que le cadre englobant et la séquence ne soient pas parfaitement alignés. Pour corriger ce décalage, vous pouvez utiliser le champ annotation_offset.", + "millisecondsToOffset": "Décalage des annotations de détection en millisecondes. Par défaut : 0", + "tips": "Astuce : Pour mieux comprendre, visualisez un clip où une personne se déplace de gauche à droite. Si le cadre englobant affiché sur la ligne de temps de l'événement se trouve constamment à gauche de la personne, cela signifie que vous devriez réduire la valeur. À l'inverse, si ce même cadre englobant apparaît systématiquement en avance sur la personne qui marche de gauche à droite, alors vous devrez l'augmenter.", + "toast": { + "success": "Le décalage d'annotation pour {{camera}} a été enregistré dans le fichier de configuration. Redémarrez Frigate pour appliquer vos modifications." + } + } + }, + "carousel": { + "next": "Diapositive suivante", + "previous": "Diapositive précédente" + }, + "trackedPoint": "Point de suivi", + "count": "{{first}} de {{second}}" + }, + "trackedObjectDetails": "Détails de l'objet suivi", + "itemMenu": { + "downloadSnapshot": { + "label": "Télécharger l'instantané", + "aria": "Télécharger l'instantané" + }, + "findSimilar": { + "label": "Trouver des éléments similaires", + "aria": "Trouver des objets suivis similaires" + }, + "viewObjectLifecycle": { + "aria": "Afficher le cycle de vie de l'objet", + "label": "Visualiser le cycle de vie de l'objet" + }, + "viewInHistory": { + "label": "Afficher dans la chronologie", + "aria": "Afficher dans la chronologie" + }, + "downloadVideo": { + "label": "Télécharger la vidéo", + "aria": "Télécharger la vidéo" + }, + "submitToPlus": { + "label": "Soumettre à Frigate+", + "aria": "Soumettre à Frigate+" + }, + "deleteTrackedObject": { + "label": "Supprimer cet objet suivi" + }, + "addTrigger": { + "label": "Ajouter un déclencheur", + "aria": "Ajouter un déclencheur pour cet objet suivi" + }, + "audioTranscription": { + "label": "Transcrire", + "aria": "Demander une transcription audio" + }, + "showObjectDetails": { + "label": "Afficher le parcours de l'objet" + }, + "hideObjectDetails": { + "label": "Masquer le parcours de l'objet" + }, + "viewTrackingDetails": { + "label": "Voir les détails du suivi", + "aria": "Afficher les détails du suivi" + }, + "downloadCleanSnapshot": { + "label": "Télécharger l'instantané vierge", + "aria": "Télécharger l'instantané vierge" + }, + "debugReplay": { + "label": "Relecture de débogage", + "aria": "Visualiser cet objet suivi dans la vue de la session de relecture de déboggage" + }, + "more": { + "aria": "Plus" + } + }, + "dialog": { + "confirmDelete": { + "title": "Confirmer la suppression", + "desc": "La suppression de cet objet suivi supprime l'instantané, les embeddings enregistrés et les entrées du cycle de vie de l'objet associé. Les images enregistrées de cet objet suivi dans la vue Chronologie NE seront PAS supprimées.

    Êtes-vous sûr de vouloir continuer ?" + }, + "toast": { + "error": "Une erreur est survenue lors de la suppression de cet objet suivi : {{errorMessage}}" + } + }, + "noTrackedObjects": "Aucun objet suivi trouvé", + "fetchingTrackedObjectsFailed": "Erreur lors de la récupération des objets suivis : {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} objet suivi ", + "trackedObjectsCount_many": "{{count}} objets suivis ", + "trackedObjectsCount_other": "{{count}} objets suivis ", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "L'objet suivi a été supprimé avec succès.", + "error": "Échec de la suppression de l'objet suivi : {{errorMessage}}" + } + }, + "tooltip": "Correspondance : {{type}} à {{confidence}}%", + "previousTrackedObject": "Objet suivi précédent", + "nextTrackedObject": "Objet suivi suivant" + }, + "exploreMore": "Explorer plus d'objets {{label}}", + "aiAnalysis": { + "title": "Analyse IA" + }, + "concerns": { + "label": "Points de vigilance" + }, + "trackingDetails": { + "title": "Détails du suivi", + "noImageFound": "Aucune image trouvée pour cet horodatage", + "createObjectMask": "Créer un masque d'objet", + "adjustAnnotationSettings": "Ajuster les paramètres d'annotation", + "scrollViewTips": "Cliquez pour voir les moments significatifs du cycle de vie de cet objet.", + "autoTrackingTips": "Les positions des cadres de détection seront imprécises pour les caméras à suivi automatique.", + "count": "{{first}} sur {{second}}", + "trackedPoint": "Point suivi", + "lifecycleItemDesc": { + "visible": "{{label}} détecté", + "entered_zone": "{{label}} est entré(e) dans {{zones}}.", + "active": "{{label}} est devenu(e) actif(ve).", + "stationary": "{{label}} s'est immobilisé(e)", + "attribute": { + "faceOrLicense_plate": "Détection de {{attribute}} pour {{label}}", + "other": "{{label}} reconnu(e) comme {{attribute}}" + }, + "gone": "Sortie de {{label}}", + "heard": "{{label}} entendu(e)", + "external": "{{label}} détecté(e)", + "header": { + "zones": "Zones", + "ratio": "Ratio", + "area": "Surface", + "score": "Score", + "computedScore": "Score calculé", + "topScore": "Meilleur score", + "toggleAdvancedScores": "Afficher/masquer les scores avancés" + } + }, + "annotationSettings": { + "offset": { + "desc": "Ces données proviennent du flux de détection de votre caméra, mais elles sont superposées aux images du flux d'enregistrement. Il est peu probable que les deux flux soient parfaitement synchronisés. Par conséquent, le cadre de délimitation et la vidéo ne s'aligneront pas parfaitement. Vous pouvez utiliser ce paramètre pour décaler les annotations vers l'avant ou vers l'arrière dans le temps afin de mieux les aligner avec la vidéo enregistrée.", + "millisecondsToOffset": "Millisecondes de décalage pour les annotations de détection. Par défaut : 0", + "tips": "Diminuez la valeur si la lecture vidéo est en avance sur les cadres de détection et les points de tracé, et augmentez-la si la lecture vidéo est en retard sur ceux-ci. Cette valeur peut être négative.", + "toast": { + "success": "Le décalage des annotations pour {{camera}} a été sauvegardé dans le fichier de configuration." + }, + "label": "Décalage d'annotation" + }, + "title": "Paramètres d'annotation", + "showAllZones": { + "title": "Afficher toutes les zones", + "desc": "Toujours afficher les zones sur les images lorsqu'un objet pénètre une zone" + } + }, + "carousel": { + "previous": "Diapositive précédente", + "next": "Diapositive suivante" + } + } +} diff --git a/web/public/locales/fr/views/exports.json b/web/public/locales/fr/views/exports.json new file mode 100644 index 0000000..f182eb6 --- /dev/null +++ b/web/public/locales/fr/views/exports.json @@ -0,0 +1,44 @@ +{ + "documentTitle": "Exports - Frigate", + "search": "Rechercher", + "noExports": "Aucune exportation trouvée", + "deleteExport": { + "label": "Supprimer l'exportation" + }, + "deleteExport.desc": "Êtes-vous sûr de vouloir supprimer {{exportName}} ?", + "editExport": { + "title": "Renommer l'exportation", + "desc": "Saisissez un nouveau nom pour cette exportation.", + "saveExport": "Enregistrer l'exportation" + }, + "toast": { + "error": { + "renameExportFailed": "Échec du renommage de l'exportation : {{errorMessage}}", + "assignCaseFailed": "Échec de la mise à jour de l'affectation au dossier : {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Partager l'exportation", + "downloadVideo": "Télécharger la vidéo", + "editName": "Modifier le nom", + "deleteExport": "Supprimer l'exportation", + "assignToCase": "Ajouter à un dossier" + }, + "headings": { + "cases": "Dossiers", + "uncategorizedExports": "Exportations non classées" + }, + "caseDialog": { + "title": "Ajouter à un dossier", + "description": "Choisissez un dossier existant ou créez en un nouveau.", + "selectLabel": "Dossier", + "newCaseOption": "Créer un nouveau dossier", + "nameLabel": "Nom du dossier", + "descriptionLabel": "Description" + }, + "deleteCase": { + "desc": "Êtes-vous sûr de vouloir supprimer {{caseName}}?", + "descKeepExports": "Les exports seront disponibles comme exports non catégorisés.", + "deleteExports": "Supprimer aussi les exports" + } +} diff --git a/web/public/locales/fr/views/faceLibrary.json b/web/public/locales/fr/views/faceLibrary.json new file mode 100644 index 0000000..e61bfe9 --- /dev/null +++ b/web/public/locales/fr/views/faceLibrary.json @@ -0,0 +1,109 @@ +{ + "description": { + "addFace": "Ajoutez une nouvelle collection à la bibliothèque de visages en téléversant votre première image.", + "placeholder": "Saisissez un nom pour cette collection", + "invalidName": "Nom invalide. Les noms ne peuvent contenir que des lettres, des chiffres, des espaces, des apostrophes, des traits de soulignement et des tirets.", + "nameCannotContainHash": "Le nom ne peut pas contenir le caractère #." + }, + "details": { + "person": "Personne", + "confidence": "Confiance", + "face": "Détails du visage", + "timestamp": "Horodatage", + "faceDesc": "Détails de l'objet suivi qui a généré ce visage", + "subLabelScore": "Score de sous-libellé", + "scoreInfo": "Le score du sous-libellé correspond au score pondéré de tous les scores de confiance des visages reconnus. Il est donc possible qu'il ne corresponde pas au score affiché sur l'instantané.", + "unknown": "Inconnu" + }, + "documentTitle": "Bibliothèque de visages - Frigate", + "uploadFaceImage": { + "title": "Téléverser l'image du visage", + "desc": "Téléversez une image pour rechercher des visages et l'inclure dans {{pageToggle}}." + }, + "createFaceLibrary": { + "title": "Créer une collection", + "desc": "Créer une nouvelle collection", + "new": "Créer un nouveau visage", + "nextSteps": "Pour construire une base solide :
  • Utilisez l'onglet Reconnaissances récentes pour sélectionner et entraîner des images pour chaque personne détectée.
  • Privilégiez les images de face pour de meilleurs résultats et évitez d'entraîner le modèle avec des images où les visages sont de biais.
  • " + }, + "train": { + "title": "Reconnaissances récentes", + "aria": "Sélectionnez des reconnaissances récentes.", + "empty": "Il n'y a pas de tentatives récentes de reconnaissance faciale.", + "titleShort": "Récent" + }, + "selectFace": "Sélectionner un visage", + "button": { + "addFace": "Ajouter un visage", + "uploadImage": "Téléverser une image", + "deleteFaceAttempts": "Supprimer les visages", + "reprocessFace": "Retraiter le visage", + "renameFace": "Renommer le visage", + "deleteFace": "Supprimer le visage" + }, + "selectItem": "Sélectionner {{item}}", + "deleteFaceLibrary": { + "title": "Supprimer le nom", + "desc": "Êtes-vous certain de vouloir supprimer la collection {{name}} ? Cette action supprimera définitivement tous les visages associés." + }, + "imageEntry": { + "dropActive": "Déposez l'image ici.", + "dropInstructions": "Glissez-déposez ou collez une image ici, ou cliquez pour la sélectionner.", + "maxSize": "Taille max : {{size}}Mo", + "validation": { + "selectImage": "Veuillez sélectionner un fichier image." + } + }, + "readTheDocs": "Lire la documentation", + "toast": { + "success": { + "deletedName_one": "{{count}} visage a été supprimé avec succès.", + "deletedName_many": "{{count}} visages ont été supprimés avec succès.", + "deletedName_other": "{{count}} visages ont été supprimés avec succès.", + "uploadedImage": "Image téléversée avec succès", + "addFaceLibrary": "{{name}} a été ajouté avec succès à la bibliothèque de visages !", + "updatedFaceScore": "Score du visage ({{score}}) de {{name}} mis à jour avec succès", + "deletedFace_one": "{{count}} visage supprimé avec succès", + "deletedFace_many": "{{count}} visages supprimés avec succès", + "deletedFace_other": "{{count}} visages supprimés avec succès", + "trainedFace": "Visage entraîné avec succès", + "renamedFace": "Visage renommé avec succès en {{name}}", + "reclassifiedFace": "Visage reclassifié avec succès." + }, + "error": { + "uploadingImageFailed": "Échec du téléversement de l'image : {{errorMessage}}", + "deleteFaceFailed": "Échec de la suppression : {{errorMessage}}", + "trainFailed": "Échec de l'entraînement : {{errorMessage}}", + "updateFaceScoreFailed": "Échec de la mise à jour du score du visage : {{errorMessage}}", + "addFaceLibraryFailed": "Échec de l'attribution du nom au visage : {{errorMessage}}", + "deleteNameFailed": "Échec de la suppression du nom : {{errorMessage}}", + "renameFaceFailed": "Échec du changement de nom du visage : {{errorMessage}}", + "reclassifyFailed": "Échec de la reclassification du visage : {{errorMessage}}" + } + }, + "trainFaceAs": "Entraîner le visage comme :", + "trainFace": "Entraîner le visage", + "steps": { + "uploadFace": "Téléverser une image de visage", + "faceName": "Saisissez le nom du visage.", + "nextSteps": "Prochaines étapes", + "description": { + "uploadFace": "Téléversez une image de {{name}} qui montre son visage de face. L'image n'a pas besoin d'être recadrée pour ne montrer que son visage." + } + }, + "renameFace": { + "title": "Renommer le visage", + "desc": "Saisissez un nouveau nom pour {{name}}." + }, + "collections": "Collections", + "deleteFaceAttempts": { + "title": "Supprimer les visages", + "desc_one": "Êtes-vous sûr de vouloir supprimer {{count}} visage ? Cette action est irréversible.", + "desc_many": "Êtes-vous sûr de vouloir supprimer {{count}} visages ? Cette action est irréversible.", + "desc_other": "Êtes-vous sûr de vouloir supprimer {{count}} visages ? Cette action est irréversible." + }, + "nofaces": "Aucun visage disponible", + "pixels": "{{area}} pixels", + "reclassifyFaceAs": "Reclassifier le visage en :", + "reclassifyFace": "Reclassifier le visage" +} diff --git a/web/public/locales/fr/views/live.json b/web/public/locales/fr/views/live.json new file mode 100644 index 0000000..fc9d6f3 --- /dev/null +++ b/web/public/locales/fr/views/live.json @@ -0,0 +1,202 @@ +{ + "documentTitle": { + "default": "Direct - Frigate" + }, + "lowBandwidthMode": "Mode bande passante faible", + "documentTitle.withCamera": "{{camera}} - Direct - Frigate", + "twoWayTalk": { + "disable": "Désactiver la conversation bidirectionnelle", + "enable": "Activer la conversation bidirectionnelle" + }, + "cameraAudio": { + "disable": "Désactiver le son de la caméra", + "enable": "Activer le son de la caméra" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Cliquez dans le cadre pour centrer la caméra", + "enable": "Activer le clic pour déplacer", + "disable": "Désactiver le clic pour déplacer", + "enableWithZoom": "Activer le clic pour déplacer / faire glisser vers le zoom" + }, + "left": { + "label": "Déplacer la caméra PTZ vers la gauche" + }, + "up": { + "label": "Déplacer la caméra PTZ vers le haut" + }, + "right": { + "label": "Déplacer la caméra PTZ vers la droite" + }, + "down": { + "label": "Déplacer la caméra PTZ vers le bas" + } + }, + "zoom": { + "in": { + "label": "Zoom avant sur la caméra PTZ" + }, + "out": { + "label": "Zoom arrière sur la caméra PTZ" + } + }, + "frame": { + "center": { + "label": "Cliquez dans le cadre pour centrer la caméra PTZ." + } + }, + "presets": "Préréglages de la caméra PTZ", + "focus": { + "in": { + "label": "Mise au point rapprochée de la caméra PTZ" + }, + "out": { + "label": "Mise au point éloignée de la caméra PTZ" + } + } + }, + "camera": { + "enable": "Activer la caméra", + "disable": "Désactiver la caméra" + }, + "detect": { + "enable": "Activer la détection", + "disable": "Désactiver la détection" + }, + "recording": { + "enable": "Activer l'enregistrement", + "disable": "Désactiver l'enregistrement" + }, + "snapshots": { + "enable": "Activer les instantanés", + "disable": "Désactiver les instantanés" + }, + "muteCameras": { + "enable": "Couper le son de toutes les caméras", + "disable": "Activer le son de toutes les caméras" + }, + "audioDetect": { + "enable": "Activer la détection audio", + "disable": "Désactiver la détection audio" + }, + "manualRecording": { + "playInBackground": { + "label": "Jouer en arrière-plan", + "desc": "Activez cette option pour continuer à diffuser lorsque le lecteur est masqué." + }, + "showStats": { + "label": "Afficher les statistiques", + "desc": "Activez cette option pour afficher les statistiques de flux en surimpression sur le flux de la caméra." + }, + "debugView": "Vue de débogage", + "start": "Démarrer l'enregistrement à la demande", + "failedToStart": "Échec du démarrage de l'enregistrement manuel à la demande", + "end": "Terminer l'enregistrement à la demande", + "ended": "Enregistrement manuel à la demande terminé", + "failedToEnd": "Impossible de terminer l'enregistrement manuel à la demande", + "started": "Enregistrement manuel à la demande démarré", + "recordDisabledTips": "Puisque l'enregistrement est désactivé ou restreint dans la configuration de cette caméra, seul un instantané sera enregistré.", + "title": "À la demande", + "tips": "Téléchargez un instantané ou démarrez un événement manuel en fonction des paramètres de conservation des enregistrements de cette caméra." + }, + "streamingSettings": "Paramètres de diffusion", + "notifications": "Notifications", + "suspend": { + "forTime": "Mettre en pause pour : " + }, + "stream": { + "audio": { + "available": "L'audio est disponible pour ce flux", + "tips": { + "documentation": "Lire la documentation ", + "title": "L'audio doit provenir de votre caméra et être configuré dans go2rtc pour ce flux." + }, + "unavailable": "L'audio n'est pas disponible pour ce flux" + }, + "twoWayTalk": { + "tips": "Votre appareil doit prendre en charge cette fonctionnalité et WebRTC doit être configuré pour la conversation bidirectionnelle.", + "tips.documentation": "Lire la documention ", + "available": "Conversation bidirectionnelle disponible pour ce flux", + "unavailable": "Conversation bidirectionnelle non disponible pour ce flux" + }, + "lowBandwidth": { + "tips": "La vue temps réel est en mode bande passante faible à cause de problèmes de mise en mémoire tampon ou d'erreurs de flux.", + "resetStream": "Réinitialiser le flux" + }, + "playInBackground": { + "tips": "Activez cette option pour continuer la diffusion lorsque le lecteur est masqué.", + "label": "Jouer en arrière-plan" + }, + "title": "Flux", + "debug": { + "picker": "La sélection de flux est indisponible en mode débogage. La vue de débogage utilise systématiquement le flux attribué au rôle de détection." + } + }, + "cameraSettings": { + "objectDetection": "Détection d'objets", + "recording": "Enregistrement", + "snapshots": "Instantanés", + "audioDetection": "Détection audio", + "autotracking": "Suivi automatique", + "cameraEnabled": "Caméra activée", + "title": "Paramètres de {{camera}}", + "transcription": "Transcription audio" + }, + "history": { + "label": "Afficher les vidéos archivées" + }, + "effectiveRetainMode": { + "modes": { + "all": "Tous", + "motion": "Mouvement", + "active_objects": "Objets actifs" + }, + "notAllTips": "Votre configuration de conservation d'enregistrement {{source}} est définie sur mode : {{effectiveRetainMode}}, donc cet enregistrement à la demande ne conservera que les segments avec {{effectiveRetainModeName}}." + }, + "audio": "Audio", + "autotracking": { + "enable": "Activer le suivi automatique", + "disable": "Désactiver le suivi automatique" + }, + "streamStats": { + "enable": "Afficher les statistiques du flux", + "disable": "Masquer les statistiques du flux" + }, + "editLayout": { + "label": "Modifier la mise en page", + "group": { + "label": "Modifier le groupe de caméras" + }, + "exitEdit": "Quitter le mode édition" + }, + "transcription": { + "enable": "Activer la transcription audio en direct", + "disable": "Désactiver la transcription audio en direct" + }, + "noCameras": { + "title": "Aucune caméra n'est configurée", + "description": "Pour commencer, connectez une caméra à Frigate.", + "buttonText": "Ajouter une caméra", + "restricted": { + "title": "Aucune caméra disponible", + "description": "Vous n'avez pas la permission de visionner les caméras de ce groupe." + }, + "default": { + "title": "Aucune caméra configurée", + "description": "Pour commencer, connectez une caméra à Frigate.", + "buttonText": "Ajouter une caméra" + }, + "group": { + "title": "Aucune caméra dans le groupe", + "description": "Ce groupe de caméras ne contient aucune caméra assignée ou activée.", + "buttonText": "Gérer les groupes" + } + }, + "snapshot": { + "takeSnapshot": "Télécharger un instantané", + "noVideoSource": "Aucune source vidéo disponible pour l'instantané", + "captureFailed": "Échec de la capture de l'instantané", + "downloadStarted": "Téléchargement de l'instantané démarré" + } +} diff --git a/web/public/locales/fr/views/motionSearch.json b/web/public/locales/fr/views/motionSearch.json new file mode 100644 index 0000000..0c99cad --- /dev/null +++ b/web/public/locales/fr/views/motionSearch.json @@ -0,0 +1,51 @@ +{ + "documentTitle": "Recherche de mouvement - Frigate", + "title": "Recherche de mouvement", + "description": "Dessinez un polygone pour définir la zonecible, et spécifiez une fourchette de temps pour chercher les mouvements dans cette zone.", + "selectCamera": "Recherche de mouvement en cours de chargement", + "startSearch": "Commencer la recherche", + "searchStarted": "Recherche démarrée", + "searchCancelled": "Recherche annulée", + "cancelSearch": "Annuler recherche", + "searching": "Recherche en cours.", + "searchComplete": "Recherche terminée", + "noResultsYet": "Lancez une recherche pour trouver les changements de mouvement dans la zone sélectionnée", + "noChangesFound": "Aucun changement de pixel détecté dans la zone sélectionnée", + "changesFound_one": "{{count}} changement de mouvement détecté", + "changesFound_many": "{{count}} changements de mouvement détectés", + "changesFound_other": "{{count}} changements de mouvement détectés", + "framesProcessed": "{{count}} images traitées", + "jumpToTime": "Aller à ce moment", + "results": "Résultats", + "showSegmentHeatmap": "Carte thermique", + "newSearch": "Nouvelle recherche", + "clearResults": "Effacer les résultats", + "clearROI": "Effacer le polygone", + "polygonControls": { + "points_one": "{{count}} point", + "points_many": "{{count}} points", + "points_other": "{{count}} points", + "undo": "Annuler le dernier point", + "reset": "Réinitialiser le polygone", + "drawMode": "Dessiner", + "moveMode": "Déplacer" + }, + "motionHeatmapLabel": "Carte thermique des mouvements", + "dialog": { + "title": "Recherche de mouvement", + "cameraLabel": "Caméra", + "previewAlt": "Aperçu de la caméra {{camera}}" + }, + "timeRange": { + "title": "Plage de recherche", + "start": "Début de la plage de recherche", + "end": "Fin de la plage de recherche" + }, + "settings": { + "title": "Paramètres de recherche", + "parallelMode": "Mode parallèle", + "parallelModeDesc": "Scanner plusieurs plages d'enregistrement simultanément (plus rapide ; nécessite plus de ressources de décodage)", + "threshold": "Seuil de sensitivité", + "thresholdDesc": "Des valeurs basses détectent de plus petits changements (1-255)" + } +} diff --git a/web/public/locales/fr/views/recording.json b/web/public/locales/fr/views/recording.json new file mode 100644 index 0000000..e1960a7 --- /dev/null +++ b/web/public/locales/fr/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Exports", + "calendar": "Calendrier", + "filter": "Filtre", + "filters": "Filtres", + "toast": { + "error": { + "noValidTimeSelected": "Aucune plage horaire valide sélectionnée", + "endTimeMustAfterStartTime": "L'heure de fin doit être postérieure à l'heure de début." + } + } +} diff --git a/web/public/locales/fr/views/replay.json b/web/public/locales/fr/views/replay.json new file mode 100644 index 0000000..abafbe7 --- /dev/null +++ b/web/public/locales/fr/views/replay.json @@ -0,0 +1,33 @@ +{ + "title": "Debug - Rejouer", + "description": "Rejouer les enregistrement de la camera, à but de débogage. La liste d'objets montre un résumé avec retard des objets détectés; et l'onglet Messages montre le flux des messages internes à Frigate liés à la vidéo rejouée.", + "websocket_messages": "Messages", + "dialog": { + "title": "Démarrer le Rejeu-Debogage", + "timeRange": "Intervalle", + "preset": { + "1m": "Dernière minute", + "5m": "5 dernières minutes", + "timeline": "Depuis la chronologie", + "custom": "Personnalisé" + }, + "startButton": "Démarrer le revisionnage", + "selectFromTimeline": "Sélectionner", + "starting": "Démarrage du revisionnage...", + "startLabel": "Démarrer", + "endLabel": "Fin", + "toast": { + "error": "Echec du démarrage du revisionnage de déboggage : {{error}}", + "alreadyActive": "Une session de revisionnage est déjà active", + "stopError": "Echec de l'arrêt du revisionnage de déboggage : {{error}}", + "goToReplay": "Vers le revisionnage" + } + }, + "page": { + "noSession": "Aucune session de revisionnage de déboggage active", + "noSessionDesc": "Démarrer un revisionnage de déboggage depuis l'Historique en cliquant sur le boutons Actions dans la barre d'outils et choisir Revisionnage de déboggage.", + "goToRecordings": "Vers l'historique", + "preparingClip": "Préparation du clip…", + "preparingClipDesc": "Frigate est encore en train de recoller les enregistrements pour l'intervalle de temps sélectionnée. Cela peut prendre une minute pour les plus longues intervalles." + } +} diff --git a/web/public/locales/fr/views/search.json b/web/public/locales/fr/views/search.json new file mode 100644 index 0000000..a9938bf --- /dev/null +++ b/web/public/locales/fr/views/search.json @@ -0,0 +1,75 @@ +{ + "savedSearches": "Recherches enregistrées", + "search": "Rechercher", + "searchFor": "Rechercher {{inputValue}}", + "button": { + "clear": "Effacer la recherche", + "filterInformation": "Filtrer les informations", + "filterActive": "Filtres actifs", + "save": "Enregistrer la recherche", + "delete": "Supprimer la recherche enregistrée" + }, + "trackedObjectId": "ID d'objet suivi", + "filter": { + "label": { + "zones": "Zones", + "sub_labels": "Sous-étiquettes", + "search_type": "Type de recherche", + "time_range": "Plage horaire", + "labels": "Étiquettes", + "cameras": "Caméras", + "after": "Après", + "before": "Avant", + "min_speed": "Vitesse minimale", + "max_speed": "Vitesse maximale", + "min_score": "Score minimum", + "recognized_license_plate": "Plaque d'immatriculation reconnue", + "has_clip": "Avec une séquence vidéo", + "has_snapshot": "Avec un instantané", + "max_score": "Score maximum", + "attributes": "Attributs" + }, + "searchType": { + "thumbnail": "Miniature", + "description": "Description" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "La date « avant » doit être postérieure à la date « après ».", + "afterDatebeEarlierBefore": "La date « après » doit être antérieure à la date « avant ».", + "minScoreMustBeLessOrEqualMaxScore": "Le « min_score » doit être inférieur ou égal au « max_score ».", + "maxScoreMustBeGreaterOrEqualMinScore": "Le « max_score » doit être supérieur ou égal au « min_score ».", + "minSpeedMustBeLessOrEqualMaxSpeed": "La vitesse minimale doit être inférieure ou égale à la vitesse maximale.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "La « vitesse maximale » doit être supérieure ou égale à la « vitesse minimale »." + } + }, + "header": { + "currentFilterType": "Valeurs du filtre", + "activeFilters": "Filtres actifs", + "noFilters": "Filtres" + }, + "tips": { + "title": "Comment utiliser les filtres de texte", + "desc": { + "text": "Les filtres vous aident à affiner vos résultats de recherche. Voici comment les utiliser dans le champ de saisie :", + "example": "Exemple: cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "step": "
    • Saisissez un nom de filtre suivi de deux points (par exemple, «cameras:»).
    • Sélectionnez une valeur parmi les suggestions ou saisissez la vôtre.
    • Utilisez plusieurs filtres en les ajoutant les uns après les autres, en laissant un espace entre eux.
    • Les filtres de date (avant: et après:) utilisent le format {{DateFormat}}.
    • Le filtre de plage horaire utilise le format {{exampleTime}}.
    • Supprimez les filtres en cliquant sur le «x» à côté d'eux.
    ", + "step1": "Saisissez un nom de clé de filtre suivi de deux points (par exemple, \"cameras:\").", + "step2": "Sélectionnez une valeur parmi les suggestions ou saisissez la vôtre.", + "step3": "Utilisez plusieurs filtres en les ajoutant les uns après les autres avec un espace entre eux.", + "step5": "Le filtre de plage horaire utilise le format {{exampleTime}}.", + "step6": "Supprimez les filtres en cliquant sur le \"x\" à côté d'eux.", + "step4": "Les filtres de dates (avant : et après :) utilisent le format {{DateFormat}}.", + "exampleLabel": "Exemple :" + } + } + }, + "similaritySearch": { + "title": "Recherche par similarité", + "active": "Recherche par similarité activée", + "clear": "Effacer la recherche par similarité" + }, + "placeholder": { + "search": "Rechercher…" + } +} diff --git a/web/public/locales/fr/views/settings.json b/web/public/locales/fr/views/settings.json new file mode 100644 index 0000000..d6891dc --- /dev/null +++ b/web/public/locales/fr/views/settings.json @@ -0,0 +1,1609 @@ +{ + "documentTitle": { + "default": "Paramètres - Frigate", + "authentication": "Paramètres d'authentification - Frigate", + "camera": "Paramètres des caméras - Frigate", + "classification": "Paramètres de classification - Frigate", + "motionTuner": "Réglage de la détection de mouvement - Frigate", + "general": "Paramètres de l'interface - Frigate", + "masksAndZones": "Éditeur de masques et de zones - Frigate", + "object": "Débogage - Frigate", + "frigatePlus": "Paramètres Frigate+ - Frigate", + "notifications": "Paramètres de notification - Frigate", + "enrichments": "Paramètres d'enrichissements - Frigate", + "cameraManagement": "Gestion des caméras - Frigate", + "cameraReview": "Paramètres des activités caméra - Frigate", + "globalConfig": "Configuration globale - Frigate", + "cameraConfig": "Configuration de la caméra - Frigate", + "maintenance": "Maintenance - Frigate", + "profiles": "Profils - Frigate" + }, + "menu": { + "ui": "Interface utilisateur", + "classification": "Classification", + "masksAndZones": "Masques / Zones", + "motionTuner": "Ajusteur de la détection de mouvement", + "debug": "Débogage", + "cameras": "Paramètres des caméras", + "users": "Utilisateurs", + "notifications": "Notifications", + "frigateplus": "Frigate+", + "enrichments": "Enrichissements", + "triggers": "Déclencheurs", + "roles": "Rôles", + "cameraManagement": "Gestion", + "cameraReview": "Activités", + "general": "Général", + "globalConfig": "Configuration globale", + "system": "Système", + "integrations": "Intégrations", + "profileSettings": "Paramètres du profil", + "globalDetect": "Détection d'objets", + "globalRecording": "Enregistrement", + "globalSnapshots": "Instantanés", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Détection de mouvement", + "globalObjects": "Objets", + "globalReview": "Activités", + "globalAudioEvents": "Événements audio", + "globalLivePlayback": "Lecture en direct", + "globalTimestampStyle": "Format d'horodatage", + "systemDatabase": "Base de données", + "systemTls": "TLS", + "systemAuthentication": "Authentification", + "systemNetworking": "Réseau", + "systemProxy": "Proxy", + "systemUi": "Interface", + "systemLogging": "Journalisation", + "systemEnvironmentVariables": "Variables d'environnement", + "systemTelemetry": "Télémétrie", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "Matériel de détection", + "systemDetectionModel": "Modèle de détection", + "systemMqtt": "MQTT", + "integrationSemanticSearch": "Recherche sémantique", + "integrationGenerativeAi": "IA générative", + "integrationFaceRecognition": "Reconnaissance faciale", + "integrationLpr": "Lecture de plaques d'immatriculation", + "integrationObjectClassification": "Classification d'objets", + "integrationAudioTranscription": "Transcription audio", + "cameraDetect": "Détection d'objets", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "Enregistrement", + "cameraSnapshots": "Instantanés", + "cameraMotion": "Détection de mouvement", + "cameraObjects": "Objets", + "cameraConfigReview": "Activités", + "cameraAudioEvents": "Évènements audio", + "cameraAudioTranscription": "Transcription audio", + "cameraNotifications": "Notifications", + "cameraLivePlayback": "Lecture en direct", + "cameraBirdseye": "Birdseye", + "cameraFaceRecognition": "Reconnaissance faciale", + "cameraLpr": "Lecture de plaques d'immatriculation", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "Interface de la caméra", + "cameraTimestampStyle": "Style d'horodatage", + "cameraMqtt": "MQTT de la caméra", + "maintenance": "Maintenance", + "uiSettings": "Paramètres IU", + "profiles": "Profils", + "systemGo2rtcStreams": "Streams go2rtc" + }, + "dialog": { + "unsavedChanges": { + "title": "Vous avez des modifications non enregistrées.", + "desc": "Voulez-vous enregistrer vos modifications avant de continuer ?" + } + }, + "cameraSetting": { + "camera": "Caméra", + "noCamera": "Aucune caméra" + }, + "general": { + "title": "Paramètres d'interface", + "liveDashboard": { + "title": "Tableau de bord en direct", + "automaticLiveView": { + "label": "Vue en direct automatique", + "desc": "Basculez automatiquement vers la vue en direct d'une caméra lorsqu'une activité est détectée. La désactivation de cette option limite la mise à jour des images statiques de la caméra sur le tableau de bord en direct à une fois par minute seulement." + }, + "playAlertVideos": { + "label": "Lire les vidéos d'alerte", + "desc": "Par défaut, les alertes récentes du tableau de bord en direct sont diffusées sous forme de petites vidéos en boucle. Désactivez cette option pour afficher uniquement une image statique des alertes récentes sur cet appareil/navigateur." + }, + "displayCameraNames": { + "label": "Toujours afficher les noms des caméras", + "desc": "Toujours afficher les noms des caméras dans une puce sur le tableau de bord de la vue en direct multi-caméras" + }, + "liveFallbackTimeout": { + "label": "Délai d'attente avant repli (Lecteur en direct)", + "desc": "Lorsque le flux en direct haute qualité d'une caméra est indisponible, le lecteur bascule en mode faible bande passante après ce nombre de secondes. Par défaut : 3." + } + }, + "storedLayouts": { + "title": "Mises en page stockées", + "desc": "La disposition des caméras d'un groupe peut être déplacée/redimensionnée. Les positions sont enregistrées dans le stockage local de votre navigateur.", + "clearAll": "Effacer toutes les mises en page" + }, + "cameraGroupStreaming": { + "title": "Paramètres de diffusion du groupe de caméras", + "desc": "Les paramètres de diffusion en continu pour chaque groupe de caméras sont stockés dans le stockage local de votre navigateur.", + "clearAll": "Effacer tous les paramètres de diffusion" + }, + "recordingsViewer": { + "title": "Visionneuse d'enregistrements", + "defaultPlaybackRate": { + "label": "Vitesse de lecture par défaut", + "desc": "Vitesse de lecture par défaut pour la lecture des enregistrements" + } + }, + "calendar": { + "firstWeekday": { + "label": "Premier jour de la semaine", + "desc": "Jour du début de la semaine du calendrier des activités", + "sunday": "Dimanche", + "monday": "Lundi" + }, + "title": "Calendrier" + }, + "toast": { + "error": { + "clearStoredLayoutFailed": "Échec de l'effacement de la mise en page enregistrée : {{errorMessage}}", + "clearStreamingSettingsFailed": "Échec de l'effacement des paramètres de diffusion : {{errorMessage}}" + }, + "success": { + "clearStreamingSettings": "Paramètres de diffusion effacés pour tous les groupes de caméras.", + "clearStoredLayout": "Mise en page enregistrée effacée pour {{cameraName}}" + } + } + }, + "notification": { + "suspendTime": { + "untilRestart": "Suspendre jusqu'au redémarrage", + "24hours": "Suspendre pendant 24 heures", + "10minutes": "Suspendre pendant 10 minutes", + "12hours": "Suspendre pendant 12 heures", + "5minutes": "Suspendre pendant 5 minutes", + "1hour": "Suspendre pendant 1 heure", + "30minutes": "Suspendre pendant 30 minutes", + "suspend": "Suspendre" + }, + "toast": { + "success": { + "registered": "Inscription réussie aux notifications. Le redémarrage de Frigate est nécessaire avant l'envoi de toute notification (y compris une notification de test).", + "settingSaved": "Les paramètres de notification ont été enregistrés." + }, + "error": { + "registerFailed": "Impossible de sauvegarder l'enregistrement de la notification." + } + }, + "cancelSuspension": "Annuler la suspension", + "notificationSettings": { + "title": "Paramètres de notification", + "documentation": "Lire la documentation", + "desc": "Frigate peut envoyer nativement des notifications push à votre appareil lorsqu'il est exécuté dans le navigateur ou installé en tant que PWA." + }, + "notificationUnavailable": { + "title": "Notifications indisponibles", + "documentation": "Lire la documentation", + "desc": "Les notifications push Web nécessitent un contexte sécurisé (https://…). Il s'agit d'une limitation du navigateur. Accédez à Frigate en toute sécurité pour utiliser les notifications." + }, + "globalSettings": { + "title": "Paramètres globaux", + "desc": "Suspendre temporairement les notifications pour des caméras spécifiques sur tous les appareils enregistrés." + }, + "email": { + "title": "Email", + "desc": "Une adresse e-mail valide est requise et sera utilisée pour vous avertir en cas de problème avec le service push.", + "placeholder": "par ex. exemple@email.com" + }, + "cameras": { + "title": "Caméras", + "noCameras": "Aucune caméra n'est disponible", + "desc": "Sélectionnez les caméras pour lesquelles activer les notifications." + }, + "deviceSpecific": "Paramètres spécifiques de l'appareil", + "suspended": "Notifications suspendues {{time}}", + "title": "Notifications", + "active": "Notifications actives", + "registerDevice": "Enregistrer cet appareil", + "unregisterDevice": "Désenregistrer cet appareil", + "sendTestNotification": "Envoyer une notification de test", + "unsavedChanges": "Modifications des notifications non enregistrées", + "unsavedRegistrations": "Enregistrements des notifications non enregistrés" + }, + "frigatePlus": { + "apiKey": { + "notValidated": "La clé API Frigate+ n'est pas détectée ou n'est pas validée.", + "title": "Clé API Frigate+", + "validated": "La clé API Frigate+ est détectée et validée", + "desc": "La clé API Frigate+ permet l'intégration avec le service Frigate+.", + "plusLink": "En savoir plus sur Frigate+" + }, + "title": "Paramètres Frigate+", + "snapshotConfig": { + "documentation": "Lire la documentation", + "desc": "La soumission à Frigate+ nécessite que les instantanés soient activés dans votre configuration.", + "title": "Configuration des instantanés", + "table": { + "snapshots": "Instantanés", + "camera": "Caméra", + "cleanCopySnapshots": "Instantanés clean_copy" + }, + "cleanCopyWarning": "Certaines caméras ont les instantanés désactivés" + }, + "modelInfo": { + "baseModel": "Modèle de base", + "modelType": "Type de modèle", + "cameras": "Caméras", + "supportedDetectors": "Détecteurs pris en charge", + "loading": "Chargement des informations du modèle…", + "title": "Informations sur le modèle", + "trainDate": "Date d'entraînement", + "error": "Échec du chargement des informations du modèle", + "availableModels": "Modèles disponibles", + "dimensions": "Dimensions", + "loadingAvailableModels": "Chargement des modèles disponibles…", + "modelSelect": "Vous pouvez sélectionner ici vos modèles disponibles dans Frigate+. Notez que seuls les modèles compatibles avec votre configuration de détecteur actuelle peuvent être sélectionnés.", + "plusModelType": { + "baseModel": "Modèle de base", + "userModel": "Optimisé" + } + }, + "toast": { + "success": "Les paramètres de Frigate+ ont été enregistrés. Redémarrez Frigate pour appliquer les modifications.", + "error": "Échec de l'enregistrement des modifications de configuration : {{errorMessage}}" + }, + "restart_required": "Redémarrage requis (modèle Frigate+ changé)", + "unsavedChanges": "Modifications de paramètres de Frigate+ non enregistrées" + }, + "classification": { + "title": "Paramètres de classification", + "semanticSearch": { + "title": "Recherche sémantique", + "reindexNow": { + "label": "Réindexer maintenant", + "confirmTitle": "Confirmer la réindexation", + "error": "Échec du démarrage de la réindexation : {{errorMessage}}", + "desc": "La réindexation génère à nouveau les plongements vectoriels pour tous les objets suivis. Ce processus s'exécute en arrière-plan et peut saturer votre processeur et prendre un certain temps, selon le nombre d'objets suivis.", + "confirmDesc": "Êtes-vous sûr de vouloir réindexer tous les plongements vectoriels d'objets suivis ? Ce processus s'exécutera en arrière-plan, mais il risque de saturer votre processeur et de prendre un certain temps. Vous pouvez suivre la progression sur la page Explorer.", + "success": "La réindexation a démarré avec succès.", + "alreadyInProgress": "La réindexation est déjà en cours.", + "confirmButton": "Réindexer" + }, + "desc": "La recherche sémantique dans Frigate vous permet de trouver des objets suivis dans vos éléments de revue en utilisant soit l'image elle-même, soit une description textuelle définie par l'utilisateur, soit une description générée automatiquement.", + "modelSize": { + "small": { + "desc": "L'utilisation de petit utilise une version quantifiée du modèle qui utilise moins de mémoire et s'exécute plus rapidement sur le processeur avec une différence très négligeable dans la qualité d'intégration.", + "title": "petit" + }, + "large": { + "desc": "L'utilisation de grand utilise le modèle Jina complet et s'exécutera automatiquement sur la carte graphique si applicable.", + "title": "grand" + }, + "desc": "Taille du modèle utilisé pour les plongements vectoriels de recherche sémantique.", + "label": "Taille du modèle" + }, + "readTheDocumentation": "Lire la documentation" + }, + "faceRecognition": { + "readTheDocumentation": "Lire la documentation", + "modelSize": { + "large": { + "title": "grand", + "desc": "L'utilisation de grand utilise un modèle d'intégration de visage ArcFace et s'exécutera automatiquement sur la carte graphique le cas échéant." + }, + "small": { + "desc": "L'utilisation de petit utilise un modèle d'intégration de visage FaceNet qui fonctionne efficacement sur la plupart des processeurs.", + "title": "petit" + }, + "label": "Taille du modèle", + "desc": "La taille du modèle utilisé pour la reconnaissance faciale." + }, + "desc": "La reconnaissance faciale permet d'attribuer un nom aux personnes. Une fois leur visage reconnu, Frigate attribuera le nom de la personne comme sous-étiquette. Ces informations sont incluses dans l'interface utilisateur, les filtres et les notifications.", + "title": "Reconnaissance faciale" + }, + "licensePlateRecognition": { + "desc": "Frigate peut reconnaître les plaques d'immatriculation des véhicules et ajouter automatiquement les caractères détectés au champ recognized_license_plate, ou un nom connu comme sous-étiquette aux objets de type voiture. Un cas d'utilisation courant est la lecture des plaques d'immatriculation des voitures entrant dans une allée ou circulant dans la rue.", + "readTheDocumentation": "Lire la documentation", + "title": "Reconnaissance de plaque d'immatriculation" + }, + "toast": { + "success": "Les paramètres de classification ont été enregistrés. Redémarrez Frigate pour appliquer vos modifications.", + "error": "Échec de l'enregistrement des modifications de configuration : {{errorMessage}}" + }, + "birdClassification": { + "title": "Classification des oiseaux", + "desc": "La classification des oiseaux identifie les oiseaux connus à l'aide d'un modèle Tensorflow quantifié. Lorsqu'un oiseau connu est reconnu, son nom commun sera ajouté en tant que sous-étiquette. Cette information est incluse dans l'interface utilisateur, les filtres, ainsi que dans les notifications." + }, + "restart_required": "Redémarrage requis (paramètres de classification changés)", + "unsavedChanges": "Modifications des paramètres de classification non enregistrées" + }, + "camera": { + "title": "Paramètres de la caméra", + "review": { + "title": "Revue d'événements", + "detections": "Détections ", + "alerts": "Alertes ", + "desc": "Activer/désactiver temporairement les alertes et les détections pour cette caméra jusqu'au redémarrage de Frigate. Si cette option est désactivée, aucun nouvel élément ne sera généré dans la revue d'événements. " + }, + "reviewClassification": { + "title": "Catégorisation de la revue d'évènements", + "objectDetectionsTips": "Tous les objets {{detectionsLabels}} non classés sur {{cameraName}} seront affichés comme des détections, quelle que soit la zone dans laquelle ils se trouvent.", + "zoneObjectDetectionsTips": { + "text": "Tous les objets {{detectionsLabels}} non classés dans {{zone}} sur {{cameraName}} seront affichés comme des détections.", + "regardlessOfZoneObjectDetectionsTips": "Tous les objets {{detectionsLabels}} non classés sur {{cameraName}} seront affichés comme des détections, quelle que soit la zone dans laquelle ils se trouvent.", + "notSelectDetections": "Tous les objets {{detectionsLabels}} détectés dans {{zone}} sur {{cameraName}} non classés comme des alertes seront affichés comme des détections, quelle que soit la zone dans laquelle ils se trouvent." + }, + "selectDetectionsZones": "Sélectionner les zones pour les détections", + "toast": { + "success": "La configuration de la classification de la revue d'événements a été enregistrée. Redémarrez Frigate pour appliquer les modifications." + }, + "readTheDocumentation": "Lire la documentation", + "objectAlertsTips": "Tous les objets {{alertsLabels}} sur {{cameraName}} seront affichés sous forme d'alertes.", + "limitDetections": "Limiter les détections à des zones spécifiques", + "zoneObjectAlertsTips": "Tous les objets {{alertsLabels}} détectés dans {{zone}} sur {{cameraName}} seront affichés sous forme d'alertes.", + "noDefinedZones": "Aucune zone n'est définie pour cette caméra.", + "selectAlertsZones": "Sélectionner les zones pour les alertes", + "desc": "Frigate classe les éléments de la revue d'événements en alertes et détections. Par défaut, toutes les détections de personnes et de voitures sont qualifiées d'alertes. Vous avez la possibilité d'affiner cette catégorisation en configurant des zones spécifiques pour ces éléments.", + "unsavedChanges": "Paramètres de classification de la revue d'événements pour {{camera}} non enregistrés" + }, + "streams": { + "title": "Flux", + "desc": "Désactive temporairement une caméra jusqu'au redémarrage de Frigate. La désactivation complète d'une caméra interrompt le traitement des flux de cette caméra par Frigate. La détection, l'enregistrement et le débogage seront indisponibles.
    Remarque : cela ne désactive pas les rediffusions go2rtc." + }, + "object_descriptions": { + "title": "Description d'objets par IA générative", + "desc": "Activer / désactiver temporairement les descriptions d'objets par IA générative pour cette caméra. Lorsqu'elles sont désactivées, les descriptions générées par IA ne seront pas demandées pour les objets suivis par cette caméra." + }, + "review_descriptions": { + "title": "Revue de descriptions par IA générative", + "desc": "Activer / désactiver temporairement la revue de descriptions d'objets par IA générative pour cette caméra. Lorsqu'elles sont désactivées, les descriptions générées par IA ne seront plus demandées pour la revue d'éléments de cette caméra." + }, + "addCamera": "Ajouter une nouvelle caméra", + "editCamera": "Éditer la caméra :", + "selectCamera": "Sélectionner une caméra", + "backToSettings": "Retour aux paramètres de la caméra", + "cameraConfig": { + "add": "Ajouter une caméra", + "edit": "Éditer la caméra", + "description": "Configurer les paramètres de la caméra y compris les flux et les rôles.", + "name": "Nom de la caméra", + "nameRequired": "Un nom de caméra est nécessaire", + "nameInvalid": "Les noms de caméra peuvent contenir uniquement des lettres, des chiffres, des tirets bas, ou des tirets", + "namePlaceholder": "par exemple, porte_entree", + "enabled": "Activé", + "ffmpeg": { + "inputs": "Flux entrants", + "path": "Chemin d'accès du flux", + "pathRequired": "Un chemin d'accès de flux est nécessaire", + "pathPlaceholder": "rtsp://...", + "roles": "Rôles", + "rolesRequired": "Au moins un rôle est nécessaire", + "rolesUnique": "Chaque rôle (audio, détection, enregistrement) ne peut être assigné qu'à un seul flux", + "addInput": "Ajouter un flux entrant", + "removeInput": "Supprimer le flux entrant", + "inputsRequired": "Au moins un flux entrant est nécessaire" + }, + "toast": { + "success": "Caméra {{cameraName}} enregistrée avec succès" + }, + "nameLength": "Le nom de la caméra doit comporter au plus 24 caractères." + } + }, + "masksAndZones": { + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Le nom de la zone doit comporter au moins 2 caractères.", + "mustNotBeSameWithCamera": "Le nom de la zone ne doit pas être le même que le nom de la caméra.", + "mustNotContainPeriod": "Le nom de la zone ne doit pas contenir de points.", + "hasIllegalCharacter": "Le nom de la zone contient des caractères interdits.", + "alreadyExists": "Une zone portant ce nom existe déjà pour cette caméra.", + "mustHaveAtLeastOneLetter": "Le nom de la zone doit comporter au moins une lettre." + } + }, + "distance": { + "error": { + "text": "La distance doit être supérieure ou égale à 0,1.", + "mustBeFilled": "Tous les champs de distance doivent être remplis pour utiliser l'estimation de la vitesse." + } + }, + "polygonDrawing": { + "removeLastPoint": "Supprimer le dernier point", + "delete": { + "title": "Confirmer la suppression", + "desc": "Êtes-vous sûr de vouloir supprimer le {{type}} {{name}} ?", + "success": "{{name}} a été supprimé." + }, + "error": { + "mustBeFinished": "Le dessin du polygone doit être terminé avant d'enregistrer." + }, + "reset": { + "label": "Effacer tous les points" + }, + "snapPoints": { + "true": "Points d'accrochage", + "false": "Ne pas réunir les points" + }, + "type": { + "zone": "zone", + "motion_mask": "masque de mouvement", + "object_mask": "masque d'objet" + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Le temps de latence doit être supérieur ou égal à 0." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "L'inertie doit être supérieure à 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Le seuil de vitesse doit être supérieur ou égal à 0.1." + } + }, + "id": { + "error": { + "mustNotBeEmpty": "L'ID ne doit pas être vide.", + "alreadyExists": "Un masque avec cet ID existe déjà pour cette caméra." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Le nom ne doit pas être vide." + } + } + }, + "zones": { + "documentTitle": "Modifier la zone - Frigate", + "desc": { + "title": "Les zones vous permettent de définir une zone spécifique de l'image afin de déterminer si un objet s'y trouve.", + "documentation": "Documentation" + }, + "add": "Ajouter une zone", + "edit": "Modifier une zone", + "name": { + "title": "Nom", + "inputPlaceHolder": "Saisissez un nom.", + "tips": "Le nom doit comporter au moins 2 caractères, dont une lettre, et ne doit pas être le nom d'une caméra ou d'une autre zone sur cette caméra." + }, + "loiteringTime": { + "desc": "Définit une durée minimale en secondes pendant laquelle l'objet doit rester dans la zone pour qu'elle s'active. Par défaut : 0", + "title": "Temps de maraudage" + }, + "speedEstimation": { + "title": "Estimation de la vitesse", + "desc": "Activer l'estimation de la vitesse des objets dans cette zone. La zone doit comporter exactement 4 points.", + "docs": "Lire la documentation", + "lineBDistance": "Distance ligne B ({{unit}})", + "lineCDistance": "Distance ligne C ({{unit}})", + "lineADistance": "Distance ligne A ({{unit}})", + "lineDDistance": "Distance ligne D ({{unit}})" + }, + "speedThreshold": { + "title": "Seuil de vitesse ({{unit}})", + "desc": "Spécifie une vitesse minimale pour que les objets soient pris en compte dans cette zone.", + "toast": { + "error": { + "loiteringTimeError": "Les zones avec des temps de latence supérieurs à 0 ne doivent pas être utilisées avec l'estimation de la vitesse.", + "pointLengthError": "L'estimation de vitesse a été désactivée pour cette zone. Les zones avec estimation de vitesse doivent comporter exactement 4 points." + } + } + }, + "point_one": "{{count}} point", + "point_many": "{{count}} points", + "point_other": "{{count}} points", + "label": "Zones", + "inertia": { + "desc": "Spécifie le nombre d'images pendant lesquelles un objet doit être dans une zone avant d'être considéré comme y étant. Par défaut : 3", + "title": "Inertie" + }, + "toast": { + "success": "La zone ({{zoneName}}) a été enregistrée." + }, + "objects": { + "title": "Objets", + "desc": "Liste des objets qui s'appliquent à cette zone." + }, + "clickDrawPolygon": "Cliquer pour dessiner un polygone sur l'image.", + "allObjects": "Tous les objets" + }, + "motionMasks": { + "label": "Masque de mouvement", + "documentTitle": "Modifier le masque de mouvement - Frigate", + "context": { + "documentation": "Lire la documentation", + "title": "Les masques de mouvement servent à empêcher les mouvements indésirables de déclencher la détection (par exemple : branches d'arbres, horodatage des caméras). Ils doivent être utilisés avec parcimonie, car un surmasquage complique le suivi des objets." + }, + "polygonAreaTooLarge": { + "title": "Le masque de mouvement couvre {{polygonArea}} % du cadre de la caméra. Les grands masques de mouvement ne sont pas recommandés.", + "tips": "Les masques de mouvement n'empêchent pas la détection des objets. Il est préférable d'utiliser une zone obligatoire.", + "documentation": "Lire la documentation" + }, + "edit": "Modifier le masque de mouvement", + "point_one": "{{count}} point", + "point_many": "{{count}} points", + "point_other": "{{count}} points", + "clickDrawPolygon": "Cliquer pour dessiner un polygone sur l'image.", + "toast": { + "success": { + "title": "{{polygonName}} a été enregistré.", + "noName": "Le masque de mouvement a été enregistré." + } + }, + "desc": { + "title": "Les masques de mouvement servent à empêcher la détection de mouvements indésirables. Un masquage excessif complique le suivi des objets.", + "documentation": "Documentation" + }, + "add": "Nouveau masque de mouvement" + }, + "objectMasks": { + "label": "Masques d'objet", + "desc": { + "documentation": "Documentation", + "title": "Les masques de filtrage d'objets sont utilisés pour filtrer les faux positifs pour un type d'objet donné en fonction de l'emplacement." + }, + "edit": "Modifier un masque d'objet", + "clickDrawPolygon": "Cliquez pour dessiner un polygone sur l'image.", + "objects": { + "title": "Objets", + "desc": "Le type d'objet qui s'applique à ce masque d'objet.", + "allObjectTypes": "Tous les types d'objet" + }, + "toast": { + "success": { + "noName": "Le masque d'objet a été enregistré.", + "title": "{{polygonName}} a été enregistré." + } + }, + "point_one": "{{count}} point", + "point_many": "{{count}} points", + "point_other": "{{count}} points", + "add": "Ajouter un masque d'objet", + "documentTitle": "Modifier le masque d'objet - Frigate", + "context": "Les masques de filtrage d'objets sont utilisés pour filtrer les faux positifs pour un type d'objet donné en fonction de l'emplacement." + }, + "filter": { + "all": "Tous les masques et zones" + }, + "toast": { + "success": { + "copyCoordinates": "Coordonnées copiées pour {{polyName}} dans le presse-papiers." + }, + "error": { + "copyCoordinatesFailed": "Impossible de copier les coordonnées dans le presse-papiers." + } + }, + "restart_required": "Redémarrage requis (masques/zones changés)", + "objectMaskLabel": "Masque d'objet {{number}}", + "motionMaskLabel": "Masque de mouvement {{number}}", + "disabledInConfig": "Cet objet est désactivé dans le fichier de configuration", + "addDisabledProfile": "Ajouter dans la configuration de base d’abord puis remplacez le dans le profil", + "profileBase": "(base)", + "profileOverride": "(remplacer)" + }, + "motionDetectionTuner": { + "title": "Réglage de la détection de mouvement", + "desc": { + "documentation": "Lisez le guide de réglage de mouvement", + "title": "Frigate utilise la détection de mouvement comme première ligne de contrôle pour voir s'il se passe quelque chose dans l'image qui mérite d'être vérifié avec la détection d'objets." + }, + "Threshold": { + "title": "Seuil", + "desc": "La valeur seuil détermine dans quelle mesure un changement dans la luminance d'un pixel est nécessaire pour être considéré comme un mouvement. Valeur par défaut : 30" + }, + "contourArea": { + "title": "Zone de contour", + "desc": "La valeur de la zone de contour est utilisée pour déterminer quels groupes de pixels modifiés sont qualifiés de mouvement. Par défaut : 10" + }, + "improveContrast": { + "title": "Améliorer le contraste", + "desc": "Améliorer le contraste pour les scènes plus sombres. Par défaut : ACTIVÉ" + }, + "toast": { + "success": "Les paramètres de mouvement ont été enregistrés." + }, + "unsavedChanges": "Modifications des réglages de mouvement non enregistrés ({{camera}})" + }, + "debug": { + "debugging": "Débogage", + "objectList": "Liste d'objets", + "boundingBoxes": { + "title": "Cadres de détection", + "colors": { + "label": "Couleurs des cadres de détection d'objet", + "info": "
  • Au démarrage, différentes couleurs seront attribuées à chaque étiquette d'objet
  • Une fine ligne bleu foncé indique que cet objet n'est pas détecté à ce moment précis
  • Une fine ligne grise indique que cet objet est détecté comme étant immobile
  • Une ligne épaisse indique que cet objet fait l'objet d'un suivi automatique (lorsqu'il est activé)
  • " + }, + "desc": "Afficher les cadres de détection autour des objets suivis" + }, + "timestamp": { + "title": "Horodatage", + "desc": "Superposer un horodatage sur l'image" + }, + "zones": { + "title": "Zones", + "desc": "Afficher un aperçu de toutes les zones définies" + }, + "mask": { + "title": "Masques de mouvement", + "desc": "Afficher les polygones du masque de mouvement" + }, + "motion": { + "desc": "Afficher des cadres autour des zones où un mouvement est détecté", + "title": "Cadres de mouvement", + "tips": "

    Cadres de mouvement


    Des cadres rouges seront superposés sur les zones de l'image où un mouvement est actuellement détecté

    " + }, + "regions": { + "title": "Régions", + "desc": "Afficher un cadre de la région d'intérêt envoyée au détecteur d'objet", + "tips": "

    Cadres de région


    Des cadres verts lumineux seront superposés sur les zones d'intérêt de l'image qui sont envoyées au détecteur d'objets.

    " + }, + "objectShapeFilterDrawing": { + "title": "Dessin de filtre de forme d'objet", + "area": "Zone", + "desc": "Dessinez un rectangle sur l'image pour afficher les détails de la zone et du rapport", + "score": "Score", + "tips": "Activez cette option pour dessiner un rectangle sur l'image de la caméra afin d'afficher sa surface et son ratio. Ces valeurs peuvent ensuite être utilisées pour définir les paramètres de filtre de forme d'objet dans votre configuration.", + "document": "Lire la documentation ", + "ratio": "Ratio" + }, + "noObjects": "Aucun objet", + "title": "Débogage", + "detectorDesc": "Frigate utilise vos détecteurs ({{detectors}}) pour détecter les objets dans le flux vidéo de votre caméra.", + "desc": "La vue de débogage affiche en temps réel les objets suivis et leurs statistiques. La liste des objets affiche un résumé différé des objets détectés.", + "paths": { + "title": "Trajets", + "desc": "Afficher les points notables du trajet de l'objet suivi", + "tips": "

    Trajets


    Les lignes et les cercles indiqueront les points notables où l'objet suivi s'est déplacé pendant son cycle de vie.

    " + }, + "audio": { + "title": "Audio", + "noAudioDetections": "Aucune détection audio", + "score": "score", + "currentRMS": "RMS actuel", + "currentdbFS": "dbFS actuel" + }, + "openCameraWebUI": "Ouvrir l'interface Web de {{camera}}" + }, + "users": { + "title": "Utilisateurs", + "management": { + "title": "Gestion des utilisateurs", + "desc": "Gérez les comptes utilisateurs de cette instance Frigate." + }, + "addUser": "Ajouter un utilisateur", + "updatePassword": "Réinitialiser le mot de passe", + "toast": { + "success": { + "roleUpdated": "Rôle mis à jour pour {{user}}", + "deleteUser": "L'utilisateur {{user}} a été supprimé avec succès", + "createUser": "L'utilisateur {{user}} a été créé avec succès", + "updatePassword": "Mot de passe mis à jour avec succès." + }, + "error": { + "setPasswordFailed": "Échec de l'enregistrement du mot de passe : {{errorMessage}}", + "createUserFailed": "Échec de la création de l'utilisateur : {{errorMessage}}", + "deleteUserFailed": "Échec de la suppression de l'utilisateur : {{errorMessage}}", + "roleUpdateFailed": "Échec de la mise à jour du rôle : {{errorMessage}}" + } + }, + "table": { + "username": "Nom d'utilisateur", + "actions": "Actions", + "noUsers": "Aucun utilisateur trouvé.", + "changeRole": "Changer le rôle d'utilisateur", + "password": "Réinitialiser le mot de passe", + "deleteUser": "Supprimer un utilisateur", + "role": "Rôle" + }, + "dialog": { + "form": { + "user": { + "title": "Nom d'utilisateur", + "placeholder": "Saisir le nom d'utilisateur", + "desc": "Seules les lettres, les chiffres, les points et les traits de soulignement sont autorisés." + }, + "password": { + "strength": { + "weak": "Faible", + "title": "Niveau de sécurité du mot de passe : ", + "medium": "Moyen", + "strong": "Fort", + "veryStrong": "Très fort" + }, + "match": "Les mots de passe correspondent", + "notMatch": "Les mots de passe ne correspondent pas.", + "placeholder": "Saisir le mot de passe", + "title": "Mot de passe", + "confirm": { + "title": "Confirmer le mot de passe", + "placeholder": "Confirmer le mot de passe" + }, + "show": "Afficher le mot de passe", + "hide": "Masquer le mot de passe", + "requirements": { + "title": "Critères du mot de passe :", + "length": "Au moins 12 caractères", + "uppercase": "Au moins une lettre majuscule", + "digit": "Au moins un chiffre", + "special": "Au moins un caractère spécial (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nouveau mot de passe", + "placeholder": "Saisissez le nouveau mot de passe.", + "confirm": { + "placeholder": "Confirmez le nouveau mot de passe." + } + }, + "usernameIsRequired": "Nom d'utilisateur requis", + "passwordIsRequired": "Mot de passe requis", + "currentPassword": { + "title": "Mot de passe actuel", + "placeholder": "Saisissez votre mot de passe actuel" + } + }, + "deleteUser": { + "title": "Supprimer un utilisateur", + "desc": "Cette action est irréversible. Elle supprimera définitivement le compte utilisateur et toutes les données associées.", + "warn": "Êtes-vous sûr de vouloir supprimer {{username}} ?" + }, + "passwordSetting": { + "updatePassword": "Mettre à jour le mot de passe pour {{username}}", + "setPassword": "Configurer un mot de passe", + "desc": "Créez un mot de passe fort pour sécuriser ce compte.", + "doNotMatch": "Les mots de passe ne correspondent pas", + "cannotBeEmpty": "Le mot de passe ne peut être vide", + "currentPasswordRequired": "Le mot de passe actuel est requis.", + "incorrectCurrentPassword": "Le mot de passe actuel est incorrect", + "passwordVerificationFailed": "Échec de la vérification du mot de passe", + "multiDeviceWarning": "Tout autre appareil connecté devra se reconnecter dans un délai de {{refresh_time}}.", + "multiDeviceAdmin": "Vous pouvez également forcer la ré-authentification immédiate de tous les utilisateurs en renouvelant votre clé de sécurité JWT." + }, + "changeRole": { + "title": "Changer le rôle de l'utilisateur", + "desc": "Mettre à jour les autorisations pour {{username}}", + "roleInfo": { + "intro": "Sélectionnez le rôle approprié pour cet utilisateur :", + "admin": "Administrateur", + "adminDesc": "Accès complet à l'ensemble des fonctionnalités.", + "viewer": "Observateur", + "viewerDesc": "Limité aux tableaux de bord Direct, Activités, Explorer et Exports.", + "customDesc": "Rôle personnalisé avec accès spécifique à la caméra" + }, + "select": "Sélectionnez un rôle" + }, + "createUser": { + "title": "Créer un nouvel utilisateur", + "desc": "Ajoutez un nouveau compte utilisateur et spécifiez un rôle pour accéder aux zones de l'interface utilisateur Frigate.", + "usernameOnlyInclude": "Le nom d'utilisateur ne peut inclure que des lettres, des chiffres, des points (.) ou des traits de soulignement (_).", + "confirmPassword": "Veuillez confirmer votre mot de passe" + } + } + }, + "enrichments": { + "title": "Paramètres d'enrichissements", + "birdClassification": { + "title": "Identification des oiseaux", + "desc": "L'identification des oiseaux est réalisée à l'aide d'un modèle TensorFlow quantifié. Lorsqu'un oiseau est reconnu, son nom commun est automatiquement ajouté comme sous-étiquette. Cette information est intégrée à l'interface utilisateur, aux filtres de recherche et aux notifications." + }, + "semanticSearch": { + "title": "Recherche sémantique", + "readTheDocumentation": "Lire la documentation", + "reindexNow": { + "label": "Réindexer maintenant", + "desc": "La réindexation va régénérer les intégrations pour tous les objets suivis. Ce processus s'exécute en arrière-plan et peut saturer votre processeur et prendre un temps considérable en fonction du nombre d'objets suivis.", + "confirmTitle": "Confirmer la réindexation", + "confirmButton": "Réindexer", + "success": "La réindexation a démarré avec succès.", + "alreadyInProgress": "La réindexation est déjà en cours.", + "error": "Échec du démarrage de la réindexation : {{errorMessage}}", + "confirmDesc": "Êtes-vous sûr de vouloir réindexer tous les embeddings des objets suivis ? Ce processus s'exécutera en arrière-plan, mais il pourrait saturer votre processeur et prendre un temps considérable. Vous pouvez suivre la progression sur la page Explorer." + }, + "modelSize": { + "desc": "La taille du modèle utilisé pour les embeddings de recherche sémantique", + "small": { + "title": "petit", + "desc": "Utiliser petit emploie une version quantifiée du modèle qui utilise moins de mémoire et s'exécute plus rapidement sur le processeur avec une différence négligeable dans la qualité des embeddings." + }, + "large": { + "title": "grand", + "desc": "Utiliser grand emploie le modèle Jina complet et s'exécutera automatiquement sur le GPU si disponible." + }, + "label": "Taille du modèle" + }, + "desc": "La recherche sémantique de Frigate permet de retrouver des objets suivis au sein de vos activités en utilisant l'image elle-même, une description personnalisée ou une description générée automatiquement." + }, + "unsavedChanges": "Modifications non enregistrées des paramètres d'enrichissements", + "faceRecognition": { + "title": "Reconnaissance faciale", + "readTheDocumentation": "Lire la documentation", + "modelSize": { + "label": "Taille du modèle", + "desc": "La taille du modèle utilisé pour la reconnaissance faciale", + "small": { + "title": "petit", + "desc": "Utiliser petit emploie un modèle d'embedding facial FaceNet qui s'exécute efficacement sur la plupart des processeurs." + }, + "large": { + "title": "grand", + "desc": "Utiliser grand emploie un modèle d'embedding facial ArcFace et s'exécutera automatiquement sur le GPU si disponible." + } + }, + "desc": "La reconnaissance faciale permet à Frigate d'identifier les individus par leur nom. Dès qu'un visage est reconnu, Frigate associe ce nom comme sous-étiquette à l'événement. Ces informations sont ensuite intégrées dans l'interface utilisateur, les options de filtrage et les notifications." + }, + "licensePlateRecognition": { + "title": "Reconnaissance des plaques d'immatriculation", + "readTheDocumentation": "Lire la documentation", + "desc": "Frigate identifie les plaques d'immatriculation des véhicules et peut automatiquement insérer les caractères détectés dans le champ recognized_license_plate. Il est également capable d'assigner un nom familier comme sous-étiquette aux objets de type \"voiture\". Par exemple, cette fonction est souvent utilisée pour lire les plaques des véhicules empruntant une allée ou une rue." + }, + "toast": { + "error": "Échec de l'enregistrement des modifications de configuration : {{errorMessage}}", + "success": "Les paramètres d'enrichissements ont été enregistrés. Redémarrez Frigate pour appliquer vos modifications." + }, + "restart_required": "Redémarrage nécessaire (paramètres d'enrichissements modifiés)" + }, + "triggers": { + "documentTitle": "Déclencheurs", + "management": { + "title": "Déclencheurs", + "desc": "Gérer les déclencheurs pour {{camera}}. Utilisez le type vignette pour déclencher à partir de vignettes similaires à l'objet suivi sélectionné. Utilisez le type description pour déclencher à partir de textes de description similaires que vous avez spécifiés." + }, + "addTrigger": "Ajouter un déclencheur", + "table": { + "name": "Nom", + "type": "Type", + "content": "Contenu", + "threshold": "Seuil", + "actions": "Actions", + "noTriggers": "Aucun déclencheur configuré pour cette caméra.", + "edit": "Modifier", + "deleteTrigger": "Supprimer le déclencheur", + "lastTriggered": "Dernier déclencheur" + }, + "type": { + "thumbnail": "Vignette", + "description": "Description" + }, + "actions": { + "alert": "Marquer comme alerte", + "notification": "Envoyer une notification", + "sub_label": "Ajouter une sous-étiquette", + "attribute": "Ajouter un attribut" + }, + "dialog": { + "createTrigger": { + "title": "Créer un déclencheur", + "desc": "Créer un déclencheur pour la caméra {{camera}}" + }, + "editTrigger": { + "title": "Modifier le déclencheur", + "desc": "Modifier les paramètres du déclencheur de la caméra {{camera}}" + }, + "deleteTrigger": { + "title": "Supprimer le déclencheur", + "desc": "Êtes-vous sûr de vouloir supprimer le déclencheur {{triggerName}} ? Cette action est irréversible." + }, + "form": { + "name": { + "title": "Nom", + "placeholder": "Nommez ce déclencheur", + "error": { + "minLength": "Le champ doit comporter au moins deux caractères.", + "invalidCharacters": "Le champ peut contenir uniquement des lettres, des nombres, des tirets bas, et des tirets.", + "alreadyExists": "Un déclencheur avec le même nom existe déjà pour cette caméra." + }, + "description": "Saisissez un nom ou une description unique pour identifier ce déclencheur." + }, + "enabled": { + "description": "Activer ou désactiver ce déclencheur" + }, + "type": { + "title": "Type", + "placeholder": "Sélectionner un type de déclencheur", + "description": "Déclencher lorsqu'une description d'objet suivi similaire est détectée", + "thumbnail": "Déclencher lorsqu'une vignette d'objet suivi similaire est détectée" + }, + "content": { + "title": "Contenu", + "imagePlaceholder": "Sélectionner une vignette", + "textPlaceholder": "Saisir le contenu du texte", + "imageDesc": "Seules les 100 vignettes les plus récentes sont affichées. Si vous ne trouvez pas la vignette souhaitée, veuillez consulter les objets précédents dans Explorer et configurer un déclencheur à partir de ce menu.", + "textDesc": "Entrez un texte pour déclencher cette action lorsqu'une description similaire d'objet suivi est détectée.", + "error": { + "required": "Le contenu est requis." + } + }, + "threshold": { + "title": "Seuil", + "error": { + "min": "Le seuil doit être au moins 0", + "max": "Le seuil peut être au plus 1" + }, + "desc": "Définissez le seuil de similarité pour ce déclencheur. Un seuil plus élevé signifie qu'une correspondance plus exacte est requise pour activer le déclencheur." + }, + "actions": { + "title": "Actions", + "desc": "Par défaut, Frigate envoie un message MQTT pour tous les déclencheurs. Les sous-étiquettes ajoutent le nom du déclencheur à l'étiquette de l'objet. Les attributs sont des métadonnées recherchables stockées séparément dans les métadonnées de l'objet suivi.", + "error": { + "min": "Au moins une action doit être sélectionnée." + } + }, + "friendly_name": { + "title": "Nom convivial", + "placeholder": "Nommez ou décrivez ce déclencheur", + "description": "Nom convivial ou texte descriptif facultatif pour ce déclencheur." + } + } + }, + "toast": { + "success": { + "createTrigger": "Le déclencheur {{name}} a été créé avec succès.", + "updateTrigger": "Le déclencheur {{name}} a été mis à jour avec succès.", + "deleteTrigger": "Le déclencheur {{name}} a été supprimé avec succès." + }, + "error": { + "createTriggerFailed": "Échec de la création du déclencheur : {{errorMessage}}", + "updateTriggerFailed": "Échec de la mise à jour du déclencheur : {{errorMessage}}", + "deleteTriggerFailed": "Échec de la suppression du déclencheur : {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "La recherche sémantique est désactivée", + "desc": "La recherche sémantique doit être activée pour utiliser les déclencheurs." + }, + "wizard": { + "title": "Créer un déclencheur", + "step1": { + "description": "Configurez les paramètres de base pour votre déclencheur." + }, + "step2": { + "description": "Configurez le contenu qui déclenchera cette action." + }, + "step3": { + "description": "Configurez le seuil et les actions pour ce déclencheur." + }, + "steps": { + "nameAndType": "Nom et type", + "configureData": "Configuration des données", + "thresholdAndActions": "Seuil et actions" + } + } + }, + "roles": { + "management": { + "title": "Gestion des rôles Observateur", + "desc": "Gérer les rôles Observateur personnalisés et leurs permissions d'accès aux caméras pour cette instance de Frigate." + }, + "addRole": "Ajouter un rôle", + "table": { + "role": "Rôle", + "cameras": "Caméras", + "actions": "Actions", + "noRoles": "Aucun rôle personnalisé trouvé.", + "editCameras": "Modifier les caméras", + "deleteRole": "Supprimer le rôle" + }, + "toast": { + "success": { + "createRole": "Rôle {{role}} créé avec succès", + "updateCameras": "Caméras mis à jour pour le rôle {{role}}", + "deleteRole": "Rôle {{role}} supprimé avec succès", + "userRolesUpdated_one": "{{count}} utilisateur affecté à ce rôle a été mis à jour avec des droits \"Observateur\", et a accès à toutes les caméras.", + "userRolesUpdated_many": "{{count}} utilisateurs affectés à ce rôle ont été mis à jour avec des droits \"Observateur\", et ont accès à toutes les caméras.", + "userRolesUpdated_other": "{{count}} utilisateurs affectés à ce rôle ont été mis à jour avec des droits \"Observateur\", et ont accès à toutes les caméras." + }, + "error": { + "createRoleFailed": "Échec dans la création du rôle : {{errorMessage}}", + "updateCamerasFailed": "Échec de la mise à jour des caméras : {{errorMessage}}", + "deleteRoleFailed": "Échec lors de la suppression du rôle : {{errorMessage}}", + "userUpdateFailed": "Echec lors de la mise à jour des rôles de l'utilisateur : {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Créer un nouveau rôle", + "desc": "Ajouter un nouveau rôle et définir les permissions d'accès à la caméra." + }, + "editCameras": { + "title": "Modifier les caméras du rôle", + "desc": "Mettre à jour les accès aux caméras pour le rôle {{role}}." + }, + "deleteRole": { + "title": "Suppression du rôle", + "desc": "Cette action est irréversible. Elle supprimera définitivement le rôle et tous les utilisateurs associés seront affectés au rôle \"Observateur\", avec un accès à toutes les caméras.", + "warn": "Êtes-vous sûr de vouloir supprimer {{role}} ?", + "deleting": "En cours de suppression..." + }, + "form": { + "role": { + "title": "Nom du rôle", + "placeholder": "Saisissez un nom de rôle.", + "desc": "Seuls les lettres, les chiffres, les points et les traits de soulignement sont autorisés.", + "roleIsRequired": "Un nom de rôle est requis", + "roleOnlyInclude": "Le nom de rôle ne peut inclure que des lettres, des chiffres, des points (.) ou des traits de soulignement (_).", + "roleExists": "Un rôle avec ce nom existe déjà." + }, + "cameras": { + "title": "Caméras", + "desc": "Sélectionnez les caméras auxquelles ce rôle aura accès. Au moins une caméra est requise.", + "required": "Au moins une caméra doit être sélectionnée." + } + } + } + }, + "cameraWizard": { + "title": "Ajouter une caméra", + "description": "Suivez les étapes ci-dessous pour ajouter une nouvelle caméra à votre installation Frigate.", + "steps": { + "nameAndConnection": "Nom et connexion", + "streamConfiguration": "Configuration du flux", + "validationAndTesting": "Validation et tests", + "probeOrSnapshot": "Sondage ou Instantané" + }, + "save": { + "success": "Nouvelle caméra {{cameraName}} enregistrée avec succès", + "failure": "Échec lors de l'enregistrement de {{cameraName}}" + }, + "testResultLabels": { + "resolution": "Résolution", + "video": "Vidéo", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Veuillez saisir une URL de flux valide.", + "testFailed": "Échec du test de flux : {{error}}" + }, + "step1": { + "description": "Saisissez les détails de votre caméra et choisissez d'interroger la caméra ou de sélectionner manuellement la marque.", + "cameraName": "Nom de la caméra", + "cameraNamePlaceholder": "par ex., porte_entree ou apercu_cour_arriere", + "host": "Hôte / Adresse IP", + "port": "Port", + "username": "Nom d'utilisateur", + "usernamePlaceholder": "Facultatif", + "password": "Mot de passe", + "passwordPlaceholder": "Facultatif", + "selectTransport": "Sélectionnez le protocole de transport.", + "cameraBrand": "Marque de la caméra", + "selectBrand": "Sélectionnez la marque de la caméra pour déterminer la forme de l'URL.", + "customUrl": "URL de flux personnalisé", + "brandInformation": "Information sur la marque", + "brandUrlFormat": "Pour les caméras avec un format d'URL RTSP comme : {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nomutilisateur:motdepasse@hote:port/chemin", + "testConnection": "Tester la connexion", + "testSuccess": "Test de connexion réussi !", + "testFailed": "Échec du test de connexion. Veuillez vérifier votre saisie et réessayez.", + "streamDetails": "Détails du flux", + "warnings": { + "noSnapshot": "Impossible de récupérer un instantané à partir du flux configuré" + }, + "errors": { + "brandOrCustomUrlRequired": "Sélectionnez une marque de caméra avec hôte/IP ou choisissez « Autre » avec une URL personnalisée.", + "nameRequired": "Le nom de la caméra est requis.", + "nameLength": "Le nom de la caméra ne doit pas dépasser 64 caractères.", + "invalidCharacters": "Le nom de la caméra contient des caractères invalides.", + "nameExists": "Ce nom de caméra est déjà utilisé.", + "brands": { + "reolink-rtsp": "Le protocole RTSP de Reolink est déconseillé. Activez le protocole HTTP dans les paramètres du firmware de la caméra, puis relancez l'assistant." + }, + "customUrlRtspRequired": "Les URL personnalisées doivent commencer par \"rtsp://\". Une configuration manuelle est requise pour les flux de caméra non-RTSP." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Vérification des métadonnées de la caméra en cours...", + "fetchingSnapshot": "Récupération de l'instantané de la caméra en cours..." + }, + "connectionSettings": "Paramètres de connexion", + "detectionMethod": "Méthode de détection du flux", + "onvifPort": "Port ONVIF", + "probeMode": "Interroger la caméra", + "manualMode": "Sélection manuelle", + "detectionMethodDescription": "Interrogez la caméra avec ONVIF (si pris en charge) pour trouver les URL de flux de la caméra, ou sélectionnez manuellement la marque de la caméra pour utiliser des URL prédéfinies. Pour saisir une URL RTSP personnalisée, choisissez la méthode manuelle et sélectionnez \"Autre\".", + "onvifPortDescription": "Pour les caméras prenant en charge ONVIF, il s'agit généralement de 80 ou 8080.", + "useDigestAuth": "Utiliser l'authentification Digest", + "useDigestAuthDescription": "Utilisez l'authentification Digest HTTP pour ONVIF. Certaines caméras peuvent nécessiter un nom d'utilisateur/mot de passe ONVIF dédié au lieu de l'utilisateur administrateur standard." + }, + "step2": { + "description": "Interrogez la caméra pour les flux disponibles ou configurez des paramètres manuels en fonction de la méthode de détection sélectionnée.", + "streamsTitle": "Flux de caméra", + "addStream": "Ajouter un flux", + "addAnotherStream": "Ajouter un autre flux", + "streamTitle": "Flux {{number}}", + "streamUrl": "URL du flux", + "streamUrlPlaceholder": "rtsp://nomutilisateur:motdepasse@hote:port/chemin", + "url": "URL", + "resolution": "Résolution", + "selectResolution": "Sélectionnez la résolution.", + "quality": "Qualité", + "selectQuality": "Sélectionnez la qualité.", + "roles": "Rôles", + "roleLabels": { + "record": "Enregistrement", + "audio": "Audio", + "detect": "Détection d'objets" + }, + "testStream": "Tester la connexion", + "testSuccess": "Test de connexion réussi !", + "testFailed": "Échec du test de connexion. Veuillez vérifier votre saisie et réessayer.", + "testFailedTitle": "Échec du test", + "connected": "Connecté", + "notConnected": "Non connecté", + "featuresTitle": "Caractéristiques", + "go2rtc": "Réduire le nombre de connexions à la caméra", + "detectRoleWarning": "Pour continuer, au moins un flux doit avoir le rôle \"détection\".", + "rolesPopover": { + "title": "Rôles du flux", + "detect": "Flux principal pour la détection d'objets", + "record": "Enregistre des extraits du flux vidéo en fonction des paramètres de configuration.", + "audio": "Flux pour la détection audio" + }, + "featuresPopover": { + "title": "Fonctionnalités du flux", + "description": "Utilisez la rediffusion du flux go2rtc pour réduire le nombre de connexions à votre caméra." + }, + "streamDetails": "Détails du flux", + "probing": "Interrogation de la caméra en cours...", + "retry": "Réessayer", + "testing": { + "probingMetadata": "Interrogation des métadonnées de la caméra en cours...", + "fetchingSnapshot": "Récupération de l'instantané de la caméra en cours..." + }, + "probeFailed": "Impossible d'interroger la caméra : {{error}}", + "probingDevice": "Interrogation de l'appareil en cours...", + "probeSuccessful": "Interrogation réussie", + "probeError": "Erreur d'interrogation", + "probeNoSuccess": "Échec de l'interrogation", + "deviceInfo": "Informations sur l'appareil", + "manufacturer": "Fabricant", + "model": "Modèle", + "firmware": "Micrologiciel", + "profiles": "Profils", + "ptzSupport": "Prise en charge PTZ", + "autotrackingSupport": "Prise en charge du suivi automatique", + "presets": "Préréglages", + "rtspCandidates": "Candidats RTSP", + "rtspCandidatesDescription": "Les URL RTSP suivantes ont été trouvées lors de l'interrogation de la caméra. Testez la connexion pour afficher les métadonnées du flux.", + "noRtspCandidates": "Aucune URL RTSP n'a été trouvée sur la caméra. Vos identifiants sont peut-être incorrects, ou la caméra ne prend peut-être pas en charge ONVIF ou la méthode utilisée pour récupérer les URL RTSP. Revenez en arrière et saisissez l'URL RTSP manuellement.", + "candidateStreamTitle": "Candidat {{number}}", + "useCandidate": "Utiliser", + "uriCopy": "Copier", + "uriCopied": "URI copiée dans le presse-papiers", + "testConnection": "Tester la connexion", + "toggleUriView": "Cliquer pour basculer l'affichage de l'URI complet", + "errors": { + "hostRequired": "L'hôte/adresse IP est requis." + } + }, + "step3": { + "description": "Configurez les rôles des flux et ajoutez des flux supplémentaires pour votre caméra.", + "validationTitle": "Validation du flux", + "connectAllStreams": "Connecter tous les flux", + "reconnectionSuccess": "Reconnexion réussie.", + "reconnectionPartial": "La reconnexion de certains flux a échoué.", + "streamUnavailable": "Aperçu du flux indisponible", + "reload": "Recharger", + "connecting": "Connexion en cours...", + "streamTitle": "Flux {{number}}", + "failed": "Échec", + "notTested": "Non testé", + "connectStream": "Connecter", + "connectingStream": "Connexion en cours", + "disconnectStream": "Déconnecter", + "estimatedBandwidth": "Débit estimé", + "roles": "Rôles", + "none": "Aucun", + "error": "Erreur", + "streamValidated": "Flux {{number}} validé avec succès", + "streamValidationFailed": "La validation du flux {{number}} a échoué", + "saveAndApply": "Enregistrer une nouvelle caméra", + "saveError": "Configuration invalide. Veuillez vérifier vos paramètres.", + "issues": { + "title": "Validation du flux", + "videoCodecGood": "Le codec vidéo est {{codec}}.", + "audioCodecGood": "Le codec audio est {{codec}}.", + "noAudioWarning": "Aucun son n'est détecté sur ce flux, les enregistrements seront muets.", + "audioCodecRecordError": "Le codec audio AAC est requis pour la prise en charge du son dans les enregistrements.", + "audioCodecRequired": "Un flux audio est requis pour prendre en charge la détection audio.", + "restreamingWarning": "La réduction des connexions à la caméra pour le flux d'enregistrement peut augmenter légèrement l'utilisation du processeur.", + "dahua": { + "substreamWarning": "Le flux secondaire 1 est limité en basse résolution. De nombreuses caméras (Dahua, Amcrest, EmpireTech...) proposent des flux supplémentaires qu'il suffit d'activer dans leurs propres paramètres. Il est recommandé de vérifier leur disponibilité et de les utiliser." + }, + "hikvision": { + "substreamWarning": "Le flux secondaire 1 est limité en basse résolution. De nombreuses caméras Hikvision proposent des flux supplémentaires qu'il suffit d'activer dans leurs propres paramètres. Il est recommandé de vérifier leur disponibilité et de les utiliser." + }, + "resolutionHigh": "La résolution {{resolution}} risque d'augmenter l'utilisation des ressources.", + "resolutionLow": "La résolution {{resolution}} risque d'être trop faible pour détecter les petits objets de manière fiable." + }, + "valid": "Valide", + "ffmpegModule": "Utiliser le mode de compatibilité du flux", + "ffmpegModuleDescription": "Si le flux ne se charge pas après plusieurs tentatives, essayez d'activer cette option. Lorsqu'elle est activée, Frigate utilisera le module ffmpeg avec go2rtc. Cela peut offrir une meilleure compatibilité avec certains flux de caméra.", + "streamsTitle": "Flux de la caméra", + "addStream": "Ajouter un flux", + "addAnotherStream": "Ajouter un autre flux", + "streamUrl": "URL du flux", + "streamUrlPlaceholder": "rtsp://nomdutilisateur:motdepasse@hote:port/chemin", + "selectStream": "Sélectionner un flux", + "searchCandidates": "Rechercher des candidats", + "noStreamFound": "Aucun flux trouvé", + "url": "URL", + "resolution": "Résolution", + "selectResolution": "Sélectionner la résolution", + "quality": "Qualité", + "selectQuality": "Sélectionner la qualité", + "roleLabels": { + "detect": "Détection d'objet", + "record": "Enregistrement", + "audio": "Audio" + }, + "testStream": "Tester la connexion", + "testSuccess": "Test du flux réussi !", + "testFailed": "Échec du test du flux", + "testFailedTitle": "Échec du test", + "connected": "Connecté", + "notConnected": "Non connecté", + "featuresTitle": "Fonctionnalités", + "go2rtc": "Réduire les connexions à la caméra", + "detectRoleWarning": "Au moins un flux doit avoir le rôle 'détection' pour continuer.", + "rolesPopover": { + "title": "Rôles du flux", + "detect": "Flux principal pour la détection d'objet", + "record": "Enregistre des segments du flux vidéo en fonction des paramètres de configuration", + "audio": "Flux pour la détection basée sur l'audio" + }, + "featuresPopover": { + "title": "Fonctionnalités du flux", + "description": "Utiliser la rediffusion go2rtc pour réduire les connexions à votre caméra" + } + }, + "step4": { + "description": "Validation et analyse finales avant d'enregistrer votre nouvelle caméra. Connectez chaque flux avant d'enregistrer.", + "validationTitle": "Validation du flux", + "connectAllStreams": "Connecter tous les flux", + "reconnectionSuccess": "Reconnexion réussie", + "reconnectionPartial": "Certains flux n'ont pas réussi à se reconnecter.", + "streamUnavailable": "Aperçu du flux non disponible", + "reload": "Recharger", + "connecting": "En cours de connexion...", + "streamTitle": "Flux {{number}}", + "valid": "Valide", + "failed": "Échec", + "notTested": "Non testé", + "connectStream": "Connecter", + "connectingStream": "En cours de connexion", + "disconnectStream": "Déconnecter", + "estimatedBandwidth": "Bande passante estimée", + "roles": "Rôles", + "ffmpegModule": "Utiliser le mode de compatibilité du flux", + "ffmpegModuleDescription": "Si le flux ne se charge pas après plusieurs tentatives, essayez d'activer cette option. Lorsqu'elle est activée, Frigate utilisera le module ffmpeg avec go2rtc. Cela peut offrir une meilleure compatibilité avec certains flux de caméra.", + "none": "Aucun", + "error": "Erreur", + "streamValidated": "Flux {{number}} validé avec succès", + "streamValidationFailed": "Échec de la validation du flux {{number}}", + "saveAndApply": "Enregistrer la nouvelle caméra", + "saveError": "Configuration invalide. Veuillez vérifier vos paramètres.", + "issues": { + "title": "Validation du flux", + "videoCodecGood": "Le codec vidéo est {{codec}}.", + "audioCodecGood": "Le codec audio est {{codec}}.", + "resolutionHigh": "Une résolution de {{resolution}} peut entraîner une utilisation accrue des ressources.", + "resolutionLow": "Une résolution de {{resolution}} peut être trop faible pour une détection fiable des petits objets.", + "noAudioWarning": "Aucun audio détecté pour ce flux, les enregistrements n'auront pas de son.", + "audioCodecRecordError": "Le codec audio AAC est requis pour prendre en charge l'audio dans les enregistrements.", + "audioCodecRequired": "Un flux audio est requis pour prendre en charge la détection audio.", + "restreamingWarning": "Réduire les connexions à la caméra pour le flux d'enregistrement peut légèrement augmenter l'utilisation du processeur.", + "brands": { + "reolink-rtsp": "Le RTSP Reolink n'est pas recommandé. Activez HTTP dans les paramètres du micrologiciel de la caméra et redémarrez l'assistant.", + "reolink-http": "Les flux HTTP de Reolink devraient utiliser FFmpeg pour une meilleure compatibilité. Activez 'Utiliser le mode de compatibilité du flux' pour ce flux." + }, + "dahua": { + "substreamWarning": "Le sous-flux 1 est limité à une basse résolution. De nombreuses caméras Dahua / Amcrest / EmpireTech prennent en charge des sous-flux supplémentaires qui doivent être activés dans les paramètres de la caméra. Il est recommandé de vérifier et d'utiliser ces flux s'ils sont disponibles." + }, + "hikvision": { + "substreamWarning": "Le sous-flux 1 est limité à une basse résolution. De nombreuses caméras Hikvision prennent en charge des sous-flux supplémentaires qui doivent être activés dans les paramètres de la caméra. Il est recommandé de vérifier et d'utiliser ces flux s'ils sont disponibles." + } + } + } + }, + "cameraManagement": { + "title": "Gérer les caméras", + "addCamera": "Ajouter une nouvelle caméra", + "editCamera": "Modifier la caméra :", + "selectCamera": "Sélectionnez une caméra", + "backToSettings": "Retour aux paramètres de la caméra", + "streams": { + "title": "Activer / désactiver les caméras", + "desc": "Désactive temporairement une caméra jusqu'au redémarrage de Frigate. La désactivation d'une caméra interrompt complètement le traitement des flux de la caméra par Frigate. La détection, l'enregistrement et le débogage deviennent alors indisponibles.
    Remarque : cela n'affecte pas les rediffusions des flux go2rtc.", + "enableLabel": "Caméras activées", + "disableLabel": "Caméra désactivées", + "disableDesc": "Activer une caméra qui n'est pas visible dans l'interface et désactivée dans la configuration. Un redémarrage de Frigate est nécessaire après l'activation.", + "enableSuccess": "Activer {{cameraName}} dans la configuration. Redémarrer Frigate pour appliquer les changements." + }, + "cameraConfig": { + "add": "Ajouter une caméra", + "edit": "Modifier la caméra", + "description": "Configurez les paramètres de la caméra, notamment les flux entrants et les rôles.", + "name": "Nom de la caméra", + "nameRequired": "Le nom de la caméra est requis", + "nameLength": "Le nom de la caméra doit comporter moins de 64 caractères.", + "namePlaceholder": "par exemple, porte d'entrée ou aperçu de la cour arrière", + "enabled": "Activé", + "ffmpeg": { + "inputs": "Flux d'entrée", + "path": "Chemin du flux", + "pathRequired": "Chemin du flux requis", + "pathPlaceholder": "rtsp://...", + "roles": "Rôles", + "rolesRequired": "Au moins un rôle est requis", + "rolesUnique": "Chaque rôle (audio, détection, enregistrement) ne peut être attribué qu'à un seul flux", + "addInput": "Ajouter un flux d'entrée", + "removeInput": "Supprimer le flux d'entrée", + "inputsRequired": "Au moins un flux d'entrée est requis" + }, + "go2rtcStreams": "Flux go2rtc", + "streamUrls": "URL des flux", + "addUrl": "Ajouter une URL", + "addGo2rtcStream": "Ajouter un flux go2rtc", + "toast": { + "success": "La caméra {{cameraName}} a été enregistrée avec succès" + } + }, + "deleteCamera": "Supprimer la caméra", + "deleteCameraDialog": { + "title": "Supprimer la caméra", + "description": "Supprimer la caméra va supprimer de façon permanente les enregistrements, les objets suivis, et la configuration de la caméra. Tous les streams go2rtc associés à la caméra devront être supprimés manuellement.", + "selectPlaceholder": "Choisir une caméra...", + "confirmTitle": "Êtes-vous sûr?", + "confirmWarning": "Supprimer \n{{cameraName}}\n ne peut être annulé.", + "deleteExports": "Supprimer aussi les exports de cette caméra", + "confirmButton": "Suppression permanente", + "success": "Caméra {{cameraName}} supprimée avec succès", + "error": "Impossible de supprimer la caméra {{cameraName}}" + }, + "profiles": { + "selectLabel": "Choisir un profil", + "description": "Configurer quelles caméras sont activées ou désactivées quand un profil est activé. Les caméras activées avec \"Inherit\" conservent leur statut de base.", + "inherit": "Hériter", + "enabled": "Activé", + "disabled": "Désactivé" + }, + "clone": { + "target": { + "newNameLabel": "Nom de la caméra", + "newNamePlaceholder": "p.ex., porte_arriere ou Porte arrière", + "newNameRequired": "Le nom de la caméra est requis", + "newNameInvalid": "Nom de caméra invalide", + "newNameCollision": "Une caméra avec ce nom existe déjà", + "newStreamsForced": "Les flux sont toujours copiés pour une nouvelle caméra.", + "existingCamerasRadio": "Caméras existantes", + "allCameras": "Toutes les caméras", + "existingPlaceholder": "Sélectionnez au moins une caméra", + "existingDisabled": "Aucune autre caméra à copier vers" + }, + "categories": { + "legend": "Paramètres à cloner", + "description": "Choisissez quels paramètres copier depuis la caméra source.", + "selectAll": "Sélectionner tout", + "selectNone": "Sélectionner aucun", + "resetDefaults": "Rétablir à la configuration d'usine", + "general": "Général", + "spatial": "Paramètres spatiaux", + "streams": "Flux", + "spatialWarning": "La résolution détectée ({{srcWidth}}x{{srcHeight}}) de la caméra source {{srcCamera}} diffère de : {{cameras}}. Les polygones peuvent ne pas être alignés sur ces caméras. Ces paramètres sont désactivés ; activer pour copier tel quel.", + "restartHint": "Redémarrage requis", + "items": { + "record": "En cours d'enregistrement", + "objects": "Objets", + "audio": "Détection audio", + "audio_transcription": "Transcription audio", + "notifications": "Notifications", + "mqtt": "MQTT", + "onvif": "ONVIF", + "face_recognition": "Reconnaissance faciale", + "semantic_search": "Recherche sémantique" + } + }, + "footer": { + "submit": "Cloner", + "submitting": "Clonage…" + }, + "toast": { + "success": "Paramètres copiés vers {{cameraName}}", + "successWithRestart": "Paramètres copiés vers {{cameraName}}. Redémarrez Frigate afin d'appliquer tous les changements.", + "successMulti_one": "Paramètres copiés vers {{count}} caméra", + "successMulti_many": "Paramètres copiés vers {{count}} caméras", + "successMulti_other": "Paramètres copiés vers {{count}} caméras" + } + } + }, + "cameraReview": { + "title": "Paramètres des activités caméra", + "object_descriptions": { + "title": "Descriptions d'objets par l'IA générative", + "desc": "Activez ou désactivez temporairement les descriptions par IA générative jusqu'au redémarrage. Si désactivé, l'IA ne sera plus sollicitée pour décrire les objets suivis sur cette caméra." + }, + "review_descriptions": { + "title": "Descriptions des activités par l'IA générative", + "desc": "Activez ou désactivez temporairement les descriptions d'activités par IA générative jusqu'au redémarrage. Si désactivé, l'IA ne sera plus sollicitée pour décrire les activités sur cette caméra." + }, + "review": { + "title": "Activités", + "desc": "Active ou désactive temporairement les alertes et les détections pour cette caméra jusqu'au redémarrage de Frigate. Lorsque cette option est désactivée, aucune activité nouvelle n'est générée. ", + "alerts": "Alertes ", + "detections": "Détections " + }, + "reviewClassification": { + "title": "Classification des activités", + "desc": "Frigate classe les activités en deux catégories : \"Alertes\" et \"Détections\". Par défaut, les objets de type personne et voiture sont considérés comme des \"Alertes\". Vous pouvez affiner cette classification en définissant des zones spécifiques pour chaque objet.", + "noDefinedZones": "Aucune zone n'est définie pour cette caméra.", + "objectAlertsTips": "Sur la caméra {{cameraName}}, tous les objets {{alertsLabels}} apparaîtront en tant qu'\"Alertes\".", + "zoneObjectAlertsTips": "Sur la caméra {{cameraName}}, tous les objets {{alertsLabels}} détectés dans la zone {{zone}} apparaîtront en tant qu'\"Alertes\".", + "objectDetectionsTips": "Sur la caméra {{cameraName}}, tous les objets {{detectionsLabels}} non catégorisés apparaîtront en tant que \"Détections\", peu importe leur zone.", + "zoneObjectDetectionsTips": { + "text": "Sur la caméra {{cameraName}}, tous les objets {{detectionsLabels}} non catégorisés dans la zone {{zone}} apparaîtront en tant que \"Détections\".", + "notSelectDetections": "Sur la caméra {{cameraName}}, tous les objets {{detectionsLabels}} détectés dans la zone {{zone}} qui ne sont pas catégorisés comme \"Alertes\" apparaîtront en tant que \"Détections\", et ce, quelle que soit leur zone.", + "regardlessOfZoneObjectDetectionsTips": "Sur la caméra {{cameraName}}, tous les objets {{detectionsLabels}} non catégorisés apparaîtront en tant que \"Détections\", peu importe leur zone." + }, + "unsavedChanges": "Paramètres de classification des activités non enregistrés pour {{camera}}", + "selectAlertsZones": "Sélectionnez les zones pour les alertes", + "selectDetectionsZones": "Sélectionner les zones pour les détections", + "limitDetections": "Limiter les détections à des zones spécifiques", + "toast": { + "success": "La configuration de la classification des activités a été enregistrée. Redémarrez Frigate pour appliquer les modifications." + } + } + }, + "saveAllPreview": { + "title": "Modifications à enregistrer", + "triggerLabel": "Examiner les modifications en attente", + "empty": "Aucune modification en attente", + "scope": { + "label": "Portée", + "global": "Global", + "camera": "Caméra : {{cameraName}}" + }, + "field": { + "label": "Champ" + }, + "value": { + "label": "Nouvelle valeur", + "reset": "Réinitialiser" + }, + "profile": { + "label": "Profil" + } + }, + "button": { + "overriddenBaseConfigTooltip": "Le profil {{profile}} remplace les paramètres de configuration dans cette section", + "overriddenGlobalTooltip": "Cette caméra remplace les paramètres de la configuration globale dans cette section", + "overriddenGlobal": "Remplacé (Global)", + "overriddenBaseConfig": "Remplacée (Configuration de base)" + }, + "maintenance": { + "title": "Maintenance", + "sync": { + "title": "Synchronisation du Média" + } + }, + "configMessages": { + "lpr": { + "vehicleNotTracked": "La reconnaissance de plaque d'immatriculation requiert que 'voiture' ou 'moto' soit suivi.", + "globalDisabled": "La reconnaissance de numéro d'immatriculation n'est pas activée au niveau global. Activez-la dans les paramètres globaux pour que la reconnaissance de plaques fonctionne au niveau caméra." + }, + "review": { + "recordDisabled": "L'enregistrement est désactivé, aucune révision ne sera générée.", + "detectDisabled": "La détection d'objet est désactivée. Les révisions requièrent que les objets détectés catégorisent les alertes et les détections.", + "allNonAlertDetections": "Toutes les activités de non alerte seront incluses en tant que détections." + }, + "audio": { + "noAudioRole": "Aucun flux ne possède de rôle audio défini. Vous devez activer le rôle audio afin de faire fonctionner la détection audio." + }, + "audioTranscription": { + "audioDetectionDisabled": "La détection audio n'est pas active pour cette caméra. La transcription audio nécessite que la détection audio soit active." + }, + "detect": { + "fpsGreaterThanFive": "Il n'est pas recommandé de régler la détection au-delà de 5 FPS." + }, + "faceRecognition": { + "globalDisabled": "La reconnaissance faciale n'est pas activée au niveau global. Activez-la dans les paramètres globaux pour que la reconnaissance faciale fonctionne au niveau caméra.", + "personNotTracked": "La reconnaissance faciale requiert que l'objet 'person' soit suivie. Assurez-vous que 'person' soit dans la liste d'objets suivis." + } + }, + "go2rtcStreams": { + "ffmpeg": { + "audioMp3": "Transcoder en PM3", + "audioExclude": "Exclure", + "hardwareNone": "Pas d'accélération matérielle", + "hardwareAuto": "Accélération matérielle automatique", + "audioCopy": "Copier", + "audioAac": "Transcoder en AAC", + "audioOpus": "Transcoder vers Opus", + "audioPcmu": "Transcoder vers PCM μ-law", + "video": "Vidéo", + "audio": "Audio", + "hardware": "Accélération matérielle", + "videoCopy": "Copier", + "videoH264": "Transcoder vers H.264", + "videoH265": "Transcoder vers H.265", + "videoExclude": "Exclure", + "useFfmpegModule": "Utiliser le mode de compatibilité (ffmpeg)", + "audioPcma": "Transcoder vers PCM A-law", + "audioPcm": "Transcoder vers PCM" + }, + "renameStream": "Renommer le flux", + "renameStreamDesc": "Saisir un nouveau nom pour ce flux. Le renommage d'un flux peut induire un problème avec les caméras ou les autres flux qui le référence par nom.", + "addStream": "Ajouter un flux", + "title": "Flux go2rtc", + "description": "Gérer les paramètres de flux go2rtc pour la rediffusion de caméra. Chaque flux possède un nom et une ou plusieurs URLs source.", + "deleteStream": "Supprimer flux", + "deleteStreamConfirm": "Êtes-vous sûr de vouloir supprimer le flux \"{{streamName}}\" ? Les caméras qui référencent ce flux pourraient ne plus fonctionner.", + "noStreams": "Aucun flux go2rtc configuré. Ajoutez un flux pour commencer.", + "validation": { + "nameRequired": "Le nom de flux est obligatoire", + "nameDuplicate": "Un flux avec ce nom existe déjà", + "nameInvalid": "Le nom de flux ne peut contenir que des lettres, nombres, underscores et tirets", + "urlRequired": "Au moins une URL est requise" + }, + "newStreamName": "Nouveau nom de flux", + "addUrl": "Ajouter URL", + "streamName": "Nom de flux", + "streamNamePlaceholder": "p. ex., porte_entree", + "streamUrlPlaceholder": "p. ex., rtsp://utilisateur:motpasse@192.168.1.100/flux", + "addStreamDesc": "Saisir un nom pour ce nouveau flux. Ce nom sera utilisé pour référencer le flux dans les paramètres de votre caméra." + }, + "onvif": { + "profileAuto": "Automatique", + "profileLoading": "Chargement des profils..." + }, + "profiles": { + "enableSwitch": "Activer les profils", + "enabledDescription": "Les profils sont actifs. Créer un nouveau profil ci-dessous, naviguer vers la section de configuration de la caméra afin de faire vos changements, et les sauvegarder afin de les prendre en compte.", + "error": { + "mustBeAtLeastTwoCharacters": "Doit comporter au moins 2 caractères", + "mustNotContainPeriod": "Ne doit pas contenir de points", + "alreadyExists": "Un profil avec cet identifiant existe déjà" + }, + "deactivated": "Profil désactivé", + "noProfiles": "Aucun profil défini.", + "noOverrides": "Aucune surcharge", + "cameraCount_one": "{{count}} caméra", + "cameraCount_many": "{{count}} caméras", + "cameraCount_other": "{{count}} caméras", + "columnCamera": "Caméra", + "columnOverrides": "Surcharges de profil", + "baseConfig": "Configuration de base", + "addProfile": "Ajouter un profil", + "newProfile": "Nouveau profil", + "friendlyNameLabel": "Nom profil", + "profileIdLabel": "ID profil", + "profileIdDescription": "Identifiant interne utilisé dans la configuration et automatisations", + "nameInvalid": "Ne sont autorisés que les lettres minuscules, nombres et underscores", + "nameDuplicate": "Un profil avec ce nom existe déjà", + "renameProfile": "Renommer profil", + "renameSuccess": "Profil renommé en '{{profile}}'", + "deleteProfile": "Supprimer Profil", + "deleteProfileConfirm": "Supprimer profil \"{{profile}}\" de toutes les caméras ? Ceci ne peut être annulé.", + "deleteSuccess": "Le profil '{{profile}}' a été supprimé", + "createSuccess": "Le profil '{{profile}}' a été créé", + "removeOverride": "Supprimer le profil surchargé", + "deleteSection": "Supprimer la section de surcharges", + "deleteSectionConfirm": "Supprimer les surcharges de {{section}} pour le profil {{profile}} sur {{camera}} ?", + "deleteSectionSuccess": "Surcharges de {{section}} supprimées pour {{profile}}", + "disabledDescription": "Les profils vous permettent de définir des ensembles nommés de surcharges de configuration de caméra (p. ex. armé, absent, nuit) qui peuvent être activés à la demande." + }, + "unsavedChanges": "Vous avez des changements non sauvegardés", + "confirmReset": "Confirmer réinitialisation", + "resetToDefaultDescription": "Cela va réinitialiser les paramètres dans cette section avec les valeurs d'usine. Cette action ne peut être annulée.", + "resetToGlobalDescription": "Ceci va réinitialiser les paramètres de cette section vers les paramètres globaux. Cette action ne peut être annulée." +} diff --git a/web/public/locales/fr/views/system.json b/web/public/locales/fr/views/system.json new file mode 100644 index 0000000..3c4577c --- /dev/null +++ b/web/public/locales/fr/views/system.json @@ -0,0 +1,274 @@ +{ + "documentTitle": { + "storage": "Statistiques de stockage - Frigate", + "cameras": "Statistiques des caméras - Frigate", + "general": "Statistiques générales - Frigate", + "enrichments": "Statistiques d'enrichissements - Frigate", + "logs": { + "frigate": "Journaux de Frigate - Frigate", + "nginx": "Journaux Nginx - Frigate", + "go2rtc": "Journaux Go2RTC - Frigate", + "websocket": "Journaux des messages - Frigate" + } + }, + "title": "Système", + "metrics": "Métriques du système", + "logs": { + "download": { + "label": "Télécharger les journaux" + }, + "copy": { + "label": "Copier dans le presse-papiers", + "success": "Journaux copiés dans le presse-papiers", + "error": "Échec de la copie des journaux dans le presse-papiers" + }, + "type": { + "label": "Type", + "timestamp": "Horodatage", + "tag": "Balise", + "message": "Message" + }, + "tips": "Les journaux sont diffusés en continu depuis le serveur", + "toast": { + "error": { + "fetchingLogsFailed": "Erreur lors de la récupération des logs : {{errorMessage}}", + "whileStreamingLogs": "Erreur lors de la diffusion des logs : {{errorMessage}}" + } + }, + "websocket": { + "label": "Messages", + "pause": "Pause", + "resume": "Reprendre", + "clear": "Effacer", + "filter": { + "all": "Tous les sujets", + "topics": "Sujets", + "events": "Évènements", + "reviews": "Revues", + "classification": "Classification", + "face_recognition": "Reconnaissance Faciale", + "lpr": "LAPI", + "camera_activity": "Activités de la caméra", + "system": "Système", + "camera": "Caméra", + "all_cameras": "Toutes les caméras", + "cameras_count_one": "{{count}} Caméra", + "cameras_count_other": "{{count}} Caméras" + }, + "empty": "Aucun message capturé jusque là", + "count_one": "{{count}} message", + "count_other": "{{count}} messages", + "expanded": { + "payload": "Charge utile" + } + } + }, + "general": { + "title": "Général", + "detector": { + "title": "Détecteurs", + "inferenceSpeed": "Vitesse d'inférence du détecteur", + "cpuUsage": "Utilisation CPU du détecteur", + "memoryUsage": "Utilisation mémoire du détecteur", + "temperature": "Température du détecteur", + "cpuUsageInformation": "Utilisation CPU pour préparer les données en entrée et en sortie des modèles de détection. Cette valeur ne mesure pas l'utilisation de l'inférence, même si un GPU ou un accélérateur est utilisé." + }, + "hardwareInfo": { + "title": "Informations sur le matériel", + "gpuUsage": "Utilisation du GPU", + "gpuMemory": "Mémoire du GPU", + "gpuEncoder": "Encodeur GPU", + "gpuDecoder": "Décodeur GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Sortie Vainfo", + "returnCode": "Code de retour : {{code}}", + "processOutput": "Sortie du processus :", + "processError": "Erreur du processus :" + }, + "nvidiaSMIOutput": { + "title": "Sortie Nvidia SMI", + "name": "Nom : {{name}}", + "cudaComputerCapability": "Capacité de calcul CUDA : {{cuda_compute}}", + "vbios": "Informations VBios : {{vbios}}", + "driver": "Pilote : {{driver}}" + }, + "copyInfo": { + "label": "Copier les informations du GPU" + }, + "toast": { + "success": "Informations GPU copiées dans le presse-papiers" + }, + "closeInfo": { + "label": "Fermer les informations du GPU" + } + }, + "npuUsage": "Utilisation NPU", + "npuMemory": "Mémoire NPU", + "intelGpuWarning": { + "title": "Avertissement relatif aux statistiques du GPU Intel", + "message": "Statistiques du GPU non disponibles", + "description": "Il s'agit d'un bug connu de l'outil de statistiques GPU d'Intel (intel_gpu_top) : il peut afficher à tort une utilisation de 0 %, même lorsque l'accélération matérielle et la détection d'objets fonctionnent correctement sur l'iGPU. Ce problème ne vient pas de Frigate. Vous pouvez redémarrer l'hôte pour rétablir temporairement l'affichage et confirmer le fonctionnement du GPU. Les performances ne sont pas affectées." + }, + "gpuTemperature": "Température du GPU", + "npuTemperature": "Température du NPU", + "gpuCompute": "Calcul GPU / Encodage" + }, + "otherProcesses": { + "title": "Autres processus", + "processCpuUsage": "Utilisation CPU du processus", + "processMemoryUsage": "Utilisation mémoire du processus", + "series": { + "go2rtc": "go2rtc", + "recording": "enregistrement", + "review_segment": "Segment d'activité", + "embeddings": "intégrations", + "audio_detector": "détecteur audio" + } + } + }, + "storage": { + "title": "Stockage", + "recordings": { + "title": "Enregistrements", + "earliestRecording": "Enregistrement le plus ancien :", + "tips": "Cette valeur correspond au stockage total utilisé par les enregistrements dans la base de données Frigate. Frigate ne suit pas l'utilisation du stockage pour tous les fichiers de votre disque." + }, + "cameraStorage": { + "title": "Stockage de la caméra", + "bandwidth": "Bande passante", + "unused": { + "title": "Inutilisé", + "tips": "Cette valeur peut ne pas représenter précisément l'espace libre disponible pour Frigate si d'autres fichiers sont stockés sur votre disque en plus des enregistrements Frigate. Frigate ne suit pas l'utilisation du stockage en dehors de ses enregistrements." + }, + "percentageOfTotalUsed": "Pourcentage du total", + "storageUsed": "Stockage", + "camera": "Caméra", + "unusedStorageInformation": "Informations sur le stockage non utilisé" + }, + "overview": "Vue d'ensemble", + "shm": { + "title": "Allocation de mémoire partagée SHM", + "warning": "La taille actuelle de la SHM de {{total}} Mo est trop petite. Augmentez-la au moins à {{min_shm}} Mo.", + "frameLifetime": { + "title": "Durée de vie de la trame", + "description": "Chaque caméra dispose de {{frames}} emplacements de trames en mémoire partagée. À la fréquence d'images de la caméra la plus rapide, chaque trame est disponible pendant environ {{lifetime}}s avant d'être écrasée." + } + } + }, + "cameras": { + "title": "Caméras", + "info": { + "cameraProbeInfo": "Informations de la sonde pour {{camera}}", + "fetching": "Récupération des données de la caméra en cours", + "stream": "Flux {{idx}}", + "fps": "IPS :", + "unknown": "Inconnu", + "audio": "Audio :", + "tips": { + "title": "Informations de la sonde caméra" + }, + "streamDataFromFFPROBE": "Les données du flux sont obtenues avec ffprobe.", + "resolution": "Résolution :", + "error": "Erreur : {{error}}", + "codec": "Codec :", + "video": "Vidéo :", + "aspectRatio": "rapport d'aspect", + "keyframes": { + "title": "Analyse par image clef", + "analyzing": "Analyse des images clefs... {{seconds}} secondes restantes", + "stillAnalyzing": "Analyse des images clefs toujours en cours ...", + "recordStream": "Flux d'enregistrement :", + "keyframeCount": "Images clefs observées :", + "observedDuration": "Durée observée :", + "gap": "Écart en images clefs (min / moy / max) :", + "segmentLength": "Durée du segment d'enregistrement :", + "ok": "Images clefs chaque ~{{seconds}}s, convient à l'enregistrement et au visionnement.", + "warning": "Images clefs fragmentées ou variables (plus grand écart d'environ {{seconds}}s), vraisemblablement un codec intelligent (H.264+/H.265+), non-recommendé.", + "error": "L'écart en images clefs (~{{seconds}}) dépasse la longueur du segment d'enregistrement ({{segmentTime}}). Certains segments n'ont peut-être pas de clef, ce qui corrompt la lecture. Désactiver le codec intelligent/+ sur la caméra ou raccourcir son intervalle en images clefs.", + "unknown": "Impossible de déterminer l'espacement en images clefs.", + "recordDisabled": "L'enregistrement est désactivé pour cette caméra." + } + }, + "framesAndDetections": "Images / Détections", + "label": { + "camera": "caméra", + "detect": "détection", + "skipped": "ignorées", + "ffmpeg": "FFmpeg", + "capture": "capture", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraSkippedDetectionsPerSecond": "{{camName}} détections ignorées par seconde", + "overallDetectionsPerSecond": "Moyenne de détections par seconde", + "overallFramesPerSecond": "images par seconde (global)", + "overallSkippedDetectionsPerSecond": "Moyenne de détections ignorées par seconde", + "cameraCapture": "{{camName}} capture", + "cameraDetect": "{{camName}} détection", + "cameraFramesPerSecond": "{{camName}} images par seconde", + "cameraDetectionsPerSecond": "{{camName}} détections par seconde", + "cameraGpu": "GPU {{camName}}" + }, + "overview": "Vue d'ensemble", + "toast": { + "success": { + "copyToClipboard": "Données de la sonde copiées dans le presse-papiers." + }, + "error": { + "unableToProbeCamera": "Impossible d'interroger la caméra : {{errorMessage}}" + } + }, + "connectionQuality": { + "title": "Qualité de la connexion", + "excellent": "Excellente", + "fair": "Acceptable", + "poor": "Médiocre", + "unusable": "Inutilisable", + "fps": "IPS", + "expectedFps": "IPS attendues", + "reconnectsLastHour": "Reconnexions (dernière heure)", + "stallsLastHour": "Baisses de qualité (dernière heure)" + }, + "noCameras": { + "title": "Aucune caméra trouvée" + } + }, + "lastRefreshed": "Dernier rafraichissement : ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} a un taux élevé d'utilisation processeur par FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} : charge CPU détection élevée ({{detectAvg}}%)", + "healthy": "Le système est sain", + "reindexingEmbeddings": "Réindexation des embeddings ({{processed}} % terminée)", + "cameraIsOffline": "{{camera}} est hors ligne", + "detectIsSlow": "{{detect}} est lent ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} est très lent ({{speed}} ms)", + "shmTooLow": "L'allocation /dev/shm ({{total}} Mo) devrait être augmentée à au moins {{min}} Mo.", + "debugReplayActive": "Session de relecture de débogage active" + }, + "enrichments": { + "title": "Enrichissements", + "infPerSecond": "Inférences par seconde", + "embeddings": { + "face_embedding_speed": "Vitesse de vectorisation des visages", + "text_embedding_speed": "Vitesse d'embedding de texte", + "image_embedding_speed": "Vitesse d'embedding d'image", + "plate_recognition_speed": "Vitesse de reconnaissance des plaques d'immatriculation", + "face_recognition_speed": "Vitesse de reconnaissance faciale", + "plate_recognition": "Reconnaissance de plaques d'immatriculation", + "image_embedding": "Embedding d'image", + "yolov9_plate_detection": "Détection de plaques d'immatriculation YOLOv9", + "face_recognition": "Reconnaissance faciale", + "text_embedding": "Vitesse d'embedding de visage", + "yolov9_plate_detection_speed": "Vitesse de détection de plaques d'immatriculation YOLOv9", + "review_description": "Description de l'activité", + "review_description_speed": "Vitesse de description des activités", + "review_description_events_per_second": "Description de l'activité", + "object_description": "Description de l'objet", + "object_description_speed": "Vitesse de la description d'objet", + "object_description_events_per_second": "Description de l'objet", + "classification": "Classification {{name}}", + "classification_speed": "Vitesse de classification {{name}}", + "classification_events_per_second": "Événements de classification par seconde {{name}}" + }, + "averageInf": "Temps d'inférence moyen" + } +} diff --git a/web/public/locales/gl/audio.json b/web/public/locales/gl/audio.json new file mode 100644 index 0000000..507de04 --- /dev/null +++ b/web/public/locales/gl/audio.json @@ -0,0 +1,19 @@ +{ + "speech": "Fala", + "babbling": "Balbuxo", + "bicycle": "Bicicleta", + "yell": "Berro", + "car": "Coche", + "crying": "Chorando", + "sigh": "Suspiro", + "singing": "Cantando", + "motorcycle": "Motocicleta", + "bus": "Bus", + "train": "Tren", + "boat": "Bote", + "bird": "Paxaro", + "cat": "Gato", + "bellow": "Abaixo", + "whoop": "Ei carballeira", + "whispering": "Murmurando" +} diff --git a/web/public/locales/gl/common.json b/web/public/locales/gl/common.json new file mode 100644 index 0000000..61b9ce5 --- /dev/null +++ b/web/public/locales/gl/common.json @@ -0,0 +1,14 @@ +{ + "time": { + "untilForTime": "Até {{time}}", + "untilForRestart": "Até que se reinicie Frigate.", + "justNow": "Xusto agora", + "last7": "Últimos 7 días", + "last14": "Últimos 14 días", + "thisWeek": "Esta semana", + "today": "Hoxe", + "untilRestart": "Ata o reinicio", + "ago": "Fai {{timeAgo}}" + }, + "readTheDocumentation": "Ler a documentación" +} diff --git a/web/public/locales/gl/components/auth.json b/web/public/locales/gl/components/auth.json new file mode 100644 index 0000000..8b0857d --- /dev/null +++ b/web/public/locales/gl/components/auth.json @@ -0,0 +1,13 @@ +{ + "form": { + "user": "Usuario/a", + "password": "Contrasinal", + "errors": { + "passwordRequired": "Contrasinal obrigatorio", + "unknownError": "Erro descoñecido. Revisa os logs.", + "usernameRequired": "Usuario/a obrigatorio", + "rateLimit": "Excedido o límite. Téntao de novo despois." + }, + "login": "Iniciar sesión" + } +} diff --git a/web/public/locales/gl/components/camera.json b/web/public/locales/gl/components/camera.json new file mode 100644 index 0000000..166eebe --- /dev/null +++ b/web/public/locales/gl/components/camera.json @@ -0,0 +1,20 @@ +{ + "group": { + "label": "Grupos de cámaras", + "add": "Engadir Grupo de cámaras", + "delete": { + "confirm": { + "title": "Confirma o borrado", + "desc": "Seguro/a que queres borrar o Grupo de cámaras {{name}}?" + }, + "label": "Borrar o Grupo de Cámaras" + }, + "name": { + "placeholder": "Introduce un nome…", + "errorMessage": { + "nameMustNotPeriod": "Grupo de Cámaras non debe conter un punto." + } + }, + "edit": "Editar o Grupo de Cámaras" + } +} diff --git a/web/public/locales/gl/components/dialog.json b/web/public/locales/gl/components/dialog.json new file mode 100644 index 0000000..d2aff40 --- /dev/null +++ b/web/public/locales/gl/components/dialog.json @@ -0,0 +1,24 @@ +{ + "restart": { + "title": "Estás seguro/a que queres reiniciar Frigate?", + "button": "Reiniciar", + "restarting": { + "button": "Forzar reinicio", + "content": "Esta páxina recargarase en {{countdown}} segundos.", + "title": "Frigate está Reiniciando" + } + }, + "explore": { + "plus": { + "review": { + "question": { + "label": "Confirma esta etiqueta para Frigate Plus", + "ask_an": "E isto un obxecto {{label}}?" + } + }, + "submitToPlus": { + "label": "Enviar a Frigate+" + } + } + } +} diff --git a/web/public/locales/gl/components/filter.json b/web/public/locales/gl/components/filter.json new file mode 100644 index 0000000..8ef5f8f --- /dev/null +++ b/web/public/locales/gl/components/filter.json @@ -0,0 +1,17 @@ +{ + "filter": "Filtrar", + "labels": { + "label": "Etiquetas", + "count_one": "{{count}} Etiqueta", + "all": { + "short": "Etiquetas", + "title": "Todas as Etiquetas" + }, + "count_other": "{{count}} Etiquetas" + }, + "zones": { + "all": { + "title": "Tódalas zonas" + } + } +} diff --git a/web/public/locales/gl/components/icons.json b/web/public/locales/gl/components/icons.json new file mode 100644 index 0000000..73100bc --- /dev/null +++ b/web/public/locales/gl/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecciona unha icona", + "search": { + "placeholder": "Pesquisar unha icona…" + } + } +} diff --git a/web/public/locales/gl/components/input.json b/web/public/locales/gl/components/input.json new file mode 100644 index 0000000..c230e54 --- /dev/null +++ b/web/public/locales/gl/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Descargar vídeo", + "toast": { + "success": "O teu vídeo de revisión comezou a descargarse." + } + } + } +} diff --git a/web/public/locales/gl/components/player.json b/web/public/locales/gl/components/player.json new file mode 100644 index 0000000..89bce7f --- /dev/null +++ b/web/public/locales/gl/components/player.json @@ -0,0 +1,14 @@ +{ + "noRecordingsFoundForThisTime": "Non se atoparon grabacións para ese período", + "noPreviewFound": "Non se atopou previsualización", + "submitFrigatePlus": { + "submit": "Enviar", + "title": "Enviar este frame a Frigate+?" + }, + "stats": { + "streamType": { + "title": "Tipo de emisión:" + } + }, + "noPreviewFoundFor": "Vista Previa non atopada para {{cameraName}}" +} diff --git a/web/public/locales/gl/config/cameras.json b/web/public/locales/gl/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/gl/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/gl/config/global.json b/web/public/locales/gl/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/gl/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/gl/config/groups.json b/web/public/locales/gl/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/gl/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/gl/config/validation.json b/web/public/locales/gl/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/gl/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/gl/objects.json b/web/public/locales/gl/objects.json new file mode 100644 index 0000000..60c5408 --- /dev/null +++ b/web/public/locales/gl/objects.json @@ -0,0 +1,18 @@ +{ + "person": "Persoa", + "bicycle": "Bicicleta", + "airplane": "Avión", + "motorcycle": "Motocicleta", + "bus": "Bus", + "train": "Tren", + "boat": "Bote", + "traffic_light": "Luces de tráfico", + "fire_hydrant": "Boca de incendio", + "street_sign": "Sinal de tráfico", + "stop_sign": "Sinal de Stop", + "parking_meter": "Parquímetro", + "bench": "Banco", + "bird": "Paxaro", + "cat": "Gato", + "car": "Coche" +} diff --git a/web/public/locales/gl/views/chat.json b/web/public/locales/gl/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/gl/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/gl/views/classificationModel.json b/web/public/locales/gl/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/gl/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/gl/views/configEditor.json b/web/public/locales/gl/views/configEditor.json new file mode 100644 index 0000000..0d84b1a --- /dev/null +++ b/web/public/locales/gl/views/configEditor.json @@ -0,0 +1,12 @@ +{ + "documentTitle": "Editor de configuración - Frigate", + "configEditor": "Editor de Preferencias", + "saveOnly": "Só gardar", + "toast": { + "error": { + "savingError": "Erro gardando configuración" + } + }, + "saveAndRestart": "Gardar e Reiniciar", + "copyConfig": "Copiar Configuración" +} diff --git a/web/public/locales/gl/views/events.json b/web/public/locales/gl/views/events.json new file mode 100644 index 0000000..56da9d9 --- /dev/null +++ b/web/public/locales/gl/views/events.json @@ -0,0 +1,13 @@ +{ + "alerts": "Alertas", + "detections": "Deteccións", + "allCameras": "Tódalas cámaras", + "timeline.aria": "Selecciona liña de tempo", + "motion": { + "only": "Só movemento", + "label": "Movemento" + }, + "empty": { + "alert": "Non hai alertas que revisar" + } +} diff --git a/web/public/locales/gl/views/explore.json b/web/public/locales/gl/views/explore.json new file mode 100644 index 0000000..6d381d8 --- /dev/null +++ b/web/public/locales/gl/views/explore.json @@ -0,0 +1,12 @@ +{ + "documentTitle": "Explorar - Frigate", + "generativeAI": "IA xenerativa", + "exploreMore": "Explorar máis obxectos {{label}}", + "exploreIsUnavailable": { + "title": "Explorar non está Dispoñible", + "embeddingsReindexing": { + "finishingShortly": "Rematando ceo", + "startingUp": "Comezando…" + } + } +} diff --git a/web/public/locales/gl/views/exports.json b/web/public/locales/gl/views/exports.json new file mode 100644 index 0000000..0b99666 --- /dev/null +++ b/web/public/locales/gl/views/exports.json @@ -0,0 +1,10 @@ +{ + "documentTitle": "Exportar - Frigate", + "search": "Pesquisar", + "deleteExport.desc": "Seguro que queres borrar {{exportName}}?", + "editExport": { + "saveExport": "Garda exportación" + }, + "deleteExport": "Borrar exportación", + "noExports": "Non se atoparon exportacións" +} diff --git a/web/public/locales/gl/views/faceLibrary.json b/web/public/locales/gl/views/faceLibrary.json new file mode 100644 index 0000000..d98ab1c --- /dev/null +++ b/web/public/locales/gl/views/faceLibrary.json @@ -0,0 +1,11 @@ +{ + "description": { + "addFace": "Navegar para engadir unha nova colección á Libraría de Caras.", + "placeholder": "Introduce un nome para esta colección", + "invalidName": "Nome non válido. Os nomes só poden incluír letras, números, espazos, apóstrofes, guións baixos e guións." + }, + "details": { + "unknown": "Descoñecido", + "person": "Persoa" + } +} diff --git a/web/public/locales/gl/views/live.json b/web/public/locales/gl/views/live.json new file mode 100644 index 0000000..4ae0e6a --- /dev/null +++ b/web/public/locales/gl/views/live.json @@ -0,0 +1,19 @@ +{ + "documentTitle": "Directo - Frigate", + "documentTitle.withCamera": "{{camera}} - Directo - Frigate", + "twoWayTalk": { + "disable": "Deshabilita a Conversa de dous sentidos", + "enable": "Habilitar a Conversa de dous sentidos" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Pincha no frame para centrar a cámara" + } + } + }, + "cameraAudio": { + "enable": "Habilitar Audio de cámara" + }, + "lowBandwidthMode": "Modo de Baixa Banda Ancha" +} diff --git a/web/public/locales/gl/views/motionSearch.json b/web/public/locales/gl/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/gl/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/gl/views/recording.json b/web/public/locales/gl/views/recording.json new file mode 100644 index 0000000..26a3ed2 --- /dev/null +++ b/web/public/locales/gl/views/recording.json @@ -0,0 +1,11 @@ +{ + "filter": "Filtrar", + "export": "Exportar", + "calendar": "Calendario", + "toast": { + "error": { + "noValidTimeSelected": "Rango de tempo inválido" + } + }, + "filters": "Filtros" +} diff --git a/web/public/locales/gl/views/replay.json b/web/public/locales/gl/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/gl/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/gl/views/search.json b/web/public/locales/gl/views/search.json new file mode 100644 index 0000000..3a90cf0 --- /dev/null +++ b/web/public/locales/gl/views/search.json @@ -0,0 +1,15 @@ +{ + "search": "Pesquisar", + "savedSearches": "Pesquisas gardadas", + "button": { + "save": "Gardar pesquisa", + "filterActive": "Filtros activos", + "clear": "Borrar pesquisa" + }, + "filter": { + "label": { + "cameras": "Cámaras" + } + }, + "searchFor": "Procurar por {{inputValue}}" +} diff --git a/web/public/locales/gl/views/settings.json b/web/public/locales/gl/views/settings.json new file mode 100644 index 0000000..6a68c2c --- /dev/null +++ b/web/public/locales/gl/views/settings.json @@ -0,0 +1,11 @@ +{ + "documentTitle": { + "default": "Preferencias - Frigate", + "authentication": "Configuracións de Autenticación - Frigate", + "camera": "Configuracións da Cámara - Frigate", + "general": "Configuracións xerais - Frigate", + "notifications": "Configuración de Notificacións - Frigate", + "enrichments": "Configuración complementarias - Frigate", + "masksAndZones": "Editor de máscaras e zonas - Frigate" + } +} diff --git a/web/public/locales/gl/views/system.json b/web/public/locales/gl/views/system.json new file mode 100644 index 0000000..55c595b --- /dev/null +++ b/web/public/locales/gl/views/system.json @@ -0,0 +1,17 @@ +{ + "documentTitle": { + "cameras": "Estatísticas de cámaras - Frigate", + "storage": "Estatísticas de Almacenamento - Frigate", + "general": "Estatísticas Xerais - Frigate", + "enrichments": "Estatísticas complementarias - Frigate", + "logs": { + "frigate": "Rexistros de Frigate - Frigate" + } + }, + "title": "Sistema", + "logs": { + "download": { + "label": "Descargar logs" + } + } +} diff --git a/web/public/locales/he/audio.json b/web/public/locales/he/audio.json new file mode 100644 index 0000000..711a8d3 --- /dev/null +++ b/web/public/locales/he/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "דיבור", + "babbling": "ממלמל", + "yell": "לצעוק", + "bellow": "מתחת", + "whoop": "יבבה", + "whispering": "לוחש", + "crying": "בוכה", + "sigh": "אנחה", + "singing": "שר", + "choir": "מקהלה", + "yodeling": "יודלינג", + "chant": "לשיר", + "mantra": "מנטרה", + "child_singing": "ילד שר", + "synthetic_singing": "שירה סינתטית", + "rapping": "ראפ", + "humming": "זמזום", + "groan": "אנקה", + "grunt": "לנחור", + "whistling": "לשרוק", + "breathing": "נשימה", + "wheeze": "גניחה", + "snoring": "נחירה", + "gasp": "להתנשף", + "pant": "להתנשם", + "snort": "שאיפה", + "cough": "שיעול", + "throat_clearing": "גרגור גרון", + "sneeze": "עיטוש", + "sniff": "לרחרח", + "run": "רץ", + "snicker": "לצחקק", + "laughter": "צחוק", + "organ": "אורגן", + "shuffle": "ערבוב", + "footsteps": "צעדים", + "chewing": "לְעִיסָה", + "biting": "נשיכה", + "gargling": "גרגור", + "stomach_rumble": "קרקור בטן", + "burping": "גיהוק", + "hiccup": "שיהוק", + "fart": "פלוץ", + "hands": "ידיים", + "finger_snapping": "לחיצה באצבעות", + "clapping": "מחיאת כף", + "dog": "כלב", + "bark": "נביחה", + "cat": "חתול", + "horse": "סוס", + "sheep": "כבשה", + "goat": "עז", + "pigeon": "יונה", + "bird": "ציפור", + "coo": "קו", + "crow": "עורב", + "caw": "קאו", + "owl": "ינשוף", + "hoot": "צפירה.", + "flapping_wings": "כנפיים מתנפנפות", + "dogs": "כלבים", + "rats": "חולדות", + "mouse": "עכבר", + "patter": "תבנית", + "insect": "חרק", + "cricket": "קריקט", + "mosquito": "יתוש", + "fly": "זבוב", + "buzz": "זמזם.", + "frog": "צפרדע", + "croak": "קִרקוּר", + "snake": "נחש", + "rattle": "טרטור", + "whale_vocalization": "קולות לוויתן", + "music": "מוזיקה", + "musical_instrument": "כלי נגינה", + "plucked_string_instrument": "כלי מיתר פריטה", + "guitar": "גיטרה", + "electric_guitar": "גיטרה חשמלית", + "bass_guitar": "גיטרה בס", + "acoustic_guitar": "גיטרה אקוסטית", + "steel_guitar": "גיטרה פלדה", + "tapping": "להקיש", + "strum": "פריטה", + "banjo": "בנג'ו", + "sitar": "סיטאר", + "mandolin": "מנדולינה", + "zither": "צִיתָר", + "ukulele": "יוקליילי", + "keyboard": "לוח מקשים", + "piano": "פסנתר", + "electric_piano": "פסנתר חשמלי", + "electronic_organ": "אורגן חשמלי", + "hammond_organ": "עוגב המונד", + "synthesizer": "סינתיסייזר", + "sampler": "דוגם", + "harpsichord": "צֶ'מבָּלוֹ", + "percussion": "הַקָשָׁה", + "boat": "סירה", + "car": "מכונית", + "motorcycle": "אופנוע", + "bus": "אוטובוס", + "bicycle": "אופניים", + "train": "אימון", + "skateboard": "סקייטבורד", + "camera": "מצלמה", + "howl": "יללה", + "bow_wow": "באו וואו", + "growling": "נהמה", + "whimper_dog": "יבבת כלבים", + "purr": "לגרגר", + "meow": "מיאו", + "hiss": "לחישה", + "caterwaul": "קטרוואל", + "livestock": "בעלי חיים", + "clip_clop": "קליפ קלופ", + "neigh": "צהלה", + "cattle": "בקר", + "door": "דלת", + "heartbeat": "דופק", + "heart_murmur": "אוושת לב", + "applause": "תשואות", + "chatter": "פטפוטים", + "crowd": "קהל", + "children_playing": "ילדים משחקים", + "animal": "חיה", + "pets": "חיות מחמד", + "cheering": "תשואות", + "yip": "ייפ", + "moo": "מוו", + "cowbell": "פעמון פרה", + "pig": "חזיר", + "oink": "אוינק", + "bleat": "פעייה", + "fowl": "עוף.", + "chicken": "עוף", + "cluck": "קרקור", + "cock_a_doodle_doo": "קוק-א-דודל-דו", + "turkey": "הודו", + "gobble": "זלילה", + "duck": "ברווז", + "quack": "קוואק", + "goose": "אווז", + "honk": "צפירה", + "wild_animals": "חיית פרא", + "roaring_cats": "חתולים שואגים", + "roar": "שאגה", + "chirp": "ציוץ", + "squawk": "צווחה", + "drum_kit": "מערכת תופים", + "drum_machine": "מכונת תופים", + "drum": "תופים", + "snare_drum": "תוף סנר", + "rimshot": "רימשוט", + "drum_roll": "תוף רול", + "bass_drum": "תופים בס", + "timpani": "טימפאני", + "tabla": "טבלה", + "cymbal": "מצילה", + "hi_hat": "היי-האט", + "wood_block": "בול עץ", + "tambourine": "טמבורין", + "maraca": "מרקה", + "gong": "גונג", + "tubular_bells": "פעמונים צינוריים", + "mallet_percussion": "כלי הקשה מסוג פטיש", + "marimba": "מרימבה", + "glockenspiel": "גלוקנשפיל", + "vibraphone": "ויברפון", + "steelpan": "פלדה-פאן", + "orchestra": "תזמורת", + "brass_instrument": "כלי נשיפה ממתכת", + "french_horn": "צופר צרפתי", + "trumpet": "חצוצרה", + "trombone": "טרומבון", + "bowed_string_instrument": "כלי קשת", + "string_section": "מקטע מחרוזות", + "violin": "כינור", + "pizzicato": "פיציקטו", + "cello": "צ'לו", + "double_bass": "בס כפול", + "wind_instrument": "כלי נשיפה", + "flute": "חליל", + "saxophone": "סקסופון", + "clarinet": "קלרינט", + "harp": "נבל", + "bell": "פעמון", + "church_bell": "פעמון כנסיה", + "jingle_bell": "ג'ינגל בל", + "bicycle_bell": "פעמון אופניים", + "chime": "צִלצוּל", + "wind_chime": "פעמון רוח", + "harmonica": "הרמוניקה", + "accordion": "אקורדיון", + "bagpipes": "חלילים", + "didgeridoo": "דיג'רידו", + "theremin": "תרמין", + "singing_bowl": "קערת שירה", + "scratching": "גירוד", + "pop_music": "מוזיקת פופ", + "hip_hop_music": "מוזיקת היפ הופ", + "beatboxing": "ביטבוקסינג", + "rock_music": "מוזיקת רוק", + "heavy_metal": "מיטל כבד", + "punk_rock": "מוזיקת פאנק", + "grunge": "גראנג'", + "progressive_rock": "רוק פרוגרסיב", + "rock_and_roll": "רוקנרול", + "psychedelic_rock": "רוק פסיכדלי", + "rhythm_and_blues": "רית'ם אנד בלוז", + "soul_music": "מוזיקת סול", + "reggae": "רגיי", + "country": "קאונטרי", + "swing_music": "מוזיקת סווינג", + "bluegrass": "בלוגראס", + "funk": "פאנק", + "folk_music": "מוזיקת פולק", + "middle_eastern_music": "מוזיקה ים תיכונית", + "opera": "אופרה", + "jazz": "ג'אז", + "disco": "דיסקו", + "classical_music": "מוזיקה קלאסית", + "electronic_music": "מוזיקה אלקטרונית", + "house_music": "מוזיקת האוס", + "techno": "טכנו", + "dubstep": "דאבסטפ", + "drum_and_bass": "דראם אנד בס", + "electronica": "אלקטרוניקה", + "electronic_dance_music": "מוזיקת ריקוד אלקטרונית", + "ambient_music": "מוזיקת אמביינט", + "flamenco": "פלמנקו", + "trance_music": "מוזיקת טראנס", + "music_of_latin_america": "מוזיקה לטינית", + "salsa_music": "מוזיקת סלסה", + "blues": "בלוז", + "music_for_children": "מוזיקת ילדים", + "new-age_music": "מוזיקת ניו אייג'", + "vocal_music": "מוזיקה ווקאלית", + "a_capella": "קאפלה", + "music_of_africa": "מוזיקה אפריקאית", + "afrobeat": "אפרוביט", + "christian_music": "מוזיקה נוצרית", + "gospel_music": "מוזיקת גוספל", + "music_of_asia": "מוזיקה אסייתית", + "carnatic_music": "מוזיקה קרנטית", + "music_of_bollywood": "מוזיקה בוליווד", + "ska": "סקא", + "traditional_music": "מוזיקה מסורתית", + "independent_music": "מחיקת המשתמש נכשלה: {{errorMessage}}", + "song": "שיר", + "background_music": "מוזיקת רקע", + "theme_music": "מוזיקת נושא", + "jingle": "ג'ינגל", + "soundtrack_music": "פסקול מוזיקה", + "lullaby": "שיר ערש", + "video_game_music": "מוזיקת משחקי וידיאו", + "christmas_music": "מוזיקת קריסמיס", + "dance_music": "מוזיקת ריקודים", + "wedding_music": "מוזיקת חתונות", + "happy_music": "מוזיקה שמחה", + "sad_music": "מוזיקה עצובה", + "tender_music": "מוזיקה עדינה", + "exciting_music": "מוזיקה מרגשת", + "angry_music": "מוזיקה כועסת", + "scary_music": "מוזיקה מפחידה", + "wind": "רוח", + "rustling_leaves": "רשרוש עלים", + "wind_noise": "רעש רוח", + "thunderstorm": "סופת רעמים", + "thunder": "רעם", + "water": "מים", + "rain": "גשם", + "rain_on_surface": "גשם על פני השטח", + "stream": "שידור", + "vehicle": "רכב", + "waterfall": "מפל", + "ocean": "ים", + "waves": "גלים", + "steam": "קיטור", + "gurgling": "גרגור", + "fire": "אש", + "crackle": "פיצוח", + "sailboat": "מפרשית", + "rowboat": "סירת משוטים", + "motorboat": "סירת מנוע", + "ship": "ספינה", + "motor_vehicle": "רכב ממונע", + "toot": "תקיעה", + "car_alarm": "אזעקת רכב", + "power_windows": "חלונות חשמליים", + "skidding": "החלקה", + "tire_squeal": "חריקת צמיגים", + "car_passing_by": "מכונית חולפת", + "race_car": "מכונית מרוץ", + "truck": "משאית", + "air_brake": "בלם אוויר", + "air_horn": "צופר אוויר", + "reversing_beeps": "צליל רוורס", + "ice_cream_truck": "אוטו גלידה", + "emergency_vehicle": "רכב חירום", + "police_car": "רכב משטרה", + "ambulance": "אמבולנס", + "fire_engine": "כבאית", + "traffic_noise": "רעש תנועה", + "rail_transport": "רכבת נוסעים", + "train_whistle": "שריקת רכבת", + "train_horn": "צופר רכבת", + "railroad_car": "קרון רכבת", + "train_wheels_squealing": "חריקת גלגלי הרכבת", + "subway": "רכבת תחתית", + "aircraft": "כלי טיס", + "aircraft_engine": "מנוע כלי טיס", + "jet_engine": "מנוע סילון", + "propeller": "פרופלור", + "helicopter": "מסוק", + "fixed-wing_aircraft": "מטוסים בעלי כנף קבועה", + "light_engine": "מנוע קל", + "engine": "מנוע", + "dental_drill's_drill": "מקדחת שיניים", + "lawn_mower": "מכסחת דשא", + "chainsaw": "מסור שרשרת", + "medium_engine": "מנוע בינוני", + "heavy_engine": "מנוע כבד", + "engine_knocking": "דפיקות מנוע", + "engine_starting": "מנוע מוצת", + "idling": "התבטלות", + "sink": "כיור", + "blender": "מערבל", + "accelerating": "מאיץ", + "doorbell": "פעמון דלת", + "ding-dong": "דינג דונג", + "sliding_door": "דלתות הזזה", + "knock": "דפיקה בדלת", + "tap": "הקשה", + "squeak": "חריקה", + "cupboard_open_or_close": "פתיחה או סגירה של ארון", + "cutlery": "סכו\"ם", + "chopping": "קצוץ", + "frying": "טיגון", + "microwave_oven": "מיקרוגל", + "water_tap": "ברז מים", + "bathtub": "אמבטיה", + "dishes": "מנות", + "scissors": "מספריים", + "toothbrush": "מברשת שיניים", + "toilet_flush": "הורדת מים לאסלה", + "electric_toothbrush": "מברשת שיניים חשמלית", + "vacuum_cleaner": "שואב אבק", + "zipper": "רוכסן", + "coin": "מטבע", + "shuffling_cards": "ערבוב קלפים", + "typing": "הקלדה", + "typewriter": "מכונת כתיבה", + "computer_keyboard": "מקלדת מחשב", + "writing": "כתיבה", + "telephone_bell_ringing": "צלצול טלפון", + "ringtone": "צלצול", + "clock": "שעון", + "telephone_dialing": "טלפון מחייג", + "dial_tone": "צליל חיוג", + "busy_signal": "צליל תפוס", + "alarm_clock": "שעון מעורר", + "siren": "סירנה", + "civil_defense_siren": "סירנה של ההגנה האזרחית", + "buzzer": "זמזם", + "smoke_detector": "גלאי עשן", + "fire_alarm": "אזעקת אש", + "foghorn": "צופר ערפל", + "whistle": "שריקה", + "steam_whistle": "שריקת קיטור", + "mechanisms": "מכניקה", + "ratchet": "מַחגֵר", + "tick": "טיק", + "tick-tock": "טיק טוק", + "gears": "הילוכים", + "pulleys": "גלגלות", + "sewing_machine": "מְכוֹנַת תְפִירָה", + "mechanical_fan": "מאוורר מכני", + "air_conditioning": "מיזוג אוויר", + "cash_register": "קוּפָּה רוֹשֶׁמֶת", + "printer": "מדפסת", + "single-lens_reflex_camera": "מצלמת רפלקס עם עדשה יחידה", + "tools": "כלים", + "hammer": "פטיש", + "jackhammer": "פטיש אוויר", + "sawing": "מסור", + "filing": "הגשה", + "sanding": "שיוף", + "power_tool": "כלי חשמלי", + "drill": "מקדחה", + "explosion": "התפוצצות", + "gunshot": "יריה", + "machine_gun": "מכונת יריה", + "fusillade": "פוסילה", + "artillery_fire": "אש ארטילרית", + "cap_gun": "Cap Gun", + "fireworks": "זיקוקים", + "firecracker": "חזיז", + "burst": "הִתפָּרְצוּת", + "eruption": "התפרצות", + "boom": "בום", + "wood": "עץ", + "chop": "לקצוץ", + "splinter": "קיסם", + "crack": "סדק", + "glass": "זכוכית", + "chink": "סדוק", + "shatter": "ניפוץ", + "silence": "השתקה", + "sound_effect": "אפקט קול", + "environmental_noise": "רעש סביבתי", + "static": "סטטי", + "white_noise": "רעש לבן", + "pink_noise": "רעש ורוד", + "radio": "רדיו", + "field_recording": "רישום שדה", + "scream": "צרחה", + "drawer_open_or_close": "מגירה פתוחה או סגורה", + "alarm": "אזעקה", + "television": "טלוויזיה", + "electric_shaver": "מכונת גילוח חשמלית", + "keys_jangling": "צלצול מפתחות", + "hair_dryer": "מייבש שיער", + "slam": "טריקה", + "telephone": "טלפון", + "tuning_fork": "מזלג כוונון", + "raindrop": "טיפות גשם", + "smash": "רסק", + "boiling": "רותח", + "sonar": "סונר", + "arrow": "חץ", + "whack": "מַהֲלוּמָה", + "sine_wave": "גל סינוס", + "harmonic": "הרמוניה", + "chirp_tone": "צליל ציוץ", + "pulse": "דוֹפֶק", + "inside": "בְּתוֹך", + "outside": "בחוץ", + "reverberation": "הִדהוּד", + "echo": "הד", + "noise": "רעש", + "mains_hum": "זמזום ראשי", + "distortion": "סַלְפָנוּת", + "sidetone": "צליל צדדי", + "cacophony": "קָקוֹפוֹניָה", + "throbbing": "פְּעִימָה", + "vibration": "רֶטֶט", + "sodeling": "מיזוג", + "change_ringing": "שינוי צלצול", + "shofar": "שופר", + "liquid": "נוזל", + "splash": "התזה", + "slosh": "שכשוך", + "squish": "מעיכה", + "drip": "טפטוף", + "pour": "לִשְׁפּוֹך", + "trickle": "לְטַפטֵף", + "gush": "פֶּרֶץ", + "fill": "מילוי", + "spray": "ריסוס", + "pump": "משאבה", + "stir": "בחישה", + "whoosh": "מהיר", + "thump": "חֲבָטָה", + "thunk": "תרועה", + "electronic_tuner": "מכוון אלקטרוני", + "effects_unit": "יחידת אפקטים", + "chorus_effect": "אפקט מקהלה", + "basketball_bounce": "קפיצת כדורסל", + "bang": "לִדפּוֹק", + "slap": "סְטִירָה", + "breaking": "שְׁבִירָה", + "bouncing": "הַקפָּצָה", + "whip": "שׁוֹט", + "flap": "מַדָף", + "scratch": "לְגַרֵד", + "scrape": "סריקה", + "rub": "שפשוף", + "roll": "גלגול", + "crushing": "מעיכה", + "crumpling": "קימוט", + "tearing": "קריעה", + "beep": "ביפ", + "ping": "פינג", + "ding": "דינג", + "clang": "צלצול מתכתי", + "squeal": "חריקה", + "creak": "חריקה", + "rustle": "רשרוש", + "whir": "זמזום", + "clatter": "רעש נקישות", + "chird": "Chird", + "sizzle": "צליל חריכה", + "clicking": "נקישות", + "clickety_clack": "נקישות רצופות", + "rumble": "רעם נמוך", + "plop": "פלופ", + "hum": "המהום", + "zing": "זמזום חד", + "boing": "בּוֹאִינְג (צליל קפיצי / אלסטי)", + "crunch": "חריקה / פיצוח" +} diff --git a/web/public/locales/he/common.json b/web/public/locales/he/common.json new file mode 100644 index 0000000..d20ee8b --- /dev/null +++ b/web/public/locales/he/common.json @@ -0,0 +1,307 @@ +{ + "time": { + "justNow": "כעת", + "untilForRestart": "עד לאתחול של Frigate.", + "untilRestart": "עד לאתחול", + "ago": "לפני {{timeAgo}}", + "today": "היום", + "untilForTime": "עד:{{time}}", + "last30": "30 ימים אחרונים", + "last14": "14 ימים אחרונים", + "year_one": "{{time}} שנה", + "year_two": "{{time}} שנים", + "year_other": "{{time}} שנים", + "last7": "7 ימים אחרונים", + "lastMonth": "חודש שעבר", + "10minutes": "10 דקות", + "yesterday": "אתמול", + "thisWeek": "השבוע", + "lastWeek": "שבוע שעבר", + "1hour": "שעה", + "12hours": "12 שעות", + "24hours": "24 שעות", + "pm": "pm", + "am": "am", + "yr": "{{time}}שנה", + "mo": "{{time}}חודש", + "d": "{{time}}יום", + "month_one": "{{time}} חודש", + "month_two": "{{time}} חודשים", + "month_other": "{{time}} חודשים", + "h": "{{time}}שעה", + "day_one": "{{time}} יום", + "day_two": "{{time}} ימים", + "day_other": "{{time}} ימים", + "hour_one": "{{time}} שעה", + "hour_two": "{{time}} שעות", + "hour_other": "{{time}} שעות", + "m": "{{time}}דקות", + "minute_one": "{{time}} דקה", + "minute_two": "{{time}} דקות", + "minute_other": "{{time}} דקות", + "s": "{{time}}שניה", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "second_one": "{{time}} שניה", + "second_two": "{{time}} שניות", + "second_other": "{{time}} שניות", + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "h:mm:ss aaa" + }, + "5minutes": "5 דקות", + "formattedTimestampMonthDayYear": { + "24hour": "MMM d, yyyy", + "12hour": "MMM d, yyyy" + }, + "30minutes": "30 דקות", + "thisMonth": "החודש", + "inProgress": "בתהליך", + "invalidStartTime": "זמן התחלה לא תקין", + "invalidEndTime": "זמן סיום לא תקין", + "never": "אף פעם" + }, + "unit": { + "speed": { + "kph": "קמ\"ש", + "mph": "מייל לשעה" + }, + "length": { + "feet": "רגל", + "meters": "מטרים" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hour", + "mbph": "MB/hour", + "gbph": "GB/hour" + } + }, + "label": { + "back": "אחורה", + "hide": "הסתר {{item}}", + "show": "הצג {{item}}", + "ID": "ID", + "none": "ללא", + "all": "הכל", + "other": "אחר" + }, + "button": { + "apply": "החל", + "reset": "איפוס", + "done": "בוצע", + "enabled": "מאופשר", + "enable": "אפשר", + "disabled": "מבוטל", + "disable": "בטל", + "save": "שמירה", + "saving": "שומר…", + "back": "חזרה", + "close": "סגירה", + "cancel": "ביטול", + "copy": "העתקה", + "twoWayTalk": "דיבור דו כיווני", + "exitFullscreen": "יציאה ממסך מלא", + "pictureInPicture": "תמונה בתוך תמונה", + "cameraAudio": "קול ממצלמה", + "off": "כבוי", + "edit": "עריכה", + "copyCoordinates": "העתקת קואורדינטות", + "delete": "מחיקה", + "yes": "כן", + "no": "לא", + "suspended": "מושהה", + "unsuspended": "ביטול השהייה", + "play": "ניגון", + "unselect": "בטל בחירה", + "export": "ייצוא", + "deleteNow": "מחיקה כעת", + "fullscreen": "מסך מלא", + "history": "היסטוריה", + "on": "פעיל", + "download": "הורדה", + "info": "מידע", + "next": "הבא", + "continue": "המשך", + "add": "להוסיף" + }, + "menu": { + "system": "מערכת", + "systemMetrics": "מדדי מערכת", + "configuration": "תצורת מערכת", + "systemLogs": "לוגים מערכת", + "settings": "הגדרות", + "configurationEditor": "עריכת תצורה", + "languages": "שפות", + "language": { + "en": "English (English)", + "es": "ספרדית", + "fr": "צרפתית", + "ar": "ערבית", + "pt": "פורטוגזית", + "it": "איטלקית", + "nl": "הולנדית", + "sv": "שוודית", + "cs": "צ'כית", + "nb": "נורווגית", + "ko": "קוריאנית", + "vi": "ויטנאמית", + "fa": "פרסית", + "pl": "פולנית", + "uk": "אוקראינית", + "he": "עברית", + "el": "יוונית", + "ro": "רומנית", + "hu": "הונגרית", + "fi": "פינית", + "da": "דנית", + "withSystem": { + "label": "השתמש בהגדרות המערכת עבור השפה" + }, + "sk": "סלובקית", + "th": "תאילנדית", + "zhCN": "סינית פשוטה", + "tr": "טורקית", + "hi": "הודית", + "ru": "רוסית", + "ja": "יפנית", + "de": "גרמנית", + "yue": "קנטונזית", + "ca": "קטלה (קטלאנית)", + "ptBR": "פורטוגזית - ברזיל", + "sr": "סרבית", + "sl": "סלובנית", + "lt": "ליטאית", + "bg": "בולגרית", + "gl": "Galego", + "id": "אינדונזית", + "ur": "اردو" + }, + "appearance": "מראה", + "darkMode": { + "label": "מצב כהה", + "light": "בהיר", + "dark": "כהה", + "withSystem": { + "label": "השתמש בהגדרות המערכת עבור מצב בהיר או כהה" + } + }, + "withSystem": "מערכת", + "theme": { + "label": "ערכת נושא", + "blue": "כחול", + "green": "ירוק", + "nord": "נורד", + "red": "אדום", + "highcontrast": "ניגודיות גבוהה", + "default": "ברירת מחדל" + }, + "review": "סקירה", + "explore": "עיון", + "help": "עזרה", + "documentation": { + "title": "תיעוד", + "label": "תיעוד Frigate" + }, + "restart": "הפעלה מחדש", + "live": { + "title": "שידור חי", + "cameras": { + "title": "מצלמות", + "count_one": "{{count}} מצלמה", + "count_two": "{{count}} מצלמות", + "count_other": "{{count}} מצלמות" + }, + "allCameras": "כל המצלמות" + }, + "export": "ייצוא", + "uiPlayground": "ממשק משתמש", + "faceLibrary": "ספריית זיהוי פנים", + "user": { + "account": "חשבון", + "anonymous": "אנונימי", + "logout": "ניתוק", + "current": "משתמש מחובר: {{user}}", + "setPassword": "קביעת סיסמה", + "title": "משתמש" + }, + "classification": "סיווג" + }, + "toast": { + "copyUrlToClipboard": "כתובת האתר המועתקת.", + "save": { + "title": "שמירה", + "error": { + "noMessage": "שמירת שינויי התצורה נכשלה", + "title": "נכשל בניסיון לשמור את ההגדרות: {{errorMessage}}" + } + } + }, + "role": { + "title": "הרשאה", + "admin": "מנהל", + "viewer": "צופה", + "desc": "למנהלי מערכת יש גישה מלאה לכל התכונות בממשק המשתמש של Frigate. הצופים מוגבלים לצפייה במצלמות, סקירת פריטים וצילומים היסטוריים בממשק המשתמש." + }, + "pagination": { + "next": { + "title": "הבא", + "label": "עבור לדף הבא" + }, + "label": "דפדוף", + "previous": { + "title": "הקודם", + "label": "עבור לדף הקודם" + }, + "more": "דפים נוספים" + }, + "accessDenied": { + "title": "הגישה נדחתה", + "desc": "אין לך הרשאה לצפות בדף הזה.", + "documentTitle": "גישה נדחתה - Frigate" + }, + "notFound": { + "documentTitle": "לא נמצא - Frigate", + "title": "404", + "desc": "דף לא נמצא" + }, + "selectItem": "בחירה:{{item}}", + "readTheDocumentation": "קרא את התיעוד", + "list": { + "two": "{{0}} ו־{{1}}", + "many": "{{items}}, ו־{{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "אופציונלי", + "internalID": "המזהה הפנימי ש־Frigate משתמש בו בהגדרות ובמסד הנתונים" + }, + "information": { + "pixels": "{{area}}px" + } +} diff --git a/web/public/locales/he/components/auth.json b/web/public/locales/he/components/auth.json new file mode 100644 index 0000000..0f6caf3 --- /dev/null +++ b/web/public/locales/he/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "שם משתמש", + "password": "סיסמה", + "login": "התחברות", + "errors": { + "usernameRequired": "נדרש שם משתמש", + "passwordRequired": "דרושה סיסמה", + "unknownError": "שגיאה לא ידועה. בדוק את הלוגים.", + "webUnknownError": "שגיאה לא ידועה, בדוק את הלוגים.", + "rateLimit": "חרגת מהמגבלת בקשות. נסה שוב מאוחר יותר.", + "loginFailed": "ההתחברות נכשלה" + }, + "firstTimeLogin": "מתחבר בפעם הראשונה? פרטי ההתחברות מודפסים בלוגים של פריגייט." + } +} diff --git a/web/public/locales/he/components/camera.json b/web/public/locales/he/components/camera.json new file mode 100644 index 0000000..184b192 --- /dev/null +++ b/web/public/locales/he/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "קבוצת מצלמות", + "camera": { + "setting": { + "streamMethod": { + "method": { + "smartStreaming": { + "desc": "שידור חכם יעדכן את תמונת המצלמה פעם בדקה כאשר לא מתרחשת פעילות ניתנת לזיהוי כדי לחסוך רוחב פס ומשאבים. כאשר מזוהה פעילות, התמונה עוברת בצורה חלקה לשידור חי.", + "label": "שידור חכם (מומלץ)" + }, + "noStreaming": { + "label": "אין שידור", + "desc": "תמונות המצלמה יתעדכנו רק פעם בדקה ולא יתבצע שידור חי." + }, + "continuousStreaming": { + "label": "שידור רציף", + "desc": { + "title": "תמונת המצלמה תמיד תהיה שידור חי כאשר היא גלויה בדשבורד, גם אם לא זוהתה פעילות.", + "warning": "שידור רציף עלול לגרום לשימוש גבוה ברוחב פס ובעיות ביצועים. יש להשתמש בזהירות." + } + } + }, + "label": "שיטת שידור", + "placeholder": "בחירת שיטת שידור" + }, + "title": "הגדרות סטרימינג של {{cameraName}}", + "label": "הגדרות זרם מצלמה", + "desc": "שנה את אפשרויות הסטרימינג בשידור חי עבור לוח המחוונים של קבוצת מצלמות זו. הגדרות אלו ספציפיות למכשיר/דפדפן.", + "audioIsAvailable": "קול זמין עבור שידור זה", + "audioIsUnavailable": "קול לא זמין לזרם זה", + "audio": { + "tips": { + "title": "יש להפיק קול מהמצלמה שלך ולהגדיר אותו ב-go2rtc עבור שידור זה.", + "document": "קרא את התיעוד " + } + }, + "stream": "זרם", + "placeholder": "בחירת זרם", + "compatibilityMode": { + "label": "מצב תאימות", + "desc": "הפעל אפשרות זו רק אם השידור החי של המצלמה שלך מציג עיוותים בצבע ויש לו קו אלכסוני בצד ימין של התמונה." + } + }, + "birdseye": "מבט על" + }, + "edit": "ערכית קבוצת מצלמות", + "delete": { + "label": "מחיקת קבוצת מצלמות", + "confirm": { + "title": "אישור מחיקה", + "desc": "האם אתה בטוח שברצונך למחוק את קבוצת המצלמות {{name}}?" + } + }, + "name": { + "label": "שם", + "placeholder": "הכנס שם…", + "errorMessage": { + "mustLeastCharacters": "שם קבוצת המצלמות חייב להיות בן 2 תווים לפחות.", + "exists": "שם קבוצת המצלמות כבר קיים.", + "nameMustNotPeriod": "שם קבוצת המצלמות אינו יכול להכיל נקודה.", + "invalid": "שם קבוצת מצלמות לא חוקי." + } + }, + "cameras": { + "label": "מצלמות", + "desc": "בחירת מצלמות עבור קבוצה זו." + }, + "icon": "אייקון", + "success": "קבוצת המצלמות ({{name}}) נשמרה.", + "add": "הוספת קבוצת מצלמות" + }, + "debug": { + "options": { + "label": "הגדרות", + "title": "אפשרויות", + "showOptions": "הצג אפשרויות", + "hideOptions": "הסתר אפשרויות" + }, + "boundingBox": "תיבת זיהוי", + "zones": "אזורים", + "mask": "מיסוך", + "motion": "תנועה", + "timestamp": "חותמת זמן", + "regions": "אזורים" + } +} diff --git a/web/public/locales/he/components/dialog.json b/web/public/locales/he/components/dialog.json new file mode 100644 index 0000000..85353b9 --- /dev/null +++ b/web/public/locales/he/components/dialog.json @@ -0,0 +1,124 @@ +{ + "restart": { + "title": "האם אתה בטוח שברצונך להפעיל מחדש את Frigate?", + "button": "הפעלה מחדש - Frigate", + "restarting": { + "title": "Frigate מופעל מחדש כעת", + "content": "דף זה ייטען מחדש בעוד {{countdown}} שניות.", + "button": "אילוץ טעינה מחדש" + } + }, + "export": { + "toast": { + "error": { + "endTimeMustAfterStartTime": "שעת הסיום חייבת להיות אחרי שעת ההתחלה", + "failed": "נכשל בהתחלת הייצוא: {{error}}", + "noVaildTimeSelected": "לא נבחר טווח זמן תקף" + }, + "success": "הייצוא התחיל בהצלחה. ניתן לצפות בקובץ בעמוד הייצוא.", + "view": "תצוגה" + }, + "time": { + "end": { + "label": "בחירת זמן סיום", + "title": "זמן סיום" + }, + "fromTimeline": "בחירה מציר זמן", + "lastHour_one": "שעה אחרונה", + "lastHour_two": "שעות אחרונות {{count}}", + "lastHour_other": "שעות אחרונות {{count}}", + "custom": "מותאם אישית", + "start": { + "title": "זמן התחלה", + "label": "בחירת זמן התחלה" + } + }, + "selectOrExport": "בחירה או ייצוא", + "name": { + "placeholder": "תן שם לייצוא" + }, + "select": "בחירה", + "export": "ייצוא", + "fromTimeline": { + "saveExport": "שמירת ייצוא", + "previewExport": "תצוגה מקדימה של ייצוא" + } + }, + "streaming": { + "restreaming": { + "desc": { + "title": "הגדר את go2rtc לקבלת אפשרויות נוספות של תצוגה חיה ושמע עבור מצלמה זו.", + "readTheDocumentation": "קרא את התיעוד" + }, + "disabled": "הזרמה מחדש אינה פעילה עבור מצלמה זו." + }, + "label": "זרם", + "showStats": { + "label": "הצג סטטיסטיקות שידור", + "desc": "הפעל אפשרות זו כדי להציג סטטיסטיקות שידור כשכבת-על על פיד המצלמה." + }, + "debugView": "תצוגת ניפוי שגיאות" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "העלה ל- +Frigate", + "desc": "אובייקטים במיקומים שברצונך להימנע מהם אינם תוצאות חיוביות שגויות. הגשתם כתוצאות חיוביות שגויות תבלבל את המודל." + }, + "review": { + "question": { + "label": "אשר תווית זו עבור +Frigate", + "ask_a": "האם אובייקט זה הוא {{label}}?", + "ask_an": "האם אובייקט זה הוא - {{label}}?", + "ask_full": "האם אובייקט זה הוא {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "נשלח" + } + } + }, + "video": { + "viewInHistory": "צפה בהיסטוריה" + } + }, + "search": { + "saveSearch": { + "label": "שמירת חיפוש", + "desc": "ספק שם לחיפוש שמור זה.", + "placeholder": "הזן שם לחיפוש שלך", + "success": "החיפוש ({{searchName}}) נשמר.", + "button": { + "save": { + "label": "שמירת החיפוש הזה" + } + }, + "overwrite": "{{searchName}} כבר קיים. שמירה תדרוס את הערך הקיים." + } + }, + "recording": { + "confirmDelete": { + "title": "אישור מחיקה", + "desc": { + "selected": "האם אתה בטוח שברצונך למחוק את כל הסרטונים המוקלטים המשויכים לפריט סקירה זה?

    החזק את מקש Shift לחוץ כדי לעקוף תיבת דו-שיח זו בעתיד." + }, + "toast": { + "success": "קטעי וידאו המשויכים לפריטי הסקירה שנבחרו נמחקו בהצלחה.", + "error": "המחיקה נכשלה: {{error}}" + } + }, + "button": { + "export": "ייצוא", + "markAsReviewed": "סמן כסוקר", + "deleteNow": "מחיקה כעת", + "markAsUnreviewed": "סימון כלא נבדק" + } + }, + "imagePicker": { + "selectImage": "בחר תמונה ממוזערת של אובייקט במעקב", + "unknownLabel": "תמונת הטריגר נשמרה", + "search": { + "placeholder": "חיפוש לפי תווית או תווית משנה…" + }, + "noImages": "לא נמצאו תמונות ממוזערות עבור מצלמה זו" + } +} diff --git a/web/public/locales/he/components/filter.json b/web/public/locales/he/components/filter.json new file mode 100644 index 0000000..b11aba9 --- /dev/null +++ b/web/public/locales/he/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "מסנן", + "features": { + "submittedToFrigatePlus": { + "tips": "עליך תחילה לסנן לפי אובייקטים במעקב שיש להם תמונת מצב.

    לא ניתן לשלוח ל-Frigate+ אובייקטים במעקב ללא תמונת מצב.", + "label": "העלאה ל- +Frigate" + }, + "label": "תכונות", + "hasVideoClip": "קיים סרטון", + "hasSnapshot": "קיימת לכידת תמונה" + }, + "labels": { + "label": "תוויות", + "all": { + "title": "כל התוויות", + "short": "תוויות" + }, + "count_one": "{{count}} תווית", + "count_other": "{{count}} תוויות" + }, + "zones": { + "label": "איזורים", + "all": { + "title": "כל האזורים", + "short": "אזורים" + } + }, + "dates": { + "selectPreset": "בחר הגדרה…", + "all": { + "title": "כל התאריכים", + "short": "תאריכים" + } + }, + "more": "מסננים נוספים", + "reset": { + "label": "איפוס מסננים לערכי ברירת מחדל" + }, + "timeRange": "טווח זמן", + "subLabels": { + "label": "תוויות משנה", + "all": "כל תוויות המשנה" + }, + "score": "ציון", + "estimatedSpeed": "מהירות משוערת ({{unit}})", + "sort": { + "label": "מיון", + "dateAsc": "תאריך (עולה)", + "dateDesc": "תאריך (יורד)", + "scoreAsc": "ציון אובייקט (עולה)", + "scoreDesc": "ציון אובייקט (יורד)", + "speedAsc": "מהירות משוערת (עולה)", + "speedDesc": "מהירות משוערת (יורד)", + "relevance": "רלוונטיות" + }, + "cameras": { + "label": "מסנן מצלמות", + "all": { + "title": "כל המצלמות", + "short": "מצלמות" + } + }, + "review": { + "showReviewed": "הצג פריטים שנבדקו" + }, + "motion": { + "showMotionOnly": "הצגת תנועה בלבד" + }, + "explore": { + "settings": { + "title": "הגדרות", + "defaultView": { + "summary": "סיכום", + "unfilteredGrid": "טבלה לא מסוננת", + "title": "תצוגת ברירת מחדל", + "desc": "כאשר לא נבחרו מסננים, הצג סיכום של האובייקטים האחרונים שעברו מעקב לפי תווית, או הצג רשת לא מסוננת." + }, + "gridColumns": { + "title": "עמודות טבלה", + "desc": "בחר את מספר העמודות בטבלה." + }, + "searchSource": { + "label": "חיפוש במקור", + "desc": "בחר אם לחפש בתמונות הממוזערות או בתיאורים של האובייקטים שבמעקב.", + "options": { + "thumbnailImage": "תמונה ממוזערת", + "description": "תיאור" + } + } + }, + "date": { + "selectDateBy": { + "label": "בחר תאריך לפיו יבוצע הסינון" + } + } + }, + "trackedObjectDelete": { + "toast": { + "success": "אובייקטים במעקב נמחקו בהצלחה.", + "error": "מחיקת אובייקטים במעקב נכשלה: {{errorMessage}}" + }, + "title": "אישור מחיקה", + "desc": "מחיקת אובייקטים אלה ({{objectLength}}) שעברו מעקב מסירה את לכידת התמונה, כל ההטמעות שנשמרו וכל ערכי שלבי האובייקט המשויכים. קטעי וידאו מוקלטים של אובייקטים אלה שעברו מעקב בתצוגת היסטוריה לא יימחקו.

    האם אתה בטוח שברצונך להמשיך?

    החזק את מקש Shift כדי לעקוף תיבת דו-שיח זו בעתיד." + }, + "zoneMask": { + "filterBy": "סינון לפי מיסוך אזור" + }, + "recognizedLicensePlates": { + "title": "לוחיות רישוי מוכרות", + "loadFailed": "טעינת לוחיות הרישוי המזוהות נכשלה.", + "loading": "טוען לוחיות רישוי מזוהות…", + "placeholder": "הקלד כדי לחפש לוחיות רישוי…", + "noLicensePlatesFound": "לא נמצאו לוחיות רישוי.", + "selectPlatesFromList": "בחירת לוחית אחת או יותר מהרשימה.", + "selectAll": "בחר הכל", + "clearAll": "נקה הכל" + }, + "logSettings": { + "label": "סינון רמת לוג", + "filterBySeverity": "סנן לוגים לפי חוּמרָה", + "loading": { + "title": "טוען", + "desc": "כאשר חלונית הלוגים מגוללת לתחתית, לוגים חדשים מוזרמים אוטומטית עם הוספתם." + }, + "disableLogStreaming": "השבתת זרימה של לוגים", + "allLogs": "כל הלוגים" + }, + "classes": { + "label": "מחלקות", + "all": { + "title": "כל המחלקות" + }, + "count_one": "{{count}} מחלקה", + "count_other": "{{count}} מחלקות" + }, + "attributes": { + "label": "מאפייני סיווג", + "all": "כל המאפיינים" + } +} diff --git a/web/public/locales/he/components/icons.json b/web/public/locales/he/components/icons.json new file mode 100644 index 0000000..ecff965 --- /dev/null +++ b/web/public/locales/he/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "בחר סמל", + "search": { + "placeholder": "חפש אייקון…" + } + } +} diff --git a/web/public/locales/he/components/input.json b/web/public/locales/he/components/input.json new file mode 100644 index 0000000..e427f53 --- /dev/null +++ b/web/public/locales/he/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "הורד וידאו", + "toast": { + "success": "הורדת סרטון הסקירה שלך החלה." + } + } + } +} diff --git a/web/public/locales/he/components/player.json b/web/public/locales/he/components/player.json new file mode 100644 index 0000000..348cb1d --- /dev/null +++ b/web/public/locales/he/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "לא נמצאו הקלטות לזמן זה", + "stats": { + "droppedFrames": { + "title": "פריימים שנשמטו:", + "short": { + "title": "נשמטו", + "value": "{{droppedFrames}} פריימים" + } + }, + "streamType": { + "title": "סוג זרם:", + "short": "סוג" + }, + "bandwidth": { + "title": "רוחב-פס:", + "short": "רוחב-פס" + }, + "latency": { + "title": "השהיה:", + "value": "{{seconds}} שניות", + "short": { + "title": "השהיה", + "value": "{{seconds}} שניה" + } + }, + "decodedFrames": "פריימים מפוענחים:", + "droppedFrameRate": "קצב פריימים מופחת:", + "totalFrames": "סך כל המסגרות:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "הפריים נשלח בהצלחה ל-Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "שליחת הפריים ל-Frigate+ נכשלה" + } + }, + "noPreviewFound": "לא נמצאה תצוגה מקדימה", + "noPreviewFoundFor": "לא נמצאה תצוגה מקדימה עבור {{cameraName}}", + "submitFrigatePlus": { + "title": "לשלוח את הפריים הזה ל-Frigate+?", + "submit": "אישור" + }, + "livePlayerRequiredIOSVersion": "נדרשת iOS 17.1 ומעלה עבור סוג השידור החי הזה.", + "streamOffline": { + "title": "זרם מצלמה לא זמין", + "desc": "לא התקבלו פריימים בזרם detect של {{cameraName}}, בדקו את יומני השגיאות" + }, + "cameraDisabled": "המצלמה מושבתת" +} diff --git a/web/public/locales/he/config/cameras.json b/web/public/locales/he/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/he/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/he/config/global.json b/web/public/locales/he/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/he/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/he/config/groups.json b/web/public/locales/he/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/he/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/he/config/validation.json b/web/public/locales/he/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/he/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/he/objects.json b/web/public/locales/he/objects.json new file mode 100644 index 0000000..65f00b1 --- /dev/null +++ b/web/public/locales/he/objects.json @@ -0,0 +1,120 @@ +{ + "person": "אדם", + "bicycle": "אופניים", + "car": "מכונית", + "motorcycle": "אופנוע", + "airplane": "מטוס", + "bus": "אוטובוס", + "train": "אימון", + "boat": "סירה", + "traffic_light": "רמזור", + "fire_hydrant": "ברז כיבוי אש", + "street_sign": "שלט רחוב", + "stop_sign": "תמרור עצור", + "parking_meter": "מדחן חניה", + "bench": "ספסל", + "bird": "ציפור", + "cat": "חתול", + "dog": "כלב", + "horse": "סוס", + "sheep": "כבשה", + "cow": "פרה", + "elephant": "פיל", + "bear": "דב", + "zebra": "זברה", + "giraffe": "ג'ירפה", + "hat": "כובע", + "backpack": "תרמיל", + "umbrella": "מטריה", + "shoe": "נעל", + "eye_glasses": "משקפיים", + "handbag": "תיק יד", + "tie": "עניבה", + "suitcase": "מזוודה", + "frisbee": "צלחת מעופפת", + "skis": "מִגלָשַׁיִם", + "snowboard": "גלשן שלג", + "sports_ball": "כדור ספורט", + "kite": "עפיפון", + "baseball_bat": "כובע בייסבול", + "baseball_glove": "כפפת בייסבול", + "skateboard": "סקייטבורד", + "surfboard": "גלשן", + "bottle": "בקבוק", + "plate": "לוחית", + "wine_glass": "כוס יין", + "mouse": "עכבר", + "keyboard": "לוח מקשים", + "bark": "נביחה", + "fox": "שועל", + "goat": "עז", + "rabbit": "ארנב", + "raccoon": "רקון", + "robot_lawnmower": "מכסחת דשא רובוטית", + "waste_bin": "פח אשפה", + "on_demand": "לפי דרישה", + "face": "פנים", + "license_plate": "לוחית רישוי", + "package": "חבילה", + "bbq_grill": "מנגל", + "amazon": "אמזון", + "usps": "USPS", + "ups": "UPS", + "fedex": "פדקס", + "dhl": "די-אייצ'-אל", + "an_post": "אנפוסט", + "purolator": "פורולטור", + "postnl": "דואר הולנד", + "nzpost": "דואר ניוזילנד", + "postnord": "דואר נורד", + "gls": "G_L_S", + "dpd": "דיפידי", + "tennis_racket": "מחבט טניס", + "pizza": "פיצה", + "donut": "דונאט", + "cake": "עוגה", + "chair": "כיסא", + "couch": "ספה", + "potted_plant": "עציץ", + "bed": "מיטה", + "mirror": "מראה", + "dining_table": "שולחן אוכל", + "window": "חלון", + "desk": "שולחן", + "toilet": "שירותים", + "door": "דלת", + "tv": "טלויזיה", + "cup": "כוס", + "fork": "מזלג", + "knife": "סכין", + "spoon": "כף", + "bowl": "קערה", + "banana": "בננה", + "apple": "תפוח", + "sandwich": "כריך", + "orange": "תפוז", + "broccoli": "ברוקולי", + "carrot": "גזר", + "hot_dog": "נקניקייה", + "laptop": "מחשב נייד", + "remote": "שלט", + "cell_phone": "טלפון נייד", + "microwave": "מיקרוגל", + "oven": "תנור", + "toaster": "טוסטר", + "refrigerator": "מקרר", + "blender": "מערבל", + "book": "ספר", + "clock": "שעון", + "vase": "אגרטל", + "scissors": "מספריים", + "teddy_bear": "דובי", + "hair_dryer": "מייבש שיער", + "toothbrush": "מברשת שיניים", + "hair_brush": "מברשת שיער", + "vehicle": "רכב", + "animal": "חיה", + "squirrel": "סנאי", + "deer": "צבי", + "sink": "כיור" +} diff --git a/web/public/locales/he/views/chat.json b/web/public/locales/he/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/he/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/he/views/classificationModel.json b/web/public/locales/he/views/classificationModel.json new file mode 100644 index 0000000..ea08b0e --- /dev/null +++ b/web/public/locales/he/views/classificationModel.json @@ -0,0 +1,196 @@ +{ + "documentTitle": "מודלי סיווג - Frigate", + "details": { + "scoreInfo": "הציון מייצג את ממוצע רמת הביטחון של הסיווג, על פני כל הזיהויים של האובייקט הזה.", + "none": "ללא ערך", + "unknown": "לא ידוע" + }, + "button": { + "deleteClassificationAttempts": "מחיקת אוסף התמונות", + "renameCategory": "שינוי שם קטגוריה", + "deleteCategory": "מחיקת קטגוריה", + "deleteImages": "מחיקת תמונות", + "trainModel": "אימון מודל", + "addClassification": "הוספת סיווג", + "deleteModels": "מחיקת מודלים", + "editModel": "עריכת מודל" + }, + "tooltip": { + "trainingInProgress": "המודל נמצא כרגע בתהליך אימון", + "noNewImages": "אין תמונות חדשות לאימון. קודם סווג עוד תמונות במערך הנתונים (Dataset).", + "noChanges": "לא בוצעו שינויים במערך הנתונים מאז האימון האחרון.", + "modelNotReady": "המודל עדיין לא מוכן לאימון" + }, + "toast": { + "success": { + "deletedCategory_one": "הקטגוריה נמחקה", + "deletedCategory_two": "", + "deletedCategory_other": "", + "deletedImage_one": "התמונות נמחקו", + "deletedImage_two": "", + "deletedImage_other": "", + "deletedModel_one": "נמחק בהצלחה {{count}} מודל", + "deletedModel_two": "נמחקו בהצלחה {{count}} מודלים", + "deletedModel_other": "", + "categorizedImage": "התמונה סווגה בהצלחה", + "trainedModel": "המודל אומן בהצלחה.", + "trainingModel": "אימון המודל התחיל בהצלחה.", + "updatedModel": "תצורת המודל עודכנה בהצלחה.", + "renamedCategory": "שם הקטגוריה שונה בהצלחה ל־{{name}}" + }, + "error": { + "deleteImageFailed": "המחיקה נכשלה: {{errorMessage}}", + "deleteCategoryFailed": "מחיקת הקטגוריה נכשלה: {{errorMessage}}", + "deleteModelFailed": "מחיקת המודל נכשלה: {{errorMessage}}", + "categorizeFailed": "סיווג התמונה נכשל: {{errorMessage}}", + "trainingFailed": "אימון המודל נכשל. בדוק בלוגים של Frigate לפרטים.", + "trainingFailedToStart": "הפעלת אימון המודל נכשלה: {{errorMessage}}", + "updateModelFailed": "עדכון המודל נכשל: {{errorMessage}}", + "renameCategoryFailed": "שינוי שם הקטגוריה נכשל: {{errorMessage}}" + } + }, + "train": { + "titleShort": "לאחרונה", + "title": "סיווגים אחרונים", + "aria": "בחר סיווגים אחרונים" + }, + "deleteCategory": { + "title": "מחיקת קטגוריה", + "desc": "האם אתה בטוח שברצונך למחוק את הקטגוריה {{name}}? פעולה זו תמחק לצמיתות את כל התמונות המשויכות, ותדרוש אימון מחדש של המודל.", + "minClassesTitle": "לא ניתן למחוק את הקטגוריה", + "minClassesDesc": "מודל סיווג חייב לכלול לפחות 2 קטגוריות. הוסף קטגוריה נוספת לפני שתמחק את הקטגוריה הזו." + }, + "deleteModel": { + "title": "מחיקת מודל סיווג", + "single": "האם אתה בטוח שברצונך למחוק את {{name}}? פעולה זו תמחק לצמיתות את כל הנתונים המשויכים, כולל תמונות ונתוני אימון. לא ניתן לבטל פעולה זו.", + "desc_one": "האם אתה בטוח שברצונך למחוק מודל אחד ({{count}})? פעולה זו תמחק לצמיתות את כל הנתונים המשויכים, כולל תמונות ונתוני אימון. לא ניתן לבטל פעולה זו.", + "desc_two": "האם אתה בטוח שברצונך למחוק {{count}} מודלים? פעולה זו תמחק לצמיתות את כל הנתונים המשויכים, כולל תמונות ונתוני אימון. לא ניתן לבטל פעולה זו.", + "desc_other": "" + }, + "edit": { + "title": "עריכת מודל סיווג", + "descriptionState": "ערוך את הקטגוריות של מודל הסיווג הזה. כל שינוי ידרוש אימון מחדש של המודל.", + "descriptionObject": "ערוך את סוג האובייקט ואת סוג הסיווג עבור מודל סיווג האובייקטים הזה.", + "stateClassesInfo": "הערה: שינוי קטגוריות המצבים מחייב אימון מחדש של המודל עם הקטגוריות המעודכנות." + }, + "deleteDatasetImages": { + "title": "מחיקת תמונות מערך הנתונים", + "desc_one": "האם אתה בטוח שברצונך למחוק {{count}} תמונה מתוך {{dataset}}? לא ניתן לבטל פעולה זו, והיא תדרוש אימון מחדש של המודל.", + "desc_two": "האם אתה בטוח שברצונך למחוק {{count}} תמונות מתוך {{dataset}}? לא ניתן לבטל פעולה זו, והיא תדרוש אימון מחדש של המודל.", + "desc_other": "" + }, + "deleteTrainImages": { + "title": "מחיקת תמונות אימון", + "desc_one": "האם אתה בטוח שברצונך למחוק {{count}} תמונה? לא ניתן לבטל פעולה זו.", + "desc_two": "האם אתה בטוח שברצונך למחוק {{count}} תמונות? לא ניתן לבטל פעולה זו.", + "desc_other": "" + }, + "renameCategory": { + "title": "שינוי שם קטגוריה", + "desc": "הזן שם חדש עבור {{name}}. יהיה עליך לאמן מחדש את המודל כדי שהשינוי בשם ייכנס לתוקף." + }, + "description": { + "invalidName": "שם לא תקין. שמות יכולים לכלול רק אותיות, מספרים, רווחים, גרש (’), קו תחתון (_) ומקף (-)." + }, + "categories": "קטגוריות", + "createCategory": { + "new": "יצירת קטגוריה חדשה" + }, + "wizard": { + "step3": { + "errors": { + "noObjectLabel": "לא נבחרה תווית אובייקט", + "generateFailed": "יצירת דוגמאות נכשלה: {{error}}", + "generationFailed": "היצירה נכשלה. נסה שוב.", + "classifyFailed": "סיווג התמונות נכשל: {{error}}", + "noCameras": "לא הוגדרו מצלמות" + }, + "generateSuccess": "תמונות לדוגמה נוצרו בהצלחה", + "missingStatesWarning": { + "title": "חסרות דוגמאות מצב", + "description": "מומלץ לבחור דוגמאות לכל המצבים כדי לקבל את התוצאות הטובות ביותר. אפשר להמשיך גם בלי לבחור את כל המצבים, אבל המודל לא יאומן עד שלכל המצבים יהיו תמונות.\nאחרי שתמשיך, השתמש בתצוגת סיווגים אחרונים כדי לסווג תמונות למצבים החסרים, ואז בצע אימון מודל." + }, + "training": { + "title": "אימון מודל", + "description": "המודל שלך נמצא כעת בתהליך אימון ברקע. אפשר לסגור את החלון הזה, והמודל יתחיל לפעול מיד לאחר סיום האימון." + }, + "classifying": "מסווג ומאמן...", + "trainingStarted": "האימון התחיל בהצלחה", + "modelCreated": "המודל נוצר בהצלחה. השתמש בתצוגת סיווגים אחרונים כדי להוסיף תמונות למצבים חסרים, ולאחר מכן אמן את המודל.", + "selectImagesPrompt": "בחר את כל התמונות עם: {{className}}", + "selectImagesDescription": "לחץ על תמונות כדי לבחור אותן. לחץ על המשך כשתסיים עם מחלקה זו.", + "allImagesRequired_one": "אנא סווג את כל התמונות. נותרה {{count}} תמונה.", + "allImagesRequired_two": "אנא סווג את כל התמונות. נותרו {{count}} תמונות.", + "allImagesRequired_other": "", + "generating": { + "title": "יוצר תמונות לדוגמה", + "description": "Frigate שואב תמונות מייצגות מההקלטות שלך. פעולה זו עשויה להימשך מספר רגעים..." + }, + "retryGenerate": "נסה ליצור מחדש", + "noImages": "לא נוצרו תמונות לדוגמה" + }, + "title": "צור סיווג חדש", + "steps": { + "nameAndDefine": "תן שם והגדר", + "stateArea": "אזור מצב", + "chooseExamples": "בחר דוגמאות" + }, + "step1": { + "description": "מודלי מצבים מנטרים אזורים קבועים במצלמה ומזהים בהם שינויי מצב (למשל: דלת פתוחה/סגורה). מודלי אובייקטים מוסיפים סיווגים לאובייקטים שזוהו (למשל: בעלי חיים מוכרים, שליחים, וכד׳).", + "name": "שם", + "namePlaceholder": "הזן שם למודל...", + "type": "סוג", + "typeState": "מצב", + "typeObject": "אובייקט", + "objectLabel": "תווית אובייקט", + "objectLabelPlaceholder": "בחר סוג אובייקט...", + "classificationType": "סוג סיווג", + "classificationTypeTip": "למד על סוגי הסיווגים", + "classificationTypeDesc": "תוויות משנה (Sub Labels) מוסיפות טקסט נוסף לתווית האובייקט (למשל: 'Person: UPS'). מאפיינים (Attributes) הם מטא־נתונים שניתנים לחיפוש, הנשמרים בנפרד בתוך מטא־הנתונים של האובייקט.", + "classificationSubLabel": "תווית משנה", + "classificationAttribute": "מאפיינים", + "classes": "מחלקות", + "states": "מצבים", + "classesTip": "למד על מחלקות", + "classesStateDesc": "הגדר את המצבים השונים שבהם אזור המצלמה יכול להיות. לדוגמה: 'open' ו־'closed' עבור דלת מוסך.", + "classesObjectDesc": "הגדר את הקטגוריות השונות לסיווג אובייקטים שזוהו. לדוגמה:\n'delivery_person', 'resident', 'stranger' עבור סיווג אנשים.", + "classPlaceholder": "הזן שם מחלקה...", + "errors": { + "nameRequired": "שם מודל הוא שדה חובה", + "nameLength": "שם המודל חייב להיות באורך של עד 64 תווים", + "nameOnlyNumbers": "שם המודל אינו יכול להכיל מספרים בלבד", + "classRequired": "נדרשת לפחות מחלקה אחת", + "classesUnique": "שמות המחלקות חייבים להיות ייחודיים", + "noneNotAllowed": "המחלקה 'none' אינה מותרת", + "stateRequiresTwoClasses": "מודלי מצבים דורשים לפחות שתי מחלקות", + "objectLabelRequired": "אנא בחר תווית אובייקט", + "objectTypeRequired": "אנא בחר סוג סיווג" + } + }, + "step2": { + "description": "בחר מצלמות והגדר את האזור לניטור עבור כל מצלמה. המודל יסווג את מצב האזורים הללו.", + "cameras": "מצלמות", + "selectCamera": "בחר מצלמה", + "noCameras": "לחץ על ‎+‎ כדי להוסיף מצלמות", + "selectCameraPrompt": "בחר מצלמה מהרשימה כדי להגדיר את אזור הניטור שלה" + } + }, + "categorizeImageAs": "סווג תמונה כ־:", + "categorizeImage": "סווג תמונה", + "menu": { + "objects": "אובייקטים", + "states": "מצבים" + }, + "noModels": { + "object": { + "title": "אין מודלים לסיווג אובייקטים", + "description": "צור מודל מותאם אישית לסיווג אובייקטים שזוהו.", + "buttonText": "צור מודל אובייקט" + }, + "state": { + "title": "אין מודלים לסיווג מצבים", + "description": "צור מודל מותאם אישית לניטור ולסיווג שינויים במצב באזורים מסוימים במצלמה.", + "buttonText": "צור מודל מצב" + } + } +} diff --git a/web/public/locales/he/views/configEditor.json b/web/public/locales/he/views/configEditor.json new file mode 100644 index 0000000..535619a --- /dev/null +++ b/web/public/locales/he/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "עורך הגדרות - Frigate", + "configEditor": "עורך תצורה", + "copyConfig": "העתקת הגדרות", + "saveAndRestart": "שמירה והפעלה מחדש", + "saveOnly": "שמירה בלבד", + "confirm": "לצאת ללא שמירה?", + "toast": { + "success": { + "copyToClipboard": "התצורה הועתקה ללוח." + }, + "error": { + "savingError": "שגיאה בשמירת ההגדרות" + } + }, + "safeConfigEditor": "עורך תצורה (מצב בטוח)", + "safeModeDescription": "Frigate במצב בטוח עקב שגיאת אימות הגדרות." +} diff --git a/web/public/locales/he/views/events.json b/web/public/locales/he/views/events.json new file mode 100644 index 0000000..636a073 --- /dev/null +++ b/web/public/locales/he/views/events.json @@ -0,0 +1,65 @@ +{ + "alerts": "התרעות", + "detections": "גילויים", + "motion": { + "label": "תנועה", + "only": "תנועה בלבד" + }, + "allCameras": "כל המצלמות", + "empty": { + "detection": "אין גילויים לבדיקה", + "alert": "אין התראות להצגה", + "motion": "לא נמצאו נתוני תנועה", + "recordingsDisabled": { + "title": "יש להפעיל הקלטות", + "description": "ניתן ליצור פריטי סקירה עבור מצלמה רק כאשר הקלטות מופעלות עבור אותה מצלמה." + } + }, + "timeline": "ציר זמן", + "timeline.aria": "בחירת ציר זמן", + "events": { + "label": "אירועים", + "aria": "בחירת אירועים", + "noFoundForTimePeriod": "לא נמצאו אירועים עבור תקופת זמן זו." + }, + "documentTitle": "סקירה - Frigate", + "recordings": { + "documentTitle": "הקלטות - Frigate" + }, + "calendarFilter": { + "last24Hours": "24 שעות אחרונות" + }, + "markAsReviewed": "סימון כנבדק", + "markTheseItemsAsReviewed": "סמן פריטים אלה כנסרקו", + "newReviewItems": { + "label": "הצג פריטי סקירה חדשים", + "button": "פריטים חדשים לסקירה" + }, + "selected_one": "נבחרו {{count}}", + "selected_other": "{{count}} נבחרו", + "camera": "מצלמה", + "detected": "זוהה", + "detail": { + "noDataFound": "אין נתונים מפורטים לבדיקה", + "aria": "הפעלה/כיבוי תצוגת פרטים", + "trackedObject_one": "אובייקט {{count}}", + "trackedObject_other": "{{count}} אובייקטים", + "noObjectDetailData": "אין נתוני אובייקט זמינים.", + "label": "פרטים", + "settings": "הגדרות תצוגת פרטים", + "alwaysExpandActive": { + "title": "תמיד להרחיב את הפעיל", + "desc": "כאשר אפשר, תמיד להציג בהרחבה את פרטי האובייקט של פריט הבדיקה הפעיל." + } + }, + "objectTrack": { + "trackedPoint": "נקודה במעקב", + "clickToSeek": "לחץ כדי לחפש את הזמן הזה" + }, + "zoomIn": "הגדל (זום פנימה)", + "zoomOut": "הקטן (זום החוצה)", + "select_all": "הכל", + "normalActivity": "רגיל", + "needsReview": "טעון בדיקה", + "securityConcern": "חשש אבטחה" +} diff --git a/web/public/locales/he/views/explore.json b/web/public/locales/he/views/explore.json new file mode 100644 index 0000000..6042b43 --- /dev/null +++ b/web/public/locales/he/views/explore.json @@ -0,0 +1,305 @@ +{ + "documentTitle": "גלה - פריגטה", + "itemMenu": { + "downloadVideo": { + "label": "הורדת וידיאו", + "aria": "הורדת וידיאו" + }, + "viewObjectLifecycle": { + "label": "צפה בשלבים של האובייקט", + "aria": "הצג את השלבים של האובייקט" + }, + "downloadSnapshot": { + "label": "הורדת לכידת תמונה", + "aria": "הורדת לכידת תמונה" + }, + "findSimilar": { + "label": "מצא דומה", + "aria": "מצא אובייקטים דומים במעקב" + }, + "submitToPlus": { + "label": "שלח ל- +Frigate", + "aria": "שלח ל +Frigate" + }, + "viewInHistory": { + "label": "צפה בהיסטוריה", + "aria": "צפה בהיסטוריה" + }, + "deleteTrackedObject": { + "label": "מחק את אובייקט המעקב הזה" + }, + "audioTranscription": { + "aria": "בקשת תמלול אודיו", + "label": "תמלל" + }, + "showObjectDetails": { + "label": "הצגת מסלול האובייקט" + }, + "hideObjectDetails": { + "label": "הסתרת מסלול האובייקט" + }, + "downloadCleanSnapshot": { + "label": "הורד תמונה נקיה", + "aria": "הורד תמונה נקיה" + }, + "viewTrackingDetails": { + "label": "הצג פרטי מעקב", + "aria": "הצג את פרטי המעקב" + }, + "addTrigger": { + "label": "הוסף טריגר", + "aria": "הוסף טריגר לאובייקט במעקב זה" + } + }, + "generativeAI": "Generative - AI", + "exploreMore": "סקור אובייקטים נוספים של {{label}}", + "exploreIsUnavailable": { + "title": "סקירה לא זמינה", + "embeddingsReindexing": { + "context": "ניתן להשתמש בסקירה לאחר סיום בניית המאגר של האובייקטים שזוהו.", + "startingUp": "מתחיל…", + "estimatedTime": "זמן משוער שנותר:", + "finishingShortly": "מסיים בקרוב", + "step": { + "thumbnailsEmbedded": "תמונות ממוזערות מוטמעות: ", + "descriptionsEmbedded": "תיאורים מוטמעים: ", + "trackedObjectsProcessed": "אובייקטים שעובדו לאחר מעקב: " + } + }, + "downloadingModels": { + "setup": { + "visionModel": "מודל חוזי", + "visionModelFeatureExtractor": "מחלץ תכונות מודל חוזי", + "textModel": "מודל טקסט", + "textTokenizer": "מפצל טקסט" + }, + "tips": { + "context": "מומלץ לעדכן את המאגר של האובייקטים אחרי שהמודלים ירדו.", + "documentation": "קרא את התיעוד" + }, + "error": "אירעה שגיאה. בדוק את הלוגים.", + "context": "Frigate מורידה את מודלי ההטמעה הדרושים כדי לתמוך בתכונת החיפוש הסמנטי. פעולה זו עשויה להימשך מספר דקות, בהתאם למהירות חיבור הרשת שלך." + } + }, + "trackedObjectDetails": "פרטי אובייקט במעקב", + "type": { + "details": "פרטים", + "snapshot": "לכידת תמונה", + "video": "וידיאו", + "object_lifecycle": "שלבי זיהוי של האובייקט", + "thumbnail": "תמונה ממוזערת", + "tracking_details": "פרטי מעקב" + }, + "objectLifecycle": { + "title": "שלבי זיהוי של האובייקט", + "noImageFound": "לא נמצאה תמונה עבור חותמת זמן זו.", + "createObjectMask": "יצירת מיסוך אובייקט", + "adjustAnnotationSettings": "שנה את הגדרות הסימון", + "scrollViewTips": "גלול כדי לצפות ברגעים המשמעותיים בשלבים של אובייקט זה.", + "count": "{{first}} מתוך{{second}}", + "trackedPoint": "נקודת מעקב", + "lifecycleItemDesc": { + "visible": "זוהה {{label}}", + "entered_zone": "{{label}} נכנס ל-{{zones}}", + "active": "{{label}} הפך לפעיל", + "stationary": "{{label}} הפך לנייח", + "attribute": { + "faceOrLicense_plate": "זוהה {{attribute}} עבור {{label}}", + "other": "{{label}} זוהה כ-{{attribute}}" + }, + "gone": "{{label}} שמאל", + "heard": "{{label}} נשמעה", + "external": "זוהה {{label}}", + "header": { + "zones": "אזורים", + "ratio": "יחס", + "area": "אזור" + } + }, + "annotationSettings": { + "title": "הגדרות סימון", + "showAllZones": { + "title": "הצג את כל האזורים", + "desc": "הצג תמיד אזורים בפריימים שבהם אובייקטים נכנסו לאזור." + }, + "offset": { + "label": "היסט ההערה", + "documentation": "עיין בתיעוד ", + "toast": { + "success": "קיזוז עבור {{camera}} נשמר בקובץ התצורה. הפעל מחדש את Frigate כדי להחיל את השינויים שלך." + }, + "tips": "טיפ: דמיינו סרטון אירוע שבו אדם הולך משמאל לימין. אם תיבת הגבול של ציר הזמן של האירוע נמצאת באופן עקבי משמאל לאדם, יש להפחית את הערך. באופן דומה, אם אדם הולך משמאל לימין והתיבה התוחמת נמצאת באופן עקבי לפני האדם, יש להגדיל את הערך.", + "millisecondsToOffset": "מספר מילישניות להסטת ההערות שנוצרו מזיהוי ברירת מחדל: 0", + "desc": "נתונים אלה מגיעים משידור הזיהוי של המצלמה שלך, אך הם מונחים על גבי תמונות משידור ההקלטה. לא סביר ששני הזרמים יהיו מסונכרנים לחלוטין. כתוצאה מכך, תיבת הגבול והצילומים לא מסונכרנים בצורה מושלמת. עם זאת, ניתן להשתמש בשדה היסט סימון כדי לסנכרן." + } + }, + "autoTrackingTips": "מיקומי תיבות הסימון לא יהיו מדויקים עבור מצלמות עם מעקב אוטומטי.", + "carousel": { + "previous": "שקופית קודמת", + "next": "שקופית הבאה" + } + }, + "details": { + "timestamp": "חותמת זמן", + "item": { + "tips": { + "mismatch_one": "זוהה אובייקט לא זמין ({{count}}) ונכלל בפריט סקירה זה. אובייקטים אלה לא עמדו בקריטריונים של התראה או זיהוי, או שכבר נוקו/נמחקו.", + "mismatch_two": "זוהו אובייקטים לא זמינים ({{count}}) ונכלל בפריט סקירה זה. אובייקטים אלה לא עמדו בקריטריונים של התראה או זיהוי, או שכבר נוקו/נמחקו.", + "mismatch_other": "זוהו אובייקטים לא זמינים ({{count}}) ונכלל בפריט סקירה זה. אובייקטים אלה לא עמדו בקריטריונים של התראה או זיהוי, או שכבר נוקו/נמחקו.", + "hasMissingObjects": "התאם את התצורה שלך אם ברצונך ש-Frigate ישמור אובייקטים שעוקבים אחריהם עבור התוויות הבאות: {{objects}}" + }, + "button": { + "viewInExplore": "הצג בסקירה", + "share": "שתף פריט זה" + }, + "toast": { + "success": { + "updatedSublabel": "תווית המשנה עודכנה בהצלחה.", + "updatedLPR": "לוחית הרישוי עודכנה בהצלחה.", + "regenerate": "תיאור חדש התבקש מ-{{provider}}. בהתאם למהירות הספק שלך, ייתכן שייקח זמן מה ליצירת התיאור החדש.", + "updatedAttributes": "המאפיינים עודכנו בהצלחה.", + "audioTranscription": "בקשת תמלול האודיו נשלחה בהצלחה. בהתאם למהירות שרת ה־Frigate שלך, התמלול עשוי להימשך זמן מה עד להשלמתו." + }, + "error": { + "regenerate": "ההתקשרות ל-{{provider}} לקבלת תיאור חדש נכשלה: {{errorMessage}}", + "updatedSublabelFailed": "עדכון תווית המשנה נכשל: {{errorMessage}}", + "updatedLPRFailed": "עדכון לוחית הרישוי נכשל: {{errorMessage}}", + "updatedAttributesFailed": "נכשל בעדכון המאפיינים: {{errorMessage}}", + "audioTranscription": "נכשל בשליחת בקשה לתמלול אודיו: {{errorMessage}}" + } + }, + "title": "סקירת הפריט", + "desc": "סקירת הפריט" + }, + "label": "תווית", + "editSubLabel": { + "title": "עריכת תווית משנה", + "desc": "הזן תווית משנה חדשה עבור {{label}}", + "descNoLabel": "הזן תווית משנה חדשה עבור אובייקט המעקב" + }, + "editLPR": { + "title": "עריכת לוחית זיהוי", + "desc": "הזן ערך לוחית רישוי חדשה עבור {{label}}", + "descNoLabel": "הזן ערך לוחית רישוי חדשה עבור האובייקט במעקב" + }, + "snapshotScore": { + "label": "ציון לכידת תמונה" + }, + "topScore": { + "label": "ציון גבוה", + "info": "הציון הגבוה ביותר הוא הציון החציוני הגבוה ביותר עבור האובייקט במעקב, כך שהוא עשוי להיות שונה מהציון המוצג בתמונה הממוזערת של תוצאת החיפוש." + }, + "recognizedLicensePlate": "לוחית רישוי מוכרת", + "estimatedSpeed": "מהירות משוערת", + "objects": "אובייקט", + "camera": "מצלמה", + "button": { + "findSimilar": "מצא דומה", + "regenerate": { + "title": "צור מחדש", + "label": "צור מחדש את תיאור אובייקט המעקב" + } + }, + "description": { + "label": "תיאור", + "placeholder": "תיאור האובייקט במעקב", + "aiTips": "Frigate לא תבקש תיאור מספק הבינה המלאכותית הגנרטיבית שלך עד לסיום כל השלבים של האובייקט במעקב." + }, + "expandRegenerationMenu": "פתח את תפריט היצירה מחדש", + "regenerateFromSnapshot": "צור מחדש מלכידת התמונה", + "regenerateFromThumbnails": "צור מחדש מתמונות ממוזערות", + "tips": { + "descriptionSaved": "התיאור נשמר בהצלחה", + "saveDescriptionFailed": "עדכון התיאור נכשל: {{errorMessage}}" + }, + "zones": "אזורים", + "editAttributes": { + "title": "ערוך מאפיינים", + "desc": "בחר מאפייני סיווג עבור {{label}} זה" + }, + "score": { + "label": "ציון" + }, + "attributes": "מאפייני סיווג", + "title": { + "label": "כותרת" + } + }, + "dialog": { + "confirmDelete": { + "title": "אישור מחיקה", + "desc": "מחיקת אובייקט זה במעקב תסיר את הצילום, כל ה־embeddings השמורים וכל רשומות פרטי המעקב המשויכות. קטעי וידאו מוקלטים של אובייקט זה בתצוגת היסטוריה לא יימחקו.

    האם אתה בטוח שברצונך להמשיך?" + } + }, + "searchResult": { + "tooltip": "תואם ל-{{type}} ב-{{confidence}}%", + "deleteTrackedObject": { + "toast": { + "error": "מחיקת האובייקט במעקב נכשלה: {{errorMessage}}", + "success": "האובייקט המעקב נמחק בהצלחה." + } + }, + "previousTrackedObject": "האובייקט הקודם במעקב", + "nextTrackedObject": "האובייקט הבא במעקב" + }, + "noTrackedObjects": "לא נמצאו אובייקטים במעקב", + "fetchingTrackedObjectsFailed": "שגיאה באחזור אובייקטים במעקב: {{errorMessage}}", + "trackedObjectsCount_one": "אובייקט במעקב ({{count}}) ", + "trackedObjectsCount_two": "אובייקטים במעקב ({{count}}) ", + "trackedObjectsCount_other": "אובייקטים במעקב ({{count}}) ", + "trackingDetails": { + "title": "פרטי מעקב", + "noImageFound": "לא נמצאה תמונה עבור חותמת הזמן הזו.", + "createObjectMask": "יצירת מסכת אובייקט", + "adjustAnnotationSettings": "התאמת הגדרות הסימון", + "scrollViewTips": "לחץ כדי לראות את הרגעים החשובים לאורך כל זמן המעקב אחרי האובייקט הזה.", + "autoTrackingTips": "מיקומי תיבות התחימה (Bounding Boxes) לא יהיו מדויקים עבור מצלמות עם מעקב אוטומטי (Autotracking).", + "count": "{{first}} מתוך {{second}}", + "trackedPoint": "נקודת מעקב", + "lifecycleItemDesc": { + "visible": "זוהה {{label}}", + "entered_zone": "{{label}} נכנס ל־{{zones}}", + "active": "{{label}} הפך לפעיל", + "stationary": "{{label}} הפך לנייח", + "attribute": { + "faceOrLicense_plate": "זוהה {{attribute}} עבור {{label}}", + "other": "{{label}} זוהה כ־{{attribute}}" + }, + "gone": "{{label}} יצא", + "heard": "{{label}} נשמע", + "external": "זוהה {{label}}", + "header": { + "zones": "אזורים", + "ratio": "יחס", + "area": "אזור", + "score": "ציון" + } + }, + "annotationSettings": { + "title": "הגדרות סימון", + "showAllZones": { + "title": "הצגת כל האזורים", + "desc": "תמיד להציג אזורים בפריימים שבהם אובייקטים נכנסו לאזור." + }, + "offset": { + "label": "היסט סימון", + "desc": "הנתונים האלה מגיעים מזרם ה־Detect של המצלמה, אבל מוצגים כשכבה מעל תמונות מזרם ה־Record. סביר ששני הזרמים לא מסונכרנים בצורה מושלמת. לכן, מסגרת הזיהוי (Bounding Box) והווידאו לא תמיד יסתדרו בדיוק אחד על השני.\nאפשר להשתמש בהגדרה הזו כדי להזיז את הסימונים קדימה או אחורה בזמן (היסט), וכך ליישר אותם טוב יותר עם ההקלטה.", + "millisecondsToOffset": "מספר המילישניות להיסט סימוני ה־Detect. ברירת מחדל: 0", + "tips": "הקטן את הערך אם הווידאו מקדים את המסגרות ונקודות המסלול, והגדל את הערך אם הווידאו מאחוריהם. הערך יכול להיות גם שלילי.", + "toast": { + "success": "היסט הסימון עבור {{camera}} נשמר בקובץ התצורה." + } + } + }, + "carousel": { + "previous": "שקופית קודמת", + "next": "שקופית הבאה" + } + }, + "aiAnalysis": { + "title": "ניתוח AI" + }, + "concerns": { + "label": "סיכונים" + } +} diff --git a/web/public/locales/he/views/exports.json b/web/public/locales/he/views/exports.json new file mode 100644 index 0000000..2bd8c7e --- /dev/null +++ b/web/public/locales/he/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "חיפוש", + "toast": { + "error": { + "renameExportFailed": "שינוי שם הייצוא נכשל: {{errorMessage}}" + } + }, + "documentTitle": "ייצוא - Frigate", + "noExports": "לא נמצא יצוא", + "deleteExport": "מחיקת ייצוא", + "deleteExport.desc": "האם אתה בטוח שברצונך למחוק את {{exportName}}?", + "editExport": { + "title": "שנה שם ייצוא", + "desc": "הכנס שם חדש עבור הייצוא הזה.", + "saveExport": "שמירת ייצוא" + }, + "tooltip": { + "shareExport": "שתף ייצוא", + "downloadVideo": "הורדת סרטון", + "editName": "עריכת שם", + "deleteExport": "מחיקת ייצוא" + } +} diff --git a/web/public/locales/he/views/faceLibrary.json b/web/public/locales/he/views/faceLibrary.json new file mode 100644 index 0000000..0918c84 --- /dev/null +++ b/web/public/locales/he/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "description": { + "addFace": "הוסף אוסף חדש לספריית הפנים באמצעות העלאת התמונה הראשונה שלך.", + "placeholder": "הזנת שם לאוסף זה", + "invalidName": "שם לא תקין. שמות יכולים לכלול רק אותיות, מספרים, רווחים, גרש (’), קו תחתון (_) ומקף (-)." + }, + "createFaceLibrary": { + "nextSteps": "כדי לבנות בסיס חזק:
  • השתמש בלשונית זיהויים אחרונים כדי לבחור ולאמן על תמונות עבור כל אדם שזוהה.
  • כדי לקבל תוצאות מיטביות, התמקד בתמונות פנים מלפנים; הימנע מתמונות אימון שבהן הפנים מצולמות בזווית.
  • ", + "title": "יצירת אוסף", + "desc": "יצירת אוסף חדש", + "new": "יצירת פנים חדשות" + }, + "toast": { + "success": { + "deletedName_one": "פנים עבור {{count}} נמחקו בהצלחה.", + "deletedName_two": "פנים של {{count}} נמחקו בהצלחה.", + "deletedName_other": "פנים של {{count}} נמחקו בהצלחה.", + "deletedFace_one": "נמחק בהצלחה {{count}} פנים.", + "deletedFace_two": "נמחקו בהצלחה {{count}} פנים.", + "deletedFace_other": "נמחקו בהצלחה {{count}} פנים.", + "uploadedImage": "התמונה הועלתה בהצלחה.", + "addFaceLibrary": "{{name}} נוסף בהצלחה לספריית הפנים!", + "renamedFace": "שם הפנים שונה בהצלחה ל-{{name}}", + "trainedFace": "פנים אומנו בהצלחה.", + "updatedFaceScore": "ציון הפנים עבור {{name}} עודכן בהצלחה ({{score}})." + }, + "error": { + "deleteFaceFailed": "המחיקה נכשלה: {{errorMessage}}", + "deleteNameFailed": "מחיקת השם: {{errorMessage}} נכשלה", + "renameFaceFailed": "שינוי שם הפנים נכשל: {{errorMessage}}", + "trainFailed": "אימון הפנים נכשל: {{errorMessage}}", + "updateFaceScoreFailed": "עדכון ציון הפנים נכשל: {{errorMessage}}", + "uploadingImageFailed": "העלאת התמונה נכשלה: {{errorMessage}}", + "addFaceLibraryFailed": "הגדרת שם הפנים נכשלה: {{errorMessage}}" + } + }, + "details": { + "person": "אדם", + "subLabelScore": "ציון תווית משנה", + "face": "פרטי פנים", + "faceDesc": "פרטי האובייקט שגרם לזיהוי הפנים הזה", + "timestamp": "חותמת זמן", + "unknown": "לא ידוע", + "scoreInfo": "ציון תווית המשנה הוא הציון המשוקלל עבור כל זיהוי הפנים המזוהים, כך שהוא עשוי להיות שונה מהציון המוצג בתמונה." + }, + "documentTitle": "ספריית זיהו פנים - Frigate", + "uploadFaceImage": { + "title": "העלאת תמונת פנים", + "desc": "העלה תמונה לסריקה לאיתור פנים והכללה {{pageToggle}}" + }, + "collections": "אוספים", + "steps": { + "faceName": "קביעת שם לפנים", + "uploadFace": "העלאת תמונת פנים", + "nextSteps": "צעדים הבאים", + "description": { + "uploadFace": "העלה תמונה של {{name}} המציגה את פניו מזווית חזית. אין צורך לחתוך את התמונה רק לפנים שלו." + } + }, + "train": { + "title": "זיהויים אחרונים", + "aria": "בחירת זיהויים אחרונים", + "empty": "אין ניסיונות זיהוי פנים אחרונים", + "titleShort": "לאחרונה" + }, + "selectItem": "בחירה:{{item}}", + "selectFace": "בחירת פנים", + "deleteFaceLibrary": { + "title": "מחיקת שם", + "desc": "האם אתה בטוח שברצונך למחוק את האוסף {{name}}? פעולה זו תמחק לצמיתות את כל הפרצופים המשויכים." + }, + "deleteFaceAttempts": { + "title": "מחיקת פנים", + "desc_one": "האם אתה בטוח שברצונך למחוק את הפנים עבור {{count}}? פעולה זו אינה ניתנת לביטול.", + "desc_two": "האם אתה בטוח שברצונך למחוק את הפנים של {{count}}? פעולה זו אינה ניתנת לביטול.", + "desc_other": "האם אתה בטוח שברצונך למחוק את הפנים של {{count}}? פעולה זו אינה ניתנת לביטול." + }, + "renameFace": { + "title": "שינוי שם של פנים", + "desc": "הזנת שם חדש עבור {{name}}" + }, + "button": { + "deleteFaceAttempts": "מחיקת פנים", + "addFace": "הוסף פנים", + "renameFace": "שנה שם פנים", + "deleteFace": "מחיקת פנים", + "uploadImage": "העלאת תמונה", + "reprocessFace": "עיבוד מחדש של הפנים" + }, + "imageEntry": { + "validation": { + "selectImage": "בחירת קובץ תמונה." + }, + "dropActive": "שחרר/י את התמונה כאן…", + "dropInstructions": "גרור ושחרר או הדבק תמונה כאן, או לחץ כדי לבחור", + "maxSize": "גודל מקסימאלי: {{size}}MB" + }, + "nofaces": "אין פנים זמינים", + "pixels": "{{area}}פיקסלים", + "readTheDocs": "עיין בתיעוד", + "trainFaceAs": "אימון פנים כ:", + "trainFace": "אימון פנים" +} diff --git a/web/public/locales/he/views/live.json b/web/public/locales/he/views/live.json new file mode 100644 index 0000000..5426392 --- /dev/null +++ b/web/public/locales/he/views/live.json @@ -0,0 +1,189 @@ +{ + "manualRecording": { + "title": "לפי דרישה", + "tips": "הורד צילום מיידי או התחל אירוע ידני בהתאם להגדרות שמירת ההקלטות של מצלמה זו.", + "playInBackground": { + "label": "ניגון ברקע", + "desc": "הפעל אפשרות זו כדי להמשיך להזרים גם כאשר הנגן מוסתר." + }, + "showStats": { + "label": "הצג סטטיסטיקות", + "desc": "הפעל אפשרות זו כדי להציג סטטיסטיקות שידור כשכבת-על על פיד המצלמה." + }, + "debugView": "תצוגת ניפוי שגיאות", + "start": "התחלת הקלטה לפי דרישה", + "started": "התחילה הקלטה ידנית לפי דרישה.", + "failedToStart": "התחלת הקלטה ידנית לפי דרישה נכשלה.", + "recordDisabledTips": "מכיוון שההקלטה מושבתת או מוגבלת בתצורה של מצלמה זו, רק לכידת תמונה תישמר.", + "end": "סיום הקלטה לפי דרישה", + "ended": "הקלטה ידנית לפי דרישה הסתיימה.", + "failedToEnd": "סיום ההקלטה הידנית לפי דרישה נכשל." + }, + "documentTitle": "שידור חי - Frigate", + "documentTitle.withCamera": "{{camera}} - שידור חי- Frigate", + "lowBandwidthMode": "מצב רוחב פס נמוך", + "twoWayTalk": { + "enable": "אפשור דיבור דו כיווני", + "disable": "ביטול דיבור דו כיווני" + }, + "cameraAudio": { + "enable": "אפשור קול מהמצלמה", + "disable": "ביטול קול מהמצלמה" + }, + "ptz": { + "move": { + "clickMove": { + "label": "לחץ בתוך המסגרת כדי למרכז את המצלמה", + "enable": "הפעל לחיצה לתנועה", + "disable": "ביטול לחיצה לתנועה" + }, + "up": { + "label": "הזזת מצלמה ממונעת למעלה" + }, + "down": { + "label": "הזזת מצלמה ממונעת למטה" + }, + "right": { + "label": "הזזת מצלמה ממונעת ימינה" + }, + "left": { + "label": "הזזת מצלמה ממונעת לשמאל" + } + }, + "zoom": { + "in": { + "label": "מצלמה ממונעת זום פנימה" + }, + "out": { + "label": "מצלמה ממונעת זום החוצה" + } + }, + "frame": { + "center": { + "label": "לחץ בתוך המסגרת כדי למרכז את המצלמה הממונעת" + } + }, + "presets": "מצלמה ממונעת - פריסטים", + "focus": { + "in": { + "label": "כניסת פוקוס מצלמת PTZ" + }, + "out": { + "label": "יציאת פוקוס מצלמת PTZ" + } + } + }, + "camera": { + "enable": "אפשור מצלמה", + "disable": "השבתת מצלמה" + }, + "muteCameras": { + "enable": "השתק את כל המצלמות", + "disable": "ביטול השתקה לכל המצלמות" + }, + "detect": { + "enable": "אפשור זיהוי", + "disable": "השבתת גילוי" + }, + "recording": { + "enable": "אפשור הקלטה", + "disable": "השבתת הקלטה" + }, + "snapshots": { + "enable": "אפשור לכידת תמונה", + "disable": "השבתת לכידת תמונה" + }, + "audioDetect": { + "disable": "השבתת זיהוי קול", + "enable": "הפעלת זיהוי שמע" + }, + "autotracking": { + "enable": "אפשור מעקב אוטומטי", + "disable": "השבתת מעקב אוטומטי" + }, + "streamStats": { + "enable": "הצג סטטיסטיקות שידור", + "disable": "הסתרת סטטיסטיקות שידור" + }, + "stream": { + "twoWayTalk": { + "tips.documentation": "קרא את התיעוד ", + "available": "שיחה דו-כיוונית זמינה עבור שידור זה", + "tips": "המכשיר שלך חייב לתמוך בתכונה ו-WebRTC חייב להיות מוגדר לתקשורת דו-כיוונית.", + "unavailable": "שיחה דו-כיוונית אינה זמינה עבור שידור זה" + }, + "lowBandwidth": { + "resetStream": "איפוס זרם", + "tips": "התצוגה החיה נמצאת במצב של רוחב פס נמוך עקב שגיאות אחסון במאגר או שידור." + }, + "playInBackground": { + "label": "נגן ברקע", + "tips": "הפעל אפשרות זו כדי להמשיך להזרים כאשר הנגן מוסתר." + }, + "title": "שידור", + "audio": { + "tips": { + "title": "יש להפיק קול מהמצלמה שלך ולהגדיר אותו ב-go2rtc עבור שידור זה.", + "documentation": "עיין בתיעוד " + }, + "available": "קול זמין עבור שידור זה", + "unavailable": "קול אינו זמין עבור שידור זה" + }, + "debug": { + "picker": "בחירת זרם אינה זמינה במצב Debug. תצוגת Debug תמיד משתמשת בזרם שמוגדר עם הייעוד detect." + } + }, + "cameraSettings": { + "title": "{{camera}} הגדרות", + "cameraEnabled": "המצלמה מאופשרת", + "objectDetection": "זיהוי אובייקטים", + "recording": "הקלטה", + "snapshots": "לכידת תמונה", + "audioDetection": "זיהוי קול", + "autotracking": "מעקב אוטומטי", + "transcription": "תמלול אודיו" + }, + "streamingSettings": "הגדרות שידור", + "notifications": "התראות", + "audio": "קול", + "suspend": { + "forTime": "השעיה עבור: " + }, + "history": { + "label": "הצג קטעים היסטוריים" + }, + "effectiveRetainMode": { + "modes": { + "all": "הכל", + "motion": "תנועה", + "active_objects": "אובייקטים פעילים" + }, + "notAllTips": "תצורת שמירת ההקלטה שלך ב-{{source}} מוגדרת ל-מצב: {{effectiveRetainMode}}, כך שהקלטה לפי דרישה זו תשמור רק מקטעים עם {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "עריכת פריסת תצוגה", + "group": { + "label": "עריכת קבוצת מצלמות" + }, + "exitEdit": "יציאה מעריכה" + }, + "snapshot": { + "takeSnapshot": "הורדת תמונת מצב מיידית", + "noVideoSource": "אין מקור וידאו זמין לצילום תמונת מצב.", + "captureFailed": "צילום תמונת מצב נכשל.", + "downloadStarted": "התחילה הורדת תמונת המצב." + }, + "transcription": { + "enable": "הפעלת תמלול אודיו חי", + "disable": "השבתת תמלול אודיו חי" + }, + "noCameras": { + "title": "לא הוגדרו מצלמות", + "description": "התחל על-ידי חיבור מצלמה ל-Frigate.", + "buttonText": "הוסף מצלמה", + "restricted": { + "title": "אין מצלמות זמינות", + "description": "אין לך הרשאה לצפות במצלמות כלשהן בקבוצה זו." + } + } +} diff --git a/web/public/locales/he/views/motionSearch.json b/web/public/locales/he/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/he/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/he/views/recording.json b/web/public/locales/he/views/recording.json new file mode 100644 index 0000000..1e45f6b --- /dev/null +++ b/web/public/locales/he/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "מסנן", + "export": "ייצוא", + "calendar": "לוח שנה", + "filters": "מסננים", + "toast": { + "error": { + "noValidTimeSelected": "לא נבחר טווח זמן תקף", + "endTimeMustAfterStartTime": "שעת הסיום חייבת להיות אחרי שעת ההתחלה" + } + } +} diff --git a/web/public/locales/he/views/replay.json b/web/public/locales/he/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/he/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/he/views/search.json b/web/public/locales/he/views/search.json new file mode 100644 index 0000000..1991713 --- /dev/null +++ b/web/public/locales/he/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "חיפוש", + "savedSearches": "חיפושים שמורים", + "searchFor": "חפש את{{inputValue}}", + "button": { + "clear": "ניקוי חיפוש", + "save": "שמור חיפוש", + "delete": "מחיקת חיפוש שמור", + "filterInformation": "סינון מידע", + "filterActive": "מסננים פעילים" + }, + "trackedObjectId": "מזהה אובייקט במעקב", + "filter": { + "label": { + "cameras": "מצלמות", + "labels": "תוויות", + "zones": "אזורים", + "sub_labels": "תוויות משנה", + "search_type": "סוג חיפוש", + "time_range": "טווח זמן", + "before": "לפני", + "after": "אחרי", + "min_score": "ציון מינימום", + "max_speed": "מהירות מקסימאלית", + "max_score": "ציון מקסימאלי", + "min_speed": "מהירות מינמאלית", + "recognized_license_plate": "לוחית רישוי מוכרת", + "has_clip": "קיים סרטון קליפ", + "has_snapshot": "לכידת תמונה קיימת", + "attributes": "מאפיינים" + }, + "searchType": { + "thumbnail": "תמונה ממוזערת", + "description": "תיאור" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "תאריך ה\"לפני\" חייב להיות מאוחר מתאריך ה\"אחרי\".", + "afterDatebeEarlierBefore": "תאריך ה'אחרי' חייב להיות מוקדם יותר מתאריך ה'לפני'.", + "minScoreMustBeLessOrEqualMaxScore": "ה-'min_score' חייב להיות קטן או שווה ל-'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "ה-'max_score' חייב להיות גדול או שווה ל-'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "ה-'min_speed' חייב להיות קטן או שווה ל-'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "ה-'max_speed' חייב להיות גדול או שווה ל-'min_speed'." + } + }, + "tips": { + "title": "כיצד להשתמש במסנני טקסט", + "desc": { + "text": "מסננים עוזרים לך לצמצם את תוצאות החיפוש שלך. כך תוכל להשתמש בהם בשדה הקלט:", + "step1": "הקלד שם של מפתח סינון ואחריו נקודתיים (לדוגמה, \"מצלמות:\").", + "step2": "בחר ערך מההצעות או הקלד ערך משלך.", + "step3": "השתמשו במספר מסננים על ידי הוספתם אחד אחרי השני עם רווח ביניהם.", + "step6": "הסר מסננים על ידי לחיצה על ה-'x' שלידם.", + "step4": "מסנני תאריך (לפני: ואחרי:) משתמשים בפורמט {{DateFormat}}.", + "step5": "מסנן טווח הזמן משתמש בפורמט {{exampleTime}}.", + "exampleLabel": "דוּגמָה:" + } + }, + "header": { + "currentFilterType": "סנן ערכים", + "noFilters": "מסננים", + "activeFilters": "מסננים פעילים" + } + }, + "similaritySearch": { + "title": "חיפוש פריטים דומים", + "active": "חיפוש דומה פעיל", + "clear": "נקה חיפוש דומה" + }, + "placeholder": { + "search": "חיפוש…" + } +} diff --git a/web/public/locales/he/views/settings.json b/web/public/locales/he/views/settings.json new file mode 100644 index 0000000..cb24111 --- /dev/null +++ b/web/public/locales/he/views/settings.json @@ -0,0 +1,1118 @@ +{ + "camera": { + "reviewClassification": { + "zoneObjectDetectionsTips": { + "notSelectDetections": "כל האובייקטים של {{detectionsLabels}} שזוהו ב-{{zone}} ב-{{cameraName}} שלא סווגו כהתראות יוצגו כזיהויים ללא קשר לאזור בו הם נמצאים.", + "text": "כל האובייקטים של {{detectionsLabels}} שאינם מסווגים ב-{{zone}} ב-{{cameraName}} יוצגו כזיהויים.", + "regardlessOfZoneObjectDetectionsTips": "כל האובייקטים של {{detectionsLabels}} שלא סווגו ב-{{cameraName}} יוצגו כזיהויים ללא קשר לאזור בו הם נמצאים." + }, + "objectAlertsTips": "כל האובייקטים של {{alertsLabels}} ב-{{cameraName}} יוצגו כהתראות.", + "objectDetectionsTips": "כל האובייקטים של {{detectionsLabels}} שלא סווגו ב-{{cameraName}} יוצגו כזיהויים ללא קשר לאזור בו הם נמצאים.", + "noDefinedZones": "בחר ערך מההצעות או הקלד ערך משלך.", + "zoneObjectAlertsTips": "כל האובייקטים של {{alertsLabels}} שזוהו ב-{{zone}} ב-{{cameraName}} יוצגו כהתראות.", + "unsavedChanges": "הגדרות סיווג סקירה שלא נשמרו עבור {{camera}}", + "selectAlertsZones": "בחירת אזורים להתראות", + "selectDetectionsZones": "בחירת אזורים לגילוי", + "limitDetections": "הגבלת הזיהוי לאזורים ספציפיים", + "toast": { + "success": "הגדרת סיווג נשמרה. יש להפעיל מחדש את Frigate כדי שהשינויים ייכנסו לתוקף." + }, + "title": "סיווג סקירה", + "readTheDocumentation": "עיין בתיעוד", + "desc": "Frigate מסווגת פריטי סקירה כהתראות וגילויים. כברירת מחדל, כל האובייקטים של אנשים ומכוניות נחשבים כהתראות. ניתן למקד את הקטגוריה של פריטי הסקירה על ידי הגדרת אזורים נדרשים עבורם." + }, + "title": "הגדרות מצלמה", + "streams": { + "title": "שידורים", + "desc": "השבתת מצלמה עוצרת לחלוטין את עיבוד הזרמים של מצלמה זו על ידי Frigate. זיהוי, הקלטה וניפוי שגיאות לא יהיו זמינים.
    הערה: פעולה זו אינה מבטלת זרמים חוזרים של go2rtc." + }, + "review": { + "title": "סקירה", + "desc": "הפעלה/השבתה של התראות וזיהויים עבור מצלמה זו. כאשר ההגדרה מושבתת, לא ייווצרו פריטי סקירה חדשים. ", + "alerts": "התרעות. ", + "detections": "גילויים. " + } + }, + "masksAndZones": { + "form": { + "inertia": { + "error": { + "mustBeAboveZero": "ההתמדה חייבת להיות מעל 0." + } + }, + "zoneName": { + "error": { + "alreadyExists": "אובייקט המעקב נמחק בהצלחה.", + "mustBeAtLeastTwoCharacters": "שם האזור חייב להיות באורך של לפחות 2 תווים.", + "mustNotBeSameWithCamera": "שם האזור לא חייב להיות זהה לשם המצלמה.", + "mustNotContainPeriod": "שם האזור אינו יכול להכיל נקודות.", + "hasIllegalCharacter": "שם האזור מכיל תווים לא חוקיים.", + "mustHaveAtLeastOneLetter": "שם האזור חייב לכלול לפחות אות אחת." + } + }, + "distance": { + "error": { + "text": "המרחק חייב להיות גדול או שווה ל-0.1.", + "mustBeFilled": "יש למלא את כל שדות המרחק כדי להשתמש בהערכת מהירות." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "זמן ההשהיה חייב להיות גדול או שווה ל-0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "סף המהירות חייב להיות גדול או שווה ל-0.1." + } + }, + "polygonDrawing": { + "removeLastPoint": "הסר את הנקודה האחרונה", + "reset": { + "label": "ניקוי כל הנקודות" + }, + "snapPoints": { + "true": "נקודות הצמדה", + "false": "אל תצמיד נקודות" + }, + "delete": { + "title": "אישור מחיקה", + "desc": "האם אתה בטוח שברצונך למחוק את ה-{{type}} {{name}}?", + "success": "{{name}} נמחק." + }, + "error": { + "mustBeFinished": "יש לסיים את ציור הפוליגון לפני השמירה." + } + } + }, + "filter": { + "all": "כל המיסוכים והאזורים" + }, + "restart_required": "נדרשת הפעלה מחדש (מיסוך/אזורים שונו)", + "toast": { + "success": { + "copyCoordinates": "הקואורדינטות עבור {{polyName}} הועתקו ללוח." + }, + "error": { + "copyCoordinatesFailed": "לא ניתן היה להעתיק קואורדינטות ללוח." + } + }, + "motionMaskLabel": "מיסוך תנועה {{number}}", + "objectMaskLabel": "מיסוך אובייקט {{number}} ({{label}})", + "zones": { + "label": "אזורים.", + "documentTitle": "עריכת אזור - Frigate", + "desc": { + "title": "אזורים מאפשרים לך להגדיר אזור ספציפי בפריים, כך שתוכל לקבוע האם אובייקט נמצא בתוך אזור מסוים או לא.", + "documentation": "תיעוד" + }, + "add": "הוספת אזור", + "edit": "עריכת אזור", + "clickDrawPolygon": "לחץ כדי לצייר פוליגון על התמונה.", + "name": { + "title": "שם", + "inputPlaceHolder": "הזן שם…", + "tips": "השם חייב להכיל לפחות 2 תווים, לכלול לפחות אות אחת, ואסור שיהיה זהה לשם של מצלמה או של אזור אחר במצלמה זו." + }, + "point_one": "נקודה {{count}}", + "point_two": "נקודות {{count}}", + "point_other": "נקודות {{count}}", + "inertia": { + "title": "אינרציה", + "desc": "מציין כמה מסגרות אובייקט חייבות להיות באזור לפני שהוא נחשב באזור. ברירת מחדל: 3" + }, + "loiteringTime": { + "title": "זמן שיטוט", + "desc": "קובע את משך הזמן המינימלי בשניות שהאובייקט חייב להיות באזור כדי שיופעל. ברירת מחדל: 0" + }, + "objects": { + "title": "אובייקטים", + "desc": "רשימת אובייקטים החלים על אזור זה." + }, + "speedEstimation": { + "title": "הערכת מהירות", + "docs": "עיין בתיעוד", + "lineADistance": "מרחק קו A ({{unit}})", + "lineBDistance": "מרחק קו B ({{unit}})", + "lineCDistance": "מרחק קו C ({{unit}})", + "lineDDistance": "מרחק קו D ({{unit}})", + "desc": "הפעל הערכת מהירות עבור אובייקטים באזור זה. האזור חייב לכלול בדיוק 4 נקודות." + }, + "speedThreshold": { + "title": "סף מהירות ({{unit}})", + "desc": "מציין מהירות מינימלית עבור אובייקטים שיש לקחת בחשבון באזור זה.", + "toast": { + "error": { + "loiteringTimeError": "אזורים עם זמן שהייה גדול מ־0 לא אמורים לשמש להערכת מהירות.", + "pointLengthError": "הערכת מהירות הושבתה עבור אזור זה. אזורים עם הערכת מהירות חייבים לכלול בדיוק 4 נקודות." + } + } + }, + "toast": { + "success": "האזור ({{zoneName}}) נשמר בהצלחה." + }, + "allObjects": "כל האובייקטים" + }, + "motionMasks": { + "point_one": "נקודה {{count}}", + "point_two": "נקודות {{count}}", + "point_other": "נקודות {{count}}", + "label": "מיסוך תנועה.", + "documentTitle": "עריכת מיסוך תנועה - Frigate", + "desc": { + "documentation": "תיעוד", + "title": "מיסוך תנועה משמש כדי למנוע זיהוי של תנועה לא רצויה. מיסוך יתר יקשה על מעקב אחר אובייקטים." + }, + "add": "מיסוך תנועה חדש", + "edit": "עריכת מיסוך תנועה", + "context": { + "documentation": "עיין בתיעוד", + "title": "מיסוך תנועה משמש כדי למנוע זיהוי מסוגים לא רצויים של תנועה (לדוגמה: ענפי עצים, חותמות זמן של מצלמה). יש להשתמש במיסוך התנועה בזהירות רבה, מיסוך יתר יקשה על מעקב אחר אובייקטים." + }, + "clickDrawPolygon": "לחץ כדי לצייר פוליגון על התמונה.", + "polygonAreaTooLarge": { + "title": "מיסוך התנועה מכסה {{polygonArea}}% ממסגרת המצלמה. מיסוך תנועה גדול אינו מומלץ.", + "tips": "מיסוך תנועה אינו מונע זיהוי של אובייקטים. עליך להשתמש באזור נדרש במקום זאת.", + "documentation": "עיין בתיעוד" + }, + "toast": { + "success": { + "title": "{{polygonName}} נשמר בהצלחה.", + "noName": "מסכת תנועה נשמרה בהצלחה." + } + } + }, + "objectMasks": { + "point_one": "נקודה {{count}}", + "point_two": "נקודות {{count}}", + "point_other": "נקודות {{count}}", + "label": "מיסוך אובייקט", + "documentTitle": "עריכת מיסוך אובייקט - Frigate", + "desc": { + "title": "מיסוך סינון אובייקטים משמש לסינון תוצאות חיוביות שגויות עבור סוג אובייקט נתון בהתבסס על מיקום.", + "documentation": "תיעוד" + }, + "add": "הוספת מיסוך אובייקט", + "edit": "עריכת מיסוך אובייקט", + "context": "מיסוך סינון אובייקטים משמש לסינון תוצאות חיוביות שגויות עבור סוג אובייקט נתון בהתבסס על מיקום.", + "clickDrawPolygon": "לחץ כדי לצייר פוליגון על התמונה.", + "objects": { + "title": "אובייקטים", + "desc": "סוג האובייקט שאליו מתייחס מיסוך האובייקט הזה.", + "allObjectTypes": "כל סוגי האובייקטים" + }, + "toast": { + "success": { + "title": "{{polygonName}} נשמר בהצלחה.", + "noName": "מיסוך האובייקט נשמר." + } + } + } + }, + "general": { + "recordingsViewer": { + "defaultPlaybackRate": { + "desc": "קצב ניגון ברירת מחדל עבור ניגון הקלטות.", + "label": "קצב השמעה ברירת המחדל" + }, + "title": "מציג הקלטות" + }, + "liveDashboard": { + "automaticLiveView": { + "label": "צפיה בשידור חי אוטומטית", + "desc": "מעבר אוטומטי לתצוגה חיה של מצלמה כאשר מזוהה פעילות. השבתת אפשרות זו גורמת לתמונות סטטיות של המצלמה בדשבורד שידור חי להתעדכן רק פעם בדקה." + }, + "title": "דשבורד שידור חי", + "playAlertVideos": { + "label": "ניגון סרטוני התראות", + "desc": "כברירת מחדל, התראות אחרונות בדשבורד שידור חי מופעלות כסרטונים קצרים בלולאה. השבת אפשרות זו כדי להציג רק תמונה סטטית של התראות אחרונות במכשיר/דפדפן זה." + }, + "displayCameraNames": { + "label": "תמיד להציג שם מצלמה", + "desc": "תמיד להציג את שמות המצלמות בצ’יפ בתצוגת הלייב מרובת המצלמות בדשבורד." + }, + "liveFallbackTimeout": { + "label": "זמן המתנה למעבר לנגן חלופי בשידור חי", + "desc": "כאשר זרם השידור חי באיכות גבוהה של מצלמה אינו זמין, המערכת תעבור למצב רוחב־פס נמוך לאחר מספר השניות הזה. ברירת מחדל: 3." + } + }, + "cameraGroupStreaming": { + "desc": "הגדרות שידור עבור כל קבוצת מצלמות נשמרות באחסון המקומי של הדפדפן שלך.", + "title": "הגדרות הזרמת קבוצת מצלמות", + "clearAll": "נקה את כל הגדרות השידור" + }, + "title": "הגדרות UI", + "storedLayouts": { + "title": "פריסות תצוגה שמורות", + "desc": "ניתן לגרור/לשנות את גודל הפריסה של המצלמות בקבוצת מצלמות. המיקומים נשמרים באחסון המקומי של הדפדפן שלך.", + "clearAll": "נקה את כל פריסות התצוגות" + }, + "calendar": { + "title": "לוח שנה", + "firstWeekday": { + "label": "היום הראשון בשבוע", + "sunday": "ראשון", + "monday": "שני", + "desc": "היום שבו מתחיל השבוע בלוח הסקירות." + } + }, + "toast": { + "success": { + "clearStoredLayout": "ניקוי הפריסה השמורה עבור {{cameraName}}", + "clearStreamingSettings": "ניקוי הגדרות השידור עבור כל קבוצות המצלמות בוצע." + }, + "error": { + "clearStoredLayoutFailed": "ניקוי פריסת התצוגה השמורה נכשל: {{errorMessage}}", + "clearStreamingSettingsFailed": "ניקוי הגדרות השידור נכשל: {{errorMessage}}" + } + } + }, + "documentTitle": { + "camera": "הגדרת מצלמה - Frigate", + "enrichments": "הגדרות העשרה - Frigate", + "masksAndZones": "עריכת מיסוך אזור - Frigate", + "motionTuner": "כיול תנועה - Frigate", + "object": "ניפוי שגיאות - Frigate", + "frigatePlus": "הגדרות +Frigate - Frigate", + "notifications": "הגדרת התראות - Frigate", + "authentication": "הגדרות אימות - Frigate", + "default": "הגדרות - Frigate", + "general": "הגדרות ממשק (UI) - Frigate", + "cameraManagement": "ניהול מצלמות - Frigate", + "cameraReview": "הגדרות סקירת מצלמה - Frigate" + }, + "menu": { + "ui": "UI - ממשק משתמש", + "cameras": "הגדרות מצלמה", + "masksAndZones": "מיסוך / אזורים", + "motionTuner": "כיול תנועה", + "debug": "ניפוי שגיאות", + "users": "משתמשים", + "notifications": "התראות", + "frigateplus": "+Frigate", + "enrichments": "תוספות", + "triggers": "הפעלות", + "cameraManagement": "ניהול", + "cameraReview": "סְקִירָה", + "roles": "תפקידים" + }, + "dialog": { + "unsavedChanges": { + "title": "ישנם שינויים שלא נשמרו.", + "desc": "האם ברצונך לשמור את השינויים לפני שתמשיך?" + } + }, + "cameraSetting": { + "camera": "מצלמה", + "noCamera": "אין מצלמה" + }, + "enrichments": { + "semanticSearch": { + "reindexNow": { + "confirmButton": "ביצוע אינדקס מחדש", + "success": "אינדקס מחדש הופעל בהצלחה.", + "error": "התחלת האינדקס מחדש נכשלה: {{errorMessage}}", + "confirmDesc": "האם אתה בטוח שברצונך לבצע אינדקס מחדש להטמעות האובייקטים במעקב? תהליך זה יפעל ברקע אך הוא עשוי להפעיל את המעבד שלך בצורה מקסימלית ועלול לקחת זמן רב. תוכל לצפות בהתקדמות בדף החיפוש.", + "alreadyInProgress": "אינדקס מחדש כבר פעיל כרגע.", + "label": "בצע אינדקס מחדש כעת", + "confirmTitle": "אישור אינדקס מחדש", + "desc": "אינדקס מחדש ייצור מחדש הטמעות עבור כל האובייקטים שבמעקב. תהליך זה פועל ברקע ועשוי למקסם את עוצמת המעבד שלך ולקחת זמן רב בהתאם למספר האובייקטים במעקב שיש לך." + }, + "modelSize": { + "label": "גודל מודל", + "small": { + "title": "קטן", + "desc": "שימוש ב-קטן משתמש בגרסה כמותית של המודל המשתמשת בפחות זיכרון RAM ופועלת מהר יותר על המעבד עם הבדל זניח מאוד באיכות ההטמעה." + }, + "large": { + "title": "גדול", + "desc": "שימוש ב-גדול מפעיל את מודל Jina המלא ויפעל אוטומטית על ה-GPU במידת הצורך." + }, + "desc": "גודל המודל המשמש להטמעות חיפוש סמנטי." + }, + "title": "חיפוש סמנטי", + "desc": "חיפוש סמנטי ב-Frigate מאפשר לך למצוא אובייקטים שעוקבים אחריהם בתוך פריטי הסקירה שלך באמצעות התמונה עצמה, תיאור טקסטואלי שהוגדר על ידי המשתמש, או תיאור שנוצר אוטומטית.", + "readTheDocumentation": "עיין בתיעוד" + }, + "faceRecognition": { + "title": "זיהוי פנים", + "desc": "זיהוי פנים מאפשר להקצות לאנשים שמות, וכאשר פנים מזוהות, Frigate תקצה את שמו של האדם כתווית משנה. מידע זה כלול בממשק המשתמש, במסננים וגם בהתראות.", + "readTheDocumentation": "עיין בתיעוד", + "modelSize": { + "label": "גודל מודל", + "desc": "גודל המודל המשמש לזיהוי פנים.", + "small": { + "title": "קטן", + "desc": "שימוש ב-קטן משתמש במודל הטמעת פנים של FaceNet שפועל ביעילות על רוב המעבדים." + }, + "large": { + "title": "גדול", + "desc": "שימוש ב-גדול משתמש במודל הטמעת פנים של ArcFace ויפעל אוטומטית על גבי ה-GPU במידת הצורך." + } + } + }, + "title": "הגדרות העשרה", + "unsavedChanges": "שינויים בהגדרות העשרה לא נשמרו", + "birdClassification": { + "title": "סיווג ציפורים", + "desc": "סיווג ציפורים מזהה ציפורים ידועות באמצעות מודל Tensorflow כמותי. כאשר ציפור ידועה מזוהה, שמה הנפוץ יתווסף כתווית משנה. מידע זה כלול בממשק המשתמש, במסננים וכן בהתראות." + }, + "licensePlateRecognition": { + "title": "זיהוי לוחיות רישוי", + "readTheDocumentation": "עיין בתיעוד", + "desc": "Frigate יכולה לזהות לוחיות רישוי על כלי רכב ולהוסיף אוטומטית את התווים שזוהו לשדה לוחית רישוי מזוהה או לשם ידוע כתווית משנה לאובייקטים מסוג מכונית. מקרה שימוש נפוץ עשוי להיות קריאת לוחיות הרישוי של מכוניות שנכנסות לחניה או מכוניות שעוברות ברחוב." + }, + "restart_required": "נדרש אתחול (הגדרות ההעשרה שונו)", + "toast": { + "success": "הגדרות העשרה נשמרו. הפעל מחדש את Frigate כדי להחיל את השינויים שלך.", + "error": "שמירת שינויי ההגדרות נכשלה: {{errorMessage}}" + } + }, + "motionDetectionTuner": { + "title": "כיול גילוי תנועה", + "unsavedChanges": "שינויים שלא נשמרו בכיול התנועה ({{camera}})", + "desc": { + "documentation": "עיין במדריך כיול התנועה", + "title": "Frigate משתמשת בזיהוי תנועה כבדיקה ראשונה כדי לראות אם קורה משהו בפריים שכדאי לבדוק עם זיהוי עצמים." + }, + "Threshold": { + "title": "סף", + "desc": "ערך הסף מכתיב כמה שינוי נדרש בבהירות של פיקסל כדי להיחשב כתנועה. ברירת מחדל: 30" + }, + "contourArea": { + "title": "אזור קונטור", + "desc": "ערך שטח המתאר משמש לקביעת אילו קבוצות של פיקסלים שהשתנו ייחשבו כתנועה. ברירת מחדל: 10" + }, + "improveContrast": { + "title": "שיפור ניגודיות", + "desc": "שפר את הניגודיות עבור סצנות כהות יותר. ברירת מחדל: מופעל" + }, + "toast": { + "success": "הגדרות התנועה נשמרו." + } + }, + "debug": { + "title": "ניפוי שגיאות.", + "detectorDesc": "Frigate משתמשת בגלאים שלך ({{detectors}}) כדי לזהות אובייקטים בזרם הווידאו של המצלמה שלך.", + "debugging": "ניפוי שגיאות", + "objectList": "רשימת אובייקטים", + "noObjects": "אין אובייקטים", + "boundingBoxes": { + "title": "תיבות זיהוי", + "desc": "הצגת תיבות זיהוי סביב אובייקטים במעקב", + "colors": { + "label": "צבעי תיבת זיהוי של אובייקטים", + "info": "
  • בהפעלה, יוקצו צבעים שונים לכל תווית אובייקט
  • קו דק כחול כהה מציין שהאובייקט לא זוהה בנקודת זמן הנוכחית
  • קו דק אפור מציין שהאובייקט זוהה כנייח
  • קו עבה מציין שהאובייקט הוא נושא המעקב האוטומטי (כאשר מופעל)
  • " + } + }, + "timestamp": { + "title": "חותמת זמן", + "desc": "הוספת חותמת זמן על התמונה" + }, + "zones": { + "title": "אזורים", + "desc": "הצגת קווי מתאר של כל אזור מוגדר" + }, + "mask": { + "title": "מיסוך תנועה", + "desc": "הצגת פוליגונים של מיסוך תנועה" + }, + "motion": { + "title": "תיבות תנועה", + "desc": "הצג תיבות סביב אזורים שבהם זוהתה תנועה", + "tips": "

    תיבות תנועה


    תיבות אדומות יונחו על אזורים בפריים שבהם מזוהה תנועה כעת

    " + }, + "regions": { + "title": "אזורים", + "desc": "הצג תיבה של אזור העניין שנשלח לגלאי האובייקטים", + "tips": "

    תיבות אזור


    תיבות ירוקות בהירות יונחו על פני אזורים מעניינים בפריים הנשלחים לגלאי האובייקטים.

    " + }, + "objectShapeFilterDrawing": { + "title": "ציור מסנן צורת אובייקט", + "desc": "צייר מלבן על התמונה כדי להציג פרטים על שטח ויחס", + "document": "עיין בתיעוד ", + "score": "ציון", + "ratio": "יחס", + "area": "אזור", + "tips": "הפעל אפשרות זו כדי לצייר מלבן על תמונת המצלמה כדי להציג את השטח והיחס שלה. ניתן להשתמש בערכים אלה כדי להגדיר פרמטרים של מסנן צורת אובייקט בתצורה שלך." + }, + "desc": "תצוגת ניפוי שגיאות מציגה תצוגה בזמן אמת של אובייקטים במעקב והסטטיסטיקות שלהם. רשימת האובייקטים מציגה סיכום בהשהיית זמן של האובייקטים שזוהו.", + "openCameraWebUI": "פתח את ממשק ה־Web של {{camera}}", + "audio": { + "title": "אודיו", + "noAudioDetections": "אין זיהויי אודיו", + "score": "ציון", + "currentRMS": "RMS נוכחי", + "currentdbFS": "dbFS נוכחי" + }, + "paths": { + "title": "נתיבים", + "desc": "הצג נקודות משמעותיות במסלול התנועה של האובייקט במעקב", + "tips": "

    נתיבים


    קווים ועיגולים יציינו נקודות משמעותיות שבהן האובייקט במעקב נע במהלך מחזור חייו.

    " + } + }, + "users": { + "title": "משתמשים", + "management": { + "title": "ניהול משתמשים", + "desc": "נהל את חשבונות המשתמשים של מופע Frigate זה." + }, + "addUser": "הוספת משתמש", + "updatePassword": "איפוס סיסמה", + "toast": { + "success": { + "createUser": "המשתמש {{user}} נוצר בהצלחה", + "deleteUser": "המשתמש {{user}} נמחק בהצלחה", + "updatePassword": "הסיסמה עודכנה בהצלחה.", + "roleUpdated": "הרשאות עודכנו עבור {{user}}" + }, + "error": { + "createUserFailed": "יצירת משתמש נכשלה: {{errorMessage}}", + "roleUpdateFailed": "עדכון ההרשאות נכשל: {{errorMessage}}", + "deleteUserFailed": "מחיקת משתמש נכשלה: {{errorMessage}}", + "setPasswordFailed": "שמירת הסיסמה נכשלה: {{errorMessage}}" + } + }, + "table": { + "actions": "פעולות", + "role": "הרשאות", + "noUsers": "לא נמצאו משתמשים.", + "changeRole": "שינוי הרשאות משתמש", + "password": "איפוס סיסמה", + "deleteUser": "מחיקת משתמש", + "username": "שם משתמש" + }, + "dialog": { + "form": { + "user": { + "title": "שם משתמש", + "desc": "מותר להשתמש רק באותיות, מספרים, נקודות וקו תחתון.", + "placeholder": "הכנס שם משתמש" + }, + "password": { + "title": "סיסמה", + "placeholder": "הכנס סיסמה", + "confirm": { + "placeholder": "אישור סיסמה", + "title": "אישור סיסמה" + }, + "strength": { + "title": "חוזק הסיסמה: ", + "weak": "חלש", + "medium": "בינוני", + "strong": "חזק", + "veryStrong": "מאוד חזק" + }, + "match": "סיסמאות תואמות", + "notMatch": "הסיסמאות אינן תואמות.", + "show": "הצג סיסמה", + "hide": "הסתר סיסמה", + "requirements": { + "title": "דרישות סיסמה:", + "length": "לפחות 8 תווים", + "uppercase": "לפחות אות גדולה אחת", + "digit": "לפחות ספרה אחת", + "special": "לפחות תו מיוחד אחד (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "סיסמה חדשה", + "placeholder": "הכנס סיסמה חדשה", + "confirm": { + "placeholder": "הזן שוב את הסיסמה החדשה" + } + }, + "usernameIsRequired": "נדרש שם משתמש", + "passwordIsRequired": "נדרשת סיסמה", + "currentPassword": { + "title": "סיסמה נוכחית", + "placeholder": "הזן את הסיסמה הנוכחית שלך" + } + }, + "createUser": { + "title": "יצירת משתמש חדש", + "desc": "הוסף חשבון משתמש חדש וציין הרשאות גישה לאזורים בממשק המשתמש של Frigate.", + "usernameOnlyInclude": "שם המשתמש יכול לכלול רק אותיות, מספרים, . או קו תחתון", + "confirmPassword": "אנא אשר את הסיסמה שלך" + }, + "deleteUser": { + "title": "מחיקת משתמש", + "desc": "לא ניתן לבטל פעולה זו. פעולה זו תמחק לצמיתות את חשבון המשתמש ותסיר את כל הנתונים המשויכים.", + "warn": "האם אתה בטוח שברצונך למחוק את {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "הסיסמה לא יכולה להיות ריקה", + "doNotMatch": "הסיסמאות אינן תואמות", + "updatePassword": "עדכון סיסמה עבור {{username}}", + "setPassword": "קבע סיסמה", + "desc": "צור סיסמה חזקה כדי לאבטח חשבון זה.", + "currentPasswordRequired": "נדרשת הסיסמה הנוכחית", + "incorrectCurrentPassword": "הסיסמה הנוכחית שגויה", + "passwordVerificationFailed": "נכשל באימות הסיסמה", + "multiDeviceWarning": "כל מכשיר אחר שבו אתה מחובר יידרש להתחבר מחדש בתוך {{refresh_time}}.", + "multiDeviceAdmin": "ניתן גם לאלץ את כל המשתמשים להתחבר מחדש באופן מיידי על־ידי החלפת מפתח ה־JWT שלך." + }, + "changeRole": { + "title": "שינוי הרשאות משתמש", + "select": "בחירת הרשאות", + "desc": "עדכון הרשאות עבור {{username}}", + "roleInfo": { + "intro": "בחר את ההרשאות המתאימות עבור משתמש זה:", + "admin": "מנהל", + "adminDesc": "גישה מלאה לכל התכונות.", + "viewer": "צופה", + "viewerDesc": "מוגבל לדשבורד שידור חי, סקירה, גילוי וייצוא בלבד.", + "customDesc": "תפקיד מותאם אישית עם גישה למצלמות מסוימות." + } + } + } + }, + "notification": { + "title": "התראות", + "notificationSettings": { + "title": "הגדרת התראות", + "desc": "Frigate יכולה לשלוח התראות דחיפה באופן טבעי למכשיר שלך כאשר הוא פועל בדפדפן או מותקן כ-PWA.", + "documentation": "עיין בתיעוד" + }, + "notificationUnavailable": { + "title": "התראות לא זמינות", + "desc": "התראות דחיפה באינטרנט דורשות קישור מאובטח (https://…). זוהי מגבלה של הדפדפן. יש לגשת ל-Frigate בצורה מאובטחת כדי להשתמש בהתראות.", + "documentation": "עיין בתיעוד" + }, + "globalSettings": { + "title": "הגדרות כלליות", + "desc": "השהה זמנית התראות עבור מצלמות ספציפיות בכל המכשירים הרשומים." + }, + "email": { + "title": "דואר", + "desc": "נדרשת כתובת דוא\"ל תקינה שתשמש כדי להודיע לך אם ישנן בעיות כלשהן בשירות הדחיפה.", + "placeholder": "e.g. example@email.com" + }, + "cameras": { + "title": "מצלמות", + "noCameras": "אין מצלמות זמינות", + "desc": "בחר עבור אילו מצלמות להפעיל התראות." + }, + "deviceSpecific": "הגדרות ספציפיות למכשיר", + "registerDevice": "רשום מכשיר זה", + "unregisterDevice": "בטל את הרישום של מכשיר זה", + "sendTestNotification": "שלח הודעת בדיקה", + "unsavedRegistrations": "רישומי התראות שלא נשמרו", + "unsavedChanges": "שינויים בהתראות שלא נשמרו", + "active": "התראות פעילות", + "suspended": "התראות הושבתו ב-{{time}}", + "suspendTime": { + "suspend": "מושבת", + "5minutes": "השבתה למשך 5 דקות", + "10minutes": "השבתה למשך 10 דקות", + "30minutes": "השבתה למשך 30 דקות", + "1hour": "השבתה למשך שעה אחת", + "12hours": "השבתה למשך 12 שעות", + "24hours": "השבתה למשך 24 שעות", + "untilRestart": "השבתה עד להפעלה מחדש" + }, + "cancelSuspension": "ביטול השבתה", + "toast": { + "success": { + "settingSaved": "הגדרות ההתראות נשמרו.", + "registered": "רישום לקבלת התראות בוצע בהצלחה. נדרשת הפעלה מחדש של Frigate לפני שניתן יהיה לשלוח התראות כלשהן (כולל הודעת בדיקה)." + }, + "error": { + "registerFailed": "שמירת רישום ההתראות נכשלה." + } + } + }, + "frigatePlus": { + "title": "הגדרות +Frigate", + "apiKey": { + "title": "Frigate+ API מפתח", + "validated": "מפתח ה-API של Frigate+ זוהה ואושר", + "notValidated": "מפתח ה-API של Frigate+ לא זוהה או לא אומת", + "desc": "מפתח ה-API של Frigate+ מאפשר אינטגרציה עם שירות Frigate+.", + "plusLink": "קרא עוד על Frigate+" + }, + "snapshotConfig": { + "title": "תצורת לכידת תמונה", + "desc": "שליחה ל-Frigate+ דורשת הפעלה של לכידת תמונה וגם של תמונות בזק clean_copy בהגדרות שלך.", + "documentation": "עיין בתיעוד", + "cleanCopyWarning": "בחלק מהמצלמות יש אפשרות לתמונות בזק, אך העתקה נקייה מושבתת. עליך להפעיל את clean_copy בתצורת הצילום שלך כדי שתוכל לשלוח תמונות מהמצלמות הללו ל-Frigate+.", + "table": { + "snapshots": "לכידת תמונה", + "camera": "מצלמה", + "cleanCopySnapshots": "תמונות clean_copy" + } + }, + "modelInfo": { + "title": "מידע על המודל", + "modelType": "סוג מודל", + "trainDate": "תאריך אימון", + "baseModel": "דגם בסיסי", + "plusModelType": { + "userModel": "כיוונון עדין", + "baseModel": "מודל בסיסי" + }, + "supportedDetectors": "גלאים נתמכים", + "cameras": "מצלמות", + "loading": "טוען מידע על המודל…", + "error": "טעינת פרטי המודל נכשלה", + "availableModels": "מודלים זמינים", + "loadingAvailableModels": "טוען מודלים זמינים…", + "modelSelect": "ניתן לבחור כאן את הדגמים הזמינים ב-Frigate+. שים לב שניתן לבחור רק דגמים התואמים לתצורת הגלאי הנוכחית שלך." + }, + "unsavedChanges": "שינויים בהגדרות של frigate+ שלא נשמרו", + "restart_required": "נדרש הפעלה מחדש (דגם Frigate+ שונה)", + "toast": { + "success": "הגדרות Frigate+ נשמרו. הפעל מחדש את Frigate כדי להחיל את השינויים.", + "error": "שמירת שינויי התצורה נכשלה: {{errorMessage}}" + } + }, + "cameraWizard": { + "step1": { + "brandInformation": "פרטי יצרן", + "brandUrlFormat": "למצלמות עם פורמט כתובת RTSP כמו: {{exampleUrl}}", + "connectionSettings": "הגדרות חיבור", + "detectionMethod": "שיטת זיהוי זרם", + "onvifPort": "פורט ONVIF", + "probeMode": "בדיקת מצלמה", + "manualMode": "בחירה ידנית", + "detectionMethodDescription": "בדוק את המצלמה באמצעות ONVIF (אם נתמך) כדי למצוא את כתובות הזרמים שלה, או בחר ידנית את יצרן המצלמה כדי להשתמש בכתובות מוגדרות מראש.\nכדי להזין כתובת RTSP מותאמת אישית, בחר בשיטה ידנית ואז בחר \"אחר\".", + "onvifPortDescription": "במצלמות שתומכות ב-ONVIF, זה בדרך כלל 80 או 8080.", + "useDigestAuth": "שימוש באימות Digest", + "useDigestAuthDescription": "השתמש באימות HTTP Digest עבור ONVIF. בחלק מהמצלמות נדרש שם משתמש/סיסמה ייעודיים ל-ONVIF, ולא משתמש ה-Admin הרגיל.", + "errors": { + "brandOrCustomUrlRequired": "בחר יצרן מצלמה והזן Host/IP, או בחר “אחר” והזן כתובת מותאמת אישית", + "nameRequired": "שם המצלמה הוא שדה חובה", + "nameLength": "שם המצלמה חייב להיות באורך של עד 64 תווים", + "invalidCharacters": "שם המצלמה מכיל תווים לא חוקיים", + "nameExists": "שם המצלמה כבר קיים", + "customUrlRtspRequired": "כתובות מותאמות אישית חייבות להתחיל ב־\"rtsp://\". עבור זרמי מצלמה שאינם RTSP נדרשת הגדרה ידנית." + }, + "description": "הזן את פרטי המצלמה ובחר אם לבצע בדיקה למצלמה או לבחור ידנית את היצרן.", + "cameraName": "שם מצלמה", + "cameraNamePlaceholder": "לדוגמה: front_door או סקירת החצר האחורית", + "host": "HOST / כתובת IP", + "port": "פורט", + "username": "שם משתמש", + "usernamePlaceholder": "אופציונלי", + "password": "סיסמה", + "passwordPlaceholder": "אופציונלי", + "selectTransport": "בחר פרוטוקול תעבורה", + "cameraBrand": "יצרן מצלמה", + "selectBrand": "בחר יצרן מצלמה עבור תבנית כתובת ה-URL", + "customUrl": "כתובת (URL) זרם מותאמת אישית", + "customUrlPlaceholder": "rtsp://username:password@host:port/path" + }, + "step2": { + "description": "בדוק את המצלמה כדי לאתר זרמים זמינים, או הגדר ידנית את ההגדרות לפי שיטת הזיהוי שבחרת.", + "testSuccess": "בדיקת החיבור הצליחה!", + "testFailed": "בדיקת החיבור נכשלה. בדוק את הנתונים שהזנת ונסה שוב.", + "testFailedTitle": "הבדיקה נכשלה", + "streamDetails": "פרטי זרם", + "probing": "בודק מצלמה...", + "retry": "נסה שוב", + "testing": { + "probingMetadata": "בודק את נתוני המטא של המצלמה…", + "fetchingSnapshot": "שולף תמונת מצב מהמצלמה…" + }, + "probeFailed": "בדיקת המצלמה נכשלה: {{error}}", + "probingDevice": "בודק את ההתקן…", + "probeSuccessful": "הבדיקה הצליחה", + "probeError": "בדיקה נכשלה", + "probeNoSuccess": "הבדיקה לא הצליחה", + "deviceInfo": "מידע על ההתקן", + "manufacturer": "יצרן", + "model": "דגם", + "firmware": "קושחה", + "profiles": "פרופילים", + "ptzSupport": "תמיכה ב-PTZ", + "autotrackingSupport": "תמיכה ב-Autotracking", + "presets": "פריסטים", + "rtspCandidates": "כתובות RTSP מוצעות", + "rtspCandidatesDescription": "כתובות ה־RTSP הבאות נמצאו בבדיקת המצלמה. בצע בדיקת חיבור כדי לצפות בנתוני הזרם (Metadata).", + "noRtspCandidates": "לא נמצאו כתובות RTSP מהמצלמה. ייתכן שפרטי ההתחברות שגויים, או שהמצלמה לא תומכת ב-ONVIF, או שהשיטה שבה השתמשנו לשליפת כתובות RTSP אינה נתמכת. חזור אחורה והזן את כתובת ה-RTSP ידנית.", + "candidateStreamTitle": "אפשרות {{number}}", + "useCandidate": "השתמש", + "uriCopy": "העתק", + "uriCopied": "הכתובת (URI) הועתקה ללוח", + "testConnection": "בדיקת חיבור", + "toggleUriView": "לחץ כדי להציג/להסתיר את הכתובת המלאה", + "connected": "מחובר", + "notConnected": "לא מחובר", + "errors": { + "hostRequired": "כתובת Host/IP היא שדה חובה" + } + }, + "step3": { + "description": "הגדר תפקידי זרם (Roles) והוסף זרמים נוספים למצלמה שלך.", + "streamsTitle": "זרמי מצלמה", + "addStream": "הוסף זרם", + "addAnotherStream": "הוסף זרם נוסף", + "streamTitle": "זרם {{number}}", + "streamUrl": "כתובת הזרם (URL)", + "selectStream": "בחר זרם", + "searchCandidates": "חיפוש אפשרויות…", + "noStreamFound": "לא נמצא זרם", + "url": "URL", + "resolution": "רזולוציה", + "selectResolution": "בחר רזולוציה", + "quality": "איכות", + "selectQuality": "בחר איכות", + "roles": "תפקידים", + "roleLabels": { + "detect": "זיהוי אובייקטים", + "record": "הקלטה", + "audio": "קול (Audio)" + }, + "testStream": "בדיקת חיבור", + "testSuccess": "בדיקת הזרם הצליחה!", + "testFailed": "בדיקת הזרם נכשלה", + "testFailedTitle": "הבדיקה נכשלה", + "connected": "מחובר", + "notConnected": "לא מחובר", + "featuresTitle": "תכונות", + "go2rtc": "הפחתת חיבורים למצלמה", + "detectRoleWarning": "כדי להמשיך, לפחות זרם אחד חייב להיות עם ייעוד \"detect\".", + "rolesPopover": { + "title": "ייעודי הזרם", + "detect": "הזרם הראשי לזיהוי אובייקטים.", + "record": "שומר קטעים מזרם הווידאו לפי הגדרות התצורה.", + "audio": "זרם לזיהוי מבוסס אודיו." + }, + "featuresPopover": { + "title": "תכונות הזרם", + "description": "השתמש ב־go2rtc לריסטרים (Restream) כדי להפחית את מספר החיבורים למצלמה שלך." + }, + "streamUrlPlaceholder": "rtsp://username:password@host:port/path" + }, + "step4": { + "description": "אימות וניתוח סופיים לפני שמירת המצלמה החדשה. התחבר לכל זרם לפני השמירה.", + "validationTitle": "אימות הזרם", + "connectAllStreams": "התחברות לכל הזרמים", + "reconnectionSuccess": "חיבור מחדש הצליח.", + "reconnectionPartial": "חלק מהזרמים לא הצליחו להתחבר מחדש.", + "streamUnavailable": "תצוגה מקדימה של הזרם אינה זמינה", + "reload": "טעינה מחדש", + "connecting": "מתחבר...", + "streamTitle": "זרם {{number}}", + "valid": "תקין", + "failed": "נכשל", + "notTested": "לא נבדק", + "connectStream": "התחבר", + "connectingStream": "מתחבר", + "disconnectStream": "נתק", + "estimatedBandwidth": "רוחב־פס משוער", + "roles": "ייעודים", + "ffmpegModule": "שימוש במצב תאימות לזרם", + "ffmpegModuleDescription": "אם הזרם לא נטען אחרי כמה ניסיונות, נסה להפעיל את זה. כשהאפשרות פעילה, Frigate ישתמש במודול ffmpeg יחד עם go2rtc. זה עשוי לשפר תאימות עם זרמים של חלק מהמצלמות.", + "none": "ללא", + "error": "שגיאה", + "streamValidated": "הזרם {{number}} אומת בהצלחה", + "streamValidationFailed": "אימות הזרם {{number}} נכשל", + "saveAndApply": "שמירת מצלמה חדשה", + "saveError": "תצורה לא תקינה. בדוק את ההגדרות שלך.", + "issues": { + "title": "אימות הזרם", + "videoCodecGood": "קידוד הווידאו הוא {{codec}}.", + "audioCodecGood": "קידוד האודיו הוא {{codec}}.", + "resolutionHigh": "רזולוציה של {{resolution}} עשויה לגרום לשימוש מוגבר במשאבים.", + "resolutionLow": "רזולוציה של {{resolution}} עשויה להיות נמוכה מדי לזיהוי אמין של אובייקטים קטנים.", + "noAudioWarning": "לא זוהה אודיו בזרם הזה, ולכן ההקלטות יהיו ללא שמע.", + "audioCodecRecordError": "כדי לכלול אודיו בהקלטות נדרש קידוד שמע AAC.", + "audioCodecRequired": "כדי לאפשר זיהוי אודיו נדרש זרם שמע.", + "restreamingWarning": "הפחתת מספר החיבורים למצלמה עבור זרם ההקלטה (record) עשויה להעלות מעט את השימוש ב־CPU.", + "brands": { + "reolink-rtsp": "RTSP של Reolink לא מומלץ. הפעל HTTP בהגדרות הקושחה של המצלמה, ואז הפעל מחדש את אשף ההגדרה.", + "reolink-http": "בזרמי HTTP של Reolink מומלץ להשתמש ב־FFmpeg לתאימות טובה יותר. הפעל עבור הזרם הזה את האפשרות “שימוש במצב תאימות לזרם”." + }, + "dahua": { + "substreamWarning": "זרם משנה 1 נעול לרזולוציה נמוכה. מצלמות רבות של Dahua / Amcrest / EmpireTech תומכות בזרמי משנה נוספים שצריך להפעיל בהגדרות המצלמה מומלץ לבדוק אם קיימים זרמי משנה כאלה ולהשתמש בהם במידה וזמינים." + }, + "hikvision": { + "substreamWarning": "זרם משנה 1 נעול לרזולוציה נמוכה. מצלמות רבות של Hikvision תומכות בזרמי משנה נוספים שצריך להפעיל בהגדרות המצלמה. מומלץ לבדוק אם קיימים זרמי משנה כאלה ולהשתמש בהם, אם הם זמינים." + } + } + }, + "title": "הוסף מצלמה", + "description": "בצע את השלבים הבאים כדי להוסיף מצלמה חדשה להתקנת ה־Frigate שלך.", + "steps": { + "nameAndConnection": "שם וחיבור", + "probeOrSnapshot": "בדיקה (Probe) או צילום תמונה (Snapshot)", + "streamConfiguration": "הגדרות זרם", + "validationAndTesting": "אימות ובדיקה" + }, + "save": { + "success": "המצלמה החדשה {{cameraName}} נשמרה בהצלחה.", + "failure": "שגיאה בשמירת {{cameraName}}." + }, + "testResultLabels": { + "resolution": "רזולוציה", + "video": "וידיאו", + "audio": "אודיו", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "אנא ספק כתובת URL תקינה לזרם", + "testFailed": "בדיקת הזרם נכשלה: {{error}}" + } + }, + "cameraManagement": { + "title": "ניהול מצלמות", + "addCamera": "הוספת מצלמה חדשה", + "editCamera": "עריכת מצלמה:", + "selectCamera": "בחירת מצלמה", + "backToSettings": "חזרה להגדרות מצלמה", + "streams": { + "title": "הפעלה / השבתה של מצלמות", + "desc": "השבת מצלמה זמנית עד ש־Frigate יופעל מחדש. השבתת מצלמה עוצרת לחלוטין את העיבוד של Frigate עבור זרמי המצלמה הזו. זיהוי, הקלטה וניפוי שגיאות לא יהיו זמינים.
    \nהערה: פעולה זו לא משביתה את ה־restreams של go2rtc." + }, + "cameraConfig": { + "add": "הוספת מצלמה", + "edit": "עריכת מצלמה", + "description": "נהל את הגדרות המצלמה, כולל קלטי הזרמים והייעודים שלהם.", + "name": "שם מצלמה", + "nameRequired": "שם המצלמה הוא שדה חובה", + "nameLength": "שם המצלמה חייב להיות קצר מ־64 תווים.", + "namePlaceholder": "לדוגמה: front_door או תצוגת סקירה של החצר האחורית", + "enabled": "מופעל", + "ffmpeg": { + "inputs": "זרמי קלט", + "path": "נתיב זרם", + "pathRequired": "נתיב זרם הוא שדה חובה", + "roles": "ייעודים", + "rolesRequired": "נדרש לפחות ייעוד אחד", + "rolesUnique": "כל ייעוד (audio, detect, record) ניתן להקצות לזרם אחד בלבד", + "addInput": "הוסף זרם קלט", + "removeInput": "הסר זרם קלט", + "inputsRequired": "נדרש לפחות זרם קלט אחד", + "pathPlaceholder": "rtsp://..." + }, + "go2rtcStreams": "זרמי go2rtc", + "streamUrls": "כתובות URL של הזרמים", + "addUrl": "הוסף URL", + "addGo2rtcStream": "הוסף זרם go2rtc", + "toast": { + "success": "המצלמה {{cameraName}} נשמרה בהצלחה" + } + } + }, + "cameraReview": { + "title": "הגדרות סקירת מצלמה", + "object_descriptions": { + "title": "Generative AI תיאורי אובייקטים", + "desc": "הפעל/השבת זמנית תיאורי אובייקטים של Generative AI עבור מצלמה זו. כאשר האפשרות מושבתת, לא יתבקשו תיאורים שנוצרו ע״י AI עבור אובייקטים במעקב במצלמה זו." + }, + "review_descriptions": { + "title": "תיאורי סקירה של Generative AI", + "desc": "הפעל/השבת זמנית תיאורי סקירה של Generative AI עבור מצלמה זו. כאשר האפשרות מושבתת, לא יתבקשו תיאורים שנוצרו ע״י AI עבור פריטי סקירה במצלמה זו." + }, + "review": { + "title": "סקירה", + "desc": "הפעל/השבת זמנית התראות וזיהויים עבור מצלמה זו עד ש-Frigate יופעל מחדש. כאשר האפשרות מושבתת, לא ייווצרו פריטי סקירה חדשים. ", + "alerts": "התראות. ", + "detections": "זיהויים. " + }, + "reviewClassification": { + "title": "סיווג סקירה", + "desc": "Frigate מסווג פריטי סקירה ל־התראות ול־זיהויים. כברירת מחדל, כל אובייקט מסוג person ו־car נחשב ל־התראה. ניתן לדייק את הסיווג של פריטי הסקירה שלך באמצעות הגדרת אזורים נדרשים עבורם.", + "noDefinedZones": "לא הוגדרו אזורים למצלמה זו.", + "objectAlertsTips": "כל האובייקטים מסוג {{alertsLabels}} ב־{{cameraName}} יוצגו כהתראות.", + "zoneObjectAlertsTips": "כל האובייקטים מסוג {{alertsLabels}} שזוהו בתוך {{zone}} ב־{{cameraName}} יוצגו כהתראות.", + "objectDetectionsTips": "כל האובייקטים מסוג {{detectionsLabels}} שלא סווגו ב־{{cameraName}} יוצגו כזיהויים, ללא קשר לאיזה אזור הם נמצאים בו.", + "zoneObjectDetectionsTips": { + "text": "כל האובייקטים מסוג {{detectionsLabels}} שלא סווגו בתוך {{zone}} ב־{{cameraName}} יוצגו כזיהויים.", + "notSelectDetections": "כל האובייקטים מסוג {{detectionsLabels}} שזוהו בתוך {{zone}} ב־{{cameraName}} ושאינם מסווגים כהתראות יוצגו כזיהויים, ללא קשר לאיזה אזור הם נמצאים בו.", + "regardlessOfZoneObjectDetectionsTips": "כל האובייקטים מסוג {{detectionsLabels}} שלא סווגו ב־{{cameraName}} יוצגו כזיהויים, ללא קשר לאיזה אזור הם נמצאים בו." + }, + "unsavedChanges": "הגדרות סיווג סקירה שלא נשמרו עבור {{camera}}", + "selectAlertsZones": "בחר אזורים עבור התראות", + "selectDetectionsZones": "בחר אזורים עבור זיהויים", + "limitDetections": "הגבל זיהויים לאזורים מסוימים", + "toast": { + "success": "הגדרות סיווג הסקירה נשמרו. הפעל מחדש את Frigate כדי להחיל את השינויים." + } + } + }, + "roles": { + "management": { + "title": "ניהול תפקיד צופה", + "desc": "נהל תפקידי צופה מותאמים אישית ואת הרשאות הגישה שלהם למצלמות עבור מופע Frigate זה." + }, + "addRole": "הוסף תפקיד", + "table": { + "role": "תפקיד", + "cameras": "מצלמות", + "actions": "פעולות", + "noRoles": "לא נמצאו תפקידים מותאמים אישית.", + "editCameras": "ערוך מצלמות", + "deleteRole": "מחק תפקיד" + }, + "toast": { + "success": { + "createRole": "התפקיד {{role}} נוצר בהצלחה", + "updateCameras": "המצלמות עודכנו עבור התפקיד {{role}}", + "deleteRole": "התפקיד {{role}} נמחק בהצלחה", + "userRolesUpdated_one": "המשתמש {{count}} שהוקצה לתפקיד זה עודכן ל־צופה (viewer), שלו יש גישה לכל המצלמות.", + "userRolesUpdated_two": "{{count}} משתמשים שהוקצו לתפקיד זה עודכנו ל־צופה (viewer), שלו יש גישה לכל המצלמות.", + "userRolesUpdated_other": "" + }, + "error": { + "createRoleFailed": "נכשל ביצירת התפקיד: {{errorMessage}}", + "updateCamerasFailed": "נכשל בעדכון המצלמות: {{errorMessage}}", + "deleteRoleFailed": "נכשל במחיקת התפקיד: {{errorMessage}}", + "userUpdateFailed": "נכשל בעדכון תפקידי המשתמשים: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "צור תפקיד חדש", + "desc": "הוסף תפקיד חדש והגדר הרשאות גישה למצלמות." + }, + "editCameras": { + "title": "ערוך מצלמות לתפקיד", + "desc": "עדכן את גישת המצלמות עבור התפקיד {{role}}." + }, + "deleteRole": { + "title": "מחק תפקיד", + "desc": "לא ניתן לבטל פעולה זו. הפעולה תמחק לצמיתות את התפקיד ותעביר כל משתמש שהוקצה לתפקיד זה לתפקיד צופה (viewer), המעניק גישה לכל המצלמות.", + "warn": "האם אתה בטוח שברצונך למחוק את {{role}}?", + "deleting": "מוחק..." + }, + "form": { + "role": { + "title": "שם תפקיד", + "placeholder": "הזן שם תפקיד", + "desc": "מותר להשתמש רק באותיות, מספרים, נקודות וקווים תחתונים.", + "roleIsRequired": "שם תפקיד הוא שדה חובה", + "roleOnlyInclude": "שם התפקיד יכול לכלול רק אותיות, מספרים, נקודות או קווים תחתונים", + "roleExists": "כבר קיים תפקיד בשם זה." + }, + "cameras": { + "title": "מצלמות", + "desc": "בחר את המצלמות שלתפקיד זה יש גישה אליהן. נדרשת לפחות מצלמה אחת.", + "required": "חובה לבחור לפחות מצלמה אחת." + } + } + } + }, + "triggers": { + "documentTitle": "טריגרים", + "semanticSearch": { + "title": "חיפוש סמנטי מושבת", + "desc": "כדי להשתמש בטריגרים, יש להפעיל חיפוש סמנטי." + }, + "management": { + "title": "טריגרים", + "desc": "נהל טריגרים עבור {{camera}}. השתמש בסוג תמונה ממוזערת (Thumbnail) כדי להפעיל טריגרים על תמונות ממוזערות דומות לאובייקט שבחרת למעקב, ובסוג תיאור (Description) כדי להפעיל טריגרים על תיאורים דומים לטקסט שתגדיר." + }, + "addTrigger": "הוסף טריגר", + "table": { + "name": "שם", + "type": "סוג", + "content": "תוכן", + "threshold": "סף", + "actions": "פעולות", + "noTriggers": "לא הוגדרו טריגרים למצלמה זו.", + "edit": "עריכה", + "deleteTrigger": "מחק טריגר", + "lastTriggered": "הפעלה אחרונה" + }, + "type": { + "thumbnail": "תמונה ממוזערת", + "description": "תיאור" + }, + "actions": { + "notification": "שלח התראה", + "sub_label": "הוסף תווית משנה", + "attribute": "הוסף מאפיינים" + }, + "dialog": { + "createTrigger": { + "title": "צור טריגר", + "desc": "צור טריגר עבור המצלמה {{camera}}" + }, + "editTrigger": { + "title": "ערוך טריגר", + "desc": "ערוך את ההגדרות עבור הטריגר במצלמה {{camera}}" + }, + "deleteTrigger": { + "title": "מחק טריגר", + "desc": "האם אתה בטוח שברצונך למחוק את הטריגר {{triggerName}}? פעולה זו אינה ניתנת לביטול." + }, + "form": { + "name": { + "title": "שם", + "placeholder": "תן שם לטריגר", + "description": "הזן שם או תיאור ייחודיים לזיהוי הטריגר הזה", + "error": { + "minLength": "השדה חייב להכיל לפחות 2 תווים.", + "invalidCharacters": "השדה יכול להכיל רק אותיות, מספרים, קווים תחתונים (_) ומקפים (-).", + "alreadyExists": "כבר קיים טריגר בשם זה עבור מצלמה זו." + } + }, + "enabled": { + "description": "הפעל או השבת טריגר זה" + }, + "type": { + "title": "סוג", + "placeholder": "בחר סוג טריגר", + "description": "הפעל טריגר כאשר מזוהה תיאור דומה של אובייקט במעקב", + "thumbnail": "הפעל טריגר כאשר מזוהה תמונה ממוזערת דומה של אובייקט במעקב" + }, + "content": { + "title": "תוכן", + "imagePlaceholder": "בחר תמונה ממוזערת", + "textPlaceholder": "הזן תוכן טקסט", + "imageDesc": "מוצגות רק 100 התמונות הממוזערות האחרונות. אם אינך מוצא את התמונה הממוזערת הרצויה, אנא סקור אובייקטים מוקדמים יותר ב־Explore והגדר משם טריגר דרך התפריט.", + "textDesc": "הזן טקסט להפעלת פעולה זו כאשר מזוהה תיאור דומה של אובייקט במעקב.", + "error": { + "required": "נדרש תוכן." + } + }, + "threshold": { + "title": "סף", + "desc": "הגדר את סף הדמיון עבור טריגר זה. סף גבוה יותר מחייב התאמה קרובה יותר כדי להפעיל את הטריגר.", + "error": { + "min": "הסף חייב להיות לפחות 0", + "max": "הסף חייב להיות לכל היותר 1" + } + }, + "actions": { + "title": "פעולות", + "desc": "כברירת מחדל, Frigate שולח הודעת MQTT עבור כל הטריגרים. תוויות משנה (Sub Labels) מוסיפות את שם הטריגר לתווית האובייקט. מאפיינים (Attributes) הם מטא־נתונים הניתנים לחיפוש, הנשמרים בנפרד במטא־הנתונים של האובייקט במעקב.", + "error": { + "min": "חובה לבחור לפחות פעולה אחת." + } + } + } + }, + "wizard": { + "title": "צור טריגר", + "step1": { + "description": "הגדר את ההגדרות הבסיסיות של הטריגר שלך." + }, + "step2": { + "description": "הגדר את התוכן שיפעיל פעולה זו." + }, + "step3": { + "description": "הגדר את הסף והפעולות עבור טריגר זה." + }, + "steps": { + "nameAndType": "שם וסוג", + "configureData": "הגדר נתונים", + "thresholdAndActions": "סף ופעולות" + } + }, + "toast": { + "success": { + "createTrigger": "הטריגר {{name}} נוצר בהצלחה.", + "updateTrigger": "הטריגר {{name}} עודכן בהצלחה.", + "deleteTrigger": "הטריגר {{name}} נמחק בהצלחה." + }, + "error": { + "createTriggerFailed": "נכשל ביצירת הטריגר: {{errorMessage}}", + "updateTriggerFailed": "נכשל בעדכון הטריגר: {{errorMessage}}", + "deleteTriggerFailed": "נכשל במחיקת הטריגר: {{errorMessage}}" + } + } + } +} diff --git a/web/public/locales/he/views/system.json b/web/public/locales/he/views/system.json new file mode 100644 index 0000000..fa32918 --- /dev/null +++ b/web/public/locales/he/views/system.json @@ -0,0 +1,208 @@ +{ + "lastRefreshed": "רענון אחרון: ", + "stats": { + "ffmpegHighCpuUsage": "ל-{{camera}} יש צריכת מעבד גבוהה של FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "ל-{{camera}} יש צריכת CPU גבוהה ({{detectAvg}}%)", + "healthy": "המערכת פועלת בצורה תקינה", + "reindexingEmbeddings": "אינדקס מחדש של ההטמעות ({{processed}}% הושלם)", + "cameraIsOffline": "{{camera}} לא זמינה", + "detectIsSlow": "{{detect}} איטי ({{speed}} אלפיות שנייה)", + "detectIsVerySlow": "{{detect}} איטי מאוד ({{speed}} אלפיות שנייה)", + "shmTooLow": "יש להגדיל את הקצאת ‎/dev/shm‏ ({{total}} MB) לפחות ל־{{min}} MB." + }, + "documentTitle": { + "cameras": "מצב מצלמות - Frigate", + "storage": "מצב אחסון - Frigate", + "general": "סטטיסטיקה כללית - Frigate", + "enrichments": "סטטיסטיקת העשרה - Frigate", + "logs": { + "frigate": "לוגים - Frigate", + "go2rtc": "לוגים - Go2RTC - Frigate", + "nginx": "לוגים - Nginx - Frigate" + } + }, + "title": "מערכת", + "metrics": "מדדי מערכת", + "logs": { + "download": { + "label": "הורדת לוגים" + }, + "copy": { + "label": "העתק ללוח עריכה", + "success": "לוגים הועתקו ללוח העריכה", + "error": "לא ניתן היה להעתיק לוגים ללוח" + }, + "type": { + "label": "סוג", + "timestamp": "חותמת זמן", + "tag": "תג", + "message": "הודעה" + }, + "tips": "יומני רישום מוצגים בזרימה מהשרת", + "toast": { + "error": { + "fetchingLogsFailed": "שגיאה באחזור לוגים: {{errorMessage}}", + "whileStreamingLogs": "שגיאה בעת הזרמת לוגים: {{errorMessage}}" + } + } + }, + "general": { + "title": "כללי", + "detector": { + "title": "גלאים", + "inferenceSpeed": "מהירות זיהוי", + "temperature": "טמפרטורת הגלאי", + "cpuUsage": "ניצול מעבד על ידי הגלאי", + "memoryUsage": "שימוש בזיכרון על ידי הגלאי", + "cpuUsageInformation": "המעבד המשמש להכנת נתוני קלט ופלט אל/ממודלי זיהוי. ערך זה אינו מודד את השימוש בהסקה, גם אם נעשה שימוש במעבד גרפי או מאיץ." + }, + "hardwareInfo": { + "gpuMemory": "זיכרון GPU", + "title": "מידע על החומרה", + "gpuUsage": "שימוש GPU", + "gpuEncoder": "מקודד GPU", + "gpuDecoder": "מפענח GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "פלט Vainfo", + "returnCode": "קוד החזרה: {{code}}", + "processOutput": "פלט תהליך:", + "processError": "שגיאת תהליך:" + }, + "nvidiaSMIOutput": { + "title": "פלט SMI של Nvidia", + "name": "שם: {{name}}", + "driver": "מנהל התקן: {{driver}}", + "cudaComputerCapability": "יכולת חישוב CUDA: {{cuda_compute}}", + "vbios": "מידע על VBios: {{vbios}}" + }, + "closeInfo": { + "label": "סגור את המידע על ה-GPU" + }, + "copyInfo": { + "label": "העתק מידע על GPU" + }, + "toast": { + "success": "מידע על ה-GPU הועתק ללוח" + } + }, + "npuUsage": "שימוש ב-NPU", + "npuMemory": "NPU זיכרון", + "intelGpuWarning": { + "title": "אזהרת סטטיסטיקות GPU של Intel", + "message": "נתוני ה־GPU אינם זמינים", + "description": "זהו באג ידוע בכלי הדיווח של Intel לסטטיסטיקות GPU ‏(intel_gpu_top): לפעמים הוא “נשבר” ומתחיל להחזיר שוב ושוב שימוש GPU של 0%, גם במקרים שבהם ההאצה החומרתית וזיהוי האובייקטים כן עובדים תקין על ה־(i)GPU.\nזה לא באג של Frigate. אפשר לאתחל את ה־Host כדי לתקן את זה זמנית, וככה גם לוודא שה־GPU באמת עובד כמו שצריך.\nהתקלה הזו לא משפיעה על הביצועים." + } + }, + "otherProcesses": { + "title": "תהליכים אחרים", + "processCpuUsage": "ניצול CPU של התהליך", + "processMemoryUsage": "ניצול זיכרון של תהליך", + "series": { + "go2rtc": "go2rtc", + "recording": "מקליט", + "review_segment": "קטע סקירה", + "embeddings": "הטמעות", + "audio_detector": "זיהוי שמע" + } + } + }, + "enrichments": { + "infPerSecond": "מספר הסקות בשנייה", + "title": "העשרה", + "embeddings": { + "image_embedding": "הטמעת תמונה", + "text_embedding": "הטמעת טקסט", + "face_recognition": "זיהוי פנים", + "plate_recognition": "זיהוי לוחית רישוי", + "image_embedding_speed": "מהירות הטמעת תמונה", + "face_embedding_speed": "מהירות הטמעת פנים", + "face_recognition_speed": "מהירות זיהוי פנים", + "plate_recognition_speed": "מהירות זיהוי לוחית", + "text_embedding_speed": "מהירות הטמעת טקסט", + "yolov9_plate_detection_speed": "מהירות זיהוי לוחיות YOLOv9", + "yolov9_plate_detection": "זיהוי לוחיות YOLOv9", + "review_description": "תיאור סקירה", + "review_description_speed": "מהירות תיאור הסקירה", + "review_description_events_per_second": "תיאור סקירה", + "object_description": "תיאור אובייקט", + "object_description_speed": "מהירות תיאור האובייקט", + "object_description_events_per_second": "תיאור אובייקט", + "classification": "סיווג {{name}}", + "classification_speed": "מהירות סיווג {{name}}", + "classification_events_per_second": "אירועי סיווג לשנייה עבור {{name}}" + }, + "averageInf": "זמן הסקה ממוצע" + }, + "storage": { + "cameraStorage": { + "storageUsed": "אחסון", + "percentageOfTotalUsed": "אחוז מהסך הכל", + "bandwidth": "רוחב פס", + "unused": { + "title": "לא בשימוש", + "tips": "ייתכן שערך זה לא מייצג במדויק את השטח הפנוי הזמין ל-Frigate אם יש לך קבצים אחרים המאוחסנים בכונן שלך מעבר להקלטות של Frigate. Frigate אינו עוקב אחר ניצול האחסון מלבד להקלטות שלו." + }, + "title": "אחסון מצלמה", + "camera": "מצלמה", + "unusedStorageInformation": "מידע על אחסון שאינו בשימוש" + }, + "title": "אחסון", + "overview": "סקירה כללית", + "recordings": { + "title": "הקלטות", + "earliestRecording": "ההקלטה המוקדמת ביותר הזמינה:", + "tips": "ערך זה מייצג את סך האחסון בו משתמשים ההקלטות במסד הנתונים של Frigate. Frigate אינו עוקב אחר ניצול האחסון עבור כל הקבצים בדיסק שלך." + }, + "shm": { + "title": "הקצאת SHM (זיכרון משותף)", + "warning": "גודל ה־SHM הנוכחי של {{total}}MB קטן מדי. הגדל אותו לפחות ל־{{min_shm}}MB." + } + }, + "cameras": { + "title": "מצלמות", + "overview": "סקירה כללית", + "info": { + "aspectRatio": "יחס גובה-רוחב", + "cameraProbeInfo": "פרטי בדיקה של מצלמה {{camera}}", + "streamDataFromFFPROBE": "נתוני השידור מתקבלים באמצעות ffprobe.", + "fetching": "טוען נתוני מצלמה", + "stream": "זרם {{idx}}", + "video": "וידיאו:", + "codec": "מקודד:", + "resolution": "רזולוציה:", + "fps": "פריימים לשניה:", + "audio": "קול:", + "error": "שגיאה: {{error}}", + "tips": { + "title": "טוען נתוני מצלמה" + }, + "unknown": "לא ידוע" + }, + "label": { + "detect": "גילוי", + "skipped": "דילוג", + "ffmpeg": "FFmpeg", + "overallDetectionsPerSecond": "סך כל הזיהויים לשנייה", + "overallSkippedDetectionsPerSecond": "סה״כ זיהויים שדולגו לשנייה", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} לכידה", + "cameraDetect": "זיהוי {{camName}}", + "cameraDetectionsPerSecond": "מספר הזיהויים של {{camName}} לשנייה", + "cameraSkippedDetectionsPerSecond": "כמות הזיהויים שדולגו בשנייה – {{camName}}", + "capture": "לכידה", + "cameraFramesPerSecond": "{{camName}} פריימים לשנייה", + "camera": "מצלמה", + "overallFramesPerSecond": "סך כל הפריימים לשנייה" + }, + "toast": { + "success": { + "copyToClipboard": "העתקת נתוני הבדיקה בוצעה בהצלחה." + }, + "error": { + "unableToProbeCamera": "לא ניתן לבדוק את המצלמה: {{errorMessage}}" + } + }, + "framesAndDetections": "פריימים / זיהויים" + } +} diff --git a/web/public/locales/hi/audio.json b/web/public/locales/hi/audio.json new file mode 100644 index 0000000..0705110 --- /dev/null +++ b/web/public/locales/hi/audio.json @@ -0,0 +1,145 @@ +{ + "babbling": "बड़बड़ाना", + "yell": "चिल्लाना", + "whispering": "फुसफुसाना", + "crying": "रोना", + "laughter": "हँसना", + "singing": "गाना", + "chant": "जपना", + "mantra": "मंत्र", + "run": "भागना", + "sniff": "सूँघना", + "sneeze": "छींंकना", + "whistling": "सीटी बजाना", + "speech": "बोलना", + "sigh": "आह भरना", + "humming": "गुनगुनाना", + "child_singing": "बच्चे का गाना", + "groan": "कराहना", + "breathing": "साँस लेना", + "snoring": "खर्राटे लेना", + "cough": "खाँसना", + "throat_clearing": "गला साफ़ करना", + "footsteps": "कदमों की आहट", + "chewing": "चबाना", + "biting": "काटना", + "gargling": "गरारे करना", + "stomach_rumble": "पेट की गुड़गुड़ाहट", + "burping": "डकारना", + "hiccup": "हिचकी", + "fart": "पादना", + "heartbeat": "धड़कन", + "cheering": "जयकार करना", + "pets": "पालतू जानवर", + "animal": "जानवर", + "children_playing": "बच्चों का खेलना", + "crowd": "भीड़", + "dog": "कुत्ता", + "hiss": "फुफकारना", + "neigh": "हिनहिनाना", + "cattle": "मवेशी", + "moo": "रंभाहट", + "goat": "बकरी", + "bleat": "मेमियाहट", + "hands": "हाथ", + "pig": "सुअर", + "clapping": "ताली बजाना", + "chatter": "गपशप", + "finger_snapping": "उँगलियाँ चटकाना", + "bark": "भौंकना", + "cowbell": "गाय की घंटी", + "sheep": "भेड़", + "yip": "कूँकना", + "livestock": "पशुधन", + "horse": "घोड़ा", + "cat": "बिल्ली", + "chicken": "मुर्गी", + "wild_animals": "जंगली जानवर", + "bird": "पक्षी", + "chirp": "चहचहाना", + "roar": "दहाड़ना", + "pigeon": "कबूतर", + "crow": "कौआ", + "flapping_wings": "पंख फड़फड़ाना", + "dogs": "कुत्ते", + "insect": "कीड़ा", + "patter": "पटपटाहट", + "cymbal": "झांझ", + "tambourine": "डफली", + "orchestra": "वाद्यवृंद", + "wind_instrument": "वायु वाद्ययंत्र", + "bowed_string_instrument": "धनुष तार वाद्ययंत्र", + "harp": "हार्प", + "bell": "घंटी", + "church_bell": "गिरजाघर का घंटा", + "accordion": "अकोर्डियन", + "opera": "ओपेरा", + "disco": "डिस्को", + "jazz": "जैज़", + "dubstep": "डबस्टेप", + "song": "गीत", + "lullaby": "लोरी", + "sad_music": "दुखभरा संगीत", + "tender_music": "कोमल संगीत", + "wind": "हवा", + "wind_noise": "हवा की आवाज़", + "thunderstorm": "आंधी-तूफ़ान", + "thunder": "गर्जना", + "water": "पानी", + "rain": "बारिश", + "rain_on_surface": "सतह पर गिरती बारिश", + "waterfall": "झरना", + "ocean": "सागर", + "waves": "लहरें", + "stream": "धारा", + "steam": "भाप", + "vehicle": "वाहन", + "car": "गाड़ी", + "boat": "नाव", + "ship": "जहाज़", + "truck": "ट्रक", + "bus": "बस", + "motor_vehicle": "मोटर वाहन", + "motorboat": "इंजन वाली नाव", + "sailboat": "पाल वाली नाव", + "police_car": "पुलिस की गाड़ी", + "saxophone": "सैक्सोफोन", + "sitar": "सितार", + "music": "संगीत", + "snake": "साँप", + "mouse": "चूहा", + "wedding_music": "शादी का संगीत", + "buzz": "भनभनाहट", + "fire": "आग", + "caw": "कांव कांव करना", + "owl": "उल्लू", + "mosquito": "मच्छर", + "scary_music": "डरावना संगीत", + "duck": "बतख", + "hoot": "उल्लू की आवाज़", + "rustling_leaves": "खड़खड़ाते पत्ते", + "rats": "चूहे", + "cricket": "झिंगुर", + "fly": "मक्खी", + "frog": "मेंढक", + "croak": "टर्राना", + "guitar": "गिटार", + "tabla": "तबला", + "trumpet": "तुरही", + "brass_instrument": "पीतल वाद्ययंत्र", + "flute": "बाँसुरी", + "clarinet": "क्लैरिनेट", + "bicycle_bell": "साइकिल की घंटी", + "harmonica": "हारमोनिका", + "bagpipes": "बैगपाइप", + "angry_music": "क्रोधित संगीत", + "music_of_bollywood": "बॉलीवुड संगीत", + "happy_music": "खुशहाल संगीत", + "exciting_music": "रोमांचक संगीत", + "raindrop": "बारिश की बूंद", + "rowboat": "चप्पू वाली नाव", + "aircraft": "विमान", + "bicycle": "साइकिल", + "bellow": "गर्जना करना", + "motorcycle": "मोटरसाइकिल" +} diff --git a/web/public/locales/hi/common.json b/web/public/locales/hi/common.json new file mode 100644 index 0000000..d4c4335 --- /dev/null +++ b/web/public/locales/hi/common.json @@ -0,0 +1,8 @@ +{ + "time": { + "untilForTime": "{{time}} तक", + "untilForRestart": "जब तक फ्रिगेट पुनः रीस्टार्ट नहीं हो जाता।", + "untilRestart": "रीस्टार्ट होने में", + "ago": "{{timeAgo}} पहले" + } +} diff --git a/web/public/locales/hi/components/auth.json b/web/public/locales/hi/components/auth.json new file mode 100644 index 0000000..c58f433 --- /dev/null +++ b/web/public/locales/hi/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "password": "पासवर्ड", + "login": "प्रवेश करें", + "errors": { + "passwordRequired": "पासवर्ड आवश्यक है", + "rateLimit": "दर सीमा पार हो गई है। बाद में पुनः प्रयास करें।", + "unknownError": "अज्ञात त्रुटि। प्रविष्टियाँ जांचें।", + "usernameRequired": "प्रयोक्ता नाम आवश्यक है", + "webUnknownError": "अज्ञात त्रुटि। कंसोल प्रविष्टियाँ जांचें।", + "loginFailed": "लॉगिन असफल हुआ" + }, + "user": "प्रयोक्ता नाम" + } +} diff --git a/web/public/locales/hi/components/camera.json b/web/public/locales/hi/components/camera.json new file mode 100644 index 0000000..74c05d4 --- /dev/null +++ b/web/public/locales/hi/components/camera.json @@ -0,0 +1,10 @@ +{ + "group": { + "label": "कैमरा समूह", + "add": "कैमरा समूह जोड़ें", + "edit": "कैमरा समूह संपादित करें", + "delete": { + "label": "कैमरा समूह हटाएँ" + } + } +} diff --git a/web/public/locales/hi/components/dialog.json b/web/public/locales/hi/components/dialog.json new file mode 100644 index 0000000..bcf4cd0 --- /dev/null +++ b/web/public/locales/hi/components/dialog.json @@ -0,0 +1,10 @@ +{ + "restart": { + "title": "क्या आप निश्चित हैं कि आप फ्रिगेट को रीस्टार्ट करना चाहते हैं?", + "button": "रीस्टार्ट", + "restarting": { + "title": "फ्रिगेट रीस्टार्ट हो रहा है", + "content": "यह पृष्ठ {{countdown}} सेकंड में पुनः लोड होगा।" + } + } +} diff --git a/web/public/locales/hi/components/filter.json b/web/public/locales/hi/components/filter.json new file mode 100644 index 0000000..a89133b --- /dev/null +++ b/web/public/locales/hi/components/filter.json @@ -0,0 +1,10 @@ +{ + "filter": "फ़िल्टर", + "labels": { + "label": "लेबल", + "all": { + "title": "सभी लेबल", + "short": "लेबल" + } + } +} diff --git a/web/public/locales/hi/components/icons.json b/web/public/locales/hi/components/icons.json new file mode 100644 index 0000000..7f5236e --- /dev/null +++ b/web/public/locales/hi/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "चिह्न चुनें", + "search": { + "placeholder": "चिह्न खोजें..।" + } + } +} diff --git a/web/public/locales/hi/components/input.json b/web/public/locales/hi/components/input.json new file mode 100644 index 0000000..13b65c1 --- /dev/null +++ b/web/public/locales/hi/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "वीडियो डाउनलोड करें", + "toast": { + "success": "आपकी समीक्षा वीडियो डाउनलोड होना शुरू हो गई है।" + } + } + } +} diff --git a/web/public/locales/hi/components/player.json b/web/public/locales/hi/components/player.json new file mode 100644 index 0000000..e5e63a8 --- /dev/null +++ b/web/public/locales/hi/components/player.json @@ -0,0 +1,8 @@ +{ + "noRecordingsFoundForThisTime": "इस समय का कोई रिकॉर्डिंग नहीं मिला", + "noPreviewFound": "कोई प्रीव्यू नहीं मिला", + "noPreviewFoundFor": "{{cameraName}} के लिए कोई पूर्वावलोकन नहीं मिला", + "submitFrigatePlus": { + "title": "इस फ्रेम को फ्रिगेट+ पर सबमिट करें?" + } +} diff --git a/web/public/locales/hi/config/cameras.json b/web/public/locales/hi/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hi/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hi/config/global.json b/web/public/locales/hi/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hi/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hi/config/groups.json b/web/public/locales/hi/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hi/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hi/config/validation.json b/web/public/locales/hi/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hi/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hi/objects.json b/web/public/locales/hi/objects.json new file mode 100644 index 0000000..a4e93c3 --- /dev/null +++ b/web/public/locales/hi/objects.json @@ -0,0 +1,18 @@ +{ + "horse": "घोड़ा", + "sheep": "भेड़", + "bark": "भौंकना", + "animal": "जानवर", + "dog": "कुत्ता", + "cat": "बिल्ली", + "goat": "बकरी", + "boat": "नाव", + "bus": "बस", + "bird": "पक्षी", + "mouse": "चूहा", + "vehicle": "वाहन", + "car": "गाड़ी", + "person": "व्यक्ति", + "bicycle": "साइकिल", + "motorcycle": "मोटरसाइकिल" +} diff --git a/web/public/locales/hi/views/chat.json b/web/public/locales/hi/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hi/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hi/views/classificationModel.json b/web/public/locales/hi/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hi/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hi/views/configEditor.json b/web/public/locales/hi/views/configEditor.json new file mode 100644 index 0000000..784f8ec --- /dev/null +++ b/web/public/locales/hi/views/configEditor.json @@ -0,0 +1,15 @@ +{ + "saveOnly": "केवल सहेजें", + "saveAndRestart": "सहेजें और पुनः प्रारंभ करें", + "configEditor": "विन्यास संपादक", + "copyConfig": "विन्यास कॉपी करें", + "toast": { + "error": { + "savingError": "विन्यास सहेजने में त्रुटि हुई" + }, + "success": { + "copyToClipboard": "विन्यास क्लिपबोर्ड पर कॉपी कर लिया गया है।" + } + }, + "documentTitle": "विन्यास संपादक - Frigate" +} diff --git a/web/public/locales/hi/views/events.json b/web/public/locales/hi/views/events.json new file mode 100644 index 0000000..ae20914 --- /dev/null +++ b/web/public/locales/hi/views/events.json @@ -0,0 +1,8 @@ +{ + "alerts": "अलर्टस", + "detections": "खोजें", + "motion": { + "label": "गति", + "only": "केवल गति" + } +} diff --git a/web/public/locales/hi/views/explore.json b/web/public/locales/hi/views/explore.json new file mode 100644 index 0000000..daafb9c --- /dev/null +++ b/web/public/locales/hi/views/explore.json @@ -0,0 +1,8 @@ +{ + "documentTitle": "अन्वेषण करें - फ्रिगेट", + "generativeAI": "जनरेटिव ए आई", + "exploreMore": "और अधिक {{label}} वस्तुओं का अन्वेषण करें", + "exploreIsUnavailable": { + "title": "अन्वेषण अनुपलब्ध है" + } +} diff --git a/web/public/locales/hi/views/exports.json b/web/public/locales/hi/views/exports.json new file mode 100644 index 0000000..b9e86da --- /dev/null +++ b/web/public/locales/hi/views/exports.json @@ -0,0 +1,6 @@ +{ + "documentTitle": "निर्यात - फ्रिगेट", + "search": "खोजें", + "noExports": "कोई निर्यात नहीं मिला", + "deleteExport": "निर्यात हटाएँ" +} diff --git a/web/public/locales/hi/views/faceLibrary.json b/web/public/locales/hi/views/faceLibrary.json new file mode 100644 index 0000000..b305282 --- /dev/null +++ b/web/public/locales/hi/views/faceLibrary.json @@ -0,0 +1,7 @@ +{ + "description": { + "addFace": "फेस लाइब्रेरी में नया संग्रह जोड़ने की प्रक्रिया को आगे बढ़ाएं।", + "placeholder": "इस संग्रह का नाम बताएं", + "invalidName": "अमान्य नाम. नाम में केवल अक्षर, संख्याएँ, रिक्त स्थान, एपॉस्ट्रॉफ़ी, अंडरस्कोर और हाइफ़न ही शामिल हो सकते हैं।" + } +} diff --git a/web/public/locales/hi/views/live.json b/web/public/locales/hi/views/live.json new file mode 100644 index 0000000..9c7edbe --- /dev/null +++ b/web/public/locales/hi/views/live.json @@ -0,0 +1,5 @@ +{ + "documentTitle": "लाइव - फ्रिगेट", + "documentTitle.withCamera": "{{camera}} - लाइव - फ्रिगेट", + "lowBandwidthMode": "कम बैंडविड्थ मोड" +} diff --git a/web/public/locales/hi/views/motionSearch.json b/web/public/locales/hi/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hi/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hi/views/recording.json b/web/public/locales/hi/views/recording.json new file mode 100644 index 0000000..a9846e4 --- /dev/null +++ b/web/public/locales/hi/views/recording.json @@ -0,0 +1,11 @@ +{ + "calendar": "पंचांग", + "toast": { + "error": { + "noValidTimeSelected": "कोई मान्य समय सीमा चयनित नहीं है", + "endTimeMustAfterStartTime": "समाप्ति समय प्रारंभ समय के बाद होना चाहिए" + } + }, + "export": "निर्यात", + "filter": "फ़िल्टर" +} diff --git a/web/public/locales/hi/views/replay.json b/web/public/locales/hi/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hi/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hi/views/search.json b/web/public/locales/hi/views/search.json new file mode 100644 index 0000000..2ea0c8c --- /dev/null +++ b/web/public/locales/hi/views/search.json @@ -0,0 +1,5 @@ +{ + "search": "खोजें", + "savedSearches": "सहेजी गई खोजें", + "searchFor": "{{inputValue}} खोजें" +} diff --git a/web/public/locales/hi/views/settings.json b/web/public/locales/hi/views/settings.json new file mode 100644 index 0000000..d9bf27f --- /dev/null +++ b/web/public/locales/hi/views/settings.json @@ -0,0 +1,7 @@ +{ + "documentTitle": { + "default": "सेटिंग्स - फ्रिगेट", + "authentication": "प्रमाणीकरण सेटिंग्स - फ्रिगेट", + "camera": "कैमरा सेटिंग्स - फ्रिगेट" + } +} diff --git a/web/public/locales/hi/views/system.json b/web/public/locales/hi/views/system.json new file mode 100644 index 0000000..23bafa3 --- /dev/null +++ b/web/public/locales/hi/views/system.json @@ -0,0 +1,7 @@ +{ + "documentTitle": { + "cameras": "कैमरा आँकड़े - फ्रिगेट", + "storage": "भंडारण आँकड़े - फ्रिगेट", + "general": "सामान्य आँकड़े - फ्रिगेट" + } +} diff --git a/web/public/locales/hr/audio.json b/web/public/locales/hr/audio.json new file mode 100644 index 0000000..8531e48 --- /dev/null +++ b/web/public/locales/hr/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Govor", + "babbling": "Brbljanje", + "bicycle": "Bicikl", + "yell": "Vikanje", + "car": "Automobil", + "bellow": "Rika", + "motorcycle": "Motocikl", + "whispering": "Šaptanje", + "bus": "Autobus", + "laughter": "Smijeh", + "train": "Vlak", + "snicker": "Smješkanje", + "boat": "Brod", + "crying": "Plakanje", + "singing": "Pjevanje", + "choir": "Zbor", + "yodeling": "Jodlanje", + "mantra": "Mantra", + "bird": "Ptica", + "child_singing": "Dijete pjeva", + "cat": "Mačka", + "dog": "Pas", + "horse": "Konj", + "sheep": "Ovca", + "whoop": "Ups", + "sigh": "Uzdah", + "chant": "Pjevanje", + "synthetic_singing": "Sintetičko pjevanje", + "rapping": "Repanje", + "humming": "Pjevušenje", + "groan": "Jauk", + "grunt": "Mrmljanje", + "whistling": "Zviždanje", + "breathing": "Disanje", + "wheeze": "Piskanje", + "snoring": "Hrkanje", + "gasp": "Izdisaj", + "pant": "Dahćanje", + "snort": "Šmrk", + "cough": "Kašalj", + "skateboard": "Skejtboard", + "door": "Vrata", + "mouse": "Miš", + "keyboard": "Tipkovnica", + "sink": "Sudoper", + "blender": "Blender", + "clock": "Sat", + "scissors": "Škare", + "hair_dryer": "Fen", + "toothbrush": "Četkica za zube", + "vehicle": "Vozilo", + "animal": "Životinja", + "bark": "Kora", + "goat": "Koza", + "camera": "Kamera", + "throat_clearing": "Pročišćavanje grla", + "sneeze": "Kihati", + "sniff": "Njuškanje", + "run": "Trčanje", + "shuffle": "Geganje", + "footsteps": "Koraci", + "chewing": "Žvakanje", + "biting": "Grizenje", + "gargling": "Grgljanje", + "stomach_rumble": "Kruljenje u želucu", + "burping": "Podrigivanje", + "hiccup": "Štucanje", + "fart": "Prdac", + "hands": "Ruke", + "finger_snapping": "Pucketanje prstima", + "clapping": "Pljesak", + "heartbeat": "Otkucaji srca", + "heart_murmur": "Šum na srcu", + "cheering": "Navijanje", + "applause": "Pljesak", + "chatter": "Brbljanje", + "crowd": "Publika", + "children_playing": "Djeca se igraju", + "pets": "Kućni ljubimci", + "yip": "Kevtanje", + "howl": "Zavijanje", + "bow_wow": "Bow Wow", + "growling": "Režanje", + "whimper_dog": "Ps Cvilenje", + "purr": "Purr", + "meow": "Mijau", + "hiss": "Šuštanje", + "caterwaul": "Caterlaul", + "livestock": "Stočarstvo", + "clip_clop": "Clip Clop", + "neigh": "Njiši", + "cattle": "Goveda", + "moo": "Muu", + "cowbell": "Kravlje zvono", + "pig": "Svinja", + "oink": "Oink", + "bleat": "Blejanje", + "fowl": "Perad", + "chicken": "Piletina", + "cluck": "Kljuc", + "cock_a_doodle_doo": "Kukurikurik", + "turkey": "Turska", + "gobble": "Halapljivo jedenje", + "duck": "Patka", + "quack": "Kvak", + "goose": "Guska", + "honk": "Truba", + "wild_animals": "Divlje životinje", + "roaring_cats": "Rikuće mačke", + "roar": "Rika", + "chirp": "Cvrkut", + "squawk": "Krik", + "pigeon": "Golub", + "coo": "Cvrkut", + "crow": "Vrana", + "caw": "Krak", + "owl": "Sova", + "hoot": "Hookanje", + "flapping_wings": "Mahanje krilima", + "dogs": "Psi", + "rats": "Štakori", + "patter": "Patkanje", + "insect": "Insekt", + "cricket": "Kriket", + "mosquito": "Komarac", + "fly": "Leti", + "buzz": "Bzz", + "frog": "Žaba", + "croak": "Krek", + "snake": "Zmija", + "rattle": "Zveckanje", + "whale_vocalization": "Vokalizacija kita", + "music": "Glazba", + "musical_instrument": "Glazbeni instrument", + "plucked_string_instrument": "Trzajući žičani instrument", + "guitar": "Gitara", + "electric_guitar": "Električna gitara", + "bass_guitar": "Bas gitara", + "acoustic_guitar": "Akustična gitara", + "steel_guitar": "Steel gitara", + "tapping": "Tapkanje", + "strum": "Tunganje", + "banjo": "Banjo (Instrument)", + "sitar": "Sitar (Instrument)", + "mandolin": "Mandolina", + "zither": "Cither", + "ukulele": "Ukulele (Instrument)", + "piano": "Klavir", + "electric_piano": "Električni klavir", + "organ": "Orgulje", + "electronic_organ": "Elektroničke orgulje", + "hammond_organ": "Hammond orgulje", + "synthesizer": "Sintesajzer", + "sampler": "Sampler (Instrument)", + "harpsichord": "Čembalo", + "percussion": "Udaraljke", + "drum_kit": "Bubnjarski set", + "drum_machine": "Bubnjarski stroj", + "drum": "Bubanj", + "snare_drum": "Doboš", + "rimshot": "Rimshot (udaranje po rubu bubnja)", + "drum_roll": "Bubnjarski uvod", + "bass_drum": "Bas bubanj", + "timpani": "Timpani bubnjevi", + "tabla": "Tabla", + "cymbal": "Činela", + "hi_hat": "Hi-Hat bubanj", + "wood_block": "Drveni blok", + "tambourine": "Tamburin", + "maraca": "Maraca (Instrument)", + "gong": "Gong (Instrument)", + "tubular_bells": "Tubular Bells (Instrument)", + "mallet_percussion": "Mallet udaraljke", + "marimba": "Marimba (Instrument)", + "glockenspiel": "Glockenspiel (Instrument)", + "vibraphone": "Vibrafon", + "steelpan": "Steelpan (Instrument)", + "orchestra": "Orkestar", + "brass_instrument": "Limeni instrumenti", + "french_horn": "Francuski rog", + "trumpet": "Truba", + "trombone": "Trombon", + "bowed_string_instrument": "Gudački žičani instrument", + "string_section": "Gudačka sekcija", + "violin": "Violina", + "pizzicato": "Pizzicato (Instrument)", + "cello": "Violončelo", + "double_bass": "Kontrabas", + "wind_instrument": "Puhački instrument", + "flute": "Flauta", + "saxophone": "Saksofon", + "clarinet": "Klarinet", + "harp": "Harfa", + "bell": "Zvono", + "church_bell": "Crkveno zvono", + "jingle_bell": "Zvončić", + "bicycle_bell": "Biciklističko zvono", + "tuning_fork": "Vilica za ugađanje", + "chime": "Zvono", + "wind_chime": "Zvono na vjetru", + "harmonica": "Usna harmonika", + "accordion": "Harmonika", + "bagpipes": "Gajde", + "didgeridoo": "Didgeridoo (Instrument)", + "theremin": "Theremin (Instrument)", + "singing_bowl": "Pjevajuća zdjela", + "scratching": "Grebanje", + "pop_music": "Pop glazba", + "hip_hop_music": "Hip-hop glazba", + "beatboxing": "Beatbox", + "rock_music": "Rock glazba", + "heavy_metal": "Heavy Metal (žanr rock glazbe)", + "punk_rock": "Punk Rock (žanr glazbe)", + "grunge": "Grunge (žanr glazbe)", + "progressive_rock": "Progresivni rock", + "rock_and_roll": "Rock and Roll (žanr glazbe)", + "psychedelic_rock": "Psihodelični rock", + "rhythm_and_blues": "Rhythm and Blues (žanr glazbe)", + "soul_music": "Soul glazba", + "reggae": "Reggae (žanr glazbe)", + "country": "Zemlja", + "swing_music": "Swing glazba", + "bluegrass": "Bluegrass (žanr glazbe)", + "funk": "Funk (žanr glazbe)", + "folk_music": "Narodna glazba", + "middle_eastern_music": "Bliskoistočna glazba", + "jazz": "Jazz (žanr glazbe)", + "disco": "Disco (žanr glazbe)", + "classical_music": "Klasična glazba", + "opera": "Opera", + "electronic_music": "Elektronička glazba", + "house_music": "House glazba", + "techno": "Techno (žanr glazbe)", + "dubstep": "Dubstep (žanr glazbe)", + "drum_and_bass": "Drum and Bass (žanr glazbe)", + "electronica": "Elektronika", + "electronic_dance_music": "Elektronička plesna glazba", + "ambient_music": "Ambijentalna glazba", + "trance_music": "Trance glazba", + "music_of_latin_america": "Glazba Latinske Amerike", + "salsa_music": "Salsa glazba", + "flamenco": "Flamenco (žanr glazbe)", + "blues": "Blues (žanr glazbe)", + "music_for_children": "Glazba za djecu", + "new-age_music": "New Age glazba", + "vocal_music": "Vokalna glazba", + "a_capella": "A Capella (Izvedba glazbe bez instrumenata)", + "music_of_africa": "Glazba Afrike", + "afrobeat": "Afrobeat (žanr glazbe)", + "christian_music": "Kršćanska glazba", + "gospel_music": "Gospel glazba", + "music_of_asia": "Glazba Azije", + "carnatic_music": "Karnatska glazba", + "music_of_bollywood": "Glazba Bollywooda", + "ska": "Ska (žanr glazbe)", + "traditional_music": "Tradicionalna glazba", + "independent_music": "Nezavisna glazba", + "song": "Pjesma", + "background_music": "Pozadinska glazba", + "theme_music": "Tematska glazba", + "jingle": "Jingle", + "soundtrack_music": "Glazba za glazbu", + "lullaby": "Uspavanka", + "video_game_music": "Glazba iz videoigara", + "christmas_music": "Božićna glazba", + "dance_music": "Plesna glazba", + "wedding_music": "Svadbena glazba", + "happy_music": "Sretna glazba", + "sad_music": "Tužna glazba", + "tender_music": "Nježna glazba", + "exciting_music": "Uzbudljiva glazba", + "angry_music": "Ljutita glazba", + "scary_music": "Strašna glazba", + "wind": "Vjetar", + "rustling_leaves": "Šuštanje lišća", + "wind_noise": "Buka vjetra", + "thunderstorm": "Grmljavinska oluja", + "thunder": "Grmljavina", + "water": "Voda", + "rain": "Kiša", + "raindrop": "Kap kiše", + "rain_on_surface": "Kiša na površini", + "stream": "Potok", + "waterfall": "Vodopad", + "ocean": "Ocean", + "waves": "Valovi", + "steam": "Parni vlakovi", + "gurgling": "Grgljanje", + "fire": "Požar", + "crackle": "Pucketanje", + "sailboat": "Jedrilica", + "rowboat": "Čamac na vesla", + "motorboat": "Motorni čamac", + "ship": "Brod", + "motor_vehicle": "Motorna vozila", + "toot": "Tut", + "car_alarm": "Automobilski alarm", + "power_windows": "Električni prozori", + "skidding": "Klizanje", + "tire_squeal": "Škripa guma", + "car_passing_by": "Prolazak automobila", + "race_car": "Trkaći automobil", + "truck": "Kamion", + "air_brake": "Zračna kočnica", + "air_horn": "Zračni rog", + "reversing_beeps": "Zvuk unatrag", + "ice_cream_truck": "Kamion za sladoled", + "emergency_vehicle": "Vozilo hitne pomoći", + "police_car": "Policijski automobil", + "ambulance": "Hitna pomoć", + "fire_engine": "Vatrogasno vozilo", + "traffic_noise": "Buka prometa", + "rail_transport": "Željeznički promet", + "train_whistle": "Zviždaljka vlaka", + "train_horn": "Sirena vlaka", + "railroad_car": "Željeznički vagon", + "train_wheels_squealing": "Škripa kotača vlaka", + "subway": "Podzemna željeznica", + "aircraft": "Zrakoplovi", + "aircraft_engine": "Zrakoplovni motor", + "jet_engine": "Mlazni motor", + "propeller": "Propeler", + "helicopter": "Helikopter", + "fixed-wing_aircraft": "Zrakoplovi s fiksnim krilima", + "engine": "Motor", + "light_engine": "Laka lokomotiva", + "dental_drill's_drill": "Dentalna bušilica", + "lawn_mower": "Kosilica za travu", + "chainsaw": "Motorna pila", + "medium_engine": "Srednji motor", + "heavy_engine": "Teški motor", + "engine_knocking": "Kucanje motora", + "engine_starting": "Pokretanje motora", + "idling": "Rad u praznom hodu", + "accelerating": "Ubrzavanje", + "doorbell": "Zvono na vratima", + "ding-dong": "Ding-Dong", + "sliding_door": "Klizna vrata", + "slam": "Slam (žanr glazbe)", + "knock": "Kuc", + "tap": "Tap", + "squeak": "Cvrkut", + "cupboard_open_or_close": "Otvaranje ili zatvaranje ormara", + "drawer_open_or_close": "Otvaranje ili zatvaranje ladice", + "dishes": "Jela", + "cutlery": "Pribor za jelo", + "chopping": "Sjeckanje", + "frying": "Prženje", + "microwave_oven": "Mikrovalna pećnica", + "water_tap": "Vodovodna slavina", + "bathtub": "Kada", + "toilet_flush": "Ispiranje toaleta", + "electric_toothbrush": "Električna četkica za zube", + "vacuum_cleaner": "Usisavač", + "zipper": "Patentni zatvarač", + "keys_jangling": "Zvuk ključeva", + "coin": "Novčić", + "electric_shaver": "Električni brijač", + "shuffling_cards": "Miješanje karata", + "typing": "Tipkanje", + "typewriter": "Pisaća mašina", + "computer_keyboard": "Računalna tipkovnica", + "writing": "Pisanje", + "alarm": "Alarm", + "telephone": "Telefon", + "telephone_bell_ringing": "Zvono telefona zvoni", + "ringtone": "Melodija zvona", + "telephone_dialing": "Telefonsko biranje", + "dial_tone": "Ton biranja", + "busy_signal": "Zauzeti signal", + "alarm_clock": "Budilica", + "siren": "Sirena", + "civil_defense_siren": "Sirena civilne zaštite", + "buzzer": "Buzzer (Uređaj)", + "smoke_detector": "Detektor dima", + "fire_alarm": "Protupožarni alarm", + "foghorn": "Maglenka", + "whistle": "Zviždaljka", + "steam_whistle": "Parna zviždaljka", + "mechanisms": "Mehanizmi", + "ratchet": "Zupčanik sa zaporom (ratchet)", + "tick": "Tik", + "tick-tock": "Tik-tak", + "gears": "Zupčanici", + "pulleys": "Koloture", + "sewing_machine": "Šivaći stroj", + "mechanical_fan": "Mehanički ventilator", + "air_conditioning": "Klima uređaj", + "cash_register": "Blagajna", + "printer": "Pisač", + "single-lens_reflex_camera": "Jednooki refleksni fotoaparat", + "tools": "Alati", + "hammer": "Čekić", + "jackhammer": "Pneumatski čekić", + "sawing": "Piljenje", + "filing": "Podnošenje", + "sanding": "Brušenje", + "power_tool": "Električni alat", + "drill": "Vježba", + "explosion": "Eksplozija", + "gunshot": "Pucanj", + "machine_gun": "Mitraljez", + "fusillade": "Pucnjava", + "artillery_fire": "Topnička paljba", + "cap_gun": "Cap Gun", + "fireworks": "Vatromet", + "firecracker": "Petarda", + "burst": "Prasak", + "eruption": "Erupcija", + "boom": "Bum", + "wood": "Drvo", + "chop": "Brzo.", + "splinter": "Rascijepanje", + "crack": "Pukotina", + "glass": "Staklo", + "chink": "Kinez", + "shatter": "Razbijanje", + "silence": "Tišina", + "sound_effect": "Zvučni efekt", + "environmental_noise": "Okolišna buka", + "static": "Statički", + "white_noise": "Bijeli šum", + "pink_noise": "Ružičasti šum", + "television": "Televizija", + "radio": "Radio", + "field_recording": "Terensko snimanje", + "scream": "Vrisak", + "sodeling": "Sodeling", + "chird": "Chird", + "change_ringing": "Trčanje zvona", + "shofar": "Šofar", + "liquid": "Tekućina", + "splash": "Pljusak", + "slosh": "Pljusak", + "squish": "Zgnječenje", + "drip": "Kapanje", + "pour": "Usipanje", + "trickle": "Kapljanje", + "gush": "Brzo izlijevanje", + "fill": "Napuni", + "spray": "Prskanje", + "pump": "Pumpa", + "stir": "Miješaj", + "boiling": "Kuhanje", + "sonar": "Sonar", + "arrow": "Strijela", + "whoosh": "Whoosh", + "thump": "Tup", + "thunk": "Tup", + "electronic_tuner": "Elektronički tuner", + "effects_unit": "Jedinica za efekte", + "chorus_effect": "Efekt zbora", + "basketball_bounce": "Košarkaški odskok", + "bang": "Bam", + "slap": "Pljusak", + "whack": "Udarac", + "smash": "Slamanje", + "breaking": "Razbijanje", + "bouncing": "Skakanje", + "whip": "Bič", + "flap": "Flop", + "scratch": "Grebanje", + "scrape": "Struganje", + "rub": "Trljanje", + "roll": "Rolanje", + "crushing": "Drobljenje", + "crumpling": "Gužvanje", + "tearing": "Razdiruća", + "beep": "Bip", + "ping": "Ping", + "ding": "Ding", + "clang": "Klang", + "squeal": "Cviljenje", + "creak": "Škripa", + "rustle": "Šuštanje", + "whir": "Šuštanje", + "clatter": "Zveket", + "sizzle": "Crvčanje", + "clicking": "Klikovi", + "clickety_clack": "Klikety klak", + "rumble": "Tutnjanje", + "plop": "Plop", + "hum": "Šum", + "zing": "Cing", + "boing": "Boing", + "crunch": "Krckanje", + "sine_wave": "Sinusni val", + "harmonic": "Harmonik", + "chirp_tone": "Ton cvrkuta", + "pulse": "Puls", + "inside": "Unutra", + "outside": "Izvana", + "reverberation": "Reverberacija", + "echo": "Jeka", + "noise": "Buka", + "mains_hum": "Zujanje glavnih zvučnika", + "distortion": "Izobličenje", + "sidetone": "Bočni Ton", + "cacophony": "Kakofonija", + "throbbing": "Pulsirajuća", + "vibration": "Vibracija" +} diff --git a/web/public/locales/hr/common.json b/web/public/locales/hr/common.json new file mode 100644 index 0000000..8206a7d --- /dev/null +++ b/web/public/locales/hr/common.json @@ -0,0 +1,306 @@ +{ + "time": { + "untilForTime": "Do {{time}}", + "untilForRestart": "Dok se Frigate ponovno pokrene.", + "untilRestart": "Do ponovnog pokretanja", + "justNow": "Upravo", + "today": "Danas", + "yesterday": "Jučer", + "last7": "Zadnjih 7 dana", + "last14": "Zadnjih 14 dana", + "last30": "Zadnjih 30 dana", + "thisWeek": "Ovaj tjedan", + "lastWeek": "Prošli tjedan", + "thisMonth": "Ovaj mjesec", + "lastMonth": "Prošli mjesec", + "5minutes": "5 minuta", + "10minutes": "10 minuta", + "30minutes": "30 minuta", + "1hour": "1 sat", + "12hours": "12 sati", + "24hours": "24 sata", + "pm": "pm", + "am": "am", + "ago": "prije {{timeAgo}}", + "yr": "{{time}}g", + "year_one": "{{time}} godina", + "year_few": "{{time}} godine", + "year_other": "{{time}} godina", + "mo": "{{time}}mj", + "month_one": "{{time}} mjesec", + "month_few": "{{time}} mjeseca", + "month_other": "{{time}} mjeseci", + "day_one": "{{time}} dan", + "day_few": "{{time}} dana", + "day_other": "{{time}} dana", + "h": "{{time}}h", + "hour_one": "{{time}} sat", + "hour_few": "{{time}} sata", + "hour_other": "{{time}} sati", + "minute_one": "{{time}} minuta", + "minute_few": "{{time}} minute", + "minute_other": "{{time}} minuta", + "second_one": "{{time}} sekunda", + "second_few": "{{time}} sekunde", + "second_other": "{{time}} sekundi", + "d": "{{time}}d", + "m": "{{time}}m", + "s": "{{time}}s", + "formattedTimestamp": { + "12hour": "d. MMM, h:mm:ss aaa", + "24hour": "d. MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d. MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d. MMM, yyyy", + "24hour": "d. MMM, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d. MMM yyyy, h:mm aaa", + "24hour": "d. MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d. MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "inProgress": "U tijeku", + "invalidStartTime": "Nevažeće vrijeme početka", + "invalidEndTime": "Nevažeće vrijeme završetka", + "never": "Nikad" + }, + "menu": { + "live": { + "cameras": { + "count_one": "{{count}} kamera", + "count_few": "{{count}} kamere", + "count_other": "{{count}} kamera", + "title": "Kamere" + }, + "title": "Uživo", + "allCameras": "Sve Kamere" + }, + "system": "Sustav", + "systemMetrics": "Metrike sustava", + "configuration": "Konfiguracija", + "systemLogs": "Zapisnici sustava", + "settings": "Postavke", + "configurationEditor": "Uređivač konfiguracije", + "languages": "Jezici", + "language": { + "en": "Engleski", + "es": "Španjolski", + "zhCN": "简体中文 (Pojednostavljeni Kineski)", + "hi": "हिन्दी (Hindi)", + "fr": "Francuski", + "ar": "العربية (Arapski)", + "pt": "Portugalski", + "ptBR": "Brazilski Portugalski", + "ru": "Ruski", + "de": "Njemački", + "ja": "Japanski", + "tr": "Turski", + "it": "Talijanski", + "nl": "Nizozemski", + "sv": "Švedski", + "cs": "Češki", + "nb": "Norveški bokmål", + "ko": "Korejski", + "vi": "Vietnamski", + "fa": "Perzijski", + "pl": "Poljski", + "uk": "Ukrajinski", + "he": "Hebrejski", + "el": "Grčki", + "ro": "Rumunjski", + "hu": "Mađarski", + "fi": "Finski", + "da": "Danski", + "sk": "Slovački", + "yue": "Kantonščina", + "th": "Tajski", + "ca": "Katalonski", + "sr": "Srpski", + "sl": "Slovenski", + "lt": "Litvanski", + "bg": "Bulgarski", + "gl": "Galicijski", + "id": "Indonezijski", + "ur": "Urdu", + "withSystem": { + "label": "Koristi postavke sustava za jezik" + } + }, + "appearance": "Izgled", + "darkMode": { + "label": "Tamni način", + "light": "Svijetla", + "dark": "Tamna", + "withSystem": { + "label": "Koristi postavke sustava za svijetli ili tamni način rada" + } + }, + "withSystem": "Sustav", + "theme": { + "label": "Tema", + "blue": "Plava", + "green": "Zelena", + "nord": "Nord", + "red": "Crvena", + "highcontrast": "Visoki Kontrast", + "default": "Zadana" + }, + "help": "Pomoć", + "documentation": { + "title": "Dokumentacija", + "label": "Frigate dokumentacija" + }, + "restart": "Ponovno pokreni Frigate", + "review": "Pregled", + "explore": "Istraži", + "export": "Izvezi", + "uiPlayground": "Igralište korisničkog sučelja", + "faceLibrary": "Biblioteka Lica", + "classification": "Klasifikacija", + "user": { + "title": "Korisnik", + "account": "Račun", + "current": "Trenutni Korisnik: {{user}}", + "anonymous": "anonimno", + "logout": "Odjava", + "setPassword": "Postavi Lozinku" + } + }, + "button": { + "save": "Spremi", + "apply": "Primjeni", + "reset": "Resetiraj", + "done": "Gotovo", + "enabled": "Omogućeno", + "enable": "Omogući", + "disabled": "Onemogućeno", + "disable": "Onemogući", + "saving": "Spremanje…", + "cancel": "Odustani", + "close": "Zatvori", + "copy": "Kopiraj", + "back": "Nazad", + "history": "Povijest", + "fullscreen": "Cijeli zaslon", + "exitFullscreen": "Izađi iz cijelog zaslona", + "pictureInPicture": "Slika u Slici", + "twoWayTalk": "Dvosmjerni razgovor", + "cameraAudio": "Kamera Zvuk", + "on": "UKLJUČENO", + "off": "ISKLJUČENO", + "edit": "Uredi", + "copyCoordinates": "Kopiraj koordinate", + "delete": "Izbriši", + "yes": "Da", + "no": "Ne", + "download": "Preuzmi", + "info": "Informacije", + "suspended": "Obustavljeno", + "unsuspended": "Ponovno aktiviraj", + "play": "Reproduciraj", + "unselect": "Odznači", + "export": "Izvezi", + "deleteNow": "Izbriši Sada", + "next": "Sljedeće", + "continue": "Nastavi" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "stopa", + "meters": "metri" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/sat", + "mbph": "MB/sat", + "gbph": "GB/sat" + } + }, + "label": { + "back": "Idi nazad", + "hide": "Sakrij {{item}}", + "show": "Prikaži {{item}}", + "ID": "ID", + "none": "Nema", + "all": "Sve", + "other": "Druge" + }, + "list": { + "two": "{{0}} i {{1}}", + "many": "{{items}} i {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opcionalno", + "internalID": "Interni ID koji Frigate koristi u konfiguraciji i bazi podataka" + }, + "toast": { + "copyUrlToClipboard": "Kopiran URL u međuspremnik.", + "save": { + "title": "Spremi", + "error": { + "title": "Neuspješno spremanje promjena konfiguracije: {{errorMessage}}", + "noMessage": "Neuspješno spremanje promjena konfiguracije" + } + } + }, + "role": { + "title": "Uloge", + "admin": "Administrator", + "viewer": "Gledatelj", + "desc": "Administratori imaju potpuni pristup svim značajkama u Frigate korisnickom sučelju. Gledatelji su ograničeni na pregled kamera, pregled stavki i povijesnog snimka u korisničkom sučelju." + }, + "pagination": { + "label": "paginacija", + "previous": { + "title": "Prethodno", + "label": "Idi na prethodnu stranicu" + }, + "next": { + "title": "Sljedeće", + "label": "Idi na sljedeću stranicu" + }, + "more": "Više stranica" + }, + "accessDenied": { + "documentTitle": "Pristup Odbijen - Frigate", + "title": "Pristup Odbijen", + "desc": "Nemaš dopuštenje za pregled ove stranice." + }, + "notFound": { + "documentTitle": "Nije Nađeno - Frigate", + "title": "404", + "desc": "Stranica nije pronađena" + }, + "selectItem": "Odaberi {{item}}", + "readTheDocumentation": "Čitaj dokumentaciju", + "information": { + "pixels": "{{area}}px" + } +} diff --git a/web/public/locales/hr/components/auth.json b/web/public/locales/hr/components/auth.json new file mode 100644 index 0000000..74a83d2 --- /dev/null +++ b/web/public/locales/hr/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Korisničko ime", + "password": "Lozinka", + "login": "Prijava", + "errors": { + "usernameRequired": "Korisničko ime je obavezno", + "passwordRequired": "Lozinka je obavezna", + "loginFailed": "Prijava nije uspjela", + "unknownError": "Nepoznata greška. Provjeri dnevnik.", + "webUnknownError": "Nepoznata greška. Provjerite logove u konzoli.", + "rateLimit": "Prekoračeno ograničenje. Pokušaj opet kasnije." + }, + "firstTimeLogin": "Prokušavaš se prijaviti prvi put? Vjerodajnice su ispisane u Frigate logovima." + } +} diff --git a/web/public/locales/hr/components/camera.json b/web/public/locales/hr/components/camera.json new file mode 100644 index 0000000..c00dc41 --- /dev/null +++ b/web/public/locales/hr/components/camera.json @@ -0,0 +1,86 @@ +{ + "group": { + "label": "Grupe kamera", + "add": "Dodaj grupu kamera", + "edit": "Uredi grupu kamera", + "delete": { + "label": "Izbriši grupu kamera", + "confirm": { + "title": "Potvrda brisanja", + "desc": "Da li ste sigurni da želite obrisati grupu kamera {{name}}?" + } + }, + "name": { + "label": "Ime", + "placeholder": "Unesite ime…", + "errorMessage": { + "mustLeastCharacters": "Ime grupe kamera mora sadržavati barem 2 karaktera.", + "exists": "Grupa kamera sa ovim imenom već postoji.", + "nameMustNotPeriod": "Naziv grupe kamera ne smije sadržavati točku.", + "invalid": "Nevažeći naziv grupe kamera." + } + }, + "cameras": { + "label": "Kamere", + "desc": "Izaberite kamere za ovu grupu." + }, + "icon": "Ikona", + "success": "Grupa kamera ({{name}}) je pohranjena.", + "camera": { + "birdseye": "Ptičja perspektiva", + "setting": { + "label": "Postavke emitiranja kamere", + "title": "{{cameraName}} Postavke Emitiranja", + "desc": "Promijenite opcije emitiranja uživo za nadzornu ploču ove grupe kamera. Ove postavke su specifične za uređaj/preglednik.", + "audioIsAvailable": "Za ovaj prijenos dostupan je zvuk", + "audioIsUnavailable": "Za ovaj prijenos zvuk nije dostupan", + "audio": { + "tips": { + "title": "Audio mora dolaziti s vaše kamere i biti konfiguriran u go2rtc za ovaj prijenos." + } + }, + "stream": "Emitiranje", + "placeholder": "Izaberi emitiranje", + "streamMethod": { + "label": "Metoda emitiranja", + "placeholder": "Odaberi metodu emitiranja", + "method": { + "noStreaming": { + "label": "Nema emitiranja", + "desc": "Slike s kamere bit će ažurirane samo jednom u minuti, a emitiranje uživo neće biti dostupno." + }, + "smartStreaming": { + "desc": "Pametno emitiranje ažurirat će sliku vaše kamere jednom u minuti kada nema prepoznatljive aktivnosti kako bi uštedjelo propusnost i resurse. Kada se detektira aktivnost, slika će se besprijekorno prebaciti na prijenos uživo.", + "label": "Pametno Emitiranje (preporučeno)" + }, + "continuousStreaming": { + "label": "Kontinuirano Emitiranje", + "desc": { + "title": "Slika kamere uvijek će biti prijenos uživo kada je vidljiva na nadzornoj ploči, čak i ako nije detektirana nikakva aktivnost.", + "warning": "Neprekidno emitiranje može uzrokovati visok unos propusnosti i probleme s izvedbom. Koristite s oprezom." + } + } + } + }, + "compatibilityMode": { + "label": "Način kompatibilnosti", + "desc": "Omogućite ovu opciju samo ako vaš prijenos uživo s kamere prikazuje artefakte boje i ima dijagonalnu liniju na desnoj strani slike." + } + } + } + }, + "debug": { + "options": { + "label": "Postavke", + "title": "Opcije", + "showOptions": "Pokaži Opcije", + "hideOptions": "Sakrij Opcije" + }, + "boundingBox": "Granični okvir", + "timestamp": "Vremenska oznaka", + "zones": "Zone", + "mask": "Maska", + "motion": "Kretnja", + "regions": "Regije" + } +} diff --git a/web/public/locales/hr/components/dialog.json b/web/public/locales/hr/components/dialog.json new file mode 100644 index 0000000..4203051 --- /dev/null +++ b/web/public/locales/hr/components/dialog.json @@ -0,0 +1,123 @@ +{ + "restart": { + "title": "Jeste li sigurni da želite ponovno pokrenuti Frigate?", + "button": "Ponovno pokreni", + "restarting": { + "title": "Frigate se ponovno pokreće", + "content": "Ova stranica će se osvježiti za {{countdown}} sekundi.", + "button": "Forsiraj ponovno pokretanje odmah" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Pošalji u Frigate+", + "desc": "Objekti u lokacijama koje želiš izbjeći nisu lažno pozitivni. Slanjem njih kao lažno pozitivnih će zbuniti model." + }, + "review": { + "question": { + "label": "Potvrdi oznaku za Frigate Plus", + "ask_a": "Da li je ovaj objekt {{label}}?", + "ask_an": "Da li je ovaj objekt {{label}}?", + "ask_full": "Da li je ovaj objekt {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Poslano" + } + } + }, + "video": { + "viewInHistory": "Pogledaj u povijesti" + } + }, + "export": { + "time": { + "lastHour_one": "Zadnji sat", + "lastHour_few": "Zadnja {{count}} sata", + "lastHour_other": "Zadnjih {{count}} sati", + "start": { + "title": "Vrijeme početka", + "label": "Odaberi vrijeme početka" + }, + "end": { + "title": "Vrijeme kraja", + "label": "Odaberi vrijeme kraja" + }, + "fromTimeline": "Izaberi sa vremenske crte", + "custom": "Prilagođeno" + }, + "name": { + "placeholder": "Imenuj Izvoz" + }, + "select": "Odaberi", + "export": "Izvoz", + "selectOrExport": "Odaberi ili Izvezi", + "toast": { + "success": "Izvoz je uspješno pokrenut. Datoteku možete pregledati na stranici za izvoz.", + "view": "Prikaz", + "error": { + "failed": "Nije uspjelo pokretanje izvoza: {{error}}", + "endTimeMustAfterStartTime": "Vrijeme završetka mora biti nakon vremena početka", + "noVaildTimeSelected": "Nema odabranog valjanog vremenskog raspona" + } + }, + "fromTimeline": { + "saveExport": "Spremi Izvoz", + "previewExport": "Pregledaj Izvoz" + } + }, + "streaming": { + "label": "Emitiraj", + "restreaming": { + "disabled": "Ponovno emitiranje nije omogućeno za ovu kameru.", + "desc": { + "title": "Postavi go2rtc za opcije dodatnog prikaza uživo i zvuk za ovu kameru." + } + }, + "showStats": { + "label": "Pokaži statistike emitiranja", + "desc": "Omogući ovu opciju za prikaz statistike emitiranja kao proziran prozor na slici kamere." + }, + "debugView": "Debug Prikaz" + }, + "search": { + "saveSearch": { + "label": "Spremi Pretragu", + "desc": "Dodaj ime za ovu spremljenu pretragu.", + "placeholder": "Unesi ime za svoju pretragu", + "overwrite": "{{searchName}} već postoji. Spremanje će prepisati postojeću vrijednost.", + "success": "Pretraga ({{searchName}}) je spremljena.", + "button": { + "save": { + "label": "Spremi ovu pretragu" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "Potvrdi Brisanje", + "desc": { + "selected": "Jeste li sigurni da želite izbrisati sav snimljen video povezan s ovom preglednom stavkom?

    DržiShift tipku za zaobilaženje ove poruke u budućnosti." + }, + "toast": { + "success": "Video snimke povezane s odabranim preglednim stavkama su uspješno izbrisane.", + "error": "Neuspješno brisanje: {{error}}" + } + }, + "button": { + "export": "Izvezi", + "markAsReviewed": "Označi kao pregledano", + "markAsUnreviewed": "Označi kao nepregledano", + "deleteNow": "Izbriši sada" + } + }, + "imagePicker": { + "selectImage": "Odaberi sličicu praćenog objekta", + "unknownLabel": "Spremljena Slika Okinuća", + "search": { + "placeholder": "Traži prema oznaci ili podoznaci..." + }, + "noImages": "Sličica nisu nađene za ovu kameru" + } +} diff --git a/web/public/locales/hr/components/filter.json b/web/public/locales/hr/components/filter.json new file mode 100644 index 0000000..deac5b1 --- /dev/null +++ b/web/public/locales/hr/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filter", + "classes": { + "label": "Klase", + "all": { + "title": "Sve klase" + }, + "count_one": "{{count}} Klasa", + "count_other": "{{count}} Klase" + }, + "labels": { + "label": "Oznake", + "all": { + "title": "Sve oznake", + "short": "Oznake" + }, + "count_one": "{{count}} oznake", + "count_other": "{{count}} oznake" + }, + "zones": { + "label": "Zone", + "all": { + "title": "Sve zone", + "short": "Zone" + } + }, + "dates": { + "selectPreset": "Odaberi predložak…", + "all": { + "title": "Svi datumi", + "short": "Datumi" + } + }, + "more": "Više filtera", + "reset": { + "label": "Ponovno postavi filtere na zadane vrijednosti" + }, + "timeRange": "Vremenski Raspon", + "subLabels": { + "label": "Podoznake", + "all": "Sve Podoznake" + }, + "attributes": { + "label": "Klasifikacijski Atributi", + "all": "Svi Atributi" + }, + "score": "Rezultat", + "estimatedSpeed": "Procijenjena Brzina ({{unit}})", + "features": { + "label": "Značajke", + "hasSnapshot": "Ima snimku", + "hasVideoClip": "Ima video isječak", + "submittedToFrigatePlus": { + "label": "Poslano na Frigate+", + "tips": "Prvo moraš filtrirati praćene objekte koji imaju snimku stanja.

    Praćeni objekti bez snimke stanja ne mogu se poslati Frigate+." + } + }, + "sort": { + "label": "Poredaj", + "dateAsc": "Datum (Uzlazno)", + "dateDesc": "Datum (Silazno)", + "scoreAsc": "Ocjena Objekta (Uzlazno)", + "scoreDesc": "Ocjena objekta (uzlazno)", + "speedAsc": "Procijenjena Brzina (Uzlazno)", + "speedDesc": "Procijenjena Brzina (Silazno)", + "relevance": "Značajnost" + }, + "cameras": { + "label": "Filter Kamera", + "all": { + "title": "Sve Kamere", + "short": "Kamere" + } + }, + "review": { + "showReviewed": "Prikaži Pregledano" + }, + "motion": { + "showMotionOnly": "Prikaži Jedino Pokrete" + }, + "explore": { + "settings": { + "title": "Postavke", + "defaultView": { + "title": "Zadani Prikaz", + "summary": "Sažetak", + "unfilteredGrid": "Nefiltrirana mreža", + "desc": "Kada filteri nisu odabrani, prikazan je sažetak najnovijih objekata po oznaci, ili je prikazana nefiltrirana mreža." + }, + "gridColumns": { + "title": "Stupci Mreže", + "desc": "Odaberi broj stupaca u mrežnom prikazu." + }, + "searchSource": { + "label": "Traži Izvor", + "desc": "Odaberi želiš li tražiti sličice ili opise tvojih praćenih objekata.", + "options": { + "thumbnailImage": "Sličica", + "description": "Opis" + } + } + }, + "date": { + "selectDateBy": { + "label": "Odaberi datum za filtriranje" + } + } + }, + "logSettings": { + "label": "Filtriraj stupanj zapisnika", + "filterBySeverity": "Filtriraj zapisnike po ozbiljnosti", + "loading": { + "title": "Učitavanje", + "desc": "Kada je prozor zapisnika listan do dna, novi zapisi se prikazuju automatski nakon stvaranja." + }, + "disableLogStreaming": "Onemogući prijenos zapisa uživo", + "allLogs": "Svi zapisi" + }, + "trackedObjectDelete": { + "title": "Potvrdi Brisanje", + "desc": "Brisanjem ovih praćenih objekata ({{objectLength}}) uklanja se snimak, svi spremljeni ugradbeni elementi i svi povezani unosi životnog ciklusa objekta. Snimljeni materijali ovih praćenih objekata u prikazu povijesti NEĆE biti izbrisani.

    Jeste li sigurni da želite nastaviti?

    Držite tipku Shift da biste u budućnosti zaobišli ovaj dijalog.", + "toast": { + "success": "Praćeni objekti su uspješno izbrisani.", + "error": "Neuspješno brisanje praćenih objekata: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtritaj prema maski zone" + }, + "recognizedLicensePlates": { + "title": "Prepoznate Registracijske Oznake", + "loadFailed": "Neuspješno učitavanje prepoznatih registracijskih oznaka.", + "loading": "Učitavanje prepoznatih registracijskih oznaka…", + "placeholder": "Upiši za traženje registracijskih oznaka…", + "noLicensePlatesFound": "Registracijske oznake nisu nađene.", + "selectPlatesFromList": "Odaberi jednu ili više registracijskih oznaka iz liste.", + "selectAll": "Odaberi sve", + "clearAll": "Očisti sve" + } +} diff --git a/web/public/locales/hr/components/icons.json b/web/public/locales/hr/components/icons.json new file mode 100644 index 0000000..f6c0fa5 --- /dev/null +++ b/web/public/locales/hr/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Odaberite ikonu", + "search": { + "placeholder": "Traži ikonu…" + } + } +} diff --git a/web/public/locales/hr/components/input.json b/web/public/locales/hr/components/input.json new file mode 100644 index 0000000..0df384b --- /dev/null +++ b/web/public/locales/hr/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Preuzmi video", + "toast": { + "success": "Preuzimanje vašeg videa za recenziju je počelo." + } + } + } +} diff --git a/web/public/locales/hr/components/player.json b/web/public/locales/hr/components/player.json new file mode 100644 index 0000000..409af51 --- /dev/null +++ b/web/public/locales/hr/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Nisu pronađene snimke za ovo vrijeme", + "submitFrigatePlus": { + "title": "Pošalji ovaj kadar u Frigate+?", + "submit": "Pošalji" + }, + "cameraDisabled": "Kamera je onemogućena", + "stats": { + "streamType": { + "short": "Vrsta", + "title": "Tip Streama:" + }, + "latency": { + "value": "{{seconds}} sekundi", + "short": { + "value": "{{seconds}} sekundi", + "title": "Kašnjenje" + }, + "title": "Kašnjenje:" + }, + "bandwidth": { + "title": "Mrežna propusnost:", + "short": "Mrežna propusnost" + }, + "totalFrames": "Ukupni broj kadrova (slika):", + "droppedFrames": { + "title": "Izgubljeni kadrovi:", + "short": { + "title": "Izgubljeno", + "value": "{{droppedFrames}} kadrova" + } + }, + "decodedFrames": "Dekodirani kadrovi:", + "droppedFrameRate": "Stopa izgubljenih kadrova:" + }, + "noPreviewFound": "Nije nađen pretpregled", + "noPreviewFoundFor": "Pretpregled nije nađen za {{cameraName}}", + "livePlayerRequiredIOSVersion": "iOS 17.1 ili noviji je potreban za ovu vrstu uživog prijenosa.", + "streamOffline": { + "title": "Emitiranje nije dostupno", + "desc": "Slike nisu primljene sa {{cameraName}} detect stream-a, provjeri logove" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Kadar je uspješno poslan u Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Neuspješno slanje kadra u Frigate+" + } + } +} diff --git a/web/public/locales/hr/config/cameras.json b/web/public/locales/hr/config/cameras.json new file mode 100644 index 0000000..1ffab63 --- /dev/null +++ b/web/public/locales/hr/config/cameras.json @@ -0,0 +1,3 @@ +{ + "label": "Konfiguracijakamere" +} diff --git a/web/public/locales/hr/config/global.json b/web/public/locales/hr/config/global.json new file mode 100644 index 0000000..9a39298 --- /dev/null +++ b/web/public/locales/hr/config/global.json @@ -0,0 +1,5 @@ +{ + "version": { + "label": "Trenutna verzija konfiguracije" + } +} diff --git a/web/public/locales/hr/config/groups.json b/web/public/locales/hr/config/groups.json new file mode 100644 index 0000000..fd99a88 --- /dev/null +++ b/web/public/locales/hr/config/groups.json @@ -0,0 +1,7 @@ +{ + "audio": { + "global": { + "detection": "Globalna detekcija" + } + } +} diff --git a/web/public/locales/hr/config/validation.json b/web/public/locales/hr/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hr/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hr/objects.json b/web/public/locales/hr/objects.json new file mode 100644 index 0000000..b27b693 --- /dev/null +++ b/web/public/locales/hr/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Osoba", + "bicycle": "Bicikl", + "car": "Automobil", + "motorcycle": "Motocikl", + "airplane": "Zrakoplov", + "bus": "Autobus", + "train": "Vlak", + "boat": "Brod", + "traffic_light": "Semafor", + "fire_hydrant": "Hidrant", + "street_sign": "Prometni znak", + "stop_sign": "Znak stop", + "bench": "Klupa", + "bird": "Ptica", + "cat": "Mačka", + "dog": "Pas", + "horse": "Konj", + "sheep": "Ovca", + "cow": "Krava", + "parking_meter": "Parkirni Automat", + "elephant": "Slon", + "bear": "Medvjed", + "zebra": "Zebra", + "giraffe": "Žirafa", + "hat": "Kapa", + "backpack": "Ruksak", + "umbrella": "Kišobran", + "shoe": "Cipela", + "eye_glasses": "Naočale", + "handbag": "Ručna torba", + "tie": "Kravata", + "suitcase": "Kovčeg", + "frisbee": "Frizbi", + "skis": "Skije", + "snowboard": "Snowboard", + "sports_ball": "Sportska Lopta", + "kite": "Zmaj", + "baseball_bat": "Baseball Palica", + "baseball_glove": "Baseball Rukavica", + "skateboard": "Skejtboard", + "surfboard": "Daska za surfanje", + "tennis_racket": "Teniski reket", + "bottle": "Boca", + "plate": "Tanjur", + "wine_glass": "Vinska Čaša", + "cup": "Šalica", + "fork": "Vilica", + "knife": "Nož", + "spoon": "Žlica", + "bowl": "Zdjela", + "banana": "Banana", + "apple": "Jabuka", + "sandwich": "Sendvič", + "orange": "Naranča", + "broccoli": "Brokula", + "carrot": "Mrkva", + "hot_dog": "Hot Dog", + "pizza": "Pizza", + "donut": "Krafna", + "cake": "Torta", + "chair": "Stolica", + "couch": "Kauč", + "potted_plant": "Biljka u Loncu", + "bed": "Krevet", + "mirror": "Ogledalo", + "dining_table": "Blagovaonski Stol", + "window": "Prozor", + "desk": "Radni Stol", + "toilet": "WC", + "door": "Vrata", + "tv": "TV", + "laptop": "Laptop", + "mouse": "Miš", + "remote": "Daljinski", + "keyboard": "Tipkovnica", + "cell_phone": "Mobilni Telefon", + "microwave": "Mikrovalna", + "oven": "Pećnica", + "toaster": "Toster", + "sink": "Sudoper", + "refrigerator": "Frižider", + "blender": "Blender", + "book": "Knjiga", + "clock": "Sat", + "vase": "Vaza", + "scissors": "Škare", + "teddy_bear": "Plišani Medo", + "hair_dryer": "Fen", + "toothbrush": "Četkica za zube", + "hair_brush": "Četka za kosu", + "vehicle": "Vozilo", + "squirrel": "Vjeverica", + "deer": "Jelen", + "animal": "Životinja", + "bark": "Kora", + "fox": "Lisica", + "goat": "Koza", + "rabbit": "Zec", + "raccoon": "Rakun", + "robot_lawnmower": "Robotska Kosilica", + "waste_bin": "Kanta za smeće", + "on_demand": "Na Zahtjev", + "face": "Lice", + "license_plate": "Registracijska oznaka", + "package": "Paket", + "bbq_grill": "Roštilj", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/web/public/locales/hr/views/chat.json b/web/public/locales/hr/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hr/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hr/views/classificationModel.json b/web/public/locales/hr/views/classificationModel.json new file mode 100644 index 0000000..b61defa --- /dev/null +++ b/web/public/locales/hr/views/classificationModel.json @@ -0,0 +1,196 @@ +{ + "documentTitle": "Klasifikacijski modeli - Frigate", + "button": { + "deleteImages": "Obriši slike", + "trainModel": "Treniraj model", + "addClassification": "Dodaj klasifikaciju", + "deleteModels": "Obriši modele", + "editModel": "Uredi model", + "deleteClassificationAttempts": "Izbriši Klasifikacijske Slike", + "renameCategory": "Preimenuj klasu", + "deleteCategory": "Izbriši Klasu" + }, + "tooltip": { + "trainingInProgress": "Model se trenutno trenira", + "modelNotReady": "Model nije spreman za treniranje", + "noNewImages": "Nema novih slika za treniranje. Prvo klasificirajte više slika u skupu podataka.", + "noChanges": "Nema promjena u skupu podataka od posljednjeg treniranja." + }, + "details": { + "unknown": "Nepoznato", + "none": "Nema", + "scoreInfo": "Rezultat predstavlja prosječnu klasifikacijsku pouzdanost kroz sve detekcije ovog objekta." + }, + "toast": { + "success": { + "deletedImage_one": "Obrisane slike", + "deletedImage_few": "", + "deletedImage_other": "", + "deletedCategory_one": "Izbrisana Klasa", + "deletedCategory_few": "", + "deletedCategory_other": "", + "deletedModel_one": "Uspješno izbrisan {{count}} model", + "deletedModel_few": "Uspješno izbrisana {{count}} modela", + "deletedModel_other": "Uspješno izbrisano {{count}} modela", + "categorizedImage": "Uspješno klasificirana slika", + "trainedModel": "Uspješno treniran model.", + "trainingModel": "Uspješno započeto treniranje modela.", + "updatedModel": "Uspješno ažurirana konfiguracija modela", + "renamedCategory": "Uspješno preimenovana klasa na {{name}}" + }, + "error": { + "deleteImageFailed": "Neuspješno brisanje: {{errorMessage}}", + "deleteCategoryFailed": "Neuspješno brisanje klase: {{errorMessage}}", + "deleteModelFailed": "Nije uspjelo brisanje modela: {{errorMessage}}", + "categorizeFailed": "Nije uspjelo kategoriziranje slike: {{errorMessage}}", + "trainingFailed": "Neuspješno treniranje modela. Provjerite Frigate zapisnike za detalje.", + "trainingFailedToStart": "Neuspješno pokretanje treniranja modela: {{errorMessage}}", + "updateModelFailed": "Neuspješno ažuriranje modela: {{errorMessage}}", + "renameCategoryFailed": "Neuspješno preimenovanje klase: {{errorMessage}}" + } + }, + "description": { + "invalidName": "Nevaljano ime. Ime može samo uključivati slova, brojeve, razmake, navodnike, podcrte i crtice." + }, + "train": { + "titleShort": "Nedavno", + "aria": "Odaberi Nedavne Klasifikacije", + "title": "Nedavne Klasifikacije" + }, + "deleteModel": { + "desc_one": "Jeste li sigurni da želite izbrisati {{count}} model? Ovo će trajno izbrisati sve povezane podatke, uključujući slike i podatke za treniranje. Ova radnja se ne može poništiti.", + "desc_few": "Jeste li sigurni da želite izbrisati {{count}} modela? Ovo će trajno izbrisati sve povezane podatke, uključujući slike i podatke za treniranje. Ova radnja se ne može poništiti.", + "desc_other": "Jeste li sigurni da želite izbrisati {{count}} modela? Ovo će trajno izbrisati sve povezane podatke, uključujući slike i podatke za treniranje. Ova radnja se ne može poništiti.", + "title": "Izbriši klasifikacijski model", + "single": "Jesi li siguran da želiš izbrisati {{name}}? To će trajno izbrisati sve povezane podatke, uključujući slike i podatke za treniranje. Ova radnja se ne može poništiti." + }, + "deleteDatasetImages": { + "desc_one": "Jeste li sigurni da želite izbrisati {{count}} sliku iz {{dataset}}? Ova radnja se ne može poništiti i zahtijevat će ponovno treniranje modela.", + "desc_few": "Jeste li sigurni da želite izbrisati {{count}} slike iz {{dataset}}? Ova radnja se ne može poništiti i zahtijevat će ponovno treniranje modela.", + "desc_other": "Jeste li sigurni da želite izbrisati {{count}} slika iz {{dataset}}? Ova radnja se ne može poništiti i zahtijevat će ponovno treniranje modela.", + "title": "Izbriši slike iz skupa podataka" + }, + "deleteTrainImages": { + "desc_one": "Jeste li sigurni da želite izbrisati {{count}} sliku? Ova radnja se ne može poništiti.", + "desc_few": "Jeste li sigurni da želite izbrisati {{count}} slike? Ova radnja se ne može poništiti.", + "desc_other": "Jeste li sigurni da želite izbrisati {{count}} slika? Ova radnja se ne može poništiti.", + "title": "Izbriši slike iz skupa za treniranje" + }, + "wizard": { + "step3": { + "allImagesRequired_one": "Molimo klasificirajte sve slike. Preostala je {{count}} slika.", + "allImagesRequired_few": "Molimo klasificirajte sve slike. Preostale su {{count}} slike.", + "allImagesRequired_other": "Molimo klasificirajte sve slike. Preostalo je {{count}} slika.", + "selectImagesPrompt": "Odaberite sve slike s: {{className}}", + "selectImagesDescription": "Kliknite na slike za odabir. Kliknite Nastavi kada završite s ovom klasom.", + "generating": { + "title": "Generiranje Primjeraka Slika", + "description": "Frigate povlači reprezentativne slike iz vaših snimaka. Ovo može potrajati..." + }, + "training": { + "title": "Treniranje Modela", + "description": "Vaš model se trenira u pozadini. Zatvorite ovaj dijalog, a model će početi raditi čim treniranje završi." + }, + "retryGenerate": "Ponovi Generiranje", + "noImages": "Nema generiranih primjeraka slika", + "classifying": "Klasificiranje & Treniranje...", + "trainingStarted": "Treniranje je uspješno pokrenuto", + "modelCreated": "Model je uspješno kreiran. Koristite prikaz Nedavnih Klasifikacija za dodavanje slika za nedostajuća stanja, a zatim trenirajte model.", + "errors": { + "noCameras": "Nema konfiguriranih kamera", + "noObjectLabel": "Nije odabrana oznaka objekta", + "generateFailed": "Neuspjelo generiranje primjera: {{error}}", + "generationFailed": "Generiranje nije uspjelo. Pokušajte ponovo.", + "classifyFailed": "Neuspjela klasifikacija slika: {{error}}" + }, + "generateSuccess": "Primjerci slika su uspješno generirani", + "missingStatesWarning": { + "title": "Nedostaju Primjeri Stanja", + "description": "Preporučuje se odabrati primjere za sva stanja radi najboljih rezultata. Možete nastaviti bez odabira svih stanja, ali model neće biti treniran dok svi statusi nemaju slike. Nakon nastavka, koristite prikaz Nedavnih Klasifikacija za klasifikaciju slika za nedostajuća stanja, a zatim trenirajte model." + } + }, + "title": "Kreiraj Novu Klasifikaciju", + "steps": { + "nameAndDefine": "Naziv & Definicija", + "stateArea": "Područje Stanja", + "chooseExamples": "Odaberi Primjere" + }, + "step1": { + "description": "Modeli stanja prate fiksna područja kamere za promjene (npr. vrata otvorena/zatvorena). Modeli objekata dodaju klasifikacije detektiranim objektima (npr. poznate životinje, dostavljači, itd.).", + "name": "Naziv", + "namePlaceholder": "Unesite naziv modela...", + "type": "Tip", + "typeState": "Stanje", + "typeObject": "Objekt", + "objectLabel": "Oznaka Objekta", + "objectLabelPlaceholder": "Odaberi tip objekta...", + "classificationType": "Tip Klasifikacije", + "classificationTypeTip": "Saznaj više o tipovima klasifikacije", + "classificationTypeDesc": "Podoznake dodaju dodatni tekst na oznaku objekta (npr. 'Osoba: UPS'). Atributi su pretraživi metapodaci pohranjeni zasebno u metapodacima objekta.", + "classificationSubLabel": "Podoznaka", + "classificationAttribute": "Atribut", + "classes": "Klase", + "states": "Stanja", + "classesTip": "Saznaj više o klasama", + "classesStateDesc": "Definiraj različita stanja u kojima područje kamere može biti. Na primjer: 'otvoreno' i 'zatvoreno' za garažna vrata.", + "classesObjectDesc": "Definiraj različite kategorije za klasifikaciju detektiranih objekata. Na primjer: 'dostavljač', 'stanar', 'nepoznata osoba' za klasifikaciju ljudi.", + "classPlaceholder": "Unesite naziv klase...", + "errors": { + "nameRequired": "Naziv modela je obavezan", + "nameLength": "Naziv modela mora imati najviše 64 znaka", + "nameOnlyNumbers": "Naziv modela ne smije sadržavati samo brojeve", + "classRequired": "Potrebna je barem 1 klasa", + "classesUnique": "Nazivi klasa moraju biti jedinstveni", + "noneNotAllowed": "Klasa 'none' nije dopuštena", + "stateRequiresTwoClasses": "Modeli stanja zahtijevaju najmanje 2 klase", + "objectLabelRequired": "Molimo odaberite oznaku objekta", + "objectTypeRequired": "Molimo odaberite tip klasifikacije" + } + }, + "step2": { + "description": "Odaberite kamere i definirajte područje praćenja za svaku kameru. Model će klasificirati stanje tih područja.", + "cameras": "Kamere", + "selectCamera": "Odaberi Kameru", + "noCameras": "Kliknite + za dodavanje kamera", + "selectCameraPrompt": "Odaberite kameru s popisa kako biste definirali područje praćenja" + } + }, + "deleteCategory": { + "title": "Izbriši klasu", + "desc": "Jesi li siguran da želiš izbrisati klasu {{name}}? To će trajno izbrisati sve povezane slike i zahtijevati ponovno treniranje modela.", + "minClassesTitle": "Nije moguće izbrisati klasu", + "minClassesDesc": "Model klasifikacije mora imati barem 2 klase. Dodaj još jednu klasu prije brisanja ove." + }, + "edit": { + "title": "Uredi model klasifikacije", + "descriptionState": "Uredi klase za ovaj model klasifikacije stanja. Promjene zahtijevaju ponovno treniranje modela.", + "descriptionObject": "Uredi tip objekta i tip klasifikacije za ovaj model klasifikacije objekata.", + "stateClassesInfo": "Napomena: Promjena klasa stanja zahtijeva ponovno treniranje modela s ažuriranim klasama." + }, + "renameCategory": { + "title": "Preimenuj klasu", + "desc": "Unesite novi naziv za {{name}}. Bit će potrebno ponovno trenirati model da promjena naziva stupi na snagu." + }, + "categories": "Klase", + "createCategory": { + "new": "Kreiraj Novu Klasu" + }, + "categorizeImageAs": "Klasificiraj Sliku Kao:", + "categorizeImage": "Klasificiraj Sliku", + "menu": { + "objects": "Objekti", + "states": "Stanja" + }, + "noModels": { + "object": { + "title": "Nema Modela Klasifikacije Objekata", + "description": "Kreiraj prilagođeni model za klasifikaciju detektiranih objekata.", + "buttonText": "Kreiraj Model Objekta" + }, + "state": { + "title": "Nema Modela Klasifikacije Stanja", + "description": "Kreiraj prilagođeni model za praćenje i klasifikaciju promjena stanja u određenim područjima kamere.", + "buttonText": "Kreiraj Model Stanja" + } + } +} diff --git a/web/public/locales/hr/views/configEditor.json b/web/public/locales/hr/views/configEditor.json new file mode 100644 index 0000000..1a5f2d2 --- /dev/null +++ b/web/public/locales/hr/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Uređivač konfiguracije - Frigate", + "copyConfig": "Kopiraj konfiguraciju", + "saveAndRestart": "Spremi i pokreni ponovno", + "saveOnly": "Samo spremi", + "confirm": "Izađi bez spremanja?", + "toast": { + "error": { + "savingError": "Greška pri spremanju konfiguracije" + }, + "success": { + "copyToClipboard": "Konfiguracija je kopirana u međuspremnik." + } + }, + "configEditor": "Uređivač konfiguracije", + "safeModeDescription": "Frigate je u sigurnom načinu zbog greške u validaciji konfiguracije.", + "safeConfigEditor": "Uređivač konfiguracije (Siguran Način)" +} diff --git a/web/public/locales/hr/views/events.json b/web/public/locales/hr/views/events.json new file mode 100644 index 0000000..3bafeee --- /dev/null +++ b/web/public/locales/hr/views/events.json @@ -0,0 +1,65 @@ +{ + "alerts": "Upozorenja", + "detections": "Detekcije", + "motion": { + "label": "Kretnja", + "only": "Samo kretnje" + }, + "allCameras": "Sve kamere", + "empty": { + "alert": "Nema uzbuna za pregledati", + "detection": "Nema detekcija za pregled", + "motion": "Nema podataka o pokretu", + "recordingsDisabled": { + "title": "Snimanja moraju biti uključena", + "description": "Stavke za pregled mogu biti stvorene za kameru jedino kada su snimanja uključena za tu kameru." + } + }, + "timeline": "Vremenska linija", + "timeline.aria": "Odaberi vremensku liniju", + "zoomOut": "Udalji", + "events": { + "label": "Događaji", + "aria": "Odaberi događaje", + "noFoundForTimePeriod": "Nema pronađenih događaja za ovaj vremenski period." + }, + "zoomIn": "Približi", + "detail": { + "label": "Detalji", + "noDataFound": "Nema detaljnih podataka za pregled", + "aria": "Uključi/isključi prikaz detalja", + "trackedObject_one": "{{count}} objekt", + "trackedObject_other": "{{count}} objekta", + "noObjectDetailData": "Nema dostupnih detaljnih podataka o objektu.", + "settings": "Postavke detaljnog prikaza", + "alwaysExpandActive": { + "title": "Uvijek proširi aktivno", + "desc": "Uvijek proširite detalje objekta aktivnog pregledanog stavka kada su dostupni." + } + }, + "objectTrack": { + "trackedPoint": "Praćena točka", + "clickToSeek": "Kliknite za pomicanje na ovo vrijeme" + }, + "documentTitle": "Pregled - Frigate", + "recordings": { + "documentTitle": "Snimke - Frigate" + }, + "calendarFilter": { + "last24Hours": "Zadnja 24 sata" + }, + "markAsReviewed": "Označi kao Pregledano", + "markTheseItemsAsReviewed": "Označi ove stavke kao pregledane", + "newReviewItems": { + "label": "Pogledaj nove stavke za pregled", + "button": "Nove stavke za pregled" + }, + "selected_one": "{{count}} odabran", + "selected_other": "{{count}} odabrano", + "select_all": "Sve", + "camera": "Kamera", + "detected": "detektirano", + "normalActivity": "Normalno", + "needsReview": "Potreban pregled", + "securityConcern": "Sigurnosna zabrinutost" +} diff --git a/web/public/locales/hr/views/explore.json b/web/public/locales/hr/views/explore.json new file mode 100644 index 0000000..fce8024 --- /dev/null +++ b/web/public/locales/hr/views/explore.json @@ -0,0 +1,250 @@ +{ + "documentTitle": "Istražite - Frigate", + "generativeAI": "Generativni AI", + "exploreIsUnavailable": { + "title": "Istraživanje je nedostupno", + "embeddingsReindexing": { + "startingUp": "Pokretanje…", + "finishingShortly": "Završava uskoro", + "estimatedTime": "Procjenjeno preostalo vrijeme:", + "context": "Istraživanje se može koristiti nakon što je završeno ponovno indeksiranje ugrađivanja praćenih objekata.", + "step": { + "thumbnailsEmbedded": "Ugrađene sličice: ", + "descriptionsEmbedded": "Ugrađeni opisi: ", + "trackedObjectsProcessed": "Procesirani praćeni objekti: " + } + }, + "downloadingModels": { + "setup": { + "textModel": "Tekstualni model", + "visionModel": "Model za vid", + "visionModelFeatureExtractor": "Ekstraktor značajki modela vizije", + "textTokenizer": "Tokenizator teksta" + }, + "context": "Frigate preuzima potrebne modele ugrađivanja kako bi podržao značajku semantičkog pretraživanja. To može potrajati nekoliko minuta, ovisno o brzini vaše mrežne veze.", + "tips": { + "context": "Možda ćete htjeti ponovno indeksirati ugrađivanja (embeddings) svojih praćenih objekata kada se modeli preuzmu." + }, + "error": "Došlo je do pogreške. Provjerite Frigate logove." + } + }, + "details": { + "timestamp": "Vremenska oznaka", + "item": { + "tips": { + "mismatch_one": "{{count}} nedostupan objekt je otkriven i uključen u ovaj pregledni stavak. Ti objekti ili nisu kvalificirani kao upozorenje ili detekcija, ili su već uklonjeni/izbrisani.", + "mismatch_few": "{{count}} nedostupna objekta su otkrivena i uključena u ovaj pregledni stavak. Ti objekti ili nisu kvalificirani kao upozorenje ili detekcija, ili su već uklonjeni/izbrisani.", + "mismatch_other": "{{count}} nedostupnih objekata je otkriveno i uključeno u ovaj pregledni stavak. Ti objekti ili nisu kvalificirani kao upozorenje ili detekcija, ili su već uklonjeni/izbrisani.", + "hasMissingObjects": "Prilagodite svoju konfiguraciju ako želite da Frigate sprema praćene objekte za sljedeće oznake: {{objects}}" + }, + "title": "Detalji o pregledu stavke", + "desc": "Detalji o pregledu stavke", + "button": { + "share": "Podijelite ovaj pregled", + "viewInExplore": "Pogledaj u Istraži" + }, + "toast": { + "success": { + "regenerate": "Zatražen je novi opis od {{provider}}. Ovisno o brzini vašeg pružatelja usluga, novi opis može trebati neko vrijeme da se regenerira.", + "updatedSublabel": "Uspješno ažurirana podoznaka.", + "updatedLPR": "Uspješno ažurirana registarska pločica.", + "updatedAttributes": "Uspješno ažurirani atributi.", + "audioTranscription": "Uspješno zatražena audio transkripcija. Ovisno o brzini vašeg Frigate servera, transkripcija može potrajati neko vrijeme." + }, + "error": { + "regenerate": "Neuspješno pozivanje {{provider}} za novi opis: {{errorMessage}}", + "updatedSublabelFailed": "Nije uspjelo ažurirati podoznake: {{errorMessage}}", + "updatedLPRFailed": "Neuspješno ažuriranje registarske pločice: {{errorMessage}}", + "updatedAttributesFailed": "Neuspješno ažuriranje atributa: {{errorMessage}}", + "audioTranscription": "Neuspješno zatraživanje audio transkripcije: {{errorMessage}}" + } + } + }, + "label": "Oznaka", + "editSubLabel": { + "title": "Uredi podoznaku", + "desc": "Unesite novu podoznaku za ovaj {{label}}", + "descNoLabel": "Unesite novu oznaku podoznake za ovaj praćeni objekt" + }, + "editLPR": { + "title": "Uredi registarsku pločicu", + "desc": "Unesite novu vrijednost registarske pločice za ovaj {{label}}", + "descNoLabel": "Unesite novu vrijednost registarske pločice za ovaj praćeni objekt" + }, + "editAttributes": { + "title": "Atributi uređivanja", + "desc": "Odaberite klasifikacijske atribute za ovaj {{label}}" + }, + "snapshotScore": { + "label": "Ocjena snimke" + }, + "topScore": { + "label": "Najbolja ocjena", + "info": "Najviša ocjena je najviši medijan za praćeni objekt, pa se može razlikovati od rezultata prikazanog na sličici rezultata pretraživanja." + }, + "score": { + "label": "Ocjena" + }, + "recognizedLicensePlate": "Priznata registarska pločica", + "attributes": "Klasifikacijski atributi", + "estimatedSpeed": "Procijenjena brzina", + "objects": "Objekti", + "camera": "Kamera", + "zones": "Zone", + "button": { + "findSimilar": "Pronađite slične", + "regenerate": { + "title": "Regeneriraj", + "label": "Ponovno generiranje opisa praćenog objekta" + } + }, + "description": { + "label": "Opis", + "placeholder": "Opis praćenog objekta", + "aiTips": "Frigate neće tražiti opis od vašeg Generative AI pružatelja dok životni ciklus praćenog objekta ne završi." + }, + "expandRegenerationMenu": "Proširi izbornik regeneracije", + "regenerateFromSnapshot": "Regeneracija iz Snimki", + "regenerateFromThumbnails": "Regeneracija iz sličica", + "tips": { + "descriptionSaved": "Uspješno spremljen opis", + "saveDescriptionFailed": "Nije ažuriran opis: {{errorMessage}}" + }, + "title": { + "label": "Naslov" + } + }, + "trackedObjectDetails": "Detalji praćenog objekta", + "type": { + "details": "detalji", + "snapshot": "snimka", + "thumbnail": "Sličica", + "video": "video", + "tracking_details": "detalji praćenja" + }, + "exploreMore": "Istraži više {{label}} objekata", + "trackingDetails": { + "title": "Detalji Praćenja", + "noImageFound": "Slika nije nađena za ovaj vremenski zapis.", + "createObjectMask": "Napravi Masku Objekta", + "adjustAnnotationSettings": "Podesi postavke anotacije", + "scrollViewTips": "Kliknite za prikaz značajnih trenutaka životnog ciklusa ovog objekta.", + "autoTrackingTips": "Pozicije ograničavajućih okvira bit će netočne za kamere s automatskim praćenjem.", + "count": "{{first}} of {{second}}", + "trackedPoint": "Praćena točka", + "lifecycleItemDesc": { + "visible": "{{label}} detektiran", + "entered_zone": "{{label}} ušlo u {{zones}}", + "active": "{{label}} postao aktivan", + "stationary": "{{label}} je postao stacionaran", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detektiran za {{label}}", + "other": "{{label}} prepoznat kao {{attribute}}" + }, + "gone": "{{label}} lijevo", + "heard": "{{label}} zvuk detektiran", + "external": "{{label}} detektiran", + "header": { + "zones": "Zone", + "ratio": "Omjer", + "area": "Površina", + "score": "Ocjena" + } + }, + "annotationSettings": { + "title": "Postavke anotacija", + "showAllZones": { + "title": "Pokaži sve zone", + "desc": "Uvijek prikaži zone u okvirima gdje su objekti ušli u zonu." + }, + "offset": { + "label": "Pomak anotacija", + "desc": "Ovi podaci dolaze s detekcijskog emitiranja vaše kamere, ali se prikazuju preko slika iz snimajućeg emitiranja. Malo je vjerojatno da su oba emitiranja potpuno sinkronizirana. Kao rezultat toga, okvir (bounding box) i snimka možda neće savršeno odgovarati. Ovom postavkom možete pomaknuti oznake unaprijed ili unatrag u vremenu kako bi bolje odgovarale snimljenoj snimci.", + "millisecondsToOffset": "Milisekunde za pomicanje detekcije anotacija za. Zadano: 0", + "tips": "Smanjite vrijednost ako je reprodukcija videa ispred kutija i točaka puta, a povećajte vrijednost ako je reprodukcija videa iza njih. Ta vrijednost može biti negativna.", + "toast": { + "success": "Pomak anotacija za {{camera}} spremljen je u konfiguracijsku datoteku." + } + } + }, + "carousel": { + "previous": "Prethodni slajd", + "next": "Sljedeći slajd" + } + }, + "trackedObjectsCount_one": "{{count}} praćeni objekt ", + "trackedObjectsCount_few": "{{count}} praćena objekta ", + "trackedObjectsCount_other": "{{count}} praćenih objekata ", + "itemMenu": { + "downloadVideo": { + "label": "Preuzmi video", + "aria": "Preuzmi video" + }, + "downloadSnapshot": { + "label": "Preuzmite snimku", + "aria": "Preuzmite snimku" + }, + "downloadCleanSnapshot": { + "label": "Preuzmite čistu snimku", + "aria": "Preuzmite čistu snimku" + }, + "viewTrackingDetails": { + "label": "Pogledajte detalje praćenja", + "aria": "Prikaži detalje praćenja" + }, + "findSimilar": { + "label": "Pronađi slične", + "aria": "Pronađi slične praćene objekte" + }, + "addTrigger": { + "label": "Dodaj okidač", + "aria": "Dodajte okidač za ovaj praćeni objekt" + }, + "audioTranscription": { + "label": "Prepisivanje", + "aria": "Zatražite audio transkripciju" + }, + "submitToPlus": { + "label": "Pošalji na Frigate+", + "aria": "Pošalji na Frigate Plus" + }, + "viewInHistory": { + "label": "Pogled u povijest", + "aria": "Pogled u povijest" + }, + "deleteTrackedObject": { + "label": "Izbriši ovaj praćeni objekt" + }, + "showObjectDetails": { + "label": "Prikaži putanju objekta" + }, + "hideObjectDetails": { + "label": "Put skrivanja objekta" + } + }, + "dialog": { + "confirmDelete": { + "title": "Potvrdi brisanje", + "desc": "Brisanjem ovog praćenog objekta uklanja se snimka, sve spremljene ugradnje i svi povezani unosi o praćenju. Snimljeni materijal ovog praćenog objekta u prikazu Povijesti NEĆE biti izbrisan.

    Jeste li sigurni da želite nastaviti?" + } + }, + "noTrackedObjects": "Nema pronađenih praćenih objekata", + "fetchingTrackedObjectsFailed": "Neuspješno dohvaćanje praćenih objekata: {{errorMessage}}", + "searchResult": { + "tooltip": "Pronađen {{type}} s pouzdanošću od {{confidence}}%", + "previousTrackedObject": "Prethodni praćeni objekt", + "nextTrackedObject": "Sljedeći praćeni objekt", + "deleteTrackedObject": { + "toast": { + "success": "Praćeni objekt je uspješno izbrisan.", + "error": "Neuspješno brisanje praćenog objekta: {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "Analiza umjetne inteligencije" + }, + "concerns": { + "label": "Zabrinutosti" + } +} diff --git a/web/public/locales/hr/views/exports.json b/web/public/locales/hr/views/exports.json new file mode 100644 index 0000000..0762ed6 --- /dev/null +++ b/web/public/locales/hr/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Izvoz - Frigate", + "search": "Pretraga", + "deleteExport.desc": "Da li si siguran da želiš obrisati {{exportName}}?", + "editExport": { + "saveExport": "Spremi izvoz", + "title": "Preimenuj izvoz", + "desc": "Unesite novo ime ovog izvoza." + }, + "tooltip": { + "shareExport": "Podijeli izvoz", + "downloadVideo": "Preuzmi video", + "editName": "Uredi ime", + "deleteExport": "Obriši izvoz" + }, + "noExports": "Izvozi nisu pronađeni", + "deleteExport": "Obriši izvoz", + "toast": { + "error": { + "renameExportFailed": "Neuspjeh preimenovanja izvoza: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/hr/views/faceLibrary.json b/web/public/locales/hr/views/faceLibrary.json new file mode 100644 index 0000000..29883ae --- /dev/null +++ b/web/public/locales/hr/views/faceLibrary.json @@ -0,0 +1,93 @@ +{ + "description": { + "addFace": "Dodaj novu kolekcije u Biblioteku lica učitavanjem prve slike.", + "placeholder": "Unesi ime za ovu kolekciju", + "invalidName": "Nevaljano ime. Ime može samo uključivati slova, brojeve, razmake, navodnike, podcrte i crtice." + }, + "steps": { + "faceName": "Unesi Ime Lica", + "uploadFace": "Prenesi Sliku Lica", + "nextSteps": "Sljedeći Koraci", + "description": { + "uploadFace": "Prenesite sliku {{name}} koja prikazuje njezino lice iz prednjeg kuta. Slika ne mora biti obrezana samo na njezino lice." + } + }, + "train": { + "title": "Nedavna Prepoznavanja", + "aria": "Odaberite nedavna prepoznavanja", + "empty": "Nema nedavnih pokušaja prepoznavanja lica", + "titleShort": "Nedavno" + }, + "deleteFaceLibrary": { + "title": "Izbriši Ime", + "desc": "Jeste li sigurni da želite izbrisati kolekciju {{name}}? Ovim će se trajno izbrisati sva povezana lica." + }, + "deleteFaceAttempts": { + "title": "Izbriši Lica", + "desc_one": "Jeste li sigurni da želite izbrisati {{count}} lice? Ova se radnja ne može poništiti.", + "desc_few": "Jeste li sigurni da želite izbrisati {{count}} lica? Ova se radnja ne može poništiti.", + "desc_other": "Jeste li sigurni da želite izbrisati {{count}} lica? Ova se radnja ne može poništiti." + }, + "details": { + "timestamp": "Vremenska oznaka", + "unknown": "Nepoznato", + "scoreInfo": "Rezultat je ponderirani prosjek svih rezultata lica, pri čemu su ponderi određeni veličinom lica na svakoj slici." + }, + "documentTitle": "Biblioteka lica - Frigate", + "uploadFaceImage": { + "title": "Učitaj sliku lica", + "desc": "Učitaj sliku za skeniranje lica i uključi za {{pageToggle}}" + }, + "collections": "Kolekcije", + "createFaceLibrary": { + "new": "Stvori novo lice", + "nextSteps": "Kako biste izgradili čvrste temelje:
  • Koristite karticu Nedavna prepoznavanja za odabir i treniranje na slikama za svaku detektiranu osobu.
  • Usredotočite se na slike snimljene direktno ispred lica za najbolje rezultate; izbjegavajte slike za treniranje koje prikazuju lica pod kutom.
  • " + }, + "renameFace": { + "title": "Preimenuj Lice", + "desc": "Unesi novo ime za {{name}}" + }, + "toast": { + "success": { + "deletedFace_one": "Uspješno izbrisano {{count}} lice.", + "deletedFace_few": "Uspješno izbrisana {{count}} lica.", + "deletedFace_other": "Uspješno izbrisano {{count}} lica.", + "deletedName_one": "{{count}} lice je uspješno izbrisano.", + "deletedName_few": "{{count}} lica su uspješno izbrisana.", + "deletedName_other": "{{count}} lica je uspješno izbrisano.", + "uploadedImage": "Uspješno učitana slika.", + "addFaceLibrary": "{{name}} je uspješno dodano u Biblioteku Lica!", + "renamedFace": "Uspješno preimenovano lice na {{name}}", + "trainedFace": "Uspješno trenirano lice.", + "updatedFaceScore": "Uspješno ažurirana ocjena lica na {{name}} ({{score}})." + }, + "error": { + "uploadingImageFailed": "Neuspješno učitavanje slike: {{errorMessage}}", + "addFaceLibraryFailed": "Neuspješno postavljanje imena lica: {{errorMessage}}", + "deleteFaceFailed": "Neuspješno brisanje: {{errorMessage}}", + "deleteNameFailed": "Neuspješno brisanje imena: {{errorMessage}}", + "renameFaceFailed": "Neuspješno preimenovanje lica: {{errorMessage}}", + "trainFailed": "Neuspješno treniranje: {{errorMessage}}", + "updateFaceScoreFailed": "Neuspješno ažuriranje ocjene lica: {{errorMessage}}" + } + }, + "button": { + "deleteFaceAttempts": "Izbriši Lica", + "addFace": "Dodaj Lice", + "renameFace": "Preimenuj Lice", + "deleteFace": "Izbriši Lice", + "uploadImage": "Učitaj Sliku", + "reprocessFace": "Ponovno Procesiraj Lice" + }, + "imageEntry": { + "validation": { + "selectImage": "Molim izaberi datoteku slike." + }, + "dropActive": "Ispusti sliku ovdje…", + "dropInstructions": "Povuci i ispusti ili zalijepi sliku ovdje, ili odaberi klikom", + "maxSize": "Max veličina: {{size}}MB" + }, + "nofaces": "Nema dostupnih lica", + "trainFaceAs": "Treniraj lice kao:", + "trainFace": "Treniraj Lice" +} diff --git a/web/public/locales/hr/views/live.json b/web/public/locales/hr/views/live.json new file mode 100644 index 0000000..a60f87c --- /dev/null +++ b/web/public/locales/hr/views/live.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "default": "Uživo - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Uživo - Frigate", + "twoWayTalk": { + "enable": "Omogući dvosmjerni razgovor", + "disable": "Onemogući dvosmjerni razgovor" + }, + "cameraAudio": { + "enable": "Omogući zvuk kamere", + "disable": "Onemogući zvuk kamere" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Klikni unutar kadra da centriraš kameru", + "enable": "Omogući pomicanje klikom", + "disable": "Onemogući pomicanje klikom" + }, + "left": { + "label": "Pomakni PTZ kameru u lijevo" + }, + "up": { + "label": "Pomakni PTZ kameru gore" + }, + "down": { + "label": "Pomakni PTZ kameru dolje" + }, + "right": { + "label": "Pomakni PTZ kameru u desno" + } + }, + "zoom": { + "in": { + "label": "Približi PTZ kameru" + }, + "out": { + "label": "Udalji PTZ kameru" + } + }, + "focus": { + "in": { + "label": "Izoštri fokus PTZ kamere" + }, + "out": { + "label": "Fokusirajte PTZ kameru prema van" + } + }, + "frame": { + "center": { + "label": "Kliknite unutar kadra da centrirate PTZ kameru" + } + }, + "presets": "Unaprijed postavljene pozicije PTZ kamere" + }, + "lowBandwidthMode": "Način niskog bandwidtha", + "camera": { + "enable": "Omogući Kameru", + "disable": "Onemogući Kameru" + }, + "muteCameras": { + "enable": "Isključi zvuk svih kamera", + "disable": "Uključi zvuk svih kamera" + }, + "detect": { + "enable": "Omogući Detekciju", + "disable": "Onemogući detekciju" + }, + "recording": { + "enable": "Omogući Snimanje", + "disable": "Onemogući Snimanje" + }, + "snapshots": { + "enable": "Omogući Snimke", + "disable": "Onemogući snimke slike" + }, + "snapshot": { + "takeSnapshot": "Preuzmi instantnu snimku slike", + "noVideoSource": "Video izvor nije dostupan za snimku slike.", + "captureFailed": "Snimanje slike neuspješno.", + "downloadStarted": "Preuzimanje snimke slike započeto." + }, + "audioDetect": { + "enable": "Omogući Zvučnu Detekciju", + "disable": "Onemogući Zvučnu Detekciju" + }, + "transcription": { + "enable": "Omogući Transkripciju Zvuka Uživo", + "disable": "Onemogući Transkripciju Zvuka Uživo" + }, + "autotracking": { + "enable": "Omogući Automatsko Praćenje", + "disable": "Onemogući Auto Praćenje" + }, + "streamStats": { + "enable": "Prikaži statistike emitiranja", + "disable": "Sakrij statistike emitiranja" + }, + "manualRecording": { + "title": "Na Zahtjev", + "tips": "Preuzmite trenutnu snimku ili pokrenite ručni događaj prema postavkama zadržavanja snimki ove kamere.", + "playInBackground": { + "label": "Reproduciraj u pozadini", + "desc": "Omogući ovu opciju za nastavak emitiranja kada je prozor za reprodukciju skriven." + }, + "showStats": { + "label": "Prikaži statistike", + "desc": "Omogući ovu opciju da se statistika emitiranja prikazuje preko prikaza kamere." + }, + "debugView": "Debug prikaz", + "start": "Pokreni snimanje na zahtjev", + "started": "Pokrenuto ručno snimanje na zahtjev.", + "failedToStart": "Neuspješno pokretanje ručnog snimanja na zahtjev.", + "recordDisabledTips": "Budući da je snimanje onemogućeno ili ograničeno u konfiguraciji za ovu kameru, spremit će se samo jedna snimka.", + "end": "Kraj snimanja na zahtjev", + "ended": "Prekinuto je ručno snimanje na zahtjev.", + "failedToEnd": "Neuspješno prekidanje ručnog snimanja na zahtjev." + }, + "streamingSettings": "Postavke emitiranja", + "notifications": "Obavijesti", + "audio": "Audio", + "suspend": { + "forTime": "Pauziraj zbog: " + }, + "stream": { + "title": "Emitiranje", + "audio": { + "tips": { + "title": "Zvuk mora biti izlazan iz vaše kamere i konfiguriran u go2rtc za ovaj stream." + }, + "available": "Audio je dostupan za ovo emitiranje", + "unavailable": "Audio nije dostupan za ovo emitiranje" + }, + "debug": { + "picker": "Odabir streama nije dostupan u debug načinu. Debug prikaz uvijek koristi emitiranje dodijeljeno ulozi detekcije." + }, + "twoWayTalk": { + "tips": "Vaš uređaj mora podržavati tu značajku, a WebRTC mora biti konfiguriran za dvosmjernu komunikaciju.", + "available": "Za ovo emitiranje dostupan je dvosmjerni razgovor", + "unavailable": "Dvosmjerni razgovor nije dostupan za ovo emitiranje" + }, + "lowBandwidth": { + "tips": "Prikaz uživo je u načinu rada s niskom propusnošću zbog međuspremnika ili grešaka u emitiranju.", + "resetStream": "Resetiraj emitiranje" + }, + "playInBackground": { + "label": "Reproduciraj u pozadini", + "tips": "Omogući ovu opciju za nastavak emitiranja kad je player skriven." + } + }, + "cameraSettings": { + "title": "{{camera}} Postavke", + "cameraEnabled": "Kamera omogućena", + "objectDetection": "Detekcija objekata", + "recording": "Snimanje", + "snapshots": "Snimke", + "audioDetection": "Detekcija zvuka", + "transcription": "Audio transkripcija", + "autotracking": "Automatsko praćenje" + }, + "history": { + "label": "Prikaži povijesne snimke" + }, + "effectiveRetainMode": { + "modes": { + "all": "Svi", + "motion": "Prijedlog", + "active_objects": "Aktivni objekti" + } + }, + "editLayout": { + "label": "Uredi raspored", + "group": { + "label": "Uredi Grupu Kamera" + }, + "exitEdit": "Izlaz iz uređivanja" + }, + "noCameras": { + "title": "Nema konfiguriranih kamera", + "description": "Započnite povezivanjem kamere na Frigate.", + "buttonText": "Dodaj kameru", + "restricted": { + "title": "Nema dostupnih kamera", + "description": "Nemate dopuštenje za gledanje kamera u ovoj grupi." + }, + "default": { + "title": "Nema konfiguriranih kamera", + "description": "Započnite povezivanjem kamere na Frigate.", + "buttonText": "Dodaj kameru" + }, + "group": { + "title": "Nema kamera u grupi", + "description": "Ova grupa kamera nema dodijeljene niti omogućene kamere.", + "buttonText": "Upravljanje grupama" + } + } +} diff --git a/web/public/locales/hr/views/motionSearch.json b/web/public/locales/hr/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hr/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hr/views/recording.json b/web/public/locales/hr/views/recording.json new file mode 100644 index 0000000..8470b3e --- /dev/null +++ b/web/public/locales/hr/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filter", + "export": "Izvoz", + "calendar": "Kalendar", + "filters": "Filteri", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Vrijeme završetka mora biti nakon vremena početka", + "noValidTimeSelected": "Nije izabran ispravan vremenski raspon" + } + } +} diff --git a/web/public/locales/hr/views/replay.json b/web/public/locales/hr/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hr/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hr/views/search.json b/web/public/locales/hr/views/search.json new file mode 100644 index 0000000..984c3f3 --- /dev/null +++ b/web/public/locales/hr/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Pretraga", + "savedSearches": "Spremljene pretrage", + "searchFor": "Pretraži {{inputValue}}", + "button": { + "clear": "Izbriši pretragu", + "save": "Spremi pretragu", + "delete": "Obriši spremljene pretrage", + "filterInformation": "Filtriraj informacije", + "filterActive": "Filteri aktivni" + }, + "filter": { + "label": { + "cameras": "Kamere", + "labels": "Oznake", + "zones": "Zone", + "search_type": "Pretraži vrstu", + "time_range": "Vremenski raspon", + "attributes": "Atributi", + "before": "Prije", + "after": "Poslije", + "min_score": "Min ocjena", + "sub_labels": "Podoznake", + "max_score": "Maksimalni rezultat", + "min_speed": "Minimalna Brzina", + "max_speed": "Maksimalna Brzina", + "recognized_license_plate": "Prepoznata Registarska Oznaka", + "has_clip": "Ima isječak", + "has_snapshot": "Ima Snimku" + }, + "searchType": { + "thumbnail": "Sličica", + "description": "Opis" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Datum 'prije' mora biti kasniji od datuma 'poslije'.", + "afterDatebeEarlierBefore": "Datum 'poslije' mora biti raniji od datuma 'prije'.", + "minScoreMustBeLessOrEqualMaxScore": "Vrijednost 'min_score' mora biti manja ili jednaka vrijednosti 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Vrijednost 'max_score' mora biti veća ili jednaka vrijednosti 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Vrijednost 'min_speed' mora biti manja ili jednaka vrijednosti 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Vrijednost 'max_speed' mora biti veća ili jednaka vrijednosti 'min_speed'." + } + }, + "tips": { + "title": "Kako koristiti text filtere", + "desc": { + "text": "Filtri pomažu suziti rezultate pretraživanja. Evo kako ih koristiti u polju za unos:", + "step1": "Upišite naziv ključa filtra, a zatim dvotočku (npr. 'cameras:').", + "step2": "Odaberite vrijednost iz prijedloga ili unesite svoju.", + "step3": "Koristite više filtera tako da ih dodajete jedan za drugim s razmakom između.", + "step4": "Filteri po datumu (before: i after:) koriste format {{DateFormat}}.", + "step5": "Filter vremenskog raspona koristi format {{exampleTime}}.", + "step6": "Uklonite filtre klikom na 'x' pored njih.", + "exampleLabel": "Primjer:" + } + }, + "header": { + "currentFilterType": "Vrijednosti Filtra", + "noFilters": "Filteri", + "activeFilters": "Aktivni Filteri" + } + }, + "trackedObjectId": "ID Praćenog Objekta", + "similaritySearch": { + "title": "Pretraga po sličnosti", + "active": "Pretraživanje po sličnosti aktivno", + "clear": "Deaktiviraj pretraživanje po sličnosti" + }, + "placeholder": { + "search": "Pretraži…" + } +} diff --git a/web/public/locales/hr/views/settings.json b/web/public/locales/hr/views/settings.json new file mode 100644 index 0000000..f93d862 --- /dev/null +++ b/web/public/locales/hr/views/settings.json @@ -0,0 +1,1072 @@ +{ + "documentTitle": { + "default": "Postavke - Frigate", + "authentication": "Postavke autentikacije - Frigate", + "cameraManagement": "Upravljaj kamerama - Frigate", + "masksAndZones": "Uređivač maski i zona - Frigate", + "general": "Postavke sučelja - Frigate", + "frigatePlus": "Frigate+ postavke - Frigate", + "notifications": "Postavke notifikacija - Frigate", + "enrichments": "Postavke obogaćivanja - Frigate", + "cameraReview": "Postavke Pregleda Kamere - Frigate", + "motionTuner": "Podešivač pokreta - Frigate", + "object": "Debug - Frigate" + }, + "menu": { + "ui": "Sučelje", + "cameraReview": "Pregled", + "enrichments": "Obogaćenja", + "masksAndZones": "Maske / Zone", + "triggers": "Okidači", + "users": "Korisnici", + "cameraManagement": "Upravljanje", + "motionTuner": "Podešivač pokreta", + "debug": "Debug", + "roles": "Uloga", + "notifications": "Obavijesti", + "frigateplus": "Frigate+" + }, + "dialog": { + "unsavedChanges": { + "title": "Imaš nespremljene promjene.", + "desc": "Želiš li spremiti promjene prije nastavka?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Nema Kamere" + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} točka", + "point_few": "{{count}} točke", + "point_other": "{{count}} točaka", + "label": "Zone", + "documentTitle": "Zona uređivanja - Frigate", + "desc": { + "title": "Zone vam omogućuju definiranje određenog područja kadra kako biste mogli odrediti nalazi li se objekt unutar određenog područja ili ne.", + "documentation": "Dokumentacija" + }, + "add": "Dodaj zonu", + "edit": "Uredi zonu", + "clickDrawPolygon": "Kliknite za crtanje poligona na slici.", + "name": { + "title": "Ime", + "inputPlaceHolder": "Unesite ime…", + "tips": "Ime mora imati najmanje 2 znaka, mora imati barem jedno slovo i ne smije biti ime kamere ili druge zone na toj kameri." + }, + "inertia": { + "title": "Inercija", + "desc": "Specificira koliko okvira objekt mora biti u zoni prije nego što se smatra u zoni. Zadani: 3" + }, + "loiteringTime": { + "title": "Vrijeme zadržavanja", + "desc": "Postavlja minimalno vrijeme u sekundama koliko objekt mora biti u zoni da bi se aktivirao. Zadano: 0" + }, + "objects": { + "title": "Objekti", + "desc": "Popis objekata koji se odnose na ovu zonu." + }, + "allObjects": "Svi predmeti", + "speedEstimation": { + "title": "Procjena brzine", + "desc": "Omogućite procjenu brzine za objekte u toj zoni. Zona mora imati točno 4 točke.", + "lineADistance": "Udaljenost linije A ({{unit}})", + "lineBDistance": "Udaljenost linije B ({{unit}})", + "lineCDistance": "Udaljenost linije C ({{unit}})", + "lineDDistance": "Udaljenost linije D ({{unit}})" + }, + "speedThreshold": { + "title": "Prag brzine ({{unit}})", + "desc": "Specificira minimalnu brzinu za objekte koji se uzimaju u obzir u ovoj zoni.", + "toast": { + "error": { + "pointLengthError": "Procjena brzine je onemogućena za ovu zonu. Zone s procjenom brzine moraju imati točno 4 točke.", + "loiteringTimeError": "Zone s vremenima zadržavanja većim od 0 ne smiju se koristiti za procjenu brzine." + } + } + }, + "toast": { + "success": "Zona ({{zoneName}}) je spremljena." + } + }, + "motionMasks": { + "point_one": "{{count}} točka", + "point_few": "{{count}} točke", + "point_other": "{{count}} točaka", + "label": "Maska za pokret", + "documentTitle": "Uredi Masku Kretnje - Frigate", + "desc": { + "title": "Maske za pokret koriste se kako bi se spriječilo da neželjeni pokreti pokrenu detekciju. Pretjerano maskiranje otežat će praćenje objekata.", + "documentation": "Dokumentacija" + }, + "add": "Nova maska za pokret", + "edit": "Uredi Masku Pokreta", + "context": { + "title": "Maske pokreta koriste se za sprječavanje neželjenih vrsta pokreta da pokrenu detekciju (primjer: grane drveća, vremenske oznake kamere). Maske za pokret treba koristiti vrlo štedljivo, prekomjerno maskiranje će otežati praćenje objekata." + }, + "clickDrawPolygon": "Kliknite za crtanje poligona na slici.", + "polygonAreaTooLarge": { + "title": "Maska pokreta pokriva {{polygonArea}}% kadra kamere. Velike maske za pokret se ne preporučuju.", + "tips": "Maske za pokret ne sprječavaju otkrivanje objekata. Trebao bi koristiti obaveznu zonu umjesto toga." + }, + "toast": { + "success": { + "title": "{{polygonName}} je spremljen.", + "noName": "Maska Pokreta je spremljena." + } + } + }, + "objectMasks": { + "point_one": "{{count}} točka", + "point_few": "{{count}} točke", + "point_other": "{{count}} točaka", + "label": "Maske Objekta", + "documentTitle": "Uredi masku objekta - Frigate", + "desc": { + "title": "Maske filtra objekta koriste se za filtriranje lažno pozitivnih rezultata za određenu vrstu objekta na temelju lokacije.", + "documentation": "Dokumentacija" + }, + "add": "Dodaj masku objekta", + "edit": "Uredi masku objekta", + "context": "Maske filtra objekta koriste se za filtriranje lažno pozitivnih rezultata za određenu vrstu objekta na temelju lokacije.", + "clickDrawPolygon": "Kliknite za crtanje poligona na slici.", + "objects": { + "title": "Objekti", + "desc": "Tip objekta koji se primjenjuje na ovu masku objekta.", + "allObjectTypes": "Sve vrste objekata" + }, + "toast": { + "success": { + "title": "{{polygonName}} je spremljen.", + "noName": "Object Mask je spremljen." + } + } + }, + "filter": { + "all": "Sve maske i zone" + }, + "restart_required": "Potrebano je ponovno pokretanje (maske/zone promijenjene)", + "toast": { + "success": { + "copyCoordinates": "Kopirane koordinate za {{polyName}} u međuspremnik." + }, + "error": { + "copyCoordinatesFailed": "Neuspješno kopiranje koordinate u međuspremnik." + } + }, + "motionMaskLabel": "Maska kretnje {{number}}", + "objectMaskLabel": "Maska objekta {{number}} ({{label}})", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Naziv zone mora imati najmanje 2 znaka.", + "mustNotBeSameWithCamera": "Naziv zone ne smije biti isti kao naziv kamere.", + "alreadyExists": "Zona s tim imenom već postoji za ovu kameru.", + "mustNotContainPeriod": "Naziv zone ne smije sadržavati točke.", + "hasIllegalCharacter": "Naziv zone sadrži ilegalne znakove.", + "mustHaveAtLeastOneLetter": "Naziv zone mora imati barem jedno slovo." + } + }, + "distance": { + "error": { + "text": "Udaljenost mora biti veća ili jednaka 0,1.", + "mustBeFilled": "Sva polja udaljenosti moraju biti ispunjena kako bi se koristila procjena brzine." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Inercija mora biti iznad 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Vrijeme zadržavanja mora biti veće ili jednako 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Prag brzine mora biti veći ili jednak 0,1." + } + }, + "polygonDrawing": { + "removeLastPoint": "Uklonite zadnju točku", + "reset": { + "label": "Očisti sve točke" + }, + "snapPoints": { + "true": "Točke hvatanja", + "false": "Nemoj hvatati točke" + }, + "delete": { + "title": "Potvrdi brisanje", + "desc": "Jeste li sigurni da želite izbrisati {{type}} {{name}}??", + "success": "{{name}} je izbrisan." + }, + "error": { + "mustBeFinished": "Crtanje poligona mora biti završeno prije spremanja." + } + } + } + }, + "roles": { + "toast": { + "success": { + "userRolesUpdated_one": "{{count}} korisnik dodijeljen ovoj ulozi ažuriran je na 'gledatelj', koji ima pristup svim kamerama.", + "userRolesUpdated_few": "{{count}} korisnika dodijeljena ovoj ulozi ažurirana su na 'gledatelj', koji imaju pristup svim kamerama.", + "userRolesUpdated_other": "{{count}} korisnika dodijeljena ovoj ulozi ažurirana su na 'gledatelj', koji imaju pristup svim kamerama.", + "createRole": "Uloga {{role}} uspješno stvorena", + "updateCameras": "Kamere ažurirane za ulogu {{role}}", + "deleteRole": "Uloga {{role}} uspješno izbrisana" + }, + "error": { + "createRoleFailed": "Nije uspjelo napraviti ulogu: {{errorMessage}}", + "updateCamerasFailed": "Nije uspjelo ažurirati kamere: {{errorMessage}}", + "deleteRoleFailed": "Nije uspio izbrisati ulogu: {{errorMessage}}", + "userUpdateFailed": "Nije uspjelo ažurirati korisničke uloge: {{errorMessage}}" + } + }, + "management": { + "title": "Upravljanje ulogama gledatelja", + "desc": "Upravljajte prilagođenim ulogama preglednika i njihovim dozvolama za pristup kameri za ovu Frigate instancu." + }, + "addRole": "Dodaj ulogu", + "table": { + "role": "Uloga", + "cameras": "Kamere", + "actions": "Radnje", + "noRoles": "Nema pronađenih prilagođenih uloga.", + "editCameras": "Uredi kamere", + "deleteRole": "Izbriši ulogu" + }, + "dialog": { + "createRole": { + "title": "Napravi novu ulogu", + "desc": "Dodajte novu ulogu i odredite dozvole za pristup kameri." + }, + "editCameras": { + "title": "Uredi kamere uloge", + "desc": "Ažurirajte pristup kameri za ulogu {{role}}." + }, + "deleteRole": { + "title": "Izbriši ulogu", + "desc": "Ova radnja se ne može poništiti. To će trajno izbrisati ulogu i dodijeliti sve korisnike s tom ulogom ulozi 'gledatelj', što će gledatelju omogućiti pristup svim kamerama.", + "warn": "Jeste li sigurni da želite izbrisati {{role}}?", + "deleting": "Brisanje..." + }, + "form": { + "role": { + "title": "Naziv uloge", + "placeholder": "Unesite naziv uloge", + "desc": "Dozvoljeni su samo slova, brojevi, točkice i podcrtice.", + "roleIsRequired": "Potreban je naziv uloge", + "roleOnlyInclude": "Naziv uloge može sadržavati samo slova, brojeve, . ili _", + "roleExists": "Već postoji uloga s tim imenom." + }, + "cameras": { + "title": "Kamere", + "desc": "Odaberite kamere kojima ova pozicija ima pristup. Potrebna je barem jedna kamera.", + "required": "Mora se odabrati barem jedna kamera." + } + } + } + }, + "general": { + "title": "Postavke Korisničkog Sučelja", + "liveDashboard": { + "title": "Nadzorna Ploča Uživo", + "automaticLiveView": { + "label": "Automatski Prikaz Uživo", + "desc": "Automatski prebacite na prikaz uživo kamere kada se detektira aktivnost. Isključivanje ove opcije uzrokuje da se statične slike kamere na Nadzornoj Ploči Uživo ažuriraju samo jednom u minuti." + }, + "playAlertVideos": { + "label": "Reproduciraj videozapise upozorenja", + "desc": "Po zadanom, nedavna upozorenja na Nadzornoj Ploči Uživo prikazuju se kao mali videozapisi u petlji. Onemogućite ovu opciju da na ovom uređaju/pregledniku prikazuje samo statičnu sliku nedavnih upozorenja." + }, + "displayCameraNames": { + "label": "Uvijek prikazuj imena kamera", + "desc": "Uvijek prikazujte imena kamera na prozoru u nadzornoj ploči uživo s više kamera." + }, + "liveFallbackTimeout": { + "label": "Istek vremena za dohvaćanje rezervne reprodukcije uživo", + "desc": "Kada visokokvalitetni prijenos uživo s kamere nije dostupan, nakon toliko sekundi vratite se na način rada s niskom propusnošću. Zadano: 3." + } + }, + "storedLayouts": { + "title": "Pohranjeni rasporedi", + "desc": "Raspored kamera u grupi kamera može se povlačiti/mijenjati veličinu. Pozicije se pohranjuju u lokalnoj pohrani vašeg preglednika.", + "clearAll": "Očistite sve rasporede" + }, + "cameraGroupStreaming": { + "title": "Postavke emitiranja grupe kamera", + "desc": "Postavke emitiranja za svaku grupu kamera pohranjene su u lokalnoj memoriji vašeg preglednika.", + "clearAll": "Obriši sve postavke emitianja" + }, + "recordingsViewer": { + "title": "Preglednik snimki", + "defaultPlaybackRate": { + "label": "Zadana brzina reprodukcije", + "desc": "Zadana brzina reprodukcije za reprodukciju snimki." + } + }, + "calendar": { + "title": "Kalendar", + "firstWeekday": { + "label": "Prvi dan tjedna", + "desc": "Dan kada počinje tjedan svih kalendara pregleda.", + "sunday": "Nedjelja", + "monday": "Ponedjeljak" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Očišćen pohranjeni raspored za {{cameraName}}", + "clearStreamingSettings": "Obrisane postavke streaminga za sve grupe kamera." + }, + "error": { + "clearStoredLayoutFailed": "Neuspješno čišćenje pohranjenih rasporeda: {{errorMessage}}", + "clearStreamingSettingsFailed": "Neuspješno čišćenje postavki emitiranja: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Postavke obogaćivanja", + "unsavedChanges": "Promjene u postavkama nespremljenih obogaćivanja", + "birdClassification": { + "title": "Klasifikacija ptica", + "desc": "Klasifikacija ptica identificira poznate ptice pomoću kvantiziranog Tensorflow modela. Kada se prepozna poznata ptica, njezino uobičajeno ime dodaje se kao sub_label. Te informacije su uključene u korisničko sučelje, filtre, kao i u obavijesti." + }, + "semanticSearch": { + "title": "Semantičko pretraživanje", + "desc": "Semantičko pretraživanje u Frigateu omogućuje pronalaženje praćenih objekata unutar tvojih preglednih stavki koristeći samu sliku, korisnički definirani tekstualni opis ili automatski generiran.", + "reindexNow": { + "label": "Ponovno indeksiraj sada", + "desc": "Reindeksiranje će generirati ugradnje za sve praćene objekte. Ovaj proces radi u pozadini i može maksimalno opteretiti CPU te potrajati dosta vremena, ovisno o broju praćenih objekata koje imaš.", + "confirmTitle": "Potvrdi ponovno indeksiranje", + "confirmDesc": "Jesi li siguran da želiš ponovno indeksirati sve ugrađene objekte praćenih? Ovaj proces će raditi u pozadini, ali može maksimalno opteretiti CPU i oduzeti dosta vremena. Napredak možeš pratiti na stranici Istraži.", + "confirmButton": "Reindeksiranje", + "success": "Reindeksiranje je započelo uspješno.", + "alreadyInProgress": "Reindeksiranje je već u tijeku.", + "error": "Nije uspjelo započeti ponovno indeksiranje: {{errorMessage}}" + }, + "modelSize": { + "label": "Veličina modela", + "desc": "Veličina modela koji se koristi za semantička pretraživanja.", + "small": { + "title": "mali", + "desc": "Korištenje malih koristi kvantiziranu verziju modela koja koristi manje RAM-a i radi brže na CPU-u uz vrlo zanemarivu razliku u kvaliteti ugrađivanja." + }, + "large": { + "title": "veliki", + "desc": "Korištenje velikih koristi puni Jina model i automatski će raditi na GPU-u ako je primjenjivo." + } + } + }, + "faceRecognition": { + "title": "Prepoznavanje lica", + "desc": "Prepoznavanje lica omogućuje ljudima dodjeljivanje imena, a kada se lice prepozna, Frigate će dodijeliti ime osobe kao podoznaku. Te informacije su uključene u korisničko sučelje, filtre, kao i u obavijesti.", + "modelSize": { + "label": "Veličina modela", + "desc": "Veličina modela koji se koristi za prepoznavanje lica.", + "small": { + "title": "mali", + "desc": "Korištenjem mali koristi FaceNet model ugradnje lica koji učinkovito radi na većini CPU-a." + }, + "large": { + "title": "veliki", + "desc": "Korištenjem veliki koristi ArcFace model ugradnje lica i automatski se pokreće na GPU-u ako je primjenjivo." + } + } + }, + "licensePlateRecognition": { + "title": "Prepoznavanje registarskih oznaka", + "desc": "Frigate može prepoznati registarske oznake na vozilima i automatski dodati detektirane znakove u polje recognized_license_plate ili poznato ime kao sub_label objektima tipa auto. Česta upotreba može biti čitanje registarskih oznaka automobila koji ulaze u prilaz ili automobila koji prolaze ulicom." + }, + "restart_required": "Potrebno ponovno pokretanje (Postavke obogaćivanja promijenjene)", + "toast": { + "success": "Postavke obogaćivanja su spremljene. Ponovno pokrenite Frigate da primijenite promjene.", + "error": "Neuspješno spremanje promjena konfiguracije: {{errorMessage}}" + } + }, + "cameraWizard": { + "title": "Dodaj kameru", + "description": "Slijedite korake u nastavku kako biste dodali novu kameru na svoju Frigate instalaciju.", + "steps": { + "nameAndConnection": "Ime i povezanost", + "probeOrSnapshot": "Sonda ili snimka", + "streamConfiguration": "Konfiguracija toka", + "validationAndTesting": "Validacija i testiranje" + }, + "save": { + "success": "Uspješno spremljena nova kamera {{cameraName}}.", + "failure": "Greška spremanja {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Rezolucija", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Molimo unesite važeći URL emitiraja", + "testFailed": "Test emitiranja nije uspio: {{error}}" + }, + "step1": { + "description": "Unesite podatke o svojoj kameri i odaberite ćete li ispitati kameru ili odabrati marku.", + "cameraName": "Ime kamere", + "cameraNamePlaceholder": "npr. front_door ili pregled dvorišta", + "host": "Host/IP adresa", + "port": "Port", + "username": "Korisničko ime", + "usernamePlaceholder": "Opcionalno", + "password": "Lozinka", + "passwordPlaceholder": "Opcionalno", + "selectTransport": "Odaberi transportni protokol", + "cameraBrand": "Marka fotoaparata", + "selectBrand": "Odaberite marku kamere za URL predložak", + "customUrl": "Prilagođeni URL emitiranja", + "brandInformation": "Informacije o marki", + "brandUrlFormat": "Za kamere s RTSP URL formatom kao: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "connectionSettings": "Postavke veze", + "detectionMethod": "Metoda detekcije emitiranja", + "onvifPort": "ONVIF port", + "probeMode": "Ispitaj kameru", + "manualMode": "Ručni odabir", + "detectionMethodDescription": "Pretražujte kameru pomoću ONVIF-a (ako je podržano) kako biste pronašli URL-ove prijenosa kamere ili ručno odaberite marku kamere za korištenje unaprijed definiranih URL-ova. Za unos prilagođenog RTSP URL-a, odaberite ručnu metodu i odaberite \"Drugo\".", + "onvifPortDescription": "Za kamere koje podržavaju ONVIF, to je obično 80 ili 8080.", + "useDigestAuth": "Koristite digest autentifikaciju", + "useDigestAuthDescription": "Koristite HTTP digest autentifikaciju za ONVIF. Neke kamere mogu zahtijevati namjensko ONVIF korisničko ime/lozinku umjesto standardnog administratorskog korisnika.", + "errors": { + "brandOrCustomUrlRequired": "Odaberite marku kamere s host/IP ili odaberite 'Drugo' s prilagođenim URL-om", + "nameRequired": "Potrebno je ime kamere", + "nameLength": "Ime kamere mora imati 64 znaka ili manje", + "invalidCharacters": "Ime kamere sadrži nevaljane znakove", + "nameExists": "Ime kamere već postoji", + "customUrlRtspRequired": "Prilagođeni URL-ovi moraju započeti s \"rtsp://\". Ručna konfiguracija potrebna je za prijenose kamera bez RTSP-a." + } + }, + "step2": { + "description": "Ispitaj kameru za dostupna emitiranja ili konfiguriraj ručne postavke prema odabranoj metodi detekcije.", + "testSuccess": "Test veze uspješan!", + "testFailed": "Test veze nije uspio. Molimo provjerite svoj unos i pokušajte. ponovno.", + "testFailedTitle": "Test neuspješan", + "streamDetails": "Detalji streama", + "probing": "Ispitivanje kamere...", + "retry": "Ponovno pokušaj", + "testing": { + "probingMetadata": "Ispitivanje metapodataka kamere...", + "fetchingSnapshot": "Dohvaćanje snimke s kamere..." + }, + "probeFailed": "Kamera nije uspješno ispitana: {{error}}", + "probingDevice": "Ispitivanje uređaja...", + "probeSuccessful": "Ispitivanje uspješno", + "probeError": "Greška ispitivanja", + "probeNoSuccess": "Ispitivanje neuspješno", + "deviceInfo": "Informacije o uređaju", + "manufacturer": "Proizvođač", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profili", + "ptzSupport": "PTZ podrška", + "autotrackingSupport": "Podrška za automatsko praćenje", + "presets": "Predlošci", + "rtspCandidates": "Kandidati RTSP-a", + "rtspCandidatesDescription": "Sljedeći RTSP URL-ovi pronađeni su putem ispitivanja kamere. Testirajte vezu za pregled metapodataka streama.", + "noRtspCandidates": "Kamera nije pronašla nikakve RTSP URL-ove. Vaše vjerodajnice mogu biti netočne, ili kamera možda ne podržava ONVIF ili metodu korištenu za dohvaćanje RTSP URL-ova. Vratite se i ručno unesite RTSP URL.", + "candidateStreamTitle": "Kandidat {{number}}", + "useCandidate": "Koristi", + "uriCopy": "Kopiraj", + "uriCopied": "URI kopiran u međuspremnik", + "testConnection": "Testiraj Vezu", + "toggleUriView": "Kliknite za isključivanje punog prikaza URI-ja", + "connected": "Povezano", + "notConnected": "Nije povezano", + "errors": { + "hostRequired": "Potrebna je host/IP adresa" + } + }, + "step3": { + "description": "Konfigurirajte uloge emitiranja i dodajte dodatna emitiranja za svoju kameru.", + "streamsTitle": "Emitiranja kamere", + "addStream": "Dodaj emitiranje", + "addAnotherStream": "Dodaj još jedno emitiranje", + "streamTitle": "Emitiranje {{number}}", + "streamUrl": "URL emitiranja", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "selectStream": "Odaberi emitiranje", + "searchCandidates": "Pretraži kandidate...", + "noStreamFound": "Nije pronađeno emitiranje", + "url": "URL", + "resolution": "Rezolucija", + "selectResolution": "Odaberite rezoluciju", + "quality": "Kvaliteta", + "selectQuality": "Odaberite kvalitetu", + "roles": "Uloge", + "roleLabels": { + "detect": "Detekcija objekata", + "record": "Snimanje", + "audio": "Audio" + }, + "testStream": "Testiraj Vezu", + "testSuccess": "Test emitiranja uspješan!", + "testFailed": "Test emitiranja neuspješan", + "testFailedTitle": "Test neuspješan", + "connected": "Povezano", + "notConnected": "Nije povezano", + "featuresTitle": "Značajke", + "go2rtc": "Smanji broj veza s kamerom", + "detectRoleWarning": "Najmanje jedano emitiranje mora imati ulogu \"detekcija\" da bi nastavili.", + "rolesPopover": { + "title": "Uloge emitiranja", + "detect": "Glavni izvor za detekciju objekata.", + "record": "Sprema segmente video toka na temelju konfiguracijskih postavki.", + "audio": "Tok za detekciju zvuka." + }, + "featuresPopover": { + "title": "Značajke emitranja", + "description": "Koristi go2rtc restreaming kako bi smanjio vezu s kamerom." + } + }, + "step4": { + "description": "Završna provjera i analiza prije spremanja nove kamere. Povežite svako emitiranje prije spremanja.", + "validationTitle": "Validacija emitiranja", + "connectAllStreams": "Poveži sva emitiranja", + "reconnectionSuccess": "Ponovno povezivanje uspješno.", + "reconnectionPartial": "Neka emitiranja se nisu uspjela ponovno povezati.", + "streamUnavailable": "Pregled emitiranja nije dostupan", + "reload": "Ponovno učitavanje", + "connecting": "Povezivanje...", + "streamTitle": "Emitiranje {{number}}", + "valid": "Valjano", + "failed": "Neuspješno", + "notTested": "Nije testirano", + "connectStream": "Poveži se", + "connectingStream": "Povezivanje", + "disconnectStream": "Prekid veze", + "estimatedBandwidth": "Procijenjena propusnost", + "roles": "Uloge", + "ffmpegModule": "Koristite način kompatibilnosti emitiranja", + "ffmpegModuleDescription": "Ako se emitiranje ne učita nakon nekoliko pokušaja, pokušajte ovo omogućiti. Kad je uključeno, Frigate će koristiti ffmpeg modul s go2rtc-om. To može pružiti bolju kompatibilnost s nekim prijenosima kamera.", + "none": "Nema", + "error": "Pogreška", + "streamValidated": "Emitiranje {{number}} uspješno validiran", + "streamValidationFailed": "Validacija emitiranja {{number}} nije uspjela", + "saveAndApply": "Spremi novu kameru", + "saveError": "Nevažeća konfiguracija. Molimo provjerite svoje postavke.", + "issues": { + "title": "Validacija emitiranja", + "videoCodecGood": "Video kodek je {{codec}}.", + "audioCodecGood": "Audio kodek je {{codec}}.", + "resolutionHigh": "Rezolucija od {{resolution}} može uzrokovati povećanu potrošnju resursa.", + "resolutionLow": "Rezolucija od {{resolution}} može biti preniska za pouzdanu detekciju malih objekata.", + "noAudioWarning": "Za ovo emitiranje nije detektiran zvuk, snimke neće imati zvuk.", + "audioCodecRecordError": "AAC audio kodek je potreban za podršku zvuka u snimkama.", + "audioCodecRequired": "Za podršku detekciji zvuka potreban je audio emitiranje.", + "restreamingWarning": "Smanjivanje broja veza s kamerom za emitiranje za snimanje može neznatno povećati upotrebu CPU-a.", + "brands": { + "reolink-rtsp": "Reolink RTSP se ne preporučuje. Omogući HTTP u postavkama firmware-a kamere i ponovno pokreni čarobnjak.", + "reolink-http": "Reolink HTTP tokovi trebali bi koristiti FFmpeg radi bolje kompatibilnosti. Omogući 'Koristi način kompatibilnosti streama' za ovaj stream." + }, + "dahua": { + "substreamWarning": "Podstream 1 je zaključan na nisku rezoluciju. Mnoge Dahua / Amcrest / EmpireTech kamere podržavaju dodatne podstreamove koje treba omogućiti u postavkama kamere. Preporučuje se provjeriti i koristiti te streamove ako su dostupni." + }, + "hikvision": { + "substreamWarning": "Podstream 1 je zaključan na nisku rezoluciju. Mnoge Hikvision kamere podržavaju dodatne podstreamove koje je potrebno omogućiti u postavkama kamere. Preporučuje se provjeriti i koristiti te streamove ako su dostupni." + } + } + } + }, + "cameraManagement": { + "title": "Upravljanje kamerama", + "addCamera": "Dodaj novu kameru", + "editCamera": "Uredi kameru:", + "selectCamera": "Odaberite kameru", + "backToSettings": "Povratak na postavke kamere", + "streams": { + "title": "Omogući / Onemogući kamere", + "desc": "Privremeno onemogući kameru dok se frigate ponovno ne pokrene. Onemogućavanje kamere potpuno zaustavlja Frigateovu obradu streamova te kamere. Detekcija, snimanje i otklanjanje grešaka neće biti dostupni.
    Napomena: Ovo ne onemogućuje go2rtc reemitiranja." + }, + "cameraConfig": { + "add": "Dodaj kameru", + "edit": "Uredi kameru", + "description": "Konfigurirajte postavke kamere uključujući ulaze i uloge emitiranja.", + "name": "Ime kamere", + "nameRequired": "Potrebno je ime kamere", + "nameLength": "Ime kamere mora biti manje od 64 znaka.", + "namePlaceholder": "npr. front_door ili pregled dvorišta", + "enabled": "Omogućeno", + "ffmpeg": { + "inputs": "Ulazni tokovi", + "path": "Put emitiranja", + "pathRequired": "Potrebna je putanja emitiranja", + "pathPlaceholder": "rtsp://...", + "roles": "Uloge", + "rolesRequired": "Potrebna je barem jedna uloga", + "rolesUnique": "Svaka uloga (audio, detekcija, snimanje) može se dodijeliti samo jednom emitiranju", + "addInput": "Dodaj ulazno emitiranje", + "removeInput": "Uklonite ulazno emitiranje", + "inputsRequired": "Potreban je barem jedan ulazno emitiranje" + }, + "go2rtcStreams": "go2rtc emitiranja", + "streamUrls": "URL-ovi emitiranja", + "addUrl": "Dodaj URL", + "addGo2rtcStream": "Dodaj go2rtc emitiranje", + "toast": { + "success": "Kamera {{cameraName}} uspješno spremljena" + } + } + }, + "cameraReview": { + "title": "Postavke za pregled kamere", + "object_descriptions": { + "title": "Generativni AI opisi objekata", + "desc": "Privremeno omogući/onemogući generirane AI opise objekata za ovu kameru dok se Frigate ne ponovno pokrene. Kada su onemogućeni, AI-generirani opisi se neće tražiti za praćene objekte na ovoj kameri." + }, + "review_descriptions": { + "title": "Opisi pregleda generativne umjetne inteligencije", + "desc": "Privremeno omogući/onemogući generirane AI opise stavki za pregled za ovu kameru dok se Frigate ne ponovno pokrene. Kada su onemogućeni, AI-generirani opisi se neće tražiti za stavke za pregled na ovoj kameri." + }, + "review": { + "title": "Pregled", + "desc": "Privremeno uključite/isključite upozorenja i detekcije za ovu kameru dok se frigate ne ponovno pokrene. Kada je onemogućen, neće se generirati nove stavke za pregled. ", + "alerts": "Upozorenja ", + "detections": "Detekcije " + }, + "reviewClassification": { + "title": "Klasifikacija pregleda", + "desc": "Frigate kategorizira predmete za pregled kao Upozorenja i Detekcije. Prema zadanim postavkama, svi objekti osoba i automobila smatraju se Obavijestima. Možete precizirati kategorizaciju svojih predmeta za pregled konfiguriranjem potrebnih zona za njih.", + "noDefinedZones": "Za ovu kameru nisu definirane zone.", + "objectAlertsTips": "Svi {{alertsLabels}} objekti na {{cameraName}} prikazat će se kao Upozorenja.", + "zoneObjectAlertsTips": "Svi {{alertsLabels}} objekti otkriveni u {{zone}} na {{cameraName}} prikazat će se kao Upozorenja.", + "objectDetectionsTips": "Svi {{detectionsLabels}} objekti koji nisu kategorizirani na {{cameraName}} prikazat će se kao Detekcije bez obzira u kojoj se zoni nalaze.", + "zoneObjectDetectionsTips": { + "text": "Svi {{detectionsLabels}} objekti koji nisu kategorizirani u {{zone}} na {{cameraName}} prikazat će se kao Detekcije.", + "notSelectDetections": "Svi {{detectionsLabels}} objekti otkriveni u {{zone}} na {{cameraName}}, koji nisu kategorizirani kao Upozorenja, prikazat će se kao Detekcije bez obzira u kojoj se zoni nalaze.", + "regardlessOfZoneObjectDetectionsTips": "Svi {{detectionsLabels}} objekti koji nisu kategorizirani na {{cameraName}} prikazat će se kao Detekcije bez obzira u kojoj se zoni nalaze." + }, + "unsavedChanges": "Postavke klasifikacije recenzije nespremljene za {{camera}}", + "selectAlertsZones": "Odaberite zone za upozorenja", + "selectDetectionsZones": "Odaberite zone za detekciju", + "limitDetections": "Ograničite detekciju na određene zone", + "toast": { + "success": "Konfiguracija klasifikacije je sačuvana. Ponovno pokrenite Frigate da primijenite promjene." + } + } + }, + "motionDetectionTuner": { + "title": "Podešivač detekcije pokreta", + "unsavedChanges": "Nespremljene promjene u Podešivač pokreta ({{camera}})", + "desc": { + "title": "Frigate koristi detekciju pokreta kao prvu provjeru da vidi događa li se nešto u kadru što vrijedi provjeriti detekcijom objekata.", + "documentation": "Pročitajte vodič za podešavanje pokreta" + }, + "Threshold": { + "title": "Prag", + "desc": "Prag određuje kolika je promjena luminancije piksela potrebna da bi se smatrala pokretom. Zadano: 30" + }, + "contourArea": { + "title": "Područje kontura", + "desc": "Vrijednost površine konture koristi se za odlučivanje koje skupine promijenjenih piksela spadaju u pokret. Standardno: 10" + }, + "improveContrast": { + "title": "Poboljšajte kontrast", + "desc": "Poboljšaj kontrast za tamnije scene. Zadano: UKLJUČENO" + }, + "toast": { + "success": "Postavke pokreta su spremljene." + } + }, + "debug": { + "title": "Debugiranje", + "detectorDesc": "Frigate koristi vaše detektore ({{detectors}}) za detekciju objekata u video prijenosu vaše kamere.", + "desc": "Prikaz za otklanjanje grešaka prikazuje prikaz praćenih objekata i njihovih statistika u stvarnom vremenu. Popis objekata prikazuje vremenski odgođeni sažetak detektiranih objekata.", + "openCameraWebUI": "Otvorite web sučelje {{camera}}", + "debugging": "Otklanjanje grešaka", + "objectList": "Popis objekata", + "noObjects": "Nema objekata", + "audio": { + "title": "Audio", + "noAudioDetections": "Nema detekcije zvuka", + "score": "ocjena", + "currentRMS": "Trenutni RMS", + "currentdbFS": "Trenutni dbFS" + }, + "boundingBoxes": { + "title": "Ograničavajuće kutije", + "desc": "Prikaži okvire za granice oko praćenih objekata", + "colors": { + "label": "Boje okvira za ograničavanje objekata", + "info": "
  • Pri pokretanju, različite boje bit će dodijeljene svakoj oznaki objekta
  • . Tamnoplava tanka linija označava da objekt nije detektiran u ovom trenutku
  • . Siva tanka linija označava da je objekt detektiran kao nepokretan.
  • Debela linija označava da je objekt subjekt automatskog praćenja (kada je omogućeno).
  • " + } + }, + "timestamp": { + "title": "Vremenska oznaka", + "desc": "Dodaj vremensku oznaku na sliku" + }, + "zones": { + "title": "Zone", + "desc": "Prikaži pregled bilo kojih definiranih zona" + }, + "mask": { + "title": "Maske pokreta", + "desc": "Prikaži poligone s maskom pokreta" + }, + "motion": { + "title": "Kutije pokreta", + "desc": "Pokažite kutije oko područja gdje se detektira gibanje", + "tips": "

    Kutije pokreta


    Crvene kutije bit će preklopljene na dijelove okvira gdje se trenutno detektira gibanje

    " + }, + "regions": { + "title": "Regije", + "desc": "Prikaži okvir regije od interesa poslan detektoru objekata", + "tips": "

    Kutije Regije


    Svijetlozelene kutije bit će preklopljene na područja od interesa u okviru koja se šalju detektoru objekata.

    " + }, + "paths": { + "title": "Putovi", + "desc": "Prikazati značajne točke putanje praćenog objekta", + "tips": "

    Putovi


    Linije i krugovi označavaju značajne točke na koje se praćeni objekt pomaknuo tijekom svog životnog ciklusa.

    " + }, + "objectShapeFilterDrawing": { + "title": "Crtanje filtera prema obliku objekta", + "desc": "Nacrtajte pravokutnik na slici kako biste vidjeli područje i detalje omjera", + "tips": "Omogućite ovu opciju za crtanje pravokutnika na slici kamere kako biste prikazali njegovu površinu i omjer. Te vrijednosti se zatim mogu koristiti za postavljanje parametara filtera oblika objekta u vašoj konfiguraciji.", + "score": "Ocjena", + "ratio": "Omjer", + "area": "Površina" + } + }, + "users": { + "title": "Korisnici", + "management": { + "title": "Upravljanje korisnicima", + "desc": "Upravljajte korisničkim računima ove Frigate instance." + }, + "addUser": "Dodaj korisnika", + "updatePassword": "Resetiraj lozinku", + "toast": { + "success": { + "createUser": "User {{user}} uspješno stvoren", + "deleteUser": "Korisnik {{user}} uspješno izbrisan", + "updatePassword": "Lozinka je uspješno ažurirana.", + "roleUpdated": "Uloga ažurirana za {{user}}" + }, + "error": { + "setPasswordFailed": "Neuspješno spremanje lozinke: {{errorMessage}}", + "createUserFailed": "Neuspješno stvaranje korisnika: {{errorMessage}}", + "deleteUserFailed": "Neuspješno brisanje korisnika: {{errorMessage}}", + "roleUpdateFailed": "Neuspješno ažuriranje uloge: {{errorMessage}}" + } + }, + "table": { + "username": "Korisničko ime", + "actions": "Radnje", + "role": "Uloga", + "noUsers": "Nema pronađenih korisnika.", + "changeRole": "Promijenite ulogu korisnika", + "password": "Resetiraj lozinku", + "deleteUser": "Izbriši korisnika" + }, + "dialog": { + "form": { + "user": { + "title": "Korisničko ime", + "desc": "Dozvoljeni su samo slova, brojevi, točkice i podcrtice.", + "placeholder": "Unesite korisničko ime" + }, + "password": { + "title": "Lozinka", + "placeholder": "Unesite lozinku", + "show": "Prikaži lozinku", + "hide": "Sakrij lozinku", + "confirm": { + "title": "Potvrdi lozinku", + "placeholder": "Potvrdi lozinku" + }, + "strength": { + "title": "Snaga lozinke: ", + "weak": "Slaba", + "medium": "Srednje jaka", + "strong": "Snažna", + "veryStrong": "Vrlo snažna" + }, + "requirements": { + "title": "Zahtjevi za lozinku:", + "length": "Najmanje 8 znakova", + "uppercase": "Barem jedno veliko slovo", + "digit": "Barem jedna znamenka", + "special": "Barem jedan poseban znak (!@#$%^&*(),.?\":{}|<>)" + }, + "match": "Lozinke se podudaraju", + "notMatch": "Lozinke se ne podudaraju" + }, + "newPassword": { + "title": "Nova lozinka", + "placeholder": "Unesite novu lozinku", + "confirm": { + "placeholder": "Ponovno unesite novu lozinku" + } + }, + "currentPassword": { + "title": "Trenutna lozinka", + "placeholder": "Unesite svoju trenutnu lozinku" + }, + "usernameIsRequired": "Potrebno je korisničko ime", + "passwordIsRequired": "Potrebna je lozinka" + }, + "createUser": { + "title": "Napravi novog korisnika", + "desc": "Dodajte novi korisnički račun i odredite ulogu za pristup područjima Frigate sučelja.", + "usernameOnlyInclude": "Korisničko ime može sadržavati samo slova, brojeve, . ili _", + "confirmPassword": "Molimo potvrdite svoju lozinku" + }, + "deleteUser": { + "title": "Izbriši korisnika", + "desc": "Ova radnja se ne može poništiti. To će trajno izbrisati korisnički račun i ukloniti sve povezane podatke.", + "warn": "Jeste li sigurni da želite izbrisati {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "Lozinka ne smije biti prazna", + "doNotMatch": "Lozinke se ne podudaraju", + "currentPasswordRequired": "Trenutna je lozinka potrebna", + "incorrectCurrentPassword": "Trenutna lozinka je netočna", + "passwordVerificationFailed": "Neuspješno potvrđivanje lozinke", + "updatePassword": "Ažuriraj lozinku za {{username}}", + "setPassword": "Postavi lozinku", + "desc": "Napravite jaku lozinku kako biste zaštitili ovaj račun.", + "multiDeviceWarning": "Svi ostali uređaji na kojima ste prijavljeni morat će se ponovno prijaviti unutar {{refresh_time}}.", + "multiDeviceAdmin": "Također možete prisiliti sve korisnike na trenutnu ponovnu autentifikaciju rotiranjem vaše JWT tajne." + }, + "changeRole": { + "title": "Promjena uloge korisnika", + "select": "Odaberite ulogu", + "desc": "Ažuriraj dopuštenja za {{username}}", + "roleInfo": { + "intro": "Odaberite odgovarajuću ulogu za ovog korisnika:", + "admin": "Admin", + "adminDesc": "Potpuni pristup svim značajkama.", + "viewer": "Gledatelj", + "viewerDesc": "Ograničeno samo na Nadzorne ploče uživo, Pregled, Istraži i Izvoz.", + "customDesc": "Prilagođena uloga s određenim pristupom kameri." + } + } + } + }, + "notification": { + "title": "Obavijesti", + "notificationSettings": { + "title": "Postavke obavijesti", + "desc": "Frigate može nativno slati push obavijesti na vaš uređaj kada je pokrenut u pregledniku ili instaliran kao PWA." + }, + "notificationUnavailable": { + "title": "Obavijesti nisu dostupne", + "desc": "Web push obavijesti zahtijevaju siguran kontekst (https://...). Ovo je ograničenje preglednika. Pristupite Frigateu sigurno kako biste koristili obavijesti." + }, + "globalSettings": { + "title": "Globalna okruženja", + "desc": "Privremeno obustavite obavijesti za određene kamere na svim registriranim uređajima." + }, + "email": { + "title": "E-pošta", + "placeholder": "npr. example@email.com", + "desc": "Potreban je valjani e-mail i koristit će se da vas obavijesti ako dođe do problema s push uslugom." + }, + "cameras": { + "title": "Kamere", + "noCameras": "Nema dostupnih kamera", + "desc": "Odaberite za koje kamere ćete uključiti obavijesti." + }, + "deviceSpecific": "Postavke specifične za uređaj", + "registerDevice": "Registrirajte ovaj uređaj", + "unregisterDevice": "Odjavite ovaj uređaj", + "sendTestNotification": "Pošaljite obavijest o testu", + "unsavedRegistrations": "Registracije nespremljenih obavijesti", + "unsavedChanges": "Promjene nespremljenih obavijesti", + "active": "Obavijesti su aktivne", + "suspended": "Obavijesti obustavljene {{time}}", + "suspendTime": { + "suspend": "Obustava", + "5minutes": "Pauziraj na 5 minuta", + "10minutes": "Pauziraj na 10 minuta", + "30minutes": "Pauziraj na 30 minuta", + "1hour": "Pauziraj na 1 sat", + "12hours": "Pauziraj na 12 sati", + "24hours": "Pauziraj na 24 sata", + "untilRestart": "Pauziraj do ponovnog pokretanja" + }, + "cancelSuspension": "Otkaži pauziranje obavijesti", + "toast": { + "success": { + "registered": "Uspješno registriran za obavijesti. Ponovno pokretanje Frigate-a je potrebno prije nego što se mogu poslati bilo kakve obavijesti (uključujući testnu obavijest).", + "settingSaved": "Postavke obavijesti su spremljene." + }, + "error": { + "registerFailed": "Neuspješno spremanje registracije obavijesti." + } + } + }, + "frigatePlus": { + "title": "Frigate+ postavke", + "apiKey": { + "title": "Frigate+ API ključ", + "validated": "Frigate+ API ključ je detektiran i validiran", + "notValidated": "Frigate+ API ključ nije detektiran ili nije validiran", + "desc": "Frigate+ API ključ omogućuje integraciju s Frigate+ uslugom.", + "plusLink": "Pročitajte više o Frigate+" + }, + "snapshotConfig": { + "title": "Konfiguracija snimaka", + "desc": "Slanje na Frigate+ zahtijeva da su u tvojoj konfiguraciji omogućeni i snapshotovi i clean_copy snapshotovi.", + "cleanCopyWarning": "Neke kamere imaju omogućene snimke, ali je čista kopija onemogućena. Morate omogućiti clean_copy u konfiguraciji za snimke da biste mogli slati slike s tih kamera na Frigate+.", + "table": { + "camera": "Kamera", + "snapshots": "Snimke", + "cleanCopySnapshots": "clean_copy Snimke" + } + }, + "modelInfo": { + "title": "Informacije o modelu", + "modelType": "Tip modela", + "trainDate": "Datum treniranja", + "baseModel": "Osnovni model", + "plusModelType": { + "baseModel": "Osnovni model", + "userModel": "Fino podešen" + }, + "supportedDetectors": "Podržani detektori", + "cameras": "Kamere", + "loading": "Učitavanje informacija o modelu…", + "error": "Neuspješno učitavanje informacije o modelu", + "availableModels": "Dostupni modeli", + "loadingAvailableModels": "Učitavanje dostupnih modela…", + "modelSelect": "Dostupne modele na Frigate+ možete odabrati ovdje. Imajte na umu da se mogu odabrati samo modeli kompatibilni s vašom trenutnom konfiguracijom detektora." + }, + "unsavedChanges": "Nespremljene promjene postavki Frigate+", + "restart_required": "Potreban je restart (promijenjen model Frigate+)", + "toast": { + "success": "Frigate+ postavke su spremljene. Ponovno pokrenite Frigate da primijenite promjene.", + "error": "Nije uspjelo spremiti promjene konfiguracije: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Okidači", + "semanticSearch": { + "title": "Semantičko pretraživanje je onemogućeno", + "desc": "Semantičko pretraživanje mora biti omogućeno za korištenje okidača." + }, + "management": { + "title": "Okidači", + "desc": "Upravljanje okidačima za {{camera}}. Koristite tip sličice za okidanje na sličnim sličicama kao odabrani praćeni objekt, a tip opisa za okidanje na sličnim opisima teksta koji ste odredili." + }, + "addTrigger": "Dodaj okidač", + "table": { + "name": "Ime", + "type": "Tip", + "content": "Sadržaj", + "threshold": "Prag", + "actions": "Radnje", + "noTriggers": "Nema konfiguriranih okidača za ovu kameru.", + "edit": "Uređivanje", + "deleteTrigger": "Okidač za brisanje", + "lastTriggered": "Zadnji put okinut" + }, + "type": { + "thumbnail": "Sličica", + "description": "Opis" + }, + "actions": { + "notification": "Pošalji obavijest", + "sub_label": "Dodaj podoznaku", + "attribute": "Dodaj atribut" + }, + "dialog": { + "createTrigger": { + "title": "Kreiraj okidač", + "desc": "Kreirajte okidač za kameru {{camera}}" + }, + "editTrigger": { + "title": "Uredi okidač", + "desc": "Uredi postavke za okidač na kameri {{camera}}" + }, + "deleteTrigger": { + "title": "Obriši okidač", + "desc": "Jeste li sigurni da želite izbrisati okidač {{triggerName}}? Ova radnja se ne može poništiti." + }, + "form": { + "name": { + "title": "Ime", + "placeholder": "Unesite ime okidača", + "description": "Unesite jedinstveno ime ili opis kako biste identificirali ovaj okidač", + "error": { + "minLength": "Polje mora imati najmanje 2 znaka.", + "invalidCharacters": "Polje može sadržavati samo slova, brojeve, podcrte i crtice.", + "alreadyExists": "Okidač s ovim imenom već postoji za ovu kameru." + } + }, + "enabled": { + "description": "Omogućite ili onemogućite ovaj okidač" + }, + "type": { + "title": "Tip", + "placeholder": "Odaberite tip okidača", + "description": "Okini kada se detektira sličan opis praćenog objekta", + "thumbnail": "Okini kada se detektira sličan sličica praćenog objekta" + }, + "content": { + "title": "Sadržaj", + "imagePlaceholder": "Odaberi sličicu", + "textPlaceholder": "Unesi tekstualni sadržaj", + "imageDesc": "Prikazuje se samo najnovijih 100 sličica. Ako ne možete pronaći željenu sličicu, molimo pregledajte ranije objekte u Explore i postavite okidač iz izbornika tamo.", + "textDesc": "Unesite tekst kako biste okinuli ovu radnju kada se detektira sličan opis praćenog objekta.", + "error": { + "required": "Sadržaj je obavezan." + } + }, + "threshold": { + "title": "Prag", + "desc": "Postavite prag sličnosti za ovaj okidač. Viši prag znači da je potrebno bliže podudaranje da bi se okidač aktivirao.", + "error": { + "min": "Prag mora biti najmanje 0", + "max": "Prag mora biti najviše 1" + } + }, + "actions": { + "title": "Radnje", + "desc": "Prema zadanim postavkama, Frigate šalje MQTT poruku za sve okidače. Podoznake dodaju ime okidača na oznaku objekta, a atributi su pretraživi metapodaci pohranjeni zasebno u metapodacima praćenih objekata.", + "error": { + "min": "Mora se odabrati barem jedna radnja." + } + } + } + }, + "wizard": { + "title": "Stvori okidač", + "step1": { + "description": "Konfigurirajte osnovne postavke za svoj okidač." + }, + "step2": { + "description": "Postavite sadržaj koji će okinuti ovu radnju." + }, + "step3": { + "description": "Konfigurirajte prag i akcije za ovaj okidač." + }, + "steps": { + "nameAndType": "Naziv i tip", + "configureData": "Konfiguriraj podatke", + "thresholdAndActions": "Prag i akcije" + } + }, + "toast": { + "success": { + "createTrigger": "Okidač {{name}} uspješno stvoren.", + "updateTrigger": "Okidač {{name}} je uspješno ažuriran.", + "deleteTrigger": "Okidač {{name}} uspješno izbrisan." + }, + "error": { + "createTriggerFailed": "Neuspješno stvaranje okidača: {{errorMessage}}", + "updateTriggerFailed": "Neuspješno ažuriranje okidača: {{errorMessage}}", + "deleteTriggerFailed": "Neuspješno brisanje okidača: {{errorMessage}}" + } + } + } +} diff --git a/web/public/locales/hr/views/system.json b/web/public/locales/hr/views/system.json new file mode 100644 index 0000000..880eca7 --- /dev/null +++ b/web/public/locales/hr/views/system.json @@ -0,0 +1,208 @@ +{ + "documentTitle": { + "cameras": "Statistika Kamera - Frigate", + "general": "Opća Statistika - Frigate", + "logs": { + "go2rtc": "Zapisnici Go2RTC - Frigate", + "nginx": "Zapisnici Nginx - Frigate", + "frigate": "Zapisnici Frigate - Frigate" + }, + "storage": "Statistika Pohrane - Frigate", + "enrichments": "Statistika Obogaćenja - Frigate" + }, + "title": "Sustav", + "logs": { + "download": { + "label": "Preuzmi Zapisnike" + }, + "type": { + "label": "Tip", + "timestamp": "Vremenska oznaka", + "tag": "Oznaka", + "message": "Poruka" + }, + "copy": { + "label": "Kopiraj u međuspremnik", + "success": "Zapisnici kopirani u međuspremnik", + "error": "Nije moguće kopirati zapisnike u međuspremnik" + }, + "tips": "Zapisnici se prenose sa servera", + "toast": { + "error": { + "fetchingLogsFailed": "Greška pri dohvaćanju zapisnika: {{errorMessage}}", + "whileStreamingLogs": "Greška tijekom prijenosa zapisnika: {{errorMessage}}" + } + } + }, + "metrics": "Metrike sustava", + "general": { + "title": "Općenito", + "detector": { + "title": "Detektori", + "inferenceSpeed": "Brzina inferencije detektora", + "temperature": "Temperatura detektora", + "cpuUsage": "Upotreba CPU-a detektora", + "cpuUsageInformation": "CPU korišten za pripremu ulaznih i izlaznih podataka za/od modela detekcije. Ova vrijednost ne mjeri korištenje za inferenciju, čak ni ako se koristi GPU ili akcelerator.", + "memoryUsage": "Upotreba memorije detektora" + }, + "hardwareInfo": { + "title": "Informacije o hardveru", + "gpuUsage": "Upotreba GPU-a", + "gpuMemory": "Memorija GPU-a", + "gpuEncoder": "GPU Encoder", + "gpuDecoder": "GPU Decoder", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo Izlaz", + "returnCode": "Povratni kod: {{code}}", + "processOutput": "Izlaz procesa:", + "processError": "Greška procesa:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI Izlaz", + "name": "Naziv: {{name}}", + "driver": "Driver: {{driver}}", + "cudaComputerCapability": "CUDA Compute Capability: {{cuda_compute}}", + "vbios": "VBios Informacije: {{vbios}}" + }, + "closeInfo": { + "label": "Zatvori informacije o GPU-u" + }, + "copyInfo": { + "label": "Kopiraj informacije o GPU-u" + }, + "toast": { + "success": "Informacije o GPU-u kopirane u međuspremnik" + } + }, + "npuUsage": "Upotreba NPU-a", + "npuMemory": "Memorija NPU-a", + "intelGpuWarning": { + "title": "Upozorenje Intel GPU Statistika", + "message": "Statistika GPU-a nije dostupna", + "description": "Ovo je poznata greška u Intelovim alatima za izvještavanje GPU statistike (intel_gpu_top) gdje će se podaci prekinuti i stalno prikazivati 0% korištenja GPU-a, čak i kada hardversko ubrzanje i detekcija objekata pravilno rade na (i)GPU-u. Ovo nije greška Frigate-a. Možete ponovno pokrenuti host da privremeno riješite problem i potvrdite da GPU radi ispravno. Ovo ne utječe na performanse." + } + }, + "otherProcesses": { + "title": "Ostali Procesi", + "processCpuUsage": "Upotreba CPU-a procesa", + "processMemoryUsage": "Upotreba memorije procesa", + "series": { + "go2rtc": "go2rtc", + "recording": "snimanje", + "review_segment": "pregled segmenta", + "embeddings": "ugrađivanja", + "audio_detector": "audio detektor" + } + } + }, + "storage": { + "title": "Pohrana", + "overview": "Pregled", + "recordings": { + "title": "Snimke", + "tips": "Ova vrijednost predstavlja ukupno korištenje prostora za snimke u Frigate bazi podataka. Frigate ne prati korištenje prostora za sve datoteke na disku.", + "earliestRecording": "Najranija dostupna snimka:" + }, + "shm": { + "title": "SHM (zajednička memorija) alokacija", + "warning": "Trenutna veličina SHM od {{total}} MB je premala. Povećajte je na najmanje {{min_shm}} MB." + }, + "cameraStorage": { + "title": "Pohrana Kamere", + "camera": "Kamera", + "unusedStorageInformation": "Informacije o neiskorištenom prostoru", + "storageUsed": "Pohrana", + "percentageOfTotalUsed": "Postotak od ukupnog", + "bandwidth": "Propusnost", + "unused": { + "title": "Neiskorišteno", + "tips": "Ova vrijednost možda ne prikazuje točno slobodan prostor dostupan Frigate-u ako imate druge datoteke pohranjene na disku osim Frigate snimaka. Frigate ne prati korištenje prostora izvan svojih snimki." + } + } + }, + "cameras": { + "title": "Kamere", + "overview": "Pregled", + "info": { + "aspectRatio": "Omjer stranica", + "cameraProbeInfo": "{{camera}} Informacije ispitane od kamere", + "streamDataFromFFPROBE": "Podaci emitiranja dohvaćeni su pomoću ffprobe.", + "fetching": "Dohvaćanje podataka kamere", + "stream": "Emitiranje {{idx}}", + "video": "Video:", + "codec": "Kodek:", + "resolution": "Rezolucija:", + "fps": "FPS:", + "unknown": "Nepoznato", + "audio": "Audio:", + "error": "Greška: {{error}}", + "tips": { + "title": "Informacije Ispitivanja Kamere" + } + }, + "framesAndDetections": "Okviri / Detekcije", + "label": { + "camera": "kamera", + "detect": "detekcija", + "skipped": "preskočeno", + "ffmpeg": "FFmpeg", + "capture": "snimanje", + "overallFramesPerSecond": "ukupni okviri po sekundi", + "overallDetectionsPerSecond": "ukupne detekcije po sekundi", + "overallSkippedDetectionsPerSecond": "ukupne preskočene detekcije po sekundi", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} snimanje", + "cameraDetect": "{{camName}} detekcija", + "cameraFramesPerSecond": "{{camName}} okviri po sekundi", + "cameraDetectionsPerSecond": "{{camName}} detekcije po sekundi", + "cameraSkippedDetectionsPerSecond": "{{camName}} preskočene detekcije po sekundi" + }, + "toast": { + "success": { + "copyToClipboard": "Podaci probe kopirani u međuspremnik." + }, + "error": { + "unableToProbeCamera": "Nije moguće probati kameru: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Zadnje osvježavanje: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} ima visoku upotrebu CPU-a za FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} ima visoku upotrebu CPU-a za detekciju ({{detectAvg}}%)", + "healthy": "Sustav je zdrav", + "reindexingEmbeddings": "Reindeksiranje embeddings ({{processed}}% završeno)", + "cameraIsOffline": "{{camera}} je offline", + "detectIsSlow": "{{detect}} je sporo ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} je vrlo sporo ({{speed}} ms)", + "shmTooLow": "/dev/shm alokacija ({{total}} MB) treba biti povećana na najmanje {{min}} MB." + }, + "enrichments": { + "title": "Obogaćenja", + "infPerSecond": "Inferencija po sekundi", + "averageInf": "Prosječno vrijeme inferencije", + "embeddings": { + "image_embedding": "Ugrađivanja slike", + "text_embedding": "Ugrađivanja teksta", + "face_recognition": "Prepoznavanje lica", + "plate_recognition": "Prepoznavanje registarskih pločica", + "image_embedding_speed": "Brzina Image Embedding", + "face_embedding_speed": "Brzina Face Embedding", + "face_recognition_speed": "Brzina prepoznavanja lica", + "plate_recognition_speed": "Brzina prepoznavanja registarskih pločica", + "text_embedding_speed": "Brzina Text Embedding", + "yolov9_plate_detection_speed": "Brzina YOLOv9 prepoznavanja pločica", + "yolov9_plate_detection": "YOLOv9 Prepoznavanje pločica", + "review_description": "Opis Pregleda", + "review_description_speed": "Brzina Opisa Pregleda", + "review_description_events_per_second": "Opis Pregleda", + "object_description": "Opis Objekta", + "object_description_speed": "Brzina Opisa Objekta", + "object_description_events_per_second": "Opis Objekta", + "classification": "{{name}} Klasifikacija", + "classification_speed": "Brzina {{name}} Klasifikacije", + "classification_events_per_second": "{{name}} Klasifikacija po događajima po sekundi" + } + } +} diff --git a/web/public/locales/hu/audio.json b/web/public/locales/hu/audio.json new file mode 100644 index 0000000..c16ebfd --- /dev/null +++ b/web/public/locales/hu/audio.json @@ -0,0 +1,437 @@ +{ + "speech": "Beszéd", + "wild_animals": "Vadállatok", + "synthetic_singing": "Szintetikus éneklés", + "cheering": "Éljenzés", + "chatter": "Csevegés", + "bird": "Madár", + "babbling": "Gügyögés", + "yell": "Kiabálás", + "whispering": "Suttogás", + "laughter": "Nevetés", + "bellow": "Ordítás", + "whoop": "Ujjongás", + "snicker": "Kuncogás", + "crying": "Sírás", + "sigh": "Sóhajtás", + "singing": "Éneklés", + "choir": "Kórus", + "yodeling": "Jódlizás", + "chant": "Dalolás", + "mantra": "Mantrázás", + "child_singing": "Gyermek éneklés", + "rapping": "Rappelés", + "groan": "Nyögés", + "grunt": "Morgás", + "whistling": "Fütyülés", + "breathing": "Légzés", + "wheeze": "Zihálás", + "snoring": "Horkolás", + "gasp": "Lihegés", + "pant": "Lihegés", + "snort": "Horkantás", + "cough": "Köhögés", + "sneeze": "Tüsszentés", + "sniff": "Szimatolás", + "run": "Futás", + "shuffle": "Csoszogás", + "footsteps": "Lépés", + "chewing": "Rágás", + "biting": "Maró", + "gargling": "Gargalizálás", + "burping": "Böfögés", + "hiccup": "Csuklás", + "fart": "Szellentés", + "hands": "Kezek", + "finger_snapping": "Csettintés", + "clapping": "Tapsolás", + "heartbeat": "Szívdobbanás", + "heart_murmur": "Szívzörej", + "applause": "Taps", + "crowd": "Tömeg", + "children_playing": "Gyerekek játszanak", + "pets": "Háziállatok", + "dog": "Kutya", + "bark": "Ugatás", + "yip": "Csaholás", + "howl": "Bömbölés", + "bow_wow": "Vau vau", + "growling": "Morgás", + "whimper_dog": "Kutya nyüszítés", + "cat": "Macska", + "purr": "Dorombolás", + "meow": "Nyávogás", + "hiss": "Sziszegés", + "caterwaul": "Nyivákolás", + "livestock": "Állatállomány", + "horse": "Ló", + "clip_clop": "Lódobogás", + "neigh": "Nyerítés", + "cattle": "Marha", + "moo": "Tehénbőgés", + "cowbell": "Kolomp", + "pig": "Disznó", + "oink": "Disznó röfögés", + "goat": "Kecske", + "bleat": "Bégetés", + "sheep": "Juh", + "fowl": "Szárnyas", + "chicken": "Csirke", + "cluck": "Kotkodácsolás", + "cock_a_doodle_doo": "Kukorékolás", + "turkey": "Pulyka", + "gobble": "Kurrogás", + "duck": "Kacsa", + "quack": "Hápogás", + "honk": "Gágogás", + "roaring_cats": "Ordító macskák", + "roar": "Üvöltés", + "chirp": "Csiripelés", + "squawk": "Rikácsolás", + "pigeon": "Galamb", + "coo": "Turbékolás", + "crow": "Varjú", + "caw": "Károgás", + "owl": "Bagoly", + "hoot": "Huhogás", + "flapping_wings": "Csapkodó szárnyak", + "dogs": "Kutyák", + "rats": "Patkányok", + "mouse": "Egér", + "patter": "Kopog", + "insect": "Rovar", + "cricket": "Tücsök", + "mosquito": "Szúnyog", + "fly": "Légy", + "buzz": "Zümmögés", + "frog": "Béka", + "croak": "Brekegés", + "snake": "Kígyó", + "rattle": "Csörgés", + "whale_vocalization": "Bálna bőgés", + "music": "Zene", + "musical_instrument": "Hangszer", + "plucked_string_instrument": "Pengetős hangszer", + "guitar": "Gitár", + "electric_guitar": "Elektromos gitár", + "bass_guitar": "Basszusgitár", + "acoustic_guitar": "Akusztikus gitár", + "steel_guitar": "Acél gitár", + "tapping": "Tappelés", + "strum": "Zongora verés", + "mandolin": "Mandolin", + "banjo": "Bendzsó", + "zither": "Citera", + "sitar": "Szitár", + "ukulele": "Ukulele", + "keyboard": "Szintetizátor", + "piano": "Zongora", + "electric_piano": "Elektromos zongora", + "organ": "Orgona", + "hammond_organ": "Hammond orgona", + "electronic_organ": "Elektromos orgona", + "synthesizer": "Szintetizátor", + "sampler": "Sampler", + "harpsichord": "Csembaló", + "percussion": "Ütős hangszer", + "drum_kit": "Dobszerkó", + "drum_machine": "Dobgép", + "drum": "Dob", + "snare_drum": "Pergődob", + "rimshot": "Peremütés", + "drum_roll": "Dobpergés", + "timpani": "Üstdob", + "bass_drum": "Basszusdob", + "tabla": "Tablá", + "cymbal": "Cintányér", + "hi_hat": "Lábcin", + "boat": "Hajó", + "car": "Autó", + "bus": "Busz", + "motorcycle": "Motor", + "train": "Betanít", + "bicycle": "Bicikli", + "scream": "Sikoly", + "throat_clearing": "Torokköszörülés", + "stomach_rumble": "Gyomorkorgás", + "animal": "Állat", + "goose": "Liba", + "humming": "Zümmögés", + "skateboard": "Gördeszka", + "door": "Ajtó", + "camera": "Kamera", + "ship": "Hajó", + "car_alarm": "Autó riasztó", + "truck": "Teherautó", + "race_car": "Versenyautó", + "ambulance": "Mentő", + "police_car": "Rendőrautó", + "ice_cream_truck": "Jégkrémes autó", + "traffic_noise": "Forgalom zaja", + "helicopter": "Helikopter", + "propeller": "Propeller", + "subway": "Metró", + "chainsaw": "Láncfűrész", + "lawn_mower": "Fűnyíró", + "dental_drill's_drill": "Fogászati Fúró", + "engine": "Motor", + "doorbell": "Csengő", + "accelerating": "Gyorsítás", + "engine_starting": "Motor indítás", + "water_tap": "Csapvíz", + "blender": "Pépesítő gép", + "microwave_oven": "Mikrohullámú sütő", + "cutlery": "Evőeszköz", + "dishes": "Edények", + "alarm": "Vészjelző", + "writing": "Írás", + "computer_keyboard": "Billentyűzet", + "typewriter": "Írógép", + "typing": "Gépelés", + "electric_shaver": "Villanyborotva", + "scissors": "Olló", + "coin": "Érme", + "vacuum_cleaner": "Porszívó", + "electric_toothbrush": "Elektromos fogkefe", + "toothbrush": "Fogkefe", + "toilet_flush": "WC lehúzás", + "hair_dryer": "Hajszárító", + "bathtub": "Fürdőkád", + "sink": "Mosdókagyló", + "ringtone": "Csengőhang", + "telephone_bell_ringing": "Telefon csörgés", + "telephone": "Telefon", + "siren": "Sziréna", + "alarm_clock": "Ébresztőóra", + "busy_signal": "Foglalt jelzés", + "dial_tone": "Hívás hang", + "clock": "Óra", + "fire_alarm": "Tűzjelző", + "smoke_detector": "Füst érzékelő", + "hammer": "Kalapács", + "printer": "Nyomtató", + "cash_register": "Pénztárgép", + "air_conditioning": "Légkondícionálás", + "sawing": "Fűrészelés", + "machine_gun": "Gépfegyver", + "gunshot": "Pisztoly lövés", + "explosion": "Robbanás", + "drill": "Fúrás", + "glass": "Üveg", + "wood": "Fa", + "fireworks": "Tűzijáték", + "white_noise": "Fehér zaj", + "static": "Statikus", + "environmental_noise": "Környezeti Zaj", + "sound_effect": "Hang effekt", + "silence": "Csend", + "radio": "Rádió", + "television": "Televízió", + "pink_noise": "Rózsaszín zaj", + "gong": "Gong", + "saxophone": "Szaxofon", + "cello": "Cselló", + "violin": "Hegedű", + "harmonica": "Harmónika", + "wind_chime": "Szélcsengő", + "bicycle_bell": "Bicikli Csengő", + "church_bell": "Templomi Harang", + "bell": "Harang", + "didgeridoo": "Didzseridú", + "heavy_metal": "Heavy Metál", + "rock_music": "Rock Zene", + "beatboxing": "Beatboxolás", + "hip_hop_music": "Hip-Hop Zene", + "pop_music": "Pop Zene", + "soul_music": "Soul Zene", + "rhythm_and_blues": "Ritmus és Blues", + "psychedelic_rock": "Pszichedelikus Rock", + "rock_and_roll": "Rock and Roll", + "folk_music": "Népzene", + "funk": "Funky", + "classical_music": "Klasszikus Zene", + "disco": "Diszkó", + "jazz": "Jazz", + "middle_eastern_music": "Közép-Keleti Zene", + "drum_and_bass": "Drum and Bass", + "dubstep": "Dubstep", + "techno": "Techno", + "opera": "Opera", + "music_of_latin_america": "Latin-amerikai Zene", + "trance_music": "Trance Zene", + "a_capella": "A Capella", + "music_for_children": "Zene Gyerekeknek", + "blues": "Blues", + "flamenco": "Flamenco", + "music_of_bollywood": "Bollywood-i Zene", + "music_of_asia": "Ázsiai Zene", + "gospel_music": "Gospel Zene", + "christian_music": "Keresztény Zene", + "music_of_africa": "Afrikai Zene", + "background_music": "Háttérzene", + "song": "Dal", + "independent_music": "Független Zene", + "ska": "Ska", + "happy_music": "Boldog Zene", + "wedding_music": "Házassági Zene", + "dance_music": "Tánc Zene", + "christmas_music": "Karácsonyi Zene", + "video_game_music": "Videojáték Zene", + "water": "Víz", + "thunder": "Villám", + "wind_noise": "Szél Zaj", + "wind": "Szél", + "angry_music": "Mérges Zene", + "sad_music": "Szomorú Zene", + "waves": "Hullámok", + "ocean": "Óceán", + "waterfall": "Vízesés", + "stream": "Adás", + "rain_on_surface": "Eső a Felületen", + "raindrop": "Esőcsepp", + "rain": "Eső", + "vehicle": "Jármű", + "fire": "Tűz", + "motorboat": "Motorcsónak", + "car_passing_by": "Áthaladó autó", + "house_music": "House Zene", + "electronic_music": "Elektronikus Zene", + "salsa_music": "Salsa Zene", + "progressive_rock": "Progresszív Rock", + "grunge": "Grunge", + "swing_music": "Swing Zene", + "country": "Ország", + "reggae": "Reggae", + "punk_rock": "Punk Rock", + "clarinet": "Klarinét", + "wood_block": "Fa hasáb", + "tambourine": "Csörgődob", + "maraca": "Maraca", + "tubular_bells": "Csőharang", + "mallet_percussion": "Kalapácsos ütőhangszer", + "marimba": "Marimba", + "glockenspiel": "Harangjáték", + "vibraphone": "Vibrafon", + "steelpan": "Stíldob", + "orchestra": "Zenekar", + "brass_instrument": "Rézfúvós Hangszer", + "french_horn": "Francia Kürt", + "trumpet": "Trombita", + "trombone": "Harsona", + "bowed_string_instrument": "Vonós Hangszer", + "string_section": "Vonós szekció", + "pizzicato": "Pizzicato", + "double_bass": "Nagybőgő", + "wind_instrument": "Fúvós Hangszer", + "flute": "Fuvola", + "harp": "Hárfa", + "jingle_bell": "Csengő", + "tuning_fork": "Hangvilla", + "chime": "Harangjáték", + "accordion": "Harmónika", + "bagpipes": "Skótduda", + "theremin": "Teremin", + "singing_bowl": "Éneklőtál", + "scratching": "Scratchelés", + "bluegrass": "Bluegrass", + "electronica": "Elektronika", + "electronic_dance_music": "Elektronikus Tánczene", + "ambient_music": "Ambient Zene", + "new-age_music": "New Age Zene", + "vocal_music": "Ének Zene", + "afrobeat": "Afrobeat", + "carnatic_music": "Carnatic Zene", + "traditional_music": "Népzene", + "theme_music": "Filmzene", + "jingle": "Reklámzene", + "soundtrack_music": "Zeneszám", + "lullaby": "Altatódal", + "tender_music": "Tender Zene", + "exciting_music": "Izgalmas Zene", + "scary_music": "Ijesztő Zene", + "rustling_leaves": "Susogó Levelek", + "thunderstorm": "Zivatar", + "steam": "Gőz", + "gurgling": "Gurgulázás", + "crackle": "Sercegés", + "sailboat": "Vitorláshajó", + "rowboat": "Csónak", + "motor_vehicle": "Motorkerékpár", + "toot": "Fújás", + "power_windows": "Elektromos ablakok", + "skidding": "csúszás", + "tire_squeal": "Kerékcsikorgás", + "air_brake": "Légfék", + "air_horn": "Légkürt", + "reversing_beeps": "Tolató csipogás", + "emergency_vehicle": "Sürgősségi jármű", + "fire_engine": "Tűzoltóautó", + "rail_transport": "Vasúti közlekedés", + "train_whistle": "Vonatsíp", + "train_horn": "Vonatkürt", + "railroad_car": "Vasúti kocsi", + "train_wheels_squealing": "Vonatkerék csikorgás", + "aircraft": "Repülő", + "aircraft_engine": "Repülő motor", + "jet_engine": "Sugárhajtású motor", + "fixed-wing_aircraft": "Szárnyas repülőgép", + "light_engine": "Kis motor", + "medium_engine": "Közepes motor", + "heavy_engine": "Nagy motor", + "engine_knocking": "Kopogó motor", + "idling": "Alapjárat", + "ding-dong": "Ding-Dong", + "sliding_door": "Tolóajtó", + "slam": "Csapódás", + "knock": "Kopogás", + "tap": "Finom kopogás", + "squeak": "Csipogás", + "cupboard_open_or_close": "Szekrény nyitás, vagy zárás", + "drawer_open_or_close": "Fiók nyitás vagy zárás", + "chopping": "Vágás", + "frying": "Sütés", + "zipper": "Cipzár", + "keys_jangling": "Kulcsok csörgése", + "shuffling_cards": "Kártyakeverés", + "telephone_dialing": "Telefonos tárcsázás", + "civil_defense_siren": "Rendvédelmi sziréna", + "buzzer": "Búgó eszköz", + "foghorn": "Ködkürt", + "whistle": "Síp", + "steam_whistle": "Gőzsíp", + "mechanisms": "Mechanikus alkatrészek", + "ratchet": "Racsni", + "tick": "Bolha", + "tick-tock": "Ketyegés", + "gears": "Fogaskerekek", + "pulleys": "emelő csigák", + "sewing_machine": "Varrógép", + "mechanical_fan": "Ventillátor", + "single-lens_reflex_camera": "Egylencsés reflexkamera", + "tools": "Eszközök", + "jackhammer": "Bontókalapács", + "filing": "Iratrendezés", + "sanding": "Csiszolás", + "power_tool": "Munkagép", + "fusillade": "Sortűz", + "artillery_fire": "Tüzérségi tűz", + "cap_gun": "Kupakos pisztoly", + "firecracker": "Petárda", + "burst": "Kitörés (Burst)", + "eruption": "Kitörés (Eruption)", + "boom": "Bumm", + "chop": "Vágás", + "splinter": "Repedés (fa)", + "crack": "Törés", + "chink": "Csörömpölés", + "shatter": "Összetörés", + "field_recording": "Helyszíni felvétel", + "noise": "Zaj", + "liquid": "Folyadék", + "splash": "Csobbanás", + "slosh": "Löttyenés", + "trickle": "Csörgedezés", + "gush": "Folyás", + "stir": "Kavarás", + "thump": "Puffanás" +} diff --git a/web/public/locales/hu/common.json b/web/public/locales/hu/common.json new file mode 100644 index 0000000..fdebbf0 --- /dev/null +++ b/web/public/locales/hu/common.json @@ -0,0 +1,302 @@ +{ + "time": { + "untilForTime": "{{time}} ideig", + "s": "{{time}} másodperc", + "untilForRestart": "Amíg a Frigate újraindul.", + "untilRestart": "Amíg újraindul", + "justNow": "Most", + "ago": "Ennyi ideje: {{timeAgo}}", + "today": "Ma", + "yesterday": "Tegnap", + "last7": "Elmúlt 7 nap", + "last14": "Elmúlt 14 nap", + "last30": "Elmúlt 30 nap", + "thisWeek": "Ezen a héten", + "lastWeek": "Előző héten", + "thisMonth": "Ebben a hónapban", + "lastMonth": "Előző hónapban", + "5minutes": "5 perc", + "10minutes": "10 perc", + "30minutes": "30 perc", + "1hour": "1 óra", + "12hours": "12 óra", + "24hours": "24 óra", + "pm": "du", + "am": "de", + "yr": "{{time}} év", + "mo": "{{time}} hónap", + "d": "{{time}} nap", + "m": "{{time}} perc", + "hour_one": "{{time}} óra", + "hour_other": "{{time}} órák", + "h": "{{time}} óra", + "minute_one": "{{time}} perc", + "minute_other": "{{time}} percek", + "second_one": "{{time}} másodperc", + "second_other": "{{time}} másodpercek", + "year_one": "{{time}} év", + "year_other": "{{time}} évek", + "month_one": "{{time}} hónap", + "month_other": "{{time}} hónapok", + "day_one": "{{time}} nap", + "day_other": "{{time}} napok", + "formattedTimestamp": { + "24hour": "MMM d, HH:mm:ss", + "12hour": "MMM d, h:mm:ss aaa" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampHourMinute": { + "24hour": "HH:mm", + "12hour": "h:mm aaa" + }, + "formattedTimestamp2": { + "24hour": "d MMM HH:mm:ss", + "12hour": "MM/dd h:mm:ssa" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "h:mm:ss aaa" + }, + "formattedTimestampMonthDayYearHourMinute": { + "24hour": "MMM d yyyy, HH:mm", + "12hour": "MMM d yyyy, h:mm aaa" + }, + "formattedTimestampFilename": { + "24hour": "yy-MM-dd-HH-mm-ss", + "12hour": "yy-MM-dd-h-mm-ss-a" + }, + "formattedTimestampMonthDayHourMinute": { + "24hour": "MMM d, HH:mm", + "12hour": "MMM d, h:mm aaa" + }, + "formattedTimestampMonthDay": "MMM d", + "inProgress": "Folyamatban", + "invalidStartTime": "Érvénytelen kezdeti idő", + "never": "Soha", + "invalidEndTime": "Érvénytelen befejezési idő" + }, + "menu": { + "darkMode": { + "label": "Sötét Mód", + "withSystem": { + "label": "Használd a rendszerbeállításokat a világos vagy sötét mód megheghatározásához" + }, + "dark": "Sötét", + "light": "Világos" + }, + "withSystem": "Rendszer", + "theme": { + "default": "Alap", + "nord": "Nord", + "red": "Piros", + "highcontrast": "Nagy Kontrasztarány", + "label": "Téma", + "blue": "Kék", + "green": "Zöld" + }, + "documentation": { + "title": "Dokumentáció", + "label": "Frigate dokumentáció" + }, + "help": "Segítség", + "explore": "Felfedezés", + "user": { + "logout": "Kijelentkezés", + "title": "Felhasználó", + "account": "Fiók", + "current": "Jelenlegi Felhasználó: {{user}}", + "anonymous": "anoním", + "setPassword": "Jelszó Beállítása" + }, + "export": "Exportálás", + "language": { + "ca": "Katalán", + "withSystem": { + "label": "Használd a rendszerbeállításoknál megadott nyelvet" + }, + "sk": "Szlovák", + "da": "Dán", + "yue": "Kantoni", + "hi": "Hindi", + "vi": "Vietnámi", + "fa": "Perzsa", + "pl": "Lengyel", + "en": "Angol", + "es": "Spanyol", + "uk": "Ukrán", + "ja": "Japán", + "tr": "Török", + "it": "Olasz", + "nl": "Holland", + "sv": "Svéd", + "he": "Héber", + "el": "Görög", + "fr": "Francia", + "nb": "Norvég", + "cs": "Cseh", + "ko": "Koreai", + "zhCN": "Egyszerűsített kínai", + "ar": "Arab", + "pt": "Portugál", + "ru": "Orosz", + "de": "Német", + "ro": "Román", + "hu": "Magyar", + "fi": "Finn", + "th": "Thai", + "ptBR": "Português brasileiro (Brazil portugál)", + "sr": "Српски (Szerb)", + "sl": "Slovenščina (Szlovén)", + "lt": "Lietuvių (Litván)", + "bg": "Български (Bolgár)", + "gl": "Galego (Galíciai)", + "id": "Bahasa Indonesia (Indonéz)", + "ur": "اردو (Urdu)", + "hr": "Horvát" + }, + "uiPlayground": "UI játszótér", + "faceLibrary": "Arc Könyvtár", + "restart": "Frigate Újraindítása", + "live": { + "title": "Élő", + "allCameras": "Minden Kamera", + "cameras": { + "title": "Kamerák", + "count_one": "{{count}} Kamera", + "count_other": "{{count}} Kamerák" + } + }, + "review": "Áttekintés", + "appearance": "Megjelenés", + "languages": "Nyelvek", + "configurationEditor": "Konfiguráció Kezelő", + "systemMetrics": "Rendszer metrikák", + "system": "Rendszer", + "configuration": "Konfiguráció", + "systemLogs": "Rendszer naplók", + "settings": "Beállítások", + "classification": "Osztályozás", + "profiles": "Profilok" + }, + "role": { + "viewer": "Néző", + "title": "Szerepkör", + "admin": "Adminisztrátor", + "desc": "Az adminisztrátoroknak teljes hozzáférése van az összes feature-höz. A nézők csak a kamerákat láthatják, áttekinthetik az elemeket és az előzményeket a UI-on." + }, + "pagination": { + "next": { + "label": "Következő oldal", + "title": "Következő" + }, + "more": "Több oldal", + "previous": { + "label": "Vissza az előző oldalra", + "title": "Előző" + }, + "label": "lapozás" + }, + "accessDenied": { + "documentTitle": "Belépés Megtagadva - Frigate", + "title": "Belépés Megtagadva", + "desc": "Nincs jogolultsága ehhez az oldalhoz." + }, + "notFound": { + "title": "404", + "desc": "Oldal nem található", + "documentTitle": "Nem található - Frigate" + }, + "toast": { + "copyUrlToClipboard": "URL kimásolva a vágólapra.", + "save": { + "title": "Mentés", + "error": { + "title": "Sikertelen konfiguráció mentés: {{errorMessage}}", + "noMessage": "Konfigurációs változtatások mentése sikertelen" + } + } + }, + "selectItem": "Kiválasztani {{item}}-et", + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "láb", + "meters": "méter" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/óra", + "mbph": "MB/óra", + "gbph": "GB/óra" + } + }, + "button": { + "save": "Mentés", + "saving": "Mentés…", + "disable": "Kikapcsol", + "close": "Bezár", + "copy": "Másolás", + "back": "Vissza", + "apply": "Alkalmazás", + "done": "Kész", + "reset": "Visszaállítás", + "enabled": "Engedélyezve", + "enable": "Engedélyez", + "disabled": "Kikapcsolva", + "cancel": "Mégsem", + "fullscreen": "Teljes képernyő", + "history": "Előzmények", + "exitFullscreen": "Kilépés a Teljes Képernyőből", + "no": "Nem", + "download": "Letöltés", + "off": "KI", + "info": "Infó", + "twoWayTalk": "Kétirányú kommunikáció", + "pictureInPicture": "Kép a Képben", + "cameraAudio": "Kamera Hang", + "on": "BE", + "edit": "Módosít", + "copyCoordinates": "Koordináták másolása", + "delete": "Törlés", + "yes": "Igen", + "unsuspended": "Bekapcsol", + "suspended": "Felfüggesztve", + "play": "Lejátszás", + "unselect": "Kijelölés megszüntetése", + "export": "Exportálás", + "deleteNow": "Törlés Most", + "next": "Következő", + "continue": "Tovább", + "add": "Hozzáad" + }, + "label": { + "back": "Vissza", + "all": "Mind", + "hide": "Elrejt {{item}}", + "show": "Mutat {{item}}", + "ID": "ID", + "none": "Nincs", + "other": "Egyéb" + }, + "readTheDocumentation": "Olvassa el a dokumentációt", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} és {{1}}", + "many": "{{items}}, és {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opcionális", + "internalID": "A belső ID, amelyet a Frigate használ a konfigurációban és az adatbázisban" + } +} diff --git a/web/public/locales/hu/components/auth.json b/web/public/locales/hu/components/auth.json new file mode 100644 index 0000000..43b8e9e --- /dev/null +++ b/web/public/locales/hu/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Felhasználói név", + "password": "Jelszó", + "login": "Bejelentkezés", + "errors": { + "usernameRequired": "Felhasználónév szükséges", + "passwordRequired": "Jelszó szükséges", + "loginFailed": "Sikertelen bejelentkezés", + "unknownError": "Ismeretlen hiba. Ellenőrizze a naplókat.", + "webUnknownError": "Ismeretlen hiba. Ellenőrizze a konzol naplókat.", + "rateLimit": "Túl sokszor próbálkozott. Próbálja meg később." + }, + "firstTimeLogin": "Először próbálsz bejelentkezni? A hitelesítési adatok a Frigate naplóiban vannak feltüntetve." + } +} diff --git a/web/public/locales/hu/components/camera.json b/web/public/locales/hu/components/camera.json new file mode 100644 index 0000000..c529481 --- /dev/null +++ b/web/public/locales/hu/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Kamera Csoportok", + "delete": { + "confirm": { + "desc": "Biztosan törölni akarja a következő kamera csoportot {{name}}?", + "title": "Törlés megerősítése" + }, + "label": "Kamera csoport törlése" + }, + "add": "Kamera csoport hozzáadása", + "edit": "Kamera csoport módosítása", + "name": { + "label": "Név", + "placeholder": "Adjon meg egy nevet…", + "errorMessage": { + "mustLeastCharacters": "A kamera csoport nevének legalább 2 karakterből kell állnia.", + "exists": "Ez a kamera csoport név már létezik.", + "nameMustNotPeriod": "A kamera csoport neve nem tartalmazhat idő intervallumot.", + "invalid": "Hibás kamera csoport név." + } + }, + "cameras": { + "label": "Kamerák", + "desc": "Válassza ki a kamerákat ehhez a csoporthoz." + }, + "icon": "Ikon", + "success": "Kamera csoport {{name}} mentve.", + "camera": { + "setting": { + "audioIsAvailable": "Hang elérhető ehhez az adáshoz", + "audioIsUnavailable": "Nem elérhető hang ehhez az adáshoz", + "audio": { + "tips": { + "document": "Olvassa el a leírást ", + "title": "A hangnak a kamerából kell jönnie és be kell állítva legyen a go2rtc-ben ehhez az adáshoz." + } + }, + "title": "{{cameraName}} Adás Beállításai", + "label": "Kamera adás beállítások", + "stream": "Adás", + "streamMethod": { + "method": { + "noStreaming": { + "label": "Nincs Adás", + "desc": "A kameraképek percenként frissülnek és nem lesz élő adás." + }, + "smartStreaming": { + "label": "Okos Adás (ajánlott)", + "desc": "Az intelligens közvetítés percenként egyszer frissíti a kameraképet, ha nem észlelhető aktivitás, így takarékoskodik a sávszélességgel és az erőforrásokkal. Amikor aktivitást észlel, a kép zökkenőmentesen átvált élő közvetítésre." + }, + "continuousStreaming": { + "label": "Folyamatos Adás", + "desc": { + "title": "A kamerakép mindig élő közvetítés lesz, amikor látható az irányítópulton, még akkor is, ha nincs észlelt aktivitás.", + "warning": "A folyamatos közvetítés magas sávszélesség-használatot és teljesítményproblémákat okozhat. Csak óvatosan használja." + } + } + }, + "label": "Adási Mód", + "placeholder": "Válasszon egy adási módot" + }, + "placeholder": "Válasszon adást", + "compatibilityMode": { + "label": "Kompatibilitás mód", + "desc": "Csak akkor engedélyezze ezt az opciót, ha a kamera élő közvetítése képhibás, és a kép jobb oldalán átlós vonal látható." + }, + "desc": "Változtassa meg az élő adás beállításait ezen kamera csoport kijelzőjén. Ezek a beállítások eszköz/böngésző-specifikusak." + }, + "birdseye": "Madártávlat" + } + }, + "debug": { + "options": { + "label": "Beállítások", + "title": "Lehetőségek", + "showOptions": "Mutasd a Lehetőségeket", + "hideOptions": "Lehetőségek Elrejtése" + }, + "timestamp": "Időbélyeg", + "zones": "Zónák", + "mask": "Maszk", + "motion": "Mozgás", + "regions": "Régiók", + "boundingBox": "Doboz" + } +} diff --git a/web/public/locales/hu/components/dialog.json b/web/public/locales/hu/components/dialog.json new file mode 100644 index 0000000..90acb43 --- /dev/null +++ b/web/public/locales/hu/components/dialog.json @@ -0,0 +1,123 @@ +{ + "restart": { + "title": "Biztosan újra szeretnéd indítani a Frigate-et?", + "button": "Újraindítás", + "restarting": { + "title": "A Frigate újraindul", + "content": "Az oldal újratölt {{countdown}} másodperc múlva.", + "button": "Erőltetett újraindítás azonnal" + }, + "description": "Ez rövid időre leállítja a Frigate programot, amíg újraindul." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Küldés a Frigate+-nak", + "desc": "Objektumok, amelyek olyan helyeken vannak, ahol nem akarod, hogy felismerésre kerüljenek, nem számítanak téves egyezésnek. Ha téves egyezésként küldöd be őket, az összezavarja a modellt." + }, + "review": { + "state": { + "submitted": "Elküldve" + }, + "question": { + "ask_a": "Ez a tárgy egy {{label}}?", + "label": "Erősítse meg ezt a cimkét a Frigate plus felé", + "ask_an": "Ez a tárgy egy {{label}}?", + "ask_full": "Ez a tárgy egy {{translatedLabel}} ({{untranslatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Megtekintés az Előzmények között" + } + }, + "export": { + "time": { + "fromTimeline": "Kiválasztás az Idővonalból", + "start": { + "title": "Kezdő időpont", + "label": "Kezdeti Időpont Kiválasztása" + }, + "end": { + "title": "Végső időpont", + "label": "Végső Időpont Kiválasztása" + }, + "lastHour_one": "Előző óra", + "lastHour_other": "Utolsó {{count}} óra", + "custom": "Egyéb" + }, + "name": { + "placeholder": "Export Elnevezése" + }, + "select": "Kiválaszt", + "export": "Exportálás", + "selectOrExport": "Kiválasztás vagy Exportálás", + "toast": { + "success": "Exportálás sikeresen megkezdődött. Tekintse meg a fájl-t a /exports mappában.", + "error": { + "failed": "Nem sikerült elkezdeni az exportálást: {{error}}", + "endTimeMustAfterStartTime": "A végső időpontnak a kezdeti időpont után kell következnie", + "noVaildTimeSelected": "Nincs érvényes idő intervallum kiválasztva" + }, + "view": "Megtekint" + }, + "fromTimeline": { + "saveExport": "Exportálás mentése", + "previewExport": "Exportálás Előnézet" + } + }, + "streaming": { + "label": "Adás", + "restreaming": { + "desc": { + "readTheDocumentation": "Olvassa el a dokumentációt", + "title": "Állítsa be a go2rtc-t további élő nézeti lehetőségek és hang támogatása érdekében ennél a kameránál." + }, + "disabled": "Az újraadás nem engedélyezett ennél a kameránál." + }, + "showStats": { + "label": "Mutasd az adás statisztikákat", + "desc": "Engedélyezze ezt az opciót, hogy a közvetítés statisztikái átfedésként megjelenjenek a kameraképen." + }, + "debugView": "Debug Nézet" + }, + "search": { + "saveSearch": { + "label": "Keresés Mentése", + "placeholder": "Adjon nevet a keresésnek", + "success": "Keresés {{searchName}} mentve.", + "button": { + "save": { + "label": "Keresés mentése" + } + }, + "desc": "Adjon meg egy nevet ehhez a mentett kereséshez.", + "overwrite": "{{searchName}} már létezik. Ha elmenti az felül fogja írni a meglévőt." + } + }, + "recording": { + "confirmDelete": { + "title": "Törlés Megerősítése", + "toast": { + "error": "Törlés sikertelen: {{error}}", + "success": "A kijelölt ellenőrzési elemekhez tartozó videofelvételek sikeresen törölve lettek." + }, + "desc": { + "selected": "Biztosan törölni szeretné az összes rögzített videót, amely ehhez az ellenőrzési elemhez tartozik?

    Tartsa lenyomva a Shift billentyűt, hogy a jövőben kihagyja ezt a párbeszédablakot." + } + }, + "button": { + "markAsReviewed": "Megjelölés áttekintettként", + "deleteNow": "Törlés Most", + "export": "Exportálás", + "markAsUnreviewed": "Megjelölés nem ellenőrzöttként" + } + }, + "imagePicker": { + "selectImage": "Válassza ki egy követett tárgy képét", + "search": { + "placeholder": "Keresés cimke vagy alcimke alapján..." + }, + "noImages": "Nem találhatók bélyegképek ehhez a kamerához" + } +} diff --git a/web/public/locales/hu/components/filter.json b/web/public/locales/hu/components/filter.json new file mode 100644 index 0000000..3ec9ee2 --- /dev/null +++ b/web/public/locales/hu/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Szűrő", + "labels": { + "label": "Cimkék", + "all": { + "title": "Minden cimke", + "short": "Cimkék" + }, + "count_other": "{{count}} cimke", + "count_one": "{{count}} Cimke" + }, + "zones": { + "label": "Zónák", + "all": { + "title": "Minden zóna", + "short": "Zónák" + } + }, + "more": "Több Szűrő", + "timeRange": "Idő intervallum", + "reset": { + "label": "Szűrők visszaállítása alapállapotba" + }, + "subLabels": { + "label": "Alcimkék", + "all": "Minden Alcimke" + }, + "logSettings": { + "label": "Szűrő napló szint", + "loading": { + "title": "Töltés", + "desc": "Amikor a naplópanel az aljára van görgetve, az új naplóbejegyzések automatikusan megjelennek, ahogy hozzáadódnak." + }, + "allLogs": "Minden napló", + "filterBySeverity": "Naplók szűrési fontosság alapján", + "disableLogStreaming": "Napló adásának kikapcsolása" + }, + "trackedObjectDelete": { + "toast": { + "error": "Követett tárgyak törlése sikertelen: {{errorMessage}}", + "success": "Követett tárgyak törlése sikeres." + }, + "title": "Törlés Megerősítése", + "desc": "A(z) {{objectLength}} követett objektum törlése eltávolítja a pillanatképet, az összes mentett beágyazást és az összes kapcsolódó objektum életciklus-bejegyzést. Ezeknek a követett objektumoknak a rögzített felvételei a Történet nézetben NEM kerülnek törlésre.

    Biztosan folytatni szeretné?

    Tartsa lenyomva a Shift billentyűt, hogy a jövőben kihagyja ezt a párbeszédablakot." + }, + "dates": { + "all": { + "title": "Minden Dátum", + "short": "Dátumok" + }, + "selectPreset": "Válasszon sablon beállítást…" + }, + "score": "Pont", + "estimatedSpeed": "Becsült Sebesség {{unit}}", + "features": { + "hasSnapshot": "Van pillanatképe", + "submittedToFrigatePlus": { + "label": "Elküldve a Frigate+-hoz", + "tips": "Először szűrjön azokra a követett objektumokra, amelyek rendelkeznek pillanatképpel.

    A pillanatkép nélküli követett objektumokat nem lehet beküldeni a Frigate+ rendszerébe." + }, + "label": "Funkciók", + "hasVideoClip": "Van videója" + }, + "sort": { + "label": "Rendezés", + "dateAsc": "Dátum szerint (Növekvő)", + "scoreDesc": "Tárgy Pontszám (Csökkenő)", + "scoreAsc": "Tárgy Pontszám (Növekvő)", + "speedAsc": "Becsült Sebesség (Növekvő)", + "dateDesc": "Dátum szerint (Csökkenő)", + "speedDesc": "Becsült Sebesség (Csökkenő)", + "relevance": "Relevancia" + }, + "cameras": { + "all": { + "title": "Minden Kamera", + "short": "Kamerák" + }, + "label": "Kamera szűrő" + }, + "review": { + "showReviewed": "Mutasd az Áttekintetteket" + }, + "motion": { + "showMotionOnly": "Mutasd Csak a Mozgást" + }, + "explore": { + "settings": { + "title": "Beállítások", + "defaultView": { + "title": "Alap Nézet", + "summary": "Összegzés", + "unfilteredGrid": "Szűrőmentes Rács", + "desc": "Ha nincs kiválasztva szűrő, jelenítse meg az egyes címkékhez tartozó legutóbbi követett objektumok összefoglalóját, vagy jelenítsen meg egy szűretlen rácsot." + }, + "searchSource": { + "label": "Keresés Forrás", + "options": { + "description": "Leírás", + "thumbnailImage": "Indexkép" + }, + "desc": "Válassza ki, hogy a követett objektumok bélyegképeiben vagy leírásaiban szeretne keresni." + }, + "gridColumns": { + "title": "Rács Oszlopok", + "desc": "Válassza ki az oszlopok számáta rács nézetben." + } + }, + "date": { + "selectDateBy": { + "label": "Válassza ki a dátumot a szűréshez" + } + } + }, + "zoneMask": { + "filterBy": "Szűrés zóna maszk alapján" + }, + "recognizedLicensePlates": { + "title": "Felismert Rendszámtáblák", + "loadFailed": "Felismert rendszámtáblák betöltése sikertelen.", + "noLicensePlatesFound": "Rendszámtábla nem található.", + "selectPlatesFromList": "Válasszon ki egy vagy több rendszámtáblát a listából.", + "loading": "Felismert rendszámtáblák betöltése…", + "placeholder": "Kezdjen gépelni a rendszámok közötti kereséshez…", + "selectAll": "Mindet kijelöl", + "clearAll": "Mindet törli" + }, + "classes": { + "label": "Osztályok", + "all": { + "title": "Minden Osztály" + }, + "count_one": "{{count}} Osztály", + "count_other": "{{count}} Osztályok" + }, + "attributes": { + "label": "Osztályozási attribútumok", + "all": "Minden attribútum" + } +} diff --git a/web/public/locales/hu/components/icons.json b/web/public/locales/hu/components/icons.json new file mode 100644 index 0000000..5145dc3 --- /dev/null +++ b/web/public/locales/hu/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Válassz ikont", + "search": { + "placeholder": "Ikon keresése…" + } + } +} diff --git a/web/public/locales/hu/components/input.json b/web/public/locales/hu/components/input.json new file mode 100644 index 0000000..8276a31 --- /dev/null +++ b/web/public/locales/hu/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Videó Letöltése", + "toast": { + "success": "Az áttekintendő videó letöltése megkezdődött." + } + } + } +} diff --git a/web/public/locales/hu/components/player.json b/web/public/locales/hu/components/player.json new file mode 100644 index 0000000..1e420ed --- /dev/null +++ b/web/public/locales/hu/components/player.json @@ -0,0 +1,52 @@ +{ + "noRecordingsFoundForThisTime": "Ebben az időpontban nem található felvétel", + "noPreviewFound": "Nincs elérhető előkép", + "submitFrigatePlus": { + "title": "Elküldi ezt a képet a Frigate+-nak?", + "submit": "Küldés", + "previewError": "Nem sikerült betölteni a pillanatkép előnézetét. Előfordulhat, hogy a felvétel jelenleg nem elérhető." + }, + "noPreviewFoundFor": "Nem található előnézet {{cameraName}}-hoz/-hez/-höz", + "livePlayerRequiredIOSVersion": "iOS 17.1 vagy újabb szükséges ehhez az élő adás típushoz.", + "streamOffline": { + "title": "Adás Nem Elérhető", + "desc": "Nem érkezett kép a {{cameraName}}-n, észlelés adásban. Ellenőrizze a hibanaplót" + }, + "cameraDisabled": "Kamera kikapcsolva", + "stats": { + "streamType": { + "title": "Adás Típus:", + "short": "Típus" + }, + "bandwidth": { + "title": "Sávszélesség:", + "short": "Sávszélesség" + }, + "latency": { + "title": "Késleltetés:", + "value": "{{seconds}} másodperc", + "short": { + "title": "Késleltetés", + "value": "{{seconds}} másodperc" + } + }, + "droppedFrames": { + "title": "Hibás Képek:", + "short": { + "value": "{{droppedFrames}} képek", + "title": "Hibás" + } + }, + "decodedFrames": "Dekódolt Képek:", + "droppedFrameRate": "Hibás Kép Sebesség:", + "totalFrames": "Összes Kép:" + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "Kép küldése a Frigate+-hoz meghiúsult" + }, + "success": { + "submittedFrigatePlus": "Kép sikeresen elküldve a Frigate+-nak" + } + } +} diff --git a/web/public/locales/hu/config/cameras.json b/web/public/locales/hu/config/cameras.json new file mode 100644 index 0000000..5bc9723 --- /dev/null +++ b/web/public/locales/hu/config/cameras.json @@ -0,0 +1,79 @@ +{ + "detect": { + "stationary": { + "label": "Mozdulatlan tárgyak beállítása", + "interval": { + "label": "Mozdulatlansági intervallum", + "description": "Milyen gyakorisággal (képkockákban) kell futtatni az észlelési ellenőrzést a mozdulatlan objektumok megerősítéséhez." + }, + "threshold": { + "label": "Mozdulatlansági küszöbérték", + "description": "Képkockák száma amennyitől az objektumot mozdulatlannak jelöli meg." + }, + "max_frames": { + "label": "Max képkockák", + "description": "Az a korlát, ami a mozdulatlan objektumokat követi mielőtt elengedi őket.", + "default": { + "label": "Alapértelmezett max képkocka szám", + "description": "Alapértelmezett maximális képkockák száma, amelyeket egy mozdulatlan objektum követése előtt meg kell jeleníteni." + }, + "objects": { + "label": "Objektum max képkockáinak száma", + "description": "Objektumonkénti felülírások a maximális képkockák számához, hogy nyomon lehessen követni a mozdulatlan objektumokat." + } + }, + "classifier": { + "label": "Vizuális osztályozó engedélyezése", + "description": "Vizuális osztályozóval lehet precízen álló tárgyakat felismerni, még akkor is, ha a keretező négyzetek mozognak." + }, + "description": "Beállítások az egy ideig mozdulatlanul maradó tárgyak észleléséhez és kezeléséhez." + } + }, + "label": "KameraBeállítás", + "name": { + "label": "Kamera neve", + "description": "A kamera neve kötelező" + }, + "friendly_name": { + "label": "Egyszerű név", + "description": "A Frigate felhasználói felületén használt, könnyen megjegyezhető kamera név" + }, + "enabled": { + "label": "Engedélyezve", + "description": "Engedélyezve" + }, + "audio": { + "label": "Hangesemények", + "description": "Hangalapú eseményérzékelés beállításai ennél a kameránál.", + "enabled": { + "label": "Hangalapú eseményérzékelés engedélyezése", + "description": "A hangalapú eseményérzékelés engedélyezése vagy letiltása ennél a kameránál." + }, + "max_not_heard": { + "description": "Ennyi másodperc után fejeződik be a hangesemény, ha a beállított hangtípus nem észlelhető.", + "label": "Időtúllépés befejezése" + }, + "min_volume": { + "label": "Minimális hangerő", + "description": "Minimum RMS hangerő a hangérzékelés futtatásához; az alacsonyabb értékek növelik az érzékenységet (pl: 200 magas, 500 közepes, 1000 alacsony érzékenységet jelent)." + }, + "listen": { + "label": "Hallgatási típúsok", + "description": "Lista a hangalapú eseményekről amit érzékelni szeretnél (angolul) (például: bark, fire_alarm, scream, speech, yell)." + }, + "filters": { + "label": "Audio szűrők (filters)", + "description": "Hangtípusonkénti szűrőbeállítások (filter), mint például a téves találatok számát mérsékelő konfidencia-küszöbök." + }, + "enabled_in_config": { + "label": "Eredeti audio állapot" + } + }, + "audio_transcription": { + "label": "Hang Feliratozás", + "description": "„Beállítások élő hang és beszéd automatikus szöveggé alakításához, eseményekhez és élő feliratozáshoz.", + "enabled": { + "label": "Hangról szövegre alakítás engedélyezése" + } + } +} diff --git a/web/public/locales/hu/config/global.json b/web/public/locales/hu/config/global.json new file mode 100644 index 0000000..1389d61 --- /dev/null +++ b/web/public/locales/hu/config/global.json @@ -0,0 +1,104 @@ +{ + "detect": { + "stationary": { + "label": "Mozdulatlan tárgyak beállítása", + "interval": { + "label": "Mozdulatlansági intervallum", + "description": "Milyen gyakorisággal (képkockákban) kell futtatni az észlelési ellenőrzést a mozdulatlan objektumok megerősítéséhez." + }, + "threshold": { + "label": "Mozdulatlansági küszöbérték", + "description": "Képkockák száma amennyitől az objektumot mozdulatlannak jelöli meg." + }, + "max_frames": { + "label": "Max képkockák", + "description": "Az a korlát, ami a mozdulatlan objektumokat követi mielőtt elengedi őket.", + "default": { + "label": "Alapértelmezett max képkocka szám", + "description": "Alapértelmezett maximális képkockák száma, amelyeket egy mozdulatlan objektum követése előtt meg kell jeleníteni." + }, + "objects": { + "label": "Objektum max képkockáinak száma", + "description": "Objektumonkénti felülírások a maximális képkockák számához, hogy nyomon lehessen követni a mozdulatlan objektumokat." + } + }, + "classifier": { + "label": "Vizuális osztályozó engedélyezése", + "description": "Vizuális osztályozóval lehet precízen álló tárgyakat felismerni, még akkor is, ha a keretező négyzetek mozognak." + }, + "description": "Beállítások az egy ideig mozdulatlanul maradó tárgyak észleléséhez és kezeléséhez." + } + }, + "version": { + "label": "Aktuális konfiguráció verzió", + "description": "Az aktív konfiguráció numerikus vagy karakterláncos változata, amely segít felismerni az migrálásokat vagy a formátumváltozásokat." + }, + "safe_mode": { + "label": "Biztonságos mód", + "description": "Ha engedélyezve van, a Frigate programot biztonsági módban indítja el, csökkentett funkciókkal a hibaelhárítás érdekében." + }, + "environment_vars": { + "label": "Környezeti változók", + "description": "A Home Assistant OS rendszerben a Frigate folyamat számára beállítandó környezeti változói. A nem HAOS-felhasználóknak helyette a Docker konfigurációját kell használniuk." + }, + "logger": { + "label": "Naplózás", + "description": "Az alapértelmezett naplózási részletességet és a komponensenkénti naplózási szintek felülírását vezérli.", + "default": { + "label": "Naplózási részletesség", + "description": "Alapértelmezett globális naplórészletesség (debug, info, warning, error)." + }, + "logs": { + "label": "Folyamatonkénti naplózási szint", + "description": "Összetevőnkénti naplózási szint felülbírálások az egyes modulok részletességének növeléséhez vagy csökkentéséhez." + } + }, + "audio": { + "label": "Hangesemények", + "enabled": { + "label": "Hangalapú eseményérzékelés engedélyezése" + }, + "max_not_heard": { + "description": "Ennyi másodperc után fejeződik be a hangesemény, ha a beállított hangtípus nem észlelhető.", + "label": "Időtúllépés befejezése" + }, + "min_volume": { + "label": "Minimális hangerő", + "description": "Minimum RMS hangerő a hangérzékelés futtatásához; az alacsonyabb értékek növelik az érzékenységet (pl: 200 magas, 500 közepes, 1000 alacsony érzékenységet jelent)." + }, + "listen": { + "label": "Hallgatási típúsok", + "description": "Lista a hangalapú eseményekről amit érzékelni szeretnél (angolul) (például: bark, fire_alarm, scream, speech, yell)." + }, + "filters": { + "label": "Audio szűrők (filters)", + "description": "Hangtípusonkénti szűrőbeállítások (filter), mint például a téves találatok számát mérsékelő konfidencia-küszöbök." + }, + "enabled_in_config": { + "label": "Eredeti audio állapot" + } + }, + "auth": { + "label": "Azonosítás", + "description": "Bejelentkezési és munkamenet-beállítások, többek között süti- és lekérdezési korlátok (rate limit) megadásához.", + "enabled": { + "label": "Bejelentkezés engedélyezése", + "description": "Natív bejelentkezés (azonosítás) engedélyezése a Frigate felületén." + }, + "reset_admin_password": { + "label": "Admin jelszó visszaállítása", + "description": "Ha igaz, akkor visszaállítja az admin felhasználó jelszavát, és induláskor a naplóba írja ki az új jelszót." + }, + "cookie_name": { + "label": "JWT süti neve", + "description": "A süti neve ami a JWT tokent tárolja a natív bejelentkezéshez." + } + }, + "audio_transcription": { + "label": "Hang Feliratozás", + "description": "„Beállítások élő hang és beszéd automatikus szöveggé alakításához, eseményekhez és élő feliratozáshoz.", + "enabled": { + "label": "Hangról szövegre alakítás engedélyezése" + } + } +} diff --git a/web/public/locales/hu/config/groups.json b/web/public/locales/hu/config/groups.json new file mode 100644 index 0000000..6d334c5 --- /dev/null +++ b/web/public/locales/hu/config/groups.json @@ -0,0 +1,58 @@ +{ + "audio": { + "global": { + "detection": "Globális észlelés", + "sensitivity": "Globális érzékenység" + }, + "cameras": { + "detection": "Észlelés", + "sensitivity": "Érzékenység" + } + }, + "timestamp_style": { + "global": { + "appearance": "Általános megjelenés" + }, + "cameras": { + "appearance": "Kinézet" + } + }, + "motion": { + "global": { + "sensitivity": "Globális érzékenység", + "algorithm": "Globális Algoritmus" + }, + "cameras": { + "sensitivity": "Érzékenység", + "algorithm": "Algoritmus" + } + }, + "detect": { + "global": { + "resolution": "Globális Felbontás", + "tracking": "Globális követés" + }, + "cameras": { + "resolution": "Felbontás", + "tracking": "Követés (tracking)" + } + }, + "snapshots": { + "global": { + "display": "Globális kijelző" + }, + "cameras": { + "display": "Kijelző" + } + }, + "objects": { + "global": { + "tracking": "Globális objektumkövetés", + "filtering": "Globális szűrés (filtering)" + }, + "cameras": { + "tracking": "Követés", + "filtering": "Szűrés (filtering)" + } + } +} diff --git a/web/public/locales/hu/config/validation.json b/web/public/locales/hu/config/validation.json new file mode 100644 index 0000000..2e6fafa --- /dev/null +++ b/web/public/locales/hu/config/validation.json @@ -0,0 +1,25 @@ +{ + "minimum": "Legalább {{limit}} kell", + "maximum": "Legfeljebb {{limit}} lehet", + "exclusiveMinimum": "Nagyobbnak kell lennie, mint {{limit}}", + "exclusiveMaximum": "Kevesebbnek kell lennie, mint {{limit}}", + "minLength": "Legalább {{limit}} karaktert kell megadni", + "maxLength": "Legfeljebb {{limit}} karakter lehet", + "minItems": "Legalább {{limit}} elemnek kell lennie", + "maxItems": "Legfeljebb {{limit}} elem lehet", + "pattern": "Érvénytelen formátum", + "required": "Ezt a mezőt kötelező kitölteni", + "type": "Érvénytelen értéktípus", + "enum": "Az engedélyezett értékek közül legalább egy kell legyen", + "const": "Az érték nem egyezik a várt állandóval", + "uniqueItems": "Minden elemnek egyedinek kell lennie", + "format": "Érvénytelen formátum", + "additionalProperties": "Ismeretlen tulajdonság nem engedélyezett", + "oneOf": "Pontosan az egyik engedélyezett sémának kell megfelelnie", + "anyOf": "Legalább az egyik engedélyezett sémának kell megfelelnie", + "ffmpeg": { + "inputs": { + "rolesUnique": "Mindegyik szerepkör (role) csak egy bemeneti (input) streamhez rendelhető hozzá." + } + } +} diff --git a/web/public/locales/hu/objects.json b/web/public/locales/hu/objects.json new file mode 100644 index 0000000..4b53d16 --- /dev/null +++ b/web/public/locales/hu/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Személy", + "bicycle": "Bicikli", + "car": "Autó", + "motorcycle": "Motor", + "airplane": "Repülőgép", + "bus": "Busz", + "train": "Betanít", + "boat": "Hajó", + "dog": "Kutya", + "cat": "Macska", + "horse": "Ló", + "sheep": "Juh", + "bird": "Madár", + "mouse": "Egér", + "keyboard": "Szintetizátor", + "animal": "Állat", + "bark": "Ugatás", + "goat": "Kecske", + "traffic_light": "Jelzőlámpa", + "fire_hydrant": "Tűzcsap", + "street_sign": "Utcatábla", + "stop_sign": "Stop tábla", + "parking_meter": "Parkoló óra", + "bench": "Pad", + "cow": "Tehén", + "elephant": "Elefánt", + "bear": "Medve", + "zebra": "Zebra", + "giraffe": "Zsiráf", + "hat": "Kalap", + "backpack": "Hátitáska", + "umbrella": "Esernyő", + "shoe": "Cipő", + "eye_glasses": "Szemüveg", + "handbag": "Kézitáska", + "tie": "Nyakkendő", + "suitcase": "Bőrönd", + "frisbee": "Frizbi", + "skis": "Síléc", + "snowboard": "Hódeszka (Snowboard)", + "sports_ball": "Sport labda", + "kite": "Sárkány", + "baseball_bat": "Baseball ütő", + "baseball_glove": "Baseball Kesztyű", + "skateboard": "Gördeszka", + "surfboard": "Szörfdeszka", + "tennis_racket": "Teniszütő", + "bottle": "Üveg", + "plate": "Tányér", + "wine_glass": "Boros üveg", + "cup": "Csésze", + "fork": "Villa", + "knife": "Kés", + "spoon": "Kanál", + "bowl": "Tál", + "banana": "Banán", + "apple": "Alma", + "sandwich": "Szendvics", + "orange": "Narancs", + "broccoli": "Brokkoli", + "carrot": "Répa", + "hot_dog": "Hot Dog", + "pizza": "Pizza", + "donut": "Fánk", + "cake": "Torta", + "chair": "Szék", + "couch": "Kanapé", + "potted_plant": "Cserepes növény", + "bed": "Ágy", + "mirror": "Tükör", + "dining_table": "Ebédlő asztal", + "window": "Ablak", + "desk": "Asztal", + "toilet": "Mosdó", + "door": "Ajtó", + "tv": "TV", + "laptop": "Laptop", + "clock": "Óra", + "scissors": "Olló", + "hair_dryer": "Hajszárító", + "sink": "Mosdókagyló", + "blender": "Pépesítő gép", + "toothbrush": "Fogkefe", + "vehicle": "Jármű", + "remote": "Távoli", + "cell_phone": "Mobiltelefon", + "microwave": "Mikrohullám", + "oven": "Sütő", + "refrigerator": "Hűtő", + "book": "Könyv", + "toaster": "Kenyérpirító", + "vase": "Váza", + "teddy_bear": "Teddy Maci", + "hair_brush": "Hajkefe", + "squirrel": "Mókus", + "deer": "Szarvas", + "fox": "Róka", + "rabbit": "Nyúl", + "raccoon": "Mosómedve", + "robot_lawnmower": "Robotfűnyíró", + "waste_bin": "Kuka", + "on_demand": "Igény Szerint", + "license_plate": "Rendszám tábla", + "package": "Csomag", + "bbq_grill": "BBQ sütő", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolátor", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD", + "face": "Arc" +} diff --git a/web/public/locales/hu/views/chat.json b/web/public/locales/hu/views/chat.json new file mode 100644 index 0000000..0130564 --- /dev/null +++ b/web/public/locales/hu/views/chat.json @@ -0,0 +1,19 @@ +{ + "documentTitle": "Chat - Frigate", + "title": "Frigate Chat", + "subtitle": "A kamerakezeléshez és elemzésekhez szükséges MI asszisztensed", + "placeholder": "Kérdezz bármit...", + "error": "Hiba történt. Kérjük, próbáld meg újra.", + "processing": "Feldolgozás...", + "toolsUsed": "Használatban: {{tools}}", + "showTools": "Eszközök megjelenítése ({{count}})", + "hideTools": "Eszközök elrejtése", + "call": "Hívás", + "result": "Eredmény", + "response": "Válasz:", + "attachment_chip_remove": "Melléklet eltávolítása", + "open_in_explore": "Megnyitás Böngészőben", + "attachment_picker_paste_label": "Vagy illeszd be az esemény ID-t", + "attachment_picker_attach": "Melléklet", + "arguments": "Argumentumok:" +} diff --git a/web/public/locales/hu/views/classificationModel.json b/web/public/locales/hu/views/classificationModel.json new file mode 100644 index 0000000..1c26b80 --- /dev/null +++ b/web/public/locales/hu/views/classificationModel.json @@ -0,0 +1,77 @@ +{ + "documentTitle": "Osztályozási modellek - Frigate", + "button": { + "deleteClassificationAttempts": "Osztályozási képek törlése", + "deleteImages": "Képek törlése", + "trainModel": "Modell betanítása", + "deleteModels": "Modellek törlése", + "editModel": "Modell szerkesztése", + "renameCategory": "Osztály átnevezése", + "deleteCategory": "Osztály törlése", + "addClassification": "Osztályozás hozzáadása" + }, + "toast": { + "success": { + "deletedImage_one": "Törölt képek", + "deletedImage_other": "", + "deletedModel_one": "Sikeresen törölve {{count}} modell", + "deletedModel_other": "Sikeresen törölve {{count}} modell", + "categorizedImage": "A kép sikeresen osztályozva", + "deletedCategory_one": "Osztály törlése", + "deletedCategory_other": "", + "trainedModel": "Sikeresen betanított modell.", + "trainingModel": "A modell tanítás sikeresen megkezdődött.", + "updatedModel": "Modellkonfiguráció sikeresen frissítve", + "renamedCategory": "Sikeresen átneveztük az osztályt {{name}} névre" + }, + "error": { + "deleteImageFailed": "Törlés sikertelen: {{errorMessage}}", + "deleteCategoryFailed": "Nem sikerült törölni az osztályt: {{errorMessage}}", + "deleteModelFailed": "Modell törlése nem sikerült: {{errorMessage}}", + "categorizeFailed": "A kép kategorizálása sikertelen: {{errorMessage}}", + "trainingFailed": "A modell képzése sikertelen volt. A részletek a Frigate naplóiban találhatók.", + "trainingFailedToStart": "A modell képzésének elindítása sikertelen: {{errorMessage}}", + "updateModelFailed": "A modell frissítése sikertelen: {{errorMessage}}", + "renameCategoryFailed": "Az osztály átnevezése sikertelen: {{errorMessage}}" + } + }, + "details": { + "none": "Nincs", + "unknown": "Ismeretlen", + "scoreInfo": "A pontszám az objektum összes észlelésében mért átlagos osztályozási megbízhatóságot jelöli." + }, + "edit": { + "title": "Osztályozási modell szerkesztése" + }, + "wizard": { + "step1": { + "name": "Név" + }, + "step2": { + "cameras": "Kamerák" + } + }, + "tooltip": { + "trainingInProgress": "A modell betanítás alatt van", + "noNewImages": "Nincsenek új képek a betanításhoz. Először osztályozzon több képet az adathalmazban.", + "noChanges": "Az adathalmazban nem történt változás az utolsó betanítás óta.", + "modelNotReady": "A modell nem áll készen a betanításra" + }, + "menu": { + "objects": "Objektumok" + }, + "train": { + "titleShort": "Friss" + }, + "deleteCategory": { + "title": "Osztály törlése", + "desc": "Biztosan törölni szeretné a {{name}} osztályt? Ezzel véglegesen törli az összes kapcsolódó képet, és a modell újratanítására lesz szükség.", + "minClassesTitle": "Osztály törlése nem lehetséges" + }, + "deleteModel": { + "title": "Osztályozási modell törlése", + "single": "Biztosan törölni szeretné a(z) {{name}}-t? Ezzel véglegesen törli az összes kapcsolódó adatot, beleértve a képeket és a tanítási adatokat is. Ez a művelet visszafordíthatatlan.", + "desc_one": "Biztosan törölni szeretné a(z) {{count}} modellt? Ezzel véglegesen törli az összes kapcsolódó adatot, beleértve a képeket és a tanítási adatokat is. Ez a művelet visszafordíthatatlan.", + "desc_other": "Biztosan törölni szeretné a(z) {{count}} modelleket? Ezzel véglegesen törli az összes kapcsolódó adatot, beleértve a képeket és a tanítási adatokat is. Ez a művelet visszafordíthatatlan." + } +} diff --git a/web/public/locales/hu/views/configEditor.json b/web/public/locales/hu/views/configEditor.json new file mode 100644 index 0000000..69fa822 --- /dev/null +++ b/web/public/locales/hu/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Konfiguráció Szerkesztő - Frigate", + "configEditor": "Konfiguráció szerkesztő", + "copyConfig": "Konfiguráció másolása", + "saveAndRestart": "Mentés és Újraindítás", + "saveOnly": "Csak mentés", + "toast": { + "success": { + "copyToClipboard": "Konfiguráció átmásolva a vágólapra." + }, + "error": { + "savingError": "Hiba a konfiguráció mentésekor" + } + }, + "confirm": "Kilép mentés nélkül?", + "safeConfigEditor": "Konfiguráció szerkesztő (Biztosnági Mód)", + "safeModeDescription": "Frigate biztonsági módban van konfigurációs hiba miatt." +} diff --git a/web/public/locales/hu/views/events.json b/web/public/locales/hu/views/events.json new file mode 100644 index 0000000..82e7b72 --- /dev/null +++ b/web/public/locales/hu/views/events.json @@ -0,0 +1,67 @@ +{ + "alerts": "Riasztások", + "empty": { + "detection": "Nincs megnézendő észlelés", + "alert": "Nincs megnézendő riasztás", + "motion": "Nem található mozgás", + "recordingsDisabled": { + "title": "A felvétel készítést engedélyezni kell", + "description": "Csak akkor hozhatók létre áttekintési elemek egy kamerához, ha az adott kamerához engedélyezve vannak a felvételek." + } + }, + "detections": "Észlelések", + "motion": { + "label": "Mozgás", + "only": "Csak mozgások" + }, + "allCameras": "Összes kamera", + "timeline": { + "label": "Idővonal" + }, + "detected": "észlelve", + "events": { + "label": "Események", + "aria": "Válassza ki az eseményeket", + "noFoundForTimePeriod": "Nem található esemény ebben az idő intervallumban." + }, + "calendarFilter": { + "last24Hours": "Elmúlt 24 Óra" + }, + "newReviewItems": { + "label": "Új áttekintendő elemek megnézése", + "button": "Áttekintendő Új Elemek" + }, + "camera": "Kamera", + "timeline.aria": "Válassza ki az idővonalat", + "documentTitle": "Áttekintés - Frigate", + "recordings": { + "documentTitle": "Felvételek - Frigate" + }, + "markTheseItemsAsReviewed": "Ezen elemek megjelölése áttekintettként", + "markAsReviewed": "Megjelölés Áttekintettként", + "selected_one": "{{count}} kiválasztva", + "selected_other": "{{count}} kiválasztva", + "suspiciousActivity": "Gyanús Tevékenység", + "threateningActivity": "Fenyegető Tevékenység", + "zoomIn": "Nagyítás", + "zoomOut": "Kicsinyítés", + "detail": { + "trackedObject_other": "{{count}} objektum", + "label": "Részletes", + "noDataFound": "Nincsenek részletes adatok áttekintésre", + "aria": "Részletes nézet kapcsolása", + "trackedObject_one": "{{count}} objektum", + "noObjectDetailData": "Nincsenek elérhető objektumrészlet adatok.", + "settings": "Részletes nézet beállításai", + "alwaysExpandActive": { + "title": "Mindig kibontja az aktív részt" + } + }, + "objectTrack": { + "trackedPoint": "Nyomon követett pont", + "clickToSeek": "Kattintson, az időponthoz ugráshoz" + }, + "select_all": "Összes", + "needsReview": "Felülvizsgálatra szorul", + "securityConcern": "Biztonsági aggályok" +} diff --git a/web/public/locales/hu/views/explore.json b/web/public/locales/hu/views/explore.json new file mode 100644 index 0000000..5e423e5 --- /dev/null +++ b/web/public/locales/hu/views/explore.json @@ -0,0 +1,260 @@ +{ + "documentTitle": "Tallózás - Frigate", + "itemMenu": { + "downloadVideo": { + "label": "Video letöltése", + "aria": "Video letöltése" + }, + "submitToPlus": { + "aria": "Küldés a Frigate Plus-nak", + "label": "Küldés a Frigate+-nak" + }, + "deleteTrackedObject": { + "label": "Követett tárgy törlése" + }, + "viewObjectLifecycle": { + "label": "Tárgy életciklusának megtekintése", + "aria": "Mutasd a tárgy életciklusát" + }, + "findSimilar": { + "label": "Keress hasonlót", + "aria": "Keress hasonló követett tárgyat" + }, + "viewInHistory": { + "label": "Megtekintés az Előzményekben", + "aria": "Megtekintés az Előzményekben" + }, + "downloadSnapshot": { + "aria": "Pillanatfelvétel letöltése", + "label": "Pillanatfelvétel letöltése" + }, + "addTrigger": { + "label": "Indító hozzáadása", + "aria": "Indító hozzáadása ehhez a követett tárgyhoz" + }, + "audioTranscription": { + "label": "Átírás", + "aria": "Hangátirat kérése" + }, + "viewTrackingDetails": { + "label": "A követés részleteinek megtekintése", + "aria": "Követési adatok megjelenítése" + } + }, + "details": { + "editLPR": { + "title": "Rendszám módosítása", + "desc": "Új rendszám hozzáadása ehhez {{label}}", + "descNoLabel": "Adjon hozzá egy új rendszámot ehhez a követett tárgyhoz" + }, + "label": "Cimke", + "editSubLabel": { + "desc": "Új alcimke létrehozása ehhez a cimkéhez {{label}}", + "title": "Alcimke módosítása", + "descNoLabel": "Adjon hozzá egy új alcimkét ehhez a követett tárgyhoz" + }, + "camera": "Kamera", + "zones": "Zónák", + "description": { + "placeholder": "Követett tárgy leírása", + "label": "Leírás", + "aiTips": "A Frigate nem fog igényelni leírást a Generatív MI szolgáltatójától, amíg a követett tárgy életciklusa be nem fejeződött." + }, + "timestamp": "Időbélyeg", + "estimatedSpeed": "Becsült Sebesség", + "objects": "Tárgyak", + "topScore": { + "label": "Legnagyobb Pontszám", + "info": "A legmagasabb pontszám a követett objektum legmagasabb medián pontszáma, ezért eltérhet a keresési eredmény bélyegképén megjelenő pontszámtól." + }, + "recognizedLicensePlate": "Felismert Rendszám", + "tips": { + "descriptionSaved": "Leírás sikeresen mentve", + "saveDescriptionFailed": "Leírás frissítése sikertelen: {{errorMessage}}" + }, + "item": { + "toast": { + "error": { + "updatedLPRFailed": "Rendszám frissítése sikertelen: {{errorMessage}}", + "updatedSublabelFailed": "Alcimke frissítése sikertelen: {{errorMessage}}", + "regenerate": "Nem sikerült meghívni a(z) {{provider}} szolgáltatót az új leírásért: {{errorMessage}}", + "audioTranscription": "Nem sikerült hangátiratot kérni: {{errorMessage}}" + }, + "success": { + "updatedSublabel": "Az alcimke sikeresen frissítve.", + "updatedLPR": "Rendszám sikeresen frissítve.", + "regenerate": "Új leírást kértünk a(z) {{provider}} szolgáltatótól. A szolgáltató sebességétől függően az új leírás előállítása eltarthat egy ideig.", + "audioTranscription": "Sikeresen kérte a hangátírást." + } + }, + "button": { + "viewInExplore": "Mutasd a Felfedezésben", + "share": "Áttekintési elem megosztása" + }, + "desc": "Áttekintendő elem részletei", + "title": "Áttekintendő Elem Részletei", + "tips": { + "mismatch_one": "{{count}} nem elérhető objektum lett észlelve és belefogalve ebbe az ellenőrzési elembe. Ez az objektum vagy nem felelt meg a riasztás vagy észlelés feltételeinek, vagy már törlésre/eltávolításra került.", + "mismatch_other": "{{count}} nem elérhető objektumok lettek észlelve és belefogalve ebbe az ellenőrzési elembe. Ezek az objektumok vagy nem feleltek meg a riasztás vagy észlelés feltételeinek, vagy már törlésre/eltávolításra kerültek.", + "hasMissingObjects": "Állítsa be a konfigurációját, ha azt szeretné, hogy a Frigate mentse a követett objektumokat a következő címkékhez: {{objects}}" + } + }, + "snapshotScore": { + "label": "Pillanatfelvétel Pontszáma" + }, + "regenerateFromThumbnails": "Újragenerálás kisképből", + "regenerateFromSnapshot": "Újragenerálás pillanatfelvételből", + "button": { + "regenerate": { + "label": "Követett tárgy leírásának újragenerálása", + "title": "Újragenerálás" + }, + "findSimilar": "Keress Hasonlót" + }, + "expandRegenerationMenu": "Újragenerálási menü kiterjesztése", + "score": { + "label": "Pontszám" + } + }, + "searchResult": { + "deleteTrackedObject": { + "toast": { + "error": "Hiba a követett tárgy törlése közben: {{errorMessage}}", + "success": "Követett tárgy sikeresen törölve." + } + }, + "tooltip": "{{type}} egyezés {{confidence}}%-os megbízhatósággal", + "nextTrackedObject": "Következő követett objektum", + "previousTrackedObject": "Előző követett objektum" + }, + "generativeAI": "Generatív MI", + "exploreIsUnavailable": { + "title": "Felfedezés nem elérhető", + "embeddingsReindexing": { + "startingUp": "Indulás…", + "estimatedTime": "Becsült hátralevő idő:", + "finishingShortly": "Hamarosan végez", + "step": { + "thumbnailsEmbedded": "Beágyazott ikonok: ", + "descriptionsEmbedded": "Beágyazott leírások: ", + "trackedObjectsProcessed": "Feldolgozott követett tárgyak: " + }, + "context": "A felfedezés azután használható, hogy a követett tárgy beágyazások újraindexálása befejeződött." + }, + "downloadingModels": { + "tips": { + "documentation": "Olvassa el a leírást", + "context": "Érdemes lehet újraindexelni a követett objektumok beágyazásait, miután a modellek letöltődtek." + }, + "setup": { + "textModel": "Szöveg modell", + "textTokenizer": "Szöveg tokenizáló", + "visionModel": "Látvány modell", + "visionModelFeatureExtractor": "Látvány modell képesség kinyerő" + }, + "error": "Hiba történt. Ellenőrizze a Frigate naplókat.", + "context": "A Frigate letölti a szemantikus keresés funkcióhoz szükséges beágyazási modelleket. Ez a hálózati kapcsolat sebességétől függően néhány percet is igénybe vehet." + } + }, + "noTrackedObjects": "Nincs követett tárgy", + "trackedObjectsCount_one": "{{count}} követett tárgy. ", + "trackedObjectsCount_other": "{{count}} követett tárgy. ", + "dialog": { + "confirmDelete": { + "title": "Törlés megerősítése", + "desc": "Ennek a követett objektumnak a törlése eltávolítja a pillanatképet, az összes mentett beágyazást és az összes kapcsolódó objektum életciklus-bejegyzést. A Történet nézetben lévő rögzített felvételek NEM kerülnek törlésre.

    Biztosan folytatni szeretné?" + } + }, + "fetchingTrackedObjectsFailed": "Hiba a követett tárgyak betöltése közben: {{errorMessage}}", + "objectLifecycle": { + "title": "Tárgy Életciklus", + "noImageFound": "Ehhez az időbélyeghez nem található kép.", + "createObjectMask": "Tárgy Maszk Létrehozása", + "scrollViewTips": "Görgessen, hogy megnézze ezen tárgy életciklusának jelentős pillanatait.", + "lifecycleItemDesc": { + "heard": "{{label}} meghallva", + "external": "{{label}} észlelve", + "header": { + "zones": "Zónák", + "ratio": "Arány", + "area": "Körzet" + }, + "active": "{{label}} aktiválódott", + "attribute": { + "other": "{{label}} felismerve mint {{attribute}}", + "faceOrLicense_plate": "{{attribute}} észlelve {{label}}-hoz/-hez/-höz" + }, + "entered_zone": "{{label}} belépett {{zones}}-ba/-be", + "visible": "{{label}} észlelve", + "gone": "{{label}} elment", + "stationary": "{{label}} mozdulatlanná vált" + }, + "annotationSettings": { + "offset": { + "documentation": "Olvassa el a leírást ", + "label": "Jelölés eltolás", + "desc": "Ez az információ a kamera észlelési csatornájából származik, de a felvételi csatorna képeire van ráhelyezve. Nem valószínű, hogy a két közvetítés tökéletesen szinkronban van. Ennek eredményeként a kijelölő keret és a felvétel nem lesz teljesen pontosan összehangolva. Azonban az annotation_offset mező segítségével ez korrigálható.", + "millisecondsToOffset": "Ezredmásodpercben megadott érték, amellyel az észlelési jelölések eltolhatók. Alapértelmezett: 0", + "tips": "TIPP: Képzelje el, hogy van egy eseményklip, amelyben egy személy balról jobbra sétál. Ha az esemény idővonalán a kijelölő keret következetesen a személy bal oldalán jelenik meg, akkor az értéket csökkenteni kell. Hasonlóképpen, ha a kijelölő keret folyamatosan a személy előtt van, akkor az értéket növelni kell.", + "toast": { + "success": "A(z) {{camera}} jelöléseltolása el lett mentve a konfigurációs fájlba. Indítsa újra a Frigate-et a módosítások alkalmazásához." + } + }, + "showAllZones": { + "desc": "Mindig mutasd a képen a zónákat, ahol a tárgy belépett a zónába.", + "title": "Mutasd az Összes Zónát" + }, + "title": "Jelölés beállításai" + }, + "carousel": { + "next": "Következő dia", + "previous": "Előző dia" + }, + "trackedPoint": "Követett Pont", + "count": "{{first}}-nek a {{second}}", + "autoTrackingTips": "A doboz helyzete pontatlan lesz az automatikusan követő kamerákhoz.", + "adjustAnnotationSettings": "Címkézés beállításainak módosítása" + }, + "type": { + "video": "videó", + "object_lifecycle": "tárgy életciklus", + "details": "részletek", + "snapshot": "pillanatfelvétel", + "thumbnail": "bélyegkép", + "tracking_details": "követési adatok" + }, + "trackedObjectDetails": "Követett Tárgy Részletei", + "exploreMore": "Fedezzen fel több {{label}} tárgyat", + "aiAnalysis": { + "title": "MI-elemzés" + }, + "concerns": { + "label": "Aggodalmak" + }, + "trackingDetails": { + "lifecycleItemDesc": { + "active": "{{label}} aktív lett", + "attribute": { + "other": "{{label}} felismerve mint {{attribute}}" + }, + "external": "{{label}} észlelve", + "header": { + "zones": "Zónák", + "ratio": "Arány", + "area": "Terület", + "score": "Pontszám" + }, + "visible": "{{label}} észlelve", + "entered_zone": "{{label}} belépett {{zones}}", + "gone": "{{label}} továbbhaladt", + "stationary": "{{label}} mozdulatlanná vált" + }, + "title": "Követési adatok", + "noImageFound": "Nem található kép ehhez az időbélyeghez.", + "createObjectMask": "Objektum maszk létrehozása", + "scrollViewTips": "Kattintson ide, hogy megtekintse az objektum életciklusának fontosabb pillanatait.", + "autoTrackingTips": "Az automatikus követésű kamerák esetében a keret pozíciói pontatlanok lesznek.", + "count": "{{first}} a {{second}} közül", + "trackedPoint": "Nyomon követett pont" + } +} diff --git a/web/public/locales/hu/views/exports.json b/web/public/locales/hu/views/exports.json new file mode 100644 index 0000000..ebf428f --- /dev/null +++ b/web/public/locales/hu/views/exports.json @@ -0,0 +1,32 @@ +{ + "documentTitle": "Exportálás - Frigate", + "search": "Keresés", + "noExports": "Export nem található", + "deleteExport.desc": "Biztos, hogy törölni akarja {{exportName}}-t?", + "deleteExport": { + "label": "Export törlése" + }, + "editExport": { + "title": "Exportálás átnevezése", + "desc": "Adjon meg egy új nevet ennek az exportnak.", + "saveExport": "Export mentése" + }, + "toast": { + "error": { + "renameExportFailed": "Sikertelen export átnevezés: {{errorMessage}}" + } + }, + "tooltip": { + "downloadVideo": "Videó letöltése", + "editName": "Név szerkesztése", + "deleteExport": "Export törlése", + "shareExport": "Export megosztása" + }, + "headings": { + "cases": "Esetek", + "uncategorizedExports": "Kategória nélküli exportok" + }, + "toolbar": { + "addExport": "Export hozzáadása" + } +} diff --git a/web/public/locales/hu/views/faceLibrary.json b/web/public/locales/hu/views/faceLibrary.json new file mode 100644 index 0000000..3733961 --- /dev/null +++ b/web/public/locales/hu/views/faceLibrary.json @@ -0,0 +1,101 @@ +{ + "renameFace": { + "title": "Arc átnevezése", + "desc": "Adjon meg egy új nevet neki: {{name}}" + }, + "details": { + "subLabelScore": "Alcimke érték", + "unknown": "Ismeretlen", + "person": "Ember", + "timestamp": "Időbélyeg", + "face": "Arc részletek", + "faceDesc": "A követett tárgy részletei, amely alapján ez az arc létrejött", + "scoreInfo": "Az alcímke pontszáma az összes felismert arc pontozásának súlyozott átlaga, ezért ez eltérhet a pillanatképen megjelenített pontszámtól." + }, + "button": { + "deleteFace": "Arc törlése", + "renameFace": "Arc átnevezése", + "deleteFaceAttempts": "Arcok törlése", + "addFace": "Arc hozzáadása", + "uploadImage": "Kép feltöltése", + "reprocessFace": "Arc Újrafeldolgozása" + }, + "collections": "Gyűjtemények", + "steps": { + "description": { + "uploadFace": "Töltsön fel egy képet {{name}}-ről amin szemből látható. A képen nem szükséges, hogy csak az arc legyen látható." + }, + "faceName": "Adjon meg egy arcnevet", + "uploadFace": "Arckép feltöltése", + "nextSteps": "Következő lépések" + }, + "deleteFaceAttempts": { + "title": "Arcok törlése", + "desc_one": "Biztos benne, hogy törölni akar {{count}} arcot? Ezt már nem tudja visszavonni.", + "desc_other": "Biztos benne, hogy törölni akar {{count}} arcot? Ezt már nem tudja visszavonni." + }, + "uploadFaceImage": { + "title": "Arckép feltöltése", + "desc": "Töltsön fel egy képet, hogy beolvasson arcokat és beillessze {{pageToggle}}-ba/-be" + }, + "createFaceLibrary": { + "title": "Gyűjtemény létrehozása", + "desc": "Új gyűjtemény létrehozása", + "new": "Új arc létrhozása", + "nextSteps": "A jó alap készítéséhez:
  • Használja a Legutóbbi felismerések fület az egyes észlelt személyekhez tartozó képek kiválasztásához és betanításához.
  • A legjobb eredmény érdekében válassza az egyenesen előre néző arcokat ábrázoló képeket és kerülje a ferde szögből készült arcképeket a tanításhoz." + }, + "description": { + "placeholder": "Adj nevet ennek a gyűjteménynek", + "invalidName": "Nem megfelelő név. A nevek csak betűket, számokat, szóközöket, aposztrófokat, alulhúzásokat és kötőjeleket tartalmazhatnak.", + "addFace": "Adj hozzá egy új gyűjteményt az Arcképtárhoz az első képed feltöltésével.", + "nameCannotContainHash": "A név nem tartalmazhat # karaktert." + }, + "selectFace": "Arc kiválasztása", + "deleteFaceLibrary": { + "title": "Név törlése", + "desc": "Biztosan törölni akarja a {{name}} gyűjteményt? Ezzel véglegesen törli a párosított arcokat." + }, + "imageEntry": { + "dropActive": "Húzza a képet ide…", + "validation": { + "selectImage": "Kérem válasszon egy képet." + }, + "maxSize": "Maximális méret: {{size}} MB", + "dropInstructions": "Fogja és húzza a képet ide, vagy kattintson a kiválasztáshoz" + }, + "trainFaceAs": "Arc tanítása mint:", + "trainFace": "Arc tanítása", + "toast": { + "success": { + "addFaceLibrary": "{{name}} sikeresen hozzáadva a az Arc Könyvtárhoz!", + "uploadedImage": "A kép sikeresen feltöltve.", + "deletedName_one": "{{count}} arc sikeresen törölve.", + "deletedName_other": "{{count}} arc sikeresen törölve.", + "renamedFace": "Arc sikeresen átnvezezve {{name}}-ra/-re", + "updatedFaceScore": "Arc pontszáma sikeresen frissítve a következőhöz {{name}} ({{score}}).", + "trainedFace": "Arc sikeresen betanítva.", + "deletedFace_one": "{{count}} arc sikeresen törölve.", + "deletedFace_other": "{{count}} arc sikeresen törölve." + }, + "error": { + "addFaceLibraryFailed": "Arc névadás sikertelen: {{errorMessage}}", + "deleteFaceFailed": "Törlés sikertelen: {{errorMessage}}", + "deleteNameFailed": "Név törlése sikertelen: {{errorMessage}}", + "uploadingImageFailed": "Kép feltöltése sikertelen: {{errorMessage}}", + "updateFaceScoreFailed": "Arc pontszám frissítése sikertelen: {{errorMessage}}", + "renameFaceFailed": "Arc átnevezése sikertelen: {{errorMessage}}", + "trainFailed": "Sikertelen tanítás: {{errorMessage}}" + } + }, + "readTheDocs": "Olvassa el a dokumentációt", + "nofaces": "Nincs elérhető arc", + "documentTitle": "Arc könyvtár - Frigate", + "train": { + "title": "Friss felismerések", + "empty": "Nincs friss arcfelismerés", + "aria": "Válassza ki a tanítást", + "titleShort": "Friss" + }, + "pixels": "{{area}}px", + "selectItem": "KIválasztani {{item}}-et" +} diff --git a/web/public/locales/hu/views/live.json b/web/public/locales/hu/views/live.json new file mode 100644 index 0000000..a24a0e6 --- /dev/null +++ b/web/public/locales/hu/views/live.json @@ -0,0 +1,188 @@ +{ + "twoWayTalk": { + "enable": "Kétirányú kommunikáció engedélyezése", + "disable": "Kétirányú kommunikáció tiltása" + }, + "documentTitle": { + "default": "Élő - Frigate" + }, + "lowBandwidthMode": "Alacsony felbontású mód", + "documentTitle.withCamera": "{{camera}} - Élő - Frigate", + "cameraAudio": { + "disable": "Kamera hangjának kikapcsolása", + "enable": "Kamera hangjának bekapcsolása" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kattintson a képre a kamera középre igazításához", + "enable": "Engedélyezze a kattintást a mozgatáshoz", + "disable": "Kattintással húzás kikapcsolása", + "enableWithZoom": "Kattintással történő mozgatás és húzással való nagyítás engedélyezése" + }, + "left": { + "label": "PTZ kamera balra mozgatása" + }, + "up": { + "label": "PTZ kamera felfele mozgatása" + }, + "down": { + "label": "PTZ kamera lefele mozgatása" + }, + "right": { + "label": "PTZ kamera jobbra mozgatása" + } + }, + "zoom": { + "in": { + "label": "PTZ kamera közelítés" + }, + "out": { + "label": "PTZ kamera távolodás" + } + }, + "frame": { + "center": { + "label": "Kattinston a képre a PTZ kamera középre igazításához" + } + }, + "presets": "PTZ kamera előzetes beállításai", + "focus": { + "in": { + "label": "PTZ kamera fókuszálás BE" + }, + "out": { + "label": "PTZ kamera fókuszálás KI" + } + } + }, + "camera": { + "enable": "Kamera Engedélyezése", + "disable": "Kamera Kikapcsolása" + }, + "muteCameras": { + "enable": "Minden Kamera Némítása", + "disable": "Minden Kamera Felhangosítása" + }, + "detect": { + "enable": "Észlelés Engedélyezése", + "disable": "Észlelés Kikapcsolása" + }, + "recording": { + "enable": "Felvétel Készítés Engedélyezése", + "disable": "Felvétel Készítés Kikapcsolása" + }, + "snapshots": { + "enable": "Pillanatfelvétel Engedélyezése", + "disable": "Pillanatfelvétel Kikapcsolása" + }, + "audioDetect": { + "enable": "Hang Észlelés Engedélyezése", + "disable": "Hang Észlelés Kikapcsolása" + }, + "autotracking": { + "enable": "Automatikus Követés Engedélyezése", + "disable": "Automatikus Követés Kikapcsolása" + }, + "streamStats": { + "enable": "Adás Statisztika Mutatása", + "disable": "Adás Statisztika Elrejtése" + }, + "manualRecording": { + "playInBackground": { + "label": "Lejátszás a háttérben", + "desc": "Engedélyezze ezt a lehetőséget, hogy az adás tovább folyjon amikor a lejátszó rejtve van." + }, + "showStats": { + "label": "Statisztika Mutatása", + "desc": "Engedélyezze ezt az opciót, hogy a közvetítés statisztikái átfedésként megjelenjenek a kameraképen." + }, + "debugView": "Hibakeresési Nézet", + "title": "Igény Szerinti Felvétel", + "start": "Igény szerinti felvétel indítása", + "started": "Kézi igény szerinti felvétel elindítva.", + "failedToStart": "Kézi igény szerinti felvétel indítása sikertelen.", + "end": "Igény szerinti felvétel befejezése", + "ended": "Kézi igény szerinti felvétel befejezve.", + "failedToEnd": "Kézi igény szerinti felvétel befejezése sikertelen.", + "tips": "Indítson el egy manuális eseményt a kamera felvételmegőrzési beállításai alapján.", + "recordDisabledTips": "Mivel a rögzítés le van tiltva vagy korlátozva van a konfigurációban ennél a kameránál, csak egy pillanatkép kerül mentésre." + }, + "streamingSettings": "Adás Beállítások", + "notifications": "Értesítések", + "audio": "Hang", + "suspend": { + "forTime": "Felfüggesztés: " + }, + "stream": { + "title": "Adás", + "audio": { + "tips": { + "documentation": "Olvassa el a leírást ", + "title": "A hangot a kamerának kell kibocsátania, és a go2rtc-ben kell konfigurálni ehhez a streamhez." + }, + "available": "Ehhez az adáshoz hang elérhető", + "unavailable": "Nem elérhető hang ehhez az adáshoz" + }, + "twoWayTalk": { + "tips.documentation": "Olvassa el a leírást ", + "available": "Kétirányú kommunikáció elérhető ehhez az adáshoz", + "unavailable": "Nem elérhető a kétirányú kommunikáció ehhez az adáshoz", + "tips": "Az eszközének támogatnia kell ezt a funkciót, és a WebRTC-nek kétirányú beszélgetésre kell lennie konfigurálva." + }, + "lowBandwidth": { + "resetStream": "Adás visszaállítása", + "tips": "Az élő nézet alacsony sávszélességű módban van a pufferelés vagy stream hibák miatt." + }, + "playInBackground": { + "label": "Lejátszás a háttérben", + "tips": "Engedélyezze ezt az opciót a folyamatos közvetítéshez akkor is, ha a lejátszó rejtve van." + }, + "debug": { + "picker": "A stream kiválasztása nem érhető el hibakeresési módban. A hibakeresési nézet mindig az észlelési szerepkörhöz rendelt streamet használja." + } + }, + "cameraSettings": { + "title": "{{camera}} Beállítások", + "cameraEnabled": "Kamera Engedélyezve", + "objectDetection": "Tárgy Észlelés", + "recording": "Felvétel", + "audioDetection": "Hang Észlelés", + "snapshots": "Pillanatképek", + "autotracking": "Automatikus követés", + "transcription": "Hang Feliratozás" + }, + "history": { + "label": "Előzmény felvételek megjelenítése" + }, + "effectiveRetainMode": { + "modes": { + "all": "Mind", + "motion": "Mozgás", + "active_objects": "Aktív objektumok" + }, + "notAllTips": "Az Ön {{source}} felvételmegőrzési beállítása akövetkezőre van állítva mode: {{effectiveRetainMode}}, így ez az igény szerinti felvétel csak a {{effectiveRetainModeName}} szegmenseket őrzi meg." + }, + "editLayout": { + "label": "Elrendezés szerkesztése", + "group": { + "label": "Kameracsoport szerkesztése" + }, + "exitEdit": "Szerkesztés bezárása" + }, + "transcription": { + "enable": "Élő Audio Feliratozás Engedélyezése", + "disable": "Élő Audio Feliratozás Kikapcsolása" + }, + "noCameras": { + "title": "Nincsenek kamerák beállítva", + "description": "Kezdje egy kamera csatlakoztatásával.", + "buttonText": "Kamera hozzáadása" + }, + "snapshot": { + "takeSnapshot": "Azonnali pillanatkép letöltése", + "noVideoSource": "Ehhez a pillanatképhez videó forrás nem elérhető.", + "captureFailed": "Pillanatkép készítése sikertelen.", + "downloadStarted": "Pillanatkép letöltése elindítva." + } +} diff --git a/web/public/locales/hu/views/motionSearch.json b/web/public/locales/hu/views/motionSearch.json new file mode 100644 index 0000000..e84683d --- /dev/null +++ b/web/public/locales/hu/views/motionSearch.json @@ -0,0 +1,22 @@ +{ + "documentTitle": "Mozgás Keresés - Frigate", + "title": "Mozgás Keresés", + "description": "Rajzoljon be egy sokszöget a vizsgálandó terület kijelöléséhez, majd adja meg az időtartományt, amelyen belül a mozgásváltozásokat szeretné keresni ezen a területen.", + "selectCamera": "Mozgás Keresés betöltése folyamatban", + "startSearch": "Keresés Indítása", + "searchStarted": "Megkezdődött a keresés", + "searchCancelled": "Keresés megszakítva", + "cancelSearch": "Mégse", + "searching": "Keresés folyamatban.", + "searchComplete": "A keresés befejeződött", + "noResultsYet": "Futtass keresést a kijelölt területen a mozgásváltozások felkutatásához", + "noChangesFound": "A kijelölt területen nem történt pixelváltozás", + "changesFound_one": "Észlelhető {{count}} mozgásváltozás", + "changesFound_other": "Észlelhető {{count}} mozgásváltozás", + "framesProcessed": "{{count}} képkocka feldolgozva", + "jumpToTime": "Ugrás erre az időpontra", + "results": "Eredmények", + "showSegmentHeatmap": "Hőtérkép", + "newSearch": "Új Keresés", + "clearResults": "Eredmények törlése" +} diff --git a/web/public/locales/hu/views/recording.json b/web/public/locales/hu/views/recording.json new file mode 100644 index 0000000..67aac28 --- /dev/null +++ b/web/public/locales/hu/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Szűrő", + "calendar": "Naptár", + "export": "Exportálás", + "filters": "Szűrők", + "toast": { + "error": { + "noValidTimeSelected": "Nem megfelelő idősáv kiválasztva", + "endTimeMustAfterStartTime": "A végpontnak később kell lennie, mint a kezdőpontnak" + } + } +} diff --git a/web/public/locales/hu/views/replay.json b/web/public/locales/hu/views/replay.json new file mode 100644 index 0000000..7df3327 --- /dev/null +++ b/web/public/locales/hu/views/replay.json @@ -0,0 +1,25 @@ +{ + "title": "Hibakeresés visszajátszás", + "websocket_messages": "Üzenetek", + "dialog": { + "title": "Hibakeresés visszajátszásának indítása", + "camera": "Forráskamera", + "timeRange": "Időtartomány", + "preset": { + "1m": "Legutóbbi 1 perc", + "5m": "Legutóbbi 5 perc", + "timeline": "Az Idővonalból", + "custom": "Egyedi" + }, + "startButton": "Visszajátszás indítása", + "selectFromTimeline": "Válassz", + "starting": "Visszajátszás indítása...", + "startLabel": "Indítás", + "endLabel": "Vége", + "toast": { + "error": "A hibakeresési visszajátszás elindítása sikertelen: {{error}}", + "alreadyActive": "A visszajátszási munkamenet már aktív", + "goToReplay": "Ugrás a visszajátszásra" + } + } +} diff --git a/web/public/locales/hu/views/search.json b/web/public/locales/hu/views/search.json new file mode 100644 index 0000000..488ad43 --- /dev/null +++ b/web/public/locales/hu/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Keresés", + "savedSearches": "Mentett keresések", + "searchFor": "{{inputValue}} keresése", + "button": { + "clear": "Keresés törlése", + "filterInformation": "Szűrő információ", + "save": "Keresés mentése", + "delete": "Mentett keresések törlése", + "filterActive": "Aktív szűrők" + }, + "trackedObjectId": "Követett Tárgy Azonosító", + "filter": { + "label": { + "cameras": "Kamerák", + "labels": "Cimkék", + "zones": "Zónák", + "sub_labels": "Alcimkék", + "search_type": "Keresés típusa", + "time_range": "Idő intervallum", + "before": "Előtte", + "after": "Utána", + "min_score": "Minimum Pont", + "max_score": "Maximális Pont", + "min_speed": "Minimum Sebesség", + "max_speed": "Maximális Sebesség", + "recognized_license_plate": "Felismert Rendszám", + "has_clip": "Van Klip", + "has_snapshot": "Van pillanatképe", + "attributes": "Tulajdonságok" + }, + "searchType": { + "description": "Leírás", + "thumbnail": "Bélyegkép" + }, + "tips": { + "title": "Hogyan használja a szöveg szűrőket", + "desc": { + "exampleLabel": "Példa:", + "step6": "Törölje a szűrőket az 'x'-re kattintva mellettük.", + "step1": "Írjon be egy szűrő kulcs nevet kettősponttal végződően (pl.: \"kamerák:\").", + "step5": "Az időbeli szűrő {{exampleTime}} formátumot használ.", + "step4": "A dátum szűrők (előtte: és utána:) {{DateFormat}} formátumban vannak.", + "text": "A szűrők segítenek szűkíteni a keresési eredményeket. Így használhatja őket a beviteli mezőben:", + "step2": "Válasszon egy értéket a lehetőségek közül, vagy írja be a sajátját.", + "step3": "Több szűrőt is használhat, ha azokat egymás után, szóközzel elválasztva adja meg." + } + }, + "header": { + "currentFilterType": "Szűrő Értékek", + "noFilters": "Szűrők", + "activeFilters": "Aktív Szűrők" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "A 'kezdeti' dátumnak később kell lennie, mint a 'későbbi' dátumnak.", + "afterDatebeEarlierBefore": "Az 'későbbi' dátumnam korábban kell lennie, mint az 'előtte' dátumnak.", + "minScoreMustBeLessOrEqualMaxScore": "A 'min_pontszám'-nak kevesebbnek vagy ugyanannyinak kell lennie, mint a 'max_pontszám'.", + "maxScoreMustBeGreaterOrEqualMinScore": "A 'max_pontszám'-nak nagyobb vagy egyenlőnek kell lennie, mint a 'min_pontszám'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "A 'min_sebesség'-nek mindenképp kevesebbnek vagy azonosnak kell lennie, mint a 'max_sebesség'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "A 'max_speed' értékének nagyobbnak vagy egyenlőnek kell lennie a 'min_speed' értékéhez képest." + } + } + }, + "similaritySearch": { + "active": "Hasonlóság keresés aktív", + "title": "Hasonlóság Keresés", + "clear": "Hasonlósági keresés törlése" + }, + "placeholder": { + "search": "Keresés…" + } +} diff --git a/web/public/locales/hu/views/settings.json b/web/public/locales/hu/views/settings.json new file mode 100644 index 0000000..3cf386f --- /dev/null +++ b/web/public/locales/hu/views/settings.json @@ -0,0 +1,897 @@ +{ + "documentTitle": { + "default": "Beállítások - Frigate", + "authentication": "Hitelesítési beállítások - Frigate", + "camera": "Kamera beállítások - Frigate", + "classification": "Osztályozási beállítások - Frigate", + "masksAndZones": "Maszk és zónaszerkesztő - Frigate", + "object": "Hibakeresés - Frigate", + "general": "Felhasználói felület beállításai - Frigate", + "frigatePlus": "Frigate+ beállítások - Frigate", + "notifications": "Értesítések beállítása - Frigate", + "motionTuner": "Mozgás Hangoló - Frigate", + "enrichments": "Kiegészítés Beállítások - Frigate", + "cameraManagement": "Kamerák kezelése - Frigate", + "cameraReview": "Kamera beállítások áttekintése – Frigate", + "globalConfig": "Globális Konfiguráció - Frigate", + "cameraConfig": "Kamera Konfiguráció - Frigate", + "maintenance": "Karbantartás - Frigate", + "profiles": "Profilok - Frigate" + }, + "menu": { + "ui": "UI", + "classification": "Osztályozás", + "cameras": "Kamera beállítások", + "masksAndZones": "Maszkok / Zónák", + "motionTuner": "Mozgás finomhangolása", + "debug": "Hibakeresés", + "users": "Felhasználók", + "notifications": "Értesítések", + "frigateplus": "Frigate+", + "enrichments": "Extra funkciók", + "triggers": "Triggerek", + "roles": "Szerepkörök", + "cameraManagement": "Menedzsment", + "cameraReview": "Vizsgálat", + "general": "Általános", + "globalConfig": "Globális konfiguráció", + "system": "Rendszer", + "integrations": "Integrációk", + "uiSettings": "UI beállítások", + "profiles": "Profilok", + "globalDetect": "Tárgy felismerés", + "globalRecording": "Felvétel", + "globalSnapshots": "Pillanatképek", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Mozgásérzékelés", + "globalObjects": "Tárgyak", + "globalReview": "Áttekintés", + "globalAudioEvents": "Hangesemények", + "cameraAudioEvents": "Hangesemények", + "cameraAudioTranscription": "Hang Feliratozás", + "integrationAudioTranscription": "Hang Feliratozás" + }, + "dialog": { + "unsavedChanges": { + "title": "Vannak nem mentett módosítások.", + "desc": "Szeretné elmenteni a módosításokat a folytatás előtt?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Nincs kamera" + }, + "general": { + "liveDashboard": { + "title": "Live irányítópult", + "automaticLiveView": { + "label": "Automata élőkép", + "desc": "Automatikusan váltson át a kamera élő nézetére, amikor aktivitást észlel. Ha ez az opció ki van kapcsolva, akkor az Élő irányítópulton a statikus kameraképek csak percenként egyszer frissülnek." + }, + "playAlertVideos": { + "label": "Riasztási Videók Lejátszása", + "desc": "Alapértelmezetten az Élő irányítópulton a legutóbbi riasztások kis, ismétlődő videóként jelennek meg. Kapcsolja ki ezt az opciót, ha csak állóképet szeretne megjeleníteni a legutóbbi riasztásokról ezen az eszközön/böngészőben." + }, + "displayCameraNames": { + "label": "Mindig mutatja a kamera nevét" + }, + "liveFallbackTimeout": { + "desc": "Ha a kamera kiváló minőségű élő közvetítése nem elérhető, ennyi másodperc elteltével váltson alacsony sávszélességű módra. Alapértelmezett: 3." + } + }, + "title": "Alapbeállítások", + "cameraGroupStreaming": { + "title": "Kamera Csoport Adás Beállítások", + "clearAll": "Minden Adás Beállítás Törlése", + "desc": "Az egyes kameracsoportok közvetítési beállításai a böngésző helyi tárolójában kerülnek mentésre." + }, + "recordingsViewer": { + "title": "Felvétel Néző", + "defaultPlaybackRate": { + "label": "Alap Lejátszási Sebesség", + "desc": "Alapértelmezett lejátszási sebesség a felvételek visszajátszásához." + } + }, + "calendar": { + "title": "Naptár", + "firstWeekday": { + "label": "Első Hétköznap", + "sunday": "Vasárnap", + "monday": "Hétfő", + "desc": "A nap, amivel az áttekintési naptár hete kezdődik." + } + }, + "toast": { + "error": { + "clearStreamingSettingsFailed": "Adásbeállítások törlése sikertelen: {{errorMessage}}", + "clearStoredLayoutFailed": "Nem sikerült törölni a mentett elrendezést: {{errorMessage}}" + }, + "success": { + "clearStoredLayout": "Mentett elrendezés törölve ehhez: {{cameraName}}", + "clearStreamingSettings": "Az összes kameracsoport közvetítési beállítása törölve." + } + }, + "storedLayouts": { + "title": "Tárolt Elrendezések", + "desc": "A kameracsoporton belüli kamerák elrendezése áthúzható és átméretezhető. A pozíciók a böngésző helyi tárolójában kerülnek mentésre.", + "clearAll": "Összes elrendezés törlése" + } + }, + "enrichments": { + "semanticSearch": { + "readTheDocumentation": "Olvassa el a Dokumentációt", + "reindexNow": { + "error": "Újraindexálás megkezdése sikertelen: {{errorMessage}}", + "label": "Újraindexálás Most", + "confirmTitle": "Újraindexálás Megerősítése", + "confirmButton": "Újraindexálás", + "success": "Újraindexálás sikeresen megkezdődött.", + "alreadyInProgress": "Újraindexálás már folyamatban.", + "desc": "Az újraindexelés újragenerálja az összes követett objektum beágyazásait. Ez a folyamat a háttérben fut, és a követett objektumok számától függően maximálisan kihasználhatja a CPU-t, valamint jelentős időt vehet igénybe.", + "confirmDesc": "Biztosan újra szeretné indexelni az összes követett objektum beágyazását? Ez a folyamat a háttérben fut, de maximálisan kihasználhatja a CPU-t, és jelentős időt vehet igénybe. A folyamat állapotát az Áttekintés oldalon követheti nyomon." + }, + "modelSize": { + "label": "Modell méret", + "small": { + "title": "kicsi", + "desc": "A small használata egy kvantált modellverziót alkalmaz, amely kevesebb memóriát használ és gyorsabban fut a CPU-n, miközben a beágyazás minősége alig észrevehető mértékben változik." + }, + "large": { + "title": "nagy", + "desc": "A large használata a teljes Jina modellt alkalmazza, és automatikusan a GPU-n futtatja, ha ez elérhető." + }, + "desc": "A szemantikus keresés beágyazásaihoz használt modell mérete." + }, + "title": "Szemantikus keresés", + "desc": "A Frigate szemantikus keresés funkciója lehetővé teszi, hogy a felülvizsgálati elemekben szereplő követett objektumokat megtalálja akár magával a képpel, egy felhasználó által megadott szöveges leírással, vagy egy automatikusan generált leírással." + }, + "birdClassification": { + "title": "Madár Osztályozás", + "desc": "A madárfelismerés egy kvantált Tensorflow modell segítségével azonosít ismert madarakat. Ha egy ismert madarat felismer, annak közismert neve sub_label jelölésként kerül hozzáadásra. Ez az információ megjelenik a felhasználói felületen, a szűrőkben, valamint az értesítésekben is." + }, + "faceRecognition": { + "title": "Arcfelismerés", + "readTheDocumentation": "Olvassa el a Dokumentációt", + "modelSize": { + "label": "Modell Mérete", + "desc": "Az arcfelismeréshez használt modell mérete.", + "small": { + "title": "kis", + "desc": "A small használata egy FaceNet arcbeágyazási modellt alkalmaz, amely a legtöbb CPU-n hatékonyan fut." + }, + "large": { + "title": "nagy", + "desc": "A large használata egy ArcFace arcbeágyazási modellt alkalmaz, és automatikusan a GPU-n fut, ha az elérhető." + } + }, + "desc": "Az arcfelismerés lehetővé teszi, hogy személyekhez neveket rendeljenek, és amikor az arcukat felismeri a Frigate, akkor a személy nevét alcímkeként rendeli hozzá. Ez az információ megjelenik a felhasználói felületen, a szűrőkben és az értesítésekben is." + }, + "licensePlateRecognition": { + "title": "Rendszámtábla Felismerés", + "readTheDocumentation": "Olvassa el a Dokumentációt", + "desc": "A Frigate képes felismerni a járművek rendszámtábláit, és automatikusan hozzáadja a felismert karaktereket a recognized_license_plate mezőhöz, vagy egy ismert nevet al_címkeként rendel az autótípusú objektumokhoz. Egy gyakori felhasználási eset lehet a bejáróra behajtó vagy az utcán elhaladó autók rendszámtáblájának olvasása." + }, + "toast": { + "error": "Konfigurációs változtatások mentése sikertelen: {{errorMessage}}", + "success": "A kiegészítő beállítások elmentésre kerültek. A módosítások alkalmazásához indítsa újra a Frigate-et." + }, + "unsavedChanges": "Mentetlen gazdagítási beállítás változtatások", + "title": "Kiegészítők Beállítása", + "restart_required": "Újraindítás szükséges (a kiegészítő beállítások megváltoztak)" + }, + "notification": { + "title": "Értesítések", + "notificationSettings": { + "documentation": "Olvassa el a Dokumentációt", + "title": "Értesítési Beállítások", + "desc": "A Frigate natívan képes push értesítéseket küldeni az eszközére, ha böngészőben fut vagy PWA-ként van telepítve." + }, + "globalSettings": { + "title": "Globális Beállítások", + "desc": "Ideiglenesen függessze fel az értesítéseket meghatározott kamerákhoz az összes regisztrált eszközön." + }, + "email": { + "title": "E-mail", + "placeholder": "Példa example@email.com", + "desc": "Érvényes e-mail cím szükséges, amelyre értesítést küldünk, ha problémák adódnak a push szolgáltatással." + }, + "suspended": "Értesítések felfüggesztve {{time}}", + "active": "Értesítések Bekapcsolva", + "suspendTime": { + "5minutes": "Felfüggesztés 5 percre", + "24hours": "Felfüggesztés 24 órára", + "suspend": "Felfüggeszt", + "12hours": "Felfüggesztés 12 órára", + "10minutes": "Felfüggesztés 10 percre", + "1hour": "Felfüggesztés 1 órára", + "30minutes": "Felfüggesztés 30 percre", + "untilRestart": "Felfüggesztés újraindításig" + }, + "toast": { + "success": { + "settingSaved": "Értesítési beállítások elmentve.", + "registered": "Sikeresen regisztrált az értesítések fogadására. Az értesítések (beleértve a tesztértesítést is) küldése előtt újra kell indítani a Frigate-et." + }, + "error": { + "registerFailed": "Nem sikerült elmenteni az értesítési regisztrációt." + } + }, + "notificationUnavailable": { + "title": "Értesítés elérhetetlen", + "documentation": "Olvassa el a Dokumentációt", + "desc": "A webes push értesítésekhez biztonságos környezet (https://…) szükséges. Ez egy böngészői korlátozás. A értesítések használatához férjen hozzá biztonságosan a Frigate-hez." + }, + "sendTestNotification": "Teszt értesítés küldése", + "deviceSpecific": "Eszköz Specifikus Beállítások", + "cameras": { + "desc": "Válassza ki mely kamerákhoz engedélyezi az értesítéseket.", + "noCameras": "Nincs elérhető kamera", + "title": "Kamerák" + }, + "unsavedChanges": "Mentetlen Értesítés változtatások", + "cancelSuspension": "Felfüggesztés megszüntetése", + "registerDevice": "Regisztráld ezt az eszközt", + "unregisterDevice": "Eszköz regisztrációjának törlése", + "unsavedRegistrations": "Nem mentett értesítési regisztrációk" + }, + "frigatePlus": { + "modelInfo": { + "error": "Modell információ betöltése sikertelen", + "plusModelType": { + "baseModel": "Alap modell", + "userModel": "Finomhangolt" + }, + "supportedDetectors": "Támogatott Érzékelők", + "baseModel": "Alap modell", + "loadingAvailableModels": "Elérhető modellek betöltése…", + "loading": "Modell információ betöltése…", + "availableModels": "Elérhető modellek", + "modelType": "Modell típus", + "title": "Modell Információ", + "cameras": "Kamerák", + "trainDate": "Tanítás dátum", + "modelSelect": "A Frigate+-on elérhető modelljeit itt választhatja ki. Figyelem: csak az aktuális érzékelő konfigurációval kompatibilis modellek választhatók." + }, + "apiKey": { + "plusLink": "Tudjon meg többet a Frigate+-ról", + "title": "Frigate+ API kulcs", + "validated": "Frigate+ API kulcs észlelve és ellenőrizve", + "notValidated": "Frigate+ API kulcs nem található vagy nem ellenőrzött", + "desc": "A Frigate+ API kulcs lehetővé teszi az integrációt a Frigate+ szolgáltatással." + }, + "snapshotConfig": { + "title": "Pillanatkép Konfiguráció", + "table": { + "snapshots": "Pillanatképek", + "camera": "Kamera", + "cleanCopySnapshots": "clean_copy pillanatképek" + }, + "documentation": "Olvassa el a dokumentációt", + "desc": "A Frigate+ szolgáltatásba történő beküldéshez a konfigurációban engedélyezni kell a pillanatképeket és a clean_copy pillanatképeket is.", + "cleanCopyWarning": "Néhány kamerán engedélyezve vannak a pillanatképek, de a tiszta másolat ki van kapcsolva. Ahhoz, hogy ezekről a kamerákról képeket tudjon beküldeni a Frigate+ szolgáltatásba, engedélyeznie kell a clean_copy beállítást a pillanatkép konfigurációban." + }, + "restart_required": "Újraindítás szükséges ( Frigate+ modell megváltozott)", + "toast": { + "success": "Frigate+ beállítások elmentve. Indítsa újra a Frigate-et a változtatások érvényrejutásához.", + "error": "Konfigurációs módosítások mentése sikertelen: {{errorMessage}}" + }, + "title": "Frigate+ Beállítások", + "unsavedChanges": "Mentetlen Frigate+ beállítás változtatások" + }, + "users": { + "dialog": { + "changeRole": { + "roleInfo": { + "viewer": "Néző", + "admin": "Adminisztrátor", + "intro": "Válassza ki a megfelelő szerepkört ehhez a felhasználóhoz:", + "adminDesc": "Teljes hozzáférés az összes funkcióhoz.", + "viewerDesc": "Csak az Élő irányítópultokhoz, Ellenőrzéshez, Felfedezéshez és Exportokhoz korlátozva.", + "customDesc": "Egyéni szerepkör meghatározott kamerahozzáféréssel." + }, + "title": "Felhasználói szerepkör módosítása", + "select": "Válasszon szerepkört", + "desc": "Engedélyek frissítése {{username}} számára" + }, + "form": { + "user": { + "desc": "Csak betűk, számok, pontok és alulhúzások engedélyezettek.", + "title": "Felhasználói név", + "placeholder": "Adja meg a felhasználói nevet" + }, + "password": { + "match": "A jelszavak egyeznek", + "strength": { + "weak": "Gyenge", + "veryStrong": "Nagyon Erős", + "strong": "Erős", + "medium": "Közepes", + "title": "Jelszó erőssége: " + }, + "placeholder": "Adja meg a jelszót", + "title": "Jelszó", + "confirm": { + "placeholder": "Erősítse meg Jelszavát", + "title": "Erősítse meg Jelszavát" + }, + "notMatch": "A jelszavak nem egyeznek" + }, + "newPassword": { + "title": "Új Jelszó", + "placeholder": "Adjon meg egy új jelszót", + "confirm": { + "placeholder": "Adja meg ismét az új jelszót" + } + }, + "usernameIsRequired": "Felhasználói név szükséges", + "passwordIsRequired": "Jelszó szükséges" + }, + "createUser": { + "usernameOnlyInclude": "A felhasználói név csak betűket, számokat, .-t vagy _-t tartalmazhat", + "title": "Új Felhasználó Létrehozása", + "confirmPassword": "Erősítse meg a jelszavát", + "desc": "Új felhasználói fiók hozzáadása, és egy szerepkör megadása a Frigate felhasználói felületének területeihez való hozzáféréshez." + }, + "deleteUser": { + "title": "Felhasználó Törlése", + "warn": "Biztosan törölni akarja {{username}}-t?", + "desc": "Ez a művelet nem vonható vissza. Ez véglegesen törli a felhasználói fiókot és minden kapcsolódó adatot." + }, + "passwordSetting": { + "setPassword": "Jelszó Megadása", + "doNotMatch": "Jelszavak nem egyeznek", + "cannotBeEmpty": "A jelszó nem lehet üres", + "updatePassword": "{{username}} Jelszavának Módosítása", + "desc": "Hozzon létre erős jelszót a fiók védelméhez." + } + }, + "table": { + "noUsers": "Felhasználók nem találhatóak.", + "username": "Felhasználói név", + "password": "Jelszó", + "deleteUser": "Felhasználó törlése", + "actions": "Akciók", + "role": "Szerepkör", + "changeRole": "felhasználói szerepkör módosítása" + }, + "toast": { + "error": { + "setPasswordFailed": "Jelszó mentése sikertelen: {{errorMessage}}", + "deleteUserFailed": "Felhasználó törlése sikertelen: {{errorMessage}}", + "createUserFailed": "Felhasználó létrehozása sikertelen: {{errorMessage}}", + "roleUpdateFailed": "Nem sikerült frissíteni a szerepkört: {{errorMessage}}" + }, + "success": { + "updatePassword": "Jelszó sikeresen módosítva.", + "deleteUser": "{{user}} felhasználó sikeresen törölve", + "createUser": "{{user}} felhasználó sikeresen létrehozva", + "roleUpdated": "A szerepkör frissítve lett a(z) {{user}} számára" + } + }, + "addUser": "Felhasználó hozzáadása", + "updatePassword": "Jelszó Módosítása", + "management": { + "desc": "Ezen Frigate példány felhasználóinak kezelése.", + "title": "Felhasználó kezelés" + }, + "title": "Felhasználók" + }, + "masksAndZones": { + "zones": { + "desc": { + "documentation": "Dokumentáció", + "title": "A zónák lehetővé teszik, hogy meghatározzon egy adott területet a képkockán belül, így eldöntheti, hogy egy objektum egy adott területen belül van-e vagy sem." + }, + "edit": "Zóne Módosítása", + "label": "Zónák", + "documentTitle": "Zóna Módosítása - Frigate", + "name": { + "title": "Név", + "tips": "A névnek legalább 2 karakterből kell állnia és nem a kamera neve vagy egy másik zónáé.", + "inputPlaceHolder": "Adjon meg egy nevet…" + }, + "objects": { + "title": "Tárgyak", + "desc": "Adott zónára vonatkozó tárgyak listája." + }, + "add": "Zóna Hozzáadása", + "allObjects": "Minden Tárgy", + "speedEstimation": { + "title": "Sebesség Becslés", + "docs": "Olvassa el a dokumentációt", + "lineADistance": "A vonal távsolság {{unit}}", + "lineBDistance": "B vonal távolsága {{unit}}", + "lineCDistance": "C vonal távolsága {{unit}}", + "lineDDistance": "D vonal távolsága {{unit}}", + "desc": "Engedélyezze a sebességbecslést a zónában lévő objektumokra. A zónának pontosan 4 pontból kell állnia." + }, + "speedThreshold": { + "title": "Sebesség Határérték {{unit}}", + "desc": "Megadja a minimális sebességet, amely felett az objektumokat ebben a zónában figyelembe kell venni.", + "toast": { + "error": { + "pointLengthError": "A sebességbecslés le lett tiltva ennél a zónánál. A sebességbecslést tartalmazó zónáknak pontosan 4 pontból kell állniuk.", + "loiteringTimeError": "Az olyan zónák, amelyeknél a tartózkodási idő nagyobb, mint 0, nem használhatók sebességbecsléssel együtt." + } + } + }, + "point_one": "{{count}} pont", + "point_other": "{{count}} pont", + "clickDrawPolygon": "Kattintson a sokszög rajzoláshoz a képen.", + "inertia": { + "title": "Inercia", + "desc": "Megadja, hogy hány képkockán keresztül kell egy objektumnak a zónában lennie, mielőtt azt a zónán belülinek tekintenénk. Alapértelmezett: 3" + }, + "loiteringTime": { + "title": "Lebzselési idő", + "desc": "Beállítja a zónában tartózkodás minimális idejét másodpercben, amelynek teljesülnie kell az aktiváláshoz. Alapértelmezett: 0" + }, + "toast": { + "success": "A(z) {{zoneName}} zóna elmentésre került. A módosítások alkalmazásához indítsa újra a Frigate-et." + } + }, + "objectMasks": { + "point_one": "{{count}} pont", + "point_other": "{{count}} pont", + "label": "Tárgymaszkok", + "desc": { + "documentation": "Dokumentáció", + "title": "Az objektumszűrő maszkokat arra használják, hogy hely alapján kiszűrjék a téves egyezéseket adott objektumtípus esetén." + }, + "documentTitle": "Tárgymaszk módosítása - Frigate", + "objects": { + "title": "Tárgyak", + "allObjectTypes": "Minden tárgytípus", + "desc": "Az objektumtípus, amelyre ez az objektummaszk vonatkozik." + }, + "edit": "Tárgymaszk módosítása", + "add": "Tárgymaszk hozzáadása", + "context": "Az objektumszűrő maszkokat arra használják, hogy hely alapján kiszűrjék az adott objektumtípus téves egyezéseit.", + "clickDrawPolygon": "Kattintson a képre, hogy poligont rajzoljon.", + "toast": { + "success": { + "title": "A(z) {{polygonName}} elmentve. A módosítások alkalmazásához indítsa újra a Frigate-et.", + "noName": "Az objektummaszk elmentésre került. A módosítások alkalmazásához indítsa újra a Frigate-et." + } + } + }, + "form": { + "polygonDrawing": { + "delete": { + "title": "Törlés Megerősítése", + "desc": "Biztosan törölni akarja {{type}}{{name}}-t?", + "success": "{{name}} törölve." + }, + "error": { + "mustBeFinished": "A sokszög rajzolásnak be kell fejeződnie mentés előtt." + }, + "removeLastPoint": "Utolsó pont törlése", + "reset": { + "label": "Minden pont törlése" + }, + "snapPoints": { + "true": "Képkivágási pontok", + "false": "Ne rögzítse a pontokat" + } + }, + "zoneName": { + "error": { + "hasIllegalCharacter": "A zóna neve olyan karaktert tartalmaz, ami nem megengedett.", + "mustBeAtLeastTwoCharacters": "A zóna neve legalább 2 karakterből kell, hogy álljon.", + "mustNotBeSameWithCamera": "A zóna neve nem egyezhet meg a kamera nevével.", + "alreadyExists": "Egy zóna ezen a néven már létezik ennél a kameránál.", + "mustNotContainPeriod": "A zóna neve nem tartalmazhat pontot." + } + }, + "distance": { + "error": { + "text": "A távolságnak nagyobb vagy egyenlőnek kell lennie, mint 0.1.", + "mustBeFilled": "A sebességbecslés használatához minden távolságmezőt ki kell tölteni." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "A tehetetlenségnek nagyobbnak kell lennie 0-nál." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "A tartózkodási időnek nagyobbnak vagy egyenlőnek kell lennie 0-hoz képest." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "A sebességküszöbnek nagyobbnak vagy egyenlőnek kell lennie 0.1-hez képest." + } + } + }, + "motionMasks": { + "desc": { + "documentation": "Dokumentáció", + "title": "A mozgásmaszkokat arra használják, hogy megakadályozzák a nem kívánt mozgástípusok észlelését. A túlzott maszkolás megnehezíti az objektumok követését." + }, + "context": { + "documentation": "Olvassa el a dokumentációt", + "title": "A mozgásmaszkokat arra használják, hogy megakadályozzák a nem kívánt mozgástípusok észlelését (például faágak, kamera időbélyegek). A mozgásmaszkokat nagyon megfontoltan kell alkalmazni, mert a túlzott maszkolás megnehezíti az objektumok követését." + }, + "edit": "Mozgási Maszk Módosítása", + "polygonAreaTooLarge": { + "documentation": "Olvassa el a dokumentációt", + "title": "A mozgásmaszk a kamera képkockájának {{polygonArea}}%-át fedi le. Nagy mozgásmaszkok használata nem ajánlott.", + "tips": "A mozgásmaszkok nem akadályozzák meg az objektumok észlelését. Ehelyett ajánlott kötelező zónát használni." + }, + "documentTitle": "Mozgási Maszk Módosítása - Frigate", + "add": "Új Mozgási Maszk", + "point_one": "{{count}} pont", + "point_other": "{{count}} pont", + "label": "Mozgási maszk", + "clickDrawPolygon": "Kattintson a sokszög rajzoláshoz a képre.", + "toast": { + "success": { + "title": "{{polygonName}} neve mentve. Indítsa újra a Frigate-et a módosítások érvényesítéséhez.", + "noName": "A mozgásmaszk elmentésre került. A módosítások alkalmazásához indítsa újra a Frigate-et." + } + } + }, + "restart_required": "Újraindítás szükséges (maszkok/zónák módosultak)", + "filter": { + "all": "Mindem Maszk és Zóna" + }, + "motionMaskLabel": "Mozgási Maszk {{number}}", + "objectMaskLabel": "Tárgy Maszk {{number}}", + "toast": { + "success": { + "copyCoordinates": "A {{polyName}} koordinátái vágólapra másolva." + }, + "error": { + "copyCoordinatesFailed": "Nem sikerült a koordinátákat a vágólapra másolni." + } + } + }, + "debug": { + "objectList": "Tárgy lista", + "debugging": "Hibakeresés", + "motion": { + "desc": "Mutasd a dobozokat azon területek körül, ahol mozgás észlelés történt", + "title": "Mozgáskeretek", + "tips": "

    Mozgáskeretek


    Piros keretek jelennek meg a képkocka azon területein, ahol jelenleg mozgást észlelhető.

    " + }, + "zones": { + "title": "Zónák", + "desc": "Mutassa a meghatározott zónák körvonalát" + }, + "objectShapeFilterDrawing": { + "score": "Pontszám", + "document": "Olvassa el a dokumentációt ", + "area": "Terület", + "title": "Objektum alak szűrő rajzolás", + "desc": "Rajzoljon egy téglalapot a képre a terület- és arányadatok megtekintéséhez", + "tips": "Engedélyezze ezt az opciót, hogy téglalapot rajzoljon a kameraképen, amely megmutatja a területét és az arányát. Ezek az értékek ezután felhasználhatók az objektumalak szűrő paramétereinek beállításához a konfigurációban.", + "ratio": "Arány" + }, + "timestamp": { + "title": "Időbélyeg", + "desc": "Időbélyeg megjelenítése a képen" + }, + "title": "Hibakeresés", + "noObjects": "Nincs tárgy", + "detectorDesc": "A Frigate a(z) {{detectors}} érzékelőit használja az objektumok észlelésére a kamera videófolyamában.", + "desc": "A hibakereső nézet valós idejű képet mutat a követett objektumokról és azok statisztikáiról. Az objektumlista késleltetett összefoglalót ad az észlelt objektumokról.", + "boundingBoxes": { + "title": "Határoló keretek", + "desc": "Mutassa a határoló kereteket a követett objektumok körül", + "colors": { + "label": "Határolókeret színek", + "info": "
  • Indításkor minden objektumcímkéhez más-más szín kerül hozzárendelésre
  • A sötétkék vékony vonal azt jelzi, hogy az objektum ebben a pillanatban nincs észlelve
  • A szürke vékony vonal azt jelzi, hogy az objektum állónak van észlelve
  • A vastag vonal azt jelzi, hogy az objektum az automatikus követés tárgya (ha engedélyezve van)
  • " + } + }, + "mask": { + "title": "Mozgásmaszkok", + "desc": "Mozgásmaszk poligonok megjelenítése" + }, + "regions": { + "title": "Régiók", + "desc": "Mutassa a célterület keretét, amelyet az objektumérzékelőhöz küldenek", + "tips": "

    Célterület keretek


    Világoszöld keretek jelennek meg a képkocka azon területein, amelyek az objektumérzékelőnek elküldésre kerülnek.

    " + }, + "paths": { + "title": "Útvonalak", + "desc": "A követett objektum útvonalához tartozó jelentősebb pontok megjelenítése", + "tips": "

    Útvonalak


    A vonalak és körök jelzik a követett objektum életciklusa során érintett jelentősebb pontokat.

    " + }, + "openCameraWebUI": "Nyissa meg a {{camera}} webes felületét", + "audio": { + "title": "Hang", + "noAudioDetections": "Nincs hangérzékelés", + "score": "pontszám", + "currentRMS": "Aktuális effektív érték", + "currentdbFS": "Aktuális dbFS" + } + }, + "motionDetectionTuner": { + "toast": { + "success": "Mozgási beállítások mentve." + }, + "improveContrast": { + "desc": "Növelje a kontrasztot a sötétebb jeleneteknél. Alapbeállítás: BE", + "title": "Kontraszt Növelése" + }, + "Threshold": { + "title": "Határérték", + "desc": "A küszöbérték határozza meg, hogy egy pixel fényességének mekkora változása szükséges ahhoz, hogy mozgásnak minősüljön. Alapértelmezett: 30" + }, + "title": "Mozgásérzékelő hangoló", + "unsavedChanges": "Nem mentett mozgásérzékelő hangolási módosítások ({{camera}})", + "desc": { + "title": "A Frigate a mozgásérzékelést elsődleges ellenőrzésként használja, hogy megállapítsa, van-e a képkockában olyan esemény, amely érdemes az objektumfelismeréssel való vizsgálatra.", + "documentation": "Olvassa el a mozgásérzékelő hangolási útmutatót" + }, + "contourArea": { + "title": "Kontúrterület", + "desc": "A kontúrterület értékét arra használják, hogy eldöntsék, melyik megváltozott pixelekből álló csoport minősül mozgásnak. Alapértelmezett: 10" + } + }, + "camera": { + "streams": { + "title": "Adások", + "desc": "Ideiglenesen tiltsa le a kamerát a Frigate újraindításáig. A kamera teljes letiltása megállítja a Frigate feldolgozását az adott kamera közvetítésénél. Az észlelés, rögzítés és hibakeresés nem lesz elérhető.
    Megjegyzés: Ez nem tiltja le a go2rtc újraközvetítéseket." + }, + "review": { + "title": "Áttekintés", + "alerts": "Riasztások ", + "detections": "Érzékelések ", + "desc": "Ideiglenesen engedélyezze/tiltsa le a riasztásokat és észleléseket ennél a kameránál a Frigate újraindításáig. Letiltás esetén nem keletkeznek új ellenőrzési elemek. " + }, + "reviewClassification": { + "readTheDocumentation": "Olvassa el a Dokumentációt", + "title": "Attekintés Osztályozás", + "noDefinedZones": "Nincs zóna meghatározva ehhez a kamerához.", + "objectAlertsTips": "Minden {{alertsLabels}} tárgy a {{cameraName}} -n Riasztásként fog megjelenni.", + "selectDetectionsZones": "Válassza ki a zónákat az Észleléshez", + "limitDetections": "Korlátozza az észleléseket adott zónákra", + "selectAlertsZones": "Válassza ki a zónákat a Riasztásokhoz", + "zoneObjectAlertsTips": "Minden {{alertsLabels}}tárgy észlelés {{zone}}-ban/-ben a {{cameraName}}-n Riasztásként lesz megjelenítve.", + "objectDetectionsTips": "Minden {{detectionsLabels}} tárgy, ami nincs bekategorizálva a {{cameraName}}-n Észleléskén lesz megjelenítve függetlenül attól, hogy melyik zónában vannak.", + "desc": "A Frigate az ellenőrzési elemeket Riasztásokra és Észlelésekre osztja. Alapértelmezés szerint az összes személy és autó objektum Riasztásnak számít. Az ellenőrzési elemek kategorizálását finomíthatja azzal, hogy megadja az észleléshez szükséges zónákat.", + "zoneObjectDetectionsTips": { + "text": "Az összes {{detectionsLabels}} objektum, amely nincs kategorizálva a(z) {{zone}} zónában a(z) {{cameraName}} kamerán, Észlelésként lesz megjelenítve.", + "notSelectDetections": "Az összes {{detectionsLabels}} objektum, amelyet a(z) {{zone}} zónában észleltek a(z) {{cameraName}} kamerán, és amely nem lett Riasztásként kategorizálva, Észlelésként lesz megjelenítve, függetlenül attól, hogy melyik zónában vannak.", + "regardlessOfZoneObjectDetectionsTips": "Az összes {{detectionsLabels}} objektum, amely nincs kategorizálva a(z) {{cameraName}} kamerán, Észlelésként lesz megjelenítve zónától függetlenül." + }, + "unsavedChanges": "Nem mentett Ellenőrzési Kategorizálási beállítások a(z) {{camera}} kamerához", + "toast": { + "success": "Az Ellenőrzési Kategorizálás beállításai elmentésre kerültek. A módosítások alkalmazásához indítsa újra a Frigate-et." + } + }, + "title": "Kamera Beállítások", + "object_descriptions": { + "title": "Generatív AI Tárgy Leírások", + "desc": "Ideiglenesen engedélyezze/tiltsa le a generatív AI objektumleírásokat ehhez a kamerához. Letiltás esetén a rendszer nem kéri le a mesterséges intelligencia által generált leírásokat a kamerán követett objektumokhoz." + }, + "addCamera": "Új Kamera Hozzáadása", + "editCamera": "Kamera Szerkesztése:", + "selectCamera": "Válasszon ki egy Kamerát", + "backToSettings": "Vissza a Kamera Beállításokhoz", + "cameraConfig": { + "add": "Kamera Hozzáadása", + "edit": "Kamera Szerkesztése", + "name": "Kamera Neve", + "nameRequired": "Kamera nevének megadása szükséges", + "description": "Konfigurálja a kamera beállításait, beleértve a stream bemeneteket és szerepeket.", + "nameInvalid": "A kamera neve csak betűket, számokat, aláhúzásjeleket vagy kötőjeleket tartalmazhat", + "namePlaceholder": "pl: bejarati_ajto", + "enabled": "Engedélyezve", + "ffmpeg": { + "inputs": "Bemeneti Adatfolyamok", + "path": "Adatfolyam útvonal", + "pathRequired": "Adatfolyam útvonal szükséges", + "pathPlaceholder": "rtsp://...", + "roles": "Szerepkörök", + "rolesRequired": "Legalább egy szerepkör megadása kötelező", + "rolesUnique": "Minden szerepkör (hang, érzékelés, rögzítés) csak egy adatfolyamhoz rendelhető hozzá", + "addInput": "Bejövő Adatfolyam Hozzáadása", + "removeInput": "Bejövő Adatfolyam Eltávolítása", + "inputsRequired": "Legalább egy bemeneti adatfolyam szükséges" + }, + "toast": { + "success": "A következő kamera sikeresen mentve: {{cameraName}}" + }, + "nameLength": "A kamera nevének kevesebbnek kell lennie 24 karakternél." + }, + "review_descriptions": { + "title": "Generatív MI Áttekintési Leírások", + "desc": "Ideiglenesen engedélyezze/tiltsa le a generatív mesterséges intelligencia által generált leírásokat ehhez a kamerához. Letiltás esetén a mesterséges intelligencia által generált leírások nem lesznek lekérve a kamerán található elemhez." + } + }, + "triggers": { + "documentTitle": "Trigger-ek", + "management": { + "title": "Triggerek kezelése", + "desc": "A(z) {{camera}} nevű kamera triggereinek kezelése. A bélyegkép típussal a kiválasztott követett objektumhoz hasonló bélyegképekre, a leírás típussal pedig a megadott szöveghez hasonló leírásokra aktiválhatja a funkciót." + }, + "addTrigger": "Trigger hozzáadása", + "table": { + "name": "Név", + "type": "Típus", + "content": "Tartalom", + "threshold": "Határérték", + "actions": "Akciók", + "noTriggers": "Nincsenek konfigurált triggerek ehhez a kamerához.", + "edit": "Szerkesztés", + "deleteTrigger": "Trigger törlése", + "lastTriggered": "Utoljára triggerelve" + }, + "type": { + "thumbnail": "Bélyegkép", + "description": "Leírás" + }, + "actions": { + "alert": "Megjelölés Riasztásként", + "notification": "Értesítés küldése" + }, + "dialog": { + "createTrigger": { + "title": "Trigger létrehozása", + "desc": "Hozz létre egy triggert a(z) {{camera}} kamerához" + }, + "editTrigger": { + "title": "Trigger Szerkesztése", + "desc": "Trigger beállítások szerkesztése a következő kamerán: {{camera}}" + }, + "deleteTrigger": { + "title": "Trigger Törlése", + "desc": "Biztosan törölni szeretné a(z){{triggerName}} triggert? Ez a művelet nem vonható vissza." + }, + "form": { + "name": { + "title": "Név", + "placeholder": "Add meg a trigger nevét", + "error": { + "minLength": "A névnek minimum 2 karakter hosszúnak kell lennie.", + "invalidCharacters": "A név csak betűket, számokat, aláhúzásjeleket és kötőjeleket tartalmazhat.", + "alreadyExists": "Már létezik egy ilyen nevű trigger ehhez a kamerához." + } + }, + "enabled": { + "description": "Engedélyezze vagy tiltsa le ezt a triggert" + }, + "type": { + "title": "Típus", + "placeholder": "Válaszd ki a trigger típusát" + }, + "content": { + "title": "Tartalom", + "imagePlaceholder": "Válassz egy képet", + "textPlaceholder": "Írja be a szöveges tartalmat", + "imageDesc": "Válasszon ki egy képet, amely aktiválja ezt a műveletet, amikor a rendszer hasonló képet észlel.", + "textDesc": "Írjon be egy szöveget, amely aktiválja ezt a műveletet, amikor a rendszer hasonló követett objektumleírást észlel.", + "error": { + "required": "Tartalom megadása kötelező." + } + }, + "threshold": { + "title": "Határérték", + "error": { + "min": "A határértéknek 0-nál nagyobbnak kell lennie", + "max": "A határérték legfeljebb 1 lehet" + } + }, + "actions": { + "title": "Akciók", + "desc": "Alapértelmezés szerint a Frigate minden trigger esetén MQTT üzenetet küld. Válasszon ki egy további műveletet, amelyet a trigger aktiválásakor végre kell hajtani.", + "error": { + "min": "Legalább egy műveletet ki kell választani." + } + }, + "friendly_name": { + "title": "Barátságos név", + "placeholder": "Nevezd meg vagy írd le ezt a triggert", + "description": "Egy opcionális felhasználóbarát név vagy leíró szöveg ehhez az eseményindítóhoz." + } + } + }, + "toast": { + "success": { + "createTrigger": "A trigger sikeresen létrehozva: {{name}}.", + "updateTrigger": "A trigger sikeresen módosítva: {{name}}.", + "deleteTrigger": "A trigger sikeresen törölve: {{name}}." + }, + "error": { + "createTriggerFailed": "A trigger létrehozása sikertelen: {{errorMessage}}", + "updateTriggerFailed": "A trigger módosítása sikertelen: {{errorMessage}}", + "deleteTriggerFailed": "A trigger törlése sikertelen: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Szemantikus keresés le van tiltva", + "desc": "A Triggerek használatához engedélyezni kell a szemantikus keresést." + }, + "wizard": { + "steps": { + "nameAndType": "Név és típus", + "configureData": "Configurációs adatok" + } + } + }, + "roles": { + "management": { + "title": "Megtekintői szerepkör-kezelés", + "desc": "Kezelje az egyéni nézői szerepköröket és a kamera-hozzáférési engedélyeiket ehhez a Frigate-példányhoz." + }, + "addRole": "Szerepkör hozzáadása", + "table": { + "role": "Szerepkör", + "cameras": "Kamerák", + "actions": "Akciók", + "noRoles": "Nem találhatók egyéni szerepkörök.", + "editCameras": "Kamerák módosítása", + "deleteRole": "Szerepkör törlése" + }, + "toast": { + "success": { + "createRole": "Szerepkör létrehozva: {{role}}", + "updateCameras": "Kamerák frissítve a szerepkörhöz: {{role}}", + "deleteRole": "Szerepkör sikeresen törölve: {{role}}", + "userRolesUpdated_one": "{{count}} felhasználó, akit ehhez a szerepkörhöz rendeltünk, frissült „néző”-re, amely hozzáféréssel rendelkezik az összes kamerához.", + "userRolesUpdated_other": "{{count}} felhasználó, akit ehhez a szerepkörhöz rendeltünk, frissült „néző”-re, amely hozzáféréssel rendelkezik az összes kamerához." + }, + "error": { + "createRoleFailed": "Nem sikerült létrehozni a szerepkört: {{errorMessage}}", + "updateCamerasFailed": "Nem sikerült frissíteni a kamerákat: {{errorMessage}}", + "deleteRoleFailed": "Nem sikerült törölni a szerepkört: {{errorMessage}}", + "userUpdateFailed": "Nem sikerült frissíteni a felhasználói szerepköröket: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Új szerepkör létrehozása", + "desc": "Adjon hozzá egy új szerepkört, és adja meg a kamera hozzáférési engedélyeit." + }, + "editCameras": { + "title": "Szerepkör kamerák szerkesztése", + "desc": "Frissítse a kamerahozzáférést a(z) {{role}} szerepkörhöz." + }, + "deleteRole": { + "title": "Szerepkör törlése", + "desc": "Ez a művelet nem vonható vissza. Ez véglegesen törli a szerepkört, és az ezzel a szerepkörrel rendelkező összes felhasználót a „megtekintő” szerepkörhöz rendeli, amivel a megtekintő hozzáférhet az összes kamerához.", + "warn": "Biztosan törölni szeretnéd a(z) {{role}} szerepkört?", + "deleting": "Törlés..." + }, + "form": { + "role": { + "title": "Szerepkör neve", + "placeholder": "Adja meg a szerepkör nevét", + "desc": "Csak betűk, számok, pontok és aláhúzásjelek engedélyezettek.", + "roleIsRequired": "A szerepkör nevének megadása kötelező", + "roleOnlyInclude": "A szerepkör neve csak betűket, számokat , . vagy _ karaktereket tartalmazhat", + "roleExists": "Már létezik egy ilyen nevű szerepkör." + }, + "cameras": { + "title": "Kamerák", + "desc": "Válassza ki azokat a kamerákat, amelyekhez ennek a szerepkörnek hozzáférése van. Legalább egy kamera megadása szükséges.", + "required": "Legalább egy kamerát ki kell választani." + } + } + } + }, + "cameraWizard": { + "title": "Kamera hozzáadása", + "description": "Kövesse az alábbi lépéseket, hogy új kamerát adjon hozzá a Frigate telepítéséhez.", + "steps": { + "nameAndConnection": "Név & adatkapcsolat", + "streamConfiguration": "Stream beállítások", + "validationAndTesting": "Validálás és tesztelés" + } + }, + "button": { + "overriddenGlobal": "Felülírt (Globális)", + "overriddenGlobalTooltip": "Ez a kamera felülírja a globális konfigurációs beállításokat ebben a részben", + "overriddenBaseConfig": "Felülírt (Alapbeállítás)", + "overriddenBaseConfigTooltip": "A {{profile}} profil felülírja a konfigurációs beállításokat ebben a részben" + }, + "detectionModel": { + "plusActive": { + "label": "A jelenlegi modell forrása" + } + } +} diff --git a/web/public/locales/hu/views/system.json b/web/public/locales/hu/views/system.json new file mode 100644 index 0000000..f922f61 --- /dev/null +++ b/web/public/locales/hu/views/system.json @@ -0,0 +1,213 @@ +{ + "documentTitle": { + "cameras": "Kamera statisztikák - Frigate", + "storage": "Tárhely statisztikák - Frigate", + "general": "Általános Statisztikák - Frigate", + "logs": { + "frigate": "Frigate naplók - Frigate", + "go2rtc": "Go2RTC naplók - Frigate", + "nginx": "Nginx naplók - Frigate", + "websocket": "Üzenet naplók - Frigate" + }, + "enrichments": "Kiegészítés statisztikák - Frigate" + }, + "cameras": { + "label": { + "ffmpeg": "FFmpeg", + "overallSkippedDetectionsPerSecond": "összes kihagyott észlelés per másodperc", + "camera": "kamera", + "detect": "észlel", + "skipped": "kihagyott", + "overallFramesPerSecond": "összes képkocka per másodperc", + "overallDetectionsPerSecond": "összes észlelés per másodperc", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraDetect": "{{camName}} észlelés", + "cameraFramesPerSecond": "{{camName}} képkocka per másodperc", + "cameraDetectionsPerSecond": "{{camName}} észlelés per másodperc", + "cameraSkippedDetectionsPerSecond": "{{camName}} kihagyott észlelés per másodperc", + "cameraCapture": "{{camName}} megszerzése", + "capture": "megszerez" + }, + "title": "Kamerák", + "info": { + "video": "Videó:", + "resolution": "Felbontás:", + "codec": "Codec:", + "error": "Hiba: {{error}}", + "cameraProbeInfo": "{{camera}} Kamera Szonda Infó", + "fetching": "Kamera Adat Begyűjtése", + "aspectRatio": "képarány", + "stream": "Folyam: {{idx}}", + "fps": "FPS:", + "unknown": "Ismeretlen", + "audio": "Hang:", + "tips": { + "title": "Kamera Szonda Infó" + }, + "streamDataFromFFPROBE": "Az adás adat begyűjtése ffprobe-bal." + }, + "overview": "Áttekintés", + "framesAndDetections": "Képek / Észlelések", + "toast": { + "success": { + "copyToClipboard": "Szonda adat másolva a vágólapra." + }, + "error": { + "unableToProbeCamera": "Nem sikerült felderíteni a kamerát: {{errorMessage}}" + } + } + }, + "title": "Rendszer", + "logs": { + "copy": { + "label": "Másolás a Vágólapra", + "error": "Nem sikerült a naplók vágólapra másolása", + "success": "Naplók a vágólapra másolva" + }, + "type": { + "label": "Típus", + "timestamp": "Időbélyeg", + "tag": "Címke", + "message": "Üzenet" + }, + "toast": { + "error": { + "fetchingLogsFailed": "Hiba a naplók begyűjtése közben: {{errorMessage}}", + "whileStreamingLogs": "Hiba a naplók bekérésekor: {{errorMessage}}" + } + }, + "download": { + "label": "Naplók letöltése" + }, + "tips": "A naplók a szerverről érkeznek", + "websocket": { + "label": "Üzenetek", + "pause": "Szüneteltetés", + "resume": "Folytatás", + "filter": { + "all": "Összes téma (topic)", + "topics": "Témák (topics)", + "events": "Események", + "reviews": "Értékelések (reviews)", + "face_recognition": "Arcfelismerés", + "classification": "Osztályozás", + "lpr": "LPR" + }, + "clear": "Töröl" + } + }, + "general": { + "title": "Általános", + "detector": { + "title": "Érzékelők", + "inferenceSpeed": "Érzékelők Inferencia Sebessége", + "cpuUsage": "Érzékelő CPU Kihasználtság", + "memoryUsage": "Érzékelő Memória Kihasználtság", + "temperature": "Érzékelő Hőmérséklete", + "cpuUsageInformation": "A detektálási modellekbe érkező és onnan távozó bemeneti és kimeneti adatok előkészítéséhez használt CPU. Ez az érték nem méri a következtetési kihasználtságot, még GPU vagy gyorsító használata esetén sem." + }, + "hardwareInfo": { + "title": "Hardver Infó", + "gpuUsage": "GPU Kihasználtság", + "gpuMemory": "GPU Memória", + "gpuInfo": { + "nvidiaSMIOutput": { + "name": "Név: {{name}}", + "title": "Nvidia SMI Kimenet", + "vbios": "VBios Infó: {{vbios}}", + "driver": "Meghajtó: {{driver}}", + "cudaComputerCapability": "CUDA Számítási Képesség: {{cuda_compute}}" + }, + "vainfoOutput": { + "processOutput": "Folyamat Kimenete:", + "processError": "Folyamat Hiba:", + "title": "Vainfo Kimenet", + "returnCode": "Visszatérési Érték: {{code}}" + }, + "copyInfo": { + "label": "GPU infó másolása" + }, + "closeInfo": { + "label": "GPU info bezárása" + }, + "toast": { + "success": "GPU infó a vágólapra másolva" + } + }, + "gpuEncoder": "GPU Enkóder", + "gpuDecoder": "GPU Dekóder", + "npuUsage": "NPU Kihasználtság", + "npuMemory": "NPU Memória", + "intelGpuWarning": { + "message": "GPU statisztika nem érhető el" + } + }, + "otherProcesses": { + "processMemoryUsage": "Folyamat Memória Kihasználtság", + "title": "Egyéb Folyamatok", + "processCpuUsage": "Folyamat CPU Kihasználtság", + "series": { + "go2rtc": "go2rtc", + "recording": "felvétel" + } + } + }, + "storage": { + "recordings": { + "title": "Felvételek", + "tips": "Ez az érték mutatja a Frigate adatbázisában található felvételek teljes tárhely felhasználását. A Frigate nem követi az összes fájl által foglalt tárhelyet.", + "earliestRecording": "Legkorábbi elérhető felvétel:" + }, + "title": "Tárhely", + "overview": "Áttekintés", + "cameraStorage": { + "unusedStorageInformation": "Felhasználatlan Tárhely Információ", + "title": "Kamera Tárhely", + "camera": "Kamera", + "storageUsed": "Tárhely", + "percentageOfTotalUsed": "Teljes Százaléka", + "bandwidth": "Sávszélesség", + "unused": { + "title": "Felhasználatlan", + "tips": "Ez az érték nem feltétlenül tükrözi pontosan a Frigate számára elérhető szabad helyet, ha a meghajtón egyéb fájlok is tárolva vannak a Frigate felvételein kívül. A Frigate nem követi a tárhelyhasználatot a saját felvételein kívül." + } + }, + "shm": { + "title": "SHM (megosztott memória) kiosztás", + "warning": "A jelenlegi SHM mérete ({{total}}MB) túl kicsi. Növeld meg minimum ennyivel: {{min_shm}}MB." + } + }, + "enrichments": { + "embeddings": { + "image_embedding_speed": "Kép Beágyazás Sebesség", + "face_embedding_speed": "Arc Beágyazás Sebesség", + "face_recognition_speed": "Arcfelismerés Sebesség", + "plate_recognition_speed": "Rendszám Felismerés Sebesség", + "text_embedding_speed": "Szöveg Beágyazás Sebesség", + "image_embedding": "Kép Beágyazása", + "text_embedding": "Szöveg Beágyazása", + "face_recognition": "Arcfelismerés", + "plate_recognition": "Rendszám Felismerés", + "yolov9_plate_detection_speed": "YOLOv9 Rendszám Felismerés Sebesség", + "yolov9_plate_detection": "YOLOv9 Rendszám Észlelés", + "classification_events_per_second": "{{name}} Osztályozási események másodpercenként", + "classification_speed": "{{name}} Osztályozási sebesség", + "classification": "{{name}} Osztályozás", + "object_description_events_per_second": "Objektum leírás" + }, + "infPerSecond": "Inferencia Per Másodperc", + "title": "Kiegészítések" + }, + "metrics": "Rendszer jellemzők", + "stats": { + "detectIsVerySlow": "{{detect}} nagyon lassú ({{speed}} ms)", + "healthy": "A rendszer egészséges", + "cameraIsOffline": "{{camera}} nem elérhető", + "detectIsSlow": "{{detect}} lassú ({{speed}} ms)", + "reindexingEmbeddings": "Beágyazások újra indexelése ({{processed}}% kész)", + "ffmpegHighCpuUsage": "{{camera}}-nak/-nek magas FFmpeg CPU felhasználása ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "A(z) {{camera}} kameránál magas az észlelési CPU-használat ({{detectAvg}}%)", + "shmTooLow": "A /dev/shm részére foglalt területet ({{total}} MB) legalább {{min}} MB-ra kell növelni." + }, + "lastRefreshed": "Utoljára frissítve: " +} diff --git a/web/public/locales/hy/audio.json b/web/public/locales/hy/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/audio.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/common.json b/web/public/locales/hy/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/common.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/components/auth.json b/web/public/locales/hy/components/auth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/components/camera.json b/web/public/locales/hy/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/components/dialog.json b/web/public/locales/hy/components/dialog.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/components/filter.json b/web/public/locales/hy/components/filter.json new file mode 100644 index 0000000..eb78edb --- /dev/null +++ b/web/public/locales/hy/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Ֆիլտր", + "labels": { + "label": "Պիտակներ", + "all": { + "title": "Բոլոր պիտակները", + "short": "Պիտակներ" + }, + "count_one": "{{count}} պիտակ", + "count_other": "{{count}} պիտակ" + }, + "zones": { + "label": "Գոտիներ", + "all": { + "title": "Բոլոր գոտիները", + "short": "Գոտիներ" + } + }, + "dates": { + "selectPreset": "Ընտրել…", + "all": { + "title": "Բոլոր ամսաթվերը", + "short": "Ամսաթվեր" + } + }, + "more": "Ավելի շատ ֆիլտրեր", + "reset": { + "label": "Վերականգնել ֆիլտրերը լռելյայն արժեքներին" + }, + "timeRange": "Ժամաին միջակայք", + "subLabels": { + "label": "Ենթապիտակները", + "all": "Բոլոր ենթապիտակները" + }, + "attributes": { + "label": "Դասակարգման ատրիբուտներ", + "all": "Բոլոր ատրիբուտները" + }, + "score": "Միավոր", + "estimatedSpeed": "Մոտավոր արագություն ({{unit}})", + "features": { + "label": "Հատկանիշներ", + "hasSnapshot": "Ունի snapshot", + "hasVideoClip": "Ունի տեսահոլովակ", + "submittedToFrigatePlus": { + "label": "Ներկայացվել է Frigate+-ին", + "tips": "Դուք նախ պետք է զտեք այն հետևված օբյեկտները, որոնք ունեն լուսանկար։

    Հետևված օբյեկտները, որոնք չունեն լուսանկար, չեն կարող ուղարկվել Frigate+-ին։" + } + }, + "sort": { + "label": "Դասավորել", + "dateAsc": "Ամսաթիվ (աճման կարգով)", + "dateDesc": "Ամսաթիվ (Նվազման կարգով)", + "scoreAsc": "Օբյեկտի գնահատական (աճման կարգով)", + "scoreDesc": "Օբյեկտի գնահատական (նվազման կարգով)", + "speedAsc": "Մոտավոր արագություն (աճման կարգով)", + "speedDesc": "Մոտավոր արագություն (նվազման կարգով)", + "relevance": "Համապատասխանություն" + }, + "cameras": { + "label": "Տեսախցիկների ֆիլտր", + "all": { + "title": "Բոլոր տեսախցիկները", + "short": "Տեսախցիկներ" + } + }, + "review": { + "showReviewed": "Ցուցադրել վերանայվածը" + }, + "motion": { + "showMotionOnly": "Ցուցադրել միայն շարժումը" + }, + "explore": { + "settings": { + "title": "Կարգավորումներ", + "defaultView": { + "title": "Լռելյայն տեսք", + "desc": "Երբ ֆիլտրեր ընտրված չեն, ցուցադրել ամենավերջին հետևված օբյեկտների ամփոփումը՝ ըստ պիտակի, կամ ցուցադրել չֆիլտրված ցանց։", + "summary": "Ամփոփում", + "unfilteredGrid": "Չֆիլտրված ցանց" + }, + "gridColumns": { + "title": "Ցանցային սյուներ", + "desc": "Ընտրեք սյուների քանակը ցանցի տեսքով։" + }, + "searchSource": { + "label": "Որոնման աղբյուր", + "desc": "Ընտրեք՝ որոնել ձեր հետևվող օբյեկտների մանրապատկերներում, թե նկարագրություններում։", + "options": { + "thumbnailImage": "Մանրապատկեր", + "description": "Նկարագրություն" + } + } + }, + "date": { + "selectDateBy": { + "label": "Ընտրեք ամսաթիվ՝ ըստ որի պետք է զտել" + } + } + }, + "logSettings": { + "label": "Ֆիլտրի գրանցամատյանի մակարդակը", + "filterBySeverity": "Զտել գրանցամատյանները ըստ ծանրության աստիճանի", + "loading": { + "title": "Բեռնվում է", + "desc": "Երբ գրանցամատյանի վահանակը գլորվում է դեպի ներքև, նոր գրանցամատյանները ավտոմատ կերպով հոսքագծվում են՝ դրանք ավելացնելուն պես։" + }, + "disableLogStreaming": "Անջատել գրանցամատյանի հոսքը", + "allLogs": "Բոլոր գրանցամատյանները" + }, + "trackedObjectDelete": { + "title": "Հաստատեք ջնջումը", + "desc": "Այս {{objectLength}} հետևված օբյեկտները ջնջելով՝ կհեռացվի լուսանկարը, պահպանված ներկառուցված ֆայլերը և դրանց հետ կապված օբյեկտի կյանքի ցիկլի գրառումները: Պատմության դիտման մեջ այս հետևված օբյեկտների ձայնագրված կադրերը ՉԵՆ ջնջվի:

    Համոզվա՞ծ եք, որ ցանկանում եք շարունակել:

    Սեղմած պահեք Shift ստեղնը՝ ապագայում այս երկխոսության պատուհանը շրջանցելու համար:", + "toast": { + "success": "Հետևվող օբյեկտները հաջողությամբ ջնջվեցին։", + "error": "Չհաջողվեց ջնջել հետևվող օբյեկտները՝ {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Զտել ըստ գոտու դիմակի" + }, + "recognizedLicensePlates": { + "title": "Ճանաչված համարանիշներ", + "loadFailed": "Չհաջողվեց բեռնել ճանաչված համարանիշները։", + "loading": "Բեռնվում են ճանաչված համարանիշները…", + "placeholder": "Մուտքագրեք՝ համարանիշներ որոնելու համար…", + "noLicensePlatesFound": "Համարանիշներ չեն հայտնաբերվել։", + "selectPlatesFromList": "Ընտրեք մեկ կամ մի քանի ափսեներ ցանկից։", + "selectAll": "Ընտրել բոլորը", + "clearAll": "Մաքրել բոլորը" + }, + "classes": { + "label": "Դասեր", + "all": { + "title": "Բոլոր դասերը" + }, + "count_one": "{{count}} դաս", + "count_other": "{{count}} դաս" + } +} diff --git a/web/public/locales/hy/components/icons.json b/web/public/locales/hy/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/components/input.json b/web/public/locales/hy/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/components/input.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/components/player.json b/web/public/locales/hy/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/components/player.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/config/cameras.json b/web/public/locales/hy/config/cameras.json new file mode 100644 index 0000000..bd50523 --- /dev/null +++ b/web/public/locales/hy/config/cameras.json @@ -0,0 +1,5 @@ +{ + "zones": { + "label": "Գոտիներ" + } +} diff --git a/web/public/locales/hy/config/global.json b/web/public/locales/hy/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/config/groups.json b/web/public/locales/hy/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/config/validation.json b/web/public/locales/hy/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/objects.json b/web/public/locales/hy/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/objects.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/chat.json b/web/public/locales/hy/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/classificationModel.json b/web/public/locales/hy/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/configEditor.json b/web/public/locales/hy/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/events.json b/web/public/locales/hy/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/events.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/explore.json b/web/public/locales/hy/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/exports.json b/web/public/locales/hy/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/faceLibrary.json b/web/public/locales/hy/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/live.json b/web/public/locales/hy/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/motionSearch.json b/web/public/locales/hy/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/recording.json b/web/public/locales/hy/views/recording.json new file mode 100644 index 0000000..c653cc5 --- /dev/null +++ b/web/public/locales/hy/views/recording.json @@ -0,0 +1,3 @@ +{ + "filter": "Ֆիլտր" +} diff --git a/web/public/locales/hy/views/replay.json b/web/public/locales/hy/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/search.json b/web/public/locales/hy/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/settings.json b/web/public/locales/hy/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hy/views/system.json b/web/public/locales/hy/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/hy/views/system.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/id/audio.json b/web/public/locales/id/audio.json new file mode 100644 index 0000000..b1ea21a --- /dev/null +++ b/web/public/locales/id/audio.json @@ -0,0 +1,503 @@ +{ + "yell": "Berteriak", + "speech": "Ucapan", + "babbling": "Mengoceh (berekah)", + "bellow": "Begadang / Meraung", + "whoop": "Tertawa lepas (whoop)", + "whispering": "Berbisik", + "snicker": "Tertawa cekikikan", + "crying": "Menangis", + "sigh": "Menghela napas", + "choir": "Paduan suara", + "yodeling": "Yodel", + "chant": "Berzikir / Menyanyi berulang", + "child_singing": "Anak bernyanyi", + "rapping": "Rap", + "humming": "Berdengung", + "groan": "Menggerung", + "grunt": "Mendengus", + "breathing": "Bernapas", + "laughter": "Tertawa", + "singing": "Bernyanyi", + "mantra": "Mantra", + "synthetic_singing": "Bernyanyi buatan (sintetis)", + "whistling": "Mendengung (mencicit / bersiul)", + "car": "Mobil", + "motorcycle": "Sepeda motor", + "bicycle": "Sepeda", + "bus": "Bus", + "train": "Kereta", + "boat": "Perahu", + "sneeze": "Bersin", + "run": "Berlari", + "footsteps": "Langkah kaki", + "chewing": "Mengunyah", + "biting": "Menggigit", + "stomach_rumble": "Suara perut bergerak", + "burping": "Sendawa", + "hiccup": "Cegukan (hikap)", + "fart": "Kentut", + "hands": "Tangan", + "heartbeat": "Detak jantung", + "applause": "Tepuk tangan massa", + "chatter": "Mengobrol", + "children_playing": "Anak‑anak bermain", + "animal": "Hewan", + "pets": "Hewan peliharaan", + "dog": "Anjing", + "bark": "Kulit kayu", + "howl": "Mengaung", + "cat": "Kucing", + "meow": "Mengeong", + "livestock": "Hewan ternak", + "horse": "Kuda", + "cattle": "Sapi", + "pig": "Babi", + "goat": "Kambing", + "sheep": "Domba", + "chicken": "Ayam", + "cluck": "Menguk / \"cluck\"", + "cock_a_doodle_doo": "Berkokok (\"cock‑a‑doodle‑doo\")", + "turkey": "Kalkun", + "duck": "Bebek", + "quack": "Menggock (\"quack\")", + "goose": "Angsa", + "wild_animals": "Hewan liar", + "bird": "Burung", + "pigeon": "Merpati", + "crow": "Gagak", + "owl": "Burung hantu", + "flapping_wings": "Sayap berkibar", + "dogs": "Anjing‑anjing", + "insect": "Serangga", + "cricket": "Kerik / Kriket", + "mosquito": "Nyamuk", + "fly": "Lalat", + "frog": "Kodok", + "snake": "Ular", + "music": "Musik", + "musical_instrument": "Instrumen musik", + "guitar": "Gitar", + "electric_guitar": "Gitar listrik", + "acoustic_guitar": "Gitar akustik", + "strum": "Mengstrum", + "banjo": "Banjo", + "snoring": "Mendengkur", + "cough": "Batuk", + "clapping": "Tepuk tangan", + "camera": "Kamera", + "wheeze": "Mengi", + "gasp": "Menggigil / Tarik napas tajam", + "sound_effect": "Efek suara (sound effect)", + "environmental_noise": "Kebisingan lingkungan", + "static": "Suara statis", + "white_noise": "White noise", + "television": "Televisi", + "radio": "Radio", + "scream": "Teriakan", + "pant": "Terengah-engah", + "snort": "Mendengus (melalui hidung)", + "throat_clearing": "Membersihkan tenggorokan", + "sniff": "Mengendus", + "shuffle": "Menyeret kaki", + "gargling": "Gargling", + "finger_snapping": "Mengklik jari", + "heart_murmur": "Murmur jantung", + "cheering": "Bersorak", + "crowd": "Kerumunan orang", + "yip": "Menggonggong pendek / ringkik", + "bow_wow": "Gonggongan \"bow wow\" khas", + "growling": "Menggeram", + "whimper_dog": "Rintihan anjing", + "purr": "Mendengkur", + "hiss": "Mendesis", + "caterwaul": "Mengeong nyaring (melolong)", + "clip_clop": "Suara kuda berlari (\"clip‑clop\")", + "neigh": "Meringkik", + "moo": "Mengamuk / \"Moo\"", + "cowbell": "Bel sapi", + "oink": "Menggonggong \"oink\"", + "bleat": "Mengebik", + "fowl": "Unggas", + "gobble": "Menggobleg", + "honk": "Bebek / \"honk\"", + "roaring_cats": "Kucing besar mengaung", + "roar": "Mengaung (raungan predator)", + "chirp": "Cicit / bernyanyi burung kecil", + "squawk": "Mengkokok / mendengung keras", + "coo": "Mengkuk \"coo\"", + "caw": "Menggagak / \"caw\"", + "hoot": "Menghoo / \"hoot\"", + "rats": "Tikus", + "mouse": "Mouse", + "patter": "Peluit kaki kecil", + "buzz": "Menggema / \"buzz\"", + "croak": "Kokok / \"croak\"", + "rattle": "Bersuara \"rattle\"", + "whale_vocalization": "Suara vokalisasi paus", + "plucked_string_instrument": "Instrumen senar dipetik", + "bass_guitar": "Bass gitar", + "steel_guitar": "Steel gitar", + "tapping": "Mengetuk", + "sitar": "Sitar", + "mandolin": "Mandolin", + "zither": "Zither", + "ukulele": "Ukulele", + "keyboard": "Keyboard", + "piano": "Piano", + "electric_piano": "Piano elektrik", + "organ": "Organ", + "electronic_organ": "Organ elektronik", + "hammond_organ": "Organ Hammond", + "synthesizer": "Synthesizer", + "sampler": "Sampler", + "harpsichord": "Harpsichord", + "percussion": "Percussion", + "drum_kit": "Kotak drum (drum kit)", + "drum_machine": "Mesin drum (drum machine)", + "drum": "Drum", + "snare_drum": "Snare drum", + "rimshot": "Rimshot", + "drum_roll": "Roll drum", + "bass_drum": "Bass drum", + "timpani": "Timpani", + "tabla": "Tabla", + "cymbal": "Cymbal", + "hi_hat": "Hi‑hat", + "wood_block": "Wood block", + "tambourine": "Tambourine", + "maraca": "Maraca", + "gong": "Gong", + "tubular_bells": "Tubular bells", + "mallet_percussion": "Percussion palu (mallet)", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibraphone", + "steelpan": "Steelpan", + "orchestra": "Orchestra", + "brass_instrument": "Instrumen tiup logam (brass)", + "french_horn": "French horn", + "trumpet": "Trumpet", + "trombone": "Trombone", + "bowed_string_instrument": "Instrumen senar di‑gesek (bowed string)", + "string_section": "Seksi biola (string section)", + "violin": "Biola (violin)", + "pizzicato": "Pizzicato", + "cello": "Violoncello (cello)", + "double_bass": "Double bass", + "wind_instrument": "Instrumen tiup (wind)", + "flute": "Flute", + "saxophone": "Saxophone", + "clarinet": "Clarinet", + "harp": "Harp", + "bell": "Bel (lonceng)", + "church_bell": "Lonceng gereja", + "jingle_bell": "Jingle bell", + "bicycle_bell": "Bel sepeda", + "tuning_fork": "Tuning fork", + "chime": "Chime", + "wind_chime": "Wind chime", + "harmonica": "Harmonika", + "accordion": "Akordian", + "bagpipes": "Bagpipes", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Singing bowl", + "scratching": "Scratching (DJ scratching)", + "pop_music": "Musik pop", + "hip_hop_music": "Musik hip‑hop", + "beatboxing": "Beatboxing", + "rock_music": "Musik rock", + "heavy_metal": "Heavy metal", + "punk_rock": "Punk rock", + "grunge": "Grunge", + "progressive_rock": "Progressive rock", + "rock_and_roll": "Rock and roll", + "psychedelic_rock": "Psychedelic rock", + "rhythm_and_blues": "Rhythm and blues", + "soul_music": "Soul", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Swing", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folk", + "middle_eastern_music": "Musik Timur Tengah", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Musik klasik", + "opera": "Opera", + "electronic_music": "Musik elektronik", + "house_music": "House music", + "techno": "Tekno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and bass", + "electronica": "Electronica", + "electronic_dance_music": "Electronic dance music (EDM)", + "ambient_music": "Musik ambient", + "trance_music": "Trance", + "music_of_latin_america": "Musik Amerika Latin", + "salsa_music": "Salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Musik anak‑anak", + "new-age_music": "Musik new age", + "vocal_music": "Musik vokal", + "a_capella": "A cappella", + "music_of_africa": "Musik Afrika", + "afrobeat": "Afrobeat", + "christian_music": "Musik krisitan / Kristen", + "gospel_music": "Musik gospel", + "music_of_asia": "Musik Asia", + "carnatic_music": "Carnatic music", + "music_of_bollywood": "Musik Bollywood", + "ska": "Ska", + "traditional_music": "Musik tradisional", + "independent_music": "Independent music", + "song": "Lagu", + "background_music": "Background music", + "theme_music": "Theme music", + "jingle": "Jingle (lagu iklan singkat)", + "soundtrack_music": "Musik soundtrack", + "lullaby": "Lullaby", + "video_game_music": "Musik video game", + "christmas_music": "Musik Natal", + "dance_music": "Musik dansa", + "wedding_music": "Musik pernikahan", + "happy_music": "Musik bahagia", + "sad_music": "Musik sedih", + "tender_music": "Musik lembut / romantis", + "exciting_music": "Musik mendebarkan", + "angry_music": "Musik marah", + "scary_music": "Musik menakutkan", + "wind": "Angin", + "rustling_leaves": "Daun bergesekan", + "wind_noise": "Suara angin", + "thunderstorm": "Badai petir", + "thunder": "Kilat / guruh (guntur)", + "water": "Air", + "rain": "Hujan", + "raindrop": "Tetesan hujan", + "rain_on_surface": "Hujan jatuh ke permukaan", + "stream": "Aliran sungai kecil", + "waterfall": "Air terjun", + "ocean": "Laut", + "waves": "Ombak", + "steam": "Uap", + "gurgling": "Menggulung / bergolak (gurgling)", + "fire": "Api", + "crackle": "Mercekik / berderak (crackle)", + "vehicle": "Kendaraan", + "sailboat": "Perahu layar", + "rowboat": "Perahu dayung", + "motorboat": "Perahu bermotor", + "ship": "Kapal besar", + "motor_vehicle": "Kendaraan bermotor", + "toot": "Bunyi klakson kecil", + "car_alarm": "Alarm mobil", + "power_windows": "Jendela bergerak dengan tenaga listrik", + "skidding": "Selipan roda", + "tire_squeal": "Roda tergelincir / berdecit", + "car_passing_by": "Mobil melintas", + "race_car": "Mobil balap", + "truck": "Truk", + "air_brake": "Rem udara", + "air_horn": "Horn udara", + "reversing_beeps": "Bunyi beeper mundur", + "ice_cream_truck": "Mobil es krim", + "emergency_vehicle": "Kendaraan darurat", + "police_car": "Mobil patroli polisi", + "ambulance": "Ambulans", + "fire_engine": "Mobil pemadam kebakaran", + "traffic_noise": "Kebisingan lalu lintas", + "rail_transport": "Transportasi rel", + "train_whistle": "Pelecut kereta", + "train_horn": "Klakson kereta api", + "railroad_car": "Gerigit kereta api", + "train_wheels_squealing": "Rel kereta berdecit", + "subway": "Kereta bawah tanah (subway)", + "aircraft": "Pesawat udara", + "aircraft_engine": "Mesin pesawat", + "jet_engine": "Mesin jet", + "propeller": "Propeller", + "helicopter": "Helikopter", + "fixed-wing_aircraft": "Pesawat sayap tetap", + "skateboard": "Papan luncur", + "engine": "Mesin", + "light_engine": "Mesin ringan", + "dental_drill's_drill": "Bor gigi", + "lawn_mower": "Mesin pemotong rumput", + "chainsaw": "Gergaji mesin / chainsaw", + "medium_engine": "Mesin menengah", + "heavy_engine": "Mesin berat", + "engine_knocking": "Mesin berdecit", + "engine_starting": "Mesin dihidupkan", + "idling": "Mesin diam tetap hidup (idling)", + "accelerating": "Percepatan (accelerating)", + "door": "Pintu", + "doorbell": "Bel pintu", + "ding-dong": "Ding‑dong (bunyi bel pintu khas)", + "sliding_door": "Pintu geser", + "slam": "Menjekat (bunyi pintu ditutup keras)", + "knock": "Ketukan", + "tap": "Mengetuk", + "squeak": "Berderit", + "cupboard_open_or_close": "Kupmar terbuka atau tertutup", + "drawer_open_or_close": "Laci terbuka atau tertutup", + "dishes": "Piring", + "cutlery": "Sendok garpu", + "chopping": "Mengiris", + "frying": "Menggoreng", + "microwave_oven": "Oven microwave", + "blender": "Blender", + "water_tap": "Kran air", + "sink": "Wastafel", + "bathtub": "Bak mandi", + "hair_dryer": "Pengering rambut", + "toilet_flush": "Siraman toilet", + "toothbrush": "Sikat gigi", + "electric_toothbrush": "Sikat gigi elektrik", + "vacuum_cleaner": "Vacuum cleaner", + "zipper": "Ritsleting", + "keys_jangling": "Kunci berdering", + "coin": "Koin", + "scissors": "Gunting", + "electric_shaver": "Alat cukur listrik", + "shuffling_cards": "Mengacaukan kartu", + "typing": "Ketikan", + "typewriter": "Mesin tik", + "computer_keyboard": "Keyboard komputer", + "writing": "Menulis", + "alarm": "Alarm", + "telephone": "Telepon", + "telephone_bell_ringing": "Bel telepon berdering", + "ringtone": "Nada dering", + "telephone_dialing": "Menelepon dengan dial", + "dial_tone": "Nada tunggu (dial tone)", + "busy_signal": "Suara sibuk", + "alarm_clock": "Alarm jam", + "siren": "Sirine", + "civil_defense_siren": "Sirine perlindungan sipil", + "buzzer": "Buzzer", + "smoke_detector": "Detektor asap", + "fire_alarm": "Alarm kebakaran", + "foghorn": "Foghorn (bunyi peluit kabut laut)", + "whistle": "Peluit", + "steam_whistle": "Peluit uap", + "mechanisms": "Mekanisme", + "ratchet": "Ratchet", + "clock": "Jam", + "tick": "Detak (tick)", + "tick-tock": "Tick‑tock", + "gears": "Roda gigi (gears)", + "pulleys": "Katrol", + "sewing_machine": "Mesin jahit", + "mechanical_fan": "Kipas baling‑baling mekanik", + "air_conditioning": "Pendingin ruangan / AC", + "cash_register": "Mesin kasir", + "printer": "Printer", + "single-lens_reflex_camera": "Kamera single‑lens reflex", + "tools": "Perkakas", + "hammer": "Palu", + "jackhammer": "Jackhammer", + "sawing": "Menggergaji", + "filing": "Mengasah", + "sanding": "Mengampelas", + "power_tool": "Power tool (perkakas bermotor)", + "drill": "Bor", + "explosion": "Ledakan", + "gunshot": "Tembakan senjata api", + "machine_gun": "Senapan mesin", + "fusillade": "Fusillade (banyak tembakan sekaligus)", + "artillery_fire": "Tembakan artileri", + "cap_gun": "Senapan mainan (cap gun)", + "fireworks": "Kembang api", + "firecracker": "Petasan kembang api", + "burst": "Ledakan pecah (burst)", + "eruption": "Letusan (eruption)", + "boom": "Boom (bunyi ledakan berat)", + "wood": "Kayu", + "chop": "Menebang (chop)", + "splinter": "Bercerai (splinter)", + "crack": "Retak / pecah (crack)", + "glass": "Kaca", + "chink": "Bunyi kaca berdenting (chink)", + "shatter": "Hancur / pecah (shatter)", + "silence": "Diam / tidak ada suara (silence)", + "pink_noise": "Pink noise", + "field_recording": "Rekaman lapangan (field recording)", + "sodeling": "Menangis tertahan", + "chird": "Derit / suara aneh", + "change_ringing": "Lantunan lonceng bergantian (change ringing)", + "shofar": "Shofar", + "liquid": "Cairan", + "splash": "Cipratan (splash)", + "slosh": "Slosh (suara cairan bergoyang)", + "squish": "Squish (bunyi renyah basah)", + "drip": "Tetes (drip)", + "pour": "Tuang (pour)", + "trickle": "Menetes (trickle)", + "gush": "Mengalir deras (gush)", + "fill": "Mengisi (fill)", + "spray": "Semprot (spray)", + "pump": "Pompa", + "stir": "Aduk (stir)", + "boiling": "Mendidih", + "sonar": "Sonar", + "arrow": "Panah", + "whoosh": "Whoosh (bunyi melesat cepat)", + "thump": "Thump (bantingan)", + "thunk": "Thunk (bunyi tebal tumpul)", + "electronic_tuner": "Tuner elektronik", + "effects_unit": "Effects unit (efek audio)", + "chorus_effect": "Efek chorus", + "basketball_bounce": "Pantulan bola basket", + "bang": "Benturan keras (bang)", + "slap": "Plak (pukulan telapak)", + "whack": "Whack (pukulan keras)", + "smash": "Smash (hancurkan keras)", + "breaking": "Memecahkan", + "bouncing": "Memantul", + "whip": "Cambuk", + "flap": "Flap (sayap / lembaran berkibar)", + "scratch": "Gores (scratch)", + "scrape": "Gesekan kasar (scrape)", + "rub": "Menggosok (rub)", + "roll": "Gulung (roll)", + "crushing": "Menghancurkan (crushing)", + "crumpling": "Menggumpalkan (crumpling)", + "tearing": "Merosak (tearing)", + "beep": "Beep", + "ping": "Ping", + "ding": "Ding", + "clang": "Clang", + "squeal": "Squeal (mengerang)", + "creak": "Creak (berderit pelan)", + "rustle": "Rustle (menggerut)", + "whir": "Whir (menderu putaran cepat)", + "clatter": "Kerincing / benturan berantai (clatter)", + "sizzle": "Sizzle (menggoreng / bersiul)", + "clicking": "Clicking (bunyi kunci)", + "clickety_clack": "Clickety clack (bunyi kaki atau rel)", + "rumble": "Rumble (gempuran / gemuruh)", + "plop": "Plop (bunyi jatuh lembut ke air)", + "hum": "Hum (mendengung)", + "zing": "Zing (bunyi gesek tipis cepat)", + "boing": "Boing (bunyi pegas)", + "crunch": "Crunch (remas keras)", + "sine_wave": "Gelombang sinus (sine wave)", + "harmonic": "Harmonik", + "chirp_tone": "Tone chirp", + "pulse": "Pulse (detak / pulsa)", + "inside": "Di dalam ruangan", + "outside": "Di luar ruangan", + "reverberation": "Gema ruang (reverberation)", + "echo": "Gema (echo)", + "noise": "Kebisingan", + "mains_hum": "Mains hum (dengungan listrik arus utama)", + "distortion": "Distorsi", + "sidetone": "Sidetone (suara sendiri saat menelepon)", + "cacophony": "Kecemasan suara (cacophony)", + "throbbing": "Berdebar / gemuruh (throbbing)", + "vibration": "Vibrasi" +} diff --git a/web/public/locales/id/common.json b/web/public/locales/id/common.json new file mode 100644 index 0000000..cdeb4ab --- /dev/null +++ b/web/public/locales/id/common.json @@ -0,0 +1,317 @@ +{ + "time": { + "untilForRestart": "Sampai Frigate dimulai ulang.", + "untilRestart": "Sampai dimulai ulang", + "ago": "{{timeAgo}} yang lalu", + "justNow": "Baru saja", + "today": "Hari ini", + "yesterday": "Kemarin", + "untilForTime": "Sampai {{time}}", + "last7": "7 hari terakhir", + "last14": "14 hari terakhir", + "last30": "30 hari terakhir", + "thisWeek": "Minggu Ini", + "never": "Tidak pernah", + "lastWeek": "Minggu Lalu", + "thisMonth": "Bulan Ini", + "lastMonth": "Bulan Lalu", + "5minutes": "5 menit", + "10minutes": "10 menit", + "30minutes": "30 menit", + "1hour": "1 jam", + "12hours": "12 jam", + "24hours": "24 jam", + "pm": "PM", + "am": "AM", + "yr": "{{time}} th", + "year_other": "{{time}} tahun", + "mo": "{{time}} bln", + "month_other": "{{time}} bulan", + "d": "{{time}} hr", + "day_other": "{{time}} hari", + "h": "{{time}} jam", + "hour_other": "{{time}} jam", + "m": "{{time}} mnt", + "minute_other": "{{time}} menit", + "s": "{{time}} dtk", + "second_other": "{{time}} detik", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "inProgress": "Sedang berlangsung", + "invalidStartTime": "Waktu mulai tidak valid", + "invalidEndTime": "Waktu selesai tidak valid" + }, + "readTheDocumentation": "Baca dokumentasi", + "menu": { + "system": "Sistem", + "profiles": "Profil", + "systemMetrics": "Metrik sistem", + "configuration": "Konfigurasi", + "systemLogs": "Log sistem", + "settings": "Pengaturan", + "configurationEditor": "Editor Konfigurasi", + "languages": "Bahasa", + "language": { + "en": "English (Inggris)", + "es": "Español (Spanyol)", + "zhCN": "简体中文 (Tionghoa Sederhana)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Prancis)", + "ar": "العربية (Arab)", + "pt": "Português (Portugis)", + "ptBR": "Português brasileiro (Portugis Brasil)", + "ru": "Русский (Rusia)", + "de": "Deutsch (Jerman)", + "ja": "日本語 (Jepang)", + "tr": "Türkçe (Turki)", + "it": "Italiano (Italia)", + "nl": "Nederlands (Belanda)", + "sv": "Svenska (Swedia)", + "cs": "Čeština (Ceko)", + "nb": "Norsk Bokmål (Norwegia Bokmål)", + "ko": "한국어 (Korea)", + "vi": "Tiếng Việt (Vietnam)", + "fa": "فارسی (Persia)", + "pl": "Polski (Polandia)", + "uk": "Українська (Ukraina)", + "he": "עברית (Ibrani)", + "el": "Ελληνικά (Yunani)", + "ro": "Română (Rumania)", + "hu": "Magyar (Hungaria)", + "fi": "Suomi (Finlandia)", + "da": "Dansk (Denmark)", + "sk": "Slovenčina (Slovakia)", + "yue": "粵語 (Kanton)", + "th": "ไทย (Thai)", + "ca": "Català (Katalan)", + "hr": "Hrvatski (Kroasia)", + "bs": "Bosanski (Bosnia)", + "sr": "Српски (Serbia)", + "sl": "Slovenščina (Slovenia)", + "lt": "Lietuvių (Lituania)", + "bg": "Български (Bulgaria)", + "gl": "Galego (Galisia)", + "id": "Bahasa Indonesia (Indonesia)", + "ur": "اردو (Urdu)", + "withSystem": { + "label": "Gunakan pengaturan sistem untuk bahasa" + }, + "zhHant": "繁體中文 (Tionghoa Tradisional)" + }, + "appearance": "Tampilan", + "darkMode": { + "label": "Mode Gelap", + "light": "Terang", + "dark": "Gelap", + "withSystem": { + "label": "Gunakan pengaturan sistem untuk mode terang atau gelap" + } + }, + "withSystem": "Sistem", + "theme": { + "label": "Tema", + "blue": "Biru", + "green": "Hijau", + "nord": "Nord", + "red": "Merah", + "highcontrast": "Kontras Tinggi", + "default": "Default" + }, + "help": "Bantuan", + "documentation": { + "title": "Dokumentasi", + "label": "Dokumentasi Frigate" + }, + "restart": "Mulai Ulang Frigate", + "live": { + "title": "Live", + "allCameras": "Semua Kamera", + "cameras": { + "title": "Kamera", + "count_other": "{{count}} Kamera" + } + }, + "review": "Tinjauan", + "explore": "Jelajah", + "export": "Ekspor", + "actions": "Tindakan", + "uiPlayground": "UI Playground", + "features": "Fitur", + "faceLibrary": "Pustaka Wajah", + "classification": "Klasifikasi", + "chat": "Chat", + "user": { + "title": "Pengguna", + "account": "Akun", + "current": "Pengguna Saat Ini: {{user}}", + "anonymous": "anonim", + "logout": "Keluar", + "setPassword": "Atur Kata Sandi" + } + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "kph" + }, + "length": { + "feet": "kaki", + "meters": "meter" + }, + "data": { + "kbps": "kB/dtk", + "mbps": "MB/dtk", + "gbps": "GB/dtk", + "kbph": "kB/jam", + "mbph": "MB/jam", + "gbph": "GB/jam" + } + }, + "label": { + "back": "Kembali", + "hide": "Sembunyikan {{item}}", + "show": "Tampilkan {{item}}", + "ID": "ID", + "none": "Tidak ada", + "all": "Semua", + "other": "Lainnya" + }, + "list": { + "two": "{{0}} dan {{1}}", + "many": "{{items}}, dan {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opsional", + "internalID": "ID Internal yang digunakan Frigate dalam konfigurasi dan basis data" + }, + "button": { + "add": "Tambah", + "apply": "Terapkan", + "applying": "Menerapkan…", + "reset": "Atur Ulang", + "undo": "Urungkan", + "done": "Selesai", + "enabled": "Diaktifkan", + "enable": "Aktifkan", + "disabled": "Dinonaktifkan", + "disable": "Nonaktifkan", + "save": "Simpan", + "saving": "Menyimpan…", + "cancel": "Batal", + "close": "Tutup", + "copy": "Salin", + "copiedToClipboard": "Disalin ke papan klip", + "back": "Kembali", + "history": "Riwayat", + "fullscreen": "Layar Penuh", + "exitFullscreen": "Keluar dari Layar Penuh", + "pictureInPicture": "Gambar dalam Gambar", + "twoWayTalk": "Audio Dua Arah", + "cameraAudio": "Audio Kamera", + "on": "AKTIF", + "off": "NONAKTIF", + "edit": "Edit", + "copyCoordinates": "Salin koordinat", + "delete": "Hapus", + "yes": "Ya", + "no": "Tidak", + "download": "Unduh", + "info": "Info", + "suspended": "Ditangguhkan", + "unsuspended": "Batalkan penangguhan", + "play": "Putar", + "unselect": "Batalkan pilihan", + "export": "Ekspor", + "deleteNow": "Hapus Sekarang", + "next": "Berikutnya", + "continue": "Lanjutkan", + "modified": "Diubah", + "overridden": "Ditimpa", + "resetToGlobal": "Atur Ulang ke Global", + "resetToDefault": "Atur Ulang ke Default", + "saveAll": "Simpan Semua", + "savingAll": "Menyimpan Semua…", + "undoAll": "Urungkan Semua", + "retry": "Coba Lagi" + }, + "toast": { + "copyUrlToClipboard": "URL disalin ke papan klip.", + "save": { + "title": "Simpan", + "error": { + "title": "Gagal menyimpan perubahan konfigurasi: {{errorMessage}}", + "noMessage": "Gagal menyimpan perubahan konfigurasi" + }, + "success": "Berhasil menyimpan perubahan konfigurasi." + } + }, + "role": { + "title": "Peran", + "admin": "Admin", + "viewer": "Penampil", + "desc": "Admin memiliki akses penuh ke semua fitur di UI Frigate. Penampil terbatas hanya untuk melihat kamera, item tinjauan, dan rekaman historis di UI." + }, + "pagination": { + "label": "paginasi", + "previous": { + "title": "Sebelumnya", + "label": "Buka halaman sebelumnya" + }, + "next": { + "title": "Berikutnya", + "label": "Buka halaman berikutnya" + }, + "more": "Halaman lainnya" + }, + "accessDenied": { + "documentTitle": "Akses Ditolak - Frigate", + "title": "Akses Ditolak", + "desc": "Anda tidak memiliki izin untuk melihat halaman ini." + }, + "notFound": { + "documentTitle": "Tidak Ditemukan - Frigate", + "title": "404", + "desc": "Halaman tidak ditemukan" + }, + "selectItem": "Pilih {{item}}", + "information": { + "pixels": "{{area}}px" + }, + "no_items": "Tidak ada item", + "validation_errors": "Kesalahan Validasi", + "credentialField": { + "savedPlaceholder": "Tersimpan — biarkan kosong untuk mempertahankan yang saat ini" + } +} diff --git a/web/public/locales/id/components/auth.json b/web/public/locales/id/components/auth.json new file mode 100644 index 0000000..742e311 --- /dev/null +++ b/web/public/locales/id/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nama pengguna", + "password": "Kata sandi", + "login": "Masuk", + "errors": { + "usernameRequired": "Username diperlukan", + "passwordRequired": "Password diperlukan", + "rateLimit": "Melewati batas permintaan. Coba lagi nanti.", + "loginFailed": "Gagal Masuk", + "unknownError": "Eror tidak diketahui. Mohon lihat log.", + "webUnknownError": "Eror tidak diketahui. Mohon lihat log konsol." + }, + "firstTimeLogin": "Mencoba masuk untuk pertama kali? Kredensial sudah dicetak di dalam riwayat Frigate." + } +} diff --git a/web/public/locales/id/components/camera.json b/web/public/locales/id/components/camera.json new file mode 100644 index 0000000..61ca36b --- /dev/null +++ b/web/public/locales/id/components/camera.json @@ -0,0 +1,90 @@ +{ + "group": { + "label": "Grup Kamera", + "add": "Tambah Grup Kamera", + "edit": "Edit Grup kamera", + "delete": { + "label": "Hapus Grup Kamera", + "confirm": { + "title": "Yakin Hapus", + "desc": "Apakah Anda yakin ingin menghapus grup kamera {{name}}?" + } + }, + "name": { + "label": "Nama", + "placeholder": "Masukkan nama…", + "errorMessage": { + "mustLeastCharacters": "Nama grup kamera minimal harus 2 karakter.", + "exists": "Nama grup kamera sudah ada.", + "nameMustNotPeriod": "Nama grup kamera tidak boleh ada titik.", + "invalid": "Nama grup kamera tidak valid." + } + }, + "cameras": { + "label": "Kamera", + "desc": "Pilih kamera untuk grup ini." + }, + "icon": "Ikon", + "success": "Grup kamera {{name}} telah disimpan.", + "camera": { + "birdseye": "Mata Elang", + "setting": { + "label": "Pengaturan Streaming Kamera", + "title": "Pengaturan Kamera {{cameraName}}", + "desc": "Ubah pengaturan streaming untuk dasbor grup kamera ini. Pengaturan ini spesifik untuk perangkat / browser tertentu.", + "audioIsAvailable": "Terdapat audio untuk stream ini", + "audioIsUnavailable": "Tidak terdapat audio untuk stream ini", + "audio": { + "tips": { + "title": "Audio harus dikeluarkan dari kamera Anda dan dikonfigurasi di go2rtc untuk stream ini." + } + }, + "stream": "Siaran", + "placeholder": "Pilih stream", + "streamMethod": { + "label": "Metode Streaming", + "placeholder": "Pilih satu metode streaming", + "method": { + "noStreaming": { + "label": "Tidak Streaming", + "desc": "Gambar kamera hanya diperbarui sekali per menit dan tidak akan ada live streaming." + }, + "smartStreaming": { + "label": "Streaming cerdas (disarankan)", + "desc": "Streaming cerdas akan memperbarui gambar kamera sekali per menit saat tidak ada aktivitas yang terdeteksi untuk menghemat bandwidth dan sumber daya. Ketika aktivitas terdeteksi, gambar akan otomatis beralih ke live stream." + }, + "continuousStreaming": { + "desc": { + "title": "Kamera akan selalu live stream selama muncul di dashboard, meskipun tidak ada aktivitas yang terdeteksi.", + "warning": "Streaming Terus Menerus dapat menyebabkan penggunaan bandwidth yang tinggi dan masalah performa. Gunakan dengan hati-hati." + }, + "label": "Streaming Terus Menerus" + } + } + }, + "compatibilityMode": { + "label": "Mode Kompatibilitas", + "desc": "Aktifkan opsi ini hanya jika live stream kamera Anda menampilkan artefak warna dan memiliki garis diagonal di sisi kanan gambar." + } + } + }, + "showAll": "Tampilkan semua grup kamera", + "showLess": "Lebih sedikit", + "editGroups": "Sunting Grup Kamera" + }, + "debug": { + "boundingBox": "Batas Kotak", + "options": { + "label": "Pengaturan", + "title": "Opsi", + "showOptions": "Lihat Opsi", + "hideOptions": "Sembunyikan Opsi" + }, + "zones": "Zona", + "motion": "Gerakan", + "regions": "Wilayah", + "timestamp": "Timestamp", + "mask": "Masker", + "paths": "Path" + } +} diff --git a/web/public/locales/id/components/dialog.json b/web/public/locales/id/components/dialog.json new file mode 100644 index 0000000..6ec42c5 --- /dev/null +++ b/web/public/locales/id/components/dialog.json @@ -0,0 +1,124 @@ +{ + "restart": { + "title": "Apakah kamu yakin ingin memulai ulang Frigate?", + "button": "Mulai ulang", + "restarting": { + "title": "Sedang Merestart Frigate", + "content": "Halaman ini akan memulai ulang dalam {{countdown}} detik.", + "button": "Muat Ulang Sekarang" + }, + "description": "Layanan Frigate akan terhenti sejenak saat proses restart." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Kirim ke Frigate+", + "desc": "Objek di lokasi yang ingin Anda hindari bukanlah deteksi palsu. Mengirimnya sebagai deteksi palsu akan membingungkan model." + }, + "review": { + "question": { + "label": "Konfirmasi label ini untuk Frigate Plus", + "ask_a": "Apakah objek ini adalah sebuah{{label}}?", + "ask_an": "Apakah objek ini {{label}}?", + "ask_full": "Apakah ini object {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Terkirim" + } + } + }, + "video": { + "viewInHistory": "Lihat di Histori" + } + }, + "export": { + "time": { + "fromTimeline": "Pilih dari Linimasa", + "lastHour_other": "{{count}} Jam Terakhir", + "custom": "Kustom", + "start": { + "title": "Waktu Mulai", + "label": "Pilih Waktu Mulai" + }, + "end": { + "title": "Waktu Akhir", + "label": "Pilih Waktu Akhir" + } + }, + "name": { + "placeholder": "Nama Ekspor" + }, + "select": "Pilih", + "export": "Ekspor", + "selectOrExport": "Pilih atau Ekspor", + "toast": { + "success": "Berhasil memulai ekspor. Lihat file pada halaman ekspor.", + "queued": "Ekspor diantrekan. Lihat progres di halaman ekspor.", + "view": "Melihat", + "batchSuccess_other": "{{count}} Ekspor dimulai. Membuka kasusnya sekarang.", + "batchPartial": "Ekspor berhasil dimulai sebanyak {{successful}} dari total {{total}} ekspor. Kamera yang gagal: {{failedCameras}}", + "batchFailed": "Gagal memulai ekspor sebanyak {{total}}. Kamera yang gagal: {{failedCameras}}" + }, + "case": { + "newCaseOption": "Membuat Kasus Baru", + "newCaseNamePlaceholder": "Nama Kasus Baru", + "newCaseDescriptionPlaceholder": "Deskripsi Kasus", + "label": "Kasus", + "nonAdminHelp": "Kasus Baru akan Dibuat untuk Ekspor ini.", + "placeholder": "Pilih Kasus" + }, + "tabs": { + "export": "Kamera Tunggal", + "multiCamera": "Multi Kamera" + }, + "multiCamera": { + "timeRange": "Rentang Waktu", + "selectFromTimeline": "Pilih dari Timeline", + "cameraSelection": "Kamera", + "checkingActivity": "Memeriksa Aktivitas Kamera...", + "noCameras": "Tidak ada Kamera Tersedia", + "detectionCount_other": "{{count}} Objek terlacak", + "nameLabel": "Nama Ekspor", + "exportButton_other": "Ekspor {{count}} kamera", + "cameraSelectionHelp": "Kamera dengan objek yang dilacak dalam rentang waktu ini telah dipilih sebelumnya", + "queueingButton": "Mengantrekan ekspor...", + "clearSelection": "Hapus pilihan", + "selectWithActivity": "Kamera dengan objek yang dilacak", + "selectGroup": "Pilih grup", + "noMatchingCameras": "Tidak ada kamera yang sesuai dengan pencarian Anda", + "selectedCount": "{{terpilih}} / {{total}} terpilih", + "namePlaceholder": "Nama dasar opsional untuk ekspor ini" + }, + "multi": { + "title_other": "Ekspor {{count}} Ulasan", + "toast": { + "startedNoCase_other": "{{count}} Ekspor Dimulai.", + "started_other": "{{count}} ekspor dimulai. Membuka kasusnya sekarang.", + "partial": "Memulai {{successful}} dari {{total}} ekspor. Gagal: {{failedItems}}", + "failed": "Gagal memulai {{total}} ekspor. Gagal: {{failedItems}}" + }, + "description": "Ekspor setiap ulasan yang dipilih. Semua Ekspor akan dikelompokkan dalam satu kasus.", + "descriptionNoCase": "Ekspor setiap ulasan yang dipilih.", + "exportButton_other": "Ekspor {{count}} ulasan", + "exportingButton": "Mengekspor...", + "caseNamePlaceholder": "Ekspor tinjauan - {{tanggal}}" + }, + "queueing": "Mengantrekan Ekspor..." + }, + "search": { + "saveSearch": { + "overwrite": "{{searchName}} sudah ada. Menyimpan akan menimpa file yang sudah ada." + } + }, + "streaming": { + "restreaming": { + "disabled": "Fitur restreaming tidak diaktifkan untuk kamera ini.", + "desc": { + "title": "Instal go2rtc untuk opsi tampilan langsung dan audio tambahan untuk kamera ini." + } + }, + "showStats": { + "label": "Tampilkan statistik streaming" + } + } +} diff --git a/web/public/locales/id/components/filter.json b/web/public/locales/id/components/filter.json new file mode 100644 index 0000000..8b39304 --- /dev/null +++ b/web/public/locales/id/components/filter.json @@ -0,0 +1,57 @@ +{ + "filter": "Saring", + "labels": { + "label": "Label", + "all": { + "title": "Semua Label", + "short": "Label" + }, + "count_one": "{{count}} Label", + "count_other": "{{count}} Label" + }, + "zones": { + "label": "Zona", + "all": { + "title": "Semua Zona", + "short": "Zona" + } + }, + "classes": { + "label": "Kelas", + "all": { + "title": "Semua Kelas" + }, + "count_one": "{{count}} Kelas", + "count_other": "{{count}} Kelas" + }, + "dates": { + "selectPreset": "Pilih preset…", + "all": { + "title": "Semua Tanggal", + "short": "Tanggal" + } + }, + "more": "Lebih Banyak", + "reset": { + "label": "Atur ulang filter ke default" + }, + "timeRange": "Rentang Waktu", + "subLabels": { + "label": "Sublabel", + "all": "Semua Sublabel" + }, + "attributes": { + "label": "Klasifikasi Atribut", + "all": "Semua Atribut" + }, + "score": "Skor", + "estimatedSpeed": "Perkiraan Kecepatan {{unit}}", + "features": { + "label": "Fitur" + }, + "cameras": { + "all": { + "short": "Kamera" + } + } +} diff --git a/web/public/locales/id/components/icons.json b/web/public/locales/id/components/icons.json new file mode 100644 index 0000000..de1a6a2 --- /dev/null +++ b/web/public/locales/id/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Pilih ikon", + "search": { + "placeholder": "Cari Icon…" + } + } +} diff --git a/web/public/locales/id/components/input.json b/web/public/locales/id/components/input.json new file mode 100644 index 0000000..8e0877b --- /dev/null +++ b/web/public/locales/id/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Unduh Video", + "toast": { + "success": "Video yang yang anda lihat sudah mulai di unduh." + } + } + } +} diff --git a/web/public/locales/id/components/player.json b/web/public/locales/id/components/player.json new file mode 100644 index 0000000..0adef26 --- /dev/null +++ b/web/public/locales/id/components/player.json @@ -0,0 +1,51 @@ +{ + "noPreviewFound": "Pratinjau Tidak Ditemukan", + "noPreviewFoundFor": "Tidak ada Pratinjau untuk {{cameraName}}", + "submitFrigatePlus": { + "submit": "Kirim", + "title": "Kirim frame ini ke Frigate+?" + }, + "noRecordingsFoundForThisTime": "Tidak ada rekaman ditemukan pada waktu ini", + "livePlayerRequiredIOSVersion": "iOS 17.1 atau yang lebih tinggi diperlukan untuk tipe siaran langsung ini.", + "streamOffline": { + "title": "Stream Tidak Aktif", + "desc": "Tidak ada gambar yang diterima dari strim detect {{cameraName}}, mohon lihat log error" + }, + "cameraDisabled": "Kamera dinonaktifkan", + "stats": { + "streamType": { + "title": "Tipe stream:", + "short": "Jenis" + }, + "bandwidth": { + "title": "Bandwith:", + "short": "Bandwith" + }, + "latency": { + "title": "Latensi:", + "value": "{{seconds}} detik", + "short": { + "title": "Latensi", + "value": "{{seconds}} detik" + } + }, + "totalFrames": "Total Frame:", + "droppedFrames": { + "title": "Frame Terbuang:", + "short": { + "title": "Terbuang", + "value": "{{droppedFrames}} frame" + } + }, + "decodedFrames": "Decoded Frames:", + "droppedFrameRate": "Frame Rate Terbuang:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Berhasil mengirim frame ke Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Gagal mengirim frame ke Frigate+" + } + } +} diff --git a/web/public/locales/id/config/cameras.json b/web/public/locales/id/config/cameras.json new file mode 100644 index 0000000..3b80196 --- /dev/null +++ b/web/public/locales/id/config/cameras.json @@ -0,0 +1,62 @@ +{ + "label": "Pengaturan Kamera", + "name": { + "label": "Nama Kamera", + "description": "Nama Kamera diwajibkan" + }, + "friendly_name": { + "label": "Nama Singkat", + "description": "Nama Singkat kamera digunakan pada tampilan UI Frigate" + }, + "audio": { + "label": "Deteksi Suara", + "description": "Pengaturan untuk Deteksi Kejadian berdasarkan Suara pada kamera ini.", + "enabled": { + "label": "Nyalakan Deteksi Suara", + "description": "Nyalakan atau matikan deteksi kejadian suara pada kamera ini." + }, + "filters": { + "threshold": { + "label": "Keyakinan-Suara Minimum" + } + }, + "min_volume": { + "label": "Volume-Suara Minimum" + } + }, + "audio_transcription": { + "label": "Transkripsi Suara", + "enabled": { + "label": "Nyalakan Transkripsi" + }, + "live_enabled": { + "label": "Transkripsi Langsung (Live)" + } + }, + "detect": { + "label": "Deteksi Objek", + "enabled": { + "label": "Nyalakan Deteksi Objek" + }, + "stationary": { + "classifier": { + "label": "Nyalakan Klasifikasi-Visual", + "description": "Menggunakan pengklasifikasi visual untuk membedakan objek-objek diam (benar-benar tidak bergerak), meskipun bounding-box kurang stabil atau bergetar (jitter)." + } + }, + "fps": { + "label": "Kecepatan (FPS) Deteksi", + "description": "Kecepatan yang ditargetkan untuk menjalankan Deteksi Objek, dalam satuan frame per second (FPS); nilai lebih rendah mengurangi intensitas proses dan dapat meringangkan beban kerja CPU. Nilai 5 direkomendasikan, sedangakan nilai 10 dianggap sangat tinggi dan hanya digunakan untuk pelacakan (tracking) benda yang bergerak dengan benar-benar cepat." + } + }, + "enabled": { + "label": "Dinyalakan", + "description": "Dinyalakan (Enabled)" + }, + "birdseye": { + "enabled": { + "description": "Nyalakan atau matikan fitur Penglihatan Atas (Birdseye View).", + "label": "Nyalakan Birdseye" + } + } +} diff --git a/web/public/locales/id/config/global.json b/web/public/locales/id/config/global.json new file mode 100644 index 0000000..9b84096 --- /dev/null +++ b/web/public/locales/id/config/global.json @@ -0,0 +1,47 @@ +{ + "version": { + "label": "Versi konfigurasi" + }, + "audio": { + "label": "Deteksi Suara", + "enabled": { + "label": "Nyalakan Deteksi Suara" + }, + "filters": { + "threshold": { + "label": "Keyakinan-Suara Minimum" + } + }, + "min_volume": { + "label": "Volume-Suara Minimum" + } + }, + "audio_transcription": { + "label": "Transkripsi Suara", + "live_enabled": { + "label": "Transkripsi Langsung (Live)" + } + }, + "detect": { + "label": "Deteksi Objek", + "enabled": { + "label": "Nyalakan Deteksi Objek" + }, + "stationary": { + "classifier": { + "label": "Nyalakan Klasifikasi-Visual", + "description": "Menggunakan pengklasifikasi visual untuk membedakan objek-objek diam (benar-benar tidak bergerak), meskipun bounding-box kurang stabil atau bergetar (jitter)." + } + }, + "fps": { + "label": "Kecepatan (FPS) Deteksi", + "description": "Kecepatan yang ditargetkan untuk menjalankan Deteksi Objek, dalam satuan frame per second (FPS); nilai lebih rendah mengurangi intensitas proses dan dapat meringangkan beban kerja CPU. Nilai 5 direkomendasikan, sedangakan nilai 10 dianggap sangat tinggi dan hanya digunakan untuk pelacakan (tracking) benda yang bergerak dengan benar-benar cepat." + } + }, + "birdseye": { + "enabled": { + "description": "Nyalakan atau matikan fitur Penglihatan Atas (Birdseye View).", + "label": "Nyalakan Birdseye" + } + } +} diff --git a/web/public/locales/id/config/groups.json b/web/public/locales/id/config/groups.json new file mode 100644 index 0000000..7e1fd9f --- /dev/null +++ b/web/public/locales/id/config/groups.json @@ -0,0 +1,7 @@ +{ + "audio": { + "global": { + "detection": "Deteksi Global" + } + } +} diff --git a/web/public/locales/id/config/validation.json b/web/public/locales/id/config/validation.json new file mode 100644 index 0000000..3be9f0d --- /dev/null +++ b/web/public/locales/id/config/validation.json @@ -0,0 +1,3 @@ +{ + "minimum": "Nilai harus lebih besar dari {{limit}}" +} diff --git a/web/public/locales/id/objects.json b/web/public/locales/id/objects.json new file mode 100644 index 0000000..82f56b0 --- /dev/null +++ b/web/public/locales/id/objects.json @@ -0,0 +1,129 @@ +{ + "person": "Orang", + "bicycle": "Sepeda", + "car": "Mobil", + "motorcycle": "Sepeda motor", + "airplane": "Pesawat", + "bus": "Bus", + "train": "Kereta", + "boat": "Perahu", + "traffic_light": "Lampu lalu lintas", + "fire_hydrant": "Hidran kebakaran", + "animal": "Hewan", + "dog": "Anjing", + "bark": "Kulit kayu", + "cat": "Kucing", + "horse": "Kuda", + "goat": "Kambing", + "sheep": "Domba", + "bird": "Burung", + "street_sign": "Rambu jalan", + "stop_sign": "Rambu berhenti", + "parking_meter": "Meter parkir", + "bench": "Bangku", + "cow": "Sapi", + "elephant": "Gajah", + "bear": "Beruang", + "zebra": "Zebra", + "giraffe": "Jerapah", + "hat": "Topi", + "backpack": "Ransel", + "mouse": "Mouse", + "keyboard": "Keyboard", + "vehicle": "Kendaraan", + "skateboard": "Papan luncur", + "door": "Pintu", + "blender": "Blender", + "sink": "Wastafel", + "hair_dryer": "Pengering rambut", + "toothbrush": "Sikat gigi", + "scissors": "Gunting", + "clock": "Jam", + "umbrella": "Payung", + "shoe": "Sepatu", + "eye_glasses": "Kacamata", + "handbag": "Tas tangan", + "tie": "Dasi", + "suitcase": "Koper", + "frisbee": "Frisbee", + "skis": "Ski", + "snowboard": "Papan seluncur salju", + "sports_ball": "Bola olahraga", + "kite": "Layang-layang", + "baseball_bat": "Tongkat bisbol", + "baseball_glove": "Sarung tangan bisbol", + "surfboard": "Papan selancar", + "tennis_racket": "Raket tenis", + "bottle": "Botol", + "plate": "Piring", + "wine_glass": "Gelas anggur", + "cup": "Cangkir", + "fork": "Garpu", + "knife": "Pisau", + "spoon": "Sendok", + "bowl": "Mangkuk", + "banana": "Pisang", + "apple": "Apel", + "sandwich": "Sandwich", + "orange": "Jeruk", + "broccoli": "Brokoli", + "carrot": "Wortel", + "hot_dog": "Hot dog", + "pizza": "Pizza", + "donut": "Donat", + "cake": "Kue", + "chair": "Kursi", + "couch": "Sofa", + "potted_plant": "Tanaman dalam pot", + "bed": "Tempat tidur", + "mirror": "Cermin", + "dining_table": "Meja makan", + "window": "Jendela", + "desk": "Meja tulis", + "toilet": "Toilet", + "tv": "TV", + "laptop": "Laptop", + "remote": "Remote", + "cell_phone": "Ponsel", + "microwave": "Microwave", + "oven": "Oven", + "toaster": "Pemanggang roti", + "refrigerator": "Kulkas", + "book": "Buku", + "vase": "Vas", + "teddy_bear": "Boneka beruang", + "hair_brush": "Sikat rambut", + "squirrel": "Tupai", + "deer": "Rusa", + "fox": "Rubah", + "rabbit": "Kelinci", + "raccoon": "Rakuns", + "robot_lawnmower": "Mesin pemotong rumput robot", + "waste_bin": "Tempat sampah", + "on_demand": "Sesuai permintaan", + "face": "Wajah", + "license_plate": "Pelat nomor", + "package": "Paket", + "bbq_grill": "Panggangan BBQ", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD", + "canada_post": "Canada Post", + "royal_mail": "Royal Mail", + "school_bus": "Bus sekolah", + "skunk": "Sigung", + "kangaroo": "Kanguru", + "baby": "Bayi", + "baby_stroller": "Kereta dorong bayi", + "rickshaw": "Becak", + "rodent": "Hewan pengerat" +} diff --git a/web/public/locales/id/views/chat.json b/web/public/locales/id/views/chat.json new file mode 100644 index 0000000..4fc251a --- /dev/null +++ b/web/public/locales/id/views/chat.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "Chat - Frigate" +} diff --git a/web/public/locales/id/views/classificationModel.json b/web/public/locales/id/views/classificationModel.json new file mode 100644 index 0000000..9ea38ad --- /dev/null +++ b/web/public/locales/id/views/classificationModel.json @@ -0,0 +1,93 @@ +{ + "documentTitle": "Klasifikasi Model - Frigate", + "details": { + "scoreInfo": "Skor tersebut mewakili rata-rata kepercayaan klasifikasi di seluruh deteksi objek ini.", + "none": "Tidak Ada", + "unknown": "Tidak diketahui" + }, + "button": { + "deleteClassificationAttempts": "Hapus Gambar Klasifikasi", + "renameCategory": "Ganti Nama Class", + "deleteCategory": "Hapus Class", + "deleteImages": "Hapus Gambar", + "trainModel": "Latih Model", + "addClassification": "Tambah Klasifikasi", + "deleteModels": "Hapus Model", + "editModel": "Ubah Model" + }, + "tooltip": { + "trainingInProgress": "Model sedang training", + "noNewImages": "Tidak ada gambar baru untuk training. Klasifikasi lebih banyak gambar di dataset terlebih dahulu.", + "noChanges": "Tidak ada perubahan dataset sejak latihan terakhir.", + "modelNotReady": "Model tidak siap untuk dilatih" + }, + "toast": { + "success": { + "deletedCategory_other": "Class Dihapus", + "deletedImage_other": "Image dihapus", + "deletedModel_other": "Berhasil menghapus {{count}} model", + "categorizedImage": "Berhasil Mengklasifikasikan Gambar", + "trainedModel": "Berhasil melatih model.", + "trainingModel": "Berhasil memulai pelatihan model.", + "updatedModel": "Berhasil memperbarui konfigurasi model", + "renamedCategory": "Berhasil mengganti nama class ke {{name}}" + }, + "error": { + "updateModelFailed": "Gagal update model: {{errorMessage}}", + "renameCategoryFailed": "Gagal merubah penamaan kelas: {{errorMessage}}", + "deleteImageFailed": "Gagal menghapus: {{errorMessage}}", + "deleteCategoryFailed": "Gagal menghapus kelas: {{errorMessage}}", + "deleteModelFailed": "Gagal menghapus model: {{errorMessage}}", + "categorizeFailed": "Gagal mengkategorikan gambar: {{errorMessage}}", + "trainingFailed": "Gagal melakukan training model. Cek log Frigate untuk rinciannya.", + "trainingFailedToStart": "Gagal memulai training model: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Kelas dihapus", + "minClassesTitle": "Dilarang menghapus Kelas", + "desc": "Apakah Anda yakin ingin menghapus class {{name}}? Ini akan menghapus semua gambar terkait secara permanen dan memerlukan re-training model.", + "minClassesDesc": "Model klasifikasi harus memiliki setidaknya 2 class. Tambahkan class lain sebelum menghapus yang ini." + }, + "train": { + "titleShort": "Terkini" + }, + "wizard": { + "title": "Buat Klasifikasi Baru", + "steps": { + "nameAndDefine": "Nama & Definisi", + "stateArea": "Pilih Area", + "chooseExamples": "Pilih Contoh" + }, + "step1": { + "description": "State model memantau area kamera yang tetap untuk setiap perubahan (contoh: pintu terbuka/tertutup). Object model menambahkan klasifikasi pada objek yang terdeteksi (contoh: hewan tertentu, kurir, dll.).", + "name": "Nama", + "namePlaceholder": "Masukkan nama model...", + "type": "Tipe", + "typeState": "Status", + "typeObject": "Objek", + "objectLabel": "Label Objek", + "objectLabelPlaceholder": "Pilih tipe objek...", + "classificationType": "Pilih Klasifikasi", + "classificationTypeTip": "Pelajari tentang tipe klasifikasi", + "classificationTypeDesc": "Sub Label menambahkan teks tambahan pada label objek (contoh: 'Orang: UPS'). Atribut adalah metadata yang dapat dicari dan disimpan secara terpisah di dalam metadata objek.", + "classificationSubLabel": "Sub Label", + "classificationAttribute": "Atribut", + "classes": "Class", + "classesTip": "Pelajari tentang class", + "classesStateDesc": "Tentukan berbagai status (state) pada area kamera Anda. Contoh: 'terbuka' dan 'tertutup' untuk pintu garasi.", + "classesObjectDesc": "Tentukan kategori berbeda untuk mengklasifikasikan objek yang terdeteksi. Contoh: 'kurir', 'penghuni', 'orang_asing' untuk klasifikasi orang.", + "classPlaceholder": "Masukkan nama class...", + "errors": { + "nameRequired": "Nama model wajib diisi", + "nameLength": "Nama model maksimal 64 karakter", + "nameOnlyNumbers": "Nama model tidak boleh hanya berisi angka", + "classRequired": "Setidaknya harus ada 1 class yang diisi", + "classesUnique": "Nama class harus unik", + "stateRequiresTwoClasses": "State model memerlukan minimal 2 class", + "objectLabelRequired": "Silakan pilih label objek", + "objectTypeRequired": "Silakan pilih tipe klasifikasi" + } + } + } +} diff --git a/web/public/locales/id/views/configEditor.json b/web/public/locales/id/views/configEditor.json new file mode 100644 index 0000000..a4d7bae --- /dev/null +++ b/web/public/locales/id/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor Konfigurasi - Frigate", + "configEditor": "Editor Konfigurasi", + "copyConfig": "Salin Konfigurasi", + "saveAndRestart": "Simpan dan Mulai ulang", + "saveOnly": "Hanya simpan", + "toast": { + "success": { + "copyToClipboard": "Konfigurasi disalin ke papan klip." + }, + "error": { + "savingError": "Gagal menyimpan konfigurasi" + } + }, + "confirm": "Keluar tanpa menyimpan?", + "safeModeDescription": "Frigate sedang dalam mode aman karena kesalahan validasi konfigurasi.", + "safeConfigEditor": "Editor Konfigurasi(Mode Aman)" +} diff --git a/web/public/locales/id/views/events.json b/web/public/locales/id/views/events.json new file mode 100644 index 0000000..bb78ea7 --- /dev/null +++ b/web/public/locales/id/views/events.json @@ -0,0 +1,94 @@ +{ + "alerts": "Peringatan", + "detections": "Deteksi", + "motion": { + "label": "Gerakan", + "only": "Hanya Gerakan" + }, + "allCameras": "Semua Kamera", + "empty": { + "detection": "Tidak ada deteksi untuk ditinjau", + "alert": "Tidak ada peringatan untuk ditinjau", + "motion": "Data gerakan tidak ditemukan", + "recordingsDisabled": { + "title": "Perekaman harus di aktifkan", + "description": "Ulasan item hanya dapat dibuat untuk kamera jika perekaman diaktifkan untuk kamera tersebut." + } + }, + "timeline.aria": "Pilih timeline", + "timeline": { + "label": "Linimasa" + }, + "zoomIn": "Perbesar", + "zoomOut": "Perkecil", + "events": { + "label": "Peristiwa-Peristiwa", + "aria": "Pilih peristiwa", + "noFoundForTimePeriod": "Tidak ada peristiwa dalam periode waktu berikut." + }, + "detail": { + "label": "Detil", + "noDataFound": "Tidak ada detil data untuk di review", + "aria": "Beralih tampilan detil", + "trackedObject_one": "{{count}} objek", + "trackedObject_other": "{{count}} objek", + "noObjectDetailData": "Tidak ada data objek detil tersedia.", + "settings": "Pengaturan Tampilan Detil", + "alwaysExpandActive": { + "title": "Selalu lebarkan yang aktif", + "desc": "Selalu perluas detil objek item tinjauan aktif jika tersedia." + } + }, + "objectTrack": { + "trackedPoint": "Titik terlacak", + "clickToSeek": "Klik untuk mencari waktu ini" + }, + "documentTitle": "Tinjauan - Frigate", + "recordings": { + "documentTitle": "Rekaman - Frigate", + "invalidSharedLink": "Tidak dapat membuka tautan rekaman bertanda waktu karena kesalahan penguraian.", + "invalidSharedCamera": "Tidak dapat membuka tautan rekaman bertanda waktu karena kamera tidak dikenal atau tidak berwenang." + }, + "calendarFilter": { + "last24Hours": "24 Jam Terakhir" + }, + "markAsReviewed": "Tandai sebagai sudah ditinjau", + "markTheseItemsAsReviewed": "Tandai item-item berikut sebagai sudah ditinjau", + "newReviewItems": { + "button": "Item Batu Untuk Ditinjau", + "label": "Lihat item ulasan baru" + }, + "selected_one": "{{count}} dipilih", + "selected_other": "{{count}} dipilih", + "camera": "Kamera", + "detected": "terdeteksi", + "suspiciousActivity": "Aktivitas Mencurigakan", + "threateningActivity": "Aktivitas yang Mengancam", + "select_all": "Semua", + "normalActivity": "Normal", + "needsReview": "Perlu ditinjau", + "securityConcern": "Kendala keamanan", + "motionSearch": { + "menuItem": "Pencarian gerakan", + "openMenu": "Opsi kamera" + }, + "motionPreviews": { + "menuItem": "Lihat pratinjau gerakan", + "title": "Pratinjau gerakan: {{camera}}", + "mobileSettingsTitle": "Setelan Pratinjau Gerakan", + "mobileSettingsDesc": "Sesuaikan kecepatan pemutaran dan peredupan, serta pilih tanggal untuk meninjau klip hanya gerakan.", + "dim": "Redup", + "dimAria": "Sesuaikan intensitas peredupan", + "dimDesc": "Tingkatkan peredupan untuk meningkatkan visibilitas area gerakan.", + "speed": "Kecepatan", + "speedAria": "Pilih kecepatan pemutaran pratinjau", + "speedDesc": "Pilih seberapa cepat klip pratinjau diputar.", + "back": "Kembali", + "empty": "Tidak ada pratinjau tersedia", + "noPreview": "Pratinjau tidak tersedia", + "seekAria": "Pindahkan {{camera}} pemain ke {{time}}", + "filter": "Filter", + "filterDesc": "Pilih area untuk hanya menampilkan klip dengan gerakan di wilayah tersebut.", + "filterClear": "Hapus" + } +} diff --git a/web/public/locales/id/views/explore.json b/web/public/locales/id/views/explore.json new file mode 100644 index 0000000..726548d --- /dev/null +++ b/web/public/locales/id/views/explore.json @@ -0,0 +1,263 @@ +{ + "documentTitle": "Jelajahi - Frigate", + "generativeAI": "AI Generatif", + "exploreIsUnavailable": { + "title": "Jelajah Tidak Tersedia", + "embeddingsReindexing": { + "context": "Jelajah dapat digunakan setelah embedding objek terlacak selesai diindeks ulang.", + "startingUp": "Sedang memulai…", + "estimatedTime": "Perkiraan waktu tersisa:", + "finishingShortly": "Segera selesai", + "step": { + "thumbnailsEmbedded": "Thumbnail yang disematkan: ", + "descriptionsEmbedded": "Deskripsi yang disematkan: ", + "trackedObjectsProcessed": "Objek terlacak yang diproses: " + } + }, + "downloadingModels": { + "context": "Frigate sedang mengunduh model embedding yang diperlukan untuk mendukung fitur Pencarian Semantik. Ini mungkin memerlukan beberapa menit tergantung pada kecepatan koneksi jaringan Anda.", + "setup": { + "visionModel": "Model visi", + "visionModelFeatureExtractor": "Pengekstrak fitur model visi", + "textModel": "Model teks", + "textTokenizer": "Tokenizer teks" + }, + "tips": { + "context": "Anda mungkin ingin mengindeks ulang embedding objek terlacak Anda setelah model selesai diunduh." + }, + "error": "Terjadi kesalahan. Periksa log Frigate." + } + }, + "details": { + "timestamp": "Cap waktu", + "item": { + "title": "Detail Item Tinjauan", + "desc": "Detail item tinjauan", + "button": { + "share": "Bagikan item tinjauan ini", + "viewInExplore": "Lihat di Jelajah" + }, + "tips": { + "mismatch_other": "{{count}} objek yang tidak tersedia terdeteksi dan disertakan dalam item tinjauan ini. Objek-objek tersebut tidak memenuhi syarat sebagai peringatan atau deteksi, atau sudah dibersihkan/dihapus.", + "hasMissingObjects": "Sesuaikan konfigurasi Anda jika Anda ingin Frigate menyimpan objek terlacak untuk label berikut: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "Deskripsi baru telah diminta dari {{provider}}. Tergantung pada kecepatan penyedia Anda, deskripsi baru mungkin memerlukan waktu untuk dibuat ulang.", + "updatedSublabel": "Berhasil memperbarui sublabel.", + "updatedLPR": "Berhasil memperbarui pelat nomor.", + "updatedAttributes": "Berhasil memperbarui atribut.", + "audioTranscription": "Berhasil meminta transkripsi audio. Tergantung pada kecepatan server Frigate Anda, transkripsi mungkin memerlukan waktu untuk selesai." + }, + "error": { + "regenerate": "Gagal memanggil {{provider}} untuk deskripsi baru: {{errorMessage}}", + "updatedSublabelFailed": "Gagal memperbarui sublabel: {{errorMessage}}", + "updatedLPRFailed": "Gagal memperbarui pelat nomor: {{errorMessage}}", + "updatedAttributesFailed": "Gagal memperbarui atribut: {{errorMessage}}", + "audioTranscription": "Gagal meminta transkripsi audio: {{errorMessage}}" + } + } + }, + "label": "Label", + "editSubLabel": { + "title": "Edit sublabel", + "desc": "Masukkan sublabel baru untuk {{label}} ini", + "descNoLabel": "Masukkan sublabel baru untuk objek terlacak ini" + }, + "editLPR": { + "title": "Edit pelat nomor", + "desc": "Masukkan nilai pelat nomor baru untuk {{label}} ini", + "descNoLabel": "Masukkan nilai pelat nomor baru untuk objek terlacak ini" + }, + "editAttributes": { + "title": "Edit atribut", + "desc": "Pilih atribut klasifikasi untuk {{label}} ini" + }, + "snapshotScore": { + "label": "Skor Snapshot" + }, + "topScore": { + "label": "Skor Tertinggi", + "info": "Skor tertinggi adalah skor median tertinggi untuk objek terlacak, jadi ini mungkin berbeda dari skor yang ditampilkan pada thumbnail hasil pencarian." + }, + "score": { + "label": "Skor" + }, + "recognizedLicensePlate": "Pelat Nomor yang Diakui", + "attributes": "Atribut Klasifikasi", + "estimatedSpeed": "Perkiraan Kecepatan", + "objects": "Objek", + "camera": "Kamera", + "zones": "Zona", + "button": { + "findSimilar": "Cari yang Serupa", + "regenerate": { + "title": "Buat Ulang", + "label": "Buat ulang deskripsi objek terlacak" + } + }, + "description": { + "label": "Deskripsi", + "placeholder": "Deskripsi objek terlacak", + "aiTips": "Frigate tidak akan meminta deskripsi dari penyedia AI Generatif Anda sampai siklus hidup objek terlacak berakhir." + }, + "expandRegenerationMenu": "Perluas menu pembuatan ulang", + "regenerateFromSnapshot": "Buat Ulang dari Snapshot", + "regenerateFromThumbnails": "Buat Ulang dari Thumbnail", + "tips": { + "descriptionSaved": "Berhasil menyimpan deskripsi", + "saveDescriptionFailed": "Gagal memperbarui deskripsi: {{errorMessage}}" + }, + "title": { + "label": "Judul" + }, + "scoreInfo": "Informasi Skor" + }, + "exploreMore": "Jelajahi lebih banyak objek {{label}}", + "trackedObjectDetails": "Detail Objek Terlacak", + "type": { + "details": "detail", + "snapshot": "snapshot", + "thumbnail": "thumbnail", + "video": "video", + "tracking_details": "detail pelacakan" + }, + "trackingDetails": { + "title": "Detail Pelacakan", + "noImageFound": "Tidak ada gambar yang ditemukan untuk cap waktu ini.", + "createObjectMask": "Buat Masker Objek", + "adjustAnnotationSettings": "Sesuaikan pengaturan anotasi", + "scrollViewTips": "Klik untuk melihat momen-momen penting dalam siklus hidup objek ini.", + "autoTrackingTips": "Posisi kotak pembatas tidak akan akurat untuk kamera dengan pelacakan otomatis.", + "count": "{{first}} dari {{second}}", + "trackedPoint": "Titik Terlacak", + "lifecycleItemDesc": { + "visible": "{{label}} terdeteksi", + "entered_zone": "{{label}} memasuki {{zones}}", + "active": "{{label}} menjadi aktif", + "stationary": "{{label}} menjadi diam", + "attribute": { + "faceOrLicense_plate": "{{attribute}} terdeteksi untuk {{label}}", + "other": "{{label}} dikenali sebagai {{attribute}}" + }, + "gone": "{{label}} pergi", + "heard": "{{label}} terdengar", + "external": "{{label}} terdeteksi", + "header": { + "zones": "Zona", + "ratio": "Rasio", + "area": "Area", + "score": "Skor", + "computedScore": "Skor Terhitung", + "topScore": "Skor Tertinggi", + "toggleAdvancedScores": "Alihkan skor lanjutan" + } + }, + "annotationSettings": { + "title": "Pengaturan Anotasi", + "showAllZones": { + "title": "Tampilkan Semua Zona", + "desc": "Selalu tampilkan zona pada frame tempat objek telah memasuki suatu zona." + }, + "offset": { + "label": "Offset Anotasi", + "desc": "Data ini berasal dari feed deteksi kamera Anda tetapi ditumpangkan pada gambar dari feed rekaman. Sangat mungkin kedua stream tersebut tidak sinkron sepenuhnya. Akibatnya, kotak pembatas dan rekaman tidak akan sejajar dengan sempurna. Anda dapat menggunakan pengaturan ini untuk menggeser anotasi maju atau mundur dalam waktu agar lebih selaras dengan rekaman video.", + "millisecondsToOffset": "Milidetik untuk menggeser anotasi deteksi. Default: 0", + "tips": "Turunkan nilainya jika pemutaran video berada di depan kotak dan titik jalur, dan naikkan nilainya jika pemutaran video berada di belakangnya. Nilai ini bisa negatif.", + "toast": { + "success": "Offset anotasi untuk {{camera}} telah disimpan ke file konfigurasi." + } + } + }, + "carousel": { + "previous": "Slide sebelumnya", + "next": "Slide berikutnya" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "Unduh video", + "aria": "Unduh video" + }, + "downloadSnapshot": { + "label": "Unduh snapshot", + "aria": "Unduh snapshot" + }, + "downloadCleanSnapshot": { + "label": "Unduh snapshot bersih", + "aria": "Unduh snapshot bersih" + }, + "viewTrackingDetails": { + "label": "Lihat detail pelacakan", + "aria": "Tampilkan detail pelacakan" + }, + "findSimilar": { + "label": "Cari yang serupa", + "aria": "Cari objek terlacak yang serupa" + }, + "addTrigger": { + "label": "Tambahkan pemicu", + "aria": "Tambahkan pemicu untuk objek terlacak ini" + }, + "audioTranscription": { + "label": "Transkripsikan", + "aria": "Minta transkripsi audio" + }, + "submitToPlus": { + "label": "Kirim ke Frigate+", + "aria": "Kirim ke Frigate Plus" + }, + "viewInHistory": { + "label": "Lihat di Riwayat", + "aria": "Lihat di Riwayat" + }, + "deleteTrackedObject": { + "label": "Hapus objek terlacak ini" + }, + "showObjectDetails": { + "label": "Tampilkan jalur objek" + }, + "hideObjectDetails": { + "label": "Sembunyikan jalur objek" + }, + "debugReplay": { + "label": "Pemutaran Ulang Debug", + "aria": "Lihat objek terlacak ini dalam tampilan pemutaran ulang debug" + }, + "more": { + "aria": "Lainnya" + } + }, + "dialog": { + "confirmDelete": { + "title": "Konfirmasi Hapus", + "desc": "Menghapus objek terlacak ini akan menghapus snapshot, embedding yang tersimpan, dan entri detail pelacakan terkait. Rekaman video dari objek terlacak ini di tampilan Riwayat TIDAK akan dihapus.

    Anda yakin ingin melanjutkan?" + }, + "toast": { + "error": "Kesalahan saat menghapus objek terlacak ini: {{errorMessage}}" + } + }, + "noTrackedObjects": "Tidak Ada Objek Terlacak Ditemukan", + "fetchingTrackedObjectsFailed": "Kesalahan saat mengambil objek terlacak: {{errorMessage}}", + "trackedObjectsCount_other": "{{count}} objek terlacak ", + "searchResult": { + "tooltip": "Cocok dengan {{type}} pada {{confidence}}%", + "previousTrackedObject": "Objek terlacak sebelumnya", + "nextTrackedObject": "Objek terlacak berikutnya", + "deleteTrackedObject": { + "toast": { + "success": "Objek terlacak berhasil dihapus.", + "error": "Gagal menghapus objek terlacak: {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "Analisis AI" + }, + "concerns": { + "label": "Kekhawatiran" + }, + "objectLifecycle": { + "noImageFound": "Tidak ada gambar yang ditemukan untuk objek terlacak ini." + } +} diff --git a/web/public/locales/id/views/exports.json b/web/public/locales/id/views/exports.json new file mode 100644 index 0000000..bee59ce --- /dev/null +++ b/web/public/locales/id/views/exports.json @@ -0,0 +1,128 @@ +{ + "documentTitle": "Ekspor - Frigate", + "search": "Pencarian", + "noExports": "Tidak ada ekspor ditemukan", + "deleteExport": { + "label": "Hapus Ekspor" + }, + "deleteExport.desc": "Anda yakin ingin menghapus {{exportName}}?", + "editExport": { + "title": "Ubah Nama Ekspor", + "desc": "Masukkan nama baru untuk ekspor ini.", + "saveExport": "Simpan Ekspor" + }, + "toast": { + "error": { + "renameExportFailed": "Gagal mengubah nama ekspor: {{errorMessage}}", + "assignCaseFailed": "Gagal memperbarui penetapan kasus: {{errorMessage}}", + "caseSaveFailed": "Gagal menyimpan kasus: {{errorMessage}}", + "caseDeleteFailed": "Gagal menghapus kasus: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Bagikan ekspor", + "downloadVideo": "Unduh video", + "editName": "Ubah nama", + "deleteExport": "Hapus ekspor", + "assignToCase": "Tambahkan ke kasus", + "removeFromCase": "Hapus dari kasus" + }, + "headings": { + "cases": "Kasus", + "uncategorizedExports": "Ekspor Tanpa Kategori" + }, + "toolbar": { + "newCase": "Kasus Baru", + "addExport": "Tambahkan Ekspor", + "editCase": "Edit Kasus", + "deleteCase": "Hapus Kasus" + }, + "deleteCase": { + "label": "Hapus Kasus", + "desc": "Anda yakin ingin menghapus {{caseName}}?", + "descKeepExports": "Ekspor akan tetap tersedia sebagai ekspor tanpa kategori.", + "descDeleteExports": "Semua ekspor dalam kasus ini akan dihapus secara permanen.", + "deleteExports": "Hapus juga ekspor" + }, + "caseDialog": { + "title": "Tambahkan ke kasus", + "description": "Pilih kasus yang sudah ada atau buat yang baru.", + "selectLabel": "Kasus", + "newCaseOption": "Buat kasus baru", + "nameLabel": "Nama kasus", + "descriptionLabel": "Deskripsi" + }, + "caseCard": { + "emptyCase": "Belum ada ekspor" + }, + "jobCard": { + "defaultName": "Ekspor {{camera}}", + "queued": "Dalam antrean", + "running": "Sedang berjalan", + "preparing": "Menyiapkan", + "copying": "Menyalin", + "encoding": "Menyandi", + "encodingRetry": "Menyandi (coba lagi)", + "finalizing": "Menyelesaikan" + }, + "caseView": { + "noDescription": "Tidak ada deskripsi", + "createdAt": "Dibuat {{value}}", + "exportCount_one": "1 ekspor", + "exportCount_other": "{{count}} ekspor", + "cameraCount_one": "1 kamera", + "cameraCount_other": "{{count}} kamera", + "showMore": "Tampilkan lebih banyak", + "showLess": "Tampilkan lebih sedikit", + "emptyTitle": "Kasus ini kosong", + "emptyDescription": "Tambahkan ekspor tanpa kategori yang sudah ada agar kasus tetap terorganisasi.", + "emptyDescriptionNoExports": "Belum ada ekspor tanpa kategori yang tersedia untuk ditambahkan." + }, + "caseEditor": { + "createTitle": "Buat Kasus", + "editTitle": "Edit Kasus", + "namePlaceholder": "Nama kasus", + "descriptionPlaceholder": "Tambahkan catatan atau konteks untuk kasus ini" + }, + "addExportDialog": { + "title": "Tambahkan Ekspor ke {{caseName}}", + "searchPlaceholder": "Cari ekspor tanpa kategori", + "empty": "Tidak ada ekspor tanpa kategori yang cocok dengan pencarian ini.", + "addButton_one": "Tambahkan 1 Ekspor", + "addButton_other": "Tambahkan {{count}} Ekspor", + "adding": "Menambahkan..." + }, + "selected_one": "{{count}} dipilih", + "selected_other": "{{count}} dipilih", + "bulkActions": { + "addToCase": "Tambahkan ke Kasus", + "moveToCase": "Pindahkan ke Kasus", + "removeFromCase": "Hapus dari Kasus", + "delete": "Hapus", + "deleteNow": "Hapus Sekarang" + }, + "bulkDelete": { + "title": "Hapus Ekspor", + "desc_one": "Anda yakin ingin menghapus {{count}} ekspor?", + "desc_other": "Anda yakin ingin menghapus {{count}} ekspor?" + }, + "bulkRemoveFromCase": { + "title": "Hapus dari Kasus", + "desc_one": "Hapus {{count}} ekspor dari kasus ini?", + "desc_other": "Hapus {{count}} ekspor dari kasus ini?", + "descKeepExports": "Ekspor akan dipindahkan ke tanpa kategori.", + "descDeleteExports": "Ekspor akan dihapus secara permanen.", + "deleteExports": "Hapus ekspor saja" + }, + "bulkToast": { + "success": { + "delete": "Berhasil menghapus ekspor", + "reassign": "Berhasil memperbarui penetapan kasus", + "remove": "Berhasil menghapus ekspor dari kasus" + }, + "error": { + "deleteFailed": "Gagal menghapus ekspor: {{errorMessage}}", + "reassignFailed": "Gagal memperbarui penetapan kasus: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/id/views/faceLibrary.json b/web/public/locales/id/views/faceLibrary.json new file mode 100644 index 0000000..ec0c700 --- /dev/null +++ b/web/public/locales/id/views/faceLibrary.json @@ -0,0 +1,93 @@ +{ + "description": { + "addFace": "Tambah ke koleksi Pustaka Wajah dengan men-upload gambar pertama anda.", + "placeholder": "Masukkan Nama untuk koleksi ini", + "invalidName": "Nama tidak valid. Nama hanya dapat berisi huruf, angka, spasi, apostrof, garis bawah, dan tanda hubung." + }, + "details": { + "person": "Orang", + "subLabelScore": "Skor Sub Label", + "face": "Detail Wajah", + "scoreInfo": "Skor sub label adalah nilai gabungan dari tingkat keyakinan sistem dalam mengenali wajah. Nilai ini bisa berbeda dengan skor yang terlihat pada gambar cuplikan.", + "timestamp": "Cap waktu", + "unknown": "Tidak diketahui", + "faceDesc": "Detail objek terlacak yang menghasilkan wajah ini" + }, + "documentTitle": "Perpustakaan Wajah - Frigate", + "collections": "Koleksi", + "createFaceLibrary": { + "desc": "Buat koleksi baru", + "title": "Buat Koleksi", + "nextSteps": "Untuk membangun fondasi yang kuat:
  • Gunakan tab Pengenalan Terbaru untuk memilih dan melatih gambar untuk setiap orang yang terdeteksi.
  • Fokus pada gambar langsung untuk hasil terbaik; hindari melatih gambar yang menangkap wajah pada sudut tertentu.
  • ", + "new": "Buat Wajah Baru" + }, + "uploadFaceImage": { + "desc": "Unggah gambar untuk dipindai wajah dan sertakan untuk {{pageToggle}}", + "title": "Unggah Gambar Wajah" + }, + "steps": { + "faceName": "Masukkan Nama Wajah", + "uploadFace": "Unggah Gambar Wajah", + "nextSteps": "Langkah Berikutnya", + "description": { + "uploadFace": "Upload sebuah gambar dari {{name}} yang menunjukkan wajah mereka dari sisi depan. Gambar tidak perlu dipotong ke wajah mereka." + } + }, + "train": { + "title": "Pengenalan Terkini", + "aria": "Pilih pengenalan terkini", + "empty": "Tidak ada percobaan pengenalan wajah baru-baru ini", + "titleShort": "Terkini" + }, + "deleteFaceLibrary": { + "title": "Hapus Nama", + "desc": "Apakah anda yakin ingin menghapus koleksi {{name}}? Ini akan menghapus semua wajah terkait secara permanen." + }, + "deleteFaceAttempts": { + "title": "Hapus Wajah-Wajah", + "desc_other": "Apakah anda yakin ingin menghapis {{count}} wajah? Aksi ini tidak dapat diurungkan." + }, + "renameFace": { + "title": "Ganti Nama Wajah", + "desc": "Masukkan nama baru untuk {{name}}" + }, + "button": { + "deleteFaceAttempts": "Hapus Wajah", + "addFace": "Tambah Wajah", + "renameFace": "Ganti Nama Wajah", + "deleteFace": "Hapus Wajah", + "uploadImage": "Unggah Gambar", + "reprocessFace": "Proses Ulang Wajah" + }, + "imageEntry": { + "validation": { + "selectImage": "Silahkan pilih sebuah file gambar." + }, + "dropActive": "Letakkan gambar di sini…", + "dropInstructions": "Seret dan lepaskan atau tempel gambar di sini, atau klik untuk memilih", + "maxSize": "Ukuran maksimum: {{size}}MB" + }, + "nofaces": "Tidak ada wajah tersedia", + "trainFaceAs": "Latih Gambar sebagai:", + "trainFace": "Latih Wajah", + "toast": { + "success": { + "uploadedImage": "Berhasil men unggah gambar.", + "addFaceLibrary": "{{name}} telah berhasil ditambahkan ke Pustaka Wajah!", + "deletedFace_other": "Berhasil menghapus {{count}} wajah.", + "deletedName_other": "{{count}} wajah telah berhasil dihapus.", + "renamedFace": "Berhasil mengganti nama wajah ke {{name}}", + "trainedFace": "Berhasil melatih wajah.", + "updatedFaceScore": "Berhasil memperbaharui nilai wajah." + }, + "error": { + "uploadingImageFailed": "Gagal menunggah gambar: {{errorMessage}}", + "addFaceLibraryFailed": "Gagal mengatur nama wajah: {{errorMessage}}", + "deleteFaceFailed": "Gagal untuk menghapus: {{errorMessage}}", + "deleteNameFailed": "Gagal menghapus nama: {{errorMessage}}", + "renameFaceFailed": "Gagal mengganti nama wajah: {{errorMessage}}", + "trainFailed": "Gagal untuk melatih: {{errorMessage}}", + "updateFaceScoreFailed": "Gagal untuk memperbaharui nilai wajah: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/id/views/live.json b/web/public/locales/id/views/live.json new file mode 100644 index 0000000..416d688 --- /dev/null +++ b/web/public/locales/id/views/live.json @@ -0,0 +1,200 @@ +{ + "documentTitle.withCamera": "{{camera}} - Live - Frigate", + "documentTitle": { + "default": "Live - Frigate" + }, + "lowBandwidthMode": "Mode bandwidth rendah", + "twoWayTalk": { + "enable": "Aktifkan Audio Dua Arah", + "disable": "Nonaktifkan Audio Dua Arah" + }, + "cameraAudio": { + "enable": "Aktifkan Audio Kamera", + "disable": "Nonaktifkan Audio Kamera" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Klik pada frame untuk memusatkan kamera", + "enable": "Aktifkan klik untuk memindahkan", + "disable": "Nonaktifkan klik untuk memindahkan", + "enableWithZoom": "Aktifkan klik untuk memindahkan / seret untuk memperbesar" + }, + "left": { + "label": "Gerakkan kamera PTZ ke kiri" + }, + "up": { + "label": "Gerakkan kamera PTZ ke atas" + }, + "down": { + "label": "Gerakkan kamera PTZ ke bawah" + }, + "right": { + "label": "Gerakkan kamera PTZ ke kanan" + } + }, + "zoom": { + "in": { + "label": "Perbesar zoom kamera PTZ" + }, + "out": { + "label": "Perkecil zoom kamera PTZ" + } + }, + "focus": { + "in": { + "label": "Fokuskan kamera PTZ ke dalam" + }, + "out": { + "label": "Fokuskan kamera PTZ ke luar" + } + }, + "frame": { + "center": { + "label": "Klik pada frame untuk memusatkan kamera PTZ" + } + }, + "presets": "Preset kamera PTZ" + }, + "camera": { + "enable": "Aktifkan Kamera", + "disable": "Nonaktifkan Kamera" + }, + "muteCameras": { + "enable": "Bisukan Semua Kamera", + "disable": "Suarakan Semua Kamera" + }, + "detect": { + "enable": "Aktifkan Deteksi", + "disable": "Nonaktifkan Deteksi" + }, + "recording": { + "enable": "Aktifkan Perekaman", + "disable": "Nonaktifkan Perekaman", + "disabledInConfig": "Perekaman harus terlebih dahulu diaktifkan di Pengaturan untuk kamera ini." + }, + "snapshots": { + "enable": "Aktifkan Snapshot", + "disable": "Nonaktifkan Snapshot" + }, + "snapshot": { + "takeSnapshot": "Unduh snapshot instan", + "noVideoSource": "Tidak ada sumber video yang tersedia untuk snapshot.", + "captureFailed": "Gagal mengambil snapshot.", + "downloadStarted": "Pengunduhan snapshot dimulai." + }, + "audioDetect": { + "enable": "Aktifkan Deteksi Audio", + "disable": "Nonaktifkan Deteksi Audio" + }, + "transcription": { + "enable": "Aktifkan Transkripsi Audio Langsung", + "disable": "Nonaktifkan Transkripsi Audio Langsung" + }, + "autotracking": { + "enable": "Aktifkan Pelacakan Otomatis", + "disable": "Nonaktifkan Pelacakan Otomatis" + }, + "streamStats": { + "enable": "Tampilkan Statistik Stream", + "disable": "Sembunyikan Statistik Stream" + }, + "manualRecording": { + "title": "Sesuai Permintaan", + "tips": "Unduh snapshot instan atau mulai event manual berdasarkan pengaturan retensi rekaman kamera ini.", + "playInBackground": { + "label": "Putar di latar belakang", + "desc": "Aktifkan opsi ini untuk melanjutkan streaming saat pemutar disembunyikan." + }, + "showStats": { + "label": "Tampilkan Statistik", + "desc": "Aktifkan opsi ini untuk menampilkan statistik stream sebagai overlay pada umpan kamera." + }, + "debugView": "Tampilan Debug", + "start": "Mulai perekaman sesuai permintaan", + "started": "Perekaman manual sesuai permintaan dimulai.", + "failedToStart": "Gagal memulai perekaman manual sesuai permintaan.", + "recordDisabledTips": "Karena perekaman dinonaktifkan atau dibatasi dalam konfigurasi untuk kamera ini, hanya snapshot yang akan disimpan.", + "end": "Akhiri perekaman sesuai permintaan", + "ended": "Perekaman manual sesuai permintaan diakhiri.", + "failedToEnd": "Gagal mengakhiri perekaman manual sesuai permintaan." + }, + "streamingSettings": "Pengaturan Streaming", + "notifications": "Notifikasi", + "audio": "Audio", + "suspend": { + "forTime": "Tangguhkan selama: " + }, + "stream": { + "title": "Stream", + "audio": { + "tips": { + "title": "Audio harus dikeluarkan oleh kamera Anda dan dikonfigurasi di go2rtc untuk stream ini." + }, + "available": "Audio tersedia untuk stream ini", + "unavailable": "Audio tidak tersedia untuk stream ini" + }, + "debug": { + "picker": "Pemilihan stream tidak tersedia dalam mode debug. Tampilan debug selalu menggunakan stream yang ditetapkan ke peran detect." + }, + "twoWayTalk": { + "tips": "Perangkat Anda harus mendukung fitur ini dan WebRTC harus dikonfigurasi untuk audio dua arah.", + "available": "Audio dua arah tersedia untuk stream ini", + "unavailable": "Audio dua arah tidak tersedia untuk stream ini" + }, + "lowBandwidth": { + "tips": "Tampilan live berada dalam mode bandwidth rendah karena buffering atau kesalahan stream.", + "resetStream": "Atur ulang stream" + }, + "playInBackground": { + "label": "Putar di latar belakang", + "tips": "Aktifkan opsi ini untuk melanjutkan streaming saat pemutar disembunyikan." + } + }, + "cameraSettings": { + "title": "Pengaturan {{camera}}", + "cameraEnabled": "Kamera Diaktifkan", + "objectDetection": "Deteksi Objek", + "recording": "Perekaman", + "snapshots": "Snapshot", + "audioDetection": "Deteksi Audio", + "transcription": "Transkripsi Audio", + "autotracking": "Pelacakan Otomatis" + }, + "history": { + "label": "Tampilkan rekaman historis" + }, + "effectiveRetainMode": { + "modes": { + "all": "Semua", + "motion": "Gerakan", + "active_objects": "Objek Aktif" + } + }, + "editLayout": { + "label": "Edit Tata Letak", + "group": { + "label": "Edit Grup Kamera" + }, + "exitEdit": "Keluar dari Mode Edit" + }, + "noCameras": { + "title": "Tidak Ada Kamera yang Dikonfigurasi", + "description": "Mulai dengan menghubungkan kamera ke Frigate.", + "buttonText": "Tambahkan Kamera", + "restricted": { + "title": "Tidak Ada Kamera yang Tersedia", + "description": "Anda tidak memiliki izin untuk melihat kamera apa pun di grup ini." + }, + "default": { + "title": "Tidak Ada Kamera yang Dikonfigurasi", + "description": "Mulai dengan menghubungkan kamera ke Frigate.", + "buttonText": "Tambahkan Kamera" + }, + "group": { + "title": "Tidak Ada Kamera dalam Grup", + "description": "Grup kamera ini tidak memiliki kamera yang ditetapkan atau diaktifkan.", + "buttonText": "Kelola Grup" + } + } +} diff --git a/web/public/locales/id/views/motionSearch.json b/web/public/locales/id/views/motionSearch.json new file mode 100644 index 0000000..3a098d0 --- /dev/null +++ b/web/public/locales/id/views/motionSearch.json @@ -0,0 +1,73 @@ +{ + "documentTitle": "Pencarian Gerakan - Frigate", + "title": "Pencarian Gerakan", + "description": "Gambar poligon untuk menentukan wilayah yang diminati, lalu tentukan rentang waktu untuk mencari perubahan gerakan di dalam wilayah tersebut.", + "selectCamera": "Pencarian Gerakan sedang dimuat", + "startSearch": "Mulai Pencarian", + "searchStarted": "Pencarian dimulai", + "searchCancelled": "Pencarian dibatalkan", + "cancelSearch": "Batal", + "searching": "Pencarian sedang berlangsung.", + "searchComplete": "Pencarian selesai", + "noResultsYet": "Jalankan pencarian untuk menemukan perubahan gerakan di wilayah yang dipilih", + "noChangesFound": "Tidak ada perubahan piksel yang terdeteksi di wilayah yang dipilih", + "changesFound_other": "Ditemukan {{count}} perubahan gerakan", + "framesProcessed": "{{count}} frame diproses", + "jumpToTime": "Lompat ke waktu ini", + "results": "Hasil", + "showSegmentHeatmap": "Peta panas", + "newSearch": "Pencarian Baru", + "clearResults": "Hapus Hasil", + "clearROI": "Hapus poligon", + "polygonControls": { + "points_other": "{{count}} titik", + "undo": "Urungkan titik terakhir", + "reset": "Atur ulang poligon" + }, + "motionHeatmapLabel": "Peta Panas Gerakan", + "dialog": { + "title": "Pencarian Gerakan", + "cameraLabel": "Kamera", + "previewAlt": "Pratinjau kamera untuk {{camera}}" + }, + "timeRange": { + "title": "Rentang Pencarian", + "start": "Waktu mulai", + "end": "Waktu selesai" + }, + "settings": { + "title": "Pengaturan Pencarian", + "parallelMode": "Mode paralel", + "parallelModeDesc": "Pindai beberapa segmen rekaman secara bersamaan (lebih cepat, tetapi penggunaan CPU jauh lebih tinggi)", + "threshold": "Ambang Sensitivitas", + "thresholdDesc": "Nilai yang lebih rendah mendeteksi perubahan yang lebih kecil (1-255)", + "minArea": "Luas Perubahan Minimum", + "minAreaDesc": "Persentase minimum dari wilayah yang diminati yang harus berubah agar dianggap signifikan", + "frameSkip": "Lewati Frame", + "frameSkipDesc": "Proses setiap frame ke-N. Atur ini ke frame rate kamera Anda untuk memproses satu frame per detik (misalnya 5 untuk kamera 5 FPS, 30 untuk kamera 30 FPS). Nilai yang lebih tinggi akan lebih cepat, tetapi bisa melewatkan kejadian gerakan singkat.", + "maxResults": "Jumlah Hasil Maksimum", + "maxResultsDesc": "Berhenti setelah sebanyak ini cap waktu yang cocok" + }, + "errors": { + "noCamera": "Silakan pilih kamera", + "noROI": "Silakan gambar wilayah yang diminati", + "noTimeRange": "Silakan pilih rentang waktu", + "invalidTimeRange": "Waktu selesai harus setelah waktu mulai", + "searchFailed": "Pencarian gagal: {{message}}", + "polygonTooSmall": "Poligon harus memiliki setidaknya 3 titik", + "unknown": "Kesalahan tidak diketahui" + }, + "changePercentage": "{{percentage}}% berubah", + "metrics": { + "title": "Metrik Pencarian", + "segmentsScanned": "Segmen dipindai", + "segmentsProcessed": "Diproses", + "segmentsSkippedInactive": "Dilewati (tidak ada aktivitas)", + "segmentsSkippedHeatmap": "Dilewati (tidak ada tumpang tindih ROI)", + "fallbackFullRange": "Pemindaian rentang penuh cadangan", + "framesDecoded": "Frame didekode", + "wallTime": "Waktu pencarian", + "segmentErrors": "Kesalahan segmen", + "seconds": "{{seconds}} dtk" + } +} diff --git a/web/public/locales/id/views/recording.json b/web/public/locales/id/views/recording.json new file mode 100644 index 0000000..8d5970f --- /dev/null +++ b/web/public/locales/id/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Saring", + "export": "Expor", + "calendar": "Kalender", + "filters": "Penyaring", + "toast": { + "error": { + "noValidTimeSelected": "Rentan Waktu yang dipilih tidak valid", + "endTimeMustAfterStartTime": "Waktu akhir harus setelah waktu mulai" + } + } +} diff --git a/web/public/locales/id/views/replay.json b/web/public/locales/id/views/replay.json new file mode 100644 index 0000000..d47dbd0 --- /dev/null +++ b/web/public/locales/id/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "Pemutaran Ulang Debug", + "description": "Putar ulang rekaman kamera untuk debugging. Daftar objek menampilkan ringkasan objek terdeteksi yang tertunda waktu, dan tab Pesan menampilkan aliran pesan internal Frigate dari rekaman pemutaran ulang.", + "websocket_messages": "Pesan", + "dialog": { + "title": "Mulai Pemutaran Ulang Debug", + "description": "Buat kamera pemutaran ulang sementara yang memutar berulang rekaman historis untuk men-debug masalah deteksi dan pelacakan objek. Kamera pemutaran ulang akan memiliki konfigurasi deteksi yang sama dengan kamera sumber. Pilih rentang waktu untuk memulai.", + "camera": "Kamera Sumber", + "timeRange": "Rentang Waktu", + "preset": { + "1m": "1 Menit Terakhir", + "5m": "5 Menit Terakhir", + "timeline": "Dari Linimasa", + "custom": "Kustom" + }, + "startButton": "Mulai Pemutaran Ulang", + "selectFromTimeline": "Pilih", + "starting": "Memulai pemutaran ulang...", + "startLabel": "Mulai", + "endLabel": "Selesai", + "toast": { + "error": "Gagal memulai pemutaran ulang debug: {{error}}", + "alreadyActive": "Sesi pemutaran ulang sudah aktif", + "stopError": "Gagal menghentikan pemutaran ulang debug: {{error}}", + "goToReplay": "Buka Pemutaran Ulang" + } + }, + "page": { + "noSession": "Tidak Ada Sesi Pemutaran Ulang Debug Aktif", + "noSessionDesc": "Mulai Pemutaran Ulang Debug dari tampilan Riwayat dengan mengeklik tombol Aksi di bilah alat dan memilih Pemutaran Ulang Debug.", + "goToRecordings": "Buka Riwayat", + "preparingClip": "Menyiapkan klip…", + "preparingClipDesc": "Frigate sedang menggabungkan rekaman untuk rentang waktu yang dipilih. Ini dapat memakan waktu satu menit untuk rentang yang lebih panjang.", + "startingCamera": "Memulai Pemutaran Ulang Debug…", + "startError": { + "title": "Gagal memulai Pemutaran Ulang Debug", + "back": "Kembali ke Riwayat" + }, + "sourceCamera": "Kamera Sumber", + "replayCamera": "Kamera Pemutaran Ulang", + "initializingReplay": "Menginisialisasi Pemutaran Ulang Debug...", + "stoppingReplay": "Menghentikan Pemutaran Ulang Debug...", + "stopReplay": "Hentikan Pemutaran Ulang", + "confirmStop": { + "title": "Hentikan Pemutaran Ulang Debug?", + "description": "Ini akan menghentikan sesi dan membersihkan semua data sementara. Anda yakin?", + "confirm": "Hentikan Pemutaran Ulang", + "cancel": "Batal" + }, + "activity": "Aktivitas", + "objects": "Daftar Objek", + "audioDetections": "Deteksi Audio", + "noActivity": "Tidak ada aktivitas terdeteksi", + "activeTracking": "Pelacakan aktif", + "noActiveTracking": "Tidak ada pelacakan aktif", + "configuration": "Konfigurasi", + "configurationDesc": "Sesuaikan secara halus pengaturan deteksi gerakan dan pelacakan objek untuk kamera Pemutaran Ulang Debug. Tidak ada perubahan yang disimpan ke file konfigurasi Frigate Anda." + } +} diff --git a/web/public/locales/id/views/search.json b/web/public/locales/id/views/search.json new file mode 100644 index 0000000..9a4aa09 --- /dev/null +++ b/web/public/locales/id/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Pencarian", + "savedSearches": "Pencarian Tersimpan", + "searchFor": "Cari untuk {{inputValue}}", + "button": { + "clear": "Hapus pencarian", + "save": "Simpan pencarian", + "delete": "Hapus pencarian yang disimpan", + "filterInformation": "Informasi filter", + "filterActive": "Filter aktif" + }, + "trackedObjectId": "ID Objek yang Dilacak", + "filter": { + "label": { + "cameras": "Kamera", + "labels": "Label", + "zones": "Zona", + "sub_labels": "Sub Label", + "attributes": "Atribut", + "search_type": "Jenis Pencarian", + "time_range": "Rentang Waktu", + "before": "Sebelum", + "after": "Sesudah", + "min_score": "Skor Minimum", + "max_score": "Skor Maksimum", + "min_speed": "Kecepatan Minimum", + "max_speed": "Kecepatan Maksimum", + "recognized_license_plate": "Pelat Nomor yang Diakui", + "has_clip": "Memiliki Video Klip", + "has_snapshot": "Memiliki Snapshot" + }, + "searchType": { + "thumbnail": "Gambar Mini", + "description": "Deskripsi" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Tanggal 'before' harus lebih akhir daripada tanggal 'after'.", + "afterDatebeEarlierBefore": "Tanggal 'after' harus lebih awal daripada tanggal 'before'.", + "minScoreMustBeLessOrEqualMaxScore": "'min_score' harus lebih kecil dari atau sama dengan 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'max_score' harus lebih besar dari atau sama dengan 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'min_speed' harus lebih kecil dari atau sama dengan 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'max_speed' harus lebih besar dari atau sama dengan 'min_speed'." + } + }, + "tips": { + "title": "Cara menggunakan filter teks", + "desc": { + "text": "Filter membantu Anda mempersempit hasil pencarian. Berikut cara menggunakannya di kolom input:", + "step1": "Ketik nama kunci filter diikuti tanda titik dua (misalnya, \"cameras:\").", + "step2": "Pilih nilai dari saran atau ketik nilai Anda sendiri.", + "step3": "Gunakan beberapa filter dengan menambahkannya satu per satu dengan spasi di antaranya.", + "step4": "Filter tanggal (before: dan after:) menggunakan format {{DateFormat}}.", + "step5": "Filter rentang waktu menggunakan format {{exampleTime}}.", + "step6": "Hapus filter dengan mengklik tanda 'x' di sebelahnya.", + "exampleLabel": "Contoh:" + } + }, + "header": { + "currentFilterType": "Nilai Filter", + "noFilters": "Filter", + "activeFilters": "Filter Aktif" + } + }, + "similaritySearch": { + "title": "Pencarian Kemiripan", + "active": "Pencarian kemiripan aktif", + "clear": "Hapus pencarian kemiripan" + }, + "placeholder": { + "search": "Pencarian…" + } +} diff --git a/web/public/locales/id/views/settings.json b/web/public/locales/id/views/settings.json new file mode 100644 index 0000000..d738cdd --- /dev/null +++ b/web/public/locales/id/views/settings.json @@ -0,0 +1,1953 @@ +{ + "documentTitle": { + "default": "Pengaturan - Frigate", + "camera": "Pengaturan Kamera - Frigate", + "classification": "Pengaturan Klasifikasi - Frigate", + "authentication": "Pengaturan Autentikasi - Frigate", + "masksAndZones": "Editor Masker dan Zona - Frigate", + "motionTuner": "Penyetel Gerakan - Frigate", + "general": "Pengaturan UI - Frigate", + "object": "Debug - Frigate", + "enrichments": "Pengaturan Pengayaan - Frigate", + "cameraManagement": "Kelola Kamera - Frigate", + "cameraReview": "Pengaturan Tinjauan Kamera - Frigate", + "frigatePlus": "Pengaturan Frigate+ - Frigate", + "notifications": "Pengaturan Notifikasi - Frigate", + "globalConfig": "Konfigurasi Global - Frigate", + "cameraConfig": "Konfigurasi Kamera - Frigate", + "detectorsAndModel": "Detektor dan model - Frigate", + "maintenance": "Pemeliharaan - Frigate", + "profiles": "Profil - Frigate" + }, + "menu": { + "cameraManagement": "Manajemen kamera", + "notifications": "Notifikasi", + "ui": "UI", + "enrichments": "Pengayaan", + "cameraReview": "Tinjauan", + "motionTuner": "Penyetel gerakan", + "triggers": "Pemicu", + "users": "Pengguna", + "roles": "Peran", + "frigateplus": "Frigate+", + "masksAndZones": "Masker / Zona", + "debug": "Debug", + "general": "Umum", + "globalConfig": "Konfigurasi global", + "system": "Sistem", + "integrations": "Integrasi", + "cameras": "Konfigurasi kamera", + "uiSettings": "Pengaturan UI", + "profiles": "Profil", + "globalDetect": "Deteksi objek", + "globalRecording": "Perekaman", + "globalSnapshots": "Cuplikan", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Deteksi gerakan", + "globalObjects": "Objek", + "globalReview": "Tinjauan", + "globalAudioEvents": "Deteksi audio", + "globalLivePlayback": "Pemutaran langsung", + "globalTimestampStyle": "Gaya stempel waktu", + "systemDatabase": "Basis data", + "systemTls": "TLS", + "systemAuthentication": "Autentikasi", + "systemNetworking": "Jaringan", + "systemProxy": "Proksi", + "systemUi": "UI", + "systemLogging": "Pencatatan log", + "systemEnvironmentVariables": "Variabel lingkungan", + "systemTelemetry": "Telemetri", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "Detektor dan model", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "Stream go2rtc", + "integrationSemanticSearch": "Pencarian semantik", + "integrationGenerativeAi": "AI generatif", + "integrationFaceRecognition": "Pengenalan wajah", + "integrationLpr": "Pengenalan pelat nomor", + "integrationObjectClassification": "Klasifikasi objek", + "integrationAudioTranscription": "Transkripsi audio", + "cameraDetect": "Deteksi objek", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "Perekaman", + "cameraSnapshots": "Cuplikan", + "cameraMotion": "Deteksi gerakan", + "cameraObjects": "Objek", + "cameraConfigReview": "Tinjauan", + "cameraAudioEvents": "Deteksi audio", + "cameraAudioTranscription": "Transkripsi audio", + "cameraNotifications": "Notifikasi", + "cameraLivePlayback": "Pemutaran langsung", + "cameraBirdseye": "Birdseye", + "cameraFaceRecognition": "Pengenalan wajah", + "cameraLpr": "Pengenalan pelat nomor", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "UI Kamera", + "cameraTimestampStyle": "Gaya stempel waktu", + "cameraMqtt": "MQTT Kamera", + "maintenance": "Pemeliharaan", + "mediaSync": "Sinkronisasi media", + "regionGrid": "Grid wilayah" + }, + "dialog": { + "unsavedChanges": { + "title": "Anda memiliki perubahan yang belum disimpan.", + "desc": "Apakah Anda ingin menyimpan perubahan sebelum melanjutkan?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Tidak ada kamera" + }, + "general": { + "title": "Pengaturan UI", + "liveDashboard": { + "title": "Dasbor Langsung", + "automaticLiveView": { + "label": "Tampilan Langsung Otomatis", + "desc": "Secara otomatis beralih ke tampilan langsung kamera saat aktivitas terdeteksi. Menonaktifkan opsi ini menyebabkan gambar kamera statis pada dasbor Langsung hanya diperbarui sekali per menit." + }, + "playAlertVideos": { + "label": "Putar Video Peringatan", + "desc": "Secara default, peringatan terbaru di dasbor Langsung diputar sebagai video kecil yang berulang. Nonaktifkan opsi ini untuk hanya menampilkan gambar statis dari peringatan terbaru pada perangkat/browser ini." + }, + "displayCameraNames": { + "label": "Selalu Tampilkan Nama Kamera", + "desc": "Selalu tampilkan nama kamera dalam chip pada dasbor tampilan langsung multi-kamera." + }, + "liveFallbackTimeout": { + "label": "Batas Waktu Fallback Pemutar Langsung", + "desc": "Saat stream langsung kualitas tinggi kamera tidak tersedia, alihkan ke mode bandwidth rendah setelah sekian detik. Default: 3." + } + }, + "storedLayouts": { + "title": "Tata Letak Tersimpan", + "desc": "Tata letak kamera dalam grup kamera dapat diseret/diubah ukurannya. Posisi disimpan di penyimpanan lokal browser Anda.", + "clearAll": "Hapus Semua Tata Letak" + }, + "cameraGroupStreaming": { + "title": "Pengaturan Streaming Grup Kamera", + "desc": "Pengaturan streaming untuk setiap grup kamera disimpan di penyimpanan lokal browser Anda.", + "clearAll": "Hapus Semua Pengaturan Streaming" + }, + "recordingsViewer": { + "title": "Penampil Rekaman", + "defaultPlaybackRate": { + "label": "Kecepatan Pemutaran Default", + "desc": "Kecepatan pemutaran default untuk pemutaran rekaman." + } + }, + "calendar": { + "title": "Kalender", + "firstWeekday": { + "label": "Hari Pertama dalam Minggu", + "desc": "Hari saat minggu pada kalender tinjauan dimulai.", + "sunday": "Minggu", + "monday": "Senin" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Tata letak tersimpan untuk {{cameraName}} telah dihapus", + "clearStreamingSettings": "Pengaturan streaming untuk semua grup kamera telah dihapus." + }, + "error": { + "clearStoredLayoutFailed": "Gagal menghapus tata letak tersimpan: {{errorMessage}}", + "clearStreamingSettingsFailed": "Gagal menghapus pengaturan streaming: {{errorMessage}}" + } + } + }, + "configMessages": { + "audioTranscription": { + "audioDetectionDisabled": "Deteksi audio tidak diaktifkan untuk kamera ini. Transkripsi audio memerlukan deteksi audio yang aktif." + }, + "detect": { + "fpsGreaterThanFive": "Mengatur FPS deteksi lebih dari 5 tidak direkomendasikan. Nilai yang lebih tinggi dapat menyebabkan masalah performa dan tidak akan memberikan manfaat apa pun.", + "disabled": "Deteksi objek dinonaktifkan. Cuplikan, item tinjauan, dan pengayaan seperti pengenalan wajah, pengenalan pelat nomor, dan AI Generatif tidak akan berfungsi.", + "resolutionShouldBeMultipleOfFour": "Untuk hasil terbaik, lebar dan tinggi deteksi sebaiknya merupakan kelipatan 4. Nilai genap lainnya dapat menyebabkan artefak visual atau distorsi ringan pada stream deteksi.", + "aspectRatioMismatch": "Lebar dan tinggi yang Anda masukkan tidak cocok dengan rasio aspek resolusi deteksi saat ini. Ini dapat menghasilkan gambar yang meregang atau terdistorsi.", + "maxFramesSet": "Menetapkan frame maksimum akan menimpa perilaku default dan menonaktifkan pelacakan objek diam. Hanya sedikit situasi yang memerlukan ini, gunakan dengan hati-hati.", + "squareResolution": "Resolusi deteksi berbentuk persegi tidak lazim. Lebar dan tinggi deteksi harus sesuai dengan rasio aspek kamera Anda, misalnya 16:9, bukan dimensi model deteksi objek. Rasio aspek yang tidak sesuai dapat meregangkan gambar dan mengurangi akurasi deteksi.", + "resolutionHigh": "Resolusi deteksi ini lebih tinggi daripada yang direkomendasikan dan dapat meningkatkan penggunaan sumber daya tanpa meningkatkan akurasi deteksi. Resolusi deteksi 1080p atau lebih rendah direkomendasikan untuk sebagian besar kamera.", + "globalResolutionMultipleCameras": "Resolusi deteksi global ditetapkan saat beberapa kamera dikonfigurasi. Kecuali semua kamera memiliki resolusi dan rasio aspek yang sama, lebar dan tinggi deteksi sebaiknya ditentukan per kamera agar sesuai dengan rasio aspek asli masing-masing kamera." + }, + "faceRecognition": { + "globalDisabled": "Pengayaan pengenalan wajah harus diaktifkan agar fitur pengenalan wajah berfungsi pada kamera ini.", + "personNotTracked": "Pengenalan wajah memerlukan objek 'person' untuk dilacak. Aktifkan 'person' di Objek untuk kamera ini.", + "modelSizeLarge": "Model 'large' memerlukan GPU atau NPU untuk performa yang wajar. Gunakan 'small' pada sistem yang hanya menggunakan CPU." + }, + "lpr": { + "globalDisabled": "Pengayaan pengenalan pelat nomor harus diaktifkan agar fitur LPR berfungsi pada kamera ini.", + "vehicleNotTracked": "Pengenalan pelat nomor memerlukan 'car' atau 'motorcycle' untuk dilacak. Aktifkan 'car' atau 'motorcycle' di Objek untuk kamera ini.", + "modelSizeLarge": "Model 'large' dioptimalkan untuk pelat nomor multi-baris. Model 'small' memberikan performa lebih baik daripada 'large' dan sebaiknya digunakan kecuali wilayah Anda menggunakan format pelat multi-baris." + }, + "review": { + "recordDisabled": "Perekaman dinonaktifkan, item tinjauan tidak akan dibuat.", + "detectDisabled": "Deteksi objek dinonaktifkan. Item tinjauan memerlukan objek yang terdeteksi untuk mengategorikan alert dan deteksi.", + "allNonAlertDetections": "Semua aktivitas non-alert akan disertakan sebagai deteksi.", + "genaiImageSourceRecordingsRecordDisabled": "Sumber gambar disetel ke 'rekaman', tetapi perekaman dinonaktifkan. Frigate akan kembali ke gambar pratinjau." + }, + "audio": { + "noAudioRole": "Tidak ada stream yang memiliki peran audio yang didefinisikan. Anda harus mengaktifkan peran audio agar deteksi audio berfungsi." + }, + "objects": { + "genaiNoDescriptionsProvider": "Anda harus mengonfigurasi penyedia GenAI dengan peran 'deskripsi' agar deskripsi dapat dibuat." + }, + "record": { + "noRecordRole": "Tidak ada stream yang memiliki peran record yang didefinisikan. Perekaman tidak akan berfungsi." + }, + "birdseye": { + "objectsModeDetectDisabled": "Birdseye disetel ke mode 'objects', tetapi deteksi objek dinonaktifkan untuk kamera ini. Kamera tidak akan muncul di Birdseye." + }, + "snapshots": { + "detectDisabled": "Deteksi objek dinonaktifkan. Cuplikan dihasilkan dari objek yang terlacak dan tidak akan dibuat." + }, + "detectors": { + "mixedTypes": "Semua detektor harus menggunakan tipe yang sama. Hapus detektor yang ada untuk menggunakan tipe yang berbeda.", + "mixedTypesSuggestion": "Semua detektor harus menggunakan tipe yang sama. Hapus detektor yang ada atau pilih {{type}}." + }, + "semanticSearch": { + "jinav2SmallModelSize": "Ukuran 'small' dengan model Jina V2 memiliki RAM tinggi dan biaya inferensi. Model 'large' dengan GPU diskrit direkomendasikan." + }, + "onvif": { + "autotrackingNoZones": "Pelacakan otomatis memerlukan setidaknya satu zona. Tentukan zona untuk kamera ini di Masker / Zona, lalu tetapkan sebagai zona wajib di bawah." + } + }, + "button": { + "overriddenGlobal": "Ditimpa (Global)", + "overriddenGlobalTooltip": "Kamera ini menimpa pengaturan konfigurasi global di bagian ini", + "overriddenGlobalHeading_other": "Kamera ini menimpa {{count}} bidang dari konfigurasi global:", + "overriddenGlobalNoDeltas": "Kamera ini menimpa konfigurasi global, tetapi tidak ada nilai bidang yang berbeda.", + "overriddenBaseConfig": "Ditimpa (Konfigurasi Dasar)", + "overriddenBaseConfigTooltip": "Profil {{profile}} menimpa pengaturan konfigurasi di bagian ini", + "overriddenBaseConfigHeading_other": "Profil {{profile}} menimpa {{count}} bidang dari konfigurasi dasar:", + "overriddenBaseConfigNoDeltas": "Profil {{profile}} menimpa bagian ini, tetapi tidak ada nilai bidang yang berbeda dari konfigurasi dasar.", + "overriddenInCameras": { + "label_other": "Ditimpa di {{count}} kamera", + "tooltip_other": "{{count}} kamera menimpa nilai di bagian ini. Klik untuk melihat detail.", + "heading_other": "Bagian global ini memiliki bidang yang ditimpa di {{count}} kamera.", + "othersField_other": "{{count}} lainnya", + "profilePrefix": "Profil {{profile}}: {{fields}}" + } + }, + "menuDot": { + "overrideGlobal": "Bagian ini menimpa konfigurasi global", + "overrideProfile": "Bagian ini ditimpa oleh profil {{profile}}", + "unsaved": "Bagian ini memiliki perubahan yang belum disimpan" + }, + "saveAllPreview": { + "title": "Perubahan yang akan disimpan", + "triggerLabel": "Tinjau perubahan yang tertunda", + "empty": "Tidak ada perubahan yang tertunda.", + "scope": { + "label": "Cakupan", + "global": "Global", + "camera": "Kamera: {{cameraName}}" + }, + "profile": { + "label": "Profil" + }, + "field": { + "label": "Bidang" + }, + "value": { + "label": "Nilai baru", + "reset": "Atur ulang" + } + }, + "enrichments": { + "title": "Pengaturan Pengayaan", + "unsavedChanges": "Perubahan pengaturan Pengayaan yang belum disimpan", + "birdClassification": { + "title": "Klasifikasi Burung", + "desc": "Klasifikasi burung mengidentifikasi burung yang dikenal menggunakan model Tensorflow terkuantisasi. Saat burung yang dikenal dikenali, nama umumnya akan ditambahkan sebagai sub_label. Informasi ini disertakan dalam UI, filter, serta notifikasi." + }, + "semanticSearch": { + "title": "Pencarian Semantik", + "desc": "Pencarian Semantik di Frigate memungkinkan Anda menemukan objek yang terlacak dalam item tinjauan menggunakan gambar itu sendiri, deskripsi teks yang ditentukan pengguna, atau deskripsi yang dibuat secara otomatis.", + "reindexNow": { + "label": "Indeks Ulang Sekarang", + "desc": "Pengindeksan ulang akan membuat ulang embedding untuk semua objek yang terlacak. Proses ini berjalan di latar belakang dan dapat memaksimalkan penggunaan CPU Anda serta memerlukan waktu cukup lama tergantung pada jumlah objek yang terlacak yang Anda miliki.", + "confirmTitle": "Konfirmasi Pengindeksan Ulang", + "confirmDesc": "Apakah Anda yakin ingin mengindeks ulang semua embedding objek yang terlacak? Proses ini akan berjalan di latar belakang, tetapi dapat memaksimalkan penggunaan CPU Anda dan memerlukan waktu cukup lama. Anda dapat memantau progresnya di halaman Jelajahi.", + "confirmButton": "Indeks Ulang", + "success": "Pengindeksan ulang berhasil dimulai.", + "alreadyInProgress": "Pengindeksan ulang sudah sedang berlangsung.", + "error": "Gagal memulai pengindeksan ulang: {{errorMessage}}" + }, + "modelSize": { + "label": "Ukuran Model", + "desc": "Ukuran model yang digunakan untuk embedding pencarian semantik.", + "small": { + "title": "kecil", + "desc": "Menggunakan small akan memakai versi model terkuantisasi yang menggunakan RAM lebih sedikit dan berjalan lebih cepat pada CPU dengan perbedaan kualitas embedding yang sangat kecil." + }, + "large": { + "title": "besar", + "desc": "Menggunakan large memakai model Jina penuh dan akan otomatis berjalan pada GPU jika memungkinkan." + } + } + }, + "faceRecognition": { + "title": "Pengenalan Wajah", + "desc": "Pengenalan wajah memungkinkan orang diberi nama, dan saat wajah mereka dikenali Frigate akan menetapkan nama orang tersebut sebagai sub_label. Informasi ini disertakan dalam UI, filter, serta notifikasi.", + "modelSize": { + "label": "Ukuran Model", + "desc": "Ukuran model yang digunakan untuk pengenalan wajah.", + "small": { + "title": "kecil", + "desc": "Menggunakan small memakai model embedding wajah FaceNet yang berjalan efisien pada sebagian besar CPU." + }, + "large": { + "title": "besar", + "desc": "Menggunakan large memakai model embedding wajah ArcFace dan akan otomatis berjalan pada GPU jika memungkinkan." + } + } + }, + "licensePlateRecognition": { + "title": "Pengenalan Pelat Nomor", + "desc": "Frigate dapat mengenali pelat nomor pada kendaraan dan secara otomatis menambahkan karakter yang terdeteksi ke field recognized_license_plate atau nama yang dikenal sebagai sub_label pada objek bertipe mobil. Kasus penggunaan yang umum adalah membaca pelat nomor mobil yang masuk ke jalan masuk rumah atau mobil yang melintas di jalan." + }, + "restart_required": "Perlu mulai ulang (pengaturan Pengayaan berubah)", + "toast": { + "success": "Pengaturan Pengayaan telah disimpan. Mulai ulang Frigate untuk menerapkan perubahan Anda.", + "error": "Gagal menyimpan perubahan konfigurasi: {{errorMessage}}" + } + }, + "cameraWizard": { + "title": "Tambah Kamera", + "description": "Ikuti langkah-langkah di bawah ini untuk menambahkan kamera baru ke instalasi Frigate Anda.", + "steps": { + "nameAndConnection": "Nama & Koneksi", + "probeOrSnapshot": "Probe atau Cuplikan", + "streamConfiguration": "Konfigurasi Stream", + "validationAndTesting": "Validasi & Pengujian" + }, + "save": { + "success": "Berhasil menyimpan kamera baru {{cameraName}}.", + "failure": "Kesalahan saat menyimpan {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolusi", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Harap berikan URL stream yang valid", + "testFailed": "Pengujian stream gagal: {{error}}" + }, + "step1": { + "description": "Masukkan detail kamera Anda dan pilih untuk memeriksa kamera atau memilih merek secara manual.", + "cameraName": "Nama Kamera", + "cameraNamePlaceholder": "mis., front_door atau Back Yard Overview", + "host": "Alamat Host/IP", + "port": "Port", + "username": "Nama Pengguna", + "usernamePlaceholder": "Opsional", + "password": "Kata Sandi", + "passwordPlaceholder": "Opsional", + "selectTransport": "Pilih protokol transport", + "cameraBrand": "Merek Kamera", + "selectBrand": "Pilih merek kamera untuk template URL", + "customUrl": "URL Stream Kustom", + "brandInformation": "Informasi merek", + "brandUrlFormat": "Untuk kamera dengan format URL RTSP seperti: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "connectionSettings": "Pengaturan Koneksi", + "detectionMethod": "Metode Deteksi Stream", + "onvifPort": "Port ONVIF", + "probeMode": "Periksa kamera", + "manualMode": "Pilihan manual", + "detectionMethodDescription": "Periksa kamera dengan ONVIF (jika didukung) untuk menemukan URL stream kamera, atau pilih merek kamera secara manual untuk menggunakan URL yang telah ditentukan sebelumnya. Untuk memasukkan URL RTSP kustom, pilih metode manual dan pilih \"Lainnya\".", + "onvifPortDescription": "Untuk kamera yang mendukung ONVIF, biasanya ini adalah 80 atau 8080.", + "useDigestAuth": "Gunakan autentikasi digest", + "useDigestAuthDescription": "Gunakan autentikasi digest HTTP untuk ONVIF. Beberapa kamera mungkin memerlukan nama pengguna/kata sandi ONVIF khusus alih-alih pengguna admin standar.", + "errors": { + "brandOrCustomUrlRequired": "Pilih merek kamera dengan host/IP atau pilih 'Lainnya' dengan URL kustom", + "nameRequired": "Nama kamera wajib diisi", + "nameLength": "Nama kamera harus 64 karakter atau kurang", + "invalidCharacters": "Nama kamera mengandung karakter yang tidak valid", + "nameExists": "Nama kamera sudah ada", + "customUrlRtspRequired": "URL kustom harus diawali dengan \"rtsp://\" atau \"rtsps://\". Konfigurasi manual diperlukan untuk stream kamera non-RTSP." + } + }, + "step2": { + "description": "Periksa kamera untuk stream yang tersedia atau konfigurasi pengaturan manual berdasarkan metode deteksi yang Anda pilih.", + "testSuccess": "Pengujian koneksi berhasil!", + "testFailed": "Pengujian koneksi gagal. Harap periksa input Anda dan coba lagi.", + "testFailedTitle": "Pengujian Gagal", + "streamDetails": "Detail Stream", + "probing": "Sedang memeriksa kamera...", + "retry": "Coba lagi", + "testing": { + "probingMetadata": "Sedang memeriksa metadata kamera...", + "fetchingSnapshot": "Sedang mengambil cuplikan kamera..." + }, + "probeFailed": "Gagal memeriksa kamera: {{error}}", + "probingDevice": "Sedang memeriksa perangkat...", + "probeSuccessful": "Pemeriksaan berhasil", + "probeError": "Kesalahan Pemeriksaan", + "probeNoSuccess": "Pemeriksaan tidak berhasil", + "deviceInfo": "Informasi Perangkat", + "manufacturer": "Produsen", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profil", + "ptzSupport": "Dukungan PTZ", + "autotrackingSupport": "Dukungan Pelacakan Otomatis", + "presets": "Preset", + "rtspCandidates": "Kandidat RTSP", + "rtspCandidatesDescription": "URL RTSP berikut ditemukan dari pemeriksaan kamera. Uji koneksi untuk melihat metadata stream.", + "noRtspCandidates": "Tidak ada URL RTSP yang ditemukan dari kamera. Kredensial Anda mungkin salah, atau kamera mungkin tidak mendukung ONVIF atau metode yang digunakan untuk mengambil URL RTSP. Kembali dan masukkan URL RTSP secara manual.", + "candidateStreamTitle": "Kandidat {{number}}", + "useCandidate": "Gunakan", + "uriCopy": "Salin", + "uriCopied": "URI disalin ke clipboard", + "testConnection": "Uji Koneksi", + "toggleUriView": "Klik untuk mengalihkan tampilan URI penuh", + "connected": "Terhubung", + "notConnected": "Tidak Terhubung", + "errors": { + "hostRequired": "Alamat host/IP wajib diisi" + } + }, + "step3": { + "description": "Konfigurasikan peran stream dan tambahkan stream tambahan untuk kamera Anda.", + "streamsTitle": "Stream Kamera", + "addStream": "Tambah Stream", + "addAnotherStream": "Tambah Stream Lain", + "streamTitle": "Stream {{number}}", + "streamUrl": "URL Stream", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "selectStream": "Pilih sebuah stream", + "searchCandidates": "Cari kandidat...", + "noStreamFound": "Stream tidak ditemukan", + "url": "URL", + "resolution": "Resolusi", + "selectResolution": "Pilih resolusi", + "quality": "Kualitas", + "selectQuality": "Pilih kualitas", + "roles": "Peran", + "roleLabels": { + "detect": "Deteksi Objek", + "record": "Perekaman", + "audio": "Audio" + }, + "testStream": "Uji Koneksi", + "testSuccess": "Pengujian stream berhasil!", + "testFailed": "Pengujian stream gagal", + "testFailedTitle": "Pengujian Gagal", + "connected": "Terhubung", + "notConnected": "Tidak Terhubung", + "featuresTitle": "Fitur", + "go2rtc": "Kurangi koneksi ke kamera", + "detectRoleWarning": "Setidaknya satu stream harus memiliki peran \"detect\" untuk melanjutkan.", + "rolesPopover": { + "title": "Peran Stream", + "detect": "Umpan utama untuk deteksi objek.", + "record": "Menyimpan segmen umpan video berdasarkan pengaturan konfigurasi.", + "audio": "Umpan untuk deteksi berbasis audio." + }, + "featuresPopover": { + "title": "Fitur Stream", + "description": "Gunakan restreaming go2rtc untuk mengurangi koneksi ke kamera Anda." + } + }, + "step4": { + "description": "Validasi dan analisis akhir sebelum menyimpan kamera baru Anda. Hubungkan setiap stream sebelum menyimpan.", + "validationTitle": "Validasi Stream", + "connectAllStreams": "Hubungkan Semua Stream", + "reconnectionSuccess": "Penyambungan ulang berhasil.", + "reconnectionPartial": "Beberapa stream gagal disambungkan ulang.", + "streamUnavailable": "Pratinjau stream tidak tersedia", + "reload": "Muat ulang", + "connecting": "Menghubungkan...", + "streamTitle": "Stream {{number}}", + "valid": "Valid", + "failed": "Gagal", + "notTested": "Belum diuji", + "connectStream": "Hubungkan", + "connectingStream": "Sedang menghubungkan", + "disconnectStream": "Putuskan", + "estimatedBandwidth": "Perkiraan Bandwidth", + "roles": "Peran", + "ffmpegModule": "Gunakan mode kompatibilitas stream", + "ffmpegModuleDescription": "Jika stream tidak dimuat setelah beberapa kali percobaan, coba aktifkan ini. Saat diaktifkan, Frigate akan menggunakan modul ffmpeg dengan go2rtc. Ini dapat memberikan kompatibilitas yang lebih baik dengan beberapa stream kamera.", + "none": "Tidak ada", + "error": "Kesalahan", + "streamValidated": "Stream {{number}} berhasil divalidasi", + "streamValidationFailed": "Validasi stream {{number}} gagal", + "saveAndApply": "Simpan Kamera Baru", + "saveError": "Konfigurasi tidak valid. Harap periksa pengaturan Anda.", + "issues": { + "title": "Validasi Stream", + "videoCodecGood": "Codec video adalah {{codec}}.", + "audioCodecGood": "Codec audio adalah {{codec}}.", + "resolutionHigh": "Resolusi {{resolution}} dapat menyebabkan peningkatan penggunaan sumber daya.", + "resolutionLow": "Resolusi {{resolution}} mungkin terlalu rendah untuk deteksi objek kecil yang andal.", + "resolutionUnknown": "Resolusi stream ini tidak dapat diperiksa. Anda harus menetapkan resolusi detect secara manual di Pengaturan atau konfigurasi Anda.", + "noAudioWarning": "Tidak ada audio yang terdeteksi untuk stream ini, rekaman tidak akan memiliki audio.", + "audioCodecRecordError": "Codec audio AAC diperlukan untuk mendukung audio dalam rekaman.", + "audioCodecRequired": "Stream audio diperlukan untuk mendukung deteksi audio.", + "restreamingWarning": "Mengurangi koneksi ke kamera untuk stream record dapat sedikit meningkatkan penggunaan CPU.", + "brands": { + "reolink-rtsp": "RTSP Reolink tidak direkomendasikan. Aktifkan HTTP di pengaturan firmware kamera dan mulai ulang wizard.", + "reolink-http": "Stream HTTP Reolink sebaiknya menggunakan FFmpeg untuk kompatibilitas yang lebih baik. Aktifkan 'Gunakan mode kompatibilitas stream' untuk stream ini." + }, + "dahua": { + "substreamWarning": "Substream 1 terkunci pada resolusi rendah. Banyak kamera Dahua / Amcrest / EmpireTech mendukung substream tambahan yang perlu diaktifkan di pengaturan kamera. Disarankan untuk memeriksa dan menggunakan stream tersebut jika tersedia." + }, + "hikvision": { + "substreamWarning": "Substream 1 terkunci pada resolusi rendah. Banyak kamera Hikvision mendukung substream tambahan yang perlu diaktifkan di pengaturan kamera. Disarankan untuk memeriksa dan menggunakan stream tersebut jika tersedia." + } + } + } + }, + "cameraManagement": { + "title": "Kelola Kamera", + "description": "Tambahkan, edit, dan hapus kamera, kendalikan status setiap kamera, serta atur penimpaan per profil dan jenis kamera. Untuk mengonfigurasi stream, deteksi, gerakan, dan pengaturan khusus kamera lainnya, pilih bagian terkait di bawah Konfigurasi Kamera.", + "addCamera": "Tambah Kamera Baru", + "deleteCamera": "Hapus Kamera", + "deleteCameraDialog": { + "title": "Hapus Kamera", + "description": "Menghapus kamera akan menghapus secara permanen semua rekaman, objek terlacak, dan konfigurasi untuk kamera tersebut. Semua stream go2rtc yang terkait dengan kamera ini mungkin masih perlu dihapus secara manual.", + "selectPlaceholder": "Pilih kamera...", + "confirmTitle": "Apakah Anda yakin?", + "confirmWarning": "Menghapus {{cameraName}} tidak dapat dibatalkan.", + "deleteExports": "Juga hapus ekspor untuk kamera ini", + "confirmButton": "Hapus Permanen", + "success": "Kamera {{cameraName}} berhasil dihapus", + "error": "Gagal menghapus kamera {{cameraName}}" + }, + "editCamera": "Edit Kamera:", + "selectCamera": "Pilih Kamera", + "backToSettings": "Kembali ke Pengaturan Kamera", + "streams": { + "title": "Status dan Detail Kamera", + "enableLabel": "Kamera yang diaktifkan", + "enableDesc": "Nonaktifkan sementara kamera yang aktif hingga Frigate dimulai ulang. Menonaktifkan kamera sepenuhnya akan menghentikan seluruh pemrosesan stream kamera ini oleh Frigate. Deteksi, perekaman, dan debugging tidak akan tersedia.
    Catatan: Ini tidak menonaktifkan restream go2rtc.

    Seret pegangan untuk mengubah urutan kamera sebagaimana ditampilkan di UI. Urutan kamera yang aktif akan tercermin di seluruh UI termasuk dasbor Langsung dan dropdown pemilihan kamera.", + "disableLabel": "Kamera yang dinonaktifkan", + "disableDesc": "Aktifkan kamera yang saat ini tidak terlihat di UI dan dinonaktifkan dalam konfigurasi. Mulai ulang Frigate diperlukan setelah mengaktifkan.", + "enableSuccess": "{{cameraName}} diaktifkan. Restart Frigate untuk menerapkan.", + "reorderHandle": "Seret untuk mengubah urutan", + "saving": "Menyimpan…", + "saved": "Tersimpan", + "friendlyName": { + "edit": "Edit nama tampilan kamera", + "title": "Edit Nama Tampilan", + "description": "Tetapkan nama ramah yang ditampilkan untuk kamera ini di seluruh UI Frigate. Biarkan kosong untuk menggunakan ID kamera.", + "rename": "Ubah Nama" + }, + "label": "Status kamera", + "description": "Atur status operasi untuk setiap kamera.

    Nyala: stream diproses secara normal.
    Mati: mem jeda pemrosesan untuk sementara. Tidak tetap setelah Frigate dimulai ulang.
    Nonaktif: menghentikan pemrosesan dan menyimpan perubahan ke konfigurasi Anda. Restart diperlukan untuk mengaktifkan kembali kamera yang dinonaktifkan.

    Catatan: Menonaktifkan tidak memengaruhi restream go2rtc.

    Seret pegangan untuk mengubah urutan kamera aktif sebagaimana tampil di seluruh UI, termasuk dasbor Live dan menu dropdown pemilihan kamera.", + "disabledSubheading": "Dinonaktifkan dalam konfigurasi", + "status": { + "on": "Nyala", + "off": "Mati", + "disabled": "Nonaktif" + }, + "disableSuccess": "{{cameraName}} dinonaktifkan dan disimpan ke konfigurasi.", + "details": { + "edit": "Edit detail kamera", + "title": "Edit Detail Kamera", + "description": "Perbarui nama tampilan, URL eksternal, dan visibilitas yang digunakan untuk kamera ini di seluruh UI Frigate.", + "friendlyNameLabel": "Nama Tampilan", + "friendlyNameHelp": "Nama ramah yang ditampilkan untuk kamera ini di seluruh UI Frigate. Biarkan kosong untuk menggunakan ID kamera.", + "webuiUrlLabel": "URL Web UI Kamera", + "webuiUrlHelp": "URL untuk membuka web UI kamera langsung dari tampilan Debug. Biarkan kosong untuk menonaktifkan tautan.", + "webuiUrlInvalid": "Harus berupa URL yang valid (misalnya, https://example.com).", + "dashboardLabel": "Tampilkan di dasbor Live", + "dashboardHelp": "Tampilkan kamera ini di dasbor Live.", + "reviewLabel": "Tampilkan di Review", + "reviewHelp": "Tampilkan kamera ini di Review, termasuk filter kamera, review gerakan, dan tampilan riwayat." + } + }, + "cameraConfig": { + "add": "Tambah Kamera", + "edit": "Edit Kamera", + "description": "Konfigurasikan pengaturan kamera termasuk input stream dan perannya.", + "name": "Nama Kamera", + "nameRequired": "Nama kamera wajib diisi", + "nameLength": "Nama kamera harus kurang dari 64 karakter.", + "namePlaceholder": "mis., front_door atau Back Yard Overview", + "enabled": "Diaktifkan", + "ffmpeg": { + "inputs": "Input Stream", + "path": "Path Stream", + "pathRequired": "Path stream wajib diisi", + "pathPlaceholder": "rtsp://...", + "roles": "Peran", + "rolesRequired": "Setidaknya satu peran wajib diisi", + "rolesUnique": "Setiap peran (audio, detect, record) hanya dapat ditetapkan ke satu stream", + "addInput": "Tambah Input Stream", + "removeInput": "Hapus Input Stream", + "inputsRequired": "Setidaknya satu input stream wajib diisi" + }, + "go2rtcStreams": "Stream go2rtc", + "streamUrls": "URL Stream", + "addUrl": "Tambah URL", + "addGo2rtcStream": "Tambah Stream go2rtc", + "toast": { + "success": "Kamera {{cameraName}} berhasil disimpan" + } + }, + "profiles": { + "title": "Penimpaan Kamera per Profil", + "selectLabel": "Pilih profil", + "description": "Atur kamera mana yang dinyalakan atau dimatikan saat suatu profil diaktifkan. Kamera yang disetel ke \"Warisi\" akan mempertahankan status default-nya.", + "inherit": "Warisi", + "enabled": "Diaktifkan", + "disabled": "Dinonaktifkan", + "on": "Nyala", + "off": "Mati" + }, + "cameraType": { + "title": "Tipe Kamera", + "label": "Tipe kamera", + "description": "Tetapkan tipe untuk setiap kamera. Kamera LPR khusus adalah kamera satu fungsi dengan zoom optik kuat untuk menangkap pelat nomor pada kendaraan yang jauh. Sebagian besar kamera sebaiknya menggunakan tipe kamera normal kecuali kamera tersebut memang khusus untuk LPR dan memiliki tampilan yang sangat terfokus pada pelat nomor.", + "normal": "Normal", + "dedicatedLpr": "LPR Khusus", + "saveSuccess": "Tipe kamera untuk {{cameraName}} telah diperbarui. Mulai ulang Frigate untuk menerapkan perubahan." + }, + "clone": { + "sectionTitle": "Kloning pengaturan", + "sectionDescription": "Salin konfigurasi dari satu kamera ke kamera lain atau ke kamera baru.", + "button": "Kloning pengaturan", + "title": "Kloning pengaturan kamera", + "description": "Salin konfigurasi sebuah kamera ke satu atau lebih kamera lain atau ke kamera baru. Identitas (nama, nama ramah, URL web UI, urutan tampilan) tidak pernah disalin.", + "source": { + "label": "Kamera sumber", + "placeholder": "Pilih kamera sumber", + "required": "Pilih kamera sumber" + }, + "target": { + "legend": "Target", + "newRadio": "Kamera baru", + "newNameLabel": "Nama kamera", + "newNamePlaceholder": "misalnya, pintu_belakang atau Pintu Belakang", + "newNameRequired": "Nama kamera wajib diisi", + "newNameInvalid": "Nama kamera tidak valid", + "newNameCollision": "Kamera dengan nama ini sudah ada", + "newStreamsForced": "Stream selalu disalin untuk kamera baru.", + "existingCamerasRadio": "Kamera yang sudah ada", + "allCameras": "Semua kamera", + "existingPlaceholder": "Pilih setidaknya satu kamera", + "existingDisabled": "Tidak ada kamera lain untuk disalin ke" + }, + "categories": { + "legend": "Pengaturan untuk dikloning", + "description": "Pilih pengaturan mana yang akan disalin dari kamera sumber.", + "selectAll": "Pilih semua", + "selectNone": "Jangan pilih apa pun", + "resetDefaults": "Reset ke default", + "general": "Umum", + "spatial": "Pengaturan spasial", + "streams": "Stream", + "spatialWarningTitle": "Ketidakcocokan resolusi", + "spatialWarning": "Resolusi deteksi kamera sumber {{srcCamera}} ({{srcWidth}}×{{srcHeight}}) berbeda dengan: {{cameras}}. Poligon mungkin tidak sejajar pada kamera-kamera tersebut. Default untuk ini adalah nonaktif; aktifkan untuk menyalin apa adanya.", + "restartHint": "Perlu restart", + "items": { + "record": "Perekaman", + "snapshots": "Snapshot", + "review": "Review", + "motion": "Deteksi gerakan", + "objects": "Objek", + "audio": "Deteksi audio", + "audio_transcription": "Transkripsi audio", + "notifications": "Notifikasi", + "birdseye": "Birdseye", + "mqtt": "MQTT", + "timestamp_style": "Gaya stempel waktu", + "onvif": "ONVIF", + "lpr": "Pengenalan pelat nomor", + "face_recognition": "Pengenalan wajah", + "semantic_search": "Pencarian semantik", + "genai": "AI generatif", + "type": "Jenis kamera (normal / LPR khusus)", + "profiles": "Profil", + "detect": "Dimensi deteksi", + "zones": "Zona", + "motion_mask": "Masker gerakan", + "object_masks": "Masker objek", + "ffmpeg_live": "URL dan peran stream" + } + }, + "footer": { + "changeCount_other": "{{count}} perubahan akan diterapkan", + "restartNeeded": "Restart akan diperlukan untuk beberapa perubahan.", + "liveOnly": "Semua perubahan akan diterapkan langsung tanpa restart.", + "submit": "Kloning", + "submitting": "Mengkloning…" + }, + "toast": { + "success": "Pengaturan disalin ke {{cameraName}}", + "successWithRestart": "Pengaturan disalin ke {{cameraName}}. Restart Frigate untuk menerapkan semua perubahan.", + "successMulti_other": "Pengaturan disalin ke {{count}} kamera", + "successMultiWithRestart_other": "Pengaturan disalin ke {{count}} kamera. Restart Frigate untuk menerapkan semua perubahan.", + "partialFailure": "{{successCount}} bagian diterapkan; '{{failedSection}}' gagal: {{errorMessage}}", + "partialFailureMulti": "Disalin ke {{successCount}} kamera; gagal untuk {{failed}}: {{errorMessage}}", + "newCameraPartialFailure": "Kamera {{cameraName}} berhasil dibuat, tetapi beberapa pengaturan gagal disalin: {{errorMessage}}", + "sourceMissing": "Kamera sumber sudah tidak ada", + "submitError": "Gagal mengkloning kamera: {{errorMessage}}" + } + } + }, + "cameraReview": { + "title": "Pengaturan Tinjauan Kamera", + "object_descriptions": { + "title": "Deskripsi Objek AI Generatif", + "desc": "Aktifkan/nonaktifkan sementara deskripsi objek AI Generatif untuk kamera ini hingga Frigate dimulai ulang. Saat dinonaktifkan, deskripsi yang dihasilkan AI tidak akan diminta untuk objek terlacak pada kamera ini." + }, + "review_descriptions": { + "title": "Deskripsi Tinjauan AI Generatif", + "desc": "Aktifkan/nonaktifkan sementara deskripsi tinjauan AI Generatif untuk kamera ini hingga Frigate dimulai ulang. Saat dinonaktifkan, deskripsi yang dihasilkan AI tidak akan diminta untuk item tinjauan pada kamera ini." + }, + "review": { + "title": "Tinjauan", + "desc": "Aktifkan/nonaktifkan sementara alert dan deteksi untuk kamera ini hingga Frigate dimulai ulang. Saat dinonaktifkan, tidak ada item tinjauan baru yang akan dibuat. ", + "alerts": "Alert ", + "detections": "Deteksi " + }, + "reviewClassification": { + "title": "Klasifikasi Tinjauan", + "desc": "Frigate mengategorikan item tinjauan sebagai Alert dan Deteksi. Secara default, semua objek person dan car dianggap sebagai Alert. Anda dapat menyempurnakan kategorisasi item tinjauan Anda dengan mengonfigurasi zona yang diwajibkan untuk item tersebut.", + "noDefinedZones": "Tidak ada zona yang didefinisikan untuk kamera ini.", + "objectAlertsTips": "Semua objek {{alertsLabels}} pada {{cameraName}} akan ditampilkan sebagai Alert.", + "zoneObjectAlertsTips": "Semua objek {{alertsLabels}} yang terdeteksi di {{zone}} pada {{cameraName}} akan ditampilkan sebagai Alert.", + "objectDetectionsTips": "Semua objek {{detectionsLabels}} yang tidak dikategorikan pada {{cameraName}} akan ditampilkan sebagai Deteksi, terlepas dari zona mana mereka berada.", + "zoneObjectDetectionsTips": { + "text": "Semua objek {{detectionsLabels}} yang tidak dikategorikan di {{zone}} pada {{cameraName}} akan ditampilkan sebagai Deteksi.", + "notSelectDetections": "Semua objek {{detectionsLabels}} yang terdeteksi di {{zone}} pada {{cameraName}} dan tidak dikategorikan sebagai Alert akan ditampilkan sebagai Deteksi, terlepas dari zona mana mereka berada.", + "regardlessOfZoneObjectDetectionsTips": "Semua objek {{detectionsLabels}} yang tidak dikategorikan pada {{cameraName}} akan ditampilkan sebagai Deteksi, terlepas dari zona mana mereka berada." + }, + "unsavedChanges": "Pengaturan Klasifikasi Tinjauan yang belum disimpan untuk {{camera}}", + "selectAlertsZones": "Pilih zona untuk Alert", + "selectDetectionsZones": "Pilih zona untuk Deteksi", + "limitDetections": "Batasi deteksi ke zona tertentu", + "toast": { + "success": "Konfigurasi Klasifikasi Tinjauan telah disimpan. Mulai ulang Frigate untuk menerapkan perubahan." + } + } + }, + "masksAndZones": { + "filter": { + "all": "Semua Masker dan Zona" + }, + "restart_required": "Perlu mulai ulang (masker/zona berubah)", + "disabledInConfig": "Item dinonaktifkan dalam file konfigurasi", + "addDisabledProfile": "Tambahkan ke konfigurasi dasar terlebih dahulu, lalu timpa di profil", + "profileBase": "(dasar)", + "profileOverride": "(timpa)", + "toast": { + "success": { + "copyCoordinates": "Koordinat untuk {{polyName}} telah disalin ke clipboard." + }, + "error": { + "copyCoordinatesFailed": "Tidak dapat menyalin koordinat ke clipboard." + } + }, + "motionMaskLabel": "Masker Gerakan {{number}}", + "objectMaskLabel": "Masker Objek {{number}}", + "form": { + "id": { + "error": { + "mustNotBeEmpty": "ID tidak boleh kosong.", + "alreadyExists": "Masker dengan ID ini sudah ada untuk kamera ini." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Nama tidak boleh kosong." + } + }, + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Nama zona harus minimal 2 karakter.", + "mustNotBeSameWithCamera": "Nama zona tidak boleh sama dengan nama kamera.", + "alreadyExists": "Zona dengan nama ini sudah ada untuk kamera ini.", + "mustNotContainPeriod": "Nama zona tidak boleh mengandung titik.", + "hasIllegalCharacter": "Nama zona mengandung karakter yang tidak valid.", + "mustHaveAtLeastOneLetter": "Nama zona harus memiliki setidaknya satu huruf." + } + }, + "distance": { + "error": { + "text": "Jarak harus lebih besar dari atau sama dengan 0.1.", + "mustBeFilled": "Semua field jarak harus diisi untuk menggunakan estimasi kecepatan." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Inersia harus lebih besar dari 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Waktu loitering harus lebih besar dari atau sama dengan 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Ambang kecepatan harus lebih besar dari atau sama dengan 0.1." + } + }, + "polygonDrawing": { + "type": { + "zone": "zona", + "motion_mask": "masker gerakan", + "object_mask": "masker objek" + }, + "removeLastPoint": "Hapus titik terakhir", + "reset": { + "label": "Hapus semua titik" + }, + "snapPoints": { + "true": "Kaitkan titik", + "false": "Jangan kaitkan titik" + }, + "delete": { + "title": "Konfirmasi Hapus", + "desc": "Apakah Anda yakin ingin menghapus {{type}} {{name}}?", + "success": "{{name}} telah dihapus." + }, + "revertOverride": { + "title": "Kembalikan ke Konfigurasi Dasar", + "desc": "Ini akan menghapus penimpaan profil untuk {{type}} {{name}} dan mengembalikannya ke konfigurasi dasar." + }, + "error": { + "mustBeFinished": "Gambar poligon harus diselesaikan sebelum menyimpan." + } + } + }, + "zones": { + "label": "Zona", + "documentTitle": "Edit Zona - Frigate", + "desc": { + "title": "Zona memungkinkan Anda menentukan area tertentu pada frame sehingga Anda dapat menentukan apakah suatu objek berada di dalam area tertentu atau tidak.", + "documentation": "Dokumentasi" + }, + "add": "Tambah Zona", + "edit": "Edit Zona", + "point_other": "{{count}} titik", + "clickDrawPolygon": "Klik untuk menggambar poligon pada gambar.", + "name": { + "title": "Nama", + "inputPlaceHolder": "Masukkan nama…", + "tips": "Nama harus minimal 2 karakter, harus memiliki setidaknya satu huruf, dan tidak boleh sama dengan nama kamera atau zona lain pada kamera ini." + }, + "enabled": { + "title": "Diaktifkan", + "description": "Menentukan apakah zona ini aktif dan diaktifkan dalam file konfigurasi. Jika dinonaktifkan, zona ini tidak dapat diaktifkan melalui MQTT. Zona yang dinonaktifkan diabaikan saat runtime." + }, + "inertia": { + "title": "Inersia", + "desc": "Menentukan berapa banyak frame suatu objek harus berada di dalam zona sebelum dianggap berada di zona tersebut. Default: 3" + }, + "loiteringTime": { + "title": "Waktu Loitering", + "desc": "Menetapkan jumlah waktu minimum dalam detik yang harus dilalui objek di dalam zona agar zona aktif. Default: 0" + }, + "objects": { + "title": "Objek", + "desc": "Daftar objek yang berlaku untuk zona ini." + }, + "allObjects": "Semua Objek", + "speedEstimation": { + "title": "Estimasi Kecepatan", + "desc": "Aktifkan estimasi kecepatan untuk objek di zona ini. Zona harus memiliki tepat 4 titik.", + "lineADistance": "Jarak garis A ({{unit}})", + "lineBDistance": "Jarak garis B ({{unit}})", + "lineCDistance": "Jarak garis C ({{unit}})", + "lineDDistance": "Jarak garis D ({{unit}})" + }, + "speedThreshold": { + "title": "Ambang Kecepatan ({{unit}})", + "desc": "Menentukan kecepatan minimum agar objek dianggap berada di zona ini.", + "toast": { + "error": { + "pointLengthError": "Estimasi kecepatan telah dinonaktifkan untuk zona ini. Zona dengan estimasi kecepatan harus memiliki tepat 4 titik.", + "loiteringTimeError": "Zona dengan waktu loitering lebih besar dari 0 sebaiknya tidak digunakan dengan estimasi kecepatan." + } + } + }, + "toast": { + "success": "Zona ({{zoneName}}) telah disimpan." + } + }, + "motionMasks": { + "label": "Masker Gerakan", + "documentTitle": "Edit Masker Gerakan - Frigate", + "desc": { + "title": "Masker gerakan digunakan untuk mencegah jenis gerakan yang tidak diinginkan memicu deteksi. Masking yang berlebihan akan membuat objek lebih sulit dilacak.", + "documentation": "Dokumentasi" + }, + "add": "Masker Gerakan Baru", + "edit": "Edit Masker Gerakan", + "defaultName": "Masker Gerakan {{number}}", + "context": { + "title": "Masker gerakan digunakan untuk mencegah jenis gerakan yang tidak diinginkan memicu deteksi (contoh: ranting pohon, stempel waktu kamera). Masker gerakan harus digunakan dengan sangat hemat, masking yang berlebihan akan membuat objek lebih sulit dilacak." + }, + "point_other": "{{count}} titik", + "clickDrawPolygon": "Klik untuk menggambar poligon pada gambar.", + "name": { + "title": "Nama", + "description": "Nama ramah opsional untuk masker gerakan ini.", + "placeholder": "Masukkan nama..." + }, + "polygonAreaTooLarge": { + "title": "Masker gerakan menutupi {{polygonArea}}% dari frame kamera. Masker gerakan besar tidak direkomendasikan.", + "tips": "Masker gerakan tidak mencegah objek terdeteksi. Anda sebaiknya menggunakan zona wajib sebagai gantinya." + }, + "toast": { + "success": { + "title": "{{polygonName}} telah disimpan.", + "noName": "Masker Gerakan telah disimpan." + } + } + }, + "objectMasks": { + "label": "Masker Objek", + "documentTitle": "Edit Masker Objek - Frigate", + "desc": { + "title": "Masker filter objek digunakan untuk menyaring positif palsu untuk tipe objek tertentu berdasarkan lokasi.", + "documentation": "Dokumentasi" + }, + "add": "Tambah Masker Objek", + "edit": "Edit Masker Objek", + "context": "Masker filter objek digunakan untuk menyaring positif palsu untuk tipe objek tertentu berdasarkan lokasi.", + "point_other": "{{count}} titik", + "clickDrawPolygon": "Klik untuk menggambar poligon pada gambar.", + "name": { + "title": "Nama", + "description": "Nama ramah opsional untuk masker objek ini.", + "placeholder": "Masukkan nama..." + }, + "objects": { + "title": "Objek", + "desc": "Tipe objek yang berlaku untuk masker objek ini.", + "allObjectTypes": "Semua tipe objek" + }, + "toast": { + "success": { + "title": "{{polygonName}} telah disimpan.", + "noName": "Masker Objek telah disimpan." + } + } + }, + "masks": { + "enabled": { + "title": "Diaktifkan", + "description": "Menentukan apakah masker ini diaktifkan dalam file konfigurasi. Jika dinonaktifkan, masker ini tidak dapat diaktifkan melalui MQTT. Masker yang dinonaktifkan diabaikan saat runtime." + } + } + }, + "motionDetectionTuner": { + "title": "Penyetel Deteksi Gerakan", + "unsavedChanges": "Perubahan Penyetel Gerakan yang belum disimpan ({{camera}})", + "desc": { + "title": "Frigate menggunakan deteksi gerakan sebagai pemeriksaan awal untuk melihat apakah ada sesuatu yang terjadi dalam frame yang layak diperiksa dengan deteksi objek.", + "documentation": "Baca Panduan Penyetelan Gerakan" + }, + "Threshold": { + "title": "Ambang", + "desc": "Nilai ambang menentukan seberapa besar perubahan luminansi piksel yang diperlukan agar dianggap sebagai gerakan. Default: 30" + }, + "contourArea": { + "title": "Area Kontur", + "desc": "Nilai area kontur digunakan untuk menentukan kelompok piksel yang berubah mana yang memenuhi syarat sebagai gerakan. Default: 10" + }, + "improveContrast": { + "title": "Tingkatkan Kontras", + "desc": "Tingkatkan kontras untuk adegan yang lebih gelap. Default: ON" + }, + "toast": { + "success": "Pengaturan gerakan telah disimpan." + } + }, + "debug": { + "title": "Debug", + "detectorDesc": "Frigate menggunakan detektor Anda ({{detectors}}) untuk mendeteksi objek di stream video kamera Anda.", + "desc": "Tampilan debug menunjukkan tampilan real-time objek yang dilacak dan statistiknya. Daftar objek menampilkan ringkasan objek yang terdeteksi dengan sedikit jeda waktu.", + "openCameraWebUI": "Buka UI Web milik {{camera}}", + "debugging": "Debugging", + "objectList": "Daftar Objek", + "noObjects": "Tidak ada objek", + "audio": { + "title": "Audio", + "noAudioDetections": "Tidak ada deteksi audio", + "score": "skor", + "currentRMS": "RMS Saat Ini", + "currentdbFS": "dbFS Saat Ini" + }, + "boundingBoxes": { + "title": "Kotak pembatas", + "desc": "Tampilkan kotak pembatas di sekitar objek yang dilacak", + "colors": { + "label": "Warna Kotak Pembatas Objek", + "info": "
  • Saat startup, warna berbeda akan ditetapkan untuk setiap label objek
  • Garis tipis biru tua menunjukkan bahwa objek tersebut tidak terdeteksi pada titik waktu saat ini
  • Garis tipis abu-abu menunjukkan bahwa objek tersebut terdeteksi sebagai diam
  • Garis tebal menunjukkan bahwa objek tersebut menjadi subjek pelacakan otomatis (jika diaktifkan)
  • " + } + }, + "timestamp": { + "title": "Stempel waktu", + "desc": "Hamparkan stempel waktu pada gambar" + }, + "zones": { + "title": "Zona", + "desc": "Tampilkan garis luar dari zona yang telah didefinisikan" + }, + "mask": { + "title": "Masker gerakan", + "desc": "Tampilkan poligon masker gerakan" + }, + "motion": { + "title": "Kotak gerakan", + "desc": "Tampilkan kotak di sekitar area tempat gerakan terdeteksi", + "tips": "

    Kotak Gerakan


    Kotak merah akan dihamparkan pada area frame tempat gerakan saat ini terdeteksi

    " + }, + "regions": { + "title": "Wilayah", + "desc": "Tampilkan kotak wilayah minat yang dikirim ke detektor objek", + "tips": "

    Kotak Wilayah


    Kotak hijau terang akan dihamparkan pada area minat di frame yang sedang dikirim ke detektor objek.

    " + }, + "paths": { + "title": "Jalur", + "desc": "Tampilkan titik-titik penting dari jalur objek yang dilacak", + "tips": "

    Jalur


    Garis dan lingkaran akan menunjukkan titik-titik penting yang telah dilalui objek yang dilacak selama siklus hidupnya.

    " + }, + "objectShapeFilterDrawing": { + "title": "Gambar Filter Bentuk Objek", + "desc": "Gambar persegi panjang pada gambar untuk melihat detail area dan rasio", + "tips": "Aktifkan opsi ini untuk menggambar persegi panjang pada gambar kamera guna menampilkan area dan rasionya. Nilai-nilai ini kemudian dapat digunakan untuk menetapkan parameter filter bentuk objek dalam konfigurasi Anda.", + "score": "Skor", + "ratio": "Rasio", + "area": "Area" + } + }, + "timestampPosition": { + "tl": "Kiri atas", + "tr": "Kanan atas", + "bl": "Kiri bawah", + "br": "Kanan bawah" + }, + "users": { + "title": "Pengguna", + "management": { + "title": "Manajemen Pengguna", + "desc": "Kelola akun pengguna untuk instance Frigate ini." + }, + "addUser": "Tambah Pengguna", + "updatePassword": "Atur Ulang Kata Sandi", + "toast": { + "success": { + "createUser": "Pengguna {{user}} berhasil dibuat", + "deleteUser": "Pengguna {{user}} berhasil dihapus", + "updatePassword": "Kata sandi berhasil diperbarui.", + "roleUpdated": "Peran untuk {{user}} diperbarui" + }, + "error": { + "setPasswordFailed": "Gagal menyimpan kata sandi: {{errorMessage}}", + "createUserFailed": "Gagal membuat pengguna: {{errorMessage}}", + "deleteUserFailed": "Gagal menghapus pengguna: {{errorMessage}}", + "roleUpdateFailed": "Gagal memperbarui peran: {{errorMessage}}" + } + }, + "table": { + "username": "Nama pengguna", + "actions": "Tindakan", + "role": "Peran", + "noUsers": "Tidak ada pengguna ditemukan.", + "changeRole": "Ubah peran pengguna", + "password": "Atur Ulang Kata Sandi", + "deleteUser": "Hapus pengguna" + }, + "dialog": { + "form": { + "user": { + "title": "Nama pengguna", + "desc": "Hanya huruf, angka, titik, dan garis bawah yang diizinkan.", + "placeholder": "Masukkan nama pengguna" + }, + "password": { + "title": "Kata sandi", + "placeholder": "Masukkan kata sandi", + "show": "Tampilkan kata sandi", + "hide": "Sembunyikan kata sandi", + "confirm": { + "title": "Konfirmasi Kata Sandi", + "placeholder": "Konfirmasi Kata Sandi" + }, + "strength": { + "title": "Kekuatan kata sandi: ", + "weak": "Lemah", + "medium": "Sedang", + "strong": "Kuat", + "veryStrong": "Sangat Kuat" + }, + "requirements": { + "title": "Persyaratan kata sandi:", + "length": "Setidaknya 12 karakter" + }, + "match": "Kata sandi cocok", + "notMatch": "Kata sandi tidak cocok" + }, + "newPassword": { + "title": "Kata Sandi Baru", + "placeholder": "Masukkan kata sandi baru", + "confirm": { + "placeholder": "Masukkan ulang kata sandi baru" + } + }, + "currentPassword": { + "title": "Kata Sandi Saat Ini", + "placeholder": "Masukkan kata sandi Anda saat ini" + }, + "usernameIsRequired": "Nama pengguna wajib diisi", + "passwordIsRequired": "Kata sandi wajib diisi" + }, + "createUser": { + "title": "Buat Pengguna Baru", + "desc": "Tambahkan akun pengguna baru dan tentukan perannya untuk akses ke area UI Frigate.", + "usernameOnlyInclude": "Nama pengguna hanya boleh berisi huruf, angka, . atau _", + "confirmPassword": "Harap konfirmasi kata sandi Anda" + }, + "deleteUser": { + "title": "Hapus Pengguna", + "desc": "Tindakan ini tidak dapat dibatalkan. Ini akan menghapus akun pengguna secara permanen dan menghapus semua data terkait.", + "warn": "Apakah Anda yakin ingin menghapus {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "Kata sandi tidak boleh kosong", + "doNotMatch": "Kata sandi tidak cocok", + "currentPasswordRequired": "Kata sandi saat ini wajib diisi", + "incorrectCurrentPassword": "Kata sandi saat ini salah", + "passwordVerificationFailed": "Gagal memverifikasi kata sandi", + "updatePassword": "Perbarui Kata Sandi untuk {{username}}", + "setPassword": "Tetapkan Kata Sandi", + "desc": "Buat kata sandi yang kuat untuk mengamankan akun ini.", + "multiDeviceWarning": "Perangkat lain tempat Anda masuk juga akan diminta login ulang dalam {{refresh_time}}.", + "multiDeviceAdmin": "Anda juga dapat memaksa semua pengguna untuk segera mengautentikasi ulang dengan memutar rahasia JWT Anda." + }, + "changeRole": { + "title": "Ubah Peran Pengguna", + "select": "Pilih peran", + "desc": "Perbarui izin untuk {{username}}", + "roleInfo": { + "intro": "Pilih peran yang sesuai untuk pengguna ini:", + "admin": "Admin", + "adminDesc": "Akses penuh ke semua fitur.", + "viewer": "Viewer", + "viewerDesc": "Terbatas hanya pada dasbor Langsung, Tinjauan, Jelajahi, dan Ekspor.", + "customDesc": "Peran kustom dengan akses kamera tertentu." + } + } + } + }, + "roles": { + "management": { + "title": "Manajemen Peran Viewer", + "desc": "Kelola peran viewer kustom dan izin akses kameranya untuk instance Frigate ini." + }, + "addRole": "Tambah Peran", + "table": { + "role": "Peran", + "cameras": "Kamera", + "actions": "Tindakan", + "noRoles": "Tidak ada peran kustom ditemukan.", + "editCameras": "Edit Kamera", + "deleteRole": "Hapus Peran" + }, + "toast": { + "success": { + "createRole": "Peran {{role}} berhasil dibuat", + "updateCameras": "Kamera untuk peran {{role}} telah diperbarui", + "deleteRole": "Peran {{role}} berhasil dihapus", + "userRolesUpdated_other": "{{count}} pengguna yang ditetapkan ke peran ini telah diperbarui menjadi 'viewer', yang memiliki akses ke semua kamera." + }, + "error": { + "createRoleFailed": "Gagal membuat peran: {{errorMessage}}", + "updateCamerasFailed": "Gagal memperbarui kamera: {{errorMessage}}", + "deleteRoleFailed": "Gagal menghapus peran: {{errorMessage}}", + "userUpdateFailed": "Gagal memperbarui peran pengguna: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Buat Peran Baru", + "desc": "Tambahkan peran baru dan tentukan izin akses kamera." + }, + "editCameras": { + "title": "Edit Kamera Peran", + "desc": "Perbarui akses kamera untuk peran {{role}}." + }, + "deleteRole": { + "title": "Hapus Peran", + "desc": "Tindakan ini tidak dapat dibatalkan. Ini akan menghapus peran secara permanen dan menetapkan pengguna mana pun dengan peran ini ke peran 'viewer', yang akan memberikan akses viewer ke semua kamera.", + "warn": "Apakah Anda yakin ingin menghapus {{role}}?", + "deleting": "Sedang menghapus..." + }, + "form": { + "role": { + "title": "Nama Peran", + "placeholder": "Masukkan nama peran", + "desc": "Hanya huruf, angka, titik, dan garis bawah yang diizinkan.", + "roleIsRequired": "Nama peran wajib diisi", + "roleOnlyInclude": "Nama peran hanya boleh berisi huruf, angka, . atau _", + "roleExists": "Peran dengan nama ini sudah ada." + }, + "cameras": { + "title": "Kamera", + "desc": "Pilih kamera yang dapat diakses oleh peran ini. Setidaknya satu kamera wajib dipilih.", + "required": "Setidaknya satu kamera harus dipilih." + } + } + } + }, + "notification": { + "title": "Notifikasi", + "notificationSettings": { + "title": "Pengaturan Notifikasi", + "desc": "Frigate dapat secara native mengirim notifikasi push ke perangkat Anda saat berjalan di browser atau diinstal sebagai PWA." + }, + "notificationUnavailable": { + "title": "Notifikasi Tidak Tersedia", + "desc": "Notifikasi push web memerlukan konteks aman (https://…). Ini adalah batasan browser. Akses Frigate secara aman untuk menggunakan notifikasi.", + "descPwa": "Di iOS, notifikasi push web hanya tersedia jika Frigate dipasang ke Layar Utama Anda. Buka menu Bagikan, pilih Tambahkan ke Layar Utama, lalu buka Frigate dari ikon baru tersebut untuk mendaftarkan perangkat ini agar menerima notifikasi." + }, + "globalSettings": { + "title": "Pengaturan Global", + "desc": "Tangguhkan notifikasi sementara untuk kamera tertentu pada semua perangkat yang terdaftar." + }, + "email": { + "title": "Email", + "placeholder": "mis. contoh@email.com", + "desc": "Email yang valid wajib diisi dan akan digunakan untuk memberi tahu Anda jika ada masalah dengan layanan push." + }, + "cameras": { + "title": "Kamera", + "noCameras": "Tidak ada kamera tersedia", + "desc": "Pilih kamera mana yang akan diaktifkan notifikasinya." + }, + "deviceSpecific": "Pengaturan Khusus Perangkat", + "registerDevice": "Daftarkan Perangkat Ini", + "unregisterDevice": "Batalkan Pendaftaran Perangkat Ini", + "sendTestNotification": "Kirim notifikasi uji", + "unsavedRegistrations": "Pendaftaran Notifikasi yang belum disimpan", + "unsavedChanges": "Perubahan Notifikasi yang belum disimpan", + "active": "Notifikasi Aktif", + "suspended": "Notifikasi ditangguhkan {{time}}", + "suspendTime": { + "suspend": "Tangguhkan", + "5minutes": "Tangguhkan selama 5 menit", + "10minutes": "Tangguhkan selama 10 menit", + "30minutes": "Tangguhkan selama 30 menit", + "1hour": "Tangguhkan selama 1 jam", + "12hours": "Tangguhkan selama 12 jam", + "24hours": "Tangguhkan selama 24 jam", + "untilRestart": "Tangguhkan hingga dimulai ulang" + }, + "cancelSuspension": "Batalkan Penangguhan", + "toast": { + "success": { + "registered": "Berhasil terdaftar untuk notifikasi. Mulai ulang Frigate diperlukan sebelum notifikasi apa pun (termasuk notifikasi uji) dapat dikirim.", + "settingSaved": "Pengaturan notifikasi telah disimpan." + }, + "error": { + "registerFailed": "Gagal menyimpan pendaftaran notifikasi." + } + } + }, + "frigatePlus": { + "title": "Pengaturan Frigate+", + "description": "Frigate+ adalah layanan berlangganan yang menyediakan akses ke fitur dan kemampuan tambahan untuk instance Frigate Anda, termasuk kemampuan menggunakan model deteksi objek kustom yang dilatih dengan data Anda sendiri. Anda dapat mengelola pengaturan model Frigate+ Anda di sini.", + "cardTitles": { + "api": "API", + "currentModel": "Model Saat Ini", + "otherModels": "Model Lainnya", + "configuration": "Konfigurasi" + }, + "apiKey": { + "title": "Kunci API Frigate+", + "validated": "Kunci API Frigate+ terdeteksi dan tervalidasi", + "notValidated": "Kunci API Frigate+ tidak terdeteksi atau tidak tervalidasi", + "desc": "Kunci API Frigate+ memungkinkan integrasi dengan layanan Frigate+.", + "plusLink": "Baca lebih lanjut tentang Frigate+" + }, + "snapshotConfig": { + "title": "Konfigurasi Cuplikan", + "desc": "Mengirim ke Frigate+ mengharuskan cuplikan diaktifkan dalam konfigurasi Anda.", + "cleanCopyWarning": "Beberapa kamera menonaktifkan cuplikan", + "table": { + "camera": "Kamera", + "snapshots": "Cuplikan" + } + }, + "modelInfo": { + "title": "Informasi Model", + "modelType": "Tipe Model", + "trainDate": "Tanggal Pelatihan", + "baseModel": "Model Dasar", + "plusModelType": { + "baseModel": "Model Dasar", + "userModel": "Disetel Halus" + }, + "supportedDetectors": "Detektor yang Didukung", + "cameras": "Kamera", + "loading": "Memuat informasi model…", + "error": "Gagal memuat informasi model", + "noModelLoaded": "Saat ini tidak ada model Frigate+ yang dimuat.", + "availableModels": "Model Frigate+ yang tersedia", + "loadingAvailableModels": "Memuat model yang tersedia…", + "selectModel": "Pilih model", + "noModelsAvailable": "Tidak ada model tersedia", + "filter": { + "ariaLabel": "Filter model berdasarkan tipe", + "baseModels": "Model Dasar", + "fineTunedModels": "Model yang Disetel Halus" + }, + "modelSelect": "Model Frigate+ Anda yang tersedia dapat dipilih di sini. Perlu dicatat bahwa hanya model yang kompatibel dengan konfigurasi detektor Anda saat ini yang dapat dipilih." + }, + "changeInDetectorsAndModel": "Ubah model", + "unsavedChanges": "Perubahan pengaturan Frigate+ yang belum disimpan", + "restart_required": "Perlu mulai ulang (model Frigate+ berubah)", + "toast": { + "success": "Pengaturan Frigate+ telah disimpan. Mulai ulang Frigate untuk menerapkan perubahan.", + "error": "Gagal menyimpan perubahan konfigurasi: {{errorMessage}}" + } + }, + "detectorsAndModel": { + "title": "Detektor dan model", + "description": "Konfigurasikan backend detektor yang menjalankan deteksi objek dan model yang digunakannya. Perubahan disimpan bersama agar detektor dan model tetap sinkron.", + "cardTitles": { + "detector": "Perangkat Keras Detektor", + "model": "Model Deteksi" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Model Kustom" + }, + "mismatch": { + "warning": "Model Frigate+ saat ini \"{{model}}\" memerlukan detektor {{required}}. Pilih model yang kompatibel di bawah atau beralih ke Model Kustom sebelum menyimpan." + }, + "plusModel": { + "requiresDetector": "Memerlukan: {{detector}}", + "noModelSelected": "Pilih model Frigate+" + }, + "toast": { + "saveSuccess": "Pengaturan detektor dan model telah disimpan. Mulai ulang Frigate untuk menerapkan perubahan.", + "saveError": "Gagal menyimpan pengaturan detektor dan model" + }, + "unsavedChanges": "Perubahan detektor dan model yang belum disimpan", + "restartRequired": "Perlu mulai ulang (detektor atau model berubah)" + }, + "triggers": { + "documentTitle": "Pemicu", + "semanticSearch": { + "title": "Pencarian Semantik dinonaktifkan", + "desc": "Pencarian Semantik harus diaktifkan untuk menggunakan Pemicu." + }, + "management": { + "title": "Pemicu", + "desc": "Kelola pemicu untuk {{camera}}. Gunakan tipe thumbnail untuk memicu pada thumbnail yang mirip dengan objek terlacak yang Anda pilih, dan tipe deskripsi untuk memicu pada deskripsi yang mirip dengan teks yang Anda tentukan." + }, + "addTrigger": "Tambah Pemicu", + "table": { + "name": "Nama", + "type": "Tipe", + "content": "Konten", + "threshold": "Ambang", + "actions": "Tindakan", + "noTriggers": "Tidak ada pemicu yang dikonfigurasi untuk kamera ini.", + "edit": "Edit", + "deleteTrigger": "Hapus Pemicu", + "lastTriggered": "Terakhir dipicu" + }, + "type": { + "thumbnail": "Thumbnail", + "description": "Deskripsi" + }, + "actions": { + "notification": "Kirim Notifikasi", + "sub_label": "Tambahkan Sub Label", + "attribute": "Tambahkan Atribut" + }, + "dialog": { + "createTrigger": { + "title": "Buat Pemicu", + "desc": "Buat pemicu untuk kamera {{camera}}" + }, + "editTrigger": { + "title": "Edit Pemicu", + "desc": "Edit pengaturan pemicu pada kamera {{camera}}" + }, + "deleteTrigger": { + "title": "Hapus Pemicu", + "desc": "Apakah Anda yakin ingin menghapus pemicu {{triggerName}}? Tindakan ini tidak dapat dibatalkan." + }, + "form": { + "name": { + "title": "Nama", + "placeholder": "Beri nama pemicu ini", + "description": "Masukkan nama atau deskripsi unik untuk mengidentifikasi pemicu ini", + "error": { + "minLength": "Bidang harus memiliki panjang minimal 2 karakter.", + "invalidCharacters": "Bidang hanya boleh berisi huruf, angka, garis bawah, dan tanda hubung.", + "alreadyExists": "Pemicu dengan nama ini sudah ada untuk kamera ini." + } + }, + "enabled": { + "description": "Aktifkan atau nonaktifkan pemicu ini" + }, + "type": { + "title": "Tipe", + "placeholder": "Pilih tipe pemicu", + "description": "Picu saat deskripsi objek terlacak yang serupa terdeteksi", + "thumbnail": "Picu saat thumbnail objek terlacak yang serupa terdeteksi" + }, + "content": { + "title": "Konten", + "imagePlaceholder": "Pilih thumbnail", + "textPlaceholder": "Masukkan konten teks", + "imageDesc": "Hanya 100 thumbnail terbaru yang ditampilkan. Jika Anda tidak dapat menemukan thumbnail yang diinginkan, silakan tinjau objek sebelumnya di Jelajahi dan atur pemicu dari menu di sana.", + "textDesc": "Masukkan teks untuk memicu tindakan ini saat deskripsi objek terlacak yang serupa terdeteksi.", + "error": { + "required": "Konten wajib diisi." + } + }, + "threshold": { + "title": "Ambang", + "desc": "Tetapkan ambang kemiripan untuk pemicu ini. Ambang yang lebih tinggi berarti kecocokan yang lebih dekat diperlukan agar pemicu dijalankan.", + "error": { + "min": "Ambang harus minimal 0", + "max": "Ambang harus maksimal 1" + } + }, + "actions": { + "title": "Tindakan", + "desc": "Secara default, Frigate mengirim pesan MQTT untuk semua pemicu. Sub label menambahkan nama pemicu ke label objek. Atribut adalah metadata yang dapat dicari dan disimpan secara terpisah dalam metadata objek terlacak.", + "error": { + "min": "Setidaknya satu tindakan harus dipilih." + } + } + } + }, + "wizard": { + "title": "Buat Pemicu", + "step1": { + "description": "Konfigurasikan pengaturan dasar untuk pemicu Anda." + }, + "step2": { + "description": "Atur konten yang akan memicu tindakan ini." + }, + "step3": { + "description": "Konfigurasikan ambang dan tindakan untuk pemicu ini." + }, + "steps": { + "nameAndType": "Nama dan Tipe", + "configureData": "Konfigurasikan Data", + "thresholdAndActions": "Ambang dan Tindakan" + } + }, + "toast": { + "success": { + "createTrigger": "Pemicu {{name}} berhasil dibuat.", + "updateTrigger": "Pemicu {{name}} berhasil diperbarui.", + "deleteTrigger": "Pemicu {{name}} berhasil dihapus." + }, + "error": { + "createTriggerFailed": "Gagal membuat pemicu: {{errorMessage}}", + "updateTriggerFailed": "Gagal memperbarui pemicu: {{errorMessage}}", + "deleteTriggerFailed": "Gagal menghapus pemicu: {{errorMessage}}" + } + } + }, + "maintenance": { + "title": "Pemeliharaan", + "sync": { + "title": "Sinkronisasi Media", + "desc": "Frigate akan secara berkala membersihkan media sesuai jadwal reguler berdasarkan konfigurasi retensi Anda. Wajar jika ada beberapa file yatim piatu saat Frigate berjalan. Gunakan fitur ini untuk menghapus file media yatim piatu dari disk yang tidak lagi direferensikan dalam basis data.", + "started": "Sinkronisasi media dimulai.", + "alreadyRunning": "Pekerjaan sinkronisasi sudah sedang berjalan", + "error": "Gagal memulai sinkronisasi", + "currentStatus": "Status", + "jobId": "ID Pekerjaan", + "startTime": "Waktu Mulai", + "endTime": "Waktu Selesai", + "statusLabel": "Status", + "results": "Hasil", + "errorLabel": "Kesalahan", + "mediaTypes": "Tipe Media", + "allMedia": "Semua Media", + "dryRun": "Dry Run", + "dryRunEnabled": "Tidak ada file yang akan dihapus", + "dryRunDisabled": "File akan dihapus", + "force": "Paksa", + "forceDesc": "Lewati ambang keamanan dan selesaikan sinkronisasi meskipun lebih dari 50% file akan dihapus.", + "verbose": "Verbose", + "verboseDesc": "Tulis daftar lengkap file yatim piatu ke disk untuk ditinjau.", + "running": "Sinkronisasi Berjalan...", + "start": "Mulai Sinkronisasi", + "inProgress": "Sinkronisasi sedang berlangsung. Halaman ini dinonaktifkan.", + "status": { + "queued": "Dalam antrean", + "running": "Berjalan", + "completed": "Selesai", + "failed": "Gagal", + "notRunning": "Tidak Berjalan" + }, + "resultsFields": { + "filesChecked": "File Diperiksa", + "orphansFound": "File Yatim Piatu Ditemukan", + "orphansDeleted": "File Yatim Piatu Dihapus", + "aborted": "Dibatalkan. Penghapusan akan melebihi ambang keamanan.", + "error": "Kesalahan", + "totals": "Total" + }, + "event_snapshots": "Cuplikan Objek Terlacak", + "event_thumbnails": "Thumbnail Objek Terlacak", + "review_thumbnails": "Thumbnail Tinjauan", + "previews": "Pratinjau", + "exports": "Ekspor", + "recordings": "Rekaman" + }, + "regionGrid": { + "title": "Grid Wilayah", + "desc": "Grid wilayah adalah optimasi yang mempelajari di mana objek dengan ukuran berbeda biasanya muncul dalam bidang pandang tiap kamera. Frigate menggunakan data ini untuk menentukan ukuran wilayah deteksi secara efisien. Grid ini dibangun secara otomatis seiring waktu dari data objek terlacak.", + "clear": "Hapus grid wilayah", + "clearConfirmTitle": "Hapus Grid Wilayah", + "clearConfirmDesc": "Menghapus grid wilayah tidak direkomendasikan kecuali Anda baru-baru ini mengubah ukuran model detektor atau mengubah posisi fisik kamera dan mengalami masalah pelacakan objek. Grid akan dibangun ulang secara otomatis seiring waktu saat objek dilacak. Frigate perlu dimulai ulang agar perubahan diterapkan.", + "clearSuccess": "Grid wilayah berhasil dihapus", + "clearError": "Gagal menghapus grid wilayah", + "restartRequired": "Perlu mulai ulang agar perubahan grid wilayah diterapkan" + } + }, + "configForm": { + "global": { + "title": "Pengaturan Global", + "description": "Pengaturan ini berlaku untuk semua kamera kecuali ditimpa dalam pengaturan khusus kamera." + }, + "camera": { + "title": "Pengaturan Kamera", + "description": "Pengaturan ini hanya berlaku untuk kamera ini dan menimpa pengaturan global.", + "noCameras": "Tidak ada kamera tersedia" + }, + "advancedSettingsCount": "Pengaturan Lanjutan ({{count}})", + "advancedCount": "Lanjutan ({{count}})", + "showAdvanced": "Tampilkan Pengaturan Lanjutan", + "tabs": { + "sharedDefaults": "Default Bersama", + "system": "Sistem", + "integrations": "Integrasi" + }, + "additionalProperties": { + "keyLabel": "Kunci", + "valueLabel": "Nilai", + "keyPlaceholder": "Kunci baru", + "remove": "Hapus" + }, + "knownPlates": { + "namePlaceholder": "mis., Mobil Istri", + "platePlaceholder": "Nomor pelat atau regex" + }, + "timezone": { + "defaultOption": "Gunakan zona waktu browser" + }, + "roleMap": { + "empty": "Tidak ada pemetaan peran", + "roleLabel": "Peran", + "groupsLabel": "Grup", + "addMapping": "Tambah pemetaan peran", + "remove": "Hapus" + }, + "ffmpegArgs": { + "preset": "Preset", + "manual": "Argumen manual", + "inherit": "Warisi dari pengaturan kamera", + "none": "Tidak ada", + "useGlobalSetting": "Warisi dari pengaturan global", + "selectPreset": "Pilih preset", + "manualPlaceholder": "Masukkan argumen FFmpeg", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (GPU Intel/AMD)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "GPU NVIDIA", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (Generik)", + "preset-http-mjpeg-generic": "HTTP MJPEG (Generik)", + "preset-http-reolink": "HTTP - Kamera Reolink", + "preset-rtmp-generic": "RTMP (Generik)", + "preset-rtsp-generic": "RTSP (Generik)", + "preset-rtsp-restream": "RTSP - Restream dari go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - Restream dari go2rtc (Latensi Rendah)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Rekam (Generik, tanpa audio)", + "preset-record-generic-audio-copy": "Rekam (Generik + Salin Audio)", + "preset-record-generic-audio-aac": "Rekam (Generik + Audio ke AAC)", + "preset-record-mjpeg": "Rekam - Kamera MJPEG", + "preset-record-jpeg": "Rekam - Kamera JPEG", + "preset-record-ubiquiti": "Rekam - Kamera Ubiquiti" + } + }, + "cameraInputs": { + "itemTitle": "Stream {{index}}" + }, + "restartRequiredField": "Perlu mulai ulang", + "restartRequiredFooter": "Konfigurasi berubah - Perlu mulai ulang", + "sections": { + "detect": "Deteksi", + "record": "Perekaman", + "snapshots": "Cuplikan", + "motion": "Gerakan", + "objects": "Objek", + "review": "Tinjauan", + "audio": "Audio", + "notifications": "Notifikasi", + "live": "Tampilan Langsung", + "timestamp_style": "Stempel waktu", + "mqtt": "MQTT", + "database": "Basis data", + "telemetry": "Telemetri", + "auth": "Autentikasi", + "tls": "TLS", + "proxy": "Proksi", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg", + "detectors": "Detektor", + "model": "Model", + "semantic_search": "Pencarian Semantik", + "genai": "GenAI", + "face_recognition": "Pengenalan Wajah", + "lpr": "Pengenalan Pelat Nomor", + "birdseye": "Birdseye", + "masksAndZones": "Masker / Zona" + }, + "detect": { + "title": "Pengaturan Deteksi" + }, + "detectors": { + "title": "Pengaturan Detektor", + "singleType": "Hanya satu detektor {{type}} yang diizinkan.", + "keyRequired": "Nama detektor wajib diisi.", + "keyDuplicate": "Nama detektor sudah ada.", + "noSchema": "Tidak ada skema detektor yang tersedia.", + "none": "Tidak ada instance detektor yang dikonfigurasi.", + "add": "Tambah detektor", + "addCustomKey": "Tambah kunci kustom" + }, + "record": { + "title": "Pengaturan Perekaman" + }, + "snapshots": { + "title": "Pengaturan Cuplikan" + }, + "motion": { + "title": "Pengaturan Gerakan" + }, + "objects": { + "title": "Pengaturan Objek" + }, + "audioLabels": { + "summary": "{{count}} label audio dipilih", + "empty": "Tidak ada label audio tersedia" + }, + "objectLabels": { + "summary": "{{count}} tipe objek dipilih", + "empty": "Tidak ada label objek tersedia" + }, + "reviewLabels": { + "summary": "{{count}} label dipilih", + "empty": "Tidak ada label tersedia" + }, + "filters": { + "objectFieldLabel": "{{field}} untuk {{label}}" + }, + "zoneNames": { + "summary": "{{count}} dipilih", + "empty": "Tidak ada zona tersedia" + }, + "inputRoles": { + "summary": "{{count}} peran dipilih", + "empty": "Tidak ada peran tersedia", + "options": { + "detect": "Deteksi", + "record": "Rekam", + "audio": "Audio" + } + }, + "genaiRoles": { + "options": { + "embeddings": "Embedding", + "descriptions": "Deskripsi", + "chat": "Chat" + } + }, + "semanticSearchModel": { + "placeholder": "Pilih model…", + "builtIn": "Model Bawaan", + "genaiProviders": "Penyedia GenAI" + }, + "review": { + "title": "Pengaturan Tinjauan" + }, + "audio": { + "title": "Pengaturan Audio" + }, + "notifications": { + "title": "Pengaturan Notifikasi" + }, + "live": { + "title": "Pengaturan Tampilan Langsung" + }, + "timestamp_style": { + "title": "Pengaturan Stempel Waktu" + }, + "searchPlaceholder": "Cari...", + "addCustomLabel": "Tambahkan label kustom...", + "genaiModel": { + "placeholder": "Pilih atau masukkan model…", + "search": "Cari atau masukkan model…", + "noModels": "Tidak ada model tersedia", + "available": "Model yang tersedia", + "useCustom": "Gunakan \"{{value}}\"", + "refresh": "Segarkan model", + "probeFailed": "Gagal memeriksa model", + "fetchedModels": "Berhasil mengambil daftar model" + }, + "liveStreams": { + "streamNameLabel": "Nama stream", + "streamNamePlaceholder": "misalnya, Stream HD Utama", + "go2rtcStreamLabel": "Stream go2rtc", + "go2rtcStreamPlaceholder": "Pilih stream go2rtc", + "go2rtcStreamSearch": "Cari atau masukkan nama stream…", + "noGo2rtcStreams": "Tidak ada stream go2rtc yang dikonfigurasi", + "availableStreams": "Stream yang tersedia", + "useCustom": "Gunakan \"{{value}}\"", + "addStream": "Tambahkan stream" + }, + "semanticSearchModelSize": { + "notApplicable": "Tidak berlaku untuk penyedia GenAI" + }, + "ptzPresets": { + "placeholder": "Pilih atau masukkan preset...", + "search": "Cari atau masukkan preset...", + "noPresets": "Tidak ada preset yang tersedia", + "available": "Preset kamera", + "useCustom": "Gunakan \"{{value}}\"" + }, + "defaultRole": { + "admin": "Admin", + "viewer": "Penampil" + } + }, + "globalConfig": { + "title": "Konfigurasi Global", + "description": "Konfigurasikan pengaturan global yang berlaku untuk semua kamera kecuali jika ditimpa.", + "toast": { + "success": "Pengaturan global berhasil disimpan", + "error": "Gagal menyimpan pengaturan global", + "validationError": "Validasi gagal" + } + }, + "cameraConfig": { + "title": "Konfigurasi Kamera", + "description": "Konfigurasikan pengaturan untuk masing-masing kamera. Pengaturan menimpa default global.", + "overriddenBadge": "Ditimpa", + "resetToGlobal": "Atur Ulang ke Global", + "toast": { + "success": "Pengaturan kamera berhasil disimpan", + "error": "Gagal menyimpan pengaturan kamera" + } + }, + "toast": { + "success": "Pengaturan berhasil disimpan", + "applied": "Pengaturan berhasil diterapkan", + "successRestartRequired": "Pengaturan berhasil disimpan. Mulai ulang Frigate untuk menerapkan perubahan Anda.", + "error": "Gagal menyimpan pengaturan", + "validationError": "Validasi gagal: {{message}}", + "resetSuccess": "Atur ulang ke default global", + "resetError": "Gagal mengatur ulang pengaturan", + "saveAllSuccess_other": "Semua {{count}} bagian berhasil disimpan.", + "saveAllSuccessRestartRequired_other": "Semua {{count}} bagian berhasil disimpan. Mulai ulang Frigate untuk menerapkan perubahan Anda.", + "saveAllPartial_other": "{{successCount}} dari {{totalCount}} bagian berhasil disimpan. {{failCount}} gagal.", + "saveAllFailure": "Gagal menyimpan semua bagian." + }, + "profiles": { + "title": "Profil", + "activeProfile": "Profil Aktif", + "noActiveProfile": "Tidak ada profil aktif", + "active": "Aktif", + "activated": "Profil '{{profile}}' diaktifkan", + "activateFailed": "Gagal menetapkan profil", + "deactivated": "Profil dinonaktifkan", + "noProfiles": "Tidak ada profil yang didefinisikan.", + "noOverrides": "Tidak ada penimpaan", + "cameraCount_other": "{{count}} kamera", + "columnCamera": "Kamera", + "columnOverrides": "Penimpaan Profil", + "baseConfig": "Konfigurasi Dasar", + "addProfile": "Tambah Profil", + "newProfile": "Profil Baru", + "profileNamePlaceholder": "mis., Armed, Away, Night Mode", + "friendlyNameLabel": "Nama Profil", + "profileIdLabel": "ID Profil", + "profileIdDescription": "Pengidentifikasi internal yang digunakan dalam konfigurasi dan otomatisasi", + "nameInvalid": "Hanya huruf kecil, angka, dan garis bawah yang diizinkan", + "nameDuplicate": "Profil dengan nama ini sudah ada", + "error": { + "mustBeAtLeastTwoCharacters": "Harus minimal 2 karakter", + "mustNotContainPeriod": "Tidak boleh mengandung titik", + "alreadyExists": "Profil dengan ID ini sudah ada" + }, + "renameProfile": "Ubah Nama Profil", + "renameSuccess": "Profil diubah namanya menjadi '{{profile}}'", + "deleteProfile": "Hapus Profil", + "deleteProfileConfirm": "Hapus profil \"{{profile}}\" dari semua kamera? Ini tidak dapat dibatalkan.", + "deleteSuccess": "Profil '{{profile}}' dihapus", + "createSuccess": "Profil '{{profile}}' dibuat", + "removeOverride": "Hapus Penimpaan Profil", + "deleteSection": "Hapus Penimpaan Bagian", + "deleteSectionConfirm": "Hapus penimpaan {{section}} untuk profil {{profile}} pada {{camera}}?", + "deleteSectionSuccess": "Penimpaan {{section}} untuk {{profile}} telah dihapus", + "enableSwitch": "Aktifkan Profil", + "enabledDescription": "Profil diaktifkan. Buat profil baru di bawah, buka bagian konfigurasi kamera untuk membuat perubahan, lalu simpan agar perubahan berlaku.", + "disabledDescription": "Profil memungkinkan Anda mendefinisikan kumpulan bernama dari penimpaan konfigurasi kamera (mis., armed, away, night) yang dapat diaktifkan sesuai kebutuhan." + }, + "unsavedChanges": "Anda memiliki perubahan yang belum disimpan", + "confirmReset": "Konfirmasi Atur Ulang", + "resetToDefaultDescription": "Ini akan mengatur ulang semua pengaturan di bagian ini ke nilai defaultnya. Tindakan ini tidak dapat dibatalkan.", + "resetToGlobalDescription": "Ini akan mengatur ulang pengaturan di bagian ini ke default global. Tindakan ini tidak dapat dibatalkan.", + "go2rtcStreams": { + "title": "Stream go2rtc", + "description": "Kelola konfigurasi stream go2rtc untuk restreaming kamera. Setiap stream memiliki nama dan satu atau lebih URL sumber.", + "addStream": "Tambah stream", + "addStreamDesc": "Masukkan nama untuk stream baru. Nama ini akan digunakan untuk merujuk stream dalam konfigurasi kamera Anda.", + "addUrl": "Tambah URL", + "streamNumber": "Stream {{index}}", + "streamName": "Nama stream", + "streamNamePlaceholder": "mis., front_door", + "streamUrlPlaceholder": "mis., rtsp://user:pass@192.168.1.100/stream", + "deleteStream": "Hapus stream", + "deleteStreamConfirm": "Apakah Anda yakin ingin menghapus stream \"{{streamName}}\"? Kamera yang merujuk stream ini mungkin berhenti berfungsi.", + "noStreams": "Tidak ada stream go2rtc yang dikonfigurasi. Tambahkan stream untuk memulai.", + "validation": { + "nameRequired": "Nama stream wajib diisi", + "nameDuplicate": "Stream dengan nama ini sudah ada", + "nameInvalid": "Nama stream hanya boleh berisi huruf, angka, garis bawah, dan tanda hubung", + "urlRequired": "Setidaknya satu URL wajib diisi" + }, + "renameStream": "Ubah nama stream", + "renameStreamDesc": "Masukkan nama baru untuk stream ini. Mengubah nama stream dapat merusak kamera atau stream lain yang merujuknya berdasarkan nama.", + "newStreamName": "Nama stream baru", + "ffmpeg": { + "useFfmpegModule": "Gunakan mode kompatibilitas (ffmpeg)", + "video": "Video", + "audio": "Audio", + "hardware": "Akselerasi perangkat keras", + "videoCopy": "Salin", + "videoH264": "Transkode ke H.264", + "videoH265": "Transkode ke H.265", + "videoExclude": "Kecualikan", + "audioCopy": "Salin", + "audioAac": "Transkode ke AAC", + "audioOpus": "Transkode ke Opus", + "audioPcmu": "Transkode ke PCM μ-law", + "audioPcma": "Transkode ke PCM A-law", + "audioPcm": "Transkode ke PCM", + "audioMp3": "Transkode ke MP3", + "audioExclude": "Kecualikan", + "hardwareNone": "Tanpa akselerasi perangkat keras", + "hardwareAuto": "Otomatis (direkomendasikan)", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "Tambah codec video", + "addAudioCodec": "Tambah codec audio", + "removeCodec": "Hapus codec" + }, + "sourceNumber": "Sumber {{index}}" + }, + "birdseye": { + "trackingMode": { + "objects": "Objek", + "motion": "Gerakan", + "continuous": "Kontinu" + }, + "cameraOrder": { + "label": "Urutan kamera", + "description": "Seret kamera untuk mengatur urutannya dalam tata letak Birdseye.", + "reorderHandle": "Seret untuk mengubah urutan", + "saving": "Menyimpan…", + "saved": "Tersimpan" + } + }, + "retainMode": { + "all": "Semua", + "motion": "Gerakan", + "active_objects": "Objek Aktif" + }, + "previewQuality": { + "very_high": "Sangat Tinggi", + "high": "Tinggi", + "medium": "Sedang", + "low": "Rendah", + "very_low": "Sangat Rendah" + }, + "ui": { + "timeFormat": { + "browser": "Browser", + "12hour": "12 jam", + "24hour": "24 jam" + }, + "TimeOrDateStyle": { + "full": "Penuh", + "long": "Panjang", + "medium": "Sedang", + "short": "Pendek" + }, + "unitSystem": { + "metric": "Metrik", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Rekaman", + "previews": "Pratinjau" + } + }, + "logger": { + "logLevel": { + "debug": "Debug", + "info": "Info", + "warning": "Peringatan", + "error": "Kesalahan", + "critical": "Kritis" + } + }, + "onvif": { + "profileAuto": "Otomatis", + "profileLoading": "Memuat profil...", + "autotracking": { + "zooming": { + "disabled": "Dinonaktifkan", + "absolute": "Absolut", + "relative": "Relatif" + } + } + }, + "modelSize": { + "small": "Kecil", + "large": "Besar" + } +} diff --git a/web/public/locales/id/views/system.json b/web/public/locales/id/views/system.json new file mode 100644 index 0000000..639d94c --- /dev/null +++ b/web/public/locales/id/views/system.json @@ -0,0 +1,259 @@ +{ + "documentTitle": { + "cameras": "Statistik Kamera - Frigate", + "storage": "Statistik Penyimpanan - Frigate", + "general": "Statistik Umum - Frigate", + "enrichments": "Statistik Enrichment - Frigate", + "logs": { + "frigate": "Log Frigate - Frigate", + "go2rtc": "Log Go2RTC - Frigate", + "nginx": "Log Nginx - Frigate", + "websocket": "Log Pesan - Frigate" + } + }, + "title": "Sistem", + "metrics": "Metrik sistem", + "logs": { + "download": { + "label": "Unduh Log" + }, + "copy": { + "label": "Salin ke Papan Klip", + "success": "Log berhasil disalin ke papan klip", + "error": "Tidak dapat menyalin log ke papan klip" + }, + "type": { + "label": "Jenis", + "timestamp": "Stempel waktu", + "tag": "Tag", + "message": "Pesan" + }, + "tips": "Log sedang dialirkan dari server", + "toast": { + "error": { + "fetchingLogsFailed": "Kesalahan saat mengambil log: {{errorMessage}}", + "whileStreamingLogs": "Kesalahan saat mengalirkan log: {{errorMessage}}" + } + }, + "websocket": { + "label": "Pesan", + "pause": "Jeda", + "resume": "Lanjutkan", + "clear": "Bersihkan", + "filter": { + "all": "Semua topik", + "topics": "Topik", + "events": "Peristiwa", + "reviews": "Tinjauan", + "classification": "Klasifikasi", + "face_recognition": "Pengenalan Wajah", + "lpr": "LPR", + "camera_activity": "Aktivitas kamera", + "system": "Sistem", + "camera": "Kamera", + "all_cameras": "Semua kamera", + "cameras_count_one": "{{count}} Kamera", + "cameras_count_other": "{{count}} Kamera" + }, + "empty": "Belum ada pesan yang ditangkap", + "count_one": "{{count}} pesan", + "count_other": "{{count}} pesan", + "expanded": { + "payload": "Payload" + } + } + }, + "general": { + "title": "Umum", + "detector": { + "title": "Detektor", + "inferenceSpeed": "Kecepatan Inferensi Detektor", + "temperature": "Suhu Detektor", + "cpuUsage": "Penggunaan CPU Detektor", + "cpuUsageInformation": "CPU yang digunakan untuk menyiapkan data input dan output ke/dari model deteksi. Nilai ini tidak mengukur penggunaan inferensi, meskipun menggunakan GPU atau akselerator.", + "memoryUsage": "Penggunaan Memori Detektor" + }, + "hardwareInfo": { + "title": "Info Perangkat Keras", + "gpuUsage": "Penggunaan GPU", + "gpuMemory": "Memori GPU", + "gpuEncoder": "Encoder GPU", + "gpuCompute": "Komputasi / Enkode GPU", + "gpuDecoder": "Decoder GPU", + "gpuTemperature": "Suhu GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Output Vainfo", + "returnCode": "Kode Pengembalian: {{code}}", + "processOutput": "Output Proses:", + "processError": "Kesalahan Proses:" + }, + "nvidiaSMIOutput": { + "title": "Output Nvidia SMI", + "name": "Nama: {{name}}", + "driver": "Driver: {{driver}}", + "cudaComputerCapability": "Kemampuan Komputasi CUDA: {{cuda_compute}}", + "vbios": "Info VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Tutup info GPU" + }, + "copyInfo": { + "label": "Salin info GPU" + }, + "toast": { + "success": "Info GPU berhasil disalin ke papan klip" + } + }, + "npuUsage": "Penggunaan NPU", + "npuMemory": "Memori NPU", + "npuTemperature": "Suhu NPU", + "intelGpuWarning": { + "title": "Peringatan Statistik GPU Intel", + "message": "Statistik GPU tidak tersedia", + "description": "Ini adalah bug yang sudah diketahui pada alat pelaporan statistik GPU Intel (intel_gpu_top) yang dapat rusak dan berulang kali mengembalikan penggunaan GPU sebesar 0% bahkan ketika akselerasi perangkat keras dan deteksi objek berjalan dengan benar pada (i)GPU. Ini bukan bug Frigate. Anda dapat memulai ulang host untuk memperbaiki masalah ini sementara dan memastikan GPU berfungsi dengan benar. Ini tidak memengaruhi kinerja." + } + }, + "otherProcesses": { + "title": "Proses Lainnya", + "processCpuUsage": "Penggunaan CPU Proses", + "processMemoryUsage": "Penggunaan Memori Proses", + "series": { + "go2rtc": "go2rtc", + "recording": "perekaman", + "review_segment": "segmen tinjauan", + "embeddings": "embedding", + "audio_detector": "detektor audio" + } + } + }, + "storage": { + "title": "Penyimpanan", + "overview": "Ringkasan", + "recordings": { + "title": "Rekaman", + "tips": "Nilai ini menunjukkan total penyimpanan yang digunakan oleh rekaman di basis data Frigate. Frigate tidak melacak penggunaan penyimpanan untuk semua file di disk Anda.", + "earliestRecording": "Rekaman paling awal yang tersedia:" + }, + "shm": { + "title": "Alokasi SHM (memori bersama)", + "warning": "Ukuran SHM saat ini sebesar {{total}}MB terlalu kecil. Tingkatkan menjadi setidaknya {{min_shm}}MB.", + "frameLifetime": { + "title": "Masa hidup frame", + "description": "Setiap kamera memiliki {{frames}} slot frame di memori bersama. Pada laju frame kamera tercepat, setiap frame tersedia selama sekitar {{lifetime}} dtk sebelum ditimpa." + } + }, + "cameraStorage": { + "title": "Penyimpanan Kamera", + "camera": "Kamera", + "unusedStorageInformation": "Informasi Penyimpanan Tidak Terpakai", + "storageUsed": "Penyimpanan", + "percentageOfTotalUsed": "Persentase dari Total", + "bandwidth": "Bandwidth", + "unused": { + "title": "Tidak Terpakai", + "tips": "Nilai ini mungkin tidak secara akurat merepresentasikan ruang kosong yang tersedia untuk Frigate jika Anda memiliki file lain yang disimpan di drive selain rekaman Frigate. Frigate tidak melacak penggunaan penyimpanan di luar rekamannya." + } + } + }, + "cameras": { + "title": "Kamera", + "overview": "Ringkasan", + "info": { + "aspectRatio": "rasio aspek", + "cameraProbeInfo": "Info Probe Kamera {{camera}}", + "streamDataFromFFPROBE": "Data stream diperoleh dengan ffprobe.", + "fetching": "Mengambil Data Kamera", + "stream": "Stream {{idx}}", + "video": "Video:", + "codec": "Codec:", + "resolution": "Resolusi:", + "fps": "FPS:", + "unknown": "Tidak diketahui", + "audio": "Audio:", + "error": "Kesalahan: {{error}}", + "tips": { + "title": "Info Probe Kamera" + } + }, + "framesAndDetections": "Frame / Deteksi", + "noCameras": { + "title": "Tidak Ada Kamera Ditemukan" + }, + "label": { + "camera": "kamera", + "detect": "deteksi", + "skipped": "dilewati", + "ffmpeg": "FFmpeg", + "capture": "penangkapan", + "overallFramesPerSecond": "jumlah frame per detik keseluruhan", + "overallDetectionsPerSecond": "jumlah deteksi per detik keseluruhan", + "overallSkippedDetectionsPerSecond": "jumlah deteksi yang dilewati per detik keseluruhan", + "cameraFfmpeg": "FFmpeg {{camName}}", + "cameraCapture": "penangkapan {{camName}}", + "cameraDetect": "deteksi {{camName}}", + "cameraGpu": "GPU {{camName}}", + "cameraFramesPerSecond": "frame per detik {{camName}}", + "cameraDetectionsPerSecond": "deteksi per detik {{camName}}", + "cameraSkippedDetectionsPerSecond": "deteksi yang dilewati per detik {{camName}}" + }, + "connectionQuality": { + "title": "Kualitas Koneksi", + "excellent": "Sangat Baik", + "fair": "Cukup", + "poor": "Buruk", + "unusable": "Tidak Dapat Digunakan", + "fps": "FPS", + "expectedFps": "FPS yang Diharapkan", + "reconnectsLastHour": "Penyambungan ulang (1 jam terakhir)", + "stallsLastHour": "Macet (1 jam terakhir)" + }, + "toast": { + "success": { + "copyToClipboard": "Data probe berhasil disalin ke papan klip." + }, + "error": { + "unableToProbeCamera": "Tidak dapat memeriksa kamera: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Terakhir diperbarui: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} memiliki penggunaan CPU FFmpeg yang tinggi ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} memiliki penggunaan CPU deteksi yang tinggi ({{detectAvg}}%)", + "healthy": "Sistem sehat", + "reindexingEmbeddings": "Mengindeks ulang embedding ({{processed}}% selesai)", + "cameraIsOffline": "{{camera}} sedang offline", + "detectIsSlow": "{{detect}} lambat ({{speed}} md)", + "detectIsVerySlow": "{{detect}} sangat lambat ({{speed}} md)", + "shmTooLow": "Alokasi /dev/shm ({{total}} MB) harus ditingkatkan menjadi setidaknya {{min}} MB.", + "debugReplayActive": "Sesi pemutaran ulang debug sedang aktif" + }, + "enrichments": { + "title": "Enrichment", + "infPerSecond": "Inferensi Per Detik", + "averageInf": "Waktu Inferensi Rata-rata", + "embeddings": { + "image_embedding": "Embedding Gambar", + "text_embedding": "Embedding Teks", + "face_recognition": "Pengenalan Wajah", + "plate_recognition": "Pengenalan Plat", + "image_embedding_speed": "Kecepatan Embedding Gambar", + "face_embedding_speed": "Kecepatan Embedding Wajah", + "face_recognition_speed": "Kecepatan Pengenalan Wajah", + "plate_recognition_speed": "Kecepatan Pengenalan Plat", + "text_embedding_speed": "Kecepatan Embedding Teks", + "yolov9_plate_detection_speed": "Kecepatan Deteksi Plat YOLOv9", + "yolov9_plate_detection": "Deteksi Plat YOLOv9", + "review_description": "Deskripsi Tinjauan", + "review_description_speed": "Kecepatan Deskripsi Tinjauan", + "review_description_events_per_second": "Deskripsi Tinjauan", + "object_description": "Deskripsi Objek", + "object_description_speed": "Kecepatan Deskripsi Objek", + "object_description_events_per_second": "Deskripsi Objek", + "classification": "Klasifikasi {{name}}", + "classification_speed": "Kecepatan Klasifikasi {{name}}", + "classification_events_per_second": "Peristiwa Klasifikasi {{name}} Per Detik" + } + } +} diff --git a/web/public/locales/is/audio.json b/web/public/locales/is/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/audio.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/common.json b/web/public/locales/is/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/common.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/components/auth.json b/web/public/locales/is/components/auth.json new file mode 100644 index 0000000..077e148 --- /dev/null +++ b/web/public/locales/is/components/auth.json @@ -0,0 +1,5 @@ +{ + "form": { + "user": "Notandanafn" + } +} diff --git a/web/public/locales/is/components/camera.json b/web/public/locales/is/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/components/dialog.json b/web/public/locales/is/components/dialog.json new file mode 100644 index 0000000..d6a23f5 --- /dev/null +++ b/web/public/locales/is/components/dialog.json @@ -0,0 +1,5 @@ +{ + "restart": { + "title": "Ert þú viss um að þú viljir endurræsa Frigate?" + } +} diff --git a/web/public/locales/is/components/filter.json b/web/public/locales/is/components/filter.json new file mode 100644 index 0000000..3066802 --- /dev/null +++ b/web/public/locales/is/components/filter.json @@ -0,0 +1,3 @@ +{ + "filter": "Sía" +} diff --git a/web/public/locales/is/components/icons.json b/web/public/locales/is/components/icons.json new file mode 100644 index 0000000..1ff5ba9 --- /dev/null +++ b/web/public/locales/is/components/icons.json @@ -0,0 +1,5 @@ +{ + "iconPicker": { + "selectIcon": "Veldu tákn" + } +} diff --git a/web/public/locales/is/components/input.json b/web/public/locales/is/components/input.json new file mode 100644 index 0000000..392bb43 --- /dev/null +++ b/web/public/locales/is/components/input.json @@ -0,0 +1,7 @@ +{ + "button": { + "downloadVideo": { + "label": "Hala niður myndbandi" + } + } +} diff --git a/web/public/locales/is/components/player.json b/web/public/locales/is/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/components/player.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/config/cameras.json b/web/public/locales/is/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/config/global.json b/web/public/locales/is/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/config/groups.json b/web/public/locales/is/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/config/validation.json b/web/public/locales/is/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/objects.json b/web/public/locales/is/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/objects.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/chat.json b/web/public/locales/is/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/classificationModel.json b/web/public/locales/is/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/configEditor.json b/web/public/locales/is/views/configEditor.json new file mode 100644 index 0000000..14f2f84 --- /dev/null +++ b/web/public/locales/is/views/configEditor.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "Stillingastjórn - Frigate" +} diff --git a/web/public/locales/is/views/events.json b/web/public/locales/is/views/events.json new file mode 100644 index 0000000..6d1ed90 --- /dev/null +++ b/web/public/locales/is/views/events.json @@ -0,0 +1,3 @@ +{ + "alerts": "Atvik" +} diff --git a/web/public/locales/is/views/explore.json b/web/public/locales/is/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/exports.json b/web/public/locales/is/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/faceLibrary.json b/web/public/locales/is/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/live.json b/web/public/locales/is/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/motionSearch.json b/web/public/locales/is/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/recording.json b/web/public/locales/is/views/recording.json new file mode 100644 index 0000000..3066802 --- /dev/null +++ b/web/public/locales/is/views/recording.json @@ -0,0 +1,3 @@ +{ + "filter": "Sía" +} diff --git a/web/public/locales/is/views/replay.json b/web/public/locales/is/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/search.json b/web/public/locales/is/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/settings.json b/web/public/locales/is/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/is/views/system.json b/web/public/locales/is/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/is/views/system.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/it/audio.json b/web/public/locales/it/audio.json new file mode 100644 index 0000000..17ca12c --- /dev/null +++ b/web/public/locales/it/audio.json @@ -0,0 +1,503 @@ +{ + "oink": "Grugnito", + "engine": "Motore", + "keyboard": "Tastiera", + "snake": "Serpente", + "mantra": "Mantra", + "radio": "Radio", + "goat": "Capra", + "bird": "Uccello", + "clock": "Orologio", + "gears": "Ingranaggi", + "fire": "Fuoco", + "hammer": "Martello", + "gunshot": "Sparo", + "bell": "Campana", + "explosion": "Esplosione", + "silence": "Silenzio", + "ambulance": "Ambulanza", + "thunder": "Tuono", + "purr": "Fusa", + "guitar": "Chitarra", + "bass_guitar": "Basso elettrico", + "eruption": "Eruzione", + "sound_effect": "Effetto sonoro", + "bicycle": "Bicicletta", + "vehicle": "Veicolo", + "flute": "Flauto", + "harp": "Arpa", + "steam": "Vapore", + "sailboat": "Barca a vela", + "helicopter": "Elicottero", + "coin": "Moneta", + "scissors": "Forbici", + "electric_shaver": "Rasoio elettrico", + "mechanisms": "Meccanismi", + "printer": "Stampante", + "television": "Televisione", + "environmental_noise": "Rumore ambientale", + "smoke_detector": "Rilevatore di fumo", + "clarinet": "Clarinetto", + "horse": "Cavallo", + "electric_guitar": "Chitarra elettrica", + "meow": "Miao", + "ringtone": "Suoneria", + "boat": "Barca", + "sampler": "Campionatore", + "song": "Canzone", + "clapping": "Applausi", + "cat": "Gatto", + "chicken": "Pollo", + "acoustic_guitar": "Chitarra acustica", + "speech": "Parlato", + "babbling": "Balbettio", + "motorcycle": "Motociclo", + "yell": "Urlo", + "whoop": "Ululato", + "car": "Automobile", + "bellow": "Ruggito", + "whispering": "Bisbiglio", + "snicker": "Risatina", + "crying": "Pianto", + "sigh": "Sospiro", + "singing": "Canto", + "choir": "Coro", + "yodeling": "Gorgheggio", + "chant": "Cantilena", + "child_singing": "Canto di bambino", + "laughter": "Risata", + "synthetic_singing": "Canto sintetico", + "rapping": "Rap", + "humming": "Mormorio", + "groan": "Gemito", + "grunt": "Grugnito", + "whistling": "Fischio", + "breathing": "Respiro", + "wheeze": "Respiro affannoso", + "snoring": "Russare", + "gasp": "Respiro profondo", + "cough": "Tosse", + "snort": "Sbuffare", + "pant": "Ansimare", + "throat_clearing": "Schiarimento della gola", + "sneeze": "Starnuto", + "sniff": "Sniffare", + "footsteps": "Passi", + "run": "Corsa", + "chewing": "Masticare", + "gargling": "Gargarismo", + "stomach_rumble": "Brontolio di stomaco", + "burping": "Rutto", + "fart": "Peto", + "hiccup": "Singhiozzo", + "finger_snapping": "Schiocco di dita", + "hands": "Mani", + "heartbeat": "Battito cardiaco", + "applause": "Applauso", + "heart_murmur": "Soffio cardiaco", + "cheering": "Tifo", + "animal": "Animale", + "pets": "Animali domestici", + "dog": "Cane", + "bark": "Abbaio", + "howl": "Ululato", + "yip": "Guaito", + "crowd": "Folla", + "livestock": "Bestiame", + "cowbell": "Campanaccio", + "turkey": "Tacchino", + "children_playing": "Bambini che giocano", + "chatter": "Chiacchiere", + "bow_wow": "Bau Bau", + "growling": "Ringhio", + "whimper_dog": "Lamento di cane", + "hiss": "Sibilo", + "caterwaul": "Miagolio", + "pig": "Maiale", + "sheep": "Pecora", + "fowl": "Pollame", + "bleat": "Belato", + "cock_a_doodle_doo": "Chicchirichì", + "gobble": "Glu Glu", + "cluck": "Chioccia", + "duck": "Anatra", + "quack": "Qua Qua", + "wild_animals": "Animali selvatici", + "goose": "Oca", + "honk": "Starnazzare", + "roar": "Ruggito", + "roaring_cats": "Ruggito felino", + "chirp": "Cinguettio", + "pigeon": "Piccione", + "crow": "Corvo", + "coo": "Tubare", + "owl": "Gufo", + "caw": "Gracchio", + "dogs": "Cani", + "hoot": "Bubbolio", + "flapping_wings": "Battito d'ali", + "mouse": "Mouse", + "rats": "Ratti", + "insect": "Insetto", + "cricket": "Grillo", + "mosquito": "Zanzara", + "buzz": "Ronzio", + "fly": "Mosca", + "frog": "Rana", + "croak": "Gracidio", + "rattle": "Sonaglio", + "music": "Musica", + "musical_instrument": "Strumento musicale", + "whale_vocalization": "Canto della balena", + "plucked_string_instrument": "Strumento a corda pizzicata", + "tapping": "Tapping (tecnica di chitarra)", + "steel_guitar": "Chitarra d'acciaio", + "strum": "Strimpellio", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolino", + "zither": "Cetra da tavolo", + "ukulele": "Ukulele", + "piano": "Pianoforte", + "electric_piano": "Pianoforte elettrico", + "organ": "Organo", + "electronic_organ": "Organo elettronico", + "synthesizer": "Sintetizzatore", + "hammond_organ": "Organo Hammond", + "drum_kit": "Batteria", + "drum": "Tamburo", + "drum_machine": "Drum Machine", + "tabla": "Tabla", + "cymbal": "Piatto", + "hi_hat": "Charleston", + "wood_block": "Blocco di legno", + "tambourine": "Tamburello", + "maraca": "Maracas", + "gong": "Gong", + "tubular_bells": "Campane tubolari", + "mallet_percussion": "Mazzuola", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibrafono", + "steelpan": "Tamburo d'acciaio", + "orchestra": "Orchestra", + "brass_instrument": "Ottoni", + "trumpet": "Tromba", + "french_horn": "Corno francese", + "trombone": "Trombone", + "bowed_string_instrument": "Strumento ad arco", + "violin": "Violino", + "string_section": "Sezione d'archi", + "cello": "Violoncello", + "double_bass": "Contrabbasso", + "pizzicato": "Pizzicato", + "wind_instrument": "Strumento a fiato", + "saxophone": "Sassofono", + "church_bell": "Campana della chiesa", + "jingle_bell": "Campanellino", + "bicycle_bell": "Campanello della bici", + "tuning_fork": "Diapason", + "chime": "Carillon", + "wind_chime": "Campane a vento", + "harmonica": "Armonica", + "accordion": "Fisarmonica", + "bagpipes": "Cornamusa", + "didgeridoo": "Didgeridoo", + "pop_music": "Musica pop", + "theremin": "Theremin", + "singing_bowl": "Campana tibetana", + "rock_music": "Musica rock", + "heavy_metal": "Heavy Metal", + "beatboxing": "Beatboxing", + "hip_hop_music": "Musica hip hop", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Rock psichedelico", + "bus": "Autobus", + "train": "Treno", + "percussion": "Percussioni", + "harpsichord": "Clavicembalo", + "snare_drum": "Rullante", + "rimshot": "Colpi nei bordi del tamburo", + "drum_roll": "Rullo di tamburi", + "bass_drum": "Grancassa", + "timpani": "Timpano", + "progressive_rock": "Rock progressivo", + "scratching": "Graffio", + "rhythm_and_blues": "Rhythm and Blues", + "soul_music": "Musica soul", + "reggae": "Reggae", + "country": "Country", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Musica folk", + "middle_eastern_music": "Musica mediorientale", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Musica classica", + "opera": "Opera", + "electronic_music": "Musica elettronica", + "house_music": "Musica house", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Elettronica", + "camera": "Telecamera", + "shuffle": "Trascinare i piedi", + "biting": "Mordere", + "clip_clop": "Trotto di cavallo", + "neigh": "Nitrire", + "cattle": "Bestiame", + "moo": "Muggire", + "squawk": "Strillo", + "patter": "Ticchettio", + "swing_music": "Musica swing", + "electronic_dance_music": "Musica dance elettronica", + "ambient_music": "Musica ambientale", + "trance_music": "Musica trance", + "blues": "Blues", + "music_for_children": "Musica per bambini", + "new-age_music": "Musica new age", + "vocal_music": "Musica vocale", + "a_capella": "A capella", + "music_of_latin_america": "Musica latinoamericana", + "music_of_africa": "Musica africana", + "afrobeat": "Afrobeat", + "christian_music": "Musica cristiana", + "gospel_music": "Musica gospel", + "music_of_asia": "Musica asiatica", + "carnatic_music": "Musica carnatica", + "music_of_bollywood": "Musica di Bollywood", + "ska": "Ska", + "traditional_music": "Musica tradizionale", + "independent_music": "Musica indipendente", + "background_music": "Musica di sottofondo", + "theme_music": "Musica a tema", + "jingle": "Motivetto", + "soundtrack_music": "Colonna sonora musicale", + "lullaby": "Ninna nanna", + "video_game_music": "Musica per videogiochi", + "christmas_music": "Musica natalizia", + "dance_music": "Musica da ballo", + "wedding_music": "Musica per matrimoni", + "happy_music": "Musica allegra", + "sad_music": "Musica triste", + "tender_music": "Musica dolce", + "salsa_music": "Musica salsa", + "flamenco": "Flamenco", + "angry_music": "Musica arrabbiata", + "scary_music": "Musica spaventosa", + "wind": "Vento", + "rustling_leaves": "Fruscio di foglie", + "wind_noise": "Rumore del vento", + "thunderstorm": "Temporale", + "water": "Acqua", + "rain": "Pioggia", + "raindrop": "Goccia di pioggia", + "stream": "Ruscello", + "waterfall": "Cascata", + "ocean": "Oceano", + "waves": "Onde", + "rain_on_surface": "Pioggia in superficie", + "gurgling": "Gorgoglio", + "crackle": "Crepitio", + "rowboat": "Barca a remi", + "motorboat": "Motoscafo", + "ship": "Nave", + "toot": "Fischio", + "motor_vehicle": "Veicolo a motore", + "car_alarm": "Allarme auto", + "power_windows": "Alzacristalli elettrici", + "skidding": "Sbandare", + "tire_squeal": "Stridio di pneumatici", + "car_passing_by": "Auto che passa", + "race_car": "Auto da corsa", + "air_brake": "Freno ad aria compressa", + "air_horn": "Clacson ad aria", + "reversing_beeps": "Bip di retromarcia", + "ice_cream_truck": "Camioncino dei gelati", + "emergency_vehicle": "Veicolo di emergenza", + "police_car": "Auto della polizia", + "fire_engine": "Camion dei pompieri", + "traffic_noise": "Rumore del traffico", + "rail_transport": "Trasporto ferroviario", + "train_whistle": "Fischio del treno", + "train_horn": "Clacson del treno", + "railroad_car": "Vagone ferroviario", + "train_wheels_squealing": "Ruote del treno che stridono", + "subway": "Metropolitana", + "aircraft": "Aeromobile", + "aircraft_engine": "Motore aeronautico", + "jet_engine": "Motore a reazione", + "propeller": "Elica", + "fixed-wing_aircraft": "Aeromobile ad ala fissa", + "skateboard": "Skateboard", + "light_engine": "Motore leggero", + "dental_drill's_drill": "Trapano dentale", + "lawn_mower": "Taglia erba", + "exciting_music": "Musica emozionante", + "truck": "Camion", + "firecracker": "Petardo", + "chainsaw": "Motosega", + "medium_engine": "Motore medio", + "heavy_engine": "Motore pesante", + "engine_knocking": "Battito del motore", + "engine_starting": "Avviamento del motore", + "idling": "Al minimo", + "accelerating": "Accelerando", + "door": "Porta", + "doorbell": "Campanello", + "ding-dong": "Ding Dong", + "sliding_door": "Porta scorrevole", + "slam": "Sbattere", + "knock": "Bussare", + "tap": "Tocco", + "squeak": "Squittio", + "cupboard_open_or_close": "Apertura o chiusura armadio", + "drawer_open_or_close": "Apertura o chiusura cassetto", + "dishes": "Piatti", + "cutlery": "Posate", + "chopping": "Tritare", + "frying": "Frittura", + "microwave_oven": "Forno a microonde", + "blender": "Miscelatore", + "water_tap": "Rubinetto dell'acqua", + "sink": "Lavello", + "bathtub": "Vasca", + "hair_dryer": "Asciugacapelli", + "toilet_flush": "Scarico del water", + "toothbrush": "Spazzolino da denti", + "electric_toothbrush": "Spazzolino elettrico", + "vacuum_cleaner": "Aspirapolvere", + "zipper": "Cerniera", + "keys_jangling": "Chiavi che tintinnano", + "shuffling_cards": "Mescolare le carte", + "typing": "Digitazione", + "typewriter": "Macchina da scrivere", + "computer_keyboard": "Tastiera del computer", + "writing": "Scrivere", + "alarm": "Allarme", + "telephone": "Telefono", + "telephone_bell_ringing": "Telefono che squilla", + "telephone_dialing": "Composizione telefonica", + "dial_tone": "Tono di linea", + "busy_signal": "Segnale di occupato", + "alarm_clock": "Sveglia", + "siren": "Sirena", + "civil_defense_siren": "Sirena della Protezione Civile", + "buzzer": "Cicalino", + "fire_alarm": "Allarme antincendio", + "foghorn": "Corno da nebbia", + "whistle": "Fischio", + "steam_whistle": "Fischio a vapore", + "ratchet": "Cricchetto", + "tick": "Tic tac", + "tick-tock": "Tic-Tac", + "pulleys": "Pulegge", + "sewing_machine": "Macchina da cucire", + "mechanical_fan": "Ventilatore meccanico", + "air_conditioning": "Aria condizionata", + "cash_register": "Registratore di cassa", + "single-lens_reflex_camera": "Telecamera reflex a obiettivo singolo", + "tools": "Utensili", + "jackhammer": "Martello pneumatico", + "sawing": "Segare", + "filing": "Limare", + "sanding": "Levigatura", + "power_tool": "Utensile elettrico", + "drill": "Trapano", + "fusillade": "Fucilazione", + "machine_gun": "Mitragliatrice", + "artillery_fire": "Fuoco di artiglieria", + "cap_gun": "Pistola a fumogeni", + "fireworks": "Fuochi d'artificio", + "burst": "Esplosione", + "boom": "Scoppio", + "wood": "Legno", + "chop": "Taglio", + "splinter": "Scheggia", + "crack": "Crepa", + "glass": "Vetro", + "chink": "Fessura", + "shatter": "Frantumare", + "static": "Statico", + "white_noise": "Rumore bianco", + "pink_noise": "Rumore rosa", + "field_recording": "Registrazione sul campo", + "scream": "Grido", + "vibration": "Vibrazione", + "sodeling": "Zollatura", + "chird": "Accordo", + "change_ringing": "Cambia suoneria", + "shofar": "Shofar", + "liquid": "Liquido", + "splash": "Schizzo", + "slosh": "Sciabordio", + "squish": "Schiacciare", + "drip": "Gocciolare", + "pour": "Versare", + "trickle": "Gocciolare", + "gush": "Sgorgare", + "fill": "Riempire", + "spray": "Spruzzare", + "pump": "Pompare", + "stir": "Mescolare", + "boiling": "Ebollizione", + "sonar": "Sonar", + "arrow": "Freccia", + "whoosh": "Sibilo", + "thump": "Tonfo", + "thunk": "Tonfo", + "electronic_tuner": "Accordatore elettronico", + "effects_unit": "Unità degli effetti", + "chorus_effect": "Effetto coro", + "basketball_bounce": "Rimbalzo di basket", + "bang": "Botto", + "slap": "Schiaffo", + "whack": "Colpo", + "smash": "Distruggere", + "breaking": "Rottura", + "bouncing": "Rimbalzo", + "whip": "Frusta", + "flap": "Patta", + "scratch": "Graffio", + "scrape": "Graffio", + "rub": "Strofinio", + "roll": "Rotolio", + "crushing": "Schiacciamento", + "crumpling": "Accartocciamento", + "tearing": "Strappo", + "beep": "Segnale acustico", + "ping": "Segnale", + "ding": "Bip", + "clang": "Fragore", + "squeal": "Strillo", + "creak": "Scricchiolio", + "rustle": "Fruscio", + "whir": "Ronzio", + "clatter": "Rumore", + "sizzle": "Sfrigolio", + "clicking": "Cliccando", + "clickety_clack": "Clic-clac", + "rumble": "Rombo", + "plop": "Tonfo", + "hum": "Ronzio", + "zing": "Brio", + "boing": "Balzo", + "crunch": "Scricchiolio", + "sine_wave": "Onda sinusoidale", + "harmonic": "Armonica", + "chirp_tone": "Tono di cinguettio", + "pulse": "Impulso", + "inside": "Dentro", + "outside": "Fuori", + "reverberation": "Riverbero", + "echo": "Eco", + "noise": "Rumore", + "mains_hum": "Ronzio di rete", + "distortion": "Distorsione", + "sidetone": "Effetto laterale", + "cacophony": "Cacofonia", + "throbbing": "Palpitante" +} diff --git a/web/public/locales/it/common.json b/web/public/locales/it/common.json new file mode 100644 index 0000000..5394212 --- /dev/null +++ b/web/public/locales/it/common.json @@ -0,0 +1,341 @@ +{ + "time": { + "last14": "Ultimi 14 giorni", + "pm": "pm", + "last30": "Ultimi 30 giorni", + "untilRestart": "Fino al riavvio", + "yesterday": "Ieri", + "am": "am", + "untilForTime": "Fino alle {{time}}", + "minute_one": "{{time}} minuto", + "minute_many": "{{time}} minuti", + "minute_other": "{{time}} minuti", + "5minutes": "5 minuti", + "24hours": "24 ore", + "second_one": "{{time}} secondo", + "second_many": "{{time}} secondi", + "second_other": "{{time}} secondi", + "untilForRestart": "Fino al riavvio di Frigate.", + "ago": "{{timeAgo}} fa", + "justNow": "Adesso", + "today": "Oggi", + "last7": "Ultimi 7 giorni", + "thisWeek": "Questa settimana", + "lastWeek": "Settimana scorsa", + "thisMonth": "Questo mese", + "lastMonth": "Mese scorso", + "10minutes": "10 minuti", + "30minutes": "30 minuti", + "1hour": "1 ora", + "12hours": "12 ore", + "year_one": "{{time}} anno", + "year_many": "{{time}} anni", + "year_other": "{{time}} anni", + "month_one": "{{time}} mese", + "month_many": "{{time}} mesi", + "month_other": "{{time}} mesi", + "day_one": "{{time}} giorno", + "day_many": "{{time}} giorni", + "day_other": "{{time}} giorni", + "hour_one": "{{time}} ora", + "hour_many": "{{time}} ore", + "hour_other": "{{time}} ore", + "yr": "{{time}}anno", + "mo": "{{time}}mese", + "d": "{{time}}giorno", + "h": "{{time}}ora", + "m": "{{time}}min", + "s": "{{time}}sec", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampExcludeSeconds": { + "12hour": "%b %-d, %I:%M %p", + "24hour": "%b %-d, %H:%M" + }, + "formattedTimestampWithYear": { + "12hour": "%b %-d %Y, %I:%M %p", + "24hour": "%b %-d %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "24hour": "MM-dd-yy-HH-mm-ss", + "12hour": "MM-dd-yy-h-mm-ss-a" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "inProgress": "In corso", + "invalidStartTime": "Ora di inizio non valida", + "invalidEndTime": "Ora di fine non valida", + "never": "Mai" + }, + "button": { + "cancel": "Annulla", + "yes": "Sì", + "unselect": "Deseleziona", + "disabled": "Disabilitato", + "fullscreen": "A schermo intero", + "save": "Salva", + "no": "No", + "edit": "Modifica", + "export": "Esporta", + "disable": "Disabilita", + "apply": "Applica", + "reset": "Reimposta", + "done": "Fatto", + "enabled": "Abilitato", + "enable": "Abilita", + "saving": "Salvataggio…", + "copy": "Copia", + "history": "Storico", + "exitFullscreen": "Esci da schermo intero", + "on": "ACCESO", + "copyCoordinates": "Copia coordinate", + "download": "Scarica", + "info": "Informazioni", + "suspended": "Sospeso", + "unsuspended": "Riattiva", + "play": "Riproduci", + "deleteNow": "Elimina ora", + "next": "Successivo", + "off": "SPENTO", + "delete": "Elimina", + "close": "Chiudi", + "back": "Indietro", + "pictureInPicture": "Immagine nell'immagine", + "twoWayTalk": "Comunicazione bidirezionale", + "cameraAudio": "Audio della telecamera", + "continue": "Continua", + "add": "Aggiungi", + "undo": "Annulla", + "copiedToClipboard": "Copiato negli appunti", + "modified": "Modificato", + "overridden": "Sovrascritto", + "resetToGlobal": "Ripristina impostazioni globali", + "resetToDefault": "Ripristina impostazioni predefinite", + "saveAll": "Salva tutto", + "savingAll": "Salvataggio di tutto…", + "undoAll": "Annulla tutto", + "applying": "Applica…", + "retry": "Riprova" + }, + "unit": { + "speed": { + "kph": "km/h", + "mph": "miglia/h" + }, + "length": { + "feet": "piedi", + "meters": "metri" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/ora", + "mbph": "MB/ora", + "gbph": "GB/ora" + } + }, + "label": { + "back": "Vai indietro", + "hide": "Nascondi {{item}}", + "show": "Mostra {{item}}", + "ID": "ID", + "none": "Nessuna", + "all": "Tutte", + "other": "Altro" + }, + "menu": { + "configuration": "Configurazione", + "languages": "Lingue", + "appearance": "Aspetto", + "systemMetrics": "Metriche di sistema", + "systemLogs": "Registri di sistema", + "settings": "Impostazioni", + "configurationEditor": "Editor di configurazione", + "language": { + "en": "English (Inglese)", + "zhCN": "简体中文 (Cinese semplificato)", + "withSystem": { + "label": "Usa la lingua di sistema" + }, + "es": "Español (Spagnolo)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Francese)", + "ar": "العربية (Arabo)", + "pt": "Português (Portoghese)", + "ru": "Русский (Russo)", + "tr": "Türkçe (Turco)", + "nl": "Nederlands (Olandese)", + "sv": "Svenska (Svedese)", + "cs": "Čeština (Ceco)", + "nb": "Norsk Bokmål (Norvegese)", + "ko": "한국어 (Coreano)", + "vi": "Tiếng Việt (Vietnamita)", + "fa": "فارسی (Persiano)", + "ro": "Română (Rumeno)", + "hu": "Magyar (Ungherese)", + "fi": "Suomi (Finlandese)", + "da": "Dansk (Danese)", + "el": "Ελληνικά (Greco)", + "sk": "Slovenčina (Slovacco)", + "ja": "日本語 (Giapponese)", + "uk": "Українська (Ucraino)", + "pl": "Polski (Polacco)", + "de": "Deutsch (Tedesco)", + "he": "עברית (Ebraico)", + "it": "Italiano (Italiano)", + "yue": "粵語 (Cantonese)", + "th": "ไทย (Tailandese)", + "ca": "Català (Catalano)", + "ptBR": "Português brasileiro (Portoghese brasiliano)", + "sr": "Српски (Serbo)", + "sl": "Slovenščina (Sloveno)", + "lt": "Lietuvių (Lituano)", + "bg": "Български (Bulgaro)", + "gl": "Galego (Galiziano)", + "id": "Bahasa Indonesia (Indonesiano)", + "ur": "اردو (Urdu)", + "hr": "Hrvatski (Croato)", + "bs": "Bosanski (Bosniaco)", + "zhHant": "繁體中文 (Cinese Tradizionale)" + }, + "darkMode": { + "label": "Modalità scura", + "light": "Chiara", + "dark": "Scura", + "withSystem": { + "label": "Usa le impostazioni di sistema per le modalità chiara/scura" + } + }, + "system": "Sistema", + "theme": { + "label": "Tema", + "blue": "Blu", + "contrast": "Alto contrasto", + "green": "Verde", + "default": "Predefinito", + "red": "Rosso", + "nord": "Nord", + "highcontrast": "Contrasto elevato" + }, + "live": { + "cameras": { + "title": "Telecamere", + "count_one": "{{count}} Telecamera", + "count_many": "{{count}} Telecamere", + "count_other": "{{count}} Telecamere" + }, + "title": "Dal vivo", + "allCameras": "Tutte le telecamere" + }, + "help": "Aiuto", + "documentation": { + "title": "Documentazione", + "label": "Documentazione di Frigate" + }, + "restart": "Riavvia Frigate", + "review": "Revisiona", + "explore": "Esplora", + "export": "Esporta", + "uiPlayground": "Interfaccia area prove", + "user": { + "account": "Account", + "current": "Utente attuale: {{user}}", + "anonymous": "anonimo", + "logout": "Esci", + "title": "Utente", + "setPassword": "Imposta password" + }, + "withSystem": "Sistema", + "faceLibrary": "Raccolta volti", + "classification": "Classificazione", + "chat": "Chat", + "profiles": "Profili", + "actions": "Azioni", + "features": "Caratteristiche" + }, + "pagination": { + "next": { + "title": "Successivo", + "label": "Vai alla pagina successiva" + }, + "previous": { + "label": "Vai alla pagina precedente", + "title": "Precedente" + }, + "label": "paginazione", + "more": "Altre pagine" + }, + "role": { + "title": "Ruolo", + "admin": "Amministratore", + "viewer": "Visualizzatore", + "desc": "Gli amministratori hanno accesso completo a tutte le funzionalità dell'interfaccia utente di Frigate. I visualizzatori possono visualizzare solo le telecamere, gli elementi di revisione e i filmati storici nell'interfaccia utente." + }, + "accessDenied": { + "desc": "Non hai i permessi per visualizzare questa pagina.", + "documentTitle": "Accesso vietato - Frigate", + "title": "Accesso vietato" + }, + "notFound": { + "desc": "Pagina non trovata", + "documentTitle": "Non trovato - Frigate", + "title": "404" + }, + "toast": { + "copyUrlToClipboard": "URL copiata negli appunti.", + "save": { + "error": { + "title": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}", + "noMessage": "Impossibile salvare le modifiche alla configurazione" + }, + "title": "Salva", + "success": "Modifiche alla configurazione salvate correttamente." + } + }, + "selectItem": "Seleziona {{item}}", + "readTheDocumentation": "Leggi la documentazione", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} e {{1}}", + "many": "{{items}}, e {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opzionale", + "internalID": "L'ID interno che Frigate utilizza nella configurazione e nel database" + }, + "no_items": "Nessun elemento", + "validation_errors": "Errori di convalida", + "credentialField": { + "savedPlaceholder": "Salvato — lascia vuoto per mantenere aggiornato" + } +} diff --git a/web/public/locales/it/components/auth.json b/web/public/locales/it/components/auth.json new file mode 100644 index 0000000..f743877 --- /dev/null +++ b/web/public/locales/it/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nome utente", + "password": "Password", + "login": "Accedi", + "errors": { + "usernameRequired": "Il nome utente è obbligatorio", + "passwordRequired": "La password è obbligatoria", + "rateLimit": "Superato il limite di tentativi. Riprova più tardi.", + "unknownError": "Errore sconosciuto. Controlla i registri.", + "webUnknownError": "Errore sconosciuto. Controlla i registri della console.", + "loginFailed": "Accesso non riuscito" + }, + "firstTimeLogin": "Stai cercando di accedere per la prima volta? Le credenziali sono scritte nei registri di Frigate." + } +} diff --git a/web/public/locales/it/components/camera.json b/web/public/locales/it/components/camera.json new file mode 100644 index 0000000..3188fea --- /dev/null +++ b/web/public/locales/it/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "Gruppo di telecamere", + "add": "Aggiungi gruppo di telecamere", + "delete": { + "label": "Elimina gruppo di telecamere", + "confirm": { + "title": "Conferma eliminazione", + "desc": "Sei sicuro di voler eliminare il gruppo di telecamere {{name}}?" + } + }, + "edit": "Modifica gruppo di telecamere", + "name": { + "label": "Nome", + "placeholder": "Inserisci un nome…", + "errorMessage": { + "exists": "Il nome scelto per il gruppo telecamere è già presente.", + "invalid": "Nome del gruppo di telecamere non valido.", + "mustLeastCharacters": "Il nome del gruppo di telecamere deve contenere almeno 2 caratteri.", + "nameMustNotPeriod": "Il nome del gruppo di telecamere non deve contenere punti." + } + }, + "camera": { + "setting": { + "label": "Impostazioni di trasmissione della telecamera", + "title": "Impostazioni di trasmissione di {{cameraName}}", + "audioIsAvailable": "L'audio è disponibile per questo flusso", + "streamMethod": { + "label": "Metodo di trasmissione", + "method": { + "smartStreaming": { + "label": "Trasmissione intelligente (consigliata)", + "desc": "La trasmissione intelligente aggiorna l'immagine della telecamera una volta al minuto quando non si verifica alcuna attività rilevabile, per risparmiare larghezza di banda e risorse. Quando viene rilevata un'attività, l'immagine passa automaticamente alla trasmissione dal vivo." + }, + "continuousStreaming": { + "label": "Trasmissione continua", + "desc": { + "warning": "La trasmissione continua può causare un elevato utilizzo di larghezza di banda e problemi di prestazioni. Da usare con cautela.", + "title": "L'immagine della telecamera sarà sempre trasmessa dal vivo quando è visibile sul cruscotto, anche se non viene rilevata alcuna attività." + } + }, + "noStreaming": { + "label": "Nessuna trasmissione", + "desc": "Le immagini delle telecamere verranno aggiornate solo una volta al minuto e non verrà effettuata alcuna trasmissione dal vivo." + } + }, + "placeholder": "Scegli un metodo di trasmissione" + }, + "compatibilityMode": { + "label": "Modalità di compatibilità", + "desc": "Abilita questa opzione solo se la trasmissione dal vivo della tua telecamera mostra artefatti cromatici e presenta una linea diagonale sul lato destro dell'immagine." + }, + "audio": { + "tips": { + "document": "Leggi la documentazione ", + "title": "L'audio deve essere trasmesso dalla tua telecamera e configurato in go2rtc per questo flusso." + } + }, + "audioIsUnavailable": "L'audio non è disponibile per questo flusso", + "desc": "Modifica le opzioni di trasmissione dal vivo per il cruscotto di questo gruppo di telecamere. Queste impostazioni sono specifiche del dispositivo/browser.", + "stream": "Flusso", + "placeholder": "Scegli un flusso" + }, + "birdseye": "Birdseye" + }, + "cameras": { + "desc": "Seleziona le telecamere per questo gruppo.", + "label": "Telecamere" + }, + "icon": "Icona", + "success": "Il gruppo di telecamere ({{name}}) è stato salvato.", + "showAll": "Mostra tutti i gruppi di telecamere", + "showLess": "Mostra meno", + "editGroups": "Modifica gruppi di telecamere" + }, + "debug": { + "options": { + "label": "Impostazioni", + "title": "Opzioni", + "showOptions": "Mostra opzioni", + "hideOptions": "Nascondi opzioni" + }, + "boundingBox": "Riquadro di delimitazione", + "timestamp": "Orario", + "zones": "Zone", + "mask": "Maschera", + "motion": "Movimento", + "regions": "Regioni", + "paths": "Percorsi" + } +} diff --git a/web/public/locales/it/components/dialog.json b/web/public/locales/it/components/dialog.json new file mode 100644 index 0000000..1cf05d4 --- /dev/null +++ b/web/public/locales/it/components/dialog.json @@ -0,0 +1,210 @@ +{ + "restart": { + "title": "Sei sicuro di voler riavviare Frigate?", + "button": "Riavvia", + "restarting": { + "title": "Frigate si sta riavviando", + "content": "Questa pagina si ricaricherà in {{countdown}} secondi.", + "button": "Forza ricarica ora" + }, + "description": "Questo fermerà brevemente Frigate mentre si riavvia." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Invia a Frigate+", + "desc": "Gli oggetti in posizioni che si desidera evitare non sono falsi positivi. Inviarli come falsi positivi confonderà il modello." + }, + "review": { + "false": { + "label": "Non confermare questa etichetta per Frigate Plus", + "false_one": "Questo non è un {{label}}", + "false_many": "Questi non sono un {{label}}", + "false_other": "Questi non sono un {{label}}" + }, + "true": { + "label": "Conferma questa etichetta per Frigate Plus", + "true_one": "Questo è un {{label}}", + "true_many": "Questi sono {{label}}", + "true_other": "Questi sono {{label}}" + }, + "state": { + "submitted": "Inviato" + }, + "question": { + "label": "Conferma questa etichetta per Frigate Plus", + "ask_a": "Questo oggetto è un {{label}}?", + "ask_an": "Questo oggetto è un {{label}}?", + "ask_full": "Questo oggetto è un {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Visualizza in Storico" + } + }, + "export": { + "time": { + "fromTimeline": "Seleziona dalla cronologia", + "custom": "Personalizzato", + "start": { + "title": "Ora di inizio", + "label": "Seleziona l'ora di inizio" + }, + "end": { + "title": "Ora di fine", + "label": "Seleziona l'ora di fine" + }, + "lastHour_one": "Ultima ora", + "lastHour_many": "Ultime {{count}} ore", + "lastHour_other": "Ultime {{count}} ore" + }, + "export": "Esporta", + "selectOrExport": "Seleziona o esporta", + "toast": { + "success": "Esportazione avviata correttamente. Visualizza il file nella pagina delle esportazioni.", + "error": { + "failed": "Impossibile mettere in coda l'esportazione: {{error}}", + "endTimeMustAfterStartTime": "L'ora di fine deve essere successiva all'ora di inizio", + "noVaildTimeSelected": "Nessun intervallo di tempo valido selezionato" + }, + "view": "Visualizzazione", + "queued": "Esportazione in coda. Visualizza lo stato di avanzamento nella pagina delle esportazioni.", + "batchSuccess_one": "Avviata 1 esportazione. Apertura del caso in corso.", + "batchSuccess_many": "Avviate {{count}} esportazioni. Apertura del caso in corso.", + "batchSuccess_other": "Avviate {{count}} esportazioni. Apertura del caso in corso.", + "batchPartial": "Avviate {{successful}} esportazioni su un totale di {{total}}. Telecamere non riuscite: {{failedCameras}}", + "batchFailed": "Impossibile avviare {{total}} esportazioni. Telecamere non riuscite: {{failedCameras}}", + "batchQueuedSuccess_one": "1 esportazione in coda. Apertura del caso in corso.", + "batchQueuedSuccess_many": "Sono in coda {{count}} esportazioni. Apertura del caso in corso.", + "batchQueuedSuccess_other": "Sono in coda {{count}} esportazioni. Apertura del caso in corso.", + "batchQueuedPartial": "In coda {{successful}} di {{total}} esportazioni. Telecamere non riuscite: {{failedCameras}}", + "batchQueueFailed": "Impossibile mettere in coda {{total}} esportazioni. Telecamere non riuscite: {{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "Salva esportazione", + "previewExport": "Anteprima esportazione", + "queueingExport": "Accodamento per l'esportazione...", + "useThisRange": "Utilizza questa intervallo" + }, + "select": "Seleziona", + "name": { + "placeholder": "Assegna un nome all'esportazione" + }, + "case": { + "label": "Caso", + "placeholder": "Seleziona un caso", + "newCaseOption": "Crea un nuovo caso", + "newCaseNamePlaceholder": "Nuovo nome del caso", + "newCaseDescriptionPlaceholder": "Descrizione del caso", + "nonAdminHelp": "Per queste esportazioni verrà creato un nuovo caso." + }, + "queueing": "Accodamento per l'esportazione...", + "tabs": { + "export": "Telecamera singola", + "multiCamera": "Multicamera" + }, + "multiCamera": { + "timeRange": "Intervallo di tempo", + "selectFromTimeline": "Seleziona dalla cronologia", + "cameraSelection": "Telecamere", + "cameraSelectionHelp": "Le telecamere con oggetti tracciati in questo intervallo di tempo sono preselezionate", + "checkingActivity": "Controllo dell'attività della telecamera...", + "noCameras": "Nessuna telecamera disponibile", + "detectionCount_one": "1 oggetto tracciato", + "detectionCount_many": "{{count}} oggetti tracciati", + "detectionCount_other": "{{count}} oggetti tracciati", + "nameLabel": "Nome di esportazione", + "namePlaceholder": "Nome base facoltativo per queste esportazioni", + "queueingButton": "Accodamento delle esportazioni...", + "exportButton_one": "Esporta 1 telecamera", + "exportButton_many": "Esporta {{count}} telecamere", + "exportButton_other": "Esporta {{count}} telecamere" + }, + "multi": { + "title_one": "Esporta 1 revisione", + "title_many": "Esporta {{count}} revisioni", + "title_other": "Esporta {{count}} revisioni", + "description": "Esporta ogni revisione selezionata. Tutte le esportazioni saranno raggruppate in un unico caso.", + "descriptionNoCase": "Esporta ogni revisione selezionata.", + "caseNamePlaceholder": "Esporta revisione - {{date}}", + "exportButton_one": "Esporta 1 revisione", + "exportButton_many": "Esporta {{count}} revisioni", + "exportButton_other": "Esporta {{count}} revisioni", + "exportingButton": "Esportazione...", + "toast": { + "started_one": "Avviata 1 esportazione. Apertura del caso in corso.", + "started_many": "Avviate {{count}} esportazioni. Apertura del caso in corso.", + "started_other": "Avviate {{count}} esportazioni. Apertura del caso in corso.", + "startedNoCase_one": "Avviata 1 esportazione.", + "startedNoCase_many": "Avviate {{count}} esportazioni.", + "startedNoCase_other": "Avviate {{count}} esportazioni.", + "partial": "Avviate {{successful}} esportazioni su un totale di {{total}}. Fallite: {{failedItems}}", + "failed": "Impossibile avviare {{total}} esportazioni. Errori: {{failedItems}}" + } + } + }, + "streaming": { + "label": "Trasmissione", + "showStats": { + "label": "Mostra statistiche di trasmissione", + "desc": "Abilita questa opzione per visualizzare le statistiche della trasmissione come sovrapposizione sul flusso della telecamera." + }, + "debugView": "Vista correzioni", + "restreaming": { + "disabled": "La ritrasmissione non è abilitata per questa telecamera.", + "desc": { + "title": "Imposta go2rtc per opzioni aggiuntive di visualizzazione dal vivo e audio per questa telecamera.", + "readTheDocumentation": "Leggi la documentazione" + } + } + }, + "search": { + "saveSearch": { + "label": "Salva ricerca", + "overwrite": "{{searchName}} esiste già. Il salvataggio sovrascriverà il valore esistente.", + "desc": "Specifica un nome per questa ricerca salvata.", + "button": { + "save": { + "label": "Salva questa ricerca" + } + }, + "placeholder": "Inserisci un nome per la tua ricerca", + "success": "La ricerca ({{searchName}}) è stata salvata." + } + }, + "recording": { + "button": { + "export": "Esporta", + "markAsReviewed": "Segna come visto", + "deleteNow": "Elimina ora", + "markAsUnreviewed": "Segna come non visto" + }, + "confirmDelete": { + "desc": { + "selected": "Vuoi davvero eliminare tutti i video registrati associati a questo elemento di visto?

    Tieni premuto il tasto Maiusc per ignorare questa finestra di dialogo in futuro." + }, + "title": "Conferma eliminazione", + "toast": { + "success": "Il filmato associato agli elementi di recensione selezionati è stato eliminato correttamente.", + "error": "Impossibile eliminare: {{error}}" + } + }, + "shareTimestamp": { + "label": "Condividi orario", + "title": "Condividi orario", + "description": "Condividi un URL con l'orario della posizione attuale del lettore oppure scegli un orario personalizzato. Tieni presente che questo URL non è pubblico ed è accessibile solo agli utenti che hanno accesso a Frigate e a questa telecamera.", + "custom": "Orario personalizzato", + "button": "URL dell'orario di condivisione", + "shareTitle": "Orario revisione Frigate: {{camera}}" + } + }, + "imagePicker": { + "selectImage": "Seleziona la miniatura di un oggetto tracciato", + "search": { + "placeholder": "Cerca per etichetta o sottoetichetta..." + }, + "noImages": "Nessuna miniatura trovata per questa telecamera", + "unknownLabel": "Immagine di attivazione salvata" + } +} diff --git a/web/public/locales/it/components/filter.json b/web/public/locales/it/components/filter.json new file mode 100644 index 0000000..ac5eaeb --- /dev/null +++ b/web/public/locales/it/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filtro", + "labels": { + "label": "Etichette", + "all": { + "title": "Tutte le etichette", + "short": "Etichette" + }, + "count_one": "{{count}} Etichetta", + "count_other": "{{count}} Etichette" + }, + "more": "Altri filtri", + "zones": { + "label": "Zone", + "all": { + "title": "Tutte le zone", + "short": "Zone" + } + }, + "dates": { + "all": { + "title": "Tutte le date", + "short": "Date" + }, + "selectPreset": "Seleziona una preimpostazione…" + }, + "reset": { + "label": "Ripristina i filtri ai valori predefiniti" + }, + "features": { + "submittedToFrigatePlus": { + "label": "Inviato a Frigate+", + "tips": "Devi prima filtrare gli oggetti tracciati che hanno un'istantanea.

    Gli oggetti tracciati senza istantanee non possono essere inviati a Frigate+." + }, + "hasSnapshot": "Contiene una istantanea", + "hasVideoClip": "Contiene un filmato video", + "label": "Caratteristiche" + }, + "sort": { + "dateAsc": "Data (crescente)", + "scoreAsc": "Punteggio dell'oggetto (crescente)", + "dateDesc": "Data (discendente)", + "speedAsc": "Velocità stimata (crescente)", + "speedDesc": "Velocità stimata (discendente)", + "relevance": "Rilevanza", + "label": "Ordina", + "scoreDesc": "Punteggio dell'oggetto (discrescente)" + }, + "explore": { + "settings": { + "title": "Impostazioni", + "gridColumns": { + "desc": "Seleziona il numero di colonne nella vista griglia.", + "title": "Colonne della griglia" + }, + "defaultView": { + "title": "Vista predefinita", + "desc": "Se non è selezionato alcun filtro, visualizza un riepilogo degli oggetti tracciati più di recente per etichetta oppure visualizza una griglia non filtrata.", + "summary": "Riepilogo", + "unfilteredGrid": "Griglia non filtrata" + }, + "searchSource": { + "label": "Cerca la fonte", + "desc": "Scegli se cercare nelle miniature o nelle descrizioni degli oggetti tracciati.", + "options": { + "thumbnailImage": "Immagine in miniatura", + "description": "Descrizione" + } + } + }, + "date": { + "selectDateBy": { + "label": "Seleziona una data da filtrare" + } + } + }, + "logSettings": { + "label": "Filtra livello di registro", + "loading": { + "title": "Caricamento", + "desc": "Scorrendo il riquadro dei registri fino in fondo, i nuovi registri vengono automaticamente visualizzati non appena vengono aggiunti." + }, + "filterBySeverity": "Filtra i registri per gravità", + "disableLogStreaming": "Disabilita la trasmissione del registro", + "allLogs": "Tutti i registri" + }, + "trackedObjectDelete": { + "toast": { + "success": "Oggetti tracciati eliminati correttamente.", + "error": "Impossibile eliminare gli oggetti tracciati: {{errorMessage}}" + }, + "desc": "L'eliminazione di questi {{objectLength}} oggetti tracciati rimuove l'istantanea, eventuali incorporamenti salvati e tutte le voci associate al ciclo di vita dell'oggetto. Il filmato registrato di questi oggetti tracciati nella vista Storico NON verrà eliminato.

    Vuoi procedere?

    Tieni premuto il tasto Maiusc per ignorare questa finestra di dialogo in futuro.", + "title": "Conferma eliminazione" + }, + "recognizedLicensePlates": { + "title": "Targhe riconosciute", + "selectPlatesFromList": "Seleziona una o più targhe dall'elenco.", + "loadFailed": "Impossibile caricare le targhe riconosciute.", + "loading": "Caricamento targhe riconosciute…", + "placeholder": "Digita per cercare le targhe…", + "noLicensePlatesFound": "Nessuna targa trovata.", + "selectAll": "Seleziona tutto", + "clearAll": "Cancella tutto" + }, + "timeRange": "Intervallo di tempo", + "subLabels": { + "label": "Sottoetichette", + "all": "Tutte le sottoetichette" + }, + "score": "Punteggio", + "estimatedSpeed": "Velocità stimata ({{unit}})", + "cameras": { + "label": "Filtro telecamere", + "all": { + "title": "Tutte le telecamere", + "short": "Telecamere" + } + }, + "review": { + "showReviewed": "Mostra visti" + }, + "motion": { + "showMotionOnly": "Mostra solo movimento" + }, + "zoneMask": { + "filterBy": "Filtra per maschera di zona" + }, + "classes": { + "label": "Classi", + "all": { + "title": "Tutte le classi" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classi" + }, + "attributes": { + "label": "Attributi di classificazione", + "all": "Tutti gli attributi" + } +} diff --git a/web/public/locales/it/components/icons.json b/web/public/locales/it/components/icons.json new file mode 100644 index 0000000..1877886 --- /dev/null +++ b/web/public/locales/it/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "Cerca un'icona…" + }, + "selectIcon": "Seleziona un'icona" + } +} diff --git a/web/public/locales/it/components/input.json b/web/public/locales/it/components/input.json new file mode 100644 index 0000000..fdd42a7 --- /dev/null +++ b/web/public/locales/it/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "toast": { + "success": "Il video dell'oggetto da te visto ha iniziato a scaricarsi." + }, + "label": "Scarica video" + } + } +} diff --git a/web/public/locales/it/components/player.json b/web/public/locales/it/components/player.json new file mode 100644 index 0000000..0937860 --- /dev/null +++ b/web/public/locales/it/components/player.json @@ -0,0 +1,53 @@ +{ + "noPreviewFound": "Nessuna anteprima trovata", + "noRecordingsFoundForThisTime": "Nessuna registrazione trovata per questo intervallo", + "noPreviewFoundFor": "Nessuna anteprima trovata per {{cameraName}}", + "submitFrigatePlus": { + "title": "Vuoi inviare questo fotogramma a Frigate+?", + "submit": "Invia", + "previewError": "Impossibile caricare l'anteprima dell'istantanea. La registrazione potrebbe non essere disponibile al momento." + }, + "livePlayerRequiredIOSVersion": "Per questo tipo di trasmissione dal vivo è richiesto iOS 17.1 o versione successiva.", + "stats": { + "streamType": { + "short": "Tipo", + "title": "Tipo di trasmissione:" + }, + "bandwidth": { + "title": "Larghezza di banda:", + "short": "Larghezza di banda" + }, + "latency": { + "title": "Latenza:", + "value": "{{seconds}} secondi", + "short": { + "title": "Latenza", + "value": "{{seconds}} sec" + } + }, + "droppedFrames": { + "title": "Fotogrammi persi:", + "short": { + "title": "Persi", + "value": "{{droppedFrames}} fotogrammi" + } + }, + "decodedFrames": "Fotogrammi decodificati:", + "totalFrames": "Totale fotogrammi:", + "droppedFrameRate": "Percentuali fotogrammi persi:" + }, + "streamOffline": { + "desc": "Nessun fotogramma ricevuto sul flusso detect di {{cameraName}}, controlla i registri degli errori", + "title": "Trasmissione disconnessa" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Fotogramma inviato correttamente a Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Impossibile inviare il fotogramma a Frigate+" + } + }, + "cameraDisabled": "La telecamera è disabilita", + "cameraOff": "La telecamera è spenta" +} diff --git a/web/public/locales/it/config/cameras.json b/web/public/locales/it/config/cameras.json new file mode 100644 index 0000000..9814907 --- /dev/null +++ b/web/public/locales/it/config/cameras.json @@ -0,0 +1,957 @@ +{ + "label": "Configurazione telecamera", + "name": { + "label": "Nome telecamera", + "description": "Il nome della telecamera è obbligatorio" + }, + "friendly_name": { + "description": "Nome descrittivo della telecamera utilizzato nell'interfaccia utente di Frigate", + "label": "Nome descrittivo" + }, + "enabled": { + "label": "Abilitata", + "description": "Abilitata" + }, + "audio": { + "label": "Rilevamento audio", + "description": "Impostazioni per il rilevamento di eventi audio per questa telecamera.", + "enabled": { + "label": "Abilita il rilevamento audio", + "description": "Abilita o disabilita il rilevamento degli eventi audio per questa telecamera." + }, + "min_volume": { + "label": "Volume minimo", + "description": "È richiesta una soglia minima di volume RMS per eseguire il rilevamento audio; valori inferiori aumentano la sensibilità (ad esempio, 200 alta, 500 media, 1000 bassa)." + }, + "max_not_heard": { + "label": "Fine pausa", + "description": "Numero di secondi senza il tipo di audio configurato prima che l'evento audio termini." + }, + "listen": { + "label": "Tipi di ascolto", + "description": "Elenco dei tipi di eventi audio da rilevare (ad esempio: abbaio, allarme antincendio, urlo, parlato, grido)." + }, + "filters": { + "label": "Filtri audio", + "description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi.", + "threshold": { + "label": "Affidabilità audio minima", + "description": "Soglia minima di fiducia affinché l'evento audio venga conteggiato." + } + }, + "enabled_in_config": { + "label": "Stato audio originale", + "description": "Indica se il rilevamento audio era originariamente abilitato nel file di configurazione statico." + }, + "num_threads": { + "label": "Processi di rilevamento", + "description": "Numero di processi da utilizzare per l'elaborazione del rilevamento audio." + } + }, + "ffmpeg": { + "path": { + "label": "Percorso FFmpeg", + "description": "Percorso del file binario FFmpeg da utilizzare o alias di versione (\"5.0\" o \"7.0\")." + }, + "label": "FFmpeg", + "hwaccel_args": { + "label": "Argomenti di accelerazione hardware", + "description": "Argomenti di accelerazione hardware per FFmpeg. Si consiglia di utilizzare preimpostazioni specifiche del provider." + }, + "inputs": { + "hwaccel_args": { + "label": "Argomenti di accelerazione hardware", + "description": "Argomenti di accelerazione hardware per questo flusso di ingresso." + }, + "global_args": { + "label": "Argomenti globali di FFmpeg", + "description": "Argomenti globali di FFmpeg per questo flusso di ingresso." + }, + "input_args": { + "label": "Argomenti di ingresso", + "description": "Inserire gli argomenti specifici per questo flusso." + }, + "label": "Ingressi della telecamera", + "description": "Elenco delle definizioni dei flussi di ingresso (percorsi e ruoli) per questa telecamera.", + "path": { + "label": "Percorso di ingresso", + "description": "URL o percorso del flusso di ingresso della telecamera." + }, + "roles": { + "label": "Ruoli di ingresso", + "description": "Ruoli per questo flusso di ingresso." + } + }, + "gpu": { + "description": "Indice GPU predefinito utilizzato per l'accelerazione hardware, se disponibile.", + "label": "Indice GPU" + }, + "description": "Impostazioni di FFmpeg, inclusi percorso binario, argomenti, opzioni hwaccel e argomenti di output per ruolo.", + "global_args": { + "label": "Argomenti globali di FFmpeg", + "description": "Argomenti globali passati ai processi FFmpeg." + }, + "input_args": { + "label": "Argomenti di ingresso", + "description": "Argomenti di ingresso applicati ai flussi di ingresso di FFmpeg." + }, + "output_args": { + "label": "Argomenti di uscita", + "description": "Argomenti di uscita predefiniti utilizzati per i diversi ruoli di FFmpeg, come rilevamento e registrazione.", + "detect": { + "label": "Rileva gli argomenti di uscita", + "description": "Argomenti di uscita predefiniti per il rilevamento dei flussi di ruolo." + }, + "record": { + "label": "Registra gli argomenti di uscita", + "description": "Argomenti di uscita predefiniti per i flussi del ruolo di registrazione." + } + }, + "retry_interval": { + "label": "Tempo di ripetizione FFmpeg", + "description": "Secondi di attesa prima di tentare di riconnettere un flusso video della telecamera dopo un errore. Il valore predefinito è 10." + }, + "apple_compatibility": { + "label": "Compatibilità Apple", + "description": "Attiva l'aggiunta di tag HEVC per una migliore compatibilità con i lettori Apple durante la registrazione in formato H.265." + } + }, + "audio_transcription": { + "label": "Trascrizione audio", + "description": "Impostazioni per la trascrizione audio in tempo reale e del parlato utilizzata per eventi e sottotitoli in tempo reale.", + "enabled": { + "label": "Abilita la trascrizione", + "description": "Abilita o disabilita la trascrizione manuale degli eventi audio." + }, + "enabled_in_config": { + "label": "Stato di trascrizione originale" + }, + "live_enabled": { + "label": "Trascrizione dal vivo", + "description": "Abilita la trascrizione in diretta dell'audio non appena viene ricevuto." + } + }, + "mqtt": { + "label": "MQTT", + "description": "Impostazioni di pubblicazione delle immagini via MQTT.", + "enabled": { + "label": "Invia immagine", + "description": "Abilita la pubblicazione delle istantanee dell'immagine per gli oggetti sui topic MQTT per questa telecamera." + }, + "timestamp": { + "label": "Aggiungi timestamp", + "description": "Sovraimprime un timestamp sulle immagini pubblicate su MQTT." + }, + "bounding_box": { + "label": "Aggiungi riquadro di delimitazione", + "description": "Disegna i riquadri di delimitazione sulle immagini pubblicate tramite MQTT." + }, + "crop": { + "label": "Ritaglia immagine", + "description": "Ritaglia le immagini pubblicate su MQTT in base al riquadro di delimitazione dell'oggetto rilevato." + }, + "height": { + "label": "Altezza immagine", + "description": "Altezza (in pixel) a cui ridimensionare le immagini pubblicate tramite MQTT." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone in cui un oggetto deve entrare affinché venga pubblicata un'immagine MQTT." + }, + "quality": { + "label": "Qualità JPEG", + "description": "Qualità JPEG per le immagini pubblicate su MQTT (0-100)." + } + }, + "onvif": { + "tls_insecure": { + "label": "Disabilita verifica TLS", + "description": "Salta la verifica TLS e disabilita l'autenticazione digest per ONVIF (non sicuro; utilizzare solo in reti sicure)." + }, + "profile": { + "label": "Profilo ONVIF", + "description": "Profilo multimediale ONVIF specifico da utilizzare per il controllo PTZ, identificato tramite token o nome. Se non impostato, viene selezionato automaticamente il primo profilo con una configurazione PTZ valida." + }, + "autotracking": { + "label": "Tracciamento automatico", + "enabled": { + "label": "Abilita il tracciamento automatico", + "description": "Abilita o disabilita il tracciamento automatico della telecamera PTZ per gli oggetti rilevati." + }, + "calibrate_on_startup": { + "label": "Calibra all'avvio", + "description": "Misura la velocità dei motori PTZ all'avvio per migliorare la precisione del tracciamento. Frigate aggiornerà la configurazione con i valori di movement_weights dopo la calibrazione." + }, + "zooming": { + "label": "Modalità ingrandimento", + "description": "Controlla il comportamento dello zoom: disabled (solo pan/tilt), absolute (più compatibile) o relative (pan/tilt/zoom simultanei)." + }, + "zoom_factor": { + "label": "Fattore di ingrandimento", + "description": "Controlla il livello di zoom sugli oggetti tracciati. Valori più bassi mantengono una porzione maggiore della scena visibile; valori più alti aumentano lo zoom ma potrebbero far perdere il tracciamento. Valori compresi tra 0.1 e 0.75." + }, + "track": { + "label": "Oggetti tracciati", + "description": "Elenco dei tipi di oggetto che dovrebbero attivare il tracciamento automatico." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Gli oggetti devono entrare in una di queste zone prima che inizi il tracciamento automatico." + }, + "timeout": { + "label": "Scadenza di ritorno", + "description": "Attendi questo numero di secondi dopo aver perso il tracciamento prima di riportare la telecamera nella posizione preimpostata." + }, + "movement_weights": { + "description": "Valori di calibrazione generati automaticamente dalla calibrazione della telecamera. Non modificare manualmente.", + "label": "Pesi del movimento" + }, + "enabled_in_config": { + "label": "Stato originale del tracciamento automatico", + "description": "Campo interno per tracciare se il tracciamento automatico era abilitato nella configurazione." + }, + "description": "Traccia automaticamente gli oggetti in movimento e li mantiene centrati nell'inquadratura utilizzando i movimenti della telecamera PTZ.", + "return_preset": { + "label": "Preset di ritorno", + "description": "Nome del preset ONVIF configurato nel firmware della telecamera a cui ritornare al termine del tracciamento." + } + }, + "ignore_time_mismatch": { + "label": "Ignora la discrepanza oraria", + "description": "Ignora le differenze di sincronizzazione oraria tra la telecamera e il server Frigate per la comunicazione ONVIF." + }, + "label": "ONVIF", + "port": { + "label": "Porta ONVIF", + "description": "Numero di porta per il servizio ONVIF." + }, + "description": "Impostazioni della connessione ONVIF e del tracciamento automatico PTZ per questa telecamera.", + "host": { + "label": "Host ONVIF", + "description": "Host (e schema opzionale) per il servizio ONVIF di questa telecamera." + }, + "user": { + "label": "Nome utente ONVIF", + "description": "Nome utente per l'autenticazione ONVIF; alcuni dispositivi richiedono un utente amministratore per l'ONVIF." + }, + "password": { + "label": "Password ONVIF", + "description": "Password per l'autenticazione ONVIF." + } + }, + "detect": { + "label": "Rilevamento oggetti", + "description": "Impostazioni per il ruolo di rilevamento/rilevamento utilizzato per eseguire il rilevamento degli oggetti e inizializzare i localizzatori.", + "enabled": { + "label": "Abilita il rilevamento degli oggetti", + "description": "Abilita o disabilita il rilevamento degli oggetti per questa telecamera." + }, + "height": { + "label": "Rileva altezza", + "description": "Altezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso." + }, + "width": { + "label": "Rileva larghezza", + "description": "Larghezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso." + }, + "fps": { + "label": "Rileva FPS", + "description": "Numero di fotogrammi al secondo desiderati per eseguire il rilevamento; valori inferiori riducono l'utilizzo della CPU (il valore consigliato è 5, impostarne uno superiore - al massimo 10 - solo se si devono tracciare oggetti in movimento estremamente rapidi)." + }, + "min_initialized": { + "label": "Frame di inizializzazione minimi", + "description": "Numero di rilevamenti consecutivi necessari prima di creare un oggetto tracciato. Aumenta questo valore per ridurre le inizializzazioni errate. Il valore predefinito è FPS diviso per 2." + }, + "max_disappeared": { + "label": "Numero di fotogrammi scomparsi", + "description": "Numero di fotogrammi senza rilevamento prima che un oggetto tracciato venga considerato scomparso." + }, + "stationary": { + "label": "Configurazione degli oggetti stazionari", + "description": "Impostazioni per rilevare e gestire gli oggetti che rimangono fermi per un certo periodo di tempo.", + "interval": { + "label": "Intervallo stazionario", + "description": "Con quale frequenza (in fotogrammi) eseguire un controllo di rilevamento per confermare che l'oggetto sia stazionario." + }, + "threshold": { + "label": "Soglia stazionaria", + "description": "Numero di fotogrammi senza cambio di posizione necessari per contrassegnare un oggetto come stazionario." + }, + "max_frames": { + "label": "Fotogrammi massimi", + "description": "Limita la durata del tracciamento degli oggetti statici prima che vengano scartati.", + "default": { + "description": "Numero massimo predefinito di fotogrammi per seguire un oggetto stazionario prima di interrompere la ripresa.", + "label": "Fotogrammi massimi predefiniti" + }, + "objects": { + "label": "Fotogrammi massimi oggetto", + "description": "Opzioni di sovrascrittura per singolo oggetto relative al numero massimo di fotogrammi necessari per tracciare oggetti statici." + } + }, + "classifier": { + "label": "Abilita il classificatore visivo", + "description": "Utilizza un classificatore visivo per rilevare oggetti realmente stazionari anche quando i riquadri di delimitazione sono instabili." + } + }, + "annotation_offset": { + "label": "Differenza annotazione", + "description": "Millisecondi per spostare le annotazioni di rilevamento dello spostamento al fine di allineare meglio i riquadri di delimitazione della cronologia con le registrazioni; può essere positivo o negativo." + } + }, + "face_recognition": { + "label": "Riconoscimento facciale", + "description": "Impostazioni per il rilevamento e il riconoscimento facciale di questa telecamera.", + "enabled": { + "label": "Abilita il riconoscimento facciale", + "description": "Attiva o disattiva il riconoscimento facciale." + }, + "min_area": { + "label": "Area minima del viso", + "description": "Area minima (in pixel) del riquadro del volto rilevato necessaria per tentare il riconoscimento." + } + }, + "review": { + "label": "Revisione", + "description": "Impostazioni che controllano gli avvisi, i rilevamenti e i riepiloghi di revisione GenAI utilizzati dall'interfaccia utente (UI) e dall'archiviazione per questa telecamera.", + "alerts": { + "label": "Configurazione avvisi", + "description": "Impostazioni relative a quali oggetti tracciati generano avvisi e alle modalità di conservazione degli avvisi stessi.", + "enabled": { + "label": "Abilita avvisi", + "description": "Abilita o disabilita la generazione di avvisi per questa telecamera." + }, + "labels": { + "label": "Etichette avvisi", + "description": "Elenco delle etichette oggetto che si qualificano come avvisi (ad esempio: car, person)." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone in cui un oggetto deve entrare per essere considerato un avviso; lascia vuoto per consentire qualsiasi zona." + }, + "enabled_in_config": { + "label": "Stato avvisi originale", + "description": "Traccia se gli avvisi erano originariamente abilitati nella configurazione statica." + }, + "cutoff_time": { + "label": "Tempo limite avvisi", + "description": "Secondi da attendere dopo l'assenza di attività che genera avvisi prima di interrompere un avviso." + } + }, + "detections": { + "label": "Configurazione rilevamenti", + "description": "Impostazioni relative a quali oggetti tracciati generano rilevamenti (non avvisi) e alle modalità di conservazione dei rilevamenti stessi.", + "enabled": { + "label": "Abilita rilevamenti", + "description": "Abilita o disabilita gli eventi di rilevamento per questa telecamera." + }, + "labels": { + "label": "Etichette rilevamenti", + "description": "Elenco delle etichette oggetto che si qualificano come eventi di rilevamento." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone in cui un oggetto deve entrare per essere considerato un rilevamento; lascia vuoto per consentire qualsiasi zona." + }, + "cutoff_time": { + "label": "Tempo limite rilevamenti", + "description": "Secondi da attendere dopo l'assenza di attività che genera rilevamenti prima di interrompere un rilevamento." + }, + "enabled_in_config": { + "label": "Stato rilevamenti originale", + "description": "Traccia se i rilevamenti erano originariamente abilitati nella configurazione statica." + } + }, + "genai": { + "label": "Configurazione GenAI", + "description": "Controlla l'uso dell'IA generativa per la produzione di descrizioni e riepiloghi degli elementi di revisione.", + "enabled": { + "label": "Abilita descrizioni GenAI", + "description": "Abilita o disabilita le descrizioni e i riepiloghi generati dalla GenAI per gli elementi di revisione." + }, + "alerts": { + "label": "Abilita GenAI per avvisi", + "description": "Usa la GenAI per generare descrizioni per gli elementi di avviso." + }, + "detections": { + "label": "Abilita GenAI per rilevamenti", + "description": "Usa la GenAI per generare descrizioni per gli elementi di rilevamento." + }, + "image_source": { + "label": "Fonte immagini di revisione", + "description": "Fonte delle immagini inviate alla GenAI ('anteprime' o 'registrazioni'); 'registrazioni' utilizza fotogrammi di qualità superiore ma consuma più token." + }, + "additional_concerns": { + "label": "Ulteriori criteri di attenzione", + "description": "Un elenco di note o criteri di attenzione aggiuntivi che la GenAI deve considerare quando valuta l'attività su questa telecamera." + }, + "debug_save_thumbnails": { + "label": "Salva miniature", + "description": "Salva le miniature inviate al provider GenAI per il debug e la revisione." + }, + "enabled_in_config": { + "label": "Stato GenAI originale", + "description": "Traccia se la revisione GenAI era originariamente abilitata nella configurazione statica." + }, + "preferred_language": { + "label": "Lingua preferita", + "description": "Lingua preferita da richiedere al provider GenAI per le risposte generate." + }, + "activity_context_prompt": { + "label": "Prompt di contesto dell'attività", + "description": "Prompt personalizzato che descrive cosa costituisce o meno un'attività sospetta, per fornire contesto ai riepiloghi della GenAI." + } + } + }, + "profiles": { + "label": "Profili", + "description": "Profili di configurazione denominati con sovrascritture parziali che possono essere attivati in fase di esecuzione." + }, + "record": { + "label": "Registrazione", + "export": { + "description": "Impostazioni utilizzate durante l'esportazione delle registrazioni come timelapse e accelerazione hardware.", + "hwaccel_args": { + "description": "Argomenti di accelerazione hardware da utilizzare per le operazioni di esportazione/transcodifica.", + "label": "Argomenti hwaccel esportazione" + }, + "label": "Configurazione esportazione", + "max_concurrent": { + "label": "Esportazioni simultanee massime", + "description": "Numero massimo di processi di esportazione da elaborare contemporaneamente." + } + }, + "description": "Impostazioni di registrazione e conservazione per questa telecamera.", + "enabled": { + "label": "Abilita registrazione", + "description": "Attiva o disattiva la registrazione per questa telecamera." + }, + "expire_interval": { + "label": "Intervallo di pulizia registrazioni", + "description": "Minuti tra i cicli di pulizia che rimuovono i segmenti di registrazione scaduti." + }, + "continuous": { + "label": "Conservazione continua", + "description": "Numero di giorni per cui conservare le registrazioni, indipendentemente dagli oggetti tracciati o dal movimento. Impostare su 0 se si desidera conservare solo le registrazioni relative agli avvisi e ai rilevamenti.", + "days": { + "label": "Giorni di conservazione", + "description": "Numero di giorni di conservazione delle registrazioni." + } + }, + "motion": { + "label": "Conservazione movimento", + "description": "Numero di giorni per cui conservare le registrazioni attivate dal movimento, indipendentemente dagli oggetti tracciati. Impostare su 0 se si desidera conservare solo le registrazioni relative ad allarmi e rilevamenti.", + "days": { + "label": "Giorni di conservazione", + "description": "Numero di giorni di conservazione delle registrazioni." + } + }, + "detections": { + "label": "Conservazione rilevamento", + "description": "Impostazioni relative alla conservazione delle registrazioni per gli eventi di rilevamento, comprese le durate prima e dopo l'acquisizione.", + "pre_capture": { + "label": "Secondi prima della cattura", + "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione." + }, + "post_capture": { + "label": "Secondi dopo la cattura", + "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione." + }, + "retain": { + "label": "Conservazione eventi", + "description": "Impostazioni di conservazione per le registrazioni degli eventi di rilevamento.", + "days": { + "label": "Giorni di conservazione", + "description": "Numero di giorni per cui conservare le registrazioni degli eventi di rilevamento." + }, + "mode": { + "label": "Modalità di conservazione", + "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)." + } + } + }, + "alerts": { + "label": "Conservazione avvisi", + "description": "Impostazioni di conservazione delle registrazioni per gli eventi di avviso, incluse le durate prima e dopo la cattura.", + "pre_capture": { + "label": "Secondi prima della cattura", + "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione." + }, + "post_capture": { + "label": "Secondi dopo la cattura", + "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione." + }, + "retain": { + "label": "Conservazione eventi", + "description": "Impostazioni di conservazione per le registrazioni degli eventi di rilevamento.", + "days": { + "label": "Giorni di conservazione", + "description": "Numero di giorni per cui conservare le registrazioni degli eventi di rilevamento." + }, + "mode": { + "label": "Modalità di conservazione", + "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)." + } + } + }, + "preview": { + "label": "Configurazione anteprima", + "description": "Impostazioni che controllano la qualità delle anteprime di registrazione mostrate nell'interfaccia utente (UI).", + "quality": { + "label": "Qualità anteprima", + "description": "Livello di qualità dell'anteprima (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Stato registrazione originale", + "description": "Indica se la registrazione era abilitata nella configurazione statica originale." + } + }, + "snapshots": { + "label": "Istantanee", + "description": "Impostazioni per le istantanee generate via API degli oggetti tracciati per questa telecamera.", + "enabled": { + "label": "Abilita istantanee", + "description": "Abilita o disabilita il salvataggio delle istantanee per questa telecamera." + }, + "timestamp": { + "label": "Sovrimpressione timestamp", + "description": "Sovraimprime un timestamp sulle istantanee provenienti dalle API." + }, + "bounding_box": { + "label": "Sovrimpressione riquadro di delimitazione", + "description": "Disegna i riquadri di delimitazione per gli oggetti tracciati sulle istantanee provenienti dalle API." + }, + "crop": { + "label": "Ritaglia istantanea", + "description": "Ritaglia le istantanee provenienti dalle API in base al riquadro di delimitazione dell'oggetto rilevato." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone in cui un oggetto deve entrare affinché venga salvata un'istantanea." + }, + "height": { + "label": "Altezza istantanea", + "description": "Altezza (in pixel) a cui ridimensionare le istantanee provenienti dalle API; lascia vuoto per mantenere la dimensione originale." + }, + "retain": { + "label": "Conservazione istantanee", + "description": "Impostazioni di conservazione per le istantanee, inclusi i giorni predefiniti e le eccezioni per singolo oggetto.", + "default": { + "label": "Conservazione predefinita", + "description": "Numero predefinito di giorni per cui conservare le istantanee." + }, + "mode": { + "label": "Modalità di conservazione", + "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)." + }, + "objects": { + "label": "Conservazione oggetti", + "description": "Eccezioni per singolo oggetto relativi ai giorni di conservazione delle istantanee." + } + }, + "quality": { + "label": "Qualità istantanee", + "description": "Qualità di codifica per le istantanee salvate (0-100)." + } + }, + "motion": { + "label": "Rilevamento movimento", + "contour_area": { + "label": "Area di contorno", + "description": "Area minima del contorno in pixel richiesta affinché un contorno di movimento venga conteggiato." + }, + "improve_contrast": { + "label": "Migliora il contrasto", + "description": "Applica un miglioramento del contrasto ai fotogrammi prima dell'analisi del movimento per facilitare il rilevamento." + }, + "description": "Impostazioni predefinite per il rilevamento del movimento di questa telecamera.", + "enabled": { + "label": "Attiva il rilevamento del movimento", + "description": "Attiva o disattiva il rilevamento di movimento per questa telecamera." + }, + "threshold": { + "label": "Soglia di movimento", + "description": "Soglia di differenza tra i pixel utilizzata dal rilevatore di movimento; valori più alti riducono la sensibilità (intervallo 1-255)." + }, + "lightning_threshold": { + "label": "Soglia di luminosità", + "description": "Soglia per rilevare e ignorare brevi picchi di luminosità (un valore più basso indica una maggiore sensibilità; i valori sono compresi tra 0.3 e 1.0). Ciò non impedisce del tutto il rilevamento del movimento, ma fa semplicemente sì che il rilevatore smetta di analizzare i fotogrammi successivi una volta superata la soglia. Durante questi eventi vengono comunque create registrazioni basate sul movimento." + }, + "skip_motion_threshold": { + "label": "Ignora soglia di movimento", + "description": "Se impostato su un valore compreso tra 0.0 e 110, e se in un singolo fotogramma cambia una porzione dell'immagine superiore a tale frazione, il rilevatore non restituirà alcun riquadro di movimento e si ricalibrerà immediatamente. Ciò consente di risparmiare risorse della CPU e ridurre i falsi positivi in caso di fulmini, temporali, ecc., ma potrebbe comportare la mancata rilevazione di eventi reali, come ad esempio il tracciamento automatico di un oggetto da parte di una telecamera PTZ. Il compromesso consiste nel scegliere se sacrificare alcuni megabyte di registrazioni o rivedere un paio di brevi video. Lasciare non impostato (Nessuno) per disabilitare questa funzione." + }, + "delta_alpha": { + "description": "Fattore di fusione alfa utilizzato nel calcolo della differenza tra fotogrammi per il calcolo del movimento.", + "label": "Delta alfa" + }, + "frame_alpha": { + "description": "Valore alfa utilizzato durante la fusione dei fotogrammi per la preelaborazione del movimento.", + "label": "Frame alfa" + }, + "frame_height": { + "label": "Altezza del frame", + "description": "Altezza in pixel alla quale ridimensionare i fotogrammi durante il calcolo del movimento." + }, + "mask": { + "label": "Coordinate della maschera", + "description": "Coordinate x, y ordinate che definiscono il poligono della maschera di movimento utilizzato per includere/escludere aree." + }, + "mqtt_off_delay": { + "label": "Ritardo di disattivazione MQTT", + "description": "Secondi di attesa dopo l'ultimo movimento prima di pubblicare uno stato 'off' MQTT." + }, + "enabled_in_config": { + "description": "Indica se il rilevamento del movimento era abilitato nella configurazione statica originale.", + "label": "Stato movimento originale" + }, + "raw_mask": { + "label": "Maschera grezza" + } + }, + "objects": { + "label": "Oggetti", + "description": "Impostazioni predefinite per il tracciamento degli oggetti, tra cui le etichette da tracciare e i filtri per singolo oggetto.", + "track": { + "label": "Oggetti da tracciare", + "description": "Elenco delle etichette degli oggetti da tracciare per questa telecamera." + }, + "filters": { + "label": "Filtri oggetto", + "description": "Filtri applicati agli oggetti rilevati per ridurre i falsi positivi (area, rapporto, livello di confidenza).", + "min_area": { + "label": "Area minima dell'oggetto", + "description": "Area minima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)." + }, + "max_area": { + "label": "Area massima dell'oggetto", + "description": "Area massima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)." + }, + "min_ratio": { + "label": "Rapporto di aspetto minimo", + "description": "Rapporto minimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "max_ratio": { + "label": "Rapporto di aspetto massimo", + "description": "Rapporto massimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "threshold": { + "label": "Soglia di confidenza", + "description": "Soglia di confidenza media di rilevamento necessaria affinché l'oggetto sia considerato un vero positivo." + }, + "min_score": { + "label": "Confidenza minima", + "description": "Livello minimo di confidenza nel rilevamento in un singolo fotogramma richiesto affinché l'oggetto venga conteggiato." + }, + "mask": { + "label": "Maschera filtro", + "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura." + }, + "raw_mask": { + "label": "Maschera grezza" + } + }, + "mask": { + "label": "Maschera oggetto", + "description": "Poligono di mascheramento utilizzato per impedire il rilevamento di oggetti in aree specifiche." + }, + "genai": { + "label": "Configurazione degli oggetti GenAI", + "description": "Opzioni GenAI per la descrizione degli oggetti tracciati e l'invio dei fotogrammi per la generazione.", + "enabled": { + "label": "Abilita GenAI", + "description": "Abilita in modo predefinito la generazione di descrizioni da parte di GenAI per gli oggetti tracciati." + }, + "use_snapshot": { + "label": "Utilizza istantanee", + "description": "Utilizza le istantanee degli oggetti anziché le miniature per la generazione di descrizioni con GenAI." + }, + "prompt": { + "description": "Modello di richiesta predefinito utilizzato per la generazione di descrizioni con GenAI.", + "label": "Richiesta di didascalia" + }, + "object_prompts": { + "description": "Richieste specifiche per ogni oggetto per personalizzare i risultati di GenAI in base a etichette specifiche.", + "label": "Richiesta di oggetto" + }, + "objects": { + "label": "Oggetti GenAI", + "description": "Elenco di etichette degli oggetti da inviare a GenAI per impostazione predefinita." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone che devono essere inserite affinché gli oggetti siano idonei alla generazione della descrizione GenAI." + }, + "debug_save_thumbnails": { + "label": "Salva miniature", + "description": "Salva le miniature inviate a GenAI per la correzione e la revisione." + }, + "send_triggers": { + "label": "Attivazioni GenAI", + "description": "Definisce quando i fotogrammi devono essere inviati a GenAI (alla fine, dopo gli aggiornamenti, ecc.).", + "tracked_object_end": { + "label": "Invia alla fine", + "description": "Invia una richiesta a GenAI quando l'oggetto tracciato termina." + }, + "after_significant_updates": { + "label": "Attivazione anticipata GenAI", + "description": "Invia una richiesta a GenAI dopo un numero specificato di aggiornamenti significativi per l'oggetto tracciato." + } + }, + "enabled_in_config": { + "label": "Stato GenAI originale", + "description": "Indica se GenAI era abilitata nella configurazione statica originale." + } + }, + "raw_mask": { + "label": "Maschera grezza" + } + }, + "live": { + "label": "Riproduzione in diretta", + "description": "Impostazioni utilizzate dall'interfaccia web per controllare la selezione, la risoluzione e la qualità della trasmissione Dal vivo.", + "streams": { + "label": "Nomi dei flussi Dal vivo", + "description": "Mappatura dei nomi dei flussi configurati ai nomi ritrasmissioni/go2rtc utilizzati per la riproduzione dal vivo." + }, + "height": { + "label": "Altezza dal vivo", + "description": "Altezza (in pixel) per visualizzare il flusso dal vivo jsmpeg nell'interfaccia utente web; deve essere <= altezza del flusso rilevato." + }, + "quality": { + "label": "Qualità dal vivo", + "description": "Qualità di codifica per il flusso jsmpeg (1 massima, 31 minima)." + } + }, + "timestamp_style": { + "label": "Stile orario", + "description": "Opzioni di stile per i timestamp applicati alle istantanee e alla vista di Debug.", + "position": { + "label": "Posizione timestamp", + "description": "Posizione del timestamp sull'immagine (tl/tr/bl/br)." + }, + "format": { + "label": "Formato timestamp", + "description": "Stringa di formato data/ora utilizzata per i timestamp (codici di formato datetime di Python)." + }, + "color": { + "label": "Colore timestamp", + "description": "Valori di colore RGB per il testo del timestamp (tutti i valori da 0 a 255).", + "red": { + "label": "Rosso", + "description": "Componente rosso (0-255) per il colore del timestamp." + }, + "green": { + "label": "Verde", + "description": "Componente verde (0-255) per il colore del timestamp." + }, + "blue": { + "label": "Blu", + "description": "Componente blu (0-255) per il colore del timestamp." + } + }, + "thickness": { + "label": "Spessore timestamp", + "description": "Spessore della linea del testo del timestamp." + }, + "effect": { + "label": "Effetto timestamp", + "description": "Effetto visivo per il testo del timestamp (none, solid, shadow)." + } + }, + "notifications": { + "label": "Notifiche", + "enabled": { + "label": "Abilita le notifiche", + "description": "Abilita o disabilita le notifiche per questa telecamera." + }, + "email": { + "label": "Email di notifica", + "description": "Indirizzo email utilizzato per le notifiche push o richiesto da alcuni fornitori di servizi di notifica." + }, + "cooldown": { + "label": "Tempo di attesa tra notifiche", + "description": "Tempo di attesa (in secondi) tra le notifiche per evitare di inviare spam ai destinatari." + }, + "enabled_in_config": { + "label": "Stato delle notifiche originali", + "description": "Indica se le notifiche erano abilitate nella configurazione statica originale." + }, + "description": "Impostazioni per abilitare e controllare le notifiche per questa telecamera." + }, + "birdseye": { + "label": "Birdseye", + "description": "Impostazioni per la vista composita Birdseye che unisce più flussi video di telecamere in un unico formato.", + "enabled": { + "label": "Abilita Birdseye", + "description": "Abilita o disabilita la funzione di visualizzazione Birdseye." + }, + "mode": { + "label": "Modalità di tracciamento", + "description": "Modalità per includere le telecamere in Birdseye: 'oggetti', 'movimento' o 'continuo'." + }, + "order": { + "label": "Posizione", + "description": "Posizione numerica che controlla l'ordine delle telecamere nella disposizione Birdseye." + } + }, + "semantic_search": { + "label": "Ricerca semantica", + "triggers": { + "label": "Attivatori", + "friendly_name": { + "label": "Nome descrittivo", + "description": "Nome descrittivo opzionale visualizzato nell'interfaccia utente per questo attivatore." + }, + "description": "Azioni e criteri di corrispondenza per gli attivatori della ricerca semantica specifici della telecamera.", + "enabled": { + "label": "Abilita questo attivatore", + "description": "Abilita o disabilita questo attivatore della ricerca semantica." + }, + "type": { + "label": "Tipo di attivatore", + "description": "Tipo di attivatore: 'miniatura' (corrispondenza con immagine) o 'descrizione' (corrispondenza con testo)." + }, + "data": { + "label": "Contenuto dell'attivatore", + "description": "Frase di testo o ID della miniatura da confrontare con gli oggetti tracciati." + }, + "threshold": { + "label": "Soglia dell'attivatore", + "description": "Punteggio minimo di somiglianza (0-1) richiesto per attivare questo attivatore." + }, + "actions": { + "label": "Azioni dell'attivatore", + "description": "Elenco delle azioni da eseguire quando l'attivatore trova una corrispondenza (notification, sub_label, attribute)." + } + }, + "description": "Impostazioni per la ricerca semantica, che crea e interroga gli embedding degli oggetti per trovare elementi simili." + }, + "lpr": { + "label": "Riconoscimento targhe", + "description": "Impostazioni di riconoscimento delle targhe, incluse le soglie di rilevamento, la formattazione e le targhe conosciute.", + "enabled": { + "label": "Abilita riconoscimento delle targhe (LPR)", + "description": "Attiva o disattiva il riconoscimento delle targhe (LPR) su questa telecamera." + }, + "expire_time": { + "label": "Scadenza in secondi", + "description": "Tempo in secondi trascorso dopo il quale una targa non identificata viene rimossa dal sistema di tracciamento (solo per telecamere LPR dedicate)." + }, + "min_area": { + "label": "Area minima della targa", + "description": "Area minima della targa (in pixel) richiesta per tentare il riconoscimento." + }, + "enhancement": { + "label": "Livello di miglioramento", + "description": "Livello di miglioramento (0-10) da applicare alle immagini delle targhe prima dell'OCR; valori più elevati potrebbero non migliorare sempre i risultati; i livelli superiori a 5 potrebbero funzionare solo con immagini di targhe scattate di notte e devono essere utilizzati con cautela." + } + }, + "ui": { + "description": "Visualizza l'ordine e la visibilità di questa telecamera nell'interfaccia utente. L'ordine influisce sul cruscotto predefinito. Per un controllo più granulare, utilizza i gruppi di telecamere.", + "order": { + "description": "L'ordine numerico viene utilizzato per ordinare le telecamere nell'interfaccia utente (cruscotto ed elenchi predefiniti); i numeri più grandi compaiono successivamente.", + "label": "Ordine interfaccia utente" + }, + "dashboard": { + "label": "Mostra nell'interfaccia utente", + "description": "Abilita o disabilita la visualizzazione di questa telecamera in ogni punto dell'interfaccia utente di Frigate. Disabilitando questa opzione, sarà necessario modificare manualmente la configurazione per visualizzare nuovamente la telecamera nell'interfaccia utente." + }, + "label": "Interfaccia utente telecamera", + "review": { + "label": "Mostra in revisione", + "description": "Abilita o disabilita la visibilità di questa telecamera nella sezione di revisione (la pagina di revisione e il relativo filtro telecamera, la revisione del movimento e la vista della cronologia)." + } + }, + "zones": { + "enabled": { + "label": "Abilitata", + "description": "Abilita o disabilita questa zona. Le zone disabilitate vengono ignorate durante l'esecuzione." + }, + "label": "Zone", + "friendly_name": { + "label": "Nome zona", + "description": "Un nome intuitivo per la zona, visualizzato nell'interfaccia utente di Frigate. Se non specificato, verrà utilizzata una versione formattata del nome della zona." + }, + "filters": { + "raw_mask": { + "label": "Maschera grezza" + }, + "label": "Filtri Zona", + "description": "Filtri da applicare agli oggetti all'interno di questa zona. Utilizzati per ridurre i falsi positivi o limitare quali oggetti sono considerati presenti nella zona.", + "min_area": { + "label": "Area minima oggetto", + "description": "Area minima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere in pixel (int) o in percentuale (float compreso tra 0.000001 e 0.99)." + }, + "max_area": { + "label": "Area massima oggetto", + "description": "Area massima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)." + }, + "min_ratio": { + "label": "Rapporto di aspetto minimo", + "description": "Rapporto minimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "max_ratio": { + "label": "Rapporto di aspetto massimo", + "description": "Rapporto massimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "threshold": { + "label": "Soglia di confidenza", + "description": "Soglia di confidenza media di rilevamento necessaria affinché l'oggetto sia considerato un vero positivo." + }, + "min_score": { + "label": "Confidenza minima", + "description": "Livello minimo di confidenza nel rilevamento in un singolo fotogramma richiesto affinché l'oggetto venga conteggiato." + }, + "mask": { + "label": "Maschera filtro", + "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura." + } + }, + "description": "Le zone consentono di definire un'area specifica dell'inquadratura per determinare se un oggetto si trova o meno all'interno di una particolare area.", + "enabled_in_config": { + "label": "Mantieni traccia dello stato originale della zona." + }, + "coordinates": { + "label": "Coordinate", + "description": "Coordinate del poligono che definiscono l'area della zona. Può essere una stringa separata da virgole o un elenco di stringhe di coordinate. Le coordinate devono essere relative (0-1) o assolute (legacy)." + }, + "distances": { + "label": "Distanze reali", + "description": "Distanze reali opzionali per ciascun lato del quadrilatero della zona, utilizzate per i calcoli di velocità o distanza. Deve avere esattamente 4 valori se impostato." + }, + "inertia": { + "label": "Fotogrammi di inerzia", + "description": "Numero di fotogrammi consecutivi in cui un oggetto deve essere rilevato nella zona prima di essere considerato presente. Aiuta a filtrare i rilevamenti transitori." + }, + "loitering_time": { + "label": "Secondi di stazionamento", + "description": "Numero di secondi per cui un oggetto deve rimanere nella zona per essere considerato in stazionamento nella stessa. Imposta a 0 per disabilitare il rilevamento dello stazionamento." + }, + "speed_threshold": { + "label": "Velocità minima", + "description": "Velocità minima (in unità reali se le distanze sono impostate) richiesta affinché un oggetto sia considerato presente nella zona. Utilizzata per gli attivatori di zona basati sulla velocità." + }, + "objects": { + "label": "Oggetti attivatori", + "description": "Elenco dei tipi di oggetto (dalla labelmap) che possono attivare questa zona. Può essere una stringa o un elenco di stringhe. Se vuoto, vengono considerati tutti gli oggetti." + } + }, + "type": { + "description": "Tipo di telecamera", + "label": "Tipo di telecamera" + }, + "best_image_timeout": { + "label": "Timeout immagine migliore", + "description": "Quanto tempo attendere per l'immagine con il punteggio di accuratezza più alto." + }, + "webui_url": { + "label": "URL telecamera", + "description": "URL per visitare direttamente la telecamera dalla pagina di sistema" + }, + "enabled_in_config": { + "label": "Stato telecamera originale", + "description": "Mantiene traccia dello stato originale della telecamera." + } +} diff --git a/web/public/locales/it/config/global.json b/web/public/locales/it/config/global.json new file mode 100644 index 0000000..b8c557c --- /dev/null +++ b/web/public/locales/it/config/global.json @@ -0,0 +1,1253 @@ +{ + "safe_mode": { + "label": "Modalità sicura", + "description": "Quando abilitata, avvia Frigate in modalità sicura con funzionalità ridotte per la risoluzione dei problemi." + }, + "environment_vars": { + "label": "Variabili d'ambiente", + "description": "Coppie chiave/valore di variabili d'ambiente da impostare per il processo Frigate in Home Assistant OS. Gli utenti non HAOS devono utilizzare la configurazione delle variabili d'ambiente di Docker." + }, + "version": { + "label": "Versione configurazione attuale", + "description": "Versione numerica o stringa della configurazione attiva per facilitare il rilevamento di migrazioni o modifiche di formato." + }, + "audio": { + "label": "Rilevamento audio", + "enabled": { + "label": "Abilita il rilevamento audio" + }, + "min_volume": { + "label": "Volume minimo", + "description": "È richiesta una soglia minima di volume RMS per eseguire il rilevamento audio; valori inferiori aumentano la sensibilità (ad esempio, 200 alta, 500 media, 1000 bassa)." + }, + "max_not_heard": { + "label": "Fine pausa", + "description": "Numero di secondi senza il tipo di audio configurato prima che l'evento audio termini." + }, + "listen": { + "label": "Tipi di ascolto", + "description": "Elenco dei tipi di eventi audio da rilevare (ad esempio: abbaio, allarme antincendio, urlo, parlato, grido)." + }, + "filters": { + "label": "Filtri audio", + "description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi.", + "threshold": { + "label": "Affidabilità audio minima", + "description": "Soglia minima di fiducia affinché l'evento audio venga conteggiato." + } + }, + "enabled_in_config": { + "label": "Stato audio originale", + "description": "Indica se il rilevamento audio era originariamente abilitato nel file di configurazione statico." + }, + "num_threads": { + "label": "Processi di rilevamento", + "description": "Numero di processi da utilizzare per l'elaborazione del rilevamento audio." + } + }, + "logger": { + "description": "Consente di controllare il livello di dettaglio predefinito dei registri e le opzioni di sovrascrittura per ciascun componente.", + "default": { + "label": "Livello del registro", + "description": "Livello di dettaglio predefinito del registro globale (debug, info, warning, error)." + }, + "logs": { + "label": "Livello di registro per processo", + "description": "Opzioni di sovrsacrittura del livello di registro per ciascun componente, per aumentare o diminuire il livello di dettaglio dei singoli moduli." + }, + "label": "Registro" + }, + "auth": { + "label": "Autenticazione", + "description": "Impostazioni di autenticazione e relative alla sessione, incluse le opzioni relative ai cookie e al limite di frequenza.", + "enabled": { + "label": "Abilita autenticazione", + "description": "Abilita l'autenticazione nativa per l'interfaccia utente di Frigate." + }, + "reset_admin_password": { + "label": "Reimposta la password di amministratore", + "description": "Se la condizione è vera, reimposta la password dell'utente amministratore all'avvio e stampa la nuova password nei registri." + }, + "cookie_name": { + "label": "Nome del cookie JWT", + "description": "Nome del cookie utilizzato per memorizzare il token JWT per l'autenticazione nativa." + }, + "cookie_secure": { + "label": "Attributo dei cookie sicuri", + "description": "Imposta l'attributo 'sicuro' sul cookie di autenticazione; deve essere impostato su 'vero' quando si utilizza TLS." + }, + "session_length": { + "label": "Durata della sessione", + "description": "Durata della sessione in secondi per le sessioni basate su JWT." + }, + "refresh_time": { + "label": "Finestra di aggiornamento della sessione", + "description": "Quando una sessione sta per scadere entro questo numero di secondi, aggiornala per ripristinarne la durata completa." + }, + "trusted_proxies": { + "label": "Proxy affidabili", + "description": "Elenco degli indirizzi IP proxy attendibili utilizzati per determinare l'indirizzo IP del client ai fini della limitazione della velocità." + }, + "roles": { + "label": "Mappatura dei ruoli", + "description": "Associa i ruoli agli elenchi delle telecamere. Un elenco vuoto garantisce l'accesso a tutte le telecamere per quel ruolo." + }, + "failed_login_rate_limit": { + "label": "Limiti di accesso non riusciti", + "description": "Regole di limitazione della frequenza per i tentativi di accesso non riusciti al fine di ridurre gli attacchi di forza bruta." + }, + "hash_iterations": { + "description": "Numero di iterazioni PBKDF2-SHA256 da utilizzare per criptare le password utente.", + "label": "Iterazioni di crittografia" + }, + "admin_first_time_login": { + "label": "Flag primo accesso amministratore", + "description": "Quando impostato su true, l'interfaccia utente può mostrare un link di aiuto nella pagina di login per informare gli utenti su come accedere dopo un ripristino della password di amministratore. " + } + }, + "ffmpeg": { + "path": { + "label": "Percorso FFmpeg", + "description": "Percorso del file binario FFmpeg da utilizzare o alias di versione (\"5.0\" o \"7.0\")." + }, + "label": "FFmpeg", + "hwaccel_args": { + "label": "Argomenti di accelerazione hardware", + "description": "Argomenti di accelerazione hardware per FFmpeg. Si consiglia di utilizzare preimpostazioni specifiche del provider." + }, + "inputs": { + "hwaccel_args": { + "label": "Argomenti di accelerazione hardware", + "description": "Argomenti di accelerazione hardware per questo flusso di ingresso." + }, + "global_args": { + "label": "Argomenti globali di FFmpeg", + "description": "Argomenti globali di FFmpeg per questo flusso di ingresso." + }, + "input_args": { + "label": "Argomenti di ingresso", + "description": "Inserire gli argomenti specifici per questo flusso." + }, + "label": "Ingressi della telecamera", + "description": "Elenco delle definizioni dei flussi di ingresso (percorsi e ruoli) per questa telecamera.", + "path": { + "label": "Percorso di ingresso", + "description": "URL o percorso del flusso di ingresso della telecamera." + }, + "roles": { + "label": "Ruoli di ingresso", + "description": "Ruoli per questo flusso di ingresso." + } + }, + "gpu": { + "description": "Indice GPU predefinito utilizzato per l'accelerazione hardware, se disponibile.", + "label": "Indice GPU" + }, + "description": "Impostazioni di FFmpeg, inclusi percorso binario, argomenti, opzioni hwaccel e argomenti di output per ruolo.", + "global_args": { + "label": "Argomenti globali di FFmpeg", + "description": "Argomenti globali passati ai processi FFmpeg." + }, + "input_args": { + "label": "Argomenti di ingresso", + "description": "Argomenti di ingresso applicati ai flussi di ingresso di FFmpeg." + }, + "output_args": { + "label": "Argomenti di uscita", + "description": "Argomenti di uscita predefiniti utilizzati per i diversi ruoli di FFmpeg, come rilevamento e registrazione.", + "detect": { + "label": "Rileva gli argomenti di uscita", + "description": "Argomenti di uscita predefiniti per il rilevamento dei flussi di ruolo." + }, + "record": { + "label": "Registra gli argomenti di uscita", + "description": "Argomenti di uscita predefiniti per i flussi del ruolo di registrazione." + } + }, + "retry_interval": { + "label": "Tempo di ripetizione FFmpeg", + "description": "Secondi di attesa prima di tentare di riconnettere un flusso video della telecamera dopo un errore. Il valore predefinito è 10." + }, + "apple_compatibility": { + "label": "Compatibilità Apple", + "description": "Attiva l'aggiunta di tag HEVC per una migliore compatibilità con i lettori Apple durante la registrazione in formato H.265." + } + }, + "detectors": { + "cpu": { + "num_threads": { + "label": "Numero di processi di rilevamento", + "description": "Il numero di processi utilizzati per l'inferenza basata sulla CPU." + }, + "description": "Rilevatore CPU TFLite che esegue modelli TensorFlow Lite sulla CPU di sistema senza accelerazione hardware. Sconsigliato.", + "label": "CPU" + }, + "label": "Dispositivo di rilevamento", + "hailo8l": { + "description": "Rilevatore Hailo-8/Hailo-8L che utilizza modelli HEF e l'SDK HailoRT per l'inferenza sul dispositivo Hailo.", + "label": "Hailo-8/Hailo-8L", + "device": { + "label": "Tipo di dispositivo", + "description": "Il dispositivo da utilizzare per l'inferenza Hailo (ad es. 'PCIe', 'M.2')." + } + }, + "openvino": { + "description": "Rilevatore OpenVINO per CPU AMD e Intel, GPU Intel e dispositivo Intel VPU." + }, + "rknn": { + "description": "Rilevatore RKNN per NPU Rockchip; esegue modelli RKNN compilati su dispositivo Rockchip." + }, + "synaptics": { + "description": "Rilevatore NPU Synaptics per modelli in formato .synap utilizzando l'SDK Synap su dispositivo Synaptics." + }, + "type": { + "label": "Tipo" + }, + "description": "Configurazione per i rilevatori di oggetti (backend CPU, GPU, ONNX) e impostazioni del modello specifiche per il rilevatore.", + "model": { + "label": "Configurazione del modello specifica del rilevatore", + "description": "Opzioni di configurazione del modello specifiche del rilevatore (percorso, dimensioni di input, ecc.).", + "path": { + "label": "Percorso del modello di rilevamento oggetti personalizzato", + "description": "Percorso di un file del modello di rilevamento personalizzato (o plus:// per i modelli Frigate+)." + }, + "labelmap_path": { + "label": "Mappa delle etichette (label map) per il rilevatore di oggetti personalizzato", + "description": "Percorso di un file labelmap che mappa le classi numeriche in etichette testuali per il rilevatore." + }, + "width": { + "label": "Larghezza di input del modello di rilevamento oggetti", + "description": "Larghezza del tensore di input del modello in pixel." + }, + "height": { + "label": "Altezza di input del modello di rilevamento oggetti", + "description": "Altezza del tensore di input del modello in pixel." + }, + "labelmap": { + "label": "Personalizzazione labelmap", + "description": "Sostituzioni o voci di rimappatura da unire alla labelmap standard." + }, + "attributes_map": { + "label": "Mappa delle etichette oggetto con le relative etichette di attributo", + "description": "Mappatura dalle etichette oggetto alle etichette di attributo utilizzata per allegare metadati (ad esempio 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Forma del tensore di input del modello", + "description": "Formato del tensore richiesto dal modello: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Formato colore dei pixel di input del modello", + "description": "Spazio colore dei pixel richiesto dal modello: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipo di dati di input del modello (Dtype)", + "description": "Tipo di dati del tensore di input del modello (ad esempio 'float32')." + }, + "model_type": { + "label": "Tipo di modello di rilevamento oggetti", + "description": "Tipo di architettura del modello del rilevatore (ssd, yolox, yolonas) utilizzato da alcuni rilevatori per l'ottimizzazione." + } + }, + "model_path": { + "label": "Percorso del modello specifico del rilevatore", + "description": "Percorso del file binario del modello del rilevatore, se richiesto dal rilevatore scelto." + }, + "axengine": { + "label": "NPU AXEngine", + "description": "Rilevatore NPU AXERA AX650N/AX8850N che esegue file .axmodel compilati tramite il runtime AXEngine." + }, + "deepstack": { + "label": "DeepStack", + "description": "Rilevatore DeepStack/CodeProject.AI che invia immagini a una API HTTP DeepStack remota per l'inferenza. Non consigliato.", + "api_url": { + "label": "URL API DeepStack", + "description": "L'URL delle API DeepStack." + }, + "api_timeout": { + "label": "Timeout API DeepStack (in secondi)", + "description": "Tempo massimo consentito per una richiesta alle API DeepStack." + }, + "api_key": { + "label": "Chiave API DeepStack (se richiesta)", + "description": "Chiave API opzionale per i servizi DeepStack autenticati." + } + }, + "degirum": { + "label": "DeGirum", + "description": "Rilevatore DeGirum per l'esecuzione di modelli tramite il cloud DeGirum o servizi di inferenza locali.", + "zoo": { + "label": "Model Zoo", + "description": "Percorso o URL del model zoo di DeGirum." + }, + "token": { + "label": "Token Cloud DeGirum", + "description": "Token per l'accesso al Cloud DeGirum." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "Rilevatore EdgeTPU che esegue modelli TensorFlow Lite compilati per Coral EdgeTPU utilizzando il delegate EdgeTPU.", + "device": { + "label": "Tipo di dispositivo", + "description": "Il dispositivo da utilizzare per l'inferenza EdgeTPU (ad es. 'usb', 'pci')." + } + }, + "memryx": { + "label": "MemryX", + "description": "Rilevatore MemryX MX3 che esegue modelli DFP compilati su acceleratori MemryX.", + "device": { + "label": "Percorso del dispositivo", + "description": "Il dispositivo da utilizzare per l'inferenza MemryX (ad es. 'PCIe')." + } + }, + "onnx": { + "label": "ONNX", + "description": "Rilevatore ONNX per l'esecuzione di modelli ONNX; utilizzerà i backend di accelerazione disponibili (CUDA/ROCm/OpenVINO) quando presenti.", + "device": { + "label": "Tipo di dispositivo", + "description": "Il dispositivo da utilizzare per l'inferenza ONNX (ad es. 'AUTO', 'CPU', 'GPU')." + } + } + }, + "audio_transcription": { + "label": "Trascrizione audio", + "description": "Impostazioni per la trascrizione audio in tempo reale e del parlato utilizzata per eventi e sottotitoli in tempo reale.", + "enabled": { + "label": "Abilita la trascrizione audio" + }, + "live_enabled": { + "label": "Trascrizione dal vivo", + "description": "Abilita la trascrizione in diretta dell'audio non appena viene ricevuto." + }, + "model_size": { + "label": "Dimensioni del modello" + } + }, + "mqtt": { + "label": "MQTT", + "enabled": { + "label": "Abilita MQTT", + "description": "Abilita o disabilita l'integrazione MQTT per stato, eventi e istantanee." + }, + "host": { + "label": "Sistema MQTT", + "description": "Nome sistema o indirizzo IP del broker MQTT." + }, + "port": { + "label": "Porta MQTT", + "description": "Porta del broker MQTT (solitamente 1883 per MQTT standard)." + }, + "topic_prefix": { + "label": "Prefisso argomento", + "description": "Prefisso dell'argomento MQTT per tutti gli argomenti Frigate; deve essere univoco se si eseguono più istanze." + }, + "client_id": { + "label": "ID client", + "description": "Identificativo del client utilizzato per la connessione al broker MQTT; deve essere univoco per ogni istanza." + }, + "stats_interval": { + "label": "Intervallo statistiche", + "description": "Intervallo in secondi per la pubblicazione delle statistiche di sistema e della telecamera su MQTT." + }, + "user": { + "label": "Nome utente MQTT", + "description": "Nome utente MQTT facoltativo; può essere fornito tramite variabili d'ambiente o segreti." + }, + "password": { + "label": "Password MQTT", + "description": "Password MQTT facoltativa; può essere fornita tramite variabili d'ambiente o segreti." + }, + "tls_ca_certs": { + "label": "Certificati CA TLS", + "description": "Percorso al certificato CA per le connessioni TLS al broker (per certificati autofirmati)." + }, + "tls_client_cert": { + "label": "Certificato client", + "description": "Percorso del certificato client per l'autenticazione reciproca TLS; non impostare nome utente/password quando si utilizzano certificati client." + }, + "description": "Impostazioni per la connessione e la pubblicazione di dati di telemetria, istantanee e dettagli degli eventi a un broker MQTT.", + "tls_client_key": { + "label": "Chiave client", + "description": "Percorso della chiave privata per il certificato client." + }, + "tls_insecure": { + "label": "TLS non sicuro", + "description": "Consenti connessioni TLS non sicure saltando la verifica del nome sistema (sconsigliato)." + }, + "qos": { + "label": "QoS MQTT", + "description": "Livello di qualità del servizio per le pubblicazioni/sottoscrizioni MQTT (0, 1 o 2)." + } + }, + "onvif": { + "tls_insecure": { + "label": "Disabilita verifica TLS", + "description": "Salta la verifica TLS e disabilita l'autenticazione digest per ONVIF (non sicuro; utilizzare solo in reti sicure)." + }, + "profile": { + "label": "Profilo ONVIF", + "description": "Profilo multimediale ONVIF specifico da utilizzare per il controllo PTZ, identificato tramite token o nome. Se non impostato, viene selezionato automaticamente il primo profilo con una configurazione PTZ valida." + }, + "autotracking": { + "label": "Tracciamento automatico", + "enabled": { + "label": "Abilita il tracciamento automatico", + "description": "Abilita o disabilita il tracciamento automatico della telecamera PTZ per gli oggetti rilevati." + }, + "calibrate_on_startup": { + "label": "Calibra all'avvio", + "description": "Misura la velocità dei motori PTZ all'avvio per migliorare la precisione del tracciamento. Frigate aggiornerà la configurazione con i valori di movement_weights dopo la calibrazione." + }, + "zooming": { + "label": "Modalità ingrandimento", + "description": "Controlla il comportamento dello zoom: disabled (solo pan/tilt), absolute (più compatibile) o relative (pan/tilt/zoom simultanei)." + }, + "zoom_factor": { + "label": "Fattore di ingrandimento", + "description": "Controlla il livello di zoom sugli oggetti tracciati. Valori più bassi mantengono una porzione maggiore della scena visibile; valori più alti aumentano lo zoom ma potrebbero far perdere il tracciamento. Valori compresi tra 0.1 e 0.75." + }, + "track": { + "label": "Oggetti tracciati", + "description": "Elenco dei tipi di oggetto che dovrebbero attivare il tracciamento automatico." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Gli oggetti devono entrare in una di queste zone prima che inizi il tracciamento automatico." + }, + "timeout": { + "label": "Scadenza di ritorno", + "description": "Attendi questo numero di secondi dopo aver perso il tracciamento prima di riportare la telecamera nella posizione preimpostata." + }, + "movement_weights": { + "description": "Valori di calibrazione generati automaticamente dalla calibrazione della telecamera. Non modificare manualmente.", + "label": "Pesi del movimento" + }, + "enabled_in_config": { + "label": "Stato originale del tracciamento automatico", + "description": "Campo interno per tracciare se il tracciamento automatico era abilitato nella configurazione." + }, + "description": "Traccia automaticamente gli oggetti in movimento e li mantiene centrati nell'inquadratura utilizzando i movimenti della telecamera PTZ.", + "return_preset": { + "label": "Preset di ritorno", + "description": "Nome del preset ONVIF configurato nel firmware della telecamera a cui ritornare al termine del tracciamento." + } + }, + "ignore_time_mismatch": { + "label": "Ignora la discrepanza oraria", + "description": "Ignora le differenze di sincronizzazione oraria tra la telecamera e il server Frigate per la comunicazione ONVIF." + }, + "label": "ONVIF", + "port": { + "label": "Porta ONVIF", + "description": "Numero di porta per il servizio ONVIF." + }, + "description": "Impostazioni della connessione ONVIF e del tracciamento automatico PTZ per questa telecamera.", + "host": { + "label": "Host ONVIF", + "description": "Host (e schema opzionale) per il servizio ONVIF di questa telecamera." + }, + "user": { + "label": "Nome utente ONVIF", + "description": "Nome utente per l'autenticazione ONVIF; alcuni dispositivi richiedono un utente amministratore per l'ONVIF." + }, + "password": { + "label": "Password ONVIF", + "description": "Password per l'autenticazione ONVIF." + } + }, + "detect": { + "label": "Rilevamento oggetti", + "description": "Impostazioni per il ruolo di rilevamento/rilevamento utilizzato per eseguire il rilevamento degli oggetti e inizializzare i localizzatori.", + "enabled": { + "label": "Abilita il rilevamento degli oggetti", + "description": "Abilita o disabilita il rilevamento degli oggetti per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera." + }, + "height": { + "label": "Rileva altezza", + "description": "Altezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso." + }, + "width": { + "label": "Rileva larghezza", + "description": "Larghezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso." + }, + "fps": { + "label": "Rileva FPS", + "description": "Numero di fotogrammi al secondo desiderati per eseguire il rilevamento; valori inferiori riducono l'utilizzo della CPU (il valore consigliato è 5, impostarne uno superiore - al massimo 10 - solo se si devono tracciare oggetti in movimento estremamente rapidi)." + }, + "min_initialized": { + "label": "Frame di inizializzazione minimi", + "description": "Numero di rilevamenti consecutivi necessari prima di creare un oggetto tracciato. Aumenta questo valore per ridurre le inizializzazioni errate. Il valore predefinito è FPS diviso per 2." + }, + "max_disappeared": { + "label": "Numero di fotogrammi scomparsi", + "description": "Numero di fotogrammi senza rilevamento prima che un oggetto tracciato venga considerato scomparso." + }, + "stationary": { + "label": "Configurazione degli oggetti stazionari", + "description": "Impostazioni per rilevare e gestire gli oggetti che rimangono fermi per un certo periodo di tempo.", + "interval": { + "label": "Intervallo stazionario", + "description": "Con quale frequenza (in fotogrammi) eseguire un controllo di rilevamento per confermare che l'oggetto sia stazionario." + }, + "threshold": { + "label": "Soglia stazionaria", + "description": "Numero di fotogrammi senza cambio di posizione necessari per contrassegnare un oggetto come stazionario." + }, + "max_frames": { + "label": "Fotogrammi massimi", + "description": "Limita la durata del tracciamento degli oggetti statici prima che vengano scartati.", + "default": { + "description": "Numero massimo predefinito di fotogrammi per seguire un oggetto stazionario prima di interrompere la ripresa.", + "label": "Fotogrammi massimi predefiniti" + }, + "objects": { + "label": "Fotogrammi massimi oggetto", + "description": "Opzioni di sovrascrittura per singolo oggetto relative al numero massimo di fotogrammi necessari per tracciare oggetti statici." + } + }, + "classifier": { + "label": "Abilita il classificatore visivo", + "description": "Utilizza un classificatore visivo per rilevare oggetti realmente stazionari anche quando i riquadri di delimitazione sono instabili." + } + }, + "annotation_offset": { + "label": "Differenza annotazione", + "description": "Millisecondi per spostare le annotazioni di rilevamento dello spostamento al fine di allineare meglio i riquadri di delimitazione della cronologia con le registrazioni; può essere positivo o negativo." + } + }, + "face_recognition": { + "label": "Riconoscimento facciale", + "model_size": { + "label": "Dimensioni del modello" + }, + "enabled": { + "label": "Abilita il riconoscimento facciale" + }, + "min_area": { + "label": "Area minima del viso", + "description": "Area minima (in pixel) del riquadro del volto rilevato necessaria per tentare il riconoscimento." + } + }, + "proxy": { + "logout_url": { + "description": "URL per reindirizzare gli utenti al momento della registrazione tramite il proxy.", + "label": "URL di disconnessione" + }, + "label": "Proxy", + "description": "Impostazioni per l'integrazione di Frigate dietro un proxy inverso che trasmette le intestazioni utente autenticate.", + "header_map": { + "label": "Mappatura dell'intestazione", + "description": "Mappa le intestazioni proxy in entrata ai campi utente e ruolo di Frigate per l'autenticazione basata su proxy.", + "user": { + "label": "Intestazione utente", + "description": "Intestazione contenente il nome utente autenticato fornito dal proxy a monte." + }, + "role": { + "label": "Intestazione ruolo", + "description": "Intestazione contenente il ruolo o i gruppi dell'utente autenticato, provenienti dal proxy a monte." + }, + "role_map": { + "label": "Mappatura dei ruoli", + "description": "Mappa i valori dei gruppi a monte dei ruoli di Frigate (ad esempio, mappa i gruppi di amministrazione al ruolo di amministratore)." + } + }, + "auth_secret": { + "label": "Segreto di proxy", + "description": "Segreto opzionale verificato rispetto all'intestazione X-Proxy-Secret per convalidare i proxy attendibili." + }, + "default_role": { + "label": "Ruolo predefinito", + "description": "Ruolo predefinito assegnato agli utenti autenticati tramite proxy quando non si applica nessuna mappatura dei ruoli." + }, + "separator": { + "label": "Carattere separatore", + "description": "Carattere utilizzato per separare valori multipli forniti negli header del proxy." + } + }, + "review": { + "label": "Revisione", + "alerts": { + "label": "Configurazione avvisi", + "description": "Impostazioni relative a quali oggetti tracciati generano avvisi e alle modalità di conservazione degli avvisi stessi.", + "enabled": { + "label": "Abilita avvisi" + }, + "labels": { + "label": "Etichette avvisi", + "description": "Elenco delle etichette oggetto che si qualificano come avvisi (ad esempio: car, person)." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone in cui un oggetto deve entrare per essere considerato un avviso; lascia vuoto per consentire qualsiasi zona." + }, + "enabled_in_config": { + "label": "Stato avvisi originale", + "description": "Traccia se gli avvisi erano originariamente abilitati nella configurazione statica." + }, + "cutoff_time": { + "label": "Tempo limite avvisi", + "description": "Secondi da attendere dopo l'assenza di attività che genera avvisi prima di interrompere un avviso." + } + }, + "detections": { + "label": "Configurazione rilevamenti", + "description": "Impostazioni relative a quali oggetti tracciati generano rilevamenti (non avvisi) e alle modalità di conservazione dei rilevamenti stessi.", + "enabled": { + "label": "Abilita rilevamenti" + }, + "labels": { + "label": "Etichette rilevamenti", + "description": "Elenco delle etichette oggetto che si qualificano come eventi di rilevamento." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone in cui un oggetto deve entrare per essere considerato un rilevamento; lascia vuoto per consentire qualsiasi zona." + }, + "cutoff_time": { + "label": "Tempo limite rilevamenti", + "description": "Secondi da attendere dopo l'assenza di attività che genera rilevamenti prima di interrompere un rilevamento." + }, + "enabled_in_config": { + "label": "Stato rilevamenti originale", + "description": "Traccia se i rilevamenti erano originariamente abilitati nella configurazione statica." + } + }, + "genai": { + "label": "Configurazione GenAI", + "description": "Controlla l'uso dell'IA generativa per la produzione di descrizioni e riepiloghi degli elementi di revisione.", + "enabled": { + "label": "Abilita descrizioni GenAI", + "description": "Abilita o disabilita le descrizioni e i riepiloghi generati dalla GenAI per gli elementi di revisione." + }, + "alerts": { + "label": "Abilita GenAI per avvisi", + "description": "Usa la GenAI per generare descrizioni per gli elementi di avviso." + }, + "detections": { + "label": "Abilita GenAI per rilevamenti", + "description": "Usa la GenAI per generare descrizioni per gli elementi di rilevamento." + }, + "image_source": { + "label": "Fonte immagini di revisione", + "description": "Fonte delle immagini inviate alla GenAI ('anteprime' o 'registrazioni'); 'registrazioni' utilizza fotogrammi di qualità superiore ma consuma più token." + }, + "additional_concerns": { + "label": "Ulteriori criteri di attenzione", + "description": "Un elenco di note o criteri di attenzione aggiuntivi che la GenAI deve considerare quando valuta l'attività su questa telecamera." + }, + "debug_save_thumbnails": { + "label": "Salva miniature", + "description": "Salva le miniature inviate al provider GenAI per il debug e la revisione." + }, + "enabled_in_config": { + "label": "Stato GenAI originale", + "description": "Traccia se la revisione GenAI era originariamente abilitata nella configurazione statica." + }, + "preferred_language": { + "label": "Lingua preferita", + "description": "Lingua preferita da richiedere al provider GenAI per le risposte generate." + }, + "activity_context_prompt": { + "label": "Prompt di contesto dell'attività", + "description": "Prompt personalizzato che descrive cosa costituisce o meno un'attività sospetta, per fornire contesto ai riepiloghi della GenAI." + } + } + }, + "ui": { + "label": "Interfaccia utente", + "description": "Preferenze dell'interfaccia utente come fuso orario, formato di data/ora e unità di misura.", + "timezone": { + "label": "Fuso orario", + "description": "Fuso orario opzionale da visualizzare nell'interfaccia utente (se non impostato, viene usata l'ora locale del browser)." + }, + "time_format": { + "label": "Formato Ora", + "description": "Formato dell'ora da utilizzare nell'interfaccia utente (browser, 12 ore o 24 ore)." + }, + "unit_system": { + "label": "Sistema di unità di misura", + "description": "Sistema di unità di misura per la visualizzazione (metrico o imperiale) utilizzato nell'interfaccia utente e nei messaggi MQTT." + } + }, + "profiles": { + "label": "Profili", + "friendly_name": { + "label": "Nome descrittivo", + "description": "Nome visualizzato per questo profilo nell'interfaccia utente." + }, + "description": "Definizioni di profili denominati con nomi descrittivi. I profili delle telecamere devono fare riferimento ai nomi definiti qui." + }, + "record": { + "label": "Registrazione", + "export": { + "description": "Impostazioni utilizzate durante l'esportazione delle registrazioni come timelapse e accelerazione hardware.", + "hwaccel_args": { + "description": "Argomenti di accelerazione hardware da utilizzare per le operazioni di esportazione/transcodifica.", + "label": "Argomenti hwaccel esportazione" + }, + "label": "Configurazione esportazione", + "max_concurrent": { + "label": "Esportazioni simultanee massime", + "description": "Numero massimo di processi di esportazione da elaborare contemporaneamente." + } + }, + "enabled": { + "label": "Abilita registrazione", + "description": "Attiva o disattiva la registrazione per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera." + }, + "expire_interval": { + "label": "Intervallo di pulizia registrazioni", + "description": "Minuti tra i cicli di pulizia che rimuovono i segmenti di registrazione scaduti." + }, + "continuous": { + "label": "Conservazione continua", + "description": "Numero di giorni per cui conservare le registrazioni, indipendentemente dagli oggetti tracciati o dal movimento. Impostare su 0 se si desidera conservare solo le registrazioni relative agli avvisi e ai rilevamenti.", + "days": { + "label": "Giorni di conservazione", + "description": "Numero di giorni di conservazione delle registrazioni." + } + }, + "motion": { + "label": "Conservazione movimento", + "description": "Numero di giorni per cui conservare le registrazioni attivate dal movimento, indipendentemente dagli oggetti tracciati. Impostare su 0 se si desidera conservare solo le registrazioni relative ad allarmi e rilevamenti.", + "days": { + "label": "Giorni di conservazione", + "description": "Numero di giorni di conservazione delle registrazioni." + } + }, + "detections": { + "label": "Conservazione rilevamento", + "description": "Impostazioni relative alla conservazione delle registrazioni per gli eventi di rilevamento, comprese le durate prima e dopo l'acquisizione.", + "pre_capture": { + "label": "Secondi prima della cattura", + "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione." + }, + "post_capture": { + "label": "Secondi dopo la cattura", + "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione." + }, + "retain": { + "label": "Conservazione eventi", + "description": "Impostazioni di conservazione per le registrazioni degli eventi di rilevamento.", + "days": { + "label": "Giorni di conservazione", + "description": "Numero di giorni per cui conservare le registrazioni degli eventi di rilevamento." + }, + "mode": { + "label": "Modalità di conservazione", + "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)." + } + } + }, + "alerts": { + "label": "Conservazione avvisi", + "description": "Impostazioni di conservazione delle registrazioni per gli eventi di avviso, incluse le durate prima e dopo la cattura.", + "pre_capture": { + "label": "Secondi prima della cattura", + "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione." + }, + "post_capture": { + "label": "Secondi dopo la cattura", + "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione." + }, + "retain": { + "label": "Conservazione eventi", + "description": "Impostazioni di conservazione per le registrazioni degli eventi di rilevamento.", + "days": { + "label": "Giorni di conservazione", + "description": "Numero di giorni per cui conservare le registrazioni degli eventi di rilevamento." + }, + "mode": { + "label": "Modalità di conservazione", + "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)." + } + } + }, + "preview": { + "label": "Configurazione anteprima", + "description": "Impostazioni che controllano la qualità delle anteprime di registrazione mostrate nell'interfaccia utente (UI).", + "quality": { + "label": "Qualità anteprima", + "description": "Livello di qualità dell'anteprima (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Stato registrazione originale", + "description": "Indica se la registrazione era abilitata nella configurazione statica originale." + }, + "description": "Impostazioni di registrazione e conservazione vengono applicate alle telecamere a meno che non vengano modificate singolarmente per ciascuna telecamera." + }, + "snapshots": { + "label": "Istantanee", + "enabled": { + "label": "Abilita istantanee" + }, + "timestamp": { + "label": "Sovrimpressione timestamp", + "description": "Sovraimprime un timestamp sulle istantanee provenienti dalle API." + }, + "bounding_box": { + "label": "Sovrimpressione riquadro di delimitazione", + "description": "Disegna i riquadri di delimitazione per gli oggetti tracciati sulle istantanee provenienti dalle API." + }, + "crop": { + "label": "Ritaglia istantanea", + "description": "Ritaglia le istantanee provenienti dalle API in base al riquadro di delimitazione dell'oggetto rilevato." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone in cui un oggetto deve entrare affinché venga salvata un'istantanea." + }, + "height": { + "label": "Altezza istantanea", + "description": "Altezza (in pixel) a cui ridimensionare le istantanee provenienti dalle API; lascia vuoto per mantenere la dimensione originale." + }, + "retain": { + "label": "Conservazione istantanee", + "description": "Impostazioni di conservazione per le istantanee, inclusi i giorni predefiniti e le eccezioni per singolo oggetto.", + "default": { + "label": "Conservazione predefinita", + "description": "Numero predefinito di giorni per cui conservare le istantanee." + }, + "mode": { + "label": "Modalità di conservazione", + "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)." + }, + "objects": { + "label": "Conservazione oggetti", + "description": "Eccezioni per singolo oggetto relativi ai giorni di conservazione delle istantanee." + } + }, + "quality": { + "label": "Qualità istantanee", + "description": "Qualità di codifica per le istantanee salvate (0-100)." + } + }, + "motion": { + "label": "Rilevamento movimento", + "contour_area": { + "label": "Area di contorno", + "description": "Area minima del contorno in pixel richiesta affinché un contorno di movimento venga conteggiato." + }, + "improve_contrast": { + "label": "Migliora il contrasto", + "description": "Applica un miglioramento del contrasto ai fotogrammi prima dell'analisi del movimento per facilitare il rilevamento." + }, + "enabled": { + "label": "Attiva il rilevamento del movimento" + }, + "threshold": { + "label": "Soglia di movimento", + "description": "Soglia di differenza tra i pixel utilizzata dal rilevatore di movimento; valori più alti riducono la sensibilità (intervallo 1-255)." + }, + "lightning_threshold": { + "label": "Soglia di luminosità", + "description": "Soglia per rilevare e ignorare brevi picchi di luminosità (un valore più basso indica una maggiore sensibilità; i valori sono compresi tra 0.3 e 1.0). Ciò non impedisce del tutto il rilevamento del movimento, ma fa semplicemente sì che il rilevatore smetta di analizzare i fotogrammi successivi una volta superata la soglia. Durante questi eventi vengono comunque create registrazioni basate sul movimento." + }, + "skip_motion_threshold": { + "label": "Ignora soglia di movimento", + "description": "Se impostato su un valore compreso tra 0.0 e 110, e se in un singolo fotogramma cambia una porzione dell'immagine superiore a tale frazione, il rilevatore non restituirà alcun riquadro di movimento e si ricalibrerà immediatamente. Ciò consente di risparmiare risorse della CPU e ridurre i falsi positivi in caso di fulmini, temporali, ecc., ma potrebbe comportare la mancata rilevazione di eventi reali, come ad esempio il tracciamento automatico di un oggetto da parte di una telecamera PTZ. Il compromesso consiste nel scegliere se sacrificare alcuni megabyte di registrazioni o rivedere un paio di brevi video. Lasciare non impostato (Nessuno) per disabilitare questa funzione." + }, + "delta_alpha": { + "description": "Fattore di fusione alfa utilizzato nel calcolo della differenza tra fotogrammi per il calcolo del movimento.", + "label": "Delta alfa" + }, + "frame_alpha": { + "description": "Valore alfa utilizzato durante la fusione dei fotogrammi per la preelaborazione del movimento.", + "label": "Frame alfa" + }, + "frame_height": { + "label": "Altezza del frame", + "description": "Altezza in pixel alla quale ridimensionare i fotogrammi durante il calcolo del movimento." + }, + "mask": { + "label": "Coordinate della maschera", + "description": "Coordinate x, y ordinate che definiscono il poligono della maschera di movimento utilizzato per includere/escludere aree." + }, + "mqtt_off_delay": { + "label": "Ritardo di disattivazione MQTT", + "description": "Secondi di attesa dopo l'ultimo movimento prima di pubblicare uno stato 'off' MQTT." + }, + "enabled_in_config": { + "description": "Indica se il rilevamento del movimento era abilitato nella configurazione statica originale.", + "label": "Stato movimento originale" + }, + "raw_mask": { + "label": "Maschera grezza" + } + }, + "objects": { + "label": "Oggetti", + "description": "Impostazioni predefinite per il tracciamento degli oggetti, tra cui le etichette da tracciare e i filtri per singolo oggetto.", + "track": { + "label": "Oggetti da tracciare", + "description": "Elenco delle etichette degli oggetti da tracciare per tutte le telecamere; può essere sovrascritto per ogni singola telecamera." + }, + "filters": { + "label": "Filtri oggetto", + "description": "Filtri applicati agli oggetti rilevati per ridurre i falsi positivi (area, rapporto, livello di confidenza).", + "min_area": { + "label": "Area minima dell'oggetto", + "description": "Area minima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)." + }, + "max_area": { + "label": "Area massima dell'oggetto", + "description": "Area massima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)." + }, + "min_ratio": { + "label": "Rapporto di aspetto minimo", + "description": "Rapporto minimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "max_ratio": { + "label": "Rapporto di aspetto massimo", + "description": "Rapporto massimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "threshold": { + "label": "Soglia di confidenza", + "description": "Soglia di confidenza media di rilevamento necessaria affinché l'oggetto sia considerato un vero positivo." + }, + "min_score": { + "label": "Confidenza minima", + "description": "Livello minimo di confidenza nel rilevamento in un singolo fotogramma richiesto affinché l'oggetto venga conteggiato." + }, + "mask": { + "label": "Maschera filtro", + "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura." + }, + "raw_mask": { + "label": "Maschera grezza" + } + }, + "mask": { + "label": "Maschera oggetto", + "description": "Poligono di mascheramento utilizzato per impedire il rilevamento di oggetti in aree specifiche." + }, + "genai": { + "label": "Configurazione degli oggetti GenAI", + "description": "Opzioni GenAI per la descrizione degli oggetti tracciati e l'invio dei fotogrammi per la generazione.", + "enabled": { + "label": "Abilita GenAI", + "description": "Abilita in modo predefinito la generazione di descrizioni da parte di GenAI per gli oggetti tracciati." + }, + "use_snapshot": { + "label": "Utilizza istantanee", + "description": "Utilizza le istantanee degli oggetti anziché le miniature per la generazione di descrizioni con GenAI." + }, + "prompt": { + "description": "Modello di richiesta predefinito utilizzato per la generazione di descrizioni con GenAI.", + "label": "Richiesta di didascalia" + }, + "object_prompts": { + "description": "Richieste specifiche per ogni oggetto per personalizzare i risultati di GenAI in base a etichette specifiche.", + "label": "Richiesta di oggetto" + }, + "objects": { + "label": "Oggetti GenAI", + "description": "Elenco di etichette degli oggetti da inviare a GenAI per impostazione predefinita." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone che devono essere inserite affinché gli oggetti siano idonei alla generazione della descrizione GenAI." + }, + "debug_save_thumbnails": { + "label": "Salva miniature", + "description": "Salva le miniature inviate a GenAI per la correzione e la revisione." + }, + "send_triggers": { + "label": "Attivazioni GenAI", + "description": "Definisce quando i fotogrammi devono essere inviati a GenAI (alla fine, dopo gli aggiornamenti, ecc.).", + "tracked_object_end": { + "label": "Invia alla fine", + "description": "Invia una richiesta a GenAI quando l'oggetto tracciato termina." + }, + "after_significant_updates": { + "label": "Attivazione anticipata GenAI", + "description": "Invia una richiesta a GenAI dopo un numero specificato di aggiornamenti significativi per l'oggetto tracciato." + } + }, + "enabled_in_config": { + "label": "Stato GenAI originale", + "description": "Indica se GenAI era abilitata nella configurazione statica originale." + } + }, + "raw_mask": { + "label": "Maschera grezza" + }, + "filters_attribute": { + "raw_mask": { + "label": "Maschera grezza" + }, + "min_ratio": { + "label": "Rapporto di aspetto minimo", + "description": "Rapporto minimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "max_ratio": { + "label": "Rapporto di aspetto massimo", + "description": "Rapporto massimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "threshold": { + "label": "Soglia di confidenza" + }, + "min_score": { + "label": "Confidenza minima" + }, + "mask": { + "label": "Maschera filtro", + "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura." + } + } + }, + "live": { + "label": "Riproduzione in diretta", + "streams": { + "label": "Nomi dei flussi Dal vivo", + "description": "Mappatura dei nomi dei flussi configurati ai nomi ritrasmissioni/go2rtc utilizzati per la riproduzione dal vivo." + }, + "height": { + "label": "Altezza dal vivo", + "description": "Altezza (in pixel) per visualizzare il flusso dal vivo jsmpeg nell'interfaccia utente web; deve essere <= altezza del flusso rilevato." + }, + "quality": { + "label": "Qualità dal vivo", + "description": "Qualità di codifica per il flusso jsmpeg (1 massima, 31 minima)." + } + }, + "timestamp_style": { + "label": "Stile orario", + "position": { + "label": "Posizione timestamp", + "description": "Posizione del timestamp sull'immagine (tl/tr/bl/br)." + }, + "format": { + "label": "Formato timestamp", + "description": "Stringa di formato data/ora utilizzata per i timestamp (codici di formato datetime di Python)." + }, + "color": { + "label": "Colore timestamp", + "description": "Valori di colore RGB per il testo del timestamp (tutti i valori da 0 a 255).", + "red": { + "label": "Rosso", + "description": "Componente rosso (0-255) per il colore del timestamp." + }, + "green": { + "label": "Verde", + "description": "Componente verde (0-255) per il colore del timestamp." + }, + "blue": { + "label": "Blu", + "description": "Componente blu (0-255) per il colore del timestamp." + } + }, + "thickness": { + "label": "Spessore timestamp", + "description": "Spessore della linea del testo del timestamp." + }, + "effect": { + "label": "Effetto timestamp", + "description": "Effetto visivo per il testo del timestamp (none, solid, shadow)." + } + }, + "database": { + "label": "Database", + "description": "Impostazioni per il database SQLite utilizzato da Frigate per memorizzare i metadati relativi agli oggetti tracciati e alle registrazioni.", + "path": { + "label": "Percorso del database", + "description": "Percorso del filesystem in cui verrà memorizzato il file del database Frigate SQLite." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "Impostazioni per il servizio di ritrasmissione integrato go2rtc utilizzato per la ritrasmissione e la traduzione di flussi dal vivo." + }, + "camera_mqtt": { + "label": "MQTT" + }, + "notifications": { + "label": "Notifiche", + "description": "Impostazioni per abilitare e controllare le notifiche per tutte le telecamere; possono essere sovrascritte per ogni singola telecamera.", + "enabled": { + "label": "Abilita le notifiche", + "description": "Abilita o disabilita le notifiche per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera." + }, + "email": { + "label": "Email di notifica", + "description": "Indirizzo email utilizzato per le notifiche push o richiesto da alcuni fornitori di servizi di notifica." + }, + "cooldown": { + "label": "Tempo di attesa tra notifiche", + "description": "Tempo di attesa (in secondi) tra le notifiche per evitare di inviare spam ai destinatari." + }, + "enabled_in_config": { + "label": "Stato delle notifiche originali", + "description": "Indica se le notifiche erano abilitate nella configurazione statica originale." + } + }, + "networking": { + "label": "Reti", + "description": "Impostazioni relative alla rete, come l'abilitazione di IPv6 per i dispositivi Frigate.", + "ipv6": { + "label": "Configurazione IPv6", + "description": "Impostazioni specifiche IPv6 per i servizi di rete Frigate.", + "enabled": { + "label": "Abilita IPv6", + "description": "Abilita il supporto IPv6 per i servizi Frigate (API e interfaccia utente) ove applicabile." + } + }, + "listen": { + "label": "Configurazione delle porte di ascolto", + "description": "Configurazione per le porte di ascolto interne ed esterne. Questa sezione è destinata agli utenti esperti. Per la maggior parte dei casi, si consiglia di modificare la sezione relativa alle porte nel file Docker Compose.", + "internal": { + "label": "Porta interna", + "description": "Porta di ascolto interna per Frigate (predefinita 5000)." + }, + "external": { + "label": "Porta esterna", + "description": "Porta di ascolto esterna per Frigate (predefinita 8971)." + } + } + }, + "tls": { + "label": "TLS", + "description": "Impostazioni TLS per gli endpoint web di Frigate (porta 8971).", + "enabled": { + "label": "Abilita TLS", + "description": "Abilita il protocollo TLS per l'interfaccia utente web e le API di Frigate sulla porta TLS configurata." + } + }, + "telemetry": { + "label": "Telemetria", + "description": "Opzioni di telemetria e statistiche di sistema, incluso il monitoraggio della GPU e della larghezza di banda di rete.", + "network_interfaces": { + "label": "Interfacce di rete", + "description": "Elenco dei prefissi dei nomi delle interfacce di rete da monitorare per le statistiche sulla larghezza di banda." + }, + "stats": { + "label": "Statistiche di sistema", + "description": "Opzioni per abilitare/disabilitare la raccolta di varie statistiche di sistema e della GPU.", + "amd_gpu_stats": { + "label": "Statistiche GPU AMD", + "description": "Abilita la raccolta delle statistiche della GPU AMD, se presente una GPU AMD." + }, + "intel_gpu_stats": { + "label": "Statistiche GPU Intel", + "description": "Abilita la raccolta delle statistiche della GPU Intel, se presente una GPU Intel." + }, + "network_bandwidth": { + "label": "Larghezza di banda di rete", + "description": "Abilita il monitoraggio della larghezza di banda di rete per singolo processo per i processi ffmpeg della telecamera e per i rilevatori (richiede capability specifiche)." + }, + "intel_gpu_device": { + "label": "Dispositivo GPU Intel", + "description": "Indirizzo bus PCI o percorso del dispositivo DRM (es. /dev/dri/card1) utilizzato per associare le statistiche della GPU Intel a un dispositivo specifico quando ne sono presenti molteplici." + } + }, + "version_check": { + "label": "Controllo versione", + "description": "Abilita un controllo in uscita per rilevare se è disponibile una versione più recente di Frigate." + } + }, + "birdseye": { + "label": "Birdseye", + "description": "Impostazioni per la vista composita Birdseye che unisce più flussi video di telecamere in un unico formato.", + "enabled": { + "label": "Abilita Birdseye", + "description": "Abilita o disabilita la funzione di visualizzazione Birdseye." + }, + "mode": { + "label": "Modalità di tracciamento", + "description": "Modalità per includere le telecamere in Birdseye: 'oggetti', 'movimento' o 'continuo'." + }, + "order": { + "label": "Posizione", + "description": "Posizione numerica che controlla l'ordine delle telecamere nella disposizione Birdseye." + } + }, + "model": { + "label": "Modello di rilevamento" + }, + "semantic_search": { + "label": "Ricerca semantica", + "triggers": { + "label": "Attivatori", + "friendly_name": { + "label": "Nome descrittivo", + "description": "Nome descrittivo opzionale visualizzato nell'interfaccia utente per questo attivatore." + }, + "description": "Azioni e criteri di corrispondenza per gli attivatori della ricerca semantica specifici della telecamera.", + "enabled": { + "label": "Abilita questo attivatore", + "description": "Abilita o disabilita questo attivatore della ricerca semantica." + }, + "type": { + "label": "Tipo di attivatore", + "description": "Tipo di attivatore: 'miniatura' (corrispondenza con immagine) o 'descrizione' (corrispondenza con testo)." + }, + "data": { + "label": "Contenuto dell'attivatore", + "description": "Frase di testo o ID della miniatura da confrontare con gli oggetti tracciati." + }, + "threshold": { + "label": "Soglia dell'attivatore", + "description": "Punteggio minimo di somiglianza (0-1) richiesto per attivare questo attivatore." + }, + "actions": { + "label": "Azioni dell'attivatore", + "description": "Elenco delle azioni da eseguire quando l'attivatore trova una corrispondenza (notification, sub_label, attribute)." + } + }, + "model_size": { + "label": "Dimensioni del modello" + } + }, + "lpr": { + "label": "Riconoscimento targhe", + "model_size": { + "label": "Dimensioni del modello" + }, + "description": "Impostazioni di riconoscimento delle targhe, incluse le soglie di rilevamento, la formattazione e le targhe conosciute.", + "enabled": { + "label": "Abilita riconoscimento delle targhe (LPR)" + }, + "expire_time": { + "label": "Scadenza in secondi", + "description": "Tempo in secondi trascorso dopo il quale una targa non identificata viene rimossa dal sistema di tracciamento (solo per telecamere LPR dedicate)." + }, + "min_area": { + "label": "Area minima della targa", + "description": "Area minima della targa (in pixel) richiesta per tentare il riconoscimento." + }, + "enhancement": { + "label": "Livello di miglioramento", + "description": "Livello di miglioramento (0-10) da applicare alle immagini delle targhe prima dell'OCR; valori più elevati potrebbero non migliorare sempre i risultati; i livelli superiori a 5 potrebbero funzionare solo con immagini di targhe scattate di notte e devono essere utilizzati con cautela." + } + }, + "classification": { + "label": "Classificazione oggetti", + "bird": { + "enabled": { + "label": "Classificazione uccelli" + } + } + }, + "genai": { + "roles": { + "label": "Ruoli" + }, + "model": { + "label": "Modello" + } + }, + "camera_ui": { + "description": "Visualizza l'ordine e la visibilità di questa telecamera nell'interfaccia utente. L'ordine influisce sul cruscotto predefinito. Per un controllo più granulare, utilizza i gruppi di telecamere.", + "order": { + "description": "L'ordine numerico viene utilizzato per ordinare le telecamere nell'interfaccia utente (cruscotto ed elenchi predefiniti); i numeri più grandi compaiono successivamente." + }, + "dashboard": { + "label": "Mostra nell'interfaccia utente", + "description": "Abilita o disabilita la visualizzazione di questa telecamera in ogni punto dell'interfaccia utente di Frigate. Disabilitando questa opzione, sarà necessario modificare manualmente la configurazione per visualizzare nuovamente la telecamera nell'interfaccia utente." + } + }, + "active_profile": { + "label": "Profilo attivo" + } +} diff --git a/web/public/locales/it/config/groups.json b/web/public/locales/it/config/groups.json new file mode 100644 index 0000000..72164c3 --- /dev/null +++ b/web/public/locales/it/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Rilevamento globale", + "sensitivity": "Sensibilità globale" + }, + "cameras": { + "detection": "Rilevamento", + "sensitivity": "Sensibilità" + } + }, + "timestamp_style": { + "global": { + "appearance": "Aspetto globale" + }, + "cameras": { + "appearance": "Aspetto" + } + }, + "motion": { + "global": { + "algorithm": "Algoritmo globale", + "sensitivity": "Sensibilità globale" + }, + "cameras": { + "sensitivity": "Sensibilità", + "algorithm": "Algoritmo" + } + }, + "snapshots": { + "global": { + "display": "Visualizzazione globale" + }, + "cameras": { + "display": "Visualizzazione" + } + }, + "detect": { + "global": { + "tracking": "Tracciamento globale", + "resolution": "Risoluzione globale" + }, + "cameras": { + "resolution": "Risoluzione", + "tracking": "Tracciamento" + } + }, + "objects": { + "global": { + "tracking": "Tracciamento globale", + "filtering": "Filtro globale" + }, + "cameras": { + "filtering": "Filtro", + "tracking": "Tracciamento" + } + }, + "record": { + "global": { + "events": "Eventi globali", + "retention": "Conservazione globale" + }, + "cameras": { + "events": "Eventi", + "retention": "Conservazione" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Parametri FFmpeg specifici per la telecamera" + } + } +} diff --git a/web/public/locales/it/config/validation.json b/web/public/locales/it/config/validation.json new file mode 100644 index 0000000..c6049d5 --- /dev/null +++ b/web/public/locales/it/config/validation.json @@ -0,0 +1,35 @@ +{ + "minimum": "Deve essere almeno {{limit}}", + "maximum": "Deve essere al massimo {{limit}}", + "exclusiveMinimum": "Deve essere maggiore di {{limit}}", + "exclusiveMaximum": "Deve essere minore di {{limit}}", + "minLength": "Deve essere almeno {{limit}} carattere(i)", + "maxLength": "Deve essere al massimo {{limit}} carattere(i)", + "minItems": "Deve contenere almeno {{limit}} elementi", + "maxItems": "Deve avere al massimo {{limit}} elementi", + "pattern": "Formato non valido", + "required": "Questo campo è obbligatorio", + "type": "Tipo di valore non valido", + "enum": "Deve essere uno dei valori consentiti", + "const": "Il valore non corrisponde alla costante prevista", + "uniqueItems": "Tutti gli elementi devono essere unici", + "format": "Formato non valido", + "additionalProperties": "Proprietà sconosciuta non consentita", + "oneOf": "Deve corrispondere esattamente a uno degli schemi consentiti", + "anyOf": "Deve corrispondere ad almeno uno degli schemi consentiti", + "proxy": { + "header_map": { + "roleHeaderRequired": "L'intestazione del ruolo è obbligatoria quando si configurano le mappature dei ruoli." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "Ciascun ruolo può essere assegnato a un solo flusso di ingresso.", + "detectRequired": "Ad almeno un flusso di ingresso deve essere assegnato il ruolo di 'rilevamento'.", + "hwaccelDetectOnly": "Solo il flusso di ingresso con il ruolo di rilevamento può definire argomenti di accelerazione hardware." + } + }, + "detect": { + "dimensionMustBeEven": "Deve essere un numero pari." + } +} diff --git a/web/public/locales/it/objects.json b/web/public/locales/it/objects.json new file mode 100644 index 0000000..230931d --- /dev/null +++ b/web/public/locales/it/objects.json @@ -0,0 +1,129 @@ +{ + "bird": "Uccello", + "clock": "Orologio", + "scissors": "Forbici", + "vehicle": "Veicolo", + "cat": "Gatto", + "boat": "Barca", + "horse": "Cavallo", + "bicycle": "Bicicletta", + "person": "Persona", + "car": "Automobile", + "motorcycle": "Motociclo", + "dog": "Cane", + "bark": "Abbaio", + "animal": "Animale", + "sheep": "Pecora", + "mouse": "Mouse", + "bear": "Orso", + "elephant": "Elefante", + "zebra": "Zebra", + "giraffe": "Giraffa", + "hat": "Cappello", + "umbrella": "Ombrello", + "train": "Treno", + "backpack": "Zaino", + "cow": "Mucca", + "airplane": "Aereo", + "bus": "Autobus", + "shoe": "Scarpa", + "skateboard": "Skateboard", + "kite": "Aquilone", + "oven": "Forno", + "sink": "Lavello", + "stop_sign": "Segnale di stop", + "raccoon": "Procione", + "postnl": "PostNL", + "nzpost": "NZPost", + "book": "Libro", + "frisbee": "Frisbee", + "laptop": "Portatile", + "knife": "Coltello", + "spoon": "Cucchiaio", + "bowl": "Ciotola", + "dhl": "DHL", + "banana": "Banana", + "carrot": "Carota", + "dining_table": "Tavolo da pranzo", + "hot_dog": "Hot Dog", + "mirror": "Specchio", + "microwave": "Microonde", + "toaster": "Tostapane", + "teddy_bear": "Orsacchiotto di peluche", + "hair_brush": "Spazzola per capelli", + "squirrel": "Scoiattolo", + "deer": "Cervo", + "robot_lawnmower": "Robot tagliaerba", + "waste_bin": "Cestino", + "on_demand": "Su richiesta", + "ups": "UPS", + "fedex": "FedEx", + "postnord": "PostNord", + "traffic_light": "Semaforo", + "fire_hydrant": "Idrante antincendio", + "street_sign": "Cartello stradale", + "parking_meter": "Parchimetro", + "bench": "Panca", + "eye_glasses": "Occhiali da vista", + "handbag": "Borsa a mano", + "tie": "Cravatta", + "suitcase": "Valigia", + "skis": "Sci", + "snowboard": "Snowboard", + "sports_ball": "Palla sportiva", + "baseball_bat": "Mazza da baseball", + "baseball_glove": "Guanto da baseball", + "surfboard": "Tavola da surf", + "tennis_racket": "Racchetta da tennis", + "bottle": "Bottiglia", + "plate": "Piatto", + "wine_glass": "Bicchiere da vino", + "cup": "Tazza", + "fork": "Forchetta", + "apple": "Mela", + "sandwich": "Panino", + "orange": "Arancia", + "broccoli": "Broccoli", + "pizza": "Pizza", + "donut": "Ciambella", + "cake": "Torta", + "chair": "Sedia", + "couch": "Divano", + "door": "Porta", + "keyboard": "Tastiera", + "potted_plant": "Pianta in vaso", + "bed": "Letto", + "window": "Finestra", + "desk": "Scrivania", + "toilet": "Toilette", + "tv": "Televisione", + "remote": "Telecomando", + "cell_phone": "Telefono cellulare", + "blender": "Miscelatore", + "refrigerator": "Frigorifero", + "hair_dryer": "Asciugacapelli", + "toothbrush": "Spazzolino da denti", + "vase": "Vaso", + "fox": "Volpe", + "goat": "Capra", + "rabbit": "Coniglio", + "face": "Viso", + "license_plate": "Targa", + "package": "Pacchetto", + "bbq_grill": "Griglia per barbecue", + "amazon": "Amazon", + "usps": "USPS", + "an_post": "An Post", + "purolator": "Purolator", + "gls": "GLS", + "dpd": "DPD", + "canada_post": "Canada Post", + "royal_mail": "Royal Mail", + "school_bus": "Autobus scolastico", + "skunk": "Puzzola", + "kangaroo": "Canguro", + "baby": "Bambino", + "baby_stroller": "Passeggino per bambini", + "rickshaw": "Risciò", + "rodent": "Roditore" +} diff --git a/web/public/locales/it/views/chat.json b/web/public/locales/it/views/chat.json new file mode 100644 index 0000000..dfe5166 --- /dev/null +++ b/web/public/locales/it/views/chat.json @@ -0,0 +1,72 @@ +{ + "documentTitle": "Chat - Frigate", + "title": "Chat Frigate", + "subtitle": "Il tuo assistente IA per la gestione e l'analisi delle telecamere", + "placeholder": "Chiedi qualsiasi cosa...", + "error": "Si è verificato un errore. Riprova.", + "processing": "Elaborazione in corso...", + "toolsUsed": "Utilizzato: {{tools}}", + "showTools": "Mostra strumenti ({{count}})", + "hideTools": "Nascondi strumenti", + "call": "Chiama", + "result": "Risultato", + "arguments": "Argomenti:", + "response": "Risposta:", + "attachment_chip_label": "{{label}} sulla {{camera}}", + "attachment_chip_remove": "Rimuovi allegato", + "open_in_explore": "Apri in Esplora", + "attach_event_aria": "Allega evento {{eventId}}", + "attachment_picker_paste_label": "Oppure incolla ID dell'evento", + "attachment_picker_attach": "Allega", + "attachment_picker_placeholder": "Allega un evento", + "quick_reply_find_similar": "Trova avvistamenti simili", + "quick_reply_tell_me_more": "Raccontami di più su questo", + "quick_reply_when_else": "In quali altre occasioni è stato avvistato?", + "quick_reply_find_similar_text": "Trova avvistamenti simili a questo.", + "quick_reply_tell_me_more_text": "Raccontami di più su questo.", + "quick_reply_when_else_text": "Quando è stato osservato in altre volte?", + "anchor": "Riferimento", + "similarity_score": "Somiglianza", + "no_similar_objects_found": "Nessun oggetto simile trovato.", + "semantic_search_required": "Per trovare oggetti simili è necessario abilitare la ricerca semantica.", + "send": "Invia", + "suggested_requests": "Prova a chiedere:", + "starting_requests": { + "show_recent_events": "Mostra gli eventi recenti", + "show_camera_status": "Mostra lo stato della telecamera", + "recap": "Cosa è successo mentre ero via?", + "watch_camera": "Guarda la telecamera per attività" + }, + "starting_requests_prompts": { + "show_recent_events": "Mostrami gli eventi recenti dell'ultima ora", + "show_camera_status": "Qual è lo stato attuale delle mie telecamere?", + "recap": "Cosa è successo mentre ero via?", + "watch_camera": "Controlla la porta d'ingresso e fammi sapere se arriva qualcuno" + }, + "new_chat": "Nuova chat", + "settings": { + "title": "Impostazioni chat", + "show_stats": { + "title": "Mostra statistiche", + "desc": "Mostra la frequenza di generazione e la dimensione del contesto per le risposte in chat.", + "while_generating": "Durante la generazione", + "always": "Sempre" + }, + "auto_scroll": { + "title": "Scorrimento automatico", + "desc": "Segui i nuovi messaggi non appena arrivano." + } + }, + "stats": { + "context": "{{tokens}} token", + "tokens_per_second": "{{rate}} t/s" + }, + "reasoning": { + "active": "Ragionamento…", + "show": "Mostra il ragionamento", + "hide": "Nascondi il ragionamento" + }, + "thinking": { + "toggle": "Alterna ragionamento" + } +} diff --git a/web/public/locales/it/views/classificationModel.json b/web/public/locales/it/views/classificationModel.json new file mode 100644 index 0000000..c5f0f75 --- /dev/null +++ b/web/public/locales/it/views/classificationModel.json @@ -0,0 +1,206 @@ +{ + "documentTitle": "Modelli di classificazione", + "button": { + "deleteClassificationAttempts": "Elimina immagini di classificazione", + "renameCategory": "Rinomina classe", + "deleteCategory": "Elimina classe", + "deleteImages": "Elimina immagini", + "trainModel": "Modello di addestramento", + "addClassification": "Aggiungi classificazione", + "deleteModels": "Elimina modelli", + "editModel": "Modifica modello" + }, + "toast": { + "success": { + "deletedCategory_one": "{{count}} classe eliminata", + "deletedCategory_many": "{{count}} classi eliminate", + "deletedCategory_other": "{{count}} classi eliminate", + "deletedImage_one": "{{count}} immagine eliminata", + "deletedImage_many": "{{count}} immagini eliminate", + "deletedImage_other": "{{count}} immagini eliminate", + "categorizedImage": "Immagine classificata con successo", + "trainedModel": "Modello addestrato con successo.", + "trainingModel": "Avviato con successo l'addestramento del modello.", + "deletedModel_one": "Eliminato con successo {{count}} modello", + "deletedModel_many": "Eliminati con successo {{count}} modelli", + "deletedModel_other": "Eliminati con successo {{count}} modelli", + "updatedModel": "Configurazione del modello aggiornata correttamente", + "renamedCategory": "Classe rinominata correttamente in {{name}}", + "reclassifiedImage": "Immagine riclassificata con successo" + }, + "error": { + "deleteImageFailed": "Impossibile eliminare: {{errorMessage}}", + "deleteCategoryFailed": "Impossibile eliminare la classe: {{errorMessage}}", + "categorizeFailed": "Impossibile categorizzare l'immagine: {{errorMessage}}", + "trainingFailed": "Addestramento del modello fallito. Controlla i registri di Frigate per i dettagli.", + "deleteModelFailed": "Impossibile eliminare il modello: {{errorMessage}}", + "updateModelFailed": "Impossibile aggiornare il modello: {{errorMessage}}", + "trainingFailedToStart": "Impossibile avviare l'addestramento del modello: {{errorMessage}}", + "renameCategoryFailed": "Impossibile rinominare la classe: {{errorMessage}}", + "reclassifyFailed": "Impossibile riclassificare l'immagine: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Elimina classe", + "desc": "Vuoi davvero eliminare la classe {{name}}? Questa operazione eliminerà definitivamente tutte le immagini associate e richiederà un nuovo addestramento del modello.", + "minClassesTitle": "Impossibile eliminare la classe", + "minClassesDesc": "Un modello di classificazione deve avere almeno 2 classi. Aggiungi un'altra classe prima di eliminare questa." + }, + "deleteDatasetImages": { + "title": "Elimina immagini della base dati", + "desc_one": "Vuoi davvero eliminare {{count}} immagine da {{dataset}}? Questa azione non può essere annullata e richiederà un nuovo addestramento del modello.", + "desc_many": "Vuoi davvero eliminare {{count}} immagini da {{dataset}}? Questa azione non può essere annullata e richiederà un nuovo addestramento del modello.", + "desc_other": "Vuoi davvero eliminare {{count}} immagini da {{dataset}}? Questa azione non può essere annullata e richiederà un nuovo addestramento del modello." + }, + "deleteTrainImages": { + "title": "Elimina le immagini di addestramento", + "desc_one": "Vuoi davvero eliminare {{count}} immagine? Questa azione non può essere annullata.", + "desc_many": "Vuoi davvero eliminare {{count}} immagini? Questa azione non può essere annullata.", + "desc_other": "Vuoi davvero eliminare {{count}} immagini? Questa azione non può essere annullata." + }, + "renameCategory": { + "title": "Rinomina classe", + "desc": "Inserisci un nuovo nome per {{name}}. Sarà necessario riaddestrare il modello affinché la modifica del nome abbia effetto." + }, + "description": { + "invalidName": "Nome non valido. I nomi possono contenere solo lettere, numeri, spazi, apostrofi, caratteri di sottolineatura e trattini." + }, + "train": { + "title": "Classificazioni recenti", + "titleShort": "Recente", + "aria": "Seleziona classificazioni recenti" + }, + "categories": "Classi", + "createCategory": { + "new": "Crea nuova classe" + }, + "categorizeImageAs": "Classifica immagine come:", + "categorizeImage": "Classifica immagine", + "noModels": { + "object": { + "title": "Nessun modello di classificazione degli oggetti", + "description": "Crea un modello personalizzato per classificare gli oggetti rilevati.", + "buttonText": "Crea modello oggetto" + }, + "state": { + "title": "Nessun modello di classificazione dello stato", + "description": "Crea un modello personalizzato per monitorare e classificare i cambiamenti di stato in aree specifiche della telecamera.", + "buttonText": "Crea modello di stato" + } + }, + "wizard": { + "title": "Crea nuova classificazione", + "steps": { + "nameAndDefine": "Nome e definizione", + "stateArea": "Area di stato", + "chooseExamples": "Scegli esempi" + }, + "step1": { + "description": "I modelli di stato monitorano le aree fisse delle telecamere per rilevare eventuali cambiamenti (ad esempio, porta aperta/chiusa). I modelli di oggetti aggiungono classificazioni agli oggetti rilevati (ad esempio, animali noti, addetti alle consegne, ecc.).", + "name": "Nome", + "namePlaceholder": "Inserisci il nome del modello...", + "type": "Tipo", + "typeState": "Stato", + "typeObject": "Oggetto", + "objectLabel": "Etichetta oggetto", + "objectLabelPlaceholder": "Seleziona il tipo di oggetto...", + "classificationType": "Tipo di classificazione", + "classificationTypeTip": "Scopri i tipi di classificazione", + "classificationTypeDesc": "Le sottoetichette aggiungono testo aggiuntivo all'etichetta dell'oggetto (ad esempio, \"Persona: UPS\"). Gli attributi sono metadati ricercabili, archiviati separatamente nei metadati dell'oggetto.", + "classificationSubLabel": "Etichetta secondaria", + "classificationAttribute": "Attributo", + "classes": "Classi", + "classesTip": "Scopri di più sulle classi", + "classesStateDesc": "Definisci i diversi stati in cui può trovarsi l'area della tua telecamera. Ad esempio: \"aperto\" e \"chiuso\" per una porta del garage.", + "classesObjectDesc": "Definisci le diverse categorie in cui classificare gli oggetti rilevati. Ad esempio: \"corriere\", \"residente\", \"straniero\" per la classificazione delle persone.", + "classPlaceholder": "Inserisci il nome della classe...", + "errors": { + "nameRequired": "Il nome del modello è obbligatorio", + "nameLength": "Il nome del modello deve contenere al massimo 64 caratteri", + "nameOnlyNumbers": "Il nome del modello non può contenere solo numeri", + "classRequired": "È richiesta almeno 1 classe", + "classesUnique": "I nomi delle classi devono essere univoci", + "stateRequiresTwoClasses": "I modelli di stato richiedono almeno 2 classi", + "objectLabelRequired": "Seleziona un'etichetta per l'oggetto", + "objectTypeRequired": "Seleziona un tipo di classificazione", + "noneNotAllowed": "La classe 'nessuno' non è consentita" + }, + "states": "Stati" + }, + "step2": { + "description": "Seleziona le telecamere e definisci l'area da monitorare per ciascuna telecamera. Il modello classificherà lo stato di queste aree.", + "cameras": "Telecamere", + "selectCamera": "Seleziona telecamera", + "noCameras": "Fai clic su + per aggiungere telecamere", + "selectCameraPrompt": "Selezionare una telecamera dall'elenco per definire la sua area di monitoraggio" + }, + "step3": { + "selectImagesPrompt": "Seleziona tutte le immagini con: {{className}}", + "selectImagesDescription": "Clicca sulle immagini per selezionarle. Clicca su Continua quando hai finito con questa classe.", + "generating": { + "title": "Generazione di immagini campione", + "description": "Frigate sta estraendo immagini rappresentative dalle registrazioni. L'operazione potrebbe richiedere qualche istante..." + }, + "training": { + "title": "Modello di addestramento", + "description": "Il tuo modello è in fase di addestramento in sottofondo. Chiudi questa finestra di dialogo e il tuo modello inizierà a funzionare non appena l'addestramento sarà completato." + }, + "retryGenerate": "Riprova generazione", + "noImages": "Nessuna immagine campione generata", + "classifying": "Classificazione e addestramento...", + "trainingStarted": "Addestramento iniziato con successo", + "errors": { + "noCameras": "Nessuna telecamera configurata", + "noObjectLabel": "Nessuna etichetta oggetto selezionata", + "generateFailed": "Impossibile generare esempi: {{error}}", + "generationFailed": "Generazione fallita. Per favore riprova.", + "classifyFailed": "Impossibile classificare le immagini: {{error}}" + }, + "generateSuccess": "Immagini campione generate correttamente", + "allImagesRequired_one": "Classifica tutte le immagini. Rimane {{count}} immagine.", + "allImagesRequired_many": "Classifica tutte le immagini. Rimangono {{count}} immagini.", + "allImagesRequired_other": "Classifica tutte le immagini. Rimangono {{count}} immagini.", + "modelCreated": "Modello creato correttamente. Utilizza la vista Classificazioni recenti per aggiungere immagini per gli stati mancanti, quindi addestrare il modello.", + "missingStatesWarning": { + "title": "Esempi di classi mancanti", + "description": "Non tutte le classi hanno esempi. Prova a generare nuovi esempi per trovare la classe mancante oppure continua e usa la vista Classificazioni recenti per aggiungere immagini in seguito." + }, + "refreshExamples": "Genera nuovi esempi", + "refreshConfirm": { + "title": "Generare nuovi esempi?", + "description": "Questo genererà una nuova serie di immagini e cancellerà tutte le selezioni, comprese le classi precedenti. Dovrai riselezionare gli esempi per tutte le classi." + } + } + }, + "deleteModel": { + "title": "Elimina modello di classificazione", + "single": "Vuoi davvero eliminare {{name}}? Questa operazione eliminerà definitivamente tutti i dati associati, comprese le immagini e i dati di allenamento. Questa azione non può essere annullata.", + "desc_one": "Vuoi davvero eliminare {{count}} modello? Questa operazione eliminerà definitivamente tutti i dati associati, comprese le immagini e i dati di addestramento. Questa azione non può essere annullata.", + "desc_many": "Vuoi davvero eliminare {{count}} modelli? Questa operazione eliminerà definitivamente tutti i dati associati, comprese le immagini e i dati di addestramento. Questa azione non può essere annullata.", + "desc_other": "Vuoi davvero eliminare {{count}} modelli? Questa operazione eliminerà definitivamente tutti i dati associati, comprese le immagini e i dati di addestramento. Questa azione non può essere annullata." + }, + "menu": { + "objects": "Oggetti", + "states": "Stati" + }, + "details": { + "scoreInfo": "Il punteggio rappresenta la confidenza media della classificazione in tutti i rilevamenti di questo oggetto.", + "none": "Nessuno", + "unknown": "Sconosciuto" + }, + "edit": { + "title": "Modifica modello di classificazione", + "descriptionState": "Modifica le classi per questo modello di classificazione dello stato. Le modifiche richiederanno un nuovo addestramento del modello.", + "descriptionObject": "Modifica il tipo di oggetto e il tipo di classificazione per questo modello di classificazione degli oggetti.", + "stateClassesInfo": "Nota: la modifica delle classi di stato richiede il riaddestramento del modello con le classi aggiornate." + }, + "tooltip": { + "trainingInProgress": "Il modello è attualmente in addestramento", + "modelNotReady": "Il modello non è pronto per l'addestramento", + "noNewImages": "Nessuna nuova immagine da addestrare. Classifica prima più immagini nel database.", + "noChanges": "Nessuna modifica al database dall'ultimo addestramento." + }, + "none": "Nessuno", + "reclassifyImageAs": "Riclassifica immagine come:", + "reclassifyImage": "Riclassifica immagine" +} diff --git a/web/public/locales/it/views/configEditor.json b/web/public/locales/it/views/configEditor.json new file mode 100644 index 0000000..f53aaed --- /dev/null +++ b/web/public/locales/it/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Editor di configurazione", + "documentTitle": "Editor di configurazione - Frigate", + "copyConfig": "Copia configurazione", + "saveAndRestart": "Salva e riavvia", + "saveOnly": "Salva soltanto", + "toast": { + "success": { + "copyToClipboard": "Configurazione copiata negli appunti." + }, + "error": { + "savingError": "Errore durante il salvataggio della configurazione" + } + }, + "confirm": "Vuoi uscire senza salvare?", + "safeConfigEditor": "Editor di configurazione (modalità provvisoria)", + "safeModeDescription": "Frigate è in modalità provvisoria a causa di un errore di convalida della configurazione." +} diff --git a/web/public/locales/it/views/events.json b/web/public/locales/it/views/events.json new file mode 100644 index 0000000..45a4664 --- /dev/null +++ b/web/public/locales/it/views/events.json @@ -0,0 +1,95 @@ +{ + "alerts": "Avvisi", + "detections": "Rilevamenti", + "motion": { + "label": "Movimento", + "only": "Solo movimenti" + }, + "empty": { + "alert": "Non ci sono avvisi da rivedere", + "detection": "Non ci sono rilevamenti da rivedere", + "motion": "Nessun dato di movimento trovato", + "recordingsDisabled": { + "description": "Gli elementi di revisione possono essere creati per una telecamera solo quando le registrazioni sono abilitate per quella telecamera.", + "title": "Le registrazioni devono essere abilitate" + } + }, + "newReviewItems": { + "label": "Visualizza i nuovi elementi da rivedere", + "button": "Nuovi elementi da rivedere" + }, + "markTheseItemsAsReviewed": "Segna questi elementi come visti", + "markAsReviewed": "Segna come visto", + "documentTitle": "Revisiona - Frigate", + "allCameras": "Tutte le telecamere", + "timeline": { + "label": "Linea temporale" + }, + "timeline.aria": "Seleziona la cronologia", + "events": { + "label": "Eventi", + "aria": "Seleziona eventi", + "noFoundForTimePeriod": "Nessun evento trovato per questo intervallo." + }, + "recordings": { + "documentTitle": "Registrazioni - Frigate", + "invalidSharedLink": "Impossibile aprire il collegamento alla registrazione con orario a causa di un errore di analisi.", + "invalidSharedCamera": "Impossibile aprire il collegamento alla registrazione con orario a causa di una telecamera sconosciuta o non autorizzata." + }, + "calendarFilter": { + "last24Hours": "Ultime 24 ore" + }, + "camera": "Telecamera", + "selected": "{{count}} selezionati", + "selected_one": "{{count}} selezionati", + "selected_other": "{{count}} selezionati", + "detected": "rilevato", + "suspiciousActivity": "Attività sospetta", + "threateningActivity": "Attività minacciosa", + "detail": { + "noDataFound": "Nessun dato dettagliato da rivedere", + "aria": "Abilita/disabilita la visualizzazione dettagliata", + "trackedObject_one": "{{count}} oggetto", + "trackedObject_other": "{{count}} oggetti", + "noObjectDetailData": "Non sono disponibili dati dettagliati sull'oggetto.", + "label": "Dettaglio", + "settings": "Impostazioni di visualizzazione dettagliata", + "alwaysExpandActive": { + "title": "Espandi sempre attivo", + "desc": "Espandere sempre i dettagli dell'oggetto dell'elemento di revisione attivo quando disponibili." + } + }, + "objectTrack": { + "trackedPoint": "Punto tracciato", + "clickToSeek": "Premi per cercare in questo momento" + }, + "zoomIn": "Ingrandisci", + "zoomOut": "Rimpicciolisci", + "normalActivity": "Normale", + "needsReview": "Necessita revisione", + "securityConcern": "Rischio per la sicurezza", + "select_all": "Tutti", + "motionSearch": { + "menuItem": "Ricerca movimento", + "openMenu": "Opzioni telecamera" + }, + "motionPreviews": { + "title": "Anteprime di movimento: {{camera}}", + "mobileSettingsTitle": "Impostazioni di anteprima del movimento", + "mobileSettingsDesc": "Regola la velocità di riproduzione e la luminosità, poi scegli una data per rivedere i filmati che mostrano solo il movimento.", + "dim": "Attenua", + "dimAria": "Regola l'intensità della luce", + "dimDesc": "Aumenta l'attenuazione per migliorare la visibilità delle aree in movimento.", + "speed": "Velocità", + "speedAria": "Seleziona la velocità di riproduzione dell'anteprima", + "speedDesc": "Scegli la velocità di riproduzione dei video di anteprima.", + "back": "Indietro", + "empty": "Nessuna anteprima disponibile", + "noPreview": "Anteprima non disponibile", + "seekAria": "Cerca il riproduttore {{camera}} a {{time}}", + "filter": "Filtro", + "filterDesc": "Seleziona le aree per visualizzare solo i video con movimento in quelle regioni.", + "filterClear": "Pulisci", + "menuItem": "Visualizza le anteprime del movimento" + } +} diff --git a/web/public/locales/it/views/explore.json b/web/public/locales/it/views/explore.json new file mode 100644 index 0000000..e08626b --- /dev/null +++ b/web/public/locales/it/views/explore.json @@ -0,0 +1,319 @@ +{ + "generativeAI": "IA Generativa", + "documentTitle": "Esplora - Frigate", + "exploreIsUnavailable": { + "title": "Esplora non è disponibile", + "embeddingsReindexing": { + "context": "Potrai usare Esplora dopo che l'incorporamento degli oggetti tracciati avrà terminato la ricostruzione dell'indice.", + "step": { + "descriptionsEmbedded": "Descrizioni incorporate: ", + "thumbnailsEmbedded": "Miniature incorporate: ", + "trackedObjectsProcessed": "Oggetti tracciati elaborati: " + }, + "startingUp": "Avvio…", + "estimatedTime": "Tempo rimanente stimato:", + "finishingShortly": "Completamento a breve" + }, + "downloadingModels": { + "setup": { + "textModel": "Modello di testo", + "visionModel": "Modello di visione", + "visionModelFeatureExtractor": "Estrattore di funzionalità del modello di visione", + "textTokenizer": "Tokenizzatore di testo" + }, + "tips": { + "documentation": "Leggi la documentazione", + "context": "Una volta scaricati i modelli, potrebbe essere necessario reindicizzare gli incorporamenti degli oggetti tracciati." + }, + "error": "Si è verificato un errore. Controlla i registri di Frigate.", + "context": "Frigate sta scaricando i modelli di incorporamento necessari per supportare la funzione di Ricerca Semantica. L'operazione potrebbe richiedere diversi minuti, a seconda della velocità della connessione di rete." + } + }, + "details": { + "timestamp": "Orario", + "snapshotScore": { + "label": "Punteggio istantanea" + }, + "regenerateFromSnapshot": "Rigenera da istantanea", + "item": { + "tips": { + "mismatch_one": "E' stato rilevato {{count}} oggetto non disponibile e incluse in questo elemento visto. Tale oggetto non è stato classificato come avviso o rilevamento oppure è già stato ripulito/eliminato.", + "mismatch_many": "Sono stati rilevati {{count}} oggetti non disponibili e inclusi in questo elemento visto. Tali oggetti non sono stati classificati come avvisi o rilevamenti oppure sono già stati ripuliti/eliminati.", + "mismatch_other": "Sono stati rilevati {{count}} oggetti non disponibili e inclusi in questo elemento visto. Tali oggetti non sono stati classificati come avvisi o rilevamenti oppure sono già stati ripuliti/eliminati.", + "hasMissingObjects": "Modifica la configurazione se vuoi che Frigate salvi gli oggetti tracciati per le seguenti etichette: {{objects}}" + }, + "title": "Dettagli dell'elemento visto", + "desc": "Dettagli dell'elemento visto", + "button": { + "share": "Condividi questo elemento visto", + "viewInExplore": "Visualizza in Esplora" + }, + "toast": { + "success": { + "regenerate": "È stata richiesta una nuova descrizione a {{provider}}. A seconda della velocità del tuo provider, la rigenerazione della nuova descrizione potrebbe richiedere del tempo.", + "updatedSublabel": "Sottoetichetta aggiornata correttamente.", + "updatedLPR": "Targa aggiornata con successo.", + "audioTranscription": "Trascrizione audio richiesta con successo. A seconda della velocità del server Frigate, la trascrizione potrebbe richiedere del tempo.", + "updatedAttributes": "Attributi aggiornati correttamente." + }, + "error": { + "regenerate": "Impossibile chiamare {{provider}} per una nuova descrizione: {{errorMessage}}", + "updatedSublabelFailed": "Impossibile aggiornare la sottoetichetta: {{errorMessage}}", + "updatedLPRFailed": "Impossibile aggiornare la targa: {{errorMessage}}", + "audioTranscription": "Impossibile richiedere la trascrizione audio: {{errorMessage}}", + "updatedAttributesFailed": "Impossibile aggiornare gli attributi: {{errorMessage}}" + } + } + }, + "zones": "Zone", + "description": { + "label": "Descrizione", + "placeholder": "Descrizione dell'oggetto tracciato", + "aiTips": "Frigate non richiederà una descrizione al tuo fornitore di Intelligenza Artificiale Generativa finché non sarà terminato il ciclo di vita dell'oggetto tracciato." + }, + "label": "Etichetta", + "editSubLabel": { + "title": "Modifica sottoetichetta", + "desc": "Inserisci una nuova sottoetichetta per questa {{label}}", + "descNoLabel": "Inserisci una nuova sottoetichetta per questo oggetto tracciato" + }, + "editLPR": { + "title": "Modifica targa", + "desc": "Inserisci un nuovo valore della targa per questa {{label}}", + "descNoLabel": "Inserisci un nuovo valore di targa per questo oggetto tracciato" + }, + "topScore": { + "label": "Punteggio massimo", + "info": "Il punteggio massimo è il punteggio mediano più alto per l'oggetto tracciato, quindi potrebbe differire dal punteggio mostrato nella miniatura del risultato della ricerca." + }, + "recognizedLicensePlate": "Targa riconosciuta", + "estimatedSpeed": "Velocità stimata", + "objects": "Oggetti", + "camera": "Telecamera", + "button": { + "findSimilar": "Trova simili", + "regenerate": { + "title": "Rigenera", + "label": "Rigenera la descrizione dell'oggetto tracciato" + } + }, + "expandRegenerationMenu": "Espandi il menu di rigenerazione", + "regenerateFromThumbnails": "Rigenera dalle miniature", + "tips": { + "descriptionSaved": "Descrizione salvata correttamente", + "saveDescriptionFailed": "Impossibile aggiornare la descrizione: {{errorMessage}}" + }, + "score": { + "label": "Punteggio" + }, + "editAttributes": { + "title": "Modifica attributi", + "desc": "Seleziona gli attributi di classificazione per questa {{label}}" + }, + "attributes": "Attributi di classificazione", + "title": { + "label": "Titolo" + }, + "scoreInfo": "Informazioni sul punteggio" + }, + "objectLifecycle": { + "annotationSettings": { + "offset": { + "tips": "SUGGERIMENTO: immagina un filmato evento con una persona che cammina da sinistra verso destra. Se il riquadro di delimitazione della cronologia dell'evento si trova costantemente a sinistra della persona, il valore dovrebbe essere diminuito. Analogamente, se una persona cammina da sinistra verso destra e il riquadro di delimitazione si trova costantemente davanti alla persona, il valore dovrebbe essere aumentato.", + "desc": "Questi dati provengono dal flusso di rilevamento della telecamera, ma vengono sovrapposti alle immagini del flusso di registrazione. È improbabile che i due flussi siano perfettamente sincronizzati. Di conseguenza, il riquadro di delimitazione e il filmato non saranno perfettamente allineati. Tuttavia, è possibile utilizzare il campo annotation_offset per correggere questo problema.", + "label": "Compensazione di annotazione", + "documentation": "Leggi la documentazione ", + "millisecondsToOffset": "Millisecondi per compensare il rilevamento delle annotazioni. Predefinito: 0", + "toast": { + "success": "La compensazione di annotazione per {{camera}} è stato salvato nel file di configurazione. Riavvia Frigate per applicare le modifiche." + } + }, + "showAllZones": { + "title": "Mostra tutte le zone", + "desc": "Mostra sempre le zone nei fotogrammi in cui gli oggetti sono entrati in una zona." + }, + "title": "Impostazioni di annotazione" + }, + "lifecycleItemDesc": { + "heard": "{{label}} sentito", + "attribute": { + "faceOrLicense_plate": "{{attribute}} rilevato per {{label}}", + "other": "{{label}} riconosciuto come {{attribute}}" + }, + "gone": "{{label}} lasciato", + "external": "{{label}} rilevato", + "visible": "{{label}} rilevato", + "entered_zone": "{{label}} è entrato in {{zones}}", + "active": "{{label}} è diventato attivo", + "stationary": "{{label}} è diventato stazionario", + "header": { + "ratio": "Rapporto", + "area": "Area", + "zones": "Zone" + } + }, + "title": "Ciclo di vita dell'oggetto", + "createObjectMask": "Crea maschera oggetto", + "noImageFound": "Nessuna immagine trovata per questo orario.", + "adjustAnnotationSettings": "Regola le impostazioni di annotazione", + "scrollViewTips": "Scorri per visualizzare i momenti più significativi del ciclo di vita di questo oggetto.", + "autoTrackingTips": "Le posizioni dei riquadri di delimitazione non saranno precise per le telecamere con tracciamento automatico.", + "carousel": { + "previous": "Diapositiva precedente", + "next": "Diapositiva successiva" + }, + "count": "{{first}} di {{second}}", + "trackedPoint": "Punto tracciato" + }, + "type": { + "snapshot": "istantanea", + "object_lifecycle": "ciclo di vita dell'oggetto", + "details": "dettagli", + "video": "video", + "thumbnail": "miniatura", + "tracking_details": "dettagli di tracciamento" + }, + "itemMenu": { + "downloadSnapshot": { + "label": "Scarica istantanea", + "aria": "Scarica istantanea" + }, + "viewInHistory": { + "label": "Visualizza in Storico", + "aria": "Visualizza in Storico" + }, + "deleteTrackedObject": { + "label": "Elimina questo oggetto tracciato" + }, + "downloadVideo": { + "label": "Scarica video", + "aria": "Scarica video" + }, + "viewObjectLifecycle": { + "label": "Visualizza il ciclo di vita dell'oggetto", + "aria": "Mostra il ciclo di vita dell'oggetto" + }, + "findSimilar": { + "label": "Trova simili", + "aria": "Trova oggetti tracciati simili" + }, + "submitToPlus": { + "label": "Invia a Frigate+", + "aria": "Invia a Frigate Plus" + }, + "addTrigger": { + "label": "Aggiungi attivatore", + "aria": "Aggiungi un attivatore per questo oggetto tracciato" + }, + "audioTranscription": { + "label": "Trascrivere", + "aria": "Richiedi la trascrizione audio" + }, + "showObjectDetails": { + "label": "Mostra il percorso dell'oggetto" + }, + "hideObjectDetails": { + "label": "Nascondi il percorso dell'oggetto" + }, + "viewTrackingDetails": { + "label": "Visualizza i dettagli di tracciamento", + "aria": "Mostra i dettagli di tracciamento" + }, + "downloadCleanSnapshot": { + "label": "Scarica istantanea pulita", + "aria": "Scarica istantanea pulita" + }, + "debugReplay": { + "label": "Riproduzione di correzioni", + "aria": "Visualizza questo oggetto tracciato nella vista di riproduzione di correzioni" + }, + "more": { + "aria": "Altri" + } + }, + "dialog": { + "confirmDelete": { + "desc": "L'eliminazione di questo oggetto tracciato rimuove l'istantanea, eventuali incorporamenti salvati e tutte le voci associate ai dettagli di tracciamento. Il filmato registrato di questo oggetto tracciato nella vista Storico NON verrà eliminato.

    Vuoi davvero procedere?", + "title": "Conferma eliminazione" + }, + "toast": { + "error": "Errore durante l'eliminazione di questo oggetto tracciato: {{errorMessage}}" + } + }, + "trackedObjectDetails": "Dettagli dell'oggetto tracciato", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "error": "Impossibile eliminare l'oggetto tracciato: {{errorMessage}}", + "success": "Oggetto tracciato eliminato correttamente." + } + }, + "tooltip": "Corrispondenza {{type}} al {{confidence}}%", + "previousTrackedObject": "Oggetto tracciato in precedenza", + "nextTrackedObject": "Prossimo oggetto tracciato" + }, + "trackedObjectsCount_one": "{{count}} oggetto tracciato ", + "trackedObjectsCount_many": "{{count}} oggetti tracciati ", + "trackedObjectsCount_other": "{{count}} oggetti tracciati ", + "fetchingTrackedObjectsFailed": "Errore durante il recupero degli oggetti tracciati: {{errorMessage}}", + "noTrackedObjects": "Nessun oggetto tracciato trovato", + "exploreMore": "Esplora altri oggetti {{label}}", + "aiAnalysis": { + "title": "Analisi IA" + }, + "concerns": { + "label": "Preoccupazioni" + }, + "trackingDetails": { + "title": "Dettagli di tracciamento", + "noImageFound": "Nessuna immagine trovata per questo orario.", + "createObjectMask": "Crea maschera oggetto", + "adjustAnnotationSettings": "Regola le impostazioni di annotazione", + "scrollViewTips": "Clicca per visualizzare i momenti più significativi del ciclo di vita di questo oggetto.", + "autoTrackingTips": "Le posizioni dei riquadri di delimitazione saranno imprecise per le telecamere con tracciamento automatico.", + "count": "{{first}} di {{second}}", + "trackedPoint": "Punto tracciato", + "lifecycleItemDesc": { + "visible": "{{label}} rilevato", + "entered_zone": "{{label}} è entrato in {{zones}}", + "active": "{{label}} è diventato attivo", + "stationary": "{{label}} è diventato stazionario", + "attribute": { + "faceOrLicense_plate": "{{attribute}} rilevato per {{label}}", + "other": "{{label}} riconosciuto come {{attribute}}" + }, + "gone": "{{label}} lasciato", + "heard": "{{label}} sentito", + "external": "{{label}} rilevato", + "header": { + "zones": "Zone", + "ratio": "Rapporto", + "area": "Area", + "score": "Punteggio", + "computedScore": "Punteggio calcolato", + "topScore": "Punteggio massimo", + "toggleAdvancedScores": "Attiva/disattiva i punteggi avanzati" + } + }, + "annotationSettings": { + "title": "Impostazioni di annotazione", + "showAllZones": { + "title": "Mostra tutte le zone", + "desc": "Mostra sempre le zone nei fotogrammi in cui gli oggetti sono entrati in una zona." + }, + "offset": { + "label": "Differenza annotazione", + "desc": "Questi dati provengono dal flusso di rilevamento della telecamera, ma vengono sovrapposti alle immagini del flusso di registrazione. È improbabile che i due flussi siano perfettamente sincronizzati. Di conseguenza, il riquadro di delimitazione e il filmato non saranno perfettamente allineati. È possibile utilizzare questa impostazione per spostare le annotazioni in avanti o indietro nel tempo per allinearle meglio al filmato registrato.", + "millisecondsToOffset": "Millisecondi per compensare il rilevamento delle annotazioni. Predefinito: 0", + "tips": "Ridurre il valore se la riproduzione video è in anticipo rispetto ai riquadri e ai punti del percorso, e aumentarlo se la riproduzione video è in ritardo rispetto ad essi. Questo valore può essere negativo.", + "toast": { + "success": "La differenza dell'annotazione per {{camera}} è stato salvato nel file di configurazione." + } + } + }, + "carousel": { + "previous": "Diapositiva precedente", + "next": "Diapositiva successiva" + } + } +} diff --git a/web/public/locales/it/views/exports.json b/web/public/locales/it/views/exports.json new file mode 100644 index 0000000..df3ee48 --- /dev/null +++ b/web/public/locales/it/views/exports.json @@ -0,0 +1,128 @@ +{ + "documentTitle": "Esporta - Frigate", + "search": "Cerca", + "noExports": "Nessuna esportazione trovata", + "deleteExport": { + "label": "Elimina esportazione" + }, + "deleteExport.desc": "Sei sicuro di voler eliminare {{exportName}}?", + "editExport": { + "desc": "Inserisci un nuovo nome per questa esportazione.", + "title": "Rinomina esportazione", + "saveExport": "Salva esportazione" + }, + "toast": { + "error": { + "renameExportFailed": "Impossibile rinominare l'esportazione: {{errorMessage}}", + "assignCaseFailed": "Impossibile aggiornare l'assegnazione del caso: {{errorMessage}}", + "caseSaveFailed": "Impossibile salvare il caso: {{errorMessage}}", + "caseDeleteFailed": "Impossibile eliminare il caso: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Condividi esportazione", + "downloadVideo": "Scarica video", + "editName": "Modifica nome", + "deleteExport": "Elimina esportazione", + "assignToCase": "Aggiungi al caso", + "removeFromCase": "Rimuovi dal caso" + }, + "headings": { + "cases": "Casi", + "uncategorizedExports": "Esportazioni non categorizzate" + }, + "caseDialog": { + "title": "Aggiungi al caso", + "description": "Scegli un caso esistente o creane uno nuovo.", + "selectLabel": "Caso", + "newCaseOption": "Crea un nuovo caso", + "nameLabel": "Nome del caso", + "descriptionLabel": "Descrizione" + }, + "toolbar": { + "newCase": "Nuovo caso", + "addExport": "Aggiungi esportazione", + "editCase": "Modifica caso", + "deleteCase": "Elimina il caso" + }, + "deleteCase": { + "label": "Eliminare il caso", + "desc": "Sei sicuro di voler eliminare {{caseName}}?", + "descKeepExports": "Le esportazioni rimarranno disponibili come esportazioni non categorizzate.", + "descDeleteExports": "In questo caso, tutte le esportazioni verranno eliminate definitivamente.", + "deleteExports": "Elimina anche le esportazioni" + }, + "caseCard": { + "emptyCase": "Ancora nessuna esportazione" + }, + "jobCard": { + "defaultName": "{{camera}} esporta", + "queued": "In coda", + "running": "In esecuzione", + "preparing": "Preparazione", + "copying": "Copia", + "encoding": "Codifica", + "encodingRetry": "Codifica (riprova)", + "finalizing": "Finalizzazione" + }, + "caseView": { + "noDescription": "Nessuna descrizione", + "createdAt": "Creato {{value}}", + "exportCount_one": "1 esportazione", + "exportCount_other": "{{count}} esportazioni", + "cameraCount_one": "1 telecamera", + "cameraCount_other": "{{count}} telecamere", + "showMore": "Mostra altro", + "showLess": "Mostra meno", + "emptyTitle": "Questo caso è vuoto", + "emptyDescription": "Aggiungi le esportazioni esistenti non categorizzate per mantenere il caso organizzato.", + "emptyDescriptionNoExports": "Al momento non sono disponibili esportazioni non categorizzate da aggiungere." + }, + "caseEditor": { + "createTitle": "Crea un caso", + "editTitle": "Modifica caso", + "namePlaceholder": "Nome del caso", + "descriptionPlaceholder": "Aggiungi note o contesto per questo caso" + }, + "addExportDialog": { + "title": "Aggiungi l'esportazione a {{caseName}}", + "searchPlaceholder": "Cerca esportazioni non categorizzate", + "empty": "Nessuna esportazione non categorizzata corrisponde a questa ricerca.", + "addButton_one": "Aggiungi 1 esportazione", + "addButton_other": "Aggiungi {{count}} esportazioni", + "adding": "In aggiunta..." + }, + "selected_one": "{{count}} selezionati", + "selected_other": "{{count}} selezionati", + "bulkActions": { + "addToCase": "Aggiungi al caso", + "moveToCase": "Sposta al caso", + "removeFromCase": "Rimuovi dal caso", + "delete": "Elimina", + "deleteNow": "Elimina ora" + }, + "bulkDelete": { + "title": "Elimina le esportazioni", + "desc_one": "Sei sicuro di voler eliminare l'esportazione {{count}}?", + "desc_other": "Sei sicuro di voler eliminare {{count}} esportazioni?" + }, + "bulkRemoveFromCase": { + "title": "Rimuovi dal caso", + "desc_one": "Rimuovere l'esportazione {{count}} da questo caso?", + "desc_other": "Rimuovere {{count}} esportazioni da questo caso?", + "descKeepExports": "Le esportazioni verranno spostate nella categoria \"non classificate\".", + "descDeleteExports": "Le esportazioni verranno eliminate definitivamente.", + "deleteExports": "Elimina almeno le esportazioni" + }, + "bulkToast": { + "success": { + "delete": "Esportazioni eliminate con successo", + "reassign": "Assegnazione del caso aggiornata con successo", + "remove": "Esportazioni rimosse con successo dal caso" + }, + "error": { + "deleteFailed": "Impossibile eliminare le esportazioni: {{errorMessage}}", + "reassignFailed": "Impossibile aggiornare l'assegnazione del caso: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/it/views/faceLibrary.json b/web/public/locales/it/views/faceLibrary.json new file mode 100644 index 0000000..842881f --- /dev/null +++ b/web/public/locales/it/views/faceLibrary.json @@ -0,0 +1,113 @@ +{ + "selectItem": "Seleziona {{item}}", + "description": { + "addFace": "Aggiungi una nuova raccolta alla Libreria dei Volti caricando la tua prima immagine.", + "placeholder": "Inserisci un nome per questa raccolta", + "invalidName": "Nome non valido. I nomi possono contenere solo lettere, numeri, spazi, apostrofi, caratteri di sottolineatura e trattini.", + "nameCannotContainHash": "Il nome non può contenere #." + }, + "details": { + "confidence": "Fiducia", + "person": "Persona", + "face": "Dettagli del volto", + "faceDesc": "Dettagli dell'oggetto tracciato che ha generato questo volto", + "timestamp": "Orario", + "subLabelScore": "Punteggio dell'etichetta secondaria", + "scoreInfo": "Il punteggio dell'etichetta secondaria è il punteggio ponderato per tutte le confidenze dei volti riconosciuti, quindi potrebbe differire dal punteggio mostrato nell'istantanea.", + "unknown": "Sconosciuto" + }, + "train": { + "title": "Riconoscimenti recenti", + "aria": "Seleziona i riconoscimenti recenti", + "empty": "Non ci sono recenti tentativi di riconoscimento facciale", + "titleShort": "Recente", + "emptyNoLibrary": { + "title": "Carica un volto", + "description": "Affinché il riconoscimento facciale funzioni è necessario aggiungere almeno un volto alla libreria." + } + }, + "button": { + "addFace": "Aggiungi volto", + "deleteFaceAttempts": "Elimina volti", + "uploadImage": "Carica immagine", + "reprocessFace": "Rielabora il volto", + "deleteFace": "Elimina volto", + "renameFace": "Rinomina volto" + }, + "trainFace": "Addestra il volto", + "toast": { + "success": { + "deletedName_one": "{{count}} volto è stato eliminato con successo.", + "deletedName_many": "{{count}} volti sono stati eliminati con successo.", + "deletedName_other": "{{count}} volti sono stati eliminati con successo.", + "trainedFace": "Volto addestrato con successo.", + "deletedFace_one": "Eliminato con successo {{count}} volto.", + "deletedFace_many": "Eliminati con successo {{count}} volti.", + "deletedFace_other": "Eliminati con successo {{count}} volti.", + "updatedFaceScore": "Punteggio del volto aggiornato con successo a {{name}} ({{score}}).", + "uploadedImage": "Immagine caricata correttamente.", + "addFaceLibrary": "{{name}} è stato aggiunto con successo alla Libreria dei Volti!", + "renamedFace": "Rinominato correttamente il volto in {{name}}", + "reclassifiedFace": "Volto riclassificato con successo." + }, + "error": { + "addFaceLibraryFailed": "Impossibile impostare il nome del volto: {{errorMessage}}", + "uploadingImageFailed": "Impossibile caricare l'immagine: {{errorMessage}}", + "deleteFaceFailed": "Impossibile eliminare: {{errorMessage}}", + "trainFailed": "Impossibile addestrare: {{errorMessage}}", + "updateFaceScoreFailed": "Impossibile aggiornare il punteggio del volto: {{errorMessage}}", + "deleteNameFailed": "Impossibile eliminare il nome: {{errorMessage}}", + "renameFaceFailed": "Impossibile rinominare il volto: {{errorMessage}}", + "reclassifyFailed": "Impossibile riclassificare il volto: {{errorMessage}}" + } + }, + "imageEntry": { + "dropActive": "Rilascia l'immagine qui…", + "dropInstructions": "Trascina e rilascia o incolla un'immagine qui oppure fai clic per selezionarla", + "maxSize": "Dimensione massima: {{size}} MB", + "validation": { + "selectImage": "Seleziona un file immagine." + } + }, + "createFaceLibrary": { + "title": "Crea raccolta", + "nextSteps": "Per costruire una base solida:
  • Usa la scheda \"Riconoscimenti recenti\" per selezionare e addestrare le immagini per ogni persona rilevata.
  • Concentrati sulle immagini dritte per ottenere risultati migliori; evita di addestrare immagini che catturano i volti da un'angolazione.
  • ", + "desc": "Crea una nuova raccolta", + "new": "Crea nuovo volto" + }, + "readTheDocs": "Leggi la documentazione", + "selectFace": "Seleziona volto", + "documentTitle": "Libreria dei Volti - Frigate", + "uploadFaceImage": { + "desc": "Carica un'immagine per scansionare i volti e includerla in {{pageToggle}}", + "title": "Carica l'immagine del volto" + }, + "deleteFaceLibrary": { + "title": "Elimina nome", + "desc": "Vuoi davvero eliminare la raccolta {{name}}? Questa operazione eliminerà definitivamente tutti i volti associati." + }, + "trainFaceAs": "Addestra il volto come:", + "steps": { + "faceName": "Inserisci il nome del volto", + "nextSteps": "Prossimi passi", + "uploadFace": "Carica l'immagine del volto", + "description": { + "uploadFace": "Carica un'immagine di {{name}} che mostri il suo viso da un'angolazione frontale. Non è necessario ritagliare l'immagine in modo da mostrare solo il viso." + } + }, + "renameFace": { + "title": "Rinomina volto", + "desc": "Inserisci un nuovo nome per {{name}}" + }, + "collections": "Collezioni", + "deleteFaceAttempts": { + "title": "Elimina volti", + "desc_one": "Vuoi davvero eliminare {{count}} volto? Questa azione non può essere annullata.", + "desc_many": "Vuoi davvero eliminare {{count}} volti? Questa azione non può essere annullata.", + "desc_other": "Vuoi davvero eliminare {{count}} volti? Questa azione non può essere annullata." + }, + "nofaces": "Nessun volto disponibile", + "pixels": "{{area}}px", + "reclassifyFaceAs": "Riclassifica il volto come:", + "reclassifyFace": "Riclassifica il volto" +} diff --git a/web/public/locales/it/views/live.json b/web/public/locales/it/views/live.json new file mode 100644 index 0000000..ac9fc12 --- /dev/null +++ b/web/public/locales/it/views/live.json @@ -0,0 +1,206 @@ +{ + "documentTitle": { + "default": "In diretta - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Dal vivo - Frigate", + "lowBandwidthMode": "Modalità a bassa larghezza di banda", + "twoWayTalk": { + "enable": "Abilita audio bidirezionale", + "disable": "Disabilita audio bidirezionale" + }, + "snapshots": { + "enable": "Abilita Istantanee", + "disable": "Disabilita istantanee" + }, + "manualRecording": { + "recordDisabledTips": "Poiché la registrazione è disabilitata o limitata nella configurazione di questa telecamera, verrà salvata solo un'istantanea.", + "title": "Su richiesta", + "tips": "Scarica un'istantanea attuale o avvia un evento manuale in base alle impostazioni di conservazione della registrazione di questa telecamera.", + "playInBackground": { + "label": "Riproduci in sottofondo", + "desc": "Abilita questa opzione per continuare la trasmissione quando il lettore è nascosto." + }, + "showStats": { + "label": "Mostra statistiche", + "desc": "Abilita questa opzione per visualizzare le statistiche della trasmissione come sovrapposizione sul flusso della telecamera." + }, + "debugView": "Vista correzioni", + "start": "Avvia la registrazione su richiesta", + "started": "Registrazione manuale su richiesta avviata.", + "failedToStart": "Impossibile avviare la registrazione manuale su richiesta.", + "end": "Termina la registrazione su richiesta", + "ended": "Registrazione manuale su richiesta terminata.", + "failedToEnd": "Impossibile terminare la registrazione manuale su richiesta." + }, + "cameraSettings": { + "snapshots": "Istantanee", + "autotracking": "Tracciamento automatico", + "title": "Impostazioni di {{camera}}", + "cameraEnabled": "Telecamera abilitata", + "objectDetection": "Rilevamento oggetti", + "recording": "Registrazione", + "audioDetection": "Rilevamento audio", + "transcription": "Trascrizione audio", + "camera": "Telecamera" + }, + "history": { + "label": "Mostra filmati storici" + }, + "notifications": "Notifiche", + "streamingSettings": "Impostazioni di trasmissione", + "audio": "Audio", + "cameraAudio": { + "enable": "Abilita audio della telecamera", + "disable": "Disabilita audio della telecamera" + }, + "ptz": { + "move": { + "clickMove": { + "enable": "Abilita clic per spostare", + "disable": "Disabilita clic per spostare", + "label": "Fai clic nella cornice per centrare la telecamera", + "enableWithZoom": "Abilita clic per muovere / trascina per ingrandire" + }, + "left": { + "label": "Sposta la telecamera PTZ a sinistra" + }, + "up": { + "label": "Sposta la telecamera PTZ verso l'alto" + }, + "down": { + "label": "Sposta la telecamera PTZ verso il basso" + }, + "right": { + "label": "Sposta la telecamera PTZ a destra" + } + }, + "zoom": { + "in": { + "label": "Ingrandisci la telecamera PTZ" + }, + "out": { + "label": "Riduci la telecamera PTZ" + } + }, + "frame": { + "center": { + "label": "Fai clic nella cornice per centrare la telecamera PTZ" + } + }, + "presets": "Preimpostazioni della telecamera PTZ", + "focus": { + "in": { + "label": "Aumenta fuoco della telecamera PTZ" + }, + "out": { + "label": "Diminuisci fuoco della telecamera PTZ" + } + } + }, + "camera": { + "enable": "Abilita telecamera", + "disable": "Disabilita telecamera", + "turnOn": "Attiva la telecamera", + "turnOff": "Disattiva la telecamera" + }, + "muteCameras": { + "enable": "Muta tutte le telecamere", + "disable": "Attiva audio di tutte le telecamere" + }, + "detect": { + "enable": "Abilita rilevamento", + "disable": "Disabilita rilevamento" + }, + "recording": { + "enable": "Abilita registrazione", + "disable": "Disabilita registrazione", + "disabledInConfig": "Per questa telecamera è necessario prima abilitare la registrazione nelle impostazioni." + }, + "audioDetect": { + "enable": "Abilita rilevamento audio", + "disable": "Disabilita rilevamento audio" + }, + "autotracking": { + "enable": "Abilita il tracciamento automatico", + "disable": "Disabilita il tracciamento automatico" + }, + "streamStats": { + "enable": "Mostra statistiche di trasmissione", + "disable": "Nascondi statistiche di trasmissione" + }, + "suspend": { + "forTime": "Sospendi per: " + }, + "stream": { + "title": "Trasmissione", + "audio": { + "tips": { + "title": "L'audio deve essere trasmesso dalla tua telecamera e configurato in go2rtc per questa trasmissione.", + "documentation": "Leggi la documentazione " + }, + "unavailable": "L'audio non è disponibile per questo flusso", + "available": "L'audio è disponibile per questo flusso" + }, + "twoWayTalk": { + "tips": "Il dispositivo deve supportare la funzionalità e WebRTC deve essere configurato per la comunicazione bidirezionale.", + "tips.documentation": "Leggi la documentazione ", + "unavailable": "La comunicazione bidirezionale non è disponibile per questo flusso", + "available": "La comunicazione bidirezionale è disponibile per questo flusso" + }, + "playInBackground": { + "tips": "Abilita questa opzione per continuare la trasmissione quando il lettore è nascosto.", + "label": "Riproduci in sottofondo" + }, + "lowBandwidth": { + "tips": "La visualizzazione dal vivo è in modalità a bassa larghezza di banda a causa di errori di caricamento o di trasmissione.", + "resetStream": "Reimposta flusso" + }, + "debug": { + "picker": "Selezione del flusso non disponibile in modalità correzioni. La visualizzazione correzioni utilizza sempre il flusso a cui è assegnato il ruolo di rilevamento." + } + }, + "effectiveRetainMode": { + "modes": { + "all": "Tutti", + "motion": "Movimento", + "active_objects": "Oggetti attivi" + }, + "notAllTips": "La configurazione di conservazione della registrazione di {{source}} è impostata su mode: {{effectiveRetainMode}}, quindi questa registrazione su richiesta conserverà solo i segmenti con {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Modifica formato", + "group": { + "label": "Modifica gruppo telecamere" + }, + "exitEdit": "Esci dalla modifica" + }, + "transcription": { + "enable": "Abilita la trascrizione audio in tempo reale", + "disable": "Disabilita la trascrizione audio in tempo reale" + }, + "noCameras": { + "buttonText": "Aggiungi telecamera", + "title": "Nessuna telecamera configurata", + "description": "Per iniziare, collega una telecamera a Frigate.", + "restricted": { + "title": "Nessuna telecamera disponibile", + "description": "Non hai l'autorizzazione per visualizzare alcuna telecamera in questo gruppo." + }, + "default": { + "title": "Nessuna telecamera configurata", + "description": "Per iniziare, collega una telecamera a Frigate.", + "buttonText": "Aggiungi telecamera" + }, + "group": { + "title": "Nessuna telecamera nel gruppo", + "description": "Questo gruppo di telecamere non ha telecamere assegnate o abilitate.", + "buttonText": "Gestisci gruppi" + } + }, + "snapshot": { + "takeSnapshot": "Scarica istantanea attuale", + "noVideoSource": "Nessuna sorgente video disponibile per l'istantanea.", + "captureFailed": "Impossibile catturare l'istantanea.", + "downloadStarted": "Scaricamento istantanea avviato." + } +} diff --git a/web/public/locales/it/views/motionSearch.json b/web/public/locales/it/views/motionSearch.json new file mode 100644 index 0000000..4421f8b --- /dev/null +++ b/web/public/locales/it/views/motionSearch.json @@ -0,0 +1,82 @@ +{ + "documentTitle": "Ricerca movimenti - Frigate", + "title": "Ricerca movimenti", + "description": "Disegna un poligono per definire la regione di interesse e specifica un intervallo di tempo per la ricerca di cambiamenti di movimento all'interno di tale regione.", + "selectCamera": "La ricerca di movimenti è in fase di caricamento", + "startSearch": "Avvia ricerca", + "searchStarted": "Ricerca avviata", + "searchCancelled": "Ricerca annullata", + "cancelSearch": "Annulla", + "searching": "Ricerca in corso.", + "searchComplete": "Ricerca completata", + "noResultsYet": "Esegui una ricerca per trovare le variazioni di movimento nella regione selezionata", + "noChangesFound": "Nessuna modifica dei pixel rilevata nella regione selezionata", + "changesFound_one": "Trovato {{count}} cambiamento di movimento", + "changesFound_many": "Trovati {{count}} cambiamenti di movimento", + "changesFound_other": "Trovati {{count}} cambiamenti di movimento", + "framesProcessed": "{{count}} fotogrammi elaborati", + "jumpToTime": "Vai a questo momento", + "results": "Risultati", + "showSegmentHeatmap": "Mappa di calore", + "newSearch": "Nuova ricerca", + "clearResults": "Pulisci risultati", + "clearROI": "Pulisci poligono", + "polygonControls": { + "points_one": "{{count}} punto", + "points_many": "{{count}} punti", + "points_other": "{{count}} punti", + "undo": "Annulla ultimo punto", + "reset": "Reimposta poligono", + "drawMode": "Disegna", + "moveMode": "Sposta" + }, + "motionHeatmapLabel": "Mappa di calore del movimento", + "dialog": { + "title": "Ricerca di movimento", + "cameraLabel": "Telecamera", + "previewAlt": "Anteprima della telecamera per {{camera}}" + }, + "timeRange": { + "title": "Intervallo di ricerca", + "start": "Ora di inizio", + "end": "Ora di fine" + }, + "settings": { + "title": "Impostazioni di ricerca", + "parallelMode": "Modalità parallela", + "parallelModeDesc": "Esegui la scansione simultanea di più intervalli di registrazione (più veloce; utilizza più risorse di decodifica)", + "threshold": "Soglia di sensibilità", + "thresholdDesc": "Valori più bassi indicano cambiamenti minori (1-255)", + "minArea": "Area di cambio minimo", + "minAreaDesc": "Dimensione minima di una singola regione mobile, espressa in percentuale della regione di interesse", + "frameSkip": "Salta fotogrammi", + "frameSkipDesc": "Elabora ogni N-esimo fotogramma. Imposta questo valore sulla frequenza dei fotogrammi della tua telecamera per elaborare un fotogramma al secondo (ad esempio, 5 per una telecamera a 5 FPS, 30 per una telecamera a 30 FPS). Valori più alti saranno più veloci, ma potrebbero perdere eventi di movimento brevi.", + "maxResults": "Risultati massimi", + "maxResultsDesc": "Interrompi dopo questo numero di orario corrispondenti" + }, + "errors": { + "noCamera": "Seleziona una telecamera", + "noROI": "Disegna una regione di interesse", + "noTimeRange": "Seleziona un intervallo di tempo", + "invalidTimeRange": "L'ora di fine deve essere successiva all'ora di inizio", + "searchFailed": "Ricerca non riuscita: {{message}}", + "polygonTooSmall": "Il poligono deve avere almeno 3 punti", + "unknown": "Errore sconosciuto" + }, + "changePercentage": "{{percentage}}% modificato", + "metrics": { + "title": "Metriche di ricerca", + "segmentsScanned": "Segmenti scansionati", + "segmentsProcessed": "Elaborati", + "segmentsSkippedInactive": "Saltati (nessuna attività)", + "segmentsSkippedHeatmap": "Saltati (nessuna sovrapposizione del ROI)", + "fallbackFullRange": "Scansione gamma completa alternativa", + "framesDecoded": "Fotogrammi decodificati", + "wallTime": "Tempo di ricerca", + "segmentErrors": "Errori di segmento", + "seconds": "{{seconds}}s", + "minutesSeconds": "{{minutes}}m {{seconds}}s", + "scanSummary": "{{segments}} segmenti · {{time}}" + }, + "scanning": "Scansione {{time}}" +} diff --git a/web/public/locales/it/views/recording.json b/web/public/locales/it/views/recording.json new file mode 100644 index 0000000..411070b --- /dev/null +++ b/web/public/locales/it/views/recording.json @@ -0,0 +1,12 @@ +{ + "calendar": "Calendario", + "export": "Esporta", + "filter": "Filtro", + "filters": "Filtri", + "toast": { + "error": { + "noValidTimeSelected": "Nessun intervallo di tempo valido selezionato", + "endTimeMustAfterStartTime": "L'ora di fine deve essere successiva all'ora di inizio" + } + } +} diff --git a/web/public/locales/it/views/replay.json b/web/public/locales/it/views/replay.json new file mode 100644 index 0000000..fc698eb --- /dev/null +++ b/web/public/locales/it/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "Riproduzione correzioni", + "description": "Riproduci le registrazioni della telecamera per le correzioni. L'elenco degli oggetti mostra un riepilogo ritardato degli oggetti rilevati e la scheda Messaggi mostra un flusso di messaggi interni di Frigate tratti dal filmato riprodotto.", + "websocket_messages": "Messaggi", + "dialog": { + "title": "Avvia la riproduzione delle correzioni", + "description": "Crea una telecamera di riproduzione temporanea che riproduca in ciclo i filmati storici per la correzione dei problemi di rilevamento e tracciamento degli oggetti. La telecamera di riproduzione avrà la stessa configurazione di rilevamento della telecamera sorgente. Scegli un intervallo di tempo da cui iniziare.", + "camera": "Telecamera sorgente", + "timeRange": "Intervallo di tempo", + "preset": { + "1m": "Ultimo minuto", + "5m": "Ultimi 5 minuti", + "timeline": "Dalla cronologia", + "custom": "Personalizza" + }, + "startButton": "Avvia riproduzione", + "selectFromTimeline": "Seleziona", + "starting": "Avvio riproduzione...", + "startLabel": "Inizio", + "endLabel": "Fine", + "toast": { + "error": "Impossibile avviare la riproduzione di correzioni: {{error}}", + "alreadyActive": "È già attiva una sessione di riproduzione", + "stopError": "Impossibile interrompere la riproduzione di correzioni: {{error}}", + "goToReplay": "Vai alla riproduzione" + } + }, + "page": { + "noSession": "Nessuna sessione di riproduzione di correzioni attiva", + "noSessionDesc": "Avvia una riproduzione di correzioni dalla visualizzazione Cronologia facendo clic sul pulsante Azioni nella barra degli strumenti e scegliendo Riproduzione di correzioni.", + "goToRecordings": "Vai alla Cronologia", + "preparingClip": "Preparazione video…", + "preparingClipDesc": "Frigate sta unendo le registrazioni relative all'intervallo di tempo selezionato. Questa operazione può richiedere un minuto per intervalli più lunghi.", + "startingCamera": "Avvio della riproduzione di correzioni…", + "startError": { + "title": "Impossibile avviare la riproduzione di correzioni", + "back": "Torna alla Cronologia" + }, + "sourceCamera": "Telecamera sorgente", + "replayCamera": "Telecamera di riproduzione", + "initializingReplay": "Inizializzazione riproduzione di correzioni...", + "stoppingReplay": "Interruzione riproduzione di correzioni...", + "stopReplay": "Ferma riproduzione", + "confirmStop": { + "title": "Interrompere la riproduzione di correzioni?", + "description": "In questo modo la sessione verrà interrotta e tutti i dati temporanei verranno eliminati. Sei sicuro?", + "confirm": "Ferma riproduzione", + "cancel": "Annulla" + }, + "activity": "Attività", + "objects": "Elenco degli oggetti", + "audioDetections": "Rilevamento audio", + "noActivity": "Nessuna attività rilevata", + "activeTracking": "Tracciamento attivo", + "noActiveTracking": "Nessun tracciamento attivo", + "configuration": "Configurazione", + "configurationDesc": "Regola con precisione le impostazioni di rilevamento del movimento e tracciamento degli oggetti per la telecamera Riproduzione di correzioni. Nessuna modifica verrà salvata nel file di configurazione di Frigate." + } +} diff --git a/web/public/locales/it/views/search.json b/web/public/locales/it/views/search.json new file mode 100644 index 0000000..97f8000 --- /dev/null +++ b/web/public/locales/it/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Cerca", + "savedSearches": "Ricerche salvate", + "searchFor": "Cerca {{inputValue}}", + "button": { + "clear": "Cancella ricerca", + "save": "Salva ricerca", + "delete": "Elimina la ricerca salvata", + "filterInformation": "Informazioni sul filtro", + "filterActive": "Filtri attivi" + }, + "filter": { + "label": { + "has_snapshot": "Contiene istantanea", + "cameras": "Telecamere", + "search_type": "Tipo di ricerca", + "has_clip": "Contiene video", + "before": "Prima", + "labels": "Etichette", + "min_score": "Punteggio minimo", + "zones": "Zone", + "max_score": "Punteggio massimo", + "min_speed": "Velocità minima", + "time_range": "Intervallo di tempo", + "after": "Dopo", + "max_speed": "Velocità massima", + "recognized_license_plate": "Targa riconosciuta", + "sub_labels": "Sottoetichette", + "attributes": "Attributi" + }, + "tips": { + "desc": { + "step3": "Puoi utilizzare più filtri aggiungendoli uno dopo l'altro, lasciando uno spazio in mezzo.", + "step5": "Il filtro intervallo di tempo utilizza il formato {{exampleTime}}.", + "exampleLabel": "Esempio:", + "step1": "Digita un nome chiave di filtro seguito da due punti (ad esempio, \"telecamere:\").", + "step2": "Seleziona un valore dai suggerimenti oppure digita il tuo valore.", + "step4": "I filtri data (prima: e dopo:) utilizzano il formato {{DateFormat}}.", + "step6": "Rimuovi i filtri cliccando sulla \"x\" accanto ad essi.", + "text": "I filtri ti aiutano a restringere i risultati della ricerca. Ecco come usarli nel campo di inserimento:" + }, + "title": "Come utilizzare i filtri di testo" + }, + "toast": { + "error": { + "minScoreMustBeLessOrEqualMaxScore": "Il 'punteggio minimo' deve essere minore o uguale del 'punteggio massimo'.", + "beforeDateBeLaterAfter": "La data 'prima' deve essere successiva alla data 'dopo'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "La 'velocità minima' deve essere minore o uguale della 'velocità massima'.", + "afterDatebeEarlierBefore": "La data 'dopo' deve essere precedente alla data 'prima'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Il 'punteggio massimo' deve essere maggiore o uguale del 'punteggio minimo'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "La 'velocità massima' deve essere maggiore o uguale della 'velocità minima'." + } + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Descrizione" + }, + "header": { + "noFilters": "Filtri", + "activeFilters": "Filtri attivi", + "currentFilterType": "Valori del filtro" + } + }, + "similaritySearch": { + "clear": "Cancella ricerca di somiglianza", + "title": "Ricerca di somiglianza", + "active": "Ricerca di somiglianza attiva" + }, + "placeholder": { + "search": "Ricerca…" + }, + "trackedObjectId": "ID oggetto tracciato" +} diff --git a/web/public/locales/it/views/settings.json b/web/public/locales/it/views/settings.json new file mode 100644 index 0000000..9ef1f0f --- /dev/null +++ b/web/public/locales/it/views/settings.json @@ -0,0 +1,2241 @@ +{ + "documentTitle": { + "authentication": "Impostazioni di autenticazione - Frigate", + "default": "Impostazioni - Frigate", + "classification": "Impostazioni di classificazione - Frigate", + "camera": "Impostazioni telecamera - Frigate", + "masksAndZones": "Editor di maschere e zone - Frigate", + "motionTuner": "Regolatore di movimento - Frigate", + "object": "Correzioni - Frigate", + "general": "Impostazioni interfaccia - Frigate", + "frigatePlus": "Impostazioni Frigate+ - Frigate", + "notifications": "Impostazioni di notifica - Frigate", + "enrichments": "Impostazioni di miglioramento - Frigate", + "cameraManagement": "Gestisci telecamere - Frigate", + "cameraReview": "Impostazioni revisione telecamera - Frigate", + "globalConfig": "Configurazione globale - Frigate", + "cameraConfig": "Configurazione telecamera - Frigate", + "maintenance": "Manutenzione - Frigate", + "profiles": "Profili - Frigate", + "detectorsAndModel": "Rilevatori e modelli - Frigate" + }, + "frigatePlus": { + "snapshotConfig": { + "cleanCopyWarning": "Alcune telecamere hanno la funzione di istantanea disabilitata", + "table": { + "snapshots": "Istantanee", + "camera": "Telecamera", + "cleanCopySnapshots": "Istantanee clean_copy" + }, + "desc": "Per inviare i dati a Frigate+ è necessario abilitare le istantanee nella configurazione.", + "documentation": "Leggi la documentazione", + "title": "Configurazione istantanee" + }, + "apiKey": { + "title": "Chiave API Frigate+", + "validated": "La chiave API Frigate+ è stata rilevata e convalidata", + "notValidated": "La chiave API Frigate+ non è stata rilevata o non è stata convalidata", + "desc": "La chiave API Frigate+ consente l'integrazione con il servizio Frigate+.", + "plusLink": "Scopri di più su Frigate+" + }, + "modelInfo": { + "trainDate": "Data addestramento", + "baseModel": "Modello base", + "cameras": "Telecamere", + "plusModelType": { + "userModel": "Messa a punto fine", + "baseModel": "Modello base" + }, + "availableModels": "Modelli Frigate+ disponibili", + "loadingAvailableModels": "Caricamento dei modelli disponibili…", + "supportedDetectors": "Rilevatori supportati", + "error": "Impossibile caricare le informazioni sul modello", + "modelType": "Tipo di modello", + "modelSelect": "Qui puoi selezionare i modelli disponibili su Frigate+. Nota: puoi selezionare solo i modelli compatibili con la configurazione attuale del tuo rilevatore.", + "title": "Informazioni sul modello", + "loading": "Caricamento informazioni sul modello…", + "noModelsAvailable": "Nessun modello disponibile", + "noModelLoaded": "Al momento non è caricato alcun modello di Frigate+.", + "selectModel": "Seleziona un modello", + "filter": { + "ariaLabel": "Filtra i modelli per tipo", + "baseModels": "Modelli base", + "fineTunedModels": "Modelli ottimizzati" + } + }, + "toast": { + "error": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}", + "success": "Le impostazioni di Frigate+ sono state salvate. Riavvia Frigate per applicare le modifiche." + }, + "title": "Impostazioni Frigate+", + "restart_required": "Riavvio richiesto (modello Frigate+ modificato)", + "unsavedChanges": "Modifiche alle impostazioni di Frigate+ non salvate", + "description": "Frigate+ è un servizio in abbonamento che offre accesso a funzionalità e capacità aggiuntive per la tua istanza di Frigate, tra cui la possibilità di utilizzare modelli di rilevamento oggetti personalizzati addestrati sui tuoi dati. Puoi gestire le impostazioni del tuo modello Frigate+ qui.", + "cardTitles": { + "api": "API", + "currentModel": "Modello attuale", + "otherModels": "Altri modelli", + "configuration": "Configurazione" + }, + "changeInDetectorsAndModel": "Cambia modello" + }, + "debug": { + "timestamp": { + "desc": "Sovrapponi l'orario all'immagine", + "title": "Orario" + }, + "objectShapeFilterDrawing": { + "tips": "Abilita questa opzione per disegnare un rettangolo sull'immagine della telecamera per mostrarne l'area e il rapporto. Questi valori possono quindi essere utilizzati per impostare i parametri del filtro forma oggetto nella configurazione.", + "area": "Area", + "title": "Disegno del filtro della forma dell'oggetto", + "desc": "Disegna un rettangolo sull'immagine per visualizzare i dettagli dell'area e del rapporto", + "document": "Leggi la documentazione ", + "score": "Punteggio", + "ratio": "Rapporto" + }, + "detectorDesc": "Frigate utilizza i tuoi rilevatori ({{detectors}}) per rilevare oggetti nel flusso video della tua telecamera.", + "boundingBoxes": { + "colors": { + "info": "
  • All'avvio, a ciascuna etichetta dell'oggetto verranno assegnati colori diversi
  • Una linea sottile blu scuro indica che l'oggetto non è stato rilevato in questo momento
  • Una linea sottile grigia indica che l'oggetto è stato rilevato come stazionario
  • Una linea spessa indica che l'oggetto è oggetto di tracciamento automatico (se abilitato)
  • ", + "label": "Colori del riquadro di delimitazione dell'oggetto" + }, + "desc": "Mostra i riquadri di delimitazione attorno agli oggetti tracciati", + "title": "Riquadri di delimitazione" + }, + "regions": { + "tips": "

    Riquadri di regione


    I riquadri di colore verde brillante verranno sovrapposte alle aree di interesse nel fotogramma che viene inviato al rilevatore di oggetti.

    ", + "title": "Regioni", + "desc": "Mostra un riquadro della regione di interesse inviata al rilevatore di oggetti" + }, + "noObjects": "Nessun oggetto", + "title": "Correzioni", + "desc": "La vista di correzione mostra una vista in tempo reale degli oggetti tracciati e delle relative statistiche. L'elenco degli oggetti mostra un riepilogo ritardato degli oggetti rilevati.", + "debugging": "Correzioni", + "objectList": "Elenco degli oggetti", + "mask": { + "desc": "Mostra i poligoni della maschera di movimento", + "title": "Maschere di movimento" + }, + "motion": { + "title": "Riquadri di movimento", + "desc": "Mostra i riquadri attorno alle aree in cui viene rilevato il movimento", + "tips": "

    Riquadri di movimento


    I riquadri rossi verranno sovrapposti alle aree dell'inquadratura in cui viene attualmente rilevato un movimento

    " + }, + "zones": { + "title": "Zone", + "desc": "Mostra un contorno di tutte le zone definite" + }, + "paths": { + "title": "Percorsi", + "desc": "Mostra i punti significativi del percorso dell'oggetto tracciato", + "tips": "

    Percorsi


    Linee e cerchi indicheranno i punti significativi in cui l'oggetto tracciato si è spostato durante il suo ciclo di vita.

    " + }, + "audio": { + "title": "Audio", + "currentdbFS": "dbFS correnti", + "noAudioDetections": "Nessun rilevamento audio", + "score": "punteggio", + "currentRMS": "RMS attuale" + }, + "openCameraWebUI": "Apri l'interfaccia utente Web di {{camera}}" + }, + "masksAndZones": { + "motionMasks": { + "context": { + "title": "Le maschere di movimento vengono utilizzate per impedire che tipi di movimento indesiderati attivino il rilevamento (ad esempio: rami di alberi, orari di telecamere). Le maschere di movimento dovrebbero essere utilizzate con molta parsimonia: un uso eccessivo renderebbe più difficile il tracciamento degli oggetti.", + "documentation": "Leggi la documentazione" + }, + "desc": { + "title": "Le maschere di movimento vengono utilizzate per impedire che movimenti indesiderati attivino il rilevamento. Un mascheramento eccessivo renderà più difficile il tracciamento degli oggetti.", + "documentation": "Documentazione" + }, + "label": "Maschera di movimento", + "edit": "Modifica maschera di movimento", + "clickDrawPolygon": "Fai clic per disegnare un poligono sull'immagine.", + "polygonAreaTooLarge": { + "title": "La maschera di movimento copre il {{polygonArea}}% dell'inquadratura. Si sconsiglia di utilizzare maschere di movimento di grandi dimensioni.", + "documentation": "Leggi la documentazione", + "tips": "Le maschere di movimento non impediscono il rilevamento degli oggetti. È consigliabile utilizzare una zona specifica." + }, + "point_one": "{{count}} punto", + "point_many": "{{count}} punti", + "point_other": "{{count}} punti", + "documentTitle": "Modifica maschera movimento - Frigate", + "add": "Nuova maschera di movimento", + "toast": { + "success": { + "title": "{{polygonName}} è stato salvato.", + "noName": "La maschera di movimento è stata salvata." + } + }, + "defaultName": "Maschera di movimento {{number}}", + "name": { + "title": "Nome", + "description": "Un nome descrittivo opzionale per questa maschera di movimento.", + "placeholder": "Inserisci un nome..." + } + }, + "form": { + "zoneName": { + "error": { + "hasIllegalCharacter": "Il nome della zona contiene caratteri non validi.", + "mustNotBeSameWithCamera": "Il nome della zona non deve essere uguale al nome della telecamera.", + "mustBeAtLeastTwoCharacters": "Il nome della zona deve essere composto da almeno 2 caratteri.", + "alreadyExists": "Per questa telecamera esiste già una zona con questo nome.", + "mustNotContainPeriod": "Il nome della zona non deve contenere punti.", + "mustHaveAtLeastOneLetter": "Il nome della zona deve contenere almeno una lettera." + } + }, + "distance": { + "error": { + "text": "La distanza deve essere maggiore o uguale a 0.1.", + "mustBeFilled": "Per utilizzare la stima della velocità è necessario compilare tutti i campi relativi alla distanza." + } + }, + "polygonDrawing": { + "delete": { + "title": "Conferma eliminazione", + "desc": "Sei sicuro di voler eliminare {{type}} {{name}}?", + "success": "{{name}} è stato eliminato." + }, + "removeLastPoint": "Rimuovi l'ultimo punto", + "reset": { + "label": "Cancella tutti i punti" + }, + "snapPoints": { + "true": "Aggancia punti", + "false": "Non agganciare punti" + }, + "error": { + "mustBeFinished": "Prima di salvare, è necessario terminare il disegno del poligono." + }, + "type": { + "zone": "zona", + "motion_mask": "maschera di movimento", + "object_mask": "maschera oggetto" + }, + "revertOverride": { + "title": "Ripristina la configurazione di base", + "desc": "Questo rimuoverà la sovrascrittura del profilo per {{type}} {{name}} e ripristinerà la configurazione di base." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "L'inerzia deve essere superiore a 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Il tempo di permanenza deve essere maggiore o uguale a 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "La soglia di velocità deve essere maggiore o uguale a 0,1." + } + }, + "id": { + "error": { + "mustNotBeEmpty": "L'ID non deve essere vuoto.", + "alreadyExists": "Esiste già una maschera con questo ID per questa telecamera." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Il nome non deve essere vuoto." + } + } + }, + "filter": { + "all": "Tutte le maschere e zone" + }, + "toast": { + "success": { + "copyCoordinates": "Coordinate per {{polyName}} copiate negli appunti." + }, + "error": { + "copyCoordinatesFailed": "Impossibile copiare le coordinate negli appunti." + } + }, + "zones": { + "speedEstimation": { + "desc": "Abilita la stima della velocità per gli oggetti in questa zona. La zona deve avere esattamente 4 punti.", + "title": "Stima della velocità", + "docs": "Leggi la documentazione", + "lineCDistance": "Distanza della linea C ({{unit}})", + "lineDDistance": "Distanza della linea D ({{unit}})", + "lineADistance": "Distanza della linea A ({{unit}})", + "lineBDistance": "Distanza della linea B ({{unit}})" + }, + "add": "Aggiungi zona", + "speedThreshold": { + "desc": "Specifica una velocità minima affinché gli oggetti vengano presi in considerazione in questa zona.", + "toast": { + "error": { + "pointLengthError": "La stima della velocità è stata disabilitata per questa zona. Le zone con stima della velocità devono avere esattamente 4 punti.", + "loiteringTimeError": "Le zone con tempi di permanenza superiori a 0 non devono essere utilizzate per la stima della velocità." + } + }, + "title": "Soglia di velocità ({{unit}})" + }, + "desc": { + "title": "Le zone consentono di definire un'area specifica dell'inquadratura, in modo da poter determinare se un oggetto si trova o meno all'interno di un'area particolare.", + "documentation": "Documentazione" + }, + "edit": "Modifica zona", + "name": { + "inputPlaceHolder": "Inserisci un nome…", + "title": "Nome", + "tips": "Il nome deve essere composto da almeno 2 caratteri, contenere almeno una lettera e non deve essere il nome di una telecamera o di un'altra zona di questa telecamera." + }, + "clickDrawPolygon": "Fai clic per disegnare un poligono sull'immagine.", + "point_one": "{{count}} punto", + "point_many": "{{count}} punti", + "point_other": "{{count}} punti", + "label": "Zone", + "documentTitle": "Modifica zona - Frigate", + "inertia": { + "title": "Inerzia", + "desc": "Specifica quanti fotogrammi deve avere un oggetto in una zona prima di essere considerato nella zona. Predefinito: 3" + }, + "loiteringTime": { + "title": "Tempo di permanenza", + "desc": "Imposta un intervallo di tempo minimo in secondi per cui l'oggetto deve trovarsi nella zona affinché venga attivato. Predefinito: 0" + }, + "objects": { + "title": "Oggetti", + "desc": "Elenco degli oggetti che si applicano a questa zona." + }, + "allObjects": "Tutti gli oggetti", + "toast": { + "success": "La zona ({{zoneName}}) è stata salvata." + }, + "enabled": { + "title": "Abilitata", + "description": "Indica se questa zona è attiva e abilitata nel file di configurazione. Se disabilitata, non può essere abilitata tramite MQTT. Le zone disabilitate vengono ignorate in fase di esecuzione." + } + }, + "objectMasks": { + "desc": { + "title": "Le maschere di filtro degli oggetti vengono utilizzate per filtrare i falsi positivi per un determinato tipo di oggetto in base alla posizione.", + "documentation": "Documentazione" + }, + "context": "Le maschere di filtro degli oggetti vengono utilizzate per filtrare i falsi positivi per un determinato tipo di oggetto in base alla posizione.", + "point_one": "{{count}} punto", + "point_many": "{{count}} punti", + "point_other": "{{count}} punti", + "add": "Aggiungi maschera oggetti", + "clickDrawPolygon": "Fai clic per disegnare un poligono sull'immagine.", + "edit": "Modifica maschera oggetti", + "objects": { + "desc": "Tipo di oggetto applicabile a questa maschera oggetto.", + "title": "Oggetti", + "allObjectTypes": "Tutti i tipi di oggetti" + }, + "toast": { + "success": { + "noName": "La maschera oggetto è stata salvata.", + "title": "{{polygonName}} è stato salvato." + } + }, + "label": "Maschere di oggetti", + "documentTitle": "Modifica maschera oggetti - Frigate", + "name": { + "title": "Nome", + "placeholder": "Inserisci un nome...", + "description": "Un nome descrittivo facoltativo per questa maschera oggetto." + } + }, + "restart_required": "Riavvio richiesto (maschere/zone modificate)", + "motionMaskLabel": "Maschera di movimento {{number}}", + "objectMaskLabel": "Maschera di oggetto {{number}}", + "masks": { + "enabled": { + "title": "Abilitata", + "description": "Indica se questa maschera è abilitata nel file di configurazione. Se disabilitata, non può essere abilitata tramite MQTT. Le maschere disabilitate vengono ignorate in fase di esecuzione." + } + }, + "disabledInConfig": "L'elemento è disabilitato nel file di configurazione", + "addDisabledProfile": "Aggiungi prima alla configurazione di base, poi sovrascrivi nel profilo", + "profileBase": "(base)", + "profileOverride": "(sovrascrivi)" + }, + "cameraSetting": { + "camera": "Telecamera", + "noCamera": "Nessuna telecamera" + }, + "camera": { + "reviewClassification": { + "limitDetections": "Limita i rilevamenti a zone specifiche", + "objectDetectionsTips": "Tutti gli oggetti {{detectionsLabels}} non categorizzati su {{cameraName}} verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano.", + "zoneObjectDetectionsTips": { + "text": "Tutti gli oggetti {{detectionsLabels}} non categorizzati in {{zone}} su {{cameraName}} verranno mostrati come Rilevamenti.", + "notSelectDetections": "Tutti gli oggetti {{detectionsLabels}} rilevati in {{zone}} su {{cameraName}} non classificati come Avvisi verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano.", + "regardlessOfZoneObjectDetectionsTips": "Tutti gli oggetti {{detectionsLabels}} non categorizzati su {{cameraName}} verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano." + }, + "title": "Classificazione della revisione", + "desc": "Frigate categorizza gli elementi di revisione come Avvisi e Rilevamenti. Per impostazione predefinita, tutti gli oggetti persona e automobile sono considerati Avvisi. Puoi perfezionare la categorizzazione degli elementi di revisione configurando le zone desiderate.", + "objectAlertsTips": "Tutti gli oggetti {{alertsLabels}} su {{cameraName}} verranno mostrati come Avvisi.", + "toast": { + "success": "La configurazione della classificazione di revisione è stata salvata. Riavvia Frigate per applicare le modifiche." + }, + "readTheDocumentation": "Leggi la documentazione", + "noDefinedZones": "Per questa telecamera non sono definite zone.", + "zoneObjectAlertsTips": "Tutti gli oggetti {{alertsLabels}} rilevati in {{zone}} su {{cameraName}} verranno visualizzati come Avvisi.", + "selectAlertsZones": "Seleziona le zone per gli Avvisi", + "selectDetectionsZones": "Seleziona le zone per i Rilevamenti", + "unsavedChanges": "Impostazioni di classificazione delle revisioni non salvate per {{camera}}" + }, + "streams": { + "desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi da parte di Frigate. Rilevamenti, registrazioni e correzioni non saranno disponibili.
    Nota: questa operazione non disabilita le ritrasmissioni di go2rtc.", + "title": "Flussi" + }, + "title": "Impostazioni telecamera", + "review": { + "title": "Rivedi", + "desc": "Abilita/disabilita temporaneamente avvisi e rilevamenti per questa telecamera fino al riavvio di Frigate. Se disabilitati, non verranno generati nuovi elementi di revisione. ", + "alerts": "Avvisi ", + "detections": "Rilevamenti " + }, + "object_descriptions": { + "title": "Descrizioni di oggetti di IA generativa", + "desc": "Abilita/disabilita temporaneamente le descrizioni degli oggetti generate dall'IA per questa telecamera. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli oggetti tracciati su questa telecamera." + }, + "review_descriptions": { + "title": "Descrizioni delle revisioni dell'IA generativa", + "desc": "Abilita/disabilita temporaneamente le descrizioni delle revisioni dell'IA generativa per questa telecamera. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli elementi da recensire su questa telecamera." + }, + "addCamera": "Aggiungi nuova telecamera", + "editCamera": "Modifica telecamera:", + "selectCamera": "Seleziona una telecamera", + "backToSettings": "Torna alle impostazioni della telecamera", + "cameraConfig": { + "add": "Aggiungi telecamera", + "edit": "Modifica telecamera", + "description": "Configura le impostazioni della telecamera, inclusi gli ingressi ed i ruoli della trasmissione.", + "name": "Nome della telecamera", + "nameRequired": "Il nome della telecamera è obbligatorio", + "nameInvalid": "Il nome della telecamera deve contenere solo lettere, numeri, caratteri di sottolineatura o trattini", + "namePlaceholder": "ad esempio: porta_principale", + "enabled": "Abilitata", + "ffmpeg": { + "inputs": "Flussi di ingresso", + "path": "Percorso del flusso", + "pathRequired": "Il percorso del flusso è obbligatorio", + "pathPlaceholder": "rtsp://...", + "roles": "Ruoli", + "rolesRequired": "È richiesto almeno un ruolo", + "rolesUnique": "Ogni ruolo (audio, rilevamento, registrazione) può essere assegnato solo ad un flusso", + "addInput": "Aggiungi flusso di ingresso", + "removeInput": "Rimuovi flusso di ingresso", + "inputsRequired": "È richiesto almeno un flusso di ingresso" + }, + "toast": { + "success": "La telecamera {{cameraName}} è stata salvata correttamente" + }, + "nameLength": "Il nome della telecamera deve contenere meno di 24 caratteri." + } + }, + "menu": { + "motionTuner": "Regolatore di movimento", + "notifications": "Notifiche", + "ui": "Interfaccia utente", + "classification": "Classificazione", + "cameras": "Configurazione telecamera", + "masksAndZones": "Maschere / Zone", + "debug": "Correzioni", + "users": "Utenti", + "frigateplus": "Frigate+", + "enrichments": "Miglioramenti", + "triggers": "Attivatori", + "roles": "Ruoli", + "cameraManagement": "Gestione della telecamera", + "cameraReview": "Revisiona", + "profiles": "Profili", + "general": "Generale", + "globalConfig": "Configurazione globale", + "system": "Sistema", + "integrations": "Integrazioni", + "uiSettings": "Impostazioni interfaccia utente", + "globalDetect": "Rilevamento oggetti", + "globalRecording": "Registrazione", + "globalSnapshots": "Istantanee", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Rilevamento movimento", + "globalObjects": "Oggetti", + "globalReview": "Revisiona", + "globalAudioEvents": "Rilevamento audio", + "globalLivePlayback": "Riproduzione in diretta", + "globalTimestampStyle": "Stile orario", + "systemDatabase": "Database", + "systemTls": "TLS", + "systemAuthentication": "Autenticazione", + "systemNetworking": "Reti", + "systemProxy": "Proxy", + "systemUi": "Interfaccia utente", + "systemLogging": "Registro", + "systemEnvironmentVariables": "Variabili d'ambiente", + "systemTelemetry": "Telemetria", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "Dispositivo di rilevamento", + "systemDetectionModel": "Modello di rilevamento", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "Flussi go2rtc", + "integrationSemanticSearch": "Ricerca semantica", + "integrationGenerativeAi": "IA Generativa", + "integrationFaceRecognition": "Riconoscimento facciale", + "integrationLpr": "Riconoscimento targhe", + "integrationObjectClassification": "Classificazione oggetti", + "integrationAudioTranscription": "Trascrizione audio", + "cameraDetect": "Rilevamento oggetti", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "Registrazione", + "cameraSnapshots": "Istantanee", + "cameraMotion": "Rilevamento movimento", + "cameraObjects": "Oggetti", + "cameraConfigReview": "Revisiona", + "cameraAudioEvents": "Rilevamento audio", + "cameraAudioTranscription": "Trascrizione audio", + "cameraNotifications": "Notifiche", + "cameraLivePlayback": "Riproduzione in diretta", + "cameraBirdseye": "Birdseye", + "cameraFaceRecognition": "Riconoscimento facciale", + "cameraLpr": "Riconoscimento targhe", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraTimestampStyle": "Stile orario", + "cameraUi": "Interfaccia utente telecamera", + "mediaSync": "Sincronizzazione multimediale", + "cameraMqtt": "MQTT telecamera", + "maintenance": "Manutenzione", + "regionGrid": "Griglia di regioni", + "systemDetectorsAndModel": "Rilevatori e modelli" + }, + "users": { + "dialog": { + "changeRole": { + "roleInfo": { + "viewerDesc": "Limitato solo ai cruscotti dal vivo, alle revisioni, alle esplorazioni e alle esportazioni.", + "intro": "Seleziona il ruolo appropriato per questo utente:", + "admin": "Amministratore", + "adminDesc": "Accesso completo a tutte le funzionalità.", + "viewer": "Visualizzatore", + "customDesc": "Ruolo personalizzato con accesso specifico alla telecamera." + }, + "title": "Cambia ruolo utente", + "desc": "Aggiorna i permessi per {{username}}", + "select": "Seleziona un ruolo" + }, + "deleteUser": { + "warn": "Sei sicuro di voler eliminare {{username}}?", + "title": "Elimina utente", + "desc": "Questa azione non può essere annullata. L'account utente verrà eliminato definitivamente e tutti i dati associati verranno rimossi." + }, + "form": { + "user": { + "placeholder": "Inserisci il nome utente", + "title": "Nome utente", + "desc": "Sono consentiti solo lettere, numeri, punti e caratteri di sottolineatura." + }, + "password": { + "confirm": { + "title": "Conferma password", + "placeholder": "Conferma password" + }, + "strength": { + "title": "Complessità della password: ", + "weak": "Debole", + "medium": "Media", + "strong": "Forte", + "veryStrong": "Molto forte" + }, + "title": "Password", + "placeholder": "Inserisci la password", + "match": "Le password corrispondono", + "notMatch": "Le password non corrispondono", + "show": "Mostra password", + "hide": "Nascondi password", + "requirements": { + "title": "Requisiti password:", + "length": "Almeno 12 caratteri", + "uppercase": "Almeno una lettera maiuscola", + "digit": "Almeno una cifra", + "special": "Almeno un carattere speciale (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nuova password", + "placeholder": "Inserisci la nuova password", + "confirm": { + "placeholder": "Reinserisci la nuova password" + } + }, + "usernameIsRequired": "Il nome utente è obbligatorio", + "passwordIsRequired": "La password è obbligatoria", + "currentPassword": { + "title": "Password attuale", + "placeholder": "Inserisci la password attuale" + } + }, + "createUser": { + "desc": "Aggiungi un nuovo account utente e specifica un ruolo per l'accesso alle aree dell'interfaccia utente di Frigate.", + "title": "Crea nuovo utente", + "usernameOnlyInclude": "Il nome utente può contenere solo lettere, numeri, . o _", + "confirmPassword": "Conferma la password" + }, + "passwordSetting": { + "updatePassword": "Aggiorna la password per {{username}}", + "setPassword": "Imposta password", + "desc": "Crea una password complessa per proteggere questo account.", + "cannotBeEmpty": "La password non può essere vuota", + "doNotMatch": "Le password non corrispondono", + "currentPasswordRequired": "È richiesta la password attuale", + "incorrectCurrentPassword": "La password attuale è errata", + "passwordVerificationFailed": "Impossibile verificare la password", + "multiDeviceWarning": "Sarà necessario effettuare nuovamente l'accesso su qualsiasi altro dispositivo entro {{refresh_time}}.", + "multiDeviceAdmin": "Puoi anche forzare tutti gli utenti a riautenticarsi immediatamente ruotando il tuo segreto JWT." + } + }, + "table": { + "password": "Reimposta password", + "username": "Nome utente", + "actions": "Azioni", + "role": "Ruolo", + "noUsers": "Nessun utente trovato.", + "changeRole": "Cambia ruolo utente", + "deleteUser": "Elimina utente" + }, + "toast": { + "error": { + "roleUpdateFailed": "Impossibile aggiornare il ruolo: {{errorMessage}}", + "setPasswordFailed": "Impossibile salvare la password: {{errorMessage}}", + "createUserFailed": "Impossibile creare l'utente: {{errorMessage}}", + "deleteUserFailed": "Impossibile eliminare l'utente: {{errorMessage}}" + }, + "success": { + "createUser": "Utente {{user}} creato con successo", + "updatePassword": "Password aggiornata con successo.", + "deleteUser": "Utente {{user}} eliminato con successo", + "roleUpdated": "Ruolo aggiornato per {{user}}" + } + }, + "title": "Utenti", + "management": { + "title": "Gestione utenti", + "desc": "Gestisci gli account utente di questa istanza Frigate." + }, + "addUser": "Aggiungi utente", + "updatePassword": "Reimposta password" + }, + "general": { + "liveDashboard": { + "automaticLiveView": { + "desc": "Passa automaticamente alla visualizzazione dal vivo di una telecamera quando viene rilevata attività. Disabilitando questa opzione, le immagini statiche della telecamera nel cruscotto dal vivo verranno aggiornate solo una volta al minuto.", + "label": "Visualizzazione automatica dal vivo" + }, + "playAlertVideos": { + "label": "Riproduci video di avvisi", + "desc": "Per impostazione predefinita, gli avvisi recenti nel cruscotto dal vivo vengono riprodotti come brevi video in ciclo. Disabilita questa opzione per visualizzare solo un'immagine statica degli avvisi recenti su questo dispositivo/browser." + }, + "title": "Cruscotto dal vivo", + "displayCameraNames": { + "label": "Mostra sempre i nomi delle telecamere", + "desc": "Mostra sempre i nomi delle telecamere in una scheda nel cruscotto della visualizzazione dal vivo multi telecamera." + }, + "liveFallbackTimeout": { + "label": "Scadenza attesa lettore dal vivo", + "desc": "Quando la trasmissione dal vivo ad alta qualità di una telecamera non è disponibile, dopo questo numero di secondi torna alla modalità a bassa larghezza di banda. Valore predefinito: 3." + } + }, + "title": "Impostazioni interfaccia utente", + "storedLayouts": { + "title": "Formati memorizzati", + "desc": "La disposizione delle telecamere in un gruppo può essere trascinata/ridimensionata. Le posizioni vengono salvate nella memoria locale del browser.", + "clearAll": "Cancella tutti i formati" + }, + "cameraGroupStreaming": { + "title": "Impostazioni di trasmissione del gruppo di telecamere", + "desc": "Le impostazioni di trasmissione per ciascun gruppo di telecamere vengono salvate nella memoria locale del browser.", + "clearAll": "Cancella tutte le impostazioni di trasmissione" + }, + "recordingsViewer": { + "title": "Visualizzatore registrazioni", + "defaultPlaybackRate": { + "label": "Velocità di riproduzione predefinita", + "desc": "Velocità di riproduzione predefinita per la riproduzione delle registrazioni." + } + }, + "calendar": { + "title": "Calendario", + "firstWeekday": { + "label": "Primo giorno della settimana", + "desc": "Giorno in cui iniziano le settimane del calendario di revisione.", + "sunday": "Domenica", + "monday": "Lunedì" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Formato memorizzato cancellato per {{cameraName}}", + "clearStreamingSettings": "Cancellate le impostazioni di trasmissione per tutti i gruppi di telecamere." + }, + "error": { + "clearStoredLayoutFailed": "Impossibile cancellare il formato memorizzato: {{errorMessage}}", + "clearStreamingSettingsFailed": "Impossibile cancellare le impostazioni di trasmissione: {{errorMessage}}" + } + } + }, + "classification": { + "licensePlateRecognition": { + "desc": "Frigate può riconoscere le targhe dei veicoli e aggiungere automaticamente i caratteri rilevati al campo recognized_license_plate o un nome noto come sub_label agli oggetti di tipo \"automobile\". Un caso d'uso comune potrebbe essere la lettura delle targhe delle auto che entrano in un vialetto o che transitano lungo una strada.", + "title": "Riconoscimento della targa", + "readTheDocumentation": "Leggi la documentazione" + }, + "title": "Impostazioni di classificazione", + "semanticSearch": { + "title": "Ricerca semantica", + "desc": "La ricerca semantica in Frigate consente di trovare gli oggetti tracciati all'interno degli elementi della recensione utilizzando l'immagine stessa, una descrizione testuale definita dall'utente o una generata automaticamente.", + "readTheDocumentation": "Leggi la documentazione", + "modelSize": { + "large": { + "title": "grande", + "desc": "L'utilizzo di large sfrutta il modello Jina completo e, se applicabile, verrà eseguito automaticamente sulla GPU." + }, + "small": { + "desc": "L'utilizzo di small sfrutta una versione quantizzata del modello che utilizza meno RAM ed è più veloce sulla CPU con una differenza davvero trascurabile nella qualità di incorporamento.", + "title": "piccolo" + }, + "label": "Dimensioni del modello", + "desc": "Dimensioni del modello utilizzato per gli incorporamenti della ricerca semantica." + }, + "reindexNow": { + "label": "Reindicizza ora", + "desc": "La reindicizzazione rigenererà gli incorporamenti per tutti gli oggetti tracciati. Questo processo viene eseguito in sottofondo e potrebbe impegnare al massimo la CPU e richiedere un tempo considerevole, a seconda del numero di oggetti tracciati.", + "confirmTitle": "Conferma reindicizzazione", + "confirmDesc": "Vuoi davvero reindicizzare tutti gli incorporamenti di oggetti tracciati? Questo processo verrà eseguito in sottofondo, ma potrebbe impegnare al massimo la CPU e richiedere molto tempo. Puoi monitorare l'avanzamento nella pagina Esplora.", + "confirmButton": "Reindicizza", + "success": "Reindicizzazione avviata con successo.", + "alreadyInProgress": "La reindicizzazione è già in corso.", + "error": "Impossibile avviare la reindicizzazione: {{errorMessage}}" + } + }, + "faceRecognition": { + "title": "Riconoscimento facciale", + "desc": "Il riconoscimento facciale consente di assegnare un nome alle persone e, quando il volto viene riconosciuto, Frigate assegnerà il nome della persona come sottoetichetta. Queste informazioni sono incluse nell'interfaccia utente, nei filtri e nelle notifiche.", + "modelSize": { + "desc": "Dimensioni del modello utilizzato per il riconoscimento facciale.", + "small": { + "title": "piccolo", + "desc": "L'utilizzo di small sfrutta un modello di incorporamento di volti FaceNet che funziona in modo efficiente sulla maggior parte delle CPU." + }, + "large": { + "title": "grande", + "desc": "L'utilizzo di large sfrutta un modello di incorporamento di volti ArcFace e verrà eseguito automaticamente sulla GPU, se applicabile." + }, + "label": "Dimensioni del modello" + }, + "readTheDocumentation": "Leggi la documentazione" + }, + "toast": { + "error": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}", + "success": "Le impostazioni di classificazione sono state salvate. Riavvia Frigate per applicare le modifiche." + }, + "birdClassification": { + "desc": "La classificazione degli uccelli identifica gli uccelli noti utilizzando un modello Tensorflow quantizzato. Quando un uccello noto viene riconosciuto, il suo nome comune viene aggiunto come sub_label. Queste informazioni sono incluse nell'interfaccia utente, nei filtri e nelle notifiche.", + "title": "Classificazione degli uccelli" + }, + "restart_required": "Riavvio richiesto (impostazioni di classificazione modificate)", + "unsavedChanges": "Modifiche alle impostazioni di classificazione non salvate" + }, + "dialog": { + "unsavedChanges": { + "title": "Sono presenti modifiche non salvate.", + "desc": "Vuoi salvare le modifiche prima di continuare?" + } + }, + "motionDetectionTuner": { + "improveContrast": { + "title": "Migliora il contrasto", + "desc": "Migliora il contrasto nelle scene più scure. Predefinito: ATTIVO" + }, + "desc": { + "title": "Frigate utilizza il rilevamento del movimento come primo controllo per verificare se nell'inquadratura si verifica qualcosa che valga la pena verificare tramite il rilevamento degli oggetti.", + "documentation": "Leggi la Guida alla Regolazione del Movimento" + }, + "title": "Regolatore di rilevamento del movimento", + "contourArea": { + "title": "Area di contorno", + "desc": "Il valore dell'area del contorno viene utilizzato per decidere quali gruppi di pixel modificati sono considerati movimento. Predefinito: 10" + }, + "Threshold": { + "title": "Soglia", + "desc": "Il valore di soglia determina l'entità della variazione di luminanza di un pixel necessaria per essere considerato movimento. Predefinito: 30" + }, + "toast": { + "success": "Le impostazioni di movimento sono state salvate." + }, + "unsavedChanges": "Modifiche non salvate del regolatore di movimento ({{camera}})" + }, + "notification": { + "email": { + "placeholder": "es. esempio@email.com", + "desc": "È richiesto un indirizzo email valido che verrà utilizzato per avvisarti in caso di problemi con il servizio push.", + "title": "Email" + }, + "cameras": { + "title": "Telecamere", + "noCameras": "Nessuna telecamera disponibile", + "desc": "Seleziona per quali telecamere abilitare le notifiche." + }, + "unregisterDevice": "Annulla la registrazione di questo dispositivo", + "sendTestNotification": "Invia una notifica di prova", + "active": "Notifiche attive", + "suspended": "Notifiche sospese {{time}}", + "suspendTime": { + "5minutes": "Sospendi per 5 minuti", + "30minutes": "Sospendi per 30 minuti", + "10minutes": "Sospendi per 10 minuti", + "1hour": "Sospendi per 1 ora", + "12hours": "Sospendi per 12 ore", + "24hours": "Sospendi per 24 ore", + "untilRestart": "Sospendi fino al riavvio", + "suspend": "Sospendi" + }, + "globalSettings": { + "desc": "Sospendi temporaneamente le notifiche per telecamere specifiche su tutti i dispositivi registrati.", + "title": "Impostazioni globali" + }, + "registerDevice": "Registra questo dispositivo", + "notificationUnavailable": { + "desc": "Le notifiche push web richiedono un contesto sicuro (https://...). Questa è una limitazione del browser. Accedi a Frigate in modo sicuro per utilizzare le notifiche.", + "documentation": "Leggi la documentazione", + "title": "Notifiche non disponibili", + "descPwa": "Su iOS, le notifiche push web sono disponibili solo se Frigate è installato sulla schermata Home. Apri il menu Condividi, scegli Aggiungi alla schermata Home, quindi apri Frigate dalla nuova icona per registrare questo dispositivo per le notifiche." + }, + "deviceSpecific": "Impostazioni specifiche del dispositivo", + "toast": { + "success": { + "registered": "Registrazione per le notifiche completata con successo. È necessario riavviare Frigate prima di poter inviare qualsiasi notifica (inclusa una notifica di prova).", + "settingSaved": "Le impostazioni di notifica sono state salvate." + }, + "error": { + "registerFailed": "Impossibile salvare la registrazione della notifica." + } + }, + "title": "Notifiche", + "notificationSettings": { + "title": "Impostazioni di notifica", + "desc": "Frigate può inviare notifiche push in modo nativo al tuo dispositivo quando è in esecuzione nel browser o installato come PWA.", + "documentation": "Leggi la documentazione" + }, + "cancelSuspension": "Annulla sospensione", + "unsavedRegistrations": "Registrazioni di notifiche non salvate", + "unsavedChanges": "Modifiche alle notifiche non salvate" + }, + "enrichments": { + "toast": { + "success": "Le impostazioni di miglioramento sono state salvate. Riavvia Frigate per applicare le modifiche.", + "error": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}" + }, + "title": "Impostazioni di miglioramento", + "semanticSearch": { + "reindexNow": { + "desc": "La reindicizzazione rigenererà gli incorporamenti per tutti gli oggetti tracciati. Questo processo viene eseguito in sottofondo e potrebbe impegnare al massimo la CPU e richiedere un tempo considerevole, a seconda del numero di oggetti tracciati.", + "success": "Reindicizzazione avviata con successo.", + "label": "Reindicizza ora", + "confirmTitle": "Conferma reindicizzazione", + "confirmDesc": "Vuoi davvero reindicizzare tutti gli incorporamenti di oggetti tracciati? Questo processo verrà eseguito in sottofondo, ma potrebbe impegnare al massimo la CPU e richiedere molto tempo. Puoi monitorare l'avanzamento nella pagina Esplora.", + "confirmButton": "Reindicizza", + "alreadyInProgress": "La reindicizzazione è già in corso.", + "error": "Impossibile avviare la reindicizzazione: {{errorMessage}}" + }, + "modelSize": { + "small": { + "desc": "Utilizzando piccolo si utilizza una versione quantizzata del modello che utilizza meno RAM ed è più veloce sulla CPU con una differenza davvero trascurabile nella qualità di incorporamento.", + "title": "piccolo" + }, + "label": "Dimensioni del modello", + "desc": "Dimensione del modello utilizzato per gli incorporamenti della ricerca semantica.", + "large": { + "title": "grande", + "desc": "L'utilizzo di grande utilizza il modello Jina completo e, se applicabile, verrà eseguito automaticamente sulla GPU." + } + }, + "title": "Ricerca semantica", + "desc": "La ricerca semantica in Frigate consente di trovare gli oggetti tracciati all'interno degli elementi della recensione utilizzando l'immagine stessa, una descrizione testuale definita dall'utente o una generata automaticamente.", + "readTheDocumentation": "Leggi la documentazione" + }, + "faceRecognition": { + "desc": "Il riconoscimento facciale consente di assegnare un nome alle persone e, quando il volto viene riconosciuto, Frigate assegnerà il nome della persona come sottoetichetta. Queste informazioni sono incluse nell'interfaccia utente, nei filtri e nelle notifiche.", + "title": "Riconoscimento facciale", + "readTheDocumentation": "Leggi la documentazione", + "modelSize": { + "label": "Dimensioni del modello", + "desc": "Dimensioni del modello utilizzato per il riconoscimento facciale.", + "small": { + "title": "piccolo", + "desc": "L'utilizzo di piccolo sfrutta un modello di incorporamento di volti FaceNet che funziona in modo efficiente sulla maggior parte delle CPU." + }, + "large": { + "title": "grande", + "desc": "L'utilizzo di grande impiega un modello di incorporamento di volti ArcFace e verrà eseguito automaticamente sulla GPU, se applicabile." + } + } + }, + "birdClassification": { + "desc": "La classificazione degli uccelli identifica gli uccelli noti utilizzando un modello Tensorflow quantizzato. Quando un uccello noto viene riconosciuto, il suo nome comune viene aggiunto come sub_label. Queste informazioni sono incluse nell'interfaccia utente, nei filtri e nelle notifiche.", + "title": "Classificazione uccelli" + }, + "licensePlateRecognition": { + "desc": "Frigate può riconoscere le targhe dei veicoli e aggiungere automaticamente i caratteri rilevati al campo recognized_license_plate o un nome noto come sub_label agli oggetti di tipo automobile (car). Un caso d'uso comune potrebbe essere la lettura delle targhe delle auto che entrano in un vialetto o che transitano lungo una strada.", + "title": "Riconoscimento targhe", + "readTheDocumentation": "Leggi la documentazione" + }, + "unsavedChanges": "Modifiche alle impostazioni di miglioramento non salvate", + "restart_required": "Riavvio richiesto (impostazioni di miglioramento modificate)" + }, + "triggers": { + "documentTitle": "Attivatori", + "management": { + "title": "Attivatori", + "desc": "Gestisci gli attivatori per {{camera}}. Utilizza il tipo miniatura per attivare miniature simili all'oggetto tracciato selezionato e il tipo descrizione per attivare descrizioni simili al testo specificato." + }, + "addTrigger": "Aggiungi attivatore", + "table": { + "name": "Nome", + "type": "Tipo", + "content": "Contenuto", + "threshold": "Soglia", + "actions": "Azioni", + "noTriggers": "Nessun attivatore configurato per questa telecamera.", + "edit": "Modifica", + "deleteTrigger": "Elimina attivatore", + "lastTriggered": "Ultimo attivatore" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descrizione" + }, + "actions": { + "alert": "Contrassegna come avviso", + "notification": "Invia notifica", + "sub_label": "Aggiungi sottoetichetta", + "attribute": "Aggiungi attributo" + }, + "dialog": { + "createTrigger": { + "title": "Crea attivatore", + "desc": "Crea un attivatore per la telecamera {{camera}}" + }, + "editTrigger": { + "title": "Modifica attivatore", + "desc": "Modifica le impostazioni per l'attivatore della telecamera {{camera}}" + }, + "deleteTrigger": { + "title": "Elimina attivatore", + "desc": "Vuoi davvero eliminare l'attivatore {{triggerName}}? Questa azione non può essere annullata." + }, + "form": { + "name": { + "title": "Nome", + "placeholder": "Assegna un nome a questo attivatore", + "error": { + "minLength": "Il campo deve contenere almeno 2 caratteri.", + "invalidCharacters": "Il campo può contenere solo lettere, numeri, caratteri di sottolineatura e trattini.", + "alreadyExists": "Per questa telecamera esiste già un attivatore con questo nome." + }, + "description": "Inserisci un nome o una descrizione univoca per identificare questo attivatore" + }, + "enabled": { + "description": "Abilita o disabilita questo attivatore" + }, + "type": { + "title": "Tipo", + "placeholder": "Seleziona il tipo di attivatore", + "description": "Si attiva quando viene rilevata una descrizione di un oggetto simile tracciato", + "thumbnail": "Attiva quando viene rilevata una miniatura di un oggetto simile tracciato" + }, + "content": { + "title": "Contenuto", + "imagePlaceholder": "Seleziona una miniatura", + "textPlaceholder": "Inserisci il contenuto del testo", + "imageDesc": "Vengono visualizzate solo le 100 miniature più recenti. Se non riesci a trovare la miniatura desiderata, controlla gli oggetti precedenti in Esplora e imposta un attivatore dal menu.", + "textDesc": "Inserisci il testo per attivare questa azione quando viene rilevata una descrizione simile dell'oggetto tracciato.", + "error": { + "required": "Il contenuto è obbligatorio." + } + }, + "threshold": { + "title": "Soglia", + "error": { + "min": "La soglia deve essere almeno 0", + "max": "La soglia deve essere al massimo 1" + }, + "desc": "Imposta la soglia di similarità per questo attivatore. Una soglia più alta indica che è necessaria una corrispondenza più vicina per attivare l'attivatore." + }, + "actions": { + "title": "Azioni", + "desc": "Per impostazione predefinita, Frigate invia un messaggio MQTT per tutti gli attivatori. Le sottoetichette aggiungono il nome dell'attivatore all'etichetta dell'oggetto. Gli attributi sono metadati ricercabili, memorizzati separatamente nei metadati dell'oggetto tracciato.", + "error": { + "min": "È necessario selezionare almeno un'azione." + } + }, + "friendly_name": { + "title": "Nome semplice", + "placeholder": "Assegna un nome o descrivi questo innesco", + "description": "Un nome semplice o un testo descrittivo facoltativo per questo innesco." + } + } + }, + "toast": { + "success": { + "createTrigger": "L'attivatore {{name}} è stato creato correttamente.", + "updateTrigger": "L'attivatore {{name}} è stato aggiornato correttamente.", + "deleteTrigger": "L'attivatore {{name}} è stato eliminato correttamente." + }, + "error": { + "createTriggerFailed": "Impossibile creare l'attivatore: {{errorMessage}}", + "updateTriggerFailed": "Impossibile aggiornare l'attivatore: {{errorMessage}}", + "deleteTriggerFailed": "Impossibile eliminare l'attivatore: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "La ricerca semantica è disabilitata", + "desc": "Per utilizzare gli attivatori, è necessario abilitare la ricerca semantica." + }, + "wizard": { + "title": "Crea attivatore", + "step1": { + "description": "Configura le impostazioni di base per il tuo attivatore." + }, + "step2": { + "description": "Imposta il contenuto che attiverà questa azione." + }, + "step3": { + "description": "Configura la soglia e le azioni per questo attivatore." + }, + "steps": { + "nameAndType": "Nome e tipo", + "configureData": "Configurare i dati", + "thresholdAndActions": "Soglia e azioni" + } + } + }, + "roles": { + "management": { + "title": "Gestione del ruolo visualizzatore", + "desc": "Gestisci i ruoli di visualizzatori personalizzati e le relative autorizzazioni di accesso alla telecamera per questa istanza Frigate." + }, + "addRole": "Aggiungi ruolo", + "table": { + "role": "Ruolo", + "cameras": "Telecamere", + "actions": "Azioni", + "noRoles": "Nessun ruolo personalizzato trovato.", + "editCameras": "Modifica telecamere", + "deleteRole": "Elimina ruolo" + }, + "toast": { + "success": { + "createRole": "Ruolo {{role}} creato con successo", + "updateCameras": "Telecamere aggiornate per il ruolo {{role}}", + "deleteRole": "Ruolo {{role}} eliminato con successo", + "userRolesUpdated_one": "{{count}} utente assegnato a questo ruolo è stato aggiornato a \"visualizzatore\", che ha accesso a tutte le telecamere.", + "userRolesUpdated_many": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"visualizzatore\", che ha accesso a tutte le telecamere.", + "userRolesUpdated_other": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"visualizzatore\", che ha accesso a tutte le telecamere." + }, + "error": { + "createRoleFailed": "Impossibile creare il ruolo: {{errorMessage}}", + "updateCamerasFailed": "Impossibile aggiornare le telecamere: {{errorMessage}}", + "deleteRoleFailed": "Impossibile eliminare il ruolo: {{errorMessage}}", + "userUpdateFailed": "Impossibile aggiornare i ruoli utente: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Crea nuovo ruolo", + "desc": "Aggiungi un nuovo ruolo e specifica le autorizzazioni di accesso alla telecamera." + }, + "editCameras": { + "title": "Modifica telecamere di ruolo", + "desc": "Aggiorna l'accesso alla telecamera per il ruolo {{role}}." + }, + "deleteRole": { + "title": "Elimina ruolo", + "desc": "Questa azione non può essere annullata. Ciò eliminerà definitivamente il ruolo e assegnerà a tutti gli utenti il ruolo di 'visualizzatore', che darà loro accesso a tutte le telecamere.", + "warn": "Sei sicuro di voler eliminare {{role}}?", + "deleting": "Eliminazione in corso..." + }, + "form": { + "role": { + "title": "Nome del ruolo", + "placeholder": "Inserisci il nome del ruolo", + "desc": "Sono consentiti solo lettere, numeri, punti e caratteri di sottolineatura.", + "roleIsRequired": "Il nome del ruolo è obbligatorio", + "roleOnlyInclude": "Il nome del ruolo può includere solo lettere, numeri, . o _", + "roleExists": "Esiste già un ruolo con questo nome." + }, + "cameras": { + "title": "Telecamere", + "desc": "Seleziona le telecamere a cui questo ruolo ha accesso. È richiesta almeno una telecamera.", + "required": "È necessario selezionare almeno una telecamera." + } + } + } + }, + "cameraReview": { + "title": "Impostazioni revisione telecamera", + "object_descriptions": { + "title": "Descrizioni oggetti IA Generativa", + "desc": "Abilita/disabilita temporaneamente le descrizioni degli oggetti generate dall'IA per questa telecamera fino al riavvio di Frigate. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli oggetti tracciati su questa telecamera." + }, + "review_descriptions": { + "title": "Descrizioni revisioni IA Generativa", + "desc": "Abilita/disabilita temporaneamente le descrizioni di revisione generate dall'IA per questa telecamera fino al riavvio di Frigate. Se disabilitate, le descrizioni generate dall'IA non saranno richieste per gli elementi di revisione su questa telecamera." + }, + "review": { + "title": "Revisiona", + "desc": "Abilita/disabilita temporaneamente avvisi e rilevamenti per questa telecamera fino al riavvio di Frigate. Se disabilitato, non verranno generati nuovi elementi di revisione. ", + "alerts": "Avvisi ", + "detections": "Rilevamenti " + }, + "reviewClassification": { + "title": "Classificazione revisione", + "desc": "Frigate categorizza gli elementi di revisione come Avvisi e Rilevamenti. Per impostazione predefinita, tutti gli oggetti persona e auto sono considerati Avvisi. È possibile perfezionare la categorizzazione degli elementi di revisione configurando le zone richieste per ciascuno di essi.", + "noDefinedZones": "Per questa telecamera non sono definite zone.", + "objectAlertsTips": "Tutti gli oggetti {{alertsLabels}} su {{cameraName}} verranno mostrati come Avvisi.", + "zoneObjectAlertsTips": "Tutti gli oggetti {{alertsLabels}} rilevati in {{zone}} su {{cameraName}} verranno mostrati come Avvisi.", + "objectDetectionsTips": "Tutti gli oggetti {{detectionsLabels}} non categorizzati su {{cameraName}} verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano.", + "zoneObjectDetectionsTips": { + "text": "Tutti gli oggetti {{detectionsLabels}} non categorizzati in {{zone}} su {{cameraName}} verranno mostrati come Rilevamenti.", + "notSelectDetections": "Tutti gli oggetti {{detectionsLabels}} rilevati in {{zone}} su {{cameraName}} non classificati come Avvisi verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano.", + "regardlessOfZoneObjectDetectionsTips": "Tutti gli oggetti {{detectionsLabels}} non categorizzati su {{cameraName}} verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano." + }, + "unsavedChanges": "Impostazioni di classificazione delle revisioni non salvate per {{camera}}", + "selectAlertsZones": "Seleziona le zone per gli Avvisi", + "selectDetectionsZones": "Seleziona le zone per i Rilevamenti", + "limitDetections": "Limita i rilevamenti a zone specifiche", + "toast": { + "success": "La configurazione della classificazione di revisione è stata salvata. Riavvia Frigate per applicare le modifiche." + } + } + }, + "cameraWizard": { + "step3": { + "streamUnavailable": "Anteprima trasmissione non disponibile", + "description": "Configura i ruoli del flusso e aggiungi altri flussi alla tua telecamera.", + "validationTitle": "Convalida del flusso", + "connectAllStreams": "Connetti tutti i flussi", + "reconnectionSuccess": "Riconnessione riuscita.", + "reconnectionPartial": "Alcuni flussi non sono riusciti a riconnettersi.", + "reload": "Ricarica", + "connecting": "Connessione...", + "streamTitle": "Flusso {{number}}", + "valid": "Convalida", + "failed": "Fallito", + "notTested": "Non verificata", + "connectStream": "Connetti", + "connectingStream": "Connessione", + "disconnectStream": "Disconnetti", + "estimatedBandwidth": "Larghezza di banda stimata", + "roles": "Ruoli", + "none": "Nessuno", + "error": "Errore", + "streamValidated": "Flusso {{number}} convalidato con successo", + "streamValidationFailed": "Convalida del flusso {{number}} non riuscita", + "saveAndApply": "Salva nuova telecamera", + "saveError": "Configurazione non valida. Controlla le impostazioni.", + "issues": { + "title": "Convalida del flusso", + "videoCodecGood": "Il codec video è {{codec}}.", + "audioCodecGood": "Il codec audio è {{codec}}.", + "noAudioWarning": "Nessun audio rilevato per questo flusso, le registrazioni non avranno audio.", + "audioCodecRecordError": "Per supportare l'audio nelle registrazioni è necessario il codec audio AAC.", + "audioCodecRequired": "Per supportare il rilevamento audio è necessario un flusso audio.", + "restreamingWarning": "Riducendo le connessioni alla telecamera per il flusso di registrazione l'utilizzo della CPU potrebbe aumentare leggermente.", + "dahua": { + "substreamWarning": "Il flusso 1 è bloccato a bassa risoluzione. Molte telecamere Dahua/Amcrest/EmpireTech supportano flussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili." + }, + "hikvision": { + "substreamWarning": "Il flusso 1 è bloccato a bassa risoluzione. Molte telecamere Hikvision supportano flussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili." + }, + "resolutionHigh": "Una risoluzione di {{resolution}} potrebbe causare un aumento dell'utilizzo delle risorse.", + "resolutionLow": "Una risoluzione di {{resolution}} potrebbe essere troppo bassa per un rilevamento affidabile di oggetti di piccole dimensioni." + }, + "ffmpegModule": "Utilizza la modalità di compatibilità della trasmissione", + "ffmpegModuleDescription": "Se il flusso non si carica dopo diversi tentativi, prova ad abilitare questa opzione. Se abilitata, Frigate utilizzerà il modulo ffmpeg con go2rtc. Questo potrebbe garantire una migliore compatibilità con alcuni flussi di telecamere.", + "streamsTitle": "Flussi della telecamera", + "addStream": "Aggiungi flusso", + "addAnotherStream": "Aggiungi un altro flusso", + "streamUrl": "URL del flusso", + "streamUrlPlaceholder": "rtsp://nomeutente:password@sistema:porta/percorso", + "selectStream": "Seleziona un flusso", + "searchCandidates": "Ricerca candidati in corso...", + "noStreamFound": "Nessun flusso trovato", + "url": "URL", + "resolution": "Risoluzione", + "selectResolution": "Seleziona la risoluzione", + "quality": "Qualità", + "selectQuality": "Seleziona la qualità", + "roleLabels": { + "detect": "Rilevamento oggetti", + "record": "Registrazione", + "audio": "Audio" + }, + "testStream": "Prova di connessione", + "testSuccess": "Prova del flusso riuscita!", + "testFailed": "Prova del flusso fallita", + "testFailedTitle": "Prova fallita", + "connected": "Connessa", + "notConnected": "Non connessa", + "featuresTitle": "Caratteristiche", + "go2rtc": "Riduci le connessioni alla telecamera", + "detectRoleWarning": "Per procedere, almeno un flusso deve avere il ruolo \"rilevamento\".", + "rolesPopover": { + "title": "Ruoli del flusso", + "detect": "Flusso principale per il rilevamento degli oggetti.", + "record": "Salva segmenti del flusso video in base alle impostazioni di configurazione.", + "audio": "Flusso per il rilevamento basato sull'audio." + }, + "featuresPopover": { + "title": "Caratteristiche del flusso", + "description": "Utilizza la ritrasmissione go2rtc per ridurre le connessioni alla tua telecamera." + } + }, + "title": "Aggiungi telecamera", + "description": "Per aggiungere una nuova telecamera alla tua installazione Frigate, segui i passaggi indicati di seguito.", + "steps": { + "nameAndConnection": "Nome e connessione", + "streamConfiguration": "Configurazione flusso", + "validationAndTesting": "Validazione e prova", + "probeOrSnapshot": "Analisi o istantanea" + }, + "save": { + "success": "Nuova telecamera {{cameraName}} salvata correttamente.", + "failure": "Errore durante il salvataggio di {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Risoluzione", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Fornisci un URL di flusso valido", + "testFailed": "Prova del flusso fallita: {{error}}" + }, + "step1": { + "description": "Inserisci i dettagli della tua telecamera e scegli se analizzarla o selezionarne manualmente la marca.", + "cameraName": "Nome telecamera", + "cameraNamePlaceholder": "ad esempio, porta_anteriore o Panoramica cortile", + "host": "Indirizzo sistema/IP", + "port": "Porta", + "username": "Nome utente", + "usernamePlaceholder": "Opzionale", + "password": "Password", + "passwordPlaceholder": "Opzionale", + "selectTransport": "Seleziona il protocollo di trasmissione", + "cameraBrand": "Marca telecamera", + "selectBrand": "Seleziona la marca della telecamera per il modello URL", + "customUrl": "URL del flusso personalizzato", + "brandInformation": "Informazioni sul marchio", + "brandUrlFormat": "Per le telecamere con formato URL RTSP come: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nomeutente:password@sistema:porta/percorso", + "testConnection": "Prova connessione", + "testSuccess": "Prova di connessione riuscita!", + "testFailed": "Prova di connessione fallita. Controlla i dati immessi e riprova.", + "streamDetails": "Dettagli del flusso", + "warnings": { + "noSnapshot": "Impossibile recuperare un'immagine dal flusso configurato." + }, + "errors": { + "brandOrCustomUrlRequired": "Seleziona una marca di telecamera con sistema/IP oppure scegli \"Altro\" con un URL personalizzato", + "nameRequired": "Il nome della telecamera è obbligatorio", + "nameLength": "Il nome della telecamera deve contenere al massimo 64 caratteri", + "invalidCharacters": "Il nome della telecamera contiene caratteri non validi", + "nameExists": "Il nome della telecamera esiste già", + "brands": { + "reolink-rtsp": "Reolink RTSP non è consigliato. Abilita HTTP nelle impostazioni del firmware della telecamera e riavvia la procedura guidata." + }, + "customUrlRtspRequired": "Gli URL personalizzati devono iniziare con \"rtsp://\" o \"rtsps://\". La configurazione manuale è necessaria per i flussi video non RTSP." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Analisi dei metadati della telecamera in corso...", + "fetchingSnapshot": "Recupero istantanea della telecamera in corso..." + }, + "probeMode": "Analisi telecamera", + "detectionMethodDescription": "Analizza la telecamera con ONVIF (se supportato) per trovare gli URL dei flussi video della telecamera oppure seleziona manualmente la marca della telecamera per utilizzare URL predefiniti. Per inserire un URL RTSP personalizzato, scegli il metodo manuale e seleziona \"Altro\".", + "connectionSettings": "Impostazioni di connessione", + "detectionMethod": "Metodo di rilevamento del flusso", + "onvifPort": "Porta ONVIF", + "manualMode": "Selezione manuale", + "onvifPortDescription": "Per le telecamere che supportano ONVIF, in genere è 80 o 8080.", + "useDigestAuth": "Utilizza l'autenticazione digest", + "useDigestAuthDescription": "Utilizza l'autenticazione HTTP digest per ONVIF. Alcune telecamere potrebbero richiedere un nome utente e una password ONVIF dedicati, anziché l'utente amministratore classico." + }, + "step2": { + "description": "Analizza la telecamera per individuare i flussi disponibili oppure configura le impostazioni manuali in base al metodo di rilevamento selezionato.", + "streamsTitle": "Flussi della telecamera", + "addStream": "Aggiungi flusso", + "addAnotherStream": "Aggiungi un altro flusso", + "streamTitle": "Flusso {{number}}", + "streamUrl": "URL del flusso", + "streamUrlPlaceholder": "rtsp://nomeutente:password@sistema:porta/percorso", + "url": "URL", + "resolution": "Risoluzione", + "selectResolution": "Seleziona la risoluzione", + "quality": "Qualità", + "selectQuality": "Seleziona la qualità", + "roles": "Ruoli", + "roleLabels": { + "detect": "Rilevamento oggetti", + "record": "Registrazione", + "audio": "Audio" + }, + "testStream": "Prova connessione", + "testSuccess": "Prova di connessione riuscita!", + "testFailed": "Prova di connessione fallita. Controlla i dati inseriti e riprova.", + "testFailedTitle": "Prova fallita", + "connected": "Connessa", + "notConnected": "Non connessa", + "featuresTitle": "Caratteristiche", + "go2rtc": "Riduci le connessioni alla telecamera", + "detectRoleWarning": "Per procedere, almeno un flusso deve avere il ruolo \"rileva\".", + "rolesPopover": { + "title": "Ruoli del flusso", + "detect": "Flusso principale per il rilevamento degli oggetti.", + "record": "Salva segmenti del flusso video in base alle impostazioni di configurazione.", + "audio": "Flusso per il rilevamento basato sull'audio." + }, + "featuresPopover": { + "title": "Caratteristiche del flusso", + "description": "Utilizza la ritrasmissione go2rtc per ridurre le connessioni alla tua telecamera." + }, + "probeFailed": "Impossibile analizzare la telecamera: {{error}}", + "probeSuccessful": "Analisi riuscita", + "probeError": "Errore analisi", + "probeNoSuccess": "Analisi non riuscita", + "rtspCandidatesDescription": "I seguenti URL RTSP sono stati trovati dall'analisi della telecamera. Prova la connessione per visualizzare i metadati della trasmissione.", + "streamDetails": "Dettagli del flusso", + "probing": "Analisi telecamera in corso...", + "retry": "Riprova", + "testing": { + "probingMetadata": "Analisi dei metadati della telecamera in corso...", + "fetchingSnapshot": "Recupero dell'istantanea della telecamera in corso..." + }, + "probingDevice": "Analisi del dispositivo in corso...", + "deviceInfo": "Informazioni sul dispositivo", + "manufacturer": "Produttore", + "model": "Modello", + "firmware": "Firmware", + "profiles": "Profili", + "ptzSupport": "Supporto PTZ", + "autotrackingSupport": "Supporto per il tracciamento automatico", + "presets": "Preimpostazioni", + "rtspCandidates": "Candidati RTSP", + "noRtspCandidates": "Nessun URL RTSP trovato dalla telecamera. Le credenziali potrebbero essere errate oppure la telecamera potrebbe non supportare ONVIF o il metodo utilizzato per recuperare gli URL RTSP. Torna indietro e inserisci manualmente l'URL RTSP.", + "candidateStreamTitle": "Candidato {{number}}}}", + "useCandidate": "Utilizza", + "uriCopy": "Copia", + "uriCopied": "URI copiato negli appunti", + "testConnection": "Prova di connessione", + "toggleUriView": "Fai clic per attivare/disattivare la visualizzazione completa dell'URI", + "errors": { + "hostRequired": "È richiesto il nome sistema/indirizzo IP" + } + }, + "step4": { + "description": "Convalida e analisi finale prima di salvare la nuova telecamera. Collega ogni flusso prima di salvare.", + "validationTitle": "Validazione del flusso", + "connectAllStreams": "Connetti tutti i flussi", + "reconnectionSuccess": "Riconnessione riuscita.", + "reconnectionPartial": "Alcuni flussi non sono riusciti a riconnettersi.", + "streamUnavailable": "Anteprima del flusso non disponibile", + "reload": "Ricarica", + "connecting": "Connessione in corso...", + "streamTitle": "Flusso {{number}}", + "valid": "Valida", + "failed": "Fallito", + "notTested": "Non verificato", + "connectStream": "Connetti", + "connectingStream": "Connessione in corso", + "disconnectStream": "Disconnetti", + "estimatedBandwidth": "Larghezza di banda stimata", + "roles": "Ruoli", + "ffmpegModule": "Utilizza la modalità di compatibilità del flusso", + "ffmpegModuleDescription": "Se il flusso non si carica dopo diversi tentativi, prova ad abilitare questa opzione. Se abilitata, Frigate utilizzerà il modulo ffmpeg con go2rtc. Questo potrebbe garantire una migliore compatibilità con alcuni flussi di telecamere.", + "none": "Nessuna", + "error": "Errore", + "streamValidated": "Flusso {{number}} convalidato con successo", + "streamValidationFailed": "Convalida del flusso {{number}} non riuscita", + "saveAndApply": "Salva nuova telecamera", + "saveError": "Configurazione non valida. Controlla le impostazioni.", + "issues": { + "title": "Validazione del flusso", + "videoCodecGood": "Il codec video è {{codec}}.", + "audioCodecGood": "Il codec audio è {{codec}}.", + "resolutionHigh": "Una risoluzione di {{resolution}} potrebbe causare un aumento dell'utilizzo delle risorse.", + "resolutionLow": "Una risoluzione di {{resolution}} potrebbe essere troppo bassa per un rilevamento affidabile di oggetti di piccole dimensioni.", + "noAudioWarning": "Nessun audio rilevato per questo flusso, le registrazioni non avranno audio.", + "audioCodecRecordError": "Per supportare l'audio nelle registrazioni è necessario il codec audio AAC.", + "audioCodecRequired": "Per supportare il rilevamento audio è necessario un flusso audio.", + "restreamingWarning": "Riducendo le connessioni alla telecamera per il flusso di registrazione l'utilizzo della CPU potrebbe aumentare leggermente.", + "brands": { + "reolink-rtsp": "Reolink RTSP non è consigliato. Abilita HTTP nelle impostazioni del firmware della telecamera e riavvia la procedura guidata.", + "reolink-http": "I flussi HTTP di Reolink dovrebbero utilizzare FFmpeg per una migliore compatibilità. Abilita \"Usa modalità compatibilità flusso\" per questo flusso." + }, + "dahua": { + "substreamWarning": "Il sottoflusso 1 è bloccato a bassa risoluzione. Molte telecamere Dahua/Amcrest/EmpireTech supportano sottoflussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili." + }, + "hikvision": { + "substreamWarning": "Il sottoflusso 1 è bloccato a bassa risoluzione. Molte telecamere Hikvision supportano sottoflussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili." + }, + "resolutionUnknown": "Non è stato possibile rilevare la risoluzione di questo flusso. È necessario impostare manualmente la risoluzione di rilevamento nelle Impostazioni o nella configurazione." + } + } + }, + "cameraManagement": { + "title": "Gestisci telecamere", + "addCamera": "Aggiungi nuova telecamera", + "editCamera": "Modifica telecamera:", + "selectCamera": "Seleziona una telecamera", + "backToSettings": "Torna alle impostazioni della telecamera", + "streams": { + "title": "Stato e dettagli della telecamera", + "desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi di questa telecamera da parte di Frigate. Rilevamento, registrazione e correzioni non saranno disponibili.
    Nota: questa operazione non disattiva le ritrasmissioni di go2rtc.", + "enableLabel": "Telecamere abilitate", + "enableDesc": "Disabilita temporaneamente una telecamera abilitata fino al riavvio di Frigate. La disabilitazione completa di una telecamera interrompe l'elaborazione dei flussi video di tale telecamera da parte di Frigate. Le funzioni di rilevamento, registrazione e correzioni non saranno disponibili.
    Nota: questa operazione non disabilita le ritrasmissioni go2rtc.

    Trascina le schede per riordinare le telecamere nell'interfaccia utente. L'ordine delle telecamere abilitate verrà visualizzato in tutta l'interfaccia utente inclusa la schermata Dal vivo e i menu a tendina di selezione delle telecamere.", + "disableLabel": "Telecamere disabilitate", + "disableDesc": "Abilita una telecamera attualmente non visibile nell'interfaccia utente e disabilitata nella configurazione. Dopo l'abilitazione è necessario riavviare Frigate.", + "enableSuccess": "{{cameraName}} abilitata. Riavvia Frigate per applicare le modifiche.", + "friendlyName": { + "edit": "Modifica il nome visualizzato della telecamera", + "title": "Modifica il nome visualizzato", + "description": "Imposta il nome amichevole visualizzato per questa telecamera nell'interfaccia utente di Frigate. Lascia vuoto per utilizzare l'ID della telecamera.", + "rename": "Rinomina" + }, + "reorderHandle": "Trascina per riordinare", + "saving": "Salvataggio…", + "saved": "Salvato", + "details": { + "edit": "Modifica i dettagli della telecamera", + "title": "Modifica i dettagli della telecamera", + "description": "Aggiorna il nome visualizzato, l'URL esterno e la visibilità utilizzati per questa telecamera nell'interfaccia utente di Frigate.", + "friendlyNameLabel": "Nome da visualizzare", + "friendlyNameHelp": "Nome descrittivo visualizzato per questa telecamera nell'interfaccia utente di Frigate. Lasciare vuoto per utilizzare l'ID della telecamera.", + "webuiUrlLabel": "URL dell'interfaccia web della telecamera", + "webuiUrlHelp": "URL per accedere direttamente all'interfaccia web della telecamera dalla vista Correzioni. Lasciare vuoto per disabilitare il collegamento.", + "webuiUrlInvalid": "Deve essere un URL valido (ad esempio, https://esempio.com).", + "dashboardLabel": "Mostra nella schermata Dal vivo", + "dashboardHelp": "Mostra questa telecamera nella schermata Dal vivo.", + "reviewLabel": "Mostra in Revisiona", + "reviewHelp": "Mostra questa telecamera in Revisiona, incluso il filtro della telecamera, la revisione del movimento e la visualizzazione della cronologia." + }, + "label": "Stato della telecamera", + "description": "Imposta lo stato operativo per ciascuna telecamera.

    Accesa: i flussi vengono elaborati normalmente.
    Spenta: mette temporaneamente in pausa l'elaborazione. Non viene mantenuta dopo il riavvio di Frigate.
    Disabilitata: interrompe l'elaborazione e salva la modifica nella configurazione. È necessario riavviare Frigate per riattivare una telecamera disabilitata.

    Nota: la disabilitazione non influisce sulle ritrasmissioni go2rtc.

    Trascina la maniglia per riordinare le telecamere attive nell'interfaccia utente, inclusi il pannello di controllo Dal vivo e i menu a tendina di selezione della telecamera.", + "disabledSubheading": "Disabilitata nella configurazione", + "status": { + "on": "Accesa", + "off": "Spenta", + "disabled": "Disabilitata" + }, + "disableSuccess": "{{cameraName}} disabilitata e salvata nella configurazione." + }, + "cameraConfig": { + "add": "Aggiungi telecamera", + "edit": "Modifica telecamera", + "description": "Configura le impostazioni della telecamera, inclusi gli ingressi ed i ruoli dei flussi.", + "name": "Nome telecamera", + "nameRequired": "Il nome della telecamera è obbligatorio", + "nameLength": "Il nome della telecamera deve contenere al massimo 64 caratteri.", + "namePlaceholder": "ad esempio, porta_anteriore o Panoramica cortile", + "toast": { + "success": "La telecamera {{cameraName}} è stata salvata correttamente" + }, + "enabled": "Abilitata", + "ffmpeg": { + "inputs": "Flussi di ingresso", + "path": "Percorso del flusso", + "pathRequired": "Il percorso del flusso è obbligatorio", + "pathPlaceholder": "rtsp://...", + "roles": "Ruoli", + "rolesRequired": "È richiesto almeno un ruolo", + "rolesUnique": "Ogni ruolo (audio, rilevamento, registrazione) può essere assegnato solo ad un flusso", + "addInput": "Aggiungi flusso di ingresso", + "removeInput": "Rimuovi flusso di ingresso", + "inputsRequired": "È richiesto almeno un flusso di ingresso" + }, + "go2rtcStreams": "Flussi go2rtc", + "streamUrls": "URL dei flussi", + "addUrl": "Aggiungi URL", + "addGo2rtcStream": "Aggiungi flusso go2rtc" + }, + "profiles": { + "enabled": "Abilitato", + "title": "Sovrascritture della telecamera del profilo", + "selectLabel": "Seleziona il profilo", + "description": "Configura quali telecamere vengono accese o spente all'attivazione di un profilo. Le telecamere impostate su \"Eredita\" mantengono il loro stato predefinito.", + "inherit": "Eredita", + "disabled": "Disabilitato", + "on": "Attivato", + "off": "Disattivato" + }, + "description": "Aggiungi, modifica ed elimina le telecamere, controlla lo stato di ciascuna telecamera e configura le impostazioni personalizzate per profilo e tipo di telecamera. Per configurare flussi video, rilevamento, movimento e altre impostazioni specifiche per ciascuna telecamera, seleziona la sezione corrispondente in Configurazione telecamera.", + "deleteCamera": "Elimina telecamera", + "deleteCameraDialog": { + "title": "Elimina telecamera", + "description": "L'eliminazione di una telecamera rimuoverà in modo permanente tutte le registrazioni, gli oggetti tracciati e la configurazione relativi a tale telecamera. Potrebbe essere comunque necessario rimuovere manualmente gli eventuali flussi go2rtc associati a questa telecamera.", + "selectPlaceholder": "Scegli telecamera...", + "confirmTitle": "Sei sicuro?", + "confirmWarning": "L'eliminazione di {{cameraName}} è irreversibile.", + "deleteExports": "Elimina anche le esportazioni per questa telecamera", + "confirmButton": "Elimina definitivamente", + "success": "Telecamera {{cameraName}} eliminata con successo", + "error": "Impossibile eliminare la telecamera {{cameraName}}" + }, + "cameraType": { + "title": "Tipo di telecamera", + "label": "Tipo di telecamera", + "description": "Imposta il tipo per ogni telecamera. Le telecamere LPR dedicate sono telecamere monouso con un potente zoom ottico per acquisire le targhe dei veicoli distanti. La maggior parte delle telecamere dovrebbe utilizzare il tipo di telecamera normale, a meno che non siano specificamente progettate per il riconoscimento delle targhe e abbiano una visuale molto ravvicinata sulle targhe.", + "normal": "Normale", + "dedicatedLpr": "LPR dedicata", + "saveSuccess": "Tipo di telecamera aggiornato per {{cameraName}}. Riavviare Frigate per applicare le modifiche." + }, + "clone": { + "sectionTitle": "Clona impostazioni", + "button": "Clona impostazioni", + "sectionDescription": "Copia la configurazione da una telecamera ad un'altra telecamera o a una nuova.", + "title": "Clona impostazioni della telecamera", + "description": "Copia la configurazione di una telecamera su una o altre telecamere o su una nuova telecamera. L'identità (nome, nome descrittivo, URL dell'interfaccia web, ordine di visualizzazione) non viene mai copiata.", + "source": { + "label": "Telecamera sorgente", + "placeholder": "Seleziona una telecamera sorgente", + "required": "Seleziona una telecamera sorgente" + }, + "target": { + "legend": "Destinazione", + "newRadio": "Nuova telecamera", + "newNameLabel": "Nome telecamera", + "newNamePlaceholder": "p.es., porta_posteriore o Porta Posteriore", + "newNameRequired": "Il nome della telecamera è obbligatorio", + "newNameInvalid": "Nome della telecamera non valido", + "newNameCollision": "Esiste già una telecamera con questo nome", + "newStreamsForced": "I flussi video vengono sempre copiati per ogni nuova telecamera.", + "existingCamerasRadio": "Telecamere esistenti", + "allCameras": "Tutte le telecamere", + "existingPlaceholder": "Seleziona almeno una telecamera", + "existingDisabled": "Nessun'altra telecamera da copiare" + }, + "categories": { + "legend": "Impostazioni da clonare", + "description": "Scegli quali impostazioni copiare dalla telecamera sorgente.", + "selectAll": "Seleziona tutto", + "selectNone": "Nessuna selezione", + "resetDefaults": "Ripristina le impostazioni predefinite", + "general": "Generale", + "spatial": "Impostazioni spaziali", + "streams": "Flussi", + "spatialWarningTitle": "Risoluzione non coincidente", + "spatialWarning": "La risoluzione di rilevamento della telecamera sorgente {{srcCamera}} ({{srcWidth}}×{{srcHeight}}) è diversa da quella di {{cameras}}. I poligoni potrebbero non allinearsi su queste telecamere. Queste impostazioni predefinite sono disattivate; abilitale per copiare così come sono.", + "restartHint": "Riavvio richiesto", + "items": { + "record": "Registrazione", + "snapshots": "Istantanee", + "review": "Revisiona", + "motion": "Rilevamento movimento", + "objects": "Oggetti", + "audio": "Rilevamento audio", + "audio_transcription": "Trascrizione audio", + "notifications": "Notifiche", + "birdseye": "Birdseye", + "mqtt": "MQTT", + "timestamp_style": "Stile orario", + "onvif": "ONVIF", + "lpr": "Riconoscimento targhe", + "face_recognition": "Riconoscimento facciale", + "semantic_search": "Ricerca semantica", + "genai": "IA Generativa", + "type": "Tipo di telecamera (normale / dedicata al riconoscimento targhe)", + "profiles": "Profili", + "detect": "Rileva le dimensioni", + "zones": "Zone", + "motion_mask": "Maschere di movimento", + "object_masks": "Maschere di oggetti", + "ffmpeg_live": "URL e ruoli del flusso" + } + }, + "footer": { + "changeCount_one": "{{count}} modifica verrà applicata", + "changeCount_many": "{{count}} modifiche verranno applicate", + "changeCount_other": "{{count}} modifiche verranno applicate", + "restartNeeded": "Per alcune modifiche sarà necessario riavviare il sistema.", + "liveOnly": "Tutte le modifiche verranno applicate immediatamente, senza bisogno di riavviare il sistema.", + "submit": "Clona", + "submitting": "Clonazione…" + }, + "toast": { + "success": "Impostazioni copiate in {{cameraName}}", + "successWithRestart": "Impostazioni copiate in {{cameraName}}. Riavvia Frigate per applicare tutte le modifiche.", + "successMulti_one": "Impostazioni copiate su {{count}} telecamera", + "successMulti_many": "Impostazioni copiate su {{count}} telecamere", + "successMulti_other": "Impostazioni copiate su {{count}} telecamere", + "successMultiWithRestart_one": "Impostazioni copiate su {{count}} telecamera. Riavvia Frigate per applicare tutte le modifiche.", + "successMultiWithRestart_many": "Impostazioni copiate su {{count}} telecamere. Riavvia Frigate per applicare tutte le modifiche.", + "successMultiWithRestart_other": "Impostazioni copiate su {{count}} telecamere. Riavvia Frigate per applicare tutte le modifiche.", + "partialFailure": "{{successCount}} sezioni applicate; '{{failedSection}}' non riuscita: {{errorMessage}}", + "partialFailureMulti": "Copiato su {{successCount}} telecamera(e); errore per {{failed}}: {{errorMessage}}", + "newCameraPartialFailure": "La telecamera {{cameraName}} è stata creata, ma alcune impostazioni non sono state copiate: {{errorMessage}}", + "sourceMissing": "La telecamera sorgente non esiste più", + "submitError": "Impossibile clonare la telecamera: {{errorMessage}}" + } + } + }, + "button": { + "overriddenGlobal": "Sovrascritto (Globale)", + "overriddenGlobalTooltip": "Questa telecamera sovrascrive le impostazioni di configurazione globali in questa sezione", + "overriddenBaseConfig": "Sovrascritto (Configurazione di base)", + "overriddenBaseConfigTooltip": "Il profilo {{profile}} sovrascrive le impostazioni di configurazione in questa sezione", + "overriddenInCameras": { + "label_one": "Sovrascritto in {{count}} telecamera", + "label_many": "Sovrascritto in {{count}} telecamere", + "label_other": "Sovrascritto in {{count}} telecamere", + "tooltip_one": "{{count}} telecamera sovrascrive i valori in questa sezione. Fai clic per visualizzare i dettagli.", + "tooltip_many": "{{count}} telecamere sovrascrivono i valori in questa sezione. Fai clic per visualizzare i dettagli.", + "tooltip_other": "{{count}} telecamere sovrascrivono i valori in questa sezione. Fai clic per visualizzare i dettagli.", + "heading_one": "Questa sezione globale contiene campi che vengono sovrascritti in {{count}} telecamera.", + "heading_many": "Questa sezione globale contiene campi che vengono sovrascritti in {{count}} telecamere.", + "heading_other": "Questa sezione globale contiene campi che vengono sovrascritti in {{count}} telecamere.", + "othersField_one": "{{count}} altro", + "othersField_many": "{{count}} altri", + "othersField_other": "{{count}} altri", + "profilePrefix": "Profilo {{profile}}: {{fields}}" + }, + "overriddenGlobalHeading_one": "Questa telecamera sovrascrive il campo {{count}} dalla configurazione globale:", + "overriddenGlobalHeading_many": "Questa telecamera sovrascrive i campi {{count}} della configurazione globale:", + "overriddenGlobalHeading_other": "Questa telecamera sovrascrive i campi {{count}} della configurazione globale:", + "overriddenGlobalNoDeltas": "Questa telecamera sovrascrive la configurazione globale, ma nessun valore dei campi risulta diverso.", + "overriddenBaseConfigHeading_one": "Il profilo {{profile}} sovrascrive il campo {{count}} della configurazione di base:", + "overriddenBaseConfigHeading_many": "Il profilo {{profile}} sovrascrive i campi {{count}} della configurazione di base:", + "overriddenBaseConfigHeading_other": "Il profilo {{profile}} sovrascrive i campi {{count}} della configurazione di base:", + "overriddenBaseConfigNoDeltas": "Il profilo {{profile}} sovrascrive questa sezione, ma nessun valore di campo differisce dalla configurazione di base." + }, + "go2rtcStreams": { + "title": "Flussi go2rtc", + "ffmpeg": { + "video": "Video", + "audio": "Audio", + "audioCopy": "Copia", + "videoCopy": "Copia", + "hardware": "Accelerazione hardware", + "hardwareNone": "Nessuna accelerazione hardware", + "hardwareAuto": "Automatico (consigliato)", + "useFfmpegModule": "Utilizza la modalità di compatibilità (ffmpeg)", + "videoH264": "Transcodifica in H.264", + "videoH265": "Transcodifica in H.265", + "videoExclude": "Escludi", + "audioAac": "Transcodifica in AAC", + "audioOpus": "Transcodifica in Opus", + "audioPcmu": "Transcodifica in PCM μ-law", + "audioPcma": "Transcodifica in PCM A-law", + "audioPcm": "Transcodifica in PCM", + "audioMp3": "Transcodifica in MP3", + "audioExclude": "Escludi", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "Aggiungi codec video", + "addAudioCodec": "Aggiungi codec audio", + "removeCodec": "Rimuovi codec" + }, + "description": "Gestisci le configurazioni del flusso go2rtc per la ritrasmissione delle immagini della telecamera. Ogni flusso ha un nome e uno o più URL sorgente.", + "addStream": "Aggiungi flusso", + "addStreamDesc": "Inserisci un nome per il nuovo flusso. Questo nome verrà utilizzato per identificare il flusso nella configurazione della telecamera.", + "addUrl": "Aggiungi URL", + "streamName": "Nome flusso", + "streamNamePlaceholder": "p. es., porta_ingresso", + "streamUrlPlaceholder": "p. es., rtsp://utente:password@192.168.1.100/flusso", + "deleteStream": "Elimina flusso", + "deleteStreamConfirm": "Sei sicuro di voler eliminare il flusso \"{{streamName}}\"? Le telecamere che fanno riferimento a questo flusso potrebbero smettere di funzionare.", + "noStreams": "Nessun flusso go2rtc configurato. Aggiungi un flusso per iniziare.", + "validation": { + "nameRequired": "Il nome del flusso è obbligatorio", + "nameDuplicate": "Esiste già un flusso con questo nome", + "nameInvalid": "Il nome del flusso può contenere solo lettere, numeri, trattini bassi e trattini", + "urlRequired": "È richiesto almeno un URL" + }, + "renameStream": "Rinomina flusso", + "renameStreamDesc": "Inserisci un nuovo nome per questo flusso. Rinominare un flusso potrebbe causare problemi alle telecamere o ad altri flussi che lo referenziano tramite il suo nome.", + "newStreamName": "Nuovo nome del flusso", + "streamNumber": "Flusso {{index}}", + "sourceNumber": "Sorgente {{index}}" + }, + "configForm": { + "sections": { + "review": "Revisiona", + "record": "Registrazione", + "snapshots": "Istantanee", + "ffmpeg": "FFmpeg", + "objects": "Oggetti", + "database": "Database", + "mqtt": "MQTT", + "notifications": "Notifiche", + "tls": "TLS", + "auth": "Autenticazione", + "proxy": "Proxy", + "telemetry": "Telemetria", + "birdseye": "Birdseye", + "semantic_search": "Ricerca semantica", + "lpr": "Riconoscimento targhe", + "face_recognition": "Riconoscimento facciale", + "masksAndZones": "Maschere / Zone", + "audio": "Audio", + "model": "Modello", + "detect": "Rilevamento", + "motion": "Movimento", + "live": "Vista dal vivo", + "timestamp_style": "Orari", + "go2rtc": "go2rtc", + "detectors": "Rivelatori", + "genai": "GenAI" + }, + "tabs": { + "system": "Sistema", + "integrations": "Integrazioni", + "sharedDefaults": "Impostazioni predefinite condivise" + }, + "inputRoles": { + "options": { + "audio": "Audio", + "detect": "Rileva", + "record": "Registra" + }, + "summary": "{{count}} ruoli selezionati", + "empty": "Nessun ruolo disponibile" + }, + "roleMap": { + "roleLabel": "Ruolo", + "remove": "Rimuovi", + "empty": "Nessuna mappatura dei ruoli", + "groupsLabel": "Gruppi", + "addMapping": "Aggiungi la mappatura dei ruoli" + }, + "notifications": { + "title": "Impostazioni di notifica" + }, + "global": { + "title": "Impostazioni globali", + "description": "Queste impostazioni si applicano a tutte le telecamere a meno che non vengano modificate nelle impostazioni specifiche della singola telecamera." + }, + "camera": { + "noCameras": "Nessuna telecamera disponibile", + "title": "Impostazioni telecamera", + "description": "Queste impostazioni si applicano solo a questa telecamera e sovrascrivono le impostazioni globali." + }, + "advancedSettingsCount": "Impostazioni avanzate ({{count}})", + "advancedCount": "Avanzato ({{count}})", + "showAdvanced": "Mostra impostazioni avanzate", + "additionalProperties": { + "keyLabel": "Chiave", + "valueLabel": "Valore", + "keyPlaceholder": "Nuova chiave", + "remove": "Rimuovi" + }, + "ffmpegArgs": { + "preset": "Preimpostazione", + "manual": "Argomenti manuali", + "inherit": "Eredita dalle impostazioni della telecamera", + "none": "Nessuna", + "useGlobalSetting": "Eredita dalle impostazioni globali", + "selectPreset": "Seleziona preimpostazione", + "manualPlaceholder": "Inserisci argomenti FFmpeg", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (GPU Intel/AMD)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "GPU NVIDIA", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (Generico)", + "preset-http-mjpeg-generic": "HTTP JPEG (Generico)", + "preset-http-reolink": "HTTP - Telecamere Reolink", + "preset-rtmp-generic": "RTMP (Generico)", + "preset-rtsp-generic": "RTSP (Generico)", + "preset-rtsp-restream": "RTSP - Ritrasmissione da go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - Ritrasmissione da go2rtc (bassa latenza)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Registrazione (Generica, senza audio)", + "preset-record-generic-audio-copy": "Registrazione (generica + copia audio)", + "preset-record-generic-audio-aac": "Registrazione (generica + audio in AAC)", + "preset-record-mjpeg": "Registrazione - Telecamere MJPEG", + "preset-record-jpeg": "Registrazione - Telecamere JPEG", + "preset-record-ubiquiti": "Registrazione - Telecamere Ubiquiti" + } + }, + "cameraInputs": { + "itemTitle": "Flusso {{index}}" + }, + "restartRequiredField": "Riavvio richiesto", + "restartRequiredFooter": "Configurazione modificata - Riavvio necessario", + "detect": { + "title": "Impostazioni di rilevamento" + }, + "detectors": { + "title": "Impostazioni del rilevatore", + "singleType": "È consentito un solo rilevatore di tipo {{type}}.", + "keyRequired": "Il nome del rilevatore è obbligatorio.", + "keyDuplicate": "Il nome del rilevatore esiste già.", + "noSchema": "Non sono disponibili schemi di rilevamento.", + "none": "Nessuna istanza del rilevatore configurata.", + "add": "Aggiungi rilevatore", + "addCustomKey": "Aggiungi chiave personalizzata" + }, + "record": { + "title": "Impostazioni di registrazione" + }, + "snapshots": { + "title": "Impostazioni istantanea" + }, + "motion": { + "title": "Impostazioni di movimento" + }, + "objects": { + "title": "Impostazioni oggetto" + }, + "audioLabels": { + "summary": "{{count}} etichette audio selezionate", + "empty": "Nessuna etichetta audio disponibile" + }, + "objectLabels": { + "summary": "{{count}} tipi di oggetto selezionati", + "empty": "Non sono disponibili etichette per gli oggetti" + }, + "reviewLabels": { + "summary": "{{count}} etichette selezionate", + "empty": "Nessuna etichetta disponibile" + }, + "filters": { + "objectFieldLabel": "{{field}} per {{label}}" + }, + "zoneNames": { + "summary": "{{count}} selezionati", + "empty": "Nessuna zona disponibile" + }, + "genaiRoles": { + "options": { + "embeddings": "Incorporamento", + "descriptions": "Descrizioni", + "chat": "Chat" + } + }, + "semanticSearchModel": { + "placeholder": "Seleziona il modello…", + "builtIn": "Modelli integrati", + "genaiProviders": "Fornitori di GenAI" + }, + "genaiModel": { + "placeholder": "Seleziona o inserisci un modello…", + "search": "Cerca o inserisci un modello…", + "noModels": "Nessun modello disponibile", + "available": "Modelli disponibili", + "useCustom": "Utilizza \"{{value}}\"", + "refresh": "Aggiorna modelli", + "probeFailed": "Impossibile rilevare i modelli", + "fetchedModels": "Elenco dei modelli recuperato con successo" + }, + "review": { + "title": "Impostazioni di revisione" + }, + "audio": { + "title": "Impostazioni audio" + }, + "live": { + "title": "Impostazioni della visualizzazione dal vivo" + }, + "timestamp_style": { + "title": "Impostazioni orario" + }, + "searchPlaceholder": "Ricerca...", + "addCustomLabel": "Aggiungi etichetta personalizzata...", + "knownPlates": { + "namePlaceholder": "p. es., auto della moglie", + "platePlaceholder": "Numero di targa o espressione regolare" + }, + "timezone": { + "defaultOption": "Utilizza il fuso orario del browser" + }, + "semanticSearchModelSize": { + "notApplicable": "Non applicabile ai fornitori GenAI" + }, + "liveStreams": { + "streamNameLabel": "Nome flusso", + "streamNamePlaceholder": "p.es., flusso HD principale", + "go2rtcStreamLabel": "flusso go2rtc", + "go2rtcStreamPlaceholder": "Seleziona un flusso go2rtc", + "go2rtcStreamSearch": "Cerca o inserisci il nome di un flusso…", + "noGo2rtcStreams": "Nessun flusso go2rtc configurato", + "availableStreams": "Flussi disponibili", + "useCustom": "Utilizza \"{{value}}\"", + "addStream": "Aggiungi flusso" + }, + "ptzPresets": { + "placeholder": "Seleziona o inserisci un valore preimpostato...", + "search": "Cerca o inserisci un valore preimpostato...", + "noPresets": "Nessun valore preimpostato disponibile", + "available": "Preimpostazioni della telecamera", + "useCustom": "Utilizza \"{{value}}\"" + }, + "defaultRole": { + "admin": "Amministratore", + "viewer": "Visualizzatore" + } + }, + "globalConfig": { + "title": "Configurazione globale", + "description": "Configura le impostazioni globali che si applicano a tutte le telecamere, a meno che non vengano sovrascritte.", + "toast": { + "success": "Impostazioni globali salvate correttamente", + "error": "Impossibile salvare le impostazioni globali", + "validationError": "Validazione fallita" + } + }, + "cameraConfig": { + "title": "Configurazione telecamera", + "description": "Configura le impostazioni per le singole telecamere. Le impostazioni personalizzate sovrascrivono le impostazioni predefinite globali.", + "overriddenBadge": "Sovrascritto", + "resetToGlobal": "Ripristina impostazioni globali", + "toast": { + "success": "Impostazioni della telecamera salvate correttamente", + "error": "Impossibile salvare le impostazioni della telecamera" + } + }, + "profiles": { + "title": "Profili", + "columnCamera": "Telecamera", + "activeProfile": "Profilo attivo", + "noActiveProfile": "Nessun profilo attivo", + "active": "Attivo", + "activated": "Profilo '{{profile}}' attivato", + "activateFailed": "Impossibile impostare il profilo", + "deactivated": "Profilo disattivato", + "noProfiles": "Nessun profilo definito.", + "noOverrides": "Nessuna sovrascrittura", + "cameraCount_one": "{{count}} telecamera", + "cameraCount_many": "{{count}} telecamere", + "cameraCount_other": "{{count}} telecamere", + "columnOverrides": "Sovrascritture del profilo", + "baseConfig": "Configurazione di base", + "addProfile": "Aggiungi profilo", + "newProfile": "Nuovo profilo", + "profileNamePlaceholder": "p. es., Inserita, Assente, Modalità notturna", + "friendlyNameLabel": "Nome profilo", + "profileIdLabel": "ID profilo", + "profileIdDescription": "Identificativo interno utilizzato nella configurazione e nelle automazioni", + "nameInvalid": "Sono consentite solo lettere minuscole, numeri e trattini bassi", + "nameDuplicate": "Esiste già un profilo con questo nome", + "error": { + "mustBeAtLeastTwoCharacters": "Deve contenere almeno 2 caratteri", + "mustNotContainPeriod": "Non deve contenere punti", + "alreadyExists": "Esiste già un profilo con questo ID" + }, + "renameProfile": "Rinomina profilo", + "renameSuccess": "Profilo rinominato in '{{profile}}'", + "deleteProfile": "Elimina profilo", + "deleteProfileConfirm": "Eliminare il profilo \"{{profile}}\" da tutte le telecamere? Questa operazione non può essere annullata.", + "deleteSuccess": "Profilo '{{profile}}' eliminato", + "createSuccess": "Profilo '{{profile}}' creato", + "removeOverride": "Rimuovi la sovrascrittura del profilo", + "deleteSection": "Elimina le sostituzioni della sezione", + "deleteSectionConfirm": "Rimuovere le sovrascritture {{section}} per il profilo {{profile}} su {{camera}}?", + "deleteSectionSuccess": "Rimosse le sovrascritture di {{section}} per {{profile}}", + "enableSwitch": "Abilita profili", + "enabledDescription": "I profili sono abilitati. Crea un nuovo profilo qui sotto, vai alla sezione di configurazione della telecamera per apportare le modifiche e salva affinché le modifiche abbiano effetto.", + "disabledDescription": "I profili consentono di definire insiemi denominati di impostazioni di configurazione della telecamera (p.es., inserita, assente, notturna) che possono essere attivate su richiesta." + }, + "timestampPosition": { + "tl": "In alto a sinistra", + "tr": "In alto a destra", + "bl": "In basso a sinistra", + "br": "In basso a destra" + }, + "detectionModel": { + "plusActive": { + "title": "Gestione del modello Frigate+", + "label": "Fonte del modello attuale", + "description": "Questa istanza utilizza un modello Frigate+. Seleziona o modifica il tuo modello nelle impostazioni di Frigate+.", + "goToFrigatePlus": "Vai alle impostazioni di Frigate+", + "showModelForm": "Configura manualmente un modello" + } + }, + "maintenance": { + "title": "Manutenzione", + "sync": { + "title": "Sincronizzazione multimediale", + "desc": "Frigate pulirà periodicamente i supporti di memorizzazione secondo una pianificazione regolare, in base alla configurazione di conservazione. È normale che Frigate visualizzi alcuni file orfani durante il suo funzionamento. Utilizza questa funzione per rimuovere dal disco i file multimediali orfani che non sono più referenziati nel database.", + "started": "Sincronizzazione multimediale avviata.", + "alreadyRunning": "È già in corso un'operazione di sincronizzazione", + "error": "Impossibile avviare la sincronizzazione", + "currentStatus": "Stato", + "statusLabel": "Stato", + "jobId": "ID lavoro", + "startTime": "Ora di inizio", + "endTime": "Ora di fine", + "results": "Risultati", + "errorLabel": "Errore", + "resultsFields": { + "error": "Errore", + "totals": "Totali", + "filesChecked": "File controllati", + "orphansFound": "Orfani trovati", + "orphansDeleted": "Orfani eliminati", + "aborted": "Interrotto. La cancellazione supererebbe la soglia di sicurezza." + }, + "event_snapshots": "Istantanee degli oggetti tracciati", + "event_thumbnails": "Miniature degli oggetti tracciati", + "review_thumbnails": "Anteprima delle miniature", + "previews": "Anteprime", + "exports": "Esportazioni", + "recordings": "Registrazioni", + "mediaTypes": "Tipi di supporto", + "allMedia": "Tutti i supporti", + "dryRun": "Prova a secco", + "dryRunEnabled": "Nessun file verrà eliminato", + "dryRunDisabled": "I file verranno eliminati", + "force": "Forza", + "forceDesc": "Ignora la soglia di sicurezza e completa la sincronizzazione anche se più del 50% dei file verrebbe eliminato.", + "verbose": "Dettagliato", + "verboseDesc": "Scrivi un elenco completo dei file orfani su disco per la revisione.", + "running": "Sincronizzazione in corso...", + "start": "Avvia sincronizzazione", + "inProgress": "Sincronizzazione in corso. Questa pagina è disabilitata.", + "status": { + "queued": "In coda", + "running": "In corso", + "completed": "Completata", + "failed": "Fallita", + "notRunning": "Non in esecuzione" + } + }, + "regionGrid": { + "title": "Griglia di regioni", + "desc": "La griglia di regioni è un algoritmo di ottimizzazione che apprende dove gli oggetti di diverse dimensioni appaiono tipicamente nel campo visivo di ciascuna telecamera. Frigate utilizza questi dati per dimensionare in modo efficiente le regioni di rilevamento. La griglia viene creata automaticamente nel tempo a partire dai dati degli oggetti tracciati.", + "clear": "Pulisci griglia di regioni", + "clearConfirmTitle": "Pulisci griglia di regioni", + "clearConfirmDesc": "La pulizia della griglia delle regioni non è consigliata, a meno che non sia stata modificata di recente la dimensione del modello del rilevatore, oppure sia stata cambiata la posizione fisica della telecamera e si riscontrino problemi di tracciamento degli oggetti. La griglia verrà ricostruita automaticamente nel tempo man mano che gli oggetti vengono tracciati. È richiesto un riavvio di Frigate affinché le modifiche abbiano effetto.", + "clearSuccess": "Griglia delle regioni pulita con successo", + "clearError": "Impossibile pulire la griglia delle regioni", + "restartRequired": "È necessario riavviare il sistema affinché le modifiche alla griglia delle regioni abbiano effetto" + } + }, + "retainMode": { + "motion": "Movimento", + "all": "Tutti", + "active_objects": "Oggetti attivi" + }, + "birdseye": { + "trackingMode": { + "motion": "Movimento", + "objects": "Oggetti", + "continuous": "Continuo" + }, + "cameraOrder": { + "reorderHandle": "Trascina per riordinare", + "saving": "Salvataggio…", + "saved": "Salvato", + "label": "Ordine delle telecamere", + "description": "Trascina le telecamere per impostarne l'ordine nella visualizzazione Birdseye." + } + }, + "toast": { + "success": "Impostazioni salvate correttamente", + "applied": "Impostazioni applicate correttamente", + "successRestartRequired": "Impostazioni salvate correttamente. Riavvia Frigate per applicare le modifiche.", + "error": "Impossibile salvare le impostazioni", + "validationError": "Validazione non riuscita: {{message}}", + "resetSuccess": "Ripristina le impostazioni predefinite globali", + "resetError": "Impossibile ripristinare le impostazioni", + "saveAllSuccess_one": "Salvata {{count}} sezione correttamente.", + "saveAllSuccess_many": "Tutte le {{count}} sezioni sono state salvate correttamente.", + "saveAllSuccess_other": "Tutte le {{count}} sezioni sono state salvate correttamente.", + "saveAllPartial_one": "{{successCount}} sezione su {{totalCount}} salvata. {{failCount}} errore.", + "saveAllPartial_many": "{{successCount}} sezioni su {{totalCount}} salvate. {{failCount}} errori.", + "saveAllPartial_other": "{{successCount}} sezioni su {{totalCount}} salvate. {{failCount}} errori.", + "saveAllFailure": "Impossibile salvare tutte le sezioni.", + "saveAllSuccessRestartRequired_one": "Salvata {{count}} sezione correttamente. Riavvia Frigate per applicare le modifiche.", + "saveAllSuccessRestartRequired_many": "Salvate {{count}} sezioni correttamente. Riavvia Frigate per applicare le modifiche.", + "saveAllSuccessRestartRequired_other": "Salvate {{count}} sezioni correttamente. Riavvia Frigate per applicare le modifiche." + }, + "unsavedChanges": "Hai delle modifiche non salvate", + "confirmReset": "Conferma il ripristino", + "resetToDefaultDescription": "Questa operazione ripristinerà tutte le impostazioni di questa sezione ai valori predefiniti. Tale azione è irreversibile.", + "resetToGlobalDescription": "Questa operazione ripristinerà le impostazioni di questa sezione ai valori predefiniti globali. Tale azione è irreversibile.", + "previewQuality": { + "very_high": "Molto alta", + "high": "Alta", + "medium": "Media", + "low": "Bassa", + "very_low": "Molto bassa" + }, + "ui": { + "TimeOrDateStyle": { + "medium": "Medio", + "full": "Completo", + "long": "Lungo", + "short": "Corto" + }, + "timeFormat": { + "browser": "Navigatore", + "12hour": "12 ore", + "24hour": "24 ore" + }, + "unitSystem": { + "metric": "Metrico", + "imperial": "Imperiale" + } + }, + "review": { + "imageSource": { + "recordings": "Registrazioni", + "previews": "Anteprime" + } + }, + "logger": { + "logLevel": { + "debug": "Correzioni", + "info": "Informazioni", + "warning": "Avviso", + "error": "Errore", + "critical": "Critico" + } + }, + "onvif": { + "profileAuto": "Automatico", + "profileLoading": "Caricamento profili...", + "autotracking": { + "zooming": { + "disabled": "Disabilitato", + "absolute": "Assoluto", + "relative": "Relativo" + } + } + }, + "modelSize": { + "small": "Piccolo", + "large": "Grande" + }, + "configMessages": { + "review": { + "recordDisabled": "La registrazione è disabilitata, pertanto non verranno generati elementi di revisione.", + "detectDisabled": "Il rilevamento degli oggetti è disabilitato. Gli elementi di revisione richiedono la presenza di oggetti rilevati per poter classificare avvisi e rilevamenti.", + "allNonAlertDetections": "Tutte le attività non di avviso saranno incluse tra i rilevamenti.", + "genaiImageSourceRecordingsRecordDisabled": "La sorgente dell'immagine è impostata su 'registrazioni', ma la registrazione è disabilitata. Frigate utilizzerà le immagini di anteprima." + }, + "audio": { + "noAudioRole": "Nessun flusso ha il ruolo audio definito. È necessario abilitare il ruolo audio affinché il rilevamento audio funzioni." + }, + "audioTranscription": { + "audioDetectionDisabled": "Il rilevamento audio non è abilitato per questa telecamera. La trascrizione audio richiede che il rilevamento audio sia attivo." + }, + "detect": { + "fpsGreaterThanFive": "Impostare il valore di FPS rilevato su un valore superiore a 5 non è consigliabile. Valori più elevati potrebbero causare problemi di prestazioni e non apporteranno alcun vantaggio.", + "disabled": "Il rilevamento degli oggetti è disabilitato. Le istantanee, gli elementi di revisione e le funzionalità aggiuntive come il riconoscimento facciale, il riconoscimento delle targhe e l'intelligenza artificiale generativa non funzioneranno.", + "resolutionShouldBeMultipleOfFour": "Per ottenere risultati ottimali, la larghezza e l'altezza di rilevamento dovrebbero essere multipli di 4. Altri valori pari potrebbero produrre artefatti visivi o una leggera distorsione nel flusso di rilevamento.", + "aspectRatioMismatch": "La larghezza e l'altezza inserite non corrispondono al rapporto d'aspetto della risoluzione di rilevamento corrente. Ciò potrebbe produrre un'immagine allungata o distorta.", + "maxFramesSet": "L'impostazione del numero massimo di fotogrammi sovrascrive il comportamento predefinito e disabilita il tracciamento degli oggetti statici. Sono rare le situazioni in cui ciò è necessario, quindi si consiglia cautela nell'utilizzarlo.", + "squareResolution": "Una risoluzione di rilevamento quadrata è insolita. La larghezza e l'altezza di rilevamento devono corrispondere al rapporto d'aspetto della telecamera (ad esempio, 16:9), non alle dimensioni del modello di rilevamento degli oggetti. Un rapporto d'aspetto non corrispondente può distorcere l'immagine e ridurre la precisione del rilevamento.", + "resolutionHigh": "Questa risoluzione di rilevamento è superiore a quella consigliata e potrebbe causare un maggiore utilizzo delle risorse senza migliorare la precisione del rilevamento. Per la maggior parte delle telecamere si consiglia una risoluzione di rilevamento pari o inferiore a 1080p.", + "globalResolutionMultipleCameras": "Quando si configurano più telecamere, viene impostata una risoluzione di rilevamento globale. A meno che tutte le telecamere non condividano la stessa risoluzione e lo stesso rapporto d'aspetto, la larghezza e l'altezza di rilevamento devono essere definite per ciascuna telecamera in modo da corrispondere al rapporto d'aspetto nativo di ciascuna." + }, + "objects": { + "genaiNoDescriptionsProvider": "Per generare le descrizioni è necessario configurare un provider GenAI con il ruolo 'descrizioni'." + }, + "faceRecognition": { + "globalDisabled": "Perché le funzionalità di riconoscimento facciale funzionino correttamente su questa telecamera, è necessario abilitare l'arricchimento del riconoscimento facciale.", + "personNotTracked": "Il riconoscimento facciale richiede che l'oggetto 'persona' venga tracciato. Abilita 'persona' nella sezione ogggetti di questa telecamera.", + "modelSizeLarge": "Il modello 'grande' richiede una GPU o una NPU per prestazioni accettabili. Utilizzare il modello 'piccolo' su sistemi dotati solo di CPU." + }, + "lpr": { + "globalDisabled": "Per il corretto funzionamento delle funzioni LPR (riconoscimento targhe) su questa telecamera, è necessario abilitare la funzione di arricchimento del riconoscimento delle targhe.", + "vehicleNotTracked": "Il riconoscimento della targa richiede che venga tracciato 'automobile' o 'moto'. Abilita 'automobile' o 'moto' nella sezione oggetti per questa telecamera.", + "modelSizeLarge": "Il modello 'grande' è ottimizzato per le targhe multilinea. Il modello 'piccolo' offre prestazioni migliori rispetto al modello 'grande' e dovrebbe essere utilizzato a meno che nella vostra regione non siano in vigore formati di targa multilinea." + }, + "record": { + "noRecordRole": "Nessun flusso ha il ruolo di registrazione definito. La registrazione non funzionerà." + }, + "birdseye": { + "objectsModeDetectDisabled": "Birdseye è impostato sulla modalità 'oggetti', ma il rilevamento degli oggetti è disabilitato per questa telecamera. La telecamera non verrà visualizzata in Birdseye." + }, + "snapshots": { + "detectDisabled": "Il rilevamento degli oggetti è disabilitato. Le istantanee vengono generate dagli oggetti tracciati e non verranno create." + }, + "detectors": { + "mixedTypes": "Tutti i rilevatori devono essere dello stesso tipo. Rimuovi i rilevatori esistenti per poter utilizzare un tipo diverso.", + "mixedTypesSuggestion": "Tutti i rilevatori devono essere dello stesso tipo. Rimuovi i rilevatori esistenti oppure seleziona {{type}}." + }, + "semanticSearch": { + "jinav2SmallModelSize": "Il modello 'piccolo' Jina V2 presenta elevati consumi di RAM e di inferenza. Si consiglia il modello 'grande' con GPU dedicata." + }, + "onvif": { + "autotrackingNoZones": "Il tracciamento automatico richiede almeno una zona. Definisci una zona per questa telecamera in Maschere/Zone, quindi impostala come zona obbligatoria qui sotto." + } + }, + "saveAllPreview": { + "title": "Modifiche da salvare", + "triggerLabel": "Revisione delle modifiche in sospeso", + "empty": "Nessuna modifica in sospeso.", + "scope": { + "label": "Ambito", + "global": "Globale", + "camera": "Telecamera: {{cameraName}}" + }, + "profile": { + "label": "Profilo" + }, + "field": { + "label": "Campo" + }, + "value": { + "label": "Nuovo valore", + "reset": "Reimposta" + } + }, + "menuDot": { + "overrideGlobal": "Questa sezione sovrascrive la configurazione globale", + "overrideProfile": "Questa sezione viene sovrascritta dal profilo {{profile}}", + "unsaved": "Questa sezione contiene modifiche non salvate" + }, + "detectorsAndModel": { + "title": "Rilevatori e modelli", + "description": "Configura il backend del rilevatore che esegue il rilevamento degli oggetti e il modello che utilizza. Le modifiche vengono salvate insieme in modo che il rilevatore e il modello rimangano sincronizzati.", + "cardTitles": { + "detector": "Dispositivo di rilevamento", + "model": "Modello di rilevamento" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Modello personalizzato" + }, + "mismatch": { + "warning": "Il modello Frigate+ attuale \"{{model}}\" richiede il rilevatore {{required}}. Seleziona un modello compatibile qui sotto oppure passa a Modello personalizzato prima di salvare." + }, + "plusModel": { + "requiresDetector": "Richiede: {{detector}}", + "noModelSelected": "Seleziona un modello Frigate+" + }, + "toast": { + "saveSuccess": "Rilevatori e impostazioni del modello salvati. Riavviare Frigate per applicare le modifiche.", + "saveError": "Impossibile salvare le impostazioni del rilevatore e del modello" + }, + "unsavedChanges": "Modifiche al rilevatore e al modello non salvate", + "restartRequired": "Riavvio richiesto (rilevatore o modello modificato)" + } +} diff --git a/web/public/locales/it/views/system.json b/web/public/locales/it/views/system.json new file mode 100644 index 0000000..ed780a5 --- /dev/null +++ b/web/public/locales/it/views/system.json @@ -0,0 +1,260 @@ +{ + "documentTitle": { + "cameras": "Statistiche telecamere - Frigate", + "enrichments": "Statistiche di miglioramento - Frigate", + "storage": "Statistiche archiviazione - Frigate", + "general": "Statistiche generali - Frigate", + "logs": { + "frigate": "Registri Frigate - Frigate", + "go2rtc": "Registri Go2RTC - Frigate", + "nginx": "Registri Nginx - Frigate", + "websocket": "Registri dei messaggi - Frigate" + } + }, + "logs": { + "type": { + "timestamp": "Orario", + "label": "Tipo", + "tag": "Etichetta", + "message": "Messaggio" + }, + "tips": "I registri vengono trasmessi dal server", + "toast": { + "error": { + "whileStreamingLogs": "Errore durante la trasmissione dei registri: {{errorMessage}}", + "fetchingLogsFailed": "Errore durante il recupero dei registri: {{errorMessage}}" + } + }, + "download": { + "label": "Scarica registri" + }, + "copy": { + "label": "Copia negli appunti", + "success": "Registri copiati negli appunti", + "error": "Impossibile copiare i registri negli appunti" + }, + "websocket": { + "label": "Messaggi", + "pause": "Pausa", + "resume": "Riprendi", + "clear": "Pulisci", + "filter": { + "all": "Tutti gli argomenti", + "topics": "Argomenti", + "events": "Eventi", + "reviews": "Rivisti", + "classification": "Classificazione", + "face_recognition": "Riconoscimento facciale", + "lpr": "Risconoscimento targhe (LPR)", + "camera_activity": "Attività della telecamera", + "system": "Sistema", + "camera": "Telecamera", + "all_cameras": "Tutte le telecamere", + "cameras_count_one": "{{count}} telecamera", + "cameras_count_other": "{{count}} telecamere" + }, + "empty": "Nessun messaggio ancora catturato", + "count_one": "{{count}} messaggio", + "count_other": "{{count}} messaggi", + "expanded": { + "payload": "Carico" + } + } + }, + "general": { + "hardwareInfo": { + "gpuInfo": { + "nvidiaSMIOutput": { + "cudaComputerCapability": "Capacità di elaborazione CUDA: {{cuda_compute}}", + "title": "Risultati Nvidia SMI", + "name": "Nome: {{name}}", + "driver": "Driver: {{driver}}", + "vbios": "Informazioni VBios: {{vbios}}" + }, + "vainfoOutput": { + "title": "Risultati Vainfo", + "returnCode": "Codice di ritorno: {{code}}", + "processOutput": "Risultati del processo:", + "processError": "Errore del processo:" + }, + "closeInfo": { + "label": "Chiudi informazioni GPU" + }, + "copyInfo": { + "label": "Copia informazioni GPU" + }, + "toast": { + "success": "Informazioni GPU copiate negli appunti" + } + }, + "title": "Informazioni hardware", + "gpuDecoder": "Decodificatore GPU", + "gpuEncoder": "Codificatore GPU", + "gpuUsage": "Utilizzo GPU", + "gpuMemory": "Memoria GPU", + "npuUsage": "Utilizzo NPU", + "npuMemory": "Memoria NPU", + "intelGpuWarning": { + "title": "Avviso statistiche GPU Intel", + "message": "Statistiche GPU non disponibili", + "description": "Si tratta di un problema noto negli strumenti di reportistica delle statistiche GPU di Intel (intel_gpu_top), che si interrompe e restituisce ripetutamente un utilizzo della GPU pari a 0% anche nei casi in cui l'accelerazione hardware e il rilevamento degli oggetti funzionano correttamente sulla (i)GPU. Non si tratta di un problema di Frigate. È possibile riavviare il sistema per risolvere temporaneamente il problema e verificare che la GPU funzioni correttamente. Ciò non influisce sulle prestazioni." + }, + "gpuTemperature": "Temperatura GPU", + "npuTemperature": "Temperatura NPU", + "gpuCompute": "Calcolo / Codifica GPU" + }, + "detector": { + "inferenceSpeed": "Velocità inferenza rilevatore", + "title": "Rilevatori", + "cpuUsage": "Utilizzo CPU rilevatore", + "memoryUsage": "Utilizzo memoria rilevatore", + "temperature": "Temperatura del rilevatore", + "cpuUsageInformation": "CPU utilizzata nella preparazione dei dati di ingresso e uscita da/verso i modelli di rilevamento. Questo valore non misura l'utilizzo dell'inferenza, anche se si utilizza una GPU o un acceleratore." + }, + "title": "Generale", + "otherProcesses": { + "title": "Altri processi", + "processCpuUsage": "Utilizzo CPU processo", + "processMemoryUsage": "Utilizzo memoria processo", + "series": { + "go2rtc": "go2rtc", + "recording": "registrazione", + "review_segment": "segmento di revisione", + "embeddings": "incorporamenti", + "audio_detector": "rilevatore audio" + } + } + }, + "enrichments": { + "embeddings": { + "face_embedding_speed": "Velocità incorporamento volti", + "plate_recognition_speed": "Velocità riconoscimento targhe", + "image_embedding_speed": "Velocità incorporamento immagini", + "text_embedding_speed": "Velocità incorporamento testo", + "face_recognition_speed": "Velocità di riconoscimento facciale", + "face_recognition": "Riconoscimento facciale", + "plate_recognition": "Riconoscimento delle targhe", + "yolov9_plate_detection_speed": "Velocità di rilevamento della targa con YOLOv9", + "yolov9_plate_detection": "Rilevamento della targa con YOLOv9", + "image_embedding": "Incorporamento di immagini", + "text_embedding": "Incorporamento di testo", + "review_description": "Descrizione della revisione", + "review_description_speed": "Velocità della descrizione di revisione", + "review_description_events_per_second": "Descrizione della revisione", + "object_description": "Descrizione dell'oggetto", + "object_description_speed": "Velocità della descrizione dell'oggetto", + "object_description_events_per_second": "Descrizione dell'oggetto", + "classification": "Classificazione {{name}}", + "classification_speed": "Velocità di classificazione {{name}}", + "classification_events_per_second": "Eventi di classificazione {{name}} al secondo" + }, + "title": "Miglioramenti", + "infPerSecond": "Inferenze al secondo", + "averageInf": "Tempo medio di inferenza" + }, + "cameras": { + "info": { + "fetching": "Recupero dati della telecamera", + "streamDataFromFFPROBE": "I dati del flusso vengono ottenuti con ffprobe.", + "cameraProbeInfo": "Informazioni analisi telecamera {{camera}}", + "stream": "Flusso {{idx}}", + "video": "Video:", + "codec": "Codec:", + "resolution": "Risoluzione:", + "fps": "FPS:", + "unknown": "Sconosciuto", + "audio": "Audio:", + "error": "Errore: {{error}}", + "tips": { + "title": "Informazioni analisi telecamera" + }, + "aspectRatio": "rapporto d'aspetto" + }, + "title": "Telecamere", + "overview": "Sommario", + "framesAndDetections": "Fotogrammi / Rilevamenti", + "label": { + "camera": "telecamera", + "detect": "rileva", + "skipped": "saltati", + "ffmpeg": "FFmpeg", + "capture": "cattura", + "overallFramesPerSecond": "fotogrammi totali al secondo", + "overallDetectionsPerSecond": "rilevamenti totali al secondo", + "overallSkippedDetectionsPerSecond": "rilevamenti totali saltati al secondo", + "cameraCapture": "{{camName}} cattura", + "cameraDetect": "{{camName}} rilevamento", + "cameraFramesPerSecond": "{{camName}} fotogrammi al secondo", + "cameraDetectionsPerSecond": "{{camName}} rilevamenti al secondo", + "cameraSkippedDetectionsPerSecond": "{{camName}} rilevamenti saltati al secondo", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraGpu": "GPU {{camName}}" + }, + "toast": { + "success": { + "copyToClipboard": "Dati di analisi copiati negli appunti." + }, + "error": { + "unableToProbeCamera": "Impossibile analizzare la telecamera: {{errorMessage}}" + } + }, + "connectionQuality": { + "title": "Qualità connessione", + "excellent": "Ottima", + "fair": "Discreta", + "poor": "Scarsa", + "unusable": "Inutilizzabile", + "fps": "FPS", + "expectedFps": "FPS previsti", + "reconnectsLastHour": "Riconnessioni (ultima ora)", + "stallsLastHour": "Blocchi (ultima ora)" + }, + "noCameras": { + "title": "Nessuna telecamera trovata" + } + }, + "stats": { + "detectHighCpuUsage": "{{camera}} ha un utilizzo elevato della CPU con il rilevamento ({{detectAvg}}%)", + "ffmpegHighCpuUsage": "{{camera}} ha un elevato utilizzo della CPU con FFmpeg ({{ffmpegAvg}}%)", + "healthy": "Il sistema è integro", + "reindexingEmbeddings": "Reindicizzazione degli incorporamenti (completata al {{processed}}%)", + "cameraIsOffline": "{{camera}} è disconnessa", + "detectIsSlow": "{{detect}} è lento ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} è molto lento ({{speed}} ms)", + "shmTooLow": "L'allocazione /dev/shm ({{total}} MB) dovrebbe essere aumentata almeno a {{min}} MB.", + "debugReplayActive": "La sessione di riproduzione delle correzioni è attiva" + }, + "title": "Sistema", + "metrics": "Metriche di sistema", + "storage": { + "title": "Archiviazione", + "overview": "Sommario", + "recordings": { + "title": "Registrazioni", + "tips": "Questo valore rappresenta lo spazio di archiviazione totale utilizzato dalle registrazioni nel database di Frigate. Frigate non tiene traccia dell'utilizzo dello spazio di archiviazione per tutti i file presenti sul disco.", + "earliestRecording": "Prima registrazione disponibile:" + }, + "cameraStorage": { + "title": "Archiviazione della telecamera", + "camera": "Telecamera", + "unusedStorageInformation": "Informazioni spazio di archiviazione non utilizzato", + "storageUsed": "Archiviazione", + "percentageOfTotalUsed": "Percentuale del totale", + "bandwidth": "Larghezza di banda", + "unused": { + "title": "Liberi", + "tips": "Questo valore potrebbe non rappresentare accuratamente lo spazio libero disponibile per Frigate se nel disco sono archiviati altri file oltre alle registrazioni di Frigate. Frigate non tiene traccia dell'utilizzo dello spazio di archiviazione al di fuori delle sue registrazioni." + } + }, + "shm": { + "title": "Allocazione SHM (memoria condivisa)", + "warning": "La dimensione SHM attuale di {{total}} MB è troppo piccola. Aumentarla ad almeno {{min_shm}} MB.", + "readTheDocumentation": "Leggi la documentazione", + "frameLifetime": { + "title": "Durata del fotogramma", + "description": "Ogni telecamera dispone di {{frames}} posti per i fotogrammi nella memoria condivisa. Alla frequenza di fotogrammi più elevata della telecamera, ogni fotogramma è disponibile per circa {{lifetime}} secondi prima di essere sovrascritto." + } + } + }, + "lastRefreshed": "Ultimo aggiornamento: " +} diff --git a/web/public/locales/ja/audio.json b/web/public/locales/ja/audio.json new file mode 100644 index 0000000..e60078e --- /dev/null +++ b/web/public/locales/ja/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "話し声", + "car": "車", + "bicycle": "自転車", + "yell": "叫び声", + "motorcycle": "オートバイ", + "babbling": "赤ちゃんの喃語", + "bellow": "怒鳴り声", + "whoop": "歓声", + "whispering": "ささやき声", + "laughter": "笑い声", + "snicker": "くすくす笑い", + "crying": "泣き声", + "sigh": "ため息", + "singing": "歌声", + "choir": "合唱", + "yodeling": "ヨーデル", + "chant": "詠唱", + "mantra": "マントラ", + "child_singing": "子供の歌声", + "synthetic_singing": "合成音声の歌", + "rapping": "ラップ", + "humming": "ハミング", + "groan": "うめき声", + "grunt": "うなり声", + "whistling": "口笛", + "breathing": "呼吸音", + "wheeze": "ぜいぜい声", + "snoring": "いびき", + "gasp": "はっと息をのむ音", + "pant": "荒い息", + "snort": "鼻を鳴らす音", + "cough": "咳", + "throat_clearing": "咳払い", + "sneeze": "くしゃみ", + "sniff": "鼻をすする音", + "run": "走る音", + "shuffle": "足を引きずる音", + "footsteps": "足音", + "chewing": "咀嚼音", + "biting": "かみつく音", + "gargling": "うがい", + "stomach_rumble": "お腹の音", + "burping": "げっぷ", + "hiccup": "しゃっくり", + "fart": "おなら", + "hands": "手の音", + "finger_snapping": "指を鳴らす音", + "clapping": "拍手", + "heartbeat": "心臓の鼓動", + "heart_murmur": "心雑音", + "cheering": "歓声", + "applause": "拍手喝采", + "chatter": "おしゃべり", + "crowd": "群衆", + "children_playing": "子供の遊ぶ声", + "animal": "動物", + "pets": "ペット", + "dog": "犬", + "bark": "吠え声", + "yip": "キャンキャン鳴く声", + "howl": "遠吠え", + "bow_wow": "ワンワン", + "growling": "うなり声", + "whimper_dog": "犬の鳴き声(クンクン)", + "cat": "猫", + "purr": "ゴロゴロ音", + "meow": "ニャー", + "hiss": "シャー", + "caterwaul": "猫のけんか声", + "livestock": "家畜", + "horse": "馬", + "clip_clop": "カツカツ音", + "neigh": "いななき", + "cattle": "牛", + "moo": "モー", + "cowbell": "カウベル", + "pig": "豚", + "oink": "ブーブー", + "goat": "ヤギ", + "bleat": "メェー", + "sheep": "羊", + "fowl": "家禽", + "chicken": "鶏", + "cluck": "コッコッ", + "cock_a_doodle_doo": "コケコッコー", + "turkey": "七面鳥", + "gobble": "グルル", + "duck": "アヒル", + "quack": "ガーガー", + "goose": "ガチョウ", + "honk": "ホンク", + "wild_animals": "野生動物", + "roaring_cats": "猛獣の鳴き声", + "roar": "咆哮", + "bird": "鳥", + "chirp": "さえずり", + "squawk": "ギャーギャー", + "pigeon": "ハト", + "coo": "クークー", + "crow": "カラス", + "caw": "カーカー", + "owl": "フクロウ", + "hoot": "ホーホー", + "flapping_wings": "羽ばたき", + "dogs": "犬たち", + "rats": "ネズミ", + "mouse": "マウス", + "patter": "パタパタ音", + "insect": "昆虫", + "cricket": "コオロギ", + "mosquito": "蚊", + "fly": "ハエ", + "buzz": "ブーン", + "frog": "カエル", + "croak": "ゲロゲロ", + "snake": "ヘビ", + "rattle": "ガラガラ音", + "whale_vocalization": "クジラの鳴き声", + "music": "音楽", + "musical_instrument": "楽器", + "plucked_string_instrument": "撥弦楽器", + "guitar": "ギター", + "electric_guitar": "エレキギター", + "bass_guitar": "ベースギター", + "acoustic_guitar": "アコースティックギター", + "steel_guitar": "スティールギター", + "tapping": "タッピング", + "strum": "ストローク", + "banjo": "バンジョー", + "sitar": "シタール", + "mandolin": "マンドリン", + "zither": "ツィター", + "ukulele": "ウクレレ", + "keyboard": "キーボード", + "piano": "ピアノ", + "electric_piano": "エレクトリックピアノ", + "organ": "オルガン", + "electronic_organ": "電子オルガン", + "hammond_organ": "ハモンドオルガン", + "synthesizer": "シンセサイザー", + "sampler": "サンプラー", + "harpsichord": "チェンバロ", + "percussion": "打楽器", + "drum_kit": "ドラムセット", + "drum_machine": "ドラムマシン", + "drum": "ドラム", + "snare_drum": "スネアドラム", + "rimshot": "リムショット", + "drum_roll": "ドラムロール", + "bass_drum": "バスドラム", + "timpani": "ティンパニ", + "tabla": "タブラ", + "cymbal": "シンバル", + "hi_hat": "ハイハット", + "wood_block": "ウッドブロック", + "tambourine": "タンバリン", + "maraca": "マラカス", + "gong": "ゴング", + "tubular_bells": "チューブラーベル", + "mallet_percussion": "マレット打楽器", + "marimba": "マリンバ", + "glockenspiel": "グロッケンシュピール", + "vibraphone": "ビブラフォン", + "steelpan": "スティールパン", + "orchestra": "オーケストラ", + "brass_instrument": "金管楽器", + "french_horn": "フレンチホルン", + "trumpet": "トランペット", + "trombone": "トロンボーン", + "bowed_string_instrument": "擦弦楽器", + "string_section": "弦楽セクション", + "violin": "バイオリン", + "pizzicato": "ピチカート", + "cello": "チェロ", + "double_bass": "コントラバス", + "wind_instrument": "木管楽器", + "flute": "フルート", + "saxophone": "サックス", + "clarinet": "クラリネット", + "harp": "ハープ", + "bell": "鐘", + "church_bell": "教会の鐘", + "jingle_bell": "ジングルベル", + "bicycle_bell": "自転車ベル", + "tuning_fork": "音叉", + "chime": "チャイム", + "wind_chime": "風鈴", + "harmonica": "ハーモニカ", + "accordion": "アコーディオン", + "bagpipes": "バグパイプ", + "didgeridoo": "ディジュリドゥ", + "theremin": "テルミン", + "singing_bowl": "シンギングボウル", + "scratching": "スクラッチ音", + "pop_music": "ポップ音楽", + "hip_hop_music": "ヒップホップ音楽", + "beatboxing": "ボイスパーカッション", + "rock_music": "ロック音楽", + "heavy_metal": "ヘビーメタル", + "punk_rock": "パンクロック", + "grunge": "グランジ", + "progressive_rock": "プログレッシブロック", + "rock_and_roll": "ロックンロール", + "psychedelic_rock": "サイケデリックロック", + "rhythm_and_blues": "リズム・アンド・ブルース", + "soul_music": "ソウル音楽", + "reggae": "レゲエ", + "country": "カントリー", + "swing_music": "スウィング音楽", + "bluegrass": "ブルーグラス", + "funk": "ファンク", + "folk_music": "フォーク音楽", + "middle_eastern_music": "中東音楽", + "jazz": "ジャズ", + "disco": "ディスコ", + "classical_music": "クラシック音楽", + "opera": "オペラ", + "electronic_music": "電子音楽", + "house_music": "ハウス", + "techno": "テクノ", + "dubstep": "ダブステップ", + "drum_and_bass": "ドラムンベース", + "electronica": "エレクトロニカ", + "electronic_dance_music": "エレクトロニック・ダンス・ミュージック", + "ambient_music": "アンビエント", + "trance_music": "トランス", + "music_of_latin_america": "ラテン音楽", + "salsa_music": "サルサ", + "flamenco": "フラメンコ", + "blues": "ブルース", + "music_for_children": "子供向け音楽", + "new-age_music": "ニューエイジ音楽", + "vocal_music": "声楽", + "a_capella": "アカペラ", + "music_of_africa": "アフリカ音楽", + "afrobeat": "アフロビート", + "christian_music": "キリスト教音楽", + "gospel_music": "ゴスペル", + "music_of_asia": "アジア音楽", + "carnatic_music": "カルナータカ音楽", + "music_of_bollywood": "ボリウッド音楽", + "ska": "スカ", + "traditional_music": "伝統音楽", + "independent_music": "インディーズ音楽", + "song": "歌", + "background_music": "BGM", + "theme_music": "テーマ音楽", + "jingle": "ジングル", + "soundtrack_music": "サウンドトラック", + "lullaby": "子守唄", + "video_game_music": "ゲーム音楽", + "christmas_music": "クリスマス音楽", + "dance_music": "ダンス音楽", + "wedding_music": "結婚式音楽", + "happy_music": "明るい音楽", + "sad_music": "悲しい音楽", + "tender_music": "優しい音楽", + "exciting_music": "ワクワクする音楽", + "angry_music": "怒りの音楽", + "scary_music": "怖い音楽", + "wind": "風", + "rustling_leaves": "木の葉のざわめき", + "wind_noise": "風の音", + "thunderstorm": "雷雨", + "thunder": "雷鳴", + "water": "水", + "rain": "雨", + "raindrop": "雨粒", + "rain_on_surface": "雨が当たる音", + "stream": "小川", + "waterfall": "滝", + "ocean": "海", + "waves": "波", + "steam": "蒸気", + "gurgling": "ゴボゴボ音", + "fire": "火", + "crackle": "パチパチ音", + "vehicle": "車両", + "boat": "ボート", + "sailboat": "帆船", + "rowboat": "手漕ぎボート", + "motorboat": "モーターボート", + "ship": "船", + "motor_vehicle": "自動車", + "toot": "クラクション", + "car_alarm": "車のアラーム", + "power_windows": "パワーウィンドウ", + "skidding": "スリップ音", + "tire_squeal": "タイヤの悲鳴", + "car_passing_by": "車が通る音", + "race_car": "レーシングカー", + "truck": "トラック", + "air_brake": "エアブレーキ", + "air_horn": "エアホーン", + "reversing_beeps": "バック警告音", + "ice_cream_truck": "アイスクリームトラック", + "bus": "バス", + "emergency_vehicle": "緊急車両", + "police_car": "パトカー", + "ambulance": "救急車", + "fire_engine": "消防車", + "traffic_noise": "交通騒音", + "rail_transport": "鉄道輸送", + "train": "電車", + "train_whistle": "汽笛", + "train_horn": "列車のホーン", + "railroad_car": "鉄道車両", + "train_wheels_squealing": "車輪のきしむ音", + "subway": "地下鉄", + "aircraft": "航空機", + "aircraft_engine": "航空機エンジン", + "jet_engine": "ジェットエンジン", + "propeller": "プロペラ", + "helicopter": "ヘリコプター", + "fixed-wing_aircraft": "固定翼機", + "skateboard": "スケートボード", + "engine": "エンジン", + "light_engine": "小型エンジン", + "dental_drill's_drill": "歯科用ドリル", + "lawn_mower": "芝刈り機", + "chainsaw": "チェーンソー", + "medium_engine": "中型エンジン", + "heavy_engine": "大型エンジン", + "engine_knocking": "ノッキング音", + "engine_starting": "エンジン始動", + "idling": "アイドリング", + "accelerating": "加速音", + "door": "ドア", + "doorbell": "ドアベル", + "ding-dong": "ピンポン", + "sliding_door": "引き戸", + "slam": "ドアをバタンと閉める音", + "knock": "ノック", + "tap": "トントン音", + "squeak": "きしみ音", + "cupboard_open_or_close": "戸棚の開閉", + "drawer_open_or_close": "引き出しの開閉", + "dishes": "食器", + "cutlery": "カトラリー", + "chopping": "包丁で切る音", + "frying": "揚げ物の音", + "microwave_oven": "電子レンジ", + "blender": "ミキサー", + "water_tap": "水道の蛇口", + "sink": "流し台", + "bathtub": "浴槽", + "hair_dryer": "ヘアドライヤー", + "toilet_flush": "トイレの水流", + "toothbrush": "歯ブラシ", + "electric_toothbrush": "電動歯ブラシ", + "vacuum_cleaner": "掃除機", + "zipper": "ファスナー", + "keys_jangling": "鍵のジャラジャラ音", + "coin": "コイン", + "scissors": "はさみ", + "electric_shaver": "電気シェーバー", + "shuffling_cards": "カードを切る音", + "typing": "タイピング音", + "typewriter": "タイプライター", + "computer_keyboard": "コンピュータキーボード", + "writing": "書く音", + "alarm": "アラーム", + "telephone": "電話", + "telephone_bell_ringing": "電話のベル音", + "ringtone": "着信音", + "telephone_dialing": "ダイヤル音", + "dial_tone": "発信音", + "busy_signal": "話中音", + "alarm_clock": "目覚まし時計", + "siren": "サイレン", + "civil_defense_siren": "防災サイレン", + "buzzer": "ブザー", + "smoke_detector": "火災警報器", + "fire_alarm": "火災報知器", + "foghorn": "霧笛", + "whistle": "ホイッスル", + "steam_whistle": "蒸気笛", + "mechanisms": "機械仕掛け", + "ratchet": "ラチェット", + "clock": "時計", + "tick": "カチカチ音", + "tick-tock": "チクタク音", + "gears": "歯車", + "pulleys": "滑車", + "sewing_machine": "ミシン", + "mechanical_fan": "扇風機", + "air_conditioning": "エアコン", + "cash_register": "レジ", + "printer": "プリンター", + "camera": "カメラ", + "single-lens_reflex_camera": "一眼レフカメラ", + "tools": "工具", + "hammer": "ハンマー", + "jackhammer": "削岩機", + "sawing": "のこぎり", + "filing": "やすりがけ", + "sanding": "研磨", + "power_tool": "電動工具", + "drill": "ドリル", + "explosion": "爆発", + "gunshot": "銃声", + "machine_gun": "機関銃", + "fusillade": "一斉射撃", + "artillery_fire": "砲撃", + "cap_gun": "おもちゃのピストル", + "fireworks": "花火", + "firecracker": "爆竹", + "burst": "破裂音", + "eruption": "噴火", + "boom": "ドカン", + "wood": "木材", + "chop": "伐採音", + "splinter": "裂ける音", + "crack": "割れる音", + "glass": "ガラス", + "chink": "チリン音", + "shatter": "粉々に割れる音", + "silence": "静寂", + "sound_effect": "効果音", + "environmental_noise": "環境音", + "static": "ノイズ", + "white_noise": "ホワイトノイズ", + "pink_noise": "ピンクノイズ", + "television": "テレビ", + "radio": "ラジオ", + "field_recording": "フィールド録音", + "scream": "悲鳴", + "sodeling": "ソデリング", + "chird": "チャープ", + "change_ringing": "着信音の変更", + "shofar": "ショファー", + "liquid": "液体", + "splash": "水しぶき", + "slosh": "水が揺れる音", + "squish": "ぐちゃっという音", + "drip": "滴る音", + "pour": "注ぐ", + "trickle": "ちょろちょろ流れる音", + "gush": "勢いよく噴き出す", + "fill": "満たす", + "spray": "噴霧", + "pump": "ポンプ", + "stir": "かき混ぜる", + "boiling": "沸騰", + "sonar": "ソナー", + "arrow": "矢", + "whoosh": "ヒューという音", + "thump": "ドンという音", + "thunk": "鈍い衝撃音", + "electronic_tuner": "電子チューナー", + "effects_unit": "エフェクター", + "chorus_effect": "コーラス効果", + "basketball_bounce": "バスケットボールのバウンド", + "bang": "バンという音", + "slap": "平手打ち", + "whack": "強打", + "smash": "粉砕", + "breaking": "破壊", + "bouncing": "跳ねる音", + "whip": "ムチの音", + "flap": "はためく音", + "scratch": "引っかく音", + "scrape": "こする音", + "rub": "こする", + "roll": "転がる", + "crushing": "押しつぶす", + "crumpling": "くしゃくしゃにする音", + "tearing": "引き裂く音", + "beep": "ビープ音", + "ping": "ピン音", + "ding": "ディン音", + "clang": "金属音", + "squeal": "きしむ音", + "creak": "きしみ", + "rustle": "かさかさ音", + "whir": "ブーンという音", + "clatter": "ガタガタ音", + "sizzle": "ジュージュー音", + "clicking": "クリック音", + "clickety_clack": "カチカチ音", + "rumble": "ゴロゴロ音", + "plop": "ポチャン", + "hum": "ハム音", + "zing": "ジーン音", + "boing": "ボイン音", + "crunch": "バリバリ音", + "sine_wave": "正弦波", + "harmonic": "倍音", + "chirp_tone": "チャープ音", + "pulse": "パルス", + "inside": "内側", + "outside": "外側", + "reverberation": "残響", + "echo": "エコー", + "noise": "ノイズ", + "mains_hum": "電源ハム", + "distortion": "歪み", + "sidetone": "サイドトーン", + "cacophony": "不協和音", + "throbbing": "脈動", + "vibration": "振動" +} diff --git a/web/public/locales/ja/common.json b/web/public/locales/ja/common.json new file mode 100644 index 0000000..4b538f7 --- /dev/null +++ b/web/public/locales/ja/common.json @@ -0,0 +1,317 @@ +{ + "time": { + "untilForRestart": "Frigate が再起動するまで。", + "untilRestart": "再起動まで", + "untilForTime": "{{time}} まで", + "ago": "{{timeAgo}} 前", + "justNow": "今", + "today": "本日", + "yesterday": "昨日", + "last7": "7日間", + "last14": "14日間", + "last30": "30日間", + "thisWeek": "今週", + "lastWeek": "先週", + "thisMonth": "今月", + "lastMonth": "先月", + "5minutes": "5 分", + "10minutes": "10 分", + "30minutes": "30 分", + "1hour": "1 時間", + "12hours": "12 時間", + "24hours": "24 時間", + "pm": "午後", + "am": "午前", + "yr": "{{time}}年", + "year_other": "{{time}} 年", + "mo": "{{time}}ヶ月", + "month_other": "{{time}} ヶ月", + "d": "{{time}}日", + "day_other": "{{time}} 日", + "h": "{{time}}時間", + "hour_other": "{{time}} 時間", + "m": "{{time}}分", + "minute_other": "{{time}} 分", + "s": "{{time}}秒", + "second_other": "{{time}} 秒", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "inProgress": "処理中", + "invalidStartTime": "開始時刻が無効です", + "invalidEndTime": "終了時刻が無効です", + "never": "なし" + }, + "readTheDocumentation": "ドキュメントを見る", + "unit": { + "speed": { + "mph": "mph", + "kph": "Km/h" + }, + "length": { + "feet": "フィート", + "meters": "メートル" + }, + "data": { + "gbph": "GB/hour", + "gbps": "GB/s", + "kbph": "kB/hour", + "kbps": "kB/s", + "mbph": "MB/hour", + "mbps": "MB/s" + } + }, + "label": { + "back": "戻る", + "hide": "{{item}} を非表示", + "show": "{{item}} を表示", + "ID": "ID", + "none": "なし", + "all": "すべて", + "other": "その他" + }, + "button": { + "apply": "適用", + "reset": "リセット", + "done": "完了", + "enabled": "有効", + "enable": "有効にする", + "disabled": "無効", + "disable": "無効にする", + "save": "保存", + "saving": "保存中…", + "cancel": "キャンセル", + "close": "閉じる", + "copy": "コピー", + "back": "戻る", + "history": "履歴", + "fullscreen": "全画面", + "exitFullscreen": "全画面解除", + "pictureInPicture": "ピクチャーインピクチャー", + "twoWayTalk": "双方向通話", + "cameraAudio": "カメラ音声", + "on": "オン", + "off": "オフ", + "edit": "編集", + "copyCoordinates": "座標をコピー", + "delete": "削除", + "yes": "はい", + "no": "いいえ", + "download": "ダウンロード", + "info": "情報", + "suspended": "一時停止", + "unsuspended": "再開", + "play": "再生", + "unselect": "選択解除", + "export": "エクスポート", + "deleteNow": "今すぐ削除", + "next": "次へ", + "continue": "続行", + "add": "追加", + "applying": "適用中…", + "undo": "元に戻す", + "copiedToClipboard": "クリップボードにコピーしました", + "modified": "変更あり", + "overridden": "上書き済み", + "resetToGlobal": "グローバル設定にリセット", + "resetToDefault": "デフォルトにリセット", + "saveAll": "すべて保存", + "savingAll": "すべて保存中…", + "undoAll": "すべて元に戻す", + "retry": "再試行" + }, + "menu": { + "system": "システム", + "systemMetrics": "システムメトリクス", + "configuration": "設定", + "systemLogs": "システムログ", + "settings": "設定", + "configurationEditor": "設定エディタ", + "languages": "言語", + "appearance": "外観", + "darkMode": { + "label": "ダークモード", + "light": "ライト", + "dark": "ダーク", + "withSystem": { + "label": "システム設定に従う" + } + }, + "withSystem": "システム", + "theme": { + "label": "テーマ", + "blue": "青", + "green": "緑", + "nord": "ノルド", + "red": "赤", + "highcontrast": "ハイコントラスト", + "default": "デフォルト" + }, + "help": "ヘルプ", + "documentation": { + "title": "ドキュメント", + "label": "Frigate ドキュメント" + }, + "restart": "Frigate を再起動", + "live": { + "title": "ライブ", + "allCameras": "全カメラ", + "cameras": { + "title": "カメラ", + "count_other": "{{count}} 台のカメラ" + } + }, + "review": "レビュー", + "explore": "ブラウズ", + "export": "エクスポート", + "uiPlayground": "UI テスト環境", + "faceLibrary": "顔データベース", + "user": { + "title": "ユーザー", + "account": "アカウント", + "current": "現在のユーザー: {{user}}", + "anonymous": "未ログイン", + "logout": "ログアウト", + "setPassword": "パスワードを設定" + }, + "language": { + "en": "English (英語)", + "es": "Español (スペイン語)", + "zhCN": "简体中文 (簡体字中国語)", + "hi": "हिन्दी (ヒンディー語)", + "fr": "Français (フランス語)", + "ar": "العربية (アラビア語)", + "pt": "Português (ポルトガル語)", + "ptBR": "Português brasileiro (ブラジルポルトガル語)", + "ru": "Русский (ロシア語)", + "de": "Deutsch (ドイツ語)", + "ja": "日本語 (日本語)", + "tr": "Türkçe (トルコ語)", + "it": "Italiano (イタリア語)", + "nl": "Nederlands (オランダ語)", + "sv": "Svenska (スウェーデン語)", + "cs": "Čeština (チェコ語)", + "nb": "Norsk Bokmål (ノルウェー語)", + "ko": "한국어 (韓国語)", + "vi": "Tiếng Việt (ベトナム語)", + "fa": "فارسی (ペルシア語)", + "pl": "Polski (ポーランド語)", + "uk": "Українська (ウクライナ語)", + "he": "עברית (ヘブライ語)", + "el": "Ελληνικά (ギリシャ語)", + "ro": "Română (ルーマニア語)", + "hu": "Magyar (ハンガリー語)", + "fi": "Suomi (フィンランド語)", + "da": "Dansk (デンマーク語)", + "sk": "Slovenčina (スロバキア語)", + "yue": "粵語 (広東語)", + "th": "ไทย (タイ語)", + "ca": "Català (カタルーニャ語)", + "sr": "Српски (セルビア語)", + "sl": "Slovenščina (スロベニア語)", + "lt": "Lietuvių (リトアニア語)", + "bg": "Български (ブルガリア語)", + "gl": "Galego (ガリシア語)", + "id": "Bahasa Indonesia (インドネシア語)", + "ur": "اردو (ウルドゥー語)", + "withSystem": { + "label": "システム設定に従う" + }, + "hr": "Hrvatski (クロアチア語)", + "bs": "Bosanski (ボスニア語)", + "zhHant": "繁體中文 (繁体字中国語)" + }, + "classification": "分類", + "profiles": "プロファイル", + "actions": "操作", + "features": "機能", + "chat": "チャット" + }, + "toast": { + "copyUrlToClipboard": "URLをクリップボードにコピーしました。", + "save": { + "title": "保存", + "error": { + "title": "設定変更の保存に失敗しました: {{errorMessage}}", + "noMessage": "設定変更の保存に失敗しました" + }, + "success": "設定変更を保存しました。" + } + }, + "role": { + "title": "役割", + "admin": "管理者", + "viewer": "閲覧者", + "desc": "管理者はFrigate UIのすべての機能に完全にアクセスできます。閲覧者はカメラ、レビュー項目、履歴映像の閲覧に制限されます。" + }, + "pagination": { + "label": "ページ移動", + "previous": { + "title": "前へ", + "label": "前のページへ" + }, + "next": { + "title": "次へ", + "label": "次のページへ" + }, + "more": "さらにページ" + }, + "accessDenied": { + "documentTitle": "アクセス拒否 - Frigate", + "title": "アクセス拒否", + "desc": "このページを表示する権限がありません。" + }, + "notFound": { + "documentTitle": "ページが見つかりません - Frigate", + "title": "404", + "desc": "ページが見つかりません" + }, + "selectItem": "{{item}} を選択", + "information": { + "pixels": "{{area}}ピクセル" + }, + "list": { + "two": "{{0}} と {{1}}", + "many": "{{items}}と {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "任意", + "internalID": "Frigate が設定で使用する内部 ID です" + }, + "no_items": "項目がありません", + "validation_errors": "入力エラー", + "credentialField": { + "savedPlaceholder": "保存済み — 変更しない場合は空欄" + } +} diff --git a/web/public/locales/ja/components/auth.json b/web/public/locales/ja/components/auth.json new file mode 100644 index 0000000..e89c35a --- /dev/null +++ b/web/public/locales/ja/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "ユーザー名", + "password": "パスワード", + "login": "ログイン", + "errors": { + "usernameRequired": "ユーザー名は必須です", + "passwordRequired": "パスワードは必須です", + "rateLimit": "リクエスト制限を超えました。後でもう一度お試しください。", + "loginFailed": "ログインに失敗しました", + "unknownError": "不明なエラー。ログを確認してください。", + "webUnknownError": "不明なエラー。コンソールログを確認してください。" + }, + "firstTimeLogin": "初めてログインしますか?認証情報は Frigate のログに表示されています。" + } +} diff --git a/web/public/locales/ja/components/camera.json b/web/public/locales/ja/components/camera.json new file mode 100644 index 0000000..17ad52f --- /dev/null +++ b/web/public/locales/ja/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "カメラグループ", + "add": "カメラグループを追加", + "edit": "カメラグループを編集", + "delete": { + "label": "カメラグループを削除", + "confirm": { + "title": "削除の確認", + "desc": "カメラグループ {{name}} を削除してもよろしいですか?" + } + }, + "name": { + "label": "名前", + "placeholder": "名前を入力…", + "errorMessage": { + "mustLeastCharacters": "カメラグループ名は2文字以上である必要があります。", + "exists": "このカメラグループ名は既に存在します。", + "nameMustNotPeriod": "カメラグループ名にピリオドは使用できません。", + "invalid": "無効なカメラグループ名です。" + } + }, + "cameras": { + "label": "カメラ", + "desc": "このグループに含めるカメラを選択します。" + }, + "icon": "アイコン", + "success": "カメラグループ({{name}})を保存しました。", + "camera": { + "birdseye": "バードアイ", + "setting": { + "label": "カメラのストリーミング設定", + "title": "{{cameraName}} のストリーミング設定", + "desc": "このカメラグループのダッシュボードでのライブストリーミングオプションを変更します。これらの設定はデバイス/ブラウザごとに異なります。", + "audioIsAvailable": "このストリームでは音声が利用可能です", + "audioIsUnavailable": "このストリームでは音声は利用できません", + "audio": { + "tips": { + "title": "このストリームで音声を使用するには、カメラから音声が出力され、go2rtc で設定されている必要があります。" + } + }, + "stream": "ストリーム", + "placeholder": "ストリームを選択", + "streamMethod": { + "label": "ストリーミング方式", + "placeholder": "方式を選択", + "method": { + "noStreaming": { + "label": "ストリーミングなし", + "desc": "カメラ画像は1分に1回のみ更新され、ライブストリーミングは行われません。" + }, + "smartStreaming": { + "label": "スマートストリーミング(推奨)", + "desc": "検知可能なアクティビティがない場合は、帯域とリソース節約のため画像を1分に1回更新します。アクティビティが検知されると、画像はシームレスにライブストリームへ切り替わります。" + }, + "continuousStreaming": { + "label": "常時ストリーミング", + "desc": { + "title": "ダッシュボードで表示されている間は、アクティビティが検知されていなくても常にライブストリームになります。", + "warning": "常時ストリーミングは高い帯域幅使用やパフォーマンス問題の原因となる場合があります。注意して使用してください。" + } + } + } + }, + "compatibilityMode": { + "label": "互換モード", + "desc": "このオプションは、ライブストリームに色のアーティファクトが表示され、画像右側に斜めの線が出る場合にのみ有効にしてください。" + } + } + } + }, + "debug": { + "options": { + "label": "設定", + "title": "オプション", + "showOptions": "オプションを表示", + "hideOptions": "オプションを非表示" + }, + "boundingBox": "バウンディングボックス", + "timestamp": "タイムスタンプ", + "zones": "ゾーン", + "mask": "マスク", + "motion": "モーション", + "regions": "領域", + "paths": "軌跡" + } +} diff --git a/web/public/locales/ja/components/dialog.json b/web/public/locales/ja/components/dialog.json new file mode 100644 index 0000000..cfe49ca --- /dev/null +++ b/web/public/locales/ja/components/dialog.json @@ -0,0 +1,179 @@ +{ + "restart": { + "title": "Frigate を再起動してもよろしいですか?", + "restarting": { + "title": "Frigate を再起動中", + "content": "このページは {{countdown}} 秒後に再読み込みされます。", + "button": "今すぐ強制再読み込み" + }, + "button": "再起動", + "description": "再起動の間、Frigateが一時的に停止します。" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Frigate+ に送信", + "desc": "回避したい場所でのオブジェクトは誤検知ではありません。誤検知として送信するとモデルが混乱します。" + }, + "review": { + "question": { + "label": "Frigate Plus 用ラベルの確認", + "ask_a": "このオブジェクトは {{label}} ですか?", + "ask_an": "このオブジェクトは {{label}} ですか?", + "ask_full": "このオブジェクトは {{untranslatedLabel}}({{translatedLabel}})ですか?" + }, + "state": { + "submitted": "送信済み" + } + } + }, + "video": { + "viewInHistory": "履歴で表示" + } + }, + "export": { + "time": { + "fromTimeline": "タイムラインから選択", + "lastHour_other": "直近{{count}}時間", + "custom": "カスタム", + "start": { + "title": "開始時刻", + "label": "開始時刻を選択" + }, + "end": { + "title": "終了時刻", + "label": "終了時刻を選択" + } + }, + "name": { + "placeholder": "エクスポートに名前を付ける" + }, + "select": "選択", + "export": "エクスポート", + "selectOrExport": "選択またはエクスポート", + "toast": { + "success": "エクスポートを開始しました。エクスポートページでファイルを確認できます。", + "error": { + "failed": "エクスポートキューの開始に失敗しました: {{error}}", + "endTimeMustAfterStartTime": "終了時間は開始時間より後である必要があります", + "noVaildTimeSelected": "有効な時間範囲が選択されていません" + }, + "view": "表示", + "queued": "エクスポートがキューに追加されました。進捗状況はエクスポートページで確認できます。", + "batchQueuedSuccess_other": "{{count}} 件のエクスポートがキューに登録されました。現在ケースをオープンしています。", + "batchQueuedPartial": "{{total}} 件中 {{successful}} 件のエクスポートがキューに追加されました。失敗したカメラ: {{failedCameras}}", + "batchQueueFailed": "{{total}} 件のエクスポートをキューに追加できませんでした。失敗したカメラ: {{failedCameras}}", + "batchSuccess_other": "{{count}} 件のエクスポートを開始しました。ケースを開きます。", + "batchPartial": "{{total}} 件中 {{successful}} 件のエクスポートを開始しました。失敗したカメラ: {{failedCameras}}", + "batchFailed": "{{total}} 件のエクスポートを開始できませんでした。失敗したカメラ: {{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "エクスポートを保存", + "previewExport": "エクスポートをプレビュー", + "queueingExport": "エクスポートをキューイングしています...", + "useThisRange": "この範囲を使用" + }, + "queueing": "エクスポートをキューイングしています...", + "multiCamera": { + "queueingButton": "エクスポートをキューイングしています...", + "timeRange": "期間", + "selectFromTimeline": "タイムラインから選択", + "cameraSelection": "カメラ", + "cameraSelectionHelp": "この期間に追跡対象が含まれるカメラは、あらかじめ選択されています", + "checkingActivity": "カメラの動作を確認中...", + "noCameras": "利用可能なカメラがありません", + "detectionCount_other": "{{count}} 追跡対象", + "nameLabel": "エクスポート名", + "namePlaceholder": "これらのエクスポート用オプションのベース名", + "exportButton_other": "{{count}} 台のカメラをエクスポート" + }, + "case": { + "newCaseOption": "新しいケースを作成", + "newCaseNamePlaceholder": "新しいケース名", + "newCaseDescriptionPlaceholder": "ケースの説明", + "label": "ケース", + "nonAdminHelp": "これらのエクスポートに対して新しいケースが作成されます。", + "placeholder": "ケースを選択" + }, + "tabs": { + "export": "シングルカメラ", + "multiCamera": "マルチカメラ" + }, + "multi": { + "title_other": "{{count}} 件のレビューをエクスポート", + "description": "選択した各レビューをエクスポートします。すべてのエクスポートは 1 つのケースにまとめられます。", + "descriptionNoCase": "選択した各レビューをエクスポートします。", + "caseNamePlaceholder": "レビューエクスポート - {{date}}", + "exportButton_other": "{{count}} 件のレビューをエクスポート", + "exportingButton": "エクスポート中...", + "toast": { + "started_other": "{{count}} 件のエクスポートを開始しました。ケースを開きます。", + "startedNoCase_other": "{{count}} 件のエクスポートを開始しました。", + "partial": "{{total}} 件中 {{successful}} 件のエクスポートを開始しました。失敗: {{failedItems}}", + "failed": "{{total}} 件のエクスポートを開始できませんでした。失敗: {{failedItems}}" + } + } + }, + "streaming": { + "label": "ストリーム", + "restreaming": { + "disabled": "このカメラではリストリーミングは有効になっていません。", + "desc": { + "title": "このカメラで追加のライブビューと音声を利用するには go2rtc をセットアップしてください。" + } + }, + "showStats": { + "label": "ストリーム統計を表示", + "desc": "有効にすると、カメラ映像に統計情報をオーバーレイ表示します。" + }, + "debugView": "デバッグビュー" + }, + "search": { + "saveSearch": { + "label": "検索を保存", + "desc": "この保存済み検索の名前を入力してください。", + "placeholder": "検索名を入力", + "overwrite": "{{searchName}} は既に存在します。保存すると上書きされます。", + "success": "検索({{searchName}})を保存しました。", + "button": { + "save": { + "label": "この検索を保存" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "削除の確認", + "desc": { + "selected": "このレビュー項目に関連付けられた録画動画をすべて削除してもよろしいですか?

    今後このダイアログを表示しない場合は Shift キーを押しながら操作してください。" + }, + "toast": { + "success": "選択したレビュー項目に関連する動画を削除しました。", + "error": "削除に失敗しました: {{error}}" + } + }, + "button": { + "export": "エクスポート", + "markAsReviewed": "レビュー済みにする", + "deleteNow": "今すぐ削除", + "markAsUnreviewed": "未レビューに戻す" + }, + "shareTimestamp": { + "label": "タイムスタンプを共有", + "title": "タイムスタンプを共有", + "description": "現在のプレーヤー位置をタイムスタンプ付き URL で共有するか、任意のタイムスタンプを指定できます。これは公開リンクではなく、Frigate とこのカメラへのアクセス権を持つユーザーのみアクセスできます。", + "custom": "カスタムタイムスタンプ", + "button": "タイムスタンプ URL を共有", + "shareTitle": "Frigate レビュータイムスタンプ: {{camera}}" + } + }, + "imagePicker": { + "selectImage": "追跡オブジェクトのサムネイルを選択", + "search": { + "placeholder": "ラベルまたはサブラベルで検索…" + }, + "noImages": "このカメラのサムネイルは見つかりません", + "unknownLabel": "保存済みトリガー画像" + } +} diff --git a/web/public/locales/ja/components/filter.json b/web/public/locales/ja/components/filter.json new file mode 100644 index 0000000..2bcd21b --- /dev/null +++ b/web/public/locales/ja/components/filter.json @@ -0,0 +1,140 @@ +{ + "labels": { + "label": "ラベル", + "all": { + "title": "すべてのラベル", + "short": "ラベル" + }, + "count_one": "{{count}} ラベル", + "count_other": "{{count}} ラベル" + }, + "filter": "フィルター", + "classes": { + "label": "クラス", + "all": { + "title": "すべてのクラス" + }, + "count_one": "{{count}} クラス", + "count_other": "{{count}} クラス" + }, + "zones": { + "label": "ゾーン", + "all": { + "title": "すべてのゾーン", + "short": "ゾーン" + } + }, + "dates": { + "selectPreset": "プリセットを選択…", + "all": { + "title": "すべての日付", + "short": "日付" + } + }, + "more": "その他のフィルター", + "reset": { + "label": "フィルターを既定値にリセット" + }, + "timeRange": "期間", + "subLabels": { + "label": "サブラベル", + "all": "すべてのサブラベル" + }, + "score": "スコア", + "estimatedSpeed": "推定速度({{unit}})", + "features": { + "label": "機能", + "hasSnapshot": "スナップショットあり", + "hasVideoClip": "ビデオクリップあり", + "submittedToFrigatePlus": { + "label": "Frigate+ に送信済み", + "tips": "まずスナップショットのある追跡オブジェクトでフィルターしてください。

    スナップショットのない追跡オブジェクトは Frigate+ に送信できません。" + } + }, + "sort": { + "label": "並び替え", + "dateAsc": "日付(昇順)", + "dateDesc": "日付(降順)", + "scoreAsc": "オブジェクトスコア(昇順)", + "scoreDesc": "オブジェクトスコア(降順)", + "speedAsc": "推定速度(昇順)", + "speedDesc": "推定速度(降順)", + "relevance": "関連度" + }, + "cameras": { + "label": "カメラフィルター", + "all": { + "title": "全カメラ", + "short": "カメラ" + } + }, + "review": { + "showReviewed": "レビュー済みを表示" + }, + "motion": { + "showMotionOnly": "モーションのみ表示" + }, + "explore": { + "settings": { + "title": "設定", + "defaultView": { + "title": "既定の表示", + "desc": "フィルター未選択時、ラベルごとの最新追跡オブジェクトの概要を表示するか、未フィルタのグリッドを表示するかを選びます。", + "summary": "概要", + "unfilteredGrid": "未フィルタグリッド" + }, + "gridColumns": { + "title": "グリッド列数", + "desc": "グリッド表示の列数を選択します。" + }, + "searchSource": { + "label": "検索対象", + "desc": "追跡オブジェクトのサムネイル画像と説明文のどちらを検索するかを選択します。", + "options": { + "thumbnailImage": "サムネイル画像", + "description": "説明" + } + } + }, + "date": { + "selectDateBy": { + "label": "フィルターする日付を選択" + } + } + }, + "logSettings": { + "label": "ログレベルでフィルター", + "filterBySeverity": "重大度でログをフィルター", + "loading": { + "title": "読み込み中", + "desc": "ログペインが最下部にあると、新しいログが追加され次第自動でストリーミング表示されます。" + }, + "disableLogStreaming": "ログのストリーミングを無効化", + "allLogs": "すべてのログ" + }, + "trackedObjectDelete": { + "title": "削除の確認", + "desc": "これら {{objectLength}} 件の追跡オブジェクトを削除すると、スナップショット、保存された埋め込み、関連するオブジェクトのライフサイクル項目が削除されます。履歴ビューの録画映像は削除されません

    続行してもよろしいですか?

    今後このダイアログを表示しない場合は Shift キーを押しながら操作してください。", + "toast": { + "success": "追跡オブジェクトを削除しました。", + "error": "追跡オブジェクトの削除に失敗しました: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "ゾーンマスクでフィルター" + }, + "recognizedLicensePlates": { + "title": "認識されたナンバープレート", + "loadFailed": "認識済みナンバープレートの読み込みに失敗しました。", + "loading": "認識済みナンバープレートを読み込み中…", + "placeholder": "ナンバープレートを入力して検索…", + "noLicensePlatesFound": "ナンバープレートが見つかりません。", + "selectPlatesFromList": "リストから1件以上選択してください。", + "selectAll": "すべて選択", + "clearAll": "すべてクリア" + }, + "attributes": { + "label": "分類属性", + "all": "すべての属性" + } +} diff --git a/web/public/locales/ja/components/icons.json b/web/public/locales/ja/components/icons.json new file mode 100644 index 0000000..2307ca4 --- /dev/null +++ b/web/public/locales/ja/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "アイコンを検索…" + }, + "selectIcon": "アイコンを選択" + } +} diff --git a/web/public/locales/ja/components/input.json b/web/public/locales/ja/components/input.json new file mode 100644 index 0000000..2272574 --- /dev/null +++ b/web/public/locales/ja/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "動画をダウンロード", + "toast": { + "success": "レビュー項目の動画のダウンロードを開始しました。" + } + } + } +} diff --git a/web/public/locales/ja/components/player.json b/web/public/locales/ja/components/player.json new file mode 100644 index 0000000..692cde4 --- /dev/null +++ b/web/public/locales/ja/components/player.json @@ -0,0 +1,53 @@ +{ + "noPreviewFound": "プレビューが見つかりません", + "noRecordingsFoundForThisTime": "この時間の録画は見つかりません", + "noPreviewFoundFor": "{{cameraName}} のプレビューが見つかりません", + "streamOffline": { + "title": "ストリームオフライン", + "desc": "{{cameraName}} の detect ストリームでフレームが受信されていません。エラーログを確認してください" + }, + "submitFrigatePlus": { + "title": "このフレームを Frigate+ に送信しますか?", + "submit": "送信", + "previewError": "スナップショットのプレビューを読み込めませんでした。現在、この録画は利用できない可能性があります。" + }, + "livePlayerRequiredIOSVersion": "このライブストリームタイプには iOS 17.1 以上が必要です。", + "cameraDisabled": "カメラは無効です", + "stats": { + "streamType": { + "title": "ストリームタイプ:", + "short": "タイプ" + }, + "bandwidth": { + "title": "帯域:", + "short": "帯域幅" + }, + "latency": { + "title": "遅延:", + "value": "{{seconds}} 秒", + "short": { + "title": "遅延", + "value": "{{seconds}} 秒" + } + }, + "totalFrames": "総フレーム:", + "droppedFrames": { + "title": "ドロップしたフレーム:", + "short": { + "title": "ドロップ", + "value": "{{droppedFrames}} フレーム" + } + }, + "decodedFrames": "デコードしたフレーム:", + "droppedFrameRate": "ドロップしたフレームレート:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "フレームを Frigate+ に送信しました" + }, + "error": { + "submitFrigatePlusFailed": "フレームの Frigate+ への送信に失敗しました" + } + }, + "cameraOff": "カメラはオフです" +} diff --git a/web/public/locales/ja/config/cameras.json b/web/public/locales/ja/config/cameras.json new file mode 100644 index 0000000..aa6ca74 --- /dev/null +++ b/web/public/locales/ja/config/cameras.json @@ -0,0 +1,953 @@ +{ + "label": "カメラ設定", + "name": { + "label": "カメラ名", + "description": "カメラ名は必須です" + }, + "enabled": { + "label": "有効", + "description": "有効" + }, + "audio": { + "label": "音声検知", + "enabled": { + "label": "音声検知を有効化", + "description": "このカメラの音声イベント検知を有効または無効にします。" + }, + "min_volume": { + "label": "最小ボリューム", + "description": "音声検知を実行するために必要な最小RMS音量閾値。値を小さくすると感度が高くなります(例:200=高、500=中、1000=低)。" + }, + "filters": { + "label": "音声フィルタ", + "description": "誤検知を減らすために使用される信頼度閾値などのフィルタ設定(オーディオタイプごと)。", + "threshold": { + "label": "音声の最低信頼度", + "description": "音声イベントとしてカウントするために必要な最低信頼度しきい値。" + } + }, + "description": "このカメラの音声ベースのイベント検知設定。", + "max_not_heard": { + "label": "タイムアウト終了", + "description": "音声検知が終了するまでの残り秒数(設定されたオーディオタイプを除く)。" + }, + "listen": { + "label": "リスニングタイプ", + "description": "検知対象の音声イベントの種類一覧(例:吠え声、火災報知器、悲鳴、会話、叫び声)。" + }, + "enabled_in_config": { + "label": "元の音声状態", + "description": "静的設定ファイルで、音声検知が当初有効にされていたかどうかを示します。" + }, + "num_threads": { + "label": "検知スレッド", + "description": "音声検知処理に使用するスレッド数。" + } + }, + "friendly_name": { + "label": "表示名", + "description": "Frigate UIで使用されるカメラの表示名" + }, + "audio_transcription": { + "label": "音声文字起こし", + "description": "イベントやリアルタイム字幕に使用される、ライブ音声およびスピーチ音声の文字起こし設定。", + "enabled": { + "label": "音声文字起こしを有効にする", + "description": "手動でトリガーされる音声イベントの文字起こしを有効または無効にします。" + }, + "enabled_in_config": { + "label": "元の文字起こし状態" + }, + "live_enabled": { + "label": "ライブ文字起こし", + "description": "音声を受信した時点で、リアルタイム文字起こしを有効にします。" + } + }, + "birdseye": { + "label": "バードアイ", + "description": "複数のカメラ映像を1つのレイアウトに合成する「バードアイ」合成ビューの設定。", + "enabled": { + "label": "バードアイを有効にする", + "description": "バードアイビュー機能を有効または無効にします。" + }, + "mode": { + "label": "トラッキングモード", + "description": "バードアイにカメラを含めるモード:「オブジェクト」「モーション」または「連続」。" + }, + "order": { + "label": "位置", + "description": "バードアイレイアウトにおけるカメラの並び順を決定する数値。" + } + }, + "detect": { + "label": "物体検知", + "description": "物体検知の実行やトラッカーの初期化に使用される、検知や検知ロールの設定。", + "enabled": { + "label": "物体検知を有効にする", + "description": "このカメラの物体検知機能を有効または無効にします。" + }, + "height": { + "label": "高さを検知", + "description": "検知ストリームに使用するフレーム高さ(ピクセル)。ネイティブストリーム解像度を使用する場合は、空欄のままにしてください。" + }, + "width": { + "label": "幅を検知", + "description": "検知ストリームで使用するフレーム幅 (ピクセル)。空欄でストリームのネイティブ解像度を使用。" + }, + "fps": { + "label": "検知 FPS", + "description": "検知を実行する目標 FPS。低くするほど CPU 使用率が下がります(推奨値は 5、極めて高速な物体を追跡する場合のみ最大 10 まで上げてください)。" + }, + "min_initialized": { + "label": "最小初期化フレーム数", + "description": "追跡オブジェクトを生成するために必要な連続検知ヒット数。値を大きくすると誤初期化が減ります。デフォルトは fps の半分。" + }, + "max_disappeared": { + "label": "最大消失フレーム数", + "description": "追跡オブジェクトが消失したと判断するまでの未検知フレーム数。" + }, + "stationary": { + "label": "静止オブジェクト設定", + "description": "一定時間静止しているオブジェクトを検知・管理するための設定。", + "interval": { + "label": "静止チェック間隔", + "description": "静止オブジェクトを確認するための検知を、何フレームおきに実行するか。" + }, + "threshold": { + "label": "静止しきい値", + "description": "オブジェクトを静止状態とみなすために必要な位置変化のないフレーム数。" + }, + "max_frames": { + "label": "最大追跡フレーム", + "description": "静止オブジェクトを破棄するまでの追跡フレーム数の上限。", + "default": { + "label": "デフォルト最大フレーム", + "description": "静止オブジェクトの追跡を停止するまでのデフォルト最大フレーム数。" + }, + "objects": { + "label": "オブジェクト別最大フレーム", + "description": "静止オブジェクト追跡の最大フレーム数をオブジェクトごとに上書きします。" + } + }, + "classifier": { + "label": "ビジュアル分類器を有効化", + "description": "バウンディングボックスが揺らいでも真に静止しているオブジェクトを検知するため、ビジュアル分類器を使用します。" + } + }, + "annotation_offset": { + "label": "注釈オフセット", + "description": "タイムライン上のバウンディングボックスを録画と揃えるため、検知注釈を時間方向にずらすミリ秒数。正負どちらも指定可能。" + } + }, + "mqtt": { + "label": "MQTT", + "description": "MQTT 画像配信の設定。", + "enabled": { + "label": "画像送信", + "description": "このカメラのオブジェクト画像スナップショットを MQTT トピックに配信する機能を有効にします。" + }, + "timestamp": { + "label": "タイムスタンプを追加", + "description": "MQTT に配信する画像にタイムスタンプを重ねて表示します。" + }, + "bounding_box": { + "label": "バウンディングボックスを追加", + "description": "MQTT に配信する画像にバウンディングボックスを描画します。" + }, + "crop": { + "label": "画像を切り抜き", + "description": "MQTT に配信する画像を検知オブジェクトのバウンディングボックスで切り抜きます。" + }, + "height": { + "label": "画像の高さ", + "description": "MQTT 配信時に画像をリサイズする高さ (ピクセル)。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "MQTT 画像を配信するためにオブジェクトが進入する必要があるゾーン。" + }, + "quality": { + "label": "JPEG 品質", + "description": "MQTT に配信する画像の JPEG 品質 (0-100)。" + } + }, + "notifications": { + "label": "通知", + "enabled": { + "label": "通知を有効化", + "description": "このカメラの通知を有効または無効にします。" + }, + "email": { + "label": "通知メールアドレス", + "description": "プッシュ通知用、または特定の通知プロバイダで必要となるメールアドレス。" + }, + "cooldown": { + "label": "クールダウン期間", + "description": "受信者への通知連投を避けるための通知間隔(秒)。" + }, + "enabled_in_config": { + "label": "元の通知状態", + "description": "元の静的設定で通知が有効化されていたかを示します。" + }, + "description": "このカメラの通知を有効化・制御する設定。" + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg の設定。バイナリパス、引数、ハードウェアアクセラレーション、ロール別の出力引数を含みます。", + "path": { + "label": "FFmpeg パス", + "description": "使用する FFmpeg バイナリのパス、またはバージョンエイリアス(「5.0」または「7.0」)。" + }, + "global_args": { + "label": "FFmpeg グローバル引数", + "description": "FFmpeg プロセスに渡されるグローバル引数。" + }, + "hwaccel_args": { + "label": "ハードウェアアクセラレーション引数", + "description": "FFmpeg のハードウェアアクセラレーション引数。プロバイダ固有のプリセットの使用を推奨。" + }, + "input_args": { + "label": "入力引数", + "description": "FFmpeg の入力ストリームに適用される引数。" + }, + "output_args": { + "label": "出力引数", + "description": "detect や record など、FFmpeg のロール別に使用されるデフォルト出力引数。", + "detect": { + "label": "検知ロールの出力引数", + "description": "detect ロールのストリームに使用されるデフォルト出力引数。" + }, + "record": { + "label": "録画ロールの出力引数", + "description": "record ロールのストリームに使用されるデフォルト出力引数。" + } + }, + "retry_interval": { + "label": "FFmpeg 再試行間隔", + "description": "カメラストリームの失敗後、再接続を試みるまでの待機秒数。デフォルトは 10 秒。" + }, + "apple_compatibility": { + "label": "Apple 互換性", + "description": "H.265 録画時に Apple プレーヤーとの互換性向上のため HEVC タグ付けを有効化します。" + }, + "gpu": { + "label": "GPU インデックス", + "description": "ハードウェアアクセラレーションで使用するデフォルト GPU インデックス。" + }, + "inputs": { + "label": "カメラ入力", + "description": "このカメラの入力ストリーム定義(パスとロール)のリスト。", + "path": { + "label": "入力パス", + "description": "カメラ入力ストリームの URL またはパス。" + }, + "roles": { + "label": "入力ロール", + "description": "この入力ストリームのロール。" + }, + "global_args": { + "label": "FFmpeg グローバル引数", + "description": "この入力ストリームに対する FFmpeg グローバル引数。" + }, + "hwaccel_args": { + "label": "ハードウェアアクセラレーション引数", + "description": "この入力ストリームのハードウェアアクセラレーション引数。" + }, + "input_args": { + "label": "入力引数", + "description": "このストリーム固有の入力引数。" + } + } + }, + "live": { + "label": "ライブ再生", + "streams": { + "label": "ライブストリーム名", + "description": "設定済みのストリーム名と、ライブ再生で使用する restream/go2rtc 名のマッピング。" + }, + "height": { + "label": "ライブの高さ", + "description": "Web UI で jsmpeg ライブストリームを描画する高さ (ピクセル)。検知ストリーム高さ以下である必要があります。" + }, + "quality": { + "label": "ライブ品質", + "description": "jsmpeg ストリームのエンコード品質 (1 が最高、31 が最低)。" + }, + "description": "ライブストリームの選択・解像度・品質を Web UI から制御する設定。" + }, + "motion": { + "label": "モーション検知", + "enabled": { + "label": "モーション検知を有効化", + "description": "このカメラのモーション検知を有効または無効にします。" + }, + "threshold": { + "label": "モーションしきい値", + "description": "モーション検出器が使用するピクセル差分しきい値。値を大きくすると感度が下がります (範囲 1-255)。" + }, + "lightning_threshold": { + "label": "雷検知しきい値", + "description": "短時間の照明スパイクを検知して無視するしきい値(値が小さいほど感度が高く、0.3 〜 1.0 が目安)。これはモーション検知を完全に止めるものではなく、しきい値超過後に検出器が追加フレームの解析を停止するだけです。モーションベースの録画はこれらのイベント中も作成されます。" + }, + "skip_motion_threshold": { + "label": "モーションスキップしきい値", + "description": "0.0 〜 1.0 の値を指定し、1 フレームでそれ以上の割合が変化した場合、検出器はモーションボックスを返さず即座に再キャリブレーションします。雷や嵐などの誤検知を減らし CPU を節約できますが、PTZ カメラのオート追跡などの実イベントを取りこぼす可能性があります。数 MB の録画を捨てるか、数本の短いクリップを確認するかのトレードオフです。無効化するには未設定 (None) のままにします。" + }, + "improve_contrast": { + "label": "コントラスト強調", + "description": "モーション解析前にフレームのコントラストを強調して検知を補助します。" + }, + "contour_area": { + "label": "輪郭面積", + "description": "モーション輪郭としてカウントするために必要な最小ピクセル数。" + }, + "delta_alpha": { + "label": "デルタアルファ", + "description": "モーション計算のフレーム差分で使用されるアルファブレンディング係数。" + }, + "frame_alpha": { + "label": "フレームアルファ", + "description": "モーション前処理でフレームをブレンドする際に使用するアルファ値。" + }, + "frame_height": { + "label": "フレーム高さ", + "description": "モーション計算時にフレームをスケーリングする高さ (ピクセル)。" + }, + "mask": { + "label": "マスク座標", + "description": "領域を含める/除外するモーションマスクポリゴンを定義する x,y 座標の順序付きリスト。" + }, + "mqtt_off_delay": { + "label": "MQTT オフ遅延", + "description": "最後のモーション検知後、MQTT で「off」状態を発行するまでの待機秒数。" + }, + "enabled_in_config": { + "label": "元のモーション状態", + "description": "元の静的設定でモーション検知が有効化されていたかを示します。" + }, + "raw_mask": { + "label": "Raw マスク" + }, + "description": "このカメラのモーション検知のデフォルト設定。" + }, + "objects": { + "label": "オブジェクト", + "description": "追跡するラベルとオブジェクト別フィルタを含む、オブジェクト追跡のデフォルト設定。", + "track": { + "label": "追跡するオブジェクト", + "description": "このカメラで追跡するオブジェクトラベルのリスト。" + }, + "filters": { + "label": "オブジェクトフィルタ", + "description": "誤検知を減らすために検知オブジェクトに適用するフィルタ(面積、比率、信頼度)。", + "min_area": { + "label": "最小オブジェクト面積", + "description": "このオブジェクト種別に必要なバウンディングボックスの最小面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。" + }, + "max_area": { + "label": "最大オブジェクト面積", + "description": "このオブジェクト種別に許容されるバウンディングボックスの最大面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。" + }, + "min_ratio": { + "label": "最小アスペクト比", + "description": "対象となるバウンディングボックスに必要な最小の幅/高さ比。" + }, + "max_ratio": { + "label": "最大アスペクト比", + "description": "対象となるバウンディングボックスに許容される最大の幅/高さ比。" + }, + "threshold": { + "label": "信頼度しきい値", + "description": "オブジェクトを真陽性とみなすために必要な平均検知信頼度。" + }, + "min_score": { + "label": "最低信頼度", + "description": "オブジェクトをカウントするために必要な単一フレームでの最低検知信頼度。" + }, + "mask": { + "label": "フィルタマスク", + "description": "フレーム内でこのフィルタが適用される範囲を定義するポリゴン座標。" + }, + "raw_mask": { + "label": "Raw マスク" + } + }, + "mask": { + "label": "オブジェクトマスク", + "description": "指定領域でオブジェクト検知を行わないようにするためのマスクポリゴン。" + }, + "raw_mask": { + "label": "Raw マスク" + }, + "genai": { + "label": "GenAI オブジェクト設定", + "description": "追跡オブジェクトの説明生成や、生成 AI へのフレーム送信に関する GenAI オプション。", + "enabled": { + "label": "GenAI を有効化", + "description": "追跡オブジェクトの説明を GenAI で生成する機能を既定で有効にします。" + }, + "use_snapshot": { + "label": "スナップショットを使用", + "description": "GenAI 説明生成にサムネイルではなくオブジェクトスナップショットを使用します。" + }, + "prompt": { + "label": "キャプションプロンプト", + "description": "GenAI で説明を生成する際に使用するデフォルトのプロンプトテンプレート。" + }, + "object_prompts": { + "label": "オブジェクト別プロンプト", + "description": "特定のラベルに対する GenAI 出力をカスタマイズするためのオブジェクト別プロンプト。" + }, + "objects": { + "label": "GenAI 対象オブジェクト", + "description": "GenAI に既定で送信するオブジェクトラベルのリスト。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "GenAI 説明生成の対象となるためにオブジェクトが進入する必要があるゾーン。" + }, + "debug_save_thumbnails": { + "label": "サムネイルを保存", + "description": "デバッグや確認のため、GenAI に送信したサムネイルを保存します。" + }, + "send_triggers": { + "label": "GenAI 送信トリガー", + "description": "フレームを GenAI に送るタイミング(終了時、更新後など)を定義します。", + "tracked_object_end": { + "label": "終了時に送信", + "description": "追跡オブジェクトが終了した時点で GenAI にリクエストを送信します。" + }, + "after_significant_updates": { + "label": "早期 GenAI トリガー", + "description": "追跡オブジェクトに対して指定回数の重要な更新があった後、GenAI にリクエストを送信します。" + } + }, + "enabled_in_config": { + "label": "元の GenAI 状態", + "description": "元の静的設定で GenAI が有効化されていたかを示します。" + } + } + }, + "record": { + "label": "録画", + "enabled": { + "label": "録画を有効化", + "description": "このカメラの録画を有効または無効にします。" + }, + "expire_interval": { + "label": "録画クリーンアップ間隔", + "description": "期限切れ録画セグメントを削除するクリーンアップを実行する間隔 (分)。" + }, + "continuous": { + "label": "常時保持", + "description": "追跡オブジェクトやモーションに関係なく録画を保持する日数。アラートと検知の録画のみを保持したい場合は 0 を指定します。", + "days": { + "label": "保持日数", + "description": "録画を保持する日数。" + } + }, + "motion": { + "label": "モーション録画保持", + "description": "追跡オブジェクトに関係なくモーションでトリガーされた録画を保持する日数。アラートと検知の録画のみを保持したい場合は 0 を指定します。", + "days": { + "label": "保持日数", + "description": "録画を保持する日数。" + } + }, + "detections": { + "label": "検知録画保持", + "description": "検知イベントの録画保持設定。前後の撮影時間を含みます。", + "pre_capture": { + "label": "イベント前秒数", + "description": "検知イベントの前に録画に含める秒数。" + }, + "post_capture": { + "label": "イベント後秒数", + "description": "検知イベントの後に録画に含める秒数。" + }, + "retain": { + "label": "イベント保持", + "description": "検知イベントの録画保持設定。", + "days": { + "label": "保持日数", + "description": "検知イベント録画を保持する日数。" + }, + "mode": { + "label": "保持モード", + "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。" + } + } + }, + "alerts": { + "label": "アラート録画保持", + "description": "アラートイベントの録画保持設定。前後の撮影時間を含みます。", + "pre_capture": { + "label": "イベント前秒数", + "description": "アラートイベントの前に録画に含める秒数。" + }, + "post_capture": { + "label": "イベント後秒数", + "description": "アラートイベントの後に録画に含める秒数。" + }, + "retain": { + "label": "イベント保持", + "description": "アラートイベントの録画保持設定。", + "days": { + "label": "保持日数", + "description": "アラートイベント録画を保持する日数。" + }, + "mode": { + "label": "保持モード", + "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。" + } + } + }, + "export": { + "label": "エクスポート設定", + "description": "タイムラプスやハードウェアアクセラレーションなど、録画をエクスポートする際に使用する設定。", + "hwaccel_args": { + "label": "エクスポート用 hwaccel 引数", + "description": "エクスポート/トランスコード処理で使用するハードウェアアクセラレーション引数。" + }, + "max_concurrent": { + "label": "同時エクスポート数の上限", + "description": "同時に処理するエクスポートジョブの最大数。" + } + }, + "preview": { + "label": "プレビュー設定", + "description": "UI に表示される録画プレビューの品質を制御する設定。", + "quality": { + "label": "プレビュー品質", + "description": "プレビュー品質レベル (very_low, low, medium, high, very_high)。" + } + }, + "enabled_in_config": { + "label": "元の録画状態", + "description": "元の静的設定で録画が有効化されていたかを示します。" + }, + "description": "このカメラの録画と保持に関する設定。" + }, + "review": { + "label": "レビュー", + "alerts": { + "label": "アラート設定", + "description": "どの追跡オブジェクトがアラートを生成するか、およびアラートの保持方法に関する設定。", + "enabled": { + "label": "アラートを有効化", + "description": "このカメラのアラート生成を有効または無効にします。" + }, + "labels": { + "label": "アラートラベル", + "description": "アラート対象となるオブジェクトラベルのリスト(例: car, person)。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "アラートとみなされるためにオブジェクトが進入する必要があるゾーン。空欄ですべてのゾーンを許可。" + }, + "enabled_in_config": { + "label": "元のアラート状態", + "description": "元の静的設定でアラートが有効化されていたかを記録します。" + }, + "cutoff_time": { + "label": "アラート打ち切り時間", + "description": "アラートを引き起こすアクティビティが途絶えてからアラートを打ち切るまでの待機秒数。" + } + }, + "detections": { + "label": "検知設定", + "description": "どの追跡オブジェクトが(アラートではない)検知を生成するか、および検知の保持方法に関する設定。", + "enabled": { + "label": "検知を有効化", + "description": "このカメラの検知イベントを有効または無効にします。" + }, + "labels": { + "label": "検知ラベル", + "description": "検知イベントの対象となるオブジェクトラベルのリスト。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "検知とみなされるためにオブジェクトが進入する必要があるゾーン。空欄ですべてのゾーンを許可。" + }, + "cutoff_time": { + "label": "検知打ち切り時間", + "description": "検知を引き起こすアクティビティが途絶えてから検知を打ち切るまでの待機秒数。" + }, + "enabled_in_config": { + "label": "元の検知状態", + "description": "元の静的設定で検知が有効化されていたかを記録します。" + } + }, + "genai": { + "label": "GenAI 設定", + "description": "レビュー項目の説明やサマリーを生成 AI で作成する機能の制御。", + "enabled": { + "label": "GenAI 説明を有効化", + "description": "レビュー項目の GenAI 生成説明やサマリーを有効または無効にします。" + }, + "alerts": { + "label": "アラートに GenAI を使用", + "description": "アラート項目の説明を GenAI で生成します。" + }, + "detections": { + "label": "検知に GenAI を使用", + "description": "検知項目の説明を GenAI で生成します。" + }, + "image_source": { + "label": "レビュー画像ソース", + "description": "GenAI に送信する画像のソース(「preview」または「recordings」)。「recordings」は高品質ですがトークン消費が増えます。" + }, + "additional_concerns": { + "label": "追加の懸念事項", + "description": "このカメラのアクティビティ評価時に GenAI に考慮させる追加の懸念や注意事項のリスト。" + }, + "debug_save_thumbnails": { + "label": "サムネイルを保存", + "description": "デバッグや確認のため、GenAI プロバイダに送信したサムネイルを保存します。" + }, + "enabled_in_config": { + "label": "元の GenAI 状態", + "description": "元の静的設定で GenAI レビューが有効化されていたかを記録します。" + }, + "preferred_language": { + "label": "希望言語", + "description": "GenAI プロバイダに生成応答で要求する言語。" + }, + "activity_context_prompt": { + "label": "活動コンテキストプロンプト", + "description": "GenAI サマリーの文脈として、何が不審な活動で何がそうでないかを記述するカスタムプロンプト。" + } + }, + "description": "このカメラの UI 表示・保存で使用するアラート、検知、GenAI レビューサマリーの設定。" + }, + "snapshots": { + "label": "スナップショット", + "enabled": { + "label": "スナップショットを有効化", + "description": "このカメラのスナップショット保存を有効または無効にします。" + }, + "timestamp": { + "label": "タイムスタンプ重ね合わせ", + "description": "API スナップショットにタイムスタンプを重ねて表示します。" + }, + "bounding_box": { + "label": "バウンディングボックス重ね合わせ", + "description": "API スナップショットに追跡オブジェクトのバウンディングボックスを描画します。" + }, + "crop": { + "label": "スナップショットを切り抜き", + "description": "API スナップショットを検知オブジェクトのバウンディングボックスで切り抜きます。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "スナップショットを保存するためにオブジェクトが進入する必要があるゾーン。" + }, + "height": { + "label": "スナップショット高さ", + "description": "API スナップショットをリサイズする高さ (ピクセル)。空欄で元のサイズを維持。" + }, + "retain": { + "label": "スナップショット保持", + "description": "デフォルト保持日数とオブジェクト別上書きを含む、スナップショット保持設定。", + "default": { + "label": "デフォルト保持期間", + "description": "スナップショットを保持するデフォルト日数。" + }, + "mode": { + "label": "保持モード", + "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。" + }, + "objects": { + "label": "オブジェクト別保持", + "description": "オブジェクトごとのスナップショット保持日数の上書き。" + } + }, + "quality": { + "label": "スナップショット品質", + "description": "保存するスナップショットのエンコード品質 (0-100)。" + }, + "description": "このカメラの追跡オブジェクトに対する API 生成スナップショットの設定。" + }, + "timestamp_style": { + "label": "タイムスタンプスタイル", + "position": { + "label": "タイムスタンプ位置", + "description": "画像内のタイムスタンプ位置 (tl/tr/bl/br)。" + }, + "format": { + "label": "タイムスタンプ書式", + "description": "タイムスタンプに使用する日時書式文字列 (Python datetime 書式コード)。" + }, + "color": { + "label": "タイムスタンプ色", + "description": "タイムスタンプ文字色の RGB 値 (各 0-255)。", + "red": { + "label": "赤", + "description": "タイムスタンプ色の赤成分 (0-255)。" + }, + "green": { + "label": "緑", + "description": "タイムスタンプ色の緑成分 (0-255)。" + }, + "blue": { + "label": "青", + "description": "タイムスタンプ色の青成分 (0-255)。" + } + }, + "thickness": { + "label": "タイムスタンプ太さ", + "description": "タイムスタンプ文字の線の太さ。" + }, + "effect": { + "label": "タイムスタンプエフェクト", + "description": "タイムスタンプ文字の視覚効果 (none, solid, shadow)。" + }, + "description": "スナップショットおよびデバッグビューに適用されるタイムスタンプの表示設定。" + }, + "semantic_search": { + "label": "セマンティック検索", + "triggers": { + "label": "トリガー", + "description": "カメラ別のセマンティック検索トリガーの動作と一致条件。", + "friendly_name": { + "label": "表示名", + "description": "このトリガーの UI 表示用の任意の名前。" + }, + "enabled": { + "label": "このトリガーを有効化", + "description": "このセマンティック検索トリガーを有効または無効にします。" + }, + "type": { + "label": "トリガー種別", + "description": "トリガー種別: 「thumbnail」(画像に対する一致)または「description」(テキストに対する一致)。" + }, + "data": { + "label": "トリガー内容", + "description": "追跡オブジェクトと照合するテキストフレーズまたはサムネイル ID。" + }, + "threshold": { + "label": "トリガーしきい値", + "description": "このトリガーを発火させるために必要な最低類似度スコア (0-1)。" + }, + "actions": { + "label": "トリガーアクション", + "description": "トリガー一致時に実行するアクションのリスト (notification, sub_label, attribute)。" + } + }, + "description": "オブジェクト埋め込みを構築・クエリして類似項目を見つけるセマンティック検索の設定。" + }, + "face_recognition": { + "label": "顔認識", + "enabled": { + "label": "顔認識を有効化", + "description": "顔認識を有効または無効にします。" + }, + "min_area": { + "label": "顔の最小面積", + "description": "認識を試みるために必要な顔ボックスの最小面積 (ピクセル)。" + }, + "description": "このカメラの顔検知と顔認識の設定。" + }, + "lpr": { + "label": "ナンバープレート認識", + "description": "ナンバープレート認識の設定。検知しきい値、書式整形、既知ナンバーなどを含みます。", + "enabled": { + "label": "LPR を有効化", + "description": "このカメラで LPR を有効または無効にします。" + }, + "min_area": { + "label": "プレート最小面積", + "description": "認識を試みるために必要なプレート最小面積 (ピクセル)。" + }, + "enhancement": { + "label": "強調レベル", + "description": "OCR 前にプレート切り出し画像に適用する強調レベル (0-10)。値を大きくしても常に改善するとは限らず、5 を超えると夜間プレートでのみ有効な場合があるため注意が必要です。" + }, + "expire_time": { + "label": "失効秒数", + "description": "未検知ナンバーをトラッカーから失効させるまでの秒数(専用 LPR カメラのみ)。" + } + }, + "profiles": { + "label": "プロファイル", + "description": "実行時に有効化できる部分上書きを持つ、名前付きの設定プロファイル。" + }, + "onvif": { + "label": "ONVIF", + "description": "このカメラの ONVIF 接続および PTZ オート追跡の設定。", + "host": { + "label": "ONVIF ホスト", + "description": "このカメラの ONVIF サービスのホスト(オプションでスキーマも)。" + }, + "port": { + "label": "ONVIF ポート", + "description": "ONVIF サービスのポート番号。" + }, + "user": { + "label": "ONVIF ユーザー名", + "description": "ONVIF 認証用のユーザー名。ONVIF に admin ユーザーが必要なデバイスもあります。" + }, + "password": { + "label": "ONVIF パスワード", + "description": "ONVIF 認証用のパスワード。" + }, + "tls_insecure": { + "label": "TLS 検証を無効化", + "description": "ONVIF の TLS 検証をスキップし、ダイジェスト認証も無効化します(安全でないため、信頼できるネットワークでのみ使用)。" + }, + "profile": { + "label": "ONVIF プロファイル", + "description": "PTZ 制御に使用する ONVIF メディアプロファイル(トークンまたは名前で指定)。未設定の場合、有効な PTZ 設定を持つ最初のプロファイルが自動選択されます。" + }, + "autotracking": { + "label": "オートトラッキング", + "description": "PTZ カメラの動作で移動中のオブジェクトを自動追跡し、フレーム中央に保ちます。", + "enabled": { + "label": "オートトラッキングを有効化", + "description": "検知オブジェクトの PTZ オート追跡を有効または無効にします。" + }, + "calibrate_on_startup": { + "label": "起動時にキャリブレーション", + "description": "追跡精度を向上させるため、起動時に PTZ モーター速度を測定します。キャリブレーション後に movement_weights が設定に書き込まれます。" + }, + "zooming": { + "label": "ズームモード", + "description": "ズーム動作の制御: disabled (パン/チルトのみ)、absolute (互換性が最も高い)、relative (パン/チルト/ズーム同時)。" + }, + "zoom_factor": { + "label": "ズーム倍率", + "description": "追跡対象オブジェクトのズームレベルを制御します。値が小さいほど広い範囲を保ち、大きいほどズームインしますが追跡を失う可能性があります。0.1 〜 0.75 の範囲で指定。" + }, + "track": { + "label": "追跡対象オブジェクト", + "description": "オートトラッキングを発動させるオブジェクト種別のリスト。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "オートトラッキング開始前にオブジェクトが進入する必要があるゾーン。" + }, + "return_preset": { + "label": "復帰プリセット", + "description": "追跡終了後にカメラが戻る、ファームウェアに設定されている ONVIF プリセット名。" + }, + "timeout": { + "label": "復帰タイムアウト", + "description": "追跡を失ってからカメラをプリセット位置に戻すまでの待機秒数。" + }, + "movement_weights": { + "label": "動作重み", + "description": "カメラキャリブレーションによって自動生成される値。手動で変更しないでください。" + }, + "enabled_in_config": { + "label": "元のオート追跡状態", + "description": "オートトラッキングが設定で有効化されていたかを追跡する内部フィールド。" + } + }, + "ignore_time_mismatch": { + "label": "時刻差異を無視", + "description": "ONVIF 通信時に、カメラと Frigate サーバー間の時刻同期差異を無視します。" + } + }, + "best_image_timeout": { + "label": "ベストイメージタイムアウト", + "description": "最高信頼度スコアの画像を取得するまでの待機時間。" + }, + "type": { + "label": "カメラタイプ", + "description": "カメラタイプ" + }, + "ui": { + "label": "カメラ UI", + "description": "UI 内でのこのカメラの表示順と表示設定。順序はデフォルトダッシュボードに影響します。より細かい制御にはカメラグループを使用してください。", + "order": { + "label": "UI 表示順", + "description": "UI 内でのカメラの並び順に使用される数値(デフォルトダッシュボードとリスト)。値が大きいほど後ろに表示されます。" + }, + "dashboard": { + "label": "UI に表示", + "description": "このカメラを Frigate UI 全体に表示するかを切り替えます。無効化した場合、再表示するには設定ファイルを手動編集する必要があります。" + } + }, + "webui_url": { + "label": "カメラ URL", + "description": "システムページからカメラに直接アクセスするための URL" + }, + "zones": { + "label": "ゾーン", + "description": "ゾーンを使うとフレーム内の特定領域を定義でき、オブジェクトがその領域内にあるかを判定できます。", + "friendly_name": { + "label": "ゾーン名", + "description": "Frigate UI に表示されるユーザー向けのゾーン名。未設定の場合、ゾーン名の整形版が使用されます。" + }, + "enabled": { + "label": "有効", + "description": "このゾーンを有効または無効にします。無効化したゾーンは実行時に無視されます。" + }, + "enabled_in_config": { + "label": "ゾーンの元の状態を保持する。" + }, + "filters": { + "label": "ゾーンフィルタ", + "description": "このゾーン内のオブジェクトに適用するフィルタ。誤検知を減らすか、ゾーン内に存在するとみなすオブジェクトを制限するために使用します。", + "min_area": { + "label": "最小オブジェクト面積", + "description": "このオブジェクト種別に必要なバウンディングボックスの最小面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。" + }, + "max_area": { + "label": "最大オブジェクト面積", + "description": "このオブジェクト種別に許容されるバウンディングボックスの最大面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。" + }, + "min_ratio": { + "label": "最小アスペクト比", + "description": "対象となるバウンディングボックスに必要な最小の幅/高さ比。" + }, + "max_ratio": { + "label": "最大アスペクト比", + "description": "対象となるバウンディングボックスに許容される最大の幅/高さ比。" + }, + "threshold": { + "label": "信頼度しきい値", + "description": "オブジェクトを真陽性とみなすために必要な平均検知信頼度。" + }, + "min_score": { + "label": "最低信頼度", + "description": "オブジェクトをカウントするために必要な単一フレームでの最低検知信頼度。" + }, + "mask": { + "label": "フィルタマスク", + "description": "フレーム内でこのフィルタが適用される範囲を定義するポリゴン座標。" + }, + "raw_mask": { + "label": "Raw マスク" + } + }, + "coordinates": { + "label": "座標", + "description": "ゾーン領域を定義するポリゴン座標。カンマ区切り文字列または座標文字列のリスト。座標は相対値 (0-1) または絶対値 (旧形式) で指定。" + }, + "distances": { + "label": "実世界の距離", + "description": "ゾーン四辺形の各辺に対応する実世界の距離(任意)。速度や距離計算に使用します。設定する場合は必ず 4 つの値が必要です。" + }, + "inertia": { + "label": "慣性フレーム数", + "description": "オブジェクトをゾーン内に存在するとみなすために、連続して検知される必要があるフレーム数。一時的な検知を除外するのに役立ちます。" + }, + "loitering_time": { + "label": "うろつき秒数", + "description": "うろつきとみなされるためにオブジェクトがゾーン内にとどまる必要がある秒数。0 でうろつき検知を無効化。" + }, + "speed_threshold": { + "label": "最低速度", + "description": "ゾーン内に存在するとみなされるためにオブジェクトに必要な最低速度(distances 設定時は実世界単位)。速度ベースのゾーントリガーに使用。" + }, + "objects": { + "label": "トリガーオブジェクト", + "description": "このゾーンをトリガーできるオブジェクト種別のリスト(labelmap から)。文字列または文字列リストで指定。空ですべてのオブジェクトが対象。" + } + }, + "enabled_in_config": { + "label": "元のカメラ状態", + "description": "カメラの元の状態を保持する。" + } +} diff --git a/web/public/locales/ja/config/global.json b/web/public/locales/ja/config/global.json new file mode 100644 index 0000000..3ccc593 --- /dev/null +++ b/web/public/locales/ja/config/global.json @@ -0,0 +1,1635 @@ +{ + "safe_mode": { + "label": "セーフモード", + "description": "有効にすると、トラブルシューティングのため機能を制限したセーフモードでFrigateを起動します。" + }, + "environment_vars": { + "label": "環境変数", + "description": "Home Assistant OS の Frigate プロセスに設定する環境変数のキー/値ペア。HAOS をご利用でない場合は、代わりに Docker の環境変数設定を使用してください。" + }, + "audio": { + "label": "音声検知", + "enabled": { + "label": "音声検知を有効化", + "description": "全カメラの音声イベント検知を有効または無効にします(カメラ別に上書き可能)。" + }, + "min_volume": { + "label": "最小ボリューム", + "description": "音声検知を実行するために必要な最小RMS音量閾値。値を小さくすると感度が高くなります(例:200=高、500=中、1000=低)。" + }, + "filters": { + "label": "音声フィルタ", + "description": "誤検知を減らすために使用される信頼度閾値などのフィルタ設定(オーディオタイプごと)。", + "threshold": { + "label": "音声の最低信頼度", + "description": "音声イベントとしてカウントするために必要な最低信頼度しきい値。" + } + }, + "max_not_heard": { + "label": "タイムアウト終了", + "description": "音声検知が終了するまでの残り秒数(設定されたオーディオタイプを除く)。" + }, + "listen": { + "label": "リスニングタイプ", + "description": "検知対象の音声イベントの種類一覧(例:吠え声、火災報知器、悲鳴、会話、叫び声)。" + }, + "enabled_in_config": { + "label": "元の音声状態", + "description": "静的設定ファイルで、音声検知が当初有効にされていたかどうかを示します。" + }, + "num_threads": { + "label": "検知スレッド", + "description": "音声検知処理に使用するスレッド数。" + }, + "description": "全カメラの音声ベースのイベント検知設定(カメラ別に上書き可能)。" + }, + "logger": { + "default": { + "label": "ログレベル", + "description": "デフォルトのグローバルログの詳細度 (debug, info, warning, error)。" + }, + "logs": { + "label": "プロセス毎のログレベル", + "description": "コンポーネントごとのログレベルの上書きにより、特定のモジュールのログ詳細度を増減できます。" + }, + "label": "ロギング", + "description": "デフォルトのログ詳細度とコンポーネントごとのログレベルの上書きを制御します。" + }, + "auth": { + "label": "認証", + "enabled": { + "label": "認証を有効化", + "description": "Frigate UI でネイティブ認証を有効にする。" + }, + "reset_admin_password": { + "label": "adminパスワードをリセット", + "description": "もし本当なら、起動時に管理者ユーザーのパスワードをリセットし、新しいパスワードをログに出力します。" + }, + "description": "認証およびセッション関連の設定(Cookieやレート制限オプションを含む)。", + "cookie_name": { + "label": "JWT Cookie名", + "description": "ネイティブ認証用のJWTトークンを保存するために使用されるCookie名。" + }, + "cookie_secure": { + "label": "Cookie のセキュリティフラグ", + "description": "認証Cookieにセキュアフラグを設定します。TLSを使用する場合はtrueにする必要があります。" + }, + "session_length": { + "label": "セッションの期間", + "description": "JWTベースのセッション継続時間(秒単位)。" + }, + "refresh_time": { + "label": "セッション更新ウィンドウ", + "description": "セッションの有効期限が切れるまで残り数秒になったら、セッションを元の期間に更新します。" + }, + "failed_login_rate_limit": { + "label": "ログイン失敗回数の上限", + "description": "ログイン失敗時の試行回数を制限するルールを設けることで、総当たり攻撃を軽減する。" + }, + "trusted_proxies": { + "label": "信頼できるプロキシ", + "description": "レート制限のためクライアントIPアドレスを特定する際に使用される、信頼できるプロキシIPのリスト。" + }, + "hash_iterations": { + "label": "ハッシュ反復処理", + "description": "ユーザーパスワードのハッシュ化に使用するPBKDF2-SHA256の反復回数。" + }, + "roles": { + "label": "ロールのマッピング", + "description": "ロールをカメラリストに割り当てます。リストが空の場合、そのロールのユーザーは全てのカメラにアクセスできます。" + }, + "admin_first_time_login": { + "label": "初回管理者フラグ", + "description": "この設定が「true」の場合、ログインページにヘルプリンクが表示され、管理者パスワードのリセット後にログインする方法がユーザーに案内されることがあります。 " + } + }, + "version": { + "label": "現在の設定バージョン", + "description": "移行やフォーマット変更の検知に役立つ、アクティブな設定の数値または文字列バージョン。" + }, + "audio_transcription": { + "label": "音声文字起こし", + "description": "イベントやリアルタイム字幕に使用される、ライブ音声およびスピーチ音声の文字起こし設定。", + "live_enabled": { + "label": "ライブ文字起こし", + "description": "音声を受信した時点で、リアルタイム文字起こしを有効にします。" + }, + "enabled": { + "label": "音声文字起こしを有効にする", + "description": "全カメラの自動音声文字起こしを有効または無効にします(カメラ別に上書き可能)。" + }, + "language": { + "label": "文字起こし言語", + "description": "文字起こし/翻訳に使用する言語コード(例: 英語は 'en')。対応言語コードは https://whisper-api.com/docs/languages/ を参照。" + }, + "device": { + "label": "文字起こしデバイス", + "description": "文字起こしモデルを実行するデバイスキー(CPU/GPU)。現在 GPU は NVIDIA CUDA のみサポート。" + }, + "model_size": { + "label": "モデルサイズ", + "description": "オフライン音声イベント文字起こしに使用するモデルサイズ。" + } + }, + "birdseye": { + "label": "バードアイ", + "description": "複数のカメラ映像を1つのレイアウトに合成する「バードアイ」合成ビューの設定。", + "enabled": { + "label": "バードアイを有効にする", + "description": "バードアイビュー機能を有効または無効にします。" + }, + "mode": { + "label": "トラッキングモード", + "description": "バードアイにカメラを含めるモード:「オブジェクト」「モーション」または「連続」。" + }, + "order": { + "label": "位置", + "description": "バードアイレイアウトにおけるカメラの並び順を決定する数値。" + }, + "restream": { + "label": "RTSP リストリーム", + "description": "バードアイ出力を RTSP フィードとしてリストリームします。有効化するとバードアイが常時動作します。" + }, + "width": { + "label": "幅", + "description": "合成されたバードアイフレームの出力幅(ピクセル)。" + }, + "height": { + "label": "高さ", + "description": "合成されたバードアイフレームの出力高さ(ピクセル)。" + }, + "quality": { + "label": "エンコード品質", + "description": "バードアイ mpeg1 フィードのエンコード品質(1 が最高、31 が最低)。" + }, + "inactivity_threshold": { + "label": "非アクティブしきい値", + "description": "この秒数だけ非アクティブが続くと、カメラはバードアイに表示されなくなります。" + }, + "layout": { + "label": "レイアウト", + "description": "バードアイ合成のレイアウトオプション。", + "scaling_factor": { + "label": "スケーリング係数", + "description": "レイアウト計算器が使用するスケーリング係数(範囲 1.0 〜 5.0)。" + }, + "max_cameras": { + "label": "最大カメラ数", + "description": "バードアイに同時表示するカメラの最大数。最新のカメラが表示されます。" + } + }, + "idle_heartbeat_fps": { + "label": "アイドル時ハートビート FPS", + "description": "アイドル時に最後の合成バードアイフレームを再送するフレームレート。0 で無効。" + } + }, + "database": { + "label": "データベース", + "description": "Frigateが追跡対象や録画メタデータを保存するために使用するSQLiteデータベースの設定。", + "path": { + "label": "データベースパス", + "description": "FrigateのSQLiteデータベースファイルが保存されるファイルシステムパス。" + } + }, + "detect": { + "label": "物体検知", + "description": "物体検知の実行やトラッカーの初期化に使用される、検知や検知ロールの設定。", + "enabled": { + "label": "物体検知を有効にする", + "description": "全カメラの物体検知を有効または無効にします(カメラ別に上書き可能)。" + }, + "height": { + "label": "高さを検知", + "description": "検知ストリームに使用するフレーム高さ(ピクセル)。ネイティブストリーム解像度を使用する場合は、空欄のままにしてください。" + }, + "width": { + "label": "幅を検知", + "description": "検知ストリームで使用するフレーム幅 (ピクセル)。空欄でストリームのネイティブ解像度を使用。" + }, + "fps": { + "label": "検知 FPS", + "description": "検知を実行する目標 FPS。低くするほど CPU 使用率が下がります(推奨値は 5、極めて高速な物体を追跡する場合のみ最大 10 まで上げてください)。" + }, + "min_initialized": { + "label": "最小初期化フレーム数", + "description": "追跡オブジェクトを生成するために必要な連続検知ヒット数。値を大きくすると誤初期化が減ります。デフォルトは fps の半分。" + }, + "max_disappeared": { + "label": "最大消失フレーム数", + "description": "追跡オブジェクトが消失したと判断するまでの未検知フレーム数。" + }, + "stationary": { + "label": "静止オブジェクト設定", + "description": "一定時間静止しているオブジェクトを検知・管理するための設定。", + "interval": { + "label": "静止チェック間隔", + "description": "静止オブジェクトを確認するための検知を、何フレームおきに実行するか。" + }, + "threshold": { + "label": "静止しきい値", + "description": "オブジェクトを静止状態とみなすために必要な位置変化のないフレーム数。" + }, + "max_frames": { + "label": "最大追跡フレーム", + "description": "静止オブジェクトを破棄するまでの追跡フレーム数の上限。", + "default": { + "label": "デフォルト最大フレーム", + "description": "静止オブジェクトの追跡を停止するまでのデフォルト最大フレーム数。" + }, + "objects": { + "label": "オブジェクト別最大フレーム", + "description": "静止オブジェクト追跡の最大フレーム数をオブジェクトごとに上書きします。" + } + }, + "classifier": { + "label": "ビジュアル分類器を有効化", + "description": "バウンディングボックスが揺らいでも真に静止しているオブジェクトを検知するため、ビジュアル分類器を使用します。" + } + }, + "annotation_offset": { + "label": "注釈オフセット", + "description": "タイムライン上のバウンディングボックスを録画と揃えるため、検知注釈を時間方向にずらすミリ秒数。正負どちらも指定可能。" + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "ライブストリーム中継および変換に利用される、統合型go2rtcリストリーミングサービスの設定。" + }, + "mqtt": { + "label": "MQTT", + "description": "テレメトリ、スナップショット、およびイベントの詳細をMQTTブローカーに接続して公開するための設定。", + "enabled": { + "label": "MQTTを有効にする", + "description": "状態、イベント、スナップショットの MQTT 連携を有効または無効にします。" + }, + "host": { + "label": "MQTT ホスト", + "description": "MQTT ブローカーのホスト名または IP アドレス。" + }, + "port": { + "label": "MQTT ポート", + "description": "MQTT ブローカーのポート(プレーン MQTT は通常 1883)。" + }, + "topic_prefix": { + "label": "トピックプレフィックス", + "description": "Frigate の全 MQTT トピックに付与するプレフィックス。複数インスタンスを起動する場合は重複しないようにします。" + }, + "client_id": { + "label": "クライアント ID", + "description": "MQTT ブローカー接続時に使用するクライアント識別子。インスタンスごとに一意である必要があります。" + }, + "stats_interval": { + "label": "統計送信間隔", + "description": "システムとカメラの統計を MQTT に発行する間隔(秒)。" + }, + "user": { + "label": "MQTT ユーザー名", + "description": "任意の MQTT ユーザー名。環境変数やシークレット経由でも指定可能。" + }, + "password": { + "label": "MQTT パスワード", + "description": "任意の MQTT パスワード。環境変数やシークレット経由でも指定可能。" + }, + "tls_ca_certs": { + "label": "TLS CA 証明書", + "description": "ブローカーへの TLS 接続用 CA 証明書のパス(自己署名証明書用)。" + }, + "tls_client_cert": { + "label": "クライアント証明書", + "description": "TLS 相互認証用のクライアント証明書パス。クライアント証明書を使う場合は user/password を設定しないでください。" + }, + "tls_client_key": { + "label": "クライアント鍵", + "description": "クライアント証明書の秘密鍵パス。" + }, + "tls_insecure": { + "label": "TLS 検証を無効化", + "description": "ホスト名検証をスキップして安全でない TLS 接続を許可します(非推奨)。" + }, + "qos": { + "label": "MQTT QoS", + "description": "MQTT パブリッシュ/サブスクライブの QoS レベル(0、1、または 2)。" + } + }, + "telemetry": { + "label": "テレメトリ", + "description": "GPU やネットワーク帯域監視を含む、システムテレメトリと統計のオプション。", + "network_interfaces": { + "label": "ネットワークインターフェイス", + "description": "帯域統計を監視するネットワークインターフェイス名のプレフィックスのリスト。" + }, + "stats": { + "label": "システム統計", + "description": "各種システム・GPU 統計の収集を有効/無効にするオプション。", + "amd_gpu_stats": { + "label": "AMD GPU 統計", + "description": "AMD GPU が存在する場合、AMD GPU 統計の収集を有効にします。" + }, + "intel_gpu_stats": { + "label": "Intel GPU 統計", + "description": "Intel GPU が存在する場合、Intel GPU 統計の収集を有効にします。" + }, + "network_bandwidth": { + "label": "ネットワーク帯域", + "description": "カメラの ffmpeg プロセスと検出器のプロセスごとのネットワーク帯域監視を有効にします(capabilities が必要)。" + }, + "intel_gpu_device": { + "label": "Intel GPU デバイス", + "description": "複数の Intel GPU が存在する場合、特定のデバイスに統計を絞り込むための PCI バスアドレスまたは DRM デバイスパス(例: /dev/dri/card1)。" + } + }, + "version_check": { + "label": "バージョンチェック", + "description": "新しい Frigate のバージョンが利用可能かを外部にチェックする機能を有効にします。" + } + }, + "notifications": { + "label": "通知", + "description": "全カメラの通知を有効化・制御する設定(カメラ別に上書き可能)。", + "enabled": { + "label": "通知を有効化", + "description": "全カメラの通知を有効または無効にします(カメラ別に上書き可能)。" + }, + "email": { + "label": "通知メールアドレス", + "description": "プッシュ通知用、または特定の通知プロバイダで必要となるメールアドレス。" + }, + "cooldown": { + "label": "クールダウン期間", + "description": "受信者への通知連投を避けるための通知間隔(秒)。" + }, + "enabled_in_config": { + "label": "元の通知状態", + "description": "元の静的設定で通知が有効化されていたかを示します。" + } + }, + "networking": { + "label": "ネットワーキング", + "description": "Frigate エンドポイントの IPv6 有効化など、ネットワーク関連の設定。", + "ipv6": { + "label": "IPv6 設定", + "description": "Frigate ネットワークサービス向けの IPv6 固有設定。", + "enabled": { + "label": "IPv6 を有効化", + "description": "適用可能な Frigate サービス(API および UI)で IPv6 サポートを有効にします。" + } + }, + "listen": { + "label": "待ち受けポート設定", + "description": "内部・外部の待ち受けポートの設定。上級者向け。多くの場合 Docker compose ファイルの ports セクションを変更することを推奨します。", + "internal": { + "label": "内部ポート", + "description": "Frigate の内部待ち受けポート(デフォルト 5000)。" + }, + "external": { + "label": "外部ポート", + "description": "Frigate の外部待ち受けポート(デフォルト 8971)。" + } + } + }, + "proxy": { + "label": "プロキシ", + "description": "認証済みユーザーヘッダーを渡すリバースプロキシの背後で Frigate を運用する際の設定。", + "header_map": { + "label": "ヘッダーマッピング", + "description": "プロキシベース認証のため、受信したプロキシヘッダーを Frigate のユーザー・ロールフィールドにマッピングします。", + "user": { + "label": "ユーザーヘッダー", + "description": "上流プロキシから提供される認証済みユーザー名を含むヘッダー。" + }, + "role": { + "label": "ロールヘッダー", + "description": "上流プロキシから提供される認証済みユーザーのロールまたはグループを含むヘッダー。" + }, + "role_map": { + "label": "ロールマッピング", + "description": "上流のグループ値を Frigate のロールにマッピングします(例: admin グループを admin ロールに)。" + } + }, + "logout_url": { + "label": "ログアウト URL", + "description": "プロキシ経由でログアウトする際にユーザーをリダイレクトする URL。" + }, + "auth_secret": { + "label": "プロキシシークレット", + "description": "信頼できるプロキシかを検証するため、X-Proxy-Secret ヘッダーと照合する任意のシークレット。" + }, + "default_role": { + "label": "デフォルトロール", + "description": "ロールマッピングが適用されない場合に、プロキシ認証されたユーザーに割り当てられるデフォルトのロール。" + }, + "separator": { + "label": "区切り文字", + "description": "プロキシヘッダーで複数値を渡す際に分割に使用する文字。" + } + }, + "tls": { + "label": "TLS", + "description": "Frigate の Web エンドポイント(ポート 8971)の TLS 設定。", + "enabled": { + "label": "TLS を有効化", + "description": "設定された TLS ポートで Frigate の Web UI と API の TLS を有効にします。" + } + }, + "ui": { + "label": "UI", + "description": "タイムゾーン、日時のフォーマット、単位など、ユーザーインターフェイスに関する設定。", + "timezone": { + "label": "タイムゾーン", + "description": "UI 全体に表示する任意のタイムゾーン(未設定時はブラウザのローカル時刻)。" + }, + "time_format": { + "label": "時刻表記", + "description": "UI で使用する時刻表記(browser、12hour、または 24hour)。" + }, + "date_style": { + "label": "日付スタイル", + "description": "UI で使用する日付スタイル(full、long、medium、short)。" + }, + "time_style": { + "label": "時刻スタイル", + "description": "UI で使用する時刻スタイル(full、long、medium、short)。" + }, + "unit_system": { + "label": "単位系", + "description": "UI と MQTT で使用する表示単位系(metric または imperial)。" + } + }, + "detectors": { + "label": "検出器ハードウェア", + "description": "物体検出器(CPU、GPU、ONNX バックエンド)と検出器固有のモデル設定。", + "type": { + "label": "タイプ" + }, + "model": { + "label": "検出器固有モデル設定", + "description": "検出器固有のモデル設定オプション(パス、入力サイズなど)。", + "path": { + "label": "カスタム物体検知モデルパス", + "description": "カスタム検知モデルファイルへのパス(または Frigate+ モデルの場合は plus://)。" + }, + "labelmap_path": { + "label": "カスタム検出器のラベルマップ", + "description": "検出器で数値クラスを文字列ラベルに対応付けるラベルマップファイルのパス。" + }, + "width": { + "label": "物体検知モデル入力幅", + "description": "モデル入力テンソルの幅(ピクセル)。" + }, + "height": { + "label": "物体検知モデル入力高さ", + "description": "モデル入力テンソルの高さ(ピクセル)。" + }, + "labelmap": { + "label": "ラベルマップカスタマイズ", + "description": "標準ラベルマップに統合する上書きや再マッピングのエントリ。" + }, + "attributes_map": { + "label": "オブジェクトラベル→属性ラベルのマップ", + "description": "メタデータを付与するためのオブジェクトラベル→属性ラベルのマッピング(例: 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "モデル入力テンソル形状", + "description": "モデルが期待するテンソル形式: 'nhwc' または 'nchw'。" + }, + "input_pixel_format": { + "label": "モデル入力ピクセルカラー形式", + "description": "モデルが期待するピクセルカラースペース: 'rgb'、'bgr'、または 'yuv'。" + }, + "input_dtype": { + "label": "モデル入力データ型", + "description": "モデル入力テンソルのデータ型(例: 'float32')。" + }, + "model_type": { + "label": "物体検知モデル種別", + "description": "一部の検出器が最適化に使用する検知モデルのアーキテクチャ種別(ssd, yolox, yolonas)。" + } + }, + "model_path": { + "label": "検出器固有モデルパス", + "description": "選択した検出器が必要とする検出器モデルバイナリのファイルパス。" + }, + "axengine": { + "label": "AXEngine NPU", + "description": "AXEngine ランタイム経由でコンパイル済み .axmodel ファイルを実行する AXERA AX650N/AX8850N NPU 検出器。" + }, + "cpu": { + "label": "CPU", + "description": "ハードウェアアクセラレーションなしにホスト CPU で TensorFlow Lite モデルを実行する CPU TFLite 検出器。非推奨。", + "num_threads": { + "label": "検知スレッド数", + "description": "CPU ベースの推論に使用するスレッド数。" + } + }, + "deepstack": { + "label": "DeepStack", + "description": "リモートの DeepStack HTTP API に画像を送信して推論する DeepStack/CodeProject.AI 検出器。非推奨。", + "api_url": { + "label": "DeepStack API URL", + "description": "DeepStack API の URL。" + }, + "api_timeout": { + "label": "DeepStack API タイムアウト(秒)", + "description": "DeepStack API リクエストの最大許容時間。" + }, + "api_key": { + "label": "DeepStack API キー(必要な場合)", + "description": "認証付き DeepStack サービス用の任意の API キー。" + } + }, + "degirum": { + "label": "DeGirum", + "description": "DeGirum クラウドまたはローカル推論サービス経由でモデルを実行する DeGirum 検出器。", + "location": { + "label": "推論ロケーション", + "description": "DeGirum 推論エンジンのロケーション(例: '@cloud'、'127.0.0.1')。" + }, + "zoo": { + "label": "モデル Zoo", + "description": "DeGirum モデル zoo のパスまたは URL。" + }, + "token": { + "label": "DeGirum クラウドトークン", + "description": "DeGirum クラウドアクセス用のトークン。" + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "EdgeTPU デリゲートを使用して Coral EdgeTPU 用にコンパイルされた TensorFlow Lite モデルを実行する EdgeTPU 検出器。", + "device": { + "label": "デバイス種別", + "description": "EdgeTPU 推論に使用するデバイス(例: 'usb'、'pci')。" + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "HEF モデルと HailoRT SDK を使用して Hailo ハードウェア上で推論する Hailo-8/Hailo-8L 検出器。", + "device": { + "label": "デバイス種別", + "description": "Hailo 推論に使用するデバイス(例: 'PCIe'、'M.2')。" + } + }, + "memryx": { + "label": "MemryX", + "description": "MemryX アクセラレータ上でコンパイル済み DFP モデルを実行する MemryX MX3 検出器。", + "device": { + "label": "デバイスパス", + "description": "MemryX 推論に使用するデバイス(例: 'PCIe')。" + } + }, + "onnx": { + "label": "ONNX", + "description": "ONNX モデルを実行する ONNX 検出器。利用可能な場合は CUDA/ROCm/OpenVINO などのアクセラレーションバックエンドを使用します。", + "device": { + "label": "デバイス種別", + "description": "ONNX 推論に使用するデバイス(例: 'AUTO'、'CPU'、'GPU')。" + } + }, + "openvino": { + "label": "OpenVINO", + "description": "AMD・Intel CPU、Intel GPU、Intel VPU ハードウェア用の OpenVINO 検出器。", + "device": { + "label": "デバイス種別", + "description": "OpenVINO 推論に使用するデバイス(例: 'CPU'、'GPU'、'NPU')。" + } + }, + "rknn": { + "label": "RKNN", + "description": "Rockchip NPU 用の RKNN 検出器。Rockchip ハードウェア上でコンパイル済み RKNN モデルを実行します。", + "num_cores": { + "label": "使用するNPUコア数。", + "description": "使用する NPU コア数(0 で自動)。" + } + }, + "synaptics": { + "label": "Synaptics", + "description": "Synaptics ハードウェア上で Synap SDK を使い .synap 形式のモデルを実行する Synaptics NPU 検出器。" + }, + "teflon_tfl": { + "label": "Teflon", + "description": "対応 GPU での推論を高速化するため Mesa Teflon delegate ライブラリを使用する TFLite 用の Teflon delegate 検出器。" + }, + "tensorrt": { + "label": "TensorRT", + "description": "シリアライズ済み TensorRT エンジンを使用して Nvidia Jetson デバイス上で推論を高速化する TensorRT 検出器。", + "device": { + "label": "GPU デバイスインデックス", + "description": "使用する GPU デバイスインデックス。" + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "ZeroMQ IPC エンドポイント経由で外部プロセスに推論をオフロードする ZMQ IPC 検出器。", + "endpoint": { + "label": "ZMQ IPC エンドポイント", + "description": "接続する ZMQ エンドポイント。" + }, + "request_timeout_ms": { + "label": "ZMQ リクエストタイムアウト(ミリ秒)", + "description": "ZMQ リクエストのタイムアウト(ミリ秒)。" + }, + "linger_ms": { + "label": "ZMQ ソケット linger(ミリ秒)", + "description": "ソケットの linger 期間(ミリ秒)。" + } + } + }, + "model": { + "label": "検知モデル", + "description": "カスタム物体検知モデルとその入力形状を設定します。", + "path": { + "label": "カスタム物体検知モデルパス", + "description": "カスタム検知モデルファイルへのパス(または Frigate+ モデルの場合は plus://)。" + }, + "labelmap_path": { + "label": "カスタム検出器のラベルマップ", + "description": "検出器で数値クラスを文字列ラベルに対応付けるラベルマップファイルのパス。" + }, + "width": { + "label": "物体検知モデル入力幅", + "description": "モデル入力テンソルの幅(ピクセル)。" + }, + "height": { + "label": "物体検知モデル入力高さ", + "description": "モデル入力テンソルの高さ(ピクセル)。" + }, + "labelmap": { + "label": "ラベルマップカスタマイズ", + "description": "標準ラベルマップに統合する上書きや再マッピングのエントリ。" + }, + "attributes_map": { + "label": "オブジェクトラベル→属性ラベルのマップ", + "description": "メタデータを付与するためのオブジェクトラベル→属性ラベルのマッピング(例: 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "モデル入力テンソル形状", + "description": "モデルが期待するテンソル形式: 'nhwc' または 'nchw'。" + }, + "input_pixel_format": { + "label": "モデル入力ピクセルカラー形式", + "description": "モデルが期待するピクセルカラースペース: 'rgb'、'bgr'、または 'yuv'。" + }, + "input_dtype": { + "label": "モデル入力データ型", + "description": "モデル入力テンソルのデータ型(例: 'float32')。" + }, + "model_type": { + "label": "物体検知モデル種別", + "description": "一部の検出器が最適化に使用する検知モデルのアーキテクチャ種別(ssd, yolox, yolonas)。" + } + }, + "genai": { + "label": "生成AI設定", + "description": "オブジェクトの説明やレビューサマリーを生成するために統合された生成 AI プロバイダの設定。", + "api_key": { + "label": "API キー", + "description": "一部のプロバイダで必要となる API キー(環境変数経由でも設定可能)。" + }, + "base_url": { + "label": "ベース URL", + "description": "セルフホスト型または互換プロバイダ向けのベース URL(例: Ollama インスタンス)。" + }, + "model": { + "label": "モデル", + "description": "説明やサマリーの生成にプロバイダから使用するモデル。" + }, + "provider": { + "label": "プロバイダ", + "description": "使用する生成 AI プロバイダ(例: ollama、gemini、openai)。" + }, + "roles": { + "label": "ロール", + "description": "GenAI のロール(chat、descriptions、embeddings)。1 ロールにつき 1 プロバイダ。" + }, + "provider_options": { + "label": "プロバイダオプション", + "description": "GenAI クライアントに渡すプロバイダ固有の追加オプション。" + }, + "runtime_options": { + "label": "ランタイムオプション", + "description": "推論呼び出しごとにプロバイダに渡されるランタイムオプション。" + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg の設定。バイナリパス、引数、ハードウェアアクセラレーション、ロール別の出力引数を含みます。", + "path": { + "label": "FFmpeg パス", + "description": "使用する FFmpeg バイナリのパス、またはバージョンエイリアス(「5.0」または「7.0」)。" + }, + "global_args": { + "label": "FFmpeg グローバル引数", + "description": "FFmpeg プロセスに渡されるグローバル引数。" + }, + "hwaccel_args": { + "label": "ハードウェアアクセラレーション引数", + "description": "FFmpeg のハードウェアアクセラレーション引数。プロバイダ固有のプリセットの使用を推奨。" + }, + "input_args": { + "label": "入力引数", + "description": "FFmpeg の入力ストリームに適用される引数。" + }, + "output_args": { + "label": "出力引数", + "description": "detect や record など、FFmpeg のロール別に使用されるデフォルト出力引数。", + "detect": { + "label": "検知ロールの出力引数", + "description": "detect ロールのストリームに使用されるデフォルト出力引数。" + }, + "record": { + "label": "録画ロールの出力引数", + "description": "record ロールのストリームに使用されるデフォルト出力引数。" + } + }, + "retry_interval": { + "label": "FFmpeg 再試行間隔", + "description": "カメラストリームの失敗後、再接続を試みるまでの待機秒数。デフォルトは 10 秒。" + }, + "apple_compatibility": { + "label": "Apple 互換性", + "description": "H.265 録画時に Apple プレーヤーとの互換性向上のため HEVC タグ付けを有効化します。" + }, + "gpu": { + "label": "GPU インデックス", + "description": "ハードウェアアクセラレーションで使用するデフォルト GPU インデックス。" + }, + "inputs": { + "label": "カメラ入力", + "description": "このカメラの入力ストリーム定義(パスとロール)のリスト。", + "path": { + "label": "入力パス", + "description": "カメラ入力ストリームの URL またはパス。" + }, + "roles": { + "label": "入力ロール", + "description": "この入力ストリームのロール。" + }, + "global_args": { + "label": "FFmpeg グローバル引数", + "description": "この入力ストリームに対する FFmpeg グローバル引数。" + }, + "hwaccel_args": { + "label": "ハードウェアアクセラレーション引数", + "description": "この入力ストリームのハードウェアアクセラレーション引数。" + }, + "input_args": { + "label": "入力引数", + "description": "このストリーム固有の入力引数。" + } + } + }, + "live": { + "label": "ライブ再生", + "description": "jsmpeg ライブストリームの解像度と品質を制御する設定。go2rtc を使用してリストリームしているカメラのライブビューには影響しません。", + "streams": { + "label": "ライブストリーム名", + "description": "設定済みのストリーム名と、ライブ再生で使用する restream/go2rtc 名のマッピング。" + }, + "height": { + "label": "ライブの高さ", + "description": "Web UI で jsmpeg ライブストリームを描画する高さ (ピクセル)。検知ストリーム高さ以下である必要があります。" + }, + "quality": { + "label": "ライブ品質", + "description": "jsmpeg ストリームのエンコード品質 (1 が最高、31 が最低)。" + } + }, + "motion": { + "label": "モーション検知", + "description": "全カメラに適用されるモーション検知のデフォルト設定(カメラ別に上書き可能)。", + "enabled": { + "label": "モーション検知を有効化", + "description": "全カメラのモーション検知を有効または無効にします(カメラ別に上書き可能)。" + }, + "threshold": { + "label": "モーションしきい値", + "description": "モーション検出器が使用するピクセル差分しきい値。値を大きくすると感度が下がります (範囲 1-255)。" + }, + "lightning_threshold": { + "label": "雷検知しきい値", + "description": "短時間の照明スパイクを検知して無視するしきい値(値が小さいほど感度が高く、0.3 〜 1.0 が目安)。これはモーション検知を完全に止めるものではなく、しきい値超過後に検出器が追加フレームの解析を停止するだけです。モーションベースの録画はこれらのイベント中も作成されます。" + }, + "skip_motion_threshold": { + "label": "モーションスキップしきい値", + "description": "0.0 〜 1.0 の値を指定し、1 フレームでそれ以上の割合が変化した場合、検出器はモーションボックスを返さず即座に再キャリブレーションします。雷や嵐などの誤検知を減らし CPU を節約できますが、PTZ カメラのオート追跡などの実イベントを取りこぼす可能性があります。数 MB の録画を捨てるか、数本の短いクリップを確認するかのトレードオフです。無効化するには未設定 (None) のままにします。" + }, + "improve_contrast": { + "label": "コントラスト強調", + "description": "モーション解析前にフレームのコントラストを強調して検知を補助します。" + }, + "contour_area": { + "label": "輪郭面積", + "description": "モーション輪郭としてカウントするために必要な最小ピクセル数。" + }, + "delta_alpha": { + "label": "デルタアルファ", + "description": "モーション計算のフレーム差分で使用されるアルファブレンディング係数。" + }, + "frame_alpha": { + "label": "フレームアルファ", + "description": "モーション前処理でフレームをブレンドする際に使用するアルファ値。" + }, + "frame_height": { + "label": "フレーム高さ", + "description": "モーション計算時にフレームをスケーリングする高さ (ピクセル)。" + }, + "mask": { + "label": "マスク座標", + "description": "領域を含める/除外するモーションマスクポリゴンを定義する x,y 座標の順序付きリスト。" + }, + "mqtt_off_delay": { + "label": "MQTT オフ遅延", + "description": "最後のモーション検知後、MQTT で「off」状態を発行するまでの待機秒数。" + }, + "enabled_in_config": { + "label": "元のモーション状態", + "description": "元の静的設定でモーション検知が有効化されていたかを示します。" + }, + "raw_mask": { + "label": "Raw マスク" + } + }, + "objects": { + "label": "オブジェクト", + "description": "追跡するラベルとオブジェクト別フィルタを含む、オブジェクト追跡のデフォルト設定。", + "track": { + "label": "追跡するオブジェクト", + "description": "全カメラで追跡するオブジェクトラベルのリスト(カメラ別に上書き可能)。" + }, + "filters": { + "label": "オブジェクトフィルタ", + "description": "誤検知を減らすために検知オブジェクトに適用するフィルタ(面積、比率、信頼度)。", + "min_area": { + "label": "最小オブジェクト面積", + "description": "このオブジェクト種別に必要なバウンディングボックスの最小面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。" + }, + "max_area": { + "label": "最大オブジェクト面積", + "description": "このオブジェクト種別に許容されるバウンディングボックスの最大面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。" + }, + "min_ratio": { + "label": "最小アスペクト比", + "description": "対象となるバウンディングボックスに必要な最小の幅/高さ比。" + }, + "max_ratio": { + "label": "最大アスペクト比", + "description": "対象となるバウンディングボックスに許容される最大の幅/高さ比。" + }, + "threshold": { + "label": "信頼度しきい値", + "description": "オブジェクトを真陽性とみなすために必要な平均検知信頼度。" + }, + "min_score": { + "label": "最低信頼度", + "description": "オブジェクトをカウントするために必要な単一フレームでの最低検知信頼度。" + }, + "mask": { + "label": "フィルタマスク", + "description": "フレーム内でこのフィルタが適用される範囲を定義するポリゴン座標。" + }, + "raw_mask": { + "label": "Raw マスク" + } + }, + "mask": { + "label": "オブジェクトマスク", + "description": "指定領域でオブジェクト検知を行わないようにするためのマスクポリゴン。" + }, + "raw_mask": { + "label": "Raw マスク" + }, + "genai": { + "label": "GenAI オブジェクト設定", + "description": "追跡オブジェクトの説明生成や、生成 AI へのフレーム送信に関する GenAI オプション。", + "enabled": { + "label": "GenAI を有効化", + "description": "追跡オブジェクトの説明を GenAI で生成する機能を既定で有効にします。" + }, + "use_snapshot": { + "label": "スナップショットを使用", + "description": "GenAI 説明生成にサムネイルではなくオブジェクトスナップショットを使用します。" + }, + "prompt": { + "label": "キャプションプロンプト", + "description": "GenAI で説明を生成する際に使用するデフォルトのプロンプトテンプレート。" + }, + "object_prompts": { + "label": "オブジェクト別プロンプト", + "description": "特定のラベルに対する GenAI 出力をカスタマイズするためのオブジェクト別プロンプト。" + }, + "objects": { + "label": "GenAI 対象オブジェクト", + "description": "GenAI に既定で送信するオブジェクトラベルのリスト。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "GenAI 説明生成の対象となるためにオブジェクトが進入する必要があるゾーン。" + }, + "debug_save_thumbnails": { + "label": "サムネイルを保存", + "description": "デバッグや確認のため、GenAI に送信したサムネイルを保存します。" + }, + "send_triggers": { + "label": "GenAI 送信トリガー", + "description": "フレームを GenAI に送るタイミング(終了時、更新後など)を定義します。", + "tracked_object_end": { + "label": "終了時に送信", + "description": "追跡オブジェクトが終了した時点で GenAI にリクエストを送信します。" + }, + "after_significant_updates": { + "label": "早期 GenAI トリガー", + "description": "追跡オブジェクトに対して指定回数の重要な更新があった後、GenAI にリクエストを送信します。" + } + }, + "enabled_in_config": { + "label": "元の GenAI 状態", + "description": "元の静的設定で GenAI が有効化されていたかを示します。" + } + }, + "filters_attribute": { + "label": "属性フィルタ", + "description": "誤検知を減らすために検知された属性に適用するフィルタ(面積、比率、信頼度)。", + "min_area": { + "label": "属性の最小面積", + "description": "この属性に必要なバウンディングボックスの最小面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。" + }, + "max_area": { + "label": "属性の最大面積", + "description": "この属性に許容されるバウンディングボックスの最大面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。" + }, + "min_ratio": { + "label": "最小アスペクト比", + "description": "対象となるバウンディングボックスに必要な最小の幅/高さ比。" + }, + "max_ratio": { + "label": "最大アスペクト比", + "description": "対象となるバウンディングボックスに許容される最大の幅/高さ比。" + }, + "threshold": { + "label": "信頼度しきい値", + "description": "属性を真陽性とみなすために必要な平均検知信頼度。" + }, + "min_score": { + "label": "最低信頼度", + "description": "この属性を親オブジェクトに関連付けるために必要な単一フレームでの最低検知信頼度。" + }, + "mask": { + "label": "フィルタマスク", + "description": "フレーム内でこのフィルタが適用される範囲を定義するポリゴン座標。" + }, + "raw_mask": { + "label": "Raw マスク" + } + } + }, + "record": { + "label": "録画", + "description": "カメラ別の上書きがない限り全カメラに適用される、録画と保持の設定。", + "enabled": { + "label": "録画を有効化", + "description": "全カメラの録画を有効または無効にします(カメラ別に上書き可能)。" + }, + "expire_interval": { + "label": "録画クリーンアップ間隔", + "description": "期限切れ録画セグメントを削除するクリーンアップを実行する間隔 (分)。" + }, + "continuous": { + "label": "常時保持", + "description": "追跡オブジェクトやモーションに関係なく録画を保持する日数。アラートと検知の録画のみを保持したい場合は 0 を指定します。", + "days": { + "label": "保持日数", + "description": "録画を保持する日数。" + } + }, + "motion": { + "label": "モーション録画保持", + "description": "追跡オブジェクトに関係なくモーションでトリガーされた録画を保持する日数。アラートと検知の録画のみを保持したい場合は 0 を指定します。", + "days": { + "label": "保持日数", + "description": "録画を保持する日数。" + } + }, + "detections": { + "label": "検知録画保持", + "description": "検知イベントの録画保持設定。前後の撮影時間を含みます。", + "pre_capture": { + "label": "イベント前秒数", + "description": "検知イベントの前に録画に含める秒数。" + }, + "post_capture": { + "label": "イベント後秒数", + "description": "検知イベントの後に録画に含める秒数。" + }, + "retain": { + "label": "イベント保持", + "description": "検知イベントの録画保持設定。", + "days": { + "label": "保持日数", + "description": "検知イベント録画を保持する日数。" + }, + "mode": { + "label": "保持モード", + "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。" + } + } + }, + "alerts": { + "label": "アラート録画保持", + "description": "アラートイベントの録画保持設定。前後の撮影時間を含みます。", + "pre_capture": { + "label": "イベント前秒数", + "description": "アラートイベントの前に録画に含める秒数。" + }, + "post_capture": { + "label": "イベント後秒数", + "description": "アラートイベントの後に録画に含める秒数。" + }, + "retain": { + "label": "イベント保持", + "description": "アラートイベントの録画保持設定。", + "days": { + "label": "保持日数", + "description": "アラートイベント録画を保持する日数。" + }, + "mode": { + "label": "保持モード", + "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。" + } + } + }, + "export": { + "label": "エクスポート設定", + "description": "タイムラプスやハードウェアアクセラレーションなど、録画をエクスポートする際に使用する設定。", + "hwaccel_args": { + "label": "エクスポート用 hwaccel 引数", + "description": "エクスポート/トランスコード処理で使用するハードウェアアクセラレーション引数。" + }, + "max_concurrent": { + "label": "同時エクスポート数の上限", + "description": "同時に処理するエクスポートジョブの最大数。" + } + }, + "preview": { + "label": "プレビュー設定", + "description": "UI に表示される録画プレビューの品質を制御する設定。", + "quality": { + "label": "プレビュー品質", + "description": "プレビュー品質レベル (very_low, low, medium, high, very_high)。" + } + }, + "enabled_in_config": { + "label": "元の録画状態", + "description": "元の静的設定で録画が有効化されていたかを示します。" + } + }, + "review": { + "label": "レビュー", + "description": "UI 表示と保存で使用するアラート、検知、GenAI レビューサマリーを制御する設定。", + "alerts": { + "label": "アラート設定", + "description": "どの追跡オブジェクトがアラートを生成するか、およびアラートの保持方法に関する設定。", + "enabled": { + "label": "アラートを有効化", + "description": "全カメラのアラート生成を有効または無効にします(カメラ別に上書き可能)。" + }, + "labels": { + "label": "アラートラベル", + "description": "アラート対象となるオブジェクトラベルのリスト(例: car, person)。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "アラートとみなされるためにオブジェクトが進入する必要があるゾーン。空欄ですべてのゾーンを許可。" + }, + "enabled_in_config": { + "label": "元のアラート状態", + "description": "元の静的設定でアラートが有効化されていたかを記録します。" + }, + "cutoff_time": { + "label": "アラート打ち切り時間", + "description": "アラートを引き起こすアクティビティが途絶えてからアラートを打ち切るまでの待機秒数。" + } + }, + "detections": { + "label": "検知設定", + "description": "どの追跡オブジェクトが(アラートではない)検知を生成するか、および検知の保持方法に関する設定。", + "enabled": { + "label": "検知を有効化", + "description": "全カメラの検知イベントを有効または無効にします(カメラ別に上書き可能)。" + }, + "labels": { + "label": "検知ラベル", + "description": "検知イベントの対象となるオブジェクトラベルのリスト。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "検知とみなされるためにオブジェクトが進入する必要があるゾーン。空欄ですべてのゾーンを許可。" + }, + "cutoff_time": { + "label": "検知打ち切り時間", + "description": "検知を引き起こすアクティビティが途絶えてから検知を打ち切るまでの待機秒数。" + }, + "enabled_in_config": { + "label": "元の検知状態", + "description": "元の静的設定で検知が有効化されていたかを記録します。" + } + }, + "genai": { + "label": "GenAI 設定", + "description": "レビュー項目の説明やサマリーを生成 AI で作成する機能の制御。", + "enabled": { + "label": "GenAI 説明を有効化", + "description": "レビュー項目の GenAI 生成説明やサマリーを有効または無効にします。" + }, + "alerts": { + "label": "アラートに GenAI を使用", + "description": "アラート項目の説明を GenAI で生成します。" + }, + "detections": { + "label": "検知に GenAI を使用", + "description": "検知項目の説明を GenAI で生成します。" + }, + "image_source": { + "label": "レビュー画像ソース", + "description": "GenAI に送信する画像のソース(「preview」または「recordings」)。「recordings」は高品質ですがトークン消費が増えます。" + }, + "additional_concerns": { + "label": "追加の懸念事項", + "description": "このカメラのアクティビティ評価時に GenAI に考慮させる追加の懸念や注意事項のリスト。" + }, + "debug_save_thumbnails": { + "label": "サムネイルを保存", + "description": "デバッグや確認のため、GenAI プロバイダに送信したサムネイルを保存します。" + }, + "enabled_in_config": { + "label": "元の GenAI 状態", + "description": "元の静的設定で GenAI レビューが有効化されていたかを記録します。" + }, + "preferred_language": { + "label": "希望言語", + "description": "GenAI プロバイダに生成応答で要求する言語。" + }, + "activity_context_prompt": { + "label": "活動コンテキストプロンプト", + "description": "GenAI サマリーの文脈として、何が不審な活動で何がそうでないかを記述するカスタムプロンプト。" + } + } + }, + "snapshots": { + "label": "スナップショット", + "description": "全カメラの追跡オブジェクトに対する API 生成スナップショットの設定(カメラ別に上書き可能)。", + "enabled": { + "label": "スナップショットを有効化", + "description": "全カメラのスナップショット保存を有効または無効にします(カメラ別に上書き可能)。" + }, + "timestamp": { + "label": "タイムスタンプ重ね合わせ", + "description": "API スナップショットにタイムスタンプを重ねて表示します。" + }, + "bounding_box": { + "label": "バウンディングボックス重ね合わせ", + "description": "API スナップショットに追跡オブジェクトのバウンディングボックスを描画します。" + }, + "crop": { + "label": "スナップショットを切り抜き", + "description": "API スナップショットを検知オブジェクトのバウンディングボックスで切り抜きます。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "スナップショットを保存するためにオブジェクトが進入する必要があるゾーン。" + }, + "height": { + "label": "スナップショット高さ", + "description": "API スナップショットをリサイズする高さ (ピクセル)。空欄で元のサイズを維持。" + }, + "retain": { + "label": "スナップショット保持", + "description": "デフォルト保持日数とオブジェクト別上書きを含む、スナップショット保持設定。", + "default": { + "label": "デフォルト保持期間", + "description": "スナップショットを保持するデフォルト日数。" + }, + "mode": { + "label": "保持モード", + "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。" + }, + "objects": { + "label": "オブジェクト別保持", + "description": "オブジェクトごとのスナップショット保持日数の上書き。" + } + }, + "quality": { + "label": "スナップショット品質", + "description": "保存するスナップショットのエンコード品質 (0-100)。" + } + }, + "timestamp_style": { + "label": "タイムスタンプスタイル", + "description": "デバッグビューとスナップショットの映像内に表示されるタイムスタンプのスタイル設定。", + "position": { + "label": "タイムスタンプ位置", + "description": "画像内のタイムスタンプ位置 (tl/tr/bl/br)。" + }, + "format": { + "label": "タイムスタンプ書式", + "description": "タイムスタンプに使用する日時書式文字列 (Python datetime 書式コード)。" + }, + "color": { + "label": "タイムスタンプ色", + "description": "タイムスタンプ文字色の RGB 値 (各 0-255)。", + "red": { + "label": "赤", + "description": "タイムスタンプ色の赤成分 (0-255)。" + }, + "green": { + "label": "緑", + "description": "タイムスタンプ色の緑成分 (0-255)。" + }, + "blue": { + "label": "青", + "description": "タイムスタンプ色の青成分 (0-255)。" + } + }, + "thickness": { + "label": "タイムスタンプ太さ", + "description": "タイムスタンプ文字の線の太さ。" + }, + "effect": { + "label": "タイムスタンプエフェクト", + "description": "タイムスタンプ文字の視覚効果 (none, solid, shadow)。" + } + }, + "classification": { + "label": "オブジェクト分類", + "description": "オブジェクトラベルの精緻化や状態分類に使用する分類モデルの設定。", + "bird": { + "label": "鳥分類設定", + "description": "鳥分類モデル固有の設定。", + "enabled": { + "label": "鳥分類", + "description": "鳥分類を有効または無効にします。" + }, + "threshold": { + "label": "最低スコア", + "description": "鳥分類を受け入れるために必要な最低分類スコア。" + } + }, + "custom": { + "label": "カスタム分類モデル", + "description": "オブジェクトまたは状態検知に使用するカスタム分類モデルの設定。", + "enabled": { + "label": "モデルを有効化", + "description": "カスタム分類モデルを有効または無効にします。" + }, + "name": { + "label": "モデル名", + "description": "使用するカスタム分類モデルの識別子。" + }, + "threshold": { + "label": "スコアしきい値", + "description": "分類状態を変更するために使用するスコアしきい値。" + }, + "save_attempts": { + "label": "保存試行数", + "description": "最近の分類 UI に表示するために保存する分類試行数。" + }, + "object_config": { + "objects": { + "label": "分類対象オブジェクト", + "description": "オブジェクト分類を実行するオブジェクト種別のリスト。" + }, + "classification_type": { + "label": "分類種別", + "description": "適用する分類種別: 'sub_label'(サブラベルを追加)またはサポートされる他の種別。" + } + }, + "state_config": { + "cameras": { + "label": "分類対象カメラ", + "description": "状態分類を実行するためのカメラ別クロップと設定。", + "crop": { + "label": "分類クロップ", + "description": "このカメラで分類を実行する際に使用するクロップ座標。" + } + }, + "motion": { + "label": "モーション時に実行", + "description": "true にすると、指定したクロップ内でモーションが検知されたときに分類を実行します。" + }, + "interval": { + "label": "分類間隔", + "description": "状態分類の周期的な実行間隔(秒)。" + } + } + } + }, + "semantic_search": { + "label": "セマンティック検索", + "description": "オブジェクト埋め込みを構築・クエリして類似項目を見つけるセマンティック検索の設定。", + "enabled": { + "label": "セマンティック検索を有効化", + "description": "セマンティック検索機能を有効または無効にします。" + }, + "reindex": { + "label": "起動時に再インデックス", + "description": "過去の追跡オブジェクトを埋め込みデータベースに完全再インデックスします。" + }, + "model": { + "label": "セマンティック検索モデルまたは GenAI プロバイダ名", + "description": "セマンティック検索に使用する埋め込みモデル(例: 'jinav1')、または embeddings ロールを持つ GenAI プロバイダ名。" + }, + "model_size": { + "label": "モデルサイズ", + "description": "モデルサイズを選択。'small' は CPU で動作、'large' は通常 GPU が必要。" + }, + "device": { + "label": "デバイス", + "description": "特定のデバイスを対象にする上書き設定 (詳細は https://onnxruntime.ai/docs/execution-providers/ を参照)" + }, + "triggers": { + "label": "トリガー", + "description": "カメラ別のセマンティック検索トリガーの動作と一致条件。", + "friendly_name": { + "label": "表示名", + "description": "このトリガーの UI 表示用の任意の名前。" + }, + "enabled": { + "label": "このトリガーを有効化", + "description": "このセマンティック検索トリガーを有効または無効にします。" + }, + "type": { + "label": "トリガー種別", + "description": "トリガー種別: 「thumbnail」(画像に対する一致)または「description」(テキストに対する一致)。" + }, + "data": { + "label": "トリガー内容", + "description": "追跡オブジェクトと照合するテキストフレーズまたはサムネイル ID。" + }, + "threshold": { + "label": "トリガーしきい値", + "description": "このトリガーを発火させるために必要な最低類似度スコア (0-1)。" + }, + "actions": { + "label": "トリガーアクション", + "description": "トリガー一致時に実行するアクションのリスト (notification, sub_label, attribute)。" + } + } + }, + "face_recognition": { + "label": "顔認識", + "description": "全カメラの顔検知と顔認識の設定(カメラ別に上書き可能)。", + "enabled": { + "label": "顔認識を有効化", + "description": "全カメラの顔認識を有効または無効にします(カメラ別に上書き可能)。" + }, + "model_size": { + "label": "モデルサイズ", + "description": "顔埋め込みに使用するモデルサイズ(small/large)。large は GPU が必要な場合があります。" + }, + "unknown_score": { + "label": "未知顔スコアしきい値", + "description": "この距離しきい値を下回る場合、顔は潜在的な一致とみなされます(値が大きいほど厳格)。" + }, + "detection_threshold": { + "label": "検知しきい値", + "description": "顔検知を有効とみなすために必要な最低検知信頼度。" + }, + "recognition_threshold": { + "label": "認識しきい値", + "description": "2 つの顔を一致とみなす顔埋め込みの距離しきい値。" + }, + "min_area": { + "label": "顔の最小面積", + "description": "認識を試みるために必要な顔ボックスの最小面積 (ピクセル)。" + }, + "min_faces": { + "label": "最低顔認識数", + "description": "認識されたサブラベルを人物に適用する前に必要な、最低顔認識回数。" + }, + "save_attempts": { + "label": "保存試行数", + "description": "最近の認識 UI に保持する顔認識試行数。" + }, + "blur_confidence_filter": { + "label": "ボケ補正による信頼度調整", + "description": "画像のボケに基づいて信頼度スコアを調整し、低品質な顔の誤検知を減らします。" + }, + "device": { + "label": "デバイス", + "description": "特定のデバイスを対象にする上書き設定 (詳細は https://onnxruntime.ai/docs/execution-providers/ を参照)" + } + }, + "lpr": { + "label": "ナンバープレート認識", + "description": "ナンバープレート認識の設定。検知しきい値、書式整形、既知ナンバーなどを含みます。", + "enabled": { + "label": "LPR を有効化", + "description": "全カメラのナンバープレート認識を有効または無効にします(カメラ別に上書き可能)。" + }, + "model_size": { + "label": "モデルサイズ", + "description": "テキスト検知/認識に使用するモデルサイズ。多くのユーザーは 'small' を使用してください。" + }, + "detection_threshold": { + "label": "検知しきい値", + "description": "プレート候補に対する OCR を開始する検知信頼度しきい値。" + }, + "min_area": { + "label": "プレート最小面積", + "description": "認識を試みるために必要なプレート最小面積 (ピクセル)。" + }, + "recognition_threshold": { + "label": "認識しきい値", + "description": "認識されたプレート文字列をサブラベルとして付与するために必要な信頼度しきい値。" + }, + "min_plate_length": { + "label": "プレート最小文字数", + "description": "有効とみなすために認識されたプレートに必要な最小文字数。" + }, + "format": { + "label": "プレート書式正規表現", + "description": "認識されたプレート文字列を期待する書式と照合する任意の正規表現。" + }, + "match_distance": { + "label": "マッチ距離", + "description": "検知されたプレートと既知のプレートを比較する際に許容する文字相違数。" + }, + "known_plates": { + "label": "既知のプレート", + "description": "特別に追跡またはアラートするプレートまたは正規表現のリスト。" + }, + "enhancement": { + "label": "強調レベル", + "description": "OCR 前にプレート切り出し画像に適用する強調レベル (0-10)。値を大きくしても常に改善するとは限らず、5 を超えると夜間プレートでのみ有効な場合があるため注意が必要です。" + }, + "debug_save_plates": { + "label": "デバッグ用プレート保存", + "description": "LPR 性能のデバッグ用にプレート切り出し画像を保存します。" + }, + "device": { + "label": "デバイス", + "description": "特定のデバイスを対象にする上書き設定 (詳細は https://onnxruntime.ai/docs/execution-providers/ を参照)" + }, + "replace_rules": { + "label": "置換ルール", + "description": "照合前に検知プレート文字列を正規化するための正規表現置換ルール。", + "pattern": { + "label": "正規表現パターン" + }, + "replacement": { + "label": "置換文字列" + } + }, + "expire_time": { + "label": "失効秒数", + "description": "未検知ナンバーをトラッカーから失効させるまでの秒数(専用 LPR カメラのみ)。" + } + }, + "camera_groups": { + "label": "カメラグループ", + "description": "UI でカメラを整理するための名前付きカメラグループの設定。", + "cameras": { + "label": "カメラリスト", + "description": "このグループに含まれるカメラ名の配列。" + }, + "icon": { + "label": "グループアイコン", + "description": "UI でカメラグループを表すアイコン。" + }, + "order": { + "label": "並び順", + "description": "UI でカメラグループを並べる数値順。値が大きいほど後ろに表示されます。" + } + }, + "profiles": { + "label": "プロファイル", + "description": "表示名付きの名前付きプロファイル定義。カメラ別プロファイルはここで定義された名前を参照する必要があります。", + "friendly_name": { + "label": "表示名", + "description": "UI に表示されるこのプロファイルの表示名。" + } + }, + "active_profile": { + "label": "アクティブプロファイル", + "description": "現在アクティブなプロファイル名。実行時のみ有効で YAML には保存されません。" + }, + "camera_mqtt": { + "label": "MQTT", + "description": "MQTT 画像配信の設定。", + "enabled": { + "label": "画像送信", + "description": "このカメラのオブジェクト画像スナップショットを MQTT トピックに配信する機能を有効にします。" + }, + "timestamp": { + "label": "タイムスタンプを追加", + "description": "MQTT に配信する画像にタイムスタンプを重ねて表示します。" + }, + "bounding_box": { + "label": "バウンディングボックスを追加", + "description": "MQTT に配信する画像にバウンディングボックスを描画します。" + }, + "crop": { + "label": "画像を切り抜き", + "description": "MQTT に配信する画像を検知オブジェクトのバウンディングボックスで切り抜きます。" + }, + "height": { + "label": "画像の高さ", + "description": "MQTT 配信時に画像をリサイズする高さ (ピクセル)。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "MQTT 画像を配信するためにオブジェクトが進入する必要があるゾーン。" + }, + "quality": { + "label": "JPEG 品質", + "description": "MQTT に配信する画像の JPEG 品質 (0-100)。" + } + }, + "camera_ui": { + "label": "カメラ UI", + "description": "UI 内でのこのカメラの表示順と表示設定。順序はデフォルトダッシュボードに影響します。より細かい制御にはカメラグループを使用してください。", + "order": { + "label": "UI 表示順", + "description": "UI 内でのカメラの並び順に使用される数値(デフォルトダッシュボードとリスト)。値が大きいほど後ろに表示されます。" + }, + "dashboard": { + "label": "UI に表示", + "description": "このカメラを Frigate UI 全体に表示するかを切り替えます。無効化した場合、再表示するには設定ファイルを手動編集する必要があります。" + } + }, + "onvif": { + "label": "ONVIF", + "description": "このカメラの ONVIF 接続および PTZ オート追跡の設定。", + "host": { + "label": "ONVIF ホスト", + "description": "このカメラの ONVIF サービスのホスト(オプションでスキーマも)。" + }, + "port": { + "label": "ONVIF ポート", + "description": "ONVIF サービスのポート番号。" + }, + "user": { + "label": "ONVIF ユーザー名", + "description": "ONVIF 認証用のユーザー名。ONVIF に admin ユーザーが必要なデバイスもあります。" + }, + "password": { + "label": "ONVIF パスワード", + "description": "ONVIF 認証用のパスワード。" + }, + "tls_insecure": { + "label": "TLS 検証を無効化", + "description": "ONVIF の TLS 検証をスキップし、ダイジェスト認証も無効化します(安全でないため、信頼できるネットワークでのみ使用)。" + }, + "profile": { + "label": "ONVIF プロファイル", + "description": "PTZ 制御に使用する ONVIF メディアプロファイル(トークンまたは名前で指定)。未設定の場合、有効な PTZ 設定を持つ最初のプロファイルが自動選択されます。" + }, + "autotracking": { + "label": "オートトラッキング", + "description": "PTZ カメラの動作で移動中のオブジェクトを自動追跡し、フレーム中央に保ちます。", + "enabled": { + "label": "オートトラッキングを有効化", + "description": "検知オブジェクトの PTZ オート追跡を有効または無効にします。" + }, + "calibrate_on_startup": { + "label": "起動時にキャリブレーション", + "description": "追跡精度を向上させるため、起動時に PTZ モーター速度を測定します。キャリブレーション後に movement_weights が設定に書き込まれます。" + }, + "zooming": { + "label": "ズームモード", + "description": "ズーム動作の制御: disabled (パン/チルトのみ)、absolute (互換性が最も高い)、relative (パン/チルト/ズーム同時)。" + }, + "zoom_factor": { + "label": "ズーム倍率", + "description": "追跡対象オブジェクトのズームレベルを制御します。値が小さいほど広い範囲を保ち、大きいほどズームインしますが追跡を失う可能性があります。0.1 〜 0.75 の範囲で指定。" + }, + "track": { + "label": "追跡対象オブジェクト", + "description": "オートトラッキングを発動させるオブジェクト種別のリスト。" + }, + "required_zones": { + "label": "必須ゾーン", + "description": "オートトラッキング開始前にオブジェクトが進入する必要があるゾーン。" + }, + "return_preset": { + "label": "復帰プリセット", + "description": "追跡終了後にカメラが戻る、ファームウェアに設定されている ONVIF プリセット名。" + }, + "timeout": { + "label": "復帰タイムアウト", + "description": "追跡を失ってからカメラをプリセット位置に戻すまでの待機秒数。" + }, + "movement_weights": { + "label": "動作重み", + "description": "カメラキャリブレーションによって自動生成される値。手動で変更しないでください。" + }, + "enabled_in_config": { + "label": "元のオート追跡状態", + "description": "オートトラッキングが設定で有効化されていたかを追跡する内部フィールド。" + } + }, + "ignore_time_mismatch": { + "label": "時刻差異を無視", + "description": "ONVIF 通信時に、カメラと Frigate サーバー間の時刻同期差異を無視します。" + } + } +} diff --git a/web/public/locales/ja/config/groups.json b/web/public/locales/ja/config/groups.json new file mode 100644 index 0000000..8e829ea --- /dev/null +++ b/web/public/locales/ja/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "sensitivity": "グローバル感度", + "detection": "グローバル検知" + }, + "cameras": { + "detection": "検知", + "sensitivity": "感度" + } + }, + "timestamp_style": { + "cameras": { + "appearance": "外観" + }, + "global": { + "appearance": "全体の外観" + } + }, + "motion": { + "cameras": { + "sensitivity": "感度", + "algorithm": "アルゴリズム" + }, + "global": { + "sensitivity": "グローバル感度", + "algorithm": "グローバルアルゴリズム" + } + }, + "detect": { + "global": { + "resolution": "グローバル解像度", + "tracking": "グローバルトラッキング" + }, + "cameras": { + "resolution": "解像度", + "tracking": "トラッキング" + } + }, + "objects": { + "global": { + "tracking": "グローバルトラッキング", + "filtering": "グローバルフィルタ" + }, + "cameras": { + "tracking": "トラッキング", + "filtering": "フィルタ" + } + }, + "record": { + "global": { + "events": "グローバルイベント", + "retention": "グローバルリテンション" + }, + "cameras": { + "retention": "リテンション", + "events": "イベント" + } + }, + "snapshots": { + "global": { + "display": "グローバル表示" + }, + "cameras": { + "display": "表示" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "カメラ固有のFFmpeg引数" + } + } +} diff --git a/web/public/locales/ja/config/validation.json b/web/public/locales/ja/config/validation.json new file mode 100644 index 0000000..9f862a3 --- /dev/null +++ b/web/public/locales/ja/config/validation.json @@ -0,0 +1,35 @@ +{ + "pattern": "無効なフォーマット", + "required": "この項目は必須です", + "type": "無効な値タイプ", + "format": "無効なフォーマット", + "minimum": "{{limit}} 以上である必要があります", + "maximum": "{{limit}} 以下でなければなりません", + "exclusiveMinimum": "{{limit}} より大きい値である必要があります", + "exclusiveMaximum": "{{limit}} 未満でなければなりません", + "minLength": "{{limit}} 文字以上入力してください", + "maxLength": "最大 {{limit}} 文字までです", + "minItems": "{{limit}} 個以上のアイテムが必要です", + "maxItems": "アイテムは最大 {{limit}} 個までです", + "enum": "許可された値のいずれかである必要があります", + "const": "値が期待される定数と一致しません", + "uniqueItems": "全てのアイテムは一意である必要があります", + "additionalProperties": "不明なプロパティは使用できません", + "oneOf": "許可されたスキーマのうち、いずれか一つに完全一致する必要があります", + "anyOf": "許可されたスキーマのうち、少なくとも1つに一致する必要があります", + "proxy": { + "header_map": { + "roleHeaderRequired": "ロールのマッピングを設定する際は、ロールヘッダーが必要です。" + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "各ロールは、1つの入力ストリームにのみ割り当てることができます。", + "detectRequired": "少なくとも1つの入力ストリームに「detect」ロールを割り当てる必要があります。", + "hwaccelDetectOnly": "ハードウェアアクセラレーション引数を定義できるのは、detect ロールを持つ入力ストリームのみです。" + } + }, + "detect": { + "dimensionMustBeEven": "偶数を指定してください。" + } +} diff --git a/web/public/locales/ja/objects.json b/web/public/locales/ja/objects.json new file mode 100644 index 0000000..10fa2b8 --- /dev/null +++ b/web/public/locales/ja/objects.json @@ -0,0 +1,129 @@ +{ + "bicycle": "自転車", + "car": "車", + "person": "人", + "motorcycle": "オートバイ", + "airplane": "飛行機", + "animal": "動物", + "dog": "犬", + "bark": "吠え声", + "cat": "猫", + "horse": "馬", + "goat": "ヤギ", + "sheep": "羊", + "bird": "鳥", + "mouse": "マウス", + "keyboard": "キーボード", + "vehicle": "車両", + "boat": "ボート", + "bus": "バス", + "train": "電車", + "skateboard": "スケートボード", + "door": "ドア", + "blender": "ミキサー", + "sink": "流し台", + "hair_dryer": "ヘアドライヤー", + "toothbrush": "歯ブラシ", + "scissors": "はさみ", + "clock": "時計", + "traffic_light": "信号機", + "fire_hydrant": "消火栓", + "street_sign": "道路標識", + "stop_sign": "一時停止標識", + "parking_meter": "駐車メーター", + "bench": "ベンチ", + "cow": "牛", + "elephant": "象", + "bear": "クマ", + "zebra": "シマウマ", + "giraffe": "キリン", + "hat": "帽子", + "backpack": "バックパック", + "umbrella": "傘", + "shoe": "靴", + "eye_glasses": "メガネ", + "handbag": "ハンドバッグ", + "tie": "ネクタイ", + "suitcase": "スーツケース", + "frisbee": "フリスビー", + "skis": "スキー板", + "snowboard": "スノーボード", + "sports_ball": "スポーツボール", + "kite": "凧", + "baseball_bat": "野球バット", + "baseball_glove": "野球グローブ", + "surfboard": "サーフボード", + "tennis_racket": "テニスラケット", + "bottle": "ボトル", + "plate": "皿", + "wine_glass": "ワイングラス", + "cup": "コップ", + "fork": "フォーク", + "knife": "ナイフ", + "spoon": "スプーン", + "bowl": "ボウル", + "banana": "バナナ", + "apple": "リンゴ", + "sandwich": "サンドイッチ", + "orange": "オレンジ", + "broccoli": "ブロッコリー", + "carrot": "ニンジン", + "hot_dog": "ホットドッグ", + "pizza": "ピザ", + "donut": "ドーナツ", + "cake": "ケーキ", + "chair": "椅子", + "couch": "ソファ", + "potted_plant": "鉢植え", + "bed": "ベッド", + "mirror": "鏡", + "dining_table": "ダイニングテーブル", + "window": "窓", + "desk": "机", + "toilet": "トイレ", + "tv": "テレビ", + "laptop": "ノートパソコン", + "remote": "リモコン", + "cell_phone": "携帯電話", + "microwave": "電子レンジ", + "oven": "オーブン", + "toaster": "トースター", + "refrigerator": "冷蔵庫", + "book": "本", + "vase": "花瓶", + "teddy_bear": "テディベア", + "hair_brush": "ヘアブラシ", + "squirrel": "リス", + "deer": "シカ", + "fox": "キツネ", + "rabbit": "ウサギ", + "raccoon": "アライグマ", + "robot_lawnmower": "ロボット芝刈り機", + "waste_bin": "ゴミ箱", + "on_demand": "オンデマンド", + "face": "顔", + "license_plate": "ナンバープレート", + "package": "荷物", + "bbq_grill": "バーベキューグリル", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD", + "canada_post": "カナダポスト", + "royal_mail": "ロイヤルメール", + "school_bus": "スクールバス", + "skunk": "スカンク", + "kangaroo": "カンガルー", + "baby": "赤ちゃん", + "baby_stroller": "ベビーカー", + "rickshaw": "人力車", + "rodent": "齧歯類" +} diff --git a/web/public/locales/ja/views/chat.json b/web/public/locales/ja/views/chat.json new file mode 100644 index 0000000..4215a31 --- /dev/null +++ b/web/public/locales/ja/views/chat.json @@ -0,0 +1,72 @@ +{ + "documentTitle": "チャット - Frigate", + "title": "Frigate チャット", + "subtitle": "カメラ管理と分析のためのAIアシスタント", + "placeholder": "何でも聞いてください…", + "error": "問題が発生しました。もう一度お試しください。", + "processing": "処理中...", + "toolsUsed": "使用済み: {{tools}}", + "showTools": "ツールを表示 ({{count}})", + "hideTools": "ツールを非表示", + "call": "呼び出す", + "result": "結果", + "arguments": "引数:", + "response": "回答:", + "attachment_chip_label": "{{camera}} の {{label}}", + "attachment_chip_remove": "添付ファイルを削除", + "open_in_explore": "エクスプローラーで開く", + "attach_event_aria": "イベント {{eventId}} を添付する", + "attachment_picker_paste_label": "またはイベントIDを貼り付け", + "attachment_picker_attach": "添付", + "attachment_picker_placeholder": "イベントを添付", + "quick_reply_find_similar": "類似の目撃情報を探す", + "quick_reply_tell_me_more": "これについてもっと詳しく教えてください", + "quick_reply_when_else": "他にいつ目撃されたか?", + "quick_reply_find_similar_text": "これと似た目撃情報を探す。", + "quick_reply_tell_me_more_text": "これについてもっと詳しく教えてください。", + "quick_reply_when_else_text": "これと同じようなことが他にありましたか?", + "anchor": "参照", + "similarity_score": "類似性", + "no_similar_objects_found": "類似するオブジェクトは見つかりませんでした。", + "semantic_search_required": "類似オブジェクトを検索するには、セマンティック検索を有効にする必要があります。", + "send": "送信", + "suggested_requests": "質問してみてください:", + "starting_requests": { + "show_recent_events": "最近のイベントを表示", + "show_camera_status": "カメラの状態を表示", + "recap": "留守中に何が起きた?", + "watch_camera": "カメラの動きを監視" + }, + "starting_requests_prompts": { + "show_recent_events": "直近1時間のイベントを見せて", + "show_camera_status": "現在のカメラの状態はどうなっていますか?", + "recap": "留守中に何が起きた?", + "watch_camera": "玄関のカメラを監視して、誰か来たら教えて" + }, + "new_chat": "新しいチャット", + "settings": { + "title": "チャット設定", + "show_stats": { + "title": "統計を表示", + "desc": "チャット応答の生成速度とコンテキストサイズを表示します。", + "while_generating": "生成中のみ", + "always": "常に表示" + }, + "auto_scroll": { + "title": "自動スクロール", + "desc": "新しいメッセージが届いたら自動でスクロールします。" + } + }, + "stats": { + "context": "{{tokens}} トークン", + "tokens_per_second": "{{rate}} t/s" + }, + "reasoning": { + "active": "推論中…", + "show": "推論を表示", + "hide": "推論を非表示" + }, + "thinking": { + "toggle": "思考の表示を切替" + } +} diff --git a/web/public/locales/ja/views/classificationModel.json b/web/public/locales/ja/views/classificationModel.json new file mode 100644 index 0000000..91ff04c --- /dev/null +++ b/web/public/locales/ja/views/classificationModel.json @@ -0,0 +1,191 @@ +{ + "documentTitle": "分類モデル - Frigate", + "button": { + "deleteImages": "画像を削除", + "deleteClassificationAttempts": "分類画像を削除", + "renameCategory": "クラス名を変更", + "deleteCategory": "クラスを削除", + "trainModel": "モデルを学習", + "addClassification": "分類を追加", + "deleteModels": "モデルを削除", + "editModel": "モデルを編集" + }, + "toast": { + "success": { + "deletedImage_other": "{{count}} 枚の画像を削除しました", + "categorizedImage": "画像の分類に成功しました", + "trainedModel": "モデルを正常に学習させました。", + "trainingModel": "モデルのトレーニングを正常に開始しました。", + "deletedCategory_other": "{{count}} 件のクラスを削除しました", + "deletedModel_other": "{{count}} 件のモデルを削除しました", + "updatedModel": "モデル設定を更新しました", + "renamedCategory": "クラス名を {{name}} に変更しました", + "reclassifiedImage": "画像の再分類に成功しました" + }, + "error": { + "deleteImageFailed": "削除に失敗しました: {{errorMessage}}", + "deleteCategoryFailed": "クラスの削除に失敗しました: {{errorMessage}}", + "deleteModelFailed": "モデルの削除に失敗しました: {{errorMessage}}", + "categorizeFailed": "画像の分類に失敗しました: {{errorMessage}}", + "trainingFailed": "モデルの学習に失敗しました。Frigate のログを確認してください。", + "trainingFailedToStart": "モデルの学習を開始できませんでした: {{errorMessage}}", + "updateModelFailed": "モデルの更新に失敗しました: {{errorMessage}}", + "renameCategoryFailed": "クラス名の変更に失敗しました: {{errorMessage}}", + "reclassifyFailed": "画像の再分類に失敗しました:{{errorMessage}}" + } + }, + "train": { + "titleShort": "最近の分類", + "title": "最近の分類結果", + "aria": "最近の分類結果を選択" + }, + "wizard": { + "step1": { + "typeObject": "オブジェクト", + "typeState": "状態", + "description": "状態モデルは固定カメラ領域の状態変化(例:ドアの開閉)を監視し、オブジェクトモデルは検知されたオブジェクトに分類(例:既知の動物や配達員など)を追加します。", + "name": "名前", + "namePlaceholder": "モデル名を入力...", + "type": "タイプ", + "objectLabel": "オブジェクトラベル", + "objectLabelPlaceholder": "オブジェクトタイプを選択...", + "classificationType": "分類タイプ", + "classificationTypeTip": "分類タイプについて", + "classificationTypeDesc": "サブラベルはオブジェクトのラベルに追加のテキストを追加します(例:「人: UPS」)。属性は、オブジェクトのメタデータとは別に保存される、検索可能なメタデータです。", + "classificationSubLabel": "サブラベル", + "classificationAttribute": "属性", + "classes": "クラス", + "states": "状態", + "classesTip": "クラスについて", + "classesStateDesc": "カメラ領域の状態を定義します。例: ガレージドアの「開」「閉」。", + "classesObjectDesc": "検知されたオブジェクトを分類するための、異なるカテゴリを定義します。例:人物の分類として「delivery_person」「resident」「stranger」など。", + "classPlaceholder": "クラス名を入力...", + "errors": { + "nameRequired": "モデル名は必須です", + "nameLength": "モデル名は 64 文字以内で入力してください", + "nameOnlyNumbers": "モデル名を数字のみにはできません", + "classRequired": "少なくとも 1 つのクラスが必要です", + "classesUnique": "クラス名は一意である必要があります", + "noneNotAllowed": "「none」というクラス名は使用できません", + "stateRequiresTwoClasses": "状態モデルには少なくとも 2 つのクラスが必要です", + "objectLabelRequired": "オブジェクトラベルを選択してください", + "objectTypeRequired": "分類タイプを選択してください" + } + }, + "title": "新しい分類を作成", + "steps": { + "nameAndDefine": "名前と定義", + "stateArea": "状態エリア", + "chooseExamples": "例を選択" + }, + "step2": { + "description": "カメラを選択し、それぞれの監視エリアを定義します。モデルはこれらのエリアの状態を分類します。", + "cameras": "カメラ", + "selectCamera": "カメラを選択", + "noCameras": "+ をクリックしてカメラを追加", + "selectCameraPrompt": "リストからカメラを選択して監視エリアを定義します" + }, + "step3": { + "selectImagesPrompt": "{{className}} の画像をすべて選択", + "selectImagesDescription": "画像をクリックして選択します。このクラスの作業が完了したら「続行」をクリックしてください。", + "allImagesRequired_other": "すべての画像を分類してください。残り {{count}} 枚です。", + "generating": { + "title": "サンプル画像を生成中", + "description": "Frigate が録画から代表的な画像を抽出しています。しばらくお待ちください..." + }, + "training": { + "title": "モデルを学習中", + "description": "モデルはバックグラウンドで学習されています。このダイアログを閉じると、学習完了後すぐにモデルが有効になります。" + }, + "retryGenerate": "再生成", + "noImages": "サンプル画像が生成されませんでした", + "classifying": "分類・学習中...", + "trainingStarted": "学習を開始しました", + "modelCreated": "モデルを作成しました。不足している状態の画像を「最近の分類」から追加し、モデルを学習してください。", + "errors": { + "noCameras": "カメラが設定されていません", + "noObjectLabel": "オブジェクトラベルが選択されていません", + "generateFailed": "例の生成に失敗しました: {{error}}", + "generationFailed": "生成に失敗しました。もう一度お試しください。", + "classifyFailed": "画像の分類に失敗しました: {{error}}" + }, + "generateSuccess": "サンプル画像を生成しました", + "missingStatesWarning": { + "title": "状態の例が不足しています", + "description": "最良の結果を得るため、すべての状態の例を選択することを推奨します。すべてを選択しなくても続行できますが、全状態に画像が揃うまでモデルは学習されません。続行後、「最近の分類」から不足分を分類し、学習を行ってください。" + }, + "refreshExamples": "新しい例を生成", + "refreshConfirm": { + "title": "新しい例を生成しますか?", + "description": "この操作により、新しい画像セットが生成され、選択済みのものも含めすべての選択がクリアされます。すべてのクラスについて、再度例を選び直す必要があります。" + } + } + }, + "details": { + "scoreInfo": "このスコアは、このオブジェクトに対するすべての検知結果の分類信頼度の平均を表します。", + "none": "なし", + "unknown": "不明" + }, + "tooltip": { + "trainingInProgress": "モデルは現在学習中です", + "noNewImages": "学習に使用できる新しい画像がありません。先にデータセット内の画像を分類してください。", + "noChanges": "前回の学習以降、データセットに変更はありません。", + "modelNotReady": "モデルはまだ学習可能な状態ではありません" + }, + "deleteCategory": { + "title": "クラスを削除", + "desc": "クラス {{name}} を削除してもよろしいですか?関連するすべての画像が完全に削除され、モデルの再学習が必要になります。", + "minClassesTitle": "クラスを削除できません", + "minClassesDesc": "分類モデルには少なくとも 2 つのクラスが必要です。別のクラスを追加してから削除してください。" + }, + "deleteModel": { + "title": "分類モデルを削除", + "single": "{{name}} を削除してもよろしいですか?画像や学習データを含むすべての関連データが完全に削除され、この操作は元に戻せません。", + "desc_other": "{{count}} 件のモデルを削除してもよろしいですか?関連するすべてのデータが完全に削除され、この操作は元に戻せません。" + }, + "edit": { + "title": "分類モデルを編集", + "descriptionState": "この状態分類モデルのクラスを編集します。変更を反映するにはモデルの再学習が必要です。", + "descriptionObject": "このオブジェクト分類モデルのオブジェクトタイプおよび分類タイプを編集します。", + "stateClassesInfo": "注意: 状態クラスを変更すると、更新後のクラスでモデルを再学習する必要があります。" + }, + "deleteDatasetImages": { + "title": "データセット画像を削除", + "desc_other": "{{dataset}} から {{count}} 枚の画像を削除してもよろしいですか?この操作は元に戻せず、モデルの再学習が必要になります。" + }, + "deleteTrainImages": { + "title": "学習用画像を削除", + "desc_other": "{{count}} 枚の画像を削除してもよろしいですか?この操作は元に戻すことができません。" + }, + "renameCategory": { + "title": "クラス名を変更", + "desc": "{{name}} の新しい名前を入力してください。変更を有効にするにはモデルの再学習が必要です。" + }, + "description": { + "invalidName": "無効な名前です。使用できるのは、英数字、空白、アポストロフィ、アンダースコア、ハイフンのみです。" + }, + "categories": "クラス", + "createCategory": { + "new": "新しいクラスを作成" + }, + "categorizeImageAs": "画像を次として分類:", + "categorizeImage": "画像を分類", + "menu": { + "objects": "オブジェクト", + "states": "状態" + }, + "noModels": { + "object": { + "title": "オブジェクト分類モデルがありません", + "description": "検知されたオブジェクトを分類するためのカスタムモデルを作成します。", + "buttonText": "オブジェクトモデルを作成" + }, + "state": { + "title": "状態分類モデルがありません", + "description": "特定のカメラ領域の状態変化を監視・分類するためのカスタムモデルを作成します。", + "buttonText": "状態モデルを作成" + } + }, + "reclassifyImageAs": "画像を次として再分類:", + "reclassifyImage": "画像を再分類" +} diff --git a/web/public/locales/ja/views/configEditor.json b/web/public/locales/ja/views/configEditor.json new file mode 100644 index 0000000..704c83d --- /dev/null +++ b/web/public/locales/ja/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "copyConfig": "設定をコピー", + "configEditor": "設定エディタ", + "saveAndRestart": "保存後再起動", + "saveOnly": "保存", + "confirm": "保存せずに終了しますか?", + "documentTitle": "設定エディタ - Frigate", + "safeConfigEditor": "設定エディタ (セーフモード)", + "safeModeDescription": "Frigate は config の検証エラーによるセーフモードです.", + "toast": { + "success": { + "copyToClipboard": "コンフィグをクリップボードにコピー。" + }, + "error": { + "savingError": "設定の保存に失敗しました" + } + } +} diff --git a/web/public/locales/ja/views/events.json b/web/public/locales/ja/views/events.json new file mode 100644 index 0000000..13dc92b --- /dev/null +++ b/web/public/locales/ja/views/events.json @@ -0,0 +1,94 @@ +{ + "detections": "検知", + "motion": { + "label": "モーション", + "only": "モーションのみ" + }, + "alerts": "アラート", + "empty": { + "detection": "レビューする検知はありません", + "alert": "レビューするアラートはありません", + "motion": "モーションデータは見つかりません", + "recordingsDisabled": { + "title": "録画を有効にする必要があります", + "description": "カメラの録画が有効になっている場合にのみ、そのカメラに対してレビューアイテムを作成できます。" + } + }, + "camera": "カメラ", + "allCameras": "全カメラ", + "timeline": { + "label": "タイムライン" + }, + "timeline.aria": "タイムラインを選択", + "events": { + "label": "イベント", + "aria": "イベントを選択", + "noFoundForTimePeriod": "この期間のイベントは見つかりません。" + }, + "documentTitle": "レビュー - Frigate", + "recordings": { + "documentTitle": "録画 - Frigate", + "invalidSharedLink": "解析エラーのため、タイムスタンプ付きの録画リンクを開くことができません。", + "invalidSharedCamera": "不明または未承認のカメラのため、タイムスタンプ付き録画のリンクを開くことができません。" + }, + "calendarFilter": { + "last24Hours": "直近24時間" + }, + "markAsReviewed": "レビュー済みにする", + "markTheseItemsAsReviewed": "これらの項目をレビュー済みにする", + "newReviewItems": { + "label": "新しいレビュー項目を表示", + "button": "レビューすべき新規項目" + }, + "selected_one": "{{count}} 選択済み", + "selected_other": "{{count}} 選択済み", + "detected": "検知", + "suspiciousActivity": "不審なアクティビティ", + "threateningActivity": "脅威となるアクティビティ", + "zoomIn": "ズームイン", + "zoomOut": "ズームアウト", + "detail": { + "label": "詳細", + "noDataFound": "確認する詳細データはありません", + "aria": "詳細表示を切り替え", + "trackedObject_one": "{{count}} 件のオブジェクト", + "trackedObject_other": "{{count}} 件のオブジェクト", + "noObjectDetailData": "オブジェクトの詳細データがありません。", + "settings": "詳細表示設定", + "alwaysExpandActive": { + "title": "アクティブ項目を常に展開", + "desc": "利用可能な場合、アクティブなレビュー項目のオブジェクト詳細を常に展開する。" + } + }, + "objectTrack": { + "trackedPoint": "追跡ポイント", + "clickToSeek": "クリックしてこの時点に移動" + }, + "select_all": "すべて", + "normalActivity": "通常", + "needsReview": "要確認", + "securityConcern": "セキュリティ上の懸念", + "motionSearch": { + "menuItem": "モーション検索", + "openMenu": "カメラオプション" + }, + "motionPreviews": { + "menuItem": "モーションプレビューを表示", + "title": "モーションプレビュー: {{camera}}", + "mobileSettingsTitle": "モーションプレビュー設定", + "mobileSettingsDesc": "再生速度と暗転具合を調整し、確認したい日付を選んでモーションのみのクリップを再生します。", + "dim": "暗転", + "dimAria": "暗転の強さを調整", + "dimDesc": "暗転を強くするとモーション領域が見やすくなります。", + "speed": "速度", + "speedAria": "プレビューの再生速度を選択", + "speedDesc": "プレビュークリップの再生速度を選びます。", + "back": "戻る", + "empty": "利用可能なプレビューがありません", + "noPreview": "プレビューを利用できません", + "seekAria": "{{camera}} のプレーヤーを {{time}} までシーク", + "filter": "フィルター", + "filterDesc": "領域を選択すると、その領域でモーションが発生したクリップのみが表示されます。", + "filterClear": "クリア" + } +} diff --git a/web/public/locales/ja/views/explore.json b/web/public/locales/ja/views/explore.json new file mode 100644 index 0000000..6b9ab4c --- /dev/null +++ b/web/public/locales/ja/views/explore.json @@ -0,0 +1,313 @@ +{ + "generativeAI": "生成AI", + "documentTitle": "探索 - Frigate", + "details": { + "timestamp": "タイムスタンプ", + "item": { + "title": "レビュー項目の詳細", + "desc": "レビュー項目の詳細", + "button": { + "share": "このレビュー項目を共有", + "viewInExplore": "探索で表示" + }, + "tips": { + "mismatch_other": "利用不可のオブジェクトが {{count}} 件、このレビュー項目に含まれています。これらはアラートまたは検知の条件を満たしていないか、既にクリーンアップ/削除されています。", + "hasMissingObjects": "次のラベルの追跡オブジェクトを保存したい場合は設定を調整してください: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "{{provider}} に新しい説明をリクエストしました。プロバイダの速度により再生成に時間がかかる場合があります。", + "updatedSublabel": "サブラベルを更新しました。", + "updatedLPR": "ナンバープレートを更新しました。", + "audioTranscription": "音声文字起こしのリクエストは正常に送信されました。Frigate サーバーの処理速度によっては、文字起こしの完了までにしばらく時間がかかる場合があります。", + "updatedAttributes": "属性が正常に更新されました。" + }, + "error": { + "regenerate": "{{provider}} への新しい説明の呼び出しに失敗しました: {{errorMessage}}", + "updatedSublabelFailed": "サブラベルの更新に失敗しました: {{errorMessage}}", + "updatedLPRFailed": "ナンバープレートの更新に失敗しました: {{errorMessage}}", + "audioTranscription": "音声文字起こしのリクエストに失敗しました: {{errorMessage}}", + "updatedAttributesFailed": "属性の更新に失敗しました: {{errorMessage}}" + } + } + }, + "label": "ラベル", + "editSubLabel": { + "title": "サブラベルを編集", + "desc": "この {{label}} の新しいサブラベルを入力", + "descNoLabel": "この追跡オブジェクトの新しいサブラベルを入力" + }, + "editLPR": { + "title": "ナンバープレートを編集", + "desc": "この {{label}} の新しいナンバープレート値を入力", + "descNoLabel": "この追跡オブジェクトの新しいナンバープレート値を入力" + }, + "snapshotScore": { + "label": "スナップショットスコア" + }, + "topScore": { + "label": "トップスコア", + "info": "トップスコアは追跡オブジェクトの最高中央値スコアであり、検索結果のサムネイルに表示されるスコアとは異なる場合があります。" + }, + "score": { + "label": "スコア" + }, + "recognizedLicensePlate": "認識されたナンバープレート", + "estimatedSpeed": "推定速度", + "objects": "オブジェクト", + "camera": "カメラ", + "zones": "ゾーン", + "button": { + "findSimilar": "類似を検索", + "regenerate": { + "title": "再生成", + "label": "追跡オブジェクトの説明を再生成" + } + }, + "description": { + "label": "説明", + "placeholder": "追跡オブジェクトの説明", + "aiTips": "追跡オブジェクトのライフサイクルが終了するまで、生成AIプロバイダに説明はリクエストされません。" + }, + "expandRegenerationMenu": "再生成メニューを展開", + "regenerateFromSnapshot": "スナップショットから再生成", + "regenerateFromThumbnails": "サムネイルから再生成", + "tips": { + "descriptionSaved": "説明を保存しました", + "saveDescriptionFailed": "説明の更新に失敗しました: {{errorMessage}}" + }, + "editAttributes": { + "title": "属性を編集", + "desc": "この {{label}} の分類属性を選択してください" + }, + "attributes": "分類属性", + "title": { + "label": "タイトル" + }, + "scoreInfo": "スコア情報" + }, + "exploreMore": "{{label}} のオブジェクトをさらに探索", + "exploreIsUnavailable": { + "title": "探索は利用できません", + "embeddingsReindexing": { + "context": "追跡オブジェクトの埋め込みの再インデックスが完了すると「探索」を使用できます。", + "startingUp": "起動中…", + "estimatedTime": "残りの推定時間:", + "finishingShortly": "まもなく完了", + "step": { + "thumbnailsEmbedded": "埋め込み済みサムネイル: ", + "descriptionsEmbedded": "埋め込み済み説明: ", + "trackedObjectsProcessed": "処理済み追跡オブジェクト: " + } + }, + "downloadingModels": { + "context": "Frigate はセマンティック検索(意味理解型画像検索)をサポートするために必要な埋め込みモデルをダウンロードしています。ネットワーク速度により数分かかる場合があります。", + "setup": { + "visionModel": "ビジョンモデル", + "visionModelFeatureExtractor": "ビジョンモデル特徴抽出器", + "textModel": "テキストモデル", + "textTokenizer": "テキストトークナイザー" + }, + "tips": { + "context": "モデルのダウンロード後、追跡オブジェクトの埋め込みを再インデックスすることを検討してください。" + }, + "error": "エラーが発生しました。Frigate のログを確認してください。" + } + }, + "trackedObjectDetails": "追跡オブジェクトの詳細", + "type": { + "details": "詳細", + "snapshot": "スナップショット", + "video": "動画", + "object_lifecycle": "オブジェクトのライフサイクル", + "thumbnail": "サムネイル", + "tracking_details": "追跡詳細" + }, + "objectLifecycle": { + "title": "オブジェクトのライフサイクル", + "noImageFound": "このタイムスタンプの画像は見つかりません。", + "createObjectMask": "オブジェクトマスクを作成", + "adjustAnnotationSettings": "アノテーション設定を調整", + "scrollViewTips": "スクロールしてこのオブジェクトのライフサイクルの重要な瞬間を表示します。", + "autoTrackingTips": "オートトラッキングカメラではバウンディングボックスの位置が正確でない場合があります。", + "count": "{{first}} / {{second}}", + "trackedPoint": "追跡ポイント", + "lifecycleItemDesc": { + "visible": "{{label}} を検出", + "entered_zone": "{{label}} が {{zones}} に進入", + "active": "{{label}} がアクティブになりました", + "stationary": "{{label}} が静止しました", + "attribute": { + "faceOrLicense_plate": "{{label}} の {{attribute}} を検出", + "other": "{{label}} を {{attribute}} として認識" + }, + "gone": "{{label}} が離脱", + "heard": "{{label}} を検知(音声)", + "external": "{{label}} を検出", + "header": { + "zones": "ゾーン", + "ratio": "比率", + "area": "面積" + } + }, + "annotationSettings": { + "title": "アノテーション設定", + "showAllZones": { + "title": "すべてのゾーンを表示", + "desc": "オブジェクトがゾーンに入ったフレームでは常にゾーンを表示します。" + }, + "offset": { + "label": "アノテーションオフセット", + "desc": "このデータはカメラの detect フィードから来ていますが、record フィードの画像に重ねて表示されます。2つのストリームが完全に同期していない可能性があるため、バウンディングボックスと映像が完全には一致しないことがあります。annotation_offset フィールドで調整できます。", + "millisecondsToOffset": "detect のアノテーションをオフセットするミリ秒数。既定: 0", + "tips": "ヒント: 左から右へ歩く人物のイベントクリップを想像してください。タイムラインのバウンディングボックスが人物より常に左側にあるなら値を小さく、常に先行しているなら値を大きくします。", + "toast": { + "success": "{{camera}} のアノテーションオフセットを設定ファイルに保存しました。変更を適用するには Frigate を再起動してください。" + } + } + }, + "carousel": { + "previous": "前のスライド", + "next": "次のスライド" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "動画をダウンロード", + "aria": "動画をダウンロード" + }, + "downloadSnapshot": { + "label": "スナップショットをダウンロード", + "aria": "スナップショットをダウンロード" + }, + "viewObjectLifecycle": { + "label": "オブジェクトのライフサイクルを表示", + "aria": "オブジェクトのライフサイクルを表示" + }, + "findSimilar": { + "label": "類似を検索", + "aria": "類似する追跡オブジェクトを検索" + }, + "addTrigger": { + "label": "トリガーを追加", + "aria": "この追跡オブジェクトのトリガーを追加" + }, + "audioTranscription": { + "label": "文字起こし", + "aria": "音声文字起こしをリクエスト" + }, + "submitToPlus": { + "label": "Frigate+ に送信", + "aria": "Frigate Plus に送信" + }, + "viewInHistory": { + "label": "履歴で表示", + "aria": "履歴で表示" + }, + "deleteTrackedObject": { + "label": "この追跡オブジェクトを削除" + }, + "downloadCleanSnapshot": { + "label": "クリーンなスナップショットをダウンロード", + "aria": "クリーンなスナップショットをダウンロード" + }, + "viewTrackingDetails": { + "label": "追跡詳細を表示", + "aria": "追跡詳細を表示" + }, + "showObjectDetails": { + "label": "オブジェクトの移動経路を表示" + }, + "hideObjectDetails": { + "label": "オブジェクトの移動経路を非表示" + }, + "debugReplay": { + "label": "デバッグリプレイ", + "aria": "この追跡オブジェクトをデバッグリプレイビューで表示" + }, + "more": { + "aria": "その他" + } + }, + "dialog": { + "confirmDelete": { + "title": "削除の確認", + "desc": "この追跡オブジェクトを削除すると、スナップショット、保存された埋め込み、および関連する追跡詳細項目が削除されます。履歴ビューの録画映像は削除されません。

    続行してもよろしいですか?" + }, + "toast": { + "error": "この追跡オブジェクトの削除に失敗しました: {{errorMessage}}" + } + }, + "noTrackedObjects": "追跡オブジェクトは見つかりませんでした", + "fetchingTrackedObjectsFailed": "追跡オブジェクトの取得エラー: {{errorMessage}}", + "trackedObjectsCount_other": "{{count}} 件の追跡オブジェクト ", + "searchResult": { + "tooltip": "{{type}} と一致({{confidence}}%)", + "deleteTrackedObject": { + "toast": { + "success": "追跡オブジェクトを削除しました。", + "error": "追跡オブジェクトの削除に失敗しました: {{errorMessage}}" + } + }, + "previousTrackedObject": "前の追跡オブジェクト", + "nextTrackedObject": "次の追跡オブジェクト" + }, + "aiAnalysis": { + "title": "AI 解析" + }, + "concerns": { + "label": "懸念" + }, + "trackingDetails": { + "title": "追跡詳細", + "noImageFound": "このタイムスタンプに対応する画像が見つかりません。", + "createObjectMask": "オブジェクトマスクを作成", + "adjustAnnotationSettings": "注釈設定を調整", + "scrollViewTips": "クリックして、このオブジェクトのライフサイクルにおける重要な瞬間を表示します。", + "autoTrackingTips": "自動追跡カメラでは、バウンディングボックスの位置が不正確になる場合があります。", + "count": "{{second}} 件中 {{first}} 件目", + "trackedPoint": "追跡ポイント", + "lifecycleItemDesc": { + "visible": "{{label}} が検知されました", + "entered_zone": "{{label}} が {{zones}} に入りました", + "active": "{{label}} がアクティブになりました", + "stationary": "{{label}} が静止状態になりました", + "attribute": { + "faceOrLicense_plate": "{{label}} に {{attribute}} が検知されました", + "other": "{{label}} は {{attribute}} と認識されました" + }, + "gone": "{{label}} が離脱しました", + "heard": "{{label}} の音が検知されました", + "external": "{{label}} が検知されました", + "header": { + "zones": "ゾーン", + "ratio": "比率", + "area": "面積", + "score": "スコア", + "computedScore": "計算スコア", + "topScore": "トップスコア", + "toggleAdvancedScores": "詳細スコアを切替" + } + }, + "annotationSettings": { + "title": "注釈設定", + "showAllZones": { + "title": "すべてのゾーンを表示", + "desc": "オブジェクトがゾーンに入ったフレームでは常にゾーンを表示します。" + }, + "offset": { + "label": "注釈オフセット", + "millisecondsToOffset": "検知アノテーションをオフセットするミリ秒数です。デフォルト: 0", + "toast": { + "success": "{{camera}} のアノテーションオフセットが設定ファイルに保存されました。" + }, + "desc": "このデータはカメラの detect ストリーム から取得されていますが、表示される画像自体は record ストリーム のものです。そのため、2 つのストリームが完全に同期している可能性は低く、バウンディングボックスと実際の映像が正確に一致しない場合があります。この設定を使用すると、注釈(アノテーション)を 時間的に前後へオフセット することができ、録画映像との位置合わせをより正確に行えます。", + "tips": "映像の再生がバウンディングボックスや軌跡ポイントより先行している場合は値を小さくし、遅れている場合は値を大きくしてください。この値は負の値も指定できます。" + } + }, + "carousel": { + "previous": "前のスライド", + "next": "次のスライド" + } + } +} diff --git a/web/public/locales/ja/views/exports.json b/web/public/locales/ja/views/exports.json new file mode 100644 index 0000000..5cff546 --- /dev/null +++ b/web/public/locales/ja/views/exports.json @@ -0,0 +1,128 @@ +{ + "documentTitle": "エクスポート - Frigate", + "noExports": "エクスポートが見つかりません", + "search": "検索", + "deleteExport": { + "label": "エクスポートを削除" + }, + "deleteExport.desc": "{{exportName}} を削除してもよろしいですか?", + "editExport": { + "title": "エクスポート名を変更", + "desc": "このエクスポートの新しい名前を入力してください。", + "saveExport": "エクスポートを保存" + }, + "toast": { + "error": { + "renameExportFailed": "エクスポート名の変更に失敗しました: {{errorMessage}}", + "assignCaseFailed": "ケース割り当ての更新に失敗しました: {{errorMessage}}", + "caseSaveFailed": "ケースの保存に失敗しました: {{errorMessage}}", + "caseDeleteFailed": "ケースの削除に失敗しました: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "エクスポートを共有", + "downloadVideo": "動画をダウンロード", + "editName": "名前を編集", + "deleteExport": "エクスポートを削除", + "assignToCase": "ケースに追加", + "removeFromCase": "ケースから削除" + }, + "headings": { + "cases": "ケース", + "uncategorizedExports": "未分類のエクスポート" + }, + "toolbar": { + "newCase": "新しいケース", + "addExport": "エクスポートに追加", + "editCase": "ケースを編集", + "deleteCase": "ケースを削除" + }, + "deleteCase": { + "label": "ケースを削除", + "desc": "本当に {{caseName}} を削除しますか ?", + "descKeepExports": "エクスポートは、分類されていないエクスポートとして引き続き利用可能です。", + "descDeleteExports": "この場合、すべてのエクスポートは完全に削除されます。", + "deleteExports": "エクスポートも削除する" + }, + "caseDialog": { + "title": "ケースに追加", + "description": "既存のケースを選択するか、新しいケースを作成してください。", + "selectLabel": "ケース", + "newCaseOption": "新しいケースを作成", + "nameLabel": "ケース名", + "descriptionLabel": "説明" + }, + "caseCard": { + "emptyCase": "まだエクスポートされていません" + }, + "jobCard": { + "defaultName": "{{camera}} エクスポート", + "queued": "キューに追加しました", + "running": "実行中", + "preparing": "準備中", + "copying": "コピー中", + "encoding": "エンコード中", + "encodingRetry": "エンコード中 (再試行)", + "finalizing": "終了処理中" + }, + "caseView": { + "noDescription": "説明がありません", + "exportCount_one": "1 件のエクスポート", + "exportCount_other": "{{count}} エクスポート", + "cameraCount_other": "{{count}} 台のカメラ", + "showMore": "さらに表示", + "showLess": "表示を減らす", + "emptyTitle": "このケースは空です", + "emptyDescription": "既存の分類されていないエクスポートを追加して、ケースを整理しましょう。", + "emptyDescriptionNoExports": "まだ追加可能な未分類のエクスポートはありません。", + "createdAt": "作成日 {{value}}", + "cameraCount_one": "1 台のカメラ" + }, + "caseEditor": { + "createTitle": "ケースを作成", + "editTitle": "ケースを編集", + "namePlaceholder": "ケース名", + "descriptionPlaceholder": "このケースに関するメモや背景情報を追加する" + }, + "addExportDialog": { + "title": "{{caseName}} にエクスポートを追加", + "searchPlaceholder": "未分類のエクスポートを検索", + "empty": "この検索条件に一致する未分類のエクスポートはありません。", + "addButton_one": "1 件のエクスポートを追加", + "addButton_other": "{{count}} 件のエクスポートを追加", + "adding": "追加中..." + }, + "selected_one": "{{count}} 選択済み", + "selected_other": "{{count}} 選択済み", + "bulkActions": { + "addToCase": "ケースに追加", + "moveToCase": "ケースに移動", + "removeFromCase": "ケースから削除", + "delete": "削除", + "deleteNow": "今すぐ削除" + }, + "bulkDelete": { + "title": "エクスポートを削除", + "desc_one": "{{count}} 件のエクスポートを削除してもよろしいですか?", + "desc_other": "{{count}} 件のエクスポートを削除してもよろしいですか?" + }, + "bulkRemoveFromCase": { + "title": "ケースから削除", + "desc_one": "このケースから {{count}} 件のエクスポートを削除しますか?", + "desc_other": "このケースから {{count}} 件のエクスポートを削除しますか?", + "descKeepExports": "エクスポートは未分類に移動されます。", + "descDeleteExports": "エクスポートは完全に削除されます。", + "deleteExports": "代わりにエクスポートを削除する" + }, + "bulkToast": { + "success": { + "delete": "エクスポートの削除に成功しました", + "reassign": "ケース割り当ての更新に成功しました", + "remove": "ケースからエクスポートを正常に削除しました" + }, + "error": { + "deleteFailed": "エクスポートの削除に失敗しました: {{errorMessage}}", + "reassignFailed": "ケース割り当ての更新に失敗しました: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/ja/views/faceLibrary.json b/web/public/locales/ja/views/faceLibrary.json new file mode 100644 index 0000000..8a79b4a --- /dev/null +++ b/web/public/locales/ja/views/faceLibrary.json @@ -0,0 +1,105 @@ +{ + "description": { + "placeholder": "このコレクションの名前を入力", + "addFace": "最初の画像をアップロードして、フェイスライブラリに新しいコレクションを追加してください。", + "invalidName": "無効な名前です。使用できるのは、英数字、空白、アポストロフィ、アンダースコア、ハイフンのみです。", + "nameCannotContainHash": "名前に # は使用できません。" + }, + "details": { + "person": "人物", + "face": "顔の詳細", + "timestamp": "タイムスタンプ", + "unknown": "不明", + "subLabelScore": "サブラベルスコア", + "scoreInfo": "サブラベルスコアは、認識された顔の信頼度の加重スコアです。スナップショットに表示されるスコアとは異なる場合があります。", + "faceDesc": "この顔を生成した追跡オブジェクトの詳細" + }, + "documentTitle": "顔データベース - Frigate", + "uploadFaceImage": { + "title": "顔画像をアップロード", + "desc": "顔を検知するために画像をアップロードし、{{pageToggle}} に追加します" + }, + "collections": "コレクション", + "createFaceLibrary": { + "title": "コレクションを作成", + "desc": "新しいコレクションを作成", + "new": "新しい顔を作成", + "nextSteps": "強固な基盤を作るために:
  • [過去の学習]タブで各人物に対して画像を選択し学習させてください。
  • 最良の結果のため、正面を向いた画像に集中し、斜めからの顔画像は学習に使わないでください。
  • " + }, + "selectItem": "{{item}} を選択", + "steps": { + "faceName": "顔の名前を入力", + "uploadFace": "顔画像をアップロード", + "nextSteps": "次のステップ", + "description": { + "uploadFace": "{{name}} の正面を向いた顔が写っている画像をアップロードしてください。顔部分だけにトリミングする必要はありません。" + } + }, + "train": { + "title": "過去の学習", + "aria": "過去の学習を選択", + "empty": "最近の顔認識の試行はありません", + "titleShort": "最近の分類", + "emptyNoLibrary": { + "title": "顔画像をアップロード", + "description": "顔認識を機能させるには、ライブラリに少なくとも 1 つの顔を追加する必要があります。" + } + }, + "selectFace": "顔を選択", + "deleteFaceLibrary": { + "title": "名前を削除", + "desc": "コレクション {{name}} を削除してもよろしいですか?関連する顔はすべて完全に削除されます。" + }, + "deleteFaceAttempts": { + "title": "顔を削除", + "desc_other": "{{count}} 件の顔を削除してもよろしいですか?この操作は元に戻せません。" + }, + "renameFace": { + "title": "顔の名前を変更", + "desc": "{{name}} の新しい名前を入力" + }, + "button": { + "deleteFaceAttempts": "顔を削除", + "addFace": "顔を追加", + "renameFace": "顔の名前を変更", + "deleteFace": "顔を削除", + "uploadImage": "画像をアップロード", + "reprocessFace": "顔を再処理" + }, + "imageEntry": { + "validation": { + "selectImage": "画像ファイルを選択してください。" + }, + "dropActive": "ここに画像をドロップ…", + "dropInstructions": "画像をここにドラッグ&ドロップ、ペースト、またはクリックして選択", + "maxSize": "最大サイズ: {{size}}MB" + }, + "nofaces": "顔はありません", + "pixels": "{{area}}px", + "trainFaceAs": "顔を次として学習:", + "trainFace": "顔を学習", + "toast": { + "success": { + "uploadedImage": "画像をアップロードしました。", + "addFaceLibrary": "{{name}} を顔データベースに追加しました!", + "deletedFace_other": "{{count}} 件の顔を削除しました。", + "deletedName_other": "{{count}} 件の顔を削除しました。", + "renamedFace": "顔の名前を {{name}} に変更しました", + "trainedFace": "顔の学習が完了しました。", + "updatedFaceScore": "顔のスコアを {{name}} ({{score}})に更新しました。", + "reclassifiedFace": "顔を再分類しました。" + }, + "error": { + "uploadingImageFailed": "画像のアップロードに失敗しました: {{errorMessage}}", + "addFaceLibraryFailed": "顔名の設定に失敗しました: {{errorMessage}}", + "deleteFaceFailed": "削除に失敗しました: {{errorMessage}}", + "deleteNameFailed": "名前の削除に失敗しました: {{errorMessage}}", + "renameFaceFailed": "顔の名前変更に失敗しました: {{errorMessage}}", + "trainFailed": "学習に失敗しました: {{errorMessage}}", + "updateFaceScoreFailed": "顔スコアの更新に失敗しました: {{errorMessage}}", + "reclassifyFailed": "顔の再分類に失敗しました: {{errorMessage}}" + } + }, + "reclassifyFaceAs": "顔を再分類する:", + "reclassifyFace": "顔の再分類" +} diff --git a/web/public/locales/ja/views/live.json b/web/public/locales/ja/views/live.json new file mode 100644 index 0000000..0fe4e7b --- /dev/null +++ b/web/public/locales/ja/views/live.json @@ -0,0 +1,204 @@ +{ + "documentTitle": { + "default": "ライブ - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - ライブ - Frigate", + "lowBandwidthMode": "低帯域モード", + "twoWayTalk": { + "enable": "双方向通話を有効化", + "disable": "双方向通話を無効化" + }, + "cameraAudio": { + "enable": "カメラ音声を有効化", + "disable": "カメラ音声を無効化" + }, + "ptz": { + "move": { + "clickMove": { + "label": "フレーム内をクリックしてカメラを中央に移動", + "enable": "クリック移動を有効化", + "disable": "クリック移動を無効化", + "enableWithZoom": "クリックで移動、ドラッグでズームを有効にする" + }, + "left": { + "label": "PTZ カメラを左へ移動" + }, + "up": { + "label": "PTZ カメラを上へ移動" + }, + "down": { + "label": "PTZ カメラを下へ移動" + }, + "right": { + "label": "PTZ カメラを右へ移動" + } + }, + "zoom": { + "in": { + "label": "PTZ カメラをズームイン" + }, + "out": { + "label": "PTZ カメラをズームアウト" + } + }, + "focus": { + "in": { + "label": "PTZ カメラをフォーカスイン" + }, + "out": { + "label": "PTZ カメラをフォーカスアウト" + } + }, + "frame": { + "center": { + "label": "フレーム内をクリックして PTZ カメラを中央へ" + } + }, + "presets": "PTZ カメラのプリセット" + }, + "camera": { + "enable": "カメラを有効化", + "disable": "カメラを無効化", + "turnOn": "カメラをオンにする", + "turnOff": "カメラをオフにする" + }, + "muteCameras": { + "enable": "全カメラをミュート", + "disable": "全カメラのミュートを解除" + }, + "detect": { + "enable": "検知を有効化", + "disable": "検知を無効化" + }, + "recording": { + "enable": "録画を有効化", + "disable": "録画を無効化", + "disabledInConfig": "このカメラでは、まず「設定」で録画機能を有効にする必要があります。" + }, + "snapshots": { + "enable": "スナップショットを有効化", + "disable": "スナップショットを無効化" + }, + "audioDetect": { + "enable": "音声検知を有効化", + "disable": "音声検知を無効化" + }, + "transcription": { + "enable": "ライブ音声文字起こしを有効化", + "disable": "ライブ音声文字起こしを無効化" + }, + "autotracking": { + "enable": "オートトラッキングを有効化", + "disable": "オートトラッキングを無効化" + }, + "streamStats": { + "enable": "ストリーム統計を表示", + "disable": "ストリーム統計を非表示" + }, + "manualRecording": { + "title": "オンデマンド録画", + "tips": "このカメラの録画保持設定に基づいて、即時スナップショットをダウンロードするか、手動イベントを開始してください。", + "playInBackground": { + "label": "バックグラウンドで再生", + "desc": "プレーヤーが非表示の場合でもストリーミングを継続するにはこのオプションを有効にします。" + }, + "showStats": { + "label": "統計を表示", + "desc": "カメラ映像にストリーム統計をオーバーレイ表示するにはこのオプションを有効にします。" + }, + "debugView": "デバッグビュー", + "start": "オンデマンド録画を開始", + "started": "手動のオンデマンド録画を開始しました。", + "failedToStart": "手動のオンデマンド録画の開始に失敗しました。", + "recordDisabledTips": "このカメラは設定で録画が無効または制限されているため、スナップショットのみ保存されます。", + "end": "オンデマンド録画を終了", + "ended": "手動のオンデマンド録画を終了しました。", + "failedToEnd": "手動のオンデマンド録画の終了に失敗しました。" + }, + "streamingSettings": "ストリーミング設定", + "notifications": "通知", + "audio": "音声", + "suspend": { + "forTime": "一時停止: " + }, + "stream": { + "title": "ストリーム", + "audio": { + "tips": { + "title": "このストリームで音声を使用するには、カメラから音声が出力され、go2rtc で設定されている必要があります。" + }, + "available": "このストリームでは音声を利用できます", + "unavailable": "このストリームでは音声は利用できません" + }, + "twoWayTalk": { + "tips": "端末が機能をサポートし、双方向通話に WebRTC が設定されている必要があります。", + "available": "このストリームで双方向通話を利用できます", + "unavailable": "このストリームで双方向通話は利用できません" + }, + "lowBandwidth": { + "tips": "バッファリングやストリームエラーのため、ライブビューは低帯域モードになっています。", + "resetStream": "ストリームをリセット" + }, + "playInBackground": { + "label": "バックグラウンドで再生", + "tips": "プレーヤーが非表示でもストリーミングを継続するにはこのオプションを有効にします。" + }, + "debug": { + "picker": "デバッグモードではストリームの選択はできません。デバッグビューは常に 検知ロールに割り当てられたストリームを使用します。" + } + }, + "cameraSettings": { + "title": "{{camera}} の設定", + "cameraEnabled": "カメラ有効", + "objectDetection": "物体検知", + "recording": "録画", + "snapshots": "スナップショット", + "audioDetection": "音声検知", + "transcription": "音声文字起こし", + "autotracking": "オートトラッキング", + "camera": "カメラ" + }, + "history": { + "label": "履歴映像を表示" + }, + "effectiveRetainMode": { + "modes": { + "all": "すべて", + "motion": "モーション", + "active_objects": "アクティブなオブジェクト" + }, + "notAllTips": "{{source}} の録画保持設定は mode: {{effectiveRetainMode}} になっているため、このオンデマンド録画では {{effectiveRetainModeName}} を含むセグメントのみが保持されます。" + }, + "editLayout": { + "label": "レイアウトを編集", + "group": { + "label": "カメラグループを編集" + }, + "exitEdit": "編集を終了" + }, + "noCameras": { + "title": "カメラが設定されていません", + "buttonText": "カメラを追加", + "description": "開始するには、Frigateにカメラを接続してください。", + "restricted": { + "title": "利用可能なカメラがありません", + "description": "このグループ内のカメラを表示する権限がありません。" + }, + "default": { + "title": "設定済みのカメラがありません", + "description": "Frigate にカメラを接続して開始しましょう。", + "buttonText": "カメラを追加" + }, + "group": { + "title": "このグループにカメラがありません", + "description": "このカメラグループには、割り当て済みまたは有効なカメラがありません。", + "buttonText": "グループを管理" + } + }, + "snapshot": { + "takeSnapshot": "即時スナップショットをダウンロード", + "noVideoSource": "スナップショットに使用できる映像ソースがありません。", + "captureFailed": "スナップショットの取得に失敗しました。", + "downloadStarted": "スナップショットのダウンロードを開始しました。" + } +} diff --git a/web/public/locales/ja/views/motionSearch.json b/web/public/locales/ja/views/motionSearch.json new file mode 100644 index 0000000..cc64882 --- /dev/null +++ b/web/public/locales/ja/views/motionSearch.json @@ -0,0 +1,78 @@ +{ + "documentTitle": "モーション検索 - Frigate", + "title": "モーション検索", + "description": "関心領域を定義する多角形を描画し、その領域内で動きの変化を検索する時間範囲を指定します。", + "selectCamera": "モーション検索を読み込み中です", + "dialog": { + "title": "モーション検索", + "cameraLabel": "カメラ", + "previewAlt": "{{camera}} のカメラプレビュー" + }, + "startSearch": "検索開始", + "searchStarted": "検索を開始しました", + "searchCancelled": "検索がキャンセルされました", + "cancelSearch": "キャンセル", + "searching": "検索中です。", + "searchComplete": "検索完了", + "noResultsYet": "選択した領域内の動きの変化を検索します", + "noChangesFound": "選択した領域でピクセルの変化は検知されませんでした", + "changesFound_other": "{{count}} 件の動きの変化が見つかりました", + "framesProcessed": "{{count}} フレームを処理しました", + "jumpToTime": "この時間に移動", + "results": "結果", + "showSegmentHeatmap": "ヒートマップ", + "newSearch": "新規検索", + "clearResults": "結果をクリア", + "clearROI": "ポリゴンをクリア", + "polygonControls": { + "points_other": "{{count}} ポイント", + "undo": "直前のポイントを元に戻す", + "reset": "ポリゴンをリセット", + "drawMode": "描画", + "moveMode": "移動" + }, + "motionHeatmapLabel": "モーションヒートマップ", + "timeRange": { + "title": "検索範囲", + "start": "開始時間", + "end": "終了時間" + }, + "settings": { + "title": "検索設定", + "parallelMode": "並列モード", + "parallelModeDesc": "複数の録画範囲を同時にスキャンする(処理が高速化されますが、デコードリソースをより多く消費します)", + "threshold": "感度しきい値", + "thresholdDesc": "値を小さくするとより小さな変化も検知します (1-255)", + "minArea": "最小変化面積", + "minAreaDesc": "単一移動領域の最小サイズ(関心領域に対するパーセンテージ)", + "frameSkip": "フレームスキップ", + "frameSkipDesc": "N フレームごとに処理します。カメラのフレームレートと同じ値にすると 1 秒あたり 1 フレーム処理されます(例: 5 FPS のカメラなら 5、30 FPS なら 30)。値を大きくすると高速になりますが、短時間のモーションを取りこぼす可能性があります。", + "maxResults": "最大結果数", + "maxResultsDesc": "この件数のタイムスタンプにヒットした時点でスキャンを停止します" + }, + "errors": { + "noCamera": "カメラを選択してください", + "noROI": "関心領域を描画してください", + "noTimeRange": "時間範囲を選択してください", + "invalidTimeRange": "終了時刻は開始時刻より後である必要があります", + "searchFailed": "検索に失敗しました: {{message}}", + "polygonTooSmall": "ポリゴンには少なくとも 3 つの点が必要です", + "unknown": "不明なエラー" + }, + "changePercentage": "{{percentage}}% 変化", + "metrics": { + "title": "検索メトリクス", + "segmentsScanned": "スキャンしたセグメント", + "segmentsProcessed": "処理済み", + "segmentsSkippedInactive": "スキップ (アクティビティなし)", + "segmentsSkippedHeatmap": "スキップ (関心領域と重なりなし)", + "fallbackFullRange": "全範囲スキャンへのフォールバック", + "framesDecoded": "デコードしたフレーム", + "wallTime": "検索時間", + "segmentErrors": "セグメントエラー", + "seconds": "{{seconds}} 秒", + "minutesSeconds": "{{minutes}} 分 {{seconds}} 秒", + "scanSummary": "{{segments}} セグメント · {{time}}" + }, + "scanning": "スキャン中 {{time}}" +} diff --git a/web/public/locales/ja/views/recording.json b/web/public/locales/ja/views/recording.json new file mode 100644 index 0000000..e505c13 --- /dev/null +++ b/web/public/locales/ja/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "フィルター", + "calendar": "カレンダー", + "export": "エクスポート", + "filters": "フィルター", + "toast": { + "error": { + "noValidTimeSelected": "適切な時刻の範囲が選択されていません", + "endTimeMustAfterStartTime": "終了時刻は開始時刻より後である必要があります" + } + } +} diff --git a/web/public/locales/ja/views/replay.json b/web/public/locales/ja/views/replay.json new file mode 100644 index 0000000..b975486 --- /dev/null +++ b/web/public/locales/ja/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "デバッグリプレイ", + "description": "デバッグ用にカメラの録画をリプレイします。オブジェクトリストには検知されたオブジェクトの遅延サマリーが表示され、「メッセージ」タブにはリプレイ映像からのFrigate内部メッセージのストリームが表示されます。", + "websocket_messages": "メッセージ", + "dialog": { + "title": "デバッグリプレイを開始", + "description": "オブジェクトの検知やトラッキングの問題をデバッグするために、過去の映像をループ再生する一時的なリプレイカメラを作成します。このリプレイカメラは、ソースカメラ(元カメラ)と同じ検知設定を引き継ぎます。開始する時間範囲を選択してください。", + "camera": "ソースカメラ", + "timeRange": "期間", + "preset": { + "1m": "直近1分間", + "5m": "直近5分間", + "timeline": "タイムラインから選択", + "custom": "カスタム" + }, + "startButton": "リプレイ開始", + "selectFromTimeline": "選択", + "starting": "リプレイを開始しています...", + "startLabel": "開始", + "endLabel": "終了", + "toast": { + "error": "デバッグリプレイの開始に失敗しました: {{error}}", + "alreadyActive": "リプレイセッションはすでに実行中です", + "stopError": "デバッグリプレイの停止に失敗しました: {{error}}", + "goToReplay": "リプレイへ移動" + } + }, + "page": { + "noSession": "アクティブなデバッグリプレイセッションはありません", + "noSessionDesc": "履歴ビューからデバッグリプレイを開始するには、ツールバーの「アクション」ボタンをクリックし、「デバッグリプレイ」を選択してください。", + "goToRecordings": "履歴画面へ", + "preparingClip": "クリップを準備しています…", + "preparingClipDesc": "Frigateは選択された時間範囲の録画を結合しています。指定した期間が長い場合、処理に1分ほどかかる場合があります。", + "startingCamera": "デバッグリプレイを開始しています…", + "startError": { + "title": "デバッグリプレイの開始に失敗しました", + "back": "履歴へ戻る" + }, + "sourceCamera": "ソースカメラ", + "replayCamera": "リプレイカメラ", + "initializingReplay": "デバッグリプレイを初期化しています...", + "stoppingReplay": "デバッグリプレイを停止しています...", + "stopReplay": "リプレイを停止", + "confirmStop": { + "title": "デバッグリプレイを停止しますか?", + "description": "セッションを停止し、すべての一時データを削除します。よろしいですか?", + "confirm": "リプレイ停止", + "cancel": "キャンセル" + }, + "activity": "アクティビティ", + "objects": "オブジェクト一覧", + "audioDetections": "音声検知", + "noActivity": "アクティビティは検知されませんでした", + "activeTracking": "アクティブトラッキング", + "noActiveTracking": "アクティブトラッキングなし", + "configuration": "設定", + "configurationDesc": "デバッグリプレイカメラのモーション検知およびオブジェクトトラッキング設定を微調整します。ここで行った変更はFrigateの設定ファイルには保存されません。" + } +} diff --git a/web/public/locales/ja/views/search.json b/web/public/locales/ja/views/search.json new file mode 100644 index 0000000..540606c --- /dev/null +++ b/web/public/locales/ja/views/search.json @@ -0,0 +1,73 @@ +{ + "searchFor": "「{{inputValue}}」を検索", + "button": { + "save": "検索を保存", + "delete": "保存済み検索を削除", + "filterInformation": "フィルター情報", + "clear": "検索をクリア", + "filterActive": "フィルターが有効" + }, + "search": "検索", + "savedSearches": "保存済み検索", + "trackedObjectId": "追跡オブジェクトID", + "filter": { + "label": { + "cameras": "カメラ", + "labels": "ラベル", + "zones": "ゾーン", + "sub_labels": "サブラベル", + "search_type": "検索タイプ", + "time_range": "期間", + "before": "以前", + "after": "以後", + "min_score": "最小スコア", + "max_score": "最大スコア", + "min_speed": "最小速度", + "max_speed": "最大速度", + "recognized_license_plate": "認識されたナンバープレート", + "has_clip": "クリップあり", + "has_snapshot": "スナップショットあり", + "attributes": "属性" + }, + "searchType": { + "thumbnail": "サムネイル", + "description": "説明" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "「以前」日付は「以後」日付より後である必要があります。", + "afterDatebeEarlierBefore": "「以後」日付は「以前」日付より前である必要があります。", + "minScoreMustBeLessOrEqualMaxScore": "「最小スコア」は「最大スコア」以下である必要があります。", + "maxScoreMustBeGreaterOrEqualMinScore": "「最大スコア」は「最小スコア」以上である必要があります。", + "minSpeedMustBeLessOrEqualMaxSpeed": "「最小速度」は「最大速度」以下である必要があります。", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "「最大速度」は「最小速度」以上である必要があります。" + } + }, + "tips": { + "title": "テキストフィルターの使い方", + "desc": { + "text": "フィルターを使うと検索結果を絞り込めます。入力欄での使い方は次の通りです。", + "step1": "フィルターのキー名の後にコロンを付けて入力します(例: \"cameras:\")。", + "step2": "候補から値を選ぶか、自分で入力します。", + "step3": "複数のフィルターは、間にスペースを入れて続けて追加できます。", + "step4": "日付フィルター(before: と after:)は {{DateFormat}} 形式を使用します。", + "step5": "期間フィルターは {{exampleTime}} 形式を使用します。", + "step6": "フィルターは隣の 'x' をクリックして削除できます。", + "exampleLabel": "例:" + } + }, + "header": { + "currentFilterType": "フィルター値", + "noFilters": "フィルター", + "activeFilters": "有効なフィルター" + } + }, + "similaritySearch": { + "title": "類似検索", + "active": "類似検索を実行中", + "clear": "類似検索をクリア" + }, + "placeholder": { + "search": "検索…" + } +} diff --git a/web/public/locales/ja/views/settings.json b/web/public/locales/ja/views/settings.json new file mode 100644 index 0000000..0222eba --- /dev/null +++ b/web/public/locales/ja/views/settings.json @@ -0,0 +1,2092 @@ +{ + "documentTitle": { + "authentication": "認証設定 - Frigate", + "camera": "カメラ設定 - Frigate", + "default": "設定 - Frigate", + "enrichments": "高度解析設定 - Frigate", + "masksAndZones": "マスク/ゾーンエディタ - Frigate", + "motionTuner": "モーションチューナー - Frigate", + "object": "デバッグ - Frigate", + "general": "UI設定 - Frigate", + "frigatePlus": "Frigate+ 設定 - Frigate", + "notifications": "通知設定 - Frigate", + "cameraManagement": "カメラ設定 - Frigate", + "cameraReview": "カメラレビュー設定 - Frigate", + "maintenance": "メンテナンス - Frigate", + "profiles": "プロファイル - Frigate", + "globalConfig": "グローバル設定 - Frigate", + "cameraConfig": "カメラ設定 - Frigate", + "detectorsAndModel": "検出器とモデル - Frigate" + }, + "menu": { + "ui": "UI", + "enrichments": "高度解析", + "cameras": "カメラ設定", + "masksAndZones": "マスク / ゾーン", + "motionTuner": "モーションチューナー", + "triggers": "トリガー", + "debug": "デバッグ", + "users": "ユーザー", + "notifications": "通知", + "frigateplus": "Frigate+", + "cameraManagement": "管理", + "cameraReview": "レビュー", + "roles": "ロール", + "general": "全般", + "globalConfig": "グローバル設定", + "system": "システム", + "integrations": "連携", + "uiSettings": "UI設定", + "profiles": "プロファイル", + "globalDetect": "物体検知", + "globalRecording": "録画", + "globalSnapshots": "スナップショット", + "globalFfmpeg": "FFmpeg", + "globalMotion": "モーション検知", + "globalObjects": "オブジェクト", + "globalReview": "レビュー", + "globalAudioEvents": "音声検知", + "globalLivePlayback": "ライブ再生", + "globalTimestampStyle": "タイムスタンプスタイル", + "systemDatabase": "データベース", + "systemTls": "TLS", + "systemAuthentication": "認証", + "systemNetworking": "ネットワーキング", + "systemProxy": "プロキシ", + "systemUi": "UI", + "systemLogging": "ロギング", + "systemEnvironmentVariables": "環境変数", + "systemTelemetry": "テレメトリ", + "systemBirdseye": "バードアイ", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "検出器とモデル", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "go2rtc ストリーム", + "integrationSemanticSearch": "セマンティック検索", + "integrationGenerativeAi": "生成AI", + "integrationFaceRecognition": "顔認識", + "integrationLpr": "ナンバープレート認識", + "integrationObjectClassification": "オブジェクト分類", + "integrationAudioTranscription": "音声文字起こし", + "cameraDetect": "物体検知", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "録画", + "cameraSnapshots": "スナップショット", + "cameraMotion": "モーション検知", + "cameraObjects": "オブジェクト", + "cameraConfigReview": "レビュー", + "cameraAudioEvents": "音声検知", + "cameraAudioTranscription": "音声文字起こし", + "cameraNotifications": "通知", + "cameraLivePlayback": "ライブ再生", + "cameraBirdseye": "バードアイ", + "cameraFaceRecognition": "顔認識", + "cameraLpr": "ナンバープレート認識", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "カメラ UI", + "cameraTimestampStyle": "タイムスタンプスタイル", + "cameraMqtt": "カメラ MQTT", + "maintenance": "メンテナンス", + "mediaSync": "メディア同期", + "regionGrid": "リージョングリッド" + }, + "dialog": { + "unsavedChanges": { + "title": "未保存の変更があります。", + "desc": "続行する前に変更を保存しますか?" + } + }, + "cameraSetting": { + "camera": "カメラ", + "noCamera": "カメラなし" + }, + "general": { + "title": "UI設定", + "liveDashboard": { + "title": "ライブダッシュボード", + "automaticLiveView": { + "label": "自動ライブビュー", + "desc": "アクティビティ検知時に自動でそのカメラのライブビューへ切り替えます。無効にすると、ライブダッシュボード上の静止画像は1分に1回のみ更新されます。" + }, + "playAlertVideos": { + "label": "アラート動画を再生", + "desc": "既定では、ライブダッシュボードの最近のアラートは小さなループ動画として再生されます。無効にすると、最近のアラートはこのデバイス/ブラウザでは静止画像のみ表示されます。" + }, + "displayCameraNames": { + "label": "常にカメラ名を表示", + "desc": "マルチカメラのライブビュー ダッシュボードで、カメラ名を常にチップ表示します。" + }, + "liveFallbackTimeout": { + "label": "ライブプレイヤーのフォールバック タイムアウト", + "desc": "カメラの高画質ライブストリームが利用できない場合、指定した秒数後に低帯域モードへ切り替えます。デフォルトは 3 秒。" + } + }, + "storedLayouts": { + "title": "保存済みレイアウト", + "desc": "カメラグループ内のレイアウトはドラッグ/リサイズできます。位置情報はブラウザのローカルストレージに保存されます。", + "clearAll": "すべてのレイアウトをクリア" + }, + "cameraGroupStreaming": { + "title": "カメラグループのストリーミング設定", + "desc": "各カメラグループのストリーミング設定はブラウザのローカルストレージに保存されます。", + "clearAll": "すべてのストリーミング設定をクリア" + }, + "recordingsViewer": { + "title": "録画ビューア", + "defaultPlaybackRate": { + "label": "既定の再生速度", + "desc": "録画再生の既定の再生速度です。" + } + }, + "calendar": { + "title": "カレンダー", + "firstWeekday": { + "label": "週の開始曜日", + "desc": "レビューカレンダーで週が始まる曜日。", + "sunday": "日曜日", + "monday": "月曜日" + } + }, + "toast": { + "success": { + "clearStoredLayout": "{{cameraName}} の保存済みレイアウトをクリアしました", + "clearStreamingSettings": "全カメラグループのストリーミング設定をクリアしました。" + }, + "error": { + "clearStoredLayoutFailed": "保存済みレイアウトのクリアに失敗しました: {{errorMessage}}", + "clearStreamingSettingsFailed": "ストリーミング設定のクリアに失敗しました: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "高度解析設定", + "unsavedChanges": "未保存の高度解析設定の変更", + "birdClassification": { + "title": "鳥類分類", + "desc": "量子化された TensorFlow モデルを使って既知の鳥を識別します。既知の鳥を認識した場合、その一般名を sub_label として追加します。この情報は UI、フィルタ、通知に含まれます。" + }, + "semanticSearch": { + "title": "セマンティック検索", + "desc": "Frigate のセマンティック検索では、画像そのもの、ユーザー定義のテキスト説明、または自動生成された説明を用いて、レビュー項目内の追跡オブジェクトを検索できます。", + "reindexNow": { + "label": "今すぐ再インデックス", + "desc": "インデックスの再構築を行うと、追跡対象のすべてのオブジェクトの埋め込みが再生成されます。この処理はバックグラウンドで実行され、追跡対象のオブジェクトの数によってはCPU使用率が最大になり、かなりの時間がかかる場合があります。", + "confirmTitle": "再インデックスの確認", + "confirmDesc": "すべての追跡オブジェクトの埋め込みを再インデックスしますか?この処理はバックグラウンドで実行されますが、CPU を使い切り、時間がかかる場合があります。進行状況は[探索]ページで確認できます。", + "confirmButton": "再インデックス", + "success": "再インデックスを開始しました。", + "alreadyInProgress": "再インデックスはすでに進行中です。", + "error": "再インデックスの開始に失敗しました: {{errorMessage}}" + }, + "modelSize": { + "label": "モデルサイズ", + "desc": "セマンティック検索の埋め込みに使用するモデルのサイズです。", + "small": { + "title": "スモール", + "desc": "small を使用すると、量子化モデルにより RAM 使用量が少なく、CPU 上で高速に動作します。埋め込み品質の差はごく僅かです。" + }, + "large": { + "title": "ラージ", + "desc": "large を使用すると、完全な Jina モデルを用い、可能であれば自動的に GPU で動作します。" + } + } + }, + "faceRecognition": { + "title": "顔認識", + "desc": "顔認識により、人に名前を割り当て、顔を認識した際にその人名をサブラベルとして付与します。この情報は UI、フィルタ、通知に含まれます。", + "modelSize": { + "label": "モデルサイズ", + "desc": "顔認識に使用するモデルのサイズです。", + "small": { + "title": "スモール", + "desc": "small は FaceNet ベースの顔埋め込みモデルを使用し、多くの CPU で効率よく動作します。" + }, + "large": { + "title": "ラージ", + "desc": "large は ArcFace ベースの顔埋め込みモデルを使用し、可能であれば自動的に GPU で動作します。" + } + } + }, + "licensePlateRecognition": { + "title": "ナンバープレート認識", + "desc": "車両のナンバープレートを認識し、検知文字列を recognized_license_plate フィールドへ、または既知の名称を car タイプのオブジェクトの sub_label として自動追加できます。一般的な用途として、私道に入ってくる車や道路を通過する車のナンバー読み取りがあります。" + }, + "restart_required": "再起動が必要です(高度解析設定を変更)", + "toast": { + "success": "高度解析設定を保存しました。変更を適用するには Frigate を再起動してください。", + "error": "設定変更の保存に失敗しました: {{errorMessage}}" + } + }, + "camera": { + "title": "カメラ設定", + "streams": { + "title": "ストリーム", + "desc": "Frigate の再起動まで、カメラを一時的に無効化します。無効化すると、このカメラのストリーム処理は完全に停止します。検出、録画、デバッグは利用できません。
    注: これは go2rtc のリストリームは無効化しません。" + }, + "object_descriptions": { + "title": "生成 AI オブジェクト説明", + "desc": "このカメラの生成 AI によるオブジェクト説明を一時的に有効/無効にします。無効にすると、追跡オブジェクトに対して説明はリクエストされません。" + }, + "review_descriptions": { + "title": "生成 AI レビュー説明", + "desc": "このカメラの生成 AI によるレビュー説明を一時的に有効/無効にします。無効にすると、レビュー項目に対して説明はリクエストされません。" + }, + "review": { + "title": "レビュー", + "desc": "Frigate の再起動まで、このカメラのアラートと検出を一時的に有効/無効にします。無効時は新しいレビュー項目は生成されません。 ", + "alerts": "アラート ", + "detections": "検出 " + }, + "reviewClassification": { + "title": "レビュー分類", + "desc": "Frigate はレビュー項目をアラートと検出に分類します。既定では personcar はアラートです。必要ゾーンを設定することで分類を細かく調整できます。", + "noDefinedZones": "このカメラにはゾーンが定義されていません。", + "objectAlertsTips": "{{cameraName}} 上の {{alertsLabels}} はすべてアラートとして表示されます。", + "zoneObjectAlertsTips": "{{cameraName}} の {{zone}} で検出された {{alertsLabels}} はすべてアラートとして表示されます。", + "objectDetectionsTips": "{{cameraName}} で未分類の {{detectionsLabels}} は、ゾーンに関わらず検出として表示されます。", + "zoneObjectDetectionsTips": { + "text": "{{cameraName}} の {{zone}} で未分類の {{detectionsLabels}} は検出として表示されます。", + "notSelectDetections": "{{cameraName}} の {{zone}} で検出された {{detectionsLabels}} のうちアラートに分類されないものは、ゾーンに関わらず検出として表示されます。", + "regardlessOfZoneObjectDetectionsTips": "{{cameraName}} で未分類の {{detectionsLabels}} は、ゾーンに関わらず検出として表示されます。" + }, + "unsavedChanges": "{{camera}} のレビュー分類設定に未保存の変更があります", + "selectAlertsZones": "アラートのゾーンを選択", + "selectDetectionsZones": "検出のゾーンを選択", + "limitDetections": "検出を特定ゾーンに制限", + "toast": { + "success": "レビュー分類設定を保存しました。変更を適用するには Frigate を再起動してください。" + } + }, + "addCamera": "新しいカメラを追加", + "editCamera": "カメラを編集:", + "selectCamera": "カメラを選択", + "backToSettings": "カメラ設定に戻る", + "cameraConfig": { + "add": "カメラを追加", + "edit": "カメラを編集", + "description": "ストリーム入力とロールを含むカメラ設定を構成します。", + "name": "カメラ名", + "nameRequired": "カメラ名は必須です", + "nameLength": "カメラ名は24文字未満である必要があります。", + "namePlaceholder": "例: front_door", + "enabled": "有効", + "ffmpeg": { + "inputs": "入力ストリーム", + "path": "ストリームパス", + "pathRequired": "ストリームパスは必須です", + "pathPlaceholder": "rtsp://...", + "roles": "ロール", + "rolesRequired": "少なくとも1つのロールが必要です", + "rolesUnique": "各ロール(audio, detect, record)は1つのストリームにのみ割り当て可能です", + "addInput": "入力ストリームを追加", + "removeInput": "入力ストリームを削除", + "inputsRequired": "少なくとも1つの入力ストリームが必要です" + }, + "toast": { + "success": "カメラ {{cameraName}} を保存しました" + } + } + }, + "masksAndZones": { + "filter": { + "all": "すべてのマスクとゾーン" + }, + "restart_required": "再起動が必要です(マスク/ゾーンを変更)", + "toast": { + "success": { + "copyCoordinates": "{{polyName}} の座標をクリップボードにコピーしました。" + }, + "error": { + "copyCoordinatesFailed": "座標をクリップボードにコピーできませんでした。" + } + }, + "motionMaskLabel": "モーションマスク {{number}}", + "objectMaskLabel": "オブジェクトマスク {{number}}", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "ゾーン名は2文字以上である必要があります。", + "mustNotBeSameWithCamera": "ゾーン名はカメラ名と同一にできません。", + "alreadyExists": "この名前のゾーンはこのカメラに既に存在します。", + "mustNotContainPeriod": "ゾーン名にピリオドは使用できません。", + "hasIllegalCharacter": "ゾーン名に不正な文字が含まれています。", + "mustHaveAtLeastOneLetter": "ゾーン名には少なくとも 1 文字が必要です。" + } + }, + "distance": { + "error": { + "text": "距離は 0.1 以上である必要があります。", + "mustBeFilled": "速度推定を使用するには、すべての距離フィールドを入力してください。" + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "慣性は 0 より大きい必要があります。" + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "滞留時間は 0 以上である必要があります。" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "速度しきい値は 0.1 以上である必要があります。" + } + }, + "polygonDrawing": { + "removeLastPoint": "最後の点を削除", + "reset": { + "label": "すべての点をクリア" + }, + "snapPoints": { + "true": "点をスナップ", + "false": "点をスナップしない" + }, + "delete": { + "title": "削除の確認", + "desc": "{{type}} {{name}} を削除してもよろしいですか?", + "success": "{{name}} を削除しました。" + }, + "error": { + "mustBeFinished": "保存する前に多角形の作図を完了してください。" + }, + "type": { + "zone": "ゾーン", + "motion_mask": "モーションマスク", + "object_mask": "オブジェクトマスク" + }, + "revertOverride": { + "title": "ベース設定に戻す", + "desc": "{{type}} {{name}} のプロファイル上書きを削除し、ベース設定に戻します。" + } + }, + "id": { + "error": { + "mustNotBeEmpty": "ID は空にできません。", + "alreadyExists": "この ID のマスクはこのカメラに既に存在します。" + } + }, + "name": { + "error": { + "mustNotBeEmpty": "名前は空にできません。" + } + } + }, + "zones": { + "label": "ゾーン", + "documentTitle": "ゾーンを編集 - Frigate", + "desc": { + "title": "ゾーンを使うと、フレーム内の特定領域を定義し、オブジェクトがその領域内にいるかどうかを判断できます。", + "documentation": "ドキュメント" + }, + "add": "ゾーンを追加", + "edit": "ゾーンを編集", + "point_other": "{{count}} 点", + "clickDrawPolygon": "画像上をクリックして多角形を描画します。", + "name": { + "title": "名前", + "inputPlaceHolder": "名前を入力…", + "tips": "名前は2文字以上で、少なくとも1文字のアルファベットを含み、このカメラ上の他のゾーン名やカメラ名と同一であってはなりません。" + }, + "inertia": { + "title": "慣性", + "desc": "オブジェクトがゾーン内にいるとみなすまでに必要なフレーム数を指定します。既定: 3" + }, + "loiteringTime": { + "title": "滞留時間", + "desc": "ゾーンが有効化されるまでに、オブジェクトがゾーン内に留まる必要がある最小秒数です。既定: 0" + }, + "objects": { + "title": "オブジェクト", + "desc": "このゾーンに適用するオブジェクトの一覧。" + }, + "allObjects": "すべてのオブジェクト", + "speedEstimation": { + "title": "速度推定", + "desc": "このゾーン内のオブジェクトに対して速度推定を有効にします。ゾーンはちょうど4点である必要があります。", + "lineADistance": "A 線の距離({{unit}})", + "lineBDistance": "B 線の距離({{unit}})", + "lineCDistance": "C 線の距離({{unit}})", + "lineDDistance": "D 線の距離({{unit}})" + }, + "speedThreshold": { + "title": "速度しきい値({{unit}})", + "desc": "このゾーンで考慮するオブジェクトの最小速度を指定します。", + "toast": { + "error": { + "pointLengthError": "このゾーンの速度推定を無効化しました。速度推定を使うゾーンは4点である必要があります。", + "loiteringTimeError": "滞留時間が 0 より大きいゾーンでは速度推定は使用しないでください。" + } + } + }, + "toast": { + "success": "ゾーン({{zoneName}})を保存しました。" + }, + "enabled": { + "title": "有効", + "description": "このゾーンを設定ファイルで有効化するかどうか。無効化すると MQTT からも有効化できず、実行時には無視されます。" + } + }, + "motionMasks": { + "label": "モーションマスク", + "documentTitle": "モーションマスクを編集 - Frigate", + "desc": { + "title": "モーションマスクは、望ましくない種類の動きで検知がトリガーされるのを防ぎます。過度なマスクはオブジェクト追跡を困難にします。", + "documentation": "ドキュメント" + }, + "add": "新しいモーションマスク", + "edit": "モーションマスクを編集", + "context": { + "title": "モーションマスクは、望ましくない動き(例: 木の枝、カメラのタイムスタンプ)で検知がトリガーされるのを防ぐために使用します。ごく控えめに使用してください。過度なマスクはオブジェクト追跡を困難にします。" + }, + "point_other": "{{count}} 点", + "clickDrawPolygon": "画像上をクリックして多角形を描画します。", + "polygonAreaTooLarge": { + "title": "モーションマスクがカメラフレームの {{polygonArea}}% を覆っています。大きなモーションマスクは推奨されません。", + "tips": "モーションマスクはオブジェクトの検知自体を防ぎません。代わりに必須ゾーンを使用してください。" + }, + "toast": { + "success": { + "title": "{{polygonName}} を保存しました。", + "noName": "モーションマスクを保存しました。" + } + }, + "defaultName": "モーションマスク {{number}}", + "name": { + "title": "名前", + "description": "このモーションマスクの任意の表示名です。", + "placeholder": "名前を入力..." + } + }, + "objectMasks": { + "label": "オブジェクトマスク", + "documentTitle": "オブジェクトマスクを編集 - Frigate", + "desc": { + "title": "オブジェクトフィルタマスクは、位置に基づいて特定のオブジェクトタイプの誤検知を除外するために使用します。", + "documentation": "ドキュメント" + }, + "add": "オブジェクトマスクを追加", + "edit": "オブジェクトマスクを編集", + "context": "オブジェクトフィルタマスクは、位置に基づいて特定のオブジェクトタイプの誤検知を除外するために使用します。", + "point_other": "{{count}} 点", + "clickDrawPolygon": "画像上をクリックして多角形を描画します。", + "objects": { + "title": "オブジェクト", + "desc": "このオブジェクトマスクに適用するオブジェクトタイプ。", + "allObjectTypes": "すべてのオブジェクトタイプ" + }, + "toast": { + "success": { + "title": "{{polygonName}} を保存しました。", + "noName": "オブジェクトマスクを保存しました。" + } + }, + "name": { + "title": "名前", + "description": "このオブジェクトマスクの任意の表示名です。", + "placeholder": "名前を入力..." + } + }, + "disabledInConfig": "この項目は設定ファイルで無効化されています", + "addDisabledProfile": "まずベース設定に追加してから、プロファイルで上書きしてください", + "profileBase": "(ベース)", + "profileOverride": "(上書き)", + "masks": { + "enabled": { + "title": "有効", + "description": "このマスクを設定ファイルで有効化するかどうか。無効化すると MQTT からも有効化できず、実行時には無視されます。" + } + } + }, + "motionDetectionTuner": { + "title": "モーション検知チューナー", + "unsavedChanges": "未保存のモーションチューナーの変更({{camera}})", + "desc": { + "title": "Frigate は、フレーム内に物体検知で確認すべき動きがあるかの一次チェックとしてモーション検知を使用します。", + "documentation": "モーション調整ガイドを読む" + }, + "Threshold": { + "title": "しきい値", + "desc": "しきい値は、ピクセルの輝度変化がモーションとみなされるために必要な変化量を決定します。既定: 30" + }, + "contourArea": { + "title": "輪郭面積", + "desc": "どの変化ピクセルのグループをモーションとして扱うかを決める値です。既定: 10" + }, + "improveContrast": { + "title": "コントラスト改善", + "desc": "暗いシーンのコントラストを改善します。既定: ON" + }, + "toast": { + "success": "モーション設定を保存しました。" + } + }, + "debug": { + "title": "デバッグ", + "detectorDesc": "Frigate は検出器({{detectors}})を使用して、カメラの映像ストリーム内のオブジェクトを検知します。", + "desc": "デバッグビューは、追跡オブジェクトとその統計をリアルタイムに表示します。オブジェクト一覧には、検知オブジェクトの時差サマリが表示されます。", + "openCameraWebUI": "{{camera}} の Web UI を開く", + "debugging": "デバッグ", + "objectList": "オブジェクト一覧", + "noObjects": "オブジェクトなし", + "audio": { + "title": "音声", + "noAudioDetections": "音声検知なし", + "score": "スコア", + "currentRMS": "現在の RMS", + "currentdbFS": "現在の dBFS" + }, + "boundingBoxes": { + "title": "バウンディングボックス", + "desc": "追跡オブジェクトの周囲にバウンディングボックスを表示します", + "colors": { + "label": "オブジェクトのボックス色", + "info": "
  • 起動時に、各オブジェクトラベルへ異なる色が割り当てられます
  • 細い濃青線は、現在時点では未検知であることを示します
  • 細い灰線は、静止していると検知されたことを示します
  • 太線は、(有効時)オートトラッキングの対象であることを示します
  • " + } + }, + "timestamp": { + "title": "タイムスタンプ", + "desc": "画像にタイムスタンプを重ねて表示します" + }, + "zones": { + "title": "ゾーン", + "desc": "定義済みゾーンのアウトラインを表示します" + }, + "mask": { + "title": "モーションマスク", + "desc": "モーションマスクの多角形を表示します" + }, + "motion": { + "title": "モーションボックス", + "desc": "モーションが検知された領域のボックスを表示します", + "tips": "

    モーションボックス


    現在モーションが検知されている領域に赤いボックスが重ねて表示されます

    " + }, + "regions": { + "title": "領域", + "desc": "物体検出器へ送られる関心領域のボックスを表示します", + "tips": "

    領域ボックス


    物体検出器へ送られるフレーム内の関心領域に明るい緑のボックスが重ねて表示されます。

    " + }, + "paths": { + "title": "軌跡", + "desc": "追跡オブジェクトの重要ポイントを表示します", + "tips": "

    軌跡


    線や円で、オブジェクトのライフサイクル中に移動した重要ポイントを示します。

    " + }, + "objectShapeFilterDrawing": { + "title": "オブジェクト形状フィルタの作図", + "desc": "画像上に矩形を描いて面積と比率の詳細を表示します", + "tips": "このオプションを有効にすると、カメラ画像上に矩形を描いてその面積と比率を表示できます。これらの値は設定ファイルのオブジェクト形状フィルタのパラメータ設定に利用できます。", + "score": "スコア", + "ratio": "比率", + "area": "面積" + } + }, + "users": { + "title": "ユーザー", + "management": { + "title": "ユーザー管理", + "desc": "この Frigate インスタンスのユーザーアカウントを管理します。" + }, + "addUser": "ユーザーを追加", + "updatePassword": "パスワードをリセット", + "toast": { + "success": { + "createUser": "ユーザー {{user}} を作成しました", + "deleteUser": "ユーザー {{user}} を削除しました", + "updatePassword": "パスワードを更新しました。", + "roleUpdated": "{{user}} のロールを更新しました" + }, + "error": { + "setPasswordFailed": "パスワードの保存に失敗しました: {{errorMessage}}", + "createUserFailed": "ユーザーの作成に失敗しました: {{errorMessage}}", + "deleteUserFailed": "ユーザーの削除に失敗しました: {{errorMessage}}", + "roleUpdateFailed": "ロールの更新に失敗しました: {{errorMessage}}" + } + }, + "table": { + "username": "ユーザー名", + "actions": "操作", + "role": "ロール", + "noUsers": "ユーザーが見つかりません。", + "changeRole": "ユーザーロールを変更", + "password": "パスワードをリセット", + "deleteUser": "ユーザーを削除" + }, + "dialog": { + "form": { + "user": { + "title": "ユーザー名", + "desc": "使用できるのは英数字、ピリオド、アンダースコアのみです。", + "placeholder": "ユーザー名を入力" + }, + "password": { + "title": "パスワード", + "placeholder": "パスワードを入力", + "confirm": { + "title": "パスワードの確認", + "placeholder": "パスワードを再入力" + }, + "strength": { + "title": "パスワード強度: ", + "weak": "弱い", + "medium": "普通", + "strong": "強い", + "veryStrong": "非常に強い" + }, + "match": "パスワードが一致しています", + "notMatch": "パスワードが一致しません", + "show": "パスワードを表示", + "hide": "パスワードを非表示", + "requirements": { + "title": "パスワード要件:", + "length": "12文字以上", + "uppercase": "大文字を 1 文字以上含める", + "digit": "数字を 1 文字以上含める", + "special": "少なくとも 1 つの特殊文字(!@#$%^&*(),.?”:{}|<>)が必要です" + } + }, + "newPassword": { + "title": "新しいパスワード", + "placeholder": "新しいパスワードを入力", + "confirm": { + "placeholder": "新しいパスワードを再入力" + } + }, + "usernameIsRequired": "ユーザー名は必須です", + "passwordIsRequired": "パスワードは必須です", + "currentPassword": { + "title": "現在のパスワード", + "placeholder": "現在のパスワードを入力" + } + }, + "createUser": { + "title": "新規ユーザーを作成", + "desc": "新しいユーザーアカウントを追加し、Frigate UI へのアクセスロールを指定します。", + "usernameOnlyInclude": "ユーザー名に使用できるのは英数字、.、_ のみです", + "confirmPassword": "パスワードを確認してください" + }, + "deleteUser": { + "title": "ユーザーを削除", + "desc": "この操作は元に戻せません。ユーザーアカウントおよび関連データは完全に削除されます。", + "warn": "{{username}} を削除してもよろしいですか?" + }, + "passwordSetting": { + "cannotBeEmpty": "パスワードを空にはできません", + "doNotMatch": "パスワードが一致しません", + "updatePassword": "{{username}} のパスワードを更新", + "setPassword": "パスワードを設定", + "desc": "強力なパスワードを作成して、このアカウントを保護してください。", + "currentPasswordRequired": "現在のパスワードは必須です", + "incorrectCurrentPassword": "現在のパスワードが正しくありません", + "passwordVerificationFailed": "パスワードの確認に失敗しました", + "multiDeviceWarning": "他のログイン中のデバイスは {{refresh_time}} 以内に再ログインが必要になります。", + "multiDeviceAdmin": "JWT シークレットをローテーションすることで、すべてのユーザーに即時再認証を強制することもできます。" + }, + "changeRole": { + "title": "ユーザーロールを変更", + "select": "ロールを選択", + "desc": "{{username}} の権限を更新します", + "roleInfo": { + "intro": "このユーザーに適切なロールを選択してください:", + "admin": "管理者", + "adminDesc": "すべての機能にフルアクセス。", + "viewer": "閲覧者", + "viewerDesc": "ライブ、レビュー、探索、エクスポートに限定。", + "customDesc": "特定のカメラアクセスを持つカスタムロール。" + } + } + } + }, + "roles": { + "management": { + "title": "閲覧者ロール管理", + "desc": "この Frigate インスタンスのカスタム閲覧者ロールと、そのカメラアクセス権を管理します。" + }, + "addRole": "ロールを追加", + "table": { + "role": "ロール", + "cameras": "カメラ", + "actions": "操作", + "noRoles": "カスタムロールが見つかりません。", + "editCameras": "カメラを編集", + "deleteRole": "ロールを削除" + }, + "toast": { + "success": { + "createRole": "ロール {{role}} を作成しました", + "updateCameras": "ロール {{role}} のカメラを更新しました", + "deleteRole": "ロール {{role}} を削除しました", + "userRolesUpdated_other": "このロールに割り当てられていた {{count}} ユーザーは「viewer」に更新され、全カメラへの閲覧アクセスが付与されました。" + }, + "error": { + "createRoleFailed": "ロールの作成に失敗しました: {{errorMessage}}", + "updateCamerasFailed": "カメラの更新に失敗しました: {{errorMessage}}", + "deleteRoleFailed": "ロールの削除に失敗しました: {{errorMessage}}", + "userUpdateFailed": "ユーザーロールの更新に失敗しました: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "新しいロールを作成", + "desc": "新しいロールを追加し、カメラアクセス権を指定します。" + }, + "editCameras": { + "title": "ロールのカメラを編集", + "desc": "ロール {{role}} のカメラアクセスを更新します。" + }, + "deleteRole": { + "title": "ロールを削除", + "desc": "この操作は元に戻せません。ロールは完全に削除され、このロールを持っていたユーザーは「viewer」ロールに再割り当てされ、全カメラへの閲覧アクセスが付与されます。", + "warn": "{{role}} を削除してもよろしいですか?", + "deleting": "削除中…" + }, + "form": { + "role": { + "title": "ロール名", + "placeholder": "ロール名を入力", + "desc": "使用できるのは英数字、ピリオド、アンダースコアのみです。", + "roleIsRequired": "ロール名は必須です", + "roleOnlyInclude": "ロール名に使用できるのは英数字、.、_ のみです", + "roleExists": "この名前のロールは既に存在します。" + }, + "cameras": { + "title": "カメラ", + "desc": "このロールでアクセス可能なカメラを選択します。少なくとも1台が必要です。", + "required": "少なくとも1台のカメラを選択してください。" + } + } + } + }, + "notification": { + "title": "通知", + "notificationSettings": { + "title": "通知設定", + "desc": "Frigate はブラウザで実行中、または PWA としてインストールされている場合に、端末へネイティブのプッシュ通知を送信できます。" + }, + "notificationUnavailable": { + "title": "通知は利用できません", + "desc": "Web プッシュ通知にはセキュアコンテキスト(https://…)が必要です。これはブラウザの制限です。通知を利用するには、セキュアに Frigate へアクセスしてください。", + "descPwa": "iOSでは、Frigateをホーム画面に追加した場合にのみ、Webプッシュ通知を利用できます。共有メニューを開き、ホーム画面に追加を選択してから、新しいアイコンからFrigateを起動し、このデバイスを通知対象として登録してください。" + }, + "globalSettings": { + "title": "グローバル設定", + "desc": "登録済みのすべてのデバイスで、特定のカメラの通知を一時停止します。" + }, + "email": { + "title": "メール", + "placeholder": "例: example@email.com", + "desc": "有効なメールが必要です。プッシュサービスに問題がある場合の通知に使用します。" + }, + "cameras": { + "title": "カメラ", + "noCameras": "利用可能なカメラがありません", + "desc": "通知を有効にするカメラを選択します。" + }, + "deviceSpecific": "デバイス固有の設定", + "registerDevice": "このデバイスを登録", + "unregisterDevice": "このデバイスの登録を解除", + "sendTestNotification": "テスト通知を送信", + "unsavedRegistrations": "未保存の通知登録", + "unsavedChanges": "未保存の通知設定の変更", + "active": "通知は有効", + "suspended": "通知は一時停止中 {{time}}", + "suspendTime": { + "suspend": "一時停止", + "5minutes": "5分間一時停止", + "10minutes": "10分間一時停止", + "30minutes": "30分間一時停止", + "1hour": "1時間一時停止", + "12hours": "12時間一時停止", + "24hours": "24時間一時停止", + "untilRestart": "再起動まで一時停止" + }, + "cancelSuspension": "一時停止を解除", + "toast": { + "success": { + "registered": "通知の登録に成功しました。通知(テスト通知を含む)を送信するには Frigate の再起動が必要です。", + "settingSaved": "通知設定を保存しました。" + }, + "error": { + "registerFailed": "通知登録の保存に失敗しました。" + } + } + }, + "frigatePlus": { + "title": "Frigate+ 設定", + "apiKey": { + "title": "Frigate+ API キー", + "validated": "Frigate+ API キーが検出され、検証されました", + "notValidated": "Frigate+ API キーが検出されないか、検証されていません", + "desc": "Frigate+ API キーは Frigate+ サービスとの統合を有効にします。", + "plusLink": "Frigate+ の詳細を読む" + }, + "snapshotConfig": { + "title": "スナップショット設定", + "desc": "Frigate+ への送信には、設定でスナップショットを有効にする必要があります。", + "cleanCopyWarning": "一部のカメラではスナップショット機能が無効になっています", + "table": { + "camera": "カメラ", + "snapshots": "スナップショット", + "cleanCopySnapshots": "clean_copy スナップショット" + } + }, + "modelInfo": { + "title": "モデル情報", + "modelType": "モデルタイプ", + "trainDate": "学習日", + "baseModel": "ベースモデル", + "plusModelType": { + "baseModel": "ベースモデル", + "userModel": "ファインチューニング済み" + }, + "supportedDetectors": "対応検出器", + "cameras": "カメラ", + "loading": "モデル情報を読み込み中…", + "error": "モデル情報の読み込みに失敗しました", + "availableModels": "利用可能なモデル", + "loadingAvailableModels": "利用可能なモデルを読み込み中…", + "modelSelect": "ここで Frigate+ 上の利用可能なモデルを選択できます。現在の検出器構成と互換性のあるモデルのみ選択可能です。", + "noModelLoaded": "Frigate+ モデルは現在読み込まれていません。", + "selectModel": "モデルを選択", + "noModelsAvailable": "利用可能なモデルがありません", + "filter": { + "ariaLabel": "モデルをタイプで絞り込み", + "baseModels": "ベースモデル", + "fineTunedModels": "ファインチューニング済みモデル" + } + }, + "unsavedChanges": "未保存の Frigate+ 設定の変更", + "restart_required": "再起動が必要です(Frigate+ モデルを変更)", + "toast": { + "success": "Frigate+ 設定を保存しました。変更を適用するには Frigate を再起動してください。", + "error": "設定変更の保存に失敗しました: {{errorMessage}}" + }, + "description": "Frigate+ はサブスクリプションサービスで、独自データで学習させたカスタム物体検知モデルなど、Frigate インスタンスに追加機能を提供します。Frigate+ モデルの設定はここから管理できます。", + "cardTitles": { + "api": "API", + "currentModel": "現在のモデル", + "otherModels": "その他のモデル", + "configuration": "設定" + }, + "changeInDetectorsAndModel": "モデルを変更" + }, + "triggers": { + "documentTitle": "トリガー", + "management": { + "title": "トリガー", + "desc": "{{camera}} のトリガーを管理します。サムネイルタイプでは、選択した追跡オブジェクトに類似するサムネイルでトリガーし、説明タイプでは、指定したテキストに類似する説明でトリガーします。" + }, + "addTrigger": "トリガーを追加", + "table": { + "name": "名前", + "type": "タイプ", + "content": "コンテンツ", + "threshold": "しきい値", + "actions": "操作", + "noTriggers": "このカメラに設定されたトリガーはありません。", + "edit": "編集", + "deleteTrigger": "トリガーを削除", + "lastTriggered": "最終トリガー時刻" + }, + "type": { + "thumbnail": "サムネイル", + "description": "説明" + }, + "actions": { + "alert": "アラートとしてマーク", + "notification": "通知を送信", + "sub_label": "サブラベルを追加", + "attribute": "属性を追加" + }, + "dialog": { + "createTrigger": { + "title": "トリガーを作成", + "desc": "カメラ {{camera}} のトリガーを作成します" + }, + "editTrigger": { + "title": "トリガーを編集", + "desc": "カメラ {{camera}} のトリガー設定を編集します" + }, + "deleteTrigger": { + "title": "トリガーを削除", + "desc": "トリガー {{triggerName}} を削除してもよろしいですか?この操作は元に戻せません。" + }, + "form": { + "name": { + "title": "名前", + "placeholder": "トリガー名を入力", + "error": { + "minLength": "この項目は2文字以上で入力してください。", + "invalidCharacters": "このフィールドに使用できるのは英数字、アンダースコア、ハイフンのみです。", + "alreadyExists": "このカメラには同名のトリガーが既に存在します。" + }, + "description": "このトリガーを識別するための一意の名前または説明を入力してください" + }, + "enabled": { + "description": "このトリガーを有効/無効にする" + }, + "type": { + "title": "タイプ", + "placeholder": "トリガータイプを選択", + "description": "類似した追跡オブジェクトの説明が検知されたときにトリガー", + "thumbnail": "類似した追跡オブジェクトのサムネイルが検知されたときにトリガー" + }, + "content": { + "title": "コンテンツ", + "imagePlaceholder": "サムネイルを選択", + "textPlaceholder": "テキストを入力", + "imageDesc": "最新のサムネイル100件のみが表示されます。目的のサムネイルが見つからない場合は、探索で過去のオブジェクトを確認し、そこのメニューからトリガーを設定してください。", + "textDesc": "類似する追跡オブジェクトの説明が検知されたときにこのアクションをトリガーするためのテキストを入力します。", + "error": { + "required": "コンテンツは必須です。" + } + }, + "threshold": { + "title": "しきい値", + "error": { + "min": "しきい値は 0 以上である必要があります", + "max": "しきい値は 1 以下である必要があります" + }, + "desc": "このトリガーの類似度しきい値を設定します。値が高いほど、より近い一致が必要になります。" + }, + "actions": { + "title": "操作", + "desc": "デフォルトでは、Frigate はすべてのトリガーに対して MQTT メッセージを送信します。サブラベルは、トリガー名をオブジェクトのラベルに追加します。属性(Attributes)は、追跡オブジェクトのメタデータとは別に保存される検索可能なメタデータです。", + "error": { + "min": "少なくとも1つのアクションを選択してください。" + } + }, + "friendly_name": { + "title": "表示名", + "placeholder": "このトリガーの名前または説明", + "description": "このトリガーの表示名または説明文" + } + } + }, + "toast": { + "success": { + "createTrigger": "トリガー {{name}} を作成しました。", + "updateTrigger": "トリガー {{name}} を更新しました。", + "deleteTrigger": "トリガー {{name}} を削除しました。" + }, + "error": { + "createTriggerFailed": "トリガーの作成に失敗しました: {{errorMessage}}", + "updateTriggerFailed": "トリガーの更新に失敗しました: {{errorMessage}}", + "deleteTriggerFailed": "トリガーの削除に失敗しました: {{errorMessage}}" + } + }, + "semanticSearch": { + "desc": "トリガーを使用するにはセマンティック検索を有効にする必要があります。", + "title": "セマンティック検索が無効です" + }, + "wizard": { + "title": "トリガーを作成", + "step1": { + "description": "トリガーの基本設定を構成します。" + }, + "step2": { + "description": "このアクションをトリガーする内容を設定します。" + }, + "step3": { + "description": "このトリガーのしきい値とアクションを設定します。" + }, + "steps": { + "nameAndType": "名前と種類", + "configureData": "データを設定", + "thresholdAndActions": "しきい値とアクション" + } + } + }, + "cameraWizard": { + "step3": { + "saveAndApply": "新しいカメラを保存", + "description": "カメラのストリームの役割を設定し、必要に応じてストリームを追加します。", + "validationTitle": "ストリーム検証", + "connectAllStreams": "すべてのストリームを接続", + "reconnectionSuccess": "再接続に成功しました。", + "reconnectionPartial": "一部のストリームの再接続に失敗しました。", + "streamUnavailable": "ストリームプレビューは利用できません", + "reload": "再読み込み", + "connecting": "接続中…", + "streamTitle": "ストリーム {{number}}", + "valid": "有効", + "failed": "失敗", + "notTested": "未テスト", + "connectStream": "接続", + "connectingStream": "接続中", + "disconnectStream": "切断", + "estimatedBandwidth": "推定帯域幅", + "roles": "ロール", + "none": "なし", + "error": "エラー", + "streamValidated": "ストリーム {{number}} の検証に成功しました", + "streamValidationFailed": "ストリーム {{number}} の検証に失敗しました", + "saveError": "無効な構成です。設定を確認してください。", + "issues": { + "title": "ストリーム検証", + "videoCodecGood": "ビデオコーデックは {{codec}} です。", + "audioCodecGood": "オーディオコーデックは {{codec}} です。", + "noAudioWarning": "このストリームでは音声が検出されません。録画には音声が含まれません。", + "audioCodecRecordError": "録画に音声を含めるには AAC オーディオコーデックが必要です。", + "audioCodecRequired": "音声検出を有効にするには音声ストリームが必要です。", + "restreamingWarning": "録画ストリームでカメラへの接続数を減らすと、CPU 使用率がわずかに増加する場合があります。", + "hikvision": { + "substreamWarning": "サブストリーム1は低解像度に固定されています。多くの Hikvision 製カメラでは、追加のサブストリームが利用可能であり、カメラ本体の設定で有効化する必要があります。使用できる場合は、それらのストリームを確認して活用することを推奨します。" + }, + "dahua": { + "substreamWarning": "サブストリーム1は低解像度に固定されています。多くの Dahua/Amcrest/EmpireTech 製カメラでは、追加のサブストリームが利用可能であり、カメラ本体の設定で有効化する必要があります。使用できる場合は、それらのストリームを確認して活用することを推奨します。" + } + }, + "streamsTitle": "カメラ ストリーム", + "addStream": "ストリームを追加", + "addAnotherStream": "別のストリームを追加", + "streamUrl": "ストリーム URL", + "streamUrlPlaceholder": "rtsp://ユーザー名:パスワード@ホスト:ポート/パス", + "selectStream": "ストリームを選択", + "searchCandidates": "候補を検索…", + "noStreamFound": "ストリームが見つかりません", + "url": "URL", + "resolution": "解像度", + "selectResolution": "解像度を選択", + "quality": "品質", + "selectQuality": "品質を選択", + "roleLabels": { + "detect": "物体検知", + "record": "録画", + "audio": "音声" + }, + "testStream": "接続をテスト", + "testSuccess": "ストリーム テスト成功!", + "testFailed": "ストリーム テスト失敗", + "testFailedTitle": "テスト失敗", + "connected": "接続済み", + "notConnected": "未接続", + "featuresTitle": "機能", + "go2rtc": "カメラへの接続数を削減", + "detectRoleWarning": "続行するには、少なくとも 1 つのストリームに「検知」ロールが必要です。", + "rolesPopover": { + "title": "ストリーム ロール", + "detect": "物体検知用のメイン フィードです。", + "record": "設定に基づいて映像フィードのセグメントを保存します。", + "audio": "音声ベース検知用のフィードです。" + }, + "featuresPopover": { + "title": "ストリーム機能", + "description": "go2rtc の再配信を使用してカメラへの接続数を削減します。" + } + }, + "title": "カメラを追加", + "description": "以下の手順に従って、Frigate に新しいカメラを追加します。", + "steps": { + "nameAndConnection": "名称と接続", + "streamConfiguration": "ストリーム設定", + "validationAndTesting": "検証とテスト", + "probeOrSnapshot": "プローブまたはスナップショット" + }, + "save": { + "success": "新しいカメラ {{cameraName}} を保存しました。", + "failure": "保存エラー: {{cameraName}}。" + }, + "testResultLabels": { + "resolution": "解像度", + "video": "映像", + "audio": "音声", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "有効なストリーム URL を入力してください", + "testFailed": "ストリームテストに失敗しました: {{error}}" + }, + "step1": { + "description": "カメラの詳細を入力し、カメラを自動検知するか、メーカーを手動で選択してください。", + "cameraName": "カメラ名", + "cameraNamePlaceholder": "例: front_door または Back Yard Overview", + "host": "ホスト/IP アドレス", + "port": "ポート", + "username": "ユーザー名", + "usernamePlaceholder": "任意", + "password": "パスワード", + "passwordPlaceholder": "任意", + "selectTransport": "トランスポートプロトコルを選択", + "cameraBrand": "カメラブランド", + "selectBrand": "URL テンプレート用のカメラブランドを選択", + "customUrl": "カスタムストリーム URL", + "brandInformation": "ブランド情報", + "brandUrlFormat": "RTSP URL 形式が {{exampleUrl}} のカメラ向け", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "testConnection": "接続テスト", + "testSuccess": "接続テストに成功しました!", + "testFailed": "接続テストに失敗しました。入力内容を確認して再試行してください。", + "streamDetails": "ストリーム詳細", + "warnings": { + "noSnapshot": "設定されたストリームからスナップショットを取得できません。" + }, + "errors": { + "brandOrCustomUrlRequired": "ホスト/IP とブランドを選択するか、「その他」を選んでカスタム URL を指定してください", + "nameRequired": "カメラ名は必須です", + "nameLength": "カメラ名は64文字以下である必要があります", + "invalidCharacters": "カメラ名に無効な文字が含まれています", + "nameExists": "このカメラ名は既に存在します", + "brands": { + "reolink-rtsp": "Reolink の RTSP は推奨されません。カメラ設定で http を有効にし、カメラウィザードを再起動することを推奨します。" + }, + "customUrlRtspRequired": "カスタムURLは「rtsp://」または「rtsps://」で始まる必要があります。RTSP以外のカメラストリームについては、手動での設定が必要です。" + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "connectionSettings": "接続設定", + "detectionMethod": "ストリーム検知方法", + "onvifPort": "ONVIF ポート", + "probeMode": "カメラをプローブ", + "manualMode": "手動選択", + "useDigestAuth": "ダイジェスト認証を使用", + "useDigestAuthDescription": "ONVIF に HTTP ダイジェスト認証を使用します。一部のカメラでは、通常の管理者ユーザーではなく専用の ONVIF ユーザー名/パスワードが必要な場合があります。", + "detectionMethodDescription": "(対応している場合)ONVIF を使用してカメラを自動設定し、カメラのストリーム URL を検知するか、カメラのブランドを手動で選択して事前定義された URL を使用します。カスタム RTSP URL を入力する場合は、手動設定を選択し、「その他」を選んでください。", + "onvifPortDescription": "ONVIF に対応しているカメラの場合、通常は 80 または 8080 です。" + }, + "step2": { + "description": "選択した検知方法に応じて、カメラから利用可能なストリームを自動検知するか、手動で設定してください。", + "streamsTitle": "カメラストリーム", + "addStream": "ストリームを追加", + "addAnotherStream": "ストリームをさらに追加", + "streamTitle": "ストリーム {{number}}", + "streamUrl": "ストリーム URL", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "url": "URL", + "resolution": "解像度", + "selectResolution": "解像度を選択", + "quality": "品質", + "selectQuality": "品質を選択", + "roles": "ロール", + "roleLabels": { + "detect": "物体検出", + "record": "録画", + "audio": "音声" + }, + "testStream": "接続テスト", + "testSuccess": "接続テストに成功しました!", + "testFailed": "接続テストに失敗しました。入力を確認し、もう一度実行してください。", + "testFailedTitle": "テスト失敗", + "connected": "接続済み", + "notConnected": "未接続", + "featuresTitle": "機能", + "go2rtc": "カメラへの接続数を削減", + "detectRoleWarning": "\"detect\" ロールを持つストリームが少なくとも1つ必要です。", + "rolesPopover": { + "title": "ストリームロール", + "detect": "物体検出のメインフィード。", + "record": "設定に基づいて映像フィードのセグメントを保存します。", + "audio": "音声検出用のフィード。" + }, + "featuresPopover": { + "title": "ストリーム機能", + "description": "go2rtc のリストリーミングを使用してカメラへの接続数を削減します。" + }, + "streamDetails": "ストリームの詳細", + "probing": "カメラをプローブ中…", + "retry": "再試行", + "testing": { + "probingMetadata": "カメラのメタデータを取得中…", + "fetchingSnapshot": "カメラのスナップショットを取得中…" + }, + "probeFailed": "カメラのプローブに失敗しました: {{error}}", + "probingDevice": "デバイスをプローブ中…", + "probeSuccessful": "プローブ成功", + "probeError": "プローブ エラー", + "probeNoSuccess": "プローブ失敗", + "deviceInfo": "デバイス情報", + "manufacturer": "メーカー", + "model": "モデル", + "firmware": "ファームウェア", + "profiles": "プロファイル", + "ptzSupport": "PTZ 対応", + "autotrackingSupport": "自動追跡対応", + "presets": "プリセット", + "rtspCandidates": "RTSP 候補", + "rtspCandidatesDescription": "カメラのプローブから以下の RTSP URL が見つかりました。接続をテストしてストリームのメタデータを確認してください。", + "candidateStreamTitle": "候補 {{number}}", + "useCandidate": "使用", + "uriCopy": "コピー", + "uriCopied": "URI をクリップボードにコピーしました", + "testConnection": "接続をテスト", + "toggleUriView": "クリックして URI の全表示を切り替え", + "errors": { + "hostRequired": "ホスト/IP アドレスは必須です" + }, + "noRtspCandidates": "カメラから RTSP URL を取得できませんでした。認証情報が正しくないか、カメラが ONVIF に対応していない、または RTSP URL を取得する方法がサポートされていない可能性があります。RTSP URL を手動で入力してください。" + }, + "step4": { + "description": "新しいカメラを保存する前の最終検証と分析です。保存前に各ストリームを接続してください。", + "validationTitle": "ストリーム検証", + "connectAllStreams": "すべてのストリームを接続", + "reconnectionSuccess": "再接続に成功しました。", + "reconnectionPartial": "一部のストリームで再接続に失敗しました。", + "streamUnavailable": "ストリームのプレビューを表示できません", + "reload": "再読み込み", + "connecting": "接続中…", + "streamTitle": "ストリーム {{number}}", + "valid": "有効", + "failed": "失敗", + "notTested": "未テスト", + "connectStream": "接続", + "connectingStream": "接続中", + "disconnectStream": "切断", + "estimatedBandwidth": "推定帯域幅", + "roles": "ロール", + "ffmpegModule": "ストリーム互換モードを使用", + "none": "なし", + "error": "エラー", + "streamValidated": "ストリーム {{number}} の検証に成功しました", + "streamValidationFailed": "ストリーム {{number}} の検証に失敗しました", + "saveAndApply": "新しいカメラを保存", + "saveError": "無効な設定です。設定を確認してください。", + "issues": { + "title": "ストリーム検証", + "videoCodecGood": "ビデオ コーデックは {{codec}} です。", + "audioCodecGood": "オーディオ コーデックは {{codec}} です。", + "resolutionHigh": "解像度 {{resolution}} はリソース使用量が増加する可能性があります。", + "resolutionLow": "解像度 {{resolution}} は小さなオブジェクトを確実に検知するには低すぎる可能性があります。", + "audioCodecRecordError": "録画で音声をサポートするには AAC オーディオ コーデックが必要です。", + "audioCodecRequired": "音声検知をサポートするには音声ストリームが必要です。", + "restreamingWarning": "録画用ストリームでカメラへの接続数を削減すると、CPU 使用率がわずかに増加する場合があります。", + "brands": { + "reolink-rtsp": "Reolink の RTSP は推奨されません。カメラのファームウェア設定で HTTP を有効にし、ウィザードを再起動してください。", + "reolink-http": "Reolink の HTTP ストリームは互換性向上のため FFmpeg を使用してください。このストリームで「ストリーム互換モードを使用」を有効にしてください。" + }, + "dahua": { + "substreamWarning": "サブストリーム 1 は低解像度に固定されています。多くの Dahua / Amcrest / EmpireTech カメラは追加のサブストリームをサポートしており、カメラ設定で有効化する必要があります。利用可能であればそれらのストリームを使用することを推奨します。" + }, + "hikvision": { + "substreamWarning": "サブストリーム 1 は低解像度に固定されています。多くの Hikvision カメラは追加のサブストリームをサポートしており、カメラ設定で有効化する必要があります。利用可能であればそれらのストリームを使用することを推奨します。" + }, + "noAudioWarning": "このストリームでは音声が検知されていません。録画には音声が含まれません。", + "resolutionUnknown": "このストリームの解像度を取得できませんでした。設定画面または設定ファイルで検知解像度を手動で指定してください。" + }, + "ffmpegModuleDescription": "何度か試してもストリームが読み込まれない場合は、このオプションを有効にしてください。有効にすると、Frigate は go2rtc と併用して ffmpeg モジュールを使用します。一部のカメラストリームでは、互換性が向上する場合があります。" + } + }, + "cameraManagement": { + "title": "カメラ管理", + "addCamera": "新しいカメラを追加", + "editCamera": "カメラを編集:", + "selectCamera": "カメラを選択", + "backToSettings": "カメラ設定に戻る", + "streams": { + "title": "カメラの有効化/無効化", + "desc": "Frigate を再起動するまで一時的にカメラを無効化します。無効化すると、このカメラのストリーム処理は完全に停止し、検出・録画・デバッグは利用できません。
    注: これは go2rtc のリストリームを無効にはしません。", + "label": "カメラの状態", + "description": "各カメラの動作状態を設定します。

    オン: ストリームを通常通り処理します。
    オフ: 処理を一時停止します。Frigate の再起動後は保持されません。
    無効: 処理を停止し、設定ファイルに保存されます。再有効化には再起動が必要です。

    注: 無効化しても go2rtc のリストリームには影響しません。

    ハンドルをドラッグすると、ライブダッシュボードやカメラ選択ドロップダウンなど、UI 内のアクティブなカメラの並び順を変更できます。", + "disabledSubheading": "設定で無効化されています", + "status": { + "on": "オン", + "off": "オフ", + "disabled": "無効" + }, + "enableSuccess": "{{cameraName}} を有効にしました。適用するには Frigate を再起動してください。", + "disableSuccess": "{{cameraName}} を無効化し、設定に保存しました。", + "reorderHandle": "ドラッグで並び替え", + "saving": "保存中…", + "saved": "保存しました", + "details": { + "edit": "カメラ詳細を編集", + "title": "カメラ詳細を編集", + "description": "このカメラの表示名と外部 URL を更新します。Frigate UI 全体で使用されます。", + "friendlyNameLabel": "表示名", + "friendlyNameHelp": "Frigate UI 全体でこのカメラに表示される名前です。空欄にするとカメラ ID が使用されます。", + "webuiUrlLabel": "カメラ Web UI の URL", + "webuiUrlHelp": "デバッグビューからカメラの Web UI に直接アクセスするための URL です。空欄にするとリンクが無効になります。", + "webuiUrlInvalid": "有効な URL を入力してください (例: https://example.com)。" + } + }, + "cameraConfig": { + "add": "カメラを追加", + "edit": "カメラを編集", + "description": "ストリーム入力とロールを含むカメラ設定を構成します。", + "name": "カメラ名", + "nameRequired": "カメラ名は必須です", + "nameLength": "カメラ名は64文字未満である必要があります。", + "namePlaceholder": "例: front_door または Back Yard Overview", + "enabled": "有効", + "ffmpeg": { + "inputs": "入力ストリーム", + "path": "ストリームパス", + "pathRequired": "ストリームパスは必須です", + "pathPlaceholder": "rtsp://...", + "roles": "ロール", + "rolesRequired": "少なくとも1つのロールが必要です", + "rolesUnique": "各ロール(audio、detect、record)は1つのストリームにのみ割り当て可能です", + "addInput": "入力ストリームを追加", + "removeInput": "入力ストリームを削除", + "inputsRequired": "少なくとも1つの入力ストリームが必要です" + }, + "go2rtcStreams": "go2rtc ストリーム", + "streamUrls": "ストリーム URL", + "addUrl": "URL を追加", + "addGo2rtcStream": "go2rtc ストリームを追加", + "toast": { + "success": "カメラ {{cameraName}} を保存しました" + } + }, + "description": "カメラの追加・編集・削除、各カメラの状態管理、プロファイル別やカメラタイプ別の上書き設定を行えます。ストリーム、検知、モーションなどカメラ固有の設定は、カメラ設定セクション内の各項目から行ってください。", + "deleteCamera": "カメラを削除", + "deleteCameraDialog": { + "title": "カメラを削除", + "description": "カメラを削除すると、そのカメラのすべての録画、追跡オブジェクト、設定が完全に削除されます。このカメラに関連する go2rtc ストリームは手動で削除が必要な場合があります。", + "selectPlaceholder": "カメラを選択...", + "confirmTitle": "本当によろしいですか?", + "confirmWarning": "{{cameraName}} の削除は元に戻せません。", + "deleteExports": "このカメラのエクスポートも削除する", + "confirmButton": "完全に削除", + "success": "カメラ {{cameraName}} を削除しました", + "error": "カメラ {{cameraName}} の削除に失敗しました" + }, + "profiles": { + "title": "プロファイル別カメラ上書き設定", + "selectLabel": "プロファイルを選択", + "description": "プロファイルが有効化されたときに、各カメラをオン/オフのどちらにするかを設定します。「継承」に設定されたカメラはデフォルトの状態を維持します。", + "inherit": "継承", + "on": "オン", + "off": "オフ" + }, + "cameraType": { + "title": "カメラタイプ", + "label": "カメラタイプ", + "description": "各カメラのタイプを設定します。専用 LPR カメラは、遠方の車両のナンバープレートを捉えるために強力な光学ズームを備えた専用カメラです。LPR 専用かつナンバープレートに焦点を絞った設置でない限り、通常は「Normal」を使用してください。", + "normal": "通常", + "dedicatedLpr": "専用 LPR", + "saveSuccess": "{{cameraName}} のカメラタイプを更新しました。変更を適用するには Frigate を再起動してください。" + }, + "clone": { + "sectionTitle": "設定を複製", + "sectionDescription": "あるカメラの設定を、別のカメラまたは新しいカメラにコピーします。", + "button": "設定を複製", + "title": "カメラ設定を複製", + "description": "あるカメラの設定を、1台以上の他のカメラまたは新しいカメラにコピーします。識別情報(名前、フレンドリーネーム、Web UI URL、表示順序)はコピーされません。", + "source": { + "label": "ソースカメラ", + "placeholder": "ソースカメラを選択", + "required": "ソースカメラを選択" + }, + "target": { + "legend": "ターゲット", + "newRadio": "新しいカメラ", + "newNameLabel": "カメラ名", + "newNamePlaceholder": "例:back_door または Back Door", + "newNameRequired": "カメラ名は必須です", + "newNameInvalid": "無効なカメラ名です", + "newNameCollision": "この名前のカメラは既に存在します", + "newStreamsForced": "新しいカメラには、常にストリームがコピーされます。", + "existingCamerasRadio": "既存のカメラ", + "allCameras": "すべてのカメラ", + "existingPlaceholder": "少なくとも1台のカメラを選択", + "existingDisabled": "コピー先のカメラがありません" + }, + "categories": { + "legend": "クローンする設定", + "description": "ソースカメラからコピーする設定を選択します。", + "selectAll": "すべて選択", + "selectNone": "選択解除", + "resetDefaults": "デフォルトにリセット", + "general": "一般", + "spatial": "空間設定", + "streams": "ストリーム", + "spatialWarningTitle": "解像度の不一致", + "spatialWarning": "ソースカメラ {{srcCamera}} の検出解像度 ({{srcWidth}}×{{srcHeight}}) は {{cameras}} と異なります。これらのカメラではポリゴンが一致しない可能性があります。これらのデフォルト設定はオフになっています。そのままコピーするには有効にしてください。", + "restartHint": "再起動が必要", + "items": { + "record": "録画", + "snapshots": "スナップショット", + "review": "再生", + "motion": "動体検知", + "objects": "物体", + "audio": "音声検知", + "audio_transcription": "音声文字起こし", + "notifications": "通知", + "birdseye": "バードアイ", + "mqtt": "MQTT", + "timestamp_style": "タイムスタンプ形式", + "onvif": "ONVIF", + "lpr": "ナンバープレート認識", + "face_recognition": "顔認識", + "semantic_search": "セマンティック検索", + "genai": "生成AI", + "type": "カメラタイプ(通常 / 専用LPR)", + "profiles": "プロファイル", + "detect": "サイズ検出", + "zones": "ゾーン", + "motion_mask": "動きマスク", + "object_masks": "オブジェクトマスク", + "ffmpeg_live": "ストリームURLとロール" + } + }, + "footer": { + "changeCount_other": "{{count}}件の変更が適用されます", + "restartNeeded": "一部の変更には再起動が必要です。", + "liveOnly": "すべての変更は再起動なしで即時適用されます。", + "submit": "クローン", + "submitting": "クローン作成中…" + }, + "toast": { + "success": "設定が {{cameraName}} にコピーされました", + "successWithRestart": "設定が {{cameraName}} にコピーされました。すべての変更を適用するには、Frigate を再起動してください。", + "successMulti_other": "設定が {{count}} 台のカメラにコピーされました", + "successMultiWithRestart_other": "設定が {{count}} 台のカメラにコピーされました。すべての変更を適用するには、Frigate を再起動してください。", + "partialFailure": "{{successCount}} セクションが適用されました。「{{failedSection}}」で失敗しました: {{errorMessage}}", + "partialFailureMulti": "{{successCount}} 台のカメラにコピーされました。{{failed}} で失敗しました: {{errorMessage}}", + "newCameraPartialFailure": "カメラ {{cameraName}} が作成されましたが、一部の設定のコピーに失敗しました: {{errorMessage}}", + "sourceMissing": "ソースカメラが存在しません", + "submitError": "カメラのクローン作成に失敗しました: {{errorMessage}}" + } + } + }, + "cameraReview": { + "title": "カメラレビュー設定", + "object_descriptions": { + "title": "生成AIによるオブジェクト説明", + "desc": "Frigateが再起動するまで、このカメラの生成AIによる物体説明を一時的に有効/無効にします。無効にすると、このカメラで追跡された物体に対してAI生成の説明は生成されません。" + }, + "review_descriptions": { + "title": "生成AIによるレビュー説明", + "desc": "Frigateが再起動するまで、このカメラの生成AIによるレビュー説明を一時的に有効/無効にします。無効にすると、このカメラのレビュー項目に対してAI生成の説明は生成されません。" + }, + "review": { + "title": "レビュー", + "desc": "Frigate を再起動するまで、このカメラのアラートと検知を一時的に有効/無効にします。無効にすると、新しいレビュー項目は生成されません。 ", + "alerts": "アラート ", + "detections": "検知 " + }, + "reviewClassification": { + "title": "レビュー分類", + "desc": "Frigate はレビュー項目をアラートと検知に分類します。既定では、すべての personcar オブジェクトはアラートとして扱われます。必須ゾーンを設定することで、分類をより細かく調整できます。", + "noDefinedZones": "このカメラにはゾーンが定義されていません。", + "objectAlertsTips": "すべての {{alertsLabels}} オブジェクトは {{cameraName}} でアラートとして表示されます。", + "zoneObjectAlertsTips": "{{cameraName}} の {{zone}} で検知されたすべての {{alertsLabels}} オブジェクトはアラートとして表示されます。", + "objectDetectionsTips": "{{cameraName}} で分類されていないすべての {{detectionsLabels}} オブジェクトは、どのゾーンにあっても検知として表示されます。", + "zoneObjectDetectionsTips": { + "text": "{{cameraName}} の {{zone}} で分類されていないすべての {{detectionsLabels}} オブジェクトは検知として表示されます。", + "notSelectDetections": "{{cameraName}} の {{zone}} で検知され、アラートに分類されなかったすべての {{detectionsLabels}} オブジェクトは、ゾーンに関係なく検知として表示されます。", + "regardlessOfZoneObjectDetectionsTips": "{{cameraName}} で分類されていないすべての {{detectionsLabels}} オブジェクトは、どのゾーンにあっても検知として表示されます。" + }, + "unsavedChanges": "未保存のレビュー分類設定({{camera}})", + "selectAlertsZones": "アラート用のゾーンを選択", + "selectDetectionsZones": "検知用のゾーンを選択", + "limitDetections": "特定のゾーンに検知を限定する", + "toast": { + "success": "レビュー分類の設定を保存しました。変更を適用するには Frigate を再起動してください。" + } + } + }, + "maintenance": { + "sync": { + "status": { + "queued": "キューに追加済み", + "running": "実行中", + "completed": "完了", + "failed": "失敗", + "notRunning": "実行されていません" + }, + "title": "メディア同期", + "desc": "Frigate は保持設定に従って定期的にメディアを整理します。動作中に孤立ファイルが少数発生するのは正常です。この機能を使うと、データベース上で参照されなくなった孤立メディアファイルをディスクから削除できます。", + "started": "メディア同期を開始しました。", + "alreadyRunning": "同期ジョブはすでに実行中です", + "error": "同期の開始に失敗しました", + "currentStatus": "ステータス", + "jobId": "ジョブ ID", + "startTime": "開始時刻", + "endTime": "終了時刻", + "statusLabel": "ステータス", + "results": "結果", + "errorLabel": "エラー", + "mediaTypes": "メディアタイプ", + "allMedia": "すべてのメディア", + "dryRun": "ドライラン", + "dryRunEnabled": "ファイルは削除されません", + "dryRunDisabled": "ファイルは削除されます", + "force": "強制実行", + "forceDesc": "安全しきい値を無視して、削除対象がファイル全体の 50% を超えても同期を完了します。", + "verbose": "詳細出力", + "verboseDesc": "孤立ファイルの全リストを確認用にディスクに書き出します。", + "running": "同期実行中...", + "start": "同期を開始", + "inProgress": "同期実行中です。このページは現在操作できません。", + "resultsFields": { + "filesChecked": "確認済みファイル数", + "orphansFound": "検知された孤立ファイル数", + "orphansDeleted": "削除した孤立ファイル数", + "aborted": "中断しました。削除が安全しきい値を超過するためです。", + "error": "エラー", + "totals": "合計" + }, + "event_snapshots": "追跡オブジェクトのスナップショット", + "event_thumbnails": "追跡オブジェクトのサムネイル", + "review_thumbnails": "レビューのサムネイル", + "previews": "プレビュー", + "exports": "エクスポート", + "recordings": "録画" + }, + "title": "メンテナンス", + "regionGrid": { + "title": "リージョングリッド", + "desc": "リージョングリッドは、各カメラの画角内でさまざまなサイズのオブジェクトが通常出現する位置を学習する最適化機能です。Frigate はこのデータをもとに検知領域のサイズを効率化します。グリッドは追跡オブジェクトのデータから時間をかけて自動的に構築されます。", + "clear": "リージョングリッドをクリア", + "clearConfirmTitle": "リージョングリッドをクリア", + "clearConfirmDesc": "リージョングリッドのクリアは、検出器のモデルサイズを最近変更した場合や、カメラの物理的な位置を変更してオブジェクト追跡に問題がある場合を除き、推奨されません。追跡が進むにつれて自動的に再構築されます。変更を反映するには Frigate の再起動が必要です。", + "clearSuccess": "リージョングリッドをクリアしました", + "clearError": "リージョングリッドのクリアに失敗しました", + "restartRequired": "リージョングリッドの変更を反映するには再起動が必要です" + } + }, + "button": { + "overriddenGlobal": "上書き済み(グローバル)", + "overriddenGlobalTooltip": "このカメラは、このセクションのグローバル設定を上書きします", + "overriddenBaseConfig": "上書き済み(基本設定)", + "overriddenBaseConfigTooltip": "{{profile}} プロファイルは、このセクションの設定を上書きします", + "overriddenInCameras": { + "label_other": "{{count}} 台のカメラで上書きされました", + "tooltip_other": "{{count}} 台のカメラがこのセクションの設定値を上書きしています。詳細を表示するにはクリックしてください。", + "heading_other": "このグローバルセクションには、{{count}} 台のカメラで上書きされているフィールドがあります。", + "othersField_other": "{{count}} その他", + "profilePrefix": "{{profile}} プロファイル: {{fields}}" + }, + "overriddenGlobalHeading_other": "このカメラはグローバル設定のうち {{count}} 項目を上書きしています:", + "overriddenGlobalNoDeltas": "このカメラはグローバル設定を上書きしていますが、値が異なる項目はありません。", + "overriddenBaseConfigHeading_other": "{{profile}} プロファイルはベース設定のうち {{count}} 項目を上書きしています:", + "overriddenBaseConfigNoDeltas": "{{profile}} プロファイルはこのセクションを上書きしていますが、ベース設定と異なる値はありません。" + }, + "menuDot": { + "overrideGlobal": "このセクションはグローバル設定を上書きします", + "overrideProfile": "このセクションは {{profile}} プロファイルによって上書きされます", + "unsaved": "このセクションには未保存の変更があります" + }, + "saveAllPreview": { + "title": "保存対象の変更", + "triggerLabel": "保留中の変更を確認", + "empty": "保留中の変更はありません。", + "scope": { + "label": "対象範囲", + "global": "グローバル", + "camera": "カメラ: {{cameraName}}" + }, + "profile": { + "label": "プロファイル" + }, + "field": { + "label": "項目" + }, + "value": { + "label": "新しい値", + "reset": "リセット" + } + }, + "timestampPosition": { + "tl": "左上", + "tr": "右上", + "bl": "左下", + "br": "右下" + }, + "detectorsAndModel": { + "title": "検出器とモデル", + "description": "物体検知を実行する検出器のバックエンドと、使用するモデルを設定します。検出器とモデルが同期するよう、変更は一括で保存されます。", + "cardTitles": { + "detector": "検出器ハードウェア", + "model": "検知モデル" + }, + "tabs": { + "plus": "Frigate+", + "custom": "カスタムモデル" + }, + "mismatch": { + "warning": "現在の Frigate+ モデル「{{model}}」は {{required}} 検出器が必要です。下から対応するモデルを選ぶか、保存前にカスタムモデルへ切り替えてください。" + }, + "plusModel": { + "requiresDetector": "必要な検出器: {{detector}}", + "noModelSelected": "Frigate+ モデルを選択" + }, + "toast": { + "saveSuccess": "検出器とモデルの設定を保存しました。変更を適用するには Frigate を再起動してください。", + "saveError": "検出器とモデルの設定の保存に失敗しました" + }, + "unsavedChanges": "検出器・モデルに未保存の変更があります", + "restartRequired": "再起動が必要です(検出器またはモデルが変更されました)" + }, + "configForm": { + "global": { + "title": "グローバル設定", + "description": "これらの設定は、カメラ固有の設定で上書きされない限り、全カメラに適用されます。" + }, + "camera": { + "title": "カメラ設定", + "description": "これらの設定はこのカメラにのみ適用され、グローバル設定を上書きします。", + "noCameras": "利用可能なカメラがありません" + }, + "advancedSettingsCount": "詳細設定 ({{count}})", + "advancedCount": "詳細 ({{count}})", + "showAdvanced": "詳細設定を表示", + "tabs": { + "sharedDefaults": "共通デフォルト", + "system": "システム", + "integrations": "連携" + }, + "additionalProperties": { + "keyLabel": "キー", + "valueLabel": "値", + "keyPlaceholder": "新しいキー", + "remove": "削除" + }, + "knownPlates": { + "namePlaceholder": "例: 妻の車", + "platePlaceholder": "ナンバーまたは正規表現" + }, + "timezone": { + "defaultOption": "ブラウザのタイムゾーンを使用" + }, + "roleMap": { + "empty": "ロールマッピングがありません", + "roleLabel": "ロール", + "groupsLabel": "グループ", + "addMapping": "ロールマッピングを追加", + "remove": "削除" + }, + "ffmpegArgs": { + "preset": "プリセット", + "manual": "手動で引数指定", + "inherit": "カメラ設定から継承", + "none": "なし", + "useGlobalSetting": "グローバル設定から継承", + "selectPreset": "プリセットを選択", + "manualPlaceholder": "FFmpeg 引数を入力", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (汎用)", + "preset-http-mjpeg-generic": "HTTP MJPEG (汎用)", + "preset-http-reolink": "HTTP - Reolink カメラ", + "preset-rtmp-generic": "RTMP (汎用)", + "preset-rtsp-generic": "RTSP (汎用)", + "preset-rtsp-restream": "RTSP - go2rtc からのリストリーム", + "preset-rtsp-restream-low-latency": "RTSP - go2rtc からのリストリーム (低遅延)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "録画 (汎用、音声なし)", + "preset-record-generic-audio-copy": "録画 (汎用 + 音声コピー)", + "preset-record-generic-audio-aac": "録画 (汎用 + 音声を AAC に変換)", + "preset-record-mjpeg": "録画 - MJPEG カメラ", + "preset-record-jpeg": "録画 - JPEG カメラ", + "preset-record-ubiquiti": "録画 - Ubiquiti カメラ" + } + }, + "cameraInputs": { + "itemTitle": "ストリーム {{index}}" + }, + "restartRequiredField": "再起動が必要", + "restartRequiredFooter": "設定が変更されました - 再起動が必要です", + "sections": { + "detect": "検知", + "record": "録画", + "snapshots": "スナップショット", + "motion": "モーション", + "objects": "オブジェクト", + "review": "レビュー", + "audio": "音声", + "notifications": "通知", + "live": "ライブビュー", + "timestamp_style": "タイムスタンプ", + "mqtt": "MQTT", + "database": "データベース", + "telemetry": "テレメトリ", + "auth": "認証", + "tls": "TLS", + "proxy": "プロキシ", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg", + "detectors": "検出器", + "model": "モデル", + "semantic_search": "セマンティック検索", + "genai": "生成AI", + "face_recognition": "顔認識", + "lpr": "ナンバープレート認識", + "birdseye": "バードアイ", + "masksAndZones": "マスク / ゾーン" + }, + "detect": { + "title": "検知設定" + }, + "detectors": { + "title": "検出器設定", + "singleType": "{{type}} 検出器は 1 つしか追加できません。", + "keyRequired": "検出器名は必須です。", + "keyDuplicate": "この検出器名は既に存在します。", + "noSchema": "利用可能な検出器スキーマがありません。", + "none": "検出器インスタンスが設定されていません。", + "add": "検出器を追加", + "addCustomKey": "カスタムキーを追加" + }, + "record": { + "title": "録画設定" + }, + "snapshots": { + "title": "スナップショット設定" + }, + "motion": { + "title": "モーション設定" + }, + "objects": { + "title": "オブジェクト設定" + }, + "audioLabels": { + "summary": "{{count}} 件の音声ラベルが選択されています", + "empty": "利用可能な音声ラベルがありません" + }, + "objectLabels": { + "summary": "{{count}} 種類のオブジェクトタイプが選択されています", + "empty": "利用可能なオブジェクトラベルがありません" + }, + "reviewLabels": { + "summary": "{{count}} 件のラベルが選択されています", + "empty": "利用可能なラベルがありません" + }, + "filters": { + "objectFieldLabel": "{{label}} の {{field}}" + }, + "zoneNames": { + "summary": "{{count}} 件選択", + "empty": "利用可能なゾーンがありません" + }, + "inputRoles": { + "summary": "{{count}} 件のロールが選択されています", + "empty": "利用可能なロールがありません", + "options": { + "detect": "検知", + "record": "録画", + "audio": "音声" + } + }, + "genaiRoles": { + "options": { + "embeddings": "埋め込み", + "descriptions": "説明", + "chat": "チャット" + } + }, + "semanticSearchModel": { + "placeholder": "モデルを選択…", + "builtIn": "組み込みモデル", + "genaiProviders": "生成AIプロバイダ" + }, + "semanticSearchModelSize": { + "notApplicable": "生成AIプロバイダには適用されません" + }, + "review": { + "title": "レビュー設定" + }, + "audio": { + "title": "音声設定" + }, + "notifications": { + "title": "通知設定" + }, + "live": { + "title": "ライブビュー設定" + }, + "timestamp_style": { + "title": "タイムスタンプ設定" + }, + "searchPlaceholder": "検索...", + "addCustomLabel": "カスタムラベルを追加...", + "genaiModel": { + "placeholder": "モデルを選択または入力…", + "search": "モデルを検索または入力…", + "noModels": "利用可能なモデルがありません", + "available": "利用可能なモデル", + "useCustom": "「{{value}}」を使用", + "refresh": "モデルを更新", + "probeFailed": "モデルの取得に失敗しました", + "fetchedModels": "モデル一覧を取得しました" + }, + "liveStreams": { + "streamNameLabel": "ストリーム名", + "streamNamePlaceholder": "例: Main HD Stream", + "go2rtcStreamLabel": "go2rtc ストリーム", + "go2rtcStreamPlaceholder": "go2rtc ストリームを選択してください", + "go2rtcStreamSearch": "ストリーム名を検索または入力してください…", + "noGo2rtcStreams": "設定済みの go2rtc ストリームはありません", + "availableStreams": "利用可能なストリーム", + "useCustom": "\"{{value}}\"を使用", + "addStream": "ストリームを追加" + }, + "ptzPresets": { + "placeholder": "プリセットを選択または入力してください...", + "search": "検索またはプリセットを入力してください...", + "noPresets": "プリセットはありません", + "available": "カメラプリセット", + "useCustom": "\"{{value}}\" を使用してください" + }, + "defaultRole": { + "admin": "管理者", + "viewer": "閲覧者" + } + }, + "globalConfig": { + "title": "グローバル設定", + "description": "上書きされない限り全カメラに適用されるグローバル設定を行います。", + "toast": { + "success": "グローバル設定を保存しました", + "error": "グローバル設定の保存に失敗しました", + "validationError": "入力検証に失敗しました" + } + }, + "cameraConfig": { + "title": "カメラ設定", + "description": "個別のカメラの設定を行います。グローバルのデフォルトを上書きします。", + "overriddenBadge": "上書き済み", + "resetToGlobal": "グローバル設定にリセット", + "toast": { + "success": "カメラ設定を保存しました", + "error": "カメラ設定の保存に失敗しました" + } + }, + "toast": { + "success": "設定を保存しました", + "applied": "設定を適用しました", + "successRestartRequired": "設定を保存しました。変更を適用するには Frigate を再起動してください。", + "error": "設定の保存に失敗しました", + "validationError": "入力検証に失敗しました: {{message}}", + "resetSuccess": "グローバルのデフォルトにリセットしました", + "resetError": "設定のリセットに失敗しました", + "saveAllSuccess_other": "{{count}} 件のセクションをすべて保存しました。", + "saveAllSuccessRestartRequired_other": "{{count}} 件のセクションをすべて保存しました。変更を適用するには Frigate を再起動してください。", + "saveAllPartial_other": "{{totalCount}} 件中 {{successCount}} 件のセクションを保存しました。{{failCount}} 件失敗。", + "saveAllFailure": "すべてのセクションの保存に失敗しました。" + }, + "profiles": { + "title": "プロファイル", + "activeProfile": "アクティブなプロファイル", + "noActiveProfile": "アクティブなプロファイルなし", + "active": "アクティブ", + "activated": "プロファイル「{{profile}}」を有効化しました", + "activateFailed": "プロファイルの設定に失敗しました", + "deactivated": "プロファイルを無効化しました", + "noProfiles": "プロファイルが定義されていません。", + "noOverrides": "上書きなし", + "cameraCount_other": "{{count}} 台のカメラ", + "columnCamera": "カメラ", + "columnOverrides": "プロファイルの上書き", + "baseConfig": "ベース設定", + "addProfile": "プロファイルを追加", + "newProfile": "新しいプロファイル", + "profileNamePlaceholder": "例: 在宅、外出、夜間モード", + "friendlyNameLabel": "プロファイル名", + "profileIdLabel": "プロファイル ID", + "profileIdDescription": "設定や自動化で使用される内部 ID です", + "nameInvalid": "使用できるのは小文字、数字、アンダースコアのみです", + "nameDuplicate": "この名前のプロファイルは既に存在します", + "error": { + "mustBeAtLeastTwoCharacters": "2 文字以上で入力してください", + "mustNotContainPeriod": "ピリオドは使用できません", + "alreadyExists": "この ID のプロファイルは既に存在します" + }, + "renameProfile": "プロファイル名を変更", + "renameSuccess": "プロファイル名を「{{profile}}」に変更しました", + "deleteProfile": "プロファイルを削除", + "deleteProfileConfirm": "プロファイル「{{profile}}」を全カメラから削除しますか?この操作は元に戻せません。", + "deleteSuccess": "プロファイル「{{profile}}」を削除しました", + "createSuccess": "プロファイル「{{profile}}」を作成しました", + "removeOverride": "プロファイル上書きを解除", + "deleteSection": "セクション上書きを削除", + "deleteSectionConfirm": "{{camera}} の {{profile}} プロファイルから {{section}} の上書きを削除しますか?", + "deleteSectionSuccess": "{{profile}} の {{section}} 上書きを削除しました", + "enableSwitch": "プロファイルを有効にする", + "enabledDescription": "プロファイルが有効化されています。下から新しいプロファイルを作成し、カメラ設定セクションで変更を加えて保存すると反映されます。", + "disabledDescription": "プロファイルを使うと、名前付きのカメラ設定上書きセット(例: 在宅、外出、夜間)を定義し、必要に応じて有効化できます。" + }, + "unsavedChanges": "未保存の変更があります", + "confirmReset": "リセットの確認", + "resetToDefaultDescription": "このセクションのすべての設定をデフォルト値にリセットします。この操作は元に戻せません。", + "resetToGlobalDescription": "このセクションの設定をグローバルのデフォルトにリセットします。この操作は元に戻せません。", + "go2rtcStreams": { + "title": "go2rtc ストリーム", + "description": "カメラのリストリーム用に go2rtc のストリーム設定を管理します。各ストリームは名前と 1 つ以上のソース URL を持ちます。", + "addStream": "ストリームを追加", + "addStreamDesc": "新しいストリームの名前を入力してください。この名前はカメラ設定でストリームを参照する際に使用されます。", + "addUrl": "URL を追加", + "streamNumber": "ストリーム {{index}}", + "streamName": "ストリーム名", + "streamNamePlaceholder": "例: front_door", + "streamUrlPlaceholder": "例: rtsp://user:pass@192.168.1.100/stream", + "deleteStream": "ストリームを削除", + "deleteStreamConfirm": "ストリーム「{{streamName}}」を削除してもよろしいですか?このストリームを参照しているカメラは動作しなくなる可能性があります。", + "noStreams": "go2rtc ストリームが設定されていません。ストリームを追加して始めてください。", + "validation": { + "nameRequired": "ストリーム名は必須です", + "nameDuplicate": "この名前のストリームは既に存在します", + "nameInvalid": "ストリーム名には英字、数字、アンダースコア、ハイフンのみ使用できます", + "urlRequired": "URL を少なくとも 1 つ指定してください" + }, + "renameStream": "ストリーム名を変更", + "renameStreamDesc": "このストリームの新しい名前を入力してください。名前を変更すると、この名前で参照しているカメラや他のストリームが動作しなくなる可能性があります。", + "newStreamName": "新しいストリーム名", + "ffmpeg": { + "useFfmpegModule": "互換モード (ffmpeg) を使用", + "video": "映像", + "audio": "音声", + "hardware": "ハードウェアアクセラレーション", + "videoCopy": "コピー", + "videoH264": "H.264 にトランスコード", + "videoH265": "H.265 にトランスコード", + "videoExclude": "除外", + "audioCopy": "コピー", + "audioAac": "AAC にトランスコード", + "audioOpus": "Opus にトランスコード", + "audioPcmu": "PCM μ-law にトランスコード", + "audioPcma": "PCM A-law にトランスコード", + "audioPcm": "PCM にトランスコード", + "audioMp3": "MP3 にトランスコード", + "audioExclude": "除外", + "hardwareNone": "ハードウェアアクセラレーションなし", + "hardwareAuto": "自動 (推奨)", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "ビデオコーデックを追加", + "addAudioCodec": "音声コーデックを追加", + "removeCodec": "コーデックを削除" + }, + "sourceNumber": "ソース {{index}}" + }, + "birdseye": { + "trackingMode": { + "objects": "オブジェクト", + "motion": "モーション", + "continuous": "常時" + }, + "cameraOrder": { + "label": "カメラ順序", + "description": "カメラをドラッグしてバードアイレイアウト内の順序を設定します。", + "reorderHandle": "ドラッグで並び替え", + "saving": "保存中…", + "saved": "保存しました" + } + }, + "retainMode": { + "all": "すべて", + "motion": "モーション", + "active_objects": "アクティブなオブジェクト" + }, + "previewQuality": { + "very_high": "非常に高", + "high": "高", + "medium": "中", + "low": "低", + "very_low": "非常に低" + }, + "ui": { + "timeFormat": { + "browser": "ブラウザ設定", + "12hour": "12 時間表示", + "24hour": "24 時間表示" + }, + "TimeOrDateStyle": { + "full": "完全表示", + "long": "長い形式", + "medium": "中程度", + "short": "短い形式" + }, + "unitSystem": { + "metric": "メートル法", + "imperial": "ヤード・ポンド法" + } + }, + "review": { + "imageSource": { + "recordings": "録画", + "previews": "プレビュー" + } + }, + "logger": { + "logLevel": { + "debug": "デバッグ", + "info": "情報", + "warning": "警告", + "error": "エラー", + "critical": "重大" + } + }, + "onvif": { + "profileAuto": "自動", + "profileLoading": "プロファイルを読み込み中...", + "autotracking": { + "zooming": { + "disabled": "無効", + "absolute": "絶対値", + "relative": "相対値" + } + } + }, + "modelSize": { + "small": "小", + "large": "大" + }, + "configMessages": { + "review": { + "recordDisabled": "録画が無効化されているため、レビュー項目は生成されません。", + "detectDisabled": "物体検知が無効化されています。レビュー項目はアラートと検知を分類するために検知オブジェクトが必要です。", + "allNonAlertDetections": "アラート以外のすべてのアクティビティが検知として含まれます。", + "genaiImageSourceRecordingsRecordDisabled": "画像ソースが「録画」に設定されていますが、録画は無効化されています。Frigate はプレビュー画像を代わりに使用します。" + }, + "audio": { + "noAudioRole": "audio ロールが定義されたストリームがありません。音声検知を機能させるには audio ロールを有効にする必要があります。" + }, + "audioTranscription": { + "audioDetectionDisabled": "このカメラでは音声検知が有効化されていません。音声文字起こしには音声検知が必要です。" + }, + "detect": { + "fpsGreaterThanFive": "検知 FPS を 5 より高く設定することは推奨されません。値を大きくしてもパフォーマンス上の問題を引き起こすだけで、メリットはありません。", + "disabled": "物体検知が無効化されています。スナップショット、レビュー項目、顔認識、ナンバープレート認識、生成AI などのエンリッチメントは機能しません。", + "resolutionShouldBeMultipleOfFour": "最良の結果を得るため、検知の幅と高さは 4 の倍数にしてください。他の偶数値でも動作しますが、検知ストリームに視覚的なノイズや軽微な歪みが生じる可能性があります。", + "aspectRatioMismatch": "入力した幅と高さは現在の検知解像度のアスペクト比と一致していません。映像が引き伸ばされたり歪んだりする可能性があります。" + }, + "objects": { + "genaiNoDescriptionsProvider": "説明を生成するには「descriptions」ロールを持つ生成AIプロバイダを設定する必要があります。" + }, + "faceRecognition": { + "globalDisabled": "このカメラで顔認識機能を使うには、顔認識のエンリッチメントを有効にする必要があります。", + "personNotTracked": "顔認識には「person」オブジェクトの追跡が必要です。このカメラのオブジェクト設定で「person」を有効にしてください。", + "modelSizeLarge": "「large」モデルは合理的な性能を発揮するために GPU または NPU が必要です。CPU のみのシステムでは「small」を使用してください。" + }, + "lpr": { + "globalDisabled": "このカメラで LPR 機能を使うには、ナンバープレート認識のエンリッチメントを有効にする必要があります。", + "vehicleNotTracked": "ナンバープレート認識には「car」または「motorcycle」の追跡が必要です。このカメラのオブジェクト設定でいずれかを有効にしてください。", + "modelSizeLarge": "「large」モデルは複数行のナンバープレート向けに最適化されています。お住まいの地域で複数行プレートが使われていない限り、「small」モデルの方が性能が良いためそちらを使用してください。" + }, + "record": { + "noRecordRole": "record ロールが定義されたストリームがありません。録画は機能しません。" + }, + "birdseye": { + "objectsModeDetectDisabled": "バードアイが「objects」モードに設定されていますが、このカメラの物体検知は無効化されています。このカメラはバードアイに表示されません。" + }, + "snapshots": { + "detectDisabled": "物体検知が無効化されています。スナップショットは追跡オブジェクトから生成されるため、作成されません。" + }, + "detectors": { + "mixedTypes": "すべての検出器は同じタイプである必要があります。別のタイプを使うには既存の検出器を削除してください。", + "mixedTypesSuggestion": "すべての検出器は同じタイプである必要があります。既存の検出器を削除するか、{{type}} を選択してください。" + }, + "semanticSearch": { + "jinav2SmallModelSize": "Jina V2 モデルの「small」サイズは RAM と推論コストが高くなります。専用 GPU と「large」モデルの組み合わせを推奨します。" + }, + "onvif": { + "autotrackingNoZones": "オートトラッキング機能を使用するには、少なくとも1つのゾーンが必要です。「マスク / ゾーン」でこのカメラ用のゾーンを定義し、以下でそれを必須ゾーンとして設定してください。" + } + } +} diff --git a/web/public/locales/ja/views/system.json b/web/public/locales/ja/views/system.json new file mode 100644 index 0000000..2971039 --- /dev/null +++ b/web/public/locales/ja/views/system.json @@ -0,0 +1,259 @@ +{ + "documentTitle": { + "cameras": "カメラ統計情報 - Frigate", + "general": "一般統計 - Frigate", + "storage": "ストレージ統計 - Frigate", + "enrichments": "高度解析統計 - Frigate", + "logs": { + "frigate": "Frigate ログ - Frigate", + "go2rtc": "Go2RTC ログ - Frigate", + "nginx": "Nginx ログ - Frigate", + "websocket": "メッセージログ - Frigate" + } + }, + "title": "システム", + "metrics": "システムメトリクス", + "logs": { + "download": { + "label": "ログをダウンロード" + }, + "copy": { + "label": "クリップボードにコピー", + "success": "ログをクリップボードにコピーしました", + "error": "ログをクリップボードにコピーできませんでした" + }, + "type": { + "label": "タイプ", + "timestamp": "タイムスタンプ", + "tag": "タグ", + "message": "メッセージ" + }, + "tips": "ログはサーバーからストリーミングされています", + "toast": { + "error": { + "fetchingLogsFailed": "ログの取得エラー: {{errorMessage}}", + "whileStreamingLogs": "ログのストリーミング中にエラー: {{errorMessage}}" + } + }, + "websocket": { + "label": "メッセージ", + "pause": "一時停止", + "resume": "再開", + "clear": "クリア", + "filter": { + "events": "イベント", + "classification": "分類", + "face_recognition": "顔認識", + "all": "全てのトピックス", + "topics": "トピックス", + "reviews": "レビュー", + "lpr": "LPR", + "camera_activity": "カメラアクティビティ", + "system": "システム", + "camera": "カメラ", + "all_cameras": "全てのカメラ", + "cameras_count_one": "{{count}} カメラ", + "cameras_count_other": "{{count}} カメラ" + }, + "empty": "まだメッセージは記録されていません", + "count_one": "{{count}} メッセージ", + "count_other": "{{count}} メッセージ", + "expanded": { + "payload": "ペイロード" + } + } + }, + "general": { + "title": "全般", + "detector": { + "title": "検出器", + "inferenceSpeed": "ディテクタ推論速度", + "temperature": "ディテクタ温度", + "cpuUsage": "ディテクタの CPU 使用率", + "cpuUsageInformation": "検知モデルへの入力/出力データの準備に使用される CPU。GPU やアクセラレータを使用していても、この値は推論の使用量を測定しません。", + "memoryUsage": "ディテクタのメモリ使用量" + }, + "hardwareInfo": { + "title": "ハードウェア情報", + "gpuUsage": "GPU 使用率", + "gpuMemory": "GPU メモリ", + "gpuEncoder": "GPU エンコーダー", + "gpuDecoder": "GPU デコーダー", + "gpuInfo": { + "vainfoOutput": { + "title": "vainfo 出力", + "returnCode": "戻りコード: {{code}}", + "processOutput": "プロセス出力:", + "processError": "プロセスエラー:" + }, + "nvidiaSMIOutput": { + "title": "NVIDIA SMI 出力", + "name": "名前: {{name}}", + "driver": "ドライバー: {{driver}}", + "cudaComputerCapability": "CUDA 計算能力: {{cuda_compute}}", + "vbios": "VBIOS 情報: {{vbios}}" + }, + "closeInfo": { + "label": "GPU 情報を閉じる" + }, + "copyInfo": { + "label": "GPU 情報をコピー" + }, + "toast": { + "success": "GPU 情報をクリップボードにコピーしました" + } + }, + "npuUsage": "NPU 使用率", + "npuMemory": "NPU メモリ", + "intelGpuWarning": { + "title": "Intel GPU 統計情報の警告", + "message": "GPU の統計情報を取得できません", + "description": "これは Intel の GPU 統計取得ツール(intel_gpu_top)における既知の不具合です。ハードウェアアクセラレーションやオブジェクト検知が (i)GPU 上で正しく動作している場合でも、GPU 使用率が 0% と繰り返し表示されることがあります。これは Frigate の不具合ではありません。ホストを再起動することで一時的に解消し、GPU が正常に動作していることを確認できます。本問題はパフォーマンスには影響しません。" + }, + "gpuCompute": "GPU 演算 / エンコード", + "gpuTemperature": "GPU 温度", + "npuTemperature": "NPU 温度" + }, + "otherProcesses": { + "title": "その他のプロセス", + "processCpuUsage": "プロセスの CPU 使用率", + "processMemoryUsage": "プロセスのメモリ使用量", + "series": { + "recording": "録画", + "review_segment": "レビューセグメント", + "audio_detector": "音声検知", + "go2rtc": "go2rtc", + "embeddings": "ベクトル埋め込み" + } + } + }, + "storage": { + "title": "ストレージ", + "overview": "概要", + "recordings": { + "title": "録画", + "tips": "この値は Frigate のデータベースで録画が使用している総ストレージ量を表します。Frigate はディスク上のすべてのファイルの使用量を追跡しているわけではありません。", + "earliestRecording": "利用可能な最古の録画:" + }, + "shm": { + "title": "SHM(共有メモリ)の割り当て", + "warning": "現在の SHM サイズ {{total}}MB は小さすぎます。少なくとも {{min_shm}}MB に増やしてください。", + "frameLifetime": { + "title": "フレーム保持時間", + "description": "各カメラは共有メモリ内に {{frames}} 個のフレームスロットを持ちます。最も高速なカメラのフレームレートでは、各フレームは上書きされるまで約 {{lifetime}} 秒間利用可能です。" + } + }, + "cameraStorage": { + "title": "カメラストレージ", + "camera": "カメラ", + "unusedStorageInformation": "未使用ストレージ情報", + "storageUsed": "ストレージ使用量", + "percentageOfTotalUsed": "総使用量に占める割合", + "bandwidth": "帯域幅", + "unused": { + "title": "未使用", + "tips": "Frigate の録画以外にドライブへ保存しているファイルがある場合、この値は Frigate が利用できる空き容量を正確に表さないことがあります。Frigate は録画以外のストレージ使用量を追跡しません。" + } + } + }, + "cameras": { + "title": "カメラ", + "overview": "概要", + "info": { + "aspectRatio": "アスペクト比", + "cameraProbeInfo": "{{camera}} カメラプローブ情報", + "streamDataFromFFPROBE": "ストリームデータは ffprobe で取得しています。", + "fetching": "カメラデータを取得中", + "stream": "ストリーム {{idx}}", + "video": "動画:", + "codec": "コーデック:", + "resolution": "解像度:", + "fps": "FPS:", + "unknown": "不明", + "audio": "音声:", + "error": "エラー: {{error}}", + "tips": { + "title": "カメラプローブ情報" + } + }, + "framesAndDetections": "フレーム / 検知", + "label": { + "camera": "カメラ", + "detect": "検知", + "skipped": "スキップ", + "ffmpeg": "FFmpeg", + "capture": "キャプチャ", + "overallFramesPerSecond": "全体フレーム/秒", + "overallDetectionsPerSecond": "全体検知/秒", + "overallSkippedDetectionsPerSecond": "全体スキップ検知/秒", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} キャプチャ", + "cameraDetect": "{{camName}} 検知", + "cameraFramesPerSecond": "{{camName}} フレーム/秒", + "cameraDetectionsPerSecond": "{{camName}} 検知/秒", + "cameraSkippedDetectionsPerSecond": "{{camName}} スキップ検知/秒", + "cameraGpu": "{{camName}} GPU" + }, + "toast": { + "success": { + "copyToClipboard": "プローブデータをクリップボードにコピーしました。" + }, + "error": { + "unableToProbeCamera": "カメラをプローブできません: {{errorMessage}}" + } + }, + "noCameras": { + "title": "カメラが見つかりません" + }, + "connectionQuality": { + "title": "接続品質", + "excellent": "非常に良好", + "fair": "普通", + "poor": "不良", + "unusable": "使用不可", + "fps": "FPS", + "expectedFps": "想定 FPS", + "reconnectsLastHour": "再接続回数 (直近1時間)", + "stallsLastHour": "ストール回数 (直近1時間)" + } + }, + "lastRefreshed": "最終更新: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} の FFmpeg の CPU 使用率が高い({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} の検知の CPU 使用率が高い({{detectAvg}}%)", + "healthy": "システムは正常です", + "reindexingEmbeddings": "埋め込みを再インデックス中({{processed}}% 完了)", + "cameraIsOffline": "{{camera}} はオフラインです", + "detectIsSlow": "{{detect}} が遅い({{speed}} ms)", + "detectIsVerySlow": "{{detect}} が非常に遅い({{speed}} ms)", + "shmTooLow": "/dev/shm の割り当て({{total}} MB)は少なくとも {{min}} MB に増やす必要があります。", + "debugReplayActive": "デバッグリプレイセッションが実行中です" + }, + "enrichments": { + "title": "高度解析", + "infPerSecond": "毎秒推論回数", + "embeddings": { + "image_embedding": "画像埋め込み", + "text_embedding": "テキスト埋め込み", + "face_recognition": "顔認識", + "plate_recognition": "ナンバープレート認識", + "image_embedding_speed": "画像埋め込み速度", + "face_embedding_speed": "顔埋め込み速度", + "face_recognition_speed": "顔認識速度", + "plate_recognition_speed": "ナンバープレート認識速度", + "text_embedding_speed": "テキスト埋め込み速度", + "yolov9_plate_detection_speed": "YOLOv9 ナンバープレート検知速度", + "yolov9_plate_detection": "YOLOv9 ナンバープレート検知", + "review_description": "レビュー説明", + "review_description_speed": "レビュー説明の処理速度", + "review_description_events_per_second": "レビュー説明", + "object_description": "オブジェクト説明", + "object_description_speed": "オブジェクト説明の処理速度", + "object_description_events_per_second": "オブジェクト説明", + "classification": "{{name}} の分類", + "classification_speed": "{{name}} 分類の処理速度", + "classification_events_per_second": "{{name}} 分類の毎秒イベント数" + }, + "averageInf": "平均推論時間" + } +} diff --git a/web/public/locales/km/audio.json b/web/public/locales/km/audio.json new file mode 100644 index 0000000..3070081 --- /dev/null +++ b/web/public/locales/km/audio.json @@ -0,0 +1,7 @@ +{ + "speech": "ការនិយាយ", + "babbling": "សំឡេង​រំខាន", + "yell": "ស្រែក", + "bellow": "ប៊ែលឡូវ", + "whoop": "អូប" +} diff --git a/web/public/locales/km/common.json b/web/public/locales/km/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/common.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/components/auth.json b/web/public/locales/km/components/auth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/components/camera.json b/web/public/locales/km/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/components/dialog.json b/web/public/locales/km/components/dialog.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/components/filter.json b/web/public/locales/km/components/filter.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/components/icons.json b/web/public/locales/km/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/components/input.json b/web/public/locales/km/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/components/input.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/components/player.json b/web/public/locales/km/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/components/player.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/config/cameras.json b/web/public/locales/km/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/config/global.json b/web/public/locales/km/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/config/groups.json b/web/public/locales/km/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/config/validation.json b/web/public/locales/km/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/objects.json b/web/public/locales/km/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/objects.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/chat.json b/web/public/locales/km/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/classificationModel.json b/web/public/locales/km/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/configEditor.json b/web/public/locales/km/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/events.json b/web/public/locales/km/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/events.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/explore.json b/web/public/locales/km/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/exports.json b/web/public/locales/km/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/faceLibrary.json b/web/public/locales/km/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/live.json b/web/public/locales/km/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/motionSearch.json b/web/public/locales/km/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/recording.json b/web/public/locales/km/views/recording.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/replay.json b/web/public/locales/km/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/search.json b/web/public/locales/km/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/settings.json b/web/public/locales/km/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/km/views/system.json b/web/public/locales/km/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/km/views/system.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/audio.json b/web/public/locales/kn/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/audio.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/common.json b/web/public/locales/kn/common.json new file mode 100644 index 0000000..9b5e63f --- /dev/null +++ b/web/public/locales/kn/common.json @@ -0,0 +1,5 @@ +{ + "time": { + "untilForTime": "{{time}}ವರೆಗೆ" + } +} diff --git a/web/public/locales/kn/components/auth.json b/web/public/locales/kn/components/auth.json new file mode 100644 index 0000000..6d4a2c2 --- /dev/null +++ b/web/public/locales/kn/components/auth.json @@ -0,0 +1,5 @@ +{ + "form": { + "user": "ಬಳಕೆದಾರರಹೆಸರು" + } +} diff --git a/web/public/locales/kn/components/camera.json b/web/public/locales/kn/components/camera.json new file mode 100644 index 0000000..71a4f69 --- /dev/null +++ b/web/public/locales/kn/components/camera.json @@ -0,0 +1,5 @@ +{ + "group": { + "label": "ಕ್ಯಾಮೆರಾ ಗುಂಪು" + } +} diff --git a/web/public/locales/kn/components/dialog.json b/web/public/locales/kn/components/dialog.json new file mode 100644 index 0000000..9fff19e --- /dev/null +++ b/web/public/locales/kn/components/dialog.json @@ -0,0 +1,5 @@ +{ + "restart": { + "title": "ನೀವು ಫ್ರಿಗೇಟನ್ನು ಖಂಡಿತವಗೆ ರೀಸ್ಟಾರ್ಟ್ ಮಾಡಬಯಸುತ್ತೀರಾ?" + } +} diff --git a/web/public/locales/kn/components/filter.json b/web/public/locales/kn/components/filter.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/components/icons.json b/web/public/locales/kn/components/icons.json new file mode 100644 index 0000000..c2c11d4 --- /dev/null +++ b/web/public/locales/kn/components/icons.json @@ -0,0 +1,5 @@ +{ + "iconPicker": { + "selectIcon": "ಬಿಂಬವನ್ನು ಆಯ್ಕೆಮಾಡಿ" + } +} diff --git a/web/public/locales/kn/components/input.json b/web/public/locales/kn/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/components/input.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/components/player.json b/web/public/locales/kn/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/components/player.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/config/cameras.json b/web/public/locales/kn/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/config/global.json b/web/public/locales/kn/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/config/groups.json b/web/public/locales/kn/config/groups.json new file mode 100644 index 0000000..70b1182 --- /dev/null +++ b/web/public/locales/kn/config/groups.json @@ -0,0 +1,7 @@ +{ + "audio": { + "global": { + "detection": "ಜಾಗತಿಕ ಪತ್ತೆದಾರಿ" + } + } +} diff --git a/web/public/locales/kn/config/validation.json b/web/public/locales/kn/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/objects.json b/web/public/locales/kn/objects.json new file mode 100644 index 0000000..4eb1d32 --- /dev/null +++ b/web/public/locales/kn/objects.json @@ -0,0 +1,3 @@ +{ + "person": "ವ್ಯಕ್ತಿ" +} diff --git a/web/public/locales/kn/views/chat.json b/web/public/locales/kn/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/views/classificationModel.json b/web/public/locales/kn/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/views/configEditor.json b/web/public/locales/kn/views/configEditor.json new file mode 100644 index 0000000..04d0948 --- /dev/null +++ b/web/public/locales/kn/views/configEditor.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "ಕಾನ್ಫಿಗ್ ಸಂಪಾದಕ - ಫ್ರಿಗೇಟ್" +} diff --git a/web/public/locales/kn/views/events.json b/web/public/locales/kn/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/views/events.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/views/explore.json b/web/public/locales/kn/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/views/exports.json b/web/public/locales/kn/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/views/faceLibrary.json b/web/public/locales/kn/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/views/live.json b/web/public/locales/kn/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/views/motionSearch.json b/web/public/locales/kn/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/views/recording.json b/web/public/locales/kn/views/recording.json new file mode 100644 index 0000000..c0a9826 --- /dev/null +++ b/web/public/locales/kn/views/recording.json @@ -0,0 +1,3 @@ +{ + "export": "ರಪ್ತು ಮಾಡು" +} diff --git a/web/public/locales/kn/views/replay.json b/web/public/locales/kn/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/views/search.json b/web/public/locales/kn/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/kn/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/kn/views/settings.json b/web/public/locales/kn/views/settings.json new file mode 100644 index 0000000..99f0d31 --- /dev/null +++ b/web/public/locales/kn/views/settings.json @@ -0,0 +1,5 @@ +{ + "documentTitle": { + "default": "ಸೆಟ್ಟಿಂಗ್‌ಗಳು - ಫ್ರಿಗೇಟ್" + } +} diff --git a/web/public/locales/kn/views/system.json b/web/public/locales/kn/views/system.json new file mode 100644 index 0000000..c7bd532 --- /dev/null +++ b/web/public/locales/kn/views/system.json @@ -0,0 +1,5 @@ +{ + "documentTitle": { + "cameras": "ಕ್ಯಾಮೆರಾ ಅಂಕಿಅಂಶಗಳು - ಫ್ರಿಗೇಟ್" + } +} diff --git a/web/public/locales/ko/audio.json b/web/public/locales/ko/audio.json new file mode 100644 index 0000000..dac93a5 --- /dev/null +++ b/web/public/locales/ko/audio.json @@ -0,0 +1,172 @@ +{ + "crying": "울음", + "snoring": "코골이", + "singing": "노래", + "yell": "비명", + "speech": "음성", + "babbling": "옹알이", + "bicycle": "자전거", + "a_capella": "아카펠라", + "accelerating": "가속", + "accordion": "아코디언", + "acoustic_guitar": "통기타", + "car": "차량", + "motorcycle": "원동기", + "bus": "버스", + "train": "기차", + "boat": "보트", + "bird": "새", + "cat": "고양이", + "dog": "개", + "horse": "말", + "sheep": "양", + "skateboard": "스케이트보드", + "door": "문", + "mouse": "마우스", + "keyboard": "키보드", + "sink": "싱크대", + "blender": "블렌더", + "clock": "벽시계", + "scissors": "가위", + "hair_dryer": "헤어 드라이어", + "toothbrush": "칫솔", + "vehicle": "탈 것", + "animal": "동물", + "bark": "짖는 소리", + "goat": "염소", + "bellow": "포효", + "whoop": "환성", + "whispering": "속삭임", + "laughter": "웃음", + "snicker": "낄낄 웃음", + "sigh": "한숨", + "choir": "합창", + "yodeling": "요들링", + "chant": "성가", + "mantra": "만트라", + "child_singing": "어린이 노래", + "synthetic_singing": "합성 가창", + "rapping": "랩", + "humming": "허밍", + "groan": "신음", + "grunt": "으르렁", + "whistling": "휘파람", + "breathing": "숨쉬는 소리", + "wheeze": "헐떡임", + "gasp": "헐떡임", + "pant": "거친숨", + "snort": "코골이", + "cough": "기침", + "throat_clearing": "목 긁는 소리", + "sneeze": "재채기", + "sniff": "훌쩍", + "run": "달리기", + "shuffle": "임의 재생", + "footsteps": "발소리", + "chewing": "씹는 소리", + "biting": "치는 소리", + "gargling": "가글", + "stomach_rumble": "배 꼬르륵", + "burping": "트림", + "camera": "카메라", + "hiccup": "딸꾹질", + "fart": "방귀", + "hands": "손", + "finger_snapping": "손가락 튕기기", + "clapping": "박수", + "heartbeat": "심장 박동", + "heart_murmur": "심장 잡음", + "cheering": "환호", + "applause": "환호", + "chatter": "수다", + "crowd": "군중", + "children_playing": "놀고 있는 아이들", + "pets": "반려동물", + "yip": "깽깽거림", + "howl": "하울링", + "bow_wow": "짖는 소리", + "growling": "으르렁거림", + "whimper_dog": "낑낑거림", + "purr": "가르릉거림", + "meow": "야옹", + "hiss": "하악질", + "caterwaul": "발정기 울음", + "livestock": "가축", + "clip_clop": "딸깍딸깍", + "neigh": "말 울음소리", + "cattle": "소", + "moo": "음메", + "cowbell": "워낭 소리", + "pig": "돼지", + "oink": "꿀꿀거림", + "bleat": "메에", + "fowl": "새", + "chicken": "닭", + "cluck": "닭 울음소리", + "cock_a_doodle_doo": "꼬꼬댁", + "turkey": "칠면조", + "gobble": "칠면조 울음소리", + "duck": "오리", + "quack": "오리 울음소리", + "goose": "거위", + "honk": "거위 울음소리", + "wild_animals": "야생 동물", + "roaring_cats": "맹수 포효", + "roar": "포효", + "chirp": "새 울음소리", + "squawk": "지저귐", + "pigeon": "비둘기", + "coo": "비둘기 울음소리", + "crow": "까마귀", + "caw": "까마귀 울음소리", + "owl": "부엉이", + "hoot": "부엉이 울음소리", + "flapping_wings": "날갯짓", + "bicycle_bell": "자전거 벨", + "tuning_fork": "소리굽쇠", + "chime": "차임벨", + "wind_chime": "풍경", + "harmonica": "하모니카", + "steel_guitar": "스틸 기타", + "tapping": "두드림", + "strum": "기타 스트로크", + "banjo": "밴조", + "sitar": "시타르", + "mandolin": "만돌린", + "zither": "지더", + "ukulele": "우쿨렐레", + "lawn_mower": "잔디깎이", + "chainsaw": "전기톱", + "medium_engine": "중형 엔진", + "heavy_engine": "대형 엔진", + "engine_knocking": "엔진 노킹", + "engine_starting": "엔진 시동", + "idling": "공회전", + "alarm": "알람", + "telephone": "전화", + "telephone_bell_ringing": "전화 소리", + "ringtone": "벨소리", + "telephone_dialing": "전화 다이얼", + "dial_tone": "발신음", + "cash_register": "금전등록기", + "printer": "프린터", + "single-lens_reflex_camera": "카메라 셔터", + "tools": "도구들", + "hammer": "망치", + "jackhammer": "착암기", + "sawing": "톱질", + "filing": "연마", + "sanding": "사포질", + "power_tool": "전동 도구", + "drill": "드릴", + "explosion": "폭발", + "gunshot": "총소리", + "machine_gun": "기관총", + "fusillade": "연속 총성", + "artillery_fire": "포격", + "cap_gun": "화약 총", + "fireworks": "불꽃놀이", + "firecracker": "폭죽", + "car_alarm": "차량 경보", + "power_windows": "전동 창문" +} diff --git a/web/public/locales/ko/common.json b/web/public/locales/ko/common.json new file mode 100644 index 0000000..94f0b19 --- /dev/null +++ b/web/public/locales/ko/common.json @@ -0,0 +1,312 @@ +{ + "readTheDocumentation": "문서 읽기", + "time": { + "untilForTime": "{{time}}까지", + "untilForRestart": "Frigate가 재시작될 때 까지.", + "10minutes": "10분", + "12hours": "12시간", + "1hour": "1시간", + "24hours": "24시간", + "30minutes": "30분", + "5minutes": "5분", + "untilRestart": "재시작 될 때까지", + "ago": "{{timeAgo}} 전", + "justNow": "방금", + "today": "오늘", + "yesterday": "어제", + "last7": "최근 7일", + "last14": "최근 14일", + "last30": "최근 30일", + "thisWeek": "이번 주", + "lastWeek": "저번 주", + "thisMonth": "이번 달", + "lastMonth": "저번 달", + "pm": "오후", + "am": "오전", + "yr": "{{time}}년", + "year_other": "{{time}} 년", + "mo": "{{time}}월", + "month_other": "{{time}} 월", + "d": "{{time}}일", + "day_other": "{{time}} 일", + "h": "{{time}}시", + "hour_other": "{{time}} 시", + "m": "{{time}}분", + "minute_other": "{{time}} 분", + "s": "{{time}}초", + "second_other": "{{time}} 초", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "never": "한 번도 없음", + "inProgress": "진행 중", + "invalidStartTime": "잘못된 시작 시간", + "invalidEndTime": "잘못된 종료 시간" + }, + "notFound": { + "title": "404", + "documentTitle": "찾을 수 없음 - Frigate", + "desc": "페이지 찾을 수 없음" + }, + "accessDenied": { + "title": "접근 거부", + "documentTitle": "접근 거부 - Frigate", + "desc": "이 페이지 접근 권한이 없습니다." + }, + "menu": { + "user": { + "account": "계정", + "title": "사용자", + "current": "현재 사용자:{{user}}", + "anonymous": "익명", + "logout": "로그아웃", + "setPassword": "비밀번호 설정" + }, + "system": "시스템", + "systemMetrics": "시스템 지표", + "configuration": "설정", + "systemLogs": "시스템 로그", + "settings": "설정", + "configurationEditor": "설정 편집기", + "languages": "언어", + "language": { + "en": "English (영어)", + "es": "Español (스페인어)", + "zhCN": "简体中文 (중국어 간체)", + "hi": "हिन्दी (힌두어)", + "fr": "Français (프랑스어)", + "ar": "العربية (아랍어)", + "pt": "Português (포르투갈어)", + "ptBR": "Português brasileiro (브라질 포르투갈어)", + "ru": "Русский (러시아어)", + "de": "Deutsch (독일어)", + "ja": "日本語 (일본어)", + "tr": "Türkçe (튀르키예어)", + "it": "Italiano (이탈리아어)", + "nl": "Nederlands (네덜란드어)", + "sv": "Svenska (스웨덴어)", + "cs": "Čeština (체코어)", + "nb": "Norsk Bokmål (노르웨이어 보크몰)", + "ko": "한국어", + "vi": "Tiếng Việt (베트남어)", + "fa": "فارسی (페르시아어)", + "pl": "Polski (폴란드어)", + "uk": "Українська (우크라이나어)", + "he": "עברית (히브리어)", + "el": "Ελληνικά (그리스어)", + "ro": "Română (루마니아어)", + "hu": "Magyar (헝가리어)", + "fi": "Suomi (Finnish)", + "da": "Dansk (덴마크어)", + "sk": "Slovenčina (슬로바키아어)", + "yue": "粵語 (광둥어)", + "th": "ไทย (태국어)", + "ca": "Català (카탈로니아어)", + "sr": "Српски (세르비아어)", + "sl": "Slovenščina (슬로베니아어)", + "lt": "Lietuvių (리투아니아어)", + "bg": "Български (불가리아어)", + "gl": "Galego (갈리시아어)", + "id": "Bahasa Indonesia (인도네시아어)", + "ur": "اردو (우르두어)", + "withSystem": { + "label": "시스템 설정 언어 사용" + }, + "hr": "Hrvatski (크로아티아어)" + }, + "appearance": "화면 설정", + "darkMode": { + "label": "다크 모드", + "light": "라이트", + "dark": "다크", + "withSystem": { + "label": "시스템 설정에 따라 설정" + } + }, + "withSystem": "시스템", + "theme": { + "label": "테마", + "blue": "파랑", + "green": "녹색", + "nord": "노드 (Nord)", + "red": "빨강", + "highcontrast": "고 대비", + "default": "기본값" + }, + "help": "도움말", + "documentation": { + "title": "문서", + "label": "Frigate 문서" + }, + "restart": "Frigate 재시작", + "live": { + "title": "실시간", + "allCameras": "모든 카메라", + "cameras": { + "title": "카메라", + "count_other": "{{count}} 카메라" + } + }, + "review": "다시보기", + "explore": "탐색", + "export": "내보내기", + "uiPlayground": "UI 실험실", + "faceLibrary": "얼굴 라이브러리", + "classification": "분류", + "chat": "채팅", + "actions": "작업", + "profiles": "프로필", + "features": "기능" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "피트", + "meters": "미터" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/시간", + "mbph": "MB/시간", + "gbph": "GB/시간" + } + }, + "label": { + "back": "뒤로", + "hide": "{{item}} 숨기기", + "show": "{{item}} 표시", + "ID": "아이디", + "none": "없음", + "all": "전체", + "other": "그 외" + }, + "button": { + "apply": "적용", + "reset": "리셋", + "done": "완료", + "enabled": "활성화됨", + "enable": "활성화", + "disabled": "비활성화됨", + "disable": "비활성화", + "save": "저장", + "saving": "저장 중…", + "cancel": "취소", + "close": "닫기", + "copy": "복사", + "back": "뒤로", + "history": "히스토리", + "fullscreen": "전체화면", + "exitFullscreen": "전체화면 나가기", + "pictureInPicture": "화면 속 화면", + "twoWayTalk": "양방향 말하기", + "cameraAudio": "카메라 오디오", + "on": "켜기", + "off": "끄기", + "edit": "편집", + "copyCoordinates": "코디네이트 복사", + "delete": "삭제", + "yes": "예", + "no": "아니오", + "download": "다운로드", + "info": "정보", + "suspended": "일시 정지됨", + "unsuspended": "재개", + "play": "재생", + "unselect": "선택 해제", + "export": "내보내기", + "deleteNow": "바로 삭제하기", + "next": "다음", + "add": "추가", + "undo": "실행 취소", + "copiedToClipboard": "클립보드에 복사", + "continue": "계속하기", + "modified": "수정됨", + "overridden": "재정의됨", + "resetToGlobal": "글로벌 설정으로 재설정", + "resetToDefault": "기본값으로 재설정", + "saveAll": "모두 저장", + "savingAll": "모두 저장 중. …", + "undoAll": "모두 실행 취소", + "applying": "적용 중…", + "retry": "재시도" + }, + "toast": { + "copyUrlToClipboard": "클립보드에 URL이 복사되었습니다.", + "save": { + "title": "저장", + "error": { + "title": "설정 저장 실패: {{errorMessage}}", + "noMessage": "설정 저장이 실패했습니다" + }, + "success": "설정 변경이 성공적으로 저장되었습니다." + } + }, + "role": { + "title": "역할", + "admin": "관리자", + "viewer": "감시자", + "desc": "관리자는 Frigate UI에 모든 접근 권한이 있습니다. 감시자는 카메라 감시, 돌아보기, 과거 영상 조회만 가능합니다." + }, + "pagination": { + "label": "페이지 번호", + "previous": { + "title": "이전", + "label": "이전 페이지" + }, + "next": { + "title": "다음", + "label": "다음 페이지" + }, + "more": "더 많은 페이지" + }, + "selectItem": "{{item}} 선택", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}}과 {{1}}", + "many": "{{items}} 그리고 {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "선택", + "internalID": "Frigate 내부 ID는 구성 및 데이터베이스에서 사용됩니다" + }, + "no_items": "내역 없음", + "validation_errors": "검증 오류" +} diff --git a/web/public/locales/ko/components/auth.json b/web/public/locales/ko/components/auth.json new file mode 100644 index 0000000..ad8e1fa --- /dev/null +++ b/web/public/locales/ko/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "사용자명", + "password": "비밀번호", + "login": "로그인", + "errors": { + "usernameRequired": "사용자명은 필수입니다", + "passwordRequired": "비밀번호는 필수입니다", + "rateLimit": "너무 많이 시도했습니다. 다음에 다시 시도하세요.", + "loginFailed": "로그인 실패", + "unknownError": "알려지지 않은 에러. 로그를 확인하세요.", + "webUnknownError": "알려지지 않은 에러. 콘솔 로그를 확인하세요." + }, + "firstTimeLogin": "처음 로그인하시나요? 로그인 정보는 Frigate 로그에 있습니다." + } +} diff --git a/web/public/locales/ko/components/camera.json b/web/public/locales/ko/components/camera.json new file mode 100644 index 0000000..610fae4 --- /dev/null +++ b/web/public/locales/ko/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "카메라 그룹", + "add": "카메라 그룹 추가", + "edit": "카메라 그룹 편집", + "delete": { + "label": "카메라 그룹 삭제", + "confirm": { + "title": "삭제 확인", + "desc": "정말로 카메라 그룹을 삭제하시겠습니까 {{name}}?" + } + }, + "name": { + "label": "이름", + "placeholder": "이름을 입력하세요…", + "errorMessage": { + "mustLeastCharacters": "카메라 그룹 이름은 최소 2자 이상 써야합니다.", + "exists": "이미 존재하는 카메라 그룹 이름입니다.", + "nameMustNotPeriod": "카메라 그룹 이름에 마침표(.)를 넣을 수 없습니다.", + "invalid": "설정 불가능한 카메라 그룹 이름." + } + }, + "cameras": { + "label": "카메라", + "desc": "이 그룹에 넣을 카메라 선택하기." + }, + "icon": "아이콘", + "success": "카메라 그룹 {{name}} 저장되었습니다.", + "camera": { + "birdseye": "버드아이", + "setting": { + "label": "카메라 스트리밍 설정", + "title": "{{cameraName}} 스트리밍 설정", + "desc": "카메라 그룹 대시보드의 실시간 스트리밍 옵션을 변경하세요. 이 설정은 기기/브라우저에 따라 다릅니다.", + "audioIsAvailable": "이 카메라는 오디오 기능을 사용할 수 있습니다", + "audioIsUnavailable": "이 카메라는 오디오 기능을 사용할 수 없습니다", + "audio": { + "tips": { + "title": "오디오를 출력하려면 카메라가 지원하거나 go2rtc에서 설정해야합니다." + } + }, + "stream": "스트림", + "placeholder": "스트림 선택", + "streamMethod": { + "label": "스트리밍 방식", + "placeholder": "스트리밍 방식 선택", + "method": { + "noStreaming": { + "label": "스트리밍 없음", + "desc": "카메라 이미지는 1분에 한 번만 보여지며 라이브 스트리밍은 되지 않습니다." + }, + "smartStreaming": { + "label": "스마트 스트리밍 (추천함)", + "desc": "스마트 스트리밍은 감지되는 활동이 없을 때 대역폭과 자원을 절약하기 위해 1분마다 한 번 카메라 이미지를 업데이트합니다. 활동이 감지되면, 이미지는 자동으로 라이브 스트림으로 원활하게 전환됩니다." + }, + "continuousStreaming": { + "label": "지속적인 스트리밍", + "desc": { + "title": "활동이 감지되지 않더라도 카메라 이미지가 대시보드에서 항상 실시간 스트림됩니다.", + "warning": "지속적인 스트리밍은 높은 대역폭 사용과 퍼포먼스 이슈를 발생할 수 있습니다. 사용에 주의해주세요." + } + } + } + }, + "compatibilityMode": { + "label": "호환 모드", + "desc": "이 옵션은 카메라 라이브 스트림 화면의 색상이 왜곡 되었거나 이미지 오른쪽에 대각선이 나타날때만 사용하세요." + } + } + } + }, + "debug": { + "options": { + "label": "설정", + "title": "옵션", + "showOptions": "옵션 보기", + "hideOptions": "옵션 숨기기" + }, + "boundingBox": "감지 영역 상자", + "timestamp": "시간 기록", + "zones": "구역 (Zones)", + "mask": "마스크", + "motion": "움직임", + "regions": "영역 (Regions)", + "paths": "경로" + } +} diff --git a/web/public/locales/ko/components/dialog.json b/web/public/locales/ko/components/dialog.json new file mode 100644 index 0000000..9a1c9ed --- /dev/null +++ b/web/public/locales/ko/components/dialog.json @@ -0,0 +1,126 @@ +{ + "restart": { + "title": "Frigate을 정말로 다시 시작할까요?", + "button": "재시작", + "restarting": { + "title": "Frigate이 재시작 중입니다", + "content": "이 페이지는 {{countdown}} 뒤에 새로 고침 됩니다.", + "button": "강제 재시작" + }, + "description": "이 작업은 Frigate가 재시작 되는 동안 잠시 작동이 중지됩니다." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Frigate+에 등록하기", + "desc": "제외하려는 위치에서 감지된 객체는 '오감지(False Positive)'가 아닙니다. 이를 오감지로 제출하면 모델 학습에 혼선을 줄 수 있습니다." + }, + "review": { + "question": { + "label": "Frigate +에 이 레이블 등록하기", + "ask_a": "이 것은 {{label}} 인가요?", + "ask_an": "이 것은 {{label}} 인가요?", + "ask_full": "이 것은 {{untranslatedLabel}} ({{translatedLabel}}) 인가요?" + }, + "state": { + "submitted": "제출됨" + } + } + }, + "video": { + "viewInHistory": "히스토리 보기" + } + }, + "export": { + "time": { + "fromTimeline": "타임라인에서 선택하기", + "lastHour_other": "지난 {{count}} 시간­", + "custom": "커스텀", + "start": { + "title": "시작 시간", + "label": "시작 시간 선택" + }, + "end": { + "title": "종료 시간", + "label": "종료 시간 선택" + } + }, + "name": { + "placeholder": "내보내기 이름" + }, + "select": "선택", + "export": "내보내기", + "selectOrExport": "선택 또는 내보내기", + "toast": { + "success": "내보내기가 성공적으로 시작되었습니다. 내보내기 페이지에서 파일을 보실 수 있습니다.", + "error": { + "failed": "내보내기 시작 실패:{{error}}", + "endTimeMustAfterStartTime": "종료 시간은 시작 시간보다 뒤에 있어야합니다", + "noVaildTimeSelected": "유효한 시간 범위가 선택되지 않았습니다" + }, + "view": "보기" + }, + "fromTimeline": { + "saveExport": "내보내기 저장", + "previewExport": "내보내기 미리보기" + }, + "case": { + "label": "유형", + "placeholder": "유형 선택" + } + }, + "streaming": { + "label": "스트림", + "restreaming": { + "disabled": "이 카메라는 재 스트리밍이 되지 않습니다.", + "desc": { + "title": "이 카메라를 위해 추가적인 라이브 뷰 옵션과 오디오를 go2rtc에서 설정하세요." + } + }, + "showStats": { + "label": "스트림 통계 보기", + "desc": "이 옵션을 활성화하면 스트림 통계가 카메라 피드에 나타납니다." + }, + "debugView": "디버그 뷰" + }, + "search": { + "saveSearch": { + "label": "검색 저장", + "desc": "저장된 검색에 이름을 지정해주세요.", + "placeholder": "검색에 이름 입력하기", + "overwrite": "{{searchName}} (이/가) 이미 존재합니다. 값을 덮어 씌웁니다.", + "success": "{{searchName}} 검색이 저장되었습니다.", + "button": { + "save": { + "label": "이 검색 저장하기" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "삭제 확인", + "desc": { + "selected": "이 리뷰 항목과 관련된 모든 녹화된 영상을 삭제하시겠습니까?

    다음에 이 팝업을 건너뛰려면 Shift 키를 누르고 삭제하세요." + }, + "toast": { + "success": "선택한 리뷰 항목과 관련된 동영상 파일이 성공적으로 삭제되었습니다.", + "error": "삭제 실패: {{error}}" + } + }, + "button": { + "export": "내보내기", + "markAsReviewed": "검토 완료로 표시", + "markAsUnreviewed": "검토 안 함 표시", + "deleteNow": "지금 삭제" + } + }, + "imagePicker": { + "selectImage": "추적된 객체의 썸네일을 선택하세요", + "unknownLabel": "저장된 트리거 이미지", + "search": { + "placeholder": "레이블 또는 서브 레이블로 검색..." + }, + "noImages": "표시할 썸네일이 없습니다" + } +} diff --git a/web/public/locales/ko/components/filter.json b/web/public/locales/ko/components/filter.json new file mode 100644 index 0000000..e3f3e6a --- /dev/null +++ b/web/public/locales/ko/components/filter.json @@ -0,0 +1,43 @@ +{ + "filter": "필터", + "labels": { + "label": "레이블", + "all": { + "title": "모든 레이블", + "short": "레이블" + }, + "count_one": "레이블 {{count}}개", + "count_other": "레이블 {{count}}개" + }, + "zones": { + "label": "구역", + "all": { + "title": "모든 구역", + "short": "구역" + } + }, + "dates": { + "selectPreset": "프리셋 선택…", + "all": { + "title": "모든 날짜", + "short": "날짜" + } + }, + "timeRange": "시간 구역", + "subLabels": { + "label": "서브 레이블", + "all": "모든 서브 레이블" + }, + "more": "더 많은 필터", + "classes": { + "label": "분류", + "all": { + "title": "모든 분류" + }, + "count_one": "{{count}}개 클래스", + "count_other": "{{count}}개 클래스" + }, + "reset": { + "label": "기본값으로 필터 초기화" + } +} diff --git a/web/public/locales/ko/components/icons.json b/web/public/locales/ko/components/icons.json new file mode 100644 index 0000000..fb1b47c --- /dev/null +++ b/web/public/locales/ko/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "아이콘을 선택해주세요", + "search": { + "placeholder": "아이콘 검색 중…" + } + } +} diff --git a/web/public/locales/ko/components/input.json b/web/public/locales/ko/components/input.json new file mode 100644 index 0000000..00a19b7 --- /dev/null +++ b/web/public/locales/ko/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "비디오 다운로드", + "toast": { + "success": "다시보기 항목 다운로드가 시작되었습니다." + } + } + } +} diff --git a/web/public/locales/ko/components/player.json b/web/public/locales/ko/components/player.json new file mode 100644 index 0000000..e6b1f0d --- /dev/null +++ b/web/public/locales/ko/components/player.json @@ -0,0 +1,52 @@ +{ + "submitFrigatePlus": { + "submit": "제출", + "title": "이 프레임을 Frigate+에 제출하시겠습니까?", + "previewError": "스냅샷 미리보기를 불러올 수 없습니다. 현재 녹화된 영상을 사용할 수 없을 수 있습니다." + }, + "stats": { + "bandwidth": { + "short": "대역폭", + "title": "대역폭:" + }, + "latency": { + "short": { + "title": "지연", + "value": "{{seconds}} 초" + }, + "title": "지연:", + "value": "{{seconds}} 초" + }, + "streamType": { + "short": "종류", + "title": "스트림 종류:" + }, + "totalFrames": "총 프레임:", + "droppedFrames": { + "title": "손실 프레임:", + "short": { + "title": "손실됨", + "value": "{{droppedFrames}} 프레임" + } + }, + "decodedFrames": "복원된 프레임:", + "droppedFrameRate": "프레임 손실률:" + }, + "noRecordingsFoundForThisTime": "이 시간대에는 녹화본이 없습니다", + "noPreviewFound": "미리 보기를 찾을 수 없습니다", + "noPreviewFoundFor": "{{cameraName}}에 미리보기를 찾을 수 없습니다", + "livePlayerRequiredIOSVersion": "이 라이브 스트림 방식은 iOS 17.1 이거나 높은 버전이 필요합니다.", + "streamOffline": { + "title": "스트림 오프라인", + "desc": "{{cameraName}} 카메라에 감지(detect) 스트림의 프레임을 얻지 못했습니다. 에러 로그를 확인하세요" + }, + "cameraDisabled": "카메라를 이용할 수 없습니다", + "toast": { + "success": { + "submittedFrigatePlus": "Frigate+에 프레임이 성공적으로 제출됐습니다" + }, + "error": { + "submitFrigatePlusFailed": "Frigate+에 프레임을 보내지 못했습니다" + } + } +} diff --git a/web/public/locales/ko/config/cameras.json b/web/public/locales/ko/config/cameras.json new file mode 100644 index 0000000..2e5d533 --- /dev/null +++ b/web/public/locales/ko/config/cameras.json @@ -0,0 +1,63 @@ +{ + "label": "카메라 설정", + "name": { + "label": "카메라 이름", + "description": "카메라 이름은 필수 항목입니다" + }, + "friendly_name": { + "label": "별칭", + "description": "Frigate UI에서 사용되는 카메라 별칭" + }, + "enabled": { + "label": "활성화됨", + "description": "활성화됨" + }, + "audio": { + "label": "오디오 이벤트", + "description": "이 카메라의 오디오 기반 이벤트 감지 설정입니다.", + "enabled": { + "label": "오디오 감지 활성화", + "description": "이 카메라의 오디오 이벤트 감지를 활성화하거나 비활성화합니다." + }, + "max_not_heard": { + "label": "종료 타임아웃", + "description": "오디오 이벤트가 종료되기 전, 설정된 오디오 유형이 감지되지 않는 시간(초)입니다." + }, + "min_volume": { + "label": "최소 볼륨", + "description": "오디오 감지를 실행하는 데 필요한 최소 RMS 볼륨 임계값으로, 낮을수록 민감도가 높아집니다(예: 200 높음, 500 보통, 1000 낮음)." + }, + "listen": { + "label": "청취 유형", + "description": "감지할 오디오 이벤트 유형 목록입니다(예: bark, fire_alarm, scream, speech, yell)." + }, + "filters": { + "label": "오디오 필터", + "description": "오탐지를 줄이기 위해 사용되는 신뢰도 임계값과 같은 오디오 유형별 필터 설정입니다." + }, + "enabled_in_config": { + "label": "원래 오디오 상태" + } + }, + "mqtt": { + "label": "MQTT" + }, + "notifications": { + "label": "알림", + "enabled": { + "label": "알림 활성화" + }, + "email": { + "label": "알림 이메일", + "description": "푸시 알림에 사용되거나 특정 알림 제공업체에서 요구하는 이메일 주소입니다." + }, + "cooldown": { + "label": "알림 재발송 대기 시간", + "description": "수신자에게 스팸 메일을 보내는 것을 방지하기 위해 알림 재발송 대기시간(초)을 설정합니다." + }, + "enabled_in_config": { + "label": "초기 알림 활성 상태", + "description": "초기 구성에서 알림이 활성화되었는지 여부를 나타냅니다." + } + } +} diff --git a/web/public/locales/ko/config/global.json b/web/public/locales/ko/config/global.json new file mode 100644 index 0000000..95fa0b1 --- /dev/null +++ b/web/public/locales/ko/config/global.json @@ -0,0 +1,199 @@ +{ + "version": { + "label": "현재 설정 버전", + "description": "마이그레이션 및 데이터 형식 변경 확인을 위한 현재 설정의 버전 정보(숫자 또는 문자열)입니다." + }, + "safe_mode": { + "label": "안전 모드", + "description": "활성화하면 문제 해결을 위해 기능이 제한된 안전 모드로 Frigate를 시작합니다." + }, + "environment_vars": { + "label": "환경 변수", + "description": "Home Assistant OS에서 Frigate 프로세스에 설정할 환경 변수의 키/값 쌍입니다. HAOS가 아닌 사용자는 대신 Docker 환경 변수 설정을 사용해야 합니다." + }, + "logger": { + "label": "로깅", + "description": "기본 로그 상세 수준 및 구성 요소별 로그 수준 재정의를 제어합니다.", + "default": { + "label": "로그 수준", + "description": "기본 전역 로그 상세 수준(debug, info, warning, error)입니다." + }, + "logs": { + "label": "프로세스별 로그 수준", + "description": "특정 모듈의 상세 수준을 높이거나 낮추기 위한 구성 요소별 로그 수준 재정의입니다." + } + }, + "audio": { + "label": "오디오 이벤트", + "enabled": { + "label": "오디오 감지 활성화" + }, + "max_not_heard": { + "label": "종료 타임아웃", + "description": "오디오 이벤트가 종료되기 전, 설정된 오디오 유형이 감지되지 않는 시간(초)입니다." + }, + "min_volume": { + "label": "최소 볼륨", + "description": "오디오 감지를 실행하는 데 필요한 최소 RMS 볼륨 임계값으로, 낮을수록 민감도가 높아집니다(예: 200 높음, 500 보통, 1000 낮음)." + }, + "listen": { + "label": "청취 유형", + "description": "감지할 오디오 이벤트 유형 목록입니다(예: bark, fire_alarm, scream, speech, yell)." + }, + "filters": { + "label": "오디오 필터", + "description": "오탐지를 줄이기 위해 사용되는 신뢰도 임계값과 같은 오디오 유형별 필터 설정입니다." + }, + "enabled_in_config": { + "label": "원래 오디오 상태" + } + }, + "auth": { + "label": "인증", + "description": "쿠키 및 속도 제한 옵션을 포함한 인증 및 세션 관련 설정입니다.", + "enabled": { + "label": "인증 활성화", + "description": "Frigate UI에 대한 기본 인증을 활성화합니다." + }, + "reset_admin_password": { + "label": "관리자 비밀번호 재설정", + "description": "true로 설정하면 시작 시 관리자 비밀번호를 재설정하고 새 비밀번호를 로그에 출력합니다." + }, + "cookie_name": { + "label": "JWT 쿠키 이름", + "description": "자체 인증용 JWT 토큰을 저장할 쿠키 이름입니다." + }, + "cookie_secure": { + "label": "보안 쿠키 설정", + "description": "인증 쿠키에 보안 플래그를 설정합니다. TLS를 사용하는 경우 'True'로 설정해야 합니다." + }, + "session_length": { + "label": "세션 길이", + "description": "JWT 기반 세션의 유지 시간(초)입니다." + }, + "refresh_time": { + "label": "세션 갱신 주기", + "description": "세션 만료까지 남은 시간이 몇 초 남지 않을 경우, 세션 시간을 다시 최대로 연장합니다." + }, + "failed_login_rate_limit": { + "label": "로그인 실패 제한", + "description": "무차별 대입 공격을 줄이기 위해 로그인 시도 실패 횟수를 제한하는 규칙을 적용합니다." + }, + "trusted_proxies": { + "label": "신뢰할 수 있는 프록시", + "description": "속도 제한을 위해 클라이언트 IP를 결정할 때 사용되는 신뢰할 수 있는 프록시 IP 목록입니다." + }, + "hash_iterations": { + "label": "해시 반복 횟수", + "description": "사용자 암호를 해싱할 때 사용할 PBKDF2-SHA256 반복 횟수입니다." + }, + "roles": { + "label": "역할 할당", + "description": "역할별로 접근 가능한 카메라 목록을 매핑합니다. 목록이 비어 있으면 해당 역할에 모든 카메라 접근 권한을 부여합니다." + }, + "admin_first_time_login": { + "label": "관리자 초기 로그인 설정", + "description": "활성화 시, 관리자 비밀번호 초기화 후 로그인 방법 안내 링크가 로그인 페이지에 표시됩니다. " + } + }, + "database": { + "label": "데이터베이스", + "description": "추적된 객체 및 녹화 메타데이터를 저장하는 SQLite 데이터베이스 설정입니다.", + "path": { + "label": "데이터베이스 경로", + "description": "Frigate SQLite 데이터베이스 파일이 저장될 파일 시스템 경로입니다." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "라이브 스트림 중계 및 번역에 사용되는 통합 go2rtc 리스트리밍 서비스 설정입니다." + }, + "mqtt": { + "label": "MQTT", + "description": "MQTT 브로커에 원격 측정 데이터, 스냅샷 및 이벤트 세부 정보를 연결하고 게시하기 위한 설정입니다.", + "enabled": { + "label": "MQTT 활성화", + "description": "상태, 이벤트 및 스냅샷에 대한 MQTT 통합을 활성화 또는 비활성화합니다." + }, + "host": { + "label": "MQTT 호스트", + "description": "MQTT 브로커의 호스트 이름 또는 IP 주소입니다." + }, + "port": { + "label": "MQTT 포트", + "description": "MQTT 브로커의 포트 번호입니다 (일반적인 포트는 1883입니다)." + }, + "topic_prefix": { + "label": "토픽 접두사", + "description": "Frigate의 모든 MQTT 메시지에 사용할 접두사입니다. 여러 대의 Frigate를 실행하는 경우 각각 고유한 이름을 사용해야 합니다." + }, + "client_id": { + "label": "클라이언트 ID", + "description": "MQTT 브로커 연결 시 사용하는 클라이언트 식별자입니다. 인스턴스마다 고유한 이름을 사용해야 합니다." + }, + "stats_interval": { + "label": "통계 간격", + "description": "시스템 및 카메라 통계 정보를 MQTT로 전송하는 간격(초)입니다." + }, + "user": { + "label": "MQTT 사용자 이름", + "description": "MQTT 사용자 이름(선택 사항)입니다. 환경 변수나 비밀 값(Secrets)을 통해 입력할 수 있습니다." + }, + "password": { + "label": "MQTT 비밀번호", + "description": "MQTT 비밀번호(선택 사항)입니다. 환경 변수나 비밀 값(Secrets)을 통해 입력할 수 있습니다." + }, + "tls_ca_certs": { + "label": "TLS CA 인증서", + "description": "브로커와의 TLS 연결에 사용할 CA 인증서 경로(자체 서명 인증서의 경우)." + }, + "tls_client_cert": { + "label": "클라이언트 인증서", + "description": "TLS 상호 인증을 위한 클라이언트 인증서 경로입니다. 클라이언트 인증서를 사용할 때는 사용자 이름/암호를 설정하지 마십시오." + }, + "tls_client_key": { + "label": "클라이언트 키", + "description": "클라이언트 인증서의 개인 키 경로입니다." + }, + "tls_insecure": { + "label": "TLS 비보안 모드", + "description": "호스트 이름 확인을 건너뛰어 안전하지 않은 TLS 연결을 허용합니다(권장하지 않음)." + }, + "qos": { + "label": "MQTT QoS", + "description": "MQTT 메시지 전송 및 구독에 대한 서비스 품질(QoS) 등급입니다 (0, 1, 2 중 선택)." + } + }, + "notifications": { + "label": "알림", + "description": "모든 카메라에 대한 알림을 활성화하고 제어하는 설정입니다. 카메라별로 설정을 재정의할 수 있습니다.", + "enabled": { + "label": "알림 활성화", + "description": "모든 카메라에 대한 알림을 활성화 또는 비활성화할 수 있으며, 카메라별로 설정을 재정의할 수 있습니다." + }, + "email": { + "label": "알림 이메일", + "description": "푸시 알림에 사용되거나 특정 알림 제공업체에서 요구하는 이메일 주소입니다." + }, + "cooldown": { + "label": "알림 재발송 대기 시간", + "description": "수신자에게 스팸 메일을 보내는 것을 방지하기 위해 알림 재발송 대기시간(초)을 설정합니다." + }, + "enabled_in_config": { + "label": "초기 알림 활성 상태", + "description": "초기 구성에서 알림이 활성화되었는지 여부를 나타냅니다." + } + }, + "networking": { + "label": "네트워킹", + "description": "Frigate 엔드포인트에 대한 IPv6 활성화와 같은 네트워크 관련 설정입니다.", + "ipv6": { + "label": "IPv6 구성", + "description": "Frigate 네트워크 서비스에 대한 IPv6 관련 설정입니다.", + "enabled": { + "label": "IPv6 활성화", + "description": "Frigate 서비스(API 및 UI)에 IPv6 지원이 필요한 경우 활성화하십시오." + } + } + } +} diff --git a/web/public/locales/ko/config/groups.json b/web/public/locales/ko/config/groups.json new file mode 100644 index 0000000..4578c83 --- /dev/null +++ b/web/public/locales/ko/config/groups.json @@ -0,0 +1,57 @@ +{ + "audio": { + "global": { + "detection": "전체 감지", + "sensitivity": "전체 민감도" + }, + "cameras": { + "detection": "감지", + "sensitivity": "민감도" + } + }, + "timestamp_style": { + "global": { + "appearance": "전역 외관" + }, + "cameras": { + "appearance": "외관" + } + }, + "motion": { + "global": { + "sensitivity": "전역 민감도", + "algorithm": "전역 알고리즘" + }, + "cameras": { + "sensitivity": "민감도", + "algorithm": "알고리즘" + } + }, + "snapshots": { + "global": { + "display": "전역 표시" + }, + "cameras": { + "display": "표시" + } + }, + "detect": { + "global": { + "resolution": "전역 해상도", + "tracking": "전역 추적" + }, + "cameras": { + "resolution": "해상도", + "tracking": "추적" + } + }, + "objects": { + "global": { + "tracking": "전역 추적", + "filtering": "전역 필터링" + }, + "cameras": { + "tracking": "추적" + } + } +} diff --git a/web/public/locales/ko/config/validation.json b/web/public/locales/ko/config/validation.json new file mode 100644 index 0000000..3e0a5a2 --- /dev/null +++ b/web/public/locales/ko/config/validation.json @@ -0,0 +1,32 @@ +{ + "minimum": "최소 {{limit}} 이상", + "maximum": "최대 {{limit}} 이하", + "exclusiveMinimum": "최소 {{limit}} 초과", + "exclusiveMaximum": "최대 {{limit}} 미만", + "minLength": "{{limit}}자 이상이어야 합니다", + "maxLength": "{{limit}}자 이하여야 합니다", + "minItems": "{{limit}}개 이상이어야 합니다", + "maxItems": "{{limit}}개 이하여야 합니다", + "pattern": "잘못된 형식", + "required": "이 항목은 필수 입력 사항입니다", + "type": "잘못된 유형입니다", + "enum": "허용된 값 중 하나여야 합니다", + "const": "값이 예상된 상수와 일치하지 않습니다", + "uniqueItems": "모든 항목은 고유해야 합니다", + "format": "잘못된 형식", + "additionalProperties": "알 수 없는 속성은 허용되지 않습니다", + "oneOf": "허용된 형식 중 하나와 일치해야 합니다", + "anyOf": "허용된 형식 중 최소 하나와 일치해야 합니다", + "proxy": { + "header_map": { + "roleHeaderRequired": "역할 매핑이 설정된 경우 역할 헤더(Role header)가 필수입니다." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "각 역할은 하나의 입력 스트림에만 할당할 수 있습니다.", + "detectRequired": "최소 하나의 입력 스트림에 'detect' 역할이 할당되어야 합니다.", + "hwaccelDetectOnly": "하드웨어 가속 설정은 'detect' 스트림에서만 가능합니다." + } + } +} diff --git a/web/public/locales/ko/objects.json b/web/public/locales/ko/objects.json new file mode 100644 index 0000000..da5ca78 --- /dev/null +++ b/web/public/locales/ko/objects.json @@ -0,0 +1,120 @@ +{ + "person": "사람", + "bicycle": "자전거", + "car": "차량", + "motorcycle": "원동기", + "airplane": "비행기", + "bus": "버스", + "train": "기차", + "boat": "보트", + "traffic_light": "신호등", + "fire_hydrant": "소화전", + "street_sign": "도로 표지판", + "stop_sign": "정지 표지판", + "parking_meter": "주차 요금 정산기", + "bench": "벤치", + "bird": "새", + "cat": "고양이", + "dog": "개", + "horse": "말", + "sheep": "양", + "cow": "소", + "elephant": "코끼리", + "bear": "곰", + "zebra": "얼룩말", + "giraffe": "기린", + "hat": "모자", + "backpack": "백팩", + "umbrella": "우산", + "shoe": "신발", + "eye_glasses": "안경", + "handbag": "핸드백", + "tie": "타이", + "suitcase": "슈트케이스", + "frisbee": "프리스비", + "skis": "스키", + "snowboard": "스노우보드", + "sports_ball": "스포츠 볼", + "kite": "연", + "baseball_bat": "야구 방망이", + "baseball_glove": "야구 글로브", + "skateboard": "스케이트보드", + "surfboard": "서핑보드", + "tennis_racket": "테니스 라켓", + "bottle": "병", + "plate": "번호판", + "wine_glass": "와인잔", + "cup": "컵", + "fork": "포크", + "knife": "칼", + "spoon": "숟가락", + "bowl": "보울", + "banana": "바나나", + "apple": "사과", + "sandwich": "샌드위치", + "orange": "오렌지", + "broccoli": "브로콜리", + "carrot": "당근", + "hot_dog": "핫도그", + "pizza": "피자", + "donut": "도넛", + "cake": "케이크", + "chair": "의자", + "couch": "소파", + "potted_plant": "화분", + "bed": "침대", + "mirror": "거울", + "dining_table": "식탁", + "window": "창문", + "desk": "책상", + "toilet": "화장실", + "door": "문", + "tv": "TV", + "laptop": "랩탑", + "mouse": "마우스", + "remote": "리모콘", + "keyboard": "키보드", + "cell_phone": "휴대폰", + "microwave": "전자레인지", + "oven": "오븐", + "toaster": "토스터기", + "sink": "싱크대", + "refrigerator": "냉장고", + "blender": "블렌더", + "book": "책", + "clock": "벽시계", + "vase": "꽃병", + "scissors": "가위", + "teddy_bear": "테디베어", + "hair_dryer": "헤어 드라이어", + "toothbrush": "칫솔", + "hair_brush": "빗", + "vehicle": "탈 것", + "squirrel": "다람쥐", + "deer": "사슴", + "animal": "동물", + "bark": "짖는 소리", + "fox": "여우", + "goat": "염소", + "rabbit": "토끼", + "raccoon": "라쿤", + "robot_lawnmower": "로봇 잔디깎기", + "waste_bin": "쓰레기통", + "on_demand": "수동", + "face": "얼굴", + "license_plate": "차량 번호판", + "package": "패키지", + "bbq_grill": "바베큐 그릴", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/web/public/locales/ko/views/chat.json b/web/public/locales/ko/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ko/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ko/views/classificationModel.json b/web/public/locales/ko/views/classificationModel.json new file mode 100644 index 0000000..832c372 --- /dev/null +++ b/web/public/locales/ko/views/classificationModel.json @@ -0,0 +1,35 @@ +{ + "documentTitle": "분류 모델 - Frigate", + "details": { + "scoreInfo": "점수는 해당 객체에 대한 모든 탐지 결과의 평균 분류 신뢰도를 나타냅니다.", + "none": "없음", + "unknown": "알 수 없음" + }, + "button": { + "deleteClassificationAttempts": "분류 이미지 삭제", + "renameCategory": "클래스 이름 변경", + "deleteCategory": "클래스 삭제", + "deleteImages": "이미지 삭제", + "trainModel": "모델 훈련", + "addClassification": "분류 추가", + "deleteModels": "모델 삭제", + "editModel": "모델 편집" + }, + "tooltip": { + "trainingInProgress": "모델이 현재 학습 중입니다", + "noNewImages": "훈련할 새 이미지가 없습니다. 먼저 데이터셋에서 더 많은 이미지를 분류하세요.", + "noChanges": "마지막 훈련 이후 데이터셋에 변경 사항이 없습니다.", + "modelNotReady": "모델이 훈련 준비가 되지 않았습니다" + }, + "toast": { + "success": { + "deletedModel_other": "{{count}}개 모델을 성공적으로 삭제했습니다", + "categorizedImage": "이미지 분류 성공", + "reclassifiedImage": "이미지 재분류 성공", + "trainedModel": "모델 훈련 완료." + } + }, + "train": { + "titleShort": "최근" + } +} diff --git a/web/public/locales/ko/views/configEditor.json b/web/public/locales/ko/views/configEditor.json new file mode 100644 index 0000000..bb8a84c --- /dev/null +++ b/web/public/locales/ko/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "설정 편집기 - Frigate", + "configEditor": "설정 편집기", + "safeConfigEditor": "설정 편집기 (안전 모드)", + "safeModeDescription": "설정 오류로 인해 Frigate가 안전 모드로 전환되었습니다.", + "copyConfig": "설정 복사", + "saveAndRestart": "저장 & 재시작", + "saveOnly": "저장만 하기", + "confirm": "저장 없이 나갈까요?", + "toast": { + "success": { + "copyToClipboard": "설정이 클립보드에 저장되었습니다." + }, + "error": { + "savingError": "설정 저장 오류" + } + } +} diff --git a/web/public/locales/ko/views/events.json b/web/public/locales/ko/views/events.json new file mode 100644 index 0000000..3e357be --- /dev/null +++ b/web/public/locales/ko/views/events.json @@ -0,0 +1,60 @@ +{ + "alerts": "경보", + "detections": "대상 감지", + "motion": { + "label": "움직임 감지", + "only": "움직임 감지만" + }, + "allCameras": "모든 카메라", + "empty": { + "alert": "다시 볼 '경보' 영상이 없습니다", + "detection": "다시 볼 '대상 감지' 영상이 없습니다", + "motion": "움직임 감지 데이터가 없습니다", + "recordingsDisabled": { + "title": "녹화가 활성화되어야 합니다", + "description": "다시 보기 항목은 해당 카메라에서 녹화가 활성화된 경우에만 카메라에 대해 생성할 수 있습니다." + } + }, + "timeline": { + "label": "타임라인" + }, + "timeline.aria": "타임라인 선택", + "events": { + "label": "이벤트", + "aria": "이벤트 선택", + "noFoundForTimePeriod": "이 시간대에 이벤트가 없습니다." + }, + "detail": { + "noDataFound": "다시 볼 상세 데이터가 없습니다", + "aria": "상세 보기", + "trackedObject_one": "추적 대상", + "trackedObject_other": "추적 대상", + "noObjectDetailData": "상세 보기 데이터가 없습니다.", + "label": "세부 정보" + }, + "objectTrack": { + "trackedPoint": "추적 포인트", + "clickToSeek": "이 시점으로 이동" + }, + "documentTitle": "다시 보기 - Frigate", + "recordings": { + "documentTitle": "녹화 - Frigate" + }, + "calendarFilter": { + "last24Hours": "최근 24시간" + }, + "markAsReviewed": "'다시 봤음'으로 표시", + "markTheseItemsAsReviewed": "이 영상들을 '다시 봤음'으로 표시", + "newReviewItems": { + "label": "새로운 '다시 보기' 영상 보기", + "button": "새로운 '다시 보기' 영상" + }, + "selected_one": "{{count}} 선택됨", + "selected_other": "{{count}} 선택됨", + "camera": "카메라", + "detected": "감지됨", + "suspiciousActivity": "수상한 행동", + "threateningActivity": "위협적인 행동", + "zoomIn": "확대", + "zoomOut": "축소" +} diff --git a/web/public/locales/ko/views/explore.json b/web/public/locales/ko/views/explore.json new file mode 100644 index 0000000..5b9c9d5 --- /dev/null +++ b/web/public/locales/ko/views/explore.json @@ -0,0 +1,39 @@ +{ + "documentTitle": "탐색 - Frigate", + "generativeAI": "생성형 AI", + "exploreMore": "{{label}} 더 많은 감지 대상 탐색하기", + "exploreIsUnavailable": { + "title": "탐색을 사용할 수 없습니다", + "embeddingsReindexing": { + "context": "추적된 객체의 임베딩 색인 재구성이 완료된 후 '탐색' 기능을 사용할 수 있습니다.", + "startingUp": "시작 중…", + "estimatedTime": "예상 남은시간:", + "finishingShortly": "곧 완료됩니다", + "step": { + "thumbnailsEmbedded": "처리된 썸네일: ", + "descriptionsEmbedded": "처리된 설명: ", + "trackedObjectsProcessed": "처리된 추적 감지: " + } + }, + "downloadingModels": { + "context": "Frigate가 시맨틱 검색 기능을 지원하기 위해 필요한 임베딩 모델을 다운로드하고 있습니다. 네트워크 연결 속도에 따라 몇 분 정도 소요될 수 있습니다.", + "setup": { + "visionModel": "비전 모델", + "visionModelFeatureExtractor": "비전 모델 특징 추출기", + "textModel": "Text model", + "textTokenizer": "텍스트 토크나이저" + }, + "tips": { + "context": "모델이 다운로드된 후 추적 객체의 임베딩을 색인 재구성하는 것이 좋습니다." + }, + "error": "오류가 발생했습니다. Frigate 로그를 확인하세요." + } + }, + "details": { + "timestamp": "시간 기록" + }, + "trackedObjectDetails": "추적 객체 세부 정보", + "type": { + "details": "세부 정보" + } +} diff --git a/web/public/locales/ko/views/exports.json b/web/public/locales/ko/views/exports.json new file mode 100644 index 0000000..588cdd8 --- /dev/null +++ b/web/public/locales/ko/views/exports.json @@ -0,0 +1,36 @@ +{ + "documentTitle": "내보내기 - Frigate", + "search": "검색", + "noExports": "내보내기가 없습니다", + "deleteExport": { + "label": "내보내기 삭제" + }, + "deleteExport.desc": "{{exportName}}을 지우시겠습니까?", + "editExport": { + "title": "내보내기 이름 변경", + "desc": "이 내보내기의 새 이름을 입력하세요.", + "saveExport": "내보내기 저장" + }, + "toast": { + "error": { + "renameExportFailed": "내보내기 이름 변경에 실패했습니다: {{errorMessage}}", + "assignCaseFailed": "케이스 할당 업데이트 실패: {{errorMessage}}" + } + }, + "headings": { + "uncategorizedExports": "분류되지 않은 내보내기", + "cases": "케이스" + }, + "tooltip": { + "shareExport": "내보내기 공유", + "downloadVideo": "동영상 다운로드", + "editName": "이름 편집", + "deleteExport": "내보내기 삭제", + "assignToCase": "케이스에 추가" + }, + "caseDialog": { + "title": "케이스에 추가", + "description": "기존 케이스를 선택하거나 새 케이스를 만드세요.", + "selectLabel": "케이스" + } +} diff --git a/web/public/locales/ko/views/faceLibrary.json b/web/public/locales/ko/views/faceLibrary.json new file mode 100644 index 0000000..a99cb38 --- /dev/null +++ b/web/public/locales/ko/views/faceLibrary.json @@ -0,0 +1,92 @@ +{ + "description": { + "placeholder": "이 모음집의 이름을 입력해주세요", + "addFace": "안면인식 라이브러리에서 첫 사진을 업로드해 새로운 컬렉션을 만들어보세요.", + "invalidName": "잘못된 이름입니다. 이름은 문자, 숫자, 공백, 따옴표 ('), 밑줄 (_), 그리고 붙임표 (-)만 포함이 가능합니다.", + "nameCannotContainHash": "이름에 #을 포함할 수 없습니다." + }, + "details": { + "person": "사람", + "subLabelScore": "보조 레이블 신뢰도", + "face": "얼굴 상세정보", + "timestamp": "시간 기록", + "unknown": "알 수 없음", + "scoreInfo": "점수는 각 이미지에서 얼굴의 크기를 가중치로 적용하여 모든 얼굴 점수의 평균을 낸 값입니다." + }, + "selectItem": "{{item}} 선택", + "documentTitle": "얼굴 라이브러리 - Frigate", + "uploadFaceImage": { + "title": "얼굴 사진 올리기", + "desc": "얼굴을 스캔하고 {{pageToggle}}에 포함하기 위해 이미지를 업로드하세요" + }, + "collections": "모음집", + "createFaceLibrary": { + "title": "모음집 만들기", + "desc": "새로운 모음집 만들기", + "new": "새 얼굴 만들기", + "nextSteps": "강력한 기반을 구축하려면:
  • 최근 인식 탭을 사용하여 감지된 각 사람의 이미지를 선택하고 학습하세요.
  • 최상의 결과를 위해 정면 이미지에 집중하고, 각도가 있는 얼굴이 촬영된 이미지는 학습에 사용하지 마세요.
  • " + }, + "steps": { + "faceName": "얼굴 이름 입력", + "uploadFace": "얼굴 사진 올리기", + "nextSteps": "다음 단계", + "description": { + "uploadFace": "정면을 향한 {{name}}의 얼굴이 보이는 이미지를 업로드하세요. 이미지를 얼굴만 자를 필요는 없습니다." + } + }, + "train": { + "title": "최근 인식", + "aria": "최근 인식 선택", + "titleShort": "최근" + }, + "selectFace": "얼굴 선택", + "deleteFaceLibrary": { + "title": "이름 삭제" + }, + "deleteFaceAttempts": { + "title": "얼굴 삭제" + }, + "renameFace": { + "title": "얼굴 이름 바꾸기", + "desc": "{{name}}의 새 이름을 입력하세요" + }, + "button": { + "deleteFaceAttempts": "얼굴 삭제", + "addFace": "얼굴 추가", + "renameFace": "얼굴 이름 바꾸기", + "deleteFace": "얼굴 삭제", + "uploadImage": "이미지 올리기", + "reprocessFace": "얼굴 재조정" + }, + "imageEntry": { + "validation": { + "selectImage": "이미지 파일을 선택해주세요." + }, + "dropActive": "여기에 이미지 놓기…", + "dropInstructions": "이미지를 끌어다 놓거나 여기에 붙여넣으세요. 선택할 수도 있습니다.", + "maxSize": "최대 용량: {{size}}MB" + }, + "nofaces": "얼굴을 찾을 수 없습니다", + "pixels": "{{area}}px", + "trainFaceAs": "얼굴을 다음과 같이 훈련하기:", + "trainFace": "얼굴 훈련하기", + "toast": { + "success": { + "uploadedImage": "이미지 업로드에 성공했습니다.", + "addFaceLibrary": "{{name}} 을 성공적으로 얼굴 라이브러리에 추가했습니다!", + "deletedFace_other": "{{count}} 얼굴을 성공적으로 삭제했습니다.", + "renamedFace": "얼굴 이름을 {{name}} 으로 성공적으로 바꿨습니다", + "trainedFace": "얼굴 훈련을 성공적으로 마쳤습니다.", + "updatedFaceScore": "{{name}} 얼굴 점수 업데이트 성공 {{score}}." + }, + "error": { + "uploadingImageFailed": "이미지 업로드 실패:{{errorMessage}}", + "addFaceLibraryFailed": "얼굴 이름 설정 실패:{{errorMessage}}", + "deleteFaceFailed": "삭제 실패:{{errorMessage}}", + "deleteNameFailed": "이름 삭제 실패:{{errorMessage}}", + "renameFaceFailed": "이름 바꾸기 실패:{{errorMessage}}", + "trainFailed": "훈련 실패:{{errorMessage}}", + "updateFaceScoreFailed": "얼굴 신뢰도 업데이트 실패:{{errorMessage}}" + } + } +} diff --git a/web/public/locales/ko/views/live.json b/web/public/locales/ko/views/live.json new file mode 100644 index 0000000..c2b05d0 --- /dev/null +++ b/web/public/locales/ko/views/live.json @@ -0,0 +1,198 @@ +{ + "documentTitle": "실시간 보기 - Frigate", + "documentTitle.withCamera": "{{camera}} - 실시간 보기 - Frigate", + "lowBandwidthMode": "저대역폭 모드", + "twoWayTalk": { + "enable": "양방향 말하기 활성화", + "disable": "양방향 말하기 비활성화" + }, + "cameraAudio": { + "enable": "카메라 오디오 활성화", + "disable": "카메라 오디오 비활성화" + }, + "ptz": { + "move": { + "clickMove": { + "label": "클릭해서 카메라 중앙 배치", + "enable": "클릭해서 움직이기 기능 활성화", + "disable": "클릭해서 움직이기 기능 비활성화", + "enableWithZoom": "클릭하여 이동 / 드래그하여 확대 활성화" + }, + "left": { + "label": "PTZ 카메라 왼쪽으로 이동" + }, + "up": { + "label": "PTZ 카메라 위로 이동" + }, + "down": { + "label": "PTZ 카메라 아래로 이동" + }, + "right": { + "label": "PTZ 카메라 오른쪽으로 이동" + } + }, + "zoom": { + "in": { + "label": "PTZ 카메라 확대" + }, + "out": { + "label": "PTZ 카메라 축소" + } + }, + "focus": { + "in": { + "label": "PTZ 카메라 포커스 인" + }, + "out": { + "label": "PTZ 카메라 포커스 아웃" + } + }, + "frame": { + "center": { + "label": "클릭해서 PTZ 카메라 중앙 배치" + } + }, + "presets": "PTZ 카메라 프리셋" + }, + "camera": { + "enable": "카메라 활성화", + "disable": "카메라 비활성화" + }, + "muteCameras": { + "enable": "모든 카메라 음소거", + "disable": "모든 카메라 음소거 해제" + }, + "detect": { + "enable": "감지 활성화", + "disable": "감지 비활성화" + }, + "recording": { + "enable": "녹화 활성화", + "disable": "녹화 비활성화" + }, + "snapshots": { + "enable": "스냅샷 활성화", + "disable": "스냅샷 비활성화" + }, + "audioDetect": { + "enable": "오디오 감지 활성화", + "disable": "오디오 감지 비활성화" + }, + "transcription": { + "enable": "실시간 오디오 자막 활성화", + "disable": "실시간 오디오 자막 비활성화" + }, + "autotracking": { + "enable": "자동 추적 활성화", + "disable": "자동 추적 비활성화" + }, + "streamStats": { + "enable": "스트림 통계 보기", + "disable": "스트림 통계 숨기기" + }, + "manualRecording": { + "title": "수동 녹화", + "tips": "이 카메라의 녹화 보관 설정에 따라 인스턴트 스냅샷을 다운로드하거나 수동 녹화를 시작할 수 있습니다.", + "playInBackground": { + "label": "백그라운드에서 재생", + "desc": "이 옵션을 활성화하면 플레이어가 숨겨져도 계속 스트리밍됩니다." + }, + "showStats": { + "label": "통계 보기", + "desc": "이 옵션을 활성화하면 카메라 피드에 스트림 통계가 나타납니다." + }, + "debugView": "디버그 보기", + "start": "수동 녹화 시작", + "started": "수동 녹화 시작되었습니다.", + "failedToStart": "수동 녹화 시작이 실패했습니다.", + "recordDisabledTips": "이 카메라 설정에서 녹화가 비활성화 되었거나 제한되어 있어 스냅샷만 저장됩니다.", + "end": "수동 녹화 종료", + "ended": "수동 녹화가 종료되었습니다.", + "failedToEnd": "수동 녹화 종료가 실패했습니다." + }, + "streamingSettings": "스트리밍 설정", + "notifications": "알림", + "audio": "오디오", + "suspend": { + "forTime": "일시정지 시간: " + }, + "stream": { + "title": "스트림", + "audio": { + "available": "이 스트림에서 오디오를 사용할 수 있습니다", + "unavailable": "이 스트림에서 오디오를 사용할 수 없습니다", + "tips": { + "title": "이 스트림에서 오디오를 사용하려면 카메라에서 오디오를 출력하고 go2rtc에서 설정해야 합니다." + } + }, + "debug": { + "picker": "디버그 모드에선 스트림 모드를 선택할 수 없습니다. 디버그 뷰에서는 항상 감지(Detect) 역할로 설정한 스트림을 사용합니다." + }, + "twoWayTalk": { + "tips": "양방향 말하기 기능을 사용하려면 기기에서 기능을 지원해야하며 WebRTC를 설정해야합니다.", + "available": "이 기기는 양방향 말하기 기능을 사용할 수 있습니다", + "unavailable": "이 기기는 양방향 말하기 기능을 사용할 수 없습니다" + }, + "lowBandwidth": { + "tips": "버퍼링 또는 스트림 오류로 실시간 화면을 저대역폭 모드로 변경되었습니다.", + "resetStream": "스트림 리셋" + }, + "playInBackground": { + "label": "백그라운드에서 재생", + "tips": "이 옵션을 활성화하면 플레이어가 숨겨져도 스트리밍이 지속됩니다." + } + }, + "cameraSettings": { + "title": "{{camera}} 설정", + "cameraEnabled": "카메라 활성화", + "objectDetection": "대상 감지", + "recording": "녹화", + "snapshots": "스냅샷", + "audioDetection": "오디오 감지", + "transcription": "오디오 자막", + "autotracking": "자동 추적" + }, + "history": { + "label": "이전 영상 보기" + }, + "effectiveRetainMode": { + "modes": { + "all": "전체", + "motion": "움직임 감지", + "active_objects": "활성 대상" + }, + "notAllTips": "{{source}} 녹화 보관 설정이 mode: {{effectiveRetainMode}}로 되어 있어, 이 수동 녹화는 {{effectiveRetainModeName}}이(가) 있는 구간만 저장됩니다." + }, + "editLayout": { + "label": "레이아웃 편집", + "group": { + "label": "카메라 그룹 편집" + }, + "exitEdit": "편집 종료" + }, + "noCameras": { + "title": "설정된 카메라 없음", + "description": "카메라를 연결해 시작하세요.", + "buttonText": "카메라 추가", + "restricted": { + "title": "연결된 카메라 없음", + "description": "이 그룹의 카메라를 볼 권한이 없습니다." + }, + "default": { + "title": "설정된 카메라 없음", + "description": "카메라를 연결하여 Frigate을 시작하세요.", + "buttonText": "카메라 추가" + }, + "group": { + "title": "그룹에 카메라 없음", + "description": "이 그룹에 할당되거나 활성화된 카메라가 없습니다.", + "buttonText": "그룹 관리" + } + }, + "snapshot": { + "takeSnapshot": "인스턴트 스냅샷 다운로드", + "noVideoSource": "스냅샷 찍을 비디오 소스가 없습니다.", + "captureFailed": "스냅샷 캡쳐를 하지 못했습니다.", + "downloadStarted": "스냅샷 다운로드가 시작됐습니다." + } +} diff --git a/web/public/locales/ko/views/motionSearch.json b/web/public/locales/ko/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ko/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ko/views/recording.json b/web/public/locales/ko/views/recording.json new file mode 100644 index 0000000..2aa9934 --- /dev/null +++ b/web/public/locales/ko/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "필터", + "export": "내보내기", + "calendar": "날짜", + "filters": "필터", + "toast": { + "error": { + "noValidTimeSelected": "올바른 시간 범위를 선택하세요", + "endTimeMustAfterStartTime": "종료 시간은 시작 시간보다 뒤에 있어야합니다" + } + } +} diff --git a/web/public/locales/ko/views/replay.json b/web/public/locales/ko/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ko/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ko/views/search.json b/web/public/locales/ko/views/search.json new file mode 100644 index 0000000..efb5c93 --- /dev/null +++ b/web/public/locales/ko/views/search.json @@ -0,0 +1,28 @@ +{ + "search": "검색", + "savedSearches": "저장된 검색들", + "button": { + "clear": "검색 초기화", + "save": "검색 저장", + "filterInformation": "필터 정보", + "delete": "저장된 검색 삭제", + "filterActive": "필터 활성화됨" + }, + "searchFor": "{{inputValue}} 검색", + "trackedObjectId": "추적 객체 ID", + "filter": { + "label": { + "cameras": "카메라", + "labels": "레이블", + "zones": "구역", + "sub_labels": "하위 레이블", + "attributes": "속성", + "search_type": "검색 유형", + "time_range": "시간 범위", + "before": "이전", + "after": "이후", + "min_score": "최소 점수", + "max_score": "최대 점수" + } + } +} diff --git a/web/public/locales/ko/views/settings.json b/web/public/locales/ko/views/settings.json new file mode 100644 index 0000000..220cceb --- /dev/null +++ b/web/public/locales/ko/views/settings.json @@ -0,0 +1,230 @@ +{ + "triggers": { + "dialog": { + "form": { + "threshold": { + "title": "임계치" + }, + "name": { + "title": "이름" + }, + "type": { + "title": "종류", + "placeholder": "트리거 종류 선택" + } + }, + "createTrigger": { + "title": "트리거 생성" + } + }, + "actions": { + "notification": "알림 전송" + } + }, + "documentTitle": { + "default": "설정 - Frigate", + "authentication": "인증 설정 - Frigate", + "camera": "카메라 설정 - Frigate", + "enrichments": "데이터 보강 설정 - Frigate", + "masksAndZones": "마스크와 구역 편집기 - Frigate", + "motionTuner": "움직임 감지 조정 - Frigate", + "object": "디버그 - Frigate", + "general": "UI 설정 - Frigate", + "frigatePlus": "Frigate+ 설정 - Frigate", + "notifications": "알림 설정 - Frigate", + "cameraManagement": "카메라 관리 - Frigate", + "cameraReview": "카메라 다시보기 설정 - Frigate", + "globalConfig": "전체 설정 - Frigate", + "cameraConfig": "카메라 설정 - Frigate", + "maintenance": "유지 관리 - Frigate", + "profiles": "프로필 - Frigate" + }, + "users": { + "table": { + "actions": "액션" + } + }, + "menu": { + "ui": "UI", + "enrichments": "데이터 보강", + "cameras": "카메라 설정", + "masksAndZones": "마스크 / 구역", + "motionTuner": "움직임 감지 조정", + "triggers": "트리거", + "debug": "디버그", + "users": "사용자", + "roles": "역할", + "notifications": "알림", + "frigateplus": "Frigate+", + "cameraManagement": "관리", + "cameraReview": "다시보기", + "general": "일반", + "globalConfig": "전체 설정", + "system": "시스템", + "integrations": "연동", + "profileSettings": "프로필 설정", + "globalDetect": "객체 감지", + "globalRecording": "녹화", + "globalSnapshots": "스냅샷", + "globalFfmpeg": "FFmpeg", + "globalMotion": "동적 감지", + "globalObjects": "객체", + "globalReview": "리뷰", + "globalAudioEvents": "오디오 이벤트", + "globalLivePlayback": "실시간 재생", + "globalTimestampStyle": "타임스탬프 스타일", + "systemDatabase": "데이터베이스", + "systemTls": "TLS", + "systemAuthentication": "인증", + "systemNetworking": "네트워크", + "systemProxy": "프록시", + "systemUi": "UI", + "systemLogging": "로그", + "systemEnvironmentVariables": "환경 변수", + "systemTelemetry": "시스템 통계", + "systemBirdseye": "전체 상황 보기", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "감지기 하드웨어", + "systemDetectionModel": "감지 모델", + "systemMqtt": "MQTT", + "integrationSemanticSearch": "의미론적 검색", + "integrationGenerativeAi": "생성형 AI", + "integrationFaceRecognition": "얼굴 인식", + "integrationLpr": "번호판 인식", + "integrationObjectClassification": "객체 분류", + "integrationAudioTranscription": "오디오 전사", + "cameraDetect": "객체 감지", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "녹화", + "cameraSnapshots": "스냅샷", + "cameraMotion": "동적 감지", + "cameraObjects": "객체", + "cameraConfigReview": "리뷰", + "cameraAudioEvents": "오디오 이벤트", + "cameraAudioTranscription": "오디오 전사", + "cameraNotifications": "알림", + "cameraLivePlayback": "실시간 재생", + "cameraBirdseye": "전체 상황", + "cameraFaceRecognition": "얼굴 인식", + "cameraLpr": "번호판 인식", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "카메라 UI", + "cameraTimestampStyle": "타임스탬프 스타일", + "cameraMqtt": "카메라 MQTT", + "mediaSync": "미디어 동기화", + "regionGrid": "영역 격자", + "profiles": "프로필" + }, + "dialog": { + "unsavedChanges": { + "title": "저장되지 않은 변경 사항이 있습니다.", + "desc": "계속하기 전에 변경 사항을 저장하시겠습니까?" + } + }, + "cameraSetting": { + "camera": "카메라", + "noCamera": "카메라 없음" + }, + "general": { + "title": "프로필 설정", + "liveDashboard": { + "title": "실시간 대시보드", + "automaticLiveView": { + "label": "실시간 화면 자동 전환", + "desc": "활동이 감지되면 해당 카메라의 실시간 화면으로 자동 전환합니다. 이 옵션을 비활성화하면 실시간 대시보드의 정지된 카메라 이미지가 1분마다 한 번씩만 업데이트됩니다." + }, + "playAlertVideos": { + "label": "알림 영상 재생", + "desc": "기본적으로 실시간 대시보드의 최근 알림은 작은 반복 재생 영상으로 표시됩니다. 이 옵션을 비활성화하면 현재 기기나 브라우저에서 최근 알림을 정지된 이미지로만 보여줍니다." + }, + "displayCameraNames": { + "label": "카메라 이름 항상 표시", + "desc": "다중 카메라 실시간 대시보드에서 카메라 이름을 항상 칩 형태로 표시합니다." + }, + "liveFallbackTimeout": { + "label": "실시간 재생 대기 시간", + "desc": "카메라의 고화질 실시간 스트리밍을 사용할 수 없을 때, 지정된 시간이 지나면 저대역폭 모드로 전환합니다. 기본값: 3초." + } + }, + "storedLayouts": { + "title": "저장된 레이아웃", + "desc": "카메라 그룹의 화면 배치는 드래그하거나 크기를 조정할 수 있습니다. 변경된 위치는 브라우저의 로컬 저장소에 저장됩니다.", + "clearAll": "레이아웃 지우기" + }, + "cameraGroupStreaming": { + "title": "카메라 그룹 스트리밍 설정", + "desc": "각각의 카메라 그룹의 스트리밍 설정은 브라우저의 로컬 저장소에 저장됩니다.", + "clearAll": "스트리밍 설정 모두 지우기" + }, + "recordingsViewer": { + "title": "녹화 영상 보기", + "defaultPlaybackRate": { + "label": "기본으로 설정된 다시보기 배속", + "desc": "다시보기 영상 재생할 때 기본 배속을 설정합니다." + } + }, + "calendar": { + "title": "캘린더", + "firstWeekday": { + "label": "주 첫째날", + "desc": "다시보기 캘린더에서 주가 시작되는 첫째날을 설정합니다.", + "sunday": "일요일", + "monday": "월요일" + } + }, + "toast": { + "success": { + "clearStoredLayout": "{{cameraName}}의 레이아웃을 지웠습니다", + "clearStreamingSettings": "모든 카메라 그룹 스트리밍 설정을 지웠습니다." + }, + "error": { + "clearStoredLayoutFailed": "레이아웃 지우기에 실패했습니다:{{errorMessage}}", + "clearStreamingSettingsFailed": "카메라 스트리밍 설정 지우기에 실패했습니다:{{errorMessage}}" + } + } + }, + "enrichments": { + "title": "데이터 보강 설정", + "unsavedChanges": "변경된 데이터 보강 설정을 저장하지 않았습니다", + "birdClassification": { + "title": "조류 분류", + "desc": "조류 분류 기능은 양자화된 TensorFlow 모델을 사용하여 알려진 새를 식별합니다. 알려진 새가 인식되면 해당 새의 일반적인 이름이 하위 분류로 추가됩니다. 이 정보는 사용자 인터페이스, 필터 및 알림에 포함됩니다." + }, + "semanticSearch": { + "reindexNow": { + "label": "색인 재구성 시작", + "desc": "색인을 재구성하면 모든 추적된 객체의 임베딩을 다시 생성합니다. 이 작업은 백그라운드에서 실행되며, 추적된 객체의 수에 따라 CPU 점유율이 최대치에 도달하거나 상당한 시간이 소요될 수 있습니다.", + "confirmTitle": "색인 재구성 확인", + "confirmDesc": "정말로 모든 추적된 객체의 임베딩 색인을 재구성하시겠습니까? 이 작업은 백그라운드에서 실행되지만, CPU 점유율이 최대치에 도달하거나 상당한 시간이 소요될 수 있습니다. 진행 상황은 '탐색' 페이지에서 확인하실 수 있습니다.", + "confirmButton": "색인 재구성", + "success": "색인 재구성이 정상적으로 시작되었습니다.", + "alreadyInProgress": "색인 재구성이 이미 진행 중입니다.", + "error": "색인 재구성을 시작하지 못했습니다: {{errorMessage}}" + } + } + }, + "saveAllPreview": { + "title": "저장할 변경 사항", + "triggerLabel": "대기 중인 변경 사항 검토", + "empty": "대기 중인 변경 사항 없음.", + "scope": { + "label": "적용 범위", + "global": "전체", + "camera": "카메라: {{cameraName}}" + }, + "field": { + "label": "항목" + }, + "value": { + "label": "새 값", + "reset": "초기화" + } + }, + "button": { + "overriddenGlobal": "전역 재정의됨", + "overriddenGlobalTooltip": "이 카메라는 이 섹션의 전역 설정을 재정의합니다", + "overriddenBaseConfig": "기본 설정 재정의됨", + "overriddenBaseConfigTooltip": "{{profile}} 프로필은 이 섹션의 구성 설정을 재정의합니다" + } +} diff --git a/web/public/locales/ko/views/system.json b/web/public/locales/ko/views/system.json new file mode 100644 index 0000000..0631370 --- /dev/null +++ b/web/public/locales/ko/views/system.json @@ -0,0 +1,210 @@ +{ + "documentTitle": { + "cameras": "카메라 통계 - Frigate", + "storage": "저장소 통계 - Frigate", + "general": "기본 통계 - Frigate", + "enrichments": "데이터 보강 통계 - Frigate", + "logs": { + "frigate": "Frigate 로그 -Frigate", + "go2rtc": "Go2RTC 로그 - Frigate", + "nginx": "Nginx 로그 - Frigate", + "websocket": "메세지 로그 - Frigate" + } + }, + "title": "시스템", + "metrics": "시스템 통계", + "logs": { + "download": { + "label": "다운로드 로그" + }, + "copy": { + "label": "클립보드에 복사하기", + "success": "클립보드에 로그가 복사되었습니다", + "error": "클립보드에 로그를 저장할 수 없습니다" + }, + "type": { + "label": "타입", + "timestamp": "시간 기록", + "tag": "태그", + "message": "메시지" + }, + "tips": "서버에서 로그 스트리밍 중", + "toast": { + "error": { + "fetchingLogsFailed": "로그 가져오기 오류: {{errorMessage}}", + "whileStreamingLogs": "스크리밍 로그 중 오류: {{errorMessage}}" + } + }, + "websocket": { + "label": "메세지", + "pause": "일시중지", + "resume": "재개", + "clear": "비우기", + "filter": { + "all": "전체 항목", + "topics": "항목", + "events": "이벤트", + "reviews": "리뷰", + "classification": "분류", + "face_recognition": "얼굴 인식", + "lpr": "번호판 인식", + "system": "시스템", + "camera": "카메라", + "all_cameras": "모든 카메라", + "cameras_count_one": "{{count}} 카메라", + "cameras_count_other": "{{count}} 카메라" + }, + "empty": "수신된 메시지 없음", + "count_one": "{{count}} 메세지", + "count_other": "{{count}} 메세지" + } + }, + "general": { + "title": "기본", + "detector": { + "title": "감지기", + "inferenceSpeed": "감지 추론 속도", + "temperature": "감지기 온도", + "cpuUsage": "감지기 CPU 사용률", + "memoryUsage": "감지기 메모리 사용률", + "cpuUsageInformation": "감지 모델로 데이터를 입력/출력하기 위한 전처리 과정에서 사용되는 CPU 사용량입니다. GPU나 가속기를 사용하는 경우에도 추론 자체의 사용량은 포함되지 않습니다." + }, + "hardwareInfo": { + "title": "하드웨어 정보", + "gpuUsage": "GPU 사용률", + "gpuMemory": "GPU 메모리", + "gpuEncoder": "GPU 인코더", + "gpuDecoder": "GPU 디코더", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo 출력", + "processOutput": "프로세스 출력:", + "processError": "프로세스 오류:", + "returnCode": "리턴 코드:{{code}}" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI 출력", + "name": "이름:{{name}}", + "driver": "드라이버:{{driver}}", + "cudaComputerCapability": "CUDA Compute Capability:{{cuda_compute}}", + "vbios": "VBios Info: {{vbios}}" + }, + "copyInfo": { + "label": "GPU 정보 복사" + }, + "toast": { + "success": "GPU 정보가 클립보드에 복사되었습니다" + }, + "closeInfo": { + "label": "GPU 정보 닫기" + } + }, + "npuUsage": "NPU 사용률", + "npuMemory": "NPU 메모리" + }, + "otherProcesses": { + "title": "다른 프로세스들", + "processCpuUsage": "사용중인 CPU 사용률", + "processMemoryUsage": "사용중인 메모리 사용률" + } + }, + "storage": { + "title": "스토리지", + "overview": "전체 현황", + "recordings": { + "title": "녹화", + "tips": "이 값은 Frigate 데이터베이스의 녹화 영상이 사용 중인 전체 저장 공간입니다. Frigate는 디스크 내 다른 파일들의 저장 공간은 추적하지 않습니다.", + "earliestRecording": "가장 오래된 녹화 영상:" + }, + "cameraStorage": { + "title": "카메라 저장소", + "camera": "카메라", + "unusedStorageInformation": "미사용 저장소 정보", + "storageUsed": "용량", + "percentageOfTotalUsed": "전체 대비 비율", + "bandwidth": "대역폭", + "unused": { + "title": "미사용", + "tips": "드라이브에 Frigate 녹화 영상 외에 다른 파일이 저장되어 있는 경우, 이 값은 Frigate에서 실제 사용 가능한 여유 공간을 정확히 나타내지 않을 수 있습니다. Frigate는 녹화 영상 외의 저장 공간 사용량을 추적하지 않습니다." + } + }, + "shm": { + "title": "SHM (공유 메모리) 할당량", + "warning": "현재 SHM 사이즈가 {{total}}MB로 너무 적습니다. 최소 {{min_shm}}MB 이상 올려주세요." + } + }, + "cameras": { + "title": "카메라", + "overview": "전체 현황", + "info": { + "aspectRatio": "종횡비", + "fetching": "카메라 데이터 수집 중", + "stream": "스트림 {{idx}}", + "streamDataFromFFPROBE": "스트림 데이터는 ffprobe에서 받습니다.", + "video": "비디오:", + "codec": "코덱:", + "resolution": "해상도:", + "fps": "FPS:", + "unknown": "알 수 없음", + "audio": "오디오:", + "error": "오류:{{error}}", + "cameraProbeInfo": "{{camera}} 카메라 장치 정보", + "tips": { + "title": "카메라 장치 정보" + } + }, + "framesAndDetections": "프레임 / 감지 (Detections)", + "label": { + "camera": "카메라", + "detect": "감지", + "skipped": "건너뜀", + "ffmpeg": "FFmpeg", + "capture": "캡쳐", + "overallFramesPerSecond": "전체 초당 프레임", + "overallDetectionsPerSecond": "전체 초당 감지", + "overallSkippedDetectionsPerSecond": "전체 초당 건너뛴 감지", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} 캡쳐", + "cameraDetect": "{{camName}} 감지", + "cameraFramesPerSecond": "{{camName}} 초당 프레임", + "cameraDetectionsPerSecond": "{{camName}} 초당 감지", + "cameraSkippedDetectionsPerSecond": "{{camName}} 초당 건너뛴 감지" + }, + "toast": { + "success": { + "copyToClipboard": "데이터 정보가 클립보드에 복사되었습니다." + }, + "error": { + "unableToProbeCamera": "카메라 정보 알 수 없음: {{errorMessage}}" + } + } + }, + "lastRefreshed": "마지막 새로고침: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} FFmpeg CPU 사용량이 높습니다 ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} 감지 CPU 사용량이 높습니다 ({{detectAvg}}%)", + "healthy": "시스템 정상", + "reindexingEmbeddings": "검색 데이터 재정리 중 ({{processed}}% 완료)", + "cameraIsOffline": "{{camera}} 오프라인입니다", + "detectIsSlow": "{{detect}} (이/가) 느립니다 ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} (이/가) 매우 느립니다 ({{speed}} ms)", + "shmTooLow": "/dev/shm 할당량을 ({{total}} MB) 최소 {{min}} MB 이상 증가시켜야합니다." + }, + "enrichments": { + "title": "데이터 보강", + "infPerSecond": "초당 추론 속도", + "embeddings": { + "image_embedding": "이미지 임베딩", + "text_embedding": "텍스트 임베딩", + "face_recognition": "얼굴 인식", + "plate_recognition": "번호판 인식", + "image_embedding_speed": "이미지 임베딩 속도", + "face_embedding_speed": "얼굴 임베딩 속도", + "face_recognition_speed": "얼굴 인식 속도", + "plate_recognition_speed": "번호판 인식 속도", + "text_embedding_speed": "텍스트 임베딩 속도", + "yolov9_plate_detection_speed": "YOLOv9 플레이트 감지 속도", + "yolov9_plate_detection": "YOLOv9 플레이트 감지" + } + } +} diff --git a/web/public/locales/lt/audio.json b/web/public/locales/lt/audio.json new file mode 100644 index 0000000..b9c772e --- /dev/null +++ b/web/public/locales/lt/audio.json @@ -0,0 +1,503 @@ +{ + "camera": "Kamera", + "speech": "Kalbėjimas", + "bicycle": "Dviratis", + "car": "Automobilis", + "motorcycle": "Motociklas", + "bus": "Autobusas", + "train": "Traukinys", + "boat": "Valtis", + "bird": "Paukštis", + "cat": "Katė", + "dog": "Šuo", + "horse": "Arklys", + "sheep": "Avis", + "babbling": "Burbėjimas", + "yell": "Šūksnis", + "skateboard": "Riedlentė", + "door": "Durys", + "mouse": "Pelė", + "keyboard": "Klaviatūra", + "sink": "Kriauklė", + "blender": "Plakiklis", + "clock": "Laikrodis", + "scissors": "Žirklės", + "hair_dryer": "Plaukų Džiovintuvas", + "toothbrush": "Dantų šepetėlis", + "vehicle": "Mašina", + "animal": "Gyvūnas", + "bark": "Lojimas", + "goat": "Ožka", + "bellow": "Apačioje", + "whoop": "Rėkavimas", + "whispering": "Šnabždėjimas", + "laughter": "Juokas", + "snicker": "Kikenimas", + "crying": "Verkimas", + "singing": "Dainavimas", + "sigh": "Atodūsis", + "choir": "Choras", + "yodeling": "Jodliavimas", + "chant": "Giedojimas", + "mantra": "Mantra", + "child_singing": "Dainuoja Vaikas", + "synthetic_singing": "Netikras Dainavimas", + "rapping": "Repavimas", + "humming": "Dūzgimas", + "groan": "Dejuoti", + "grunt": "Niurzgėti", + "whistling": "Švilpti", + "breathing": "Kvepavimas", + "wheeze": "Švokštimas", + "snoring": "Knarkimas", + "gasp": "Aiktelėti", + "pant": "Kelnės", + "snort": "Knarkti", + "cough": "Kosėti", + "throat_clearing": "Atsikrenkšti", + "sneeze": "Čiaudėti", + "sniff": "Uostyti", + "run": "Bėgti", + "shuffle": "Maišyti", + "footsteps": "Žingsniai", + "chewing": "Kramtymas", + "biting": "Kandžiojimas", + "gargling": "Skalavimas", + "stomach_rumble": "Pilvo gurguliavimas", + "burping": "Atsirūgimas", + "hiccup": "Žaksėjimas", + "fart": "Bezdėjimas", + "hands": "Rankos", + "finger_snapping": "Spragsėjimas", + "clapping": "Plojimas", + "heartbeat": "Širdies plakimas", + "heart_murmur": "Širdies Ūžesys", + "cheering": "Džiūgavimas", + "applause": "Aplodismentai", + "chatter": "Plepėti", + "crowd": "Minia", + "children_playing": "Žaidžiantys Vaikai", + "pets": "Gyvūnai", + "yip": "Cyptelėjimas", + "howl": "Kaukimas", + "whimper_dog": "Šuns inkštimas", + "growling": "Urzgimas", + "bow_wow": "Au au", + "purr": "Murkimas", + "meow": "Miaukimas", + "hiss": "Šnypštimas", + "livestock": "Gyvuliai", + "caterwaul": "Kniaukimas", + "clip_clop": "Kanopų Kaukšėjimas", + "neigh": "Prunkštimas", + "moo": "Mūkimas", + "cattle": "Galvijai", + "cowbell": "Karvutės Varpelis", + "pig": "Kiaulė", + "oink": "Kriuksėjimas", + "bleat": "Bliovimas", + "chicken": "Višta", + "cock_a_doodle_doo": "Kakuriakuoti", + "cluck": "Kudakavimas", + "fowl": "Paukščiai", + "turkey": "Kalakutas", + "gobble": "Gargaliavimas", + "duck": "Antis", + "quack": "Kreksėjimas", + "goose": "Žąsis", + "wild_animals": "Laukiniai Gyvūnai", + "honk": "Gagenimas", + "roar": "Riaumoti", + "roaring_cats": "Riaumojančios Katės", + "pigeon": "Balandis", + "chirp": "Čiulbėti", + "crow": "Varna", + "squawk": "Klykimas", + "coo": "Ku", + "owl": "Pelėda", + "caw": "Kranksėjimas", + "hoot": "Ūkti", + "flapping_wings": "Sparnų plazdėjimas", + "dogs": "Šunys", + "rats": "Žiurkės", + "insect": "Vabzdžiai", + "cricket": "Svirpliai", + "mosquito": "Uodai", + "fly": "Musės", + "buzz": "Užėsys", + "patter": "Tekšėjimas", + "frog": "Varlė", + "snake": "Gyvatė", + "croak": "Kvarksėti", + "rattle": "Barškėti", + "whale_vocalization": "Banginio Įgarsinimas", + "music": "Muzika", + "musical_instrument": "Muzikinis Instrumentas", + "plucked_string_instrument": "Sugedęs Styginis Instrumentas", + "guitar": "Gitara", + "electric_guitar": "Elektrinė Gitara", + "bass_guitar": "Bosinė Gitara", + "acoustic_guitar": "Akustinė Gitara", + "steel_guitar": "Metalinė Gitara", + "sitar": "Sitara", + "mandolin": "Mandolina", + "ukulele": "Ukulėle", + "piano": "Pianinas", + "electric_piano": "Elektrinis pianinas", + "organ": "Vargonai", + "banjo": "Bandžia", + "scream": "Rėkti", + "field_recording": "Įrašinėjimas lauke", + "radio": "Radijas", + "television": "Televizija", + "white_noise": "Baltasis triukšmas", + "pink_noise": "Rožinis triukšmas", + "silence": "Tyla", + "shatter": "Dūžimas", + "glass": "Stiklas", + "crack": "Trūkimas", + "wood": "Medis", + "chop": "Kapojimas", + "boom": "Bumtėlti", + "eruption": "Išsiveržimas", + "fireworks": "Fejerverkai", + "artillery_fire": "Artilerinė ugnis", + "explosion": "Sprogimas", + "drill": "Grežimas", + "sanding": "Šveisti", + "power_tool": "Elektriniai įrankiai", + "machine_gun": "Kulkosvaidis", + "filing": "Dildinti", + "sawing": "Pjauti", + "jackhammer": "Kūjis", + "hammer": "Plaktukas", + "tools": "Įrankiai", + "printer": "Spausdintuvas", + "cash_register": "Kasos Aparatas", + "air_conditioning": "Oro Kondicionavimas", + "sewing_machine": "Siuvimo Mašina", + "pulleys": "Skriemulys", + "gears": "Dantračiai", + "tick-tock": "Tiksėjimas", + "tick": "Tik", + "mechanisms": "Mechanizmas", + "whistle": "Švilpimas", + "steam_whistle": "Garinis Švilpimas", + "fire_alarm": "Gaistro Signalas", + "smoke_detector": "Dūmų detektorius", + "siren": "Sirena", + "alarm_clock": "Žadintuvas", + "telephone": "Telefonas", + "writing": "Rašymas", + "shuffling_cards": "Kortų Maišymas", + "zipper": "Užtrauktukas", + "electric_toothbrush": "Elektrinis Dantų Šepetėlis", + "tapping": "Tapsėjimas", + "strum": "Brazdėjimas", + "electronic_organ": "Elektriniai Vargonai", + "hammond_organ": "Hammond Vargonai", + "synthesizer": "Sintezatorius", + "sampler": "Sampleris", + "harpsichord": "Fortepionas", + "percussion": "Perkusija", + "drum_kit": "Būgnų Rinkinys", + "drum_machine": "Būgnų Mašina", + "drum": "Būgnas", + "snare_drum": "Snare Būgnas", + "timpani": "Timpanas", + "tabla": "Tabla", + "cymbal": "Cimbala", + "hi_hat": "Lėkštės", + "wood_block": "Medienos Lentgalis", + "tambourine": "Tamburinas", + "maraca": "Maraka", + "gong": "Gongas", + "tubular_bells": "Vamzdiniai Varpeliai", + "mallet_percussion": "Malet Perkusija", + "vibraphone": "Vibrafonas", + "steelpan": "Metalinė Lėkštė", + "orchestra": "Orkestras", + "brass_instrument": "Variniai Instrumentai", + "trombone": "Trombonas", + "string_section": "Stygų Sekcija", + "violin": "Smuikas", + "double_bass": "Dvigubas Bosas", + "wind_instrument": "Vėjo Instrumentas", + "flute": "Fleita", + "saxophone": "Saksofonas", + "clarinet": "Klarnetas", + "bell": "Varpas", + "church_bell": "Bažnyčios Varpas", + "jingle_bell": "Kalėdinis Varpelis", + "bicycle_bell": "Dviračio Skambutis", + "tuning_fork": "Derinimo Šakutė", + "chime": "Skambesys", + "wind_chime": "Vėjo Skambesys", + "harmonica": "Lūpinė armonika", + "accordion": "Akordionas", + "bagpipes": "Dūdmaišis", + "pop_music": "Pop Muzika", + "hip_hop_music": "Hip-Hop Muzika", + "beatboxing": "Beatboksingas", + "rock_music": "Roko Muzika", + "heavy_metal": "Sunkusis Metalas", + "punk_rock": "Pank Rokas", + "progressive_rock": "Progresyvus Rokas", + "rock_and_roll": "Rokenrolas", + "psychedelic_rock": "Psichodelinis Rokas", + "rhythm_and_blues": "Ritmbliuzas", + "reggae": "Regis", + "swing_music": "Swingas", + "folk_music": "Liaudies Muzikas", + "middle_eastern_music": "Viduriniųjų Rytų Muzika", + "jazz": "Jazas", + "disco": "Disko", + "classical_music": "Klasikinė Muzika", + "opera": "Opera", + "electronic_music": "Elektroninė Muzika", + "house_music": "House Muzika", + "techno": "Techno", + "dubstep": "Dubstepas", + "electronica": "Elektroninė", + "electronic_dance_music": "Elektroninė Šokių Muzika", + "trance_music": "Transo Muzika", + "music_of_latin_america": "Lotynų Amerikos Muzika", + "salsa_music": "Salsa Muzika", + "flamenco": "Flamenko", + "blues": "Bliuzas", + "music_for_children": "Vaikų Muzika", + "vocal_music": "Vokalinė Muzika", + "a_capella": "Akapela", + "music_of_africa": "Afrikietiška Muzika", + "christian_music": "Krikščioniška Muzika", + "gospel_music": "Gospelo Muzika", + "music_of_asia": "Azijietiška Muzika", + "music_of_bollywood": "Bolivudo Muzika", + "traditional_music": "Tradicinė Muzika", + "song": "Daina", + "background_music": "Foninė Muzika", + "theme_music": "Teminė Muzika", + "jingle": "Džinglas", + "soundtrack_music": "Garsotakelio Muzika", + "lullaby": "Lopšinė", + "video_game_music": "Video Žaidimų Muzika", + "christmas_music": "Kalėdinė Muzika", + "dance_music": "Šokių Muzika", + "wedding_music": "Vestuvinė Muzika", + "sad_music": "Liūdna Muzika", + "happy_music": "Laiminga Muzika", + "angry_music": "Pikta Muzika", + "scary_music": "Gązdinanti Muzika", + "wind": "Vėjas", + "rustling_leaves": "Šlamantys Lapai", + "wind_noise": "Vėjo Švilpimas", + "thunderstorm": "Perkūnija", + "thunder": "Griaustinis", + "water": "Vanduo", + "rain": "Lietus", + "raindrop": "Lietaus Lašai", + "rain_on_surface": "Lija ant Paviršiaus", + "stream": "Srovė", + "waterfall": "Krioklys", + "ocean": "Okeanas", + "waves": "Bangos", + "steam": "Garai", + "gurgling": "Gurguliavimas", + "fire": "Ugnis", + "crackle": "Spragėjimas", + "sailboat": "Burlaivis", + "rowboat": "Irklinė valtis", + "motorboat": "Motorinė Valtis", + "ship": "Laivas", + "motor_vehicle": "Motorinis Transportas", + "car_alarm": "Mašinos Signalizacija", + "power_windows": "Elektriniai Langai", + "tire_squeal": "Padangų cypimas", + "car_passing_by": "Pravažiuojanti Mašina", + "race_car": "Lenktyninė Mašina", + "truck": "Sunkvežimis", + "air_brake": "Oro Stabdis", + "reversing_beeps": "Atbulinės Eigos Signalas", + "ice_cream_truck": "Ledų Mašina", + "emergency_vehicle": "Pagalbos Transportas", + "police_car": "Policijos Mašina", + "ambulance": "Greitoji", + "fire_engine": "Užvesti Variklis", + "traffic_noise": "Esimo Triukšmas", + "train_whistle": "Traukinio Švilpimas", + "train_horn": "Traukinio Pypsėjimas", + "subway": "Metro", + "aircraft": "Orlaivis", + "aircraft_engine": "Orlaivio Variklis", + "jet_engine": "Reaktyvinis Variklis", + "propeller": "Propeleris", + "helicopter": "Malūnsparnis", + "fixed-wing_aircraft": "Fiksuotų Sparnų Orlaivis", + "engine": "Variklis", + "light_engine": "Mažas Variklis", + "dental_drill's_drill": "Dantų Gręžimas", + "lawn_mower": "Žoliapjovė", + "chainsaw": "Grandininis Pjūklas", + "medium_engine": "Vidutinis Variklis", + "heavy_engine": "Didelis Variklis", + "engine_knocking": "Variklio Kalimas", + "engine_starting": "Užsikuriantis Variklis", + "idling": "Laisvai Dirbantis", + "accelerating": "Įsibegėjantis", + "doorbell": "Dūrų Skambutis", + "sliding_door": "Slankiojančios Durys", + "slam": "Trenkti", + "knock": "Stuksėti", + "tap": "Tapšnoti", + "cupboard_open_or_close": "Spintelė Atidaryti ar Užsidaryti", + "drawer_open_or_close": "Stalčių Atidaryti ar Uždaryti", + "dishes": "Indai", + "cutlery": "Stalo Įrankiai", + "chopping": "Kapoti", + "static": "Statinis", + "environmental_noise": "Aplinkos Triukšmas", + "sound_effect": "Garso efektai", + "firecracker": "Ugnies Spragėjimas", + "gunshot": "Ginklo Šūvis", + "single-lens_reflex_camera": "Veidrodinis Fotoparatas", + "mechanical_fan": "Mechaninis Fenas", + "ratchet": "Raketė", + "civil_defense_siren": "Civilinės Saugos Sirena", + "busy_signal": "Užimtas Signalas", + "dial_tone": "Numerio Rinkimo Tonas", + "telephone_dialing": "Telefono Rinkimas", + "ringtone": "Skambėjimo Tonas", + "telephone_bell_ringing": "Skamba Telefonas", + "alarm": "Signalizacija", + "computer_keyboard": "Kopiuterio Klaviatūra", + "typewriter": "Spausdinimo Mašina", + "typing": "Spausdinti", + "electric_shaver": "Barzdaskutė", + "coin": "Moneta", + "keys_jangling": "Žvangantys Raktai", + "vacuum_cleaner": "Siurblys", + "toilet_flush": "Tualeto Nuleidimas", + "bathtub": "Vonia", + "water_tap": "Vandens Kranas", + "microwave_oven": "Mikorbangų Krosnelė", + "frying": "Gruzdinimas", + "zither": "Citara", + "rimshot": "Mušimas per kraštą", + "drum_roll": "Būgno dundesys", + "bass_drum": "Bosinis Būgnas", + "marimba": "Marimba", + "glockenspiel": "Varpelis", + "french_horn": "Prancūzų Ragas", + "trumpet": "Trimitas", + "bowed_string_instrument": "Styginiai Instrumentai", + "pizzicato": "Pizikatas", + "cello": "Violončelė", + "harp": "Arfa", + "didgeridoo": "Didžeridū", + "theremin": "Tereminas", + "singing_bowl": "Dainuojantis Dubuo", + "scratching": "Skrečavimas", + "grunge": "Grandžas", + "soul_music": "Soul Muzika", + "country": "Country Muzika", + "bluegrass": "Bluegrass", + "funk": "Funk", + "drum_and_bass": "Drum & Bass", + "ambient_music": "Ambient Muzika", + "new-age_music": "Naujojo Amžiaus Muzika", + "afrobeat": "Afrikietiški Ritmai", + "carnatic_music": "Karnatietiška Muzika", + "ska": "Ska", + "independent_music": "Nepriklausoma Muzika", + "tender_music": "Švelni Muzika", + "exciting_music": "Jaudinanti Muzika", + "toot": "Pyptelėjimas", + "skidding": "Slydimas", + "air_horn": "Klaksonas", + "rail_transport": "Bėginis Transportas", + "railroad_car": "Geležinkelio Vagonas", + "train_wheels_squealing": "Cypiantys Traukino Ratai", + "ding-dong": "Ding-Dong", + "squeak": "Cypimas", + "buzzer": "Skambutis", + "foghorn": "Rūko Sirena", + "fusillade": "Šaudymas", + "cap_gun": "Kapsulinis Pistoletas", + "burst": "Sprogimas", + "splinter": "Skeveldra", + "chink": "Skambėjimas", + "sodeling": "Sodliavimas", + "chird": "Akordai", + "change_ringing": "Kintantis Skambinimas", + "shofar": "Šofaras", + "liquid": "Skystis", + "splash": "Pliūpsnis", + "slosh": "Telkšo", + "squish": "Šliurpti", + "drip": "Kapsi", + "pour": "Pilasi", + "trickle": "Sruvenimas", + "gush": "Plūstelėjimas", + "fill": "Pildyti", + "spray": "Purkšti", + "pump": "Siurbti", + "stir": "Makaluoti", + "boiling": "Virimas", + "sonar": "Sonaras", + "arrow": "Strėlė", + "whoosh": "Užt", + "thump": "Bumpt", + "thunk": "Tunkt", + "electronic_tuner": "Elektroninis Imtuvas", + "effects_unit": "Efektų Mašina", + "chorus_effect": "Chorinis Efektas", + "basketball_bounce": "Krepšinio Kamuolio Atšokimas", + "bang": "Trenksmas", + "slap": "Plaukštelėjimas", + "whack": "Kirtis", + "smash": "Tėkšti", + "breaking": "Lūžtantis", + "bouncing": "Atsimušinėjimas", + "whip": "Vykšt", + "flap": "Klapt", + "scratch": "Braižyti", + "scrape": "Gramdyti", + "rub": "Trinti", + "roll": "Vynioti", + "crushing": "Traiškymas", + "crumpling": "Glamžymas", + "tearing": "Trinasi", + "beep": "Pypt", + "ping": "Pingsi", + "ding": "Dingsi", + "clang": "Klenga", + "squeal": "Žviegimas", + "creak": "Girgžt", + "rustle": "Šlamėjimas", + "whir": "Ūžimas", + "clatter": "Klapsėjimas", + "sizzle": "Čirškėjimas", + "clicking": "Spaksėjimas", + "clickety_clack": "Klaukšėjimas", + "rumble": "Dundėjimas", + "plop": "Plapsėjimas", + "hum": "Zyzimas", + "zing": "Zinga", + "boing": "Blyngt", + "crunch": "Trekšt", + "sine_wave": "Sinuso Banga", + "harmonic": "Harmoniškas", + "chirp_tone": "Svirpiantis Tonas", + "pulse": "Pusluojantis", + "inside": "Viduje", + "outside": "Išorėje", + "reverberation": "Atgarsis", + "echo": "Aidas", + "noise": "Triukšmas", + "mains_hum": "Įvado užesys", + "distortion": "Iškraipymai", + "sidetone": "Šalutinis tonas", + "cacophony": "Kokofonija", + "throbbing": "Frobingas", + "vibration": "Vibracija" +} diff --git a/web/public/locales/lt/common.json b/web/public/locales/lt/common.json new file mode 100644 index 0000000..7f2927f --- /dev/null +++ b/web/public/locales/lt/common.json @@ -0,0 +1,306 @@ +{ + "time": { + "untilForTime": "Iki {{time}}", + "untilForRestart": "Iki kol Frigate persikraus.", + "untilRestart": "Iki perkrovimo", + "ago": "prieš {{timeAgo}}", + "justNow": "Ką tik", + "today": "Šiandien", + "yesterday": "Vakar", + "last7": "Paskutinės 7 dienos", + "last14": "Paskutinės 14 dienų", + "last30": "Paskutinės 30 dienų", + "thisWeek": "Šią Savaitę", + "lastWeek": "Praeitą Savaitę", + "thisMonth": "Šį Mėnesį", + "lastMonth": "Praeitą Mėnesį", + "5minutes": "5 minutės", + "10minutes": "10 minučių", + "30minutes": "30 minučių", + "1hour": "1 valandą", + "12hours": "12 valandų", + "24hours": "24 valandos", + "pm": "pm", + "am": "am", + "yr": "{{time}}m", + "year_one": "{{time}} metai", + "year_few": "{{time}} metai", + "year_other": "{{time}} metų", + "mo": "{{time}}mėn", + "month_one": "{{time}} mėnuo", + "month_few": "{{time}} mėnesiai", + "month_other": "{{time}} mėnesių", + "d": "{{time}}d", + "day_one": "{{time}} diena", + "day_few": "{{time}} dienos", + "day_other": "{{time}} dienų", + "h": "{{time}}v", + "hour_one": "{{time}} valanda", + "hour_few": "{{time}} valandos", + "hour_other": "{{time}} valandų", + "m": "{{time}}min", + "minute_one": "{{time}} minutė", + "minute_few": "{{time}} minutės", + "minute_other": "{{time}} minučių", + "s": "{{time}}s", + "second_one": "{{time}} sekundė", + "second_few": "{{time}} sekundės", + "second_other": "{{time}} sekundžių", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "inProgress": "Apdorojama", + "invalidStartTime": "Netinkamas pradžios laikas", + "invalidEndTime": "Netinkamas pabaigos laikas", + "never": "Niekada" + }, + "unit": { + "speed": { + "kph": "kmh", + "mph": "mph" + }, + "length": { + "feet": "pėdos", + "meters": "metrai" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/val", + "mbph": "MB/val", + "gbph": "GB/val" + } + }, + "label": { + "back": "Eiti atgal", + "hide": "Slėpti {{item}}", + "show": "Rodyti {{item}}", + "ID": "ID", + "none": "Nėra", + "all": "Visi", + "other": "Kiti" + }, + "button": { + "apply": "Pritaikyti", + "reset": "Atstatyti", + "done": "Baigta", + "enabled": "Įjungta", + "enable": "Įjungti", + "disabled": "Išjungta", + "disable": "Išjungti", + "save": "Išsaugoti", + "saving": "Saugoma…", + "cancel": "Atšaukti", + "close": "Užverti", + "copy": "Kopijuoti", + "back": "Atgal", + "history": "Istorija", + "fullscreen": "Pilnas Ekranas", + "exitFullscreen": "Išeiti iš Pilno Ekrano", + "pictureInPicture": "Paveikslėlis Paveiksle", + "twoWayTalk": "Dvikryptis Kalbėjimas", + "cameraAudio": "Kameros Garsas", + "on": "ON", + "edit": "Redaguoti", + "copyCoordinates": "Kopijuoti koordinates", + "delete": "Ištrinti", + "yes": "Taip", + "no": "Ne", + "download": "Atsisiųsti", + "info": "Info", + "suspended": "Pristatbdytas", + "unsuspended": "Atnaujinti", + "play": "Groti", + "unselect": "Atžymėti", + "export": "Eksportuoti", + "deleteNow": "Trinti Dabar", + "next": "Kitas", + "off": "OFF", + "continue": "Tęsti" + }, + "menu": { + "system": "Sistema", + "systemMetrics": "Sistemos duomenys", + "configuration": "Konfiguracija", + "systemLogs": "Sistemos įrašai", + "settings": "Nustatymai", + "configurationEditor": "Konfiguracijos Redaktorius", + "languages": "Kalbos", + "language": { + "en": "Anglų", + "es": "Ispanų", + "zhCN": "Kinų (supaprastinta)", + "fr": "Prancūzų", + "ar": "Arabų", + "pt": "Portugalų", + "ru": "Rusų", + "de": "Vokiečių", + "ja": "Japonų", + "tr": "Turkų", + "it": "Italų", + "nl": "Olandų", + "sv": "Švedų", + "cs": "Čekų", + "nb": "Norvegų", + "vi": "Vietnamiečių", + "fa": "Persų", + "pl": "Lenkų", + "uk": "Ukrainos", + "el": "Graikų", + "ro": "Romūnijos", + "hu": "Vengrų", + "fi": "Suomių", + "da": "Danų", + "sk": "Slovakų", + "withSystem": { + "label": "Kalbai naudoti sistemos nustatymus" + }, + "hi": "Hindi", + "ptBR": "Brazilietiška Portugalų", + "ko": "Korėjiečių", + "he": "Hebrajų", + "yue": "Kantoniečių", + "th": "Tailandiečių", + "ca": "Kataloniečių", + "sr": "Serbų", + "sl": "Slovėnų", + "lt": "Lietuvių", + "bg": "Bulgarų", + "gl": "Galician", + "id": "Indonesian", + "ur": "Urdu" + }, + "appearance": "Išvaizda", + "darkMode": { + "label": "Tamsusis Rėžimas", + "light": "Šviesus", + "dark": "Tamsus", + "withSystem": { + "label": "Šviesiam ar tamsiam rėžimui naudoti sistemos nustatymus" + } + }, + "withSystem": "Sistema", + "theme": { + "label": "Tema", + "blue": "Mėlyna", + "green": "Žalia", + "nord": "Šiaurietiška", + "red": "Raudona", + "highcontrast": "Didelio Kontrasto", + "default": "Numatyta" + }, + "help": "Pagalba", + "documentation": { + "title": "Dokumentacija", + "label": "Frigate dokumentacija" + }, + "restart": "Perkrauti Frigate", + "live": { + "title": "Tiesiogiai", + "allCameras": "Visos Kameros", + "cameras": { + "title": "Kameros", + "count_one": "{{count}} Kamera", + "count_few": "{{count}} Kameros", + "count_other": "{{count}} Kamerų" + } + }, + "review": "Peržiūros", + "explore": "Iškoti", + "export": "Eksportuoti", + "faceLibrary": "Veidų Biblioteka", + "user": { + "title": "Vartotojas", + "account": "Paskyra", + "current": "Esamas vartotojas: {{user}}", + "anonymous": "neidentifikuotas", + "logout": "atsijungti", + "setPassword": "Nustatyti Slaptažodi" + }, + "uiPlayground": "UI Playground", + "classification": "Klasifikavimas" + }, + "toast": { + "copyUrlToClipboard": "URL nukopijuotas į atmintį.", + "save": { + "title": "Išsaugoti", + "error": { + "title": "Nepavyko išsaugoti konfiguracijos pakeitimų: {{errorMessage}}", + "noMessage": "Nepavyko išsaugoti konfiguracijos pakeitimų" + } + } + }, + "role": { + "title": "Rolė", + "admin": "Adminas", + "viewer": "Žiūrėtojas", + "desc": "Adminai turi pilną prieigą prie visų Frigate vartotojo sąsajos fukncijų. Žiūrėtojai yra apriboti peržiūrėti kameras, peržiūrų įrašus ir istorinius įrašus." + }, + "pagination": { + "label": "puslapiavimas", + "previous": { + "title": "Ankstesnis", + "label": "Eiti į ankstesnį puslapį" + }, + "next": { + "title": "Sekantis", + "label": "Eiti į sekantį puslapį" + }, + "more": "Daugiau puslapių" + }, + "accessDenied": { + "documentTitle": "Priegai Nesuteikta - Frigate", + "title": "Prieiga Nesuteikta", + "desc": "Jūs neturite leidimo žiūrėti šį puslapį." + }, + "notFound": { + "documentTitle": "Nerasta - Frigate", + "title": "404", + "desc": "Puslapis nerastas" + }, + "selectItem": "Pasirinkti {{item}}", + "readTheDocumentation": "Skaityti dokumentaciją", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} ir {{1}}", + "many": "{{items}}, ir {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Pasirinktinis", + "internalID": "Vidinį ID Frigate naudoja konfiguracijoje ir duombazėje" + } +} diff --git a/web/public/locales/lt/components/auth.json b/web/public/locales/lt/components/auth.json new file mode 100644 index 0000000..3ba7d10 --- /dev/null +++ b/web/public/locales/lt/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Vartotojo vardas", + "password": "Slaptažodis", + "login": "Prisijungti", + "errors": { + "usernameRequired": "Vartotojo vardas yra privalomas", + "passwordRequired": "Slaptažodis yra privalomas", + "rateLimit": "Viršytos nustatytos ribos. Pabandykite vėliau.", + "loginFailed": "Prisijungti nepavyko", + "unknownError": "Nežinoma klaida. Patikrinkite įrašus.", + "webUnknownError": "Nežinoma klaida. Patikrinkite konsolės įrašus." + }, + "firstTimeLogin": "Bandote prisijungti pirmą kartą? Prisijungimo informaciją rasite Frigate loguose." + } +} diff --git a/web/public/locales/lt/components/camera.json b/web/public/locales/lt/components/camera.json new file mode 100644 index 0000000..2e3ef8a --- /dev/null +++ b/web/public/locales/lt/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Kamerų Grupės", + "add": "Sukurti Kamerų Grupę", + "edit": "Modifikuoti Kamerų Grupę", + "delete": { + "label": "Ištrinti Kamerų Grupę", + "confirm": { + "title": "Patvirtinti ištrynimą", + "desc": "Esate įsitikinę, kad norite ištrinti šią kamerų grupę {{name}}?" + } + }, + "name": { + "label": "Pavadinimas", + "placeholder": "Įveskite pavadinimą…", + "errorMessage": { + "mustLeastCharacters": "Kamerų grupės pavadinimas turi būti bent 2 simbolių.", + "exists": "Kamerų grupės pavadinimas jau egzistuoja.", + "nameMustNotPeriod": "Kamerų grupės pavadinime negali būti taško.", + "invalid": "Nepriimtinas kamera grupės pavadinimas." + } + }, + "cameras": { + "label": "Kameros", + "desc": "Pasirinkite kameras šiai grupei." + }, + "icon": "Ikona", + "success": "Kameraų grupė {{name}} išsaugota.", + "camera": { + "setting": { + "label": "Kamerų Transliacijos Nustatymai", + "title": "{{cameraName}} Transliavimo Nustatymai", + "desc": "Keisti tiesioginės tranliacijos nustatymus šiai kamerų grupės valdymo lentai. Šie nustatymai yra specifiniai įrenginiui/ naršyklei.", + "audioIsAvailable": "Šiai transliacijai yra garso takelis", + "audioIsUnavailable": "Šiai transliacijai nėra garso takelio", + "audio": { + "tips": { + "title": "Šiai transliacijai garsas turi būti teikiamas iš kameros ir konfiguruojamas naudojant go2rtc.", + "document": "Skaityti dokumentaciją " + } + }, + "stream": "Transliacija", + "placeholder": "Pasirinkti transliaciją", + "streamMethod": { + "label": "Transliacijos Metodas", + "placeholder": "Pasirinkti transliacijos metodą", + "method": { + "noStreaming": { + "label": "Nėra transliacijos", + "desc": "Kameros vaizdas atsinaujins tik kartą per mintuę ir nebus tiesioginės transliacijos." + }, + "smartStreaming": { + "label": "Išmanus Transliavimas (rekomenduojama)", + "desc": "Išmanus transliavimas atnaujins jūsų kameros vaizdą kartą per minutę jei nebus aptinkama jokia veikla tam kad saugoti tinklo pralaiduma ir kitus resursus. Aptikus veiklą atvaizdavimas nepertraukiamai persijungs į tiesioginę transliaciją." + }, + "continuousStreaming": { + "label": "Nuolatinė Transliacija", + "desc": { + "title": "Kameros vaizdas visada bus tiesioginė transliacija, jei jis bus matomas valdymo lentoje, net jei jokia veikla nėra aptinkama.", + "warning": "Nepertraukiama transliacija gali naudoti daug tinklo duomenų bei sukelti našumo problemų. Naudoti su atsarga." + } + } + } + }, + "compatibilityMode": { + "desc": "Šį nustatymą naudoti tik jei jūsų kameros tiesioginėje transliacijoje matomi spalvų neatitikimai arba matoma įstriža linija dešinėje vaizdo pusėje.", + "label": "Suderinamumo rėžimas" + } + }, + "birdseye": "Birdseye" + } + }, + "debug": { + "options": { + "label": "Nustatymai", + "title": "Pasirinkimai", + "showOptions": "Rodyti Pasirinkimus", + "hideOptions": "Slėpti Pasirinkimus" + }, + "boundingBox": "Apribojantis Stačiakampis", + "timestamp": "Laiko žymė", + "zones": "Zonos", + "mask": "Maskuotė", + "motion": "Judesys", + "regions": "Regionas" + } +} diff --git a/web/public/locales/lt/components/dialog.json b/web/public/locales/lt/components/dialog.json new file mode 100644 index 0000000..fe2235a --- /dev/null +++ b/web/public/locales/lt/components/dialog.json @@ -0,0 +1,124 @@ +{ + "restart": { + "title": "Esate įsitikinę, kad norite perkrauti Frigate?", + "button": "Perkrauti", + "restarting": { + "title": "Frigate Persikrauna", + "content": "Šis puslapis persikraus už {{countdown}} sekundžių.", + "button": "Priverstinai Perkrauti Dabar" + }, + "description": "Frigate laikinai sustabdoma, iki kol programa persikraus." + }, + "explore": { + "plus": { + "review": { + "question": { + "ask_a": "Ar šis objektas yra {{label}}?", + "ask_an": "Ar šis objektas yra {{label}}?", + "label": "Patvirtinti šią etiketę į Frigate Plus", + "ask_full": "Ar šis objektas yra {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Pateikta" + } + }, + "submitToPlus": { + "label": "Pateiktį į Frigate+", + "desc": "Objektai vietose kurių norite vengti nėra klaidingai teigiami. Pateikiant juos kaip klaidingai teigiamus įneš neatitikimų į modelį." + } + }, + "video": { + "viewInHistory": "Pažiūrėti Istorijoje" + } + }, + "streaming": { + "restreaming": { + "disabled": "Šiai kamerai pertransliavimas nėra įjungtas.", + "desc": { + "title": "Nustatyti go2rtc papildomoms tiesioginės transliacijos galimybėms ir šios kameros garsui." + } + }, + "label": "Srautas", + "showStats": { + "label": "Rodyti transliacijos statistiką", + "desc": "Įjungti šią galimybę rodyti transliacijos statistiką kaip pridėtinę informaciją kameros vaizde." + }, + "debugView": "Debug Vaizdas" + }, + "export": { + "time": { + "lastHour_one": "Paskutinė {{count}} Valanda", + "lastHour_few": "Paskutinės {{count}} Valandos", + "lastHour_other": "Paskutinės {{count}} Valandų", + "fromTimeline": "Pasirinkit iš Laiko juostos", + "custom": "Pasirinkimas", + "start": { + "title": "Pradžios Laikas", + "label": "Pasirinkti Pradžios Laiką" + }, + "end": { + "title": "Pabaigos Laikas", + "label": "Pasirinkti Pabaigos Laiką" + } + }, + "fromTimeline": { + "previewExport": "Peržiūrėti Eksportuotus", + "saveExport": "Išsaugoti Exportuojamą" + }, + "name": { + "placeholder": "Pavadinti eksportuojamą įrašą" + }, + "select": "Pasirinkti", + "export": "Eksportuoti", + "selectOrExport": "Pasirinkti ar Eksportuoti", + "toast": { + "success": "Sėkmingai pradėtas eksportavimas. Peržiūrėti įrašą exports puslapyje.", + "error": { + "failed": "Nepavyko pradėti eksportavimo: {{error}}", + "endTimeMustAfterStartTime": "Pabaigos Laikas privalo būti vėliau nei pradžios laikas", + "noVaildTimeSelected": "Nėra pasirinkto tinkamo laikotarpio" + }, + "view": "Žiūrėti" + } + }, + "recording": { + "button": { + "markAsReviewed": "Žymėti kaip peržiūrėtą", + "export": "Eksportuoti", + "deleteNow": "Ištrinti Dabar", + "markAsUnreviewed": "Pažymėti kaip nematytą" + }, + "confirmDelete": { + "desc": { + "selected": "Ar esate įsitikinę, kad norite ištrinti visus įrašytus vaizdo įrašus susijusius su šiuo peržiūros elementu?

    LaikykiteShift norint ateityje praleisti šį pranešimą." + }, + "title": "Patvirtinti Ištrynimą", + "toast": { + "success": "Vaizdo įrašas susijęs su pasirinkta peržiūra buvo sėkmingai ištrintas.", + "error": "Nepavyko ištrinti: {{error}}" + } + } + }, + "search": { + "saveSearch": { + "label": "Išsaugoti Paiešką", + "desc": "Suteikite vardą šiai išsaugotai paieškai.", + "placeholder": "Įveskite pavadinima savo paieškai", + "overwrite": "{{searchName}} jau egzistuoja. Jei išsaugosite esamas įrašas bus perrašytas.", + "success": "Paieška ({{searchName}}) buvo išsaugota.", + "button": { + "save": { + "label": "Išsaugoti šią paiešką" + } + } + } + }, + "imagePicker": { + "selectImage": "Pasirinkti miniatiūrą sekamam objektui", + "search": { + "placeholder": "Ieškoti pagal etiketę arba sub etiketę..." + }, + "noImages": "Šiai kamerai miniatiūrų nerasta", + "unknownLabel": "Išsaugotas Trigerio Paveiksliukas" + } +} diff --git a/web/public/locales/lt/components/filter.json b/web/public/locales/lt/components/filter.json new file mode 100644 index 0000000..0f276ef --- /dev/null +++ b/web/public/locales/lt/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filtras", + "labels": { + "label": "Etiketės", + "all": { + "title": "Visos Etiketės", + "short": "Etiketės" + }, + "count_one": "{{count}} Etiketė", + "count_other": "{{count}} Etiketės" + }, + "zones": { + "label": "Zonos", + "all": { + "title": "Visos Zonos", + "short": "Zonos" + } + }, + "review": { + "showReviewed": "Rodyti Peržiūrėtus" + }, + "trackedObjectDelete": { + "desc": "Trinant šiuos {{objectLength}} sekamus objektus taip pat pašalins momentines iškarpas, išsaugotus įterpius, priskirtus objekto gyvavimo ciklo įrašus. Šių sekamų objektų įrašyta filmuota medžiaga Istorijos vaizde ištrinta NEBUS.

    Ar esate įsitikinę, kad norite tęsti?

    Laikykite Shift norint ateityje praleisti šį pranešimą.", + "title": "Patvirtinkite Ištrynimą", + "toast": { + "success": "Sekami objektai sėkmingai ištrinti.", + "error": "Nepavyko ištrinti sekamų objektų: {{errorMessage}}" + } + }, + "classes": { + "label": "Klasės", + "all": { + "title": "Visos Klasės" + }, + "count_one": "{{count}} Klasė", + "count_other": "{{count}} Klasių" + }, + "dates": { + "selectPreset": "Pasirinkti Nustatytą poziciją…", + "all": { + "title": "Visos Datos", + "short": "Datos" + } + }, + "more": "Daugiau Filtrų", + "reset": { + "label": "Atstatyti bazines filtrų reikšmes" + }, + "timeRange": "Laiko Rėžis", + "subLabels": { + "label": "Sub Etiketės", + "all": "Visos Sub Etiktės" + }, + "score": "Balas", + "estimatedSpeed": "Nustatytas Greitis ({{unit}})", + "features": { + "label": "Funkcijos", + "hasSnapshot": "Turi Momentinę Nuotrauką", + "hasVideoClip": "Turi vaizdo klipą", + "submittedToFrigatePlus": { + "label": "Pateikta į Frigate+", + "tips": "Pradžioje turite išfiltruoti sekamus objektus su momentinėmis nuotraukomis.

    Sekami Objektai be momentinių nuotraukų negali būti pateikti į Frigate+." + } + }, + "sort": { + "label": "Rikiuoti", + "dateAsc": "Datos (Didėjančiai)", + "dateDesc": "Datos (Mažėjančiai)", + "scoreAsc": "Objekto Balai (Didėjančiai)", + "scoreDesc": "Objekto Balai (Mažėjančiai)", + "speedAsc": "Įvertintas Greitis (Didėjančiai)", + "speedDesc": "Įvertintas Greitis (Mažėjančiai)", + "relevance": "Aktualumą" + }, + "cameras": { + "label": "Kamerų Filtrai", + "all": { + "title": "Visos Kameros", + "short": "Kameros" + } + }, + "motion": { + "showMotionOnly": "Rodyti Tik Judesius" + }, + "explore": { + "settings": { + "title": "Nustatymai", + "defaultView": { + "title": "Bazinis Vaizdas", + "summary": "Santrauka", + "unfilteredGrid": "Nefiltruotas Tinklelis", + "desc": "Kai jokie filtrai nėra parinkti, rodom santrauka naujienų sekamiems objektas pagal etiketę arba nefiltruotas tinklelis." + }, + "gridColumns": { + "title": "Tiklelio Stulpeliai", + "desc": "Pasirinkti kiekį stulpelių atvaizduojant tinkleliu." + }, + "searchSource": { + "label": "Paiškos Šaltinis", + "desc": "Pasirinkite kaip jūsų sekamiems objektams bus vykdoma paieška, naudojant miniatiūras ar tekstinius aprašymus.", + "options": { + "thumbnailImage": "Miniatiūros Paveikslėlis", + "description": "Aprašymas" + } + } + }, + "date": { + "selectDateBy": { + "label": "Pasirinkite datą filtravimui" + } + } + }, + "logSettings": { + "label": "Filtruoti sekimo įrašų lygį", + "filterBySeverity": "Filtruoti įrašus pagal svarbą", + "loading": { + "title": "Kraunama", + "desc": "Kai įrašų puslapyje pasiekiama įrašų pabaiga, nauji įrašai atsiras automatiškai." + }, + "disableLogStreaming": "Išjungti įrašų transliavimą", + "allLogs": "Visi įrašai" + }, + "zoneMask": { + "filterBy": "Filtruoti naudojant zonų maskavimus" + }, + "recognizedLicensePlates": { + "title": "Atpažinti Registracijos Numeriai", + "loadFailed": "Nepavyko pateikti atpažintų registracijos numerių.", + "loading": "Ištraukiami atpažinti registracijos numeriai…", + "placeholder": "Įveskite norėdami ieškoti registracijos numerių…", + "noLicensePlatesFound": "Registracjos numerių nerasta.", + "selectPlatesFromList": "Pasirinkti vieną ar daugiau numerių iš sąrašo.", + "selectAll": "Pasirinkti viską", + "clearAll": "Išvalyti viską" + }, + "attributes": { + "label": "Klasifikavimo Atributai", + "all": "Visi Atributai" + } +} diff --git a/web/public/locales/lt/components/icons.json b/web/public/locales/lt/components/icons.json new file mode 100644 index 0000000..82dadb3 --- /dev/null +++ b/web/public/locales/lt/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Pasirinkti ikoną", + "search": { + "placeholder": "Surasti ikoną…" + } + } +} diff --git a/web/public/locales/lt/components/input.json b/web/public/locales/lt/components/input.json new file mode 100644 index 0000000..6d3ff72 --- /dev/null +++ b/web/public/locales/lt/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Parsisiųsti Video", + "toast": { + "success": "Jūsų peržiūros elemento parsisiuntimas pradėtas." + } + } + } +} diff --git a/web/public/locales/lt/components/player.json b/web/public/locales/lt/components/player.json new file mode 100644 index 0000000..4924c6c --- /dev/null +++ b/web/public/locales/lt/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Šiam laiko tarpui įrašų nerasta", + "noPreviewFound": "Peržiūrų nerasta", + "noPreviewFoundFor": "Peržiūrų nerasta {{cameraName}}", + "submitFrigatePlus": { + "title": "Pateikti šį kadrą į Frigate+?", + "submit": "Pateikti" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 ar naujesni yra privalomi šiam tiesioginės transliacijos tipui.", + "streamOffline": { + "title": "Transliacija nepasiekiama", + "desc": "Jokių transliacijos kadrų negauta iš {{cameraName}}detect, patikrinkite klaidų sąrašus" + }, + "cameraDisabled": "Kamera yra išjungta", + "stats": { + "streamType": { + "title": "Transliacijos Tipas:", + "short": "Tipas" + }, + "bandwidth": { + "title": "Pralaidumas:", + "short": "Pralaidumas" + }, + "latency": { + "title": "Vėlavimas:", + "value": "{{seconds}} sekundžių", + "short": { + "title": "Vėlavimas", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Iš viso Kadrų:", + "droppedFrames": { + "title": "Pamestų Kadrų:", + "short": { + "title": "Pamesti", + "value": "{{droppedFrames}} kadrai" + } + }, + "decodedFrames": "Dekoduoti Kadrai:", + "droppedFrameRate": "Pamestų Kadrų Dažnis:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Kadras sėkmingai pateiktas į Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Nepavyko pateikti kadro į Frigate+" + } + } +} diff --git a/web/public/locales/lt/config/cameras.json b/web/public/locales/lt/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lt/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lt/config/global.json b/web/public/locales/lt/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lt/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lt/config/groups.json b/web/public/locales/lt/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lt/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lt/config/validation.json b/web/public/locales/lt/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lt/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lt/objects.json b/web/public/locales/lt/objects.json new file mode 100644 index 0000000..9aa9b5d --- /dev/null +++ b/web/public/locales/lt/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Žmogus", + "bicycle": "Dviratis", + "car": "Automobilis", + "motorcycle": "Motociklas", + "airplane": "Lėktuvas", + "bus": "Autobusas", + "train": "Traukinys", + "boat": "Valtis", + "traffic_light": "Šviesoforas", + "fire_hydrant": "Hidrantas", + "street_sign": "Kelio ženklas", + "stop_sign": "Stop ženklas", + "parking_meter": "Stovėjimo automatas", + "bench": "Suoliukas", + "bird": "Paukštis", + "cat": "Katė", + "dog": "Šuo", + "horse": "Arklys", + "sheep": "Avis", + "cow": "Karvė", + "elephant": "Dramblys", + "bear": "Lokys", + "zebra": "Zebras", + "giraffe": "Žirafa", + "hat": "Kepurė", + "backpack": "Kuprinė", + "umbrella": "Skėtis", + "shoe": "Batas", + "eye_glasses": "Akiniai", + "handbag": "Rankinė", + "tie": "Kaklaraštis", + "suitcase": "Lagaminas", + "frisbee": "Skraidanti lėkštė", + "snowboard": "Snieglentė", + "skis": "Slidės", + "sports_ball": "Sporto Kamuolys", + "kite": "Aitvaras", + "baseball_bat": "Beisbolo lazda", + "baseball_glove": "Beisbolo Pirštinė", + "skateboard": "Riedlentė", + "surfboard": "Banglentė", + "tennis_racket": "Teniso Raketė", + "bottle": "Butelis", + "plate": "Lėkštė", + "wine_glass": "Vyno Taurė", + "cup": "Puodelis", + "fork": "Šakutė", + "knife": "Peilis", + "spoon": "Šaukštas", + "bowl": "Dubuo", + "banana": "Bananas", + "apple": "Obuolys", + "sandwich": "Sumuštinis", + "orange": "Apelsinas", + "broccoli": "Brokolis", + "carrot": "Morka", + "hot_dog": "Hot Dog", + "pizza": "Pica", + "donut": "Spurga", + "cake": "Tortas", + "chair": "Kėdė", + "couch": "Sofa", + "potted_plant": "Pasodintas Augalas", + "bed": "Lova", + "mirror": "Veidrodis", + "dining_table": "Valgomasis Stalas", + "window": "Langas", + "desk": "Stalas", + "toilet": "Tualetas", + "door": "Durys", + "tv": "TV", + "laptop": "Nešiojamasis Kompiuteris", + "mouse": "Pelė", + "remote": "Nuotolinis valdymo pultas", + "keyboard": "Klaviatūra", + "cell_phone": "Mobilus Telefonas", + "microwave": "Mikrobangų krosnelė", + "oven": "Orkaitė", + "toaster": "Skrudintuvas", + "sink": "Kriauklė", + "refrigerator": "Šaldiklis", + "blender": "Plakiklis", + "book": "Knyga", + "clock": "Laikrodis", + "vase": "Vaza", + "scissors": "Žirklės", + "teddy_bear": "Pliušinis Meškiukas", + "hair_dryer": "Plaukų Džiovintuvas", + "toothbrush": "Dantų šepetėlis", + "hair_brush": "Plaukų šepetys", + "vehicle": "Mašina", + "squirrel": "Voverė", + "deer": "Elnias", + "animal": "Gyvūnas", + "bark": "Lojimas", + "fox": "Lapė", + "goat": "Ožka", + "rabbit": "Triušis", + "raccoon": "Meškėnas", + "robot_lawnmower": "Robotas Vejapjovė", + "waste_bin": "Šiukšliadėžė", + "on_demand": "Pagal Poreikį", + "face": "Veidas", + "license_plate": "Registracijos Numeris", + "package": "Pakuotė", + "bbq_grill": "BBQ kepsninė", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/web/public/locales/lt/views/chat.json b/web/public/locales/lt/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lt/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lt/views/classificationModel.json b/web/public/locales/lt/views/classificationModel.json new file mode 100644 index 0000000..878ae22 --- /dev/null +++ b/web/public/locales/lt/views/classificationModel.json @@ -0,0 +1,196 @@ +{ + "documentTitle": "Klasifikavimo Modeliai - Frigate", + "button": { + "deleteClassificationAttempts": "Trinti Klasisifikavimo Nuotraukas", + "renameCategory": "Pervadinti Klasę", + "deleteCategory": "Trinti Klasę", + "deleteImages": "Trinti Nuotraukas", + "trainModel": "Treniruoti Modelį", + "addClassification": "Pridėti Klasifikatorių", + "deleteModels": "Ištrinti Modelius", + "editModel": "Koreguoti Modelį" + }, + "toast": { + "success": { + "deletedCategory_one": "Ištrinta Klasę", + "deletedCategory_few": "", + "deletedCategory_other": "", + "deletedImage_one": "Ištrinti Nuotraukas", + "deletedImage_few": "", + "deletedImage_other": "", + "categorizedImage": "Sekmingai Klasifikuotas Nuotrauka", + "trainedModel": "Modelis sėkmingai apmokytas.", + "trainingModel": "Sėkmingai pradėtas modelio apmokymas.", + "deletedModel_one": "Sėkmingai ištrintas {{count}} modelis", + "deletedModel_few": "Sėkmingai ištrinti {{count}} modeliai", + "deletedModel_other": "Sėkmingai ištrinta {{count}} modelių", + "updatedModel": "Modelio nustatymai atnaujinti sėkmingai", + "renamedCategory": "Klasifikatorius sėkmingai pervadintas į {{name}}" + }, + "error": { + "deleteImageFailed": "Nepavyko ištrinti:{{errorMessage}}", + "deleteCategoryFailed": "Nepavyko ištrinti klasės:{{errorMessage}}", + "categorizeFailed": "Nepavyko kategorizuoti nuotraukos:{{errorMessage}}", + "trainingFailed": "Modelio treniravimas nepavyko. Patikrinkite Frigate log'ų detales.", + "deleteModelFailed": "Nepavyko ištrinti modelio: {{errorMessage}}", + "trainingFailedToStart": "Nepavyko pradėti modelio treniravimo: {{errorMessage}}", + "updateModelFailed": "Nepavyko atnaujinti modelio: {{errorMessage}}", + "renameCategoryFailed": "Nepavyko pervadinti klasifikatoriaus: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Trinti Klasę", + "desc": "Esate įsitikinę, norite ištrinti klasę {{name}}? Tai negrįžtamai ištrins visas susijusias nuotraukas ir reikės iš naujo apmokinti modelį.", + "minClassesTitle": "Negalima Ištrinti Klasifikatoriaus", + "minClassesDesc": "Klasifikavimo modelis turi turėti bent 2 klasifikatorius. Pridėkite dar vieną klasifikatoriu prieš ištrinant šį." + }, + "deleteDatasetImages": { + "title": "Ištrinti Imties Nuotraukas", + "desc_one": "Esate įsitikinę norite ištrinti {{count}} nautrauką iš {{dataset}}? Šis veiksmas negrįžtamas ir reikės iš naujo apmokinti modelį.", + "desc_few": "Esate įsitikinę norite ištrinti {{count}} nautraukas iš {{dataset}}? Šis veiksmas negrįžtamas ir reikės iš naujo apmokinti modelį.", + "desc_other": "Esate įsitikinę norite ištrinti {{count}} nautraukų iš {{dataset}}? Šis veiksmas negrįžtamas ir reikės iš naujo apmokinti modelį." + }, + "deleteTrainImages": { + "title": "Ištrinti Apmokymo Nuotraukas", + "desc_one": "Ar esate įsitikinę, kad norite ištrinti {{count}} nuotrauką? Šis veiksmas negrįžtamas.", + "desc_few": "Ar esate įsitikinę, kad norite ištrinti {{count}} nuotraukas? Šis veiksmas negrįžtamas.", + "desc_other": "Ar esate įsitikinę, kad norite ištrinti {{count}} nuotraukų? Šis veiksmas negrįžtamas." + }, + "renameCategory": { + "title": "Pervadinti Klasę", + "desc": "Įveskite naują vardą vietoje {{name}}. Jums reikės iš naujo apmokinti modelį, kad pavadinimas įsigaliotų." + }, + "description": { + "invalidName": "Netinkamas vardas. Vardas gali būti sudarytas tik iš raidžiū, skaičių, tarpų, apostrofų, pabraukimų ar brūkšnelių." + }, + "train": { + "title": "Pastarosios Klasifikacijos", + "aria": "Pasirinkti Pastarasias Klasifikacijas", + "titleShort": "Paskutiniai" + }, + "categories": "Klasės", + "createCategory": { + "new": "Sukurti Naują Klasę" + }, + "categorizeImageAs": "Klasifikuoti Nuotrauką Kaip:", + "categorizeImage": "Klasifikuoti Nuotrauką", + "noModels": { + "object": { + "title": "Nėra Objektų Klasifikavimo Modelių", + "description": "Sukurti individualų modelį ištrintų objektų klasifikavimui.", + "buttonText": "Sukurti Objekto Modelį" + }, + "state": { + "title": "Nėra Būklės Klasifikavimo Modelių", + "description": "Sukurti individualų modelį sekti ir klasifikuoti būsenų pokyčius konkrečiuose kameros plotuose.", + "buttonText": "Sukurti Būsenos Modelį" + } + }, + "details": { + "scoreInfo": "Įvertinimas atspindi vidutinį klasivikavimo pasitikėjimą tarp visų šio objekto atpažinimų.", + "none": "Nėra", + "unknown": "Nežinoma" + }, + "tooltip": { + "trainingInProgress": "Šiuo metu vyksta modelio apmokymas", + "noNewImages": "Nėra naujų paveikslėlių apmokymui. Pradžiai suklasifikuokite daugiau paveikslėlių duomenų rinkinyje.", + "noChanges": "Po paskutinio apmokymo duomenų rinkinyje pakeitimų nėra.", + "modelNotReady": "Modelis neparuoštas apmokymui" + }, + "deleteModel": { + "title": "Ištrinti Klasifikavimo Modelį", + "single": "Ar įsitikinę kad norite trinti {{name}}? Tai negrįžtamai ištrins ir susijusius paveikslėlius bei apmokymo duomenis. Tai negali būti sugražinta.", + "desc_one": "Ar esate įsitikinę kad norite ištrinti {{count}} modelį? Tai negrįžtamai ištrins ir susijusius paveikslėlius bei apmokymo duomenis. Tai negali būti sugražinta.", + "desc_few": "Ar esate įsitikinę kad norite ištrinti {{count}} modelius? Tai negrįžtamai ištrins ir susijusius paveikslėlius bei apmokymo duomenis. Tai negali būti sugražinta.", + "desc_other": "Ar esate įsitikinę kad norite ištrinti {{count}} modelių? Tai negrįžtamai ištrins ir susijusius paveikslėlius bei apmokymo duomenis. Tai negali būti sugražinta." + }, + "edit": { + "title": "Koreguoti Klasifikavimo Modelį", + "descriptionState": "Koreguoti klasifikatorius šiam būklės klasifikavimo modeliui. Pokyčiams reikės išnaujo apmokinti modelį.", + "descriptionObject": "Koreguoti objekto tipą ir klasifikavimo tipą šiam objektų klasifikavimo modeliui.", + "stateClassesInfo": "Pastaba: Keičiant statuso klasifikatorius privaloma iš naujo apmokinti modelį." + }, + "wizard": { + "step3": { + "allImagesRequired_one": "Prašom klasifikuoti visus paveikslėlius. Liko {{count}} paveikslėlis.", + "allImagesRequired_few": "Prašom klasifikuoti visus paveikslėlius. Liko {{count}} paveikslėliai.", + "allImagesRequired_other": "Prašom klasifikuoti visus paveikslėlius. Liko {{count}} paveikslėlių.", + "selectImagesPrompt": "Pasirinkti visus paveikslėlius su: {{className}}", + "selectImagesDescription": "Spauskite ant paveikslėlių, kad juos pasirinkti. Spauskite Tęsti kai su šia klase baigsite.", + "generating": { + "title": "Generuojami Pavyzdiniai Paveikslėliai", + "description": "Frigate traukia iš jūsų įrašų reprezentatyvius paveikslėlius. Tai gali užtrukti..." + }, + "training": { + "title": "Modelio Apmokymas", + "description": "Jūsų modelis yra apmokomas fone. Uždarykite šį pranešimą, jūsų modelis pradės veikti iš karto kai bus užbaigtas." + }, + "retryGenerate": "Pakartoti Generavimą", + "noImages": "Pavyzdinių paveikslėlių nesugeneruota", + "classifying": "Klasifikuojama ir Apmokoma...", + "trainingStarted": "Apmokymai pradėti sėkmingai", + "modelCreated": "Modelis sėkmingai sukurtas. Naudoti paskutinius Klasifikavimus, žiūrėti paveiksliukus trūkstamų būklių pridėjimui ir tada apmokyti modelį.", + "errors": { + "noCameras": "Nėra sukonfiguruotų kamerų", + "noObjectLabel": "Nepasirinkta objekto etiketė", + "generateFailed": "Nepavyko sugeneruoti pavyzdžių: {{error}}", + "generationFailed": "Generavimas nepavyko. Prašau bandykite dar kartą.", + "classifyFailed": "Nepavyko suklasifikuoti paveikslėlių: {{error}}" + }, + "generateSuccess": "Sėkmingai sugeneruoti pavyzdiniai paveikslėliai", + "missingStatesWarning": { + "title": "Trūksta Būklių Pavyzdžių", + "description": "Geriausiam rezultatui rekomenduojama parinkti pavyzdžių visoms būklėms. Jūs galite tęsti ir nepasirinkdami visų būklių, tačiau modelis nebus apmokytas kol visos būklės turės pavyzdinius paveikslėlius. Tęsiant naudokite Pastarųjų Klasifikavimų meniu, kad klasifikuoti paveikslėlius trūkstamoms būklėms, tada apmokykite modelį." + } + }, + "title": "Sukurti Naują Klasifikavimą", + "steps": { + "nameAndDefine": "Pavadinimas ir Apibūdinimas", + "stateArea": "Būsenos Plotas", + "chooseExamples": "Pasirinkti Pavyzdžius" + }, + "step1": { + "description": "Būklės modeliai vertina pasikeitimus fiksuotuose kamerų plotuose (pvz., door open/closed). Objektų modeliai pridedą klasifikavimą aptiktiems objektams (pvz., žinomi gyvūnai, pristatymų kurjeriai, kt.).", + "name": "Pavadinimas", + "namePlaceholder": "Įveskite modelio pavadinimą...", + "type": "Tipas", + "typeState": "Būklė", + "typeObject": "Objektas", + "objectLabel": "Ojektų Etiketė", + "objectLabelPlaceholder": "Pasirinkite objekto tipą...", + "classificationType": "Klasifikavimo Tipas", + "classificationTypeTip": "Sužinoti daugiau apie klasifikavimo tipus", + "classificationTypeDesc": "Sub Etiketės prideda papildomą teksta prie objekto etikečių (pvz., 'Asmuo: UPS'). Atributai yra paieškai naudojami metaduomenys saugomi atskirai su objektų metaduomenimis.", + "classificationSubLabel": "Sub Etiketė", + "classificationAttribute": "Atributas", + "classes": "Klasės", + "states": "Būklės", + "classesTip": "Sužinoti apie klases", + "classesStateDesc": "Įvardinkite skirtingas būkles kokiose jūsų kameros plotas gali skaitytis. Pavyzdžiui: 'Atidaryti' ir 'Uždaryti' garažo vartai.", + "classesObjectDesc": "Įvardinkite skirtingas klasifikavimo kategorijas aptiktų objektų klasifikavimui. Pavyzdžiui: Asmenų klasifikavimui 'Pristatymų_kurjeris','Gyventojas','Nepažystamasis'.", + "classPlaceholder": "Įveskite klasės pavadinimą...", + "errors": { + "nameRequired": "Modelio pavadinimas privalomas", + "nameLength": "Modelio pavadinimas privalo būti trumpesnis nei 64 simboliai", + "nameOnlyNumbers": "Modelio pavadinime negali būti tik skaičiai", + "classRequired": "Bent viena klasė yra privaloma", + "classesUnique": "Klasių pavadinimai privalo būti unikalūs", + "noneNotAllowed": "Klasė 'nėra' yra neleidžiama", + "stateRequiresTwoClasses": "Būklių modeliui privalomos bent 2 klasės", + "objectLabelRequired": "Prašome pasirinkti objekto etiketę", + "objectTypeRequired": "Prašome pasirinkti klasifikavimo tipą" + } + }, + "step2": { + "description": "Pasirinkite kameras ir nustatykite plotą stebėjimui kiekvienai kamerai. Modelis klasifikuos būkles šiems plotams.", + "cameras": "Kameros", + "selectCamera": "Pasirinkite Kamerą", + "noCameras": "Spauskite + kad pridėti kameras", + "selectCameraPrompt": "Pasirinkite iš kamerą iš sarašo kad nurodytumėte plotą" + } + }, + "menu": { + "objects": "Objektai", + "states": "Būsenos" + } +} diff --git a/web/public/locales/lt/views/configEditor.json b/web/public/locales/lt/views/configEditor.json new file mode 100644 index 0000000..e9c67dc --- /dev/null +++ b/web/public/locales/lt/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Konfiguracijos redaktorius - Frigate", + "configEditor": "Konfiguracijos Redaktorius", + "copyConfig": "Kopijuoti Konfiguraciją", + "saveAndRestart": "Išsaugoti ir Perkrauti", + "saveOnly": "Tik Išsaugoti", + "confirm": "Išeiti neišsaugant?", + "toast": { + "success": { + "copyToClipboard": "Konfiguracija nukopijuota į atmintį." + }, + "error": { + "savingError": "Klaida išsaugant konfiguraciją" + } + }, + "safeConfigEditor": "Konfiguracijos Redaktorius (Saugus Rėžimas)", + "safeModeDescription": "Frigate yra saugiame rėžime dėl konfiguracijos tinkamumo klaidos." +} diff --git a/web/public/locales/lt/views/events.json b/web/public/locales/lt/views/events.json new file mode 100644 index 0000000..c3e670a --- /dev/null +++ b/web/public/locales/lt/views/events.json @@ -0,0 +1,67 @@ +{ + "motion": { + "label": "Judesys", + "only": "Tik judesys" + }, + "allCameras": "Visos kameros", + "timeline": "Laiko juosta", + "timeline.aria": "Pasirink laiko juostą", + "events": { + "label": "Įvykiai", + "aria": "Pasirinkti įvykius", + "noFoundForTimePeriod": "Šiam laiko periodui įvykių nėrasta." + }, + "calendarFilter": { + "last24Hours": "Paskutinė para" + }, + "selected_one": "{{count}} pasirinktas", + "selected_other": "{{count}} pasirinkta", + "camera": "Kamera", + "alerts": "Įspėjimai", + "detections": "Aptikimai", + "empty": { + "alert": "Nėra pranešimų peržiūrai", + "detection": "Nėra aptikimų peržiūrai", + "motion": "Duomenų apie judesius nėra", + "recordingsDisabled": { + "title": "Įrašai privalo būti įjungti", + "description": "Peržiūros gali būti kuriamos tik tada kai kamerai yra aktyvuoti įrašymai." + } + }, + "documentTitle": "Peržiūros - Frigate", + "recordings": { + "documentTitle": "Įrašai - Frigate" + }, + "markAsReviewed": "Pažymėti kaip peržiūrėtą", + "markTheseItemsAsReviewed": "Pažymėti šiuos įrašus kaip peržiūrėtus", + "newReviewItems": { + "label": "Pamatyti naujus peržiūros įrašus", + "button": "Nauji Įrašai Peržiūrėjimui" + }, + "detected": "aptikta", + "suspiciousActivity": "Įtartina Veikla", + "threateningActivity": "Grėsminga Veikla", + "detail": { + "noDataFound": "Peržiūrai informacijos nėra", + "aria": "Perjungti į detalų vaizdą", + "trackedObject_one": "{{count}} objektas", + "trackedObject_other": "{{count}} objektai", + "noObjectDetailData": "Nėra objekto detalių duomenų.", + "label": "Detalės", + "settings": "Vaizdo Nustatymai Detaliau", + "alwaysExpandActive": { + "title": "Visada išskleisti aktyvų", + "desc": "Aktyviai peržiūrimam įrašui visada išskleisti objekto detales jei jos yra." + } + }, + "objectTrack": { + "trackedPoint": "Susektas taškas", + "clickToSeek": "Spustelkite perkelti į šį laiką" + }, + "zoomIn": "Priartinti", + "zoomOut": "Patolinti", + "select_all": "Viską", + "normalActivity": "Normali veikla", + "needsReview": "Reikalinga peržiūra", + "securityConcern": "Saugumo rūpestis" +} diff --git a/web/public/locales/lt/views/explore.json b/web/public/locales/lt/views/explore.json new file mode 100644 index 0000000..187def5 --- /dev/null +++ b/web/public/locales/lt/views/explore.json @@ -0,0 +1,303 @@ +{ + "documentTitle": "Tyrinėti - Frigate", + "generativeAI": "Generatyvinis DI", + "exploreMore": "Apžvelgti daugiau {{label}} objektų", + "exploreIsUnavailable": { + "embeddingsReindexing": { + "startingUp": "Paleidžiama…", + "estimatedTime": "Apytikris likęs laikas:", + "context": "Tyrinėjimai gali būti naudojami po to kai sekamų objektų įterpiai bus užbaigti indeksuoti.", + "finishingShortly": "Paigiama netrukus", + "step": { + "thumbnailsEmbedded": "Įterptos Miniatiūros: ", + "descriptionsEmbedded": "Įterpti aprašymai: ", + "trackedObjectsProcessed": "Apdorota sekamų objektų: " + } + }, + "title": "Tyrinėjimai Negalimi", + "downloadingModels": { + "context": "Frigate siunčiasi reikalingus įterpimo modelius, kad būtų palaikoma Semantic Paieškos funkcija. Tai gali užtrukti priklausomai nuo duomenų srauto greičio.", + "setup": { + "visionModel": "Vaizdo modelis", + "visionModelFeatureExtractor": "Vaizdo modelio funkcijų išgavimas", + "textModel": "Teksto modelis", + "textTokenizer": "Teksto tekenizatorius" + }, + "tips": { + "context": "Galimai norėsite iš naujo indeksuoti savo sekamų objektų įterpius po to kai modeliai parsisiųs." + }, + "error": "Įvyko klaida. Patikrinkite Frigate įrašus." + } + }, + "details": { + "timestamp": "Laiko žyma", + "item": { + "tips": { + "mismatch_one": "{{count}} neesamas objektas aptiktas ir pridėtas į šį peržiuros įrašą. Tie objektai arba neatitinką įspėjimų ar aptikimų sąlygų arba jau buvo išvalyti/ištrinti.", + "mismatch_few": "{{count}} neesami objektai aptikti ir pridėti į šį peržiuros įrašą. Tie objektai arba neatitinką įspėjimų ar aptikimų sąlygų arba jau buvo išvalyti/ištrinti.", + "mismatch_other": "{{count}} neesamų objektų aptiktų ir pridėtų į šį peržiuros įrašą. Tie objektai arba neatitinką įspėjimų ar aptikimų sąlygų arba jau buvo išvalyti/ištrinti.", + "hasMissingObjects": "Koreguokite savo nustatymus jeigu norite, kad Frigate saugoti objektus su šiomis etiketėmis: {{objects}}" + }, + "title": "Peržiūrėti Įrašo Detales", + "desc": "Peržiūrėti Įrašo detales", + "button": { + "share": "Dalintis šiuo peržiūros įrašu", + "viewInExplore": "Žiūrėti Tyrinėjime" + }, + "toast": { + "success": { + "regenerate": "Gauta nauja užklausa iš {{provider}} naujam aprašymui. Priklausomai nuo jūsų tiekėjo greičio, naują aprašymą sukurti gali užtrukti.", + "updatedSublabel": "Sėkmingai atnaujinta sub etiketė.", + "updatedLPR": "Sėkmingai atnaujinti registracijos numeriai.", + "audioTranscription": "Sėkmingai užklausta garso aprašymo. Priklausomai nuo jūsų Frigate serverio pajėgumų, tai gali užtrukti.", + "updatedAttributes": "Atributai sekmingai atnaujinti." + }, + "error": { + "regenerate": "Nepavyko pakviesti {{provider}} naujam aprašymui: {{errorMessage}}", + "updatedSublabelFailed": "Nepavyko atnaujinti sub etikečių: {{errorMessage}}", + "updatedLPRFailed": "Nepavyko atnaujinti registracijos numerių: {{errorMessage}}", + "audioTranscription": "Nepavyko užklausti garso aprašymo: {{errorMessage}}", + "updatedAttributesFailed": "Nepavyko atnaujinti atributų: {{errorMessage}}" + } + } + }, + "label": "Etiketė", + "editSubLabel": { + "title": "Koreguoti sub etiketę", + "desc": "Įveskite naują sub etiketę šiai etiketei {{label}}", + "descNoLabel": "Įveskite naują sub etiketę šiam sekamam objektui" + }, + "editLPR": { + "title": "Redaguoti registracijos numerį", + "desc": "Įvesti naują registracijos numerio reikšmę šiai etiketei {{label}}", + "descNoLabel": "Įvesti naują registracijos numerio reikšmę šiam objektui" + }, + "snapshotScore": { + "label": "Momentinės nuotraukos balas" + }, + "topScore": { + "label": "Top Balas", + "info": "Aukščiausias balas yra didžiausia medianos reikšmė sekamam objektui, taigi tai gali skirtis nuo balų pateiktų miniatiūrų paieškos rezultatuose." + }, + "score": { + "label": "Balas" + }, + "recognizedLicensePlate": "Atpažinti Registracijos Numeriai", + "estimatedSpeed": "Nustatytas Greitis", + "objects": "Objektai", + "camera": "Kamera", + "zones": "Zonos", + "button": { + "findSimilar": "Rasti Panašų", + "regenerate": { + "title": "Regeneruoti", + "label": "Regeneruoti sekamų objektų aprašymus" + } + }, + "description": { + "label": "Aprašymas", + "placeholder": "Sekamo objekto aprašymas", + "aiTips": "Iki kol sekamo objekto gyvavimo ciklas užsibaigs Frigate neklaus aprašymo iš jūsų Generatyvinio DI tiekėjo." + }, + "expandRegenerationMenu": "Išskleisti regeneravimo meniu", + "regenerateFromSnapshot": "Regeneruoti iš Momentinės Nuotraukos", + "regenerateFromThumbnails": "Regenruoti iš Miniatiūros", + "tips": { + "descriptionSaved": "Aprašymas sėkmingai išsaugotas", + "saveDescriptionFailed": "Nepavyko atnaujinti aprašymo: {{errorMessage}}" + }, + "editAttributes": { + "title": "Koreguoti atributus", + "desc": "Pasirinkite klasifikavimo atributus šiai etiketei {{label}}" + }, + "attributes": "Klasifikavimo Atributai", + "title": { + "label": "Antraštė" + } + }, + "trackedObjectsCount_one": "{{count}} sekamas objektas ", + "trackedObjectsCount_few": "{{count}} sekami objektai ", + "trackedObjectsCount_other": "{{count}} sekamų objektų ", + "objectLifecycle": { + "lifecycleItemDesc": { + "visible": "{{label}} aptikta", + "attribute": { + "faceOrLicense_plate": "{{attribute}} aptiktas etiketei {{label}}", + "other": "{{label}} atpažintas kaip {{attribute}}" + }, + "external": "{{label}} aptiktas", + "entered_zone": "{{label}} pateko į {{zones}}", + "active": "{{label}} tapo aktyvus", + "stationary": "{{label}} nebejuda", + "gone": "{{label}} paliko", + "heard": "{{label}} girdėta", + "header": { + "zones": "Zonos", + "ratio": "Santykis", + "area": "Plotas" + } + }, + "annotationSettings": { + "offset": { + "desc": "Šie duomenys gaunami iš jūsų kameros aptikimo srauto bet yra užkeliami ant vaizdo gaunamo iš įrašymo srauto. Mažai tikėtina kad abeji srautais bus tobulai sinchronizuoti. Rezultate, apibrėžimo dėžutė ir įrašas nesilygiuos tobulai. Tačiau, annotation_offset reikšmė gali būti naudojama tai koreguoti.", + "millisecondsToOffset": "Praslinkti aptikimų anotacijas per mili-sekundes. Bazinis: 0", + "label": "Anotacijos Perstūmimas", + "tips": "Patarimas: Įsivaizduokite kad yra įvykio klipas kur žmogus eina iš kairės į dešinę. Jei apibrėžimo dėžutė nuolatos yra žmogui iš kairės tuomet reikšmę sumažinkite. Analogiškai, jei dėžutė piešiama priekyje žmogaus tuomet reikšmę padidinkite.", + "toast": { + "success": "Anotacijos perslinkimas kamerai {{camera}} buvo išsaugota konfiguracijoje. Perkraukite Frigate, kad pritaikytumėte pokyčius." + } + }, + "title": "Anotacijų Nustatymai", + "showAllZones": { + "title": "Rodyti Visas Zonas", + "desc": "Visada rodyti zonas tuose kadruose, kuriuose objektas pateko į zoną." + } + }, + "title": "Objekto Gyvavimo Ciklas", + "noImageFound": "Šiam laikotarpiui vaizdų nerasta.", + "createObjectMask": "Sukurta Objekto Maskuotė", + "adjustAnnotationSettings": "Koreguoti anotacijų nustatymus", + "scrollViewTips": "Peržiūrėti šio objekto gyvavimo cikle esančius reikšmingus momentus.", + "autoTrackingTips": "Automatiškai sekančių kamerų apibrėžiančios dėžutės pozicija bus netiksli.", + "count": "{{first}} iš {{second}}", + "trackedPoint": "Sekamas Taškas", + "carousel": { + "previous": "Ankstesnė skaidrė", + "next": "Sekanti skaidrė" + } + }, + "dialog": { + "confirmDelete": { + "desc": "Trinant šį sekamą objektą taip pat bus pašalintos momentinės iškarpos, išsaugoti įterpiai ir kitos susios sekimo detalės. Šių sekamų objektų įrašyta filmuota medžiaga Istorijos vaizde ištrinta NEBUS.

    Ar esate įsitikinę, kad norite tęsti?", + "title": "Patvirtinti Ištrynimą" + } + }, + "trackedObjectDetails": "Sekamų Objektų Detalės", + "type": { + "details": "detalės", + "snapshot": "momentinės nuotraukos", + "video": "vaizdas", + "object_lifecycle": "objekto gyvavimo ciklas", + "thumbnail": "miniatiūra", + "tracking_details": "sekimo detalės" + }, + "itemMenu": { + "downloadVideo": { + "label": "Atsisiųsti video", + "aria": "Atsisiųsti video" + }, + "downloadSnapshot": { + "label": "Atsisiųsti momentinę nuotrauką", + "aria": "Atsisiųsti momentinę nuotrauką" + }, + "viewObjectLifecycle": { + "label": "Peržiūrėti objekto gyvavimo ciklą", + "aria": "Rodyti objekto gyvavimo ciklą" + }, + "findSimilar": { + "label": "Rasti panašų", + "aria": "Rasti panašius sekamus objektus" + }, + "addTrigger": { + "label": "Pridėti trigerį", + "aria": "Šiam sekamam objektui pridėti trigerį" + }, + "audioTranscription": { + "label": "Aprašyti", + "aria": "Užklausti garso aprašymo" + }, + "submitToPlus": { + "label": "Pateikti į Frigate+", + "aria": "Pateikti į Frigate Plius" + }, + "viewInHistory": { + "label": "Žiūrėti Istorijoje", + "aria": "Žiūrėti Istorijoje" + }, + "deleteTrackedObject": { + "label": "Ištrinti šį sekamą objektą" + }, + "showObjectDetails": { + "label": "Rodyti objektų kelią" + }, + "hideObjectDetails": { + "label": "Slėpti objektų kelią" + }, + "downloadCleanSnapshot": { + "label": "Atsisiųsti švarią nuotrauką", + "aria": "Atsisiųsti švarią nuotrauką" + }, + "viewTrackingDetails": { + "label": "Žiūrėti sekimo detales", + "aria": "Rodyti sekimo detales" + } + }, + "noTrackedObjects": "Sekamų Objektų Nerasta", + "fetchingTrackedObjectsFailed": "Sekamų objektų ištraukti nepavyko: {{errorMessage}}", + "searchResult": { + "tooltip": "Sutapo {{type}} su {{confidence}}% patikimumu", + "deleteTrackedObject": { + "toast": { + "success": "Sekami objektai sėkmingai ištrinti.", + "error": "Sekamų objektų nepavyko ištrinti: {{errorMessage}}" + } + }, + "previousTrackedObject": "Anksčiau sekti objektai", + "nextTrackedObject": "Sekantis sekamas objektas" + }, + "aiAnalysis": { + "title": "DI Analizė" + }, + "concerns": { + "label": "Rūpesčiai" + }, + "trackingDetails": { + "title": "Sekimo Detalės", + "noImageFound": "Šiai miniatiūrai paveikslėlis nerastas.", + "createObjectMask": "Sukurti Objekto Maskavimą", + "adjustAnnotationSettings": "Patikslinti pastabų nustatymus", + "scrollViewTips": "Spustelkite pamatyti svarbius objekto gyvavimo momentus.", + "autoTrackingTips": "Apribojančio stačiakampio pozicijos nebus tikslios kameroms su autosekimu.", + "count": "{{first}} iš {{second}}", + "trackedPoint": "Sekamas Taškas", + "lifecycleItemDesc": { + "visible": "{{label}} aptiktas", + "entered_zone": "{{label}} pateko į {{zones}}", + "active": "{{label}} tapo aktyvus", + "stationary": "{{label}} tapo statinis", + "attribute": { + "faceOrLicense_plate": "{{attribute}} aptiktas etiketei {{label}}", + "other": "{{label}} atpažintas kaip {{attribute}}" + }, + "gone": "{{label}} kairė", + "heard": "{{label}} girdėta", + "external": "{{label}} aptikta", + "header": { + "zones": "Zonos", + "ratio": "Santykis", + "area": "Plotas", + "score": "Balas" + } + }, + "annotationSettings": { + "title": "Anotacijų Nustatymai", + "showAllZones": { + "title": "Rodyti Visas Zonas", + "desc": "Visada rodyti zonas kadruose kur objektas patenka į zoną." + }, + "offset": { + "label": "Anotacijų offset", + "desc": "Šie duomenys gaunami iš kameros aptikimo srauto, tačiau yra užkeliami ant atvaizdo sluoksnio iš įrašymo srauto. Tobulai synchronizuoti du srautai yra mažai tikėtini. Kaip rezultatas, apribojančio stačiakampio ir vaizdo medžiaga tobulai nesusilygiuos. Norėdami geriau sulygiuoti su įrašų medžiaga, jūs galite naudoti šį nustatymą srauto perslinkimui pirmyn arba atgal.", + "millisecondsToOffset": "Aptikimo anotacijų perslinkimas milisekundėmis. Default: 0", + "tips": "Sumažinkite reikšmę jei video įrašas yra pirmesnis nei kvadratai ar kelio taškai, ir padidinkite reikšmė jei video įrašas atsilieka. Ši reikšmė gali būti neigiama.", + "toast": { + "success": "Anotacijų perslinkimas kamerai {{camera}} išsaugotas konfiguracijoje." + } + } + }, + "carousel": { + "previous": "Ankstesnė skaidrė", + "next": "Kita skaidrė" + } + } +} diff --git a/web/public/locales/lt/views/exports.json b/web/public/locales/lt/views/exports.json new file mode 100644 index 0000000..32c2f6a --- /dev/null +++ b/web/public/locales/lt/views/exports.json @@ -0,0 +1,37 @@ +{ + "search": "Paieška", + "documentTitle": "Eksportuoti - Frigate", + "noExports": "Eksportuotų įrašų nerasta", + "deleteExport": "Ištrinti Eksportuotą Įrašą", + "deleteExport.desc": "Esate įsitikinę, kad norite ištrinti {{exportName}}?", + "editExport": { + "title": "Pervadinti Eksportuojamą įrašą", + "desc": "Įveskite nauja pavadinimą šiam eksportuojamam įrašui.", + "saveExport": "Išsaugoti Eksportuojamą Įrašą" + }, + "toast": { + "error": { + "renameExportFailed": "Nepavyko pervadinti eksportuojamo įrašo: {{errorMessage}}", + "assignCaseFailed": "Nepavyko atnaujinti atvėjo užduoties: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Pasidalinti įrašu", + "downloadVideo": "Atsisiųsti video", + "editName": "Koreguoti pavadinimą", + "deleteExport": "Ištrinti eksportus", + "assignToCase": "Pridėti prie atvėjo" + }, + "headings": { + "cases": "Atvėjai", + "uncategorizedExports": "Nekategorizuoti eksportai" + }, + "caseDialog": { + "title": "Pridėti prie atvėjo", + "selectLabel": "Atvėjis", + "newCaseOption": "Sukurti naują atvėjį", + "descriptionLabel": "Aprašymas", + "nameLabel": "Atvėjo pavadinimas", + "description": "Pasirinkite jau egzisutojantį atvėjį arba sukurkite naują." + } +} diff --git a/web/public/locales/lt/views/faceLibrary.json b/web/public/locales/lt/views/faceLibrary.json new file mode 100644 index 0000000..7122bfb --- /dev/null +++ b/web/public/locales/lt/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "description": { + "addFace": "Pridėkite naują kolekciją į Veidų Kolekciją įkeldami savo pirmą nuotrauką.", + "placeholder": "Įveskite pavadinimą šiai kolekcijai", + "invalidName": "Netinkamas vardas. Vardas gali būti sudarytas tik iš raidžiū, skaičių, tarpų, apostrofų, pabraukimų ar brūkšnelių.", + "nameCannotContainHash": "Pavadinime negali būti #." + }, + "details": { + "person": "Žmogus", + "face": "Veido detelės", + "timestamp": "Laiko žyma", + "unknown": "Nežinoma", + "subLabelScore": "Sub Etiketės Balas", + "scoreInfo": "Sub etiketės balas yra pasvertas balas pagal visų atpažintų veidų užtikrintumą, taigi gali skirtis nuo balo rodomo momentinėje nuotraukoje.", + "faceDesc": "Papildoma informacija sekamo objekto, kuris sugeneravo šį veidą" + }, + "selectItem": "Pasirinkti {{item}}", + "deleteFaceAttempts": { + "desc_one": "Esate įsitikine, kad norite ištrinti {{count}} veidą? Šio veiksmo sugrąžinimas negalimas.", + "desc_few": "Esate įsitikine, kad norite ištrinti {{count}} veidus? Šio veiksmo sugrąžinimas negalimas.", + "desc_other": "Esate įsitikine, kad norite ištrinti {{count}} veidų? Šio veiksmo sugrąžinimas negalimas.", + "title": "Ištrinti Veidus" + }, + "toast": { + "success": { + "deletedFace_one": "Sėkmingai ištrintas{{count}} veidas.", + "deletedFace_few": "Sėkmingai ištrinti {{count}} veidai.", + "deletedFace_other": "Sėkmingai ištrinta {{count}} veidų.", + "deletedName_one": "{{count}} veidas buvo sėkmingai ištrintas.", + "deletedName_few": "{{count}} veidai buvo sėkmingai ištrinti.", + "deletedName_other": "{{count}} veidų buvo sėkmingai ištrinta.", + "uploadedImage": "Nuotrauka sėkmingai įkelta.", + "addFaceLibrary": "{{name}} vardas buvo sėkmingai pridėtas į Veidų Katalogą!", + "renamedFace": "Sėkmingai veidas pervadintas į {{name}}", + "trainedFace": "Veidas apmokytas sėkmingai.", + "updatedFaceScore": "Veido balas atnaujintas sėkmingai į {{name}} {{score}}." + }, + "error": { + "uploadingImageFailed": "Nepavyko įkelti nuotraukos: {{errorMessage}}", + "addFaceLibraryFailed": "Nepavyko priskirti vardo veidui: {{errorMessage}}", + "deleteFaceFailed": "Nepavyko ištrinti: {{errorMessage}}", + "deleteNameFailed": "Vardo ištrinti nepavyko: {{errorMessage}}", + "renameFaceFailed": "Nepavyko pervardinti veido: {{errorMessage}}", + "trainFailed": "Nepavyko apmokinti: {{errorMessage}}", + "updateFaceScoreFailed": "Veido balų atnaujinti nepavyko: {{errorMessage}}" + } + }, + "createFaceLibrary": { + "nextSteps": "Kad sukurtumėte stiprų pagrindą:
  • Naudokite Pastarieji Atpažinimai skirtuką pasirinkti ir paveikslėliais apmokyti kiekvieną aptiktą asmenį.
  • Norint pasiekti geriausią režultatą, susitelkite prie nuotraukų iš priekio; Venkite naudoti veidų nuotraukas kampu pasuktu veidu.
  • ", + "title": "Sukurti Kolekciją", + "desc": "Sukurti naują kolekciją", + "new": "Sukurti Naują Veidą" + }, + "deleteFaceLibrary": { + "desc": "Esate įsitikinę, kad norite ištrinti kolenkciją vardu {{name}}? Visi susiję veidai bus ištrinti negražinamai.", + "title": "Ištrinti Vardą" + }, + "documentTitle": "Veidų Katalogas - Frigate", + "uploadFaceImage": { + "title": "Įkelti Veido Nuotrauką", + "desc": "Įkelti nuotrauką veidų skanavimui ir įtraukti {{pageToggle}}" + }, + "collections": "Kolekcijos", + "steps": { + "faceName": "Įveskite Vardą Veidui", + "uploadFace": "Įkelti Veido Nuotrauką", + "nextSteps": "Sekantis Žingsnis", + "description": { + "uploadFace": "Įkelti nuotrauką {{name}} kuri atvaizduoja veidą iš priekio. Nuotraukos kadruoti nereikia." + } + }, + "train": { + "title": "Pastarieji Atpažinimai", + "aria": "Pasirinkti pastaruosius atpažinimus", + "empty": "Pastaruoju metu nebuvo atliktas veidų atpažinimas", + "titleShort": "Paskutiniai" + }, + "selectFace": "Pasirinkti Veidą", + "renameFace": { + "title": "Pervadinti Veidą", + "desc": "Įveskite {{name}} naują vardą" + }, + "button": { + "deleteFaceAttempts": "Ištrinti Veidus", + "addFace": "Pridėti Veidą", + "renameFace": "Pervadinti Veidą", + "deleteFace": "Ištrinti Veidą", + "uploadImage": "Įkelti Nuotrauką", + "reprocessFace": "Patikrinti Veidą" + }, + "imageEntry": { + "validation": { + "selectImage": "Prašome pasirinkti nuotraukos bylą." + }, + "dropActive": "Įkelkite nuotrauką čia…", + "dropInstructions": "Užvilkite nuotrauką čia, arba spragtelkite pasirinkti", + "maxSize": "Max dydis: {{size}}MB" + }, + "nofaces": "Nėra veidų", + "pixels": "{{area}}px", + "trainFaceAs": "Apmokyti Veidą kaip:", + "trainFace": "Apmokyti Veidą" +} diff --git a/web/public/locales/lt/views/live.json b/web/public/locales/lt/views/live.json new file mode 100644 index 0000000..97813cc --- /dev/null +++ b/web/public/locales/lt/views/live.json @@ -0,0 +1,197 @@ +{ + "documentTitle": "Gyvai - Frigate", + "documentTitle.withCamera": "{{camera}} - Tiesiogiai - Frigate", + "lowBandwidthMode": "Mažo-pralaidumo Rėžimas", + "cameraAudio": { + "enable": "Įgalinti Kamerų Garsą", + "disable": "Išjungti Kamerų Garsą" + }, + "twoWayTalk": { + "enable": "Įgalinti Dvipusį Pokalbį", + "disable": "Išjungti Dvipusį Pokalbį" + }, + "detect": { + "enable": "Įjungti Aptikimą", + "disable": "Išjungti Aptikimą" + }, + "audioDetect": { + "enable": "Įjungti Garso Aptikimą", + "disable": "Išjungti Garso Aptikimą" + }, + "cameraSettings": { + "objectDetection": "Objektų Aptikimai", + "audioDetection": "Garso Aptikimas", + "title": "{{camera}} Nustatymai", + "cameraEnabled": "Kamera įjungta", + "recording": "Įrašinėjimas", + "snapshots": "Momentinės Nuotraukos", + "transcription": "Garso Transkripcija", + "autotracking": "Automatinis sekimas" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Norint išcentruoti kamerą, spragtelti kadre", + "enable": "Įjungti paspaudimą, kad judinti", + "disable": "Išjungti paspaudimą kad judinti" + }, + "left": { + "label": "Pasukti PTZ kamerą į kairę" + }, + "up": { + "label": "Pasukti PTZ kamerą į viršų" + }, + "down": { + "label": "Pasukti PTZ kamera žemyn" + }, + "right": { + "label": "Pasukti PTZ kamerą į dešinę" + } + }, + "zoom": { + "in": { + "label": "Priartinti PTZ kamerą" + }, + "out": { + "label": "Atitolinti PTZ kamerą" + } + }, + "focus": { + "in": { + "label": "Sutelkti PTZ kameros fokusą" + }, + "out": { + "label": "Išleisti PTZ kameros fokusą" + } + }, + "frame": { + "center": { + "label": "Spragtelkite kadre norint centruoti PTZ kamerą" + } + }, + "presets": "PTZ kameros nustatytos pozicijos" + }, + "camera": { + "enable": "Įjungti Kamerą", + "disable": "Išjungti Kamerą" + }, + "muteCameras": { + "enable": "Užtildyti Visas Kameras", + "disable": "Aktyvuoti Garsą Visoms Kameroms" + }, + "recording": { + "enable": "Įjungti Įrašymus", + "disable": "Įšjungti Įrašymus" + }, + "snapshots": { + "enable": "Įjungti Momentines Nuotraukas", + "disable": "Išjungti Momentines Nuotraukas" + }, + "transcription": { + "enable": "Įjungti Gyvą Garso Aprašymą", + "disable": "Išjungti Gyvą Garso Aprašymą" + }, + "autotracking": { + "enable": "Įjungti Autosekimą", + "disable": "Išjungti Autosekimą" + }, + "streamStats": { + "enable": "Rodyti Transliacijos Stats", + "disable": "Paslėpti Transliacijos Stats" + }, + "manualRecording": { + "title": "Pagal-Poreikį", + "tips": "Atsisiųsti momentinę nuotrauką arba kurti manualaus saugojimo nustatymus pagal įvykius iš šios kameros.", + "playInBackground": { + "label": "Paleisti fone", + "desc": "Įjungti šią funkciją kad transliaciją išliktų net paslėpus grotuvą." + }, + "showStats": { + "label": "Rodytis Stats", + "desc": "Įjungti šią funkciją, kad matytumėte transliacijos statistiką kameros vaizde." + }, + "debugView": "Debug Vaizdas", + "start": "Pradėti įrašymą pagal pageidavimą", + "started": "Pradėtas įrašymas pagal pageidavimą.", + "failedToStart": "Nepavyko pradėti įrašymo pagal poreikį.", + "recordDisabledTips": "Įrašymas šiai kamerai yra išjungtas todėl bus saugomos tik momentinės nuotraukos.", + "end": "Baigti įrašymą pagal pageidavimą", + "ended": "Baigtas įrašymas pagal pageidavimą.", + "failedToEnd": "Nepavyko sustabdyti įrašymo pagal pageidavimą." + }, + "streamingSettings": "Transliacijos Nustatymai", + "notifications": "Pranešimai", + "audio": "Garsas", + "suspend": { + "forTime": "Sustabdyti laikui: " + }, + "stream": { + "title": "Transliacija", + "audio": { + "tips": { + "title": "Šiai transliacijai garso išvestis turi būti sukonfiguruota naudojant go2rtc." + }, + "available": "Ši transliacija palaiko garsą", + "unavailable": "Ši transliacija nepalaiko garso" + }, + "twoWayTalk": { + "tips": "Jūsų įranga turi palaikyti šią funkciją, taip pat dvipusiam pokalbiui reikia sukonfiguruoti WebRTC.", + "available": "Šioje transliacijoje galimas dvipusis pokalbis", + "unavailable": "Šioje transliacijoje dvipusio pokalbio galimybių nėra" + }, + "lowBandwidth": { + "tips": "Dėl buffering ar transliacijos klaidų tiesioginė transliacija yra mažos reiškos rėžime.", + "resetStream": "Atstatyti transliaciją" + }, + "playInBackground": { + "label": "Paleisti fone", + "tips": "Norėdami kad transliacija tęstūsi kai grotuvas paslėpiamas įjunkite šią funkciją." + }, + "debug": { + "picker": "Debug rėžime srauto pasirinkimas negalimas. Debug lange naudojamas tas srautas, kuris priskirtas aptikimo rolei." + } + }, + "history": { + "label": "Rodyti istorinius įrašus" + }, + "effectiveRetainMode": { + "modes": { + "all": "Visi", + "motion": "Judesys", + "active_objects": "Aktyvūs Objektai" + }, + "notAllTips": "Jūsų {{source}} įrašų saugojimo pasirinkimas nustatytas rėžime: {{effectiveRetainMode}}, taigi įrašai pagal poreikį irgi bus saugomi pritaikant {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Redaguoti Išdėstymą", + "group": { + "label": "Redaguoti Kamerų Grupę" + }, + "exitEdit": "Išeiti Iš Redagavimo" + }, + "noCameras": { + "title": "Nėra Sukonfiguruotų Kamerų", + "description": "Pradėti nuo kameros prijungimo pire Frigate.", + "buttonText": "Pridėti Kamerą", + "restricted": { + "title": "Nėra Prieinamų Kamerų", + "description": "Jūs neturite leidimo matyti kameras šioje grupėje." + }, + "default": { + "title": "Nėra Sukonfiguruotų Kamerų", + "description": "Pradėkite nuo kameros prijungimo prie Frigate.", + "buttonText": "Pridėti Kamerą" + }, + "group": { + "title": "Grupėje Kamerų Nėra", + "description": "Ši kamerų grupė neturi priskirtų ar įjungtų kamerų.", + "buttonText": "Valdyti Grupes" + } + }, + "snapshot": { + "takeSnapshot": "Atsisiųsti momentinį kadrą", + "noVideoSource": "Momentinei nuotraukai nėra prieinamo video šaltinio.", + "captureFailed": "Nepavyko užfiksuoti kadro.", + "downloadStarted": "Momentinės nuotraukos atsisiuntimas pradėtas." + } +} diff --git a/web/public/locales/lt/views/motionSearch.json b/web/public/locales/lt/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lt/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lt/views/recording.json b/web/public/locales/lt/views/recording.json new file mode 100644 index 0000000..5acee13 --- /dev/null +++ b/web/public/locales/lt/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtras", + "export": "Eksportuoti", + "calendar": "Kalendorius", + "filters": "Filtrai", + "toast": { + "error": { + "noValidTimeSelected": "Pasriniktas laiko periodas netinkamas", + "endTimeMustAfterStartTime": "Pabaigos laikas privalo būti vėlesnis nei pradžios laikas" + } + } +} diff --git a/web/public/locales/lt/views/replay.json b/web/public/locales/lt/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lt/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lt/views/search.json b/web/public/locales/lt/views/search.json new file mode 100644 index 0000000..eac3b4f --- /dev/null +++ b/web/public/locales/lt/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Paieška", + "savedSearches": "Išsaugotos Paieškos", + "searchFor": "Ieškoti {{inputValue}}", + "button": { + "clear": "Išvalyti paiešką", + "save": "Išsaugoti paiešką", + "delete": "Ištrinti išsaugotą paiešką", + "filterInformation": "Filtruoti informaciją", + "filterActive": "Aktyvūs filtrai" + }, + "trackedObjectId": "Sekamo Objekto ID", + "filter": { + "label": { + "cameras": "Kameros", + "labels": "Etiketės", + "zones": "Zonos", + "search_type": "Paieškos Tipas", + "time_range": "Laiko rėžis", + "before": "Prieš", + "after": "Po", + "min_score": "Min Balas", + "max_score": "Max Balas", + "min_speed": "Min Greitis", + "max_speed": "Max Greitis", + "recognized_license_plate": "Atpažinti Registracijos Numeriai", + "has_clip": "Turi Klipą", + "has_snapshot": "Turi Nuotrauką", + "sub_labels": "Sub Etiketės", + "attributes": "Atributai" + }, + "searchType": { + "thumbnail": "Miniatiūra", + "description": "Aprašymas" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Data 'prieš' turi būti vėliau nei data 'po'.", + "afterDatebeEarlierBefore": "Data 'po' turi būti anksčiau nei data 'prieš'.", + "minScoreMustBeLessOrEqualMaxScore": "'min balas' turi būti mažesnis arba lygus 'max balui'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'max balas' turi būti didesnis arba lygus 'min balui'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'min greitis' privalo būti mažesnis arba lygus 'max greičiui'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'max greitis' privalo būti didesnis arba lygus 'min greičiui'." + } + }, + "tips": { + "title": "Kaip naudoti tekstinius filtrus", + "desc": { + "text": "Filtrai leidžia susiaurinti paieškos rezultatus. Štai kaip juos naudoti įvesties laukelyje:", + "step1": "Įveskite filtravimo raktą po kurio seks dvitaškis (pvz., \"cameras:\").", + "step2": "Pasirinkite reikšmę iš siūlomų arba įveskite savo sugalvotą.", + "step3": "Naudokite kelis filtrus įvesdami juos vieną paskui kitą su tarpu tarp jų.", + "step5": "Laiko rėžio filtro naudojamas {{exampleTime}} formatas.", + "step6": "Pašalinti filtrus spaudžiant 'x' šalia jų.", + "exampleLabel": "Pavyzdys:", + "step4": "Datų filtrai (before: and after:) naudoti {{DateFormat}} formatą." + } + }, + "header": { + "currentFilterType": "Filtruoti Reikšmes", + "noFilters": "Filtrai", + "activeFilters": "Aktyvūs Filtrai" + } + }, + "similaritySearch": { + "title": "Panašumų Paieška", + "active": "Panašumų paieška aktyvi", + "clear": "Išvalyti panašumų paiešką" + }, + "placeholder": { + "search": "Ieškoma…" + } +} diff --git a/web/public/locales/lt/views/settings.json b/web/public/locales/lt/views/settings.json new file mode 100644 index 0000000..5914d3f --- /dev/null +++ b/web/public/locales/lt/views/settings.json @@ -0,0 +1,1007 @@ +{ + "documentTitle": { + "default": "Nustatymai - Frigate", + "authentication": "Autentifikavimo Nustatymai - Frigate", + "camera": "Kameros Nustatymai - Frigate", + "object": "Debug - Frigate", + "general": "Išvaizdos Parametrai - Frigate", + "frigatePlus": "Frigate+ Nustatymai - Frigate", + "notifications": "Pranešimų Nustatymai - Frigate", + "motionTuner": "Judesio Derinimas - Frigate", + "enrichments": "Patobulinimų Nustatymai - Frigate", + "masksAndZones": "Maskavimo ir Zonų redaktorius - Frigate", + "cameraManagement": "Valdyti Kameras - Frigate", + "cameraReview": "Kameros Peržiūros Nustatymai - Frigate", + "globalConfig": "Visuotiniai Parametrai — Frigate", + "cameraConfig": "Kameros Parametrai — Frigate" + }, + "menu": { + "ui": "UI", + "enrichments": "Patobulinimai", + "cameras": "Kameros Nustatymai", + "masksAndZones": "Maskavimai / Zonos", + "motionTuner": "Judesio Derinimas", + "debug": "Debug", + "users": "Vartotojai", + "notifications": "Pranešimai", + "frigateplus": "Frigate+", + "triggers": "Trigeriai", + "roles": "Rolės", + "cameraManagement": "Valdymas", + "cameraReview": "Peržiūra" + }, + "dialog": { + "unsavedChanges": { + "title": "Yra neišsaugotų pakeitimų.", + "desc": "Ar norite išsaugoti savo pakeitimus prieš tęsdami?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Nėra Kameros" + }, + "general": { + "title": "Vartotojo Sąsajos Nustatymai", + "liveDashboard": { + "title": "Tiesioginės Transliacijos Skydelis", + "automaticLiveView": { + "label": "Automatinis Tiesioginis Vaizdas", + "desc": "Automatiškai perjungti į kameros tiesioginį vaizdą kai aptinkama veikla. Išjungus šią funkciją tiesioginės transliacijos skydelyje kamerų vaizdai atsinaujis tik kartą per minutę." + }, + "playAlertVideos": { + "label": "Leist Įspejimų Vaizdus", + "desc": "Pagal nutylėjimą, paskutinieji įspėjimai rodomį kaip maži cikliški vaizdo įrašai. Šią funkciją išjunkite jei norite matyti statinius įspėjimų paveiksliukus šiame įrenginyje/naršyklėje." + }, + "displayCameraNames": { + "label": "Visada Rodyti Kamerų Pavadinimus", + "desc": "Keletos kamerų tiesioginės transliacijos tinklelyje visada rodyti kameros pavadinimą žymoje." + }, + "liveFallbackTimeout": { + "label": "Transliacijos atstatymas neišlauktas", + "desc": "Kai kameros aukštos raiškos transliacija nepasiekiama, persijungti į žemos raiškos rėžimą po tiek tai sekundžių. Default: 3." + } + }, + "storedLayouts": { + "title": "Išsaugoti Išdėstymai", + "desc": "Kamerų išdėstymai kamerų grupėje gali būti perkeliami/keičiami dydžiai. Pozicijos išsaugomos jūsų naršyklės vietinėje atmintyje.", + "clearAll": "Išvalyti Visus Išdėstymus" + }, + "cameraGroupStreaming": { + "title": "Kamerų Grupės Transliacijos Nustatymai", + "desc": "Transliacijos nustatymai kiekvienai kamerų grupei yra saugomi jūsų naršyklės vietinėje atmintyje.", + "clearAll": "Išvalyti Visus Transliavimo Nustatymus" + }, + "recordingsViewer": { + "title": "Įrašų Naršyklė", + "defaultPlaybackRate": { + "label": "Numatytasis Atkūrimo Dažnis", + "desc": "Numatytas atkūrimo dažnis įrašų atkūrimui." + } + }, + "calendar": { + "title": "Kalendorius", + "firstWeekday": { + "label": "Pirma Savaitės Diena", + "desc": "Diena kuria prasideda savaitės peržiūrų kalendoriuje.", + "sunday": "Sekmadienis", + "monday": "Pirmadienis" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Saugoti išdėstimai kamerai{{cameraName}} išvalyti", + "clearStreamingSettings": "Visų kamerų grupių transliavimo nustatymai išvalyti." + }, + "error": { + "clearStoredLayoutFailed": "Nepavyko išvalyti išsaugotų pozicijų išdėstymų: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nepavyko išvalyti transliavimo nustatymų: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Pagerinimų Nustatymai", + "unsavedChanges": "Neišsaugoti Pagerinimų nustatymų pakeitimai", + "birdClassification": { + "title": "Paukščių Klasifikatorius", + "desc": "Paukščių klasifikatorius identifikuoja žinomus paukščius naudojant kvantinizuotą Tensorflow modelį. Kai žinomas paukštis atpažįstamas, jo bendrinis pavadinimas bus pridėtas prie sub_etikečių. Ši informacija yra pridedama vartotojo sąsajoje, filtruose, taip pat ir pranešimuose." + }, + "semanticSearch": { + "title": "Semantic Paieška", + "desc": "Frigate Semantic Paieška leidžia jums atrasti sekamus objektus tarp peržiūrų, naudojant arba pačius paveiksliuks, vartotojo pateiktus tekstinius aprašymus arba automatiškai sugeneruotas reikšmes.", + "reindexNow": { + "label": "Perindeksuoti Dabar", + "desc": "Perindeksavimas sugeneruos įterpinius visiems sekamiems objektams. Šis procesas veiks fone ir priklausomai nuo jūsų turimo sekamų objektų kiekio gali maksimaliai apkrauti jūsų CPU bei užtrukti nemažai laiko.", + "confirmTitle": "Patvirtinti Reindeksavimą", + "confirmDesc": "Ar esate įsitikinę, kad norite reindeksuoti visų sekamų objektų įterpius? Šis processas veiks fone ir gali maksimaliai apkrauti jūsų CPU bei užtrukti nemažai laiko. Progresą jūs galėsite stebėti Tyrinėjimo puslapyje.", + "confirmButton": "Reindeksuoti", + "success": "Reindeksavimas pradėtas sėkmingai.", + "alreadyInProgress": "Redindeksavimas jau vykdomas.", + "error": "Nepavyko pradėti reindeksavimo: {{errorMessage}}" + }, + "modelSize": { + "label": "Modelio Dydis", + "desc": "Modelio dydis naudojamas semantic paieškos įterpiuose.", + "small": { + "title": "mažas", + "desc": "Naudojant mažą pasitelkiama kvantizuota modelio versija kuri reikalauja mažiau RAM, naudojant CPU veikia greičiau su nežįmiu skirtumu įterpių kokybei." + }, + "large": { + "title": "didelis", + "desc": "Naudojant didelį pasitelkiamas pilnas Jina modelis ir automatiškai naudos GPU jei yra galimas." + } + } + }, + "faceRecognition": { + "title": "Veidų Atpažinimas", + "desc": "Veidų atpažinimas leidžia priskirti vardus žmonėms ir kai jų veidai atpažįstami Frigate priskirs žmogaus vardą kaip sub etiketę. Ši informacija prieinama vartotojo sąsajoje, filtruose, taip ir pranešimuose.", + "modelSize": { + "label": "Modelio Dydis", + "desc": "Modelio dydis naudojamas veidų atpažinimui.", + "small": { + "title": "mažas", + "desc": "Naudojant mažą pasitelkiamas FaceNet veidų įterpių modelis kuris efektyviai veikia su daugeliu CPU." + }, + "large": { + "title": "didelis", + "desc": "Naudojant didelį pasitelkiamas ArcFace face embedding modelis ir jei yra galimybė automatiškai naudos GPU." + } + } + }, + "licensePlateRecognition": { + "title": "Registracijos Numerių Atpažinimas", + "desc": "Frigate gali atpažinti automobilių registracijos numerius ir automatiškai pridėti aptikitus simbolius į \"recognized_license_plate\" laukelį arba žinoma pavadinima kaip sub_etiketę \"mašina\" tipo objektams. Dažnas panaudojimas būtų nuskaityti numerius mašinų įvažiuojančių į įvažiavimą arba mašinų pravažiuojančių gatve." + }, + "restart_required": "Privaloma perkrauti (Patobulinimų nustatymai pakeisti)", + "toast": { + "success": "Patobulinimų nustaty buvo pakeisti. Kad pkyčiai būtų pritaikyti perkraukite Frigate.", + "error": "Nepavyko išsaugoti konfiguracijos pakeitimų: {{errorMessage}}" + } + }, + "camera": { + "title": "Kamerų Nustatymai", + "streams": { + "title": "Transliacijos", + "desc": "Laikinai išjunkite kamerą kol Frigate bus perkrautas. Išjungiant kamerą visiškai sustabdo Frigate veiklą šiai kamerai. Nebus aptikimų, įrašų ar debug informacijos.
    Pastaba: Tai neišjungs go2rtc sratų." + }, + "review": { + "desc": "Trumpam įjungti/išjungti įspėjimus ir aptikimus šiai kamerai iki kol Frigate bus perkrautas. Kai išjungta, naujos peržiūros nebus kuriamos. ", + "detections": "Aptikimai ", + "title": "Peržiūra", + "alerts": "Įspėjimai " + }, + "reviewClassification": { + "desc": "Frigate kategorizuoja peržiūras į Įspėjimus ir Aptikimus. Pagal nutylėjimą, visi žmonių ir mašinų objektai yra vertinami kaip įspėjimai. Jūs galite detalizuoti peržiūrų kategorizavimą priskiriant objektas privalomas zonas.", + "zoneObjectAlertsTips": "Visi {{alertsLabels}} objektai aptikti {{zone}} ir {{cameraName}} bus rodomi kaip Įspėjimai.", + "objectDetectionsTips": "Visi {{detectionsLabels}} objektai nekategorizuoti {{cameraName}} bus rodomi kaip Aptikimai nepriklausomai kurioje zonoje ji yra.", + "zoneObjectDetectionsTips": { + "text": "Visi {{detectionsLabels}} objektai nekategorizuoti {{zone}} ir {{cameraName}} bus rodomi kaip Aptikimai.", + "notSelectDetections": "Visi {{detectionsLabels}} objektai aptikti {{zone}} ir {{cameraName}} nekategorizuojami kaip Įspėjimai bus rodomi kaip Aptikimai nepriklausomai kurioje zonoje ji yra.", + "regardlessOfZoneObjectDetectionsTips": "Visi {{detectionsLabels}} objektai nekategorizuoti {{cameraName}} bus rodomi kaip Aptikimai nepriklausomai kurioje zonoje ji yra." + }, + "selectDetectionsZones": "Pasirinkti zonas Aptikimams", + "limitDetections": "Apriboti aptikimus specifinėms zonoms", + "title": "Apžvalgų Kalsifikavimas", + "noDefinedZones": "Šiai kamerai sukurtų zonų nėra.", + "objectAlertsTips": "Visi {{alertsLabels}} objektai kemeroje {{cameraName}} bus rodomi kaip Įspėjimai.", + "unsavedChanges": "Neišsaugoti Apžvalgos Klasifikavimo nustatymai kamerai {{camera}}", + "selectAlertsZones": "Pasirinkti zonas Įspėjimams", + "toast": { + "success": "Apžvalgų Klasifikavimo konfiguracija buvo išsaugota. Restartuoti Frigate kad pokyčiai būtų pritaikyti." + } + }, + "cameraConfig": { + "ffmpeg": { + "rolesUnique": "Kiekviena role (garso, aptikimo, įrašymo) gali buti priskirta tik vienam srautui", + "inputs": "Įvesties Srautas", + "path": "Srauto Kelias", + "pathRequired": "Srauto Kelias yra privalomas", + "pathPlaceholder": "rtsp://...", + "roles": "Rolės", + "rolesRequired": "Privaloma bent viena rolė", + "addInput": "Pridėti Įvesties Srautą", + "removeInput": "Pašalinti Įvesties Srautą", + "inputsRequired": "Privalomas bent vienas įvesties srautas" + }, + "add": "Pridėti Kamerą", + "edit": "Koreguoti Kamerą", + "description": "Konfiguruoti kameros nustatymus įskaitant įvesties srautus ir roles.", + "name": "Kamera Pavadinimas", + "nameRequired": "Kamera pavadinimas yra privalomas", + "nameLength": "Kamera pavadininas privalo būti trumpesnis nei 24 simboliai.", + "namePlaceholder": "pvz., priekinės_durys", + "enabled": "Šjungti", + "toast": { + "success": "Kamera {{cameraName}} sėkmingai išsaugota" + } + }, + "object_descriptions": { + "title": "Generatyvinio DI Objektų Aprašymai", + "desc": "Laikinai įjungti/ išjungti Ganaratyvinio DI objektų aprašymus šiai kamerai. Kai išjungta, šios kameros sekamiems objektams nebus generuojami aprašymai." + }, + "review_descriptions": { + "title": "Generatyvinio DI Apžvalgų Aprašymai", + "desc": "Laikinai įjungti/ išjungti Ganaratyvinio DI apžvalgų aprašymus šiai kamerai. Kai išjungta, šios kameros apžvalgoms nebus generuojami aprašymai." + }, + "addCamera": "Pridėti Naują Kamerą", + "editCamera": "Koreguoti Kamerą:", + "selectCamera": "Pasirinkti Kamera", + "backToSettings": "Atgal į Kameros Nustatymus" + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} taškas", + "point_few": "{{count}} taškai", + "point_other": "{{count}} taškų", + "label": "Zonos", + "documentTitle": "Redaguoti Zonas - Frigate", + "desc": { + "title": "Zonos leidžia apibrėžti specifinį kadro plotą tam, kad galėtumėte įvardinti ar objektas yra tam tikrame plote.", + "documentation": "Dokumentacija" + }, + "add": "Pridėti Zoną", + "edit": "Redaguoti Zoną", + "clickDrawPolygon": "Spragtelkite ant paveiksliuko kad pradėtumėte piešti poligoną.", + "name": { + "title": "Pavadinimas", + "inputPlaceHolder": "Įveskite pavadinimą …", + "tips": "Pavadinimas privalo būti bent 2 simboliai, privalo turėti bent vieną raidę ir negali būti toks pat kaip kita kamera ar kita šios kameros zona." + }, + "inertia": { + "title": "Inercija", + "desc": "Nurodo kiek kadrų objektas turi būti zonoje, kad užskaitytu kaip esantį zonoje. Bazinis: 3" + }, + "loiteringTime": { + "title": "Delsos Laikas", + "desc": "Nurodo minimalų laiką sekundėmis, kurį objektas turi būti zonoje, kad aktyvuotūsi. Bazinis: 0" + }, + "objects": { + "title": "Objektai", + "desc": "Objektų sąrašas kurie taikomi šiai zonai." + }, + "allObjects": "Visi Objektai", + "speedEstimation": { + "title": "Greičio Vertinimas", + "desc": "Įjungti greičio vertinimą objektams šioje zonoje. Zona privalo turėti būtent 4 taškus.", + "lineADistance": "Linijos A atstumas ({{unit}})", + "lineBDistance": "Linijos B atstumas ({{unit}})", + "lineCDistance": "Linijos C atstumas ({{unit}})", + "lineDDistance": "Linijos D atstumas ({{unit}})" + }, + "speedThreshold": { + "title": "Greičio Riba ({{unit}})", + "desc": "Nurodo mnimalų objekto greitį, kad užskaityti esantį zonoje.", + "toast": { + "error": { + "pointLengthError": "Greičio vertinimas buvo išjungtas šiai zonai. Zonos su greičio vertinimu privalo turėti tiksliai 4 taškus.", + "loiteringTimeError": "Zonos su delsos laiku didesniu nei 0 turėtų būti nenaudojamos su greičio vertinimu." + } + } + }, + "toast": { + "success": "Zona ({{zoneName}}) buvo išsaugota." + } + }, + "motionMasks": { + "point_one": "{{count}} taškas", + "point_few": "{{count}} taškai", + "point_other": "{{count}} taškų", + "desc": { + "title": "Judesių maskavimai yra naudojami sumažinti aptikimų užklausoms dėl nepageidajamų judesių. Objektų sekimas taps kėblesnis jei maskuosite per daug.", + "documentation": "Dokumentacija" + }, + "context": { + "title": "Judesių maskavimai yra naudojami sumažinti aptikimų užklausoms dėl nepageidajamų judesių (pvz: medžių šakos, kameros laiko užrašas). Judesių maskavimas turi būti naudojamas labai saikingai, bjektų sekimas taps kėblesnis jei maskuosite per daug." + }, + "polygonAreaTooLarge": { + "tips": "Judesių maskavimas netrukdo objektų aptikimui. Vietoj to turėtumėte naudoti privalomas zonas.", + "title": "Judesio maskuoti dengia {{polygonArea}}% kameros ploto. Didelės judesio maskuotės nerekomenduojamos." + }, + "label": "Judesio Maskuotė", + "documentTitle": "Redaguoti Judesio Maskuotę - Frigate", + "add": "Nauja Judesio Maskuotė", + "edit": "Redaguoti Judesio Maskuotę", + "clickDrawPolygon": "Spragtelti kad piešti poligoną ant atvaizdo.", + "toast": { + "success": { + "title": "{{polygonName}} išsaugotas.", + "noName": "Judesio Maskuotė buvo išsaugota." + } + } + }, + "objectMasks": { + "point_one": "{{count}} taškas", + "point_few": "{{count}} taškai", + "point_other": "{{count}} taškų", + "label": "Objekto Maskuotė", + "documentTitle": "Redaguoti Objekto Maskuotę - Frigate", + "desc": { + "title": "Objektų filtravimo maskuotės naudojamos išfiltruoti klaidingus teigiamus rezultatus pagal vietą parinktam objekto tipui.", + "documentation": "Dokumentacija" + }, + "add": "Pridėti Objekto Maskuotę", + "edit": "Redaguoti Objekto Maskuotę", + "context": "Objektų filtravimo maskuotės naudojamos išfiltruoti klaidingus teigiamus rezultatus pagal vietą parinktam objekto tipui.", + "clickDrawPolygon": "Spragtelti kad piešti poligoną ant atvaizdo.", + "objects": { + "title": "Objektai", + "desc": "Objekto tipai, kurie taikomi šiai objekto maskuotei.", + "allObjectTypes": "Visi objektų tipai" + }, + "toast": { + "success": { + "title": "{{polygonName}} buvo išsaugotas.", + "noName": "Objektų Maskuotė buvo išsaugota." + } + } + }, + "filter": { + "all": "Visos Maskuotės ir Zonos" + }, + "restart_required": "Reikalingas perkrovimas (maskavimai/ zonos pakeisti)", + "toast": { + "success": { + "copyCoordinates": "Poligono {{polyName}} koordinatės nukopijuotos į iškarpinę." + }, + "error": { + "copyCoordinatesFailed": "Nepavyko koordinačių nukopijuoti į iškarpinę." + } + }, + "motionMaskLabel": "Judesio Maskuotė {{number}}", + "objectMaskLabel": "Obejkto Maskuotė {{number}}", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Zonos pavadinime turi būti bent 2 simboliai.", + "mustNotBeSameWithCamera": "Zonos pavadinimas privalo skirtis nuo kameros pavadinimo.", + "alreadyExists": "Šiai kamerai zona šiuo pavadinimu jau egzistuoja.", + "mustNotContainPeriod": "Zonos pavadinimas negali turėti taško.", + "hasIllegalCharacter": "Zonos pavadinime yra neleistinų simbolių." + } + }, + "distance": { + "error": { + "text": "Atstumas privalo būti didesnis arba lygu 0.1.", + "mustBeFilled": "Norint naudoti greičio nustatymą visi atstumų laukai privalo būti užpildyti." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Incerciją privalo būti virš 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Delsos laikas privalo būti didesnis arba lygus 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Greičio riba privalo būti didesnė arba lygi 0.1." + } + }, + "polygonDrawing": { + "removeLastPoint": "Pašalinti paskutinį tašką", + "reset": { + "label": "Išvalyti visus taškus" + }, + "snapPoints": { + "true": "Prikabinti taškus", + "false": "Neprikabinti taškų" + }, + "delete": { + "title": "Patvirtinti Trynimą", + "desc": "Ar esate įsitikinę, kad norite ištrinti {{type}} {{name}}?", + "success": "{{name}} buvo ištrintas." + }, + "error": { + "mustBeFinished": "Poligono brėžinys privalo būti užbaigtas prieš išsaugant." + } + } + } + }, + "motionDetectionTuner": { + "title": "Judesių Aptikimų Derinimas", + "unsavedChanges": "Neišsaugoti Judesių Derinimo pokyčiai ({{camera}})", + "desc": { + "title": "Frigate naudoja judesių aptikimą kaip pirmos eilės patikrinimą įvertinti ar yra kadre kažkas, kam verta būtų atlikti objektų atpažinimą.", + "documentation": "Skaityti Judesių Derinimo Gidą" + }, + "Threshold": { + "title": "Riba", + "desc": "Ribos reikšmė diktuoja kiek pokyčio pikselio apšvietime turi būti, kad būtų traktuojama kaip judesys. Bazinis: 30" + }, + "contourArea": { + "title": "Kontūro Plotas", + "desc": "Kontūro ploto reikšmė yra naudojama įvertinti kurios grupės pasikeitusių pikselių bus vertinami kaip judesys. Bazinis: 10" + }, + "improveContrast": { + "title": "Pagerinti Kontrastą", + "desc": "Pagerinti kontrastą tamsiose scenose. Bazinis: Įjungta" + }, + "toast": { + "success": "Judesių nustatymai buvo išsaugoti." + } + }, + "debug": { + "detectorDesc": "Frigate naudoja jūsų detektorius ({{detectors}}) objektų aptikimui jūsų kameros transliacijoje.", + "audio": { + "noAudioDetections": "Nėra Garso aptikimų", + "title": "Garsas", + "score": "balai", + "currentRMS": "Dabartinis RMS", + "currentdbFS": "Dabartinis dbFS" + }, + "title": "Debug", + "desc": "Debug vaizde rodomas tiesioginis vaizdas sekamų objektų ir statistikos. Objektų sąrašas rodo užvėlintą santrauką aptiktų objektų.", + "openCameraWebUI": "Atverti {{camera}} kameros Web prieigą", + "debugging": "Debugging", + "objectList": "Objektų sąrašas", + "noObjects": "Objektų nėra", + "boundingBoxes": { + "title": "Apribojantys stačiakampiai", + "desc": "Rodyti apribojančius stačiakampius aplink sekamus objektus", + "colors": { + "label": "Objektus Apribojančių Stačiakampių Spalvos", + "info": "
  • Pradžioje, skirtingos spalvos bus priskirtos kiekvienai objekto etiketei
  • Tamsiai mėlyna plona linija simbolizuoja, kad objektas esamu momentu dar nėra aptiktas
  • Pilka linija nurodo kad objektas yra aptiktas kaip nejudantis
  • Stora linija nurodo kad objektas yra automatiškai sekamas (kai įjungta)
  • " + } + }, + "timestamp": { + "title": "Laiko Žyma", + "desc": "Atvaizduoti laiko žymą vaizde" + }, + "zones": { + "title": "Zonos", + "desc": "Rodyti bet kurios zonos ribas" + }, + "mask": { + "title": "Judesio maskuotės", + "desc": "Rodyti judesio maskavimo poligonus" + }, + "motion": { + "title": "Judesio dėžutės", + "desc": "Rodyti dėžutes aplink vietas kur yra aptiktas judesys", + "tips": "

    Judesio Dėžutės


    Raudonos dėžutės bus kadro vietose kur judesys yra aptiktas

    " + }, + "regions": { + "title": "Regionai", + "desc": "Rodyti dėžutes regionų kurie yra parduoti į detektorių", + "tips": "

    Regionų Dėžutės


    Ryškiai žalios dėžutės atvaizduojamos vietose kurios yra perduotos objektų detektoriui.

    " + }, + "paths": { + "title": "Keliai", + "desc": "Rodyti sekamo objekto kelio išskirtinius taškus", + "tips": "

    Keliai


    Linijos ir apskritimai nurodo sekamo objekto judėjimo išskirtinius taškus

    " + }, + "objectShapeFilterDrawing": { + "title": "Filtrų Brėžiniai Objektų Formoms", + "desc": "Norėdami sužinoti atvaizdo plotą ir santykio detales nubrėžkite keturkampį ant atvaizdo", + "score": "Balai", + "ratio": "Santykis", + "area": "Plotas", + "tips": "Įjunkite šią funkciją nupiešti keturkampį ant kameros vaizdo, kad parodyti plotą ir santykį. Šios reikšmės gali būti naudojamos objekto formos filtro parametrams jūsų konfiguracijoje." + } + }, + "users": { + "dialog": { + "deleteUser": { + "warn": "Ar esate įsitikinę, kad norite ištrinti {{username}}?", + "title": "Ištrinti Vartotoją", + "desc": "Šis veiksmas negalės būti atkurtas. Tai visam laikui ištrins vartotojo paskyrą ir ištrins visą susijusią informaciją." + }, + "form": { + "user": { + "title": "Vartotojo vardas", + "desc": "Leidžiamos tik raidės, skaičiai, taškai ir pabraukimai.", + "placeholder": "Įvesti vartotojo vardą" + }, + "password": { + "title": "Slaptažodis", + "placeholder": "Įvesti slaptažodį", + "confirm": { + "title": "Patvirtinti Slaptažodį", + "placeholder": "Patvirtinti Slaptažodį" + }, + "strength": { + "title": "Slaptažodžio sudėtingumas: ", + "weak": "Silpnas", + "medium": "Vidutinis", + "strong": "Stiprus", + "veryStrong": "Labai Stiprus" + }, + "match": "Slaptažodžiai sutampa", + "notMatch": "Slaptažodžiai nesutampa" + }, + "newPassword": { + "title": "Naujas Slaptažodis", + "placeholder": "Įveskite naują slaptažodį", + "confirm": { + "placeholder": "Pakartokite naują slaptažodį" + } + }, + "usernameIsRequired": "Vartotojo vardas yra privalomas", + "passwordIsRequired": "Slaptažodis yra privalomas" + }, + "createUser": { + "title": "Sukurti Naują Vartotoją", + "desc": "Pridėti naują vartotojo paskyrą ir nurodyti prieigos roles prie Frigate funkcijų.", + "usernameOnlyInclude": "Vartotojo vardas gali būti sudarytas iš raidžių, skaičių, . arba _", + "confirmPassword": "Prašome patvirtinti slaptažodį" + }, + "passwordSetting": { + "cannotBeEmpty": "Slaptažodis negali būti tuščias", + "doNotMatch": "Slaptažodžiai nesutampa", + "updatePassword": "Atnaujinkite Spaltažodį vartotojui {{username}}", + "setPassword": "Sukurti Slaptažodį", + "desc": "Sukurti stiprų slaptažodį kad apsaugoti paskyrą." + }, + "changeRole": { + "title": "Pakeisti vartotojo Rolę", + "select": "Pasirinkti rolę", + "desc": "Atnaujinti leidimus vartotojui {{username}}", + "roleInfo": { + "intro": "Pasirinkti tinkama rolę šiam vartotojui:", + "admin": "Admin", + "adminDesc": "Pilna prieiga prie visų funkcijų.", + "viewer": "Žiūrovas", + "viewerDesc": "Leidžiama prie Tiesioginio vaizdo tinklelio, Peržiūrų, Paieškų ir Eksportavimo funkcijų.", + "customDesc": "Specializuota rolė su prieiga prie konkrečios kameros." + } + } + }, + "title": "Vartotojai", + "management": { + "title": "Vartotojų Valdymas", + "desc": "Valdyti šios Frigate aplinkos vartotojų paskyras." + }, + "addUser": "Pridėti Vartotoją", + "updatePassword": "Atkurti Slaptažodį", + "toast": { + "success": { + "createUser": "Vartotojas {{user}} sėkmingai sukurtas", + "deleteUser": "Vartotojas {{user}} sėkmingai ištrintas", + "updatePassword": "Slaptažodis atnaujintas sėkmingai.", + "roleUpdated": "Vartotojui {{user}} rolė sėkmingai atnaujinta" + }, + "error": { + "setPasswordFailed": "nepavyko išsaugoti slaptažodžio: {{errorMessage}}", + "createUserFailed": "Nepavyko sukurti vartotojo: {{errorMessage}}", + "deleteUserFailed": "Nepavyko ištrinti vartotojo: {{errorMessage}}", + "roleUpdateFailed": "Nepavyko atnaujinti rolės: {{errorMessage}}" + } + }, + "table": { + "username": "Vartotojo vardas", + "actions": "Veiksmai", + "role": "Rolė", + "noUsers": "Vartotojų nerasta.", + "changeRole": "Pakeisti vartotojo rolę", + "password": "Atkurti Slaptažodį", + "deleteUser": "Ištrinti vartotoją" + } + }, + "triggers": { + "dialog": { + "deleteTrigger": { + "desc": "Ar esate įsitikinę, kad norite ištrinti trigerį {{triggerName}}? Šis veiksmas negalės būti atstatytas.", + "title": "Ištrinti Trigerį" + }, + "createTrigger": { + "title": "Sukurti Trigerį", + "desc": "Sukurti trigerį kamerai {{camera}}" + }, + "editTrigger": { + "title": "Koreguoti Trigerį", + "desc": "Koreguoti trigerio nustatymus kamerai {{camera}}" + }, + "form": { + "name": { + "title": "Pavadinimas", + "placeholder": "Užvadinkite trigerį", + "error": { + "minLength": "Laukelis turi būti bent dviejų simbolių ilgio.", + "invalidCharacters": "Laukelyje gali būti tik raidės, skaičiai, pabraukimai ir brūkšnelis.", + "alreadyExists": "Trigeris su tokiu vardu jau yra šiai kamerai." + } + }, + "enabled": { + "description": "Įjungti ar išjungti šį trigerį" + }, + "type": { + "title": "Tipas", + "placeholder": "Pasirinkti trigerio tipą" + }, + "content": { + "title": "Turinys", + "imagePlaceholder": "Parinkti miniatiūrą", + "textPlaceholder": "Įvesti teksto turinį", + "imageDesc": "Rodoma tik 100 paskutinių miniatiūrų. Jei norimos miniatiūros nerandate, galite ieškoti senesnių įrašų per Paieškų meniu ir tenai kurti trigerį.", + "textDesc": "Įveskite tekstą kad inicijuotumėte veiksmą kai panašus sekamo objekto aprašymas bus aptiktas.", + "error": { + "required": "Turinys privalomas." + } + }, + "threshold": { + "title": "Riba", + "error": { + "min": "Riba privalo būti bent jau 0", + "max": "Riba privalo būti daugiausiai 1" + } + }, + "actions": { + "title": "Veiksmai", + "desc": "Pagal nutylėjimą, Frigate sukuria MQTT žinutę visiem trigeriams. Subetiketės prideda trigerio pavadinimą prie objekto etiketės. Atributai, tai paieškai pasiekiami metaduomenys saugomi atskirai sekamų objektų metaduomenyse.", + "error": { + "min": "Bent vienas veiksmas privalo būti parinktas." + } + }, + "friendly_name": { + "title": "Draugiškas Pavadinimas", + "placeholder": "Pavadinikite ar apibūdinkite trigerį", + "description": "Draugiškas pavadinimas ar apibūdinimas šiam trigeriui nėra būtinas." + } + } + }, + "documentTitle": "Trigeriai", + "management": { + "title": "Trigeriai", + "desc": "Valdykite trigerius kamerai {{camera}}. Naudokite miniatiūros tipą, kad panašios miniatiūros būtų jūsų pasirinkto objekto trigeris, o aprašymo trigerį kad panašūs aprašymai būtų trigeris pagal jūsų parašytą tekstą." + }, + "addTrigger": "Pridėti Trigerį", + "table": { + "name": "Pavadinimas", + "type": "Tipas", + "content": "Turinys", + "threshold": "Riba", + "actions": "Veiksmai", + "noTriggers": "Šiai kamerai nėra sukonfiguruotų trigerių.", + "edit": "Koreguoti", + "deleteTrigger": "Trinti Trigerį", + "lastTriggered": "Paskutinį kartą suveikė" + }, + "type": { + "thumbnail": "Miniatiūra", + "description": "Aprašymas" + }, + "actions": { + "alert": "Pažymėti kaip įspėjimą", + "notification": "Siųsti Pranešimą" + }, + "toast": { + "success": { + "createTrigger": "Trigeris {{name}} sėkmingai sukurtas.", + "updateTrigger": "Trigeris {{name}} sėkmingai atnaujintas.", + "deleteTrigger": "Trigeris {{name}} sėkmingai ištrintas." + }, + "error": { + "createTriggerFailed": "Nepavyko sukurti trigerio: {{errorMessage}}", + "updateTriggerFailed": "Nepavyko atnaujinti trigerio: {{errorMessage}}", + "deleteTriggerFailed": "Nepavyko ištrinti trigerio: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Semantic Paieška išjungta", + "desc": "Norint naudoti Trigerius Semantic Paieška privalo būti įjungta." + } + }, + "notification": { + "title": "Pranešimai", + "notificationSettings": { + "title": "Pranešimų Nustatymai", + "desc": "Frigate praneįimai sukurti veikti su push pranešimais į įrenginį kai naršoma per naršyklę arba įdiegta kaip PWA." + }, + "notificationUnavailable": { + "title": "Pranešimai Negalimi", + "desc": "Web push pranešimai reikalauja saugios aplinkos (https://...). Tai yra naršyklės apribojimai. Atsidarykit Frigate saugiu kanalu kad galėtumėte naudotis pranešimais." + }, + "globalSettings": { + "title": "Visuotiniai Nustatymai", + "desc": "Laikinai sustabdyti pranešimus iš konkrečios kameros į registruotus įrenginius." + }, + "email": { + "title": "El.paštas", + "placeholder": "pvz.: laiskas@laiskas.com", + "desc": "El paštas turi būti veikiantis ir jums prieinamas, kad jus pasiektų informacija jei bus problemų su push pranešimų paslauga." + }, + "cameras": { + "title": "Kameros", + "noCameras": "Nėra kamerų", + "desc": "Pasirinkite kurioms kameroms norite įjungti pranešimus." + }, + "deviceSpecific": "Įrenginio Specifiniai Nustatymai", + "registerDevice": "Registruoti Šį Įrenginį", + "unregisterDevice": "Išregistruoti Šį Įrenginį", + "sendTestNotification": "Siųsti bandomąjį pranešimą", + "unsavedRegistrations": "Neišsaugotos Pranešimo registracijos", + "unsavedChanges": "Neišsaugoti Pranešimų pakeitimai", + "active": "Aktyvūs Pranešimai", + "suspended": "Pranešimai sustabdyti {{time}}", + "suspendTime": { + "suspend": "Sustabdyti", + "5minutes": "Sustabdyti 5 minutėms", + "10minutes": "Sustabdyti 10 minučių", + "30minutes": "Sustabdyti 30 minučių", + "1hour": "Sustabdyti 1 valandai", + "12hours": "Sustabdyti 12 valandų", + "24hours": "Sustabdyti 24 valandoms", + "untilRestart": "Sustabdyti iki perkrovimo" + }, + "cancelSuspension": "Atšaukti Sustabdymą", + "toast": { + "success": { + "registered": "Sėkmingai užregistruota pranešimams. Frigate perkrovimas yra būtinas, kad nors kokie pranešimai būtų išsiųsti (net ir bandomieji).", + "settingSaved": "Pranešimų nustatymai buvo išsaugoti." + }, + "error": { + "registerFailed": "Nepavyko išsaugoti pranešimų registravimo." + } + } + }, + "frigatePlus": { + "title": "Frigate+ Nustatymai", + "apiKey": { + "title": "Frigate+ API raktas", + "validated": "Frigate+ API raktas aptiktas ir patvirtintas", + "notValidated": "Frigate+ API raktas neaptiktas ir nepatvirtintas", + "desc": "Frigate+ API raktas įgalina integraciją su Frigate+ paslauga.", + "plusLink": "Skaityti daugiau apie Frigate+" + }, + "snapshotConfig": { + "title": "Momentinių kadrų Konfiguravimas", + "desc": "Pateikti į Frigate+ reikalauja abiejų, momentinių kadrų ir švarios_kopijosmomentinių kadrų įjungimo jūsų konfiguracijoje.", + "cleanCopyWarning": "Kai kurios kameros turi momentinius kadrus įjungtus tačiau švari kopija išjungta. Turite įjungti švarią_kopiją savo momentinės kopijos nustatymuose, kad galėtumėte teikti paveikslėlius į Frigate+.", + "table": { + "camera": "Kamera", + "snapshots": "Momentiniai Kadrai", + "cleanCopySnapshots": "Momentiniai kadrai švari_kopija" + } + }, + "modelInfo": { + "title": "Modelio Informacija", + "modelType": "Modelio Tipas", + "trainDate": "Apmokymo Data", + "baseModel": "Bazinis Modelis", + "plusModelType": { + "baseModel": "Bazinis Modelis", + "userModel": "Priderinta" + }, + "supportedDetectors": "Palaikomi Detektoriai", + "cameras": "Kameros", + "loading": "Užkraunama modelio informacija…", + "error": "Nepavyko užkrauti modelio informacijos", + "availableModels": "Pireinami Modeliai", + "loadingAvailableModels": "Kraunami prieinami modeliai…", + "modelSelect": "Jūsų prieinami modeliai iš Frigate+ gali būti pasirinkti čia. Pastaba, pasirinkiti galite modelius tik tuos kurie suderinami su esamu detektoriumi." + }, + "unsavedChanges": "Neišsaugoti Frigate+ nustatymų pokyčiai", + "restart_required": "Perkrovimas privalomas (Frigate+ modeliai pakeisti)", + "toast": { + "success": "Frigate+ nustatymai buvo išsaugoti. Perkraukite Frigate kad pritaikytumėte pokyčius.", + "error": "Nepavyko išsaugoti konfiguraijos pokyčių: {{errorMessage}}" + } + }, + "roles": { + "addRole": "Pridėti rolę", + "table": { + "role": "Rolė", + "cameras": "Kameros", + "actions": "Veiksmai", + "deleteRole": "Pašalinti rolę", + "noRoles": "Specializuotų rolių nerasta.", + "editCameras": "Koreguoti Kameras" + }, + "toast": { + "success": { + "deleteRole": "Rolė {{role}} sėkmingai pašalinta", + "userRolesUpdated_one": "{{count}} šios rolės vartotojas buvo priskirti rolei 'žiūrovas', kuri turi prieigą prie visų kamerų.", + "userRolesUpdated_few": "{{count}} šios rolės vartotojai buvo priskirti rolei 'žiūrovas', kuri turi prieigą prie visų kamerų.", + "userRolesUpdated_other": "{{count}} šios rolės vartotojų buvo priskirti rolei 'žiūrovas', kuri turi prieigą prie visų kamerų.", + "createRole": "Rolė {{role}} sėkmingai sukurta", + "updateCameras": "Atnaujintos kameros rolei {{role}}" + }, + "error": { + "createRoleFailed": "Nepavyko sukurti rolės: {{errorMessage}}", + "updateCamerasFailed": "Nepavyko atnaujinti kamerų: {{errorMessage}}", + "deleteRoleFailed": "Nepavyko ištrinti rolės: {{errorMessage}}", + "userUpdateFailed": "Nepavyko atnaujinti vartotojo rolių: {{errorMessage}}" + } + }, + "dialog": { + "deleteRole": { + "title": "Pašalinti rolę", + "deleting": "Šalinama...", + "desc": "Šis veiksmas neatkuriamas. Rolė bus ištrinta, likusiems jos turėtojams bus priskirta 'žiūrovo' rolė, kuri leis vartotojui matyti visas kameras.", + "warn": "Ar esate įsitikinę, kad norite ištrinti {{role}}?" + }, + "form": { + "cameras": { + "title": "Kameros", + "required": "Mažiausiai viena kamera turi būti pažymėta.", + "desc": "Pasirinkinte kamerą prie kurios ši rolė suteiks prieigą. Privaloma nurodyti bent vieną." + }, + "role": { + "title": "Rolės pavadinimas", + "placeholder": "Įveskite rolės pavadinimą", + "roleIsRequired": "Rolės pavadinimas yra privalomas", + "roleExists": "Toks rolės pavadinimas jau egzistuoja.", + "desc": "Ledžiama naudoti tik raides, skaičius, taškus ir pabraukimus.", + "roleOnlyInclude": "Rolės pavadinime gali būti tik raides, skaičius, . ar _" + } + }, + "createRole": { + "title": "Sukurti Naują Rolę", + "desc": "Pridėti naują rolę ir priskirti prieigas prie kamerų." + }, + "editCameras": { + "title": "Koreguoti Rolės Kameras", + "desc": "Atnaujinti prieigą prie kameros rolei {{role}}." + } + }, + "management": { + "title": "Žiūrovo Rolės Valdymas", + "desc": "Valdyti šios Frigate aplinkos specializuotas žiūrovo roles ir kamerų prieigos leidimus." + } + }, + "cameraWizard": { + "title": "Pridėti Kamerą", + "description": "Sekite žemiau nurodytus žingsnius norėdami pridėti naują kamerą prie savo Frigate.", + "steps": { + "nameAndConnection": "Pavadinimas ir Jungtis", + "streamConfiguration": "Transliacijos Nustatymai", + "validationAndTesting": "Patikra ir Testavimas", + "probeOrSnapshot": "Mėginys ar Kadras" + }, + "save": { + "success": "Nauja kamera sėkmingai išsaugota {{cameraName}}.", + "failure": "Klaida išsaugant {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Rezoliucija", + "video": "Vaizdas", + "audio": "Garsas", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Prašau pateikti galiojantį transliacijos URL", + "testFailed": "Transliacijos testas nepavyko: {{error}}" + }, + "step1": { + "description": "Įveskite savo kameros informaciją ir pasirinkite \"probe\" kamerai arba rankiniu būdų pasirinkite gamintoją.", + "cameraName": "Kameros Pavadinimas", + "cameraNamePlaceholder": "pvz., priekines_durys arba Galinio Kiemo Vaizdas", + "host": "Host/IP Adresas", + "port": "Port", + "username": "Vartotojo vardas", + "usernamePlaceholder": "Pasirinktinai", + "password": "Slaptažodis", + "passwordPlaceholder": "Pasirinktinai", + "selectTransport": "Pasirinkite perdavimo protokolą", + "cameraBrand": "Kameros Gamintojas", + "selectBrand": "Pasirinkite kameros gamintoją URL šablonui", + "customUrl": "Kameros Transliacijos URL", + "brandInformation": "Gamintojo informacija", + "brandUrlFormat": "Kamerai su RTSP URL formatas kaip: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://vartotojas:slaptažodis@host:port/path", + "testConnection": "Testuoti Susijungimą", + "testSuccess": "Susijungimo testas sėkmingas!", + "connectionSettings": "Prisijungimo Nustatymai", + "detectionMethod": "Transliacijos Aptikimo Metodas", + "onvifPort": "ONVIF Portas", + "probeMode": "Probuoti kamerą", + "manualMode": "Rankinis pasirinkimas", + "detectionMethodDescription": "Probuoti kamerą su ONVIF (jei palaikomas), kad rasti kameros srautų URL, arba rankiniu būdu pasirinkite kameros gamintoją kad naudoti iš anksto žinomus URL adresus. Kad įvesti individualų RTSP URL, pasirinkite rankinį metodą ir pasirinkite \"Kitas\".", + "onvifPortDescription": "Kameroms su palaikomu ONVIF, tai dažniausiai 80 ar 8080.", + "useDigestAuth": "Naudoti digest autentifikavimą", + "useDigestAuthDescription": "Naudoti HTTP digest autentifikavimą su ONVIF. Kai kurios kameros gali reikalauti priskirto ONVIF vartojo/ slaptažodžio vietoje standartinio admin vartotojo.", + "errors": { + "brandOrCustomUrlRequired": "Arba pasirinkite kameros gamintoją su host/IP arb rinkites 'Kita' su individualiu URL", + "nameRequired": "Kameros pavadinimas privalomas", + "nameLength": "Kameros pavadinimas privalo būti 64 ar mažiau simbolių", + "invalidCharacters": "Kameros pavadinime yra neleistinų simbolių", + "nameExists": "Toks kameros pavadinimas jau yra", + "customUrlRtspRequired": "Individualus URL turi prasidėti su \"rtsp://\". Rankinis konfiguravimas yra reikalaujas ne-RTSP kamerų transliacijoms." + } + }, + "step2": { + "description": "Probuoti kamerą srautams atrasti ar konfiguruoti nustatymus rankiniu būdų pasirinktam aptikimo metodui.", + "testSuccess": "Prisijungimo testas sėkmingas!", + "testFailed": "Prisijungimo testas nepavyko. Prašau patikrinkite savo įvestis ir bandykite vėl.", + "testFailedTitle": "Testas Nepavyko", + "streamDetails": "Srauto Detalės", + "probing": "Probuojama kamera...", + "retry": "Pakartoti", + "testing": { + "probingMetadata": "Probuojami kameros metaduomenys...", + "fetchingSnapshot": "Gaunama kameros atvaizdas..." + }, + "probeFailed": "Nepavyko probuoti kameros: {{error}}", + "probingDevice": "Probuojamas įrenginys...", + "probeSuccessful": "Probuojamas sėkmingas", + "probeError": "Probavimo Klaida", + "probeNoSuccess": "Probavimas nesėkmingas", + "deviceInfo": "Įrenginio Informacija", + "manufacturer": "Gamintojas", + "model": "Modelis", + "firmware": "Programinė Įranga", + "profiles": "Profiliai", + "ptzSupport": "PTZ Palaikymas", + "autotrackingSupport": "Autosekimo Palaikymas", + "presets": "Paruoštukai", + "rtspCandidates": "RTSP Kandidatai", + "rtspCandidatesDescription": "Sekantys RTSP URL adresai buvo rasti probuojant kamerą. Testuoti prisijungimą kad pamatyti srauto metaduomenis.", + "noRtspCandidates": "Iš kameros nerasta RTSP URL'ų. Jūsų prisijungimo duomenys gali būti neteisingi. ar kamera nepalaiko ONVIF ar metodas naudotas rasti RTSP URL. Grįžkite atgal ir rankiniu būdu suveskite RTSP URL.", + "candidateStreamTitle": "Kandidatų {{number}}", + "useCandidate": "Naudoti", + "uriCopy": "Kopijuoti", + "uriCopied": "URI nukopijuotas į iškarpinę", + "testConnection": "Testuoti Prisijungimą", + "toggleUriView": "Spustelkit kad atskleisti pilną URI vaizdą", + "connected": "Prisijungta", + "notConnected": "Neprisijungta", + "errors": { + "hostRequired": "Host/IP adresas yra privalomas" + } + }, + "step3": { + "description": "Konfiguruoti srauto roles ir prideti papildomus srautus jūsų kamerai.", + "streamsTitle": "Kameros Srautai", + "addStream": "Pridėti Srautus", + "addAnotherStream": "Dar Pridėti Srautą", + "streamTitle": "Srautų {{number}}", + "streamUrl": "Srauto URL", + "streamUrlPlaceholder": "rtsp://vartotojas:slaptažodis@host:port/kelias", + "selectStream": "Pasirinkti srautą", + "searchCandidates": "Ieškoti kandidatų...", + "noStreamFound": "Srautų nerasta", + "url": "URL", + "resolution": "Raiška", + "selectResolution": "Pasirinkti raišką", + "quality": "Kokybė", + "selectQuality": "Pasirinkti kokybę", + "roles": "Rolės", + "roleLabels": { + "detect": "Objektų Aptikimas", + "record": "Įrašymas", + "audio": "Garsas" + }, + "testStream": "Testuoti Prisijungimą", + "testSuccess": "Srauto testas sėkmingas!", + "testFailed": "Srauto testas nepavyko", + "testFailedTitle": "Testas Nepavyko", + "connected": "Prisijungta", + "notConnected": "Neprisijungta", + "featuresTitle": "Savybės", + "go2rtc": "Sumažinti prisijungimų prie kameros", + "detectRoleWarning": "Bent vienas srautas privalo turėti \"aptikimo\" rolę norint tęsti.", + "rolesPopover": { + "title": "Srauto Rolės", + "detect": "Pagrindinis srautas ojektų aptikimui.", + "record": "Įrašo vaizdo segmentus pagal konfiguracijos nustatymus.", + "audio": "Srautas audio aptikimams." + }, + "featuresPopover": { + "title": "Srauto Savybės", + "description": "Naudoti go2rtc pertransliavimą kad sumažinti prisijungimus prie kameros." + } + }, + "step4": { + "description": "Galutinis patvirtinimas ir analizė prieš išsaugant jūsų naują kamerą. Prijunkite kiekvieną srautą prieš išsaugant.", + "validationTitle": "Srauto Tikrinimas", + "connectAllStreams": "Prijungti Visus Srautus", + "reconnectionSuccess": "Pakartotinis Prisijungimas Sėkmingas.", + "reconnectionPartial": "Prie kai kurių srautų nepavyko atstatyti prisijungimo.", + "streamUnavailable": "Srauto peržiūra negalia", + "reload": "Perleisti", + "connecting": "Prisijungiama...", + "streamTitle": "Srautų {{number}}", + "valid": "Tinkamas", + "failed": "Nepavyko", + "notTested": "Netestuota", + "connectStream": "Prijungti", + "connectingStream": "Prisijungiama", + "disconnectStream": "Atsijungti", + "estimatedBandwidth": "Tikėtinas Greitis", + "roles": "Rolės", + "ffmpegModule": "Naudoti srauto pritaikymo rėžimą" + } + } +} diff --git a/web/public/locales/lt/views/system.json b/web/public/locales/lt/views/system.json new file mode 100644 index 0000000..06493b4 --- /dev/null +++ b/web/public/locales/lt/views/system.json @@ -0,0 +1,208 @@ +{ + "documentTitle": { + "cameras": "Kamerų Statistika - Frigate", + "storage": "Saugyklos Statistika - Frigate", + "logs": { + "frigate": "Frigate Žurnalas - Frigate", + "go2rtc": "Go2RTC Žurnalas - Frigate", + "nginx": "Nginx Žurnalas - Frigate" + }, + "general": "Bendroji Statistika - Frigate", + "enrichments": "Pagerinimų Statistika - Frigate" + }, + "title": "Sistema", + "metrics": "Sistemos metrikos", + "logs": { + "download": { + "label": "Parsisiųsti Žurnalą" + }, + "copy": { + "label": "Kopijuoti į iškarpinę", + "success": "Nukopijuoti įrašai į iškarpinę", + "error": "Nepavyko nukopijuoti įrašų į iškarpinę" + }, + "type": { + "label": "Tipas", + "timestamp": "Laiko žymė", + "tag": "Žyma", + "message": "Žinutė" + }, + "tips": "Įrašai yra transliuojami iš serverio", + "toast": { + "error": { + "fetchingLogsFailed": "Klaida nuskaitant įrašus: {{errorMessage}}", + "whileStreamingLogs": "Klaidai transliuojant įrašus: {{errorMessage}}" + } + } + }, + "general": { + "title": "Bendrinis", + "detector": { + "title": "Detektoriai", + "inferenceSpeed": "Detektorių darbo greitis", + "temperature": "Detektorių Temperatūra", + "cpuUsage": "Detektorių CPU Naudojimas", + "memoryUsage": "Detektorių Atminties Naudojimas", + "cpuUsageInformation": "CPU vartojimas ruošiant duomenis detektorių modeliams. Ši reikšmė nevertina inference vartojimo, net jei yra naudojamas GPU akseleratorius." + }, + "hardwareInfo": { + "title": "Techninės įrangos Info", + "gpuUsage": "GPU Naudojimas", + "gpuMemory": "GPU Atmintis", + "gpuEncoder": "GPU Kodavimas", + "gpuDecoder": "GPU Dekodavimas", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo Išvestis", + "returnCode": "Grįžtamas Kodas: {{code}}", + "processOutput": "Proceso Išvestis:", + "processError": "Proceso Klaida:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI Išvestis", + "name": "Pavadinimas: {{name}}", + "driver": "Tvarkyklė: {{driver}}", + "cudaComputerCapability": "CUDA Compute Galimybės: {{cuda_compute}}", + "vbios": "VBios Info: {{vbios}}" + }, + "closeInfo": { + "label": "Užverti GPU info" + }, + "copyInfo": { + "label": "Kopijuoti GPU Info" + }, + "toast": { + "success": "Nukopijuota GPU info į iškarpinę" + } + }, + "npuUsage": "NPU Naudojimas", + "npuMemory": "NPU Atmintis", + "intelGpuWarning": { + "title": "Intel GPU statistikų įspėjimas", + "message": "GPU statistika negalima", + "description": "Tai žinoma problema su Intel GPU statistikos raportavimo įrankiu (intel_gpu_top), kai jis stringa ir pakartotinai grąžina GPU vartojimas 0% net tais atvejais kai hardware acceleration ir objektų aptikimas taisiklingai veikia naudojant (i)GPU. Tai nėra Frigate klaida. Laikinai padeda įrenginio perkrovimas, kad įsitikinti teisingu GPU funkcionavimu. Tai neįtakoja spartos." + } + }, + "otherProcesses": { + "title": "Kiti Procesai", + "processCpuUsage": "Procesų CPU Naudojimas", + "processMemoryUsage": "Procesu Atminties Naudojimas", + "series": { + "go2rtc": "go2rtc", + "recording": "įrašas", + "review_segment": "peržiūros segmentas", + "embeddings": "įterpiniai", + "audio_detector": "garso aptikiklis" + } + } + }, + "storage": { + "title": "Saugykla", + "overview": "Apžvalga", + "recordings": { + "title": "Įrašai", + "tips": "Ši reikšmė nurodo kiek iš viso Frigate duombazėje esantys įrašai užima vietos saugykloje. Frigate neseka kiek vietos užima visi kiti failai esantys laikmenoje.", + "earliestRecording": "Anksčiausias esantis įrašas:" + }, + "cameraStorage": { + "title": "Kameros Saugykla", + "camera": "Kamera", + "unusedStorageInformation": "Neišnaudotos Saugyklos Informacija", + "storageUsed": "Saugykla", + "percentageOfTotalUsed": "Procentas nuo Viso", + "bandwidth": "Pralaidumas", + "unused": { + "title": "Nepanaudota", + "tips": "Jei saugykloje turite daugiau failų apart Frigate įrašų, ši reikšmė neatspindės tikslios likusios laisvos vietos Frigate panaudojimui. Frigate neseka saugyklos panaudojimo už savo įrašų ribų." + } + }, + "shm": { + "title": "SHM (bendrinama atmintis) priskyrimas", + "warning": "Esamas SHM dydis {{total}}MB yra per mažas. Pridėkite bent jau {{min_shm}}MB." + } + }, + "cameras": { + "title": "Kameros", + "overview": "Apžvalga", + "info": { + "aspectRatio": "formato santykis", + "cameraProbeInfo": "{{camera}} Kameros srauto informacija", + "streamDataFromFFPROBE": "Transliacijos duomenys yra surenkami su ffprobe.", + "fetching": "Gaunamai Kameros Duomenys", + "stream": "Transliacija {{idx}}", + "video": "Vaizdas:", + "codec": "Kodekas:", + "resolution": "Raiška:", + "fps": "FPS:", + "unknown": "Nežinoma", + "audio": "Garsas:", + "error": "Klaida:{{error}}", + "tips": { + "title": "Kameros Srauto Informacija" + } + }, + "framesAndDetections": "Kadrai / Aptikimai", + "label": { + "camera": "kamera", + "detect": "aptikti", + "skipped": "praleista", + "ffmpeg": "FFmpeg", + "capture": "užfiksuota", + "overallFramesPerSecond": "viso kadrų per sekundę", + "overallDetectionsPerSecond": "viso aptikimų per sekundę", + "overallSkippedDetectionsPerSecond": "viso praleista aptikimų per sekundę", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} ufiksuota", + "cameraDetect": "{{camName}} susekta", + "cameraFramesPerSecond": "{{camName}} kadrai per sekundę", + "cameraDetectionsPerSecond": "{{camName}} aptikimai per sekundę", + "cameraSkippedDetectionsPerSecond": "{{camName}} praleista aptikimų per sekundę" + }, + "toast": { + "success": { + "copyToClipboard": "Srauto informacija nukopijuotą į iškarpinę." + }, + "error": { + "unableToProbeCamera": "Negalima gauti kameros mėginio: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Paskutinį kartą atnaujinta: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} turi aukštą CPU suvartojimą FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} turi auktšą CPU vartojimą aptikimams ({{detectAvg}}%)", + "healthy": "Sistemos būklė sveika", + "reindexingEmbeddings": "Įterpinių reideksavimas ({{processed}}% baigtas)", + "cameraIsOffline": "{{camera}} yra nepasiekiama", + "detectIsSlow": "{{detect}} yra lėtas ({{speed}}ms)", + "detectIsVerySlow": "{{detect}} yra labai lėtas ({{speed}}ms)", + "shmTooLow": "/dev/shm priskirta ({{total}} MB) turi būti padidinta bent jau iki {{min}} MB." + }, + "enrichments": { + "title": "Patobulinimai", + "embeddings": { + "yolov9_plate_detection": "YOLOv9 Numerių Aptikimai", + "yolov9_plate_detection_speed": "YOLOv9 Numerių Aptikimų Greitis", + "text_embedding_speed": "Teksto Įterpimų Greitis", + "plate_recognition_speed": "Numerių Atpažinimo Greitis", + "face_recognition_speed": "Veidų Atpažinimo Greitis", + "face_embedding_speed": "Veidų Įterpimų Greitis", + "image_embedding_speed": "Vaizdo Įterpimo Greitis", + "plate_recognition": "Numerių Atpažinimas", + "face_recognition": "Veido Atpažinimas", + "text_embedding": "Teksto Įterpimas", + "image_embedding": "Vaizdo Įterpimas", + "review_description": "Apžvalgos Aprašymas", + "review_description_speed": "Apžvalgos Aprašymo Greitis", + "review_description_events_per_second": "Apžvalgos Aprašymas", + "object_description": "Objekto Aprašymas", + "object_description_speed": "Objekto Aprašymo Greitis", + "object_description_events_per_second": "Objekto Aprašymas", + "classification": "{{name}} Klasifikavimas", + "classification_speed": "{{name}} Klasifikavimo Greitis", + "classification_events_per_second": "{{name}} Klasifikavimo Įvykiai Per Sekundę" + }, + "infPerSecond": "Išvadų Per Sekundę", + "averageInf": "Vidutinis Vertinimo Laikas" + } +} diff --git a/web/public/locales/lv/audio.json b/web/public/locales/lv/audio.json new file mode 100644 index 0000000..56bab07 --- /dev/null +++ b/web/public/locales/lv/audio.json @@ -0,0 +1,126 @@ +{ + "speech": "Runāšana", + "bicycle": "Velosipēds", + "babbling": "Pļāpāšana", + "car": "Automašīna", + "yell": "Kliedziens", + "motorcycle": "Motocikls", + "bellow": "Rēciens", + "whoop": "Izsauciens", + "bus": "Autobuss", + "whispering": "Čuksti", + "train": "Vilciens", + "insect": "Kukainis", + "mosquito": "Ods", + "fly": "Muša", + "frog": "Varde", + "snake": "Čūska", + "music": "Mūzika", + "musical_instrument": "Mūzikas instruments", + "plucked_string_instrument": "Stīgu instruments", + "guitar": "Ģitāra", + "electric_guitar": "Elektriskā ģitāra", + "bass_guitar": "Basģitāra", + "acoustic_guitar": "Akustiskā ģitāra", + "banjo": "Bandžo", + "piano": "Klavieres", + "electric_piano": "Sintezators", + "organ": "Ērģeles", + "electronic_organ": "Elektriskās ērģeles", + "harpsichord": "Klavesīns", + "laughter": "Smiekli", + "boat": "Laiva", + "snicker": "Ķiķināšana", + "camera": "Kamera", + "crying": "Raudāšana", + "child_singing": "Bērna dziedāšana", + "humming": "Dungošana", + "sneeze": "Šķaudīšana", + "footsteps": "Soļi", + "run": "Skrien", + "chewing": "Košļāšana", + "fart": "Pirdiens", + "pets": "Mājdzīvnieki", + "dog": "Suns", + "bark": "Riešana", + "cat": "Kaķis", + "purr": "Murrā", + "meow": "Ņaud", + "horse": "Zirgs", + "pig": "Cūka", + "turkey": "Tītars", + "duck": "Pīle", + "quack": "Pēkšk", + "goose": "Zos", + "owl": "Pūce", + "dogs": "Suņi", + "rats": "Žurkas", + "mouse": "Pele", + "buzz": "Dūc", + "pulse": "Pulsē", + "inside": "Iekšā", + "outside": "Ārā", + "echo": "Atbals", + "noise": "Troksnis", + "vibration": "Vibrācija", + "sigh": "Nopūta", + "singing": "Nopūsties", + "choir": "Koris", + "yodeling": "Jodelēšana", + "mantra": "Mantra", + "rapping": "Repot", + "whistling": "Svilpot", + "sniff": "Ošņāt", + "hands": "Rokas", + "animal": "Dzīvnieks", + "drum_kit": "Bungu komplekts", + "drum": "Bungas", + "gong": "Gongs", + "violin": "Vijole", + "flute": "Flauta", + "cello": "Čells", + "bell": "Zvans", + "ship": "Kuģis", + "motorboat": "Motorlaiva", + "sailboat": "Buru laiva", + "motor_vehicle": "Automašīna", + "car_alarm": "Auto signalizācija", + "truck": "Smagā automašīna", + "air_horn": "Gaisa taure", + "police_car": "Policijas auto", + "fire_engine": "Ugunsdzēsēju auto", + "train_whistle": "Vilciena taure", + "aircraft": "Lidmašīna", + "aircraft_engine": "Lidmašīnas dzinējs", + "jet_engine": "Reaktīvais dzinējs", + "propeller": "Propellers", + "helicopter": "Helikopters", + "engine": "Dzinējs", + "lawn_mower": "Zāles pļāvējs", + "door": "Durvis", + "doorbell": "Durvju zvans", + "accelerating": "Paātrinās", + "engine_starting": "Dzinēja iedarbināšana", + "idling": "rūc", + "slam": "Aizcirst", + "sink": "Izlietne", + "bathtub": "Vanna", + "hair_dryer": "Fēns", + "toothbrush": "Zobu birste", + "coin": "Monēta", + "alarm": "Signalizācija", + "telephone": "Telefons", + "siren": "Sirēna", + "smoke_detector": "Dūmu detektors", + "clock": "Pulkstens", + "printer": "Printeris", + "whistle": "Svilpe", + "tools": "Instrumenti", + "drill": "Urbis", + "gunshot": "Šaviens", + "explosion": "Sprādziens", + "fireworks": "Uguņošana", + "glass": "Stikls", + "white_noise": "Baltais troksnis", + "radio": "Radio" +} diff --git a/web/public/locales/lv/common.json b/web/public/locales/lv/common.json new file mode 100644 index 0000000..61b3d07 --- /dev/null +++ b/web/public/locales/lv/common.json @@ -0,0 +1,316 @@ +{ + "time": { + "untilForTime": "Līdz {{time}}", + "today": "Šodien", + "yesterday": "Vakar", + "last7": "Pēdējās 7 dienas", + "last14": "Pēdējās 14 dienas", + "last30": "Pēdējās 30 dienas", + "thisWeek": "Šonedēļ", + "lastWeek": "Pagājušajā nedēļā", + "thisMonth": "Šomēnes", + "lastMonth": "Pagājušajā mēnesī", + "5minutes": "5 minūtes", + "10minutes": "10 minūtes", + "30minutes": "30 minūtes", + "1hour": "1 stunda", + "12hours": "12 stundas", + "24hours": "24 stundas", + "pm": "pm", + "am": "am", + "yr": "{{time}}g", + "year_zero": "{{time}} gadi", + "year_one": "{{time}} gads", + "year_other": "{{time}} gadi", + "mo": "{{time}}mēn", + "month_zero": "{{time}}mēneši", + "month_one": "{{time}}mēnesis", + "month_other": "{{time}}mēneši", + "d": "{{time}}d", + "day_zero": "{{time}} dienas", + "day_one": "{{time}} diena", + "day_other": "{{time}} dienas", + "h": "{{time}}s", + "hour_zero": "{{time}}stundas", + "hour_one": "{{time}}stunda", + "hour_other": "{{time}}stundas", + "m": "{{time}}m", + "minute_zero": "{{time}} minutes", + "minute_one": "{{time}} minute", + "minute_other": "{{time}} minutes", + "s": "{{time}}sek", + "second_zero": "{{time}} sekundes", + "second_one": "{{time}} sekunde", + "second_other": "{{time}} sekundes", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "inProgress": "Izpilda", + "invalidStartTime": "Nederīgs sākuma laiks", + "invalidEndTime": "Nederīgs beigu laiks", + "untilForRestart": "Līdz Frigate pārstartējas.", + "untilRestart": "Līdz pārstartēšanai", + "ago": "{{timeAgo}} pirms", + "justNow": "Nupat", + "never": "Nekad" + }, + "unit": { + "speed": { + "mph": "mp/h", + "kph": "km/h" + }, + "length": { + "feet": "Pēda", + "meters": "metri" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/stundā", + "mbph": "MB/stundā", + "gbph": "GB/stundā" + } + }, + "label": { + "back": "Atgriezties", + "hide": "Paslēpt {{item}}", + "show": "Rādīt {{item}}", + "ID": "ID", + "none": "Nav", + "all": "Viss", + "other": "Cits" + }, + "list": { + "two": "{{0}} un {{1}}", + "many": "{{items}}, un {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Pēc izvēles", + "internalID": "Iekšējais frigates identifikators, ko izmanto konfigurācijā un datubāzē" + }, + "button": { + "apply": "Apstiprināt", + "reset": "Atiestatīt", + "done": "Gatavs", + "enabled": "Ieslēgts", + "enable": "Ieslēgt", + "disabled": "Izslēgts", + "disable": "Izslēgt", + "save": "Saglabāt", + "saving": "Saglabā…", + "cancel": "Atcelt", + "close": "Aizvērt", + "copy": "Kopēt", + "back": "Atpakaļ", + "history": "Vēsture", + "fullscreen": "Pilnekrāna režīms", + "exitFullscreen": "Iziet no pilnekrāna režīma", + "pictureInPicture": "Attēls attēlā", + "twoWayTalk": "Divvirzienu saruna", + "cameraAudio": "Kameras Audio", + "on": "Iesl", + "off": "Izsl", + "edit": "Labot", + "copyCoordinates": "Kopēt koordinātas", + "delete": "Dzēst", + "yes": "Jā", + "no": "Nē", + "download": "Lejupielādēt", + "info": "Informācija", + "suspended": "Apturēts", + "unsuspended": "Atjaunot", + "play": "Atskaņot", + "unselect": "Noņemt izvēlēto", + "export": "Eksportēt", + "deleteNow": "Izdzēst tagad", + "next": "Nākamais", + "continue": "Turpināt", + "add": "Pievienot", + "applying": "Apstiprina…", + "copiedToClipboard": "Nokopēts atmiņā", + "undo": "Atsaukt", + "undoAll": "Atsaukt visu", + "saveAll": "Saglabāt visu", + "retry": "Mēģināt vēlreiz", + "savingAll": "Saglabā visu…" + }, + "menu": { + "system": "Sistēma", + "systemMetrics": "Sistēmas rādītāji", + "configuration": "Konfigurācija", + "systemLogs": "Sistēmas žurnāli", + "settings": "Iestatījumi", + "configurationEditor": "Konfigurācijas redaktors", + "languages": "Valodas", + "language": { + "en": "English (angļu)", + "es": "Español (spāņu)", + "zhCN": "简体中文 (Vienkāršotā ķīniešu)", + "hi": "हिन्दी (hindi)", + "fr": "Français (Franču)", + "ar": "العربية (Arābu)", + "pt": "Português (Portugāļu)", + "ptBR": "Português brasileiro (Brazīlijas portugāļu)", + "ru": "Русский (Krievu)", + "de": "Deutsch (Vācu)", + "ja": "日本語 (Japāņu)", + "tr": "Türkçe (Turku)", + "it": "Italiano (Itāļu)", + "nl": "Nederlands (Nīderlandiešu)", + "sv": "Svenska (Zviedru)", + "cs": "Čeština (Čehu)", + "nb": "Norsk Bokmål (Norvēģu Bokmål)", + "ko": "한국어 (Korejiešu)", + "vi": "Tiếng Việt (Vjetnamiešu)", + "fa": "فارسی (Persiešu)", + "pl": "Polski (Poļu)", + "uk": "Українська (Ukraiņu)", + "he": "עברית (Ebreju)", + "el": "Ελληνικά (Grieķu)", + "ro": "Română (Rumāņu)", + "hu": "Magyar (Ungāru)", + "fi": "Suomi (Somu)", + "da": "Dansk (Dāņu)", + "sk": "Slovenčina (Slovāku)", + "yue": "粵語 (Kantonas)", + "th": "ไทย (Taju)", + "ca": "Català (Katalāņu)", + "sr": "Српски (Serbu)", + "sl": "Slovenščina (Slovēņu)", + "lt": "Lietuvių (Lietuviešu)", + "bg": "Български (Bulgāru)", + "gl": "Galego (Galisiešu)", + "id": "Bahasa Indonesia (Indonēziešu)", + "ur": "اردو (urdu)", + "withSystem": { + "label": "Izmantojiet sistēmas iestatījumus valodai" + }, + "zhHant": "繁體中文 (Tradicionālā ķīniešu)" + }, + "appearance": "Izskats", + "darkMode": { + "label": "Tumšais režīms", + "light": "Gaišs", + "dark": "Tumšs", + "withSystem": { + "label": "Izmantojiet sistēmas iestatījumus gaišajam vai tumšajam režīmam" + } + }, + "withSystem": "Sistēma", + "theme": { + "label": "Tēma", + "blue": "Zila", + "green": "Zaļa", + "nord": "Ziemeļu", + "red": "Sarkana", + "highcontrast": "Augsta kontrasta", + "default": "Noklusējuma" + }, + "help": "Palīdzība", + "documentation": { + "title": "Dokumentācija", + "label": "Frigates dokumentācija" + }, + "restart": "Restartēt Frigate", + "live": { + "title": "Tiešraide", + "allCameras": "Visas kameras", + "cameras": { + "title": "Kameras", + "count_zero": "{{count}}kameras", + "count_one": "{{count}}kamera", + "count_other": "{{count}}kameras" + } + }, + "review": "Pārskats", + "explore": "Meklēt notikumus", + "export": "Eksportēt", + "uiPlayground": "Interfeisa testēšanas vide", + "faceLibrary": "Seju bibliotēka", + "classification": "Atpazīšana", + "user": { + "title": "Lietotājs", + "account": "Konts", + "current": "Pašreizējais lietotājs: {{user}}", + "anonymous": "anonīms", + "logout": "Iziet", + "setPassword": "Izveidot paroli" + }, + "profiles": "Profili" + }, + "toast": { + "copyUrlToClipboard": "Adrese nokopēta.", + "save": { + "title": "Saglabāt", + "error": { + "title": "Neizdevās saglabāt konfigurācijas izmaiņas: {{errorMessage}}", + "noMessage": "Neizdevās saglabāt konfigurācijas izmaiņas" + } + } + }, + "role": { + "title": "Loma", + "admin": "Administrators", + "viewer": "Skatītājs", + "desc": "Administratoriem ir pilna piekļuve visām Frigate funkcijām. Skatītāji var skatīt tikai kameras, skatāmos elementus un arhivētos ierakstus." + }, + "pagination": { + "label": "lappuse", + "previous": { + "title": "Iepriekšējais", + "label": "Pāriet uz iepriekšējo lapu" + }, + "next": { + "title": "Nākamā", + "label": "Dodieties uz nākamo lapu" + }, + "more": "Vairāk lapas" + }, + "accessDenied": { + "documentTitle": "Piekļuve liegta - Frigate", + "title": "Piekļuve liegta", + "desc": "Jums nav atļaujas skatīt šo lapu." + }, + "notFound": { + "documentTitle": "Nav atrasts - Frigate", + "title": "404", + "desc": "Lapa nav atrasta" + }, + "selectItem": "Izvēlēties {{item}}", + "readTheDocumentation": "Laslīt dokumentāciju", + "information": { + "pixels": "{{area}}px" + } +} diff --git a/web/public/locales/lv/components/auth.json b/web/public/locales/lv/components/auth.json new file mode 100644 index 0000000..b8b99fa --- /dev/null +++ b/web/public/locales/lv/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Lietotājvārds", + "password": "Parole", + "login": "Pieteikties", + "firstTimeLogin": "Vai mēģināt pieteikties pirmo reizi? Jūsu akreditācijas dati ir norādīti Frigate žurnālos.", + "errors": { + "usernameRequired": "Nepieciešams lietotājvārds", + "passwordRequired": "Nepieciešams ievadīt paroli", + "rateLimit": "Pārsniegts mēģinājumu skaits. Lūdzu, mēģiniet vēlreiz vēlāk.", + "loginFailed": "Pieteikšanās neizdevās", + "unknownError": "Nezināma kļūda. Pārbaudiet žurnālu.", + "webUnknownError": "Nezināma kļūda. Pārbaudiet konsoles žurnālus." + } + } +} diff --git a/web/public/locales/lv/components/camera.json b/web/public/locales/lv/components/camera.json new file mode 100644 index 0000000..4c1e5f1 --- /dev/null +++ b/web/public/locales/lv/components/camera.json @@ -0,0 +1,90 @@ +{ + "group": { + "label": "Kameru grupas", + "add": "Pievienot kameru grupu", + "edit": "Rediģēt kameru grupu", + "delete": { + "label": "Dzēst kameru grupu", + "confirm": { + "title": "Apstiprināt dzēšanu", + "desc": "Vai esi pārliecināts, ka vēlies izdzēst kameru grupu {{name}}?" + } + }, + "name": { + "label": "Nosaukums", + "placeholder": "Ievadiet vārdu…", + "errorMessage": { + "mustLeastCharacters": "Kameru grupas nosaukumam jāsastāv no vismaz 2 rakstzīmēm.", + "exists": "Kameru grupas nosaukums jau pastāv.", + "nameMustNotPeriod": "Kameru grupas nosaukumā nedrīkst būt punkts.", + "invalid": "Nederīgs kameru grupas nosaukums." + } + }, + "cameras": { + "label": "Kameras", + "desc": "Atlasiet kameras šai grupai." + }, + "icon": "Ikona", + "success": "Kameru grupa ({{name}}) ir saglabāta.", + "camera": { + "birdseye": "Birds-eye", + "setting": { + "label": "Kameras straumēšanas iestatījumi", + "title": "Straumēšanas iestatījumi{{cameraName}}", + "desc": "Mainiet šīs kameru grupas paneļa tiešraides straumes iestatījumus. Šie iestatījumi ir atkarīgi no ierīces/pārlūkprogrammas.", + "audioIsAvailable": "Šai straumei ir pieejams audio", + "audioIsUnavailable": "Šai straumei nav pieejams audio", + "audio": { + "tips": { + "title": "Šai straumei audio ir jāizvada no kameras un tas ir jākonfigurē go2rtc." + } + }, + "stream": "Straume", + "placeholder": "Izvēlieties straumi", + "streamMethod": { + "label": "Straumēšanas metode", + "placeholder": "Izvēlieties straumēšanas metodi", + "method": { + "noStreaming": { + "label": "Nav straumēšanas", + "desc": "Kameru attēli tiks atjaunināti tikai reizi minūtē, bez tiešraides straumēšanas." + }, + "smartStreaming": { + "label": "Viedā straumēšana (ieteicams)", + "desc": "Viedā straumēšana atjauninās kameras attēlu reizi minūtē, ja netiks konstatēta aktivitāte, lai taupītu joslas platumu un resursus. Kad tiks konstatēta aktivitāte, attēls nemanāmi pārslēdzas uz tiešraides straumēšanu." + }, + "continuousStreaming": { + "label": "Nepārtraukta straumēšana", + "desc": { + "title": "Kameras attēls vienmēr būs tiešraidē, kad tas būs redzams informācijas panelī, pat ja netiks konstatēta nekāda aktivitāte.", + "warning": "Nepārtraukta straumēšana var izraisīt lielu joslas platuma izmantošanu un veiktspējas problēmas. Izmantojiet to piesardzīgi." + } + } + } + }, + "compatibilityMode": { + "label": "Saderības režīms", + "desc": "Iespējojiet šo opciju tikai tad, ja kameras tiešraides straumē tiek rādīti krāsu artefakti un attēla labajā pusē ir diagonāla līnija." + } + } + }, + "showAll": "Parādīt visas kameru grupas", + "showLess": "Rādīt mazāk", + "editGroups": "Rediģēt kameru grupas" + }, + "debug": { + "options": { + "label": "Iestatījumi", + "title": "Iespējas", + "showOptions": "Rādīt Iespējas", + "hideOptions": "Paslēpt Iespējas" + }, + "boundingBox": "Ierobežojošs rāmis", + "timestamp": "Laika zīmogs", + "zones": "Zonas", + "mask": "Maska", + "motion": "Kustība", + "regions": "Reģioni", + "paths": "Ceļi" + } +} diff --git a/web/public/locales/lv/components/dialog.json b/web/public/locales/lv/components/dialog.json new file mode 100644 index 0000000..3f52bc6 --- /dev/null +++ b/web/public/locales/lv/components/dialog.json @@ -0,0 +1,123 @@ +{ + "restart": { + "title": "Vai esi pārliecināts, ka vēlies pārstartēt Frigati?", + "button": "Pārstartēt", + "restarting": { + "title": "Frigate tiek pārstartēta", + "content": "Šī lapa tiks atkārtoti ielādēta pēc {{countdown}} sekundēm.", + "button": "Atjaunot tagad" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Sūtīt uz Frigate+", + "desc": "Objekti vietās, no kurām vēlaties izvairīties, nav kļūdaini pozitīvi. To iesniegšana kā kļūdaini pozitīvi radīs modelim neskaidrības." + }, + "review": { + "question": { + "label": "Apstipriniet šo tagu pakalpojumam Frigate Plus", + "ask_a": "Vai šis objekts{{label}}?", + "ask_an": "Vai šis objekts ir{{label}}?", + "ask_full": "Vai šis objekts{{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Iesniegts" + } + } + }, + "video": { + "viewInHistory": "Skatīt vēsturē" + } + }, + "export": { + "time": { + "fromTimeline": "Izvēlieties no laika skalas", + "lastHour_zero": "Pēdējās stundas", + "lastHour_one": "Pēdējās{{count}}stundas", + "lastHour_other": "Pēdējās {{count}} stundas", + "custom": "Pielāgots", + "start": { + "title": "Sākuma laiks", + "label": "Izvēlieties Sākuma laiks" + }, + "end": { + "title": "Beigu laiks", + "label": "Atlasiet Beigu laiks" + } + }, + "name": { + "placeholder": "Ievadiet eksporta nosaukumu" + }, + "select": "Izvēlieties", + "export": "Eksportēt", + "selectOrExport": "Atlasīt vai Eksportēt", + "toast": { + "success": "Eksportēšana veiksmīgi sākta. Skatiet failu eksportēšanas lapā.", + "view": "Skatīt", + "error": { + "failed": "Neizdevās sākt eksportēšanu: {{error}}", + "endTimeMustAfterStartTime": "Beigu laikam ir jābūt pēc sākuma laika", + "noVaildTimeSelected": "Nav izvēlēts derīgs laika diapazons" + } + }, + "fromTimeline": { + "saveExport": "Saglabāt Eksportu", + "previewExport": "Priekšskatīt Eksportu" + } + }, + "streaming": { + "label": "Straume", + "restreaming": { + "disabled": "Šai kamerai nav iespējota atkārtota straumēšana.", + "desc": { + "title": "Konfigurējiet go2rtc, lai šai kamerai varētu piekļūt papildu tiešraides skatīšanās un audio opcijām." + } + }, + "showStats": { + "label": "Rādīt straumes statistiku", + "desc": "Iespējojiet šo opciju, lai straumes statistika tiktu rādīta kā pārklājums kameras attēlam." + }, + "debugView": "Atkļūdošanas režīms" + }, + "search": { + "saveSearch": { + "label": "Saglabāt meklēšanu", + "desc": "Norādiet šīs saglabātās meklēšanas nosaukumu.", + "placeholder": "Ievadiet meklēšanas nosaukumu", + "overwrite": "{{searchName}} jau pastāv. Saglabājot, esošā vērtība tiks pārrakstīta.", + "success": "Meklēšanas ({{searchName}}) ir saglabāts.", + "button": { + "save": { + "label": "Saglabāt šo meklēšanu" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "Apstipriniet dzēšanu", + "desc": { + "selected": "Vai tiešām vēlaties dzēst visus ierakstītos video, kas saistīti ar šo pārskata vienumu?

    Turiet nospiestu taustiņu Shift, lai turpmāk apietu šo dialoglodziņu." + }, + "toast": { + "success": "Ar atlasītajiem pārskata vienumiem saistītais videoieraksts ir veiksmīgi izdzēsts.", + "error": "Neizdevās dzēst: {{error}}" + } + }, + "button": { + "export": "Eksportēt", + "markAsReviewed": "Atzīmēt kā skatītu", + "markAsUnreviewed": "Atzīmēt kā neskatītu", + "deleteNow": "Dzēst tūlīt" + } + }, + "imagePicker": { + "selectImage": "Izsekojamā objekta sīktēla atlasīšana", + "unknownLabel": "Saglabāts sprūda attēls", + "search": { + "placeholder": "Meklēt pēc etiķetes vai apakšetiķetes..." + }, + "noImages": "Šai kamerai nav atrasti sīktēli" + } +} diff --git a/web/public/locales/lv/components/filter.json b/web/public/locales/lv/components/filter.json new file mode 100644 index 0000000..2929468 --- /dev/null +++ b/web/public/locales/lv/components/filter.json @@ -0,0 +1,52 @@ +{ + "filter": "Filtrs", + "classes": { + "label": "Klases", + "all": { + "title": "Visas klases" + }, + "count_one": "{{count}} klase", + "count_other": "{{count}} klases" + }, + "labels": { + "label": "Atzīmes", + "all": { + "title": "Visas atzīmes", + "short": "Atzīmes" + }, + "count_one": "{{count}} atzīme", + "count_other": "{{count}} Atzīmes" + }, + "zones": { + "label": "Zonas", + "all": { + "title": "Visas zonas", + "short": "Zonas" + } + }, + "dates": { + "selectPreset": "Periods…", + "all": { + "title": "Visi datumi", + "short": "Datumi" + } + }, + "more": "Vairāk filtru", + "reset": { + "label": "Atiestatīt filtrus uz noklusējuma vērtībām" + }, + "timeRange": "Laika diapazons", + "subLabels": { + "label": "Papildus atzīmes", + "all": "Visas papildus atzīmes" + }, + "attributes": { + "label": "Klasifikācijas atribūti", + "all": "Visi atribūti" + }, + "score": "Vērtējums", + "estimatedSpeed": "Paredzamais ātrums ({{unit}})", + "features": { + "label": "Funkcijas" + } +} diff --git a/web/public/locales/lv/components/icons.json b/web/public/locales/lv/components/icons.json new file mode 100644 index 0000000..180b0b0 --- /dev/null +++ b/web/public/locales/lv/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Izvēlēties ikonu", + "search": { + "placeholder": "Meklēt ikonu…" + } + } +} diff --git a/web/public/locales/lv/components/input.json b/web/public/locales/lv/components/input.json new file mode 100644 index 0000000..f4af8fe --- /dev/null +++ b/web/public/locales/lv/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Lejuplādēt video", + "toast": { + "success": "Video augšupielāde ir sākusies." + } + } + } +} diff --git a/web/public/locales/lv/components/player.json b/web/public/locales/lv/components/player.json new file mode 100644 index 0000000..c5c04ec --- /dev/null +++ b/web/public/locales/lv/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "Šim laika brīdim nav atrasti ieraksti", + "noPreviewFound": "Priekšskatījums nav atrasts", + "noPreviewFoundFor": "{{cameraName}} nav atrasts priekšskatījums", + "submitFrigatePlus": { + "title": "Nosūtīt šo kadru uz Frigate+?", + "submit": "Iesniegt", + "previewError": "Nevar ielādēt atēla priekškatījumu. Ieraksts var nebūt pieejams, šōbrīd." + }, + "livePlayerRequiredIOSVersion": "Šāda veida straumēšanai nepieciešama iOS 17.1 vai jaunāka versija.", + "streamOffline": { + "title": "Bezsaistes straume", + "desc": "No kameras {{cameraName}} detect straumes netika saņemti kadri, pārbaudiet kļūdu žurnālus" + }, + "cameraDisabled": "Kamera ir izslēgta", + "stats": { + "streamType": { + "title": "Straumes veids:", + "short": "Tips" + }, + "bandwidth": { + "title": "Joslas platums:", + "short": "Joslas platums" + }, + "latency": { + "title": "Latentums:", + "value": "{{seconds}} sekundes", + "short": { + "title": "Latentums", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Kopējais kadru skaits:", + "droppedFrames": { + "title": "Izlaisti kadri:", + "short": { + "title": "Izlaisti", + "value": "{{droppedFrames}} kadri" + } + }, + "decodedFrames": "Dekodētie kadri:", + "droppedFrameRate": "Kadru nomaiņas ātruma kritums:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Kadrs veiksmīgi iesniegts pakalpojumam Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Neizdevās iesniegt kadru Frigate+" + } + }, + "cameraOff": "Kamera ir izslēgta" +} diff --git a/web/public/locales/lv/config/cameras.json b/web/public/locales/lv/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lv/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lv/config/global.json b/web/public/locales/lv/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lv/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lv/config/groups.json b/web/public/locales/lv/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lv/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lv/config/validation.json b/web/public/locales/lv/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lv/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lv/objects.json b/web/public/locales/lv/objects.json new file mode 100644 index 0000000..cf1edce --- /dev/null +++ b/web/public/locales/lv/objects.json @@ -0,0 +1,30 @@ +{ + "person": "Persona", + "bicycle": "Velosipēds", + "car": "Automašīna", + "motorcycle": "Motocikls", + "airplane": "Lidmašīna", + "bus": "Autobuss", + "train": "Vilciens", + "package": "Paciņa", + "bbq_grill": "Grils", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "postnl": "PostNL", + "dpd": "DPD", + "boat": "Laiva", + "dog": "Suns", + "bark": "Riešana", + "cat": "Kaķis", + "horse": "Zirgs", + "mouse": "Pele", + "animal": "Dzīvnieks", + "door": "Durvis", + "sink": "Izlietne", + "hair_dryer": "Fēns", + "toothbrush": "Zobu birste", + "clock": "Pulkstens" +} diff --git a/web/public/locales/lv/views/chat.json b/web/public/locales/lv/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lv/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lv/views/classificationModel.json b/web/public/locales/lv/views/classificationModel.json new file mode 100644 index 0000000..5d26d5b --- /dev/null +++ b/web/public/locales/lv/views/classificationModel.json @@ -0,0 +1,36 @@ +{ + "documentTitle": "Klassifikācijas modeļi", + "details": { + "scoreInfo": "Rezultāts atbilst vidējai klasifikācijas ticamībai no visām objekta detektēšanas reizēm.", + "none": "Nav", + "unknown": "Nezināms" + }, + "description": { + "invalidName": "Nederīgs nosaukums. Nosaukumi drīkst saturēt tikai burtus, ciparus, atstarpes, apostrofus, pasvītras un defises." + }, + "button": { + "deleteClassificationAttempts": "Dzēst klasifikācijas attēlus", + "renameCategory": "Pārdēvēt klasi", + "deleteCategory": "Dzēst klasi", + "deleteImages": "Dzēst attēlus" + }, + "wizard": { + "step3": { + "training": { + "title": "Trenē modeli", + "description": "Tavs modelis tiek trenēts. Aizver šo paziņojumu, un tavs modelis tiks izmantots, tiklīdz trenēšana ir pabeigta." + }, + "retryGenerate": "Atkārtot ģenerēšanu", + "classifying": "Klasificē un trenē...", + "trainingStarted": "Trenēšana veiksmīgi uzsākta", + "errors": { + "generateFailed": "Neizdevās ģenerēt piemērus: {{error}}", + "generationFailed": "Ģenerēšana neizdevās. Mēģini vēlreiz.", + "classifyFailed": "Neizdevās klasificēt attēlus: {{error}}" + } + } + }, + "train": { + "titleShort": "Pēdējās" + } +} diff --git a/web/public/locales/lv/views/configEditor.json b/web/public/locales/lv/views/configEditor.json new file mode 100644 index 0000000..286da8e --- /dev/null +++ b/web/public/locales/lv/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Konfigurācijas rediģēšana - Frigate", + "configEditor": "Konfigurācijas redaktors", + "safeConfigEditor": "Konfigurācijas redaktors (drošais režīms)", + "safeModeDescription": "Frigate ir drošajā režīmā konfigurācijas pārbaudes kļūdas dēļ.", + "copyConfig": "Kopēt konfigurāciju", + "saveAndRestart": "Saglabāt un pārstartēt", + "saveOnly": "Tikai saglabāt", + "confirm": "Vai iziet, nesaglabājot?", + "toast": { + "success": { + "copyToClipboard": "Konfigurācija ir kopēta starpliktuvē." + }, + "error": { + "savingError": "Saglabājot konfigurāciju, radās kļūda" + } + } +} diff --git a/web/public/locales/lv/views/events.json b/web/public/locales/lv/views/events.json new file mode 100644 index 0000000..77d4d34 --- /dev/null +++ b/web/public/locales/lv/views/events.json @@ -0,0 +1,61 @@ +{ + "alerts": "Paziņojumi", + "detections": "Atklājumi", + "motion": { + "label": "Kustība", + "only": "Tikai kustība" + }, + "allCameras": "Visas kameras", + "empty": { + "alert": "Nav paziņojumu, kurus pārskatīt", + "detection": "Nav apskatāmu konstatējumu", + "motion": "Nav atrasti kustības dati" + }, + "timeline": "Laika skala", + "timeline.aria": "Izvēlieties laika skalu", + "zoomIn": "Pietuvināt", + "zoomOut": "Tālināt", + "events": { + "label": "Notikumi", + "aria": "Izvēlieties notikumus", + "noFoundForTimePeriod": "Šajā laika periodā nav atrasts neviens notikums." + }, + "detail": { + "label": "Detaļas", + "noDataFound": "Nav detalizētu datu pārskatīšanai", + "aria": "Pārslēgt detalizēto skatu", + "trackedObject_one": "{{count}} objekts", + "trackedObject_other": "{{count}} objekti", + "noObjectDetailData": "Nav pieejami objekta detalizēti dati.", + "settings": "Detaļas skata iestatījumi", + "alwaysExpandActive": { + "title": "Vienmēr izvērst aktīvs", + "desc": "Vienmēr izvērsiet aktīvā pārskata vienuma objekta informāciju, ja tāda ir pieejama." + } + }, + "objectTrack": { + "trackedPoint": "Izsekots punkts", + "clickToSeek": "Noklikšķiniet, lai pārietu uz šo laiku" + }, + "documentTitle": "Pārskats - Frigate", + "recordings": { + "documentTitle": "Ieraksti - Frigate" + }, + "calendarFilter": { + "last24Hours": "Pēdējās 24 stundas" + }, + "markAsReviewed": "Atzīmēt kā pārskatītu", + "markTheseItemsAsReviewed": "Atzīmēt šos vienumus kā pārskatītus", + "newReviewItems": { + "label": "Skatīt jaunus atsauksmju vienumus", + "button": "Jauni vienumi, kas jāpārskata" + }, + "selected_one": "atlasīts {{count}}", + "selected_other": "atlasīts {{count}}", + "select_all": "Viss", + "camera": "Kamera", + "detected": "atklāts", + "normalActivity": "Normāls", + "needsReview": "Nepieciešama pārskatīšana", + "securityConcern": "Drošības jautājums" +} diff --git a/web/public/locales/lv/views/explore.json b/web/public/locales/lv/views/explore.json new file mode 100644 index 0000000..90df025 --- /dev/null +++ b/web/public/locales/lv/views/explore.json @@ -0,0 +1,50 @@ +{ + "documentTitle": "Notikumu meklēšana - Frigate", + "generativeAI": "Ģeneratīvs AI", + "exploreMore": "Paskatīt vairāk objektu{{label}}", + "details": { + "timestamp": "Laika zīmogs" + }, + "exploreIsUnavailable": { + "title": "Notikumu meklēšana nav pieejama", + "embeddingsReindexing": { + "context": "Meklēšana būs pieejama pēc tam, kad būs pabeigta izsekoto objektu atkārtota indeksēšana.", + "startingUp": "Notiek palaišana…", + "estimatedTime": "Paredzamais atlikušais laiks:", + "finishingShortly": "Drīz pabeigs" + } + }, + "itemMenu": { + "findSimilar": { + "label": "Atrast līdzīgus", + "aria": "Atrast līdzīgus izsekotos priekšmetus" + }, + "submitToPlus": { + "label": "Iesniegt Frigate+", + "aria": "Iesniegt Frigate Plus" + }, + "viewInHistory": { + "label": "Atrast vēsturē" + }, + "deleteTrackedObject": { + "label": "Dzēst šo izsekoto priekšmetu" + } + }, + "dialog": { + "confirmDelete": { + "title": "Apstiprināt dzēšanu" + } + }, + "searchResult": { + "nextTrackedObject": "Nākamais izsekotais objekts", + "deleteTrackedObject": { + "toast": { + "success": "Izsekotais objekts veiksmīgi izdzēsts.", + "error": "Neizdevās izdzēst izsekoto objektu: {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "MI analīze" + } +} diff --git a/web/public/locales/lv/views/exports.json b/web/public/locales/lv/views/exports.json new file mode 100644 index 0000000..73209ce --- /dev/null +++ b/web/public/locales/lv/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Eksportēt - Frigate", + "search": "Meklēt", + "noExports": "Eksporti nav atrasti", + "deleteExport": "Dzēst eksportu", + "deleteExport.desc": "Vai esi pārliecināts, ka vēlies izdzēst{{exportName}}?", + "editExport": { + "title": "Pārdēvēt eksportu", + "desc": "Ievadiet jaunu nosaukumu šim eksportam.", + "saveExport": "Saglabāt eksportu" + }, + "tooltip": { + "shareExport": "Kopīgot eksportu", + "downloadVideo": "Lejupielādēt video", + "editName": "Rediģēt nosaukumu", + "deleteExport": "Eksporta dzēšana" + }, + "toast": { + "error": { + "renameExportFailed": "Neizdevās pārdēvēt eksporta failu: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/lv/views/faceLibrary.json b/web/public/locales/lv/views/faceLibrary.json new file mode 100644 index 0000000..be06af6 --- /dev/null +++ b/web/public/locales/lv/views/faceLibrary.json @@ -0,0 +1,94 @@ +{ + "description": { + "addFace": "Pievienojiet savai seju bibliotēkai jaunu kolekciju, augšupielādējot savu pirmo attēlu.", + "placeholder": "Ievadi kolekcijas nosaukumu", + "invalidName": "Nederīgs nosaukums. Nosaukumi drīkst saturēt tikai burtus, ciparus, atstarpes, apostrofus, pasvītras un defises.", + "nameCannotContainHash": "Vārds nedrīkst saturēt #." + }, + "details": { + "timestamp": "Laika zīmogs", + "unknown": "Nezināms", + "scoreInfo": "Rezultāts ir visu seju rezultāta vidējais, svērts pēc sejas izmēra katrā attēlā." + }, + "documentTitle": "Seju bibliotēka - Frigate", + "uploadFaceImage": { + "title": "Augšupielādējiet sejas attēlu", + "desc": "Augšupielādējiet attēlu, lai skenētu sejas un iekļautu to lapā {{pageToggle}}" + }, + "collections": "Kolekcijas", + "createFaceLibrary": { + "new": "Izveidojiet jaunu seju", + "nextSteps": "Lai izveidotu stabilu pamatu:
  • izmantojiet cilni “Nesenās atpazīšanas”, lai atlasītu un apmācītu attēlus katrai atpazītajai personai.
  • Lai iegūtu labākos rezultātus, koncentrējieties uz tiešiem attēliem; izvairieties no attēlu apmācības, kuros sejas ir attēlotas leņķī.
  • " + }, + "steps": { + "faceName": "Ievadiet sejas nosaukumu", + "uploadFace": "Augšupielādējiet sejas attēlu", + "nextSteps": "Nākamie soļi", + "description": { + "uploadFace": "Augšupielādējiet lietotāja {{name}} attēlu, kurā redzama viņa seja no priekšpuses. Attēls nav jāapgriež, lai redzētu tikai viņa seju." + } + }, + "train": { + "title": "Pēdējās atpazīšanas", + "titleShort": "Pēdējās", + "aria": "Atlasiet pēdējās atpazīšanas", + "empty": "Nav pēdējo sejas atpazīšanas mēģinājumu" + }, + "deleteFaceLibrary": { + "title": "Dzēst Vārdu", + "desc": "Vai tiešām vēlaties dzēst kolekciju {{name}}? Tas neatgriezeniski dzēsīs visas saistītās sejas." + }, + "deleteFaceAttempts": { + "title": "Dzēst sejas", + "desc_zero": "Vai tiešām vēlaties dzēst {{count}} sejas? Šo darbību nevar atsaukt.", + "desc_one": "Vai tiešām vēlaties dzēst {{count}} seju? Šo darbību nevar atsaukt.", + "desc_other": "Vai tiešām vēlaties dzēst {{count}} sejas? Šo darbību nevar atsaukt." + }, + "renameFace": { + "title": "Pārdēvēt seju", + "desc": "Ievadiet jaunu vārdu priekš {{name}}" + }, + "button": { + "deleteFaceAttempts": "Dzēst sejas", + "addFace": "Pievienot seju", + "renameFace": "Pārdēvēt seju", + "deleteFace": "Dzēst seju", + "uploadImage": "Augšupielādēt attēlu", + "reprocessFace": "Atkārtoti apstrādāt seju" + }, + "imageEntry": { + "validation": { + "selectImage": "Lūdzu, atlasiet attēla failu." + }, + "dropActive": "Ievelciet attēlu šeit…", + "dropInstructions": "Velciet un nometiet vai ielīmējiet attēlu šeit vai noklikšķiniet, lai atlasītu", + "maxSize": "Max izmērs: {{size}} MB" + }, + "nofaces": "Nav pieejama neviena seja", + "trainFaceAs": "Atcerieties seju kā:", + "trainFace": "Atcerieties seju", + "toast": { + "success": { + "uploadedImage": "Attēls veiksmīgi augšupielādēts.", + "addFaceLibrary": "{{name}} ir veiksmīgi pievienots seju bibliotēkai!", + "deletedFace_zero": "Veiksmīgi izdzēstas {{count}} sejas.", + "deletedFace_one": "Veiksmīgi izdzēsta {{count}} seja.", + "deletedFace_other": "Veiksmīgi izdzēstas {{count}} sejas.", + "deletedName_zero": "{{count}} sejas ir veiksmīgi izdzēstas.", + "deletedName_one": "{{count}} seja ir veiksmīgi izdzēsta.", + "deletedName_other": "{{count}} sejas ir veiksmīgi izdzēstas.", + "renamedFace": "Seja veiksmīgi pārdēvēta par {{name}}", + "trainedFace": "Seja ir veiksmīgi iegaumēta.", + "updatedFaceScore": "Sejas vērtējums ir veiksmīgi atjaunināts uz {{name}} ({{score}})." + }, + "error": { + "uploadingImageFailed": "Neizdevās augšupielādēt attēlu: {{errorMessage}}", + "addFaceLibraryFailed": "Neizdevās iestatīt sejas vārdu: {{errorMessage}}", + "deleteFaceFailed": "Neizdevās dzēst: {{errorMessage}}", + "deleteNameFailed": "Neizdevās izdzēst vārdu: {{errorMessage}}", + "renameFaceFailed": "Neizdevās pārdēvēt seju: {{errorMessage}}", + "trainFailed": "Neizdevās atcerēties: {{errorMessage}}", + "updateFaceScoreFailed": "Neizdevās atjaunināt sejas vērtējumu: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/lv/views/live.json b/web/public/locales/lv/views/live.json new file mode 100644 index 0000000..b0f6887 --- /dev/null +++ b/web/public/locales/lv/views/live.json @@ -0,0 +1,13 @@ +{ + "documentTitle": "Tiešraide - Frigate", + "documentTitle.withCamera": "{{camera}} - Tiešraide - Frigate", + "lowBandwidthMode": "Eko režīms", + "twoWayTalk": { + "enable": "Iespējot divvirzienu saziņu", + "disable": "Izslēgt divvirzienu sarunu" + }, + "cameraAudio": { + "enable": "Iespējot kameras audio", + "disable": "Izslēgt kameras audio" + } +} diff --git a/web/public/locales/lv/views/motionSearch.json b/web/public/locales/lv/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lv/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lv/views/recording.json b/web/public/locales/lv/views/recording.json new file mode 100644 index 0000000..f2fb166 --- /dev/null +++ b/web/public/locales/lv/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Eksportēt", + "filter": "Filtrs", + "calendar": "Kalendārs", + "filters": "Filtri", + "toast": { + "error": { + "noValidTimeSelected": "Atlasīts nederīgs laika diapazons", + "endTimeMustAfterStartTime": "Beigu laikam jābūt pēc sākuma laika" + } + } +} diff --git a/web/public/locales/lv/views/replay.json b/web/public/locales/lv/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/lv/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/lv/views/search.json b/web/public/locales/lv/views/search.json new file mode 100644 index 0000000..b8bb465 --- /dev/null +++ b/web/public/locales/lv/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Meklēt", + "savedSearches": "Saglabātie meklējumi", + "searchFor": "Meklēt {{inputValue}}", + "button": { + "clear": "Notīrīt meklēšanu", + "save": "Saglabāt meklēto", + "delete": "Izdzēst saglabāto meklējumu", + "filterInformation": "Filtra informācija", + "filterActive": "Filtri aktīvi" + }, + "trackedObjectId": "Izsekojamā objekta ID", + "filter": { + "label": { + "cameras": "Kameras", + "labels": "Etiķetes", + "zones": "Zonas", + "sub_labels": "Papildu etiķetes", + "attributes": "Atribūti", + "search_type": "Meklēšanas veids", + "time_range": "Laika diapazons", + "before": "Pirms", + "after": "Pēc", + "min_score": "Minimālais rezultāts", + "max_score": "Maksimālais rezultāts", + "min_speed": "Minimālais ātrums", + "max_speed": "Maksimālais ātrums", + "recognized_license_plate": "Atpazīta numura zīme", + "has_clip": "Ir klips", + "has_snapshot": "Ir momentuzņēmums" + }, + "searchType": { + "thumbnail": "Sīktēls", + "description": "Apraksts" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "'Pirms' datumam jābūt vēlākam par 'pēc' datumu.", + "afterDatebeEarlierBefore": "Datumam 'pēc' ir jābūt agrākam par datumu 'pirms'.", + "minScoreMustBeLessOrEqualMaxScore": "'Min_score' vērtībai ir jābūt mazākai vai vienādai ar 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Vērtībai 'max_score' ir jābūt lielākai vai vienādai ar 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'Min_speed' ir jābūt mazākam vai vienādam ar 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Vērtībai 'max_speed' ir jābūt lielākai vai vienādai ar 'min_speed'." + } + }, + "tips": { + "title": "Kā lietot teksta filtrus", + "desc": { + "text": "Filtri palīdz sašaurināt meklēšanas rezultātus. Lūk, kā tos izmantot ievades laukā:", + "step1": "Ierakstiet filtra atslēgas nosaukumu, kam seko kols (piemēram, \"kameras:\").", + "step2": "Atlasiet vērtību no ieteikumiem vai ierakstiet savu.", + "step3": "Izmantojiet vairākus filtrus, pievienojot tos vienu pēc otra ar atstarpi starp tiem.", + "step4": "Datuma filtri (pirms: un pēc:) izmanto formātu {{DateFormat}}.", + "step5": "Laika diapazona filtrs izmanto formātu {{exampleTime}}.", + "step6": "Noņemiet filtrus, noklikšķinot uz 'x' blakus tiem.", + "exampleLabel": "Piemērs:" + } + }, + "header": { + "currentFilterType": "Filtrēt vērtības", + "noFilters": "Filtri", + "activeFilters": "Aktīvie filtri" + } + }, + "similaritySearch": { + "title": "Līdzības meklēšana", + "active": "Aktīva līdzības meklēšana", + "clear": "Notīrīt līdzības meklēšanu" + }, + "placeholder": { + "search": "Meklēt…" + } +} diff --git a/web/public/locales/lv/views/settings.json b/web/public/locales/lv/views/settings.json new file mode 100644 index 0000000..867d693 --- /dev/null +++ b/web/public/locales/lv/views/settings.json @@ -0,0 +1,183 @@ +{ + "documentTitle": { + "default": "Iestatījumi - Frigate", + "authentication": "Autentifikācijas iestatījumi - Frigate", + "cameraManagement": "Pārvaldīt kameras - Frigate", + "cameraReview": "Kameras skata iestatījumi - Frigate", + "enrichments": "Bagātināšanas iestatījumi - Frigate", + "masksAndZones": "Masku un zonu rediģētājs - Frigate", + "motionTuner": "Kustības noteikšana - Frigate" + }, + "cameraWizard": { + "step1": { + "cameraName": "Kameras nosaukums", + "cameraNamePlaceholder": "piem. ieejas_durvis vai Pagalma kopskats", + "username": "Lietotājvārds", + "password": "Parole", + "cameraBrand": "Kameras ražotājs", + "brandInformation": "Ražotāja informācija", + "onvifPortDescription": "Kamerām, kas atbalsta ONVIF, ports parasti ir 80 vai 8080.", + "errors": { + "nameRequired": "Nepieciešams kameras noaukums", + "nameLength": "Kameras nosaukums nedrīkst būt garāks par 64 simboliem", + "invalidCharacters": "Kameras nosaukumā ir neatļauti simboli", + "nameExists": "Kameras nosaukums jau pastāv" + }, + "onvifPort": "ONVIF Ports", + "port": "Ports" + }, + "title": "Pievienot Kameru", + "testResultLabels": { + "audio": "Audio", + "video": "Video", + "resolution": "Izšķirtspēja", + "fps": "FSP" + }, + "save": { + "failure": "Kļūda saglabājot {{cameraName}}." + }, + "steps": { + "nameAndConnection": "Vārds un savienojums" + }, + "step2": { + "retry": "Atkārtot", + "connected": "Savienots" + }, + "step3": { + "quality": "Kvalitāte", + "resolution": "Izšķirtspēja", + "selectQuality": "Izvēlies kvalitāti", + "roleLabels": { + "audio": "Audio" + }, + "testStream": "Pārbaudīt Savienojumu", + "connected": "Savienots", + "notConnected": "Nav Savienots", + "testFailedTitle": "Tests Neizdevās" + }, + "step4": { + "connectStream": "Savienot", + "connectingStream": "Savienojas", + "failed": "Neizdevās", + "roles": "Lomas", + "error": "Kļūda" + } + }, + "menu": { + "users": "Lietotāji", + "roles": "Lomas", + "frigateplus": "Frigate+", + "notifications": "Paziņojumi", + "triggers": "Trigeri", + "profiles": "Profili" + }, + "cameraSetting": { + "camera": "Kamera" + }, + "dialog": { + "unsavedChanges": { + "title": "Tev ir nesaglabātas izmaiņas.", + "desc": "Vai vēlies saglabāt izmaiņas pirms turpini?" + } + }, + "general": { + "liveDashboard": { + "displayCameraNames": { + "label": "Vienmēr rādīt kameras nosaukumus" + } + }, + "calendar": { + "title": "Kalendārs", + "firstWeekday": { + "label": "Nedēļas pirmā diena", + "sunday": "Svētdiena", + "monday": "Pirmdiena" + } + } + }, + "enrichments": { + "semanticSearch": { + "reindexNow": { + "confirmButton": "Pārindeksēt", + "label": "Pārindeksēt tagad", + "confirmTitle": "Apstiprināt Pārindeksāciju", + "alreadyInProgress": "Pārindeksācija jau notiek." + }, + "modelSize": { + "small": { + "title": "mazs" + }, + "large": { + "title": "liels" + }, + "label": "Modeļa izmērs" + } + }, + "birdClassification": { + "title": "Putnu klasifikācija" + }, + "faceRecognition": { + "title": "Sejas Atpazīšana", + "modelSize": { + "label": "Modeļa izmērs", + "small": { + "title": "mazs" + }, + "large": { + "title": "liels" + } + } + }, + "licensePlateRecognition": { + "title": "Auto numura zīmes atpazīšana" + }, + "toast": { + "error": "Neizdevās saglabāt konfigurācijas izmaiņas: {{errorMessage}}" + } + }, + "cameraManagement": { + "addCamera": "Pievienot Jaunu Kameru", + "selectCamera": "Izvēlēties Kameru", + "cameraConfig": { + "add": "Pievienot Kameru", + "edit": "Labot Kameru", + "name": "Kameras Vārds" + } + }, + "triggers": { + "wizard": { + "steps": { + "nameAndType": "Vārds un Tips" + } + }, + "dialog": { + "form": { + "name": { + "title": "Vārds" + } + } + }, + "table": { + "edit": "Labot", + "name": "Vārds", + "type": "Tips", + "content": "Saturs" + } + }, + "frigatePlus": { + "modelInfo": { + "cameras": "Kameras" + }, + "snapshotConfig": { + "table": { + "camera": "Kamera" + } + } + }, + "notification": { + "title": "Paziņojumi", + "cameras": { + "title": "Kameras" + } + } +} diff --git a/web/public/locales/lv/views/system.json b/web/public/locales/lv/views/system.json new file mode 100644 index 0000000..f6a3161 --- /dev/null +++ b/web/public/locales/lv/views/system.json @@ -0,0 +1,30 @@ +{ + "documentTitle": { + "cameras": "Kameru statistika - Frigate", + "storage": "Uzglabāšanas statistika - Frigate", + "general": "Vispārīgā statistika - Frigate", + "enrichments": "Bagātināšanas statistika - Frigate", + "logs": { + "frigate": "Frigate konfigurācija - Frigate", + "go2rtc": "Logi Go2RTC - Frigate", + "nginx": "Logi Nginx - Frigate" + } + }, + "stats": { + "cameraIsOffline": "{{camera}} ir izslēgta", + "detectIsSlow": "{{detect}} ir lēns ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} ir ļoti lēns ({{speed}} ms)" + }, + "enrichments": { + "infPerSecond": "Inferences sekundē", + "averageInf": "Vidējais inferences ilgums", + "embeddings": { + "face_recognition": "Seju atpazīšana", + "plate_recognition": "Numurzīmju atpazīšana", + "plate_recognition_speed": "Numurzīmju atpazīšanas ātrums", + "object_description": "Objekta apraksts", + "object_description_events_per_second": "Objekta apraksts" + } + }, + "title": "Sistēma" +} diff --git a/web/public/locales/ml/audio.json b/web/public/locales/ml/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/audio.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/common.json b/web/public/locales/ml/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/common.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/components/auth.json b/web/public/locales/ml/components/auth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/components/camera.json b/web/public/locales/ml/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/components/dialog.json b/web/public/locales/ml/components/dialog.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/components/filter.json b/web/public/locales/ml/components/filter.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/components/icons.json b/web/public/locales/ml/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/components/input.json b/web/public/locales/ml/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/components/input.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/components/player.json b/web/public/locales/ml/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/components/player.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/config/cameras.json b/web/public/locales/ml/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/config/global.json b/web/public/locales/ml/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/config/groups.json b/web/public/locales/ml/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/config/validation.json b/web/public/locales/ml/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/objects.json b/web/public/locales/ml/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/objects.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/chat.json b/web/public/locales/ml/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/classificationModel.json b/web/public/locales/ml/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/configEditor.json b/web/public/locales/ml/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/events.json b/web/public/locales/ml/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/events.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/explore.json b/web/public/locales/ml/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/exports.json b/web/public/locales/ml/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/faceLibrary.json b/web/public/locales/ml/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/live.json b/web/public/locales/ml/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/motionSearch.json b/web/public/locales/ml/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/recording.json b/web/public/locales/ml/views/recording.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/replay.json b/web/public/locales/ml/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/search.json b/web/public/locales/ml/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/settings.json b/web/public/locales/ml/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ml/views/system.json b/web/public/locales/ml/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ml/views/system.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/nb-NO/audio.json b/web/public/locales/nb-NO/audio.json new file mode 100644 index 0000000..cdc92c4 --- /dev/null +++ b/web/public/locales/nb-NO/audio.json @@ -0,0 +1,503 @@ +{ + "bird": "Fugl", + "door": "Dør", + "sink": "Vask", + "blender": "Blender", + "bicycle": "Sykkel", + "motorcycle": "Motorsykkel", + "car": "Bil", + "bus": "Buss", + "train": "Tog", + "boat": "Båt", + "cat": "Katt", + "dog": "Hund", + "horse": "Hest", + "sheep": "Sau", + "skateboard": "Skateboard", + "mouse": "Mus", + "keyboard": "Tastatur", + "clock": "Klokke", + "scissors": "Saks", + "hair_dryer": "Hårføner", + "toothbrush": "Tannbørste", + "vehicle": "Kjøretøy", + "animal": "Dyr", + "bark": "Bjeff", + "goat": "Geit", + "groan": "Stønn", + "throat_clearing": "Kremting", + "sneeze": "Nysing", + "applause": "Applaus", + "chatter": "Skravling", + "crowd": "Folkemengde", + "bow_wow": "Voff-voff", + "livestock": "Husdyr", + "clip_clop": "Klipp-klopp", + "cluck": "Kakling", + "cock_a_doodle_doo": "Kykeliky", + "wild_animals": "Ville dyr", + "roaring_cats": "Brølende kattedyr", + "dogs": "Hunder", + "whale_vocalization": "Hval-vokalisering", + "strum": "Strumming", + "gong": "Gong", + "tuning_fork": "Stemmegaffel", + "opera": "Opera", + "waterfall": "Foss", + "motor_vehicle": "Motorvogn", + "emergency_vehicle": "Utrykningskjøretøy", + "police_car": "Politibil", + "rail_transport": "Jernbanetransport", + "fixed-wing_aircraft": "Fly med faste vinger", + "engine": "Motor", + "bathtub": "Badekar", + "toilet_flush": "Toalettskylling", + "coin": "Mynt", + "mechanisms": "Mekanismer", + "field_recording": "Feltinnspilling", + "speech": "Tale", + "babbling": "Babling", + "yell": "Rop", + "bellow": "Brøl", + "whoop": "Jubelrop", + "whispering": "Hvisking", + "laughter": "Latter", + "snicker": "Fnising", + "crying": "Gråt", + "sigh": "Sukk", + "singing": "Sang", + "choir": "Kor", + "yodeling": "Jodling", + "chant": "Sangrop", + "mantra": "Mantra", + "child_singing": "Barnesang", + "synthetic_singing": "Syntetisk sang", + "rapping": "Rapping", + "humming": "Nynning", + "grunt": "Grynt", + "whistling": "Plystring", + "breathing": "Pusting", + "wheeze": "Hvesing", + "snoring": "Snorking", + "gasp": "Gisp", + "pant": "Pesing", + "snort": "Snøfting", + "cough": "Hoste", + "sniff": "Snufs", + "run": "Løping", + "shuffle": "Sleping (av føtter)", + "footsteps": "Fottrinn", + "chewing": "Tygging", + "biting": "Biting", + "gargling": "Gurgling", + "stomach_rumble": "Mageknurr", + "burping": "Raping", + "hiccup": "Hikke", + "fart": "Promp", + "hands": "Hender", + "finger_snapping": "Fingerknipsing", + "clapping": "Applaus", + "heartbeat": "Hjerteslag", + "heart_murmur": "Hjertelyd (unormal)", + "cheering": "Jubel", + "children_playing": "Barn som leker", + "pets": "Kjæledyr", + "yip": "Klynk", + "howl": "Uling", + "growling": "Knurring", + "whimper_dog": "Klynking (hund)", + "purr": "Malelyd", + "meow": "Mjauing", + "hiss": "Fres", + "caterwaul": "Kattemjau", + "neigh": "Vrinsk", + "cattle": "Storfe", + "moo": "Rauting", + "cowbell": "Kubjelle", + "pig": "Gris", + "oink": "Nøff", + "bleat": "Breking", + "fowl": "Fjærfe", + "chicken": "Kylling", + "turkey": "Kalkun", + "gobble": "Kalkunlyd", + "duck": "And", + "quack": "Kvakking", + "goose": "Gås", + "honk": "Gåselyd", + "roar": "Brøl", + "chirp": "Kvitre", + "squawk": "Skvatring", + "pigeon": "Due", + "coo": "Kurre", + "crow": "Kråke", + "caw": "Krah", + "owl": "Ugle", + "hoot": "Uglelyd", + "flapping_wings": "Vingeslag", + "rats": "Rotter", + "patter": "Tripping", + "insect": "Insekt", + "cricket": "Gresshoppe", + "mosquito": "Mygg", + "fly": "Flue", + "buzz": "Summing", + "frog": "Frosk", + "croak": "Kvekking", + "snake": "Slange", + "rattle": "Ranglelyd", + "music": "Musikk", + "musical_instrument": "Musikkinstrument", + "plucked_string_instrument": "Klimpreinstrument", + "guitar": "Gitar", + "electric_guitar": "Elektrisk gitar", + "bass_guitar": "Bassgitar", + "acoustic_guitar": "Akustisk gitar", + "steel_guitar": "Steelgitar", + "tapping": "Tapping", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolin", + "zither": "Siter", + "ukulele": "Ukulele", + "piano": "Piano", + "electric_piano": "Elektrisk piano", + "organ": "Orgel", + "electronic_organ": "Elektronisk orgel", + "hammond_organ": "Hammondorgel", + "synthesizer": "Synthesizer", + "sampler": "Sampler", + "harpsichord": "Cembalo", + "percussion": "Perkusjon", + "drum_kit": "Trommesett", + "drum_machine": "Trommemaskin", + "drum": "Tromme", + "snare_drum": "Skarptromme", + "rimshot": "Slag på trommekanten", + "drum_roll": "Trommevirvel", + "bass_drum": "Basstromme", + "timpani": "Pauker", + "tabla": "Tabla", + "cymbal": "Symbal", + "hi_hat": "Hi-hat", + "wood_block": "Treblokk", + "tambourine": "Tamburin", + "maraca": "Maracas", + "tubular_bells": "Rørklokker", + "mallet_percussion": "Slagverk med køller", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibrafon", + "steelpan": "Steelpan", + "orchestra": "Orkester", + "brass_instrument": "Messingblåseinstrument", + "french_horn": "Valthorn", + "trumpet": "Trompet", + "trombone": "Trombone", + "bowed_string_instrument": "Strykeinstrument", + "string_section": "Strykere", + "violin": "Fiolin", + "pizzicato": "Pizzicato", + "cello": "Cello", + "double_bass": "Kontrabass", + "wind_instrument": "Treblåseinstrument", + "flute": "Fløyte", + "saxophone": "Saksofon", + "clarinet": "Klarinett", + "harp": "Harpe", + "bell": "Klokke", + "church_bell": "Kirkeklokke", + "jingle_bell": "Bjelle", + "bicycle_bell": "Sykkelbjelle", + "chime": "Klokkespill", + "wind_chime": "Vindklokke", + "harmonica": "Munnspill", + "accordion": "Akkordeon", + "bagpipes": "Sekkepipe", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Syngeskål", + "scratching": "Skraping", + "pop_music": "Popmusikk", + "hip_hop_music": "Hip-hop musikk", + "beatboxing": "Beatboxing", + "rock_music": "Rockemusikk", + "heavy_metal": "Heavy metal", + "punk_rock": "Punkrock", + "grunge": "Grunge", + "progressive_rock": "Progressiv rock", + "rock_and_roll": "Rock and roll", + "psychedelic_rock": "Psykedelisk rock", + "rhythm_and_blues": "Rhythm and blues", + "soul_music": "Soulmusikk", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Swingmusikk", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folkemusikk", + "middle_eastern_music": "Midtøsten-musikk", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Klassisk musikk", + "electronic_music": "Elektronisk musikk", + "house_music": "House-musikk", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and bass", + "electronica": "Electronica", + "electronic_dance_music": "Elektronisk dansemusikk", + "trance_music": "Trancemusikk", + "ambient_music": "Ambient musikk", + "music_of_latin_america": "Latinamerikansk musikk", + "salsa_music": "Salsamusikk", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Musikk for barn", + "new-age_music": "New age-musikk", + "vocal_music": "Vokalmusikk", + "a_capella": "A cappella", + "music_of_africa": "Afrikansk musikk", + "afrobeat": "Afrobeat", + "christian_music": "Kristelig musikk", + "gospel_music": "Gospelmusikk", + "music_of_asia": "Asiatisk musikk", + "carnatic_music": "Karnatisk musikk", + "music_of_bollywood": "Bollywood-musikk", + "ska": "Ska", + "traditional_music": "Tradisjonell musikk", + "independent_music": "Indie-musikk", + "song": "Sang", + "background_music": "Bakgrunnsmusikk", + "theme_music": "Temamusikk", + "jingle": "Jingle", + "soundtrack_music": "Filmmusikk", + "lullaby": "Vuggevise", + "video_game_music": "Videospillmusikk", + "christmas_music": "Julemusikk", + "dance_music": "Dansemusikk", + "wedding_music": "Bryllupsmusikk", + "happy_music": "Glad musikk", + "sad_music": "Trist musikk", + "tender_music": "Vakker musikk", + "exciting_music": "Spennende musikk", + "angry_music": "Sint musikk", + "scary_music": "Skummel musikk", + "wind": "Vind", + "rustling_leaves": "Raslende blader", + "wind_noise": "Vindstøy", + "thunderstorm": "Tordenvær", + "thunder": "Torden", + "water": "Vann", + "rain": "Regn", + "raindrop": "Regndråpe", + "rain_on_surface": "Regn på overflate", + "stream": "Bekkeleie", + "ocean": "Hav", + "waves": "Bølger", + "steam": "Damp", + "gurgling": "Gurgling", + "fire": "Brann", + "crackle": "Knakking", + "sailboat": "Seilbåt", + "rowboat": "Robåt", + "motorboat": "Motorbåt", + "ship": "Skip", + "toot": "Tuting", + "car_alarm": "Bilalarm", + "power_windows": "Elektriske vinduer", + "skidding": "Skrens", + "tire_squeal": "Hvinende dekk", + "car_passing_by": "Bil som kjører forbi", + "race_car": "Racerbil", + "truck": "Lastebil", + "air_brake": "Luftbrems", + "air_horn": "Lufthorn", + "reversing_beeps": "Ryggesignal", + "ice_cream_truck": "Iskrembil", + "ambulance": "Ambulanse", + "fire_engine": "Brannbil", + "aircraft_engine": "Flymotor", + "traffic_noise": "Trafikkstøy", + "train_whistle": "Togfløyte", + "train_horn": "Toghorn", + "railroad_car": "Jernbanevogn", + "train_wheels_squealing": "Hvinende togskinner", + "subway": "T-bane", + "aircraft": "Fly", + "jet_engine": "Jetmotor", + "propeller": "Propell", + "helicopter": "Helikopter", + "light_engine": "Lett motor", + "dental_drill's_drill": "Tannlegebor", + "lawn_mower": "Gressklipper", + "chainsaw": "Motorsag", + "medium_engine": "Middels tung motor", + "heavy_engine": "Tung motor", + "engine_knocking": "Motorbanking", + "engine_starting": "Motorstart", + "idling": "Tomgang", + "accelerating": "Akselerasjon", + "doorbell": "Dørklokke", + "ding-dong": "Ding-dong", + "sliding_door": "Skyvedør", + "slam": "Smell", + "knock": "Bank", + "tap": "Tapp", + "squeak": "Knirk", + "cupboard_open_or_close": "Skapdør som åpnes eller lukkes", + "drawer_open_or_close": "Skuff som åpnes eller lukkes", + "dishes": "Oppvask", + "cutlery": "Bestikk", + "chopping": "Hugging", + "frying": "Steking", + "microwave_oven": "Mikrobølgeovn", + "water_tap": "Vannkran", + "electric_toothbrush": "Elektrisk tannbørste", + "vacuum_cleaner": "Støvsuger", + "zipper": "Glidelås", + "keys_jangling": "Klingende nøkler", + "electric_shaver": "Elektrisk barbermaskin", + "shuffling_cards": "Kortstokk som stokkes", + "typing": "Skriving (på tastatur)", + "typewriter": "Skrivemaskin", + "computer_keyboard": "Datatastatur", + "writing": "Skriving", + "alarm": "Alarm", + "telephone": "Telefon", + "telephone_bell_ringing": "Telefon som ringer", + "ringtone": "Ringetone", + "telephone_dialing": "Telefon som slås", + "dial_tone": "Summetone", + "busy_signal": "Opptattsignal", + "alarm_clock": "Vekkerklokke", + "siren": "Sirene", + "civil_defense_siren": "Luftsirene", + "buzzer": "Summer", + "smoke_detector": "Røykvarsler", + "fire_alarm": "Brannalarm", + "foghorn": "Tåkelur", + "whistle": "Fløyte", + "steam_whistle": "Dampfløyte", + "ratchet": "Skralle", + "tick": "Tikk", + "tick-tock": "Tikk-takk", + "gears": "Tannhjul", + "pulleys": "Trinser", + "sewing_machine": "Symaskin", + "mechanical_fan": "Mekanisk vifte", + "air_conditioning": "Klimaanlegg", + "cash_register": "Kasseapparat", + "printer": "Skriver", + "single-lens_reflex_camera": "Speilreflekskamera", + "camera": "Kamera", + "tools": "Verktøy", + "hammer": "Hammer", + "jackhammer": "Trykkluftbor", + "sawing": "Saging", + "filing": "Filing", + "sanding": "Pussing", + "power_tool": "Elektroverktøy", + "drill": "Boremaskin", + "explosion": "Eksplosjon", + "gunshot": "Skudd", + "machine_gun": "Maskingevær", + "fusillade": "Salver", + "artillery_fire": "Artilleriild", + "cap_gun": "Leketøyspistol", + "fireworks": "Fyrverkeri", + "firecracker": "Kinaputt", + "burst": "Spreng", + "eruption": "Utslipp", + "boom": "Drønn", + "wood": "Tre", + "chop": "Hakk", + "splinter": "Splint", + "crack": "Sprekk", + "glass": "Glass", + "chink": "Klirr", + "shatter": "Knuse", + "silence": "Stillhet", + "sound_effect": "Lydeffekt", + "environmental_noise": "Miljøstøy", + "static": "Statisk støy", + "white_noise": "Hvit støy", + "pink_noise": "Rosa støy", + "television": "Fjernsyn", + "radio": "Radio", + "scream": "Skrik", + "sodeling": "sodeling", + "chird": "chird", + "change_ringing": "klokkeringing", + "shofar": "shofar", + "liquid": "væske", + "splash": "plask", + "slosh": "skvulp", + "squish": "klemmelyd", + "drip": "drypp", + "pour": "helle", + "trickle": "sildre", + "gush": "strøm", + "fill": "fylle", + "spray": "spray", + "pump": "pumpe", + "stir": "røre", + "boiling": "koking", + "sonar": "sonar", + "arrow": "pil", + "whoosh": "sus", + "thump": "dump", + "thunk": "dunk", + "electronic_tuner": "elektronisk stemmeapparat", + "effects_unit": "effektenhet", + "chorus_effect": "kor-effekt", + "basketball_bounce": "basketsprettp", + "bang": "smell", + "slap": "klask", + "whack": "slag", + "smash": "knuselyd", + "breaking": "bryting", + "bouncing": "spretting", + "whip": "pisk", + "flap": "flaks", + "scratch": "skrap", + "scrape": "skrape", + "rub": "gnidning", + "roll": "rulling", + "crushing": "knusing", + "crumpling": "krølling", + "tearing": "riving", + "beep": "pip", + "ping": "ping", + "ding": "ding", + "clang": "klang", + "squeal": "hvin", + "creak": "knirk", + "rustle": "rasling", + "whir": "surr", + "clatter": "klirrelyd", + "sizzle": "susing", + "clicking": "klikkelyd", + "clickety_clack": "klikk-klakk", + "rumble": "rumling", + "plop": "plopp", + "hum": "brumming", + "zing": "svisj", + "boing": "boing", + "crunch": "knekk", + "sine_wave": "sinusbølge", + "harmonic": "harmonisk", + "chirp_tone": "pipetone", + "pulse": "puls", + "inside": "innendørs", + "outside": "utendørs", + "reverberation": "etterklang", + "echo": "ekko", + "noise": "støy", + "mains_hum": "nettbrumming", + "distortion": "forvrengning", + "sidetone": "sidetone", + "cacophony": "kakofoni", + "throbbing": "pulsering", + "vibration": "vibrasjon" +} diff --git a/web/public/locales/nb-NO/common.json b/web/public/locales/nb-NO/common.json new file mode 100644 index 0000000..efe6953 --- /dev/null +++ b/web/public/locales/nb-NO/common.json @@ -0,0 +1,334 @@ +{ + "time": { + "yr": "{{time}} år", + "year_one": "{{time}} år", + "year_other": "{{time}} år", + "minute_one": "{{time}} minutt", + "minute_other": "{{time}} minutter", + "s": "{{time}}s", + "second_one": "{{time}} sekund", + "second_other": "{{time}} sekunder", + "formattedTimestampExcludeSeconds": { + "24hour": "%-d. %b, %H:%M", + "12hour": "%-d. %b, %I:%M %p" + }, + "untilForTime": "Inntil {{time}}", + "untilForRestart": "Inntil Frigate starter på nytt.", + "untilRestart": "Inntil omstart", + "ago": "{{timeAgo}} siden", + "justNow": "Akkurat nå", + "today": "I dag", + "yesterday": "I går", + "last7": "Siste 7 dager", + "last14": "Siste 14 dager", + "last30": "Siste 30 dager", + "thisWeek": "Denne uken", + "lastWeek": "Forrige uke", + "thisMonth": "Denne måneden", + "lastMonth": "Forrige måned", + "5minutes": "5 minutter", + "10minutes": "10 minutter", + "30minutes": "30 minutter", + "1hour": "1 time", + "12hours": "12 timer", + "24hours": "24 timer", + "pm": "pm", + "am": "am", + "mo": "{{time}} mnd", + "month_one": "{{time}} måned", + "month_other": "{{time}} måneder", + "d": "{{time}}d", + "day_one": "{{time}} dag", + "day_other": "{{time}} dager", + "h": "{{time}}t", + "hour_one": "{{time}} time", + "hour_other": "{{time}} timer", + "m": "{{time}}m", + "formattedTimestamp": { + "12hour": "d. MMM, h:mm:ss aaa", + "24hour": "d. MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d. MMM HH:mm:ss" + }, + "formattedTimestampWithYear": { + "12hour": "%-d. %b %Y, %I:%M %p", + "24hour": "%-d. %b %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%-d. %b", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d. MMM, h:mm aaa", + "24hour": "d. MMM, HH:mm" + }, + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d. MMM yyyy, h:mm aaa", + "24hour": "d. MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d. MMM", + "formattedTimestampMonthDayYear": { + "24hour": "d. MMM, yyyy", + "12hour": "d. MMM, yyyy" + }, + "inProgress": "Pågår", + "invalidStartTime": "Ugyldig starttid", + "invalidEndTime": "Ugyldig sluttid", + "never": "Aldri" + }, + "button": { + "copy": "Kopier", + "delete": "Slett", + "apply": "Bruk", + "reset": "Tilbakestill", + "done": "Ferdig", + "enabled": "Aktivert", + "enable": "Aktiver", + "disabled": "Deaktivert", + "disable": "Deaktiver", + "save": "Lagre", + "saving": "Lagrer…", + "cancel": "Avbryt", + "close": "Lukk", + "back": "Tilbake", + "history": "Historikk", + "fullscreen": "Fullskjerm", + "exitFullscreen": "Avslutt fullskjerm", + "pictureInPicture": "Bilde-i-bilde", + "twoWayTalk": "Toveis tale", + "cameraAudio": "Kameralyd", + "on": "PÅ", + "off": "AV", + "edit": "Rediger", + "copyCoordinates": "Kopier koordinater", + "yes": "Ja", + "no": "Nei", + "download": "Last ned", + "info": "Info", + "suspended": "Suspendert", + "unsuspended": "Opphev suspensjon", + "play": "Spill av", + "unselect": "Fjern valg", + "export": "Eksporter", + "deleteNow": "Slett nå", + "next": "Neste", + "continue": "Fortsett", + "add": "Legg til", + "undo": "Angre", + "copiedToClipboard": "Kopiert til utklippstavlen", + "modified": "Modifisert", + "saveAll": "Lagre alt", + "savingAll": "Lagrer alt…", + "undoAll": "Angre alt", + "applying": "Bruker…", + "overridden": "Overstyrt", + "resetToGlobal": "Tilbakestill til global", + "resetToDefault": "Tilbakestill til standard", + "retry": "Prøv igjen" + }, + "menu": { + "help": "Hjelp", + "documentation": { + "title": "Dokumentasjon", + "label": "Frigate-dokumentasjon" + }, + "restart": "Start Frigate på nytt", + "live": { + "title": "Direkte", + "allCameras": "Alle kameraer", + "cameras": { + "title": "Kameraer", + "count_one": "{{count}} kamera", + "count_other": "{{count}} kameraer" + } + }, + "review": "Inspiser", + "explore": "Utforsk", + "export": "Eksporter", + "uiPlayground": "UI-Sandkasse", + "faceLibrary": "Ansiktsbibliotek", + "user": { + "title": "Bruker", + "account": "Konto", + "current": "Nåværende bruker: {{user}}", + "anonymous": "anonym", + "logout": "Logg ut", + "setPassword": "Angi passord" + }, + "system": "System", + "systemMetrics": "Systemmålinger", + "configuration": "Konfigurasjon", + "systemLogs": "Systemlogger", + "settings": "Innstillinger", + "configurationEditor": "Rediger konfigurasjonsfil", + "languages": "Språk", + "language": { + "en": "English (Engelsk)", + "zhCN": "简体中文 (Forenklet kinesisk)", + "withSystem": { + "label": "Bruk systemets språkinnstillinger" + }, + "fr": "Français (Fransk)", + "es": "Español (Spansk)", + "hi": "हिन्दी (Hindi)", + "ar": "العربية (Arabisk)", + "pt": "Português (Portugisisk)", + "ru": "Русский (Russisk)", + "de": "Deutsch (Tysk)", + "ja": "日本語 (Japansk)", + "tr": "Türkçe (Tyrkisk)", + "it": "Italiano (Italiensk)", + "nl": "Nederlands (Nederlandsk)", + "sv": "Svenska (Svensk)", + "cs": "Čeština (Tsjekkisk)", + "nb": "Norsk Bokmål", + "ko": "한국어 (Koreansk)", + "vi": "Tiếng Việt (Vietnamesisk)", + "fa": "فارسی (Persisk)", + "he": "עברית (Hebraisk)", + "el": "Ελληνικά (Gresk)", + "ro": "Română (Rumensk)", + "hu": "Magyar (Ungarsk)", + "fi": "Suomi (Finsk)", + "da": "Dansk (Dansk)", + "sk": "Slovenčina (Slovensk)", + "pl": "Polski (Polsk)", + "uk": "Українська (Ukrainsk)", + "yue": "粵語 (Kantonesisk)", + "th": "ไทย (Thai)", + "ca": "Català (Katalansk)", + "ptBR": "Português brasileiro (Brasiliansk portugisisk)", + "sr": "Српски (Serbisk)", + "sl": "Slovenščina (Slovensk)", + "lt": "Lietuvių (Litauisk)", + "bg": "Български (Bulgarsk)", + "gl": "Galego (Galisisk)", + "id": "Bahasa Indonesia (Indonesisk)", + "ur": "اردو (Urdu)", + "hr": "Hrvatski (Kroatisk)", + "bs": "Bosanski (Bosnisk)", + "zhHant": "繁體中文 (Tradisjonell kinesisk)" + }, + "appearance": "Utseende", + "darkMode": { + "label": "Mørk modus", + "light": "Lys", + "dark": "Mørk", + "withSystem": { + "label": "Bruk systemets innstillinger for lys eller mørk modus" + } + }, + "withSystem": "System", + "theme": { + "label": "Tema", + "blue": "Blå", + "green": "Grønn", + "nord": "Nord", + "red": "Rød", + "contrast": "Høy kontrast", + "default": "Standard", + "highcontrast": "Høy kontrast" + }, + "classification": "Klassifisering", + "profiles": "Profiler", + "chat": "Chat", + "actions": "Handlinger", + "features": "Funksjoner" + }, + "pagination": { + "next": { + "title": "Neste", + "label": "Gå til neste side" + }, + "label": "paginering", + "previous": { + "title": "Forrige", + "label": "Gå til forrige side" + }, + "more": "Flere sider" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/t" + }, + "length": { + "meters": "meter", + "feet": "fot" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/time", + "mbph": "MB/time", + "gbph": "GB/time" + } + }, + "label": { + "back": "Gå tilbake", + "hide": "Skjul {{item}}", + "show": "Vis {{item}}", + "ID": "ID", + "none": "Ingen", + "all": "Alle", + "other": "Annet" + }, + "toast": { + "copyUrlToClipboard": "Nettadresse kopiert til utklippstavlen.", + "save": { + "title": "Lagre", + "error": { + "title": "Kunne ikke lagre endringer i konfigurasjonen: {{errorMessage}}", + "noMessage": "Kunne ikke lagre endringer i konfigurasjonen" + }, + "success": "Konfigurasjonsendringer lagret." + } + }, + "role": { + "title": "Rolle", + "admin": "Administrator", + "viewer": "Visningsbruker", + "desc": "Administratorer har full tilgang til alle funksjoner i Frigate brukergrensesnittet. Visningsbrukere er begrenset til å se kameraer, inspisere elementer og se historiske opptak." + }, + "accessDenied": { + "documentTitle": "Ingen tilgang – Frigate", + "title": "Ingen tilgang", + "desc": "Du har ikke tillatelse til å vise denne siden." + }, + "notFound": { + "documentTitle": "Ikke funnet – Frigate", + "title": "404", + "desc": "Siden ble ikke funnet" + }, + "selectItem": "Velg {{item}}", + "readTheDocumentation": "Se dokumentasjonen", + "information": { + "pixels": "{{area}}piklser" + }, + "field": { + "internalID": "Den interne ID-en som Frigate bruker i konfigurasjonen og databasen", + "optional": "Valgfritt" + }, + "list": { + "two": "{{0}} og {{1}}", + "many": "{{items}}, og {{last}}", + "separatorWithSpace": ", " + }, + "validation_errors": "Valideringsfeil", + "no_items": "Ingen elementer", + "credentialField": { + "savedPlaceholder": "Lagret – la stå tomt for å beholde gjeldende" + } +} diff --git a/web/public/locales/nb-NO/components/auth.json b/web/public/locales/nb-NO/components/auth.json new file mode 100644 index 0000000..c59cd4e --- /dev/null +++ b/web/public/locales/nb-NO/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Brukernavn", + "password": "Passord", + "login": "Logg inn", + "errors": { + "usernameRequired": "Brukernavn er påkrevd", + "passwordRequired": "Passord er påkrevd", + "rateLimit": "Grense for antall forsøk overskredet. Prøv igjen senere.", + "loginFailed": "Innlogging mislyktes", + "unknownError": "Ukjent feil. Sjekk loggene.", + "webUnknownError": "Ukjent feil. Sjekk konsoll-loggene." + }, + "firstTimeLogin": "Prøver du å logge inn for første gang? Påloggingsinformasjonen er skrevet ut i Frigate-loggene." + } +} diff --git a/web/public/locales/nb-NO/components/camera.json b/web/public/locales/nb-NO/components/camera.json new file mode 100644 index 0000000..60d318a --- /dev/null +++ b/web/public/locales/nb-NO/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "Kameragrupper", + "name": { + "placeholder": "Skriv inn et navn…", + "errorMessage": { + "mustLeastCharacters": "Navnet på kameragruppen må være minst 2 tegn.", + "exists": "Navnet på kameragruppen finnes allerede.", + "nameMustNotPeriod": "Navnet på kameragruppen kan ikke inneholde punktum.", + "invalid": "Ugyldig navn på kameragruppe." + }, + "label": "Navn" + }, + "camera": { + "setting": { + "streamMethod": { + "method": { + "continuousStreaming": { + "label": "Kontinuerlig strømming", + "desc": { + "title": "Kamerabildet vil alltid være en direktestrøm når det vises på dashbordet, selv om ingen aktivitet oppdages.", + "warning": "Kontinuerlig strømming kan føre til høy båndbreddebruk og ytelsesproblemer. Bruk med forsiktighet." + } + }, + "noStreaming": { + "label": "Ingen strømming", + "desc": "Kamerabilder vil bare oppdateres én gang i minuttet, og ingen direktestrømming vil finne sted." + }, + "smartStreaming": { + "label": "Smart strømming (anbefalt)", + "desc": "Smart strømming oppdaterer kamerabilder én gang i minuttet når ingen aktivitet oppdages, for å spare båndbredde og ressurser. Når aktivitet oppdages, byttes bildet sømløst til direktestrøm." + } + }, + "label": "Strømmemetode", + "placeholder": "Velg en strømmemetode" + }, + "compatibilityMode": { + "label": "Kompatibilitetsmodus", + "desc": "Aktiver dette alternativet kun hvis kameraets direktestrøm viser fargeforstyrrelser og har en diagonal linje på høyre side av bildet." + }, + "label": "Innstillinger for kamerastrømming", + "title": "{{cameraName}} strømmeinnstillinger", + "desc": "Endre direktestrømmingsalternativene for denne kameragruppens dashbord. Disse innstillingene er spesifikke for enhet/nettleser.", + "audioIsAvailable": "Lyd er tilgjengelig for denne strømmen", + "audioIsUnavailable": "Lyd er ikke tilgjengelig for denne strømmen", + "audio": { + "tips": { + "title": "Lyd må komme fra kameraet ditt og konfigureres i go2rtc for denne strømmen.", + "document": "Se dokumentasjonen " + } + }, + "stream": "Strøm", + "placeholder": "Velg en strøm" + }, + "birdseye": "Fugleperspektiv" + }, + "add": "Legg til kameragruppe", + "edit": "Rediger kameragruppe", + "delete": { + "label": "Slett kameragruppe", + "confirm": { + "title": "Bekreft sletting", + "desc": "Er du sikker på at du vil slette kameragruppen {{name}}?" + } + }, + "cameras": { + "label": "Kameraer", + "desc": "Velg kameraer for denne gruppen." + }, + "icon": "Ikon", + "success": "Kameragruppen ({{name}}) er lagret.", + "showAll": "Vis alle kameragrupper", + "showLess": "Vis mindre", + "editGroups": "Rediger kameragrupper" + }, + "debug": { + "options": { + "label": "Innstillinger", + "title": "Alternativer", + "showOptions": "Vis alternativer", + "hideOptions": "Skjul alternativer" + }, + "boundingBox": "Markeringsramme", + "timestamp": "Tidsstempel", + "zones": "Soner", + "mask": "Maske", + "motion": "Bevegelse", + "regions": "Regioner", + "paths": "Stier" + } +} diff --git a/web/public/locales/nb-NO/components/dialog.json b/web/public/locales/nb-NO/components/dialog.json new file mode 100644 index 0000000..cdb0930 --- /dev/null +++ b/web/public/locales/nb-NO/components/dialog.json @@ -0,0 +1,209 @@ +{ + "restart": { + "title": "Er du sikker på at du vil starte Frigate på nytt?", + "button": "Start på nytt", + "restarting": { + "title": "Frigate starter på nytt", + "button": "Tving omlasting nå", + "content": "Denne siden vil lastes inn på nytt om {{countdown}} sekunder." + }, + "description": "Dette vil stoppe Frigate et øyeblikk mens det starter på nytt." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Send til Frigate+", + "desc": "Objekter på steder du vil unngå er ikke falske positiver. Å sende dem som falske positiver vil forvirre modellen." + }, + "review": { + "true": { + "label": "Bekreft denne merkelappen for Frigate Plus", + "true_one": "Dette er en {{label}}", + "true_other": "Dette er en {{label}}" + }, + "state": { + "submitted": "Sendt inn" + }, + "false": { + "label": "Ikke bekreft denne merkelappen for Frigate Plus", + "false_one": "Dette er ikke en {{label}}", + "false_other": "Dette er ikke en {{label}}" + }, + "question": { + "label": "Bekreft denne etiketten for Frigate Plus", + "ask_an": "Er dette objekt en {{label}}?", + "ask_a": "Er dette objektet en {{label}}?", + "ask_full": "Er dette objekt en {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "toast": { + "error": "Kunne ikke sende til Frigate+. Vennligst sjekk nettverksforbindelsen din og prøv igjen." + } + } + }, + "video": { + "viewInHistory": "Vis i historikk" + } + }, + "export": { + "time": { + "lastHour_one": "Siste time", + "lastHour_other": "Siste {{count}} timer", + "custom": "Tilpasset", + "start": { + "title": "Starttid", + "label": "Velg starttid" + }, + "fromTimeline": "Velg fra tidslinje", + "end": { + "title": "Sluttid", + "label": "Velg sluttid" + } + }, + "toast": { + "success": "Eksport startet. Se filen på eksportsiden.", + "error": { + "failed": "Kunne ikke legge eksport i kø: {{error}}", + "noVaildTimeSelected": "Ingen gyldig tidsperiode valgt", + "endTimeMustAfterStartTime": "Sluttid må være etter starttid" + }, + "view": "Vis", + "queued": "Eksport lagt i kø. Se fremdrift på eksportsiden.", + "batchPartial": "Startet {{successful}} av {{total}} eksporter. Kameraer som feilet: {{failedCameras}}", + "batchFailed": "Kunne ikke starte {{total}} eksporter. Kameraer som feilet: {{failedCameras}}", + "batchQueuedPartial": "La {{successful}} av {{total}} eksporter i kø. Kameraer som feilet: {{failedCameras}}", + "batchQueueFailed": "Kunne ikke legge {{total}} eksporter i kø. Kameraer som feilet: {{failedCameras}}", + "batchSuccess_one": "Startet 1 eksport. Åpner saken nå.", + "batchSuccess_other": "Startet {{count}} eksporter. Åpner saken nå.", + "batchQueuedSuccess_one": "La 1 eksport i kø. Åpner saken nå.", + "batchQueuedSuccess_other": "La {{count}} eksporter i kø. Åpner saken nå." + }, + "fromTimeline": { + "previewExport": "Forhåndsvis eksport", + "saveExport": "Lagre eksport", + "queueingExport": "Legger eksport i kø...", + "useThisRange": "Bruk dette tidsrommet" + }, + "name": { + "placeholder": "Gi eksporten et navn" + }, + "select": "Velg", + "export": "Eksporter", + "selectOrExport": "Velg eller eksporter", + "case": { + "label": "Sak", + "placeholder": "Velg en sak", + "newCaseOption": "Opprett ny sak", + "newCaseNamePlaceholder": "Navn på ny sak", + "newCaseDescriptionPlaceholder": "Saksbeskrivelse", + "nonAdminHelp": "En ny sak vil bli opprettet for disse eksportene." + }, + "queueing": "Legger eksport i kø...", + "tabs": { + "export": "Enkeltkamera", + "multiCamera": "Multikamera" + }, + "multiCamera": { + "timeRange": "Tidsrom", + "selectFromTimeline": "Velg fra tidslinje", + "cameraSelection": "Kameraer", + "cameraSelectionHelp": "Kameraer med sporede objekter i dette tidsrommet er forhåndsvalgt", + "checkingActivity": "Sjekker kameraaktivitet...", + "noCameras": "Ingen kameraer tilgjengelig", + "nameLabel": "Eksportnavn", + "namePlaceholder": "Valgfritt navneprefiks for disse eksportene", + "queueingButton": "Legger eksporter i kø...", + "detectionCount_one": "1 sporet objekt", + "detectionCount_other": "{{count}} sporede objekter", + "exportButton_one": "Eksporter 1 kamera", + "exportButton_other": "Eksporter {{count}} kameraer", + "searchOrSelectGroup": "Søk, eller velg en kameragruppe...", + "selectAll": "Velg alle kameraer", + "clearSelection": "Nullstill valg", + "selectWithActivity": "Kameraer med sporede objekter", + "selectGroup": "Velg gruppe", + "noMatchingCameras": "Ingen kameraer samsvarer med søket ditt", + "selectedCount": "{{selected}} / {{total}} valgt" + }, + "multi": { + "description": "Eksporter hver valgte inspeksjon. Alle eksporter vil bli gruppert under én sak.", + "descriptionNoCase": "Eksporter hver valgte inspeksjon.", + "caseNamePlaceholder": "Eksport av inspeksjon - {{date}}", + "exportingButton": "Eksporterer...", + "toast": { + "partial": "Startet {{successful}} av {{total}} eksporter. Feilet: {{failedItems}}", + "failed": "Kunne ikke starte {{total}} eksporter. Feilet: {{failedItems}}", + "started_one": "Startet 1 eksport. Åpner saken nå.", + "started_other": "Startet {{count}} eksporter. Åpner saken nå.", + "startedNoCase_one": "Startet 1 eksport.", + "startedNoCase_other": "Startet {{count}} eksporter." + }, + "title_one": "Eksporter 1 inspeksjon", + "title_other": "Eksporter {{count}} inspeksjon", + "exportButton_one": "Eksporter 1 inspeksjon", + "exportButton_other": "Eksporter {{count}} inspeksjon" + } + }, + "streaming": { + "label": "Strøm", + "restreaming": { + "disabled": "Restrømming er ikke aktivert for dette kameraet.", + "desc": { + "readTheDocumentation": "Se dokumentasjonen", + "title": "Konfigurer go2rtc for flere direktestrømmingsalternativer og lyd for dette kameraet." + } + }, + "showStats": { + "label": "Vis strømmestatistikk", + "desc": "Aktiver dette alternativet for å vise strømmestatistikk som et overlegg på kamerabildet." + }, + "debugView": "Feilsøkingsvisning" + }, + "search": { + "saveSearch": { + "button": { + "save": { + "label": "Lagre dette søket" + } + }, + "label": "Lagre søk", + "desc": "Skriv inn et navn for dette lagrede søket.", + "placeholder": "Skriv inn et navn for søket", + "overwrite": "{{searchName}} finnes allerede. Lagring vil overskrive eksisterende verdi.", + "success": "Søk ({{searchName}}) er lagret." + } + }, + "recording": { + "confirmDelete": { + "title": "Bekreft sletting", + "desc": { + "selected": "Er du sikker på at du vil slette alle opptak knyttet til dette inspeksjonselementet?

    Hold inne Shift-tasten for å hoppe over denne dialogen i fremtiden." + }, + "toast": { + "success": "Videomaterialet knyttet til de valgte inspeksjonselementene har blitt slettet.", + "error": "Kunne ikke slette: {{error}}" + } + }, + "button": { + "export": "Eksportér", + "markAsReviewed": "Merk som inspisert", + "deleteNow": "Slett nå", + "markAsUnreviewed": "Merk som ikke inspisert" + }, + "shareTimestamp": { + "description": "Del en tidsstemplet URL fra avspillerens nåværende posisjon, eller velg et egendefinert tidsstempel. Merk at dette ikke er en offentlig delingslenke, og at den kun er tilgjengelig for brukere med tilgang til Frigate og dette kameraet.", + "custom": "Egendefinert tidsstempel", + "title": "Del tidsstempel", + "label": "Del tidsstempel", + "button": "Del URL med tidsstempel", + "shareTitle": "Tidsstempel for Frigate-inspeksjon: {{camera}}" + } + }, + "imagePicker": { + "selectImage": "Velg et sporet objekts miniatyrbilde", + "search": { + "placeholder": "Søk etter (under-)etikett..." + }, + "noImages": "Ingen miniatyrbilder funnet for dette kameraet", + "unknownLabel": "Lagret utløserbilde" + } +} diff --git a/web/public/locales/nb-NO/components/filter.json b/web/public/locales/nb-NO/components/filter.json new file mode 100644 index 0000000..eb06846 --- /dev/null +++ b/web/public/locales/nb-NO/components/filter.json @@ -0,0 +1,141 @@ +{ + "filter": "Filter", + "labels": { + "label": "Etiketter", + "all": { + "title": "Alle etiketter", + "short": "Etiketter" + }, + "count": "{{count}} merkelapper", + "count_other": "{{count}} Etiketter", + "count_one": "{{count}} Etikett" + }, + "features": { + "hasVideoClip": "Har et videoklipp", + "submittedToFrigatePlus": { + "label": "Sendt til Frigate+", + "tips": "Du må først filtrere på sporede objekter som har et stillbilde.

    Sporede objekter uten et stillbilde kan ikke sendes til Frigate+." + }, + "label": "Funksjoner", + "hasSnapshot": "Har et stillbilde" + }, + "sort": { + "label": "Sorter", + "dateAsc": "Dato (Stigende)", + "dateDesc": "Dato (Synkende)", + "scoreAsc": "Objektscore (Stigende)", + "scoreDesc": "Objektscore (Synkende)", + "speedAsc": "Estimert hastighet (Stigende)", + "speedDesc": "Estimert hastighet (Synkende)", + "relevance": "Relevans" + }, + "explore": { + "date": { + "selectDateBy": { + "label": "Velg en dato å filtrere etter" + } + }, + "settings": { + "title": "Innstillinger", + "defaultView": { + "title": "Standard visning", + "desc": "Når ingen filtre er valgt, vis et sammendrag av de nyeste sporede objektene per etikett, eller vis et ufiltrert rutenett.", + "summary": "Sammendrag", + "unfilteredGrid": "Ufiltrert rutenett" + }, + "gridColumns": { + "title": "Rutenett kolonner", + "desc": "Velg antall kolonner i rutenettvisningen." + }, + "searchSource": { + "label": "Søkekilde", + "desc": "Velg om du vil søke i bildene eller beskrivelsene av de sporede objektene dine.", + "options": { + "thumbnailImage": "Miniatyrbilde", + "description": "Beskrivelse" + } + } + } + }, + "logSettings": { + "label": "Filtrer loggnivå", + "filterBySeverity": "Filtrer logger etter alvorlighetsgrad", + "loading": { + "title": "Laster inn", + "desc": "Når loggvinduet rulles til bunnen, strømmes nye logger automatisk etter hvert som de legges til." + }, + "disableLogStreaming": "Deaktiver loggstrømming", + "allLogs": "Alle logger" + }, + "trackedObjectDelete": { + "title": "Bekreft sletting", + "desc": "Sletting av disse {{objectLength}} sporede objektene fjerner stillbildet, eventuelle lagrede vektorrepresentasjoner og tilhørende objekt livssyklusoppføringer. Opptak av disse sporede objektene i Historikkvisning vil IKKE bli slettet.

    Er du sikker på at du vil fortsette?

    Hold Shift-tasten for å unngå denne dialogboksen i fremtiden.", + "toast": { + "success": "Sporede objekter ble slettet.", + "error": "Kunne ikke slette sporede objekter: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrer etter sonemaske" + }, + "recognizedLicensePlates": { + "noLicensePlatesFound": "Ingen kjennemerker funnet.", + "selectPlatesFromList": "Velg ett eller flere kjennemerker fra listen.", + "title": "Gjenkjente kjennemerker", + "loadFailed": "Kunne ikke laste inn gjenkjente kjennemerker.", + "loading": "Laster inn gjenkjente kjennemerker…", + "placeholder": "Skriv for å søke etter kjennemerker…", + "selectAll": "Velg alle", + "clearAll": "Fjern alle" + }, + "dates": { + "all": { + "title": "Alle datoer", + "short": "Datoer" + }, + "selectPreset": "Velg en forhåndsinnstilling.…" + }, + "more": "Flere filtre", + "reset": { + "label": "Nullstill filtre til standardverdier" + }, + "timeRange": "Tidsrom", + "subLabels": { + "label": "Underetiketter", + "all": "Alle underetiketter" + }, + "score": "Score", + "estimatedSpeed": "Estimert hastighet ({{unit}})", + "cameras": { + "all": { + "title": "Alle kameraer", + "short": "Kameraer" + }, + "label": "Kamerafilter" + }, + "review": { + "showReviewed": "Vis inspiserte" + }, + "motion": { + "showMotionOnly": "Vis kun bevegelse" + }, + "zones": { + "label": "Soner", + "all": { + "title": "Alle soner", + "short": "Soner" + } + }, + "classes": { + "label": "Klasser", + "all": { + "title": "Alle klasser" + }, + "count_one": "{{count}} Klasse", + "count_other": "{{count}} Klasser" + }, + "attributes": { + "label": "Klassifiseringsattributter", + "all": "Alle attributter" + } +} diff --git a/web/public/locales/nb-NO/components/icons.json b/web/public/locales/nb-NO/components/icons.json new file mode 100644 index 0000000..937a8d0 --- /dev/null +++ b/web/public/locales/nb-NO/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Velg et ikon", + "search": { + "placeholder": "Søk etter et ikon…" + } + } +} diff --git a/web/public/locales/nb-NO/components/input.json b/web/public/locales/nb-NO/components/input.json new file mode 100644 index 0000000..eb03da4 --- /dev/null +++ b/web/public/locales/nb-NO/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Last ned video", + "toast": { + "success": "Videoen for inspeksjonselementet ditt har startet nedlasting." + } + } + } +} diff --git a/web/public/locales/nb-NO/components/player.json b/web/public/locales/nb-NO/components/player.json new file mode 100644 index 0000000..31e8e28 --- /dev/null +++ b/web/public/locales/nb-NO/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "Ingen opptak funnet for dette tidspunktet", + "stats": { + "streamType": { + "short": "Type", + "title": "Strømmetype:" + }, + "droppedFrames": { + "short": { + "title": "Tapt", + "value": "{{droppedFrames}} bilder" + }, + "title": "Tapte bilder:" + }, + "bandwidth": { + "title": "Båndbredde:", + "short": "Båndbredde" + }, + "latency": { + "title": "Forsinkelse:", + "value": "{{seconds}} sekunder", + "short": { + "title": "Forsinkelse", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Totalt antall bilder:", + "decodedFrames": "Dekodede bilder:", + "droppedFrameRate": "Tapte bilder per sekund:" + }, + "noPreviewFound": "Ingen forhåndsvisning funnet", + "noPreviewFoundFor": "Ingen forhåndsvisning funnet for {{cameraName}}", + "submitFrigatePlus": { + "title": "Send dette bildet til Frigate+?", + "submit": "Send", + "previewError": "Kunne ikke laste forhåndsvisning av stillbilde. Opptaket er kanskje ikke tilgjengelig for øyeblikket." + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 eller høyere kreves for denne typen direkte-strømming.", + "streamOffline": { + "title": "Strømmen er frakoblet", + "desc": "Ingen bilder er mottatt på {{cameraName}} detekt-strømmen, sjekk feilloggene" + }, + "cameraDisabled": "Kameraet er deaktivert", + "toast": { + "success": { + "submittedFrigatePlus": "Bildet ble sendt til Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Kunne ikke sende bildet til Frigate+" + } + }, + "cameraOff": "Kameraet er av" +} diff --git a/web/public/locales/nb-NO/config/cameras.json b/web/public/locales/nb-NO/config/cameras.json new file mode 100644 index 0000000..5f8c818 --- /dev/null +++ b/web/public/locales/nb-NO/config/cameras.json @@ -0,0 +1,960 @@ +{ + "mqtt": { + "label": "MQTT", + "bounding_box": { + "description": "Tegn markeringsrammer på bilder som publiseres over MQTT.", + "label": "Legg til markeringsramme" + }, + "crop": { + "description": "Beskjær bilder publisert til MQTT til det detekterte objektets markeringsramme.", + "label": "Beskjær bilde" + }, + "description": "Innstillinger for bilde-publisering via MQTT.", + "enabled": { + "description": "Aktiver publisering av stillbilder for objekter til MQTT-emner for dette kameraet.", + "label": "Send bilde" + }, + "height": { + "description": "Høyde (piksler) for bilder som publiseres over MQTT.", + "label": "Bildehøyde" + }, + "quality": { + "description": "JPEG-kvalitet for bilder publisert til MQTT (0-100).", + "label": "JPEG-kvalitet" + }, + "required_zones": { + "description": "Soner et objekt må tre inn i for at et MQTT-bilde skal publiseres.", + "label": "Påkrevde soner" + }, + "timestamp": { + "description": "Legg et tidsstempel over bilder som publiseres til MQTT.", + "label": "Legg til tidsstempel" + } + }, + "notifications": { + "label": "Varslinger", + "enabled": { + "label": "Aktiver varslinger", + "description": "Aktiver eller deaktiver varslinger for dette kameraet." + }, + "email": { + "label": "E-postadresse for varsling", + "description": "E-postadresse som brukes for push-varslinger eller som kreves av visse varslingstjenester." + }, + "cooldown": { + "label": "Nedkjølingsperiode", + "description": "Nedkjøling (sekunder) mellom varslinger for å unngå å spamme mottakere." + }, + "enabled_in_config": { + "label": "Opprinnelig varslingsstatus", + "description": "Indikerer om varslinger var aktivert i den opprinnelige statiske konfigurasjonen." + }, + "description": "Innstillinger for å aktivere og kontrollere varslinger for dette kameraet." + }, + "audio": { + "label": "Lyddeteksjon", + "enabled": { + "label": "Aktiver lyddeteksjon", + "description": "Aktiver eller deaktiver deteksjon av lydhendelser for dette kameraet." + }, + "max_not_heard": { + "label": "Tidsavbrudd for avslutning", + "description": "Antall sekunder uten den konfigurerte lydtypen før lydhendelsen avsluttes." + }, + "min_volume": { + "label": "Minimumsvolum", + "description": "Minimum terskel for RMS-volum som kreves for å kjøre lyddeteksjon; lavere verdier øker følsomheten (f.eks. 200 høy, 500 middels, 1000 lav)." + }, + "listen": { + "label": "Lyttetyper", + "description": "Liste over typer lydhendelser som skal detekteres (f.eks. bjeff, brannalarm, tale, rop)." + }, + "filters": { + "label": "Lydfiltre", + "description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive.", + "threshold": { + "description": "Minimum konfidens-terskel for at lydhendelsen skal bli registrert.", + "label": "Minimum konfidens for lyd" + } + }, + "enabled_in_config": { + "label": "Opprinnelig lydstatus", + "description": "Indikerer om lyddeteksjon opprinnelig var aktivert i den statiske konfigurasjonsfilen." + }, + "num_threads": { + "label": "Deteksjonstråder", + "description": "Antall tråder som skal brukes til prosessering av lyddeteksjon." + }, + "description": "Innstillinger for lydbasert hendelsesdeteksjon for dette kameraet." + }, + "birdseye": { + "label": "Fugleperspektiv", + "description": "Innstillinger for Fugleperspektiv (Birdseye) som setter sammen flere kamerastrømmer til ett felles oppsett.", + "enabled": { + "label": "Aktiver Fugleperspektiv", + "description": "Aktiver eller deaktiver funksjonen for Fugleperspektiv." + }, + "mode": { + "label": "Sporingsmodus", + "description": "Modus for å inkludere kameraer i Fugleperspektiv: 'objects', 'motion' eller 'continuous'." + }, + "order": { + "label": "Posisjon", + "description": "Numerisk posisjon som kontrollerer kameraenes rekkefølge i Fugleperspektiv-oppsettet." + } + }, + "detect": { + "label": "Objektdeteksjon", + "description": "Innstillinger for deteksjonsrollen brukt til å kjøre objektdeteksjon og starte sporing (trackere).", + "enabled": { + "label": "Aktiver objektdeteksjon", + "description": "Aktiver eller deaktiver objektdeteksjon for dette kameraet." + }, + "height": { + "label": "Deteksjonshøyde", + "description": "Høyde (piksler) på bilder brukt for deteksjonsstrømmen; la stå tom for å bruke strømmens opprinnelige oppløsning." + }, + "width": { + "label": "Deteksjonsbredde", + "description": "Bredde (piksler) på bilder brukt for deteksjonsstrømmen; la stå tom for å bruke strømmens opprinnelige oppløsning." + }, + "fps": { + "label": "Deteksjons-FPS", + "description": "Ønsket antall bilder per sekund (FPS) for deteksjon; lavere verdier reduserer CPU-bruk (anbefalt verdi er 5, sett kun høyere – maks 10 – ved sporing av objekter i svært høy fart)." + }, + "min_initialized": { + "label": "Minimum initialiseringsbilder", + "description": "Antall påfølgende deteksjonstreff som kreves før et sporet objekt opprettes. Øk for å redusere falske initialiseringer. Standardverdi er FPS delt på 2." + }, + "max_disappeared": { + "label": "Maks bilder borte", + "description": "Antall bilder uten deteksjon før et sporet objekt anses som borte." + }, + "stationary": { + "label": "Konfigurasjon for stasjonære objekter", + "description": "Innstillinger for å detektere og håndtere objekter som forblir i ro over en viss tid.", + "interval": { + "label": "Intervall for stasjonære objekter", + "description": "Hvor ofte (i antall bilder) det skal kjøres en deteksjonssjekk for å bekrefte et stasjonært objekt." + }, + "threshold": { + "label": "Terskel for stasjonære objekter", + "description": "Antall bilder uten posisjonsendring som kreves for å markere et objekt som stasjonært." + }, + "max_frames": { + "label": "Maks antall bilder", + "description": "Begrenser hvor lenge stasjonære objekter spores før de forkastes.", + "default": { + "label": "Standard maks bilder", + "description": "Standard maksimalt antall bilder et stasjonært objekt spores før det stoppes." + }, + "objects": { + "label": "Maks bilder per objekt", + "description": "Overstyringer per objekttype for maksimalt antall bilder stasjonære objekter skal spores." + } + }, + "classifier": { + "label": "Aktiver visuell klassifiserer", + "description": "Bruk en visuell klassifiserer for å detektere reelt stasjonære objekter selv når markeringsrammene \"skjelver\" (jitter)." + } + }, + "annotation_offset": { + "label": "Forskyvning av annotering", + "description": "Millisekunder for å forskyve deteksjonsannoteringer for bedre samsvar mellom markeringsrammer på tidslinjen og opptakene; kan være positiv eller negativ." + } + }, + "ffmpeg": { + "label": "Strømmer (FFmpeg)", + "description": "FFmpeg-innstillinger, inkludert sti til binærfil, argumenter, alternativer for maskinvareakselerasjon og utdata-argumenter per rolle.", + "path": { + "label": "FFmpeg-sti", + "description": "Sti til FFmpeg-binærfilen som skal brukes, eller et versjonsalias (\"7.0\" eller \"8.0\")." + }, + "global_args": { + "label": "Globale FFmpeg-argumenter", + "description": "Globale argumenter som sendes til FFmpeg-prosesser." + }, + "hwaccel_args": { + "label": "Argumenter for maskinvareakselerasjon", + "description": "Argumenter for maskinvareakselerasjon i FFmpeg. Leverandørspesifikke forhåndsinnstillinger anbefales." + }, + "input_args": { + "label": "Inndata-argumenter", + "description": "Inndata-argumenter som brukes på FFmpeg-innstrømmer." + }, + "output_args": { + "label": "Utdata-argumenter", + "description": "Standard utdata-argumenter brukt for ulike FFmpeg-roller som deteksjon og opptak.", + "detect": { + "label": "Utdata-argumenter for deteksjon", + "description": "Standard utdata-argumenter for strømmer med deteksjonsrolle." + }, + "record": { + "label": "Utdata-argumenter for opptak", + "description": "Standard utdata-argumenter for strømmer med opptaksrolle." + } + }, + "retry_interval": { + "label": "FFmpeg-forsøksintervall", + "description": "Sekunder å vente før man prøver å koble til en kamerastrøm på nytt etter feil. Standard er 10." + }, + "apple_compatibility": { + "label": "Apple-kompatibilitet", + "description": "Aktiver HEVC-tagging for bedre kompatibilitet med Apple-avspillere ved opptak i H.265." + }, + "gpu": { + "label": "GPU-indeks", + "description": "Standard GPU-indeks som brukes til maskinvareakselerasjon hvis tilgjengelig." + }, + "inputs": { + "label": "Kamerainndata", + "description": "Liste over definisjoner for inndatastrømmer (stier og roller) for dette kameraet.", + "path": { + "label": "Inndatasti", + "description": "URL eller sti for kameraets inndatastrøm." + }, + "roles": { + "label": "Inndataroller", + "description": "Roller for denne inndatastrømmen." + }, + "global_args": { + "label": "Globale FFmpeg-argumenter", + "description": "Globale FFmpeg-argumenter for denne inndatastrømmen." + }, + "hwaccel_args": { + "label": "Argumenter for maskinvareakselerasjon", + "description": "Argumenter for maskinvareakselerasjon for denne inndatastrømmen." + }, + "input_args": { + "label": "Inndata-argumenter", + "description": "Inndata-argumenter spesifisert for denne strømmen." + } + } + }, + "live": { + "label": "Direkteavspilling", + "streams": { + "label": "Navn på direktestrømmer", + "description": "Kobling mellom konfigurerte strøm-navn og restream/go2rtc-navn brukt for direkteavspilling." + }, + "height": { + "label": "Direktevisningshøyde", + "description": "Høyde (piksler) for jsmpeg-direktestrømmen i web-grensesnittet; må være <= høyden på deteksjonsstrømmen." + }, + "quality": { + "label": "Direktevisningskvalitet", + "description": "Kodingskvalitet for jsmpeg-strømmen (1 høyest, 31 lavest)." + }, + "description": "Innstillinger brukt av web-grensesnittet for valg av direktestrøm, oppløsning og kvalitet." + }, + "motion": { + "label": "Bevegelsesdeteksjon", + "enabled": { + "label": "Aktiver bevegelsesdeteksjon", + "description": "Aktiver eller deaktiver bevegelsesdeteksjon for dette kameraet." + }, + "threshold": { + "label": "Terskel for bevegelse", + "description": "Terskel for pikselendring brukt av bevegelsesdetektoren; høyere verdier reduserer følsomheten (intervall 1–255)." + }, + "lightning_threshold": { + "label": "Terskel for lyn/lysglimt", + "description": "Terskel for å oppdage og ignorere korte lysglimt (lavere er mer følsom, verdier mellom 0,3 og 1,0). Dette stopper ikke bevegelsesdeteksjon helt; det fører bare til at detektoren slutter å analysere flere bilder når terskelen er nådd. Bevegelsesbaserte opptak blir fortsatt laget under slike hendelser." + }, + "skip_motion_threshold": { + "label": "Terskel for å hoppe over bevegelse", + "description": "Hvis satt til en verdi mellom 0,0 og 1,0, og mer enn denne andelen av bildet endres i ett enkelt bilde, vil detektoren ikke returnere noen bevegelsesbokser og kalibrere på nytt umiddelbart. Dette kan spare CPU og redusere falske positive under lyn, storm, osv., men kan gå glipp av ekte hendelser som at et PTZ-kamera autosporer et objekt. Avveiningen står mellom å miste noen megabyte med opptak mot å måtte se gjennom et par korte klipp. La stå tom (None) for å deaktivere denne funksjonen." + }, + "improve_contrast": { + "label": "Forbedre kontrast", + "description": "Bruk kontrastforbedring på bilder før bevegelsesanalyse for å hjelpe deteksjonen." + }, + "contour_area": { + "label": "Konturområde", + "description": "Minimum konturområde i piksler som kreves for at en bevegelseskontur skal telles med." + }, + "delta_alpha": { + "label": "Delta alfa", + "description": "Alfa-blandingsfaktor brukt i bildedifferensiering for bevegelsesberegning." + }, + "frame_alpha": { + "label": "Bilde-alfa", + "description": "Alfa-verdi brukt ved sammenfletting av bilder for forhåndsbehandling av bevegelse." + }, + "frame_height": { + "label": "Bildehøyde", + "description": "Høyde i piksler som bildene skal skaleres til ved beregning av bevegelse." + }, + "mask": { + "label": "Maskekoordinater", + "description": "Sorterte x,y-koordinater som definerer polygonet for bevegelsesmasken brukt til å inkludere/ekskludere områder." + }, + "mqtt_off_delay": { + "label": "MQTT-av-forsinkelse", + "description": "Sekunder å vente etter siste bevegelse før en MQTT 'av'-status publiseres." + }, + "enabled_in_config": { + "label": "Opprinnelig bevegelsesstatus", + "description": "Indikerer om bevegelsesdeteksjon var aktivert i den opprinnelige statiske konfigurasjonen." + }, + "raw_mask": { + "label": "Råmaske" + }, + "description": "Standardinnstillinger for bevegelsesdeteksjon for dette kameraet." + }, + "objects": { + "label": "Objekter", + "description": "Standardinnstillinger for objektsporing, inkludert hvilke etiketter som skal spores og filtre per objekt.", + "track": { + "label": "Objekter som skal spores", + "description": "Liste over objektetiketter som skal spores for dette kameraet." + }, + "filters": { + "label": "Objektfiltre", + "description": "Filtre som brukes på detekterte objekter for å redusere falske positive (område, forhold, konfidens).", + "min_area": { + "label": "Minimum objektområde", + "description": "Minimum areal for markeringsramme (piksler eller prosent) som kreves for denne objekttypen. Kan oppgis i piksler (heltall) eller prosent (desimaltall mellom 0,000001 og 0,99)." + }, + "max_area": { + "label": "Maksimum objektområde", + "description": "Maksimalt areal for markeringsramme (piksler eller prosent) tillatt for denne objekttypen." + }, + "min_ratio": { + "label": "Minimum størrelsesforhold", + "description": "Minimum forhold mellom bredde og høyde som kreves for at markeringsrammen skal kvalifisere." + }, + "max_ratio": { + "label": "Maksimum størrelsesforhold", + "description": "Maksimalt forhold mellom bredde og høyde tillatt for at markeringsrammen skal kvalifisere." + }, + "threshold": { + "label": "Konfidensterskel", + "description": "Gjennomsnittlig terskel for deteksjonskonfidens som kreves for at objektet skal anses som en ekte positiv." + }, + "min_score": { + "label": "Minimum konfidens", + "description": "Minimum deteksjonskonfidens for et enkeltbilde som kreves for at objektet skal telles med." + }, + "mask": { + "label": "Filtermaske", + "description": "Polygonkoordinater som definerer hvor dette filteret gjelder innenfor bildet." + }, + "raw_mask": { + "label": "Råmaske" + } + }, + "mask": { + "label": "Objektmaske", + "description": "Maskepolygon brukt for å forhindre objektdeteksjon i spesifiserte områder." + }, + "raw_mask": { + "label": "Råmaske" + }, + "genai": { + "label": "GenAI-objektkonfigurasjon", + "description": "GenAI-alternativer for å beskrive sporede objekter og sende bilder til generering.", + "enabled": { + "label": "Aktiver GenAI", + "description": "Aktiver GenAI-generering av beskrivelser for sporede objekter som standard." + }, + "use_snapshot": { + "label": "Bruk stillbilder", + "description": "Bruk stillbilder av objekter i stedet for miniatyrbilder for GenAI-beskrivelsesgenerering." + }, + "prompt": { + "label": "Ledetekst for bildetekst", + "description": "Standardmal for ledetekst brukt ved generering av beskrivelser med GenAI." + }, + "object_prompts": { + "label": "Objektspesifikke ledetekster", + "description": "Ledetekster per objekt for å tilpasse GenAI-resultater for spesifikke etiketter." + }, + "objects": { + "label": "GenAI-objekter", + "description": "Liste over objektetiketter som skal sendes til GenAI som standard." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Soner som må entres for at objekter skal kvalifisere for GenAI-beskrivelsesgenerering." + }, + "debug_save_thumbnails": { + "label": "Lagre miniatyrbilder", + "description": "Lagre miniatyrbilder sendt til GenAI for feilsøking og inspeksjon." + }, + "send_triggers": { + "label": "GenAI-utløsere", + "description": "Definerer når bilder skal sendes til GenAI (ved slutt, etter oppdateringer, osv.).", + "tracked_object_end": { + "label": "Send ved avslutning", + "description": "Send en forespørsel til GenAI når det sporede objektet avsluttes." + }, + "after_significant_updates": { + "label": "Tidlig GenAI-utløser", + "description": "Send en forespørsel til GenAI etter et spesifisert antall signifikante oppdateringer for det sporede objektet." + } + }, + "enabled_in_config": { + "label": "Opprinnelig GenAI-status", + "description": "Indikerer om GenAI var aktivert i den opprinnelige statiske konfigurasjonen." + } + } + }, + "record": { + "label": "Opptak", + "enabled": { + "label": "Aktiver opptak", + "description": "Aktiver eller deaktiver opptak for dette kameraet." + }, + "expire_interval": { + "label": "Intervall for opprydding av opptak", + "description": "Minutter mellom hver opprydding som fjerner foreldede opptakssegmenter." + }, + "continuous": { + "label": "Kontinuerlig bevaring", + "description": "Antall dager opptak skal bevares uavhengig av sporede objekter eller bevegelse.", + "days": { + "label": "Bevaringsdager", + "description": "Dager opptak skal bevares." + } + }, + "motion": { + "label": "Bevaring ved bevegelse", + "description": "Antall dager opptak utløst av bevegelse skal bevares uavhengig av sporede objekter.", + "days": { + "label": "Bevaringsdager", + "description": "Dager opptak skal bevares." + } + }, + "detections": { + "label": "Bevaring ved deteksjon", + "description": "Innstillinger for bevaring av opptak for deteksjonshendelser, inkludert varighet for forhånds-/etteropptak.", + "pre_capture": { + "label": "Sekunder forhåndsopptak", + "description": "Antall sekunder før deteksjonshendelsen som skal inkluderes i opptaket." + }, + "post_capture": { + "label": "Sekunder etteropptak", + "description": "Antall sekunder etter deteksjonshendelsen som skal inkluderes i opptaket." + }, + "retain": { + "label": "Hendelsesbevaring", + "description": "Bevaringsinnstillinger for opptak av deteksjonshendelser.", + "days": { + "label": "Bevaringsdager", + "description": "Antall dager opptak av deteksjonshendelser skal bevares." + }, + "mode": { + "label": "Bevaringsmodus", + "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)." + } + } + }, + "alerts": { + "label": "Bevaring av varsler", + "description": "Innstillinger for bevaring av opptak for varslingshendelser, inkludert varighet for forhånds-/etteropptak.", + "pre_capture": { + "label": "Sekunder forhåndsopptak", + "description": "Antall sekunder før deteksjonshendelsen som skal inkluderes i opptaket." + }, + "post_capture": { + "label": "Sekunder etteropptak", + "description": "Antall sekunder etter deteksjonshendelsen som skal inkluderes i opptaket." + }, + "retain": { + "label": "Hendelsesbevaring", + "description": "Bevaringsinnstillinger for opptak av deteksjonshendelser.", + "days": { + "label": "Bevaringsdager", + "description": "Antall dager opptak av deteksjonshendelser skal bevares." + }, + "mode": { + "label": "Bevaringsmodus", + "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)." + } + } + }, + "export": { + "label": "Konfigurasjon for eksport", + "description": "Innstillinger som brukes ved eksport av opptak, som for eksempel tidsforløp (timelapse) og maskinvareakselerasjon.", + "hwaccel_args": { + "label": "Argumenter for maskinvareakselerasjon ved eksport", + "description": "Argumenter for maskinvareakselerasjon som skal brukes ved eksport og transkoding." + }, + "max_concurrent": { + "description": "Maksimalt antall eksportjobber som kan behandles samtidig.", + "label": "Maksimalt antall samtidige eksporter" + }, + "chapters": { + "label": "Kapittelmetadata som skal bygges inn i eksporterte opptak" + } + }, + "preview": { + "label": "Konfigurasjon for forhåndsvisning", + "description": "Innstillinger som kontrollerer kvaliteten på forhåndsvisninger av opptak i grensesnittet.", + "quality": { + "label": "Kvalitet på forhåndsvisning", + "description": "Kvalitetsnivå for forhåndsvisning (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Opprinnelig opptaksstatus", + "description": "Indikerer om opptak var aktivert i den opprinnelige statiske konfigurasjonen." + }, + "description": "Innstillinger for opptak og bevaring for dette kameraet." + }, + "review": { + "label": "Inspeksjon", + "alerts": { + "label": "Konfigurasjon for varsler", + "description": "Innstillinger for hvilke sporede objekter som genererer varsler og hvordan disse bevares.", + "enabled": { + "label": "Aktiver varsler", + "description": "Aktiver eller deaktiver generering av varsler for dette kameraet." + }, + "labels": { + "label": "Varslingsetiketter", + "description": "Liste over objektetiketter som kvalifiserer som varsler (for eksempel: bil, person)." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Soner et objekt må tre inn i for å anses som et varsel; la stå tom for å tillate alle soner." + }, + "enabled_in_config": { + "label": "Opprinnelig varslingsstatus", + "description": "Registrerer om varsler opprinnelig var aktivert i den statiske konfigurasjonen." + }, + "cutoff_time": { + "label": "Avskjæringstid for varsler", + "description": "Sekunder å vente etter at varslingsutløsende aktivitet har opphørt før et varsel avsluttes." + } + }, + "detections": { + "label": "Konfigurasjon for deteksjoner", + "description": "Innstillinger for hvilke sporede objekter som genererer deteksjoner (ikke-varsler) og hvordan disse bevares.", + "enabled": { + "label": "Aktiver deteksjoner", + "description": "Aktiver eller deaktiver deteksjonshendelser for dette kameraet." + }, + "labels": { + "label": "Deteksjonsetiketter", + "description": "Liste over objektetiketter som kvalifiserer som deteksjonshendelser." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Soner et objekt må tre inn i for å anses som en deteksjon; la stå tom for å tillate alle soner." + }, + "cutoff_time": { + "label": "Avskjæringstid for deteksjoner", + "description": "Sekunder å vente etter at deteksjonsutløsende aktivitet har opphørt før en deteksjon avsluttes." + }, + "enabled_in_config": { + "label": "Opprinnelig deteksjonsstatus", + "description": "Registrerer om deteksjoner opprinnelig var aktivert i den statiske konfigurasjonen." + } + }, + "genai": { + "label": "GenAI-konfigurasjon", + "description": "Kontrollerer bruk av generativ AI for å produsere beskrivelser og sammendrag av inspeksjonselementer.", + "enabled": { + "label": "Aktiver GenAI-beskrivelser", + "description": "Aktiver eller deaktiver GenAI-genererte beskrivelser og sammendrag for inspeksjonselementer." + }, + "alerts": { + "label": "Aktiver GenAI for varsler", + "description": "Bruk GenAI til å generere beskrivelser for varslingselementer." + }, + "detections": { + "label": "Aktiver GenAI for deteksjoner", + "description": "Bruk GenAI til å generere beskrivelser for deteksjonselementer." + }, + "image_source": { + "label": "Bildekilde for inspeksjon", + "description": "Kilde for bilder sendt til GenAI ('preview' eller 'recordings'); 'recordings' bruker bilder med høyere kvalitet, men flere tokens." + }, + "additional_concerns": { + "label": "Tilleggshensyn", + "description": "En liste over tilleggshensyn eller notater GenAI bør vurdere ved evaluering av aktivitet på dette kameraet." + }, + "debug_save_thumbnails": { + "label": "Lagre miniatyrbilder", + "description": "Lagre miniatyrbilder som sendes til GenAI-leverandøren for feilsøking og inspeksjon." + }, + "enabled_in_config": { + "label": "Opprinnelig GenAI-status", + "description": "Registrerer om GenAI-inspeksjon opprinnelig var aktivert i den statiske konfigurasjonen." + }, + "preferred_language": { + "label": "Foretrukket språk", + "description": "Foretrukket språk som skal etterspørres fra GenAI-leverandøren for genererte svar." + }, + "activity_context_prompt": { + "label": "Ledetekst for aktivitetskontekst", + "description": "Egendefinert ledetekst som beskriver hva som er og ikke er mistenkelig aktivitet for å gi kontekst til GenAI-sammendrag." + } + }, + "description": "Innstillinger for varsler, deteksjoner og GenAI-sammendrag for dette kameraet." + }, + "snapshots": { + "label": "Stillbilder", + "enabled": { + "label": "Aktiver stillbilder", + "description": "Aktiver eller deaktiver lagring av stillbilder for dette kameraet." + }, + "timestamp": { + "label": "Tidsstempel-overlegg", + "description": "Legg et tidsstempel over stillbilder fra API-et." + }, + "bounding_box": { + "label": "Overlegg for markeringsramme", + "description": "Tegn markeringsrammer for sporede objekter på stillbilder fra API-et." + }, + "crop": { + "label": "Beskjær stillbilde", + "description": "Beskjær stillbilder fra API-et til det detekterte objektets markeringsramme." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Soner et objekt må tre inn i for at et stillbilde skal lagres." + }, + "height": { + "label": "Høyde på stillbilde", + "description": "Høyde (piksler) som stillbilder fra API-et skal skaleres til; la stå tom for å beholde opprinnelig størrelse." + }, + "retain": { + "label": "Bevaring av stillbilder", + "description": "Bevaringsinnstillinger for stillbilder, inkludert standard antall dager og overstyringer per objekt.", + "default": { + "label": "Standard bevaring", + "description": "Standard antall dager stillbilder skal bevares." + }, + "mode": { + "label": "Bevaringsmodus", + "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)." + }, + "objects": { + "label": "Objektbevaring", + "description": "Overstyringer per objekt for antall dager stillbilder skal bevares." + } + }, + "quality": { + "label": "Kvalitet på stillbilde", + "description": "Kodingskvalitet for lagrede stillbilder (0-100)." + }, + "description": "Innstillinger for API-genererte stillbilder av sporede objekter for dette kameraet." + }, + "timestamp_style": { + "label": "Stil for tidsstempel", + "position": { + "label": "Posisjon for tidsstempel", + "description": "Posisjonen til tidsstempelet på bildet (tl/tr/bl/br)." + }, + "format": { + "label": "Format for tidsstempel", + "description": "Formatstreng for dato og tid brukt for tidsstempler (Python datetime-formatkoder)." + }, + "color": { + "label": "Farge på tidsstempel", + "description": "RGB-fargeverdier for tidsstempelteksten (alle verdier 0-255).", + "red": { + "label": "Rød", + "description": "Rød komponent (0-255) for tidsstempelfarge." + }, + "green": { + "label": "Grønn", + "description": "Grønn komponent (0-255) for tidsstempelfarge." + }, + "blue": { + "label": "Blå", + "description": "Blå komponent (0-255) for tidsstempelfarge." + } + }, + "thickness": { + "label": "Tykkelse på tidsstempel", + "description": "Linjetykkelsen på tidsstempelteksten." + }, + "effect": { + "label": "Effekt for tidsstempel", + "description": "Visuell effekt for tidsstempelteksten (none, solid, shadow)." + }, + "description": "Stilalternativer for tidsstempler brukt på stillbilder og i Feilsøkingsvisning." + }, + "audio_transcription": { + "label": "Lydtranskripsjon", + "description": "Innstillinger for tale- og lydtranskripsjon i sanntid, brukt for hendelser og teksting.", + "live_enabled": { + "label": "Sanntidstranskripsjon", + "description": "Aktiver løpende transkripsjon av lyd etter hvert som den mottas." + }, + "enabled": { + "description": "Aktiver eller deaktiver manuelt utløst transkripsjon av lydhendelser.", + "label": "Aktiver transkripsjon" + }, + "enabled_in_config": { + "label": "Opprinnelig transkripsjonsstatus" + } + }, + "semantic_search": { + "label": "Semantisk søk", + "triggers": { + "label": "Utløsere", + "description": "Handlinger og kriterier for kameraspesifikke utløsere for semantisk søk.", + "friendly_name": { + "label": "Visningsnavn", + "description": "Valgfritt visningsnavn for denne utløseren i grensesnittet." + }, + "enabled": { + "label": "Aktiver denne utløseren", + "description": "Aktiver eller deaktiver denne utløseren for semantisk søk." + }, + "type": { + "label": "Utløsertype", + "description": "Type utløser: 'miniatyrbilde' (match mot bilde) eller 'beskrivelse' (match mot tekst)." + }, + "data": { + "label": "Utløserinnhold", + "description": "Tekstfrase eller miniatyrbilde-ID som skal matches mot sporede objekter." + }, + "threshold": { + "label": "Utløser-terskel", + "description": "Minimum likhetsscore (0-1) som kreves for å aktivere denne utløseren." + }, + "actions": { + "label": "Utløserhandlinger", + "description": "Liste over handlinger som skal utføres når utløseren matches (varsling, underetikett, egenskap)." + } + }, + "description": "Innstillinger for semantisk søk som bygger og søker i objekt-embeddings for å finne lignende elementer." + }, + "face_recognition": { + "label": "Ansiktsgjenkjenning", + "enabled": { + "label": "Aktiver ansiktsgjenkjenning", + "description": "Aktiver eller deaktiver ansiktsgjenkjenning." + }, + "min_area": { + "label": "Minimum ansiktsareal", + "description": "Minimum areal (piksler) for en ansiktsboks før gjenkjenning forsøkes." + }, + "description": "Innstillinger for ansiktsdeteksjon og gjenkjenning for dette kameraet." + }, + "lpr": { + "label": "Gjenkjenning av kjennemerker", + "description": "Innstillinger for gjenkjenning av kjennemerker, inkludert deteksjonsterskler og kjente kjennemerkeer.", + "enabled": { + "label": "Aktiver skiltgjenkjenning", + "description": "Aktiver eller deaktiver kjennemerkegjenkjenning på dette kameraet." + }, + "min_area": { + "label": "Minimum areal for kjennemerke", + "description": "Minimum areal (piksler) for et kjennemerke før gjenkjenning forsøkes." + }, + "enhancement": { + "label": "Forbedringsnivå", + "description": "Forbedringsnivå (0-10) som brukes på kjennemerkebeskjæringer før OCR; høyere verdier forbedrer ikke alltid resultatet, nivåer over 5 fungerer ofte kun på nattbilder og bør brukes med forsiktighet." + }, + "expire_time": { + "label": "Utløpstid (sekunder)", + "description": "Tid i sekunder før et ukjent kjennemerke foreldes fra sporingen (kun for dedikerte LPR-kameraer)." + } + }, + "profiles": { + "label": "Profiler", + "description": "Navngitte konfigurasjonsprofiler med delvise overstyringer som kan aktiveres i kjøretid." + }, + "onvif": { + "label": "ONVIF", + "description": "ONVIF-tilkobling og innstillinger for PTZ-autosporing for dette kameraet.", + "host": { + "label": "ONVIF-vert", + "description": "Vert (og valgfritt skjema) for ONVIF-tjenesten for dette kameraet." + }, + "port": { + "label": "ONVIF-port", + "description": "Portnummer for ONVIF-tjenesten." + }, + "user": { + "label": "ONVIF-brukernavn", + "description": "Brukernavn for ONVIF-autentisering; enkelte enheter krever admin-bruker for ONVIF." + }, + "password": { + "label": "ONVIF-passord", + "description": "Passord for ONVIF-autentisering." + }, + "tls_insecure": { + "label": "Deaktiver TLS-verifisering", + "description": "Hopp over TLS-verifisering og deaktiver digest-autentisering for ONVIF (usikre; bruk kun i trygge nettverk)." + }, + "profile": { + "label": "ONVIF-profil", + "description": "Spesifikk ONVIF-medieprofil for PTZ-kontroll. Hvis ikke satt, velges den første profilen med gyldig PTZ-konfigurasjon automatisk." + }, + "autotracking": { + "label": "Autosporing", + "description": "Spor bevegelige objekter automatisk og hold dem sentrert ved bruk av PTZ-bevegelser.", + "enabled": { + "label": "Aktiver autosporing", + "description": "Aktiver eller deaktiver automatisk PTZ-sporing av detekterte objekter." + }, + "calibrate_on_startup": { + "label": "Kalibrer ved start", + "description": "Mål PTZ-motorhastigheter ved oppstart for å forbedre sporingsnøyaktighet. Frigate vil oppdatere konfigurasjonen etter kalibrering." + }, + "zooming": { + "label": "Zoom-modus", + "description": "Kontroller zoom-oppførsel: deaktivert, absolutt (mest kompatibel) eller relativ." + }, + "zoom_factor": { + "label": "Zoom-faktor", + "description": "Kontrollere zoom-nivå på sporede objekter. Lavere verdier gir mer oversikt; høyere verdier zoomer tettere inn. Verdier mellom 0.1 og 0.75." + }, + "track": { + "label": "Sporede objekter", + "description": "Liste over objekttyper som skal utløse autosporing." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Objekter må tre inn i en av disse sonene før autosporing starter." + }, + "return_preset": { + "label": "Forhåndsinnstilling for retur", + "description": "Navn på ONVIF-forhåndsinnstilling kameraet skal returnere til når sporingen avsluttes." + }, + "timeout": { + "label": "Tidsavbrudd for retur", + "description": "Antall sekunder å vente etter mistet sporing før kameraet returnerer til forhåndsinnstilt posisjon." + }, + "movement_weights": { + "label": "Bevegelsesvekting", + "description": "Kalibreringsverdier som genereres automatisk. Ikke endre manuelt." + }, + "enabled_in_config": { + "label": "Opprinnelig autosporingsstatus", + "description": "Internt felt for å spore om autosporing var aktivert i konfigurasjonen." + } + }, + "ignore_time_mismatch": { + "label": "Ignorer tidsavvik", + "description": "Ignorer forskjeller i tidssynkronisering mellom kamera og server ved ONVIF-kommunikasjon." + } + }, + "best_image_timeout": { + "description": "Hvor lenge man skal vente på bildet med høyest konfidensscore.", + "label": "Tidsavbrudd for beste bilde" + }, + "enabled": { + "description": "Aktivert", + "label": "Aktivert" + }, + "enabled_in_config": { + "description": "Bevar opprinnelig status for kameraet.", + "label": "Opprinnelig kamerastatus" + }, + "friendly_name": { + "description": "Kameraets visningsnavn i Frigate-grensesnittet", + "label": "Visningsnavn" + }, + "label": "Kamerakonfigurasjon", + "name": { + "description": "Kameranavn er påkrevd", + "label": "Kameranavn" + }, + "type": { + "description": "Kameratype", + "label": "Kameratype" + }, + "ui": { + "dashboard": { + "description": "Velg om dette kameraet skal være synlig i Frigate-grensesnittet. Deaktivering krever manuell redigering av konfigurasjonen for å vise kameraet igjen.", + "label": "Vis i grensesnitt" + }, + "description": "Sortering og synlighet for kameraet i grensesnittet. Påvirker standard dashbord. For mer detaljert kontroll, bruk kameragrupper.", + "label": "Brukergrensesnitt for kamera", + "order": { + "description": "Numerisk rekkefølge for sortering av kameraet i grensesnittet; høyere tall vises senere.", + "label": "Rekkefølge i UI" + }, + "review": { + "description": "Velg om dette kameraet skal være synlig i inspeksjonen (inspeksjonssiden og dens kamerafilter, bevegelsesinspeksjon og historikkvisning).", + "label": "Vis i inspeksjon" + } + }, + "webui_url": { + "description": "URL for å besøke kameraet direkte fra systemsiden", + "label": "Kamera-URL" + }, + "zones": { + "coordinates": { + "label": "Koordinater", + "description": "Polygonkoordinater som definerer soneområdet. Kan være en kommaseparert streng eller en liste med koordinatstrenger. Koordinater bør være relative (0–1) eller absolutte (legacy)." + }, + "description": "Soner lar deg definere spesifikke områder i bildet for å avgjøre om et objekt befinner seg i et bestemt område.", + "distances": { + "label": "Faktiske avstander", + "description": "Valgfrie faktiske avstander for hver side av sonens firkant, brukt til beregning av hastighet eller avstand. Må ha nøyaktig 4 verdier hvis spesifisert." + }, + "enabled": { + "description": "Aktiver eller deaktiver denne sonen. Deaktiverte soner ignoreres i kjøretid.", + "label": "Aktivert" + }, + "enabled_in_config": { + "label": "Bevar opprinnelig status for sonen." + }, + "filters": { + "description": "Filtre for objekter i denne sonen. Brukes for å redusere falske positive eller begrense hvilke objekter som regnes som tilstede.", + "label": "Sonefiltre", + "mask": { + "description": "Polygonkoordinater som definerer hvor dette filteret gjelder innenfor bildet.", + "label": "Filtermaske" + }, + "max_area": { + "label": "Maksimum objektområde", + "description": "Maksimalt areal for markeringsramme (piksler eller prosent) tillatt for denne objekttypen. Kan oppgis i piksler (heltall) eller prosent (desimaltall mellom 0,000001 og 0,99)." + }, + "max_ratio": { + "description": "Maksimalt forhold mellom bredde og høyde tillatt for at markeringsrammen skal kvalifere.", + "label": "Maksimum størrelsesforhold" + }, + "min_area": { + "label": "Minimum objektområde", + "description": "Minimum areal for markeringsramme (piksler eller prosent) som kreves for denne objekttypen." + }, + "min_ratio": { + "description": "Minimum forhold mellom bredde og høyde som kreves for at markeringsrammen skal kvalifisere.", + "label": "Minimum størrelsesforhold" + }, + "min_score": { + "description": "Minimum deteksjonskonfidens for et enkeltbilde som kreves for at objektet skal telles med.", + "label": "Minimum konfidens" + }, + "raw_mask": { + "label": "Råmaske" + }, + "threshold": { + "description": "Gjennomsnittlig terskel for deteksjonskonfidens som kreves for at objektet skal anses som en ekte positiv.", + "label": "Konfidensterskel" + } + }, + "friendly_name": { + "description": "Et brukervennlig navn på sonen som vises i grensesnittet. Hvis ikke satt, brukes en formatert versjon av sonenavnet.", + "label": "Sonenavn" + }, + "inertia": { + "description": "Antall påfølgende bilder et objekt må detekteres i sonen før det regnes som tilstede. Hjelper med å filtrere ut kortvarige feildeteksjoner.", + "label": "Treghetsbilder" + }, + "label": "Soner", + "loitering_time": { + "description": "Antall sekunder et objekt må oppholde seg i sonen for å bli regnet som uønsket opphold (\"loitering\"). Sett til 0 for å deaktivere.", + "label": "Oppholdssekunder" + }, + "objects": { + "label": "Utløsende objekter", + "description": "Liste over objekttyper (fra etikettkartet) som kan utløse denne sonen. Kan være en enkeltstreng eller en liste med strenger. Hvis feltet er tomt, blir alle objekter vurdert." + }, + "speed_threshold": { + "label": "Minimum hastighet", + "description": "Minimumshastighet (i faktiske enheter hvis avstander er satt) som kreves for at et objekt skal regnes som tilstede i sonen. Brukes for hastighets-baserte soneutløsere." + } + } +} diff --git a/web/public/locales/nb-NO/config/global.json b/web/public/locales/nb-NO/config/global.json new file mode 100644 index 0000000..5f50981 --- /dev/null +++ b/web/public/locales/nb-NO/config/global.json @@ -0,0 +1,1642 @@ +{ + "version": { + "label": "Nåværende konfigurasjonsversjon", + "description": "Numerisk eller tekstbasert versjon av den aktive konfigurasjonen for å hjelpe med å oppdage migreringer eller formatendringer." + }, + "safe_mode": { + "label": "Trygg modus", + "description": "Når aktivert, start Frigate i trygg modus med redusert funksjonalitet for feilsøking." + }, + "environment_vars": { + "label": "Miljøvariabler", + "description": "Nøkkel-/verdipar for miljøvariabler som skal settes for Frigate-prosessen i Home Assistant OS. Brukere uten HAOS må bruke miljøvariabelkonfigurasjon i Docker i stedet." + }, + "logger": { + "label": "Logging", + "description": "Kontrollerer standard loggdetaljnivå og overstyringer av loggnivå per komponent.", + "default": { + "label": "Loggenivå", + "description": "Standard globale loggedetaljer (debug, info, warning, error)." + }, + "logs": { + "label": "Loggnivå per prosess", + "description": "Overstyringer av loggnivå per komponent for å øke eller redusere detaljrikdommen for spesifikke moduler." + } + }, + "auth": { + "label": "Autentisering", + "description": "Innstillinger for autentisering og økter, inkludert alternativer for informasjonskapsler (cookies) og hastighetsbegrensning.", + "enabled": { + "label": "Aktiver autentisering", + "description": "Aktiver innebygd autentisering for Frigate-grensesnittet." + }, + "reset_admin_password": { + "label": "Nullstill admin-passord", + "description": "Hvis sann, nullstill admin-brukerens passord ved oppstart og skriv ut det nye passordet i loggen." + }, + "cookie_name": { + "label": "Navn på JWT-informasjonskapsel", + "description": "Navnet på informasjonskapselen som brukes til å lagre JWT-tokenet for innebygd autentisering." + }, + "cookie_secure": { + "label": "Flagg for sikker informasjonskapsel", + "description": "Sett \"secure\"-flagget på autentiseringskapselen; bør være sann ved bruk av TLS." + }, + "session_length": { + "label": "Øktvarighet", + "description": "Varighet på økten i sekunder for JWT-baserte økter." + }, + "refresh_time": { + "label": "Vindu for øktfornyelse", + "description": "Når en økt har så mange sekunder igjen før den utløper, fornyes den til full lengde." + }, + "failed_login_rate_limit": { + "label": "Begrensninger for mislykkede pålogginger", + "description": "Regler for hastighetsbegrensning for mislykkede påloggingsforsøk for å redusere brute-force-angrep." + }, + "trusted_proxies": { + "label": "Betrodde proxyer", + "description": "Liste over betrodde proxy-IP-er som brukes ved fastsettelse av klient-IP for hastighetsbegrensning." + }, + "hash_iterations": { + "label": "Hash-iterasjoner", + "description": "Antall PBKDF2-SHA256-iterasjoner som skal brukes ved hashing av brukerpassord." + }, + "roles": { + "label": "Rolletilordninger", + "description": "Tilordne roller til kameralister. En tom liste gir tilgang til alle kameraer for rollen." + }, + "admin_first_time_login": { + "label": "Flagg for førstegangs admin-innlogging", + "description": "Når sann, kan grensesnittet vise en hjelpelenke på påloggingssiden som informerer brukere om hvordan de logger inn etter en nullstilling av admin-passordet. " + } + }, + "database": { + "label": "Database", + "description": "Innstillinger for SQLite-databasen som brukes av Frigate til å lagre sporede objekter og metadata for opptak.", + "path": { + "label": "Sti til database", + "description": "Sti i filsystemet der Frigates SQLite-databasefil vil bli lagret." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "Innstillinger for den integrerte go2rtc-tjenesten for videreformidling og oversettelse av direktestrømmer." + }, + "mqtt": { + "label": "MQTT", + "description": "Innstillinger for tilkobling og publisering av telemetri, stillbilder og hendelsesdetaljer til en MQTT-megler.", + "enabled": { + "label": "Aktiver MQTT", + "description": "Aktiver eller deaktiver MQTT-integrasjon for status, hendelser og stillbilder." + }, + "host": { + "label": "MQTT-vert", + "description": "Vertsnavn eller IP-adresse til MQTT-megleren." + }, + "port": { + "label": "MQTT-port", + "description": "Port til MQTT-megleren (vanligvis 1883 for vanlig MQTT)." + }, + "topic_prefix": { + "label": "Emne-prefiks", + "description": "MQTT-emneprefiks for alle Frigate-emner; må være unikt hvis man kjører flere instanser." + }, + "client_id": { + "label": "Klient-ID", + "description": "Klientidentifikator som brukes ved tilkobling til MQTT-megleren; bør være unik per instans." + }, + "stats_interval": { + "label": "Statistikkintervall", + "description": "Intervall i sekunder for publisering av system- og kamerastatistikk til MQTT." + }, + "user": { + "label": "MQTT-brukernavn", + "description": "Valgfritt MQTT-brukernavn; kan oppgis via miljøvariabler eller hemmeligheter (secrets)." + }, + "password": { + "label": "MQTT-passord", + "description": "Valgfritt MQTT-passord; kan oppgis via miljøvariabler eller hemmeligheter (secrets)." + }, + "tls_ca_certs": { + "label": "TLS CA-sertifikater", + "description": "Sti til CA-sertifikat for TLS-tilkoblinger til megleren (for selvsignerte sertifikater)." + }, + "tls_client_cert": { + "label": "Klientsertifikat", + "description": "Sti til klientsertifikat for gjensidig TLS-autentisering; ikke sett brukernavn/passord når klientsertifikater brukes." + }, + "tls_client_key": { + "label": "Klientnøkkel", + "description": "Sti til privat nøkkel for klientsertifikatet." + }, + "tls_insecure": { + "label": "Usikker TLS", + "description": "Tillat usikre TLS-tilkoblinger ved å hoppe over verifisering av vertsnavn (ikke anbefalt)." + }, + "qos": { + "label": "MQTT QoS", + "description": "Quality of Service-nivå for MQTT-publiseringer/abonnementer (0, 1 eller 2)." + } + }, + "notifications": { + "label": "Varslinger", + "description": "Innstillinger for å aktivere og kontrollere varslinger for alle kameraer; kan overstyres per kamera.", + "enabled": { + "label": "Aktiver varslinger", + "description": "Aktiver eller deaktiver varslinger for alle kameraer; kan overstyres per kamera." + }, + "email": { + "label": "E-postadresse for varsling", + "description": "E-postadresse som brukes for push-varslinger eller som kreves av visse varslingstjenester." + }, + "cooldown": { + "label": "Nedkjølingsperiode", + "description": "Nedkjøling (sekunder) mellom varslinger for å unngå å spamme mottakere." + }, + "enabled_in_config": { + "label": "Opprinnelig varslingsstatus", + "description": "Indikerer om varslinger var aktivert i den opprinnelige statiske konfigurasjonen." + } + }, + "networking": { + "label": "Nettverk", + "description": "Nettverksrelaterte innstillinger, som aktivering av IPv6 for Frigate-endepunkter.", + "ipv6": { + "label": "IPv6-konfigurasjon", + "description": "IPv6-spesifikke innstillinger for Frigates nettverkstjenester.", + "enabled": { + "label": "Aktiver IPv6", + "description": "Aktiver IPv6-støtte for Frigate-tjenester (API og brukergrensesnitt) der det er aktuelt." + } + }, + "listen": { + "label": "Konfigurasjon for lytteporter", + "description": "Konfigurasjon for interne og eksterne lytteporter. Dette er for avanserte brukere. For de fleste brukstilfeller anbefales det å endre port-seksjonen i din Docker compose-fil i stedet.", + "internal": { + "label": "Intern port", + "description": "Intern lytteport for Frigate (standard 5000)." + }, + "external": { + "label": "Ekstern port", + "description": "Ekstern lytteport for Frigate (standard 8971)." + } + } + }, + "proxy": { + "label": "Proxy", + "description": "Innstillinger for å integrere Frigate bak en reverse proxy som sender videre hoder for autentiserte brukere.", + "header_map": { + "label": "Tilordning av hoder (Header mapping)", + "description": "Tilordne innkommende proxy-hoder til Frigates bruker- og rollefelt for proxy-basert autentisering.", + "user": { + "label": "Bruker-hode (User header)", + "description": "Hode (header) som inneholder det autentiserte brukernavnet fra oppstrøms proxy." + }, + "role": { + "label": "Rolle-hode (Role header)", + "description": "Hode (header) som inneholder den autentiserte brukerens rolle eller grupper fra oppstrøms proxy." + }, + "role_map": { + "label": "Rolletilordning", + "description": "Tilordne gruppeverdier fra oppstrøms proxy til Frigate-roller (for eksempel tilordne admin-grupper til admin-rollen)." + } + }, + "logout_url": { + "label": "Utloggings-URL", + "description": "URL som brukere skal videresendes til ved utlogging via proxyen." + }, + "auth_secret": { + "label": "Proxy-hemmelighet", + "description": "Valgfri hemmelighet som sjekkes mot X-Proxy-Secret-hodet for å verifisere betrodde proxyer." + }, + "default_role": { + "label": "Standardrolle", + "description": "Standardrolle tildelt proxy-autentiserte brukere når ingen rolletilordning gjelder." + }, + "separator": { + "label": "Skilletegn", + "description": "Tegn som brukes til å dele opp flere verdier i proxy-hoder." + } + }, + "telemetry": { + "label": "Telemetri", + "description": "Alternativer for systemtelemetri og statistikk, inkludert overvåking av GPU og nettverksbåndbredde.", + "network_interfaces": { + "label": "Nettverksgrensesnitt", + "description": "Liste over prefikser for navn på nettverksgrensesnitt som skal overvåkes for båndbreddestatistikk." + }, + "stats": { + "label": "Systemstatistikk", + "description": "Alternativer for å aktivere/deaktivere innsamling av ulike system- og GPU-statistikker.", + "amd_gpu_stats": { + "label": "AMD GPU-statistikk", + "description": "Aktiver innsamling av AMD GPU-statistikk hvis en AMD GPU er til stede." + }, + "intel_gpu_stats": { + "label": "Intel GPU-statistikk", + "description": "Aktiver innsamling av Intel GPU-statistikk hvis en Intel GPU er til stede." + }, + "network_bandwidth": { + "label": "Nettverksbåndbredde", + "description": "Aktiver overvåking av nettverksbåndbredde per prosess for kamera-ffmpeg-prosesser og detektorer." + }, + "intel_gpu_device": { + "label": "Intel GPU-enhet", + "description": "Enhetsidentifikator som brukes når Intel-GPU-er behandles som SR-IOV for å korrigere GPU-statistikk." + } + }, + "version_check": { + "label": "Versjonskontroll", + "description": "Aktiver en utgående sjekk for å oppdage om en nyere versjon av Frigate er tilgjengelig." + } + }, + "tls": { + "label": "TLS", + "description": "TLS-innstillinger for Frigates web-endepunkter (port 8971).", + "enabled": { + "label": "Aktiver TLS", + "description": "Aktiver TLS for Frigates web-grensesnitt og API på den konfigurerte TLS-porten." + } + }, + "ui": { + "label": "Brukergrensesnitt", + "description": "Innstillinger for brukergrensesnitt, som tidssone, formatering av tid/dato og enheter.", + "timezone": { + "label": "Tidssone", + "description": "Valgfri tidssone som skal vises i grensesnittet (standard er nettleserens lokale tid)." + }, + "time_format": { + "label": "Tidsformat", + "description": "Tidsformat som skal brukes i grensesnittet (nettleser, 12-timers eller 24-timers)." + }, + "date_style": { + "label": "Datostil", + "description": "Datostil som skal brukes i grensesnittet (full, lang, middels, kort)." + }, + "time_style": { + "label": "Tidsstil", + "description": "Tidsstil som skal brukes i grensesnittet (full, lang, middels, kort)." + }, + "unit_system": { + "label": "Enhetssystem", + "description": "Enhetssystem for visning (metrisk eller imperisk) brukt i grensesnittet og MQTT." + } + }, + "detectors": { + "label": "Maskinvare for detektor", + "description": "Konfigurasjon for objektdetektorer (CPU, GPU, ONNX-bakender) og eventuelle detektorspesifikke modellinnstillinger.", + "type": { + "label": "Type" + }, + "model": { + "label": "Detektorspesifikk modellkonfigurasjon", + "description": "Detektorspesifikke konfigurasjonsalternativer for modell (sti, inndatastørrelse, osv.).", + "path": { + "label": "Sti til egendefinert objektdetektormodell", + "description": "Sti til en egendefinert deteksjonsmodellfil (eller plus:// for Frigate+-modeller)." + }, + "labelmap_path": { + "label": "Etikettkart (labelmap) for egendefinert detektor", + "description": "Sti til en labelmap-fil som kobler numeriske klasser til tekstetiketter for detektoren." + }, + "width": { + "label": "Inndatabredde for deteksjonsmodell", + "description": "Bredden på modellens inndata-tensor i piksler." + }, + "height": { + "label": "Inndatahøyde for deteksjonsmodell", + "description": "Høyden på modellens inndata-tensor i piksler." + }, + "labelmap": { + "label": "Tilpasning av etikettkart (labelmap)", + "description": "Overstyringer eller tilordninger som skal flettes inn i standard etikettkart." + }, + "attributes_map": { + "label": "Kartlegging av objektetiketter til deres egenskaper", + "description": "Kobling fra objektetiketter til egenskapsetiketter brukt for metadata (f.eks. 'bil' -> ['kjennemerke'])." + }, + "input_tensor": { + "label": "Modellens inndata-tensorform", + "description": "Tensorformatet som forventes av modellen: 'nhwc' eller 'nchw'." + }, + "input_pixel_format": { + "label": "Pikselformat for modellens inndata", + "description": "Pikselfargerom som forventes av modellen: 'rgb', 'bgr' eller 'yuv'." + }, + "input_dtype": { + "label": "Datatype for modellens inndata", + "description": "Datatypen til modellens inndata-tensor (for eksempel 'float32')." + }, + "model_type": { + "label": "Type objektdeteksjonsmodell", + "description": "Arkitekturtype for detektormodellen (ssd, yolox, yolonas) brukt av enkelte detektorer for optimalisering." + } + }, + "model_path": { + "label": "Detektorspesifikk modellsti", + "description": "Filsti til detektormodellens binærfil hvis påkrevd av valgt detektor." + }, + "axengine": { + "label": "AXEngine NPU", + "description": "AXERA AX650N/AX8850N NPU-detektor som kjører kompilerte .axmodel-filer via AXEngine-miljøet." + }, + "cpu": { + "label": "CPU", + "description": "CPU TFLite-detektor som kjører TensorFlow Lite-modeller på vertens CPU uten maskinvareakselerasjon. Ikke anbefalt.", + "num_threads": { + "label": "Antall deteksjonstråder", + "description": "Antallet tråder som brukes til CPU-basert inferens." + } + }, + "deepstack": { + "label": "DeepStack", + "description": "DeepStack/CodeProject.AI-detektor som sender bilder til et eksternt DeepStack HTTP-API for inferens. Ikke anbefalt.", + "api_url": { + "label": "URL for DeepStack-API", + "description": "URL-adressen til DeepStack-API-et." + }, + "api_timeout": { + "label": "Tidsavbrudd for DeepStack-API (i sekunder)", + "description": "Maksimal tid tillatt for en forespørsel til DeepStack-API-et." + }, + "api_key": { + "label": "DeepStack-API-nøkkel (hvis påkrevd)", + "description": "Valgfri API-nøkkel for autentiserte DeepStack-tjenester." + } + }, + "degirum": { + "label": "DeGirum", + "description": "DeGirum-detektor for kjøring av modeller via DeGirum-skyen eller lokale inferens-tjenester.", + "location": { + "label": "Inferenslokasjon", + "description": "Lokasjonen til DeGirim-inferensmotoren (f.eks. '@cloud', '127.0.0.1')." + }, + "zoo": { + "label": "Modell-zoo", + "description": "Sti eller URL til DeGirum modell-zoo." + }, + "token": { + "label": "DeGirum-skytoken", + "description": "Token for tilgang til DeGirum-skyen." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "EdgeTPU-detektor som kjører TensorFlow Lite-modeller kompilert for Coral EdgeTPU ved bruk av EdgeTPU-delegaten.", + "device": { + "label": "Enhetstype", + "description": "Enheten som skal brukes til EdgeTPU-inferens (f.eks. 'usb', 'pci')." + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "Hailo-8/Hailo-8L-detektor som bruker HEF-modeller og HailoRT SDK for inferens på Hailo-maskinvare.", + "device": { + "label": "Enhetstype", + "description": "Enheten som skal brukes til Hailo-inferens (f.eks. 'PCIe', 'M.2')." + } + }, + "memryx": { + "label": "MemryX", + "description": "MemryX MX3-detektor som kjører kompilerte DFP-modeller på MemryX-akseleratorer.", + "device": { + "label": "Enhetssti", + "description": "Enheten som skal brukes for MemryX-inferens (f.eks. 'PCIe')." + } + }, + "onnx": { + "label": "ONNX", + "description": "ONNX-detektor for kjøring av ONNX-modeller; bruker tilgjengelige akselerasjonsbakender (CUDA/ROCm/OpenVINO) når de er tilgjengelige.", + "device": { + "label": "Enhetstype", + "description": "Enheten som skal brukes for ONNX-inferens (f.eks. 'AUTO', 'CPU', 'GPU')." + } + }, + "openvino": { + "label": "OpenVINO", + "description": "OpenVINO-detektor for AMD- og Intel-CPU-er, Intel-GPU-er og Intel VPU-maskinvare.", + "device": { + "label": "Enhetstype", + "description": "Enheten som skal brukes for OpenVINO-inferens (f.eks. 'CPU', 'GPU', 'NPU')." + } + }, + "rknn": { + "label": "RKNN", + "description": "RKNN-detektor for Rockchip NPU-er; kjører kompilerte RKNN-modeller på Rockchip-maskinvare.", + "num_cores": { + "label": "Antall NPU-kjerner som skal brukes.", + "description": "Antall NPU-kjerner som skal brukes (0 for auto)." + } + }, + "synaptics": { + "label": "Synaptics", + "description": "Synaptics NPU-detektor for modeller i .synap-format ved bruk av Synap SDK på Synaptics-maskinvare." + }, + "teflon_tfl": { + "label": "Teflon", + "description": "Teflon-delegatdetektor for TFLite som bruker Mesa Teflon-delegatbiblioteket for å akselerere inferens på støttede GPU-er." + }, + "tensorrt": { + "label": "TensorRT", + "description": "TensorRT-detektor for Nvidia Jetson-enheter som bruker serialiserte TensorRT-motorer for akselerert inferens.", + "device": { + "label": "GPU-enhetsindeks", + "description": "GPU-enhetsindeksen som skal brukes." + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "ZMQ IPC-detektor som flytter inferenssprosesser til en ekstern prosess via et ZeroMQ IPC-endepunkt.", + "endpoint": { + "label": "ZMQ IPC-endepunkt", + "description": "ZMQ-endepunktet som skal tilkobles." + }, + "request_timeout_ms": { + "label": "Tidsavbrudd for ZMQ-forespørsel i millisekunder", + "description": "Tidsavbrudd for ZMQ-forespørsler i millisekunder." + }, + "linger_ms": { + "label": "ZMQ-sokkel ventetid i millisekunder", + "description": "Sokkelens ventetid i millisekunder." + } + } + }, + "model": { + "label": "Deteksjonsmodell", + "description": "Innstillinger for å konfigurere en egendefinert objektdeteksjonsmodell og dens inndataform.", + "path": { + "label": "Sti til egendefinert objektdeteksjonsmodell", + "description": "Sti til en egendefinert deteksjonsmodellfil (eller plus:// for Frigate+-modeller)." + }, + "labelmap_path": { + "label": "Etikettkart (labelmap) for egendefinert detektor", + "description": "Sti til en labelmap-fil som kobler numeriske klasser til tekstetiketter for detektoren." + }, + "width": { + "label": "Inndatabredde for deteksjonsmodell", + "description": "Bredden på modellens inndata-tensor i piksler." + }, + "height": { + "label": "Inndatahøyde for deteksjonsmodell", + "description": "Høyden på modellens inndata-tensor i piksler." + }, + "labelmap": { + "label": "Tilpasning av etikettkart (labelmap)", + "description": "Overstyringer eller tilordninger som skal flettes inn i standard etikettkart." + }, + "attributes_map": { + "label": "Kartlegging av objektetiketter til deres egenskaper", + "description": "Kobling fra objektetiketter til egenskapsetiketter brukt for metadata (f.eks. 'bil' -> ['kjennemerke'])." + }, + "input_tensor": { + "label": "Modellens inndata-tensorform", + "description": "Tensorformatet som forventes av modellen: 'nhwc' eller 'nchw'." + }, + "input_pixel_format": { + "label": "Pikselformat for modellens inndata", + "description": "Pikselfargerom som forventes av modellen: 'rgb', 'bgr' eller 'yuv'." + }, + "input_dtype": { + "label": "Datatype for modellens inndata", + "description": "Datatypen til modellens inndata-tensor (for eksempel 'float32')." + }, + "model_type": { + "label": "Type objektdeteksjonsmodell", + "description": "Arkitekturtype for detektormodellen (ssd, yolox, yolonas) brukt av enkelte detektorer for optimalisering." + } + }, + "genai": { + "label": "Konfigurasjon for generativ AI", + "description": "Innstillinger for integrerte generativ AI-leverandører brukt til å generere objektbeskrivelser og inspeksjonssammendrag.", + "api_key": { + "label": "API-nøkkel", + "description": "API-nøkkel som kreves av enkelte leverandører (kan også settes via miljøvariabler)." + }, + "base_url": { + "label": "Basis-URL", + "description": "Basis-URL for selvhostede eller kompatible leverandører (for eksempel en Ollama-instans)." + }, + "model": { + "label": "Modell", + "description": "Modellen som skal brukes fra leverandøren for å generere beskrivelser eller sammendrag." + }, + "provider": { + "label": "Leverandør", + "description": "GenAI-leverandøren som skal brukes (for eksempel: ollama, gemini, openai)." + }, + "roles": { + "label": "Roller", + "description": "GenAI-roller (chat, beskrivelser, vektorrepresentasjoner); én leverandør per rolle." + }, + "provider_options": { + "label": "Leverandøralternativer", + "description": "Ekstra leverandørspesifikke alternativer som sendes til GenAI-klienten." + }, + "runtime_options": { + "label": "Kjøretidsalternativer", + "description": "Kjøretidsalternativer som sendes til leverandøren for hver inferenssforespørsel." + } + }, + "audio": { + "label": "Lyddeteksjon", + "description": "Innstillinger for lydbasert hendelsesdeteksjon for alle kameraer; kan overstyres per kamera.", + "enabled": { + "label": "Aktiver lyddeteksjon", + "description": "Aktiver eller deaktiver lydhendelsesdeteksjon for alle kameraer; kan overstyres per kamera." + }, + "max_not_heard": { + "label": "Tidsavbrudd for avslutning", + "description": "Antall sekunder uten den konfigurerte lydtypen før lydhendelsen avsluttes." + }, + "min_volume": { + "label": "Minimumsvolum", + "description": "Minimum terskel for RMS-volum som kreves for å kjøre lyddeteksjon; lavere verdier øker følsomheten (f.eks. 200 høy, 500 middels, 1000 lav)." + }, + "listen": { + "label": "Lyttetyper", + "description": "Liste over typer lydhendelser som skal detekteres (f.eks. bjeff, brannalarm, tale, rop)." + }, + "filters": { + "label": "Lydfiltre", + "description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive.", + "threshold": { + "description": "Minimum konfidens-terskel for at lydhendelsen skal bli registrert.", + "label": "Minimum konfidens for lyd" + } + }, + "enabled_in_config": { + "label": "Opprinnelig lydstatus", + "description": "Indikerer om lyddeteksjon opprinnelig var aktivert i den statiske konfigurasjonsfilen." + }, + "num_threads": { + "label": "Deteksjonstråder", + "description": "Antall tråder som skal brukes til prosessering av lyddeteksjon." + } + }, + "birdseye": { + "label": "Fugleperspektiv", + "description": "Innstillinger for Fugleperspektiv (Birdseye) som setter sammen flere kamerastrømmer til ett felles oppsett.", + "enabled": { + "label": "Aktiver Fugleperspektiv", + "description": "Aktiver eller deaktiver funksjonen for Fugleperspektiv." + }, + "mode": { + "label": "Sporingsmodus", + "description": "Modus for å inkludere kameraer i Fugleperspektiv: 'objects', 'motion' eller 'continuous'." + }, + "restream": { + "label": "Videreformidle RTSP", + "description": "Videreformidle Fugleperspektiv-utdataen som en RTSP-strøm; aktivering av dette vil holde Fugleperspektiv kjørende kontinuerlig." + }, + "width": { + "label": "Bredde", + "description": "Utgangsbredde (piksler) for det sammensatte Fugleperspektiv-bildet." + }, + "height": { + "label": "Høyde", + "description": "Utgangshøyde (piksler) for det sammensatte Fugleperspektiv-bildet." + }, + "quality": { + "label": "Kodingskvalitet", + "description": "Kodingskvalitet for mpeg1-strømmen i Fugleperspektiv (1 høyest kvalitet, 31 lavest)." + }, + "inactivity_threshold": { + "label": "Terskel for inaktivitet", + "description": "Sekunder med inaktivitet før et kamera slutter å vises i Fugleperspektiv." + }, + "layout": { + "label": "Oppsett", + "description": "Oppsettsalternativer for Fugleperspektiv-sammensetningen.", + "scaling_factor": { + "label": "Skaleringsfaktor", + "description": "Skaleringsfaktor brukt av oppsettskalkulatoren (intervall 1.0 til 5.0)." + }, + "max_cameras": { + "label": "Maks antall kameraer", + "description": "Maksimalt antall kameraer som vises samtidig i Fugleperspektiv; viser de nyeste kameraene." + } + }, + "idle_heartbeat_fps": { + "label": "FPS for hjerteslag ved inaktivitet", + "description": "Bilder per sekund (FPS) for å sende det siste sammensatte Fugleperspektiv-bildet på nytt ved inaktivitet; sett til 0 for å deaktivere." + }, + "order": { + "label": "Posisjon", + "description": "Numerisk posisjon som kontrollerer kameraenes rekkefølge i Fugleperspektiv-oppsettet." + } + }, + "detect": { + "label": "Objektdeteksjon", + "description": "Innstillinger for deteksjonsrollen brukt til å kjøre objektdeteksjon og starte sporing (trackere).", + "enabled": { + "label": "Aktiver objektdeteksjon", + "description": "Aktiver eller deaktiver objektdeteksjon for alle kameraer; kan overstyres per kamera." + }, + "height": { + "label": "Deteksjonshøyde", + "description": "Høyde (piksler) på bilder brukt for deteksjonsstrømmen; la stå tom for å bruke strømmens opprinnelige oppløsning." + }, + "width": { + "label": "Deteksjonsbredde", + "description": "Bredde (piksler) på bilder brukt for deteksjonsstrømmen; la stå tom for å bruke strømmens opprinnelige oppløsning." + }, + "fps": { + "label": "Deteksjons-FPS", + "description": "Ønsket antall bilder per sekund (FPS) for deteksjon; lavere verdier reduserer CPU-bruk (anbefalt verdi er 5, sett kun høyere – maks 10 – ved sporing av objekter i svært høy fart)." + }, + "min_initialized": { + "label": "Minimum initialiseringsbilder", + "description": "Antall påfølgende deteksjonstreff som kreves før et sporet objekt opprettes. Øk for å redusere falske initialiseringer. Standardverdi er FPS delt på 2." + }, + "max_disappeared": { + "label": "Maks bilder borte", + "description": "Antall bilder uten deteksjon før et sporet objekt anses som borte." + }, + "stationary": { + "label": "Konfigurasjon for stasjonære objekter", + "description": "Innstillinger for å detektere og håndtere objekter som forblir i ro over en viss tid.", + "interval": { + "label": "Intervall for stasjonære objekter", + "description": "Hvor ofte (i antall bilder) det skal kjøres en deteksjonssjekk for å bekrefte et stasjonært objekt." + }, + "threshold": { + "label": "Terskel for stasjonære objekter", + "description": "Antall bilder uten posisjonsendring som kreves for å markere et objekt som stasjonært." + }, + "max_frames": { + "label": "Maks antall bilder", + "description": "Begrenser hvor lenge stasjonære objekter spores før de forkastes.", + "default": { + "label": "Standard maks bilder", + "description": "Standard maksimalt antall bilder et stasjonært objekt spores før det stoppes." + }, + "objects": { + "label": "Maks bilder per objekt", + "description": "Overstyringer per objekttype for maksimalt antall bilder stasjonære objekter skal spores." + } + }, + "classifier": { + "label": "Aktiver visuell klassifiserer", + "description": "Bruk en visuell klassifiserer for å detektere reelt stasjonære objekter selv når markeringsrammene \"skjelver\" (jitter)." + } + }, + "annotation_offset": { + "label": "Forskyvning av annotering", + "description": "Millisekunder for å forskyve deteksjonsannoteringer for bedre samsvar mellom markeringsrammer på tidslinjen og opptakene; kan være positiv eller negativ." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg-innstillinger, inkludert sti til binærfil, argumenter, alternativer for maskinvareakselerasjon og utdata-argumenter per rolle.", + "path": { + "label": "FFmpeg-sti", + "description": "Sti til FFmpeg-binærfilen som skal brukes, eller et versjonsalias (\"7.0\" eller \"8.0\")." + }, + "global_args": { + "label": "Globale FFmpeg-argumenter", + "description": "Globale argumenter som sendes til FFmpeg-prosesser." + }, + "hwaccel_args": { + "label": "Argumenter for maskinvareakselerasjon", + "description": "Argumenter for maskinvareakselerasjon i FFmpeg. Leverandørspesifikke forhåndsinnstillinger anbefales." + }, + "input_args": { + "label": "Inndata-argumenter", + "description": "Inndata-argumenter som brukes på FFmpeg-innstrømmer." + }, + "output_args": { + "label": "Utdata-argumenter", + "description": "Standard utdata-argumenter brukt for ulike FFmpeg-roller som deteksjon og opptak.", + "detect": { + "label": "Utdata-argumenter for deteksjon", + "description": "Standard utdata-argumenter for strømmer med deteksjonsrolle." + }, + "record": { + "label": "Utdata-argumenter for opptak", + "description": "Standard utdata-argumenter for strømmer med opptaksrolle." + } + }, + "retry_interval": { + "label": "FFmpeg-forsøksintervall", + "description": "Sekunder å vente før man prøver å koble til en kamerastrøm på nytt etter feil. Standard er 10." + }, + "apple_compatibility": { + "label": "Apple-kompatibilitet", + "description": "Aktiver HEVC-tagging for bedre kompatibilitet med Apple-avspillere ved opptak i H.265." + }, + "gpu": { + "label": "GPU-indeks", + "description": "Standard GPU-indeks som brukes til maskinvareakselerasjon hvis tilgjengelig." + }, + "inputs": { + "label": "Kamerainndata", + "description": "Liste over definisjoner for inndatastrømmer (stier og roller) for dette kameraet.", + "path": { + "label": "Inndatasti", + "description": "URL eller sti for kameraets inndatastrøm." + }, + "roles": { + "label": "Inndataroller", + "description": "Roller for denne inndatastrømmen." + }, + "global_args": { + "label": "Globale FFmpeg-argumenter", + "description": "Globale FFmpeg-argumenter for denne inndatastrømmen." + }, + "hwaccel_args": { + "label": "Argumenter for maskinvareakselerasjon", + "description": "Argumenter for maskinvareakselerasjon for denne inndatastrømmen." + }, + "input_args": { + "label": "Inndata-argumenter", + "description": "Inndata-argumenter spesifisert for denne strømmen." + } + } + }, + "live": { + "label": "Direkteavspilling", + "description": "Innstillinger for å kontrollere oppløsning og kvalitet på jsmpeg-direktestrømmer. Dette påvirker ikke kameraer som bruker go2rtc for direktevisning.", + "streams": { + "label": "Navn på direktestrømmer", + "description": "Kobling mellom konfigurerte strøm-navn og restream/go2rtc-navn brukt for direkteavspilling." + }, + "height": { + "label": "Direktevisningshøyde", + "description": "Høyde (piksler) for jsmpeg-direktestrømmen i web-grensesnittet; må være <= høyden på deteksjonsstrømmen." + }, + "quality": { + "label": "Direktevisningskvalitet", + "description": "Kodingskvalitet for jsmpeg-strømmen (1 høyest, 31 lavest)." + } + }, + "motion": { + "label": "Bevegelsesdeteksjon", + "description": "Standardinnstillinger for bevegelsesdeteksjon som gjelder for kameraer med mindre de overstyres per kamera.", + "enabled": { + "label": "Aktiver bevegelsesdeteksjon", + "description": "Aktiver eller deaktiver bevegelsesdeteksjon for alle kameraer; kan overstyres per kamera." + }, + "threshold": { + "label": "Terskel for bevegelse", + "description": "Terskel for pikselendring brukt av bevegelsesdetektoren; høyere verdier reduserer følsomheten (intervall 1–255)." + }, + "lightning_threshold": { + "label": "Terskel for lyn/lysglimt", + "description": "Terskel for å oppdage og ignorere korte lysglimt (lavere er mer følsom, verdier mellom 0,3 og 1,0). Dette stopper ikke bevegelsesdeteksjon helt; det fører bare til at detektoren slutter å analysere flere bilder når terskelen er nådd. Bevegelsesbaserte opptak blir fortsatt laget under slike hendelser." + }, + "skip_motion_threshold": { + "label": "Terskel for å hoppe over bevegelse", + "description": "Hvis satt til en verdi mellom 0,0 og 1,0, og mer enn denne andelen av bildet endres i ett enkelt bilde, vil detektoren ikke returnere noen bevegelsesbokser og kalibrere på nytt umiddelbart. Dette kan spare CPU og redusere falske positive under lyn, storm, osv., men kan gå glipp av ekte hendelser som at et PTZ-kamera autosporer et objekt. Avveiningen står mellom å miste noen megabyte med opptak mot å måtte se gjennom et par korte klipp. La stå tom (None) for å deaktivere denne funksjonen." + }, + "improve_contrast": { + "label": "Forbedre kontrast", + "description": "Bruk kontrastforbedring på bilder før bevegelsesanalyse for å hjelpe deteksjonen." + }, + "contour_area": { + "label": "Konturområde", + "description": "Minimum konturområde i piksler som kreves for at en bevegelseskontur skal telles med." + }, + "delta_alpha": { + "label": "Delta alfa", + "description": "Alfa-blandingsfaktor brukt i bildedifferensiering for bevegelsesberegning." + }, + "frame_alpha": { + "label": "Bilde-alfa", + "description": "Alfa-verdi brukt ved sammenfletting av bilder for forhåndsbehandling av bevegelse." + }, + "frame_height": { + "label": "Bildehøyde", + "description": "Høyde i piksler som bildene skal skaleres til ved beregning av bevegelse." + }, + "mask": { + "label": "Maskekoordinater", + "description": "Sorterte x,y-koordinater som definerer polygonet for bevegelsesmasken brukt til å inkludere/ekskludere områder." + }, + "mqtt_off_delay": { + "label": "MQTT-av-forsinkelse", + "description": "Sekunder å vente etter siste bevegelse før en MQTT 'av'-status publiseres." + }, + "enabled_in_config": { + "label": "Opprinnelig bevegelsesstatus", + "description": "Indikerer om bevegelsesdeteksjon var aktivert i den opprinnelige statiske konfigurasjonen." + }, + "raw_mask": { + "label": "Råmaske" + } + }, + "objects": { + "label": "Objekter", + "description": "Standardinnstillinger for objektsporing, inkludert hvilke etiketter som skal spores og filtre per objekt.", + "track": { + "label": "Objekter som skal spores", + "description": "Liste over objektetiketter som skal spores for alle kameraer; kan overstyres per kamera." + }, + "filters": { + "label": "Objektfiltre", + "description": "Filtre som brukes på detekterte objekter for å redusere falske positive (område, forhold, konfidens).", + "min_area": { + "label": "Minimum objektområde", + "description": "Minimum areal for markeringsramme (piksler eller prosent) som kreves for denne objekttypen. Kan oppgis i piksler (heltall) eller prosent (desimaltall mellom 0,000001 og 0,99)." + }, + "max_area": { + "label": "Maksimum objektområde", + "description": "Maksimalt areal for markeringsramme (piksler eller prosent) tillatt for denne objekttypen." + }, + "min_ratio": { + "label": "Minimum størrelsesforhold", + "description": "Minimum forhold mellom bredde og høyde som kreves for at markeringsrammen skal kvalifisere." + }, + "max_ratio": { + "label": "Maksimum størrelsesforhold", + "description": "Maksimalt forhold mellom bredde og høyde tillatt for at markeringsrammen skal kvalifisere." + }, + "threshold": { + "label": "Konfidensterskel", + "description": "Gjennomsnittlig terskel for deteksjonskonfidens som kreves for at objektet skal anses som en ekte positiv." + }, + "min_score": { + "label": "Minimum konfidens", + "description": "Minimum deteksjonskonfidens for et enkeltbilde som kreves for at objektet skal telles med." + }, + "mask": { + "label": "Filtermaske", + "description": "Polygonkoordinater som definerer hvor dette filteret gjelder innenfor bildet." + }, + "raw_mask": { + "label": "Råmaske" + } + }, + "mask": { + "label": "Objektmaske", + "description": "Maskepolygon brukt for å forhindre objektdeteksjon i spesifiserte områder." + }, + "raw_mask": { + "label": "Råmaske" + }, + "genai": { + "label": "GenAI-objektkonfigurasjon", + "description": "GenAI-alternativer for å beskrive sporede objekter og sende bilder til generering.", + "enabled": { + "label": "Aktiver GenAI", + "description": "Aktiver GenAI-generering av beskrivelser for sporede objekter som standard." + }, + "use_snapshot": { + "label": "Bruk stillbilder", + "description": "Bruk stillbilder av objekter i stedet for miniatyrbilder for GenAI-beskrivelsesgenerering." + }, + "prompt": { + "label": "Ledetekst for bildetekst", + "description": "Standardmal for ledetekst brukt ved generering av beskrivelser med GenAI." + }, + "object_prompts": { + "label": "Objektspesifikke ledetekster", + "description": "Ledetekster per objekt for å tilpasse GenAI-resultater for spesifikke etiketter." + }, + "objects": { + "label": "GenAI-objekter", + "description": "Liste over objektetiketter som skal sendes til GenAI som standard." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Soner som må entres for at objekter skal kvalifisere for GenAI-beskrivelsesgenerering." + }, + "debug_save_thumbnails": { + "label": "Lagre miniatyrbilder", + "description": "Lagre miniatyrbilder sendt til GenAI for feilsøking og inspeksjon." + }, + "send_triggers": { + "label": "GenAI-utløsere", + "description": "Definerer når bilder skal sendes til GenAI (ved slutt, etter oppdateringer, osv.).", + "tracked_object_end": { + "label": "Send ved avslutning", + "description": "Send en forespørsel til GenAI når det sporede objektet avsluttes." + }, + "after_significant_updates": { + "label": "Tidlig GenAI-utløser", + "description": "Send en forespørsel til GenAI etter et spesifisert antall signifikante oppdateringer for det sporede objektet." + } + }, + "enabled_in_config": { + "label": "Opprinnelig GenAI-status", + "description": "Indikerer om GenAI var aktivert i den opprinnelige statiske konfigurasjonen." + } + }, + "filters_attribute": { + "label": "Attributtfiltre", + "description": "Filtre som brukes på detekterte attributter for å redusere falske positiver (område, sideforhold, konfidens).", + "min_area": { + "label": "Minimum attributtområde", + "description": "Minimum areal for markeringsrammen(piksler eller prosent) som kreves for dette attributtet. Kan oppgis i piksler (heltall) eller prosent (desimaltall mellom 0.000001 og 0.99)." + }, + "max_area": { + "label": "Maksimum attributtområde", + "description": "Maksimum areal for markeringsrammen (piksler eller prosent) tillatt for dette attributtet. Kan oppgis i piksler (heltall) eller prosent (desimaltall mellom 0.000001 og 0.99)." + }, + "min_ratio": { + "label": "Minimum sideforhold", + "description": "Minimum bredde-/høydeforhold som kreves for at markeringsrammen skal kvalifisere." + }, + "max_ratio": { + "label": "Maksimum sideforhold", + "description": "Maksimum bredde-/høydeforhold tillatt for at markeringsrammen skal kvalifisere." + }, + "threshold": { + "label": "Konfidensterskel", + "description": "Gjennomsnittlig terskel for deteksjonskonfidens som kreves for at attributtet skal anses som en ekte positiv." + }, + "min_score": { + "label": "Minimum konfidens", + "description": "Minimum deteksjonskonfidens i et enkeltbilde som kreves for å knytte dette attributtet til sitt hovedobjekt." + }, + "mask": { + "label": "Filtermaske", + "description": "Polygonkoordinater som definerer hvor denne masken gjelder innenfor bildet." + }, + "raw_mask": { + "label": "Råmaske" + } + } + }, + "record": { + "label": "Opptak", + "description": "Innstillinger for opptak og bevaring (retention) som gjelder for kameraer med mindre de overstyres per kamera.", + "enabled": { + "label": "Aktiver opptak", + "description": "Aktiver eller deaktiver opptak for alle kameraer; kan overstyres per kamera." + }, + "expire_interval": { + "label": "Intervall for opprydding av opptak", + "description": "Minutter mellom hver opprydding som fjerner foreldede opptakssegmenter." + }, + "continuous": { + "label": "Kontinuerlig bevaring", + "description": "Antall dager opptak skal bevares uavhengig av sporede objekter eller bevegelse.", + "days": { + "label": "Bevaringsdager", + "description": "Dager opptak skal bevares." + } + }, + "motion": { + "label": "Bevaring ved bevegelse", + "description": "Antall dager opptak utløst av bevegelse skal bevares uavhengig av sporede objekter.", + "days": { + "label": "Bevaringsdager", + "description": "Dager opptak skal bevares." + } + }, + "detections": { + "label": "Bevaring ved deteksjon", + "description": "Innstillinger for bevaring av opptak for deteksjonshendelser, inkludert varighet for forhånds-/etteropptak.", + "pre_capture": { + "label": "Sekunder forhåndsopptak", + "description": "Antall sekunder før deteksjonshendelsen som skal inkluderes i opptaket." + }, + "post_capture": { + "label": "Sekunder etteropptak", + "description": "Antall sekunder etter deteksjonshendelsen som skal inkluderes i opptaket." + }, + "retain": { + "label": "Hendelsesbevaring", + "description": "Bevaringsinnstillinger for opptak av deteksjonshendelser.", + "days": { + "label": "Bevaringsdager", + "description": "Antall dager opptak av deteksjonshendelser skal bevares." + }, + "mode": { + "label": "Bevaringsmodus", + "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)." + } + } + }, + "alerts": { + "label": "Bevaring av varsler", + "description": "Innstillinger for bevaring av opptak for varslingshendelser, inkludert varighet for forhånds-/etteropptak.", + "pre_capture": { + "label": "Sekunder forhåndsopptak", + "description": "Antall sekunder før deteksjonshendelsen som skal inkluderes i opptaket." + }, + "post_capture": { + "label": "Sekunder etteropptak", + "description": "Antall sekunder etter deteksjonshendelsen som skal inkluderes i opptaket." + }, + "retain": { + "label": "Hendelsesbevaring", + "description": "Bevaringsinnstillinger for opptak av deteksjonshendelser.", + "days": { + "label": "Bevaringsdager", + "description": "Antall dager opptak av deteksjonshendelser skal bevares." + }, + "mode": { + "label": "Bevaringsmodus", + "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)." + } + } + }, + "export": { + "label": "Konfigurasjon for eksport", + "description": "Innstillinger som brukes ved eksport av opptak, som for eksempel tidsforløp (timelapse) og maskinvareakselerasjon.", + "hwaccel_args": { + "label": "Argumenter for maskinvareakselerasjon ved eksport", + "description": "Argumenter for maskinvareakselerasjon som skal brukes ved eksport og transkoding." + }, + "max_concurrent": { + "description": "Maksimalt antall eksportjobber som kan behandles samtidig.", + "label": "Maksimalt antall samtidige eksporter" + }, + "chapters": { + "label": "Kapittelmetadata som skal bygges inn i eksporterte opptak" + } + }, + "preview": { + "label": "Konfigurasjon for forhåndsvisning", + "description": "Innstillinger som kontrollerer kvaliteten på forhåndsvisninger av opptak i grensesnittet.", + "quality": { + "label": "Kvalitet på forhåndsvisning", + "description": "Kvalitetsnivå for forhåndsvisning (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Opprinnelig opptaksstatus", + "description": "Indikerer om opptak var aktivert i den opprinnelige statiske konfigurasjonen." + } + }, + "review": { + "label": "Inspeksjon", + "description": "Innstillinger som kontrollerer varsler, deteksjoner og GenAI-sammendrag brukt av grensesnittet og lagring.", + "alerts": { + "label": "Konfigurasjon for varsler", + "description": "Innstillinger for hvilke sporede objekter som genererer varsler og hvordan disse bevares.", + "enabled": { + "label": "Aktiver varsler", + "description": "Aktiver eller deaktiver generering av varsler for alle kameraer; kan overstyres per kamera." + }, + "labels": { + "label": "Varslingsetiketter", + "description": "Liste over objektetiketter som kvalifiserer som varsler (for eksempel: bil, person)." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Soner et objekt må tre inn i for å anses som et varsel; la stå tom for å tillate alle soner." + }, + "enabled_in_config": { + "label": "Opprinnelig varslingsstatus", + "description": "Registrerer om varsler opprinnelig var aktivert i den statiske konfigurasjonen." + }, + "cutoff_time": { + "label": "Avskjæringstid for varsler", + "description": "Sekunder å vente etter at varslingsutløsende aktivitet har opphørt før et varsel avsluttes." + } + }, + "detections": { + "label": "Konfigurasjon for deteksjoner", + "description": "Innstillinger for hvilke sporede objekter som genererer deteksjoner (ikke-varsler) og hvordan disse bevares.", + "enabled": { + "label": "Aktiver deteksjoner", + "description": "Aktiver eller deaktiver deteksjonshendelser for alle kameraer; kan overstyres per kamera." + }, + "labels": { + "label": "Deteksjonsetiketter", + "description": "Liste over objektetiketter som kvalifiserer som deteksjonshendelser." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Soner et objekt må tre inn i for å anses som en deteksjon; la stå tom for å tillate alle soner." + }, + "cutoff_time": { + "label": "Avskjæringstid for deteksjoner", + "description": "Sekunder å vente etter at deteksjonsutløsende aktivitet har opphørt før en deteksjon avsluttes." + }, + "enabled_in_config": { + "label": "Opprinnelig deteksjonsstatus", + "description": "Registrerer om deteksjoner opprinnelig var aktivert i den statiske konfigurasjonen." + } + }, + "genai": { + "label": "GenAI-konfigurasjon", + "description": "Kontrollerer bruk av generativ AI for å produsere beskrivelser og sammendrag av inspeksjonselementer.", + "enabled": { + "label": "Aktiver GenAI-beskrivelser", + "description": "Aktiver eller deaktiver GenAI-genererte beskrivelser og sammendrag for inspeksjonselementer." + }, + "alerts": { + "label": "Aktiver GenAI for varsler", + "description": "Bruk GenAI til å generere beskrivelser for varslingselementer." + }, + "detections": { + "label": "Aktiver GenAI for deteksjoner", + "description": "Bruk GenAI til å generere beskrivelser for deteksjonselementer." + }, + "image_source": { + "label": "Bildekilde for inspeksjon", + "description": "Kilde for bilder sendt til GenAI ('preview' eller 'recordings'); 'recordings' bruker bilder med høyere kvalitet, men flere tokens." + }, + "additional_concerns": { + "label": "Tilleggshensyn", + "description": "En liste over tilleggshensyn eller notater GenAI bør vurdere ved evaluering av aktivitet på dette kameraet." + }, + "debug_save_thumbnails": { + "label": "Lagre miniatyrbilder", + "description": "Lagre miniatyrbilder som sendes til GenAI-leverandøren for feilsøking og inspeksjon." + }, + "enabled_in_config": { + "label": "Opprinnelig GenAI-status", + "description": "Registrerer om GenAI-inspeksjon opprinnelig var aktivert i den statiske konfigurasjonen." + }, + "preferred_language": { + "label": "Foretrukket språk", + "description": "Foretrukket språk som skal etterspørres fra GenAI-leverandøren for genererte svar." + }, + "activity_context_prompt": { + "label": "Ledetekst for aktivitetskontekst", + "description": "Egendefinert ledetekst som beskriver hva som er og ikke er mistenkelig aktivitet for å gi kontekst til GenAI-sammendrag." + } + } + }, + "snapshots": { + "label": "Stillbilder", + "description": "Innstillinger for API-genererte stillbilder av sporede objekter for alle kameraer; kan overstyres per kamera.", + "enabled": { + "label": "Aktiver stillbilder", + "description": "Aktiver eller deaktiver lagring av stillbilder for alle kameraer; kan overstyres per kamera." + }, + "timestamp": { + "label": "Tidsstempel-overlegg", + "description": "Legg et tidsstempel over stillbilder fra API-et." + }, + "bounding_box": { + "label": "Overlegg for markeringsramme", + "description": "Tegn markeringsrammer for sporede objekter på stillbilder fra API-et." + }, + "crop": { + "label": "Beskjær stillbilde", + "description": "Beskjær stillbilder fra API-et til det detekterte objektets markeringsramme." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Soner et objekt må tre inn i for at et stillbilde skal lagres." + }, + "height": { + "label": "Høyde på stillbilde", + "description": "Høyde (piksler) som stillbilder fra API-et skal skaleres til; la stå tom for å beholde opprinnelig størrelse." + }, + "retain": { + "label": "Bevaring av stillbilder", + "description": "Bevaringsinnstillinger for stillbilder, inkludert standard antall dager og overstyringer per objekt.", + "default": { + "label": "Standard bevaring", + "description": "Standard antall dager stillbilder skal bevares." + }, + "mode": { + "label": "Bevaringsmodus", + "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)." + }, + "objects": { + "label": "Objektbevaring", + "description": "Overstyringer per objekt for antall dager stillbilder skal bevares." + } + }, + "quality": { + "label": "Kvalitet på stillbilde", + "description": "Kodingskvalitet for lagrede stillbilder (0-100)." + } + }, + "timestamp_style": { + "label": "Stil for tidsstempel", + "description": "Stilalternativer for tidsstempler i strømmen, brukt i testvisning (debug) og stillbilder.", + "position": { + "label": "Posisjon for tidsstempel", + "description": "Posisjonen til tidsstempelet på bildet (tl/tr/bl/br)." + }, + "format": { + "label": "Format for tidsstempel", + "description": "Formatstreng for dato og tid brukt for tidsstempler (Python datetime-formatkoder)." + }, + "color": { + "label": "Farge på tidsstempel", + "description": "RGB-fargeverdier for tidsstempelteksten (alle verdier 0-255).", + "red": { + "label": "Rød", + "description": "Rød komponent (0-255) for tidsstempelfarge." + }, + "green": { + "label": "Grønn", + "description": "Grønn komponent (0-255) for tidsstempelfarge." + }, + "blue": { + "label": "Blå", + "description": "Blå komponent (0-255) for tidsstempelfarge." + } + }, + "thickness": { + "label": "Tykkelse på tidsstempel", + "description": "Linjetykkelsen på tidsstempelteksten." + }, + "effect": { + "label": "Effekt for tidsstempel", + "description": "Visuell effekt for tidsstempelteksten (none, solid, shadow)." + } + }, + "audio_transcription": { + "label": "Lydtranskripsjon", + "description": "Innstillinger for tale- og lydtranskripsjon i sanntid, brukt for hendelser og teksting.", + "enabled": { + "label": "Aktiver lydtranskripsjon", + "description": "Aktiver eller deaktiver automatisk lydtranskripsjon for alle kameraer; kan overstyres per kamera." + }, + "language": { + "label": "Språk for transkripsjon", + "description": "Språkkode som brukes for transkripsjon/oversettelse (f.eks. 'no' for norsk)." + }, + "device": { + "label": "Enhet for transkripsjon", + "description": "Enhet (CPU/GPU) som transkripsjonsmodellen skal kjøre på." + }, + "model_size": { + "label": "Modellstørrelse", + "description": "Modellstørrelse som skal brukes for transkripsjon av lydhendelser lokalt." + }, + "live_enabled": { + "label": "Sanntidstranskripsjon", + "description": "Aktiver løpende transkripsjon av lyd etter hvert som den mottas." + } + }, + "classification": { + "label": "Objektklassifisering", + "description": "Innstillinger for klassifiseringsmodeller brukt til å forbedre objektetiketter eller statusklassifisering.", + "bird": { + "label": "Konfigurasjon for fugleklassifisering", + "description": "Spesifikke innstillinger for modeller for fugleklassifisering.", + "enabled": { + "label": "Fugleklassifisering", + "description": "Aktiver eller deaktiver fugleklassifisering." + }, + "threshold": { + "label": "Minimumsscore", + "description": "Minimum klassifiseringsscore som kreves for å godta en fugleklassifisering." + } + }, + "custom": { + "label": "Egendefinerte klassifiseringsmodeller", + "description": "Konfigurasjon for egendefinerte klassifiseringsmodeller brukt for objekter eller statusdeteksjon.", + "enabled": { + "label": "Aktiver modell", + "description": "Aktiver eller deaktiver den egendefinerte klassifiseringsmodellen." + }, + "name": { + "label": "Modellnavn", + "description": "Identifikator for den egendefinerte klassifiseringsmodellen som skal brukes." + }, + "threshold": { + "label": "Score-terskel", + "description": "Score-terskel brukt for å endre klassifiseringsstatus." + }, + "save_attempts": { + "label": "Lagre forsøk", + "description": "Hvor mange klassifiseringsforsøk som skal lagres for visning i grensesnittet." + }, + "object_config": { + "objects": { + "label": "Klassifiser objekter", + "description": "Liste over objekttyper det skal kjøres objektklassifisering på." + }, + "classification_type": { + "label": "Klassifiseringstype", + "description": "Klassifiseringstype som brukes: 'sub_label' (legger til underetikett) eller andre støttede typer." + } + }, + "state_config": { + "cameras": { + "label": "Kameraer for klassifisering", + "description": "Beskjæring og innstillinger per kamera for kjøring av statusklassifisering.", + "crop": { + "label": "Beskjæring for klassifisering", + "description": "Beskjæringskoordinater som skal brukes for klassifisering på dette kameraet." + } + }, + "motion": { + "label": "Kjør ved bevegelse", + "description": "Hvis sann, kjør klassifisering når bevegelse detekteres innenfor det spesifiserte området." + }, + "interval": { + "label": "Klassifiseringsintervall", + "description": "Intervall (sekunder) mellom periodiske kjøringer for statusklassifisering." + } + } + } + }, + "semantic_search": { + "label": "Semantisk søk", + "description": "Innstillinger for semantisk søk, som bygger og søker i objekt-vektorrepresentasjoner for å finne lignende elementer.", + "enabled": { + "label": "Aktiver semantisk søk", + "description": "Aktiver eller deaktiver funksjonen for semantisk søk." + }, + "reindex": { + "label": "Reindekser ved oppstart", + "description": "Utløs en fullstendig reindeksering av historiske sporede objekter i databasen for vektorrepresentasjoner." + }, + "model": { + "label": "Modell for semantisk søk eller GenAI-leverandør", + "description": "Modellen for vektorrepresentasjoner som skal brukes for semantisk søk (f.eks. 'jinav1'), eller navnet på en GenAI-leverandør." + }, + "model_size": { + "label": "Modellstørrelse", + "description": "Velg modellstørrelse; 'liten' kjører på CPU og 'stor' krever vanligvis GPU." + }, + "device": { + "label": "Enhet", + "description": "Dette er en overstyring for å målrette en spesifikk enhet. Se https://onnxruntime.ai/docs/execution-providers/ for mer informasjon" + }, + "triggers": { + "label": "Utløsere", + "description": "Handlinger og kriterier for kameraspesifikke utløsere for semantisk søk.", + "friendly_name": { + "label": "Visningsnavn", + "description": "Valgfritt visningsnavn for denne utløseren i grensesnittet." + }, + "enabled": { + "label": "Aktiver denne utløseren", + "description": "Aktiver eller deaktiver denne utløseren for semantisk søk." + }, + "type": { + "label": "Utløsertype", + "description": "Type utløser: 'miniatyrbilde' (match mot bilde) eller 'beskrivelse' (match mot tekst)." + }, + "data": { + "label": "Utløserinnhold", + "description": "Tekstfrase eller miniatyrbilde-ID som skal matches mot sporede objekter." + }, + "threshold": { + "label": "Utløser-terskel", + "description": "Minimum likhetsscore (0-1) som kreves for å aktivere denne utløseren." + }, + "actions": { + "label": "Utløserhandlinger", + "description": "Liste over handlinger som skal utføres når utløseren matches (varsling, underetikett, egenskap)." + } + } + }, + "face_recognition": { + "label": "Ansiktsgjenkjenning", + "description": "Innstillinger for ansiktsdeteksjon og gjenkjenning for alle kameraer; kan overstyres per kamera.", + "enabled": { + "label": "Aktiver ansiktsgjenkjenning", + "description": "Aktiver eller deaktiver ansiktsgjenkjenning for alle kameraer; kan overstyres per kamera." + }, + "model_size": { + "label": "Modellstørrelse", + "description": "Modellstørrelse for vektorrepresentasjoner for ansikts (liten/stor); 'stor' kan kreve GPU." + }, + "unknown_score": { + "label": "Terskel for ukjent person", + "description": "Avstandsterskel der et ansikt anses som en potensiell match (høyere = strengere)." + }, + "detection_threshold": { + "label": "Deteksjonsterskel", + "description": "Minimum konfidens som kreves for at en ansiktsdeteksjon skal anses som gyldig." + }, + "recognition_threshold": { + "label": "Gjenkjenningsterskel", + "description": "Terskel for avstand mellom vektorrepresentasjoner for ansikt for å anse to ansikter som like." + }, + "min_area": { + "label": "Minimum ansiktsareal", + "description": "Minimum areal (piksler) for en ansiktsboks før gjenkjenning forsøkes." + }, + "min_faces": { + "label": "Minimum antall ansikter", + "description": "Minimum antall gjenkjenninger som kreves før en underetikett tas i bruk." + }, + "save_attempts": { + "label": "Lagre forsøk", + "description": "Antall gjenkjenningsforsøk som skal lagres for visning i grensesnittet." + }, + "blur_confidence_filter": { + "label": "Filter for uskarphet", + "description": "Juster konfidensscore basert på uskarphet i bildet for å redusere falske positive." + }, + "device": { + "label": "Enhet", + "description": "Dette er en overstyring for å målrette en spesifikk enhet. Se https://onnxruntime.ai/docs/execution-providers/ for mer informasjon" + } + }, + "lpr": { + "label": "Gjenkjenning av kjennemerker", + "description": "Innstillinger for gjenkjenning av kjennemerker, inkludert deteksjonsterskler og kjente kjennemerkeer.", + "enabled": { + "label": "Aktiver skiltgjenkjenning", + "description": "Aktiver eller deaktiver gjenkjenning av kjennemerker for alle kameraer; kan overstyres per kamera." + }, + "model_size": { + "label": "Modellstørrelse", + "description": "Modellstørrelse brukt for tekstdeteksjon og gjenkjenning. De fleste brukere bør bruke 'liten'." + }, + "detection_threshold": { + "label": "Deteksjonsterskel", + "description": "Terskel for deteksjonskonfidens for å starte OCR på et antatt kjennemerke." + }, + "min_area": { + "label": "Minimum areal for kjennemerke", + "description": "Minimum areal (piksler) for et kjennemerke før gjenkjenning forsøkes." + }, + "recognition_threshold": { + "label": "Gjenkjenningsterskel", + "description": "Konfidensterskel som kreves for at gjenkjent tekst på kjennemerke skal legges til som underetikett." + }, + "min_plate_length": { + "label": "Minimum kjennemerkelengde", + "description": "Minimum antall tegn et gjenkjent kjennemerke må inneholde for å anses som gyldig." + }, + "format": { + "label": "Regex for kjennemerkeformat", + "description": "Valgfri regex for å validere gjenkjente kjennemerkestrenger mot et forventet format." + }, + "match_distance": { + "label": "Match-distanse", + "description": "Antall tegnfeil som tillates ved sammenligning av detekterte kjennemerkeer mot kjente kjennemerkeer." + }, + "known_plates": { + "label": "Kjente kjennemerkeer", + "description": "Liste over kjennemerkeer eller regexer som skal spores spesielt eller utløse varsel." + }, + "enhancement": { + "label": "Forbedringsnivå", + "description": "Forbedringsnivå (0-10) som brukes på kjennemerkebeskjæringer før OCR; høyere verdier forbedrer ikke alltid resultatet, nivåer over 5 fungerer ofte kun på nattbilder og bør brukes med forsiktighet." + }, + "debug_save_plates": { + "label": "Lagre feilsøkingsbilder", + "description": "Lagre beskjærte kjennemerkebilder for feilsøking av LPR-ytelse." + }, + "device": { + "label": "Enhet", + "description": "Dette er en overstyring for å målrette en spesifikk enhet. Se https://onnxruntime.ai/docs/execution-providers/ for mer informasjon" + }, + "replace_rules": { + "label": "Erstatningsregler", + "description": "Regex-erstatningsregler brukt for å normalisere detekterte kjennemerkestrenger før matching.", + "pattern": { + "label": "Regex-mønster" + }, + "replacement": { + "label": "Erstatningsstreng" + } + }, + "expire_time": { + "label": "Utløpstid (sekunder)", + "description": "Tid i sekunder før et ukjent kjennemerke foreldes fra sporingen (kun for dedikerte LPR-kameraer)." + } + }, + "camera_groups": { + "label": "Kameragrupper", + "description": "Konfigurasjon for navngitte kameragrupper brukt til å organisere kameraer i grensesnittet.", + "cameras": { + "label": "Kameraliste", + "description": "Liste over kameranavn som er inkludert i denne gruppen." + }, + "icon": { + "label": "Gruppeikon", + "description": "Ikon som representerer kameragruppen i grensesnittet." + }, + "order": { + "label": "Sorteringsrekkefølge", + "description": "Numerisk rekkefølge for sortering av kameragrupper i grensesnittet; høyere tall vises senere." + } + }, + "profiles": { + "label": "Profiler", + "description": "Navngitte profil-definisjoner. Kameraprofiler må referere til navn definert her.", + "friendly_name": { + "label": "Visningsnavn", + "description": "Visningsnavn for denne profilen i grensesnittet." + } + }, + "active_profile": { + "label": "Aktiv profil", + "description": "Navn på profil som er aktiv nå. Kun for kjøretid, lagres ikke i YAML." + }, + "camera_mqtt": { + "label": "MQTT", + "description": "Innstillinger for bilde-publisering via MQTT.", + "enabled": { + "label": "Send bilde", + "description": "Aktiver publisering av bilde-stillbilder for objekter til MQTT-emner for dette kameraet." + }, + "timestamp": { + "label": "Legg til tidsstempel", + "description": "Legg et tidsstempel over bilder som publiseres til MQTT." + }, + "bounding_box": { + "label": "Legg til markeringsramme", + "description": "Tegn markeringsrammer på bilder som publiseres over MQTT." + }, + "crop": { + "label": "Beskjær bilde", + "description": "Beskjær bilder publisert til MQTT til det detekterte objektets markeringsramme." + }, + "height": { + "label": "Bildehøyde", + "description": "Høyde (piksler) for bilder som publiseres over MQTT." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Soner et objekt må tre inn i for at et MQTT-bilde skal publiseres." + }, + "quality": { + "label": "JPEG-kvalitet", + "description": "JPEG-kvalitet for bilder publisert til MQTT (0-100)." + } + }, + "camera_ui": { + "label": "Brukergrensesnitt for kamera", + "description": "Sortering og synlighet for dette kameraet i grensesnittet. Sortering påvirker standard dashbord. For mer detaljert kontroll, bruk kameragrupper.", + "order": { + "label": "Rekkefølge i UI", + "description": "Numerisk rekkefølge for sortering av kameraet i grensesnittet; høyere tall vises senere." + }, + "dashboard": { + "label": "Vis i grensesnitt", + "description": "Velg om dette kameraet skal være synlig i Frigate-grensesnittet. Deaktivering krever manuell redigering av konfigurasjonen for å vise kameraet igjen." + }, + "review": { + "label": "Vis i inspeksjon", + "description": "Velg om dette kameraet skal være synlig i inspeksjon (inspeksjonssiden og dens kamerafilter, bevegelsesinspeksjon og historikkvisning)." + } + }, + "onvif": { + "label": "ONVIF", + "description": "ONVIF-tilkobling og innstillinger for PTZ-autosporing for dette kameraet.", + "host": { + "label": "ONVIF-vert", + "description": "Vert (og valgfritt skjema) for ONVIF-tjenesten for dette kameraet." + }, + "port": { + "label": "ONVIF-port", + "description": "Portnummer for ONVIF-tjenesten." + }, + "user": { + "label": "ONVIF-brukernavn", + "description": "Brukernavn for ONVIF-autentisering; enkelte enheter krever admin-bruker for ONVIF." + }, + "password": { + "label": "ONVIF-passord", + "description": "Passord for ONVIF-autentisering." + }, + "tls_insecure": { + "label": "Deaktiver TLS-verifisering", + "description": "Hopp over TLS-verifisering og deaktiver digest-autentisering for ONVIF (usikre; bruk kun i trygge nettverk)." + }, + "profile": { + "label": "ONVIF-profil", + "description": "Spesifikk ONVIF-medieprofil for PTZ-kontroll. Hvis ikke satt, velges den første profilen med gyldig PTZ-konfigurasjon automatisk." + }, + "autotracking": { + "label": "Autosporing", + "description": "Spor bevegelige objekter automatisk og hold dem sentrert ved bruk av PTZ-bevegelser.", + "enabled": { + "label": "Aktiver autosporing", + "description": "Aktiver eller deaktiver automatisk PTZ-sporing av detekterte objekter." + }, + "calibrate_on_startup": { + "label": "Kalibrer ved start", + "description": "Mål PTZ-motorhastigheter ved oppstart for å forbedre sporingsnøyaktighet. Frigate vil oppdatere konfigurasjonen etter kalibrering." + }, + "zooming": { + "label": "Zoom-modus", + "description": "Kontroller zoom-oppførsel: deaktivert, absolutt (mest kompatibel) eller relativ." + }, + "zoom_factor": { + "label": "Zoom-faktor", + "description": "Kontrollere zoom-nivå på sporede objekter. Lavere verdier gir mer oversikt; høyere verdier zoomer tettere inn. Verdier mellom 0.1 og 0.75." + }, + "track": { + "label": "Sporede objekter", + "description": "Liste over objekttyper som skal utløse autosporing." + }, + "required_zones": { + "label": "Påkrevde soner", + "description": "Objekter må tre inn i en av disse sonene før autosporing starter." + }, + "return_preset": { + "label": "Forhåndsinnstilling for retur", + "description": "Navn på ONVIF-forhåndsinnstilling kameraet skal returnere til når sporingen avsluttes." + }, + "timeout": { + "label": "Tidsavbrudd for retur", + "description": "Antall sekunder å vente etter mistet sporing før kameraet returnerer til forhåndsinnstilt posisjon." + }, + "movement_weights": { + "label": "Bevegelsesvekting", + "description": "Kalibreringsverdier som genereres automatisk. Ikke endre manuelt." + }, + "enabled_in_config": { + "label": "Opprinnelig autosporingsstatus", + "description": "Internt felt for å spore om autosporing var aktivert i konfigurasjonen." + } + }, + "ignore_time_mismatch": { + "label": "Ignorer tidsavvik", + "description": "Ignorer forskjeller i tidssynkronisering mellom kamera og server ved ONVIF-kommunikasjon." + } + } +} diff --git a/web/public/locales/nb-NO/config/groups.json b/web/public/locales/nb-NO/config/groups.json new file mode 100644 index 0000000..254a343 --- /dev/null +++ b/web/public/locales/nb-NO/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Global deteksjon", + "sensitivity": "Global følsomhet" + }, + "cameras": { + "detection": "Deteksjon", + "sensitivity": "Følsomhet" + } + }, + "timestamp_style": { + "global": { + "appearance": "Globalt utseende" + }, + "cameras": { + "appearance": "Utseende" + } + }, + "motion": { + "global": { + "sensitivity": "Global følsomhet", + "algorithm": "Global algoritme" + }, + "cameras": { + "sensitivity": "Følsomhet", + "algorithm": "Algoritme" + } + }, + "snapshots": { + "global": { + "display": "Global visning" + }, + "cameras": { + "display": "Visning" + } + }, + "detect": { + "global": { + "resolution": "Global oppløsning", + "tracking": "Global sporing" + }, + "cameras": { + "resolution": "Oppløsning", + "tracking": "Sporing" + } + }, + "objects": { + "global": { + "tracking": "Global sporing", + "filtering": "Global filtrering" + }, + "cameras": { + "tracking": "Sporing", + "filtering": "Filtrering" + } + }, + "record": { + "global": { + "retention": "Global opptaksbevaring", + "events": "Globale hendelser" + }, + "cameras": { + "retention": "Opptaksbevaring", + "events": "Hendelser" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Kamera-spesifikke FFmpeg argumenter" + } + } +} diff --git a/web/public/locales/nb-NO/config/validation.json b/web/public/locales/nb-NO/config/validation.json new file mode 100644 index 0000000..afc1e4b --- /dev/null +++ b/web/public/locales/nb-NO/config/validation.json @@ -0,0 +1,35 @@ +{ + "minimum": "Må være minst {{limit}}", + "maximum": "Må være maksimalt {{limit}}", + "exclusiveMinimum": "Må være større enn {{limit}}", + "exclusiveMaximum": "Må være mindre enn {{limit}}", + "minItems": "Må ha minst {{limit}} elementer", + "maxItems": "Må ha maksimalt {{limit}} elementer", + "pattern": "Ugyldig format", + "required": "Dette feltet er obligatorisk", + "type": "Ugyldig verditype", + "enum": "Må være en av de tillatte verdiene", + "const": "Verdien samsvarer ikke med forventet konstant", + "uniqueItems": "Alle elementer må være unike", + "format": "Ugyldig format", + "additionalProperties": "Ukjent egenskap er ikke tillatt", + "oneOf": "Må samsvare med nøyaktig ett av de tillatte skjemaene", + "anyOf": "Må samsvare med minst ett av de tillatte skjemaene", + "proxy": { + "header_map": { + "roleHeaderRequired": "Rollehode (header) er påkrevd når rolletilordninger er konfigurert." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "Hver rolle kan bare tildeles én inngangsstrøm.", + "detectRequired": "Minst èn inngangsstrøm må være tildelt rollen 'deteksjon'.", + "hwaccelDetectOnly": "Bare inngangsstrømmen med rollen 'deteksjon' kan definere argumenter for maskinvareakselerasjon." + } + }, + "minLength": "Må være minst {{limit}} tegn", + "maxLength": "Må være maks {{limit}} tegn", + "detect": { + "dimensionMustBeEven": "Må være et partall." + } +} diff --git a/web/public/locales/nb-NO/objects.json b/web/public/locales/nb-NO/objects.json new file mode 100644 index 0000000..ab35271 --- /dev/null +++ b/web/public/locales/nb-NO/objects.json @@ -0,0 +1,130 @@ +{ + "motorcycle": "Motorsykkel", + "airplane": "Fly", + "bus": "Buss", + "train": "Tog", + "boat": "Båt", + "traffic_light": "Trafikklys", + "wine_glass": "Vinglass", + "cup": "Kopp", + "chair": "Stol", + "couch": "Sofa", + "potted_plant": "Potteplante", + "bed": "Seng", + "gls": "GLS", + "person": "Person", + "bicycle": "Sykkel", + "car": "Bil", + "fire_hydrant": "Brannhydrant", + "street_sign": "Gateskilt", + "stop_sign": "Stoppskilt", + "parking_meter": "Parkeringsautomat", + "bench": "Benk", + "bird": "Fugl", + "cat": "Katt", + "dog": "Hund", + "horse": "Hest", + "sheep": "Sau", + "cow": "Ku", + "elephant": "Elefant", + "bear": "Bjørn", + "zebra": "Sebra", + "giraffe": "Giraff", + "hat": "Hatt", + "backpack": "Ryggsekk", + "umbrella": "Paraply", + "shoe": "Sko", + "eye_glasses": "Briller", + "handbag": "Håndveske", + "tie": "Slips", + "suitcase": "Koffert", + "frisbee": "Frisbee", + "skis": "Ski", + "snowboard": "Snøbrett", + "sports_ball": "Ball", + "kite": "Drage", + "baseball_bat": "Baseballkølle", + "baseball_glove": "Baseballhanske", + "skateboard": "Skateboard", + "surfboard": "Surfebrett", + "tennis_racket": "Tennisracket", + "bottle": "Flaske", + "plate": "Tallerken", + "fork": "Gaffel", + "knife": "Kniv", + "spoon": "Skje", + "bowl": "Bolle", + "banana": "Banan", + "apple": "Eple", + "broccoli": "Brokkoli", + "sandwich": "Sandwich", + "orange": "Appelsin", + "carrot": "Gulrot", + "hot_dog": "Pølse i brød", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Kake", + "mirror": "Speil", + "dining_table": "Spisebord", + "window": "Vindu", + "desk": "Skrivebord", + "toilet": "Toalett", + "door": "Dør", + "tv": "TV", + "laptop": "Bærbar datamaskin", + "mouse": "Mus", + "remote": "Fjernkontroll", + "keyboard": "Tastatur", + "cell_phone": "Mobiltelefon", + "sink": "Vask", + "microwave": "Mikrobølgeovn", + "oven": "Ovn", + "toaster": "Brødrister", + "refrigerator": "Kjøleskap", + "blender": "Blender", + "book": "Bok", + "clock": "Klokke", + "vase": "Vase", + "scissors": "Saks", + "teddy_bear": "Teddybjørn", + "hair_dryer": "Hårføner", + "toothbrush": "Tannbørste", + "hair_brush": "Hårbørste", + "vehicle": "Kjøretøy", + "squirrel": "Ekorn", + "deer": "Hjort", + "animal": "Dyr", + "bark": "Bjeff", + "fox": "Rev", + "goat": "Geit", + "rabbit": "Kanin", + "raccoon": "Vaskebjørn", + "robot_lawnmower": "Robotgressklipper", + "waste_bin": "Avfallsbeholder", + "on_demand": "Manuelt opptak", + "face": "Ansikt", + "license_plate": "Kjennemerke", + "package": "Pakke", + "bbq_grill": "Grill", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "dpd": "DPD", + "kangaroo": "Kenguru", + "skunk": "Skunk", + "school_bus": "Skolebuss", + "royal_mail": "Royal Mail", + "canada_post": "Canada Post", + "baby_stroller": "Barnevogn", + "Rodent": "Gnager", + "baby": "Baby", + "rickshaw": "Rickshaw", + "rodent": "Gnager" +} diff --git a/web/public/locales/nb-NO/views/chat.json b/web/public/locales/nb-NO/views/chat.json new file mode 100644 index 0000000..fcef107 --- /dev/null +++ b/web/public/locales/nb-NO/views/chat.json @@ -0,0 +1,72 @@ +{ + "documentTitle": "Chat - Frigate", + "title": "Frigate Chat", + "subtitle": "Din AI-assistent for kamerahåndtering og innsikt", + "placeholder": "Spør om hva som helst...", + "error": "Noe gikk galt. Vennligst prøv igjen.", + "processing": "Behandler...", + "toolsUsed": "Brukt: {{tools}}", + "showTools": "Vis verktøy ({{count}})", + "hideTools": "Skjul verktøy", + "call": "Kall", + "result": "Resultat", + "arguments": "Argumenter:", + "response": "Svar:", + "attachment_chip_label": "{{label}} på {{camera}}", + "attachment_chip_remove": "Fjern vedlegg", + "open_in_explore": "Åpne i Utforsk", + "attach_event_aria": "Legg ved hendelse {{eventId}}", + "attachment_picker_paste_label": "Eller lim inn hendelses-ID", + "attachment_picker_attach": "Legg ved", + "attachment_picker_placeholder": "Legg ved en hendelse", + "quick_reply_find_similar": "Finn lignende observasjoner", + "quick_reply_tell_me_more": "Fortell meg mer om dette", + "quick_reply_when_else": "Når ellers ble det sett?", + "quick_reply_find_similar_text": "Finn lignende observasjoner som denne.", + "quick_reply_tell_me_more_text": "Fortell meg mer om denne.", + "quick_reply_when_else_text": "Når ellers ble denne sett?", + "anchor": "Referanse", + "similarity_score": "Likhet", + "no_similar_objects_found": "Ingen lignende objekter funnet.", + "semantic_search_required": "Semantisk søk må være aktivert for å finne lignende objekter.", + "send": "Send", + "suggested_requests": "Prøv å spørre:", + "starting_requests": { + "show_recent_events": "Vis nylige hendelser", + "show_camera_status": "Vis kamerastatus", + "recap": "Hva skjedde mens jeg var borte?", + "watch_camera": "Overvåk et kamera for aktivitet" + }, + "starting_requests_prompts": { + "show_recent_events": "Vis meg nylige hendelser fra den siste timen", + "show_camera_status": "Hva er status for kameraene mine akkurat nå?", + "recap": "Hva skjedde mens jeg var borte?", + "watch_camera": "Overvåk inngangsdøren og gi meg beskjed hvis noen dukker opp" + }, + "new_chat": "Ny chat", + "settings": { + "title": "Chat-innstillinger", + "show_stats": { + "title": "Vis statistikk", + "desc": "Vis genereringshastighet og kontekststørrelse for chat-svar.", + "while_generating": "Under generering", + "always": "Alltid" + }, + "auto_scroll": { + "title": "Auto-rulling", + "desc": "Følg nye meldinger etter hvert som de ankommer." + } + }, + "stats": { + "context": "{{tokens}} tokens", + "tokens_per_second": "{{rate}} t/s" + }, + "reasoning": { + "active": "Resonnerer…", + "show": "Vis resonnering", + "hide": "Skjul resonnering" + }, + "thinking": { + "toggle": "Aktiver/deaktiver tenking" + } +} diff --git a/web/public/locales/nb-NO/views/classificationModel.json b/web/public/locales/nb-NO/views/classificationModel.json new file mode 100644 index 0000000..2a57708 --- /dev/null +++ b/web/public/locales/nb-NO/views/classificationModel.json @@ -0,0 +1,196 @@ +{ + "documentTitle": "Klassifiseringsmodeller - Frigate", + "button": { + "deleteClassificationAttempts": "Slett klassifiseringsbilder", + "renameCategory": "Omdøp klasse", + "deleteCategory": "Slett klasse", + "deleteImages": "Slett bilder", + "trainModel": "Tren modell", + "addClassification": "Legg til klassifisering", + "deleteModels": "Slett modeller", + "editModel": "Rediger modell" + }, + "toast": { + "success": { + "deletedCategory_one": "Slettet {{count}} klasse", + "deletedCategory_other": "Slettet {{count}} klasser", + "deletedImage_one": "Slettet {{count}} bilde", + "deletedImage_other": "Slettet {{count}} bilder", + "categorizedImage": "Klassifiserte bildet", + "trainedModel": "Modellen ble trent.", + "trainingModel": "Modelltrening startet.", + "deletedModel_one": "{{count}} modell ble slettet", + "deletedModel_other": "{{count}} modeller ble slettet", + "updatedModel": "Modellkonfigurasjonen ble oppdatert", + "renamedCategory": "Klassen ble omdøpt til {{name}}", + "reclassifiedImage": "Bildet ble reklassifisert" + }, + "error": { + "deleteImageFailed": "Kunne ikke slette: {{errorMessage}}", + "deleteCategoryFailed": "Kunne ikke slette klasse: {{errorMessage}}", + "categorizeFailed": "Kunne ikke klassifisere bilde: {{errorMessage}}", + "trainingFailed": "Modelltrening mislyktes. Sjekk Frigate-loggene for detaljer.", + "deleteModelFailed": "Kunne ikke slette modell: {{errorMessage}}", + "trainingFailedToStart": "Kunne ikke starte modelltrening: {{errorMessage}}", + "updateModelFailed": "Kunne ikke oppdatere modell: {{errorMessage}}", + "renameCategoryFailed": "Kunne ikke omdøpe klasse: {{errorMessage}}", + "reclassifyFailed": "Kunne ikke reklassifisere bilde: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Slett klasse", + "desc": "Er du sikker på at du vil slette klassen {{name}}? Dette vil permanent slette alle tilknyttede bilder og kreve at modellen trenes på nytt.", + "minClassesTitle": "Kan ikke slette klasse", + "minClassesDesc": "En klassifiseringsmodell må ha minst 2 klasser. Legg til en ny klasse før du sletter denne." + }, + "deleteDatasetImages": { + "title": "Slett datasettbilder", + "desc": "Er du sikker på at du vil slette {{count}} bilder fra {{dataset}}? Denne handlingen kan ikke angres og krever at modellen trenes på nytt." + }, + "deleteTrainImages": { + "title": "Slett treningsbilder", + "desc": "Er du sikker på at du vil slette {{count}} bilder? Denne handlingen kan ikke angres." + }, + "renameCategory": { + "title": "Omdøp klasse", + "desc": "Skriv inn et nytt navn for {{name}}. Du må trene modellen på nytt for at navneendringen skal tre i kraft." + }, + "description": { + "invalidName": "Ugyldig navn. Navn kan kun inneholde bokstaver, tall, mellomrom, apostrof, understrek og bindestrek." + }, + "train": { + "title": "Nylige klassifiseringer", + "aria": "Velg nylige klassifiseringer", + "titleShort": "Nylige" + }, + "categories": "Klasser", + "createCategory": { + "new": "Opprett ny klasse" + }, + "categorizeImageAs": "Klassifiser bilde som:", + "categorizeImage": "Klassifiser bilde", + "noModels": { + "object": { + "title": "Ingen objektklassifiseringsmodeller", + "description": "Opprett en tilpasset modell for å klassifisere oppdagede objekter.", + "buttonText": "Opprett objektmodell" + }, + "state": { + "title": "Ingen tilstandsklassifiseringsmodeller", + "description": "Opprett en tilpasset modell for å overvåke og klassifisere tilstandsendringer i spesifikke kamerasoner.", + "buttonText": "Opprett tilstandsmodell" + } + }, + "wizard": { + "title": "Opprett ny klassifisering", + "steps": { + "nameAndDefine": "Navn og definér", + "stateArea": "Tilstandsområde", + "chooseExamples": "Velg eksempler" + }, + "step1": { + "description": "Tilstandsmodeller overvåker faste kamerasoner for endringer (f.eks. dør åpen/lukket). Objektmodeller legger til klassifiseringer for oppdagede objekter (f.eks. kjente dyr, bud, osv.).", + "name": "Navn", + "namePlaceholder": "Skriv inn modellnavn...", + "type": "Type", + "typeState": "Tilstand", + "typeObject": "Objekt", + "objectLabel": "Objektetikett", + "objectLabelPlaceholder": "Velg objekttype...", + "classificationType": "Klassifiseringstype", + "classificationTypeTip": "Lær om klassifiseringstyper", + "classificationTypeDesc": "Underetiketter legger til ekstra tekst på objektetiketten (f.eks. 'Person: Posten'). Attributter er søkbare metadata som lagres separat i objektets metadata.", + "classificationSubLabel": "Underetikett", + "classificationAttribute": "Attributt", + "classes": "Klasser", + "classesTip": "Lær om klasser", + "classesStateDesc": "Definer de ulike tilstandene kamerasonen kan være i. For eksempel: 'åpen' og 'lukket' for en garasjeport.", + "classesObjectDesc": "Definer klassene du vil klassifisere oppdagede objekter i. For eksempel: 'bud', 'beboer', 'fremmed' for personklassifisering.", + "classPlaceholder": "Skriv inn klassenavn...", + "errors": { + "nameRequired": "Modellnavn er påkrevd", + "nameLength": "Modellnavn må være på 64 tegn eller mindre", + "nameOnlyNumbers": "Modellnavn kan ikke bare inneholde tall", + "classRequired": "Minst én klasse er påkrevd", + "classesUnique": "Klassenavn må være unike", + "stateRequiresTwoClasses": "Tilstandsmodeller krever minst 2 klasser", + "objectLabelRequired": "Velg en objektetikett", + "objectTypeRequired": "Velg en klassifiseringstype", + "noneNotAllowed": "Klassen 'ingen' er ikke tillatt" + }, + "states": "Tilstander" + }, + "step2": { + "description": "Velg kameraer og definer området som skal overvåkes for hvert kamera. Modellen vil klassifisere tilstanden til disse områdene.", + "cameras": "Kameraer", + "selectCamera": "Velg kamera", + "noCameras": "Klikk + for å legge til kameraer", + "selectCameraPrompt": "Velg et kamera fra listen for å definere overvåkingsområdet" + }, + "step3": { + "selectImagesPrompt": "Velg alle bilder med: {{className}}", + "selectImagesDescription": "Klikk på bilder for å velge dem. Klikk Fortsett når du er ferdig med denne klassen.", + "generating": { + "title": "Genererer eksempelbilder", + "description": "Frigate henter representative bilder fra opptakene dine. Dette kan ta litt tid..." + }, + "training": { + "title": "Trener modell", + "description": "Modellen din trenes i bakgrunnen. Lukk dette vinduet, så starter modellen når treningen er ferdig." + }, + "retryGenerate": "Prøv å generere på nytt", + "noImages": "Ingen eksempelbilder generert", + "classifying": "Klassifiserer og trener...", + "trainingStarted": "Trening startet", + "errors": { + "noCameras": "Ingen kameraer konfigurert", + "noObjectLabel": "Ingen objektetikett valgt", + "generateFailed": "Kunne ikke generere eksempler: {{error}}", + "generationFailed": "Generering mislyktes. Prøv igjen.", + "classifyFailed": "Kunne ikke klassifisere bilder: {{error}}" + }, + "generateSuccess": "Eksempelbilder ble generert", + "allImagesRequired_one": "Vennligst klassifiser alle bildene. {{count}} bilde gjenstår.", + "allImagesRequired_other": "Vennligst klassifiser alle bildene. {{count}} bilder gjenstår.", + "modelCreated": "Modellen ble opprettet. Bruk visningen Nylige klassifiseringer for å legge til bilder for manglende tilstander, og tren deretter modellen.", + "missingStatesWarning": { + "title": "Manglende klasseeksempler", + "description": "Ikke alle klasser har eksempler. Prøv å generere nye eksempler for å finne den manglende klassen, eller fortsett å bruke visningen 'Siste klassifiseringer' for å legge til bilder senere." + }, + "refreshExamples": "Generer nye eksempler", + "refreshConfirm": { + "title": "Generere nye eksempler?", + "description": "Dette vil generere et nytt sett med bilder og tilbakestille alle valg, inkludert tidligere klasser. Du må velge eksempler på nytt for alle klasser." + } + } + }, + "deleteModel": { + "title": "Slett klassifiseringsmodell", + "single": "Er du sikker på at du vil slette {{name}}? Dette vil permanent slette alle tilknyttede data, inkludert bilder og treningsdata. Denne handlingen kan ikke angres.", + "desc": "Er du sikker på at du vil slette {{count}} modell(er)? Dette vil permanent slette alle tilknyttede data, inkludert bilder og treningsdata. Denne handlingen kan ikke angres." + }, + "menu": { + "objects": "Objekter", + "states": "Tilstander" + }, + "details": { + "scoreInfo": "Score representerer gjennomsnittlig klassifiseringskonfidens på tvers av alle deteksjoner av dette objektet.", + "none": "Ingen", + "unknown": "Ukjent" + }, + "tooltip": { + "trainingInProgress": "Modellen trenes nå", + "noNewImages": "Ingen nye bilder å trene på. Klassifiser flere bilder i datasettet først.", + "noChanges": "Ingen endringer i datasettet siden forrige trening.", + "modelNotReady": "Modellen er ikke klar til å trenes" + }, + "edit": { + "title": "Rediger klassifiseringsmodell", + "descriptionState": "Rediger klassene for denne tilstandsklassifiseringsmodellen. Endringer vil kreve at modellen trenes på nytt.", + "descriptionObject": "Rediger objekttypen og klassifiseringstypen for denne objektklassifiseringsmodellen.", + "stateClassesInfo": "Merk: Endring av tilstandsklasser krever at modellen trenes på nytt med de oppdaterte klassene." + }, + "none": "Ingen", + "reclassifyImageAs": "Reklassifiser bilde som:", + "reclassifyImage": "Reklassifiser bilde" +} diff --git a/web/public/locales/nb-NO/views/configEditor.json b/web/public/locales/nb-NO/views/configEditor.json new file mode 100644 index 0000000..2aa6fad --- /dev/null +++ b/web/public/locales/nb-NO/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Konfigurasjonsfil - Frigate", + "toast": { + "error": { + "savingError": "Feil ved lagring av konfigurasjon" + }, + "success": { + "copyToClipboard": "Konfigurasjonen ble kopiert til utklippstavlen." + } + }, + "configEditor": "Konfigurasjonsfil", + "copyConfig": "Kopier konfigurasjonen", + "saveAndRestart": "Lagre og omstart", + "saveOnly": "Kun lagre", + "confirm": "Avslutt uten å lagre?", + "safeConfigEditor": "Konfigurasjonsfil (Sikker modus)", + "safeModeDescription": "Frigate er i sikker modus grunnet en feil i validering av konfigurasjonen." +} diff --git a/web/public/locales/nb-NO/views/events.json b/web/public/locales/nb-NO/views/events.json new file mode 100644 index 0000000..4a46f3f --- /dev/null +++ b/web/public/locales/nb-NO/views/events.json @@ -0,0 +1,94 @@ +{ + "camera": "Kamera", + "empty": { + "alert": "Det er ingen varsler å inspisere", + "detection": "Det er ingen deteksjoner å inspisere", + "motion": "Ingen bevegelsesdata funnet", + "recordingsDisabled": { + "title": "Opptak må være aktivert", + "description": "Inspeksjonselementer kan kun opprettes for et kamera når opptak er aktivert for det kameraet." + } + }, + "timeline": { + "label": "Tidslinje" + }, + "events": { + "label": "Hendelser", + "aria": "Velg hendelser", + "noFoundForTimePeriod": "Ingen hendelser funnet for denne tidsperioden." + }, + "newReviewItems": { + "label": "Vis nye inspeksjonselementer", + "button": "Nye elementer å inspisere" + }, + "alerts": "Varsler", + "detections": "Deteksjoner", + "motion": { + "label": "Bevegelse", + "only": "Kun bevegelse" + }, + "allCameras": "Alle kameraer", + "timeline.aria": "Velg tidslinje", + "documentTitle": "Inspeksjon - Frigate", + "recordings": { + "documentTitle": "Opptak - Frigate", + "invalidSharedLink": "Kunne ikke åpne tidsstemplet opptakslenke på grunn av tolkningsfeil.", + "invalidSharedCamera": "Kunne ikke åpne tidsstemplet opptakslenke på grunn av et ukjent eller uautorisert kamera." + }, + "calendarFilter": { + "last24Hours": "Siste 24 timer" + }, + "markAsReviewed": "Merk som inspisert", + "markTheseItemsAsReviewed": "Merk disse elementene som inspiserte", + "selected_one": "{{count}} valgt", + "selected_other": "{{count}} valgte", + "detected": "detektert", + "suspiciousActivity": "Mistenkelig aktivitet", + "threateningActivity": "Truende aktivitet", + "detail": { + "noDataFound": "Ingen detaljer å inspisere", + "aria": "Vis/skjul detaljvisning", + "trackedObject_one": "{{count}} objekt", + "trackedObject_other": "{{count}} objekter", + "noObjectDetailData": "Ingen detaljdata for objektet tilgjengelig.", + "label": "Detalj", + "settings": "Detaljvisning – innstillinger", + "alwaysExpandActive": { + "desc": "Utvid alltid objektdetaljene for det aktive gjennomgangselementet når tilgjengelig.", + "title": "Utvid alltid for aktive" + } + }, + "objectTrack": { + "trackedPoint": "Sporingspunkt", + "clickToSeek": "Klikk for å gå til dette tidspunktet" + }, + "zoomIn": "Zoom inn", + "zoomOut": "Zoom ut", + "normalActivity": "Normal", + "needsReview": "Trenger inspeksjon", + "securityConcern": "Sikkerhetsrisiko", + "select_all": "Alle", + "motionSearch": { + "menuItem": "Bevegelsessøk", + "openMenu": "Kameravalg" + }, + "motionPreviews": { + "menuItem": "Vis forhåndsvisning av bevegelse", + "title": "Bevegelsesvisning: {{camera}}", + "mobileSettingsTitle": "Innstillinger for forhåndsvisning", + "mobileSettingsDesc": "Juster avspillingshastighet og dimming, og velg en dato for å inspisere klipp med kun bevegelse.", + "dim": "Dimming", + "dimAria": "Juster dimmestyrke", + "dimDesc": "Øk dimming for å gjøre bevegelsesområder tydeligere.", + "speed": "Hastighet", + "speedAria": "Velg avspillingshastighet", + "speedDesc": "Velg hvor raskt klippene skal spilles av.", + "back": "Tilbake", + "empty": "Ingen forhåndsvisninger tilgjengelig", + "noPreview": "Forhåndsvisning utilgjengelig", + "seekAria": "Flytt {{camera}}-avspilleren til {{time}}", + "filter": "Filter", + "filterDesc": "Velg områder for å kun vise klipp med bevegelse i disse sonene.", + "filterClear": "Tøm" + } +} diff --git a/web/public/locales/nb-NO/views/explore.json b/web/public/locales/nb-NO/views/explore.json new file mode 100644 index 0000000..1e3b485 --- /dev/null +++ b/web/public/locales/nb-NO/views/explore.json @@ -0,0 +1,317 @@ +{ + "documentTitle": "Utforsk - Frigate", + "generativeAI": "Generativ AI", + "exploreIsUnavailable": { + "title": "Utforsk er utilgjengelig", + "embeddingsReindexing": { + "startingUp": "Starter opp…", + "estimatedTime": "Estimert gjenværende tid:", + "context": "Utforsk kan brukes etter at reindekseringen av vektorrepresentasjoner for sporede objekter er fullført.", + "finishingShortly": "Avsluttes snart", + "step": { + "thumbnailsEmbedded": "Miniatyrbilder innebygd: ", + "descriptionsEmbedded": "Beskrivelser innebygd: ", + "trackedObjectsProcessed": "Sporede objekter behandlet: " + } + }, + "downloadingModels": { + "setup": { + "visionModel": "Modell for bildegjenkjenning", + "visionModelFeatureExtractor": "Kjennetegnsuttrekker for bildegjenkjenning", + "textModel": "Tekstmodell", + "textTokenizer": "Tekst-tokeniserer" + }, + "context": "Frigate laster ned de nødvendige vektorrepresentasjonsmodellene for å støtte funksjonen for semantisk søk. Dette kan ta flere minutter, avhengig av hastigheten på nettverksforbindelsen din.", + "tips": { + "context": "Du bør vurdere å reindeksere vektorrepresentasjoner for de sporede objektene dine når modellene er lastet ned.", + "documentation": "Se dokumentasjonen" + }, + "error": "En feil har oppstått. Sjekk Frigate-loggene." + } + }, + "objectLifecycle": { + "createObjectMask": "Lag objektmaske", + "adjustAnnotationSettings": "Juster annoteringsinnstillinger", + "scrollViewTips": "Sveip for å se de viktigste hendelsene i dette objektets livssyklus.", + "autoTrackingTips": "Posisjoner for omsluttende boks vil være unøyaktige for kameraer med automatisk sporing.", + "lifecycleItemDesc": { + "visible": "{{label}} oppdaget", + "attribute": { + "other": "{{label}} gjenkjent som {{attribute}}", + "faceOrLicense_plate": "{{attribute}} oppdaget for {{label}}" + }, + "gone": "{{label}} forlot", + "heard": "{{label}} hørt", + "external": "{{label}} oppdaget", + "entered_zone": "{{label}} gikk inn i {{zones}}", + "active": "{{label}} ble aktiv", + "stationary": "{{label}} ble stasjonær", + "header": { + "zones": "Soner", + "ratio": "Forhold", + "area": "Areal" + } + }, + "annotationSettings": { + "title": "Annoteringsinnstillinger", + "showAllZones": { + "title": "Vis alle soner", + "desc": "Vis alltid soner på bilder der objekter har gått inn i en sone." + }, + "offset": { + "documentation": "Se dokumentasjonen ", + "label": "Annoteringsforskyvning", + "desc": "Disse dataene kommer fra kameraets deteksjonsstrøm, men legges over bilder fra opptaksstrømmen. Det er usannsynlig at de to strømmene er perfekt synkronisert. Som et resultat vil ikke den omsluttende boksen og opptakene stemme perfekt overens. Imidlertid kan feltet annotation_offset brukes til å justere dette.", + "millisecondsToOffset": "Millisekunder å forskyve annoteringsdata. Standard: 0", + "tips": "TIPS: Tenk deg et hendelsesklipp med en person som går fra venstre til høyre. Hvis den omsluttende boksen i hendelsestidslinjen konsekvent er til venstre for personen, bør verdien reduseres. Tilsvarende, hvis en person går fra venstre til høyre og den omsluttende boksen konsekvent er foran personen, bør verdien økes.", + "toast": { + "success": "Annoteringsforskyvning for {{camera}} er lagret i konfigurasjonsfilen. Start Frigate på nytt for å aktivere endringene dine." + } + } + }, + "carousel": { + "previous": "Forrige lysbilde", + "next": "Neste lysbilde" + }, + "title": "Objektets livssyklus", + "noImageFound": "Ingen bilder funnet for dette tidsstempelet.", + "count": "{{first}} av {{second}}", + "trackedPoint": "Sporet punkt" + }, + "details": { + "item": { + "title": "Detaljer for inspeksjonelement", + "button": { + "share": "Del dette inspeksjonselementet", + "viewInExplore": "Vis i Utforsk" + }, + "toast": { + "success": { + "updatedSublabel": "Underetikett ble oppdatert.", + "updatedLPR": "Vellykket oppdatering av kjennemerke.", + "regenerate": "En ny beskrivelse har blitt anmodet fra {{provider}}. Avhengig av hastigheten til leverandøren din, kan den nye beskrivelsen ta litt tid å regenerere.", + "audioTranscription": "Lydtranskripsjon ble forespurt. Avhengig av ytelsen på din Frigate server kan transkripsjonen ta noe tid å fullføre.", + "updatedAttributes": "Attributter ble oppdatert." + }, + "error": { + "regenerate": "Feil ved anrop til {{provider}} for en ny beskrivelse: {{errorMessage}}", + "updatedLPRFailed": "Oppdatering av kjennemerke feilet: {{errorMessage}}", + "updatedSublabelFailed": "Feil ved oppdatering av underetikett: {{errorMessage}}", + "audioTranscription": "Forespørsel om lydtranskripsjon feilet: {{errorMessage}}", + "updatedAttributesFailed": "Kunne ikke oppdatere attributter: {{errorMessage}}" + } + }, + "desc": "Detaljer for inspeksjonselement", + "tips": { + "mismatch_one": "{{count}} utilgjengelig objekt ble oppdaget og inkludert i dette inspeksjonselementet. Disse objektene kvalifiserte ikke som et varsel eller deteksjon, eller har allerede blitt ryddet opp/slettet.", + "mismatch_other": "{{count}} utilgjengelige objekter ble oppdaget og inkludert i dette inspeksjonselementet. Disse objektene kvalifiserte ikke som et varsel eller deteksjon, eller har allerede blitt ryddet opp/slettet.", + "hasMissingObjects": "Juster konfigurasjonen hvis du vil at Frigate skal lagre sporede objekter for følgende etiketter: {{objects}}" + } + }, + "topScore": { + "info": "Toppscoren er den høyeste medianverdien for det sporede objektet, så denne kan avvike fra scoren som vises på miniatyrbildet i søkeresultatet.", + "label": "Toppscore" + }, + "estimatedSpeed": "Estimert hastighet", + "objects": "Objekter", + "button": { + "findSimilar": "Finn lignende", + "regenerate": { + "title": "Regenerer", + "label": "Regenerer beskrivelse for sporet objekt" + } + }, + "description": { + "placeholder": "Beskrivelse av det sporede objektet", + "aiTips": "Frigate vil ikke anmode om en beskrivelse fra din generative AI-leverandør før livssyklusen til det sporede objektet er avsluttet.", + "label": "Beskrivelse" + }, + "regenerateFromThumbnails": "Regenerer fra miniatyrbilder", + "tips": { + "descriptionSaved": "Beskrivelsen ble lagret", + "saveDescriptionFailed": "Feil ved lagring av beskrivelse: {{errorMessage}}" + }, + "label": "Etikett", + "editLPR": { + "title": "Rediger kjennemerke", + "descNoLabel": "Skriv inn et nytt kjennemerke for dette sporede objekt", + "desc": "Skriv inn et nytt kjennemerke for denne {{label}}" + }, + "recognizedLicensePlate": "Gjenkjent kjennemerke", + "camera": "Kamera", + "zones": "Soner", + "timestamp": "Tidsstempel", + "expandRegenerationMenu": "Utvid regenereringsmenyen", + "regenerateFromSnapshot": "Regenerer fra stillbilde", + "editSubLabel": { + "title": "Rediger underetikett", + "desc": "Angi en ny underetikett for \"{{label}}\"", + "descNoLabel": "Angi en ny underetikett for dette sporede objektet" + }, + "snapshotScore": { + "label": "Stillbilde score" + }, + "score": { + "label": "Score" + }, + "editAttributes": { + "title": "Rediger attributter", + "desc": "Velg klassifiseringsattributter for denne {{label}}" + }, + "attributes": "Klassifiseringsattributter", + "title": { + "label": "Tittel" + }, + "scoreInfo": "Score-informasjon" + }, + "itemMenu": { + "viewInHistory": { + "label": "Vis i Historikk", + "aria": "Vis i Historikk" + }, + "downloadVideo": { + "aria": "Last ned video", + "label": "Last ned video" + }, + "downloadSnapshot": { + "label": "Last ned stillbilde", + "aria": "Last ned stillbilde" + }, + "viewObjectLifecycle": { + "label": "Vis objektets livssyklus", + "aria": "Vis objektets livssyklus" + }, + "findSimilar": { + "label": "Finn lignende", + "aria": "Finn lignende sporede objekter" + }, + "deleteTrackedObject": { + "label": "Slett dette sporede objektet" + }, + "submitToPlus": { + "label": "Send til Frigate+", + "aria": "Send til Frigate Plus" + }, + "addTrigger": { + "label": "Legg til utløser", + "aria": "Legg til en utløser for dette sporede objektet" + }, + "audioTranscription": { + "label": "Transkriber", + "aria": "Forespør lydtranskripsjon" + }, + "showObjectDetails": { + "label": "Vis objektets sti" + }, + "hideObjectDetails": { + "label": "Skjul objektets sti" + }, + "viewTrackingDetails": { + "label": "Vis sporingsdetaljer", + "aria": "Vis sporingsdetaljene" + }, + "downloadCleanSnapshot": { + "label": "Last ned rent stillbilde", + "aria": "Last ned stillbilde uten markeringer" + }, + "debugReplay": { + "aria": "Vis dette sporede objektet i reprise for feilsøking", + "label": "Reprise for feilsøking" + }, + "more": { + "aria": "Mer" + } + }, + "searchResult": { + "deleteTrackedObject": { + "toast": { + "error": "Feil ved sletting av sporet objekt: {{errorMessage}}", + "success": "Sporet objekt ble slettet." + } + }, + "tooltip": "Samsvarer {{type}} til {{confidence}}%", + "previousTrackedObject": "Forrige sporede objekt", + "nextTrackedObject": "Neste sporede objekt" + }, + "trackedObjectDetails": "Detaljer om sporet objekt", + "type": { + "details": "detaljer", + "snapshot": "stillbilde", + "video": "video", + "object_lifecycle": "objektets livssyklus", + "thumbnail": "miniatyrbilde", + "tracking_details": "sporingsdetaljer" + }, + "dialog": { + "confirmDelete": { + "title": "Bekreft sletting", + "desc": "Sletting av dette sporede objektet fjerner stillbildet, alle lagrede vektorrepresentasjoner og tilknyttede oppføringer for sporingsdetaljer. Opptak av dette objektet i Historikk-visningen vil IKKE bli slettet.

    Er du sikker på at du vil fortsette?" + }, + "toast": { + "error": "Kunne ikke slette dette sporede objektet: {{errorMessage}}" + } + }, + "noTrackedObjects": "Fant ingen sporede objekter", + "fetchingTrackedObjectsFailed": "Feil ved henting av sporede objekter: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} sporet objekt ", + "trackedObjectsCount_other": "{{count}} sporede objekter ", + "exploreMore": "Utforsk flere {{label}} objekter", + "aiAnalysis": { + "title": "AI-Analyse" + }, + "concerns": { + "label": "Bekymringer" + }, + "trackingDetails": { + "title": "Sporingsdetaljer", + "noImageFound": "Ingen bilder funnet for dette tidsstempelet.", + "createObjectMask": "Opprett objektmaske", + "adjustAnnotationSettings": "Juster annoteringsinnstillinger", + "scrollViewTips": "Klikk for å se de viktige øyeblikkene i dette objektets livssyklus.", + "autoTrackingTips": "Posisjonene til markeringsrammer vil være unøyaktige for kameraer med automatisk sporing.", + "count": "{{first}} av {{second}}", + "trackedPoint": "Sporet punkt", + "lifecycleItemDesc": { + "visible": "{{label}} oppdaget", + "entered_zone": "{{label}} gikk inn i {{zones}}", + "active": "{{label}} ble aktiv", + "stationary": "{{label}} ble stasjonær", + "attribute": { + "faceOrLicense_plate": "{{attribute}} oppdaget for {{label}}", + "other": "{{label}} gjenkjent som {{attribute}}" + }, + "gone": "{{label}} forsvant", + "heard": "{{label}} hørt", + "external": "{{label}} oppdaget", + "header": { + "zones": "Soner", + "ratio": "Sideforhold", + "area": "Område", + "score": "Score", + "computedScore": "Beregnet score", + "topScore": "Toppscore", + "toggleAdvancedScores": "Vis/skjul avanserte scoringer" + } + }, + "annotationSettings": { + "title": "Annoteringsinnstillinger", + "showAllZones": { + "title": "Vis alle soner", + "desc": "Alltid vis soner på bilderammer der objekter har gått inn i en sone." + }, + "offset": { + "label": "Annoteringsforskyvning", + "desc": "Disse dataene kommer fra kameraets deteksjonsstrøm, men legges over bilder fra opptaksstrømmen. Det er lite sannsynlig at de to strømmene er perfekt synkronisert. Som et resultat vil markeringsrammen og opptaket ikke stemme perfekt overens. Du kan bruke denne innstillingen til å forskyve annoteringene fremover eller bakover i tid for å tilpasse dem bedre til det innspilte opptaket.", + "millisecondsToOffset": "Antall millisekunder deteksjonsannoteringene skal forskyves med. Standard: 0", + "tips": "Senk verdien hvis videoen ligger foran boksene og punktene på stien, og øk den hvis videoen ligger bak. Verdien kan være negativ.", + "toast": { + "success": "Annoteringsforskyvning for {{camera}} er lagret i konfigurasjonsfilen." + } + } + }, + "carousel": { + "previous": "Forrige lysbilde", + "next": "Neste lysbilde" + } + } +} diff --git a/web/public/locales/nb-NO/views/exports.json b/web/public/locales/nb-NO/views/exports.json new file mode 100644 index 0000000..d455fd8 --- /dev/null +++ b/web/public/locales/nb-NO/views/exports.json @@ -0,0 +1,128 @@ +{ + "documentTitle": "Eksport - Frigate", + "search": "Søk", + "noExports": "Ingen eksporter funnet", + "deleteExport": { + "label": "Slett eksport" + }, + "deleteExport.desc": "Er du sikker på at du vil slette {{exportName}}?", + "editExport": { + "title": "Gi nytt navn til eksport", + "desc": "Skriv inn et nytt navn for denne eksporten.", + "saveExport": "Lagre eksport" + }, + "toast": { + "error": { + "renameExportFailed": "Kunne ikke gi nytt navn til eksport: {{errorMessage}}", + "assignCaseFailed": "Kunne ikke oppdatere sakstilknytning: {{errorMessage}}", + "caseSaveFailed": "Kunne ikke lagre sak: {{errorMessage}}", + "caseDeleteFailed": "Kunne ikke slette sak: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Del eksport", + "downloadVideo": "Last ned video", + "editName": "Rediger navn", + "deleteExport": "Slett eksport", + "assignToCase": "Legg til i sak", + "removeFromCase": "Fjern fra sak" + }, + "caseDialog": { + "nameLabel": "Saksnavn", + "descriptionLabel": "Beskrivelse", + "newCaseOption": "Opprett en ny sak", + "selectLabel": "Sak", + "description": "Velg en eksisterende sak eller opprett en ny.", + "title": "Legg til sak" + }, + "headings": { + "cases": "Saker", + "uncategorizedExports": "Eksporter uten sak" + }, + "toolbar": { + "newCase": "Ny sak", + "addExport": "Legg til eksport", + "editCase": "Rediger sak", + "deleteCase": "Slett sak" + }, + "deleteCase": { + "label": "Slett sak", + "desc": "Er du sikker på at du vil slette {{caseName}}?", + "descKeepExports": "Eksporter vil fortsatt være tilgjengelige som eksporter uten sak.", + "descDeleteExports": "Alle eksporter i denne saken vil bli slettet permanent.", + "deleteExports": "Slett også eksporter" + }, + "caseCard": { + "emptyCase": "Ingen eksporter ennå" + }, + "jobCard": { + "defaultName": "{{camera}}-eksport", + "queued": "Lagt i kø", + "running": "Kjører", + "preparing": "Forbereder", + "copying": "Kopierer", + "encoding": "Enkoder", + "encodingRetry": "Enkoder (nytt forsøk)", + "finalizing": "Fullfører" + }, + "caseView": { + "noDescription": "Ingen beskrivelse", + "createdAt": "Opprettet {{value}}", + "exportCount_one": "1 eksport", + "exportCount_other": "{{count}} eksporter", + "cameraCount_one": "1 kamera", + "cameraCount_other": "{{count}} kameraer", + "showMore": "Vis mer", + "showLess": "Vis mindre", + "emptyTitle": "Denne saken er tom", + "emptyDescription": "Legg til eksisterende eksporter uten sak for å holde orden i saken.", + "emptyDescriptionNoExports": "Det er ingen ledige eksporter tilgjengelig for å legges til ennå." + }, + "caseEditor": { + "createTitle": "Opprett sak", + "editTitle": "Rediger sak", + "namePlaceholder": "Saksnavn", + "descriptionPlaceholder": "Legg til notater eller kontekst for denne saken" + }, + "addExportDialog": { + "title": "Legg til eksport i {{caseName}}", + "searchPlaceholder": "Søk i eksporter uten sak", + "empty": "Ingen eksporter uten sak samsvarer med søket.", + "addButton_one": "Legg til 1 eksport", + "addButton_other": "Legg til {{count}} eksporter", + "adding": "Legger til..." + }, + "selected_one": "{{count}} valgt", + "selected_other": "{{count}} valgte", + "bulkActions": { + "addToCase": "Legg til i sak", + "moveToCase": "Flytt til sak", + "removeFromCase": "Fjern fra sak", + "delete": "Slett", + "deleteNow": "Slett nå" + }, + "bulkDelete": { + "title": "Slett eksporter", + "desc_one": "Er du sikker på at du vil slette {{count}} eksport?", + "desc_other": "Er du sikker på at du vil slette {{count}} eksporter?" + }, + "bulkRemoveFromCase": { + "title": "Fjern fra sak", + "desc_one": "Fjerne {{count}} eksport fra denne saken?", + "desc_other": "Fjerne {{count}} eksporter fra denne saken?", + "descKeepExports": "Eksporter vil bli flyttet til \"uten sak\".", + "descDeleteExports": "Eksporter vil bli slettet permanent.", + "deleteExports": "Slett eksporter i stedet" + }, + "bulkToast": { + "success": { + "delete": "Eksporter ble slettet", + "reassign": "Sakstilknytning ble oppdatert", + "remove": "Eksporter ble fjernet fra saken" + }, + "error": { + "deleteFailed": "Kunne ikke slette eksporter: {{errorMessage}}", + "reassignFailed": "Kunne ikke oppdatere sakstilknytning: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/nb-NO/views/faceLibrary.json b/web/public/locales/nb-NO/views/faceLibrary.json new file mode 100644 index 0000000..4ea7e81 --- /dev/null +++ b/web/public/locales/nb-NO/views/faceLibrary.json @@ -0,0 +1,110 @@ +{ + "selectItem": "Velg {{item}}", + "description": { + "addFace": "Legg til en ny samling i ansiktsbiblioteket ved å laste opp ditt første bilde.", + "placeholder": "Skriv inn et navn for denne samlingen", + "invalidName": "Ugyldig navn. Navn kan kun inneholde bokstaver, tall, mellomrom, apostrof, understrek og bindestrek.", + "nameCannotContainHash": "Navn kan ikke inneholde #." + }, + "details": { + "person": "Person", + "confidence": "Konfidens", + "face": "Ansiktsdetaljer", + "faceDesc": "Detaljer for sporet objekt som genererte dette ansiktet", + "timestamp": "Tidsstempel", + "scoreInfo": "Score er et vektet gjennomsnitt av alle ansiktsscorer, vektet etter størrelsen på ansiktet i hvert bilde.", + "subLabelScore": "Poengsum for under-merkelapp", + "unknown": "Ukjent" + }, + "documentTitle": "Ansiktsbibliotek – Frigate", + "createFaceLibrary": { + "new": "Opprett nytt ansikt", + "title": "Opprett samling", + "desc": "Opprett en ny samling", + "nextSteps": "For å bygge et sterkt grunnlag:
  • Bruk Nylige gjenkjennelser-fanen for å velge og trene på bilder for hver oppdaget person.
  • Fokuser på bilder rett forfra for best resultat; unngå å trene bilder som fanger ansikter i vinkel.
  • " + }, + "train": { + "aria": "Velg nylige gjenkjennelser", + "title": "Nylige gjenkjennelser", + "empty": "Det er ingen nylige forsøk på ansiktsgjenkjenning", + "titleShort": "Nylige", + "emptyNoLibrary": { + "description": "Du må legge til minst ett ansikt i biblioteket for at ansiktsgjenkjenning skal fungere.", + "title": "Last opp et ansikt" + } + }, + "selectFace": "Velg ansikt", + "deleteFaceLibrary": { + "title": "Slett navn", + "desc": "Er du sikker på at du vil slette samlingen {{name}}? Dette vil permanent slette alle tilknyttede ansikter." + }, + "trainFace": "Tren ansikt", + "toast": { + "error": { + "deleteFaceFailed": "Kunne ikke slette: {{errorMessage}}", + "uploadingImageFailed": "Kunne ikke laste opp bilde: {{errorMessage}}", + "trainFailed": "Kunne ikke trene: {{errorMessage}}", + "updateFaceScoreFailed": "Kunne ikke oppdatere ansiktsscore: {{errorMessage}}", + "addFaceLibraryFailed": "Kunne ikke angi ansiktsnavn: {{errorMessage}}", + "deleteNameFailed": "Kunne ikke slette navn: {{errorMessage}}", + "renameFaceFailed": "Kunne ikke gi nytt navn til ansikt: {{errorMessage}}", + "reclassifyFailed": "Kunne ikke reklassifisere ansikt: {{errorMessage}}" + }, + "success": { + "deletedFace_one": "Slettet {{count}} ansikt.", + "deletedFace_other": "Slettet {{count}} ansikter.", + "deletedName_one": "{{count}} ansikt ble slettet.", + "deletedName_other": "{{count}} ansikter ble slettet.", + "trainedFace": "Ansiktet ble trent.", + "updatedFaceScore": "Oppdaterte ansiktsscore for {{name}} ({{score}}).", + "uploadedImage": "Bildet ble lastet opp.", + "addFaceLibrary": "{{name}} ble lagt til i ansiktsbiblioteket!", + "renamedFace": "Nytt navn ble gitt til ansikt {{name}}", + "reclassifiedFace": "Ansikt ble reklassifisert." + } + }, + "imageEntry": { + "dropActive": "Slipp bildet her…", + "dropInstructions": "Dra og slipp, lim inn et bilde her eller klikk for å velge", + "maxSize": "Maks størrelse: {{size}}MB", + "validation": { + "selectImage": "Vennligst velg en bildefil." + } + }, + "readTheDocs": "Se dokumentasjonen", + "button": { + "addFace": "Legg til ansikt", + "uploadImage": "Last opp bilde", + "deleteFaceAttempts": "Slett ansikter", + "reprocessFace": "Prosesser ansiktet på nytt", + "deleteFace": "Slett ansikt", + "renameFace": "Gi nytt navn til ansikt" + }, + "uploadFaceImage": { + "desc": "Last opp et bilde for å skanne etter ansikter og inkludere det for {{pageToggle}}", + "title": "Last opp ansiktsbilde" + }, + "trainFaceAs": "Tren ansikt som:", + "steps": { + "faceName": "Skriv inn ansiktsnavn", + "uploadFace": "Last opp ansiktsbilde", + "nextSteps": "Neste trinn", + "description": { + "uploadFace": "Last opp et bilde av {{name}} som viser ansiktet deres forfra. Bildet trenger ikke å være beskåret til kun ansiktet." + } + }, + "renameFace": { + "desc": "Skriv inn et nytt navn for {{name}}", + "title": "Gi nytt navn til ansikt" + }, + "collections": "Samlinger", + "deleteFaceAttempts": { + "title": "Slett ansikter", + "desc_one": "Er du sikker på at du vil slette {{count}} ansikt? Denne handlingen kan ikke angres.", + "desc_other": "Er du sikker på at du vil slette {{count}} ansikter? Denne handlingen kan ikke angres." + }, + "nofaces": "Ingen ansikter tilgjengelig", + "pixels": "{{area}}piksler", + "reclassifyFaceAs": "Reklassifiser ansikt som:", + "reclassifyFace": "Reklassifiser ansikt" +} diff --git a/web/public/locales/nb-NO/views/live.json b/web/public/locales/nb-NO/views/live.json new file mode 100644 index 0000000..4ba80b0 --- /dev/null +++ b/web/public/locales/nb-NO/views/live.json @@ -0,0 +1,206 @@ +{ + "documentTitle": { + "default": "Direkte - Frigate" + }, + "lowBandwidthMode": "Lav båndbreddemodus", + "documentTitle.withCamera": "{{camera}} - Direkte - Frigate", + "ptz": { + "move": { + "clickMove": { + "label": "Klikk i rammen for å sentrere kameraet", + "enable": "Aktiver klikk for å flytte", + "disable": "Deaktiver klikk for å flytte", + "enableWithZoom": "Aktiver \"klikk for å flytte\" / \"dra for å zoome\"" + }, + "left": { + "label": "Flytt PTZ-kameraet til venstre" + }, + "up": { + "label": "Flytt PTZ-kameraet opp" + }, + "down": { + "label": "Flytt PTZ-kameraet ned" + }, + "right": { + "label": "Flytt PTZ-kameraet til høyre" + } + }, + "presets": "PTZ-kamera forhåndsinnstillinger", + "zoom": { + "in": { + "label": "Zoom inn på PTZ-kameraet" + }, + "out": { + "label": "Zoom ut på PTZ-kameraet" + } + }, + "frame": { + "center": { + "label": "Klikk i rammen for å sentrere PTZ-kameraet" + } + }, + "focus": { + "in": { + "label": "Fokuser inn på PTZ-kameraet" + }, + "out": { + "label": "Fokuser ut på PTZ-kameraet" + } + } + }, + "camera": { + "enable": "Aktiver kamera", + "disable": "Deaktiver kamera", + "turnOn": "Slå kamera på", + "turnOff": "Skå kamera av" + }, + "snapshots": { + "enable": "Aktiver stillbilder", + "disable": "Deaktiver stillbilder" + }, + "audioDetect": { + "enable": "Aktiver lydregistrering", + "disable": "Deaktiver lydregistrering" + }, + "autotracking": { + "enable": "Aktiver automatisk sporing", + "disable": "Deaktiver automatisk sporing" + }, + "manualRecording": { + "tips": "Last ned et stillbilde, eller start en manuell hendelse basert på dette kameraets innstillinger for opptaksbevaring.", + "playInBackground": { + "label": "Spill av i bakgrunnen", + "desc": "Aktiver dette alternativet for å fortsette strømming når spilleren er skjult." + }, + "showStats": { + "label": "Vis statistikk", + "desc": "Aktiver dette alternativet for å vise strømmestatistikk som et overlegg på kamerastrømmen." + }, + "started": "Startet manuelt opptak.", + "end": "Avslutt manuelt opptak", + "title": "Manuelt opptak", + "debugView": "Feilsøkingsvisning", + "start": "Start manuelt opptak", + "failedToStart": "Kunne ikke starte manuelt opptak.", + "recordDisabledTips": "Siden opptak er deaktivert eller begrenset i konfigurasjonen for dette kameraet, vil kun et stillbilde bli lagret.", + "ended": "Avsluttet manuelt opptak.", + "failedToEnd": "Kunne ikke avslutte manuelt opptak." + }, + "audio": "Lyd", + "suspend": { + "forTime": "Pause i: " + }, + "stream": { + "audio": { + "tips": { + "title": "Lyd må være aktivert på kameraet ditt og konfigurert i go2rtc for denne strømmen.", + "documentation": "Se dokumentasjonen " + }, + "available": "Lyd er tilgjengelig for denne strømmen", + "unavailable": "Lyd er ikke tilgjengelig for denne strømmen" + }, + "twoWayTalk": { + "tips": "Enheten din må støtte funksjonen og WebRTC må være konfigurert for toveis tale.", + "tips.documentation": "Se dokumentasjonen ", + "available": "Toveis tale er tilgjengelig for denne strømmen", + "unavailable": "Toveis tale er ikke tilgjengelig for denne strømmen" + }, + "lowBandwidth": { + "tips": "Direktevisning er i lav båndbreddemodus på grunn av buffering eller strømmefeil.", + "resetStream": "Tilbakestill strøm" + }, + "title": "Strøm", + "playInBackground": { + "label": "Spill av i bakgrunnen", + "tips": "Aktiver dette alternativet for å fortsette strømming når spilleren er skjult." + }, + "debug": { + "picker": "Strømmevalg er ikke tilgjengelig i feilsøkingsmodus. Feilsøkingsvisningen bruker alltid strømmen som er tildelt deteksjonsrollen." + } + }, + "history": { + "label": "Vis historiske opptak" + }, + "effectiveRetainMode": { + "modes": { + "all": "Alle", + "motion": "Bevegelse", + "active_objects": "Aktive objekter" + }, + "notAllTips": "Konfigurasjonen for opptaksbevaring for {{source}} er satt til mode: {{effectiveRetainMode}}, så dette manuelle opptaket vil kun beholde segmenter med {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Rediger oppsett", + "group": { + "label": "Rediger kameragruppe" + }, + "exitEdit": "Avslutt redigering" + }, + "twoWayTalk": { + "enable": "Aktiver toveis tale", + "disable": "Deaktiver toveis tale" + }, + "cameraAudio": { + "enable": "Aktiver kameralyd", + "disable": "Deaktiver kameralyd" + }, + "muteCameras": { + "enable": "Demp alle kameraer", + "disable": "Slå på lyd på alle kameraer" + }, + "detect": { + "enable": "Aktiver deteksjon", + "disable": "Deaktiver deteksjon" + }, + "recording": { + "enable": "Aktiver opptak", + "disable": "Deaktiver opptak", + "disabledInConfig": "Opptak må først aktiveres i Innstillinger for dette kameraet." + }, + "streamStats": { + "enable": "Vis Strømmestatistikk", + "disable": "Skjul strømmestatistikk" + }, + "streamingSettings": "Strømmingsinnstillinger", + "notifications": "Meldingsvarsler", + "cameraSettings": { + "title": "{{camera}}-innstillinger", + "cameraEnabled": "Kamera aktivert", + "objectDetection": "Objektdeteksjon", + "recording": "Opptak", + "snapshots": "Stillbilder", + "audioDetection": "Lydregistrering", + "autotracking": "Automatisk sporing", + "transcription": "Lydtranskripsjon", + "camera": "Kamera" + }, + "transcription": { + "enable": "Aktiver direkte lydtranskripsjon", + "disable": "Deaktiver direkte lydtranskripsjon" + }, + "snapshot": { + "noVideoSource": "Ingen videokilde tilgjengelig for stillbilde.", + "captureFailed": "Kunne ikke ta stillbilde.", + "downloadStarted": "Nedlasting av stillbilde startet.", + "takeSnapshot": "Last ned stillbilde" + }, + "noCameras": { + "title": "Ingen kameraer konfigurert", + "description": "Kom i gang ved å koble et kamera til Frigate.", + "buttonText": "Legg til kamera", + "restricted": { + "title": "Ingen kameraer tilgjengelig", + "description": "Du har ikke tilgang for å se noen kameraer i denne gruppen." + }, + "default": { + "title": "Ingen kameraer konfigurert", + "description": "Kom i gang ved å koble et kamera til Frigate.", + "buttonText": "Legg til kamera" + }, + "group": { + "title": "Ingen kameraer i gruppe", + "description": "Denne kameragruppen har ingen tildelte eller aktiverte kameraer.", + "buttonText": "Administrer grupper" + } + } +} diff --git a/web/public/locales/nb-NO/views/motionSearch.json b/web/public/locales/nb-NO/views/motionSearch.json new file mode 100644 index 0000000..79e2a6d --- /dev/null +++ b/web/public/locales/nb-NO/views/motionSearch.json @@ -0,0 +1,80 @@ +{ + "documentTitle": "Bevegelsessøk - Frigate", + "title": "Bevegelsessøk", + "description": "Tegn et polygon for å definere et interesseområde, og angi et tidsrom for å søke etter bevegelsesendringer i dette området.", + "selectCamera": "Bevegelsessøk laster", + "startSearch": "Start søk", + "searchStarted": "Søk startet", + "searchCancelled": "Søk avbrutt", + "cancelSearch": "Avbryt", + "searching": "Søk pågår...", + "searchComplete": "Søk fullført", + "noResultsYet": "Kjør et søk for å finne bevegelsesendringer i det valgte området", + "noChangesFound": "Ingen pikselendringer ble funnet i det valgte området", + "changesFound_one": "Fant {{count}} bevegelsesendring", + "changesFound_other": "Fant {{count}} bevegelsesendringer", + "framesProcessed": "{{count}} bilder behandlet", + "jumpToTime": "Gå til dette tidspunktet", + "results": "Resultater", + "showSegmentHeatmap": "Varmekart", + "newSearch": "Nytt søk", + "clearResults": "Tøm resultater", + "clearROI": "Slett polygon", + "polygonControls": { + "points_one": "{{count}} punkt", + "points_other": "{{count}} punkter", + "undo": "Angre siste punkt", + "reset": "Tilbakestill polygon", + "moveMode": "Flytt", + "drawMode": "Tegn" + }, + "motionHeatmapLabel": "Varmekart for bevegelse", + "dialog": { + "title": "Bevegelsessøk", + "cameraLabel": "Kamera", + "previewAlt": "Forhåndsvisning av kamera for {{camera}}" + }, + "timeRange": { + "title": "Søkeperiode", + "start": "Starttid", + "end": "Sluttid" + }, + "settings": { + "title": "Søkeinnstillinger", + "parallelMode": "Parallellmodus", + "parallelModeDesc": "Skann flere opptaksintervaller samtidig (raskere, men krever mer dekodingsressurser)", + "threshold": "Følsomhetsterskel", + "thresholdDesc": "Lavere verdier detekterer mindre endringer (1–255)", + "minArea": "Minimum endringsområde", + "minAreaDesc": "Minimumsstørrelse for et enkelt bevegelsesområde, som en prosentandel av interesseområdet", + "frameSkip": "Bilde-sprang", + "frameSkipDesc": "Behandle hvert N-te bilde. Sett denne til kameraets bildefrekvens for å behandle ett bilde i sekundet (f.eks. 5 for et 5 FPS-kamera, 30 for et 30 FPS-kamera). Høyere verdier vil være raskere, men kan gå glipp av korte bevegelseshendelser.", + "maxResults": "Maksimalt antall resultater", + "maxResultsDesc": "Stopp etter dette antallet samsvarende tidsstempler" + }, + "errors": { + "noCamera": "Vennligst velg et kamera", + "noROI": "Vennligst tegn et interesseområde", + "noTimeRange": "Vennligst velg en tidsperiode", + "invalidTimeRange": "Sluttid må være etter starttid", + "searchFailed": "Søk mislyktes: {{message}}", + "polygonTooSmall": "Polygonet må ha minst 3 punkter", + "unknown": "Ukjent feil" + }, + "changePercentage": "{{percentage}} % endret", + "metrics": { + "title": "Statistikk for søk", + "segmentsScanned": "Segmenter skannet", + "segmentsProcessed": "Behandlet", + "segmentsSkippedInactive": "Hoppet over (ingen aktivitet)", + "segmentsSkippedHeatmap": "Hoppet over (manglende ROI-overlapp)", + "fallbackFullRange": "Fullskanning som reserve", + "framesDecoded": "Bilder dekodet", + "wallTime": "Søketid", + "segmentErrors": "Segmentfeil", + "seconds": "{{seconds}}s", + "scanSummary": "{{segments}} segmenter · {{time}}", + "minutesSeconds": "{{minutes}}m {{seconds}}s" + }, + "scanning": "Skanner {{time}}" +} diff --git a/web/public/locales/nb-NO/views/recording.json b/web/public/locales/nb-NO/views/recording.json new file mode 100644 index 0000000..262eb43 --- /dev/null +++ b/web/public/locales/nb-NO/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filter", + "export": "Eksporter", + "calendar": "Kalender", + "filters": "Filtre", + "toast": { + "error": { + "noValidTimeSelected": "Ingen gyldig tidsperiode valgt", + "endTimeMustAfterStartTime": "Sluttid må være etter starttid" + } + } +} diff --git a/web/public/locales/nb-NO/views/replay.json b/web/public/locales/nb-NO/views/replay.json new file mode 100644 index 0000000..3cf72e2 --- /dev/null +++ b/web/public/locales/nb-NO/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "Feilsøkingsavspilling", + "description": "Spill av kameraopptak for feilsøking. Objektlisten viser et tidsforsinket sammendrag av detekterte objekter, og fanen Meldinger viser en strøm av Frigates interne meldinger fra avspillingen.", + "websocket_messages": "Meldinger", + "dialog": { + "title": "Start feilsøkingsavspilling", + "description": "Opprett et midlertidig avspillingskamera som repeterer historisk materiale for å feilsøke problemer med objektdeteksjon og sporing. Avspillingskameraet vil ha samme deteksjonskonfigurasjon som kildekameraet. Velg et tidsrom for å begynne.", + "camera": "Kildekamera", + "timeRange": "Tidsrom", + "preset": { + "1m": "Siste minutt", + "5m": "Siste 5 minutter", + "timeline": "Fra tidslinje", + "custom": "Egendefinert" + }, + "startButton": "Start avspilling", + "selectFromTimeline": "Velg", + "starting": "Starter avspilling...", + "startLabel": "Start", + "endLabel": "Slutt", + "toast": { + "error": "Kunne ikke starte feilsøkingsavspilling: {{error}}", + "alreadyActive": "En avspillingsøkt er allerede aktiv", + "stopError": "Kunne ikke stoppe feilsøkingsavspilling: {{error}}", + "goToReplay": "Gå til avspilling" + } + }, + "page": { + "noSession": "Ingen aktiv feilsøkingsøkt", + "noSessionDesc": "Start en feilsøkingsavspilling fra Historikk-visningen ved å klikke på Handlinger-knappen i verktøylinjen og velge Feilsøkingsavspilling.", + "goToRecordings": "Gå til historikk", + "preparingClip": "Forbereder klipp…", + "preparingClipDesc": "Frigate syr sammen opptak for det valgte tidsrommet. Dette kan ta et minutt for lengre perioder.", + "startingCamera": "Starter feilsøkingsavspilling…", + "startError": { + "title": "Kunne ikke starte feilsøkingsavspilling", + "back": "Tilbake til historikk" + }, + "sourceCamera": "Kildekamera", + "replayCamera": "Avspillingskamera", + "initializingReplay": "Initialiserer feilsøkingsavspilling...", + "stoppingReplay": "Stopper feilsøkingsavspilling...", + "stopReplay": "Stopp avspilling", + "confirmStop": { + "title": "Stoppe feilsøkingsavspilling?", + "description": "Dette vil stoppe økten og slette alle midlertidige data. Er du sikker?", + "confirm": "Stopp avspilling", + "cancel": "Avbryt" + }, + "activity": "Aktivitet", + "objects": "Objektliste", + "audioDetections": "Lyd-deteksjoner", + "noActivity": "Ingen aktivitet detektert", + "activeTracking": "Aktiv sporing", + "noActiveTracking": "Ingen aktiv sporing", + "configuration": "Konfigurasjon", + "configurationDesc": "Finjuster innstillinger for bevegelsesdeteksjon og objektsporing for feilsøkingskameraet. Ingen endringer lagres i din Frigate-konfigurasjonsfil." + } +} diff --git a/web/public/locales/nb-NO/views/search.json b/web/public/locales/nb-NO/views/search.json new file mode 100644 index 0000000..f25bf70 --- /dev/null +++ b/web/public/locales/nb-NO/views/search.json @@ -0,0 +1,75 @@ +{ + "search": "Søk", + "savedSearches": "Lagrede søk", + "searchFor": "Søk etter {{inputValue}}", + "button": { + "clear": "Fjern søk", + "save": "Lagre søk", + "delete": "Slett lagret søk", + "filterInformation": "Filterinformasjon", + "filterActive": "Filtre aktive" + }, + "filter": { + "label": { + "cameras": "Kameraer", + "labels": "Etiketter", + "search_type": "Søketype", + "after": "Etter", + "min_score": "Min. score", + "max_score": "Maks. score", + "min_speed": "Min. hastighet", + "zones": "Soner", + "sub_labels": "Underetiketter", + "time_range": "Tidsintervall", + "before": "Før", + "max_speed": "Maks. hastighet", + "recognized_license_plate": "Gjenkjent kjennemerke", + "has_clip": "Har videoklipp", + "has_snapshot": "Har stillbilde", + "attributes": "Attributter" + }, + "searchType": { + "thumbnail": "Miniatyrbilde", + "description": "Beskrivelse" + }, + "toast": { + "error": { + "minSpeedMustBeLessOrEqualMaxSpeed": "Minimum hastighet 'min_speed' må være mindre enn eller lik maksimum hastighet 'max_speed'.", + "beforeDateBeLaterAfter": "Før-datoen 'before' må være senere enn etter-datoen 'after'.", + "afterDatebeEarlierBefore": "Etter-datoen 'after' må være tidligere enn før-datoen 'before'.", + "minScoreMustBeLessOrEqualMaxScore": "Minimum score 'min_score' må være mindre enn eller lik maksimum score 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Maksimum score 'max_score' må være større enn eller lik minimum score 'min_score'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Maksimum hastighet 'max_speed' må være større enn eller lik minimum hastighet 'min_speed'." + } + }, + "tips": { + "title": "Hvordan bruke tekstfiltre", + "desc": { + "text": "Filtre hjelper deg med å begrense søkeresultatene dine. Slik bruker du dem i inndatafeltet:", + "example": "Eksempel: cameras:inngangsdør label:person before:01012024 time_range:3:00PM-4:00PM ", + "step": "
    • Skriv inn et filternavn etterfulgt av et kolon (f.eks. \"cameras:\").
    • Velg en verdi fra forslagene eller skriv inn din egen.
    • Bruk flere filtre ved å legge dem til etter hverandre med mellomrom imellom.
    • Dato-filtre (before: og after:) bruker {{DateFormat}}-formatet.
    • Tidsintervall-filteret bruker {{exampleTime}}-formatet.
    • Fjern filtre ved å klikke på 'x' ved siden av dem.
    ", + "step2": "Velg en verdi fra forslagene eller skriv inn din egen.", + "step4": "Dato-filtre (before: and after:) bruker {{DateFormat}} format.", + "step5": "Tidsintervall-filter bruker {{exampleTime}} format.", + "step6": "Fjern filtre ved å klikke på 'x'en ved siden av dem.", + "exampleLabel": "Eksempel:", + "step1": "Skriv inn et filter-nøkkelnavn etterfulgt av et kolon (f.eks \"cameras:\").", + "step3": "Bruk flere filtre ved å legge dem til en etter en, med mellomrom." + } + }, + "header": { + "currentFilterType": "Filterverdier", + "noFilters": "Filtre", + "activeFilters": "Aktive filtre" + } + }, + "placeholder": { + "search": "Søk…" + }, + "trackedObjectId": "Sporings-ID for objekt", + "similaritySearch": { + "title": "Søk etter likhet", + "active": "Søk etter likhet er aktivt", + "clear": "Fjern søk etter likhet" + } +} diff --git a/web/public/locales/nb-NO/views/settings.json b/web/public/locales/nb-NO/views/settings.json new file mode 100644 index 0000000..73de4de --- /dev/null +++ b/web/public/locales/nb-NO/views/settings.json @@ -0,0 +1,2251 @@ +{ + "documentTitle": { + "default": "Innstillinger - Frigate", + "authentication": "Autentiseringsinnstillinger - Frigate", + "camera": "Kamerainnstillinger - Frigate", + "masksAndZones": "Maske- og soneeditor - Frigate", + "motionTuner": "Bevegelsesjustering - Frigate", + "object": "Test og feilsøk - Frigate", + "general": "Innstillinger for brukergrensesnitt - Frigate", + "classification": "Klassifiseringsinnstillinger - Frigate", + "frigatePlus": "Frigate+ innstillinger - Frigate", + "notifications": "Innstillinger for meldingsvarsler - Frigate", + "enrichments": "Innstillinger for utvidelser - Frigate", + "cameraManagement": "Administrer kameraer - Frigate", + "cameraReview": "Innstillinger for kamerainspeksjon - Frigate", + "globalConfig": "Global konfigurasjon - Frigate", + "cameraConfig": "Kamerakonfigurasjon - Frigate", + "profiles": "Profiler - Frigate", + "maintenance": "Vedlikehold - Frigate", + "detectorsAndModel": "Detektorer og modell - Frigate" + }, + "menu": { + "classification": "Klassifisering", + "cameras": "Kamerainnstillinger", + "masksAndZones": "Masker / Soner", + "motionTuner": "Finjustering av bevegelse", + "debug": "Test og feilsøk", + "users": "Brukere", + "frigateplus": "Frigate+", + "ui": "Brukergrensesnitt", + "notifications": "Meldingsvarsler", + "enrichments": "Utvidelser", + "triggers": "Utløsere", + "cameraManagement": "Kamerabehandling", + "cameraReview": "Inspeksjon", + "roles": "Roller", + "profiles": "Profiler", + "cameraFaceRecognition": "Ansiktsgjenkjenning", + "integrationFaceRecognition": "Ansiktsgjenkjenning", + "systemAuthentication": "Autentisering", + "cameraMotion": "Bevegelsesdeteksjon", + "globalMotion": "Bevegelsesdeteksjon", + "systemUi": "Brukergrensesnitt", + "cameraUi": "Brukergrensesnitt for kamera", + "systemDatabase": "Database", + "systemDetectionModel": "Deteksjonsmodell", + "cameraLivePlayback": "Direkteavspilling", + "globalLivePlayback": "Direkteavspilling", + "cameraFfmpeg": "Strømmer (FFmpeg)", + "globalFfmpeg": "FFmpeg", + "systemFfmpeg": "FFmpeg", + "cameraBirdseye": "Fugleperspektiv", + "systemBirdseye": "Fugleperspektiv", + "integrationGenerativeAi": "Generativ AI", + "general": "Generelt", + "globalConfig": "Global konfigurasjon", + "systemGo2rtcStreams": "go2rtc-strømmer", + "uiSettings": "Innstillinger for brukergrensesnitt", + "cameraConfigReview": "Inspeksjon", + "globalReview": "Inspeksjon", + "integrations": "Integrasjoner", + "cameraLpr": "Kjennemerke-gjenkjenning", + "integrationLpr": "Kjennemerke-gjenkjenning", + "systemLogging": "Logging", + "cameraAudioEvents": "Lyd-deteksjon", + "globalAudioEvents": "Lyd-deteksjon", + "cameraAudioTranscription": "Lydtranskripsjon", + "integrationAudioTranscription": "Lydtranskripsjon", + "systemDetectorHardware": "Maskinvare for detektor", + "mediaSync": "Mediesynkronisering", + "cameraNotifications": "Meldingsvarsler", + "systemEnvironmentVariables": "Miljøvariabler", + "cameraMqttConfig": "MQTT", + "systemMqtt": "MQTT", + "cameraMqtt": "MQTT for kamera", + "systemNetworking": "Nettverk", + "cameraDetect": "Objektdeteksjon", + "globalDetect": "Objektdeteksjon", + "cameraObjects": "Objekter", + "globalObjects": "Objekter", + "integrationObjectClassification": "Objektklassifisering", + "cameraOnvif": "ONVIF", + "cameraRecording": "Opptak", + "globalRecording": "Opptak", + "systemProxy": "Proxy", + "regionGrid": "Regionrutenett", + "integrationSemanticSearch": "Semantisk søk", + "cameraSnapshots": "Stillbilder", + "globalSnapshots": "Stillbilde", + "cameraTimestampStyle": "Stil for tidsstempel", + "globalTimestampStyle": "Stil for tidsstempel", + "system": "System", + "systemTelemetry": "Telemetri", + "systemTls": "TLS", + "maintenance": "Vedlikehold", + "systemDetectorsAndModel": "Detektorer og modell" + }, + "dialog": { + "unsavedChanges": { + "title": "Du har ulagrede endringer.", + "desc": "Vil du lagre endringene dine før du fortsetter?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Ingen kamera" + }, + "general": { + "liveDashboard": { + "playAlertVideos": { + "label": "Spill av varselvideoer", + "desc": "Som standard vises nylige varsler på Direkte-dashbord som små videoer som gjentas. Deaktiver dette alternativet for kun å vise et statisk bilde av nylige varsler på denne enheten/nettleseren." + }, + "title": "Direkte-dashbord", + "automaticLiveView": { + "label": "Automatisk direktevisning", + "desc": "Bytt automatisk til et kameras direktevisning når aktivitet oppdages. Deaktivering av dette valget gjør at statiske kamerabilder i Direkte-dashbord kun oppdateres én gang i minuttet." + }, + "displayCameraNames": { + "label": "Vis alltid kameranavn", + "desc": "Vis alltid kameranavnene i en merkelapp i dashbordet for direktevisning med flere kameraer." + }, + "liveFallbackTimeout": { + "label": "Tidsavbrudd for reserveløsning for direkteavspiller", + "desc": "Når et kameras direktestrøm med høy kvalitet er utilgjengelig, bytt til lav båndbreddemodus etter dette antallet sekunder. Standard: 3." + } + }, + "storedLayouts": { + "title": "Lagrede oppsett", + "desc": "Kameraplasseringer i en gruppe kan dras og endres. Posisjonene lagres lokalt i nettleseren.", + "clearAll": "Fjern alle oppsett" + }, + "recordingsViewer": { + "title": "Opptaksvisning", + "defaultPlaybackRate": { + "label": "Standard avspillingshastighet", + "desc": "Standard hastighet for avspilling av opptak." + } + }, + "calendar": { + "firstWeekday": { + "sunday": "Søndag", + "label": "Første ukedag", + "desc": "Dagen ukene starter på i inspeksjonskalenderen.", + "monday": "Mandag" + }, + "title": "Kalender" + }, + "toast": { + "success": { + "clearStreamingSettings": "Strømmingsinnstillinger for alle kameragrupper ble fjernet.", + "clearStoredLayout": "Lagret oppsett for {{cameraName}} ble fjernet" + }, + "error": { + "clearStoredLayoutFailed": "Kunne ikke fjerne lagret oppsett: {{errorMessage}}", + "clearStreamingSettingsFailed": "Kunne ikke fjerne strømmingsinnstillinger: {{errorMessage}}" + } + }, + "title": "Innstillinger for brukergrensesnitt", + "cameraGroupStreaming": { + "title": "Strømmingsinnstillinger for kameragrupper", + "desc": "Strømmingsinnstillingene lagres lokalt i nettleseren.", + "clearAll": "Fjern alle strømmingsinnstillinger" + } + }, + "classification": { + "semanticSearch": { + "title": "Semantisk søk", + "desc": "Semantisk søk i Frigate lar deg finne sporede objekter i inspeksjonselementene ved hjelp av enten bildet, en egendefinert tekstbeskrivelse eller en automatisk generert beskrivelse.", + "reindexNow": { + "confirmTitle": "Bekreft reindeksering", + "error": "Kunne ikke starte reindeksering: {{errorMessage}}", + "label": "Reindekser nå", + "confirmButton": "Reindekser", + "success": "Reindeksering startet.", + "alreadyInProgress": "Reindeksering pågår allerede.", + "desc": "Reindeksering vil regenerere vektorrepresentasjoner for alle sporede objekter. Prosessen kjøres i bakgrunnen, kan belaste CPU-en maksimalt og ta mye tid avhengig av antall sporede objekter.", + "confirmDesc": "Er du sikker på at du vil reindeksere vektorrepresentasjoner for alle sporede objekter? Dette vil kjøres i bakgrunnen, men kan bruke all CPU og ta tid. Du kan følge fremdriften på Utforsk-siden." + }, + "readTheDocumentation": "Les dokumentasjonen", + "modelSize": { + "label": "Modellstørrelse", + "small": { + "title": "liten", + "desc": "Ved å bruke liten brukes en kvantisert modell som bruker mindre RAM og er raskere, med ubetydelig tap av kvalitet for vektorrepresentasjoner." + }, + "large": { + "title": "stor", + "desc": "Ved å bruke stor brukes hele Jina-modellen og den vil automatisk bruke GPU hvis tilgjengelig." + }, + "desc": "Størrelsen på modellen som brukes for vektorrepresentasjoner for semantiske søk." + } + }, + "faceRecognition": { + "title": "Ansiktsgjenkjenning", + "modelSize": { + "small": { + "title": "liten", + "desc": "Liten bruker en FaceNet-modell for vektorrepresentasjoner som fungerer effektivt på de fleste CPU-er." + }, + "large": { + "title": "stor", + "desc": "Stor bruker en ArcFace-modell for vektorrepresentasjoner og vil automatisk kjøre på GPU hvis tilgjengelig." + }, + "label": "Modellstørrelse", + "desc": "Størrelsen på modellen brukt for ansiktsgjenkjenning." + }, + "readTheDocumentation": "Les dokumentasjonen", + "desc": "Ansiktsgjenkjenning lar deg tilordne navn til personer. Når et ansikt gjenkjennes, legges navnet til som under-merkelapp. Denne informasjonen vises i brukergrensesnittet, i filtre, samt i meldingsvarsler." + }, + "licensePlateRecognition": { + "title": "Gjenkjenning av kjennemerker", + "readTheDocumentation": "Les dokumentasjonen", + "desc": "Frigate kan gjenkjenne kjennemerker og automatisk legge inn tall/bokstaver i 'recognized_license_plate'-feltet, eller et kjent navn som under-merkelapp til objekter av typen bil. Vanlig bruk er å lese kjennemerker ved innkjørsel eller på vei." + }, + "title": "Klassifiseringsinnstillinger", + "toast": { + "success": "Klassifiseringsinnstillinger lagret. Start Frigate på nytt for å bruke endringene.", + "error": "Kunne ikke lagre konfigurasjonsendringer: {{errorMessage}}" + }, + "birdClassification": { + "title": "Artsbestemmelse for fugler", + "desc": "Artsbestemmelse identifiserer kjente fugler ved hjelp av en kvantisert TensorFlow-modell. Når en kjent fugl gjenkjennes, legges det vanlige navnet til som en under-merkelapp. Denne informasjonen vises i brukergrensesnittet, i filtre, samt i meldingsvarsler." + }, + "restart_required": "Omstart påkrevd (Klassifiseringsinnstillinger endret)", + "unsavedChanges": "Ulagrede endringer i klassifiseringsinnstillinger" + }, + "camera": { + "streams": { + "desc": "Midlertidig deaktiver et kamera til Frigate startes på nytt. Deaktivering av et kamera stopper Frigates behandling av dette kameraets strømmer fullstendig. Deteksjon, opptak og feilsøking vil være utilgjengelig.
    Merk: Dette deaktiverer ikke go2rtc-restrømming.", + "title": "Strømmer" + }, + "reviewClassification": { + "title": "Inspeksjonssklassifisering", + "zoneObjectAlertsTips": "Alle {{alertsLabels}}-objekter oppdaget i {{zone}} på {{cameraName}} vises som varsler.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert i {{zone}} på {{cameraName}}, vises som deteksjoner.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert på {{cameraName}}, vises som deteksjoner uavhengig av sone.", + "notSelectDetections": "Alle {{detectionsLabels}}-objekter oppdaget i {{zone}} på {{cameraName}} som ikke er kategorisert som Varsler, vises som deteksjoner uavhengig av sone." + }, + "selectAlertsZones": "Velg soner for varsler", + "desc": "Frigate kategoriserer inspeksjonselementer som Varsler og Deteksjoner. Som standard regnes alle person- og bil-objekter som Varsler. Du kan finjustere klassifiseringen ved å konfigurere nødvendige soner.", + "readTheDocumentation": "Se dokumentasjonen", + "noDefinedZones": "Ingen soner er definert for dette kameraet.", + "objectAlertsTips": "Alle {{alertsLabels}}-objekter på {{cameraName}} vises som varsler.", + "objectDetectionsTips": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert på {{cameraName}}, vises som deteksjoner uavhengig av sone.", + "selectDetectionsZones": "Velg soner for deteksjoner", + "limitDetections": "Avgrens deteksjoner til bestemte soner", + "toast": { + "success": "Konfigurasjonen for inspeksjonsklassifisering er lagret. Start Frigate på nytt for å bruke endringer." + }, + "unsavedChanges": "Ulagrede innstillinger for inspeksjonsklassifisering for {{camera}}" + }, + "title": "Kamerainnstillinger", + "review": { + "title": "Inspeksjon", + "desc": "Aktiver/deaktiver varsler og deteksjoner midlertidig for dette kameraet til Frigate startes på nytt. Når deaktivert, vil det ikke genereres nye inspeksjonselementer. ", + "alerts": "Varsler ", + "detections": "Deteksjoner " + }, + "object_descriptions": { + "desc": "Midlertidig aktiver/deaktiver generative KI-objektbeskrivelser for dette kameraet. Når deaktivert, vil KI-genererte beskrivelser ikke bli forespurt for sporede objekter på dette kameraet.", + "title": "Generative KI-objektbeskrivelser" + }, + "cameraConfig": { + "nameInvalid": "Kameranavnet kan bare inneholde bokstaver, tall, understreker eller bindestreker", + "add": "Legg til kamera", + "edit": "Rediger kamera", + "description": "Konfigurer kamerainnstillinger, inkludert strømmeinnganger og roller.", + "name": "Kamera navn", + "nameRequired": "Kamera navn er påkrevd", + "nameLength": "Kamera navn må være mindre enn 24 tegn.", + "namePlaceholder": "f.eks front_dør", + "enabled": "Aktivert", + "ffmpeg": { + "inputs": "Inngangsstrømmer", + "path": "Lenke til strøm", + "pathRequired": "Lenke til strøm er påkrevd", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "Minst én rolle er påkrevd", + "rolesUnique": "Hver rolle (lyd, gjenkjenning, opptak) kan bare tildeles én strøm", + "addInput": "Legg til inngangsstrøm", + "removeInput": "Fjern inngangsstrøm", + "inputsRequired": "Minst én inngangsstrøm er påkrevd" + }, + "toast": { + "success": "Kamera {{cameraName}} ble lagret" + } + }, + "review_descriptions": { + "title": "Generative KI beskrivelser for inspeksjon", + "desc": "Midlertidig aktiver/deaktiver inspeksjonsbeskrivelser med generativ KI for dette kameraet. Når deaktivert, vil det ikke bli bedt om KI-genererte beskrivelser for inspeksjonselementer på dette kameraet." + }, + "addCamera": "Legg til nytt kamera", + "editCamera": "Rediger kamera:", + "selectCamera": "Velg et kamera", + "backToSettings": "Tilbake til kamerainnstillinger" + }, + "masksAndZones": { + "filter": { + "all": "Alle masker og soner" + }, + "toast": { + "success": { + "copyCoordinates": "Koordinater for {{polyName}} kopiert til utklippstavlen." + }, + "error": { + "copyCoordinatesFailed": "Kunne ikke kopiere koordinater til utklippstavlen." + } + }, + "form": { + "zoneName": { + "error": { + "mustNotBeSameWithCamera": "Sonenavnet kan ikke være det samme som kameranavnet.", + "alreadyExists": "En sone med dette navnet finnes allerede for dette kameraet.", + "mustBeAtLeastTwoCharacters": "Sonenavnet må være minst 2 tegn langt.", + "mustNotContainPeriod": "Sonenavnet kan ikke inneholde punktum.", + "hasIllegalCharacter": "Sonenavnet inneholder ugyldige tegn.", + "mustHaveAtLeastOneLetter": "Sonenavnet må inneholde minst én bokstav." + } + }, + "distance": { + "error": { + "mustBeFilled": "Alle avstandsfeltene må fylles ut for å bruke hastighetsestimering.", + "text": "Avstanden må være større enn eller lik 0,1." + } + }, + "polygonDrawing": { + "delete": { + "title": "Bekreft sletting", + "desc": "Er du sikker på at du vil slette {{type}} {{name}}?", + "success": "{{name}} har blitt slettet." + }, + "removeLastPoint": "Fjern siste punkt", + "reset": { + "label": "Fjern alle punkter" + }, + "snapPoints": { + "true": "Fest punkter", + "false": "Ikke fest punkter" + }, + "error": { + "mustBeFinished": "Tegningen av polygonet må fullføres før lagring." + }, + "type": { + "zone": "sone", + "motion_mask": "bevegelsesmaske", + "object_mask": "objektmaske" + }, + "revertOverride": { + "title": "Tilbakestill til basiskonfigurasjon", + "desc": "Dette vil fjerne profiloverstyringen for {{type}} {{name}} og tilbakestille til basiskonfigurasjonen." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Treghet må være over 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Oppholdstid må være større enn eller lik 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Terskelverdi for hastighet må være større enn eller lik 0.1." + } + }, + "id": { + "error": { + "alreadyExists": "En maske med denne ID-en eksisterer allerede for dette kameraet.", + "mustNotBeEmpty": "ID kan ikke være tom." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Navn kan ikke være tomt." + } + } + }, + "zones": { + "label": "Soner", + "documentTitle": "Rediger sone - Frigate", + "edit": "Rediger sone", + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkter", + "clickDrawPolygon": "Klikk for å tegne et polygon på bildet.", + "inertia": { + "title": "Treghet", + "desc": "Angir hvor mange bilder et objekt må være i en sone før det regnes som en del av sonen. Standard: 3" + }, + "desc": { + "title": "Soner lar deg definere et spesifikt område i bildet, slik at du kan bestemme om et objekt er innenfor et bestemt område.", + "documentation": "Dokumentasjon" + }, + "add": "Legg til sone", + "name": { + "title": "Navn", + "inputPlaceHolder": "Skriv inn et navn…", + "tips": "Navnet må være minst 2 tegn langt, inneholde minst én bokstav, og må ikke være det samme som et kamera- eller sone-navn for dette kameraet." + }, + "loiteringTime": { + "title": "Oppholdstid", + "desc": "Setter minimumstid i sekunder som objektet må være i sonen for at den skal aktiveres. Standard: 0" + }, + "objects": { + "title": "Objekter", + "desc": "Liste over objekter som gjelder for denne sonen." + }, + "allObjects": "Alle objekter", + "speedEstimation": { + "title": "Hastighetsestimering", + "desc": "Aktiver hastighetsestimering for objekter i denne sonen. Sonen må ha nøyaktig 4 punkter.", + "docs": "Se dokumentasjonen", + "lineADistance": "Linje A avstand ({{unit}})", + "lineBDistance": "Linje B avstand ({{unit}})", + "lineCDistance": "Linje C avstand ({{unit}})", + "lineDDistance": "Linje D avstand ({{unit}})" + }, + "speedThreshold": { + "title": "Hastighetsgrense ({{unit}})", + "desc": "Angir en minimumshastighet for objekter for at de skal anses som en del av denne sonen.", + "toast": { + "error": { + "pointLengthError": "Hastighetsestimering er deaktivert for denne sonen. Soner med hastighetsestimering må ha nøyaktig 4 punkter.", + "loiteringTimeError": "Soner med oppholdstider større enn 0 bør ikke brukes til hastighetsestimering." + } + } + }, + "toast": { + "success": "Sone ({{zoneName}}) er lagret." + }, + "enabled": { + "title": "Aktivert", + "description": "Om denne sonen er aktiv og aktivert i konfigurasjonsfilen. Hvis deaktivert, kan den ikke aktiveres via MQTT. Deaktiverte soner ignoreres ved kjøring." + } + }, + "motionMasks": { + "label": "Bevegelsesmasker", + "desc": { + "documentation": "Dokumentasjon", + "title": "Bevegelsesmasker brukes til å hindre uønsket type bevegelse fra å utløse deteksjon. For mye maskering kan gjøre det vanskeligere å spore objekter." + }, + "add": "Ny bevegelsesmaske", + "documentTitle": "Rediger bevegelsesmaske - Frigate", + "edit": "Rediger bevegelsesmaske", + "context": { + "title": "Bevegelsesmasker brukes til å hindre uønsket type bevegelse fra å utløse deteksjon (eksempel: tregrener, kameratidsstempler). Bevegelsesmasker bør brukes svært sparsomt. For mye maskering vil gjøre det vanskeligere å spore objekter.", + "documentation": "Se dokumentasjonen" + }, + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkter", + "clickDrawPolygon": "Klikk for å tegne et polygon på bildet.", + "polygonAreaTooLarge": { + "title": "Bevegelsesmasken dekker {{polygonArea}}% av kameraets bilde. Store bevegelsesmasker anbefales ikke.", + "tips": "Bevegelsesmasker hindrer ikke objektene fra å bli detektert. Du bør bruke en påkrevd sone i stedet.", + "documentation": "Se dokumentasjonen" + }, + "toast": { + "success": { + "title": "{{polygonName}} er lagret.", + "noName": "Bevegelsesmasken er lagret." + } + }, + "defaultName": "Bevegelsesmaske {{number}}", + "name": { + "description": "Et valgfritt visningsnavn for denne bevegelsesmasken.", + "title": "Navn", + "placeholder": "Skriv inn et navn..." + } + }, + "objectMasks": { + "clickDrawPolygon": "Klikk for å tegne et polygon på bildet.", + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkter", + "label": "Objektmasker", + "documentTitle": "Rediger objektmaske - Frigate", + "desc": { + "title": "Objektfiltermasker brukes for å filtrere ut falske positiver for en gitt objekttype basert på plassering.", + "documentation": "Dokumentasjon" + }, + "add": "Legg til objektmaske", + "edit": "Rediger objektmaske", + "context": "Objektfiltermasker brukes for å filtrere ut falske positiver for en gitt objekttype basert på plassering.", + "objects": { + "title": "Objekter", + "desc": "Objekttypen som gjelder for denne objektmasken.", + "allObjectTypes": "Alle objekttyper" + }, + "toast": { + "success": { + "title": "{{polygonName}} er lagret.", + "noName": "Objektmasken er lagret." + } + }, + "name": { + "description": "Et valgfritt visningsnavn for denne objektmasken.", + "title": "Navn", + "placeholder": "Skriv inn et navn..." + } + }, + "restart_required": "Omstart påkrevd (masker/soner endret)", + "motionMaskLabel": "Bevegelsesmaske {{number}}", + "objectMaskLabel": "Objektmaske {{number}}", + "profileBase": "(basis)", + "profileOverride": "(overstyring)", + "masks": { + "enabled": { + "title": "Aktivert", + "description": "Om denne masken er aktivert i konfigurasjonsfilen. Hvis deaktivert, kan den ikke aktiveres via MQTT. Deaktiverte masker ignoreres ved kjøring." + } + }, + "disabledInConfig": "Elementet er deaktivert i konfigurasjonsfilen", + "addDisabledProfile": "Legg til i basiskonfigurasjonen først, og overstyr deretter i profilen" + }, + "motionDetectionTuner": { + "title": "Finjustering av bevegelsesdeteksjon", + "Threshold": { + "title": "Terskel", + "desc": "Terskelverdien bestemmer hvor mye endring i en piksels lysstyrke som kreves for å bli betraktet som bevegelse. Standard: 30" + }, + "contourArea": { + "title": "Konturområde", + "desc": "Konturområdets verdi brukes til å bestemme hvilke grupper av endrede piksler som kvalifiserer som bevegelse. Standard: 10" + }, + "improveContrast": { + "desc": "Forbedre kontrasten for mørkere scener. Standard: PÅ", + "title": "Forbedre kontrast" + }, + "desc": { + "title": "Frigate bruker bevegelsesdeteksjon som en første sjekk for å se om det skjer noe i bildet som er verdt å sjekke med objektdeteksjon.", + "documentation": "Les guiden for bevegelsesjustering" + }, + "toast": { + "success": "Bevegelsesinnstillingene er lagret." + }, + "unsavedChanges": "Ulagrede endringer i bevegelsesjustering ({{camera}})" + }, + "debug": { + "title": "Test og feilsøking", + "objectList": "Objektliste", + "noObjects": "Ingen objekter", + "boundingBoxes": { + "title": "Markeringsrammer", + "desc": "Vis markeringsrammer rundt sporede objekter", + "colors": { + "label": "Farge på markeringsrammer for objekt", + "info": "
  • Ved oppstart vil forskjellige farger bli tildelt hver objekttype
  • En mørkeblå tynn linje indikerer at objektet ikke er detektert på dette tidspunktet
  • En grå tynn linje indikerer at objektet er detektert som stasjonært
  • En tykk linje indikerer at objektet er under autosporing (når aktivert)
  • " + } + }, + "timestamp": { + "title": "Tidsstempel", + "desc": "Legg et tidsstempel over bildet" + }, + "zones": { + "title": "Soner", + "desc": "Vis en kontur av alle definerte soner" + }, + "motion": { + "desc": "Vis bokser rundt områder der bevegelse er detektert", + "tips": "

    Bevegelsesbokser


    Røde bokser vil vises på områder i bildet hvor bevegelse for øyeblikket blir detektert

    ", + "title": "Bevegelsesbokser" + }, + "regions": { + "tips": "

    Regionbokser


    Lysegrønne bokser vil vises på områder av interesse i bildet som blir sendt til objektdetektoren.

    ", + "title": "Regioner", + "desc": "Vis en boks for interesseområdet sendt til objektdetektoren" + }, + "objectShapeFilterDrawing": { + "document": "Se dokumentasjonen ", + "score": "Score", + "ratio": "Sideforhold", + "area": "Areal", + "title": "Tegning av objektformfilter", + "desc": "Tegn et rektangel på bildet for å vise areal- og størrelsesforhold", + "tips": "Aktiver dette alternativet for å tegne et rektangel på kamerabildet for å vise areal og størrelsesforholdet. Disse verdiene kan deretter brukes til å sette filterparametere for objektform i konfigurasjonen." + }, + "detectorDesc": "Frigate bruker dine detektorer ({{detectors}}) for å oppdage objekter i kameraets videostrøm.", + "desc": "Test og feilsøk viser sporede objekter i sanntid og deres statistikk. Objektlisten viser en tidsforsinket oppsummering av detekterte objekter.", + "debugging": "Test og feilsøk", + "mask": { + "title": "Bevegelsesmasker", + "desc": "Vis polygoner for bevegelsesmasker" + }, + "openCameraWebUI": "Åpne {{camera}} sitt nettgrensesnitt", + "audio": { + "title": "Lyd", + "noAudioDetections": "Ingen lyddeteksjoner", + "score": "score", + "currentRMS": "Nåværende RMS", + "currentdbFS": "Nåværende dbFS" + }, + "paths": { + "title": "Stier", + "desc": "Vis betydningsfulle punkter på det sporede objektets sti", + "tips": "

    Stier


    Linjer og sirkler vil indikere viktige punkter som det sporede objektet har beveget seg gjennom i løpet av sin livssyklus.

    " + } + }, + "users": { + "title": "Brukere", + "management": { + "title": "Brukeradministrasjon", + "desc": "Administrer brukerprofiler for denne Frigate-instansen." + }, + "addUser": "Legg til bruker", + "updatePassword": "Nullstill passord", + "toast": { + "success": { + "deleteUser": "Bruker {{user}} ble slettet", + "updatePassword": "Passordet ble oppdatert.", + "createUser": "Bruker {{user}} ble opprettet", + "roleUpdated": "Rolle oppdatert for {{user}}" + }, + "error": { + "deleteUserFailed": "Kunne ikke slette bruker: {{errorMessage}}", + "setPasswordFailed": "Kunne ikke lagre passord: {{errorMessage}}", + "createUserFailed": "Kunne ikke opprette bruker: {{errorMessage}}", + "roleUpdateFailed": "Kunne ikke oppdatere rolle: {{errorMessage}}" + } + }, + "dialog": { + "form": { + "user": { + "placeholder": "Skriv inn brukernavn", + "title": "Brukernavn", + "desc": "Bare bokstaver, tall, punktum og understreker tillatt." + }, + "password": { + "title": "Passord", + "placeholder": "Skriv inn passord", + "confirm": { + "placeholder": "Bekreft passord", + "title": "Bekreft passord" + }, + "strength": { + "title": "Passordstyrke: ", + "veryStrong": "Veldig sterkt", + "weak": "Svakt", + "medium": "Medium", + "strong": "Sterkt" + }, + "match": "Passordene samsvarer", + "notMatch": "Passordene samsvarer ikke", + "show": "Vis passord", + "hide": "Skjul passord", + "requirements": { + "title": "Passordkrav:", + "length": "Minst 12 tegn", + "uppercase": "Minst en stor bokstav", + "digit": "Minst ett tall", + "special": "Minst ett spesialtegn (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nytt passord", + "placeholder": "Skriv inn nytt passord", + "confirm": { + "placeholder": "Skriv inn nytt passord igjen" + } + }, + "usernameIsRequired": "Brukernavn er påkrevd", + "passwordIsRequired": "Passord er påkrevd", + "currentPassword": { + "title": "Nåværende passord", + "placeholder": "Skriv inn nåværende passord" + } + }, + "changeRole": { + "desc": "Oppdater tillatelser for {{username}}", + "title": "Endre brukerrolle", + "roleInfo": { + "intro": "Velg en passende rolle for denne bruker:", + "admin": "Administrator", + "adminDesc": "Full tilgang til alle funksjoner.", + "viewer": "Visningsbruker", + "viewerDesc": "Begrenset til kun Direkte-dashbord, Inspiser, Utforsk og Eksporter.", + "customDesc": "Tilpasset rolle med spesifikk kameratilgang." + }, + "select": "Velg en rolle" + }, + "createUser": { + "title": "Opprett ny bruker", + "desc": "Legg til en ny brukerkonto og spesifiser en rolle for tilgang til Frigate-brukergrensesnittet.", + "usernameOnlyInclude": "Brukernavn kan bare inneholde bokstaver, tall, punktum eller _", + "confirmPassword": "Vennligst bekreft ditt passord" + }, + "deleteUser": { + "title": "Slett bruker", + "desc": "Denne handlingen kan ikke angres. Dette vil permanent slette brukerkontoen og fjerne alle tilknyttede data.", + "warn": "Er du sikker på at du vil slette {{username}}?" + }, + "passwordSetting": { + "updatePassword": "Oppdater passord for {{username}}", + "setPassword": "Angi passord", + "desc": "Opprett et sterkt passord for å sikre denne kontoen.", + "cannotBeEmpty": "Passordet kan ikke være tomt", + "doNotMatch": "Passordene samsvarer ikke", + "currentPasswordRequired": "Nåværende passord er påkrevd", + "incorrectCurrentPassword": "Nåværende passord er feil", + "passwordVerificationFailed": "Kunne ikke verifisere passord", + "multiDeviceWarning": "Andre enheter du er logget inn på vil kreve ny innlogging innen {{refresh_time}}.", + "multiDeviceAdmin": "Du kan også tvinge alle brukere til å logge inn på nytt ved å endre JWT (JSON Web Token)-nøkkelen." + } + }, + "table": { + "username": "Brukernavn", + "actions": "Handlinger", + "role": "Rolle", + "changeRole": "Endre brukerrolle", + "password": "Nullstill passord", + "deleteUser": "Slett bruker", + "noUsers": "Ingen brukere funnet." + } + }, + "notification": { + "notificationSettings": { + "desc": "Frigate kan sende push-varsler til enheten din når den kjører i nettleseren eller er installert som en progressiv webapplikasjon (PWA).", + "documentation": "Se dokumentasjonen", + "title": "Innstillinger for meldingsvarsler" + }, + "notificationUnavailable": { + "documentation": "Se dokumentasjonen", + "title": "Meldingsvarsler utilgjengelig", + "desc": "Nettleser push-varsler krever et sikkert miljø (https://…). Dette er en nettleserbegrensning. Få tilgang til Frigate på en sikker måte for å bruke meldingsvarsler.", + "descPwa": "På iOS er web-pushvarsler kun tilgjengelig når Frigate er installert på hjemskjermen din. Åpne Del-menyen, velg Legg til på hjemskjermen, og åpne deretter Frigate fra det nye ikonet for å registrere denne enheten for varsler." + }, + "email": { + "title": "E-post", + "placeholder": "f.eks. eksempel@email.com", + "desc": "En gyldig e-postadresse kreves og vil bli brukt til å varsle deg om det skulle oppstå problemer med push-tjenesten." + }, + "cameras": { + "title": "Kameraer", + "noCameras": "Ingen kameraer tilgjengelig", + "desc": "Velg hvilke kameraer meldingsvarsler skal aktiveres for." + }, + "deviceSpecific": "Enhetsspesifikke innstillinger", + "registerDevice": "Registrer denne enheten", + "unregisterDevice": "Fjern registrering av enheten", + "suspendTime": { + "5minutes": "Suspender i 5 minutter", + "10minutes": "Suspender i 10 minutter", + "30minutes": "Suspender i 30 minutter", + "1hour": "Suspender i 1 time", + "12hours": "Suspender i 12 timer", + "24hours": "Suspender i 24 timer", + "untilRestart": "Suspender til omstart", + "suspend": "Suspender" + }, + "suspended": "Meldingsvarsler suspendert {{time}}", + "toast": { + "success": { + "registered": "Registrering for meldingsvarsler var vellykket. En omstart av Frigate er nødvendig før noen meldingsvarsler (inkludert et testvarsel) kan sendes.", + "settingSaved": "Innstillinger for meldingsvarsler er lagret." + }, + "error": { + "registerFailed": "Kunne ikke lagre registrering for meldingsvarsler." + } + }, + "globalSettings": { + "title": "Globale innstillinger", + "desc": "Midlertidig suspender meldingsvarsler for spesifikke kameraer på alle registrerte enheter." + }, + "cancelSuspension": "Avbryt suspensjon", + "title": "Meldingsvarsler", + "sendTestNotification": "Send en meldingsvarsel for test", + "active": "Meldingsvarsler aktivert", + "unsavedChanges": "Ulagrede endringer for meldingsvarsler", + "unsavedRegistrations": "Ulagrede registreringer for meldingsvarsler" + }, + "frigatePlus": { + "apiKey": { + "notValidated": "Frigate+ API-nøkkel er ikke detektert eller validert", + "title": "Frigate+ API-nøkkel", + "validated": "Frigate+ API-nøkkel er detektert og validert", + "desc": "Frigate+ API-nøkkelen muliggjør integrasjon med Frigate+ tjenesten.", + "plusLink": "Les mer om Frigate+" + }, + "modelInfo": { + "trainDate": "Treningsdato", + "baseModel": "Basismodell", + "loading": "Laster modellinformasjon…", + "error": "Kunne ikke laste modellinformasjon", + "loadingAvailableModels": "Laster tilgjengelige modeller…", + "title": "Modellinformasjon", + "modelType": "Modelltype", + "supportedDetectors": "Støttede detektorer", + "dimensions": "Dimensjoner", + "cameras": "Kameraer", + "availableModels": "Tilgjengelige Frigate+ modeller", + "modelSelect": "Dine tilgjengelige modeller på Frigate+ kan velges her. Merk at bare modeller som er kompatible med din nåværende detektorkonfigurasjon kan velges.", + "plusModelType": { + "userModel": "Finjustert", + "baseModel": "Basismodell" + }, + "noModelLoaded": "Ingen Frigate+-modell er lastet inn for øyeblikket.", + "selectModel": "Velg en modell", + "noModelsAvailable": "Ingen modeller tilgjengelig", + "filter": { + "ariaLabel": "Filtrer modeller etter type", + "baseModels": "Basismodeller", + "fineTunedModels": "Finjusterte modeller" + } + }, + "title": "Frigate+ Innstillinger", + "snapshotConfig": { + "title": "Konfigurasjon av stillbilde", + "desc": "Innsending til Frigate+ krever at både stillbilder og clean_copy-stillbilder er aktivert i konfigurasjonen din.", + "documentation": "Se dokumentasjonen", + "table": { + "camera": "Kamera", + "snapshots": "Stillbilder", + "cleanCopySnapshots": "clean_copy-stillbilder" + }, + "cleanCopyWarning": "Noen kameraer har stillbilder deaktivert" + }, + "toast": { + "success": "Frigate+ innstillingene er lagret. Start Frigate på nytt for å aktivere endringene.", + "error": "Kunne ikke lagre konfigurasjonsendringer: {{errorMessage}}" + }, + "restart_required": "Omstart påkrevd (Frigate+ modell endret)", + "unsavedChanges": "Ulagrede endringer for Frigate+ innstillinger", + "cardTitles": { + "otherModels": "Andre modeller", + "api": "API", + "currentModel": "Gjeldende modell", + "configuration": "Konfigurasjon" + }, + "description": "Frigate+ er en abonnementstjeneste som gir tilgang til tilleggsfunksjoner og kapasiteter for Frigate-instansen din, inkludert muligheten til å bruke egendefinerte objektdeteksjonsmodeller trent på dine egne data. Du kan administrere innstillingene for Frigate+-modellen din her.", + "changeInDetectorsAndModel": "Endre modell" + }, + "enrichments": { + "title": "Innstillinger for utvidelser", + "licensePlateRecognition": { + "desc": "Frigate kan gjenkjenne kjennemerker på kjøretøy og automatisk legge til de oppdagede tegnene i feltet \"recognized_license_plate\", eller et kjent navn som en underetikett på objekter av typen bil. Et vanlig brukstilfelle kan være å lese kjennemerker på biler som kjører inn i en innkjørsel eller biler som passerer på en gate.", + "title": "Kjennemerke gjenkjenning", + "readTheDocumentation": "Se dokumentasjonen" + }, + "birdClassification": { + "desc": "Fugleklassifisering identifiserer kjente fugler ved hjelp av en kvantisert TensorFlow-modell. Når en fugl gjenkjennes, vil det vanlige navnet legges til som en underetikett. Denne informasjonen vises i brukergrensesnittet, filtre, samt i meldingsvarsler.", + "title": "Klassifisering av fugler" + }, + "semanticSearch": { + "reindexNow": { + "desc": "Reindeksering vil regenerere vektorrepresentasjoner for alle sporede objekter. Denne prosessen kjøres i bakgrunnen og kan maksimere CPU-belastningen, samt ta en del tid avhengig av hvor mange sporede objekter du har.", + "confirmButton": "Reindekser", + "confirmTitle": "Bekreft reindeksering", + "confirmDesc": "Er du sikker på at du vil reindeksere alle vektorrepresentasjoner for sporede objekter? Denne prosessen vil kjøre i bakgrunnen, men den kan maksimere CPU-belastningen og ta en del tid. Du kan følge fremdriften på Utforsk-siden.", + "label": "Reindekser nå", + "success": "Reindeksering ble startet.", + "alreadyInProgress": "Reindeksering pågar allerede.", + "error": "Kunne ikke starte reindeksering: {{errorMessage}}" + }, + "modelSize": { + "small": { + "desc": "Ved å bruke liten benyttes en kvantisert versjon av modellen som bruker mindre RAM og kjører raskere på CPU, med en svært ubetydelig forskjell i kvalitet på vektorrepresentasjoner.", + "title": "liten" + }, + "label": "Modellstørrelse", + "desc": "Størrelsen på modellen brukt til vektorrepresentasjoner for semantisk søk.", + "large": { + "title": "stor", + "desc": "Ved å bruke stor benyttes den fullstendige Jina-modellen, og den vil automatisk kjøres på GPU dersom tilgjengelig." + } + }, + "title": "Semantisk søk", + "desc": "Semantisk søk i Frigate lar deg finne sporede objekter i inspeksjonsselementene dine ved å bruke enten selve bildet, en brukerdefinert tekstbeskrivelse eller en automatisk generert beskrivelse.", + "readTheDocumentation": "Se dokumentasjonen" + }, + "faceRecognition": { + "modelSize": { + "small": { + "title": "liten", + "desc": "Ved å bruke liten benyttes en FaceNet-modell for vektorrepresentasjoner for ansikt som kjører effektivt på de fleste CPU-er." + }, + "label": "Modellstørrelse", + "desc": "Størrelsen på modellen brukt til ansiktsgjenkjenning.", + "large": { + "title": "stor", + "desc": "Ved å bruke stor benyttes en ArcFace-modell for vektorrepresentasjoner for ansikt, og den vil automatisk kjøres på GPU dersom tilgjengelig." + } + }, + "title": "Ansiktsgjenkjenning", + "desc": "Ansiktsgjenkjenning gjør det mulig å tildele navn til personer, og når ansiktet deres gjenkjennes, vil Frigate tildele personens navn som en underetikett. Denne informasjonen vises i brukergrensesnittet, filtre, samt i meldingsvarsler.", + "readTheDocumentation": "Se dokumentasjonen" + }, + "unsavedChanges": "Ulagrede endringer i innstillinger for utvidelser", + "restart_required": "Omstart påkrevd (Innstillinger for utvidelser er endret)", + "toast": { + "success": "Innstillinger for utvidelser har blitt lagret. Start Frigate på nytt for å aktivere endringene.", + "error": "Kunne ikke lagre konfigurasjonsendringer: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Utløsere", + "management": { + "title": "Utløser", + "desc": "Administrer utløsere for {{camera}}. Bruk miniatyrbilde-type for å utløse på lignende miniatyrbilder som det sporede objektet du har valgt, og beskrivelsestype for å utløse på lignende beskrivelser basert på teksten du spesifiserer." + }, + "addTrigger": "Legg til utløser", + "table": { + "name": "Navn", + "type": "Type", + "content": "Innhold", + "threshold": "Terskel", + "actions": "Handlinger", + "noTriggers": "Ingen utløsere er konfigurert for dette kameraet.", + "edit": "Rediger", + "deleteTrigger": "Slett utløser", + "lastTriggered": "Sist utløst" + }, + "type": { + "thumbnail": "Miniatyrbilde", + "description": "Beskrivelse" + }, + "actions": { + "alert": "Marker som varsel", + "notification": "Send meldingsvarsel", + "sub_label": "Legg til underetikett", + "attribute": "Legg til attributt" + }, + "dialog": { + "createTrigger": { + "title": "Opprett utløser", + "desc": "Opprett en utløser for kamera {{camera}}" + }, + "editTrigger": { + "title": "Rediger utløser", + "desc": "Rediger innstillingene for utløser på kamera {{camera}}" + }, + "deleteTrigger": { + "title": "Slett utløser", + "desc": "Er du sikker på at du vil slette utløseren {{triggerName}}? Denne handlingen kan ikke angres." + }, + "form": { + "name": { + "title": "Navn", + "placeholder": "Navngi denne utløseren", + "error": { + "minLength": "Feltet må være minst 2 tegn langt.", + "invalidCharacters": "Feltet kan bare inneholde bokstaver, tall, understreker og bindestreker.", + "alreadyExists": "En utløser med dette navnet finnes allerede for dette kameraet." + }, + "description": "Skriv inn et unikt navn eller beskrivelse for å identifisere denne utløseren" + }, + "enabled": { + "description": "Aktiver eller deaktiver denne utløseren" + }, + "type": { + "title": "Type", + "placeholder": "Velg utløsertype", + "description": "Utløs når en lignende sporet objektbeskrivelse blir detektert", + "thumbnail": "Utløs når et lignende sporet miniatyrbilde blir detektert" + }, + "content": { + "title": "Innhold", + "imagePlaceholder": "Velg et miniatyrbilde", + "textPlaceholder": "Skriv inn tekstinnhold", + "imageDesc": "Kun de siste 100 miniatyrbildene vises. Hvis du ikke finner ønsket miniatyrbilde, kan du se gjennom tidligere objekter i Utforsk og opprette en utløser fra menyen der.", + "textDesc": "Skriv inn tekst for å utløse denne handlingen når en lignende beskrivelse av et sporet objekt oppdages.", + "error": { + "required": "Innhold er påkrevd." + } + }, + "threshold": { + "title": "Terskel", + "error": { + "min": "Terskelverdien må minst være 0", + "max": "Terskelverdien kan maksimum være 1" + }, + "desc": "Angi likhetsgrensen for denne utløseren. En høyere grense betyr at et høyere samsvar kreves for å utløse hendelsen." + }, + "actions": { + "title": "Handlinger", + "desc": "Som standard sender Frigate en MQTT-melding for alle utløsere. Underetiketter legger til navnet på utløseren i objektetiketten. Attributter er søkbare metadata som lagres separat i objektets sporingsmetadata.", + "error": { + "min": "Minst én handling må velges." + } + }, + "friendly_name": { + "description": "Et valgfritt brukervennlig navn eller beskrivende tekst for denne utløseren.", + "title": "Brukervennlig navn", + "placeholder": "Navngi eller beskriv denne utløseren" + } + } + }, + "toast": { + "success": { + "createTrigger": "Utløseren {{name}} ble opprettet.", + "updateTrigger": "Utløseren {{name}} ble oppdatert.", + "deleteTrigger": "Utløseren {{name}} ble slettet." + }, + "error": { + "createTriggerFailed": "Kunne ikke opprette utløser: {{errorMessage}}", + "updateTriggerFailed": "Kunne ikke oppdatere utløser: {{errorMessage}}", + "deleteTriggerFailed": "Kunne ikke slette utløser: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Semantisk søk er deaktivert", + "desc": "Semantisk søk må aktiveres for å bruke utløsere." + }, + "wizard": { + "title": "Opprett utløser", + "step1": { + "description": "Konfigurer grunnleggende innstillinger for utløseren." + }, + "step2": { + "description": "Sett opp innholdet som skal utløse denne handlingen." + }, + "step3": { + "description": "Konfigurer terskelen og handlingene for denne utløseren." + }, + "steps": { + "nameAndType": "Navn og type", + "configureData": "Konfigurer data", + "thresholdAndActions": "Terskel og handlinger" + } + } + }, + "roles": { + "management": { + "title": "Administrasjon av visningsrolle", + "desc": "Administrer tilpassede visningsroller og deres kameratilgangstillatelser for denne Frigate-instansen." + }, + "addRole": "Legg til rolle", + "table": { + "role": "Rolle", + "cameras": "Kameraer", + "actions": "Handlinger", + "noRoles": "Ingen tilpassede roller funnet.", + "editCameras": "Rediger kameraer", + "deleteRole": "Slett rolle" + }, + "toast": { + "success": { + "createRole": "Rollen {{role}} ble opprettet", + "updateCameras": "Kameraer oppdatert for rollen {{role}}", + "deleteRole": "Rollen {{role}} ble slettet", + "userRolesUpdated_one": "{{count}} bruker tildelt denne rollen er oppdatert til \"visningsbruker\", som har tilgang til alle kameraer.", + "userRolesUpdated_other": "{{count}} brukere tildelt denne rollen er oppdatert til \"visningsbruker\", som har tilgang til alle kameraer." + }, + "error": { + "createRoleFailed": "Kunne ikke opprette rolle: {{errorMessage}}", + "updateCamerasFailed": "Kunne ikke oppdatere kameraer: {{errorMessage}}", + "deleteRoleFailed": "Kunne ikke slette rolle: {{errorMessage}}", + "userUpdateFailed": "Kunne ikke oppdatere brukerroller: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Opprett ny rolle", + "desc": "Legg til en ny rolle og angi tillatelser for kameratilgang." + }, + "editCameras": { + "desc": "Oppdater kameratilgang for rollen {{role}}.", + "title": "Rediger kameraer for rolle" + }, + "deleteRole": { + "title": "Slett rolle", + "desc": "Denne handlingen kan ikke angres. Dette vil permanent slette rollen og tildele alle brukere med denne rollen til «visningsbruker»-rollen, som gir tilgang til alle kameraer.", + "warn": "Er du sikker på at du vil slette {{role}}?", + "deleting": "Sletter..." + }, + "form": { + "role": { + "title": "Rollenavn", + "placeholder": "Skriv inn rollenavn", + "desc": "Kun bokstaver, tall, punktum og understreker er tillatt.", + "roleIsRequired": "Rollenavn er påkrevd", + "roleOnlyInclude": "Rollenavn kan kun inneholde bokstaver, tall, . eller _", + "roleExists": "En rolle med dette navnet finnes allerede." + }, + "cameras": { + "title": "Kameraer", + "desc": "Velg hvilke kameraer denne rollen skal ha tilgang til. Minst ett kamera må velges.", + "required": "Minst ett kamera må velges." + } + } + } + }, + "cameraWizard": { + "title": "Legg til kamera", + "description": "Følg trinnene nedenfor for å legge til et nytt kamera i din Frigate-installasjon.", + "steps": { + "nameAndConnection": "Navn og tilkobling", + "streamConfiguration": "Strømkonfigurasjon", + "validationAndTesting": "Validering og testing", + "probeOrSnapshot": "Sjekk eller stillbilde" + }, + "save": { + "success": "Lagret nytt kamera {{cameraName}}.", + "failure": "Feil ved lagring av {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Oppløsning", + "video": "Video", + "audio": "Lyd", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Vennligst oppgi en gyldig strøm-URL", + "testFailed": "Strømmetest feilet: {{error}}" + }, + "step1": { + "description": "Skriv inn kameradetaljene dine og velg å sjekke kameraet eller manuelt velg produsent.", + "cameraName": "Kameranavn", + "cameraNamePlaceholder": "f.eks. front_dor eller Hageoversikt", + "host": "Vert/IP-adresse", + "port": "Port", + "username": "Brukernavn", + "usernamePlaceholder": "Valgfritt", + "password": "Passord", + "passwordPlaceholder": "Valgfritt", + "selectTransport": "Velg transportprotokoll", + "cameraBrand": "Kameramerke", + "selectBrand": "Velg kameramerke for URL-mal", + "customUrl": "Egendefinert strømme-URL", + "brandInformation": "Merkevare-informasjon", + "brandUrlFormat": "For kameraer med RTSP URL-format som: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://brukernavn:passord@vert:port/sti", + "testConnection": "Test tilkobling", + "testSuccess": "Tilkoblingstesten var vellykket!", + "testFailed": "Tilkoblingstesten feilet. Vennligst sjekk inntastingen din og prøv igjen.", + "streamDetails": "Strømdetaljer", + "warnings": { + "noSnapshot": "Kunne ikke hente et øyeblikksbilde fra den konfigurerte strømmen." + }, + "errors": { + "brandOrCustomUrlRequired": "Enten velg et kameramerke med vert/IP eller velg 'Annet' med en egendefinert URL", + "nameRequired": "Kameranavn er påkrevd", + "nameLength": "Kameranavnet må være 64 tegn eller mindre", + "invalidCharacters": "Kameranavnet inneholder ugyldige tegn", + "nameExists": "Kameranavnet finnes allerede", + "brands": { + "reolink-rtsp": "Reolink RTSP anbefales ikke. Aktiver HTTP i kameraets fastvare-innstillinger og start kameraveiviseren på nytt." + }, + "customUrlRtspRequired": "Egendefinerte URL-er må begynne med «rtsp://» eller «rtsps://». Manuell konfigurasjon kreves for kamerastrømmer som ikke bruker RTSP." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Henter kamerametadata…", + "fetchingSnapshot": "Henter øyeblikksbilde for kamera..." + }, + "connectionSettings": "Tilkoblingsinnstillinger", + "detectionMethod": "Metode for strømdeteksjon", + "onvifPort": "ONVIF-port", + "probeMode": "Sjekk kamera", + "manualMode": "Manuelt valg", + "detectionMethodDescription": "Sjekk kameraet med ONVIF (hvis støttet) for å finne kameraets strømme-URL-er, eller velg kameramerke manuelt for å bruke forhåndsdefinerte URL-er. For å skrive inn en egendefinert RTSP URL, velg manuell metode og velg \"Annet\".", + "onvifPortDescription": "For kameraer som støtter ONVIF, er dette vanligvis 80 eller 8080.", + "useDigestAuth": "Bruk digest-autentisering", + "useDigestAuthDescription": "Bruk HTTP digest-autentisering for ONVIF. Noen kameraer kan kreve et dedikert ONVIF brukernavn/passord i stedet for standard administrator-bruker." + }, + "step2": { + "description": "Sjekk kameraet for tilgjengelige strømmer eller konfigurer manuelle innstillinger basert på valgt deteksjonsmetode.", + "streamsTitle": "Kamerastrømmer", + "addStream": "Legg til strøm", + "addAnotherStream": "Legg til en annen strøm", + "streamTitle": "Strøm {{number}}", + "streamUrl": "Strøm-URL", + "streamUrlPlaceholder": "rtsp://brukernavn:passord@vert:port/sti", + "url": "URL", + "resolution": "Oppløsning", + "selectResolution": "Velg oppløsning", + "quality": "Kvalitet", + "selectQuality": "Velg kvalitet", + "roles": "Roller", + "roleLabels": { + "detect": "Objektdeteksjon", + "record": "Opptak", + "audio": "Lyd" + }, + "testStream": "Test tilkobling", + "testSuccess": "Tilkoblingstest vellykket!", + "testFailed": "Tilkoblingstest feilet. Vennligst sjekk inndataene dine og prøv igjen.", + "testFailedTitle": "Test feilet", + "connected": "Tilkoblet", + "notConnected": "Ikke tilkoblet", + "featuresTitle": "Funksjoner", + "go2rtc": "Reduser tilkoblinger til kameraet", + "detectRoleWarning": "Minst én strøm må ha rollen «deteksjon» for å fortsette.", + "rolesPopover": { + "title": "Strømroller", + "detect": "Hovedstrøm for objektdeteksjon.", + "record": "Lagrer segmenter av videostrømmen basert på konfigurasjonsinnstillinger.", + "audio": "Strøm for lydbasert deteksjon." + }, + "featuresPopover": { + "title": "Strømfunksjoner", + "description": "Bruk go2rtc-restrømming for å redusere antall tilkoblinger til kameraet ditt." + }, + "streamDetails": "Strømdetaljer", + "probing": "Test kamera...", + "retry": "Prøv igjen", + "testing": { + "probingMetadata": "Sjekker metadata for kamera...", + "fetchingSnapshot": "Henter stillbilde fra kamera..." + }, + "probeFailed": "Kunne ikke å sjekke kamera: {{error}}", + "probingDevice": "Tester enhet...", + "probeSuccessful": "Sjekk vellykket", + "probeError": "Sjekk feilet", + "probeNoSuccess": "Sjekk mislyktes", + "deviceInfo": "Enhetsinformasjon", + "manufacturer": "Produsent", + "model": "Modell", + "firmware": "Fastvare", + "profiles": "Profiler", + "ptzSupport": "PTZ-støtte", + "autotrackingSupport": "Autosporing-støtte", + "presets": "Forhåndsinnstillinger", + "rtspCandidates": "RTSP-kandidater", + "rtspCandidatesDescription": "Følgende RTSP URL-er ble funnet ved sjekk av kameraet. Test tilkoblingen for å se strømmetadata.", + "noRtspCandidates": "Ingen RTSP URL-er ble funnet fra kameraet. Det kan hende at påloggingsinformasjonen din er feil, eller at kameraet ikke støtter ONVIF eller metoden som brukes for å hente RTSP URL-er. Gå tilbake og skriv inn RTSP URL-en manuelt.", + "candidateStreamTitle": "Kandidat {{number}}", + "useCandidate": "Bruk", + "uriCopy": "Kopier", + "uriCopied": "URI kopiert til utklippstavlen", + "testConnection": "Test tilkobling", + "toggleUriView": "Klikk for å vise/skjule full URI", + "errors": { + "hostRequired": "Vert/IP-adresse er påkrevd" + } + }, + "step3": { + "description": "Konfigurer strømroller og legg til flere strømmer for kameraet ditt.", + "validationTitle": "Strømvalidering", + "connectAllStreams": "Koble til alle strømmer", + "reconnectionSuccess": "Gjenoppkobling vellykket.", + "reconnectionPartial": "Noen strømmer kunne ikke gjenoppkobles.", + "streamUnavailable": "Forhåndsvisning av strøm utilgjengelig", + "reload": "Last inn på nytt", + "connecting": "Kobler til...", + "streamTitle": "Strøm {{number}}", + "valid": "Gyldig", + "failed": "Feilet", + "notTested": "Ikke testet", + "connectStream": "Koble til", + "connectingStream": "Kobler til", + "disconnectStream": "Koble fra", + "estimatedBandwidth": "Estimert båndbredde", + "roles": "Roller", + "none": "Ingen", + "error": "Feil", + "streamValidated": "Strøm {{number}} ble validert", + "streamValidationFailed": "Validering av strøm {{number}} feilet", + "saveAndApply": "Lagre nytt kamera", + "saveError": "Ugyldig konfigurasjon. Vennligst sjekk innstillingene dine.", + "issues": { + "title": "Strømvalidering", + "videoCodecGood": "Video-kodek er {{codec}}.", + "audioCodecGood": "Lyd-kodek er {{codec}}.", + "noAudioWarning": "Ingen lyd oppdaget for denne strømmen, opptak vil ikke ha lyd.", + "audioCodecRecordError": "AAC lyd-kodek er påkrevd for å støtte lyd i opptak.", + "audioCodecRequired": "En lydstrøm er påkrevd for å støtte lyddeteksjon.", + "restreamingWarning": "Å redusere tilkoblinger til kameraet for opptaksstrømmen kan øke CPU-bruken noe.", + "dahua": { + "substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Dahua / Amcrest / EmpireTech-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige." + }, + "hikvision": { + "substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Hikvision-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige." + }, + "resolutionHigh": "En oppløsning på {{resolution}} kan føre til økt ressursbruk.", + "resolutionLow": "En oppløsning på {{resolution}} kan være for lav for pålitelig deteksjon av små objekter." + }, + "ffmpegModuleDescription": "Hvis strømmen ikke lastes inn etter flere forsøk, kan du prøve å aktivere dette. Når det er aktivert, vil Frigate bruke ffmpeg-modulen sammen med go2rtc. Dette kan gi bedre kompatibilitet med enkelte kamerastrømmer.", + "ffmpegModule": "Bruk kompatibilitetsmodus for strøm", + "streamsTitle": "Kamerastrømmer", + "addStream": "Legg til strøm", + "addAnotherStream": "Legg til en annen strøm", + "streamUrl": "Strøm-URL", + "streamUrlPlaceholder": "rtsp://brukernavn:passord@vert:port/sti", + "selectStream": "Velg en strøm", + "searchCandidates": "Søk blant kandidater...", + "noStreamFound": "Ingen strøm funnet", + "url": "URL", + "resolution": "Oppløsning", + "selectResolution": "Velg oppløsning", + "quality": "Kvalitet", + "selectQuality": "Velg kvalitet", + "roleLabels": { + "detect": "Objektdeteksjon", + "record": "Opptak", + "audio": "Lyd" + }, + "testStream": "Test tilkobling", + "testSuccess": "Strømmetest vellykket!", + "testFailed": "Strømmetest feilet", + "testFailedTitle": "Test feilet", + "connected": "Tilkoblet", + "notConnected": "Ikke tilkoblet", + "featuresTitle": "Funksjoner", + "go2rtc": "Reduser antall tilkoblinger til kamera", + "detectRoleWarning": "Minst én strøm må ha \"deteksjon\"-rollen for å fortsette.", + "rolesPopover": { + "title": "Strømroller", + "detect": "Hovedstrøm for objektdeteksjon.", + "record": "Lagrer segmenter av videostrømmen basert på konfigurasjonsinnstillinger.", + "audio": "Strøm for lydbasert deteksjon." + }, + "featuresPopover": { + "title": "Strømfunksjoner", + "description": "Bruk go2rtc-restrømming for å redusere antall tilkoblinger til kameraet." + } + }, + "step4": { + "description": "Endelig validering og analyse før du lagrer det nye kameraet. Koble til hver strøm før du lagrer.", + "validationTitle": "Strømvalidering", + "connectAllStreams": "Koble til alle strømmer", + "reconnectionSuccess": "Tilkobling vellykket.", + "reconnectionPartial": "Noen strømmer kunne ikke koble til på nytt.", + "streamUnavailable": "Forhåndsvisning av strøm utilgjengelig", + "reload": "Last på nytt", + "connecting": "Kobler til...", + "streamTitle": "Strøm {{number}}", + "valid": "Gyldig", + "failed": "Feilet", + "notTested": "Ikke testet", + "connectStream": "Koble til", + "connectingStream": "Kobler til", + "disconnectStream": "Koble fra", + "estimatedBandwidth": "Estimert båndbredde", + "roles": "Roller", + "ffmpegModule": "Bruk kompatibilitetsmodus for strøm", + "ffmpegModuleDescription": "Hvis strømmen ikke lastes etter flere forsøk, prøv å aktivere dette. Når aktivert, vil Frigate bruke ffmpeg-modulen med go2rtc. Dette kan gi bedre kompatibilitet med noen kamerastrømmer.", + "none": "Ingen", + "error": "Feil", + "streamValidated": "Strøm {{number}} validert", + "streamValidationFailed": "Validering av strøm {{number}} feilet", + "saveAndApply": "Lagre nytt kamera", + "saveError": "Ugyldig konfigurasjon. Vennligst sjekk innstillingene dine.", + "issues": { + "title": "Strømvalidering", + "videoCodecGood": "Videokodek er {{codec}}.", + "audioCodecGood": "Lydkodek er {{codec}}.", + "resolutionHigh": "En oppløsning på {{resolution}} kan føre til økt ressursbruk.", + "resolutionLow": "En oppløsning på {{resolution}} kan være for lav for pålitelig deteksjon av små objekter.", + "noAudioWarning": "Ingen lyd oppdaget for denne strømmen, opptak vil ikke ha lyd.", + "audioCodecRecordError": "AAC-lydkodeken kreves for å støtte lyd i opptak.", + "audioCodecRequired": "En lydstrøm kreves for å støtte lyddeteksjon.", + "restreamingWarning": "Å redusere tilkoblinger til kameraet for opptaksstrømmen kan øke CPU-bruken noe.", + "brands": { + "reolink-rtsp": "Reolink RTSP anbefales ikke. Aktiver HTTP i kameraets fastvareinnstillinger og start veiviseren på nytt.", + "reolink-http": "Reolink HTTP-strømmer bør bruke FFmpeg for bedre kompatibilitet. Aktiver 'Bruk kompatibilitetsmodus for strøm' for denne strømmen." + }, + "dahua": { + "substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Dahua / Amcrest / EmpireTech-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige." + }, + "hikvision": { + "substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Hikvision-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige." + }, + "resolutionUnknown": "Oppløsningen for denne strømmen kunne ikke fastslås. Du bør angi deteksjonsoppløsningen manuelt i innstillingene eller i konfigurasjonen din." + } + } + }, + "cameraManagement": { + "title": "Administrer kameraer", + "addCamera": "Legg til nytt kamera", + "editCamera": "Rediger kamera:", + "selectCamera": "Velg et kamera", + "backToSettings": "Tilbake til kamerainnstillinger", + "streams": { + "title": "Kamerastatus og detaljer", + "desc": "Midlertidig deaktiver et kamera til Frigate startes på nytt. Deaktivering av et kamera stopper Frigates behandling av dette kameraets strømmer fullstendig. Deteksjon, opptak og feilsøking vil være utilgjengelig.
    Merk: Dette deaktiverer ikke go2rtc-restrømming.", + "disableDesc": "Aktiver et kamera som for øyeblikket ikke er synlig i grensesnittet og deaktivert i konfigurasjonen. En omstart av Frigate kreves etter aktivering.", + "enableSuccess": "Aktiverte {{cameraName}}. Start Frigate på nytt for å ta i bruk.", + "enableLabel": "Aktiverte kameraer", + "enableDesc": "Deaktiver et aktivert kamera midlertidig frem til Frigate starter på nytt. Deaktivering av et kamera stopper all prosessering av kameraets strømmer. Deteksjon, opptak og feilsøking vil være utilgjengelig.
    Merk: Dette deaktiverer ikke videreformidling (restream) i go2rtc.

    Dra i feltet for å endre rekkefølgen på kameraene slik de vises i grensesnittet. Rekkefølgen på de aktiverte kameraene vil gjenspeiles i hele grensesnittet, inkludert Live-dashbordet og rullegardinmenyene for kameravalg.", + "disableLabel": "Deaktiverte kameraer", + "friendlyName": { + "edit": "Rediger visningsnavn for kamera", + "title": "Rediger visningsnavn", + "description": "Angi visningsnavnet som skal brukes for dette kameraet i Frigate-grensesnittet. La feltet stå tomt for å bruke kamera-ID.", + "rename": "Omdøp" + }, + "reorderHandle": "Dra for å endre rekkefølge", + "saving": "Lagrer…", + "saved": "Lagret", + "label": "Kamerastatus", + "description": "Angi driftsstatus for hvert kamera.

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

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

    Dra i ikonet for å endre rekkefølgen på aktive kameraer slik de vises i grensesnittet, inkludert Live-dashbordet og rullegardinmenyene for kameravalg.", + "disabledSubheading": "Deaktivert i konfigurasjonen", + "status": { + "on": "På", + "off": "Av", + "disabled": "Deaktivert" + }, + "disableSuccess": "Deaktiverte {{cameraName}} og lagret i konfigurasjonen.", + "details": { + "edit": "Rediger kameradetaljer", + "title": "Rediger kameradetaljer", + "description": "Oppdater visningsnavn, ekstern URL og synlighet brukt for dette kameraet i Frigate-grensesnittet.", + "friendlyNameLabel": "Visningsnavn", + "friendlyNameHelp": "Vennlig navn som vises for dette kameraet i Frigate-grensesnittet. La stå tomt for å bruke kamera-ID.", + "webuiUrlLabel": "URL til kameraets webgrensesnitt", + "webuiUrlHelp": "URL for å besøke kameraets webgrensesnitt direkte fra feilsøkingsvisningen (Debug). La stå tomt for å deaktivere lenken.", + "webuiUrlInvalid": "Må være en gyldig URL (f.eks. https://example.com).", + "dashboardLabel": "Vis på Live-dashbordet", + "dashboardHelp": "Vis dette kameraet på Live-dashbordet.", + "reviewLabel": "Vis i Inspeksjons-modus", + "reviewHelp": "Vis dette kameraet i Inspeksjon (Review), inkludert kamerafilteret, bevegelsesgjennomgang og historikkvisningen." + } + }, + "cameraConfig": { + "add": "Legg til kamera", + "edit": "Rediger kamera", + "description": "Konfigurer kamerainnstillinger, inkludert strømmeinnganger og roller.", + "name": "Kameranavn", + "nameRequired": "Kameranavn er påkrevd", + "nameLength": "Kameranavnet må være mindre enn 64 tegn.", + "namePlaceholder": "f.eks front_dor eller Hage Oversikt", + "enabled": "Aktivert", + "ffmpeg": { + "inputs": "Inngangsstrømmer", + "path": "Lenke til strøm", + "pathRequired": "Lenke til strøm er påkrevd", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "Minst én rolle er påkrevd", + "rolesUnique": "Hver rolle (lyd, deteksjon, opptak) kan bare tildeles én strøm", + "addInput": "Legg til inngangsstrøm", + "removeInput": "Fjern inngangsstrøm", + "inputsRequired": "Minst én inngangsstrøm er påkrevd" + }, + "go2rtcStreams": "go2rtc-strømmer", + "streamUrls": "Strøm-URL'er", + "addUrl": "Legg til URL", + "addGo2rtcStream": "Legg til go2rtc-strøm", + "toast": { + "success": "Kamera {{cameraName}} ble lagret" + } + }, + "profiles": { + "enabled": "Aktivert", + "inherit": "Arv", + "disabled": "Deaktivert", + "description": "Konfigurer hvilke kameraer som slås på eller av når en profil aktiveres. Kameraer satt til «Arv» beholder sin standardstatus.", + "title": "Profiloverstyringer for kamera", + "selectLabel": "Velg profil", + "on": "På", + "off": "Av" + }, + "deleteCameraDialog": { + "confirmTitle": "Er du sikker?", + "success": "Kamera {{cameraName}} ble slettet", + "error": "Kunne ikke slette kamera {{cameraName}}", + "title": "Slett kamera", + "deleteExports": "Slett også eksporterte filer for dette kameraet", + "confirmButton": "Slett permanent", + "confirmWarning": "Sletting av {{cameraName}} kan ikke angres.", + "description": "Sletting av et kamera vil fjerne alle opptak, sporede objekter og konfigurasjon for dette kameraet permanent. Eventuelle go2rtc-strømmer tilknyttet kameraet må eventuelt fjernes manuelt.", + "selectPlaceholder": "Velg kamera..." + }, + "deleteCamera": "Slett kamera", + "cameraType": { + "title": "Kameratype", + "label": "Kameratype", + "description": "Angi type for hvert kamera. Dedikerte LPR-kameraer er spesialkameraer med kraftig optisk zoom for å fange opp kjennemerker på kjøretøy langt unna. De fleste kameraer bør bruke typen \"Normal\", med mindre kameraet er spesifikt for gjenkjenning av kjennemerker og har et snevert fokus på kjennemerker.", + "normal": "Normal", + "dedicatedLpr": "Dedikert LPR (lesing av kjennemerker)", + "saveSuccess": "Kameratype oppdatert for {{cameraName}}. Start Frigate på nytt for å bruke endringene." + }, + "description": "Legg til, rediger og slett kameraer, kontroller statusen til hvert kamera, og konfigurer overstyringer per profil og kameratype. For å konfigurere strømmer, deteksjon, bevegelse og andre kameraspesifikke innstillinger, velg den aktuelle seksjonen under Kamerakonfigurasjon.", + "clone": { + "sectionTitle": "Klon innstillinger", + "sectionDescription": "Kopier konfigurasjon fra ett kamera til et annet eller til et nytt kamera.", + "button": "Klon innstillinger", + "title": "Klon kamerainnstillinger", + "description": "Kopier et kameras konfigurasjon til ett eller flere andre kameraer eller til et nytt kamera. Identitet (navn, visningsnavn, URL til webgrensesnitt, visningsrekkefølge) kopieres ikke.", + "source": { + "label": "Kildekamera", + "placeholder": "Velg et kildekamera", + "required": "Kildekamera må velges" + }, + "target": { + "legend": "Mål", + "newRadio": "Nytt kamera", + "newNameLabel": "Kameranavn", + "newNamePlaceholder": "f.eks. bakdoer eller Bakdør", + "newNameRequired": "Kameranavn kreves", + "newNameInvalid": "Ugyldig kameranavn", + "newNameCollision": "Et kamera med dette navnet eksisterer allerede", + "newStreamsForced": "Strømmer kopieres alltid for et nytt kamera.", + "existingCamerasRadio": "Eksisterende kameraer", + "allCameras": "Alle kameraer", + "existingPlaceholder": "Velg minst ett kamera", + "existingDisabled": "Ingen andre kameraer å kopiere til" + }, + "categories": { + "legend": "Innstillinger som skal klones", + "description": "Velg hvilke innstillinger som skal kopieres fra kildekameraet.", + "selectAll": "Velg alle", + "selectNone": "Velg ingen", + "resetDefaults": "Tilbakestill til standardverdier", + "general": "Generelt", + "spatial": "Romlige innstillinger", + "streams": "Strømmer", + "spatialWarningTitle": "Uoverensstemmelse i oppløsning", + "spatialWarning": "Kildekameraet {{srcCamera}} sin deteksjonsoppløsning ({{srcWidth}}×{{srcHeight}}) er forskjellig fra: {{cameras}}. Polygoner vil kanskje ikke samsvare på disse kameraene. Disse er deaktivert som standard; aktiver for å kopiere som de er.", + "restartHint": "Omstart kreves", + "items": { + "record": "Opptak pågår", + "snapshots": "Øyeblikksbilder", + "review": "Inspeksjon", + "motion": "Bevegelsesdeteksjon", + "objects": "Objekter", + "audio": "Lyddeteksjon", + "audio_transcription": "Lydtranskripsjon", + "notifications": "Varsler", + "birdseye": "Fugleperspektiv", + "mqtt": "MQTT", + "timestamp_style": "Tidsstempelstil", + "onvif": "ONVIF", + "lpr": "Kjennemerke gjenkjenning", + "face_recognition": "Ansiktsgjenkjenning", + "semantic_search": "Semantisk søk", + "genai": "Generativ AI", + "type": "Kameratype (normal / dedikert LPR)", + "profiles": "Profiler", + "detect": "Deteksjonsdimensjoner", + "zones": "Soner", + "motion_mask": "Bevegelsesmasker", + "object_masks": "Objektmasker", + "ffmpeg_live": "Strømme-URL-er og roller" + } + }, + "footer": { + "restartNeeded": "Omstart vil kreves for enkelte endringer.", + "liveOnly": "Alle endringer vil trå i kraft direkte uten omstart.", + "submit": "Klon", + "submitting": "Kloner…", + "changeCount_one": "{{count}} endring vil bli brukt", + "changeCount_other": "{{count}} endringer vil bli brukt" + }, + "toast": { + "success": "Innstillinger kopiert til {{cameraName}}", + "successWithRestart": "Innstillinger kopiert til {{cameraName}}. Start Frigate på nytt for å ta i bruk alle endringer.", + "partialFailure": "{{successCount}} seksjoner ble tatt i bruk; «{{failedSection}}» feilet: {{errorMessage}}", + "partialFailureMulti": "Kopiert til {{successCount}} kamera(er); feilet for {{failed}}: {{errorMessage}}", + "newCameraPartialFailure": "Kameraet {{cameraName}} ble opprettet, men noen innstillinger kunne ikke kopieres: {{errorMessage}}", + "sourceMissing": "Kildekameraet eksisterer ikke lenger", + "submitError": "Kunne ikke klone kamera: {{errorMessage}}", + "successMulti_one": "Innstillinger kopiert til {{count}} kamera", + "successMulti_other": "Innstillinger kopiert til {{count}} kameraer", + "successMultiWithRestart_one": "Innstillinger kopiert til {{count}} kamera. Start Frigate på nytt for å aktivere alle endringene.", + "successMultiWithRestart_other": "Innstillinger kopiert til {{count}} kameraer. Start Frigate på nytt for å bruke alle endringene." + } + } + }, + "cameraReview": { + "title": "Innstillinger for kamerainspeksjon", + "object_descriptions": { + "title": "Generative KI-objektbeskrivelser", + "desc": "Midlertidig aktiver/deaktiver generative KI-objektbeskrivelser for dette kameraet til Frigate starter på nytt. Når deaktivert, vil KI-genererte beskrivelser ikke bli forespurt for sporede objekter på dette kameraet." + }, + "review_descriptions": { + "title": "Generative KI beskrivelser for inspeksjon", + "desc": "Midlertidig aktiver/deaktiver inspeksjonsbeskrivelser med generativ KI for dette kameraet til Frigate startes på nytt. Når deaktivert, vil det ikke bli bedt om KI-genererte beskrivelser for inspeksjonselementer på dette kameraet." + }, + "review": { + "title": "Inspiser", + "desc": "Aktiver/deaktiver varsler og deteksjoner midlertidig for dette kameraet til Frigate startes på nytt. Når deaktivert, vil det ikke genereres nye inspeksjonselementer. ", + "alerts": "Varsler ", + "detections": "Deteksjoner " + }, + "reviewClassification": { + "title": "Inspeksjonsklassifisering", + "desc": "Frigate kategoriserer inspeksjonselementer som Varsler og Deteksjoner. Som standard regnes alle person- og bil-objekter som Varsler. Du kan finjustere klassifiseringen ved å konfigurere nødvendige soner.", + "noDefinedZones": "Ingen soner er definert for dette kameraet.", + "objectAlertsTips": "Alle {{alertsLabels}}-objekter på {{cameraName}} vil bli vist som varsler.", + "zoneObjectAlertsTips": "Alle {{alertsLabels}}-objekter oppdaget i {{zone}} på {{cameraName}} vil bli vist som varsler.", + "objectDetectionsTips": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert for kamera {{cameraName}}, vil bli vist som deteksjoner uavhengig av hvilken sone de er i.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert i sone {{zone}} for kamera {{cameraName}}, vil bli vist som deteksjoner.", + "notSelectDetections": "Alle {{detectionsLabels}}-objekter oppdaget i sone {{zone}} for kamera {{cameraName}} som ikke er kategorisert som varsler, vil bli vist som deteksjoner uavhengig av hvilken sone de er i.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert for kamera {{cameraName}}, vil bli vist som deteksjoner uavhengig av hvilken sone de er i." + }, + "unsavedChanges": "Ulagrede innstillinger for inspeksjonsklassifisering for {{camera}}", + "selectAlertsZones": "Velg soner for varsler", + "selectDetectionsZones": "Velg soner for deteksjoner", + "limitDetections": "Avgrens deteksjoner til bestemte soner", + "toast": { + "success": "Konfigurasjonen for inspeksjonsklassifisering er lagret. Start Frigate på nytt for å aktivere endringer." + } + } + }, + "configForm": { + "reviewLabels": { + "summary": "{{count}} etiketter valgt", + "empty": "Ingen etiketter tilgjengelig" + }, + "audioLabels": { + "summary": "{{count}} lydetiketter valgt", + "empty": "Ingen lydetiketter tilgjengelig" + }, + "objectLabels": { + "summary": "{{count}} objekttyper valgt", + "empty": "Ingen objektetiketter tilgjengelig" + }, + "inputRoles": { + "summary": "{{count}} roller valgt", + "options": { + "detect": "Deteksjon", + "audio": "Lyd", + "record": "Opptak" + }, + "empty": "Ingen roller tilgjengelig" + }, + "zoneNames": { + "summary": "{{count}} valgt", + "empty": "Ingen soner tilgjengelig" + }, + "filters": { + "objectFieldLabel": "{{field}} for {{label}}" + }, + "sections": { + "face_recognition": "Ansiktsgjenkjenning", + "auth": "Autentisering", + "motion": "Bevegelse", + "database": "Database", + "detect": "Deteksjon", + "detectors": "Detektorer", + "live": "Direktevisning", + "ffmpeg": "FFmpeg", + "birdseye": "Fugleperspektiv", + "genai": "GenAI", + "go2rtc": "go2rtc", + "review": "Inspeksjon", + "lpr": "Kjennemerke-gjenkjenning", + "audio": "Lyd", + "masksAndZones": "Masker / Soner", + "notifications": "Meldingsvarsler", + "model": "Modell", + "mqtt": "MQTT", + "objects": "Objekter", + "record": "Opptak", + "proxy": "Proxy", + "semantic_search": "Semantisk søk", + "snapshots": "Stillbilder", + "telemetry": "Telemetri", + "timestamp_style": "Tidsstempler", + "tls": "TLS" + }, + "ffmpegArgs": { + "useGlobalSetting": "Arv fra global innstilling", + "inherit": "Arv fra kamerainnstilling", + "preset": "Forhåndsinnstilling", + "presetLabels": { + "preset-http-reolink": "HTTP - Reolink-kameraer", + "preset-http-jpeg-generic": "HTTP JPEG (Generisk)", + "preset-http-mjpeg-generic": "HTTP MJPEG (Generisk)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-record-jpeg": "Opptak - JPEG-kameraer", + "preset-record-mjpeg": "Opptak - MJPEG-kameraer", + "preset-record-ubiquiti": "Opptak - Ubiquiti-kameraer", + "preset-record-generic-audio-copy": "Opptak (Generisk + kopier lyd)", + "preset-record-generic-audio-aac": "Opptak (Generisk + lyd til AAC)", + "preset-record-generic": "Opptak (Generisk, uten lyd)", + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-rtmp-generic": "RTMP (Generisk)", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-restream": "RTSP - Videreformidling fra go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - Videreformidling fra go2rtc (lav forsinkelse)", + "preset-rtsp-generic": "RTSP (Generisk)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)" + }, + "none": "Ingen", + "manual": "Manuelle argumenter", + "manualPlaceholder": "Skriv inn FFmpeg-argumenter", + "selectPreset": "Velg forhåndsinnstilling" + }, + "advancedCount": "Avansert ({{count}})", + "advancedSettingsCount": "Avanserte innstillinger ({{count}})", + "timezone": { + "defaultOption": "Bruk nettleserens tidssone" + }, + "tabs": { + "sharedDefaults": "Delte standardverdier", + "integrations": "Integrasjoner", + "system": "System" + }, + "detectors": { + "keyDuplicate": "Detektornavn eksisterer allerede.", + "keyRequired": "Detektornavn er påkrevd.", + "none": "Ingen detektor-instanser er konfigurert.", + "noSchema": "Ingen detektorskjemaer er tilgjengelige.", + "title": "Innstillinger for detektorer", + "singleType": "Kun én {{type}}-detektor er tillatt.", + "add": "Legg til detektor", + "addCustomKey": "Legg til egendefinert nøkkel" + }, + "global": { + "description": "Disse innstillingene gjelder for alle kameraer med mindre de overstyres i kameraspesifikke innstillinger.", + "title": "Globale innstillinger" + }, + "camera": { + "description": "Disse innstillingene gjelder kun for dette kameraet og overstyrer de globale innstillingene.", + "noCameras": "Ingen kameraer tilgjengelig", + "title": "Kamerainnstillinger" + }, + "genaiRoles": { + "options": { + "embeddings": "Vektorrepresentasjoner", + "tools": "Verktøy", + "vision": "Bildegjenkjenning", + "descriptions": "Beskrivelser", + "chat": "Chat" + } + }, + "additionalProperties": { + "remove": "Fjern", + "keyPlaceholder": "Ny nøkkel", + "keyLabel": "Nøkkel", + "valueLabel": "Verdi" + }, + "roleMap": { + "remove": "Fjern", + "groupsLabel": "Grupper", + "empty": "Ingen rolletilordninger", + "addMapping": "Legg til rolletilordning", + "roleLabel": "Rolle" + }, + "semanticSearchModel": { + "genaiProviders": "GenAI-leverandører", + "builtIn": "Innebygde modeller", + "placeholder": "Velg modell…" + }, + "motion": { + "title": "Innstillinger for bevegelse" + }, + "detect": { + "title": "Innstillinger for deteksjon" + }, + "live": { + "title": "Innstillinger for direktevisning" + }, + "review": { + "title": "Innstillinger for inspeksjon" + }, + "audio": { + "title": "Innstillinger for lyd" + }, + "objects": { + "title": "Innstillinger for objekter" + }, + "record": { + "title": "Innstillinger for opptak" + }, + "snapshots": { + "title": "Innstillinger for stillbilder" + }, + "timestamp_style": { + "title": "Innstillinger for tidsstempling" + }, + "notifications": { + "title": "Innstillinger for varsler" + }, + "restartRequiredFooter": "Konfigurasjon endret - Omstart påkrevd", + "addCustomLabel": "Legg til egendefinert etikett...", + "restartRequiredField": "Omstart påkrevd", + "cameraInputs": { + "itemTitle": "Strøm {{index}}", + "sourceMode": { + "restream": "Videreformidling / Restream (go2rtc)", + "manual": "Manuell kildesti", + "go2rtcStreamLabel": "go2rtc-strøm", + "go2rtcStreamPlaceholder": "Velg en go2rtc-strøm", + "noGo2rtcStreams": "Ingen go2rtc-strømmer er konfigurert", + "go2rtcStreamSearch": "Søk i strømmer...", + "availableStreams": "Tilgjengelige strømmer", + "noMatchingStreams": "Ingen matchende strømmer" + } + }, + "searchPlaceholder": "Søk...", + "showAdvanced": "Vis avanserte innstillinger", + "genaiModel": { + "placeholder": "Velg eller skriv inn en modell…", + "search": "Søk eller skriv inn en modell…", + "noModels": "Ingen modeller tilgjengelig", + "available": "Tilgjengelige modeller", + "useCustom": "Bruk «{{value}}»", + "refresh": "Oppdater modeller", + "probeFailed": "Kunne ikke hente modeller", + "fetchedModels": "Modelliste ble hentet" + }, + "knownPlates": { + "platePlaceholder": "Kjennemerke nummer eller regex", + "namePlaceholder": "f.eks konas bil" + }, + "liveStreams": { + "streamNameLabel": "Strømnavn", + "streamNamePlaceholder": "f.eks. Hovedstrøm HD", + "go2rtcStreamLabel": "go2rtc-strøm", + "go2rtcStreamPlaceholder": "Velg en go2rtc-strøm", + "go2rtcStreamSearch": "Søk eller skriv inn et strømnavn…", + "noGo2rtcStreams": "Ingen go2rtc-strømmer er konfigurert", + "availableStreams": "Tilgjengelige strømmer", + "useCustom": "Bruk «{{value}}»", + "addStream": "Legg til strøm" + }, + "semanticSearchModelSize": { + "notApplicable": "Ikke relevant for leverandører av generativ AI" + }, + "ptzPresets": { + "placeholder": "Velg eller skriv inn en forhåndsinnstilling...", + "search": "Søk eller skriv inn en forhåndsinnstilling...", + "noPresets": "Ingen forhåndsinnstillinger tilgjengelig", + "available": "Forhåndsinnstillinger for kamera", + "useCustom": "Bruk «{{value}}»" + }, + "defaultRole": { + "admin": "Administrator", + "viewer": "Seer (Viewer)" + } + }, + "button": { + "overriddenBaseConfigTooltip": "{{profile}}-profilen overstyrer konfigurasjonsinnstillinger i denne seksjonen", + "overriddenGlobalTooltip": "Dette kameraet overstyrer globale konfigurasjonsinnstillinger i denne seksjonen", + "overriddenBaseConfig": "Overstyrt (Basiskonfigurasjon)", + "overriddenGlobal": "Overstyrt (Global)", + "overriddenInCameras": { + "label_one": "Overstyrt i {{count}} kamera", + "label_other": "Overstyrt i {{count}} kameraer", + "tooltip_one": "{{count}} kamera overstyrer verdier i denne seksjonen. Klikk for å se detaljer.", + "tooltip_other": "{{count}} kameraer overstyrer verdier i denne seksjonen. Klikk for å se detaljer.", + "heading_one": "Denne globale seksjonen har felt som er overstyrt i {{count}} kamera.", + "heading_other": "Denne globale seksjonen har felt som er overstyrt i {{count}} kameraer.", + "othersField_one": "{{count}} annen", + "othersField_other": "{{count}} andre", + "profilePrefix": "{{profile}}-profil: {{fields}}" + }, + "overriddenGlobalHeading_one": "Dette kameraet overstyrer {{count}} felt fra den globale konfigurasjonen:", + "overriddenGlobalHeading_other": "Dette kameraet overstyrer {{count}} felt fra den globale konfigurasjonen:", + "overriddenGlobalNoDeltas": "Dette kameraet overstyrer den globale konfigurasjonen, men ingen feltverdier er forskjellige.", + "overriddenBaseConfigHeading_one": "{{profile}}-profilen overstyrer {{count}} felt fra basiskonfigurasjonen:", + "overriddenBaseConfigHeading_other": "{{profile}}-profilen overstyrer {{count}} felt fra basiskonfigurasjonen:", + "overriddenBaseConfigNoDeltas": "{{profile}}-profilen overstyrer denne seksjonen, men ingen feltverdier er forskjellige fra basiskonfigurasjonen." + }, + "detectionModel": { + "plusActive": { + "title": "Administrasjon av Frigate+-modell", + "description": "Denne instansen kjører en Frigate+-modell. Velg eller endre modell i Frigate+-innstillingene.", + "goToFrigatePlus": "Gå til Frigate+-innstillinger", + "label": "Kilde for gjeldende modell", + "showModelForm": "Konfigurer en modell manuelt" + } + }, + "go2rtcStreams": { + "description": "Administrer go2rtc-strømkonfigurasjoner for videreformidling av kamera-strømmer. Hver strøm har ett navn og én eller flere kilde-URL-er.", + "ffmpeg": { + "hardwareAuto": "Automatisk (anbefalt)", + "useFfmpegModule": "Bruk kompatibilitetsmodus (ffmpeg)", + "audioExclude": "Ekskluder", + "videoExclude": "Ekskluder", + "hardwareNone": "Ingen maskinvareakselerasjon", + "audioCopy": "Kopier", + "videoCopy": "Kopier", + "audio": "Lyd", + "hardware": "Maskinvareakselerasjon", + "videoH264": "Transkod til H.264", + "videoH265": "Transkod til H.265", + "audioMp3": "Transkod til MP3", + "audioOpus": "Transkod til Opus", + "audioPcm": "Transkod til PCM", + "audioPcma": "Transkod til PCM A-law", + "audioPcmu": "Transkod til PCM μ-law", + "audioAac": "Transkod til AAC", + "video": "Video", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "Legg til videokodek", + "addAudioCodec": "Legg til lydkodek", + "removeCodec": "Fjern kodek" + }, + "validation": { + "nameDuplicate": "En strøm med dette navnet eksisterer allerede", + "urlRequired": "Minst én URL er påkrevd", + "nameRequired": "Strømnavn er påkrevd", + "nameInvalid": "Strømnavn kan kun inneholde bokstaver, tall, understrek og bindestrek" + }, + "deleteStreamConfirm": "Er du sikker på at du vil slette strømmen \"{{streamName}}\"? Kameraer som refererer til denne strømmen kan slutte å fungere.", + "streamUrlPlaceholder": "f.eks. rtsp://bruker:passord@192.168.1.100/stream", + "streamNamePlaceholder": "f.eks. ytterdor", + "renameStream": "Gi strøm nytt navn", + "title": "go2rtc-strømmer", + "noStreams": "Ingen go2rtc-strømmer er konfigurert. Legg til en strøm for å komme i gang.", + "addStream": "Legg til strøm", + "addUrl": "Legg til URL", + "newStreamName": "Nytt strømnavn", + "addStreamDesc": "Skriv inn et navn for den nye strømmen. Dette navnet vil bli brukt til å referere til strømmen i kamerakonfigurasjonen din.", + "renameStreamDesc": "Skriv inn et nytt navn for denne strømmen. Endring av navn kan ødelegge for kameraer eller andre strømmer som refererer til den ved navn.", + "deleteStream": "Slett strøm", + "streamName": "Strømnavn", + "streamNumber": "Strøm {{index}}", + "sourceNumber": "Kilde {{index}}" + }, + "profiles": { + "active": "Aktiv", + "activeProfile": "Aktiv profil", + "enableSwitch": "Aktiver profiler", + "baseConfig": "Basiskonfigurasjon", + "error": { + "alreadyExists": "En profil med denne ID-en eksisterer allerede", + "mustNotContainPeriod": "Kan ikke inneholde punktum", + "mustBeAtLeastTwoCharacters": "Må være minst 2 tegn" + }, + "nameDuplicate": "En profil med dette navnet eksisterer allerede", + "profileNamePlaceholder": "f.eks. Sikret, Borte, Nattmodus", + "deleteSectionConfirm": "Fjern {{section}}-overstyringene for profil {{profile}} på {{camera}}?", + "removeOverride": "Fjern profiloverstyring", + "deleteSectionSuccess": "Fjernet {{section}}-overstyringer for {{profile}}", + "renameProfile": "Gi profil nytt navn", + "noActiveProfile": "Ingen aktiv profil", + "noOverrides": "Ingen overstyringer", + "noProfiles": "Ingen profiler er definert.", + "profileIdDescription": "Intern identifikator brukt i konfigurasjon og automatiseringer", + "columnCamera": "Kamera", + "nameInvalid": "Kun små bokstaver, tall og understrek er tillatt", + "activateFailed": "Kunne ikke aktivere profil", + "addProfile": "Legg til profil", + "newProfile": "Ny profil", + "activated": "Profil '{{profile}}' aktivert", + "deactivated": "Profil deaktivert", + "createSuccess": "Profilen '{{profile}}' ble opprettet", + "deleteSuccess": "Profilen '{{profile}}' ble slettet", + "renameSuccess": "Profilen har fått nytt navn: '{{profile}}'", + "title": "Profiler", + "enabledDescription": "Profiler er aktivert. Opprett en ny profil nedenfor, naviger til en seksjon for kamerakonfigurasjon for å gjøre endringer, og lagre for at endringene skal tre i kraft.", + "disabledDescription": "Profiler lar deg definere navngitte sett med overstyringer for kamerakonfigurasjon (f.eks. sikret, borte, natt) som kan aktiveres ved behov.", + "profileIdLabel": "Profil-ID", + "friendlyNameLabel": "Profilnavn", + "columnOverrides": "Profiloverstyringer", + "deleteProfile": "Slett profil", + "deleteProfileConfirm": "Slett profilen \"{{profile}}\" fra alle kameraer? Dette kan ikke angres.", + "deleteSection": "Slett seksjonsoverstyringer", + "cameraCount_one": "{{count}} kamera", + "cameraCount_other": "{{count}} kameraer" + }, + "configMessages": { + "review": { + "allNonAlertDetections": "All aktivitet som ikke er et varsel, vil bli inkludert som deteksjoner.", + "detectDisabled": "Objektdeteksjon er deaktivert. Inspeksjonselementer krever detekterte objekter for å kategorisere varsler og deteksjoner.", + "recordDisabled": "Opptak er deaktivert, inspeksjonselementer vil ikke bli generert.", + "genaiImageSourceRecordingsRecordDisabled": "Bildekilde er satt til \"opptak\", men opptak er deaktivert. Frigate vil falle tilbake til forhåndsvisningsbilder." + }, + "detectors": { + "mixedTypesSuggestion": "Alle detektorer må bruke samme type. Fjern eksisterende detektorer eller velg {{type}}.", + "mixedTypes": "Alle detektorer må bruke samme type. Fjern eksisterende detektorer for å bruke en annen type." + }, + "faceRecognition": { + "globalDisabled": "Utvidelse for ansiktsgjenkjenning må være aktivert for at funksjoner for ansiktsgjenkjenning skal fungere på dette kameraet.", + "personNotTracked": "Ansiktsgjenkjenning krever at objektet 'person' spores. Aktiver 'person' under Objekter for dette kameraet.", + "modelSizeLarge": "Den store (large) modellen krever GPU eller NPU for akseptabel ytelse. Bruk liten (small) på systemer med kun CPU." + }, + "detect": { + "fpsGreaterThanFive": "Det anbefales ikke å sette FPS for deteksjon høyere enn 5. Høyere verdier kan føre til ytelsesproblemer uten å gi noen fordeler.", + "disabled": "Objektdeteksjon er deaktivert. Stillbilder, inspeksjonselementer og utvidelser som ansiktsgjenkjenning, lesing av kjennemerker og generativ AI vil ikke fungere.", + "resolutionShouldBeMultipleOfFour": "For best resultat bør deteksjonsbredde og -høyde være delelig med 4. Andre partall kan forårsake visuelle feil eller svak forvrengning i deteksjonsstrømmen.", + "aspectRatioMismatch": "Bredden og høyden du har oppgitt samsvarer ikke med sideforholdet til din nåværende deteksjonsoppløsning. Dette kan føre til et strukket eller forvrengt bilde.", + "maxFramesSet": "Å angi maksimalt antall bilder (max frames) overstyrer standard oppførsel og deaktiverer sporing av stasjonære objekter. Det er svært få situasjoner hvor dette er nødvendig, så bruk det med forsiktighet.", + "squareResolution": "En kvadratisk deteksjonsoppløsning er uvanlig. Deteksjonsbredden og -høyden bør samsvare med kameraets sideforhold (for eksempel 16:9), ikke dimensjonene til objektdeteksjonsmodellen. Et feilaktig sideforhold kan strekke bildet og redusere treffsikkerheten for deteksjon.", + "resolutionHigh": "Denne deteksjonsoppløsningen er høyere enn anbefalt og kan føre til økt ressursbruk uten å forbedre deteksjonsnøyaktigheten. En deteksjonsoppløsning på eller under 1080p anbefales for de fleste kameraer.", + "globalResolutionMultipleCameras": "En global deteksjonsoppløsning er satt samtidig som flere kameraer er konfigurert. Med mindre alle kameraer deler nøyaktig samme oppløsning og sideforhold, bør deteksjonsbredden og -høyden defineres per kamera for å matche hvert kameras opprinnelige sideforhold." + }, + "birdseye": { + "objectsModeDetectDisabled": "Fugleperspektiv er satt til 'objekter'-modus, men objektdeteksjon er deaktivert for dette kameraet. Kameraet vil ikke vises i Fugleperspektiv." + }, + "audio": { + "noAudioRole": "Ingen strømmer har definert lydrolle. Du må aktivere lydrollen for at lyddeteksjon skal fungere." + }, + "record": { + "noRecordRole": "Ingen strømmer har definert opptaksrolle. Opptak vil ikke fungere." + }, + "audioTranscription": { + "audioDetectionDisabled": "Lyddeteksjon er ikke aktivert for dette kameraet. Lydtranskripsjon krever at lyddeteksjon er aktiv." + }, + "snapshots": { + "detectDisabled": "Objektdeteksjon er deaktivert. Stillbilder genereres fra sporede objekter og vil ikke bli opprettet." + }, + "lpr": { + "globalDisabled": "Utvidelse for identifisering av kjennemerker må være aktivert for at LPR-funksjoner skal fungere på dette kameraet.", + "vehicleNotTracked": "Identifisering av kjennemerker krever at 'bil' eller 'motorsykkel' spores. Aktiver 'bil' eller 'motorsykkel' under Objekter for dette kameraet.", + "modelSizeLarge": "Den store (large) modellen er optimalisert for kjennemerker over flere linjer. Den lille (small) modellen gir bedre ytelse og bør brukes med mindre din region bruker skiltformater med flere linjer." + }, + "objects": { + "genaiNoDescriptionsProvider": "Du må konfigurere en GenAI-leverandør med rollen \"beskrivelser\" for at beskrivelser skal kunne genereres." + }, + "semanticSearch": { + "jinav2SmallModelSize": "Størrelsen \"liten\" med Jina V2-modellen har høyt minnebruk og beregningskostnad. Den \"store\" modellen med en dedikert GPU anbefales." + }, + "model": { + "optimizedFor320": "Frigate er optimalisert for en 320x320-modell, som er det beste valget for de fleste oppsett. En 640x640-modell er tregere og hjelper kun i spesifikke scenarier.", + "inputDimensionsNotDetectResolution": "Modellens inndata for bredde og høyde er dimensjonene til selve objektdeteksjonsmodellen, ikke kameraets deteksjonsoppløsning. De skal samsvare med dimensjonene til modellen du bruker – vanligvis en kvadratisk størrelse som 320x320 eller 640x640." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "Manuelle argumenter for maskinvareakselerasjon anbefales ikke. Med mindre det foreligger et spesifikt behov, bør du velge forhåndsinnstillingen som samsvarer med maskinvaren din." + }, + "onvif": { + "autotrackingNoZones": "Autosporing krever minst én sone. Definer en sone for dette kameraet under Masker / Soner, og angi den deretter som en påkrevd sone nedenfor." + } + }, + "maintenance": { + "sync": { + "allMedia": "Alle medier", + "resultsFields": { + "aborted": "Avbrutt. Sletting ville overskredet sikkerhetsterskelen.", + "error": "Feil", + "filesChecked": "Filer kontrollert", + "orphansFound": "Foreldreløse filer funnet", + "orphansDeleted": "Foreldreløse filer slettet", + "totals": "Totalt" + }, + "verbose": "Detaljert (Verbose)", + "exports": "Eksporterte filer", + "alreadyRunning": "En synkroniseringsjobb kjører allerede", + "errorLabel": "Feil", + "dryRunDisabled": "Filer vil bli slettet", + "previews": "Forhåndsvisninger", + "desc": "Frigate vil periodisk rydde opp i mediefiler etter en fast plan i samsvar med din konfigurasjon for bevaring. Det er normalt at noen foreldreløse filer oppstår mens Frigate kjører. Bruk denne funksjonen til å fjerne foreldreløse mediefiler fra disk som ikke lenger er referert til i databasen.", + "status": { + "completed": "Fullført", + "queued": "I kø", + "running": "Kjører", + "notRunning": "Kjører ikke", + "failed": "Mislyktes" + }, + "forceDesc": "Gå utenom sikkerhetsterskelen og fullfør synkronisering selv om mer enn 50 % av filene vil bli slettet.", + "dryRunEnabled": "Ingen filer vil bli slettet", + "jobId": "Jobb-ID", + "error": "Kunne ikke starte synkronisering", + "title": "Mediesynkronisering", + "started": "Mediesynkronisering har startet.", + "mediaTypes": "Medietyper", + "event_thumbnails": "Miniatyrbilder av sporede objekter", + "review_thumbnails": "Miniatyrbilder for inspeksjon", + "recordings": "Opptak", + "results": "Resultater", + "verboseDesc": "Skriv en fullstendig liste over foreldreløse filer til disk for inspeksjon.", + "endTime": "Sluttidspunkt", + "event_snapshots": "Stillbilder av sporede objekter", + "start": "Start synkronisering", + "startTime": "Starttidspunkt", + "currentStatus": "Status", + "statusLabel": "Status", + "running": "Synkronisering kjører...", + "inProgress": "Synkronisering pågår. Denne siden er deaktivert.", + "dryRun": "Testkjøring (Dry Run)", + "force": "Tving" + }, + "regionGrid": { + "clearConfirmDesc": "Det anbefales ikke å tømme regionrutenettet med mindre du nylig har endret størrelsen på detektormodellen eller har endret kameraets fysiske posisjon og har problemer med objektsporing. Rutenettet vil automatisk bli bygget opp igjen over tid etter hvert som objekter spores. En omstart av Frigate kreves for at endringene skal tre i kraft.", + "clearError": "Kunne ikke tømme regionrutenettet", + "restartRequired": "Omstart kreves for at endringer i regionrutenettet skal tre i kraft", + "title": "Regionrutenett", + "clearSuccess": "Regionrutenettet ble tømt", + "desc": "Regionrutenettet er en optimalisering som lærer hvor objekter av ulike størrelser vanligvis dukker opp i hvert kameras synsfelt. Frigate bruker disse dataene til å dimensjonere deteksjonsregioner effektivt. Rutenettet bygges automatisk over tid basert på data fra sporede objekter.", + "clear": "Tøm regionrutenett", + "clearConfirmTitle": "Tøm regionrutenett" + }, + "title": "Vedlikehold" + }, + "onvif": { + "profileAuto": "Auto", + "profileLoading": "Laster profiler...", + "autotracking": { + "zooming": { + "disabled": "Deaktivert", + "absolute": "Absolutt", + "relative": "Relativ" + } + } + }, + "confirmReset": "Bekreft nullstilling", + "resetToDefaultDescription": "Dette vil nullstille alle innstillinger i denne seksjonen til standardverdiene. Denne handlingen kan ikke angres.", + "resetToGlobalDescription": "Dette vil nullstille innstillingene i denne seksjonen til de globale standardverdiene. Denne handlingen kan ikke angres.", + "unsavedChanges": "Du har ulagrede endringer", + "saveAllPreview": { + "title": "Endringer som skal lagres", + "field": { + "label": "Felt" + }, + "scope": { + "global": "Global", + "camera": "Kamera: {{cameraName}}", + "label": "Omfang" + }, + "empty": "Ingen ventende endringer.", + "value": { + "reset": "Nullstill", + "label": "Ny verdi" + }, + "profile": { + "label": "Profil" + }, + "triggerLabel": "Se over ventende endringer" + }, + "globalConfig": { + "title": "Global konfigurasjon", + "toast": { + "success": "Globale innstillinger ble lagret", + "error": "Kunne ikke lagre globale innstillinger", + "validationError": "Validering feilet" + }, + "description": "Konfigurer globale innstillinger som gjelder for alle kameraer med mindre de overstyres." + }, + "toast": { + "success": "Innstillinger lagret", + "successRestartRequired": "Innstillinger lagret. Start Frigate på nytt for å aktivere endringene.", + "applied": "Innstillinger tatt i bruk", + "saveAllFailure": "Kunne ikke lagre alle seksjoner.", + "error": "Kunne ikke lagre innstillinger", + "resetError": "Kunne ikke nullstille innstillinger", + "resetSuccess": "Nullstilt til globale standardverdier", + "validationError": "Validering feilet: {{message}}", + "saveAllPartial_one": "{{successCount}} av {{totalCount}} seksjoner lagret. {{failCount}} feilet.", + "saveAllPartial_other": "{{successCount}} av {{totalCount}} seksjoner lagret. {{failCount}} feilet.", + "saveAllSuccess_one": "{{count}} seksjon ble lagret.", + "saveAllSuccess_other": "{{count}} seksjoner ble lagret.", + "saveAllSuccessRestartRequired_one": "Lagret {{count}} seksjon. Start Frigate på nytt for å aktivere endringene.", + "saveAllSuccessRestartRequired_other": "Alle {{count}} seksjoner ble lagret. Start Frigate på nytt for å aktivere endringene." + }, + "cameraConfig": { + "toast": { + "success": "Kamerainnstillinger ble lagret", + "error": "Kunne ikke lagre kamerainnstillinger" + }, + "title": "Kamerakonfigurasjon", + "description": "Konfigurer innstillinger for enkeltkameraer. Innstillingene overstyrer globale standardverdier.", + "resetToGlobal": "Nullstill til globale verdier", + "overriddenBadge": "Overstyrt" + }, + "timestampPosition": { + "br": "Nederst til høyre", + "bl": "Nederst til venstre", + "tr": "Øverst til høyre", + "tl": "Øverst til venstre" + }, + "birdseye": { + "trackingMode": { + "objects": "Objekter", + "motion": "Bevegelse", + "continuous": "Kontinuerlig" + }, + "cameraOrder": { + "label": "Kamerarekkefølge", + "description": "Dra kameraene for å angi rekkefølgen deres i Fugleperspektiv-oppsettet.", + "reorderHandle": "Dra for å endre rekkefølge", + "saving": "Lagrer…", + "saved": "Lagret" + } + }, + "snapshot": { + "retainMode": { + "all": "Alle", + "motion": "Bevegelse", + "active_objects": "Aktive objekter" + } + }, + "ui": { + "timeFormat": { + "browser": "Nettleser", + "12hour": "12 timer", + "24hour": "24 timer" + }, + "TimeOrDateStyle": { + "full": "Full", + "long": "Lang", + "medium": "Middels", + "short": "Kort" + }, + "unitSystem": { + "metric": "Metrisk", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Opptak", + "previews": "Forhåndsvisninger" + } + }, + "logger": { + "logLevel": { + "debug": "Debug", + "info": "Info", + "warning": "Advarsel", + "error": "Feil", + "critical": "Kritisk" + } + }, + "modelSize": { + "small": "Liten", + "large": "Stor" + }, + "retainMode": { + "all": "Alle", + "motion": "Bevegelse", + "active_objects": "Aktive objekter" + }, + "previewQuality": { + "very_high": "Svært høy", + "high": "Høy", + "medium": "Middels", + "low": "Lav", + "very_low": "Svært lav" + }, + "menuDot": { + "overrideGlobal": "Denne seksjonen overstyrer den globale konfigurasjonen", + "overrideProfile": "Denne seksjonen overstyres av {{profile}}-profilen", + "unsaved": "Denne seksjonen har ulagrede endringer" + }, + "detectorsAndModel": { + "title": "Detektorer og modell", + "description": "Konfigurer detektor-bakenden som kjører objektdeteksjon, og modellen den bruker. Endringer lagres sammen slik at detektoren og modellen forblir synkronisert.", + "cardTitles": { + "detector": "Detektor-maskinvare", + "model": "Deteksjonsmodell" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Egendefinert modell" + }, + "mismatch": { + "warning": "Den gjeldende Frigate+-modellen \"{{model}}\" krever {{required}}-detektoren. Velg en kompatibel modell nedenfor eller bytt til Egendefinert modell før du lagrer." + }, + "plusModel": { + "requiresDetector": "Krever: {{detector}}", + "noModelSelected": "Velg en Frigate+ modell" + }, + "toast": { + "saveSuccess": "Innstillinger for detektorer og modell er lagret. Start Frigate på nytt for å aktivere endringene.", + "saveError": "Kunne ikke lagre innstillinger for detektorer og modell" + }, + "unsavedChanges": "Ulagrede endringer for detektorer og modell", + "restartRequired": "Omstart kreves (detektor eller modell er endret)" + } +} diff --git a/web/public/locales/nb-NO/views/system.json b/web/public/locales/nb-NO/views/system.json new file mode 100644 index 0000000..f849d70 --- /dev/null +++ b/web/public/locales/nb-NO/views/system.json @@ -0,0 +1,274 @@ +{ + "documentTitle": { + "cameras": "Kamerastatistikk - Frigate", + "storage": "Lagringsstatistikk - Frigate", + "logs": { + "frigate": "Frigate-logger - Frigate", + "go2rtc": "Go2RTC-logger - Frigate", + "nginx": "Nginx-logger - Frigate", + "websocket": "Meldingslogger - Frigate" + }, + "general": "Generell statistikk - Frigate", + "enrichments": "Statistikk for utvidelser - Frigate" + }, + "logs": { + "copy": { + "success": "Logger kopiert til utklippstavlen", + "error": "Kunne ikke kopiere logger til utklippstavlen", + "label": "Kopier til utklippstavle" + }, + "type": { + "label": "Type", + "timestamp": "Tidsstempel", + "tag": "Merke", + "message": "Melding" + }, + "toast": { + "error": { + "fetchingLogsFailed": "Feil ved henting av logger: {{errorMessage}}", + "whileStreamingLogs": "Feil under strømming av logger: {{errorMessage}}" + } + }, + "download": { + "label": "Last ned logger" + }, + "tips": "Logger strømmer fra serveren", + "websocket": { + "label": "Meldinger", + "pause": "Pause", + "resume": "Gjenoppta", + "clear": "Tøm", + "filter": { + "all": "Alle emner", + "topics": "Emner", + "events": "Hendelser", + "reviews": "Inspeksjoner", + "classification": "Klassifisering", + "face_recognition": "Ansiktsgjenkjenning", + "lpr": "LPR", + "camera_activity": "Kameraaktivitet", + "system": "System", + "camera": "Kamera", + "all_cameras": "Alle kameraer", + "cameras_count_one": "{{count}} kamera", + "cameras_count_other": "{{count}} kameraer" + }, + "empty": "Ingen meldinger fanget opp ennå", + "count_one": "{{count}} melding", + "count_other": "{{count}} meldinger", + "expanded": { + "payload": "Payload" + } + } + }, + "general": { + "title": "Generelt", + "detector": { + "inferenceSpeed": "Detektor inferenshastighet", + "title": "Detektorer", + "cpuUsage": "Detektor CPU-belastning", + "memoryUsage": "Detektor minnebruk", + "temperature": "Detektor temperatur", + "cpuUsageInformation": "CPU brukt til å forberede inn- og utdata til/fra deteksjonsmodeller. Denne verdien måler ikke bruk under inferens, selv om en GPU eller akselerator benyttes." + }, + "hardwareInfo": { + "gpuMemory": "GPU-minne", + "gpuEncoder": "GPU-enkoder", + "gpuDecoder": "GPU-dekoder", + "gpuInfo": { + "nvidiaSMIOutput": { + "driver": "Driver: {{driver}}", + "cudaComputerCapability": "CUDA beregningsevne: {{cuda_compute}}", + "vbios": "VBios-info: {{vbios}}", + "title": "Nvidia SMI-utdata", + "name": "Navn: {{name}}" + }, + "copyInfo": { + "label": "Kopier GPU-informasjon" + }, + "toast": { + "success": "GPU-informasjon kopiert til utklippstavlen" + }, + "vainfoOutput": { + "title": "Vainfo-utdata", + "returnCode": "Returkode: {{code}}", + "processOutput": "Prosessutdata:", + "processError": "Prosessfeil:" + }, + "closeInfo": { + "label": "Lukk GPU-informasjon" + } + }, + "title": "Maskinvareinformasjon", + "gpuUsage": "GPU-belastning", + "npuMemory": "NPU minne", + "npuUsage": "NPU-belastning", + "intelGpuWarning": { + "title": "Til info om Intel GPU-statistikk", + "message": "GPU statistikk ikke tilgjengelig", + "description": "Dette er en kjent feil i Intels verktøy for rapportering av GPU-statistikk (intel_gpu_top), der verktøyet slutter å fungere og gjentatte ganger viser 0 % GPU-bruk, selv om maskinvareakselerasjon og objektdeteksjon kjører korrekt på (i)GPU-en. Dette er ikke en feil i Frigate. Du kan starte verten på nytt for å løse problemet midlertidig, og for å bekrefte at GPU-en fungerer som den skal. Dette påvirker ikke ytelsen." + }, + "gpuTemperature": "GPU-temperatur", + "npuTemperature": "NPU-temperatur", + "gpuCompute": "GPU prossesering / enkoding" + }, + "otherProcesses": { + "title": "Andre prosesser", + "processCpuUsage": "Prosessenes CPU-belastning", + "processMemoryUsage": "Prosessenes minnebruk", + "series": { + "go2rtc": "go2rtc", + "recording": "opptak", + "review_segment": "inspeksjonselementer", + "embeddings": "vektorrepresentasjoner", + "audio_detector": "lyddetektor" + } + } + }, + "storage": { + "overview": "Oversikt", + "recordings": { + "earliestRecording": "Tidligste opptak tilgjengelig:", + "title": "Opptak", + "tips": "Denne verdien representerer total lagringsplass brukt av opptakene i Frigates database. Frigate sporer ikke lagringsbruk for alle filer på disken din." + }, + "cameraStorage": { + "storageUsed": "Lagringsbruk", + "bandwidth": "Båndbredde", + "title": "Kameralagring", + "camera": "Kamera", + "unusedStorageInformation": "Ubrukt lagringsinformasjon", + "percentageOfTotalUsed": "Prosentandel av tilgjengelig", + "unused": { + "title": "Ubrukt", + "tips": "Denne verdien representerer kanskje ikke nøyaktig den ledige plassen Frigate har tilgang til, dersom det finnes andre filer lagret på disken. Frigate sporer kun lagring brukt av egne opptak." + } + }, + "title": "Lagring", + "shm": { + "title": "SHM (delt minne) allokering", + "warning": "Den nåværende SHM-størrelsen på {{total}} MB er for liten. Øk den til minst {{min_shm}} MB.", + "frameLifetime": { + "title": "Bildelevetid", + "description": "Hvert kamera har {{frames}} bildeplasser i delt minne (SHM). Ved høyeste bildefrekvens er hvert bilde tilgjengelig i omtrent {{lifetime}} s før det overskrives." + } + } + }, + "cameras": { + "info": { + "codec": "Kodek:", + "resolution": "Oppløsning:", + "audio": "Lyd:", + "error": "Feil: {{error}}", + "cameraProbeInfo": "{{camera}} - kamerainformasjon", + "streamDataFromFFPROBE": "Strømmedata er hentet med ffprobe.", + "fetching": "Henter kameradata", + "stream": "Strøm {{idx}}", + "video": "Video:", + "fps": "FPS:", + "unknown": "Ukjent", + "tips": { + "title": "Kamerainformasjon" + }, + "aspectRatio": "bildeforhold", + "keyframes": { + "error": "Keyframe-intervallet (~{{seconds}}s) overskrider lengden på opptakssegmentet ({{segmentTime}}s). Enkelte segmenter kan mangle en keyframe, noe som ødelegger avspillingen. Deaktiver smart / +kodek'en på kameraet eller forkort keyframe-intervallet.", + "warning": "Spredte eller variable ke,yframes (lengste avstand ~{{seconds}}s), sannsynligvis på grunn av en smart-kodek (H.264+/H.265+). Dette er ikke anbefalt.", + "unknown": "Kunne ikke bestemme keyframe-intervallet.", + "ok": "Keyframes hvert ~{{seconds}}s, optimalt for opptak og avspilling.", + "gap": "Keyframe-intervall (min / snitt / maks):", + "keyframeCount": "Registrerte keyframes:", + "stillAnalyzing": "Analyserer keyframes fremdeles...", + "analyzing": "Analyserer keyframes... {{seconds}} sekunder gjenstår", + "title": "Keyframe-analyse", + "recordStream": "Opptaksstrøm:", + "observedDuration": "Observert varighet:", + "segmentLength": "Lengde på opptakssegment:", + "recordDisabled": "Opptak er deaktivert for dette kameraet." + } + }, + "framesAndDetections": "Bilder / Deteksjoner", + "title": "Kameraer", + "overview": "Oversikt", + "label": { + "camera": "kamera", + "detect": "detektering", + "skipped": "forkastet", + "ffmpeg": "FFmpeg", + "capture": "opptak", + "cameraDetectionsPerSecond": "{{camName}} deteksjoner per sekund", + "cameraSkippedDetectionsPerSecond": "{{camName}} forkastede deteksjoner per sekund", + "cameraFramesPerSecond": "{{camName}} bilder per sekund", + "cameraCapture": "{{camName}} opptak", + "cameraDetect": "{{camName}} detekt", + "cameraFfmpeg": "{{camName}} FFmpeg", + "overallDetectionsPerSecond": "totale deteksjoner per sekund", + "overallSkippedDetectionsPerSecond": "totalt forkastede deteksjoner per sekund", + "overallFramesPerSecond": "totalt bilder per sekund", + "cameraGpu": "{{camName}} GPU" + }, + "toast": { + "success": { + "copyToClipboard": "Kameradata kopiert til utklippstavlen." + }, + "error": { + "unableToProbeCamera": "Kunne ikke hente informasjon fra kamera: {{errorMessage}}" + } + }, + "connectionQuality": { + "title": "Tilkoblingskvalitet", + "excellent": "Utmerket", + "fair": "Grei", + "poor": "Dårlig", + "unusable": "Ubrukelig", + "fps": "BPS", + "expectedFps": "Forventet BPS", + "reconnectsLastHour": "Gjentatte tilkoblinger (siste time)", + "stallsLastHour": "Avbrudd (siste time)" + }, + "noCameras": { + "title": "Ingen kameraer funnet" + } + }, + "enrichments": { + "embeddings": { + "plate_recognition_speed": "Hastighet for kjennemerkegjenkjenning", + "face_embedding_speed": "Hastighet ansikt-vektorrepresentasjon", + "text_embedding_speed": "Hastighet tekst-vektorrepresentasjoner", + "image_embedding_speed": "Hastighet bilde-vektorrepresentasjoner", + "face_recognition_speed": "Hastighet for ansiktsgjenkjenning", + "image_embedding": "Bilde-vektorrepresentasjoner", + "face_recognition": "Ansiktsgjenkjenning", + "text_embedding": "Tekst-vektorrepresentasjoner", + "plate_recognition": "Kjennemerke gjenkjenning", + "yolov9_plate_detection_speed": "Hastighet for YOLOv9 kjennemerkedeteksjon", + "yolov9_plate_detection": "YOLOv9 kjennemerkedeteksjon", + "object_description": "Objektbeskrivelse", + "object_description_speed": "Objektbeskrivelse hastighet", + "object_description_events_per_second": "Objektbeskrivelse", + "review_description": "Inspeksjonsbeskrivelse", + "review_description_events_per_second": "Inspeksjonsbeskrivelse", + "review_description_speed": "Inspeksjonsbeskrivelse hastighet", + "classification": "{{name}} Klassifisering", + "classification_speed": "{{name}} klassifisering hastighet", + "classification_events_per_second": "{{name}} klassifiseringshendelser per sekund" + }, + "title": "Utvidelser", + "infPerSecond": "Inferenser per sekund", + "averageInf": "Gjennomsnittlig inferenstid" + }, + "title": "System", + "metrics": "Systemmålinger", + "lastRefreshed": "Sist oppdatert: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} har høy CPU-belastning for FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} har høy CPU-belastning for detektering ({{detectAvg}}%)", + "healthy": "Systemet fungerer som det skal", + "reindexingEmbeddings": "Reindeksering av vektorrepresentasjoner ({{processed}}% fullført)", + "cameraIsOffline": "{{camera}} er frakoblet", + "detectIsSlow": "{{detect}} er treg ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} er veldig treg ({{speed}} ms)", + "shmTooLow": "/dev/shm-allokeringen ({{total}} MB) bør økes til minst {{min}} MB.", + "debugReplayActive": "Debug-reprise pågår" + } +} diff --git a/web/public/locales/ne/audio.json b/web/public/locales/ne/audio.json new file mode 100644 index 0000000..795ce51 --- /dev/null +++ b/web/public/locales/ne/audio.json @@ -0,0 +1,21 @@ +{ + "speech": "बोली", + "bicycle": "साइकल", + "yell": "चिच्याउनु", + "car": "कार", + "bellow": "तलतिर", + "motorcycle": "मोटरसाइकल", + "whoop": "हुप (Whoop)", + "whispering": "सानो बोल्दै", + "babbling": "बडबडाउँदै", + "bus": "बस", + "laughter": "हाँसो", + "train": "रेल", + "snicker": "स्निकर", + "boat": "डुङ्गा", + "crying": "रुँदै", + "singing": "गाउँदै", + "choir": "गायन यन्त्र", + "yodeling": "योडेलिङ", + "chant": "मन्त्र" +} diff --git a/web/public/locales/ne/common.json b/web/public/locales/ne/common.json new file mode 100644 index 0000000..ec2203d --- /dev/null +++ b/web/public/locales/ne/common.json @@ -0,0 +1,18 @@ +{ + "time": { + "untilForRestart": "फ्रिगेट पुनः सुरु नभएसम्म।", + "untilRestart": "पुन: सुरु नभएसम्म", + "never": "कहिल्यै होइन", + "ago": "{{timeAgo}} अघि", + "untilForTime": "{{time}} सम्म", + "justNow": "भर्खरै", + "today": "आज", + "yesterday": "हिजो", + "last7": "पछिल्लो ७ दिन", + "last14": "पछिल्लो १४ दिन", + "last30": "पछिल्लो ३० दिन", + "thisWeek": "यो हप्ता", + "lastWeek": "गत हप्ता", + "thisMonth": "यो महिना" + } +} diff --git a/web/public/locales/ne/components/auth.json b/web/public/locales/ne/components/auth.json new file mode 100644 index 0000000..e61a7b7 --- /dev/null +++ b/web/public/locales/ne/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "प्रयोगकर्ता नाम", + "password": "पासवर्ड", + "login": "लगइन", + "firstTimeLogin": "पहिलो पटक लग इन गर्ने प्रयास गर्दै हुनुहुन्छ? प्रमाणपत्रहरू फ्रिगेट लगहरूमा छापिएका हुन्छन्।", + "errors": { + "usernameRequired": "प्रयोगकर्ता नाम आवश्यक छ", + "passwordRequired": "पासवर्ड आवश्यक छ", + "rateLimit": "दर सीमा नाघ्यो। पछि फेरि प्रयास गर्नुहोस्।", + "loginFailed": "लगइन असफल भयो", + "unknownError": "अज्ञात त्रुटि। लगहरू जाँच गर्नुहोस्", + "webUnknownError": "अज्ञात त्रुटि। कन्सोल लगहरू जाँच गर्नुहोस्।" + } + } +} diff --git a/web/public/locales/ne/components/camera.json b/web/public/locales/ne/components/camera.json new file mode 100644 index 0000000..59a1682 --- /dev/null +++ b/web/public/locales/ne/components/camera.json @@ -0,0 +1,28 @@ +{ + "group": { + "label": "क्यामेरा समूहहरू", + "add": "क्यामेरा समूह थप्नुहोस्", + "edit": "क्यामेरा समूह सम्पादन गर्नुहोस्", + "delete": { + "label": "क्यामेरा समूह मेटाउनुहोस्", + "confirm": { + "title": "मेटाउने पुष्टि गर्नुहोस्", + "desc": "के तपाईं क्यामेरा समूह {{name}} मेटाउन निश्चित हुनुहुन्छ?" + } + }, + "name": { + "label": "नाम", + "placeholder": "नाम प्रविष्ट गर्नुहोस्…", + "errorMessage": { + "mustLeastCharacters": "क्यामेरा समूहको नाम कम्तिमा २ वर्णको हुनुपर्छ।", + "exists": "क्यामेरा समूहको नाम पहिले नै अवस्थित छ।", + "nameMustNotPeriod": "क्यामेरा समूहको नाममा पूर्णविराम हुनुहुँदैन।", + "invalid": "क्यामेरा समूहको नाम अमान्य छ।" + } + }, + "cameras": { + "label": "क्यामेराहरू", + "desc": "यस समूहको लागि क्यामेराहरू चयन गर्नुहोस्।" + } + } +} diff --git a/web/public/locales/ne/components/dialog.json b/web/public/locales/ne/components/dialog.json new file mode 100644 index 0000000..25cdda5 --- /dev/null +++ b/web/public/locales/ne/components/dialog.json @@ -0,0 +1,34 @@ +{ + "restart": { + "title": "के तपाईं फ्रिगेट पुन: सुरु गर्न चाहनुहुन्छ?", + "description": "यसले फ्रिगेट पुन: सुरु हुँदा केही समयको लागि रोक्नेछ।", + "button": "पुनः सुरु", + "restarting": { + "title": "फ्रिगेट पुन: सुरु हुँदैछ", + "content": "यो पृष्ठ {{countdown}} सेकेन्डमा पुन: लोड हुनेछ।", + "button": "अहिले नै जबरजस्ती पुन: लोड गर्नुहोस्" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "फ्रिगेट+ मा पेश गर्नुहोस्", + "desc": "तपाईंले बेवास्ता गर्न चाहनुभएको स्थानहरूमा रहेका वस्तुहरू गलत सकारात्मक होइनन्। तिनीहरूलाई गलत सकारात्मकको रूपमा पेश गर्नाले मोडेल भ्रमित हुनेछ।" + }, + "review": { + "question": { + "label": "फ्रिगेट प्लसको लागि यो लेबल पुष्टि गर्नुहोस्", + "ask_a": "के यो वस्तु {{label}} हो?", + "ask_an": "के यो वस्तु {{label}} हो?", + "ask_full": "के यो वस्तु {{untranslatedLabel}} ({{translatedLabel}}) हो?" + }, + "state": { + "submitted": "पेश गरियो" + } + } + }, + "video": { + "viewInHistory": "इतिहासमा हेर्नुहोस्" + } + } +} diff --git a/web/public/locales/ne/components/filter.json b/web/public/locales/ne/components/filter.json new file mode 100644 index 0000000..40a28af --- /dev/null +++ b/web/public/locales/ne/components/filter.json @@ -0,0 +1,30 @@ +{ + "filter": "फिल्टर गर्नुहोस्", + "classes": { + "label": "कक्षाहरू", + "all": { + "title": "सबै कक्षाहरू" + }, + "count_one": "{{count}} कक्षा", + "count_other": "{{count}} कक्षाहरू" + }, + "labels": { + "label": "लेबलहरू", + "all": { + "title": "सबै लेबलहरू", + "short": "लेबलहरू" + }, + "count_one": "{{count}} लेबल", + "count_other": "{{count}} लेबलहरू" + }, + "zones": { + "label": "क्षेत्रहरू", + "all": { + "title": "सबै क्षेत्रहरू", + "short": "क्षेत्रहरू" + } + }, + "dates": { + "selectPreset": "प्रिसेट चयन गर्नुहोस्…" + } +} diff --git a/web/public/locales/ne/components/icons.json b/web/public/locales/ne/components/icons.json new file mode 100644 index 0000000..208c39a --- /dev/null +++ b/web/public/locales/ne/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "आइकन चयन गर्नुहोस्", + "search": { + "placeholder": "आइकन खोज्नुहोस्…" + } + } +} diff --git a/web/public/locales/ne/components/input.json b/web/public/locales/ne/components/input.json new file mode 100644 index 0000000..c1990b9 --- /dev/null +++ b/web/public/locales/ne/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "डाउनलोड भिडियो", + "toast": { + "success": "तपाईंको समीक्षा वस्तुको भिडियो डाउनलोड हुन थालेको छ।" + } + } + } +} diff --git a/web/public/locales/ne/components/player.json b/web/public/locales/ne/components/player.json new file mode 100644 index 0000000..ffb97d5 --- /dev/null +++ b/web/public/locales/ne/components/player.json @@ -0,0 +1,26 @@ +{ + "noPreviewFound": "कुनै पूर्वावलोकन फेला परेन", + "noPreviewFoundFor": "{{cameraName}} को लागि कुनै पूर्वावलोकन फेला परेन।", + "submitFrigatePlus": { + "title": "यो फ्रेम Frigate+ मा बुझाउने हो?", + "submit": "पेश गर्नुहोस्", + "previewError": "स्न्यापसट पूर्वावलोकन लोड गर्न सकिएन। रेकर्डिङ यस समयमा उपलब्ध नहुन सक्छ।" + }, + "noRecordingsFoundForThisTime": "यस समयको लागि कुनै रेकर्डिङ फेला परेन", + "livePlayerRequiredIOSVersion": "यस लाइभ स्ट्रिम प्रकारको लागि iOS १७.१ वा सोभन्दा माथिको संस्करण आवश्यक छ।", + "streamOffline": { + "title": "अफलाइन स्ट्रिम गर्नुहोस्", + "desc": "{{cameraName}} detect स्ट्रिममा कुनै पनि फ्रेमहरू प्राप्त भएका छैनन्, त्रुटि लगहरू जाँच गर्नुहोस्" + }, + "cameraDisabled": "क्यामेरा असक्षम पारिएको छ", + "stats": { + "streamType": { + "title": "स्ट्रिम प्रकार:", + "short": "प्रकार" + }, + "bandwidth": { + "title": "ब्यान्डविथ:", + "short": "ब्यान्डविथ" + } + } +} diff --git a/web/public/locales/ne/config/cameras.json b/web/public/locales/ne/config/cameras.json new file mode 100644 index 0000000..3179ad3 --- /dev/null +++ b/web/public/locales/ne/config/cameras.json @@ -0,0 +1,33 @@ +{ + "label": "क्यामेरा कन्फिग", + "name": { + "label": "क्यामेराको नाम", + "description": "क्यामेराको नाम आवश्यक छ" + }, + "friendly_name": { + "label": "मैत्रीपूर्ण नाम", + "description": "फ्रिगेट UI मा प्रयोग गरिएको क्यामेरा मैत्री नाम" + }, + "enabled": { + "label": "सक्षम पारिएको", + "description": "सक्षम पारिएको" + }, + "audio": { + "label": "अडियो पत्ता लगाउने सुविधा", + "description": "यस क्यामेराको लागि अडियो-आधारित घटना पत्ता लगाउने सेटिङहरू।", + "enabled": { + "label": "अडियो पत्ता लगाउने सुविधा सक्षम पार्नुहोस्", + "description": "यस क्यामेराको लागि अडियो घटना पत्ता लगाउने सुविधा सक्षम वा असक्षम पार्नुहोस्।" + }, + "max_not_heard": { + "label": "समयसीमा समाप्त गर्नुहोस्", + "description": "अडियो घटना समाप्त हुनुभन्दा पहिले कन्फिगर गरिएको अडियो प्रकार बिना सेकेन्डको मात्रा।" + }, + "min_volume": { + "label": "न्यूनतम भोल्युम" + } + }, + "zones": { + "label": "क्षेत्रहरू" + } +} diff --git a/web/public/locales/ne/config/global.json b/web/public/locales/ne/config/global.json new file mode 100644 index 0000000..fe3f978 --- /dev/null +++ b/web/public/locales/ne/config/global.json @@ -0,0 +1,42 @@ +{ + "version": { + "label": "हालको कन्फिगरेसन संस्करण", + "description": "माइग्रेसन वा ढाँचा परिवर्तनहरू पत्ता लगाउन मद्दत गर्न सक्रिय कन्फिगरेसनको संख्यात्मक वा स्ट्रिङ संस्करण।" + }, + "safe_mode": { + "label": "सुरक्षित मोड", + "description": "सक्षम हुँदा, समस्या निवारणको लागि कम सुविधाहरूको साथ सुरक्षित मोडमा फ्रिगेट सुरु गर्नुहोस्।" + }, + "environment_vars": { + "label": "वातावरणीय चरहरू", + "description": "होम असिस्टेन्ट ओएसमा फ्रिगेट प्रक्रियाको लागि सेट गर्नुपर्ने वातावरण चरहरूको कुञ्जी/मान जोडीहरू। गैर-HAOS प्रयोगकर्ताहरूले यसको सट्टा डकर वातावरण चर कन्फिगरेसन प्रयोग गर्नुपर्छ।" + }, + "logger": { + "label": "लगिङ", + "description": "पूर्वनिर्धारित लग शब्दावली र प्रति-घटक लग स्तर ओभरराइडहरू नियन्त्रण गर्दछ।", + "default": { + "label": "लगिङ स्तर", + "description": "पूर्वनिर्धारित विश्वव्यापी लग शब्दावली (डिबग, जानकारी, चेतावनी, त्रुटि)।" + }, + "logs": { + "label": "प्रति-प्रक्रिया लग स्तर", + "description": "विशिष्ट मोड्युलहरूको लागि शब्दावली बढाउन वा घटाउन प्रति-घटक लग स्तर ओभरराइड हुन्छ।" + } + }, + "audio": { + "label": "अडियो पत्ता लगाउने सुविधा", + "enabled": { + "label": "अडियो पत्ता लगाउने सुविधा सक्षम पार्नुहोस्" + }, + "max_not_heard": { + "label": "समयसीमा समाप्त गर्नुहोस्", + "description": "अडियो घटना समाप्त हुनुभन्दा पहिले कन्फिगर गरिएको अडियो प्रकार बिना सेकेन्डको मात्रा।" + }, + "min_volume": { + "label": "न्यूनतम भोल्युम" + } + }, + "auth": { + "label": "प्रमाणीकरण" + } +} diff --git a/web/public/locales/ne/config/groups.json b/web/public/locales/ne/config/groups.json new file mode 100644 index 0000000..f3ea1a4 --- /dev/null +++ b/web/public/locales/ne/config/groups.json @@ -0,0 +1,44 @@ +{ + "audio": { + "global": { + "detection": "विश्वव्यापी पत्ता-लगाउने", + "sensitivity": "विश्वव्यापी संवेदनशीलता" + }, + "cameras": { + "detection": "पत्ता-लगाउने", + "sensitivity": "संवेदनशीलता" + } + }, + "timestamp_style": { + "global": { + "appearance": "विश्वव्यापी उपस्थिति" + }, + "cameras": { + "appearance": "उपस्थिति" + } + }, + "motion": { + "global": { + "sensitivity": "विश्वव्यापी संवेदनशीलता", + "algorithm": "विश्वव्यापी एल्गोरिथम" + }, + "cameras": { + "sensitivity": "संवेदनशीलता", + "algorithm": "एल्गोरिथ्म" + } + }, + "snapshots": { + "global": { + "display": "विश्वव्यापी प्रदर्शन" + }, + "cameras": { + "display": "प्रदर्शन" + } + }, + "detect": { + "global": { + "resolution": "विश्वव्यापी रिजोल्युसन", + "tracking": "विश्वव्यापी ट्र्याकिङ" + } + } +} diff --git a/web/public/locales/ne/config/validation.json b/web/public/locales/ne/config/validation.json new file mode 100644 index 0000000..ead6ccf --- /dev/null +++ b/web/public/locales/ne/config/validation.json @@ -0,0 +1,16 @@ +{ + "minimum": "कम्तिमा हुनुपर्छ {{limit}}", + "maximum": "बढीमा हुनुपर्छ {{limit}}", + "exclusiveMinimum": "{{limit}} भन्दा बढी हुनुपर्छ", + "exclusiveMaximum": ".{{limit}} भन्दा कम हुनुपर्छ", + "minLength": "कम्तिमा {{limit}} वर्ण(हरू) हुनुपर्छ।", + "maxLength": "बढीमा {{limit}} वर्ण(हरू) हुनु पर्छ", + "minItems": "कम्तिमा {{limit}} वस्तुहरू हुनुपर्छ", + "maxItems": "बढीमा {{limit}} वस्तुहरू हुनुपर्छ", + "pattern": "अमान्य ढाँचा", + "required": "यो क्षेत्र आवश्यक छ", + "type": "अमान्य मान प्रकार", + "enum": "अनुमति दिइएको मानहरू मध्ये एक हुनुपर्छ", + "const": "मान अपेक्षित स्थिरांकसँग मेल खाँदैन", + "uniqueItems": "सबै वस्तुहरू अद्वितीय हुनुपर्छ" +} diff --git a/web/public/locales/ne/objects.json b/web/public/locales/ne/objects.json new file mode 100644 index 0000000..e1826aa --- /dev/null +++ b/web/public/locales/ne/objects.json @@ -0,0 +1,16 @@ +{ + "person": "व्यक्ति", + "bicycle": "साइकल", + "car": "कार", + "motorcycle": "मोटरसाइकल", + "airplane": "हवाइजहाज", + "bus": "बस", + "train": "रेल", + "boat": "डुङ्गा", + "traffic_light": "ट्राफिक लाइट", + "fire_hydrant": "आगो निभाउने यन्त्र", + "street_sign": "सडक चिन्ह", + "stop_sign": "रोक चिन्ह", + "parking_meter": "पार्किङ मिटर", + "bench": "बेन्च" +} diff --git a/web/public/locales/ne/views/chat.json b/web/public/locales/ne/views/chat.json new file mode 100644 index 0000000..1afa99c --- /dev/null +++ b/web/public/locales/ne/views/chat.json @@ -0,0 +1,15 @@ +{ + "documentTitle": "च्याट - फ्रिगेट", + "title": "फ्रिगेट च्याट", + "subtitle": "क्यामेरा व्यवस्थापन र अन्तर्दृष्टिको लागि तपाईंको एआई सहायक", + "placeholder": "सोध्नुहोस्...", + "error": "केही गडबड भयो। कृपया फेरि प्रयास गर्नुहोस्।", + "processing": "प्रशोधन गर्दै...", + "toolsUsed": "प्रयोग गरिएको: {{tools}}", + "showTools": "उपकरणहरू देखाउनुहोस् ({{count}})", + "hideTools": "उपकरणहरू लुकाउनुहोस्", + "call": "कल गर्नुहोस्", + "result": "नतिजा", + "arguments": "तर्कहरू:", + "response": "प्रतिक्रिया:" +} diff --git a/web/public/locales/ne/views/classificationModel.json b/web/public/locales/ne/views/classificationModel.json new file mode 100644 index 0000000..e37b8ee --- /dev/null +++ b/web/public/locales/ne/views/classificationModel.json @@ -0,0 +1,25 @@ +{ + "documentTitle": "वर्गीकरण मोडेलहरू - फ्रिगेट", + "details": { + "scoreInfo": "स्कोरले यस वस्तुको सबै पत्ता लगाउने कार्यहरूमा औसत वर्गीकरण विश्वासलाई प्रतिनिधित्व गर्दछ।", + "none": "कुनै पनि होइन", + "unknown": "अज्ञात" + }, + "description": { + "invalidName": "अमान्य नाम। नामहरूमा अक्षर, संख्या, खाली ठाउँ, अपोस्ट्रोफी, अन्डरस्कोर र हाइफन मात्र समावेश हुन सक्छन्।" + }, + "button": { + "deleteClassificationAttempts": "वर्गीकरण छविहरू मेटाउनुहोस्", + "renameCategory": "वर्गको नाम बदल्नुहोस्", + "deleteCategory": "कक्षा मेटाउनुहोस्", + "deleteImages": "छविहरू मेटाउनुहोस्", + "trainModel": "रेल मोडेल", + "addClassification": "वर्गीकरण थप्नुहोस्", + "deleteModels": "मोडेलहरू मेटाउनुहोस्", + "editModel": "मोडेल सम्पादन गर्नुहोस्" + }, + "tooltip": { + "trainingInProgress": "मोडेल हाल प्रशिक्षणमा छिन्", + "noNewImages": "तालिम दिनको लागि कुनै नयाँ तस्बिरहरू छैनन्। पहिले डेटासेटमा थप तस्बिरहरू वर्गीकृत गर्नुहोस्।" + } +} diff --git a/web/public/locales/ne/views/configEditor.json b/web/public/locales/ne/views/configEditor.json new file mode 100644 index 0000000..ac03412 --- /dev/null +++ b/web/public/locales/ne/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "कन्फिग सम्पादक - फ्रिगेट", + "configEditor": "कन्फिग सम्पादक", + "safeConfigEditor": "कन्फिग सम्पादक (सुरक्षित मोड)", + "safeModeDescription": "कन्फिग प्रमाणीकरण त्रुटिको कारणले फ्रिगेट सुरक्षित मोडमा छ।", + "copyConfig": "कन्फिग प्रतिलिपि गर्नुहोस्", + "saveAndRestart": "बचत गर्नुहोस् र पुन: सुरु गर्नुहोस्", + "saveOnly": "बचत मात्र", + "confirm": "बचत नगरी बाहिर निस्कने हो?", + "toast": { + "success": { + "copyToClipboard": "कन्फिगरेसन क्लिपबोर्डमा प्रतिलिपि गरियो।" + }, + "error": { + "savingError": "कन्फिगरेसन बचत गर्दा त्रुटि भयो" + } + } +} diff --git a/web/public/locales/ne/views/events.json b/web/public/locales/ne/views/events.json new file mode 100644 index 0000000..b04c4d7 --- /dev/null +++ b/web/public/locales/ne/views/events.json @@ -0,0 +1,23 @@ +{ + "alerts": "अलर्टहरू", + "detections": "पत्ता लगाउने", + "motion": { + "label": "गति", + "only": "गति मात्र" + }, + "allCameras": "सबै क्यामेराहरू", + "empty": { + "alert": "समीक्षा गर्न कुनै अलर्टहरू छैनन्", + "detection": "समीक्षा गर्न कुनै पनि पत्ता लगाइएको छैन", + "motion": "गतिसम्बन्धी कुनै डेटा फेला परेन", + "recordingsDisabled": { + "title": "रेकर्डिङहरू सक्षम पारिएको हुनुपर्छ", + "description": "क्यामेराको लागि रेकर्डिङ सक्षम पारिएको बेला मात्र समीक्षा वस्तुहरू सिर्जना गर्न सकिन्छ।" + } + }, + "timeline": { + "label": "समयरेखा", + "aria": "टाइमलाइन चयन गर्नुहोस्" + }, + "zoomIn": "जुम इन गर्नुहोस्" +} diff --git a/web/public/locales/ne/views/explore.json b/web/public/locales/ne/views/explore.json new file mode 100644 index 0000000..80ca127 --- /dev/null +++ b/web/public/locales/ne/views/explore.json @@ -0,0 +1,28 @@ +{ + "details": { + "timestamp": "टाइमस्ट्याम्प" + }, + "documentTitle": "अन्वेषण गर्नुहोस् - फ्रिगेट", + "generativeAI": "जेनेरेटिभ एआई", + "exploreMore": "थप {{label}} वस्तुहरू अन्वेषण गर्नुहोस्", + "exploreIsUnavailable": { + "title": "अन्वेषण उपलब्ध छैन", + "embeddingsReindexing": { + "context": "ट्र्याक गरिएका वस्तु इम्बेडिङहरूले पुन: अनुक्रमणिका समाप्त गरेपछि अन्वेषण प्रयोग गर्न सकिन्छ।", + "startingUp": "सुरु गर्दै…", + "estimatedTime": "अनुमानित बाँकी समय:", + "finishingShortly": "चाँडै नै समाप्त हुँदैछ", + "step": { + "thumbnailsEmbedded": "इम्बेड गरिएका थम्बनेलहरू: ", + "descriptionsEmbedded": "इम्बेड गरिएका विवरणहरू: ", + "trackedObjectsProcessed": "ट्र्याक गरिएका वस्तुहरू प्रशोधन गरियो: " + } + }, + "downloadingModels": { + "context": "फ्रिगेटले सिमान्टिक खोज सुविधालाई समर्थन गर्न आवश्यक इम्बेडिङ मोडेलहरू डाउनलोड गर्दैछ। तपाईंको नेटवर्क जडानको गतिमा निर्भर गर्दै यसले धेरै मिनेट लिन सक्छ।", + "setup": { + "visionModel": "भिजन मोडेल" + } + } + } +} diff --git a/web/public/locales/ne/views/exports.json b/web/public/locales/ne/views/exports.json new file mode 100644 index 0000000..2afa762 --- /dev/null +++ b/web/public/locales/ne/views/exports.json @@ -0,0 +1,24 @@ +{ + "search": "खोज्नुहोस्", + "noExports": "कुनै निर्यात भेटिएन", + "headings": { + "cases": "केसहरू", + "uncategorizedExports": "वर्गीकृत नगरिएका निर्यातहरू" + }, + "documentTitle": "निर्यात - फ्रिगेट", + "deleteExport": { + "label": "निर्यात मेटाउनुहोस्", + "desc": "के तपाईं {{exportName}} मेटाउन चाहनुहुन्छ?" + }, + "editExport": { + "title": "निर्यातको नाम बदल्नुहोस्", + "desc": "यो निर्यातको लागि नयाँ नाम प्रविष्ट गर्नुहोस्।", + "saveExport": "निर्यात बचत गर्नुहोस्" + }, + "tooltip": { + "shareExport": "निर्यात सेयर गर्नुहोस्", + "downloadVideo": "भिडियो डाउनलोड गर्नुहोस्", + "editName": "नाम सम्पादन गर्नुहोस्", + "deleteExport": "निर्यात मेटाउनुहोस्" + } +} diff --git a/web/public/locales/ne/views/faceLibrary.json b/web/public/locales/ne/views/faceLibrary.json new file mode 100644 index 0000000..1728f99 --- /dev/null +++ b/web/public/locales/ne/views/faceLibrary.json @@ -0,0 +1,26 @@ +{ + "description": { + "addFace": "आफ्नो पहिलो तस्विर अपलोड गरेर अनुहार पुस्तकालयमा नयाँ संग्रह थप्नुहोस्।", + "placeholder": "यो सङ्ग्रहको लागि नाम प्रविष्ट गर्नुहोस्", + "invalidName": "अमान्य नाम। नामहरूमा अक्षर, संख्या, खाली ठाउँ, अपोस्ट्रोफी, अन्डरस्कोर र हाइफन मात्र समावेश हुन सक्छन्।", + "nameCannotContainHash": "नाममा # हुन सक्दैन।" + }, + "details": { + "unknown": "अज्ञात", + "timestamp": "टाइमस्ट्याम्प", + "scoreInfo": "स्कोर भनेको सबै अनुहारको स्कोरको भारित औसत हो, जुन प्रत्येक छविमा अनुहारको आकारद्वारा भारित हुन्छ।" + }, + "documentTitle": "फेस लाइब्रेरी - फ्रिगेट", + "uploadFaceImage": { + "title": "अनुहारको छवि अपलोड गर्नुहोस्", + "desc": "अनुहारहरू स्क्यान गर्न र {{pageToggle}} को लागि समावेश गर्न एउटा छवि अपलोड गर्नुहोस्" + }, + "collections": "सङ्ग्रहहरू", + "createFaceLibrary": { + "new": "नयाँ अनुहार सिर्जना गर्नुहोस्", + "nextSteps": "बलियो जग निर्माण गर्न:
  • प्रत्येक पत्ता लागेको व्यक्तिको लागि छविहरू चयन गर्न र तालिम दिन हालसालैको पहिचान ट्याब प्रयोग गर्नुहोस्।
  • उत्तम परिणामहरूको लागि सिधा-अन छविहरूमा ध्यान केन्द्रित गर्नुहोस्; कोणमा अनुहारहरू खिच्ने तालिम छविहरूबाट बच्नुहोस्।
  • " + }, + "steps": { + "faceName": "अनुहारको नाम प्रविष्ट गर्नुहोस्" + } +} diff --git a/web/public/locales/ne/views/live.json b/web/public/locales/ne/views/live.json new file mode 100644 index 0000000..2a4a191 --- /dev/null +++ b/web/public/locales/ne/views/live.json @@ -0,0 +1,34 @@ +{ + "documentTitle": { + "default": "प्रत्यक्ष - फ्रिगेट", + "withCamera": "{{camera}} - प्रत्यक्ष - फ्रिगेट" + }, + "lowBandwidthMode": "कम-ब्यान्डविथ मोड", + "twoWayTalk": { + "enable": "दुईतर्फी कुराकानी सक्षम पार्नुहोस्", + "disable": "दुईतर्फी कुराकानी असक्षम पार्नुहोस्" + }, + "cameraAudio": { + "enable": "क्यामेरा अडियो सक्षम पार्नुहोस्", + "disable": "क्यामेरा अडियो असक्षम पार्नुहोस्" + }, + "ptz": { + "move": { + "clickMove": { + "label": "क्यामेरालाई केन्द्रमा राख्न फ्रेममा क्लिक गर्नुहोस्", + "enable": "सार्न क्लिक गर्नुहोस् सक्षम पार्नुहोस्", + "enableWithZoom": "सार्न क्लिक गर्नुहोस् / जुम गर्न तान्नुहोस् सक्षम गर्नुहोस्", + "disable": "सार्न क्लिक गर्ने सुविधा असक्षम पार्नुहोस्" + }, + "left": { + "label": "PTZ क्यामेरालाई बायाँतिर सार्नुहोस्" + }, + "up": { + "label": "PTZ क्यामेरा माथि सार्नुहोस्" + }, + "down": { + "label": "PTZ क्यामेरा तल सार्नुहोस्" + } + } + } +} diff --git a/web/public/locales/ne/views/motionSearch.json b/web/public/locales/ne/views/motionSearch.json new file mode 100644 index 0000000..22533b1 --- /dev/null +++ b/web/public/locales/ne/views/motionSearch.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "गति खोज - फ्रिगेट", + "title": "गति खोज", + "description": "रुचिको क्षेत्र परिभाषित गर्न बहुभुज कोर्नुहोस्, र त्यो क्षेत्र भित्र गति परिवर्तनहरू खोज्नको लागि समय दायरा निर्दिष्ट गर्नुहोस्।", + "selectCamera": "गति खोज लोड हुँदैछ", + "startSearch": "खोज सुरु गर्नुहोस्", + "searchStarted": "खोजी सुरु भयो", + "searchCancelled": "खोज रद्द गरियो", + "cancelSearch": "रद्द गर्नुहोस्", + "searching": "खोजी भइरहेको छ।", + "searchComplete": "खोज पूरा भयो", + "noResultsYet": "चयन गरिएको क्षेत्रमा चाल परिवर्तनहरू फेला पार्न खोज चलाउनुहोस्", + "noChangesFound": "चयन गरिएको क्षेत्रमा कुनै पिक्सेल परिवर्तनहरू फेला परेनन्", + "changesFound_one": "{{count}} गति परिवर्तन फेला पर्यो", + "changesFound_other": "{{count}} गति परिवर्तनहरू फेला परे", + "framesProcessed": "{{count}} फ्रेमहरू प्रशोधन गरियो" +} diff --git a/web/public/locales/ne/views/recording.json b/web/public/locales/ne/views/recording.json new file mode 100644 index 0000000..03ee1d4 --- /dev/null +++ b/web/public/locales/ne/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "निर्यात", + "filter": "फिल्टर गर्नुहोस्", + "calendar": "कैलेंडर", + "filters": "फिल्टरहरू", + "toast": { + "error": { + "noValidTimeSelected": "कुनै मान्य समय दायरा चयन गरिएको छैन", + "endTimeMustAfterStartTime": "अन्त्य समय सुरु समय पछि हुनुपर्छ" + } + } +} diff --git a/web/public/locales/ne/views/replay.json b/web/public/locales/ne/views/replay.json new file mode 100644 index 0000000..a9185bb --- /dev/null +++ b/web/public/locales/ne/views/replay.json @@ -0,0 +1,20 @@ +{ + "title": "डिबग उत्तर", + "description": "डिबगिङको लागि क्यामेरा रेकर्डिङहरू पुन: प्ले गर्नुहोस्। वस्तु सूचीले पत्ता लगाइएका वस्तुहरूको समय-ढिलाइ भएको सारांश देखाउँछ र सन्देश ट्याबले रिप्ले फुटेजबाट फ्रिगेटको आन्तरिक सन्देशहरूको स्ट्रिम देखाउँछ।", + "websocket_messages": "सन्देशहरू", + "dialog": { + "title": "डिबग रिप्ले सुरु गर्नुहोस्", + "description": "वस्तु पत्ता लगाउने र ट्र्याकिङ समस्याहरू डिबग गर्न ऐतिहासिक फुटेज लुप गर्ने अस्थायी रिप्ले क्यामेरा सिर्जना गर्नुहोस्। रिप्ले क्यामेरामा स्रोत क्यामेरा जस्तै पत्ता लगाउने कन्फिगरेसन हुनेछ। सुरु गर्न समय दायरा छनौट गर्नुहोस्।", + "camera": "स्रोत क्यामेरा", + "timeRange": "समय दायरा", + "preset": { + "1m": "अन्तिम १ मिनेट", + "5m": "अन्तिम ५ मिनेट", + "timeline": "टाइमलाइनबाट", + "custom": "अनुकूलन" + }, + "startButton": "रिप्ले सुरु गर्नुहोस्", + "selectFromTimeline": "चयन गर्नुहोस्", + "starting": "रिप्ले सुरु गर्दै..." + } +} diff --git a/web/public/locales/ne/views/search.json b/web/public/locales/ne/views/search.json new file mode 100644 index 0000000..185843f --- /dev/null +++ b/web/public/locales/ne/views/search.json @@ -0,0 +1,22 @@ +{ + "search": "खोज्नुहोस्", + "savedSearches": "सुरक्षित गरिएका खोजहरू", + "searchFor": "खोज्नुहोस् {{inputValue}}", + "button": { + "clear": "खोज खाली गर्नुहोस्", + "save": "खोज बचत गर्नुहोस्", + "delete": "सुरक्षित गरिएको खोज मेटाउनुहोस्", + "filterInformation": "फिल्टर जानकारी", + "filterActive": "फिल्टरहरू सक्रिय छन्" + }, + "trackedObjectId": "ट्र्याक गरिएको वस्तु ID", + "filter": { + "label": { + "cameras": "क्यामेराहरू", + "labels": "लेबलहरू", + "zones": "क्षेत्रहरू", + "sub_labels": "उप लेबलहरू", + "attributes": "विशेषताहरू" + } + } +} diff --git a/web/public/locales/ne/views/settings.json b/web/public/locales/ne/views/settings.json new file mode 100644 index 0000000..0ce812e --- /dev/null +++ b/web/public/locales/ne/views/settings.json @@ -0,0 +1,17 @@ +{ + "documentTitle": { + "default": "सेटिङहरू - फ्रिगेट", + "authentication": "प्रमाणीकरण सेटिङहरू - फ्रिगेट", + "cameraManagement": "क्यामेराहरू व्यवस्थापन गर्नुहोस् - फ्रिगेट", + "cameraReview": "क्यामेरा समीक्षा सेटिङहरू - फ्रिगेट", + "enrichments": "संवर्धन सेटिङहरू - फ्रिगेट", + "masksAndZones": "मास्क र जोन सम्पादक - फ्रिगेट", + "motionTuner": "मोशन ट्युनर - फ्रिगेट", + "object": "डिबग - फ्रिगेट", + "general": "UI सेटिङहरू - फ्रिगेट", + "globalConfig": "विश्वव्यापी कन्फिगरेसन - फ्रिगेट", + "cameraConfig": "क्यामेरा कन्फिगरेसन - फ्रिगेट", + "frigatePlus": "फ्रिगेट+ सेटिङहरू - फ्रिगेट", + "notifications": "सूचना सेटिङहरू - फ्रिगेट" + } +} diff --git a/web/public/locales/ne/views/system.json b/web/public/locales/ne/views/system.json new file mode 100644 index 0000000..e399283 --- /dev/null +++ b/web/public/locales/ne/views/system.json @@ -0,0 +1,24 @@ +{ + "documentTitle": { + "cameras": "क्यामेरा तथ्याङ्क - फ्रिगेट", + "storage": "भण्डारण तथ्याङ्क - फ्रिगेट", + "general": "सामान्य तथ्याङ्क - फ्रिगेट", + "enrichments": "संवर्धन तथ्याङ्क - फ्रिगेट", + "logs": { + "frigate": "फ्रिगेट लगहरू - फ्रिगेट", + "go2rtc": "Go2RTC लगहरू - फ्रिगेट", + "nginx": "Nginx लगहरू - फ्रिगेट", + "websocket": "सन्देश लगहरू - फ्रिगेट" + } + }, + "title": "प्रणाली", + "metrics": "प्रणाली मेट्रिक्स", + "logs": { + "websocket": { + "label": "सन्देशहरू", + "pause": "पज गर्नुहोस्", + "resume": "पुनःसुरु गर्नुहोस्", + "clear": "खाली गर्नुहोस्" + } + } +} diff --git a/web/public/locales/nl/audio.json b/web/public/locales/nl/audio.json new file mode 100644 index 0000000..59268c7 --- /dev/null +++ b/web/public/locales/nl/audio.json @@ -0,0 +1,503 @@ +{ + "babbling": "Brabbelen", + "bellow": "Brullen", + "laughter": "Gelach", + "snicker": "Grinniken", + "crying": "Huilen", + "sigh": "Zucht", + "singing": "Zingen", + "choir": "Koor", + "yodeling": "Jodelen", + "mantra": "Mantra", + "child_singing": "Zingend kind", + "rapping": "Rappen", + "breathing": "Ademhaling", + "wheeze": "Piepen", + "snoring": "Snurken", + "gasp": "Snakken naar adem", + "pant": "Hijgen", + "snort": "Snuiven", + "sneeze": "Niezen", + "shuffle": "Schudden", + "footsteps": "Voetstappen", + "gargling": "Gorgelen", + "stomach_rumble": "Maag rommelt", + "burping": "Boeren", + "fart": "Scheet", + "finger_snapping": "Vingerknippen", + "applause": "Applaus", + "chatter": "Geklets", + "howl": "Huilkreet", + "bow_wow": "Woef woef", + "growling": "Grommen", + "whimper_dog": "Hondengejank", + "meow": "Miauw", + "hiss": "Sissen", + "livestock": "Vee", + "horse": "Paard", + "clip_clop": "Hoefslagen", + "neigh": "Hinniken", + "cattle": "Runderen", + "moo": "loeien", + "cowbell": "Koeienbel", + "pig": "Varkens", + "oink": "Knorren", + "bleat": "Blaten", + "chicken": "Kip", + "cluck": "Tok", + "turkey": "Kalkoen", + "quack": "Kwak", + "goose": "Gans", + "honk": "gakken", + "wild_animals": "Wilde dieren", + "roaring_cats": "Schreeuwende kat", + "roar": "Brul", + "bird": "Vogel", + "chirp": "Tjilpen", + "squawk": "Gekrijs", + "pigeon": "Duif", + "coo": "Koeren", + "crow": "Kraai", + "caw": "Kauw", + "owl": "Uil", + "hoot": "uilengeroep", + "flapping_wings": "Flapperende vleugels", + "rats": "Ratten", + "mouse": "Muis", + "patter": "Getrippel", + "insect": "Insect", + "mosquito": "Mug", + "buzz": "Gezoem", + "frog": "Kikker", + "croak": "Kwaken", + "snake": "Slang", + "rattle": "Rammel", + "music": "Muziek", + "plucked_string_instrument": "Snaarinstrument", + "guitar": "Gitaar", + "electric_guitar": "Elektrische gitaar", + "bass_guitar": "Basgitaar", + "acoustic_guitar": "Akoestische gitaar", + "steel_guitar": "Steel Guitar", + "tapping": "Tikken", + "strum": "Aanslaan", + "sitar": "Sitar", + "mandolin": "Mandoline", + "zither": "Citer", + "sniff": "Snuif", + "yell": "Schreeuwen", + "whoop": "Gejuich", + "chant": "Lied", + "whistling": "Gefluit", + "hands": "Handen", + "sheep": "Schaap", + "synthetic_singing": "Synthetisch zingen", + "run": "Ren", + "humming": "Zoemen", + "chewing": "Kauwen", + "bark": "Blaffen", + "animal": "Dier", + "cough": "Hoest", + "throat_clearing": "Keel schrapen", + "biting": "Bijten", + "heart_murmur": "Hartruis", + "crowd": "Menigte", + "pets": "Huisdieren", + "heartbeat": "Hartslag", + "children_playing": "Kinderen spelen", + "purr": "Spinnen", + "speech": "Spraak", + "whispering": "Fluisteren", + "yip": "Kef", + "groan": "Kreunen", + "fowl": "Gevogelte", + "grunt": "brommend", + "cock_a_doodle_doo": "Kukeleku", + "cheering": "Juichen", + "caterwaul": "Kattengehuil", + "hiccup": "Hik", + "clapping": "Klappen", + "dogs": "Honden", + "cat": "Kat", + "dog": "Hond", + "goat": "Geit", + "cricket": "Krekel", + "musical_instrument": "Muziek Instrument", + "duck": "Eend", + "banjo": "Banjo", + "gobble": "kalkoenroep", + "fly": "Vlieg", + "whale_vocalization": "Vocalisatie van walvissen", + "keyboard": "Klavier", + "piano": "Piano", + "ukulele": "Ukulele", + "electric_piano": "Elektrische piano", + "organ": "Orgel", + "sampler": "Sampler", + "harpsichord": "Klavecimbel", + "percussion": "Slagwerk", + "drum_kit": "Drumstel", + "snare_drum": "Snaartrommel", + "drum_roll": "Tromgeroffel", + "bass_drum": "Basdrum", + "tabla": "Tabla", + "cymbal": "Bekken", + "hi_hat": "Hi-Hat", + "maraca": "Sambabal", + "tubular_bells": "Buisklokken", + "mallet_percussion": "Mallet instrumenten", + "marimba": "Marimba", + "glockenspiel": "Klokkenspel", + "steelpan": "Steeldrum", + "brass_instrument": "Koperblaasinstrumenten", + "french_horn": "Waldhoorn", + "trombone": "Trombone", + "string_section": "Snaar sectie", + "wind_instrument": "Blaasinstrument", + "clarinet": "Klarinet", + "harp": "Harp", + "bell": "Klok", + "church_bell": "Kerkklok", + "jingle_bell": "Klingelbel", + "bicycle_bell": "Fietsbel", + "tuning_fork": "Stemvork", + "chime": "Bel", + "wind_chime": "Windgong", + "accordion": "Accordeon", + "bagpipes": "Doedelzakken", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Klankschaal", + "rock_music": "Rockmuziek", + "rhythm_and_blues": "Rhythm-and-blues", + "soul_music": "Soulmuziek", + "reggae": "Reggae", + "country": "Countrymuziek", + "bluegrass": "Bluegrass", + "funk": "Funk", + "middle_eastern_music": "Midden-Oosterse muziek", + "jazz": "Jazz", + "cello": "Cello", + "swing_music": "Swingmuziek", + "gong": "gong", + "synthesizer": "Synthesizer", + "punk_rock": "Punkrock", + "wood_block": "Houten klankblok", + "double_bass": "Contrabas", + "beatboxing": "Beatbox", + "orchestra": "Orkest", + "progressive_rock": "Progressieve rock", + "pop_music": "Popmuziek", + "folk_music": "Volksmuziek", + "drum_machine": "Drum", + "pizzicato": "Pizzicato", + "grunge": "Grunge", + "heavy_metal": "Heavy Metal", + "timpani": "Pauken", + "electronic_organ": "Elektronisch orgel", + "trumpet": "Trompet", + "hip_hop_music": "Hip-Hop Muziek", + "hammond_organ": "Hammondorgel", + "drum": "Trommel", + "rimshot": "Rimshot", + "harmonica": "Mondharmonica", + "tambourine": "Tamboerijn", + "psychedelic_rock": "Psychedelische rock", + "vibraphone": "Vibrafoon", + "bowed_string_instrument": "Strijkinstrument", + "violin": "Viool", + "flute": "Fluit", + "saxophone": "Saxofoon", + "scratching": "Krabben", + "rock_and_roll": "Rock 'n Roll", + "disco": "Disco", + "classical_music": "Klassieke muziek", + "opera": "Opera", + "electronic_music": "Elektronische muziek", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum en bas", + "electronic_dance_music": "Elektronische dansmuziek", + "electronica": "Electronica", + "ambient_music": "Ambientmuziek", + "house_music": "Housemuziek", + "trance_music": "Trance Music", + "salsa_music": "Salsamuziek", + "flamenco": "Flamenco", + "blues": "Blues", + "music_of_bollywood": "Music of Bollywood", + "ska": "Ska", + "independent_music": "Onafhankelijke muziek", + "soundtrack_music": "Soundtrack", + "bathtub": "Bad", + "keys_jangling": "Sleutels rinkelen", + "coin": "Munt", + "dial_tone": "Kiestoon", + "busy_signal": "In-gesprektoon", + "buzzer": "Zoemer", + "foghorn": "Misthoorn", + "whistle": "Fluiten", + "steam_whistle": "Stoomfluit", + "ratchet": "Ratel", + "gears": "Tandwielen", + "air_conditioning": "Airconditioning", + "single-lens_reflex_camera": "Spiegelreflexcamera", + "machine_gun": "Geweerschot", + "fusillade": "Schotenwisseling", + "artillery_fire": "Artillerievuur", + "firecracker": "Vuurwerkknaller", + "burst": "Knal", + "boom": "Boem", + "chop": "Hout Hakken", + "splinter": "Splinter", + "chink": "Spleet", + "shatter": "Stukslaan", + "static": "Statisch geluid", + "white_noise": "Witte ruis", + "pink_noise": "Roze ruis", + "television": "Televisie", + "radio": "Radio", + "field_recording": "Veldopname", + "music_for_children": "Muziek voor kinderen", + "vocal_music": "Vocal Music", + "a_capella": "A capella", + "christian_music": "Christelijke Muziek", + "crackle": "Gekraak", + "cupboard_open_or_close": "Kast open of dicht", + "motor_vehicle": "Motorvoertuig", + "police_car": "Politieauto", + "knock": "Klop", + "music_of_asia": "Muziek uit Azië", + "dance_music": "Dansmuziek", + "light_engine": "Lichte motor", + "frying": "Frituren", + "gospel_music": "Gospelmuziek", + "ice_cream_truck": "IJscowagen", + "engine_starting": "Motor starten", + "new-age_music": "New age (muziek)", + "wedding_music": "Bruiloftsmuziek", + "music_of_africa": "Muziek van Afrika", + "thunder": "Donder", + "waterfall": "Waterval", + "skidding": "Slippen", + "truck": "Vrachtwagen", + "ambulance": "Ambulance", + "jet_engine": "Straalmotor", + "lullaby": "Slaapliedje", + "sad_music": "Droevige muziek", + "video_game_music": "Videogamemuziek", + "angry_music": "Boze muziek", + "steam": "Stromend water", + "vehicle": "Voertuig", + "boat": "Boot", + "rowboat": "Roeiboot", + "emergency_vehicle": "Hulpverleningsvoertuig", + "slam": "Slag", + "chopping": "Hakken", + "happy_music": "Vrolijke muziek", + "raindrop": "Regendruppel", + "dental_drill's_drill": "Tandartsboor", + "scissors": "Schaar", + "shuffling_cards": "Kaarten schudden", + "printer": "Printer", + "afrobeat": "Afrobeat", + "traditional_music": "Traditionele muziek", + "gurgling": "Gorgelend", + "train_wheels_squealing": "Piepende treinwielen", + "subway": "Metro", + "bicycle": "Fiets", + "medium_engine": "Middelgrote motor", + "squeak": "Piep", + "dishes": "Borden", + "zipper": "Rits", + "tick-tock": "Ticktack", + "race_car": "Raceauto", + "railroad_car": "Spoorwagon", + "music_of_latin_america": "Muziek uit Latijns-Amerika", + "carnatic_music": "Carnatische muziek", + "helicopter": "Helikopter", + "chainsaw": "Kettingzaag", + "ding-dong": "Ding-Dong", + "sink": "Wasbak", + "wind_noise": "Windgeruis", + "wind": "Wind", + "sailboat": "Zeilboot", + "song": "Liedje", + "toot": "Toeteren", + "bus": "Bus", + "traffic_noise": "Verkeerslawaai", + "train_horn": "Treinhoorn", + "thunderstorm": "Onweer", + "typewriter": "Typemachine", + "background_music": "Achtergrondmuziek", + "car": "Auto", + "ringtone": "Beltoon", + "theme_music": "Themamuziek", + "sliding_door": "Schuifdeur", + "jingle": "Jingle", + "waves": "Golven", + "stream": "Stromend water", + "sewing_machine": "Naaimachine", + "mechanical_fan": "Mechanische ventilator", + "camera": "Camera", + "cap_gun": "Speelgoedpistool", + "tender_music": "Tedere muziek", + "ship": "Schip", + "explosion": "Explosie", + "christmas_music": "Kerstmuziek", + "microwave_oven": "Magnetron", + "toilet_flush": "Toilet doorspoelen", + "exciting_music": "Spannende muziek", + "scary_music": "Enge muziek", + "rustling_leaves": "Ritselende bladeren", + "tire_squeal": "Piepende banden", + "fire_engine": "Brandweerwagen", + "water_tap": "Waterkraan", + "water": "Water", + "rain": "Regen", + "motorcycle": "Motorfiets", + "aircraft_engine": "Vliegtuigmotor", + "rain_on_surface": "Regen op een oppervlakte", + "motorboat": "Motorboot", + "car_passing_by": "Passerende auto", + "reversing_beeps": "Achteruitrijsignalen", + "train": "Trein", + "doorbell": "Deurbel", + "drawer_open_or_close": "Lade open of dicht", + "fire": "Vuur", + "power_windows": "Elektrische ramen", + "train_whistle": "Treinfluitje", + "fixed-wing_aircraft": "Vliegtuig met vaste vleugels", + "engine_knocking": "Motorklopgeluid", + "ocean": "Oceaan", + "rail_transport": "Spoorvervoer", + "aircraft": "Vliegtuigen", + "car_alarm": "Autoalarm", + "idling": "Stationair", + "door": "Deur", + "air_brake": "Luchtrem", + "propeller": "Propeller", + "air_horn": "Luchthoorn", + "skateboard": "Skateboard", + "engine": "Motor", + "accelerating": "Versnellen", + "blender": "Blender", + "gunshot": "Schot", + "lawn_mower": "Grasmaaier", + "heavy_engine": "Zware motor", + "tap": "Tik op", + "hair_dryer": "Föhn", + "cash_register": "Kassa", + "cutlery": "Bestek", + "power_tool": "Elektrisch gereedschap", + "computer_keyboard": "Computertoetsenbord", + "vacuum_cleaner": "Stofzuiger", + "tick": "Teek", + "alarm": "Alarm", + "toothbrush": "Tandenborstel", + "electric_shaver": "Scheerapparaat", + "writing": "Schrijven", + "telephone": "Telefoon", + "jackhammer": "Drilboor", + "alarm_clock": "Wekker", + "civil_defense_siren": "Luchtalarm", + "typing": "Typen", + "pulleys": "Katrollen", + "drill": "Boor", + "telephone_dialing": "Telefoonnummer draaien", + "telephone_bell_ringing": "Rinkelen van de telefoon", + "electric_toothbrush": "Elektrische tandenborstel", + "hammer": "Hamer", + "sanding": "Schuren", + "siren": "Sirene", + "smoke_detector": "Rookmelder", + "fire_alarm": "Brandalarm", + "mechanisms": "Mechanismen", + "filing": "Vijlen", + "clock": "Klok", + "glass": "Glas", + "sawing": "Zagen", + "tools": "Hulpmiddelen", + "wood": "Hout", + "fireworks": "Vuurwerk", + "eruption": "Uitbarsting", + "crack": "Scheur", + "environmental_noise": "Omgevingsgeluid", + "silence": "Stilte", + "sound_effect": "Geluidseffect", + "scream": "Schreeuw", + "sodeling": "Sodeling", + "chird": "Chird", + "change_ringing": "Beltoon wijzigen", + "shofar": "Sjofar", + "liquid": "Vloeistof", + "splash": "Plons", + "slosh": "Klotsen", + "squish": "Pletten", + "drip": "Druppelen", + "pour": "Gieten", + "trickle": "Gerinkel", + "gush": "Stroom", + "fill": "Vullen", + "spray": "Spuiten", + "pump": "Pomp", + "stir": "Roeren", + "boiling": "Koken", + "sonar": "Sonar", + "arrow": "Pijl", + "whoosh": "Woesj", + "thump": "Dreun", + "thunk": "doffe dreun", + "electronic_tuner": "Elektronische tuner", + "effects_unit": "Effecteneenheid", + "chorus_effect": "Kooreffect", + "basketball_bounce": "Basketbal stuiteren", + "bang": "Knal", + "slap": "Klap", + "whack": "Mep", + "smash": "Verpletteren", + "breaking": "Breken", + "bouncing": "Stuiteren", + "whip": "Zweep", + "flap": "Klep", + "scratch": "Kras", + "scrape": "Schrapen", + "rub": "Wrijven", + "roll": "Rollen", + "crushing": "Verpletteren", + "crumpling": "Verpletteren", + "tearing": "Scheuren", + "beep": "Piep", + "ping": "Ping", + "ding": "Ding", + "clang": "Klang", + "squeal": "Piepen", + "creak": "Kraken", + "rustle": "Geritsel", + "whir": "Snorren", + "clatter": "Gekletter", + "sizzle": "Sissen", + "clicking": "Klikken", + "clickety_clack": "Klik-klak", + "rumble": "Gerommel", + "plop": "Plop", + "hum": "Hum", + "zing": "Zing", + "boing": "Boing", + "crunch": "Kraak", + "sine_wave": "Sinusgolf", + "harmonic": "Harmonisch", + "chirp_tone": "Pieptoon", + "pulse": "Puls", + "inside": "Binnen", + "outside": "Buiten", + "reverberation": "Nagalm", + "echo": "Echo", + "noise": "Lawaai", + "mains_hum": "Netstroomgezoe", + "distortion": "Vervorming", + "sidetone": "Zijtoon", + "cacophony": "Kakofonie", + "throbbing": "Bonzend", + "vibration": "Trilling" +} diff --git a/web/public/locales/nl/common.json b/web/public/locales/nl/common.json new file mode 100644 index 0000000..d9630b7 --- /dev/null +++ b/web/public/locales/nl/common.json @@ -0,0 +1,334 @@ +{ + "time": { + "untilForTime": "Totdat {{time}}", + "untilForRestart": "Totdat Frigate herstart.", + "untilRestart": "Tot herstart", + "12hours": "12 uur", + "lastWeek": "Vorige week", + "last7": "Afgelopen 7 dagen", + "last30": "Afgelopen 30 dagen", + "yr": "{{time}} jaar", + "5minutes": "5 minuten", + "10minutes": "10 minuten", + "24hours": "24 uur", + "30minutes": "30 minuten", + "ago": "{{timeAgo}} geleden", + "justNow": "Zojuist", + "today": "Vandaag", + "yesterday": "Gisteren", + "last14": "Afgelopen 14 dagen", + "thisWeek": "Deze week", + "thisMonth": "Deze maand", + "lastMonth": "Vorige maand", + "1hour": "1 uur", + "pm": "pm", + "am": "am", + "year_one": "{{time}} Jaar", + "year_other": "{{time}} Jaren", + "mo": "{{time}} maand", + "month_one": "{{time}} maand", + "month_other": "{{time}} maanden", + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ss", + "24hour": "d MMM HH:mm:ss" + }, + "s": "{{time}}s", + "formattedTimestamp": { + "12hour": "d MMM, HH:mm:ss", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestampOnlyMonthAndDay": "%-d %b", + "d": "{{time}}d", + "day_one": "{{time}} dag", + "day_other": "{{time}} dagen", + "h": "{{time}}u", + "hour_one": "{{time}} uur", + "hour_other": "{{time}} uren", + "m": "{{time}}min", + "formattedTimestampWithYear": { + "12hour": "%-d %b %Y, %H:%M", + "24hour": "%-d %b %Y, %H:%M" + }, + "formattedTimestampExcludeSeconds": { + "24hour": "%-d %b, %H:%M", + "12hour": "%-d %b, %H:%M" + }, + "minute_one": "{{time}} minuut", + "minute_other": "{{time}} minuten", + "second_one": "{{time}} seconde", + "second_other": "{{time}} seconden", + "formattedTimestampHourMinute": { + "24hour": "HH:mm", + "12hour": "h:mm aaa" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, HH:mm", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-HH-mm-ss", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, HH:mm", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM yyyy", + "24hour": "d MMM yyyy" + }, + "inProgress": "Wordt uitgevoerd", + "invalidStartTime": "Ongeldige starttijd", + "invalidEndTime": "Ongeldige eindtijd", + "never": "Nooit" + }, + "button": { + "enabled": "Ingeschakeld", + "back": "Terug", + "apply": "Toepassen", + "reset": "Opnieuw instellen", + "enable": "Inschakelen", + "disabled": "Uitgeschakeld", + "cancel": "Annuleren", + "close": "Sluiten", + "copy": "Kopieer", + "done": "Klaar", + "saving": "Opslaan…", + "disable": "Uitschakelen", + "save": "Opslaan", + "history": "Geschiedenis", + "fullscreen": "Volledig scherm", + "pictureInPicture": "Pop-up venster", + "twoWayTalk": "Tweerichtingsgesprek", + "cameraAudio": "Camera geluid", + "on": "aan", + "copyCoordinates": "Coördinaten kopiëren", + "delete": "Verwijder", + "yes": "Ja", + "no": "Nee", + "suspended": "Opgeschort", + "unsuspended": "Heractiveren", + "export": "Exporteren", + "exitFullscreen": "Verlaat volledig scherm", + "play": "Speel", + "off": "uit", + "info": "Info", + "edit": "Bewerken", + "download": "Download", + "unselect": "Deselecteren", + "next": "Volgende", + "deleteNow": "Nu verwijderen", + "continue": "Doorgaan", + "add": "Toevoegen", + "undo": "Ongedaan maken", + "copiedToClipboard": "Gekopieerd naar klembord", + "applying": "Verwerken…", + "modified": "Gewijzigd", + "overridden": "Overschreven", + "resetToGlobal": "Reset naar Globaal", + "resetToDefault": "Terugzetten naar Standaard", + "saveAll": "Alles Opslaan", + "savingAll": "Alles aan het opslaan…", + "undoAll": "Alles ongedaan maken", + "retry": "Opnieuw proberen" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/u" + }, + "length": { + "feet": "voet", + "meters": "meter" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/uur", + "mbph": "MB/uur", + "gbph": "GB/uur" + } + }, + "label": { + "back": "Ga terug", + "hide": "Verberg {{item}}", + "show": "Toon {{item}}", + "ID": "ID", + "none": "Geen", + "all": "Alle", + "other": "Overige" + }, + "menu": { + "system": "Systeem", + "systemMetrics": "Systeemstatistieken", + "settings": "Instellingen", + "configuration": "Configuratie", + "systemLogs": "Systeem logboeken", + "configurationEditor": "Configuratie bewerker", + "languages": "Talen", + "language": { + "en": "English (Engels)", + "zhCN": "简体中文 (Vereenvoudigd Chinees)", + "withSystem": { + "label": "Gebruik de systeeminstellingen voor de taal" + }, + "ar": "العربية (Arabisch)", + "pt": "Português (Portugees)", + "ru": "Русский (Russisch)", + "de": "Deutsch (Duits)", + "tr": "Türkçe (Turks)", + "it": "Italiano (Italiaans)", + "nl": "Nederlands (Nederlands)", + "sv": "Svenska (Zweeds)", + "cs": "Čeština (Tsjechisch)", + "fa": "فارسی (Perzisch)", + "pl": "Polski (Pools)", + "he": "עברית (Hebreeuws)", + "el": "Ελληνικά (Grieks)", + "ro": "Română (Roemeense)", + "hu": "Magyar (Hongaars)", + "fi": "Suomi (Fins)", + "da": "Dansk (Deens)", + "sk": "Slovenčina (Slowaaks)", + "ko": "한국어 (Koreaans)", + "nb": "Norsk Bokmål (Noors Bokmål)", + "fr": "Français (Frans)", + "uk": "Українська (Oekraïens)", + "es": "Español (Spaans)", + "vi": "Tiếng Việt (Vietnamees)", + "hi": "हिन्दी (Hindi)", + "ja": "日本語 (Japans)", + "yue": "粵語 (Kantonees)", + "th": "ไทย (Thais)", + "ca": "Català (Catalaans)", + "ptBR": "Português brasileiro (Braziliaans Portugees)", + "sr": "Српски (Servisch)", + "sl": "Slovenščina (Sloveens)", + "lt": "Lietuvių (Litouws)", + "bg": "Български (Bulgaars)", + "gl": "Galego (Galicisch)", + "id": "Bahasa Indonesia (Indonesisch)", + "ur": "اردو (Urdu)", + "hr": "Hrvatski (Kroatisch)", + "zhHant": "繁體中文 (Traditioneel Chinees)", + "bs": "Bosanksi (Bosnisch)" + }, + "darkMode": { + "label": "Donkere modus", + "light": "Licht", + "dark": "Donker", + "withSystem": { + "label": "Gebruik de systeeminstellingen voor de lichte of donkere modus" + } + }, + "appearance": "Opmaak", + "theme": { + "blue": "Blauw", + "contrast": "Hoog contrast", + "label": "Thema", + "green": "Groen", + "nord": "Nord", + "red": "Rood", + "default": "Standaard", + "highcontrast": "Hoog contrast" + }, + "withSystem": "Systeem", + "help": "Help", + "live": { + "title": "Live", + "allCameras": "Alle Camera's", + "cameras": { + "title": "Camera's", + "count_one": "{{count}} Camera", + "count_other": "{{count}} Camera's" + } + }, + "restart": "Herstart Frigate", + "documentation": { + "title": "Documentatie", + "label": "Frigate documentatie" + }, + "review": "Beoordelen", + "explore": "Verkennen", + "export": "Exporteren", + "uiPlayground": "Testgebied voor gebruikersinterface", + "faceLibrary": "Gezichtenbibliotheek", + "user": { + "title": "Gebruik", + "current": "Huidige gebruiker: {{user}}", + "logout": "Uitloggen", + "setPassword": "Wachtwoord instellen", + "account": "Account", + "anonymous": "anoniem" + }, + "classification": "Classificatie", + "profiles": "Profielen", + "actions": "Acties", + "features": "Functies", + "chat": "Chat" + }, + "toast": { + "copyUrlToClipboard": "URL naar klembord gekopieerd.", + "save": { + "title": "Opslaan", + "error": { + "title": "Opslaan van configuratiewijzigingen mislukt: {{errorMessage}}", + "noMessage": "Het opslaan van configuratiewijzigingen is mislukt" + }, + "success": "De configuratie aanpassingen zijn succesvol opgeslagen." + } + }, + "role": { + "title": "Rol", + "admin": "Beheerder", + "viewer": "Kijker", + "desc": "Beheerders hebben volledige toegang tot alle functies in de Frigate-interface. Kijkers kunnen alleen camera’s bekijken, items beoordelen en historische beelden terugkijken." + }, + "pagination": { + "previous": { + "title": "Vorig", + "label": "Ga naar de vorige pagina" + }, + "more": "Meer pagina's", + "label": "Paginering", + "next": { + "title": "Volgende", + "label": "Ga naar volgende pagina" + } + }, + "accessDenied": { + "documentTitle": "Toegang geweigerd - Frigate", + "desc": "Je hebt geen toestemming om deze pagina te bekijken.", + "title": "Toegang geweigerd" + }, + "notFound": { + "desc": "Pagina niet gevonden", + "title": "404", + "documentTitle": "Niet gevonden - Frigate" + }, + "selectItem": "Selecteer {{item}}", + "readTheDocumentation": "Lees de documentatie", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} en {{1}}", + "many": "{{items}}, en {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Optioneel", + "internalID": "De interne ID die Frigate gebruikt in de configuratie en database" + }, + "credentialField": { + "savedPlaceholder": "Opgeslagen - leeg laten om huidige te behouden" + }, + "no_items": "Geen items", + "validation_errors": "Validatie fouten" +} diff --git a/web/public/locales/nl/components/auth.json b/web/public/locales/nl/components/auth.json new file mode 100644 index 0000000..14fb57a --- /dev/null +++ b/web/public/locales/nl/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Wachtwoord", + "login": "Inloggen", + "errors": { + "rateLimit": "Limiet overschreden. Probeer het later opnieuw.", + "loginFailed": "Inloggen mislukt", + "usernameRequired": "Gebruikersnaam is vereist", + "passwordRequired": "Wachtwoord is vereist", + "unknownError": "Onbekende fout. Bekijk de logs.", + "webUnknownError": "Onbekende fout. Controleer consolelogboeken." + }, + "user": "Gebruikersnaam", + "firstTimeLogin": "Probeer je voor het eerst in te loggen? De inloggegevens staan vermeld in de Frigate-logs." + } +} diff --git a/web/public/locales/nl/components/camera.json b/web/public/locales/nl/components/camera.json new file mode 100644 index 0000000..54de610 --- /dev/null +++ b/web/public/locales/nl/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "Cameragroepen", + "add": "Cameragroep toevoegen", + "edit": "Cameragroep bewerken", + "delete": { + "label": "Cameragroep verwijderen", + "confirm": { + "title": "Bevestig Verwijderen", + "desc": "Weet u zeker dat u de cameragroep {{name}} wilt verwijderen?" + } + }, + "name": { + "label": "Naam", + "placeholder": "Voer een naam in…", + "errorMessage": { + "exists": "De cameragroepnaam bestaat al.", + "invalid": "Ongeldige cameragroepnaam.", + "nameMustNotPeriod": "De naam van de cameragroep mag geen punt bevatten.", + "mustLeastCharacters": "De naam van de cameragroep moet minimaal 2 tekens lang zijn." + } + }, + "cameras": { + "desc": "Selecteer camera's voor deze groep.", + "label": "Camera's" + }, + "success": "Cameragroep ({{name}}) is opgeslagen.", + "camera": { + "setting": { + "audioIsAvailable": "Audio is beschikbaar voor deze stream", + "audioIsUnavailable": "Audio is niet beschikbaar voor deze stream", + "audio": { + "tips": { + "document": "Lees de documentatie ", + "title": "Audio moet worden uitgevoerd vanaf je camera en geconfigureerd in go2rtc voor deze stream." + } + }, + "streamMethod": { + "method": { + "smartStreaming": { + "label": "Slim streamen (aanbevolen)", + "desc": "Slim streamen werkt het camerabeeld één keer per minuut bij wanneer er geen detecteerbare activiteit is, om bandbreedte en systeembronnen te besparen. Zodra er activiteit wordt gedetecteerd, schakelt het beeld automatisch over naar een livestream." + }, + "continuousStreaming": { + "label": "Continue streaming", + "desc": { + "title": "Het camerabeeld is altijd een live stream wanneer het zichtbaar is op het dashboard, zelfs als er geen activiteit wordt gedetecteerd.", + "warning": "Let op: continu streamen kan leiden tot hoog bandbreedtegebruik en prestatieproblemen." + } + }, + "noStreaming": { + "label": "Geen streaming", + "desc": "Camerabeelden worden slechts één keer per minuut bijgewerkt en er vindt geen livestreaming plaats." + } + }, + "label": "Streamingmethode", + "placeholder": "Kies een streamingmethode" + }, + "compatibilityMode": { + "desc": "Schakel deze optie alleen in als de live stream van je camera kleurvervormingen toont en een diagonale lijn aan de rechterkant van het beeld heeft.", + "label": "Compatibiliteitsmodus" + }, + "desc": "Wijzig de live streaming-opties voor het dashboard van deze cameragroep. Deze instellingen zijn specifiek voor het apparaat en de browser.", + "label": "Camera streaming-instellingen", + "title": "{{cameraName}} Streaming-instellingen", + "stream": "Stream", + "placeholder": "Kies een stream" + }, + "birdseye": "Birdseye" + }, + "icon": "Icon", + "showAll": "Toon alle camera groepen", + "showLess": "Minder laten zien", + "editGroups": "Bewerken Camera Groepen" + }, + "debug": { + "options": { + "label": "Instellingen", + "title": "Opties", + "showOptions": "Opties weergeven", + "hideOptions": "Opties verbergen" + }, + "mask": "Masker", + "motion": "Beweging", + "zones": "Zones", + "boundingBox": "Objectkader", + "timestamp": "Tijdstempel", + "regions": "Regio's", + "paths": "Paden" + } +} diff --git a/web/public/locales/nl/components/dialog.json b/web/public/locales/nl/components/dialog.json new file mode 100644 index 0000000..a69f382 --- /dev/null +++ b/web/public/locales/nl/components/dialog.json @@ -0,0 +1,189 @@ +{ + "restart": { + "title": "Weet je zeker dat je Frigate opnieuw wilt opstarten?", + "button": "Herstart", + "restarting": { + "title": "Frigate wordt opnieuw gestart", + "button": "Forceer herladen nu", + "content": "Deze pagina zal herladen in {{countdown}} seconden." + }, + "description": "Dit zal Frigate kort stoppen terwijl het opnieuw opstart." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Verzenden naar Frigate+", + "desc": "Objecten op locaties die je wilt vermijden, zijn geen vals-positieven. Als je ze als vals-positieven indient, brengt dit het model in verwarring." + }, + "review": { + "true": { + "true_one": "Dit is een {{label}}", + "true_other": "Dit zijn {{label}}", + "label": "Bevestig dit label voor Frigate Plus" + }, + "false": { + "false_one": "Dit is geen {{label}}", + "false_other": "Dit zijn geen {{label}}", + "label": "Bevestig dit label niet voor Frigate Plus" + }, + "state": { + "submitted": "Ingediend" + }, + "question": { + "ask_an": "Is dit object een {{label}}?", + "label": "Bevestig dit label voor Frigate Plus", + "ask_a": "Is dit object een {{label}}?", + "ask_full": "Is dit object een {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Bekijk in Geschiedenis" + } + }, + "export": { + "time": { + "fromTimeline": "Selecteer uit Tijdlijn", + "end": { + "label": "Selecteer eindtijd", + "title": "Eindtijd" + }, + "lastHour_one": "Afgelopen uur", + "lastHour_other": "Afgelopen {{count}} uren", + "custom": "Aangepast", + "start": { + "title": "Starttijd", + "label": "Selecteer starttijd" + } + }, + "name": { + "placeholder": "Geef de export een naam" + }, + "select": "Selecteer", + "toast": { + "error": { + "failed": "Exporteren is mislukt: {{error}}", + "noVaildTimeSelected": "Geen geldig tijdsbereik geselecteerd", + "endTimeMustAfterStartTime": "Eindtijd moet na starttijd zijn" + }, + "success": "Export is succesvol gestart. Bekijk het bestand op de exportpagina.", + "view": "Weergeven", + "queued": "Export staat in de wachtrij. Bekijk voortgang op de exports pagina." + }, + "fromTimeline": { + "saveExport": "Export opslaan", + "previewExport": "Export vooraf bekijken", + "useThisRange": "Gebruik dit bereik", + "queueingExport": "Export in wachtrij zetten..." + }, + "export": "Exporteren", + "selectOrExport": "Selecteren of exporteren", + "case": { + "label": "Dossier", + "placeholder": "Selecteer een dossier", + "newCaseOption": "Maak nieuwe case", + "newCaseNamePlaceholder": "Nieuwe case naam", + "newCaseDescriptionPlaceholder": "Case beschrijving", + "nonAdminHelp": "Een nieuwe case wordt gemaakt voor deze exports." + }, + "queueing": "Export in wachtrij zetten...", + "tabs": { + "export": "Enkele Camera", + "multiCamera": "Multi-Camera" + }, + "multiCamera": { + "timeRange": "Tijdspanne", + "selectFromTimeline": "Selecteer van tijdslijn", + "cameraSelection": "Camera's", + "cameraSelectionHelp": "Camera’s met getrackte objecten in deze tijdspanne zijn vooraf geselecteerd", + "checkingActivity": "Camera-activiteit controleren...", + "noCameras": "Geen camera's beschikbaar", + "detectionCount_one": "1 gevolgd object", + "detectionCount_other": "{{count}} gevolgde objecten", + "nameLabel": "Exportnaam", + "namePlaceholder": "Optionele basisnaam voor deze exporten", + "queueingButton": "Exporten in wachtrij plaatsen...", + "exportButton_one": "Export 1 Camera", + "exportButton_other": "{{count}} camera's exporteren" + }, + "multi": { + "title_one": "Review 1 exporteren", + "title_other": "{{count}} reviews exporteren", + "description": "Exporteer alle geselecteerde reviews. Alle exports worden samengevoegd in één case.", + "descriptionNoCase": "Exporteer elke geselecteerde review.", + "toast": { + "startedNoCase_one": "1 export gestart.", + "startedNoCase_other": "{{count}} Exports gestart.", + "partial": "{{successful}} van {{total}} exports gestart. Mislukte items: {{failedItems}}", + "failed": "Het starten van {{total}} exports is mislukt. Mislukt: {{failedItems}}" + }, + "exportingButton": "Exporteren...", + "exportButton_one": "Exporteer 1 revisie", + "exportButton_other": "Exporteer {{count}} revisies", + "caseNamePlaceholder": "Inspecteer export - {{date}}" + } + }, + "streaming": { + "label": "Stream", + "restreaming": { + "desc": { + "title": "Stel go2rtc in voor extra liveweergaveopties en audio voor deze camera.", + "readTheDocumentation": "Lees de documentatie" + }, + "disabled": "Herstreamen is niet ingeschakeld voor deze camera." + }, + "showStats": { + "label": "Streamstatistieken tonen", + "desc": "Schakel deze optie in om streamstatistieken als overlay op de camerafeed weer te geven." + }, + "debugView": "Debugweergave" + }, + "search": { + "saveSearch": { + "label": "Zoekopdracht opslaan", + "desc": "Geef een naam op voor deze opgeslagen zoekopdracht.", + "success": "Zoekopdracht ({{searchName}}) is opgeslagen.", + "button": { + "save": { + "label": "Bewaar deze zoekopdracht" + } + }, + "overwrite": "{{searchName}} bestaat al. Opslaan overschrijft de bestaande waarde.", + "placeholder": "Voer een naam in voor uw zoekopdracht" + } + }, + "recording": { + "button": { + "deleteNow": "Verwijder nu", + "export": "Exporteren", + "markAsReviewed": "Markeren als beoordeeld", + "markAsUnreviewed": "Markeren als niet beoordeeld" + }, + "confirmDelete": { + "desc": { + "selected": "Weet u zeker dat u alle opgenomen videobeelden wilt verwijderen die aan dit beoordelingsitem zijn gekoppeld?

    Houd de Shift-toets ingedrukt om dit dialoogvenster in de toekomst over te slaan." + }, + "title": "Bevestig Verwijderen", + "toast": { + "error": "Verwijderen mislukt: {{error}}", + "success": "De videobeelden die aan de geselecteerde beoordelingsitems zijn gekoppeld, zijn succesvol verwijderd." + } + }, + "shareTimestamp": { + "label": "Tijdstempel delen", + "title": "Tijdstempel delen", + "description": "Deel een URL met tijdstempel van de huidige positie in de afspeler of kies een aangepaste tijdstempel. Let op: dit is geen openbare deel-URL en is alleen toegankelijk voor gebruikers die toegang hebben tot Frigate en deze camera.", + "custom": "Aangepaste tijdstempel", + "button": "Deel tijdstempel URL", + "shareTitle": "Frigate Beoordelingstijdstempel: {{camera}}" + } + }, + "imagePicker": { + "selectImage": "Kies miniatuur van gevolgd object", + "noImages": "Geen miniaturen gevonden voor deze camera", + "search": { + "placeholder": "Zoeken op label of sub label..." + }, + "unknownLabel": "Opgeslagen triggerafbeelding" + } +} diff --git a/web/public/locales/nl/components/filter.json b/web/public/locales/nl/components/filter.json new file mode 100644 index 0000000..e910acd --- /dev/null +++ b/web/public/locales/nl/components/filter.json @@ -0,0 +1,141 @@ +{ + "labels": { + "count": "{{count}} Labels", + "all": { + "short": "Labels", + "title": "Alle labels" + }, + "label": "Labels", + "count_one": "{{count}} Label", + "count_other": "{{count}} Labels" + }, + "zones": { + "label": "Zones", + "all": { + "title": "Alle zones", + "short": "Zones" + } + }, + "dates": { + "all": { + "title": "Alle datums", + "short": "Datums" + }, + "selectPreset": "Selecteer een voorinstelling…" + }, + "features": { + "hasVideoClip": "Heeft een videoclip", + "label": "Functies", + "hasSnapshot": "Heeft een snapshot", + "submittedToFrigatePlus": { + "label": "Ingediend bij Frigate+", + "tips": "Je moet eerst filteren op gevolgde objecten met een snapshot.

    Gevolgde objecten zonder snapshot kunnen niet worden verzonden naar Frigate+." + } + }, + "review": { + "showReviewed": "Toon beoordeelde items" + }, + "motion": { + "showMotionOnly": "Alleen bewegingen weergeven" + }, + "explore": { + "settings": { + "title": "Instellingen", + "defaultView": { + "title": "Standaardweergave", + "unfilteredGrid": "Ongefilterd overzicht", + "summary": "Samenvatting", + "desc": "Wanneer er geen filters zijn geselecteerd, wordt er een samenvatting van de meest recent gevolgde objecten per label weergegeven, of wordt er een ongefilterd overzicht weergegeven." + }, + "gridColumns": { + "title": "Overzichtskolommen", + "desc": "Selecteer het aantal kolommen in het overzicht." + }, + "searchSource": { + "options": { + "description": "Beschrijving", + "thumbnailImage": "Thumbnail afbeelding" + }, + "desc": "Kies of u wilt zoeken in de thumbnails of beschrijvingen van de objecten die u volgt.", + "label": "Zoekbron" + } + }, + "date": { + "selectDateBy": { + "label": "Selecteer een datum om op te filteren" + } + } + }, + "zoneMask": { + "filterBy": "Filteren op zonemasker" + }, + "recognizedLicensePlates": { + "loadFailed": "Het laden van herkende kentekenplaten is mislukt.", + "placeholder": "Type om kentekens te zoeken…", + "title": "Herkende kentekenplaten", + "noLicensePlatesFound": "Geen kentekenplaten gevonden.", + "selectPlatesFromList": "Selecteer een of meer kentekens uit de lijst.", + "loading": "Herkende kentekenplaten laden…", + "selectAll": "Selecteer alles", + "clearAll": "Alles wissen" + }, + "score": "Score", + "sort": { + "scoreAsc": "Objectscore (oplopend)", + "dateAsc": "Datum (oplopend)", + "speedAsc": "Geschatte snelheid (oplopend)", + "label": "Sorteer", + "relevance": "Relevantie", + "dateDesc": "Datum (aflopend)", + "scoreDesc": "Objectscore (aflopend)", + "speedDesc": "Geschatte snelheid (aflopend)" + }, + "cameras": { + "all": { + "title": "Alle camera's", + "short": "Camera's" + }, + "label": "Camerafilter" + }, + "subLabels": { + "label": "Sublabels", + "all": "Alle sublabels" + }, + "logSettings": { + "loading": { + "title": "Bezig met laden", + "desc": "Wanneer u het logvenster naar beneden scrolt, worden nieuwe logs automatisch weergegeven terwijl ze worden toegevoegd." + }, + "disableLogStreaming": "Logstreaming uitschakelen", + "allLogs": "Alle logs", + "label": "Filterlogniveau", + "filterBySeverity": "Filter logs op ernst" + }, + "filter": "Filter", + "timeRange": "Tijdsbereik", + "trackedObjectDelete": { + "toast": { + "success": "Gevolgde objecten succesvol verwijderd.", + "error": "Het verwijderen van gevolgde objecten is mislukt: {{errorMessage}}" + }, + "title": "Bevestig Verwijderen", + "desc": "Het verwijderen van deze {{objectLength}} gevolgde objecten verwijdert de snapshot, eventuele opgeslagen embeddings en bijbehorende levenscyclusgegevens van het object. Opgenomen videobeelden van deze objecten in de geschiedenisweergave worden NIET verwijderd.

    Weet je zeker dat je wilt doorgaan?

    Houd de Shift-toets ingedrukt om deze melding in de toekomst over te slaan." + }, + "reset": { + "label": "Filters resetten naar standaardwaarden" + }, + "more": "Meer filters", + "estimatedSpeed": "Geschatte snelheid ({{unit}})", + "classes": { + "label": "Klassen", + "all": { + "title": "Alle klassen" + }, + "count_one": "{{count}} klasse", + "count_other": "{{count}} Klassen" + }, + "attributes": { + "label": "Classificatie-kenmerken", + "all": "Alle attributen" + } +} diff --git a/web/public/locales/nl/components/icons.json b/web/public/locales/nl/components/icons.json new file mode 100644 index 0000000..af65664 --- /dev/null +++ b/web/public/locales/nl/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecteer een pictogram", + "search": { + "placeholder": "Zoek naar een pictogram…" + } + } +} diff --git a/web/public/locales/nl/components/input.json b/web/public/locales/nl/components/input.json new file mode 100644 index 0000000..fa5707a --- /dev/null +++ b/web/public/locales/nl/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Video downloaden", + "toast": { + "success": "Het downloaden van uw beoordelingsvideo is gestart." + } + } + } +} diff --git a/web/public/locales/nl/components/player.json b/web/public/locales/nl/components/player.json new file mode 100644 index 0000000..511c32e --- /dev/null +++ b/web/public/locales/nl/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "Geen opnames gevonden voor deze tijd", + "stats": { + "latency": { + "value": "{{seconds}} seconden", + "short": { + "title": "Latentie", + "value": "{{seconds}} s" + }, + "title": "Latentie:" + }, + "totalFrames": "Totaal aantal frames:", + "droppedFrames": { + "title": "Overgeslagen frames:", + "short": { + "value": "{{droppedFrames}} frames", + "title": "Overgeslagen" + } + }, + "droppedFrameRate": "Frequentie van overgeslagen frames:", + "bandwidth": { + "short": "Bandbreedte", + "title": "Bandbreedte:" + }, + "streamType": { + "short": "Type", + "title": "Stream Type:" + }, + "decodedFrames": "Gedecodeerde frames:" + }, + "submitFrigatePlus": { + "title": "Dit frame indienen bij Frigate+?", + "submit": "Indienen", + "previewError": "Het was niet mogelijk om de snapshot preview te laden. De opname is mogelijk niet beschikbaar op dit moment." + }, + "streamOffline": { + "title": "Stream is Offline", + "desc": "Er zijn geen frames ontvangen in de detect-stream van {{cameraName}}, controleer de error logs" + }, + "noPreviewFound": "Geen voorbeeld gevonden", + "noPreviewFoundFor": "Geen voorbeeld gevonden voor {{cameraName}}", + "livePlayerRequiredIOSVersion": "Voor dit type livestream is iOS 17.1 of hoger vereist.", + "cameraDisabled": "Camera is uitgeschakeld", + "toast": { + "success": { + "submittedFrigatePlus": "Frame succesvol ingediend bij Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Het is niet gelukt om een frame naar Frigate+ te sturen" + } + }, + "cameraOff": "De camera staat uit" +} diff --git a/web/public/locales/nl/config/cameras.json b/web/public/locales/nl/config/cameras.json new file mode 100644 index 0000000..772bd75 --- /dev/null +++ b/web/public/locales/nl/config/cameras.json @@ -0,0 +1,822 @@ +{ + "label": "CameraConfiguratie", + "name": { + "label": "Camera naam", + "description": "Camera naam is vereist" + }, + "friendly_name": { + "description": "Camera naam te gebruiken in de Frigate UI", + "label": "Eenvoudige naam" + }, + "enabled": { + "label": "Geactiveerd", + "description": "Geactiveerd" + }, + "audio": { + "label": "Audio events", + "description": "Instellingen voor audio-gebaseerde detectie voor deze camera.", + "enabled": { + "label": "Geluiddetectie inschakelen", + "description": "Schakel de detectie van audio-events voor deze camera in of uit." + }, + "max_not_heard": { + "label": "Einde time-out", + "description": "Aantal seconden zonder het geconfigureerde audiotype voordat de audio-event wordt beëindigd." + }, + "min_volume": { + "label": "Minimumvolume", + "description": "Minimale RMS-volumedrempel die nodig is om audiodetectie te starten; hoe lager de waarde, hoe gevoeliger de detectie (bijvoorbeeld, 200 hoog, 500 gemiddeld, 1000 laag)." + }, + "listen": { + "label": "Luistercategorieën", + "description": "Lijst van luistercategorie gebeurtenissen voor detectie (zoals: blaffen, band_alarm, schreeuw, praten, roepen)." + }, + "filters": { + "label": "Geluidsfilters", + "description": "Instellingen per audiotype, waaronder betrouwbaarheidsdrempels, ter vermindering van foutieve detecties.", + "threshold": { + "label": "Minimale audiobetrouwbaarheid", + "description": "Minimale betrouwbaarheidsdrempel voor de audiogebeurtenis om te worden geteld." + } + }, + "enabled_in_config": { + "label": "Originele audio-instelling", + "description": "Geeft aan of audiodetectie oorspronkelijk was geactiveerd in het statische configuratiebestand." + }, + "num_threads": { + "label": "Detectiethreads", + "description": "Aantal threads voor audiodetectieverwerking." + } + }, + "audio_transcription": { + "label": "Audiotranscriptie", + "description": "Instellingen voor live en spraakgestuurde audiotranscriptie voor gebeurtenissen en live ondertitels.", + "enabled": { + "label": "Spraaktranscriptie inschakelen", + "description": "Schakel transcriptie van handmatig getriggerde audiogebeurtenissen in of uit." + }, + "enabled_in_config": { + "label": "Originele transcriptiestatus" + }, + "live_enabled": { + "label": "Live transcriptie", + "description": "Live streaming‑transcriptie van audio inschakelen tijdens ontvangst." + } + }, + "birdseye": { + "label": "Birdseye-overzicht", + "description": "Instellingen voor de overzichtsweergave die meerdere camerafeeds combineert tot één lay‑out.", + "enabled": { + "label": "Birdseye-overzicht inschakelen", + "description": "De overzichtsweergavefunctie in- of uitschakelen." + }, + "mode": { + "label": "Weergavemodus", + "description": "Modus voor het opnemen van camera’s in overzichtsweergave: ‘objecten’, ‘beweging’ of ‘continu’." + }, + "order": { + "label": "Positie", + "description": "Numerieke positie die de volgorde van de camera in de overzichtsweergave lay-out bepaalt." + } + }, + "detect": { + "label": "Objectdetectie", + "description": "Instellingen voor de detectierol om objecten te detecteren en trackers te starten.", + "enabled": { + "label": "Detectie inschakelen", + "description": "Objectdetectie voor deze camera in- of uitschakelen. Detectie moet zijn ingeschakeld om objecttracking te laten werken." + }, + "height": { + "label": "Detectiehoogte", + "description": "De hoogte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken." + }, + "width": { + "label": "Detectiebreedte", + "description": "De breedte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken." + }, + "fps": { + "label": "Detectie‑FPS", + "description": "Gewenst aantal frames per seconde waarop detectie wordt uitgevoerd; lagere waarden verlagen het CPU‑gebruik (aanbevolen waarde is 5, stel alleen hoger in — maximaal 10 — bij het volgen van extreem snel bewegende objecten)." + }, + "min_initialized": { + "label": "Minimale initialisatieframes", + "description": "Aantal opeenvolgende detectieresultaten dat vereist is voordat een gevolgd object wordt aangemaakt. Verhoog deze waarde om valse initialisaties te verminderen. De standaardwaarde is FPS gedeeld door 2." + }, + "max_disappeared": { + "label": "Maximaal aantal verdwenen frames", + "description": "Aantal frames zonder detectie voordat een gevolgd object als verdwenen wordt beschouwd." + }, + "stationary": { + "label": "Instellingen voor stilstaande objecten", + "description": "Instellingen voor het detecteren en beheren van objecten die gedurende een bepaalde tijd stil blijven staan.", + "interval": { + "label": "Interval voor stilstaande objecten", + "description": "Frequentie (in frames) waarmee detectie wordt gecontroleerd om stilstaande objecten te bevestigen." + }, + "threshold": { + "label": "Drempel voor stilstaande objecten", + "description": "Het aantal frames waarin geen positieverandering wordt gedetecteerd voordat een object als stilstaand wordt beschouwd." + }, + "max_frames": { + "label": "Maximaal aantal frames", + "description": "Stelt een limiet aan de duur van tracking van stilstaande objecten.", + "default": { + "label": "Standaard maximaal aantal frames", + "description": "Standaardlimiet voor het aantal frames dat een stilstaand object wordt gevolgd voordat wordt gestopt." + }, + "objects": { + "label": "Maximaal aantal frames per object", + "description": "Maximum aantal frames per object bij het volgen van stilstaande objecten." + } + }, + "classifier": { + "label": "Visuele classifier inschakelen", + "description": "Gebruik een visuele classifier om echt stilstaande objecten te detecteren, zelfs wanneer detectiekaders licht verschuiven." + } + }, + "annotation_offset": { + "label": "Annotatie-offset", + "description": "Milliseconden om detectieannotaties te verschuiven voor betere uitlijning van tijdlijn-detectiekaders met opnames; kan positief of negatief zijn." + } + }, + "profiles": { + "label": "Profielen" + }, + "zones": { + "label": "Zones", + "description": "Met zones kun je een specifiek deel van het frame definiëren, zodat je kunt bepalen of een object zich binnen dat specifieke gebied bevindt.", + "friendly_name": { + "label": "Zone naam", + "description": "Een gebruiksvriendelijke naam voor de zone, die wordt weergegeven in de Frigate-gebruikersinterface. Als deze niet is ingesteld, wordt een opgemaakte versie van de zonenaam gebruikt." + }, + "enabled": { + "label": "Ingeschakeld", + "description": "Schakel deze zone in of uit. Uitgeschakelde zones worden tijdens de uitvoering genegeerd." + }, + "filters": { + "label": "Zone filters", + "description": "Filters die op objecten binnen deze zone moeten worden toegepast. Worden gebruikt om het aantal valse positieven te verminderen of om te beperken welke objecten als aanwezig in de zone worden beschouwd.", + "min_area": { + "label": "Minimale oppervlakte van het object" + } + } + }, + "mqtt": { + "label": "MQTT" + }, + "notifications": { + "label": "Meldingen", + "enabled": { + "label": "Meldingen inschakelen" + }, + "email": { + "label": "Melding email", + "description": "E-mailadres voor pushmeldingen of vereist door bepaalde meldingsproviders." + }, + "cooldown": { + "label": "Wachttijd", + "description": "Wachttijd (seconden) tussen meldingen om spammen te voorkomen." + }, + "enabled_in_config": { + "label": "Originele meldingsstatus", + "description": "Geeft aan of meldingen waren ingeschakeld in de originele statische configuratie." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg-instellingen inclusief binaire pad, argumenten, hardwareversnellingsopties en uitvoerargumenten per rol.", + "path": { + "label": "FFmpeg-pad", + "description": "Pad naar het te gebruiken FFmpeg-binaire bestand of een versie-alias (\"5.0\" of \"7.0\")." + }, + "global_args": { + "label": "FFmpeg globale argumenten", + "description": "Globale argumenten voor FFmpeg-processen." + }, + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten", + "description": "Hardwareversnellingsargumenten voor FFmpeg. Provider-specifieke presets worden aanbevolen." + }, + "input_args": { + "label": "Invoerargumenten", + "description": "Invoerargumenten voor FFmpeg-invoerstromen." + }, + "output_args": { + "label": "Uitvoerargumenten", + "description": "Standaard uitvoerargumenten voor verschillende FFmpeg-rollen zoals detectie en opname.", + "detect": { + "label": "Uitvoerargumenten voor detectie", + "description": "Standaard uitvoerargumenten voor streams met detectierol." + }, + "record": { + "label": "Uitvoerargumenten voor opname", + "description": "Standaard uitvoerargumenten voor streams met opnamerol." + } + }, + "retry_interval": { + "label": "FFmpeg-herverbindingstijd", + "description": "Seconden wachten voor een herverbindingspoging na een mislukte camerastream. Standaard is 10." + }, + "apple_compatibility": { + "label": "Apple-compatibiliteit", + "description": "HEVC-tagging inschakelen voor betere Apple-spelercompatibiliteit bij het opnemen van H.265." + }, + "gpu": { + "label": "GPU-index", + "description": "Standaard GPU-index voor hardwareversnelling indien beschikbaar." + }, + "inputs": { + "label": "Camera-invoer", + "description": "Lijst van invoerstream-definities (paden en rollen) voor deze camera.", + "path": { + "label": "Invoerpad", + "description": "URL of pad van de camera-invoerstroom." + }, + "roles": { + "label": "Invoerrollen", + "description": "Rollen voor deze invoerstroom." + }, + "global_args": { + "label": "FFmpeg globale argumenten", + "description": "FFmpeg globale argumenten voor deze invoerstroom." + }, + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten", + "description": "Hardwareversnellingsargumenten voor deze invoerstroom." + }, + "input_args": { + "label": "Invoerargumenten", + "description": "Invoerargumenten specifiek voor deze stream." + } + } + }, + "live": { + "label": "Live weergave", + "streams": { + "label": "Live streamnamen", + "description": "Koppeling van geconfigureerde streamnamen aan restream/go2rtc-namen voor live weergave." + }, + "height": { + "label": "Live hoogte", + "description": "Hoogte (pixels) voor weergave van de jsmpeg-livestream in de webinterface; moet ≤ hoogte van de detectiestream zijn." + }, + "quality": { + "label": "Live kwaliteit", + "description": "Coderingskwaliteit voor de jsmpeg-stream (1 hoogste, 31 laagste)." + } + }, + "motion": { + "label": "Bewegingsdetectie", + "enabled": { + "label": "Bewegingsdetectie inschakelen" + }, + "threshold": { + "label": "Bewegingsdrempel", + "description": "Pixelverschildrempel voor de bewegingsdetector; hogere waarden verminderen de gevoeligheid (bereik 1-255)." + }, + "lightning_threshold": { + "label": "Bliksemdrempel", + "description": "Drempel om korte lichtflitsen te detecteren en te negeren (lager is gevoeliger, waarden tussen 0,3 en 1,0). Dit voorkomt bewegingsdetectie niet volledig; het zorgt er alleen voor dat de detector stopt met het analyseren van extra frames zodra de drempel wordt overschreden. Op beweging gebaseerde opnames worden tijdens deze gebeurtenissen nog steeds aangemaakt." + }, + "skip_motion_threshold": { + "label": "Drempel voor overgeslagen beweging", + "description": "Als ingesteld op een waarde tussen 0,0 en 1,0, en meer dan dit deel van het beeld verandert in één frame, geeft de detector geen bewegingsvakken terug en kalibreert hij direct opnieuw. Dit bespaart CPU en vermindert vals-positieven bij bliksem, stormen e.d., maar kan echte gebeurtenissen zoals PTZ-tracking missen. De afweging is tussen het weggooien van enkele megabytes opnames versus het bekijken van een paar korte clips. Leeg laten (None) om deze functie uit te schakelen." + }, + "improve_contrast": { + "label": "Contrast verbeteren", + "description": "Contrastverbetering op frames toepassen vóór bewegingsanalyse om detectie te verbeteren." + }, + "contour_area": { + "label": "Contouroppervlakte", + "description": "Minimale contouroppervlakte in pixels voor een bewegingscontour om te worden geteld." + }, + "delta_alpha": { + "label": "Delta-alfa", + "description": "Alpha-mengfactor voor frameverschil bij bewegingsberekening." + }, + "frame_alpha": { + "label": "Frame-alfa", + "description": "Alpha-waarde voor het mengen van frames bij bewegingsvoorverwerking." + }, + "frame_height": { + "label": "Framehoogte", + "description": "Hoogte in pixels waarnaar frames worden geschaald bij het berekenen van beweging." + }, + "mask": { + "label": "Maskercoördinaten", + "description": "Geordende x,y-coördinaten die het bewegingsmaskeerpolygoon definiëren voor het in- of uitsluiten van gebieden." + }, + "mqtt_off_delay": { + "label": "MQTT uit-vertraging", + "description": "Seconden wachten na de laatste beweging vóór publicatie van een MQTT 'off'-status." + }, + "enabled_in_config": { + "label": "Originele bewegingsstatus", + "description": "Geeft aan of bewegingsdetectie was ingeschakeld in de originele statische configuratie." + }, + "raw_mask": { + "label": "Onbewerkt masker" + } + }, + "objects": { + "label": "Objecten", + "description": "Standaardinstellingen voor objectvolging, inclusief te volgen labels en per-object filters.", + "track": { + "label": "Te volgen objecten" + }, + "filters": { + "label": "Objectfilters", + "description": "Filters op gedetecteerde objecten om vals-positieven te verminderen (oppervlakte, verhouding, betrouwbaarheid).", + "min_area": { + "label": "Minimale objectoppervlakte", + "description": "Minimale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn." + }, + "max_area": { + "label": "Maximale objectoppervlakte", + "description": "Maximale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn." + }, + "min_ratio": { + "label": "Minimale beeldverhouding", + "description": "Minimale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren." + }, + "max_ratio": { + "label": "Maximale beeldverhouding", + "description": "Maximale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren." + }, + "threshold": { + "label": "Betrouwbaarheidsdrempel", + "description": "Gemiddelde detectiebetrouwbaarheidsdrempel om een object als terecht positief te beschouwen." + }, + "min_score": { + "label": "Minimale betrouwbaarheid", + "description": "Minimale detectiebetrouwbaarheid in één frame om het object te tellen." + }, + "mask": { + "label": "Filtermasker", + "description": "Polygooncoördinaten die aangeven waar dit filter van toepassing is in het frame." + }, + "raw_mask": { + "label": "Onbewerkt masker" + } + }, + "mask": { + "label": "Objectmasker", + "description": "Maskeerpolygoon om objectdetectie in bepaalde gebieden te voorkomen." + }, + "raw_mask": { + "label": "Onbewerkt masker" + }, + "genai": { + "label": "GenAI-objectconfiguratie", + "description": "GenAI-opties voor het beschrijven van gevolgde objecten en het versturen van frames voor generatie.", + "enabled": { + "label": "GenAI inschakelen", + "description": "GenAI-beschrijvingen voor gevolgde objecten standaard inschakelen." + }, + "use_snapshot": { + "label": "Snapshots gebruiken", + "description": "Objectsnapshots gebruiken in plaats van miniaturen voor GenAI-beschrijving." + }, + "prompt": { + "label": "Bijschriftprompt", + "description": "Standaard promptsjabloon voor het genereren van beschrijvingen met GenAI." + }, + "object_prompts": { + "label": "Objectprompts", + "description": "Prompts per object voor het aanpassen van GenAI-uitvoer voor specifieke labels." + }, + "objects": { + "label": "GenAI-objecten", + "description": "Lijst van objectlabels die standaard naar GenAI worden gestuurd." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die objecten moeten betreden om in aanmerking te komen voor GenAI-beschrijving." + }, + "debug_save_thumbnails": { + "label": "Snapshots opslaan", + "description": "Snapshots die naar GenAI worden gestuurd opslaan voor foutopsporing." + }, + "send_triggers": { + "label": "GenAI-triggers", + "description": "Bepaalt wanneer frames naar GenAI worden gestuurd (bij einde, na updates, enz.).", + "tracked_object_end": { + "label": "Sturen bij beëindiging", + "description": "Een verzoek naar GenAI sturen wanneer het gevolgde object eindigt." + }, + "after_significant_updates": { + "label": "Vroege GenAI-trigger", + "description": "Een verzoek naar GenAI sturen na een bepaald aantal significante updates voor het gevolgde object." + } + }, + "enabled_in_config": { + "label": "Originele GenAI-status", + "description": "Geeft aan of GenAI was ingeschakeld in de originele statische configuratie." + } + } + }, + "record": { + "label": "Opname", + "enabled": { + "label": "Opname inschakelen" + }, + "expire_interval": { + "label": "Opruiminterval opnames", + "description": "Minuten tussen opruimrondes die verlopen opnamesegmenten verwijderen." + }, + "continuous": { + "label": "Continue bewaring", + "description": "Aantal dagen om opnames te bewaren ongeacht gevolgde objecten of beweging. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.", + "days": { + "label": "Bewaardagen", + "description": "Dagen om opnames te bewaren." + } + }, + "motion": { + "label": "Bewegingsretentie", + "description": "Aantal dagen om opnames veroorzaakt door beweging te bewaren, ongeacht gevolgde objecten. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.", + "days": { + "label": "Bewaardagen", + "description": "Dagen om opnames te bewaren." + } + }, + "detections": { + "label": "Detectieretentie", + "description": "Opname-retentie-instellingen voor detectiegebeurtenissen inclusief pre/post-captureduur.", + "pre_capture": { + "label": "Seconden vóór opname", + "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname." + }, + "post_capture": { + "label": "Seconden na opname", + "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname." + }, + "retain": { + "label": "Gebeurtenisbewaring", + "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.", + "days": { + "label": "Bewaardagen", + "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + } + } + }, + "alerts": { + "label": "Meldingsbewaring", + "description": "Opname-retentie-instellingen voor alertgebeurtenissen inclusief pre/post-captureduur.", + "pre_capture": { + "label": "Seconden vóór opname", + "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname." + }, + "post_capture": { + "label": "Seconden na opname", + "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname." + }, + "retain": { + "label": "Gebeurtenisbewaring", + "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.", + "days": { + "label": "Bewaardagen", + "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + } + } + }, + "export": { + "label": "Exportconfiguratie", + "description": "Instellingen voor het exporteren van opnames, zoals timelapse en hardwareversnelling.", + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten voor export", + "description": "Hardwareversnellingsargumenten voor export/transcodering." + }, + "max_concurrent": { + "label": "Maximaal aantal gelijktijdige exports", + "description": "Maximum aantal exporttaken dat tegelijk wordt verwerkt." + } + }, + "preview": { + "label": "Voorbeeldconfiguratie", + "description": "Instellingen voor de kwaliteit van opnamevoorbeelden in de UI.", + "quality": { + "label": "Voorbeeldkwaliteit", + "description": "Kwaliteitsniveau voor voorbeelden (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Originele opnamestatus", + "description": "Geeft aan of opname was ingeschakeld in de originele statische configuratie." + } + }, + "review": { + "label": "Beoordeling", + "alerts": { + "label": "Meldingsconfiguratie", + "description": "Instellingen voor welke gevolgde objecten alerts genereren en hoe alerts worden bewaard.", + "enabled": { + "label": "Alerts inschakelen" + }, + "labels": { + "label": "Meldingslabels", + "description": "Lijst met objectlabels die kwalificeren als meldingen (bijv. auto, persoon)." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden om als melding te worden beschouwd; leeg laten voor elke zone." + }, + "enabled_in_config": { + "label": "Originele meldingsstatus", + "description": "Geeft aan of meldingen oorspronkelijk waren ingeschakeld in de statische configuratie." + }, + "cutoff_time": { + "label": "Afsluitingstijd meldingen", + "description": "Seconden wachten na het uitblijven van melding veroorzakende activiteit voordat een melding wordt afgesloten." + } + }, + "detections": { + "label": "Detectieconfiguratie", + "description": "Instellingen voor welke gevolgde objecten detecties genereren en hoe detecties worden bewaard.", + "enabled": { + "label": "Detecties inschakelen" + }, + "labels": { + "label": "Detectielabels", + "description": "Lijst met objectlabels die kwalificeren als detectiegebeurtenissen." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden om als detectie te worden beschouwd; leeg laten voor elke zone." + }, + "cutoff_time": { + "label": "Afsluitingstijd detecties", + "description": "Seconden wachten na het uitblijven van detectie veroorzakende activiteit voordat een detectie wordt afgesloten." + }, + "enabled_in_config": { + "label": "Originele detectiestatus", + "description": "Geeft aan of detecties oorspronkelijk waren ingeschakeld in de statische configuratie." + } + }, + "genai": { + "label": "GenAI-configuratie", + "description": "Beheert het gebruik van generatieve AI voor het produceren van beschrijvingen en samenvattingen van beoordelingsitems.", + "enabled": { + "label": "GenAI-beschrijvingen inschakelen", + "description": "Door GenAI gegenereerde beschrijvingen en samenvattingen voor beoordelingsitems in- of uitschakelen." + }, + "alerts": { + "label": "GenAI inschakelen voor meldingen", + "description": "GenAI gebruiken voor het genereren van beschrijvingen bij meldingsitems." + }, + "detections": { + "label": "GenAI inschakelen voor detecties", + "description": "GenAI gebruiken voor het genereren van beschrijvingen bij detectiebeoordelingen." + }, + "image_source": { + "label": "Afbeeldingsbron voor beoordeling", + "description": "Bron van afbeeldingen naar GenAI ('preview' of 'recordings'); 'recordings' gebruikt hogere kwaliteit maar meer tokens." + }, + "additional_concerns": { + "label": "Aanvullende aandachtspunten", + "description": "Een lijst met aanvullende aandachtspunten die GenAI moet meenemen bij het beoordelen van activiteit op deze camera." + }, + "debug_save_thumbnails": { + "label": "Snapshots opslaan", + "description": "Snapshots die naar de GenAI-provider worden gestuurd opslaan voor foutopsporing." + }, + "enabled_in_config": { + "label": "Originele GenAI-status", + "description": "Geeft aan of GenAI-beoordeling oorspronkelijk was ingeschakeld in de statische configuratie." + }, + "preferred_language": { + "label": "Voorkeurstaal", + "description": "Voorkeurstaal voor gegenereerde antwoorden van de GenAI-provider." + }, + "activity_context_prompt": { + "label": "Activiteitscontextprompt", + "description": "Aangepaste prompt die beschrijft wat wel en niet verdachte activiteit is, als context voor GenAI-samenvattingen." + } + } + }, + "snapshots": { + "label": "Snapshots", + "enabled": { + "label": "Snapshots inschakelen" + }, + "timestamp": { + "label": "Tijdstempel-overlay", + "description": "Een tijdstempel op API-snapshots weergeven." + }, + "bounding_box": { + "label": "Detectiekader-overlay", + "description": "Detectiekaders voor gevolgde objecten tekenen op API-snapshots." + }, + "crop": { + "label": "Snapshot bijsnijden", + "description": "API-snapshots bijsnijden tot het detectiekader van het gedetecteerde object." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden voordat een snapshot wordt opgeslagen." + }, + "height": { + "label": "Snapshothoogte", + "description": "Hoogte (pixels) om API-snapshots naar te schalen; leeg laten om de originele grootte te behouden." + }, + "retain": { + "label": "Snapshot-bewaring", + "description": "Bewaarinstellingen voor snapshots inclusief standaarddagen en per-object overschrijvingen.", + "default": { + "label": "Standaard retentie", + "description": "Standaard aantal dagen om snapshots te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + }, + "objects": { + "label": "Objectbewaring", + "description": "Objectspecifieke overschrijvingen voor het aantal bewaardagen van snapshots." + } + }, + "quality": { + "label": "Snapshotkwaliteit", + "description": "Coderingskwaliteit voor opgeslagen snapshots (0-100)." + } + }, + "timestamp_style": { + "label": "Tijdstempelstijl", + "position": { + "label": "Tijdstempelpositie", + "description": "Positie van de tijdstempel op de afbeelding (tl/tr/bl/br)." + }, + "format": { + "label": "Tijdstempelformaat", + "description": "Datumtijdformaatstring voor tijdstempels (Python datetime-formaatcodes)." + }, + "color": { + "label": "Tijdstempelkleur", + "description": "RGB-kleurwaarden voor de tijdstempeltekst (alle waarden 0-255).", + "red": { + "label": "Rood", + "description": "Roodcomponent (0-255) voor de tijdstempelkleur." + }, + "green": { + "label": "Groen", + "description": "Groencomponent (0-255) voor de tijdstempelkleur." + }, + "blue": { + "label": "Blauw", + "description": "Blauwcomponent (0-255) voor de tijdstempelkleur." + } + }, + "thickness": { + "label": "Tijdstempeldikte", + "description": "Lijndikte van de tijdstempeltekst." + }, + "effect": { + "label": "Tijdstempeleffect", + "description": "Visueel effect voor de tijdstempeltekst (geen, effen, schaduw)." + } + }, + "semantic_search": { + "label": "Semantisch zoeken", + "triggers": { + "label": "Triggers", + "description": "Acties en matchcriteria voor cameraspecifieke semantisch-zoeken-triggers.", + "friendly_name": { + "label": "Weergavenaam", + "description": "Optionele weergavenaam voor deze trigger in de UI." + }, + "enabled": { + "label": "Trigger inschakelen", + "description": "Deze semantisch-zoeken-trigger in- of uitschakelen." + }, + "type": { + "label": "Triggertype", + "description": "Type trigger: 'thumbnail' (vergelijk met afbeelding) of 'description' (vergelijk met tekst)." + }, + "data": { + "label": "Triggerinhoud", + "description": "Tekstzin of miniatuur-ID om te vergelijken met gevolgde objecten." + }, + "threshold": { + "label": "Triggerdrempel", + "description": "Minimale gelijkenisscore (0-1) om deze trigger te activeren." + }, + "actions": { + "label": "Triggeracties", + "description": "Lijst van uit te voeren acties bij triggermatch (melding, sub_label, attribuut)." + } + } + }, + "face_recognition": { + "label": "Gezichtsherkenning", + "enabled": { + "label": "Gezichtsherkenning inschakelen" + }, + "min_area": { + "label": "Minimale gezichtsoppervlakte", + "description": "Minimale oppervlakte (pixels) van een gedetecteerd gezichtskader om herkenning te proberen." + } + }, + "lpr": { + "label": "Kentekenherkenning", + "description": "Instellingen voor kentekenherkenning inclusief detectiedrempels, opmaak en bekende kentekens.", + "enabled": { + "label": "LPR inschakelen" + }, + "min_area": { + "label": "Minimale kentekenoppervlakte", + "description": "Minimale kentekenoppervlakte (pixels) om herkenning te proberen." + }, + "enhancement": { + "label": "Verbeteringsniveau", + "description": "Verbeteringsniveau (0-10) voor kentekenuitsneden vóór OCR; hogere waarden verbeteren niet altijd het resultaat; niveaus boven 5 werken mogelijk alleen voor nachtelijke kentekens en moeten voorzichtig worden gebruikt." + }, + "expire_time": { + "label": "Vervaltijd in seconden", + "description": "Tijd in seconden waarna een niet-gezien kenteken vervalt uit de tracker (alleen voor dedicated LPR-camera's)." + } + }, + "onvif": { + "label": "ONVIF", + "description": "ONVIF-verbindings- en PTZ-autovolgingsinstellingen voor deze camera.", + "host": { + "label": "ONVIF-host", + "description": "Host (en optioneel schema) voor de ONVIF-dienst van deze camera." + }, + "port": { + "label": "ONVIF-poort", + "description": "Poortnummer voor de ONVIF-dienst." + }, + "user": { + "label": "ONVIF-gebruikersnaam", + "description": "Gebruikersnaam voor ONVIF-authenticatie; sommige apparaten vereisen de admin-gebruiker voor ONVIF." + }, + "password": { + "label": "ONVIF-wachtwoord", + "description": "Wachtwoord voor ONVIF-authenticatie." + }, + "tls_insecure": { + "label": "TLS-verificatie uitschakelen", + "description": "TLS-verificatie overslaan en digest-authenticatie uitschakelen voor ONVIF (onveilig; alleen in veilige netwerken)." + }, + "profile": { + "label": "ONVIF-profiel", + "description": "Specifiek ONVIF-mediaprofiel voor PTZ-besturing, gekoppeld via token of naam. Indien niet ingesteld, wordt het eerste profiel met geldige PTZ-configuratie automatisch geselecteerd." + }, + "autotracking": { + "label": "Automatisch volgen", + "description": "Bewegende objecten automatisch volgen en gecentreerd houden in het beeld via PTZ-camerabewegingen.", + "enabled": { + "label": "Automatisch volgen inschakelen", + "description": "Automatisch PTZ-camera volgen van gedetecteerde objecten in- of uitschakelen." + }, + "calibrate_on_startup": { + "label": "Kalibreren bij opstarten", + "description": "PTZ-motorsnelheden meten bij opstarten voor nauwkeurigere volging. Frigate werkt de configuratie bij met movement_weights na kalibratie." + }, + "zooming": { + "label": "Zoommodus", + "description": "Zoomgedrag instellen: disabled (alleen pan/tilt), absolute (meest compatibel) of relative (gelijktijdig pan/tilt/zoom)." + }, + "zoom_factor": { + "label": "Zoomfactor", + "description": "Zoomniveau voor gevolgde objecten instellen. Lagere waarden tonen meer van de scène; hogere waarden zoomen verder in maar kunnen de volging verliezen. Waarden tussen 0,1 en 0,75." + }, + "track": { + "label": "Gevolgde objecten", + "description": "Lijst van objecttypen die automatisch volgen activeren." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Objecten moeten een van deze zones betreden voordat automatisch volgen begint." + }, + "return_preset": { + "label": "Terugkeer-voorinstelling", + "description": "ONVIF-voorkeuzeinstelling in de camerafirmware om naar terug te keren na het volgen." + }, + "timeout": { + "label": "Terugkeertimeout", + "description": "Dit aantal seconden wachten na het verliezen van de volging voordat de camera naar de voorkeuze-positie terugkeert." + }, + "movement_weights": { + "label": "Bewegingsgewichten", + "description": "Kalibratiewaarden automatisch gegenereerd door camerakalbratie. Niet handmatig aanpassen." + }, + "enabled_in_config": { + "label": "Originele autovolgstatus", + "description": "Intern veld om bij te houden of automatisch volgen was ingeschakeld in de configuratie." + } + }, + "ignore_time_mismatch": { + "label": "Tijdsverschil negeren", + "description": "Tijdsynchronisatieverschillen tussen camera en Frigate-server negeren voor ONVIF-communicatie." + } + } +} diff --git a/web/public/locales/nl/config/global.json b/web/public/locales/nl/config/global.json new file mode 100644 index 0000000..8da53d6 --- /dev/null +++ b/web/public/locales/nl/config/global.json @@ -0,0 +1,1600 @@ +{ + "audio": { + "label": "Audio events", + "enabled": { + "label": "Geluiddetectie inschakelen", + "description": "Audioeventdetectie voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "max_not_heard": { + "label": "Einde time-out", + "description": "Aantal seconden zonder het geconfigureerde audiotype voordat de audio-event wordt beëindigd." + }, + "min_volume": { + "label": "Minimumvolume", + "description": "Minimale RMS-volumedrempel die nodig is om audiodetectie te starten; hoe lager de waarde, hoe gevoeliger de detectie (bijvoorbeeld, 200 hoog, 500 gemiddeld, 1000 laag)." + }, + "listen": { + "label": "Luistercategorieën", + "description": "Lijst van luistercategorie gebeurtenissen voor detectie (zoals: blaffen, band_alarm, schreeuw, praten, roepen)." + }, + "filters": { + "label": "Geluidsfilters", + "description": "Instellingen per audiotype, waaronder betrouwbaarheidsdrempels, ter vermindering van foutieve detecties.", + "threshold": { + "label": "Minimale audiobetrouwbaarheid", + "description": "Minimale betrouwbaarheidsdrempel voor de audiogebeurtenis om te worden geteld." + } + }, + "enabled_in_config": { + "label": "Originele audio-instelling", + "description": "Geeft aan of audiodetectie oorspronkelijk was geactiveerd in het statische configuratiebestand." + }, + "num_threads": { + "label": "Detectiethreads", + "description": "Aantal threads voor audiodetectieverwerking." + }, + "description": "Instellingen voor audiogebaseerde gebeurtenisdetectie voor alle camera's; kan per camera worden overschreven." + }, + "audio_transcription": { + "label": "Audiotranscriptie", + "description": "Instellingen voor live en spraakgestuurde audiotranscriptie voor gebeurtenissen en live ondertitels.", + "live_enabled": { + "label": "Live transcriptie", + "description": "Live streaming‑transcriptie van audio inschakelen tijdens ontvangst." + }, + "enabled": { + "label": "Audiotranscriptie inschakelen", + "description": "Automatische audiotranscriptie voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "language": { + "label": "Transcriptietaal", + "description": "Taalcode voor transcriptie/vertaling (bijv. 'nl' voor Nederlands). Zie https://whisper-api.com/docs/languages/ voor ondersteunde taalcodes." + }, + "device": { + "label": "Transcriptieapparaat", + "description": "Apparaat (CPU/GPU) voor het uitvoeren van het transcriptiemodel. Momenteel worden alleen NVIDIA CUDA GPU's ondersteund voor transcriptie." + }, + "model_size": { + "label": "Modelgrootte", + "description": "Modelgrootte voor offline audiotranscriptie." + } + }, + "birdseye": { + "label": "Birdseye-overzicht", + "description": "Instellingen voor de overzichtsweergave die meerdere camerafeeds combineert tot één lay‑out.", + "enabled": { + "label": "Birdseye-overzicht inschakelen", + "description": "De overzichtsweergavefunctie in- of uitschakelen." + }, + "mode": { + "label": "Weergavemodus", + "description": "Modus voor het opnemen van camera’s in overzichtsweergave: ‘objecten’, ‘beweging’ of ‘continu’." + }, + "order": { + "label": "Positie", + "description": "Numerieke positie die de volgorde van de camera in de overzichtsweergave lay-out bepaalt." + }, + "restream": { + "label": "RTSP-herstreaming", + "description": "De Birdseye-uitvoer herstreamen als RTSP-feed; hierdoor blijft Birdseye continu actief." + }, + "width": { + "label": "Breedte", + "description": "Uitvoerbreedte (pixels) van het samengestelde Birdseye-frame." + }, + "height": { + "label": "Hoogte", + "description": "Uitvoerhoogte (pixels) van het samengestelde Birdseye-frame." + }, + "quality": { + "label": "Coderingskwaliteit", + "description": "Coderingskwaliteit van de Birdseye MPEG-1-feed (1 = hoogste kwaliteit, 31 = laagste)." + }, + "inactivity_threshold": { + "label": "Inactiviteitsdrempel", + "description": "Seconden inactiviteit waarna een camera niet meer in Birdseye wordt getoond." + }, + "layout": { + "label": "Lay-out", + "description": "Lay-outopties voor de Birdseye-samenstelling.", + "scaling_factor": { + "label": "Schaalfactor", + "description": "Schaalfactor voor de lay-outcalculator (bereik 1,0 tot 5,0)." + }, + "max_cameras": { + "label": "Maximum camera's", + "description": "Maximaal aantal camera's dat tegelijk in Birdseye wordt weergegeven; toont de meest recente camera's." + } + }, + "idle_heartbeat_fps": { + "label": "Inactief heartbeat-FPS", + "description": "Frames per seconde voor het opnieuw verzenden van het laatste Birdseye-frame tijdens inactiviteit; stel 0 in om uit te schakelen." + } + }, + "detect": { + "label": "Objectdetectie", + "description": "Instellingen voor de detectierol om objecten te detecteren en trackers te starten.", + "enabled": { + "label": "Detectie inschakelen", + "description": "Objectdetectie voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "height": { + "label": "Detectiehoogte", + "description": "De hoogte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken." + }, + "width": { + "label": "Detectiebreedte", + "description": "De breedte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken." + }, + "fps": { + "label": "Detectie‑FPS", + "description": "Gewenst aantal frames per seconde waarop detectie wordt uitgevoerd; lagere waarden verlagen het CPU‑gebruik (aanbevolen waarde is 5, stel alleen hoger in — maximaal 10 — bij het volgen van extreem snel bewegende objecten)." + }, + "min_initialized": { + "label": "Minimale initialisatieframes", + "description": "Aantal opeenvolgende detectieresultaten dat vereist is voordat een gevolgd object wordt aangemaakt. Verhoog deze waarde om valse initialisaties te verminderen. De standaardwaarde is FPS gedeeld door 2." + }, + "max_disappeared": { + "label": "Maximaal aantal verdwenen frames", + "description": "Aantal frames zonder detectie voordat een gevolgd object als verdwenen wordt beschouwd." + }, + "stationary": { + "label": "Instellingen voor stilstaande objecten", + "description": "Instellingen voor het detecteren en beheren van objecten die gedurende een bepaalde tijd stil blijven staan.", + "interval": { + "label": "Interval voor stilstaande objecten", + "description": "Frequentie (in frames) waarmee detectie wordt gecontroleerd om stilstaande objecten te bevestigen." + }, + "threshold": { + "label": "Drempel voor stilstaande objecten", + "description": "Het aantal frames waarin geen positieverandering wordt gedetecteerd voordat een object als stilstaand wordt beschouwd." + }, + "max_frames": { + "label": "Maximaal aantal frames", + "description": "Stelt een limiet aan de duur van tracking van stilstaande objecten.", + "default": { + "label": "Standaard maximaal aantal frames", + "description": "Standaardlimiet voor het aantal frames dat een stilstaand object wordt gevolgd voordat wordt gestopt." + }, + "objects": { + "label": "Maximaal aantal frames per object", + "description": "Maximum aantal frames per object bij het volgen van stilstaande objecten." + } + }, + "classifier": { + "label": "Visuele classifier inschakelen", + "description": "Gebruik een visuele classifier om echt stilstaande objecten te detecteren, zelfs wanneer detectiekaders licht verschuiven." + } + }, + "annotation_offset": { + "label": "Annotatie-offset", + "description": "Milliseconden om detectieannotaties te verschuiven voor betere uitlijning van tijdlijn-detectiekaders met opnames; kan positief of negatief zijn." + } + }, + "version": { + "description": "Numerieke of string-versie van de actieve configuratie om migraties of formaatwijzigingen te helpen detecteren.", + "label": "Huidige config-versie" + }, + "safe_mode": { + "label": "Veilige modus", + "description": "Wanneer ingeschakeld, start Frigate op in veilige modus met beperkte functies voor probleemoplossing." + }, + "environment_vars": { + "label": "Omgevingsvariabelen", + "description": "Sleutel/waarde-paren van omgevingsvariabelen die ingesteld worden voor het Frigate-proces in Home Assistant OS. Gebruikers zonder HAOS moeten in plaats daarvan de Docker-omgevingsvariabelenconfiguratie gebruiken." + }, + "auth": { + "label": "Authenticatie", + "enabled": { + "label": "Authenticatie inschakelen", + "description": "Schakel native authenticatie in voor de Frigate UI." + }, + "reset_admin_password": { + "label": "Adminwachtwoord resetten", + "description": "Indien waar, reset het wachtwoord van de admingebruiker tijdens opstarten en print het nieuwe wachtwoord in het logboek." + }, + "description": "Authenticatie- en sessie-instellingen inclusief cookie- en snelheidsbeperkingsopties.", + "cookie_name": { + "label": "JWT-cookienaam", + "description": "Naam van de gebruikte cookie om de JWT token voor native authenticatie op te slaan." + }, + "cookie_secure": { + "label": "Secure-cookievlag", + "description": "Stel de veilige instelling in op de auth cookie; moet waar zijn indien TLS in gebruik." + }, + "session_length": { + "label": "Sessieduur", + "description": "Sessieduur in seconden voor JWT-gebaseerde sessies." + }, + "refresh_time": { + "label": "Sessie-verversperiode", + "description": "Als een sessie binnen dit aantal seconden verloopt, wordt de sessie verlengd tot de volledige duur." + }, + "failed_login_rate_limit": { + "label": "Limieten voor mislukte inlogpogingen", + "description": "Rate-limitregels voor mislukte inlogpogingen om brute-forceaanvallen te beperken." + }, + "trusted_proxies": { + "label": "Vertrouwde proxies", + "description": "Lijst met vertrouwde proxy-IP's die worden gebruikt bij het bepalen van het client-IP voor rate limiting." + }, + "hash_iterations": { + "label": "Hash-iteraties", + "description": "Aantal PBKDF2-SHA256-iteraties voor het hashen van gebruikerswachtwoorden." + }, + "roles": { + "label": "Roltoewijzingen", + "description": "Koppel rollen aan cameralijsten. Een lege lijst geeft de rol toegang tot alle camera's." + }, + "admin_first_time_login": { + "label": "Eerste keer admin-vlag", + "description": "Wanneer ingeschakeld kan de UI een helplink tonen op de inlogpagina om gebruikers te informeren hoe ze kunnen inloggen na een admin-wachtwoordreset. " + } + }, + "logger": { + "default": { + "label": "Loggingsniveau", + "description": "Standaard globale logdetailniveau (debug, info, warning, error)." + }, + "label": "Logging", + "logs": { + "label": "Logboekniveau per proces", + "description": "Logboekniveau-afwijkingen per component om het detailniveau per specifieke module te verhogen of verlagen." + }, + "description": "Beheert het standaard logdetailniveau en afwijkende instellingen per logboek." + }, + "profiles": { + "label": "Profielen", + "description": "Benoemde profieldefinities met weergavenamen. Cameraprofielen moeten verwijzen naar hier gedefinieerde namen.", + "friendly_name": { + "label": "Weergavenaam", + "description": "Weergavenaam voor dit profiel in de UI." + } + }, + "database": { + "label": "Database", + "description": "Instellingen voor de SQLite-database die Frigate gebruikt om gevolgde objecten en opname-metadata op te slaan.", + "path": { + "label": "Databasepad", + "description": "Bestandssysteempad waar het Frigate SQLite-databasebestand wordt opgeslagen." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "Instellingen voor de geïntegreerde go2rtc-restreaming-service voor het doorzenden en omzetten van live streams." + }, + "mqtt": { + "label": "MQTT", + "description": "Instellingen voor het verbinden met en publiceren van telemetrie, snapshots en gebeurtenisdetails naar een MQTT-broker.", + "enabled": { + "label": "MQTT inschakelen", + "description": "MQTT-integratie voor status, gebeurtenissen en snapshots in- of uitschakelen." + }, + "host": { + "label": "MQTT-host", + "description": "Hostnaam of IP-adres van de MQTT-broker." + }, + "port": { + "label": "MQTT-poort", + "description": "Poort van de MQTT-broker (gewoonlijk 1883 voor gewoon MQTT)." + }, + "topic_prefix": { + "label": "Topic-prefix", + "description": "MQTT-topic-prefix voor alle Frigate-topics; moet uniek zijn bij meerdere instanties." + }, + "client_id": { + "label": "Client-ID", + "description": "Client-ID voor verbinding met de MQTT-broker; moet uniek zijn per instantie." + }, + "stats_interval": { + "label": "Statistiekeninterval", + "description": "Interval in seconden voor het publiceren van systeem- en camerastatistieken naar MQTT." + }, + "user": { + "label": "MQTT-gebruikersnaam", + "description": "Optionele MQTT-gebruikersnaam; kan via omgevingsvariabelen of secrets worden opgegeven." + }, + "password": { + "label": "MQTT-wachtwoord", + "description": "Optioneel MQTT-wachtwoord; kan via omgevingsvariabelen of secrets worden opgegeven." + }, + "tls_ca_certs": { + "label": "TLS CA-certificaten", + "description": "Pad naar het CA-certificaat voor TLS-verbindingen met de broker (voor zelfondertekende certificaten)." + }, + "tls_client_cert": { + "label": "Clientcertificaat", + "description": "Pad naar het clientcertificaat voor wederzijdse TLS-authenticatie; stel geen gebruiker/wachtwoord in bij gebruik van clientcertificaten." + }, + "tls_client_key": { + "label": "Clientsleutel", + "description": "Pad naar de privésleutel van het clientcertificaat." + }, + "tls_insecure": { + "label": "Onveilige TLS", + "description": "Onveilige TLS-verbindingen toestaan door hostnaamverificatie over te slaan (niet aanbevolen)." + }, + "qos": { + "label": "MQTT QoS-niveau", + "description": "QoS-niveau voor MQTT-publicaties/abonnementen (0, 1 of 2)." + } + }, + "notifications": { + "label": "Meldingen", + "description": "Instellingen om meldingen voor alle camera's in te schakelen en te beheren; kan per camera worden overschreven.", + "enabled": { + "label": "Meldingen inschakelen", + "description": "Meldingen voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "email": { + "label": "Melding email", + "description": "E-mailadres voor pushmeldingen of vereist door bepaalde meldingsproviders." + }, + "cooldown": { + "label": "Wachttijd", + "description": "Wachttijd (seconden) tussen meldingen om spammen te voorkomen." + }, + "enabled_in_config": { + "label": "Originele meldingsstatus", + "description": "Geeft aan of meldingen waren ingeschakeld in de originele statische configuratie." + } + }, + "networking": { + "label": "Netwerken", + "description": "Netwerkinstellingen zoals IPv6-ondersteuning voor Frigate-eindpunten.", + "ipv6": { + "label": "IPv6-configuratie", + "description": "IPv6-instellingen voor Frigate-netwerkdiensten.", + "enabled": { + "label": "IPv6 inschakelen", + "description": "IPv6-ondersteuning voor Frigate-diensten (API en UI) inschakelen waar van toepassing." + } + }, + "listen": { + "label": "Luisterpoortenconfiguratie", + "description": "Configuratie van interne en externe luisterpoorten. Dit is voor gevorderde gebruikers. In de meeste gevallen wordt aanbevolen de poortensectie in het Docker Compose-bestand aan te passen.", + "internal": { + "label": "Interne poort", + "description": "Interne luisterpoort voor Frigate (standaard 5000)." + }, + "external": { + "label": "Externe poort", + "description": "Externe luisterpoort voor Frigate (standaard 8971)." + } + } + }, + "proxy": { + "label": "Proxy", + "description": "Instellingen voor het integreren van Frigate achter een reverse proxy die geauthenticeerde gebruikersheaders doorgeeft.", + "header_map": { + "label": "Headertoewijzing", + "description": "Inkomende proxyheaders koppelen aan Frigate gebruikers- en rolvelden voor proxy-authenticatie.", + "user": { + "label": "Gebruikersheader", + "description": "Header met de geauthenticeerde gebruikersnaam van de upstream-proxy." + }, + "role": { + "label": "Rolheader", + "description": "Header met de rol of groepen van de geauthenticeerde gebruiker van de upstream-proxy." + }, + "role_map": { + "label": "Roltoewijzing", + "description": "Koppel upstream-groepswaarden aan Frigate-rollen (bijv. admingroepen aan de adminrol)." + } + }, + "logout_url": { + "label": "Uitlog-URL", + "description": "URL waarnaar gebruikers worden doorgestuurd bij uitloggen via de proxy." + }, + "auth_secret": { + "label": "Proxygeheim", + "description": "Optioneel geheim dat wordt gecontroleerd tegen de X-Proxy-Secret-header om vertrouwde proxies te verifiëren." + }, + "default_role": { + "label": "Standaardrol", + "description": "Standaardrol toegewezen aan proxy-geauthenticeerde gebruikers wanneer geen roltoewijzing van toepassing is (admin of viewer)." + }, + "separator": { + "label": "Scheidingsteken", + "description": "Scheidingsteken voor meerdere waarden in proxyheaders." + } + }, + "telemetry": { + "label": "Telemetrie", + "description": "Opties voor systeemtelemetrie en statistieken, inclusief GPU- en netwerkbandbreedtebewaking.", + "network_interfaces": { + "label": "Netwerkinterfaces", + "description": "Lijst met netwerkinterfacenaamprefixen voor bandbreedtestatistieken." + }, + "stats": { + "label": "Systeemstatistieken", + "description": "Opties voor het in- of uitschakelen van het verzamelen van systeem- en GPU-statistieken.", + "amd_gpu_stats": { + "label": "AMD GPU-statistieken", + "description": "Verzameling van AMD GPU-statistieken inschakelen indien een AMD GPU aanwezig is." + }, + "intel_gpu_stats": { + "label": "Intel GPU-statistieken", + "description": "Verzameling van Intel GPU-statistieken inschakelen indien een Intel GPU aanwezig is." + }, + "network_bandwidth": { + "label": "Netwerkbandbreedte", + "description": "Per-proces netwerkbandbreedtebewaking voor camera-ffmpeg-processen en detectoren inschakelen (vereist Linux-capabilities)." + }, + "intel_gpu_device": { + "label": "Intel GPU-apparaat", + "description": "PCI-busadres of DRM-apparaatpad (bijv. /dev/dri/card1) om Intel GPU-statistieken aan een specifiek apparaat te koppelen bij meerdere GPU's." + } + }, + "version_check": { + "label": "Versiecontrole", + "description": "Een uitgaande controle inschakelen om te detecteren of een nieuwere Frigate-versie beschikbaar is." + } + }, + "tls": { + "label": "TLS", + "description": "TLS-instellingen voor de Frigate-webservice (poort 8971).", + "enabled": { + "label": "TLS inschakelen", + "description": "TLS inschakelen voor de Frigate-webinterface en API op de geconfigureerde TLS-poort." + } + }, + "ui": { + "label": "UI", + "description": "Gebruikersinterfacevoorkeuren zoals tijdzone, tijd/datumopmaak en eenheden.", + "timezone": { + "label": "Tijdzone", + "description": "Optionele tijdzone voor weergave in de UI (standaard browsertijd indien niet ingesteld)." + }, + "time_format": { + "label": "Tijdnotatie", + "description": "Tijdnotatie voor de UI (browser, 12-uurs of 24-uurs)." + }, + "date_style": { + "label": "Datumstijl", + "description": "Datumstijl voor de UI (vol, lang, middel, kort)." + }, + "time_style": { + "label": "Tijdstijl", + "description": "Tijdstijl voor de UI (vol, lang, middel, kort)." + }, + "unit_system": { + "label": "Eenhedensysteem", + "description": "Eenhedensysteem voor weergave (metrisch of imperiaal) in de UI en MQTT." + } + }, + "detectors": { + "label": "Detector hardware", + "description": "Configuratie voor objectdetectors (CPU, GPU, ONNX-backends) en detector-specifieke modelinstellingen.", + "type": { + "label": "Type" + }, + "model": { + "label": "Detector-specifieke modelconfiguratie", + "description": "Detector-specifieke modelconfiguratie-opties (pad, invoergrootte, enz.).", + "path": { + "label": "Pad naar aangepast objectdetectormodel", + "description": "Pad naar een aangepast detectiemodel (of plus:// voor Frigate+-modellen)." + }, + "labelmap_path": { + "label": "Labelmap voor aangepaste objectdetector", + "description": "Pad naar een labelmap-bestand dat numerieke klassen koppelt aan string-labels voor de detector." + }, + "width": { + "label": "Invoerbreedte objectdetectiemodel", + "description": "Breedte van de modelinvoertensor in pixels." + }, + "height": { + "label": "Invoerhoogte objectdetectiemodel", + "description": "Hoogte van de modelinvoertensor in pixels." + }, + "labelmap": { + "label": "Labelmap-aanpassing", + "description": "Overschrijvingen of herwijzingen om samen te voegen met de standaard labelmap." + }, + "attributes_map": { + "label": "Koppeling van objectlabels aan attribuutlabels", + "description": "Koppeling tussen objectlabels en attribuutlabels voor metadata (bijv. 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Invoertensorvorm van het model", + "description": "Tensorformaat dat het model verwacht: 'nhwc' of 'nchw'." + }, + "input_pixel_format": { + "label": "Invoerpixelkleurformaat van het model", + "description": "Pixelkleurruimte die het model verwacht: 'rgb', 'bgr' of 'yuv'." + }, + "input_dtype": { + "label": "Invoergegevenstype van het model", + "description": "Gegevenstype van de modelinvoertensor (bijv. 'float32')." + }, + "model_type": { + "label": "Modeltype voor objectdetectie", + "description": "Modelarchitectuurtype van de detector (ssd, yolox, yolonas) voor optimalisatie door sommige detectors." + } + }, + "model_path": { + "label": "Detector-specifiek modelpad", + "description": "Bestandspad naar het detector-modelbinaire bestand, indien vereist door de gekozen detector." + }, + "axengine": { + "label": "AXEngine NPU", + "description": "AXERA AX650N/AX8850N NPU-detector die gecompileerde .axmodel-bestanden uitvoert via de AXEngine-runtime." + }, + "cpu": { + "label": "CPU", + "description": "CPU TFLite-detector die TensorFlow Lite-modellen uitvoert op de host-CPU zonder hardwareversnelling. Niet aanbevolen.", + "num_threads": { + "label": "Aantal detectiethreads", + "description": "Het aantal threads voor CPU-gebaseerde inferentie." + } + }, + "deepstack": { + "label": "DeepStack", + "description": "DeepStack/CodeProject.AI-detector die afbeeldingen naar een externe DeepStack HTTP API stuurt voor inferentie. Niet aanbevolen.", + "api_url": { + "label": "DeepStack API URL", + "description": "De URL van de DeepStack API." + }, + "api_timeout": { + "label": "DeepStack API-timeout (in seconden)", + "description": "Maximale toegestane tijd voor een DeepStack API-verzoek." + }, + "api_key": { + "label": "DeepStack API-sleutel (indien vereist)", + "description": "Optionele API-sleutel voor geauthenticeerde DeepStack-diensten." + } + }, + "degirum": { + "label": "DeGirum", + "description": "DeGirum-detector voor het uitvoeren van modellen via DeGirum-cloud of lokale inferentiediensten.", + "location": { + "label": "Locatie van inferentie-engine", + "description": "Locatie van de DeGirum-inferentie-engine (bijv. '@cloud', '127.0.0.1')." + }, + "zoo": { + "label": "Model Zoo", + "description": "Pad of URL naar de DeGirum model zoo." + }, + "token": { + "label": "DeGirum-cloudtoken", + "description": "Token voor toegang tot de DeGirum-cloud." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "EdgeTPU-detector die TensorFlow Lite-modellen uitvoert die zijn gecompileerd voor Coral EdgeTPU via de EdgeTPU-delegate.", + "device": { + "label": "Apparaattype", + "description": "Het apparaat voor EdgeTPU-inferentie (bijv. 'usb', 'pci')." + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "Hailo-8/Hailo-8L-detector die HEF-modellen en de HailoRT SDK gebruikt voor inferentie op Hailo-hardware.", + "device": { + "label": "Apparaattype", + "description": "Het apparaat voor Hailo-inferentie (bijv. 'PCIe', 'M.2')." + } + }, + "memryx": { + "label": "MemryX", + "description": "MemryX MX3-detector die gecompileerde DFP-modellen uitvoert op MemryX-accelerators.", + "device": { + "label": "Apparaatpad", + "description": "Het apparaat voor MemryX-inferentie (bijv. 'PCIe')." + } + }, + "onnx": { + "label": "ONNX", + "description": "ONNX-detector voor het uitvoeren van ONNX-modellen; gebruikt beschikbare versnellingsbackends (CUDA/ROCm/OpenVINO) indien beschikbaar.", + "device": { + "label": "Apparaattype", + "description": "Het apparaat voor ONNX-inferentie (bijv. 'AUTO', 'CPU', 'GPU')." + } + }, + "openvino": { + "label": "OpenVINO", + "description": "OpenVINO-detector voor AMD- en Intel-CPU's, Intel GPU's en Intel VPU-hardware.", + "device": { + "label": "Apparaattype", + "description": "Het apparaat voor OpenVINO-inferentie (bijv. 'CPU', 'GPU', 'NPU')." + } + }, + "rknn": { + "label": "RKNN", + "description": "RKNN-detector voor Rockchip NPU's; voert gecompileerde RKNN-modellen uit op Rockchip-hardware.", + "num_cores": { + "label": "Aantal te gebruiken NPU-kernen.", + "description": "Het aantal te gebruiken NPU-kernen (0 voor automatisch)." + } + }, + "synaptics": { + "label": "Synaptics", + "description": "Synaptics NPU-detector voor modellen in .synap-formaat via de Synap SDK op Synaptics-hardware." + }, + "teflon_tfl": { + "label": "Teflon", + "description": "Teflon delegate-detector voor TFLite via de Mesa Teflon delegate-bibliotheek voor GPU-versnelling." + }, + "tensorrt": { + "label": "TensorRT", + "description": "TensorRT-detector voor Nvidia Jetson-apparaten via geserialiseerde TensorRT-engines voor versnelde inferentie.", + "device": { + "label": "GPU-apparaatindex", + "description": "De te gebruiken GPU-apparaatindex." + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "ZMQ IPC-detector die inferentie uitbesteedt aan een extern proces via een ZeroMQ IPC-eindpunt.", + "endpoint": { + "label": "ZMQ IPC-eindpunt", + "description": "Het ZMQ-eindpunt waarmee verbinding wordt gemaakt." + }, + "request_timeout_ms": { + "label": "ZMQ-verzoektimeout in milliseconden", + "description": "Timeout voor ZMQ-verzoeken in milliseconden." + }, + "linger_ms": { + "label": "ZMQ-socket linger in milliseconden", + "description": "Socket linger-periode in milliseconden." + } + } + }, + "model": { + "label": "Detectie model", + "description": "Instellingen voor het configureren van een aangepast objectdetectiemodel en de invoervorm.", + "path": { + "label": "Pad naar aangepast objectdetectormodel", + "description": "Pad naar een aangepast detectiemodel (of plus:// voor Frigate+-modellen)." + }, + "labelmap_path": { + "label": "Labelmap voor aangepaste objectdetector", + "description": "Pad naar een labelmap-bestand dat numerieke klassen koppelt aan string-labels voor de detector." + }, + "width": { + "label": "Invoerbreedte objectdetectiemodel", + "description": "Breedte van de modelinvoertensor in pixels." + }, + "height": { + "label": "Invoerhoogte objectdetectiemodel", + "description": "Hoogte van de modelinvoertensor in pixels." + }, + "labelmap": { + "label": "Labelmap-aanpassing", + "description": "Overschrijvingen of herwijzingen om samen te voegen met de standaard labelmap." + }, + "attributes_map": { + "label": "Koppeling van objectlabels aan attribuutlabels", + "description": "Koppeling tussen objectlabels en attribuutlabels voor metadata (bijv. 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Invoertensorvorm van het model", + "description": "Tensorformaat dat het model verwacht: 'nhwc' of 'nchw'." + }, + "input_pixel_format": { + "label": "Invoerpixelkleurformaat van het model", + "description": "Pixelkleurruimte die het model verwacht: 'rgb', 'bgr' of 'yuv'." + }, + "input_dtype": { + "label": "Invoergegevenstype van het model", + "description": "Gegevenstype van de modelinvoertensor (bijv. 'float32')." + }, + "model_type": { + "label": "Modeltype voor objectdetectie", + "description": "Modelarchitectuurtype van de detector (ssd, yolox, yolonas) voor optimalisatie door sommige detectors." + } + }, + "genai": { + "label": "Generatieve AI-configuratie", + "description": "Instellingen voor geïntegreerde generatieve AI-providers voor het genereren van objectbeschrijvingen en beoordelingssamenvattingen.", + "api_key": { + "label": "API-sleutel", + "description": "API-sleutel vereist door sommige providers (kan ook via omgevingsvariabelen worden ingesteld)." + }, + "base_url": { + "label": "Basis-URL", + "description": "Basis-URL voor zelf-gehoste of compatibele providers (bijv. een Ollama-instantie)." + }, + "model": { + "label": "Model", + "description": "Het model van de provider voor het genereren van beschrijvingen of samenvattingen." + }, + "provider": { + "label": "Provider", + "description": "De te gebruiken GenAI-provider (bijv. ollama, gemini, openai)." + }, + "roles": { + "label": "Rollen", + "description": "GenAI-rollen (chat, beschrijvingen, inbeddingen); één provider per rol." + }, + "provider_options": { + "label": "Provideropties", + "description": "Aanvullende provider-specifieke opties voor de GenAI-client." + }, + "runtime_options": { + "label": "Runtime-opties", + "description": "Runtime-opties die bij elke inferentieaanroep aan de provider worden meegegeven." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg-instellingen inclusief binaire pad, argumenten, hardwareversnellingsopties en uitvoerargumenten per rol.", + "path": { + "label": "FFmpeg-pad", + "description": "Pad naar het te gebruiken FFmpeg-binaire bestand of een versie-alias (\"5.0\" of \"7.0\")." + }, + "global_args": { + "label": "FFmpeg globale argumenten", + "description": "Globale argumenten voor FFmpeg-processen." + }, + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten", + "description": "Hardwareversnellingsargumenten voor FFmpeg. Provider-specifieke presets worden aanbevolen." + }, + "input_args": { + "label": "Invoerargumenten", + "description": "Invoerargumenten voor FFmpeg-invoerstromen." + }, + "output_args": { + "label": "Uitvoerargumenten", + "description": "Standaard uitvoerargumenten voor verschillende FFmpeg-rollen zoals detectie en opname.", + "detect": { + "label": "Uitvoerargumenten voor detectie", + "description": "Standaard uitvoerargumenten voor streams met detectierol." + }, + "record": { + "label": "Uitvoerargumenten voor opname", + "description": "Standaard uitvoerargumenten voor streams met opnamerol." + } + }, + "retry_interval": { + "label": "FFmpeg-herverbindingstijd", + "description": "Seconden wachten voor een herverbindingspoging na een mislukte camerastream. Standaard is 10." + }, + "apple_compatibility": { + "label": "Apple-compatibiliteit", + "description": "HEVC-tagging inschakelen voor betere Apple-spelercompatibiliteit bij het opnemen van H.265." + }, + "gpu": { + "label": "GPU-index", + "description": "Standaard GPU-index voor hardwareversnelling indien beschikbaar." + }, + "inputs": { + "label": "Camera-invoer", + "description": "Lijst van invoerstream-definities (paden en rollen) voor deze camera.", + "path": { + "label": "Invoerpad", + "description": "URL of pad van de camera-invoerstroom." + }, + "roles": { + "label": "Invoerrollen", + "description": "Rollen voor deze invoerstroom." + }, + "global_args": { + "label": "FFmpeg globale argumenten", + "description": "FFmpeg globale argumenten voor deze invoerstroom." + }, + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten", + "description": "Hardwareversnellingsargumenten voor deze invoerstroom." + }, + "input_args": { + "label": "Invoerargumenten", + "description": "Invoerargumenten specifiek voor deze stream." + } + } + }, + "live": { + "label": "Live weergave", + "description": "Instellingen voor de jsmpeg-livestream-resolutie en -kwaliteit. Dit heeft geen invloed op gerestreamde camera's die go2rtc gebruiken voor live weergave.", + "streams": { + "label": "Live streamnamen", + "description": "Koppeling van geconfigureerde streamnamen aan restream/go2rtc-namen voor live weergave." + }, + "height": { + "label": "Live hoogte", + "description": "Hoogte (pixels) voor weergave van de jsmpeg-livestream in de webinterface; moet ≤ hoogte van de detectiestream zijn." + }, + "quality": { + "label": "Live kwaliteit", + "description": "Coderingskwaliteit voor de jsmpeg-stream (1 hoogste, 31 laagste)." + } + }, + "motion": { + "label": "Bewegingsdetectie", + "description": "Standaard bewegingsdetectie-instellingen die worden toegepast op camera's tenzij per camera overschreven.", + "enabled": { + "label": "Bewegingsdetectie inschakelen", + "description": "Bewegingsdetectie voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "threshold": { + "label": "Bewegingsdrempel", + "description": "Pixelverschildrempel voor de bewegingsdetector; hogere waarden verminderen de gevoeligheid (bereik 1-255)." + }, + "lightning_threshold": { + "label": "Bliksemdrempel", + "description": "Drempel om korte lichtflitsen te detecteren en te negeren (lager is gevoeliger, waarden tussen 0,3 en 1,0). Dit voorkomt bewegingsdetectie niet volledig; het zorgt er alleen voor dat de detector stopt met het analyseren van extra frames zodra de drempel wordt overschreden. Op beweging gebaseerde opnames worden tijdens deze gebeurtenissen nog steeds aangemaakt." + }, + "skip_motion_threshold": { + "label": "Drempel voor overgeslagen beweging", + "description": "Als ingesteld op een waarde tussen 0,0 en 1,0, en meer dan dit deel van het beeld verandert in één frame, geeft de detector geen bewegingsvakken terug en kalibreert hij direct opnieuw. Dit bespaart CPU en vermindert vals-positieven bij bliksem, stormen e.d., maar kan echte gebeurtenissen zoals PTZ-tracking missen. De afweging is tussen het weggooien van enkele megabytes opnames versus het bekijken van een paar korte clips. Leeg laten (None) om deze functie uit te schakelen." + }, + "improve_contrast": { + "label": "Contrast verbeteren", + "description": "Contrastverbetering op frames toepassen vóór bewegingsanalyse om detectie te verbeteren." + }, + "contour_area": { + "label": "Contouroppervlakte", + "description": "Minimale contouroppervlakte in pixels voor een bewegingscontour om te worden geteld." + }, + "delta_alpha": { + "label": "Delta-alfa", + "description": "Alpha-mengfactor voor frameverschil bij bewegingsberekening." + }, + "frame_alpha": { + "label": "Frame-alfa", + "description": "Alpha-waarde voor het mengen van frames bij bewegingsvoorverwerking." + }, + "frame_height": { + "label": "Framehoogte", + "description": "Hoogte in pixels waarnaar frames worden geschaald bij het berekenen van beweging." + }, + "mask": { + "label": "Maskercoördinaten", + "description": "Geordende x,y-coördinaten die het bewegingsmaskeerpolygoon definiëren voor het in- of uitsluiten van gebieden." + }, + "mqtt_off_delay": { + "label": "MQTT uit-vertraging", + "description": "Seconden wachten na de laatste beweging vóór publicatie van een MQTT 'off'-status." + }, + "enabled_in_config": { + "label": "Originele bewegingsstatus", + "description": "Geeft aan of bewegingsdetectie was ingeschakeld in de originele statische configuratie." + }, + "raw_mask": { + "label": "Onbewerkt masker" + } + }, + "objects": { + "label": "Objecten", + "description": "Standaardinstellingen voor objectvolging, inclusief te volgen labels en per-object filters.", + "track": { + "label": "Te volgen objecten", + "description": "Lijst met objectlabels om te volgen voor alle camera's; kan per camera worden overschreven." + }, + "filters": { + "label": "Objectfilters", + "description": "Filters op gedetecteerde objecten om vals-positieven te verminderen (oppervlakte, verhouding, betrouwbaarheid).", + "min_area": { + "label": "Minimale objectoppervlakte", + "description": "Minimale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn." + }, + "max_area": { + "label": "Maximale objectoppervlakte", + "description": "Maximale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn." + }, + "min_ratio": { + "label": "Minimale beeldverhouding", + "description": "Minimale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren." + }, + "max_ratio": { + "label": "Maximale beeldverhouding", + "description": "Maximale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren." + }, + "threshold": { + "label": "Betrouwbaarheidsdrempel", + "description": "Gemiddelde detectiebetrouwbaarheidsdrempel om een object als terecht positief te beschouwen." + }, + "min_score": { + "label": "Minimale betrouwbaarheid", + "description": "Minimale detectiebetrouwbaarheid in één frame om het object te tellen." + }, + "mask": { + "label": "Filtermasker", + "description": "Polygooncoördinaten die aangeven waar dit filter van toepassing is in het frame." + }, + "raw_mask": { + "label": "Onbewerkt masker" + } + }, + "mask": { + "label": "Objectmasker", + "description": "Maskeerpolygoon om objectdetectie in bepaalde gebieden te voorkomen." + }, + "raw_mask": { + "label": "Onbewerkt masker" + }, + "genai": { + "label": "GenAI-objectconfiguratie", + "description": "GenAI-opties voor het beschrijven van gevolgde objecten en het versturen van frames voor generatie.", + "enabled": { + "label": "GenAI inschakelen", + "description": "GenAI-beschrijvingen voor gevolgde objecten standaard inschakelen." + }, + "use_snapshot": { + "label": "Snapshots gebruiken", + "description": "Objectsnapshots gebruiken in plaats van miniaturen voor GenAI-beschrijving." + }, + "prompt": { + "label": "Bijschriftprompt", + "description": "Standaard promptsjabloon voor het genereren van beschrijvingen met GenAI." + }, + "object_prompts": { + "label": "Objectprompts", + "description": "Prompts per object voor het aanpassen van GenAI-uitvoer voor specifieke labels." + }, + "objects": { + "label": "GenAI-objecten", + "description": "Lijst van objectlabels die standaard naar GenAI worden gestuurd." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die objecten moeten betreden om in aanmerking te komen voor GenAI-beschrijving." + }, + "debug_save_thumbnails": { + "label": "Snapshots opslaan", + "description": "Snapshots die naar GenAI worden gestuurd opslaan voor foutopsporing." + }, + "send_triggers": { + "label": "GenAI-triggers", + "description": "Bepaalt wanneer frames naar GenAI worden gestuurd (bij einde, na updates, enz.).", + "tracked_object_end": { + "label": "Sturen bij beëindiging", + "description": "Een verzoek naar GenAI sturen wanneer het gevolgde object eindigt." + }, + "after_significant_updates": { + "label": "Vroege GenAI-trigger", + "description": "Een verzoek naar GenAI sturen na een bepaald aantal significante updates voor het gevolgde object." + } + }, + "enabled_in_config": { + "label": "Originele GenAI-status", + "description": "Geeft aan of GenAI was ingeschakeld in de originele statische configuratie." + } + } + }, + "record": { + "label": "Opname", + "description": "Opname- en bewaarinstellingen die worden toegepast op camera's tenzij per camera overschreven.", + "enabled": { + "label": "Opname inschakelen", + "description": "Opname voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "expire_interval": { + "label": "Opruiminterval opnames", + "description": "Minuten tussen opruimrondes die verlopen opnamesegmenten verwijderen." + }, + "continuous": { + "label": "Continue bewaring", + "description": "Aantal dagen om opnames te bewaren ongeacht gevolgde objecten of beweging. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.", + "days": { + "label": "Bewaardagen", + "description": "Dagen om opnames te bewaren." + } + }, + "motion": { + "label": "Bewegingsretentie", + "description": "Aantal dagen om opnames veroorzaakt door beweging te bewaren, ongeacht gevolgde objecten. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.", + "days": { + "label": "Bewaardagen", + "description": "Dagen om opnames te bewaren." + } + }, + "detections": { + "label": "Detectieretentie", + "description": "Opname-retentie-instellingen voor detectiegebeurtenissen inclusief pre/post-captureduur.", + "pre_capture": { + "label": "Seconden vóór opname", + "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname." + }, + "post_capture": { + "label": "Seconden na opname", + "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname." + }, + "retain": { + "label": "Gebeurtenisbewaring", + "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.", + "days": { + "label": "Bewaardagen", + "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + } + } + }, + "alerts": { + "label": "Meldingsbewaring", + "description": "Opname-retentie-instellingen voor alertgebeurtenissen inclusief pre/post-captureduur.", + "pre_capture": { + "label": "Seconden vóór opname", + "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname." + }, + "post_capture": { + "label": "Seconden na opname", + "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname." + }, + "retain": { + "label": "Gebeurtenisbewaring", + "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.", + "days": { + "label": "Bewaardagen", + "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + } + } + }, + "export": { + "label": "Exportconfiguratie", + "description": "Instellingen voor het exporteren van opnames, zoals timelapse en hardwareversnelling.", + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten voor export", + "description": "Hardwareversnellingsargumenten voor export/transcodering." + }, + "max_concurrent": { + "label": "Maximaal aantal gelijktijdige exports", + "description": "Maximum aantal exporttaken dat tegelijk wordt verwerkt." + } + }, + "preview": { + "label": "Voorbeeldconfiguratie", + "description": "Instellingen voor de kwaliteit van opnamevoorbeelden in de UI.", + "quality": { + "label": "Voorbeeldkwaliteit", + "description": "Kwaliteitsniveau voor voorbeelden (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Originele opnamestatus", + "description": "Geeft aan of opname was ingeschakeld in de originele statische configuratie." + } + }, + "review": { + "label": "Beoordeling", + "description": "Instellingen voor meldingen, detecties en GenAI-beoordelingssamenvattingen in de UI en opslag.", + "alerts": { + "label": "Meldingsconfiguratie", + "description": "Instellingen voor welke gevolgde objecten alerts genereren en hoe alerts worden bewaard.", + "enabled": { + "label": "Alerts inschakelen", + "description": "Genereren van meldingen voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "labels": { + "label": "Meldingslabels", + "description": "Lijst met objectlabels die kwalificeren als meldingen (bijv. auto, persoon)." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden om als melding te worden beschouwd; leeg laten voor elke zone." + }, + "enabled_in_config": { + "label": "Originele meldingsstatus", + "description": "Geeft aan of meldingen oorspronkelijk waren ingeschakeld in de statische configuratie." + }, + "cutoff_time": { + "label": "Afsluitingstijd meldingen", + "description": "Seconden wachten na het uitblijven van melding veroorzakende activiteit voordat een melding wordt afgesloten." + } + }, + "detections": { + "label": "Detectieconfiguratie", + "description": "Instellingen voor welke gevolgde objecten detecties genereren en hoe detecties worden bewaard.", + "enabled": { + "label": "Detecties inschakelen", + "description": "Detecties voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "labels": { + "label": "Detectielabels", + "description": "Lijst met objectlabels die kwalificeren als detectiegebeurtenissen." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden om als detectie te worden beschouwd; leeg laten voor elke zone." + }, + "cutoff_time": { + "label": "Afsluitingstijd detecties", + "description": "Seconden wachten na het uitblijven van detectie veroorzakende activiteit voordat een detectie wordt afgesloten." + }, + "enabled_in_config": { + "label": "Originele detectiestatus", + "description": "Geeft aan of detecties oorspronkelijk waren ingeschakeld in de statische configuratie." + } + }, + "genai": { + "label": "GenAI-configuratie", + "description": "Beheert het gebruik van generatieve AI voor het produceren van beschrijvingen en samenvattingen van beoordelingsitems.", + "enabled": { + "label": "GenAI-beschrijvingen inschakelen", + "description": "Door GenAI gegenereerde beschrijvingen en samenvattingen voor beoordelingsitems in- of uitschakelen." + }, + "alerts": { + "label": "GenAI inschakelen voor meldingen", + "description": "GenAI gebruiken voor het genereren van beschrijvingen bij meldingsitems." + }, + "detections": { + "label": "GenAI inschakelen voor detecties", + "description": "GenAI gebruiken voor het genereren van beschrijvingen bij detectiebeoordelingen." + }, + "image_source": { + "label": "Afbeeldingsbron voor beoordeling", + "description": "Bron van afbeeldingen naar GenAI ('preview' of 'recordings'); 'recordings' gebruikt hogere kwaliteit maar meer tokens." + }, + "additional_concerns": { + "label": "Aanvullende aandachtspunten", + "description": "Een lijst met aanvullende aandachtspunten die GenAI moet meenemen bij het beoordelen van activiteit op deze camera." + }, + "debug_save_thumbnails": { + "label": "Snapshots opslaan", + "description": "Snapshots die naar de GenAI-provider worden gestuurd opslaan voor foutopsporing." + }, + "enabled_in_config": { + "label": "Originele GenAI-status", + "description": "Geeft aan of GenAI-beoordeling oorspronkelijk was ingeschakeld in de statische configuratie." + }, + "preferred_language": { + "label": "Voorkeurstaal", + "description": "Voorkeurstaal voor gegenereerde antwoorden van de GenAI-provider." + }, + "activity_context_prompt": { + "label": "Activiteitscontextprompt", + "description": "Aangepaste prompt die beschrijft wat wel en niet verdachte activiteit is, als context voor GenAI-samenvattingen." + } + } + }, + "snapshots": { + "label": "Snapshots", + "description": "Instellingen voor API-gegenereerde snapshots van gevolgde objecten voor alle camera's; kan per camera worden overschreven.", + "enabled": { + "label": "Snapshots inschakelen", + "description": "Het opslaan van snapshots voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "timestamp": { + "label": "Tijdstempel-overlay", + "description": "Een tijdstempel op API-snapshots weergeven." + }, + "bounding_box": { + "label": "Detectiekader-overlay", + "description": "Detectiekaders voor gevolgde objecten tekenen op API-snapshots." + }, + "crop": { + "label": "Snapshot bijsnijden", + "description": "API-snapshots bijsnijden tot het detectiekader van het gedetecteerde object." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden voordat een snapshot wordt opgeslagen." + }, + "height": { + "label": "Snapshothoogte", + "description": "Hoogte (pixels) om API-snapshots naar te schalen; leeg laten om de originele grootte te behouden." + }, + "retain": { + "label": "Snapshot-bewaring", + "description": "Bewaarinstellingen voor snapshots inclusief standaarddagen en per-object overschrijvingen.", + "default": { + "label": "Standaard retentie", + "description": "Standaard aantal dagen om snapshots te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + }, + "objects": { + "label": "Objectbewaring", + "description": "Objectspecifieke overschrijvingen voor het aantal bewaardagen van snapshots." + } + }, + "quality": { + "label": "Snapshotkwaliteit", + "description": "Coderingskwaliteit voor opgeslagen snapshots (0-100)." + } + }, + "timestamp_style": { + "label": "Tijdstempelstijl", + "description": "Stijlopties voor tijdstempels in de feed, toegepast op de debugweergave en snapshots.", + "position": { + "label": "Tijdstempelpositie", + "description": "Positie van de tijdstempel op de afbeelding (tl/tr/bl/br)." + }, + "format": { + "label": "Tijdstempelformaat", + "description": "Datumtijdformaatstring voor tijdstempels (Python datetime-formaatcodes)." + }, + "color": { + "label": "Tijdstempelkleur", + "description": "RGB-kleurwaarden voor de tijdstempeltekst (alle waarden 0-255).", + "red": { + "label": "Rood", + "description": "Roodcomponent (0-255) voor de tijdstempelkleur." + }, + "green": { + "label": "Groen", + "description": "Groencomponent (0-255) voor de tijdstempelkleur." + }, + "blue": { + "label": "Blauw", + "description": "Blauwcomponent (0-255) voor de tijdstempelkleur." + } + }, + "thickness": { + "label": "Tijdstempeldikte", + "description": "Lijndikte van de tijdstempeltekst." + }, + "effect": { + "label": "Tijdstempeleffect", + "description": "Visueel effect voor de tijdstempeltekst (geen, effen, schaduw)." + } + }, + "classification": { + "label": "Objectclassificatie", + "description": "Instellingen voor classificatiemodellen die worden gebruikt om objectlabels of statusclassificatie te verfijnen.", + "bird": { + "label": "Vogelclassificatieconfiguratie", + "description": "Instellingen specifiek voor vogelclassificatiemodellen.", + "enabled": { + "label": "Vogelclassificatie", + "description": "Vogelclassificatie in- of uitschakelen." + }, + "threshold": { + "label": "Minimale score", + "description": "Minimale classificatiescore om een vogelclassificatie te accepteren." + } + }, + "custom": { + "label": "Aangepaste classificatiemodellen", + "description": "Configuratie voor aangepaste classificatiemodellen voor object- of statusdetectie.", + "enabled": { + "label": "Model inschakelen", + "description": "Het aangepaste classificatiemodel in- of uitschakelen." + }, + "name": { + "label": "Modelnaam", + "description": "Identifier van het te gebruiken aangepaste classificatiemodel." + }, + "threshold": { + "label": "Scoredrempel", + "description": "Scoredrempel voor het wijzigen van de classificatiestatus." + }, + "save_attempts": { + "label": "Opgeslagen pogingen", + "description": "Aantal classificatiepogingen dat wordt bijgehouden voor de recente classificaties in de UI." + }, + "object_config": { + "objects": { + "label": "Objecten classificeren", + "description": "Lijst van objecttypen waarop objectclassificatie wordt uitgevoerd." + }, + "classification_type": { + "label": "Classificatietype", + "description": "Toegepast classificatietype: 'sub_label' (voegt sub_label toe) of andere ondersteunde typen." + } + }, + "state_config": { + "cameras": { + "label": "Classificatiecamera's", + "description": "Per-camera bijsnijdinstellingen voor statusclassificatie.", + "crop": { + "label": "Classificatie-uitsnede", + "description": "Bijsnijdcoördinaten voor classificatie op deze camera." + } + }, + "motion": { + "label": "Uitvoeren bij beweging", + "description": "Indien ingeschakeld, classificatie uitvoeren wanneer beweging wordt gedetecteerd in het opgegeven bijsnijdgebied." + }, + "interval": { + "label": "Classificatie-interval", + "description": "Interval (seconden) tussen periodieke classificatierondes voor statusclassificatie." + } + } + } + }, + "semantic_search": { + "label": "Semantisch zoeken", + "description": "Instellingen voor semantisch zoeken, dat objectinbeddingen opbouwt en bevraagt om vergelijkbare items te vinden.", + "enabled": { + "label": "Semantisch zoeken inschakelen", + "description": "De semantisch zoeken-functie in- of uitschakelen." + }, + "reindex": { + "label": "Herindexeren bij opstarten", + "description": "Een volledige herindexering van historische gevolgde objecten in de inbeddingsdatabase starten." + }, + "model": { + "label": "Semantisch zoekmodel of GenAI-providernaam", + "description": "Het inbeddingsmodel voor semantisch zoeken (bijv. 'jinav1'), of de naam van een GenAI-provider met de inbeddingsrol." + }, + "model_size": { + "label": "Modelgrootte", + "description": "Selecteer modelgrootte; 'small' draait op CPU en 'large' vereist doorgaans een GPU." + }, + "device": { + "label": "Apparaat", + "description": "Dit is een overschrijving om een specifiek apparaat te targeten. Zie https://onnxruntime.ai/docs/execution-providers/ voor meer informatie" + }, + "triggers": { + "label": "Triggers", + "description": "Acties en matchcriteria voor cameraspecifieke semantisch-zoeken-triggers.", + "friendly_name": { + "label": "Weergavenaam", + "description": "Optionele weergavenaam voor deze trigger in de UI." + }, + "enabled": { + "label": "Trigger inschakelen", + "description": "Deze semantisch-zoeken-trigger in- of uitschakelen." + }, + "type": { + "label": "Triggertype", + "description": "Type trigger: 'thumbnail' (vergelijk met afbeelding) of 'description' (vergelijk met tekst)." + }, + "data": { + "label": "Triggerinhoud", + "description": "Tekstzin of miniatuur-ID om te vergelijken met gevolgde objecten." + }, + "threshold": { + "label": "Triggerdrempel", + "description": "Minimale gelijkenisscore (0-1) om deze trigger te activeren." + }, + "actions": { + "label": "Triggeracties", + "description": "Lijst van uit te voeren acties bij triggermatch (melding, sub_label, attribuut)." + } + } + }, + "face_recognition": { + "label": "Gezichtsherkenning", + "description": "Instellingen voor gezichtsdetectie en -herkenning voor alle camera's; kan per camera worden overschreven.", + "enabled": { + "label": "Gezichtsherkenning inschakelen", + "description": "Gezichtsherkenning voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "model_size": { + "label": "Modelgrootte", + "description": "Modelgrootte voor gezichtsinbeddingen (small/large); groter vereist mogelijk een GPU." + }, + "unknown_score": { + "label": "Drempel voor onbekende score", + "description": "Afstandsdrempel waaronder een gezicht als mogelijke match wordt beschouwd (hoger = strikter)." + }, + "detection_threshold": { + "label": "Detectiedrempel", + "description": "Minimale detectiebetrouwbaarheid om een gezichtsdetectie als geldig te beschouwen." + }, + "recognition_threshold": { + "label": "Herkenningsdrempel", + "description": "Gezichtsinbeddingsafstandsdrempel om twee gezichten als match te beschouwen." + }, + "min_area": { + "label": "Minimale gezichtsoppervlakte", + "description": "Minimale oppervlakte (pixels) van een gedetecteerd gezichtskader om herkenning te proberen." + }, + "min_faces": { + "label": "Minimum gezichten", + "description": "Minimum aantal gezichtsherkeningen vereist voordat een herkend sub-label aan een persoon wordt toegekend." + }, + "save_attempts": { + "label": "Opgeslagen pogingen", + "description": "Aantal gezichtsherkenningspogingen dat wordt bijgehouden voor de recente herkenningen in de UI." + }, + "blur_confidence_filter": { + "label": "Vaagheidsbetrouwbaarheidsfilter", + "description": "Betrouwbaarheidsscores aanpassen op basis van beeldvaagheid om vals-positieven bij slechte gezichtskwaliteit te verminderen." + }, + "device": { + "label": "Apparaat", + "description": "Dit is een overschrijving om een specifiek apparaat te targeten. Zie https://onnxruntime.ai/docs/execution-providers/ voor meer informatie" + } + }, + "lpr": { + "label": "Kentekenherkenning", + "description": "Instellingen voor kentekenherkenning inclusief detectiedrempels, opmaak en bekende kentekens.", + "enabled": { + "label": "LPR inschakelen", + "description": "Kentekenherkenning voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "model_size": { + "label": "Modelgrootte", + "description": "Modelgrootte voor tekstdetectie/-herkenning. De meeste gebruikers moeten 'small' gebruiken." + }, + "detection_threshold": { + "label": "Detectiedrempel", + "description": "Detectiebetrouwbaarheidsdrempel om OCR te starten op een vermoedelijk kenteken." + }, + "min_area": { + "label": "Minimale kentekenoppervlakte", + "description": "Minimale kentekenoppervlakte (pixels) om herkenning te proberen." + }, + "recognition_threshold": { + "label": "Herkenningsdrempel", + "description": "Betrouwbaarheidsdrempel voor herkende kentekentekst om als sub-label toe te voegen." + }, + "min_plate_length": { + "label": "Minimale kentekenlengte", + "description": "Minimum aantal tekens dat een herkend kenteken moet bevatten om geldig te zijn." + }, + "format": { + "label": "Regex voor kentekenformaat", + "description": "Optionele regex om herkende kentekens te valideren tegen een verwacht formaat." + }, + "match_distance": { + "label": "Overeenkomstafstand", + "description": "Aantal toegestane tekenfouten bij vergelijking van gedetecteerde kentekens met bekende kentekens." + }, + "known_plates": { + "label": "Bekende kentekens", + "description": "Lijst met kentekens of regex-patronen om specifiek te volgen of meldingen voor te genereren." + }, + "enhancement": { + "label": "Verbeteringsniveau", + "description": "Verbeteringsniveau (0-10) voor kentekenuitsneden vóór OCR; hogere waarden verbeteren niet altijd het resultaat; niveaus boven 5 werken mogelijk alleen voor nachtelijke kentekens en moeten voorzichtig worden gebruikt." + }, + "debug_save_plates": { + "label": "Kentekenplaten opslaan voor foutopsporing", + "description": "Kentekenuitsneden opslaan voor foutopsporing van LPR-prestaties." + }, + "device": { + "label": "Apparaat", + "description": "Dit is een overschrijving om een specifiek apparaat te targeten. Zie https://onnxruntime.ai/docs/execution-providers/ voor meer informatie" + }, + "replace_rules": { + "label": "Vervangingsregels", + "description": "Regex-vervangingsregels voor het normaliseren van gedetecteerde kentekenstrings vóór vergelijking.", + "pattern": { + "label": "Regex-patroon" + }, + "replacement": { + "label": "Vervangende tekst" + } + }, + "expire_time": { + "label": "Vervaltijd in seconden", + "description": "Tijd in seconden waarna een niet-gezien kenteken vervalt uit de tracker (alleen voor dedicated LPR-camera's)." + } + }, + "camera_groups": { + "label": "Cameragroepen", + "description": "Configuratie voor benoemde cameragroepen voor het organiseren van camera's in de UI.", + "cameras": { + "label": "Cameralijst", + "description": "Lijst met cameranamen in deze groep." + }, + "icon": { + "label": "Groepspictogram", + "description": "Pictogram voor de cameragroep in de UI." + }, + "order": { + "label": "Sorteervolgorde", + "description": "Numerieke volgorde voor het sorteren van cameragroepen in de UI; grotere nummers verschijnen later." + } + }, + "active_profile": { + "label": "Actief profiel", + "description": "Naam van het momenteel actieve profiel. Alleen runtime, wordt niet opgeslagen in YAML." + }, + "camera_mqtt": { + "label": "MQTT", + "description": "Instellingen voor het publiceren van MQTT-afbeeldingen.", + "enabled": { + "label": "Afbeelding versturen", + "description": "Het publiceren van afbeeldingssnapshots van objecten naar MQTT-topics voor deze camera inschakelen." + }, + "timestamp": { + "label": "Tijdstempel toevoegen", + "description": "Een tijdstempel op naar MQTT gepubliceerde afbeeldingen weergeven." + }, + "bounding_box": { + "label": "Detectiekader toevoegen", + "description": "Detectiekaders tekenen op via MQTT gepubliceerde afbeeldingen." + }, + "crop": { + "label": "Afbeelding bijsnijden", + "description": "Naar MQTT gepubliceerde afbeeldingen bijsnijden tot het detectiekader van het gedetecteerde object." + }, + "height": { + "label": "Afbeeldingshoogte", + "description": "Hoogte (pixels) voor het schalen van via MQTT gepubliceerde afbeeldingen." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden voordat een MQTT-afbeelding wordt gepubliceerd." + }, + "quality": { + "label": "JPEG-kwaliteit", + "description": "JPEG-kwaliteit voor naar MQTT gepubliceerde afbeeldingen (0-100)." + } + }, + "camera_ui": { + "label": "Camera-UI", + "description": "Weergavevolgorde en zichtbaarheid van deze camera in de UI. De volgorde heeft invloed op het standaarddashboard. Gebruik cameragroepen voor fijnere controle.", + "order": { + "label": "UI-volgorde", + "description": "Numerieke volgorde voor het sorteren van de camera in de UI (standaarddashboard en lijsten); grotere nummers verschijnen later." + }, + "dashboard": { + "label": "Tonen in UI", + "description": "Schakel de zichtbaarheid van deze camera overal in de Frigate-UI in of uit. Uitschakelen vereist handmatige aanpassing van de configuratie om de camera opnieuw te bekijken." + } + }, + "onvif": { + "label": "ONVIF", + "description": "ONVIF-verbindings- en PTZ-autovolgingsinstellingen voor deze camera.", + "host": { + "label": "ONVIF-host", + "description": "Host (en optioneel schema) voor de ONVIF-dienst van deze camera." + }, + "port": { + "label": "ONVIF-poort", + "description": "Poortnummer voor de ONVIF-dienst." + }, + "user": { + "label": "ONVIF-gebruikersnaam", + "description": "Gebruikersnaam voor ONVIF-authenticatie; sommige apparaten vereisen de admin-gebruiker voor ONVIF." + }, + "password": { + "label": "ONVIF-wachtwoord", + "description": "Wachtwoord voor ONVIF-authenticatie." + }, + "tls_insecure": { + "label": "TLS-verificatie uitschakelen", + "description": "TLS-verificatie overslaan en digest-authenticatie uitschakelen voor ONVIF (onveilig; alleen in veilige netwerken)." + }, + "profile": { + "label": "ONVIF-profiel", + "description": "Specifiek ONVIF-mediaprofiel voor PTZ-besturing, gekoppeld via token of naam. Indien niet ingesteld, wordt het eerste profiel met geldige PTZ-configuratie automatisch geselecteerd." + }, + "autotracking": { + "label": "Automatisch volgen", + "description": "Bewegende objecten automatisch volgen en gecentreerd houden in het beeld via PTZ-camerabewegingen.", + "enabled": { + "label": "Automatisch volgen inschakelen", + "description": "Automatisch PTZ-camera volgen van gedetecteerde objecten in- of uitschakelen." + }, + "calibrate_on_startup": { + "label": "Kalibreren bij opstarten", + "description": "PTZ-motorsnelheden meten bij opstarten voor nauwkeurigere volging. Frigate werkt de configuratie bij met movement_weights na kalibratie." + }, + "zooming": { + "label": "Zoommodus", + "description": "Zoomgedrag instellen: disabled (alleen pan/tilt), absolute (meest compatibel) of relative (gelijktijdig pan/tilt/zoom)." + }, + "zoom_factor": { + "label": "Zoomfactor", + "description": "Zoomniveau voor gevolgde objecten instellen. Lagere waarden tonen meer van de scène; hogere waarden zoomen verder in maar kunnen de volging verliezen. Waarden tussen 0,1 en 0,75." + }, + "track": { + "label": "Gevolgde objecten", + "description": "Lijst van objecttypen die automatisch volgen activeren." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Objecten moeten een van deze zones betreden voordat automatisch volgen begint." + }, + "return_preset": { + "label": "Terugkeer-voorinstelling", + "description": "ONVIF-voorkeuzeinstelling in de camerafirmware om naar terug te keren na het volgen." + }, + "timeout": { + "label": "Terugkeertimeout", + "description": "Dit aantal seconden wachten na het verliezen van de volging voordat de camera naar de voorkeuze-positie terugkeert." + }, + "movement_weights": { + "label": "Bewegingsgewichten", + "description": "Kalibratiewaarden automatisch gegenereerd door camerakalbratie. Niet handmatig aanpassen." + }, + "enabled_in_config": { + "label": "Originele autovolgstatus", + "description": "Intern veld om bij te houden of automatisch volgen was ingeschakeld in de configuratie." + } + }, + "ignore_time_mismatch": { + "label": "Tijdsverschil negeren", + "description": "Tijdsynchronisatieverschillen tussen camera en Frigate-server negeren voor ONVIF-communicatie." + } + } +} diff --git a/web/public/locales/nl/config/groups.json b/web/public/locales/nl/config/groups.json new file mode 100644 index 0000000..e69cd65 --- /dev/null +++ b/web/public/locales/nl/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Globale detectie", + "sensitivity": "Globale sensiviteit" + }, + "cameras": { + "detection": "Detectie", + "sensitivity": "Gevoeligheid" + } + }, + "motion": { + "global": { + "algorithm": "Globaal algoritme", + "sensitivity": "Globale gevoeligheid" + }, + "cameras": { + "sensitivity": "Gevoeligheid", + "algorithm": "Algoritme" + } + }, + "snapshots": { + "cameras": { + "display": "Weergave" + }, + "global": { + "display": "Globale weergave" + } + }, + "detect": { + "cameras": { + "resolution": "Resolutie", + "tracking": "Volgen" + }, + "global": { + "resolution": "Globale resolutie", + "tracking": "Globaal volgen" + } + }, + "objects": { + "cameras": { + "tracking": "Volgen", + "filtering": "Filteren" + }, + "global": { + "tracking": "Globaal volgen", + "filtering": "Globaal filteren" + } + }, + "timestamp_style": { + "global": { + "appearance": "Algemeen uiterlijk" + }, + "cameras": { + "appearance": "Voorkomen" + } + }, + "record": { + "global": { + "retention": "Globale retentie", + "events": "Globale gebeurtenissen" + }, + "cameras": { + "retention": "Retentie", + "events": "Gebeurtenissen" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Camera-specifieke FFmpeg argumenten" + } + } +} diff --git a/web/public/locales/nl/config/validation.json b/web/public/locales/nl/config/validation.json new file mode 100644 index 0000000..8f5391c --- /dev/null +++ b/web/public/locales/nl/config/validation.json @@ -0,0 +1,35 @@ +{ + "minimum": "Minimale waarde van {{limit}} vereist", + "maximum": "Mag niet meer dan {{limit}} bedragen", + "exclusiveMinimum": "Waarde moet groter zijn dan {{limit}}", + "exclusiveMaximum": "Moet minder zijn dan {{limit}}", + "minLength": "Moet minstens {{limit}} karakters zijn", + "maxLength": "Moet maximaal {{limit}} karakters zijn", + "minItems": "Moet minstens {{limit}} items hebben", + "maxItems": "Moet maximaal {{limit}} items hebben", + "pattern": "Ongeldig formaat", + "required": "Dit veld is vereist", + "type": "Ongeldig waarde type", + "enum": "Moet een van de toegestane waarden zijn", + "const": "Waarde komt niet overeen met de verwachte constante", + "uniqueItems": "Alle items moeten uniek zijn", + "format": "Ongeldig formaat", + "additionalProperties": "Onbekend kenmerk is niet toegestaan", + "oneOf": "Moet exact met een van de volgende schema's overeenkomen", + "anyOf": "Moet overeenkomen met minstens een van de toegestane schema's", + "ffmpeg": { + "inputs": { + "rolesUnique": "Elke rol kan slechts tot één input stroom worden toebedeeld.", + "detectRequired": "Minstens één input stroom moet toegewezen zijn aan de 'detectie' rol.", + "hwaccelDetectOnly": "Enkel de input stroom met de detectie rol kan hardwareversnelling argumenten instellen." + } + }, + "proxy": { + "header_map": { + "roleHeaderRequired": "Rol titel is vereist wanneer rol bindingen zijn geconfigureerd." + } + }, + "detect": { + "dimensionMustBeEven": "Het moet een even getal zijn." + } +} diff --git a/web/public/locales/nl/objects.json b/web/public/locales/nl/objects.json new file mode 100644 index 0000000..d95ecb8 --- /dev/null +++ b/web/public/locales/nl/objects.json @@ -0,0 +1,129 @@ +{ + "cat": "Kat", + "horse": "Paard", + "bird": "Vogel", + "bark": "Blaffen", + "goat": "Geit", + "sheep": "Schaap", + "animal": "Dier", + "mouse": "Muis", + "dog": "Hond", + "keyboard": "Klavier", + "person": "Persoon", + "airplane": "Vliegtuig", + "traffic_light": "Verkeerslicht", + "street_sign": "Verkeersbord", + "stop_sign": "Stopbord", + "parking_meter": "Parkeermeter", + "bench": "Bankje", + "cow": "Koe", + "giraffe": "Giraffe", + "hat": "Hoed", + "backpack": "Rugzak", + "shoe": "Schoen", + "baseball_bat": "Honkbalknuppel", + "baseball_glove": "Honkbalhandschoen", + "tennis_racket": "Tennis Racket", + "bottle": "Fles", + "plate": "Bord", + "fork": "Vork", + "spoon": "Lepel", + "bowl": "Schaal", + "sandwich": "Sandwich", + "broccoli": "Broccoli", + "hot_dog": "Hot Dog", + "cake": "Taart", + "chair": "Stoel", + "potted_plant": "Potplant", + "bed": "Bed", + "mirror": "Spiegel", + "laptop": "Laptop", + "cell_phone": "Mobiele telefoon", + "oven": "Oven", + "toaster": "Broodrooster", + "refrigerator": "Koelkast", + "book": "Boek", + "clock": "Klok", + "vase": "Vaas", + "toothbrush": "Tandenborstel", + "teddy_bear": "Teddybeer", + "vehicle": "Voertuig", + "squirrel": "Eekhoorn", + "deer": "Hert", + "fox": "Vos", + "rabbit": "Konijn", + "raccoon": "Wasbeer", + "waste_bin": "Afvalbak", + "on_demand": "Handmatige opnames", + "ups": "UPS", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD", + "skateboard": "Skateboard", + "boat": "Boot", + "scissors": "Schaar", + "bicycle": "Fiets", + "sink": "Wasbak", + "bus": "Bus", + "car": "Auto", + "motorcycle": "Motorfiets", + "train": "Trein", + "door": "Deur", + "blender": "Blender", + "hair_dryer": "Föhn", + "banana": "Banaan", + "umbrella": "Paraplu", + "suitcase": "Koffer", + "license_plate": "Kentekenplaat", + "orange": "Oranje", + "postnl": "PostNL", + "snowboard": "Snowboard", + "sports_ball": "Bal", + "donut": "Donut", + "couch": "Bank", + "package": "Pakket", + "dining_table": "Etenstafel", + "microwave": "Magnetron", + "toilet": "Toilet", + "cup": "Beker", + "carrot": "Wortel", + "eye_glasses": "Brillen", + "bear": "Beer", + "zebra": "Zebra", + "handbag": "Handtas", + "surfboard": "Surfplank", + "wine_glass": "Wijnglas", + "hair_brush": "Haarborstel", + "fire_hydrant": "Brandkraan", + "elephant": "Olifant", + "remote": "Op afstand", + "tie": "Stropdas", + "kite": "Vlieger", + "frisbee": "Frisbee", + "skis": "Ski's", + "desk": "Bureau", + "knife": "Mes", + "apple": "Appel", + "pizza": "Pizza", + "window": "Raam", + "fedex": "FedEx", + "tv": "TV", + "robot_lawnmower": "Robot grasmaaier", + "usps": "USPS", + "dhl": "DHL", + "bbq_grill": "BBQ-grill", + "amazon": "Amazon", + "face": "Gezicht", + "an_post": "An Post", + "purolator": "Purolator", + "kangaroo": "Kangoeroe", + "skunk": "Stinkdier", + "school_bus": "Schoolbus", + "royal_mail": "Royal Mail", + "canada_post": "Canada Post", + "baby": "Baby", + "baby_stroller": "Kinderwagen", + "rickshaw": "Riksja", + "rodent": "Knaagdier" +} diff --git a/web/public/locales/nl/views/chat.json b/web/public/locales/nl/views/chat.json new file mode 100644 index 0000000..d4ffad1 --- /dev/null +++ b/web/public/locales/nl/views/chat.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "Chat - Frigate", + "placeholder": "Stel een vraag...", + "error": "Er is iets misgegaan. Probeer opnieuw.", + "processing": "Verwerken...", + "toolsUsed": "Gebruikt: {{tools}}", + "hideTools": "Gereedschap verbergen", + "call": "Rinkel", + "title": "Frigate Chat", + "subtitle": "Jouw AI assistent voor camera beheer en inzichten", + "result": "Uitkomst", + "arguments": "Argumenten:", + "response": "Antwoord:", + "attachment_chip_remove": "Verwijder bijlage", + "open_in_explore": "Openen in Verken", + "showTools": "Gereedschap tonen" +} diff --git a/web/public/locales/nl/views/classificationModel.json b/web/public/locales/nl/views/classificationModel.json new file mode 100644 index 0000000..c58fc60 --- /dev/null +++ b/web/public/locales/nl/views/classificationModel.json @@ -0,0 +1,199 @@ +{ + "documentTitle": "Classificatiemodellen - Frigate", + "button": { + "deleteClassificationAttempts": "Classificatieafbeeldingen verwijderen", + "renameCategory": "Klasse hernoemen", + "deleteCategory": "Klasse verwijderen", + "deleteImages": "Afbeeldingen verwijderen", + "trainModel": "Model trainen", + "addClassification": "Classificatie toevoegen", + "deleteModels": "Modellen verwijderen", + "editModel": "Model bewerken" + }, + "toast": { + "success": { + "deletedCategory_one": "Verwijderd {{count}} klasse", + "deletedCategory_other": "Verwijderde {{count}} klassen", + "deletedImage_one": "Verwijderde {{count}} afbeelding", + "deletedImage_other": "Verwijderde {{count}} afbeeldingen", + "categorizedImage": "Succesvol geclassificeerde afbeelding", + "trainedModel": "Succesvol getraind model.", + "trainingModel": "Modeltraining succesvol gestart.", + "deletedModel_one": "{{count}} model succesvol verwijderd", + "deletedModel_other": "{{count}} modellen succesvol verwijderd", + "updatedModel": "Modelconfiguratie succesvol bijgewerkt", + "renamedCategory": "Klasse succesvol hernoemd naar {{name}}", + "reclassifiedImage": "Afbeelding succesvol opnieuw geclassificeerd" + }, + "error": { + "deleteImageFailed": "Verwijderen mislukt: {{errorMessage}}", + "deleteCategoryFailed": "Het verwijderen van de klasse is mislukt: {{errorMessage}}", + "categorizeFailed": "Afbeelding categoriseren mislukt: {{errorMessage}}", + "trainingFailed": "Modeltraining mislukt. Raadpleeg de Frigate-logs voor details.", + "deleteModelFailed": "Model verwijderen mislukt: {{errorMessage}}", + "updateModelFailed": "Bijwerken van model mislukt: {{errorMessage}}", + "renameCategoryFailed": "Hernoemen van klasse mislukt: {{errorMessage}}", + "trainingFailedToStart": "Het is niet gelukt om het model te trainen: {{errorMessage}}", + "reclassifyFailed": "Opnieuw classificeren van afbeelding mislukt: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Klasse verwijderen", + "desc": "Weet je zeker dat je de klasse {{name}} wilt verwijderen? Hiermee worden alle bijbehorende afbeeldingen permanent verwijderd en moet het model opnieuw worden getraind.", + "minClassesTitle": "Kan klasse niet verwijderen", + "minClassesDesc": "Een classificatiemodel moet minimaal twee klassen hebben. Voeg een andere klasse toe voordat u deze verwijdert." + }, + "deleteDatasetImages": { + "title": "Datasetafbeeldingen verwijderen", + "desc_one": "Weet u zeker dat u {{count}} afbeelding uit {{dataset}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt en vereist een hertraining van het model.", + "desc_other": "Weet u zeker dat u {{count}} afbeeldingen uit {{dataset}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt en vereist een hertraining van het model." + }, + "deleteTrainImages": { + "title": "Trainingsafbeeldingen verwijderen", + "desc_one": "Weet je zeker dat je {{count}} afbeelding wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", + "desc_other": "Weet je zeker dat je {{count}} afbeeldingen wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt." + }, + "renameCategory": { + "title": "Klasse hernoemen", + "desc": "Voer een nieuwe naam in voor {{name}}. U moet het model opnieuw trainen om de naamswijziging door te voeren." + }, + "description": { + "invalidName": "Ongeldige naam. Namen mogen alleen letters, cijfers, spaties, apostroffen, underscores en koppeltekens bevatten." + }, + "train": { + "title": "Recente classificaties", + "aria": "Selecteer recente classificaties", + "titleShort": "Recent" + }, + "categories": "Klassen", + "createCategory": { + "new": "Nieuwe klasse maken" + }, + "categorizeImageAs": "Afbeelding classificeren als:", + "categorizeImage": "Afbeelding classificeren", + "noModels": { + "object": { + "title": "Geen objectclassificatiemodellen", + "description": "Maak een aangepast model om gedetecteerde objecten te classificeren.", + "buttonText": "Objectmodel maken" + }, + "state": { + "title": "Geen status-classificatiemodellen", + "description": "Maak een aangepast model om statuswijzigingen in specifieke cameragebieden te monitoren en te classificeren.", + "buttonText": "Maak een statusmodel" + } + }, + "wizard": { + "title": "Nieuwe classificatie maken", + "steps": { + "nameAndDefine": "Naam & definiëren", + "stateArea": "Staatsgebied", + "chooseExamples": "Voorbeelden kiezen" + }, + "step1": { + "description": "Statusmodellen houden vaste cameragebieden in de gaten op veranderingen (bijv. deur open/dicht). Objectmodellen voegen classificaties toe aan gedetecteerde objecten (bijv. bekende dieren, bezorgers, enz.).", + "name": "Naam", + "namePlaceholder": "Voer modelnaam in...", + "type": "Type", + "typeState": "Staat", + "typeObject": "Object", + "objectLabel": "Objectlabel", + "objectLabelPlaceholder": "Selecteer objecttype...", + "classificationType": "Classificatietype", + "classificationTypeTip": "Leer meer over classificatietypen", + "classificationTypeDesc": "Sublabels voegen extra tekst toe aan het objectlabel (bijv. ‘Persoon: UPS’). Attributen zijn doorzoekbare metadata die apart in de objectmetadata worden opgeslagen.", + "classificationSubLabel": "Sublabel", + "classificationAttribute": "Attribuut", + "classes": "Klassen", + "classesTip": "Meer over klassen leren", + "classesStateDesc": "Definieer de verschillende staten waarin uw cameragebied zich kan bevinden. Bijvoorbeeld: ‘open’ en ‘gesloten’ voor een garagedeur.", + "classesObjectDesc": "Definieer de verschillende categorieën om gedetecteerde objecten in te classificeren. Bijvoorbeeld: ‘bezorger’, ‘bewoner’, ‘vreemdeling’ voor persoonsclassificatie.", + "classPlaceholder": "Voer klassenaam in...", + "errors": { + "nameRequired": "Modelnaam is vereist", + "nameLength": "De modelnaam mag maximaal 64 tekens lang zijn", + "nameOnlyNumbers": "Modelnaam mag niet alleen uit cijfers bestaan", + "classRequired": "Minimaal 1 klasse is vereist", + "classesUnique": "Klassennamen moeten uniek zijn", + "stateRequiresTwoClasses": "Statusmodellen vereisen minimaal 2 klassen", + "objectLabelRequired": "Selecteer een objectlabel", + "objectTypeRequired": "Selecteer een classificatietype", + "noneNotAllowed": "De klasse 'none' is niet toegestaan" + }, + "states": "Staten" + }, + "step2": { + "description": "Selecteer camera’s en definieer voor elke camera het te monitoren gebied. Het model zal de status van deze gebieden classificeren.", + "cameras": "Camera's", + "selectCamera": "Selecteer camera", + "noCameras": "Klik op + om camera’s toe te voegen", + "selectCameraPrompt": "Selecteer een camera uit de lijst om het te monitoren gebied te definiëren" + }, + "step3": { + "selectImagesPrompt": "Selecteer alle afbeeldingen met: {{className}}", + "selectImagesDescription": "Klik op afbeeldingen om ze te selecteren. Klik op doorgaan wanneer je klaar bent met deze klasse.", + "generating": { + "title": "Voorbeeldafbeeldingen genereren", + "description": "Frigate haalt representatieve afbeeldingen uit je opnames. Dit kan even duren..." + }, + "training": { + "title": "Model trainen", + "description": "Je model wordt op de achtergrond getraind. Sluit dit venster, en je model zal starten zodra de training is voltooid." + }, + "retryGenerate": "Generatie opnieuw proberen", + "noImages": "Geen voorbeeldafbeeldingen gegenereerd", + "classifying": "Classificeren en trainen...", + "trainingStarted": "Training succesvol gestart", + "errors": { + "noCameras": "Geen camera’s geconfigureerd", + "noObjectLabel": "Geen objectlabel geselecteerd", + "generateFailed": "Genereren van voorbeelden mislukt: {{error}}", + "generationFailed": "Generatie mislukt. Probeer het opnieuw.", + "classifyFailed": "Afbeeldingen classificeren mislukt: {{error}}" + }, + "generateSuccess": "Met succes gegenereerde voorbeeldafbeeldingen", + "allImagesRequired_one": "Classificeer alle afbeeldingen. {{count}} afbeelding resterend.", + "allImagesRequired_other": "Classificeer alle afbeeldingen. {{count}} afbeeldingen resterend.", + "modelCreated": "Model succesvol aangemaakt. Gebruik de weergave Recente classificaties om afbeeldingen voor ontbrekende statussen toe te voegen en train vervolgens het model.", + "missingStatesWarning": { + "title": "Ontbrekende klassevoorbeelden", + "description": "Niet alle klassen hebben voorbeelden. Probeer nieuwe voorbeelden te genereren om de ontbrekende klasse te vinden, of ga verder en gebruik de weergave 'Recente classificaties' om later afbeeldingen toe te voegen." + }, + "refreshExamples": "Nieuwe voorbeelden genereren", + "refreshConfirm": { + "title": "Nieuwe voorbeelden genereren?", + "description": "Dit genereert een nieuwe set afbeeldingen en wist alle selecties, inclusief eerdere klassen. Je moet opnieuw voorbeelden selecteren voor alle klassen." + } + } + }, + "deleteModel": { + "title": "Classificatiemodel verwijderen", + "single": "Weet u zeker dat u {{name}} wilt verwijderen? Hiermee worden alle bijbehorende gegevens, inclusief afbeeldingen en trainingsgegevens, definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt.", + "desc_one": "Weet u zeker dat u {{count}} model wilt verwijderen? Hiermee worden alle bijbehorende gegevens, inclusief afbeeldingen en trainingsgegevens, permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt.", + "desc_other": "Weet u zeker dat u {{count}} modellen wilt verwijderen? Hiermee worden alle bijbehorende gegevens, inclusief afbeeldingen en trainingsgegevens, permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt." + }, + "menu": { + "objects": "Objecten", + "states": "Staten" + }, + "details": { + "scoreInfo": "Score geeft het gemiddelde classificatievertrouwen weer over alle detecties van dit object.", + "none": "Geen overeenkomst", + "unknown": "Onbekend" + }, + "edit": { + "title": "Classificatiemodel bewerken", + "descriptionState": "Bewerk de klassen voor dit statusclassificatiemodel. Wijzigingen vereisen dat het model opnieuw wordt getraind.", + "descriptionObject": "Bewerk het objecttype en het classificatietype voor dit objectclassificatiemodel.", + "stateClassesInfo": "Let op: het wijzigen van statusklassen vereist dat het model opnieuw wordt getraind met de bijgewerkte klassen." + }, + "tooltip": { + "trainingInProgress": "Model is momenteel aan het trainen", + "noNewImages": "Geen nieuwe afbeeldingen om te trainen. Classificeer eerst meer afbeeldingen in de dataset.", + "modelNotReady": "Model is niet klaar voor training", + "noChanges": "Geen wijzigingen in de dataset sinds de laatste training." + }, + "none": "Geen overeenkomst", + "reclassifyImageAs": "Afbeelding opnieuw classificeren als:", + "reclassifyImage": "Afbeelding opnieuw classificeren" +} diff --git a/web/public/locales/nl/views/configEditor.json b/web/public/locales/nl/views/configEditor.json new file mode 100644 index 0000000..50a146c --- /dev/null +++ b/web/public/locales/nl/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Configuratie-bewerken - Frigate", + "copyConfig": "Configuratie kopiëren", + "saveAndRestart": "Opslaan en opnieuw opstarten", + "toast": { + "error": { + "savingError": "Fout bij het opslaan van de configuratie" + }, + "success": { + "copyToClipboard": "Configuratie gekopieerd naar klembord." + } + }, + "configEditor": "Configuratie Bewerken", + "saveOnly": "Alleen opslaan", + "confirm": "Afsluiten zonder op te slaan?", + "safeConfigEditor": "Configuratie-editor (veilige modus)", + "safeModeDescription": "Frigate is in veilige modus vanwege een configuratievalidatiefout." +} diff --git a/web/public/locales/nl/views/events.json b/web/public/locales/nl/views/events.json new file mode 100644 index 0000000..ff2d687 --- /dev/null +++ b/web/public/locales/nl/views/events.json @@ -0,0 +1,69 @@ +{ + "documentTitle": "Beoordelen - Frigate", + "camera": "Camera", + "newReviewItems": { + "button": "Nieuwe items om te beoordelen", + "label": "Bekijk nieuwe beoordelingsitems" + }, + "timeline.aria": "Selecteer tijdlijn", + "recordings": { + "documentTitle": "Opnamen - Frigate" + }, + "timeline": { + "label": "Tijdslijn" + }, + "empty": { + "alert": "Er zijn geen meldingen om te beoordelen", + "detection": "Er zijn geen detecties om te beoordelen", + "motion": "Geen bewegingsgegevens gevonden", + "recordingsDisabled": { + "title": "Opnames moeten zijn ingeschakeld", + "description": "Beoordelingsitems kunnen alleen voor een camera worden aangemaakt als opnames voor die camera zijn ingeschakeld." + } + }, + "events": { + "aria": "Selecteer activiteiten", + "noFoundForTimePeriod": "Er zijn geen activiteiten gevonden voor deze periode.", + "label": "Activiteiten" + }, + "calendarFilter": { + "last24Hours": "Laatste 24 uur" + }, + "alerts": "Meldingen", + "motion": { + "label": "Bewegingen", + "only": "Alleen bewegingen" + }, + "allCameras": "Alle camera's", + "markAsReviewed": "Markeren als beoordeeld", + "detections": "Detecties", + "markTheseItemsAsReviewed": "Markeer deze items als beoordeeld", + "selected_other": "{{count}} geselecteerd", + "selected_one": "{{count}} geselecteerd", + "detected": "gedetecteerd", + "suspiciousActivity": "Verdachte activiteit", + "threateningActivity": "Bedreigende activiteit", + "detail": { + "noDataFound": "Geen gedetailleerde gegevens om te beoordelen", + "aria": "Detailweergave in- of uitschakelen", + "trackedObject_one": "{{count}} object", + "trackedObject_other": "{{count}} objecten", + "noObjectDetailData": "Geen objectdetails beschikbaar.", + "label": "Detail", + "settings": "Instellingen voor detailweergave", + "alwaysExpandActive": { + "desc": "Altijd de objectdetails van het actieve beoordelingsitem uitklappen wanneer deze beschikbaar zijn.", + "title": "Het huidige item altijd uitvouwen" + } + }, + "objectTrack": { + "trackedPoint": "Gevolgd punt", + "clickToSeek": "Klik om naar deze tijd te zoeken" + }, + "zoomIn": "Zoom in", + "zoomOut": "Zoom uit", + "normalActivity": "Normaal", + "needsReview": "Heeft een beoordeling nodig", + "securityConcern": "Beveiligingsprobleem", + "select_all": "Alle" +} diff --git a/web/public/locales/nl/views/explore.json b/web/public/locales/nl/views/explore.json new file mode 100644 index 0000000..9111390 --- /dev/null +++ b/web/public/locales/nl/views/explore.json @@ -0,0 +1,317 @@ +{ + "generativeAI": "Generatieve AI", + "exploreIsUnavailable": { + "embeddingsReindexing": { + "finishingShortly": "Bijna klaar", + "step": { + "trackedObjectsProcessed": "Gevolgde objecten verwerkt: ", + "descriptionsEmbedded": "Beschrijving ingesloten: ", + "thumbnailsEmbedded": "Thumbnails ingesloten: " + }, + "context": "Verkennen kan worden gebruikt nadat de embeddings van gevolgde objecten opnieuw zijn geïndexeerd.", + "estimatedTime": "Geschatte resterende tijd:", + "startingUp": "Opstarten…" + }, + "downloadingModels": { + "setup": { + "textTokenizer": "Teksttokenizer", + "visionModel": "Visiemodel", + "visionModelFeatureExtractor": "kenmerkextractie van een visiemodel", + "textModel": "Tekstmodel" + }, + "error": "Er is iets misgegaan. Raadpleeg de Frigate-logs.", + "context": "Frigate downloadt de vereiste inbedmodellen om de functie Semantisch zoeken te ondersteunen. Dit kan enkele minuten duren, afhankelijk van de snelheid van je netwerkverbinding.", + "tips": { + "context": "Je wilt misschien de embeddings van je gevolgde objecten opnieuw indexeren zodra de modellen zijn gedownload.", + "documentation": "Lees de documentatie" + } + }, + "title": "Verkennen is niet beschikbaar" + }, + "trackedObjectDetails": "Details getraceerd object", + "type": { + "details": "Details", + "video": "video", + "snapshot": "snapshot", + "object_lifecycle": "objectlevenscyclus", + "thumbnail": "thumbnail", + "tracking_details": "trackinggegevens" + }, + "objectLifecycle": { + "createObjectMask": "Objectmasker maken", + "lifecycleItemDesc": { + "visible": "{{label}} Gedetecteerd", + "entered_zone": "{{label}} in zone {{zones}}", + "attribute": { + "other": "{{label}} Herkend als {{attribute}}", + "faceOrLicense_plate": "{{attribute}} Gedetecteerd voor {{label}}" + }, + "heard": "{{label}} gehoord", + "gone": "{{label}} is vertrokken", + "active": "{{label}} Werd actief", + "stationary": "{{label}} werd stationair", + "external": "{{label}} gedetecteerd", + "header": { + "zones": "Zones", + "ratio": "Verhouding", + "area": "Gebied" + } + }, + "annotationSettings": { + "title": "Annotatie-instellingen", + "offset": { + "millisecondsToOffset": "Aantal milliseconden om objectkader mee te verschuiven. Standaard: 0", + "desc": "Deze gegevens zijn afkomstig van de detectiestroom van je camera, maar worden weergegeven op beelden uit de opnamestroom. Het is onwaarschijnlijk dat deze twee streams perfect gesynchroniseerd zijn. Hierdoor zullen het objectkader en het beeld niet exact op elkaar aansluiten. Het veld annotation_offset kan echter worden gebruikt om deze annotatie-afwijking te corrigeren.", + "documentation": "Lees de documentatie ", + "label": "Annotatie-afwijking", + "tips": "TIP: Stel je voor dat er een clip is waarin een persoon van links naar rechts loopt. Als het objectkader in de tijdlijn van het object steeds links van de persoon ligt, dan moet de waarde verlaagd worden. Op dezelfde manier als het objectkader consequent vóór de persoon ligt dus vooruitloopt, moet de waarde verhoogd worden.", + "toast": { + "success": "Annotatieverschuiving voor {{camera}} is opgeslagen in het configuratiebestand. Herstart Frigate om je wijzigingen toe te passen." + } + }, + "showAllZones": { + "title": "Toon alle zones", + "desc": "Toon altijd zones op frames waar objecten een zone zijn binnengegaan." + } + }, + "noImageFound": "Er is geen afbeelding beschikbaar voor dit tijdstip.", + "title": "Objectlevenscyclus", + "adjustAnnotationSettings": "Annotatie-instellingen aanpassen", + "scrollViewTips": "Scroll om de belangrijke momenten uit de levenscyclus van dit object te bekijken.", + "autoTrackingTips": "Als u een automatische objectvolgende camera gebruikt, zal het objectkader onnauwkeurig zijn.", + "carousel": { + "previous": "Vorige dia", + "next": "Volgende dia" + }, + "count": "{{first}} van {{second}}", + "trackedPoint": "Volgpunt" + }, + "documentTitle": "Verken - Frigate", + "details": { + "item": { + "button": { + "viewInExplore": "Bekijk in Verkennen", + "share": "Deel dit beoordelingsitem" + }, + "title": "Details van item bekijken", + "desc": "Details van item bekijken", + "tips": { + "hasMissingObjects": "Pas je configuratie aan als je wilt dat Frigate gevolgde objecten opslaat voor de volgende labels: {{objects}}", + "mismatch_one": "{{count}} Niet-beschikbaar object werd gedetecteerd en opgenomen in dit beoordelingsitem. Het voldeed mogelijk niet aan de criteria voor een waarschuwing of detectie, of is inmiddels opgeschoond of verwijderd.", + "mismatch_other": "{{count}} Niet-beschikbare objecten zijn gedetecteerd en opgenomen in dit beoordelingsitem. Deze objecten voldeden mogelijk niet aan de voorwaarden voor een waarschuwing of detectie, of zijn inmiddels verwijderd of opgeruimd." + }, + "toast": { + "success": { + "regenerate": "Er is een nieuwe beschrijving aangevraagd bij {{provider}}. Afhankelijk van de snelheid van je provider kan het regenereren van de nieuwe beschrijving enige tijd duren.", + "updatedSublabel": "Sublabel succesvol bijgewerkt.", + "updatedLPR": "Kenteken succesvol bijgewerkt.", + "audioTranscription": "Audio-transcriptie succesvol aangevraagd. Afhankelijk van de snelheid van uw Frigate-server kan het even duren voordat de transcriptie voltooid is.", + "updatedAttributes": "Attributen succesvol bijgewerkt." + }, + "error": { + "updatedSublabelFailed": "Het is niet gelukt om het sublabel bij te werken: {{errorMessage}}", + "regenerate": "Het is niet gelukt om {{provider}} aan te roepen voor een nieuwe beschrijving: {{errorMessage}}", + "updatedLPRFailed": "Kentekenplaat bijwerken mislukt: {{errorMessage}}", + "audioTranscription": "Audiotranscriptie aanvragen mislukt: {{errorMessage}}", + "updatedAttributesFailed": "Attributen konden niet worden bijgewerkt: {{errorMessage}}" + } + } + }, + "label": "Label", + "editSubLabel": { + "title": "Sublabel bewerken", + "descNoLabel": "Voer een nieuw sublabel in voor dit gevolgde object", + "desc": "Voer een nieuw sublabel in voor deze {{label}}" + }, + "topScore": { + "label": "Hoogste score", + "info": "De topscore is de hoogste mediaanscore voor het gevolgde object, waardoor deze kan afwijken van de score die wordt weergegeven op de thumbnail van het zoekresultaat." + }, + "objects": "Objecten", + "zones": "Zones", + "button": { + "findSimilar": "Vind vergelijkbare", + "regenerate": { + "label": "Regenereer de beschrijving van het gevolgde object", + "title": "Regenereren" + } + }, + "description": { + "placeholder": "Beschrijving van het gevolgde object", + "label": "Beschrijving", + "aiTips": "Frigate vraagt pas om een beschrijving van uw Generative AI-provider als de levenscyclus van het gevolgde object is afgelopen." + }, + "expandRegenerationMenu": "Regeneratiemenu uitbreiden", + "regenerateFromSnapshot": "Regenereren vanuit Snapshot", + "tips": { + "descriptionSaved": "Beschrijving succesvol opgeslagen", + "saveDescriptionFailed": "Het is niet gelukt om de beschrijving bij te werken: {{errorMessage}}" + }, + "timestamp": "Tijdstempel", + "regenerateFromThumbnails": "Regeneratie van Thumbnails", + "camera": "Camera", + "estimatedSpeed": "Geschatte snelheid", + "editLPR": { + "title": "Kenteken bewerken", + "desc": "Voer een nieuwe kentekenwaarde in voor deze {{label}}", + "descNoLabel": "Voer een nieuwe kentekenwaarde in voor dit gevolgde object" + }, + "recognizedLicensePlate": "Erkende kentekenplaat", + "snapshotScore": { + "label": "Snapshot score" + }, + "score": { + "label": "Score" + }, + "editAttributes": { + "title": "Bewerk attributen", + "desc": "Selecteer classificatiekenmerken voor dit {{label}}" + }, + "attributes": "Classificatie-kenmerken", + "title": { + "label": "Titel" + }, + "scoreInfo": "Informatie over de score" + }, + "itemMenu": { + "downloadVideo": { + "label": "Download video", + "aria": "Download video" + }, + "viewObjectLifecycle": { + "aria": "Toon de levenscyclus van het object", + "label": "Levenscyclus van object bekijken" + }, + "submitToPlus": { + "label": "Verzenden naar Frigate+", + "aria": "Verzenden naar Frigate Plus" + }, + "viewInHistory": { + "aria": "Bekijk in Geschiedenis", + "label": "Bekijk in Geschiedenis" + }, + "deleteTrackedObject": { + "label": "Verwijder dit gevolgde object" + }, + "findSimilar": { + "label": "Vind vergelijkbare", + "aria": "Vind vergelijkbare gevolgde objecten" + }, + "downloadSnapshot": { + "label": "Download snapshot", + "aria": "Download snapshot" + }, + "addTrigger": { + "label": "Trigger toevoegen", + "aria": "Voeg een trigger toe voor dit gevolgde object" + }, + "audioTranscription": { + "label": "Transcriberen", + "aria": "Audiotranscriptie aanvragen" + }, + "showObjectDetails": { + "label": "Objectpad weergeven" + }, + "hideObjectDetails": { + "label": "Verberg objectpad" + }, + "viewTrackingDetails": { + "label": "Bekijk trackinggegevens", + "aria": "Toon de trackinggegevens" + }, + "downloadCleanSnapshot": { + "label": "Download schone snapshot", + "aria": "Download schone snapshot" + }, + "debugReplay": { + "label": "Debug-herhaling", + "aria": "Bekijk dit gevolgde object in de weergave voor het afspelen van foutopsporing" + }, + "more": { + "aria": "Meer" + } + }, + "noTrackedObjects": "Geen gevolgde objecten gevonden", + "trackedObjectsCount_one": "{{count}} gevolgd object ", + "trackedObjectsCount_other": "{{count}} gevolgde objecten ", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Het gevolgde object is succesvol verwijderd.", + "error": "Verwijderen van gevolgd object mislukt: {{errorMessage}}" + } + }, + "tooltip": "{{type}} komt voor {{confidence}}% overeen met de zoekopdracht", + "previousTrackedObject": "Vorig gevolgd object", + "nextTrackedObject": "Volgende gevolgde object" + }, + "dialog": { + "confirmDelete": { + "title": "Bevestig Verwijderen", + "desc": "Het verwijderen van dit gevolgde object verwijdert de snapshot, alle opgeslagen embeddings en eventuele bijbehorende trackinggegevens van het object. Opgenomen videobeelden van dit object in de Geschiedenisweergave worden NIET verwijderd.

    Weet je zeker dat je wilt doorgaan?" + }, + "toast": { + "error": "Fout bij het verwijderen van dit bijgehouden object: {{errorMessage}}" + } + }, + "fetchingTrackedObjectsFailed": "Fout bij het ophalen van gevolgde objecten: {{errorMessage}}", + "exploreMore": "Verken meer {{label}} objecten", + "aiAnalysis": { + "title": "AI-analyse" + }, + "concerns": { + "label": "Zorgen" + }, + "trackingDetails": { + "title": "Trackinggegevens", + "noImageFound": "Er is geen afbeelding beschikbaar voor dit tijdstip.", + "createObjectMask": "Objectmasker maken", + "adjustAnnotationSettings": "Annotatie-instellingen aanpassen", + "scrollViewTips": "Klik om de belangrijke momenten uit de levenscyclus van dit object te bekijken.", + "autoTrackingTips": "Als u een automatische objectvolgende camera gebruikt, zal het objectkader onnauwkeurig zijn.", + "count": "{{first}} van {{second}}", + "trackedPoint": "Volgpunt", + "lifecycleItemDesc": { + "visible": "{{label}} gedetecteerd", + "entered_zone": "{{label}} in zone {{zones}}", + "active": "{{label}} Werd actief", + "stationary": "{{label}} werd stationair", + "attribute": { + "faceOrLicense_plate": "{{attribute}} Gedetecteerd voor {{label}}", + "other": "{{label}} Herkend als {{attribute}}" + }, + "gone": "{{label}} vertrok", + "heard": "{{label}} gehoord", + "external": "{{label}} gedetecteerd", + "header": { + "zones": "Zones", + "ratio": "Verhouding", + "area": "Gebied", + "score": "Score", + "computedScore": "Berekende score", + "topScore": "Hoogste score", + "toggleAdvancedScores": "Geavanceerde scores weergeven" + } + }, + "annotationSettings": { + "title": "Annotatie-instellingen", + "showAllZones": { + "title": "Toon alle zones", + "desc": "Toon altijd zones op frames waar objecten een zone zijn binnengegaan." + }, + "offset": { + "label": "Annotatie-afwijking", + "desc": "Deze gegevens zijn afkomstig van de detectiestream van je camera, maar worden weergegeven op beelden uit de opnamestream. Het is onwaarschijnlijk dat deze twee streams perfect gesynchroniseerd zijn. Hierdoor zullen het objectkader en het beeld niet exact op elkaar aansluiten. Met deze instelling kun je de annotaties vooruit of achteruit in de tijd verschuiven om ze beter uit te lijnen met het opgenomen beeldmateriaal.", + "millisecondsToOffset": "Aantal milliseconden om objectkader mee te verschuiven. Standaard: 0", + "tips": "Verlaag de waarde als de videoweergave sneller is dan de objectkaders en hun trajectpunten, en verhoog de waarde als de videoweergave achterloopt. Deze waarde kan negatief zijn.", + "toast": { + "success": "Annotatieverschuiving voor {{camera}} is opgeslagen in het configuratiebestand." + } + } + }, + "carousel": { + "previous": "Vorige dia", + "next": "Volgende dia" + } + } +} diff --git a/web/public/locales/nl/views/exports.json b/web/public/locales/nl/views/exports.json new file mode 100644 index 0000000..e90ebd9 --- /dev/null +++ b/web/public/locales/nl/views/exports.json @@ -0,0 +1,67 @@ +{ + "documentTitle": "Exporteren - Frigate", + "search": "Zoek", + "toast": { + "error": { + "renameExportFailed": "Het is niet gelukt om de export te hernoemen: {{errorMessage}}", + "assignCaseFailed": "Kan toewijzing aan dossier niet bijwerken: {{errorMessage}}", + "caseSaveFailed": "Mislukt om zaak op te slaan: {{errorMessage}}" + } + }, + "editExport": { + "title": "Hernoemen Export", + "saveExport": "Export opslaan", + "desc": "Voer een nieuwe naam in voor deze export." + }, + "noExports": "Geen export gevonden", + "deleteExport": { + "label": "Verwijder export" + }, + "deleteExport.desc": "Weet je zeker dat je dit wilt wissen: {{exportName}}?", + "tooltip": { + "shareExport": "Deel export", + "downloadVideo": "Download video", + "editName": "Naam bewerken", + "deleteExport": "Verwijder export", + "assignToCase": "Toevoegen aan dossier", + "removeFromCase": "Van zaak verwijderen" + }, + "headings": { + "cases": "Gevallen", + "uncategorizedExports": "Ongecategoriseerde exporten" + }, + "caseDialog": { + "title": "Toevoegen aan dossier", + "description": "Kies een bestaand dossier of maak een nieuw dossier aan.", + "selectLabel": "Dossier", + "newCaseOption": "Nieuw dossier aanmaken", + "nameLabel": "Dossiernaam", + "descriptionLabel": "Beschrijving" + }, + "deleteCase": { + "desc": "Weet je zeker dat je {{caseName}} wilt verwijderen?" + }, + "caseCard": { + "emptyCase": "Nog geen exports" + }, + "caseEditor": { + "createTitle": "Zaak aanmaken", + "editTitle": "Zaak wijzigen", + "namePlaceholder": "Zaaknaam", + "descriptionPlaceholder": "Voeg notities of context toe voor deze zaak" + }, + "addExportDialog": { + "title": "Voeg export toe aan {{caseName}}", + "searchPlaceholder": "Zoek ongecategoriseerde exports", + "empty": "Geen niet-gecategoriseerde exports voldoen aan deze zoekopdracht.", + "addButton_one": "Voeg 1 export toe", + "addButton_other": "Voeg {{count}} exports toe", + "adding": "Toevoegen..." + }, + "toolbar": { + "newCase": "Nieuwe zaak", + "addExport": "Export toevoegen", + "editCase": "Wijzig zaak", + "deleteCase": "Verwijder zaak" + } +} diff --git a/web/public/locales/nl/views/faceLibrary.json b/web/public/locales/nl/views/faceLibrary.json new file mode 100644 index 0000000..d314250 --- /dev/null +++ b/web/public/locales/nl/views/faceLibrary.json @@ -0,0 +1,110 @@ +{ + "selectItem": "Selecteer {{item}}", + "details": { + "timestamp": "Tijdstempel", + "person": "Persoon", + "confidence": "Vertrouwen", + "face": "Gezicht Details", + "faceDesc": "Details van het gevolgde object dat dit gezicht heeft gegenereerd", + "scoreInfo": "De sublabelscore is het gewogen gemiddelde van hoe zeker de gezichtsherkenningen zijn. Deze score kan anders zijn dan die op de snapshot.", + "subLabelScore": "Score voor sublabel", + "unknown": "Onbekend" + }, + "documentTitle": "Gezichtsbibliotheek - Frigate", + "description": { + "placeholder": "Voer een naam in voor deze verzameling", + "addFace": "Voeg een nieuwe collectie toe aan de gezichtenbibliotheek door je eerste afbeelding te uploaden.", + "invalidName": "Ongeldige naam. Namen mogen alleen letters, cijfers, spaties, apostroffen, underscores en koppeltekens bevatten.", + "nameCannotContainHash": "De naam mag geen # bevatten." + }, + "train": { + "title": "Recente herkenningen", + "aria": "Selecteer recente herkenningen", + "empty": "Er zijn geen recente pogingen tot gezichtsherkenning", + "titleShort": "Recent", + "emptyNoLibrary": { + "title": "Een gezicht uploaden", + "description": "U moet ten minste één gezicht aan de bibliotheek toevoegen om gezichtsherkenning te laten werken." + } + }, + "selectFace": "Selecteer gezicht", + "toast": { + "error": { + "addFaceLibraryFailed": "Het is niet gelukt om de gezichtsnaam in te stellen: {{errorMessage}}", + "deleteFaceFailed": "Verwijderen mislukt: {{errorMessage}}", + "deleteNameFailed": "Naam verwijderen is niet gelukt: {{errorMessage}}", + "updateFaceScoreFailed": "Niet gelukt om gezichtsscore bij te werken: {{errorMessage}}", + "uploadingImageFailed": "Afbeelding uploaden mislukt: {{errorMessage}}", + "trainFailed": "Trainen mislukt: {{errorMessage}}", + "renameFaceFailed": "Het is niet gelukt om het gezicht te hernoemen: {{errorMessage}}", + "reclassifyFailed": "Opnieuw classificeren van gezicht mislukt: {{errorMessage}}" + }, + "success": { + "deletedFace_one": "{{count}} gezicht is succesvol verwijderd.", + "deletedFace_other": "{{count}} gezichten zijn succesvol verwijderd.", + "trainedFace": "Met succes getraind gezicht.", + "updatedFaceScore": "De gezichtsscore is succesvol bijgewerkt naar {{name}} ({{score}}).", + "deletedName_one": "{{count}} gezicht is succesvol verwijderd.", + "deletedName_other": "{{count}} gezichten zijn succesvol verwijderd.", + "uploadedImage": "Afbeelding succesvol geüpload.", + "addFaceLibrary": "{{name}} is succesvol toegevoegd aan de Gezichtenbibliotheek!", + "renamedFace": "Gezicht succesvol hernoemd naar {{name}}", + "reclassifiedFace": "Gezicht succesvol geherclassificeerd." + } + }, + "imageEntry": { + "dropActive": "Zet de afbeelding hier neer…", + "dropInstructions": "Sleep een afbeelding hierheen, of klik om te selecteren", + "maxSize": "Maximale grootte: {{size}}MB", + "validation": { + "selectImage": "Selecteer een afbeeldingbestand." + } + }, + "createFaceLibrary": { + "title": "Collectie maken", + "desc": "Een nieuwe collectie maken", + "new": "Creëer een nieuw gezicht", + "nextSteps": "Om een sterke basis op te bouwen:
  • Gebruik het tabblad ‘Recente herkenningen’ om afbeeldingen te selecteren en te trainen voor elke gedetecteerde persoon.
  • Richt je op afbeeldingen die recht van voren genomen zijn voor de beste resultaten, vermijd trainingsafbeeldingen waarop gezichten onder een hoek te zien zijn.
  • " + }, + "button": { + "addFace": "Gezicht toevoegen", + "uploadImage": "Afbeelding uploaden", + "deleteFaceAttempts": "Verwijder gezicht", + "reprocessFace": "Herverwerk gezicht", + "renameFace": "Gezicht hernoemen", + "deleteFace": "Gezicht verwijderen" + }, + "uploadFaceImage": { + "desc": "Upload een afbeelding om te scannen op gezichten en op te nemen voor {{pageToggle}}", + "title": "Upload een afbeelding van het gezicht" + }, + "deleteFaceLibrary": { + "title": "Verwijder Naam", + "desc": "Weet je zeker dat je de collectie {{name}} wilt verwijderen? Dit zal permanent alle geassocieerde gezichten verwijderen." + }, + "trainFaceAs": "Gezicht trainen als:", + "trainFace": "Gezicht trainen", + "readTheDocs": "Lees de documentatie", + "steps": { + "nextSteps": "Volgende stappen", + "faceName": "Voer een naam in", + "uploadFace": "Upload een afbeelding van het gezicht", + "description": { + "uploadFace": "Upload een afbeelding van {{name}} waarop het gezicht van voren te zien is. De afbeelding hoeft niet bijgesneden te zijn tot alleen het gezicht." + } + }, + "renameFace": { + "title": "Gezicht hernoemen", + "desc": "Voer een nieuwe naam in voor {{name}}" + }, + "deleteFaceAttempts": { + "title": "Verwijder gezicht", + "desc_one": "Weet je zeker dat je {{count}} gezicht wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", + "desc_other": "Weet je zeker dat je {{count}} gezichten wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt." + }, + "collections": "Collecties", + "nofaces": "Geen gezichten beschikbaar", + "pixels": "{{area}}px", + "reclassifyFaceAs": "Herclassificeer ‘Face’ als:", + "reclassifyFace": "Gezicht opnieuw classificeren" +} diff --git a/web/public/locales/nl/views/live.json b/web/public/locales/nl/views/live.json new file mode 100644 index 0000000..e12f191 --- /dev/null +++ b/web/public/locales/nl/views/live.json @@ -0,0 +1,206 @@ +{ + "lowBandwidthMode": "Modus voor lage bandbreedte", + "twoWayTalk": { + "enable": "Tweerichtingsgesprek inschakelen", + "disable": "Tweerichtingsgesprek uitschakelen" + }, + "cameraAudio": { + "enable": "Camera geluid inschakelen", + "disable": "Camera geluid uitschakelen" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Klik in het frame om de camera te centreren", + "enable": "Klikken om te bewegen inschakelen", + "disable": "Klikken om te bewegen uitschakelen", + "enableWithZoom": "Schakel klikken om te verplaatsen / slepen om te zoomen in" + }, + "right": { + "label": "Beweeg de PTZ-camera naar rechts" + }, + "down": { + "label": "Beweeg de PTZ-camera naar beneden" + }, + "up": { + "label": "Beweeg de PTZ-camera naar boven" + }, + "left": { + "label": "Beweeg de PTZ-camera naar links" + } + }, + "zoom": { + "in": { + "label": "Zoom de PTZ-camera in" + }, + "out": { + "label": "Zoom de PTZ-camera uit" + } + }, + "frame": { + "center": { + "label": "Klik in het frame om de PTZ-camera te centreren" + } + }, + "presets": "PTZ-camerapresets", + "focus": { + "in": { + "label": "Focus PTZ-camera in" + }, + "out": { + "label": "Focus PTZ-camera uit" + } + } + }, + "camera": { + "enable": "Camera inschakelen", + "disable": "Camera uitschakelen", + "turnOn": "Camera inschakelen", + "turnOff": "Camera uitschakelen" + }, + "muteCameras": { + "enable": "Alle camera's dempen", + "disable": "Dempen van alle camera's opheffen" + }, + "detect": { + "disable": "Detectie uitschakelen", + "enable": "Detectie inschakelen" + }, + "snapshots": { + "enable": "Momentopnamen inschakelen", + "disable": "Schakel snapshots uit" + }, + "audioDetect": { + "disable": "Audiodetectie uitschakelen", + "enable": "Audiodetectie inschakelen" + }, + "streamStats": { + "disable": "Verberg streamstatistieken", + "enable": "Streamstatistieken weergeven" + }, + "manualRecording": { + "showStats": { + "label": "Statistieken weergeven", + "desc": "Schakel deze optie in om streamstatistieken als overlay op de camerafeed weer te geven." + }, + "started": "Handmatige opname gestart.", + "start": "Handmatige opname starten", + "debugView": "Debug weergave", + "end": "Handmatige opname stoppen", + "ended": "Handmatige opname gestopt.", + "failedToEnd": "Het beëindigen van de handmatige opname is mislukt.", + "playInBackground": { + "label": "Speel op de achtergrond", + "desc": "Schakel deze optie in om te blijven streamen wanneer de speler verborgen is." + }, + "recordDisabledTips": "Aangezien opnemen is uitgeschakeld of beperkt in de configuratie van deze camera, zal alleen een momentopname worden opgeslagen.", + "title": "Op aanvraag", + "tips": "Download direct een snapshot of start handmatig een gebeurtenis op basis van de opnamebewaarinstellingen van deze camera.", + "failedToStart": "Handmatige opname starten mislukt." + }, + "notifications": "Meldingen", + "audio": "Geluid", + "documentTitle": { + "default": "Live - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Live - Frigate", + "autotracking": { + "enable": "Automatisch volgen inschakelen", + "disable": "Automatisch volgen uitschakelen" + }, + "recording": { + "disable": "Opname uitschakelen", + "enable": "Opname inschakelen", + "disabledInConfig": "De opnamefunctie moet eerst worden ingeschakeld in de instellingen van deze camera." + }, + "suspend": { + "forTime": "Onderbreken voor: " + }, + "streamingSettings": "Streaming-instellingen", + "stream": { + "twoWayTalk": { + "tips.documentation": "Lees de documenten ", + "tips": "Uw apparaat moet deze functie ondersteunen en WebRTC moet geconfigureerd zijn voor tweerichtingsgesprekken.", + "unavailable": "Tweerichtingsgesprek is niet beschikbaar voor deze stream", + "available": "Voor deze stream is tweerichtingsgesprek beschikbaar" + }, + "lowBandwidth": { + "resetStream": "Stream resetten", + "tips": "Liveweergave staat in de lagebandbreedtemodus vanwege buffering of streamfouten." + }, + "title": "Stream", + "audio": { + "tips": { + "documentation": "Lees de documentatie ", + "title": "Audio moet via je camera komen en in go2rtc geconfigureerd zijn voor deze stream." + }, + "unavailable": "Audio is niet beschikbaar voor deze stream", + "available": "Audio is beschikbaar voor deze stream" + }, + "playInBackground": { + "label": "Afspelen op de achtergrond", + "tips": "Schakel deze optie in om te blijven streamen wanneer de speler verborgen is." + }, + "debug": { + "picker": "Streamselectie is niet beschikbaar in de debugmodus. De debugweergave gebruikt altijd de stream waaraan de detectierol is toegewezen." + } + }, + "cameraSettings": { + "title": "{{camera}} Instellingen", + "objectDetection": "Objectdetectie", + "recording": "Opname", + "audioDetection": "Audiodetectie", + "autotracking": "Automatisch volgen", + "snapshots": "Momentopnames", + "cameraEnabled": "Camera ingeschakeld", + "transcription": "Audiotranscriptie", + "camera": "Camera" + }, + "history": { + "label": "Historische beelden weergeven" + }, + "effectiveRetainMode": { + "modes": { + "all": "Alle", + "active_objects": "Actieve objecten", + "motion": "Beweging" + }, + "notAllTips": "De bewaarbeleid-configuratie voor {{source}} is ingesteld op modus: {{effectiveRetainMode}}, dus deze opname op aanvraag bewaart alleen segmenten met {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Lay-out bewerken", + "exitEdit": "Bewerken verlaten", + "group": { + "label": "Cameragroep bewerken" + } + }, + "transcription": { + "enable": "Live audiotranscriptie inschakelen", + "disable": "Live audiotranscriptie uitschakelen" + }, + "snapshot": { + "takeSnapshot": "Direct een snapshot downloaden", + "noVideoSource": "Geen videobron beschikbaar voor snapshot.", + "captureFailed": "Het is niet gelukt om een snapshot te maken.", + "downloadStarted": "Snapshot downloaden gestart." + }, + "noCameras": { + "title": "Geen camera’s ingesteld", + "description": "Begin door een camera te verbinden met Frigate.", + "buttonText": "Camera toevoegen", + "restricted": { + "title": "Geen camera's beschikbaar", + "description": "Je hebt geen toestemming om camera's in deze groep te bekijken." + }, + "default": { + "title": "Geen camera’s ingesteld", + "description": "Begin door een camera te verbinden met Frigate.", + "buttonText": "Camera toevoegen" + }, + "group": { + "title": "Geen camera's in groep", + "description": "Deze cameragroep heeft geen toegewezen of ingeschakelde camera's.", + "buttonText": "Groepen beheren" + } + } +} diff --git a/web/public/locales/nl/views/motionSearch.json b/web/public/locales/nl/views/motionSearch.json new file mode 100644 index 0000000..b289113 --- /dev/null +++ b/web/public/locales/nl/views/motionSearch.json @@ -0,0 +1,65 @@ +{ + "startSearch": "Zoeken Starten", + "searchStarted": "Zoekopdracht gestart", + "searchCancelled": "Zoekopdracht geannuleerd", + "cancelSearch": "Annuleer", + "searching": "Zoekopdracht bezig.", + "searchComplete": "Zoekopdracht voltooid", + "title": "Beweging Zoeken", + "selectCamera": "Beweging Zoeken is aan het laden", + "noResultsYet": "Start een zoekactie om beweging te vinden in de geselecteerde regio", + "noChangesFound": "Geen pixel wijziging gedetecteerd in de geselecteerde regio", + "changesFound_one": "{{count}} bewegingsverandering gevonden", + "changesFound_other": "{{count}} bewegingsveranderingen gevonden", + "framesProcessed": "{{count}} frames verwerkt", + "jumpToTime": "Spring naar deze tijd", + "results": "Resultaten", + "documentTitle": "Beweging Zoeken - Frigate", + "description": "Teken een polygoon om het interessegebied te definieren en specifeer een tijdspanne voor het zoeken in dit gebied.", + "newSearch": "Nieuwe Zoekopdracht", + "clearResults": "Verwijder Resultaten", + "clearROI": "Verwijder Polygoon", + "polygonControls": { + "points_one": "{{count}} punt", + "points_other": "{{count}} punten", + "undo": "Verwijder het laatste punt", + "reset": "Herstel Polygoon" + }, + "dialog": { + "title": "Beweging Zoeken", + "cameraLabel": "Camera" + }, + "timeRange": { + "start": "Starttijd", + "end": "Eindtijd" + }, + "settings": { + "title": "Zoekinstellingen", + "parallelMode": "Parallelle modus", + "parallelModeDesc": "Scan meerdere video segmenten tegelijk (sneller, maar significant meer CPU gebruik)", + "threshold": "Gevoeligheid drempel", + "thresholdDesc": "Lagere waardes detecteren eerder veranderingen (1-255)", + "minArea": "Minimaal wijzigings gebied", + "minAreaDesc": "Minimale percentage van gebied welke moet wijzigen om als significante wijziging aan te merken", + "frameSkip": "Frame overlaan", + "maxResults": "Maximaal aantal resultaten", + "maxResultsDesc": "Stop na dit aantal overeenkomende tijdstempels" + }, + "errors": { + "polygonTooSmall": "De Polygoon moet minstens 3 punten bevatten", + "unknown": "Onbekende fout", + "noCamera": "Selecteer een camera", + "noROI": "Teken een interesse gebied a.u.b.", + "noTimeRange": "Selecteer een tijdsbereik a.u.b.", + "invalidTimeRange": "Eindtijd moet na de starttijd liggen", + "searchFailed": "Zoeken gefaald: {{message}}" + }, + "changePercentage": "{{percentage}}% gewijzigd", + "metrics": { + "title": "Zoek Meetgegevens", + "segmentsScanned": "Gescande segmenten", + "segmentsProcessed": "Verwerkt", + "segmentsSkippedInactive": "Overgeslagen (geen activiteit)", + "segmentsSkippedHeatmap": "Overgeslagen (geen ROI overlap)" + } +} diff --git a/web/public/locales/nl/views/recording.json b/web/public/locales/nl/views/recording.json new file mode 100644 index 0000000..5a40650 --- /dev/null +++ b/web/public/locales/nl/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Exporteren", + "toast": { + "error": { + "endTimeMustAfterStartTime": "De eindtijd moet na de starttijd zijn", + "noValidTimeSelected": "Er is geen geldig tijdsbereik geselecteerd" + } + }, + "filter": "Filter", + "calendar": "Kalender", + "filters": "Filters" +} diff --git a/web/public/locales/nl/views/replay.json b/web/public/locales/nl/views/replay.json new file mode 100644 index 0000000..143c16e --- /dev/null +++ b/web/public/locales/nl/views/replay.json @@ -0,0 +1,59 @@ +{ + "websocket_messages": "Berichten", + "dialog": { + "camera": "Broncamera", + "preset": { + "1m": "Laatste 1 minuut", + "5m": "Laatste 5 minuten", + "timeline": "Vanaf tijdlijn", + "custom": "Aangepast" + }, + "title": "Start Debug Herhaling", + "timeRange": "Tijdsbereik", + "startButton": "Start herhaling", + "selectFromTimeline": "Selecteer", + "starting": "Herhaling starten...", + "startLabel": "Start", + "endLabel": "Einde", + "description": "Maak een tijdelijke herhalingscamera die historische beelden in een lus afspeelt voor het debuggen van objectdetectie- en trackingproblemen. De herhalingscamera gebruikt dezelfde detectieconfiguratie als de broncamera. Kies een tijdsbereik om te beginnen.", + "toast": { + "error": "Kan debugherhaling niet starten: {{error}}", + "alreadyActive": "Er is al een herhalingssessie actief", + "stopError": "Kan debugherhaling niet stoppen: {{error}}", + "goToReplay": "Ga naar herhaling" + } + }, + "title": "Debug Herhaling", + "description": "Herhaal camera-opnames voor foutopsporing. De objectlijst toont een vertraagde samenvatting van gedetecteerde objecten en het tabblad Berichten toont een stream van interne Frigate-berichten uit de herhaalde beelden.", + "page": { + "noSession": "Geen actieve debugherhalingssessie", + "noSessionDesc": "Start een debugherhaling vanuit de Geschiedenis-weergave door op de knop Acties in de werkbalk te klikken en Debug Herhaling te kiezen.", + "goToRecordings": "Ga naar Geschiedenis", + "preparingClip": "Clip voorbereiden…", + "preparingClipDesc": "Frigate voegt opnames samen voor het geselecteerde tijdsbereik. Dit kan bij langere bereiken even duren.", + "startingCamera": "Debugherhaling starten…", + "startError": { + "title": "Kan debugherhaling niet starten", + "back": "Terug naar Geschiedenis" + }, + "sourceCamera": "Broncamera", + "replayCamera": "Herhalingscamera", + "initializingReplay": "Debugherhaling initialiseren...", + "stoppingReplay": "Debugherhaling stoppen...", + "stopReplay": "Stop herhaling", + "confirmStop": { + "title": "Debugherhaling stoppen?", + "description": "Dit stopt de sessie en ruimt alle tijdelijke gegevens op. Weet je het zeker?", + "confirm": "Stop herhaling", + "cancel": "Annuleren" + }, + "activity": "Activiteit", + "objects": "Objectlijst", + "audioDetections": "Audiodetecties", + "noActivity": "Geen activiteit gedetecteerd", + "activeTracking": "Actieve tracking", + "noActiveTracking": "Geen actieve tracking", + "configuration": "Configuratie", + "configurationDesc": "Stem de instellingen voor bewegingsdetectie en objecttracking van de debugherhalingscamera nauwkeurig af. Wijzigingen worden niet opgeslagen in je Frigate-configuratiebestand." + } +} diff --git a/web/public/locales/nl/views/search.json b/web/public/locales/nl/views/search.json new file mode 100644 index 0000000..7552d14 --- /dev/null +++ b/web/public/locales/nl/views/search.json @@ -0,0 +1,75 @@ +{ + "button": { + "delete": "Opgeslagen zoekopdracht verwijderen", + "save": "Zoekopdracht opslaan", + "clear": "Zoekopdracht wissen", + "filterInformation": "Filter informatie", + "filterActive": "Filters actief" + }, + "search": "Zoek", + "savedSearches": "Opgeslagen zoekopdrachten", + "searchFor": "Zoeken naar {{inputValue}}", + "trackedObjectId": "Getraceerd object-ID", + "filter": { + "label": { + "cameras": "Camera's", + "labels": "Labels", + "sub_labels": "Sublabels", + "time_range": "Tijdsbereik", + "before": "Voor", + "min_score": "Min Score", + "max_score": "Max Score", + "min_speed": "Min snelheid", + "recognized_license_plate": "Herkend kenteken", + "has_snapshot": "Heeft Snapshot", + "has_clip": "Heeft Clip", + "search_type": "Zoektype", + "zones": "Zones", + "max_speed": "Max snelheid", + "after": "Na", + "attributes": "Kenmerken" + }, + "toast": { + "error": { + "maxSpeedMustBeGreaterOrEqualMinSpeed": "De ‘max_speed’ moet groter zijn dan of gelijk aan de ‘min_speed’.", + "beforeDateBeLaterAfter": "De 'voor' datum moet later zijn dan de 'na' datum.", + "afterDatebeEarlierBefore": "De ‘na’ datum moet eerder zijn dan de ‘voor’ datum.", + "minScoreMustBeLessOrEqualMaxScore": "De ‘min_score’ moet kleiner zijn dan of gelijk aan de ‘max_score’.", + "maxScoreMustBeGreaterOrEqualMinScore": "De ‘max_score’ moet groter zijn dan of gelijk aan de ‘min_score’.", + "minSpeedMustBeLessOrEqualMaxSpeed": "De ‘min_snelheid’ moet kleiner zijn dan of gelijk aan de ‘max_snelheid’." + } + }, + "tips": { + "title": "Hoe tekstfilters te gebruiken", + "desc": { + "example": "Voorbeeld: camera's:voordeur label:persoon vóór:01012024 tijdsbereik:15:00-16:00", + "text": "Filters helpen je om je zoekresultaten te beperken. Zo gebruik je ze in het invoerveld:", + "step": "
    • Typ een filternaam gevolgd door een dubbele punt (bijv. \"cameras:\").
    • Selecteer een waarde uit de suggesties of typ je eigen waarde.
    • Gebruik meerdere filters door ze achter elkaar toe te voegen, gescheiden door een spatie.
    • Datumfilters (before: en after:) gebruiken {{DateFormat}} formaat
    • Het tijdsfilter gebruikt {{exampleTime}} formaat
    • Verwijder filters door op de 'x' ernaast te klikken.
    ", + "step3": "Gebruik meerdere filters door ze achter elkaar toe te voegen met een spatie ertussen.", + "step4": "Datumfilters (voor: en na:) gebruiken het {{DateFormat}} formaat.", + "step5": "Het tijdsfilter gebruikt het {{exampleTime}} als formaat.", + "step2": "Selecteer een waarde uit de suggesties of voer je eigen waarde in.", + "step1": "Typ een filtersleutelnaam gevolgd door een dubbele punt (bijv. \"camera's:\").", + "step6": "Verwijder filters door op de 'x' naast de filteroptie te klikken.", + "exampleLabel": "Voorbeeld:" + } + }, + "searchType": { + "thumbnail": "Thumbnail", + "description": "Beschrijving" + }, + "header": { + "currentFilterType": "Filterwaarden", + "noFilters": "Filters", + "activeFilters": "Actieve filters" + } + }, + "similaritySearch": { + "active": "Gelijkenis zoeken actief", + "title": "Gelijkenis zoeken", + "clear": "Gelijksoortige zoekopdracht wissen" + }, + "placeholder": { + "search": "Zoek…" + } +} diff --git a/web/public/locales/nl/views/settings.json b/web/public/locales/nl/views/settings.json new file mode 100644 index 0000000..d1cd829 --- /dev/null +++ b/web/public/locales/nl/views/settings.json @@ -0,0 +1,2040 @@ +{ + "documentTitle": { + "default": "Instellingen - Frigate", + "camera": "Camera-instellingen - Frigate", + "authentication": "Authenticatie-instellingen - Frigate", + "motionTuner": "Beweging Tuner - Frigate", + "classification": "Classificatie-instellingen - Frigate", + "masksAndZones": "Masker- en zone-editor - Frigate", + "object": "Foutopsporing Frigate", + "general": "UI Instellingen - Frigate", + "frigatePlus": "Frigate+ Instellingen - Frigate", + "notifications": "Meldingsinstellingen - Frigate", + "enrichments": "Verrijkingsinstellingen - Frigate", + "cameraManagement": "Camera's beheren - Frigate", + "cameraReview": "Camera Beoordeling Instellingen - Frigate", + "globalConfig": "Globaale configuratie - Frigate", + "cameraConfig": "Camera-instellingen - Frigate", + "maintenance": "Onderhoud - Frigate", + "profiles": "Profielen - Frigate", + "detectorsAndModel": "Detectoren en model - Frigate" + }, + "menu": { + "ui": "Gebruikersinterface", + "classification": "Classificatie", + "masksAndZones": "Maskers / Zones", + "motionTuner": "Bewegingsdetectie-afstellen", + "debug": "foutopsporing", + "users": "Gebruikers", + "notifications": "Meldingen", + "cameras": "Camera-instellingen", + "frigateplus": "Frigate+", + "enrichments": "Verrijkingen", + "triggers": "Triggers", + "roles": "Rollen", + "cameraManagement": "Beheer", + "cameraReview": "Beoordeel", + "general": "Algemeen", + "globalConfig": "Globaale configuratie", + "system": "Systeem", + "integrations": "Integraties", + "profileSettings": "Profielinstellingen", + "globalDetect": "Objectdetectie", + "globalRecording": "Opname", + "globalSnapshots": "Snapshots", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Bewegingsdetectie", + "globalObjects": "Objecten", + "globalReview": "Beoordelen", + "globalAudioEvents": "Geluidsgebeurtenissen", + "globalLivePlayback": "Live afspelen", + "globalTimestampStyle": "Tijdstempelstijl", + "systemDatabase": "Database", + "systemTls": "TLS", + "systemAuthentication": "Authenticatie", + "cameraNotifications": "Notificaties", + "integrationGenerativeAi": "Generatieve AI", + "systemNetworking": "Netwerken", + "profiles": "Profielen", + "uiSettings": "UI instellingen", + "systemProxy": "Proxy", + "systemUi": "UI", + "systemLogging": "Logging", + "integrationFaceRecognition": "Gezichtsherkenning", + "integrationLpr": "Kentekenplaat herkenning", + "integrationObjectClassification": "Object classificatie", + "integrationAudioTranscription": "Audio transcriptie", + "cameraDetect": "Object detectie", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "Opnemen", + "cameraSnapshots": "Momentopnames", + "cameraMotion": "Bewegingsdetectie", + "cameraObjects": "Objecten", + "cameraAudioEvents": "Audio gebeurtenissen", + "cameraAudioTranscription": "Audio transcriptie", + "integrationSemanticSearch": "Semantisch zoeken", + "systemDetectionModel": "Detectie model", + "systemMqtt": "MQTT", + "systemEnvironmentVariables": "Omgevingsvariabelen", + "systemTelemetry": "Telemetrie", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "Detectie hardware", + "cameraFaceRecognition": "Gezichtsherkenning", + "systemGo2rtcStreams": "go2rtc streams", + "cameraConfigReview": "Beoordeling", + "cameraLivePlayback": "Live weergave", + "cameraLpr": "Kentekenplaat herkenning", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "Camera UI", + "cameraTimestampStyle": "Tijdstempel stijl", + "maintenance": "Onderhoud", + "systemDetectorsAndModel": "Detectoren en model", + "cameraBirdseye": "Birdseye", + "cameraMqtt": "Camera MQTT", + "mediaSync": "Media-synchronisatie", + "regionGrid": "Regio-raster" + }, + "dialog": { + "unsavedChanges": { + "title": "Je hebt niet-opgeslagen wijzigingen.", + "desc": "Wilt je jouw wijzigingen opslaan voordat u verdergaat?" + } + }, + "cameraSetting": { + "camera": "Camera", + "noCamera": "Geen camera" + }, + "general": { + "liveDashboard": { + "title": "Live-dashboard", + "automaticLiveView": { + "label": "Automatische liveweergave", + "desc": "Schakel automatisch over naar de liveweergave van een camera wanneer er activiteit wordt gedetecteerd. Als u deze optie uitschakelt, worden de statische camerabeelden op het live dashboard slechts eenmaal per minuut bijgewerkt." + }, + "playAlertVideos": { + "label": "Meldingen afspelen", + "desc": "Standaard worden recente meldingen op het Live dashboard afgespeeld als kleine lusvideo's. Schakel deze optie uit om alleen een statische afbeelding van recente meldingen weer te geven op dit apparaat/browser." + }, + "displayCameraNames": { + "label": "Altijd cameranamen weergeven", + "desc": "Toon altijd de cameranamen in een label op het live-cameradashboard." + }, + "liveFallbackTimeout": { + "label": "Live speler fallback time-out", + "desc": "Wanneer de hoogwaardige livestream van een camera niet beschikbaar is, schakel dan na dit aantal seconden terug naar de modus voor lage bandbreedte. Standaard: 3." + } + }, + "title": "Gebruikersinterface instellingen", + "storedLayouts": { + "title": "Opgeslagen indelingen", + "clearAll": "Alle indelingen wissen", + "desc": "De indeling van camera's in een cameragroep kan worden versleept en in formaat worden aangepast. De posities en afmetingen worden opgeslagen in de lokale opslag van je browser." + }, + "cameraGroupStreaming": { + "title": "Streaminginstellingen voor cameragroep", + "desc": "De streaminginstellingen voor elke cameragroep worden opgeslagen in de lokale opslag van uw browser.", + "clearAll": "Alle streaminginstellingen wissen" + }, + "recordingsViewer": { + "title": "Opnameweergave", + "defaultPlaybackRate": { + "label": "Standaard afspeelsnelheid", + "desc": "Standaard afspeelsnelheid voor het afspelen van opnames." + } + }, + "calendar": { + "firstWeekday": { + "label": "Eerste weekdag", + "sunday": "Zondag", + "monday": "Maandag", + "desc": "De eerste dag van de week die in de kalender in de interface wordt weergegeven." + }, + "title": "Kalender" + }, + "toast": { + "success": { + "clearStoredLayout": "Verwijderde opgeslagen indeling voor {{cameraName}}", + "clearStreamingSettings": "Verwijderde streaming-instellingen voor alle cameragroepen." + }, + "error": { + "clearStoredLayoutFailed": "Het wissen van de opgeslagen indelingen is mislukt: {{errorMessage}}", + "clearStreamingSettingsFailed": "Het wissen van de streaminginstellingen is mislukt: {{errorMessage}}" + } + } + }, + "classification": { + "semanticSearch": { + "title": "Semantisch zoeken", + "reindexNow": { + "label": "Nu opnieuw indexeren", + "confirmTitle": "Bevestig herindexering", + "confirmButton": "Opnieuw indexeren", + "alreadyInProgress": "Het herindexeren is al bezig.", + "success": "Het herindexeren is succesvol gestart.", + "error": "Het opnieuw indexeren is mislukt: {{errorMessage}}", + "desc": "Opnieuw indexeren zal embeddings regenereren voor alle gevolgde objecten. Dit proces wordt op de achtergrond uitgevoerd en kan je CPU zwaar belasten en een behoorlijke hoeveelheid tijd in beslag nemen, afhankelijk van het aantal gevolgde objecten dat je hebt.", + "confirmDesc": "Weet u zeker dat u alle gevolgde object-embeddings opnieuw wilt indexeren? Dit proces wordt op de achtergrond uitgevoerd, maar kan uw CPU zwaar belasten en enige tijd in beslag nemen. U kunt de voortgang bekijken op de pagina Verkennen." + }, + "modelSize": { + "label": "Modelgrootte", + "desc": "De grootte van het model dat wordt gebruikt voor semantische zoekopdrachten.", + "small": { + "title": "klein", + "desc": "Het gebruik van small maakt gebruik van een gequantiseerde versie van het model die minder RAM verbruikt en sneller draait op de CPU, met een verwaarloosbaar verschil in embeddingkwaliteit." + }, + "large": { + "title": "groot", + "desc": "Het gebruik van large maakt gebruik van het volledige Jina-model en wordt automatisch op de GPU uitgevoerd als die beschikbaar is." + } + }, + "readTheDocumentation": "Lees de documentatie", + "desc": "Met semantisch zoeken in Frigate kun je getraceerde objecten in je overzichtsitems vinden aan de hand van de afbeelding zelf, een door de gebruiker gedefinieerde tekstbeschrijving of een automatisch gegenereerde beschrijving." + }, + "faceRecognition": { + "title": "Gezichtsherkenning", + "modelSize": { + "label": "Modelgrootte", + "desc": "De grootte van het model dat gebruikt wordt voor gezichtsherkenning.", + "small": { + "title": "klein", + "desc": "Met small wordt een FaceNet-model voor gezichtsinbedding gebruikt dat efficiënt werkt op de meeste CPU's." + }, + "large": { + "desc": "Het gebruik van groot maakt gebruik van een ArcFace-gezichtsembeddingmodel en wordt automatisch op de GPU uitgevoerd als die beschikbaar is.", + "title": "groot" + } + }, + "desc": "Gezichtsherkenning maakt het mogelijk om namen aan mensen toe te wijzen. Wanneer hun gezicht wordt herkend, wijst Frigate de naam van de persoon toe als sublabel. Deze informatie is opgenomen in de gebruikersinterface, filters en meldingen.", + "readTheDocumentation": "Lees de documentatie" + }, + "title": "Classificatie-instellingen", + "licensePlateRecognition": { + "title": "Kentekenherkenning", + "readTheDocumentation": "Lees de documentatie", + "desc": "Frigate kan kentekenplaten op voertuigen herkennen en automatisch de gedetecteerde tekens toevoegen aan het veld recognized_license_plate of een bekende naam als sublabel toekennen aan objecten van het type auto. Een veelvoorkomende toepassing is het uitlezen van kentekens van auto's die een oprit oprijden of voorbijrijden op straat." + }, + "toast": { + "success": "Classificatie-instellingen zijn opgeslagen. Start Frigate opnieuw op om de wijzigingen toe te passen.", + "error": "Configuratiewijzigingen konden niet worden opgeslagen: {{errorMessage}}" + }, + "birdClassification": { + "title": "Vogelclassificatie", + "desc": "Vogelclassificatie herkent bekende vogels met behulp van een gequantiseerd TensorFlow-model. Wanneer een bekende vogel wordt herkend, wordt de algemene naam toegevoegd als sublabel. Deze informatie wordt weergegeven in de interface, is beschikbaar in filters en wordt ook opgenomen in meldingen." + }, + "restart_required": "Opnieuw opstarten vereist (Classificatie-instellingen gewijzigd)", + "unsavedChanges": "Niet-opgeslagen wijzigingen in de classificatie-instellingen" + }, + "camera": { + "review": { + "title": "Beoordeel", + "alerts": "Meldingen ", + "detections": "Detecties ", + "desc": "Schakel waarschuwingen en detecties voor deze camera tijdelijk in of uit totdat Frigate opnieuw wordt gestart. Wanneer uitgeschakeld, worden er geen nieuwe beoordelingsitems gegenereerd. " + }, + "reviewClassification": { + "objectAlertsTips": "Alle {{alertsLabels}}-objecten op {{cameraName}} worden weergegeven als meldingen.", + "zoneObjectAlertsTips": "Alle {{alertsLabels}}-objecten die zijn gedetecteerd in {{zone}} op {{cameraName}} worden weergegeven als meldingen.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-objecten die in {{zone}} op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties.", + "notSelectDetections": "Alle {{detectionsLabels}}-objecten die in {{zone}} op {{cameraName}} worden gedetecteerd en niet als waarschuwing zijn gecategoriseerd, worden weergegeven als detecties – ongeacht in welke zone ze zich bevinden.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}}-objecten die op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties – ongeacht in welke zone ze zich bevinden." + }, + "selectAlertsZones": "Zones selecteren voor meldingen", + "selectDetectionsZones": "Selecteer zones voor detecties", + "limitDetections": "Beperk detecties tot specifieke zones", + "toast": { + "success": "Configuratie voor beoordelingsclassificatie is opgeslagen. Herstart Frigate om de wijzigingen toe te passen." + }, + "readTheDocumentation": "Lees de documentatie", + "noDefinedZones": "Voor deze camera zijn nog geen zones ingesteld.", + "desc": "Frigate categoriseert beoordelingsitems als waarschuwingen en detecties.Standaard worden alle person- en car-objecten als waarschuwingen beschouwd. Je kunt de categorisatie verfijnen door zones te configureren waarin uitsluitend deze objecten gedetecteerd moeten worden.", + "title": "Beoordelingsclassificatie", + "objectDetectionsTips": "Alle {{detectionsLabels}}-objecten die op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties, ongeacht in welke zone ze zich bevinden.", + "unsavedChanges": "Niet-opgeslagen classificatie-instellingen voor {{camera}}" + }, + "streams": { + "desc": "Schakel een camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig door Frigate. Detectie, opname en foutopsporing zijn dan niet beschikbaar.
    Let op: dit schakelt go2rtc-restreams niet uit.", + "title": "Streams" + }, + "title": "Camera-instellingen", + "object_descriptions": { + "title": "AI-gegenereerde objectomschrijvingen", + "desc": "AI-gegenereerde objectomschrijvingen tijdelijk uitschakelen voor deze camera. Wanneer uitgeschakeld, zullen omschrijvingen van gevolgde objecten op deze camera niet aangevraagd worden." + }, + "review_descriptions": { + "title": "Generatieve-AI Beoordelingsbeschrijvingen", + "desc": "Tijdelijk generatieve-AI-beoordelingsbeschrijvingen voor deze camera in- of uitschakelen. Wanneer dit is uitgeschakeld, worden er geen door AI gegenereerde beschrijvingen opgevraagd voor beoordelingsitems van deze camera." + }, + "addCamera": "Nieuwe camera toevoegen", + "editCamera": "Camera bewerken:", + "selectCamera": "Selecteer een camera", + "backToSettings": "Terug naar camera-instellingen", + "cameraConfig": { + "add": "Camera toevoegen", + "edit": "Camera bewerken", + "description": "Configureer de camera-instellingen, inclusief streaming-inputs en functies.", + "name": "Cameranaam", + "nameRequired": "Cameranaam is vereist", + "nameInvalid": "De cameranaam mag alleen letters, cijfers, onderstrepingstekens of koppeltekens bevatten", + "namePlaceholder": "bijv. voor_deur", + "enabled": "Ingeschakeld", + "ffmpeg": { + "inputs": "Streams-Input", + "path": "Stroompad", + "pathRequired": "Streampad is vereist", + "pathPlaceholder": "rtsp://...", + "roles": "Functie", + "rolesRequired": "Er is ten minste één functie vereist", + "rolesUnique": "Elke functie (audio, detecteren, opnemen) kan slechts aan één stream worden toegewezen", + "addInput": "Inputstream toevoegen", + "removeInput": "Inputstream verwijderen", + "inputsRequired": "Er is ten minste één stream-input vereist" + }, + "toast": { + "success": "Camera {{cameraName}} is succesvol opgeslagen" + }, + "nameLength": "Cameranaam mag niet langer zijn dan 24 tekens." + } + }, + "masksAndZones": { + "filter": { + "all": "Alle maskers en zones" + }, + "toast": { + "success": { + "copyCoordinates": "Coördinaten voor {{polyName}} gekopieerd naar klembord." + }, + "error": { + "copyCoordinatesFailed": "De coördinaten konden niet naar het klembord worden gekopieerd." + } + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "De zonenaam moet minimaal 2 tekens lang zijn.", + "mustNotContainPeriod": "De zonenaam mag geen punten bevatten.", + "hasIllegalCharacter": "De zonenaam bevat ongeldige tekens.", + "mustNotBeSameWithCamera": "De zonenaam mag niet gelijk zijn aan de cameranaam.", + "alreadyExists": "Er bestaat al een zone met deze naam voor deze camera.", + "mustHaveAtLeastOneLetter": "De zonenaam moet minimaal één letter bevatten." + } + }, + "distance": { + "error": { + "text": "Afstand moet groter dan of gelijk zijn aan 0,1.", + "mustBeFilled": "Alle afstandsvelden moeten worden ingevuld om de snelheid te kunnen schatten." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "De minimale snelheid moet meer zijn dan 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "De verblijftijd moet groter dan of gelijk aan 0 zijn." + } + }, + "polygonDrawing": { + "removeLastPoint": "Laatste punt verwijderen", + "snapPoints": { + "true": "Verbind punten", + "false": "Punten niet verbinden" + }, + "delete": { + "title": "Bevestig Verwijderen", + "desc": "Weet je zeker dat je de {{type}} {{name}} wilt verwijderen?", + "success": "{{name}} is verwijderd." + }, + "error": { + "mustBeFinished": "De polygoontekening moet voltooid zijn voordat u deze kunt opslaan." + }, + "reset": { + "label": "Alle punten wissen" + }, + "type": { + "zone": "zone", + "motion_mask": "bewegingsmasker", + "object_mask": "objectmasker" + }, + "revertOverride": { + "title": "Terugzetten naar basisconfiguratie", + "desc": "Dit verwijdert de profieloverschrijving voor de {{type}} {{name}} en zet deze terug naar de basisconfiguratie." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "De snelheidsdrempel moet groter dan of gelijk zijn aan 0,1." + } + }, + "id": { + "error": { + "mustNotBeEmpty": "ID mag niet leeg zijn.", + "alreadyExists": "Er bestaat al een masker met deze ID voor deze camera." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Naam mag niet leeg zijn." + } + } + }, + "zones": { + "documentTitle": "Bewerkingszone - Frigate", + "desc": { + "title": "Zones stellen je in staat om een specifiek gedeelte van het beeld te definiëren, zodat je kunt bepalen of een object zich binnen dat gebied bevindt of niet.", + "documentation": "Documentatie" + }, + "edit": "Bewerk zone", + "clickDrawPolygon": "Klik om een polygoon op de afbeelding te tekenen.", + "name": { + "title": "Naam", + "inputPlaceHolder": "Voer een naam in…", + "tips": "De naam moet minimaal 2 tekens lang zijn, minimaal één letter bevatten en mag niet gelijk zijn aan de naam van een camera of andere zone op deze camera." + }, + "inertia": { + "title": "Traagheid", + "desc": "Geeft aan hoeveel frames een object in een zone moet zijn voordat het als 'in de zone' wordt beschouwd. Standaard: 3" + }, + "loiteringTime": { + "title": "Stationaire tijd", + "desc": "Stelt de minimale tijd in (in seconden) die een object in de zone moet blijven voordat deze wordt geactiveerd. Standaard: 0" + }, + "objects": { + "title": "Objecten", + "desc": "Lijst met objecten die van toepassing zijn op deze zone." + }, + "speedEstimation": { + "desc": "Snelheidsschatting inschakelen voor objecten in deze zone. De zone moet precies 4 punten hebben.", + "title": "Snelheidsschatting", + "docs": "Lees de documentatie", + "lineADistance": "Afstand van lijn A ({{unit}})", + "lineBDistance": "Afstand van lijn B ({{unit}})", + "lineCDistance": "Afstand van lijn C ({{unit}})", + "lineDDistance": "Afstand van lijn D ({{unit}})" + }, + "speedThreshold": { + "desc": "Geeft een minimumsnelheid op voor objecten die in deze zone moeten worden beschouwd.", + "toast": { + "error": { + "pointLengthError": "De snelheidsschatting is uitgeschakeld voor deze zone. Zones met snelheidsschatting moeten precies 4 punten hebben.", + "loiteringTimeError": "Zones met een stationaire tijd groter dan 0 mogen niet worden gebruikt in combinatie met snelheidsschatting." + } + }, + "title": "Snelheidsdrempel ({{unit}})" + }, + "point_one": "{{count}} punt", + "point_other": "{{count}} punten", + "label": "Zones", + "add": "Zone toevoegen", + "allObjects": "Alle objecten", + "toast": { + "success": "Zone ({{zoneName}}) is opgeslagen." + }, + "enabled": { + "title": "Ingeschakeld", + "description": "Of deze zone actief en ingeschakeld is in het configuratiebestand. Als deze is uitgeschakeld, kan deze niet via MQTT worden ingeschakeld. Uitgeschakelde zones worden tijdens runtime genegeerd." + } + }, + "motionMasks": { + "label": "Bewegingsmasker", + "documentTitle": "Bewerken Bewegingsmasker - Frigate", + "desc": { + "documentation": "Documentatie", + "title": "Bewegingsmaskers worden gebruikt om te voorkomen dat ongewenste vormen van beweging een detectie activeren. Te veel maskeren maakt het moeilijker om objecten te volgen." + }, + "edit": "Bewerk bewegingsmasker", + "context": { + "documentation": "Lees de documentatie", + "title": "Bewegingsmaskers worden gebruikt om te voorkomen dat ongewenste soorten beweging een detectie activeren (bijvoorbeeld: bewegende boomtakken of tijdstempels in het camerabeeld). Bewegingsmaskers moeten zeer spaarzaam worden gebruikt – te veel maskeren maakt het moeilijker om objecten te volgen." + }, + "clickDrawPolygon": "Klik om een polygoon op de afbeelding te tekenen.", + "polygonAreaTooLarge": { + "documentation": "Lees de documentatie", + "title": "Het bewegingsmasker bedekt {{polygonArea}}% van het camerabeeld. Grote bewegingsmaskers worden niet aanbevolen.", + "tips": "Bewegingsmaskers voorkomen niet dat objecten worden gedetecteerd. Gebruik in plaats daarvan een objectmasker." + }, + "point_one": "{{count}} punt", + "point_other": "{{count}} punten", + "toast": { + "success": { + "title": "{{polygonName}} is opgeslagen.", + "noName": "Bewegingsmasker is opgeslagen." + } + }, + "add": "Nieuw bewegingsmasker", + "defaultName": "Beweging Mask {{number}}", + "name": { + "title": "Name", + "description": "Een optionele vriendelijke naam voor dit bewegingsmasker.", + "placeholder": "Voer een naam in..." + } + }, + "objectMasks": { + "label": "Objectmaskers", + "documentTitle": "Objectmasker bewerken - Frigate", + "desc": { + "title": "Objectfiltermaskers worden gebruikt om valse positieven uit te filteren voor een bepaald objecttype op basis van locatie.", + "documentation": "Documentatie" + }, + "add": "Objectmasker toevoegen", + "objects": { + "desc": "Het objecttype dat van toepassing is op dit objectmasker.", + "allObjectTypes": "Alle objecttypen", + "title": "Objecten" + }, + "toast": { + "success": { + "title": "{{polygonName}} is opgeslagen.", + "noName": "Objectmasker is opgeslagen." + } + }, + "point_one": "{{count}} punt", + "point_other": "{{count}} punten", + "clickDrawPolygon": "Klik om een polygoon op de afbeelding te tekenen.", + "context": "Objectfiltermaskers worden gebruikt om valse positieven uit te filteren voor een bepaald objecttype op basis van locatie.", + "edit": "Objectmasker bewerken", + "name": { + "title": "Name", + "description": "Een optionele vriendelijke naam voor dit objectmasker.", + "placeholder": "Voer een naam in..." + } + }, + "restart_required": "Herstart vereist (maskers/zones gewijzigd)", + "motionMaskLabel": "Bewegingsmasker {{number}}", + "objectMaskLabel": "Objectmasker {{number}}", + "disabledInConfig": "Item is uitgeschakeld in het configuratiebestand", + "addDisabledProfile": "Voeg dit eerst toe aan de basisconfiguratie en overschrijf het daarna in het profiel", + "profileBase": "(basis)", + "profileOverride": "(overschrijving)", + "masks": { + "enabled": { + "title": "Ingeschakeld", + "description": "Of dit masker is ingeschakeld in het configuratiebestand. Als het is uitgeschakeld, kan het niet via MQTT worden ingeschakeld. Uitgeschakelde maskers worden tijdens runtime genegeerd." + } + } + }, + "motionDetectionTuner": { + "title": "Bewegingsdetectie-afsteller", + "desc": { + "title": "Frigate gebruikt bewegingsdetectie als eerste controle om te zien of er iets gebeurt in het frame dat de moeite waard is om te controleren met objectdetectie.", + "documentation": "Lees de handleiding voor bewegingsafstelling" + }, + "Threshold": { + "title": "Drempelwaarde", + "desc": "De drempelwaarde bepaalt hoeveel verandering in de luminantie van een pixel nodig is om als beweging te worden beschouwd. Standaard: 30" + }, + "contourArea": { + "title": "Contourgebied", + "desc": "De waarde voor het contourgebied wordt gebruikt om te bepalen welke groepen gewijzigde pixels in aanmerking komen als beweging. Standaard: 10" + }, + "improveContrast": { + "title": "Contrast verbeteren", + "desc": "Verbeter het contrast bij weinig licht. Standaard: AAN" + }, + "toast": { + "success": "De bewegingsinstellingen zijn opgeslagen." + }, + "unsavedChanges": "Niet-opgeslagen wijzigingen in Bewegingsdetectie-afsteller ({{camera}})" + }, + "debug": { + "title": "Foutopsporing", + "desc": "De debugweergave toont een realtime overzicht van gevolgde objecten en hun statistieken. De objectlijst toont een samenvatting van gedetecteerde objecten met een tijdsvertraging.", + "debugging": "Foutopsporing", + "objectList": "Objectenlijst", + "noObjects": "Geen objecten", + "boundingBoxes": { + "title": "Objectkaders", + "desc": "Toon objectkaders rond gevolgde objecten", + "colors": { + "label": "Kleuren van objectkaders", + "info": "
  • Bij het opstarten wordt er een andere kleur toegewezen aan elk objectlabel
  • Een dunne donkerblauwe lijn geeft aan dat het object op dit moment niet wordt gedetecteerd
  • Een dunne grijze lijn geeft aan dat het object als stilstaand wordt herkend
  • Een dikke lijn geeft aan dat het object het doelwit is van automatische tracking (indien ingeschakeld)
  • " + } + }, + "timestamp": { + "title": "Tijdstempel", + "desc": "Toon een tijdstempel als overlay op het beeld" + }, + "zones": { + "desc": "Toon een overzicht van alle gedefinieerde zones", + "title": "Zones" + }, + "mask": { + "title": "Bewegingsmaskers", + "desc": "Toon bewegingsmasker-polygonen" + }, + "motion": { + "title": "Bewegingskaders", + "desc": "Toon kaders rondom gebieden waar beweging wordt gedetecteerd", + "tips": "

    Bewegingskaders


    Rode kaders worden over het beeld geplaatst op de plekken waar momenteel beweging wordt gedetecteerd.

    " + }, + "regions": { + "title": "Regio's", + "desc": "Toon een kader rond het interessegebied dat naar de objectdetector wordt gestuurd", + "tips": "

    Interessekaders


    Heldergroene kaders worden over het beeld geplaatst op de interessegebieden die naar de objectdetector worden gestuurd.

    " + }, + "objectShapeFilterDrawing": { + "title": "Objectvormfilter tekenen", + "desc": "Teken een rechthoek op het beeld om details over oppervlakte en verhouding te bekijken", + "document": "Lees de documentatie ", + "area": "Gebied", + "tips": "Schakel deze optie in om een rechthoek op het camerabeeld te tekenen die de oppervlakte en verhouding weergeeft. Deze waarden kunnen vervolgens worden gebruikt om parameters voor het objectvormfilter in je configuratie in te stellen.", + "score": "Score", + "ratio": "Verhouding" + }, + "detectorDesc": "Frigate gebruikt je detectoren ({{detectors}}) om objecten in de videostream van je camera te detecteren.", + "paths": { + "title": "Paden", + "desc": "Toon belangrijke punten van het pad van het gevolgde object", + "tips": "

    Paden


    Lijnen en cirkels geven belangrijke punten aan waar het gevolgde object zich tijdens zijn levensduur heeft verplaatst.

    " + }, + "openCameraWebUI": "Open de webinterface van {{camera}}", + "audio": { + "title": "Audio", + "noAudioDetections": "Geen audiodetecties", + "score": "score", + "currentRMS": "Huidige RMS", + "currentdbFS": "Huidige dbFS" + } + }, + "users": { + "title": "Gebruikers", + "management": { + "desc": "Beheer de gebruikersaccounts van deze Frigate-installatie.", + "title": "Gebruikersbeheer" + }, + "addUser": "Gebruiker toevoegen", + "updatePassword": "Wachtwoord opnieuw instellen", + "toast": { + "success": { + "createUser": "Gebruiker {{user}} succesvol aangemaakt", + "deleteUser": "Gebruiker {{user}} succesvol verwijderd", + "updatePassword": "Wachtwoord succesvol bijgewerkt.", + "roleUpdated": "De rol bijgewerkt voor {{user}}" + }, + "error": { + "setPasswordFailed": "Het wachtwoord kon niet worden opgeslagen: {{errorMessage}}", + "createUserFailed": "Gebruiker aanmaken mislukt: {{errorMessage}}", + "deleteUserFailed": "Gebruiker verwijderen mislukt: {{errorMessage}}", + "roleUpdateFailed": "Rol bijwerken mislukt: {{errorMessage}}" + } + }, + "table": { + "actions": "Acties", + "role": "Rol", + "noUsers": "Geen gebruikers gevonden.", + "changeRole": "Gebruikersrol wijzigen", + "password": "Wachtwoord opnieuw instellen", + "deleteUser": "Verwijder gebruiker", + "username": "Gebruikersnaam" + }, + "dialog": { + "form": { + "user": { + "desc": "Alleen letters, cijfers, punten en onderstrepingstekens zijn toegestaan.", + "title": "Gebruikersnaam", + "placeholder": "Gebruikersnaam invoeren" + }, + "password": { + "title": "Wachtwoord", + "strength": { + "medium": "Matig", + "strong": "Sterk", + "veryStrong": "Zeer sterk", + "title": "Wachtwoordsterkte: ", + "weak": "Zwak" + }, + "match": "Wachtwoorden komen overeen", + "confirm": { + "title": "Wachtwoord bevestigen", + "placeholder": "Wachtwoord bevestigen" + }, + "placeholder": "Wachtwoord invoeren", + "notMatch": "Wachtwoorden komen niet overeen", + "show": "Wachtwoord weergeven", + "hide": "Wachtwoord verbergen", + "requirements": { + "title": "Wachtwoordvereisten:", + "length": "Minimaal 12 tekens", + "uppercase": "Minimaal één hoofdletter", + "digit": "Minimaal één cijfer", + "special": "Minimaal één speciaal teken (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nieuw wachtwoord", + "placeholder": "Voer een nieuw wachtwoord in", + "confirm": { + "placeholder": "Voer het nieuwe wachtwoord opnieuw in" + } + }, + "usernameIsRequired": "Gebruikersnaam is vereist", + "passwordIsRequired": "Wachtwoord is vereist", + "currentPassword": { + "title": "Huidig wachtwoord", + "placeholder": "Voer uw huidige wachtwoord in" + } + }, + "createUser": { + "title": "Nieuwe gebruiker aanmaken", + "desc": "Voeg een nieuw gebruikersaccount toe en geef een rol op voor toegang tot onderdelen van de Frigate-interface.", + "usernameOnlyInclude": "Gebruikersnaam mag alleen letters, cijfers, . of _ bevatten", + "confirmPassword": "Bevestig uw wachtwoord" + }, + "deleteUser": { + "title": "Verwijder gebruiker", + "warn": "Weet je zeker dat je {{username}} wilt verwijderen?", + "desc": "Deze actie kan niet ongedaan worden gemaakt. Het gebruikersaccount wordt permanent verwijderd, samen met alle bijbehorende gegevens." + }, + "changeRole": { + "desc": "Machtigingen bijwerken voor {{username}}", + "title": "Gebruikersrol wijzigen", + "roleInfo": { + "intro": "Selecteer een gepaste rol voor deze gebruiker:", + "admin": "Beheerder", + "adminDesc": "Volledige toegang tot alle functies.", + "viewer": "Kijker", + "viewerDesc": "Alleen toegang tot Live-dashboards, Beoordelen, Verkennen en Exports.", + "customDesc": "Aangepaste rol met specifieke cameratoegang." + }, + "select": "Selecteer een rol" + }, + "passwordSetting": { + "setPassword": "Wachtwoord instellen", + "updatePassword": "Wachtwoord bijwerken voor {{username}}", + "desc": "Maak een sterk wachtwoord aan om dit account te beveiligen.", + "cannotBeEmpty": "Het wachtwoord kan niet leeg zijn", + "doNotMatch": "Wachtwoorden komen niet overeen", + "currentPasswordRequired": "Huidig wachtwoord is vereist", + "incorrectCurrentPassword": "Het huidige wachtwoord is onjuist", + "passwordVerificationFailed": "Wachtwoord kan niet worden geverifieerd", + "multiDeviceWarning": "Op alle andere apparaten waarop u bent ingelogd, moet u binnen {{refresh_time}} opnieuw inloggen.", + "multiDeviceAdmin": "Je kunt ook alle gebruikers forceren zich onmiddellijk opnieuw te authenticeren door je JWT-geheim te roteren." + } + } + }, + "notification": { + "notificationSettings": { + "title": "Meldingen instellen", + "desc": "Frigate kan rechtstreeks pushmeldingen naar uw apparaat verzenden als het in de browser actief is of als een PWA geïnstalleerd is.", + "documentation": "Lees de documentatie" + }, + "notificationUnavailable": { + "title": "Meldingen niet beschikbaar", + "documentation": "Lees de documentatie", + "desc": "Webpushmeldingen vereisen een veilige omgeving (https://…). Dit is een beperking van de browser. Open Frigate via een beveiligde verbinding om meldingen te kunnen ontvangen." + }, + "globalSettings": { + "title": "Globaale instellingen", + "desc": "Meldingen voor specifieke camera's op alle geregistreerde apparaten tijdelijk uitschakelen." + }, + "email": { + "title": "E-mail", + "placeholder": "bijv. voorbeeld@email.com", + "desc": "Een geldig e-mailadres is verplicht en wordt gebruikt om je te waarschuwen als er problemen zijn met de pushmeldingsdienst." + }, + "cameras": { + "noCameras": "Geen camera's beschikbaar", + "desc": "Selecteer voor welke camera's je meldingen wilt inschakelen.", + "title": "Camera's" + }, + "deviceSpecific": "Apparaatspecifieke instellingen", + "active": "Meldingen actief", + "suspendTime": { + "5minutes": "Onderbreek voor 5 minuten", + "30minutes": "Onderbreek voor 30 minuten", + "1hour": "Onderbreek voor 1 uur", + "12hours": "Onderbreek voor 12 uur", + "24hours": "Onderbreek voor 24 uur", + "untilRestart": "Opschorten tot herstart", + "10minutes": "Onderbreek voor 10 minuten", + "suspend": "Pauzeren" + }, + "cancelSuspension": "Onderbreking annuleren", + "toast": { + "success": { + "settingSaved": "De instellingen voor meldingen zijn opgeslagen.", + "registered": "Succesvol geregistreerd voor meldingen. Het opnieuw starten van Frigate is vereist voordat meldingen kunnen worden verzonden (inclusief een testmelding)." + }, + "error": { + "registerFailed": "Het opslaan van de meldingsregistratie is mislukt." + } + }, + "title": "Meldingen", + "sendTestNotification": "Stuur een testmelding", + "registerDevice": "Registreer dit apparaat", + "unregisterDevice": "Dit apparaat afmelden", + "suspended": "Meldingen onderbroken {{time}}", + "unsavedChanges": "Niet-opgeslagen wijzigingen in meldingen", + "unsavedRegistrations": "Niet-opgeslagen notificatieregistraties" + }, + "frigatePlus": { + "title": "Frigate+ Instellingen", + "apiKey": { + "title": "Frigate+ API-sleutel", + "plusLink": "Lees meer over Frigate+", + "validated": "Frigate+ API-sleutel is gedetecteerd en gevalideerd", + "desc": "Met de Frigate+ API-sleutel is integratie met de Frigate+ service mogelijk.", + "notValidated": "Frigate+ API-sleutel wordt niet gedetecteerd of niet gevalideerd" + }, + "snapshotConfig": { + "title": "Snapshot-configuratie", + "desc": "Om te verzenden naar Frigate+ moeten snapshots ingeschakeld zijn in je configuratie.", + "documentation": "Lees de documentatie", + "table": { + "camera": "Camera", + "snapshots": "Snapshots", + "cleanCopySnapshots": "clean_copy Snapshots" + }, + "cleanCopyWarning": "Sommige camera's hebben snapshots uitgeschakeld" + }, + "modelInfo": { + "title": "Modelinformatie", + "modelType": "Type model", + "trainDate": "Trainingsdatum", + "baseModel": "Basismodel", + "cameras": "Camera's", + "error": "Het laden van modelinformatie is mislukt", + "loadingAvailableModels": "Beschikbare modellen laden…", + "modelSelect": "Je beschikbare modellen op Frigate+ kunnen hier worden geselecteerd. Houd er rekening mee dat alleen modellen die compatibel zijn met je huidige detectorconfiguratie geselecteerd kunnen worden.", + "dimensions": "Afmetingen", + "supportedDetectors": "Ondersteunde detectoren", + "availableModels": "Beschikbare modellen", + "loading": "Modelinformatie laden…", + "plusModelType": { + "baseModel": "Basismodel", + "userModel": "Verfijnd" + }, + "noModelLoaded": "Er is momenteel geen Frigate+-model geladen.", + "selectModel": "Selecteren a model", + "noModelsAvailable": "Geen modellen beschikbaar", + "filter": { + "ariaLabel": "Modellen filteren op type", + "baseModels": "Basismodellen", + "fineTunedModels": "Verfijnde modellen" + } + }, + "toast": { + "success": "Frigate+ instellingen zijn opgeslagen. Herstart Frigate om de wijzigingen toe te passen.", + "error": "Configuratiewijzigingen konden niet worden opgeslagen: {{errorMessage}}" + }, + "restart_required": "Herstart vereist (Frigate+ model gewijzigd)", + "unsavedChanges": "Niet-opgeslagen wijzigingen in Frigate+ instellingen", + "description": "Frigate+ is een abonnementsdienst die toegang biedt tot extra functies en mogelijkheden voor je Frigate-installatie, waaronder het gebruik van aangepaste objectdetectiemodellen die op je eigen gegevens zijn getraind. Je kunt je Frigate+-modelinstellingen hier beheren.", + "cardTitles": { + "api": "API", + "currentModel": "Huidig model", + "otherModels": "Andere modellen", + "configuration": "Configuratie" + }, + "changeInDetectorsAndModel": "Van model wisselen" + }, + "enrichments": { + "semanticSearch": { + "reindexNow": { + "confirmDesc": "Weet u zeker dat u alle gevolgde object-embeddings opnieuw wilt indexeren? Dit proces wordt op de achtergrond uitgevoerd, maar kan uw CPU zwaar belasten en enige tijd in beslag nemen. U kunt de voortgang bekijken op de pagina Verkenner.", + "label": "Nu opnieuw indexeren", + "desc": "Opnieuw indexeren zal embeddings regenereren voor alle gevolgde objecten. Dit proces wordt op de achtergrond uitgevoerd en kan je CPU zwaar belasten en een behoorlijke hoeveelheid tijd in beslag nemen, afhankelijk van het aantal gevolgde objecten dat je hebt.", + "confirmButton": "Opnieuw indexeren", + "success": "Het herindexeren is succesvol gestart.", + "alreadyInProgress": "Het herindexeren is al bezig.", + "error": "Het opnieuw indexeren is mislukt: {{errorMessage}}", + "confirmTitle": "Bevestig herindexering" + }, + "modelSize": { + "large": { + "title": "groot", + "desc": "Het gebruik van large maakt gebruik van het volledige Jina-model en wordt automatisch op de GPU uitgevoerd als die beschikbaar is." + }, + "label": "Modelgrootte", + "desc": "De grootte van het model dat wordt gebruikt voor semantische zoekopdrachten.", + "small": { + "title": "klein", + "desc": "Het gebruik van small maakt gebruik van een gequantiseerde versie van het model die minder RAM verbruikt en sneller draait op de CPU, met een verwaarloosbaar verschil in embeddingkwaliteit." + } + }, + "title": "Semantisch zoeken", + "desc": "Semantisch zoeken in Frigate stelt je in staat om getraceerde objecten binnen je review-items te vinden, met behulp van de afbeelding zelf, een door de gebruiker gedefinieerde tekstbeschrijving of een automatisch gegenereerde beschrijving.", + "readTheDocumentation": "Lees de documentatie" + }, + "faceRecognition": { + "modelSize": { + "small": { + "desc": "Met small wordt een FaceNet-model voor gezichtsinbedding gebruikt dat efficiënt werkt op de meeste CPU's.", + "title": "klein" + }, + "desc": "De grootte van het model dat gebruikt wordt voor gezichtsherkenning.", + "label": "Modelgrootte", + "large": { + "title": "groot", + "desc": "Het gebruik van groot maakt gebruik van een ArcFace-gezichtsembeddingmodel en wordt automatisch op de GPU uitgevoerd als die beschikbaar is." + } + }, + "desc": "Gezichtsherkenning maakt het mogelijk om namen aan mensen toe te wijzen. Wanneer hun gezicht wordt herkend, wijst Frigate de naam van de persoon toe als sublabel. Deze informatie is opgenomen in de gebruikersinterface, filters en meldingen.", + "title": "Gezichtsherkenning", + "readTheDocumentation": "Lees de documentatie" + }, + "licensePlateRecognition": { + "desc": "Frigate kan kentekenplaten op voertuigen herkennen en automatisch de gedetecteerde tekens toevoegen aan het veld recognized_license_plate of een bekende naam als sublabel toekennen aan objecten van het type auto. Een veelvoorkomende toepassing is het uitlezen van kentekens van auto's die een oprit oprijden of voorbijrijden op straat.", + "title": "Kentekenherkenning", + "readTheDocumentation": "Lees de documentatie" + }, + "birdClassification": { + "desc": "Vogelclassificatie herkent bekende vogels met behulp van een gequantiseerd TensorFlow-model. Wanneer een bekende vogel wordt herkend, wordt de algemene naam toegevoegd als sublabel. Deze informatie wordt weergegeven in de interface, is beschikbaar in filters en wordt ook opgenomen in meldingen.", + "title": "Vogelclassificatie" + }, + "title": "Verrijkingsinstellingen", + "unsavedChanges": "Niet-opgeslagen wijzigingen in verrijkingsinstellingen", + "restart_required": "Opnieuw opstarten vereist (verrijkingsinstellingen gewijzigd)", + "toast": { + "success": "Verrijkingsinstellingen zijn opgeslagen. Start Frigate opnieuw op om je wijzigingen toe te passen.", + "error": "Configuratiewijzigingen konden niet worden opgeslagen: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Triggers", + "management": { + "title": "Triggers", + "desc": "Beheer triggers voor {{camera}}. Gebruik een thumbnail om te triggeren op vergelijkbare thumbnails van het door jou gevolgde object, of gebruik een objectbeschrijving om te triggeren op vergelijkbare beschrijvingen van de door jou opgegeven tekst." + }, + "addTrigger": "Trigger toevoegen", + "table": { + "name": "Naam", + "type": "Type", + "content": "Inhoud", + "threshold": "Drempel", + "actions": "Acties", + "noTriggers": "Er zijn geen triggers geconfigureerd voor deze camera.", + "edit": "Bewerken", + "deleteTrigger": "Trigger verwijderen", + "lastTriggered": "Laatst geactiveerd" + }, + "type": { + "thumbnail": "Thumbnail", + "description": "Beschrijving" + }, + "actions": { + "alert": "Markeren als waarschuwing", + "notification": "Melding verzenden", + "sub_label": "Sublabel toevoegen", + "attribute": "Attribuut toevoegen" + }, + "dialog": { + "createTrigger": { + "title": "Trigger aanmaken", + "desc": "Maak een trigger voor camera {{camera}}" + }, + "editTrigger": { + "title": "Trigger bewerken", + "desc": "Wijzig de instellingen voor de trigger op camera {{camera}}" + }, + "deleteTrigger": { + "title": "Trigger verwijderen", + "desc": "Weet u zeker dat u de trigger {{triggerName}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt." + }, + "form": { + "name": { + "title": "Naam", + "placeholder": "Geef deze trigger een naam", + "error": { + "minLength": "Het veld moet minimaal 2 tekens lang zijn.", + "invalidCharacters": "Dit veld mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten.", + "alreadyExists": "Er bestaat al een trigger met deze naam voor deze camera." + }, + "description": "Voer een unieke naam of beschrijving in om deze trigger te identificeren" + }, + "enabled": { + "description": "Deze trigger in- of uitschakelen" + }, + "type": { + "title": "Type", + "placeholder": "Selecteer het type trigger", + "description": "Activeer wanneer een vergelijkbare beschrijving van een gevolgd object wordt gedetecteerd", + "thumbnail": "Activeer wanneer een vergelijkbare thumbnail van een gevolgd object wordt gedetecteerd" + }, + "content": { + "title": "Inhoud", + "imagePlaceholder": "Selecteer een thumbnail", + "textPlaceholder": "Tekst invoeren", + "imageDesc": "Alleen de meest recente 100 thumbnails worden weergegeven. Als je de gewenste thumbnail niet kunt vinden, bekijk dan eerdere objecten in Verkennen en stel daar een trigger in via het menu.", + "textDesc": "Voer tekst in om deze actie te activeren wanneer een vergelijkbare beschrijving van een gevolgd object wordt gedetecteerd.", + "error": { + "required": "Inhoud is vereist." + } + }, + "threshold": { + "title": "Drempel", + "error": { + "min": "De drempelwaarde moet minimaal 0 zijn", + "max": "De drempelwaarde mag maximaal 1 zijn" + }, + "desc": "Stel de vergelijkingsdrempel in voor deze trigger. Een hogere drempel betekent dat er een nauwere overeenkomst vereist is om de trigger te activeren." + }, + "actions": { + "title": "Acties", + "desc": "Standaard stuurt Frigate een MQTT-bericht voor alle triggers. Sublabels voegen de triggernaam toe aan het objectlabel. Attributen zijn doorzoekbare metadata die afzonderlijk worden opgeslagen in de metadata van het gevolgde object.", + "error": { + "min": "Er moet ten minste één actie worden geselecteerd." + } + }, + "friendly_name": { + "title": "Gebruiksvriendelijke naam", + "placeholder": "Geef een naam of beschrijf deze trigger", + "description": "Een optionele gebruiksvriendelijke naam of beschrijvende tekst voor deze trigger." + } + } + }, + "toast": { + "success": { + "createTrigger": "Trigger {{name}} is succesvol aangemaakt.", + "updateTrigger": "Trigger {{name}} is succesvol bijgewerkt.", + "deleteTrigger": "Trigger {{name}} succesvol verwijderd." + }, + "error": { + "createTriggerFailed": "Trigger kan niet worden gemaakt: {{errorMessage}}", + "updateTriggerFailed": "Trigger kan niet worden bijgewerkt: {{errorMessage}}", + "deleteTriggerFailed": "Trigger kan niet worden verwijderd: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Semantisch zoeken is uitgeschakeld", + "desc": "Semantisch zoeken moet ingeschakeld zijn om triggers te kunnen gebruiken." + }, + "wizard": { + "title": "Trigger maken", + "step1": { + "description": "Configureer de basisinstellingen voor uw trigger." + }, + "step2": { + "description": "Stel de inhoud in die deze trigger activeert." + }, + "step3": { + "description": "Configureer de drempelwaarde en acties voor deze trigger." + }, + "steps": { + "nameAndType": "Naam en type", + "configureData": "Gegevens configureren", + "thresholdAndActions": "Drempel en acties" + } + } + }, + "roles": { + "management": { + "title": "Beheer van kijkersrollen", + "desc": "Beheer aangepaste kijkersrollen en hun camera-toegangsrechten voor deze Frigate-instantie." + }, + "addRole": "Rol toevoegen", + "table": { + "role": "Rol", + "cameras": "Camera's", + "actions": "Acties", + "noRoles": "Er zijn geen aangepaste rollen gevonden.", + "editCameras": "Camera's bewerken", + "deleteRole": "Rol verwijderen" + }, + "toast": { + "success": { + "createRole": "Rol {{role}} succesvol aangemaakt", + "updateCameras": "Camera's bijgewerkt voor rol {{role}}", + "deleteRole": "Rol {{role}} succesvol verwijderd", + "userRolesUpdated_one": "{{count}} gebruiker die aan deze rol was toegewezen, is bijgewerkt naar de rol ‘kijker’, die toegang heeft tot alle camera’s.", + "userRolesUpdated_other": "{{count}} gebruikers die aan deze rol waren toegewezen, zijn bijgewerkt naar de rol ‘kijker’, die toegang heeft tot alle camera’s." + }, + "error": { + "createRoleFailed": "Kan rol niet aanmaken: {{errorMessage}}", + "updateCamerasFailed": "Het is niet gelukt om de camera's bij te werken: {{errorMessage}}", + "deleteRoleFailed": "Kan rol niet verwijderen: {{errorMessage}}", + "userUpdateFailed": "Het bijwerken van gebruikersrollen is mislukt: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Nieuwe rol maken", + "desc": "Voeg een nieuwe rol toe en specificeer de camera-toegangsrechten." + }, + "editCameras": { + "title": "Camera’s voor rol bewerken", + "desc": "Werk de camera-toegang bij voor de rol {{role}}." + }, + "deleteRole": { + "title": "Rol verwijderen", + "desc": "Deze actie kan niet ongedaan worden gemaakt. De rol wordt permanent verwijderd en alle gebruikers met deze rol worden toegewezen aan de rol ‘kijker’, die toegang geeft tot alle camera’s.", + "warn": "Weet u zeker dat u {{role}} wilt verwijderen?", + "deleting": "Verwijderen..." + }, + "form": { + "role": { + "title": "Rolnaam", + "placeholder": "Voer rolnaam in", + "desc": "Alleen letters, cijfers, punten en underscores zijn toegestaan.", + "roleIsRequired": "Rolnaam is vereist", + "roleOnlyInclude": "De rolnaam mag alleen letters, cijfers, . of _ bevatten", + "roleExists": "Er bestaat al een rol met deze naam." + }, + "cameras": { + "title": "Camera's", + "desc": "Selecteer de camera's waartoe deze rol toegang heeft. Er is minimaal één camera vereist.", + "required": "Er moet minimaal één camera worden geselecteerd." + } + } + } + }, + "cameraWizard": { + "title": "Camera toevoegen", + "description": "Volg de onderstaande stappen om een nieuwe camera toe te voegen aan uw Frigate-installatie.", + "steps": { + "nameAndConnection": "Naam & Verbinding", + "streamConfiguration": "Streamconfiguratie", + "validationAndTesting": "Validatie & testen", + "probeOrSnapshot": "Test of Snapshot" + }, + "save": { + "success": "Nieuwe camera {{cameraName}} succesvol opgeslagen.", + "failure": "Fout bij het opslaan van {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolutie", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Geef een geldige stream-URL op", + "testFailed": "Streamtest mislukt: {{error}}" + }, + "step1": { + "description": "Voer de gegevens van uw camera in en kies ervoor om de camera te scannen of selecteer handmatig het merk.", + "cameraName": "Cameranaam", + "cameraNamePlaceholder": "bijv. voordeur of achtertuin camera", + "host": "Host/IP-adres", + "port": "Port", + "username": "Gebruikersnaam", + "usernamePlaceholder": "Optioneel", + "password": "Wachtwoord", + "passwordPlaceholder": "Optioneel", + "selectTransport": "Selecteer transportprotocol", + "cameraBrand": "Cameramerk", + "selectBrand": "Selecteer cameramerk voor URL-sjabloon", + "customUrl": "Aangepaste stream-URL", + "brandInformation": "Merkinformatie", + "brandUrlFormat": "Voor camera's met het RTSP URL-formaat als: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://gebruikersnaam:wachtwoord@host:poort/pad", + "testConnection": "Testverbinding", + "testSuccess": "Verbindingstest succesvol!", + "testFailed": "Verbindingstest mislukt. Controleer uw invoer en probeer het opnieuw.", + "streamDetails": "Streamdetails", + "warnings": { + "noSnapshot": "Er kan geen snapshot worden opgehaald uit de geconfigureerde stream." + }, + "errors": { + "brandOrCustomUrlRequired": "Selecteer een cameramerk met host/IP of kies 'Overig' voor een aangepaste URL", + "nameRequired": "Cameranaam is vereist", + "nameLength": "De cameranaam mag maximaal 64 tekens lang zijn", + "invalidCharacters": "Cameranaam bevat ongeldige tekens", + "nameExists": "Cameranaam bestaat al", + "brands": { + "reolink-rtsp": "Reolink RTSP wordt niet aanbevolen. Schakel HTTP in via de firmware-instellingen van de camera en start de wizard opnieuw." + }, + "customUrlRtspRequired": "Aangepaste URL’s moeten beginnen met “rtsp://”. Handmatige configuratie is vereist voor camera­streams die geen RTSP gebruiken." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Camerametadata wordt onderzocht...", + "fetchingSnapshot": "Camerasnapshot ophalen..." + }, + "connectionSettings": "Verbindingsinstellingen", + "detectionMethod": "Stream-detectiemethode", + "onvifPort": "ONVIF-poort", + "probeMode": "Camera testen", + "manualMode": "Handmatige selectie", + "detectionMethodDescription": "Test de camera met ONVIF (indien ondersteund) om de stream-URL’s van de camera te vinden, of selecteer handmatig het cameramerk om vooraf gedefinieerde URL’s te gebruiken. Om een aangepaste RTSP-URL in te voeren, kies de handmatige methode en selecteer “Anders”.", + "onvifPortDescription": "Voor camera's die ONVIF ondersteunen, is dit meestal 80 of 8080.", + "useDigestAuth": "Gebruik digest-authenticatie", + "useDigestAuthDescription": "Gebruik HTTP-digestauthenticatie voor ONVIF. Sommige camera’s vereisen mogelijk een aparte ONVIF-gebruikersnaam en -wachtwoord in plaats van de standaard ‘admin’ gebruiker." + }, + "step2": { + "description": "Controleer de camera op beschikbare streams of configureer handmatige instellingen op basis van de door u geselecteerde detectiemethode.", + "streamsTitle": "Camerastreams", + "addStream": "Stream toevoegen", + "addAnotherStream": "Voeg een extra stream toe", + "streamTitle": "Stream {{number}}", + "streamUrl": "Stream-URL", + "streamUrlPlaceholder": "rtsp://gebruikersnaam:wachtwoord@host:poort/pad", + "url": "URL", + "resolution": "Resolutie", + "selectResolution": "Selecteer resolutie", + "quality": "Kwaliteit", + "selectQuality": "Selecteer kwaliteit", + "roles": "Functie", + "roleLabels": { + "detect": "Objectdetectie", + "record": "Opname", + "audio": "Audio" + }, + "testStream": "Testverbinding", + "testSuccess": "Verbindingstest succesvol!", + "testFailed": "Verbindingstest mislukt. Controleer uw invoer en probeer het opnieuw.", + "testFailedTitle": "Test mislukt", + "connected": "Aangesloten", + "notConnected": "Niet verbonden", + "featuresTitle": "Functies", + "go2rtc": "Verminder verbindingen met de camera", + "detectRoleWarning": "Er moet minimaal één stream de rol 'detecteren' hebben om door te kunnen gaan.", + "rolesPopover": { + "title": "Streamrollen", + "detect": "Hoofdfeed voor objectdetectie.", + "record": "Slaat segmenten van de videofeed op op basis van de configuratie-instellingen.", + "audio": "Feed voor op audio gebaseerde detectie." + }, + "featuresPopover": { + "title": "Streamfuncties", + "description": "Gebruik go2rtc-herstreaming om het aantal verbindingen met je camera te verminderen." + }, + "streamDetails": "Streamdetails", + "probing": "Camera wordt getest...", + "retry": "Opnieuw proberen", + "testing": { + "probingMetadata": "Camera-metadata onderzoeken...", + "fetchingSnapshot": "Camerasnapshot ophalen..." + }, + "probeFailed": "Het testen van de camera is mislukt: {{error}}", + "probingDevice": "Onderzoekapparaat...", + "probeSuccessful": "Test succesvol", + "probeError": "Testfout", + "probeNoSuccess": "Test mislukt", + "deviceInfo": "Apparaatinformatie", + "manufacturer": "Fabrikant", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profielen", + "ptzSupport": "PTZ-ondersteuning", + "autotrackingSupport": "Ondersteuning voor automatische tracking", + "presets": "Standaardinstellingen", + "rtspCandidates": "RTSP-kandidaten", + "rtspCandidatesDescription": "De volgende RTSP-URL's zijn gevonden door de camera te scannen. Test de verbinding om de metagegevens van de stream te bekijken.", + "noRtspCandidates": "Er zijn geen RTSP-URL’s gevonden van de camera. Je inloggegevens zijn mogelijk onjuist, of de camera ondersteunt ONVIF of de gebruikte methode voor het ophalen van RTSP-URL’s niet. Ga terug en voer de RTSP-URL handmatig in.", + "candidateStreamTitle": "Kandidaat {{number}}", + "useCandidate": "Gebruik", + "uriCopy": "Kopiëren", + "uriCopied": "URI gekopieerd naar klembord", + "testConnection": "Testverbinding", + "toggleUriView": "Klik om te schakelen tussen volledige URI-weergave", + "errors": { + "hostRequired": "Host/IP-adres is vereist" + } + }, + "step3": { + "description": "Configureer streamrollen en voeg extra streams toe voor uw camera.", + "validationTitle": "Streamvalidatie", + "connectAllStreams": "Verbind alle streams", + "reconnectionSuccess": "Opnieuw verbinden gelukt.", + "reconnectionPartial": "Bij sommige streams kon de verbinding niet worden hersteld.", + "streamUnavailable": "Streamvoorbeeld niet beschikbaar", + "reload": "Herladen", + "connecting": "Verbinden...", + "streamTitle": "Stream {{number}}", + "valid": "Geldig", + "failed": "Mislukt", + "notTested": "Niet getest", + "connectStream": "Verbinden", + "connectingStream": "Verbinden", + "disconnectStream": "Verbreek verbinding", + "estimatedBandwidth": "Geschatte bandbreedte", + "roles": "Functie", + "none": "Niets", + "error": "Fout", + "streamValidated": "Stream {{number}} is succesvol gevalideerd", + "streamValidationFailed": "Stream {{number}} validatie mislukt", + "saveAndApply": "Nieuwe camera opslaan", + "saveError": "Ongeldige configuratie, Controleer uw instellingen.", + "issues": { + "title": "Streamvalidatie", + "videoCodecGood": "Videocodec is {{codec}}.", + "audioCodecGood": "Audiocodec is {{codec}}.", + "noAudioWarning": "Geen audio gedetecteerd voor deze stream, opnames bevatten geen audio.", + "audioCodecRecordError": "De AAC-audiocodec is vereist om audio in opnames te ondersteunen.", + "audioCodecRequired": "Ter ondersteuning van audiodetectie is een audiostream vereist.", + "restreamingWarning": "Als u het aantal verbindingen met de camera voor de opnamestream vermindert, kan het CPU-gebruik iets toenemen.", + "dahua": { + "substreamWarning": "Substream 1 is beperkt tot een lage resolutie. Veel Dahua / Amcrest / EmpireTech camera’s ondersteunen extra substreams die in de instellingen van de camera ingeschakeld moeten worden. Het wordt aanbevolen deze streams te controleren en te gebruiken indien beschikbaar." + }, + "hikvision": { + "substreamWarning": "Substream 1 is beperkt tot een lage resolutie. Veel Hikvision-camera’s ondersteunen extra substreams die in de instellingen van de camera ingeschakeld moeten worden. Het wordt aanbevolen deze streams te controleren en te gebruiken indien beschikbaar." + }, + "resolutionHigh": "Een resolutie van {{resolution}} kan leiden tot een verhoogd gebruik van systeembronnen.", + "resolutionLow": "Een resolutie van {{resolution}} kan te laag zijn voor betrouwbare detectie van kleine objecten." + }, + "ffmpegModule": "Gebruik stream-compatibiliteitsmodus", + "ffmpegModuleDescription": "Als de stream na meerdere pogingen niet wordt geladen, probeer dit dan in te schakelen. Wanneer deze optie is ingeschakeld, gebruikt Frigate de ffmpeg-module samen met go2rtc. Dit kan zorgen voor een betere compatibiliteit met sommige camerastreams.", + "streamsTitle": "Camerastreams", + "addStream": "Stream toevoegen", + "addAnotherStream": "Voeg een extra stream toe", + "streamUrl": "Stream-URL", + "streamUrlPlaceholder": "rtsp://gebruikersnaam:wachtwoord@host:poort/pad", + "selectStream": "Selecteer een stream", + "searchCandidates": "Zoek kandidaten...", + "noStreamFound": "Geen stream gevonden", + "url": "URL", + "resolution": "Resolutie", + "selectResolution": "Selecteer resolutie", + "quality": "Kwaliteit", + "selectQuality": "Selecteer kwaliteit", + "roleLabels": { + "detect": "Objectdetectie", + "record": "Opname", + "audio": "Audio" + }, + "testStream": "Testverbinding", + "testSuccess": "Streamtest succesvol!", + "testFailed": "Streamtest mislukt", + "testFailedTitle": "Test mislukt", + "connected": "Aangesloten", + "notConnected": "Niet verbonden", + "featuresTitle": "Functies", + "go2rtc": "Verminder verbindingen met de camera", + "detectRoleWarning": "Er moet minimaal één stream de rol 'detecteren' hebben om door te kunnen gaan.", + "rolesPopover": { + "title": "Streamrollen", + "detect": "Hoofdstream voor objectdetectie.", + "record": "Slaat segmenten van de videostream op op basis van de configuratie-instellingen.", + "audio": "Stream voor op audio gebaseerde detectie." + }, + "featuresPopover": { + "title": "Streamfuncties", + "description": "Gebruik go2rtc-herstreaming om het aantal verbindingen met je camera te verminderen." + } + }, + "step4": { + "description": "Laatste controle en analyse voordat je je nieuwe camera opslaat. Verbind elke stream voordat je opslaat.", + "validationTitle": "Streamvalidatie", + "connectAllStreams": "Verbind alle streams", + "reconnectionSuccess": "Opnieuw verbinden gelukt.", + "reconnectionPartial": "Bij sommige streams kon de verbinding niet worden hersteld.", + "streamUnavailable": "Streamvoorbeeld niet beschikbaar", + "reload": "Herladen", + "connecting": "Verbinden...", + "streamTitle": "Stream {{number}}", + "valid": "Geldig", + "failed": "Mislukt", + "notTested": "Niet getest", + "connectStream": "Verbinden", + "connectingStream": "Verbinden", + "disconnectStream": "Verbreek verbinding", + "estimatedBandwidth": "Geschatte bandbreedte", + "roles": "Rollen", + "ffmpegModule": "Gebruik stream-compatibiliteitsmodus", + "ffmpegModuleDescription": "Als de stream na meerdere pogingen niet wordt geladen, probeer dit dan in te schakelen. Wanneer deze optie is ingeschakeld, gebruikt Frigate de ffmpeg-module samen met go2rtc. Dit kan zorgen voor een betere compatibiliteit met sommige camerastreams.", + "none": "Geen", + "error": "Fout", + "streamValidated": "Stream {{number}} is succesvol gevalideerd", + "streamValidationFailed": "Stream {{number}} validatie mislukt", + "saveAndApply": "Nieuwe camera opslaan", + "saveError": "Ongeldige configuratie, Controleer uw instellingen.", + "issues": { + "title": "Streamvalidatie", + "videoCodecGood": "Videocodec is {{codec}}.", + "audioCodecGood": "Audiocodec is {{codec}}.", + "resolutionHigh": "Een resolutie van {{resolution}} kan leiden tot een verhoogd gebruik van systeembronnen.", + "resolutionLow": "Een resolutie van {{resolution}} kan te laag zijn voor betrouwbare detectie van kleine objecten.", + "noAudioWarning": "Geen audio gedetecteerd voor deze stream, opnames bevatten geen audio.", + "audioCodecRecordError": "De AAC-audiocodec is vereist om audio in opnames te ondersteunen.", + "audioCodecRequired": "Ter ondersteuning van audiodetectie is een audiostream vereist.", + "restreamingWarning": "Als u het aantal verbindingen met de camera voor de opnamestream vermindert, kan het CPU-gebruik iets toenemen.", + "brands": { + "reolink-rtsp": "Reolink RTSP wordt niet aanbevolen. Schakel HTTP in via de firmware-instellingen van de camera en start de wizard opnieuw.", + "reolink-http": "Reolink HTTP-streams moeten FFmpeg gebruiken voor een betere compatibiliteit. Schakel ‘stream-compatibiliteitsmodus’ in voor deze stream." + }, + "dahua": { + "substreamWarning": "Substream 1 is beperkt tot een lage resolutie. Veel Dahua / Amcrest / EmpireTech camera’s ondersteunen extra substreams die in de instellingen van de camera ingeschakeld moeten worden. Het wordt aanbevolen deze streams te controleren en te gebruiken indien beschikbaar." + }, + "hikvision": { + "substreamWarning": "Substream 1 is beperkt tot een lage resolutie. Veel Hikvision-camera’s ondersteunen extra substreams die in de instellingen van de camera ingeschakeld moeten worden. Het wordt aanbevolen deze streams te controleren en te gebruiken indien beschikbaar." + }, + "resolutionUnknown": "De resolutie van deze stream kon niet worden uitgelezen. Stel de detectieresolutie handmatig in via Instellingen of in je configuratie." + } + } + }, + "cameraManagement": { + "title": "Camera’s beheren", + "addCamera": "Nieuwe camera toevoegen", + "editCamera": "Camera bewerken:", + "selectCamera": "Selecteer een camera", + "backToSettings": "Terug naar camera-instellingen", + "streams": { + "title": "Camera's in-/uitschakelen", + "desc": "Schakel een camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig door Frigate. Detectie, opname en foutopsporing zijn dan niet beschikbaar.
    Let op: dit schakelt go2rtc-restreams niet uit.", + "enableLabel": "Ingeschakeld cameras", + "enableDesc": "Schakel een ingeschakelde camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig. Detectie, opname en foutopsporing zijn dan niet beschikbaar.
    Let op: dit schakelt go2rtc-restreams niet uit.", + "disableLabel": "Uitgeschakeld cameras", + "disableDesc": "Schakel een camera in die momenteel niet zichtbaar is in de UI en is uitgeschakeld in de configuratie. Na het inschakelen is een herstart van Frigate vereist.", + "enableSuccess": "{{cameraName}} ingeschakeld in de configuratie. Herstart Frigate om de wijzigingen toe te passen.", + "friendlyName": { + "edit": "Cameranaam bewerken", + "title": "Weergavenaam bewerken", + "description": "Stel de vriendelijke naam in die voor deze camera in de Frigate-UI wordt weergegeven. Laat leeg om de camera-ID te gebruiken.", + "rename": "Hernoemen" + } + }, + "cameraConfig": { + "add": "Camera toevoegen", + "edit": "Camera bewerken", + "description": "Configureer de camera-instellingen, inclusief streaming-inputs en functies.", + "name": "Cameranaam", + "nameRequired": "Cameranaam is vereist", + "nameLength": "Cameranaam mag niet langer zijn dan 64 tekens.", + "namePlaceholder": "bijv. voordeur of achtertuin camera", + "enabled": "Ingeschakeld", + "ffmpeg": { + "inputs": "Streams-Input", + "path": "Streampad", + "pathRequired": "Streampad is vereist", + "pathPlaceholder": "rtsp://...", + "roles": "Functie", + "rolesRequired": "Er is ten minste één functie vereist", + "rolesUnique": "Elke functie (audio, detecteren, opnemen) kan slechts aan één stream worden toegewezen", + "addInput": "Inputstream toevoegen", + "removeInput": "Inputstream verwijderen", + "inputsRequired": "Er is ten minste één stream-input vereist" + }, + "go2rtcStreams": "go2C Streams", + "streamUrls": "Stream URLs", + "addUrl": "URL toevoegen", + "addGo2rtcStream": "Voeg go2rtc Stream toe", + "toast": { + "success": "Camera {{cameraName}} is succesvol opgeslagen" + } + }, + "description": "Voeg camera's toe, bewerk of verwijder ze, bepaal welke camera's zijn ingeschakeld en configureer overschrijvingen per profiel en cameratype. Kies voor streams, detectie, beweging en andere cameraspecifieke instellingen de betreffende sectie onder Cameraconfiguratie.", + "deleteCamera": "Verwijderen Camera", + "deleteCameraDialog": { + "title": "Verwijderen Camera", + "description": "Het verwijderen van een camera verwijdert permanent alle opnames, gevolgde objecten en configuratie voor die camera. Eventuele go2rtc-streams die aan deze camera zijn gekoppeld, moeten mogelijk nog handmatig worden verwijderd.", + "selectPlaceholder": "Kies camera...", + "confirmTitle": "Weet je het zeker?", + "confirmWarning": "Het verwijderen van {{cameraName}} kan niet ongedaan worden gemaakt.", + "deleteExports": "Verwijder ook exports voor deze camera", + "confirmButton": "Verwijderen Permanently", + "success": "Camera {{cameraName}} is succesvol verwijderd", + "error": "Kan camera {{cameraName}} niet verwijderen" + }, + "profiles": { + "title": "Profiel Camera Overrides", + "selectLabel": "Selecteren profile", + "description": "Configureer welke camera's zijn ingeschakeld of uitgeschakeld wanneer een profiel wordt geactiveerd. Camera's die op \"Overnemen\" staan, behouden hun basisstatus.", + "inherit": "Overnemen", + "enabled": "Ingeschakeld", + "disabled": "Uitgeschakeld" + }, + "cameraType": { + "title": "Cameratype", + "label": "Cameratype", + "description": "Stel het type voor elke camera in. Speciale LPR-camera's zijn camera's met één doel en krachtige optische zoom om kentekens van voertuigen op afstand vast te leggen. De meeste camera's moeten het normale cameratype gebruiken, tenzij de camera specifiek voor LPR is bedoeld en een nauwkeurig gericht beeld op kentekens heeft.", + "normal": "Normal", + "dedicatedLpr": "Speciale LPR", + "saveSuccess": "Cameratype voor {{cameraName}} bijgewerkt. Herstart Frigate om de wijzigingen toe te passen." + } + }, + "cameraReview": { + "title": "Camerabeoordelings-instellingen", + "object_descriptions": { + "title": "AI-gegenereerde objectomschrijvingen", + "desc": "AI-gegenereerde objectomschrijvingen tijdelijk uitschakelen voor deze camera totdat Frigate opnieuw opstart. Wanneer uitgeschakeld, zullen omschrijvingen van gevolgde objecten op deze camera niet aangevraagd worden." + }, + "review_descriptions": { + "title": "Generatieve-AI Beoordelingsbeschrijvingen", + "desc": "Schakel tijdelijk generatieve AI-beoordelingsbeschrijvingen voor deze camera in/uit totdat Frigate opnieuw wordt opgestart. Wanneer deze functie is uitgeschakeld, worden er geen door AI gegenereerde beschrijvingen gevraagd voor beoordelingsitems op deze camera." + }, + "review": { + "title": "Beoordeel", + "desc": "Schakel waarschuwingen en detecties voor deze camera tijdelijk in of uit totdat Frigate opnieuw wordt gestart. Wanneer uitgeschakeld, worden er geen nieuwe beoordelingsitems gegenereerd. ", + "alerts": "Meldingen ", + "detections": "Detecties " + }, + "reviewClassification": { + "title": "Beoordelingsclassificatie", + "desc": "Frigate categoriseert beoordelingsitems als meldingen en detecties.Standaard worden alle person- en car-objecten als meldingen beschouwd. Je kunt de categorisatie verfijnen door zones te configureren waarin uitsluitend deze objecten gedetecteerd moeten worden.", + "noDefinedZones": "Voor deze camera zijn nog geen zones ingesteld.", + "objectAlertsTips": "Alle {{alertsLabels}}-objecten op {{cameraName}} worden weergegeven als meldingen.", + "zoneObjectAlertsTips": "Alle {{alertsLabels}}-objecten die zijn gedetecteerd in {{zone}} op {{cameraName}} worden weergegeven als meldingen.", + "objectDetectionsTips": "Alle {{detectionsLabels}}-objecten die op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties ongeacht in welke zone ze zich bevinden.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-objecten die in {{zone}} op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties.", + "notSelectDetections": "Alle {{detectionsLabels}}-objecten die in {{zone}} op {{cameraName}} worden gedetecteerd en niet als melding zijn gecategoriseerd, worden weergegeven als detecties – ongeacht in welke zone ze zich bevinden.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}}-objecten die op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties ongeacht in welke zone ze zich bevinden." + }, + "unsavedChanges": "Niet-opgeslagen classificatie-instellingen voor {{camera}}", + "selectAlertsZones": "Zones selecteren voor meldingen", + "selectDetectionsZones": "Selecteer zones voor detecties", + "limitDetections": "Beperk detecties tot specifieke zones", + "toast": { + "success": "Configuratie voor beoordelingsclassificatie is opgeslagen. Herstart Frigate om de wijzigingen toe te passen." + } + } + }, + "button": { + "overriddenGlobal": "Overschreven (globaal)", + "overriddenGlobalTooltip": "Deze camera heeft voorrang op de algemene configuratie-instellingen in dit gedeelte", + "overriddenBaseConfig": "Overschreven (basis configuratie)", + "overriddenBaseConfigTooltip": "Het profiel {{profile}} heeft voorrang op de configuratie-instellingen in dit gedeelte", + "overriddenGlobalHeading_one": "Deze camera overschrijft {{count}} veld uit de globale configuratie:", + "overriddenGlobalHeading_other": "Deze camera overschrijft {{count}} velden uit de globale configuratie:", + "overriddenGlobalNoDeltas": "Deze camera overschrijft de globale configuratie, maar er zijn geen afwijkende veldwaarden.", + "overriddenBaseConfigHeading_one": "Het profiel {{profile}} overschrijft {{count}} veld uit de basisconfiguratie:", + "overriddenBaseConfigHeading_other": "Het profiel {{profile}} overschrijft {{count}} velden uit de basisconfiguratie:", + "overriddenBaseConfigNoDeltas": "Het profiel {{profile}} overschrijft deze sectie, maar er zijn geen afwijkende veldwaarden ten opzichte van de basisconfiguratie.", + "overriddenInCameras": { + "label_one": "Overschreven in {{count}} camera", + "label_other": "Overschreven in {{count}} camera's", + "tooltip_one": "{{count}} camera overschrijft waarden in deze sectie. Klik om details te bekijken.", + "tooltip_other": "{{count}} camera's overschrijven waarden in deze sectie. Klik om details te bekijken.", + "heading_one": "Deze globale sectie bevat velden die in {{count}} camera worden overschreven.", + "heading_other": "Deze globale sectie bevat velden die in {{count}} camera's worden overschreven.", + "othersField_one": "{{count}} andere", + "othersField_other": "{{count}} andere", + "profilePrefix": "{{profile}}-profiel: {{fields}}" + } + }, + "saveAllPreview": { + "title": "Wijzigingen die worden opgeslagen", + "triggerLabel": "Beoordeling pending changes", + "empty": "Geen openstaande wijzigingen.", + "scope": { + "label": "Bereik", + "global": "Globaal", + "camera": "Camera: {{cameraName}}" + }, + "profile": { + "label": "Profiel" + }, + "field": { + "label": "Veld" + }, + "value": { + "label": "Nieuwe waarde", + "reset": "Resetten" + } + }, + "timestampPosition": { + "tl": "Linksboven", + "tr": "Rechtsboven", + "bl": "Linksonder", + "br": "Rechtsonder" + }, + "detectorsAndModel": { + "title": "Detectoren en model", + "description": "Configureer de detector-backend die objectdetectie uitvoert en het model dat daarbij wordt gebruikt. Wijzigingen worden samen opgeslagen zodat de detector en het model gesynchroniseerd blijven.", + "cardTitles": { + "detector": "Detector-hardware", + "model": "Detectie Model" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Aangepast Model" + }, + "mismatch": { + "warning": "Het huidige Frigate+-model \"{{model}}\" vereist de {{required}}-detector. Kies hieronder een compatibel model of schakel over naar Aangepast model voordat je opslaat." + }, + "plusModel": { + "requiresDetector": "Vereist: {{detector}}", + "noModelSelected": "Selecteren a Frigate+ model" + }, + "toast": { + "saveSuccess": "Detector- en modelinstellingen zijn opgeslagen. Herstart Frigate om de wijzigingen toe te passen.", + "saveError": "Kan detector- en modelinstellingen niet opslaan" + }, + "unsavedChanges": "Niet-opgeslagen wijzigingen aan detector en model", + "restartRequired": "Herstart vereist (detector of model gewijzigd)" + }, + "maintenance": { + "title": "Onderhoud", + "sync": { + "title": "Media synchroniseren", + "desc": "Frigate ruimt media periodiek op volgens je retentieconfiguratie. Het is normaal dat er tijdens het gebruik van Frigate enkele verweesde bestanden ontstaan. Gebruik deze functie om verweesde mediabestanden van de schijf te verwijderen die niet langer in de database worden gebruikt.", + "started": "De mediasynchronisatie is gestart.", + "alreadyRunning": "Er wordt al een synchronisatietaak uitgevoerd", + "error": "Kan synchronisatie niet starten", + "currentStatus": "Status", + "jobId": "Verwerkingsnummer", + "startTime": "Starttijd", + "endTime": "Eindtijd", + "statusLabel": "Status", + "results": "Resultaten", + "errorLabel": "Fout", + "mediaTypes": "Mediatypen", + "allMedia": "Alle media", + "dryRun": "Proefdraaien", + "dryRunEnabled": "Er worden geen bestanden verwijderd", + "dryRunDisabled": "Bestanden worden verwijderd", + "force": "Gedwongen", + "forceDesc": "Negeer de veiligheidsdrempel en voltooi de synchronisatie, zelfs als meer dan 50% van de bestanden zou worden verwijderd.", + "verbose": "Uitgebreid", + "verboseDesc": "Schrijf een volledige lijst van verweesde bestanden naar de schijf ter controle.", + "running": "Synchroniseren bezig...", + "start": "Synchronisatie starten", + "inProgress": "Synchronisatie is bezig. Deze pagina is uitgeschakeld.", + "status": { + "queued": "In de wachtrij", + "running": "Bezig", + "completed": "Voltooid", + "failed": "Mislukt", + "notRunning": "Niet actief" + }, + "resultsFields": { + "filesChecked": "Gecontroleerde bestanden", + "orphansFound": "Wezen gevonden", + "orphansDeleted": "Orphans Verwijderend", + "aborted": "Afgebroken. Het verwijderen zou de veiligheidsdrempel overschrijden.", + "error": "Fout", + "totals": "Totalen" + }, + "event_snapshots": "Snapshots van gevolgde objecten", + "event_thumbnails": "Thumbnails van gevolgde objecten", + "review_thumbnails": "Beoordeling Thumbnails", + "previews": "Vooruitblikken", + "exports": "Exports", + "recordings": "Opnames" + }, + "regionGrid": { + "title": "Regio-raster", + "desc": "Het region grid is een optimalisatie die leert waar objecten van verschillende groottes meestal verschijnen in het gezichtsveld van elke camera. Frigate gebruikt deze gegevens om detectieregio's efficiënt te schalen. Het grid wordt na verloop van tijd automatisch opgebouwd uit gegevens van gevolgde objecten.", + "clear": "Raster van de regio wissen", + "clearConfirmTitle": "Raster van de regio wissen", + "clearConfirmDesc": "Het wissen van het region grid wordt niet aanbevolen, tenzij je onlangs de modelgrootte van je detector hebt gewijzigd of de fysieke positie van je camera hebt aangepast en problemen hebt met objecttracking. Het grid wordt na verloop van tijd automatisch opnieuw opgebouwd terwijl objecten worden gevolgd. Een herstart van Frigate is vereist om de wijzigingen toe te passen.", + "clearSuccess": "Het raster van de regio is succesvol gewist", + "clearError": "Kan region grid niet wissen", + "restartRequired": "Herstart vereist om wijzigingen aan het region grid toe te passen" + } + }, + "configForm": { + "global": { + "title": "Globaal Instellingen", + "description": "Deze instellingen gelden voor alle camera's, tenzij ze worden overschreven in de cameraspecifieke instellingen." + }, + "camera": { + "title": "Camera Instellingen", + "description": "Deze instellingen gelden alleen voor deze camera en overschrijven de globale instellingen.", + "noCameras": "Geen camera's beschikbaar" + }, + "advancedSettingsCount": "Advanced Instellingen ({{count}})", + "advancedCount": "Geavanceerd ({{count}})", + "showAdvanced": "Show Advanced Instellingen", + "tabs": { + "sharedDefaults": "Shared Standaards", + "system": "System", + "integrations": "Integraties" + }, + "additionalProperties": { + "keyLabel": "Sleutel", + "valueLabel": "Waarde", + "keyPlaceholder": "Nieuwe sleutel", + "remove": "Verwijderen" + }, + "knownPlates": { + "namePlaceholder": "bijv. de auto van mijn vrouw", + "platePlaceholder": "Kenteken of reguliere expressie" + }, + "timezone": { + "defaultOption": "Tijdzone van browser gebruiken" + }, + "roleMap": { + "empty": "Geen rolkoppelingen", + "roleLabel": "Role", + "groupsLabel": "Groepen", + "addMapping": "Rolkoppeling toevoegen", + "remove": "Verwijderen" + }, + "ffmpegArgs": { + "preset": "Voorinstelling", + "manual": "Handmatige argumenten", + "inherit": "Overnemen van camera-instelling", + "none": "Geen", + "useGlobalSetting": "Overnemen uit algemene instelling", + "selectPreset": "Selecteren preset", + "manualPlaceholder": "Voer FFmpeg-argumenten in", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (Generiek)", + "preset-http-mjpeg-generic": "HTTP MJPEG (Generiek)", + "preset-http-reolink": "HTTP - Reolink Camera's", + "preset-rtmp-generic": "RTMP (Generiek)", + "preset-rtsp-generic": "RTSP (Generiek)", + "preset-rtsp-restream": "RTSP - her-stream van go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - her-stream van go2rtc (Lage latentie)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Opnemen (generiek, geen audio)", + "preset-record-generic-audio-copy": "Opnemen (Generiek + Audio kopiëren)", + "preset-record-generic-audio-aac": "Opnemen (generiek + audio naar AAC)", + "preset-record-mjpeg": "Record - MJPEG Camera's", + "preset-record-jpeg": "Record - JPEG Camera's", + "preset-record-ubiquiti": "Record - Ubiquiti Camera's" + } + }, + "cameraInputs": { + "itemTitle": "Stream {{index}}" + }, + "restartRequiredField": "Herstart vereist", + "restartRequiredFooter": "Configuratie changed - Restart required", + "sections": { + "detect": "Detectie", + "record": "Opname", + "snapshots": "Snapshots", + "motion": "Beweging", + "objects": "Objecten", + "review": "Beoordeling", + "audio": "Audio", + "notifications": "Meldingen", + "live": "Live weergaven", + "timestamp_style": "Tijdstempels", + "mqtt": "MQTT", + "database": "Database", + "telemetry": "Telemetrie", + "auth": "Authenticatie", + "tls": "TLS", + "proxy": "Proxy", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg", + "detectors": "Detectoren", + "model": "Model", + "semantic_search": "Semantic Zoeken", + "genai": "GenAI", + "face_recognition": "Gezichtsherkenning", + "lpr": "Kentekenherkenning", + "birdseye": "Birdseye", + "masksAndZones": "Maskers / Zones" + }, + "detect": { + "title": "Detectie Instellingen" + }, + "detectors": { + "title": "Detector Instellingen", + "singleType": "Er is slechts één {{type}}-detector toegestaan.", + "keyRequired": "Detectornaam is vereist.", + "keyDuplicate": "De naam van de detector bestaat al.", + "noSchema": "Geen detectorschema's beschikbaar.", + "none": "Geen detectorinstanties geconfigureerd.", + "add": "Detector toevoegen", + "addCustomKey": "Aangepaste sleutel toevoegen" + }, + "record": { + "title": "Opname Instellingen" + }, + "snapshots": { + "title": "Snapshot Instellingen" + }, + "motion": { + "title": "Beweging Instellingen" + }, + "objects": { + "title": "Object Instellingen" + }, + "audioLabels": { + "summary": "{{count}} audiolabels geselecteerd", + "empty": "Geen audiolabels beschikbaar" + }, + "objectLabels": { + "summary": "{{count}} objecttypen geselecteerd", + "empty": "Geen objectlabels beschikbaar" + }, + "reviewLabels": { + "summary": "{{count}} labels geselecteerd", + "empty": "Geen labels beschikbaar" + }, + "filters": { + "objectFieldLabel": "{{field}} voor {{label}}" + }, + "zoneNames": { + "summary": "{{count}} geselecteerd", + "empty": "Geen zones beschikbaar" + }, + "inputRoles": { + "summary": "{{count}} rollen geselecteerd", + "empty": "Geen rollen beschikbaar", + "options": { + "detect": "Detecteren", + "record": "Opnemen", + "audio": "Audio" + } + }, + "genaiRoles": { + "options": { + "embeddings": "Embedding", + "descriptions": "Beschrijvingen", + "chat": "Chat" + } + }, + "semanticSearchModel": { + "placeholder": "Selecteren model…", + "builtIn": "Ingebouwde modellen", + "genaiProviders": "Aanbieders van generatieve AI" + }, + "review": { + "title": "Beoordeling Instellingen" + }, + "audio": { + "title": "Audio Instellingen" + }, + "notifications": { + "title": "Melding Instellingen" + }, + "live": { + "title": "Live View Instellingen" + }, + "timestamp_style": { + "title": "Timestamp Instellingen" + }, + "searchPlaceholder": "Zoeken...", + "addCustomLabel": "Aangepast label toevoegen...", + "genaiModel": { + "placeholder": "Selecteren model…", + "search": "Zoeken models…", + "noModels": "Geen modellen beschikbaar" + } + }, + "globalConfig": { + "title": "Globaal Configuratie", + "description": "Configureer globale instellingen die op alle camera's van toepassing zijn, tenzij ze worden overschreven.", + "toast": { + "success": "Globaal settings saved successfully", + "error": "Kan globale instellingen niet opslaan", + "validationError": "Validatie is mislukt" + } + }, + "cameraConfig": { + "title": "Camera Configuratie", + "description": "Configure settings for individual cameras. Instellingen override global defaults.", + "overriddenBadge": "Overschreven", + "resetToGlobal": "Resetten to Globaal", + "toast": { + "success": "Camera-instellingen zijn succesvol opgeslagen", + "error": "Kan camera-instellingen niet opslaan" + } + }, + "toast": { + "success": "Instellingen saved successfully", + "applied": "Instellingen applied successfully", + "successRestartRequired": "Instellingen saved successfully. Restart Frigate to apply your changes.", + "error": "Kan instellingen niet opslaan", + "validationError": "Validatie mislukt: {{message}}", + "resetSuccess": "Resetten to global defaults", + "resetError": "Kan instellingen niet resetten", + "saveAllSuccess_one": "Opslaand {{count}} section successfully.", + "saveAllSuccess_other": "Alle {{count}} secties zijn succesvol opgeslagen.", + "saveAllPartial_one": "{{successCount}} van {{totalCount}} sectie opgeslagen. {{failCount}} mislukt.", + "saveAllPartial_other": "{{successCount}} van {{totalCount}} secties opgeslagen. {{failCount}} mislukt.", + "saveAllFailure": "Kan niet alle secties opslaan." + }, + "profiles": { + "title": "Profielen", + "activeProfile": "Active Profiel", + "noActiveProfile": "Geen actief profiel", + "active": "Active", + "activated": "Profiel '{{profile}}' activated", + "activateFailed": "Kan profiel niet instellen", + "deactivated": "Profiel deactivated", + "noProfiles": "Geen profielen gedefinieerd.", + "noOverrides": "Geen overschrijvingen", + "cameraCount_one": "{{count}} camera", + "cameraCount_other": "{{count}} cameras", + "columnCamera": "Camera", + "columnOverrides": "Profiel Overrides", + "baseConfig": "Basisconfiguratie", + "addProfile": "Toevoegen Profiel", + "newProfile": "New Profiel", + "profileNamePlaceholder": "bijv. Ingeschakeld, Afwezig, Nachtmodus", + "friendlyNameLabel": "Profiel Name", + "profileIdLabel": "Profiel ID", + "profileIdDescription": "Interne identificatie die wordt gebruikt in configuratie en automatiseringen", + "nameInvalid": "Alleen kleine letters, cijfers en onderstrepingstekens zijn toegestaan", + "nameDuplicate": "Er bestaat al een profiel met deze naam", + "error": { + "mustBeAtLeastTwoCharacters": "Moet minimaal 2 tekens bevatten", + "mustNotContainPeriod": "Mag geen punten bevatten", + "alreadyExists": "Er bestaat al een profiel met deze ID" + }, + "renameProfile": "Rename Profiel", + "renameSuccess": "Profiel renamed to '{{profile}}'", + "deleteProfile": "Verwijderen Profiel", + "deleteProfileConfirm": "Profiel \"{{profile}}\" van alle camera's verwijderen? Dit kan niet ongedaan worden gemaakt.", + "deleteSuccess": "Profiel '{{profile}}' deleted", + "createSuccess": "Profiel '{{profile}}' created", + "removeOverride": "Verwijderen Profiel Override", + "deleteSection": "Verwijderen Section Overrides", + "deleteSectionConfirm": "De {{section}}-overschrijvingen voor profiel {{profile}} op {{camera}} verwijderen?", + "deleteSectionSuccess": "{{section}}-overschrijvingen voor {{profile}} verwijderd", + "enableSwitch": "Enable Profielen", + "enabledDescription": "Profielen zijn ingeschakeld. Maak hieronder een nieuw profiel aan, ga naar een cameraconfiguratiesectie om je wijzigingen aan te brengen en sla op om de wijzigingen toe te passen.", + "disabledDescription": "Met profielen kun je benoemde sets van cameraconfiguratie-overschrijvingen definiëren (bijv. ingeschakeld, afwezig, nacht) die op verzoek kunnen worden geactiveerd." + }, + "unsavedChanges": "Er zijn wijzigingen die nog niet zijn opgeslagen", + "confirmReset": "Confirm Resetten", + "resetToDefaultDescription": "Dit zet alle instellingen in deze sectie terug naar hun standaardwaarden. Deze actie kan niet ongedaan worden gemaakt.", + "resetToGlobalDescription": "Dit zet de instellingen in deze sectie terug naar de globale standaardwaarden. Deze actie kan niet ongedaan worden gemaakt.", + "go2rtcStreams": { + "title": "go2rtc Streams", + "description": "Beheer go2rtc-streamconfiguraties voor het restreamen van camera's. Elke stream heeft een naam en één of meer bron-URL's.", + "addStream": "Stream toevoegen", + "addStreamDesc": "Voer een naam in voor de nieuwe stream. Deze naam wordt gebruikt om naar de stream te verwijzen in je cameraconfiguratie.", + "addUrl": "URL toevoegen", + "streamName": "Stream naam", + "streamNamePlaceholder": "bijv. voor_deur", + "streamUrlPlaceholder": "bijv, rtsp://user:pass@192.168.1.100/stream", + "deleteStream": "Verwijderen stream", + "deleteStreamConfirm": "Weet je zeker dat je de stream \"{{streamName}}\" wilt verwijderen? Camera's die naar deze stream verwijzen, werken mogelijk niet meer.", + "noStreams": "Geen go2rtc-streams geconfigureerd. Voeg een stream toe om te beginnen.", + "validation": { + "nameRequired": "Streamnaam is vereist", + "nameDuplicate": "Er bestaat al een stream met deze naam", + "nameInvalid": "Streamnaam mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten", + "urlRequired": "Er is minimaal één URL vereist" + }, + "renameStream": "Stream hernoemen", + "renameStreamDesc": "Voer een nieuwe naam in voor deze stream. Het hernoemen van een stream kan camera's of andere streams die er op naam naar verwijzen verstoren.", + "newStreamName": "Nieuwe stream naam", + "ffmpeg": { + "useFfmpegModule": "Compatibiliteitsmodus gebruiken (ffmpeg)", + "video": "Video", + "audio": "Audio", + "hardware": "Hardware-versnelling", + "videoCopy": "Kopiëren", + "videoH264": "Transcoderen naar H.264", + "videoH265": "Transcoderen naar H.265", + "videoExclude": "Uitsluiten", + "audioCopy": "Kopiëren", + "audioAac": "Transcoderen naar AAC", + "audioOpus": "Transcoderen naar Opus", + "audioPcmu": "Transcoderen naar PCM μ-law", + "audioPcma": "Transcoderen naar PCM A-law", + "audioPcm": "Transcoderen naar PCM", + "audioMp3": "Transcoderen naar MP3", + "audioExclude": "Uitsluiten", + "hardwareNone": "Geen hardwareversnelling", + "hardwareAuto": "Automatische hardware-versnelling" + } + }, + "birdseye": { + "trackingMode": { + "objects": "Objecten", + "motion": "Beweging", + "continuous": "Doorlopend" + } + }, + "retainMode": { + "all": "Alle", + "motion": "Beweging", + "active_objects": "Active Objecten" + }, + "previewQuality": { + "very_high": "Zeer hoog", + "high": "High", + "medium": "Medium", + "low": "Low", + "very_low": "Zeer laag" + }, + "ui": { + "timeFormat": { + "browser": "Browser", + "12hour": "12 uur", + "24hour": "24 uur" + }, + "TimeOrDateStyle": { + "full": "Full", + "long": "Lang", + "medium": "Medium", + "short": "Kort" + }, + "unitSystem": { + "metric": "Metrisch", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Opnames", + "previews": "Voorbeelden" + } + }, + "logger": { + "logLevel": { + "debug": "Foutopsporing", + "info": "Info", + "warning": "Waarschuwing", + "error": "Fout", + "critical": "Kritisch" + } + }, + "onvif": { + "profileAuto": "Auto", + "profileLoading": "Profielen laden...", + "autotracking": { + "zooming": { + "disabled": "Uitgeschakeld", + "absolute": "Absoluut", + "relative": "Relatief" + } + } + }, + "modelSize": { + "small": "Klein", + "large": "Large" + }, + "configMessages": { + "review": { + "recordDisabled": "Opname is disabled, review items will not be generated.", + "detectDisabled": "Object detection is disabled. Beoordeling items require detected objects to categorize alerts and detections.", + "allNonAlertDetections": "Alle activiteit die geen melding is, wordt opgenomen als detecties.", + "genaiImageSourceRecordingsRecordDisabled": "De afbeeldingsbron is ingesteld op 'recordings', maar opnemen is uitgeschakeld. Frigate valt terug op voorbeeldafbeeldingen." + }, + "audio": { + "noAudioRole": "Er zijn geen streams met de audiorol gedefinieerd. Je moet de audiorol inschakelen om audiodetectie te laten werken." + }, + "audioTranscription": { + "audioDetectionDisabled": "Audiodetectie is niet ingeschakeld voor deze camera. Audiotranscriptie vereist dat audiodetectie actief is." + }, + "detect": { + "fpsGreaterThanFive": "Het instellen van de detectie-FPS hoger dan 5 wordt niet aanbevolen. Hogere waarden kunnen prestatieproblemen veroorzaken en leveren geen voordeel op.", + "disabled": "Objectdetectie is uitgeschakeld. Snapshots, beoordelingsitems en verrijkingen zoals gezichtsherkenning, kentekenherkenning en generatieve AI werken dan niet." + }, + "objects": { + "genaiNoDescriptionsProvider": "Je moet een GenAI-provider configureren met de rol 'descriptions' om beschrijvingen te kunnen genereren." + }, + "faceRecognition": { + "globalDisabled": "De verrijking voor gezichtsherkenning moet zijn ingeschakeld om gezichtsherkenningsfuncties op deze camera te laten werken.", + "personNotTracked": "Gezichtsherkenning vereist dat het object 'person' wordt gevolgd. Schakel 'person' in bij Objecten voor deze camera.", + "modelSizeLarge": "Het 'large'-model vereist een GPU of NPU voor redelijke prestaties. Gebruik 'small' op systemen met alleen een CPU." + }, + "lpr": { + "globalDisabled": "De verrijking voor kentekenherkenning moet zijn ingeschakeld om LPR-functies op deze camera te laten werken.", + "vehicleNotTracked": "Kentekenherkenning vereist dat 'car' of 'motorcycle' wordt gevolgd. Schakel 'car' of 'motorcycle' in bij Objecten voor deze camera.", + "modelSizeLarge": "Het 'large'-model is geoptimaliseerd voor kentekenplaten met meerdere regels. Het 'small'-model presteert beter dan 'large' en moet worden gebruikt tenzij jouw regio kentekenformaten met meerdere regels gebruikt." + }, + "record": { + "noRecordRole": "Er zijn geen streams met de opnamerol gedefinieerd. Opnemen werkt dan niet." + }, + "birdseye": { + "objectsModeDetectDisabled": "Birdseye staat ingesteld op de modus 'objects', maar objectdetectie is uitgeschakeld voor deze camera. De camera wordt niet weergegeven in Birdseye." + }, + "snapshots": { + "detectDisabled": "Objectdetectie is uitgeschakeld. Snapshots worden gegenereerd uit gevolgde objecten en worden daarom niet aangemaakt." + }, + "detectors": { + "mixedTypes": "Alle detectoren moeten hetzelfde type gebruiken. Verwijder bestaande detectoren om een ander type te gebruiken.", + "mixedTypesSuggestion": "Alle detectoren moeten hetzelfde type gebruiken. Verwijder bestaande detectoren of selecteer {{type}}." + }, + "semanticSearch": { + "jinav2SmallModelSize": "De 'small'-grootte met het Jina V2-model heeft hoge RAM- en inferentiekosten. Het 'large'-model met een aparte GPU wordt aanbevolen." + } + } +} diff --git a/web/public/locales/nl/views/system.json b/web/public/locales/nl/views/system.json new file mode 100644 index 0000000..d31cd6e --- /dev/null +++ b/web/public/locales/nl/views/system.json @@ -0,0 +1,234 @@ +{ + "documentTitle": { + "general": "Algemene Statistieken - Frigate", + "logs": { + "nginx": "Nginx Logboeken - Frigate", + "go2rtc": "Go2RTC Logboeken - Frigate", + "frigate": "Frigate Logboek - Frigate", + "websocket": "Berichten Logboeken - Frigate" + }, + "storage": "Opslag Statistieken - Frigate", + "cameras": "Camera Statistieken - Frigate", + "enrichments": "Verrijkings Statistieken - Frigate" + }, + "title": "Systeem", + "metrics": "Systeemstatistieken", + "logs": { + "download": { + "label": "Logs Downloaden" + }, + "copy": { + "label": "Kopieeren naar klembord", + "success": "Logs zijn gekopieerd naar klembord", + "error": "Logs kopieeren naar klembord mislukt" + }, + "type": { + "timestamp": "Tijdsvermelding", + "message": "Bericht", + "tag": "Tag", + "label": "Type" + }, + "toast": { + "error": { + "whileStreamingLogs": "Fout bij streamen van logs: {{errorMessage}}", + "fetchingLogsFailed": "Fout bij ophalen van logs: {{errorMessage}}" + } + }, + "tips": "Logs worden gestreamd vanaf de server", + "websocket": { + "label": "Berichten", + "pause": "Pauze", + "resume": "Hervatten", + "clear": "Leegmaken", + "filter": { + "all": "Alle onderwerpen", + "topics": "Onderwerpen", + "events": "Gebeurtenissen", + "reviews": "Beoordelingen", + "classification": "Classificatie", + "face_recognition": "Gezichtsherkenning", + "lpr": "Kentekenplaatherkenning", + "camera_activity": "Camera activiteit", + "system": "Systeem", + "camera": "Camera", + "all_cameras": "Alle camera's", + "cameras_count_one": "{{count}} Camera", + "cameras_count_other": "{{count}} Cameras" + }, + "empty": "Nog geen berichten ontvangen", + "count_one": "{{count}} berichten" + } + }, + "general": { + "detector": { + "title": "Detectoren", + "cpuUsage": "Detector CPU-verbruik", + "memoryUsage": "Detector Geheugen Gebruik", + "inferenceSpeed": "Detector Interferentie Snelheid", + "temperature": "Detectortemperatuur", + "cpuUsageInformation": "CPU-gebruik bij het voorbereiden van in en uitvoer van gegevens voor detectiemodellen. Deze waarde geeft geen inferentie gebruik weer, ook niet wanneer een GPU of accelerator wordt gebruikt." + }, + "hardwareInfo": { + "title": "Systeemgegevens", + "gpuUsage": "GPU-verbruik", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo Resultaat", + "returnCode": "Terugvoer Code: {{code}}", + "processError": "Process Fout:", + "processOutput": "Process Resultaat:" + }, + "nvidiaSMIOutput": { + "name": "Naam: {{name}}", + "vbios": "VBios Informatie: {{vbios}}", + "cudaComputerCapability": "CUDA Verwerking Capaciteit: {{cuda_compute}}", + "driver": "Stuurprogramma: {{driver}}", + "title": "Nvidia SMI Uitvoer" + }, + "closeInfo": { + "label": "Sluit GPU info" + }, + "copyInfo": { + "label": "Kopieer GPU Info" + }, + "toast": { + "success": "GPU info gekopieerd naar klembord" + } + }, + "gpuDecoder": "GPU Decodeerder", + "gpuEncoder": "GPU Encodeerder", + "gpuMemory": "GPU-geheugen", + "npuUsage": "NPU-gebruik", + "npuMemory": "NPU-geheugen", + "intelGpuWarning": { + "title": "Waarschuwing Intel GPU-statistieken", + "message": "GPU-statistieken niet beschikbaar", + "description": "Dit is een bekend probleem in de GPU-statistiekentools van Intel (intel_gpu_top). Deze raken defect en geven herhaaldelijk een GPU-gebruik van 0% weer, zelfs wanneer hardware-acceleratie en objectdetectie correct draaien op de (i)GPU. Dit is geen bug in Frigate. Je kunt de host opnieuw opstarten om het tijdelijk op te lossen en te controleren dat de GPU goed werkt. Dit heeft geen invloed op de prestaties." + }, + "gpuTemperature": "GPU Temperatuur" + }, + "otherProcesses": { + "processMemoryUsage": "Process Geheugen Gebruik", + "processCpuUsage": "Process CPU-verbruik", + "title": "Verdere Processen", + "series": { + "go2rtc": "go2rtc", + "recording": "opname", + "review_segment": "beoordelingssegment", + "embeddings": "inbeddingen", + "audio_detector": "Geluidsdetector" + } + }, + "title": "Algemeen" + }, + "storage": { + "overview": "Overzicht", + "recordings": { + "title": "Opnames", + "earliestRecording": "Oudste beschikbare opname:", + "tips": "Deze waarde laat het totale opslag ruimte gebruik voor opnames zien in de database van Frigate. Frigate houdt geen opslag gebruiks gegevens bij van alle bestanden op uw schijf." + }, + "cameraStorage": { + "title": "Camera Opslag", + "unusedStorageInformation": "Ongebruikte opslagruimte informatie", + "storageUsed": "Opslag", + "percentageOfTotalUsed": "Percentage van Totaal", + "unused": { + "title": "Ongebruikt", + "tips": "Deze waarde kan de beschikbare opslag ruimte voor Frigate niet goed weergeven indien er ook andere bestanden op de schijf staan. Frigate houdt geen opslag gegevens bij van bestanden buiten haar eigen opnames." + }, + "camera": "Camera", + "bandwidth": "Bandbreedte" + }, + "title": "Opslag", + "shm": { + "title": "SHM (gedeeld geheugen) toewijzing", + "warning": "De huidige SHM-grootte van {{total}} MB is te klein. Vergroot deze tot minimaal {{min_shm}} MB.", + "readTheDocumentation": "Lees de documentatie" + } + }, + "cameras": { + "title": "Cameras", + "overview": "Overzicht", + "info": { + "cameraProbeInfo": "{{camera}} Informatie opgehaald uit de camerastream", + "streamDataFromFFPROBE": "Streamgegevens zijn ontvangen via ffprobe.", + "stream": "Stream {{idx}}", + "resolution": "Resolutie:", + "unknown": "Onbekend", + "error": "Fout: {{error}}", + "tips": { + "title": "Informatie ophalen uit de camerastream" + }, + "fps": "FPS:", + "codec": "Codec:", + "video": "Video:", + "fetching": "Camera Gegevens Opvragen", + "audio": "Audio:", + "aspectRatio": "beeldverhouding" + }, + "framesAndDetections": "Frames / Detecties", + "label": { + "camera": "camera", + "detect": "detectie", + "skipped": "overgeslagen", + "ffmpeg": "FFmpeg", + "capture": "registratie", + "overallSkippedDetectionsPerSecond": "totaal aantal overgeslagen detecties per seconde", + "overallFramesPerSecond": "totale aantal frames per seconde", + "overallDetectionsPerSecond": "totale aantal detecties per seconde", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraFramesPerSecond": "{{camName}} frames per seconde", + "cameraDetectionsPerSecond": "{{camName}} detecties per seconde", + "cameraSkippedDetectionsPerSecond": "{{camName}} overgeslagen detecties per seconde", + "cameraCapture": "{{camName}} opname", + "cameraDetect": "{{camName}} detecteren" + }, + "toast": { + "success": { + "copyToClipboard": "Uitvraag gegevens naar klembord gekopieerd." + }, + "error": { + "unableToProbeCamera": "Kan camera niet uitvragen: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Voor het laatst vernieuwd: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} zorgt voor hoge FFmpeg CPU belasting ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} zorgt voor hoge detectie CPU belasting ({{detectAvg}}%)", + "healthy": "Geen problemen", + "reindexingEmbeddings": "Herindexering van inbeddingen ({{processed}}% compleet)", + "detectIsSlow": "{{detect}} is traag ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} is erg traag ({{speed}} ms)", + "cameraIsOffline": "{{camera}} is offline", + "shmTooLow": "Vergroot de /dev/shm toewijzing van {{total}} MB naar minimaal {{min}} MB." + }, + "enrichments": { + "title": "Verrijkingen", + "infPerSecond": "Interferenties Per Seconde", + "embeddings": { + "image_embedding_speed": "Afbeelding Inplaatsings Snelheid", + "face_embedding_speed": "Gezicht Inplaatsings Snelheid", + "text_embedding_speed": "Text Inplaatsing Snelheid", + "plate_recognition_speed": "Kentekenplaat Herkenning Snelheid", + "face_recognition_speed": "Snelheid van gezichtsherkenning", + "image_embedding": "Afbeelding Inbedden", + "text_embedding": "Tekstinbedden", + "face_recognition": "Gezichtsherkenning", + "yolov9_plate_detection_speed": "YOLOv9 Kentekenplaat Detectiesnelheid", + "yolov9_plate_detection": "YOLOv9 Kentekenplaatdetectie", + "plate_recognition": "Kentekenherkenning", + "review_description": "Beoordelingsbeschrijving", + "review_description_speed": "Snelheid beoordelingsbeschrijving", + "review_description_events_per_second": "Beoordelingsbeschrijving", + "object_description": "Objectbeschrijving", + "object_description_speed": "Objectbeschrijvingssnelheid", + "object_description_events_per_second": "Objectbeschrijving", + "classification": "{{name}} Classificatie", + "classification_speed": "{{name}} Classificatiesnelheid", + "classification_events_per_second": "{{name}} Classificatie gebeurtenissen per seconde" + }, + "averageInf": "Gemiddelde inferentietijd" + } +} diff --git a/web/public/locales/pl/audio.json b/web/public/locales/pl/audio.json new file mode 100644 index 0000000..9a58b98 --- /dev/null +++ b/web/public/locales/pl/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Mowa", + "babbling": "Gaworzenie", + "yell": "Krzyk", + "bellow": "Poniżej", + "whoop": "Okrzyk", + "whispering": "Szept", + "laughter": "Śmiech", + "snicker": "Chichot", + "crying": "Płacz", + "sigh": "Westchnienie", + "singing": "Śpiewanie", + "choir": "Chór", + "yodeling": "Jodłowanie", + "chant": "Skandowanie", + "mantra": "Mantra", + "child_singing": "Śpiew dziecka", + "synthetic_singing": "Śpiew syntetyczny", + "rapping": "Rapowanie", + "humming": "Nucenie", + "groan": "Jęk", + "grunt": "Chrząknięcie", + "whistling": "Gwizdanie", + "breathing": "Oddychanie", + "wheeze": "Świszczący oddech", + "snoring": "Chrapanie", + "gasp": "Sapnięcie", + "pant": "Dyszenie", + "snort": "Prychnięcie", + "cough": "Kaszel", + "throat_clearing": "Odchrząkiwanie", + "sneeze": "Kichnięcie", + "sniff": "Pociągnięcie nosem", + "run": "Bieg", + "shuffle": "Szuranie", + "footsteps": "Kroki", + "chewing": "Żucie", + "biting": "Gryzienie", + "gargling": "Płukanie gardła", + "stomach_rumble": "Burczenie w brzuchu", + "burping": "Bekanie", + "hiccup": "Czkawka", + "pets": "Zwierzęta domowe", + "finger_snapping": "Pstrykanie palcami", + "heartbeat": "Bicie serca", + "dog": "Pies", + "heart_murmur": "Szmer serca", + "chatter": "Gwar", + "children_playing": "Bawiące się dzieci", + "animal": "Zwierzę", + "applause": "Oklaski", + "cheering": "Wiwatowanie", + "bark": "Szczekanie", + "fart": "Pierdnięcie", + "hands": "Dłonie", + "clapping": "Klaskanie", + "crowd": "Tłum", + "hiss": "Syczenie", + "purr": "Mruczenie", + "yip": "Poszczekiwanie", + "howl": "Wycie", + "growling": "Warczenie", + "meow": "Miauczenie", + "bow_wow": "Hau hau", + "whimper_dog": "Skomlenie psa", + "cat": "Kot", + "caterwaul": "Koci wrzask", + "livestock": "Zwierzęta hodowlane", + "horse": "Koń", + "clip_clop": "Stukot kopyt", + "neigh": "Rżenie", + "cattle": "Bydło", + "moo": "Muczenie", + "cowbell": "Krowi dzwonek", + "goat": "Koza", + "bleat": "Beczenie", + "pig": "Świnia", + "oink": "Chrumkanie", + "sheep": "Owca", + "fowl": "Drób", + "chicken": "Kura", + "cluck": "Gdakanie", + "cock_a_doodle_doo": "Kukuryku", + "turkey": "Indyk", + "gobble": "Gulgotanie", + "duck": "Kaczka", + "quack": "Kwakanie", + "goose": "Gęś", + "honk": "Gęganie", + "wild_animals": "Dzikie zwierzęta", + "roaring_cats": "Ryczące koty", + "roar": "Ryk", + "bird": "Ptak", + "chirp": "Ćwierkanie", + "squawk": "Skrzeczenie", + "pigeon": "Gołąb", + "coo": "Gruchanie", + "crow": "Wrona", + "caw": "Krakanie", + "owl": "Sowa", + "hoot": "Pohukiwanie", + "flapping_wings": "Trzepot skrzydeł", + "insect": "Owad", + "cricket": "Świerszcz", + "mosquito": "Komar", + "fly": "Mucha", + "buzz": "Bzyczenie", + "frog": "Żaba", + "croak": "Kumkanie", + "snake": "Wąż", + "rattle": "Grzechotanie", + "whale_vocalization": "Wokalizacja wieloryba", + "music": "Muzyka", + "plucked_string_instrument": "Instrument strunowy szarpany", + "guitar": "Gitara", + "electric_guitar": "Gitara elektryczna", + "bass_guitar": "Gitara basowa", + "acoustic_guitar": "Gitara akustyczna", + "steel_guitar": "Gitara stalowa", + "tapping": "Tapowanie", + "strum": "Szarpnięcie strun", + "sitar": "Sitar", + "mandolin": "Mandolina", + "zither": "Cytra", + "ukulele": "Ukulele", + "keyboard": "Klawiatura", + "rimshot": "Uderzenie w obręcz", + "drum_roll": "Werbel (tremolo)", + "bass_drum": "Bęben basowy", + "timpani": "Kotły", + "tabla": "Tabla", + "cymbal": "Talerz", + "hi_hat": "Hi-hat", + "wood_block": "Pudełko drewniane", + "tambourine": "Tamburyn", + "maraca": "Marakasy", + "gong": "Gong", + "brass_instrument": "Instrument dęty blaszany", + "french_horn": "Waltornia", + "tubular_bells": "Dzwony rurowe", + "mallet_percussion": "Instrumenty perkusyjne pałkowe", + "marimba": "Marimba", + "glockenspiel": "Dzwonki", + "vibraphone": "Wibrafon", + "steelpan": "Instrument z beczek", + "orchestra": "Orkiestra", + "trumpet": "Trąbka", + "trombone": "Puzon", + "bowed_string_instrument": "Instrument strunowy smyczkowy", + "string_section": "Sekcja smyczkowa", + "violin": "Skrzypce", + "pizzicato": "Pizzicato", + "double_bass": "Kontrabas", + "wind_instrument": "Instrument dęty", + "saxophone": "Saksofon", + "clarinet": "Klarnet", + "harp": "Harfa", + "bell": "Dzwonek", + "church_bell": "Dzwon kościelny", + "jingle_bell": "Dzwoneczek", + "bicycle_bell": "Dzwonek rowerowy", + "tuning_fork": "Kamerton", + "chime": "Dzwonki", + "wind_chime": "Dzwonki wietrzne", + "harmonica": "Harmonijka", + "accordion": "Akordeon", + "bagpipes": "Dudy", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Misa dźwiękowa", + "scratching": "Drapanie", + "pop_music": "Muzyka pop", + "hip_hop_music": "Muzyka hip-hopowa", + "beatboxing": "Beatbox", + "rock_music": "Muzyka rockowa", + "heavy_metal": "Heavy metal", + "punk_rock": "Punk rock", + "grunge": "Paskudztwo", + "progressive_rock": "Rock progresywny", + "rock_and_roll": "Rock and roll", + "psychedelic_rock": "Rock psychodeliczny", + "rhythm_and_blues": "Rhythm and blues", + "soul_music": "Muzyka soul", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Muzyka swingowa", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Muzyka folkowa", + "middle_eastern_music": "Muzyka bliskowschodnia", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Muzyka klasyczna", + "opera": "Opera", + "electronic_music": "Muzyka elektroniczna", + "house_music": "Muzyka house", + "electronica": "Electronica", + "electronic_dance_music": "Muzyka elektroniczna taneczna", + "ambient_music": "Muzyka ambient", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and bass", + "trance_music": "Muzyka trance", + "music_of_latin_america": "Muzyka Latynowska", + "salsa_music": "Muzyka salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Muzyka dla dzieci", + "new-age_music": "Muzyka new age", + "vocal_music": "Muzyka wokalna", + "a_capella": "A cappella", + "music_of_africa": "Muzyka afrykańska", + "afrobeat": "Afrobeat", + "christian_music": "Muzyka chrześcijańska", + "gospel_music": "Muzyka gospel", + "music_of_asia": "Muzyka azjatycka", + "carnatic_music": "Muzyka karnatycka", + "music_of_bollywood": "Muzyka bollywood", + "ska": "Ska", + "independent_music": "Muzyka niezależna", + "song": "Piosenka", + "background_music": "Muzyka tła", + "theme_music": "Muzyka tematyczna", + "jingle": "Dżingiel", + "soundtrack_music": "Muzyka filmowa", + "lullaby": "Kołysanka", + "video_game_music": "Muzyka z gier wideo", + "christmas_music": "Muzyka świąteczna", + "dance_music": "Muzyka taneczna", + "wedding_music": "Muzyka weselna", + "happy_music": "Muzyka radosna", + "sad_music": "Muzyka smutna", + "tender_music": "Muzyka łagodna", + "exciting_music": "Muzyka ekscytująca", + "angry_music": "Muzyka gniewna", + "scary_music": "Muzyka straszna", + "wind": "Wiatr", + "rustling_leaves": "Szeleszczące liście", + "wind_noise": "Szum wiatru", + "thunderstorm": "Burza", + "thunder": "Grzmot", + "water": "Woda", + "rain": "Deszcz", + "raindrop": "Kropla deszczu", + "rain_on_surface": "Deszcz na powierzchni", + "stream": "Strumień", + "waterfall": "Wodospad", + "ocean": "Ocean", + "fire": "Ogień", + "boat": "Łódź", + "sailboat": "Żaglówka", + "motorboat": "Motorówka", + "ship": "Statek", + "motor_vehicle": "Pojazd silnikowy", + "car": "Samochód", + "toot": "Klakson", + "car_alarm": "Alarm samochodowy", + "power_windows": "Elektryczne szyby", + "skidding": "Poślizg", + "tire_squeal": "Pisk opon", + "ambulance": "Karetka", + "fire_engine": "Wóz strażacki", + "motorcycle": "Motocykl", + "traffic_noise": "Hałas uliczny", + "rail_transport": "Transport kolejowy", + "train": "Pociąg", + "train_whistle": "Gwizd pociągu", + "train_horn": "Sygnał dźwiękowy pociągu", + "railroad_car": "Wagon kolejowy", + "train_wheels_squealing": "Pisk kół pociągu", + "subway": "Metro", + "aircraft": "Statek powietrzny", + "aircraft_engine": "Silnik samolotu", + "jet_engine": "Silnik odrzutowy", + "propeller": "Śmigło", + "helicopter": "Helikopter", + "fixed-wing_aircraft": "Samolot", + "bicycle": "Rower", + "skateboard": "Deskorolka", + "engine": "Silnik", + "light_engine": "Lekki silnik", + "dental_drill's_drill": "Wiertło dentystyczne", + "lawn_mower": "Kosiarka do trawy", + "chainsaw": "Piła łańcuchowa", + "medium_engine": "Średni silnik", + "heavy_engine": "Ciężki silnik", + "engine_knocking": "Stukanie silnika", + "engine_starting": "Uruchamianie silnika", + "idling": "Praca na biegu jałowym", + "accelerating": "Przyspieszanie", + "door": "Drzwi", + "doorbell": "Dzwonek do drzwi", + "ding-dong": "Ding-dong", + "sliding_door": "Drzwi przesuwne", + "slam": "Trzaśnięcie", + "knock": "Pukanie", + "tap": "Stukanie", + "frying": "Smażenie", + "microwave_oven": "Kuchenka mikrofalowa", + "blender": "Blender", + "water_tap": "Kran", + "sink": "Zlew", + "bathtub": "Wanna", + "hair_dryer": "Suszarka do włosów", + "toilet_flush": "Spłuczka toalety", + "toothbrush": "Szczoteczka do zębów", + "electric_toothbrush": "Elektryczna szczoteczka do zębów", + "vacuum_cleaner": "Odkurzacz", + "zipper": "Zamek błyskawiczny", + "keys_jangling": "Brzęk kluczy", + "coin": "Moneta", + "shuffling_cards": "Tasowanie kart", + "typing": "Pisanie na klawiaturze", + "typewriter": "Maszyna do pisania", + "computer_keyboard": "Klawiatura komputerowa", + "writing": "Pisanie", + "alarm": "Alarm", + "telephone": "Telefon", + "telephone_bell_ringing": "Dzwonek telefonu", + "ringtone": "Dzwonek", + "telephone_dialing": "Wybieranie numeru", + "dial_tone": "Sygnał wybierania", + "busy_signal": "Sygnał zajętości", + "alarm_clock": "Budzik", + "siren": "Syrena", + "civil_defense_siren": "Syrena obrony cywilnej", + "buzzer": "Brzęczyk", + "smoke_detector": "Czujnik dymu", + "fire_alarm": "Alarm pożarowy", + "foghorn": "Sygnał mgłowy", + "whistle": "Gwizdek", + "steam_whistle": "Gwizdek parowy", + "drum_machine": "Automat perkusyjny", + "rats": "Szczury", + "harpsichord": "Klawesyn", + "musical_instrument": "Instrument muzyczny", + "organ": "Organy", + "dogs": "Psy", + "piano": "Fortepian", + "synthesizer": "Syntezator", + "sampler": "Sampler", + "electronic_organ": "Organy elektroniczne", + "patter": "Tupot", + "drum": "Bęben", + "banjo": "Banjo", + "snare_drum": "Werbel", + "mouse": "Mysz", + "electric_piano": "Pianino elektryczne", + "percussion": "Perkusja", + "hammond_organ": "Organy Hammonda", + "drum_kit": "Zestaw perkusyjny", + "air_brake": "Hamulec pneumatyczny", + "flute": "Flet", + "rowboat": "Łódź wiosłowa", + "squeak": "Skrzypienie", + "cupboard_open_or_close": "Otwieranie lub zamykanie szafki", + "chopping": "Siekanie", + "cello": "Wiolonczela", + "dishes": "Naczynia", + "cutlery": "Sztućce", + "car_passing_by": "Przejeżdżający samochód", + "ice_cream_truck": "Samochód z lodami", + "waves": "Fale", + "race_car": "Samochód wyścigowy", + "steam": "Para", + "reversing_beeps": "Sygnał cofania", + "police_car": "Radiowóz", + "bus": "Autobus", + "emergency_vehicle": "Pojazd uprzywilejowany", + "drawer_open_or_close": "Otwieranie lub zamykanie szuflady", + "traditional_music": "Muzyka tradycyjna", + "electric_shaver": "Elektryczna golarka", + "gurgling": "Bulgotanie", + "crackle": "Trzask", + "vehicle": "Pojazd", + "air_horn": "Klakson pneumatyczny", + "truck": "Ciężarówka", + "scissors": "Nożyczki", + "mechanisms": "Mechanizmy", + "ratchet": "Zapadka", + "tick-tock": "Tik-tak", + "gears": "Przekładnie", + "sewing_machine": "Maszyna do szycia", + "mechanical_fan": "Wentylator mechaniczny", + "air_conditioning": "Klimatyzacja", + "cash_register": "Kasa fiskalna", + "printer": "Drukarka", + "camera": "Kamera", + "single-lens_reflex_camera": "Lustrzanka jednooobiektywowa", + "tools": "Narzędzia", + "hammer": "Młotek", + "jackhammer": "Młot pneumatyczny", + "sawing": "Piłowanie", + "filing": "Pilnikowanie", + "power_tool": "Elektronarzędzie", + "drill": "Wiertarka", + "explosion": "Eksplozja", + "gunshot": "Strzał", + "machine_gun": "Karabin maszynowy", + "fusillade": "Kanonada", + "artillery_fire": "Ogień artyleryjski", + "cap_gun": "Pistolet na kapiszony", + "fireworks": "Fajerwerki", + "firecracker": "Petarda", + "burst": "Wybuch", + "eruption": "Erupcja", + "boom": "Huk", + "wood": "Drewno", + "chop": "Rąbanie", + "splinter": "Drzazga", + "crack": "Pęknięcie", + "glass": "Szkło", + "chink": "Brzęk", + "shatter": "Rozbicie", + "silence": "Cisza", + "sound_effect": "Efekt dźwiękowy", + "environmental_noise": "Hałas otoczenia", + "static": "Szum", + "white_noise": "Biały szum", + "pink_noise": "Różowy szum", + "television": "Telewizor", + "radio": "Radio", + "field_recording": "Nagranie terenowe", + "scream": "Wrzask", + "pulleys": "Bloczki", + "sanding": "Szlifowanie", + "clock": "Zegar", + "tick": "Tykanie", + "sodeling": "Jodłowanie", + "liquid": "Płyn", + "splash": "Plusk", + "slosh": "Rozchlapywanie", + "squish": "Ściskanie", + "drip": "Kapanie", + "pour": "Wlewanie", + "spray": "Pryskanie", + "pump": "Pompowanie", + "stir": "Mieszanie", + "boiling": "Gotowanie", + "arrow": "Strzała", + "breaking": "Łamanie", + "bouncing": "Odbijanie", + "beep": "Pisk", + "clicking": "Klikanie", + "inside": "Wewnątrz", + "outside": "Na zewnątrz", + "chird": "Akord", + "change_ringing": "Karylion", + "shofar": "Szofar", + "trickle": "Spływanie", + "gush": "Wylew", + "fill": "Napełnianie", + "sonar": "Sonar", + "whoosh": "Szybki ruch", + "thump": "Uderzenie", + "thunk": "Odgłos uderzenia", + "electronic_tuner": "Tuner elektroniczny", + "effects_unit": "Moduł efektów", + "chorus_effect": "Efekt chóru", + "basketball_bounce": "Odbijanie piłki", + "bang": "Bum", + "slap": "Policzkowanie", + "whack": "Uderzyć", + "smash": "Rozbić", + "whip": "Bicz", + "flap": "Klapa", + "scratch": "Zdrapywanie", + "scrape": "Skrobać", + "rub": "Pocierać", + "roll": "Rolować", + "crushing": "Rozbijać", + "crumpling": "Zgniatanie", + "tearing": "Rozrywanie", + "ping": "Ping", + "ding": "Dzwonienie", + "clang": "Brzdęk", + "squeal": "Piszczenie", + "creak": "Skrzypieć", + "rustle": "Szelest", + "whir": "Świst", + "clatter": "Stukot", + "sizzle": "Skwierczenie", + "clickety_clack": "Klik-klak", + "rumble": "Grzmot", + "plop": "Pluśnięcie", + "hum": "Szum", + "zing": "Błysk", + "boing": "Odbicie", + "crunch": "Chrupnięcie", + "sine_wave": "Sinusoida", + "harmonic": "Harmoniczna", + "chirp_tone": "Ustawianie tonów", + "pulse": "Puls", + "reverberation": "Pogłos", + "echo": "Echo", + "noise": "Hałas", + "mains_hum": "Szum sieciowy", + "distortion": "Zniekształcenie", + "sidetone": "Sygnał zwrotny", + "throbbing": "Pulsowanie", + "vibration": "Wibracja", + "cacophony": "Kakofonia" +} diff --git a/web/public/locales/pl/common.json b/web/public/locales/pl/common.json new file mode 100644 index 0000000..f9238b4 --- /dev/null +++ b/web/public/locales/pl/common.json @@ -0,0 +1,341 @@ +{ + "time": { + "12hours": "12 godzin", + "1hour": "1 godzina", + "24hours": "24 godziny", + "last7": "Ostatnie 7 dni", + "last14": "Ostatnie 14 dni", + "last30": "Ostatnie 30 dni", + "thisWeek": "W tym tygodniu", + "lastWeek": "Ostatni tydzień", + "thisMonth": "W tym miesiącu", + "lastMonth": "Ostatni miesiąc", + "5minutes": "5 minut", + "10minutes": "10 minut", + "30minutes": "30 minut", + "untilForTime": "Do {{time}}", + "untilForRestart": "Do czasu restartu Frigate.", + "untilRestart": "Do restartu", + "ago": "{{timeAgo}} temu", + "justNow": "W tej chwili", + "today": "Dzisiaj", + "yesterday": "Wczoraj", + "pm": "pm", + "am": "am", + "yr": "{{time}}r", + "year_one": "{{time}} rok", + "year_few": "{{time}} lata", + "year_many": "{{time}} lat", + "mo": "{{time}}m", + "d": "{{time}}d", + "day_one": "{{time}} dzień", + "day_few": "{{time}} dni", + "day_many": "{{time}} dni", + "h": "{{time}}godz", + "m": "{{time}}min", + "s": "{{time}}s", + "month_one": "{{time}} miesiąc", + "month_few": "{{time}} miesiące", + "month_many": "{{time}} miesięcy", + "hour_one": "{{time}} godzina", + "hour_few": "{{time}} godziny", + "hour_many": "{{time}} godzin", + "minute_one": "{{time}} minuta", + "minute_few": "{{time}} minuty", + "minute_many": "{{time}} minut", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampExcludeSeconds": { + "12hour": "%b %-d, %I:%M %p", + "24hour": "%b %-d, %H:%M" + }, + "formattedTimestampWithYear": { + "12hour": "%b %-d %Y, %I:%M %p", + "24hour": "%b %-d %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "second_one": "{{time}} sekunda", + "second_few": "{{time}} sekundy", + "second_many": "{{time}} sekund", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMMM yyyy", + "24hour": "d MMMM yyyy" + }, + "inProgress": "W trakcie", + "invalidStartTime": "Nieprawidłowy czas rozpoczęcia", + "invalidEndTime": "Nieprawidłowy czas zakończenia", + "never": "Nigdy" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "stopy", + "meters": "metry" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/godz", + "mbph": "MB/h", + "gbph": "GB/h" + } + }, + "label": { + "back": "Wróć", + "hide": "Ukryj {{item}}", + "show": "Pokaż {{item}}", + "ID": "ID", + "none": "Brak", + "all": "Wszystko", + "other": "Inne" + }, + "button": { + "apply": "Zastosuj", + "reset": "Resetuj", + "done": "Gotowe", + "enabled": "Włączone", + "enable": "Włącz", + "disable": "Wyłącz", + "save": "Zapisz", + "history": "Historia", + "fullscreen": "Pełny ekran", + "exitFullscreen": "Wyjdź z pełnego ekranu", + "pictureInPicture": "Obraz w obrazie", + "delete": "Usuń", + "yes": "Tak", + "no": "Nie", + "download": "Pobierz", + "info": "Informacje", + "suspended": "Wstrzymane", + "unsuspended": "Wznów", + "play": "Odtwórz", + "unselect": "Odznacz", + "export": "Eksportuj", + "deleteNow": "Usuń teraz", + "next": "Dalej", + "disabled": "Wyłączone", + "back": "Wstecz", + "saving": "Zapisywanie…", + "on": "WŁĄCZ", + "cancel": "Anuluj", + "twoWayTalk": "Komunikacja dwustronna", + "close": "Zamknij", + "copy": "Kopiuj", + "cameraAudio": "Dźwięk kamery", + "off": "WYŁĄCZ", + "edit": "Edytuj", + "copyCoordinates": "Kopiuj współrzędne", + "continue": "Kontynuuj", + "add": "Dodaj", + "applying": "Zastosowywanie…", + "undo": "Cofnij", + "copiedToClipboard": "Skopiowano do schowka", + "modified": "Zmodyfikowane", + "overridden": "Nadpisany", + "resetToDefault": "Przywróć do domyślnych", + "saveAll": "Zapisz wszystkie", + "savingAll": "Zapisywanie wszystkich…", + "undoAll": "Cofnij wszystko", + "retry": "Powtórz", + "resetToGlobal": "Przywróć ustawienie globalne" + }, + "menu": { + "system": "System", + "systemMetrics": "Metryki systemowe", + "configuration": "Konfiguracja", + "systemLogs": "Logi systemowe", + "languages": "Języki", + "language": { + "en": "English (Angielski)", + "zhCN": "简体中文 (Uproszczony Chiński)", + "withSystem": { + "label": "Użyj ustawień systemowych dla języka" + }, + "es": "Español (Hiszpański)", + "cs": "Čeština (Czeski)", + "pl": "Polski (Polski)", + "hi": "हिन्दी (hinduski)", + "ar": "العربية (arabski)", + "fr": "Français (francuski)", + "pt": "Português (portugalski)", + "ru": "Русский (rosyjski)", + "de": "Deutsch (niemiecki)", + "ja": "日本語 (japoński)", + "tr": "Türkçe (Turecki)", + "it": "Italiano (Włoski)", + "nl": "Nederlands (Holenderski)", + "sv": "Svenska (Szwedzki)", + "nb": "Norsk Bokmål (Norweski)", + "ko": "한국어 (Koreański)", + "fa": "فارسی (Perski)", + "uk": "Українська (Ukraiński)", + "he": "עברית (Hebrajski)", + "el": "Ελληνικά (Grecki)", + "hu": "Magyar (Węgierski)", + "da": "Dansk (Duński)", + "sk": "Slovenčina (Słowacki)", + "vi": "Tiếng Việt (Wietnamski)", + "ro": "Română (Rumuński)", + "fi": "Suomi (Fiński)", + "yue": "粵語 (Kantoński)", + "th": "ไทย (Tajski)", + "ca": "Català (Kataloński)", + "ptBR": "Português brasileiro (portugalski - Brazylia)", + "sr": "Српски (Serbski)", + "sl": "Slovenščina (Słowacki)", + "lt": "Lietuvių (Litewski)", + "bg": "Български (Bułgarski)", + "gl": "Galego (Galicyjski)", + "id": "Bahasa Indonesia (Indonezyjski)", + "ur": "اردو (Urdu)", + "hr": "Hrvatski (Chorwacki)", + "zhHant": "繁體中文 (Chiński Tradycyjny)", + "bs": "Bosanski (Bośniacki)" + }, + "appearance": "Wygląd", + "darkMode": { + "label": "Tryb ciemny", + "light": "Jasny", + "dark": "Ciemny", + "withSystem": { + "label": "Użyj ustawień systemowych dla trybu jasnego/ciemnego" + } + }, + "withSystem": "System", + "theme": { + "label": "Motyw", + "blue": "Niebieski", + "green": "Zielony", + "contrast": "Wysoki kontrast", + "default": "Domyślny", + "red": "Czerwony", + "nord": "Nordycki", + "highcontrast": "Wysoki kontrast" + }, + "restart": "Uruchom ponownie Frigate", + "live": { + "title": "Na żywo", + "allCameras": "Wszystkie kamery", + "cameras": { + "title": "Kamery", + "count_one": "{{count}} Kamera", + "count_few": "{{count}} Kamer", + "count_many": "{{count}} Kamery" + } + }, + "review": "Przegląd", + "uiPlayground": "Plac testowy UI", + "faceLibrary": "Biblioteka twarzy", + "user": { + "account": "Konto", + "current": "Aktualny użytkownik: {{user}}", + "anonymous": "anonimowy", + "logout": "Wyloguj", + "setPassword": "Ustaw hasło", + "title": "Użytkownik" + }, + "documentation": { + "title": "Dokumentacja", + "label": "Dokumentacja Frigate" + }, + "explore": "Przeglądaj", + "configurationEditor": "Edytor konfiguracji", + "help": "Pomoc", + "settings": "Ustawienia", + "export": "Eksportuj", + "classification": "Klasyfikacja", + "profiles": "Profile", + "actions": "Akcje", + "features": "Funkcje", + "chat": "Czat" + }, + "role": { + "viewer": "Przeglądający", + "desc": "Administratorzy mają pełny dostęp do wszystkich funkcji w interfejsie Frigate. Przeglądający mają ograniczony dostęp tylko do podglądu kamer, przeglądania nagrań i historycznych materiałów w interfejsie.", + "title": "Rola", + "admin": "Administrator" + }, + "pagination": { + "label": "paginacja", + "previous": { + "title": "Poprzednia", + "label": "Przejdź do poprzedniej strony" + }, + "next": { + "title": "Następna", + "label": "Przejdź do następnej strony" + }, + "more": "Więcej stron" + }, + "accessDenied": { + "title": "Dostęp Zabroniony", + "desc": "Nie masz uprawnień do wyświetlenia tej strony.", + "documentTitle": "Dostęp Zabroniony - Frigate" + }, + "notFound": { + "title": "404", + "desc": "Strona nie znaleziona", + "documentTitle": "Nie Znaleziono - Frigate" + }, + "selectItem": "Wybierz {{item}}", + "toast": { + "copyUrlToClipboard": "Skopiowano URL do schowka.", + "save": { + "error": { + "title": "Nie udało się zapisać zmian konfiguracji: {{errorMessage}}", + "noMessage": "Nie udało się zapisać zmian konfiguracji" + }, + "title": "Zapisz", + "success": "Konfiguracja zapisana." + } + }, + "readTheDocumentation": "Przeczytaj dokumentację", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} i {{1}}", + "many": "{{items}}, oraz {{last}}", + "separatorWithSpace": "; " + }, + "field": { + "optional": "Opcjonalny", + "internalID": "Wewnętrzny identyfikator używany przez Frigate w konfiguracji i bazie danych" + }, + "no_items": "Brak elementów", + "validation_errors": "Błędy walidacji", + "credentialField": { + "savedPlaceholder": "Zapisano — pozostaw puste, aby zachować bieżącą wartość" + } +} diff --git a/web/public/locales/pl/components/auth.json b/web/public/locales/pl/components/auth.json new file mode 100644 index 0000000..12aba0f --- /dev/null +++ b/web/public/locales/pl/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nazwa użytkownika", + "password": "Hasło", + "login": "Login", + "errors": { + "usernameRequired": "Nazwa użytkownika jest wymagana", + "passwordRequired": "Hasło jest wymagane", + "loginFailed": "Logowanie nieudane", + "unknownError": "Nieznany błąd. Sprawdź logi.", + "webUnknownError": "Nieznany błąd. Sprawdź konsolę.", + "rateLimit": "Przekroczono limit częstotliwości. Spróbuj ponownie później." + }, + "firstTimeLogin": "Próbujesz się zalogować po raz pierwszy? Dane logowania są dostępne w logach Frigate." + } +} diff --git a/web/public/locales/pl/components/camera.json b/web/public/locales/pl/components/camera.json new file mode 100644 index 0000000..dfbd823 --- /dev/null +++ b/web/public/locales/pl/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "Grupy kamer", + "add": "Dodaj grupę kamer", + "edit": "Edytuj grupę kamer", + "delete": { + "label": "Usuń grupę kamer", + "confirm": { + "title": "Potwierdź usuwanie", + "desc": "Czy jesteś pewny że chcesz usunąć grupę kamer {{name}}?" + } + }, + "name": { + "placeholder": "Wprowadź nazwę…", + "label": "Nazwa", + "errorMessage": { + "mustLeastCharacters": "Nazwa grupy kamer musi mieć co najmniej 2 znaki.", + "exists": "Grupa kamer o takiej nazwie już istnieje.", + "nameMustNotPeriod": "Nazwa grupy kamer nie może zawierać kropki.", + "invalid": "Niepoprawna nazwa grupy kamer." + } + }, + "cameras": { + "label": "Kamery", + "desc": "Wykierz kamery dla tej grupy." + }, + "icon": "Ikona", + "success": "Grupa kamer ({{name}}) została zapisana.", + "camera": { + "setting": { + "audio": { + "tips": { + "document": "Czytaj dokumentację ", + "title": "Dźwięk musi być wysyłany z kamery i skonfigurowany w go2rtc dla tego strumienia." + } + }, + "label": "Ustawienia Strumieniowania Kamery", + "title": "Ustawienia Strumieniowania {{cameraName}}", + "desc": "Zmień opcje strumieniowania na żywo dla pulpitu tej grupy kamer. Te ustawienia są specyficzne dla urządzenia/przeglądarki.", + "audioIsAvailable": "Dźwięk jest dostępny dla tego strumienia", + "audioIsUnavailable": "Dźwięk jest niedostępny dla tego strumienia", + "streamMethod": { + "label": "Metoda Strumieniowania", + "method": { + "noStreaming": { + "label": "Brak Strumieniowania", + "desc": "Obrazy z kamery będą aktualizowane tylko raz na minutę, bez strumieniowania na żywo." + }, + "smartStreaming": { + "label": "Inteligentne Strumieniowanie (zalecane)", + "desc": "Inteligentne strumieniowanie aktualizuje obraz kamery raz na minutę gdy nie wykryto aktywności, aby oszczędzać przepustowość i zasoby. Gdy aktywność zostanie wykryta, obraz płynnie przełącza się na transmisję na żywo." + }, + "continuousStreaming": { + "desc": { + "title": "Obraz kamery będzie zawsze strumieniowany na żywo, gdy jest widoczny na pulpicie, nawet jeśli nie wykryto aktywności.", + "warning": "Ciągłe strumieniowanie może powodować wysokie zużycie przepustowości i problemy z wydajnością. Używaj ostrożnie." + }, + "label": "Ciągłe Strumieniowanie" + } + }, + "placeholder": "Wybierz sposób strumieniowania" + }, + "compatibilityMode": { + "label": "Tryb kompatybilności", + "desc": "Włącz tę opcję tylko jeśli transmisja na żywo z kamery wyświetla artefakty kolorów i ma ukośną linię po prawej stronie obrazu." + }, + "placeholder": "Wybierz strumień", + "stream": "Strumień" + }, + "birdseye": "Widok z lotu ptaka" + }, + "showAll": "Pokaż wszystkie grupy kamer", + "showLess": "Pokaż mniej", + "editGroups": "Edytuj grupy kamer" + }, + "debug": { + "options": { + "label": "Ustawienia", + "title": "Opcje", + "showOptions": "Pokaż Opcje", + "hideOptions": "Ukryj Opcje" + }, + "timestamp": "Znacznik czasu", + "zones": "Strefy", + "mask": "Maski", + "regions": "Regiony", + "motion": "Ruch", + "boundingBox": "Ramka Ograniczająca", + "paths": "Ścieżki" + } +} diff --git a/web/public/locales/pl/components/dialog.json b/web/public/locales/pl/components/dialog.json new file mode 100644 index 0000000..c2cde61 --- /dev/null +++ b/web/public/locales/pl/components/dialog.json @@ -0,0 +1,217 @@ +{ + "restart": { + "title": "Czy na pewno chcesz ponownie uruchomić Frigate?", + "button": "Uruchom ponownie", + "restarting": { + "title": "Frigate uruchamia się ponownie", + "content": "Strona odświeży się za {{countdown}} sekund.", + "button": "Wymuś odświeżenie" + }, + "description": "Spowoduje to chwilowe zatrzymanie Frigate i ponowne uruchomienie." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Wyślij do Frigate+", + "desc": "Obiekty w miejscach, których chcesz unikać, nie są fałszywymi alarmami. Zgłaszanie ich jako fałszywe alarmy zdezorientuje model." + }, + "review": { + "true": { + "label": "Potwierdź tę etykietę dla Frigate Plus", + "true_one": "To jest {{label}}", + "true_few": "To są {{label}}", + "true_many": "To są {{label}}" + }, + "false": { + "label": "Nie potwierdzaj tej etykiety dla Frigate Plus", + "false_one": "To nie jest {{label}}", + "false_few": "To nie są {{label}}", + "false_many": "To nie są {{label}}" + }, + "state": { + "submitted": "Przesłano" + }, + "question": { + "ask_a": "Czy ten obiekt to {{label}}?", + "ask_an": "Czy ten obiekt to {{label}}?", + "ask_full": "Czy ten obiekt to {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Potwierdź tę etykietę dla Frigate Plus" + } + } + }, + "video": { + "viewInHistory": "Zobacz w Historii" + } + }, + "export": { + "time": { + "lastHour_one": "Ostatnia godzina", + "lastHour_few": "Ostatnie {{count}} godziny", + "lastHour_many": "Ostatnie {{count}} godzin", + "fromTimeline": "Wybierz z Osi Czasu", + "custom": "Niestandardowy", + "start": { + "title": "Czas Rozpoczęcia", + "label": "Wybierz Czas Rozpoczęcia" + }, + "end": { + "title": "Czas Zakończenia", + "label": "Wybierz Czas Zakończenia" + } + }, + "name": { + "placeholder": "Nazwij Eksport" + }, + "select": "Wybierz", + "export": "Eksportuj", + "selectOrExport": "Wybierz lub Eksportuj", + "toast": { + "success": "Pomyślnie rozpoczęto eksport. Zobacz plik na stronie eksportów.", + "error": { + "failed": "Nie udało się zakolejkować eksportu: {{error}}", + "endTimeMustAfterStartTime": "Czas zakończenia musi być późniejszy niż czas rozpoczęcia", + "noVaildTimeSelected": "Nie wybrano prawidłowego zakresu czasu" + }, + "view": "Widok", + "queued": "Eksport dodany do kolejki. Postęp można sprawdzić na stronie eksportów.", + "batchSuccess_one": "Rozpoczęto 1 eksport. Otwieranie przypadku.", + "batchSuccess_few": "Rozpoczęto {{count}} eksporty Otwieranie przypadku.", + "batchSuccess_many": "Rozpoczęto {{count}} eksportów. Otwieranie przypadku.", + "batchPartial": "Rozpoczęto {{successful}} z {{total}} eksportów. Nieudane kamery: {{failedCameras}}", + "batchFailed": "Nie udało się uruchomić {{total}} eksportów. Nieudane kamery: {{failedCameras}}", + "batchQueuedSuccess_one": "Eksport dodany do kolejki. Otwieranie przypadku.", + "batchQueuedSuccess_few": "Dodano {{count}} eksportydo kolejki. Otwieranie przypadku.", + "batchQueuedSuccess_many": "Dodano {{count}} eksportów do kolejki. Otwieranie przypadku.", + "batchQueuedPartial": "Dodano do kolejki {{successful}} z {{total}} eksportów. Nieudane kamery: {{failedCameras}}", + "batchQueueFailed": "Nie udało się dodać do kolejki {{total}} eksportów. Nieudane kamery: {{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "Zapisz Eksport", + "previewExport": "Podgląd Eksportu", + "queueingExport": "Kolejkowanie eksportu...", + "useThisRange": "Użyj tego zakresu" + }, + "multiCamera": { + "timeRange": "Zakres czasu", + "selectFromTimeline": "Wybierz z osi czasu", + "cameraSelection": "Kamery", + "cameraSelectionHelp": "Kamery ze śledzonymi obiektami w tym przedziale czasowym są wstępnie wybierane", + "checkingActivity": "Sprawdzanie aktywności kamery...", + "noCameras": "Brak dostępnych kamer", + "detectionCount_one": "{{count}} śledzony obiekt", + "detectionCount_few": "{{count}} śledzone obiekty", + "detectionCount_many": "{{count}} śledzonych obiektów", + "nameLabel": "Wyeksportuj nazwę", + "namePlaceholder": "Opcjonalna nazwa bazowa eksportów", + "queueingButton": "Kolejkowanie eksportów...", + "exportButton_one": "Eksport 1 kamery", + "exportButton_few": "Eksport {{count}} kamer", + "exportButton_many": "Eksport {{count}} kamer", + "searchOrSelectGroup": "Szukaj lub wybierz grupę kamer ...", + "selectAll": "Wybierz wszystkie kamery", + "clearSelection": "Wyczyść wybór", + "selectWithActivity": "Kamery z obiektami śledzonymi", + "selectGroup": "Wybierz grupę", + "noMatchingCameras": "Nie ma kamer spełniających Twoje wyszukiwanie", + "selectedCount": "{{selected}} / {{total}} wybrane" + }, + "case": { + "newCaseOption": "Utwórz nowy przypadek", + "newCaseNamePlaceholder": "Nazwa nowego przypadku", + "newCaseDescriptionPlaceholder": "Opis przypadku", + "label": "Przypadek", + "nonAdminHelp": "Dla tych eksportów zostanie utworzony nowy przypadek.", + "placeholder": "Wybierz przypadek" + }, + "queueing": "Dodawanie eksportu do kolejki...", + "tabs": { + "export": "Pojedyncza kamera", + "multiCamera": "Wiele kamer" + }, + "multi": { + "title_one": "Eksport 1 recenzji", + "title_few": "Eksport {{count}} recenzji", + "title_many": "Eksport {{count}} recenzji", + "description": "Eksportuj każdą wybraną recenzję. Wszystkie eksporty zostaną pogrupowane w jednym przypadku.", + "descriptionNoCase": "Eksportuj każdą wybraną recenzję.", + "caseNamePlaceholder": "Eksport recenzji – {{date}}", + "exportButton_one": "Eksport 1 recenzji", + "exportButton_few": "Eksport {{count}} recenzji", + "exportButton_many": "Eksport {{count}} recenzji", + "exportingButton": "Eksportowanie...", + "toast": { + "started_one": "Rozpoczęto 1 eksport. Otwieranie przypadku.", + "started_few": "Rozpoczęto {{count}} eksportów. Otwieranie przypadku.", + "started_many": "Rozpoczęto {{count}} eksportów. Otwieranie przypadku.", + "startedNoCase_one": "Rozpoczęto 1 eksport.", + "startedNoCase_few": "Rozpoczęto {{count}} eksportów.", + "startedNoCase_many": "Rozpoczęto {{count}} eksportów.", + "partial": "Rozpoczęto {{successful}} z {{total}} eksportów. Niepowodzenia: {{failedItems}}", + "failed": "Nie udało się uruchomić {{total}} eksportów. Niepowodzenia: {{failedItems}}" + } + } + }, + "recording": { + "button": { + "markAsReviewed": "Oznacz jako sprawdzone", + "deleteNow": "Usuń teraz", + "export": "Eksportuj", + "markAsUnreviewed": "Oznacz jako niesprawdzone" + }, + "confirmDelete": { + "title": "Potwierdź Usunięcie", + "desc": { + "selected": "Czy na pewno chcesz usunąć wszystkie nagrane wideo powiązane z tym elementem recenzji?

    Przytrzymaj klawisz Shift, aby pominąć to okno dialogowe w przyszłości." + }, + "toast": { + "success": "Nagrania wideo powiązane z wybranymi elementami przeglądu zostały pomyślnie usunięte.", + "error": "Nie udało się usunąć: {{error}}" + } + }, + "shareTimestamp": { + "label": "Udostępnij znacznik czasu", + "title": "Udostępnij znacznik czasu", + "description": "Udostępnij adres URL z aktualnym znacznikiem czasu pozycji odtwarzacza lub wybierz niestandardowy znacznik czasu. Należy pamiętać, że nie jest to publiczny link udostępniania i jest dostępny wyłącznie dla użytkowników mających dostęp do Frigate i tej kamery.", + "custom": "Własny znacznik czasu", + "button": "Udostępnij adres URL ze znacznikiem czasu", + "shareTitle": "Znacznik czasu przeglądu Frigate: {{camera}}" + } + }, + "streaming": { + "label": "Strumień", + "restreaming": { + "disabled": "Restreaming nie jest włączony dla tej kamery.", + "desc": { + "title": "Skonfiguruj go2rtc dla dodatkowych opcji podglądu na żywo i dźwięku dla tej kamery.", + "readTheDocumentation": "Przeczytaj dokumentację" + } + }, + "showStats": { + "label": "Pokaż statystyki strumienia", + "desc": "Włącz tę opcję, aby wyświetlać statystyki strumienia jako nakładkę na obrazie z kamery." + }, + "debugView": "Widok Debugowania" + }, + "search": { + "saveSearch": { + "label": "Zapisz Wyszukiwanie", + "desc": "Podaj nazwę dla tego zapisanego wyszukiwania.", + "placeholder": "Wprowadź nazwę dla swojego wyszukiwania", + "overwrite": "{{searchName}} już istnieje. Zapisanie nadpisze istniejącą wartość.", + "success": "Wyszukiwanie ({{searchName}}) zostało zapisane.", + "button": { + "save": { + "label": "Zapisz to wyszukiwanie" + } + } + } + }, + "imagePicker": { + "selectImage": "Wybierz miniaturkę śledzonego obiektu", + "search": { + "placeholder": "Wyszukaj po etykiecie (label) lub etykiecie potomnej (sub label)..." + }, + "noImages": "Brak miniatur dla tej kamery", + "unknownLabel": "Zapisany obraz wyzwalacza" + } +} diff --git a/web/public/locales/pl/components/filter.json b/web/public/locales/pl/components/filter.json new file mode 100644 index 0000000..7de30b2 --- /dev/null +++ b/web/public/locales/pl/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filtr", + "labels": { + "label": "Etykiety", + "all": { + "title": "Wszystkie Etykiety", + "short": "Etykiety" + }, + "count_one": "{{count}} Etykieta", + "count_other": "{{count}} Etykiet(y)" + }, + "zones": { + "label": "Strefy", + "all": { + "title": "Wszystkie Strefy", + "short": "Strefy" + } + }, + "subLabels": { + "all": "Wszystkie Podetykiety", + "label": "Podetykiety" + }, + "score": "Wynik", + "estimatedSpeed": "Szacowana Prędkość ({{unit}})", + "features": { + "label": "Funkcje", + "hasSnapshot": "Posiada stop klatkę", + "hasVideoClip": "Posiada klip wideo", + "submittedToFrigatePlus": { + "label": "Przesłano do Frigate+", + "tips": "Musisz najpierw filtrować obiekty śledzone, które mają stop klatkę.

    Obiekty śledzone bez stop klatki nie mogą być przesłane do Frigate+." + } + }, + "sort": { + "dateDesc": "Data (Malejąco)", + "scoreAsc": "Wynik Obiektu (Rosnąco)", + "scoreDesc": "Wynik Obiektu (Malejąco)", + "speedAsc": "Szacowana Prędkość (Rosnąco)", + "speedDesc": "Szacowana Prędkość (Malejąco)", + "label": "Sortuj", + "dateAsc": "Data (Rosnąco)", + "relevance": "Trafność" + }, + "explore": { + "settings": { + "gridColumns": { + "desc": "Wybierz liczbę kolumn w widoku siatki.", + "title": "Kolumny Siatki" + }, + "searchSource": { + "label": "Źródło Wyszukiwania", + "desc": "Wybierz, czy przeszukiwać miniatury czy opisy śledzonych obiektów.", + "options": { + "description": "Opis", + "thumbnailImage": "Obraz Miniatury" + } + }, + "defaultView": { + "title": "Domyślny Widok", + "summary": "Podsumowanie", + "unfilteredGrid": "Niefiltrowana Siatka", + "desc": "Gdy nie wybrano filtrów, wyświetl podsumowanie najnowszych śledzonych obiektów dla każdej etykiety lub wyświetl niefiltrowaną siatkę." + }, + "title": "Ustawienia" + }, + "date": { + "selectDateBy": { + "label": "Wybierz datę do filtrowania" + } + } + }, + "logSettings": { + "label": "Filtruj poziom logów", + "filterBySeverity": "Filtruj logi według ważności", + "loading": { + "title": "Ładowanie", + "desc": "Gdy panel logów jest przewinięty do dołu, nowe logi są automatycznie strumieniowane w miarę ich dodawania." + }, + "disableLogStreaming": "Wyłącz strumieniowanie logów", + "allLogs": "Wszystkie logi" + }, + "recognizedLicensePlates": { + "loading": "Ładowanie rozpoznanych tablic rejestracyjnych…", + "placeholder": "Wpisz, aby wyszukać tablice rejestracyjne…", + "noLicensePlatesFound": "Nie znaleziono tablic rejestracyjnych.", + "title": "Rozpoznane Tablice Rejestracyjne", + "loadFailed": "Nie udało się załadować rozpoznanych tablic rejestracyjnych.", + "selectPlatesFromList": "Wybierz jedną lub więcej tablic z listy.", + "selectAll": "Wybierz wszystko", + "clearAll": "Wyczyść wszystko" + }, + "dates": { + "all": { + "title": "Wszystkie Daty", + "short": "Daty" + }, + "selectPreset": "Wybierz ustawienie…" + }, + "more": "Więcej Filtrów", + "reset": { + "label": "Resetuj filtry do wartości domyślnych" + }, + "timeRange": "Zakres Czasu", + "cameras": { + "label": "Filtr Kamer", + "all": { + "title": "Wszystkie Kamery", + "short": "Kamery" + } + }, + "review": { + "showReviewed": "Pokaż Przejrzane" + }, + "motion": { + "showMotionOnly": "Pokaż Tylko Ruch" + }, + "trackedObjectDelete": { + "toast": { + "success": "Śledzone obiekty zostały pomyślnie usunięte.", + "error": "Nie udało się usunąć śledzonych obiektów: {{errorMessage}}" + }, + "title": "Potwierdź Usunięcie", + "desc": "Usunięcie tych {{objectLength}} śledzonych obiektów usuwa stop klatki, wszelkie zapisane osadzenia i wszystkie powiązane wpisy cyklu życia obiektu. Nagrane materiały tych śledzonych obiektów w widoku Historii NIE zostaną usunięte.

    Czy na pewno chcesz kontynuować?

    Przytrzymaj klawisz Shift, aby pominąć to okno dialogowe w przyszłości." + }, + "zoneMask": { + "filterBy": "Filtruj według maski strefy" + }, + "classes": { + "label": "Klasy", + "all": { + "title": "Wszystkie Klasy" + }, + "count_one": "{{count}} Klasa", + "count_other": "{{count}} Klas(y)" + }, + "attributes": { + "all": "Wszystkie atrybuty", + "label": "Atrybuty klasyfikacji" + } +} diff --git a/web/public/locales/pl/components/icons.json b/web/public/locales/pl/components/icons.json new file mode 100644 index 0000000..35cbdc1 --- /dev/null +++ b/web/public/locales/pl/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Wybierz ikonę", + "search": { + "placeholder": "Wyszukaj ikonę…" + } + } +} diff --git a/web/public/locales/pl/components/input.json b/web/public/locales/pl/components/input.json new file mode 100644 index 0000000..1216c7a --- /dev/null +++ b/web/public/locales/pl/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Pobierz Wideo", + "toast": { + "success": "Rozpoczęto pobieranie nagrania do przeglądu." + } + } + } +} diff --git a/web/public/locales/pl/components/player.json b/web/public/locales/pl/components/player.json new file mode 100644 index 0000000..3cd8520 --- /dev/null +++ b/web/public/locales/pl/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "Nie znaleziono żadnych nagrań w tym czasie", + "cameraDisabled": "Kamera jest wyłączona", + "stats": { + "latency": { + "title": "Opóźnienie:", + "value": "{{seconds}} sekund", + "short": { + "title": "Opóźnienie", + "value": "{{seconds}} s" + } + }, + "totalFrames": "Całkowita liczba klatek:", + "streamType": { + "title": "Typ transmisji:", + "short": "Typ" + }, + "bandwidth": { + "title": "Przepustowość:", + "short": "Przepustowość" + }, + "droppedFrames": { + "title": "Porzucone klatki:", + "short": { + "title": "Porzucone", + "value": "{{droppedFrames}} klatek" + } + }, + "decodedFrames": "Zdekodowane klatki:", + "droppedFrameRate": "Współczynnik porzuconych klatek:" + }, + "noPreviewFound": "Nie znaleziono podglądu", + "noPreviewFoundFor": "Nie znaleziono podglądu dla {{cameraName}}", + "submitFrigatePlus": { + "title": "Wyślij tę klatkę do Frigate+?", + "submit": "Wyślij", + "previewError": "Nie udało się załadować podglądu nagrania. Nagranie może być niedostępne w podanym czasie." + }, + "livePlayerRequiredIOSVersion": "Wymagana wersja iOS 17.1 lub nowsza dla tego typu transmisji na żywo.", + "streamOffline": { + "title": "Transmisja offline", + "desc": "Nie otrzymano klatek na strumieniu {{cameraName}} detect, sprawdź logi błędów" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Pomyślnie wysłano klatkę do Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Nie udało się wysłać klatki do Frigate+" + } + }, + "cameraOff": "Kamera jest wyłączona" +} diff --git a/web/public/locales/pl/config/cameras.json b/web/public/locales/pl/config/cameras.json new file mode 100644 index 0000000..f890558 --- /dev/null +++ b/web/public/locales/pl/config/cameras.json @@ -0,0 +1,340 @@ +{ + "objects": { + "genai": { + "required_zones": { + "label": "Wymagane strefy", + "description": "Strefy, które należy wprowadzić, aby obiekty kwalifikowały się do generowania opisu GenAI." + }, + "debug_save_thumbnails": { + "label": "Zapisz miniatury", + "description": "Zapisz miniatury wysłane do GenAI w celu debugowania i przeglądu." + }, + "send_triggers": { + "label": "Wyzwalacze GenAI", + "description": "Określa, kiedy ramki powinny być wysyłane do GenAI (na końcu, po aktualizacjach itp.).", + "tracked_object_end": { + "label": "Wyślij na koniec", + "description": "Wyślij żądanie do GenAI, gdy śledzony obiekt się zakończy." + }, + "after_significant_updates": { + "label": "Wczesny wyzwalacz GenAI", + "description": "Wyślij żądanie do GenAI po określonej liczbie istotnych aktualizacji dla śledzonego obiektu." + } + }, + "enabled_in_config": { + "label": "Oryginalny stan GenAI", + "description": "Wskazuje, czy GenAI było włączone w oryginalnej konfiguracji statycznej." + } + } + }, + "record": { + "label": "Nagrywanie", + "enabled": { + "label": "Włącz nagrywanie" + }, + "expire_interval": { + "label": "Częstotliwość usuwania nagrań", + "description": "Minuty między kolejnymi operacjami czyszczenia, które usuwają nieaktualne segmenty nagrań." + }, + "continuous": { + "label": "Ciągłe przechowywanie", + "description": "Liczba dni przechowywania nagrań niezależnie od śledzonych obiektów lub ruchu. Ustaw wartość 0, jeśli chcesz przechowywać tylko nagrania alertów i wykrytych zdarzeń.", + "days": { + "label": "Ilość dni przechowywania", + "description": "Dni przechowywania nagrań." + } + }, + "motion": { + "label": "Przechowywanie ruchu", + "description": "Liczba dni przechowywania nagrań wywołanych ruchem, niezależnie od śledzonych obiektów. Ustaw wartość 0, jeśli chcesz przechowywać tylko nagrania alertów i wykrytych zdarzeń.", + "days": { + "label": "Dni przechowywania", + "description": "Dni przechowywania nagrań." + } + }, + "detections": { + "label": "Przechowywanie detekcji", + "description": "Ustawienia przechowywania nagrań dla zdarzeń wykrywania, w tym czas przed i po przechwyceniu.", + "pre_capture": { + "label": "Sekundy pre-alarmu", + "description": "Liczba sekund przed zdarzeniem wykrycia, które ma zostać uwzględnione w nagraniu." + }, + "post_capture": { + "label": "Sekundy post-alarmu", + "description": "Liczba sekund po wykryciu zdarzenia, które ma zostać uwzględnione w nagraniu." + }, + "retain": { + "label": "Przechowywanie zdarzeń", + "description": "Ustawienia przechowywania nagrań zdarzeń wykrytych.", + "days": { + "label": "Dni przechowywania", + "description": "Liczba dni przechowywania nagrań zdarzeń wykrytych." + }, + "mode": { + "label": "Tryb przechowywania", + "description": "Tryb przechowywania: wszystkie (zapisz wszystkie segmenty), ruch (zapisz segmenty z ruchem) lub aktywne obiekty (zapisz segmenty z aktywnymi obiektami)." + } + } + }, + "alerts": { + "label": "Przechowywanie alarmów", + "description": "Ustawienia przechowywania nagrań dla zdarzeń alarmowych, w tym czas przed i po przechwyceniu.", + "pre_capture": { + "label": "Długość pre-alarmu", + "description": "Liczba sekund przed zdarzeniem wykrycia, które ma zostać uwzględnione w nagraniu." + }, + "post_capture": { + "label": "Długość post-alarmu", + "description": "Liczba sekund po wykryciu zdarzenia, które ma zostać uwzględnione w nagraniu." + }, + "retain": { + "label": "Przechowywanie zdarzeń", + "description": "Ustawienia przechowywania nagrań zdarzeń wykrytych.", + "days": { + "label": "Dni przechowywania", + "description": "Liczba dni przechowywania nagrań zdarzeń wykrytych." + }, + "mode": { + "label": "Tryb przechowywania", + "description": "Tryb przechowywania: wszystkie (zapisz wszystkie segmenty), ruch (zapisz segmenty z ruchem) lub aktywne obiekty (zapisz segmenty z aktywnymi obiektami)." + } + } + }, + "export": { + "label": "Eksport konfiguracji", + "description": "Ustawienia używane podczas eksportowania nagrań, takie jak przyspieszenie czasu i przyspieszenie sprzętowe.", + "hwaccel_args": { + "label": "Eksportuj argumenty akceleracji sprzętowej", + "description": "Argumenty przyspieszenia sprzętowego do wykorzystania podczas operacji eksportu/transkodowania." + } + }, + "preview": { + "label": "Podgląd konfiguracji", + "description": "Ustawienia kontrolujące jakość podglądu nagrań wyświetlanych w interfejsie użytkownika.", + "quality": { + "label": "Jakość podglądu", + "description": "Poziom jakości podglądu (bardzo_niski, niski, średni, wysoki, bardzo_wysoki)." + } + }, + "enabled_in_config": { + "label": "Oryginalny stan nagrania", + "description": "Wskazuje, czy nagrywanie było włączone w oryginalnej konfiguracji statycznej." + } + }, + "review": { + "label": "Recenzja", + "alerts": { + "label": "Konfiguracja alarmów", + "description": "Ustawienia dotyczące obiektów śledzonych, które generują alerty, oraz sposobu przechowywania alertów.", + "enabled": { + "label": "Włącz alarmy" + }, + "labels": { + "label": "Etykiety alarmów", + "description": "Lista etykiet obiektów, które kwalifikują się jako alerty (na przykład: samochód, osoba)." + }, + "required_zones": { + "label": "Wymagane strefy", + "description": "Strefy, do których obiekt musi wejść, aby zostać uznanym za alarm; pozostaw puste, aby zezwolić na dowolną strefę." + }, + "enabled_in_config": { + "label": "Oryginalny stan alarmów", + "description": "Śledzi, czy alarmy były pierwotnie włączone w konfiguracji statycznej." + }, + "cutoff_time": { + "label": "Czas wyłączenia alarmów", + "description": "Sekundy, które należy odczekać po zakończeniu czynności powodującej alarm, zanim alarm zostanie wyłączony." + } + }, + "detections": { + "label": "Konfiguracja detekcji", + "description": "Ustawienia dotyczące tworzenia zdarzeń wykrywania (niebędących alertami) oraz czasu ich przechowywania.", + "enabled": { + "label": "Włącz detekcje" + }, + "labels": { + "label": "Etykiety detekcji", + "description": "Lista etykiet(klas) obiektów, które kwalifikują się jako zdarzenia wykrycia." + }, + "required_zones": { + "label": "Wymagane strefy", + "description": "Strefy, do których obiekt musi wejść, aby zostać wykryty; pozostaw puste, aby zezwolić na dowolną strefę." + } + } + }, + "label": "Konfiguracja kamery", + "name": { + "label": "Nazwa kamery", + "description": "Nazwa kamery jest wymagana" + }, + "friendly_name": { + "label": "Przyjazna nazwa", + "description": "Przyjazna nazwa kamery używana w interfejsie Frigate" + }, + "enabled": { + "label": "Włączone", + "description": "Włączone" + }, + "audio": { + "label": "Wykrywanie dźwięku", + "description": "Ustawienia detekcji zdarzeń audio dla tej kamery.", + "enabled": { + "label": "Włącz detekcję audio", + "description": "Włącz lub wyłącz detekcję zdarzeń audio dla tej kamery." + }, + "max_not_heard": { + "label": "Limit czasu zakończenia", + "description": "Czas w sekundach bez wykrycia skonfigurowanego typu audio, po którym zdarzenie audio zostaje zakończone." + }, + "min_volume": { + "label": "Minimalna głośność", + "description": "Minimalny próg głośności RMS wymagany do uruchomienia detekcji audio; niższe wartości zwiększają czułość (np. 200 wysoka, 500 średnia, 1000 niska)." + }, + "listen": { + "label": "Typy nasłuchu", + "description": "Lista typów zdarzeń audio do wykrywania (na przykład: szczekanie, alarm pożarowy, krzyk, mowa, wrzask)." + }, + "filters": { + "label": "Filtry audio", + "description": "Ustawienia filtrów dla poszczególnych typów audio, takie jak progi pewności, używane do redukcji fałszywych alarmów.", + "threshold": { + "label": "Minimalna pewność wykrycia dźwięku", + "description": "Minimalny próg pewności, aby zdarzenie audio zostało zliczone." + } + }, + "enabled_in_config": { + "label": "Pierwotny stan audio", + "description": "Wskazuje, czy detekcja audio była pierwotnie włączona w statycznym pliku konfiguracyjnym." + }, + "num_threads": { + "label": "Wątki detekcji", + "description": "Liczba wątków używanych do przetwarzania detekcji audio." + } + }, + "audio_transcription": { + "label": "Transkrypcja audio", + "description": "Ustawienia transkrypcji audio na żywo i mowy, używane do zdarzeń i napisów na żywo.", + "enabled": { + "label": "Włącz transkrypcję", + "description": "Włącz lub wyłącz ręcznie wyzwalaną transkrypcję zdarzeń audio." + }, + "enabled_in_config": { + "label": "Pierwotny stan transkrypcji" + }, + "live_enabled": { + "label": "Transkrypcja na żywo", + "description": "Włącz transkrypcję strumieniową audio na żywo w momencie jego odbierania." + } + }, + "birdseye": { + "mode": { + "label": "Tryb śledzenia", + "description": "Tryb uwzględniania kamer w widoku z lotu ptaka: „obiekty”, „ruch” lub „ciągły”." + }, + "order": { + "label": "Pozycja", + "description": "Pozycja numeryczna określająca kolejność kamery w układzie widoku z lotu ptaka." + }, + "label": "Widok z lotu ptaka", + "description": "Ustawienia widoku z lotu ptaka „Birdseye”, który łączy obraz z wielu kamer w jeden układ.", + "enabled": { + "label": "Włącz widok z lotu ptaka", + "description": "Włącz lub wyłącz funkcję widoku z lotu ptaka." + } + }, + "detect": { + "enabled": { + "label": "Włącz wykrywanie obiektów", + "description": "Włącza lub wyłącza wykrywanie obiektów dla tej kamery." + }, + "stationary": { + "label": "Konfiguracja obiektów stacjonarnych", + "description": "Ustawienia wykrywania i obsługi obiektów statycznych przez określony czas.", + "interval": { + "label": "Okres bezruchu", + "description": "Częstotliwość (w klatkach) sprawdzania detekcji dla obiektów nieruchomych." + }, + "threshold": { + "label": "Próg bezruchu", + "description": "Liczba klatek bez zmiany pozycji potrzebna do uznania obiektu za nieruchomy." + }, + "max_frames": { + "label": "Maksymalna liczba klatek", + "description": "Ogranicza czas śledzenia nieruchomych obiektów przed ich usunięciem.", + "default": { + "label": "Domyślna maks. liczba klatek", + "description": "Domyślna maksymalna liczba klatek śledzenia nieruchomego obiektu przed zatrzymaniem." + }, + "objects": { + "label": "Maksymalna liczba klatek obiektu", + "description": "Nadpisania dla poszczególnych obiektów określające maksymalną liczbę klatek śledzenia nieruchomych obiektów." + } + }, + "classifier": { + "label": "Włącz klasyfikator wizualny", + "description": "Użyj klasyfikatora wizualnego do wykrywania rzeczywiście nieruchomych obiektów, nawet gdy ramki ograniczające drżą." + } + }, + "label": "Wykrywanie obiektów", + "description": "Ustawienia roli „detection/detect” używanej do wykrywania obiektów i inicjalizacji trackerów.", + "height": { + "label": "Wysokość wykrywania", + "description": "Wysokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia." + }, + "width": { + "label": "Szerokość wykrywania", + "description": "Szerokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia." + }, + "fps": { + "label": "FPS wykrywania", + "description": "Docelowa liczba klatek na sekundę używana do detekcji; niższe wartości zmniejszają użycie CPU (zalecana wartość to 5, ustaw wyższą — maksymalnie 10 — tylko jeśli śledzisz bardzo szybko poruszające się obiekty)." + }, + "min_initialized": { + "label": "Minimalna liczba klatek inicjalizacyjnych", + "description": "Liczba kolejnych trafień detekcji wymagana do utworzenia śledzonego obiektu. Zwiększenie wartości zmniejsza liczbę fałszywych inicjalizacji. Wartość domyślna to fps podzielone przez 2." + }, + "max_disappeared": { + "label": "Maksymalna liczba pominiętych klatek", + "description": "Liczba klatek bez detekcji, po której obiekt śledzony jest uznawany za utracony." + }, + "annotation_offset": { + "label": "Przesunięcie oznaczenia", + "description": "Liczba milisekund przesunięcia adnotacji detekcji w celu lepszego dopasowania ramek ograniczających na osi czasu do nagrań; wartość może być dodatnia lub ujemna." + } + }, + "face_recognition": { + "label": "Rozpoznawanie twarzy", + "description": "Ustawienia wykrywania i rozpoznawania twarzy dla tej kamery.", + "enabled": { + "label": "Włącz rozpoznawanie twarzy", + "description": "Włącz lub wyłącz rozpoznawanie twarzy." + }, + "min_area": { + "label": "Minimalny obszar twarzy", + "description": "Minimalny obszar twarzy (w pikselach) wymagany do podjęcia próby rozpoznania." + } + }, + "ffmpeg": { + "label": "Strumienie (FFmpeg)", + "output_args": { + "detect": { + "description": "Domyślne argumenty wyjściowe dla strumieni z rolą detekcji." + }, + "record": { + "description": "Domyślne argumenty wyjściowe dla strumieni z rolą zapisu." + } + }, + "retry_interval": { + "label": "Czas ponownej próby FFmpeg", + "description": "Czas (w sekundach) przed ponownym uruchomieniem utraconego strumienia kamery. Domyślnie: 10." + }, + "apple_compatibility": { + "label": "Kompatybilność z Apple", + "description": "Włącz tagowanie HEVC, aby zapewnić lepszą zgodność z odtwarzaczami Apple podczas nagrywania w formacie H.265." + }, + "gpu": { + "label": "Indeks GPU", + "description": "Domyślny indeks karty graficznej używany do akceleracji sprzętowej, jeśli jest dostępna." + } + } +} diff --git a/web/public/locales/pl/config/global.json b/web/public/locales/pl/config/global.json new file mode 100644 index 0000000..59d2232 --- /dev/null +++ b/web/public/locales/pl/config/global.json @@ -0,0 +1,183 @@ +{ + "audio": { + "label": "Wykrywanie dźwięku", + "enabled": { + "label": "Włącz detekcję audio" + }, + "max_not_heard": { + "label": "Limit czasu zakończenia", + "description": "Czas w sekundach bez wykrycia skonfigurowanego typu audio, po którym zdarzenie audio zostaje zakończone." + }, + "min_volume": { + "label": "Minimalna głośność", + "description": "Minimalny próg głośności RMS wymagany do uruchomienia detekcji audio; niższe wartości zwiększają czułość (np. 200 wysoka, 500 średnia, 1000 niska)." + }, + "listen": { + "label": "Typy nasłuchu", + "description": "Lista typów zdarzeń audio do wykrywania (na przykład: szczekanie, alarm pożarowy, krzyk, mowa, wrzask)." + }, + "filters": { + "label": "Filtry audio", + "description": "Ustawienia filtrów dla poszczególnych typów audio, takie jak progi pewności, używane do redukcji fałszywych alarmów.", + "threshold": { + "label": "Minimalna pewność wykrycia dźwięku", + "description": "Minimalny próg pewności, aby zdarzenie audio zostało zliczone." + } + }, + "enabled_in_config": { + "label": "Pierwotny stan audio", + "description": "Wskazuje, czy detekcja audio była pierwotnie włączona w statycznym pliku konfiguracyjnym." + }, + "num_threads": { + "label": "Wątki detekcji", + "description": "Liczba wątków używanych do przetwarzania detekcji audio." + } + }, + "audio_transcription": { + "label": "Transkrypcja audio", + "description": "Ustawienia transkrypcji audio na żywo i mowy, używane do zdarzeń i napisów na żywo.", + "live_enabled": { + "label": "Transkrypcja na żywo", + "description": "Włącz transkrypcję strumieniową audio na żywo w momencie jego odbierania." + } + }, + "version": { + "label": "Bieżąca wersja konfiguracji", + "description": "Liczbowa lub znakowa wersja aktywnej konfiguracji w celu wykrywania migracji lub zmiany formatu." + }, + "safe_mode": { + "label": "Tryb bezpieczny", + "description": "Po włączeniu Frigate uruchomi się w trybie awaryjnym z ograniczonymi funkcjami, co ułatwi rozwiązywanie problemów." + }, + "environment_vars": { + "label": "Zmienne środowiskowe", + "description": "Pary klucz-wartość zmiennych środowiskowych, które należy ustawić dla procesu Frigate w systemie Home Assistant. Użytkownicy niekorzystający z Home Assistant powinni zamiast tego skorzystać z konfiguracji zmiennych środowiskowych w Dockerze." + }, + "logger": { + "label": "Rejestrowanie", + "description": "Konfiguracja domyślnej szczegółowości logów oraz indywidualnych ustawień dla komponentów.", + "default": { + "label": "Poziom szczegółowości logów", + "description": "Domyślny poziom szczegółowości logów globalnych (debug, info, warning, error)." + }, + "logs": { + "label": "Poziom logowania dla poszczególnych procesów", + "description": "Zmiana poziomu logowania dla poszczególnych komponentów w celu zwiększenia lub zmniejszenia szczegółowości logów dla określonych modułów." + } + }, + "auth": { + "label": "Uwierzytelnianie", + "description": "Ustawienia związane z uwierzytelnianiem i sesjami, w tym opcje dotyczące plików cookie i ograniczeń częstotliwości.", + "enabled": { + "label": "Włącz uwierzytelnianie", + "description": "Włącz natywne uwierzytelnianie dla interfejsu Frigate." + } + }, + "birdseye": { + "mode": { + "label": "Tryb śledzenia", + "description": "Tryb uwzględniania kamer w widoku z lotu ptaka: „obiekty”, „ruch” lub „ciągły”." + }, + "order": { + "label": "Pozycja", + "description": "Pozycja numeryczna określająca kolejność kamery w układzie widoku z lotu ptaka." + }, + "label": "Widok z lotu ptaka", + "description": "Ustawienia widoku z lotu ptaka „Birdseye”, który łączy obraz z wielu kamer w jeden układ.", + "enabled": { + "label": "Włącz widok z lotu ptaka", + "description": "Włącz lub wyłącz funkcję widoku z lotu ptaka." + } + }, + "detect": { + "enabled": { + "label": "Włącz wykrywanie obiektów" + }, + "stationary": { + "label": "Konfiguracja obiektów stacjonarnych", + "description": "Ustawienia wykrywania i obsługi obiektów statycznych przez określony czas.", + "interval": { + "label": "Okres bezruchu", + "description": "Częstotliwość (w klatkach) sprawdzania detekcji dla obiektów nieruchomych." + }, + "threshold": { + "label": "Próg bezruchu", + "description": "Liczba klatek bez zmiany pozycji potrzebna do uznania obiektu za nieruchomy." + }, + "max_frames": { + "label": "Maksymalna liczba klatek", + "description": "Ogranicza czas śledzenia nieruchomych obiektów przed ich usunięciem.", + "default": { + "label": "Domyślna maks. liczba klatek", + "description": "Domyślna maksymalna liczba klatek śledzenia nieruchomego obiektu przed zatrzymaniem." + }, + "objects": { + "label": "Maksymalna liczba klatek obiektu", + "description": "Nadpisania dla poszczególnych obiektów określające maksymalną liczbę klatek śledzenia nieruchomych obiektów." + } + }, + "classifier": { + "label": "Włącz klasyfikator wizualny", + "description": "Użyj klasyfikatora wizualnego do wykrywania rzeczywiście nieruchomych obiektów, nawet gdy ramki ograniczające drżą." + } + }, + "label": "Wykrywanie obiektów", + "description": "Ustawienia roli „detection/detect” używanej do wykrywania obiektów i inicjalizacji trackerów.", + "height": { + "label": "Wysokość wykrywania", + "description": "Wysokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia." + }, + "width": { + "label": "Szerokość wykrywania", + "description": "Szerokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia." + }, + "fps": { + "label": "FPS wykrywania", + "description": "Docelowa liczba klatek na sekundę używana do detekcji; niższe wartości zmniejszają użycie CPU (zalecana wartość to 5, ustaw wyższą — maksymalnie 10 — tylko jeśli śledzisz bardzo szybko poruszające się obiekty)." + }, + "min_initialized": { + "label": "Minimalna liczba klatek inicjalizacyjnych", + "description": "Liczba kolejnych trafień detekcji wymagana do utworzenia śledzonego obiektu. Zwiększenie wartości zmniejsza liczbę fałszywych inicjalizacji. Wartość domyślna to fps podzielone przez 2." + }, + "max_disappeared": { + "label": "Maksymalna liczba pominiętych klatek", + "description": "Liczba klatek bez detekcji, po której obiekt śledzony jest uznawany za utracony." + }, + "annotation_offset": { + "label": "Przesunięcie oznaczenia", + "description": "Liczba milisekund przesunięcia adnotacji detekcji w celu lepszego dopasowania ramek ograniczających na osi czasu do nagrań; wartość może być dodatnia lub ujemna." + } + }, + "face_recognition": { + "label": "Rozpoznawanie twarzy", + "enabled": { + "label": "Włącz rozpoznawanie twarzy" + }, + "min_area": { + "label": "Minimalny obszar twarzy", + "description": "Minimalny obszar twarzy (w pikselach) wymagany do podjęcia próby rozpoznania." + } + }, + "ffmpeg": { + "output_args": { + "detect": { + "description": "Domyślne argumenty wyjściowe dla strumieni z rolą detekcji." + }, + "record": { + "description": "Domyślne argumenty wyjściowe dla strumieni z rolą zapisu." + } + }, + "retry_interval": { + "label": "Czas ponownej próby FFmpeg", + "description": "Czas (w sekundach) przed ponownym uruchomieniem utraconego strumienia kamery. Domyślnie: 10." + }, + "apple_compatibility": { + "label": "Kompatybilność z Apple", + "description": "Włącz tagowanie HEVC, aby zapewnić lepszą zgodność z odtwarzaczami Apple podczas nagrywania w formacie H.265." + }, + "gpu": { + "label": "Indeks GPU", + "description": "Domyślny indeks karty graficznej używany do akceleracji sprzętowej, jeśli jest dostępna." + } + } +} diff --git a/web/public/locales/pl/config/groups.json b/web/public/locales/pl/config/groups.json new file mode 100644 index 0000000..1d6d94b --- /dev/null +++ b/web/public/locales/pl/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Ogólne Wykrywanie", + "sensitivity": "Ogólna Czułość" + }, + "cameras": { + "detection": "Wykrywanie", + "sensitivity": "Czułość" + } + }, + "timestamp_style": { + "global": { + "appearance": "Ogólny Wygląd" + }, + "cameras": { + "appearance": "Wygląd" + } + }, + "motion": { + "global": { + "sensitivity": "Ogólna Czułość", + "algorithm": "Ogólny Algorytm" + }, + "cameras": { + "sensitivity": "Czułość", + "algorithm": "Algorytm" + } + }, + "snapshots": { + "global": { + "display": "Ogólne Ustawienia Wyświetlania" + }, + "cameras": { + "display": "Wyświetlanie" + } + }, + "detect": { + "global": { + "resolution": "Ogólna Rozdzielczość", + "tracking": "Ogólne Śledzenie" + }, + "cameras": { + "resolution": "Rozdzielczość", + "tracking": "Śledzenie" + } + }, + "objects": { + "global": { + "tracking": "Ogólne Śledzenie", + "filtering": "Ogólne Filtrowanie" + }, + "cameras": { + "tracking": "Śledzenie", + "filtering": "Filtrowanie" + } + }, + "record": { + "global": { + "retention": "Ogólne Przechowywanie", + "events": "Ogólne Zdarzenia" + }, + "cameras": { + "retention": "Przechowywanie", + "events": "Zdarzenia" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Argumenty FFmpeg dotyczące konkretnej kamery" + } + } +} diff --git a/web/public/locales/pl/config/validation.json b/web/public/locales/pl/config/validation.json new file mode 100644 index 0000000..cded1fb --- /dev/null +++ b/web/public/locales/pl/config/validation.json @@ -0,0 +1,18 @@ +{ + "minimum": "Musi wynosić przynajmniej {{limit}}", + "maximum": "Może wynosić najwyżej {{limit}}", + "exclusiveMinimum": "Musi być większe niż {{limit}}", + "exclusiveMaximum": "Musi być mniejsze niż {{limit}}", + "minLength": "Musi być co najmniej {{limit}} znaków", + "maxLength": "Maksymalnie może być {{limit}} znaków", + "minItems": "Musi zawierać co najmniej {{limit}} pozycji", + "maxItems": "Maksymalnie {{limit}} pozycji", + "pattern": "Nieprawidłowy format", + "required": "To pole jest obowiązkowe", + "type": "Nieprawidłowy typ wartości", + "enum": "Musi to być jedna z dozwolonych wartości", + "const": "Wartość niezgodna z oczekiwaną stałą", + "uniqueItems": "Wszystkie pozycje muszą być unikalne", + "format": "Nieprawidłowy format", + "additionalProperties": "Nieznana właściwość jest niedozwolona" +} diff --git a/web/public/locales/pl/objects.json b/web/public/locales/pl/objects.json new file mode 100644 index 0000000..3923ec7 --- /dev/null +++ b/web/public/locales/pl/objects.json @@ -0,0 +1,120 @@ +{ + "bird": "Ptak", + "boat": "Łódź", + "car": "Samochód", + "bus": "Autobus", + "motorcycle": "Motocykl", + "train": "Pociąg", + "bicycle": "Rower", + "sheep": "Owca", + "mouse": "Mysz", + "keyboard": "Klawiatura", + "door": "Drzwi", + "blender": "Blender", + "sink": "Zlew", + "vehicle": "Pojazd", + "hair_dryer": "Suszarka do włosów", + "toothbrush": "Szczoteczka do zębów", + "scissors": "Nożyczki", + "goat": "Koza", + "skateboard": "Deskorolka", + "dog": "Pies", + "cat": "Kot", + "horse": "Koń", + "clock": "Zegar", + "animal": "Zwierzę", + "bark": "Szczekanie", + "person": "Osoba", + "airplane": "Samolot", + "traffic_light": "Światła Uliczne", + "fire_hydrant": "Hydrant", + "street_sign": "Znak Drogowy", + "stop_sign": "Znak Stopu", + "parking_meter": "Parkometr", + "bench": "Ławka", + "cow": "Krowa", + "bear": "Niedźwiedź", + "giraffe": "Żyrafa", + "backpack": "Plecak", + "umbrella": "Parasolka", + "shoe": "But", + "eye_glasses": "Okulary Przeciwsłoneczne", + "tie": "Krawat", + "skis": "Narty", + "tennis_racket": "Rakieta Tenisowa", + "bottle": "Butelka", + "plate": "Tależ", + "wine_glass": "Kieliszek do Wina", + "cup": "Kubek", + "fork": "Widelec", + "banana": "Banan", + "apple": "Jabłko", + "carrot": "Marchewka", + "hot_dog": "Hot Dog", + "pizza": "Pizza", + "cake": "Ciastko", + "chair": "Krzesło", + "bed": "Łóżko", + "mirror": "Lustro", + "dining_table": "Stół Jadalny", + "window": "Okno", + "toilet": "Toaleta", + "tv": "Telewizor", + "laptop": "Laptop", + "remote": "Pilot", + "toaster": "Toster", + "refrigerator": "Lodówka", + "book": "Książka", + "vase": "Waza", + "hair_brush": "Szczotka do Włosów", + "squirrel": "Wiewiórka", + "fox": "Lis", + "waste_bin": "Kosz na Śmieci", + "face": "Twarz", + "license_plate": "Tablica Rejestracyjna", + "package": "Paczka", + "bbq_grill": "Grill", + "amazon": "Amazon", + "dhl": "DHL", + "gls": "GLS", + "dpd": "DPD", + "baseball_glove": "Rękawica Bejsbolowa", + "baseball_bat": "Kij Bejsbolowy", + "bowl": "Miska", + "spoon": "Łyżka", + "sandwich": "Kanapka", + "zebra": "Zebra", + "snowboard": "Snowboard", + "knife": "Nóż", + "broccoli": "Brokuł", + "elephant": "Śłoń", + "desk": "Biurko", + "orange": "Pomarańcza", + "cell_phone": "Telefon Komórkowy", + "microwave": "Mikrofalówka", + "oven": "Piekarnik", + "hat": "Kapelusz", + "handbag": "Torebka", + "suitcase": "Walizka", + "sports_ball": "Piłka sportowa", + "kite": "Latawiec", + "surfboard": "Deska surfingowa", + "donut": "Pączek", + "couch": "Kanapa", + "potted_plant": "Roślina doniczkowa", + "teddy_bear": "Miś pluszowy", + "deer": "Jeleń", + "rabbit": "Królik", + "raccoon": "Szop pracz", + "robot_lawnmower": "Robot koszący", + "usps": "USPS (Poczta Amerykańska)", + "on_demand": "Na żądanie", + "ups": "UPS", + "fedex": "FedEx", + "an_post": "An Post (Poczta Irlandzka)", + "purolator": "Purolator", + "postnl": "PostNL (Poczta Holenderska)", + "nzpost": "NZPost (Poczta Nowozelandzka)", + "postnord": "PostNord (Poczta Skandynawska)", + "frisbee": "Frisbee" +} diff --git a/web/public/locales/pl/views/chat.json b/web/public/locales/pl/views/chat.json new file mode 100644 index 0000000..f16ccc2 --- /dev/null +++ b/web/public/locales/pl/views/chat.json @@ -0,0 +1,4 @@ +{ + "documentTitle": "Czat - Frigate", + "title": "Frigate Czat" +} diff --git a/web/public/locales/pl/views/classificationModel.json b/web/public/locales/pl/views/classificationModel.json new file mode 100644 index 0000000..b5a0488 --- /dev/null +++ b/web/public/locales/pl/views/classificationModel.json @@ -0,0 +1,206 @@ +{ + "documentTitle": "Modele klasyfikacji - Frigate", + "button": { + "deleteClassificationAttempts": "Usuń obrazy klasyfikacyjne", + "renameCategory": "Zmień nazwę klasy", + "deleteCategory": "Usuń klasyfikację", + "deleteImages": "Usuń obrazy", + "trainModel": "Trenuj model", + "addClassification": "Dodaj klasyfikację", + "deleteModels": "Usuń modele", + "editModel": "Edytuj model" + }, + "details": { + "scoreInfo": "Wynik przedstawia średnią pewność klasyfikacji wszystkich wykryć danego obiektu.", + "none": "Brak", + "unknown": "Nieznany" + }, + "toast": { + "success": { + "deletedCategory_one": "Usunięto {{count}} klasę", + "deletedCategory_few": "Usunięto {{count}} klasy", + "deletedCategory_many": "Usunięto {{count}} klas", + "deletedImage_one": "Usunięto {{count}} obraz", + "deletedImage_few": "Usunięto {{count}} obrazy", + "deletedImage_many": "Usunięto {{count}} obrazów", + "deletedModel_one": "Pomyślenie usunięto {{count}} model", + "deletedModel_few": "Pomyślenie usunięto {{count}} modele", + "deletedModel_many": "Pomyślenie usunięto {{count}} modeli", + "categorizedImage": "Obraz pomyślnie sklasyfikowany", + "trainedModel": "Model pomyślnie wytrenowany.", + "trainingModel": "Pomyślnie uruchomiono trenowanie modelu.", + "updatedModel": "Pomyślnie zaktualizowane ustawienia modelu", + "renamedCategory": "Pomyślnie zmieniono nazwę klasy na {{name}}", + "reclassifiedImage": "Pomyślne przeklasyfikowanie obrazu" + }, + "error": { + "deleteImageFailed": "Nie udało się usunąć: {{errorMessage}}", + "deleteCategoryFailed": "Nie udało się usunąć klasy: {{errorMessage}}", + "deleteModelFailed": "Nie udało się usunąć modelu: {{errorMessage}}", + "categorizeFailed": "Nie udało się skategoryzować obrazka: {{errorMessage}}", + "trainingFailed": "Trening modelu zakończył się niepowodzeniem. Sprawdź logi Frigate aby uzyskać więcej informacji.", + "updateModelFailed": "Nie udało się zaktualizować modelu: {{errorMessage}}", + "trainingFailedToStart": "Nie udało się rozpocząć trenowania modelu: {{errorMessage}}", + "renameCategoryFailed": "Nie udało się zmienić nazwy klasy: {{errorMessage}}", + "reclassifyFailed": "Niepowodzenie zmiany klasyfikacji obrazu: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Usuń klasę", + "desc": "Czy na pewno chcesz usunąć klasę {{name}}? Spowoduje to trawałe usunięcie wszystkich powiązanych obrazków i konieczność ponownego trenowania modelu.", + "minClassesTitle": "Nie można usunąć kategorii", + "minClassesDesc": "Model klasyfikacyjny musi posiadać co najmniej dwie kategorie. Dodaj inną kategorię aby możliwe było usunięcie tej kategorii." + }, + "deleteModel": { + "title": "Usuń model klasyfikacji", + "single": "Czy na pewno chcesz usunąć {{name}}? Spowoduje to trwałe usunięcie wszystkich powiązanych data włącznie z obrazkami i danymi treningowymi. Nie można cofnąć tej operacji.", + "desc_one": "Czy na pewno chcesz usunąć {{count}} model? Spowoduje to trwałe usunięcie wszystkich powiązanych danych, włącznie z obrazami i danymi treningowymi. Nie można cofnąć tej operacji.", + "desc_few": "Czy na pewno chcesz usunąć {{count}} modele? Spowoduje to trwałe usunięcie wszystkich powiązanych danych, włącznie z obrazami i danymi treningowymi. Nie można cofnąć tej operacji.", + "desc_many": "Czy na pewno chcesz usunąć {{count}} modeli? Spowoduje to trwałe usunięcie wszystkich powiązanych danych, włącznie z obrazami i danymi treningowymi. Nie można cofnąć tej operacji." + }, + "edit": { + "title": "Edytuj model klasyfikacji", + "descriptionObject": "Zmień typ obiektu i kryteria dla tego modelu klasyfikacji.", + "stateClassesInfo": "Uwaga: Zmiana typu klasyfikacji wymaga treningu nowego modelu.", + "descriptionState": "Edycja klas dla tego modelu klasyfikacji stanu. Zmiany będą wymagały przekwalifikowania modelu." + }, + "tooltip": { + "trainingInProgress": "Trwa trenowanie modelu", + "modelNotReady": "Mode nie jest gotowy do trenowania", + "noChanges": "Brak zmian w zbiorze danych od czasu ostatniego treningu.", + "noNewImages": "Nie ma więcej obrazów do trenowania. Zaklasyfikuj więcej obrazów do zbioru danych." + }, + "deleteDatasetImages": { + "title": "Usuń obrazy z puli danych", + "desc_one": "Czy na pewno chcesz usunąć {{count}} obraz z {{dataset}}? Ta akcja nie może zostać cofnięta i będzie wymagała przetrenowania modelu.", + "desc_few": "Czy na pewno chcesz usunąć obrazy {{count}} z {{dataset}}? Ta akcja nie może zostać cofnięta i będzie wymagała przetrenowania modelu.", + "desc_many": "Czy na pewno chcesz usunąć obrazy {{count}} z {{dataset}}? Ta akcja nie może zostać cofnięta i będzie wymagała przetrenowania modelu." + }, + "renameCategory": { + "title": "Zmień nazwę klasy", + "desc": "Wprowadź nową nazwę dla {{name}}. Zastosowanie tej zmiany wymagać będzie treningu nowego modelu." + }, + "description": { + "invalidName": "Niepoprawna nazwa. Nazwy mogą zawierać tylko: litery, cyfry, spacje, cudzysłowy, podkreślniniki i myślniki." + }, + "train": { + "title": "Ostatnie Klasyfikacje", + "titleShort": "Ostatnie", + "aria": "Wybierz Najnowsze Klasyfikacje" + }, + "createCategory": { + "new": "Stwórz nową klasyfikację" + }, + "deleteTrainImages": { + "title": "Usuń obrazy pociągów", + "desc_one": "Czy na pewno chcesz usunąć {{count}} obraz? Tego działania nie da się cofnąć.", + "desc_few": "Czy na pewno chcesz usunąć obrazy {{count}}? Tego działania nie da się cofnąć.", + "desc_many": "Czy na pewno chcesz usunąć obrazy {{count}}? Tego działania nie da się cofnąć." + }, + "categories": "Zajęcia", + "none": "Nic", + "categorizeImageAs": "Klasyfikuj Obraz Jako:", + "categorizeImage": "Klasyfikuj obraz", + "menu": { + "objects": "Obiekty", + "states": "Stany" + }, + "noModels": { + "object": { + "title": "Brak modeli klasyfikacji obiektów", + "description": "Utwórz model niestandardowy do klasyfikacji wykrytych obiektów.", + "buttonText": "Tworzenie modelu obiektu" + }, + "state": { + "title": "Brak państwowych modeli klasyfikacji", + "description": "Utwórz niestandardowy model do monitorowania i klasyfikacji zmian stanu w określonych obszarach kamery.", + "buttonText": "Utwórz model stanu" + } + }, + "wizard": { + "title": "Tworzenie nowej klasyfikacji", + "steps": { + "nameAndDefine": "Nazwij i zdefiniuj", + "stateArea": "Obszar stanu", + "chooseExamples": "Wybierz przykłady" + }, + "step1": { + "name": "Nazwa", + "type": "Typ", + "classes": "Klasy", + "errors": { + "noneNotAllowed": "Klasa 'żadne' jest niedozwolona", + "stateRequiresTwoClasses": "Modele stanowe wymagają co najmniej dwie klasy", + "objectLabelRequired": "Proszę wybrać etykietę obiektu", + "objectTypeRequired": "Proszę wybrać typ klasyfikacji", + "nameRequired": "Nazwa modelu jest wymagana", + "nameLength": "Nazwa modelu może mieć 64 znaki lub mniej", + "nameOnlyNumbers": "Nazwa modelu nie może być ciągiem cyfr", + "classRequired": "Przynajmniej jedna klasa jest wymagana", + "classesUnique": "Nazwa klasy musi być unikalna" + }, + "classPlaceholder": "Wpisz nazwę klasy...", + "classesObjectDesc": "Zdefiniuj różne kategorie, do których będą klasyfikowane wykryte obiekty. Na przykład: 'dostawca', 'mieszkaniec', 'nieznajomy' w przypadku klasyfikacji osób.", + "description": "Modele stanowe monitorują stałe obszary kamer pod kątem zmian (np. otwarte/zamknięte drzwi). Modele obiektów dodają klasyfikacje do wykrytych obiektów (np. znane zwierzęta, dostawcy itp.).", + "namePlaceholder": "Wprowadź nazwę modelu...", + "typeState": "Stan", + "typeObject": "Obiekt", + "objectLabel": "Etykieta obiektu", + "objectLabelPlaceholder": "Wybierz typ obiektu...", + "classificationType": "Rodzaj klasyfikacji", + "classificationTypeTip": "Dowiedz się więcej o typach klasyfikacji", + "classificationTypeDesc": "Podetykiety dodają dodatkowy tekst do etykiety obiektu (np. 'Osoba: UPS'). Atrybuty to metadane, które można przeszukiwać, przechowywane oddzielnie w metadanych obiektu.", + "classificationSubLabel": "Podetykieta", + "classificationAttribute": "Atrybut", + "states": "Stany", + "classesTip": "Dowiedz się więcej o klasach obiektów", + "classesStateDesc": "Zdefiniuj różne stany, w jakich może znajdować się obszar objęty zasięgiem kamery. Na przykład: 'otwarte' i 'zamknięte' dla bramy garażowej." + }, + "step2": { + "description": "Wybierz kamery i określ obszar monitorowania dla każdej z nich. Model sklasyfikuje stan tych obszarów.", + "cameras": "Kamery", + "selectCamera": "Wybierz kamerę", + "noCameras": "Kliknij +, aby dodać kamery", + "selectCameraPrompt": "Wybierz kamerę z listy, aby zdefiniować jej obszar monitorowania" + }, + "step3": { + "selectImagesPrompt": "Zaznacz wszystkie obrazy z: {{className}}", + "selectImagesDescription": "Kliknij na zdjęcia, aby je wybrać. Po zakończeniu zajęć kliknij „Kontynuuj”.", + "allImagesRequired_one": "Proszę sklasyfikować wszystkie obrazy. Pozostał {{count}} obraz.", + "allImagesRequired_few": "Proszę sklasyfikować wszystkie obrazy. Pozostały {{count}} obrazy.", + "allImagesRequired_many": "Proszę sklasyfikować wszystkie obrazy. Pozostało {{count}} obrazów.", + "generating": { + "title": "Generowanie przykładowych obrazów", + "description": "Frigate pobiera reprezentatywne obrazy z Twoich nagrań. Może to chwilę potrwać..." + }, + "trainingStarted": "Szkolenie rozpoczęło się pomyślnie", + "training": { + "title": "Model treningowy", + "description": "Twój model jest szkolony w tle. Zamknij to okno dialogowe, a model zacznie działać zaraz po zakończeniu szkolenia." + }, + "retryGenerate": "Ponowne generowanie", + "noImages": "Nie wygenerowano przykładowych obrazów", + "classifying": "Klasyfikacja i szkolenie...", + "modelCreated": "Model został pomyślnie utworzony. Użyj widoku Ostatnie klasyfikacje, aby dodać obrazy dla brakujących stanów, a następnie wytrenuj model.", + "errors": { + "noCameras": "Brak skonfigurowanych kamer", + "noObjectLabel": "Nie wybrano żadnej etykiety obiektu", + "generateFailed": "Nie udało się wygenerować przykładów: {{error}}", + "generationFailed": "Generowanie nie powiodło się. Spróbuj ponownie.", + "classifyFailed": "Nie udało się sklasyfikować obrazów: {{error}}" + }, + "generateSuccess": "Pomyślnie wygenerowane przykładowe obrazy", + "missingStatesWarning": { + "title": "Przykłady brakujących stanów", + "description": "Aby uzyskać najlepsze wyniki, zaleca się wybranie przykładów dla wszystkich stanów. Można kontynuować bez wybierania wszystkich stanów, ale model nie zostanie wytrenowany, dopóki wszystkie stany nie będą miały obrazów. Po kontynuowaniu należy użyć widoku Ostatnie klasyfikacje, aby sklasyfikować obrazy dla brakujących stanów, a następnie wytrenować model." + }, + "refreshExamples": "Generuj nowe przykłady", + "refreshConfirm": { + "title": "Czy generować nowe przykłady?", + "description": "Spowoduje to wygenerowanie nowego zestawu obrazów i wyczyszczenie wszystkich zaznaczeń, w tym dotychczasowych klas. Konieczne będzie ponowne wybranie przykładów dla wszystkich klas." + } + } + }, + "reclassifyImageAs": "Klasyfikuj jako:", + "reclassifyImage": "Zmiana klasyfikacji obrazu" +} diff --git a/web/public/locales/pl/views/configEditor.json b/web/public/locales/pl/views/configEditor.json new file mode 100644 index 0000000..a8c3740 --- /dev/null +++ b/web/public/locales/pl/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Edytor konfiguracji - Frigate", + "configEditor": "Edytor konfiguracji", + "copyConfig": "Skopiuj konfigurację", + "toast": { + "success": { + "copyToClipboard": "Konfiguracja skopiowana do schowka." + }, + "error": { + "savingError": "Błąd podczas zapisywanie konfiguracji" + } + }, + "saveOnly": "Tylko zapisz", + "saveAndRestart": "Zapisz i uruchom ponownie", + "confirm": "Zamknąć bez zapisywania?", + "safeConfigEditor": "Edytor Konfiguracji (tryb bezpieczny)", + "safeModeDescription": "Frigate jest w trybie bezpiecznym przez błąd walidacji konfiguracji." +} diff --git a/web/public/locales/pl/views/events.json b/web/public/locales/pl/views/events.json new file mode 100644 index 0000000..1e85b72 --- /dev/null +++ b/web/public/locales/pl/views/events.json @@ -0,0 +1,69 @@ +{ + "camera": "Kamera", + "alerts": "Alerty", + "detections": "Wykrycia", + "motion": { + "label": "Ruch", + "only": "Tylko ruch" + }, + "allCameras": "Wszystkie kamery", + "empty": { + "alert": "Brak alertów do przejrzenia", + "detection": "Brak detekcji do przejrzenia", + "motion": "Nie znaleziono danych o ruchu", + "recordingsDisabled": { + "title": "Nagrywanie musi być włączone", + "description": "Elementy przeglądu można tworzyć dla kamery tylko wtedy, gdy dla tej kamery włączono nagrywanie." + } + }, + "timeline": { + "label": "Oś czasu" + }, + "timeline.aria": "Wybierz oś czasu", + "events": { + "label": "Zdarzenia", + "aria": "Wybierz zdarzenia", + "noFoundForTimePeriod": "Brak zdarzeń w tym okresie czasu." + }, + "documentTitle": "Przegląd - Frigate", + "recordings": { + "documentTitle": "Nagrania - Frigate" + }, + "markAsReviewed": "Oznacz jako przejrzane", + "markTheseItemsAsReviewed": "Oznacz te elementy jako przejrzane", + "calendarFilter": { + "last24Hours": "Ostatnie 24 godziny" + }, + "newReviewItems": { + "label": "Zobacz nowe elementy do przeglądu", + "button": "Nowe elementy do przeglądu" + }, + "selected_one": "{{count}} wybrane", + "selected_other": "{{count}} wybrane", + "detected": "wykryto", + "suspiciousActivity": "Podejrzana aktywność", + "threateningActivity": "Niebezpieczne działania", + "zoomIn": "Przybliż", + "zoomOut": "Oddal", + "detail": { + "label": "Szczegóły", + "noDataFound": "Brak szczegółów do przejrzenia", + "aria": "Przełącz widok szczegółów", + "trackedObject_one": "{{count}} obiekt", + "trackedObject_other": "{{count}} obiekty", + "noObjectDetailData": "Brak danych szczegółowych dla obiektu.", + "settings": "Ustawienia widoku szczegółów", + "alwaysExpandActive": { + "title": "Zawsze rozwiń aktywne", + "desc": "Zawsze rozwijaj szczegóły aktywnego obiektu, jeżeli są dostępne." + } + }, + "objectTrack": { + "trackedPoint": "Śledzony punkt", + "clickToSeek": "Kliknij aby przewinąć do tego miejsca" + }, + "needsReview": "Wymaga manualnego sprawdzenia", + "normalActivity": "Normalne", + "select_all": "Wszystko", + "securityConcern": "Kwestie bezpieczeństwa" +} diff --git a/web/public/locales/pl/views/explore.json b/web/public/locales/pl/views/explore.json new file mode 100644 index 0000000..3dfe9f5 --- /dev/null +++ b/web/public/locales/pl/views/explore.json @@ -0,0 +1,319 @@ +{ + "generativeAI": "Generatywna SI", + "documentTitle": "Eksploruj - Frigate", + "details": { + "timestamp": "Znacznik czasu", + "item": { + "desc": "Szczegóły elementu do przeglądu", + "title": "Szczegóły Elementu do Przeglądu", + "button": { + "share": "Udostępnij ten element", + "viewInExplore": "Zobacz w Eksploracji" + }, + "tips": { + "hasMissingObjects": "Dostosuj swoją konfigurację, jeśli chcesz, aby Frigate zapisywał śledzone obiekty dla następujących etykiet: {{objects}}", + "mismatch_one": "{{count}} niedostępny obiekt został wykryty i uwzględniony w tym elemencie przeglądu. Ten obiekt albo nie kwalifikował się jako alert lub detekcja, albo został już wyczyszczont/usunięty.", + "mismatch_few": "{{count}} niedostępne obiekty zostały wykryte i uwzględnione w tym elemencie przeglądu. Te obiekty albo nie kwalifikowały się jako alert lub detekcja, albo zostały już wyczyszczone/usunięte.", + "mismatch_many": "{{count}} niedostępnych obiektów zostało wykrytych i uwzględnionych w tym elemencie przeglądu. Te obiekty albo nie kwalifikowały się jako alert lub detekcja, albo zostały już wyczyszczone/usunięte." + }, + "toast": { + "success": { + "regenerate": "Zażądano nowego opisu od {{provider}}. W zależności od szybkości twojego dostawcy, wygenerowanie nowego opisu może zająć trochę czasu.", + "updatedSublabel": "Pomyślnie zaktualizowano podetykietę.", + "updatedLPR": "Pomyślnie zaktualizowano tablicę rejestracyjną.", + "audioTranscription": "Wysłano prośbę o audio transkrypcję. W zależności od szybkości serwera Frigate, transkrypcja może potrwać trochę czasu.", + "updatedAttributes": "Atrybuty zostały pomyślnie zaktualizowane." + }, + "error": { + "regenerate": "Nie udało się wezwać {{provider}} dla nowego opisu: {{errorMessage}}", + "updatedSublabelFailed": "Nie udało się zaktualizować podetykiety: {{errorMessage}}", + "updatedLPRFailed": "Nie udało się zaktualizować tablicy rejestracyjnej: {{errorMessage}}", + "audioTranscription": "Nie udało się włączyć audio transkrypcji: {{errorMessage}}", + "updatedAttributesFailed": "Nie udało się zaktualizować atrybutów: {{errorMessage}}" + } + } + }, + "topScore": { + "info": "Najwyższy wynik to najwyższa mediana wyniku dla śledzonego obiektu, więc może się różnić od wyniku pokazanego na miniaturze wyników wyszukiwania.", + "label": "Najwyższy wynik" + }, + "editSubLabel": { + "descNoLabel": "Wprowadź nową podetykietę dla tego śledzonego obiektu", + "title": "Edytuj podetykietę", + "desc": "Wprowadź nową podetykietę dla tego {{label}}" + }, + "estimatedSpeed": "Szacowana prędkość", + "label": "Etykieta", + "button": { + "regenerate": { + "title": "Regeneruj", + "label": "Regeneruj opis śledzonego obiektu" + }, + "findSimilar": "Znajdź Podobne" + }, + "objects": "Obiekty", + "camera": "Kamera", + "zones": "Strefy", + "expandRegenerationMenu": "Rozwiń menu regeneracji", + "description": { + "label": "Opis", + "placeholder": "Opis śledzonego obiektu", + "aiTips": "Frigate nie poprosi o opis od twojego dostawcy AI, dopóki cykl życia śledzonego obiektu nie dobiegnie końca." + }, + "editLPR": { + "title": "Edytuj tablicę rejestracyjną", + "desc": "Wprowadź nową wartość tablicy rejestracyjnej dla tego {{label}}", + "descNoLabel": "Wprowadź nową wartość tablicy rejestracyjnej dla tego śledzonego obiektu" + }, + "tips": { + "descriptionSaved": "Pomyślnie zapisano opis", + "saveDescriptionFailed": "Nie udało się zaktualizować opisu: {{errorMessage}}" + }, + "recognizedLicensePlate": "Rozpoznana tablica rejestracyjna", + "regenerateFromSnapshot": "Regeneruj ze zrzutu ekranu", + "regenerateFromThumbnails": "Regeneruj z miniatur", + "snapshotScore": { + "label": "Wynik zrzutu" + }, + "score": { + "label": "Wynik" + }, + "editAttributes": { + "title": "Edytuj atrybuty", + "desc": "Wybierz atrybuty klasyfikacji dla tego {{label}}" + }, + "attributes": "Atrybuty klasyfikacji", + "title": { + "label": "Tytuł" + }, + "scoreInfo": "Informacje o wyniku" + }, + "objectLifecycle": { + "annotationSettings": { + "title": "Ustawienia adnotacji", + "showAllZones": { + "title": "Pokaż wszystkie strefy", + "desc": "Zawsze pokazuj strefy na klatkach, w których obiekty weszły do strefy." + }, + "offset": { + "desc": "Te dane pochodzą z kanału detekcji kamery, ale są nakładane na obrazy z kanału nagrywania. Mało prawdopodobne, aby oba strumienie były idealnie zsynchronizowane. W rezultacie ramka ograniczająca i nagranie mogą nie być idealnie dopasowane. Jednak pole annotation_offset może być użyte do regulacji tego.", + "documentation": "Przeczytaj dokumentację ", + "label": "Przesunięcie adnotacji", + "millisecondsToOffset": "Milisekundy do przesunięcia adnotacji detekcji. Domyślnie: 0", + "tips": "WSKAZÓWKA: Wyobraź sobie, że istnieje klip zdarzenia z osobą idącą od lewej do prawej. Jeśli na osi czasu zdarzenia ramka ograniczająca jest konsekwentnie na lewo od osoby, wartość powinna być zmniejszona. Podobnie, jeśli osoba idzie od lewej do prawej, a ramka ograniczająca jest konsekwentnie przed osobą, wartość powinna być zwiększona.", + "toast": { + "success": "Przesunięcie adnotacji dla {{camera}} zostało zapisane w pliku konfiguracyjnym. Zrestartuj Frigate, aby zastosować zmiany." + } + } + }, + "title": "Cykl życia obiektu", + "noImageFound": "Nie znaleziono obrazu dla tego znacznika czasu.", + "scrollViewTips": "Przewiń, aby zobaczyć kluczowe momenty cyklu życia tego obiektu.", + "autoTrackingTips": "Pozycje ramek ograniczających mogą być niedokładne dla kamer z automatycznym śledzeniem.", + "lifecycleItemDesc": { + "visible": "{{label}} wykryty", + "entered_zone": "{{label}} wszedł w strefę {{zones}}", + "active": "{{label}} stał się aktywny", + "stationary": "{{label}} stał się nieruchomy", + "attribute": { + "faceOrLicense_plate": "{{attribute}} wykryty dla {{label}}", + "other": "{{label}} rozpoznany jako {{attribute}}" + }, + "gone": "{{label}} zniknął", + "heard": "{{label}} usłyszany", + "external": "{{label}} wykryty", + "header": { + "ratio": "Współczynnik", + "area": "Obszar", + "zones": "Strefy" + } + }, + "carousel": { + "previous": "Poprzedni slajd", + "next": "Następny slajd" + }, + "createObjectMask": "Utwórz maskę obiektu", + "adjustAnnotationSettings": "Dostosuj ustawienia adnotacji", + "trackedPoint": "Punkt Śledzenia", + "count": "{{first}} z {{second}}" + }, + "exploreIsUnavailable": { + "title": "Eksploracja jest niedostępna", + "embeddingsReindexing": { + "context": "Eksploracja będzie dostępna po zakończeniu ponownego indeksowania osadzenia śledzonych obiektów.", + "startingUp": "Uruchamianie…", + "estimatedTime": "Szacowany pozostały czas:", + "finishingShortly": "Zaraz się zakończy", + "step": { + "thumbnailsEmbedded": "Osadzone miniatury: ", + "descriptionsEmbedded": "Osadzone opisy: ", + "trackedObjectsProcessed": "Przetworzone śledzone obiekty: " + } + }, + "downloadingModels": { + "context": "Frigate pobiera niezbędne modele osadzenia do obsługi funkcji wyszukiwania semantycznego. Może to potrwać kilka minut, w zależności od prędkości Twojego połączenia sieciowego.", + "setup": { + "visionModelFeatureExtractor": "Ekstraktor cech modelu wizyjnego", + "textModel": "Model tekstowy", + "visionModel": "Model wizyjny", + "textTokenizer": "Tokenizer tekstu" + }, + "tips": { + "context": "Po pobraniu modeli warto ponownie zindeksować osadzenia śledzonych obiektów.", + "documentation": "Przeczytaj dokumentację" + }, + "error": "Wystąpił błąd. Sprawdź logi Frigate." + } + }, + "trackedObjectDetails": "Szczegóły śledzonego obiektu", + "type": { + "details": "szczegóły", + "snapshot": "zrzut ekranu", + "video": "wideo", + "object_lifecycle": "cykl życia obiektu", + "thumbnail": "miniaturka", + "tracking_details": "szczegóły śledzenia" + }, + "itemMenu": { + "downloadSnapshot": { + "aria": "Pobierz zrzut ekranu", + "label": "Pobierz zrzut ekranu" + }, + "viewObjectLifecycle": { + "label": "Wyświetl cykl życia obiektu", + "aria": "Pokaż cykl życia obiektu" + }, + "downloadVideo": { + "label": "Pobierz wideo", + "aria": "Pobierz wideo" + }, + "findSimilar": { + "label": "Znajdź podobne", + "aria": "Znajdź podobne śledzone obiekty" + }, + "submitToPlus": { + "label": "Prześlij do Frigate+", + "aria": "Prześlij do Frigate Plus" + }, + "viewInHistory": { + "label": "Wyświetl w Historii", + "aria": "Wyświetl w Historii" + }, + "deleteTrackedObject": { + "label": "Usuń ten śledzony obiekt" + }, + "addTrigger": { + "label": "Dodaj wyzwalacz", + "aria": "Dodaj wyzwalacz dla tego śledzonego obiektu" + }, + "audioTranscription": { + "label": "Rozpisz", + "aria": "Poproś o audiotranskrypcję" + }, + "downloadCleanSnapshot": { + "label": "Pobierz czysty snapshot", + "aria": "Pobierz czysty snapshot" + }, + "viewTrackingDetails": { + "label": "Wyświetl szczegóły śledzenia", + "aria": "Pokaż szczegóły śledzenia" + }, + "showObjectDetails": { + "label": "Pokaż ścieżkę obiektu" + }, + "hideObjectDetails": { + "label": "Ukryj ścieżkę obiektu" + }, + "debugReplay": { + "label": "Odtwarzanie debugowania", + "aria": "Wyświetl śledzony obiekt w widoku odtwarzania debugowania" + }, + "more": { + "aria": "Więcej" + } + }, + "trackedObjectsCount_one": "{{count}} śledzony obiekt ", + "trackedObjectsCount_few": "{{count}} śledzone obiekty ", + "trackedObjectsCount_many": "{{count}} śledzonych obiektów ", + "noTrackedObjects": "Nie znaleziono śledzonych obiektów", + "dialog": { + "confirmDelete": { + "desc": "Usunięcie tego śledzonego obiektu usuwa zrzut ekranu, wszelkie zapisane osadzenia i wszystkie powiązane wpisy śledzenia obiektu. Nagrany materiał tego śledzonego obiektu w widoku Historii NIE zostanie usunięty.

    Czy na pewno chcesz kontynuować?", + "title": "Potwierdź usunięcie" + }, + "toast": { + "error": "Błąd kasowania śledzonego obiektu: {{errorMessage}}" + } + }, + "fetchingTrackedObjectsFailed": "Błąd pobierania śledzonych obiektów: {{errorMessage}}", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Śledzony obiekt usunięty pomyślnie.", + "error": "Nie udało się usunąć śledzonego obiektu: {{errorMessage}}" + } + }, + "tooltip": "Pasuje do {{type}} z pewnością {{confidence}}%", + "previousTrackedObject": "Poprzednio śledzony obiekt", + "nextTrackedObject": "Następny śledzony obiekt" + }, + "exploreMore": "Odkryj więcej obiektów typu {{label}}", + "aiAnalysis": { + "title": "Analiza SI" + }, + "concerns": { + "label": "Obawy" + }, + "trackingDetails": { + "title": "Szczegóły śledzenia", + "noImageFound": "Nie znaleziono obrazka dla podanego czasu.", + "createObjectMask": "Utwórz maskę obiektu", + "adjustAnnotationSettings": "Dostosuj ustawienia adnotacji", + "scrollViewTips": "Kliknij, aby zobaczyć najważniejsze momenty cyklu życia tego obiektu.", + "count": "{{first}} z {{second}}", + "autoTrackingTips": "Pozycja znacznika obiektu jest niedokładna dla kamer z automatycznym śledzeniem.", + "lifecycleItemDesc": { + "visible": "Wykryto {{label}}", + "entered_zone": "{{label}} pojawił się w {{zones}}", + "active": "{{label}} poruszył się", + "stationary": "{{label}} zatrzymał się", + "attribute": { + "faceOrLicense_plate": "Wykryto {{attribute}} dla obiektu {{label}}", + "other": "{{label}} został rozpoznany jako {{attribute}}" + }, + "gone": "Utracono śledzenie dla {{label}}", + "external": "Wykryto {{label}}", + "header": { + "zones": "Strefy", + "area": "Powierzchnia", + "score": "Wynik", + "ratio": "Proporcje", + "toggleAdvancedScores": "Przełącz widok wyników zaawansowanych", + "computedScore": "Obliczony wynik", + "topScore": "Najwyższy wynik" + }, + "heard": "{{label}} słyszałem" + }, + "annotationSettings": { + "title": "Ustawienia adnotacji", + "showAllZones": { + "title": "Pokaż wszystkie strefy", + "desc": "Pokazuj linie stref w momencie wejścia obiektu w strefę." + }, + "offset": { + "label": "Przesunięcie adnotacji", + "desc": "Dane te pochodzą z kanału wykrywania kamery, ale są nakładane na obrazy z kanału nagrywania. Jest mało prawdopodobne, aby oba strumienie były idealnie zsynchronizowane. W rezultacie ramka ograniczająca i materiał filmowy nie będą idealnie dopasowane. Można użyć tego ustawienia, aby przesunąć adnotacje do przodu lub do tyłu w czasie, aby lepiej dopasować je do nagranego materiału filmowego.", + "millisecondsToOffset": "Milisekundy, po których wykrywane są adnotacje. Domyślnie: 0", + "tips": "Zmniejsz wartość, jeśli odtwarzanie wideo wyprzedza pola i punkty ścieżki, i zwiększ wartość, jeśli odtwarzanie wideo pozostaje w tyle. Wartość ta może być ujemna.", + "toast": { + "success": "Przesunięcie adnotacji dla {{camera}} zostało zapisane w pliku konfiguracyjnym." + } + } + }, + "trackedPoint": "Śledzony Punkt", + "carousel": { + "previous": "Poprzedni slajd", + "next": "Następny slajd" + } + } +} diff --git a/web/public/locales/pl/views/exports.json b/web/public/locales/pl/views/exports.json new file mode 100644 index 0000000..82e6857 --- /dev/null +++ b/web/public/locales/pl/views/exports.json @@ -0,0 +1,30 @@ +{ + "search": "Szukaj", + "documentTitle": "Eksportuj - Frigate", + "noExports": "Nie znaleziono eksportów", + "deleteExport": { + "label": "Usuń eksport" + }, + "deleteExport.desc": "Czy na pewno chcesz usunąć {{exportName}}?", + "editExport": { + "title": "Zmień nazwę eksportu", + "desc": "Wprowadź nową nazwę dla tego eksportu.", + "saveExport": "Zapisz eksport" + }, + "toast": { + "error": { + "renameExportFailed": "Nie udało się zmienić nazwy eksportu: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Udostępnij eksport", + "downloadVideo": "Pobierz wideo", + "editName": "Edytuj nazwę", + "deleteExport": "Usuń eksport", + "assignToCase": "Dodaj do sprawy" + }, + "headings": { + "cases": "Przypadki", + "uncategorizedExports": "Bez kategorii Eksport" + } +} diff --git a/web/public/locales/pl/views/faceLibrary.json b/web/public/locales/pl/views/faceLibrary.json new file mode 100644 index 0000000..19b6dbb --- /dev/null +++ b/web/public/locales/pl/views/faceLibrary.json @@ -0,0 +1,105 @@ +{ + "selectItem": "Wybierz {{item}}", + "description": { + "addFace": "Dodaj nową kolekcję do biblioteki twarzy, przesyłając swoje pierwsze zdjęcie.", + "placeholder": "Wprowadź nazwę tej kolekcji", + "invalidName": "Niepoprawna nazwa. Nazwy mogą zawierać tylko: litery, cyfry, spacje, cudzysłowy, podkreślniniki i myślniki.", + "nameCannotContainHash": "Nazwa nie może zawierać #." + }, + "details": { + "person": "Osoba", + "confidence": "Pewność", + "face": "Szczegóły twarzy", + "faceDesc": "Szczegóły śledzonego obiektu, który wygenerował tę twarz", + "timestamp": "Znacznik czasu", + "subLabelScore": "Ocena podetykiety", + "scoreInfo": "Ocena podetykiety to ważona ocena uwzględniająca wszystkie rozpoznane twarze i ich poziom rozpoznania , dlatego może się różnić od oceny pokazanej na stopklatce.", + "unknown": "Nieznany" + }, + "documentTitle": "Biblioteka twarzy - Frigate", + "uploadFaceImage": { + "title": "Wgraj zdjęcie twarzy", + "desc": "Wgraj obraz do skanowania twarzy i dołącz do {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Utwórz kolekcję", + "desc": "Utwórz nową kolekcję", + "new": "Utwórz nową twarz", + "nextSteps": "Aby zbudować solidną podstawę:
  • Użyj zakładki Ostatnie rozpoznania, aby wybrać i trenować na obrazach dla każdej wykrytej osoby.
  • Skup się na zdjęciach twarzy na wprost dla najlepszych wyników; unikaj trenowania na zdjęciach, które pokazują twarze pod kątem.
  • " + }, + "train": { + "aria": "Wybierz ostatnio rozpoznane", + "title": "Ostatnie rozpoznania", + "empty": "Nie podjęto ostatnio żadnych prób rozpoznawania twarzy", + "titleShort": "Ostatnie" + }, + "selectFace": "Wybierz twarz", + "deleteFaceLibrary": { + "title": "Usuń nazwę", + "desc": "Czy na pewno chcesz usunąć kolekcję {{name}}? Spowoduje to trwałe usunięcie wszystkich powiązanych twarzy." + }, + "button": { + "addFace": "Dodaj twarz", + "uploadImage": "Wgraj obraz", + "reprocessFace": "Przetwórz twarz ponownie", + "deleteFaceAttempts": "Usuń twarze", + "renameFace": "Zmień nazwę twarzy", + "deleteFace": "Usuń twarz" + }, + "imageEntry": { + "validation": { + "selectImage": "Proszę wybrać plik obrazu." + }, + "dropActive": "Upuść obraz tutaj…", + "dropInstructions": "Przeciągnij i upuść obraz tutaj lub kliknij, aby wybrać", + "maxSize": "Maksymalny rozmiar: {{size}}MB" + }, + "toast": { + "success": { + "deletedName_one": "{{count}} twarz została pomyślnie usunięta.", + "deletedName_few": "{{count}} twarze zostały pomyślnie usunięte.", + "deletedName_many": "{{count}} twarzy zostało pomyślnie usuniętych.", + "deletedFace_one": "Pomyślnie usunięto {{count}} twarz.", + "deletedFace_few": "Pomyślnie usunięto {{count}} twarze.", + "deletedFace_many": "Pomyślnie usunięto {{count}} twarzy.", + "uploadedImage": "Pomyślnie wgrano obraz.", + "addFaceLibrary": "{{name}} został pomyślnie dodany do Biblioteki Twarzy!", + "trainedFace": "Pomyślnie wytrenowano twarz.", + "updatedFaceScore": "Pomyślnie zaktualizowano wynik twarzy do {{name}} {{score}}.", + "renamedFace": "Pomyślnie zmieniono nazwę twarzy na {{name}}" + }, + "error": { + "addFaceLibraryFailed": "Nie udało się ustawić nazwy twarzy: {{errorMessage}}", + "deleteFaceFailed": "Nie udało się usunąć: {{errorMessage}}", + "deleteNameFailed": "Nie udało się usunąć nazwy: {{errorMessage}}", + "trainFailed": "Nie udało się przeprowadzić treningu: {{errorMessage}}", + "updateFaceScoreFailed": "Nie udało się zaktualizować wyniku twarzy: {{errorMessage}}", + "uploadingImageFailed": "Nie udało się wgrać obrazu: {{errorMessage}}", + "renameFaceFailed": "Nie udało się zmienić nazwy twarzy: {{errorMessage}}" + } + }, + "readTheDocs": "Przeczytaj dokumentację", + "trainFaceAs": "Trenuj twarz jako:", + "trainFace": "Trenuj twarz", + "steps": { + "faceName": "Wprowadź nazwę twarzy", + "uploadFace": "Prześlij obraz twarzy", + "nextSteps": "Kolejne kroki", + "description": { + "uploadFace": "Prześlij zdjęcie {{name}}, na którym twarz jest widoczna z przodu. Obraz nie musi być przycięty wyłącznie do twarzy." + } + }, + "renameFace": { + "title": "Zmień nazwę twarzy", + "desc": "Wprowadź nową nazwę dla {{name}}" + }, + "collections": "Kolekcje", + "deleteFaceAttempts": { + "title": "Usuń twarze", + "desc_one": "Czy na pewno chcesz usunąć {{count}} twarz? Tej czynności nie można cofnąć.", + "desc_few": "Czy na pewno chcesz usunąć {{count}} twarze? Tej czynności nie można cofnąć.", + "desc_many": "Czy na pewno chcesz usunąć {{count}} twarzy? Tej czynności nie można cofnąć." + }, + "nofaces": "Brak dostępnych twarzy", + "pixels": "{{area}}px" +} diff --git a/web/public/locales/pl/views/live.json b/web/public/locales/pl/views/live.json new file mode 100644 index 0000000..77f1a42 --- /dev/null +++ b/web/public/locales/pl/views/live.json @@ -0,0 +1,202 @@ +{ + "documentTitle": { + "default": "Transmisja - Frigate" + }, + "documentTitle.withCamera": "{{camera}}- Na żywo - Frigate", + "lowBandwidthMode": "Tryb niskiej przepustowości", + "twoWayTalk": { + "enable": "Włącz komunikację dwukierunkową", + "disable": "Wyłącz komunikację dwukierunkową" + }, + "cameraAudio": { + "enable": "Włącz dźwięk kamery", + "disable": "Wyłącz dźwięk kamery" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kliknij w ramce, aby wyśrodkować kamerę", + "enable": "Włącz kliknięcie do przesuwania", + "disable": "Wyłącz kliknięcie do przesuwania", + "enableWithZoom": "Włącz kliknij, aby przesunąć / przeciągnij, aby przybliżyć" + }, + "left": { + "label": "Przesuń kamerę PTZ w lewo" + }, + "right": { + "label": "Przesuń kamerę PTZ w prawo" + }, + "down": { + "label": "Przesuń kamerę PTZ w dół" + }, + "up": { + "label": "Przesuń kamerę PTZ w górę" + } + }, + "zoom": { + "in": { + "label": "Przybliż kamerę PTZ" + }, + "out": { + "label": "Oddal kamerę PTZ" + } + }, + "frame": { + "center": { + "label": "Kliknij w ramce, aby wyśrodkować kamerę PTZ" + } + }, + "presets": "Presety kamery PTZ", + "focus": { + "in": { + "label": "Zmniejsz ostrość kamery PTZ" + }, + "out": { + "label": "Wyostrz kamerę PTZ" + } + } + }, + "recording": { + "enable": "Włącz nagrywanie", + "disable": "Wyłącz nagrywanie" + }, + "snapshots": { + "enable": "Włącz zrzuty ekranu", + "disable": "Wyłącz zrzuty ekranu" + }, + "streamStats": { + "disable": "Ukryj statystyki strumienia", + "enable": "Pokaż statystyki strumienia" + }, + "manualRecording": { + "title": "Nagrywanie na żądanie", + "tips": "Ręcznie rozpocznij zdarzenie w oparciu o ustawienia przechowywania nagrań tej kamery.", + "playInBackground": { + "label": "Odtwarzaj w tle", + "desc": "Włącz tę opcję, aby kontynuować transmisję, gdy odtwarzacz jest ukryty." + }, + "showStats": { + "label": "Pokaż statystyki", + "desc": "Włącz tę opcję, aby pokazać statystyki strumienia jako nakładkę na podgląd kamery." + }, + "debugView": "Widok debugowania", + "start": "Rozpocznij nagrywanie na żądanie", + "started": "Rozpoczęto ręczne nagrywanie na żądanie.", + "failedToStart": "Nie udało się rozpocząć ręcznego nagrywania na żądanie.", + "recordDisabledTips": "Ponieważ nagrywanie jest wyłączone lub ograniczone w konfiguracji tej kamery, zostanie zapisany tylko zrzut ekranu.", + "end": "Zakończ nagrywanie na żądanie", + "ended": "Zakończono ręczne nagrywanie na żądanie.", + "failedToEnd": "Nie udało się zakończyć ręcznego nagrywania na żądanie." + }, + "notifications": "Powiadomienia", + "audio": "Dźwięk", + "suspend": { + "forTime": "Zawieś na: " + }, + "stream": { + "title": "Strumień", + "audio": { + "tips": { + "title": "Dźwięk musi być wysyłany z kamery i skonfigurowany w go2rtc dla tego strumienia.", + "documentation": "Przeczytaj dokumentację " + }, + "available": "Dźwięk jest dostępny dla tego strumienia", + "unavailable": "Dźwięk nie jest dostępny dla tego strumienia" + }, + "twoWayTalk": { + "tips.documentation": "Przeczytaj dokumentację ", + "tips": "Twoje urządzenie musi obsługiwać tę funkcję, a WebRTC musi być skonfigurowany dla komunikacji dwukierunkowej.", + "unavailable": "Komunikacja dwukierunkowa jest niedostępna dla tego strumienia", + "available": "Komunikacja dwukierunkowa jest dostępna dla tego strumienia" + }, + "lowBandwidth": { + "resetStream": "Zresetuj strumień", + "tips": "Podgląd na żywo jest w trybie niskiej przepustowości z powodu buforowania lub błędów strumienia." + }, + "playInBackground": { + "tips": "Włącz tę opcję, aby kontynuować transmisję, gdy odtwarzacz jest ukryty.", + "label": "Odtwarzaj w tle" + }, + "debug": { + "picker": "Wybór strumienia jest niedostępny w trybie debug. Widok w trybie debug zawsze pokazuje strumień przypisany do detektora." + } + }, + "cameraSettings": { + "title": "Ustawienia {{camera}}", + "cameraEnabled": "Kamera włączona", + "objectDetection": "Wykrywanie obiektów", + "recording": "Nagrywanie", + "snapshots": "Zrzuty ekranu", + "audioDetection": "Wykrywanie dźwięku", + "autotracking": "Automatyczne śledzenie", + "transcription": "Stenogram" + }, + "effectiveRetainMode": { + "modes": { + "all": "Wszystkie", + "active_objects": "Aktywne obiekty", + "motion": "Ruch" + }, + "notAllTips": "Twoja konfiguracja przechowywania nagrań {{source}} jest ustawiona na tryb: {{effectiveRetainMode}}, więc to nagrywanie na żądanie zachowa tylko segmenty z {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Edytuj układ", + "group": { + "label": "Edytuj grupę kamer" + }, + "exitEdit": "Zakończ edycję" + }, + "muteCameras": { + "enable": "Wycisz wszystkie kamery", + "disable": "Wyłącz wyciszenie wszystkich kamer" + }, + "camera": { + "disable": "Wyłącz kamerę", + "enable": "Włącz kamerę" + }, + "autotracking": { + "enable": "Włącz automatyczne śledzenie", + "disable": "Wyłącz automatyczne śledzenie" + }, + "detect": { + "disable": "Wyłącz wykrywanie", + "enable": "Włącz wykrywanie" + }, + "audioDetect": { + "enable": "Włącz wykrywanie dźwięku", + "disable": "Wyłącz wykrywanie dźwięku" + }, + "streamingSettings": "Ustawienia transmisji", + "history": { + "label": "Pokaż nagrania archiwalne" + }, + "transcription": { + "enable": "Włącz audio transkrypcję na żywo", + "disable": "Wyłącz audio transkrypcję na żywo" + }, + "noCameras": { + "buttonText": "Dodaj kamerę", + "description": "Zacznij od podłączenia kamery do Frigate.", + "title": "Nie skonfigurowano żadnej kamery", + "restricted": { + "title": "Brak dostępnych kamer", + "description": "Nie masz uprawnień aby przeglądać kamery w tej grupie." + }, + "default": { + "title": "Nie skonfigurowano żadnej kamery", + "buttonText": "Dodaj kamerę", + "description": "Zacznij od podłączenia kamery do Frigate." + }, + "group": { + "title": "Brak kamer w grupie", + "description": "Grupa kamer nie ma przypisanych ani włączonych kamer.", + "buttonText": "Zarządzaj grupami" + } + }, + "snapshot": { + "takeSnapshot": "Pobierz miniaturę", + "captureFailed": "Nie udało się wykonać migawki.", + "downloadStarted": "Pobieranie migawki rozpoczęte.", + "noVideoSource": "Brak źródeł video dostępnych do wykonania migawki." + } +} diff --git a/web/public/locales/pl/views/motionSearch.json b/web/public/locales/pl/views/motionSearch.json new file mode 100644 index 0000000..6406859 --- /dev/null +++ b/web/public/locales/pl/views/motionSearch.json @@ -0,0 +1,58 @@ +{ + "documentTitle": "Wyszukiwanie zdarzeń ruchu - Frigate", + "title": "Wyszukiwanie zdarzeń ruchu", + "description": "Narysuj wielokąt aby wyznaczyć obszar zainteresowania, a następnie określ przedział czasowy w którym chcesz wyszukać zmiany ruchu w tym obszarze.", + "selectCamera": "Ładowanie wyszukiwania zdarzeń ruchu", + "startSearch": "Start wyszukiwania", + "searchStarted": "Uruchomiono wyszukiwanie", + "searchCancelled": "Zatrzymano wyszukiwanie", + "cancelSearch": "Anuluj", + "searching": "Wyszukiwanie w trakcie.", + "searchComplete": "Wyszukiwanie zakończono", + "noResultsYet": "Przeprowadź wyszukiwanie aby znaleźć zmiany ruchu w zaznaczonym obszarze", + "noChangesFound": "W wybranym obszarze nie wykryto żadnych zmian w pikselach", + "framesProcessed": "{{count}} przetworzonych klatek obrazu", + "jumpToTime": "Przejdź do tego momentu", + "results": "Wyniki", + "showSegmentHeatmap": "Mapa cieplna", + "newSearch": "Nowe wyszukiwanie", + "clearResults": "Wyczyść wyniki", + "clearROI": "Wyczyść strefę", + "polygonControls": { + "undo": "Cofnij ostatnią zmianę", + "reset": "Reset strefy" + }, + "motionHeatmapLabel": "Mapa aktywności ruchu", + "dialog": { + "title": "Wyszukiwanie według ruchu", + "cameraLabel": "Kamera", + "previewAlt": "Podgląd z kamery {{camera}}" + }, + "timeRange": { + "title": "Zakres wyszukiwania", + "start": "Czas rozpoczęcia", + "end": "Czas zakończenia" + }, + "settings": { + "title": "Ustawienia wyszukiwania", + "parallelMode": "Tryb równoległy", + "parallelModeDesc": "Przeskanuj wiele fragmentów nagrania jednocześnie (szybsze, ale znacznie bardziej obciążające procesor)", + "threshold": "Próg czułości", + "thresholdDesc": "Niższe wartości pozwalają wykrywać mniejsze zmiany (1–255)", + "minArea": "Obszar minimalnej zmiany", + "minAreaDesc": "Minimalny procent obszaru zainteresowania który musi ulec zmianie aby uznano to za istotne", + "frameSkip": "Pominięcie klatki", + "frameSkipDesc": "Przetwarza co N klatkę. Ustaw wartość na liczbę klatek na sekundę swojej kamery, aby przetwarzać jedną klatkę na sekundę (np. 5 dla kamery o 5 klatkach na sekundę, 30 dla kamery o 30 klatkach na sekundę). Wyższe wartości zapewniają większą szybkość ale mogą powodować pominięcie krótkich zdarzeń ruchu.", + "maxResults": "Maksymalne wyniki", + "maxResultsDesc": "Zatrzymaj się po osiągnięciu określonej liczby pasujących znaczników czasu" + }, + "errors": { + "noCamera": "Wybierz kamerę", + "noROI": "Zaznacz obszar zainteresowania", + "noTimeRange": "Ustaw przedział czasowy", + "invalidTimeRange": "Czas zakończenia musi być późniejszy niż czas rozpoczęcia", + "searchFailed": "Wyszukiwanie nie powiodło się: {{message}}", + "polygonTooSmall": "Obszar musi mieć co najmniej 3 punkty", + "unknown": "Nieznany błąd" + } +} diff --git a/web/public/locales/pl/views/recording.json b/web/public/locales/pl/views/recording.json new file mode 100644 index 0000000..dfaf0c3 --- /dev/null +++ b/web/public/locales/pl/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtr", + "export": "Eksportuj", + "calendar": "Kalendarz", + "filters": "Filtry", + "toast": { + "error": { + "noValidTimeSelected": "Nie wybrano poprawnego zakresu czasu", + "endTimeMustAfterStartTime": "Czas końca musi być po czasie początku" + } + } +} diff --git a/web/public/locales/pl/views/replay.json b/web/public/locales/pl/views/replay.json new file mode 100644 index 0000000..bfc3c4f --- /dev/null +++ b/web/public/locales/pl/views/replay.json @@ -0,0 +1,6 @@ +{ + "page": { + "preparingClip": "Przygotuje urywek…" + }, + "title": "Debugowanie nagrań" +} diff --git a/web/public/locales/pl/views/search.json b/web/public/locales/pl/views/search.json new file mode 100644 index 0000000..9de364f --- /dev/null +++ b/web/public/locales/pl/views/search.json @@ -0,0 +1,75 @@ +{ + "search": "Szukaj", + "savedSearches": "Zapisane wyszukiwania", + "searchFor": "Szukaj {{inputValue}}", + "button": { + "clear": "Wyczyść wyszukiwanie", + "save": "Zapisz wyszukiwanie", + "delete": "Usuń zapisane wyszukiwanie", + "filterInformation": "Informacje o filtrze", + "filterActive": "Aktywne filtry" + }, + "trackedObjectId": "ID śledzonego obiektu", + "filter": { + "label": { + "cameras": "Kamery", + "labels": "Etykiety", + "zones": "Strefy", + "sub_labels": "Podetykiety", + "min_score": "Min. wynik", + "max_score": "Maks. wynik", + "min_speed": "Min. prędkość", + "max_speed": "Maks. prędkość", + "recognized_license_plate": "Rozpoznana tablica rejestracyjna", + "has_clip": "Posiada klip", + "has_snapshot": "Posiada zrzut ekranu", + "after": "Po", + "search_type": "Typ wyszukiwania", + "time_range": "Zakres czasu", + "before": "Przed", + "attributes": "Właściwości" + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Opis" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Data 'przed' musi być późniejsza niż data 'po'.", + "afterDatebeEarlierBefore": "Data 'po' musi być wcześniejsza niż data 'przed'.", + "minScoreMustBeLessOrEqualMaxScore": "'Min. wynik' musi być mniejszy lub równy 'maks. wynikowi'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'Maks. wynik' musi być większy lub równy 'min. wynikowi'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'Min. prędkość' musi być mniejsza lub równa 'maks. prędkości'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'Maks. prędkość' musi być większa lub równa 'min. prędkości'." + } + }, + "tips": { + "title": "Jak używać filtrów tekstowych", + "desc": { + "text": "Filtry pomagają zawęzić wyniki wyszukiwania. Oto jak używać ich w polu wejściowym:", + "example": "Przykład: cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "step": "
    • Wpisz nazwę filtra, a następnie dwukropek (np., \"kamery:\").
    • Wybierz wartość z sugestii lub wpisz własną.
    • Używaj wielu filtrów, dodając je jeden po drugim z odstępem pomiędzy.
    • Filtry dat (przed: i po:) używają formatu {{DateFormat}}.
    • Filtr zakresu czasu używa formatu {{exampleTime}}.
    • Usuwaj filtry klikając 'x' obok nich.
    ", + "step2": "Wybierz wartość z sugestii lub wpisz własną.", + "step3": "Użyj kilku filtrów dodając jeden po drugim i oddzielając je spacją.", + "exampleLabel": "Przykład:", + "step1": "Wprowadź nazwę filtra z dwukropkiem (np.: \"cameras:\").", + "step4": "Filtry dat (before: i after:) użyj formatu {{DateFormat}}.", + "step5": "Filtr zakresu czasu wykorzystuje format {{exampleTime}}.", + "step6": "Usuń filtry klikając 'x' obok nich." + } + }, + "header": { + "currentFilterType": "Wartości filtrów", + "noFilters": "Filtry", + "activeFilters": "Aktywne filtry" + } + }, + "similaritySearch": { + "title": "Wyszukiwanie podobieństw", + "active": "Wyszukiwanie podobieństw aktywne", + "clear": "Wyczyść wyszukiwanie podobieństw" + }, + "placeholder": { + "search": "Szukaj…" + } +} diff --git a/web/public/locales/pl/views/settings.json b/web/public/locales/pl/views/settings.json new file mode 100644 index 0000000..6167a9d --- /dev/null +++ b/web/public/locales/pl/views/settings.json @@ -0,0 +1,1333 @@ +{ + "menu": { + "users": "Użytkownicy", + "notifications": "Powiadomienia", + "ui": "Interfejs Użytkownika", + "classification": "Klasyfikacja", + "cameras": "Ustawienia Kamery", + "frigateplus": "Frigate+", + "masksAndZones": "Maski / Strefy", + "motionTuner": "Konfigurator ruchu", + "debug": "Debugowanie", + "enrichments": "Wzbogacenia", + "triggers": "Wyzwalacze", + "roles": "Role", + "cameraManagement": "Zarządzanie", + "cameraReview": "Przegląd", + "integrations": "Integracje", + "uiSettings": "Ustawienia interfejsu użytkownika", + "profiles": "Profile", + "globalDetect": "Detekcja obiektów", + "globalRecording": "Nagrywanie", + "globalSnapshots": "Snapshoty", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Detekcja ruchu", + "globalObjects": "Obiekty", + "globalReview": "Recenzja", + "globalAudioEvents": "Detekcja dźwięku", + "globalLivePlayback": "Podgląd na żywo", + "globalTimestampStyle": "Styl znacznika czasu", + "systemDatabase": "Baza danych", + "systemTls": "TLS", + "systemAuthentication": "Autentykacja", + "systemNetworking": "Sieć", + "systemProxy": "Proxy", + "systemUi": "UI", + "systemLogging": "Logowanie", + "systemEnvironmentVariables": "Zmienne środowiskowe", + "systemTelemetry": "Telemetria", + "systemBirdseye": "Podgląd obrazu", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "Detektory i model", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "strumienie go2rtc", + "integrationSemanticSearch": "Wyszukiwanie semantyczne", + "integrationGenerativeAi": "Generatywna sztuczna inteligencja", + "integrationFaceRecognition": "Rozpoznawanie twarzy", + "integrationLpr": "Rozpoznawanie tablic rejestracyjnych", + "integrationObjectClassification": "Klasyfikacja obiektów", + "integrationAudioTranscription": "Transkrypcja dźwięku", + "cameraDetect": "Detekcja obiektów", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "Nagrywanie", + "cameraBirdseye": "Podgląd obrazu", + "cameraFaceRecognition": "Rozpoznawanie twarzy", + "cameraLpr": "Rozpoznawanie tablic rejestracyjnych", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraAudioTranscription": "Transkrypcja dźwięku", + "cameraNotifications": "Powiadomienia", + "cameraLivePlayback": "Podgląd na żywo", + "cameraSnapshots": "Snapshoty", + "cameraMotion": "Detekcja ruchu", + "cameraObjects": "Obiekty", + "cameraConfigReview": "Recenzja", + "cameraAudioEvents": "Detekcja dźwięku", + "cameraUi": "Interfejs użytkownika kamery", + "cameraTimestampStyle": "Styl znacznika czasu", + "cameraMqtt": "MQTT kamery", + "maintenance": "Utrzymanie", + "mediaSync": "Synchronizacja mediów", + "regionGrid": "Siatka regionalna" + }, + "dialog": { + "unsavedChanges": { + "title": "Masz niezapisane zmiany.", + "desc": "Czy chcesz zapisać swoje zmiany przed kontynuowaniem?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Brak Kamery" + }, + "general": { + "title": "Ustawienia interfejsu użytkownika", + "storedLayouts": { + "title": "Zapisane Układy", + "clearAll": "Wyczyść Wszystkie Układy", + "desc": "Układ kamer w grupie można przeciągać/zmieniać rozmiar. Pozycje są zapisywane w lokalnej pamięci przeglądarki." + }, + "calendar": { + "title": "Kalendarz", + "firstWeekday": { + "label": "Pierwszy dzień tygodnia", + "sunday": "Niedziela", + "monday": "Poniedziałek", + "desc": "Dzień od którego zaczyna się kalendarz przeglądu." + } + }, + "liveDashboard": { + "automaticLiveView": { + "label": "Automatyczny Podgląd na Żywo", + "desc": "Automatycznie przełącz na podgląd na żywo kamery, gdy wykryta zostanie aktywność. Wyłączenie tej opcji spowoduje, że statyczne obrazy kamer na panelu Na Żywo będą aktualizowane tylko raz na minutę." + }, + "title": "Panel Na Żywo", + "playAlertVideos": { + "label": "Odtwarzaj Filmy Alarmowe", + "desc": "Domyślnie, ostatnie alerty na panelu Na Żywo są odtwarzane jako małe zapętlone filmy. Wyłącz tę opcję, aby pokazywać tylko statyczny obraz ostatnich alertów na tym urządzeniu/przeglądarce." + }, + "displayCameraNames": { + "label": "Zawsze pokazuj nazwy kamer", + "desc": "Zawsze pokazuj nazwę kamery w widoku wielu kamer." + }, + "liveFallbackTimeout": { + "label": "Przekroczono czas oczekiwania dla strumienia", + "desc": "W wypadku utraty strumienia wysokiej jakości, użyj trybu niskiej przepustowości po X sekund od utracenia połączenia. Sugerowana wartość: 3." + } + }, + "cameraGroupStreaming": { + "title": "Ustawienia Strumieniowania Grup Kamer", + "desc": "Ustawienia strumieniowania dla każdej grupy kamer są przechowywane w lokalnej pamięci przeglądarki.", + "clearAll": "Wyczyść Wszystkie Ustawienia Strumieniowania" + }, + "recordingsViewer": { + "title": "Przeglądarka Nagrań", + "defaultPlaybackRate": { + "label": "Domyślna Prędkość Odtwarzania", + "desc": "Domyślna prędkość odtwarzania dla odtwarzania nagrań." + } + }, + "toast": { + "success": { + "clearStoredLayout": "Wyczyszczono zapisany układ dla {{cameraName}}", + "clearStreamingSettings": "Wyczyszczono ustawienia strumieniowania dla wszystkich grup kamer." + }, + "error": { + "clearStoredLayoutFailed": "Nie udało się wyczyścić zapisanego układu: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nie udało się wyczyścić ustawień strumieniowania: {{errorMessage}}" + } + } + }, + "documentTitle": { + "default": "Ustawienia - Frigate", + "camera": "Ustawienia Kamery - Frigate", + "masksAndZones": "Edytor Masek i Stref - Frigate", + "frigatePlus": "Ustawienia Frigate+ - Frigate", + "classification": "Ustawienia Klasyfikacji - Frigate", + "general": "Ustawienia Interfejsu - Frigate", + "authentication": "Ustawienia Uwierzytelniania - Frigate", + "motionTuner": "Konfigurator Ruchu - Frigate", + "object": "Debug - Frigate", + "notifications": "Ustawienia powiadomień - Frigate", + "enrichments": "Ustawienia wzbogacania - Frigate", + "cameraManagement": "Zarządzanie kamerami – Frigate", + "cameraReview": "Ustawienia przeglądu kamer - Frigate", + "globalConfig": "Konfiguracja globalna - Frigate", + "cameraConfig": "Konfiguracja kamery - Frigate", + "maintenance": "Konserwacja – Frigate", + "profiles": "Profile - Frigate", + "detectorsAndModel": "Detektory i model" + }, + "classification": { + "title": "Ustawienia Klasyfikacji", + "semanticSearch": { + "modelSize": { + "small": { + "title": "mały", + "desc": "Używanie małego modelu wykorzystuje skwantyzowaną wersję, która zużywa mniej pamięci RAM i działa szybciej na procesorze przy bardzo nieznacznej różnicy w jakości osadzeń." + }, + "large": { + "title": "duży", + "desc": "Używanie dużego modelu wykorzystuje pełny model Jina i automatycznie uruchomi się na GPU, jeśli jest dostępny." + }, + "desc": "Rozmiar modelu używanego do osadzeń wyszukiwania semantycznego.", + "label": "Rozmiar Modelu" + }, + "title": "Wyszukiwanie Semantyczne", + "desc": "Wyszukiwanie semantyczne w Frigate pozwala znaleźć śledzone obiekty w elementach przeglądu za pomocą samego obrazu, zdefiniowanego przez użytkownika opisu tekstowego lub automatycznie wygenerowanego opisu.", + "readTheDocumentation": "Przeczytaj Dokumentację", + "reindexNow": { + "label": "Przeindeksuj Teraz", + "desc": "Przeindeksowanie wygeneruje ponownie osadzenia dla wszystkich śledzonych obiektów. Ten proces działa w tle i może maksymalnie obciążyć procesor oraz zająć sporo czasu w zależności od liczby śledzonych obiektów.", + "confirmButton": "Przeindeksuj", + "success": "Przeindeksowanie rozpoczęte pomyślnie.", + "alreadyInProgress": "Przeindeksowanie jest już w toku.", + "error": "Nie udało się rozpocząć przeindeksowania: {{errorMessage}}", + "confirmTitle": "Potwierdź Przeindeksowanie", + "confirmDesc": "Czy na pewno chcesz przeindeksować osadzenia wszystkich śledzonych obiektów? Ten proces będzie działał w tle, ale może maksymalnie obciążyć procesor i zająć sporo czasu. Możesz śledzić postęp na stronie Eksploruj." + } + }, + "faceRecognition": { + "title": "Rozpoznawanie Twarzy", + "desc": "Rozpoznawanie twarzy pozwala na przypisanie imion do osób przez Frigate jako podrzędna etykieta. Ta informacja jest zawarta w interfejsie użytkownika, filtrach jak i w powiadomieniach.", + "modelSize": { + "label": "Rozmiar Modelu", + "desc": "Rozmiar modelu używanego do rozpoznawania twarzy.", + "small": { + "title": "mały", + "desc": "Używanie małego modelu wykorzystuje model osadzania twarzy FaceNet, który działa efektywnie na większości procesorów." + }, + "large": { + "title": "duży", + "desc": "Używanie dużego modelu wykorzystuje model osadzania twarzy ArcFace i automatycznie uruchomi się na GPU, jeśli jest dostępny." + } + }, + "readTheDocumentation": "Przeczytaj Dokumentację" + }, + "licensePlateRecognition": { + "title": "Rozpoznawanie Tablic Rejestracyjnych", + "desc": "Frigate może rozpoznawać tablice rejestracyjne pojazdów i automatycznie dodawać wykryte znaki do pola recognized_license_plate albo znaną nazwę jako etykietę podrzędną do obiektów typu samochód. Częstym przypadkiem użycia może być odczytywanie tablic rejestracyjnych samochodów wjeżdżających na podjazd albo przejeżdżających ulicą.", + "readTheDocumentation": "Przeczytaj Dokumentację" + }, + "toast": { + "error": "Nie udało się zapisać zmian w konfiguracji: {{errorMessage}}", + "success": "Ustawienia klasyfikacji zostały zapisane. Uruchom ponownie Frigate aby wprowadzić swoje zmiany." + }, + "birdClassification": { + "desc": "Klasyfikacja ptaków identyfikuje znane ptaki przy użyciu skwantyzowanego modelu Tensorflow. Gdy znany ptak zostanie rozpoznany, jego popularna nazwa zostanie dodana jako sub_label. Ta informacja jest uwzględniana w interfejsie użytkownika, filtrach oraz powiadomieniach.", + "title": "Klasyfikacja Ptaków" + }, + "restart_required": "Wymagane ponowne uruchomienie (Zmienione ustawienia klasyfikacji)", + "unsavedChanges": "Niezapisane zmiany w ustawieniach klasyfikacji" + }, + "camera": { + "title": "Ustawienia Kamery", + "reviewClassification": { + "noDefinedZones": "Brak stref zdefiniowanych dla tej kamery.", + "selectAlertsZones": "Wybierz strefy dla Alertów", + "title": "Klasyfikacja Przeglądu", + "desc": "Frigate kategoryzuje elementy przeglądu jako Alerty i Wykrycia. Domyślnie wszystkie obiekty osoba i samochód są traktowane jako Alerty. Możesz doprecyzować kategoryzację elementów przeglądu, konfigurując dla nich wymagane strefy.", + "readTheDocumentation": "Przeczytaj Dokumentację", + "objectAlertsTips": "Wszystkie obiekty {{alertsLabels}} na {{cameraName}} będą wyświetlane jako Alerty.", + "zoneObjectAlertsTips": "Wszystkie obiekty {{alertsLabels}} wykryte w strefie {{zone}} na {{cameraName}} będą wyświetlane jako Alerty.", + "zoneObjectDetectionsTips": { + "notSelectDetections": "Wszystkie obiekty {{detectionsLabels}} wykryte w strefie {{zone}} na {{cameraName}}, które nie są skategoryzowane jako Alerty, będą wyświetlane jako Wykrycia niezależnie od strefy, w której się znajdują.", + "regardlessOfZoneObjectDetectionsTips": "Wszystkie nieskategoryzowane obiekty {{detectionsLabels}} na {{cameraName}} będą wyświetlane jako Wykrycia niezależnie od strefy, w której się znajdują.", + "text": "Wszystkie nieskategoryzowane obiekty {{detectionsLabels}} w strefie {{zone}} na {{cameraName}} będą wyświetlane jako Wykrycia." + }, + "toast": { + "success": "Konfiguracja klasyfikacji przeglądu została zapisana. Uruchom ponownie Frigate, aby zastosować zmiany." + }, + "objectDetectionsTips": "Wszystkie nieskategoryzowane obiekty {{detectionsLabels}} na {{cameraName}} będą wyświetlane jako Wykrycia niezależnie od strefy, w której się znajdują.", + "limitDetections": "Ogranicz wykrycia do określonych stref", + "selectDetectionsZones": "Wybierz strefy dla Wykryć", + "unsavedChanges": "Niezapisane ustawienia klasyfikacji przeglądu dla {{camera}}" + }, + "review": { + "alerts": "Alerty ", + "title": "Przegląd", + "detections": "Wykrycia ", + "desc": "Tymczasowo włącz/wyłącz alerty i wykrywania dla tej kamery do czasu restartu Frigate. Po wyłączeniu nie będą generowane nowe elementy do przeglądu. " + }, + "streams": { + "desc": "Tymczasowo wyłącz kamerę dopóki Frigate nie uruchomi się ponownie. Wyłączenie kamery całkowicie zatrzymuje przetwarzanie strumieni tej kamery przez Frigate. Wykrywanie, nagrywanie i debugowanie będą niedostępne.
    Uwaga: Nie wyłącza to przekazywania strumieni go2rtc.", + "title": "Strumienie" + }, + "object_descriptions": { + "title": "Opisy obiektów wygenerowane przez Sztuczną Inteligencję", + "desc": "Tymczasowo włącz/wyłącz opisy obiektów generowane przez SI. Gdy zostanie to wyłączone, prośby o opis śledzonych obiektów dla tej kamery nie będzie przesyłany do SI." + }, + "review_descriptions": { + "title": "Opis recenzji od SI", + "desc": "Tymczasowo włącz/wyłącz recenzje opisów SI dla tej kamery. Gdy wyłączone prośby o wykonanie opisów nie zostaną przekazane do SI dla tej kamery." + }, + "addCamera": "Dodaj nową kamerę", + "editCamera": "Edytuj kamerę:", + "selectCamera": "Wybierz kamerę", + "backToSettings": "Powrót do ustawień kamery", + "cameraConfig": { + "add": "Dodaj kamerę", + "edit": "Edytuj kamerę", + "description": "Konfiguracja ustawień kamery wraz ze strumieniem wejściowym i rolami.", + "name": "Nazwa kamery", + "nameRequired": "Nazwa kamery jest wymagana", + "nameLength": "Nazwa kamery musi być krótsza niż 24 znaki.", + "namePlaceholder": "np. drzwi_wejsciowe", + "enabled": "Włączony", + "ffmpeg": { + "inputs": "Strumienie wejściowe", + "path": "Ścieżka do strumienia", + "pathRequired": "Ścieżka do strumienia jest wymagana", + "pathPlaceholder": "rtsp://...", + "roles": "Role", + "rolesRequired": "Przynajmniej jedna rola jest wymagana", + "rolesUnique": "Każda z ról (audio, detect, record) może być przypisana tylko do jednego strumienia", + "addInput": "Dodaj strumień wejściowy", + "removeInput": "Usuń strumień wejściowy", + "inputsRequired": "Przynajmniej jeden strumień wejściowy jest wymagany" + }, + "toast": { + "success": "Konfiguracja kamery {{cameraName}} została zapisana" + } + } + }, + "masksAndZones": { + "filter": { + "all": "Wszystkie Maski i Strefy" + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Nazwa strefy musi mieć co najmniej 2 znaki.", + "mustNotBeSameWithCamera": "Nazwa strefy nie może być taka sama jak nazwa kamery.", + "alreadyExists": "Strefa z tą nazwą już istnieje dla tej kamery.", + "hasIllegalCharacter": "Nazwa strefy zawiera niedozwolone znaki.", + "mustNotContainPeriod": "Nazwa strefy nie może zawierać kropki.", + "mustHaveAtLeastOneLetter": "Nazwa strefy musi zawierać co najmniej jedną literę." + } + }, + "distance": { + "error": { + "text": "Odległość musi być większa lub równa 0.1.", + "mustBeFilled": "Wszystkie pola odległości muszą być wypełnione, aby używać szacowania prędkości." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Bezwładność musi być większa od 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Czas przebywania musi być większy lub równy 0." + } + }, + "polygonDrawing": { + "snapPoints": { + "false": "Nie przyciągaj punktów", + "true": "Przyciągaj punkty" + }, + "removeLastPoint": "Usuń ostatni punkt", + "reset": { + "label": "Wyczyść wszystkie punkty" + }, + "delete": { + "title": "Potwierdź usunięcie", + "success": "{{name}} został usunięty.", + "desc": "Czy na pewno chcesz usunąć {{type}} {{name}}?" + }, + "error": { + "mustBeFinished": "Rysowanie wielokąta musi być zakończone przed zapisaniem." + }, + "type": { + "object_mask": "maska obiektowa", + "motion_mask": "maska ruchu", + "zone": "strefa" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Próg prędkości musi być większy lub równy 0,1." + } + } + }, + "zones": { + "label": "Strefy", + "documentTitle": "Edytuj Strefę - Frigate", + "desc": { + "title": "Strefy pozwalają na zdefiniowanie konkretnych obszarów kadru, dzięki czemu można sprawdzić, czy obiekt znajduje się w danym obszarze.", + "documentation": "Dokumentacja" + }, + "add": "Dodaj Strefę", + "clickDrawPolygon": "Kliknij aby narysować wielokąt na obrazie.", + "edit": "Edytuj Strefę", + "name": { + "title": "Nazwa", + "inputPlaceHolder": "Wprowadź nazwę…", + "tips": "Nazwa musi mieć co najmniej 1 znak i nie może być taka sama jak nazwa kamery lub innej strefy." + }, + "objects": { + "title": "Obiekty", + "desc": "Lista obiektów dla tej strefy." + }, + "allObjects": "Wszystkie Obiekty", + "point_one": "{{count}} punkt", + "point_few": "{{count}} punkty", + "point_many": "{{count}} punktów", + "inertia": { + "title": "Bezwładność", + "desc": "Określa, przez ile klatek obiekt musi znajdować się w strefie, zanim zostanie uznany za będący w strefie. Domyślnie: 3" + }, + "loiteringTime": { + "title": "Czas przebywania", + "desc": "Ustala minimalny czas w sekundach, przez który obiekt musi znajdować się w strefie, aby ją aktywować. Domyślnie: 0" + }, + "speedThreshold": { + "title": "Próg prędkości ({{unit}})", + "desc": "Określa minimalną prędkość, przy której obiekty są uwzględniane w tej strefie.", + "toast": { + "error": { + "loiteringTimeError": "Strefy z czasem przebywania większym niż 0 nie powinny być używane z szacowaniem prędkości.", + "pointLengthError": "Szacowanie prędkości zostało wyłączone dla tej strefy. Strefy z szacowaniem prędkości muszą mieć dokładnie 4 punkty." + } + } + }, + "speedEstimation": { + "title": "Szacowanie prędkości", + "desc": "Włącz szacowanie prędkości dla obiektów w tej strefie. Strefa musi mieć dokładnie 4 punkty.", + "docs": "Przeczytaj dokumentację", + "lineADistance": "Odległość linii A ({{unit}})", + "lineBDistance": "Odległość linii B ({{unit}})", + "lineCDistance": "Odległość linii C ({{unit}})", + "lineDDistance": "Odległość linii D ({{unit}})" + }, + "toast": { + "success": "Strefa ({{zoneName}}) została zapisana." + } + }, + "motionMasks": { + "desc": { + "documentation": "Dokumentacja", + "title": "Maski ruchu służą do zapobiegania wykrywaniu niepożądanych typów ruchu. Zbyt intensywne maskowanie utrudni śledzenie obiektów." + }, + "point_one": "{{count}} punkt", + "point_few": "{{count}} punkty", + "point_many": "{{count}} punktów", + "clickDrawPolygon": "Kliknij aby narysować wielokąt na obrazie.", + "documentTitle": "Edytuj maskę ruchu - Frigate", + "add": "Nowa Maska Ruchu", + "polygonAreaTooLarge": { + "tips": "Maski ruchu nie zapobiegają wykrywaniu obiektów. Powinieneś użyć wymaganej strefy zamiast tego.", + "documentation": "Przeczytaj dokumentację", + "title": "Maska ruchu pokrywa {{polygonArea}}% ramki kamery. Duże maski ruchu nie są zalecane." + }, + "toast": { + "success": { + "title": "{{polygonName}} został zapisany.", + "noName": "Maska Ruchu została zapisana." + } + }, + "label": "Maska ruchu", + "edit": "Edytuj Maskę Ruchu", + "context": { + "documentation": "Przeczytaj dokumentację", + "title": "Maski ruchu są używane do zapobiegania niepożądanym typom ruchu przed wyzwalaniem detekcji (przykład: gałęzie drzew, znaczniki czasowe kamery). Maski ruchu powinny być używane bardzo oszczędnie, nadmierne maskowanie utrudni śledzenie obiektów." + } + }, + "objectMasks": { + "toast": { + "success": { + "title": "{{polygonName}} został zapisany.", + "noName": "Maska Obiektu została zapisana." + } + }, + "objects": { + "title": "Obiekty", + "allObjectTypes": "Wszystkie typy obiektów", + "desc": "Typ obiektu, który ma zastosowanie do tej maski obiektu." + }, + "point_one": "{{count}} punkt", + "point_few": "{{count}} punkty", + "point_many": "{{count}} punktów", + "label": "Maski Obiektów", + "documentTitle": "Edytuj Maskę Obiektu - Frigate", + "desc": { + "title": "Maski filtrujące obiekty są używane do filtrowania fałszywych detekcji dla danego typu obiektu na podstawie lokalizacji.", + "documentation": "Dokumentacja" + }, + "add": "Dodaj Maskę Obiektu", + "edit": "Edytuj Maskę Obiektu", + "clickDrawPolygon": "Kliknij, aby narysować wielokąt na obrazie.", + "context": "Maski filtrujące obiekty są używane do filtrowania fałszywych detekcji dla danego typu obiektu na podstawie lokalizacji." + }, + "toast": { + "success": { + "copyCoordinates": "Skopiowano współrzędne dla {{polyName}} do schowka." + }, + "error": { + "copyCoordinatesFailed": "Nie udało się skopiować współrzędnych do schowka." + } + }, + "restart_required": "Wymagane ponowne uruchomienie (maski/strefy zmienione)", + "motionMaskLabel": "Maska Ruchu {{number}}", + "objectMaskLabel": "Maska Obiektu {{number}}" + }, + "debug": { + "objectList": "Lista Obiektów", + "debugging": "Debugowanie", + "title": "Debugowanie", + "detectorDesc": "Frigate używa twoich detektorów ({{detectors}}) do wykrywania obiektów w strumieniu wideo kamery.", + "boundingBoxes": { + "desc": "Pokaż ramki ograniczające wokół śledzonych obiektów", + "colors": { + "label": "Kolory Ramek Ograniczających Obiekty", + "info": "
  • Przy uruchomieniu, różne kolory zostaną przypisane do każdej etykiety obiektu
  • Ciemnoniebieska cienka linia oznacza, że obiekt nie jest wykrywany w tym momencie
  • Szara cienka linia oznacza, że obiekt jest wykrywany jako nieruchomy
  • Gruba linia oznacza, że obiekt jest przedmiotem automatycznego śledzenia (gdy włączone)
  • " + }, + "title": "Ramki ograniczające" + }, + "zones": { + "desc": "Pokaż kontur zdefiniowanych stref", + "title": "Strefy" + }, + "desc": "Widok debugowania pokazuje podgląd śledzonych obiektów w czasie rzeczywistym i ich statystyki. Lista obiektów pokazuje opóźnione podsumowanie wykrytych obiektów.", + "noObjects": "Brak obiektów", + "timestamp": { + "title": "Znacznik czasu", + "desc": "Nałóż znacznik czasu na obraz" + }, + "mask": { + "title": "Maski ruchu", + "desc": "Pokaż wielokąty maski ruchu" + }, + "motion": { + "title": "Ramki ruchu", + "desc": "Pokaż ramki wokół obszarów, gdzie wykryto ruch", + "tips": "

    Ramki Ruchu


    Czerwone ramki będą nakładane na obszary kadru, gdzie aktualnie wykrywany jest ruch

    " + }, + "regions": { + "title": "Regiony", + "desc": "Pokaż ramkę regionu zainteresowania wysyłanego do detektora obiektów", + "tips": "

    Ramki Regionów


    Jasnozielone ramki będą nakładane na obszary zainteresowania w kadrze, które są wysyłane do detektora obiektów.

    " + }, + "objectShapeFilterDrawing": { + "document": "Przeczytaj dokumentację ", + "title": "Rysowanie Filtra Kształtu Obiektu", + "ratio": "Proporcja", + "score": "Wynik", + "tips": "Włącz tę opcję, aby narysować prostokąt na obrazie kamery w celu pokazania jego obszaru i proporcji. Te wartości mogą być następnie użyte do ustawienia parametrów filtra kształtu obiektu w twojej konfiguracji.", + "desc": "Narysuj prostokąt na obrazie, aby zobaczyć szczegóły obszaru i proporcji", + "area": "Obszar" + }, + "openCameraWebUI": "Otwórz interfejs kamery {{camera}}", + "audio": { + "title": "Audio", + "noAudioDetections": "Nie wykryto dźwięku", + "score": "wynik", + "currentRMS": "Bieżąca moc RMS", + "currentdbFS": "Bieżące dbFS" + }, + "paths": { + "title": "Ścieżki", + "desc": "Pokaż punkty znaczące ścieżki dla śledzonego obiektu", + "tips": "

    Ścieżki


    Linie i koła wskażą punkty znaczące po których poruszał się obiekt podczas śledzenia.

    " + } + }, + "motionDetectionTuner": { + "title": "Tuner Wykrywania Ruchu", + "desc": { + "title": "Frigate używa wykrywania ruchu jako pierwszej linii sprawdzenia, czy w kadrze dzieje się coś wartego sprawdzenia przez detekcję obiektów.", + "documentation": "Przeczytaj Przewodnik Dostrajania Ruchu" + }, + "Threshold": { + "desc": "Wartość progowa określa, jak duża zmiana jasności piksela jest wymagana, aby uznać ją za ruch. Domyślnie: 30", + "title": "Próg" + }, + "contourArea": { + "title": "Obszar Konturu", + "desc": "Wartość obszaru konturu służy do określenia, które grupy zmienionych pikseli kwalifikują się jako ruch. Domyślnie: 10" + }, + "improveContrast": { + "desc": "Popraw kontrast dla ciemniejszych scen. Domyślnie: WŁĄCZONE", + "title": "Popraw Kontrast" + }, + "toast": { + "success": "Ustawienia ruchu zostały zapisane." + }, + "unsavedChanges": "Niezapisane zmiany w korektorze ruchu ({{camera}})" + }, + "users": { + "addUser": "Dodaj Użytkownika", + "updatePassword": "Resetuj Hasło", + "toast": { + "success": { + "createUser": "Użytkownik {{user}} został utworzony pomyślnie", + "deleteUser": "Użytkownik {{user}} został usunięty pomyślnie", + "updatePassword": "Hasło zaktualizowane pomyślnie.", + "roleUpdated": "Rola zaktualizowana dla {{user}}" + }, + "error": { + "setPasswordFailed": "Nie udało się zapisać hasła: {{errorMessage}}", + "createUserFailed": "Nie udało się utworzyć użytkownika: {{errorMessage}}", + "deleteUserFailed": "Nie udało się usunąć użytkownika: {{errorMessage}}", + "roleUpdateFailed": "Nie udało się zaktualizować roli: {{errorMessage}}" + } + }, + "table": { + "username": "Nazwa użytkownika", + "actions": "Akcje", + "role": "Rola", + "noUsers": "Nie znaleziono użytkowników.", + "changeRole": "Zmień rolę użytkownika", + "password": "Resetuj hasło", + "deleteUser": "Usuń użytkownika" + }, + "dialog": { + "form": { + "user": { + "title": "Nazwa użytkownika", + "desc": "Dozwolone są tylko litery, cyfry, kropki i podkreślenia.", + "placeholder": "Wprowadź nazwę użytkownika" + }, + "password": { + "strength": { + "strong": "Silne", + "title": "Siła hasła: ", + "weak": "Słabe", + "medium": "Średnie", + "veryStrong": "Bardzo silne" + }, + "match": "Hasła pasują", + "confirm": { + "placeholder": "Potwierdź hasło", + "title": "Potwierdź hasło" + }, + "title": "Hasło", + "placeholder": "Wprowadź hasło", + "notMatch": "Hasła nie pasują", + "show": "Pokaż hasło", + "hide": "Ukryj hasło", + "requirements": { + "title": "Wymagania hasła:", + "length": "Co najmniej 12 znaków", + "uppercase": "Co najmniej jedna duża litera", + "digit": "Co najmniej jedna cyfra", + "special": "Co najmniej jeden znak specjalny (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "placeholder": "Wprowadź nowe hasło", + "title": "Nowe hasło", + "confirm": { + "placeholder": "Wprowadź ponownie nowe hasło" + } + }, + "usernameIsRequired": "Nazwa użytkownika jest wymagana", + "passwordIsRequired": "Hasło jest wymagane", + "currentPassword": { + "title": "Aktualne hasło", + "placeholder": "Wprowadź aktualne hasło" + } + }, + "changeRole": { + "desc": "Aktualizuj uprawnienia dla {{username}}", + "roleInfo": { + "intro": "Wybierz właściwą rolę dla tego użytkownika:", + "admin": "Admin", + "adminDesc": "Pełny dostęp do wszystkich funkcjonalności.", + "viewerDesc": "Ograniczony wyłącznie do pulpitów na żywo, przeglądania, eksploracji i eksportu.", + "viewer": "Przeglądający", + "customDesc": "Własna rola z dedykowanym dostępem do kamery." + }, + "title": "Zmień rolę użytkownika", + "select": "Wybierz role" + }, + "createUser": { + "title": "Utwórz nowego użytkownika", + "desc": "Dodaj nowe konto użytkownika i określ rolę dla dostępu do obszarów interfejsu Frigate.", + "usernameOnlyInclude": "Nazwa użytkownika może zawierać tylko litery, cyfry lub znak _", + "confirmPassword": "Proszę potwierdź swoje hasło" + }, + "deleteUser": { + "title": "Usuń użytkownika", + "desc": "Tej akcji nie można cofnąć. Spowoduje to trwałe usunięcie konta użytkownika i wszystkich powiązanych danych.", + "warn": "Czy na pewno chcesz usunąć {{username}}?" + }, + "passwordSetting": { + "updatePassword": "Aktualizuj hasło dla {{username}}", + "setPassword": "Ustaw hasło", + "desc": "Utwórz silne hasło, aby zabezpieczyć to konto.", + "cannotBeEmpty": "Hasło nie może być puste", + "doNotMatch": "Hasła nie pasują do siebie", + "currentPasswordRequired": "Wymagane jest aktualne hasło", + "incorrectCurrentPassword": "Aktualne hasło jest nieprawidłowe", + "passwordVerificationFailed": "Nie udało się zweryfikować hasła", + "multiDeviceWarning": "Wszystkie inne urządzenia, na których jesteś zalogowany, będą wymagały ponownego zalogowania się w ciągu {{refresh_time}}.", + "multiDeviceAdmin": "Możesz również wymusić natychmiastowe ponowne uwierzytelnienie wszystkich użytkowników poprzez zmianę sekretu JWT." + } + }, + "management": { + "title": "Zarządzanie Użytkownikami", + "desc": "Zarządzaj kontami użytkowników tej instancji Frigate." + }, + "title": "Użytkownicy" + }, + "notification": { + "title": "Powiadomienia", + "notificationSettings": { + "title": "Ustawienia powiadomień", + "desc": "Frigate może wysyłać natywne powiadomienia push na twoje urządzenie, gdy działa w przeglądarce lub jest zainstalowany jako PWA.", + "documentation": "Przeczytaj dokumentację" + }, + "notificationUnavailable": { + "title": "Powiadomienia niedostępne", + "desc": "Powiadomienia push w przeglądarce wymagają bezpiecznego kontekstu (https://…). To jest ograniczenie przeglądarki. Uzyskaj dostęp do Frigate przez bezpieczne połączenie, aby korzystać z powiadomień.", + "documentation": "Przeczytaj dokumentację" + }, + "globalSettings": { + "title": "Ustawienia globalne", + "desc": "Tymczasowo wstrzymaj powiadomienia dla określonych kamer na wszystkich zarejestrowanych urządzeniach." + }, + "suspendTime": { + "12hours": "Zawieś na 12 godzin", + "24hours": "Zawieś na 24 godziny", + "untilRestart": "Zawieś do restartu", + "1hour": "Zawieś na 1 godzinę", + "5minutes": "Zawieś na 5 minut", + "10minutes": "Zawieś na 10 minut", + "30minutes": "Zawieś na 30 minut", + "suspend": "Wstrzymaj" + }, + "cancelSuspension": "Anuluj zawieszenie", + "toast": { + "error": { + "registerFailed": "Nie udało się zapisać rejestracji powiadomień." + }, + "success": { + "settingSaved": "Ustawienia powiadomień zostały zapisane.", + "registered": "Rejestracja powiadomień zakończona powodzeniem. Przed wysłaniem jakichkolwiek powiadomień (włącznie z testowym) wymagane jest ponowne uruchomienie Frigate." + } + }, + "email": { + "title": "Email", + "placeholder": "np. przyklad@email.com", + "desc": "Wymagany jest prawidłowy adres email, który będzie używany do powiadamiania Cię w przypadku problemów z usługą push." + }, + "cameras": { + "title": "Kamery", + "noCameras": "Brak dostępnych kamer", + "desc": "Wybierz kamery, dla których chcesz włączyć powiadomienia." + }, + "deviceSpecific": "Ustawienia specyficzne dla urządzenia", + "registerDevice": "Zarejestruj to urządzenie", + "active": "Powiadomienia aktywne", + "suspended": "Powiadomienia zawieszone {{time}}", + "unregisterDevice": "Wyrejestruj to urządzenie", + "sendTestNotification": "Wyślij testowe powiadomienie", + "unsavedRegistrations": "Niezapisane ustawienia rejestracji powiadomień", + "unsavedChanges": "Niezapisane zmiany ustawień powiadomień" + }, + "frigatePlus": { + "title": "Ustawienia Frigate+", + "apiKey": { + "title": "Klucz API Frigate+", + "validated": "Klucz API Frigate+ został wykryty i zweryfikowany", + "plusLink": "Dowiedz się więcej o Frigate+", + "notValidated": "Klucz API Frigate+ nie został wykryty lub nie został zweryfikowany", + "desc": "Klucz API Frigate+ umożliwia integrację z usługą Frigate+." + }, + "snapshotConfig": { + "title": "Konfiguracja zrzutów ekranu", + "documentation": "Przeczytaj dokumentację", + "desc": "Aby wysyłać dane do Frigate+, w konfiguracji muszą być włączone zarówno zwykłe zrzuty ekranu, jak i zrzuty typu clean_copy.", + "table": { + "snapshots": "Zrzuty ekranu", + "cleanCopySnapshots": "Zrzuty ekranu clean_copy", + "camera": "Kamera" + }, + "cleanCopyWarning": "Niektóre kamery mają wyłączone migawki" + }, + "modelInfo": { + "title": "Informacje o modelu", + "modelType": "Typ modelu", + "trainDate": "Data treningu", + "supportedDetectors": "Wspierane detektory", + "dimensions": "Wymiary", + "cameras": "Kamery", + "loading": "Ładowanie informacji o modelu…", + "error": "Nie udało się załadować informacji o modelu", + "availableModels": "Dostępne modele", + "loadingAvailableModels": "Ładowanie dostępnych modeli…", + "baseModel": "Model bazowy", + "modelSelect": "Tutaj możesz wybrać swoje dostępne modele w Frigate+. Pamiętaj, że można wybrać tylko modele kompatybilne z Twoją aktualną konfiguracją detektora.", + "plusModelType": { + "baseModel": "Model bazowy", + "userModel": "Dostrojony" + } + }, + "toast": { + "success": "Ustawienia Frigate+ zostały zapisane. Uruchom ponownie Frigate, aby zastosować zmiany.", + "error": "Nie udało się zapisać zmian konfiguracji: {{errorMessage}}" + }, + "restart_required": "Wymagane ponowne uruchomienie (Zmieniony model Frigate+)", + "unsavedChanges": "Niezapisane zmiany ustawień Frigate+" + }, + "enrichments": { + "faceRecognition": { + "title": "Rozpoznawanie twarzy", + "desc": "Rozpoznawanie twarzy pozwala na przypisywanie imion osobom, a gdy ich twarz zostanie rozpoznana, Frigate przypisze imię osoby jako sub label. Ta informacja jest wyświetlana w interfejsie, filtrach oraz powiadomieniach.", + "readTheDocumentation": "Przeczytaj Dokumentację", + "modelSize": { + "label": "Rozmiar modelu", + "desc": "Rozmiar modelu używanego do rozpoznawania twarzy.", + "small": { + "title": "mały", + "desc": "Użycie opcji mały wykorzystuje model osadzeń twarzy FaceNet, który działa wydajnie na większości procesorów." + }, + "large": { + "title": "duży", + "desc": "Użycie opcji duży wykorzystuje model osadzeń twarzy ArcFace i automatycznie uruchomi się na karcie graficznej, jeśli jest dostępna." + } + } + }, + "title": "Ustawienia wzbogaceń", + "unsavedChanges": "Niezapisane zmiany ustawień wzbogacania", + "birdClassification": { + "title": "Klasyfikacja ptaków", + "desc": "Klasyfikacja ptaków identyfikuje znane gatunki przy użyciu skwantyzowanego modelu Tensorflow. Gdy rozpoznany zostanie znany ptak, jego zwyczajowa nazwa zostanie dodana jako sub_label. Ta informacja jest wyświetlana w interfejsie, filtrach oraz powiadomieniach." + }, + "semanticSearch": { + "title": "Wyszukiwanie semantyczne", + "desc": "Wyszukiwanie semantyczne w Frigate pozwala na znajdowanie śledzonych obiektów w elementach przeglądowych za pomocą samego obrazu, opisu tekstowego zdefiniowanego przez użytkownika lub automatycznie wygenerowanego.", + "readTheDocumentation": "Przeczytaj Dokumentację", + "reindexNow": { + "label": "Reindeksuj teraz", + "desc": "Reindeksowanie zregeneruje osadzenia dla wszystkich śledzonych obiektów. Ten proces działa w tle i może maksymalnie obciążyć procesor oraz zająć sporo czasu w zależności od liczby śledzonych obiektów.", + "confirmTitle": "Potwierdź reindeksowanie", + "confirmDesc": "Czy na pewno chcesz reindeksować wszystkie osadzenia śledzonych obiektów? Ten proces będzie działał w tle, ale może maksymalnie obciążyć procesor i zająć sporo czasu. Postęp możesz śledzić na stronie Eksploruj.", + "confirmButton": "Reindeksuj", + "success": "Reindeksowanie zostało pomyślnie uruchomione.", + "alreadyInProgress": "Reindeksowanie już trwa.", + "error": "Nie udało się uruchomić reindeksowania: {{errorMessage}}" + }, + "modelSize": { + "label": "Rozmiar modelu", + "desc": "Rozmiar modelu używanego do osadzeń wyszukiwania semantycznego.", + "small": { + "title": "mały", + "desc": "Użycie opcji mały wykorzystuje skwantyzowaną wersję modelu, która zużywa mniej pamięci RAM i działa szybciej na procesorze przy zaniedbywalnej różnicy w jakości osadzeń." + }, + "large": { + "title": "duży", + "desc": "Użycie opcji duży wykorzystuje pełny model Jina i automatycznie uruchomi się na karcie graficznej, jeśli jest dostępna." + } + } + }, + "licensePlateRecognition": { + "title": "Rozpoznawanie tablic rejestracyjnych", + "desc": "Frigate może rozpoznawać tablice rejestracyjne na pojazdach i automatycznie dodawać wykryte znaki do pola recognized_license_plate lub znaną nazwę jako sub_label do obiektów typu samochód. Typowy przypadek użycia to odczytywanie tablic rejestracyjnych samochodów wjeżdżających na podjazd lub przejeżdżających ulicą.", + "readTheDocumentation": "Przeczytaj Dokumentację" + }, + "restart_required": "Wymagany restart (zmieniono ustawienia wzbogacania)", + "toast": { + "success": "Ustawienia wzbogacania zostały zapisane. Uruchom ponownie Frigate, aby zastosować zmiany.", + "error": "Nie udało się zapisać zmian konfiguracji: {{errorMessage}}" + } + }, + "roles": { + "management": { + "title": "Zarządzanie rolami podglądu", + "desc": "Zarządzaj własnymi rolami podglądu i ich dostępem do kamer dla tej instancji Frigate." + }, + "addRole": "Dodaj rolę", + "table": { + "role": "Rola", + "cameras": "Kamery", + "actions": "Akcje", + "noRoles": "Brak własnych ról.", + "editCameras": "Edytuj kamery", + "deleteRole": "Usuń rolę" + }, + "toast": { + "success": { + "createRole": "Utworzono rolę {{role}}", + "updateCameras": "Zaktualizowano kamery dla roli {{role}}", + "deleteRole": "Rola {{role}} została usunięta", + "userRolesUpdated_one": "{{count}} użytkownik przypisany do tej roli został zaktualizowany do roli 'viewer', która ma dostęp do wszystkich kamer.", + "userRolesUpdated_few": "{{count}} użytkowników przypisanych do tej roli zostało zaktualizowanych do roli 'viewer', która ma dostęp do wszystkich kamer.", + "userRolesUpdated_many": "{{count}} użytkowników przypisanych do tej roli zostało zaktualizowanych do roli 'viewer', która ma dostęp do wszystkich kamer." + }, + "error": { + "createRoleFailed": "Nie udało się utworzyć roli: {{errorMessage}}", + "updateCamerasFailed": "Nie udało się zaktualizować kamery: {{errorMessage}}", + "deleteRoleFailed": "Nie udało się usunąć roli: {{errorMessage}}", + "userUpdateFailed": "Nie udało się zaktualizować ról użytkownika: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Dodaj nową rolę", + "desc": "Dodaj nową rolę i określ prawa dostępu do kamer." + }, + "editCameras": { + "title": "Edytuj kamery roli", + "desc": "Aktualizuj dostęp do kamer dla roli {{role}}." + }, + "deleteRole": { + "title": "Usuń rolę", + "desc": "Ta akcja nie może zostać wycofana. To usunie rolę na stałe i przypisze jej użytkowników do roli 'viewer' która ma dostęp do wszystkich kamer.", + "warn": "Czy na pewno chcesz usunąć rolę {{role}}?", + "deleting": "Usuwanie..." + }, + "form": { + "role": { + "title": "Nazwa roli", + "placeholder": "Wprowadź nazwę roli", + "desc": "Tylko litery, liczby, kropki i podkreślenie są dozwolone.", + "roleIsRequired": "Nazwa roli jest wymagana", + "roleOnlyInclude": "Nazwa roli może zawierać litery, liczby, . albo _", + "roleExists": "Taka rola już istnieje." + }, + "cameras": { + "title": "Kamery", + "desc": "Wybierz do jakich kamer ta rola ma dostęp. Wymagana jest przynajmniej jedna kamera.", + "required": "Przynajmniej jedna kamera musi zostać wybrana." + } + } + } + }, + "triggers": { + "documentTitle": "Wyzwalacze", + "management": { + "title": "Wyzwalacze", + "desc": "Zarządzaj wyzwalaczami dla kamery {{camera}}. Użyj typu miniatury, aby aktywować miniatury podobne do wybranego śledzonego obiektu, i typu opisu, aby aktywować opisy podobne do określonego tekstu." + }, + "addTrigger": "Dodaj wyzwalacz", + "table": { + "name": "Nazwa", + "type": "Typ", + "content": "Zawartość", + "threshold": "Próg", + "actions": "Akcje", + "noTriggers": "Brak wyzwalaczy dla tej kamery.", + "edit": "Edytuj", + "deleteTrigger": "Usuń wyzwalacz", + "lastTriggered": "Ostatnio wyzwolony" + }, + "type": { + "thumbnail": "Miniaturka", + "description": "Opis" + }, + "actions": { + "alert": "Oznacz jako alarm", + "notification": "Wyślij powiadomienie", + "sub_label": "Dodaj podetykietę", + "attribute": "Dodaj atrybut" + }, + "dialog": { + "createTrigger": { + "title": "Utwórz wyzwalacz", + "desc": "Utwórz wyzwalacz dla kamery {{camera}}" + }, + "editTrigger": { + "title": "Edytuj wyzwalacz", + "desc": "Edytuj ustawienia wyzwalacza na kamerze {{camera}}" + }, + "deleteTrigger": { + "title": "Usuń wyzwalacz", + "desc": "Czy na pewno chcesz usunąć wyzwalacz {{triggerName}}? To działanie jest nieodwracalne." + }, + "form": { + "name": { + "title": "Nazwa", + "placeholder": "Wprowadź nazwę wyzwalacza", + "error": { + "minLength": "Pole musi mieć co najmniej 2 znaki.", + "invalidCharacters": "Pole może zawierać jedynie litery, liczby, podkreślenie i myślniki.", + "alreadyExists": "Wyzwalacz o tej nazwie istnieje już dla tej kamery." + }, + "description": "Wprowadź unikalną nazwę lub opis, aby zidentyfikować ten wyzwalacz" + }, + "enabled": { + "description": "Włącz lub wyłącz ten wyzwalacz" + }, + "type": { + "title": "Typ", + "placeholder": "Wybierz typ wyzwalacza", + "description": "Uruchom, gdy wykryty zostanie podobny opis śledzonego obiektu", + "thumbnail": "Uruchom, gdy wykryta zostanie podobna miniatura śledzonego obiektu" + }, + "content": { + "title": "Zawartość", + "imagePlaceholder": "Wybierz miniaturkę", + "textPlaceholder": "Wprowadź treść", + "imageDesc": "Wyświetlane jest tylko 100 najnowszych miniatur. Jeśli nie możesz znaleźć żądanej miniatury, przejrzyj wcześniejsze obiekty w sekcji Eksploruj i skonfiguruj wyzwalacz z menu w tym miejscu.", + "textDesc": "Wprowadź tekst, który spowoduje uruchomienie tej akcji po wykryciu podobnego opisu śledzonego obiektu.", + "error": { + "required": "Zawartość jest wymagana." + } + }, + "threshold": { + "title": "Próg", + "error": { + "min": "Próg musi wynosić co najmniej 0", + "max": "Próg nie może być większy niż 1" + }, + "desc": "Ustaw próg podobieństwa dla tego wyzwalacza. Wyższy próg oznacza, że do uruchomienia wyzwalacza wymagane jest większe dopasowanie." + }, + "actions": { + "title": "Akcje", + "desc": "Domyślnie Frigate wysyła komunikat MQTT dla wszystkich wyzwalaczy. Podetykiety dodają nazwę wyzwalacza do etykiety obiektu. Atrybuty to metadane, które można przeszukiwać, przechowywane oddzielnie w metadanych śledzonego obiektu.", + "error": { + "min": "Musisz wybrać co najmniej jedną akcję." + } + }, + "friendly_name": { + "title": "Przyjazna nazwa", + "placeholder": "Nazwij lub opisz ten trigger", + "description": "Opcjonalna przyjazna nazwa lub opis tego triggera." + } + } + }, + "toast": { + "success": { + "createTrigger": "Utworzono wyzwalacz {{name}}.", + "updateTrigger": "Zaktualizowano wyzwalacz {{name}}.", + "deleteTrigger": "Usunięto wyzwalacz {{name}}." + }, + "error": { + "createTriggerFailed": "Nie udało się utworzyć wyzwalacza: {{errorMessage}}", + "updateTriggerFailed": "Nie udało się zaktualizować wyzwalacza: {{errorMessage}}", + "deleteTriggerFailed": "Nie udało się usunąć wyzwalacza: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Wyszukiwanie semantyczne jest zablokowane", + "desc": "Wyszukiwanie semantyczne musi być włączone, aby korzystać z triggerów." + }, + "wizard": { + "title": "Utwórz wyzwalacz", + "step1": { + "description": "Skonfiguruj podstawowe ustawienia wyzwalacza." + }, + "step2": { + "description": "Skonfiguruj treść, która uruchomi tę akcję." + }, + "step3": { + "description": "Skonfiguruj próg i działania dla tego wyzwalacza." + }, + "steps": { + "nameAndType": "Nazwa i typ", + "configureData": "Skonfiguruj dane", + "thresholdAndActions": "Próg i akcje" + } + } + }, + "cameraWizard": { + "title": "Dodaj kamerę", + "steps": { + "streamConfiguration": "Konfiguracja strumienia", + "nameAndConnection": "Nazwa i połączenie", + "probeOrSnapshot": "Sonda lub migawka", + "validationAndTesting": "Walidacja i testowanie" + }, + "save": { + "success": "Zapisano ustawienia nowej kamery {{cameraName}}.", + "failure": "Błąd zapisu {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Rozdzielczość", + "fps": "kl./s", + "video": "Wideo", + "audio": "Audio" + }, + "commonErrors": { + "noUrl": "Podaj poprawny adres URL", + "testFailed": "Negatywny wynik testu strumienia: {{error}}" + }, + "step1": { + "cameraName": "Nazwa kamery", + "cameraNamePlaceholder": "np. drzwi_frontowe lub Ogród", + "host": "Host/Adres IP", + "port": "Port", + "username": "Nazwa użytkownika", + "usernamePlaceholder": "Opcjonalne", + "password": "Hasło", + "passwordPlaceholder": "Opcjonalne", + "selectTransport": "Wybierz protokół warstwy transportowej", + "cameraBrand": "Marka Kamery", + "selectBrand": "Wybierz markę kamery aby dostosować wzór adresu URL", + "customUrl": "Niestandardowy adres URL strumienia", + "brandInformation": "Informacje o marce", + "brandUrlFormat": "Dla kamer z formatem RTSP, formatuj URL jako: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nazwa_użytkownika:hasło@host:port/scieżka", + "connectionSettings": "Ustawienia Połączenia", + "detectionMethod": "Metoda wykrywania strumienia", + "onvifPort": "Port ONVIF", + "manualMode": "Ręczny wybór", + "onvifPortDescription": "Dla kamer wspierających protokół ONVIF, port to zazwyczaj 80 lub 8080.", + "errors": { + "brandOrCustomUrlRequired": "Wybierz markę kamery oraz host/adres IP lub wybierz 'Inny' i podaj niestandardowy adres URL", + "nameRequired": "Wymagana nazwa kamery", + "nameLength": "Nazwa kamery musi mieć 64 lub mniej znaków", + "invalidCharacters": "Nazwa kamery zawiera niepoprawne znaki", + "nameExists": "Nazwa kamery jest już zajęta", + "customUrlRtspRequired": "Niestandardowe adresy URL muszą zaczynać się od \"rtsp://\". Ręczna konfiguracja wymagana jest dla strumieniów innych niż RTSP." + }, + "description": "Wprowadź szczegóły kamery i wybierz autodetekcję lub ręcznie wybierz firmę.", + "probeMode": "Wykryj kamerę", + "detectionMethodDescription": "Wykryj kamerę za pomocą ONVIF (jeśli wspierane) by znaleźć adresy strumieni lub wybierz ręcznie markę kamery by wybrać predefiniowane adresy. By wpisać własny adres strumienia RTSP użyj ręcznej metody i wybierz \"Inne\".", + "useDigestAuth": "Użyj przesyłania skrótu autentykacji", + "useDigestAuthDescription": "Użyj przesyłania skrótu logowania HTTP dla ONVIF. Niektóre kamery mogą wymagać dedykowanego użytkownika i hasła ONVIF zamiast standardowego konta admin." + }, + "step2": { + "testSuccess": "Test połączenia udany!", + "testFailed": "Test połączenia nieudany. Sprawdź adres źródła obrazu i spróbuj ponownie.", + "testFailedTitle": "Test Nieudany", + "streamDetails": "Szczegóły Strumienia", + "testing": { + "fetchingSnapshot": "Przygotowywanie migawki kamery...", + "probingMetadata": "Wykrywanie metadanych kamery..." + }, + "deviceInfo": "Informacje o urządzeniu", + "manufacturer": "Producent", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profile", + "ptzSupport": "Wsparcie PTZ", + "autotrackingSupport": "Wsparcie auto-śledzenia", + "uriCopy": "Kopiuj", + "uriCopied": "Adres URL skopiowano do schowka", + "testConnection": "Przetestuj połączenie", + "errors": { + "hostRequired": "Wymagany jest Host/Adres IP" + }, + "description": "Wykryj dostępne strumienie kamery lub skonfiguruj ręcznie ustawienia na podstawie wybranej metody detekcji.", + "probing": "Wykrywanie kamery...", + "retry": "Ponów", + "probeFailed": "Błąd wykrywania kamery: {{error}}", + "probingDevice": "Wykrywanie urządzenia...", + "probeSuccessful": "Wykrywanie udane", + "probeError": "Błąd wykrywania", + "probeNoSuccess": "Niepowodzenie wykrywania", + "presets": "Ustawienia wstępne", + "rtspCandidates": "Kandydaci RTSP", + "rtspCandidatesDescription": "W wyniku sprawdzania kamery znaleziono następujące adresy URL RTSP. Przetestuj połączenie, aby wyświetlić metadane strumienia.", + "noRtspCandidates": "Nie znaleziono adresów URL RTSP z kamery. Twoje dane uwierzytelniające mogą być nieprawidłowe lub kamera może nie obsługiwać protokołu ONVIF lub metody używanej do pobierania adresów URL RTSP. Wróć i wprowadź adres URL RTSP ręcznie.", + "candidateStreamTitle": "Kandydat {{number}}", + "useCandidate": "Użyj", + "toggleUriView": "Kliknij, aby przełączyć widok pełnego adresu URI", + "connected": "Połączony", + "notConnected": "Niepołączony" + }, + "step3": { + "streamTitle": "Strumień numer: {{number}}", + "streamUrl": "URL strumienia", + "streamUrlPlaceholder": "rtsp://nazwa_użytkownika:hasło@host:port/scieżka", + "selectStream": "Wybierz strumień", + "noStreamFound": "Nie znaleziono żadnego strumienia", + "url": "adres URL", + "resolution": "Rozdzielczość", + "selectResolution": "Wybierz rozdzielczość", + "quality": "Jakość", + "selectQuality": "wybierz jakość", + "roles": "Role", + "roleLabels": { + "detect": "Wykrywanie obiektów", + "record": "Nagrywanie", + "audio": "Dźwięk" + }, + "testStream": "Przetestuj połączenie", + "testSuccess": "Test strumienia udany!", + "testFailed": "Test strumienia nieudany", + "testFailedTitle": "Test nieudany", + "connected": "Połączono", + "notConnected": "Nie połączono", + "featuresTitle": "Funkcje", + "go2rtc": "Ogranicz połączenia do kamery", + "detectRoleWarning": "Przynajmniej jeden strumień musi mieć rolę \"detect\".", + "rolesPopover": { + "title": "Role strumienia", + "detect": "Główny strumień służący do wykrywania obiektów.", + "record": "Zapisuje fragmenty strumienia wideo zgodnie z ustawieniami konfiguracyjnymi.", + "audio": "Kanał do wykrywania opartego na dźwięku." + }, + "featuresPopover": { + "title": "Funkcje strumienia", + "description": "Użyj funkcji ponownego przesyłania strumienia go2rtc, aby zmniejszyć liczbę połączeń z kamerą." + }, + "description": "Skonfiguruj role strumieni i dodaj dodatkowe strumienie dla swojej kamery.", + "streamsTitle": "Strumienie kamery", + "addStream": "Dodaj strumień", + "addAnotherStream": "Dodaj kolejny strumień", + "searchCandidates": "Szukaj kandydatów..." + }, + "step4": { + "description": "Końcowa walidacja i analiza przed zapisaniem ustawień nowej kamery. Połącz się z każdym strumieniem przed zapisaniem.", + "validationTitle": "Walidacja strumienia", + "reconnectionSuccess": "Ponowna próba połączenia udana.", + "streamUnavailable": "Podgląd strumienia niedostępny", + "connecting": "Łączenie...", + "streamTitle": "Strumień numer: {{number}}", + "valid": "Poprawny", + "connectingStream": "Łączenie", + "disconnectStream": "Rozłącz", + "estimatedBandwidth": "Przewidywana przepustowość", + "roles": "Role", + "ffmpegModuleDescription": "Jeżeli po kilku próbach strumień nadal nie ładuje się, uruchom ten tryb. Gdy włączony jest ten tryb Frigate będzie używać modułu ffmpeg z go2rtc. Może to zapewnić lepszą kompatybilność z niektórymi typami strumieniów.", + "none": "Brak", + "error": "Błąd", + "streamValidated": "Strumień numer: {{number}} przeszedł test pozytywnie", + "streamValidationFailed": "Strumień numer: {{number}} test nieudany", + "saveAndApply": "Zapisz nową kamerę", + "saveError": "Nieprawidłowa konfiguracja. Sprawdź ustawienia.", + "issues": { + "title": "Walidacja strumienia", + "audioCodecGood": "Kodek dźwięku to {{codec}}.", + "resolutionHigh": "Rozdzielczość {{resolution}} może spowodować większe zużycie zasobów.", + "resolutionLow": "Rozdzielczość {{resolution}} może okazać się za mała aby poprawnie wykrywać małe obiekty.", + "noAudioWarning": "Nie wykryto dźwięku dla tego strumienia, nagrania również nie będą zawierać dźwięku.", + "audioCodecRecordError": "Kodek AAC jest wymagany aby uwzględnić dźwięk w nagraniach.", + "audioCodecRequired": "Strumień audio jest wymagany aby umożliwić wykrywanie dźwięku.", + "restreamingWarning": "Ograniczenie ilości połączeń do strumienia nagrań może delikatnie zwiększyć użycie procesora.", + "brands": { + "reolink-rtsp": "Strumień RTSP dla kamer firmy Reolink nie jest rekomendowany. Uruchom strumień HTTP w oprogramowaniu kamery i uruchom kreator jeszcze raz.", + "reolink-http": "Strumienie HTTP Reolink powinny korzystać z FFmpeg w celu zapewnienia lepszej kompatybilności. Włącz opcję 'Użyj trybu kompatybilności strumienia' dla tego strumienia." + }, + "videoCodecGood": "Kodek wideo to {{codec}}.", + "dahua": { + "substreamWarning": "Podstrumień 1 jest zablokowany na niskiej rozdzielczości. Wiele kamer Dahua / Amcrest / EmpireTech obsługuje dodatkowe podstrumienie, które należy włączyć w ustawieniach kamery. Zaleca się sprawdzenie i wykorzystanie tych strumieni, jeśli są dostępne." + }, + "hikvision": { + "substreamWarning": "Podstrumień 1 jest zablokowany na niskiej rozdzielczości. Wiele kamer Hikvision obsługuje dodatkowe podstrumienie, które należy włączyć w ustawieniach kamery. Zaleca się sprawdzenie i wykorzystanie tych strumieni, jeśli są dostępne." + } + }, + "connectAllStreams": "Podłącz wszystkie strumienie", + "reconnectionPartial": "Niektóre strumienie nie zostały ponownie połączone.", + "reload": "Przeładuj", + "failed": "Nie powiodło się", + "notTested": "Nietestowane", + "connectStream": "Połącz", + "ffmpegModule": "Użyj trybu zgodności strumienia" + }, + "description": "Wykonaj poniższe kroki aby dodać nową kamerę do Frigate." + }, + "cameraManagement": { + "title": "Zarządzaj kamerami", + "addCamera": "Dodaj nową kamerę", + "editCamera": "Edytuj kamerę:", + "selectCamera": "Wybierz kamerę", + "backToSettings": "Powrót do ustawień kamery", + "streams": { + "title": "Włącz / Wyłącz kamery", + "desc": "Tymczasowo wyłącz kamerę do momentu ponownego uruchomienia Frigate. Wyłączenie kamery całkowicie zatrzymuje przetwarzanie strumieni z tej kamery przez program Frigate. Funkcje detekcji, nagrywania i debugowania będą niedostępne.
    Uwaga: nie powoduje to wyłączenia strumieni go2rtc." + }, + "cameraConfig": { + "add": "Dodaj kamerę", + "edit": "Edytuj kamerę", + "description": "Skonfiguruj ustawienia kamery, wliczając strumienie wejściowe i ich role.", + "name": "Nazwa kamery", + "nameRequired": "Wymagana nazwa kamery", + "nameLength": "Nazwa kamery musi mieć 64 lub mniej znaków.", + "namePlaceholder": "np. drzwi_frontowe lub Ogród", + "enabled": "Włączone", + "ffmpeg": { + "inputs": "Strumienie wejściowe", + "path": "Ścieżka strumienia", + "pathRequired": "Ścieżka strumienia jest wymagana", + "pathPlaceholder": "rtsp://...", + "roles": "Role", + "rolesRequired": "Wymagana jest przynajmniej jedna rola", + "rolesUnique": "Każda rola ('audio', 'detect', 'record') może zostać przypisana tylko raz", + "addInput": "Dodaj strumień wejściowy", + "removeInput": "Usuń strumień wejściowy", + "inputsRequired": "Wymagany jest przynajmniej jeden strumień wejściowy" + }, + "go2rtcStreams": "Strumienie go2rtc", + "streamUrls": "Adresy URL strumieni", + "addUrl": "Dodaj adres URL", + "addGo2rtcStream": "Dodaj strumień go2rtc", + "toast": { + "success": "Zapisano poprawnie kamerę {{cameraName}}" + } + } + }, + "cameraReview": { + "review": { + "alerts": "Alerty ", + "detections": "Wykrycia ", + "title": "Recenzja", + "desc": "Tymczasowo włącz/wyłącz alerty i wykrywania dla tej kamery do momentu ponownego uruchomienia programu Frigate. Po wyłączeniu nie będą generowane żadne nowe elementy do przeglądu. " + }, + "reviewClassification": { + "title": "Przegląd klasyfikacji", + "noDefinedZones": "Nie zdefiniowano żadnych stref dla tej kamery.", + "objectDetectionsTips": "Wszystkie obiekty w kategorii {{detectionsLabels}} wykryte przez kamerę {{cameraName}} będą wyświetlane jako Wykrycia niezależnie od strefy w której zostały wykryte.", + "zoneObjectDetectionsTips": { + "text": "Wszystkie obiekty w kategorii {{detectionsLabels}} nieskategoryzowane w strefie {{zone}} kamery {{cameraName}} będą wyświetlane jako Wykrycia.", + "notSelectDetections": "Wszystkie obiekty w kategorii {{detectionsLabels}} wykryte w strefie {{zone}} kamery {{cameraName}} nieskategoryzowane jako Alerty będą wyświetlane jako Wykrycia, niezależnie w której strefie zostaną wykryte.", + "regardlessOfZoneObjectDetectionsTips": "Wszystkie obiekty w kategorii {{detectionsLabels}} nieskategoryzowane dla kamery {{cameraName}} będą wyświetlane jako Wykrycia niezależnie w której strefie zostaną wykryte." + }, + "unsavedChanges": "Niezapisane ustawienia klasyfikacji przeglądu dla kamery {{camera}}", + "selectAlertsZones": "Wybierz strefę dla Alertów", + "selectDetectionsZones": "Wybierz strefę dla Wykryć", + "limitDetections": "Ogranicz detekcje do konkretnych stref", + "desc": "Frigate dzieli elementy do przeglądu na alarmy i detekcje. Domyślnie wszystkie obiekty typu osoba i samochód są traktowane jako alerty. Możesz doprecyzować kategoryzację elementów do przeglądu, konfigurując dla nich wymagane strefy.", + "objectAlertsTips": "Wszystkie obiekty {{alertsLabels}} w {{cameraName}} będą wyświetlane jako alarmy.", + "zoneObjectAlertsTips": "Wszystkie obiekty {{alertsLabels}} wykryte w {{zone}} na {{cameraName}} zostaną wyświetlone jako alarmy.", + "toast": { + "success": "Konfiguracja klasyfikacji została zapisana. Uruchom ponownie program Frigate, aby zastosować zmiany." + } + }, + "title": "Ustawienia przeglądu kamery", + "object_descriptions": { + "title": "Generatywne opisy obiektów AI", + "desc": "Tymczasowo włącz/wyłącz generatywne opisy obiektów AI dla tej kamery. Po wyłączeniu funkcja nie będzie generować opisów AI dla obiektów śledzonych przez tę kamerę." + }, + "review_descriptions": { + "title": "Opisy generatywnej sztucznej inteligencji", + "desc": "Tymczasowo włącz/wyłącz generatywne opisy AI dla tej kamery. Po wyłączeniu opisy generowane przez AI nie będą wymagane dla elementów przeglądu w tej kamerze." + } + }, + "button": { + "overriddenGlobal": "Nadpisane (globalnie)", + "overriddenGlobalTooltip": "Ta kamera nadpisuje globalną konfigurację w tej sekcji", + "overriddenGlobalHeading_one": "Ta kamera nadpisuje pole {{count}} z globalnej konfiguracji:", + "overriddenGlobalHeading_few": "Ta kamera nadpisuje pola {{count}} z globalnej konfiguracji:", + "overriddenGlobalHeading_many": "Ta kamera nadpisuje pola {{count}} z globalnej konfiguracji:", + "overriddenGlobalNoDeltas": "Ta kamera nadpisuje ustawienia globalne, ale żadne wartości pól się nie różnią.", + "overriddenBaseConfig": "Nadpisane (bazowa konfiguracja)", + "overriddenBaseConfigTooltip": "Profil {{profile}} zastępuje ustawienia konfiguracyjne w tej sekcji", + "overriddenBaseConfigHeading_one": "Profil {{profile}} zastępuje pole {{count}} z konfiguracji podstawowej:", + "overriddenBaseConfigHeading_few": "Profile {{profile}} zastępują pola {{count}} z konfiguracji podstawowej:", + "overriddenBaseConfigHeading_many": "Profile {{profile}} zastępują pola {{count}} z konfiguracji podstawowej:", + "overriddenBaseConfigNoDeltas": "Profil {{profile}} zastępuje tę sekcję, ale żadne wartości pól nie różnią się od konfiguracji podstawowej.", + "overriddenInCameras": { + "label_one": "Zastąpiono w kamerze {{count}}", + "label_few": "Zastąpiono w kamerach {{count}}", + "label_many": "Zastąpiono w kamerach {{count}}", + "tooltip_one": "Kamera {{count}} zastępuje wartości w tej sekcji. Kliknij, aby wyświetlić szczegóły.", + "tooltip_few": "Kamery {{count}} zastępują wartości w tej sekcji. Kliknij, aby wyświetlić szczegóły.", + "tooltip_many": "Kamery {{count}} zastępują wartości w tej sekcji. Kliknij, aby wyświetlić szczegóły." + } + }, + "saveAllPreview": { + "scope": { + "label": "Zakres", + "global": "Globalne", + "camera": "Kamera: {{cameraName}}" + }, + "profile": { + "label": "Profil" + }, + "field": { + "label": "Pole" + }, + "value": { + "label": "Nowa wartość", + "reset": "Reset" + }, + "title": "Zmiany do zapisania", + "triggerLabel": "Przejrzyj oczekujące zmiany", + "empty": "Brak oczekujących zmian." + } +} diff --git a/web/public/locales/pl/views/system.json b/web/public/locales/pl/views/system.json new file mode 100644 index 0000000..cc58510 --- /dev/null +++ b/web/public/locales/pl/views/system.json @@ -0,0 +1,219 @@ +{ + "documentTitle": { + "cameras": "Statystyki kamer - Frigate", + "storage": "Statystyki Magazynowania - Frigate", + "general": "Statystyki Ogólne - Frigate", + "enrichments": "Statystyki Wzbogacania - Frigate", + "logs": { + "frigate": "Logi Frigate - Frigate", + "go2rtc": "Logi Go2RTC - Frigate", + "nginx": "Logi Nginx - Frigate", + "websocket": "Logi Websocket - Frigate" + } + }, + "general": { + "hardwareInfo": { + "gpuInfo": { + "vainfoOutput": { + "title": "Wynik Vainfo", + "returnCode": "Kod zwrotny: {{code}}", + "processOutput": "Wynik procesu:", + "processError": "Błąd procesu:" + }, + "nvidiaSMIOutput": { + "title": "Wynik Nvidia SMI", + "name": "Nazwa: {{name}}", + "driver": "Sterownik: {{driver}}", + "cudaComputerCapability": "Możliwości obliczeniowe CUDA: {{cuda_compute}}", + "vbios": "Informacje VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Zamknij informacje o GPU" + }, + "copyInfo": { + "label": "Kopiuj informacje o GPU" + }, + "toast": { + "success": "Skopiowano informacje o GPU do schowka" + } + }, + "title": "Informacje o sprzęcie", + "gpuEncoder": "Enkoder GPU", + "gpuDecoder": "Dekoder GPU", + "gpuMemory": "Pamięć GPU", + "gpuUsage": "Użycie GPU", + "npuUsage": "Użycie NPU", + "npuMemory": "Pamięć NPU", + "intelGpuWarning": { + "message": "Statystyki układu graficznego niedostępne", + "description": "W narzędziach telemetrii i statystyki układów graficznych firmy Intel (intel_gpu_top) znajduje się znany błąd powodujący raportowanie użycia układu graficznego wynoszące 0%, nawet gdy akceleracja sprzętowa i wykrywanie obiektów działa prawidłowo korzystając ze zintegrowanego układu graficznego. To nie jest błąd oprogramowania Frigate. Restart hosta może chwilowo rozwiązać problem i pozwolić na weryfikację działania układu graficznego. Ten bład nie wpływa na wydajność systemu.", + "title": "Ostrzeżenie dotyczące statystyk Intel GPU" + } + }, + "title": "Ogólne", + "detector": { + "title": "Detektory", + "inferenceSpeed": "Szybkość wnioskowania detektora", + "cpuUsage": "Użycie CPU przez detektor", + "memoryUsage": "Użycie pamięci przez detektor", + "temperature": "Temperatura detektora", + "cpuUsageInformation": "Procesor został użyty w przygotowaniu wejścia i obsłudze danych do i z modeli wykrywających. Ta wartość nie mierzy czasu wnioskowania, nawet jeśli został użyty akcelerator lub GPU." + }, + "otherProcesses": { + "title": "Inne procesy", + "processCpuUsage": "Użycie CPU przez proces", + "processMemoryUsage": "Użycie pamięci przez proces", + "series": { + "audio_detector": "detektor dźwięku", + "go2rtc": "go2rtc", + "recording": "nagranie", + "review_segment": "przejrzyj fragment", + "embeddings": "osadzone opisy" + } + } + }, + "cameras": { + "info": { + "stream": "Strumień {{idx}}", + "cameraProbeInfo": "{{camera}} Informacje o ustawieniach kamery", + "streamDataFromFFPROBE": "Dane strumienia są pozyskiwane za pomocą ffprobe.", + "video": "Wideo:", + "codec": "Kodek:", + "resolution": "Rozdzielczość:", + "fps": "FPS:", + "unknown": "Nieznany", + "audio": "Audio:", + "error": "Błąd: {{error}}", + "tips": { + "title": "Informacje o sondowaniu kamery" + }, + "fetching": "Pobieranie danych kamery", + "aspectRatio": "proporcje" + }, + "toast": { + "success": { + "copyToClipboard": "Skopiowano dane sondowania do schowka." + }, + "error": { + "unableToProbeCamera": "Nie można sondować kamery: {{errorMessage}}" + } + }, + "title": "Kamery", + "overview": "Przegląd", + "framesAndDetections": "Klatki / Detekcje", + "label": { + "camera": "kamera", + "detect": "wykryj", + "skipped": "pominięte", + "ffmpeg": "FFmpeg", + "capture": "przechwytywanie", + "overallSkippedDetectionsPerSecond": "łączna liczba pominiętych detekcji na sekundę", + "cameraSkippedDetectionsPerSecond": "{{camName}} liczba pominiętych detekcji na sekundę", + "overallFramesPerSecond": "łączna liczba klatek na sekundę", + "overallDetectionsPerSecond": "łączna liczba detekcji na sekundę", + "cameraCapture": "{{camName}} przechwytywanie", + "cameraDetect": "{{camName}} detekcja", + "cameraFramesPerSecond": "{{camName}} liczba klatek na sekundę", + "cameraDetectionsPerSecond": "{{camName}} liczba detekcji na sekundę", + "cameraFfmpeg": "{{camName}} FFmpeg" + } + }, + "storage": { + "cameraStorage": { + "unused": { + "title": "Niewykorzystane", + "tips": "Ta wartość może niedokładnie przedstawiać wolne miejsce dostępne dla Frigate, jeśli masz inne pliki przechowywane na dysku poza nagraniami Frigate. Frigate nie śledzi wykorzystania magazynu poza swoimi nagraniami." + }, + "title": "Magazyn kamery", + "camera": "Kamera", + "storageUsed": "Wykorzystana przestrzeń", + "percentageOfTotalUsed": "Procent całości", + "bandwidth": "Przepustowość", + "unusedStorageInformation": "Informacja o niewykorzystanym magazynie" + }, + "title": "Przestrzeń dyskowa", + "overview": "Przegląd", + "recordings": { + "title": "Nagrania", + "tips": "Ta wartość reprezentuje całkowite miejsce zajmowane przez nagrania w bazie danych Frigate. Frigate nie śledzi wykorzystania magazynu dla wszystkich plików na twoim dysku.", + "earliestRecording": "Najwcześniejsze dostępne nagranie:" + }, + "shm": { + "title": "Wykorzystanie pamięci współdzielonej SHM", + "warning": "Obecny rozmiar pamięci współdzielonej SHM {{total}}MB jest za mały. Zwiększ shm_size do co najmniej {{min_shm}}MB." + } + }, + "logs": { + "copy": { + "error": "Nie udało się skopiować logów do schowka", + "label": "Kopiuj do Schowka", + "success": "Skopiowano logi do schowka" + }, + "download": { + "label": "Pobierz Logi" + }, + "type": { + "label": "Typ", + "timestamp": "Znacznik czasu", + "tag": "Tag", + "message": "Wiadomość" + }, + "tips": "Logi są przesyłane strumieniowo z serwera", + "toast": { + "error": { + "fetchingLogsFailed": "Błąd pobierania logów: {{errorMessage}}", + "whileStreamingLogs": "Błąd podczas strumieniowania logów: {{errorMessage}}" + } + }, + "websocket": { + "label": "Wiadomości", + "pause": "Pauza", + "resume": "Wznów", + "clear": "Wyczyść", + "filter": { + "all": "Wszystkie tematy", + "topics": "Tematy" + } + } + }, + "title": "System", + "metrics": "Metryki systemowe", + "lastRefreshed": "Ostatnie odświeżenie: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} ma wysokie użycie CPU przez FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} ma wysokie użycie CPU przez detekcję ({{detectAvg}}%)", + "healthy": "System jest sprawny", + "reindexingEmbeddings": "Ponowne indeksowanie osadzeń ({{processed}}% ukończone)", + "detectIsSlow": "{{detect}} jest wolne ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} jest bardzo wolne ({{speed}} ms)", + "cameraIsOffline": "{{camera}} jest niedostępna", + "shmTooLow": "przydział {{total}} MB dla /dev/shm powinien zostać zwiększony do przynajmniej {{min}} MB." + }, + "enrichments": { + "title": "Wzbogacenia", + "infPerSecond": "Wnioskowania na sekundę", + "embeddings": { + "image_embedding_speed": "Szybkość osadzania obrazów", + "face_embedding_speed": "Szybkość osadzania twarzy", + "plate_recognition_speed": "Szybkość rozpoznawania tablic rejestracyjnych", + "text_embedding_speed": "Szybkość osadzania tekstu", + "face_recognition_speed": "Szybkość rozpoznawania twarzy", + "image_embedding": "Osadzenie obrazu", + "plate_recognition": "Rozpoznawanie rejestracji samochodowych", + "yolov9_plate_detection_speed": "Prędkość detekcji rejestracji samochodowych YOLOv9", + "yolov9_plate_detection": "Detekcja rejestracji samochodowych YOLOv9", + "text_embedding": "Osadzenie tekstu", + "face_recognition": "Rozpoznawanie twarzy", + "classification_events_per_second": "{{name}} Klasyfikacja zdarzeń na sekundę", + "classification_speed": "{{name}} Szybkość klasyfikacji", + "classification": "{{name}} Klasyfikacja", + "review_description": "Opis recenzji", + "review_description_speed": "Szybkość opisu recenzji", + "review_description_events_per_second": "Opis recenzji", + "object_description": "Opis obiektu", + "object_description_speed": "Szybkość opisu obiektu", + "object_description_events_per_second": "Opis obiektu" + }, + "averageInf": "Średni czas wnioskowania" + } +} diff --git a/web/public/locales/pt-BR/audio.json b/web/public/locales/pt-BR/audio.json new file mode 100644 index 0000000..1444d4d --- /dev/null +++ b/web/public/locales/pt-BR/audio.json @@ -0,0 +1,502 @@ +{ + "mantra": "Mantra", + "child_singing": "Criança cantando", + "speech": "Fala", + "yell": "Gritar", + "chant": "Canto", + "babbling": "Balbuciando", + "bellow": "Abaixo", + "whoop": "Grito de Felicidade", + "whispering": "Sussurrando", + "laughter": "Risada", + "snicker": "Risada", + "crying": "Choro", + "sigh": "Suspirar", + "singing": "Cantoria", + "choir": "Coro", + "yodeling": "Cantando", + "bicycle": "Bicicleta", + "car": "Carro", + "motorcycle": "Moto", + "bus": "Ônibus", + "train": "Trem", + "boat": "Barco", + "bird": "Pássaro", + "cat": "Gato", + "dog": "Cachorro", + "rapping": "Cantando rap", + "horse": "Cavalo", + "humming": "Cantarolando", + "sheep": "Ovelha", + "synthetic_singing": "Canto Sintético", + "groan": "Gemido", + "grunt": "Grunhido", + "whistling": "Assobio", + "breathing": "Respiração", + "camera": "Câmera", + "wheeze": "Chiado", + "snoring": "Ronco", + "gasp": "Respiração Ofegante", + "pant": "Arfado", + "snort": "Bufado", + "cough": "Tosse", + "throat_clearing": "Pigarro", + "sneeze": "Espirro", + "sniff": "Fungado", + "run": "Executar", + "shuffle": "Embaralhar", + "footsteps": "Passos", + "chewing": "Mastigação", + "biting": "Mordida", + "gargling": "Gargarejo", + "stomach_rumble": "Ronco de Estômago", + "burping": "Arroto", + "skateboard": "Skate", + "hiccup": "Soluço", + "fart": "Flatulência", + "hands": "Mãos", + "finger_snapping": "Estalar de Dedos", + "clapping": "Palmas", + "heartbeat": "Batida de Coração", + "heart_murmur": "Sopro Cardíaco", + "cheering": "Comemoração", + "applause": "Aplausos", + "chatter": "Conversa", + "crowd": "Multidão", + "children_playing": "Crianças Brincando", + "animal": "Animal", + "pets": "Animais de Estimação", + "bark": "Latido", + "yip": "Latido / Grito Agudo", + "howl": "Uivado", + "bow_wow": "Latido", + "growling": "Rosnado", + "whimper_dog": "Choro de Cachorro", + "purr": "Ronronado", + "meow": "Miado", + "hiss": "Sibilo", + "caterwaul": "Lamúria", + "livestock": "Animais de Criação", + "clip_clop": "Galope", + "neigh": "Relincho", + "door": "Porta", + "cattle": "Gado", + "moo": "Mugido", + "cowbell": "Sino de Vaca", + "mouse": "Rato", + "pig": "Porco", + "oink": "Grunhido de Porco", + "keyboard": "Teclado", + "goat": "Cabra", + "bleat": "Balido", + "fowl": "Ave", + "chicken": "Galinha", + "sink": "Pia", + "cluck": "Cacarejo", + "cock_a_doodle_doo": "Cacarejado", + "blender": "Liquidificador", + "turkey": "Peru", + "gobble": "Deglutição", + "clock": "Relógio", + "duck": "Pato", + "quack": "Grasnado", + "scissors": "Tesouras", + "goose": "Ganso", + "honk": "Buzina", + "hair_dryer": "Secador de Cabelo", + "wild_animals": "Animais Selvagens", + "toothbrush": "Escova de Dentes", + "roaring_cats": "Felinos Rugindo", + "roar": "Rugido", + "vehicle": "Veículo", + "chirp": "Piado", + "squawk": "Guincho Animal", + "pigeon": "Pombo", + "dogs": "Cachorros", + "rats": "Ratos", + "coo": "Arrulhado de Pombo", + "crow": "Corvo", + "caw": "Grasnado de Corvo", + "owl": "Coruja", + "hoot": "Chirriado de Coruja", + "flapping_wings": "Bater de Asas", + "patter": "Passos Leves", + "insect": "Inseto", + "cricket": "Grilo", + "mosquito": "Mosquito", + "fly": "Mosca", + "buzz": "Zumbido", + "frog": "Sapo", + "croak": "Coaxado", + "snake": "Cobra", + "rattle": "Chocalho", + "whale_vocalization": "Vocalização de Baleia", + "music": "Música", + "musical_instrument": "Instrumento Musical", + "plucked_string_instrument": "Instrumento de Cordas Dedilhadas", + "guitar": "Violão", + "electric_guitar": "Guitarra", + "bass_guitar": "Baixo", + "acoustic_guitar": "Violão Acústico", + "steel_guitar": "Guitarra Havaiana", + "tapping": "Batidas Leves", + "strum": "Dedilhado", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Bandolim", + "zither": "Cítara", + "ukulele": "Ukulele", + "piano": "Piano", + "electric_piano": "Piano Elétrico", + "organ": "Órgão", + "electronic_organ": "Órgão Eletrônico", + "hammond_organ": "Órgão Hammond", + "synthesizer": "Sintetizador", + "sampler": "Sampler", + "harpsichord": "Cravo (Instrumento Musical)", + "percussion": "Percussão", + "drum_kit": "Kit de Baterias", + "drum_machine": "Bateria Eletrônica", + "drum": "Tambor", + "snare_drum": "Caixa Clara", + "rimshot": "Rimshot", + "drum_roll": "Tambores Rufando", + "bass_drum": "Bumbo", + "timpani": "Tímpanos (Instrumento Musical)", + "tabla": "Tabla", + "wood_block": "Bloco de Madeira", + "bagpipes": "Gaita de Fole", + "pop_music": "Música Pop", + "grunge": "Grunge", + "middle_eastern_music": "Música do Oriente Médio", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Música Clássica", + "opera": "Ópera", + "electronic_music": "Música Eletrónica", + "house_music": "Música House", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Eletrónica", + "cymbal": "Címbalo", + "hi_hat": "Chimbau", + "tambourine": "Pandeiro", + "maraca": "Maraca", + "gong": "Gongo", + "tubular_bells": "Sinos Tubulares", + "mallet_percussion": "Percussão de Martelo", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibrafone", + "steelpan": "Panela de Aço", + "orchestra": "Orquestra", + "brass_instrument": "Instrumento de Metal", + "french_horn": "Trompa Francesa", + "trumpet": "Trombeta", + "trombone": "Trombone", + "bowed_string_instrument": "Instrumento de Cordas Friccionadas", + "string_section": "Seção de Cordas", + "violin": "Violino", + "pizzicato": "Pizzicato", + "cello": "Violoncelo", + "double_bass": "Contrabaixo", + "wind_instrument": "Instrumento de Sopro", + "flute": "Flauta", + "saxophone": "Saxofone", + "clarinet": "Clarinete", + "harp": "Harpa", + "bell": "Sino", + "church_bell": "Sino de Igreja", + "jingle_bell": "Guizo", + "bicycle_bell": "Campainha de Bicicleta", + "tuning_fork": "Diapasão", + "chime": "Carrilhão", + "wind_chime": "Sinos de Vento", + "harmonica": "Gaita", + "accordion": "Acordeão", + "didgeridoo": "Didjeridu", + "theremin": "Teremim", + "scratching": "Arranhado", + "hip_hop_music": "Música Hip-Hop", + "beatboxing": "Beatbox", + "rock_music": "Rock", + "heavy_metal": "Heavy Metal", + "punk_rock": "Punk Rock", + "progressive_rock": "Rock Progressivo", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Rock Psicodélico", + "rhythm_and_blues": "Rhythm and Blues", + "soul_music": "Música Soul", + "music_of_latin_america": "Music of Latin America", + "salsa_music": "Música Salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Música para Crianças", + "new-age_music": "Música New Age", + "vocal_music": "Música Vocal", + "a_capella": "A Capella", + "music_of_africa": "Music of Africa", + "afrobeat": "Afrobeat", + "christian_music": "Música Cristã", + "gospel_music": "Música Gospel", + "music_of_asia": "Music of Asia", + "carnatic_music": "Música Carnática", + "music_of_bollywood": "Música de Bollywood", + "ska": "Ska", + "traditional_music": "Música Tradicional", + "independent_music": "Música Independente", + "song": "Música", + "thunderstorm": "Tempestade", + "thunder": "Trovão", + "water": "Água", + "rain": "Chuva", + "raindrop": "Gota de Chuva", + "rain_on_surface": "Chuva na Superfície", + "stream": "Transmissão", + "waterfall": "Cachoeira", + "ocean": "Oceano", + "waves": "Ondas", + "steam": "Vapor", + "gurgling": "Borbulhado", + "fire": "Fogo", + "crackle": "Estalo", + "sailboat": "Veleiro", + "rowboat": "Barco a Remo", + "motorboat": "Lancha", + "ship": "Navio", + "motor_vehicle": "Veículo Motorizado", + "toot": "Buzinado", + "car_alarm": "Alarme de Carro", + "power_windows": "Vidros Elétricos", + "skidding": "Derrapado", + "singing_bowl": "Tigela Tibetana", + "reggae": "Reggae", + "country": "País", + "swing_music": "Música Swing", + "bluegrass": "Música Bluegrass", + "funk": "Funk", + "folk_music": "Música Folk", + "electronic_dance_music": "Música Eletrônica", + "ambient_music": "Música Ambiente", + "trance_music": "Música Trance", + "background_music": "Música de Fundo", + "theme_music": "Música Tema", + "jingle": "Jingle", + "soundtrack_music": "Música de Trilha Sonora", + "lullaby": "Canção de Ninar", + "video_game_music": "Música de Video Game", + "christmas_music": "Música Natalina", + "dance_music": "Música Dance", + "wedding_music": "Música de Casamento", + "happy_music": "Música Feliz", + "sad_music": "Música Triste", + "tender_music": "Música Suave", + "exciting_music": "Música Empolgante", + "angry_music": "Música Raivosa", + "scary_music": "Música Assustadora", + "wind": "Vento", + "rustling_leaves": "Folhas Farfalhantes", + "fixed-wing_aircraft": "Aeronave de Asa Fixa", + "engine": "Motor", + "light_engine": "Motor Leve", + "dental_drill's_drill": "Broca Odontológica", + "lawn_mower": "Cortador de Grama", + "chainsaw": "Motosserra", + "medium_engine": "Motor Médio", + "heavy_engine": "Motor Pesado", + "engine_knocking": "Motor Batendo", + "engine_starting": "Motor Partindo", + "idling": "Marcha Lenta", + "chopping": "Cortando", + "frying": "Fritando", + "microwave_oven": "Forno Microondas", + "water_tap": "Torneira de Água", + "bathtub": "Banheira", + "toilet_flush": "Descarga de Vaso Sanitário", + "computer_keyboard": "Teclado de Computador", + "writing": "Escrita", + "alarm": "Alarme", + "telephone": "Telefone", + "telephone_bell_ringing": "Telefone Tocando", + "ringtone": "Toque de Celular", + "telephone_dialing": "Telefone Discando", + "dial_tone": "Tom de Discagem", + "busy_signal": "Sinal de Ocupado", + "alarm_clock": "Despertador", + "siren": "Sirene", + "civil_defense_siren": "Sirene de Defesa Civil", + "wind_noise": "Ruído de Vento", + "tire_squeal": "Pneus Cantando", + "car_passing_by": "Carro Passando", + "race_car": "Carro de Corrida", + "truck": "Pickup / Caminhão", + "air_brake": "Freios a Ar", + "air_horn": "Buzina a Ar", + "reversing_beeps": "Alarme de Ré", + "ice_cream_truck": "Carro de Sorvete", + "emergency_vehicle": "Veículo de Emergência", + "police_car": "Carro de Polícia", + "ambulance": "Ambulância", + "fire_engine": "Caminhão de Bombeiros", + "traffic_noise": "Barulho de Tráfego", + "rail_transport": "Transporte Ferroviário", + "train_whistle": "Apito de Trem", + "train_horn": "Buzina de Trem", + "railroad_car": "Vagão de Trem", + "train_wheels_squealing": "Rodas de Trem Rangendo", + "subway": "Metrô", + "aircraft": "Aeronave", + "aircraft_engine": "Motor de Aeronave", + "jet_engine": "Motor a Jato", + "propeller": "Hélice", + "helicopter": "Helicóptero", + "accelerating": "Acelerando", + "doorbell": "Campainha", + "ding-dong": "Toque de Campainha", + "sliding_door": "Porta de Correr", + "slam": "Batida Forte", + "knock": "Batida na Porta", + "burst": "Estouro / Rajada", + "eruption": "Erupção", + "boom": "Estrondo", + "wood": "Madeira", + "chop": "Barulho de Corte", + "splinter": "Lascado", + "crack": "Rachado", + "glass": "Vidro", + "chink": "Fenda", + "shatter": "Estilhaçado", + "silence": "Silêncio", + "sound_effect": "Efeito Sonoro", + "environmental_noise": "Ruido Ambiente", + "static": "Estático", + "white_noise": "Ruido Branco", + "pink_noise": "Ruido Rosa", + "television": "Televisão", + "radio": "Rádio", + "field_recording": "Gravação de Campo", + "scream": "Grito", + "tap": "Toque", + "squeak": "Rangido", + "cupboard_open_or_close": "Cristaleira Abrindo ou Fechando", + "drawer_open_or_close": "Gaveteiro Abrindo ou Fechando", + "dishes": "Pratos", + "cutlery": "Talheres", + "electric_toothbrush": "Escova de Dentes Elétrica", + "vacuum_cleaner": "Aspirador de Pó", + "zipper": "Zíper", + "keys_jangling": "Chaves Chacoalhando", + "coin": "Moeda", + "electric_shaver": "Barbeador Elétrico", + "shuffling_cards": "Embaralhar de Cartas", + "typing": "Digitação", + "typewriter": "Máquina de Escrever", + "buzzer": "Zumbador", + "smoke_detector": "Detector de Fumaça", + "fire_alarm": "Alarme de Incêndio", + "foghorn": "Buzina de Nevoeiro", + "whistle": "Apito", + "steam_whistle": "Apito a Vapor", + "mechanisms": "Mecanismos", + "ratchet": "Catraca", + "tick": "Tique", + "tick-tock": "Tique-Toque", + "gears": "Engrenagens", + "pulleys": "Polias", + "sewing_machine": "Máquina de Costura", + "mechanical_fan": "Ventilador Mecânico", + "air_conditioning": "Ar-Condicionado", + "cash_register": "Caixa Registradora", + "printer": "Impressora", + "single-lens_reflex_camera": "Câmera Single-Lens Reflex", + "tools": "Ferramentas", + "hammer": "Martelo", + "jackhammer": "Britadeira", + "sawing": "Som de Serra", + "filing": "Som de Lima", + "sanding": "Lixamento", + "power_tool": "Ferramenta Elétrica", + "drill": "Furadeira", + "explosion": "Explosão", + "gunshot": "Tiro", + "machine_gun": "Metralhadora", + "fusillade": "Fuzilamento", + "artillery_fire": "Fogo de Artilharia", + "cap_gun": "Espoleta", + "fireworks": "Fogos de Artifício", + "firecracker": "Rojões", + "noise": "Ruído", + "distortion": "Distorção", + "cacophony": "Cacofonia", + "vibration": "Vibração", + "change_ringing": "Mudar Toque", + "shofar": "Berrante", + "liquid": "Líquido", + "splash": "Respingar", + "slosh": "Respingo", + "squish": "Esmagar", + "drip": "Pingar", + "pour": "Derramar", + "trickle": "Gotejar", + "gush": "Jorrar", + "fill": "Preencher", + "spray": "Borrifar", + "pump": "Bombear", + "stir": "Mexer", + "boiling": "Fervendo", + "sonar": "Sonar", + "arrow": "Flecha", + "whoosh": "Uau", + "thump": "Baque", + "thunk": "Tombo", + "electronic_tuner": "Afinador Eletrônico", + "effects_unit": "Unidade de Efeitos", + "chorus_effect": "Efeito Coro", + "basketball_bounce": "Quique da bola", + "bang": "Batida", + "slap": "Tapa", + "whack": "Bater", + "smash": "Esmagar", + "breaking": "Quebrando", + "bouncing": "Quicando", + "whip": "Chicote", + "flap": "Aba", + "scratch": "Arranhão", + "scrape": "Raspagem", + "rub": "Esfregar", + "roll": "Rolar", + "crushing": "Esmagamento", + "crumpling": "Amarrotar", + "tearing": "Rasgando", + "beep": "Bip", + "ping": "Pingo", + "ding": "Campainha", + "clang": "Estridente", + "squeal": "Guincho", + "creak": "Ranger", + "rustle": "Farfalhar", + "whir": "Zumbir", + "clatter": "Barulho", + "sizzle": "Chiado", + "clicking": "Clicando", + "clickety_clack": "Tique-taque", + "rumble": "Estrondo", + "plop": "Ploft", + "hum": "Zumbir", + "zing": "Zangando", + "boing": "Poin", + "crunch": "Mastigar", + "sine_wave": "Onda Senoidal", + "harmonic": "Harmonica", + "chirp_tone": "Som Agudo", + "pulse": "Pulso", + "inside": "Dentro", + "outside": "Fora", + "reverberation": "Reverberação", + "echo": "Eco", + "mains_hum": "Zumbido Elétrico", + "sidetone": "Retorno de Voz", + "throbbing": "Latejante", + "sodeling": "Soldagem" +} diff --git a/web/public/locales/pt-BR/common.json b/web/public/locales/pt-BR/common.json new file mode 100644 index 0000000..3ce687c --- /dev/null +++ b/web/public/locales/pt-BR/common.json @@ -0,0 +1,331 @@ +{ + "time": { + "untilForTime": "Até {{time}}", + "untilForRestart": "Até o Frigate reiniciar.", + "untilRestart": "Até reiniciar", + "ago": "{{timeAgo}} atrás", + "justNow": "Agora mesmo", + "today": "Hoje", + "yesterday": "Ontem", + "last7": "Últimos 7 dias", + "last14": "Últimos 14 dias", + "last30": "Últimos 30 dias", + "thisWeek": "Essa semana", + "lastWeek": "Semana passada", + "thisMonth": "Este mês", + "lastMonth": "Mês passado", + "5minutes": "5 minutos", + "10minutes": "10 minutos", + "30minutes": "30 minutos", + "1hour": "1 hora", + "12hours": "12 horas", + "24hours": "24 horas", + "pm": "pm", + "am": "am", + "yr": "{{time}}ano", + "year_one": "{{time}} ano", + "year_many": "{{time}} anos", + "year_other": "{{time}} anos", + "mo": "{{time}}mês", + "month_one": "{{time}} mês", + "month_many": "{{time}} meses", + "month_other": "{{time}} meses", + "d": "{{time}} dia", + "day_one": "{{time}} dia", + "day_many": "{{time}} dias", + "day_other": "{{time}} dias", + "h": "{{time}}h", + "hour_one": "{{time}} hora", + "hour_many": "{{time}} horas", + "hour_other": "{{time}} horas", + "m": "{{time}}m", + "minute_one": "{{time}} minuto", + "minute_many": "{{time}} minutos", + "minute_other": "{{time}} minutos", + "s": "{{time}}s", + "second_one": "{{time}} segundo", + "second_many": "{{time}} segundos", + "second_other": "{{time}} segundos", + "formattedTimestamp": { + "12hour": "d MMM,h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-hh-mm-ss", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "never": "Nunca", + "inProgress": "Em progresso", + "invalidStartTime": "Horário de início inválido", + "invalidEndTime": "Horário de término inválido" + }, + "selectItem": "Selecionar {{item}}", + "unit": { + "speed": { + "mph": "mi/h", + "kph": "km/h" + }, + "length": { + "feet": "pés", + "meters": "metros" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hora", + "mbph": "MB/hora", + "gbph": "GB/hora" + } + }, + "label": { + "back": "Voltar", + "hide": "Esconder {{item}}", + "show": "Mostrar {{item}}", + "ID": "ID", + "none": "Nenhum", + "all": "Todos", + "other": "Outros" + }, + "button": { + "apply": "Aplicar", + "reset": "Resetar", + "done": "Concluído", + "enabled": "Habilitado", + "enable": "Habilitar", + "disabled": "Desativado", + "disable": "Desativar", + "save": "Salvar", + "saving": "Salvando…", + "cancel": "Cancelar", + "close": "Fechar", + "copy": "Copiar", + "back": "Voltar", + "history": "Histórico", + "fullscreen": "Tela Inteira", + "exitFullscreen": "Sair da Tela Inteira", + "pictureInPicture": "Miniatura Flutuante", + "twoWayTalk": "Áudio Bidirecional", + "cameraAudio": "Áudio da Câmera", + "on": "LIGADO", + "off": "DESLIGADO", + "edit": "Editar", + "copyCoordinates": "Copiar coordenadas", + "delete": "Deletar", + "yes": "Sim", + "no": "Não", + "download": "Baixar", + "info": "Informação", + "suspended": "Suspenso", + "unsuspended": "Não Suspenso", + "play": "Reproduzir", + "unselect": "Deselecionar", + "export": "Exportar", + "deleteNow": "Deletar Agora", + "next": "Próximo", + "add": "Adicionar", + "undo": "Desfazer", + "copiedToClipboard": "Copiado para a área de transferência", + "continue": "Continuar", + "modified": "Modificado", + "overridden": "Substituído", + "resetToGlobal": "Redefinir para o Global", + "resetToDefault": "Redefinir para o Padrão", + "saveAll": "Salvar Tudo", + "savingAll": "Salvando Tudo…", + "undoAll": "Desfazer Tudo", + "applying": "Aplicando…", + "retry": "Tente novamente" + }, + "menu": { + "system": "Sistema", + "systemMetrics": "Métricas de sistema", + "configuration": "Configuração", + "language": { + "hi": "हिन्दी (Hindi)", + "fr": "Français (Francês)", + "en": "English (Inglês)", + "es": "Español (Espanhol)", + "zhCN": "简体中文 (Chinês Simplificado)", + "ar": "العربية (Arábico)", + "pt": "Português (Português)", + "ru": "Русский (Russo)", + "de": "Deustch (Alemão)", + "ja": "日本語 (Japonês)", + "tr": "Türkçe (Turco)", + "it": "Italiano (Italiano)", + "nl": "Nederlands (Holandês)", + "sv": "Svenska (Sueco)", + "cs": "Čeština (Checo)", + "nb": "Norsk Bokmål (Bokmål Norueguês)", + "ko": "한국어 (Coreano)", + "vi": "Tiếng Việt (Vietnamita)", + "fa": "فارسی (Persa)", + "pl": "Polski (Polonês)", + "uk": "Українська (Ucraniano)", + "he": "עברית (Hebraico)", + "el": "Ελληνικά (Grego)", + "ro": "Română (Romeno)", + "hu": "Magyar (Húngaro)", + "fi": "Suomi (Finlandês)", + "da": "Dansk (Dinamarquês)", + "sk": "Slovenčina (Eslovaco)", + "yue": "粵語 (Cantonês)", + "th": "ไทย (Tailandês)", + "ca": "Català (Catalão)", + "withSystem": { + "label": "Usar as configurações de sistema para o idioma" + }, + "ptBR": "Português Brasileiro (Português Brasileiro)", + "sr": "Српски (Sérvio)", + "sl": "Slovenščina (Esloveno)", + "lt": "Lietuvių (Lituano)", + "bg": "Български (Búlgaro)", + "gl": "Galego (Galego)", + "id": "Bahasa Indonesia (Indonésio)", + "ur": "اردو (Urdu)", + "hr": "Hrvatski (Croata)", + "bs": "Bosanski (Bósnio)", + "zhHant": "Chinês Tradicional" + }, + "systemLogs": "Logs de sistema", + "settings": "Configurações", + "configurationEditor": "Editor de Configuração", + "languages": "Idiomas", + "appearance": "Aparência", + "darkMode": { + "label": "Modo Escuro", + "light": "Claro", + "dark": "Escuro", + "withSystem": { + "label": "Use as configurações do sistema para modo claro ou escuro" + } + }, + "withSystem": "Sistema", + "theme": { + "label": "Tema", + "blue": "Azul", + "green": "Verde", + "nord": "Nord", + "red": "Vermelho", + "highcontrast": "Alto Contraste", + "default": "Padrão" + }, + "help": "Ajuda", + "documentation": { + "title": "Documentação", + "label": "Documentação do Frigate" + }, + "restart": "Reiniciar o Frigate", + "live": { + "title": "Ao Vivo", + "allCameras": "Todas as câmeras", + "cameras": { + "title": "Câmeras", + "count_one": "{{count}} Câmera", + "count_many": "{{count}} Câmeras", + "count_other": "{{count}} Câmeras" + } + }, + "review": "Revisar", + "explore": "Explorar", + "export": "Exportar", + "uiPlayground": "Playground da UI", + "faceLibrary": "Biblioteca de Rostos", + "user": { + "title": "Usuário", + "account": "Conta", + "current": "Usuário Atual: {{user}}", + "anonymous": "anônimo", + "logout": "Sair", + "setPassword": "Definir Senha" + }, + "classification": "Classificação", + "chat": "Chat", + "profiles": "Perfis", + "actions": "Ações", + "features": "Funcionalidades" + }, + "toast": { + "copyUrlToClipboard": "URL copiada para a área de transferência.", + "save": { + "title": "Salvar", + "error": { + "title": "Falha ao salvar as alterações de configuração: {{errorMessage}}", + "noMessage": "Falha ao salvar as alterações de configuração" + }, + "success": "Alterações salvas com sucesso." + } + }, + "role": { + "title": "Papel", + "admin": "Administrador", + "viewer": "Espectador", + "desc": "Administradores possuem acesso total a todos os recursos da interface do Frigate. Espectadores são limitados a ver as câmeras, revisar itens, e filmagens históricas na interface." + }, + "pagination": { + "label": "paginação", + "previous": { + "title": "Anterior", + "label": "Ir para a página anterior" + }, + "next": { + "title": "Próximo", + "label": "Ir para a próxima página" + }, + "more": "Mais páginas" + }, + "accessDenied": { + "documentTitle": "Acesso Negado - Frigate", + "title": "Acesso Negado", + "desc": "Você não possui permissão para visualizar essa página." + }, + "notFound": { + "documentTitle": "Não Encontrado - Frigate", + "title": "404", + "desc": "Página não encontrada" + }, + "readTheDocumentation": "Leia a documentação", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} e {{1}}", + "many": "{{items}} e {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opcional", + "internalID": "O ID interno que o Frigate usa na configuração e banco de dados" + }, + "no_items": "Sem itens", + "validation_errors": "Erros de validação", + "credentialField": { + "savedPlaceholder": "Salvo - deixar em branco para manter a atual" + } +} diff --git a/web/public/locales/pt-BR/components/auth.json b/web/public/locales/pt-BR/components/auth.json new file mode 100644 index 0000000..2777581 --- /dev/null +++ b/web/public/locales/pt-BR/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nome de Usuário", + "password": "Senha", + "login": "Login", + "errors": { + "usernameRequired": "Nome de usuário é necessário", + "passwordRequired": "Senha necessária", + "rateLimit": "Limite de taxa excedido. Tente novamente mais tarde.", + "loginFailed": "Falha no Login", + "unknownError": "Erro desconhecido. Checar registros.", + "webUnknownError": "Erro desconhecido. Verifique os logs do console." + }, + "firstTimeLogin": "Fazendo login pela primeira vez? As credenciais estão escritas nos logs do Frigate." + } +} diff --git a/web/public/locales/pt-BR/components/camera.json b/web/public/locales/pt-BR/components/camera.json new file mode 100644 index 0000000..a746960 --- /dev/null +++ b/web/public/locales/pt-BR/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "Grupo de Câmeras", + "add": "Adicionar Grupo de Câmeras", + "edit": "Edição Grupo de Câmera", + "delete": { + "label": "Deletar Grupo de Câmera", + "confirm": { + "title": "Confirmar Apagar", + "desc": "Você tem certeza que quer apagar o grupo de câmera {{name}}?" + } + }, + "name": { + "label": "Nome", + "placeholder": "Digite um nome…", + "errorMessage": { + "mustLeastCharacters": "O nome do grupo de câmeras deve ter pelo menos 2 caracteres.", + "exists": "O nome do grupo de câmeras já existe.", + "nameMustNotPeriod": "O nome do grupo de câmeras não deve conter ponto.", + "invalid": "Nome de grupo de câmeras inválido." + } + }, + "cameras": { + "label": "Câmeras", + "desc": "Selecione as câmeras para este grupo." + }, + "icon": "Ícone", + "success": "O grupo de câmeras {{name}} foi salvo.", + "camera": { + "setting": { + "label": "Configurações de Streaming da Câmera", + "title": "Configurações de streaming da câmera {{cameraName}}", + "audioIsAvailable": "Áudio está disponível para esta transmissão", + "audioIsUnavailable": "Áudio indisponível para esta transmissão", + "desc": "Alterar as opções de transmissão ao vivo para o painel desse grupo de câmera. Esses ajustes são específicos para esse dispositivo/navegador.", + "audio": { + "tips": { + "title": "O audio deve ter a sua saída da câmera e configurado em go2rtc para essa transmissão.", + "document": "Leia a documentação " + } + }, + "stream": "Transmissão", + "placeholder": "Selecionar transmissão ao vivo", + "streamMethod": { + "label": "Método de Transmissão", + "placeholder": "Selecione um método de transmissão", + "method": { + "noStreaming": { + "label": "Sem Transmissão", + "desc": "Imagens da câmera atualizarão apenas uma vez por minuto e não haverá transmissão ao vivo." + }, + "smartStreaming": { + "label": "Transmissão Inteligente (recomendado)", + "desc": "O streaming inteligente atualizará a imagem da câmera uma vez por minuto quando não houver atividade detectável para economizar largura de banda e recursos. Quando alguma atividade for detectada, a imagem automáticamente mudará para a transmissão ao vivo." + }, + "continuousStreaming": { + "label": "Transmissão Contínua", + "desc": { + "title": "A imagem da câmera será sempre uma transmissão ao vivo quando visível no painel, mesmo que não haja atividade sendo detectada.", + "warning": "A transmissão contínua pode causar alta utilização de banda e problemas de performance. Use com cuidado." + } + } + } + }, + "compatibilityMode": { + "label": "Modo de compatibilidade", + "desc": "Habilite essa opção somente se a transmissão ao vivo da sua câmera estiver exibindo artefatos de cor e possui uma linha diagonal no canto esquerdo da imagem." + } + }, + "birdseye": "Visão Panorâmica" + }, + "showAll": "Exibir todos os grupos de câmeras", + "showLess": "Mostrar menos", + "editGroups": "Editar grupos de câmeras" + }, + "debug": { + "options": { + "label": "Configurações", + "title": "Opções", + "showOptions": "Exibir Opções", + "hideOptions": "Ocultar Opções" + }, + "zones": "Zonas", + "mask": "Máscara", + "motion": "Movimento", + "regions": "Regiões", + "boundingBox": "Caixa Delimitadora", + "timestamp": "Timestamp", + "paths": "Caminhos" + } +} diff --git a/web/public/locales/pt-BR/components/dialog.json b/web/public/locales/pt-BR/components/dialog.json new file mode 100644 index 0000000..7f877ad --- /dev/null +++ b/web/public/locales/pt-BR/components/dialog.json @@ -0,0 +1,202 @@ +{ + "restart": { + "title": "Você tem certeza que deseja reiniciar o Frigate?", + "button": "Reiniciar", + "restarting": { + "title": "Frigate está Reiniciando", + "content": "Essa página vai recarregar em {{countdown}} segundos.", + "button": "Forçar Recarregar Agora" + }, + "description": "Isto irá parar brevemente o Frigate enquanto reinicia." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Enviar para Frigate+", + "desc": "Objetos nos lugares que você quer evitar não são falsos positivos. Enviá-los como falsos positivos confundirá o modelo." + }, + "review": { + "question": { + "label": "Confirmar esse rótulo para Frigate Plus", + "ask_a": "Este objeto é um {{label}}?", + "ask_an": "Este objeto é um{{label}}?", + "ask_full": "Este objeto é um{{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Enviado" + }, + "toast": { + "error": "Falha ao enviar para o Frigate+. Verifique sua conexão de rede e tente novamente." + } + } + }, + "video": { + "viewInHistory": "Ver no Histórico" + } + }, + "export": { + "time": { + "fromTimeline": "Selecione da Linha do tempo", + "lastHour_one": "Última hora", + "lastHour_many": "Últimas {{count}} horas", + "lastHour_other": "Últimas {{count}} horas", + "custom": "Personalizado", + "start": { + "title": "Hora de Início", + "label": "Selecione a Hora de Início" + }, + "end": { + "title": "Hora de Término", + "label": "Selecione a Hora de Término" + } + }, + "name": { + "placeholder": "Nomeie a Exportação" + }, + "select": "Selecionar", + "export": "Exportar", + "selectOrExport": "Selecionar ou Exportar", + "toast": { + "success": "Exportação iniciada com sucesso. Veja o arquivo na tela exportar.", + "error": { + "failed": "Falha ao iniciar exportação: {{error}}", + "endTimeMustAfterStartTime": "Tempo de finalização deve ser após tempo de início", + "noVaildTimeSelected": "Nenhuma faixa de tempo válida selecionada" + }, + "view": "Ver", + "batchQueuedSuccess_one": "1 exportação na fila. Abrindo o caso agora.", + "batchQueuedSuccess_many": "{{count}} exportações colocadas na fila. Abrindo o caso agora.", + "batchQueuedSuccess_other": "", + "batchQueuedPartial": "{{successful}} de {{total}} exportações na fila. Câmeras com falha: {{failedCameras}}", + "batchQueueFailed": "Falha ao enfileirar {{total}} exportações. Câmeras com falha: {{failedCameras}}", + "batchPartial": "Iniciadas {{successful}} de {{total}} exportações. Câmeras com falha: {{failedCameras}}", + "batchFailed": "Falha ao iniciar {{total}} exportações. Câmeras com falha: {{failedCameras}}", + "queued": "Exportação na fila. Acompanhe o progresso na página de exportações." + }, + "fromTimeline": { + "saveExport": "Salvar Exportação", + "previewExport": "Pré-Visualizar Exportação", + "queueingExport": "Enfileirando exportação...", + "useThisRange": "Use esta faixa" + }, + "case": { + "label": "Caso", + "placeholder": "Selecione um caso", + "newCaseOption": "Criar novo caso de uso", + "newCaseNamePlaceholder": "Novo caso de uso", + "newCaseDescriptionPlaceholder": "Descrição do caso de uso", + "nonAdminHelp": "Um novo caso de uso será criado para estas exportações." + }, + "queueing": "Exportação na fila...", + "tabs": { + "export": "Câmera única", + "multiCamera": "Multi-Câmera" + }, + "multiCamera": { + "timeRange": "Intervalo de tempo", + "selectFromTimeline": "Selecione do intervale de tempo", + "cameraSelection": "Câmeras", + "cameraSelectionHelp": "Câmeras com objetos localizados neste intervalo de tempo estão pré-selecionados", + "checkingActivity": "Verificando se a câmera está ativa...", + "noCameras": "Sem câmeras disponíveis", + "detectionCount_one": "Objeto localizado", + "detectionCount_many": "{{count}} objetos localizados", + "detectionCount_other": "{{count}} objetos localizados", + "nameLabel": "Nome do arquivo exportado", + "namePlaceholder": "Padrão de nome para exportação", + "queueingButton": "Exportações na fila...", + "exportButton_one": "Exportar câmera", + "exportButton_many": "Exportar {{count}} câmeras", + "exportButton_other": "Exportar {{count}} câmeras", + "searchOrSelectGroup": "Procurar, ou selecionar um grupo de câmeras...", + "selectAll": "Selecionar todas as câmeras", + "clearSelection": "Limpar seleção", + "selectWithActivity": "Câmeras com rastreamento de objetos", + "selectGroup": "Selecione grupo", + "noMatchingCameras": "Nenhuma câmera corresponde à sua pesquisa", + "selectedCount": "{{selected}} / {{total}} selecionados" + }, + "multi": { + "title_one": "Exportar análise", + "title_many": "Exportar {{count}} análises", + "title_other": "Exportar {{count}} análises", + "description": "Exportar cada análise selecionada. Todas as exportações serão agrupadas em um único caso.", + "descriptionNoCase": "Exportar cada análise selecionada.", + "caseNamePlaceholder": "Exportar análise - {{date}}", + "exportButton_one": "Exportar análise", + "exportButton_many": "Exportar {{count}} análises", + "exportButton_other": "Exportar {{count}} análises", + "exportingButton": "Exportando...", + "toast": { + "started_one": "Uma exportação iniciada. Abrindo o caso agora.", + "started_many": "Iniciadas {{count}} exportações. Abrindo o caso agora.", + "started_other": "", + "failed": "Falha ao iniciar {{total}} exportações. Falhas: {{failedItems}}", + "partial": "Iniciadas {{successful}} de {{total}} exportações. Falhas: {{failedItems}}" + } + } + }, + "streaming": { + "label": "Transmissão", + "restreaming": { + "disabled": "A retransmissão não está habilitada para essa câmera.", + "desc": { + "title": "Configurar o go2rtc para opções de visualização ao vivo e audio adicional para essa câmera.", + "readTheDocumentation": "Leia a documentação" + } + }, + "showStats": { + "label": "Exibir estatísticas da transmissão", + "desc": "Habilite essa opção para exibir as estatísticas de transmissão como uma sobreposição na transmissão da câmera." + }, + "debugView": "Visualização do Depurador" + }, + "search": { + "saveSearch": { + "label": "Salvar Busca", + "desc": "Indique um nome para essa pesquisa salva.", + "placeholder": "Dê um nome para a sua busca", + "overwrite": "{{searchName}} já existe. Salvar substituirá o valor existente.", + "success": "A pesquisa ({{searchName}}) foi salva.", + "button": { + "save": { + "label": "Salvar esta pesquisa" + } + } + } + }, + "recording": { + "confirmDelete": { + "desc": { + "selected": "Tem certeza de que deseja excluir todos os vídeos gravados associados a este item de revisão?

    Segure a tecla Shift para ignorar esta caixa de diálogo no futuro." + }, + "toast": { + "success": "As filmagens associadas aos itens de revisão selecionados foram excluídas com sucesso.", + "error": "Falha ao deletar: {{error}}" + }, + "title": "Confirmar Exclusão" + }, + "button": { + "markAsReviewed": "Marcar como revisado", + "export": "Exportar", + "deleteNow": "Deletar Agora", + "markAsUnreviewed": "Marcar como não revisado" + }, + "shareTimestamp": { + "label": "Compartilhar marca de tempo", + "title": "Compartilhar marca de tempo", + "description": "Compartilhe uma URL com marca de tempo da posição atual da reprodução ou escolha uma marca de tempo personalizada. Observe que esta não é uma URL de compartilhamento público e só pode ser acessada por usuários que tenham acesso ao Frigate e a esta câmera.", + "custom": "Marcação de data/hora personalizado", + "button": "Compartilhar URL com marcação de tempo", + "shareTitle": "Marcação de data/hora da análise do Frigate: {{camera}}" + } + }, + "imagePicker": { + "selectImage": "Selecionar a miniatura de um objeto rastreado", + "search": { + "placeholder": "Pesquisar por rótulo ou sub-rótulo…" + }, + "noImages": "Nenhuma miniatura encontrada para essa câmera", + "unknownLabel": "Imagem de Gatilho Salva" + } +} diff --git a/web/public/locales/pt-BR/components/filter.json b/web/public/locales/pt-BR/components/filter.json new file mode 100644 index 0000000..cb76078 --- /dev/null +++ b/web/public/locales/pt-BR/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filtro", + "labels": { + "label": "Rótulos", + "all": { + "title": "Todos os Rótulos", + "short": "Rótulos" + }, + "count_one": "{{count}} Rótulo", + "count_other": "{{count}} Rótulos" + }, + "zones": { + "label": "Zonas", + "all": { + "title": "Todas as Zonas", + "short": "Zonas" + } + }, + "dates": { + "selectPreset": "Selecione uma predefinição…", + "all": { + "title": "Todas as datas", + "short": "Datas" + } + }, + "more": "Mais Filtros", + "reset": { + "label": "Resetar filtros para valores padrão" + }, + "timeRange": "Intervalo de Tempo", + "subLabels": { + "label": "Sub-Rótulos", + "all": "Todos os Sub-Rótulos" + }, + "score": "Pontuação", + "estimatedSpeed": "Velocidade Estimada {{unit}}", + "features": { + "hasSnapshot": "Tem um snapshot", + "label": "Características", + "hasVideoClip": "Possui videoclipe", + "submittedToFrigatePlus": { + "label": "Enviado ao Frigate+", + "tips": "Você deve filtrar primeiro objetos que possuem capturas de imagem.

    Objetos rastreados sem capturas de imagem não serão enviados ao Frigate+." + } + }, + "sort": { + "label": "Ordenar", + "dateAsc": "Data (Ascendente)", + "dateDesc": "Data (Descendente)", + "scoreAsc": "Pontuação do Objeto (Ascendente)", + "scoreDesc": "Pontuação de Objeto (Descendente)", + "speedAsc": "Velocidade Estimada (Ascendente)", + "speedDesc": "Velocidade Estimada (Descendente)", + "relevance": "Relevância" + }, + "cameras": { + "label": "Filtro de Câmeras", + "all": { + "title": "Todas as Câmeras", + "short": "Câmeras" + } + }, + "review": { + "showReviewed": "Exibir Revisados" + }, + "motion": { + "showMotionOnly": "Exibir Movimento Apenas" + }, + "explore": { + "settings": { + "title": "Configurações", + "defaultView": { + "title": "Visualização Padrão", + "desc": "Quando nenhum filtro é selecionado, exibe um sumário dos objetos mais recentes rastreados por rótulo, ou exibe uma grade sem filtro.", + "summary": "Sumário", + "unfilteredGrid": "Grade Sem Filtros" + }, + "gridColumns": { + "desc": "Selecione o número de colunas na visualização em grade.", + "title": "Colunas de Grade" + }, + "searchSource": { + "desc": "Escolha se deseja pesquisar nas miniaturas ou descrições dos seus objetos rastreados.", + "label": "Buscar Fonte", + "options": { + "thumbnailImage": "Imagem da Miniatura", + "description": "Descrição" + } + } + }, + "date": { + "selectDateBy": { + "label": "Selecione uma data para filtrar" + } + } + }, + "logSettings": { + "label": "Nível de filtro de log", + "filterBySeverity": "Filtrar logs por severidade", + "loading": { + "title": "Carregando", + "desc": "Quando o painel de log é rolado para baixo, novos logs são transmitidos automaticamente conforme são adicionados." + }, + "disableLogStreaming": "Desativar o log de tranmissão", + "allLogs": "Todos os logs" + }, + "trackedObjectDelete": { + "title": "Confirmar Exclusão", + "desc": "Deletar esses {{objectLength}} objetos rastreados remove as capturas de imagem, quaisquer embeddings salvos, e quaisquer entradas do ciclo de vida associadas do objeto. Gravações desses objetos rastreados na visualização de Histórico NÃO irão ser deletadas.

    Tem certeza que quer proceder?

    Segure a tecla Shift para pular esse diálogo no futuro.", + "toast": { + "success": "Objetos rastreados deletados com sucesso.", + "error": "Falha ao deletar objeto rastreado: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrar por máscara de zona" + }, + "recognizedLicensePlates": { + "title": "Placas de Identificação Reconhecidas", + "loadFailed": "Falha ao carregar placas de identificação reconhecidas.", + "loading": "Carregando placas de identificação reconhecidas…", + "placeholder": "Digite para pesquisar por placas de identificação…", + "noLicensePlatesFound": "Nenhuma placa de identificação encontrada.", + "selectPlatesFromList": "Seleciona uma ou mais placas da lista.", + "selectAll": "Selecionar todos", + "clearAll": "Limpar todos" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Todas as Classes" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classes" + }, + "attributes": { + "label": "Atributos de Classificação", + "all": "Todos os Atributos" + } +} diff --git a/web/public/locales/pt-BR/components/icons.json b/web/public/locales/pt-BR/components/icons.json new file mode 100644 index 0000000..c038a02 --- /dev/null +++ b/web/public/locales/pt-BR/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecione um ícone", + "search": { + "placeholder": "Buscar por um ícone…" + } + } +} diff --git a/web/public/locales/pt-BR/components/input.json b/web/public/locales/pt-BR/components/input.json new file mode 100644 index 0000000..25a8190 --- /dev/null +++ b/web/public/locales/pt-BR/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Baixar Video", + "toast": { + "success": "Sua análise do item de vídeo começou a ser baixado." + } + } + } +} diff --git a/web/public/locales/pt-BR/components/player.json b/web/public/locales/pt-BR/components/player.json new file mode 100644 index 0000000..e783725 --- /dev/null +++ b/web/public/locales/pt-BR/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "Nenhuma gravação encontrada para este horário", + "noPreviewFound": "Nenhuma pré-visualização encontrada", + "noPreviewFoundFor": "Nenhuma Pré-Visualização Encontrada em {{cameraName}}", + "submitFrigatePlus": { + "title": "Enviar esse frame para Frigate+?", + "submit": "Enviar", + "previewError": "Não foi possível carregar a prévia do instantâneo. A gravação pode não estar disponível no momento." + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 ou superior é necessário para esse tipo de transmissão ao vivo.", + "streamOffline": { + "title": "Stream Offiline", + "desc": "Nenhum quadro foi recebido na stream {{cameraName}}detect, checar registros de erros" + }, + "cameraDisabled": "A câmera está desativada", + "stats": { + "streamType": { + "title": "Tipo de fluxo:", + "short": "Tipo" + }, + "bandwidth": { + "title": "Largura de banda:", + "short": "Largura de banda" + }, + "latency": { + "title": "Latência:", + "value": "{{seconds}} segundos", + "short": { + "title": "Latência", + "value": "{{seconds}} s" + } + }, + "totalFrames": "Total de Quadros:", + "droppedFrames": { + "title": "Quadros perdidos:", + "short": { + "title": "Perdidos", + "value": "{{droppedFrames}} quadros" + } + }, + "decodedFrames": "Quadros Decodificados:", + "droppedFrameRate": "Taxa de Quadros Perdidos:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Quadro enviado ao Frigate+ com sucesso" + }, + "error": { + "submitFrigatePlusFailed": "Falha em submeter quadro ao Frigate+" + } + }, + "cameraOff": "A câmera está desligada" +} diff --git a/web/public/locales/pt-BR/config/cameras.json b/web/public/locales/pt-BR/config/cameras.json new file mode 100644 index 0000000..4be948c --- /dev/null +++ b/web/public/locales/pt-BR/config/cameras.json @@ -0,0 +1,545 @@ +{ + "name": { + "label": "Nome da câmera", + "description": "Nome da câmera é obrigatório" + }, + "friendly_name": { + "label": "Nome amigável", + "description": "Nome amigável da câmera utilizado na Interface de Usuário do Frigate" + }, + "enabled": { + "label": "Habilitado", + "description": "Habilitado" + }, + "audio": { + "label": "Detecção de áudio", + "description": "Configurações para detecção de eventos baseados em áudio para esta câmera.", + "enabled": { + "label": "Habilitar detecção de áudio", + "description": "Habilitar ou desabilitar o evento de detecção de áudio para esta câmera." + }, + "max_not_heard": { + "label": "Tempo limite final", + "description": "Quantidade de segundos sem o tipo de áudio configurado antes do término do evento de áudio." + }, + "min_volume": { + "label": "Volume mínimo", + "description": "Limiar mínimo de volume RMS necessário para executar a detecção de áudio; valores mais baixos aumentam a sensibilidade (por exemplo, 200 para volume alto, 500 para volume médio, 1000 para volume baixo)." + }, + "listen": { + "label": "Tipos de escuta", + "description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, fala, grito)." + }, + "filters": { + "label": "Filtros de áudio", + "description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos.", + "threshold": { + "label": "Confiança mínima do áudio", + "description": "Limite mínimo de confiança para que o evento de áudio seja contabilizado." + } + }, + "enabled_in_config": { + "label": "Estado de áudio original", + "description": "Indica se a detecção de áudio foi originalmente ativada no arquivo de configuração estática." + }, + "num_threads": { + "label": "Threads de detecção", + "description": "Número de threads a serem usadas para o processamento de detecção de áudio." + } + }, + "label": "Configuração da Câmera", + "audio_transcription": { + "label": "Transcrição de áudio", + "enabled": { + "label": "Habilitar transcrição", + "description": "Ative ou desative a transcrição de eventos de áudio acionada manualmente." + }, + "live_enabled": { + "label": "Transcrição em tempo real", + "description": "Ative a transcrição ao vivo por streaming para o áudio à medida que ele é recebido." + }, + "description": "Configurações de transcrição de áudio e voz ao vivo para eventos e legendas em tempo real.", + "enabled_in_config": { + "label": "Estado original da transcrição" + } + }, + "detect": { + "enabled": { + "label": "Ativar detecção de objetos", + "description": "Ative ou desative a detecção de objetos para esta câmera." + }, + "height": { + "label": "Detectar altura", + "description": "Altura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo." + }, + "width": { + "label": "Detectar largura", + "description": "Largura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo." + }, + "fps": { + "label": "Detectar FPS", + "description": "Taxa de quadros por segundo desejada para a execução da detecção; valores mais baixos reduzem o uso da CPU (o valor recomendado é 5; defina um valor mais alto — no máximo 10 — apenas se estiver rastreando objetos que se movem extremamente rápido)." + }, + "min_initialized": { + "label": "Quadros de inicialização mínimos", + "description": "Número de detecções consecutivas necessárias antes de criar um objeto rastreado. Aumente esse valor para reduzir inicializações falsas. O valor padrão é o fps dividido por 2." + }, + "max_disappeared": { + "label": "Máximo de quadros perdidos", + "description": "Número de quadros sem detecção antes que um objeto rastreado seja considerado desaparecido." + }, + "stationary": { + "label": "Configuração de objetos estáticos", + "description": "Configurações para detectar e gerenciar objetos que permanecem parados por um período de tempo.", + "interval": { + "label": "Intervalo estacionário", + "description": "Com que frequência (em quadros) realizar uma verificação de detecção para confirmar um objeto estacionário." + }, + "threshold": { + "label": "Limiar estacionário", + "description": "Número de quadros sem alteração de posição necessários para marcar um objeto como estacionário." + }, + "max_frames": { + "label": "Quadros máximos", + "description": "Limita por quanto tempo objetos estacionários são rastreados antes de serem descartados.", + "default": { + "label": "Máximo de quadros padrão", + "description": "Número máximo padrão de quadros para rastrear um objeto estacionário antes de parar." + }, + "objects": { + "label": "Quadros máximos do objeto", + "description": "Substituições por objeto para o número máximo de quadros de rastreamento de objetos estacionários." + } + }, + "classifier": { + "label": "Ativar classificador visual", + "description": "Use um classificador visual para detectar objetos verdadeiramente estacionários, mesmo quando as caixas delimitadoras oscilam." + } + }, + "annotation_offset": { + "label": "Deslocamento da anotação", + "description": "Milissegundos para deslocar as anotações de detecção, a fim de alinhar melhor as caixas delimitadoras da linha do tempo com as gravações; pode ser um valor positivo ou negativo." + }, + "label": "Detecção de Objetos", + "description": "Configurações para a função de detecção (*detect*) usada para executar a detecção de objetos e inicializar rastreadores." + }, + "face_recognition": { + "label": "Reconhecimento facial", + "description": "Configurações de detecção e reconhecimento facial para esta câmera.", + "enabled": { + "label": "Ativar reconhecimento facial", + "description": "Ative ou desative o reconhecimento facial." + }, + "min_area": { + "label": "Área mínima do rosto", + "description": "Área mínima (em pixels) da caixa delimitadora de um rosto detectado necessária para tentar o reconhecimento." + } + }, + "ffmpeg": { + "label": "Fluxos (FFmpeg)", + "description": "Entradas de fluxo de câmera e opções do FFmpeg, incluindo caminho do binário, argumentos, aceleração de hardware (hwaccel) e argumentos de saída por função.", + "path": { + "label": "Caminho do FFmpeg", + "description": "Caminho para o binário do FFmpeg a ser utilizado ou um alias de versão (\"7.0\" ou \"8.0\")." + }, + "global_args": { + "label": "Argumentos globais do FFmpeg", + "description": "Argumentos globais passados para processos do FFmpeg." + }, + "hwaccel_args": { + "label": "Argumentos de aceleração de hardware", + "description": "Argumentos de aceleração de hardware para o FFmpeg. Recomenda-se o uso de predefinições específicas do provedor." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada aplicados aos fluxos de entrada do FFmpeg." + }, + "output_args": { + "label": "Argumentos de saída", + "description": "Argumentos de saída padrão utilizados para diferentes funções do FFmpeg, como detecção e gravação.", + "detect": { + "label": "Detectar argumentos de saída", + "description": "Argumentos de saída padrão para fluxos de detecção de função." + }, + "record": { + "label": "Registrar argumentos de saída", + "description": "Argumentos de saída padrão para fluxos de função de registro." + } + }, + "retry_interval": { + "label": "Tempo de nova tentativa do FFmpeg", + "description": "Segundos de espera antes de tentar reconectar um fluxo de câmera após uma falha. O padrão é 10." + }, + "apple_compatibility": { + "label": "Compatibilidade com a Apple", + "description": "Ative a marcação HEVC para melhor compatibilidade com reprodutores da Apple ao gravar em H.265." + }, + "gpu": { + "label": "Índice da GPU", + "description": "Índice da GPU padrão usado para aceleração de hardware, se disponível." + }, + "inputs": { + "label": "Entradas de câmera", + "description": "Lista de definições de fluxo de entrada (caminhos e funções) para esta câmera.", + "path": { + "label": "Caminho de entrada", + "description": "URL ou caminho do fluxo de entrada da câmera." + }, + "roles": { + "label": "Funções de entrada", + "description": "Funções para este fluxo de entrada." + }, + "global_args": { + "label": "Argumentos globais do FFmpeg", + "description": "Argumentos globais do FFmpeg para este fluxo de entrada." + }, + "hwaccel_args": { + "label": "Argumentos de aceleração de hardware", + "description": "Argumentos de aceleração de hardware para este fluxo de entrada." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada específicos para este fluxo." + } + } + }, + "birdseye": { + "mode": { + "label": "Modo de rastreamento", + "description": "Modo de inclusão de câmeras no Birdseye: 'objetos', 'movimento' ou 'contínuo'." + }, + "order": { + "label": "Posição", + "description": "Posição numérica que controla a ordem da câmera no layout de vista aérea (Birdseye)." + }, + "label": "Birdseye", + "description": "Configurações para a visualização composta \"Birdseye\", que combina múltiplas transmissões de câmera em um único layout.", + "enabled": { + "label": "Ativar Birdseye" + } + }, + "live": { + "label": "Reprodução ao vivo", + "description": "Configurações utilizadas pela interface web para controlar a seleção, a resolução e a qualidade da transmissão ao vivo.", + "streams": { + "label": "Nomes de transmissões ao vivo", + "description": "Mapeamento de nomes de fluxos configurados para nomes de restream/go2rtc usados na reprodução ao vivo." + }, + "height": { + "description": "Altura (em pixels) para renderizar a transmissão ao vivo jsmpeg na interface web; deve ser menor ou igual à altura da transmissão de detecção." + }, + "quality": { + "description": "Qualidade de codificação para o fluxo jsmpeg (1 para a mais alta, 31 para a mais baixa)." + } + }, + "motion": { + "enabled": { + "label": "Ativar detecção de movimento", + "description": "Ative ou desative a detecção de movimento para esta câmera." + }, + "threshold": { + "label": "Limiar de movimento", + "description": "Limiar de diferença de pixels utilizado pelo detector de movimento; valores mais altos reduzem a sensibilidade (faixa de 1 a 255)." + }, + "lightning_threshold": { + "label": "Limiar de relâmpago", + "description": "Limite para detectar e ignorar picos breves de luminosidade (valores mais baixos indicam maior sensibilidade; valores entre 0,3 e 1,0). Isso não impede totalmente a detecção de movimento; apenas faz com que o detector pare de analisar quadros adicionais assim que o limite é excedido. Gravações baseadas em movimento continuam sendo geradas durante esses eventos." + }, + "skip_motion_threshold": { + "label": "Limite de movimento para pular", + "description": "Se definido para um valor entre 0,0 e 1,0, e se uma proporção da imagem superior a esse valor sofrer alterações em um único quadro, o detector não retornará caixas de detecção de movimento e será recalibrado imediatamente. Isso pode economizar recursos da CPU e reduzir falsos positivos causados por relâmpagos, tempestades, etc., mas pode deixar passar eventos reais, como o rastreamento automático de um objeto por uma câmera PTZ. A escolha envolve um equilíbrio entre descartar alguns megabytes de gravações e analisar alguns clipes curtos. Deixe a opção não definida (None) para desativar esse recurso." + }, + "improve_contrast": { + "label": "Melhorar contraste", + "description": "Aplique melhoria de contraste aos quadros antes da análise de movimento para auxiliar na detecção." + }, + "contour_area": { + "label": "Área de contorno", + "description": "Área mínima do contorno, em pixels, necessária para que um contorno de movimento seja contabilizado." + }, + "delta_alpha": { + "label": "Delta Alfa", + "description": "Fator de *alpha blending* utilizado na diferenciação de quadros para o cálculo de movimento." + }, + "frame_alpha": { + "label": "Quadro alfa", + "description": "Valor alfa utilizado ao combinar quadros para o pré-processamento de movimento." + }, + "frame_height": { + "label": "Altura da quadro", + "description": "Altura em pixels para a qual os quadros são redimensionados ao calcular o movimento." + }, + "mask": { + "label": "Coordenadas da máscara", + "description": "Coordenadas x,y ordenadas que definem o polígono da máscara de movimento utilizado para incluir ou excluir áreas." + }, + "mqtt_off_delay": { + "label": "Atraso de desligamento MQTT", + "description": "Segundos de espera após o último movimento antes de publicar um estado \"off\" via MQTT." + }, + "enabled_in_config": { + "label": "Estado de movimento original", + "description": "Indica se a detecção de movimento estava habilitada na configuração estática original." + }, + "raw_mask": { + "label": "Máscara Bruta" + }, + "label": "Detecção de movimento", + "description": "Configurações padrão de detecção de movimento para esta câmera." + }, + "objects": { + "label": "Objetos", + "description": "Configurações padrão de rastreamento de objetos, incluindo quais rótulos rastrear e filtros por objeto.", + "track": { + "label": "Objetos a rastrear", + "description": "Lista de rótulos de objetos a serem rastreados para esta câmera." + }, + "filters": { + "label": "Filtros de objeto", + "description": "Filtros aplicados aos objetos detectados para reduzir falsos positivos (área, proporção, confiança).", + "min_area": { + "label": "Área mínima do objeto", + "description": "Área mínima da caixa delimitadora (em pixels ou porcentagem) necessária para este tipo de objeto. Pode ser especificada em pixels (inteiro) ou porcentagem (valor de ponto flutuante entre 0,000001 e 0,99)." + }, + "max_area": { + "label": "Área máxima do objeto", + "description": "Área máxima da caixa delimitadora (pixels ou porcentagem) permitida para este tipo de objeto. Pode ser pixels (int) ou porcentagem (flutuar entre 0,000001 e 0,99)." + }, + "min_ratio": { + "label": "Razão de aspecto mínimo", + "description": "Razão mínima entre largura e altura necessária para que a caixa delimitadora se qualifique." + }, + "max_ratio": { + "label": "Razão de aspecto máxima", + "description": "Razão máxima entre largura e altura permitida para que a caixa delimitadora qualifique." + }, + "threshold": { + "label": "Limiar de confiança", + "description": "Limiar médio de confiança de detecção necessário para que o objeto seja considerado um verdadeiro positivo." + }, + "min_score": { + "label": "Confiança mínima", + "description": "Confiança mínima de detecção em um único quadro necessária para que o objeto seja contabilizado." + }, + "mask": { + "label": "Máscara com filtro", + "description": "Coordenadas do polígono que definem onde este filtro é aplicado dentro do quadro." + }, + "raw_mask": { + "label": "Máscara Bruta" + } + }, + "mask": { + "label": "Máscara de objeto", + "description": "Polígono de máscara usado para impedir a detecção de objetos em áreas específicas." + }, + "raw_mask": { + "label": "Máscara Bruta" + }, + "genai": { + "label": "Configuração de objeto da GenAI", + "description": "Opções de GenIA para descrever objetos rastreados e enviar quadros para geração.", + "enabled": { + "label": "Ativar GenAI", + "description": "Habilitar por padrão a geração de descrições para objetos rastreados usando GenAI." + }, + "use_snapshot": { + "label": "Usar instantâneos", + "description": "Use instantâneos de objetos em vez de miniaturas para a geração de descrições pela GenAI." + }, + "prompt": { + "label": "Sugestão de legenda", + "description": "Modelo de prompt padrão usado ao gerar descrições com GenAI." + }, + "object_prompts": { + "label": "Prompts de objeto", + "description": "Prompts por objeto para personalizar as saídas de GenAI para rótulos específicos." + }, + "objects": { + "label": "Objetos de GenAI", + "description": "Lista de rótulos de objetos a serem enviados à GenAI por padrão." + }, + "required_zones": { + "label": "Zonas obrigatórias", + "description": "Zonas que devem ser acessadas para que os objetos se qualifiquem para a geração de descrições pela GenAI." + }, + "debug_save_thumbnails": { + "label": "Salvar miniaturas", + "description": "Salve as miniaturas enviadas à GenAI para depuração e revisão." + }, + "send_triggers": { + "label": "Gatilhos de GenAI", + "description": "Define quando os quadros devem ser enviados para a GenAI (ao final, após atualizações, etc.).", + "tracked_object_end": { + "label": "Enviar no fim", + "description": "Envie uma solicitação à GenAI quando o objeto rastreado terminar." + }, + "after_significant_updates": { + "label": "Acionamento antecipado da GenAI", + "description": "Envie uma solicitação à GenAI após um número especificado de atualizações significativas do objeto monitorado." + } + }, + "enabled_in_config": { + "label": "Estado original da GenAI", + "description": "Indica se a GenAI foi habilitada na configuração estática original." + } + } + }, + "lpr": { + "enabled": { + "label": "Ativar LPR", + "description": "Ative ou desative o LPR nesta câmera." + }, + "expire_time": { + "label": "Segundos para expirar", + "description": "Tempo, em segundos, após o qual uma placa não detectada é removida do rastreador (apenas para câmeras LPR dedicadas)." + }, + "min_area": { + "label": "Área mínima da placa", + "description": "Área mínima da placa (em pixels) necessária para tentar o reconhecimento." + }, + "enhancement": { + "label": "Nível de aprimoramento", + "description": "Nível de aprimoramento (0-10) a ser aplicado aos recortes da placa antes do OCR; valores mais altos nem sempre melhoram os resultados, e níveis acima de 5 podem funcionar apenas com placas noturnas, devendo ser utilizados com cautela." + } + }, + "record": { + "label": "Gravação", + "description": "Configurações de gravação e retenção para esta câmera.", + "enabled": { + "label": "Ativar gravação", + "description": "Ative ou desative a gravação para esta câmera." + }, + "expire_interval": { + "label": "Intervalo de limpeza de gravação", + "description": "Minutos entre as execuções de limpeza que removem segmentos de gravação expirados." + }, + "continuous": { + "label": "Retenção contínua", + "description": "Número de dias para manter as gravações, independentemente de objetos rastreados ou movimento. Defina como 0 se quiser manter apenas gravações de alertas e detecções.", + "days": { + "label": "Dias de retenção", + "description": "Dias para manter as gravações." + } + }, + "motion": { + "label": "Retenção de movimento", + "description": "Número de dias para manter gravações acionadas por movimento, independentemente dos objetos rastreados. Defina como 0 se quiser manter apenas gravações de alertas e detecções.", + "days": { + "label": "Dias de retenção", + "description": "Dias para manter as gravações." + } + }, + "detections": { + "label": "Retenção de detecção", + "description": "Configurações de retenção de gravação para eventos de detecção, incluindo durações de pré e pós-captura.", + "pre_capture": { + "label": "Segundos de pré-captura", + "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação." + }, + "post_capture": { + "label": "Segundos após a captura", + "description": "Número de segundos após o evento de detecção a serem incluídos na gravação." + }, + "retain": { + "label": "Retenção de eventos", + "description": "Configurações de retenção para gravações de eventos de detecção.", + "days": { + "label": "Dias de retenção", + "description": "Número de dias para manter as gravações de eventos de detecção." + }, + "mode": { + "label": "Modo de retenção", + "description": "Modo de retenção: all (salva todos os segmentos), motion (salva segmentos com movimento) ou active_objects (salva segmentos com objetos ativos)." + } + } + }, + "alerts": { + "label": "Retenção de alertas", + "description": "Configurações de retenção de gravações para eventos de alerta, incluindo durações de pré e pós-captura.", + "pre_capture": { + "label": "Segundos de pré-captura", + "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação." + }, + "post_capture": { + "label": "Segundos após a captura", + "description": "Número de segundos após o evento de detecção a serem incluídos na gravação." + }, + "retain": { + "label": "Retenção de eventos", + "description": "Configurações de retenção para gravações de eventos de detecção.", + "days": { + "label": "Dias de retenção", + "description": "Número de dias para manter as gravações de eventos de detecção." + }, + "mode": { + "label": "Modo de retenção", + "description": "Modo de retenção: all (salvar todos os segmentos), motion (salvar segmentos com movimento) ou active_objects (salvar segmentos com objetos ativos)." + } + } + }, + "export": { + "label": "Exportar configuração", + "description": "Configurações utilizadas ao exportar gravações, como timelapse e aceleração de hardware.", + "hwaccel_args": { + "label": "Exportar argumentos de hwaccel", + "description": "Argumentos de aceleração de hardware a serem usados para operações de exportação/transcodificação." + }, + "max_concurrent": { + "label": "Máximo de exportações simultâneas", + "description": "Número máximo de tarefas de exportação a serem processadas simultaneamente." + }, + "chapters": { + "label": "Metadados de capítulos para incorporar nas gravações exportadas" + } + }, + "preview": { + "label": "Pré-visualizar configuração", + "description": "Configurações que controlam a qualidade das prévias de gravação exibidas na interface de usuário.", + "quality": { + "label": "Qualidade da pré-visualização", + "description": "Nível de qualidade da pré-visualização (muito_baixa, baixa, média, alta, muito_alta)." + } + }, + "enabled_in_config": { + "label": "Estado original da gravação", + "description": "Indica se a gravação foi habilitada na configuração estática original." + } + }, + "review": { + "label": "Rever", + "description": "Configurações que controlam alertas, detecções e resumos de revisão por GenAI utilizados pela interface do usuário e pelo armazenamento desta câmera.", + "alerts": { + "label": "Configuração de alertas", + "description": "Configurações sobre quais objetos monitorados geram alertas e como os alertas são retidos.", + "enabled": { + "label": "Ativar alertas", + "description": "Ative ou desative a geração de alertas para esta câmera." + }, + "labels": { + "label": "Etiquetas de alerta", + "description": "Lista de rótulos de objetos que se qualificam como alertas (por exemplo: carro, pessoa)." + }, + "required_zones": { + "label": "Zonas obrigatórias", + "description": "Zonas nas quais um objeto deve entrar para ser considerado um alerta; deixe em branco para permitir qualquer zona." + }, + "enabled_in_config": { + "label": "Estado dos alertas originais", + "description": "Monitora se os alertas estavam originalmente habilitados na configuração estática." + }, + "cutoff_time": { + "label": "Horário limite dos alertas", + "description": "Segundos de espera após a ausência de atividade que gere alerta antes de encerrar o alerta." + } + }, + "detections": { + "label": "Configuração de detecções", + "description": "Configurações que definem para quais objetos rastreados são geradas detecções (sem gerar alerta) e como essas detecções são retidas.", + "enabled": { + "label": "Ativar detecções", + "description": "Ative ou desative eventos de detecção para esta câmera." + } + } + } +} diff --git a/web/public/locales/pt-BR/config/global.json b/web/public/locales/pt-BR/config/global.json new file mode 100644 index 0000000..1cf4a28 --- /dev/null +++ b/web/public/locales/pt-BR/config/global.json @@ -0,0 +1,559 @@ +{ + "version": { + "label": "Versão atual da configuração", + "description": "Versão numérica ou em caracteres da configuração ativa para ajudar detectar migrações ou mudanças de formato." + }, + "safe_mode": { + "label": "Modo Seguro", + "description": "Quando habilitado, Frigate inicia em modo seguro com recursos reduzidos para solucionar problemas." + }, + "environment_vars": { + "label": "Variáveis de ambiente", + "description": "Pares de chave/valor de variáveis de ambiente para atribuir ao processo do Frigate no Home Assistant OS. Usuários que não usam HAOS devem usar variáveis de ambiente do Docker." + }, + "logger": { + "label": "Logando", + "description": "Controla o padrão de verbosidade de registro e sobrescrever o nível de registro por componente.", + "default": { + "label": "Nível de registro", + "description": "Padrão global de verbosidade de registro (debug, info, aviso, erro)." + }, + "logs": { + "label": "Nível de registro por processo", + "description": "Configurações de nível de registro por componente para aumentar ou diminuir a verbosidade de módulos específicos." + } + }, + "audio": { + "max_not_heard": { + "label": "Tempo limite final", + "description": "Quantidade de segundos sem o tipo de áudio configurado antes do término do evento de áudio." + }, + "min_volume": { + "label": "Volume mínimo", + "description": "Limiar mínimo de volume RMS necessário para executar a detecção de áudio; valores mais baixos aumentam a sensibilidade (por exemplo, 200 para volume alto, 500 para volume médio, 1000 para volume baixo)." + }, + "listen": { + "label": "Tipos de escuta", + "description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, fala, grito)." + }, + "filters": { + "label": "Filtros de áudio", + "description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos.", + "threshold": { + "label": "Confiança mínima do áudio", + "description": "Limite mínimo de confiança para que o evento de áudio seja contabilizado." + } + }, + "enabled_in_config": { + "label": "Estado de áudio original", + "description": "Indica se a detecção de áudio foi originalmente ativada no arquivo de configuração estática." + }, + "num_threads": { + "label": "Threads de detecção", + "description": "Número de threads a serem usadas para o processamento de detecção de áudio." + }, + "label": "Detecção de áudio" + }, + "auth": { + "label": "Autenticação", + "description": "Configurações de autenticação e relacionadas à sessão, incluindo opções de cookies e limite de taxa.", + "enabled": { + "label": "Habilitar autenticação", + "description": "Ative a autenticação nativa para a interface do usuário do Frigate." + }, + "reset_admin_password": { + "label": "Redefinir senha de administrador", + "description": "Se verdadeiro, redefina a senha do usuário administrador na inicialização e imprima a nova senha nos registros." + }, + "cookie_name": { + "label": "nome do cookie JWT", + "description": "Nome do cookie usado para armazenar o token JWT para autenticação nativa." + }, + "cookie_secure": { + "label": "Sinalizador de cookie seguro", + "description": "Defina o atributo \"secure\" no cookie de autenticação; ele deve ser verdadeiro ao usar TLS." + }, + "session_length": { + "label": "Duração da sessão", + "description": "Duração da sessão em segundos para sessões baseadas em JWT." + }, + "failed_login_rate_limit": { + "label": "Limites de falha de login" + }, + "refresh_time": { + "label": "Janela de atualização da sessão" + } + }, + "audio_transcription": { + "label": "Transcrição de áudio", + "live_enabled": { + "label": "Transcrição em tempo real", + "description": "Ative a transcrição ao vivo por streaming para o áudio à medida que ele é recebido." + }, + "description": "Configurações de transcrição de áudio e voz ao vivo para eventos e legendas em tempo real." + }, + "detect": { + "enabled": { + "label": "Ativar detecção de objetos" + }, + "height": { + "label": "Detectar altura", + "description": "Altura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo." + }, + "width": { + "label": "Detectar largura", + "description": "Largura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo." + }, + "fps": { + "label": "Detectar FPS", + "description": "Taxa de quadros por segundo desejada para a execução da detecção; valores mais baixos reduzem o uso da CPU (o valor recomendado é 5; defina um valor mais alto — no máximo 10 — apenas se estiver rastreando objetos que se movem extremamente rápido)." + }, + "min_initialized": { + "label": "Quadros de inicialização mínimos", + "description": "Número de detecções consecutivas necessárias antes de criar um objeto rastreado. Aumente esse valor para reduzir inicializações falsas. O valor padrão é o fps dividido por 2." + }, + "max_disappeared": { + "label": "Máximo de quadros perdidos", + "description": "Número de quadros sem detecção antes que um objeto rastreado seja considerado desaparecido." + }, + "stationary": { + "label": "Configuração de objetos estáticos", + "description": "Configurações para detectar e gerenciar objetos que permanecem parados por um período de tempo.", + "interval": { + "label": "Intervalo estacionário", + "description": "Com que frequência (em quadros) realizar uma verificação de detecção para confirmar um objeto estacionário." + }, + "threshold": { + "label": "Limiar estacionário", + "description": "Número de quadros sem alteração de posição necessários para marcar um objeto como estacionário." + }, + "max_frames": { + "label": "Quadros máximos", + "description": "Limita por quanto tempo objetos estacionários são rastreados antes de serem descartados.", + "default": { + "label": "Máximo de quadros padrão", + "description": "Número máximo padrão de quadros para rastrear um objeto estacionário antes de parar." + }, + "objects": { + "label": "Quadros máximos do objeto", + "description": "Substituições por objeto para o número máximo de quadros de rastreamento de objetos estacionários." + } + }, + "classifier": { + "label": "Ativar classificador visual", + "description": "Use um classificador visual para detectar objetos verdadeiramente estacionários, mesmo quando as caixas delimitadoras oscilam." + } + }, + "annotation_offset": { + "label": "Deslocamento da anotação", + "description": "Milissegundos para deslocar as anotações de detecção, a fim de alinhar melhor as caixas delimitadoras da linha do tempo com as gravações; pode ser um valor positivo ou negativo." + }, + "label": "Detecção de Objetos", + "description": "Configurações para a função de detecção (*detect*) usada para executar a detecção de objetos e inicializar rastreadores." + }, + "face_recognition": { + "label": "Reconhecimento facial", + "enabled": { + "label": "Ativar reconhecimento facial" + }, + "min_area": { + "label": "Área mínima do rosto", + "description": "Área mínima (em pixels) da caixa delimitadora de um rosto detectado necessária para tentar o reconhecimento." + } + }, + "ffmpeg": { + "path": { + "label": "Caminho do FFmpeg", + "description": "Caminho para o binário do FFmpeg a ser utilizado ou um alias de versão (\"7.0\" ou \"8.0\")." + }, + "global_args": { + "label": "Argumentos globais do FFmpeg", + "description": "Argumentos globais passados para processos do FFmpeg." + }, + "hwaccel_args": { + "label": "Argumentos de aceleração de hardware", + "description": "Argumentos de aceleração de hardware para o FFmpeg. Recomenda-se o uso de predefinições específicas do provedor." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada aplicados aos fluxos de entrada do FFmpeg." + }, + "output_args": { + "label": "Argumentos de saída", + "description": "Argumentos de saída padrão utilizados para diferentes funções do FFmpeg, como detecção e gravação.", + "detect": { + "label": "Detectar argumentos de saída", + "description": "Argumentos de saída padrão para fluxos de detecção de função." + }, + "record": { + "label": "Registrar argumentos de saída", + "description": "Argumentos de saída padrão para fluxos de função de registro." + } + }, + "retry_interval": { + "label": "Tempo de nova tentativa do FFmpeg", + "description": "Segundos de espera antes de tentar reconectar um fluxo de câmera após uma falha. O padrão é 10." + }, + "apple_compatibility": { + "label": "Compatibilidade com a Apple", + "description": "Ative a marcação HEVC para melhor compatibilidade com reprodutores da Apple ao gravar em H.265." + }, + "gpu": { + "label": "Índice da GPU", + "description": "Índice da GPU padrão usado para aceleração de hardware, se disponível." + }, + "inputs": { + "label": "Entradas de câmera", + "description": "Lista de definições de fluxo de entrada (caminhos e funções) para esta câmera.", + "path": { + "label": "Caminho de entrada", + "description": "URL ou caminho do fluxo de entrada da câmera." + }, + "roles": { + "label": "Funções de entrada", + "description": "Funções para este fluxo de entrada." + }, + "global_args": { + "label": "Argumentos globais do FFmpeg", + "description": "Argumentos globais do FFmpeg para este fluxo de entrada." + }, + "hwaccel_args": { + "label": "Argumentos de aceleração de hardware", + "description": "Argumentos de aceleração de hardware para este fluxo de entrada." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada específicos para este fluxo." + } + } + }, + "birdseye": { + "mode": { + "label": "Modo de rastreamento", + "description": "Modo de inclusão de câmeras no Birdseye: 'objetos', 'movimento' ou 'contínuo'." + }, + "order": { + "label": "Posição", + "description": "Posição numérica que controla a ordem da câmera no layout de vista aérea (Birdseye)." + }, + "label": "Birdseye", + "description": "Configurações para a visualização composta \"Birdseye\", que combina múltiplas transmissões de câmera em um único layout.", + "enabled": { + "label": "Ativar Birdseye" + } + }, + "live": { + "label": "Reprodução ao vivo", + "streams": { + "label": "Nomes de transmissões ao vivo", + "description": "Mapeamento de nomes de fluxos configurados para nomes de restream/go2rtc usados na reprodução ao vivo." + }, + "height": { + "description": "Altura (em pixels) para renderizar a transmissão ao vivo jsmpeg na interface web; deve ser menor ou igual à altura da transmissão de detecção." + }, + "quality": { + "description": "Qualidade de codificação para o fluxo jsmpeg (1 para a mais alta, 31 para a mais baixa)." + } + }, + "motion": { + "enabled": { + "label": "Ativar detecção de movimento" + }, + "threshold": { + "label": "Limiar de movimento", + "description": "Limiar de diferença de pixels utilizado pelo detector de movimento; valores mais altos reduzem a sensibilidade (faixa de 1 a 255)." + }, + "lightning_threshold": { + "label": "Limiar de relâmpago", + "description": "Limite para detectar e ignorar picos breves de luminosidade (valores mais baixos indicam maior sensibilidade; valores entre 0,3 e 1,0). Isso não impede totalmente a detecção de movimento; apenas faz com que o detector pare de analisar quadros adicionais assim que o limite é excedido. Gravações baseadas em movimento continuam sendo geradas durante esses eventos." + }, + "skip_motion_threshold": { + "label": "Limite de movimento para pular", + "description": "Se definido para um valor entre 0,0 e 1,0, e se uma proporção da imagem superior a esse valor sofrer alterações em um único quadro, o detector não retornará caixas de detecção de movimento e será recalibrado imediatamente. Isso pode economizar recursos da CPU e reduzir falsos positivos causados por relâmpagos, tempestades, etc., mas pode deixar passar eventos reais, como o rastreamento automático de um objeto por uma câmera PTZ. A escolha envolve um equilíbrio entre descartar alguns megabytes de gravações e analisar alguns clipes curtos. Deixe a opção não definida (None) para desativar esse recurso." + }, + "improve_contrast": { + "label": "Melhorar contraste", + "description": "Aplique melhoria de contraste aos quadros antes da análise de movimento para auxiliar na detecção." + }, + "contour_area": { + "label": "Área de contorno", + "description": "Área mínima do contorno, em pixels, necessária para que um contorno de movimento seja contabilizado." + }, + "delta_alpha": { + "label": "Delta Alfa", + "description": "Fator de *alpha blending* utilizado na diferenciação de quadros para o cálculo de movimento." + }, + "frame_alpha": { + "label": "Quadro alfa", + "description": "Valor alfa utilizado ao combinar quadros para o pré-processamento de movimento." + }, + "frame_height": { + "label": "Altura da quadro", + "description": "Altura em pixels para a qual os quadros são redimensionados ao calcular o movimento." + }, + "mask": { + "label": "Coordenadas da máscara", + "description": "Coordenadas x,y ordenadas que definem o polígono da máscara de movimento utilizado para incluir ou excluir áreas." + }, + "mqtt_off_delay": { + "label": "Atraso de desligamento MQTT", + "description": "Segundos de espera após o último movimento antes de publicar um estado \"off\" via MQTT." + }, + "enabled_in_config": { + "label": "Estado de movimento original", + "description": "Indica se a detecção de movimento estava habilitada na configuração estática original." + }, + "raw_mask": { + "label": "Máscara Bruta" + }, + "label": "Detecção de movimento" + }, + "objects": { + "label": "Objetos", + "description": "Configurações padrão de rastreamento de objetos, incluindo quais rótulos rastrear e filtros por objeto.", + "track": { + "label": "Objetos a rastrear" + }, + "filters": { + "label": "Filtros de objeto", + "description": "Filtros aplicados aos objetos detectados para reduzir falsos positivos (área, proporção, confiança).", + "min_area": { + "label": "Área mínima do objeto", + "description": "Área mínima da caixa delimitadora (em pixels ou porcentagem) necessária para este tipo de objeto. Pode ser especificada em pixels (inteiro) ou porcentagem (valor de ponto flutuante entre 0,000001 e 0,99)." + }, + "max_area": { + "label": "Área máxima do objeto", + "description": "Área máxima da caixa delimitadora (pixels ou porcentagem) permitida para este tipo de objeto. Pode ser pixels (int) ou porcentagem (flutuar entre 0,000001 e 0,99)." + }, + "min_ratio": { + "label": "Razão de aspecto mínimo", + "description": "Razão mínima entre largura e altura necessária para que a caixa delimitadora se qualifique." + }, + "max_ratio": { + "label": "Razão de aspecto máxima", + "description": "Razão máxima entre largura e altura permitida para que a caixa delimitadora qualifique." + }, + "threshold": { + "label": "Limiar de confiança", + "description": "Limiar médio de confiança de detecção necessário para que o objeto seja considerado um verdadeiro positivo." + }, + "min_score": { + "label": "Confiança mínima", + "description": "Confiança mínima de detecção em um único quadro necessária para que o objeto seja contabilizado." + }, + "mask": { + "label": "Máscara com filtro", + "description": "Coordenadas do polígono que definem onde este filtro é aplicado dentro do quadro." + }, + "raw_mask": { + "label": "Máscara Bruta" + } + }, + "mask": { + "label": "Máscara de objeto", + "description": "Polígono de máscara usado para impedir a detecção de objetos em áreas específicas." + }, + "raw_mask": { + "label": "Máscara Bruta" + }, + "genai": { + "label": "Configuração de objeto da GenAI", + "description": "Opções de GenIA para descrever objetos rastreados e enviar quadros para geração.", + "enabled": { + "label": "Ativar GenAI", + "description": "Habilitar por padrão a geração de descrições para objetos rastreados usando GenAI." + }, + "use_snapshot": { + "label": "Usar instantâneos", + "description": "Use instantâneos de objetos em vez de miniaturas para a geração de descrições pela GenAI." + }, + "prompt": { + "label": "Sugestão de legenda", + "description": "Modelo de prompt padrão usado ao gerar descrições com GenAI." + }, + "object_prompts": { + "label": "Prompts de objeto", + "description": "Prompts por objeto para personalizar as saídas de GenAI para rótulos específicos." + }, + "objects": { + "label": "Objetos de GenAI", + "description": "Lista de rótulos de objetos a serem enviados à GenAI por padrão." + }, + "required_zones": { + "label": "Zonas obrigatórias", + "description": "Zonas que devem ser acessadas para que os objetos se qualifiquem para a geração de descrições pela GenAI." + }, + "debug_save_thumbnails": { + "label": "Salvar miniaturas", + "description": "Salve as miniaturas enviadas à GenAI para depuração e revisão." + }, + "send_triggers": { + "label": "Gatilhos de GenAI", + "description": "Define quando os quadros devem ser enviados para a GenAI (ao final, após atualizações, etc.).", + "tracked_object_end": { + "label": "Enviar no fim", + "description": "Envie uma solicitação à GenAI quando o objeto rastreado terminar." + }, + "after_significant_updates": { + "label": "Acionamento antecipado da GenAI", + "description": "Envie uma solicitação à GenAI após um número especificado de atualizações significativas do objeto monitorado." + } + }, + "enabled_in_config": { + "label": "Estado original da GenAI", + "description": "Indica se a GenAI foi habilitada na configuração estática original." + } + } + }, + "lpr": { + "enabled": { + "label": "Ativar LPR" + }, + "expire_time": { + "label": "Segundos para expirar", + "description": "Tempo, em segundos, após o qual uma placa não detectada é removida do rastreador (apenas para câmeras LPR dedicadas)." + }, + "min_area": { + "label": "Área mínima da placa", + "description": "Área mínima da placa (em pixels) necessária para tentar o reconhecimento." + }, + "enhancement": { + "label": "Nível de aprimoramento", + "description": "Nível de aprimoramento (0-10) a ser aplicado aos recortes da placa antes do OCR; valores mais altos nem sempre melhoram os resultados, e níveis acima de 5 podem funcionar apenas com placas noturnas, devendo ser utilizados com cautela." + } + }, + "record": { + "label": "Gravação", + "enabled": { + "label": "Ativar gravação" + }, + "expire_interval": { + "label": "Intervalo de limpeza de gravação", + "description": "Minutos entre as execuções de limpeza que removem segmentos de gravação expirados." + }, + "continuous": { + "label": "Retenção contínua", + "description": "Número de dias para manter as gravações, independentemente de objetos rastreados ou movimento. Defina como 0 se quiser manter apenas gravações de alertas e detecções.", + "days": { + "label": "Dias de retenção", + "description": "Dias para manter as gravações." + } + }, + "motion": { + "label": "Retenção de movimento", + "description": "Número de dias para manter gravações acionadas por movimento, independentemente dos objetos rastreados. Defina como 0 se quiser manter apenas gravações de alertas e detecções.", + "days": { + "label": "Dias de retenção", + "description": "Dias para manter as gravações." + } + }, + "detections": { + "label": "Retenção de detecção", + "description": "Configurações de retenção de gravação para eventos de detecção, incluindo durações de pré e pós-captura.", + "pre_capture": { + "label": "Segundos de pré-captura", + "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação." + }, + "post_capture": { + "label": "Segundos após a captura", + "description": "Número de segundos após o evento de detecção a serem incluídos na gravação." + }, + "retain": { + "label": "Retenção de eventos", + "description": "Configurações de retenção para gravações de eventos de detecção.", + "days": { + "label": "Dias de retenção", + "description": "Número de dias para manter as gravações de eventos de detecção." + }, + "mode": { + "label": "Modo de retenção", + "description": "Modo de retenção: all (salva todos os segmentos), motion (salva segmentos com movimento) ou active_objects (salva segmentos com objetos ativos)." + } + } + }, + "alerts": { + "label": "Retenção de alertas", + "description": "Configurações de retenção de gravações para eventos de alerta, incluindo durações de pré e pós-captura.", + "pre_capture": { + "label": "Segundos de pré-captura", + "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação." + }, + "post_capture": { + "label": "Segundos após a captura", + "description": "Número de segundos após o evento de detecção a serem incluídos na gravação." + }, + "retain": { + "label": "Retenção de eventos", + "description": "Configurações de retenção para gravações de eventos de detecção.", + "days": { + "label": "Dias de retenção", + "description": "Número de dias para manter as gravações de eventos de detecção." + }, + "mode": { + "label": "Modo de retenção", + "description": "Modo de retenção: all (salvar todos os segmentos), motion (salvar segmentos com movimento) ou active_objects (salvar segmentos com objetos ativos)." + } + } + }, + "export": { + "label": "Exportar configuração", + "description": "Configurações utilizadas ao exportar gravações, como timelapse e aceleração de hardware.", + "hwaccel_args": { + "label": "Exportar argumentos de hwaccel", + "description": "Argumentos de aceleração de hardware a serem usados para operações de exportação/transcodificação." + }, + "max_concurrent": { + "label": "Máximo de exportações simultâneas", + "description": "Número máximo de tarefas de exportação a serem processadas simultaneamente." + }, + "chapters": { + "label": "Metadados de capítulos para incorporar nas gravações exportadas" + } + }, + "preview": { + "label": "Pré-visualizar configuração", + "description": "Configurações que controlam a qualidade das prévias de gravação exibidas na interface de usuário.", + "quality": { + "label": "Qualidade da pré-visualização", + "description": "Nível de qualidade da pré-visualização (muito_baixa, baixa, média, alta, muito_alta)." + } + }, + "enabled_in_config": { + "label": "Estado original da gravação", + "description": "Indica se a gravação foi habilitada na configuração estática original." + } + }, + "review": { + "label": "Rever", + "alerts": { + "label": "Configuração de alertas", + "description": "Configurações sobre quais objetos monitorados geram alertas e como os alertas são retidos.", + "enabled": { + "label": "Ativar alertas" + }, + "labels": { + "label": "Etiquetas de alerta", + "description": "Lista de rótulos de objetos que se qualificam como alertas (por exemplo: carro, pessoa)." + }, + "required_zones": { + "label": "Zonas obrigatórias", + "description": "Zonas nas quais um objeto deve entrar para ser considerado um alerta; deixe em branco para permitir qualquer zona." + }, + "enabled_in_config": { + "label": "Estado dos alertas originais", + "description": "Monitora se os alertas estavam originalmente habilitados na configuração estática." + }, + "cutoff_time": { + "label": "Horário limite dos alertas", + "description": "Segundos de espera após a ausência de atividade que gere alerta antes de encerrar o alerta." + } + }, + "detections": { + "label": "Configuração de detecções", + "description": "Configurações que definem para quais objetos rastreados são geradas detecções (sem gerar alerta) e como essas detecções são retidas.", + "enabled": { + "label": "Ativar detecções" + } + } + } +} diff --git a/web/public/locales/pt-BR/config/groups.json b/web/public/locales/pt-BR/config/groups.json new file mode 100644 index 0000000..cfe1a88 --- /dev/null +++ b/web/public/locales/pt-BR/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Detecção Global", + "sensitivity": "Sensibilidade Global" + }, + "cameras": { + "detection": "Detecção", + "sensitivity": "Sensibilidade" + } + }, + "timestamp_style": { + "global": { + "appearance": "Aparência Global" + }, + "cameras": { + "appearance": "Aparência" + } + }, + "motion": { + "global": { + "sensitivity": "Sensibilidade Global", + "algorithm": "Algoritmo Global" + }, + "cameras": { + "sensitivity": "Sensibilidade", + "algorithm": "Algoritmo" + } + }, + "snapshots": { + "global": { + "display": "Exibição Global" + }, + "cameras": { + "display": "Exibição" + } + }, + "detect": { + "global": { + "resolution": "Resolução Global", + "tracking": "Rastreamento Global" + }, + "cameras": { + "resolution": "Resolução", + "tracking": "Monitorando" + } + }, + "objects": { + "global": { + "tracking": "Rastreamento Global", + "filtering": "Filtragem global" + }, + "cameras": { + "tracking": "Monitorando", + "filtering": "Filtragem" + } + }, + "record": { + "global": { + "retention": "Retenção Global", + "events": "Eventos Globais" + }, + "cameras": { + "retention": "Retenção", + "events": "Eventos" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Argumentos do FFmpeg específicos para a câmera" + } + } +} diff --git a/web/public/locales/pt-BR/config/validation.json b/web/public/locales/pt-BR/config/validation.json new file mode 100644 index 0000000..073d1a5 --- /dev/null +++ b/web/public/locales/pt-BR/config/validation.json @@ -0,0 +1,35 @@ +{ + "minimum": "Deve ser pelo menos {{limit}}", + "maximum": "Deve ser no máximo {{limit}}", + "exclusiveMinimum": "Deve ser maior do que {{limit}}", + "exclusiveMaximum": "Deve ser menor do que {{limit}}", + "minLength": "Deve ter pelo menos {{limit}} caractere(s)", + "maxLength": "Deve ter no máximo {{limit}} caractere(s)", + "minItems": "Deve ter pelo menos {{limit}} itens", + "maxItems": "Deve ter no máximo {{limit}} itens", + "pattern": "Formato inválido", + "required": "Esse campo é necessário", + "type": "Tipo de valor inválido", + "enum": "Deve ser um dos valores permitidos", + "const": "Valor não condiz com a constante esperada", + "uniqueItems": "Todos os itens devem ser únicos", + "format": "Formato inválido", + "additionalProperties": "Propriedade desconhecida não é permitida", + "oneOf": "Deve corresponder exatamente a um dos esquemas permitidos", + "anyOf": "Deve corresponder a pelo menos um dos esquemas permitidos", + "proxy": { + "header_map": { + "roleHeaderRequired": "O cabeçalho de função é obrigatório quando os mapeamentos de função são configurados." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "Cada função só pode ser atribuída a um fluxo de entrada.", + "detectRequired": "Pelo menos um fluxo de entrada deve ter a função 'detectar' atribuída.", + "hwaccelDetectOnly": "Somente o fluxo de entrada com a função de detecção pode definir argumentos de aceleração de hardware." + } + }, + "detect": { + "dimensionMustBeEven": "Deve ser um número par." + } +} diff --git a/web/public/locales/pt-BR/objects.json b/web/public/locales/pt-BR/objects.json new file mode 100644 index 0000000..48283d1 --- /dev/null +++ b/web/public/locales/pt-BR/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Pessoa", + "bicycle": "Bicicleta", + "car": "Carro", + "motorcycle": "Moto", + "airplane": "Avião", + "bus": "Ônibus", + "train": "Trem", + "boat": "Barco", + "traffic_light": "Semáforo", + "fire_hydrant": "Hidrante", + "street_sign": "Placa de rua", + "stop_sign": "Sinal de parada", + "parking_meter": "Parquímetro", + "bench": "Banco", + "bird": "Pássaro", + "cat": "Gato", + "dog": "Cachorro", + "horse": "Cavalo", + "sheep": "Ovelha", + "cow": "Vaca", + "elephant": "Elefante", + "bear": "Urso", + "zebra": "Zebra", + "giraffe": "Girafa", + "hat": "Chapéu", + "backpack": "Mochila", + "umbrella": "Guarda-Chuva", + "shoe": "Sapato", + "eye_glasses": "Óculos", + "handbag": "Bolsa", + "tie": "Gravata", + "suitcase": "Mala", + "frisbee": "Frisbe", + "skis": "Esquis", + "snowboard": "Snowboard", + "sports_ball": "Bola de Esportes", + "kite": "Pipa", + "baseball_bat": "Taco de Basebol", + "baseball_glove": "Luva de Basebol", + "skateboard": "Skate", + "plate": "Placa", + "surfboard": "Prancha de Surfe", + "tennis_racket": "Raquete de Tênis", + "bottle": "Garrafa", + "wine_glass": "Garrafa de Vinho", + "cup": "Copo", + "fork": "Garfo", + "knife": "Faca", + "spoon": "Colher", + "bowl": "Tigela", + "banana": "Banana", + "apple": "Maçã", + "animal": "Animal", + "sandwich": "Sanduíche", + "orange": "Laranja", + "broccoli": "Brócolis", + "bark": "Latido", + "carrot": "Cenoura", + "hot_dog": "Cachorro-Quente", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Bolo", + "chair": "Cadeira", + "couch": "Sofá", + "potted_plant": "Planta em Vaso", + "bed": "Cama", + "mirror": "Espelho", + "dining_table": "Mesa de Jantar", + "window": "Janela", + "desk": "Mesa", + "toilet": "Vaso Sanitário", + "door": "Porta", + "tv": "TV", + "laptop": "Laptop", + "mouse": "Rato", + "remote": "Controle Remoto", + "keyboard": "Teclado", + "goat": "Cabra", + "cell_phone": "Celular", + "microwave": "Microondas", + "oven": "Forno", + "toaster": "Torradeira", + "sink": "Pia", + "refrigerator": "Geladeira", + "blender": "Liquidificador", + "book": "Livro", + "clock": "Relógio", + "vase": "Vaso", + "scissors": "Tesouras", + "teddy_bear": "Ursinho de Pelúcia", + "hair_dryer": "Secador de Cabelo", + "toothbrush": "Escova de Dentes", + "hair_brush": "Escova de Cabelo", + "vehicle": "Veículo", + "squirrel": "Esquilo", + "deer": "Veado", + "on_demand": "Sob Demanda", + "face": "Rosto", + "fox": "Raposa", + "rabbit": "Coelho", + "raccoon": "Guaxinim", + "robot_lawnmower": "Cortador de Grama Robô", + "waste_bin": "Lixeira", + "license_plate": "Placa de Identificação", + "package": "Pacote", + "bbq_grill": "Grelha de Churrasco", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/web/public/locales/pt-BR/views/chat.json b/web/public/locales/pt-BR/views/chat.json new file mode 100644 index 0000000..8f63a9e --- /dev/null +++ b/web/public/locales/pt-BR/views/chat.json @@ -0,0 +1,63 @@ +{ + "documentTitle": "Frigate-Chat", + "title": "Chat-Frigate", + "subtitle": "Seu assistente de IA para gerenciamento de câmeras e insights", + "placeholder": "Pergunte o que quiser...", + "error": "Algo deu errado. Por favor, tente novamente.", + "processing": "Processando...", + "toolsUsed": "Usado: {{tools}}", + "showTools": "Mostrar ferramentas ({{count}})", + "hideTools": "Ocultar ferramentas", + "call": "Chamar", + "result": "Resultado", + "arguments": "Argumentos:", + "response": "Resposta:", + "attachment_chip_label": "{{label}} em {{camera}}", + "attachment_chip_remove": "Remover anexo", + "open_in_explore": "Abrir no Explorar", + "attach_event_aria": "Anexar evento {{eventId}}", + "attachment_picker_paste_label": "Ou cole o ID do evento", + "attachment_picker_attach": "Anexar", + "attachment_picker_placeholder": "Anexar um evento", + "quick_reply_find_similar": "Encontre avistamentos semelhantes", + "quick_reply_tell_me_more": "Conte-me mais sobre isso", + "quick_reply_when_else": "Em que outra ocasião isso foi visto?", + "quick_reply_find_similar_text": "Encontre avistamentos semelhantes a este.", + "quick_reply_tell_me_more_text": "Conte-me mais sobre este.", + "quick_reply_when_else_text": "Em que outra ocasião isso foi visto?", + "anchor": "Referência", + "similarity_score": "Semelhança", + "no_similar_objects_found": "Nenhum objeto semelhante encontrado.", + "semantic_search_required": "A busca semântica deve estar habilitada para encontrar objetos semelhantes.", + "send": "Enviar", + "suggested_requests": "Experimente perguntar:", + "starting_requests": { + "show_recent_events": "Mostrar eventos recentes", + "show_camera_status": "Mostrar status da câmera", + "recap": "O que aconteceu enquanto eu estava fora?", + "watch_camera": "Monitore uma câmera em busca de atividade" + }, + "starting_requests_prompts": { + "show_recent_events": "Mostre-me os eventos recentes da última hora", + "show_camera_status": "Qual é o status atual das minhas câmeras?", + "recap": "O que aconteceu enquanto eu estava fora?", + "watch_camera": "Fique de olho na porta da frente e me avise se alguém aparecer" + }, + "new_chat": "Novo chat", + "settings": { + "title": "Configurações do chat", + "show_stats": { + "title": "Mostrar estatísticas", + "desc": "Exibir a taxa de geração e o tamanho do contexto para respostas de chat.", + "while_generating": "Durante a geração", + "always": "Sempre" + }, + "auto_scroll": { + "title": "Rolagem automática", + "desc": "Acompanhe as novas mensagens à medida que chegam." + } + }, + "stats": { + "context": "{{tokens}} tokens" + } +} diff --git a/web/public/locales/pt-BR/views/classificationModel.json b/web/public/locales/pt-BR/views/classificationModel.json new file mode 100644 index 0000000..39bc880 --- /dev/null +++ b/web/public/locales/pt-BR/views/classificationModel.json @@ -0,0 +1,123 @@ +{ + "documentTitle": "Modelos de Classificação - Frigate", + "button": { + "deleteClassificationAttempts": "Apagar Imagens de Classificação", + "renameCategory": "Renomear Classe", + "deleteCategory": "Apagar Classe", + "deleteImages": "Apagar Imagens", + "trainModel": "Treinar Modelo", + "addClassification": "Adicionar classificação", + "deleteModels": "Excluir modelos", + "editModel": "Editar Modelo" + }, + "toast": { + "success": { + "deletedCategory_one": "Classe Apagada", + "deletedCategory_many": "Classes apagadas", + "deletedCategory_other": "", + "deletedImage_one": "Imagen Apagada", + "deletedImage_many": "Imagens Apagadas", + "deletedImage_other": "", + "categorizedImage": "Imagem Classificada com Sucesso", + "trainedModel": "Modelo treinado com sucesso.", + "trainingModel": "Treinamento do modelo iniciado com sucesso.", + "deletedModel_one": "{{count}} modelo excluído com sucesso", + "deletedModel_many": "{{count}} modelos excluídos com sucesso", + "deletedModel_other": "{{count}} modelos excluídos com sucesso", + "updatedModel": "Configuração do modelo atualizada com sucesso", + "renamedCategory": "Classe renomeada para {{name}} com sucesso", + "reclassifiedImage": "Imagem reclassificada com sucesso" + }, + "error": { + "deleteImageFailed": "Falha ao excluir:{{errorMessage}}", + "deleteCategoryFailed": "Falha ao excluir classe:{{errorMessage}}", + "categorizeFailed": "Falha ao categorizar imagem:{{errorMessage}}", + "deleteModelFailed": "Falha ao excluir o modelo: {{errorMessage}}", + "trainingFailed": "Treinamento do modelo falhou. Verifique os logs do Frigate para mais detalhes.", + "trainingFailedToStart": "Falha ao iniciar o treinamento do modelo: {{errorMessage}}", + "updateModelFailed": "Falha ao atualizar modelo: {{errorMessage}}", + "renameCategoryFailed": "Falha ao renomear classe: {{errorMessage}}", + "reclassifyFailed": "Falha ao reclassificar imagem: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Excluir Classe", + "desc": "Tem certeza de que deseja excluir a classe {{name}}? Isso excluirá permanentemente todas as imagens associadas e exigirá o treinamento do modelo novamente.", + "minClassesTitle": "Não é possível apagar a classe", + "minClassesDesc": "Um modelo de classificação deve ter pelo menos 2 classes. Adicione outra classe antes de excluir esta." + }, + "deleteModel": { + "title": "Deletar modelo de classificação", + "single": "Tem certeza de que deseja excluir {{name}}? Isso excluirá permanentemente todos os dados associados, incluindo imagens e dados de treinamento. Esta ação não pode ser desfeita.", + "desc_one": "Tem certeza de que deseja excluir {{count}} modelo? Isso excluirá permanentemente todos os dados associados, incluindo imagens e dados de treinamento. Esta ação não pode ser desfeita.", + "desc_many": "Tem certeza de que deseja excluir {{count}} modelos? Isso excluirá permanentemente todos os dados associados, incluindo imagens e dados de treinamento. Esta ação não pode ser desfeita.", + "desc_other": "Tem certeza de que deseja excluir {{count}} modelos? Isso excluirá permanentemente todos os dados associados, incluindo imagens e dados de treinamento. Esta ação não pode ser desfeita." + }, + "details": { + "scoreInfo": "A pontuação representa a média de confiança da classificação de todas as detecções deste objeto.", + "none": "Nenhum", + "unknown": "Desconhecido" + }, + "tooltip": { + "trainingInProgress": "O modelo está sendo treinado", + "noNewImages": "Nenhuma nova imagem para treinar. Classifique mais imagens para treinar mais.", + "noChanges": "Nenhuma alteração ao conjunto de dados desde o último treinamento.", + "modelNotReady": "O modelo não está pronto para treinamento" + }, + "deleteTrainImages": { + "desc_one": "Tem certeza que deseja deletar {{count}} imagem? Esta ação não pode ser desfeita.", + "desc_many": "Tem certeza que deseja deletar {{count}} imagens? Esta ação não pode ser desfeita.", + "desc_other": "Tem certeza que deseja deletar {{count}} imagens? Esta ação não pode ser desfeita.", + "title": "Apagar Imagens de Treinamento" + }, + "renameCategory": { + "title": "Renomear Classe", + "desc": "Insira um novo nome para {{name}}. O modelo deverá ser treinado novamente para a mudança de nome ter efeito." + }, + "description": { + "invalidName": "Nome inválido. Nomes podem conter letras, números, espacos, apóstrofos, sublinhado e hífens." + }, + "train": { + "title": "Classificações Recentes", + "titleShort": "Recente", + "aria": "Selecionar Classificações Recentes" + }, + "categories": "Classes", + "createCategory": { + "new": "Criar Nova Classe" + }, + "categorizeImageAs": "Classificar Imagem Como:", + "categorizeImage": "Classificar Imagem", + "menu": { + "objects": "Objetos", + "states": "Estados" + }, + "noModels": { + "object": { + "title": "Nenhum Modelo de Classificação de Objetos", + "description": "Crie um modelo personalizado para classificar objetos detectados.", + "buttonText": "Criar modelo de objeto" + }, + "state": { + "title": "Modelos de Classificação Sem Estado", + "description": "Crie um modelo personalizado para monitorar e classificar mudanças de estado em áreas específicas da câmera.", + "buttonText": "Criar modelo de estado" + } + }, + "deleteDatasetImages": { + "title": "Deletar Imagens do Dataset" + }, + "edit": { + "title": "Editar Modelo de Classificação", + "descriptionState": "Edite as classes deste modelo de classificação de estado. As alterações exigirão treinar novamente o modelo.", + "descriptionObject": "Edite o tipo de objeto e tipo de classificação para este modelo de classificação de objeto." + }, + "reclassifyImageAs": "Reclassificar imagem como:", + "reclassifyImage": "Reclassificar imagem", + "wizard": { + "title": "Criar nova classificação", + "steps": { + "nameAndDefine": "Nomear e definir" + } + } +} diff --git a/web/public/locales/pt-BR/views/configEditor.json b/web/public/locales/pt-BR/views/configEditor.json new file mode 100644 index 0000000..46c4808 --- /dev/null +++ b/web/public/locales/pt-BR/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor de Configuração - Frigate", + "configEditor": "Editor de configuração", + "copyConfig": "Copiar Configuração", + "saveAndRestart": "Salvar & Reiniciar", + "saveOnly": "Salvar apenas", + "confirm": "Sair sem salvar?", + "toast": { + "success": { + "copyToClipboard": "Configuração copiada para a área de transferência." + }, + "error": { + "savingError": "Erro ao salvar configuração" + } + }, + "safeConfigEditor": "Editor de Configuração (Modo Seguro)", + "safeModeDescription": "O Frigate está no modo seguro devido a um erro de validação de configuração." +} diff --git a/web/public/locales/pt-BR/views/events.json b/web/public/locales/pt-BR/views/events.json new file mode 100644 index 0000000..15282d4 --- /dev/null +++ b/web/public/locales/pt-BR/views/events.json @@ -0,0 +1,68 @@ +{ + "alerts": "Alertas", + "detections": "Detecções", + "motion": { + "label": "Movimento", + "only": "Somente Movimento" + }, + "allCameras": "Todas as Câmeras", + "empty": { + "alert": "Não existe nenhum alerta para revisar", + "detection": "Não há nenhuma detecção para revisar", + "motion": "Nenhum dado de movimento encontrado", + "recordingsDisabled": { + "title": "As gravações devem estar habilitadas", + "description": "A revisão de itens só pode ser criada para uma câmera quando a gravação está habilitada." + } + }, + "timeline": { + "label": "Linha do tempo" + }, + "timeline.aria": "Selecione a linha do tempo", + "events": { + "label": "Eventos", + "aria": "Selecione eventos", + "noFoundForTimePeriod": "Nenhum evento encontrado neste período." + }, + "recordings": { + "documentTitle": "Gravações - Frigate" + }, + "calendarFilter": { + "last24Hours": "Últimas 24 horas" + }, + "markTheseItemsAsReviewed": "Marque estes itens como revisados", + "newReviewItems": { + "button": "Novos Itens para Revisar", + "label": "Ver novos itens para revisão" + }, + "selected_one": "{{count}} selecionado(s)", + "documentTitle": "Revisar - Frigate", + "markAsReviewed": "Marcar como Revisado", + "selected_other": "{{count}} selecionado(s)", + "camera": "Câmera", + "detected": "detectado", + "suspiciousActivity": "Atividade Suspeita", + "threateningActivity": "Atividade de Ameaça", + "detail": { + "noDataFound": "Nenhum dado de detalhe para revisar", + "aria": "Alternar visualização de detalhe", + "trackedObject_one": "{{count}} objeto(s)", + "trackedObject_other": "{{count}} objetos", + "noObjectDetailData": "Nenhum dado de detalhe de objeto disponível.", + "label": "Detalhe", + "settings": "Configurações de visualização detalhada", + "alwaysExpandActive": { + "title": "Expandir sempre o modo ativo", + "desc": "Sempre expandir os detalhes do objeto do item de revisão ativo quando disponíveis." + } + }, + "objectTrack": { + "trackedPoint": "Ponto rastreado", + "clickToSeek": "Clique para ir para esse horário" + }, + "zoomIn": "Ampliar", + "zoomOut": "Diminuir o zoom", + "select_all": "Todos", + "normalActivity": "Normal", + "needsReview": "Precisa de revisão" +} diff --git a/web/public/locales/pt-BR/views/explore.json b/web/public/locales/pt-BR/views/explore.json new file mode 100644 index 0000000..1db62f0 --- /dev/null +++ b/web/public/locales/pt-BR/views/explore.json @@ -0,0 +1,267 @@ +{ + "documentTitle": "Explorar - Frigate", + "generativeAI": "IA Generativa", + "exploreMore": "Explorar mais objetos {{label}}", + "exploreIsUnavailable": { + "title": "A seção Explorar está indisponível", + "embeddingsReindexing": { + "context": "O menu explorar pode ser usado após os embeddings de objetos rastreados terem terminado de reindexar.", + "startingUp": "Iniciando…", + "estimatedTime": "Tempo estimado restante:", + "finishingShortly": "Finalizando em breve", + "step": { + "thumbnailsEmbedded": "Miniaturas embedded: ", + "descriptionsEmbedded": "Descrições embedded: ", + "trackedObjectsProcessed": "Objetos rastreados processados: " + } + }, + "downloadingModels": { + "context": "Frigate está baixando os modelos de embeddings necessários para oferecer suporte ao recurso de Pesquisa Semântica. Isso pode levar vários minutos, dependendo da velocidade da sua conexão de rede.", + "setup": { + "textModel": "Modelo de texto", + "textTokenizer": "Tokenizador de Texto", + "visionModel": "Modelo de visão", + "visionModelFeatureExtractor": "Extrator de características do modelo de visão" + }, + "tips": { + "context": "Você pode querer reindexar os embeddings de seus objetos rastreados uma vez que os modelos forem baixados.", + "documentation": "Leia a documentação" + }, + "error": "Um erro ocorreu. Verifique os registos do Frigate." + } + }, + "details": { + "timestamp": "Estampa de Tempo", + "item": { + "title": "Rever Detalhe dos itens", + "desc": "Revisar os detalhes do item", + "button": { + "share": "Compartilhar esse item revisado", + "viewInExplore": "Ver em Explorar" + }, + "tips": { + "mismatch_one": "{{count}} objeto indisponível foi detectado e incluido nesse item de revisão. Esse objeto ou não se qualifica para um alerta ou detecção, ou já foi limpo/deletado.", + "mismatch_many": "{{count}} objetos indisponíveis foram detectados e incluídos nesse item de revisão. Esses objetos ou não se qualificam para um alerta ou detecção, ou já foram limpos/deletados.", + "mismatch_other": "{{count}} objetos indisponíveis foram detectados e incluídos nesse item de revisão. Esses objetos ou não se qualificam para um alerta ou detecção, ou já foram limpos/deletados.", + "hasMissingObjects": "Ajustar a sua configuração se quiser que o Frigate salve objetos rastreados com os seguintes rótulos: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "Uma nova descrição foi solicitada do {{provider}}. Dependendo da velocidade do seu fornecedor, a nova descrição pode levar algum tempo para regenerar.", + "updatedSublabel": "Sub-rótulo atualizado com sucesso.", + "updatedLPR": "Placa de identificação atualizada com sucesso.", + "audioTranscription": "Transcrição de áudio requisitada com sucesso. Dependendo da velocidade de seu servidor Frigate, a transcrição pode demorar um tempo para completar." + }, + "error": { + "regenerate": "Falha ao ligar para {{provider}} para uma descrição nova: {{errorMessage}}", + "updatedSublabelFailed": "Falha ao atualizar sub-rótulo: {{errorMessage}}", + "updatedLPRFailed": "Falha ao atualizar placa de identificação: {{errorMessage}}", + "audioTranscription": "Falha ao requisitar transcrição de áudio: {{errorMessage}}" + } + } + }, + "label": "Rótulo", + "editSubLabel": { + "title": "Editar sub-rótulo", + "desc": "Nomeie um novo sub-rótulo para esse(a) {{label}}", + "descNoLabel": "Nomeie um sub-rótulo para esse objeto rastreado" + }, + "editLPR": { + "title": "Editar placa de identificação", + "desc": "Entre um valor de placa de identificação para esse(a) {{label}}", + "descNoLabel": "Entre um novo valor de placa de identificação para esse objeto rastrado" + }, + "snapshotScore": { + "label": "Pontuação da Captura de Imagem" + }, + "topScore": { + "label": "Pontuação Mais Alta", + "info": "A pontuação mais alta é a pontuação mediana mais alta para o objeto rastreado, então pode ser diferente da pontuação mostrada na miniatura dos resultados de busca." + }, + "recognizedLicensePlate": "Placa de Identificação Reconhecida", + "estimatedSpeed": "Velocidade Estimada", + "objects": "Objetos", + "camera": "Câmera", + "zones": "Zonas", + "button": { + "findSimilar": "Encontrar Semelhante", + "regenerate": { + "title": "Regenerar", + "label": "Regenerar descrição de objetos rastreados" + } + }, + "description": { + "label": "Descrição", + "placeholder": "Descrição do objeto rastreado", + "aiTips": "O Frigate não solicitará a descrição do seu fornecedor de IA Generativa até que o ciclo de vida do objeto rastreado tenha finalizado." + }, + "expandRegenerationMenu": "Expandir menu de regeneração", + "regenerateFromSnapshot": "Regenerar a partir de Captura de Imagem", + "regenerateFromThumbnails": "Regenerar a partir de Miniaturas", + "tips": { + "descriptionSaved": "Descrição salva com sucesso", + "saveDescriptionFailed": "Falha ao atualizar a descrição: {{errorMessage}}" + }, + "score": { + "label": "Pontuação" + } + }, + "trackedObjectDetails": "Detalhes do Objeto Rastreado", + "type": { + "details": "detalhes", + "snapshot": "captura de imagem", + "video": "vídeo", + "object_lifecycle": "ciclo de vida do objeto", + "thumbnail": "thumbnail", + "tracking_details": "detalhes de rastreamento" + }, + "objectLifecycle": { + "title": "Ciclo de Vida do Objeto", + "noImageFound": "Nenhuma imagem encontrada nessa marcação de horário.", + "createObjectMask": "Criar Máscara de Objeto", + "adjustAnnotationSettings": "Ajustar configurações de anotação", + "scrollViewTips": "Role a tela para ver momentos significantes do ciclo de vida desse objeto.", + "autoTrackingTips": "As posições da caixa delimitadora será inacurada para cameras com rastreamento automático.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Ponto Rastreado", + "lifecycleItemDesc": { + "visible": "{{label}} detectado", + "entered_zone": "{{label}} entrou em {{zones}}", + "active": "{{label}} se tornou ativo", + "stationary": "{{label}} se tornou estacionário", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectado para {{label}}", + "other": "{{label}} reconhecido como {{attribute}}" + }, + "gone": "{{label}} esquerda", + "heard": "{{label}} escutado(a)", + "header": { + "zones": "Zonas", + "area": "Área", + "ratio": "Proporção" + }, + "external": "{{label}} detectado(a)" + }, + "annotationSettings": { + "title": "Configurações de anotação", + "showAllZones": { + "title": "Mostrar todas as zonas", + "desc": "Sempre exibir zonas nos quadros em que objetos entraram em uma zona." + }, + "offset": { + "label": "Deslocamento da Anotação", + "desc": "Esses dados vem do feed de detecção da sua câmera, porém estão sobrepondo imagens da gravação. É improvável que duas transmissões estejam perfeitamente sincronizadas. Como resultado, as caixas delimitadoras e a gravação não se alinharam perfeitamente. Porém, o campo annotation_offset pode ser utilizado para ajustar isso.", + "documentation": "Leia a documentação. ", + "millisecondsToOffset": "Milisegundos para separar detecções de anotações.Default: 0", + "tips": "DICA: Imagine que haja um clipe de evento com uma pessoa caminhando da esquerda para a direita. Se a caixa delimitadora da linha do tempo do evento está consistentemente à esquerda da pessoa, então o valor deve ser reduzido. Similarmente, se a pessoa está caminhando da esquerda para a direita e a caixa delimitadora está consistentemente à frente da pessoa, então o valor deve ser aumentado.", + "toast": { + "success": "O deslocamento de anotação para a câmera {{camera}} foi salvo no arquivo de configuração. Reinicie o Frigate para aplicar as alterações." + } + } + }, + "carousel": { + "previous": "Slide anterior", + "next": "Próximo slide" + } + }, + "itemMenu": { + "findSimilar": { + "aria": "Encontrar objetos rastreados similares", + "label": "Encontrar similar" + }, + "submitToPlus": { + "label": "Enviar ao Frigate+", + "aria": "Enviar ao Frigate Plus" + }, + "downloadVideo": { + "label": "Baixar vídeo", + "aria": "Baixar vídeo" + }, + "downloadSnapshot": { + "label": "Baixar captura de imagem", + "aria": "Baixar captura de imagem" + }, + "viewObjectLifecycle": { + "label": "Ver ciclo de vida do objeto", + "aria": "Exibir o ciclo de vida do objeto" + }, + "viewInHistory": { + "label": "Ver no Histórico", + "aria": "Ver no Histórico" + }, + "deleteTrackedObject": { + "label": "Deletar esse objeto rastreado" + }, + "addTrigger": { + "label": "Adicionar gatilho", + "aria": "Adicionar um gatilho para esse objeto rastreado" + }, + "audioTranscription": { + "label": "Transcrever", + "aria": "Solicitar transcrição de áudio" + } + }, + "dialog": { + "confirmDelete": { + "title": "Confirmar Exclusão", + "desc": "Deletar esse objeto rastreado remove a captura de imagem, os embeddings salvos, e os detalhes de rastreamento associados. Gravações desse objeto rastreado na visualização de Histórico NÃO serão deletadas.

    Tem certeza que deseja prosseguir?" + } + }, + "noTrackedObjects": "Nenhum Objeto Rastreado Encontrado", + "fetchingTrackedObjectsFailed": "Erro ao buscar por objetos rastreados: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} objeto rastreado ", + "trackedObjectsCount_many": "{{count}} objetos rastreados ", + "trackedObjectsCount_other": "{{count}} objetos rastreados ", + "searchResult": { + "tooltip": "Correspondência com {{type}} de {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "success": "Objeto rastreado deletado com sucesso.", + "error": "Falha ao detectar objeto rastreado {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "Análise de IA" + }, + "concerns": { + "label": "Preocupações" + }, + "trackingDetails": { + "lifecycleItemDesc": { + "gone": "{{label}} saiu", + "visible": "{{label}} detectado(a)", + "entered_zone": "{{label}} entrou em {{zones}}", + "active": "{{label}} em movimento", + "stationary": "{{label}} parou", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectado para {{label}}", + "other": "{{label}} reconhecido como {{attribute}}" + }, + "heard": "Som de {{label}} detectado", + "external": "{{label}} detectado(a)", + "header": { + "zones": "Zonas", + "area": "Área", + "ratio": "Proporção" + } + }, + "title": "Detalhes de Rastreamento", + "createObjectMask": "Criar máscara de objeto", + "annotationSettings": { + "showAllZones": { + "title": "Mostrar todas as Zonas" + } + }, + "carousel": { + "previous": "Slide anterior", + "next": "Próximo slide" + }, + "noImageFound": "Sem imagens encontradas para esse período.", + "adjustAnnotationSettings": "Ajustar configurações de anotação", + "scrollViewTips": "Clique para ver os momentos importantes do ciclo de vida deste objeto.", + "autoTrackingTips": "As posições das caixas de delimitação ficarão imprecisas para câmeras com rastreamento automático.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Ponto Rastreado" + } +} diff --git a/web/public/locales/pt-BR/views/exports.json b/web/public/locales/pt-BR/views/exports.json new file mode 100644 index 0000000..83a5c29 --- /dev/null +++ b/web/public/locales/pt-BR/views/exports.json @@ -0,0 +1,40 @@ +{ + "documentTitle": "Exportar - Frigate", + "search": "Buscar", + "noExports": "Nenhuma exportação encontrada", + "deleteExport": { + "label": "Excluir Exportação" + }, + "deleteExport.desc": "Você tem certeza que quer apagar {{exportName}}?", + "editExport": { + "title": "Renomear Exportação", + "desc": "Entre um novo nome para essa exportação.", + "saveExport": "Salvar exportação" + }, + "toast": { + "error": { + "renameExportFailed": "Falha ao renomear exportação: {{errorMessage}}", + "assignCaseFailed": "Falha ao atualizar atribuição ao caso: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Compartilhar exportação", + "downloadVideo": "Baixar vídeo", + "editName": "Editar nome", + "deleteExport": "Apagar exportação", + "assignToCase": "Adicionar ao caso", + "removeFromCase": "Remover da caixa" + }, + "headings": { + "uncategorizedExports": "Exportações não categorizadas", + "cases": "Casos" + }, + "caseDialog": { + "title": "Adicionar ao caso", + "description": "Escolha um caso existente ou crie um novo.", + "selectLabel": "Caso", + "newCaseOption": "Criar novo caso", + "nameLabel": "Nome do caso", + "descriptionLabel": "Descrição" + } +} diff --git a/web/public/locales/pt-BR/views/faceLibrary.json b/web/public/locales/pt-BR/views/faceLibrary.json new file mode 100644 index 0000000..9e7a5b3 --- /dev/null +++ b/web/public/locales/pt-BR/views/faceLibrary.json @@ -0,0 +1,112 @@ +{ + "details": { + "person": "Pessoa", + "unknown": "Desconhecido", + "face": "Detalhes do Rosto", + "subLabelScore": "Pontuação do Sub-Rótulo", + "scoreInfo": "A pontuação do sub-rótulo é a pontuação ponderada de todas as confidências faciais reconhecidas, então a pontuação pode ser diferente da mostrada na foto instantânea.", + "faceDesc": "Detalhes do objeto rastreado que gerou este rosto", + "timestamp": "Estampa de Tempo" + }, + "selectItem": "Selecione {{item}}", + "imageEntry": { + "validation": { + "selectImage": "Por favor selecione um arquivo de imagem." + }, + "maxSize": "Tamanho máximo: {{size}}MB", + "dropActive": "Solte a imagem aqui…", + "dropInstructions": "Arraste e solte ou cole uma imagem aqui ou clique para selecionar" + }, + "deleteFaceLibrary": { + "title": "Apagar Nome", + "desc": "Tem certeza que quer deletar a coleção {{name}}? Isso deletará permanentemente todos os rostos associados." + }, + "button": { + "addFace": "Adicionar Rosto", + "renameFace": "Renomear Rosto", + "deleteFace": "Remover Rosto", + "deleteFaceAttempts": "Remover Rostos", + "reprocessFace": "Reprocessar Rosto", + "uploadImage": "Enviar Imagem" + }, + "createFaceLibrary": { + "new": "Criar Novo Rosto", + "title": "Criar Coleção", + "desc": "Criar uma nova coleção", + "nextSteps": "Para construir uma base forte:
  • Use a aba Reconhecimentos Recentes para selecionar e treinar em imagens para cada pessoa detectada.
  • Foque em imagens retas para melhores resultados; evite treinar imagens que capturam rostos em um ângulo.
  • " + }, + "deleteFaceAttempts": { + "title": "Apagar Rostos", + "desc_one": "Você tem certeza que quer deletar {{count}} rosto? Essa ação não pode ser desfeita.", + "desc_many": "Você tem certeza que quer deletar os {{count}} rostos? Essa ação não pode ser desfeita.", + "desc_other": "Você tem certeza que quer deletar os {{count}} rostos? Essa ação não pode ser desfeita." + }, + "renameFace": { + "title": "Renomear Rosto", + "desc": "Entre com o novo nome para {{name}}" + }, + "nofaces": "Nenhum rosto disponível", + "pixels": "{{area}}px", + "readTheDocs": "Leia a documentação", + "steps": { + "nextSteps": "Próximos Passos", + "faceName": "Digite o Nome do Rosto", + "uploadFace": "Enviar Imagem de Rosto", + "description": { + "uploadFace": "Faça o upload de uma imagem de {{name}} que mostre seu rosto visto de frente. A imagem não precisa estar recortada apenas com o rosto." + } + }, + "description": { + "placeholder": "Informe um nome para esta coleção", + "addFace": "Adicione uma nova coleção à Biblioteca Facial subindo a sua primeira imagem.", + "invalidName": "Nome inválido. Nomes podem conter letras, números, espacos, apóstrofos, sublinhado e hífens.", + "nameCannotContainHash": "O nome não pode conter #." + }, + "documentTitle": "Biblioteca de rostos - Frigate", + "uploadFaceImage": { + "title": "Carregar imagem facial", + "desc": "Envie uma imagem para escanear por faces e incluir em {{pageToggle}}" + }, + "collections": "Coleções", + "train": { + "title": "Reconhecimentos Recentes", + "aria": "Selecionar reconhecimentos recentes", + "empty": "Não há tentativas recentes de reconhecimento facial", + "titleShort": "Recente", + "emptyNoLibrary": { + "title": "Carregue uma foto de rosto", + "description": "Você deve adicionar pelo menos um rosto à biblioteca para que o reconhecimento facial funcione." + } + }, + "selectFace": "Selecionar Rosto", + "trainFaceAs": "Treinar Rosto como:", + "trainFace": "Treinar Rosto", + "toast": { + "success": { + "uploadedImage": "Imagens enviadas com sucesso.", + "addFaceLibrary": "{{name}} foi adicionado com sucesso à Biblioteca de Rostos!", + "deletedFace_one": "{{count}} rosto apagado com sucesso.", + "deletedFace_many": "{{count}} rostos apagados com sucesso.", + "deletedFace_other": "{{count}} rostos apagados com sucesso.", + "trainedFace": "Rosto treinado com sucesso.", + "updatedFaceScore": "Pontuação de rosto atualizada com sucesso para {{name}}{{score}}.", + "renamedFace": "O rosto foi renomeado com sucesso para {{name}}", + "deletedName_one": "{{count}} rosto foi deletado com sucesso.", + "deletedName_many": "{{count}} rostos foram deletados com sucesso.", + "deletedName_other": "{{count}} rostos foram deletados com sucesso.", + "reclassifiedFace": "Rosto reclassificado com sucesso." + }, + "error": { + "uploadingImageFailed": "Falha ao enviar a imagem: {{errorMessage}}", + "addFaceLibraryFailed": "Falha ao definir o nome do rosto: {{errorMessage}}", + "deleteFaceFailed": "Falha em deletar: {{errorMessage}}", + "deleteNameFailed": "Falha ao deletar nome: {{errorMessage}}", + "renameFaceFailed": "Falha ao renomear rosto: {{errorMessage}}", + "trainFailed": "Falha ao treinar: {{errorMessage}}", + "updateFaceScoreFailed": "Falha ao atualizar pontuação de rosto: {{errorMessage}}", + "reclassifyFailed": "Falha ao reclassificar o rosto: {{errorMessage}}" + } + }, + "reclassifyFaceAs": "Reclassificar rosto como:", + "reclassifyFace": "Reclassificar rosto" +} diff --git a/web/public/locales/pt-BR/views/live.json b/web/public/locales/pt-BR/views/live.json new file mode 100644 index 0000000..79d446d --- /dev/null +++ b/web/public/locales/pt-BR/views/live.json @@ -0,0 +1,206 @@ +{ + "documentTitle": { + "default": "Ao vivo - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Ao vivo - Frigate", + "lowBandwidthMode": "Modo de baixa largura de banda", + "twoWayTalk": { + "enable": "Habilitar Fala em Dois Sentidos", + "disable": "Desabilitar Fala em Dois Sentidos" + }, + "cameraAudio": { + "enable": "Habilitar Áudio da Câmera", + "disable": "Desabilitar Audio da Câmera" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Clique no quadro para centralizar a câmera", + "enable": "Ativar clique para mover", + "disable": "Desativar clique para mover", + "enableWithZoom": "Habilitar clicar para mover / arrastar para zoom" + }, + "left": { + "label": "Mova a câmera PTZ para a esquerda" + }, + "up": { + "label": "Mova a câmera PTZ para cima" + }, + "down": { + "label": "Mova a câmera PTZ para baixo" + }, + "right": { + "label": "Mova a câmera PTZ para a direita" + } + }, + "frame": { + "center": { + "label": "Clique no quadro para centralizar a câmera PTZ" + } + }, + "presets": "Predefinições de câmera PTZ", + "zoom": { + "in": { + "label": "Aumentar Zoom na câmera PTZ" + }, + "out": { + "label": "Diminuir Zoom na câmera PTZ" + } + }, + "focus": { + "in": { + "label": "Aumentar foco da câmera PTZ" + }, + "out": { + "label": "Tirar foco da câmera PTZ" + } + } + }, + "camera": { + "enable": "Ativar Câmera", + "disable": "Desabilitar Câmera", + "turnOn": "Ligar a câmera", + "turnOff": "Desligar a câmera" + }, + "muteCameras": { + "enable": "Silenciar Todas as Câmeras", + "disable": "Ativar Áudio de Todas as Câmeras" + }, + "detect": { + "enable": "Ativar Detecção", + "disable": "Desativar Detecção" + }, + "recording": { + "enable": "Ativar Gravação", + "disable": "Desativar Gravação", + "disabledInConfig": "A gravação deve ser habilitada primeiro nas configurações desta câmera." + }, + "snapshots": { + "enable": "Permitir Capturas de Imagem", + "disable": "Desativar Capturas de Imagem" + }, + "audioDetect": { + "enable": "Ativar Detecção de Áudio", + "disable": "Desabilitar Detecção de Áudio" + }, + "autotracking": { + "enable": "Habilitar Rastreamento Automático", + "disable": "Desabilitar Rastreamento Automático" + }, + "streamStats": { + "enable": "Exibir Estatísticas de Transmissão", + "disable": "Ocultar Estatísticas de Transmissão" + }, + "manualRecording": { + "title": "Sob Demanda", + "tips": "Baixe uma captura de tela instantânea ou Inicie um evento manual baseado nas configurações de retenção de gravação dessa câmera.", + "playInBackground": { + "label": "Reproduzir em segundo plano", + "desc": "Habilite essa opção para continuar transmitindo quando o reprodutor estiver oculto." + }, + "showStats": { + "label": "Exibir Estatísticas", + "desc": "Habilite esta opção para exibir as estatísticas da transmissão como uma sobreposição no feed da câmera." + }, + "start": "Iniciar gravação sob demanda", + "started": "Iniciou a gravação manual sob demanda.", + "failedToStart": "Falha ao iniciar a gravação manual sob demanda.", + "recordDisabledTips": "Como a gravação está desabilitada ou restrita na configuração desta câmera, apenas um instantâneo será salvo.", + "end": "Fim da gravação sob demanda", + "failedToEnd": "Falha ao finalizar a gravação manual sob demanda.", + "debugView": "Visualização de Depuração", + "ended": "Gravação manual sob demanda finalizada." + }, + "streamingSettings": "Configurações de Transmissão", + "notifications": "Notificações", + "audio": "Áudio", + "suspend": { + "forTime": "Suspender por: " + }, + "stream": { + "title": "Transmissão", + "audio": { + "tips": { + "title": "O áudio deve sair da sua câmera e configurado no go2rtc para essa transmissão.", + "documentation": "Leia da documentação. " + }, + "available": "Áudio disponível para essa transmissão", + "unavailable": "O áudio não está disponível para essa transmissão" + }, + "twoWayTalk": { + "tips": "O seu dispostivio precisa suportar esse recurso e o WebRTC precisa estar configurado para áudio bidirecional.", + "tips.documentation": "Leia a documentação. ", + "available": "Áudio bidirecional está disponível para essa transmissão", + "unavailable": "Áudio bidirecional está indisponível para essa transmissão" + }, + "lowBandwidth": { + "tips": "A transmissão ao vivo está em modo de economia de dados devido a erros de buffering ou de transmissão.", + "resetStream": "Resetar transmissão" + }, + "playInBackground": { + "label": "Reproduzir em segundo plano", + "tips": "Habilitar essa opção para continuar a transmissão quando o reprodutor estiver oculto." + }, + "debug": { + "picker": "A seleção da transmissão fica indisponível em modo de depuração. A visualização de depuração sempre usa o papel de detecção atribuído à transmissão." + } + }, + "cameraSettings": { + "title": "Configurações de {{camera}}", + "cameraEnabled": "Câmera Habilitada", + "objectDetection": "Detecção de Objeto", + "recording": "Gravação", + "snapshots": "Capturas de Imagem", + "audioDetection": "Detecção de Áudio", + "autotracking": "Auto Rastreamento", + "transcription": "Transcrição de Áudio", + "camera": "Câmera" + }, + "history": { + "label": "Exibir gravação histórica" + }, + "effectiveRetainMode": { + "modes": { + "all": "Todos", + "motion": "Movimento", + "active_objects": "Objetos Ativos" + }, + "notAllTips": "A configuração de retenção da sua gravação do(a) {{source}} está definida para o modo: {{effectiveRetainMode}}, então essa gravação sob demanda irá manter somente os segmentos com o {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Editar Layout", + "group": { + "label": "Editar Grupo de Câmera" + }, + "exitEdit": "Sair da Edição" + }, + "transcription": { + "enable": "Habilitar Transcrição de Áudio em Tempo Real", + "disable": "Desabilitar Transcrição de Áudio em Tempo Real" + }, + "noCameras": { + "title": "Nenhuma Câmera Configurada", + "description": "Inicie conectando uma câmera ao Frigate.", + "buttonText": "Adicionar Câmera", + "restricted": { + "title": "Nenhuma Câmera Disponível", + "description": "Você não tem permissão para ver quaisquer câmeras neste grupo." + }, + "default": { + "title": "Nenhuma Câmera Configurada", + "description": "Comece conectando uma câmera ao Frigate.", + "buttonText": "Adicionar Câmera" + }, + "group": { + "title": "Nenhuma Câmera no Grupo", + "description": "Este grupo de câmeras não tem nenhuma câmera atribuída ou habilitada.", + "buttonText": "Gerenciar Grupos" + } + }, + "snapshot": { + "takeSnapshot": "Baixar captura de imagem instantânea", + "noVideoSource": "Nenhuma fonte de vídeo disponível para captura de imagem.", + "captureFailed": "Falha ao capturar imagem.", + "downloadStarted": "Download de capturas de imagem iniciado." + } +} diff --git a/web/public/locales/pt-BR/views/motionSearch.json b/web/public/locales/pt-BR/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/pt-BR/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/pt-BR/views/recording.json b/web/public/locales/pt-BR/views/recording.json new file mode 100644 index 0000000..fd7cf6e --- /dev/null +++ b/web/public/locales/pt-BR/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtro", + "export": "Exportar", + "calendar": "Calendário", + "filters": "Filtros", + "toast": { + "error": { + "noValidTimeSelected": "Nenhum intervalo de tempo selecionado", + "endTimeMustAfterStartTime": "O tempo de término deve ser após o tempo de início" + } + } +} diff --git a/web/public/locales/pt-BR/views/replay.json b/web/public/locales/pt-BR/views/replay.json new file mode 100644 index 0000000..58ccb69 --- /dev/null +++ b/web/public/locales/pt-BR/views/replay.json @@ -0,0 +1,19 @@ +{ + "title": "Depuração de Replay", + "description": "“Reproduza gravações de câmera para depuração. A lista de objetos exibe um resumo com atraso temporal, dos objetos detectados e a aba de mensagens mostra um fluxo de mensagens internas do Frigate provenientes da gravação reproduzida.", + "websocket_messages": "Mensagens", + "dialog": { + "title": "Iniciar Replay de Depuração", + "description": "Crie uma câmera de reprodução temporária que exiba imagens históricas em loop para depurar problemas de detecção e rastreamento de objetos. A câmera de reprodução terá a mesma configuração de detecção da câmera de origem. Escolha um intervalo de tempo para começar.", + "camera": "Câmera de origem", + "timeRange": "Intervalo de tempo", + "preset": { + "1m": "Último minuto", + "5m": "Últimos 5 minutos", + "timeline": "Da Linha do Tempo", + "custom": "Personalizada" + }, + "startButton": "Iniciar Reprodução", + "selectFromTimeline": "Selecionar" + } +} diff --git a/web/public/locales/pt-BR/views/search.json b/web/public/locales/pt-BR/views/search.json new file mode 100644 index 0000000..a41f3c4 --- /dev/null +++ b/web/public/locales/pt-BR/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Buscar", + "savedSearches": "Buscas Salvas", + "searchFor": "Procurar por {{inputValue}}", + "button": { + "clear": "Limpar procurar", + "save": "Salvar pesquisa", + "delete": "Apagar procura salva", + "filterInformation": "Filtrar informação", + "filterActive": "Filtros ativos" + }, + "trackedObjectId": "ID do objeto rastreado", + "filter": { + "label": { + "cameras": "Câmeras", + "labels": "Rótulos", + "zones": "Zonas", + "before": "Antes", + "after": "Depois", + "min_score": "Pontuação Mínima", + "max_score": "Pontuação Máxima", + "min_speed": "Velocidade Mínima", + "max_speed": "Velocidade Máxima", + "sub_labels": "Sub-Rótulos", + "search_type": "Tipo de Busca", + "time_range": "Intervalo de Tempo", + "recognized_license_plate": "Placa de Carro Reconhecida", + "has_clip": "Possui Clipe", + "has_snapshot": "Possui Captura de Imagem", + "attributes": "Atributos" + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Descrição" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "A data 'antes' deve ser depois da data 'após'.", + "afterDatebeEarlierBefore": "A data 'após' deve ser antes da data 'antes'.", + "minScoreMustBeLessOrEqualMaxScore": "A 'pontuação_min' deve ser menor ou igual a 'pontuação_max'.", + "maxScoreMustBeGreaterOrEqualMinScore": "A 'pontuação_max' deve ser maior ou igual a 'pontuação_min'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "A 'velocidad_min' deve ser menor ou igual a 'velocidad_max'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "A 'velocidad_max' deve ser maior ou igual a 'velocidade_min'." + } + }, + "tips": { + "title": "Como utilizar filtros de texto", + "desc": { + "text": "Filtros ajudam a refinar os resultados da busca. Veja como utilizá-los na campo de pesquisa:", + "step1": "Digite um nome de chave de filtro seguido por \":\" (ex., \"câmeras:\").", + "step2": "Selecione um valor a partir das sugestões ou digite a sua própria.", + "step3": "Usar filtros múltiplos adicionando um após o outro com um espaço entre eles.", + "step4": "Filtros de data (antes: e após:) usam o formato {{DateFormat}}.", + "step5": "Filtros de tempo usam o formato {{exampleTime}}.", + "step6": "Remova os filtros clicando no \"x\" ao lado deles.", + "exampleLabel": "Exemplo:" + } + }, + "header": { + "noFilters": "Filtros", + "activeFilters": "Filtros ativos", + "currentFilterType": "Valores de Filtros" + } + }, + "similaritySearch": { + "active": "Pesquisa por similaridade ativa", + "title": "Buscar por Similaridade", + "clear": "Limpar buscar por similaridade" + }, + "placeholder": { + "search": "Pesquisar…" + } +} diff --git a/web/public/locales/pt-BR/views/settings.json b/web/public/locales/pt-BR/views/settings.json new file mode 100644 index 0000000..00d86e2 --- /dev/null +++ b/web/public/locales/pt-BR/views/settings.json @@ -0,0 +1,980 @@ +{ + "documentTitle": { + "default": "Configurações - Frigate", + "authentication": "Configurações de Autenticação - Frigate", + "camera": "Configurações de Câmera - Frigate", + "enrichments": "Configurações de Enriquecimento - Frigate", + "masksAndZones": "Editor de Máscara e Zona - Frigate", + "motionTuner": "Ajuste de Movimento - Frigate", + "object": "Debug - Frigate", + "general": "Configurações da interface - Frigate", + "frigatePlus": "Frigate+ Configurações- Frigate", + "notifications": "Configurações de notificação - Frigate", + "cameraManagement": "Gerenciar Câmeras - Frigate", + "cameraReview": "Configurações de Revisão de Câmera - Frigate", + "globalConfig": "Configuração Global - Frigate", + "cameraConfig": "Configuração da Câmera - Frigate", + "maintenance": "Manutenção - Frigate", + "profiles": "Perfis - Frigate", + "detectorsAndModel": "Detectores e modelo - Frigate" + }, + "menu": { + "ui": "UI", + "cameras": "Configurações da câmera", + "masksAndZones": "Máscaras / Zonas", + "users": "Usuários", + "notifications": "Notificações", + "frigateplus": "Frigate+", + "motionTuner": "Ajuste de Movimento", + "debug": "Depurar", + "enrichments": "Enriquecimentos", + "triggers": "Gatilhos", + "roles": "Papéis", + "cameraManagement": "Gerenciamento", + "cameraReview": "Revisar", + "general": "Geral", + "globalConfig": "Configuração global", + "system": "Sistema", + "integrations": "Integrações", + "uiSettings": "Configurações de interface", + "profiles": "Perfis", + "globalDetect": "Detecção de objeto", + "globalRecording": "Gravando", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Detecção de movimento", + "globalObjects": "Objetos", + "globalReview": "Revisar", + "globalSnapshots": "Instantâneos", + "globalAudioEvents": "Detecção de áudio", + "globalLivePlayback": "Reprodução ao vivo", + "globalTimestampStyle": "Estilo de carimbo de data/hora", + "systemDatabase": "Banco de dados", + "systemTls": "TLS", + "systemAuthentication": "Autenticação", + "systemNetworking": "Rede", + "systemProxy": "Proxy", + "systemUi": "IU", + "systemLogging": "Registro", + "systemEnvironmentVariables": "Variáveis de ambiente", + "systemTelemetry": "Telemetria", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "Detectores e modelo", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "Fluxo de go2rtc", + "integrationSemanticSearch": "Busca semântica", + "integrationGenerativeAi": "IA generativa", + "integrationFaceRecognition": "Reconhecimento facial", + "integrationLpr": "Reconhecimento de placas", + "integrationObjectClassification": "Classificação de objetos", + "integrationAudioTranscription": "Transcrição de áudio", + "cameraDetect": "Detecção de objetos", + "cameraFfmpeg": "Fluxos (FFmpeg)", + "cameraRecording": "Gravação", + "cameraSnapshots": "Instantâneos", + "cameraMotion": "Detecção de movimento", + "cameraObjects": "Objetos", + "cameraConfigReview": "Análise", + "cameraAudioEvents": "Detecção de áudio", + "cameraAudioTranscription": "Transcrição de áudio", + "cameraNotifications": "Notificações", + "cameraLivePlayback": "Reprodução ao vivo" + }, + "dialog": { + "unsavedChanges": { + "title": "Você tem alterações não salvas.", + "desc": "Você deseja salvar as alterações antes de continuar?" + } + }, + "cameraSetting": { + "camera": "Câmera", + "noCamera": "Sem Câmera" + }, + "general": { + "title": "Configurações de Interface", + "liveDashboard": { + "title": "Painel em Tempo Real", + "automaticLiveView": { + "label": "Visualização em Tempo Real Automática", + "desc": "Automaticamente alterar para a visualização em tempo real da câmera quando alguma atividade for detectada. Desativar essa opção faz com que as imagens estáticas da câmera no Painel em Tempo Real atualizem apenas uma vez por minuto." + }, + "playAlertVideos": { + "label": "Reproduzir Alertas de Video", + "desc": "Por padrão, alertas recentes no Painel em Tempo Real são reproduzidos como vídeos em loop. Desative essa opção para mostrar apenas a imagens estáticas de alertas recentes nesse dispositivo / navegador." + }, + "displayCameraNames": { + "label": "Sempre mostrar os nomes das câmeras", + "desc": "Sempre mostrar os nomes das câmeras em um chip no painel de visualização ao vivo com várias câmeras." + }, + "liveFallbackTimeout": { + "label": "Tempo limite de fallback do reprodutor ao vivo", + "desc": "Quando o stream ao vivo em alta qualidade de uma câmera não estiver disponível, fazer fallback para o modo de baixa largura de banda após este número de segundos. Padrão: 3." + } + }, + "storedLayouts": { + "title": "Layouts Salvos", + "desc": "O layout das câmeras em um grupo de câmeras pode ser arrastado/redimensionado. As posições são salvas no armazenamento local do seu navegador.", + "clearAll": "Apagar Todos os Layouts" + }, + "cameraGroupStreaming": { + "title": "Opções de Streaming de Grupo de Câmeras", + "desc": "Os ajustes de streaming para cada grupo de câmera são salvos no armazenamento local do seu navegador.", + "clearAll": "Apagar Todos os Ajustes de Streaming" + }, + "recordingsViewer": { + "title": "Visualizador de Gravações", + "defaultPlaybackRate": { + "label": "Velocidade Padrão de Reprodução", + "desc": "Velocidade padrão de reprodução para gravações." + } + }, + "calendar": { + "title": "Calendário", + "firstWeekday": { + "label": "Primeiro Dia da Semana", + "desc": "Dia em que as semanas no calendário de revisão iniciam.", + "sunday": "Domingo", + "monday": "Segunda-Feira" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Layout deletado para {{cameraName}}", + "clearStreamingSettings": "Ajustes de streaming para todos os grupos de câmera limpados." + }, + "error": { + "clearStoredLayoutFailed": "Não foi possível apagar o layout:{{errorMessage}}", + "clearStreamingSettingsFailed": "Não foi possível apagar os ajustes de streaming:{{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Configurações de Enriquecimento", + "unsavedChanges": "Alterações de configurações de Enriquecimento não salvas", + "birdClassification": { + "title": "Classificação de Pássaros", + "desc": "A classificação de pássaros identifica pássaros conhecidos usando o modelo Tensorflow quantizado. Quando um pássaro é reconhecido, o seu nome comum será adicionado como um sub-rótulo. Essa informação é incluida na UI, filtros e notificações." + }, + "semanticSearch": { + "title": "Busca Semântica", + "desc": "A Busca Semântica no Frigate permite você encontrar objetos rastreados dentro dos seus itens revisados, usando ou a imagem em si, uma descrição de texto definida pelo usuário ou uma gerada automaticamente.", + "readTheDocumentation": "Leia a Documentação", + "reindexNow": { + "label": "Reindexar Agora", + "desc": "A reindexação irá regenerar os embeddings para todos os objetos rastreados. Esse processo roda em segundo plano e pode demandar 100% da CPU e levar um tempo considerável dependendo do número de objetos rastreados que você possui.", + "confirmTitle": "Confirmar Reindexação", + "confirmDesc": "Tem certeza que quer reindexar todos os embeddings de objetos rastreados? Esse processo rodará em segundo plano porém utilizará 100% da CPU e levará uma quantidade de tempo considerável. Você pode acompanhar o progresso na página Explorar.", + "confirmButton": "Reindexar", + "success": "A reindexação iniciou com sucesso.", + "alreadyInProgress": "A reindexação já está em progresso.", + "error": "Falha ao iniciar a reindexação: {{errorMessage}}" + }, + "modelSize": { + "label": "Tamanho do Modelo", + "desc": "O tamanho do modelo usado para embeddings de pesquisa semântica.", + "small": { + "title": "pequeno", + "desc": "Usando pequeno emprega a versão quantizada do modelo que utiliza menos RAM e roda mais rápido na CPU, com diferenças negligíveis na qualidade dos embeddings." + }, + "large": { + "title": "grande", + "desc": "Usar grande emprega o modelo Jina completo e roda na GPU automáticamente caso aplicável." + } + } + }, + "faceRecognition": { + "title": "Reconhecimento Facial", + "desc": "O reconhecimento facial permite que pessoas sejam associadas a nomes e quando seus rostos forem reconhecidos, o Frigate associará o nome da pessoa como um sub-rótulo. Essa informação é inclusa na UI, filtros e notificações.", + "readTheDocumentation": "Leia a Documentação", + "modelSize": { + "label": "Tamanho do Modelo", + "desc": "O tamanho do modelo usado para reconhecimento facial.", + "small": { + "title": "pequeno", + "desc": "Usar o pequeno emprega o modelo de embedding de rosto FaceNet, que roda de maneira eficiente na maioria das CPUs." + }, + "large": { + "title": "grande", + "desc": "Usar o grande emprega um modelo de embedding de rosto ArcFace e irá automáticamente rodar pela GPU se aplicável." + } + } + }, + "licensePlateRecognition": { + "title": "Reconhecimento de Placa de Identificação", + "desc": "O Frigate pode reconhecer placas de identificação em veículos e automáticamente adicionar os caracteres detectados ao campo placas_de_identificação_reconhecidas ou um nome conhecido como um sub-rótulo a objetos que são do tipo carro. Um uso típico é ler a placa de carros entrando em uma garagem ou carros passando pela rua.", + "readTheDocumentation": "Leia a Documentação" + }, + "restart_required": "Necessário reiniciar (configurações de enriquecimento foram alteradas)", + "toast": { + "success": "As regras de enriquecimento foram salvas. Reinicie o Frigate para aplicar as alterações.", + "error": "Falha ao salvar alterações de configurações: {{errorMessage}}" + } + }, + "camera": { + "title": "Configurações de Câmera", + "streams": { + "title": "Transmissões", + "desc": "Temporáriamente desativa a câmera até o Frigate reiniciar. Desativar a câmera completamente impede o processamento da transmissão dessa câmera pelo Frigate. Detecções, gravações e depuração estarão indisponíveis.
    Nota: Isso não desativa as retransmissões do go2rtc." + }, + "review": { + "title": "Revisar", + "desc": "Temporariamente habilita/desabilita alertas e detecções para essa câmera até o Frigate reiniciar. Quando desabilitado, nenhum novo item de revisão será gerado. ", + "alerts": "Alertas ", + "detections": "Detecções " + }, + "reviewClassification": { + "title": "Classificação de Revisões", + "desc": "O Frigate categoriza itens de revisão como Alertas e Detecções. Por padrão, todas as pessoas e carros são considerados alertas. Você pode refinar a categorização dos seus itens revisados configurando as zonas requeridas para eles.", + "readTheDocumentation": "Leia a Documentação", + "noDefinedZones": "Nenhuma zona definida para essa câmera.", + "selectAlertsZones": "Selecionar as zonas para Alertas", + "selectDetectionsZones": "Selecionar as zonas para Detecções", + "objectAlertsTips": "Todos os objetos {{alertsLabels}} em {{cameraName}} serão exibidos como Alertas.", + "zoneObjectAlertsTips": "Todos os {{alertsLabels}} objetos detectados em {{zone}} em {{cameraName}} serão exibidos como Alertas.", + "objectDetectionsTips": "Todos os objetos {{detectionsLabels}} não categorizados em {{cameraName}} serão exibidos como Detecções independente de qual zona eles estiverem.", + "zoneObjectDetectionsTips": { + "text": "Todos os objetos de {{detectionsLabels}} não categorizados em {{zone}} em {{cameraName}} serão exibidos como Detecções.", + "notSelectDetections": "Todos os objetos {{detectionsLabels}} detectados em {{zone}} em {{cameraName}} não categorizados como Alertas serão exibidos como Detecções independente da zona em que estiverem.", + "regardlessOfZoneObjectDetectionsTips": "Todos os objetos {{detectionsLabels}} não categorizados em {{cameraName}} serão exibidos como Detecções independente de quais zonas estiverem." + }, + "unsavedChanges": "Configurações de Classificação de Revisões Não Salvas para {{camera}}", + "limitDetections": "Limitar detecções a zonas específicas", + "toast": { + "success": "A configuração de Revisão de Classificação foi salva. Reinicie o Frigate para aplicar as mudanças." + } + }, + "object_descriptions": { + "title": "Descrições de Objeto por IA Generativa", + "desc": "Habilitar descrições por IA Generativa temporariamente para essa câmera. Quando desativada, as descrições geradas por IA não serão requisitadas para objetos rastreados para essa câmera." + }, + "review_descriptions": { + "title": "Revisar Descrições de IA Generativa", + "desc": "Habilitar/desabilitar temporariamente descrições de revisão de IA Generativa para essa câmera. Quando desativada, as descrições de IA Generativa não serão solicitadas para revisão para essa câmera." + }, + "addCamera": "Adicionar Câmera Nova", + "editCamera": "Editar Câmera:", + "selectCamera": "Selecione uma Câmera", + "backToSettings": "Voltar para as Configurções de Câmera", + "cameraConfig": { + "add": "Adicionar Câmera", + "edit": "Editar Câmera", + "description": "Configure as opções da câmera incluindo as de transmissão e papéis.", + "name": "Nome da Câmera", + "nameRequired": "Nome para a câmera é requerido", + "nameInvalid": "O nome da câmera deve contar apenas letras, números, sublinhado ou hífens", + "namePlaceholder": "ex: porta_da_frente", + "enabled": "Habilitado", + "ffmpeg": { + "inputs": "Transmissões de Entrada", + "path": "Caminho da Transmissão", + "pathRequired": "Um caminho para a transmissão é requerido", + "pathPlaceholder": "rtsp://...", + "roles": "Regras", + "rolesRequired": "Ao menos um papel é requerido", + "rolesUnique": "Cada papel (áudio, detecção, gravação) pode ser atribuído a uma única transmissão", + "addInput": "Adicionar Transmissão de Entrada", + "removeInput": "Remover Transmissão de Entrada", + "inputsRequired": "Ao menos uma transmissão de entrada é requerida" + }, + "toast": { + "success": "Câmera {{cameraName}} salva com sucesso" + }, + "nameLength": "O nome da câmera deve ter ao menos 24 caracteres." + } + }, + "masksAndZones": { + "filter": { + "all": "Todas as Máscaras e Zonas" + }, + "restart_required": "Reinicialização requerida (máscaras/zonas foram alteradas)", + "toast": { + "success": { + "copyCoordinates": "Coordenadas copiadas para {{polyName}} para a área de transferência." + }, + "error": { + "copyCoordinatesFailed": "Não foi possível copiar as coordenadas para a área de transferência." + } + }, + "motionMaskLabel": "Máscara de Movimento {{number}}", + "objectMaskLabel": "Máscara de Objeto {{number}} ({{label}})", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "O nome da zona deve conter ao menos 2 caracteres.", + "mustNotBeSameWithCamera": "O nome da zona não pode ser igual ao nome da câmera.", + "alreadyExists": "Uma zona com esse noma já existe para essa câmera.", + "mustNotContainPeriod": "O nome da zona não pode conter ponto final.", + "hasIllegalCharacter": "O nome da zona contém caracteres ilegais." + } + }, + "distance": { + "error": { + "text": "A distância deve sair maior ou igual a 0.1.", + "mustBeFilled": "Todos os campos de distância devem ser preenchidos para utilizar a estimativa de velocidade." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "A inércia deve ser maior que 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "O tempo de permanência deve ser maior ou igual a zero." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "O limiar de velocidade deve ser maior ou igual a 0.1." + } + }, + "polygonDrawing": { + "removeLastPoint": "Remover o ultimo ponto", + "reset": { + "label": "Limpar todos os pontos" + }, + "snapPoints": { + "true": "Pontos de encaixe", + "false": "Não encaixar os pontos" + }, + "delete": { + "title": "Confirmar Deletar", + "desc": "Tem certeza que quer deletar o {{type}} {{name}}?", + "success": "{{name}} foi deletado." + }, + "error": { + "mustBeFinished": "O desenho do polígono deve ser finalizado antes de salvar." + } + } + }, + "zones": { + "label": "Zonas", + "documentTitle": "Editar Zona - Frigate", + "desc": { + "title": "Zonas permitem que você defina uma área específica do quadro para que você possa determinar se um objeto está ou não em uma área em particular.", + "documentation": "Documentação" + }, + "add": "Adicionar Zona", + "edit": "Editar Zona", + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "clickDrawPolygon": "Clique para desenhar um polígono na imagem.", + "name": { + "title": "Nome", + "inputPlaceHolder": "Digite um nome…", + "tips": "O nome deve ter no mínimo 2 caracteres, deve ter ao menos uma letra e não pode ter o nome de uma câmera ou outra zona nesta câmera." + }, + "inertia": { + "title": "Inércia", + "desc": "Especifica por quantos quadros um objeto deve permanecer em uma zona para que seja considerado na zona. Padrão: 3" + }, + "loiteringTime": { + "title": "Tempo de Permanência", + "desc": "Define o tempo mínimo em segundos que o objeto deve estar na zona para ser ativado. Padrão: 0" + }, + "objects": { + "title": "Objetos", + "desc": "Lista de objetos que se aplicam a essa zona." + }, + "allObjects": "Todos os Objetos", + "speedEstimation": { + "title": "Estimativa de Velocidade", + "desc": "Habilitar estimativa de velocidade para objetos nesta zona. A zona deve ter exatamente 4 pontos.", + "docs": "Leia a documentação", + "lineADistance": "Distância da linha A ({{unit}})", + "lineBDistance": "Distância da Linha B ({{unit}})", + "lineCDistance": "Distância da linha C ({{unit}})", + "lineDDistance": "Distância da linha D ({{unit}})" + }, + "speedThreshold": { + "title": "Limiar de Velocidade ({{unit}})", + "desc": "Especifique a velocidade mínima para o objeto ser considerado nessa zona.", + "toast": { + "error": { + "pointLengthError": "A estimativa de velocidade foi desativada para essa zona. Zonas com estimação de velocidade devem ter exatamente 4 pontos.", + "loiteringTimeError": "Zonas com tempo de permanência acima de 0 não devem ser usadas com estimativa de velocidade." + } + } + }, + "toast": { + "success": "A zona ({{zoneName}}) foi salva." + } + }, + "objectMasks": { + "objects": { + "allObjectTypes": "Todos os tipos de objetos", + "title": "Objetos", + "desc": "O tipo de objeto que se aplica para essa máscara de objeto." + }, + "toast": { + "success": { + "title": "{{polygonName}} foi salvo.", + "noName": "A máscara de objeto foi salva." + } + }, + "label": "Máscaras de Objeto", + "documentTitle": "Editar Máscara de Objeto - Frigate", + "desc": { + "title": "Máscaras de filtro de objetos são usadas para filtrar falsos positivos para um determinado tipo de objeto baseado na localização.", + "documentation": "Documentação" + }, + "add": "Adicionar Máscara de Objeto", + "edit": "Editar Máscara de Objeto", + "context": "Filtro de máscaras de objeto são usados para filtrar falsos positivos para um dado tipo de objeto baseado na localização.", + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "clickDrawPolygon": "Clique para desenhar um polígono na imagem." + }, + "motionMasks": { + "label": "Máscara de Movimento", + "documentTitle": "Editar Máscara de Movimento - Frigate", + "desc": { + "title": "Máscaras de movimento são usadas para prevenir tipos de movimento de ativarem uma detecção. Excesso de mascaramento tornará mais difícil que objetos sejam rastreados.", + "documentation": "Documentação" + }, + "add": "Nova Máscara de Movimento", + "edit": "Editar Máscara de Movimento", + "context": { + "title": "Máscaras de movimento são usadas para prevenir tipos de movimento não desejados de ativarem uma detecção (exemplo: galhos de árvores, timestamps de câmeras). Máscaras de movimento devem ser usadas com moderação . Excesso de mascaramento tornará o rastreamento de objetos mais difícil.", + "documentation": "Leia a documentação" + }, + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "clickDrawPolygon": "Clique para desenhar um polígono na imagem.", + "polygonAreaTooLarge": { + "title": "A máscara de movimento está cobrindo {{polygonArea}}% do quadro da câmera. Máscaras de movimento grandes não são recomendadas.", + "tips": "Máscaras de movimento não previnem objetos de serem detectados. Em vez disso você deve usar uma zona obrigatória.", + "documentation": "Leia a documentação" + }, + "toast": { + "success": { + "title": "{{polygonName}} foi salvo.", + "noName": "Máscara de Movimento salva." + } + } + } + }, + "motionDetectionTuner": { + "desc": { + "title": "O Frigate usa a detecção de movimento como uma verificação de primeira linha para ver se há algo acontecendo no quadro que valha a pena verificar com a detecção de objetos.", + "documentation": "Leia o Guia de Ajuste de Movimento" + }, + "Threshold": { + "title": "Limiar", + "desc": "O valor do limiar dita o quanto de mudança na luminância de um pixel é requerida para ser considerada movimento. Padrão: 30" + }, + "contourArea": { + "title": "Área de contorno", + "desc": "O valor da área de contorno é usado para decidir quais grupos de mudança de pixel se qualificam como movimento. Padrão: 10" + }, + "improveContrast": { + "title": "Melhorar o contraste", + "desc": "Melhorar contraste para cenas escuras. Padrão: Ativado" + }, + "toast": { + "success": "As configurações de movimento foram salvas." + }, + "title": "Ajuste de Detecção de Movimento", + "unsavedChanges": "Alterações do Ajuste de Movimento Não Salvas ({{camera}})" + }, + "debug": { + "detectorDesc": "O Frigate usa seus detectores ({{detectors}}) para detectar objetos no fluxo de vídeo da sua câmera.", + "desc": "A visualização de depuração mostra uma visão em tempo real dos objetos rastreados e suas estatísticas. A lista de objetos mostra um resumo com atraso de tempo dos objetos detectados.", + "objectList": "Lista de Objetos", + "boundingBoxes": { + "desc": "Mostrar caixas delimitadoras ao redor de objetos rastreados", + "colors": { + "label": "Cores da caixa delimitadora de objetos", + "info": "
  • Na inicialização, cores diferentes serão atribuídas a cada rótulo de objeto
  • Uma linha fina azul escura indica que o objeto não foi detectado neste momento
  • Uma linha fina cinza indica que o objeto foi detectado como estacionário
  • Uma linha grossa indica que o objeto está sujeito ao rastreamento automático (quando ativado)
  • " + }, + "title": "Caixas delimitadoras" + }, + "zones": { + "title": "Zonas", + "desc": "Mostrar um esboço de quaisquer zonas definidas" + }, + "mask": { + "title": "Máscaras de movimento", + "desc": "Mostrar polígonos de máscara de movimento" + }, + "motion": { + "title": "Caixas de movimento", + "desc": "Mostrar caixas ao redor das áreas onde o movimento é detectado", + "tips": "

    Caixas de movimento


    Caixas vermelhas serão sobrepostas em áreas do quadro onde o movimento está sendo detectado

    " + }, + "regions": { + "title": "Regiões", + "desc": "Mostrar uma caixa da região de interesse enviada ao detector de objetos", + "tips": "

    Caixas de Região


    Caixas verdes claras serão sobrepostas em áreas de interesse no quadro que está sendo enviado ao detector de objetos.

    " + }, + "title": "Depuração", + "debugging": "Depuração", + "objectShapeFilterDrawing": { + "desc": "Desenhe um retângulo na imagem para ver os detalhes da área e proporções", + "tips": "Habilite essa opção para desenhar um retângulo na imagem da camera para mostrar a sua área e proporção. Esses valores podem ser usados para estabelecer parâmetros de filtro de formato de objetos na sua configuração.", + "document": "Leia a documentação ", + "score": "Pontuação", + "ratio": "Proporção", + "area": "Área", + "title": "Desenho de Filtro de Formato de Objeto" + }, + "noObjects": "Nenhum Objeto", + "timestamp": { + "title": "Timestamp", + "desc": "Sobrepor um timestamp na imagem" + }, + "paths": { + "title": "Caminho", + "desc": "Mostrar pontos significantes do caminho do objeto rastreado", + "tips": "

    Caminhos


    Linhas e círculos indicarão pontos significantes por onde o objeto rastreado se moveu durante o seu ciclo de vida.

    " + }, + "audio": { + "title": "Áudio", + "noAudioDetections": "Nenhuma detecção de áudio", + "score": "pontuanção", + "currentRMS": "RMS Atual", + "currentdbFS": "dbFS Atual" + }, + "openCameraWebUI": "Abrir a Interface Web de {{camera}}" + }, + "users": { + "title": "Usuários", + "management": { + "title": "Gerenciamento de Usuário", + "desc": "Gerenciar as contas de usuário dessa instância do Frigate." + }, + "addUser": "Adicionar Usuário", + "updatePassword": "Resetar Senha", + "toast": { + "success": { + "createUser": "Usuário {{user}} criado com sucesso", + "deleteUser": "Usuário {{user}} deletado com sucesso", + "updatePassword": "Senha atualizada com sucesso.", + "roleUpdated": "Papel atualizado para {{user}}" + }, + "error": { + "setPasswordFailed": "Falha ao salvar a senha: {{errorMessage}}", + "createUserFailed": "Falha ao criar usuário: {{errorMessage}}", + "deleteUserFailed": "Falha ao deletar usuário: {{errorMessage}}", + "roleUpdateFailed": "Falha ao atualizar papel: {{errorMessage}}" + } + }, + "dialog": { + "form": { + "password": { + "match": "As senhas correspondem", + "notMatch": "As senhas são diferentes", + "title": "Senha", + "placeholder": "Digita a senha", + "confirm": { + "title": "Confirmar Senha", + "placeholder": "Confirmar Senha" + }, + "strength": { + "title": "Nível de segurança da senha: ", + "weak": "Fraca", + "medium": "Mediana", + "strong": "Forte", + "veryStrong": "Muito Forte" + } + }, + "newPassword": { + "title": "Senha Nova", + "placeholder": "Digite uma senha nova", + "confirm": { + "placeholder": "Digite a senha novamente" + } + }, + "usernameIsRequired": "Nome de usuário requerido", + "passwordIsRequired": "Senha requerida", + "user": { + "title": "Nome de Usuário", + "desc": "Apenas letras, números, pontos e sublinhados são permitidos.", + "placeholder": "Digite o nome de usuário" + } + }, + "createUser": { + "title": "Criar Novo Usuário", + "desc": "Adicionar um novo usuário e especificar um papel para acesso às áreas da interface do Frigate.", + "usernameOnlyInclude": "O nome de usuário pode conter apenas letras, números, . ou _", + "confirmPassword": "Por favor confirme a sua senha" + }, + "deleteUser": { + "title": "Deletar Usuário", + "desc": "Essa ação não pode ser desfeita. Isso irá deletar permanentemente a conta do usuário e remover todos os dados associados.", + "warn": "Tem certeza que quer deletar {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "A senha não pode estar vazia", + "doNotMatch": "As senhas não correspondem", + "updatePassword": "Atualizar Senha para {{username}}", + "setPassword": "Definir Senha", + "desc": "Crie uma senha forte para proteger essa conta." + }, + "changeRole": { + "title": "Alterar Papel do Usuário", + "select": "Selecionar um papel", + "desc": "Atualizar permissões para {{username}}", + "roleInfo": { + "intro": "Selecione o papel apropriado para esse usuário:", + "admin": "Administrador", + "adminDesc": "Acesso total a todos os recursos.", + "viewer": "Espectador", + "viewerDesc": "Limitado aos Painéis ao Vivo, Revisar, Explorar, e Exportar somente.", + "customDesc": "Papel customizado com acesso a câmeras específicas." + } + } + }, + "table": { + "username": "Nome de Usuário", + "actions": "Ações", + "role": "Papel", + "noUsers": "Nenhum usuário encontrado.", + "changeRole": "Mudar papel do usuário", + "password": "Resetar Senha", + "deleteUser": "Deletar usuário" + } + }, + "notification": { + "suspendTime": { + "10minutes": "Suspender por 10 minutos", + "30minutes": "Suspender por 30 minutos", + "1hour": "Suspender por 1 hora", + "12hours": "Suspender por 12 horas", + "24hours": "Suspender por 24 horas", + "untilRestart": "Suspender até reiniciar", + "suspend": "Suspender", + "5minutes": "Suspender por 5 minutos" + }, + "cancelSuspension": "Cancelar Suspensão", + "toast": { + "success": { + "registered": "Registrados para notificações com sucesso. É necessário reiniciar o Frigate para que as notificações possam ser enviadas (incluindo a notificação de teste).", + "settingSaved": "As configurações de notificações foram salvas." + }, + "error": { + "registerFailed": "Falha ao salvar o registro para notificações." + } + }, + "title": "Notificações", + "notificationSettings": { + "title": "Configurações de Notificação", + "desc": "O Frigate pode enviar notificações push nativamente ao seu dispositivo quando estiver sendo executado no navegador ou instalado como um PWA.", + "documentation": "Leia a Documentação" + }, + "notificationUnavailable": { + "title": "Notificações Indisponíveis", + "desc": "Notificações push da Web exigem um contexto seguro (https://…). Essa é uma limitação do navegador. Acesse o Frigate com seguraça para usar as notificações.", + "documentation": "Leia a Documentação" + }, + "globalSettings": { + "title": "Configurações Globais", + "desc": "Suspender as notificações temporáriamente para câmeras específicas em todos os dispositivos registrados." + }, + "email": { + "title": "Email", + "placeholder": "ex: exemplo@email.com", + "desc": "Um email válido é requerido e será usado para notificar você caso haja algum problema com o serviço push." + }, + "cameras": { + "title": "Câmeras", + "noCameras": "Nenhuma câmera disponível", + "desc": "Selecionar para quais câmeras habilitar as notificações." + }, + "deviceSpecific": "Configurações Específicas do Dispositivo", + "registerDevice": "Registre Esse Dispositivo", + "unregisterDevice": "Cancelar Registro Desse Dispositivo", + "sendTestNotification": "Enviar uma notificação de teste", + "unsavedRegistrations": "Registros de Notificações Não Salvos", + "unsavedChanges": "Alterações de Notificações Não Salvas", + "active": "Notificações Ativas", + "suspended": "Notificações suspensas {{time}}" + }, + "frigatePlus": { + "title": "Configurações do Frigate+", + "apiKey": { + "title": "Chave de API do Frigate+", + "validated": "A chave de API do Frigate+ detectada e validada", + "notValidated": "Chave de API do Frigate+ não detectada ou não validada", + "desc": "A chave de API do Frigate+ habilita a integração com o serviço do Frigate+.", + "plusLink": "Leia mais sobre o Frigate+" + }, + "modelInfo": { + "plusModelType": { + "baseModel": "Modelo Base", + "userModel": "Ajuste Refinado" + }, + "supportedDetectors": "Detectores Suportados", + "cameras": "Câmeras", + "loading": "Carregando informações do modelo…", + "error": "Falha ao carregar as informações do modelo", + "availableModels": "Modelos Disponíveis", + "loadingAvailableModels": "Carregando modelos disponíveis…", + "title": "Informação do Modelo", + "modelType": "Tipo de Modelo", + "trainDate": "Data do Treinamento", + "baseModel": "Modelo Base", + "modelSelect": "Os seus modelos disponíveis no Frigate+ podem ser selecionados aqui. Note que apenas modelos compatíveis com a sua configuração atual de detector podem ser selecionados." + }, + "snapshotConfig": { + "title": "Configuração de Captura de Imagem", + "desc": "Envios ao Frigate+ requerem tanto a captura de imagem normais quanto a captura de imagem clean_copy estarem habilitadas na sua configuração.", + "documentation": "Leia a documentação", + "cleanCopyWarning": "Algumas câmeras possuem captura de imagem habilitada porém têm a cópia limpa desabilitada. Você precisa habilitar a clean_copy nas suas configurações de captura de imagem para poder submeter imagems dessa câmera ao Frigate+.", + "table": { + "camera": "Câmera", + "snapshots": "Capturas de Imagem", + "cleanCopySnapshots": "Capturas de Imagem clean_copy" + } + }, + "unsavedChanges": "Alterações de configurações do Frigate+ não salvas", + "restart_required": "Reinicialização necessária (modelo do Frigate+ foi alterado)", + "toast": { + "success": "As configurações do Frigate+ foram salvas. Reinicie o Frigate para aplicar as alterações.", + "error": "Falha ao salvar as alterações de configuração: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Gatilhos", + "management": { + "title": "Gatilhos", + "desc": "Gerenciar gatilhos para {{camera}}. Use o tipo de miniatura para acionar miniaturas semelhantes para os seus objetos rastreados selecionados, e o tipo de descrição para acionar descrições semelhantes para textos que você especifica." + }, + "addTrigger": "Adicionar Gatilho", + "table": { + "name": "Nome", + "type": "Tipo", + "content": "Conteúdo", + "threshold": "Limiar", + "actions": "Ações", + "noTriggers": "Nenhum gatilho configurado para essa câmera.", + "edit": "Editar", + "deleteTrigger": "Apagar Gatilho", + "lastTriggered": "Acionado pela última vez" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descrição" + }, + "actions": { + "alert": "Marcar como Alerta", + "notification": "Enviar Notificação" + }, + "dialog": { + "createTrigger": { + "title": "Criar Gatilho", + "desc": "Criar gatilho para a câmera {{camera}}" + }, + "editTrigger": { + "title": "Editar Gatilho", + "desc": "Editar as configurações de gatilho na câmera {{camera}}" + }, + "deleteTrigger": { + "title": "Apagar Gatilho", + "desc": "Tem certeza que quer deletar o gatilho {{triggerName}}? Essa ação não pode ser desfeita." + }, + "form": { + "name": { + "title": "Nome", + "placeholder": "Nomeie este gatilho", + "error": { + "minLength": "O campo precisa ter no mínimo 2 caracteres.", + "invalidCharacters": "O campo pode contar apenas letras, números, sublinhados, e hifens.", + "alreadyExists": "Um gatilho com esse nome já existe para essa câmera." + } + }, + "enabled": { + "description": "Habilitar ou desabilitar esse gatilho" + }, + "type": { + "title": "Tipo", + "placeholder": "Selecionar o tipo de gatilho" + }, + "content": { + "title": "Conteúdo", + "imagePlaceholder": "Selecionar um thumbnail", + "textPlaceholder": "Digitar conteúdo do texto", + "imageDesc": "Selecionar uma imagem para acionar essa ação quando uma imagem semelhante for detectada.", + "textDesc": "Digite o texto para ativar essa ação quando uma descrição semelhante de objeto rastreado for detectada.", + "error": { + "required": "Um conteúdo é requerido." + } + }, + "threshold": { + "title": "Limiar", + "error": { + "min": "O limitar deve ser no mínimo 0", + "max": "O limiar deve ser no mínimo 1" + } + }, + "actions": { + "title": "Ações", + "desc": "Por padrão, o Frigate dispara uma mensagem MQTT para todos os gatilhos. Escolha uma ação adicional para realizar quando uma ação for disparada.", + "error": { + "min": "Ao menos uma ação deve ser selecionada." + } + }, + "friendly_name": { + "title": "Nome Amigável", + "placeholder": "Nomeie ou descreva esse gatilho", + "description": "Um nome amigável ou descritivo opcional para esse gatilho." + } + } + }, + "toast": { + "success": { + "createTrigger": "Gatilho {{name}} criado com sucesso.", + "updateTrigger": "Gatilho {{name}} atualizado com sucesso.", + "deleteTrigger": "Gatilho {{name}} apagado com sucesso." + }, + "error": { + "createTriggerFailed": "Falha ao criar gatilho: {{errorMessage}}", + "updateTriggerFailed": "Falha ao atualizar gatilho: {{errorMessage}}", + "deleteTriggerFailed": "Falha ao apagar gatilho: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Busca Semântica desativada", + "desc": "Busca Semântica deve estar habilitada para usar os Gatilhos." + } + }, + "roles": { + "management": { + "title": "Gerenciamento do Papel de Visualizador", + "desc": "Gerenciar papéis de visualizador customizados e suas permissões de acesso para essa instância do Frigate." + }, + "addRole": "Adicionar Papel", + "table": { + "role": "Papel", + "cameras": "Câmeras", + "actions": "Ações", + "noRoles": "Nenhum papel customizado encontrado.", + "editCameras": "Editar Câmeras", + "deleteRole": "Apagar Papel" + }, + "toast": { + "success": { + "createRole": "Papel {{role}} criado com sucesso", + "updateCameras": "Câmeras atualizados para o papel {{role}}", + "deleteRole": "Papel {{role}} apagado com sucesso", + "userRolesUpdated_one": "{{count}} usuário atribuído a essa função foi atualizado para 'visualizador', com acesso a todas as câmeras.", + "userRolesUpdated_many": "{{count}} usuários atribuídos a essa função foram atualizados para 'visualizador', com acesso a todas as câmeras.", + "userRolesUpdated_other": "{{count}} usuários atribuídos a esse papel foram atualizados para 'visualizador', com acesso a todas as câmeras." + }, + "error": { + "createRoleFailed": "Falha ao criar papel: {{errorMessage}}", + "updateCamerasFailed": "Falha ao atualizar câmeras: {{errorMessage}}", + "deleteRoleFailed": "Falha ao apagar papel: {{errorMessage}}", + "userUpdateFailed": "Falha ao atualizar papel do usuário: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Criar Novo Papel", + "desc": "Adicionar um novo papel e especificar permissões de acesso." + }, + "editCameras": { + "title": "Editar Câmeras de Papéis", + "desc": "Atualizar acesso da câmera para o papel {{role}}." + }, + "deleteRole": { + "title": "Deletar Papel", + "desc": "Essa ação não pode ser desfeita. Isso irá apagar permanentemente o papel e atribuir a quaisquer usuários com esse papel como 'visualizador', o que dará acesso de visualização para todas as câmeras.", + "warn": "Tem certeza que quer apagar {{role}}?", + "deleting": "Apagando…" + }, + "form": { + "role": { + "title": "Nome do Papel", + "placeholder": "Digitar nome do papel", + "desc": "Apenas letras, números, pontos e sublinhados são permitidos.", + "roleIsRequired": "Nome para o papel é requerido", + "roleOnlyInclude": "O nome do papel pode conter apenas letras, números, pontos ou sublinhados", + "roleExists": "Um papel com esse nome já existe." + }, + "cameras": { + "title": "Câmeras", + "desc": "Selecione as câmeras que esse papel terá acesso. Ao menos uma câmera é requerida.", + "required": "Ao menos uma câmera deve ser selecionada." + } + } + } + }, + "cameraWizard": { + "title": "Adicionar Câmera", + "description": "Siga os passos abaixo para adicionar uma câmera nova no seu Frigate.", + "steps": { + "nameAndConnection": "Nome e Conexão", + "streamConfiguration": "Configuração de Stream", + "validationAndTesting": "Validação e Teste" + }, + "save": { + "success": "Nova câmera {{cameraName}} salva com sucesso.", + "failure": "Erro ao salvar {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolução", + "video": "Vídeo", + "audio": "Áudio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Favor fornecer uma URL de stream válida", + "testFailed": "Teste de stream falhou: {{error}}" + }, + "step1": { + "description": "Insira os detalhes da sua câmera e escolha entre sondar a câmera ou selecionar a marca manualmente.", + "cameraName": "Nome da Câmera", + "cameraNamePlaceholder": "ex., porta_entrada ou Visão Geral do Quintal", + "host": "Host/Endereço IP", + "port": "Porta", + "username": "Nome de Usuário", + "usernamePlaceholder": "Opcional", + "password": "Senha", + "passwordPlaceholder": "Opcional", + "selectTransport": "Selecionar protocolo de transporte", + "cameraBrand": "Marca da Câmera", + "selectBrand": "Selecione a marca da câmera para template de URL", + "customUrl": "URL Customizada de Stream", + "brandInformation": "Informação da marca", + "brandUrlFormat": "Para câmeras com o formato de URL RTSP como: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nomedeusuario:senha@host:porta/caminho", + "testConnection": "Testar Conexão", + "testSuccess": "Teste de conexão bem sucedido!", + "testFailed": "Teste de conexão falhou. Favor verifique os dados e tente novamente.", + "streamDetails": "Detalhes do Stream", + "warnings": { + "noSnapshot": "Não foi possível adquirir uma captura de imagem do stream configurado." + }, + "errors": { + "brandOrCustomUrlRequired": "Selecione a marca da câmera com o host/IP or selecione 'Outro' com uma URL customizada", + "nameRequired": "Nome para a câmera requerido", + "nameLength": "O nome da câmera deve ter 64 caracteres ou menos", + "invalidCharacters": "Nome da câmera contém caracteres inválidos", + "nameExists": "Nome da câmera já existe" + }, + "testing": { + "probingMetadata": "Inferindo o metadata da câmera...", + "fetchingSnapshot": "Buscando a captura de imagem da câmera..." + } + } + }, + "button": { + "overriddenGlobal": "Substituir (Global)", + "overriddenGlobalTooltip": "Esta câmera substitui as configurações globais desta seção", + "overriddenBaseConfig": "Substituído (Configuração base)", + "overriddenBaseConfigTooltip": "O perfil {{profile}} substitui as configurações desta seção", + "overriddenBaseConfigNoDeltas": "O perfil {{profile}} substitui esta seção, mas nenhum valor de campo difere da configuração base.", + "overriddenInCameras": { + "profilePrefix": "{{profile}} perfil: {{fields}}" + } + }, + "menuDot": { + "overrideGlobal": "Esta seção substitui a configuração global", + "overrideProfile": "Esta seção é substituída pelo perfil {{profile}}", + "unsaved": "Esta seção contém alterações não salvas" + } +} diff --git a/web/public/locales/pt-BR/views/system.json b/web/public/locales/pt-BR/views/system.json new file mode 100644 index 0000000..431fd6b --- /dev/null +++ b/web/public/locales/pt-BR/views/system.json @@ -0,0 +1,207 @@ +{ + "documentTitle": { + "cameras": "Estatísticas das Câmeras - Frigate", + "storage": "Status de Armazenamento - Frigate", + "general": "Estatísticas Gerais - Frigate", + "enrichments": "Estatísticas de Enriquecimento - Frigate", + "logs": { + "frigate": "Registros Frigate - Frigate", + "go2rtc": "Registros GoRTC - Frigate", + "nginx": "Registros Nginx - Frigate", + "websocket": "Registros de Mensagem - Frigate" + } + }, + "title": "Sistema", + "metrics": "Métricas do sistema", + "logs": { + "download": { + "label": "Baixar registros" + }, + "copy": { + "label": "Copiar para a área de transferência", + "success": "Registros copiados para a área de transferência", + "error": "Não foi possível copiar os registros para a área de transferência" + }, + "type": { + "label": "Tipo", + "timestamp": "Marca temporal", + "tag": "Marcador", + "message": "Mensagem" + }, + "tips": "Os Registros estão sendo transmitidos do servidor", + "toast": { + "error": { + "fetchingLogsFailed": "Erro ao buscar registros: {{errorMessage}}", + "whileStreamingLogs": "Erro ao transmitir registros: {{errorMessage}}" + } + }, + "websocket": { + "label": "Mensagens", + "pause": "Pausar", + "resume": "Resumir", + "clear": "Limpar", + "filter": { + "all": "Todos os tópicos", + "topics": "Tópicos", + "events": "Eventos", + "reviews": "Avaliações", + "classification": "Classificação", + "face_recognition": "Reconhecimento facial", + "lpr": "LPR", + "camera_activity": "Atividade da câmera", + "system": "Sistema", + "camera": "Camera", + "all_cameras": "Todas as cameras" + }, + "empty": "Nenhuma mensagem capturada ainda" + } + }, + "general": { + "title": "Geral", + "detector": { + "title": "Detectores", + "inferenceSpeed": "Velocidade de Inferência do Detector", + "temperature": "Detector Temperatura", + "cpuUsage": "Utilização de CPU de Detecção", + "memoryUsage": "Utilização de Memória do Detector", + "cpuUsageInformation": "CPU utilizado para preparar os dados de entrada e saída de/para os modelos de detecção. Esse valor não mede a utilização da inferência, mesmo se estiver usando um GPU ou acelerador." + }, + "hardwareInfo": { + "title": "Informações de Hardware", + "gpuUsage": "Utilização de GPU", + "gpuMemory": "Memória da GPU", + "gpuEncoder": "Codificador da GPU", + "gpuDecoder": "Decodificador da GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Saída do Vainfo", + "returnCode": "Código de Retorno: {{code}}", + "processOutput": "Saída do Processo:", + "processError": "Erro de Processo:" + }, + "nvidiaSMIOutput": { + "title": "Saída SMI da Nvidia", + "name": "Nome: {{name}}", + "driver": "Motorista: {{driver}}", + "cudaComputerCapability": "Capacidade de Computação CUDA: {{cuda_compute}}", + "vbios": "Informação de VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Fechar informações da GPU" + }, + "copyInfo": { + "label": "Copiar informações da GPU" + }, + "toast": { + "success": "Informações da GPU copiadas para a área de transferência" + } + }, + "npuUsage": "Uso da NPU", + "npuMemory": "Memória da NPU" + }, + "otherProcesses": { + "title": "Outros processos", + "processCpuUsage": "Uso de Processamento da CPU", + "processMemoryUsage": "Uso de Memória de Processos" + } + }, + "storage": { + "title": "Armazenamento", + "overview": "Visão Geral", + "recordings": { + "title": "Gravações", + "earliestRecording": "Gravação mais recente disponível:", + "tips": "Esse valor representa o armazenamento total usado pelas gravações no banco de dados do Frigate. O Frigate não rastreia o uso de armazenamento para todos os arquivos do seu disco." + }, + "cameraStorage": { + "title": "Armazenamento da Câmera", + "camera": "Câmera", + "unusedStorageInformation": "Informação de Armazenamento Não Utilizado", + "storageUsed": "Armazenamento", + "percentageOfTotalUsed": "Porcentagem do Total", + "bandwidth": "Largura de Banda", + "unused": { + "title": "Não Utilizado", + "tips": "Esse valor por não representar com precisão o espaço livre disponí®el para o Frigate se você possui outros arquivos armazenados no seu drive além das gravações do Frigate. O Frigate não rastreia a utilização do armazenamento além de suas próprias gravações." + } + }, + "shm": { + "title": "Alocação de memória compartilhada (SHM)", + "warning": "O tamanho de {{total}}MB de memória compartilhada (SHM) é insuficiente. Aumente para ao menos {{min_shm}}MB." + } + }, + "cameras": { + "title": "Câmeras", + "overview": "Visão Geral", + "info": { + "aspectRatio": "proporção", + "cameraProbeInfo": "{{camera}} Informação de Probe da Câmera", + "streamDataFromFFPROBE": "Os dados da tranmissão são obtidos com o ffprobe.", + "fetching": "Buscando Dados da Câmera", + "stream": "Transmissão {{idx}}", + "video": "Vídeo:", + "codec": "Codec:", + "resolution": "Resolução:", + "fps": "FPS:", + "unknown": "Desconhecido", + "audio": "Áudio:", + "error": "Erro: {{error}}", + "tips": { + "title": "Informação de Probe de Câmera" + } + }, + "framesAndDetections": "Quadros / Detecções", + "label": { + "camera": "câmera", + "detect": "detectar", + "skipped": "ignoradas", + "ffmpeg": "FFmpeg", + "capture": "captura", + "overallFramesPerSecond": "quadros por segundo em geral", + "overallDetectionsPerSecond": "detecções por segundo em geral", + "overallSkippedDetectionsPerSecond": "detecções puladas por segundo em geral", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} captura", + "cameraDetect": "{{camName}} detectar", + "cameraFramesPerSecond": "{{camName}} quadros por segundo", + "cameraDetectionsPerSecond": "{{camName}} detecções por segundo", + "cameraSkippedDetectionsPerSecond": "{{camName}} detecções puladas por segundo" + }, + "toast": { + "success": { + "copyToClipboard": "Dados do probe copiados para a área de transferência." + }, + "error": { + "unableToProbeCamera": "Não foi possível fazer o probe da câmera: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Atualizado pela última vez: ", + "stats": { + "detectIsVerySlow": "{{detect}} está muito lento ({{speed}} ms)", + "ffmpegHighCpuUsage": "{{camera}} possui alta utilização de CPU para FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} possui alta utilização de CPU para detecção ({{detectAvg}}%)", + "healthy": "O sistema está saudável", + "cameraIsOffline": "{{camera}} está offline", + "reindexingEmbeddings": "Reindexando os embeddings ({{processed}}% completado)", + "detectIsSlow": "{{detect}} está lento ({{speed}} ms)", + "shmTooLow": "A alocação ({{total}} MB) para a pasta /dev/shm deve ser aumentada para ao menos {{min}} MB." + }, + "enrichments": { + "title": "Enriquecimentos", + "infPerSecond": "Inferências por Segundo", + "embeddings": { + "face_recognition": "Reconhecimento Facial", + "plate_recognition": "Reconhecimento de Placa", + "plate_recognition_speed": "Velocidade de Reconhecimento de Placas", + "text_embedding_speed": "Velocidade de Embeddings de Texto", + "yolov9_plate_detection_speed": "Velocidade de Reconhecimento de Placas do YOLOv9", + "yolov9_plate_detection": "Detecção de Placas do YOLOv9", + "image_embedding": "Embeddings de Imagens", + "text_embedding": "Embeddings de Texto", + "image_embedding_speed": "Velocidade de Embeddings de Imagens", + "face_embedding_speed": "Velocidade de Embedding de Rostos", + "face_recognition_speed": "Velocidade de Reconhecimento de Rostos" + } + } +} diff --git a/web/public/locales/pt/audio.json b/web/public/locales/pt/audio.json new file mode 100644 index 0000000..e0a4f35 --- /dev/null +++ b/web/public/locales/pt/audio.json @@ -0,0 +1,500 @@ +{ + "babbling": "Falador", + "speech": "Discurso", + "whoop": "Grito de Alegria", + "bellow": "Debaixo", + "yell": "Gritar", + "whispering": "Sussurrar", + "child_singing": "Criança a Cantar", + "crying": "Choro", + "singing": "Canto", + "laughter": "Rir", + "breathing": "Respirar", + "applause": "Aplausos", + "meow": "Miau", + "run": "Correr", + "sheep": "Ovelha", + "motorcycle": "Mota", + "car": "Carro", + "cat": "Gato", + "horse": "Cavalo", + "bus": "Autocarro", + "boat": "Barco", + "bicycle": "Bicicleta", + "skateboard": "Skate", + "door": "Porta", + "bird": "Pássaro", + "train": "Comboio", + "dog": "Cão", + "mantra": "Mantra", + "humming": "Cantarolar", + "sigh": "Suspiro", + "grunt": "Grunhido", + "whistling": "Assobiar", + "wheeze": "Chiadeira", + "gasp": "Ofegar", + "cough": "Tossir", + "sneeze": "Espirrar", + "footsteps": "Passos", + "chewing": "Mastigar", + "biting": "Morder", + "gargling": "Gargarejar", + "stomach_rumble": "Ronco de Estômago", + "burping": "Arroto", + "hiccup": "Soluço", + "fart": "Pum", + "hands": "Mãos", + "finger_snapping": "Estalar os Dedos", + "clapping": "Palmas", + "heartbeat": "Batimento Cardíaco", + "heart_murmur": "Sopro Cardíaco", + "cheering": "Aplausos Entusiásticos", + "chatter": "Conversar", + "crowd": "Multidão", + "snoring": "Ressonar", + "choir": "Coro", + "yodeling": "Iodel", + "chant": "Cântico", + "synthetic_singing": "Canto Sintético", + "rapping": "Rap", + "groan": "Gemido", + "snicker": "Risinho", + "animal": "Animal", + "pets": "Animais de Estimação", + "bark": "Latido", + "howl": "Uivar", + "bow_wow": "Au-Au", + "growling": "Rosnar", + "whimper_dog": "Choro de Cão", + "pig": "Porco", + "goat": "Cabra", + "fowl": "Aves de Capoeira", + "chicken": "Galinha", + "turkey": "Peru", + "duck": "Pato", + "quack": "Quá-Quá", + "goose": "Ganso", + "wild_animals": "Animais Selvagens", + "pigeon": "Pombo", + "dogs": "Cães", + "insect": "Inseto", + "cricket": "Grilo", + "mosquito": "Mosquito", + "fly": "Mosca", + "frog": "Rã", + "snake": "Cobra", + "rattle": "Chocalhar", + "music": "Música", + "musical_instrument": "Instrumento Musical", + "banjo": "Banjo", + "keyboard": "Teclado", + "piano": "Piano", + "organ": "Órgão", + "synthesizer": "Sintetizador", + "tambourine": "Pandeireta", + "clarinet": "Clarinete", + "harp": "Harpa", + "psychedelic_rock": "Rock Psicadélico", + "waterfall": "Cascata", + "ocean": "Oceano", + "fire": "Fogo", + "ship": "Navio", + "car_alarm": "Alarme de Carro", + "race_car": "Carro de Corridas", + "truck": "Camião", + "ice_cream_truck": "Carrinha de Gelados", + "emergency_vehicle": "Veículo de Emergência", + "police_car": "Carro da Polícia", + "ambulance": "Ambulância", + "helicopter": "Helicóptero", + "engine": "Motor", + "coin": "Moeda", + "scissors": "Tesouras", + "electric_shaver": "Barbeador Elétrico", + "computer_keyboard": "Teclado de Computador", + "alarm": "Alarme", + "telephone": "Telefone", + "siren": "Sirene", + "smoke_detector": "Detetor de Fumo", + "fire_alarm": "Alarme de Incêndio", + "whistle": "Assobio", + "clock": "Relógio", + "tools": "Ferramentas", + "camera": "Câmara", + "chink": "Estilhaçar", + "sound_effect": "Efeito Sonoro", + "static": "Estática", + "pink_noise": "Ruído Rosa", + "television": "Televisão", + "scream": "Grito Agudo", + "glass": "Vidro", + "wood": "Madeira", + "crack": "Rachar", + "silence": "Silêncio", + "steam": "Vapor", + "progressive_rock": "Rock Progressivo", + "white_noise": "Ruído Branco", + "maraca": "Maraca", + "percussion": "Percussão", + "rats": "Ratos", + "oink": "Oinc", + "waves": "Ondas", + "shatter": "Quebrar", + "radio": "Rádio", + "splinter": "Lasca", + "owl": "Coruja", + "mouse": "Rato", + "vehicle": "Veículo", + "hair_dryer": "Secador de Cabelo", + "toothbrush": "Escova de Dentes", + "sink": "Banca", + "blender": "Liquidificador", + "pant": "Ofegar", + "snort": "Resfolegar", + "throat_clearing": "Limpar a Garganta", + "sniff": "Cheirar", + "shuffle": "Embaralhar", + "children_playing": "Crianças a Brincar", + "purr": "Ronronar", + "livestock": "Gado", + "cattle": "Gado Bovino", + "cock_a_doodle_doo": "Cucurucu", + "coo": "Arrulhar", + "flapping_wings": "Bater de Asas", + "crow": "Corvo", + "hoot": "Piar", + "mandolin": "Mandolim", + "whale_vocalization": "Vocalização de Baleia", + "sitar": "Sitar", + "plucked_string_instrument": "Instrumento de Cordas Dedilhado", + "croak": "Coaxar", + "guitar": "Guitarra", + "electric_guitar": "Guitarra Elétrica", + "bass_guitar": "Baixo Elétrico", + "acoustic_guitar": "Guitarra Acústica", + "ukulele": "Ukulele", + "tapping": "Tocar com os Dedos", + "strum": "Dedilhar", + "drum_kit": "Bateria (Kit)", + "gong": "Gongo", + "orchestra": "Orquestra", + "flute": "Flauta", + "saxophone": "Saxofone", + "harmonica": "Harmónica", + "wind_instrument": "Instrumento de Sopro", + "trumpet": "Trompete", + "violin": "Violino", + "cello": "Violoncelo", + "double_bass": "Contrabaixo", + "church_bell": "Sino de Igreja", + "bicycle_bell": "Sino de Bicicleta", + "bagpipes": "Gaita de Foles", + "cowbell": "Sino de Vaca", + "hiss": "Sibilar", + "caterwaul": "Miado Forte", + "clip_clop": "Cavalgar", + "neigh": "Relincho", + "moo": "Mugido", + "gobble": "Gluglu (Peru)", + "cluck": "Cacarejar", + "caw": "Grasnido", + "chirp": "Piar (Passarinho)", + "yip": "Latido Agudo", + "bleat": "Balar", + "honk": "Buzina", + "roaring_cats": "Gatos a Ruge", + "roar": "Rugido", + "squawk": "Chilrear Estridente", + "patter": "Tamborilar", + "buzz": "Zumbido", + "steel_guitar": "Guitarra Steel", + "zither": "Cítara", + "electric_piano": "Piano Elétrico", + "electronic_organ": "Órgão Eletrónico", + "hammond_organ": "Órgão Hammond", + "sampler": "Sampler", + "harpsichord": "Cravo (Instrumento)", + "drum_machine": "Máquina de Ritmos", + "drum": "Tambor", + "snare_drum": "Tarola", + "rimshot": "Tocada de Caixa (Rimshot)", + "drum_roll": "Rufar de Tambor", + "bass_drum": "Bombo", + "timpani": "Tímpano", + "tabla": "Tabla", + "cymbal": "Prato de Bateria", + "hi_hat": "Prato Hi-Hat", + "wood_block": "Bloco de Madeira", + "tubular_bells": "Sinos Tubulares", + "mallet_percussion": "Percussão com Baquetas", + "glockenspiel": "Glockenspiel", + "electronic_dance_music": "Música de Dança Eletrónica", + "ambient_music": "Música Ambiente", + "trance_music": "Música Trance", + "music_of_latin_america": "Música da América Latina", + "salsa_music": "Música Salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Música para Crianças", + "new-age_music": "Música New Age", + "vocal_music": "Música Vocal", + "a_capella": "A Capella", + "music_of_africa": "Música de África", + "lullaby": "Canção de Embalar", + "video_game_music": "Música de Videogame", + "christmas_music": "Música de Natal", + "dance_music": "Música de Dança", + "wedding_music": "Música de Casamento", + "happy_music": "Música Alegre", + "sad_music": "Música Triste", + "tender_music": "Música Suave", + "exciting_music": "Música Excitante", + "scary_music": "Música Assustadora", + "wind": "Vento", + "rustling_leaves": "Folhas a Mexer", + "wind_noise": "Ruído do Vento", + "thunderstorm": "Trovoada", + "thunder": "Trovão", + "water": "Água", + "rain": "Chuva", + "raindrop": "Gota de Chuva", + "rain_on_surface": "Chuva a Cair na Superfície", + "stream": "Torrente", + "gurgling": "Gorgolejar", + "train_whistle": "Apito de Comboio", + "train_horn": "Buzina de Comboio", + "railroad_car": "Carruagem Ferroviária", + "train_wheels_squealing": "Rodas de Comboio a Ranger", + "subway": "Metro", + "aircraft": "Aeronave", + "aircraft_engine": "Motor de Aeronave", + "jet_engine": "Motor a Jato", + "propeller": "Hélice", + "jackhammer": "Britadeira", + "sawing": "Serrar", + "filing": "Lixar", + "power_tool": "Ferramenta Elétrica", + "sanding": "Lixar Madeira", + "drill": "Berbequim", + "explosion": "Explosão", + "gunshot": "Disparo de Arma", + "machine_gun": "Metralhadora", + "fusillade": "Rajada de Disparos", + "artillery_fire": "Fogo de Artilharia", + "cap_gun": "Pistola de Brincar", + "fireworks": "Fogo de Artifício", + "firecracker": "Bombinha", + "burst": "Ruptura", + "typing": "Digitar", + "angry_music": "Música Zangada", + "typewriter": "Máquina de Escrever", + "marimba": "Marimba", + "vibraphone": "Vibrafone", + "steelpan": "Tambor de aço", + "brass_instrument": "Instrumento de Metal", + "french_horn": "Trompa", + "trombone": "Trombone", + "heavy_metal": "Heavy Metal", + "bowed_string_instrument": "Instrumento de Cordas com Arco", + "string_section": "Secção de Cordas", + "bell": "Sino", + "jingle_bell": "Sino de Natal", + "tuning_fork": "Diapasão", + "wind_chime": "Sino de Vento", + "pizzicato": "Pizzicato", + "chime": "Carrilhão", + "accordion": "Acordeão", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Tigela Sonora", + "scratching": "Raspar", + "hip_hop_music": "Música Hip-Hop", + "beatboxing": "Beatbox", + "rock_music": "Música Rock", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "rock_and_roll": "Rock and Roll", + "rhythm_and_blues": "Rhythm and Blues", + "pop_music": "Música Pop", + "soul_music": "Música Soul", + "reggae": "Reggae", + "country": "Country", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Música Folclórica", + "swing_music": "Música Swing", + "middle_eastern_music": "Música do Médio Oriente", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Música Clássica", + "opera": "Ópera", + "electronic_music": "Música Eletrónica", + "house_music": "Música House", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Eletrónica", + "afrobeat": "Afrobeat", + "christian_music": "Música Cristã", + "gospel_music": "Música Gospel", + "music_of_asia": "Música da Ásia", + "carnatic_music": "Música Carnática", + "music_of_bollywood": "Música de Bollywood", + "ska": "Ska", + "traditional_music": "Música Tradicional", + "independent_music": "Música Independente", + "song": "Canção", + "background_music": "Música de Fundo", + "theme_music": "Música de Tema", + "jingle": "Tilintar", + "soundtrack_music": "Música de Banda Sonora", + "air_brake": "Travão de Ar", + "air_horn": "Buzina de Ar", + "reversing_beeps": "Bip de Marcha-Atrás", + "crackle": "Estalidos", + "traffic_noise": "Ruído de Trânsito", + "power_windows": "Janelas Elétricas", + "skidding": "Derrapar", + "tire_squeal": "Guinada de Pneus", + "car_passing_by": "Carro a Passar", + "sailboat": "Veleiro", + "rowboat": "Barco a Remos", + "motorboat": "Barco a Motor", + "motor_vehicle": "Veículo Motorizado", + "fire_engine": "Carro dos Bombeiros", + "toot": "Buzina Curta", + "rail_transport": "Transporte Ferroviário", + "accelerating": "Aceleração", + "dental_drill's_drill": "Broca Dentária", + "lawn_mower": "Corta-relva", + "chainsaw": "Motosserra", + "medium_engine": "Motor Médio", + "engine_knocking": "Batidas do Motor", + "engine_starting": "Partida de Motor", + "idling": "Ao Ralenti", + "slam": "Bater Forte", + "light_engine": "Motor Leve", + "sliding_door": "Porta de Correr", + "knock": "Tocar à Porta", + "fixed-wing_aircraft": "Aeronave de Asa Fixa", + "doorbell": "Campainha de Porta", + "ding-dong": "Ding-Dong", + "heavy_engine": "Motor Pesado", + "squeak": "Ranger", + "cupboard_open_or_close": "Armário a Abrir ou Fechar", + "drawer_open_or_close": "Gaveta a Abrir ou Fechar", + "tap": "Toque", + "dishes": "Pratos", + "cutlery": "Talheres", + "chopping": "Cortar", + "frying": "Fritar", + "microwave_oven": "Forno Micro-ondas", + "water_tap": "Torneira", + "bathtub": "Banheira", + "keys_jangling": "Chaves a Tilintar", + "vacuum_cleaner": "Aspirador", + "zipper": "Fecho Éclair", + "shuffling_cards": "Embaralhar Cartas", + "toilet_flush": "Descarga de Sanita", + "electric_toothbrush": "Escova de Dentes Elétrica", + "writing": "Escrever", + "telephone_bell_ringing": "Campainha de Telefone", + "ringtone": "Toque de Telemóvel", + "telephone_dialing": "Discar Telefone", + "dial_tone": "Tom de Marcações", + "busy_signal": "Sinal de Ocupado", + "steam_whistle": "Apito a Vapor", + "mechanisms": "Mecanismos", + "ratchet": "Catraca", + "civil_defense_siren": "Sirene de Defesa Civil", + "buzzer": "Campainha", + "foghorn": "Buzina de Nevoeiro", + "alarm_clock": "Despertador", + "gears": "Engrenagens", + "pulleys": "Polias", + "sewing_machine": "Máquina de Costura", + "mechanical_fan": "Ventoinha Mecânica", + "air_conditioning": "Ar Condicionado", + "cash_register": "Caixa Registadora", + "printer": "Impressora", + "tick": "Tique-taque", + "tick-tock": "Tique-Taque", + "single-lens_reflex_camera": "Câmara Reflex de Lente Única", + "hammer": "Martelo", + "boom": "Estrondo", + "chop": "Corte", + "eruption": "Erupção", + "environmental_noise": "Ruído Ambiental", + "field_recording": "Gravação de Campo", + "change_ringing": "Mudar Toque", + "shofar": "Berrante", + "liquid": "Líquido", + "splash": "Respingar", + "slosh": "Respingo", + "squish": "Esmagar", + "drip": "Pingar", + "pour": "Entornar", + "trickle": "Gotejar", + "gush": "Jorrar", + "fill": "Preencher", + "spray": "Borrifar", + "pump": "Bombear", + "stir": "Mexer", + "boiling": "A ferver", + "sonar": "Sonar", + "arrow": "Flecha", + "whoosh": "Uau", + "thump": "Baque", + "thunk": "Tombo", + "electronic_tuner": "Afinador Eletrônico", + "effects_unit": "Unidade de Efeitos", + "chorus_effect": "Efeito Coro", + "basketball_bounce": "Quique da bola", + "bang": "Batida", + "slap": "Tapa", + "whack": "Bater", + "smash": "Esmagar", + "breaking": "A quebrar", + "bouncing": "A quicar", + "whip": "Chicote", + "flap": "Guia", + "scratch": "Arranhão", + "scrape": "Raspagem", + "rub": "Esfregar", + "roll": "Rolar", + "crushing": "Esmagamento", + "crumpling": "Amarrotar", + "tearing": "A rasgar", + "beep": "Bip", + "ping": "Pingo", + "ding": "Campainha", + "clang": "Estridente", + "squeal": "Guincho", + "creak": "Ranger", + "rustle": "Farfalhar", + "whir": "Zumbir", + "clatter": "Barulho", + "sizzle": "Chiado", + "clicking": "A clicar", + "clickety_clack": "Tique-taque", + "rumble": "Estrondo", + "plop": "Ploft", + "hum": "Zumbir", + "boing": "Poin", + "crunch": "Mastigar", + "sine_wave": "Onda Senoidal", + "harmonic": "Harmonica", + "chirp_tone": "Som Agudo", + "pulse": "Pulso", + "inside": "Dentro", + "outside": "Fora", + "reverberation": "Reverberação", + "echo": "Eco", + "noise": "Ruído", + "mains_hum": "Zumbido Elétrico", + "distortion": "Distorção", + "sidetone": "Retorno de Voz", + "cacophony": "Cacofonia", + "throbbing": "Latejante", + "vibration": "Vibração" +} diff --git a/web/public/locales/pt/common.json b/web/public/locales/pt/common.json new file mode 100644 index 0000000..7e7e101 --- /dev/null +++ b/web/public/locales/pt/common.json @@ -0,0 +1,341 @@ +{ + "time": { + "last30": "Últimos 30 dias", + "12hours": "12 horas", + "justNow": "Agora mesmo", + "yesterday": "Ontem", + "today": "Hoje", + "last7": "Últimos 7 dias", + "last14": "Últimos 14 dias", + "thisWeek": "Esta Semana", + "lastWeek": "Semana Passada", + "5minutes": "5 minutos", + "10minutes": "10 minutos", + "30minutes": "30 minutos", + "24hours": "24 horas", + "pm": "pm", + "am": "am", + "year_one": "{{time}} ano", + "year_many": "{{time}} de anos", + "year_other": "{{time}} anos", + "month_one": "{{time}} mês", + "month_many": "{{time}} meses", + "month_other": "{{time}} meses", + "day_one": "{{time}} dia", + "day_many": "{{time}} dias", + "day_other": "{{time}} dias", + "thisMonth": "Este Mês", + "lastMonth": "Mês Passado", + "1hour": "1 hora", + "hour_one": "{{time}} hora", + "hour_many": "{{time}} horas", + "hour_other": "{{time}} horas", + "minute_one": "{{time}} minuto", + "minute_many": "{{time}} minutos", + "minute_other": "{{time}} minutos", + "second_one": "{{time}} segundo", + "second_many": "{{time}} segundos", + "second_other": "{{time}} segundos", + "untilForTime": "Até {{time}}", + "untilForRestart": "Até que o Frigate reinicie.", + "untilRestart": "Até reiniciar", + "ago": "há {{timeAgo}}", + "d": "{{time}}d", + "h": "{{time}}h", + "m": "{{time}}m", + "s": "{{time}}s", + "yr": "{{time}}ano", + "mo": "{{time}}mês", + "formattedTimestamp": { + "12hour": "%b %-d, %I:%M:%S %p", + "24hour": "%b %-d, %H:%M:%S" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ss a", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampExcludeSeconds": { + "12hour": "%b %-d, %I:%M %p", + "24hour": "%b %-d, %H:%M" + }, + "formattedTimestampWithYear": { + "12hour": "%b %-d %Y, %I:%M %p", + "24hour": "%b %-d %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "inProgress": "Em andamento", + "invalidStartTime": "Horário de início inválido", + "invalidEndTime": "Horário de término inválido", + "never": "Nunca" + }, + "unit": { + "speed": { + "kph": "km/h", + "mph": "mph" + }, + "length": { + "feet": "pés", + "meters": "metros" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hora", + "mbph": "MB/hora", + "gbph": "GB/hora" + } + }, + "button": { + "enabled": "Ativado", + "enable": "Ativar", + "done": "Feito", + "reset": "Reiniciar", + "disabled": "Desativado", + "saving": "A guardar…", + "apply": "Aplicar", + "disable": "Desativar", + "save": "Salvar", + "copy": "Copiar", + "cancel": "Cancelar", + "close": "Fechar", + "history": "Histórico", + "back": "Voltar", + "fullscreen": "Ecrã Completo", + "exitFullscreen": "Sair do Ecrã Completo", + "twoWayTalk": "Conversa Bidirecional", + "cameraAudio": "Áudio da Câmera", + "edit": "Editar", + "off": "DESLIGADO", + "copyCoordinates": "Copiar coordenadas", + "on": "LIGADO", + "delete": "Eliminar", + "download": "Transferir", + "info": "Informação", + "no": "Não", + "suspended": "Suspenso", + "yes": "Sim", + "unselect": "Desselecionar", + "unsuspended": "Dessuspender", + "deleteNow": "Eliminar Agora", + "export": "Exportar", + "next": "Seguinte", + "play": "Tocar", + "pictureInPicture": "Imagem sobre Imagem", + "continue": "Continuar", + "add": "Adicionar", + "applying": "A aplicar…", + "undo": "Desfazer", + "copiedToClipboard": "Copiado para a área de transferência", + "modified": "Modificado", + "overridden": "Substituído", + "resetToGlobal": "Redefinir para o Global", + "resetToDefault": "Redefinir para o Padrão", + "saveAll": "Gravar Tudo", + "savingAll": "A gravar tudo…", + "undoAll": "Desfazer Tudo", + "retry": "Tente novamente" + }, + "label": { + "back": "Voltar", + "hide": "Ocultar {{item}}", + "show": "Exibir {{item}}", + "ID": "ID", + "none": "Nenhum", + "all": "Todos", + "other": "Outros" + }, + "menu": { + "user": { + "logout": "Terminar sessão", + "account": "Conta", + "current": "Utilizador atual: {{user}}", + "setPassword": "Definir Palavra-passe", + "title": "Utilizador", + "anonymous": "anónimo" + }, + "faceLibrary": "Biblioteca de Rostos", + "withSystem": "Sistema", + "theme": { + "label": "Tema", + "blue": "Azul", + "green": "Verde", + "red": "Vermelho", + "contrast": "Alto contraste", + "default": "Predefinição", + "highcontrast": "Alto Contraste", + "nord": "Nord" + }, + "system": "Sistema", + "systemMetrics": "Métricas do sistema", + "configuration": "Configuração", + "systemLogs": "Registos do sistema", + "settings": "Definições", + "configurationEditor": "Editor de Configuração", + "languages": "Idiomas", + "language": { + "en": "Inglês (EUA)", + "zhCN": "简体中文 (Chinês Simplificado)", + "withSystem": { + "label": "Utilizar as definições do sistema para o idioma" + }, + "fr": "Francês (França)", + "es": "Espanhol (Espanha)", + "ru": "Russo", + "de": "Alemão (Alemanha)", + "ja": "Japonês", + "yue": "Cantonês", + "ar": "Árabe", + "uk": "Ucraniano", + "el": "Grego", + "hi": "Híndi (Índia)", + "pt": "Português (Portugal)", + "tr": "Turco (Turquia)", + "it": "Italiano (Itália)", + "nb": "Norueguês Bokmål", + "ko": "Coreano", + "vi": "Vietnamita", + "nl": "Holandês (Holanda)", + "sv": "Sueco", + "cs": "Checo", + "fa": "Persa", + "pl": "Polaco", + "he": "Hebraico", + "fi": "Finlandês", + "da": "Dinamarquês", + "ro": "Romeno", + "hu": "Húngaro", + "sk": "Eslovaco", + "th": "Tailandês", + "ca": "Catalão", + "ptBR": "Português (Brazil)", + "sr": "Sérvio", + "sl": "Esloveno", + "lt": "Lituano", + "bg": "Búlgaro", + "gl": "Galego", + "id": "Indonésio Bahasa", + "ur": "Urdu", + "hr": "Hrvatski (Croata)", + "bs": "Bosanski (Bósnio)", + "zhHant": "繁體中文 (Chinês Tradicional)" + }, + "appearance": "Aparência", + "darkMode": { + "label": "Modo Escuro", + "withSystem": { + "label": "Utilizar as definições do sistema para o modo claro ou escuro" + }, + "light": "Claro", + "dark": "Escuro" + }, + "help": "Ajuda", + "documentation": { + "title": "Documentação", + "label": "Documentação do Frigate" + }, + "restart": "Reiniciar Frigate", + "live": { + "title": "Ao vivo", + "allCameras": "Todas as Câmaras", + "cameras": { + "title": "Câmaras", + "count_one": "{{count}} Câmera", + "count_many": "{{count}} Câmeras", + "count_other": "{{count}} Câmeras" + } + }, + "export": "Exportar", + "explore": "Explorar", + "review": "Rever", + "uiPlayground": "Área de Testes da IU", + "classification": "Classificação", + "profiles": "Perfis", + "actions": "Ações", + "features": "Funcionalidades", + "chat": "Chat" + }, + "pagination": { + "previous": { + "label": "Ir para a página anterior", + "title": "Anterior" + }, + "label": "paginação", + "next": { + "title": "Seguinte", + "label": "Ir para a página seguinte" + }, + "more": "Mais páginas" + }, + "role": { + "admin": "Administrador", + "viewer": "Visualizador", + "title": "Função", + "desc": "Os administradores têm acesso completo a todas as funcionalidades da IU do Frigate. Os visualizadores estão limitados a visualizar as câmeras, rever itens, e o histórico de gravaçoes na IU." + }, + "toast": { + "copyUrlToClipboard": "URL copiado para a área de transferência.", + "save": { + "title": "Guardar", + "error": { + "noMessage": "Não foi possível guardar as alterações da configuração", + "title": "Não foi possível guardar as alterações da configuração: {{errorMessage}}" + }, + "success": "Alterações gravadas com sucesso." + } + }, + "accessDenied": { + "documentTitle": "Frigate - Acesso Negado", + "title": "Acesso Negado", + "desc": "Não tem permissão para ver esta página." + }, + "notFound": { + "documentTitle": "Frigate - Não Encontrado", + "desc": "Página não encontrada", + "title": "404" + }, + "selectItem": "Selecionar {{item}}", + "readTheDocumentation": "Leia a documentação", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} e {{1}}", + "many": "{{items}} e {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opcional", + "internalID": "o Frigate utiliza o ID na configuração e no banco de dados" + }, + "no_items": "Sem elementos", + "validation_errors": "Erros de validação", + "credentialField": { + "savedPlaceholder": "Gravado - deixar em branco para manter a atual" + } +} diff --git a/web/public/locales/pt/components/auth.json b/web/public/locales/pt/components/auth.json new file mode 100644 index 0000000..3fa777b --- /dev/null +++ b/web/public/locales/pt/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nome do utilizador", + "login": "Iniciar sessão", + "errors": { + "usernameRequired": "O nome do utilizador é obrigatório", + "passwordRequired": "A palavra-passe é obrigatória", + "rateLimit": "Limite de taxa excedido. Tente novamente mais tarde.", + "loginFailed": "Autenticação falhou", + "unknownError": "Erro desconhecido. Verifique os registos.", + "webUnknownError": "Erro desconhecido. Verifique os registos da consola." + }, + "password": "Palavra-passe", + "firstTimeLogin": "Está tentando fazer login pela primeira vez? As credenciais estão impressas nos registros do Frigate." + } +} diff --git a/web/public/locales/pt/components/camera.json b/web/public/locales/pt/components/camera.json new file mode 100644 index 0000000..53e2853 --- /dev/null +++ b/web/public/locales/pt/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "Grupos de Câmaras", + "add": "Adicionar Gupo de Câmaras", + "edit": "Editar Grupo de Câmaras", + "delete": { + "label": "Eliminar Grupo de Câmaras", + "confirm": { + "title": "Confirmar Eliminar", + "desc": "Tem a certeza que deseja eliminar o grupo de câmaras {{name}}?" + } + }, + "name": { + "label": "Nome", + "placeholder": "Inserir um nome…", + "errorMessage": { + "exists": "O nome do grupo de câmaras já existe.", + "nameMustNotPeriod": "O nome do grupo de câmaras não deve conter pontos.", + "mustLeastCharacters": "O nome do grupo de câmaras deve ter pelo menos 2 carateres.", + "invalid": "Nome do grupo de câmaras inválido." + } + }, + "cameras": { + "desc": "Selecione as câmaras para este grupo.", + "label": "Câmaras" + }, + "icon": "Ícone", + "success": "O grupo de câmaras ({{name}}) foi guardado.", + "camera": { + "setting": { + "audioIsAvailable": "O áudio está disponível para esta transmissão", + "audioIsUnavailable": "O áudio não está disponível para esta transmissão", + "audio": { + "tips": { + "document": "Leia a documentação ", + "title": "O áudio deve ser emitido pela sua câmara e configurado no go2rtc para esta transmissão." + } + }, + "streamMethod": { + "label": "Método de Transmissão", + "method": { + "smartStreaming": { + "label": "Transmissão Inteligente (recomendado)", + "desc": "A transmissão inteligente atualizará a imagem da sua câmara uma vez por minuto quando não ocorrer nenhuma atividade detetável para conservar largura de banda e recursos. Quando a atividade é detetada, a imagem muda perfeitamente para uma transmissão ao vivo." + }, + "continuousStreaming": { + "label": "Transmissão Contínua", + "desc": { + "warning": "A transmissão contínua pode causar a utilização alta da largura de banda e problemas de desempenho. Utilize com precaução.", + "title": "A imagem da câmara será sempre uma transmissão ao vivo quando visível no painel, mesmo que não esteja a ser detetada nenhuma atividade." + } + }, + "noStreaming": { + "label": "Sem transmissão", + "desc": "As imagens da câmara serão atualizadas apenas uma vez por minuto e não haverá transmissão ao vivo." + } + }, + "placeholder": "Escolha um método de transmissão" + }, + "compatibilityMode": { + "label": "Modo de compatibilidade", + "desc": "Ative esta opção apenas se a transmissão ao vivo da sua câmara estiver a exibir artefatos de cor e tiver uma linha diagonal no lado direito da imagem." + }, + "label": "Definições de Transmissão da Câmara", + "desc": "Altere as opções de transmissão ao vivo para o painel deste grupo de câmaras. Estas definições são específicas do dispositivo/navegador.", + "title": "{{cameraName}} Definições de Transmissão", + "placeholder": "Escolha uma transmissão", + "stream": "Transmissão" + }, + "birdseye": "Vista Aérea" + }, + "showAll": "Mostrar todos os Grupos de Câmeras", + "showLess": "Mostrar Menos", + "editGroups": "Editar Grupos de Câmeras" + }, + "debug": { + "options": { + "label": "Definições", + "title": "Opções", + "hideOptions": "Ocultar Opções", + "showOptions": "Mostrar Opções" + }, + "boundingBox": "Caixa Delimitadora", + "timestamp": "Carimbo de hora", + "zones": "Zonas", + "mask": "Máscara", + "motion": "Movimento", + "regions": "Regiões", + "paths": "Caminhos" + } +} diff --git a/web/public/locales/pt/components/dialog.json b/web/public/locales/pt/components/dialog.json new file mode 100644 index 0000000..966c953 --- /dev/null +++ b/web/public/locales/pt/components/dialog.json @@ -0,0 +1,144 @@ +{ + "restart": { + "button": "Reiniciar", + "restarting": { + "title": "Frigate está a reiniciar", + "content": "Esta página será recarregada em {{countdown}} segundos.", + "button": "Forçar Recarregar Agora" + }, + "title": "Tem a certeza que deseja reiniciar o Frigate?", + "description": "Isto irá parar brevemente o Frigate enquanto reinicia." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Submeter para Frigate+", + "desc": "Os objetos nas localizações que quer evitar não são falsos positivos. Submete-los como falsos positivos confundirá o modelo." + }, + "review": { + "true": { + "label": "Confirme esta etiqueta para Frigate Plus", + "true_one": "Este é um {{label}}", + "true_many": "Estes são muitos {{label}}", + "true_other": "Estão são {{label}}" + }, + "state": { + "submitted": "Submetido" + }, + "false": { + "label": "Não confirmar esta etiqueta para Frigate Plus", + "false_one": "Este não é um {{label}}", + "false_many": "Estes não são muitos {{label}}", + "false_other": "Estes não são {{label}}" + }, + "question": { + "label": "Confirme esta etiqueta para Frigate Plus", + "ask_a": "Este objeto é um {{label}}?", + "ask_an": "Este objeto é um {{label}}?", + "ask_full": "Este objeto é um {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "toast": { + "error": "Não foi possível submeter para Frigate+. Por favor verifique a sua connecção e tente novamente." + } + } + }, + "video": { + "viewInHistory": "Ver no Histórico" + } + }, + "export": { + "time": { + "fromTimeline": "Selecione na linha do tempo", + "start": { + "title": "Hora de início", + "label": "Selecione a hora de início" + }, + "end": { + "title": "Hora de término", + "label": "Selecione a hora de término" + }, + "custom": "Personalizado", + "lastHour_one": "Última hora", + "lastHour_many": "Últimas {{count}} horas", + "lastHour_other": "Últimas {{count}} horas" + }, + "export": "Exportar", + "toast": { + "success": "Exportação iniciada com sucesso. Veja o ficheiro na pasta de exportações.", + "error": { + "failed": "Não foi possível iniciar a exportação: {{error}}", + "endTimeMustAfterStartTime": "O horário de término deve ser posterior ao horário de início", + "noVaildTimeSelected": "Nenhum intervalo de tempo válido selecionado" + } + }, + "selectOrExport": "Selecionar ou Exportar", + "fromTimeline": { + "saveExport": "Guardar Exportação", + "previewExport": "Pré-visualizar Exportação" + }, + "select": "Selecionar", + "name": { + "placeholder": "Nome da Exportação" + }, + "case": { + "newCaseOption": "Criar um novo caso", + "newCaseNamePlaceholder": "Nome do novo caso" + }, + "multiCamera": { + "selectFromTimeline": "Selecionar da Timeline" + } + }, + "streaming": { + "showStats": { + "label": "Mostrar estatísticas de transmissão", + "desc": "Ative esta opção para mostrar as estatísticas de transmissão como uma sobreposição na feed da câmara." + }, + "restreaming": { + "desc": { + "title": "Configure go2rtc para obter opções adicionais da visualização ao vivo e o áudio para esta câmara.", + "readTheDocumentation": "Leia a documentação" + }, + "disabled": "A retransmissão não está ativada para esta câmara." + }, + "label": "Transmissão", + "debugView": "Ver Depuração" + }, + "search": { + "saveSearch": { + "label": "Guardar Procura", + "overwrite": "{{searchName}} já existe. Ao guardar irá substituir o valor existente.", + "success": "A procura ({{searchName}}) foi guardada.", + "button": { + "save": { + "label": "Guardar esta procura" + } + }, + "placeholder": "Insira um nome para a sua procura", + "desc": "Forneça um nome para esta procura guardada." + } + }, + "recording": { + "confirmDelete": { + "title": "Confirmar Eliminar", + "desc": { + "selected": "Tem a certeza que deseja eliminar todos os vídeos guardados associados com este item de análise?

    Pressione a tecla Shift para ignorar esta janela no futuro." + }, + "toast": { + "success": "As imagens de vídeo associadas com os itens de análise selecionados foram elimiandos com sucesso.", + "error": "Não foi possível eliminar: {{error}}" + } + }, + "button": { + "export": "Exportar", + "markAsReviewed": "Marcar como analisado", + "deleteNow": "Eliminar Agora" + } + }, + "imagePicker": { + "selectImage": "Selecione a miniatura de um objeto rastreado", + "search": { + "placeholder": "Pesquisar por etiqueta ou sub-etiqueta..." + }, + "noImages": "Nenhuma miniatura encontrada para esta câmera" + } +} diff --git a/web/public/locales/pt/components/filter.json b/web/public/locales/pt/components/filter.json new file mode 100644 index 0000000..3f7fce7 --- /dev/null +++ b/web/public/locales/pt/components/filter.json @@ -0,0 +1,137 @@ +{ + "labels": { + "label": "Etiquetas", + "all": { + "short": "Etiquetas", + "title": "Todas as etiquetas" + }, + "count": "{{count}} etiquetas", + "count_one": "{{count}} Etiqueta", + "count_other": "{{count}} Etiquetas" + }, + "filter": "Filtro", + "zones": { + "label": "Zonas", + "all": { + "title": "Todas as Zonas", + "short": "Zonas" + } + }, + "dates": { + "all": { + "title": "Todas as Datas", + "short": "Datas" + }, + "selectPreset": "Selecionar um Pré-ajuste…" + }, + "more": "Mais Filtros", + "reset": { + "label": "Redefinir filtros para valores padrão" + }, + "subLabels": { + "label": "Sub etiquetas", + "all": "Todas sub etiquetas" + }, + "score": "Pontuação", + "features": { + "label": "Funcionalidades", + "hasSnapshot": "Tem uma captura", + "hasVideoClip": "Tem um videoclipe", + "submittedToFrigatePlus": { + "label": "Submetido para Frigate+", + "tips": "Primeiro, deve filtrar os objetos rastreados que têm uma captura.

    Os objetos rastreados sem uma captura não podem ser submetidos para Frigate+." + } + }, + "sort": { + "label": "Ordenar", + "dateAsc": "Data (Ascendente)", + "scoreAsc": "Pontuação do Objeto (Ascendente)", + "scoreDesc": "Pontuação do Objeto (Descendente)", + "speedDesc": "Velocidade Estimada (Descendente)", + "speedAsc": "Velocidade Estimada (Ascendente)", + "dateDesc": "Data (Decrescente)", + "relevance": "Relevância" + }, + "cameras": { + "label": "Filtro de Câmaras", + "all": { + "short": "Câmaras", + "title": "Todas as Câmaras" + } + }, + "review": { + "showReviewed": "Mostrar analisados" + }, + "motion": { + "showMotionOnly": "Mostrar apenas movimento" + }, + "explore": { + "settings": { + "title": "Definições", + "defaultView": { + "title": "Visualização Predefinida", + "summary": "Resumo", + "unfilteredGrid": "Grelha não Filtrada", + "desc": "Quando não for selecionado nenhum filtro, exiba um resumo dos objetos rastreados mais recentes por etiqueta, ou exiba uma grelha não filtrada." + }, + "gridColumns": { + "title": "Colunas da Grelha", + "desc": "Selecione o número de colunas na visualização em grelha." + }, + "searchSource": { + "label": "Procurar Fonte", + "desc": "Escolha se deseja procurar nas miniaturas ou descrições dos seus objetos rastreados.", + "options": { + "thumbnailImage": "Imagem em Miniatura", + "description": "Descrição" + } + } + }, + "date": { + "selectDateBy": { + "label": "Selecione uma data para filtrar" + } + } + }, + "logSettings": { + "label": "Nível de registo do filtro", + "loading": { + "title": "A carregar", + "desc": "Quando desliza até ao fundo no painel de registos, os novos registos são apresentados automaticamente à medida que são adicionados." + }, + "filterBySeverity": "Filtrar registos por gravidade", + "disableLogStreaming": "Desativar transmissão de registos", + "allLogs": "Todos os registos" + }, + "estimatedSpeed": "Velocidade estimada ({{unit}})", + "timeRange": "Intervalo de tempo", + "zoneMask": { + "filterBy": "Filtrar por máscara de zona" + }, + "trackedObjectDelete": { + "title": "Confirmar Eliminar", + "toast": { + "success": "Objetos rastreados eliminados com sucesso.", + "error": "Não foi possível eliminar os objetos rastreados: {{errorMessage}}" + }, + "desc": "Ao eliminar estes {{objectLength}} objetos rastreados remove a captura de imagem, quaisquer integrações guardadas, e todas as entradas associadas ao ciclo de vida do objeto. As gravações desses objetos rastreados na visualização do Histórico NÃO serão eliminadas.

    Tem a certeza que deseja continuar?

    Mantenha pressionada a tecla Shift para ignorar esta janela no futuro." + }, + "recognizedLicensePlates": { + "title": "Matrículas Reconhecidas", + "noLicensePlatesFound": "Não foram encontradas matrículas.", + "selectPlatesFromList": "Selecione uma ou mais matrículas da lista.", + "loadFailed": "Não foi possível carregar as matrículas reconhecidas.", + "loading": "A carregar as matrículas reconhecidas…", + "placeholder": "Digite para procurar matrículas…", + "selectAll": "Selecionar tudo", + "clearAll": "Limpar tudo" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Todas as Classes" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classes" + } +} diff --git a/web/public/locales/pt/components/icons.json b/web/public/locales/pt/components/icons.json new file mode 100644 index 0000000..71b767a --- /dev/null +++ b/web/public/locales/pt/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecione um ícone", + "search": { + "placeholder": "Procurar por um ícone…" + } + } +} diff --git a/web/public/locales/pt/components/input.json b/web/public/locales/pt/components/input.json new file mode 100644 index 0000000..9861b92 --- /dev/null +++ b/web/public/locales/pt/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Descarregar Vídeo", + "toast": { + "success": "O vídeo do seu item de análise começou a ser descarregado." + } + } + } +} diff --git a/web/public/locales/pt/components/player.json b/web/public/locales/pt/components/player.json new file mode 100644 index 0000000..741d37e --- /dev/null +++ b/web/public/locales/pt/components/player.json @@ -0,0 +1,51 @@ +{ + "noPreviewFound": "Nenhuma pré-visualização encontrada", + "noPreviewFoundFor": "Nenhuma pré-visualização encontrada para {{cameraName}}", + "submitFrigatePlus": { + "title": "Submeter esta imagem para Frigate+?", + "submit": "Submeter" + }, + "streamOffline": { + "title": "Transmissão Off-line", + "desc": "Nenhum quadro foi recebido na transmissão de detecção {{cameraName}}, verifique os logs de erro" + }, + "cameraDisabled": "A câmara está desativada", + "stats": { + "streamType": { + "title": "Tipo de transmissão:", + "short": "Tipo" + }, + "bandwidth": { + "title": "Largura de banda:", + "short": "Largura de banda" + }, + "latency": { + "value": "{{seconds}} segundos", + "short": { + "title": "Latência", + "value": "{{seconds}} seg" + }, + "title": "Latência:" + }, + "totalFrames": "Total de quadros:", + "droppedFrames": { + "title": "Imagens perdidas:", + "short": { + "title": "Perdida", + "value": "{{droppedFrames}} imagens" + } + }, + "decodedFrames": "Quadros decodificados:", + "droppedFrameRate": "Taxa de imagem perdida:" + }, + "noRecordingsFoundForThisTime": "Nenhuma gravação encontrada para este momento", + "livePlayerRequiredIOSVersion": "É necessário o iOS 17.1 ou superior para este tipo de transmissão ao vivo.", + "toast": { + "success": { + "submittedFrigatePlus": "Imagem submetida com sucesso para Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Não foi possível submeter a imagem para Frigate+" + } + } +} diff --git a/web/public/locales/pt/config/cameras.json b/web/public/locales/pt/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/pt/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/pt/config/global.json b/web/public/locales/pt/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/pt/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/pt/config/groups.json b/web/public/locales/pt/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/pt/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/pt/config/validation.json b/web/public/locales/pt/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/pt/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/pt/objects.json b/web/public/locales/pt/objects.json new file mode 100644 index 0000000..88762a7 --- /dev/null +++ b/web/public/locales/pt/objects.json @@ -0,0 +1,120 @@ +{ + "giraffe": "Girafa", + "cup": "Chávena", + "person": "Pessoa", + "stop_sign": "Sinal de Parar", + "sheep": "Ovelha", + "sandwich": "Sande", + "carrot": "Cenoura", + "dining_table": "Mesa de Jantar", + "motorcycle": "Mota", + "bicycle": "Bicicleta", + "street_sign": "Sinal de Rua", + "pizza": "Pizza", + "parking_meter": "Parcómetro", + "skateboard": "Skate", + "bottle": "Garrafa", + "car": "Carro", + "airplane": "Avião", + "bus": "Autocarro", + "train": "Comboio", + "boat": "Barco", + "traffic_light": "Semáforo", + "fire_hydrant": "Boca de Incêndio", + "bird": "Pássaro", + "cat": "Gato", + "bench": "Banco de Jardim", + "elephant": "Elefante", + "hat": "Chapéu", + "backpack": "Mochila", + "shoe": "Sapato", + "handbag": "Carteira", + "tie": "Gravata", + "suitcase": "Mala de Viagem", + "frisbee": "Disco de Frisbee", + "skis": "Esquis", + "kite": "Papagaio de Papel", + "baseball_bat": "Taco de Basebol", + "tennis_racket": "Raquete de Ténis", + "plate": "Prato", + "wine_glass": "Copo de Vinho", + "fork": "Garfo", + "spoon": "Colher", + "bowl": "Tijela", + "banana": "Banana", + "apple": "Maça", + "hot_dog": "Cachorro Quente", + "donut": "Donut", + "cake": "Bolo", + "chair": "Cadeira", + "potted_plant": "Planta em Vaso", + "mirror": "Espelho", + "desk": "Escrivaninha", + "toilet": "Casa de Banho", + "door": "Porta", + "baseball_glove": "Luva de Basebol", + "surfboard": "Prancha de Surf", + "broccoli": "Brócolos", + "snowboard": "Snowboard", + "dog": "Cão", + "bear": "Urso", + "eye_glasses": "Óculos", + "umbrella": "Guarda-chuva", + "horse": "Cavalo", + "bed": "Cama", + "cow": "Vaca", + "zebra": "Zebra", + "sports_ball": "Bola", + "knife": "Faca", + "orange": "Laranja", + "window": "Janela", + "clock": "Relógio", + "keyboard": "Teclado", + "animal": "Animal", + "bark": "Latido", + "goat": "Cabra", + "vehicle": "Veículo", + "scissors": "Tesouras", + "mouse": "Rato", + "teddy_bear": "Urso de Peluche", + "hair_dryer": "Secador de Cabelo", + "toothbrush": "Escova de Dentes", + "hair_brush": "Escova de Cabelo", + "squirrel": "Esquilo", + "couch": "Sofá", + "tv": "TV", + "laptop": "Portátil", + "remote": "Comando", + "cell_phone": "Telemóvel", + "microwave": "Microondas", + "oven": "Forno", + "toaster": "Torradeira", + "sink": "Banca", + "refrigerator": "Frigorífico", + "blender": "Liquidificador", + "book": "Livro", + "vase": "Vaso", + "deer": "Veado", + "fox": "Raposa", + "rabbit": "Coelho", + "raccoon": "Guaxinim", + "robot_lawnmower": "Robô de Cortar Relva", + "waste_bin": "Contentor do Lixo", + "on_demand": "On Demand", + "face": "Rosto", + "license_plate": "Matrícula", + "package": "Pacote", + "bbq_grill": "Churrasqueira", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/web/public/locales/pt/views/chat.json b/web/public/locales/pt/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/pt/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/pt/views/classificationModel.json b/web/public/locales/pt/views/classificationModel.json new file mode 100644 index 0000000..06403c1 --- /dev/null +++ b/web/public/locales/pt/views/classificationModel.json @@ -0,0 +1,58 @@ +{ + "button": { + "trainModel": "Treinar Modelo", + "addClassification": "Adicionar Classificação", + "deleteModels": "Apagar Modelos", + "editModel": "Editar Modelo", + "deleteClassificationAttempts": "Excluir imagens de classificação", + "renameCategory": "Renomear Classe", + "deleteCategory": "Excluir Classe", + "deleteImages": "Excluir imagens" + }, + "tooltip": { + "trainingInProgress": "Modelo está a ser treinado", + "noNewImages": "Não há novas imagens para treinar. Classifique mais imagens no dataset.", + "noChanges": "Nenhuma alteração foi feita no conjunto de dados desde o último treinamento.", + "modelNotReady": "O modelo não está pronto para treinamento" + }, + "details": { + "scoreInfo": "A pontuação representa a confiança média da classificação em todas as detecções deste objeto.", + "none": "Nenhum", + "unknown": "Desconhecido" + }, + "toast": { + "success": { + "deletedCategory_one": "Classe excluída", + "deletedCategory_many": "", + "deletedCategory_other": "", + "deletedImage_one": "Imagens excluídas", + "deletedImage_many": "", + "deletedImage_other": "", + "categorizedImage": "Imagem classificada com sucesso", + "trainedModel": "Modelo treinado com sucesso.", + "trainingModel": "Treinamento do modelo iniciado com sucesso.", + "updatedModel": "Configuração do modelo atualizada com sucesso", + "renamedCategory": "Classe renomeada com sucesso para {{name}}" + }, + "error": { + "deleteImageFailed": "Falha ao excluir: {{errorMessage}}", + "deleteCategoryFailed": "Falha ao excluir a classe: {{errorMessage}}", + "deleteModelFailed": "Falha ao excluir o modelo: {{errorMessage}}", + "categorizeFailed": "Falha ao categorizar a imagem: {{errorMessage}}", + "trainingFailed": "O treinamento do modelo falhou. Verifique os registros do Frigate para obter detalhes.", + "trainingFailedToStart": "Falha ao iniciar o treinamento do modelo: {{errorMessage}}", + "updateModelFailed": "Falha ao atualizar o modelo: {{errorMessage}}", + "renameCategoryFailed": "Falha ao renomear a classe: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Excluir Classe", + "desc": "Tem certeza de que deseja excluir a classe {{name}}? Isso excluirá permanentemente todas as imagens associadas e exigirá o treinamento do modelo novamente.", + "minClassesTitle": "Não é possível excluir a classe", + "minClassesDesc": "Um modelo de classificação deve ter pelo menos duas classes. Adicione outra classe antes de excluir esta." + }, + "documentTitle": "Modelos de Classificação – Frigate", + "description": { + "invalidName": "Nome inválido. Os nomes podem incluir apenas letras, números, espaços, apóstrofos, sublinhados e hífens." + } +} diff --git a/web/public/locales/pt/views/configEditor.json b/web/public/locales/pt/views/configEditor.json new file mode 100644 index 0000000..fb1d337 --- /dev/null +++ b/web/public/locales/pt/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Editor de Configuração", + "copyConfig": "Copiar Configuração", + "saveAndRestart": "Guardar e Reiniciar", + "saveOnly": "Guardar Apenas", + "toast": { + "success": { + "copyToClipboard": "Configuração copiada para a área de transferência." + }, + "error": { + "savingError": "Erro ao guardar a configuração" + } + }, + "documentTitle": "Frigate - Editor de Configuração", + "confirm": "Sair sem guardar?", + "safeConfigEditor": "Editor de Configuração (Modo de Segurança)", + "safeModeDescription": "O Frigate está no modo de segurança devido a um erro de validação da configuração." +} diff --git a/web/public/locales/pt/views/events.json b/web/public/locales/pt/views/events.json new file mode 100644 index 0000000..bb9b2e0 --- /dev/null +++ b/web/public/locales/pt/views/events.json @@ -0,0 +1,40 @@ +{ + "detections": "Deteções", + "motion": { + "label": "Movimento", + "only": "Apenas movimento" + }, + "allCameras": "Todas as Câmaras", + "empty": { + "motion": "Nenhum dado de movimento encontrado", + "alert": "Não há alertas para análise", + "detection": "Não há detecções para análise" + }, + "timeline": "Linha do tempo", + "events": { + "aria": "Selecionar eventos", + "label": "Eventos", + "noFoundForTimePeriod": "Nenhum evento encontrado para este período." + }, + "timeline.aria": "Selecione a linha do tempo", + "alerts": "Alertas", + "documentTitle": "Análise - Frigate", + "recordings": { + "documentTitle": "Frigate - Gravações" + }, + "calendarFilter": { + "last24Hours": "Últimas 24 horas" + }, + "markAsReviewed": "Marcar como analisado", + "markTheseItemsAsReviewed": "Marque esses itens como analisados", + "newReviewItems": { + "label": "Ver novos itens para analisar", + "button": "Novos itens para analisar" + }, + "camera": "Câmara", + "detected": "detetado", + "selected_one": "{{count}} selecionado", + "selected_other": "{{count}} selecionados", + "suspiciousActivity": "Atividade Suspeita", + "threateningActivity": "Atividade Ameaçadora" +} diff --git a/web/public/locales/pt/views/explore.json b/web/public/locales/pt/views/explore.json new file mode 100644 index 0000000..7215081 --- /dev/null +++ b/web/public/locales/pt/views/explore.json @@ -0,0 +1,228 @@ +{ + "generativeAI": "IA Generativa", + "exploreIsUnavailable": { + "embeddingsReindexing": { + "startingUp": "A iniciar…", + "estimatedTime": "Tempo restante estimado:", + "finishingShortly": "Terminando em breve", + "step": { + "thumbnailsEmbedded": "Miniaturas incorporadas: ", + "descriptionsEmbedded": "Descrições incorporadas: ", + "trackedObjectsProcessed": "Objetos rastreados processados: " + }, + "context": "O explorar pode ser usado depois que as incorporações de objetos rastreados terminarem de ser reindexadas." + }, + "downloadingModels": { + "setup": { + "visionModel": "Modelo de visão", + "textModel": "Modelo de texto", + "textTokenizer": "Tokenizador de texto", + "visionModelFeatureExtractor": "Extrator de funcionalidade de modelo de visão" + }, + "context": "O Frigate está a transferir os modelos de incorporação necessários para suportar a funcionalidade de \"Procura Semântica\". Isto pode levar vários minutos, dependendo da velocidade da sua ligação de rede.", + "tips": { + "context": "Talvez queira reindexar as incorporações dos seus objetos rastreados depois de os modelos serem transferidos.", + "documentation": "Leia a documentação" + }, + "error": "Ocorreu um erro. Verifique os registos do Frigate." + }, + "title": "Explorar não está disponível" + }, + "details": { + "timestamp": "Carimbo de hora", + "item": { + "title": "Analisar detalhes do item", + "desc": "Analisar detalhes do item", + "tips": { + "hasMissingObjects": "Ajuste sua configuração se você quiser que o Frigate guarde os objetos rastreados para os seguintes rótulos: {{objects}}", + "mismatch_one": "{{count}} objeto indisponível foi detectado e incluído neste item de análise. Esses objetos não qualificaram como alerta ou detecção ou já foram limpos/excluídos.", + "mismatch_many": "{{count}} objetos indisponíveis foram detectados e incluídos neste item de análise. Esses objetos não qualificaram como alerta ou detecção ou já foram limpos/excluídos.", + "mismatch_other": "{{count}} objetos indisponíveis foram detectados e incluídos neste item de análise. Esses objetos não qualificaram como alerta ou detecção ou já foram limpos/excluídos." + }, + "toast": { + "success": { + "regenerate": "Uma nova descrição foi solicitada pelo {{provider}}. Dependendo da velocidade do seu fornecedor, a nova descrição pode levar algum tempo para ser regenerada.", + "updatedSublabel": "Sub-rotulo atualizado com sucesso.", + "updatedLPR": "Matrícula atualizada com sucesso.", + "audioTranscription": "Transcrição de áudio solicitada com sucesso." + }, + "error": { + "regenerate": "Falha ao chamar {{provider}} para uma nova descrição: {{errorMessage}}", + "updatedSublabelFailed": "Falha ao atualizar o sub-rotulo: {{errorMessage}}", + "updatedLPRFailed": "Falha ao atualizar a matrícula: {{errorMessage}}", + "audioTranscription": "Falha ao solicitar transcrição de áudio: {{errorMessage}}" + } + }, + "button": { + "share": "Compartilhe este item para análise", + "viewInExplore": "Ver no Explorar" + } + }, + "zones": "Zonas", + "description": { + "label": "Descrição", + "aiTips": "O Frigate não solicitará uma descrição do seu fornecedor de IA Generativa até que o ciclo de vida do objeto rastreado tenha terminado.", + "placeholder": "Descrição do objeto rastreado" + }, + "camera": "Câmera", + "snapshotScore": { + "label": "Pontuação da captura" + }, + "topScore": { + "label": "Maior pontuação", + "info": "A maior pontuação é a maior pontuação mediana para o objeto rastreado, portanto, isso pode diferir da pontuação exibida na miniatura do resultado da pesquisa." + }, + "button": { + "findSimilar": "Encontrar similar", + "regenerate": { + "title": "Regenerar", + "label": "Regenerar descrição do objeto rastreado" + } + }, + "label": "Rótulo", + "editSubLabel": { + "title": "Editar sub-rotulo", + "desc": "Digite um novo sub-rotulo para este {{label}}", + "descNoLabel": "Digite um novo sub-rotulo para este objeto rastreado" + }, + "editLPR": { + "title": "Editar matrícula", + "desc": "Digite um novo valor da matrícula para este {{label}}", + "descNoLabel": "Digite um novo valor da matrícula para este objeto rastreado" + }, + "recognizedLicensePlate": "Matrícula reconhecida", + "estimatedSpeed": "Velocidade estimada", + "objects": "Objetos", + "expandRegenerationMenu": "Expandir menu de regeneração", + "regenerateFromSnapshot": "Regenerar a partir da captura", + "regenerateFromThumbnails": "Regenerar a partir das miniaturas", + "tips": { + "descriptionSaved": "Descrição salva com sucesso", + "saveDescriptionFailed": "Falha ao atualizar a descrição: {{errorMessage}}" + }, + "score": { + "label": "Classificação" + } + }, + "documentTitle": "Frigate - Explorar", + "trackedObjectDetails": "Detalhes do objeto rastreado", + "type": { + "details": "detalhes", + "video": "vídeo", + "object_lifecycle": "ciclo de vida do objeto", + "snapshot": "captura de ecrã" + }, + "objectLifecycle": { + "title": "Ciclo de vida do objeto", + "lifecycleItemDesc": { + "attribute": { + "other": "{{label}} reconhecido como {{attribute}}", + "faceOrLicense_plate": "{{attribute}} detetado por {{label}}" + }, + "gone": "{{label}} saiu", + "heard": "{{label}} ouvido", + "visible": "{{label}} detectado", + "external": "{{label}} detetado", + "entered_zone": "{{label}} entrou em {{zones}}", + "active": "{{label}} se tornou ativo", + "stationary": "{{label}} se tornou estacionário", + "header": { + "zones": "Zonas", + "ratio": "Proporção", + "area": "Área" + } + }, + "annotationSettings": { + "title": "Definições de Anotação", + "offset": { + "documentation": "Leia a documentação ", + "desc": "Esses dados vêm do feed de detecção da sua câmara, mas são sobrepostos nas imagens do feed de gravação. É improvável que os dois streams estejam perfeitamente sincronizados. Como resultado, a caixa delimitadora e o vídeo não se alinharão perfeitamente. No entanto, o campo annotation_offset pode ser usado para ajustar isso.", + "tips": "DICA: Imagine que há um clipe de evento com uma pessoa a andar da esquerda para a direita. Se a caixa delimitadora da linha do tempo do evento estiver consistentemente à esquerda da pessoa, o valor deve ser diminuído. Da mesma forma, se uma pessoa estiver andando da esquerda para a direita e a caixa delimitadora estiver consistentemente à frente da pessoa, o valor deve ser aumentado.", + "label": "Deslocamento de Anotação", + "millisecondsToOffset": "Milissegundos para deslocar as anotações de detecção. Padrão: 0", + "toast": { + "success": "O deslocamento de anotação para {{camera}} foi salvo no arquivo de configuração. Reinicie o Frigate para aplicar as alterações." + } + }, + "showAllZones": { + "title": "Mostrar Todas as Zonas", + "desc": "Mostrar sempre as zonas nas imagens onde os objetos entraram numa zona." + } + }, + "carousel": { + "previous": "Slide anterior", + "next": "Próximo slide" + }, + "noImageFound": "Nenhuma imagem encontrada para este carimbo de data/hora.", + "createObjectMask": "Criar Máscara de Objeto", + "adjustAnnotationSettings": "Ajustar definições de anotação", + "autoTrackingTips": "As posições da caixa delimitadora serão imprecisas para as câmaras com rastreamento automático.", + "scrollViewTips": "Deslize para ver os momentos significativos do ciclo de vida deste objeto.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Ponto Rastreado" + }, + "itemMenu": { + "downloadSnapshot": { + "aria": "Descarregar captura", + "label": "Descarregar captura" + }, + "viewObjectLifecycle": { + "label": "Ver ciclo de vida do objeto", + "aria": "Mostrar o ciclo de vida do objeto" + }, + "viewInHistory": { + "label": "Ver no Histórico", + "aria": "Ver no Histórico" + }, + "downloadVideo": { + "label": "Descarregar vídeo", + "aria": "Descarregar vídeo" + }, + "findSimilar": { + "label": "Encontrar similar", + "aria": "Encontrar objetos rastreados similares" + }, + "submitToPlus": { + "label": "Enviar para o Frigate+", + "aria": "Enviar para o Frigate Plus" + }, + "deleteTrackedObject": { + "label": "Excluir este objeto rastreado" + }, + "addTrigger": { + "label": "Adicionar gatilho", + "aria": "Adicione um gatilho para este objeto rastreado" + }, + "audioTranscription": { + "label": "Transcrever", + "aria": "Solicitar transcrição de áudio" + } + }, + "searchResult": { + "tooltip": "Encontrado {{type}} com {{confidence}}% de confiança", + "deleteTrackedObject": { + "toast": { + "success": "Objeto rastreado excluído com sucesso.", + "error": "Falha ao excluir objeto rastreado: {{errorMessage}}" + } + } + }, + "dialog": { + "confirmDelete": { + "desc": "Excluir este objeto rastreado remove a captura de imagem, quaisquer incorporações salvas e todas as entradas associadas ao ciclo de vida do objeto. As gravações desse objeto rastreado na visualização do Histórico NÃO serão excluídas.

    Tem certeza de que deseja continuar?", + "title": "Confirmar exclusão" + } + }, + "fetchingTrackedObjectsFailed": "Erro ao buscar objetos rastreados: {{errorMessage}}", + "noTrackedObjects": "Nenhum objeto rastreado encontrado", + "trackedObjectsCount_one": "{{count}} objeto rastreado ", + "trackedObjectsCount_many": "{{count}} objetos rastreados ", + "trackedObjectsCount_other": "", + "exploreMore": "Explora mais objetos {{label}}", + "aiAnalysis": { + "title": "Análise IA" + }, + "concerns": { + "label": "Preocupações" + } +} diff --git a/web/public/locales/pt/views/exports.json b/web/public/locales/pt/views/exports.json new file mode 100644 index 0000000..d8cc40f --- /dev/null +++ b/web/public/locales/pt/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Exportar - Frigate", + "search": "Pesquisar", + "noExports": "Nenhuma exportação encontrada", + "deleteExport": "Excluir exportação", + "editExport": { + "title": "Renomear exportação", + "desc": "Digite um novo nome para esta exportação.", + "saveExport": "Salvar exportação" + }, + "toast": { + "error": { + "renameExportFailed": "Falha ao renomear exportação: {{errorMessage}}" + } + }, + "deleteExport.desc": "Tem a certeza de que deseja excluir {{exportName}}?", + "tooltip": { + "shareExport": "Partilhar exportação" + }, + "headings": { + "uncategorizedExports": "Exportações sem categoria" + } +} diff --git a/web/public/locales/pt/views/faceLibrary.json b/web/public/locales/pt/views/faceLibrary.json new file mode 100644 index 0000000..9b19549 --- /dev/null +++ b/web/public/locales/pt/views/faceLibrary.json @@ -0,0 +1,104 @@ +{ + "description": { + "placeholder": "Introduza um nome para esta coleção", + "addFace": "Veja como adicionar uma nova coleção à biblioteca de rostos.", + "invalidName": "Nome inválido. Os nomes podem incluir apenas letras, números, espaços, apóstrofos, sublinhados e hífens.", + "nameCannotContainHash": "O nome não pode conter #." + }, + "details": { + "person": "Pessoa", + "face": "Detalhes do rosto", + "faceDesc": "Detalhes do objeto encontrado que gerou esta cara", + "timestamp": "Carimbo de hora", + "confidence": "Confiança", + "scoreInfo": "A pontuação da subetiqueta é a pontuação ponderada de todas as confianças de rostos reconhecidos, portanto, ela pode ser diferente da pontuação exibida na captura de imagem.", + "subLabelScore": "Pontuação da Subetiqueta", + "unknown": "Desconhecido" + }, + "documentTitle": "Biblioteca de rostos - Frigate", + "uploadFaceImage": { + "title": "Carregar imagem do rosto", + "desc": "Carregue uma imagem para procurar rostos e incluir em {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Criar coleção", + "desc": "Criar uma nova coleção", + "new": "Criar novo rosto", + "nextSteps": "Para construir uma base sólida:
  • Use a aba Treinar para selecionar e treinar em imagens para cada pessoa detectada.
  • Concentre-se em imagens diretas para obter melhores resultados; evite imagens de treino que capturem rostos em ângulo.
  • " + }, + "train": { + "aria": "Selecionar treino", + "title": "Reconhecimentos Recentes", + "empty": "Não há tentativas recentes de reconhecimento facial" + }, + "selectItem": "Selecionar {{item}}", + "selectFace": "Selecionar rosto", + "deleteFaceLibrary": { + "title": "Excluir nome", + "desc": "Tem certeza de que deseja excluir a coleção {{name}}? Isso excluirá permanentemente todos os rostos associados." + }, + "button": { + "addFace": "Adicionar rosto", + "uploadImage": "Carregar imagem", + "deleteFaceAttempts": "Excluir rostos", + "reprocessFace": "Reprocessar Rosto", + "renameFace": "Renomear rosto", + "deleteFace": "Excluir rosto" + }, + "imageEntry": { + "validation": { + "selectImage": "Selecione um arquivo de imagem." + }, + "dropActive": "Solte a imagem aqui…", + "maxSize": "Tamanho máximo: {{size}}MB", + "dropInstructions": "Arraste e solte uma imagem aqui ou clique para selecionar" + }, + "trainFace": "Treinar rosto", + "toast": { + "success": { + "updatedFaceScore": "Pontuação facial atualizada com sucesso para {{name}} ({{score}}).", + "trainedFace": "Rosto treinado com sucesso.", + "deletedFace_one": "{{count}} rosto excluído com sucesso.", + "deletedFace_many": "{{count}} rostos excluídos com sucesso.", + "deletedFace_other": "{{count}} rostos excluídos com sucesso.", + "deletedName_one": "{{count}} rosto foi excluído com sucesso.", + "deletedName_many": "{{count}} rostos foram excluídos com sucesso.", + "deletedName_other": "{{count}} rostos foram excluídos com sucesso.", + "uploadedImage": "Imagem carregada com sucesso.", + "addFaceLibrary": "{{name}} foi adicionado com sucesso à biblioteca de rostos!", + "renamedFace": "Rosto renomeado com sucesso para {{name}}" + }, + "error": { + "uploadingImageFailed": "Falha ao carregar a imagem: {{errorMessage}}", + "deleteFaceFailed": "Falha ao excluir: {{errorMessage}}", + "deleteNameFailed": "Falha ao excluir nome: {{errorMessage}}", + "addFaceLibraryFailed": "Falhou ao definir nome do rosto: {{errorMessage}}", + "trainFailed": "Falhou ao treinar: {{errorMessage}}", + "updateFaceScoreFailed": "Falhou ao atualizar pontuação do rosto: {{errorMessage}}", + "renameFaceFailed": "Falha ao renomear o rosto: {{errorMessage}}" + } + }, + "readTheDocs": "Leia a documentação", + "trainFaceAs": "Treinar rosto como:", + "steps": { + "faceName": "Digite o Nome do Rosto", + "uploadFace": "Carregar imagem do rosto", + "nextSteps": "Próximos passos", + "description": { + "uploadFace": "Carregue uma imagem de {{name}} que mostre a cara de frente. A imagem não precisa de ser cortada para mostrar apenas a cara." + } + }, + "renameFace": { + "desc": "Entre com um novo nome para {{name}}", + "title": "Renomear rosto" + }, + "collections": "Coleções", + "deleteFaceAttempts": { + "title": "Excluir rostos", + "desc_one": "Tem a certeza que pretende apagar {{count}} cara? Esta ação não pode ser revertida.", + "desc_many": "Tem a certeza que pretende apagar {{count}} caras? Esta ação não pode ser revertida.", + "desc_other": "Tem a certeza que pretende apagar {{count}} caras? Esta ação não pode ser revertida." + }, + "nofaces": "Não tem caras disponíveis", + "pixels": "{{area}}px" +} diff --git a/web/public/locales/pt/views/live.json b/web/public/locales/pt/views/live.json new file mode 100644 index 0000000..770028a --- /dev/null +++ b/web/public/locales/pt/views/live.json @@ -0,0 +1,171 @@ +{ + "documentTitle": "Ao vivo - Frigate", + "documentTitle.withCamera": "{{camera}} - Ao vivo - Frigate", + "twoWayTalk": { + "disable": "Desativar conversa bidirecional", + "enable": "Habilitar conversa bidirecional" + }, + "cameraAudio": { + "enable": "Habilitar áudio da câmara", + "disable": "Desativar áudio da câmara" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Clique no quadro para centralizar a câmara", + "enable": "Habilitar clique para mover", + "disable": "Desativar clique para mover" + }, + "down": { + "label": "Mover a câmara PTZ para baixo" + }, + "up": { + "label": "Mover a câmara PTZ para cima" + }, + "left": { + "label": "Mover a câmara PTZ para a esquerda" + }, + "right": { + "label": "Mover a câmara PTZ para a direita" + } + }, + "zoom": { + "in": { + "label": "Aumentar zoom da câmara PTZ" + }, + "out": { + "label": "Diminuir zoom da câmara PTZ" + } + }, + "presets": "Predefinições de câmara PTZ", + "frame": { + "center": { + "label": "Clique no quadro para centralizar a câmara PTZ" + } + }, + "focus": { + "in": { + "label": "Em foco da câmera PTZ" + }, + "out": { + "label": "Fora foco da câmera PTZ em" + } + } + }, + "lowBandwidthMode": "Modo de baixa largura de banda", + "camera": { + "enable": "Habilitar câmara", + "disable": "Desativar câmara" + }, + "muteCameras": { + "disable": "Ativar áudio de todas as câmaras", + "enable": "Silenciar todas as câmaras" + }, + "detect": { + "enable": "Habilitar detecção", + "disable": "Desativar detecção" + }, + "snapshots": { + "enable": "Habilitar snapshots", + "disable": "Desativar snapshots" + }, + "audioDetect": { + "enable": "Habilitar detecção de áudio", + "disable": "Desativar detecção de áudio" + }, + "autotracking": { + "enable": "Habilitar rastreamento automático", + "disable": "Desativar rastreamento automático" + }, + "streamStats": { + "enable": "Mostrar estatísticas de transmissão", + "disable": "Ocultar estatísticas de transmissão" + }, + "manualRecording": { + "tips": "Inicie um evento manual com base nas configurações de retenção de gravação desta câmara.", + "playInBackground": { + "label": "Reproduzir em segundo plano", + "desc": "Habilite esta opção para continuar a transmissão quando o player estiver oculto." + }, + "showStats": { + "label": "Mostrar estatísticas", + "desc": "Habilite esta opção para mostrar estatísticas de transmissão como uma sobreposição no feed da câmara." + }, + "start": "Iniciar gravação on-demand", + "recordDisabledTips": "Como a gravação está desabilitada ou restrita na configuração desta câmara, apenas um snapshot será salvo.", + "end": "Encerrar gravação on-demand", + "ended": "Fim da gravação manual on-demand.", + "failedToEnd": "Falha ao finalizar a gravação manual on-demand.", + "failedToStart": "Falha ao iniciar a gravação manual on-demand.", + "title": "Gravação on-demand", + "started": "Iniciou a gravação manual on-demand.", + "debugView": "Exibição de depuração" + }, + "streamingSettings": "Configurações de transmissão", + "notifications": "Notificações", + "suspend": { + "forTime": "Suspender por: " + }, + "stream": { + "title": "Transmissão", + "audio": { + "tips": { + "title": "O áudio deve ser emitido pela sua câmara e configurado no go2rtc para esta transmissão.", + "documentation": "Leia a documentação " + }, + "available": "O áudio está disponível para esta transmissão", + "unavailable": "O áudio não está disponível para esta transmissão" + }, + "twoWayTalk": { + "tips.documentation": "Leia a documentação ", + "unavailable": "Conversa bidirecional não está disponível para esta transmissão", + "tips": "Seu dispositivo deve suportar o recurso e o WebRTC deve ser configurado para conversa bidirecional.", + "available": "Conversa bidirecional está disponível para esta transmissão" + }, + "lowBandwidth": { + "tips": "A visualização ao vivo está em modo de baixa largura de banda devido a erros de buffer ou transmissão.", + "resetStream": "Reiniciar transmissão" + }, + "playInBackground": { + "label": "Reproduzir em segundo plano", + "tips": "Habilite esta opção para continuar a transmissão quando o player estiver oculto." + } + }, + "cameraSettings": { + "title": "{{camera}} configurações", + "cameraEnabled": "Câmara habilitada", + "objectDetection": "Detecção de objeto", + "recording": "Gravando", + "audioDetection": "Detecção de áudio", + "autotracking": "Rastreamento automático", + "snapshots": "Snapshots", + "transcription": "Transcrição de áudio" + }, + "effectiveRetainMode": { + "modes": { + "active_objects": "Objetos ativos", + "motion": "Movimento", + "all": "Todos" + }, + "notAllTips": "Sua configuração de retenção de gravação {{source}} está definida como modo: {{effectiveRetainMode}}, portanto, esta gravação on-demand manterá apenas segmentos com {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Editar layout", + "group": { + "label": "Editar grupo de câmaras" + }, + "exitEdit": "Sair da edição" + }, + "audio": "Áudio", + "recording": { + "enable": "Habilitar gravação", + "disable": "Desativar gravação" + }, + "history": { + "label": "Mostrar filmagens históricas" + }, + "transcription": { + "enable": "Habilitar transcrição de áudio ao vivo", + "disable": "Desabilitar transcrição de áudio ao vivo" + } +} diff --git a/web/public/locales/pt/views/motionSearch.json b/web/public/locales/pt/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/pt/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/pt/views/recording.json b/web/public/locales/pt/views/recording.json new file mode 100644 index 0000000..a8b6e39 --- /dev/null +++ b/web/public/locales/pt/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Exportar", + "calendar": "Calendário", + "filter": "Filtro", + "filters": "Filtros", + "toast": { + "error": { + "endTimeMustAfterStartTime": "O horário de término deve ser posterior ao horário de início", + "noValidTimeSelected": "Nenhum intervalo de tempo válido selecionado" + } + } +} diff --git a/web/public/locales/pt/views/replay.json b/web/public/locales/pt/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/pt/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/pt/views/search.json b/web/public/locales/pt/views/search.json new file mode 100644 index 0000000..4443f27 --- /dev/null +++ b/web/public/locales/pt/views/search.json @@ -0,0 +1,74 @@ +{ + "search": "Pesquisar", + "savedSearches": "Pesquisas salvas", + "searchFor": "Pesquisar por {{inputValue}}", + "button": { + "save": "Salvar pesquisa", + "delete": "Excluir pesquisa salva", + "filterInformation": "Informação do filtro", + "filterActive": "Filtros ativos", + "clear": "Limpar pesquisa" + }, + "trackedObjectId": "ID do objeto rastreado", + "filter": { + "label": { + "sub_labels": "Sub etiquetas", + "zones": "Zonas", + "cameras": "Câmaras", + "labels": "Etiquetas", + "search_type": "Tipo de pesquisa", + "time_range": "Intervalo de tempo", + "before": "Antes", + "after": "Depois", + "min_score": "Pontuação mínima", + "max_score": "Pontuação máxima", + "min_speed": "Velocidade mínima", + "max_speed": "Velocidade máxima", + "recognized_license_plate": "Matrícula reconhecida", + "has_clip": "Tem Clipe", + "has_snapshot": "Tem Captura de Imagem" + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Descrição" + }, + "header": { + "noFilters": "Filtros", + "activeFilters": "Filtros ativos", + "currentFilterType": "Valores de filtro" + }, + "tips": { + "desc": { + "text": "Os filtros ajudam você a restringir os resultados da sua pesquisa. Veja como usá-los no campo de entrada:", + "example": "Exemplo: cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "step": "
    • Digite um nome de filtro seguido de dois pontos (ex: \"cameras:\").
    • Selecione um valor entre as sugestões ou digite o seu próprio.
    • Use múltiplos filtros adicionando-os um após o outro com um espaço entre eles.
    • Filtros de data (before: e after:) usam o formato {{DateFormat}}.
    • O filtro de intervalo de tempo usa o formato {{exampleTime}}.
    • Remova filtros clicando no 'x' ao lado deles.
    ", + "step1": "Digite um nome para o filtro seguido de dois pontos (exemplo \"camaras:\").", + "step2": "Selecione um valor entre as sugestões ou digite o seu próprio.", + "step3": "Use vários filtros adicionando-os um após o outro com um espaço entre eles.", + "step5": "O filtro de intervalo de tempo usa o formato {{exampleTime}}.", + "step6": "Remova os filtros clicando no 'x' ao lado deles.", + "step4": "Os filtros de data (antes: e depois:) utilizam o formato {{DateFormat}}.", + "exampleLabel": "Exemplo:" + }, + "title": "Como usar filtros de texto" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "A data 'antes' deve ser posterior à data 'depois'.", + "minScoreMustBeLessOrEqualMaxScore": "O 'min_score' deve ser inferior ou igual ao 'max_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "O 'min_speed' deve ser inferior ou igual ao 'max_speed'.", + "afterDatebeEarlierBefore": "A data \"depois\" deve ser anterior à data \"antes\".", + "maxScoreMustBeGreaterOrEqualMinScore": "O 'max_score' deve ser maior ou igual ao 'min_score'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "O 'max_speed' deve ser maior ou igual ao 'min_speed'." + } + } + }, + "placeholder": { + "search": "Pesquisar…" + }, + "similaritySearch": { + "title": "Pesquisa de similaridade", + "active": "Busca de semelhança ativa", + "clear": "Pesquisa de semelhança clara" + } +} diff --git a/web/public/locales/pt/views/settings.json b/web/public/locales/pt/views/settings.json new file mode 100644 index 0000000..e5229eb --- /dev/null +++ b/web/public/locales/pt/views/settings.json @@ -0,0 +1,987 @@ +{ + "documentTitle": { + "camera": "Configurações da câmera - Frigate", + "classification": "Configurações de classificação - Frigate", + "masksAndZones": "Editor de máscara e zona - Frigate", + "motionTuner": "Ajuste de movimento - Frigate", + "object": "Depuração - Frigate", + "authentication": "Configurações de autenticação - Frigate", + "general": "Configurações gerais - Frigate", + "frigatePlus": "Configurações do Frigate+ - Frigate", + "default": "Configurações - Frigate", + "notifications": "Configuração de Notificações - Frigate", + "enrichments": "Configurações Avançadas - Frigate", + "cameraManagement": "Gerir Câmaras - Frigate", + "cameraReview": "Configurações de Revisão de Câmara - Frigate" + }, + "menu": { + "ui": "UI", + "masksAndZones": "Máscaras / Zonas", + "cameras": "Configurações da câmara", + "classification": "Classificação", + "motionTuner": "Ajuste de movimento", + "debug": "Depuração", + "users": "Utilizadores", + "notifications": "Notificações", + "frigateplus": "Frigate+", + "enrichments": "Avançado", + "triggers": "Gatilhos", + "cameraManagement": "Gestão", + "cameraReview": "Rever", + "roles": "Papéis", + "profiles": "Perfis" + }, + "dialog": { + "unsavedChanges": { + "title": "Você tem alterações não salvas.", + "desc": "Deseja salvar suas alterações antes de continuar?" + } + }, + "cameraSetting": { + "camera": "Câmara", + "noCamera": "Sem câmara" + }, + "general": { + "title": "Configurações gerais", + "liveDashboard": { + "title": "Painel ao vivo", + "automaticLiveView": { + "label": "Visualização ao vivo automática", + "desc": "Alternar automaticamente para a visualização ao vivo de uma câmara quando uma atividade for detectada. Desativar esta opção faz com que as imagens estáticas das câmaras no painel Ao Vivo sejam atualizadas apenas uma vez por minuto." + }, + "playAlertVideos": { + "label": "Reproduzir vídeos de alerta", + "desc": "Por padrão, alertas recentes no painel ao vivo são reproduzidos como vídeos curtos em loop. Desative esta opção para mostrar apenas uma imagem estática dos alertas recentes neste dispositivo/navegador." + } + }, + "calendar": { + "title": "Calendário", + "firstWeekday": { + "label": "Primeiro dia da semana", + "sunday": "Domingo", + "monday": "Segunda-feira", + "desc": "O dia em que as semanas do calendário de análise começam." + } + }, + "storedLayouts": { + "title": "Layouts armazenados", + "desc": "O layout das câmaras em um grupo de câmaras pode ser arrastado/redimensionado. As posições são armazenadas no armazenamento local do seu navegador.", + "clearAll": "Limpar todos os layouts" + }, + "recordingsViewer": { + "defaultPlaybackRate": { + "label": "Taxa de reprodução padrão", + "desc": "Taxa de reprodução padrão para a reprodução das gravações." + }, + "title": "Visualizador de gravações" + }, + "cameraGroupStreaming": { + "desc": "As configurações de transmissão de cada grupo de câmaras são armazenadas no armazenamento local do seu navegador.", + "title": "Configurações de transmissão do grupo de câmaras", + "clearAll": "Limpar todas as configurações de transmissão" + }, + "toast": { + "success": { + "clearStreamingSettings": "Configurações de transmissão para todos os grupos de câmaras limpas.", + "clearStoredLayout": "Limpo layout armazenado para {{cameraName}}" + }, + "error": { + "clearStoredLayoutFailed": "Falha ao limpar o layout armazenado: {{errorMessage}}", + "clearStreamingSettingsFailed": "Falha ao limpar as configurações de transmissão: {{errorMessage}}" + } + } + }, + "classification": { + "faceRecognition": { + "modelSize": { + "label": "Tamanho do modelo", + "small": { + "title": "pequeno", + "desc": "Usar pequeno utiliza um modelo de incorporação facial FaceNet que é eficiente na maioria dos CPUs." + }, + "large": { + "title": "grande", + "desc": "Usar grande utiliza um modelo de incorporação facial ArcFace e será executado automaticamente na GPU, se aplicável." + }, + "desc": "O tamanho do modelo utilizado para o reconhecimento facial." + }, + "readTheDocumentation": "Leia a documentação", + "title": "Reconhecimento facial", + "desc": "O reconhecimento facial permite que pessoas sejam atribuídas a nomes e, quando o rosto delas for reconhecido, o Frigate atribuirá o nome da pessoa como um sub-rótulo. Essa informação é incluída na interface do usuário, filtros e também nas notificações." + }, + "licensePlateRecognition": { + "readTheDocumentation": "Leia a documentação", + "title": "Reconhecimento de placas de veículo", + "desc": "O Frigate pode reconhecer placas de veículos e adicionar automaticamente os caracteres detectados ao campo placa_de_veículo_reconhecida ou um nome conhecido como sub_rótulo para objetos do tipo carro. Um caso de uso comum pode ser ler as placas de veículos que entram em uma garagem ou os carros que passam por uma rua." + }, + "semanticSearch": { + "readTheDocumentation": "Leia a documentação", + "title": "Pesquisa semântica", + "reindexNow": { + "label": "Reindexar agora", + "desc": "A reindexação irá regenerar as incorporações de todos os objetos rastreados. Esse processo é executado em segundo plano e pode sobrecarregar seu CPU e levar um bom tempo, dependendo do número de objetos rastreados que você possui.", + "confirmTitle": "Confirmar reindexação", + "confirmDesc": "Você tem certeza de que deseja reindexar todos as incorporações dos objetos rastreados? Esse processo será executado em segundo plano, mas pode sobrecarregar seu CPU e levar um bom tempo. Você pode acompanhar o progresso na página Explorar.", + "confirmButton": "Reindexar", + "alreadyInProgress": "A reindexação já está em andamento.", + "error": "Falha ao iniciar a reindexação: {{errorMessage}}", + "success": "Reindexação iniciada com sucesso." + }, + "modelSize": { + "label": "Tamanho do modelo", + "desc": "O tamanho do modelo utilizado para as incorporações de pesquisa semântica.", + "small": { + "title": "pequeno", + "desc": "Usar pequeno utiliza uma versão quantizada do modelo, que usa menos RAM e executa mais rápido no CPU, com uma diferença insignificante na qualidade da incorporação." + }, + "large": { + "title": "grande", + "desc": "Usar grande utiliza o modelo completo do Jina e será executado automaticamente na GPU, se aplicável." + } + }, + "desc": "A Pesquisa Semântica no Frigate permite que você encontre objetos rastreados dentro dos itens de revisão usando a imagem em si, uma descrição de texto definida pelo usuário ou uma descrição gerada automaticamente." + }, + "title": "Configurações de classificação", + "toast": { + "success": "As configurações de classificação foram salvas. Reinicie o Frigate para aplicar as alterações.", + "error": "Falha ao salvar as alterações de configuração: {{errorMessage}}" + }, + "birdClassification": { + "title": "Classificação de Pássaros", + "desc": "A classificação de aves/pássaros identifica aves conhecidas usando um modelo Tensorflow quantizado. Quando uma ave/ pássaro conhecida(o) for reconhecida(o), o seu nome comum será adicionado como um sub_rótulo. Estas informações estão incluídas na interface do utilizador, nos filtros e também nas notificações." + }, + "restart_required": "Reinício necessário (configurações de classificação alteradas)", + "unsavedChanges": "Alterações nas configurações de Classificação não estão salvas" + }, + "notification": { + "globalSettings": { + "title": "Configurações globais", + "desc": "Suspenda temporariamente as notificações para câmaras específicas em todos os dispositivos registados." + }, + "notificationSettings": { + "documentation": "Leia a documentação", + "title": "Configurações de notificação", + "desc": "O Frigate pode enviar notificações push para o seu dispositivo quando está a ser executado no browser ou instalado como um PWA." + }, + "notificationUnavailable": { + "documentation": "Leia a documentação", + "title": "Notificações indisponíveis", + "desc": "As notificações push da Web exigem um contexto seguro (https://…). Esta é uma limitação do navegador. Acesse o Frigate com segurança para usar as notificações." + }, + "cameras": { + "title": "Câmaras", + "noCameras": "Nenhuma câmara disponível", + "desc": "Selecione para que câmaras as notificações serão ativadas." + }, + "deviceSpecific": "Configurações específicas do dispositivo", + "registerDevice": "Registe este dispositivo", + "email": { + "placeholder": "por exemplo: exemplo@email.com", + "desc": "É necessário um e-mail válido que será utilizado para o notificar caso haja algum problema com o serviço push.", + "title": "E-mail" + }, + "title": "Notificações", + "unregisterDevice": "Cancelar o registro deste dispositivo", + "suspendTime": { + "5minutes": "Suspender por 5 minutos", + "1hour": "Suspender por 1 hora", + "12hours": "Suspender por 12 horas", + "untilRestart": "Suspender até reiniciar", + "10minutes": "Suspender por 10 minutos", + "suspend": "Suspender", + "30minutes": "Suspender por 30 minutos", + "24hours": "Suspender por 24 horas" + }, + "cancelSuspension": "Cancelar Suspensão", + "toast": { + "success": { + "registered": "Registo para notificações concluído com sucesso. É necessário reiniciar o Frigate antes que qualquer notificação (incluindo uma notificação de teste) possa ser enviada.", + "settingSaved": "As configurações de notificação foram salvas." + }, + "error": { + "registerFailed": "Falha ao guardar o registo das notificações." + } + }, + "sendTestNotification": "Envie uma notificação de teste", + "active": "Notificações ativas", + "suspended": "Notificações suspensas {{time}}", + "unsavedRegistrations": "Registros de notificação não salvos", + "unsavedChanges": "Registros de notificação não salvos" + }, + "frigatePlus": { + "snapshotConfig": { + "documentation": "Leia a documentação", + "table": { + "snapshots": "Snapshots", + "camera": "Câmara", + "cleanCopySnapshots": "clean_copy Snapshots" + }, + "title": "Configuração de snapshots", + "desc": "O envio para o Frigate+ requer que tanto os snapshots quanto os snapshots clean_copy estejam habilitados na sua configuração.", + "cleanCopyWarning": "Algumas câmaras têm snapshots habilitados, mas a cópia limpa está desabilitada. É necessário habilitar clean_copy na sua configuração de snapshot para poder enviar imagens dessas câmaras para o Frigate+." + }, + "toast": { + "success": "As definições do Frigate+ foram guardadas. Reinicie o Frigate para aplicar as alterações.", + "error": "Falha ao guardar alterações de configuração: {{errorMessage}}" + }, + "modelInfo": { + "modelType": "Tipo de modelo", + "trainDate": "Data do treino", + "title": "Informações do modelo", + "error": "Falha ao carregar informações do modelo", + "availableModels": "Modelos Disponíveis", + "baseModel": "Modelo Básico", + "plusModelType": { + "userModel": "Ajuste-Fino", + "baseModel": "Modelo Básico" + }, + "supportedDetectors": "Detectores Suportados", + "loading": "Carregando informações do modelo…", + "cameras": "Câmaras", + "loadingAvailableModels": "Carregando modelos disponíveis…", + "modelSelect": "Os modelos disponíveis no Frigate+ podem ser selecionados aqui. Observe que apenas modelos compatíveis com a configuração atual do seu detector podem ser selecionados." + }, + "title": "Configurações Frigate+", + "apiKey": { + "validated": "A chave da API do Frigate+ foi detectada e validada", + "notValidated": "A chave da API do Frigate+ não foi detectada ou não foi validada", + "desc": "A chave de API do Frigate+ permite a integração com o serviço Frigate+.", + "plusLink": "Saiba mais sobre o Frigate+", + "title": "Chave de API do Frigate+" + }, + "restart_required": "Reinicialização necessária (modelo Frigate+ alterado)", + "unsavedChanges": "Alterações nas configurações do Frigate+ não salvas" + }, + "masksAndZones": { + "motionMasks": { + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "context": { + "documentation": "Leia a documentação", + "title": "As máscaras de movimento são usadas para impedir que tipos indesejados de movimento acionem a detecção (exemplo: galhos de árvores, carimbos de data/hora da câmera). As máscaras de movimento devem ser usadas com moderação, o uso excessivo de máscaras dificultará o rastreamento de objetos." + }, + "polygonAreaTooLarge": { + "documentation": "Leia a documentação", + "tips": "As máscaras de movimento não impedem que objetos sejam detectados. Você deve usar uma zona obrigatória em vez disso.", + "title": "A máscara de movimento está cobrindo {{polygonArea}}% da área da câmara. Máscaras de movimento grandes não são recomendadas." + }, + "label": "Máscara de movimento", + "desc": { + "documentation": "Documentação", + "title": "As máscaras de movimento são usadas para impedir que tipos indesejados de movimento acionem a detecção. O uso excessivo de máscaras dificultará o rastreamento de objetos." + }, + "clickDrawPolygon": "Clique para desenhar um polígono na imagem.", + "toast": { + "success": { + "noName": "O filtro de movimento foi guardado. Reinicie o Frigate para aplicar as alterações.", + "title": "{{polygonName}} foi salvo. Reinicie o Frigate para aplicar as alterações." + } + }, + "edit": "Editar Máscara de Movimento", + "documentTitle": "Editar Máscara de Movimento - Frigate", + "add": "Nova Máscara de Movimento" + }, + "zones": { + "label": "Zonas", + "documentTitle": "Editar Zona - Frigate", + "desc": { + "documentation": "Documentação", + "title": "As zonas permitem definir uma área específica do quadro para que você possa determinar se um objeto está ou não dentro de uma área particular." + }, + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "add": "Adicionar Zona", + "edit": "Editar Zona", + "clickDrawPolygon": "Clique para desenhar um polígono na imagem.", + "name": { + "title": "Nome", + "inputPlaceHolder": "Digite um nome…", + "tips": "O nome deve ter pelo menos 2 caracteres e não pode ser o nome de uma câmara ou de outra zona." + }, + "inertia": { + "title": "Inércia", + "desc": "Especifica quantos quadros um objeto deve estar em uma zona antes de ser considerado dentro da zona. Padrão: 3" + }, + "loiteringTime": { + "title": "Tempo de permanência", + "desc": "Define o tempo mínimo, em segundos, que o objeto deve permanecer na zona para que ela seja ativada. Padrão: 0" + }, + "objects": { + "title": "Objetos", + "desc": "Lista de objetos que se aplicam a esta zona." + }, + "allObjects": "Todos os objetos", + "speedEstimation": { + "title": "Estimativa de velocidade", + "desc": "Ativar estimativa de velocidade para objetos nesta zona. A zona deve ter exatamente 4 pontos.", + "docs": "Lê a documentação", + "lineBDistance": "Distância da Linha B ({{unit}})", + "lineCDistance": "Distância da Linha C ({{unit}})", + "lineDDistance": "Distância da Linha D ({{unit}})", + "lineADistance": "Distância da Linha A ({{unit}})" + }, + "speedThreshold": { + "title": "Limiar de velocidade ({{unit}})", + "desc": "Especifica uma velocidade mínima para que os objetos sejam considerados nesta zona.", + "toast": { + "error": { + "pointLengthError": "A estimativa de velocidade foi desativada para esta zona. Zonas com estimativa de velocidade devem ter exatamente 4 pontos.", + "loiteringTimeError": "Zonas com tempos de permanência maiores que 0 não devem ser usadas com estimativa de velocidade." + } + } + }, + "toast": { + "success": "A zona ({{zoneName}}) foi salva. Reinicie o Frigate para aplicar as alterações." + } + }, + "filter": { + "all": "Todas as Máscaras e Zonas" + }, + "toast": { + "success": { + "copyCoordinates": "Coordenadas de {{polyName}} copiadas para a área de transferência." + }, + "error": { + "copyCoordinatesFailed": "Não foi possível copiar as coordenadas para a área de transferência." + } + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "O nome da zona deve ter pelo menos 2 caracteres.", + "mustNotContainPeriod": "O nome da zona não pode conter pontos.", + "hasIllegalCharacter": "O nome da zona contém caracteres ilegais.", + "mustNotBeSameWithCamera": "O nome da zona não pode ser o mesmo que o nome da câmara.", + "alreadyExists": "Já existe uma zona com esse nome para esta câmara." + } + }, + "distance": { + "error": { + "text": "A distância deve ser maior ou igual a 0,1.", + "mustBeFilled": "Todos os campos de distância devem ser preenchidos para usar a estimativa de velocidade." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "A inércia deve ser maior que 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "O tempo de permanência deve ser maior ou igual a 0." + } + }, + "polygonDrawing": { + "removeLastPoint": "Remover o último ponto", + "reset": { + "label": "Limpar todos os pontos" + }, + "snapPoints": { + "true": "Fixar pontos", + "false": "Não fixar pontos" + }, + "delete": { + "title": "Confirmar exclusão", + "success": "{{name}} foi excluído.", + "desc": "Você tem certeza de que deseja excluir o {{type}} {{name}}?" + }, + "error": { + "mustBeFinished": "A criação do polígono deve ser concluída antes de salvar." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "O limiar de velocidade deve ser igual ou superior a 0,1." + } + } + }, + "objectMasks": { + "label": "Máscaras de objetos", + "clickDrawPolygon": "Clique para desenhar um polígono na imagem.", + "desc": { + "documentation": "Documentação", + "title": "As máscaras de filtro de objetos são usadas para filtrar falsos positivos para um determinado tipo de objeto com base na localização." + }, + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "objects": { + "allObjectTypes": "Todos os tipos de objeto", + "title": "Objetos", + "desc": "O tipo de objeto que se aplica a esta máscara de objeto." + }, + "add": "Adicionar filtro para objecto", + "edit": "Editar filtro de objecto", + "documentTitle": "Editar filtro de movimento - Frigate", + "toast": { + "success": { + "noName": "A máscara de objeto foi salva. Reinicie o Frigate para aplicar as alterações.", + "title": "{{polygonName}} foi salvo. Reinicie o Frigate para aplicar as alterações." + } + }, + "context": "As máscaras de filtro de objetos são usadas para filtrar falsos positivos para um determinado tipo de objeto com base na localização." + }, + "restart_required": "É necessário reiniciar (máscaras/zonas alteradas)", + "motionMaskLabel": "Mascara movimento {{number}}", + "objectMaskLabel": "Mascara de objecto {{number}} ({{label}})" + }, + "debug": { + "zones": { + "title": "Zonas", + "desc": "Mostrar um esboço de quaisquer zonas definidas" + }, + "timestamp": { + "title": "Carimbo de hora", + "desc": "Sobrepor um carimbo de data/hora na imagem" + }, + "title": "Depurar", + "detectorDesc": "O Frigate utiliza os seus detectores ({{detectors}}) para detectar objetos no fluxo de vídeo da sua câmara.", + "desc": "A vista de depuração apresenta uma vista em tempo real dos objetos localizados e das respectivas estatísticas. A lista de objetos apresenta um resumo dos objetos detectados em tempo real.", + "debugging": "A depurar", + "objectList": "Lista de Objetos", + "noObjects": "Sem Objetos", + "boundingBoxes": { + "title": "Caixas de contorno", + "desc": "Mostrar caixas de contorno à volta dos objetos seguidos", + "colors": { + "label": "Cores da caixa de contorno de objetos", + "info": "
  • Na inicialização, cores diferentes serão atribuídas a cada rótulo de objeto
  • Uma linha fina azul escura indica que o objeto não foi detectado neste momento
  • Uma linha fina cinza indica que o objeto foi detectado como estacionário
  • Uma linha grossa indica que o objeto está sujeito ao rastreamento automático (quando ativado)
  • " + } + }, + "objectShapeFilterDrawing": { + "tips": "Habilite esta opção para desenhar um retângulo na imagem da câmara para mostrar sua área e proporção. Esses valores podem ser usados para definir parâmetros de filtro de formato de objeto na sua configuração.", + "document": "Leia a documentação ", + "score": "Pontuação", + "ratio": "Razão", + "area": "Area", + "desc": "Desenhe um retângulo na imagem para visualizar detalhes da área e da proporção", + "title": "Desenho de filtro de forma de objeto" + }, + "regions": { + "title": "Regiões", + "desc": "Mostrar uma caixa da região de interesse enviada ao detector de objetos", + "tips": "

    Caixas de região


    Caixas verdes brilhantes serão sobrepostas em áreas de interesse no quadro que estão sendo enviadas ao detector de objetos.

    " + }, + "motion": { + "tips": "

    Caixas de movimento


    Caixas vermelhas serão sobrepostas em áreas do quadro onde o movimento está sendo detectado

    ", + "title": "Caixas de movimento", + "desc": "Mostrar caixas ao redor das áreas onde o movimento é detectado" + }, + "mask": { + "title": "Máscaras de movimento", + "desc": "Mostrar polígonos de máscara de movimento" + }, + "paths": { + "title": "Caminhos", + "desc": "Mostrar pontos significativos do caminho do objeto rastreado", + "tips": "

    Paths


    Linhas e círculos indicarão pontos significativos que o objeto rastreado moveu durante seu ciclo de vida.

    " + }, + "openCameraWebUI": "Abrir a Interface Web de {{camera}}", + "audio": { + "title": "Áudio", + "noAudioDetections": "Nenhuma detecção de áudio", + "score": "pontuanção", + "currentRMS": "RMS Atual", + "currentdbFS": "dbFS Atual" + } + }, + "camera": { + "reviewClassification": { + "readTheDocumentation": "Leia a documentação", + "title": "Classificação da Análise", + "noDefinedZones": "Nenhuma zona está definida para esta câmara.", + "objectAlertsTips": "Todos os objetos {{alertsLabels}} na câmara {{cameraName}} serão exibidos como Alertas.", + "zoneObjectDetectionsTips": { + "text": "Todos os objetos {{detectionsLabels}} não categorizados na zona {{zone}} na câmara {{cameraName}} serão exibidos como Detecções.", + "regardlessOfZoneObjectDetectionsTips": "Todos os objetos {{detectionsLabels}} não categorizados na câmara {{cameraName}} serão exibidos como Detecções, independentemente da zona em que se encontram.", + "notSelectDetections": "Todos os objetos {{detectionsLabels}} detectados na zona {{zone}} na câmara {{cameraName}} que não forem categorizados como Alertas serão exibidos como Detecções, independentemente da zona em que se encontram." + }, + "selectAlertsZones": "Selecionar zonas para Alertas", + "selectDetectionsZones": "Selecionar zonas para Detecções", + "limitDetections": "Limitar detecções a zonas específicas", + "desc": "O Frigate categoriza os itens de análise como Alertas e Detecções. Por padrão, todos os objetos do tipo pessoa e carro são considerados Alertas. Você pode refinar a categorização dos seus itens de análise configurando as zonas necessárias para eles.", + "objectDetectionsTips": "Todos os objetos {{detectionsLabels}} não categorizados na câmara {{cameraName}} serão exibidos como Detecções, independentemente da zona em que se encontram.", + "zoneObjectAlertsTips": "Todos os objetos {{alertsLabels}} detectados na zona {{zone}} na câmara {{cameraName}} serão exibidos como Alertas.", + "toast": { + "success": "A configuração de Classificação de análise foi salva. Reinicie o Frigate para aplicar as alterações." + }, + "unsavedChanges": "Configurações de classificação de análises não salvas para {{camera}}" + }, + "title": "Configurações da câmara", + "streams": { + "title": "Transmissões", + "desc": "Desativar uma câmara interrompe completamente o processamento das transmissões dessa câmara pelo Frigate. Detecção, gravação e depuração ficarão indisponíveis.
    Observação: Isso não desativa as retransmissões do go2rtc." + }, + "review": { + "title": "Análise", + "desc": "Ative ou desative alertas e detecções para esta câmara. Quando desativado, nenhum novo item de análise será gerado. ", + "alerts": "Alertas ", + "detections": "Detecções " + }, + "object_descriptions": { + "title": "Descrições de objetos de IA generativa", + "desc": "Ative/desative temporariamente as descrições de objetos de IA generativa para esta câmera. Quando desativadas, as descrições geradas por IA não serão solicitadas para objetos rastreados nesta câmera." + }, + "review_descriptions": { + "title": "Descrições de análises de IA generativa", + "desc": "Ative/desative temporariamente as descrições de avaliação geradas por IA para esta câmera. Quando desativadas, as descrições geradas por IA não serão solicitadas para itens de avaliação nesta câmera." + }, + "addCamera": "Adicionar Nova Câmera", + "editCamera": "Editar Câmera:", + "selectCamera": "Selecione uma Câmera", + "backToSettings": "Voltar para as Configurações da Câmera", + "cameraConfig": { + "add": "Adicionar Câmera", + "edit": "Editar Câmera", + "description": "Configure as definições da câmera, incluindo entradas de transmissão e funções.", + "name": "Nome da Câmera", + "nameRequired": "O nome da câmera é obrigatório", + "nameInvalid": "O nome da câmera deve conter apenas letras, números, sublinhados ou hifens", + "namePlaceholder": "e.g., porta_da_frente", + "enabled": "Habilitado", + "ffmpeg": { + "inputs": "Entrada de Streams", + "path": "Caminho da Stream", + "pathRequired": "Caminho da Stream é obrigatória", + "pathPlaceholder": "rtsp://...", + "roles": "Funções", + "rolesRequired": "Pelo menos uma função é necessária", + "rolesUnique": "Cada função (áudio, detecção, gravação) só pode ser atribuída a uma stream", + "addInput": "Adicionar Entrada de Stream", + "removeInput": "Remover Entrada de Stream", + "inputsRequired": "É necessário pelo menos uma stream de entrada" + }, + "toast": { + "success": "Câmera {{cameraName}} guardada com sucesso" + }, + "nameLength": "O nome da câmara deve ter ao menos 24 caracteres." + } + }, + "motionDetectionTuner": { + "contourArea": { + "title": "Área de contorno", + "desc": "O valor da área de contorno é usado para decidir quais grupos de pixels alterados são qualificados como movimento. Valor padrão: 10" + }, + "improveContrast": { + "title": "Melhorar o contraste", + "desc": "Melhorar o contraste para cenas mais escuras. Defeito: ON" + }, + "Threshold": { + "title": "Limite", + "desc": "O valor do limiar determina quanto de alteração na luminância de um pixel é necessário para ser considerado movimento. Valor padrão: 30" + }, + "desc": { + "title": "O Frigate utiliza a detecção de movimento como uma primeira verificação para ver se há algo acontecendo no quadro que valha a pena ser verificado com a detecção de objetos.", + "documentation": "Leia o Guia de Ajuste de Movimento" + }, + "title": "Ajustador de Detecção de Movimento", + "unsavedChanges": "Alterações do Ajuste de Movimento não guardadas ({{camera}})", + "toast": { + "success": "Definições para Movimento foram salvas." + } + }, + "enrichments": { + "faceRecognition": { + "desc": "O reconhecimento facial permite que as pessoas recebam nomes e, quando o rosto delas for reconhecido, o Frigate atribuirá o nome da pessoa como um subrótulo. Essas informações estão incluídas na interface do utilizador, nos filtros e nas notificações.", + "modelSize": { + "small": { + "desc": "O uso de pequeno emprega um modelo de incorporação facial do FaceNet que funciona eficientemente na maioria dos CPUs.", + "title": "pequeno" + }, + "large": { + "desc": "O uso de grande emprega um modelo de incorporação de rostos do ArcFace e será executado automaticamente no GPU, se aplicável.", + "title": "grande" + }, + "label": "Tamanho do modelo", + "desc": "O tamanho do modelo usado para reconhecimento facial." + }, + "title": "Reconhecimento facial", + "readTheDocumentation": "Leia a documentação" + }, + "semanticSearch": { + "modelSize": { + "small": { + "desc": "Usar pequeno emprega uma versão quantizada do modelo que usa menos RAM e roda mais rápido no CPU, com uma diferença muito insignificante na qualidade de incorporação.", + "title": "pequeno" + }, + "label": "Tamanho do modelo", + "desc": "O tamanho do modelo usado para incorporações de pesquisa semântica.", + "large": { + "title": "grande", + "desc": "Usar grande emprega o modelo Jina completo e será executado automaticamente no GPU, se aplicável." + } + }, + "reindexNow": { + "desc": "A reindexação regenerará os embeddings para todos os objetos rastreados. Esse processo é executado em segundo plano e pode sobrecarregar o seu CPU e levar um tempo considerável, dependendo do número de objetos rastreados.", + "label": "Reindexar agora", + "confirmTitle": "Confirmar reindexação", + "confirmDesc": "Tem certeza de que deseja reindexar todos os objetos incorporados rastreados? Este processo será executado em segundo plano, mas pode sobrecarregar o seu CPU e levar bastante tempo. Você pode acompanhar o progresso na página Explorar.", + "confirmButton": "Reindexar", + "success": "Reindexação iniciada com sucesso.", + "alreadyInProgress": "A reindexação já está em andamento.", + "error": "Falha ao iniciar a reindexação: {{errorMessage}}" + }, + "desc": "A Pesquisa Semântica no Frigate permite que você encontre objetos rastreados dentro dos seus itens de análise usando a própria imagem, uma descrição de texto definida pelo utilizador ou uma gerada automaticamente.", + "readTheDocumentation": "Leia a documentação", + "title": "Busca semântica" + }, + "licensePlateRecognition": { + "desc": "O Frigate pode reconhecer placas de veículos e adicionar automaticamente os caracteres detectados ao campo recognized_license_plate ou um nome conhecido como subrótulo para objetos do tipo carro. Um caso de uso comum pode ser a leitura de placas de carros entrando numa garagem ou de carros passando por uma rua.", + "title": "Reconhecimento de placas", + "readTheDocumentation": "Leia a documentação" + }, + "birdClassification": { + "desc": "A classificação de aves identifica aves conhecidas usando um modelo quantizado do Tensorflow. Quando uma ave conhecida é reconhecida, seu nome comum é adicionado como um sub_label. Essas informações são incluídas na interface do utilizador, nos filtros e nas notificações.", + "title": "Classificação de aves" + }, + "unsavedChanges": "Alterações nas configurações de enriquecimentos não salvos", + "title": "Configurações de enriquecimentos", + "restart_required": "Reinicialização necessária (configurações de enriquecimento alteradas)", + "toast": { + "success": "As configurações de enriquecimento foram salvas. Reinicie o Frigate para aplicar as alterações.", + "error": "Falha ao salvar alterações de configuração: {{errorMessage}}" + } + }, + "users": { + "dialog": { + "changeRole": { + "roleInfo": { + "admin": "Administrador", + "adminDesc": "Acesso total a todos os recursos.", + "viewer": "Visualização", + "viewerDesc": "Limitado apenas a painéis ao vivo, análise, exploração e exportações.", + "intro": "Selecione a função apropriada para este utilizador:", + "customDesc": "Papel customizado com acesso a câmaras específicas." + }, + "title": "Alterar função do utilizador", + "desc": "Atualizar permissões para {{username}}", + "select": "Selecione uma função" + }, + "deleteUser": { + "title": "Excluir utilizador", + "desc": "Esta ação não pode ser desfeita. Isso excluirá permanentemente a conta do utilizador e removerá todos os dados associados.", + "warn": "Tem certeza de que deseja excluir {{username}}?" + }, + "form": { + "user": { + "title": "Nome de utilizador", + "desc": "Somente letras, números, pontos e sublinhados são permitidos.", + "placeholder": "Digite o nome de utilizador" + }, + "password": { + "strength": { + "weak": "Fraco", + "medium": "Médio", + "strong": "Forte", + "title": "Força da senha: ", + "veryStrong": "Muito forte" + }, + "notMatch": "As senhas não correspondem", + "title": "Senha", + "placeholder": "Digite a senha", + "confirm": { + "title": "Digite uma senha", + "placeholder": "Confirme sua senha" + }, + "match": "Correspondência de senhas" + }, + "newPassword": { + "title": "Nova Senha", + "placeholder": "Digite a nova senha", + "confirm": { + "placeholder": "Digite novamente a nova senha" + } + }, + "usernameIsRequired": "O nome de utilizador é obrigatório", + "passwordIsRequired": "A senha é obrigatória" + }, + "createUser": { + "title": "Criar novo utilizador", + "desc": "Adicione uma nova conta de utilizador e especifique uma função para acesso a áreas da interface do utilizador do Frigate.", + "usernameOnlyInclude": "O nome de utilizador pode incluir apenas letras, números, . ou _", + "confirmPassword": "Por favor confirme sua senha" + }, + "passwordSetting": { + "setPassword": "Definir Senha", + "desc": "Crie uma senha forte para proteger esta conta.", + "updatePassword": "Atualizar senha para {{username}}", + "cannotBeEmpty": "A senha não pode ficar vazia", + "doNotMatch": "As senhas não correspondem" + } + }, + "management": { + "desc": "Gestão de utilizadores desta instância do Frigate.", + "title": "Gestão de Utilizadores" + }, + "table": { + "noUsers": "Nenhum utilizador encontrado.", + "password": "Senha", + "deleteUser": "Excluir utilizador", + "changeRole": "Alterar função do utilizador", + "username": "Nome de utilizador", + "actions": "Ações", + "role": "Papel" + }, + "title": "Utilizadores", + "addUser": "Adicionar utilizador", + "updatePassword": "Atualizar senha", + "toast": { + "success": { + "createUser": "Utilizador {{user}} criado com sucesso", + "deleteUser": "Utilizador {{user}} excluído com sucesso", + "updatePassword": "Senha atualizada com sucesso.", + "roleUpdated": "Função atualizada para {{user}}" + }, + "error": { + "setPasswordFailed": "Falha ao salvar a senha: {{errorMessage}}", + "createUserFailed": "Falha ao criar utilizador: {{errorMessage}}", + "deleteUserFailed": "Falha ao excluir o utilizador: {{errorMessage}}", + "roleUpdateFailed": "Falha ao atualizar a função: {{errorMessage}}" + } + } + }, + "triggers": { + "documentTitle": "Triggers (gatilhos)", + "management": { + "title": "Gestão de Triggers", + "desc": "Gira triggers para {{camera}}. Use o tipo de miniatura para acionar miniaturas semelhantes ao objeto rastreado selecionado e o tipo de descrição para acionar descrições semelhantes ao texto especificado." + }, + "addTrigger": "Adicionar Trigger", + "table": { + "name": "Nome", + "type": "Tipo", + "content": "Conteúdo", + "threshold": "Limite", + "actions": "Ações", + "noTriggers": "Nenhum trigger configurado para esta câmera.", + "edit": "Editar", + "deleteTrigger": "Apagar Trigger", + "lastTriggered": "Último acionado" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descrição" + }, + "actions": { + "alert": "Marcar como Alerta", + "notification": "Enviar Notificação" + }, + "dialog": { + "createTrigger": { + "title": "Criar Trigger", + "desc": "Crie um trigger para a câmera {{camera}}" + }, + "editTrigger": { + "title": "Editar Trigger", + "desc": "Editar as definições do trigger na câmera {{camera}}" + }, + "deleteTrigger": { + "title": "Apagar Trigger", + "desc": "Tem certeza de que deseja apagar o trigger {{triggerName}}? Esta ação não pode ser desfeita." + }, + "form": { + "name": { + "title": "Nome", + "placeholder": "Insira o nome do trigger", + "error": { + "minLength": "O nome deve ter pelo menos 2 caracteres.", + "invalidCharacters": "O nome só pode conter letras, números, sublinhados e hifens.", + "alreadyExists": "Já existe um trigger com este nome para esta câmera." + } + }, + "enabled": { + "description": "Habilitar ou desabilitar este trigger" + }, + "type": { + "title": "Tipo", + "placeholder": "Selecione o tipo de trigger" + }, + "content": { + "title": "Conteúdo", + "imagePlaceholder": "Selecione uma imagem", + "textPlaceholder": "Insira o conteúdo do texto", + "imageDesc": "Selecione uma imagem para acionar esta ação quando uma imagem semelhante for detectada.", + "textDesc": "Insira um texto para acionar esta ação quando uma descrição de objeto rastreado semelhante for detectada.", + "error": { + "required": "O Conteúdo é obrigatório." + } + }, + "threshold": { + "title": "Limite", + "error": { + "min": "Limite deve ser pelo menos 0", + "max": "Limite deve ser no máximo 1" + } + }, + "actions": { + "title": "Ações", + "desc": "Por padrão, o Frigate envia uma mensagem MQTT para todos os triggers. Escolha uma ação adicional a ser executada quando este trigger for disparado.", + "error": { + "min": "Pelo menos uma ação deve ser selecionada." + } + }, + "friendly_name": { + "title": "Nome Amigável", + "placeholder": "Nomeie ou descreva este gatilho", + "description": "Um nome amigável ou descritivo opcional para este gatilho." + } + } + }, + "toast": { + "success": { + "createTrigger": "Trigger {{name}} criado com sucesso.", + "updateTrigger": "Trigger {{name}} atualizado com sucesso.", + "deleteTrigger": "Trigger {{name}} apagado com sucesso." + }, + "error": { + "createTriggerFailed": "Falha ao criar trigger: {{errorMessage}}", + "updateTriggerFailed": "Falha ao atualizar o trigger: {{errorMessage}}", + "deleteTriggerFailed": "Falha ao apagar o trigger: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Pesquisa Semântica desativada", + "desc": "Pesquisa Semântica deve estar ativada para usar os Gatilhos." + } + }, + "roles": { + "management": { + "title": "Gestão do Papel de Visualizador", + "desc": "Gerir papéis de visualizador customizados e as suas permissões de acesso para esta instância do Frigate." + }, + "addRole": "Adicionar Papel", + "table": { + "role": "Papel", + "cameras": "Câmaras", + "actions": "Ações", + "noRoles": "Nenhum papel customizado encontrado.", + "editCameras": "Editar Câmaras", + "deleteRole": "Apagar Papel" + }, + "toast": { + "success": { + "createRole": "Papel {{role}} criado com sucesso", + "updateCameras": "Câmaras atualizados para o papel {{role}}", + "deleteRole": "Papel {{role}} apagado com sucesso", + "userRolesUpdated_one": "{{count}} utilizador(os) atribuídos a este papel foram atualizados para 'visualizador', que possui acesso a todas as câmaras.", + "userRolesUpdated_many": "", + "userRolesUpdated_other": "" + }, + "error": { + "createRoleFailed": "Falha ao criar papel: {{errorMessage}}", + "updateCamerasFailed": "Falha ao atualizar câmaras: {{errorMessage}}", + "deleteRoleFailed": "Falha ao apagar papel: {{errorMessage}}", + "userUpdateFailed": "Falha ao atualizar papel do utilizador: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Criar Novo Papel", + "desc": "Adicionar um novo papel e especificar permissões de acesso." + }, + "editCameras": { + "title": "Editar Câmaras de Papéis", + "desc": "Atualizar acesso da câmara para o papel {{role}}." + }, + "deleteRole": { + "title": "Apagar Papel", + "desc": "Esta ação não pode ser desfeita. Isto irá apagar permanentemente o papel e atribuir a quaisquer utilizadores com este papel como 'visualizador', o que dará acesso de visualização para todas as câmaras.", + "warn": "Tem certeza que quer apagar {{role}}?", + "deleting": "A apagar…" + }, + "form": { + "role": { + "title": "Nome do Papel", + "placeholder": "Digitar nome do papel", + "desc": "Apenas letras, números, pontos e sublinhados são permitidos.", + "roleIsRequired": "Nome para o papel é requerido", + "roleOnlyInclude": "O nome do papel pode conter apenas letras, números, pontos ou sublinhados", + "roleExists": "Um papel com este nome já existe." + }, + "cameras": { + "title": "Câmaras", + "desc": "Selecione as câmaras que este papel terá acesso. Ao menos uma câmara é requerida.", + "required": "Ao menos uma câmara deve ser selecionada." + } + } + } + }, + "cameraWizard": { + "title": "Adicionar Câmara", + "description": "Siga os passos abaixo para adicionar uma câmara nova no seu Frigate.", + "steps": { + "nameAndConnection": "Nome e Conexão", + "streamConfiguration": "Configuração de Stream", + "validationAndTesting": "Validação e Teste" + }, + "save": { + "success": "Nova câmara {{cameraName}} grava com sucesso.", + "failure": "Erro ao gravar {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolução", + "video": "Vídeo", + "audio": "Áudio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Favor fornecer uma URL de stream válida", + "testFailed": "Teste de stream falhou: {{error}}" + }, + "step1": { + "description": "Adicione os pormenores da sua câmara e teste a conexão.", + "cameraName": "Nome da Câmara", + "cameraNamePlaceholder": "ex., porta_entrada ou Visão Geral do Quintal", + "host": "Host/Endereço IP", + "port": "Porta", + "username": "Nome de Utilizador", + "usernamePlaceholder": "Opcional", + "password": "Palavra-passe", + "passwordPlaceholder": "Opcional", + "selectTransport": "Selecionar protocolo de transporte", + "cameraBrand": "Marca da Câmara", + "selectBrand": "Selecione a marca da câmara para template de URL", + "customUrl": "URL Customizada de Stream", + "brandInformation": "Informação da marca", + "brandUrlFormat": "Para câmaras com o formato de URL RTSP como: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nomedeusuario:palavra-passe@host:porta/caminho", + "testConnection": "Testar Conexão", + "testSuccess": "Teste de conexão bem sucedido!", + "testFailed": "Teste de conexão falhou. Favor verifique os dados e tente novamente.", + "streamDetails": "Pormenores do Stream", + "warnings": { + "noSnapshot": "Não foi possível adquirir uma captura de imagem do stream configurado." + }, + "errors": { + "brandOrCustomUrlRequired": "Selecione a marca da câmara com o host/IP or selecione 'Outro' com uma URL customizada", + "nameRequired": "Nome para a câmara requerido" + } + }, + "step2": { + "url": "URL", + "roleLabels": { + "audio": "Áudio" + } + }, + "step3": { + "reload": "Recarregar", + "valid": "Válido", + "failed": "Falhou", + "none": "Nenhum", + "error": "Erro" + } + }, + "cameraManagement": { + "cameraConfig": { + "enabled": "Ativado", + "addUrl": "Adicionar URL" + } + }, + "cameraReview": { + "review": { + "title": "Rever" + } + } +} diff --git a/web/public/locales/pt/views/system.json b/web/public/locales/pt/views/system.json new file mode 100644 index 0000000..9f90073 --- /dev/null +++ b/web/public/locales/pt/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "storage": "Frigate - Estatísticas de Armazenamento", + "general": "Frigate - Estatísticas Gerais", + "enrichments": "Frigate - Estatísticas de Enriquecimento", + "logs": { + "frigate": "Frigate - Registos de Eventos do Frigate", + "go2rtc": "Frigate - Registos de Eventos do Go2RTC", + "nginx": "Frigate - Registos de Eventos do Nginx" + }, + "cameras": "Frigate - Estatísticas das Câmaras" + }, + "title": "Sistema", + "metrics": "Métricas do sistema", + "logs": { + "type": { + "label": "Tipo", + "timestamp": "Carimbo de hora", + "tag": "Etiqueta", + "message": "Mensagem" + }, + "copy": { + "success": "Registos copiados para a área de transferência", + "label": "Copiar para a Área de Transferência", + "error": "Não foi possível copiar os registos para a área de transferência" + }, + "download": { + "label": "Transferir Registos" + }, + "tips": "Os registos estão a ser transmitidos do servidor", + "toast": { + "error": { + "fetchingLogsFailed": "Erro ao obter os registos: {{errorMessage}}", + "whileStreamingLogs": "Erro enquanto transmitia os registos: {{errorMessage}}" + } + } + }, + "storage": { + "cameraStorage": { + "camera": "Câmara", + "storageUsed": "Armazenamento", + "percentageOfTotalUsed": "Porcentagem do total", + "bandwidth": "Largura de banda", + "unused": { + "tips": "Este valor pode não representar com precisão o espaço livre disponível para o Frigate se você tiver outros ficheiros armazenados em sua unidade além das gravações do Frigate. O Frigate não rastreia o uso de armazenamento fora de suas gravações.", + "title": "Não utilizado" + }, + "unusedStorageInformation": "Informações de armazenamento não utilizado", + "title": "Armazenamento da câmara" + }, + "title": "Armazenamento", + "overview": "Sinopse", + "recordings": { + "title": "Gravações", + "earliestRecording": "Primeira gravação disponível:", + "tips": "Este valor representa o armazenamento total utilizado pelas gravações na base de dados do Frigate. O Frigate não acompanha a utilização do armazenamento de todos os ficheiros no seu disco." + }, + "shm": { + "title": "Alocação SHM (memória partilhada)", + "warning": "A tamanho atual de SHM de {{total}} MB é muito pequeno. Aumente-o para pelo menos {{min_shm}} MB." + } + }, + "cameras": { + "title": "Câmaras", + "info": { + "video": "Vídeo:", + "unknown": "Desconhecido", + "error": "Erro: {{error}}", + "fetching": "Obtendo dados da câmara", + "resolution": "Resolução:", + "codec": "Codec:", + "fps": "FPS:", + "stream": "Transmissão {{idx}}", + "audio": "Áudio:", + "cameraProbeInfo": "{{camera}} Explorar informações da Camara", + "tips": { + "title": "Explorar informações da Camara" + }, + "streamDataFromFFPROBE": "Os dados de transmissão são obtidos com ffprobe.", + "aspectRatio": "relação de aspeto" + }, + "framesAndDetections": "Quadros / Detecções", + "label": { + "camera": "câmara", + "detect": "detectar", + "capture": "capturar", + "skipped": "ignorado", + "ffmpeg": "FFmpeg", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraFramesPerSecond": "imagens por segundo de {{camName}}", + "cameraCapture": "captura de {{camName}}", + "cameraDetectionsPerSecond": "deteções por segundo de {{camName}}", + "overallFramesPerSecond": "imagens por segundo totais (FPS)", + "overallDetectionsPerSecond": "deteções por segundo totais", + "overallSkippedDetectionsPerSecond": "deteções ignoradas por segundo totais", + "cameraDetect": "deteção de {{camName}}", + "cameraSkippedDetectionsPerSecond": "deteções ignoradas por segundo de {{camName}}" + }, + "overview": "Visão geral", + "toast": { + "success": { + "copyToClipboard": "Dados de exploração copiados para a área de transferência." + }, + "error": { + "unableToProbeCamera": "Não foi possível explorar a câmara: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Última atualização: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} tem alta utilização da CPU FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} tem alta utilização da CPU de deteção ({{detectAvg}}%)", + "healthy": "O sistema está saudável", + "reindexingEmbeddings": "Reindexando incorporações ({{processed}}% completo)", + "detectIsVerySlow": "{{detect}} está muito lento ({{speed}} ms)", + "cameraIsOffline": "{{camera}} está off-line", + "detectIsSlow": "{{detect}} está lento ({{speed}} ms)", + "shmTooLow": "/dev/shm alocação ({{total}} MB) deveria ser aumentada pelo menos {{min}} MB." + }, + "general": { + "title": "Geral", + "detector": { + "title": "Detetores", + "cpuUsage": "Utilização do CPU do Detetor", + "memoryUsage": "Utilização da Memória do Detetor", + "inferenceSpeed": "Velocidade de Inferência do Detetor", + "temperature": "Temperatura do Detetor", + "cpuUsageInformation": "CPU utilizada na preparação de dados de entrada e saída de/para os modelos de deteção. Este valor não mede oa utilização da inferência, mesmo se estiver a utilizar uma GPU ou acelerador." + }, + "hardwareInfo": { + "title": "Informação de Hardware", + "gpuUsage": "Utilização da GPU", + "gpuMemory": "Memória da GPU", + "gpuInfo": { + "nvidiaSMIOutput": { + "driver": "Controlador: {{driver}}", + "vbios": "Informação VBios: {{vbios}}", + "name": "Nome: {{name}}", + "cudaComputerCapability": "Capacidade de computação CUDA: {{cuda_compute}}", + "title": "Saída Nvidia SMI" + }, + "copyInfo": { + "label": "Copiar informação da GPU" + }, + "closeInfo": { + "label": "Fechar informação da GPU" + }, + "toast": { + "success": "Informação da GPU copiada para a área de transferência" + }, + "vainfoOutput": { + "title": "Saída do Vainfo", + "returnCode": "Código de retorno: {{code}}", + "processOutput": "Saída do processo:", + "processError": "Erro no processo:" + } + }, + "gpuEncoder": "Codificador da GPU", + "gpuDecoder": "Descodificador da GPU", + "npuUsage": "Utilização NPU", + "npuMemory": "Memória NPU" + }, + "otherProcesses": { + "title": "Outros Processos", + "processCpuUsage": "Utilização da CPU do Processo", + "processMemoryUsage": "Utilização da Memória do Processo" + } + }, + "enrichments": { + "title": "Enriquecimentos", + "infPerSecond": "Inferências por Segundo", + "embeddings": { + "image_embedding_speed": "Velocidade de Incorporação de Imagem", + "face_embedding_speed": "Velocidade de Incorporação Facial", + "plate_recognition_speed": "Velocidade de Reconhecimento de Placas", + "text_embedding_speed": "Velocidade de Incorporação de Texto", + "face_recognition_speed": "Velocidade de Reconhecimento Facial", + "plate_recognition": "Reconhecimento de Placas", + "image_embedding": "Incorporação de Imagem", + "text_embedding": "Incorporação de Texto", + "face_recognition": "Reconhecimento Facial", + "yolov9_plate_detection_speed": "Velocidade de Deteção de Placas YOLOv9", + "yolov9_plate_detection": "Deteção de Placas YOLOv9" + } + } +} diff --git a/web/public/locales/ro/audio.json b/web/public/locales/ro/audio.json new file mode 100644 index 0000000..56815c6 --- /dev/null +++ b/web/public/locales/ro/audio.json @@ -0,0 +1,503 @@ +{ + "gunshot": "Foc de arma", + "machine_gun": "Mitraliera", + "speech": "Vorbire", + "babbling": "Murmur", + "yell": "Striga", + "bellow": "Sub", + "dog": "Câine", + "horse": "Cal", + "bird": "Pasare", + "sheep": "Oaie", + "boat": "Barcă", + "motorcycle": "Motocicletă", + "bus": "Autobuz", + "train": "Tren", + "skateboard": "Skateboard", + "camera": "Camera foto", + "bicycle": "Bicicletă", + "car": "Mașină", + "cat": "Pisică", + "animal": "Animal", + "goat": "Capra", + "keyboard": "Orga", + "vehicle": "Vehicul", + "sink": "Chiuveta", + "scissors": "Foarfeca", + "hair_dryer": "Uscator de Par", + "door": "Usa", + "blender": "Blender", + "mouse": "Soarece", + "clock": "Ceas", + "toothbrush": "Periuta de Dinti", + "bark": "Latrat", + "burping": "Ragaie", + "hiccup": "Sughite", + "bass_drum": "Toba Bass", + "fart": "Basina", + "bell": "Clopotel", + "reggae": "Reggae", + "accordion": "Acordeon", + "trombone": "Trombon", + "punk_rock": "Punk rock", + "church_bell": "Clopot de biserica", + "sanding": "Slefuire", + "whispering": "Soapte", + "laughter": "Raset", + "crying": "Planset", + "choir": "Cor", + "singing": "Canta", + "whoop": "Tusi", + "yodeling": "Vocalize", + "snicker": "Chicotit", + "sigh": "Suspin", + "mantra": "Mantră", + "child_singing": "Cantec de copil", + "snoring": "Sforaie", + "whistling": "Fluiera", + "breathing": "Respira", + "cough": "Tuseste", + "throat_clearing": "Curata gatul", + "wheeze": "Gafaie", + "gasp": "Suspina", + "snort": "Horcaie", + "humming": "Fredoneaza", + "groan": "Geamat", + "grunt": "Mormait", + "pant": "Gafaie", + "sneeze": "stranuta", + "sniff": "Adulmeca", + "run": "Fuge", + "footsteps": "Pasi", + "chewing": "Mesteca", + "hands": "Maini", + "clapping": "Aplauda", + "heartbeat": "Batai inima", + "cheering": "Incurajeaza", + "applause": "Aplauda", + "crowd": "Multime", + "pets": "Animal de companie", + "purr": "Toarce", + "meow": "Miau", + "duck": "Rata", + "quack": "Mac", + "goose": "Gasca", + "wild_animals": "Animal Salbatic", + "cattle": "Vita", + "moo": "Muu", + "cowbell": "Clopot", + "pig": "Porc", + "oink": "Guit", + "chicken": "Gaina", + "cock_a_doodle_doo": "Cucurigu", + "turkey": "Curcan", + "roar": "Raget", + "chirp": "Cipcirit", + "pigeon": "Porumbel", + "crow": "Cioara", + "owl": "Bufnita", + "dogs": "Caini", + "rats": "Sobolani", + "insect": "Insecta", + "cricket": "Greier", + "mosquito": "Tantar", + "fly": "Musca", + "frog": "Broasca", + "snake": "Sarpe", + "music": "Muzica", + "musical_instrument": "Instrument Muzical", + "electric_guitar": "Chitara Electronica", + "guitar": "Chitara", + "bass_guitar": "Chitara Bass", + "acoustic_guitar": "Chitara Acustica", + "tapping": "Bataie", + "banjo": "Banjo", + "mandolin": "Mandolina", + "piano": "Pian", + "electric_piano": "Pian Electronic", + "synthesizer": "Sintetizator", + "percussion": "Percutie", + "drum_kit": "Tobe", + "drum": "Toba", + "tambourine": "Tamburina", + "gong": "Gong", + "orchestra": "Orchestră", + "trumpet": "Trompeta", + "violin": "Vioară", + "cello": "Violoncel", + "flute": "Flaut", + "saxophone": "Saxofon", + "clarinet": "Clarinet", + "harp": "Harpa", + "bicycle_bell": "Sonerie de bicicletă", + "tuning_fork": "Diapazon", + "harmonica": "Muzicuta", + "bagpipes": "Cimpoi", + "pop_music": "Muzica Pop", + "hip_hop_music": "Muzica Hip-Hop", + "rock_music": "Muzica Rock", + "heavy_metal": "Heavy metal", + "progressive_rock": "Rock Progresiv", + "rock_and_roll": "Rock and Roll", + "soul_music": "Muzica Soul", + "funk": "Funk", + "folk_music": "Muzica Folk", + "country": "Muzica Country", + "jazz": "Jazz", + "disco": "Muzica Disco", + "classical_music": "Muzica Clasica", + "opera": "Opera", + "electronic_music": "Muzica Electronica", + "house_music": "Muzica House", + "techno": "Muzica Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Electronică", + "electronic_dance_music": "Muzica de dans electronica", + "ambient_music": "Muzica Ambientala", + "trance_music": "Muzica Trance", + "music_of_latin_america": "Muzica Latino", + "salsa_music": "Salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "vocal_music": "Muzica Vocala", + "a_capella": "A Capella", + "music_of_africa": "Muzica Africana", + "christian_music": "Muzica Crestina", + "gospel_music": "Muzica Gaspel", + "music_of_asia": "Muzica Asiatica", + "music_of_bollywood": "Muzica Bollywood", + "traditional_music": "Muzica Traditionala", + "song": "Cantec", + "background_music": "Muzica de fundal", + "theme_music": "Muzica Tematica", + "jingle": "Colind", + "lullaby": "Muzica de adormit copiii", + "christmas_music": "Colind", + "dance_music": "Muzica Dance", + "wedding_music": "Muzica de nunta", + "happy_music": "Muzica Vesela", + "sad_music": "Muzica Trista", + "wind": "Vant", + "rustling_leaves": "Fosnet de frunze", + "wind_noise": "Zgomot de vant", + "thunderstorm": "Furtuna", + "thunder": "Tunet", + "water": "Apa", + "rain": "Ploaie", + "raindrop": "Picaturi", + "waterfall": "Cascada", + "ocean": "Ocean", + "waves": "Valuri", + "steam": "Abur", + "fire": "Foc", + "rowboat": "Barca cu vasle", + "sailboat": "Barca cu panza", + "motorboat": "Barca cu motor", + "ship": "Vapor", + "motor_vehicle": "Autovehicul", + "toot": "claxon", + "car_alarm": "Alarma de mașină", + "power_windows": "Geamuri electrice", + "skidding": "Derapaj", + "tire_squeal": "Scartait de roti", + "car_passing_by": "Mașină în trecere", + "race_car": "Mașină de curse", + "truck": "Camion", + "air_horn": "claxon", + "ice_cream_truck": "Mașină de înghețată", + "police_car": "Mașină de poliție", + "ambulance": "Ambulanta", + "fire_engine": "Mașină de pompieri", + "traffic_noise": "Zgomot de trafic", + "train_wheels_squealing": "Scartait de roti de tren", + "subway": "Metrou", + "aircraft": "Aeronava", + "aircraft_engine": "Motor de avion", + "jet_engine": "Motor cu reactie", + "propeller": "Elice", + "helicopter": "Elicopter", + "fixed-wing_aircraft": "Aeronava cu aripi fixe", + "engine": "Motor", + "lawn_mower": "Mașină de tuns iarba", + "chainsaw": "Drujba", + "engine_starting": "Pornire motor", + "idling": "Relanti", + "accelerating": "Accelerare", + "doorbell": "Sonerie", + "sliding_door": "Usa culisanta", + "slam": "Trantit", + "dishes": "Vase", + "cutlery": "Tacamuri", + "chopping": "Tocare", + "frying": "Prajire", + "microwave_oven": "Cuptor cu microunde", + "water_tap": "Robine", + "bathtub": "Cada", + "toilet_flush": "Tras apa", + "electric_toothbrush": "Periuta de dinti electronica", + "vacuum_cleaner": "Aspirator", + "keys_jangling": "Zornait de chei", + "coin": "Moneda", + "zipper": "Fermoar", + "electric_shaver": "Aparat de ras electric", + "shuffling_cards": "Amestecat de carti", + "typing": "Scrie", + "typewriter": "Mașină de scris", + "computer_keyboard": "Tastatura", + "writing": "Scrie", + "alarm": "Alarma", + "telephone": "Telefon", + "telephone_bell_ringing": "Sonerie de telefon", + "ringtone": "Ton de apel", + "dial_tone": "Ton", + "busy_signal": "Ocupat", + "alarm_clock": "Alarma de trezire", + "siren": "Sirena", + "smoke_detector": "Detector de fum", + "fire_alarm": "Alarma de incendiu", + "foghorn": "Sirena de ceata", + "whistle": "Fluierat", + "mechanisms": "Mecanism", + "gears": "Rotite", + "pulleys": "Scripeti", + "sewing_machine": "Mașină de cusut", + "mechanical_fan": "Ventilator mecanic", + "air_conditioning": "Aer Conditionat", + "printer": "Imprimanta", + "tools": "Unelte", + "hammer": "Ciocan", + "jackhammer": "Picamer", + "sawing": "Taiere", + "filing": "Umplere", + "power_tool": "Scule Electrice", + "drill": "Gaurire", + "explosion": "Explozie", + "artillery_fire": "Foc de artilerie", + "cap_gun": "Pistol cu capse", + "fireworks": "Foc de artificii", + "firecracker": "Pocnitoare", + "burst": "Izbucni", + "eruption": "Eruptie", + "boom": "Bum", + "wood": "Lemn", + "chop": "Reteza", + "splinter": "Aschie", + "crack": "Crapa", + "glass": "Geam", + "chink": "Fisura", + "shatter": "Sparge", + "silence": "Liniste", + "sound_effect": "Efect sonor", + "environmental_noise": "Zgomot de fundal", + "static": "Electrostatice", + "white_noise": "Zgomot alb", + "television": "Televizor", + "radio": "Radio", + "scream": "Tipa", + "chant": "Cântec", + "synthetic_singing": "Cântat sintetic", + "rapping": "Rap", + "shuffle": "Amestecă", + "biting": "Mușcare", + "gargling": "Gargară", + "stomach_rumble": "Gâdilitură stomacală", + "finger_snapping": "Pocnit din degete", + "heart_murmur": "Murmur inimă", + "chatter": "Conversații", + "children_playing": "Joacă copii", + "yip": "Lătrat", + "howl": "Urlet", + "bow_wow": "Ham-ham", + "growling": "Mârâit", + "whimper_dog": "Scheunat de câine", + "hiss": "Suflat", + "caterwaul": "Mieunat", + "livestock": "Animale de fermă", + "clip_clop": "Zgomot copite", + "neigh": "Nechezat", + "bleat": "Bâzâit", + "fowl": "Sunet păsări de curte", + "cluck": "Cotcodăcit", + "gobble": "Clocotit", + "honk": "Claxon", + "roaring_cats": "Răget", + "squawk": "Cârâit", + "coo": "Guguștiuc", + "caw": "Croncănit", + "hoot": "Huhuială", + "flapping_wings": "Fluturare aripi", + "patter": "Picurare", + "buzz": "Zumzăit", + "croak": "Orăcăit", + "rattle": "Zdrăngănit", + "whale_vocalization": "Vocalizare balenă", + "plucked_string_instrument": "Instrument cu corzi ciupite", + "steel_guitar": "Chitară cu bară metalică", + "strum": "Strângerea coardelor", + "sitar": "Sitar", + "zither": "Ziteră", + "ukulele": "Ukulele", + "organ": "Orgă", + "electronic_organ": "Orgă electronică", + "hammond_organ": "Orgă Hammond", + "sampler": "Eșantionator", + "harpsichord": "Clavecin", + "drum_machine": "Mașină de tobe", + "snare_drum": "Tobă mică", + "rimshot": "Bătaie pe tobă", + "drum_roll": "Rulou de tobe", + "timpani": "Timpane", + "tabla": "Tabla", + "cymbal": "Cinele", + "hi_hat": "Hi-Hat", + "wood_block": "Bloc de lemn", + "tubular_bells": "Clopote tubulare", + "mallet_percussion": "Percuție cu Baghete", + "marimba": "Marimbă", + "glockenspiel": "Glockenspiel", + "maraca": "Maracă", + "vibraphone": "Vibrafon", + "steelpan": "Steelpan", + "brass_instrument": "Instrument de alamă", + "french_horn": "Corn francez", + "bowed_string_instrument": "Instrument cu corzi cu arcuș", + "string_section": "Sectiunea corzi", + "pizzicato": "Pizzicato", + "double_bass": "Contrabas", + "wind_instrument": "Instrument de suflat", + "jingle_bell": "Zurgălău", + "chime": "Clopoțel", + "wind_chime": "Clopoțel de vânt", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Bol cântător", + "scratching": "Zgâriere", + "beatboxing": "Beatboxing", + "grunge": "Grunge", + "psychedelic_rock": "Rock psihedelic", + "rhythm_and_blues": "R&B", + "swing_music": "Muzică swing", + "bluegrass": "Bluegrass", + "middle_eastern_music": "Muziă din Orientul mijlociu", + "music_for_children": "Muzică penru copii", + "new-age_music": "Muzică New Age", + "afrobeat": "Afrobeat", + "carnatic_music": "Muzică carnatică", + "ska": "Ska", + "independent_music": "Muzică independentă", + "soundtrack_music": "Muzica soundtrack", + "video_game_music": "Muzică de jocuri video", + "tender_music": "Muzică tandră", + "exciting_music": "Muzică antrenantă", + "angry_music": "Muzică furioasă", + "scary_music": "Muzică de speriat", + "rain_on_surface": "Ploaie pe suprafață", + "stream": "Stream", + "gurgling": "Gâlgâit", + "crackle": "Trosnet", + "air_brake": "Frână pneumatică", + "reversing_beeps": "Bipuri de mers înapoi", + "emergency_vehicle": "Vehicul de urgență", + "rail_transport": "Transportul feroviar", + "train_whistle": "Fluier tren", + "train_horn": "Goarnă tren", + "railroad_car": "Vagon", + "light_engine": "Motor ușor", + "dental_drill's_drill": "Burghiu dentar", + "medium_engine": "Motor mediu", + "heavy_engine": "Motor greu", + "engine_knocking": "Bătăi ale motorului", + "ding-dong": "Ding-Dong", + "knock": "Cioc-cioc", + "tap": "Apasă", + "squeak": "Screchet", + "cupboard_open_or_close": "Ușă dulap deschisă sau închisă", + "drawer_open_or_close": "Sertar deschis sau închis", + "telephone_dialing": "Formare apel telefonic", + "civil_defense_siren": "Sirena de apărare civilă", + "buzzer": "Buzzer", + "steam_whistle": "Fluier cu aburi", + "ratchet": "Clichet", + "tick": "Tic-tac", + "tick-tock": "Tic-tac", + "cash_register": "Casa de marcat", + "single-lens_reflex_camera": "Cameră reflex cu un singur obiectiv", + "fusillade": "descărcare de focuri", + "pink_noise": "Zgomot roz", + "field_recording": "Înregistrare pe teren", + "sodeling": "*Sodeling*", + "chird": "*Chird*", + "change_ringing": "Schimbă soneria", + "shofar": "Șofar", + "liquid": "Lichid", + "splash": "Stropire", + "slosh": "Sloș", + "squish": "Plescăit", + "drip": "Picur", + "pour": "Toarnă", + "trickle": "Picurare", + "gush": "Șuvoi", + "fill": "Umplere", + "spray": "Pulverizare", + "pump": "Pompă", + "stir": "Amestecare", + "boiling": "Fierbere", + "sonar": "Sonar", + "arrow": "Săgeată", + "whoosh": "Whoosh", + "thump": "Bufnitură", + "thunk": "Buft", + "electronic_tuner": "Tuner electronic", + "effects_unit": "Efect de unitate", + "chorus_effect": "Efect de cor", + "basketball_bounce": "Săritură minge basket", + "bang": "Bubuitură", + "slap": "Pălmuială", + "whack": "Lovitură", + "smash": "Zdrobitură", + "breaking": "Rupere", + "bouncing": "Saritură", + "whip": "Bici", + "flap": "fâlfâit", + "scratch": "Zgâriat", + "scrape": "Răzuire", + "rub": "Frecare", + "roll": "Rostogolire", + "crushing": "Spargere", + "crumpling": "Șifonare", + "tearing": "Sfâșiere", + "beep": "Bip", + "ping": "Ping", + "ding": "Ding", + "clang": "zăngănit", + "squeal": "Țipăt", + "creak": "Scârțâit", + "rustle": "Foșnet", + "whir": "Vuiet", + "clatter": "Zdrăngăneală", + "sizzle": "Sfârâit", + "clicking": "Clănțănit", + "clickety_clack": "Clănțăneală", + "rumble": "Bubuit", + "plop": "Plop", + "hum": "murmur", + "zing": "Zing", + "boing": "Boing", + "crunch": "ronţăire", + "sine_wave": "Unda Sinusoidală", + "harmonic": "Armonic", + "chirp_tone": "ton de ciripit", + "pulse": "Puls", + "inside": "În interior", + "outside": "Afară", + "reverberation": "Reverberație", + "echo": "Ecou", + "noise": "Gălăgie", + "mains_hum": "Zumzet principal", + "distortion": "Distorsionare", + "sidetone": "Ton lateral", + "cacophony": "Cacofonie", + "throbbing": "Trepidant", + "vibration": "Vibrație" +} diff --git a/web/public/locales/ro/common.json b/web/public/locales/ro/common.json new file mode 100644 index 0000000..0ce39ae --- /dev/null +++ b/web/public/locales/ro/common.json @@ -0,0 +1,331 @@ +{ + "time": { + "untilForTime": "Până la {{time}}", + "untilForRestart": "Până la repornirea Frigate.", + "untilRestart": "Până la repornire", + "ago": "{{timeAgo}} în urmă", + "justNow": "Acum", + "today": "Astăzi", + "yesterday": "Ieri", + "last7": "Ultimele 7 zile", + "last14": "Ultimele 14 zile", + "last30": "Ultimele 30 de zile", + "thisWeek": "Săptămâna aceasta", + "lastWeek": "Săptămâna trecută", + "thisMonth": "Luna aceasta", + "lastMonth": "Luna trecută", + "5minutes": "5 minute", + "10minutes": "10 minute", + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyy" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "30minutes": "30 de minute", + "1hour": "1 oră", + "12hours": "12 ore", + "24hours": "24 de ore", + "pm": "PM", + "am": "AM", + "mo": "{{time}}lună", + "yr": "{{time}}an", + "year_one": "{{time}} an", + "year_few": "{{time}} ani", + "year_other": "{{time}} de ani", + "d": "{{time}}z", + "h": "{{time}}o", + "m": "{{time}}m", + "s": "{{time}}s", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "month_one": "{{time}} lună", + "month_few": "{{time}} luni", + "month_other": "{{time}} de luni", + "day_one": "{{time}} zi", + "day_few": "{{time}} zile", + "day_other": "{{time}} de zile", + "hour_one": "{{time}} oră", + "hour_few": "{{time}} ore", + "hour_other": "{{time}} de ore", + "minute_one": "{{time}} minut", + "minute_few": "{{time}} minute", + "minute_other": "{{time}} de minute", + "second_one": "{{time}} secundă", + "second_few": "{{time}} secunde", + "second_other": "{{time}} de secunde", + "inProgress": "În desfășurare", + "invalidStartTime": "Oră de început invalidă", + "invalidEndTime": "Oră de sfârșit invalidă", + "never": "Niciodată" + }, + "menu": { + "documentation": { + "title": "Documentație", + "label": "Documentație Frigate" + }, + "explore": "Căutare", + "uiPlayground": "UI Playground", + "faceLibrary": "Biblioteca de fețe", + "export": "Exportă", + "language": { + "ca": "Català (Catalană)", + "withSystem": { + "label": "Utilizează setările de limbă ale sistemului" + }, + "ja": "日本語 (Japoneză)", + "fa": "فارسی (Persiană)", + "pl": "Polski (Poloneză)", + "uk": "Українська (Ucrainiană)", + "he": "עברית (Ebraică)", + "yue": "粵語 (Cantoneză)", + "en": "English (Engleză)", + "de": "Deutsch (Germană)", + "es": "Español (Spaniolă)", + "zhCN": "简体中文 (Chineză simplificată)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Franceză)", + "ar": "العربية (Arabă)", + "pt": "Português (Portugheză)", + "ru": "Русский (Rusă)", + "tr": "Türkçe (Turcă)", + "it": "Italiano (Italiană)", + "nl": "Nederlands (Olandeză)", + "sv": "Svenska (Suedeză)", + "cs": "Čeština (Cehă)", + "nb": "Norsk Bokmål (Norvegiană Bokmål)", + "ko": "한국어 (Coreană)", + "vi": "Tiếng Việt (Vietnameză)", + "da": "Dansk (Daneză)", + "sk": "Slovenčina (Slovacă)", + "el": "Ελληνικά (Greacă)", + "ro": "Română (Română)", + "hu": "Magyar (Maghiară)", + "fi": "Suomi (Finlandeză)", + "th": "ไทย (Thailandeză)", + "ptBR": "Português brasileiro (Portugheză braziliană)", + "sr": "Српски (Sârbă)", + "sl": "Slovenščina (Slovenă)", + "lt": "Lietuvių (Lituaniană)", + "bg": "Български (Bulgară)", + "gl": "Galego (Galiciană)", + "id": "Bahasa Indonesia (Indoneziană)", + "ur": "اردو (Urdu)", + "hr": "Hrvatski (Croată)", + "bs": "Bosanski (Bosniacă)", + "zhHant": "繁體中文 (Chineză tradițională)" + }, + "theme": { + "default": "Implicit", + "highcontrast": "Contrast ridicat", + "label": "Temă", + "blue": "Albastru", + "green": "Verde", + "nord": "Nord", + "red": "Roșu" + }, + "user": { + "title": "Utilizator", + "account": "Cont", + "current": "Utilizator actual: {{user}}", + "logout": "Deconectare", + "anonymous": "anonim", + "setPassword": "Schimă parola" + }, + "live": { + "cameras": { + "count_one": "{{count}} cameră", + "count_few": "{{count}} camere", + "count_other": "{{count}} de camere", + "title": "Camere" + }, + "title": "Live", + "allCameras": "Toate camerele" + }, + "help": "Ajutor", + "system": "Sistem", + "systemMetrics": "Metrici de sistem", + "configuration": "Configurație", + "systemLogs": "Jurnale de sistem", + "settings": "Setări", + "configurationEditor": "Editor de configurație", + "languages": "Limba", + "appearance": "Aspect", + "darkMode": { + "label": "Mod luminozitate", + "light": "Luminos", + "dark": "Întunecat", + "withSystem": { + "label": "Utilizează setările de sistem pentru modul luminos sau întunecat" + } + }, + "withSystem": "Modul sistemului", + "restart": "Repornește Frigate", + "review": "Revizuire", + "classification": "Clasificare", + "chat": "Chat", + "actions": "Acțiuni", + "profiles": "Profile", + "features": "Funcționalități" + }, + "button": { + "cameraAudio": "Sunet cameră", + "apply": "Aplică", + "reset": "Resetare", + "done": "Gata", + "enabled": "Activat", + "copyCoordinates": "Copiază coordonate", + "on": "PORNIT", + "off": "OPRIT", + "edit": "Editează", + "delete": "Șterge", + "yes": "Da", + "no": "Nu", + "download": "Descarcă", + "info": "Informații", + "enable": "Activează", + "twoWayTalk": "Conversație bidirecțională", + "disabled": "Dezactivat", + "disable": "Dezactivează", + "save": "Salvează", + "saving": "Se salvează…", + "cancel": "Renunță", + "close": "Închide", + "copy": "Copiază", + "back": "Înapoi", + "history": "Istorie", + "fullscreen": "Ecran complet", + "exitFullscreen": "ieși din ecran complet", + "pictureInPicture": "Imagine în imagine", + "suspended": "Suspendat", + "unsuspended": "Nesuspendat", + "play": "Redă", + "unselect": "Deselectează", + "export": "Exportă", + "deleteNow": "Șterge acum", + "next": "Următorul", + "continue": "Continuă", + "add": "Adaugă", + "undo": "Anulează", + "copiedToClipboard": "Copiat în clipboard", + "modified": "Modificat", + "overridden": "Suprascris", + "resetToGlobal": "Resetare la valori Globale", + "resetToDefault": "Resetare la valori implicite", + "saveAll": "Salvează toate", + "savingAll": "Se salvează toate…", + "undoAll": "Anulează toate", + "applying": "Se aplică…", + "retry": "Reîncearcă" + }, + "unit": { + "speed": { + "mph": "mile/h", + "kph": "km/h" + }, + "length": { + "feet": "picioare", + "meters": "metri" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/oră", + "mbph": "MB/oră", + "gbph": "GB/oră" + } + }, + "label": { + "back": "Mergi înapoi", + "hide": "Ascunde {{item}}", + "show": "Afișează {{item}}", + "ID": "ID", + "none": "Niciuna", + "all": "Toate", + "other": "Altele" + }, + "selectItem": "Selectează {{item}}", + "pagination": { + "label": "paginare", + "next": { + "label": "Mergi la pagina următoare", + "title": "Următor" + }, + "previous": { + "title": "Anterior", + "label": "Meri la pagina anterioară" + }, + "more": "Mai multe pagini" + }, + "role": { + "viewer": "Vizualizator", + "desc": "Administratorii au acces complet la toate funcționalitățile din interfața Frigate. Vizualizatorii sunt limitați la vizualizarea camerelor, a elementelor de revizuire și a înregistrărilor istorice în interfață.", + "admin": "Administrator", + "title": "Rol" + }, + "toast": { + "copyUrlToClipboard": "URL-ul a fost copiat.", + "save": { + "title": "Salvează", + "error": { + "noMessage": "Nu s-au putut salva modificările de configurație", + "title": "Salvarea modificărilor de configurație a eșuat: {{errorMessage}}" + }, + "success": "Modificările de configurare au fost salvate cu succes." + } + }, + "accessDenied": { + "title": "Acces refuzat", + "desc": "Nu ai permisiunea să vizualizezi această pagină.", + "documentTitle": "Acces refuzat - Frigate" + }, + "notFound": { + "documentTitle": "Nu a fost găsit - Frigate", + "title": "404", + "desc": "Pagină negăsită" + }, + "readTheDocumentation": "Citește documentația", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} și {{1}}", + "many": "{{items}}, și {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opțional", + "internalID": "ID-ul Intern pe care Frigate îl folosește în configurație și în baza de date" + }, + "no_items": "Niciun element", + "validation_errors": "Erori de validare", + "credentialField": { + "savedPlaceholder": "Salvat — lasă necompletat pentru a păstra valoarea curentă" + } +} diff --git a/web/public/locales/ro/components/auth.json b/web/public/locales/ro/components/auth.json new file mode 100644 index 0000000..cc3b592 --- /dev/null +++ b/web/public/locales/ro/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nume utilizator", + "password": "Parola", + "login": "Logare", + "errors": { + "passwordRequired": "Parola este necesara", + "rateLimit": "Limita a fost depasita. Reincearca mai tarziu.", + "loginFailed": "Logare esuata", + "webUnknownError": "Eroare necunoscuta. Verifica logurile din consola.", + "usernameRequired": "Utilizatorul este necesar", + "unknownError": "Eroare necunoscuta. Verifica logurile." + }, + "firstTimeLogin": "Încercați să vă conectați pentru prima dată? Datele de autentificare sunt tipărite în jurnalele Frigate." + } +} diff --git a/web/public/locales/ro/components/camera.json b/web/public/locales/ro/components/camera.json new file mode 100644 index 0000000..7ee0be4 --- /dev/null +++ b/web/public/locales/ro/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "Grupuri de Camere", + "add": "Adaugă un grup de camere", + "edit": "Editează grupul de camere", + "delete": { + "label": "Șterge grupul de camere", + "confirm": { + "title": "Confirmă ștergerea", + "desc": "Ești sigur că dorești să ștergi grupul de camere {{name}}?" + } + }, + "name": { + "label": "Nume", + "placeholder": "Introdu un nume…", + "errorMessage": { + "mustLeastCharacters": "Numele grupului de cmere trebuie să conțină minim 2 caractere.", + "exists": "Numele grupului de camere există deja.", + "nameMustNotPeriod": "Numele grupului de camere nu trebuie să conțină punct.", + "invalid": "Nume invalid pentru grupul de camere." + } + }, + "cameras": { + "label": "Camere", + "desc": "Selectează camerele pentru acest grup." + }, + "icon": "Pictograma", + "success": "Grupul de camere ({{name}}) a fost salvat.", + "camera": { + "setting": { + "label": "Setările de streaming ale camerei", + "title": "{{cameraName}} Setări de streaming", + "stream": "Stream", + "placeholder": "Alege un stream", + "desc": "Schimbă opțiunile de streaming live pentru panoul acestui grup de camere. Aceste setări sunt specifice dispozitivului/browser-ului.", + "audioIsUnavailable": "Sunetul nu este disponibil pentru acest stream", + "audioIsAvailable": "Sunetul este disponibil pentru acest stream", + "audio": { + "tips": { + "title": "Sunetul trebuie să fie redat de camera ta și configurat în go2rtc pentru acest stream.", + "document": "Citește documentația " + } + }, + "streamMethod": { + "label": "Metoda de streaming", + "placeholder": "Alege o metodă de streaming", + "method": { + "noStreaming": { + "label": "Fără streaming", + "desc": "Imaginile camerelor se vor actualiza doar o dată pe minut și nu va exista streaming live." + }, + "smartStreaming": { + "label": "Streaming Inteligent (recomandat)", + "desc": "Streaming-ul inteligent va actualiza imaginea camerei o dată pe minut când nu există activitate detectabilă, pentru a economisi trafic de date și resurse. Când se detectează activitate, imaginea trece la streaming live." + }, + "continuousStreaming": { + "label": "Streaming continu", + "desc": { + "title": "Imaginea camerei va fi întotdeauna un stream live când este vizibilă pe panou, chiar dacă nu se detectează activitate.", + "warning": "Streaming-ul continuu poate provoca un consum mare de lățime de bandă și probleme de performanță. Folosește cu prudență." + } + } + } + }, + "compatibilityMode": { + "label": "Mod compatibilitate", + "desc": "Activează această opțiune doar dacă stream-ul live al camerei afișează artefacte de culoare și are o linie diagonală pe partea dreaptă a imaginii." + } + }, + "birdseye": "Birdseye" + }, + "showAll": "Arată toate grupurile de camere", + "showLess": "Arată mai puțin", + "editGroups": "Editează grupurile de camere" + }, + "debug": { + "options": { + "label": "Setări", + "title": "Optiuni", + "showOptions": "Arata Optiuni", + "hideOptions": "Ascunde opțiunile" + }, + "boundingBox": "Casetă de delimitare", + "timestamp": "Marcaj temporal", + "zones": "Zone", + "mask": "Mască", + "motion": "Mișcare", + "regions": "Regiuni", + "paths": "Căi" + } +} diff --git a/web/public/locales/ro/components/dialog.json b/web/public/locales/ro/components/dialog.json new file mode 100644 index 0000000..56bee65 --- /dev/null +++ b/web/public/locales/ro/components/dialog.json @@ -0,0 +1,210 @@ +{ + "restart": { + "title": "Ești sigur că dorești să repornești Frigate?", + "button": "Repornește", + "restarting": { + "title": "Frigate repornește", + "content": "Această pagină se va reâncărca automat în {{countdown}} secunde.", + "button": "Forțează acum reîncărcarea" + }, + "description": "Acest lucru va opri temporar Frigate în timpul repornirii." + }, + "explore": { + "plus": { + "review": { + "true": { + "label": "Confirma aceasta eticheta pentru Frigate Plus", + "true_one": "Asta e o {{label}}", + "true_few": "Astea sunt {{label}}", + "true_other": "Astea sunt {{label}}" + }, + "false": { + "label": "Nu confirma aceasta eticheta pentru Frigate Plus", + "false_one": "Asta nu este {{label}}", + "false_few": "Astea nu sunt {{label}}", + "false_other": "Astea nu sunt {{label}}" + }, + "state": { + "submitted": "Trimis" + }, + "question": { + "label": "Confirmă această etichetă pentru Frigate Plus", + "ask_a": "Este acest obiect un {{label}}?", + "ask_an": "Este acest obiect un {{label}}?", + "ask_full": "Este acest obiect un {{untranslatedLabel}} ({{translatedLabel}})?" + } + }, + "submitToPlus": { + "label": "Trimite catre Frigate+", + "desc": "Obiectele din locațiile pe care dorești să le eviți nu sunt false-pozitive. Marcarea lor ca false-pozitive va induce confuzie modelul." + } + }, + "video": { + "viewInHistory": "Vezi în istoric" + } + }, + "recording": { + "button": { + "deleteNow": "Șterge acum", + "export": "Exportă", + "markAsReviewed": "Marchează ca revizuit", + "markAsUnreviewed": "Marchează ca nerevizuit" + }, + "confirmDelete": { + "toast": { + "success": "Înregistrările video asociate elementelor de revizuire selectate au fost șterse cu succes.", + "error": "Ștergerea a eșuat: {{error}}" + }, + "title": "Confirmă ștergerea", + "desc": { + "selected": "Ești sigur că vrei să ștergi toate videoclipurile înregistrate asociate acestui element de revizuire?

    Ține apăsată tasta Shift pentru a sări peste această confirmare pe viitor." + } + }, + "shareTimestamp": { + "label": "Partajează timestamp-ul", + "title": "Partajează timestamp-ul", + "description": "Partajează un URL cu timestamp al poziției actuale din player sau alege un timestamp personalizat. Reține că acesta nu este un URL de partajare publică și este accesibil doar utilizatorilor care au acces la Frigate și la această cameră.", + "custom": "Timestamp personalizat", + "button": "Partajează URL-ul cu timestamp", + "shareTitle": "Timestamp review Frigate: {{camera}}" + } + }, + "export": { + "time": { + "custom": "personalizat", + "fromTimeline": "Selectează din cronologie", + "lastHour_one": "Ultima oră", + "lastHour_few": "Ultimele {{count}} ore", + "lastHour_other": "Ultimele {{count}} ore", + "start": { + "title": "Ora de început", + "label": "Selectează ora de început" + }, + "end": { + "title": "Oră terminare", + "label": "Selectează ora de terminare" + } + }, + "name": { + "placeholder": "Denumește exportul" + }, + "select": "Selectează", + "export": "Exportă", + "selectOrExport": "Selectează sau exportă", + "toast": { + "success": "Exportul a început cu succes. Vizualizați fișierul pe pagina de exporturi.", + "error": { + "failed": "Nu s-a putut adăuga exportul în coadă: {{error}}", + "endTimeMustAfterStartTime": "Ora de sfârșit trebuie să fie după ora de început", + "noVaildTimeSelected": "Nu a fost selectat un interval de timp valid" + }, + "view": "Vizualizează", + "queued": "Export pus la coadă. Vezi progresul pe pagina de exporturi.", + "batchSuccess_one": "A început 1 export. Se deschide cazul acum.", + "batchSuccess_few": "Au început {{count}} exporturi. Se deschide cazul acum.", + "batchSuccess_other": "Au început {{count}} de exporturi. Se deschide cazul acum.", + "batchPartial": "Au început {{successful}} din {{total}} exporturi. Camere eșuate: {{failedCameras}}", + "batchFailed": "Eșec la pornirea a {{total}} exporturi. Camere eșuate: {{failedCameras}}", + "batchQueuedSuccess_one": "1 export pus la coadă. Se deschide cazul acum.", + "batchQueuedSuccess_few": "{{count}} exporturi puse la coadă. Se deschide cazul acum.", + "batchQueuedSuccess_other": "{{count}} de exporturi puse la coadă. Se deschide cazul acum.", + "batchQueuedPartial": "S-au pus la coadă {{successful}} din {{total}} exporturi. Camere eșuate: {{failedCameras}}", + "batchQueueFailed": "Eșec la punerea la coadă a {{total}} exporturi. Camere eșuate: {{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "Salvează exportul", + "previewExport": "Previzualizează exportul", + "queueingExport": "Se adaugă exportul la coadă...", + "useThisRange": "Folosește acest interval" + }, + "case": { + "label": "Caz", + "placeholder": "Selectează caz", + "newCaseOption": "Creează un caz nou", + "newCaseNamePlaceholder": "Nume caz nou", + "newCaseDescriptionPlaceholder": "Descrierea cazului", + "nonAdminHelp": "Un caz nou va fi creat pentru aceste exporturi." + }, + "queueing": "Se adaugă exportul la coadă...", + "tabs": { + "export": "O singură cameră", + "multiCamera": "Mai multe camere" + }, + "multiCamera": { + "timeRange": "Interval de timp", + "selectFromTimeline": "Selectează din timeline", + "cameraSelection": "Camere", + "cameraSelectionHelp": "Camerele cu obiecte urmărite în acest interval de timp sunt pre-selectate", + "checkingActivity": "Se verifică activitatea camerei...", + "noCameras": "Nu sunt camere disponibile", + "detectionCount_one": "1 obiect urmărit", + "detectionCount_few": "{{count}} obiecte urmărite", + "detectionCount_other": "{{count}} de obiecte urmărite", + "nameLabel": "Nume export", + "namePlaceholder": "Nume de bază opțional pentru aceste exporturi", + "queueingButton": "Se adaugă exporturile la coadă...", + "exportButton_one": "Exportă 1 cameră", + "exportButton_few": "Exportă {{count}} camere", + "exportButton_other": "Exportă {{count}} de camere" + }, + "multi": { + "title_one": "Exportă o revizuire", + "title_few": "Exportă {{count}} revizuiri", + "title_other": "Exportă {{count}} de revizuiri", + "description": "Exportă fiecare revizuire selectată. Toate exporturile vor fi grupate sub un singur caz.", + "descriptionNoCase": "Exportă fiecare revizuire selectată.", + "caseNamePlaceholder": "Export revizuiri - {{date}}", + "exportButton_one": "Exportă o revizuire", + "exportButton_few": "Exportă {{count}} revizuiri", + "exportButton_other": "Exportă {{count}} de revizuiri", + "exportingButton": "Se exportă...", + "toast": { + "started_one": "A început 1 export. Se deschide cazul acum.", + "started_few": "Au început {{count}} exporturi. Se deschide cazul acum.", + "started_other": "Au început {{count}} de exporturi. Se deschide cazul acum.", + "startedNoCase_one": "A început 1 export.", + "startedNoCase_few": "Au început {{count}} exporturi.", + "startedNoCase_other": "Au început {{count}} de exporturi.", + "partial": "Au început {{successful}} din {{total}} exporturi. Au eșuat: {{failedItems}}", + "failed": "Eșec la pornirea a {{total}} exporturi. Au eșuat: {{failedItems}}" + } + } + }, + "streaming": { + "label": "Stream", + "restreaming": { + "disabled": "Restreaming-ul nu este activat pentru această cameră.", + "desc": { + "title": "Configurează go2rtc pentru opțiuni suplimentare de vizualizare live și audio pentru această cameră.", + "readTheDocumentation": "Citește documentația" + } + }, + "showStats": { + "label": "Afișează statistici streaming", + "desc": "Activează această opțiune pentru a afișa statisticile de streaming ca un overlay peste stream-ul camerei." + }, + "debugView": "Vizualizator depanare" + }, + "search": { + "saveSearch": { + "label": "Salvează căutarea", + "desc": "Alege un nume pentru această căutare salvată.", + "placeholder": "Introdu un nume pentru căutarea ta", + "overwrite": "{{searchName}} există deja. Salvarea va suprascrie valoarea existentă.", + "success": "Căutarea ({{searchName}}) a fost salvată.", + "button": { + "save": { + "label": "Salvează această căutare" + } + } + } + }, + "imagePicker": { + "selectImage": "Selectează miniatura unui obiect urmărit", + "search": { + "placeholder": "Caută după etichetă sau subetichetă..." + }, + "noImages": "Nu s-au găsit miniaturi pentru această cameră", + "unknownLabel": "Imaginea declanșator salvată" + } +} diff --git a/web/public/locales/ro/components/filter.json b/web/public/locales/ro/components/filter.json new file mode 100644 index 0000000..a0438b5 --- /dev/null +++ b/web/public/locales/ro/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filtru", + "labels": { + "label": "Etichete", + "all": { + "title": "Toate etichetele", + "short": "Etichete" + }, + "count_one": "Etichetă {{count}}", + "count_other": "{{count}} etichete" + }, + "dates": { + "selectPreset": "Selectează o presetare…", + "all": { + "title": "Toate datele", + "short": "Date" + } + }, + "zones": { + "label": "Zone", + "all": { + "title": "Toate zonele", + "short": "Zone" + } + }, + "reset": { + "label": "Resetează filtrele la valorile implicite" + }, + "timeRange": "Interval de timp", + "subLabels": { + "label": "Sub-etichete", + "all": "Toate sub-etichetele" + }, + "more": "Mai multe filtre", + "score": "Scor", + "estimatedSpeed": "Viteza estimată ({{unit}})", + "features": { + "label": "Caracteristici", + "hasSnapshot": "Are snapshot", + "hasVideoClip": "Are un videoclip", + "submittedToFrigatePlus": { + "label": "Trimis către Frigate+", + "tips": "Trebuie mai întâi să filtrezi obiectele urmărite care au un snapshot.

    Obiectele urmărite fără snapshot nu pot fi trimise către Frigate+." + } + }, + "sort": { + "label": "Sortează", + "dateAsc": "Dată (crescător)", + "dateDesc": "Dată (descrescător)", + "scoreAsc": "Scor obiect (crescător)", + "scoreDesc": "Scor obiect (descrescător)", + "speedAsc": "Viteză estimată (crescător)", + "speedDesc": "Viteză estimată (descrescător)", + "relevance": "Relevanță" + }, + "cameras": { + "all": { + "short": "Camere", + "title": "Toate camerele" + }, + "label": "Filtru camere" + }, + "review": { + "showReviewed": "Afișează cele revizuite" + }, + "motion": { + "showMotionOnly": "Afișează doar mișcarea" + }, + "explore": { + "settings": { + "title": "Setări", + "defaultView": { + "title": "Vizualizare implicită", + "unfilteredGrid": "Grilă nefiltrată", + "desc": "Când nu sunt selectate filtre, afișează un rezumat al celor mai recente obiecte urmărite pentru fiecare etichetă sau afișează o grilă nefiltrată.", + "summary": "Sumar" + }, + "gridColumns": { + "title": "Coloane grilă", + "desc": "Selectează numărul de coloane în vizualizarea grilă." + }, + "searchSource": { + "label": "Sursa căutării", + "desc": "Alege dacă dorești să cauți în miniaturi sau în descrierile obiectelor urmărite.", + "options": { + "thumbnailImage": "Imagine miniatură", + "description": "Descriere" + } + } + }, + "date": { + "selectDateBy": { + "label": "Selectează o dată pentru filtrare" + } + } + }, + "logSettings": { + "label": "Filtrează nivelul jurnalului", + "filterBySeverity": "Filtrează jurnalele după severitate", + "loading": { + "title": "Încărcare continuă", + "desc": "Când panoul de jurnale este derulat până jos, noile jurnale sunt afișate automat pe măsură ce sunt adăugate." + }, + "disableLogStreaming": "Dezactivează încărcarea continuă", + "allLogs": "Toate jurnalele" + }, + "trackedObjectDelete": { + "title": "Confirmă ștergerea", + "desc": "Ștergerea acestor {{objectLength}} obiecte urmărite elimină snapshot-ul, orice înglobări salvate și orice înregistrări asociate ciclului de viață al obiectului. Filmările înregistrate ale acestor obiecte urmărite în vizualizarea Istoric NU vor fi șterse.

    Ești sigur că dorești să continui?

    Țineți apăsată tasta Shift pentru a sări peste acest dialog în viitor.", + "toast": { + "success": "Obiectele urmărite au fost șterse cu succes.", + "error": "Ștergerea obiectelor urmărite a eșuat: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrează după masca de zonă" + }, + "recognizedLicensePlates": { + "title": "Numere de înmatriculare recunoscute", + "noLicensePlatesFound": "Nu s-au găsit plăcuțe de înmatriculare.", + "selectPlatesFromList": "Selectează una sau mai multe plăcuțe din listă.", + "loading": "Se încarcă numerele de înmatriculare recunoscute…", + "placeholder": "Caută plăcuțe de înmatriculare…", + "loadFailed": "Nu s-au putut încărca numerele de înmatriculare recunoscute.", + "selectAll": "Selectează tot", + "clearAll": "Elimină tot" + }, + "classes": { + "label": "Clase", + "all": { + "title": "Toate clasele" + }, + "count_one": "{{count}} Clasă", + "count_other": "{{count}} Clase" + }, + "attributes": { + "label": "Atribute de clasificare", + "all": "Toate atributele" + } +} diff --git a/web/public/locales/ro/components/icons.json b/web/public/locales/ro/components/icons.json new file mode 100644 index 0000000..0d8ee62 --- /dev/null +++ b/web/public/locales/ro/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selectează o pictogramă", + "search": { + "placeholder": "Caută o pictogramă…" + } + } +} diff --git a/web/public/locales/ro/components/input.json b/web/public/locales/ro/components/input.json new file mode 100644 index 0000000..8faa621 --- /dev/null +++ b/web/public/locales/ro/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Descarca Video", + "toast": { + "success": "A inceput descarcarea clipului ce contine articolul revizuit." + } + } + } +} diff --git a/web/public/locales/ro/components/player.json b/web/public/locales/ro/components/player.json new file mode 100644 index 0000000..71cb3cb --- /dev/null +++ b/web/public/locales/ro/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "Nu au fost gasite inregistrari in perioada de timp mentionata", + "noPreviewFound": "Nu a fost gasita o Previzualizare", + "noPreviewFoundFor": "Nu există previzualizari pentru {{cameraName}}", + "submitFrigatePlus": { + "title": "Trimiteti acest cadru catre Frigate+?", + "submit": "Trimite", + "previewError": "Nu s-a putut încărca previzualizarea snapshot-ului. S-ar putea ca înregistrarea să nu fie disponibilă în acest moment." + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 sau mai recent este necesar pentru acest tip de stream live.", + "streamOffline": { + "title": "Stream Offline", + "desc": "Nici un cadru nu a fost receptionat de la streamul {{cameraName}} detect, verifica logurile de eroare" + }, + "cameraDisabled": "Camera este dezactivata", + "stats": { + "streamType": { + "title": "Tip Stream:", + "short": "Tip" + }, + "bandwidth": { + "title": "Latime de Banda:", + "short": "Latime de Banda" + }, + "latency": { + "title": "Latenta:", + "value": "{{seconds}} secunde", + "short": { + "title": "Latenta", + "value": "{{seconds}} sec" + } + }, + "totalFrames": "Total Cadre:", + "droppedFrames": { + "title": "Cadre Pierdute:", + "short": { + "title": "Pierdut", + "value": "{{droppedFrames}} cadre" + } + }, + "decodedFrames": "Cadre Decodate:", + "droppedFrameRate": "Rata de Cadre Pierdute:" + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "Eraoare trimitere Cadru catre Frigate+" + }, + "success": { + "submittedFrigatePlus": "Cadru trimis cu Succes catre Frigate+" + } + }, + "cameraOff": "Camera este oprită" +} diff --git a/web/public/locales/ro/config/cameras.json b/web/public/locales/ro/config/cameras.json new file mode 100644 index 0000000..7c68458 --- /dev/null +++ b/web/public/locales/ro/config/cameras.json @@ -0,0 +1,961 @@ +{ + "label": "Configurație Cameră", + "name": { + "label": "Nume cameră", + "description": "Numele camerei este obligatoriu" + }, + "friendly_name": { + "label": "Nume prietenos", + "description": "Numele camerei afișat în interfața Frigate" + }, + "enabled": { + "label": "Activată", + "description": "Activată" + }, + "audio": { + "label": "Detecție audio", + "description": "Setări pentru detectarea evenimentelor bazate pe sunet pentru această cameră.", + "enabled": { + "label": "Activare detecție audio", + "description": "Activează sau dezactivează detecția evenimentelor audio pentru această cameră." + }, + "max_not_heard": { + "label": "Timeout final", + "description": "Secunde fără tipul audio configurat înainte ca evenimentul să fie încheiat." + }, + "min_volume": { + "label": "Volum minim", + "description": "Pragul minim de volum RMS; valorile mici cresc sensibilitatea (ex: 200 ridicată, 500 medie, 1000 scăzută)." + }, + "listen": { + "label": "Tipuri ascultate", + "description": "Lista de evenimente audio de detectat (ex: lătrat, alarmă_incendiu, vorbire)." + }, + "filters": { + "label": "Filtre audio", + "description": "Setări de filtrare per tip audio, cum ar fi pragul de încredere.", + "threshold": { + "label": "Încredere audio minimă", + "description": "Pragul minim de încredere pentru ca evenimentul audio să fie luat în considerare." + } + }, + "enabled_in_config": { + "label": "Stare audio originală", + "description": "Indică dacă detecția audio a fost activată inițial în fișierul de configurare static." + }, + "num_threads": { + "label": "Thread-uri detecție", + "description": "Numărul de thread-uri pentru procesarea detecției audio." + } + }, + "audio_transcription": { + "label": "Transcriere audio", + "description": "Setări pentru transcrierea audio live și a vorbirii pentru evenimente.", + "enabled": { + "label": "Activare transcriere", + "description": "Activează sau dezactivează transcrierea declanșată manual pentru evenimentele audio." + }, + "enabled_in_config": { + "label": "Stare transcriere originală" + }, + "live_enabled": { + "label": "Transcriere live", + "description": "Activează streaming-ul de transcriere live pe măsură ce sunetul e recepționat." + } + }, + "birdseye": { + "label": "Birdseye", + "description": "Setări pentru vizualizarea compusă Birdseye care combină mai multe stream-uri într-un singur layout.", + "enabled": { + "label": "Activare Birdseye", + "description": "Activează sau dezactivează funcția Birdseye." + }, + "mode": { + "label": "Mod urmărire", + "description": "Modul de includere a camerelor în Birdseye: 'objects', 'motion' sau 'continuous'." + }, + "order": { + "label": "Poziție", + "description": "Poziția numerică ce controlează ordinea camerei în layout-ul Birdseye." + } + }, + "detect": { + "label": "Detecție obiecte", + "description": "Setări pentru rolul de detecție folosit pentru a rula recunoașterea obiectelor și trackerele.", + "enabled": { + "label": "Activează detecția de obiecte", + "description": "Activează sau dezactivează detecția obiectelor pentru această cameră." + }, + "height": { + "label": "Înălțime detect", + "description": "Înălțimea cadrelor pentru stream-ul de detect; lasă gol pentru rezoluția nativă." + }, + "width": { + "label": "Lățime detect", + "description": "Lățimea cadrelor pentru stream-ul de detect; lasă gol pentru rezoluția nativă." + }, + "fps": { + "label": "FPS detect", + "description": "FPS-ul dorit pentru detecție; valori mici reduc consumul CPU (recomandat 5, max 10 pentru obiecte foarte rapide)." + }, + "min_initialized": { + "label": "Cadre minime inițializare", + "description": "Numărul de detecții consecutive necesare înainte de a crea un obiect urmărit. Crește valoarea pentru a reduce alarmele false." + }, + "max_disappeared": { + "label": "Cadre maxime dispariție", + "description": "Numărul de cadre fără detecție înainte ca un obiect urmărit să fie considerat dispărut." + }, + "stationary": { + "label": "Configurație obiecte staționare", + "description": "Setări pentru gestionarea obiectelor care rămân nemișcate o perioadă.", + "interval": { + "label": "Interval staționar", + "description": "Cât de des (în cadre) se verifică prezența unui obiect staționar." + }, + "threshold": { + "label": "Prag staționar", + "description": "Numărul de cadre fără schimbare de poziție pentru a marca un obiect ca staționar." + }, + "max_frames": { + "label": "Cadre maxime", + "description": "Limitează cât timp sunt urmărite obiectele staționare înainte de a fi ignorate.", + "default": { + "label": "Cadre maxime implicit", + "description": "Valoarea implicită pentru urmărirea obiectelor staționare." + }, + "objects": { + "label": "Cadre maxime per obiect", + "description": "Suprascrieri per obiect pentru durata urmăririi staționare." + } + }, + "classifier": { + "label": "Activare clasificator vizual", + "description": "Folosește un clasificator vizual pentru a detecta obiectele cu adevărat staționare, chiar dacă chenarul oscilează." + } + }, + "annotation_offset": { + "label": "Offset adnotare", + "description": "Milisecunde pentru a decala adnotările de detecție pentru a alinia mai bine chenarele cu înregistrarea." + } + }, + "face_recognition": { + "label": "Recunoaștere facială", + "description": "Setări pentru detecția și recunoașterea fețelor pentru această cameră.", + "enabled": { + "label": "Activare recunoaștere facială", + "description": "Activează sau dezactivează recunoașterea facială." + }, + "min_area": { + "label": "Arie minimă față", + "description": "Aria minimă (pixeli) pentru a încerca recunoașterea." + } + }, + "ffmpeg": { + "label": "Stream-uri (FFmpeg)", + "description": "Intrările stream-ului camerei și opțiunile FFmpeg, inclusiv calea către binar, argumentele, hwaccel și argumentele de ieșire per rol.", + "path": { + "label": "Cale FFmpeg", + "description": "Calea către binarul FFmpeg sau un alias de versiune (\"7.0\" sau \"8.0\")." + }, + "global_args": { + "label": "Argumente globale FFmpeg", + "description": "Argumente globale pasate proceselor FFmpeg." + }, + "hwaccel_args": { + "label": "Argumente accelerare hardware", + "description": "Argumente pentru accelerarea hardware. Se recomandă presetările specifice furnizorului." + }, + "input_args": { + "label": "Argumente intrare", + "description": "Argumente aplicate stream-urilor de intrare FFmpeg." + }, + "output_args": { + "label": "Argumente ieșire", + "description": "Argumente de ieșire implicite pentru diverse roluri (detect, record).", + "detect": { + "label": "Argumente ieșire detect", + "description": "Argumente implicite pentru stream-urile cu rol detect." + }, + "record": { + "label": "Argumente ieșire record", + "description": "Argumente implicite pentru stream-urile cu rol record." + } + }, + "retry_interval": { + "label": "Timp reîncercare FFmpeg", + "description": "Secunde de așteptare înainte de reconectarea unui stream după o eroare. Implicit 10." + }, + "apple_compatibility": { + "label": "Compatibilitate Apple", + "description": "Activează tag-ul HEVC pentru compatibilitate mai bună cu playerele Apple la înregistrările H.265." + }, + "gpu": { + "label": "Index GPU", + "description": "Indexul GPU implicit folosit pentru accelerarea hardware." + }, + "inputs": { + "label": "Intrări cameră", + "description": "Listă de definiții pentru stream-urile de intrare (căi și roluri).", + "path": { + "label": "Cale intrare", + "description": "URL-ul sau calea stream-ului de intrare al camerei." + }, + "roles": { + "label": "Roluri intrare", + "description": "Rolurile atribuite acestui stream de intrare." + }, + "global_args": { + "label": "Argumente globale FFmpeg", + "description": "Argumente globale pentru acest stream de intrare." + }, + "hwaccel_args": { + "label": "Argumente accelerare hardware", + "description": "Argumente de accelerare hardware pentru acest stream." + }, + "input_args": { + "label": "Argumente intrare", + "description": "Argumente specifice acestui stream." + } + } + }, + "live": { + "label": "Redare live", + "description": "Setări folosite de interfața web pentru a controla selecția, rezoluția și calitatea stream-ului live.", + "streams": { + "label": "Nume stream-uri live", + "description": "Maparea numelor de stream-uri configurate către numele restream/go2rtc folosite live." + }, + "height": { + "label": "Înălțime live", + "description": "Înălțimea (pixeli) pentru redarea jsmpeg în UI; trebuie să fie <= înălțimea stream-ului de detect." + }, + "quality": { + "label": "Calitate live", + "description": "Calitatea encodării pentru stream-ul jsmpeg (1 maxim, 31 minim)." + } + }, + "lpr": { + "label": "Recunoaștere numere înmatriculare", + "description": "Setări pentru recunoașterea numerelor de înmatriculare, inclusiv praguri de detecție, formatare și numere cunoscute.", + "enabled": { + "label": "Activare LPR", + "description": "Activează sau dezactivează LPR pe această cameră." + }, + "expire_time": { + "label": "Secunde expirare", + "description": "Timpul în secunde după care un număr nevăzut este expirat din tracker (doar pentru camerele LPR dedicate)." + }, + "min_area": { + "label": "Arie minimă plăcuță", + "description": "Aria minimă (pixeli) pentru a încerca recunoașterea." + }, + "enhancement": { + "label": "Nivel îmbunătățire", + "description": "Nivelul de îmbunătățire (0-10) aplicat decupajelor cu numere înainte de OCR; valorile mai mari nu îmbunătățesc mereu rezultatele, iar nivelurile peste 5 pot funcționa doar cu numerele pe timp de noapte și trebuie folosite cu atenție." + } + }, + "motion": { + "label": "Detecție mișcare", + "description": "Setări implicite pentru detecția mișcării pentru această cameră.", + "enabled": { + "label": "Activare detecție mișcare", + "description": "Activează sau dezactivează detecția mișcării pentru această cameră." + }, + "threshold": { + "label": "Prag mișcare", + "description": "Pragul de diferență între pixeli; valorile mari reduc sensibilitatea (1-255)." + }, + "lightning_threshold": { + "label": "Prag fulger/lumină", + "description": "Prag pentru detectarea și ignorarea vârfurilor scurte de lumină (o valoare mai mică este mai sensibilă, valori între 0.3 și 1.0). Acest lucru nu oprește complet detecția mișcării; doar determină detectorul să nu mai analizeze cadre suplimentare odată ce pragul este depășit. Înregistrările bazate pe mișcare sunt create în continuare în timpul acestor evenimente." + }, + "improve_contrast": { + "label": "Îmbunătățire contrast", + "description": "Aplică o îmbunătățire a contrastului înainte de analiza mișcării pentru a ajuta detecția." + }, + "contour_area": { + "label": "Arie contur", + "description": "Aria minimă a conturului în pixeli pentru a fi considerat mișcare." + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "Factor de blending alpha folosit în diferențierea cadrelor." + }, + "frame_alpha": { + "label": "Cadru alfa", + "description": "Valoarea alpha pentru amestecarea cadrelor la preprocesarea mișcării." + }, + "frame_height": { + "label": "Înălțime cadru", + "description": "Înălțimea la care sunt scalate cadrele pentru calculul mișcării." + }, + "mask": { + "label": "Coordonate mască", + "description": "Coordonate x,y care definesc poligonul măștii de mișcare." + }, + "mqtt_off_delay": { + "label": "Întârziere MQTT off", + "description": "Secunde de așteptare după ultima mișcare înainte de a trimite starea 'off' prin MQTT." + }, + "enabled_in_config": { + "label": "Stare mișcare originală", + "description": "Indică dacă detecția mișcării a fost activă în configurația inițială." + }, + "raw_mask": { + "label": "Mască brută" + }, + "skip_motion_threshold": { + "label": "Ignoră pragul de mișcare", + "description": "Dacă este setat la o valoare între 0.0 și 1.0, și mai mult decât această fracție din imagine se modifică într-un singur cadru, detectorul nu va returna casete de mișcare și se va recalibra imediat. Acest lucru poate economisi CPU și reduce rezultatele fals pozitive în timpul fulgerelor, furtunilor etc., dar poate rata evenimente reale, cum ar fi o cameră PTZ care urmărește automat un obiect. Compromisul este între a pierde câțiva megabytes de înregistrări versus a revizui câteva clipuri scurte. Lasă nesetat (None) pentru a dezactiva această funcție." + } + }, + "objects": { + "label": "Obiecte", + "description": "Setări implicite pentru urmărire, inclusiv ce etichete se urmăresc și filtrele per obiect.", + "track": { + "label": "Obiecte de urmărit", + "description": "Lista etichetelor de obiecte de urmărit pentru această cameră." + }, + "filters": { + "label": "Filtre obiecte", + "description": "Filtre pentru a reduce alarmele false (arie, raport, încredere).", + "min_area": { + "label": "Arie minimă obiect", + "description": "Aria minimă a chenarului (pixeli sau procent)." + }, + "max_area": { + "label": "Arie maximă obiect", + "description": "Aria maximă a chenarului (pixeli sau procent)." + }, + "min_ratio": { + "label": "Raport aspect minim", + "description": "Raportul minim lățime/înălțime pentru chenar." + }, + "max_ratio": { + "label": "Raport aspect maxim", + "description": "Raportul maxim lățime/înălțime pentru chenar." + }, + "threshold": { + "label": "Prag încredere", + "description": "Încrederea medie necesară pentru a considera obiectul valid." + }, + "min_score": { + "label": "Scor minim", + "description": "Încrederea minimă la un singur cadru pentru a număra obiectul." + }, + "mask": { + "label": "Mască filtru", + "description": "Poligonul unde se aplică acest filtru în cadru." + }, + "raw_mask": { + "label": "Mască brută" + } + }, + "mask": { + "label": "Mască obiect", + "description": "Mască pentru a preveni detecția obiectelor în anumite zone." + }, + "raw_mask": { + "label": "Mască brută" + }, + "genai": { + "label": "Configurație obiecte GenAI", + "description": "Opțiuni GenAI pentru descrierea obiectelor urmărite și trimiterea cadrelor.", + "enabled": { + "label": "Activare GenAI", + "description": "Activează generarea de descrieri prin GenAI pentru obiectele urmărite." + }, + "use_snapshot": { + "label": "Folosește snapshot-uri", + "description": "Folosește snapshot-urile obiectelor în loc de miniaturi pentru GenAI." + }, + "prompt": { + "label": "Prompt descriere", + "description": "Șablonul de prompt implicit pentru descrierile GenAI." + }, + "object_prompts": { + "label": "Prompt-uri per obiect", + "description": "Prompt-uri personalizate pentru anumite etichete de obiecte." + }, + "objects": { + "label": "Obiecte GenAI", + "description": "Lista etichetelor de obiecte care vor fi trimise la GenAI." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Zonele prin care trebuie să treacă obiectele pentru a genera descrieri." + }, + "debug_save_thumbnails": { + "label": "Salvează miniaturile", + "description": "Salvează miniaturile trimise la GenAI pentru depanare." + }, + "send_triggers": { + "label": "Trigger-e GenAI", + "description": "Definește când sunt trimise cadrele la GenAI (la final, după actualizări etc.).", + "tracked_object_end": { + "label": "Trimite la final", + "description": "Trimite cererea la GenAI când urmărirea obiectului s-a terminat." + }, + "after_significant_updates": { + "label": "Trigger GenAI timpuriu", + "description": "Trimite la GenAI după un număr de actualizări semnificative ale obiectului." + } + }, + "enabled_in_config": { + "label": "Stare GenAI originală", + "description": "Indică dacă GenAI a fost activat în configurația inițială." + } + } + }, + "record": { + "label": "Înregistrare", + "description": "Setări de înregistrare și retenție pentru această cameră.", + "enabled": { + "label": "Activare înregistrare", + "description": "Activează sau dezactivează înregistrarea pentru această cameră." + }, + "expire_interval": { + "label": "Interval curățare înregistrări", + "description": "Minute între trecerile de curățare a segmentelor expirate." + }, + "continuous": { + "label": "Retenție continuă", + "description": "Zile de păstrare a înregistrărilor indiferent de obiecte sau mișcare. Pune 0 pentru a păstra doar alerte/detecții.", + "days": { + "label": "Zile retenție", + "description": "Numărul de zile pentru păstrare." + } + }, + "motion": { + "label": "Retenție mișcare", + "description": "Zile de păstrare pentru înregistrările declanșate de mișcare.", + "days": { + "label": "Zile retenție", + "description": "Numărul de zile pentru păstrare." + } + }, + "detections": { + "label": "Retenție detecții", + "description": "Setări pentru evenimentele de detecție, inclusiv duratele pre/post captură.", + "pre_capture": { + "label": "Secunde pre-captură", + "description": "Secunde incluse înainte de evenimentul detectat." + }, + "post_capture": { + "label": "Secunde post-captură", + "description": "Secunde incluse după încheierea evenimentului." + }, + "retain": { + "label": "Retenție eveniment", + "description": "Setări de retenție pentru clipurile cu detecții.", + "days": { + "label": "Zile retenție", + "description": "Numărul de zile de păstrare." + }, + "mode": { + "label": "Mod retenție", + "description": "Mod: 'all' (tot), 'motion' (doar segmente cu mișcare) sau 'active_objects' (doar cu obiecte active)." + } + } + }, + "alerts": { + "label": "Retenție alerte", + "description": "Setări de retenție pentru evenimentele de tip alertă.", + "pre_capture": { + "label": "Secunde pre-captură", + "description": "Secunde incluse înainte de alertă." + }, + "post_capture": { + "label": "Secunde post-captură", + "description": "Secunde incluse după alertă." + }, + "retain": { + "label": "Retenție eveniment", + "description": "Setări de păstrare pentru alerte.", + "days": { + "label": "Zile retenție", + "description": "Numărul de zile pentru păstrare." + }, + "mode": { + "label": "Mod retenție", + "description": "Modul de păstrare a segmentelor." + } + } + }, + "export": { + "label": "Configurație export", + "description": "Setări pentru exportul înregistrărilor (timelapse, accelerare hardware).", + "hwaccel_args": { + "label": "Argumente hwaccel export", + "description": "Argumente de accelerare hardware pentru operațiunile de export/transcodare." + }, + "max_concurrent": { + "description": "Numărul maxim de sarcini de export de procesat în același timp.", + "label": "Număr maxim de exporturi simultane" + } + }, + "preview": { + "label": "Configurație preview", + "description": "Setări pentru calitatea preview-urilor din interfață.", + "quality": { + "label": "Calitate preview", + "description": "Nivel calitate (foarte_scăzută, scăzută, medie, ridicată, foarte ridicată)." + } + }, + "enabled_in_config": { + "label": "Stare înregistrare originală", + "description": "Indică dacă înregistrarea a fost activă în configurația inițială." + } + }, + "review": { + "label": "Revizuire", + "description": "Setări care controlează alertele, detecțiile și rezumatele de tip GenAI folosite de interfață și stocare pentru această cameră.", + "alerts": { + "label": "Configurație alerte", + "description": "Setări pentru obiectele care generează alerte și modul lor de retenție.", + "enabled": { + "label": "Activare alerte", + "description": "Activează sau dezactivează generarea de alerte pentru această cameră." + }, + "labels": { + "label": "Etichete alerte", + "description": "Obiecte care sunt considerate alerte (ex: om, mașină)." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Zonele necesare pentru a declanșa o alertă." + }, + "enabled_in_config": { + "label": "Stare alerte originală", + "description": "Dacă alertele au fost active inițial în fișierul de config." + }, + "cutoff_time": { + "label": "Timp limită alerte", + "description": "Secunde de așteptare după încetarea activității înainte de a încheia alerta." + } + }, + "detections": { + "label": "Configurație detecții", + "description": "Setări pentru care obiecte urmărite generează detecții (fără alertă) și cum sunt păstrate detecțiile.", + "enabled": { + "label": "Activare detecții", + "description": "Activează sau dezactivează evenimentele de detecție pentru această cameră." + }, + "labels": { + "label": "Etichete detecții", + "description": "Obiecte care se consideră detecții." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Zonele necesare pentru o detecție." + }, + "cutoff_time": { + "label": "Timp limită detecții", + "description": "Secunde de așteptare înainte de a încheia o detecție." + }, + "enabled_in_config": { + "label": "Stare detecții originală", + "description": "Dacă detecțiile au fost active în configurația inițială." + } + }, + "genai": { + "label": "Configurație GenAI", + "description": "Controlul AI-ului generativ pentru descrieri și rezumate în review.", + "enabled": { + "label": "Activare descrieri GenAI", + "description": "Activează descrierile și rezumatele generate de AI pentru elementele de review." + }, + "alerts": { + "label": "GenAI pentru alerte", + "description": "Folosește GenAI pentru descrierea alertelor." + }, + "detections": { + "label": "GenAI pentru detecții", + "description": "Folosește GenAI pentru descrierea detecțiilor." + }, + "image_source": { + "label": "Sursă imagine review", + "description": "Sursa imaginilor ('preview' sau 'recordings'); 'recordings' e mai calitativ dar consumă mai multe token-uri." + }, + "additional_concerns": { + "label": "Preocupări suplimentare", + "description": "Listă de note sau griji pe care GenAI să le considere când evaluează activitatea pe cameră." + }, + "debug_save_thumbnails": { + "label": "Salvează miniaturile", + "description": "Salvează miniaturile trimise la furnizorul GenAI pentru depanare." + }, + "enabled_in_config": { + "label": "Stare GenAI originală", + "description": "Dacă review-ul GenAI a fost activ inițial." + }, + "preferred_language": { + "label": "Limbă preferată", + "description": "Limba în care vrei ca GenAI să genereze răspunsurile." + }, + "activity_context_prompt": { + "label": "Prompt context activitate", + "description": "Prompt personalizat care descrie ce este suspect și ce nu pentru rezumatele GenAI." + } + } + }, + "semantic_search": { + "label": "Căutare semantică", + "description": "Setări pentru căutarea semantică care construiește și interoghează înglobări de obiecte pentru a găsi elemente similare.", + "triggers": { + "label": "Trigger-e", + "description": "Acțiuni și criterii pentru trigger-ele de căutare semantică specifice camerelor.", + "friendly_name": { + "label": "Nume sugestiv", + "description": "Nume opțional afișat în UI pentru acest trigger." + }, + "enabled": { + "label": "Activare trigger", + "description": "Activează sau dezactivează acest trigger." + }, + "type": { + "label": "Tip trigger", + "description": "Tip: 'thumbnail' (compară cu imagine) sau 'description' (compară cu text)." + }, + "data": { + "label": "Conținut trigger", + "description": "Textul sau ID-ul miniaturii de comparat cu obiectele urmărite." + }, + "threshold": { + "label": "Prag trigger", + "description": "Scorul minim de similitudine (0-1) pentru activare." + }, + "actions": { + "label": "Acțiuni trigger", + "description": "Lista de acțiuni (notificare, sub_label, atribut) la activare." + } + } + }, + "snapshots": { + "label": "Snapshot-uri", + "description": "Setări pentru snapshot-uri generate prin API ale obiectelor urmărite pentru această cameră.", + "enabled": { + "label": "Activează snapshot-urile", + "description": "Activează sau dezactivează salvarea de snapshots pentru această cameră." + }, + "clean_copy": { + "label": "Salvează copie curată", + "description": "Salvează și o copie fără adnotări a snapshot-ului." + }, + "timestamp": { + "label": "Overlay timestamp", + "description": "Suprapune data și ora pe snapshot-urile din API." + }, + "bounding_box": { + "label": "Overlay chenar", + "description": "Desenează chenarele obiectelor urmărite pe snapshot-urile din API." + }, + "crop": { + "label": "Decupează snapshot-ul", + "description": "Decupează snapshot-urile din API pe chenarul obiectului detectat." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Zonele prin care trebuie să treacă un obiect pentru a salva un snapshot." + }, + "height": { + "label": "Înălțime snapshot", + "description": "Înălțimea (în pixeli) la care să se redimensioneze snapshot-urile din API; lasă gol pentru a păstra dimensiunea originală." + }, + "retain": { + "label": "Retenție snapshot-uri", + "description": "Setări de reținere pentru snapshot-uri, incluzând zilele implicite și suprascrierile per obiect.", + "default": { + "label": "Retenție implicită", + "description": "Numărul implicit de zile pentru păstrare." + }, + "mode": { + "label": "Mod retenție", + "description": "Mod retenție: 'all', 'motion' sau 'active_objects'." + }, + "objects": { + "label": "Retenție per obiect", + "description": "Suprascrieri pentru zilele de retenție ale snapshot-urilor per obiect." + } + }, + "quality": { + "label": "Calitatea snapshot-ului", + "description": "Calitatea encodării pentru snapshot-urile salvate (0-100)." + } + }, + "timestamp_style": { + "label": "Stil timestamp", + "description": "Opțiuni de stilizare pentru marcajele de timp aplicate snapshot-urilor și vizualizării Debug.", + "position": { + "label": "Poziție timestamp", + "description": "Unde apare data/ora pe imagine (stânga-sus/dreapta-sus etc.)." + }, + "format": { + "label": "Format timestamp", + "description": "Formatul datei (coduri Python datetime)." + }, + "color": { + "label": "Culoare timestamp", + "description": "Valori RGB pentru textul datei.", + "red": { + "label": "Roșu", + "description": "Componenta roșie (0-255)." + }, + "green": { + "label": "Verde", + "description": "Componenta verde (0-255)." + }, + "blue": { + "label": "Albastru", + "description": "Componenta albastră (0-255)." + } + }, + "thickness": { + "label": "Grosime timestamp", + "description": "Grosimea liniei textului." + }, + "effect": { + "label": "Efect timestamp", + "description": "Efect vizual pentru text (fără, solid, umbră)." + } + }, + "best_image_timeout": { + "label": "Timp limită cea mai bună imagine", + "description": "Cât timp să se aștepte pentru imaginea cu cel mai mare scor de încredere." + }, + "mqtt": { + "label": "MQTT", + "description": "Setări de publicare a imaginilor prin MQTT.", + "enabled": { + "label": "Trimite imagine", + "description": "Activează publicarea de snapshot-uri cu imagini pentru obiecte pe topic-urile MQTT pentru această cameră." + }, + "timestamp": { + "label": "Adaugă timestamp", + "description": "Suprapune un timestamp pe imaginile publicate pe MQTT." + }, + "bounding_box": { + "label": "Adaugă bounding box", + "description": "Desenează chenare pe imaginile publicate prin MQTT." + }, + "crop": { + "label": "Decupează imaginea", + "description": "Decupează imaginile publicate pe MQTT la dimensiunea chenarului obiectului detectat." + }, + "height": { + "label": "Înălțime imagine", + "description": "Înălțimea (pixeli) la care să fie redimensionate imaginile publicate prin MQTT." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Zonele în care trebuie să intre un obiect pentru ca o imagine MQTT să fie publicată." + }, + "quality": { + "label": "Calitate JPEG", + "description": "Calitatea JPEG pentru imaginile publicate pe MQTT (0-100)." + } + }, + "notifications": { + "label": "Notificări", + "description": "Setări pentru activarea și controlul notificărilor pentru această cameră.", + "enabled": { + "label": "Activează notificările", + "description": "Activează sau dezactivează notificările pentru această cameră." + }, + "email": { + "label": "Email notificare", + "description": "Adresa de email folosită pentru notificări push sau cerută de anumiți furnizori de notificări." + }, + "cooldown": { + "label": "Perioadă de răcire", + "description": "Timpul de așteptare (secunde) între notificări pentru a evita spamarea destinatarilor." + }, + "enabled_in_config": { + "label": "Stare originală notificări", + "description": "Indică dacă notificările au fost activate în configurația statică originală." + } + }, + "onvif": { + "label": "ONVIF", + "description": "Setări pentru conexiunea ONVIF și autotracking PTZ pentru această cameră.", + "host": { + "label": "Gazdă ONVIF", + "description": "Gazda (și schema opțională) pentru serviciul ONVIF al acestei camere." + }, + "port": { + "label": "Port ONVIF", + "description": "Numărul portului pentru serviciul ONVIF." + }, + "user": { + "label": "Utilizator ONVIF", + "description": "Utilizator pentru autentificarea ONVIF; unele dispozitive necesită un utilizator administrator pentru ONVIF." + }, + "password": { + "label": "Parolă ONVIF", + "description": "Parola pentru autentificarea ONVIF." + }, + "tls_insecure": { + "label": "Dezactivează verificare TLS", + "description": "Sari peste verificarea TLS și dezactivează autentificarea digest pentru ONVIF (nesigur; a se utiliza doar în rețele sigure)." + }, + "autotracking": { + "label": "Urmărire automată", + "description": "Urmărește automat obiectele în mișcare și menține-le centrate în cadru folosind mișcările camerei PTZ.", + "enabled": { + "label": "Activează Autotracking", + "description": "Activează sau dezactivează urmărirea automată PTZ a obiectelor detectate." + }, + "calibrate_on_startup": { + "label": "Calibrare la pornire", + "description": "Măsoară vitezele motorului PTZ la pornire pentru a îmbunătăți precizia urmăririi. Frigate va actualiza config-ul cu movement_weights după calibrare." + }, + "zooming": { + "label": "Mod zoom", + "description": "Controlează comportamentul zoom-ului: dezactivat (doar pan/tilt), absolut (cel mai compatibil) sau relativ (pan/tilt/zoom concurent)." + }, + "zoom_factor": { + "label": "Factor zoom", + "description": "Controlează nivelul de zoom pe obiectele urmărite. Valorile mai mici păstrează mai mult din scenă; valorile mai mari fac zoom mai aproape, dar pot pierde urmărirea. Valori între 0.1 și 0.75." + }, + "track": { + "label": "Obiecte urmărite", + "description": "Listă de tipuri de obiecte care ar trebui să declanșeze autotracking-ul." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Obiectele trebuie să intre în una dintre aceste zone înainte ca autotracking-ul să înceapă." + }, + "return_preset": { + "label": "Preset de întoarcere", + "description": "Numele preset-ului ONVIF configurat în firmware-ul camerei pentru întoarcere după ce urmărirea se termină." + }, + "timeout": { + "label": "Timeout întoarcere", + "description": "Așteaptă acest număr de secunde după pierderea urmăririi înainte de a returna camera la poziția presetată." + }, + "movement_weights": { + "label": "Ponderi mișcare", + "description": "Valori de calibrare generate automat de calibrarea camerei. Nu modifica manual." + }, + "enabled_in_config": { + "label": "Stare originală autotrack", + "description": "Câmp intern pentru a urmări dacă autotracking-ul a fost activat în configurație." + } + }, + "ignore_time_mismatch": { + "label": "Ignoră decalaj timp", + "description": "Ignoră diferențele de sincronizare a timpului între cameră și serverul Frigate pentru comunicarea ONVIF." + }, + "profile": { + "label": "Profil ONVIF", + "description": "Profil media ONVIF specific de utilizat pentru control PTZ, potrivit după token sau nume. Dacă nu este setat, se selectează automat primul profil cu configurație PTZ validă." + } + }, + "type": { + "label": "Tip cameră", + "description": "Tipul Camerei" + }, + "ui": { + "label": "Interfață cameră", + "description": "Ordinea de afișare și vizibilitatea pentru această cameră în interfață. Ordinea afectează dashboard-ul implicit. Pentru control mai granular, folosește grupuri de camere.", + "order": { + "label": "Ordine interfață", + "description": "Ordine numerică folosită pentru sortarea camerei în interfață (dashboard și liste); numerele mai mari apar mai târziu." + }, + "dashboard": { + "label": "Arată în interfață", + "description": "Comută vizibilitatea acestei camere peste tot în interfața Frigate. Dezactivarea acestei opțiuni va necesita editarea manuală a configurației pentru a vedea din nou camera în interfață." + }, + "review": { + "label": "Arată în Revizuire", + "description": "Comută dacă această cameră este vizibilă în Rrevizuire (pagina de revizuire și filtrul ei de camere, revizuirea mișcărilor și vizualizarea istoricului)." + } + }, + "webui_url": { + "label": "URL cameră", + "description": "URL pentru a vizita camera direct din pagina de sistem" + }, + "zones": { + "label": "Zone", + "description": "Zonele îți permit să definești o arie specifică în cadru pentru a determina dacă un obiect se află sau nu într-un anumit loc.", + "friendly_name": { + "label": "Nume zonă", + "description": "Un nume ușor de recunoscut pentru zonă, afișat în interfața Frigate. Dacă nu este setat, se va folosi o versiune formatată a numelui zonei." + }, + "enabled": { + "label": "Activat", + "description": "Activează sau dezactivează această zonă. Zonele dezactivate sunt ignorate în timpul funcționării." + }, + "enabled_in_config": { + "label": "Păstrează starea originală a zonei." + }, + "filters": { + "label": "Filtre zonă", + "description": "Filtre aplicate obiectelor din această zonă. Folosite pentru a reduce alarmele false sau pentru a restricționa ce obiecte sunt considerate prezente în zonă.", + "min_area": { + "label": "Aria minimă obiect", + "description": "Aria minimă a chenarului (pixeli sau procentaj) necesară pentru acest tip de obiect. Poate fi în pixeli (int) sau procentaj (float între 0.000001 și 0.99)." + }, + "max_area": { + "label": "Aria maximă obiect", + "description": "Aria maximă a chenarului (pixeli sau procentaj) permisă pentru acest tip de obiect. Poate fi în pixeli (int) sau procentaj (float între 0.000001 și 0.99)." + }, + "min_ratio": { + "label": "Raport aspect minim", + "description": "Raportul minim lățime/înălțime cerut pentru ca chenarul să se califice." + }, + "max_ratio": { + "label": "Raport aspect maxim", + "description": "Raportul maxim lățime/înălțime permis pentru ca chenarul să se califice." + }, + "threshold": { + "label": "Prag de încredere", + "description": "Pragul mediu de încredere a detecției necesar pentru ca obiectul să fie considerat un rezultat real." + }, + "min_score": { + "label": "Încredere minimă", + "description": "Încrederea minimă a detecției pe un singur cadru necesară pentru ca obiectul să fie numărat." + }, + "mask": { + "label": "Mască filtru", + "description": "Coordonatele poligonului care definesc unde se aplică acest filtru în cadrul imaginii." + }, + "raw_mask": { + "label": "Mască brută" + } + }, + "coordinates": { + "label": "Coordonate", + "description": "Coordonatele poligonului care definesc aria zonei. Poate fi un șir separat prin virgule sau o listă de șiruri de coordonate. Coordonatele trebuie să fie relative (0-1) sau absolute (legacy)." + }, + "distances": { + "label": "Distanțe reale", + "description": "Distanțe reale opționale pentru fiecare latură a patrulaterului zonei, folosite pentru calcule de viteză sau distanță. Trebuie să aibă exact 4 valori dacă este setat." + }, + "inertia": { + "label": "Cadre de inerție", + "description": "Numărul de cadre consecutive în care un obiect trebuie detectat în zonă înainte de a fi considerat prezent. Ajută la filtrarea detecțiilor trecătoare." + }, + "loitering_time": { + "label": "Secunde staționare", + "description": "Numărul de secunde în care un obiect trebuie să rămână în zonă pentru a fi considerat în staționare (loitering). Setează pe 0 pentru a dezactiva detecția staționării." + }, + "speed_threshold": { + "label": "Viteză minimă", + "description": "Viteza minimă (în unități reale dacă distanțele sunt setate) necesară pentru ca un obiect să fie considerat prezent în zonă. Folosit pentru trigger-e de zonă bazate pe viteză." + }, + "objects": { + "label": "Obiecte trigger", + "description": "Lista tipurilor de obiecte (din labelmap) care pot declanșa această zonă. Poate fi un șir sau o listă de șiruri. Dacă este gol, toate obiectele sunt luate în considerare." + } + }, + "enabled_in_config": { + "label": "Stare inițială cameră", + "description": "Păstrează starea originală a camerei." + }, + "profiles": { + "label": "Profiluri", + "description": "Profile de configurare denumite cu suprascrieri parțiale care pot fi activate la rulare." + } +} diff --git a/web/public/locales/ro/config/global.json b/web/public/locales/ro/config/global.json new file mode 100644 index 0000000..1dca8a4 --- /dev/null +++ b/web/public/locales/ro/config/global.json @@ -0,0 +1,2358 @@ +{ + "audio": { + "label": "Detecție audio", + "enabled": { + "label": "Activare detecție audio", + "description": "Activează sau dezactivează detecția audio pentru toate camerele." + }, + "max_not_heard": { + "label": "Timeout final", + "description": "Secunde fără tipul audio configurat înainte ca evenimentul să fie încheiat." + }, + "min_volume": { + "label": "Volum minim", + "description": "Pragul minim de volum RMS; valorile mici cresc sensibilitatea (ex: 200 ridicată, 500 medie, 1000 scăzută)." + }, + "listen": { + "label": "Tipuri ascultate", + "description": "Lista de evenimente audio de detectat (ex: lătrat, alarmă_incendiu, vorbire)." + }, + "filters": { + "label": "Filtre audio", + "description": "Setări de filtrare per tip audio, cum ar fi pragul de încredere.", + "threshold": { + "label": "Încredere audio minimă", + "description": "Pragul minim de încredere pentru ca evenimentul audio să fie luat în considerare." + } + }, + "enabled_in_config": { + "label": "Stare audio originală", + "description": "Indică dacă detecția audio a fost activată inițial în fișierul de configurare static." + }, + "num_threads": { + "label": "Thread-uri detecție", + "description": "Numărul de thread-uri pentru procesarea detecției audio." + }, + "description": "Setări pentru detecția evenimentelor audio; pot fi suprascrise per cameră." + }, + "audio_transcription": { + "label": "Transcriere audio", + "description": "Setări pentru transcrierea audio live și a vorbirii pentru evenimente.", + "live_enabled": { + "label": "Transcriere live", + "description": "Activează streaming-ul de transcriere live pe măsură ce sunetul e recepționat." + }, + "enabled": { + "label": "Activare transcriere audio", + "description": "Activează transcrierea automată pentru toate camerele." + }, + "language": { + "label": "Limbă transcriere", + "description": "Codul de limbă (ex: 'ro' pentru română)." + }, + "device": { + "label": "Dispozitiv transcriere", + "description": "CPU/GPU pentru modelul de transcriere. Momentan sunt suportate doar plăcile NVIDIA CUDA." + }, + "model_size": { + "label": "Mărime model", + "description": "Mărimea modelului pentru transcrierea offline a evenimentelor." + } + }, + "birdseye": { + "label": "Birdseye", + "description": "Setări pentru vizualizarea compusă Birdseye care combină mai multe stream-uri într-un singur layout.", + "enabled": { + "label": "Activare Birdseye", + "description": "Activează sau dezactivează funcția Birdseye." + }, + "mode": { + "label": "Mod urmărire", + "description": "Modul de includere a camerelor în Birdseye: 'objects', 'motion' sau 'continuous'." + }, + "order": { + "label": "Poziție", + "description": "Poziția numerică ce controlează ordinea camerei în layout-ul Birdseye." + }, + "restream": { + "label": "Restream-uri RTSP", + "description": "Redifuzează ieșirea Birdseye ca stream RTSP; activarea va menține Birdseye pornit continuu." + }, + "width": { + "label": "Lățime", + "description": "Lățimea de ieșire în pixeli a cadrului Birdseye." + }, + "height": { + "label": "Înălțime", + "description": "Înălțimea de ieșire în pixeli a cadrului Birdseye." + }, + "quality": { + "label": "Calitate encodare", + "description": "Calitatea encodării pentru feed-ul mpeg1 Birdseye (1 maxim, 31 minim)." + }, + "inactivity_threshold": { + "label": "Prag inactivitate", + "description": "Secunde de inactivitate după care o cameră nu mai este afișată în Birdseye." + }, + "layout": { + "label": "Layout", + "description": "Opțiuni de layout pentru compoziția Birdseye.", + "scaling_factor": { + "label": "Factor scalare", + "description": "Factorul de scalare folosit de calculatorul de layout (între 1.0 și 5.0)." + }, + "max_cameras": { + "label": "Nr. maxim camere", + "description": "Numărul maxim de camere afișate simultan în Birdseye." + } + }, + "idle_heartbeat_fps": { + "label": "FPS heartbeat inactiv", + "description": "Cadre pe secundă pentru re-trimiterea ultimului cadru Birdseye când e inactiv; 0 pentru dezactivare." + } + }, + "detect": { + "label": "Detecție obiecte", + "description": "Setări pentru rolul de detecție folosit pentru a rula recunoașterea obiectelor și trackerele.", + "enabled": { + "label": "Activează detecția de obiecte", + "description": "Activează sau dezactivează detecția de obiecte pentru toate camerele; poate fi suprascrisă pentru fiecare cameră în parte." + }, + "height": { + "label": "Înălțime detect", + "description": "Înălțimea cadrelor pentru stream-ul de detect; lasă gol pentru rezoluția nativă." + }, + "width": { + "label": "Lățime detect", + "description": "Lățimea cadrelor pentru stream-ul de detect; lasă gol pentru rezoluția nativă." + }, + "fps": { + "label": "FPS detect", + "description": "FPS-ul dorit pentru detecție; valori mici reduc consumul CPU (recomandat 5, max 10 pentru obiecte foarte rapide)." + }, + "min_initialized": { + "label": "Cadre minime inițializare", + "description": "Numărul de detecții consecutive necesare înainte de a crea un obiect urmărit. Crește valoarea pentru a reduce alarmele false." + }, + "max_disappeared": { + "label": "Cadre maxime dispariție", + "description": "Numărul de cadre fără detecție înainte ca un obiect urmărit să fie considerat dispărut." + }, + "stationary": { + "label": "Configurație obiecte staționare", + "description": "Setări pentru gestionarea obiectelor care rămân nemișcate o perioadă.", + "interval": { + "label": "Interval staționar", + "description": "Cât de des (în cadre) se verifică prezența unui obiect staționar." + }, + "threshold": { + "label": "Prag staționar", + "description": "Numărul de cadre fără schimbare de poziție pentru a marca un obiect ca staționar." + }, + "max_frames": { + "label": "Cadre maxime", + "description": "Limitează cât timp sunt urmărite obiectele staționare înainte de a fi ignorate.", + "default": { + "label": "Cadre maxime implicit", + "description": "Valoarea implicită pentru urmărirea obiectelor staționare." + }, + "objects": { + "label": "Cadre maxime per obiect", + "description": "Suprascrieri per obiect pentru durata urmăririi staționare." + } + }, + "classifier": { + "label": "Activare clasificator vizual", + "description": "Folosește un clasificator vizual pentru a detecta obiectele cu adevărat staționare, chiar dacă chenarul oscilează." + } + }, + "annotation_offset": { + "label": "Offset adnotare", + "description": "Milisecunde pentru a decala adnotările de detecție pentru a alinia mai bine chenarele cu înregistrarea." + } + }, + "face_recognition": { + "label": "Recunoaștere facială", + "enabled": { + "label": "Activare recunoaștere facială", + "description": "Activează sau dezactivează recunoașterea facială." + }, + "min_area": { + "label": "Arie minimă față", + "description": "Aria minimă (pixeli) pentru a încerca recunoașterea." + }, + "description": "Setări pentru detecția și recunoașterea fețelor.", + "model_size": { + "label": "Mărime model", + "description": "Mărimea modelului pentru înglobări faciale; cel mare poate cere GPU." + }, + "unknown_score": { + "label": "Prag scor necunoscut", + "description": "Pragul de distanță sub care o față e considerată potrivire (mai mare = mai strict)." + }, + "detection_threshold": { + "label": "Prag detecție", + "description": "Încrederea minimă pentru o detecție facială validă." + }, + "recognition_threshold": { + "label": "Prag recunoaștere", + "description": "Distanța de înglobare pentru a considera că două fețe se potrivesc." + }, + "min_faces": { + "label": "Nr. minim fețe", + "description": "Numărul de recunoașteri necesare înainte de a aplica o sub-etichetă persoanei." + }, + "save_attempts": { + "label": "Salvează încercările", + "description": "Numărul de încercări păstrate pentru interfața de recunoașteri recente." + }, + "blur_confidence_filter": { + "label": "Filtru încredere blur", + "description": "Ajustează scorul în funcție de claritate pentru a reduce rezultatele false la fețele de slabă calitate." + }, + "device": { + "label": "Dispozitiv", + "description": "Aceasta este o suprascriere pentru a viza un anumit dispozitiv. Consultă https://onnxruntime.ai/docs/execution-providers/ pentru mai multe informații" + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "Setări FFmpeg: cale binar, argumente, accelerare hardware și ieșiri per rol.", + "path": { + "label": "Cale FFmpeg", + "description": "Calea către binarul FFmpeg sau un alias de versiune (\"7.0\" sau \"8.0\")." + }, + "global_args": { + "label": "Argumente globale FFmpeg", + "description": "Argumente globale pasate proceselor FFmpeg." + }, + "hwaccel_args": { + "label": "Argumente accelerare hardware", + "description": "Argumente pentru accelerarea hardware. Se recomandă presetările specifice furnizorului." + }, + "input_args": { + "label": "Argumente intrare", + "description": "Argumente aplicate stream-urilor de intrare FFmpeg." + }, + "output_args": { + "label": "Argumente ieșire", + "description": "Argumente de ieșire implicite pentru diverse roluri (detect, record).", + "detect": { + "label": "Argumente ieșire detect", + "description": "Argumente implicite pentru stream-urile cu rol detect." + }, + "record": { + "label": "Argumente ieșire record", + "description": "Argumente implicite pentru stream-urile cu rol record." + } + }, + "retry_interval": { + "label": "Timp reîncercare FFmpeg", + "description": "Secunde de așteptare înainte de reconectarea unui stream după o eroare. Implicit 10." + }, + "apple_compatibility": { + "label": "Compatibilitate Apple", + "description": "Activează tag-ul HEVC pentru compatibilitate mai bună cu playerele Apple la înregistrările H.265." + }, + "gpu": { + "label": "Index GPU", + "description": "Indexul GPU implicit folosit pentru accelerarea hardware." + }, + "inputs": { + "label": "Intrări cameră", + "description": "Listă de definiții pentru stream-urile de intrare (căi și roluri).", + "path": { + "label": "Cale intrare", + "description": "URL-ul sau calea stream-ului de intrare al camerei." + }, + "roles": { + "label": "Roluri intrare", + "description": "Rolurile atribuite acestui stream de intrare." + }, + "global_args": { + "label": "Argumente globale FFmpeg", + "description": "Argumente globale pentru acest stream de intrare." + }, + "hwaccel_args": { + "label": "Argumente accelerare hardware", + "description": "Argumente de accelerare hardware pentru acest stream." + }, + "input_args": { + "label": "Argumente intrare", + "description": "Argumente specifice acestui stream." + } + } + }, + "live": { + "label": "Redare live", + "streams": { + "label": "Nume stream-uri live", + "description": "Maparea numelor de stream-uri configurate către numele restream/go2rtc folosite live." + }, + "height": { + "label": "Înălțime live", + "description": "Înălțimea (pixeli) pentru redarea jsmpeg în UI; trebuie să fie <= înălțimea stream-ului de detect." + }, + "quality": { + "label": "Calitate live", + "description": "Calitatea encodării pentru stream-ul jsmpeg (1 maxim, 31 minim)." + }, + "description": "Setări pentru a controla rezoluția și calitatea stream live jsmpeg. Acest lucru nu afectează camerele retransmise care folosesc go2rtc pentru vizualizare live." + }, + "lpr": { + "label": "Recunoaștere numere înmatriculare", + "description": "Setări pentru recunoașterea numerelor de înmatriculare, inclusiv praguri de detecție, formatare și numere cunoscute.", + "enabled": { + "label": "Activare LPR", + "description": "Activează sau dezactivează recunoașterea numerelor de înmatriculare." + }, + "expire_time": { + "label": "Secunde expirare", + "description": "Timpul în secunde după care un număr nevăzut este expirat din tracker (doar pentru camerele LPR dedicate)." + }, + "min_area": { + "label": "Arie minimă plăcuță", + "description": "Aria minimă (pixeli) pentru a încerca recunoașterea." + }, + "enhancement": { + "label": "Nivel îmbunătățire", + "description": "Nivelul de îmbunătățire (0-10) aplicat decupajelor cu numere înainte de OCR; valorile mai mari nu îmbunătățesc mereu rezultatele, iar nivelurile peste 5 pot funcționa doar cu numerele pe timp de noapte și trebuie folosite cu atenție." + }, + "model_size": { + "label": "Mărime model", + "description": "Mărimea modelului pentru text. Majoritatea ar trebui să folosească 'small'." + }, + "detection_threshold": { + "label": "Prag detecție", + "description": "Încrederea necesară pentru a începe scanarea OCR pe o plăcuță suspectă." + }, + "recognition_threshold": { + "label": "Prag recunoaștere", + "description": "Încrederea necesară pentru a atașa textul recunoscut ca sub-etichetă." + }, + "min_plate_length": { + "label": "Lungime minimă plăcuță", + "description": "Numărul minim de caractere pentru ca un număr să fie considerat valid." + }, + "format": { + "label": "Regex format număr", + "description": "Regex opțional pentru validarea șirurilor de numere recunoscute față de un format așteptat." + }, + "match_distance": { + "label": "Distanță de potrivire", + "description": "Numărul de nepotriviri de caractere permise la compararea numerelor detectate cu cele cunoscute." + }, + "known_plates": { + "label": "Numere cunoscute", + "description": "Listă de numere sau regex-uri pentru monitorizare specială sau alerte." + }, + "debug_save_plates": { + "label": "Salvare numere pentru debug", + "description": "Salvează imaginile decupate cu numere pentru depanarea performanței LPR." + }, + "device": { + "label": "Dispozitiv", + "description": "Aceasta este o suprascriere pentru a viza un anumit dispozitiv. Consultă https://onnxruntime.ai/docs/execution-providers/ pentru mai multe informații" + }, + "replace_rules": { + "label": "Reguli de înlocuire", + "description": "Reguli de înlocuire regex folosite pentru a normaliza șirurile numerelor detectate înainte de potrivire.", + "pattern": { + "label": "Model regex" + }, + "replacement": { + "label": "Șir de înlocuire" + } + } + }, + "motion": { + "label": "Detecție mișcare", + "enabled": { + "label": "Activare detecție mișcare", + "description": "Activează sau dezactivează detecția mișcării pentru toate camerele." + }, + "threshold": { + "label": "Prag mișcare", + "description": "Pragul de diferență între pixeli; valorile mari reduc sensibilitatea (1-255)." + }, + "lightning_threshold": { + "label": "Prag fulger/lumină", + "description": "Prag pentru detectarea și ignorarea vârfurilor scurte de lumină (o valoare mai mică este mai sensibilă, valori între 0.3 și 1.0). Acest lucru nu oprește complet detecția mișcării; doar determină detectorul să nu mai analizeze cadre suplimentare odată ce pragul este depășit. Înregistrările bazate pe mișcare sunt create în continuare în timpul acestor evenimente." + }, + "improve_contrast": { + "label": "Îmbunătățire contrast", + "description": "Aplică o îmbunătățire a contrastului înainte de analiza mișcării pentru a ajuta detecția." + }, + "contour_area": { + "label": "Arie contur", + "description": "Aria minimă a conturului în pixeli pentru a fi considerat mișcare." + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "Factor de blending alpha folosit în diferențierea cadrelor." + }, + "frame_alpha": { + "label": "Cadru alfa", + "description": "Valoarea alpha pentru amestecarea cadrelor la preprocesarea mișcării." + }, + "frame_height": { + "label": "Înălțime cadru", + "description": "Înălțimea la care sunt scalate cadrele pentru calculul mișcării." + }, + "mask": { + "label": "Coordonate mască", + "description": "Coordonate x,y care definesc poligonul măștii de mișcare." + }, + "mqtt_off_delay": { + "label": "Întârziere MQTT off", + "description": "Secunde de așteptare după ultima mișcare înainte de a trimite starea 'off' prin MQTT." + }, + "enabled_in_config": { + "label": "Stare mișcare originală", + "description": "Indică dacă detecția mișcării a fost activă în configurația inițială." + }, + "raw_mask": { + "label": "Mască brută" + }, + "description": "Setări implicite pentru detecția mișcării, aplicate dacă nu sunt suprascrise per cameră.", + "skip_motion_threshold": { + "label": "Ignoră pragul de mișcare", + "description": "Dacă este setat la o valoare între 0.0 și 1.0, și mai mult decât această fracție din imagine se modifică într-un singur cadru, detectorul nu va returna casete de mișcare și se va recalibra imediat. Acest lucru poate economisi CPU și reduce rezultatele fals pozitive în timpul fulgerelor, furtunilor etc., dar poate rata evenimente reale, cum ar fi o cameră PTZ care urmărește automat un obiect. Compromisul este între a pierde câțiva megabytes de înregistrări versus a revizui câteva clipuri scurte. Lasă nesetat (None) pentru a dezactiva această funcție." + } + }, + "objects": { + "label": "Obiecte", + "description": "Setări implicite pentru urmărire, inclusiv ce etichete se urmăresc și filtrele per obiect.", + "track": { + "label": "Obiecte de urmărit", + "description": "Lista etichetelor de obiecte urmărite global." + }, + "filters": { + "label": "Filtre obiecte", + "description": "Filtre pentru a reduce alarmele false (arie, raport, încredere).", + "min_area": { + "label": "Arie minimă obiect", + "description": "Aria minimă a chenarului (pixeli sau procent)." + }, + "max_area": { + "label": "Arie maximă obiect", + "description": "Aria maximă a chenarului (pixeli sau procent)." + }, + "min_ratio": { + "label": "Raport aspect minim", + "description": "Raportul minim lățime/înălțime pentru chenar." + }, + "max_ratio": { + "label": "Raport aspect maxim", + "description": "Raportul maxim lățime/înălțime pentru chenar." + }, + "threshold": { + "label": "Prag încredere", + "description": "Încrederea medie necesară pentru a considera obiectul valid." + }, + "min_score": { + "label": "Scor minim", + "description": "Încrederea minimă la un singur cadru pentru a număra obiectul." + }, + "mask": { + "label": "Mască filtru", + "description": "Poligonul unde se aplică acest filtru în cadru." + }, + "raw_mask": { + "label": "Mască brută" + } + }, + "mask": { + "label": "Mască obiect", + "description": "Mască pentru a preveni detecția obiectelor în anumite zone." + }, + "raw_mask": { + "label": "Mască brută" + }, + "genai": { + "label": "Configurație obiecte GenAI", + "description": "Opțiuni GenAI pentru descrierea obiectelor urmărite și trimiterea cadrelor.", + "enabled": { + "label": "Activare GenAI", + "description": "Activează generarea de descrieri prin GenAI pentru obiectele urmărite." + }, + "use_snapshot": { + "label": "Folosește snapshot-uri", + "description": "Folosește snapshot-urile obiectelor în loc de miniaturi pentru GenAI." + }, + "prompt": { + "label": "Prompt descriere", + "description": "Șablonul de prompt implicit pentru descrierile GenAI." + }, + "object_prompts": { + "label": "Prompt-uri per obiect", + "description": "Prompt-uri personalizate pentru anumite etichete de obiecte." + }, + "objects": { + "label": "Obiecte GenAI", + "description": "Lista etichetelor de obiecte care vor fi trimise la GenAI." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Zonele prin care trebuie să treacă obiectele pentru a genera descrieri." + }, + "debug_save_thumbnails": { + "label": "Salvează miniaturile", + "description": "Salvează miniaturile trimise la GenAI pentru depanare." + }, + "send_triggers": { + "label": "Trigger-e GenAI", + "description": "Definește când sunt trimise cadrele la GenAI (la final, după actualizări etc.).", + "tracked_object_end": { + "label": "Trimite la final", + "description": "Trimite cererea la GenAI când urmărirea obiectului s-a terminat." + }, + "after_significant_updates": { + "label": "Trigger GenAI timpuriu", + "description": "Trimite la GenAI după un număr de actualizări semnificative ale obiectului." + } + }, + "enabled_in_config": { + "label": "Stare GenAI originală", + "description": "Indică dacă GenAI a fost activat în configurația inițială." + } + }, + "filters_attribute": { + "label": "Filtre de atribute", + "description": "Filtre aplicate atributelor detectate pentru a reduce rezultatele fals pozitive (arie, raport, încredere).", + "min_area": { + "label": "Aria minimă a atributului", + "description": "Aria minimă a casetei de încadrare (pixeli sau procentaj) necesară pentru acest atribut. Poate fi în pixeli (int) sau procentaj (între 0.000001 și 0.99)." + }, + "max_area": { + "label": "Aria maximă a atributului", + "description": "Aria minimă a casetei de încadrare (pixeli sau procentaj) necesară pentru acest atribut. Poate fi în pixeli (int) sau procentaj (între 0.000001 și 0.99)." + }, + "min_ratio": { + "label": "Raport de aspect minim", + "description": "Raportul minim lățime/înălțime necesar pentru ca o casetă de încadrare să fie validă." + }, + "max_ratio": { + "label": "Raport de aspect maxim", + "description": "Raportul maxim lățime/înălțime permis pentru ca o casetă de încadrare să fie validă." + }, + "threshold": { + "label": "Prag de încredere", + "description": "Pragul mediu de încredere a detecției necesar pentru ca atributul să fie considerat un rezultat adevărat pozitiv." + }, + "min_score": { + "label": "Încredere minimă", + "description": "Încrederea minimă de detecție pe un singur cadru necesară pentru a asocia acest atribut cu obiectul său părinte." + }, + "mask": { + "label": "Mască de filtrare", + "description": "Coordonatele poligonului care definesc unde se aplică acest filtru în cadru." + }, + "raw_mask": { + "label": "Mască brută" + } + } + }, + "record": { + "label": "Înregistrare", + "enabled": { + "label": "Activare înregistrare", + "description": "Activează sau dezactivează înregistrarea global." + }, + "expire_interval": { + "label": "Interval curățare înregistrări", + "description": "Minute între trecerile de curățare a segmentelor expirate." + }, + "continuous": { + "label": "Retenție continuă", + "description": "Zile de păstrare a înregistrărilor indiferent de obiecte sau mișcare. Pune 0 pentru a păstra doar alerte/detecții.", + "days": { + "label": "Zile retenție", + "description": "Numărul de zile pentru păstrare." + } + }, + "motion": { + "label": "Retenție mișcare", + "description": "Zile de păstrare pentru înregistrările declanșate de mișcare.", + "days": { + "label": "Zile retenție", + "description": "Numărul de zile pentru păstrare." + } + }, + "detections": { + "label": "Retenție detecții", + "description": "Setări pentru evenimentele de detecție, inclusiv duratele pre/post captură.", + "pre_capture": { + "label": "Secunde pre-captură", + "description": "Secunde incluse înainte de evenimentul detectat." + }, + "post_capture": { + "label": "Secunde post-captură", + "description": "Secunde incluse după încheierea evenimentului." + }, + "retain": { + "label": "Retenție eveniment", + "description": "Setări de retenție pentru clipurile cu detecții.", + "days": { + "label": "Zile retenție", + "description": "Numărul de zile de păstrare." + }, + "mode": { + "label": "Mod retenție", + "description": "Mod: 'all' (tot), 'motion' (doar segmente cu mișcare) sau 'active_objects' (doar cu obiecte active)." + } + } + }, + "alerts": { + "label": "Retenție alerte", + "description": "Setări de retenție pentru evenimentele de tip alertă.", + "pre_capture": { + "label": "Secunde pre-captură", + "description": "Secunde incluse înainte de alertă." + }, + "post_capture": { + "label": "Secunde post-captură", + "description": "Secunde incluse după alertă." + }, + "retain": { + "label": "Retenție eveniment", + "description": "Setări de păstrare pentru alerte.", + "days": { + "label": "Zile retenție", + "description": "Numărul de zile pentru păstrare." + }, + "mode": { + "label": "Mod retenție", + "description": "Modul de păstrare a segmentelor." + } + } + }, + "export": { + "label": "Configurație export", + "description": "Setări pentru exportul înregistrărilor (timelapse, accelerare hardware).", + "hwaccel_args": { + "label": "Argumente hwaccel export", + "description": "Argumente de accelerare hardware pentru operațiunile de export/transcodare." + }, + "max_concurrent": { + "description": "Numărul maxim de sarcini de export de procesat în același timp.", + "label": "Număr maxim de exporturi simultane" + } + }, + "preview": { + "label": "Configurație preview", + "description": "Setări pentru calitatea preview-urilor din interfață.", + "quality": { + "label": "Calitate preview", + "description": "Nivel calitate (foarte_scăzută, scăzută, medie, ridicată, foarte ridicată)." + } + }, + "enabled_in_config": { + "label": "Stare înregistrare originală", + "description": "Indică dacă înregistrarea a fost activă în configurația inițială." + }, + "description": "Setări de înregistrare și retenție aplicate camerelor." + }, + "review": { + "label": "Revizuire", + "alerts": { + "label": "Configurație alerte", + "description": "Setări pentru obiectele care generează alerte și modul lor de retenție.", + "enabled": { + "label": "Activare alerte", + "description": "Activează sau dezactivează generarea alertelor." + }, + "labels": { + "label": "Etichete alerte", + "description": "Obiecte care sunt considerate alerte (ex: om, mașină)." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Zonele necesare pentru a declanșa o alertă." + }, + "enabled_in_config": { + "label": "Stare alerte originală", + "description": "Dacă alertele au fost active inițial în fișierul de config." + }, + "cutoff_time": { + "label": "Timp limită alerte", + "description": "Secunde de așteptare după încetarea activității înainte de a încheia alerta." + } + }, + "detections": { + "label": "Configurație detecții", + "description": "Setări pentru care obiecte urmărite generează detecții (fără alertă) și cum sunt păstrate detecțiile.", + "enabled": { + "label": "Activare detecții", + "description": "Activează sau dezactivează evenimentele de detecție." + }, + "labels": { + "label": "Etichete detecții", + "description": "Obiecte care se consideră detecții." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Zonele necesare pentru o detecție." + }, + "cutoff_time": { + "label": "Timp limită detecții", + "description": "Secunde de așteptare înainte de a încheia o detecție." + }, + "enabled_in_config": { + "label": "Stare detecții originală", + "description": "Dacă detecțiile au fost active în configurația inițială." + } + }, + "genai": { + "label": "Configurație GenAI", + "description": "Controlul AI-ului generativ pentru descrieri și rezumate în review.", + "enabled": { + "label": "Activare descrieri GenAI", + "description": "Activează descrierile și rezumatele generate de AI pentru elementele de review." + }, + "alerts": { + "label": "GenAI pentru alerte", + "description": "Folosește GenAI pentru descrierea alertelor." + }, + "detections": { + "label": "GenAI pentru detecții", + "description": "Folosește GenAI pentru descrierea detecțiilor." + }, + "image_source": { + "label": "Sursă imagine review", + "description": "Sursa imaginilor ('preview' sau 'recordings'); 'recordings' e mai calitativ dar consumă mai multe token-uri." + }, + "additional_concerns": { + "label": "Preocupări suplimentare", + "description": "Listă de note sau griji pe care GenAI să le considere când evaluează activitatea pe cameră." + }, + "debug_save_thumbnails": { + "label": "Salvează miniaturile", + "description": "Salvează miniaturile trimise la furnizorul GenAI pentru depanare." + }, + "enabled_in_config": { + "label": "Stare GenAI originală", + "description": "Dacă review-ul GenAI a fost activ inițial." + }, + "preferred_language": { + "label": "Limbă preferată", + "description": "Limba în care vrei ca GenAI să genereze răspunsurile." + }, + "activity_context_prompt": { + "label": "Prompt context activitate", + "description": "Prompt personalizat care descrie ce este suspect și ce nu pentru rezumatele GenAI." + } + }, + "description": "Setări pentru alerte, detecții și rezumate GenAI folosite în UI și stocare." + }, + "semantic_search": { + "label": "Căutare semantică", + "triggers": { + "label": "Trigger-e", + "description": "Acțiuni și criterii pentru trigger-ele de căutare semantică specifice camerelor.", + "friendly_name": { + "label": "Nume sugestiv", + "description": "Nume opțional afișat în UI pentru acest trigger." + }, + "enabled": { + "label": "Activare trigger", + "description": "Activează sau dezactivează acest trigger." + }, + "type": { + "label": "Tip trigger", + "description": "Tip: 'thumbnail' (compară cu imagine) sau 'description' (compară cu text)." + }, + "data": { + "label": "Conținut trigger", + "description": "Textul sau ID-ul miniaturii de comparat cu obiectele urmărite." + }, + "threshold": { + "label": "Prag trigger", + "description": "Scorul minim de similitudine (0-1) pentru activare." + }, + "actions": { + "label": "Acțiuni trigger", + "description": "Lista de acțiuni (notificare, sub_label, atribut) la activare." + } + }, + "description": "Setări pentru căutarea semantică, care creează și caută în înglobări de obiecte pentru a găsi elemente similare.", + "enabled": { + "label": "Activare căutare semantică", + "description": "Activează sau dezactivează funcția de căutare semantică." + }, + "reindex": { + "label": "Reindexare la pornire", + "description": "Declanșează o reindexare completă a obiectelor istorice în baza de date de înglobări." + }, + "model": { + "label": "Model de căutare semantică sau nume furnizor GenAI", + "description": "Modelul de înglobări de folosit pentru căutarea semantică (de exemplu 'jinav1'), sau numele unui furnizor GenAI cu rolul de înglobări." + }, + "model_size": { + "label": "Mărime model", + "description": "Alege mărimea; 'small' rulează pe CPU, 'large' cere de regulă GPU." + }, + "device": { + "label": "Dispozitiv", + "description": "Aceasta este o suprascriere pentru a viza un anumit dispozitiv. Consultă https://onnxruntime.ai/docs/execution-providers/ pentru mai multe informații" + } + }, + "snapshots": { + "label": "Snapshot-uri", + "enabled": { + "label": "Activează snapshot-urile", + "description": "Activează sau dezactivează salvarea de snapshot-uri." + }, + "clean_copy": { + "label": "Salvează copie curată", + "description": "Salvează și o copie fără adnotări a snapshot-ului." + }, + "timestamp": { + "label": "Overlay timestamp", + "description": "Suprapune data și ora pe snapshot-urile din API." + }, + "bounding_box": { + "label": "Overlay chenar", + "description": "Desenează chenarele obiectelor urmărite pe snapshot-urile din API." + }, + "crop": { + "label": "Decupează snapshot-ul", + "description": "Decupează snapshot-urile din API pe chenarul obiectului detectat." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Zonele prin care trebuie să treacă un obiect pentru a salva un snapshot." + }, + "height": { + "label": "Înălțime snapshot", + "description": "Înălțimea (în pixeli) la care să se redimensioneze snapshot-urile din API; lasă gol pentru a păstra dimensiunea originală." + }, + "retain": { + "label": "Retenție snapshot-uri", + "description": "Setări de reținere pentru snapshot-uri, incluzând zilele implicite și suprascrierile per obiect.", + "default": { + "label": "Retenție implicită", + "description": "Numărul implicit de zile pentru păstrare." + }, + "mode": { + "label": "Mod retenție", + "description": "Mod retenție: 'all', 'motion' sau 'active_objects'." + }, + "objects": { + "label": "Retenție per obiect", + "description": "Suprascrieri pentru zilele de retenție ale snapshot-urilor per obiect." + } + }, + "quality": { + "label": "Calitatea snapshot-ului", + "description": "Calitatea encodării pentru snapshot-urile salvate (0-100)." + }, + "description": "Setări pentru snapshot-urile obiectelor urmărite, generate prin API, pentru toate camerele; pot fi suprascrise pentru fiecare cameră în parte." + }, + "timestamp_style": { + "label": "Stil timestamp", + "position": { + "label": "Poziție timestamp", + "description": "Unde apare data/ora pe imagine (stânga-sus/dreapta-sus etc.)." + }, + "format": { + "label": "Format timestamp", + "description": "Formatul datei (coduri Python datetime)." + }, + "color": { + "label": "Culoare timestamp", + "description": "Valori RGB pentru textul datei.", + "red": { + "label": "Roșu", + "description": "Componenta roșie (0-255)." + }, + "green": { + "label": "Verde", + "description": "Componenta verde (0-255)." + }, + "blue": { + "label": "Albastru", + "description": "Componenta albastră (0-255)." + } + }, + "thickness": { + "label": "Grosime timestamp", + "description": "Grosimea liniei textului." + }, + "effect": { + "label": "Efect timestamp", + "description": "Efect vizual pentru text (fără, solid, umbră)." + }, + "description": "Opțiuni de stilizare pentru data și ora din feed-ul de debug și snapshot-uri." + }, + "mqtt": { + "label": "MQTT", + "description": "Setări pentru conectarea și publicarea telemetriei, snapshot-urilor și detaliilor despre evenimente către un broker MQTT.", + "enabled": { + "label": "Activează MQTT", + "description": "Activează sau dezactivează integrarea MQTT pentru stare, evenimente și snapshot-uri." + }, + "host": { + "label": "Host MQTT", + "description": "Numele de gazdă sau adresa IP a brokerului MQTT." + }, + "port": { + "label": "Port MQTT", + "description": "Portul brokerului MQTT (de obicei 1883 pentru MQTT simplu)." + }, + "topic_prefix": { + "label": "Prefix topic", + "description": "Prefixul topicului MQTT pentru toate subiectele Frigate; trebuie să fie unic dacă rulezi mai multe instanțe." + }, + "client_id": { + "label": "ID client", + "description": "Identificatorul de client folosit la conectarea la brokerul MQTT; ar trebui să fie unic pe instanță." + }, + "stats_interval": { + "label": "Interval statistici", + "description": "Intervalul în secunde pentru publicarea statisticilor de sistem și de cameră către MQTT." + }, + "user": { + "label": "Utilizator MQTT", + "description": "Utilizator MQTT opțional; poate fi furnizat prin variabile de mediu sau secrete." + }, + "password": { + "label": "Parolă MQTT", + "description": "Parolă MQTT opțională; poate fi furnizată prin variabile de mediu sau secrete." + }, + "tls_ca_certs": { + "label": "Certificate CA TLS", + "description": "Calea către certificatul CA pentru conexiuni TLS la broker (pentru certificate auto-semnate)." + }, + "tls_client_cert": { + "label": "Certificat client", + "description": "Calea certificatului de client pentru autentificare reciprocă TLS; nu seta utilizator/parolă când folosești certificate de client." + }, + "tls_client_key": { + "label": "Cheie client", + "description": "Calea cheii private pentru certificatul de client." + }, + "tls_insecure": { + "label": "TLS nesecurizat", + "description": "Permite conexiuni TLS nesecurizate prin săritura verificării numelui de gazdă (nu este recomandat)." + }, + "qos": { + "label": "QoS MQTT", + "description": "Nivelul de calitate a serviciului pentru publicările/abonările MQTT (0, 1 sau 2)." + } + }, + "notifications": { + "label": "Notificări", + "enabled": { + "label": "Activează notificările", + "description": "Activează sau dezactivează notificările pentru toate camerele; pot fi suprascrise per cameră." + }, + "email": { + "label": "Email notificare", + "description": "Adresa de email folosită pentru notificări push sau cerută de anumiți furnizori de notificări." + }, + "cooldown": { + "label": "Perioadă de răcire", + "description": "Timpul de așteptare (secunde) între notificări pentru a evita spamarea destinatarilor." + }, + "enabled_in_config": { + "label": "Stare originală notificări", + "description": "Indică dacă notificările au fost activate în configurația statică originală." + }, + "description": "Setări pentru a activa și controla notificările pentru toate camerele; pot fi suprascrise per cameră." + }, + "onvif": { + "label": "ONVIF", + "description": "Setări pentru conexiunea ONVIF și autotracking PTZ pentru această cameră.", + "host": { + "label": "Gazdă ONVIF", + "description": "Gazda (și schema opțională) pentru serviciul ONVIF al acestei camere." + }, + "port": { + "label": "Port ONVIF", + "description": "Numărul portului pentru serviciul ONVIF." + }, + "user": { + "label": "Utilizator ONVIF", + "description": "Utilizator pentru autentificarea ONVIF; unele dispozitive necesită un utilizator administrator pentru ONVIF." + }, + "password": { + "label": "Parolă ONVIF", + "description": "Parola pentru autentificarea ONVIF." + }, + "tls_insecure": { + "label": "Dezactivează verificare TLS", + "description": "Sari peste verificarea TLS și dezactivează autentificarea digest pentru ONVIF (nesigur; a se utiliza doar în rețele sigure)." + }, + "autotracking": { + "label": "Urmărire automată", + "description": "Urmărește automat obiectele în mișcare și menține-le centrate în cadru folosind mișcările camerei PTZ.", + "enabled": { + "label": "Activează Autotracking", + "description": "Activează sau dezactivează urmărirea automată PTZ a obiectelor detectate." + }, + "calibrate_on_startup": { + "label": "Calibrare la pornire", + "description": "Măsoară vitezele motorului PTZ la pornire pentru a îmbunătăți precizia urmăririi. Frigate va actualiza config-ul cu movement_weights după calibrare." + }, + "zooming": { + "label": "Mod zoom", + "description": "Controlează comportamentul zoom-ului: dezactivat (doar pan/tilt), absolut (cel mai compatibil) sau relativ (pan/tilt/zoom concurent)." + }, + "zoom_factor": { + "label": "Factor zoom", + "description": "Controlează nivelul de zoom pe obiectele urmărite. Valorile mai mici păstrează mai mult din scenă; valorile mai mari fac zoom mai aproape, dar pot pierde urmărirea. Valori între 0.1 și 0.75." + }, + "track": { + "label": "Obiecte urmărite", + "description": "Listă de tipuri de obiecte care ar trebui să declanșeze autotracking-ul." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Obiectele trebuie să intre în una dintre aceste zone înainte ca autotracking-ul să înceapă." + }, + "return_preset": { + "label": "Preset de întoarcere", + "description": "Numele preset-ului ONVIF configurat în firmware-ul camerei pentru întoarcere după ce urmărirea se termină." + }, + "timeout": { + "label": "Timeout întoarcere", + "description": "Așteaptă acest număr de secunde după pierderea urmăririi înainte de a returna camera la poziția presetată." + }, + "movement_weights": { + "label": "Ponderi mișcare", + "description": "Valori de calibrare generate automat de calibrarea camerei. Nu modifica manual." + }, + "enabled_in_config": { + "label": "Stare originală autotrack", + "description": "Câmp intern pentru a urmări dacă autotracking-ul a fost activat în configurație." + } + }, + "ignore_time_mismatch": { + "label": "Ignoră decalaj timp", + "description": "Ignoră diferențele de sincronizare a timpului între cameră și serverul Frigate pentru comunicarea ONVIF." + }, + "profile": { + "label": "Profil ONVIF", + "description": "Profil media ONVIF specific de utilizat pentru control PTZ, potrivit după token sau nume. Dacă nu este setat, se selectează automat primul profil cu configurație PTZ validă." + } + }, + "version": { + "label": "Versiunea actuală a configurării", + "description": "Versiunea numerică sau de tip text a configurării active pentru a ajuta la detectarea migrărilor sau a schimbărilor de format." + }, + "safe_mode": { + "label": "Mod de siguranță", + "description": "Când este activat, pornește Frigate în mod de siguranță, cu funcții reduse, pentru depanare." + }, + "environment_vars": { + "label": "Variabile de mediu", + "description": "Perechi cheie/valoare ale variabilelor de mediu de setat pentru procesul Frigate în Home Assistant OS. Utilizatorii care nu folosesc HAOS trebuie să folosească configurarea variabilelor de mediu din Docker." + }, + "logger": { + "label": "Jurnalizare", + "description": "Controlează nivelul de detaliu implicit al jurnalului și suprascrierile pentru fiecare componentă.", + "default": { + "label": "Nivel de jurnalizare", + "description": "Nivelul global implicit de detaliu al jurnalului (debug, info, warning, error)." + }, + "logs": { + "label": "Nivel jurnalizare pe proces", + "description": "Suprascrieri ale nivelului de jurnalizare pe componentă pentru a crește sau descrește detaliile pentru module specifice." + } + }, + "auth": { + "label": "Autentificare", + "description": "Setări de autentificare și sesiune, inclusiv opțiuni pentru cookie-uri și limite de viteză.", + "enabled": { + "label": "Activează autentificarea", + "description": "Activează autentificarea nativă pentru interfața Frigate." + }, + "reset_admin_password": { + "label": "Resetează parola de admin", + "description": "Dacă este setat pe true, resetează parola administratorului la pornire și afișează noua parolă în jurnale." + }, + "cookie_name": { + "label": "Nume cookie JWT", + "description": "Numele cookie-ului folosit pentru a stoca token-ul JWT pentru autentificarea nativă." + }, + "cookie_secure": { + "label": "Flag de cookie securizat", + "description": "Setează flag-ul secure pe cookie-ul de autentificare; ar trebui să fie true când se folosește TLS." + }, + "session_length": { + "label": "Durata sesiunii", + "description": "Durata sesiunii în secunde pentru sesiunile bazate pe JWT." + }, + "refresh_time": { + "label": "Fereastră reîmprospătare sesiune", + "description": "Când o sesiune este la acest număr de secunde de expirare, se reîmprospătează la durata maximă." + }, + "failed_login_rate_limit": { + "label": "Limite login eșuat", + "description": "Reguli de limitare a încercărilor de autentificare eșuate pentru a reduce atacurile de tip brute-force." + }, + "trusted_proxies": { + "label": "Proxy-uri de încredere", + "description": "Listă de IP-uri proxy de încredere folosite pentru a determina IP-ul clientului pentru limitarea vitezei." + }, + "hash_iterations": { + "label": "Iterații hash", + "description": "Numărul de iterații PBKDF2-SHA256 de folosit la hashing-ul parolelor utilizatorilor." + }, + "roles": { + "label": "Mapări roluri", + "description": "Mapează rolurile la liste de camere. O listă goală oferă acces la toate camerele pentru acel rol." + }, + "admin_first_time_login": { + "label": "Flag prima autentificare admin", + "description": "Când este true, interfața poate afișa un link de ajutor pe pagina de login, informând utilizatorii cum să se autentifice după o resetare a parolei de admin. " + } + }, + "database": { + "label": "Bază de date", + "description": "Setări pentru baza de date SQLite folosită de Frigate pentru a stoca obiectele urmărite și metadatele înregistrărilor.", + "path": { + "label": "Cale bază de date", + "description": "Calea în sistemul de fișiere unde va fi stocat fișierul bazei de date SQLite." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "Setări pentru serviciul integrat de restreaming go2rtc, folosit pentru releul și translatarea stream-urilor live." + }, + "networking": { + "label": "Rețea", + "description": "Setări legate de rețea, cum ar fi activarea IPv6 pentru endpoint-urile Frigate.", + "ipv6": { + "label": "Configurare IPv6", + "description": "Setări specifice IPv6 pentru serviciile de rețea Frigate.", + "enabled": { + "label": "Activează IPv6", + "description": "Activează suportul IPv6 pentru serviciile Frigate (API și interfață) acolo unde este cazul." + } + }, + "listen": { + "label": "Configurare porturi ascultare", + "description": "Configurare pentru porturile de ascultare interne și externe. Aceasta este pentru utilizatori avansați. Pentru majoritatea cazurilor, se recomandă schimbarea secțiunii de porturi din fișierul Docker compose.", + "internal": { + "label": "Port intern", + "description": "Portul de ascultare intern pentru Frigate (implicit 5000)." + }, + "external": { + "label": "Port extern", + "description": "Portul de ascultare extern pentru Frigate (implicit 8971)." + } + } + }, + "proxy": { + "label": "Proxy", + "description": "Setări pentru integrarea Frigate în spatele unui reverse proxy care transmite headere de utilizator autentificat.", + "header_map": { + "label": "Mapare headere", + "description": "Mapează headerele proxy primite către câmpurile de utilizator și rol din Frigate pentru autentificarea bazată pe proxy.", + "user": { + "label": "Header utilizator", + "description": "Header care conține numele de utilizator autentificat furnizat de proxy-ul amonte." + }, + "role": { + "label": "Header rol", + "description": "Header care conține rolul sau grupurile utilizatorului autentificat din proxy-ul amonte." + }, + "role_map": { + "label": "Mapare roluri", + "description": "Mapează valorile grupurilor din amonte la rolurile Frigate (de exemplu, mapează grupurile de admin la rolul de admin)." + } + }, + "logout_url": { + "label": "URL deconectare", + "description": "URL-ul către care sunt redirecționați utilizatorii la deconectarea prin proxy." + }, + "auth_secret": { + "label": "Secret proxy", + "description": "Secret opțional verificat împotriva header-ului X-Proxy-Secret pentru a verifica proxy-urile de încredere." + }, + "default_role": { + "label": "Rol implicit", + "description": "Rolul implicit atribuit utilizatorilor autentificați prin proxy când nu se aplică nicio mapare de rol." + }, + "separator": { + "label": "Caracter separator", + "description": "Caracterul folosit pentru a separa mai multe valori furnizate în headerele proxy." + } + }, + "telemetry": { + "label": "Telemetrie", + "description": "Opțiuni pentru telemetria sistemului și statistici, inclusiv monitorizarea GPU și a lățimii de bandă a rețelei.", + "network_interfaces": { + "label": "Interfețe de rețea", + "description": "Listă de prefixe de nume pentru interfețele de rețea de monitorizat pentru statistici de lățime de bandă." + }, + "stats": { + "label": "Statistici sistem", + "description": "Opțiuni pentru a activa/dezactiva colectarea diverselor statistici de sistem și GPU.", + "amd_gpu_stats": { + "label": "Statistici GPU AMD", + "description": "Activează colectarea statisticilor pentru GPU AMD, dacă acesta este prezent." + }, + "intel_gpu_stats": { + "label": "Statistici GPU Intel", + "description": "Activează colectarea statisticilor pentru GPU Intel, dacă acesta este prezent." + }, + "network_bandwidth": { + "label": "Lățime de bandă rețea", + "description": "Activează monitorizarea lățimii de bandă a rețelei pe proces pentru procesele ffmpeg ale camerelor și detectoare (necesită capabilități)." + }, + "intel_gpu_device": { + "label": "Dispozitiv GPU Intel", + "description": "Adresa magistralei PCI sau calea dispozitivului DRM (ex./dev/dri/card1) folosită pentru a fixa statisticile GPU Intel la un anumit dispozitiv când sunt prezente mai multe." + } + }, + "version_check": { + "label": "Verificare versiune", + "description": "Activează o verificare externă pentru a detecta dacă este disponibilă o versiune Frigate mai nouă." + } + }, + "tls": { + "label": "TLS", + "description": "Setări TLS pentru endpoint-urile web Frigate (port 8971).", + "enabled": { + "label": "Activează TLS", + "description": "Activează TLS pentru interfața web și API-ul Frigate pe portul TLS configurat." + } + }, + "ui": { + "label": "UI", + "description": "Preferințe pentru interfața de utilizator, cum ar fi fusul orar, formatarea orei/datei și unitățile de măsură.", + "timezone": { + "label": "Fus orar", + "description": "Fus orar opțional de afișat în interfață (implicit se folosește ora locală a browserului)." + }, + "time_format": { + "label": "Format oră", + "description": "Formatul orei de utilizat în interfață (browser, 12 ore sau 24 ore)." + }, + "date_style": { + "label": "Stil dată", + "description": "Stilul datei de utilizat în interfață (full, long, medium, short)." + }, + "time_style": { + "label": "Stil oră", + "description": "Stilul orei de utilizat în interfață (full, long, medium, short)." + }, + "unit_system": { + "label": "Sistem de unități", + "description": "Sistemul de unități pentru afișare (metric sau imperial) folosit în interfață și MQTT." + } + }, + "detectors": { + "label": "Hardware detector", + "description": "Configurare pentru detectoarele de obiecte (CPU, GPU, backend-uri ONNX) și orice setări de model specifice detectorului.", + "type": { + "label": "Tip", + "description": "Tipul de detector de folosit pentru detecția obiectelor (de exemplu, 'cpu', 'edgetpu', 'openvino')." + }, + "cpu": { + "label": "CPU", + "description": "Detector TFLite pentru CPU care rulează modele TensorFlow Lite pe procesorul gazdă fără accelerare hardware. Nu este recomandat.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + }, + "num_threads": { + "label": "Număr fire detecție", + "description": "Numărul de fire (threads) folosite pentru inferența bazată pe CPU." + } + }, + "deepstack": { + "label": "DeepStack", + "description": "Detector DeepStack/CodeProject.AI care trimite imagini către un API HTTP DeepStack la distanță pentru inferență. Nu este recomandat.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + }, + "api_url": { + "label": "URL API DeepStack", + "description": "URL-ul API-ului DeepStack." + }, + "api_timeout": { + "label": "Timeout API DeepStack (în secunde)", + "description": "Timpul maxim permis pentru o cerere la API-ul DeepStack." + }, + "api_key": { + "label": "Cheie API DeepStack (dacă e necesară)", + "description": "Cheie API opțională pentru serviciile DeepStack autentificate." + } + }, + "degirum": { + "label": "DeGirum", + "description": "Detector DeGirum pentru rularea modelelor prin cloud-ul DeGirum sau servicii locale de inferență.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + }, + "location": { + "label": "Locație inferență", + "description": "Locația motorului de inferență DeGirum (ex. '@cloud', '127.0.0.1')." + }, + "zoo": { + "label": "Model Zoo", + "description": "Calea sau URL-ul către model zoo DeGirum." + }, + "token": { + "label": "Token Cloud DeGirum", + "description": "Token pentru accesul la Cloud-ul DeGirum." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "Detector EdgeTPU care rulează modele TensorFlow Lite compilate pentru Coral EdgeTPU folosind delegatul EdgeTPU.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + }, + "device": { + "label": "Tip dispozitiv", + "description": "Dispozitivul de folosit pentru inferența EdgeTPU (ex. 'usb', 'pci')." + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "Detector Hailo-8/Hailo-8L care folosește modele HEF și HailoRT SDK pentru inferență pe hardware Hailo.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + }, + "device": { + "label": "Tip dispozitiv", + "description": "Dispozitivul de folosit pentru inferența Hailo (ex. 'PCIe', 'M.2')." + } + }, + "memryx": { + "label": "MemryX", + "description": "Detector MemryX MX3 care rulează modele DFP compilate pe acceleratoare MemryX.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + }, + "device": { + "label": "Cale dispozitiv", + "description": "Dispozitivul de folosit pentru inferența MemryX (ex. 'PCIe')." + } + }, + "onnx": { + "label": "ONNX", + "description": "Detector ONNX pentru rularea modelelor ONNX; va folosi backend-urile de accelerare disponibile (CUDA/ROCm/OpenVINO) atunci când sunt prezente.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + }, + "device": { + "label": "Tip dispozitiv", + "description": "Dispozitivul de folosit pentru inferența ONNX (ex. 'AUTO', 'CPU', 'GPU')." + } + }, + "openvino": { + "label": "OpenVINO", + "description": "Detector OpenVINO pentru procesoare AMD și Intel, GPU-uri Intel și hardware Intel VPU.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + }, + "device": { + "label": "Tip dispozitiv", + "description": "Dispozitivul de folosit pentru inferența OpenVINO (ex. 'CPU', 'GPU', 'NPU')." + } + }, + "rknn": { + "label": "RKNN", + "description": "Detector RKNN pentru NPU-uri Rockchip; rulează modele RKNN compilate pe hardware Rockchip.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + }, + "num_cores": { + "label": "Număr de nuclee NPU de folosit.", + "description": "Numărul de nuclee NPU de folosit (0 pentru auto)." + } + }, + "synaptics": { + "label": "Synaptics", + "description": "Detector Synaptics NPU pentru modele în format .synap folosind Synap SDK pe hardware Synaptics.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + } + }, + "teflon_tfl": { + "label": "Teflon", + "description": "Detector cu delegat Teflon pentru TFLite care folosește biblioteca Mesa Teflon pentru a accelera inferența pe GPU-urile suportate.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + } + }, + "tensorrt": { + "label": "TensorRT", + "description": "Detector TensorRT pentru dispozitive Nvidia Jetson care folosește motoare TensorRT serializate pentru inferență accelerată.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurare model specific detectorului", + "description": "Opțiuni de configurare specifice modelului detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale labelmap pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice la etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau reamapări de intrări pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Mapare de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu, 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unele detectoare pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specific detectorului", + "description": "Calea către binarul modelului detectorului, dacă este cerut de detectorul ales." + }, + "device": { + "label": "Index dispozitiv GPU", + "description": "Indexul dispozitivului GPU de folosit." + } + }, + "zmq": { + "label": "Detector ZMQ IPC", + "description": "Detector ZMQ IPC care trimite procesul de inferență către un proces extern printr-un endpoint ZeroMQ IPC.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurație model specifică detectorului", + "description": "Opțiuni de configurare specifice modelului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model personalizat detecție obiecte", + "description": "Calea către un fișier de model personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale label map pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice în etichete text pentru detector." + }, + "width": { + "label": "Lățime intrare model detecție obiecte", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție obiecte", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri sau intrări de re-mapare pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la etichete atribute", + "description": "Maparea de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu 'mașină' -> ['număr_înmatriculare'])." + }, + "input_tensor": { + "label": "Formă tensor intrare model", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare model", + "description": "Tipul de date al tensorului de intrare (de exemplu 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului (ssd, yolox, yolonas) utilizat de unii detectori pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specifică detectorului", + "description": "Calea către binarul modelului, dacă este cerut de detectorul ales." + }, + "endpoint": { + "label": "Endpoint ZMQ IPC", + "description": "Endpoint-ul ZMQ la care se face conexiunea." + }, + "request_timeout_ms": { + "label": "Timeout cerere ZMQ (ms)", + "description": "Timpul de expirare pentru cererile ZMQ în milisecunde." + }, + "linger_ms": { + "label": "Linger socket ZMQ (ms)", + "description": "Perioada de tip 'linger' a socket-ului în milisecunde." + } + }, + "axengine": { + "label": "NPU AXEngine", + "description": "Detector NPU AXERA AX650N/AX8850N care rulează fișiere .axmodel compilate prin intermediul runtime-ului AXEngine.", + "type": { + "label": "Tip" + }, + "model": { + "label": "Configurație model specifică detectorului", + "description": "Opțiuni de configurare a modelului specifice detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Calea către modelul personalizat de detecție a obiectelor", + "description": "Calea către un fișier de model de detecție personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Harta etichetelor pentru detectorul de obiecte personalizat", + "description": "Calea către un fișier de hartă a etichetelor care asociază clasele numerice cu etichete de tip text pentru detector." + }, + "width": { + "label": "Lățimea de intrare a modelului de detecție a obiectelor", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțimea tensorului de intrare al modelului în pixeli", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizarea hărții etichetelor", + "description": "Suprascrieri sau reasocieri de intrări pentru a le fuziona în harta de etichete standard." + }, + "attributes_map": { + "label": "Harta etichetelor de obiecte către etichetele atributelor acestora", + "description": "Harta de la etichetele obiectelor la etichetele atributelor utilizate pentru a atașa metadate (de exemplu „car” -> [„license_plate”])." + }, + "input_tensor": { + "label": "Forma tensorului de intrare al modelului", + "description": "Formatul tensorului așteptat de model: „nhwc” sau „nchw”." + }, + "input_pixel_format": { + "label": "Format culoare pixeli pentru intrarea modelului", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip D intrare model", + "description": "Tipul de date al tensorului de intrare al modelului (de exemplu 'float32')." + }, + "model_type": { + "label": "Tip model detecție obiecte", + "description": "Tipul arhitecturii modelului detector (ssd, yolox, yolonas) folosit de unii detectori pentru optimizare." + } + }, + "model_path": { + "label": "Cale model specifică detectorului", + "description": "Calea fișierului către binarul modelului detector, dacă este cerută de detectorul ales." + } + }, + "model": { + "label": "Configurația modelului specifică detectorului", + "description": "Opțiuni de configurare a modelului specifice detectorului (cale, dimensiune intrare etc.).", + "path": { + "label": "Cale model detector de obiecte personalizat", + "description": "Calea către un fișier al modelului personalizat de detecție (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Harta de etichete pentru detectorul personalizat de obiecte", + "description": "Calea către un fișier labelmap care asociază clasele numerice cu etichete text pentru detector." + }, + "width": { + "label": "Lățimea de intrare pentru modelul de detecție a obiectelor", + "description": "Lățimea tensorului de intrare al modelului în pixeli." + }, + "height": { + "label": "Înălțimea de intrare pentru modelul de detecție a obiectelor", + "description": "Înălțimea tensorului de intrare al modelului în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrie sau remapază intrările pentru a fi combinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Harta etichetelor obiectelor la etichetele atributelor lor", + "description": "Maparea de la etichetele obiectelor la etichetele atributelor folosite pentru a atașa metadate (de exemplu 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Forma tensorului de intrare al modelului", + "description": "Formatul tensorului așteptat de model: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Formatul de culoare al pixelilor de intrare pentru model", + "description": "Spațiul de culoare al pixelilor așteptat de model: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tipul de date (D Type) de intrare pentru model", + "description": "Tipul de date pentru tensorul de intrare al modelului (de exemplu 'float32')." + }, + "model_type": { + "label": "Tipul modelului de detecție a obiectelor", + "description": "Tipul arhitecturii modelului detectorului (ssd, yolox, yolonas) folosit de unii detectori pentru optimizare." + } + }, + "model_path": { + "label": "Calea modelului specifică detectorului", + "description": "Calea fișierului către binarul modelului detectorului, dacă e cerută de detectorul ales." + } + }, + "model": { + "label": "Model detecție", + "description": "Setări pentru configurarea unui model personalizat de detecție și a formei intrării acestuia.", + "path": { + "label": "Cale model detector de obiecte personalizat", + "description": "Calea către un fișier de model personalizat (sau plus:// pentru modelele Frigate+)." + }, + "labelmap_path": { + "label": "Cale label map pentru detector personalizat", + "description": "Calea către un fișier labelmap care mapează clasele numerice în etichete text." + }, + "width": { + "label": "Lățime intrare model detecție", + "description": "Lățimea tensorului de intrare în pixeli." + }, + "height": { + "label": "Înălțime intrare model detecție", + "description": "Înălțimea tensorului de intrare în pixeli." + }, + "labelmap": { + "label": "Personalizare labelmap", + "description": "Suprascrieri pentru a fi îmbinate în labelmap-ul standard." + }, + "attributes_map": { + "label": "Mapare etichete obiecte la atribute", + "description": "Asocierea obiectelor cu atributele lor pentru metadate." + }, + "input_tensor": { + "label": "Formă tensor intrare", + "description": "Formatul tensorului: 'nhwc' sau 'nchw'." + }, + "input_pixel_format": { + "label": "Format pixeli intrare", + "description": "Spațiul de culoare: 'rgb', 'bgr' sau 'yuv'." + }, + "input_dtype": { + "label": "Tip date intrare", + "description": "Tipul de date (ex: 'float32')." + }, + "model_type": { + "label": "Tip model detecție", + "description": "Arhitectura modelului (ssd, yolox, yolonas)." + } + }, + "genai": { + "label": "Configurație AI generativ", + "description": "Setări pentru furnizorii de AI generativ folosiți pentru descrieri de obiecte și rezumate.", + "api_key": { + "label": "Cheie API", + "description": "Cheia API necesară (poate fi setată și prin variabile de mediu)." + }, + "base_url": { + "label": "URL de bază", + "description": "URL-ul de bază pentru furnizori self-hosted (ex: o instanță Ollama)." + }, + "model": { + "label": "Model", + "description": "Modelul utilizat pentru descrieri sau rezumate." + }, + "provider": { + "label": "Furnizor", + "description": "Furnizorul GenAI (ex: ollama, gemini, openai)." + }, + "roles": { + "label": "Roluri", + "description": "Roluri GenAI (chat, descrieri, înglobări); un furnizor per rol." + }, + "provider_options": { + "label": "Opțiuni furnizor", + "description": "Opțiuni suplimentare trimise către clientul GenAI." + }, + "runtime_options": { + "label": "Opțiuni runtime", + "description": "Opțiuni trimise furnizorului la fiecare apel de inferență." + } + }, + "classification": { + "label": "Clasificare obiecte", + "description": "Setări pentru modelele de clasificare folosite pentru a rafina etichetele sau stările.", + "bird": { + "label": "Configurație clasificare păsări", + "description": "Setări specifice pentru modelele de clasificare a păsărilor.", + "enabled": { + "label": "Clasificare păsări", + "description": "Activează sau dezactivează clasificarea păsărilor." + }, + "threshold": { + "label": "Scor minim", + "description": "Scorul minim pentru a accepta clasificarea unei păsări." + } + }, + "custom": { + "label": "Modele de clasificare personalizate", + "description": "Configurarea modelelor personalizate pentru obiecte sau stări.", + "enabled": { + "label": "Activare model", + "description": "Activează sau dezactivează modelul personalizat." + }, + "name": { + "label": "Nume model", + "description": "Identificatorul modelului de clasificare." + }, + "threshold": { + "label": "Prag scor", + "description": "Pragul folosit pentru a schimba starea de clasificare." + }, + "save_attempts": { + "label": "Salvează încercările", + "description": "Câte încercări de clasificare să fie păstrate pentru interfața de istoric." + }, + "object_config": { + "objects": { + "label": "Clasifică obiecte", + "description": "Lista de tipuri de obiecte pe care se face clasificare." + }, + "classification_type": { + "label": "Tip clasificare", + "description": "Tipul aplicat: 'sub_label' (adaugă o sub-etichetă) sau altele." + } + }, + "state_config": { + "cameras": { + "label": "Camere clasificare", + "description": "Decupaj și setări per cameră pentru clasificarea stărilor.", + "crop": { + "label": "Crop clasificare", + "description": "Coordonatele de crop folosite pentru clasificare pe această cameră." + } + }, + "motion": { + "label": "Rulează la mișcare", + "description": "Dacă e activ, rulează clasificarea când se detectează mișcare în zona de crop." + }, + "interval": { + "label": "Interval clasificare", + "description": "Intervalul (secunde) între rulările periodice pentru clasificarea stărilor." + } + } + } + }, + "camera_groups": { + "label": "Grupuri camere", + "description": "Configurație pentru grupuri de camere denumite, folosite pentru a organiza camerele în interfață.", + "cameras": { + "label": "Listă camere", + "description": "Listă de nume de camere incluse în acest grup." + }, + "icon": { + "label": "Pictogramă grup", + "description": "Pictogramă folosită pentru a reprezenta grupul de camere în interfață." + }, + "order": { + "label": "Ordine sortare", + "description": "Ordinea numerică folosită pentru sortarea grupurilor de camere în interfață; numerele mai mari apar mai târziu." + } + }, + "camera_mqtt": { + "label": "MQTT", + "description": "Setări pentru publicarea imaginilor prin MQTT.", + "enabled": { + "label": "Trimite imaginea", + "description": "Activează publicarea de snapshot-uri cu obiecte către subiectele MQTT pentru această cameră." + }, + "timestamp": { + "label": "Adaugă timestamp", + "description": "Suprapune un timestamp pe imaginile publicate prin MQTT." + }, + "bounding_box": { + "label": "Adaugă bounding box", + "description": "Desenază bounding box-uri pe imaginile publicate prin MQTT." + }, + "crop": { + "label": "Decupează imaginea", + "description": "Decupează imaginile publicate prin MQTT la bounding box-ul obiectului detectat." + }, + "height": { + "label": "Înălțime imagine", + "description": "Înălțimea (pixeli) pentru redimensionarea imaginilor publicate prin MQTT." + }, + "required_zones": { + "label": "Zone obligatorii", + "description": "Zonele în care trebuie să intre un obiect pentru ca un snapshot prin MQTT să fie publicat." + }, + "quality": { + "label": "Calitate JPEG", + "description": "Calitatea JPEG pentru imaginile publicate prin MQTT (0-100)." + } + }, + "camera_ui": { + "label": "Interfață cameră", + "description": "Ordinea de afișare și vizibilitatea pentru această cameră în interfață. Ordinea afectează dashboard-ul implicit. Pentru un control mai detaliat, folosește grupurile de camere.", + "order": { + "label": "Ordine interfață", + "description": "Ordinea numerică folosită pentru sortarea camerei în interfață (dashboard-ul implicit și liste); numerele mai mari apar mai târziu." + }, + "dashboard": { + "label": "Arată în interfață", + "description": "Comută dacă această cameră este vizibilă peste tot în interfața Frigate. Dezactivarea acestei opțiuni va necesita editarea manuală a config-ului pentru a vedea din nou camera în interfață." + }, + "review": { + "label": "Arată în Revizuire", + "description": "Comută dacă această cameră este vizibilă în Revizuire (pagina de revizuire și filtrul ei de camere, revizuirea mișcărilor și vizualizarea istoricului)." + } + }, + "profiles": { + "label": "Profiluri", + "description": "Definiții de profiluri numite cu nume prietenoase. Profilurile camerelor trebuie să facă referință la numele definite aici.", + "friendly_name": { + "label": "Nume prietenos", + "description": "Numele afișat pentru acest profil în interfața utilizatorului (UI)." + } + }, + "active_profile": { + "label": "Profil activ", + "description": "Numele profilului activ în prezent. Doar la rulare (runtime), nu este salvat în YAML." + } +} diff --git a/web/public/locales/ro/config/groups.json b/web/public/locales/ro/config/groups.json new file mode 100644 index 0000000..7b0d7a4 --- /dev/null +++ b/web/public/locales/ro/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Detectare globală", + "sensitivity": "Sensibilitate globală" + }, + "cameras": { + "detection": "Detectare", + "sensitivity": "Sensibilitate" + } + }, + "timestamp_style": { + "global": { + "appearance": "Aspect global" + }, + "cameras": { + "appearance": "Aspect" + } + }, + "motion": { + "global": { + "sensitivity": "Sensibilitate globală", + "algorithm": "Algoritm global" + }, + "cameras": { + "sensitivity": "Sensibilitate", + "algorithm": "Algoritm" + } + }, + "snapshots": { + "global": { + "display": "Afișare snapshot-uri globală" + }, + "cameras": { + "display": "Afișare snapshot-uri" + } + }, + "detect": { + "global": { + "resolution": "Rezoluție globală", + "tracking": "Urmărire globală" + }, + "cameras": { + "resolution": "Rezoluție", + "tracking": "Urmărire" + } + }, + "objects": { + "global": { + "tracking": "Urmărire globală", + "filtering": "Filtrare globală" + }, + "cameras": { + "tracking": "Urmărire", + "filtering": "Filtrare" + } + }, + "record": { + "global": { + "retention": "Păstrare globală", + "events": "Evenimente globale" + }, + "cameras": { + "retention": "Păstrare", + "events": "Evenimente" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Argumente FFmpeg specifice camerei" + } + } +} diff --git a/web/public/locales/ro/config/validation.json b/web/public/locales/ro/config/validation.json new file mode 100644 index 0000000..4f0dbab --- /dev/null +++ b/web/public/locales/ro/config/validation.json @@ -0,0 +1,35 @@ +{ + "minimum": "Trebuie să fie cel puțin {{limit}}", + "maximum": "Trebuie să fie cel mult {{limit}}", + "exclusiveMinimum": "Trebuie să fie mai mare de {{limit}}", + "exclusiveMaximum": "Trebuie să fie mai mic de {{limit}}", + "minLength": "Trebuie să aibă cel puțin {{limit}} caracter(e)", + "maxLength": "Trebuie să aibă cel mult {{limit}} caracter(e)", + "minItems": "Trebuie să conțină cel puțin {{limit}} elemente", + "maxItems": "Trebuie să conțină cel mult {{limit}} elemente", + "pattern": "Format nevalid", + "required": "Acest câmp este obligatoriu", + "type": "Tip de valoare nevalid", + "enum": "Trebuie să fie una dintre valorile permise", + "const": "Valoarea nu corespunde constantei așteptate", + "uniqueItems": "Toate elementele trebuie să fie unice", + "format": "Format nevalid", + "additionalProperties": "Proprietatea necunoscută nu este permisă", + "oneOf": "Trebuie să corespundă exact uneia dintre schemele permise", + "anyOf": "Trebuie să corespundă cel puțin uneia dintre schemele permise", + "proxy": { + "header_map": { + "roleHeaderRequired": "Header-ul de rol este obligatoriu atunci când sunt configurate mapări de roluri." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "Fiecare rol poate fi atribuit unui singur stream.", + "detectRequired": "Cel puțin un stream trebuie să aibă atribuit rolul 'detect'.", + "hwaccelDetectOnly": "Doar stream-ul cu rolul 'detect' poate defini argumente pentru accelerare hardware." + } + }, + "detect": { + "dimensionMustBeEven": "Trebuie să fie un număr par." + } +} diff --git a/web/public/locales/ro/objects.json b/web/public/locales/ro/objects.json new file mode 100644 index 0000000..122244d --- /dev/null +++ b/web/public/locales/ro/objects.json @@ -0,0 +1,130 @@ +{ + "person": "Persoană", + "bicycle": "Bicicletă", + "car": "Mașină", + "airplane": "Avion", + "bus": "Autobuz", + "train": "Tren", + "boat": "Barcă", + "fire_hydrant": "Hidrant", + "street_sign": "Semn de Circulatie", + "stop_sign": "Semn de Stop", + "parking_meter": "Automat de Parcare", + "bench": "Bancheta", + "bird": "Pasare", + "cat": "Pisică", + "dog": "Câine", + "horse": "Cal", + "cow": "Vacă", + "elephant": "Elefant", + "bear": "Urs", + "giraffe": "Girafa", + "hat": "Palarie", + "backpack": "Rucsac", + "umbrella": "Umbrela", + "shoe": "Pantof", + "eye_glasses": "Ochelari", + "tie": "Cravata", + "suitcase": "Servieta", + "frisbee": "Frisbee", + "skis": "Schiuri", + "snowboard": "Placa de Snowboard", + "sports_ball": "Minge pentru Sport", + "kite": "Zmeu", + "baseball_bat": "Bata de Baseball", + "baseball_glove": "Manusa de Baseball", + "skateboard": "Skateboard", + "surfboard": "Placa de Surf", + "tennis_racket": "Racheta de Tenis", + "bottle": "Sticla", + "plate": "Placa", + "wine_glass": "Pahar de Vin", + "cup": "Ceasca", + "fork": "Furculita", + "knife": "Cutit", + "spoon": "Lingura", + "bowl": "Castron", + "banana": "Banana", + "apple": "Mar", + "motorcycle": "Motocicletă", + "traffic_light": "Semafor", + "sheep": "Oaie", + "zebra": "Zebra", + "handbag": "Geanta de mana", + "sandwich": "Sandwich", + "gls": "GLS", + "dpd": "DPD", + "sink": "Chiuveta", + "raccoon": "Raton", + "orange": "Portocala", + "laptop": "Laptop", + "fox": "Vulpe", + "animal": "Animal", + "package": "Pachet", + "remote": "Telecomanda", + "toilet": "Toaleta", + "amazon": "Amazon", + "broccoli": "Broccoli", + "carrot": "Morcov", + "hot_dog": "Hot Dog", + "dining_table": "Masa", + "hair_dryer": "Uscator de Par", + "pizza": "Pizza", + "donut": "Gogoasa", + "teddy_bear": "Ursulet de Plus", + "waste_bin": "Tomberon", + "cake": "Tort", + "window": "Fereastra", + "chair": "Scaun", + "door": "Usa", + "on_demand": "La Cerere", + "usps": "USPS", + "couch": "Canapea", + "blender": "Blender", + "scissors": "Foarfeca", + "cell_phone": "Telefon Mobil", + "potted_plant": "Ghiveci de Plante", + "bed": "Pat", + "refrigerator": "Frigider", + "mirror": "Oglinda", + "desk": "Birou", + "tv": "TV", + "ups": "UPS", + "fedex": "FedEx", + "mouse": "Soarece", + "keyboard": "Orga", + "microwave": "Microunde", + "oven": "Cuptor", + "rabbit": "Iepure", + "robot_lawnmower": "Robot de Tuns Iarba", + "toaster": "Prajitor de Paine", + "book": "Carte", + "clock": "Ceas", + "vase": "Vaza", + "toothbrush": "Periuta de Dinti", + "hair_brush": "Perie de Par", + "vehicle": "Vehicul", + "squirrel": "Veverita", + "deer": "Caprioara", + "bark": "Latrat", + "goat": "Capra", + "bbq_grill": "Gratar", + "face": "Fata", + "purolator": "Purolator", + "license_plate": "Numar de Inmatriculare", + "dhl": "DHL", + "an_post": "An Post", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "canada_post": "Canada Post", + "royal_mail": "Royal Mail", + "school_bus": "Autobus Scolar", + "skunk": "Sconcs", + "kangaroo": "Cangur", + "baby": "Bebeluș", + "baby_stroller": "Cărucior de copii", + "rickshaw": "Ricșă", + "Rodent": "Rozătoare", + "rodent": "Rozătoare" +} diff --git a/web/public/locales/ro/views/chat.json b/web/public/locales/ro/views/chat.json new file mode 100644 index 0000000..36a892c --- /dev/null +++ b/web/public/locales/ro/views/chat.json @@ -0,0 +1,72 @@ +{ + "documentTitle": "Chat - Frigate", + "title": "Chat Frigate", + "subtitle": "Asistentul tău AI pentru gestionarea camerelor și informații", + "placeholder": "Întreabă orice...", + "error": "Ceva a mers prost. Te rog încearcă din nou.", + "processing": "Procesare...", + "toolsUsed": "Folosit: {{tools}}", + "showTools": "Arată uneltele ({{count}})", + "hideTools": "Ascunde uneltele", + "call": "Apelează", + "result": "Rezultat", + "arguments": "Argumente:", + "response": "Răspuns:", + "attachment_chip_label": "{{label}} pe {{camera}}", + "attachment_chip_remove": "Elimină atașamentul", + "open_in_explore": "Deschide în Explorare", + "attach_event_aria": "Atașează evenimentul {{eventId}}", + "attachment_picker_paste_label": "Sau lipește ID-ul evenimentului", + "attachment_picker_attach": "Atașează", + "attachment_picker_placeholder": "Atașează un eveniment", + "quick_reply_find_similar": "Găsește apariții similare", + "quick_reply_tell_me_more": "Spune-mi mai multe despre asta", + "quick_reply_when_else": "Când a mai fost văzut?", + "quick_reply_find_similar_text": "Găsește apariții similare cu aceasta.", + "quick_reply_tell_me_more_text": "Spune-mi mai multe despre acesta.", + "quick_reply_when_else_text": "Când a mai fost văzut acesta?", + "anchor": "Referință", + "similarity_score": "Similaritate", + "no_similar_objects_found": "Nu au fost găsite obiecte similare.", + "semantic_search_required": "Căutarea semantică trebuie să fie activată pentru a găsi obiecte similare.", + "send": "Trimite", + "suggested_requests": "Încearcă să întrebi:", + "starting_requests": { + "show_recent_events": "Arată evenimentele recente", + "show_camera_status": "Arată starea camerei", + "recap": "Ce s-a întâmplat cât am fost plecat?", + "watch_camera": "Urmărește o cameră pentru activitate" + }, + "starting_requests_prompts": { + "show_recent_events": "Arată-mi evenimentele recente din ultima oră", + "show_camera_status": "Care este starea actuală a camerelor mele?", + "recap": "Ce s-a întâmplat cât am fost plecat?", + "watch_camera": "Urmărește ușa din față și anunță-mă dacă apare cineva" + }, + "new_chat": "Chat nou", + "settings": { + "title": "Setări chat", + "show_stats": { + "title": "Afișează statistici", + "desc": "Afișează rata de generare și dimensiunea contextului pentru răspunsurile de chat.", + "always": "Întotdeauna", + "while_generating": "În timpul generării" + }, + "auto_scroll": { + "title": "Derulare automată", + "desc": "Urmărește mesajele noi pe măsură ce sosesc." + } + }, + "stats": { + "tokens_per_second": "{{rate}} t/s", + "context": "{{tokens}} token-uri" + }, + "reasoning": { + "active": "Raționament…", + "show": "Afișează raționamentul", + "hide": "Ascunde raționamentul" + }, + "thinking": { + "toggle": "Comută gândirea" + } +} diff --git a/web/public/locales/ro/views/classificationModel.json b/web/public/locales/ro/views/classificationModel.json new file mode 100644 index 0000000..8a7a077 --- /dev/null +++ b/web/public/locales/ro/views/classificationModel.json @@ -0,0 +1,206 @@ +{ + "documentTitle": "Modele de clasificare - Frigate", + "button": { + "deleteClassificationAttempts": "Șterge imaginile de clasificare", + "renameCategory": "Redenumește clasa", + "deleteCategory": "Șterge clasa", + "deleteImages": "Șterge imaginile", + "trainModel": "Antrenează modelul", + "addClassification": "Adaugă clasificare", + "deleteModels": "Șterge modelele", + "editModel": "Editează modelul" + }, + "toast": { + "success": { + "deletedCategory_one": "Am șters {{count}} clasă", + "deletedCategory_few": "Am șters {{count}} clase", + "deletedCategory_other": "Am șters {{count}} de clase", + "deletedImage_one": "Am șters {{count}} imagine", + "deletedImage_few": "Am șters {{count}} imagini", + "deletedImage_other": "Am șters {{count}} de imagini", + "categorizedImage": "Imagine clasificată cu succes", + "trainedModel": "Model antrenat cu succes.", + "trainingModel": "Antrenamentul modelului a fost pornit cu succes.", + "deletedModel_one": "{{count}} model șters cu succes", + "deletedModel_few": "{{count}} modele șterse cu succes", + "deletedModel_other": "{{count}} modele șterse cu succes", + "updatedModel": "Configurația modelului a fost actualizată cu succes", + "renamedCategory": "Clasa a fost redenumită cu succes în {{name}}", + "reclassifiedImage": "Imagine reclasificată cu succes" + }, + "error": { + "deleteImageFailed": "Ștergerea a eșuat: {{errorMessage}}", + "deleteCategoryFailed": "Ștergerea clasei a eșuat: {{errorMessage}}", + "categorizeFailed": "Categorisirea imaginii a eșuat: {{errorMessage}}", + "trainingFailed": "Antrenarea modelului a eșuat. Verifică jurnalele Frigate pentru detalii.", + "deleteModelFailed": "Ștergerea modelului a eșuat: {{errorMessage}}", + "updateModelFailed": "Actualizarea modelului a eșuat: {{errorMessage}}", + "renameCategoryFailed": "Redenumirea clasei a eșuat: {{errorMessage}}", + "trainingFailedToStart": "Nu s-a putut porni antrenarea modelului: {{errorMessage}}", + "reclassifyFailed": "Nu am putut reclasifica imaginea: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Șterge clasa", + "desc": "Sigur doriți să ștergeți clasa {{name}}? Aceasta va șterge permanent toate imaginile asociate și va necesita reantrenarea modelului.", + "minClassesTitle": "Nu se poate șterge clasa", + "minClassesDesc": "Un model de clasificare trebuie să aibă cel puțin 2 clase. Adaugă o altă clasă înainte de a o șterge pe aceasta." + }, + "deleteDatasetImages": { + "title": "Șterge imaginile setului de date", + "desc_one": "Sigur doriți să ștergeți {{count}} imagine din {{dataset}}? Această acțiune nu poate fi anulată și va necesita reantrenarea modelului.", + "desc_few": "Sigur doriți să ștergeți {{count}} imagini din {{dataset}}? Această acțiune nu poate fi anulată și va necesita reantrenarea modelului.", + "desc_other": "Sigur doriți să ștergeți {{count}} de imagini din {{dataset}}? Această acțiune nu poate fi anulată și va necesita reantrenarea modelului." + }, + "deleteTrainImages": { + "title": "Șterge imaginile de antrenament", + "desc_one": "Sigur doriți să ștergeți {{count}} imagine? Această acțiune nu poate fi anulată.", + "desc_few": "Sigur doriți să ștergeți {{count}} imagini? Această acțiune nu poate fi anulată.", + "desc_other": "Sigur doriți să ștergeți {{count}} de imagini? Această acțiune nu poate fi anulată." + }, + "renameCategory": { + "title": "Redenumește clasa", + "desc": "Introduceți un nume nou pentru {{name}}. Va trebui să reantrenați modelul pentru ca modificarea numelui să aibă efect." + }, + "description": { + "invalidName": "Nume invalid. Numele pot include doar litere, cifre, spații, apostrofuri, underscore-uri și liniuțe." + }, + "train": { + "title": "Clasificări recente", + "titleShort": "Recent", + "aria": "Selectează clasificările recente" + }, + "categories": "Clase", + "createCategory": { + "new": "Creează clasă nouă" + }, + "categorizeImageAs": "Clasifică imaginea ca:", + "categorizeImage": "Clasifică imaginea", + "noModels": { + "object": { + "title": "Nu există modele de clasificare a obiectelor", + "description": "Creează un model personalizat pentru a clasifica obiectele detectate.", + "buttonText": "Creează model de obiect" + }, + "state": { + "title": "Nu există modele de clasificare a stării", + "description": "Creează un model personalizat pentru a monitoriza și clasifica schimbările de stare în anumite zone ale camerei.", + "buttonText": "Creează model de stare" + } + }, + "wizard": { + "title": "Creează clasificare nouă", + "steps": { + "nameAndDefine": "Numire și definire", + "stateArea": "Zona de stare", + "chooseExamples": "Alege exemple" + }, + "step1": { + "description": "Modelele de stare monitorizează zone fixe ale camerei pentru schimbări (de exemplu, ușă deschisă/închisă). Modelele de obiect adaugă clasificări obiectelor detectate (de exemplu, animale cunoscute, curieri etc.).", + "name": "Nume", + "namePlaceholder": "Introduceți numele modelului...", + "type": "Tip", + "typeState": "Stare", + "typeObject": "Obiect", + "objectLabel": "Etichetă obiect", + "objectLabelPlaceholder": "Selectează tipul obiectului...", + "classificationType": "Tip de clasificare", + "classificationTypeTip": "Află despre tipurile de clasificare", + "classificationTypeDesc": "Subetichetele adaugă text suplimentar la eticheta obiectului (de exemplu, 'Persoană: UPS'). Atributele sunt metadate căutabile, stocate separat în metadatele obiectului.", + "classificationSubLabel": "Subeticheta", + "classificationAttribute": "Atribut", + "classes": "Clase", + "classesTip": "Află despre clase", + "classesStateDesc": "Definește diferitele stări în care poate fi zona camerei tale. De exemplu: 'deschis' și 'închis' pentru o ușă de garaj.", + "classesObjectDesc": "Definește diferitele categorii în care să fie clasificate obiectele detectate. De exemplu: 'curier', 'rezident', 'necunoscut' pentru clasificarea persoanelor.", + "classPlaceholder": "Introduceți numele clasei...", + "errors": { + "nameRequired": "Numele modelului este obligatoriu", + "nameLength": "Numele modelului trebuie să aibă 64 de caractere sau mai puțin", + "nameOnlyNumbers": "Numele modelului nu poate conține doar cifre", + "classRequired": "Este necesară cel puțin 1 clasă", + "classesUnique": "Numele claselor trebuie să fie unice", + "stateRequiresTwoClasses": "Modelele de stare necesită cel puțin 2 clase", + "objectLabelRequired": "Vă rugăm să selectați o etichetă de obiect", + "objectTypeRequired": "Vă rugăm să selectați un tip de clasificare", + "noneNotAllowed": "Clasa 'niciuna' nu este permisă" + }, + "states": "Stări" + }, + "step2": { + "description": "Selectați camerele și definiți zona de monitorizat pentru fiecare cameră. Modelul va clasifica starea acestor zone.", + "cameras": "Camere", + "selectCamera": "Selectează camera", + "noCameras": "Apasă pe + pentru a adăuga camere", + "selectCameraPrompt": "Selectați o cameră din listă pentru a defini aria sa de monitorizare" + }, + "step3": { + "selectImagesPrompt": "Selectați toate imaginile cu: {{className}}", + "selectImagesDescription": "Apăsați pe imagini pentru a le selecta. Apăsați pe Continuare când ați terminat cu această clasă.", + "generating": { + "title": "Generare imagini de exemplu", + "description": "Frigate preia imagini reprezentative din înregistrările tale. Aceasta poate dura câteva momente..." + }, + "training": { + "title": "Antrenare model", + "description": "Modelul tău este antrenat în fundal. Închide această fereastră și modelul va începe să ruleze imediat ce antrenamentul este finalizat." + }, + "retryGenerate": "Reîncearcă generarea", + "noImages": "Nu s-au generat imagini de exemplu", + "classifying": "Clasificare și antrenare...", + "trainingStarted": "Antrenamentul a început cu succes", + "errors": { + "noCameras": "Nu există camere configurate", + "noObjectLabel": "Nu a fost selectată nicio etichetă de obiect", + "generateFailed": "Generarea exemplelor a eșuat: {{error}}", + "generationFailed": "Generarea a eșuat. Vă rugăm să încercați din nou.", + "classifyFailed": "Clasificarea imaginilor a eșuat: {{error}}" + }, + "generateSuccess": "Imaginile de exemplu au fost generate cu succes", + "allImagesRequired_one": "Te rog să clasifici toate imaginile. {{count}} imagine rămasă.", + "allImagesRequired_few": "Te rog să clasifici toate imaginile. {{count}} imagini rămase.", + "allImagesRequired_other": "Te rog să clasifici toate imaginile. {{count}} de imagini rămase.", + "modelCreated": "Modelul a fost creat cu succes. Folosește vizualizarea Clasificări recente pentru a adăuga imagini pentru stările lipsă, apoi antrenează modelul.", + "missingStatesWarning": { + "title": "Exemple de clase lipsă", + "description": "Nu toate clasele au exemple. Încearcă să generezi exemple noi pentru a găsi clasa lipsă, sau continuă și folosește vizualizarea Clasificări recente pentru a adăuga imagini mai târziu." + }, + "refreshExamples": "Generează exemple noi", + "refreshConfirm": { + "title": "Generezi exemple noi?", + "description": "Asta va genera un set nou de imagini și va goli toate selecțiile, inclusiv clasele anterioare. Va trebui să selectezi din nou exemple pentru toate clasele." + } + } + }, + "deleteModel": { + "title": "Șterge modelul de clasificare", + "single": "Sigur doriți să ștergeți {{name}}? Aceasta va șterge permanent toate datele asociate, inclusiv imaginile și datele de antrenament. Această acțiune nu poate fi anulată.", + "desc_one": "Sigur doriți să ștergeți {{count}} model? Aceasta va șterge permanent toate datele asociate, inclusiv imaginile și datele de antrenament. Această acțiune nu poate fi anulată.", + "desc_few": "Sigur doriți să ștergeți {{count}} modele? Aceasta va șterge permanent toate datele asociate, inclusiv imaginile și datele de antrenament. Această acțiune nu poate fi anulată.", + "desc_other": "Sigur doriți să ștergeți {{count}} de modele? Aceasta va șterge permanent toate datele asociate, inclusiv imaginile și datele de antrenament. Această acțiune nu poate fi anulată." + }, + "menu": { + "objects": "Obiecte", + "states": "Stări" + }, + "details": { + "scoreInfo": "Scorul reprezintă încrederea medie a clasificării pentru toate detecțiile acestui obiect.", + "none": "Niciuna", + "unknown": "Necunoscut" + }, + "edit": { + "title": "Editează modelul de clasificare", + "descriptionState": "Editează clasele pentru acest model de clasificare a stării. Modificările vor necesita reantrenarea modelului.", + "descriptionObject": "Editează tipul de obiect și tipul de clasificare pentru acest model de clasificare a obiectelor.", + "stateClassesInfo": "Notă: Modificarea claselor de stare necesită reantrenarea modelului cu clasele actualizate." + }, + "tooltip": { + "trainingInProgress": "Modelul este în curs de antrenare", + "noNewImages": "Nu există imagini noi pentru antrenare. Clasifică mai întâi mai multe imagini în setul de date.", + "modelNotReady": "Modelul nu este pregătit pentru antrenare", + "noChanges": "Nicio modificare a setului de date de la ultima antrenare." + }, + "none": "Niciuna", + "reclassifyImageAs": "Reclasifică imaginea ca:", + "reclassifyImage": "Reclasifică imaginea" +} diff --git a/web/public/locales/ro/views/configEditor.json b/web/public/locales/ro/views/configEditor.json new file mode 100644 index 0000000..165bd90 --- /dev/null +++ b/web/public/locales/ro/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor de configurație - Frigate", + "configEditor": "Editor de configurație", + "copyConfig": "Copiază", + "saveAndRestart": "Salvează și repornește", + "saveOnly": "Salvează", + "toast": { + "success": { + "copyToClipboard": "Setări copiate." + }, + "error": { + "savingError": "Eroare la salvarea setărilor" + } + }, + "confirm": "Ieși fără să salvezi?", + "safeConfigEditor": "Editor de configurație (mod de siguranță)", + "safeModeDescription": "Frigate este în modul de siguranță din cauza unei erori de validare a configurației." +} diff --git a/web/public/locales/ro/views/events.json b/web/public/locales/ro/views/events.json new file mode 100644 index 0000000..5adb141 --- /dev/null +++ b/web/public/locales/ro/views/events.json @@ -0,0 +1,94 @@ +{ + "alerts": "Alerte", + "motion": { + "label": "Mișcare", + "only": "Doar mișcare" + }, + "allCameras": "Toate Camerele", + "empty": { + "alert": "Nu sunt alerte de revizuit", + "detection": "Nu sunt detecții de revizuit", + "motion": "Nu au fost găsite date despre mișcare", + "recordingsDisabled": { + "title": "Înregistrările trebuie să fie activate", + "description": "Elementele de revizuire pot fi create doar pentru o cameră atunci când înregistrările sunt activate pentru acea cameră." + } + }, + "timeline": { + "label": "Cronologie" + }, + "timeline.aria": "Selectează cronologia", + "events": { + "aria": "Selectează evenimente", + "noFoundForTimePeriod": "Niciun eveniment gasit pentru acest interval de timp.", + "label": "Evenimente" + }, + "documentTitle": "Revizuieste - Frigate", + "recordings": { + "documentTitle": "Inregistrari - frigate", + "invalidSharedLink": "Nu s-a putut deschide linkul înregistrării cu timestamp din cauza unei erori de parsare.", + "invalidSharedCamera": "Nu s-a putut deschide linkul înregistrării cu timestamp din cauza unei camere necunoscute sau neautorizate." + }, + "calendarFilter": { + "last24Hours": "Ultimele 24 de ore" + }, + "markAsReviewed": "Marchează ca revizuit", + "markTheseItemsAsReviewed": "Marchează aceste articole ca revizuite", + "newReviewItems": { + "label": "Vezi articole noi de revizuit", + "button": "Articole Noi de Revizuit" + }, + "camera": "Camera foto", + "detections": "Detecții", + "detected": "detectat", + "selected_one": "{{count}} selectat", + "selected_other": "{{count}} selectate", + "suspiciousActivity": "Activitate suspectă", + "threateningActivity": "Activitate amenințătoare", + "detail": { + "noDataFound": "Nicio dată detaliată de revizuit", + "aria": "Comută vizualizarea detaliată", + "trackedObject_one": "{{count}} obiect", + "trackedObject_other": "{{count}} obiecte", + "noObjectDetailData": "Nicio dată de detaliu obiect disponibilă.", + "label": "Detaliu", + "settings": "Setări vizualizare detaliată", + "alwaysExpandActive": { + "title": "Extinde întotdeauna activul", + "desc": "Extinde întotdeauna detaliile obiectului elementului activ de revizuire, atunci când sunt disponibile." + } + }, + "objectTrack": { + "trackedPoint": "Punct urmărit", + "clickToSeek": "Apasă pentru a naviga la acest moment" + }, + "zoomIn": "Mărește", + "zoomOut": "Micșorează", + "normalActivity": "Normal", + "needsReview": "Necesită revizuire", + "securityConcern": "Potențială problemă de securitate", + "select_all": "Toate", + "motionSearch": { + "menuItem": "Căutare mișcare", + "openMenu": "Opțiuni cameră" + }, + "motionPreviews": { + "menuItem": "Vezi previzualizări mișcare", + "title": "Previzualizări mișcare: {{camera}}", + "mobileSettingsTitle": "Setări previzualizări mișcare", + "mobileSettingsDesc": "Ajustează viteza de redare și estomparea, și alege o dată pentru a revizui clipurile doar cu mișcare.", + "dim": "Estompare", + "dimAria": "Ajustează intensitatea estompării", + "dimDesc": "Crește intensitatea estompării pentru a mări vizibilitatea zonei de mișcare.", + "speed": "Vitez", + "speedAria": "Selectează viteza de redare a previzualizării", + "speedDesc": "Alege cât de repede sunt redate clipurile de previzualizare.", + "back": "Înapoi", + "empty": "Nicio previzualizare disponibilă", + "noPreview": "Previzualizare indisponibilă", + "seekAria": "Derulează player-ul {{camera}} la {{time}}", + "filter": "Filtru", + "filterDesc": "Selectează zonele pentru a afișa doar clipurile cu mișcare în acele regiuni.", + "filterClear": "Șterge" + } +} diff --git a/web/public/locales/ro/views/explore.json b/web/public/locales/ro/views/explore.json new file mode 100644 index 0000000..afdb9d8 --- /dev/null +++ b/web/public/locales/ro/views/explore.json @@ -0,0 +1,319 @@ +{ + "documentTitle": "Căutare - Frigate", + "generativeAI": "AI Generativ", + "exploreIsUnavailable": { + "title": "Explorarea este Indisponibila", + "embeddingsReindexing": { + "startingUp": "Porneste…", + "estimatedTime": "Timp ramas estimat:", + "finishingShortly": "Termina curand", + "step": { + "descriptionsEmbedded": "Descrieri încorporate: ", + "trackedObjectsProcessed": "Obiecte urmărite procesate: ", + "thumbnailsEmbedded": "Miniaturi încorporate: " + }, + "context": "Funcția de căutare poate fi utilizată după ce reindexarea obiectelor înglobate este finalizată." + }, + "downloadingModels": { + "context": "Frigate descarcă modelele de înglobare necesare pentru a susține funcția de Căutare Semantică. Acest lucru poate dura câteva minute, în funcție de viteza conexiunii rețelei dvs.", + "setup": { + "visionModel": "Model viziune", + "visionModelFeatureExtractor": "Extractor de caracteristici pentru modelul de viziune", + "textModel": "Model de text", + "textTokenizer": "Tokenizer text" + }, + "tips": { + "context": "S-ar putea să dorești să reindexezi înglobările obiectelor urmărite odată ce modelele sunt descărcate.", + "documentation": "Citește documentația" + }, + "error": "A apărut o eroare. Verifică jurnalele Frigate." + } + }, + "type": { + "details": "detalii", + "snapshot": "snapshot", + "video": "video", + "object_lifecycle": "ciclul de viață al obiectului", + "thumbnail": "miniatură", + "tracking_details": "detalii de urmărire" + }, + "objectLifecycle": { + "lifecycleItemDesc": { + "visible": "S-a detectat {{label}}", + "active": "{{label}} a devenit activ", + "entered_zone": "{{label}} a intrat în {{zones}}", + "stationary": "{{label}} a devenit staționar", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectat pentru {{label}}", + "other": "{{label}} recunoscut ca {{attribute}}" + }, + "header": { + "zones": "Zone", + "ratio": "Raport", + "area": "Suprafață" + }, + "gone": "{{label}} a părasit cadrul", + "heard": "{{label}} auzit(ă)", + "external": "{{label}} detectat(ă)" + }, + "title": "Ciclul de viață al obiectului", + "count": "{{first}} din {{second}}", + "trackedPoint": "Punct urmărit", + "noImageFound": "Nicio imagine găsită pentru această marcă temporală.", + "createObjectMask": "Creează mască de obiecte", + "adjustAnnotationSettings": "Ajustează setările de adnotare", + "scrollViewTips": "Derulează pentru a vizualiza momentele semnificative din ciclul de viață al acestui obiect.", + "autoTrackingTips": "Pozițiile casetelor de delimitare vor fi inexacte pentru camerele cu urmărire automată.", + "annotationSettings": { + "showAllZones": { + "title": "Afișează toate zonele", + "desc": "Afișează întotdeauna zonele pe cadrele în care obiectele au intrat într-o zonă." + }, + "offset": { + "label": "Compensare adnotare", + "documentation": "Citește documentația ", + "desc": "Aceste date provin din stream-ul de detecție al camerei tale, dar sunt suprapuse pe imaginile din stream-ul de înregistrare. Este puțin probabil ca cele două stream-uri să fie perfect sincronizate. Ca urmare, caseta de delimitare și materialul video nu se vor potrivi perfect. Totuși, câmpul annotation_offset poate fi folosit pentru a ajusta acest lucru.", + "millisecondsToOffset": "Millisecondele cu care să compensezi adnotările de detecție. Implicit: 0", + "tips": "SFAT: Imaginează-ți că există un clip de eveniment cu o persoană care merge de la stânga la dreapta. Dacă caseta de delimitare de pe linia temporală a evenimentului este constant în partea stângă a persoanei, atunci valoarea ar trebui să fie scăzută. În mod similar, dacă persoana merge de la stânga la dreapta și caseta de delimitare este constant înaintea persoanei, atunci valoarea ar trebui să fie crescută.", + "toast": { + "success": "Compensarea adnotării pentru {{camera}} a fost salvată în fișierul de configurație. Repornește Frigate pentru a aplica modificările." + } + }, + "title": "Setări adnotare" + }, + "carousel": { + "previous": "Slide-ul anterior", + "next": "Slide-ul următor" + } + }, + "details": { + "timestamp": "Marcaj timp", + "item": { + "title": "Revizuiește detaliile articolului", + "desc": "Revizuiește detaliile articolului", + "button": { + "share": "Partajează acest articol de revizuire", + "viewInExplore": "Vezi în explorator" + }, + "tips": { + "mismatch_one": "{{count}} obiect indisponibil a fost detectat și inclus în acest element de revizuire. Acest obiect fie nu s-a calificat ca alertă sau detecție, fie a fost deja curățat/șters.", + "mismatch_few": "{{count}} obiecte indisponibile au fost detectate și incluse în acest element de revizuire. Aceste obiecte fie nu s-au calificat ca alertă sau detecție, fie au fost deja curățate/șterse.", + "mismatch_other": "{{count}} de obiecte indisponibile au fost detectate și incluse în acest element de revizuire. Aceste obiecte fie nu s-au calificat ca alertă sau detecție, fie au fost deja curățate/șterse.", + "hasMissingObjects": "Ajustează-ți configurația dacă vrei ca Frigate să salveze obiectele urmărite pentru următoarele etichete: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "O nouă descriere a fost solicitată de la {{provider}}. În funcție de viteza furnizorului tău, regenerarea noii descrieri poate dura ceva timp.", + "updatedSublabel": "Subeticheta a fost actualizată cu succes.", + "updatedLPR": "Plăcuța de înmatriculare a fost actualizată cu succes.", + "audioTranscription": "Transcrierea audio a fost solicitată cu succes. În funcție de viteza serverului dumneavoastră Frigate, transcrierea poate dura ceva timp până la finalizare.", + "updatedAttributes": "Atributele au fost actualizate cu succes." + }, + "error": { + "updatedSublabelFailed": "Nu s-a putut actualiza sub-etichetarea: {{errorMessage}}", + "updatedLPRFailed": "Plăcuța de înmatriculare nu a putut fi actualizată: {{errorMessage}}", + "regenerate": "Eroare la apelarea {{provider}} pentru o nouă descriere: {{errorMessage}}", + "audioTranscription": "Solicitarea transcrierii audio a eșuat: {{errorMessage}}", + "updatedAttributesFailed": "Actualizarea atributelor a eșuat: {{errorMessage}}" + } + } + }, + "editSubLabel": { + "title": "Editează subeticheta", + "desc": "Introdu o sub-etichetă nouă pentru acest {{label}}", + "descNoLabel": "Introduceți o nouă subetichetă pentru acest obiect urmărit" + }, + "editLPR": { + "desc": "Introdu o nouă valoare pentru numărul de înmatriculare pentru acest {{label}}", + "descNoLabel": "Introduceți o nouă valoare a plăcuței de înmatriculare pentru acest obiect urmărit", + "title": "Editează plăcuța de înmatriculare" + }, + "topScore": { + "label": "Cel mai bun scor", + "info": "Scorul cel mai bun este scorul median cel mai ridicat pentru obiectul urmărit, prin urmare, acesta poate diferi de scorul afișat pe miniatura rezultatului căutării." + }, + "estimatedSpeed": "Viteză estimată", + "objects": "Obiecte", + "recognizedLicensePlate": "Plăcuță de înmatriculare recunoscută", + "snapshotScore": { + "label": "Scor snapshot" + }, + "camera": "Cameră", + "zones": "Zone", + "button": { + "findSimilar": "Găsește similare", + "regenerate": { + "title": "Regenerează", + "label": "Regenerează descrierea obiectului urmărit" + } + }, + "tips": { + "saveDescriptionFailed": "Actualizarea descrierii a eșuat: {{errorMessage}}", + "descriptionSaved": "Descrierea a fost salvată cu succes" + }, + "label": "Etichetă", + "description": { + "label": "Descriere", + "placeholder": "Descrierea obiectului urmărit", + "aiTips": "Frigate nu va solicita o descriere de la furnizorul tău de AI până când ciclul de viață al obiectului urmărit nu se încheie." + }, + "expandRegenerationMenu": "Extinde meniul de regenerare", + "regenerateFromSnapshot": "Regenerează din snapshot", + "regenerateFromThumbnails": "Regenerează din miniaturi", + "score": { + "label": "Scor" + }, + "editAttributes": { + "title": "Editează atribute", + "desc": "Selectează atributele de clasificare pentru acest {{label}}" + }, + "attributes": "Atribute de clasificare", + "title": { + "label": "Titlu" + }, + "scoreInfo": "Informații scor" + }, + "exploreMore": "Explorează mai multe obiecte cu {{label}}", + "trackedObjectDetails": "Detalii despre obiectul urmărit", + "trackedObjectsCount_one": "{{count}} obiect urmărit ", + "trackedObjectsCount_few": "{{count}} obiecte urmărite ", + "trackedObjectsCount_other": "{{count}} de obiecte urmărite ", + "itemMenu": { + "downloadSnapshot": { + "aria": "Descarcă snapshot-ul", + "label": "Descarcă snapshot-ul" + }, + "viewObjectLifecycle": { + "label": "Afișează ciclul de viață al obiectului", + "aria": "Arată ciclul de viață al obiectului" + }, + "findSimilar": { + "label": "Găsește similare", + "aria": "Găsește obiecte urmărite similare" + }, + "viewInHistory": { + "label": "Vizualizează în Istoric", + "aria": "Vizualizează în Istoric" + }, + "deleteTrackedObject": { + "label": "Șterge acest obiect urmărit" + }, + "downloadVideo": { + "label": "Descarcă video-ul", + "aria": "Descarcă video-ul" + }, + "submitToPlus": { + "label": "Trimite către Frigate+", + "aria": "Trimite către Frigate Plus" + }, + "addTrigger": { + "label": "Adaugă declanșator", + "aria": "Adaugă un declanșator pentru acest obiect urmărit" + }, + "audioTranscription": { + "label": "Transcrie", + "aria": "Solicită transcrierea audio" + }, + "viewTrackingDetails": { + "label": "Vizualizați detaliile de urmărire", + "aria": "Vizualizați detaliile de urmărire" + }, + "showObjectDetails": { + "label": "Afișează traseul obiectului" + }, + "hideObjectDetails": { + "label": "Ascunde traseul obiectului" + }, + "downloadCleanSnapshot": { + "label": "Descarcă un snapshot curat", + "aria": "Descarcă snapshot curat" + }, + "debugReplay": { + "label": "Reluare depanare", + "aria": "Vezi acest obiect urmărit în vizualizarea de reluare de depanare" + }, + "more": { + "aria": "Mai mult" + } + }, + "dialog": { + "confirmDelete": { + "title": "Confirmă ștergerea", + "desc": "Ștergerea acestui obiect urmărit elimină snapshot-ul, orice înglobări salvate și orice intrări asociate detaliilor de urmărire. Materialul video înregistrat al acestui obiect urmărit în vizualizarea Istoric NU va fi șters.

    Ești sigur că vrei să continui?" + }, + "toast": { + "error": "Eroare la ștergerea acestui obiect urmărit: {{errorMessage}}" + } + }, + "noTrackedObjects": "Nu au fost găsite obiecte urmărite", + "fetchingTrackedObjectsFailed": "Eroare la preluarea obiectelor urmărite: {{errorMessage}}", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Obiectul urmărit a fost șters cu succes.", + "error": "Ștergerea obiectului urmărit a eșuat: {{errorMessage}}" + } + }, + "tooltip": "Potrivire {{type}} cu {{confidence}}%", + "previousTrackedObject": "Obiectul urmărit anterior", + "nextTrackedObject": "Următorul obiect urmărit" + }, + "aiAnalysis": { + "title": "Analiză AI" + }, + "concerns": { + "label": "Îngrijorări" + }, + "trackingDetails": { + "title": "Detalii de Urmărire", + "noImageFound": "Nu s-a găsit nicio imagine pentru acest marcaj de timp.", + "createObjectMask": "Creează Masca Obiectului", + "adjustAnnotationSettings": "Ajustează Setările de anotare", + "scrollViewTips": "Apasă pentru a vizualiza momentele semnificative din ciclul de viață al acestui obiect.", + "autoTrackingTips": "Pozițiile casetelor de delimitare vor fi inexacte pentru camerele cu urmărire automată.", + "count": "{{first}} din {{second}}", + "trackedPoint": "Punct Urmărit", + "lifecycleItemDesc": { + "visible": "detectat {{label}}", + "entered_zone": "{{label}} a intrat în {{zones}}", + "active": "{{label}} a devenit activ", + "stationary": "{{label}} a devenit staționar", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectat pentru {{label}}", + "other": "{{label}} recunoscut ca {{attribute}}" + }, + "gone": "{{label}} a plecat", + "heard": "{{label}} auzit", + "external": "{{label}} detectat", + "header": { + "zones": "Zone", + "ratio": "Raport", + "area": "Aria", + "score": "Scor", + "computedScore": "Scor calculat", + "topScore": "Cel mai mare scor", + "toggleAdvancedScores": "Comută scorurile avansate" + } + }, + "annotationSettings": { + "title": "Setări de adnotare", + "showAllZones": { + "title": "Afișează toate", + "desc": "Afișează întotdeauna zonele pe cadrele în care obiectele au intrat într-o zonă." + }, + "offset": { + "label": "Compensare adnotare", + "desc": "Aceste date provin din stream-ul de detectare al camerei tale, dar sunt suprapuse pe imaginile din stream-ul de înregistrare. Este puțin probabil ca cele două stream-uri să fie perfect sincronizate. Drept urmare, caseta delimitatoare și materialul video nu se vor alinia perfect. Poți folosi această setare pentru a decală adnotările înainte sau înapoi în timp, pentru a le alinia mai bine cu materialul înregistrat.", + "millisecondsToOffset": "Millisecunde pentru a decalca adnotările de detectare. Implicit: 0", + "tips": "Reduceți valoarea dacă redarea video este înaintea casetelor și punctelor de traseu și creșteți valoarea dacă redarea video este în urma acestora. Această valoare poate fi negativă.", + "toast": { + "success": "Decalajul de adnotare pentru {{camera}} a fost salvat în fișierul de configurare." + } + } + }, + "carousel": { + "previous": "Slide-ul anterior", + "next": "Slide-ul următor" + } + } +} diff --git a/web/public/locales/ro/views/exports.json b/web/public/locales/ro/views/exports.json new file mode 100644 index 0000000..be98403 --- /dev/null +++ b/web/public/locales/ro/views/exports.json @@ -0,0 +1,128 @@ +{ + "search": "Căutare", + "documentTitle": "Exporturi - Frigate", + "noExports": "Nu s-au găsit exporturi", + "deleteExport": { + "label": "Șterge export" + }, + "deleteExport.desc": "Sigur vrei să ștergi {{exportName}}?", + "editExport": { + "title": "Redenumire export", + "saveExport": "Salvează exportul", + "desc": "Introdu un nume nou pentru acest export." + }, + "toast": { + "error": { + "renameExportFailed": "Redenumirea exportului a eșuat: {{errorMessage}}", + "assignCaseFailed": "Actualizarea atribuirii cazului a eșuat: {{errorMessage}}", + "caseSaveFailed": "Salvarea cazului a eșuat: {{errorMessage}}", + "caseDeleteFailed": "Ștergerea cazului a eșuat: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Partajează exportul", + "downloadVideo": "Descarcă video", + "editName": "Editează numele", + "deleteExport": "Șterge exportul", + "assignToCase": "Adaugă la un caz", + "removeFromCase": "Elimină din caz" + }, + "headings": { + "cases": "Cazuri", + "uncategorizedExports": "Exporturi necategorizate" + }, + "caseDialog": { + "title": "Adaugă la un caz", + "description": "Alege un caz existent sau creează unul nou.", + "selectLabel": "Caz", + "newCaseOption": "Creează un caz nou", + "nameLabel": "Numele cazului", + "descriptionLabel": "Descriere" + }, + "toolbar": { + "newCase": "Caz nou", + "addExport": "Adaugă export", + "editCase": "Editează cazul", + "deleteCase": "Șterge cazul" + }, + "deleteCase": { + "label": "Șterge cazul", + "desc": "Sigur vrei să ștergi {{caseName}}?", + "descKeepExports": "Exporturile vor rămâne disponibile ca exporturi necategorizate.", + "descDeleteExports": "Toate exporturile din acest caz vor fi șterse definitiv.", + "deleteExports": "Șterge și exporturile" + }, + "caseCard": { + "emptyCase": "Niciun export încă" + }, + "jobCard": { + "defaultName": "Export {{camera}}", + "queued": "În așteptare", + "running": "În rulare", + "preparing": "Pregătire", + "copying": "Copiere", + "encoding": "Codare", + "encodingRetry": "Codare (reîncercare)", + "finalizing": "Finalizare" + }, + "caseView": { + "noDescription": "Fără descriere", + "createdAt": "Creat {{value}}", + "exportCount_one": "1 export", + "exportCount_other": "{{count}} exporturi", + "cameraCount_one": "1 cameră", + "cameraCount_other": "{{count}} camere", + "showMore": "Afișează mai mult", + "showLess": "Afișează mai puțin", + "emptyTitle": "Acest caz este gol", + "emptyDescription": "Adaugă exporturile necategorizate existente pentru a menține cazul organizat.", + "emptyDescriptionNoExports": "Nu există încă exporturi necategorizate disponibile pentru a fi adăugate." + }, + "caseEditor": { + "createTitle": "Creează caz", + "editTitle": "Editează cazul", + "namePlaceholder": "Nume caz", + "descriptionPlaceholder": "Adaugă note sau context pentru acest caz" + }, + "addExportDialog": { + "title": "Adaugă export la {{caseName}}", + "searchPlaceholder": "Caută exporturi necategorizate", + "empty": "Niciun export necategorizat nu se potrivește cu această căutare.", + "addButton_one": "Adaugă 1 export", + "addButton_other": "Adaugă {{count}} exporturi", + "adding": "Se adaugă..." + }, + "selected_one": "{{count}} selectat", + "selected_other": "{{count}} selectate", + "bulkActions": { + "addToCase": "Adaugă la caz", + "moveToCase": "Mută la caz", + "removeFromCase": "Elimină din caz", + "delete": "Șterge", + "deleteNow": "Șterge acum" + }, + "bulkDelete": { + "title": "Șterge exporturile", + "desc_one": "Sigur vrei să ștergi {{count}} export?", + "desc_other": "Sigur vrei să ștergi {{count}} exporturi?" + }, + "bulkRemoveFromCase": { + "title": "Elimină din caz", + "desc_one": "Elimini {{count}} export din acest caz?", + "desc_other": "Elimini {{count}} exporturi din acest caz?", + "descKeepExports": "Exporturile vor fi mutate la necategorizate.", + "descDeleteExports": "Exporturile vor fi șterse definitiv.", + "deleteExports": "Șterge exporturile în schimb" + }, + "bulkToast": { + "success": { + "delete": "Exporturile au fost șterse cu succes", + "reassign": "Alocarea cazului a fost actualizată cu succes", + "remove": "Exporturile au fost eliminate din caz cu succes" + }, + "error": { + "deleteFailed": "Ștergerea exporturilor a eșuat: {{errorMessage}}", + "reassignFailed": "Actualizarea alocării cazului a eșuat: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/ro/views/faceLibrary.json b/web/public/locales/ro/views/faceLibrary.json new file mode 100644 index 0000000..a6227cd --- /dev/null +++ b/web/public/locales/ro/views/faceLibrary.json @@ -0,0 +1,112 @@ +{ + "description": { + "addFace": "Adaugă o colecție nouă în Biblioteca de fețe încărcând prima ta imagine.", + "placeholder": "Introduceti un nume pentru aceasta colectie", + "invalidName": "Nume invalid. Numele pot include doar litere, cifre, spații, apostrofuri, underscore-uri și liniuțe.", + "nameCannotContainHash": "Numele nu poate conține #." + }, + "details": { + "person": "Persoană", + "subLabelScore": "Scor subetichetă", + "unknown": "Necunoscut", + "scoreInfo": "Scorul sub-etichetă este scorul ponderat pentru toate fețele recunoscute, așa că acesta poate diferi de scorul afișat în snapshot.", + "face": "Detalii față", + "faceDesc": "Detalii despre obiectul urmărit care a generat această față", + "timestamp": "Marcaj timp" + }, + "uploadFaceImage": { + "desc": "Încarcă o imagine pentru a scana fețele și a include pentru {{pageToggle}}", + "title": "Încarcă imaginea feței" + }, + "createFaceLibrary": { + "desc": "Creează o colecție nouă", + "title": "Creează colecție", + "nextSteps": "Pentru a construi o bază solidă:
  • Folosește fila „Recunoașteri Recente” pentru a selecta și antrena pe imagini pentru fiecare persoană detectată.
  • Concentrează-te pe imagini frontale pentru cele mai bune rezultate; evită imaginile de antrenament care surprind fețe din unghiuri laterale.
  • ", + "new": "Crează o față nouă" + }, + "collections": "Colecții", + "documentTitle": "Bibliotecă fețe - Frigate", + "train": { + "empty": "Nu există încercări recente de recunoaștere facială", + "title": "Recunoașteri Recente", + "aria": "Selectează Recunoașteri Recente", + "titleShort": "Recent", + "emptyNoLibrary": { + "title": "Încarcă o față", + "description": "Trebuie să adaugi cel puțin o față în librărie pentru ca recunoașterea facială să funcționeze." + } + }, + "steps": { + "description": { + "uploadFace": "Încarcă o imagine cu {{name}} care să arate fața dintr-un unghi frontal. Imaginea nu trebuie să fie decupată doar la nivelul feței." + }, + "faceName": "Introdu numele feței", + "uploadFace": "Încarcă imaginea feței", + "nextSteps": "Pașii următori" + }, + "selectFace": "Selectează fața", + "deleteFaceLibrary": { + "title": "Șterge numele", + "desc": "Ești sigur că vrei să ștergi colecția {{name}}? Aceasta va șterge definitiv toate fețele asociate." + }, + "renameFace": { + "title": "Redenumește fața", + "desc": "Introdu un nume nou pentru {{name}}" + }, + "deleteFaceAttempts": { + "title": "Șterge fețele", + "desc_one": "Ești sigur că vrei să ștergi {{count}} față? Această acțiune nu poate fi anulată.", + "desc_few": "Ești sigur că vrei să ștergi {{count}} fețe? Această acțiune nu poate fi anulată.", + "desc_other": "Ești sigur că vrei să ștergi {{count}} de fețe? Această acțiune nu poate fi anulată." + }, + "button": { + "addFace": "Adaugă față", + "deleteFaceAttempts": "Șterge fețele", + "renameFace": "Redenumește fața", + "uploadImage": "Încarcă imagine", + "deleteFace": "Șterge fața", + "reprocessFace": "Reprocesează fața" + }, + "selectItem": "Selectează {{item}}", + "toast": { + "success": { + "deletedName_one": "{{count}} față a fost ștearsă cu succes.", + "deletedName_few": "{{count}} fețe au fost șterse cu succes.", + "deletedName_other": "{{count}} de fețe au fost șterse cu succes.", + "trainedFace": "Față antrenată cu succes.", + "renamedFace": "Fața a fost redenumită cu succes ca {{name}}", + "updatedFaceScore": "Scorul feței a fost actualizat cu succes la {{name}} ({{score}}).", + "deletedFace_one": "{{count}} față a fost ștersă cu succes.", + "deletedFace_few": "{{count}} fețe au fost șterse cu succes.", + "deletedFace_other": "{{count}} de fețe au fost șterse cu succes.", + "uploadedImage": "Imagine încărcată cu succes.", + "addFaceLibrary": "{{name}} a fost adăugat(ă) cu succes la biblioteca de fețe!", + "reclassifiedFace": "Față reclasificată cu succes." + }, + "error": { + "addFaceLibraryFailed": "Setarea numelui feței a eșuat: {{errorMessage}}", + "deleteFaceFailed": "Ștergerea a eșuat: {{errorMessage}}", + "deleteNameFailed": "Ștergerea numelui a eșuat: {{errorMessage}}", + "renameFaceFailed": "Redenumirea feței a eșuat: {{errorMessage}}", + "trainFailed": "Antrenarea a eșuat: {{errorMessage}}", + "uploadingImageFailed": "Încărcarea imaginii a eșuat: {{errorMessage}}", + "updateFaceScoreFailed": "Nu s-a putut actualiza scorul feței: {{errorMessage}}", + "reclassifyFailed": "Nu s-a putut reclasifica fața: {{errorMessage}}" + } + }, + "imageEntry": { + "dropActive": "Trage imaginea aici…", + "dropInstructions": "Trage și plasează sau lipește o imagine aici sau fă clic pentru a selecta", + "maxSize": "Dimensiunea maximă: {{size}}MB", + "validation": { + "selectImage": "Te rog să selectezi un fișier imagine." + } + }, + "trainFaceAs": "Antrenează fața ca:", + "trainFace": "Antrenează fața", + "readTheDocs": "Citește documentația", + "nofaces": "Nu sunt fețe disponibile", + "pixels": "{{area}}px", + "reclassifyFaceAs": "Reclasifică fața ca:", + "reclassifyFace": "Reclasifică fața" +} diff --git a/web/public/locales/ro/views/live.json b/web/public/locales/ro/views/live.json new file mode 100644 index 0000000..971d3f3 --- /dev/null +++ b/web/public/locales/ro/views/live.json @@ -0,0 +1,206 @@ +{ + "documentTitle": { + "default": "Live - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Frigate - Live", + "lowBandwidthMode": "Mod Latime de Banda Limitata", + "twoWayTalk": { + "enable": "Activare dialog", + "disable": "Dezactivează conversația bidirecțională" + }, + "ptz": { + "zoom": { + "out": { + "label": "Îndepărtează camera PTZ" + }, + "in": { + "label": "Apropie camera PTZ" + } + }, + "move": { + "clickMove": { + "label": "Apasă în cadrul imaginii pentru a centra camera", + "enable": "Activează mutarea prin clic", + "disable": "Dezactivează mutarea prin clic", + "enableWithZoom": "Activează clic pentru mutare / trage pentru zoom" + }, + "left": { + "label": "Mișcă camera PTZ spre stânga" + }, + "up": { + "label": "Mișcă camera PTZ în sus" + }, + "down": { + "label": "Mișcă camera PTZ în jos" + }, + "right": { + "label": "Mișcă camera PTZ spre dreapta" + } + }, + "frame": { + "center": { + "label": "Apasă în cadru pentru a centra camera PTZ" + } + }, + "presets": "Presetări cameră PTZ", + "focus": { + "in": { + "label": "Focalizează camera PTZ în interior" + }, + "out": { + "label": "Focalizează camera PTZ în exterior" + } + } + }, + "cameraAudio": { + "enable": "Activează sunetul camerei", + "disable": "Dezactivează sunetul camerei" + }, + "camera": { + "enable": "Activează camera", + "disable": "Dezactivează camera", + "turnOn": "Camera este pornittă", + "turnOff": "Oprește camera" + }, + "muteCameras": { + "enable": "Dezactivează sunetul pentru toate camerele", + "disable": "Activează sunetul pentru toate camerele" + }, + "detect": { + "enable": "Activează detectarea", + "disable": "Dezactivează detectarea" + }, + "recording": { + "enable": "Activează înregistrarea", + "disable": "Dezactivează înregistrarea", + "disabledInConfig": "Înregistrarea trebuie mai întâi activată în Setări pentru această cameră." + }, + "snapshots": { + "disable": "Dezactivează snapshoturile", + "enable": "Activează snapshot-urile" + }, + "audioDetect": { + "enable": "Activează detectarea audio", + "disable": "Dezactivează detectarea audio" + }, + "autotracking": { + "enable": "Activează urmărirea automată", + "disable": "Dezactivează urmărirea automată" + }, + "streamStats": { + "enable": "Afișează statistici streaming", + "disable": "Ascunde statisticile de streaming" + }, + "manualRecording": { + "title": "La-cerere", + "tips": "Descarcă un snapshot instant sau pornește un eveniment manual pe baza setărilor de reținere a înregistrărilor acestei camere.", + "playInBackground": { + "label": "Redă în fundal", + "desc": "Activează această opțiune pentru a continua redarea streaming-ului chiar și atunci când playerul este ascuns." + }, + "showStats": { + "label": "Afișează statistici", + "desc": "Activează această opțiune pentru a afișa statisticile de streaming suprapus peste imaginea camerei." + }, + "debugView": "Vizualizator depanare", + "start": "Pornește înregistrarea la cerere", + "started": "Înregistrare la cerere pornită manual.", + "failedToStart": "Nu s-a putut porni înregistrarea manuală la cerere.", + "recordDisabledTips": "Deoarece înregistrarea este dezactivată sau restricționată în configurația pentru această cameră, doar un snapshot va fi salvat.", + "end": "Oprește înregistrarea la cerere", + "ended": "Înregistrarea manuală la cerere s-a încheiat.", + "failedToEnd": "Nu s-a reușit încheierea înregistrării manuale la cerere." + }, + "streamingSettings": "Setări streaming", + "notifications": "Notificări", + "audio": "Audio", + "suspend": { + "forTime": "Suspendă pentru: " + }, + "stream": { + "title": "Stream", + "audio": { + "tips": { + "title": "Sunetul trebuie să fie redat de camera dvs. și configurat în go2rtc pentru acest stream.", + "documentation": "Citește documentația " + }, + "available": "Sunetul este disponibil pentru acest stream", + "unavailable": "Sunetul nu este disponibil pentru acest stream" + }, + "twoWayTalk": { + "tips": "Dispozitivul dvs. trebuie să suporte această funcție, iar WebRTC trebuie configurat pentru comunicare bidirecțională.", + "tips.documentation": "Citește documentația ", + "available": "Comunicarea bidirecțională este disponibilă pentru acest stream", + "unavailable": "Comunicarea bidirecțională nu este disponibilă pentru acest stream" + }, + "lowBandwidth": { + "tips": "Vizualizarea live este în modul de lățime de bandă redusă din cauza întârzierilor sau a erorilor de streaming.", + "resetStream": "Resetează stream-ul" + }, + "playInBackground": { + "label": "Redare în fundal", + "tips": "Activează această opțiune pentru a continua streaming-ul când player-ul este ascuns." + }, + "debug": { + "picker": "Selectarea stream-ului nu este disponibilă în modul de depanare. Vizualizarea de depanare folosește întotdeauna stream-ul atribuit rolului de detectare." + } + }, + "cameraSettings": { + "title": "Setări pentru {{camera}}", + "cameraEnabled": "Cameră activată", + "objectDetection": "Detectare obiecte", + "recording": "Înregistrare", + "snapshots": "Snapshot-uri", + "audioDetection": "Detectare sunet", + "autotracking": "Urmărire automată", + "transcription": "Transcriere audio", + "camera": "Cameră" + }, + "history": { + "label": "Afișează înregistrările istorice" + }, + "effectiveRetainMode": { + "modes": { + "all": "Toate", + "motion": "Mișcare", + "active_objects": "Obiecte active" + }, + "notAllTips": "Configurația ta de retenție pentru înregistrările {{source}} este setată la mode: {{effectiveRetainMode}}, astfel că această înregistrare la cerere va păstra doar segmentele cu {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Editează aspectul", + "group": { + "label": "Editează grupul de camere" + }, + "exitEdit": "Ieși din modul de editare" + }, + "transcription": { + "enable": "Activează transcrierea audio în timp real", + "disable": "Dezactivează transcrierea audio în timp real" + }, + "snapshot": { + "takeSnapshot": "Descarcă snapshot instant", + "noVideoSource": "Nicio sursă video disponibilă pentru snapshot.", + "captureFailed": "Eșec la capturarea snapshot-ului.", + "downloadStarted": "Descărcarea snapshot-ului a început." + }, + "noCameras": { + "title": "Nicio Cameră Configurată", + "description": "Începe prin a conecta o cameră la Frigate.", + "buttonText": "Adaugă cameră", + "restricted": { + "title": "Nicio Cameră Disponibilă", + "description": "Nu aveți permisiunea de a vizualiza camere în acest grup." + }, + "default": { + "title": "Nicio cameră configurată", + "description": "Începe prin a conecta o cameră la Frigate.", + "buttonText": "Adaugă cameră" + }, + "group": { + "title": "Nicio cameră în grup", + "description": "Acest grup de camere nu are camere alocate sau activate.", + "buttonText": "Gestionează grupuri" + } + } +} diff --git a/web/public/locales/ro/views/motionSearch.json b/web/public/locales/ro/views/motionSearch.json new file mode 100644 index 0000000..1b455df --- /dev/null +++ b/web/public/locales/ro/views/motionSearch.json @@ -0,0 +1,82 @@ +{ + "documentTitle": "Căutare mișcare - Frigate", + "title": "Căutare mișcare", + "description": "Desenează un poligon pentru a defini regiunea de interes și specifică un interval de timp pentru a căuta schimbări de mișcare în acea regiune.", + "selectCamera": "Căutarea de mișcare se încarcă", + "startSearch": "Începe căutarea", + "searchStarted": "Căutarea a început", + "searchCancelled": "Căutare anulată", + "cancelSearch": "Anulează", + "searching": "Căutare în curs.", + "searchComplete": "Căutare finalizată", + "noResultsYet": "Rulează o căutare pentru a găsi schimbări de mișcare în regiunea selectată", + "noChangesFound": "Nu au fost detectate schimbări de pixeli în regiunea selectată", + "changesFound_one": "Am găsit {{count}} schimbare de mișcare", + "changesFound_few": "Am găsit {{count}} schimbări de mișcare", + "changesFound_other": "Am găsit {{count}} de schimbări de mișcare", + "framesProcessed": "{{count}} cadre procesate", + "jumpToTime": "Sari la acest timp", + "results": "Rezultate", + "showSegmentHeatmap": "Hartă termică", + "newSearch": "Căutare nouă", + "clearResults": "Curăță rezultatele", + "clearROI": "Curăță poligonul", + "polygonControls": { + "points_one": "{{count}} punct", + "points_few": "{{count}} puncte", + "points_other": "{{count}} de puncte", + "undo": "Anulează ultimul punct", + "reset": "Resetează poligonul", + "moveMode": "Mută", + "drawMode": "Desenează" + }, + "motionHeatmapLabel": "Harta termică a mișcării", + "dialog": { + "title": "Căutare mișcare", + "cameraLabel": "Cameră", + "previewAlt": "Previzualizarea camerei pentru {{camera}}" + }, + "timeRange": { + "title": "Interval de căutare", + "start": "Timp de început", + "end": "Timp de sfârșit" + }, + "settings": { + "title": "Setări de căutare", + "parallelMode": "Mod paralel", + "parallelModeDesc": "Scanează mai multe intervale de înregistrare în același timp (mai rapid; utilizează mai multe resurse de decodare)", + "threshold": "Prag de sensibilitate", + "thresholdDesc": "Valorile mai mici detectează schimbări mai mici (1-255)", + "minArea": "Arie minimă de schimbare", + "minAreaDesc": "Dimensiunea minimă a unei singure regiuni în mișcare, ca procent din regiunea de interes", + "frameSkip": "Omitere cadre", + "frameSkipDesc": "Procesează fiecare al N-lea cadru. Setează asta la rata de cadre a camerei tale pentru a procesa un cadru pe secundă (ex. 5 pentru o cameră de 5 FPS, 30 pentru o cameră de 30 FPS). Valorile mai mari vor fi mai rapide, dar pot rata evenimente scurte de mișcare.", + "maxResults": "Rezultate maxime", + "maxResultsDesc": "Oprește-te după acest număr de marcaje de timp potrivite" + }, + "errors": { + "noCamera": "Te rog selectează o cameră", + "noROI": "Te rog desenează o regiune de interes", + "noTimeRange": "Te rog selectează un interval de timp", + "invalidTimeRange": "Timpul de sfârșit trebuie să fie după timpul de început", + "searchFailed": "Căutarea a eșuat: {{message}}", + "polygonTooSmall": "Poligonul trebuie să aibă cel puțin 3 puncte", + "unknown": "Eroare necunoscută" + }, + "changePercentage": "{{percentage}}% schimbat", + "metrics": { + "title": "Metrici de căutare", + "segmentsScanned": "Segmente scanate", + "segmentsProcessed": "Procesat", + "segmentsSkippedInactive": "Omis (fără activitate)", + "segmentsSkippedHeatmap": "Omis (fără suprapunere ROI)", + "fallbackFullRange": "Scanare completă de rezervă", + "framesDecoded": "Cadre decodate", + "wallTime": "Timp de căutare", + "segmentErrors": "Erori segment", + "seconds": "{{seconds}}s", + "minutesSeconds": "{{minutes}}m {{seconds}}s", + "scanSummary": "{{segments}} segmente · {{time}}" + }, + "scanning": "Scanare {{time}}" +} diff --git a/web/public/locales/ro/views/recording.json b/web/public/locales/ro/views/recording.json new file mode 100644 index 0000000..1b96b7c --- /dev/null +++ b/web/public/locales/ro/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtru", + "export": "Exporta", + "calendar": "Calendar", + "filters": "Filtre", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Timpul de sfarsit trebuie sa fie dupa cel de start", + "noValidTimeSelected": "Niciun interval de timp valid nu a fost selectat" + } + } +} diff --git a/web/public/locales/ro/views/replay.json b/web/public/locales/ro/views/replay.json new file mode 100644 index 0000000..b3c854f --- /dev/null +++ b/web/public/locales/ro/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "Reluare de depanare", + "description": "Redă înregistrările camerei pentru depanare. Lista de obiecte arată un rezumat decalat în timp al obiectelor detectate, iar tab-ul Mesaje arată un flux de mesaje interne ale Frigate din înregistrarea redată.", + "websocket_messages": "Mesaje", + "dialog": { + "title": "Pornește reluarea de depanare", + "description": "Creează o cameră temporară de reluare care rulează în buclă înregistrări istorice pentru depanarea problemelor de detecție și urmărire a obiectelor. Camera de reluare va avea aceeași configurație de detecție ca și camera sursă. Alege un interval de timp pentru a începe.", + "camera": "Cameră sursă", + "timeRange": "Interval de timp", + "preset": { + "1m": "Ultimul minut", + "5m": "Ultimele 5 minute", + "timeline": "Din cronologie", + "custom": "Personalizat" + }, + "startButton": "Începe reluarea", + "selectFromTimeline": "Selectează", + "starting": "Pornire reluare...", + "startLabel": "Început", + "endLabel": "Sfârșit", + "toast": { + "error": "Pornirea reluării de depanare a eșuat: {{error}}", + "alreadyActive": "O sesiune de reluare este deja activă", + "stopError": "Oprirea reluării de depanare a eșuat: {{error}}", + "goToReplay": "Mergi la reluare" + } + }, + "page": { + "noSession": "Nicio sesiune de reluare de depanare activă", + "noSessionDesc": "Pornește o reluare de depanare din vizualizarea Istoric dând click pe butonul Acțiuni din bara de instrumente și alegând Reluare depanare.", + "goToRecordings": "Mergi la istoric", + "preparingClip": "Pregătire clip…", + "preparingClipDesc": "Frigate îmbină înregistrările pentru intervalul de timp selectat. Acest lucru poate dura un minut pentru intervale mai mari.", + "startingCamera": "Pornire reluare depanare…", + "startError": { + "title": "Pornirea reluării de depanare a eșuat", + "back": "Înapoi la istoric" + }, + "sourceCamera": "Camera sursă", + "replayCamera": "Camera de reluare", + "initializingReplay": "Inițializare reluare depanare...", + "stoppingReplay": "Oprire reluare depanare...", + "stopReplay": "Oprește reluarea", + "confirmStop": { + "title": "Oprești reluarea de depanare?", + "description": "Aceasta va opri sesiunea și va șterge toate datele temporare. Ești sigur?", + "confirm": "Oprește reluarea", + "cancel": "Anulează" + }, + "activity": "Activitate", + "objects": "Listă de obiecte", + "audioDetections": "Detecții audio", + "noActivity": "Nicio activitate detectată", + "activeTracking": "Urmărire activă", + "noActiveTracking": "Nicio urmărire activă", + "configuration": "Configurație", + "configurationDesc": "Ajustează setările de detecție a mișcării și urmărire a obiectelor pentru camera de reluare de depanare. Nicio modificare nu este salvată în fișierul tău de configurare Frigate." + } +} diff --git a/web/public/locales/ro/views/search.json b/web/public/locales/ro/views/search.json new file mode 100644 index 0000000..81304ac --- /dev/null +++ b/web/public/locales/ro/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Căutare", + "savedSearches": "Căutări salvate", + "searchFor": "Caută {{inputValue}}", + "button": { + "clear": "Șterge căutarea", + "save": "Salvează căutarea", + "filterInformation": "Filtrează informațiile", + "delete": "Șterge căutarea salvată", + "filterActive": "Filtre active" + }, + "trackedObjectId": "ID-ul obiectului urmărit", + "filter": { + "label": { + "cameras": "Camere", + "labels": "Etichete", + "zones": "Zone", + "sub_labels": "Sub-etichete", + "search_type": "Tip căutare", + "time_range": "Interval de timp", + "max_score": "Scor maxim", + "before": "Înainte", + "after": "După", + "min_score": "Scor minim", + "min_speed": "Viteza minimă", + "max_speed": "Viteza maximă", + "recognized_license_plate": "Număr de înmatriculare recunoscut", + "has_clip": "Are videoclip", + "has_snapshot": "Are snapshot", + "attributes": "Atribute" + }, + "tips": { + "desc": { + "step1": "Tastează un nume de filtru urmat de două puncte (ex. „camere:” ).", + "step3": "Folosește mai multe filtre adăugându-le unul după altul, separate prin spațiu.", + "step4": "Filtrele de dată (înainte: și după:) folosesc formatul {{DateFormat}}.", + "step6": "Elimină filtrele apăsând pe „X”-ul de lângă ele.", + "exampleLabel": "Exemplu:", + "step5": "Filtrul pentru intervalul de timp folosește formatul {{exampleTime}}.", + "step2": "Selectează o valoare din sugestii sau tastează propria valoare.", + "text": "Filtrele te ajută să restrângi rezultatele căutării. Iată cum să le folosești în câmpul de introducere:" + }, + "title": "Cum să folosești filtrele de text" + }, + "header": { + "noFilters": "Filtre", + "currentFilterType": "Valori filtru", + "activeFilters": "Filtre active" + }, + "searchType": { + "thumbnail": "Miniatură", + "description": "Descriere" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Data „înainte” trebuie să fie ulterioară datei „după”.", + "afterDatebeEarlierBefore": "Data „după” trebuie să fie mai recentă decât data „înainte”.", + "minScoreMustBeLessOrEqualMaxScore": "Valoarea „min_score” trebuie să fie mai mică sau egală cu „max_score”.", + "maxScoreMustBeGreaterOrEqualMinScore": "Valoarea „max_score” trebuie să fie mai mare sau egală cu „min_score”.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Valoarea „min_speed” trebuie să fie mai mică sau egală cu „max_speed”.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Valoarea „max_speed” trebuie să fie mai mare sau egală cu „min_speed”." + } + } + }, + "similaritySearch": { + "title": "Căutare după similaritate", + "active": "Căutarea după similaritate este activată", + "clear": "Șterge căutarea după similaritate" + }, + "placeholder": { + "search": "Căutare…" + } +} diff --git a/web/public/locales/ro/views/settings.json b/web/public/locales/ro/views/settings.json new file mode 100644 index 0000000..c47c028 --- /dev/null +++ b/web/public/locales/ro/views/settings.json @@ -0,0 +1,2202 @@ +{ + "documentTitle": { + "authentication": "Setări Autentificare - Frigate", + "camera": "Setări cameră - Frigate", + "default": "Setări - Frigate", + "classification": "Setări de clasificare - Frigate", + "masksAndZones": "Editor Mască și Zonă - Frigate", + "notifications": "Setări Notificări - Frigate", + "motionTuner": "Reglaj Mișcare - Frigate", + "object": "Depanare - Frigate", + "general": "Setări interfață - Frigate", + "frigatePlus": "Setări Frigate+ - Frigate", + "enrichments": "Setări Îmbunătățiri - Frigate", + "cameraManagement": "Gestionare Camere - Frigate", + "cameraReview": "Setări Review Cameră - Frigate", + "globalConfig": "Configurație Globală - Frigate", + "cameraConfig": "Configurație Cameră - Frigate", + "maintenance": "Mentenanță - Frigate", + "profiles": "Profile - Frigate", + "detectorsAndModel": "Detectoare și model - Frigate" + }, + "menu": { + "ui": "Interfață (UI)", + "cameras": "Configurație cameră", + "masksAndZones": "Măști / Zone", + "motionTuner": "Reglaj mișcare", + "enrichments": "Îmbunătățiri", + "debug": "Depanare", + "users": "Utilizatori", + "notifications": "Notificări", + "frigateplus": "Frigate+", + "triggers": "Declanșatori", + "roles": "Roluri", + "cameraManagement": "Gestionare cameră", + "cameraReview": "Revizuire", + "general": "General", + "globalConfig": "Configurație globală", + "system": "Sistem", + "integrations": "Integrări", + "profileSettings": "Setări profil", + "globalDetect": "Detecție obiecte", + "globalRecording": "Înregistrare", + "globalSnapshots": "Snapshot-uri", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Detecție mișcare", + "globalObjects": "Obiecte", + "globalReview": "Revizuire", + "globalAudioEvents": "Detecție audio", + "globalLivePlayback": "Redare live", + "globalTimestampStyle": "Stil timestamp", + "systemDatabase": "Bază de date", + "systemTls": "TLS", + "systemAuthentication": "Autentificare", + "systemNetworking": "Rețea", + "systemProxy": "Proxy", + "systemUi": "Interfață (UI)", + "systemLogging": "Jurnale (Logging)", + "systemEnvironmentVariables": "Variabile de mediu", + "systemTelemetry": "Telemetrie", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "Hardware detector", + "systemDetectionModel": "Model detecție", + "systemMqtt": "MQTT", + "integrationSemanticSearch": "Căutare semantică", + "integrationGenerativeAi": "AI Generativ", + "integrationFaceRecognition": "Recunoaștere facială", + "integrationLpr": "Recunoaștere numere înmatriculare", + "integrationObjectClassification": "Clasificare obiecte", + "integrationAudioTranscription": "Transcriere audio", + "cameraDetect": "Detecție obiecte", + "cameraFfmpeg": "Stream-uri (FFmpeg)", + "cameraRecording": "Înregistrare", + "cameraSnapshots": "Snapshot-uri", + "cameraMotion": "Detecție mișcare", + "cameraObjects": "Obiecte", + "cameraConfigReview": "Revizuire", + "cameraAudioEvents": "Detecție audio", + "cameraAudioTranscription": "Transcriere audio", + "cameraNotifications": "Notificări", + "cameraLivePlayback": "Redare live", + "cameraBirdseye": "Birdseye", + "cameraFaceRecognition": "Recunoaștere facială", + "cameraLpr": "Recunoaștere numere înmatriculare", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "Interfață cameră", + "cameraTimestampStyle": "Stil timestamp", + "cameraMqtt": "MQTT Cameră", + "maintenance": "Mentenanță", + "mediaSync": "Sincronizare media", + "regionGrid": "Grilă regiune", + "uiSettings": "Setări UI", + "profiles": "Profile", + "systemGo2rtcStreams": "stream-uri go2rtc", + "systemDetectorsAndModel": "Detectori și model" + }, + "dialog": { + "unsavedChanges": { + "title": "Ai modificări nesalvate.", + "desc": "Vrei să salvezi modificările înainte de a continua?" + } + }, + "cameraSetting": { + "camera": "Cameră", + "noCamera": "Nicio cameră" + }, + "general": { + "title": "Setări UI", + "liveDashboard": { + "title": "Dashboard Live", + "automaticLiveView": { + "desc": "Comută automat la vizualizarea live a unei camere când este detectată activitate. Dezactivarea acestei opțiuni face ca imaginile statice din dashboard-ul Live să se actualizeze doar o dată pe minut.", + "label": "Vizualizare Live Automată" + }, + "playAlertVideos": { + "label": "Redă Clipuri de Alertă", + "desc": "Implicit, alertele recente din dashboard-ul Live rulează ca mici clipuri video în buclă. Dezactivează această opțiune pentru a afișa doar o imagine statică a alertelor recente pe acest dispozitiv/browser." + }, + "displayCameraNames": { + "label": "Arată mereu numele camerelor", + "desc": "Arată întotdeauna numele camerelor într-un tag (chip) în dashboard-ul live multi-cameră." + }, + "liveFallbackTimeout": { + "label": "Timeout Rezecție Player Live", + "desc": "Când stream-ul live de înaltă calitate nu este disponibil, comută pe modul de bandă redusă după acest număr de secunde. Implicit: 3." + } + }, + "storedLayouts": { + "title": "Layout-uri Salvate", + "desc": "Aranjamentul camerelor într-un grup poate fi tras sau redimensionat. Pozițiile sunt stocate în memoria locală a browserului tău.", + "clearAll": "Șterge toate layout-urile" + }, + "cameraGroupStreaming": { + "title": "Setări Streaming Grup Camere", + "desc": "Setările de streaming pentru fiecare grup de camere sunt stocate în memoria locală a browserului tău.", + "clearAll": "Șterge toate setările de streaming" + }, + "recordingsViewer": { + "title": "Vizualizator Înregistrări", + "defaultPlaybackRate": { + "desc": "Viteza de redare implicită pentru vizionarea înregistrărilor.", + "label": "Viteză de redare implicită" + } + }, + "calendar": { + "title": "Calendar", + "firstWeekday": { + "label": "Prima zi a săptămânii", + "desc": "Ziua cu care încep săptămânile în calendarul de revizuire.", + "sunday": "Duminică", + "monday": "Luni" + } + }, + "toast": { + "success": { + "clearStoredLayout": "S-a șters layout-ul salvat pentru {{cameraName}}", + "clearStreamingSettings": "S-au șters setările de streaming pentru toate grupurile de camere." + }, + "error": { + "clearStoredLayoutFailed": "Eșec la ștergerea layout-ului salvat: {{errorMessage}}", + "clearStreamingSettingsFailed": "Eșec la ștergerea setărilor de streaming: {{errorMessage}}" + } + } + }, + "enrichments": { + "faceRecognition": { + "modelSize": { + "large": { + "desc": "Opțiunea mare folosește un model de înglobări faciale ArcFace și va rula automat pe GPU dacă este disponibil.", + "title": "mare (large)" + }, + "desc": "Mărimea modelului folosit pentru recunoașterea facială.", + "small": { + "title": "mic (small)", + "desc": "Opțiunea mic folosește un model de înglobări faciale FaceNet care rulează eficient pe majoritatea procesoarelor." + }, + "label": "Mărime Model" + }, + "title": "Recunoaștere Facială", + "desc": "Recunoașterea facială permite alocarea de nume persoanelor; când o față este recunoscută, Frigate va asocia numele ca sub-etichetă. Informația apare în UI, filtre și notificări.", + "readTheDocumentation": "Citește documentația" + }, + "semanticSearch": { + "reindexNow": { + "confirmDesc": "Sigur vrei să reindexezi toate înglobările obiectelor urmărite? Procesul va rula în fundal, dar poate solicita procesorul la maximum. Poți urmări progresul pe pagina Explore.", + "label": "Reindexează acum", + "desc": "Reindexarea va regenera înglobările pentru toate obiectele urmărite. Acest proces rulează în fundal, poate solicita procesorul la maximum și poate dura destul de mult în funcție de numărul de obiecte.", + "confirmTitle": "Confirmă reindexarea", + "confirmButton": "Reindexează", + "success": "Reindexarea a pornit cu succes.", + "alreadyInProgress": "Reindexarea este deja în curs.", + "error": "Eșec la pornirea reindexării: {{errorMessage}}" + }, + "title": "Căutare Semantică", + "desc": "Căutarea semantică în Frigate îți permite să găsești obiecte urmărite folosind fie imaginea în sine, o descriere text definită de utilizator, sau una generată automat.", + "readTheDocumentation": "Citește documentația", + "modelSize": { + "label": "Mărime Model", + "desc": "Mărimea modelului folosit pentru înglobarea căutării semantice.", + "small": { + "title": "mic (small)", + "desc": "Opțiunea mic folosește o versiune cuantizată a modelului care ocupă mai puțin RAM și rulează mai rapid pe procesor, cu o diferență neglijabilă de calitate." + }, + "large": { + "title": "mare (large)", + "desc": "Opțiunea mare folosește modelul Jina complet și va rula automat pe placa video (GPU) dacă este disponibilă." + } + } + }, + "licensePlateRecognition": { + "desc": "Frigate poate recunoaște plăcuțele de înmatriculare și poate adăuga caracterele detectate în câmpul recognized_license_plate sau un nume cunoscut ca sub-etichetă pentru obiectele de tip mașină. Util pentru mașini care intră pe alee sau trec pe stradă.", + "title": "Recunoaștere Numere Înmatriculare", + "readTheDocumentation": "Citește documentația" + }, + "title": "Setări Îmbunătățiri", + "unsavedChanges": "Modificări nesalvate la setările de îmbunătățiri", + "birdClassification": { + "title": "Clasificare Păsări", + "desc": "Clasificarea păsărilor identifică speciile cunoscute folosind un model Tensorflow cuantizat. Când o pasăre este recunoscută, numele său comun va fi adăugat ca sub-etichetă (sub_label). Această informație este inclusă în interfață, filtre și notificări." + }, + "restart_required": "Repornire necesară (setările de îmbunătățiri s-au modificat)", + "toast": { + "success": "Setările de îmbunătățiri au fost salvate. Repornește Frigate pentru a aplica modificările.", + "error": "Eșec la salvarea modificărilor de configurare: {{errorMessage}}" + } + }, + "camera": { + "title": "Setări cameră", + "streams": { + "title": "Stream-uri", + "desc": "Dezactivează temporar o cameră până la repornirea Frigate. Dezactivarea completă a unei camere oprește procesarea streamului acesteia de către Frigate. Detecția, înregistrarea și depanarea nu vor fi disponibile.
    Notă: Aceasta nu dezactivează restream-urile go2rtc." + }, + "review": { + "title": "Revizuire", + "desc": "Activează/dezactivează temporar alertele și detecțiile pentru această cameră până la repornirea Frigate. Când este dezactivată, nu vor fi generate noi elemente pentru revizuire. ", + "alerts": "Alerte. ", + "detections": "Detecții. " + }, + "reviewClassification": { + "title": "Clasificare revizuiri", + "desc": "Frigate clasifică elementele de revizuire în \"alerte\" și \"detecții\". În mod implicit, toate obiectele de tip persoană și mașină sunt considerate alerte. Poți rafina clasificarea elementelor de revizuire configurând zonele necesare pentru acestea.", + "readTheDocumentation": "Citește documentația", + "unsavedChanges": "Setări de clasificare a revizuirilor nesalvate pentru {{camera}}", + "limitDetections": "Limitează detecțiile la zone specifice", + "zoneObjectDetectionsTips": { + "notSelectDetections": "Toate obiectele {{detectionsLabels}} detectate în {{zone}} pe {{cameraName}} care nu sunt categorisite ca alerte vor fi afișate ca detecții, indiferent de zona în care se află.", + "regardlessOfZoneObjectDetectionsTips": "Toate obiectele {{detectionsLabels}} necategorisite pe {{cameraName}} vor fi afișate ca detecții, indiferent de zona în care se află.", + "text": "Toate obiectele {{detectionsLabels}} care nu sunt categorisite în {{zone}} pe {{cameraName}} vor fi afișate ca detecții." + }, + "selectDetectionsZones": "Selectează zone pentru detecții", + "zoneObjectAlertsTips": "Toate obiectele {{alertsLabels}} detectate în {{zone}} pe {{cameraName}} vor fi afișate ca alerte.", + "objectDetectionsTips": "Toate obiectele {{detectionsLabels}} necategorisite pe {{cameraName}} vor fi afișate ca detecții, indiferent de zona în care se află.", + "toast": { + "success": "Configurația clasificării pentru revizuire a fost salvată. Repornește Frigate pentru a aplica modificările." + }, + "selectAlertsZones": "Selectează zone pentru alerte", + "noDefinedZones": "Nu sunt definite zone pentru această cameră.", + "objectAlertsTips": "Toate obiectele {{alertsLabels}} de pe {{cameraName}} vor fi afișate ca alerte." + }, + "object_descriptions": { + "title": "Descrieri de obiecte generate de AI", + "desc": "Activează/dezactivează temporar descrierile de obiecte generate de AI pentru această cameră. Când această funcție este dezactivată, descrierile generate de AI nu vor fi solicitate pentru obiectele urmărite pe această cameră." + }, + "review_descriptions": { + "title": "Descrieri de revizuiri generate de AI", + "desc": "Activează/dezactivează temporar descrierile recenziilor generate de AI pentru această cameră. Când această funcție este dezactivată, descrierile generate de AI nu vor fi solicitate pentru elementele de recenzie de pe această cameră." + }, + "addCamera": "Adaugă cameră nouă", + "editCamera": "Editează camera:", + "selectCamera": "Selectează camera", + "backToSettings": "Înapoi la setările camerei", + "cameraConfig": { + "add": "Adaugă cameră", + "edit": "Editează camera", + "description": "Configurează setările camerei, inclusiv intrările de flux și rolurile.", + "name": "Numele camerei", + "nameRequired": "Numele camerei este obligatoriu", + "nameInvalid": "Numele camerei trebuie să conțină doar litere, cifre, underscore-uri sau cratime", + "namePlaceholder": "de ex.: usa_principala", + "enabled": "Activat", + "ffmpeg": { + "inputs": "Stream-uri de intrare", + "path": "Cale stream", + "pathRequired": "Calea stream-ului este obligatorie", + "pathPlaceholder": "rtsp://...", + "roles": "Roluri", + "rolesRequired": "Este necesar cel puțin un rol", + "rolesUnique": "Fiecare rol (audio, detectare, înregistrare) poate fi atribuit doar unui singur stream", + "addInput": "Adaugă stream de intrare", + "removeInput": "Elimină stream-ul de intrare", + "inputsRequired": "Este necesar cel puțin un stream de intrare" + }, + "toast": { + "success": "Camera {{cameraName}} a fost salvată cu succes" + }, + "nameLength": "Numele camerei trebuie să aibă mai puțin de 24 de caractere." + } + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} punct", + "point_few": "{{count}} puncte", + "point_other": "{{count}} de puncte", + "loiteringTime": { + "title": "Timp de staționare", + "desc": "Setează timpul minim în secunde pe care un obiect trebuie să îl petreacă în zonă pentru ca aceasta să se activeze. Implicit: 0" + }, + "speedEstimation": { + "desc": "Activează estimarea vitezei pentru obiectele din această zonă. Zona trebuie să aibă exact 4 puncte.", + "title": "Estimare viteză", + "docs": "Citește documentația", + "lineADistance": "Distanța Liniei A ({{unit}})", + "lineBDistance": "Distanța Liniei B ({{unit}})", + "lineCDistance": "Distanța Liniei C ({{unit}})", + "lineDDistance": "Distanța Liniei D ({{unit}})" + }, + "add": "Adaugă Zonă", + "desc": { + "title": "Zonele îți permit să definești arii specifice în cadru pentru a determina dacă un obiect se află sau nu într-un anumit loc.", + "documentation": "Documentație" + }, + "edit": "Editează Zona", + "name": { + "inputPlaceHolder": "Introdu un nume…", + "title": "Nume", + "tips": "Numele trebuie să aibă cel puțin 2 caractere, să conțină cel puțin o literă și să nu coincidă cu numele unei camere sau al altei zone de pe această cameră." + }, + "inertia": { + "title": "Inerție", + "desc": "Specifică în câte cadre trebuie să apară un obiect într-o zonă înainte de a fi considerat ca fiind în acea zonă. Implicit: 3" + }, + "speedThreshold": { + "toast": { + "error": { + "pointLengthError": "Estimarea vitezei a fost dezactivată pentru această zonă. Zonele cu estimare de viteză trebuie să aibă exact 4 puncte.", + "loiteringTimeError": "Zonele cu timpi de staționare mai mari de 0 nu ar trebui folosite împreună cu estimarea vitezei." + } + }, + "title": "Prag viteză ({{unit}})", + "desc": "Specifică viteza minimă pentru ca obiectele să fie luate în considerare în această zonă." + }, + "documentTitle": "Editare Zonă - Frigate", + "clickDrawPolygon": "Click pentru a desena un poligon pe imagine.", + "toast": { + "success": "Zona ({{zoneName}}) a fost salvată." + }, + "label": "Zone", + "objects": { + "title": "Obiecte", + "desc": "Lista de obiecte care se aplică acestei zone." + }, + "allObjects": "Toate obiectele", + "enabled": { + "title": "Activată", + "description": "Specifică dacă această zonă este activă și activată în fișierul de configurare. Dacă este dezactivată, nu poate fi activată prin MQTT. Zonele dezactivate sunt ignorate la rulare." + } + }, + "motionMasks": { + "point_one": "{{count}} punct", + "point_few": "{{count}} puncte", + "point_other": "{{count}} de puncte", + "clickDrawPolygon": "Click pentru a desena un poligon pe imagine.", + "label": "Mască de mișcare", + "documentTitle": "Editare mască de mișcare - Frigate", + "desc": { + "documentation": "Documentație", + "title": "Măștile de mișcare sunt folosite pentru a preveni declanșarea detecției de către tipuri de mișcare nedorite. Mascarea excesivă va îngreuna urmărirea obiectelor." + }, + "add": "Mască de mișcare nouă", + "edit": "Editează masca de mișcare", + "context": { + "documentation": "Citește documentația", + "title": "Măștile de mișcare sunt folosite pentru a preveni declanșarea detecției de către mișcări nedorite (exemplu: ramuri de copaci, marcaje de timp ale camerei). Măștile de mișcare ar trebui folosite cu moderație; mascarea excesivă va îngreuna urmărirea obiectelor." + }, + "toast": { + "success": { + "title": "{{polygonName}} a fost salvată.", + "noName": "Masca de mișcare a fost salvată." + } + }, + "polygonAreaTooLarge": { + "tips": "Măștile de mișcare nu previn detectarea obiectelor. Ar trebui să folosești o zonă obligatorie în schimb.", + "title": "Masca de mișcare acoperă {{polygonArea}}% din cadrul camerei. Măștile de mișcare mari nu sunt recomandate.", + "documentation": "Citește documentația" + }, + "defaultName": "Mască de mișcare {{number}}", + "name": { + "title": "Nume", + "description": "Un nume opțional pentru această mască de mișcare.", + "placeholder": "Introdu un nume..." + } + }, + "objectMasks": { + "point_one": "{{count}} punct", + "point_few": "{{count}} puncte", + "point_other": "{{count}} de puncte", + "documentTitle": "Editare mască de obiect - Frigate", + "add": "Adaugă mască de obiect", + "edit": "Editează masca de obiect", + "desc": { + "documentation": "Documentație", + "title": "Măștile de filtrare a obiectelor sunt folosite pentru a elimina alertele false pentru un anumit tip de obiect, în funcție de locație." + }, + "label": "Măști de obiecte", + "objects": { + "desc": "Tipul de obiect care se aplică acestei măști de obiect.", + "allObjectTypes": "Toate tipurile de obiecte", + "title": "Obiecte" + }, + "toast": { + "success": { + "noName": "Masca de obiect a fost salvată.", + "title": "{{polygonName}} a fost salvată." + } + }, + "clickDrawPolygon": "Click pentru a desena un poligon pe imagine.", + "context": "Măștile de filtrare a obiectelor sunt folosite pentru a elimina alertele false pentru un anumit tip de obiect, în funcție de locație.", + "name": { + "title": "Nume", + "description": "Un nume opțional pentru această mască de obiect.", + "placeholder": "Introdu un nume..." + } + }, + "restart_required": "Repornire necesară (măști/zone modificate)", + "toast": { + "success": { + "copyCoordinates": "Coordonatele pentru {{polyName}} au fost copiate." + }, + "error": { + "copyCoordinatesFailed": "Nu am putut copia coordonatele." + } + }, + "filter": { + "all": "Toate Măștile și Zonele" + }, + "motionMaskLabel": "Mască Mișcare {{number}}", + "objectMaskLabel": "Mască Obiect {{number}}", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Numele zonei trebuie să aibă cel puțin 2 caractere.", + "mustNotContainPeriod": "Numele zonei nu trebuie să conțină puncte.", + "hasIllegalCharacter": "Numele zonei conține caractere nepermise.", + "mustNotBeSameWithCamera": "Numele zonei nu poate fi același cu numele camerei.", + "alreadyExists": "O zonă cu acest nume există deja pentru această cameră.", + "mustHaveAtLeastOneLetter": "Numele zonei trebuie să conțină cel puțin o literă." + } + }, + "polygonDrawing": { + "delete": { + "desc": "Sigur vrei să ștergi {{type}} {{name}}?", + "success": "{{name}} a fost șters.", + "title": "Confirmă Ștergerea" + }, + "removeLastPoint": "Elimină ultimul punct", + "reset": { + "label": "Șterge toate punctele" + }, + "snapPoints": { + "false": "Atragere puncte inactivă", + "true": "Atragere puncte activă" + }, + "error": { + "mustBeFinished": "Desenul poligonului trebuie finalizat înainte de salvare." + }, + "type": { + "zone": "zonă", + "motion_mask": "mască mișcare", + "object_mask": "mască obiect" + }, + "revertOverride": { + "desc": "Asta va elimina suprascrierea de profil pentru {{type}} {{name}} și va reveni la configurația de bază.", + "title": "Revino la configurația de bază" + } + }, + "distance": { + "error": { + "mustBeFilled": "Toate câmpurile de distanță trebuie completate pentru estimarea vitezei.", + "text": "Distanța trebuie să fie mai mare sau egală cu 0.1." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Inerția trebuie să fie mai mare de 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Timpul de staționare trebuie să fie mai mare sau egal cu 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Pragul de viteză trebuie să fie mai mare sau egal cu 0.1." + } + }, + "id": { + "error": { + "mustNotBeEmpty": "ID-ul nu trebuie să fie gol.", + "alreadyExists": "O mască cu acest ID există deja pentru această cameră." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Numele nu trebuie să fie gol." + } + } + }, + "disabledInConfig": "Elementul este dezactivat în fișierul de configurare", + "masks": { + "enabled": { + "title": "Activată", + "description": "Specifică dacă această mască este activată în fișierul de configurare. Dacă este dezactivată, nu poate fi activată prin MQTT. Măștile dezactivate sunt ignorate la rulare." + } + }, + "profileBase": "(bază)", + "profileOverride": "(suprascriere)", + "addDisabledProfile": "Adaugă mai întâi în configurația de bază, apoi suprascrie în profil" + }, + "debug": { + "motion": { + "tips": "

    Chenare de mișcare


    Chenare roșii vor fi suprapuse pe zonele din cadru unde este detectată mișcare în prezent

    ", + "title": "Chenare de mișcare", + "desc": "Afișează chenare în zonele unde este detectată mișcare" + }, + "regions": { + "tips": "

    Chenare de regiune


    Chenare verzi aprinse vor fi suprapuse pe zonele de interes din cadru care sunt trimise către detectorul de obiecte.

    ", + "title": "Regiuni", + "desc": "Afișează regiunea de interes trimisă către detectorul de obiecte" + }, + "desc": "Vizualizarea de depanare arată în timp real obiectele urmărite și statisticile lor. Lista de obiecte arată un rezumat al obiectelor detectate.", + "objectShapeFilterDrawing": { + "document": "Citește documentația ", + "area": "Arie", + "title": "Desenare filtru formă obiect", + "desc": "Desenază un dreptunghi pe imagine pentru a vedea detaliile despre arie și raport", + "tips": "Activează această opțiune pentru a desena un dreptunghi pe imaginea camerei și a vedea aria și raportul acestuia. Aceste valori pot fi folosite ulterior pentru a seta parametrii filtrului de formă în configurație.", + "score": "Scor", + "ratio": "Raport" + }, + "noObjects": "Niciun obiect", + "boundingBoxes": { + "title": "Chenare de încadrare", + "desc": "Afișează chenarele de încadrare în jurul obiectelor urmărite", + "colors": { + "label": "Culori chenare obiecte", + "info": "
  • La pornire, culori diferite vor fi atribuite fiecărei etichete de obiect
  • O linie subțire albastru închis indică faptul că obiectul nu este detectat în acest moment
  • O linie subțire gri indică faptul că obiectul este detectat ca fiind staționar
  • O linie groasă indică faptul că obiectul este subiectul urmăririi automate (când este activată)
  • " + } + }, + "title": "Depanare", + "debugging": "Depanare", + "objectList": "Listă obiecte", + "mask": { + "desc": "Afișează poligoanele măștilor de mișcare", + "title": "Măști de mișcare" + }, + "detectorDesc": "Frigate folosește detectoarele tale ({{detectors}}) pentru a detecta obiecte în stream-ul video al camerei.", + "timestamp": { + "title": "Marcaj temporal", + "desc": "Suprapune un marcaj temporal pe imagine" + }, + "zones": { + "title": "Zone", + "desc": "Afișează conturul oricărei zone definite" + }, + "paths": { + "title": "Trasee", + "desc": "Afișează punctele semnificative ale traseului obiectului urmărit", + "tips": "

    Trasee


    Liniile și cercurile vor indica punctele semnificative prin care obiectul urmărit a trecut în timpul existenței sale.

    " + }, + "audio": { + "title": "Audio", + "noAudioDetections": "Nicio detecție audio", + "score": "scor", + "currentRMS": "RMS actual", + "currentdbFS": "dbFS actual" + }, + "openCameraWebUI": "Deschide interfața web a camerei {{camera}}" + }, + "users": { + "dialog": { + "deleteUser": { + "warn": "Sigur vrei să ștergi utilizatorul {{username}}?", + "title": "Șterge utilizator", + "desc": "Această acțiune nu poate fi anulată. Contul va fi șters definitiv împreună cu toate datele asociate." + }, + "changeRole": { + "desc": "Actualizează permisiunile pentru {{username}}", + "roleInfo": { + "intro": "Selectează rolul corespunzător pentru acest utilizator:", + "admin": "Administrator", + "adminDesc": "Acces complet la toate funcțiile.", + "viewer": "Vizualizator", + "viewerDesc": "Limitat la tablouri de bord Live, Revizuire, Explorare și Exporturi.", + "customDesc": "Rol personalizat cu acces la camere specifice." + }, + "select": "Selectează un rol", + "title": "Schimbă rolul utilizatorului" + }, + "form": { + "password": { + "strength": { + "weak": "Slabă", + "title": "Complexitate parolă: ", + "veryStrong": "Foarte puternică", + "medium": "Medie", + "strong": "Puternică" + }, + "placeholder": "Introdu parola", + "confirm": { + "title": "Confirmă parola", + "placeholder": "Confirmă parola" + }, + "title": "Parolă", + "match": "Parolele se potrivesc", + "notMatch": "Parolele nu se potrivesc", + "show": "Arată parola", + "hide": "Ascunde parola", + "requirements": { + "title": "Cerințe parolă:", + "length": "Cel puțin 12 caractere", + "uppercase": "Cel puțin o literă majusculă", + "digit": "Cel puțin o cifră", + "special": "Cel puțin un caracter special (!@#$%^&*(),.?\":{}|<>)" + } + }, + "passwordIsRequired": "Parola este obligatorie", + "user": { + "placeholder": "Introdu numele de utilizator", + "title": "Nume utilizator", + "desc": "Sunt permise doar litere, cifre, puncte și sublinieri." + }, + "newPassword": { + "title": "Parolă nouă", + "placeholder": "Introdu noua parolă", + "confirm": { + "placeholder": "Introdu din nou noua parolă" + } + }, + "usernameIsRequired": "Numele de utilizator este obligatoriu", + "currentPassword": { + "title": "Parola actuală", + "placeholder": "Introdu parola actuală" + } + }, + "createUser": { + "confirmPassword": "Te rugăm să confirmi parola", + "title": "Creează utilizator nou", + "desc": "Adaugă un cont nou și specifică un rol pentru accesul în interfața Frigate.", + "usernameOnlyInclude": "Numele de utilizator poate conține doar litere, cifre, puncte (.) sau sublinieri (_)" + }, + "passwordSetting": { + "cannotBeEmpty": "Parola nu poate fi goală", + "doNotMatch": "Parolele nu se potrivesc", + "updatePassword": "Actualizează parola pentru {{username}}", + "setPassword": "Setează parola", + "desc": "Creează o parolă puternică pentru a securiza acest cont.", + "currentPasswordRequired": "Parola actuală este obligatorie", + "incorrectCurrentPassword": "Parola actuală este incorectă", + "passwordVerificationFailed": "Verificarea parolei a eșuat", + "multiDeviceWarning": "Orice alt dispozitiv pe care ești autentificat va necesita reautentificarea în {{refresh_time}}.", + "multiDeviceAdmin": "Poți de asemenea să forțezi toți utilizatorii să se reautentifice imediat prin rotirea secretului JWT." + } + }, + "addUser": "Adaugă utilizator", + "management": { + "desc": "Gestionează conturile de utilizator ale acestei instanțe Frigate.", + "title": "Administrare utilizatori" + }, + "toast": { + "success": { + "roleUpdated": "Rolul a fost actualizat pentru {{user}}", + "updatePassword": "Parola a fost actualizată cu succes.", + "createUser": "Utilizatorul {{user}} a fost creat cu succes", + "deleteUser": "Utilizatorul {{user}} a fost șters cu succes" + }, + "error": { + "setPasswordFailed": "Eroare la salvarea parolei: {{errorMessage}}", + "createUserFailed": "Eroare la crearea utilizatorului: {{errorMessage}}", + "roleUpdateFailed": "Eroare la actualizarea rolului: {{errorMessage}}", + "deleteUserFailed": "Eroare la ștergerea utilizatorului: {{errorMessage}}" + } + }, + "updatePassword": "Resetează parola", + "title": "Utilizatori", + "table": { + "username": "Nume utilizator", + "actions": "Acțiuni", + "role": "Rol", + "noUsers": "Niciun utilizator găsit.", + "changeRole": "Schimbă rolul utilizatorului", + "deleteUser": "Șterge utilizatorul", + "password": "Resetează parola" + } + }, + "notification": { + "notificationSettings": { + "title": "Setări Notificări", + "desc": "Frigate poate trimite notificări push direct pe dispozitivul tău când rulează în browser sau este instalat ca PWA.", + "documentation": "Citește documentația" + }, + "globalSettings": { + "desc": "Suspendă temporar notificările pentru anumite camere pe toate dispozitivele înregistrate.", + "title": "Setări Globale" + }, + "email": { + "placeholder": "ex: exemplu@email.com", + "desc": "E necesară o adresă de email validă pentru a te anunța dacă apar probleme cu serviciul de push.", + "title": "Email" + }, + "notificationUnavailable": { + "documentation": "Citește documentația", + "desc": "Notificările push web necesită un context securizat (https://…). Aceasta este o limitare a browserului. Accesează Frigate în mod securizat pentru a utiliza notificările.", + "title": "Notificări Indisponibile", + "descPwa": "Pe iOS, notificările web push sunt disponibile doar când Frigate este instalat pe ecranul principal. Deschide meniul Partajare, alege Adaugă pe ecranul principal, apoi deschide Frigate din noua pictogramă pentru a înregistra acest dispozitiv pentru notificări." + }, + "cameras": { + "title": "Camere", + "desc": "Alege camerele pentru care vrei să activezi notificările.", + "noCameras": "Nicio cameră disponibilă" + }, + "deviceSpecific": "Setări Specifice Dispozitivului", + "registerDevice": "Înregistrează acest Dispozitiv", + "unregisterDevice": "Anulează Înregistrarea Dispozitivului", + "sendTestNotification": "Trimite o notificare de test", + "suspendTime": { + "12hours": "Suspendă pentru 12 ore", + "suspend": "Suspendă", + "5minutes": "Suspendă pentru 5 minute", + "10minutes": "Suspendă pentru 10 minute", + "24hours": "Suspendă pentru 24 ore", + "untilRestart": "Suspendă până la repornire", + "1hour": "Suspendă pentru o oră", + "30minutes": "Suspendă pentru 30 minute" + }, + "toast": { + "success": { + "registered": "Înregistrarea pentru notificări a reușit. Este necesară repornirea Frigate înainte de a putea trimite orice notificare (inclusiv cea de test).", + "settingSaved": "Setările pentru notificări au fost salvate." + }, + "error": { + "registerFailed": "Eroare la salvarea înregistrării notificărilor." + } + }, + "suspended": "Notificări suspendate {{time}}", + "active": "Notificări Active", + "unsavedRegistrations": "Înregistrări de notificări nesalvate", + "unsavedChanges": "Modificări de notificări nesalvate", + "title": "Notificări", + "cancelSuspension": "Anulează Suspendarea" + }, + "frigatePlus": { + "apiKey": { + "plusLink": "Citește mai multe despre Frigate+", + "desc": "Cheia API Frigate+ permite integrarea cu serviciul Frigate+.", + "validated": "Cheia API Frigate+ a fost detectată și validată", + "title": "Cheie API Frigate+", + "notValidated": "Cheia API Frigate+ nu este detectată sau validată" + }, + "snapshotConfig": { + "title": "Configurare snapshot-uri", + "table": { + "snapshots": "Snapshot-uri", + "cleanCopySnapshots": "Snapshot-uri clean_copy", + "camera": "Cameră" + }, + "documentation": "Citește documentația", + "cleanCopyWarning": "Unele camere au snapshot-urile dezactivate", + "desc": "Trimiterea către Frigate+ necesită ca snapshot-urile să fie activate în configurația ta." + }, + "modelInfo": { + "title": "Informații Model", + "supportedDetectors": "Detectoare Suportate", + "plusModelType": { + "baseModel": "Model de Bază", + "userModel": "Reglat fin" + }, + "loadingAvailableModels": "Se încarcă modelele disponibile…", + "modelSelect": "Poți selecta modelele tale disponibile pe Frigate+ aici. Reține că pot fi selectate doar modelele compatibile cu configurația actuală a detectorului.", + "baseModel": "Model de Bază", + "loading": "Se încarcă informațiile despre model…", + "error": "Eroare la încărcarea informațiilor despre model", + "availableModels": "Modele Frigate+ disponibile", + "modelType": "Tip Model", + "trainDate": "Data Antrenării", + "cameras": "Camere", + "noModelLoaded": "Niciun model Frigate+ nu este încărcat în prezent.", + "selectModel": "Selectează un model", + "noModelsAvailable": "Niciun model disponibil", + "filter": { + "ariaLabel": "Filtrează modelele după tip", + "baseModels": "Modele de bază", + "fineTunedModels": "Modele optimizate" + } + }, + "toast": { + "error": "Eroare la salvarea modificărilor de config: {{errorMessage}}", + "success": "Setările Frigate+ au fost salvate. Repornește Frigate pentru a aplica modificările." + }, + "restart_required": "Repornire necesară (modelul Frigate+ a fost schimbat)", + "unsavedChanges": "Modificări Frigate+ nesalvate", + "title": "Setări Frigate+", + "description": "Frigate+ este un serviciu pe bază de abonament care oferă funcții suplimentare, inclusiv posibilitatea de a folosi modele personalizate de detecție obiecte, antrenate pe propriile date. Poți gestiona setările modelului Frigate+ aici.", + "cardTitles": { + "api": "API", + "currentModel": "Model Actual", + "otherModels": "Alte Modele", + "configuration": "Configurație" + }, + "changeInDetectorsAndModel": "Schimbă modelul" + }, + "motionDetectionTuner": { + "unsavedChanges": "Modificări nesalvate la reglajul de mișcare ({{camera}})", + "Threshold": { + "title": "Prag", + "desc": "Valoarea pragului dictează cât de mult trebuie să se schimbe luminanța unui pixel pentru a fi considerat mișcare. Implicit: 30" + }, + "contourArea": { + "desc": "Valoarea ariei conturului este folosită pentru a decide care grupuri de pixeli modificați se califică drept mișcare. Implicit: 10", + "title": "Aria conturului" + }, + "improveContrast": { + "title": "Îmbunătățire contrast", + "desc": "Îmbunătățește contrastul pentru scenele întunecate. Implicit: ACTIVAT" + }, + "desc": { + "title": "Frigate folosește detecția de mișcare ca prim pas pentru a verifica dacă se întâmplă ceva în cadru ce merită verificat cu detecția de obiecte.", + "documentation": "Citește Ghidul de reglare a mișcării" + }, + "toast": { + "success": "Setările de mișcare au fost salvate." + }, + "title": "Reglaj detecție mișcare" + }, + "triggers": { + "documentTitle": "Triggere", + "management": { + "title": "Triggere", + "desc": "Gestionează triggerele pentru {{camera}}. Folosește tipul „thumbnail” pentru a declanșa la miniaturi similare cu obiectul urmărit, și tipul „descriere” pentru a declanșa la descrieri similare cu textul specificat." + }, + "addTrigger": "Adaugă Trigger", + "table": { + "name": "Nume", + "type": "Tip", + "content": "Conținut", + "threshold": "Prag", + "actions": "Acțiuni", + "noTriggers": "Nu există triggere configurate pentru această cameră.", + "edit": "Editează", + "deleteTrigger": "Șterge Trigger", + "lastTriggered": "Ultima declanșare" + }, + "type": { + "thumbnail": "Miniatură", + "description": "Descriere" + }, + "actions": { + "alert": "Marchează ca alertă", + "notification": "Trimite Notificare", + "sub_label": "Adaugă Sub-etichetă", + "attribute": "Adaugă Atribut" + }, + "dialog": { + "createTrigger": { + "title": "Creează Trigger", + "desc": "Creează un trigger pentru camera {{camera}}" + }, + "editTrigger": { + "title": "Editează Trigger", + "desc": "Editează setările pentru triggerul de pe camera {{camera}}" + }, + "deleteTrigger": { + "title": "Șterge Trigger", + "desc": "Sigur vrei să ștergi triggerul {{triggerName}}? Această acțiune nu poate fi anulată." + }, + "form": { + "name": { + "title": "Nume", + "placeholder": "Pune un nume acestui trigger", + "error": { + "minLength": "Câmpul trebuie să aibă cel puțin 2 caractere.", + "invalidCharacters": "Câmpul poate conține doar litere, cifre, underscore (_) și cratime (-).", + "alreadyExists": "Un trigger cu acest nume există deja pentru această cameră." + }, + "description": "Introdu un nume unic sau o descriere pentru a identifica acest trigger" + }, + "enabled": { + "description": "Activează sau dezactivează acest trigger" + }, + "type": { + "title": "Tip", + "placeholder": "Selectează tipul triggerului", + "description": "Declanșează când este detectată o descriere similară a obiectului urmărit", + "thumbnail": "Declanșează când este detectată o miniatură similară a obiectului urmărit" + }, + "content": { + "title": "Conținut", + "imagePlaceholder": "Selectează o miniatură", + "textPlaceholder": "Introdu textul", + "imageDesc": "Sunt afișate doar cele mai recente 100 de miniaturi. Dacă nu găsești miniatura dorită, verifică obiectele anterioare în secțiunea Explore și configurează un trigger de acolo.", + "textDesc": "Introdu textul pentru a declanșa acțiunea atunci când este detectată o descriere similară a obiectului urmărit.", + "error": { + "required": "Conținutul este obligatoriu." + } + }, + "threshold": { + "title": "Prag (Threshold)", + "error": { + "min": "Pragul trebuie să fie cel puțin 0", + "max": "Pragul trebuie să fie maxim 1" + }, + "desc": "Setează pragul de similitudine. Un prag mai mare înseamnă că este necesară o potrivire mai exactă pentru a declanșa." + }, + "actions": { + "title": "Acțiuni", + "desc": "În mod implicit, Frigate trimite un mesaj MQTT pentru toate triggerele. Sub-etichetele adaugă numele triggerului la eticheta obiectului. Atributele sunt metadate căutabile stocate separat.", + "error": { + "min": "Trebuie selectată cel puțin o acțiune." + } + }, + "friendly_name": { + "title": "Nume prietenos", + "placeholder": "Denumește sau descrie acest declanșator", + "description": "Un nume prietenos opțional sau un text descriptiv pentru acest declanșator." + } + } + }, + "toast": { + "success": { + "createTrigger": "Triggerul {{name}} a fost creat cu succes.", + "updateTrigger": "Triggerul {{name}} a fost actualizat cu succes.", + "deleteTrigger": "Triggerul {{name}} a fost șters." + }, + "error": { + "createTriggerFailed": "Eroare la crearea triggerului: {{errorMessage}}", + "updateTriggerFailed": "Eroare la actualizarea triggerului: {{errorMessage}}", + "deleteTriggerFailed": "Eroare la ștergerea triggerului: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Căutarea Semantică este dezactivată", + "desc": "Căutarea Semantică trebuie să fie activată pentru a folosi Triggere." + }, + "wizard": { + "title": "Creează Trigger", + "step1": { + "description": "Configurează setările de bază pentru trigger." + }, + "step2": { + "description": "Setează conținutul care va declanșa această acțiune." + }, + "step3": { + "description": "Configurează pragul și acțiunile pentru acest trigger." + }, + "steps": { + "nameAndType": "Nume și Tip", + "configureData": "Configurare Date", + "thresholdAndActions": "Prag și Acțiuni" + } + } + }, + "roles": { + "management": { + "title": "Administrare roluri vizualizator", + "desc": "Gestionează rolurile de vizualizator personalizate și permisiunile de acces la camere pentru această instanță Frigate." + }, + "addRole": "Adaugă rol", + "table": { + "role": "Rol", + "cameras": "Camere", + "actions": "Acțiuni", + "noRoles": "Nu s-au găsit roluri personalizate.", + "editCameras": "Editează camerele", + "deleteRole": "Șterge rolul" + }, + "toast": { + "success": { + "createRole": "Rolul {{role}} a fost creat cu succes", + "updateCameras": "Camerele au fost actualizate pentru rolul {{role}}", + "deleteRole": "Rolul {{role}} a fost șters cu succes", + "userRolesUpdated_one": "{{count}} utilizator atribuit acestui rol a fost actualizat la 'vizualizator', care are acces la toate camerele.", + "userRolesUpdated_few": "{{count}} utilizatori atribuiți acestui rol au fost actualizați la 'vizualizator', care are acces la toate camerele.", + "userRolesUpdated_other": "{{count}} de utilizatori atribuiți acestui rol au fost actualizați la 'vizualizator', care are acces la toate camerele." + }, + "error": { + "createRoleFailed": "Eroare la crearea rolului: {{errorMessage}}", + "updateCamerasFailed": "Eroare la actualizarea camerelor: {{errorMessage}}", + "deleteRoleFailed": "Eroare la ștergerea rolului: {{errorMessage}}", + "userUpdateFailed": "Eroare la actualizarea rolurilor utilizatorilor: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Creează rol nou", + "desc": "Adaugă un rol nou și specifică permisiunile de acces la camere." + }, + "editCameras": { + "title": "Editează camerele rolului", + "desc": "Actualizează accesul la camere pentru rolul {{role}}." + }, + "deleteRole": { + "title": "Șterge Rolul", + "desc": "Această acțiune este ireversibilă. Rolul va fi șters definitiv, iar utilizatorii cu acest rol vor primi rolul de „viewer”, având acces de vizualizare la toate camerele.", + "warn": "Sigur vrei să ștergi {{role}}?", + "deleting": "Se șterge..." + }, + "form": { + "role": { + "title": "Nume Rol", + "placeholder": "Introdu numele rolului", + "desc": "Sunt permise doar litere, cifre, puncte și caractere de subliniere (_).", + "roleIsRequired": "Numele rolului este obligatoriu", + "roleOnlyInclude": "Numele rolului poate conține doar litere, cifre, . sau _", + "roleExists": "Există deja un rol cu acest nume." + }, + "cameras": { + "title": "Camere", + "desc": "Selectează camerele la care are acces acest rol. Este necesară cel puțin o cameră.", + "required": "Trebuie selectată cel puțin o cameră." + } + } + } + }, + "cameraWizard": { + "title": "Adaugă Cameră", + "description": "Urmează pașii de mai jos pentru a adăuga o cameră nouă în instalația Frigate.", + "steps": { + "nameAndConnection": "Nume și Conexiune", + "streamConfiguration": "Configurare Stream", + "validationAndTesting": "Validare și Testare", + "probeOrSnapshot": "Sondare sau snapshot" + }, + "save": { + "success": "Camera {{cameraName}} a fost salvată cu succes.", + "failure": "Eroare la salvarea {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Rezoluție", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Te rog introdu un URL de stream valid", + "testFailed": "Testul stream-ului a eșuat: {{error}}" + }, + "step1": { + "description": "Introdu detaliile camerei și alege să o scanezi automat sau să selectezi manual marca.", + "cameraName": "Nume Cameră", + "cameraNamePlaceholder": "ex: usa_intrare sau Curte Spate", + "host": "Host/Adresă IP", + "port": "Port", + "username": "Utilizator", + "usernamePlaceholder": "Opțional", + "password": "Parolă", + "passwordPlaceholder": "Opțional", + "selectTransport": "Selectează protocolul de transport", + "cameraBrand": "Marca Camerei", + "selectBrand": "Alege marca pentru șablonul de URL", + "customUrl": "URL Stream Personalizat", + "brandInformation": "Informații marcă", + "brandUrlFormat": "Pentru camere cu formatul de URL RTSP: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://utilizator:parola@host:port/cale", + "testConnection": "Testează Conexiunea", + "testSuccess": "Testul de conexiune a reușit!", + "testFailed": "Testul de conexiune a eșuat. Te rog să verifici datele introduse și să încerci din nou.", + "streamDetails": "Detalii stream", + "warnings": { + "noSnapshot": "Nu se poate obține un snapshot de pe stream-ul configurat." + }, + "errors": { + "brandOrCustomUrlRequired": "Selectează o marcă cu host/IP sau alege 'Other' cu un URL personalizat", + "nameRequired": "Numele camerei este obligatoriu", + "nameLength": "Numele camerei trebuie să aibă maxim 64 de caractere", + "invalidCharacters": "Numele camerei conține caractere nepermise", + "nameExists": "Numele camerei există deja", + "brands": { + "reolink-rtsp": "RTSP Reolink nu este recomandat. Activează HTTP în setările firmware ale camerei și repornește asistentul." + }, + "customUrlRtspRequired": "URL-urile personalizate trebuie să înceapă cu „rtsp://” sau „rtsps://”. Configurarea manuală este necesară pentru stream-urile care nu sunt RTSP." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Sondare metadate cameră...", + "fetchingSnapshot": "Preluare snapshot cameră..." + }, + "connectionSettings": "Setări Conexiune", + "detectionMethod": "Metodă Detecție Stream", + "onvifPort": "Port ONVIF", + "probeMode": "Scanează camera", + "manualMode": "Selecție manuală", + "detectionMethodDescription": "Scanează camera prin ONVIF (dacă e suportat) pentru a găsi URL-urile stream-urilor, sau alege manual marca pentru a folosi URL-uri predefinite. Pentru un URL RTSP personalizat, alege metoda manuală și selectează \"Other\".", + "onvifPortDescription": "Pentru camerele cu suport ONVIF, acesta este de obicei 80 sau 8080.", + "useDigestAuth": "Folosește autentificare digest", + "useDigestAuthDescription": "Folosește autentificarea HTTP digest pentru ONVIF. Unele camere pot necesita un utilizator/parolă dedicat pentru ONVIF, diferit de cel de admin." + }, + "step2": { + "description": "Testează camera pentru stream-uri disponibile sau configurează manual setările în funcție de metoda de detecție aleasă.", + "streamsTitle": "Stream-uri cameră", + "addStream": "Adaugă stream", + "addAnotherStream": "Adaugă un alt stream", + "streamTitle": "Stream {{number}}", + "streamUrl": "URL stream", + "streamUrlPlaceholder": "rtsp://utilizator:parolă@gazdă:port/cale", + "url": "URL", + "resolution": "Rezoluție", + "selectResolution": "Selectează rezoluția", + "quality": "Calitate", + "selectQuality": "Selectează calitatea", + "roles": "Roluri", + "roleLabels": { + "detect": "Detecție obiecte", + "record": "Înregistrare", + "audio": "Audio" + }, + "testStream": "Testează conexiunea", + "testSuccess": "Testul de conexiune a reușit!", + "testFailed": "Testul de conexiune a eșuat. Verifică datele introduse și încearcă din nou.", + "testFailedTitle": "Test Eșuat", + "connected": "Conectat", + "notConnected": "Neconectat", + "featuresTitle": "Funcționalități", + "go2rtc": "Redu conexiunile la cameră", + "detectRoleWarning": "Cel puțin un stream trebuie să aibă rolul „detectare” pentru a continua.", + "rolesPopover": { + "title": "Roluri de streaming", + "detect": "Stream principal pentru detecția obiectelor.", + "record": "Salvează segmente ale stream-ului video pe baza setărilor de configurare.", + "audio": "Stream pentru detecția bazată pe sunet." + }, + "featuresPopover": { + "title": "Funcționalități streaming", + "description": "Folosește restreaming go2rtc pentru a reduce conexiunile la cameră." + }, + "streamDetails": "Detalii stream", + "probing": "Se testează camera...", + "retry": "Reîncearcă", + "testing": { + "probingMetadata": "Se extrag metadatele camerei...", + "fetchingSnapshot": "Se preia snapshot-ul..." + }, + "probeFailed": "Eșec la testarea camerei: {{error}}", + "probingDevice": "Se testează dispozitivul...", + "probeSuccessful": "Test reușit", + "probeError": "Eroare la testare", + "probeNoSuccess": "Testul nu a avut succes", + "deviceInfo": "Informații Dispozitiv", + "manufacturer": "Producător", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profiluri", + "ptzSupport": "Suport PTZ", + "autotrackingSupport": "Suport Auto-tracking", + "presets": "Presetări", + "rtspCandidates": "Candidate RTSP", + "rtspCandidatesDescription": "Următoarele URL-uri RTSP au fost găsite în urma testării camerei. Testează conexiunea pentru a vedea metadatele stream-ului.", + "noRtspCandidates": "Nu au fost găsite URL-uri RTSP pentru această cameră. E posibil ca datele de autentificare să fie greșite sau camera să nu suporte ONVIF. Mergi înapoi și introdu URL-ul RTSP manual.", + "candidateStreamTitle": "Candidat {{number}}", + "useCandidate": "Folosește", + "uriCopy": "Copiază", + "uriCopied": "URI copiat în clipboard", + "testConnection": "Testează Conexiunea", + "toggleUriView": "Click pentru a vedea URI-ul complet", + "errors": { + "hostRequired": "Adresa Host/IP este obligatorie" + } + }, + "step3": { + "description": "Configurează rolurile stream-urilor și adaugă stream-uri suplimentare pentru camera ta.", + "validationTitle": "Validare stream", + "connectAllStreams": "Conectează toate stream-urile", + "reconnectionSuccess": "Reconectare reușită.", + "reconnectionPartial": "Unele stram-uri nu s-au reconectat.", + "streamUnavailable": "Previzualizare streaming indisponibilă", + "reload": "Reîncarcă", + "connecting": "Conectare...", + "streamTitle": "Stream-ul {{number}}", + "valid": "Valid", + "failed": "Eșuat", + "notTested": "Netestat", + "connectStream": "Conectare", + "connectingStream": "Se conectează", + "disconnectStream": "Deconectare", + "estimatedBandwidth": "Lățime de bandă estimată", + "roles": "Roluri", + "none": "Niciunul", + "error": "Eroare", + "streamValidated": "Stream {{number}} validat cu succes", + "streamValidationFailed": "Validarea pentru stream {{number}} a eșuat", + "saveAndApply": "Salvează Camera Nouă", + "saveError": "Configurație invalidă. Verifică setările.", + "issues": { + "title": "Validare stream", + "videoCodecGood": "Codecul video este {{codec}}.", + "audioCodecGood": "Codecul audio este {{codec}}.", + "noAudioWarning": "Nu s-a detectat audio pentru acest strem, înregistrările nu vor avea sunet.", + "audioCodecRecordError": "Codec-ul audio AAC este necesar pentru a suporta audio în înregistrări.", + "audioCodecRequired": "Un stream audio este necesar pentru a suporta detecția audio.", + "restreamingWarning": "Reducerea conexiunilor la cameră pentru stream-ul de înregistrare poate crește ușor utilizarea procesorului.", + "dahua": { + "substreamWarning": "Substream-ul 1 este blocat la o rezoluție scăzută. Multe camere Dahua / Amcrest / EmpireTech suportă substream-uri suplimentare care trebuie să fie activate în setările camerei. Este recomandat să verifici și să utilizezi acele stream-uri, dacă sunt disponibile." + }, + "hikvision": { + "substreamWarning": "Substream-ul 1 este blocat la o rezoluție scăzută. Multe camere Hikvision suportă substream-uri suplimentare care trebuie să fie activate în setările camerei. Este recomandat să verifici și să utilizezi acele strem-uri, dacă sunt disponibile." + }, + "resolutionHigh": "O rezoluție de {{resolution}} poate cauza o utilizare crescută a resurselor.", + "resolutionLow": "O rezoluție de {{resolution}} poate fi prea mică pentru detectarea fiabilă a obiectelor mici." + }, + "ffmpegModule": "Folosește modul de compatibilitate pentru stream-uri", + "ffmpegModuleDescription": "Dacă fluxul nu se încarcă după mai multe încercări, activați această opțiune. Când este activată, Frigate va folosi modulul ffmpeg împreună cu go2rtc. Aceasta poate oferi o compatibilitate mai bună cu unele fluxuri de camere.", + "streamsTitle": "Stream-uri cameră", + "addStream": "Adaugă un stream", + "addAnotherStream": "Mai adaugă un stream", + "streamUrl": "URL stream", + "streamUrlPlaceholder": "rtsp://utilizator:parola@host:port/cale", + "selectStream": "Selectează un stream", + "searchCandidates": "Caută candidați...", + "noStreamFound": "Niciun stream găsit", + "url": "URL", + "resolution": "Rezoluție", + "quality": "Calitate", + "selectResolution": "Selectează rezoluția", + "selectQuality": "Selectează calitatea", + "roleLabels": { + "detect": "Detecție Obiecte", + "record": "Înregistrare", + "audio": "Audio" + }, + "testStream": "Testează Conexiunea", + "testSuccess": "Testul stream-ului a reușit!", + "testFailed": "Testul stream-ului a eșuat", + "testFailedTitle": "Test Eșuat", + "connected": "Conectat", + "notConnected": "Neconectat", + "featuresTitle": "Funcții", + "go2rtc": "Redu conexiunile către cameră", + "detectRoleWarning": "Cel puțin un stream trebuie să aibă rolul „detect” pentru a continua.", + "rolesPopover": { + "title": "Roluri de stream", + "detect": "Fluxul principal pentru detecția obiectelor.", + "record": "Salvează segmente video conform setărilor de configurare.", + "audio": "Flux pentru detecția bazată pe sunet." + }, + "featuresPopover": { + "title": "Caracteristici stream", + "description": "Folosește restreaming prin go2rtc pentru a reduce numărul de conexiuni directe către cameră." + } + }, + "step4": { + "description": "Validare finală și analiză înainte de salvare. Conectează fiecare stream înainte de a salva.", + "validationTitle": "Validare stream", + "connectAllStreams": "Conectează Toate stream-urile", + "reconnectionSuccess": "Reconectare reușită.", + "reconnectionPartial": "Unele stream-uri nu s-au putut reconecta.", + "streamUnavailable": "Previzualizarea stream-ului este indisponibilă", + "reload": "Reîncarcă", + "connecting": "Se conectează...", + "streamTitle": "stream-ul {{number}}", + "valid": "Valid", + "failed": "Eșuat", + "notTested": "Netestat", + "connectStream": "Conectează", + "connectingStream": "Se conectează", + "disconnectStream": "Deconectează", + "estimatedBandwidth": "Lățime de bandă estimată", + "roles": "Roluri", + "ffmpegModule": "Folosește modul de compatibilitate de stream-uri", + "ffmpegModuleDescription": "Dacă stream-ul nu se încarcă după mai multe încercări, activează asta. Frigate va folosi modulul ffmpeg cu go2rtc, ceea ce poate ajuta la compatibilitatea cu anumite camere.", + "none": "Niciunul", + "error": "Eroare", + "streamValidated": "Stream-ul {{number}} a fost validat cu succes", + "streamValidationFailed": "Validarea stream-ului {{number}} a eșuat", + "saveAndApply": "Salvează Camera Nouă", + "saveError": "Configurație nevalidă. Verifică setările.", + "issues": { + "title": "Validare stream", + "videoCodecGood": "Codecul video este {{codec}}.", + "audioCodecGood": "Codecul audio este {{codec}}.", + "resolutionHigh": "O rezoluție de {{resolution}} poate crește consumul de resurse.", + "resolutionLow": "O rezoluție de {{resolution}} ar putea fi prea mică pentru detecția sigură a obiectelor mici.", + "noAudioWarning": "Nu s-a detectat audio pentru acest stream; înregistrările nu vor avea sunet.", + "audioCodecRecordError": "Codecul audio AAC este necesar pentru a avea sunet în înregistrări.", + "audioCodecRequired": "Un stream audio este necesar pentru suportul detecției audio.", + "restreamingWarning": "Reducerea conexiunilor către cameră pentru stream-ul de înregistrare poate crește ușor utilizarea procesorului (CPU).", + "brands": { + "reolink-rtsp": "Reolink RTSP nu este recomandat. Activează HTTP în setările camerei și repornește asistentul.", + "reolink-http": "Stream-urile Reolink HTTP ar trebui să folosească FFmpeg pentru o mai bună compatibilitate. Activează „Folosește modul de compatibilitate de stream” pentru acest stream." + }, + "dahua": { + "substreamWarning": "Substream 1 este limitat la o rezoluție mică. Multe camere Dahua / Amcrest / EmpireTech suportă substream-uri adiționale care trebuie activate din setările camerei. Se recomandă verificarea și utilizarea acestora." + }, + "hikvision": { + "substreamWarning": "Substream 1 este limitat la o rezoluție mică. Multe camere Hikvision suportă substream-uri adiționale care trebuie activate din setările camerei. Se recomandă verificarea și utilizarea acestora." + }, + "resolutionUnknown": "Rezoluția acestui stream nu a putut fi sondată. Ar trebui să setezi manual rezoluția de detecție în Setări sau în configurația ta." + } + } + }, + "cameraManagement": { + "title": "Gestionare Camere", + "addCamera": "Adaugă Cameră Nouă", + "editCamera": "Editează Camera:", + "selectCamera": "Selectează o Cameră", + "backToSettings": "Înapoi la Setări Cameră", + "streams": { + "title": "Stare și detalii cameră", + "desc": "Dezactivează temporar o cameră până la repornirea Frigate. Dezactivarea unei camere oprește complet procesarea streamingului acestei camere de către Frigate. Detecția, înregistrarea și depanarea vor fi indisponibile.
    Notă: Aceasta nu dezactivează restreamingul go2rtc.", + "enableLabel": "Camere activate", + "enableDesc": "Dezactivează temporar o cameră activată până la repornirea Frigate. Dezactivarea unei camere oprește complet procesarea de către Frigate a fluxurilor acestei camere. Detectarea, înregistrarea și depanarea vor fi indisponibile.
    Notă: Acest lucru nu dezactivează retransmisiile go2rtc..

    Trage de mâner pentru a reordona camerele așa cum apar în interfață. Ordinea camerelor activate va fi reflectată în întreaga interfață, inclusiv în tabloul de bord Live și în meniurile derulante pentru selectarea camerelor.", + "disableLabel": "Camere dezactivate", + "disableDesc": "Activează o cameră care este ascunsă în interfață și dezactivată în configurație. Este necesară repornirea Frigate după activare.", + "enableSuccess": "S-a activat {{cameraName}}. Repornește Frigate pentru a aplica.", + "friendlyName": { + "edit": "Editează numele afișat al camerei", + "title": "Editează numele afișat", + "description": "Setează numele afișat pentru această cameră în întreaga interfață Frigate. Lasă necompletat pentru a folosi ID-ul camerei.", + "rename": "Redenumește" + }, + "reorderHandle": "Trage pentru a reordona", + "saving": "Se salvează…", + "saved": "Salvat", + "details": { + "edit": "Editează detaliile camerei", + "title": "Editează detaliile camerei", + "description": "Actualizează numele de afișare, URL-ul extern și vizibilitatea folosite pentru această cameră în tot UI-ul Frigate.", + "friendlyNameLabel": "Nume afișat", + "friendlyNameHelp": "Numele prietenos afișat pentru această cameră în întreaga interfață Frigate. Lasă gol pentru a utiliza ID-ul camerei.", + "webuiUrlLabel": "URL-ul interfeței web a camerei", + "webuiUrlHelp": "URL pentru a vizita interfața web a camerei direct din vizualizarea Depanare (Debug). Lasă gol pentru a dezactiva linkul.", + "webuiUrlInvalid": "Trebuie să fie un URL valid (de exemplu, https://exemplu.com).", + "dashboardLabel": "Arată pe dashboard-ul Live", + "reviewLabel": "Arată în Revizuire", + "dashboardHelp": "Arată această cameră pe dashboard-ul Live.", + "reviewHelp": "Arată această cameră în revizuiri, inclusiv filtrul de camere, revizuirea mișcărilor și vizualizarea istoricului." + }, + "label": "Stare cameră", + "description": "Setează starea de funcționare pentru fiecare cameră.

    Pornit: stream-urile sunt procesate normal.
    Oprit: pune temporar pe pauză procesarea. Nu se menține după repornirile Frigate.
    Dezactivat: oprește procesarea și salvează modificarea în configurația ta. Este necesară o repornire pentru a reactiva o cameră dezactivată.

    Notă: Dezactivarea nu afectează restream-urile go2rtc.

    Trage de mâner pentru a reordona camerele active așa cum apar în interfață, inclusiv în panoul Live și în meniurile drop-down de selecție a camerei.", + "disabledSubheading": "Dezactivat în configurație", + "status": { + "on": "Pornit", + "off": "Oprit", + "disabled": "Dezactivat" + }, + "disableSuccess": "S-a dezactivat {{cameraName}} și s-a salvat în configurație." + }, + "cameraConfig": { + "add": "Adaugă Cameră", + "edit": "Editează Camera", + "description": "Configurează setările camerei, inclusiv stream-urile de intrare și rolurile acestora.", + "name": "Nume Cameră", + "nameRequired": "Numele camerei este obligatoriu", + "nameLength": "Numele camerei trebuie să aibă sub 64 de caractere.", + "namePlaceholder": "ex: usa_fata sau Curte Spate", + "enabled": "Activat", + "ffmpeg": { + "inputs": "Stream-uri de intrare", + "path": "Cale stream", + "pathRequired": "Calea stream-ului este obligatorie", + "pathPlaceholder": "rtsp://...", + "roles": "Roluri", + "rolesRequired": "Este necesar cel puțin un rol", + "rolesUnique": "Fiecare rol (audio, detect, record) poate fi atribuit unui singur stream", + "addInput": "Adaugă stream de Intrare", + "removeInput": "Elimină stream de Intrare", + "inputsRequired": "Este necesar cel puțin un stream de intrare" + }, + "go2rtcStreams": "Stream-uri go2rtc", + "streamUrls": "URL-uri de stream", + "addUrl": "Adaugă URL", + "addGo2rtcStream": "Adaugă stream go2rtc", + "toast": { + "success": "Camera {{cameraName}} a fost salvată cu succes" + } + }, + "deleteCamera": "Șterge camera", + "deleteCameraDialog": { + "title": "Șterge camera", + "description": "Ștergerea unei camere va elimina definitiv toate înregistrările, obiectele urmărite și configurația pentru acea cameră. Orice fluxuri go2rtc asociate cu această cameră ar putea necesita în continuare eliminarea manuală.", + "selectPlaceholder": "Alege camera...", + "confirmTitle": "Ești sigur?", + "confirmWarning": "Ștergerea {{cameraName}} nu poate fi anulată.", + "deleteExports": "Șterge și exporturile pentru această cameră", + "confirmButton": "Șterge definitiv", + "success": "Camera {{cameraName}} a fost ștearsă cu succes", + "error": "Eroare la ștergerea camerei {{cameraName}}" + }, + "profiles": { + "title": "Suprascrieri profil cameră", + "selectLabel": "Selectează profilul", + "description": "Configurează ce camere sunt pornite sau oprite când un profil este activat. Camerele setate pe \"Moștenește\" își păstrează starea implicită.", + "inherit": "Moștenire", + "enabled": "Activat", + "disabled": "Dezactivat", + "on": "Pornit", + "off": "Oprit" + }, + "cameraType": { + "title": "Tip cameră", + "label": "Tip cameră", + "description": "Setează tipul pentru fiecare cameră. Camerele LPR dedicate sunt camere cu un singur scop, cu zoom optic puternic pentru a captura plăcuțele de înmatriculare ale vehiculelor aflate la distanță. Majoritatea camerelor ar trebui să folosească tipul normal de cameră, cu excepția cazului în care camera este special pentru LPR și are o vedere strâns focalizată pe plăcuțele de înmatriculare.", + "normal": "Normal", + "dedicatedLpr": "LPR dedicat", + "saveSuccess": "Tipul camerei a fost actualizat pentru {{cameraName}}. Repornește Frigate pentru a aplica modificările." + }, + "description": "Adaugă, editează și șterge camere, controlează starea fiecărei camere și configurează excepții pe profil și pe tip de cameră. Pentru a configura stream-uri, detecție, mișcare și alte setări specifice camerelor, alege secțiunea corespunzătoare din Configurare cameră.", + "clone": { + "sectionTitle": "Clonează setările", + "sectionDescription": "Copiază configurația de la o cameră la altă cameră sau la una nouă.", + "button": "Clonează setările", + "title": "Clonează setările camerei", + "description": "Copiază configurația unei camere la una sau mai multe alte camere sau la o cameră nouă. Identitatea (nume, nume prietenos, URL interfață web, ordine de afișare) nu este niciodată copiată.", + "source": { + "label": "Cameră sursă", + "required": "Selectează o cameră sursă", + "placeholder": "Selectează o cameră sursă" + }, + "target": { + "newRadio": "Cameră nouă", + "newNameLabel": "Numele camerei", + "legend": "Țintă", + "newNamePlaceholder": "ex., usa_spate sau Ușa din spate", + "newNameRequired": "Numele camerei este obligatoriu", + "newNameInvalid": "Nume cameră invalid", + "newNameCollision": "O cameră cu acest nume există deja", + "newStreamsForced": "Stream-urile sunt mereu copiate pentru o cameră nouă.", + "allCameras": "Toate camerele", + "existingCamerasRadio": "Camere existente", + "existingPlaceholder": "Selectează cel puțin o cameră", + "existingDisabled": "Nu există alte camere către care să copiezi" + }, + "categories": { + "legend": "Setări de clonat", + "selectAll": "Selectează tot", + "selectNone": "Deselectează tot", + "description": "Alege ce setări să copiezi de la camera sursă.", + "general": "General", + "resetDefaults": "Resetează la setările implicite", + "spatial": "Setări spațiale", + "streams": "Stream-uri", + "spatialWarningTitle": "Nepotrivire de rezoluție", + "spatialWarning": "Rezoluția de detecție ({{srcWidth}}×{{srcHeight}}) a camerei sursă {{srcCamera}} este diferă de: {{cameras}}. Poligoanele s-ar putea să nu se alinieze pe acele camere. Aceste setări implicite sunt dezactivate; activează-le pentru a copia ca atare.", + "restartHint": "Repornire necesară", + "items": { + "record": "Înregistrare", + "snapshots": "Snapshot-uri", + "review": "Revizuire", + "objects": "Obiecte", + "motion": "Detecție mișcare", + "audio": "Detecție sunet", + "notifications": "Notificări", + "birdseye": "Birdseye", + "audio_transcription": "Transcriere audio", + "mqtt": "MQTT", + "onvif": "ONVIF", + "timestamp_style": "Stil marcaj temporal", + "lpr": "Recunoașterea plăcuțelor de înmatriculare", + "face_recognition": "Recunoaștere facială", + "semantic_search": "Căutare semantică", + "genai": "AI Generativ", + "type": "Tip cameră (normală / LPR dedicată)", + "profiles": "Profile", + "zones": "Zone", + "detect": "Dimensiuni de detecție", + "motion_mask": "Măști de mișcare", + "object_masks": "Măști de obiecte", + "ffmpeg_live": "URL-uri și roluri pentru stream-uri" + } + }, + "footer": { + "changeCount_one": "{{count}} modificare va fi aplicată", + "changeCount_few": "{{count}} modificări vor fi aplicate", + "changeCount_other": "{{count}} de modificări vor fi aplicate", + "restartNeeded": "Va fi necesară o repornire pentru anumite modificări.", + "submit": "Clonare", + "submitting": "Se clonează…", + "liveOnly": "Toate modificările se vor aplica în timp real, fără repornire." + }, + "toast": { + "success": "Setări copiate la {{cameraName}}", + "successWithRestart": "Setări copiate la {{cameraName}}. Reporniți Frigate pentru a aplica toate modificările.", + "successMulti_one": "Setări copiate la {{count}} cameră", + "successMulti_few": "Setări copiate la {{count}} camere", + "successMulti_other": "Setări copiate la {{count}} de camere", + "partialFailure": "{{successCount}} secțiuni aplicate; '{{failedSection}}' a eșuat: {{errorMessage}}", + "successMultiWithRestart_one": "Setări copiate la {{count}} cameră. Reporniți Frigate pentru a aplica toate modificările.", + "successMultiWithRestart_few": "Setări copiate la {{count}} camere. Reporniți Frigate pentru a aplica toate modificările.", + "successMultiWithRestart_other": "Setări copiate la {{count}} de camere. Reporniți Frigate pentru a aplica toate modificările.", + "partialFailureMulti": "Copiat la {{successCount}} cameră(e); a eșuat pentru {{failed}}: {{errorMessage}}", + "newCameraPartialFailure": "Camera {{cameraName}} a fost creată, dar unele setări nu au putut fi copiate: {{errorMessage}}", + "sourceMissing": "Camera sursă nu mai există", + "submitError": "Clonarea camerei a eșuat: {{errorMessage}}" + } + } + }, + "cameraReview": { + "title": "Setări Review Cameră", + "object_descriptions": { + "title": "Descrieri Obiecte cu AI Generativ", + "desc": "Activează/dezactivează temporar descrierile AI pentru această cameră. Când sunt dezactivate, nu se vor solicita descrieri AI pentru obiectele urmărite." + }, + "review_descriptions": { + "title": "Descrieri Review cu AI Generativ", + "desc": "Activează/dezactivează temporar descrierile AI pentru elementele de review. Când sunt dezactivate, nu se vor genera descrieri AI pentru acestea." + }, + "review": { + "title": "Revizuire", + "desc": "Activează/dezactivează temporar alertele și detecțiile pentru această cameră până la repornirea Frigate. Când sunt dezactivate, nu se vor genera elemente noi de review. ", + "alerts": "Alerte ", + "detections": "Detecții " + }, + "reviewClassification": { + "title": "Clasificare Review", + "desc": "Frigate clasifică elementele de review ca Alerte sau Detecții. Implicit, toate obiectele de tip persoană și mașină sunt considerate Alerte. Poți rafina clasificarea configurând zone obligatorii.", + "noDefinedZones": "Nu există zone definite pentru această cameră.", + "objectAlertsTips": "Toate obiectele {{alertsLabels}} de pe {{cameraName}} vor apărea ca Alerte.", + "zoneObjectAlertsTips": "Toate obiectele {{alertsLabels}} detectate în {{zone}} pe {{cameraName}} vor apărea ca Alerte.", + "objectDetectionsTips": "Toate obiectele {{detectionsLabels}} neclasificate pe {{cameraName}} vor apărea ca Detecții, indiferent de zonă.", + "zoneObjectDetectionsTips": { + "text": "Toate obiectele {{detectionsLabels}} neclasificate în {{zone}} pe {{cameraName}} vor apărea ca Detecții.", + "notSelectDetections": "Toate obiectele {{detectionsLabels}} detectate în {{zone}} pe {{cameraName}} care nu sunt Alerte vor apărea ca Detecții, indiferent de zonă.", + "regardlessOfZoneObjectDetectionsTips": "Toate obiectele {{detectionsLabels}} neclasificate pe {{cameraName}} vor apărea ca Detecții, indiferent de zonă." + }, + "unsavedChanges": "Modificări nesalvate la Clasificarea Review pentru {{camera}}", + "selectAlertsZones": "Selectează zonele pentru Alerte", + "selectDetectionsZones": "Selectează zonele pentru Detecții", + "limitDetections": "Limitează detecțiile la anumite zone", + "toast": { + "success": "Configurația Clasificării Review a fost salvată. Repornește Frigate pentru aplicare." + } + } + }, + "saveAllPreview": { + "title": "Modificări de salvat", + "triggerLabel": "Revizuiește modificările în așteptare", + "empty": "Nicio modificare în așteptare.", + "scope": { + "label": "Domeniu", + "global": "Global", + "camera": "Cameră: {{cameraName}}" + }, + "field": { + "label": "Câmp" + }, + "value": { + "label": "Valoare nouă", + "reset": "Resetare" + }, + "profile": { + "label": "Profil" + } + }, + "detectionModel": { + "plusActive": { + "title": "Gestionare model Frigate+", + "label": "Sursa modelului curent", + "description": "Această instanță rulează un model Frigate+. Selectează sau schimbă modelul în setările Frigate+.", + "goToFrigatePlus": "Mergi la setările Frigate+", + "showModelForm": "Configurează manual un model" + } + }, + "maintenance": { + "title": "Mentenanță", + "sync": { + "title": "Sincronizare Media", + "desc": "Frigate va curăța periodic fișierele media conform setărilor de retenție. Este normal să apară câteva fișiere orfane în timp ce Frigate rulează. Folosește această funcție pentru a șterge fișierele media de pe disc care nu mai sunt referențiate în baza de date.", + "started": "Sincronizarea media a început.", + "alreadyRunning": "O sarcină de sincronizare rulează deja", + "error": "Eroare la pornirea sincronizării", + "currentStatus": "Stare", + "jobId": "ID Job", + "startTime": "Oră Start", + "endTime": "Oră Final", + "statusLabel": "Status", + "results": "Rezultate", + "errorLabel": "Eroare", + "mediaTypes": "Tipuri Media", + "allMedia": "Toate fișierele", + "dryRun": "Mod Simulare (Dry Run)", + "dryRunEnabled": "Niciun fișier nu va fi șters", + "dryRunDisabled": "Fișierele vor fi șterse", + "force": "Forțează", + "forceDesc": "Ignoră pragul de siguranță și finalizează sincronizarea chiar dacă mai mult de 50% din fișiere ar urma să fie șterse.", + "running": "Sincronizare în curs...", + "start": "Pornește Sincronizarea", + "inProgress": "Sincronizarea este în curs. Această pagină este dezactivată.", + "status": { + "queued": "În așteptare", + "running": "Rulează", + "completed": "Finalizat", + "failed": "Eșuat", + "notRunning": "Nu rulează" + }, + "resultsFields": { + "filesChecked": "Fișiere Verificate", + "orphansFound": "Fișiere Orfane Găsite", + "orphansDeleted": "Fișiere Orfane Șterse", + "aborted": "Abandonat. Ștergerea ar depăși pragul de siguranță.", + "error": "Eroare", + "totals": "Totaluri" + }, + "event_snapshots": "Snapshot-uri Obiecte Urmărite", + "event_thumbnails": "Miniaturi Obiecte Urmărite", + "review_thumbnails": "Miniaturi Review", + "previews": "Previzualizări", + "exports": "Exporturi", + "recordings": "Înregistrări", + "verbose": "Detaliat", + "verboseDesc": "Scrie pe disc o listă completă a fișierelor orfane pentru verificare." + }, + "regionGrid": { + "title": "Grilă regiune", + "desc": "Grila de regiune este o optimizare care învață unde apar de obicei obiectele de diferite dimensiuni în câmpul vizual al fiecărei camere. Frigate folosește aceste date pentru a redimensiona eficient regiunile de detecție. Grila este construită automat în timp, pe baza datelor de la obiectele urmărite.", + "clear": "Șterge grila regiune", + "clearConfirmTitle": "Șterge grila regiune", + "clearConfirmDesc": "Curățarea grilei de regiune nu este recomandată decât dacă ai schimbat recent mărimea modelului de detecție sau ai schimbat poziția fizică a camerei și ai probleme cu urmărirea obiectelor. Grila va fi reconstruită automat în timp, pe măsură ce obiectele sunt urmărite. Este necesară o repornire a Frigate pentru ca modificările să intre în vigoare.", + "clearSuccess": "Grila de regiune a fost ștearsă cu succes", + "clearError": "Eșec la ștergerea grilei de regiune", + "restartRequired": "Este necesară o repornire pentru ca modificările grilei de regiune să intre în vigoare" + } + }, + "configForm": { + "global": { + "title": "Setări Globale", + "description": "Aceste setări se aplică tuturor camerelor, cu excepția cazului în care sunt suprascrise în setările specifice ale unei camere." + }, + "camera": { + "title": "Setări Cameră", + "description": "Aceste setări se aplică doar pentru această cameră și suprascriu setările globale.", + "noCameras": "Nicio cameră disponibilă" + }, + "advancedSettingsCount": "Setări Avansate ({{count}})", + "advancedCount": "Avansat ({{count}})", + "showAdvanced": "Afișează Setările Avansate", + "tabs": { + "sharedDefaults": "Valori Implicite Comune", + "system": "Sistem", + "integrations": "Integrări" + }, + "additionalProperties": { + "keyLabel": "Cheie", + "valueLabel": "Valoare", + "keyPlaceholder": "Cheie nouă", + "remove": "Elimină" + }, + "timezone": { + "defaultOption": "Folosește fusul orar al browserului" + }, + "roleMap": { + "empty": "Nu există asocieri de roluri", + "roleLabel": "Rol", + "groupsLabel": "Grupuri", + "addMapping": "Adaugă asociere de rol", + "remove": "Elimină" + }, + "ffmpegArgs": { + "preset": "Presetare", + "manual": "Argumente manuale", + "inherit": "Moștenește de la setările camerei", + "selectPreset": "Selectează presetarea", + "manualPlaceholder": "Introdu argumentele FFmpeg", + "none": "Niciunul", + "useGlobalSetting": "Moștenește din setarea globală", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (GPU Intel/AMD)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "GPU NVIDIA", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (Generic)", + "preset-http-mjpeg-generic": "HTTP MJPEG (Generic)", + "preset-http-reolink": "HTTP - Camere Reolink", + "preset-rtmp-generic": "RTMP (Generic)", + "preset-rtsp-generic": "RTSP (Generic)", + "preset-rtsp-restream": "RTSP - Restream de la go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - Restream de la go2rtc (Latență scăzută)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Înregistrare (Generic, fără audio)", + "preset-record-generic-audio-copy": "Înregistrare (Generic + Copiere audio)", + "preset-record-generic-audio-aac": "Înregistrare (Generic + Audio în AAC)", + "preset-record-mjpeg": "Înregistrare - Camere MJPEG", + "preset-record-jpeg": "Înregistrare - Camere JPEG", + "preset-record-ubiquiti": "Înregistrare - Camere Ubiquiti" + } + }, + "cameraInputs": { + "itemTitle": "Stream-ul {{index}}", + "sourceMode": { + "restream": "Restream (go2rtc)", + "manual": "Cale de intrare manuală", + "go2rtcStreamLabel": "Stream go2rtc", + "go2rtcStreamPlaceholder": "Selectează un stream go2rtc", + "noGo2rtcStreams": "Niciun stream go2rtc configurat", + "availableStreams": "Stream-uri disponibile", + "go2rtcStreamSearch": "Caută stream-uri...", + "noMatchingStreams": "Niciun stream corespunzător" + } + }, + "restartRequiredField": "Necesită repornire", + "restartRequiredFooter": "Configurația a fost modificată - Necesită repornire", + "sections": { + "detect": "Detecție", + "record": "Înregistrare", + "snapshots": "Snapshot-uri", + "motion": "Mișcare", + "objects": "Obiecte", + "review": "Revizuire", + "audio": "Audio", + "notifications": "Notificări", + "live": "Vizualizare Live", + "timestamp_style": "Timestamp-uri", + "mqtt": "MQTT", + "database": "Bază de date", + "telemetry": "Telemetrie", + "auth": "Autentificare", + "tls": "TLS", + "proxy": "Proxy", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg", + "detectors": "Detectoare", + "model": "Model", + "semantic_search": "Căutare Semantică", + "genai": "GenAI", + "face_recognition": "Recunoaștere Facială", + "lpr": "Recunoaștere Numere Înmatriculare", + "birdseye": "Birdseye", + "masksAndZones": "Măști / zone" + }, + "detect": { + "title": "Setări Detecție" + }, + "detectors": { + "title": "Setări Detector", + "singleType": "Este permis un singur detector de tip {{type}}.", + "keyRequired": "Numele detectorului este obligatoriu.", + "keyDuplicate": "Numele detectorului există deja.", + "noSchema": "Nu sunt disponibile scheme de detectoare.", + "none": "Nicio instanță de detector configurată.", + "add": "Adaugă detector", + "addCustomKey": "Adaugă cheie personalizată" + }, + "record": { + "title": "Setări Înregistrare" + }, + "snapshots": { + "title": "Setări snapshot-uri" + }, + "motion": { + "title": "Setări Mișcare" + }, + "objects": { + "title": "Setări Obiecte" + }, + "audioLabels": { + "summary": "{{count}} etichete audio selectate", + "empty": "Nu există etichete audio disponibile" + }, + "objectLabels": { + "summary": "{{count}} tipuri de obiecte selectate", + "empty": "Nu există etichete de obiecte disponibile" + }, + "filters": { + "objectFieldLabel": "{{field}} pentru {{label}}" + }, + "zoneNames": { + "summary": "{{count}} selectate", + "empty": "Nu există zone disponibile" + }, + "inputRoles": { + "summary": "{{count}} roluri selectate", + "empty": "Nu există roluri disponibile", + "options": { + "detect": "Detecție", + "record": "Înregistrare", + "audio": "Audio" + } + }, + "review": { + "title": "Setări Revizuire" + }, + "audio": { + "title": "Setări Audio" + }, + "notifications": { + "title": "Setări Notificări" + }, + "live": { + "title": "Setări Vizualizare Live" + }, + "timestamp_style": { + "title": "Setări Timestamp" + }, + "searchPlaceholder": "Caută...", + "genaiRoles": { + "options": { + "embeddings": "Înglobare", + "vision": "Viziune", + "tools": "Instrumente", + "descriptions": "Descrieri", + "chat": "Chat" + } + }, + "semanticSearchModel": { + "placeholder": "Selectează modelul…", + "builtIn": "Modele integrate", + "genaiProviders": "Furnizori GenAI" + }, + "reviewLabels": { + "summary": "{{count}} etichete selectate", + "empty": "Nicio etichetă disponibilă", + "allNonAlertDetections": "Toată activitatea fără alertă va fi inclusă ca detecții." + }, + "addCustomLabel": "Adaugă etichetă personalizată...", + "genaiModel": { + "placeholder": "Selectează sau introdu un model…", + "search": "Caută sau introdu un model…", + "noModels": "Niciun model disponibil", + "available": "Modele disponibile", + "useCustom": "Folosește \"{{value}}\"", + "refresh": "Reîmprospătează modelele", + "probeFailed": "Nu s-au putut interoga modelele", + "fetchedModels": "Lista de modele preluată cu succes" + }, + "knownPlates": { + "namePlaceholder": "ex. Mașina soției", + "platePlaceholder": "Număr plăcuță sau regex" + }, + "semanticSearchModelSize": { + "notApplicable": "Nu se aplică pentru furnizorii de GenAI" + }, + "liveStreams": { + "streamNameLabel": "Nume stream", + "go2rtcStreamLabel": "stream go2rtc", + "go2rtcStreamPlaceholder": "Selectează un stream go2rtc", + "streamNamePlaceholder": "ex., Stream HD principal", + "go2rtcStreamSearch": "Căutați sau introduceți un nume de stream…", + "noGo2rtcStreams": "Niciun stream go2rtc configurat", + "availableStreams": "Stream-uri disponibile", + "useCustom": "Folosește \"{{value}}\"", + "addStream": "Adaugă stream" + }, + "ptzPresets": { + "placeholder": "Selectați sau introduceți o presetare...", + "search": "Căutați sau introduceți o presetare...", + "available": "Presetări cameră", + "noPresets": "Nu sunt presetări disponibile", + "useCustom": "Folosește \"{{value}}\"" + }, + "defaultRole": { + "admin": "Administrator", + "viewer": "Vizualizator" + } + }, + "globalConfig": { + "title": "Configurare Globală", + "description": "Configurează setările globale care se aplică tuturor camerelor, cu excepția celor suprascrie.", + "toast": { + "success": "Setările globale au fost salvate cu succes", + "error": "Eroare la salvarea setărilor globale", + "validationError": "Validarea a eșuat" + } + }, + "cameraConfig": { + "title": "Configurare Cameră", + "description": "Configurează setările pentru camere individuale. Aceste setări suprascriu valorile globale.", + "overriddenBadge": "Suprascris", + "resetToGlobal": "Resetează la Global", + "toast": { + "success": "Setările camerei au fost salvate cu succes", + "error": "Eroare la salvarea setărilor camerei" + } + }, + "toast": { + "success": "Setările au fost salvate cu succes", + "successRestartRequired": "Setările au fost salvate cu succes. Repornește Frigate pentru a aplica modificările.", + "error": "Eroare la salvarea setărilor", + "validationError": "Validarea a eșuat: {{message}}", + "resetSuccess": "Resetat la valorile globale implicite", + "resetError": "Eroare la resetarea setărilor", + "saveAllSuccess_one": "S-a salvat cu succes {{count}} secțiune.", + "saveAllSuccess_few": "Cele {{count}} secțiuni au fost salvate cu succes.", + "saveAllSuccess_other": "Toate cele {{count}} de secțiuni au fost salvate cu succes.", + "saveAllPartial_one": "{{successCount}} din {{totalCount}} secțiune salvată. {{failCount}} eșuate.", + "saveAllPartial_few": "{{successCount}} din {{totalCount}} secțiuni salvate. {{failCount}} eșuate.", + "saveAllPartial_other": "{{successCount}} din {{totalCount}} de secțiuni salvate. {{failCount}} eșuate.", + "saveAllFailure": "Eroare la salvarea tuturor secțiunilor.", + "applied": "Setările au fost aplicate cu succes", + "saveAllSuccessRestartRequired_one": "{{count}} secțiune salvată cu succes. Repornește Frigate pentru a aplica modificările.", + "saveAllSuccessRestartRequired_few": "{{count}} secțiuni salvate cu succes. Repornește Frigate pentru a aplica modificările.", + "saveAllSuccessRestartRequired_other": "{{count}} de secțiuni salvate cu succes. Repornește Frigate pentru a aplica modificările." + }, + "unsavedChanges": "Ai modificări nesalvate", + "confirmReset": "Confirmă Resetarea", + "resetToDefaultDescription": "Această acțiune va reseta toate setările din această secțiune la valorile implicite. Acțiunea este ireversibilă.", + "resetToGlobalDescription": "Această acțiune va reseta setările din această secțiune la valorile globale implicite. Acțiunea este ireversibilă.", + "button": { + "overriddenGlobal": "Suprascris (global)", + "overriddenGlobalTooltip": "Această cameră suprascrie setările globale de configurare din această secțiune", + "overriddenBaseConfig": "Suprascris (configurația de bază)", + "overriddenBaseConfigTooltip": "Profilul {{profile}} suprascrie setările de configurare din această secțiune", + "overriddenInCameras": { + "label_one": "Suprascris în {{count}} cameră", + "label_few": "Suprascris în {{count}} camere", + "label_other": "Suprascris în {{count}} de camere", + "tooltip_one": "{{count}} cameră suprascrie valorile din această secțiune. Click pentru a vedea detaliile.", + "tooltip_few": "{{count}} camere suprascriu valorile din această secțiune. Click pentru a vedea detaliile.", + "tooltip_other": "{{count}} de camere suprascriu valorile din această secțiune. Click pentru a vedea detaliile.", + "heading_one": "Această secțiune globală are câmpuri care sunt suprascrise în {{count}} cameră.", + "heading_few": "Această secțiune globală are câmpuri care sunt suprascrise în {{count}} camere.", + "heading_other": "Această secțiune globală are câmpuri care sunt suprascrise în {{count}} de camere.", + "othersField_one": "{{count}} alta", + "othersField_few": "{{count}} alte", + "othersField_other": "{{count}} de alte", + "profilePrefix": "Profil {{profile}}: {{fields}}" + }, + "overriddenGlobalHeading_one": "Această cameră suprascrie {{count}} câmp din configurația globală:", + "overriddenGlobalHeading_few": "Această cameră suprascrie {{count}} câmpuri din configurația globală:", + "overriddenGlobalHeading_other": "Această cameră suprascrie {{count}} de câmpuri din configurația globală:", + "overriddenGlobalNoDeltas": "Această cameră suprascrie configurația globală, dar nicio valoare a câmpurilor nu diferă.", + "overriddenBaseConfigHeading_one": "Profilul {{profile}} suprascrie {{count}} câmp din configurația de bază:", + "overriddenBaseConfigHeading_few": "Profilul {{profile}} suprascrie {{count}} câmpuri din configurația de bază:", + "overriddenBaseConfigHeading_other": "Profilul {{profile}} suprascrie {{count}} de câmpuri din configurația de bază:", + "overriddenBaseConfigNoDeltas": "Profilul {{profile}} suprascrie această secțiune, dar nicio valoare a câmpurilor nu diferă de configurația de bază." + }, + "profiles": { + "title": "Profile", + "activeProfile": "Profil activ", + "noActiveProfile": "Niciun profil activ", + "active": "Activ", + "activated": "Profilul '{{profile}}' a fost activat", + "activateFailed": "Setarea profilului a eșuat", + "deactivated": "Profil dezactivat", + "noProfiles": "Niciun profil definit.", + "noOverrides": "Fără suprascrieri", + "cameraCount_one": "{{count}} cameră", + "cameraCount_few": "{{count}} camere", + "cameraCount_other": "{{count}} de camere", + "baseConfig": "Configurație de bază", + "addProfile": "Adaugă profil", + "newProfile": "Profil nou", + "profileNamePlaceholder": "de ex., Armat, Plecat, Mod noapte", + "friendlyNameLabel": "Nume profil", + "profileIdLabel": "ID profil", + "profileIdDescription": "Identificator intern folosit în configurație și automatizări", + "nameInvalid": "Sunt permise doar litere mici, numere și underscore-uri", + "nameDuplicate": "Un profil cu acest nume există deja", + "error": { + "mustBeAtLeastTwoCharacters": "Trebuie să aibă cel puțin 2 caractere", + "mustNotContainPeriod": "Nu trebuie să conțină puncte", + "alreadyExists": "Un profil cu acest ID există deja" + }, + "renameProfile": "Redenumește profilul", + "renameSuccess": "Profilul a fost redenumit în '{{profile}}'", + "deleteProfile": "Șterge profilul", + "deleteProfileConfirm": "Ștergi profilul \"{{profile}}\" de pe toate camerele? Această acțiune nu poate fi anulată.", + "deleteSuccess": "Profilul '{{profile}}' a fost șters", + "createSuccess": "Profilul '{{profile}}' a fost creat", + "removeOverride": "Elimină suprascrierea profilului", + "deleteSection": "Șterge suprascrierile secțiunii", + "deleteSectionConfirm": "Elimini suprascrierile {{section}} pentru profilul {{profile}} de pe {{camera}}?", + "deleteSectionSuccess": "Au fost eliminate suprascrierile {{section}} pentru {{profile}}", + "enableSwitch": "Activează profilele", + "enabledDescription": "Profilele sunt activate. Creează un profil nou mai jos, navighează la o secțiune de configurare a camerei pentru a face modificările, și salvează pentru ca acestea să aibă efect.", + "disabledDescription": "Profilele îți permit să definești seturi denumite de suprascrieri pentru configurația camerei (de ex., armat, plecat, noapte) care pot fi activate la cerere.", + "columnCamera": "Camera", + "columnOverrides": "Suprascrieri profil" + }, + "go2rtcStreams": { + "title": "Stream-uri go2rtc", + "description": "Gestionează configurațiile de stream-uri go2rtc pentru retransmisia camerelor. Fiecare stream are un nume și unul sau mai multe URL-uri sursă.", + "addStream": "Adaugă stream", + "addStreamDesc": "Introdu un nume pentru noul stream. Acest nume va fi folosit pentru a referenția stream-ul în configurația camerei tale.", + "addUrl": "Adaugă URL", + "streamName": "Nume stream", + "streamNamePlaceholder": "de ex., usa_intrare", + "streamUrlPlaceholder": "de ex., rtsp://user:parola@192.168.1.100/stream", + "deleteStream": "Șterge stream", + "deleteStreamConfirm": "Sigur vrei să ștergi stream-ul \"{{streamName}}\"? Camerele care referențiază acest stream s-ar putea să nu mai funcționeze.", + "noStreams": "Niciun stream go2rtc configurat. Adaugă un stream pentru a începe.", + "validation": { + "nameRequired": "Numele stream-ului este obligatoriu", + "nameDuplicate": "Un stream cu acest nume există deja", + "nameInvalid": "Numele stream-ului poate conține doar litere, numere, underscore-uri și cratime", + "urlRequired": "Cel puțin un URL este obligatoriu" + }, + "renameStream": "Redenumește stream-ul", + "renameStreamDesc": "Introdu un nume nou pentru acest stream. Redenumirea unui stream poate strica camerele sau alte stream-uri care îl referențiază după nume.", + "newStreamName": "Nume nou de stream", + "ffmpeg": { + "useFfmpegModule": "Folosește modul de compatibilitate (ffmpeg)", + "video": "Video", + "audio": "Audio", + "hardware": "Accelerare hardware", + "videoCopy": "Copiază", + "videoH264": "Transcodează în H.264", + "videoH265": "Transcodează în H.265", + "videoExclude": "Exclude", + "audioCopy": "Copiază", + "audioAac": "Transcodează în AAC", + "audioOpus": "Transcodează în Opus", + "audioPcmu": "Transcodează în PCM μ-law", + "audioPcma": "Transcodează în PCM A-law", + "audioPcm": "Transcodează în PCM", + "audioMp3": "Transcodează în MP3", + "audioExclude": "Exclude", + "hardwareNone": "Fără accelerare hardware", + "hardwareAuto": "Automat (recomandat)", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "Adaugă codec video", + "addAudioCodec": "Adaugă codec audio", + "removeCodec": "Elimină codecul" + }, + "streamNumber": "Stream {{index}}", + "sourceNumber": "Sursă {{index}}" + }, + "timestampPosition": { + "tl": "Sus stânga", + "tr": "Sus dreapta", + "bl": "Jos stânga", + "br": "Jos dreapta" + }, + "onvif": { + "profileAuto": "Auto", + "profileLoading": "Se încarcă profilurile...", + "autotracking": { + "zooming": { + "disabled": "Dezactivat", + "absolute": "Absolut", + "relative": "Relativ" + } + } + }, + "configMessages": { + "review": { + "recordDisabled": "Înregistrarea este dezactivată, elementele de revizuire nu vor fi generate.", + "detectDisabled": "Detecția obiectelor este dezactivată. Elementele de revizuire necesită obiecte detectate pentru a categorisi alertele și detecțiile.", + "allNonAlertDetections": "Toată activitatea fără alertă va fi inclusă ca detecții.", + "genaiImageSourceRecordingsRecordDisabled": "Sursa imaginii este setată pe 'recordings', dar înregistrarea este dezactivată. Frigate va reveni la imaginile de previzualizare." + }, + "audio": { + "noAudioRole": "Niciun flux nu are rolul audio definit. Trebuie să activați rolul audio pentru ca detecția audio să funcționeze." + }, + "audioTranscription": { + "audioDetectionDisabled": "Detecția audio nu este activată pentru această cameră. Transcrierea audio necesită ca detecția audio să fie activă." + }, + "detect": { + "fpsGreaterThanFive": "Setarea FPS-ului de detecție mai mare de 5 nu este recomandată. Valorile mai mari pot cauza probleme de performanță și nu vor oferi niciun beneficiu.", + "disabled": "Detecția de obiecte este dezactivată. Snapshot-urile, elementele de revizuire și îmbogățirile precum recunoașterea facială, recunoașterea plăcuțelor de înmatriculare și AI-ul generativ nu vor funcționa.", + "resolutionShouldBeMultipleOfFour": "Pentru rezultate optime, lățimea și înălțimea de detecție ar trebui să fie multipli de 4. Alte valori pare pot produce artefacte vizuale sau o ușoară distorsionare în fluxul de detecție.", + "aspectRatioMismatch": "Lățimea și înălțimea introduse nu se potrivesc cu raportul de aspect al rezoluției actuale de detecție. Acest lucru poate produce o imagine întinsă sau distorsionată.", + "maxFramesSet": "Setarea numărului maxim de cadre suprascrie comportamentul implicit și dezactivează urmărirea obiectelor staționare. Există foarte puține situații în care acest lucru este necesar, folosește cu precauție.", + "squareResolution": "O rezoluție de detecție pătrată este neobișnuită. Lățimea și înălțimea de detecție ar trebui să se potrivească cu raportul de aspect al camerei tale (de exemplu, 16:9), nu cu dimensiunile modelului de detecție a obiectelor. Un raport de aspect nepotrivit poate întinde imaginea și reduce acuratețea detecției.", + "resolutionHigh": "Această rezoluție de detecție este mai mare decât cea recomandată și poate cauza un consum crescut de resurse fără a îmbunătăți acuratețea detecției. O rezoluție de detecție de 1080p sau mai mică este recomandată pentru majoritatea camerelor.", + "globalResolutionMultipleCameras": "O rezoluție de detecție globală este setată în timp ce sunt configurate mai multe camere. Dacă nu cumva toate camerele împart aceeași rezoluție și același raport de aspect, lățimea și înălțimea de detecție ar trebui definite per cameră pentru a se potrivi cu raportul de aspect nativ al fiecărei camere." + }, + "faceRecognition": { + "globalDisabled": "Îmbogățirea pentru recunoaștere facială trebuie activată pentru ca funcțiile de recunoaștere facială să funcționeze pe această cameră.", + "personNotTracked": "Recunoașterea facială necesită ca obiectul 'person' să fie urmărit. Activează 'person' în Obiecte pentru această cameră.", + "modelSizeLarge": "Modelul 'large' necesită un GPU sau NPU pentru o performanță rezonabilă. Folosește 'small' pe sistemele doar cu CPU." + }, + "lpr": { + "globalDisabled": "Îmbogățirea pentru recunoașterea plăcuțelor de înmatriculare trebuie activată pentru ca funcțiile LPR să funcționeze pe această cameră.", + "vehicleNotTracked": "Recunoașterea plăcuțelor de înmatriculare necesită ca „car” sau „motorcycle” să fie urmărite.", + "modelSizeLarge": "Modelul 'large' este optimizat pentru plăcuțele de înmatriculare pe mai multe rânduri. Modelul 'small' oferă o performanță mai bună decât 'large' și ar trebui folosit cu excepția cazului în care regiunea ta folosește formate de plăcuțe pe mai multe rânduri." + }, + "record": { + "noRecordRole": "Niciun flux nu are rolul de înregistrare definit. Înregistrarea nu va funcționa." + }, + "birdseye": { + "objectsModeDetectDisabled": "Birdseye este setat pe modul 'objects', dar detecția obiectelor este dezactivată pentru această cameră. Camera nu va apărea în Birdseye." + }, + "snapshots": { + "detectDisabled": "Detecția obiectelor este dezactivată. Snapshot-urile sunt generate din obiectele urmărite și nu vor fi create." + }, + "detectors": { + "mixedTypes": "Toți detectorii trebuie să folosească același tip. Șterge detectorii existenți pentru a folosi un alt tip.", + "mixedTypesSuggestion": "Toți detectorii trebuie să folosească același tip. Șterge detectorii existenți sau selectează {{type}}." + }, + "objects": { + "genaiNoDescriptionsProvider": "Trebuie să configurezi un furnizor GenAI cu rolul 'descriptions' pentru ca descrierile să fie generate." + }, + "semanticSearch": { + "jinav2SmallModelSize": "Dimensiunea 'small' cu modelul Jina V2 are un cost ridicat de RAM și inferență. Modelul 'large' cu un GPU dedicat este recomandat." + }, + "onvif": { + "autotrackingNoZones": "Autotracking-ul necesită cel puțin o zonă. Definește o zonă pentru această cameră în Măști / Zone, apoi seteaz-o ca zonă obligatorie mai jos." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "Argumentele manuale pentru accelerarea hardware nu sunt recomandate. Dacă nu există o cerință specifică, selectează presetarea care se potrivește cu hardware-ul tău." + } + }, + "birdseye": { + "trackingMode": { + "objects": "Obiecte", + "motion": "Mișcare", + "continuous": "Continuu" + }, + "cameraOrder": { + "label": "Ordinea camerelor", + "description": "Trage camerele pentru a le seta ordinea în aranjamentul Birdseye.", + "saving": "Se salvează…", + "saved": "Salvat", + "reorderHandle": "Trage pentru a reordona" + } + }, + "snapshot": { + "retainMode": { + "all": "Toate", + "motion": "Mișcare", + "active_objects": "Obiecte active" + } + }, + "ui": { + "timeFormat": { + "browser": "Browser", + "12hour": "12 ore", + "24hour": "24 de ore" + }, + "TimeOrDateStyle": { + "long": "Lung", + "medium": "Mediu", + "short": "Scurt", + "full": "Complet" + }, + "unitSystem": { + "metric": "Metric", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Înregistrări", + "previews": "Previzualizări" + } + }, + "logger": { + "logLevel": { + "debug": "Depanare", + "info": "Informații", + "warning": "Avertisment", + "error": "Eroare", + "critical": "Critic" + } + }, + "modelSize": { + "small": "Mic", + "large": "Mare" + }, + "retainMode": { + "all": "Toate", + "motion": "Mișcare", + "active_objects": "Obiecte active" + }, + "previewQuality": { + "medium": "Mediu", + "very_high": "Foarte ridicat", + "high": "Ridicat", + "low": "Scăzut", + "very_low": "Foarte scăzut" + }, + "menuDot": { + "overrideGlobal": "Această secțiune suprascrie configurația globală", + "overrideProfile": "Această secțiune este suprascrisă de profilul {{profile}}", + "unsaved": "Această secțiune are modificări nesalvate" + }, + "detectorsAndModel": { + "title": "Detectori și model", + "description": "Configurează backend-ul detectorului care rulează detecția obiectelor și modelul pe care îl folosește. Modificările sunt salvate împreună, astfel încât detectorul și modelul să rămână sincronizate.", + "cardTitles": { + "model": "Model de detecție", + "detector": "Hardware detector" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Model personalizat" + }, + "mismatch": { + "warning": "Modelul curent Frigate+ \"{{model}}\" necesită detectorul {{required}}. Alege un model compatibil mai jos sau treci la Model personalizat înainte de a salva." + }, + "plusModel": { + "requiresDetector": "Necesită: {{detector}}", + "noModelSelected": "Selectează un model Frigate+" + }, + "toast": { + "saveSuccess": "Setările pentru detectoare și model au fost salvate. Repornește Frigate pentru a aplica modificările.", + "saveError": "Nu s-au putut salva setările pentru detector și model" + }, + "unsavedChanges": "Modificări nesalvate pentru detector și model", + "restartRequired": "Repornire necesară (detector sau model schimbat)" + } +} diff --git a/web/public/locales/ro/views/system.json b/web/public/locales/ro/views/system.json new file mode 100644 index 0000000..7c0f634 --- /dev/null +++ b/web/public/locales/ro/views/system.json @@ -0,0 +1,276 @@ +{ + "documentTitle": { + "storage": "Statistici Stocare - Frigate", + "cameras": "Statistici Camere - Frigate", + "general": "Statistici Generale - Frigate", + "logs": { + "go2rtc": "Jurnale Go2RTC - Frigate", + "nginx": "Jurnale Nginx - Frigate", + "frigate": "Jurnale Frigate - Frigate", + "websocket": "Jurnale de mesaje - Frigate" + }, + "enrichments": "Statistici Procesări Avansate - Frigate" + }, + "general": { + "hardwareInfo": { + "npuUsage": "Utilizare NPU", + "npuMemory": "Memorie NPU", + "gpuUsage": "Utilizare GPU", + "gpuMemory": "Memorie GPU", + "title": "Informații Hardware", + "gpuEncoder": "Encoder GPU", + "gpuDecoder": "Decoder GPU", + "gpuInfo": { + "vainfoOutput": { + "returnCode": "Cod de retur: {{code}}", + "processOutput": "Ieșire proces:", + "title": "Ieșire Vainfo", + "processError": "Eroare proces:" + }, + "nvidiaSMIOutput": { + "title": "Ieșire Nvidia SMI", + "name": "Nume: {{name}}", + "driver": "Driver: {{driver}}", + "cudaComputerCapability": "Capacitate calcul CUDA: {{cuda_compute}}", + "vbios": "Info VBios: {{vbios}}" + }, + "copyInfo": { + "label": "Copiază info GPU" + }, + "toast": { + "success": "Am copiat informațiile GPU în clipboard" + }, + "closeInfo": { + "label": "Închide info GPU" + } + }, + "intelGpuWarning": { + "title": "Avertisment statistici GPU Intel", + "message": "Statisticile GPU sunt indisponibile", + "description": "Acesta este un bug cunoscut în instrumentele de raportare GPU Intel (intel_gpu_top), unde acestea se blochează și returnează repetat o utilizare GPU de 0% chiar și atunci când accelerarea hardware și detecția obiectelor rulează corect pe (i)GPU. Aceasta nu este o problemă Frigate. Poți reporni host-ul pentru a remedia temporar problema și a confirma că GPU-ul funcționează corect. Performanța nu este afectată." + }, + "gpuTemperature": "Temperatură GPU", + "npuTemperature": "Temperatură NPU", + "gpuCompute": "Calcul / Codare GPU" + }, + "detector": { + "temperature": "Temperatură detector", + "title": "Detectoare", + "cpuUsage": "Utilizare CPU detector", + "inferenceSpeed": "Viteză inferență detector", + "memoryUsage": "Utilizare memorie detector", + "cpuUsageInformation": "CPU utilizat pentru pregătirea datelor de intrare și ieșire către/dinspre modelele de detecție. Această valoare nu măsoară utilizarea inferenței, chiar dacă se folosește un GPU sau un accelerator." + }, + "otherProcesses": { + "title": "Alte procese", + "processCpuUsage": "Utilizare CPU procese", + "processMemoryUsage": "Utilizare memorie procese", + "series": { + "go2rtc": "go2rtc", + "recording": "înregistrare", + "review_segment": "segment revizuire", + "embeddings": "înglobări", + "audio_detector": "detector audio" + } + }, + "title": "General" + }, + "storage": { + "recordings": { + "title": "Înregistrări", + "earliestRecording": "Cea mai veche înregistrare disponibilă:", + "tips": "Această valoare reprezintă stocarea totală folosită de înregistrări în baza de date Frigate. Frigate nu monitorizează utilizarea stocării pentru toate fișierele de pe disc." + }, + "title": "Stocare", + "cameraStorage": { + "title": "Stocare Cameră", + "camera": "Cameră", + "unusedStorageInformation": "Informații stocare neutilizată", + "storageUsed": "Stocare", + "percentageOfTotalUsed": "Procent din total", + "unused": { + "title": "Neutilizat", + "tips": "Este posibil ca această valoare să nu reprezinte cu precizie spațiul liber disponibil pentru Frigate dacă ai și alte fișiere pe disc în afara înregistrărilor Frigate. Frigate nu urmărește utilizarea stocării în afara propriilor înregistrări." + }, + "bandwidth": "Lățime de bandă" + }, + "overview": "Prezentare generală", + "shm": { + "title": "Alocare SHM (shared memory)", + "warning": "Dimensiunea actuală SHM de {{total}}MB este prea mică. Crește-o la cel puțin {{min_shm}}MB.", + "readTheDocumentation": "Citește documentația", + "frameLifetime": { + "title": "Durata de viață a cadrului", + "description": "Fiecare cameră are {{frames}} sloturi de cadre în memoria partajată. La rata de cadre a celei mai rapide camere, fiecare cadru este disponibil pentru aproximativ {{lifetime}}s înainte de a fi suprascris." + } + } + }, + "title": "Sistem", + "logs": { + "download": { + "label": "Descarcă jurnalele" + }, + "copy": { + "label": "Copiază în clipboard", + "success": "Am copiat jurnalele în clipboard", + "error": "Nu am putut copia jurnalele în clipboard" + }, + "type": { + "label": "Tip", + "timestamp": "Ștampilă de timp", + "tag": "Etichetă", + "message": "Mesaj" + }, + "tips": "Jurnalele sunt transmise în timp real de la server", + "toast": { + "error": { + "fetchingLogsFailed": "Eroare la preluarea jurnalelor: {{errorMessage}}", + "whileStreamingLogs": "Eroare în timpul transmiterii jurnalelor: {{errorMessage}}" + } + }, + "websocket": { + "label": "Mesaje", + "pause": "Pauză", + "resume": "Reluare", + "clear": "Șterge", + "filter": { + "all": "Toate subiectele", + "topics": "Subiecte", + "events": "Evenimente", + "reviews": "Revizuiri", + "classification": "Clasificare", + "face_recognition": "Recunoaștere facială", + "lpr": "Recunoașterea numerelor de înmatriculare (LPR)", + "camera_activity": "Activitate cameră", + "system": "Sistem", + "camera": "Cameră", + "all_cameras": "Toate camerele", + "cameras_count_one": "{{count}} Cameră", + "cameras_count_other": "{{count}} Camere" + }, + "empty": "Niciun mesaj capturat încă", + "count": "{{count}} mesaje", + "expanded": { + "payload": "Conținut" + }, + "count_one": "{{count}} mesaj", + "count_other": "{{count}} mesaje" + } + }, + "metrics": "Metrici sistem", + "enrichments": { + "title": "Procesări Avansate", + "embeddings": { + "image_embedding": "Înglobări de imagini", + "text_embedding": "Înglobări de text", + "plate_recognition": "Recunoaștere Numere Înmatriculare", + "image_embedding_speed": "Viteză înglobări de imagini", + "face_recognition": "Recunoaștere Facială", + "face_recognition_speed": "Viteză Recunoaștere Facială", + "plate_recognition_speed": "Viteză Recunoaștere Numere", + "face_embedding_speed": "Viteză înglobări faciale", + "yolov9_plate_detection_speed": "Viteză Detecție Numere YOLOv9", + "text_embedding_speed": "Viteză înglobări de text", + "yolov9_plate_detection": "Detecție Numere YOLOv9", + "review_description": "Descriere revizuire", + "review_description_speed": "Viteză descriere revizuire", + "review_description_events_per_second": "Descriere revizuire", + "object_description": "Descriere Obiect", + "object_description_speed": "Viteză Descriere Obiect", + "object_description_events_per_second": "Descriere Obiect", + "classification": "Clasificare {{name}}", + "classification_speed": "Viteză Clasificare {{name}}", + "classification_events_per_second": "Evenimente Clasificare {{name}} pe secundă" + }, + "infPerSecond": "Inferențe pe secundă", + "averageInf": "Timp mediu de inferență" + }, + "cameras": { + "info": { + "codec": "Codec:", + "resolution": "Rezoluție:", + "cameraProbeInfo": "Info Sondă Cameră {{camera}}", + "streamDataFromFFPROBE": "Datele stream-ului sunt obținute cu ffprobe.", + "aspectRatio": "raport aspect", + "fetching": "Se preiau datele camerei", + "stream": "Stream {{idx}}", + "video": "Video:", + "audio": "Audio:", + "error": "Eroare: {{error}}", + "tips": { + "title": "Info Sondă Cameră" + }, + "fps": "FPS:", + "unknown": "Necunoscut", + "keyframes": { + "title": "Analiză keyframe", + "analyzing": "Se analizează keyframe-urile... {{seconds}} secunde rămase", + "stillAnalyzing": "Încă se analizează keyframe-urile...", + "keyframeCount": "Keyframe-uri observate:", + "recordStream": "Stream de înregistrare:", + "observedDuration": "Durată observată:", + "gap": "Interval keyframe (min / med / max):", + "ok": "Keyframe-uri la fiecare ~{{seconds}}s, bune pentru înregistrare și redare.", + "segmentLength": "Lungime segment de înregistrare:", + "warning": "Keyframe-uri rare sau variabile (cel mai lung interval ~{{seconds}}s), probabil un codec smart (H.264+/H.265+), acest lucru nu este recomandat.", + "error": "Intervalul keyframe (~{{seconds}}s) depășește lungimea segmentului de înregistrare ({{segmentTime}}s). Unele segmente pot să nu aibă niciun keyframe, ceea ce întrerupe redarea. Dezactivează codecul smart/+ de pe cameră sau scurtează intervalul keyframe.", + "unknown": "Nu s-a putut determina distanțarea keyframe-urilor.", + "recordDisabled": "Înregistrarea este dezactivată pentru această cameră." + } + }, + "label": { + "capture": "captură", + "skipped": "sărit", + "overallSkippedDetectionsPerSecond": "detecții sărite pe secundă total", + "cameraCapture": "captură {{camName}}", + "cameraDetect": "detecție {{camName}}", + "cameraFramesPerSecond": "cadre pe secundă {{camName}}", + "cameraDetectionsPerSecond": "detecții pe secundă {{camName}}", + "cameraSkippedDetectionsPerSecond": "detecții sărite pe secundă {{camName}}", + "overallFramesPerSecond": "cadre pe secundă total", + "overallDetectionsPerSecond": "detecții pe secundă total", + "detect": "detectează", + "cameraFfmpeg": "FFmpeg {{camName}}", + "camera": "cameră", + "ffmpeg": "FFmpeg", + "cameraGpu": "{{camName}} GPU" + }, + "title": "Camere", + "overview": "Prezentare generală", + "framesAndDetections": "Cadre / Detecții", + "toast": { + "success": { + "copyToClipboard": "Am copiat datele sondei în clipboard." + }, + "error": { + "unableToProbeCamera": "Nu s-a putut sonda camera: {{errorMessage}}" + } + }, + "connectionQuality": { + "title": "Calitate Conexiune", + "excellent": "Excelentă", + "fair": "Acceptabilă", + "poor": "Slabă", + "unusable": "Inutilizabilă", + "fps": "FPS", + "expectedFps": "FPS așteptat", + "reconnectsLastHour": "Reconectări (ultima oră)", + "stallsLastHour": "Blocaje (ultima oră)" + }, + "noCameras": { + "title": "Nicio cameră găsită" + } + }, + "stats": { + "reindexingEmbeddings": "Se reindexează înglobările ({{processed}}% gata)", + "detectIsVerySlow": "{{detect}} este foarte lent ({{speed}} ms)", + "detectIsSlow": "{{detect}} este lent ({{speed}} ms)", + "detectHighCpuUsage": "{{camera}} are o utilizare ridicată CPU detecție ({{detectAvg}}%)", + "ffmpegHighCpuUsage": "{{camera}} are o utilizare ridicată CPU FFmpeg ({{ffmpegAvg}}%)", + "cameraIsOffline": "{{camera}} este offline", + "healthy": "Sistemul este sănătos", + "shmTooLow": "Alocarea /dev/shm ({{total}} MB) ar trebui mărită la cel puțin {{min}} MB.", + "debugReplayActive": "Sesiunea de reluare de depanare este activă" + }, + "lastRefreshed": "Ultima actualizare: " +} diff --git a/web/public/locales/ru/audio.json b/web/public/locales/ru/audio.json new file mode 100644 index 0000000..e9e6bfc --- /dev/null +++ b/web/public/locales/ru/audio.json @@ -0,0 +1,503 @@ +{ + "grunt": "Хрюк", + "babbling": "Бормотание", + "laughter": "Смех", + "choir": "Хор", + "mantra": "Мантра", + "speech": "Речь", + "child_singing": "Детское пение", + "whistling": "Свист", + "breathing": "Дыхание", + "synthetic_singing": "Синтетическое пение", + "rapping": "Рэп", + "yell": "Крик", + "humming": "Гудение", + "groan": "Стон", + "bellow": "Рёв", + "whispering": "Шёпот", + "whoop": "Возглас", + "crying": "Плач", + "yodeling": "Йодль (пение)", + "snicker": "Смешок", + "sigh": "Сигнал", + "singing": "Пение", + "wheeze": "Хрип", + "snoring": "Храп", + "gasp": "Вздох", + "pant": "Пыхтение", + "snort": "Фырканье", + "sniff": "Нюхание", + "burping": "Отрыжка", + "cough": "Кашель", + "run": "Бег", + "throat_clearing": "Прочистка горла", + "sneeze": "Чихание", + "shuffle": "Шаркание", + "chewing": "Жевание", + "biting": "Кусание", + "gargling": "Полоскание горла", + "stomach_rumble": "Урчание живота", + "hiccup": "Икание", + "fart": "Пукание", + "footsteps": "Шаги", + "chant": "Песнопение", + "hands": "Руки", + "finger_snapping": "Щелкать пальцами", + "clapping": "Хлопать", + "moo": "Мычание", + "cowbell": "Коровий колокольчик", + "heart_murmur": "Сердечный шум", + "cheering": "Ликование", + "applause": "Аплодисменты", + "chatter": "Болтовня", + "crowd": "Толпа", + "children_playing": "Игра детей", + "animal": "Животное", + "pets": "Домашние животные", + "dog": "Собака", + "bark": "Лай", + "yip": "Тявканье", + "howl": "Вой", + "whimper_dog": "Собачий скулеж", + "cat": "Кошка", + "purr": "Мурлыканье", + "meow": "Мяуканье", + "hiss": "Шипение", + "growling": "Рычание", + "bow_wow": "Гавканье", + "heartbeat": "Сердцебиение", + "caterwaul": "Кошачий вой", + "horse": "Лошадь", + "clip_clop": "Цоканье", + "neigh": "Ржание", + "livestock": "Скот", + "cattle": "Крупный рогатый скот", + "pig": "Свинья", + "oink": "Хрюканье", + "bleat": "Блеяние", + "sheep": "Овца", + "fowl": "Домашняя птица", + "goat": "Коза", + "chicken": "Курица", + "cluck": "Кудахтанье", + "cock_a_doodle_doo": "Кукареканье", + "turkey": "Индейка", + "gobble": "Бормотание индейки", + "duck": "Утка", + "quack": "Кряканье", + "goose": "Гусь", + "honk": "Гоготание", + "wild_animals": "Дикие животные", + "roaring_cats": "Рычащие кошки", + "roar": "Рык", + "chirp": "Чириканье", + "squawk": "Птичий крик", + "pigeon": "Голубь", + "coo": "Воркование", + "crow": "Ворона", + "caw": "Карканье", + "owl": "Сова", + "hoot": "Уханье", + "flapping_wings": "Хлопание крыльев", + "dogs": "Собаки", + "rats": "Крысы", + "mouse": "Мышь", + "insect": "Насекомое", + "cricket": "Сверчок", + "mosquito": "Комар", + "fly": "Муха", + "buzz": "Жужжание", + "frog": "Лягушка", + "croak": "Кваканье", + "snake": "Змея", + "rattle": "Треск", + "music": "Музыка", + "musical_instrument": "Музыкальный инструмент", + "whale_vocalization": "Пение кита", + "plucked_string_instrument": "Щипковый струнный инструмент", + "guitar": "Гитара", + "patter": "Шорох", + "bass_guitar": "Бас-гитара", + "steel_guitar": "Стальная гитара", + "tapping": "Постукивание", + "car": "Автомобиль", + "motorcycle": "Мотоцикл", + "bicycle": "Велосипед", + "bird": "Птица", + "electric_guitar": "Электрогитара", + "acoustic_guitar": "Акустическая гитара", + "scream": "Крик", + "strum": "Звук струн", + "banjo": "Банджо", + "zither": "Цитра", + "ukulele": "Укулеле", + "keyboard": "Клавиатура", + "electric_piano": "Электропианино", + "organ": "Орган", + "electronic_organ": "Электроорган", + "synthesizer": "Синтезатор", + "hammond_organ": "Орган Хаммонда", + "sampler": "Сэмплер", + "harpsichord": "Клавесин", + "percussion": "Ударные инструменты", + "drum_kit": "Ударная установка", + "drum_machine": "Драммашина", + "drum": "Барабан", + "snare_drum": "Малый барабан", + "rimshot": "Обод барабана", + "drum_roll": "Барабанная дробь", + "bass_drum": "Бас-барабан", + "timpani": "Литавры", + "tabla": "Табла", + "cymbal": "Тарелка", + "hi_hat": "Хай-хэт", + "wood_block": "Вуд-блок", + "tambourine": "Бубен", + "maraca": "Маракас", + "gong": "Гонг", + "tubular_bells": "Трубчатые колокола", + "mallet_percussion": "Маллет-перкуссия", + "marimba": "Маримба", + "glockenspiel": "Колокольчики", + "vibraphone": "Вибрафон", + "steelpan": "Стальной барабан", + "orchestra": "Оркестр", + "brass_instrument": "Медный духовой инструмент", + "french_horn": "Валторна", + "trumpet": "Труба", + "trombone": "Тромбон", + "bowed_string_instrument": "Смычковый струнный инструмент", + "string_section": "Струнная секция", + "mandolin": "Мандолина", + "piano": "Пианино", + "sitar": "Ситар", + "violin": "Скрипка", + "pizzicato": "Пиццикато", + "cello": "Виолончель", + "double_bass": "Контрабас", + "wind_instrument": "Духовой инструмент", + "flute": "Флейта", + "saxophone": "Саксофон", + "clarinet": "Кларнет", + "harp": "Арфа", + "bell": "Колокол", + "church_bell": "Церковный колокол", + "jingle_bell": "Бубенчик", + "bicycle_bell": "Велосипедный звонок", + "tuning_fork": "Камертон", + "chime": "Колокольчик", + "wind_chime": "Музыка ветра", + "harmonica": "Губная гармошка", + "accordion": "Аккордеон", + "bagpipes": "Волынка", + "didgeridoo": "Диджериду", + "theremin": "Терменвокс", + "singing_bowl": "Поющая чаша", + "scratching": "Скрэтчинг", + "pop_music": "Поп-музыка", + "hip_hop_music": "Хип-хоп", + "beatboxing": "Битбоксинг", + "rock_music": "Рок-музыка", + "heavy_metal": "Хеви-метал", + "punk_rock": "Панк-рок", + "grunge": "Гранж", + "progressive_rock": "Прогрессив-рок", + "rock_and_roll": "Рок-н-ролл", + "psychedelic_rock": "Психоделический рок", + "rhythm_and_blues": "Ритм-н-блюз", + "soul_music": "Соул", + "bluegrass": "Блюграсс", + "funk": "Фанк", + "middle_eastern_music": "Ближневосточная музыка", + "jazz": "Джаз", + "disco": "Диско", + "classical_music": "Классическая музыка", + "opera": "Опера", + "house_music": "Хаус", + "techno": "Техно", + "dubstep": "Дабстеп", + "drum_and_bass": "Драм-н-бейс", + "electronica": "Электроника", + "electronic_dance_music": "Электронная танцевальная музыка", + "ambient_music": "Эмбиент", + "music_of_latin_america": "Латиноамериканская музыка", + "salsa_music": "Сальса", + "flamenco": "Фламенко", + "blues": "Блюз", + "music_for_children": "Детская музыка", + "new-age_music": "Нью-эйдж", + "a_capella": "А капелла", + "music_of_africa": "Африканская музыка", + "afrobeat": "Афробит", + "christian_music": "Христианская музыка", + "gospel_music": "Госпел", + "music_of_asia": "Азиатская музыка", + "carnatic_music": "Карнатическая музыка", + "music_of_bollywood": "Музыка Болливуда", + "ska": "Ска", + "traditional_music": "Традиционная музыка", + "independent_music": "Инди", + "song": "Песня", + "background_music": "Фоновая музыка", + "theme_music": "Тематическая музыка", + "jingle": "Джингл", + "soundtrack_music": "Саундтрек", + "lullaby": "Колыбельная", + "video_game_music": "Музыка из видеоигр", + "christmas_music": "Рождественская музыка", + "dance_music": "Танцевальная музыка", + "wedding_music": "Свадебная музыка", + "happy_music": "Весёлая музыка", + "sad_music": "Грустная музыка", + "tender_music": "Нежная музыка", + "exciting_music": "Энергичная музыка", + "angry_music": "Агрессивная музыка", + "scary_music": "Жуткая музыка", + "wind": "Ветер", + "rustling_leaves": "Шуршание листьев", + "wind_noise": "Шум ветра", + "thunderstorm": "Гроза", + "thunder": "Гром", + "water": "Вода", + "rain": "Дождь", + "raindrop": "Капли дождя", + "rain_on_surface": "Дождь на поверхности", + "stream": "Поток", + "waterfall": "Водопад", + "gurgling": "Журчание", + "fire": "Огонь", + "crackle": "Потрескивание", + "vehicle": "Транспорт", + "boat": "Лодка", + "sailboat": "Парусник", + "rowboat": "Вёсельная лодка", + "motorboat": "Моторная лодка", + "ship": "Корабль", + "motor_vehicle": "Моторный транспорт", + "power_windows": "Электростеклоподъемники", + "skidding": "Занос", + "tire_squeal": "Визг шин", + "car_passing_by": "Проезжающая машина", + "race_car": "Гоночный автомобиль", + "truck": "Грузовик", + "air_brake": "Пневматический тормоз", + "air_horn": "Пневматический гудок", + "reversing_beeps": "Сигнал заднего хода", + "ice_cream_truck": "Грузовик с мороженым", + "bus": "Автобус", + "emergency_vehicle": "Транспорт экстренных служб", + "police_car": "Полицейский автомобиль", + "fire_engine": "Пожарная машина", + "rail_transport": "Рельсовый транспорт", + "train": "Поезд", + "train_whistle": "Свисток поезда", + "train_horn": "Гудок поезда", + "railroad_car": "Железнодорожный вагон", + "train_wheels_squealing": "Визг колес поезда", + "subway": "Метро", + "aircraft": "Воздушное судно", + "aircraft_engine": "Двигатель воздушного судна", + "jet_engine": "Реактивный двигатель", + "propeller": "Пропеллер", + "fixed-wing_aircraft": "Самолет с неподвижным крылом", + "skateboard": "Скейтборд", + "engine": "Двигатель", + "light_engine": "Легкий двигатель", + "dental_drill's_drill": "Стоматологическая бормашина", + "medium_engine": "Средний двигатель", + "heavy_engine": "Тяжёлый двигатель", + "engine_knocking": "Детонация двигателя", + "engine_starting": "Запуск двигателя", + "idling": "Холостой ход", + "accelerating": "Ускорение", + "ding-dong": "Дин-дон", + "sliding_door": "Раздвижная дверь", + "slam": "Хлопок", + "knock": "Стук", + "tap": "Небольшой стук", + "squeak": "Писк", + "cupboard_open_or_close": "Открытие или закрытие шкафа", + "drawer_open_or_close": "Открытие или закрытие ящика", + "dishes": "Тарелки", + "cutlery": "Столовые приборы", + "chopping": "Нарезание", + "frying": "Жарка", + "microwave_oven": "Микроволновка", + "blender": "Блендер", + "water_tap": "Водопроводный кран", + "sink": "Раковина", + "bathtub": "Ванна", + "hair_dryer": "Фен", + "toilet_flush": "Слив унитаза", + "toothbrush": "Зубная щетка", + "zipper": "Молния на одежде", + "keys_jangling": "Бряканье ключей", + "coin": "Монета", + "scissors": "Ножницы", + "electric_shaver": "Электробритва", + "shuffling_cards": "Тасование карт", + "typing": "Печатание", + "typewriter": "Печатная машинка", + "computer_keyboard": "Компьютерная клавиатура", + "writing": "Письмо", + "alarm": "Сигнализация", + "telephone": "Телефон", + "telephone_bell_ringing": "Звонок телефона", + "ringtone": "Рингтон", + "telephone_dialing": "Набор телефонного номера", + "dial_tone": "Телефонный гудок", + "busy_signal": "Сигнал занято", + "alarm_clock": "Будильник", + "siren": "Сирена", + "civil_defense_siren": "Сирена гражданской обороны", + "foghorn": "Туманный горн", + "whistle": "Свисток", + "steam_whistle": "Паровой свисток", + "mechanisms": "Механизмы", + "clock": "Часы", + "tick": "Тик", + "tick-tock": "Тик-так", + "gears": "Шестерни", + "pulleys": "Шкивы", + "sewing_machine": "Швейная машинка", + "mechanical_fan": "Механический вентилятор", + "printer": "Принтер", + "camera": "Камера", + "single-lens_reflex_camera": "Зеркальная камера", + "tools": "Инструменты", + "sawing": "Распиловка", + "filing": "Звук напильника", + "sanding": "Шлифовка", + "power_tool": "Электроинструмент", + "drill": "Дрель", + "explosion": "Взрыв", + "gunshot": "Выстрел", + "machine_gun": "Автомат", + "fusillade": "Оружейная очередь", + "artillery_fire": "Артиллерийский огонь", + "burst": "Очередь выстрелов", + "eruption": "Извержение", + "boom": "Бум", + "wood": "Дерево", + "chop": "Рубка", + "splinter": "Щепка", + "glass": "Стекло", + "crack": "Трещина", + "chink": "Звон", + "shatter": "Разбитие", + "silence": "Тишина", + "sound_effect": "Звуковой эффект", + "environmental_noise": "Шум окружающей среды", + "static": "Статический шум", + "field_recording": "Полевая запись", + "country": "Кантри", + "vocal_music": "Вокальная музыка", + "electronic_music": "Электронная музыка", + "folk_music": "Фолк-музыка", + "trance_music": "Транс", + "swing_music": "Свинг", + "reggae": "Регги", + "waves": "Волны", + "ambulance": "Скорая помощь", + "helicopter": "Вертолет", + "radio": "Радио", + "lawn_mower": "Газонокосилка", + "electric_toothbrush": "Электрическая зубная щетка", + "air_conditioning": "Кондиционер", + "toot": "Гудок", + "traffic_noise": "Дорожный шум", + "ocean": "Океан", + "steam": "Пар", + "car_alarm": "Автомобильная сигнализация", + "buzzer": "Зуммер", + "chainsaw": "Цепная пила", + "door": "Дверь", + "doorbell": "Дверной звонок", + "smoke_detector": "Датчик дыма", + "white_noise": "Белый шум", + "cash_register": "Касса", + "vacuum_cleaner": "Пылесос", + "fire_alarm": "Пожарная сигнализация", + "ratchet": "Трещотка", + "cap_gun": "Игрушечный пистолет", + "fireworks": "Фейерверк", + "jackhammer": "Отбойный молоток", + "pink_noise": "Розовый шум", + "hammer": "Молоток", + "firecracker": "Петарда", + "television": "Телевидение", + "echo": "Эхо", + "noise": "Шум", + "mains_hum": "Гул сети", + "cacophony": "Какофония", + "throbbing": "Пульсирующий", + "vibration": "Вибрация", + "sodeling": "Соделинг", + "chird": "Чирд", + "change_ringing": "Перезвон", + "shofar": "Шофар", + "liquid": "Жидкость", + "splash": "Брызги", + "slosh": "Плеск", + "squish": "Хлюпанье", + "drip": "Капля", + "pour": "Литьё", + "trickle": "Струйка", + "gush": "Бурный поток", + "fill": "Наполнение", + "spray": "Распыление", + "pump": "Насос", + "stir": "Перемешивание", + "boiling": "Кипение", + "sonar": "Сонар", + "arrow": "Стрела", + "whoosh": "Вжух", + "thump": "Глухой удар", + "thunk": "Тупой удар", + "electronic_tuner": "Электронный тюнер", + "effects_unit": "Блок эффектов", + "chorus_effect": "Эффект хоруса", + "basketball_bounce": "Отскок баскетбольного мяча", + "bang": "Бах", + "slap": "Шлепок", + "whack": "Удар", + "smash": "Разбивание", + "breaking": "Разрушение", + "bouncing": "Отскок", + "whip": "Хлыст", + "flap": "Хлопание", + "scratch": "Царапанье", + "scrape": "Скребок", + "rub": "Трение", + "roll": "Качение", + "crushing": "Дробление", + "crumpling": "Сминание", + "tearing": "Разрывание", + "beep": "Бип", + "ping": "Пинг", + "ding": "Динь", + "clang": "Лязг", + "squeal": "Визг", + "creak": "Скрипение", + "rustle": "Шуршание", + "whir": "Жужжание", + "clatter": "Грохот", + "sizzle": "Шипение", + "clicking": "Щелканье", + "clickety_clack": "Щелчок-Клак", + "rumble": "Грохотать", + "plop": "Плюх", + "hum": "Гул", + "zing": "Зинг", + "boing": "Боинг", + "crunch": "Хруст", + "sine_wave": "Синусоида", + "harmonic": "Гармоника", + "chirp_tone": "Тон чириканья", + "pulse": "Импульс", + "inside": "Внутри", + "outside": "Снаружи", + "reverberation": "Реверберация", + "distortion": "Искажение", + "sidetone": "Боковой тон" +} diff --git a/web/public/locales/ru/common.json b/web/public/locales/ru/common.json new file mode 100644 index 0000000..5e27bc1 --- /dev/null +++ b/web/public/locales/ru/common.json @@ -0,0 +1,341 @@ +{ + "time": { + "untilForTime": "До {{time}}", + "untilForRestart": "До перезапуска Frigate.", + "untilRestart": "До перезапуска", + "ago": "{{timeAgo}} назад", + "justNow": "Только что", + "today": "Сегодня", + "yesterday": "Вчера", + "thisWeek": "На этой неделе", + "last14": "Последние 14 дней", + "last30": "Последние 30 дней", + "last7": "Последние 7 дней", + "thisMonth": "В этом месяце", + "5minutes": "5 минут", + "30minutes": "30 минут", + "1hour": "1 час", + "12hours": "12 часов", + "24hours": "24 часа", + "pm": "pm", + "am": "am", + "yr": "{{time}} л", + "year_one": "{{time}} год", + "year_few": "{{time}} года", + "year_many": "{{time}} лет", + "mo": "{{time}} мес", + "month_one": "{{time}} месяц", + "month_few": "{{time}} месяца", + "month_many": "{{time}} месяцев", + "d": "{{time}} д", + "h": "{{time}} ч", + "hour_one": "{{time}} час", + "hour_few": "{{time}} часа", + "hour_many": "{{time}} часов", + "m": "{{time}} мин", + "minute_one": "{{time}} минута", + "minute_few": "{{time}} минуты", + "minute_many": "{{time}} минут", + "day_one": "{{time}} день", + "day_few": "{{time}} дня", + "day_many": "{{time}} дней", + "lastWeek": "На прошлой неделе", + "lastMonth": "В прошлом месяце", + "10minutes": "10 минут", + "s": "{{time}} с", + "second_one": "{{time}} секунда", + "second_few": "{{time}} секунды", + "second_many": "{{time}} секунд", + "formattedTimestampExcludeSeconds": { + "24hour": "%b %-d, %H:%M", + "12hour": "%b %-d, %I:%M %p" + }, + "formattedTimestampWithYear": { + "24hour": "%b %-d %Y, %H:%M", + "12hour": "%b %-d %Y, %I:%M %p" + }, + "formattedTimestamp2": { + "24hour": "d MMM HH:mm:ss", + "12hour": "dd/MM h:mm:ssa" + }, + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampFilename": { + "24hour": "dd-MM-yy-HH-mm-ss", + "12hour": "dd-MM-yy-h-mm-ss-a" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "inProgress": "В процессе", + "invalidStartTime": "Некорректное время начала", + "invalidEndTime": "Некорректное время окончания", + "never": "Никогда" + }, + "selectItem": "Выбрать {{item}}", + "button": { + "apply": "Применить", + "done": "Готово", + "enabled": "Включено", + "enable": "Включить", + "save": "Сохранить", + "saving": "Сохранение…", + "fullscreen": "Полноэкранный режим", + "pictureInPicture": "Картинка в картинке", + "twoWayTalk": "Двусторонняя связь", + "cameraAudio": "Аудио с камеры", + "on": "Вкл", + "edit": "Редактировать", + "copyCoordinates": "Скопировать координаты", + "delete": "Удалить", + "yes": "Да", + "no": "Нет", + "download": "Загрузить", + "info": "Информация", + "suspended": "Приостановлено", + "cancel": "Отменить", + "disable": "Отключить", + "reset": "Сбросить", + "disabled": "Отключено", + "close": "Закрыть", + "copy": "Скопировать", + "back": "Назад", + "history": "История", + "off": "Выкл", + "exitFullscreen": "Выйти из полноэкранного режима", + "unsuspended": "Возобновить", + "play": "Воспроизвести", + "unselect": "Снять выбор", + "export": "Экспортировать", + "deleteNow": "Удалить сейчас", + "next": "Следующий", + "continue": "Продолжить", + "add": "Добавить", + "applying": "Применяется…", + "undo": "Отменить", + "copiedToClipboard": "Скопировано в буфер обмена", + "saveAll": "Сохранить все", + "savingAll": "Сохранение всего…", + "undoAll": "Откатить всё", + "retry": "Повторить", + "modified": "Изменено", + "overridden": "Переопределено", + "resetToGlobal": "Сброс к Глобальному значению", + "resetToDefault": "Сброс к значению по умолчанию" + }, + "label": { + "back": "Вернуться", + "hide": "Скрыть {{item}}", + "show": "Показать {{item}}", + "ID": "ID", + "all": "Все", + "none": "Ничего", + "other": "Другой" + }, + "unit": { + "speed": { + "kph": "км/ч", + "mph": "миль/ч" + }, + "length": { + "meters": "метры", + "feet": "футы" + }, + "data": { + "kbps": "кБ/с", + "mbps": "МБ/с", + "gbps": "ГБ/с", + "kbph": "кБ/час", + "mbph": "МБ/час", + "gbph": "ГБ/час" + } + }, + "menu": { + "configuration": "Конфигурация", + "systemLogs": "Логи системы", + "settings": "Настройки", + "configurationEditor": "Редактор конфигурации", + "system": "Система", + "systemMetrics": "Метрики системы", + "languages": "Языки", + "language": { + "en": "English (Английский)", + "zhCN": "简体中文 (Упрощённый Китайский)", + "es": "Español (Испанский)", + "hi": "हिन्दी (Хинди)", + "fr": "Français (Французский)", + "ar": "العربية (Арабский)", + "pt": "Português (Португальский)", + "ru": "Русский", + "tr": "Türkçe (Турецкий)", + "nl": "Nederlands (Нидерландский)", + "cs": "Čeština (Чешский)", + "nb": "Norsk Bokmål (Норвежский (букмол))", + "vi": "Tiếng Việt (Вьетнамский)", + "fa": "فارسی (Фарси)", + "pl": "Polski (Польский)", + "uk": "Українська (Украинский)", + "el": "Ελληνικά (Греческий)", + "da": "Dansk (Датский)", + "sk": "Slovenčina (Словацкий)", + "sv": "Svenska (Шведский)", + "hu": "Magyar (Венгерский)", + "fi": "Suomi (Финский)", + "ro": "Română (Румынский)", + "ja": "日本語 (Японский)", + "it": "Italiano (Итальянский)", + "de": "Deutsch (Немецкий)", + "ko": "한국어 (Корейский)", + "he": "עברית (Иврит)", + "withSystem": { + "label": "Использовать системные настройки языка" + }, + "yue": "粵語 (Кантонский)", + "th": "ไทย (Тайский)", + "ca": "Català (Каталонский)", + "ptBR": "Português brasileiro (Бразильский португальский)", + "sr": "Српски (Сербский)", + "sl": "Slovenščina (Словенский)", + "lt": "Lietuvių (Литовский)", + "bg": "Български (Болгарский)", + "gl": "Galego (Галисийский)", + "id": "Bahasa Indonesia (Индонезийский)", + "ur": "اردو (Урду)", + "zhHant": "繁體中文 (Традиционный Китайский)", + "hr": "Hrvatski (Хорватский)", + "bs": "Bosanski (Боснийский)" + }, + "darkMode": { + "withSystem": { + "label": "Использовать системные настройки светлой/тёмной темы" + }, + "label": "Тёмный режим", + "light": "Светлый", + "dark": "Тёмный" + }, + "withSystem": "Системный", + "theme": { + "label": "Тема", + "blue": "Синяя", + "default": "По умолчанию", + "green": "Зелёная", + "nord": "Северная", + "red": "Красная", + "contrast": "Высокий контраст", + "highcontrast": "Контрастная" + }, + "help": "Помощь", + "documentation": { + "title": "Документация", + "label": "Документация по Frigate" + }, + "explore": "Поиск событий", + "restart": "Перезапуск Frigate", + "live": { + "title": "Прямой эфир", + "allCameras": "Все камеры", + "cameras": { + "count_one": "{{count}} камера", + "count_few": "{{count}} камеры", + "count_many": "{{count}} камер", + "title": "Камеры" + } + }, + "review": "Обзор событий", + "export": "Экспортировать", + "uiPlayground": "Среда тестирования интерфейсов", + "faceLibrary": "Библиотека лиц", + "user": { + "title": "Пользователь", + "account": "Аккаунт", + "current": "Текущий пользователь: {{user}}", + "anonymous": "anonymous", + "logout": "Выход", + "setPassword": "Установить пароль" + }, + "appearance": "Внешний вид", + "classification": "Распознование", + "profiles": "Профили", + "chat": "Чат", + "actions": "Действия", + "features": "Функции" + }, + "pagination": { + "label": "пагинация", + "previous": { + "title": "Предыдущая", + "label": "Переход на предыдущую страницу" + }, + "next": { + "title": "Следующая", + "label": "Переход на следующую страницу" + }, + "more": "Больше страниц" + }, + "accessDenied": { + "desc": "У вас нет разрешения на просмотр этой страницы.", + "documentTitle": "Доступ запрещён - Frigate", + "title": "Доступ запрещён" + }, + "notFound": { + "desc": "Страница не найдена", + "documentTitle": "Не найдена - Frigate", + "title": "404" + }, + "toast": { + "copyUrlToClipboard": "URL скопирован в буфер обмена.", + "save": { + "error": { + "noMessage": "Не удалось сохранить изменения конфигурации", + "title": "Не удалось сохранить изменения конфигурации: {{errorMessage}}" + }, + "title": "Сохранить", + "success": "Изменения конфигурации сохранены успешно." + } + }, + "role": { + "title": "Роль", + "admin": "Администратор", + "viewer": "Наблюдатель", + "desc": "Администраторы имеют полный доступ ко всем функциям в интерфейсе Frigate. Наблюдатели ограничены просмотром камер, элементов просмотра и архивных записей." + }, + "readTheDocumentation": "Читать документацию", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} и {{1}}", + "many": "{{items}}, и {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Необязательный", + "internalID": "Внутренний идентификатор Frigate, используемый в конфигурации и базе данных" + }, + "validation_errors": "Ошибки Валидации", + "no_items": "Нет элементов", + "credentialField": { + "savedPlaceholder": "Сохранено — оставьте пустым, чтобы сохранить текущее значение" + } +} diff --git a/web/public/locales/ru/components/auth.json b/web/public/locales/ru/components/auth.json new file mode 100644 index 0000000..17b9839 --- /dev/null +++ b/web/public/locales/ru/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Имя пользователя", + "password": "Пароль", + "login": "Логин", + "errors": { + "usernameRequired": "Необходимо ввести имя пользователя", + "passwordRequired": "Необходимо ввести пароль", + "rateLimit": "Превышение числа попыток. Попробуй еще раз позже.", + "loginFailed": "Ошибка входа", + "unknownError": "Неизвестная ошибка. Проверьте логи.", + "webUnknownError": "Неизвестная ошибка. Проверьте логи консоли." + }, + "firstTimeLogin": "Пытаетесь войти в систему впервые? Учетные данные указаны в логах Frigate." + } +} diff --git a/web/public/locales/ru/components/camera.json b/web/public/locales/ru/components/camera.json new file mode 100644 index 0000000..3ff007c --- /dev/null +++ b/web/public/locales/ru/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "Группы камер", + "add": "Добавить группу камер", + "edit": "Редактировать группу камер", + "delete": { + "label": "Удалить группу камер", + "confirm": { + "title": "Подтвердить удаление", + "desc": "Вы уверены, что хотите удалить группу камер {{name}}?" + } + }, + "name": { + "label": "Название", + "placeholder": "Введите название…", + "errorMessage": { + "exists": "Такое название группы камер уже существует.", + "nameMustNotPeriod": "Название группы камер не должно содержать точки.", + "invalid": "Неверное название группы камер.", + "mustLeastCharacters": "Название группы камер должно содержать не менее 2 символов." + } + }, + "cameras": { + "label": "Камеры", + "desc": "Выберите камеры для этой группы." + }, + "icon": "Иконка", + "success": "Группа камер {{name}} сохранена.", + "camera": { + "setting": { + "label": "Настройки видеопотока", + "desc": "Изменение параметров прямой трансляции для панели этой группы камер. Эти настройки зависят от устройства/браузера.", + "audioIsAvailable": "Для этого потока доступен звук", + "audioIsUnavailable": "Для этого потока звук недоступен", + "audio": { + "tips": { + "title": "Аудио должно выводиться с вашей камеры и быть настроено в go2rtc для этого потока.", + "document": "Читать документацию " + } + }, + "streamMethod": { + "label": "Метод стриминга", + "method": { + "noStreaming": { + "label": "Нет потока", + "desc": "Кадры с камеры обновляются раз в минуту, без прямой трансляции." + }, + "smartStreaming": { + "label": "Умный поток (рекомендуется)", + "desc": "Для экономии ресурсов поток обновляется раз в минуту. При обнаружении активности автоматически активируется прямая трансляция." + }, + "continuousStreaming": { + "label": "Непрерывный поток", + "desc": { + "warning": "Непрерывная потоковая передача может привести к высокому потреблению трафика и проблемам с производительностью. Используйте с осторожностью.", + "title": "Когда изображение выводится на панель, оно всегда обновляется в режиме реального времени, вне зависимости от обнаружения активности." + } + } + }, + "placeholder": "Выберите способ потоковой передачи" + }, + "compatibilityMode": { + "label": "Режим совместимости", + "desc": "Активируйте эту настройку только при появлении цветовых искажений или диагональной полосы с правого края в прямой трансляции." + }, + "title": "Настройки видеопотока {{cameraName}}", + "stream": "Поток", + "placeholder": "Выбрать поток" + }, + "birdseye": "Birdseye" + }, + "showAll": "Показать все группы камер", + "showLess": "Показать меньше", + "editGroups": "Редактировать группы камер" + }, + "debug": { + "options": { + "label": "Настройки", + "title": "Опции", + "hideOptions": "Скрыть опции", + "showOptions": "Показать опции" + }, + "boundingBox": "Ограничивающая рамка", + "timestamp": "Метка времени", + "zones": "Зоны", + "mask": "Маска", + "motion": "Движение", + "regions": "Регионы", + "paths": "Пути" + } +} diff --git a/web/public/locales/ru/components/dialog.json b/web/public/locales/ru/components/dialog.json new file mode 100644 index 0000000..787fa1b --- /dev/null +++ b/web/public/locales/ru/components/dialog.json @@ -0,0 +1,180 @@ +{ + "restart": { + "title": "Вы уверены, что хотите перезапустить Frigate?", + "button": "Перезапуск", + "restarting": { + "title": "Frigate перезапускается", + "content": "Эта страница перезагрузится через {{countdown}} сек.", + "button": "Принудительная перезагрузка" + }, + "description": "Это перезагрузки перезагрузит Frigate." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Отправить в Frigate+", + "desc": "Объекты в местах, которых вы хотите избежать, не являются ложными срабатываниями. Отправка их как ложных срабатываний запутает модель." + }, + "review": { + "true": { + "label": "Подтвердите метку для Frigate Plus", + "true_one": "Это {{label}}", + "true_few": "Это {{label}}", + "true_many": "Это {{label}}" + }, + "false": { + "label": "Не подтверждать эту метку для Frigate Plus", + "false_one": "Это не {{label}}", + "false_few": "Это не {{label}}", + "false_many": "Это не {{label}}" + }, + "state": { + "submitted": "Отправлено" + }, + "question": { + "ask_an": "Это объект — {{label}} ?", + "label": "Подтвердить эту метку для Frigate Plus", + "ask_a": "Это объект — {{label}}?", + "ask_full": "Это объект — {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "toast": { + "error": "Не удалось подключиться к Frigate+. Пожалуйста, проверьте настройки сети и попробуйте еще раз." + } + } + }, + "video": { + "viewInHistory": "Посмотреть в истории" + } + }, + "export": { + "time": { + "fromTimeline": "Выбрать на таймлайне", + "custom": "Пользовательский", + "start": { + "title": "Время начала", + "label": "Выберите время начала" + }, + "end": { + "title": "Время окончания", + "label": "Выберите время окончания" + }, + "lastHour_one": "Последний час", + "lastHour_few": "Последние {{count}} часа", + "lastHour_many": "Последние {{count}} часов" + }, + "name": { + "placeholder": "Введите название для экспорта" + }, + "select": "Выбрать", + "export": "Экспорт", + "selectOrExport": "Выбрать или экспортировать", + "toast": { + "success": "Экспорт успешно запущен. Файл доступен на странице экспорта.", + "error": { + "failed": "Не удалось запустить экспорт: {{error}}", + "noVaildTimeSelected": "Не выбран допустимый временной диапазон", + "endTimeMustAfterStartTime": "Время окончания должно быть после времени начала" + }, + "view": "Просмотр", + "queued": "Экспорт поставлен в очередь. Следите за ходом выполнения на странице экспорта.", + "batchSuccess_one": "Начат {{count}} экспорт. Открываем случай.", + "batchSuccess_few": "Начато {{count}} экспорта. Открываем случай.", + "batchSuccess_many": "Начато {{count}} экспортов. Открываем случай.", + "batchPartial": "Начато {{successful}} из {{total}} экспортов. Неудавшиеся камеры: {{failedCameras}}", + "batchFailed": "Неудавшихся экспортов: {{total}}. Неудавшиеся камеры: {{failedCameras}}", + "batchQueuedSuccess_one": "1 экспорт поставлен в очередь. Открываем случай.", + "batchQueuedSuccess_few": "{{count}} экспорта поставлено в очередь. Открываем случай.", + "batchQueuedSuccess_many": "{{count}} экспортов поставлены в очередь. Открываем случай." + }, + "fromTimeline": { + "saveExport": "Сохранить экспорт", + "previewExport": "Предпросмотр экспорта" + }, + "case": { + "label": "Случай", + "placeholder": "Выберите случай", + "newCaseOption": "Создайте новый случай", + "newCaseNamePlaceholder": "Имя нового случая", + "newCaseDescriptionPlaceholder": "Описание Случая", + "nonAdminHelp": "Для этих экспортов будет создан новый случай." + }, + "queueing": "Помещаем Экспорт в очередь...", + "tabs": { + "export": "Одиночная Камера", + "multiCamera": "Мульти-Камера" + }, + "multiCamera": { + "timeRange": "Временной промежуток", + "selectFromTimeline": "Выбрать на таймлайне", + "cameraSelection": "Камеры", + "cameraSelectionHelp": "Камеры с отслеживаемыми объектами в этом временном диапазоне выбраны заранее", + "searchOrSelectGroup": "Найдите или выберите группу камер...", + "selectAll": "Выберите все камеры", + "clearSelection": "Очистить выбор", + "selectWithActivity": "Камеры с отслеживаемыми объектами", + "selectGroup": "Выбрать группу", + "noMatchingCameras": "Камеры по вашему запросу не найдены", + "selectedCount": "{{selected}} / {{total}} выбрано", + "checkingActivity": "Проверка доступности камеры...", + "noCameras": "Нет доступных камер", + "detectionCount_one": "{{count}} отслеживаемый объект", + "detectionCount_few": "{{count}} отслеживаемых объекта", + "detectionCount_many": "{{count}} отслеживаемых объектов" + } + }, + "streaming": { + "label": "Поток", + "restreaming": { + "disabled": "Рестриминг не включён для этой камеры.", + "desc": { + "title": "Настройте go2rtc для дополнительных вариантов просмотра в реальном времени и аудио для этой камеры.", + "readTheDocumentation": "Читать документацию" + } + }, + "debugView": "Режим отладки", + "showStats": { + "label": "Отображение статистики потока", + "desc": "Включите эту опцию, чтобы отображать статистику потока в виде наложения на изображение с камеры." + } + }, + "search": { + "saveSearch": { + "label": "Сохранить поиск", + "placeholder": "Введите название для вашего поиска", + "overwrite": "{{searchName}} уже существует. Сохранение перезапишет существующее значение.", + "success": "Поиск {{searchName}} был сохранен.", + "button": { + "save": { + "label": "Сохранить этот поиск" + } + }, + "desc": "Укажите название этого сохранённого поиска." + } + }, + "recording": { + "confirmDelete": { + "title": "Подтвердить удаление", + "desc": { + "selected": "Вы уверены, что хотите удалить все записанное видео, связанное с этим элементом просмотра?

    Удерживайте клавишу Shift, чтобы пропустить это окно в будущем." + }, + "toast": { + "error": "Не удалось удалить: {{error}}", + "success": "Видеоматериалы, связанные с выбранными предметами просмотра, были успешно удалены." + } + }, + "button": { + "export": "Экспорт", + "markAsReviewed": "Пометить как просмотренное", + "deleteNow": "Удалить сейчас", + "markAsUnreviewed": "Отметить как непросмотренное" + } + }, + "imagePicker": { + "search": { + "placeholder": "Искать по метке..." + }, + "selectImage": "Выбор миниатюры отслеживаемого объекта", + "noImages": "Не обнаружено миниатюр для этой камеры", + "unknownLabel": "Сохраненное изображение триггера" + } +} diff --git a/web/public/locales/ru/components/filter.json b/web/public/locales/ru/components/filter.json new file mode 100644 index 0000000..095ea91 --- /dev/null +++ b/web/public/locales/ru/components/filter.json @@ -0,0 +1,141 @@ +{ + "filter": "Фильтр", + "labels": { + "label": "Метки", + "all": { + "title": "Все метки", + "short": "Метки" + }, + "count": "{{count}} меток", + "count_one": "{{count}} Метка", + "count_other": "{{count}} меток" + }, + "zones": { + "all": { + "title": "Все зоны", + "short": "Зоны" + }, + "label": "Зоны" + }, + "dates": { + "all": { + "title": "Все даты", + "short": "Даты" + }, + "selectPreset": "Период…" + }, + "timeRange": "Временной диапазон", + "subLabels": { + "label": "Дополнительные метки", + "all": "Все дополнительные метки" + }, + "score": "Оценка", + "estimatedSpeed": "Расчетная скорость ({{unit}})", + "more": "Больше фильтров", + "reset": { + "label": "Сброс фильтров к значениям по умолчанию" + }, + "features": { + "hasSnapshot": "Есть снимок", + "hasVideoClip": "Есть видеоклип", + "submittedToFrigatePlus": { + "label": "Отправлено в Frigate+", + "tips": "Сначала необходимо отфильтровать отслеживаемые объекты, у которых есть снимок.

    Отслеживаемые объекты без снимка нельзя отправить в Frigate+." + }, + "label": "Функции" + }, + "sort": { + "speedAsc": "Расчетная скорость (по возрастанию)", + "speedDesc": "Расчетная скорость (по убыванию)", + "label": "Сортировка", + "dateAsc": "Дата (по возрастанию)", + "dateDesc": "Дата (по убыванию)", + "scoreAsc": "Оценка объекта (по возрастанию)", + "scoreDesc": "Оценка объекта (по убыванию)", + "relevance": "Релевантность" + }, + "cameras": { + "label": "Фильтр камер", + "all": { + "title": "Все камеры", + "short": "Камеры" + } + }, + "explore": { + "settings": { + "defaultView": { + "unfilteredGrid": "Нефильтрованная сетка", + "summary": "Сводка", + "title": "Вид по умолчанию", + "desc": "При отсутствии выбранных фильтров отображать сводку последних отслеживаемых объектов для каждой метки или показывать нефильтрованную сетку." + }, + "gridColumns": { + "title": "Столбцы сетки", + "desc": "Выберите количество столбцов сетки." + }, + "searchSource": { + "label": "Источник поиска", + "desc": "Выберите, выполнять поиск по миниатюрам или описаниям отслеживаемых объектов.", + "options": { + "thumbnailImage": "Изображение миниатюры", + "description": "Описание" + } + }, + "title": "Настройки" + }, + "date": { + "selectDateBy": { + "label": "Выберите дату для фильтрации" + } + } + }, + "logSettings": { + "filterBySeverity": "Фильтровать логи по уровню важности", + "loading": { + "title": "Загрузка", + "desc": "При прокрутке панели логов в самый низ новые записи автоматически отображаются по мере их добавления." + }, + "label": "Уровень детализации логов", + "allLogs": "Все логи", + "disableLogStreaming": "Отключить потоковую передачу логов" + }, + "trackedObjectDelete": { + "title": "Подтвердить удаление", + "toast": { + "error": "Не удалось удалить отслеживаемые объекты: {{errorMessage}}", + "success": "Отслеживаемые объекты успешно удалены." + }, + "desc": "Удаление этих {{objectLength}} отслеживаемых объектов приведёт к удалению их снимков, сохранённых эмбеддингов и записей жизненного цикла. НО сами записи в разделе «История» останутся.

    Вы уверены, что хотите продолжить?

    Удерживайте Shift, чтобы пропустить это окно в будущем." + }, + "zoneMask": { + "filterBy": "Фильтр по маске зоны" + }, + "recognizedLicensePlates": { + "noLicensePlatesFound": "Номерных знаков не найдено.", + "placeholder": "Введите номер для поиска знака…", + "title": "Распознанные номерные знаки", + "loadFailed": "Не удалось загрузить распознанные номерные знаки.", + "loading": "Загрузка распознанных номерных знаков…", + "selectPlatesFromList": "Выберите один или более знаков из списка.", + "selectAll": "Выбрать все", + "clearAll": "Очистить все" + }, + "review": { + "showReviewed": "Показать просмотренные" + }, + "motion": { + "showMotionOnly": "Показывать только движение" + }, + "classes": { + "label": "Классы", + "all": { + "title": "Все классы" + }, + "count_one": "{{count}} класс", + "count_other": "{{count}} классы" + }, + "attributes": { + "label": "Атрибуты классификации", + "all": "Все атрибуты" + } +} diff --git a/web/public/locales/ru/components/icons.json b/web/public/locales/ru/components/icons.json new file mode 100644 index 0000000..2d0f3cc --- /dev/null +++ b/web/public/locales/ru/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Выберите иконку", + "search": { + "placeholder": "Поиск иконки…" + } + } +} diff --git a/web/public/locales/ru/components/input.json b/web/public/locales/ru/components/input.json new file mode 100644 index 0000000..149b56d --- /dev/null +++ b/web/public/locales/ru/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Скачать видео", + "toast": { + "success": "Загрузка видео начата." + } + } + } +} diff --git a/web/public/locales/ru/components/player.json b/web/public/locales/ru/components/player.json new file mode 100644 index 0000000..f0a44ef --- /dev/null +++ b/web/public/locales/ru/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Не найдено ни одной записи", + "noPreviewFound": "Предпросмотр не найден", + "submitFrigatePlus": { + "title": "Отправить этот кадр в Frigate+?", + "submit": "Отправить" + }, + "noPreviewFoundFor": "Не найдено предпросмотра для {{cameraName}}", + "livePlayerRequiredIOSVersion": "iOS 17.1 или выше требуется для этого типа стрима.", + "streamOffline": { + "title": "Поток оффлайн", + "desc": "С потока detect камеры {{cameraName}} не получено кадров, проверьте логи ошибок" + }, + "cameraDisabled": "Камера отключена", + "stats": { + "streamType": { + "title": "Тип потока:", + "short": "Тип" + }, + "bandwidth": { + "title": "Пропускная способность:", + "short": "Пропускная способность" + }, + "latency": { + "title": "Задержка:", + "value": "{{seconds}} сек", + "short": { + "title": "Задержка", + "value": "{{seconds}} сек" + } + }, + "totalFrames": "Всего кадров:", + "droppedFrames": { + "title": "Пропущено кадров:", + "short": { + "title": "Пропущено", + "value": "{{droppedFrames}} кадров" + } + }, + "decodedFrames": "Декодированные кадры:", + "droppedFrameRate": "Частота пропущенных кадров:" + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "Не удалось отправить кадр в Frigate+" + }, + "success": { + "submittedFrigatePlus": "Кадр успешно загружен в Frigate+" + } + } +} diff --git a/web/public/locales/ru/config/cameras.json b/web/public/locales/ru/config/cameras.json new file mode 100644 index 0000000..d50ac03 --- /dev/null +++ b/web/public/locales/ru/config/cameras.json @@ -0,0 +1,110 @@ +{ + "name": { + "label": "Наименование камеры", + "description": "Наименование камеры это обязательное поле" + }, + "enabled": { + "label": "Включено", + "description": "Включено" + }, + "friendly_name": { + "label": "Отображаемое имя", + "description": "Отображаемое имя уже используется" + }, + "label": "Конфигурация", + "audio": { + "label": "Аудиособытия", + "description": "Настройки обнаружения аудиособытий для этой камеры.", + "enabled": { + "label": "Включить обнаружение звука", + "description": "Включить или отключить аудиособытия для этой камеры." + }, + "max_not_heard": { + "label": "Завершение таймаута", + "description": "Количество секунд без указания типа звука до завершения звукового события." + }, + "min_volume": { + "label": "Минимальная громкость", + "description": "Для запуска функции обнаружения звука требуется минимальный пороговый уровень громкости RMS; более низкие значения повышают чувствительность (например, 200 — высокий, 500 — средний, 1000 — низкий)." + }, + "listen": { + "description": "Список типов аудиособытий для обнаружения (например: лай, пожарная тревога, крик, речь, вопль).", + "label": "Типы аудиособытий" + }, + "filters": { + "label": "Аудиофильтры", + "description": "Настройки фильтров для каждого типа аудиофайлов, такие как пороговые значения, используются для уменьшения количества ложных срабатываний." + }, + "enabled_in_config": { + "label": "Исходное состояние звука", + "description": "Указывает, было ли изначально включено обнаружение звука в статическом конфигурационном файле." + }, + "num_threads": { + "label": "Обнаружение потоков", + "description": "Количество потоков, используемых для обработки обнаружения звука." + } + }, + "audio_transcription": { + "label": "Расшифровка аудиозаписи", + "description": "Настройки для транскрипции аудио в реальном времени и речи, используемые для событий и субтитров в реальном времени.", + "enabled": { + "label": "Включить транскрипцию", + "description": "Включить или отключить транскрипцию аудиособытий, запускаемую вручную." + }, + "enabled_in_config": { + "label": "Исходное состояние транскрипции" + }, + "live_enabled": { + "label": "Транскрипция в реальном времени", + "description": "Включить потоковую транскрипцию аудио в режиме реального времени по мере его поступления." + } + }, + "birdseye": { + "description": "Настройки для составного режима просмотра Birdseye, который объединяет видеопоток с нескольких камер в единый макет.", + "label": "Режим Birdseye", + "enabled": { + "label": "Включить Birdseye", + "description": "Включить или отключить функцию Birdseye." + }, + "mode": { + "label": "Режим слежения", + "description": "Режимы добавления камер в Birdseye: «объекты», «движение» или «непрерывный»." + }, + "order": { + "label": "Позиция", + "description": "Числовое значение, управляющее порядком расположения камер в схеме Birdseye." + } + }, + "detect": { + "label": "Обнаружение объектов", + "description": "Настройки роли обнаружения, используемые для запуска обнаружения объектов и инициализации трекеров.", + "enabled": { + "label": "Включить обнаружение объектов", + "description": "Включить или отключить обнаружение объектов для этой камеры." + }, + "height": { + "label": "Высота обнаружения", + "description": "Высота (в пикселях) кадров, используемых для обнаружения потока; оставьте поле пустым, чтобы использовать собственное разрешение потока." + }, + "width": { + "label": "Ширина обнаружения", + "description": "Ширина (в пикселях) кадров, используемых для обнаружения потока; оставьте поле пустым, чтобы использовать собственное разрешение потока." + }, + "fps": { + "label": "Частота кадров обнаружения", + "description": "Желаемое количество кадров в секунду для выполнения обнаружения; более низкие значения снижают нагрузку на ЦП (рекомендуемое значение — 5, более высокое значение — максимум 10 — следует устанавливать только при отслеживании чрезвычайно быстро движущихся объектов)." + }, + "min_initialized": { + "label": "Минимальное количество кадров инициализации", + "description": "Количество последовательных срабатываний обнаружения, необходимых для создания отслеживаемого объекта. Увеличьте это значение, чтобы уменьшить количество ложных инициализаций. Значение по умолчанию — частота кадров, деленная на 2." + }, + "max_disappeared": { + "label": "Максимальное количество исчезнувших кадров", + "description": "Количество кадров без обнаружения до того, как отслеживаемый объект будет считаться исчезнувшим." + }, + "stationary": { + "label": "Конфигурация стационарных объектов", + "description": "Настройки для обнаружения и управления объектами, которые остаются неподвижными в течение определенного периода времени." + } + } +} diff --git a/web/public/locales/ru/config/global.json b/web/public/locales/ru/config/global.json new file mode 100644 index 0000000..64b4958 --- /dev/null +++ b/web/public/locales/ru/config/global.json @@ -0,0 +1,141 @@ +{ + "audio": { + "label": "Аудиособытия", + "enabled": { + "label": "Включить обнаружение звука" + }, + "max_not_heard": { + "label": "Завершение таймаута", + "description": "Количество секунд без указания типа звука до завершения звукового события." + }, + "min_volume": { + "label": "Минимальная громкость", + "description": "Для запуска функции обнаружения звука требуется минимальный пороговый уровень громкости RMS; более низкие значения повышают чувствительность (например, 200 — высокий, 500 — средний, 1000 — низкий)." + }, + "listen": { + "description": "Список типов аудиособытий для обнаружения (например: лай, пожарная тревога, крик, речь, вопль).", + "label": "Типы аудиособытий" + }, + "filters": { + "label": "Аудиофильтры", + "description": "Настройки фильтров для каждого типа аудиофайлов, такие как пороговые значения, используются для уменьшения количества ложных срабатываний." + }, + "enabled_in_config": { + "label": "Исходное состояние звука", + "description": "Указывает, было ли изначально включено обнаружение звука в статическом конфигурационном файле." + }, + "num_threads": { + "label": "Обнаружение потоков", + "description": "Количество потоков, используемых для обработки обнаружения звука." + } + }, + "audio_transcription": { + "label": "Расшифровка аудиозаписи", + "description": "Настройки для транскрипции аудио в реальном времени и речи, используемые для событий и субтитров в реальном времени.", + "live_enabled": { + "label": "Транскрипция в реальном времени", + "description": "Включить потоковую транскрипцию аудио в режиме реального времени по мере его поступления." + } + }, + "birdseye": { + "description": "Настройки для составного режима просмотра Birdseye, который объединяет видеопоток с нескольких камер в единый макет.", + "label": "Режим Birdseye", + "enabled": { + "label": "Включить Birdseye", + "description": "Включить или отключить функцию Birdseye." + }, + "mode": { + "label": "Режим слежения", + "description": "Режимы добавления камер в Birdseye: «объекты», «движение» или «непрерывный»." + }, + "order": { + "label": "Позиция", + "description": "Числовое значение, управляющее порядком расположения камер в схеме Birdseye." + } + }, + "detect": { + "label": "Обнаружение объектов", + "description": "Настройки роли обнаружения, используемые для запуска обнаружения объектов и инициализации трекеров.", + "enabled": { + "label": "Включить обнаружение объектов" + }, + "height": { + "label": "Высота обнаружения", + "description": "Высота (в пикселях) кадров, используемых для обнаружения потока; оставьте поле пустым, чтобы использовать собственное разрешение потока." + }, + "width": { + "label": "Ширина обнаружения", + "description": "Ширина (в пикселях) кадров, используемых для обнаружения потока; оставьте поле пустым, чтобы использовать собственное разрешение потока." + }, + "fps": { + "label": "Частота кадров обнаружения", + "description": "Желаемое количество кадров в секунду для выполнения обнаружения; более низкие значения снижают нагрузку на ЦП (рекомендуемое значение — 5, более высокое значение — максимум 10 — следует устанавливать только при отслеживании чрезвычайно быстро движущихся объектов)." + }, + "min_initialized": { + "label": "Минимальное количество кадров инициализации", + "description": "Количество последовательных срабатываний обнаружения, необходимых для создания отслеживаемого объекта. Увеличьте это значение, чтобы уменьшить количество ложных инициализаций. Значение по умолчанию — частота кадров, деленная на 2." + }, + "max_disappeared": { + "label": "Максимальное количество исчезнувших кадров", + "description": "Количество кадров без обнаружения до того, как отслеживаемый объект будет считаться исчезнувшим." + }, + "stationary": { + "label": "Конфигурация стационарных объектов", + "description": "Настройки для обнаружения и управления объектами, которые остаются неподвижными в течение определенного периода времени." + } + }, + "version": { + "label": "Текущая версия конфигурации", + "description": "Число или строка версии текущей конфигурации, которая может использоваться для определения миграций или форматирования изменений." + }, + "safe_mode": { + "label": "Безопасный режим", + "description": "Когда включено, Frigate запустится в безопасном режиме с ограниченными функциями для поиска неисправностей." + }, + "environment_vars": { + "label": "Переменные окружения", + "description": "Пары ключ/значения для переменных окружения которые необходимо задать для процесса Frigate в Home Assistant OS. Пользователи, которые не исползуют HAOS должны испольовать переменные окружения в Docker." + }, + "logger": { + "label": "Логирование", + "description": "Управляет уровнем логирования по умолчанию и переопределением уровня для каждого компонента.", + "default": { + "label": "Уровень логирования", + "description": "Стандартный глобальный уровень логирования (debug, info, warning, error)." + }, + "logs": { + "label": "Уровень логирования для каждого процесса" + } + }, + "auth": { + "label": "Аутентификация", + "description": "Настройки аутентификации и сеанса, включая параметры cookie и ограничения скорости.", + "enabled": { + "label": "Включить аутентификацию", + "description": "Включить встроенную аутентификацию для интерфейса Frigate." + }, + "reset_admin_password": { + "label": "Сбросить пароль администратора", + "description": "Если выбрано, сбросить пароль администратора при запуске и отобразить новый пароль в логе." + }, + "cookie_name": { + "label": "Имя куки JWT", + "description": "Имя куки, используемого для хранения JWT токена для стандартной аутенфикации." + }, + "cookie_secure": { + "label": "Флаг \"безопасный куки\"", + "description": "Устанавливает флаг \"secure\" на куки аутенфикации; должно быть включено когда используется TLS." + }, + "session_length": { + "label": "Длинна сессии", + "description": "Длина сессии в секундах для JWT сессий." + }, + "refresh_time": { + "label": "Окно обновления сессии", + "description": "Когда сессия в стольки секундах от истечения, обновить её обратно к полной длительности." + }, + "failed_login_rate_limit": { + "label": "Лимит неудавшихся попыток логина" + } + } +} diff --git a/web/public/locales/ru/config/groups.json b/web/public/locales/ru/config/groups.json new file mode 100644 index 0000000..a7c9152 --- /dev/null +++ b/web/public/locales/ru/config/groups.json @@ -0,0 +1,65 @@ +{ + "audio": { + "global": { + "sensitivity": "Общая чувствительность", + "detection": "Общее обнаружение" + }, + "cameras": { + "detection": "Обнаружение", + "sensitivity": "Чувствительность" + } + }, + "timestamp_style": { + "global": { + "appearance": "Глобальный вид" + }, + "cameras": { + "appearance": "Вид" + } + }, + "motion": { + "global": { + "sensitivity": "Глобальная чувствительность", + "algorithm": "Глобальный алгоритм" + }, + "cameras": { + "sensitivity": "Чувствительность", + "algorithm": "Алгоритм" + } + }, + "detect": { + "global": { + "resolution": "Глобальное разрешение", + "tracking": "Глобальное отслеживание" + }, + "cameras": { + "resolution": "Разрешение", + "tracking": "Отслеживание" + } + }, + "objects": { + "global": { + "tracking": "Глобальное отслеживание", + "filtering": "Глобальная фильтрация" + }, + "cameras": { + "tracking": "Отслеживание", + "filtering": "Фильтрация" + } + }, + "record": { + "global": { + "retention": "Глобальное сохранение данных", + "events": "Глобальные события" + }, + "cameras": { + "retention": "Сохранение данных", + "events": "События" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Аргументы FFmpeg для этой камеры" + } + } +} diff --git a/web/public/locales/ru/config/validation.json b/web/public/locales/ru/config/validation.json new file mode 100644 index 0000000..2314881 --- /dev/null +++ b/web/public/locales/ru/config/validation.json @@ -0,0 +1,32 @@ +{ + "maximum": "Должно быть максимум {{limit}}", + "exclusiveMinimum": "Должно быть больше {{limit}}", + "exclusiveMaximum": "Должно быть не более {{limit}}", + "minLength": "Должно быть не менее {{limit}} символов", + "maxLength": "Должно быть не более {{limit}} символов", + "minItems": "Должно быть не менее {{limit}} значений", + "maxItems": "Должно быть не более {{limit}} значений", + "pattern": "Неправильный формат", + "required": "Это поле обязательно", + "type": "Неправильный тип значения", + "enum": "Должно быть одним из списка разрешенных значений", + "const": "Значение не совпадает с ожидаемой константой", + "uniqueItems": "Все значения должны быть уникальны", + "format": "Неправильный формат", + "additionalProperties": "Неизвестное значение недопустимо", + "oneOf": "Должно совпадать только с одной из разрешенных схем", + "anyOf": "Должно совпадать как минимум с одной из разрешенных схем", + "proxy": { + "header_map": { + "roleHeaderRequired": "Заголовок роли требуется когда маппинги ролей настроены." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "Каждой роли может быть назначен только один входной поток.", + "detectRequired": "Как минимум один входной поток должен быть назначен роли 'detect'.", + "hwaccelDetectOnly": "Только входной поток с ролью detect может настраивать аппаратное ускорение." + } + }, + "minimum": "Должно быть минимум {{limit}}" +} diff --git a/web/public/locales/ru/objects.json b/web/public/locales/ru/objects.json new file mode 100644 index 0000000..c8cdac4 --- /dev/null +++ b/web/public/locales/ru/objects.json @@ -0,0 +1,120 @@ +{ + "dog": "Собака", + "cat": "Кошка", + "animal": "Животное", + "bark": "Лай", + "person": "Человек", + "bicycle": "Велосипед", + "car": "Автомобиль", + "motorcycle": "Мотоцикл", + "bird": "Птица", + "horse": "Лошадь", + "sheep": "Овца", + "mouse": "Мышь", + "goat": "Коза", + "airplane": "Самолет", + "keyboard": "Клавиатура", + "boat": "Лодка", + "bus": "Автобус", + "train": "Поезд", + "skateboard": "Скейтборд", + "door": "Дверь", + "blender": "Блендер", + "sink": "Раковина", + "clock": "Часы", + "vehicle": "Транспорт", + "hair_dryer": "Фен", + "toothbrush": "Зубная щетка", + "scissors": "Ножницы", + "traffic_light": "Светофор", + "fire_hydrant": "Пожарный гидрант", + "street_sign": "Дорожный знак", + "stop_sign": "Знак Стоп", + "parking_meter": "Парковочный счётчик", + "bench": "Скамейка", + "cow": "Корова", + "elephant": "Слон", + "bear": "Медведь", + "zebra": "Зебра", + "giraffe": "Жираф", + "hat": "Шляпа", + "backpack": "Рюкзак", + "umbrella": "Зонтик", + "shoe": "Обувь", + "eye_glasses": "Очки", + "tie": "Галстук", + "suitcase": "Чемодан", + "handbag": "Сумочка", + "frisbee": "Фрисби", + "skis": "Лыжи", + "snowboard": "Сноуборд", + "kite": "Воздушный змей", + "baseball_bat": "Бейсбольная бита", + "baseball_glove": "Бейсбольная перчатка", + "sports_ball": "Спортивный мяч", + "surfboard": "Доска для серфинга", + "tennis_racket": "Теннисная ракетка", + "bottle": "Бутылка", + "plate": "Тарелка", + "wine_glass": "Винный бокал", + "cup": "Чашка", + "fork": "Вилка", + "spoon": "Ложка", + "bowl": "Миска", + "banana": "Банан", + "apple": "Яблоко", + "orange": "Апельсин", + "broccoli": "Брокколи", + "sandwich": "Сэндвич", + "carrot": "Морковь", + "hot_dog": "Хот-дог", + "pizza": "Пицца", + "donut": "Пончик", + "cake": "Торт", + "chair": "Стул", + "couch": "Диван", + "potted_plant": "Комнатное растение", + "bed": "Кровать", + "mirror": "Зеркало", + "dining_table": "Обеденный стол", + "window": "Окно", + "desk": "Стол", + "toilet": "Туалет", + "tv": "Телевизор", + "laptop": "Ноутбук", + "remote": "Пульт дистанционного управления", + "cell_phone": "Мобильный телефон", + "microwave": "Микроволновка", + "oven": "Духовка", + "toaster": "Тостер", + "refrigerator": "Холодильник", + "book": "Книга", + "vase": "Ваза", + "teddy_bear": "Плюшевый мишка", + "hair_brush": "Расчёска", + "squirrel": "Белка", + "deer": "Олень", + "fox": "Лиса", + "rabbit": "Кролик", + "raccoon": "Енот", + "robot_lawnmower": "Роботизированная газонокосилка", + "waste_bin": "Мусорное ведро", + "on_demand": "По требованию", + "face": "Лицо", + "license_plate": "Номерной знак", + "package": "Посылка", + "bbq_grill": "Гриль и барбекю", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "knife": "Нож", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/web/public/locales/ru/views/chat.json b/web/public/locales/ru/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ru/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ru/views/classificationModel.json b/web/public/locales/ru/views/classificationModel.json new file mode 100644 index 0000000..1213912 --- /dev/null +++ b/web/public/locales/ru/views/classificationModel.json @@ -0,0 +1,198 @@ +{ + "documentTitle": "Классификация моделей - Frigate", + "details": { + "scoreInfo": "Оценка представляет собой среднюю степень достоверности классификации по всем обнаружениям данного объекта.", + "none": "Нет", + "unknown": "Неизвестно" + }, + "button": { + "deleteClassificationAttempts": "Удалить изображения классификации", + "renameCategory": "Переименовать класс", + "deleteCategory": "Удалить класс", + "deleteImages": "Удалить изображения", + "trainModel": "Тренировать модель", + "addClassification": "Добавить классификацию", + "deleteModels": "Удалить модели", + "editModel": "Редактировать модель" + }, + "toast": { + "success": { + "deletedCategory_one": "Класс удалён", + "deletedCategory_few": "Класса удалено", + "deletedCategory_many": "Классов удалено", + "deletedImage_one": "Изображения удалены", + "deletedImage_few": "Изображения удалено", + "deletedImage_many": "Изображений удалено", + "deletedModel_one": "Успешно удалена {{count}} модель", + "deletedModel_few": "Успешно удалены {{count}} модели", + "deletedModel_many": "Успешно удалены {{count}} моделей", + "categorizedImage": "Изображение успешно классифицировано", + "trainedModel": "Модель успешно обучена.", + "trainingModel": "Обучение модели успешно запущено.", + "updatedModel": "Конфигурация модели успешно обновлена", + "renamedCategory": "Класс успешно переименован в {{name}}", + "reclassifiedImage": "Изображение успешно переклассифцировано" + }, + "error": { + "deleteImageFailed": "Не удалось удалить: {{errorMessage}}", + "deleteCategoryFailed": "Не удалось удалить класс: {{errorMessage}}", + "deleteModelFailed": "Не удалось удалить модель: {{errorMessage}}", + "categorizeFailed": "Не удалось классифицировать изображение: {{errorMessage}}", + "trainingFailed": "Ошибка обучения модели. Проверьте логи Frigate для получения подробной информации.", + "updateModelFailed": "Не удалось обновить модель: {{errorMessage}}", + "renameCategoryFailed": "Не удалось переименовать класс: {{errorMessage}}", + "trainingFailedToStart": "Не удалось начать обучение модели: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Удалить класс", + "desc": "Вы уверены, что хотите удалить класс {{name}}? Это приведёт к безвозвратному удалению всех связанных с ним изображений и потребует повторного обучения модели.", + "minClassesTitle": "Не удалось удалить класс", + "minClassesDesc": "Модель классификации должна содержать как минимум 2 класса. Добавьте ещё один класс перед удалением этого." + }, + "deleteModel": { + "title": "Удалить модель классификации", + "single": "Вы уверены, что хотите удалить {{name}}? Это приведёт к безвозвратному удалению всех связанных данных, включая изображения и данные обучения. Это действие нельзя отменить.", + "desc_one": "Вы уверены, что хотите удалить {{count}} модель? Это приведёт к безвозвратному удалению всех связанных данных, включая изображения и данные обучения. Это действие нельзя отменить.", + "desc_few": "Вы уверены, что хотите удалить {{count}} модели? Это приведёт к безвозвратному удалению всех связанных данных, включая изображения и данные обучения. Это действие нельзя отменить.", + "desc_many": "Вы уверены, что хотите удалить {{count}} моделей? Это приведёт к безвозвратному удалению всех связанных данных, включая изображения и данные обучения. Это действие нельзя отменить." + }, + "edit": { + "title": "Редактировать модель классификации", + "descriptionState": "Редактировать классы для этой модели классификации состояний. Изменения потребуют повторного обучения модели.", + "descriptionObject": "Редактировать тип объекта и тип классификации для этой модели классификации объектов.", + "stateClassesInfo": "Примечание: изменение классов состояний требует повторного обучения модели с обновлёнными классами." + }, + "deleteDatasetImages": { + "title": "Удалить изображения набора данных", + "desc_one": "Вы уверены, что хотите удалить {{count}} изображение из {{dataset}}? Это действие нельзя отменить и потребует повторного обучения модели.", + "desc_few": "Вы уверены, что хотите удалить {{count}} изображения из {{dataset}}? Это действие нельзя отменить и потребует повторного обучения модели.", + "desc_many": "Вы уверены, что хотите удалить {{count}} изображений из {{dataset}}? Это действие нельзя отменить и потребует повторного обучения модели." + }, + "deleteTrainImages": { + "title": "Удалить обучающие изображения", + "desc_one": "Вы уверены, что хотите удалить {{count}} изображение? Это действие нельзя отменить.", + "desc_few": "Вы уверены, что хотите удалить {{count}} изображения? Это действие нельзя отменить.", + "desc_many": "Вы уверены, что хотите удалить {{count}} изображений? Это действие нельзя отменить." + }, + "renameCategory": { + "title": "Переименовать класс", + "desc": "Введите новое имя для {{name}}. Вам потребуется повторно обучить модель, чтобы изменение имени вступило в силу." + }, + "description": { + "invalidName": "Недопустимое имя. Имена могут содержать только буквы, цифры, пробелы, апострофы, подчёркивания и дефисы." + }, + "train": { + "title": "Недавние классификации", + "titleShort": "Недавнее", + "aria": "Выбрать недавние классификации" + }, + "categories": "Классы", + "createCategory": { + "new": "Создать новый класс" + }, + "categorizeImageAs": "Классифицировать изображение как:", + "categorizeImage": "Классифицировать изображение", + "menu": { + "objects": "Объекты", + "states": "Состояния" + }, + "noModels": { + "object": { + "title": "Нет моделей классификации объектов", + "description": "Создайте пользовательскую модель для классификации обнаруженных объектов.", + "buttonText": "Создать модель объекта" + }, + "state": { + "title": "Нет моделей классификации состояний", + "description": "Создайте пользовательскую модель для мониторинга и классификации изменений состояний в определённых областях камеры.", + "buttonText": "Создать модель состояния" + } + }, + "wizard": { + "title": "Создать новую классификацию", + "steps": { + "nameAndDefine": "Имя и определение", + "stateArea": "Область состояния", + "chooseExamples": "Выбрать примеры" + }, + "step1": { + "description": "Модели состояний отслеживают фиксированные области камеры на предмет изменений (например, дверь открыта/закрыта). Модели объектов добавляют классификации к обнаруженным объектам (например, известные животные, курьеры и т.д.).", + "name": "Имя", + "namePlaceholder": "Введите имя модели…", + "type": "Тип", + "typeState": "Состояние", + "typeObject": "Объект", + "objectLabel": "Метка объекта", + "objectLabelPlaceholder": "Выберите тип объекта…", + "classificationType": "Тип классификации", + "classificationTypeTip": "Узнать о типах классификации", + "classificationTypeDesc": "Подметки добавляют дополнительный текст к метке объекта (например, 'Человек: UPS'). Атрибуты — это доступные для поиска метаданные, хранящиеся отдельно в метаданных объекта.", + "classificationSubLabel": "Подметка", + "classificationAttribute": "Атрибут", + "classes": "Классы", + "states": "Состояния", + "classesTip": "Узнать о классах", + "classesStateDesc": "Определите различные состояния, в которых может находиться область вашей камеры. Например: 'открыто' и 'закрыто' для гаражных ворот.", + "classesObjectDesc": "Определите различные категории для классификации обнаруженных объектов. Например: 'курьер', 'житель', 'незнакомец' для классификации людей.", + "classPlaceholder": "Введите имя класса…", + "errors": { + "nameRequired": "Имя модели обязательно", + "nameLength": "Имя модели должно содержать не более 64 символов", + "nameOnlyNumbers": "Имя модели не может состоять только из цифр", + "classRequired": "Требуется хотя бы 1 класс", + "classesUnique": "Имена классов должны быть уникальными", + "stateRequiresTwoClasses": "Модели состояний требуют не менее 2 классов", + "objectLabelRequired": "Пожалуйста, выберите метку объекта", + "objectTypeRequired": "Пожалуйста, выберите тип классификации", + "noneNotAllowed": "Класс 'нет' не допускается" + } + }, + "step2": { + "description": "Выберите камеры и определите область для мониторинга для каждой камеры. Модель будет классифицировать состояние этих областей.", + "cameras": "Камеры", + "selectCamera": "Выбрать камеру", + "noCameras": "Нажмите +, чтобы добавить камеры", + "selectCameraPrompt": "Выберите камеру из списка, чтобы определить область её мониторинга" + }, + "step3": { + "selectImagesPrompt": "Выберите все изображения с {{className}}", + "selectImagesDescription": "Нажмите на изображения, чтобы выбрать их. Нажмите Продолжить, когда закончите с этим классом.", + "generating": { + "title": "Генерация примеров изображений", + "description": "Frigate извлекает репрезентативные изображения из ваших записей. Это может занять некоторое время…" + }, + "training": { + "title": "Обучение модели", + "description": "Ваша модель обучается в фоновом режиме. Закройте это диалоговое окно, и ваша модель начнёт работать, как только обучение будет завершено." + }, + "retryGenerate": "Повторить генерацию", + "noImages": "Примеры изображений не сгенерированы", + "classifying": "Классификация и обучение…", + "trainingStarted": "Обучение успешно запущено", + "errors": { + "noCameras": "Камеры не настроены", + "noObjectLabel": "Метка объекта не выбрана", + "generateFailed": "Не удалось сгенерировать примеры: {{error}}", + "generationFailed": "Генерация не удалась. Пожалуйста, попробуйте снова.", + "classifyFailed": "Не удалось классифицировать изображения: {{error}}" + }, + "generateSuccess": "Примеры изображений успешно сгенерированы", + "allImagesRequired_one": "Пожалуйста, классифицируйте все изображения. Осталось {{count}} изображение.", + "allImagesRequired_few": "Пожалуйста, классифицируйте все изображения. Осталось {{count}} изображения.", + "allImagesRequired_many": "Пожалуйста, классифицируйте все изображения. Осталось {{count}} изображений.", + "modelCreated": "Модель успешно создана. Используйте раздел \"Последние классификации\", чтобы добавить изображения для отсутствующих состояний, а затем обучите модель.", + "missingStatesWarning": { + "title": "Примеры отсутствующих состояний", + "description": "Рекомендуется выбрать примеры для всех состояний для достижения наилучших результатов. Вы можете продолжить, не выбрав все состояния, но модель не будет обучена, пока для всех состояний не появятся изображения. После продолжения используйте раздел «Последние классификации», чтобы классифицировать изображения для отсутствующих состояний, а затем обучите модель." + } + } + }, + "tooltip": { + "trainingInProgress": "Модель в данный момент обучается", + "noNewImages": "Нет новых изображений для обучения. Сначала классифицируйте больше изображений в наборе данных.", + "noChanges": "В наборе данных не было изменений с момента последнего обучения.", + "modelNotReady": "Модель не готова к обучению" + }, + "none": "Нет" +} diff --git a/web/public/locales/ru/views/configEditor.json b/web/public/locales/ru/views/configEditor.json new file mode 100644 index 0000000..0dd775b --- /dev/null +++ b/web/public/locales/ru/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Редактор конфигурации", + "copyConfig": "Скопировать конфигурацию", + "saveAndRestart": "Сохранить и перезапустить", + "saveOnly": "Только сохранить", + "documentTitle": "Редактор конфигурации - Frigate", + "toast": { + "success": { + "copyToClipboard": "Конфигурация скопирована в буфер обмена." + }, + "error": { + "savingError": "Ошибка сохранения конфигурации" + } + }, + "confirm": "Выйти без сохранения?", + "safeConfigEditor": "Редактор конфигурации (безопасный режим)", + "safeModeDescription": "Frigate находится в безопасном режиме из-за ошибки проверки конфигурации." +} diff --git a/web/public/locales/ru/views/events.json b/web/public/locales/ru/views/events.json new file mode 100644 index 0000000..20fa143 --- /dev/null +++ b/web/public/locales/ru/views/events.json @@ -0,0 +1,70 @@ +{ + "alerts": "Тревоги", + "detections": "Обнаружения", + "motion": { + "label": "Движение", + "only": "Только движение" + }, + "allCameras": "Все камеры", + "camera": "Камера", + "empty": { + "alert": "Отсутствуют тревоги для просмотра", + "detection": "Отсутствуют обнаружения для просмотра", + "motion": "Не найдено данных о движении", + "recordingsDisabled": { + "title": "Запись должна быть включена", + "description": "Элементы обзора могут быть созданы для камеры только в том случае, если запись включена для этой камеры." + } + }, + "timeline": { + "label": "Хронология" + }, + "timeline.aria": "Выбор таймлайна", + "events": { + "label": "События", + "aria": "Выбор событий", + "noFoundForTimePeriod": "Для этого периода времени не найдено ни одного события." + }, + "documentTitle": "Обзор событий - Frigate", + "recordings": { + "documentTitle": "Записи - Frigate" + }, + "calendarFilter": { + "last24Hours": "Последние 24 часа" + }, + "markAsReviewed": "Пометить как просмотренное", + "newReviewItems": { + "label": "Посмотреть новые элементы для просмотра", + "button": "Новые элементы для просмотра" + }, + "markTheseItemsAsReviewed": "Пометить эти элементы как просмотренные", + "selected": "{{count}} выбрано", + "selected_one": "{{count}} выбрано", + "selected_other": "{{count}} выбрано", + "detected": "обнаружен", + "suspiciousActivity": "Подозрительная активность", + "threateningActivity": "Угрожающая активность", + "detail": { + "noDataFound": "Нет данных для просмотра", + "aria": "Переключить подробный режим просмотра", + "trackedObject_one": "{{count}} объект", + "trackedObject_other": "{{count}} объекта", + "noObjectDetailData": "Данные о деталях объекта недоступны.", + "label": "Деталь", + "settings": "Настройки подробного просмотра", + "alwaysExpandActive": { + "title": "Всегда раскрывать активный", + "desc": "Всегда раскрывать сведения об объекте активного элемента обзора, если они доступны." + } + }, + "objectTrack": { + "trackedPoint": "Отслеживаемая точка", + "clickToSeek": "Перейти к этому моменту" + }, + "zoomIn": "Увеличить", + "zoomOut": "Отдалить", + "select_all": "Всё", + "normalActivity": "Нормальный", + "needsReview": "Требуется ревью", + "securityConcern": "Вопрос безопасности" +} diff --git a/web/public/locales/ru/views/explore.json b/web/public/locales/ru/views/explore.json new file mode 100644 index 0000000..8431293 --- /dev/null +++ b/web/public/locales/ru/views/explore.json @@ -0,0 +1,305 @@ +{ + "exploreIsUnavailable": { + "embeddingsReindexing": { + "context": "Поиск станет доступен после завершения переиндексации эмбеддингов отслеживаемых объектов.", + "startingUp": "Запуск…", + "estimatedTime": "Оставшееся время:", + "finishingShortly": "Скоро завершится", + "step": { + "descriptionsEmbedded": "Встроенные описания: ", + "trackedObjectsProcessed": "Обработанные отслеживаемые объекты: ", + "thumbnailsEmbedded": "Встроенные миниатюры: " + } + }, + "title": "Поиск событий недоступен", + "downloadingModels": { + "setup": { + "visionModel": "Модель компьютерного зрения", + "visionModelFeatureExtractor": "Экстрактор признаков модели компьютерного зрения", + "textModel": "Текстовая модель", + "textTokenizer": "Текстовый токенизатор" + }, + "tips": { + "context": "Возможно, вы захотите переиндексировать эмбеддинги отслеживаемых объектов после загрузки моделей.", + "documentation": "Читать документацию" + }, + "context": "Frigate загружает необходимые модели эмбеддингов для поддержки функции семантического поиска. Это может занять несколько минут в зависимости от скорости вашего интернет-соединения.", + "error": "Произошла ошибка. Проверьте логи Frigate." + } + }, + "generativeAI": "Генеративный ИИ", + "documentTitle": "Поиск событий - Frigate", + "details": { + "timestamp": "Метка времени", + "item": { + "title": "Детали элемента просмотра", + "desc": "Детали элемента просмотра", + "button": { + "share": "Поделиться этим элементом просмотра", + "viewInExplore": "Смотреть в Поиске событий" + }, + "tips": { + "hasMissingObjects": "Настройте конфигурацию, если хотите, чтобы Frigate сохранял отслеживаемые объекты для следующих меток: {{objects}}", + "mismatch_one": "{{count}} недоступный объект обнаружен и включен в этот элемент просмотра. Эти объекты либо не соответствовали критериям тревоги/детекции, либо уже были удалены.", + "mismatch_few": "{{count}} недоступных объекта обнаружено и включено в этот элемент просмотра. Эти объекты либо не соответствовали критериям тревоги/детекции, либо уже были удалены.", + "mismatch_many": "{{count}} недоступных объектов обнаружено и включено в этот элемент просмотра. Эти объекты либо не соответствовали критериям тревоги/детекции, либо уже были удалены." + }, + "toast": { + "success": { + "updatedSublabel": "Успешно обновлена дополнительная метка.", + "updatedLPR": "Номерной знак успешно обновлён.", + "regenerate": "Новое описание запрошено у {{provider}}. В зависимости от скорости работы вашего провайдера, генерация нового описания может занять некоторое время.", + "audioTranscription": "Запрос на расшифровку аудио успешно отправлен. В зависимости от скорости вашего сервера Frigate, расшифровка может занять некоторое время.", + "updatedAttributes": "Атрибуты успешно обновлены." + }, + "error": { + "updatedSublabelFailed": "Не удалось обновить дополнительную метку: {{errorMessage}}", + "updatedLPRFailed": "Не удалось обновить номерной знак: {{errorMessage}}", + "regenerate": "Не удалось запросить новое описание у {{provider}}: {{errorMessage}}", + "audioTranscription": "Не удалось запросить транскрипцию аудио: {{errorMessage}}", + "updatedAttributesFailed": "Не удалось обновить атрибуты: {{errorMessage}}" + } + } + }, + "editSubLabel": { + "descNoLabel": "Введите новую дополнительную метку для этого отслеживаемого объекта", + "title": "Редактирование дополнительной метки", + "desc": "Введите новую дополнительную метку для {{label}}" + }, + "topScore": { + "label": "Лучшая оценка", + "info": "Лучшая оценка — это наивысшая медианная оценка для отслеживаемого объекта, поэтому она может отличаться от оценки, показанной на превью в результатах поиска." + }, + "estimatedSpeed": "Расчётная скорость", + "tips": { + "saveDescriptionFailed": "Не удалось обновить описание: {{errorMessage}}", + "descriptionSaved": "Описание успешно сохранено" + }, + "label": "Метка", + "editLPR": { + "title": "Редактирование номерного знака", + "descNoLabel": "Введите новое значение номерного знака для этого отслеживаемого объекта", + "desc": "Введите новое значение номерного знака для {{label}}" + }, + "recognizedLicensePlate": "Распознанный номерной знак", + "objects": "Объекты", + "camera": "Камера", + "zones": "Зоны", + "button": { + "findSimilar": "Найти похожее", + "regenerate": { + "title": "Перегенерировать", + "label": "Перегенерировать описание отслеживаемого объекта" + } + }, + "description": { + "label": "Описание", + "aiTips": "Frigate не будет запрашивать описание у вашего генеративного ИИ-провайдера, пока жизненный цикл отслеживаемого объекта не завершится.", + "placeholder": "Описание отслеживаемого объекта" + }, + "expandRegenerationMenu": "Развернуть меню перегенерации", + "regenerateFromSnapshot": "Перегенерировать из снимка", + "regenerateFromThumbnails": "Перегенерировать из миниатюры", + "snapshotScore": { + "label": "Оценка снимка" + }, + "score": { + "label": "Оценка" + }, + "editAttributes": { + "title": "Редактировать атрибуты", + "desc": "Выберите атрибуты классификации для этого {{label}}" + }, + "attributes": "Атрибуты классификации", + "title": { + "label": "Заголовок" + } + }, + "trackedObjectDetails": "Детали объекта", + "type": { + "details": "детали", + "snapshot": "снимок", + "video": "видео", + "object_lifecycle": "жизненный цикл объекта", + "thumbnail": "миниатюра", + "tracking_details": "подробности отслеживания" + }, + "objectLifecycle": { + "title": "Жизненный цикл объекта", + "noImageFound": "Для этой метки времени изображение не найдено.", + "createObjectMask": "Создать маску объекта", + "adjustAnnotationSettings": "Изменить настройки аннотаций", + "scrollViewTips": "Прокрутите, чтобы просмотреть ключевые моменты жизненного цикла этого объекта.", + "autoTrackingTips": "Позиции ограничивающих рамок будут неточными для камер с автотрекингом.", + "lifecycleItemDesc": { + "visible": "Обнаружен(а) {{label}}", + "entered_zone": "{{label}} зафиксирован(а) в {{zones}}", + "active": "{{label}} активировался(ась)", + "stationary": "{{label}} перестал(а) двигаться", + "attribute": { + "faceOrLicense_plate": "{{attribute}} обнаружен для {{label}}", + "other": "{{label}} распознан(а) как {{attribute}}" + }, + "gone": "{{label}} покинул(а) зону", + "heard": "Обнаружен звук {{label}}", + "external": "Обнаружен(а) {{label}}", + "header": { + "zones": "Зоны", + "ratio": "Соотношение", + "area": "Область" + } + }, + "annotationSettings": { + "title": "Настройки аннотаций", + "showAllZones": { + "title": "Показать все зоны", + "desc": "Всегда показывать зоны на кадрах, где объекты вошли в зону." + }, + "offset": { + "label": "Сдвиг аннотаций", + "desc": "Эти данные поступают из потока детекции вашей камеры, но накладываются на изображения из потока записи. Потоки вряд ли идеально синхронизированы, поэтому ограничивающая рамка и видео могут не совпадать. Для корректировки используйте поле Сдвиг аннотаций.", + "millisecondsToOffset": "Смещение аннотаций детекции в миллисекундах. По умолчанию: 0", + "documentation": "Читать документацию ", + "tips": "СОВЕТ: Представьте, у вас клип события, где человек идёт слева направо. Если рамка на таймлайне постоянно смещена влево от человека — уменьшите значение. Если рамка опережает движение — увеличьте значение.", + "toast": { + "success": "В конфигурационном файле сохранено значение смещения для {{camera}}. Перезапустите Frigate, чтобы применить изменения." + } + } + }, + "carousel": { + "previous": "Предыдущий слайд", + "next": "Следующий слайд" + }, + "count": "{{first}} из {{second}}", + "trackedPoint": "Отслеживаемая точка" + }, + "itemMenu": { + "downloadVideo": { + "label": "Скачать видео", + "aria": "Скачать видео" + }, + "downloadSnapshot": { + "label": "Скачать снимок", + "aria": "Скачать снимок" + }, + "viewObjectLifecycle": { + "label": "Просмотр жизненного цикла объекта", + "aria": "Показать жизненный цикл объекта" + }, + "findSimilar": { + "label": "Найти похожее", + "aria": "Найти похожие отслеживаемые объекты" + }, + "submitToPlus": { + "label": "Отправить в Frigate+", + "aria": "Отправить в Frigate Plus" + }, + "viewInHistory": { + "label": "Посмотреть в Истории", + "aria": "Посмотреть в Истории" + }, + "deleteTrackedObject": { + "label": "Удалить этот отслеживаемый объект" + }, + "addTrigger": { + "label": "Добавить триггер", + "aria": "Добавить триггер для этого отслеживаемого объекта" + }, + "audioTranscription": { + "label": "Транскрибировать", + "aria": "Запросить аудиотранскрипцию" + }, + "viewTrackingDetails": { + "label": "Просмотреть детали отслеживания", + "aria": "Показать детали отслеживания" + }, + "showObjectDetails": { + "label": "Показать путь объекта" + }, + "hideObjectDetails": { + "label": "Скрыть путь объекта" + }, + "downloadCleanSnapshot": { + "label": "Скачать чистый снимок", + "aria": "Скачать чистый снимок" + } + }, + "dialog": { + "confirmDelete": { + "title": "Подтвердить удаление", + "desc": "Удаление этого отслеживаемого объекта приведёт к удалению снимка, всех сохранённых эмбеддингов и всех связанных записей деталей отслеживания. Записанное видео этого отслеживаемого объекта в представлении Истории НЕ будет удалено.

    Вы уверены, что хотите продолжить?" + } + }, + "noTrackedObjects": "Отслеживаемые объекты не найдены", + "fetchingTrackedObjectsFailed": "Ошибка при получении отслеживаемых объектов: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} отслеживаемый объект ", + "trackedObjectsCount_few": "{{count}} отслеживаемых объекта ", + "trackedObjectsCount_many": "{{count}} отслеживаемых объектов ", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Отслеживаемый объект успешно удалён.", + "error": "Не удалось удалить отслеживаемый объект: {{errorMessage}}" + } + }, + "tooltip": "Соответствие с {{type}} на {{confidence}}%", + "previousTrackedObject": "Предыдущий отслеживаемый объект", + "nextTrackedObject": "Следующий отслеживаемый объект" + }, + "exploreMore": "Просмотреть больше объектов {{label}}", + "aiAnalysis": { + "title": "Анализ при помощи ИИ" + }, + "concerns": { + "label": "Требуют внимания" + }, + "trackingDetails": { + "count": "{{first}} из {{second}}", + "title": "Детали отслеживания", + "noImageFound": "Для этой метки времени изображение не найдено.", + "createObjectMask": "Создать маску объекта", + "adjustAnnotationSettings": "Изменить настройки аннотаций", + "scrollViewTips": "Нажмите, чтобы просмотреть ключевые моменты жизненного цикла этого объекта.", + "autoTrackingTips": "Позиции ограничивающих рамок будут неточными для камер с автотрекингом.", + "trackedPoint": "Отслеживаемая точка", + "lifecycleItemDesc": { + "visible": "Обнаружен(а) {{label}}", + "entered_zone": "{{label}} зафиксирован(а) в {{zones}}", + "active": "{{label}} активировался(ась)", + "stationary": "{{label}} перестал(а) двигаться", + "attribute": { + "faceOrLicense_plate": "{{attribute}} обнаружен для {{label}}", + "other": "{{label}} распознан(а) как {{attribute}}" + }, + "gone": "{{label}} покинул(а) зону", + "heard": "Обнаружен звук {{label}}", + "external": "Обнаружен(а) {{label}}", + "header": { + "zones": "Зоны", + "ratio": "Соотношение", + "area": "Область", + "score": "Оценка" + } + }, + "annotationSettings": { + "title": "Настройки аннотаций", + "showAllZones": { + "title": "Показать все зоны", + "desc": "Всегда показывать зоны на кадрах, где объекты вошли в зону." + }, + "offset": { + "label": "Сдвиг аннотаций", + "desc": "Эти данные поступают из потока детекции вашей камеры, но накладываются на изображения из потока записи. Потоки вряд ли идеально синхронизированы, поэтому ограничивающая рамка и видео могут не совпадать. Вы можете использовать эту настройку для смещения аннотаций вперед или назад во времени, чтобы лучше выровнять их с записанным видео.", + "millisecondsToOffset": "Смещение аннотаций детекции в миллисекундах. По умолчанию: 0", + "tips": "Уменьшите значение, если воспроизведение видео опережает рамки и точки пути, и увеличьте значение, если воспроизведение видео отстаёт от них. Это значение может быть отрицательным.", + "toast": { + "success": "Смещение аннотаций для {{camera}} сохранено в конфигурационном файле." + } + } + }, + "carousel": { + "previous": "Предыдущий слайд", + "next": "Следующий слайд" + } + } +} diff --git a/web/public/locales/ru/views/exports.json b/web/public/locales/ru/views/exports.json new file mode 100644 index 0000000..70f8753 --- /dev/null +++ b/web/public/locales/ru/views/exports.json @@ -0,0 +1,39 @@ +{ + "documentTitle": "Экспорт - Frigate", + "search": "Поиск", + "noExports": "Не найдено файлов экспорта", + "deleteExport": { + "label": "Удалить экспорт" + }, + "deleteExport.desc": "Вы уверены, что хотите удалить {{exportName}}?", + "editExport": { + "title": "Переименовать экспорт", + "desc": "Введите новое имя для этого экспорта.", + "saveExport": "Сохранить экспорт" + }, + "toast": { + "error": { + "renameExportFailed": "Не удалось переименовать экспорт: {{errorMessage}}", + "assignCaseFailed": "Не удалось обновить назначение случая: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Поделиться экспортом", + "downloadVideo": "Скачать видео", + "editName": "Изменить название", + "deleteExport": "Удалить экспорт", + "assignToCase": "Добавить в случай" + }, + "headings": { + "cases": "Случаи", + "uncategorizedExports": "Некатегоризированные экспорты" + }, + "caseDialog": { + "title": "Добавить в случай", + "description": "Выберите существующий случай или создайте новый.", + "selectLabel": "Случай", + "newCaseOption": "Создать новый случай", + "nameLabel": "Название случая", + "descriptionLabel": "Описание" + } +} diff --git a/web/public/locales/ru/views/faceLibrary.json b/web/public/locales/ru/views/faceLibrary.json new file mode 100644 index 0000000..d3950b3 --- /dev/null +++ b/web/public/locales/ru/views/faceLibrary.json @@ -0,0 +1,105 @@ +{ + "details": { + "person": "Человек", + "timestamp": "Метка времени", + "face": "Подробности о лице", + "faceDesc": "Информация об отслеживаемом объекте, который сгенерировал это лицо", + "confidence": "Достоверность", + "scoreInfo": "Оценка доп. метки — это взвешенная оценка всех распознанных лиц, поэтому она может отличаться от оценки на снимке.", + "subLabelScore": "Оценка доп. метки", + "unknown": "Неизвестно" + }, + "documentTitle": "Библиотека лиц - Frigate", + "description": { + "placeholder": "Введите название коллекции", + "addFace": "Добавьте новую коллекцию в библиотеку лиц, загрузив свое первое изображение.", + "invalidName": "Недопустимое имя. Имена могут содержать только буквы, цифры, пробелы, апострофы, подчёркивания и дефисы.", + "nameCannotContainHash": "Имя не может содержать #." + }, + "createFaceLibrary": { + "desc": "Создание новой коллекции", + "nextSteps": "Для создания надежной базы:
  • Используйте вкладку \"Недавние распознавания\", чтобы выбрать изображения каждого обнаруженного человека и обучить систему
  • Используйте фронтальные изображения для лучшего результата; избегайте изображений с лицами, снятыми под углом.
  • ", + "title": "Создать коллекцию", + "new": "Создать новое лицо" + }, + "selectFace": "Выбор лица", + "uploadFaceImage": { + "desc": "Загрузите изображение для поиска лиц и связывания с {{pageToggle}}", + "title": "Загрузка изображения с лицом" + }, + "selectItem": "Выбор {{item}}", + "train": { + "aria": "Выберите последние распознавания", + "title": "Последние распознавания", + "empty": "Нет недавних попыток распознавания лиц", + "titleShort": "Недавнее" + }, + "toast": { + "success": { + "deletedFace_one": "Успешно удалено {{count}} лицо.", + "deletedFace_few": "Успешно удалено {{count}} лица.", + "deletedFace_many": "Успешно удалено {{count}} лиц.", + "deletedName_one": "{{count}} лицо успешно удалено.", + "deletedName_few": "{{count}} лица успешно удалено.", + "deletedName_many": "{{count}} лиц успешно удалено.", + "uploadedImage": "Изображение успешно загружено.", + "trainedFace": "Лицо успешно запомнено.", + "addFaceLibrary": "{{name}} успешно добавлен(а) в Библиотеку лиц!", + "updatedFaceScore": "Оценка лица успешно обновлена для {{name}} {{score}}.", + "renamedFace": "Лицо успешно переименовано в {{name}}" + }, + "error": { + "deleteFaceFailed": "Не удалось удалить: {{errorMessage}}", + "uploadingImageFailed": "Не удалось загрузить изображение: {{errorMessage}}", + "trainFailed": "Не удалось запомнить: {{errorMessage}}", + "updateFaceScoreFailed": "Не удалось обновить оценку лица: {{errorMessage}}", + "addFaceLibraryFailed": "Не удалось установить имя для лица: {{errorMessage}}", + "deleteNameFailed": "Не удалось удалить имя: {{errorMessage}}", + "renameFaceFailed": "Не удалось переименовать лицо: {{errorMessage}}" + } + }, + "deleteFaceLibrary": { + "title": "Удалить имя", + "desc": "Вы уверены, что хотите удалить коллекцию «{{name}}»? Это действие безвозвратно удалит все лица в коллекции." + }, + "imageEntry": { + "dropActive": "Перетащите изображение сюда…", + "dropInstructions": "Перетащите или вставьте изображение сюда или щелкните, чтобы выбрать", + "maxSize": "Макс. размер: {{size}}Мб", + "validation": { + "selectImage": "Пожалуйста, выберите файл изображения." + } + }, + "readTheDocs": "Читать документацию", + "trainFaceAs": "Запомнить лицо как:", + "button": { + "uploadImage": "Загрузить изображение", + "deleteFaceAttempts": "Удалить лица", + "addFace": "Добавить лицо", + "reprocessFace": "Обработать лицо повторно", + "renameFace": "Переименовать лицо", + "deleteFace": "Удалить лицо" + }, + "trainFace": "Запомнить лицо", + "steps": { + "faceName": "Введите имя лица", + "nextSteps": "Следующие шаги", + "uploadFace": "Загрузить изображение лица", + "description": { + "uploadFace": "Загрузите изображение {{name}}, на котором лицо показано спереди. Не нужно обрезать фотографию только до лица." + } + }, + "renameFace": { + "desc": "Введите новое имя для {{name}}", + "title": "Переименовать лицо" + }, + "collections": "Коллекции", + "deleteFaceAttempts": { + "title": "Удалить лица", + "desc_one": "Вы уверены, что хотите удалить {{count}} лицо? Это действие нельзя отменить.", + "desc_few": "Вы уверены, что хотите удалить {{count}} лица? Это действие нельзя отменить.", + "desc_many": "Вы уверены, что хотите удалить {{count}} лиц? Это действие нельзя отменить." + }, + "nofaces": "Лица отсутствуют", + "pixels": "{{area}} пикс" +} diff --git a/web/public/locales/ru/views/live.json b/web/public/locales/ru/views/live.json new file mode 100644 index 0000000..437a44a --- /dev/null +++ b/web/public/locales/ru/views/live.json @@ -0,0 +1,202 @@ +{ + "documentTitle": { + "default": "Прямой эфир - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Прямой эфир - Frigate", + "lowBandwidthMode": "Экономичный режим", + "twoWayTalk": { + "enable": "Включить двустороннюю связь", + "disable": "Отключить двустороннюю связь" + }, + "cameraAudio": { + "enable": "Включить звук с камеры", + "disable": "Отключить звук с камеры" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Кликните в кадре для центрирования камеры", + "enable": "Включить перемещение по клику", + "disable": "Отключить перемещение по клику", + "enableWithZoom": "Включить \"клик для перемещения / перетащить для масштабирования\"" + }, + "left": { + "label": "Переместить PTZ-камеру влево" + }, + "down": { + "label": "Переместить PTZ-камеру вниз" + }, + "up": { + "label": "Переместить PTZ-камеру вверх" + }, + "right": { + "label": "Переместить PTZ-камеру вправо" + } + }, + "zoom": { + "in": { + "label": "Приблизить PTZ-камеру" + }, + "out": { + "label": "Отдалить PTZ-камеру" + } + }, + "frame": { + "center": { + "label": "Кликните в кадре для центрирования PTZ-камеры" + } + }, + "presets": "Предустановки PTZ-камеры", + "focus": { + "in": { + "label": "Сфокусировать PTZ камеру на" + }, + "out": { + "label": "Отдалить фокус PTZ камеры" + } + } + }, + "camera": { + "enable": "Включить камеру", + "disable": "Отключить камеру" + }, + "muteCameras": { + "enable": "Отключить звук на всех камерах", + "disable": "Включить звук на всех камерах" + }, + "detect": { + "enable": "Включить детекцию", + "disable": "Отключить детекцию" + }, + "recording": { + "enable": "Включить запись", + "disable": "Отключить запись" + }, + "snapshots": { + "enable": "Включить снимки", + "disable": "Отключить снимки" + }, + "audioDetect": { + "enable": "Включить детекцию аудио", + "disable": "Отключить детекцию аудио" + }, + "autotracking": { + "enable": "Включить автотрекинг", + "disable": "Отключить автотрекинг" + }, + "streamStats": { + "enable": "Показать статистику потока", + "disable": "Скрыть статистику потока" + }, + "manualRecording": { + "title": "По требованию", + "tips": "Скачать моментальный снимок или создать ручное событие, исходя из настроек хранения записей для этой камеры.", + "playInBackground": { + "label": "Воспроизведение в фоне", + "desc": "Включите эту опцию, чтобы продолжать трансляцию при скрытом плеере." + }, + "showStats": { + "label": "Показать статистику", + "desc": "Включите эту опцию, чтобы отображать статистику потока в виде наложения на изображение с камеры." + }, + "debugView": "Режим отладки", + "start": "Запустить запись по запросу", + "started": "Запущена запись по запросу.", + "failedToStart": "Не удалось запустить запись по требованию.", + "recordDisabledTips": "Поскольку запись отключена или ограничена в конфигурации для этой камеры, будет сохранён только снимок.", + "end": "Завершить запись по требованию", + "ended": "Запись по требованию остановлена.", + "failedToEnd": "Не удалось остановить запись по требованию." + }, + "streamingSettings": "Настройки потока", + "suspend": { + "forTime": "Приостановить на: " + }, + "stream": { + "audio": { + "tips": { + "documentation": "Читать документацию ", + "title": "Аудио должно выводиться с вашей камеры и быть настроено в go2rtc для этого потока." + }, + "available": "Для этого потока доступен звук", + "unavailable": "Аудио недоступно для этого потока" + }, + "title": "Поток", + "twoWayTalk": { + "tips": "Ваше устройство должно поддерживать эту функцию, а WebRTC должен быть настроен для двусторонней связи.", + "tips.documentation": "Читать документацию ", + "available": "Двусторонняя связь доступна для этого потока", + "unavailable": "Двусторонняя связь недоступна для этого потока" + }, + "lowBandwidth": { + "tips": "Режим просмотра в реальном времени переведён в экономичный режим из-за буферизации или ошибок потока.", + "resetStream": "Сброс потока" + }, + "playInBackground": { + "label": "Воспроизвести в фоне", + "tips": "Включите эту опцию, чтобы продолжать трансляцию при скрытом плеере." + }, + "debug": { + "picker": "Выбор потока недоступен в режиме отладки. В отладочном представлении всегда используется поток, назначенный на роль обнаружения." + } + }, + "cameraSettings": { + "title": "Настройки {{camera}}", + "objectDetection": "Обнаружение объектов", + "recording": "Запись", + "audioDetection": "Детекция аудио", + "snapshots": "Снимки", + "autotracking": "Автотрекинг", + "cameraEnabled": "Камера активирована", + "transcription": "Транскрипция аудио" + }, + "history": { + "label": "Отобразить архивные записи" + }, + "effectiveRetainMode": { + "modes": { + "all": "Все", + "motion": "Движение", + "active_objects": "Активные объекты" + }, + "notAllTips": "Ваша конфигурация хранения записей {{source}} установлена в mode: {{effectiveRetainMode}}, поэтому эта запись по запросу будет сохранять только сегменты с {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Редактировать макет", + "group": { + "label": "Редактирование группы камер" + }, + "exitEdit": "Выход из редактирования" + }, + "audio": "Аудио", + "notifications": "Уведомления", + "transcription": { + "enable": "Включить транскрипцию звука в реальном времени", + "disable": "Выключить транскрипцию звука" + }, + "snapshot": { + "noVideoSource": "Нет видеоисточника для снимка.", + "captureFailed": "Не удалось сделать снимок.", + "takeSnapshot": "Скачать моментальный снимок", + "downloadStarted": "Загрузка снимка началась." + }, + "noCameras": { + "title": "Камеры не настроены", + "description": "Начните с подключения камеры к Frigate.", + "buttonText": "Добавить камеру", + "restricted": { + "title": "Нет доступных камер", + "description": "У вас нет разрешения на просмотр камер в этой группе." + }, + "default": { + "title": "Камеры не настроены", + "description": "Начните с подключения камеры к Frigate.", + "buttonText": "Добавить камеру" + }, + "group": { + "title": "В группе нет камер", + "description": "В этой группе камер нет назначенных или включенных камер.", + "buttonText": "Управление группами" + } + } +} diff --git a/web/public/locales/ru/views/motionSearch.json b/web/public/locales/ru/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ru/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ru/views/recording.json b/web/public/locales/ru/views/recording.json new file mode 100644 index 0000000..24d34f5 --- /dev/null +++ b/web/public/locales/ru/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Фильтр", + "export": "Экспорт", + "calendar": "Календарь", + "filters": "Фильтры", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Конечное время должно быть позже начального", + "noValidTimeSelected": "Выбран недопустимый временной диапазон" + } + } +} diff --git a/web/public/locales/ru/views/replay.json b/web/public/locales/ru/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ru/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ru/views/search.json b/web/public/locales/ru/views/search.json new file mode 100644 index 0000000..cf90fb1 --- /dev/null +++ b/web/public/locales/ru/views/search.json @@ -0,0 +1,75 @@ +{ + "savedSearches": "Сохраненные поиски", + "button": { + "clear": "Очистить поиск", + "save": "Сохранить поиск", + "delete": "Удалить сохранённый поиск", + "filterActive": "Активные фильтры", + "filterInformation": "Информация о фильтре" + }, + "search": "Поиск", + "searchFor": "Поиск {{inputValue}}", + "trackedObjectId": "ID отслеживаемого объекта", + "filter": { + "label": { + "cameras": "Камеры", + "zones": "Зоны", + "sub_labels": "Дополнительные метки", + "search_type": "Тип поиска", + "time_range": "Временной диапазон", + "before": "До", + "after": "После", + "min_score": "Мин. оценка", + "max_score": "Макс. оценка", + "min_speed": "Мин. скорость", + "recognized_license_plate": "Распознанный номерной знак", + "max_speed": "Макс. скорость", + "has_clip": "Есть клип", + "has_snapshot": "Есть снимок", + "labels": "Метки", + "attributes": "Атрибуты" + }, + "searchType": { + "thumbnail": "Миниатюра", + "description": "Описание" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Дата 'до' должна быть позже, чем дата 'после'.", + "afterDatebeEarlierBefore": "Дата 'после' должна быть раньше, чем дата 'до'.", + "minScoreMustBeLessOrEqualMaxScore": "Значение 'min_score' должно быть меньше или равно значению 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Значение 'max_score' должно быть больше или равно значению 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Значение 'min_speed' должно быть меньше или равно значению 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Значение 'max_speed' должно быть больше или равно значению 'min_speed'." + } + }, + "tips": { + "title": "Как использовать текстовые фильтры", + "desc": { + "text": "Фильтры помогают уточнить результаты поиска. Вот как их использовать в поле ввода:", + "step": "
    • Введите название фильтра, затем двоеточие (например, \"камеры:\").
    • Выберите значение из подсказок или введите своё.
    • Используйте несколько фильтров, добавляя их через пробел.
    • Фильтры даты (before:/after:) используют формат {{DateFormat}}.
    • Временной диапазон — в формате {{exampleTime}}.
    • Удаляйте фильтры нажатием на «×» рядом с ними.
    ", + "example": "Пример: cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "step1": "Введите имя ключа фильтра с двоеточием (например, \"камеры:\").", + "step5": "Фильтр временного диапазона использует формат {{exampleTime}}.", + "exampleLabel": "Пример:", + "step2": "Выберите значение из предложенных или введите свое собственное.", + "step3": "Вы можете применять несколько фильтров, указывая их подряд через пробел.", + "step6": "Удаляйте фильтры, нажав на значок \"x\" рядом с ними.", + "step4": "Фильтры по дате (до: и после:) используют формат {{DateFormat}}." + } + }, + "header": { + "currentFilterType": "Значения фильтров", + "noFilters": "Фильтры", + "activeFilters": "Активные фильтры" + } + }, + "similaritySearch": { + "title": "Поиск похожего", + "active": "Активен поиск похожего", + "clear": "Очистить поиск похожего" + }, + "placeholder": { + "search": "Поиск…" + } +} diff --git a/web/public/locales/ru/views/settings.json b/web/public/locales/ru/views/settings.json new file mode 100644 index 0000000..cee2a74 --- /dev/null +++ b/web/public/locales/ru/views/settings.json @@ -0,0 +1,1336 @@ +{ + "documentTitle": { + "default": "Настройки - Frigate", + "camera": "Настройки камеры - Frigate", + "masksAndZones": "Маски и Зоны - Frigate", + "motionTuner": "Детекции движения - Frigate", + "general": "Настройки интерфейса - Frigate", + "frigatePlus": "Настройки Frigate+ - Frigate", + "authentication": "Настройки аутентификации - Frigate", + "classification": "Настройки распознавания - Frigate", + "object": "Отладка - Frigate", + "notifications": "Настройки уведомлений - Frigate", + "enrichments": "Настройки обогащения - Frigate", + "cameraManagement": "Управление камерами - Frigate", + "cameraReview": "Настройки просмотра камеры - Frigate", + "globalConfig": "Глобальная конфигурация - Frigate", + "cameraConfig": "Настройки камеры - Frigate", + "maintenance": "Обслуживание - Frigate", + "profiles": "Профили - Frigate", + "detectorsAndModel": "Обнаружения и модель - Frigate" + }, + "menu": { + "cameras": "Настройки камеры", + "masksAndZones": "Маски / Зоны", + "motionTuner": "Детекции движения", + "debug": "Отладка", + "users": "Пользователи", + "notifications": "Уведомления", + "frigateplus": "Frigate+", + "ui": "Интерфейс", + "classification": "Распознавание", + "enrichments": "Обогащения", + "triggers": "Триггеры", + "cameraManagement": "Управление", + "cameraReview": "Обзор", + "roles": "Роли", + "general": "Общее", + "globalConfig": "Глобальная конфигурация", + "system": "Система", + "integrations": "Интеграции", + "uiSettings": "Настройки интерфейса", + "profiles": "Профили", + "globalDetect": "Обнаружение объектов", + "globalReview": "Обзор событий", + "globalAudioEvents": "Обнаружение аудио", + "systemAuthentication": "Аутентификация", + "systemNetworking": "Сеть", + "systemProxy": "Прокси", + "systemUi": "Интерфейс", + "systemLogging": "Логирование", + "systemEnvironmentVariables": "Переменные окружения", + "systemTelemetry": "Телеметрия", + "systemBirdseye": "Режим Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "Детекторы и модель", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "потоки go2rtc", + "integrationSemanticSearch": "Семантический поиск", + "integrationGenerativeAi": "Генеративный ИИ", + "integrationFaceRecognition": "Распознавание лиц", + "integrationLpr": "Распознавание номерных знаков", + "integrationObjectClassification": "Классификация объектов", + "integrationAudioTranscription": "Расшифровка аудиозаписи", + "cameraDetect": "Обнаружение объектов", + "cameraFfmpeg": "Потоки (FFmpeg)", + "cameraRecording": "Запись", + "cameraSnapshots": "Снимки", + "cameraMotion": "Обнаружение движения", + "cameraObjects": "Объекты", + "cameraConfigReview": "Обзор событий", + "cameraAudioEvents": "Обнаружение аудио", + "cameraAudioTranscription": "Расшифровка аудиозаписи", + "cameraNotifications": "Уведомления" + }, + "dialog": { + "unsavedChanges": { + "title": "У вас есть несохраненные изменения.", + "desc": "Хотите сохранить изменения перед продолжением?" + } + }, + "cameraSetting": { + "camera": "Камера", + "noCamera": "Нет камеры" + }, + "general": { + "title": "Настройки интерфейса", + "liveDashboard": { + "title": "Панель мониторинга", + "automaticLiveView": { + "desc": "Автоматически переключаться на просмотр камеры в реальном времени при обнаружении активности. Если отключить эту опцию, статичные изображения камер на панели мониторинга будут обновляться только раз в минуту.", + "label": "Автоматический просмотр в реальном времени" + }, + "playAlertVideos": { + "label": "Воспроизводить видео с тревогами", + "desc": "По умолчанию последние тревоги на панели мониторинга воспроизводятся как короткие зацикленные видео. Отключите эту опцию, чтобы показывать только статичное изображение последних оповещений на этом устройстве/браузере." + }, + "displayCameraNames": { + "label": "Всегда показывать названия камер", + "desc": "Всегда показывать названия камер в виде метки на панели мониторинга с несколькими камерами." + }, + "liveFallbackTimeout": { + "label": "Таймаут переключения на низкое качество", + "desc": "Когда высококачественный поток камеры недоступен, переключиться на режим низкой пропускной способности через указанное количество секунд. По умолчанию: 3." + } + }, + "calendar": { + "title": "Календарь", + "firstWeekday": { + "sunday": "Воскресенье", + "monday": "Понедельник", + "label": "Первый день недели", + "desc": "День, с которого начинаются недели в календаре обзора событий." + } + }, + "recordingsViewer": { + "title": "Просмотр записей", + "defaultPlaybackRate": { + "label": "Скорость воспроизведения по умолчанию", + "desc": "Скорость воспроизведения записей по умолчанию." + } + }, + "storedLayouts": { + "clearAll": "Сбросить все макеты", + "desc": "Расположение камер в группе можно настраивать перетаскиванием и изменением размера. Позиции сохраняются в локальном хранилище браузера.", + "title": "Сохранённые макеты" + }, + "cameraGroupStreaming": { + "title": "Настройки трансляции группы камер", + "desc": "Настройки трансляции для каждой группы камер хранятся локально в вашем браузере.", + "clearAll": "Очистить все настройки трансляции" + }, + "toast": { + "success": { + "clearStoredLayout": "Сохранённый макет для {{cameraName}} удалён", + "clearStreamingSettings": "Настройки потоков для всех групп камер сброшены." + }, + "error": { + "clearStoredLayoutFailed": "Не удалось удалить макет: {{errorMessage}}", + "clearStreamingSettingsFailed": "Не удалось очистить настройки потока: {{errorMessage}}" + } + } + }, + "classification": { + "semanticSearch": { + "title": "Семантический поиск", + "readTheDocumentation": "Читать документацию", + "reindexNow": { + "label": "Переиндексировать сейчас", + "confirmButton": "Переиндексировать", + "alreadyInProgress": "Переиндексация уже выполняется.", + "desc": "Переиндексация заново сгенерирует векторные представления для всех отслеживаемых объектов. Этот процесс выполняется в фоновом режиме и может максимально загрузить ваш процессор, а также занять значительное время в зависимости от количества отслеживаемых объектов.", + "confirmTitle": "Подтвердить переиндексацию", + "success": "Реиндексация запущена успешно.", + "error": "Не удалось начать реиндексацию: {{errorMessage}}", + "confirmDesc": "Вы уверены, что хотите переиндексировать все векторные представления отслеживаемых объектов? Этот процесс будет выполняться в фоновом режиме, но может максимально загрузить ваш процессор и занять довольно много времени. Вы можете следить за ходом выполнения на странице «Поиск событий»." + }, + "desc": "Семантический поиск во Frigate позволяет находить отслеживаемые объекты в записях с помощью самого изображения, пользовательского текстового описания или автоматически сгенерированного описания.", + "modelSize": { + "label": "Размер модели", + "desc": "Размер модели, используемой для создания векторных представлений для семантического поиска.", + "small": { + "title": "малый", + "desc": "Использование малой модели задействует квантованную версию модели, которая потребляет меньше оперативной памяти и работает быстрее на CPU с очень незначительной разницей в качестве эмбеддингов." + }, + "large": { + "title": "большой", + "desc": "Использование большой модели задействует полную модель Jina и автоматически запускается на GPU, если это возможно." + } + } + }, + "faceRecognition": { + "desc": "Функция распознавания лиц позволяет присваивать людям имена, и когда их лицо будет распознано, Frigate присвоит имя человека в качестве дополнительной метки. Эта информация содержится в пользовательском интерфейсе, фильтрах, а также в уведомлениях.", + "title": "Распознавание лиц", + "readTheDocumentation": "Читать документацию", + "modelSize": { + "label": "Размер модели", + "desc": "Размер модели, используемой для распознавания лиц.", + "small": { + "title": "малый", + "desc": "Использование малой модели задействует модель FaceNet для векторного представления лиц, которая эффективно работает на большинстве CPU." + }, + "large": { + "title": "большой", + "desc": "При выборе большой модели используется модель векторизации лиц ArcFace, которая автоматически задействует GPU (если он доступен)." + } + } + }, + "licensePlateRecognition": { + "title": "Распознавание номерных знаков", + "readTheDocumentation": "Читать документацию", + "desc": "Frigate может распознавать автомобильные номера и автоматически добавлять для объектов типа «автомобиль» обнаруженные символы в поле «распознанный номерной знак» или известное имя в качестве дополнительной метки. Типичный пример использования — чтение номеров автомобилей, заезжающих на подъездную дорожку или проезжающих по улице." + }, + "toast": { + "success": "Настройки классификации сохранены. Перезапустите Frigate, чтобы применить внесенные изменения.", + "error": "Не удалось сохранить изменения конфигурации: {{errorMessage}}" + }, + "title": "Настройки классификации", + "birdClassification": { + "title": "Классификация птиц", + "desc": "Классификация птиц определяет известные виды с помощью квантованной модели TensorFlow. Когда птица распознана, её обиходное название добавляется в качестве дополнительной метки. Эти информация используется в интерфейсе, фильтрах и уведомлениях." + }, + "restart_required": "Требуется перезапуск (изменены настройки классификации)", + "unsavedChanges": "Настройки классификации не сохранены" + }, + "users": { + "dialog": { + "passwordSetting": { + "updatePassword": "Обновить пароль для {{username}}", + "setPassword": "Установить пароль", + "desc": "Создайте надежный пароль для защиты аккаунта.", + "cannotBeEmpty": "Пароль не может быть пустым", + "doNotMatch": "Пароли не совпадают", + "currentPasswordRequired": "Текущий пароль обязателен", + "incorrectCurrentPassword": "Текущий пароль указан неверно", + "passwordVerificationFailed": "Не удалось проверить пароль", + "multiDeviceWarning": "Все остальные устройства, на которых вы вошли в систему, потребуют повторного входа в течение {{refresh_time}}.", + "multiDeviceAdmin": "Вы также можете принудительно заставить всех пользователей повторно пройти аутентификацию немедленно, обновив свой JWT-секрет." + }, + "deleteUser": { + "warn": "Вы уверены, что хотите удалить пользователя {{username}}?", + "title": "Удалить пользователя", + "desc": "Это действие необратимо. Учётная запись пользователя и все связанные с ней данные будут удалены без возможности восстановления." + }, + "changeRole": { + "title": "Изменить роль пользователя", + "desc": "Обновить права доступа для {{username}}", + "roleInfo": { + "intro": "Выберите подходящую роль для этого пользователя:", + "viewer": "Наблюдатель", + "viewerDesc": "Доступны только панель мониторинга, обзор событий, поиск и экспорт данных.", + "admin": "Администратор", + "adminDesc": "Полный доступ ко всем функциям.", + "customDesc": "Роль с настраиваемыми правами доступа к определённым камерам." + }, + "select": "Выбрать роль" + }, + "form": { + "user": { + "placeholder": "Введите имя пользователя", + "desc": "Допустимо использовать только буквы, цифры, точки и подчёркивания.", + "title": "Имя пользователя" + }, + "password": { + "title": "Пароль", + "placeholder": "Введите пароль", + "confirm": { + "title": "Подтвердите пароль", + "placeholder": "Подтвердите пароль" + }, + "strength": { + "title": "Сложность пароля: ", + "weak": "Слабый", + "medium": "Средний", + "strong": "Сложный", + "veryStrong": "Очень сложный" + }, + "match": "Пароли совпадают", + "notMatch": "Пароли не совпадают", + "show": "Показать пароль", + "hide": "Скрыть пароль", + "requirements": { + "title": "Требования к паролю:", + "length": "Не менее 8 символов", + "uppercase": "Как минимум одна заглавная буква", + "digit": "Как минимум одна цифра", + "special": "Хотя бы один специальный символ (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Новый пароль", + "confirm": { + "placeholder": "Повторно введите новый пароль" + }, + "placeholder": "Введите новый пароль" + }, + "usernameIsRequired": "Необходимо ввести имя пользователя", + "passwordIsRequired": "Требуется пароль", + "currentPassword": { + "title": "Текущий пароль", + "placeholder": "Введите ваш текущий пароль" + } + }, + "createUser": { + "title": "Создать нового пользователя", + "usernameOnlyInclude": "Имя пользователя может включать только буквы, цифры, . или _", + "desc": "Добавить новую учетную запись пользователя и определить роль для доступа к разделам интерфейса Frigate.", + "confirmPassword": "Пожалуйста, подтвердите пароль" + } + }, + "title": "Пользователи", + "toast": { + "success": { + "roleUpdated": "Обновлена роль для {{user}}", + "createUser": "Пользователь {{user}} успешно создан", + "deleteUser": "Пользователь {{user}} успешно удалён", + "updatePassword": "Пароль успешно обновлён." + }, + "error": { + "setPasswordFailed": "Не удалось сохранить пароль: {{errorMessage}}", + "createUserFailed": "Не удалось создать пользователя: {{errorMessage}}", + "deleteUserFailed": "Не удалось удалить пользователя: {{errorMessage}}", + "roleUpdateFailed": "Не удалось обновить роль: {{errorMessage}}" + } + }, + "table": { + "username": "Имя пользователя", + "actions": "Действия", + "password": "Сбросить пароль", + "noUsers": "Пользователей не найдено.", + "changeRole": "Изменить роль пользователя", + "role": "Роль", + "deleteUser": "Удалить пользователя" + }, + "management": { + "title": "Управление пользователями", + "desc": "Управление учетными записями пользователей Frigate." + }, + "updatePassword": "Сбросить пароль", + "addUser": "Добавить пользователя" + }, + "notification": { + "title": "Уведомления", + "notificationSettings": { + "documentation": "Читать документацию", + "title": "Настройки уведомлений", + "desc": "Frigate может отправлять push-уведомления на ваше устройство, когда приложение открыто в браузере или установлено как PWA." + }, + "notificationUnavailable": { + "documentation": "Читать документацию", + "title": "Уведомления недоступны", + "desc": "Веб-уведомления требуют защищённого контекста (https://…). Это ограничение браузера. Получите безопасный доступ к Frigate, чтобы использовать уведомления." + }, + "email": { + "title": "Email", + "desc": "Для уведомлений о проблемах с push-сервисом требуется указать действующий адрес электронной почты.", + "placeholder": "например, example@email.com" + }, + "globalSettings": { + "title": "Глобальные настройки", + "desc": "Временно приостановить уведомления для определённых камер на всех зарегистрированных устройствах." + }, + "cameras": { + "title": "Камеры", + "noCameras": "Нет доступных камер", + "desc": "Выберите камеры для активации уведомлений." + }, + "deviceSpecific": "Настройки для конкретного устройства", + "registerDevice": "Зарегистрировать это устройство", + "unregisterDevice": "Отменить регистрацию этого устройства", + "suspended": "Уведомления приостановлены {{time}}", + "sendTestNotification": "Отправить тестовое уведомление", + "active": "Уведомления активны", + "suspendTime": { + "30minutes": "Приостановить на 30 минут", + "1hour": "Приостановить на 1 час", + "12hours": "Приостановить на 12 часов", + "24hours": "Приостановить на 24 часа", + "untilRestart": "Приостановить до перезапуска", + "5minutes": "Приостановить на 5 минут", + "10minutes": "Приостановить на 10 минут", + "suspend": "Приостановить" + }, + "toast": { + "success": { + "settingSaved": "Настройки уведомлений сохранены.", + "registered": "Регистрация для уведомлений успешно завершена. Перезапуск Frigate необходим перед отправкой любых уведомлений (включая тестовое уведомление)." + }, + "error": { + "registerFailed": "Не удалось сохранить регистрацию уведомлений." + } + }, + "cancelSuspension": "Отменить приостановку", + "unsavedChanges": "Изменения уведомлений не сохранены", + "unsavedRegistrations": "Регистрации уведомлений не сохранены" + }, + "camera": { + "review": { + "alerts": "Тревоги ", + "desc": "Временно включить/отключить тревоги и обнаружения для этой камеры до перезапуска Frigate. В отключенном состоянии новые события не будут записываться. ", + "detections": "Обнаружения ", + "title": "Обзор событий" + }, + "reviewClassification": { + "objectAlertsTips": "Все объекты {{alertsLabels}} на камере {{cameraName}} будут отображаться как тревоги.", + "desc": "Frigate разделяет записи для проверки на два типа как «Тревоги» и «Обнаружения». По умолчанию все объекты person и car считаются тревогами. Вы можете уточнить эту классификацию, настроив для них требуемые зоны.", + "selectAlertsZones": "Выберите зоны для тревог", + "zoneObjectDetectionsTips": { + "notSelectDetections": "Все объекты {{detectionsLabels}}, обнаруженные в {{zone}} на камере {{cameraName}}, которые не отнесены к тревогам, будут отображаться как обнаружения, независимо от того, в какой зоне они находятся.", + "text": "Все объекты {{detectionsLabels}}, не отнесённые к категории в {{zone}} на камере {{cameraName}}, будут отображаться как обнаружения.", + "regardlessOfZoneObjectDetectionsTips": "Все объекты {{detectionsLabels}}, не отнесённые к категории на камере {{cameraName}}, будут отображаться как обнаружения, независимо от того, в какой зоне они находятся." + }, + "zoneObjectAlertsTips": "Все объекты {{alertsLabels}}, обнаруженные в {{zone}} на камере {{cameraName}}, будут отображаться как тревоги.", + "selectDetectionsZones": "Выберите зоны для обнаружения", + "noDefinedZones": "Для этой камеры не определено ни одной зоны.", + "objectDetectionsTips": "Все объекты {{detectionsLabels}}, не отнесённые к категории на камере {{cameraName}}, будут отображаться как обнаружения, независимо от того, в какой зоне они находятся.", + "title": "Классификация событий", + "readTheDocumentation": "Читать документацию", + "limitDetections": "Ограничение обнаружения отдельными зонами", + "toast": { + "success": "Конфигурация классификации событий была сохранена. Перезапустите Frigate для применения изменений." + }, + "unsavedChanges": "Настройки классификации событий для {{camera}} не сохранены" + }, + "title": "Настройки камеры", + "streams": { + "title": "Потоки", + "desc": "Временно отключить камеру до перезапуска Frigate. Отключение камеры полностью останавливает обработку потоков этой камеры в Frigate. Обнаружение, запись и отладка будут недоступны.
    Примечание: Это не отключает рестриминг go2rtc." + }, + "object_descriptions": { + "title": "Сгенерировать описания объектов при помощи ИИ", + "desc": "Временно включить/отключить описание объектов при помощи генеративного ИИ для этой камеры. При отключении описания, описание объектов при помощи генеративного ИИ не будут запрашиваться для отслеживаемых объектов на этой камере." + }, + "review_descriptions": { + "title": "Описания обзоров генеративного ИИ", + "desc": "Временно включить/отключить описания обзоров с помощью генеративного ИИ для этой камеры. Если отключено, описания, описания обзоров с помощью генеративного ИИ, не будут запрашиваться для элементов обзора для этой камеры." + }, + "addCamera": "Добавить новую камеру", + "editCamera": "Редактировать камеру:", + "selectCamera": "Выбрать камеру", + "backToSettings": "Вернуться к настройкам камеры", + "cameraConfig": { + "add": "Добавить камеру", + "edit": "Редактировать камеру", + "description": "Настройте параметры камеры, включая входные трансляции и роли.", + "name": "Название камеры", + "nameRequired": "Требуется имя камеры", + "nameInvalid": "Имя камеры должно содержать только буквы, цифры, подчеркивания или дефисы", + "namePlaceholder": "например, front_door", + "enabled": "Включено", + "ffmpeg": { + "inputs": "Входные трансляции", + "path": "Путь трансляции", + "pathRequired": "Требуется путь трансляции", + "pathPlaceholder": "rtsp://...", + "roles": "Роли", + "rolesRequired": "Требуется хотя бы одна роль", + "rolesUnique": "Каждая роль (аудио, обнаружение, запись) может быть назначена только одной трансляции", + "addInput": "Добавить входной поток", + "removeInput": "Удалить входной поток", + "inputsRequired": "Требуется хотя бы 1 входной поток" + }, + "toast": { + "success": "Камера {{cameraName}} успешно сохранена" + }, + "nameLength": "Название камеры должно содержать не более 24 символов." + } + }, + "masksAndZones": { + "zones": { + "objects": { + "title": "Объекты", + "desc": "Список объектов, применяемых к этой зоне." + }, + "speedEstimation": { + "desc": "Включить оценку скорости объектов в этой зоне. Зона должна состоять ровно из 4 точек.", + "title": "Расчёт скорости", + "docs": "Читать документацию", + "lineBDistance": "Длина линии B ({{unit}})", + "lineADistance": "Длина линии A ({{unit}})", + "lineCDistance": "Длина линии C ({{unit}})", + "lineDDistance": "Длина линии D ({{unit}})" + }, + "label": "Зоны", + "documentTitle": "Редактирование зоны - Frigate", + "desc": { + "title": "Зоны позволяют определить конкретную область кадра, чтобы можно было определить, находится ли объект в заданной области.", + "documentation": "Документация" + }, + "add": "Добавить зону", + "edit": "Редактировать зону", + "point_one": "{{count}} точка", + "point_few": "{{count}} точки", + "point_many": "{{count}} точек", + "clickDrawPolygon": "Кликните, чтобы нарисовать полигон на изображении.", + "name": { + "title": "Название", + "inputPlaceHolder": "Введите название…", + "tips": "Имя должно содержать не менее 2 символов, включать хотя бы одну букву и не должно совпадать с названием камеры или другой зоны на этой камере." + }, + "inertia": { + "title": "Инерция", + "desc": "Указывает, сколько кадров объект должен находиться в зоне, прежде чем он будет считаться находящимся в ней. Значение по умолчанию: 3" + }, + "loiteringTime": { + "title": "Время присутствия", + "desc": "Устанавливает минимальное время в секундах, которое объект должен находиться в зоне для её активации. Значение по умолчанию: 0" + }, + "allObjects": "Все объекты", + "speedThreshold": { + "title": "Предел скорости ({{unit}})", + "toast": { + "error": { + "loiteringTimeError": "Зоны с установленным временем присутствия более 0 не должны использоваться для вычисления скорости.", + "pointLengthError": "Расчёт скорости отключён для этой зоны. Зоны с расчётом скорости должны содержать ровно 4 точки." + } + }, + "desc": "Задаёт минимальную скорость объектов для учёта в этой зоне." + }, + "toast": { + "success": "Зона ({{zoneName}}) сохранена." + } + }, + "motionMasks": { + "desc": { + "documentation": "Документация", + "title": "Маски движения используются, чтобы предотвратить срабатывание обнаружений на нежелательные типы движения. Чрезмерная маскировка усложняет отслеживание объектов." + }, + "add": "Новая маска движения", + "edit": "Редактировать маску движения", + "context": { + "documentation": "Читать документацию", + "title": "Маски движения используются, чтобы предотвратить срабатывание обнаружений на нежелательные типы движения (например, ветки деревьев, метки времени на камере). При этом маски движения нужно использовать очень умеренно: чрезмерное применение масок затруднит отслеживание объектов." + }, + "clickDrawPolygon": "Нажмите, чтобы нарисовать полигон на изображении.", + "polygonAreaTooLarge": { + "documentation": "Читать документацию", + "title": "Маска движения покрывает {{polygonArea}}% кадра. Большие маски движения не рекомендуются.", + "tips": "Маски движения не предотвращают обнаружение объектов. Вместо этого следует использовать обязательную зону." + }, + "point_one": "{{count}} точка", + "point_few": "{{count}} точки", + "point_many": "{{count}} точек", + "label": "Маска движения", + "documentTitle": "Редактирование маски движения - Frigate", + "toast": { + "success": { + "title": "{{polygonName}} сохранена.", + "noName": "Маска движения сохранена." + } + } + }, + "filter": { + "all": "Все маски и зоны" + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Имя зоны должно содержать не менее 2 символов.", + "mustNotBeSameWithCamera": "Имя зоны не должно совпадать с именем камеры.", + "hasIllegalCharacter": "Имя зоны содержит недопустимые символы.", + "alreadyExists": "Зона с таким именем уже существует для этой камеры.", + "mustNotContainPeriod": "Имя зоны не должно содержать точки.", + "mustHaveAtLeastOneLetter": "Название зоны должно содержать хотя бы одну букву." + } + }, + "distance": { + "error": { + "text": "Расстояние должно быть больше или равно 0.1.", + "mustBeFilled": "Все поля расстояния должны быть заполнены для расчёта скорости." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Инерция должна быть больше 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Время присутствия должно быть больше или равно 0." + } + }, + "polygonDrawing": { + "removeLastPoint": "Удалить последнюю точку", + "error": { + "mustBeFinished": "Рисование полигона должно быть завершено перед сохранением." + }, + "delete": { + "success": "{{name}} удалён.", + "title": "Подтвердить удаление", + "desc": "Вы уверены, что хотите удалить {{type}} {{name}}?" + }, + "snapPoints": { + "false": "Не привязывать к точкам", + "true": "Привязать точки" + }, + "reset": { + "label": "Удалить все точки" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Порог скорости должен быть не меньше 0,1." + } + } + }, + "toast": { + "error": { + "copyCoordinatesFailed": "Не удалось скопировать координаты в буфер обмена." + }, + "success": { + "copyCoordinates": "Координаты {{polyName}} скопированы в буфер обмена." + } + }, + "objectMasks": { + "label": "Маски объектов", + "desc": { + "documentation": "Документация", + "title": "Маски фильтра объектов используются для исключения ложных срабатываний определённого типа объектов в зависимости от местоположения." + }, + "documentTitle": "Редактирование маски объектов - Frigate", + "add": "Добавить маску объектов", + "clickDrawPolygon": "Кликните, чтобы нарисовать полигон на изображении.", + "edit": "Редактирование маски объектов", + "context": "Маски фильтра объектов используются для исключения ложных срабатываний определённого типа объектов в зависимости от местоположения.", + "point_one": "{{count}} точка", + "point_few": "{{count}} точки", + "point_many": "{{count}} точек", + "objects": { + "allObjectTypes": "Все типы объектов", + "title": "Объекты", + "desc": "Тип объекта, который применяется к этой маске объекта." + }, + "toast": { + "success": { + "title": "{{polygonName}} сохранена.", + "noName": "Маска объектов сохранена." + } + } + }, + "restart_required": "Требуется перезапуск (изменены маски/зоны)", + "objectMaskLabel": "Маска объекта {{number}} {{label}}", + "motionMaskLabel": "Маска движения {{number}}" + }, + "motionDetectionTuner": { + "desc": { + "documentation": "Читать руководство по настройке детекции движения", + "title": "Frigate использует детекцию движения как первичную проверку, чтобы определить, есть ли в кадре что-то, что стоит анализировать с помощью детекции объектов." + }, + "title": "Настройка детекции движения", + "contourArea": { + "title": "Площадь контура", + "desc": "Параметр площади контура определяет, какие группы изменённых пикселей считаются движением. По умолчанию: 10" + }, + "improveContrast": { + "title": "Улучшить контрастность", + "desc": "Улучшение контрастности в тёмных сценах. По умолчанию: ВКЛ" + }, + "Threshold": { + "title": "Порог", + "desc": "Пороговое значение определяет, насколько должна измениться яркость пикселя, чтобы считаться движением. По умолчанию: 30" + }, + "toast": { + "success": "Настройки движения сохранены." + }, + "unsavedChanges": "Настройки детекции движения для ({{camera}}) не сохранены" + }, + "debug": { + "objectShapeFilterDrawing": { + "document": "Читать документацию ", + "title": "Отрисовка фильтра формы объекта", + "desc": "Отображает прямоугольник на изображении, чтобы видеть данные о площади и соотношении сторон", + "tips": "Включите эту опцию, чтобы нарисовать прямоугольник на изображении с камеры для отображения его площади и соотношения сторон. Эти значения можно затем использовать для настройки параметров фильтра формы объектов в вашем конфигурационном файле.", + "area": "Площадь", + "ratio": "Соотношение", + "score": "Оценка" + }, + "detectorDesc": "Frigate использует ваши детекторы ({{detectors}}) для обнаружения объектов в видеопотоке с камер.", + "desc": "Режим отладки отображает отслеживаемые объекты и их статистику в реальном времени. Список объектов показывает отложенную по времени сводку обнаруженных объектов.", + "debugging": "Отладка", + "title": "Отладка", + "boundingBoxes": { + "colors": { + "label": "Цвета ограничивающих рамок объектов", + "info": "
  • При запуске каждой метке объекта назначается уникальный цвет
  • Тонкая синяя линия: объект в данный момент не обнаружен
  • Тонкая серая линия: объект помечен как статичный
  • Толстая линия: объект под автотрекингом (если включено)
  • " + }, + "title": "Ограничивающие рамки", + "desc": "Показывать ограничивающие рамки вокруг отслеживаемых объектов" + }, + "objectList": "Список объектов", + "noObjects": "Нет объектов", + "timestamp": { + "title": "Метка времени", + "desc": "Наложить временную метку на изображение" + }, + "zones": { + "title": "Зоны", + "desc": "Показать контур всех определённых зон" + }, + "mask": { + "title": "Маски движения", + "desc": "Показать полигоны маски движения" + }, + "motion": { + "title": "Области движения", + "desc": "Показать рамки вокруг областей, в которых определяется движение", + "tips": "

    Области движения


    Красные рамки будут наложены на участки кадра, где в данный момент обнаружено движение

    " + }, + "regions": { + "title": "Регионы", + "desc": "Показать рамку области интереса, отправленной детектору объектов", + "tips": "

    Рамки областей интереса


    Ярко-зелёные рамки будут наложены на области интереса в кадре, которые отправляются детектору объектов.

    " + }, + "paths": { + "title": "Пути", + "desc": "Показывать значимые точки пути отслеживаемого объекта", + "tips": "

    Пути


    Линии и круги будут обозначать важные точки, которые отслеживаемый объект посетил в течение своего жизненного цикла.

    " + }, + "openCameraWebUI": "Открыть веб-интерфейс {{camera}}", + "audio": { + "title": "Аудио", + "noAudioDetections": "Аудиообнаружений нет", + "score": "оценка", + "currentRMS": "Текущий RMS", + "currentdbFS": "Текущий dbFS" + } + }, + "frigatePlus": { + "snapshotConfig": { + "documentation": "Читать документацию", + "title": "Настройки снимков", + "cleanCopyWarning": "У некоторых камер включены снимки (snapshots), но отключена опция чистой копии (clean copy). Чтобы иметь возможность отправлять изображения с этих камер в Frigate+, необходимо включить параметр clean_copy в конфигурации снимков.", + "table": { + "cleanCopySnapshots": "Снимки clean_copy", + "camera": "Камера", + "snapshots": "Снимки" + }, + "desc": "Отправка в Frigate+ требует, чтобы в вашей конфигурации были включены как снимки (snapshots), так и снимки clean_copy." + }, + "title": "Настройки Frigate+", + "apiKey": { + "title": "Ключ API Frigate+", + "validated": "Ключ API Frigate+ найден и проверен", + "notValidated": "Ключ API Frigate+ не найден или не проверен", + "desc": "Ключ API Frigate+ включает интеграцию с сервисом Frigate+.", + "plusLink": "Подробнее про Frigate+" + }, + "modelInfo": { + "title": "Информация о модели", + "modelType": "Тип модели", + "trainDate": "Дата обучения", + "error": "Не удалось загрузить информацию о модели", + "availableModels": "Доступные модели", + "loadingAvailableModels": "Загрузка доступных моделей…", + "modelSelect": "Здесь можно выбрать ваши доступные модели на Frigate+. Обратите внимание, что могут быть выбраны только модели, совместимые с текущей конфигурацией детектора.", + "baseModel": "Базовая модель", + "supportedDetectors": "Поддерживаемые детекторы", + "dimensions": "Размеры", + "loading": "Загрузка информации о модели…", + "cameras": "Камеры", + "plusModelType": { + "baseModel": "Базовая модель", + "userModel": "Дообученная" + } + }, + "toast": { + "success": "Настройки Frigate+ были сохранены. Перезапустите Frigate, чтобы применить изменения.", + "error": "Не удалось сохранить изменения конфигурации: {{errorMessage}}" + }, + "restart_required": "Требуется перезапуск (изменена модель Frigate+)", + "unsavedChanges": "Настройки Frigate+ не сохранены" + }, + "enrichments": { + "title": "Настройки обогащения", + "semanticSearch": { + "readTheDocumentation": "Читать документацию", + "desc": "Семантический поиск во Frigate позволяет находить отслеживаемые объекты в записях с помощью самого изображения, пользовательского текстового описания или автоматически сгенерированного описания.", + "reindexNow": { + "desc": "Переиндексация заново сгенерирует векторные представления для всех отслеживаемых объектов. Этот процесс выполняется в фоновом режиме и может максимально загрузить ваш процессор, а также занять значительное время в зависимости от количества отслеживаемых объектов.", + "label": "Переиндексировать сейчас", + "confirmTitle": "Подтвердить переиндексацию", + "confirmDesc": "Вы уверены, что хотите переиндексировать все векторные представления отслеживаемых объектов? Этот процесс будет выполняться в фоновом режиме, но может максимально загрузить ваш процессор и занять довольно много времени. Вы можете следить за ходом выполнения на странице «Поиск событий».", + "confirmButton": "Переиндексировать", + "success": "Переиндексация успешно запущена.", + "alreadyInProgress": "Переиндексация уже выполняется.", + "error": "Не удалось запустить переиндексацию: {{errorMessage}}" + }, + "modelSize": { + "desc": "Размер модели, используемой для создания векторных представлений для семантического поиска.", + "small": { + "desc": "Использование малой модели задействует квантованную версию модели, которая потребляет меньше оперативной памяти и работает быстрее на CPU с очень незначительной разницей в качестве эмбеддингов.", + "title": "малый" + }, + "label": "Размер модели", + "large": { + "title": "большой", + "desc": "Использование большой модели задействует полную модель Jina и автоматически запускается на GPU, если это возможно." + } + }, + "title": "Семантический поиск" + }, + "birdClassification": { + "desc": "Классификация птиц определяет известные виды с помощью квантованной модели TensorFlow. Когда птица распознана, её обиходное название добавляется в качестве дополнительной метки. Эти информация используется в интерфейсе, фильтрах и уведомлениях.", + "title": "Классификация птиц" + }, + "faceRecognition": { + "modelSize": { + "large": { + "desc": "При выборе большой модели используется модель векторизации лиц ArcFace, которая автоматически задействует GPU (если он доступен).", + "title": "большой" + }, + "small": { + "title": "малый", + "desc": "Использование малой модели задействует модель FaceNet для векторного представления лиц, которая эффективно работает на большинстве CPU." + }, + "label": "Размер модели", + "desc": "Размер модели, используемой для распознавания лиц." + }, + "desc": "Функция распознавания лиц позволяет присваивать людям имена, и когда их лицо будет распознано, Frigate присвоит имя человека в качестве дополнительной метки. Эта информация содержится в пользовательском интерфейсе, фильтрах, а также в уведомлениях.", + "title": "Распознавание лиц", + "readTheDocumentation": "Читать документацию" + }, + "licensePlateRecognition": { + "desc": "Frigate может распознавать автомобильные номера и автоматически добавлять для объектов типа «автомобиль» обнаруженные символы в поле «распознанный номерной знак» или известное имя в качестве дополнительной метки. Типичный пример использования — чтение номеров автомобилей, заезжающих на подъездную дорожку или проезжающих по улице.", + "title": "Распознавание номерных знаков", + "readTheDocumentation": "Читать документацию" + }, + "unsavedChanges": "Несохранённые изменения настроек обогащений", + "restart_required": "Требуется перезапуск (изменены настройки обогащений)", + "toast": { + "success": "Настройки обогащений сохранены. Перезапустите Frigate, чтобы применить изменения.", + "error": "Не удалось сохранить изменения: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Триггеры", + "management": { + "title": "Триггеры", + "desc": "Управление триггерами для камеры {{camera}}. Используйте тип миниатюры для срабатывания по миниатюрам, похожим на выбранный отслеживаемый объект, и тип описания для срабатывания по описаниям, похожим на указанный вами текст." + }, + "addTrigger": "Добавить Триггер", + "table": { + "name": "Имя", + "type": "Тип", + "content": "Содержимое", + "threshold": "Порог", + "actions": "Действия", + "noTriggers": "Для этой камеры не настроены триггеры.", + "edit": "Редактировать", + "deleteTrigger": "Удалить триггер", + "lastTriggered": "Последний сработавший" + }, + "type": { + "thumbnail": "Миниатюра", + "description": "Описание" + }, + "actions": { + "alert": "Отметить как предупреждение", + "notification": "Отправить оповещение", + "sub_label": "Добавить подметку", + "attribute": "Добавить атрибут" + }, + "dialog": { + "createTrigger": { + "title": "Создать триггер", + "desc": "Создать триггер для камеры {{camera}}" + }, + "editTrigger": { + "title": "Изменить триггер", + "desc": "Изменить настройки триггера для камеры {{camera}}" + }, + "deleteTrigger": { + "title": "Удалить триггер", + "desc": "Вы уверены, что хотите удалить триггер {{triggerName}}? Это действие не может быть отменено." + }, + "form": { + "name": { + "title": "Имя", + "placeholder": "Назовите этот триггер", + "error": { + "minLength": "Поле должно содержать не менее 2 символов.", + "invalidCharacters": "Поле может содержать только буквы, цифры, символы подчеркивания и дефисы.", + "alreadyExists": "Триггер с таким именем уже существует для этой камеры." + }, + "description": "Введите уникальное имя или описание для идентификации этого триггера" + }, + "enabled": { + "description": "Включить или отключить этот триггер" + }, + "type": { + "title": "Тип", + "placeholder": "Выберите тип триггера", + "description": "Срабатывать при обнаружении похожего описания отслеживаемого объекта", + "thumbnail": "Срабатывать при обнаружении похожей миниатюры отслеживаемого объекта" + }, + "content": { + "title": "Содержимое", + "imagePlaceholder": "Выберите миниатюру", + "textPlaceholder": "Введите текстовое содержимое", + "imageDesc": "Отображаются только 100 последних миниатюр. Если вы не можете найти нужную миниатюру, просмотрите предыдущие объекты в разделе \"Обзор\" и настройте триггер оттуда через меню.", + "textDesc": "Введите текст, чтобы активировать это действие при обнаружении похожего описания отслеживаемого объекта.", + "error": { + "required": "Требуется содержимое." + } + }, + "threshold": { + "title": "Порог", + "error": { + "min": "Порог должен быть не менее 0", + "max": "Порог должен быть не более 1" + }, + "desc": "Установите порог схожести для этого триггера. Более высокое значение требует более точного совпадения для срабатывания триггера." + }, + "actions": { + "title": "Действия", + "desc": "По умолчанию Frigate отправляет MQTT-сообщение для всех триггеров. Подметки добавляют имя триггера к метке объекта. Атрибуты — это доступные для поиска метаданные, хранящиеся отдельно в метаданных отслеживаемого объекта.", + "error": { + "min": "Необходимо выбрать хотя бы одно действие." + } + }, + "friendly_name": { + "description": "Необязательное название или описание к этому триггеру", + "placeholder": "Название или описание триггера", + "title": "Понятное название" + } + } + }, + "toast": { + "success": { + "createTrigger": "Триггер {{name}} успешно создан.", + "updateTrigger": "Триггер {{name}} успешно обновлен.", + "deleteTrigger": "Триггер {{name}} успешно удален." + }, + "error": { + "createTriggerFailed": "Не удалось создать триггер: {{errorMessage}}", + "updateTriggerFailed": "Не удалось обновить триггер: {{errorMessage}}", + "deleteTriggerFailed": "Не удалось удалить триггер: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Семантический поиск выключен", + "desc": "Для использования триггеров необходимо включить семантический поиск." + }, + "wizard": { + "title": "Создать триггер", + "step1": { + "description": "Настройте основные параметры вашего триггера." + }, + "step2": { + "description": "Настройте содержимое, которое будет активировать это действие." + }, + "step3": { + "description": "Настройте порог и действия для этого триггера." + }, + "steps": { + "nameAndType": "Имя и тип", + "configureData": "Настроить данные", + "thresholdAndActions": "Порог и действия" + } + } + }, + "cameraWizard": { + "title": "Добавить камеру", + "description": "Следуйте инструкциям ниже, чтобы добавить новую камеру в вашу установку Frigate.", + "steps": { + "nameAndConnection": "Имя и подключение", + "streamConfiguration": "Конфигурация потока", + "validationAndTesting": "Проверка и тестирование", + "probeOrSnapshot": "Проверка или снимок" + }, + "save": { + "success": "Новая камера {{cameraName}} успешно сохранена.", + "failure": "Ошибка при сохранении {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Разрешение", + "video": "Видео", + "audio": "Аудио", + "fps": "Кадры в секунду (FPS)" + }, + "commonErrors": { + "noUrl": "Пожалуйста, укажите корректный URL потока", + "testFailed": "Тест потока не удался: {{error}}" + }, + "step1": { + "description": "Введите параметры вашей камеры и выберите: автоматическое определение или ручной выбор производителя.", + "cameraName": "Имя камеры", + "cameraNamePlaceholder": "Например, front_door или Обзор заднего двора", + "host": "Хост/IP-адрес", + "port": "Порт", + "username": "Имя пользователя", + "usernamePlaceholder": "Необязательно", + "password": "Пароль", + "passwordPlaceholder": "Необязательно", + "selectTransport": "Выберите транспортный протокол", + "cameraBrand": "Бренд камеры", + "selectBrand": "Выберите бренд камеры для шаблона URL", + "customUrl": "Пользовательский URL потока", + "brandInformation": "Информация о бренде", + "brandUrlFormat": "Для камер с форматом RTSP-URL вида: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://имя_пользователя:пароль@хост:порт/путь", + "testConnection": "Проверить соединение", + "testSuccess": "Соединение успешно установлено!", + "testFailed": "Проверка соединения не удалась. Проверьте введённые данные и попробуйте снова.", + "streamDetails": "Детали потока", + "warnings": { + "noSnapshot": "Не удалось получить снимок из настроенного потока." + }, + "errors": { + "brandOrCustomUrlRequired": "Выберите бренд камеры с указанием хоста/IP или выберите \"Другое\" и укажите пользовательский URL", + "nameRequired": "Необходимо указать имя камеры", + "nameLength": "Имя камеры должно содержать не более 64 символов", + "invalidCharacters": "Имя камеры содержит недопустимые символы", + "nameExists": "Имя камеры уже используется", + "brands": { + "reolink-rtsp": "RTSP от Reolink не рекомендуется. Включите HTTP в настройках камеры и перезапустите мастер настройки камеры." + }, + "customUrlRtspRequired": "Пользовательские URL должны начинаться с \"rtsp://\". Для потоков камер, не использующих RTSP, требуется ручная настройка." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Проверка метаданных камеры…", + "fetchingSnapshot": "Получение снимка с камеры…" + }, + "connectionSettings": "Настройки подключения", + "detectionMethod": "Метод обнаружения потока", + "onvifPort": "Порт ONVIF", + "probeMode": "Проверить камеру", + "manualMode": "Ручной выбор", + "detectionMethodDescription": "Проверьте камеру с помощью ONVIF (если поддерживается) для поиска URL потоков камеры или вручную выберите бренд камеры для использования предопределённых URL. Чтобы ввести пользовательский RTSP URL, выберите ручной метод и выберите \"Другое\".", + "onvifPortDescription": "Для камер, поддерживающих ONVIF, это обычно 80 или 8080.", + "useDigestAuth": "Использовать digest-аутентификацию", + "useDigestAuthDescription": "Использовать HTTP digest-аутентификацию для ONVIF. Некоторые камеры могут требовать отдельное имя пользователя/пароль ONVIF вместо стандартного пользователя администратора." + }, + "step2": { + "description": "Проверьте камеру на наличие доступных потоков или настройте параметры вручную в зависимости от выбранного метода обнаружения.", + "streamsTitle": "Потоки камеры", + "addStream": "Добавить поток", + "addAnotherStream": "Добавить ещё один поток", + "streamTitle": "Поток {{number}}", + "streamUrl": "URL потока", + "streamUrlPlaceholder": "rtsp://имя_пользователя:пароль@хост:порт/путь", + "url": "URL", + "resolution": "Разрешение", + "selectResolution": "Выберите разрешение", + "quality": "Качество", + "selectQuality": "Выберите качество", + "roles": "Роли", + "roleLabels": { + "detect": "Обнаружение объектов", + "record": "Запись", + "audio": "Аудио" + }, + "testStream": "Проверить соединение", + "testSuccess": "Проверка соединения успешна!", + "testFailed": "Проверка соединения не удалась. Проверьте введённые данные и попробуйте снова.", + "testFailedTitle": "Проверка не удалась", + "streamDetails": "Детали потока", + "probing": "Проверка камеры…", + "retry": "Повторить", + "testing": { + "probingMetadata": "Проверка метаданных камеры…", + "fetchingSnapshot": "Получение снимка с камеры…" + }, + "probeFailed": "Не удалось проверить камеру: {{error}}", + "probingDevice": "Проверка устройства…", + "probeSuccessful": "Проверка успешна", + "probeError": "Ошибка проверки", + "probeNoSuccess": "Проверка не удалась", + "deviceInfo": "Информация об устройстве", + "manufacturer": "Производитель", + "model": "Модель", + "firmware": "Прошивка", + "profiles": "Профили", + "ptzSupport": "Поддержка PTZ", + "autotrackingSupport": "Поддержка автотрекинга", + "presets": "Предустановки", + "rtspCandidates": "Кандидаты RTSP", + "rtspCandidatesDescription": "Следующие RTSP URL были найдены при проверке камеры. Проверьте соединение, чтобы просмотреть метаданные потока.", + "noRtspCandidates": "RTSP URL не найдены для камеры. Ваши учётные данные могут быть неверными, или камера может не поддерживать ONVIF или метод, используемый для получения RTSP URL. Вернитесь назад и введите RTSP URL вручную.", + "candidateStreamTitle": "Кандидат {{number}}", + "useCandidate": "Использовать", + "uriCopy": "Копировать", + "uriCopied": "URI скопирован в буфер обмена", + "testConnection": "Проверить соединение", + "toggleUriView": "Нажмите, чтобы переключить полный вид URI", + "connected": "Подключено", + "notConnected": "Не подключено", + "errors": { + "hostRequired": "Требуется хост/IP-адрес" + } + }, + "step3": { + "description": "Настройте роли потоков и добавьте дополнительные потоки для вашей камеры.", + "streamsTitle": "Потоки камеры", + "addStream": "Добавить поток", + "addAnotherStream": "Добавить ещё поток", + "streamTitle": "Поток {{number}}", + "streamUrl": "URL потока", + "streamUrlPlaceholder": "rtsp://имя_пользователя:пароль@хост:порт/путь", + "selectStream": "Выбрать поток", + "searchCandidates": "Поиск кандидатов…", + "noStreamFound": "Поток не найден", + "url": "URL", + "resolution": "Разрешение", + "selectResolution": "Выберите разрешение", + "quality": "Качество", + "selectQuality": "Выберите качество", + "roles": "Роли", + "roleLabels": { + "detect": "Обнаружение объектов", + "record": "Запись", + "audio": "Аудио" + }, + "testStream": "Проверить соединение", + "testSuccess": "Тест потока выполнен успешно!", + "testFailed": "Тест потока не пройден", + "testFailedTitle": "Тест не пройден", + "connected": "Подключено", + "notConnected": "Не подключено", + "featuresTitle": "Функции", + "go2rtc": "Уменьшить количество подключений к камере", + "detectRoleWarning": "Хотя бы один поток должен иметь роль \"detect\" для продолжения.", + "rolesPopover": { + "title": "Роли потоков", + "detect": "Основной поток для обнаружения объектов.", + "record": "Сохраняет сегменты видеопотока на основе настроек конфигурации.", + "audio": "Поток для обнаружения на основе аудио." + }, + "featuresPopover": { + "title": "Функции потоков", + "description": "Использовать рестриминг go2rtc для уменьшения количества подключений к камере." + } + }, + "step4": { + "description": "Финальная проверка и анализ перед сохранением новой камеры. Подключите каждый поток перед сохранением.", + "validationTitle": "Проверка потоков", + "connectAllStreams": "Подключить все потоки", + "reconnectionSuccess": "Переподключение успешно.", + "reconnectionPartial": "Некоторые потоки не удалось переподключить.", + "streamUnavailable": "Предпросмотр потока недоступен", + "reload": "Перезагрузить", + "connecting": "Подключение…", + "streamTitle": "Поток {{number}}", + "valid": "Действителен", + "failed": "Не удалось", + "notTested": "Не проверен", + "connectStream": "Подключить", + "connectingStream": "Подключение", + "disconnectStream": "Отключить", + "estimatedBandwidth": "Расчётная пропускная способность", + "roles": "Роли", + "ffmpegModule": "Использовать режим совместимости потоков", + "ffmpegModuleDescription": "Если поток не загружается после нескольких попыток, попробуйте включить это. При включении Frigate будет использовать модуль ffmpeg с go2rtc. Это может обеспечить лучшую совместимость с некоторыми потоками камер.", + "none": "Нет", + "error": "Ошибка", + "streamValidated": "Поток {{number}} успешно проверен", + "streamValidationFailed": "Проверка потока {{number}} не удалась", + "saveAndApply": "Сохранить новую камеру", + "saveError": "Неверная конфигурация. Пожалуйста, проверьте настройки.", + "issues": { + "title": "Проверка потоков", + "videoCodecGood": "Видеокодек: {{codec}}.", + "audioCodecGood": "Аудиокодек: {{codec}}.", + "resolutionHigh": "Разрешение {{resolution}} может привести к увеличению использования ресурсов.", + "resolutionLow": "Разрешение {{resolution}} может быть слишком низким для надёжного обнаружения мелких объектов.", + "noAudioWarning": "Аудио не обнаружено для этого потока, записи не будут содержать аудио.", + "audioCodecRecordError": "Для поддержки аудио в записях требуется аудиокодек AAC.", + "audioCodecRequired": "Для поддержки обнаружения аудио требуется аудиопоток.", + "restreamingWarning": "Уменьшение количества подключений к камере для потока записи может немного увеличить использование CPU.", + "brands": { + "reolink-rtsp": "RTSP от Reolink не рекомендуется. Включите HTTP в настройках прошивки камеры и перезапустите мастер.", + "reolink-http": "HTTP потоки Reolink должны использовать FFmpeg для лучшей совместимости. Включите 'Использовать режим совместимости потоков' для этого потока." + }, + "dahua": { + "substreamWarning": "Подпоток 1 заблокирован на низком разрешении. Многие камеры Dahua / Amcrest / EmpireTech поддерживают дополнительные подпотоки, которые необходимо включить в настройках камеры. Рекомендуется проверить и использовать эти потоки, если они доступны." + }, + "hikvision": { + "substreamWarning": "Подпоток 1 заблокирован на низком разрешении. Многие камеры Hikvision поддерживают дополнительные подпотоки, которые необходимо включить в настройках камеры. Рекомендуется проверить и использовать эти потоки, если они доступны." + } + } + } + }, + "roles": { + "addRole": "Добавить роль", + "table": { + "role": "Роль", + "cameras": "Камеры", + "actions": "Действия", + "editCameras": "Редактировать камеры", + "deleteRole": "Удалить роль", + "noRoles": "Пользовательских ролей не найдено." + }, + "toast": { + "success": { + "createRole": "Роль {{role}} успешно создана", + "updateCameras": "Камеры обновлены для роли {{role}}", + "deleteRole": "Роль {{role}} успешно удалена", + "userRolesUpdated_one": "{{count}} пользователь, назначенный на эту роль, был обновлён до роли 'наблюдатель', которая имеет доступ ко всем камерам.", + "userRolesUpdated_few": "{{count}} пользователя, назначенных на эту роль, были обновлены до роли 'наблюдатель', которая имеет доступ ко всем камерам.", + "userRolesUpdated_many": "{{count}} пользователей, назначенных на эту роль, были обновлены до роли 'наблюдатель', которая имеет доступ ко всем камерам." + }, + "error": { + "createRoleFailed": "Не удалось создать роль: {{errorMessage}}", + "updateCamerasFailed": "Не удалось обновить камеры: {{errorMessage}}", + "deleteRoleFailed": "Не удалось удалить роль: {{errorMessage}}", + "userUpdateFailed": "Не удалось обновить роли пользователей: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Создать новую роль", + "desc": "Добавьте новую роль и укажите права доступа к камерам." + }, + "editCameras": { + "title": "Редактировать камеры роли", + "desc": "Обновите доступ к камерам для роли {{role}}." + }, + "deleteRole": { + "title": "Удалить роль", + "desc": "Это действие нельзя отменить. Это действие навсегда удалит роль и назначит всех пользователей с этой ролью на роль 'наблюдатель', что даст наблюдателю доступ ко всем камерам.", + "warn": "Вы уверены, что хотите удалить {{role}}?", + "deleting": "Удаление…" + }, + "form": { + "role": { + "title": "Название роли", + "placeholder": "Введите название роли", + "desc": "Разрешены только буквы, цифры, точки и подчёркивания.", + "roleIsRequired": "Требуется название роли", + "roleOnlyInclude": "Название роли может содержать только буквы, цифры, . или _", + "roleExists": "Роль с таким названием уже существует." + }, + "cameras": { + "title": "Камеры", + "desc": "Выберите камеры, к которым эта роль имеет доступ. Необходимо выбрать хотя бы одну камеру.", + "required": "Необходимо выбрать хотя бы одну камеру." + } + } + }, + "management": { + "title": "Управление ролями наблюдателя", + "desc": "Управление пользовательскими ролями наблюдателя и их правами доступа к камерам для этого экземпляра Frigate." + } + }, + "cameraManagement": { + "title": "Управление камерами", + "addCamera": "Добавить новую камеру", + "editCamera": "Редактировать камеру:", + "selectCamera": "Выбрать камеру", + "backToSettings": "Вернуться к настройкам камеры", + "streams": { + "title": "Включить / Отключить камеры", + "desc": "Временно отключить камеру до перезапуска Frigate. Отключение камеры полностью останавливает обработку потоков этой камеры в Frigate. Обнаружение, запись и отладка будут недоступны.
    Примечание: Это не отключает рестриминг go2rtc.", + "label": "Состояние камеры", + "status": { + "disabled": "Отключено" + }, + "enableSuccess": "Включена {{cameraName}}. Перезапустите Frigate для применения изменений.", + "disableSuccess": "Отключено {{cameraName}} и сохранено в конфигурации.", + "reorderHandle": "Перетяните для изменения порядка", + "saving": "Сохранение…", + "saved": "Сохранено", + "details": { + "edit": "Редактировать детали камеры", + "title": "Редактировать детали камеры", + "description": "Обновите отображаемое имя, внешний URL и видимость для этой камеры в пользовательском интерфейсе Frigate.", + "friendlyNameLabel": "Отображаемое Имя", + "friendlyNameHelp": "Дружественное имя, которое отображается для этой камеры в пользовательском интерфейсе Frigate. Оставьте пустым для использования идентификатора камеры.", + "webuiUrlLabel": "Ссылка на Web интерфейс камеры", + "webuiUrlHelp": "URL-адрес для посещения веб-UI камеры непосредственно с отладочного меню. Оставьте пустым, чтобы отключить ссылку.", + "webuiUrlInvalid": "Должен быть действительный URL (например, https://example.com).", + "dashboardLabel": "Показывать на Live dashboard", + "dashboardHelp": "Показывать эту камеру на Live dashboard." + } + }, + "cameraConfig": { + "add": "Добавить камеру", + "edit": "Редактировать камеру", + "description": "Настройте параметры камеры, включая входные трансляции и роли.", + "name": "Название камеры", + "nameRequired": "Требуется имя камеры", + "nameLength": "Название камеры должно содержать менее 64 символов.", + "namePlaceholder": "например, front_door или Обзор заднего двора", + "enabled": "Включено", + "ffmpeg": { + "inputs": "Входные потоки", + "path": "Путь потока", + "pathRequired": "Требуется путь потока", + "pathPlaceholder": "rtsp://…", + "roles": "Роли", + "rolesRequired": "Требуется хотя бы одна роль", + "rolesUnique": "Каждая роль (аудио, обнаружение, запись) может быть назначена только одной трансляции", + "addInput": "Добавить входной поток", + "removeInput": "Удалить входной поток", + "inputsRequired": "Требуется хотя бы один входной поток" + }, + "go2rtcStreams": "Потоки go2rtc", + "streamUrls": "URL потоков", + "addUrl": "Добавить URL", + "addGo2rtcStream": "Добавить поток go2rtc", + "toast": { + "success": "Камера {{cameraName}} успешно сохранена" + } + }, + "deleteCameraDialog": { + "confirmWarning": "Удаление {{cameraName}} не может быть отменено.", + "success": "Камера {{cameraName}} успешно удалена", + "error": "Не удалось удалить камеру {{cameraName}}" + } + }, + "cameraReview": { + "title": "Настройки просмотра камеры", + "object_descriptions": { + "title": "Генеративные описания объектов ИИ", + "desc": "Временно включить/отключить генеративные описания объектов ИИ для этой камеры. При отключении описания объектов, сгенерированные ИИ, не будут запрашиваться для отслеживаемых объектов на этой камере." + }, + "review_descriptions": { + "title": "Генеративные описания обзоров ИИ", + "desc": "Временно включить/отключить генеративные описания обзоров ИИ для этой камеры. При отключении описания обзоров, сгенерированные ИИ, не будут запрашиваться для элементов обзора на этой камере." + }, + "review": { + "title": "Обзор", + "desc": "Временно включить/отключить тревоги и обнаружения для этой камеры до перезапуска Frigate. При отключении новые элементы обзора не будут создаваться. ", + "alerts": "Тревоги ", + "detections": "Обнаружения " + }, + "reviewClassification": { + "title": "Классификация обзора", + "desc": "Frigate классифицирует элементы обзора как Тревоги и Обнаружения. По умолчанию все объекты person и car считаются Тревогами. Вы можете уточнить классификацию элементов обзора, настроив для них требуемые зоны.", + "noDefinedZones": "Для этой камеры не определено ни одной зоны.", + "objectAlertsTips": "Все объекты {{alertsLabels}} на камере {{cameraName}} будут отображаться как Тревоги.", + "zoneObjectAlertsTips": "Все объекты {{alertsLabels}}, обнаруженные в {{zone}} на камере {{cameraName}}, будут отображаться как Тревоги.", + "objectDetectionsTips": "Все объекты {{detectionsLabels}}, не отнесённые к категории на камере {{cameraName}}, будут отображаться как Обнаружения, независимо от того, в какой зоне они находятся.", + "zoneObjectDetectionsTips": { + "text": "Все объекты {{detectionsLabels}}, не отнесённые к категории в {{zone}} на камере {{cameraName}}, будут отображаться как Обнаружения.", + "notSelectDetections": "Все объекты {{detectionsLabels}}, обнаруженные в {{zone}} на камере {{cameraName}}, которые не отнесены к Тревогам, будут отображаться как Обнаружения, независимо от того, в какой зоне они находятся.", + "regardlessOfZoneObjectDetectionsTips": "Все объекты {{detectionsLabels}}, не отнесённые к категории на камере {{cameraName}}, будут отображаться как Обнаружения, независимо от того, в какой зоне они находятся." + }, + "unsavedChanges": "Несохранённые настройки классификации обзора для {{camera}}", + "selectAlertsZones": "Выберите зоны для Тревог", + "selectDetectionsZones": "Выберите зоны для обнаружений", + "limitDetections": "Ограничить обнаружения определёнными зонами", + "toast": { + "success": "Конфигурация классификации обзора была сохранена. Перезапустите Frigate для применения изменений." + } + } + }, + "button": { + "overriddenGlobal": "Перезаписано (глобально)", + "overriddenGlobalTooltip": "Эта камера перезаписывает глобальные настройки в этой секции", + "overriddenBaseConfig": "Перезаписано (базовые настройки)", + "overriddenBaseConfigTooltip": "Перезаписи настроек профиля {{profile}} в этой секции" + } +} diff --git a/web/public/locales/ru/views/system.json b/web/public/locales/ru/views/system.json new file mode 100644 index 0000000..887678a --- /dev/null +++ b/web/public/locales/ru/views/system.json @@ -0,0 +1,230 @@ +{ + "documentTitle": { + "cameras": "Статистика камер - Frigate", + "storage": "Статистика хранилища - Frigate", + "general": "Общая статистика - Frigate", + "enrichments": "Статистика обогащений - Frigate", + "logs": { + "frigate": "Логи Frigate - Frigate", + "go2rtc": "Логи Go2RTC - Frigate", + "nginx": "Логи Nginx - Frigate", + "websocket": "Логи сообщений - Frigate" + } + }, + "title": "Система", + "metrics": "Показатели системы", + "logs": { + "download": { + "label": "Загрузить логи" + }, + "copy": { + "label": "Копировать в буфер", + "success": "Логи скопированы в буфер", + "error": "Не удалось скопировать логи в буфер обмена" + }, + "type": { + "label": "Тип", + "timestamp": "Метка времени", + "tag": "Тег", + "message": "Сообщение" + }, + "tips": "Логи передаются с сервера в потоковом режиме", + "toast": { + "error": { + "fetchingLogsFailed": "Ошибка получения логов: {{errorMessage}}", + "whileStreamingLogs": "Ошибка при потоковой передаче логов: {{errorMessage}}" + } + }, + "websocket": { + "label": "Сообщения", + "pause": "Пауза", + "resume": "Продолжить", + "clear": "Очистить", + "filter": { + "all": "Все топики", + "topics": "Топики", + "events": "События", + "classification": "Классификация", + "face_recognition": "Распознавание лиц", + "lpr": "Распознавание номерных знаков", + "camera_activity": "Активность камеры", + "system": "Система", + "camera": "Камера", + "all_cameras": "Все камеры", + "cameras_count_one": "{{count}} камера", + "cameras_count_other": "{{count}} камеры" + }, + "empty": "Сообщения ещё не были получены" + } + }, + "general": { + "title": "Общие", + "detector": { + "title": "Детекторы", + "inferenceSpeed": "Скорость вывода детектора", + "cpuUsage": "Использование CPU детектором", + "memoryUsage": "Использование памяти детектором", + "temperature": "Температура детектора", + "cpuUsageInformation": "CPU используется при подготовке входных и выходных данных к/от моделей обнаружения. Это значение не измеряет использование вывода, даже если использовать GPU или ускоритель." + }, + "hardwareInfo": { + "title": "Информация об оборудовании", + "gpuUsage": "Использование GPU", + "gpuMemory": "Память GPU", + "gpuEncoder": "GPU-кодировщик", + "gpuDecoder": "GPU-декодер", + "gpuInfo": { + "vainfoOutput": { + "title": "Вывод Vainfo", + "returnCode": "Код возврата: {{code}}", + "processOutput": "Вывод процесса:", + "processError": "Ошибка процесса:" + }, + "nvidiaSMIOutput": { + "title": "Вывод Nvidia SMI", + "name": "Название: {{name}}", + "driver": "Драйвер: {{driver}}", + "cudaComputerCapability": "Вычислительная способность CUDA: {{cuda_compute}}", + "vbios": "Информация VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Закрыть информацию GPU" + }, + "copyInfo": { + "label": "Скопировать информацию о GPU" + }, + "toast": { + "success": "Информация о GPU скопирована в буфер обмена" + } + }, + "npuMemory": "Память NPU", + "npuUsage": "Использование NPU", + "intelGpuWarning": { + "title": "Предупреждение: статистика Intel GPU", + "message": "Статистика GPU недоступна", + "description": "Это известная ошибка в инструментах отчетности статистики Intel GPU (intel_gpu_top), из-за которой они ломаются и постоянно возвращают уровень использования GPU 0%, даже в случаях, когда аппаратное ускорение и обнаружение объектов корректно работают на (i)GPU. Это не ошибка Frigate. Вы можете перезапустить хост-систему, чтобы временно устранить проблему и убедиться, что GPU работает правильно. На производительность это не влияет." + } + }, + "otherProcesses": { + "title": "Другие процессы", + "processCpuUsage": "Использование CPU процессом", + "processMemoryUsage": "Использование памяти процессом", + "series": { + "go2rtc": "go2rtc", + "recording": "запись", + "review_segment": "сегмент обзора", + "embeddings": "вложения", + "audio_detector": "аудиодетектор" + } + } + }, + "storage": { + "title": "Хранилище", + "overview": "Обзор", + "recordings": { + "title": "Записи", + "tips": "Это значение показывает, сколько места в хранилище занимают записи из базы данных Frigate. Frigate не учитывает другие файлы на диске.", + "earliestRecording": "Первая запись:" + }, + "cameraStorage": { + "title": "Хранилище камеры", + "camera": "Камера", + "unusedStorageInformation": "Информация о неиспользованном хранилище", + "storageUsed": "Хранилище", + "percentageOfTotalUsed": "Доля (%)", + "bandwidth": "Пропускная способность", + "unused": { + "title": "Не используется", + "tips": "Это значение может неточно отражать свободное место, доступное Frigate, если на вашем диске есть другие файлы помимо записей Frigate. Frigate не отслеживает использование хранилища за пределами своих записей." + } + }, + "shm": { + "title": "Выделение разделяемой памяти", + "warning": "Текущеее значение разделяемой памяти в {{total}}MB слишком мало. Увеличьте его хотя бы до {{min_shm}}MB." + } + }, + "cameras": { + "title": "Камеры", + "overview": "Обзор", + "info": { + "cameraProbeInfo": "Информация о проверке камеры {{camera}}", + "streamDataFromFFPROBE": "Данные о потоке получены от ffprobe.", + "fetching": "Получение данных камеры", + "stream": "Поток {{idx}}", + "video": "Видео:", + "codec": "Кодек:", + "resolution": "Разрешение:", + "fps": "FPS:", + "unknown": "Неизвестно", + "audio": "Аудио:", + "error": "Ошибка: {{error}}", + "tips": { + "title": "Информация о тестировании камеры" + }, + "aspectRatio": "соотношение сторон" + }, + "framesAndDetections": "Кадры/детекции", + "label": { + "ffmpeg": "FFmpeg", + "camera": "камера", + "capture": "захват", + "skipped": "пропущено", + "detect": "детекция", + "cameraDetectionsPerSecond": "{{camName}} обнаружений в секунду", + "cameraSkippedDetectionsPerSecond": "{{camName}} пропущенных обнаружений в секунду", + "cameraFramesPerSecond": "{{camName}} кадров в секунду", + "overallFramesPerSecond": "общее количество кадров в секунду", + "overallDetectionsPerSecond": "общее количество обнаружений в секунду", + "overallSkippedDetectionsPerSecond": "общее количество пропущенных обнаружений в секунду", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} захват", + "cameraDetect": "{{camName}} обнаружения" + }, + "toast": { + "success": { + "copyToClipboard": "Данные тестирования скопированы в буфер обмена." + }, + "error": { + "unableToProbeCamera": "Не удалось протестировать камеру: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Обновлено: ", + "stats": { + "ffmpegHighCpuUsage": "Камера {{camera}} использует чрезмерно много ресурсов CPU в FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "Камера {{camera}} использует слишком много ресурсов CPU для детекции ({{detectAvg}}%)", + "healthy": "Система в порядке", + "reindexingEmbeddings": "Переиндексация эмбеддингов (выполнено {{processed}} %)", + "cameraIsOffline": "{{camera}} отключена", + "detectIsVerySlow": "{{detect}} идёт очень медленно ({{speed}} мс)", + "detectIsSlow": "{{detect}} идёт медленно ({{speed}} мс)", + "shmTooLow": "Объем выделенной памяти /dev/shm ({{total}} МБ) должен быть увеличен как минимум до {{min}} МБ." + }, + "enrichments": { + "title": "Обогащение данных", + "infPerSecond": "Выводов в секунду", + "embeddings": { + "image_embedding_speed": "Скорость векторизации изображений", + "plate_recognition_speed": "Скорость распознавания номеров", + "text_embedding_speed": "Скорость векторизации текста", + "face_embedding_speed": "Скорость векторизации лиц", + "face_recognition_speed": "Скорость распознавания лиц", + "text_embedding": "Векторизация текста", + "yolov9_plate_detection_speed": "Скорость обнаружения номеров YOLOv9", + "yolov9_plate_detection": "Обнаружение номеров YOLOv9", + "face_recognition": "Распознавание лиц", + "plate_recognition": "Распознавание номеров", + "image_embedding": "Векторизация изображений", + "review_description": "Описание проверки", + "review_description_speed": "Скорость просмотра описания", + "review_description_events_per_second": "Описание проверки", + "object_description": "Описание объекта", + "object_description_speed": "Скорость описания объекта", + "object_description_events_per_second": "Описание объекта", + "classification": "{{name}} Классификация", + "classification_speed": "{{name}}Классификация скорости", + "classification_events_per_second": "{{name}} событий классификации в секунду" + }, + "averageInf": "Среднее время обработки" + } +} diff --git a/web/public/locales/sk/audio.json b/web/public/locales/sk/audio.json new file mode 100644 index 0000000..460f94c --- /dev/null +++ b/web/public/locales/sk/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Reč", + "babbling": "Bľabotanie", + "yell": "Krik", + "bellow": "Pod", + "whispering": "Šepkanie", + "whoop": "Výskanie", + "laughter": "Smiech", + "crying": "Plač", + "sigh": "Vzdych", + "singing": "Spev", + "snicker": "Chichotanie", + "choir": "Zbor", + "yodeling": "Jódlovanie", + "chant": "Štrajkovanie", + "mantra": "Mantra", + "child_singing": "Detský spev", + "synthetic_singing": "Syntetický spev", + "rapping": "Repovanie", + "humming": "Bzučanie", + "groan": "Stenanie", + "grunt": "Zabručanie", + "whistling": "Pískot", + "breathing": "Dych", + "wheeze": "Sipenie", + "snoring": "Chrápanie", + "gasp": "Zalapanie po dychu", + "snort": "Funenie", + "cough": "Kašel", + "throat_clearing": "Odkašlanie", + "sneeze": "Kýchnutie", + "sniff": "Čuchanie", + "run": "Beh", + "shuffle": "Miešenie", + "footsteps": "Kroky", + "chewing": "Žuvanie", + "biting": "Hrizenie", + "bicycle": "Bicykel", + "car": "Auto", + "motorcycle": "Motocykel", + "bus": "Autobus", + "train": "Vlak", + "boat": "Čln", + "bird": "Vták", + "cat": "Mačka", + "dog": "Pes", + "horse": "Kôň", + "sheep": "Ovce", + "camera": "Kamera", + "pant": "Oddychávanie", + "gargling": "Grganie", + "stomach_rumble": "Škvŕkanie v žalúdku", + "burping": "Grganie", + "skateboard": "Skateboard", + "hiccup": "Škytavka", + "fart": "Prd", + "hands": "Ruky", + "finger_snapping": "Lusknutie prstom", + "clapping": "Tlieskanie", + "heartbeat": "Tlkot srdca", + "heart_murmur": "Srdcový šelest", + "cheering": "Fandenie", + "applause": "Potlesk", + "chatter": "Chatárčenie", + "crowd": "Dav", + "children_playing": "Deti hrajúce sa", + "animal": "Zviera", + "pets": "Domáce zvieratá", + "bark": "Kôra", + "yip": "Áno", + "howl": "Zavýjať", + "bow_wow": "Hlasitého protestu", + "growling": "Vrčanie", + "whimper_dog": "Psie kňučanie", + "purr": "Pradenie", + "meow": "Mňau", + "hiss": "Syčanie", + "caterwaul": "Kričať", + "livestock": "Hospodárske zvieratá", + "clip_clop": "Klepanie kopyt", + "neigh": "Eržanie", + "door": "Dvere", + "cattle": "Hovädzí dobytok", + "moo": "Búčanie", + "cowbell": "Kravský zvonec", + "mouse": "Myška", + "pig": "Prasa", + "oink": "Chrčanie", + "keyboard": "Klávesnica", + "goat": "Koza", + "bleat": "nariekať", + "fowl": "Sliepky", + "chicken": "Slepica", + "sink": "Umývadlo", + "cluck": "Kvákanie", + "cock_a_doodle_doo": "Kykyryký", + "blender": "Mixér", + "turkey": "Morka", + "gobble": "Hltať", + "clock": "Hodiny", + "duck": "Kačica", + "wild_animals": "Divoké zvieratá", + "toothbrush": "Zubná kefka", + "roaring_cats": "Revúce mačky", + "roar": "Revať", + "vehicle": "Vozidlo", + "quack": "Quack", + "scissors": "Nožnice", + "goose": "Hus", + "honk": "Truba", + "hair_dryer": "Sušič vlasov", + "chirp": "Cvrlikanie", + "squawk": "Škriekanie", + "pigeon": "Holub", + "coo": "Vrkanie", + "crow": "Vrana", + "caw": "Krákanie", + "owl": "Sova", + "hoot": "Húkanie", + "flapping_wings": "Mávanie krídel", + "dogs": "Psi", + "rats": "Potkany", + "patter": "Plácanie", + "insect": "Hmyz", + "cricket": "Cvrček", + "mosquito": "Komár", + "fly": "Mucha", + "buzz": "Bzučanie", + "frog": "Žaba", + "croak": "Kvákanie žaby", + "snake": "Had", + "rattle": "Hrkanie", + "whale_vocalization": "Veľrybí spev", + "music": "Hudba", + "musical_instrument": "Hudobný nástroj", + "plucked_string_instrument": "Drnkací strunový nástroj", + "guitar": "Gitara", + "electric_guitar": "Elektrická gitara", + "bass_guitar": "Basová gitara", + "acoustic_guitar": "Akustická gitara", + "steel_guitar": "Oceľová gitara", + "tapping": "Ťukanie", + "strum": "Brnkanie", + "banjo": "Banjo", + "sitar": "Sitár", + "mandolin": "Mandolína", + "zither": "Citera", + "ukulele": "Ukulele", + "piano": "Klavír", + "electric_piano": "Elektrický klavír", + "organ": "Organ", + "electronic_organ": "Elektronické organ", + "gong": "Gong", + "tubular_bells": "Trubicové zvony", + "mallet_percussion": "Palička perkusie", + "marimba": "Marimba", + "orchestra": "Orchester", + "brass_instrument": "Žesťový nástroj", + "french_horn": "Lesný roh", + "trumpet": "Rúrka", + "trombone": "Trombón", + "bowed_string_instrument": "Sláčikový nástroj", + "string_section": "Sláčiková sekcia", + "violin": "Husle", + "pizzicato": "Pizzicato", + "cello": "Cello", + "double_bass": "Kontrabas", + "wind_instrument": "Dychový nástroj", + "flute": "Flauta", + "saxophone": "Saxofón", + "clarinet": "Klarinet", + "harp": "Harfa", + "bell": "Zvon", + "church_bell": "Kostolný zvon", + "jingle_bell": "Rolnička", + "bicycle_bell": "Cyklistický zvonček", + "tuning_fork": "Ladička", + "chime": "Zvonenie", + "wind_chime": "Zvonkohra", + "harmonica": "Harmonika", + "accordion": "Akordeón", + "bagpipes": "Dudy", + "didgeridoo": "Didžeridu", + "theremin": "Theremin", + "singing_bowl": "Singing Bowl", + "scratching": "Škrabanie", + "pop_music": "Popová hudba", + "hip_hop_music": "Hip-hopová muzika", + "beatboxing": "Beatboxing", + "rock_music": "Rocková muzika", + "heavy_metal": "Heavy metal", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "progressive_rock": "Progressive Rock", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Psychadelický Rock", + "rhythm_and_blues": "Rythm & Blues", + "soul_music": "Soulová hudba", + "reggae": "Reggae", + "country": "Krajina", + "swing_music": "Swingová hudba", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folková hudba", + "middle_eastern_music": "Stredo-východná hudba", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Klasická hudba", + "opera": "Opera", + "electronic_music": "Elektronická hudba", + "house_music": "House hudba", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Elektronická hudba", + "electronic_dance_music": "Elektronická tanečná hudba", + "ambient_music": "Ambientná hudba", + "trance_music": "Trance hudba", + "music_of_latin_america": "Latinsko-americká hudba", + "salsa_music": "Salsa Music", + "flamenco": "Flamengo", + "blues": "Blues", + "music_for_children": "Hudba pre deti", + "new-age_music": "Novodobá hudba", + "vocal_music": "Vokálna hudba", + "a_capella": "A Capella", + "music_of_africa": "Africká hudba", + "afrobeat": "Afrobeat", + "christian_music": "Kresťanská hudba", + "gospel_music": "Gospelová hudba", + "music_of_asia": "Ázijská hudba", + "carnatic_music": "Karnatická hudba", + "music_of_bollywood": "Hudba z Bollywoodu", + "ska": "Ska", + "traditional_music": "Tradičná hudba", + "independent_music": "Nezávislá hudba", + "song": "Pieseň", + "background_music": "Hudba na pozadí", + "theme_music": "Tematická hudba", + "jingle": "Jingle", + "soundtrack_music": "Soundtracková hudba", + "lullaby": "Uspávanka", + "video_game_music": "Herná hudba", + "shuffling_cards": "Miešanie kariet", + "hammond_organ": "Hammondovy organ", + "synthesizer": "Syntezátor", + "sampler": "Sampler", + "harpsichord": "Cembalo", + "percussion": "Perkusia", + "drum_kit": "Bubny", + "drum_machine": "Bicí automat", + "drum": "Bubon", + "snare_drum": "Malý bubon", + "rimshot": "Rana na obruč", + "drum_roll": "Vírenie", + "bass_drum": "Basový bubon", + "timpani": "Tympány", + "tabla": "Tabla", + "cymbal": "Činel", + "hi_hat": "Hi-hat", + "wood_block": "Drevený blok", + "tambourine": "Tamburína", + "maraca": "Maraka", + "glockenspiel": "Zvonkohra", + "vibraphone": "Vibrafón", + "steelpan": "Ocelový bubon", + "christmas_music": "Vianočná hudba", + "dance_music": "Tanečná hudba", + "wedding_music": "Svadobná hudba", + "happy_music": "Šťastná hudba", + "sad_music": "Smutná hudba", + "tender_music": "Nežná hudba", + "exciting_music": "Vzrušujúca hudba", + "angry_music": "Naštvaná hudba", + "scary_music": "Strašidelná hudba", + "wind": "Vietor", + "rustling_leaves": "Šuštiace Listy", + "wind_noise": "Hluk Vetra", + "thunderstorm": "Búrka", + "thunder": "Hrom", + "water": "Voda", + "rain": "Dážď", + "raindrop": "Dažďové kvapky", + "rain_on_surface": "Dážď na povrchu", + "stream": "Prúd", + "waterfall": "Vodopád", + "ocean": "Oceán", + "waves": "Vlny", + "steam": "Para", + "gurgling": "Grganie", + "fire": "Oheň", + "crackle": "Praskať", + "sailboat": "Plachtenie", + "rowboat": "Veslica", + "motorboat": "Motorový čln", + "ship": "Loď", + "motor_vehicle": "Motorové vozidlo", + "toot": "Trúbenie", + "car_alarm": "Autoalarm", + "power_windows": "Elektrické okná", + "skidding": "Šmykom", + "tire_squeal": "Pískanie pneumatík", + "car_passing_by": "Prechádzajúce auto", + "race_car": "Závodné auto", + "truck": "Kamión", + "air_brake": "Vzduchová brzda", + "air_horn": "Vzduchový klaksón", + "reversing_beeps": "Pípanie pri cúvaní", + "ice_cream_truck": "Auto so zmrzlinou", + "emergency_vehicle": "Pohotovostné vozidlo", + "police_car": "Policajné auto", + "ambulance": "Ambulancia", + "fire_engine": "Hasiči", + "traffic_noise": "Hluk z dopravy", + "rail_transport": "Železničná preprava", + "train_whistle": "Húkanie vlaku", + "train_horn": "Rúrenie vlaku", + "railroad_car": "Železničný vagón", + "train_wheels_squealing": "Škrípanie kolies vlaku", + "subway": "Metro", + "aircraft": "Lietadlo", + "aircraft_engine": "Motor lietadla", + "jet_engine": "Tryskový motor", + "propeller": "Vrtuľa", + "helicopter": "Helikoptéra", + "fixed-wing_aircraft": "Lietadlo s pevnými krídlami", + "engine": "Motor", + "light_engine": "Ľahký motor", + "dental_drill's_drill": "Zubná vŕtačka", + "lawn_mower": "Kosačka", + "chainsaw": "Motorová píla", + "medium_engine": "Stredný motor", + "heavy_engine": "Ťažký motor", + "engine_knocking": "Klepanie motora", + "engine_starting": "Štartovanie motora", + "idling": "Bežiaci motor", + "accelerating": "Pridávanie plynu", + "doorbell": "Zvonček", + "ding-dong": "Cink", + "sliding_door": "Posuvné dvere", + "slam": "Búchnutie", + "knock": "Klepanie", + "tap": "Poklepanie", + "squeak": "Škrípanie", + "cupboard_open_or_close": "Otváranie alebo zatváranie skrine", + "drawer_open_or_close": "Otváranie alebo zatváranie šuplíka", + "dishes": "Riad", + "cutlery": "Príbory", + "chopping": "Krájanie", + "frying": "Vyprážanie", + "microwave_oven": "Mikrovnka", + "water_tap": "Vodovodný kohútik", + "bathtub": "Vaňa", + "toilet_flush": "Splachovanie toalety", + "electric_toothbrush": "Elektrická zubná kefka", + "vacuum_cleaner": "Vysávač", + "zipper": "Zips", + "keys_jangling": "Klepanie kľúčov", + "coin": "Mince", + "electric_shaver": "Elektrický holiaci strojček", + "typing": "Písanie", + "typewriter": "Písací stroj", + "computer_keyboard": "Počítačový kľúč", + "writing": "Písanie", + "alarm": "Alarm", + "telephone": "Telefón", + "telephone_bell_ringing": "Zvonenie telefónu", + "ringtone": "Vyzváňací tón", + "telephone_dialing": "Telefonické vytáčanie", + "dial_tone": "Vytáčací tón", + "busy_signal": "Zaneprázdnený signál", + "alarm_clock": "Budík", + "siren": "Siréna", + "civil_defense_siren": "Siréna civilnej obrany", + "buzzer": "Bzučiak", + "smoke_detector": "Detektor dymu", + "fire_alarm": "Požiarny Alarm", + "foghorn": "Hmlovka", + "whistle": "Zapískať", + "steam_whistle": "Parná píšťalka", + "mechanisms": "Mechanizmy", + "ratchet": "Račňa", + "tick": "Ťik", + "tick-tock": "Tik-tok", + "gears": "Ozubené kolesá", + "pulleys": "Kladky", + "sewing_machine": "Šijací stroj", + "mechanical_fan": "Mechanický ventilátor", + "air_conditioning": "Klimatizácia", + "cash_register": "Registračná pokladňa", + "printer": "Tlačiareň", + "single-lens_reflex_camera": "Jednooká zrkadlovka", + "tools": "Nástroje", + "hammer": "Kladivo", + "jackhammer": "Zbíjačka", + "sawing": "Pílenie", + "filing": "Podanie", + "sanding": "Brúsenie", + "power_tool": "Elektrické náradie", + "drill": "Vŕtačka", + "explosion": "Explózia", + "gunshot": "Výstrel", + "machine_gun": "Guľomet", + "fusillade": "Streľba", + "artillery_fire": "Delostrelecká paľba", + "cap_gun": "Kapslíková pištoľ", + "fireworks": "Ohňostroj", + "firecracker": "Petarda", + "burst": "Prasknutie", + "eruption": "Erupcia", + "boom": "Bum", + "wood": "Drevo", + "chop": "Nasekať", + "splinter": "Trieska", + "crack": "Prasknutie", + "glass": "Sklo", + "chink": "Cinknutie", + "shatter": "Rozbiť", + "silence": "Ticho", + "sound_effect": "Zvukový efekt", + "environmental_noise": "Okolitý hluk", + "static": "Statické", + "white_noise": "Biely šum", + "pink_noise": "Ružový šum", + "television": "Televízia", + "radio": "Rádio", + "field_recording": "Záznam v teréne", + "scream": "Kričať", + "sodeling": "Sodeling", + "chird": "Chord", + "change_ringing": "Zmeniť zvonenie", + "shofar": "Šofar", + "liquid": "Kvapalina", + "splash": "Šplechnutie", + "slosh": "Slosh", + "squish": "Vytlačiť", + "drip": "Kvapkať", + "pour": "Nalej", + "trickle": "Pokvapkať", + "gush": "Striekať", + "fill": "Vyplňte", + "spray": "Striekajte", + "pump": "Pumpa", + "stir": "Miešajte", + "boiling": "Varenie", + "sonar": "Sonar", + "arrow": "Šípka", + "whoosh": "Ktoosh", + "thump": "Palec", + "thunk": "Thunk", + "electronic_tuner": "Elektronický tuner", + "effects_unit": "Efektuje jednotky", + "chorus_effect": "Zborový efekt", + "basketball_bounce": "Odrážanie basketbalovej lopty", + "bang": "Bang", + "slap": "Buchnutie", + "whack": "Odpáliť", + "smash": "Rozbiť", + "breaking": "Prelomenie", + "bouncing": "Odskakovanie", + "whip": "Bič", + "flap": "Klapka", + "scratch": "Poškriabanie", + "scrape": "Škrabať", + "rub": "Potrieť", + "roll": "Rolovať", + "crushing": "Rozdrvovanie", + "crumpling": "Mačkanie", + "tearing": "Trhanie", + "beep": "Pípnutie", + "ping": "Ping", + "ding": "Ding", + "clang": "Zvonenie", + "squeal": "Kňučať", + "creak": "Vŕzganie", + "rustle": "Šuchot", + "whir": "Vrčanie", + "clatter": "Cvakať", + "sizzle": "Syčať", + "clicking": "Klikanie", + "clickety_clack": "Klikanie kľak", + "rumble": "Rachot", + "plop": "Prasknutie", + "hum": "Hmkanie", + "zing": "Zing", + "boing": "Boing", + "crunch": "Chrumnutie", + "sine_wave": "Sínusoida", + "harmonic": "Harmonický", + "chirp_tone": "Cvrlikací tón", + "pulse": "Pulz", + "inside": "Vnútri", + "outside": "Vonku", + "reverberation": "Dozvuk", + "echo": "Ozvena", + "noise": "Zvuk", + "mains_hum": "Hlavné Hum", + "distortion": "Skreslenie", + "sidetone": "Vedľajší tón", + "cacophony": "Kakofónia", + "throbbing": "Pulzujúci", + "vibration": "Vibrácia" +} diff --git a/web/public/locales/sk/common.json b/web/public/locales/sk/common.json new file mode 100644 index 0000000..f78c910 --- /dev/null +++ b/web/public/locales/sk/common.json @@ -0,0 +1,326 @@ +{ + "time": { + "untilForTime": "Do{{time}}", + "untilForRestart": "Do reštartu Frigate.", + "untilRestart": "Do reštartu", + "ago": "{{timeAgo}} pred časom", + "justNow": "Práve teraz", + "today": "Dnes", + "yesterday": "Včera", + "last7": "Posledných 7 dní", + "last14": "Posledných 14 dní", + "last30": "Posledných 30 dní", + "thisWeek": "Tento týždeň", + "lastWeek": "Minulý týždeň", + "thisMonth": "Tento mesiac", + "lastMonth": "Minulý mesiac", + "5minutes": "5 minút", + "10minutes": "10 minút", + "30minutes": "30 minút", + "1hour": "1 hodina", + "12hours": "12 hodín", + "24hours": "24 hodín", + "am": "ráno", + "yr": "{{time}}r", + "pm": "popoludní", + "year_one": "{{time}}rok", + "year_few": "{{time}}rokov", + "year_other": "{{time}}rokov", + "mo": "{{time}}mes", + "month_one": "{{time}}mesiac", + "month_few": "{{time}} mesiace", + "month_other": "{{time}} mesiaca", + "d": "{{time}}d", + "day_one": "{{time}}deň", + "day_few": "{{time}}dni", + "day_other": "{{time}}dni", + "h": "{{time}}h", + "hour_one": "{{time}}hodina", + "hour_few": "{{time}}hodiny", + "hour_other": "{{time}}hodin", + "m": "{{time}} min", + "s": "{{time}}s", + "minute_one": "{{time}}minuta", + "minute_few": "{{time}}minuty", + "minute_other": "{{time}}minut", + "second_one": "{{time}}sekunda", + "second_few": "{{time}}sekundy", + "second_other": "{{time}}sekund", + "formattedTimestamp": { + "12hour": "Deň MMM, h:mm:ss aaa", + "24hour": "Deň MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "inProgress": "Spracováva sa", + "invalidStartTime": "Neplatný čas štartu", + "invalidEndTime": "Neplatný čas ukončenia", + "never": "Nikdy" + }, + "selectItem": "Vyberte {{item}}", + "unit": { + "speed": { + "mph": "mph", + "kph": "Km/h" + }, + "length": { + "feet": "nohy", + "meters": "metrov" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kb/hodina", + "mbph": "MB/hodinu", + "gbph": "GB/hodinu" + } + }, + "readTheDocumentation": "Prečítajte si dokumentáciu", + "label": { + "back": "Choď späť", + "hide": "Skryť {{item}}", + "show": "Zobraziť {{item}}", + "ID": "ID", + "none": "None", + "all": "Všetko", + "other": "Iné" + }, + "button": { + "apply": "Použiť", + "reset": "Resetovať", + "done": "Hotovo", + "enabled": "Povolené", + "enable": "Povoliť", + "disabled": "Zakázané", + "disable": "Zakázať", + "save": "Uložiť", + "saving": "Ukladá sa…", + "cancel": "Zrušiť", + "close": "Zavrieť", + "copy": "Kopírovať", + "back": "Späť", + "history": "História", + "fullscreen": "Celá obrazovka", + "exitFullscreen": "Opustiť režim celú obrazovku", + "pictureInPicture": "Obraz v obraze", + "twoWayTalk": "Obojsmerná komunikácia", + "cameraAudio": "Zvuk kamery", + "on": "ON", + "off": "OFF", + "edit": "Upraviť", + "copyCoordinates": "Kopírovať súradnice", + "delete": "Odstrániť", + "yes": "Ano", + "no": "Nie", + "download": "Stiahnuť", + "info": "Informacie", + "suspended": "Pozastavené", + "export": "Exportovať", + "deleteNow": "Odstrániť teraz", + "next": "Ďalej", + "unsuspended": "Zrušte pozastavenie", + "play": "Hrať", + "unselect": "Zrušte výber", + "continue": "Pokračovať", + "copiedToClipboard": "Skopírované do schránky", + "modified": "Upravené", + "overridden": "Prepísané", + "resetToGlobal": "Obnoviť na globálne", + "resetToDefault": "Obnoviť predvolené", + "saveAll": "Uložiť všetko", + "savingAll": "Ukladá sa všetko…", + "undoAll": "Vrátiť späť všetko", + "retry": "Skúste to znova" + }, + "menu": { + "system": "Systém", + "systemMetrics": "Systémové metriky", + "configuration": "Konfigurácia", + "systemLogs": "Systémový záznam", + "settings": "Nastavenia", + "configurationEditor": "Editor konfigurácie", + "languages": "Jazyky", + "language": { + "en": "English (Angličtina)", + "es": "Español (Španielčina)", + "zhCN": "简体中文 (Zjednodušená čínština)", + "hi": "हिन्दी (Hindčina)", + "fr": "Français (Francúzština)", + "ar": "العربية (Arabčina)", + "pt": "Portugalčina (Portugalčina)", + "ptBR": "Português brasileiro (Brazílska Portugalčina)", + "ru": "Русский (Ruština)", + "de": "nemčina (Nemčina)", + "ja": "日本語 (Japončina)", + "tr": "Türkçe (Turečtina)", + "it": "Italiano (Taliančina)", + "nl": "Nederlands (Holandčina)", + "sv": "Svenska (Švédčina)", + "cs": "Czech (Čeština)", + "nb": "Norsk Bokmål (Norský Bokmål)", + "ko": "한국어 (Korejština)", + "vi": "Tiếng Việt (Vietnamština)", + "fa": "فارسی (Perština)", + "pl": "Polski (Polština)", + "uk": "Українська (Ukrainština)", + "he": "עברית (Hebrejština)", + "el": "Ελληνικά (Gréčtina)", + "ro": "Română (Rumunčina)", + "hu": "Magyar (Maďarština)", + "fi": "Suomi (Fínčina)", + "da": "Dansk (Dánština)", + "sk": "Slovenčina (Slovenčina)", + "yue": "粵語 (Kantónčina)", + "th": "ไทย (Thajčina)", + "ca": "Català (Katalánčina)", + "sr": "Српски (Serbsky)", + "sl": "Slovinština (Slovinsko)", + "lt": "Lietuvių (Lithuanian)", + "bg": "Български (Bulgarian)", + "gl": "Galego (Galician)", + "id": "Bahasa Indonesia (Indonesian)", + "ur": "اردو (Urdu)", + "withSystem": { + "label": "Použiť systémové nastavenia pre jazyk" + }, + "hr": "Hrvatski (Croatian)" + }, + "restart": "Reštartovať Frigate", + "live": { + "title": "Naživo", + "allCameras": "Všetky kamery", + "cameras": { + "title": "Kamery", + "count_one": "{{count}}kamera", + "count_few": "{{count}}kamery", + "count_other": "{{count}}kamier" + } + }, + "export": "Exportovať", + "uiPlayground": "UI ihrisko", + "faceLibrary": "Knižnica Tvárov", + "user": { + "title": "Užívateľ", + "account": "Účet", + "current": "Aktuálny používateľ: {{user}}", + "anonymous": "anonymný", + "logout": "Odhlásiť", + "setPassword": "Nastaviť heslo" + }, + "appearance": "Vzhľad", + "darkMode": { + "label": "Tmavý režim", + "light": "Svetlý", + "dark": "Tma", + "withSystem": { + "label": "Použiť systémové nastavenia pre svetlý a tmavý režim" + } + }, + "withSystem": "Systém", + "theme": { + "label": "Téma", + "blue": "Modrá", + "green": "Zelená", + "nord": "Polárna", + "red": "Červená", + "highcontrast": "Vysoký kontrast", + "default": "Predvolené" + }, + "help": "Pomocník", + "documentation": { + "title": "Dokumentácia", + "label": "Dokumentácia Frigate" + }, + "review": "Recenzia", + "explore": "Preskúmať", + "classification": "Klasifikácia", + "profiles": "Profily", + "actions": "Akcie", + "features": "Vlastnosti", + "chat": "Chat" + }, + "toast": { + "copyUrlToClipboard": "Adresa URL bola skopírovaná do schránky.", + "save": { + "title": "Uložiť", + "error": { + "title": "Chyba pri ukladaní zmien konfigurácie: {{errorMessage}}", + "noMessage": "Chyba pri ukladaní zmien konfigurácie" + }, + "success": "Zmeny konfigurácie boli úspešne uložené." + } + }, + "role": { + "title": "Rola", + "admin": "Správca", + "viewer": "Divák", + "desc": "Správcovia majú plný prístup ku všetkým funkciám v užívateľskom rozhraní Frigate. Diváci sú obmedzení na sledovanie kamier, položiek prehľadu a historických záznamov v UI." + }, + "pagination": { + "label": "stránkovanie", + "previous": { + "title": "Predchádzajúci", + "label": "Ísť na predchádzajúcu stranu" + }, + "next": { + "title": "Ďalšia", + "label": "Ísť na ďalšiu stranu" + }, + "more": "Viac strán" + }, + "accessDenied": { + "documentTitle": "Prístup odmietnutý - Frigate", + "title": "Prístup odmietnutý", + "desc": "Nemáte oprávnenie zobraziť túto stránku." + }, + "notFound": { + "documentTitle": "Nenájdené - Frigate", + "title": "404", + "desc": "Stránka nenájdená" + }, + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} a {{1}}", + "many": "{{items}}, a {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Voliteľné", + "internalID": "Interné ID Frigate používa v konfigurácii a databáze" + }, + "no_items": "Žiadne položky", + "validation_errors": "Chyby overenia", + "credentialField": { + "savedPlaceholder": "Uložené – pre aktuálne informácie nechajte prázdne" + } +} diff --git a/web/public/locales/sk/components/auth.json b/web/public/locales/sk/components/auth.json new file mode 100644 index 0000000..5d44c93 --- /dev/null +++ b/web/public/locales/sk/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Používateľské meno", + "password": "Heslo", + "login": "Prihlásenie", + "errors": { + "usernameRequired": "Vyžaduje sa používateľské meno", + "passwordRequired": "Heslo je povinné", + "rateLimit": "Prekročený limit. Skúste to znova neskôr.", + "loginFailed": "Prihlásenie zlyhalo", + "unknownError": "Neznáma chyba. Skontrolujte protokoly.", + "webUnknownError": "Neznáma chyba. Skontrolujte protokoly konzoly." + }, + "firstTimeLogin": "Snažíte sa prihlásiť prvýkrát? Prihlasovacie údaje sú vytlačené v protokoloch Frigate." + } +} diff --git a/web/public/locales/sk/components/camera.json b/web/public/locales/sk/components/camera.json new file mode 100644 index 0000000..0048c19 --- /dev/null +++ b/web/public/locales/sk/components/camera.json @@ -0,0 +1,90 @@ +{ + "group": { + "label": "Skupiny Kamier", + "add": "Pridať skupinu kamier", + "edit": "Upraviť skupinu kamier", + "delete": { + "label": "Odstrániť skupinu kamier", + "confirm": { + "title": "Potvrďte Odstránenie", + "desc": "Ste si istý že chcete vymazať skupinu kamier {{name}}?" + } + }, + "name": { + "label": "Meno", + "placeholder": "Zadajte meno…", + "errorMessage": { + "mustLeastCharacters": "Názov skupiny kamier musí mať aspoň 2 znaky.", + "exists": "Názov skupiny kamier už existuje.", + "nameMustNotPeriod": "Názov skupiny kamier nesmie obsahovať bodku.", + "invalid": "Neplatný názov skupiny kamier." + } + }, + "cameras": { + "label": "Kamery", + "desc": "Vyberte kamery pre túto skupinu." + }, + "icon": "Ikona", + "success": "Skupina kamier ({{name}}) bola uložená.", + "camera": { + "setting": { + "label": "Nastavenia streamovania z kamery", + "title": "Nastavenia streamovania {{cameraName}}", + "desc": "Zmeňte možnosti živého vysielania pre ovládací panel tejto skupiny kamier. Tieto nastavenia sú špecifické pre zariadenie/prehliadač.", + "audioIsAvailable": "Pre tento stream je k dispozícii zvuk", + "audioIsUnavailable": "Zvuk nie je pre tento stream k dispozícii", + "audio": { + "tips": { + "title": "Zvuk musí byť vyvedený z vašej kamery a nakonfigurovaný v go2rtc pre tento stream.", + "document": "Prečítajte si dokumentáciu " + } + }, + "stream": "Prúd", + "placeholder": "Vyberte prúd", + "streamMethod": { + "label": "Metóda streamovania", + "placeholder": "Vyberte metódu vysielania", + "method": { + "noStreaming": { + "label": "Žiadny stream", + "desc": "Snímky z kamery sa budú aktualizovať iba raz za minútu a nebude prebiehať žiadne živé vysielanie." + }, + "smartStreaming": { + "label": "Inteligentné streamovanie (odporúčané)", + "desc": "Inteligentné streamovanie aktualizuje obraz z kamery raz za minútu, keď sa neprejavuje žiadna detekovateľná aktivita, aby sa šetrila šírka pásma a zdroje. Keď sa zistí aktivita, obraz sa plynule prepne na živý stream." + }, + "continuousStreaming": { + "label": "Nepretržité streamovanie", + "desc": { + "title": "Obraz z kamery bude vždy vysielaný naživo, keď bude viditeľný na palubnej doske, aj keď nebude detekovaná žiadna aktivita.", + "warning": "Nepretržité streamovanie môže spôsobiť vysoké využitie šírky pásma a problémy s výkonom. Používajte opatrne." + } + } + } + }, + "compatibilityMode": { + "label": "Režim kompatibility", + "desc": "Túto možnosť povoľte iba v prípade, že živý prenos z vašej kamery zobrazuje farebné artefakty a na pravej strane obrazu sa nachádza diagonálna čiara." + } + }, + "birdseye": "Vtáčie oko" + }, + "showAll": "Zobraziť všetky skupiny kamier", + "showLess": "Zobraziť menej", + "editGroups": "Upraviť skupiny kamier" + }, + "debug": { + "options": { + "label": "Nastavenia", + "title": "Možnosti", + "showOptions": "Zobraziť možnosti", + "hideOptions": "Skryť možnosti" + }, + "boundingBox": "Hranica", + "timestamp": "Časová pečiatka", + "zones": "Zóny", + "mask": "Maska", + "motion": "Pohyb", + "regions": "Kraje" + } +} diff --git a/web/public/locales/sk/components/dialog.json b/web/public/locales/sk/components/dialog.json new file mode 100644 index 0000000..6eab291 --- /dev/null +++ b/web/public/locales/sk/components/dialog.json @@ -0,0 +1,144 @@ +{ + "restart": { + "title": "Ste si istý, že chcete reštartovať Frigate ?", + "button": "Reštart", + "restarting": { + "title": "Frigate sa reštartuje", + "content": "Táto stránka bude obnovená o {{countdown}} sekúnd.", + "button": "Vynútiť opätovné načítanie teraz" + }, + "description": "Počas reštartu bude Frigate nakrátko zastavený." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Odoslať do Frigate+", + "desc": "Objekty na miestach, ktorým sa chcete vyhnúť, nie sú falošne pozitívne. Ich odoslanie ako falošne pozitívnych výsledkov spôsobí zmätok v modeli." + }, + "review": { + "question": { + "label": "Potvrďte tento štítok pre Frigate Plus", + "ask_a": "Je tento objekt typom {{label}}?", + "ask_an": "Tento objekt je {{label}}?", + "ask_full": "Je tento objekt typom {{untranslatedLabel}}{{translatedLabel}}?" + }, + "state": { + "submitted": "Predložené" + } + } + }, + "video": { + "viewInHistory": "Zobraziť v histórii" + } + }, + "export": { + "time": { + "fromTimeline": "Vyberte z časovej osi", + "custom": "Vlastné", + "start": { + "title": "Čas začiatku", + "label": "Vyberte Čas začiatku" + }, + "end": { + "title": "Čas ukončenia", + "label": "Vybrat čas ukončenia" + }, + "lastHour_one": "Minulu hodinu", + "lastHour_few": "Minule{{count}}hodiny", + "lastHour_other": "Minulych{{count}}hodin" + }, + "name": { + "placeholder": "Pomenujte Export" + }, + "select": "Vybrat", + "export": "Exportovať", + "selectOrExport": "Vybrať pre Export", + "toast": { + "success": "Export bol úspešne spustený. Súbor si pozrite na stránke exportov.", + "error": { + "failed": "Chyba spustenia exportu: {{error}}", + "endTimeMustAfterStartTime": "Čas konca musí byť po čase začiatku", + "noVaildTimeSelected": "Nie je vybrané žiadne platné časové obdobie" + }, + "view": "Zobraziť" + }, + "fromTimeline": { + "saveExport": "Uložiť Export", + "previewExport": "Export ukážky" + }, + "case": { + "newCaseOption": "Vytvoriť nový prípad", + "newCaseNamePlaceholder": "Názov nového prípadu", + "newCaseDescriptionPlaceholder": "Popis prípadu", + "label": "Prípad", + "nonAdminHelp": "Pre tieto exporty bude vytvorený nový prípad.", + "placeholder": "Vyberte prípad" + }, + "queueing": "Zaradenie exportu do frontu...", + "tabs": { + "export": "Jedna kamera", + "multiCamera": "Viac kamier" + }, + "multiCamera": { + "cameraSelection": "Kamery", + "cameraSelectionHelp": "Kamery so sledovanými objektmi v tomto časovom rozsahu sú vopred vybrané", + "checkingActivity": "Kontrola aktivity kamery...", + "noCameras": "Nie sú k dispozícii žiadne kamery" + } + }, + "streaming": { + "label": "Stream", + "restreaming": { + "disabled": "Opätovné streamovanie nie je pre túto kameru povolené.", + "desc": { + "title": "Pre ďalšie možnosti živého náhľadu a zvuku pre túto kameru nastavte go2rtc.", + "readTheDocumentation": "Prečítajte si dokumentáciu" + } + }, + "showStats": { + "label": "Zobraziť štatistiky streamu", + "desc": "Povoľte túto možnosť, ak chcete zobraziť štatistiky streamu ako prekrytie na obraze z kamery." + }, + "debugView": "Zobrazenie ladenia" + }, + "search": { + "saveSearch": { + "label": "Uložiť vyhľadávanie", + "desc": "Zadajte názov pre toto uložené vyhľadávanie.", + "placeholder": "Zadajte názov pre vyhľadávanie", + "overwrite": "{{searchName}} už existuje. Uložením sa prepíše existujúca hodnota.", + "success": "Hľadanie ({{searchName}}) bolo uložené.", + "button": { + "save": { + "label": "Uložte toto vyhľadávanie" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "Potvrďte Odstrániť", + "desc": { + "selected": "Naozaj chcete odstrániť všetky nahrané videá spojené s touto položkou recenzie?

    Podržte kláves Shift, aby ste v budúcnosti toto dialógové okno obišli." + }, + "toast": { + "success": "Videozáznam spojený s vybranými položkami recenzie bol úspešne odstránený.", + "error": "Nepodarilo sa odstrániť: {{error}}" + } + }, + "button": { + "export": "Exportovať", + "markAsReviewed": "Označiť ako skontrolované", + "deleteNow": "Odstrániť teraz", + "markAsUnreviewed": "Označiť ako neskontrolované" + } + }, + "imagePicker": { + "selectImage": "Výber miniatúry sledovaného objektu", + "search": { + "placeholder": "Hľadať podľa štítku alebo podštítku..." + }, + "noImages": "Pre tuto kameru sa nenašli žiadne miniatúry", + "unknownLabel": "Uložený obrázok spúšťača" + } +} diff --git a/web/public/locales/sk/components/filter.json b/web/public/locales/sk/components/filter.json new file mode 100644 index 0000000..ae1dbfd --- /dev/null +++ b/web/public/locales/sk/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filter", + "labels": { + "label": "Označenia", + "all": { + "title": "Všetky popisky", + "short": "Štítky" + }, + "count_one": "{{count}} Štítok", + "count_other": "{{count}} Štítkov" + }, + "zones": { + "label": "Zóny", + "all": { + "title": "Všetky zóny", + "short": "Zóny" + } + }, + "dates": { + "selectPreset": "Vyberte predvoľbu…", + "all": { + "title": "Všetky dátumy", + "short": "Dátumy" + } + }, + "more": "Viac filtrov", + "reset": { + "label": "Obnoviť filtre na predvolené hodnoty" + }, + "timeRange": "Časový rozsah", + "subLabels": { + "label": "Podštítky", + "all": "Všetky vedľajšie štítky" + }, + "score": "Skóre", + "estimatedSpeed": "Odhadovaná rýchlosť ({{unit}})", + "features": { + "label": "Vlastnosti", + "hasSnapshot": "Má snímku", + "hasVideoClip": "Má videoklip", + "submittedToFrigatePlus": { + "label": "Odoslané do Frigate+", + "tips": "Najprv musíte filtrovať sledované objekty, ktoré majú snímku.

    Sledované objekty bez snímky nie je možné odoslať do Frigate+." + } + }, + "sort": { + "label": "Zoradiť", + "dateAsc": "Dátum (Vzostupne)", + "dateDesc": "Dátum (Zostupne)", + "scoreAsc": "Skóre objektu (Vzostupne)", + "scoreDesc": "Skóre objektu (zostupne)", + "speedAsc": "Odhadovaná rýchlosť (vzostupne)", + "speedDesc": "Odhadovaná rýchlosť (zostupne)", + "relevance": "Relevantnosť" + }, + "cameras": { + "label": "Filter kamier", + "all": { + "title": "Všetky kamery", + "short": "Kamery" + } + }, + "classes": { + "label": "Triedy", + "all": { + "title": "Všetky triedy" + }, + "count_one": "Trieda {{count}}", + "count_other": "Triedy {{count}}" + }, + "review": { + "showReviewed": "Zobraziť skontrolované" + }, + "motion": { + "showMotionOnly": "Zobraziť len pohyb" + }, + "explore": { + "settings": { + "title": "Nastavenia", + "defaultView": { + "title": "Predvolené zobrazenie", + "desc": "Ak nie sú vybraté žiadne filtre, zobrazte súhrn naposledy sledovaných objektov pre každý štítok alebo zobrazte nefiltrovanú mriežku.", + "summary": "Zhrnutie", + "unfilteredGrid": "Nefiltrovaná mriežka" + }, + "gridColumns": { + "title": "Stĺpce mriežky", + "desc": "Vyberte počet stĺpcov v mriežkovom zobrazení." + }, + "searchSource": { + "label": "Vyhľadať zdroj", + "desc": "Vyberte, či chcete vyhľadávať v miniatúrach alebo v popisoch sledovaných objektov.", + "options": { + "thumbnailImage": "Obrázok miniatúry", + "description": "Popis" + } + } + }, + "date": { + "selectDateBy": { + "label": "Vyberte dátum, podľa ktorého chcete filtrovať" + } + } + }, + "logSettings": { + "label": "Úroveň denníka filtra", + "filterBySeverity": "Filtrujte protokoly podľa závažnosti", + "loading": { + "title": "Načítava sa", + "desc": "Keď sa panel protokolov posunie nadol, nové protokoly sa automaticky streamujú hneď po ich pridaní." + }, + "disableLogStreaming": "Zakázať streamovanie denníka", + "allLogs": "Všetky denníky" + }, + "trackedObjectDelete": { + "title": "Potvrďte Odstrániť", + "desc": "Odstránením týchto sledovaných objektov ({{objectLength}}) sa odstráni snímka, všetky uložené vnorenia a všetky súvisiace položky životného cyklu objektu. Zaznamenané zábery týchto sledovaných objektov v zobrazení História NEBUDÚ odstránené.

    Naozaj chcete pokračovať?

    Podržte kláves Shift, aby ste v budúcnosti toto dialógové okno obišli.", + "toast": { + "success": "Sledované objekty boli úspešne odstránené.", + "error": "Nepodarilo sa odstrániť sledované objekty: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrujte podľa masky zóny" + }, + "recognizedLicensePlates": { + "title": "Rozpoznané evidenčné čísla vozidiel", + "loadFailed": "Nepodarilo sa načítať rozpoznané evidenčné čísla vozidiel.", + "loading": "Načítavajú sa rozpoznané evidenčné čísla…", + "placeholder": "Zadajte text pre vyhľadávanie evidenčných čísel…", + "noLicensePlatesFound": "Neboli nájdené evidenčné čísla vozidiel.", + "selectPlatesFromList": "Vyberte jeden alebo viacero tanierov zo zoznamu.", + "selectAll": "Vybrať všetko", + "clearAll": "Vymazať všetko" + }, + "attributes": { + "label": "Klasifikačné Atribúty", + "all": "Všetky Atribúty" + } +} diff --git a/web/public/locales/sk/components/icons.json b/web/public/locales/sk/components/icons.json new file mode 100644 index 0000000..dc780fb --- /dev/null +++ b/web/public/locales/sk/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Zvoľte ikonu", + "search": { + "placeholder": "Hľadať ikonu…" + } + } +} diff --git a/web/public/locales/sk/components/input.json b/web/public/locales/sk/components/input.json new file mode 100644 index 0000000..e13e5ea --- /dev/null +++ b/web/public/locales/sk/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Stiahnuť Video", + "toast": { + "success": "Video pre náhľad sa začalo sťahovanie." + } + } + } +} diff --git a/web/public/locales/sk/components/player.json b/web/public/locales/sk/components/player.json new file mode 100644 index 0000000..fed9c0b --- /dev/null +++ b/web/public/locales/sk/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "V danom čase nenájdená žiadna nahrávka", + "noPreviewFound": "Náhľad nenájdený", + "noPreviewFoundFor": "Žiadný náhľad nájdený pre {{cameraName}}", + "submitFrigatePlus": { + "title": "Odoslať tento rám na Frigate+?", + "submit": "Odoslať" + }, + "livePlayerRequiredIOSVersion": "Pre tento typ živého vysielania je potrebný systém iOS 17.1 alebo novší.", + "streamOffline": { + "title": "Streamujte Offline", + "desc": "V streame detect {{cameraName}} neboli prijaté žiadne snímky, skontrolujte protokoly chýb" + }, + "cameraDisabled": "Kamera je zakázaná", + "stats": { + "streamType": { + "title": "Typ streamu:", + "short": "Typ" + }, + "bandwidth": { + "title": "Šírka pásma:", + "short": "Šírka pásma" + }, + "latency": { + "title": "Latencia:", + "value": "{{seconds}} sekund", + "short": { + "title": "Latencia", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Celkový počet snímok:", + "droppedFrames": { + "title": "Znížené snímky:", + "short": { + "title": "Spadol", + "value": "{{droppedFrames}} snimku" + } + }, + "decodedFrames": "Dekódované snímky:", + "droppedFrameRate": "Frekvencia stratených snímok:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Snímok bol úspešne odoslaný službe Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Nepodarilo sa odoslať snímku službe Frigate+" + } + } +} diff --git a/web/public/locales/sk/config/cameras.json b/web/public/locales/sk/config/cameras.json new file mode 100644 index 0000000..f6bfc32 --- /dev/null +++ b/web/public/locales/sk/config/cameras.json @@ -0,0 +1,26 @@ +{ + "birdseye": { + "order": { + "label": "Pozícia", + "description": "Číselná pozícia ovládajúca poradie kamery v rozložení Birdseye." + } + }, + "detect": { + "label": "Detekcia objektov", + "enabled": { + "label": "Povoliť detekciu objektov" + } + }, + "ffmpeg": { + "output_args": { + "detect": { + "label": "Zistiť výstupné argumenty", + "description": "Predvolené výstupné argumenty pre detekciu streamov rolí." + }, + "record": { + "label": "Zaznamenajte výstupné argumenty", + "description": "Predvolené výstupné argumenty pre streamy rolí záznamov." + } + } + } +} diff --git a/web/public/locales/sk/config/global.json b/web/public/locales/sk/config/global.json new file mode 100644 index 0000000..f6bfc32 --- /dev/null +++ b/web/public/locales/sk/config/global.json @@ -0,0 +1,26 @@ +{ + "birdseye": { + "order": { + "label": "Pozícia", + "description": "Číselná pozícia ovládajúca poradie kamery v rozložení Birdseye." + } + }, + "detect": { + "label": "Detekcia objektov", + "enabled": { + "label": "Povoliť detekciu objektov" + } + }, + "ffmpeg": { + "output_args": { + "detect": { + "label": "Zistiť výstupné argumenty", + "description": "Predvolené výstupné argumenty pre detekciu streamov rolí." + }, + "record": { + "label": "Zaznamenajte výstupné argumenty", + "description": "Predvolené výstupné argumenty pre streamy rolí záznamov." + } + } + } +} diff --git a/web/public/locales/sk/config/groups.json b/web/public/locales/sk/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sk/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sk/config/validation.json b/web/public/locales/sk/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sk/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sk/objects.json b/web/public/locales/sk/objects.json new file mode 100644 index 0000000..eb36ec1 --- /dev/null +++ b/web/public/locales/sk/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Osoba", + "bicycle": "Bicykel", + "car": "Auto", + "motorcycle": "Motocykel", + "airplane": "Lietadlo", + "bus": "Autobus", + "train": "Vlak", + "boat": "Čln", + "traffic_light": "Semafor", + "fire_hydrant": "Požiarny hydrant", + "street_sign": "Značka ulice", + "stop_sign": "Značka Stop", + "parking_meter": "Parkovací automat", + "bench": "Lavička", + "bird": "Vták", + "cat": "Mačka", + "dog": "Pes", + "horse": "Kôň", + "sheep": "Ovce", + "cow": "Krava", + "elephant": "Slon", + "bear": "Medveď", + "zebra": "Zebra", + "giraffe": "Žirafa", + "hat": "Čiapka", + "backpack": "Batoh", + "umbrella": "Dáždnik", + "shoe": "Topánka", + "eye_glasses": "Okuliare", + "handbag": "Kabelka", + "tie": "Kravata", + "suitcase": "Kufor", + "frisbee": "Frisbee", + "skis": "Lyže", + "snowboard": "Snowboard", + "sports_ball": "Športová lopta", + "kite": "Drak", + "baseball_bat": "Bejzbalová pálka", + "baseball_glove": "Baseballová rukavica", + "skateboard": "Skateboard", + "surfboard": "Surfová doska", + "tennis_racket": "Tenisová raketa", + "bottle": "Fľaša", + "plate": "Doska", + "wine_glass": "Pohár na víno", + "cup": "Pohár", + "fork": "Vidlička", + "knife": "Nôž", + "spoon": "Lyžica", + "bowl": "Misa", + "banana": "Banán", + "apple": "Jablko", + "animal": "Zviera", + "sandwich": "Sendvič", + "orange": "Pomaranč", + "broccoli": "Brokolica", + "bark": "Kôra", + "carrot": "Mrkva", + "hot_dog": "Hot Dog", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Koláč", + "chair": "Stolička", + "couch": "Gauč", + "potted_plant": "Rastlina v kvetináči", + "bed": "Posteľ", + "mirror": "Zrkadlo", + "dining_table": "Jedálenský stôl", + "window": "okno", + "desk": "Stôl", + "toilet": "Toaleta", + "door": "Dvere", + "tv": "TV", + "laptop": "Laptop", + "mouse": "Myška", + "remote": "Diaľkové ovládanie", + "keyboard": "Klávesnica", + "goat": "Koza", + "cell_phone": "Mobilný telefón", + "microwave": "Mikrovlnná rúra", + "oven": "Rúra", + "toaster": "Hriankovač", + "sink": "Umývadlo", + "refrigerator": "Chladnička", + "blender": "Mixér", + "book": "Kniha", + "clock": "Hodiny", + "vase": "Váza", + "toothbrush": "Zubná kefka", + "hair_brush": "Kefa na vlasy", + "vehicle": "Vozidlo", + "squirrel": "Veverička", + "scissors": "Nožnice", + "teddy_bear": "Medvedík", + "hair_dryer": "Sušič vlasov", + "deer": "Jeleň", + "fox": "Líška", + "rabbit": "Zajac", + "raccoon": "Mýval", + "robot_lawnmower": "Robotická kosačka", + "waste_bin": "Odpadkový kôš", + "on_demand": "Na požiadanie", + "face": "Tvár", + "license_plate": "Evidenčné Číslo Vozidla", + "package": "Balíček", + "bbq_grill": "Gril", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Čistič", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/web/public/locales/sk/views/chat.json b/web/public/locales/sk/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sk/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sk/views/classificationModel.json b/web/public/locales/sk/views/classificationModel.json new file mode 100644 index 0000000..8075240 --- /dev/null +++ b/web/public/locales/sk/views/classificationModel.json @@ -0,0 +1,196 @@ +{ + "documentTitle": "Klasifikačné modely - Frigate", + "button": { + "deleteClassificationAttempts": "Odstrániť Obrázky Klasifikácie", + "renameCategory": "Premenovať Triedu", + "deleteCategory": "Odstrániť Triedu", + "deleteImages": "Odstrániť Obrázky", + "trainModel": "Trénovať Model", + "addClassification": "Pridať Klasifikáciu", + "deleteModels": "Odstrániť Modely", + "editModel": "Upraviť Model" + }, + "toast": { + "success": { + "deletedCategory_one": "Vymazaná Trieda", + "deletedCategory_few": "", + "deletedCategory_other": "", + "deletedImage_one": "Vymazané Obrázky", + "deletedImage_few": "", + "deletedImage_other": "", + "categorizedImage": "Obrázok bol úspešne klasifikovaný", + "trainedModel": "Úspešne vyškolený model.", + "trainingModel": "Úspešne spustené trénovanie modelu.", + "deletedModel_one": "Úspešne zmazaný {{count}} model", + "deletedModel_few": "Úspešne zmazané {{count}} modely", + "deletedModel_other": "Úspešne zmazaných {{count}} modelov", + "updatedModel": "Úspešne zmenená konfigurácia modelu", + "renamedCategory": "Úspešne premenovaná trieda na {{name}}" + }, + "error": { + "deleteImageFailed": "Nepodarilo sa odstrániť: {{errorMessage}}", + "deleteCategoryFailed": "Nepodarilo sa odstrániť triedu: {{errorMessage}}", + "categorizeFailed": "Nepodarilo sa kategorizovať obrázok: {{errorMessage}}", + "trainingFailed": "Trénovanie modelu zlyhalo. Skontroluj záznamy Frigate pre viac podrobností.", + "deleteModelFailed": "Nepodarilo sa odstrániť model: {{errorMessage}}", + "trainingFailedToStart": "Neuspešné spustenie trénovania modelu: {{errorMessage}}", + "updateModelFailed": "Chyba pri aktualizácii modelu: {{errorMessage}}", + "renameCategoryFailed": "Chyba pri premenovaní triedy: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Odstrániť Triedu", + "desc": "Naozaj chcete odstrániť triedu {{name}}? Týmto sa natrvalo odstránia všetky súvisiace obrázky a bude potrebné pretrénovať model.", + "minClassesTitle": "Nemožete zmazať triedu", + "minClassesDesc": "Klasifikačný model musí mať aspoň 2 triedy. Pred odstránením tejto triedy pridajte ďalšiu triedu." + }, + "deleteDatasetImages": { + "title": "Odstrániť obrázky množiny údajov", + "desc_one": "Naozaj chcete odstrániť {{count}} obrázok z {{dataset}}? Túto akciu nie je možné vrátiť späť a bude si vyžadovať pretrénovanie modelu.", + "desc_few": "Naozaj chcete odstrániť {{count}} obrázky z {{dataset}}? Túto akciu nie je možné vrátiť späť a bude si vyžadovať pretrénovanie modelu.", + "desc_other": "Naozaj chcete odstrániť {{count}} obrázkov z {{dataset}}? Túto akciu nie je možné vrátiť späť a bude si vyžadovať pretrénovanie modelu." + }, + "deleteTrainImages": { + "title": "Odstrániť Trénovacie Obrázky", + "desc_one": "Naozaj chcete odstrániť {{count}} obrázok? Túto akciu nie je možné vrátiť späť.", + "desc_few": "Naozaj chcete odstrániť {{count}} obrázky? Túto akciu nie je možné vrátiť späť.", + "desc_other": "Naozaj chcete odstrániť {{count}} obrázkov? Túto akciu nie je možné vrátiť späť." + }, + "renameCategory": { + "title": "Premenovať Triedu", + "desc": "Zadajte nové meno pre {{name}}. Budete musieť model pretrénovať, aby sa zmena názvu prejavila." + }, + "description": { + "invalidName": "Neplatné meno. Mená môžu obsahovať iba písmená, čísla, medzery, apostrofy, podčiarkovníky a spojovníky." + }, + "train": { + "title": "Posledné klasifikácie", + "aria": "Vyberte Nedávne Klasifikácie", + "titleShort": "Nedávne" + }, + "categories": "Triedy", + "createCategory": { + "new": "Vytvorenie novej triedy" + }, + "categorizeImageAs": "Klasifikovať obrázok ako:", + "categorizeImage": "Klasifikovať obrázok", + "noModels": { + "object": { + "title": "Žiadne modely klasifikácie objektov", + "description": "Vytvorte si vlastný model na klasifikáciu detekovaných objektov.", + "buttonText": "Vytvorte objektový model" + }, + "state": { + "title": "Žiadne modely klasifikácie štátov", + "description": "Vytvorte si vlastný model na monitorovanie a klasifikáciu zmien stavu v špecifických oblastiach kamery.", + "buttonText": "Vytvorte model stavu" + } + }, + "wizard": { + "title": "Vytvorte novú klasifikáciu", + "steps": { + "nameAndDefine": "Názov a definícia", + "stateArea": "Štátna oblasť", + "chooseExamples": "Vyberte Príklady" + }, + "step1": { + "description": "Stavové modely monitorujú oblasti pevných kamier a sledujú zmeny (napr. otvorenie/zatvorenie dverí). Objektové modely pridávajú klasifikácie k detekovaným objektom (napr. známe zvieratá, doručovatelia atď.).", + "name": "Meno", + "namePlaceholder": "Zadajte názov modelu...", + "type": "Typ", + "typeState": "štátu", + "typeObject": "Objekt", + "objectLabel": "Označenie objektu", + "objectLabelPlaceholder": "Vyberte typ objektu...", + "classificationType": "Typ klasifikácie", + "classificationTypeTip": "Získajte informácie o typoch klasifikácie", + "classificationTypeDesc": "Podznačky pridávajú k označeniu objektu ďalší text (napr. „Osoba: UPS“). Atribúty sú vyhľadávateľné metadáta uložené samostatne v metadátach objektu.", + "classificationSubLabel": "Podštítky", + "classificationAttribute": "Atribút", + "classes": "Triedy", + "classesTip": "Naučte sa o triedach", + "classesStateDesc": "Definujte rôzne stavy, v ktorých sa môže nachádzať oblasť kamery. Napríklad: „otvorené“ a „zatvorené“ pre garážovú bránu.", + "classesObjectDesc": "Definujte rôzne kategórie, do ktorých sa majú detekované objekty klasifikovať. Napríklad: „doručovateľ/doručovateľka“, „obyvateľ/obyvateľka“, „cudzinec/cudzinec“ pre klasifikáciu osôb.", + "classPlaceholder": "Zadajte názov triedy...", + "errors": { + "nameRequired": "Vyžaduje sa názov modelu", + "nameLength": "Názov modelu musí mať 64 znakov alebo menej", + "nameOnlyNumbers": "Názov modelu nemôže obsahovať iba čísla", + "classRequired": "Vyžaduje sa aspoň 1 kurz", + "classesUnique": "Názvy tried musia byť jedinečné", + "stateRequiresTwoClasses": "Modely štátov vyžadujú aspoň 2 triedy", + "objectLabelRequired": "Vyberte označenie objektu", + "objectTypeRequired": "Vyberte typ klasifikácie", + "noneNotAllowed": "Trieda 'none' nie je povolená" + }, + "states": "Štátov" + }, + "step2": { + "description": "Vyberte kamery a definujte oblasť, ktorú chcete pre každú kameru monitorovať. Model klasifikuje stav týchto oblastí.", + "cameras": "Kamery", + "selectCamera": "Vyberte kameru", + "noCameras": "Kliknite + na pridanie kamier", + "selectCameraPrompt": "Vyberte kameru zo zoznamu a definujte jej oblasť monitorovania" + }, + "step3": { + "selectImagesPrompt": "Vybrať všetky obrázky s: {{className}}", + "selectImagesDescription": "Kliknite na obrázky a vyberte ich. Po dokončení tejto hodiny kliknite na tlačidlo Pokračovať.", + "generating": { + "title": "Generovanie vzorových obrázkov", + "description": "Frigate načítava reprezentatívne obrázky z vašich nahrávok. Môže to chvíľu trvať..." + }, + "training": { + "title": "Tréningový model", + "description": "Váš model sa trénuje na pozadí. Zatvorte toto dialógové okno a váš model sa spustí hneď po dokončení trénovania." + }, + "retryGenerate": "Opakovať generovanie", + "noImages": "Nevygenerovali sa žiadne vzorové obrázky", + "classifying": "Klasifikácia a tréning...", + "trainingStarted": "Školenie začalo úspešne", + "errors": { + "noCameras": "Nie sú nakonfigurované žiadne kamery", + "noObjectLabel": "Nie je vybratý žiadny štítok objektu", + "generateFailed": "Nepodarilo sa vygenerovať príklady: {{error}}", + "generationFailed": "Generovanie zlyhalo. Skúste to znova.", + "classifyFailed": "Nepodarilo sa klasifikovať obrázky: {{error}}" + }, + "generateSuccess": "Vzorové obrázky boli úspešne vygenerované", + "allImagesRequired_one": "Uveďte všetky obrázky. {{count}} obrázok zostáva.", + "allImagesRequired_few": "Uveďte všetky obrázky. {{count}} obrázky zostávajú.", + "allImagesRequired_other": "Uveďte všetky obrázky. {{count}} obrázkov zostávajú.", + "modelCreated": "Model vytvorený úspešne. Použite aktuálne klasifikácie na pridanie obrázkov pre chýbajúce stavy a nasledne dajte trénovať model.", + "missingStatesWarning": { + "title": "Chýbajúce príklady stavov", + "description": "Odporúča sa vybrať príklady pre všetky stavy pre dosiahnutie najlepších výsledkov. Môžeš pokračovať bez zvolenia všetkých stavov, ale model nebude natrénovaný pokiaľ všetky stavy nemajú obrázky. Po pokračovaní použi náhľad Nedávne Klasifikácie na klasifikovanie obrázkov pre chýbajúce stavy, potom natrénuj model." + } + } + }, + "deleteModel": { + "title": "Odstrániť klasifikačný model", + "single": "Ste si istí, že chcete odstrániť {{name}}? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a vzdelávacích údajov. Táto akcia nemôže byť neporušená.", + "desc_one": "Ste si istí, že chcete odstrániť {{count}} model? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a trénovacích údajov. Táto akcia nemôže byť neporušená.", + "desc_few": "Ste si istí, že chcete odstrániť {{count}} modely? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a trénovacích údajov. Táto akcia nemôže byť neporušená.", + "desc_other": "Ste si istí, že chcete odstrániť {{count}} modelov? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a trénovacích údajov. Táto akcia nemôže byť neporušená." + }, + "menu": { + "objects": "Objekty", + "states": "Štátov" + }, + "details": { + "scoreInfo": "Skóre predstavuje priemernú istotu klasifikácie naprieč všetkými detekciami tohoto objektu.", + "none": "Žiadny", + "unknown": "Neznámy" + }, + "tooltip": { + "trainingInProgress": "Model sa aktuálne trénuje", + "noNewImages": "Žiadne nové obrázky na trénovanie. Najskôr klasifikuj nové obrazky do datasetu.", + "noChanges": "Žiadne zmeny v datasete od posledného tréningu.", + "modelNotReady": "Model nie je pripravený na trénovanie" + }, + "edit": { + "title": "Nastavenie Klasifikácie Modelu", + "descriptionState": "Upravte triedy pre tento model stavovej klasifikácie. Zmeny budú vyžadovať pretrénovanie modelu.", + "descriptionObject": "Upravte typ objektu a typ klasifikácie pre tento objektový model klasifikácie.", + "stateClassesInfo": "Poznámka: Zmena tried stavov vyžaduje pretrénovanie modelu s aktualizovanými triedami." + } +} diff --git a/web/public/locales/sk/views/configEditor.json b/web/public/locales/sk/views/configEditor.json new file mode 100644 index 0000000..c10f789 --- /dev/null +++ b/web/public/locales/sk/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor nastavení - Frigate", + "configEditor": "Editor nastavení", + "copyConfig": "Kopírovať konfiguráciu", + "saveAndRestart": "Uložiť a reštartovať", + "saveOnly": "Len uložit", + "confirm": "Opustiť bez uloženia?", + "toast": { + "success": { + "copyToClipboard": "Konfigurácia bola skopírovaná do schránky." + }, + "error": { + "savingError": "Chyba ukladaní konfigurácie" + } + }, + "safeConfigEditor": "Editor konfigurácie (núdzový režim)", + "safeModeDescription": "Frigate je v núdzovom režime kvôli chybe overenia konfigurácie." +} diff --git a/web/public/locales/sk/views/events.json b/web/public/locales/sk/views/events.json new file mode 100644 index 0000000..fe86d41 --- /dev/null +++ b/web/public/locales/sk/views/events.json @@ -0,0 +1,62 @@ +{ + "alerts": "Upozornenia", + "detections": "Detekcie", + "motion": { + "label": "Pohyb", + "only": "Iba pohyb" + }, + "allCameras": "Všetky Kamery", + "empty": { + "alert": "Nie sú žiadne upozornenia na kontrolu", + "detection": "Nie sú žiadne detekcie na kontrolu", + "motion": "Nenašli sa žiadne údaje o pohybe" + }, + "timeline": "Časová os", + "timeline.aria": "Vyberte časovú os", + "events": { + "label": "Udalosti", + "aria": "Vyberte udalosti", + "noFoundForTimePeriod": "Pre toto časové obdobie sa nenašli žiadne udalosti." + }, + "documentTitle": "Recenzia - Frikgate", + "recordings": { + "documentTitle": "Nahrávky - Frigate" + }, + "calendarFilter": { + "last24Hours": "Posledných 24 hodín" + }, + "markAsReviewed": "Označiť ako skontrolované", + "markTheseItemsAsReviewed": "Označiť tieto položky ako skontrolované", + "newReviewItems": { + "label": "Zobraziť nové položky recenzie", + "button": "Nové položky na kontrolu" + }, + "selected_one": "{{count}} vybraných", + "selected_other": "{{count}} vybraných", + "camera": "Kamera", + "detected": "Detekované", + "suspiciousActivity": "Podozrivá aktivita", + "threateningActivity": "Ohrozujúca činnosť", + "detail": { + "noDataFound": "Žiadne podrobné údaje na kontrolu", + "aria": "Prepnúť zobrazenie detailov", + "trackedObject_one": "objekt", + "trackedObject_other": "objekty", + "noObjectDetailData": "Nie sú k dispozícii žiadne podrobné údaje o objekte.", + "label": "Detail", + "settings": "Nastavenia podrobného zobrazenia", + "alwaysExpandActive": { + "title": "Rozbaľte vždy aktívne", + "desc": "Vždy rozbaľte podrobnosti objektu aktívnej položky recenzie, ak sú k dispozícii." + } + }, + "objectTrack": { + "trackedPoint": "Sledovaný bod", + "clickToSeek": "Kliknutím prejdete na tento čas" + }, + "zoomIn": "Priblížiť", + "zoomOut": "Oddialiť", + "normalActivity": "Narmalne", + "needsReview": "Potrebuje preakúmať", + "securityConcern": "Záujem o bezpečnosť" +} diff --git a/web/public/locales/sk/views/explore.json b/web/public/locales/sk/views/explore.json new file mode 100644 index 0000000..0cb2c0b --- /dev/null +++ b/web/public/locales/sk/views/explore.json @@ -0,0 +1,295 @@ +{ + "documentTitle": "Preskúmať - Frigate", + "generativeAI": "Generatívna AI", + "details": { + "timestamp": "Časová pečiatka", + "item": { + "title": "Skontrolujte podrobnosti položky", + "desc": "Skontrolujte podrobnosti položky", + "button": { + "share": "Zdieľajte túto recenziu", + "viewInExplore": "Zobraziť v Preskúmať" + }, + "tips": { + "mismatch_one": "Bol zistený a zahrnutý do tejto položky kontroly nedostupný objekt ({{count}}). Tieto objekty buď neboli kvalifikované ako upozornenie alebo detekcia, alebo už boli vyčistené/odstránené.", + "mismatch_few": "Bolo zistených a zahrnutých do tejto položky kontroly {{count}} nedostupných objektov. Tieto objekty buď neboli kvalifikované ako upozornenie alebo detekcia, alebo už boli vyčistené/odstránené.", + "mismatch_other": "Bolo zistených a zahrnutých do tejto položky kontroly {{count}} nedostupných objektov. Tieto objekty buď neboli kvalifikované ako upozornenie alebo detekcia, alebo už boli vyčistené/odstránené.", + "hasMissingObjects": "Upravte si konfiguráciu, ak chcete, aby Frigate ukladal sledované objekty pre nasledujúce označenia: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "Od poskytovateľa {{provider}} bol vyžiadaný nový popis. V závislosti od rýchlosti vášho poskytovateľa môže jeho obnovenie chvíľu trvať.", + "updatedSublabel": "Podštítok bol úspešne aktualizovaný.", + "updatedLPR": "ŠPZ bola úspešne aktualizovaná.", + "audioTranscription": "Úspešne požiadané o prepis zvuku. V závislosti od rýchlosti vášho servera Frigate môže dokončenie prepisu trvať určitý čas." + }, + "error": { + "regenerate": "Nepodarilo sa zavolať od {{provider}} pre nový popis: {{errorMessage}}", + "updatedSublabelFailed": "Nepodarilo sa aktualizovať podštítok: {{errorMessage}}", + "updatedLPRFailed": "Nepodarilo sa aktualizovať evidenčné číslo vozidla: {{errorMessage}}", + "audioTranscription": "Nepodarilo sa vyžiadať prepis zvuku: {{errorMessage}}" + } + } + }, + "label": "Označenie", + "editSubLabel": { + "title": "Upraviť vedľajší štítok", + "desc": "Zadajte nový podštítok pre tento {{label}}", + "descNoLabel": "Zadajte nový podštítok pre tento sledovaný objekt" + }, + "editLPR": { + "title": "Upraviť ŠPZ", + "desc": "Zadajte novú hodnotu evidenčného čísla vozidla pre toto {{label}}", + "descNoLabel": "Zadajte novú hodnotu evidenčného čísla vozidla pre tento sledovaný objekt" + }, + "snapshotScore": { + "label": "Snímka skóre" + }, + "topScore": { + "label": "Najlepšie skóre", + "info": "Najvyššie skóre je najvyššie mediánové skóre sledovaného objektu, takže sa môže líšiť od skóre zobrazeného na miniatúre výsledkov vyhľadávania." + }, + "score": { + "label": "Skóre" + }, + "recognizedLicensePlate": "Uznaná SPZ", + "estimatedSpeed": "Odhadovaná rýchlosť", + "objects": "Objekty", + "camera": "Kamera", + "zones": "Zóny", + "button": { + "findSimilar": "Nájsť podobné", + "regenerate": { + "title": "Regenerovať", + "label": "Obnoviť popis sledovaného objektu" + } + }, + "description": { + "placeholder": "Popis sledovaného objektu", + "aiTips": "Frigate si od vášho poskytovateľa generatívnej umelej inteligencie nevyžiada popis, kým sa neukončí životný cyklus sledovaného objektu.", + "label": "Popis" + }, + "expandRegenerationMenu": "Rozbaľte ponuku regenerácie", + "regenerateFromSnapshot": "Obnoviť zo snímky", + "regenerateFromThumbnails": "Obnoviť z miniatúr", + "tips": { + "descriptionSaved": "Úspešne uložený popis", + "saveDescriptionFailed": "Nepodarilo sa aktualizovať popis: {{errorMessage}}" + } + }, + "exploreMore": "Preskumať viac {{label}} objektov", + "exploreIsUnavailable": { + "title": "Preskúmanie nie je k dispozícii", + "embeddingsReindexing": { + "context": "Preskúmanie je možné použiť po dokončení opätovného indexovania vložených sledovaných objektov.", + "startingUp": "Spúšťanie…", + "estimatedTime": "Odhadovaný zostávajúci čas:", + "finishingShortly": "Čoskoro končí", + "step": { + "thumbnailsEmbedded": "Vložené miniatúry: ", + "descriptionsEmbedded": "Vložené popisy: ", + "trackedObjectsProcessed": "Spracované sledované objekty: " + } + }, + "downloadingModels": { + "context": "Frigate sťahuje potrebné modely vkladania na podporu funkcie sémantického vyhľadávania. V závislosti od rýchlosti vášho sieťového pripojenia to môže trvať niekoľko minút.", + "setup": { + "visionModel": "Model vízie", + "visionModelFeatureExtractor": "Extraktor prvkov modelu videnia", + "textModel": "Textový model", + "textTokenizer": "Textový tokenizér" + }, + "tips": { + "context": "Po stiahnutí modelov možno budete chcieť znova indexovať vloženia sledovaných objektov.", + "documentation": "Prečítajte si dokumentáciu" + }, + "error": "Vyskytla sa chyba. Skontrolujte protokoly Fregaty." + } + }, + "trackedObjectDetails": "Podrobnosti sledovaného objektu", + "type": { + "details": "detaily", + "snapshot": "snímka", + "video": "video", + "object_lifecycle": "životný cyklus objektu", + "thumbnail": "Náhľad", + "tracking_details": "Pohybové detaili" + }, + "objectLifecycle": { + "title": "Životný cyklus Objektu", + "noImageFound": "Žiadny obrázok pre túto časovú pečiatku.", + "createObjectMask": "Vytvoriť masku objektu", + "adjustAnnotationSettings": "Upravte nastavenia anotácií", + "scrollViewTips": "Posúvaním zobrazíte významné momenty životného cyklu tohto objektu.", + "autoTrackingTips": "Pozície ohraničujúcich rámčekov budú pre kamery s automatickým sledovaním nepresné.", + "count": "{{first}} z {{second}}", + "trackedPoint": "Sledovaný bod", + "lifecycleItemDesc": { + "visible": "Zistený {{label}}", + "entered_zone": "{{label}} vstúpil do {{zones}}", + "active": "{{label}} sa stal aktívnym", + "stationary": "{{label}} sa zastavil", + "attribute": { + "faceOrLicense_plate": "Pre {{label}} bol zistený {{attribute}}", + "other": "{{label}} rozpoznané ako {{attribute}}" + }, + "gone": "{{label}} zostalo", + "heard": "{{label}} počul", + "external": "Zistený {{label}}", + "header": { + "zones": "Zóny", + "ratio": "Pomer", + "area": "Oblasť" + } + }, + "annotationSettings": { + "title": "Nastavenia anotácií", + "showAllZones": { + "title": "Zobraziť všetky zóny", + "desc": "Vždy zobrazovať zóny na rámoch, do ktorých objekty vstúpili." + }, + "offset": { + "label": "Odsadenie anotácie", + "desc": "Tieto údaje pochádzajú z detekčného kanála vašej kamery, ale prekrývajú sa s obrázkami zo záznamového kanála. Je nepravdepodobné, že tieto dva streamy sú dokonale synchronizované. V dôsledku toho sa ohraničujúci rámček a zábery nebudú dokonale zarovnané. Na úpravu tohto posunu je však možné použiť pole annotation_offset.", + "documentation": "Prečítajte si dokumentáciu ", + "millisecondsToOffset": "Milisekundy na posunutie detekcie anotácií. Predvolené: 0", + "tips": "TIP: Predstavte si klip udalosti, v ktorom osoba kráča zľava doprava. Ak je ohraničujúci rámček časovej osi udalosti stále naľavo od osoby, hodnota by sa mala znížiť. Podobne, ak osoba kráča zľava doprava a ohraničujúci rámček je stále pred ňou, hodnota by sa mala zvýšiť.", + "toast": { + "success": "Odsadenie anotácie pre {{camera}} bolo uložené do konfiguračného súboru. Reštartujte Frigate, aby sa zmeny prejavili." + } + } + }, + "carousel": { + "previous": "Predchádzajúca snímka", + "next": "Ďalšia snímka" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "Stiahnut video", + "aria": "Stiahnite si video" + }, + "downloadSnapshot": { + "label": "Stiahnite si snímok", + "aria": "Stiahnite si snímok" + }, + "viewObjectLifecycle": { + "label": "Pozrieť životný cyklus objektu", + "aria": "Životný cyklus objektu" + }, + "findSimilar": { + "label": "Nájsť podobné", + "aria": "Nájdite podobné sledované objekty" + }, + "addTrigger": { + "label": "Pridať spúšťač", + "aria": "Pridať spúšťač pre tento sledovaný objekt" + }, + "audioTranscription": { + "label": "Prepisovať", + "aria": "Požiadajte o prepis zvuku" + }, + "submitToPlus": { + "label": "Odoslať na Frigate+", + "aria": "Odoslať na Frigate Plus" + }, + "viewInHistory": { + "label": "Zobraziť v histórii", + "aria": "Zobraziť v histórii" + }, + "deleteTrackedObject": { + "label": "Odstrániť tento sledovaný objekt" + }, + "showObjectDetails": { + "label": "Zobraziť cestu objektu" + }, + "hideObjectDetails": { + "label": "Skryť cestu objektu" + }, + "viewTrackingDetails": { + "label": "Zobraziť podrobnosti sledovania", + "aria": "Zobraziť podrobnosti o sledovaní" + }, + "downloadCleanSnapshot": { + "label": "Stiahnuť čistý snapshot", + "aria": "Stiahnuť čistý snapshot" + } + }, + "dialog": { + "confirmDelete": { + "title": "Potvrdiť zmazanie", + "desc": "Odstránením tohto sledovaného objektu sa odstráni snímka, všetky uložené vložené prvky a všetky súvisiace položky s podrobnosťami o sledovaní. Zaznamenané zábery tohto sledovaného objektu v zobrazení História NEBUDÚ odstránené.

    Naozaj chcete pokračovať?" + } + }, + "noTrackedObjects": "Žiadne sledované objekty neboli nájdené", + "fetchingTrackedObjectsFailed": "Chyba pri načítaní sledovaných objektov: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} sledovaný objekt ", + "trackedObjectsCount_few": "{{count}} sledované objekty ", + "trackedObjectsCount_other": "{{count}} sledovaných objektov ", + "searchResult": { + "tooltip": "Zhoda s {{type}} na {{confidence}} %", + "deleteTrackedObject": { + "toast": { + "success": "Sledovaný objekt úspešne zmazaný.", + "error": "Sledovaný objekt sa nepodarilo zmazať: {{errorMessage}}" + } + }, + "previousTrackedObject": "Predchádzajúci trackovaný objekt", + "nextTrackedObject": "Ďalší trackovaný objekt" + }, + "aiAnalysis": { + "title": "Analýza AI" + }, + "concerns": { + "label": "Obavy" + }, + "trackingDetails": { + "title": "Podrobnosti sledovania", + "noImageFound": "Pre túto časovú pečiatku sa nenašiel žiadny obrázok.", + "createObjectMask": "Vytvoriť masku objektu", + "adjustAnnotationSettings": "Upravte nastavenia anotácií", + "scrollViewTips": "Kliknite pre zobrazenie významných momentov životného cyklu tohto objektu.", + "autoTrackingTips": "Pozície ohraničujúcich rámčekov budú pre kamery s automatickým sledovaním nepresné.", + "count": "{{first}} z {{second}}", + "trackedPoint": "Sledovaný bod", + "lifecycleItemDesc": { + "visible": "Zistený {{label}}", + "entered_zone": "{{label}} vstúpil do {{zones}}", + "active": "{{label}} sa stal aktívnym", + "stationary": "{{label}} sa zastavil", + "attribute": { + "faceOrLicense_plate": "Pre {{label}} bol zistený {{attribute}}", + "other": "{{label}} rozpoznané ako {{attribute}}" + }, + "gone": "{{label}} zostalo", + "heard": "{{label}} počul", + "external": "Zistený {{label}}", + "header": { + "zones": "Zóny", + "ratio": "Pomer", + "area": "Oblasť", + "score": "Skóre" + } + }, + "annotationSettings": { + "title": "Nastavenia anotácií", + "showAllZones": { + "title": "Zobraziť všetky zóny", + "desc": "Vždy zobrazovať zóny na rámoch, do ktorých objekty vstúpili." + }, + "offset": { + "label": "Odsadenie anotácie", + "desc": "Tieto údaje pochádzajú z detektoru kamery, ale sú prepustené na obrázky z rekordného krmiva. Je nepravdepodobné, že dva prúdy sú perfektne synchronizované. V dôsledku toho, skreslenie box a zábery nebudú dokonale zaradiť. Toto nastavenie môžete použiť na ofsetovanie annotácií dopredu alebo dozadu, aby ste ich lepšie zladili s zaznamenanými zábermi.", + "millisecondsToOffset": "Milisekundy na posunutie detekcie anotácií. Predvolené: 0", + "tips": "TIP: Predstavte si klip udalosti, v ktorom osoba kráča zľava doprava. Ak je ohraničujúci rámček časovej osi udalosti stále naľavo od osoby, hodnota by sa mala znížiť. Podobne, ak osoba kráča zľava doprava a ohraničujúci rámček je stále pred ňou, hodnota by sa mala zvýšiť.", + "toast": { + "success": "Odsadenie anotácie pre {{camera}} bolo uložené do konfiguračného súboru." + } + } + }, + "carousel": { + "previous": "Predchádzajúca snímka", + "next": "Ďalšia snímka" + } + } +} diff --git a/web/public/locales/sk/views/exports.json b/web/public/locales/sk/views/exports.json new file mode 100644 index 0000000..d9df685 --- /dev/null +++ b/web/public/locales/sk/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Exportovať - Frigate", + "search": "Nájsť", + "noExports": "Nenašli sa žiadne exporty", + "deleteExport": "Vymazať export", + "deleteExport.desc": "Ste si isty že chcete vymazať {{exportName}}?", + "editExport": { + "title": "Premenovať Export", + "desc": "Zadajte nové meno pre tento export.", + "saveExport": "Uložiť Export" + }, + "toast": { + "error": { + "renameExportFailed": "Nepodarilo sa premenovať export: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Zdieľať export", + "downloadVideo": "Stiahnite si video", + "editName": "Upraviť meno", + "deleteExport": "Odstrániť export" + } +} diff --git a/web/public/locales/sk/views/faceLibrary.json b/web/public/locales/sk/views/faceLibrary.json new file mode 100644 index 0000000..c10b44a --- /dev/null +++ b/web/public/locales/sk/views/faceLibrary.json @@ -0,0 +1,102 @@ +{ + "description": { + "addFace": "Pridajte novú kolekciu do Face Library nahrať svoj prvý obrázok.", + "invalidName": "Neplatné meno. Mená môžu obsahovať iba písmená, čísla, medzery, apostrofy, podčiarkovníky a spojovníky.", + "placeholder": "Zadajte názov pre túto kolekciu" + }, + "details": { + "person": "Osoba", + "subLabelScore": "Skóre podúrovne", + "scoreInfo": "Skóre podúrovne je vážené skóre všetkých rozpoznaných dôverností tváre, to ale môže byť odlišné od skóre zobrazeného na snímke.", + "face": "Detail tváre", + "faceDesc": "Podrobnosti o sledovanom objekte, ktorý vytvoril túto tvár", + "timestamp": "Časová pečiatka", + "unknown": "Neznámy" + }, + "documentTitle": "Knižnica tvárí", + "uploadFaceImage": { + "title": "Nahrať obrázok tváre", + "desc": "Nahrajte obrázok na skenovanie tvárí a zahrňte ho do {{pageToggle}}" + }, + "collections": "Zbierky", + "createFaceLibrary": { + "title": "Vytvoriť Zbierku", + "desc": "Vytvoriť novú zbierku", + "new": "Vytvoriť novú tvár", + "nextSteps": "Vybudovanie silného základu:
  • Použite kartu Nedávne rozpoznania na výber a trénovanie obrázkov pre každú rozpoznanú osobu.
  • Pre dosiahnutie najlepších výsledkov sa zamerajte na priame obrázky; vyhnite sa trénovaniu obrázkov, ktoré zachytávajú tváre pod uhlom.
  • " + }, + "steps": { + "faceName": "Zadajte Meno tváre", + "uploadFace": "Nahrať obrázok tváre", + "nextSteps": "Ďalšie kroky", + "description": { + "uploadFace": "Nahrajte obrázok {{name}}, ktorý zobrazuje tvár osoby z čelného uhla. Obrázok nemusí byť orezaný len na jej tvár." + } + }, + "train": { + "title": "Nedávne uznania", + "aria": "Vyberte posledné rozpoznania", + "empty": "Neexistujú žiadne predchádzajúce pokusy o rozpoznávanie tváre" + }, + "selectItem": "Vyberte {{item}}", + "selectFace": "Vyberte tvár", + "deleteFaceLibrary": { + "title": "Odstrániť Meno", + "desc": "Ste si istí, že chcete odstrániť kolekciu {{name}}? Tým sa natrvalo odstránia všetky pridružené tváre." + }, + "deleteFaceAttempts": { + "title": "Odstrániť tváre", + "desc_one": "Ste si istí, že chcete odstrániť {{count}} tvár? Túto akciu nemožno vrátiť späť.", + "desc_few": "Ste si istí, že chcete odstrániť {{count}} tvárí? Túto akciu nemožno vrátiť späť.", + "desc_other": "Ste si istí, že chcete odstrániť {{count}} tvárí? Túto akciu nemožno vrátiť späť." + }, + "renameFace": { + "title": "Premonovať tvár", + "desc": "Zadajte nový názov pre {{name}}" + }, + "button": { + "deleteFaceAttempts": "Odstraniť tváre", + "addFace": "Pridať tvár", + "renameFace": "Premenovať tvár", + "deleteFace": "Zmazať tvár", + "uploadImage": "Nahrať obrázok", + "reprocessFace": "Nanovo spracovať tvár" + }, + "imageEntry": { + "validation": { + "selectImage": "Vyberte súbor s obrázkom." + }, + "dropActive": "Presunte obrázok sem…", + "dropInstructions": "Pretiahnite obrázok tu, alebo kliknite na výber", + "maxSize": "Max velkosť: {{size}} MB" + }, + "nofaces": "Žiadne tváre", + "pixels": "{{area}}px", + "readTheDocs": "Prečitajte si návod", + "trainFaceAs": "Trénovať tvár ako:", + "trainFace": "Trénovať tvár", + "toast": { + "success": { + "uploadedImage": "Obrázok bol úspešne nahraný.", + "addFaceLibrary": "{{name}} bol(a) úspešne pridaný(á) do knižnice tvárí!", + "deletedFace_one": "Úspešne odstránená {{count}} tvár.", + "deletedFace_few": "Úspešne odstránené {{count}} tváre.", + "deletedFace_other": "Úspešne odstránených {{count}} tvárí.", + "deletedName_one": "{{count}} tvár bola úspešne odstránená.", + "deletedName_few": "{{count}} tváre boli úspešne odstránené.", + "deletedName_other": "{{count}} tvárí bolo úspešne odstránených.", + "renamedFace": "Úspešne premenovaná tvár na {{name}}", + "trainedFace": "Úspešne natrénovaná tvár.", + "updatedFaceScore": "Úspešne aktualizované skóre tváre." + }, + "error": { + "uploadingImageFailed": "Nepodarilo sa nahrať obrázok: {{errorMessage}}", + "addFaceLibraryFailed": "Nepodarilo sa nastaviť meno tváre: {{errorMessage}}", + "deleteFaceFailed": "Nepodarilo sa odstrániť: {{errorMessage}}", + "deleteNameFailed": "Nepodarilo sa odstrániť meno: {{errorMessage}}", + "renameFaceFailed": "Nepodarilo sa premenovať tvár: {{errorMessage}}", + "trainFailed": "Nepodarilo sa trénovať: {{errorMessage}}", + "updateFaceScoreFailed": "Nepodarilo sa aktualizovať skóre tváre: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/sk/views/live.json b/web/public/locales/sk/views/live.json new file mode 100644 index 0000000..546a603 --- /dev/null +++ b/web/public/locales/sk/views/live.json @@ -0,0 +1,187 @@ +{ + "documentTitle": "Naživo - Frigate", + "documentTitle.withCamera": "{{camera}} - Naživo - Frigate", + "lowBandwidthMode": "Režim nízkej šírky pásma", + "twoWayTalk": { + "enable": "Povoliť obojsmernú komunikáciu", + "disable": "Zakázať obojsmernú komunikáciu" + }, + "cameraAudio": { + "enable": "Povoliť zvuk kamery", + "disable": "Zakázať zvuk kamerám" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kliknite do rámčeka pre vycentrovanie kamery", + "enable": "Povoliť pohyb kliknutím", + "disable": "Zakázať pohyb kliknutím" + }, + "left": { + "label": "Posuňte PTZ kameru doľava" + }, + "up": { + "label": "Posuňte PTZ kameru nahor" + }, + "down": { + "label": "Posuňte PTZ kameru nadol" + }, + "right": { + "label": "Posuňte PTZ kameru doprava" + } + }, + "zoom": { + "in": { + "label": "Priblíženie PTZ kamery" + }, + "out": { + "label": "Oddialenie PTZ kamery" + } + }, + "frame": { + "center": { + "label": "Kliknite do rámčeka pre vycentrovanie PTZ kamery" + } + }, + "presets": "Predvoľby PTZ kamery", + "focus": { + "in": { + "label": "Zaostrenie PTZ kamery v" + }, + "out": { + "label": "Výstup zaostrenia PTZ kamery" + } + } + }, + "camera": { + "enable": "Povoliť kameru", + "disable": "Zakázať kameru" + }, + "muteCameras": { + "enable": "Stlmiť všetky kamery", + "disable": "Zapnúť zvuk všetkých kamier" + }, + "detect": { + "enable": "Povoliť detekciu", + "disable": "Zakázať detekciu" + }, + "recording": { + "enable": "Povoliť nahrávanie", + "disable": "Zakázať nahrávanie" + }, + "snapshots": { + "enable": "Povoliť vytváranie snímok", + "disable": "Zakázať snímky" + }, + "audioDetect": { + "enable": "Povoliť detekciu zvuku", + "disable": "Zakázať detekciu zvuku" + }, + "autotracking": { + "enable": "Povoliť automatické sledovanie", + "disable": "Zakázať automatické sledovanie" + }, + "transcription": { + "enable": "Povoliť živý prepis zvuku", + "disable": "Zakázať živý prepis zvuku" + }, + "streamStats": { + "enable": "Zobraziť štatistiky streamu", + "disable": "Skryť štatistiky streamu" + }, + "manualRecording": { + "title": "Na požiadanie", + "tips": "Stiahnite si okamžité snímky alebo začnite manuálnu akciu založenú na nastavení nahrávania tejto kamery.", + "playInBackground": { + "label": "Hrať na pozadí", + "desc": "Povoľte túto možnosť, ak chcete pokračovať v streamovaní, aj keď je prehrávač skrytý." + }, + "showStats": { + "label": "Zobraziť štatistiky", + "desc": "Povoľte túto možnosť, ak chcete zobraziť štatistiky streamu ako prekrytie na obraze z kamery." + }, + "debugView": "Zobrazenie ladenia", + "start": "Spustiť nahrávanie na požiadanie", + "started": "Spustené manuálne nahrávanie na požiadanie.", + "failedToStart": "Nepodarilo sa spustiť manuálne nahrávanie na požiadanie.", + "recordDisabledTips": "Keďže nahrávanie je v konfigurácii tejto kamery zakázané alebo obmedzené, uloží sa iba snímka.", + "end": "Ukončiť nahrávanie na požiadanie", + "ended": "Manuálne nahrávanie na požiadanie bolo ukončené.", + "failedToEnd": "Nepodarilo sa ukončiť manuálne nahrávanie na požiadanie." + }, + "streamingSettings": "Nastavenia streamovania", + "notifications": "Notifikacie", + "audio": "Zvuk", + "suspend": { + "forTime": "Pozastaviť na: " + }, + "stream": { + "title": "Stream", + "audio": { + "tips": { + "title": "Zvuk musí byť vyvedený z vašej kamery a nakonfigurovaný v go2rtc pre tento stream." + }, + "available": "Pre tento stream je k dispozícii zvuk", + "unavailable": "Zvuk nie je pre tento stream k dispozícii" + }, + "twoWayTalk": { + "tips": "Vaše zariadenie musí túto funkciu podporovať a WebRTC musí byť nakonfigurované na obojsmernú komunikáciu.", + "available": "Pre tento stream je k dispozícii obojsmerná komunikácia", + "unavailable": "Obojsmerná komunikácia nie je pre tento stream k dispozícii" + }, + "lowBandwidth": { + "tips": "Živý náhľad je v režime nízkej šírky pásma z dôvodu chýb načítavania do vyrovnávacej pamäte alebo streamu.", + "resetStream": "Obnoviť stream" + }, + "playInBackground": { + "label": "Hrať na pozadí", + "tips": "Povoľte túto možnosť, ak chcete pokračovať v streamovaní, aj keď je prehrávač skrytý." + }, + "debug": { + "picker": "Výber streamu nie je k dispozícii v režime ladenia. Zobrazenie ladenia vždy používa stream, ktorému je priradená rola detekcie." + } + }, + "cameraSettings": { + "title": "Nastavenia {{camera}}", + "cameraEnabled": "Kamera povolená", + "objectDetection": "Detekcia objektov", + "recording": "Nahrávanie", + "snapshots": "Snímky", + "audioDetection": "Detekcia zvuku", + "transcription": "Zvukový prepis", + "autotracking": "Automatické sledovanie" + }, + "history": { + "label": "Zobraziť historické zábery" + }, + "effectiveRetainMode": { + "modes": { + "all": "Všetko", + "motion": "Pohyb", + "active_objects": "Aktívne objekty" + }, + "notAllTips": "Vaša konfigurácia uchovávania nahrávok {{source}} je nastavená na režim : {{effectiveRetainMode}}, takže táto nahrávka na požiadanie uchová iba segmenty s nastavením {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Upraviť rozloženie", + "group": { + "label": "Upraviť skupinu kamier" + }, + "exitEdit": "Ukončiť úpravy" + }, + "noCameras": { + "title": "Nie sú konfigurované žiadne kamery", + "description": "Začnite tým, že pripojíte kameru do Frigate.", + "buttonText": "Pridať kameru", + "restricted": { + "title": "Žiadne kamery k dispozícii", + "description": "Nemáte povolenie na zobrazenie akýchkoľvek kamier v tejto skupine." + } + }, + "snapshot": { + "takeSnapshot": "Stiahnite si okamžité snímky", + "noVideoSource": "Žiadny zdroj videa k dispozícii pre snapshot.", + "captureFailed": "Nepodarilo sa zachytiť snímku.", + "downloadStarted": "Sťahovanie snímky sa začalo." + } +} diff --git a/web/public/locales/sk/views/motionSearch.json b/web/public/locales/sk/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sk/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sk/views/recording.json b/web/public/locales/sk/views/recording.json new file mode 100644 index 0000000..d14b865 --- /dev/null +++ b/web/public/locales/sk/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filter", + "export": "Exportovať", + "calendar": "Kalendár", + "filters": "Filtre", + "toast": { + "error": { + "noValidTimeSelected": "Nie je vybratý žiadny platný časový rozsah", + "endTimeMustAfterStartTime": "Čas konca musí byť po čase začiatku" + } + } +} diff --git a/web/public/locales/sk/views/replay.json b/web/public/locales/sk/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sk/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sk/views/search.json b/web/public/locales/sk/views/search.json new file mode 100644 index 0000000..a368ca1 --- /dev/null +++ b/web/public/locales/sk/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Nájsť", + "savedSearches": "Uložené vyhľadávania", + "searchFor": "Hľadať pre {{inputValue}}", + "button": { + "clear": "Prehľadné vyhľadávanie", + "save": "Uložiť vyhladávanie", + "delete": "Vymazať uložené vyhladávania", + "filterInformation": "Filtrovanie informacii", + "filterActive": "Aktívne filtre" + }, + "trackedObjectId": "ID sledovaného objektu", + "filter": { + "label": { + "cameras": "Kamery", + "labels": "Štítky", + "zones": "Zóny", + "sub_labels": "Podštítky", + "search_type": "Typ vyhľadávania", + "time_range": "Časový rozsah", + "before": "Predtým", + "after": "Po", + "min_score": "Min. Skóre", + "max_score": "Maximálne skóre", + "min_speed": "Min rýchlosť", + "max_speed": "Max rýchlosť", + "recognized_license_plate": "Uznaná poznávacia značka", + "has_clip": "Má Klip", + "has_snapshot": "Má Snímok" + }, + "searchType": { + "thumbnail": "Náhľad", + "description": "Popis" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Dátum 'pred' musí byť neskorší ako dátum 'po'.", + "afterDatebeEarlierBefore": "Dátum „po“ musí byť skorší ako dátum „pred“.", + "minScoreMustBeLessOrEqualMaxScore": "Hodnota „min_score“ musí byť menšia alebo rovná hodnote „max_score“.", + "maxScoreMustBeGreaterOrEqualMinScore": "Hodnota „max_score“ musí byť väčšia alebo rovná hodnote „min_score“.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Hodnota „min_speed“ musí byť menšia alebo rovná hodnote „max_speed“.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Hodnota „max_speed“ musí byť väčšia alebo rovná hodnote „min_speed“." + } + }, + "tips": { + "title": "Ako používať textové filtre", + "desc": { + "text": "Filtre vám pomôžu zúžiť výsledky vyhľadávania. Tu je postup, ako ich použiť vo vstupnom poli:", + "step1": "Zadajte názov kľúča filtra, za ktorým nasleduje dvojbodka (napr. „kamery:“).", + "step2": "Vyberte hodnotu z návrhov alebo zadajte vlastnú.", + "step3": "Použite viacero filtrov tak, že ich pridáte jeden po druhom s medzerou medzi nimi.", + "step4": "Filtre dátumu (pred: a po:) používajú formát {{DateFormat}}.", + "step5": "Filter časového rozsahu používa formát {{exampleTime}}.", + "step6": "Filtre odstránite kliknutím na „x“ vedľa nich.", + "exampleLabel": "Príklad:" + } + }, + "header": { + "currentFilterType": "Hodnoty filtra", + "noFilters": "Filtre", + "activeFilters": "Aktívne filtre" + } + }, + "similaritySearch": { + "title": "Vyhľadávanie podobností", + "active": "Vyhľadávanie podobnosti je aktívne", + "clear": "Jasné vyhľadávanie podobnosti" + }, + "placeholder": { + "search": "Hľadať…" + } +} diff --git a/web/public/locales/sk/views/settings.json b/web/public/locales/sk/views/settings.json new file mode 100644 index 0000000..dbb8faf --- /dev/null +++ b/web/public/locales/sk/views/settings.json @@ -0,0 +1,1211 @@ +{ + "documentTitle": { + "default": "Nastavenia - Frigate", + "authentication": "Nastavenie autentifikácie- Frigate", + "camera": "Nastavenia Kamier– Frigate", + "enrichments": "Nastavenia obohatenia – Frigate", + "masksAndZones": "Editor masky a zón - Frigate", + "motionTuner": "Ladič detekcie pohybu - Frigate", + "object": "Ladenie - Frigate", + "general": "UI nastavenia – Frigate", + "frigatePlus": "Nastavenia Frigate+ – Frigate", + "notifications": "Nastavenia upozornení – Frigate", + "cameraManagement": "Manažment kamier - Frigate", + "cameraReview": "Nastavenie kamier - Frigate" + }, + "menu": { + "ui": "Uživaťelské rozohranie", + "enrichments": "Obohatenia", + "cameras": "Nastavenia kamier", + "masksAndZones": "Masky / Zóny", + "motionTuner": "Ladenie detekcie pohybu", + "debug": "Ladenie", + "users": "Uživatelia", + "notifications": "Notifikacie", + "frigateplus": "Frigate+", + "triggers": "Spúšťače", + "roles": "Roly", + "cameraManagement": "Manažment", + "cameraReview": "Recenzia", + "profiles": "Profily" + }, + "dialog": { + "unsavedChanges": { + "title": "Máte neuložené zmeny.", + "desc": "Chcete uložiť zmeny pred pokračovaním?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Žiadna Kamera" + }, + "general": { + "title": "UI nastavenia", + "liveDashboard": { + "title": "Živý Dashboard", + "automaticLiveView": { + "label": "Automatický živý náhľad", + "desc": "Pri detekcii aktivity sa automaticky prepne na živý náhľad kamery. Vypnutie tejto možnosti spôsobí, že sa statické snímky z kamery na ovládacom paneli Live aktualizujú iba raz za minútu." + }, + "playAlertVideos": { + "label": "Prehrať videá s upozornením", + "desc": "Predvolene sa nedávne upozornenia na paneli Živé vysielanie prehrávajú ako krátke cyklické videá. Túto možnosť vypnite, ak chcete zobrazovať iba statický obrázok nedávnych upozornení na tomto zariadení/prehliadači." + }, + "displayCameraNames": { + "label": "Vždy Zobraziť názvy kamier", + "desc": "Vždy zobrazujte názvy kamier v čipe na ovládacom paneli živého náhľadu z viacerých kamier." + }, + "liveFallbackTimeout": { + "label": "Časový limit", + "desc": "Keď je kamerový vysoko kvalitný živý stream nedostupný, prejdite späť na režim nízkej kvality. Predvolené: 3." + } + }, + "storedLayouts": { + "title": "Uložené rozloženia", + "desc": "Rozloženie kamier v skupine kamier je možné presúvať/zmeniť jeho veľkosť. Pozície sú uložené v lokálnom úložisku vášho prehliadača.", + "clearAll": "Vymazať všetky rozloženia" + }, + "cameraGroupStreaming": { + "title": "Nastavenia streamovania skupiny kamier", + "desc": "Nastavenia streamovania pre každú skupinu kamier sú uložené v lokálnom úložisku vášho prehliadača.", + "clearAll": "Vymazať všetky nastavenia streamovania" + }, + "recordingsViewer": { + "title": "Prehliadač nahrávok", + "defaultPlaybackRate": { + "label": "Predvolená rýchlosť prehrávania", + "desc": "Predvolená rýchlosť prehrávania nahrávok." + } + }, + "calendar": { + "title": "Kalendár", + "firstWeekday": { + "label": "Prvý pracovný deň", + "desc": "Deň, kedy začínajú týždne v kalendári kontroly.", + "sunday": "Nedeľa", + "monday": "Pondelok" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Uložené rozloženie pre {{cameraName}} bolo vymazané", + "clearStreamingSettings": "Nastavenia streamovania pre všetky skupiny kamier boli vymazané." + }, + "error": { + "clearStoredLayoutFailed": "Nepodarilo sa vymazať uložené rozloženie: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nepodarilo sa vymazať nastavenia streamovania: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Nastavenia obohatení", + "unsavedChanges": "Zmeny nastavení neuložených obohatení", + "birdClassification": { + "title": "Klasifikácia vtákov", + "desc": "Klasifikácia vtákov identifikuje známe vtáky pomocou kvantizovaného modelu Tensorflow. Keď je známy vták rozpoznaný, jeho bežný názov sa pridá ako podoznačenie (sub_label). Tieto informácie sú zahrnuté v používateľskom rozhraní, filtroch, ako aj v oznámeniach." + }, + "semanticSearch": { + "title": "Sémantické vyhľadávanie", + "desc": "Sémantické vyhľadávanie vo Frigate vám umožňuje nájsť sledované objekty v rámci vašich recenzovaných položiek pomocou samotného obrázka, textového popisu definovaného používateľom alebo automaticky vygenerovaného popisu.", + "reindexNow": { + "label": "Preindexovať teraz", + "desc": "Reindexovanie obnoví vložené súbory pre všetky sledované objekty. Tento proces beží na pozadí a môže maximálne zaťažiť váš procesor a trvať pomerne dlho v závislosti od počtu sledovaných objektov, ktoré máte.", + "confirmTitle": "Potvrďte opätovné indexovanie", + "confirmDesc": "Naozaj chcete preindexovať všetky sledované vložené objekty? Tento proces bude bežať na pozadí, ale môže maximálne zaťažiť váš procesor a trvať pomerne dlho. Priebeh si môžete pozrieť na stránke Preskúmať.", + "confirmButton": "Preindexovať", + "success": "Reindexovanie sa úspešne spustilo.", + "alreadyInProgress": "Reindexovanie už prebieha.", + "error": "Nepodarilo sa spustiť reindexáciu: {{errorMessage}}" + }, + "modelSize": { + "label": "Veľkosť modelu", + "desc": "Veľkosť modelu použitého pre vkladanie sémantického vyhľadávania.", + "small": { + "title": "malý", + "desc": "Použitie funkcie small využíva kvantizovanú verziu modelu, ktorá spotrebuje menej pamäte RAM a beží rýchlejšie na CPU s veľmi zanedbateľným rozdielom v kvalite vkladania." + }, + "large": { + "title": "veľký", + "desc": "Použitie parametra large využíva celý model Jina a v prípade potreby sa automaticky spustí na GPU." + } + } + }, + "faceRecognition": { + "title": "Rozpoznávanie tváre", + "desc": "Rozpoznávanie tváre umožňuje priradiť ľuďom mená a po rozpoznaní ich tváre Frigate priradí meno osoby ako podštítok. Tieto informácie sú zahrnuté v používateľskom rozhraní, filtroch, ako aj v upozorneniach.", + "modelSize": { + "label": "Veľkosť modelu", + "desc": "Veľkosť modelu použitého na rozpoznávanie tváre.", + "small": { + "title": "malý", + "desc": "Použitie funkcie small využíva model vkladania tvárí FaceNet, ktorý efektívne beží na väčšine procesorov." + }, + "large": { + "title": "veľký", + "desc": "Použitie funkcie large využíva model vkladania tvárí ArcFace a v prípade potreby sa automaticky spustí na grafickom procesore." + } + } + }, + "licensePlateRecognition": { + "title": "Rozpoznávanie Evidenčných Čísel Vozidiel", + "desc": "Frigate dokáže rozpoznávať evidenčné čísla vozidiel a automaticky pridávať detekované znaky do poľa recognized_license_plate alebo známy názov ako podradený štítok k objektom typu car. Bežným prípadom použitia môže byť čítanie evidenčných čísel áut vchádzajúcich na príjazdovú cestu alebo áut prechádzajúcich po ulici." + }, + "restart_required": "Vyžaduje sa reštart (zmenené nastavenia obohatenia)", + "toast": { + "success": "Nastavenia obohatenia boli uložené. Reštartujte Frigate, aby sa zmeny prejavili.", + "error": "Nepodarilo sa uložiť zmeny konfigurácie: {{errorMessage}}" + } + }, + "camera": { + "title": "Nastavenie kamier", + "streams": { + "title": "Streamy", + "desc": "Dočasne deaktivujte kameru, kým sa Frigate nereštartuje. Deaktivácia kamery úplne zastaví spracovanie streamov z tejto kamery aplikáciou Frigate. Detekcia, nahrávanie a ladenie nebudú k dispozícii.
    Poznámka: Toto nezakáže restreamy go2rtc." + }, + "review": { + "title": "Recenzia", + "desc": "Dočasne povoliť/zakázať upozornenia a detekcie pre túto kameru, kým sa Frigate nereštartuje. Po zakázaní sa nebudú generovať žiadne nové položky kontroly. ", + "alerts": "Upozornenia ", + "detections": "Detekcie " + }, + "object_descriptions": { + "title": "Generatívne popisy objektov umelej inteligencie", + "desc": "Dočasne povoliť/zakázať generatívne popisy objektov AI pre túto kameru. Ak je táto funkcia zakázaná, pre sledované objekty na tejto kamere sa nebudú vyžadovať popisy generované AI." + }, + "review_descriptions": { + "title": "Popisy generatívnej umelej inteligencie", + "desc": "Dočasne povoliť/zakázať generatívne popisy kontroly pomocou umelej inteligencie pre túto kameru. Ak je táto funkcia zakázaná, popisy generované umelou inteligenciou sa nebudú vyžadovať pre položky kontroly v tejto kamere." + }, + "reviewClassification": { + "title": "Preskúmať klasifikáciu", + "desc": "Frigate kategorizuje položky recenzií ako Upozornenia a Detekcie. Predvolene sa všetky objekty typu osoba a auto považujú za Upozornenia. Kategorizáciu položiek recenzií môžete spresniť konfiguráciou požadovaných zón pre ne.", + "noDefinedZones": "Pre túto kameru nie sú definované žiadne zóny.", + "objectAlertsTips": "Všetky objekty {{alertsLabels}} na {{cameraName}} sa zobrazia ako Upozornenia.", + "zoneObjectAlertsTips": "Všetky objekty {{alertsLabels}} detekované v {{zone}} na {{cameraName}} budú zobrazené ako Upozornenia.", + "objectDetectionsTips": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{cameraName}}, sa zobrazia ako detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú.", + "zoneObjectDetectionsTips": { + "text": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{zone}} na kamere {{cameraName}}, budú zobrazené ako detekcie.", + "notSelectDetections": "Všetky objekty typu {{detectionsLabels}} detekované v zóne {{zone}} na kamere {{cameraName}}, ktoré nie sú zaradené do kategórie Upozornenia, sa zobrazia ako Detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú.", + "regardlessOfZoneObjectDetectionsTips": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{cameraName}}, sa zobrazia ako detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú." + }, + "unsavedChanges": "Neuložené nastavenia klasifikácie recenzií pre {{camera}}", + "selectAlertsZones": "Vyberte podobné sledované objekty", + "selectDetectionsZones": "Vyberte zóny pre detekcie", + "limitDetections": "Obmedziť detekciu na konkrétne zóny", + "toast": { + "success": "Konfigurácia klasifikácie bola uložená. Reštartujte Frigate, aby sa zmeny prejavili." + } + }, + "addCamera": "Pridať novu kameru", + "editCamera": "Upraviť kameru:", + "selectCamera": "Vyberte kameru", + "backToSettings": "Späť na nastavenia kamery", + "cameraConfig": { + "add": "Pridať kameru", + "edit": "Upraviť kameru", + "description": "Konfigurovať nastavenia kamery, vrátane vstupov streamu a rolí.", + "name": "Názov kamery", + "nameRequired": "Názov kamery je povinný", + "nameLength": "Názov kamery musí mať menej ako 24 znakov.", + "namePlaceholder": "napr. predné dvere", + "enabled": "Povoliť", + "ffmpeg": { + "inputs": "Vstupné streamy", + "path": "Cesta streamu", + "pathRequired": "Cesta k streamu je povinná", + "pathPlaceholder": "rtsp://...", + "roles": "Roly", + "rolesRequired": "Je vyžadovaná aspoň jedna rola", + "rolesUnique": "Každá rola (audio, detekcia, záznam) môže byť priradená iba k jednému streamu", + "addInput": "Pridať vstupný stream", + "removeInput": "Odobrať vstupný stream", + "inputsRequired": "Je vyžadovaný aspoň jeden vstupný stream" + }, + "toast": { + "success": "Kamera {{cameraName}} bola úspešne uložená" + } + } + }, + "masksAndZones": { + "filter": { + "all": "Všetky Masky a Zóny" + }, + "restart_required": "Vyžadovaný reštart (masky/zóny boli zmenené)", + "toast": { + "success": { + "copyCoordinates": "Súradnice pre {{polyName}} skopírované do schránky." + }, + "error": { + "copyCoordinatesFailed": "Nemohol kopírovať súradnice na klipboard." + } + }, + "form": { + "polygonDrawing": { + "error": { + "mustBeFinished": "Kreslenie polygónu musí byť pred uložením dokončené." + }, + "removeLastPoint": "Odobrať posledný bod", + "reset": { + "label": "Vymazať všetky body" + }, + "snapPoints": { + "true": "Prichytávať body", + "false": "Neprichytávať body" + }, + "delete": { + "title": "Potvrdiť Zmazanie", + "desc": "Naozaj chcete zmazať {{type}}{{name}}?", + "success": "{{name}} bolo zmazané." + } + }, + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Názov Zóny musia mať minimálne 2 znaky.", + "mustNotBeSameWithCamera": "Názov Zóny nesmie byť rovnaký ako názov kamery.", + "alreadyExists": "Zóna s rovnakým názvom pri tejto kamere už existuje.", + "mustNotContainPeriod": "Názov zóny nesmie obsahovať bodky.", + "hasIllegalCharacter": "Názov zóny obsahuje zakázané znaky.", + "mustHaveAtLeastOneLetter": "Názov zóny musí mať aspoň jedno písmeno." + } + }, + "distance": { + "error": { + "text": "Vzdialenosť musí byť väčšia alebo rovná 0.1.", + "mustBeFilled": "Na použitie odhadu rýchlosti musia byť vyplnené všetky polia pre vzdialenosť." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Zotrvačnosť musí byť väčšia ako 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Doba zotrvania musí byť väčšia alebo rovná nule." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Prahová hodnota rýchlosti musí byť väčšia alebo rovná 0,1." + } + } + }, + "zones": { + "label": "Zóny", + "documentTitle": "Upraviť Zónu - Frigate", + "desc": { + "title": "Zóny umožňujú definovať konkrétnu oblasť v zábere, vďaka čomu je možné určiť, či sa objekt nachádza v danej oblasti alebo nie.", + "documentation": "Dokumentácia" + }, + "clickDrawPolygon": "Kliknite pre kreslenie polygónu na obrázku.", + "name": { + "title": "Meno", + "inputPlaceHolder": "Zadajte meno…", + "tips": "Názov musí mať aspoň 2 znaky, musí mať aspoň jedno písmeno a nesmie byť názvom kamery alebo inej zóny v tejto kamere." + }, + "inertia": { + "title": "Zotrvačnosť", + "desc": "Určuje, po koľkých snímkach strávených v zóne je objekt považovaný za prítomný v tejto zóne.Predvolená hodnota: 3" + }, + "loiteringTime": { + "title": "Doba zotrvania", + "desc": "Nastavuje minimálnu dobu v sekundách, počas ktorej musí byť objekt v zóne, aby došlo k aktivácii.Predvolená hodnota: 0" + }, + "objects": { + "title": "Objekty", + "desc": "Zoznam objektov, na ktoré sa táto zóna vzťahuje." + }, + "allObjects": "Všetky Objekty", + "speedEstimation": { + "title": "Odhad rýchlosti", + "desc": "Povoliť odhad rýchlosti pre objekty v tejto zóne. Zóna musí mať presne 4 body.", + "lineADistance": "Vzdialenosť linky A ({{unit}})", + "lineBDistance": "Vzdialenosť linky B ({{unit}})", + "lineCDistance": "Vzdialenosť linky C ({{unit}})", + "lineDDistance": "Vzdialenosť linky D ({{unit}})" + }, + "speedThreshold": { + "title": "Prah rýchlosti ({{unit}})", + "desc": "Určuje minimálnu rýchlosť, pri ktorej sú objekty v tejto zóne zohľadnené.", + "toast": { + "error": { + "pointLengthError": "Odhad rýchlosti bol pre túto zónu deaktivovaný. Zóny s odhadom rýchlosti musia mať presne 4 body.", + "loiteringTimeError": "Pokiaľ má zóna nastavenú dobu zotrvania väčšiu ako 0, neodporúča sa používať odhad rýchlosti." + } + } + }, + "toast": { + "success": "Zóna {{zoneName}} bola uložená." + }, + "add": "Pridať zónu", + "edit": "Upraviť zónu", + "point_one": "{{count}}bod", + "point_few": "{{count}}body", + "point_other": "{{count}}bodov" + }, + "motionMasks": { + "label": "Maska Detekcia pohybu", + "documentTitle": "Editovať Masku Detekcia pohybu - Frigate", + "desc": { + "title": "Masky detekcie pohybu slúžia na zabránenie nežiaducim typom pohybu v spustení detekcie. Príliš rozsiahle maskovanie však môže sťažiť sledovanie objektov.", + "documentation": "Dokumentácia" + }, + "add": "Nová Maska Detekcia pohybu", + "edit": "Upraviť Masku Detekcia pohybu", + "context": { + "title": "Masky detekcie pohybu slúžia na zabránenie tomu, aby nežiaduce typy pohybu spúšťali detekciu (napríklad vetvy stromov alebo časové značky kamery). Masky detekcie pohybu by sa mali používať veľmi striedmo – príliš rozsiahle maskovanie môže sťažiť sledovanie objektov." + }, + "point_one": "{{count}} bod", + "point_few": "{{count}} body", + "point_other": "{{count}} bodov", + "clickDrawPolygon": "Kliknutím nakreslíte polygón do obrázku.", + "polygonAreaTooLarge": { + "title": "Maska detekcie pohybu pokrýva {{polygonArea}}% záberu kamery. Príliš veľké masky detekcie pohybu nie sú odporúčané.", + "tips": "Masky detekcie pohybu nebránia detekcii objektov. Namiesto toho by ste mali použiť požadovanú zónu." + }, + "toast": { + "success": { + "title": "{{polygonName}} bol uložený.", + "noName": "Maska detekcie pohybu bola uložená." + } + } + }, + "objectMasks": { + "label": "Masky Objektu", + "documentTitle": "Upraviť Masku Objektu - Frigate", + "desc": { + "title": "Masky filtrovania objektov slúžia na odfiltrovanie falošných detekcií daného typu objektu na základe jeho umiestnenia.", + "documentation": "Dokumentácia" + }, + "add": "Pridať Masku Objektu", + "edit": "Upraviť Masku Objektu", + "context": "Masky filtrovania objektov slúžia na odfiltrovanie falošných poplachov konkrétneho typu objektu na základe jeho umiestnenia.", + "point_one": "{{count}}bod", + "point_few": "{{count}}body", + "point_other": "{{count}} bodov", + "clickDrawPolygon": "Kliknutím nakreslite polygón do obrázku.", + "objects": { + "title": "Objekty", + "desc": "Typ objektu, na ktorý sa táto maska objektu vzťahuje.", + "allObjectTypes": "Všetky typy objektov" + }, + "toast": { + "success": { + "title": "{{polygonName}} bol uložený.", + "noName": "Maska Objektu bola uložená." + } + } + }, + "motionMaskLabel": "Maska Detekcia pohybu {{number}}", + "objectMaskLabel": "Maska Objektu {{number}} {{label}}" + }, + "motionDetectionTuner": { + "title": "Ladenie detekcie pohybu", + "unsavedChanges": "Neuložené zmeny ladenia detekcie pohybu {{camera}}", + "desc": { + "title": "Frigate používa detekciu pohybu ako prvú kontrolu na overenie, či sa v snímke deje niečo, čo stojí za ďalšiu analýzu pomocou detekcie objektov.", + "documentation": "Prečítajte si príručku Ladenie detekcie pohybu" + }, + "Threshold": { + "title": "Prah", + "desc": "Prahová hodnota určuje, aká veľká zmena jasu pixelu je nutná, aby bol považovaný za pohyb. Predvolené: 30" + }, + "contourArea": { + "title": "Obrysová Oblasť", + "desc": "Hodnota plochy obrysu sa používa na rozhodnutie, ktoré skupiny zmenených pixelov sa kvalifikujú ako pohyb. Predvolené: 10" + }, + "improveContrast": { + "title": "Zlepšiť Kontrast", + "desc": "Zlepšiť kontrast pre tmavé scény Predvolené: ON" + }, + "toast": { + "success": "Nastavenie detekcie pohybu bolo uložené." + } + }, + "debug": { + "title": "Ladenie", + "detectorDesc": "Frigate používa vaše detektory {{detectors}} na detekciu objektov v streame vašich kamier.", + "desc": "Ladiace zobrazenie ukazuje sledované objekty a ich štatistiky v reálnom čase. Zoznam objektov zobrazuje časovo oneskorený prehľad detekovaných objektov.", + "openCameraWebUI": "Otvoriť webové rozhranie {{camera}}", + "debugging": "Ladenie", + "objectList": "Zoznam objektov", + "noObjects": "Žiadne objekty", + "audio": { + "title": "Zvuk", + "noAudioDetections": "Žiadne detekcia zvuku", + "score": "skóre", + "currentRMS": "Aktuálne RMS", + "currentdbFS": "Aktuálne dbFS" + }, + "boundingBoxes": { + "title": "Ohraničujúce rámčeky", + "desc": "Zobraziť ohraničujúce rámčeky okolo sledovaných objektov", + "colors": { + "label": "Farby Ohraničujúcich Rámčekov Objektov", + "info": "
  • Pri spustení bude každému objektovému štítku priradená iná farba.
  • Tenká tmavo modrá čiara označuje, že objekt nie je v danom okamihu detekovaný.
  • Tenká šedá čiara znamená, že objekt je detekovaný ako nehybný.
  • Silná čiara je označovaná aktivované).
  • " + } + }, + "timestamp": { + "title": "Časová pečiatka", + "desc": "Prekryť obrázok časovou pečiatkou" + }, + "zones": { + "title": "Zóny", + "desc": "Zobraziť obrys všetkých definovaných zón" + }, + "mask": { + "title": "Masky detekcie pohybu", + "desc": "Zobraziť polygóny masiek detekcie pohybu" + }, + "motion": { + "title": "Rámčeky detekcie pohybu", + "desc": "Zobraziť rámčeky okolo oblastí, kde bol detekovaný pohyb", + "tips": "

    Boxy pohybu


    Červené boxy budú prekryté na miestach snímky, kde je práve detekovaný pohyb.

    " + }, + "regions": { + "title": "Regióny", + "desc": "Zobraziť rámček oblasti záujmu odoslaný do detektora objektov", + "tips": "

    Oblasti regiónov


    Jasnozelené políčka budú prekrývať oblasti záujmu v zábere, ktoré sa odosielajú do detektora objektov.

    " + }, + "paths": { + "title": "Cesty", + "desc": "Zobraziť významné body dráhy sledovaného objektu", + "tips": "

    Cesty


    Čiary a kruhy označujú významné body, ktorými sa sledovaný objekt počas svojho životného cyklu pohyboval.

    " + }, + "objectShapeFilterDrawing": { + "title": "Výkres filtra tvaru objektu", + "desc": "Nakreslite na obrázok obdĺžnik, aby ste zobrazili podrobnosti o ploche a pomere", + "tips": "Povolením tejto možnosti nakreslíte na obraze kamery obdĺžnik, ktorý zobrazuje jeho plochu a pomer strán. Tieto hodnoty sa potom dajú použiť na nastavenie parametrov filtra tvaru objektu vo vašej konfigurácii.", + "score": "Skóre", + "ratio": "Pomer", + "area": "Oblasť" + } + }, + "cameraWizard": { + "title": "Pridať kameru", + "description": "Postupujte podľa pokynov nižšie a pridajte novú kameru na inštaláciu Frigate.", + "steps": { + "nameAndConnection": "Meno a pripojenie", + "streamConfiguration": "Konfigurácia prúdu", + "validationAndTesting": "Platnosť a testovanie", + "probeOrSnapshot": "Probe alebo Snapshot" + }, + "save": { + "success": "Úspešne zachránil novú kameru {{cameraName}}.", + "failure": "Úspora chýb {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Rozlíšenie", + "video": "Video", + "audio": "Zvuk", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Uveďte platnú adresu streamu", + "testFailed": "Test Stream zlyhal: {{error}}" + }, + "step1": { + "description": "Zadajte detaily kamery a vyskúšajte pripojenie.", + "cameraName": "Názov kamery", + "cameraNamePlaceholder": "e.g., front_door alebo Back Yard Prehľad", + "host": "Hostia / IP adresa", + "port": "Prístav", + "username": "Používateľské meno", + "usernamePlaceholder": "Voliteľné", + "password": "Heslo", + "passwordPlaceholder": "Voliteľné", + "selectTransport": "Vyberte dopravný protokol", + "cameraBrand": "Značka kamery", + "selectBrand": "Vyberte značku kamery pre URL šablónu", + "customUrl": "Vlastné Stream URL", + "brandInformation": "Informácie o značke", + "brandUrlFormat": "Pre kamery s formátom RTSP URL ako: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "testConnection": "Testovacie pripojenie", + "testSuccess": "Test pripojenia úspešný!", + "testFailed": "Test pripojenia zlyhal. Skontrolujte svoj vstup a skúste to znova.", + "streamDetails": "Detaily vysielania", + "warnings": { + "noSnapshot": "Nemožno načítať snímku z konfigurovaného vysielania." + }, + "errors": { + "brandOrCustomUrlRequired": "Buď vyberte značku kamery s hostiteľom / IP alebo si vyberte \"Iný\" s vlastnou URL", + "nameRequired": "Názov kamery je povinný", + "nameLength": "Názov kamery musí byť 64 znakov alebo menej", + "invalidCharacters": "Názov kamery obsahuje neplatné znaky", + "nameExists": "Názov kamery už existuje", + "brands": { + "reolink-rtsp": "Reolink RTSP sa neodporúča. Odporúča sa povoliť HTTP v nastavení kamery a reštartovať sprievodca kamery." + }, + "customUrlRtspRequired": "Vlastné URL musia začať s \"rtsp / \"\". Manuálna konfigurácia je potrebná pre non-RTSP kamerové prúdy." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Skúmanie metadát kamery...", + "fetchingSnapshot": "Načítava sa snímka z kamery..." + }, + "connectionSettings": "Nastavenie pripojenia", + "detectionMethod": "Stream Detekcia Metóda", + "onvifPort": "ONVIF Port", + "probeMode": "Probe kamera", + "manualMode": "Ručný výber", + "detectionMethodDescription": "Vyskúša cez ONVIF (ak je podporovaný) nájsť kamery streamové adresy, alebo ručne vyberte značku kamery a jej preddefinované URL. Ak chcete zadať vlastnú URL RTSP, vyberte manuálne zadanie a označte \"Ostatné\".", + "onvifPortDescription": "Pre kamery, ktoré podporujú ONVIF, to je zvyčajne 80 alebo 8080.", + "useDigestAuth": "Použite overenie súhrnu", + "useDigestAuthDescription": "Použite HTTP stráviteľné overenie pre ONVIF. Niektoré kamery môžu vyžadovať vyhradený ONVIF užívateľské meno/password namiesto štandardného správcu." + }, + "step2": { + "description": "Vyhľadajte dostupné streamy z kamery alebo nakonfigurujte manuálne nastavenia na základe zvolenej metódy detekcie.", + "streamsTitle": "Kamerové prúdy", + "addStream": "Pridať Stream", + "addAnotherStream": "Pridať ďalší Stream", + "streamTitle": "Stream {{number}}", + "streamUrl": "Stream URL", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "url": "URL", + "resolution": "Rozlíšenie", + "selectResolution": "Vyberte rozlíšenie", + "quality": "Kvalita", + "selectQuality": "Vyberte kvalitu", + "roles": "Roly", + "roleLabels": { + "detect": "Detekcia objektov", + "record": "Nahrávanie", + "audio": "Zvuk" + }, + "testStream": "Testovacie pripojenie", + "testSuccess": "Test pripojenia bol úspešný!", + "testFailed": "Test pripojenia zlyhal. Skontrolujte zadané údaje a skúste to znova.", + "testFailedTitle": "Test Zlyhal", + "connected": "Pripojené", + "notConnected": "Nie je pripojený", + "featuresTitle": "Vlastnosti", + "go2rtc": "Znížte počet pripojení ku kamere", + "detectRoleWarning": "Aspoň jeden prúd musí mať \"detekt\" úlohu pokračovať.", + "rolesPopover": { + "title": "Roly streamu", + "detect": "Hlavné krmivo pre detekciu objektu.", + "record": "Ukladá segmenty video kanála na základe nastavení konfigurácie.", + "audio": "Kŕmenie pre detekciu zvuku." + }, + "featuresPopover": { + "title": "Funkcie streamu", + "description": "Použite prekrytie go2rtc na zníženie pripojenia k fotoaparátu." + }, + "streamDetails": "Detaily vysielania", + "probing": "Skúmajúca kamera...", + "retry": "Skúste to znova", + "testing": { + "probingMetadata": "Skúmanie metadát kamery...", + "fetchingSnapshot": "Načítava sa snímka z fotoaparátu..." + }, + "probeFailed": "Nepodarilo sa otestovať kameru: {{error}}", + "probingDevice": "Snímacie zariadenie...", + "probeSuccessful": "Sonda úspešná", + "probeError": "Chyba sondy", + "probeNoSuccess": "Sonda neúspešná", + "deviceInfo": "Informácie o zariadení", + "manufacturer": "Výrobca", + "model": "Model", + "firmware": "Firmvér", + "profiles": "Profily", + "ptzSupport": "PTZ Podpora", + "autotrackingSupport": "Podpora automatického sledovania", + "presets": "Prestavby", + "rtspCandidates": "RTSP kandidátov", + "rtspCandidatesDescription": "Z kamery boli nájdené nasledujúce adresy URL RTSP. Otestujte pripojenie a zobrazte metadáta streamu.", + "noRtspCandidates": "Z kamery sa nenašli žiadne URL adresy RTSP. Vaše prihlasovacie údaje môžu byť nesprávne alebo kamera nepodporuje protokol ONVIF alebo metódu použitú na získanie URL adries RTSP. Vráťte sa späť a zadajte URL adresu RTSP manuálne.", + "candidateStreamTitle": "Kandidát {{number}}", + "useCandidate": "Použitie", + "uriCopy": "Kopírovať", + "uriCopied": "URI skopírované do schránky", + "testConnection": "Testovacie pripojenie", + "toggleUriView": "Kliknutím prepnete zobrazenie celého URI", + "errors": { + "hostRequired": "Vyžaduje sa hostiteľská/IP adresa" + } + }, + "step3": { + "connectStream": "Pripojiť", + "connectingStream": "Pripája", + "disconnectStream": "Odpojiť", + "estimatedBandwidth": "Odhadovaná šírka pásma", + "roles": "Roly", + "none": "Žiadny", + "error": "Chyba", + "streamValidated": "Stream {{number}} úspešne overený", + "streamValidationFailed": "Stream {{number}} validácia zlyhala", + "saveAndApply": "Uložiť novú kameru", + "saveError": "Neplatná konfigurácia. Skontrolujte nastavenia.", + "issues": { + "title": "Stream Platnosť", + "videoCodecGood": "Kód videa {{codec}}.", + "audioCodecGood": "Audio kódc je {{codec}}.", + "noAudioWarning": "Žiadne audio zistené pre tento prúd, nahrávanie nebude mať audio.", + "audioCodecRecordError": "AAC audio kodek je potrebný na podporu audio v záznamoch.", + "audioCodecRequired": "Zvukový prúd je povinný podporovať detekciu zvuku.", + "restreamingWarning": "Zníženie pripojenia ku kamery pre rekordný prúd môže mierne zvýšiť využitie CPU.", + "dahua": { + "substreamWarning": "Substream 1 je uzamknutý na nízke rozlíšenie. Mnoho Dahua / Amcrest / EmpireTech kamery podporujú ďalšie podstreamy, ktoré musia byť povolené v nastavení kamery. Odporúča sa skontrolovať a využiť tie prúdy, ak je k dispozícii." + }, + "hikvision": { + "substreamWarning": "Substream 1 je uzamknutý na nízke rozlíšenie. Mnoho Hikvision kamery podporujú ďalšie podstreamy, ktoré musia byť povolené v nastavení kamery. Odporúča sa skontrolovať a využiť tie prúdy, ak je k dispozícii." + }, + "resolutionHigh": "Rozlíšenie {{resolution}} môže spôsobiť zvýšenú spotrebu zdrojov.", + "resolutionLow": "Rozlíšenie {{resolution}} môže byť príliš nízka pre spoľahlivú detekciu malých objektov." + }, + "description": "Nakonfigurujte role streamov a pridajte ďalšie streamy pre vašu kameru.", + "validationTitle": "Stream Platnosť", + "connectAllStreams": "Pripojte všetky prúdy", + "reconnectionSuccess": "Opätovné pripojenie bolo úspešné.", + "reconnectionPartial": "Niektoré prúdy sa nepodarilo prepojiť.", + "streamUnavailable": "Ukážka streamu nie je k dispozícii", + "reload": "Znovu načítať", + "connecting": "Pripája...", + "streamTitle": "Stream {{number}}", + "valid": "Platné", + "failed": "Zlyhanie", + "notTested": "Netestované", + "streamsTitle": "Kamerové prúdy", + "addStream": "Pridať Stream", + "addAnotherStream": "Pridať ďalší Stream", + "streamUrl": "Stream URL", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "selectStream": "Vyberte stream", + "searchCandidates": "Hľadať kandidátov...", + "noStreamFound": "Nenašiel sa žiadny stream", + "url": "URL", + "resolution": "Rozlíšenie", + "selectResolution": "Vyberte rozlíšenie", + "quality": "Kvalita", + "selectQuality": "Vyberte kvalitu", + "roleLabels": { + "detect": "Detekcia objektov", + "record": "Nahrávanie", + "audio": "Zvuk" + }, + "testStream": "Testovanie pripojenia", + "testSuccess": "Stream test úspešné!", + "testFailed": "Stream test zlyhal", + "testFailedTitle": "Test Zlyhal", + "connected": "Pripojené", + "notConnected": "Nie je pripojený", + "featuresTitle": "Vlastnosti", + "go2rtc": "Znížte počet pripojení ku kamere", + "detectRoleWarning": "Aspoň jeden prúd musí mať \"detekt\" úlohu pokračovať.", + "rolesPopover": { + "title": "Roly streamu", + "detect": "Hlavné krmivo pre detekciu objektu.", + "record": "Ukladá segmenty video kanála na základe nastavení konfigurácie.", + "audio": "Kŕmenie pre detekciu zvuku." + }, + "featuresPopover": { + "title": "Funkcie streamu", + "description": "Použite prekrytie go2rtc na zníženie pripojenia k fotoaparátu." + } + }, + "step4": { + "description": "Záverečné overenie a analýza pred uložením nového fotoaparátu. Pripojte každý prúd pred uložením.", + "validationTitle": "Stream Platnosť", + "connectAllStreams": "Pripojte všetky prúdy", + "reconnectionSuccess": "Opätovné pripojenie bolo úspešné.", + "reconnectionPartial": "Niektoré prúdy sa nepodarilo prepojiť.", + "streamUnavailable": "Ukážka streamu nie je k dispozícii", + "reload": "Znovu načítať", + "connecting": "Pripája...", + "streamTitle": "Stream {{number}}", + "valid": "Platné", + "failed": "Zlyhanie", + "notTested": "Netestované", + "connectStream": "Pripojiť", + "connectingStream": "Pripája", + "disconnectStream": "Odpojiť", + "estimatedBandwidth": "Odhadovaná šírka pásma", + "roles": "Roly", + "ffmpegModule": "Použite režim kompatibility prúdu", + "ffmpegModuleDescription": "Ak sa stream nenačíta ani po niekoľkých pokusoch, skúste túto funkciu povoliť. Keď je táto funkcia povolená, Frigate použije modul ffmpeg s go2rtc. To môže poskytnúť lepšiu kompatibilitu s niektorými streammi z kamier.", + "none": "Žiadne", + "error": "Chyba", + "streamValidated": "Stream {{number}} úspešne overený", + "streamValidationFailed": "Stream {{number}} validácia zlyhala", + "saveAndApply": "Uložiť novú kameru", + "saveError": "Neplatná konfigurácia. Skontrolujte nastavenia.", + "issues": { + "title": "Platnosť Streamu", + "videoCodecGood": "Kód videa je {{codec}}.", + "audioCodecGood": "Audio kódc je {{codec}}.", + "resolutionHigh": "Rozlíšenie {{resolution}} môže spôsobiť zvýšenú spotrebu zdrojov.", + "resolutionLow": "Rozlíšenie {{resolution}} môže byť príliš nízka pre spoľahlivú detekciu malých objektov.", + "noAudioWarning": "Žiadne audio nebolo detekovane pre tento prúd, nahrávanie nebude mať audio.", + "audioCodecRecordError": "AAC audio kodek je potrebný na podporu audio v záznamoch.", + "audioCodecRequired": "Zvukový prúd je povinný podporovať detekciu zvuku.", + "restreamingWarning": "Zníženie pripojenia ku kamery pre rekordný prúd môže mierne zvýšiť využitie CPU.", + "brands": { + "reolink-rtsp": "Reolink RTSP sa neodporúča. Odporúča sa povoliť HTTP v nastavení kamery a reštartovať sprievodca kamery." + }, + "dahua": { + "substreamWarning": "Čiastkový stream 1 je uzamknutý na nízke rozlíšenie. Mnoho kamier Dahua / Amcrest / EmpireTech podporuje ďalšie čiastkové streamy, ktoré je potrebné povoliť v nastaveniach kamery. Odporúča sa skontrolovať a využiť tieto streamy, ak sú k dispozícii." + }, + "hikvision": { + "substreamWarning": "Čiastkový stream 1 je uzamknutý na nízke rozlíšenie. Mnoho kamier Hikvision podporuje ďalšie čiastkové streamy, ktoré je potrebné povoliť v nastaveniach kamery. Odporúča sa skontrolovať a využiť tieto streamy, ak sú k dispozícii." + } + } + } + }, + "cameraManagement": { + "title": "Správa kamier", + "addCamera": "Pridať novu kameru", + "editCamera": "Upraviť kameru:", + "selectCamera": "Vyberte kameru", + "backToSettings": "Späť na nastavenia kamery", + "streams": { + "title": "Enable / Disable kamery", + "desc": "Dočasne deaktivujte kameru, kým sa Frigate nereštartuje. Deaktivácia kamery úplne zastaví spracovanie streamov z tejto kamery aplikáciou Frigate. Detekcia, nahrávanie a ladenie nebudú k dispozícii.
    Poznámka: Toto nezakáže restreamy go2rtc." + }, + "cameraConfig": { + "add": "Pridať kameru", + "edit": "Upraviť kameru", + "description": "Konfigurovať nastavenia kamery, vrátane vstupov streamu a rolí.", + "name": "Názov kamery", + "nameRequired": "Názov kamery je povinný", + "nameLength": "Názov kamery musí byť menšia ako 64 znakov.", + "namePlaceholder": "e.g., predne_dvere alebo Prehľad Záhrady", + "enabled": "Povoliť", + "ffmpeg": { + "inputs": "Vstupné streamy", + "path": "Cesta streamu", + "pathRequired": "Cesta k streamu je povinná", + "pathPlaceholder": "rtsp://...", + "roles": "Roly", + "rolesRequired": "Je vyžadovaná aspoň jedna rola", + "rolesUnique": "Každá rola (audio, detekcia, záznam) môže byť priradená iba k jednému streamu", + "addInput": "Pridať vstupný stream", + "removeInput": "Odobrať vstupný stream", + "inputsRequired": "Je vyžadovaný aspoň jeden vstupný stream" + }, + "go2rtcStreams": "go2rtc Streamy", + "streamUrls": "Stream URLs", + "addUrl": "Pridať URL", + "addGo2rtcStream": "Pridať go2rtc Stream", + "toast": { + "success": "Kamera {{cameraName}} bola úspešne uložená" + } + } + }, + "cameraReview": { + "title": "Nastavenie recenzie kamery", + "object_descriptions": { + "title": "Generatívne popisy objektov umelej inteligencie", + "desc": "Dočasne povoľ/zakáž AI vygenerované popisy objektov pre túto kameru pokiaľ nebude Frigate reštartovaná. Keď je zakázané, AI vygenerované popisy nebudú žiadané pre sledované objekty na tejto kamere." + }, + "review_descriptions": { + "title": "Popisy generatívnej umelej inteligencie", + "desc": "Dočasne povoľ/zakáž AI vygenerované popisy revízií pre túto kameru pokiaľ nebude Frigate reštartovaná. Keď je zakázané, AI vygenerované popisy nebudú žiadané pre sledované objekty na tejto kamere." + }, + "review": { + "title": "Recenzia", + "desc": "Dočasne umožňujú/disable upozornenia a detekcia pre tento fotoaparát až do reštartu Frigate. Pri vypnutých, nebudú vygenerované žiadne nové položky preskúmania. ", + "alerts": "Upozornenia ", + "detections": "Detekcie " + }, + "reviewClassification": { + "title": "Preskúmať klasifikáciu", + "desc": "Frigate kategorizuje položky recenzií ako Upozornenia a Detekcie. Predvolene sa všetky objekty typu osoba a auto považujú za Upozornenia. Kategorizáciu položiek recenzií môžete spresniť konfiguráciou požadovaných zón pre ne.", + "noDefinedZones": "Pre túto kameru nie sú definované žiadne zóny.", + "objectAlertsTips": "Všetky objekty {{alertsLabels}} na {{cameraName}} sa zobrazia ako Upozornenia.", + "zoneObjectAlertsTips": "Všetky objekty {{alertsLabels}} detekované v {{zone}} na {{cameraName}} budú zobrazené ako Upozornenia.", + "objectDetectionsTips": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{cameraName}}, sa zobrazia ako detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú.", + "zoneObjectDetectionsTips": { + "text": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{zone}} na kamere {{cameraName}}, budú zobrazené ako detekcie.", + "notSelectDetections": "Všetky objekty typu {{detectionsLabels}} detekované v zóne {{zone}} na kamere {{cameraName}}, ktoré nie sú zaradené do kategórie Upozornenia, sa zobrazia ako Detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú.", + "regardlessOfZoneObjectDetectionsTips": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{cameraName}}, sa zobrazia ako detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú." + }, + "unsavedChanges": "Nezaradené Nastavenie hodnotenia pre {{camera}}", + "selectAlertsZones": "Vyberte zóny pre upozornenia", + "selectDetectionsZones": "Vyberte zóny pre detekcie", + "limitDetections": "Obmedziť detekciu na konkrétne zóny", + "toast": { + "success": "Konfigurácia klasifikácie bola uložená. Reštartujte Frigate, aby sa zmeny prejavili." + } + } + }, + "users": { + "title": "Používatelia", + "management": { + "title": "Správa používateľov", + "desc": "Spravovať používateľské účty tejto inštancie Frigate." + }, + "addUser": "Pridať používateľa", + "updatePassword": "Obnoviť Heslo", + "toast": { + "success": { + "createUser": "Užívateľ {{user}} úspešne vytvorený", + "deleteUser": "Užívateľ {{user}} úspešne odobraný", + "updatePassword": "Heslo úspešne aktualizované.", + "roleUpdated": "Aktualizovaná rola pre používateľa {{user}}" + }, + "error": { + "setPasswordFailed": "Nepodarilo sa uložiť heslo: {{errorMessage}}", + "createUserFailed": "Nepodarilo sa vytvoriť používateľa: {{errorMessage}}", + "deleteUserFailed": "Nepodarilo sa odstrániť používateľa: {{errorMessage}}", + "roleUpdateFailed": "Nepodarilo sa aktualizovať rolu: {{errorMessage}}" + } + }, + "table": { + "username": "Používateľské meno", + "actions": "Akcie", + "role": "Rola", + "noUsers": "Nenašli sa žiadni používatelia.", + "changeRole": "Zmeniť rolu používateľa", + "password": "Resetovať Heslo", + "deleteUser": "Odstrániť používateľa" + }, + "dialog": { + "form": { + "user": { + "title": "Používateľské meno", + "desc": "Povolené sú iba písmená, čísla, bodky a podčiarkovníky.", + "placeholder": "Zadajte používateľské meno" + }, + "password": { + "title": "Heslo", + "placeholder": "Zadajte heslo", + "confirm": { + "title": "Potvrdiť heslo", + "placeholder": "Potvrdiť heslo" + }, + "strength": { + "title": "Sila hesla: ", + "weak": "Slabý", + "medium": "Stredná", + "strong": "Silný", + "veryStrong": "Veľmi silný" + }, + "match": "Heslá sa zhodujú", + "notMatch": "Heslá sa nezhodujú" + }, + "newPassword": { + "title": "Nové heslo", + "placeholder": "Zadajte nové heslo", + "confirm": { + "placeholder": "Znovu zadajte nové heslo" + } + }, + "usernameIsRequired": "Vyžaduje sa používateľské meno", + "passwordIsRequired": "Heslo je povinné" + }, + "createUser": { + "title": "Vytvorenie nového užívateľa", + "desc": "Pridajte nový používateľský účet a zadajte rolu pre prístup k oblastiam používateľského rozhrania Frigate.", + "usernameOnlyInclude": "Používateľské meno môže obsahovať iba písmená, číslice, . alebo _", + "confirmPassword": "Potvrďte svoje heslo" + }, + "deleteUser": { + "title": "Odstrániť užívateľa", + "desc": "Túto akciu nie je možné vrátiť späť. Týmto sa natrvalo odstráni používateľský účet a odstránia sa všetky súvisiace údaje.", + "warn": "Naozaj chcete odstrániť používateľa {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "Heslo nemôže byť prázdne", + "doNotMatch": "Heslá sa nezhodujú", + "updatePassword": "Aktualizácia hesla pre {{username}}", + "setPassword": "Nastaviť heslo", + "desc": "Vytvorte si silné heslo na zabezpečenie tohto účtu." + }, + "changeRole": { + "title": "Zmeniť rolu používateľa", + "select": "Vyberte rolu", + "desc": "Aktualizovať povolenia pre používateľa {{username}}", + "roleInfo": { + "intro": "Vyberte príslušnú rolu pre tohto používateľa:", + "admin": "Správca", + "adminDesc": "Úplný prístup ku všetkým funkciám.", + "viewer": "Divák", + "viewerDesc": "Obmedzené iba na živé dashboardy, funkcie Review, Explore a Exports.", + "customDesc": "Vlastná rola so špecifickým prístupom k kamere." + } + } + } + }, + "roles": { + "management": { + "title": "Správa roly diváka", + "desc": "Spravujte vlastné roly divákov a ich povolenia na prístup ku kamere pre túto inštanciu Frigate." + }, + "addRole": "Pridať rolu", + "table": { + "role": "Rola", + "cameras": "Kamery", + "actions": "Akcie", + "noRoles": "Neboli nájdené žiadne vlastné role.", + "editCameras": "Editovať kamery", + "deleteRole": "Odstrániť rolu" + }, + "toast": { + "success": { + "createRole": "Rola {{role}} bola úspešne vytvorená", + "updateCameras": "Kamery aktualizované pre rolu {{role}}", + "deleteRole": "Rola {{role}} bola úspešne odstránená", + "userRolesUpdated_one": "{{count}} užívateľ priradený tejto úlohe bol aktualizovaný na \"viewer\", ktorý má prístup ku všetkým kamerám.", + "userRolesUpdated_few": "{{count}} užívatelia priradení tejto úlohe boli aktualizovaní na \"viewer\", ktorý má prístup ku všetkým kamerám.", + "userRolesUpdated_other": "{{count}} užívatelia priradení tejto úlohe boli aktualizovaní na \"viewer\", ktorý má prístup ku všetkým kamerám." + }, + "error": { + "createRoleFailed": "Nepodarilo sa vytvoriť rolu: {{errorMessage}}", + "updateCamerasFailed": "Nepodarilo sa aktualizovať kamery: {{errorMessage}}", + "deleteRoleFailed": "Nepodarilo sa odstrániť rolu: {{errorMessage}}", + "userUpdateFailed": "Nepodarilo sa aktualizovať používateľské role: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Vytvoriť novú rolu", + "desc": "Pridajte novú úlohu a zadajte prístup k kamerám." + }, + "editCameras": { + "title": "Editovať Rolu Kamery", + "desc": "Aktualizujte prístup k kamere pre rolu {{role}}." + }, + "deleteRole": { + "title": "Odstrániť rolu", + "desc": "Túto akciu nie je možné vrátiť späť. Týmto sa rola natrvalo odstráni a všetci používatelia s touto rolou budú priradení k role „pozerač“, ktorá umožní divákovi prístup ku všetkým kamerám.", + "warn": "Ste si istí, že chcete odstrániť {{role}}?", + "deleting": "Odstraňuje sa..." + }, + "form": { + "role": { + "title": "Názov role", + "placeholder": "Zadajte názov roly", + "desc": "Povolené sú iba písmená, čísla, bodky a podčiarkovníky.", + "roleIsRequired": "Vyžaduje sa názov roly", + "roleOnlyInclude": "Názov role môže obsahovať iba písmená, čísla, . alebo _", + "roleExists": "Úloha s týmto menom už existuje." + }, + "cameras": { + "title": "Kamery", + "desc": "Vyberte kamery, ku ktorým má táto rola prístup. Vyžaduje sa aspoň jedna kamera.", + "required": "Aspoň jedna kamera musí byť vybraná." + } + } + } + }, + "notification": { + "title": "Notifikacie", + "notificationSettings": { + "title": "Nastavenia notifikácií", + "desc": "Frigate dokáže natívne odosielať push notifikácie do vášho zariadenia, keď je spustený v prehliadači alebo nainštalovaný ako PWA." + }, + "notificationUnavailable": { + "title": "Notifikacie su nedostupné", + "desc": "Webové push notifikácie vyžadujú zabezpečený kontext (https://…). Ide o obmedzenie prehliadača. Ak chcete používať notifikácie, pristupujte k Frigate bezpečne." + }, + "globalSettings": { + "title": "Globálne nastavenia", + "desc": "Dočasne pozastaviť upozornenia pre konkrétne kamery na všetkých registrovaných zariadeniach." + }, + "email": { + "title": "E-mail", + "placeholder": "e.g. príklad@email.com", + "desc": "Vyžaduje sa platný e-mail, ktorý bude použitý na upozornenie v prípade akýchkoľvek problémov so službou push." + }, + "cameras": { + "title": "Kamery", + "noCameras": "K dispozícii nie sú žiadne kamery", + "desc": "Vyberte, na ktoré kamery umožňujú notifikácie." + }, + "deviceSpecific": "Špecifické nastavenia zariadenia", + "registerDevice": "Registrovať toto zariadenie", + "unregisterDevice": "Zrušte registráciu tohto zariadenia", + "sendTestNotification": "Odoslať testovacie oznámenie", + "unsavedRegistrations": "Neuložené registrácie oznámení", + "unsavedChanges": "Neuložené zmeny upozornení", + "active": "Upozornenia sú aktívne", + "suspended": "Oznámenie pozastavuju {{time}}", + "suspendTime": { + "suspend": "Pozastaviť", + "5minutes": "Pozastaviť na 5 minút", + "10minutes": "Pozastaviť na 10 minút", + "30minutes": "Pozastaviť na 30 minút", + "1hour": "Pozastaviť na 1 hodinu", + "12hours": "Pozastaviť na 12 hodín", + "24hours": "Pozastaviť na 24 hodín", + "untilRestart": "Pozastaviť do reštartovania" + }, + "cancelSuspension": "Zrušiť pozastavenie", + "toast": { + "success": { + "registered": "Úspešne zaregistrované pre upozornenia. Pred odoslaním akýchkoľvek upozornení (vrátane testovacieho upozornenia) je potrebné reštartovať Frigate.", + "settingSaved": "Nastavenie oznámenia boli uložené." + }, + "error": { + "registerFailed": "Uloženie registrácie upozornenia zlyhalo." + } + } + }, + "frigatePlus": { + "title": "Nastavenie Frigate+", + "apiKey": { + "title": "Frigate + API kľúč", + "validated": "Frigate + API kľúč je detekovaný a overený", + "notValidated": "Frigate + API kľúč nie je detekovaný alebo nie je overený", + "desc": "Frigate+ API kľúč umožňuje integráciu s Frigate+ služby.", + "plusLink": "Prečítajte si viac o Frigate+" + }, + "snapshotConfig": { + "title": "Konfigurácia snímky", + "desc": "Odosielanie do Frigate+ vyžaduje, aby boli v konfigurácii povolené snímky aj snímky clean_copy.", + "cleanCopyWarning": "Niektoré kamery majú povolené snímky, ale voľba clean_copy je zakázaná. Pre možnosť odosielania snímok z týchto kamier do služby Frigate+ je nutné túto voľbu povoliť v konfigurácii snímok.", + "table": { + "camera": "Kamera", + "snapshots": "Snímky", + "cleanCopySnapshots": "clean_copy Snímky" + } + }, + "modelInfo": { + "title": "Informácie o Modele", + "modelType": "Typ Modelu", + "trainDate": "Dátum Tréningu", + "baseModel": "Základný Model", + "plusModelType": { + "baseModel": "Základný Model", + "userModel": "Doladené" + }, + "supportedDetectors": "Podporované Detektory", + "cameras": "Kamery", + "loading": "Načítavam informácie o modeli…", + "error": "Chyba načítania informácií o modeli", + "availableModels": "Dostupné Moduly", + "loadingAvailableModels": "Načítavam dostupné modely…", + "modelSelect": "Tu môžete vybrať dostupné modely zo služby Frigate+. Upozorňujeme, že je možné zvoliť iba modely kompatibilné s aktuálnou konfiguráciou detektora." + }, + "unsavedChanges": "Neuložené zmeny nastavenia Frigate+", + "restart_required": "Vyžadovaný reštart (model Frigate+ zmenený)", + "toast": { + "success": "Nastavenia Frigate+ boli uložené. Reštartujte Frigate+ pre aplikovanie zmien.", + "error": "Chyba pri ukladaní zmien konfigurácie: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Spúšťače", + "semanticSearch": { + "title": "Sémantické vyhľadávanie je vypnuté", + "desc": "Na používanie spúšťačov musí byť povolené sémantické vyhľadávanie." + }, + "management": { + "title": "Spúšťače", + "desc": "Správa spúšťa {{camera}}. Použite typ miniatúry, aby ste spustili na podobných miniatúr na vybraných tracked objekt, a typ popisu, aby ste spustili podobné popisy na text, ktorý určíte." + }, + "addTrigger": "Pridať Spúšťač", + "table": { + "name": "Meno", + "type": "Typ", + "content": "Obsah", + "threshold": "Prah", + "actions": "Akcie", + "noTriggers": "Pre túto kameru nie sú nakonfigurované žiadne spúšťače.", + "edit": "Upraviť", + "deleteTrigger": "Odstrániť spúšťač", + "lastTriggered": "Naposledy spustené" + }, + "type": { + "thumbnail": "Náhľad", + "description": "Popis" + }, + "actions": { + "notification": "Poslať upozornenie", + "sub_label": "Pridať vedľajší štítok", + "attribute": "Pridať atribút" + }, + "dialog": { + "createTrigger": { + "title": "Vytvoriť spúšťač", + "desc": "Vytvorte spúšť pre kameru {{camera}}" + }, + "editTrigger": { + "title": "Upraviť spúšťač", + "desc": "Upraviť nastavenia spúšťača na kamere {{camera}}" + }, + "deleteTrigger": { + "title": "Odstrániť spúšťač", + "desc": "Naozaj chcete odstrániť spúšťač {{triggerName}}? Túto akciu nie je možné vrátiť späť." + }, + "form": { + "name": { + "title": "Meno", + "placeholder": "Zadajte meno pre spúšťača", + "description": "Zadajte jedinečné meno alebo popis na identifikáciu tohto spúšťania", + "error": { + "minLength": "Názov musí mať aspoň 2 znaky.", + "invalidCharacters": "Meno môže obsahovať iba písmená, číslice, podčiarkovníky a pomlčky.", + "alreadyExists": "Spúšťač s týmto názvom už pre túto kameru existuje." + } + }, + "enabled": { + "description": "Povoliť alebo zakázať tento spúšťač" + }, + "type": { + "title": "Typ", + "placeholder": "Vybrať typ spúšťača", + "description": "Spustiť, keď sa zistí podobný popis sledovaného objektu", + "thumbnail": "Spustiť, keď sa zistí podobná miniatúra sledovaného objektu" + }, + "content": { + "title": "Obsah", + "imagePlaceholder": "Vyberte miniatúru", + "textPlaceholder": "Zadajte obsah textu", + "imageDesc": "Zobrazujú sa iba posledné 100 miniatúr. Ak nemôžete nájsť požadovanú miniatúru, prečítajte si skôr objekty v preskúmať a nastaviť spúšťací z ponuky tam.", + "textDesc": "Zadajte text, aby ste spustili túto akciu, keď je detekovaný podobný popis objektu.", + "error": { + "required": "Obsah je potrebný." + } + }, + "threshold": { + "title": "Prah", + "desc": "Nastavte prah podobnosti pre tento spúšťač. Vyšší prah znamená, že na spustenie spúšťača je potrebná bližšia zhoda.", + "error": { + "min": "Threshold musí byť aspoň 0", + "max": "Threshold musí byť na väčšine 1" + } + }, + "actions": { + "title": "Akcie", + "desc": "V predvolenom nastavení Frigate odosiela MQTT správu pre všetky spúšťače. Zvoľte dodatočnú akciu, ktorá sa má vykonať, keď sa tento spúšťač aktivuje.", + "error": { + "min": "Musí byť vybraná aspoň jedna akcia." + } + } + } + }, + "wizard": { + "title": "Vytvoriť spúšťač", + "step1": { + "description": "Konfigurujte základné nastavenia pre vašu spúšť." + }, + "step2": { + "description": "Nastavte obsah, ktorý spustí túto akciu." + }, + "step3": { + "description": "Konfigurovať prah a akcie pre tento spúšťač." + }, + "steps": { + "nameAndType": "Meno a typ", + "configureData": "Konfigurovať údaje", + "thresholdAndActions": "Prah a akcie" + } + }, + "toast": { + "success": { + "createTrigger": "Spúšťač {{name}} bol úspešne vytvorený.", + "updateTrigger": "Spúšťač {{name}} bol úspešne aktualizovaný.", + "deleteTrigger": "Spúšťač {{name}} bol úspešne zmazaný." + }, + "error": { + "createTriggerFailed": "Nepodarilo sa vytvoriť spúšťač: {{errorMessage}}", + "updateTriggerFailed": "Nepodarilo sa aktualizovať spúšťač: {{errorMessage}}", + "deleteTriggerFailed": "Nepodarilo sa zmazať spúšťač: {{errorMessage}}" + } + } + } +} diff --git a/web/public/locales/sk/views/system.json b/web/public/locales/sk/views/system.json new file mode 100644 index 0000000..6b40329 --- /dev/null +++ b/web/public/locales/sk/views/system.json @@ -0,0 +1,207 @@ +{ + "documentTitle": { + "cameras": "Štatistiky kamier - Frigate", + "storage": "Štatistiky úložiska - Frigate", + "general": "Všeobecné štatistiky - Frigate", + "enrichments": "Štatistiky obohatenia - Frigate", + "logs": { + "frigate": "Protokoly Frigate - Frigate", + "go2rtc": "Protokoly Go2RTC - Frigate", + "nginx": "Protokoly Ngnix - Frigate" + } + }, + "title": "System", + "metrics": "Systémové metriky", + "logs": { + "download": { + "label": "Stiahnúť záznamy" + }, + "copy": { + "label": "Kopírovať do schránky", + "success": "Protokoly boli skopírované do schránky", + "error": "Nepodarilo sa skopírovať protokoly do schránky" + }, + "type": { + "label": "Typ", + "timestamp": "Časová pečiatka", + "tag": "Šťitok (Tag)", + "message": "Správa" + }, + "tips": "Záznamy sa streamujú zo servera", + "toast": { + "error": { + "fetchingLogsFailed": "Chyba pri načítaní protokolov: {{errorMessage}}", + "whileStreamingLogs": "Chyba pri streamovaní protokolov: {{errorMessage}}" + } + } + }, + "general": { + "title": "Hlavný", + "detector": { + "title": "Detektory", + "inferenceSpeed": "Detekčná rýchlosť", + "temperature": "Detekčná teplota", + "cpuUsage": "Detektor využitia CPU", + "memoryUsage": "Detektor využitia pamäte", + "cpuUsageInformation": "CPU použitý na prípravu vstupných a výstupných údajov do/z detekčných modelov. Táto hodnota nemeria využitie inferencie, a to ani v prípade použitia GPU alebo akcelerátora." + }, + "hardwareInfo": { + "title": "Informácie o hardvéri", + "gpuUsage": "Využitie GPU", + "gpuMemory": "Pamäť GPU", + "gpuEncoder": "GPU kódovač", + "gpuDecoder": "GPU dekodér", + "gpuInfo": { + "vainfoOutput": { + "title": "Výstup Vainfo", + "returnCode": "Návratový kód: {{code}}", + "processOutput": "Výstup procesu:", + "processError": "Chyba procesu:" + }, + "nvidiaSMIOutput": { + "title": "Výstup Nvidia SMI", + "name": "Meno: {{name}}", + "driver": "Vodič: {{driver}}", + "cudaComputerCapability": "Výpočtové možnosti CUDA: {{cuda_compute}}", + "vbios": "Informácie o VBiose: {{vbios}}" + }, + "closeInfo": { + "label": "Zatvorte informácie o GPU" + }, + "copyInfo": { + "label": "Kopírovať informácie o GPU" + }, + "toast": { + "success": "Informácie o grafickej karte boli skopírované do schránky" + } + }, + "npuUsage": "Použitie NPU", + "npuMemory": "Pamäť NPU", + "intelGpuWarning": { + "title": "Intel GPU Stats Upozornenie", + "message": "Štatistiky GPU nedostupné", + "description": "Toto je známa chyba v Štatistike správ Intel (intel_gpu_top) kde sa rozpadne a opakovane vráti používanie GPU 0% aj v prípadoch, keď hardvér detekcie objektov správne beží na (i)GPU. Toto nie je Frigate chyba. Môžete reštartovať a tak dočasne opraviť problém a potvrdiť, že GPU funguje správne. Toto nemá vplyv na výkon." + } + }, + "otherProcesses": { + "title": "Iné procesy", + "processCpuUsage": "Proces využitia CPU", + "processMemoryUsage": "Procesné využitie pamäte", + "series": { + "go2rtc": "go2rtc", + "recording": "nahrávka", + "review_segment": "skontrolovať segment", + "audio_detector": "zvukový detektor" + } + } + }, + "storage": { + "title": "Skladovanie", + "overview": "Prehľad", + "recordings": { + "title": "Nahrávky", + "tips": "Táto hodnota predstavuje celkové úložisko, ktoré používajú nahrávky v databáze Frigate. Frigate nesleduje využitie úložiska pre všetky súbory na vašom disku.", + "earliestRecording": "Najstaršia dostupná nahrávka:" + }, + "shm": { + "title": "Alokácia SHM (zdieľanej pamäte)", + "warning": "Aktuálna veľkosť SHM {{total}}MB je príliš malá. Zvýšte ju aspoň na {{min_shm}}MB." + }, + "cameraStorage": { + "title": "Úložisko kamery", + "camera": "Kamera", + "unusedStorageInformation": "Nepoužité informácie o úložisku", + "storageUsed": "Skladovanie", + "percentageOfTotalUsed": "Percento z celkového počtu", + "bandwidth": "Šírka pásma", + "unused": { + "title": "Nepoužité", + "tips": "Táto hodnota nemusí presne zodpovedať voľnému miestu dostupnému pre Frigate, ak máte na disku uložené aj iné súbory okrem nahrávok Frigate. Frigate nesleduje využitie úložiska mimo svojich nahrávok." + } + } + }, + "cameras": { + "title": "Kamery", + "overview": "Prehľad", + "info": { + "aspectRatio": "pomer strán", + "cameraProbeInfo": "{{camera}} Informácie o sonde kamery", + "streamDataFromFFPROBE": "Údaje zo streamu sa získavajú pomocou príkazu ffprobe.", + "fetching": "Načítavajú sa údaje z kamery", + "stream": "Stream {{idx}}", + "video": "Video:", + "codec": "Kodek:", + "resolution": "Rozlíšenie:", + "fps": "FPS:", + "unknown": "Neznámy", + "audio": "Zvuk:", + "error": "Chyba: {{error}}", + "tips": { + "title": "Informácie o kamerovej sonde" + } + }, + "framesAndDetections": "Rámy / Detekcie", + "label": { + "camera": "kamera", + "detect": "odhaliť", + "skipped": "preskočené", + "ffmpeg": "FFmpeg", + "capture": "zachytiť", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "zachytiť{{camName}}", + "cameraDetect": "Detekcia {{camName}}", + "overallFramesPerSecond": "celkový počet snímok za sekundu", + "overallDetectionsPerSecond": "celkový počet detekcií za sekundu", + "overallSkippedDetectionsPerSecond": "celkový počet vynechaných detekcií za sekundu", + "cameraFramesPerSecond": "{{camName}} snímky za sekundu", + "cameraDetectionsPerSecond": "{{camName}}detekcie za sekundu", + "cameraSkippedDetectionsPerSecond": "{{camName}} vynechaných detekcií za sekundu" + }, + "toast": { + "success": { + "copyToClipboard": "Dáta sondy boli skopírované do schránky." + }, + "error": { + "unableToProbeCamera": "Nepodarilo sa overiť kameru: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Naposledy obnovené: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} má vysoké využitie CPU vo formáte FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} má vysoké využitie CPU pri detekcii ({{detectAvg}}%)", + "healthy": "Systém je zdravý", + "reindexingEmbeddings": "Preindexovanie vložených prvkov (dokončené na {{processed}} %)", + "cameraIsOffline": "{{camera}} je offline", + "detectIsSlow": "{{detect}} je pomalý ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} je veľmi pomalý ({{speed}} ms)", + "shmTooLow": "Alokácia /dev/shm ({{total}} MB) by sa mala zvýšiť aspoň na {{min}} MB." + }, + "enrichments": { + "title": "Obohatenia", + "infPerSecond": "Inferencie za sekundu", + "embeddings": { + "image_embedding": "Vkladanie obrázkov", + "text_embedding": "Vkladanie textu", + "face_recognition": "Rozpoznávanie tváre", + "plate_recognition": "Rozpoznávanie EČV", + "image_embedding_speed": "Rýchlosť vkladania obrázkov", + "face_embedding_speed": "Rýchlosť vkladania tváre", + "face_recognition_speed": "Rýchlosť rozpoznávania tváre", + "plate_recognition_speed": "Rýchlosť rozpoznávania EČV", + "text_embedding_speed": "Rýchlosť vkladania textu", + "yolov9_plate_detection_speed": "YOLOv9 rýchlosť detekcie ŠPZ", + "yolov9_plate_detection": "YOLOv9 Detekcia ŠPZ", + "review_description": "Popis recenzie", + "review_description_speed": "Popis recenzie Rýchlosťi", + "review_description_events_per_second": "Popis", + "object_description": "Popis objektu", + "object_description_speed": "Popis objektu Rýchlosť", + "object_description_events_per_second": "Popis objektu", + "classification": "{{name}} Klasifikácia", + "classification_speed": "{{name}} Rýchlosť Klasifikácie", + "classification_events_per_second": "{{name}} Klasifikácia Udalosti Za Sekundu" + }, + "averageInf": "Priemerný čas inferencie" + } +} diff --git a/web/public/locales/sl/audio.json b/web/public/locales/sl/audio.json new file mode 100644 index 0000000..8a1294d --- /dev/null +++ b/web/public/locales/sl/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Govor", + "babbling": "Mrmranje", + "yell": "Vpitje", + "whispering": "Šepetanje", + "laughter": "Smeh", + "crying": "Jok", + "sigh": "Vzdih", + "singing": "Petje", + "yodeling": "Jodlanje", + "rapping": "Repanje", + "run": "Tek", + "whistling": "Žvižganje", + "breathing": "Dihanje", + "snoring": "Smrčanje", + "cough": "Kašelj", + "animal": "Žival", + "pets": "Hišni ljubljenčki", + "dog": "Pes", + "cat": "Mačka", + "meow": "Mijavkanje", + "horse": "Konj", + "moo": "Mukanje", + "cowbell": "Kravji zvonec", + "pig": "Prašič", + "goat": "Koza", + "sheep": "Ovca", + "chicken": "Kokoš", + "turkey": "Puran", + "duck": "Raca", + "goose": "Gozd", + "bird": "Ptica", + "radio": "Radio", + "television": "Televizija", + "footsteps": "Koraki", + "bus": "Avtobus", + "train": "Vlak", + "toothbrush": "Zobna ščetka", + "bark": "Lubje", + "mouse": "Miška", + "keyboard": "Tipkovnica", + "boat": "Čoln", + "vehicle": "Vozilo", + "car": "Avto", + "motorcycle": "Motor", + "bicycle": "Kolo", + "skateboard": "Skejtbord", + "door": "Vrata", + "sink": "Umivalnik", + "blender": "Mešalnik", + "hair_dryer": "Fen", + "scissors": "Škarje", + "clock": "Ura", + "camera": "Kamera", + "bellow": "Rjovenje", + "whoop": "Vriskanje", + "musical_instrument": "Glasbilo", + "choir": "Zbor", + "burping": "Riganje", + "hiccup": "Kolcanje", + "fart": "Prdenje", + "hands": "Roke", + "finger_snapping": "Pokanje s prsti", + "clapping": "Ploskanje", + "heartbeat": "Utrip srca", + "cheering": "Navijanje", + "applause": "Aplavz", + "crowd": "Množica", + "children_playing": "Otroška igra", + "howl": "Tuljenje", + "purr": "Predenje", + "hiss": "Sikanje", + "livestock": "Živina", + "cattle": "Govedo", + "quack": "Gaganje", + "cluck": "Kokodakanje", + "cock_a_doodle_doo": "Kikirikanje", + "bleat": "Megetanje", + "neigh": "Frčanje", + "chirp": "Čivkanje", + "pigeon": "Golob", + "coo": "Gruljenje", + "crow": "Vrana", + "caw": "Krakanje", + "owl": "Sova", + "hoot": "Skovikanje", + "flapping_wings": "Mahanje s krili", + "dogs": "Psi", + "rats": "Podgane", + "insect": "Insekt", + "cricket": "Čriček", + "mosquito": "Komar", + "fly": "Muha", + "frog": "Žaba", + "snake": "Kača", + "music": "Glasba", + "guitar": "Kitara", + "electric_guitar": "Električna kitara", + "bass_guitar": "Bas kitara", + "acoustic_guitar": "Akustična kitara", + "strum": "Brenkaš", + "banjo": "Bandžo", + "sitar": "Sitar", + "mandolin": "Mandolina", + "ukulele": "Ukulele", + "piano": "Klavir", + "electric_piano": "Električni klavir", + "organ": "Orgale", + "electronic_organ": "Elektronske orgle", + "chant": "Prepevanje", + "mantra": "Mantra", + "child_singing": "Otroško petje", + "synthetic_singing": "Sintetično petje", + "humming": "Mrmranje (melodija)", + "groan": "Ston", + "grunt": "Frktanje", + "wheeze": "Sopenje", + "gasp": "Hlastanje za zrakom", + "pant": "Pihanje", + "snort": "Frkanje", + "throat_clearing": "Odkašljevanje", + "sneeze": "Kihanje", + "sniff": "Smrkanje", + "chewing": "Žvečenje", + "biting": "Grizenje", + "gargling": "Grgranje", + "stomach_rumble": "Kruljenje v trebuhu", + "heart_murmur": "Šum na srcu", + "chatter": "Klepetanje", + "yip": "Cviljenje", + "growling": "Režanje", + "whimper_dog": "Cviljenje psa", + "oink": "Siktanje", + "gobble": "Glavkanje", + "wild_animals": "Divje živali", + "roaring_cats": "Rjovenje velikih mačk", + "roar": "Rjovenje", + "squawk": "Skarat", + "patter": "Drobni koraki", + "croak": "Kvakanje", + "rattle": "Ropotulja/Sikanje", + "whale_vocalization": "Oglašanje kitov", + "plucked_string_instrument": "Bralna struna", + "snicker": "Hihitanje", + "shuffle": "Podrsavanje", + "bow_wow": "Hov-hov", + "caterwaul": "Mačje tuljenje", + "clip_clop": "Topot kopit", + "fowl": "Perutnina", + "honk": "Gaganje gosi", + "buzz": "Brenčanje", + "steel_guitar": "Steel kitara", + "tapping": "Tapkaš", + "zither": "Citre", + "hammond_organ": "Hammond orgle", + "synthesizer": "Sintetizator", + "sampler": "Sampler", + "harpsichord": "Čembalo", + "percussion": "Tolkala", + "drum_kit": "Boberji", + "drum_machine": "Ritem mašina", + "drum": "Boben", + "snare_drum": "Mali boben", + "rimshot": "Udarec ob rob", + "drum_roll": "Bobnanje", + "bass_drum": "Veliki boben", + "timpani": "Timpan", + "tabla": "Tabla (boben)", + "cymbal": "Činele", + "hi_hat": "Hi-Hat", + "wood_block": "Leseni blok", + "tambourine": "Tamburin", + "maraca": "Marakas", + "gong": "Gong", + "tubular_bells": "Cevni zvonovi", + "mallet_percussion": "Tolkala s palicami", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibrafon", + "steelpan": "Steelpan", + "orchestra": "Orkester", + "brass_instrument": "Trobilno glasbilo", + "french_horn": "Rog", + "trumpet": "Trobenta", + "trombone": "Pozavna", + "bowed_string_instrument": "Godalo", + "string_section": "Godalna sekcija", + "violin": "Violina", + "pizzicato": "Pizzicato", + "cello": "Čelo", + "double_bass": "Kontrabas", + "wind_instrument": "Pihalo", + "flute": "Flavta", + "saxophone": "Saksofon", + "clarinet": "Klarinet", + "harp": "Harfa", + "bell": "Zvonec", + "church_bell": "Cerkveni zvon", + "jingle_bell": "Kraguljček", + "bicycle_bell": "Zvonec na kolesu", + "tuning_fork": "Glasbene vilice", + "chime": "Zvončkljanje", + "wind_chime": "Vetrni zvonček", + "harmonica": "Ustna harmonika", + "accordion": "Harmonika", + "bagpipes": "Dude", + "didgeridoo": "Didžeridu", + "theremin": "Teremin", + "singing_bowl": "Pivska posoda", + "scratching": "Praskanje", + "pop_music": "Pop glasba", + "hip_hop_music": "Hip-hop glasba", + "beatboxing": "Beatboxing", + "rock_music": "Rock glasba", + "heavy_metal": "Heavy Metal", + "punk_rock": "Punk rock", + "grunge": "Grunge", + "progressive_rock": "Progresivni rock", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Psihedelični rock", + "rhythm_and_blues": "Rhythm and Blues", + "soul_music": "Soul glasba", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Swing glasba", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Ljudska glasba", + "middle_eastern_music": "Bližnjevzhodna glasba", + "jazz": "Jazz", + "disco": "Disko", + "classical_music": "Klasična glasba", + "opera": "Opera", + "electronic_music": "Elektronska glasba", + "house_music": "House glasba", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Electronica", + "electronic_dance_music": "Elektronska plesna glasba", + "ambient_music": "Ambientalna glasba", + "trance_music": "Trance glasba", + "music_of_latin_america": "Latinskoameriška glasba", + "salsa_music": "Salsa", + "flamenco": "Flamenko", + "blues": "Blues", + "music_for_children": "Otroška glasba", + "new-age_music": "New Age glasba", + "vocal_music": "Vokalna glasba", + "a_capella": "A Capella", + "music_of_africa": "Afriška glasba", + "afrobeat": "Afrobeat", + "christian_music": "Krščanska glasba", + "gospel_music": "Gospel glasba", + "music_of_asia": "Azijska glasba", + "carnatic_music": "Karnatska glasba", + "music_of_bollywood": "Bollywoodska glasba", + "ska": "Ska", + "traditional_music": "Tradicionalna glasba", + "independent_music": "Neodvisna glasba", + "song": "Pesem", + "background_music": "Glasba v ozadju", + "theme_music": "Naslovna glasba", + "jingle": "Džingl", + "soundtrack_music": "Filmska glasba", + "lullaby": "Uspavanka", + "video_game_music": "Glasba iz videoiger", + "christmas_music": "Božična glasba", + "dance_music": "Plesna glasba", + "wedding_music": "Poročna glasba", + "happy_music": "Vesela glasba", + "sad_music": "Žalostna glasba", + "tender_music": "Nežna glasba", + "exciting_music": "Navdušujoča glasba", + "angry_music": "Jezična glasba", + "scary_music": "Strašljiva glasba", + "wind": "Veter", + "rustling_leaves": "Šuštenje listja", + "wind_noise": "Šum vetra", + "thunderstorm": "Nevihta", + "thunder": "Grom", + "water": "Voda", + "rain": "Dež", + "raindrop": "Dežna kaplja", + "rain_on_surface": "Dež na površini", + "stream": "Potok", + "waterfall": "Slap", + "ocean": "Ocean", + "waves": "Valovi", + "steam": "Para", + "gurgling": "Grgranje vode", + "fire": "Ogenj", + "crackle": "Prasketanje", + "sailboat": "Jadrnica", + "rowboat": "Čoln na vesla", + "motorboat": "Motorna žaga", + "ship": "Ladja", + "motor_vehicle": "Motorno vozilo", + "toot": "Trobljenje", + "car_alarm": "Avtomobilski alarm", + "power_windows": "Električni pomik stekel", + "skidding": "Zanašanje", + "tire_squeal": "Cviljenje gum", + "car_passing_by": "Avto pelje mimo", + "race_car": "Dirkalnik", + "truck": "Tovornjak", + "air_brake": "Zračna zavora", + "air_horn": "Zračna hupa", + "reversing_beeps": "Piskač za vzvratno vožnjo", + "ice_cream_truck": "Kombi s sladoledom", + "emergency_vehicle": "Intervencijsko vozilo", + "police_car": "Policijski avto", + "ambulance": "Rešilec", + "fire_engine": "Gasilski avto", + "traffic_noise": "Prometni hrup", + "rail_transport": "Železniški promet", + "train_whistle": "Piščal vlaka", + "train_horn": "Hupa vlaka", + "railroad_car": "Vagon", + "train_wheels_squealing": "Cviljenje koles vlaka", + "subway": "Podzemna železnica", + "aircraft": "Zrakoplov", + "aircraft_engine": "Letalski motor", + "jet_engine": "Reaktivni motor", + "propeller": "Propeler", + "helicopter": "Helikopter", + "fixed-wing_aircraft": "Letalo s fiksnimi krili", + "engine": "Motor (stroj)", + "light_engine": "Lahki motor", + "dental_drill's_drill": "Zobozdravniški vrtalnik", + "lawn_mower": "Kosilnica", + "chainsaw": "Motorna žaga", + "medium_engine": "Srednji motor", + "heavy_engine": "Težki motor", + "engine_knocking": "Klenkanje motorja", + "engine_starting": "Zagon motorja", + "idling": "Tek v prostem teku", + "accelerating": "Pospeševanje", + "doorbell": "Zvonec pri vratih", + "ding-dong": "Ding-dong", + "sliding_door": "Drsna vrata", + "slam": "Zaloputniti", + "knock": "Trkanje", + "tap": "Potrkati", + "squeak": "Cviljenje", + "cupboard_open_or_close": "Odpiranje/zapiranje omare", + "drawer_open_or_close": "Odpiranje/zapiranje predala", + "dishes": "Posoda", + "cutlery": "Pribor", + "chopping": "Sekanje", + "frying": "Cvrtje", + "microwave_oven": "Mikrovalovka", + "water_tap": "Pipa", + "bathtub": "Kopalna kad", + "toilet_flush": "Izplakovanje stranišča", + "electric_toothbrush": "Električna zobna ščetka", + "vacuum_cleaner": "Sesalnik", + "zipper": "Zadrga", + "keys_jangling": "Žvenketanje ključev", + "coin": "Kovanec", + "electric_shaver": "Električni brivnik", + "shuffling_cards": "Mešanje kart", + "typing": "Tipkanje", + "typewriter": "Pisalni stroj", + "computer_keyboard": "Računalniška tipkovnica", + "writing": "Pisanje", + "alarm": "Alarm", + "telephone": "Telefon", + "telephone_bell_ringing": "Zvonjenje telefona", + "ringtone": "Melodija zvonjenja", + "telephone_dialing": "Tipkanje številke", + "dial_tone": "Ton za klicanje", + "busy_signal": "Zasedeno", + "alarm_clock": "Budilka", + "siren": "Sirena", + "civil_defense_siren": "Sirena za javno alarmiranje", + "buzzer": "Zunčalo", + "smoke_detector": "Detektor dima", + "fire_alarm": "Požarni alarm", + "foghorn": "Ladijska hupa za meglo", + "whistle": "Piščalka", + "steam_whistle": "Parna piščal", + "mechanisms": "Mehanizmi", + "ratchet": "Zaskočnik", + "tick": "Tik", + "tick-tock": "Tik-tak", + "gears": "Zobniki", + "pulleys": "Škripci", + "sewing_machine": "Šivalni stroj", + "mechanical_fan": "Ventilator", + "air_conditioning": "Klima", + "cash_register": "Blagajna", + "printer": "Tiskalnik", + "single-lens_reflex_camera": "Zrcalnorefleksni fotoaparat", + "tools": "Orodja", + "hammer": "Kladivo", + "jackhammer": "Pnevmatsko kladivo", + "sawing": "Žaganje", + "filing": "Piljenje", + "sanding": "Brušenje", + "power_tool": "Električno orodje", + "drill": "Vrtalnik", + "explosion": "Eksplozija", + "gunshot": "Strel", + "machine_gun": "Mitraljez", + "fusillade": "Streljanje", + "artillery_fire": "Artilerijsko obstreljevanje", + "cap_gun": "Otroška pištola na kapice", + "fireworks": "Ognjemet", + "firecracker": "Petarda", + "burst": "Pok", + "eruption": "Izbruh", + "boom": "Bum", + "wood": "Les", + "chop": "Sekati", + "splinter": "Iver", + "crack": "Pokanje", + "glass": "Steklo", + "chink": "Zvenket", + "shatter": "Razbitje", + "silence": "Tišina", + "sound_effect": "Zvočni učinek", + "environmental_noise": "Hrup iz okolja", + "static": "Šum", + "white_noise": "Beli šum", + "pink_noise": "Rožnati šum", + "field_recording": "Posnetek s terena", + "scream": "Krik", + "sodeling": "Jodlanje", + "chird": "Čivkanje", + "change_ringing": "Zvonjenje zvonov", + "shofar": "Šofar", + "liquid": "Tekočina", + "splash": "Pljusk", + "slosh": "Pretakanje", + "squish": "Mljask", + "drip": "Kapljanje", + "pour": "Točenje", + "trickle": "Curjanje", + "gush": "Bruhanje (voda)", + "fill": "Polnjenje", + "spray": "Pršenje", + "pump": "Črpanje", + "stir": "Mešanje", + "boiling": "Vretje", + "sonar": "Sonar", + "arrow": "Puščica", + "whoosh": "Švist", + "thump": "Udarec", + "thunk": "Top udarec", + "electronic_tuner": "Elektronski uglaševalec", + "effects_unit": "Enota za efekte", + "chorus_effect": "Chorus efekt", + "basketball_bounce": "Odboj košarkarske žoge", + "bang": "Pok", + "slap": "Ploska", + "whack": "Udarec", + "smash": "Razbitje", + "breaking": "Lomljenje", + "bouncing": "Odskakovanje", + "whip": "Bič", + "flap": "Plapolanje", + "scratch": "Praska", + "scrape": "Praskanje", + "rub": "Drgnjenje", + "roll": "Kotaljenje", + "crushing": "Mečkanje", + "crumpling": "Mečkanje papirja", + "tearing": "Trganje", + "beep": "Pisk", + "ping": "Ping", + "ding": "Ding", + "clang": "Zven", + "squeal": "Cviljenje", + "creak": "Škripanje", + "rustle": "Šuštenje", + "whir": "Brenčanje", + "clatter": "Ropotanje", + "sizzle": "Cvrčanje", + "clicking": "Klikanje", + "clickety_clack": "Klak-klak", + "rumble": "Grmenje", + "plop": "Pljusk", + "hum": "Brenčanje", + "zing": "Zing", + "boing": "Boing", + "crunch": "Hrustanje", + "sine_wave": "Sinusni val", + "harmonic": "Harmonik", + "chirp_tone": "Čivkajoč ton", + "pulse": "Pulz", + "inside": "Znotraj", + "outside": "Zunaj", + "reverberation": "Odmev (reverb)", + "echo": "Eho", + "noise": "Hrup", + "mains_hum": "Omrežni brum", + "distortion": "Popačenje", + "sidetone": "Stranski ton", + "cacophony": "Kakofonija", + "throbbing": "Utripanje", + "vibration": "Vibracija" +} diff --git a/web/public/locales/sl/common.json b/web/public/locales/sl/common.json new file mode 100644 index 0000000..aa913de --- /dev/null +++ b/web/public/locales/sl/common.json @@ -0,0 +1,327 @@ +{ + "time": { + "untilForTime": "Do {{time}}", + "untilRestart": "Do ponovnega zagona", + "ago": "pred {{timeAgo}}", + "justNow": "Ravnokar", + "untilForRestart": "Dokler se Frigate ne ponovno zažene.", + "thisWeek": "Ta teden", + "lastWeek": "Prejšnji teden", + "thisMonth": "Ta mesec", + "year_one": "{{time}} leto", + "year_two": "", + "year_few": "", + "year_other": "{{time}} let", + "second_one": "{{time}} sekunda", + "second_two": "", + "second_few": "", + "second_other": "{{time}} sekund", + "month_one": "{{time}} mesec", + "month_two": "", + "month_few": "", + "month_other": "{{time}} mesecev", + "day_one": "{{time}} dan", + "day_two": "", + "day_few": "", + "day_other": "{{time}} dni", + "hour_one": "{{time}} ura", + "hour_two": "", + "hour_few": "", + "hour_other": "{{time}} ur", + "minute_one": "{{time}} minuta", + "minute_two": "", + "minute_few": "", + "minute_other": "{{time}} minut", + "10minutes": "10 minut", + "lastMonth": "Prejšnji mesec", + "5minutes": "5 minut", + "today": "Danes", + "yesterday": "Včeraj", + "last7": "Zadnjih 7 dni", + "last14": "Zadnjih 14 dni", + "last30": "Zadnjih 30 dni", + "1hour": "1 ura", + "12hours": "12 ur", + "24hours": "24 ur", + "30minutes": "30 minut", + "am": "dop.", + "pm": "pop.", + "mo": "{{time}} m", + "d": "{{time}} d", + "h": "{{time}} u", + "m": "{{time}} min", + "s": "{{time}} s", + "yr": "{{time}} l", + "formattedTimestamp": { + "12hour": "d. MMM, h:mm:ss aaa", + "24hour": "d. MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "d. MM. h:mm:ssa", + "24hour": "d. MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d. MMM, h:mm aaa", + "24hour": "d. MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d. MMM yyyy", + "24hour": "d. MMM yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d. MMM yyyy, h:mm aaa", + "24hour": "d. MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d. MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "invalidStartTime": "Neveljaven čas začetka", + "invalidEndTime": "Neveljaven čas konca", + "inProgress": "V teku", + "never": "Nikoli" + }, + "menu": { + "live": { + "cameras": { + "count_one": "{{count}} kamera", + "count_two": "", + "count_few": "", + "count_other": "{{count}} kamer", + "title": "Kamere" + }, + "allCameras": "Vse kamere", + "title": "V živo" + }, + "explore": "Razišči", + "theme": { + "nord": "Nord", + "label": "Tema", + "blue": "Modra", + "green": "Zelena", + "red": "Rdeča", + "highcontrast": "Visok kontrast", + "default": "Privzeta" + }, + "review": "Pregled", + "system": "Sistem", + "systemMetrics": "Sistemske metrike", + "configuration": "Konfiguracija", + "systemLogs": "Sistemski dnevniki", + "settings": "Nastavitve", + "configurationEditor": "Urejevalnik konfiguracije", + "languages": "Jeziki", + "language": { + "en": "Angleščina (English)", + "es": "Španščina (Español)", + "zhCN": "Kitajščina (简体中文)", + "hi": "Hindi (हिन्दी)", + "fr": "Francoščina (Français)", + "ar": "Arabščina (العربية)", + "pt": "Portugalščina (Português)", + "ru": "Ruščina (Русский)", + "de": "Nemščina (Deutsch)", + "ja": "Japonščina (日本語)", + "tr": "Turščina (Türkçe)", + "it": "Italijanščina (Italiano)", + "nl": "Nizozemščina (Nederlands)", + "sv": "Švedščina (Svenska)", + "cs": "Češčina (Čeština)", + "nb": "Norveščina (Norsk Bokmål)", + "ko": "Korejščina (한국어)", + "vi": "Vietnamščina (Tiếng Việt)", + "fa": "Perzijščina (فارسی)", + "pl": "Poljščina (Polski)", + "uk": "Ukrajinščina (Українська)", + "he": "Hebrejščina (עברית)", + "el": "Grščina (Ελληνικά)", + "ro": "Romunščina (Română)", + "hu": "Madžarščina (Magyar)", + "fi": "Finščina (Suomi)", + "da": "Danščina (Dansk)", + "sk": "Slovaščina (Slovenčina)", + "yue": "Kantonščina (粵語)", + "th": "Tajščina (ไทย)", + "sr": "Srbska (Српски)", + "sl": "Slovenščina", + "bg": "Bolgarščina (Български)", + "withSystem": { + "label": "Uporabi sistemske nastavitve za jezik" + }, + "ptBR": "Brazilsko portugalsko (Português brasileiro)", + "ca": "Katalonščina (Català)", + "lt": "Litovščina (Lietuvių)", + "gl": "Galicijščina (Galego)", + "id": "Indonezijščina (Bahasa Indonesia)", + "ur": "Urdu (اردو)", + "hr": "Hrvaščina (Hrvatski)" + }, + "appearance": "Videz", + "darkMode": { + "label": "Temni način", + "light": "Svetel", + "dark": "Temen", + "withSystem": { + "label": "Uporabi sistemske nastavitve za svetel ali temen način" + } + }, + "withSystem": "Sistem", + "help": "Pomoč", + "documentation": { + "title": "Dokumentacija", + "label": "Frigate dokumentacija" + }, + "restart": "Ponovno zaženi Frigate", + "export": "Izvoz", + "faceLibrary": "Knjižnica obrazov", + "user": { + "title": "Uporabnik", + "account": "Račun", + "current": "Trenutni uporabnik: {{user}}", + "anonymous": "anonimen", + "logout": "Odjava", + "setPassword": "Nastavi geslo" + }, + "uiPlayground": "UI Playground", + "classification": "Klasifikacija", + "actions": "Dejanja", + "chat": "Klepet" + }, + "button": { + "apply": "Uveljavi", + "reset": "Ponastavi", + "done": "Končano", + "disable": "Onemogoči", + "close": "Zapri", + "back": "Nazaj", + "pictureInPicture": "Slika v sliki", + "history": "Zgodovina", + "disabled": "Onemogočeno", + "copy": "Kopiraj", + "exitFullscreen": "Izhod iz celozaslonskega načina", + "enabled": "Omogočeno", + "enable": "Omogoči", + "save": "Shrani", + "saving": "Shranjujem…", + "cancel": "Prekliči", + "fullscreen": "Celozaslonski način", + "twoWayTalk": "Dvosmerni pogovor", + "cameraAudio": "Zvok kamere", + "on": "VKLOP", + "off": "IZKLOP", + "edit": "Uredi", + "copyCoordinates": "Kopiraj koordinate", + "delete": "Izbriši", + "yes": "Da", + "no": "Ne", + "download": "Prenesi", + "info": "Informacije", + "suspended": "Prekinjeno", + "unsuspended": "Nadaljuj", + "play": "Predvajaj", + "unselect": "Počisti izbiro", + "export": "Izvozi", + "deleteNow": "Izbriši zdaj", + "next": "Naprej", + "continue": "Nadaljuj", + "add": "Dodaj", + "applying": "Uveljavljam…", + "undo": "Razveljavi", + "copiedToClipboard": "Kopirano v odložišče", + "modified": "Spremenjeno", + "overridden": "Povoženo", + "resetToGlobal": "Ponastavi na globalno", + "resetToDefault": "Ponastavi na privzeto", + "saveAll": "Shrani vse", + "savingAll": "Shranjujem vse…", + "undoAll": "Razveljavi vse" + }, + "unit": { + "speed": { + "kph": "km/h", + "mph": "mph" + }, + "length": { + "feet": "čevljev", + "meters": "metrov" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/uro", + "mbph": "MB/uro", + "gbph": "GB/uro" + } + }, + "label": { + "back": "Nazaj", + "hide": "Skrij {{item}}", + "show": "Pokaži {{item}}", + "ID": "ID", + "none": "Brez", + "all": "Vse", + "other": "Ostalo" + }, + "pagination": { + "next": { + "label": "Pojdi na naslednjo stran", + "title": "Naprej" + }, + "label": "strani", + "previous": { + "title": "Nazaj", + "label": "Pojdi na prejšnjo stran" + }, + "more": "Več strani" + }, + "selectItem": "Izberi {{item}}", + "toast": { + "copyUrlToClipboard": "URL kopiran v odložišče.", + "save": { + "title": "Shrani", + "error": { + "title": "Napaka pri shranjevanju konfiguracije: {{errorMessage}}", + "noMessage": "Spremembe konfiguracije ni bilo mogoče shraniti" + } + } + }, + "role": { + "title": "Vloga", + "admin": "Administrator", + "viewer": "Gledalec", + "desc": "Administratorji imajo poln dostop do vseh funkcij. Gledalci so omejeni na ogled kamer, pregled dogodkov in zgodovinskih posnetkov." + }, + "accessDenied": { + "documentTitle": "Dostop zavrnjen - Frigate", + "title": "Dostop zavrnjen", + "desc": "Nimaš dovoljenja za ogled te strani." + }, + "notFound": { + "documentTitle": "Ni mogoče najti - Frigate", + "title": "404", + "desc": "Stran ne obstaja" + }, + "readTheDocumentation": "Preberi dokumentacijo", + "list": { + "two": "{{0}} in {{1}}", + "many": "{{items}} in {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Izbirno", + "internalID": "Notranji ID, ki ga Frigate uporablja v konfiguraciji in bazi podatkov" + }, + "information": { + "pixels": "{{area}}px" + } +} diff --git a/web/public/locales/sl/components/auth.json b/web/public/locales/sl/components/auth.json new file mode 100644 index 0000000..36cc69b --- /dev/null +++ b/web/public/locales/sl/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Uporabniško ime", + "password": "Geslo", + "login": "Prijava", + "errors": { + "usernameRequired": "Uporabniško ime je obvezno", + "passwordRequired": "Geslo je obvezno", + "rateLimit": "Preveč poskusov prijave. Poskusi znova kasneje.", + "loginFailed": "Prijava ni uspela", + "unknownError": "Neznana napaka. Preveri dnevnike (logs).", + "webUnknownError": "Neznana napaka. Preveri dnevnike v konzoli." + }, + "firstTimeLogin": "Se prijavljaš prvič? Podatke za prijavo najdeš v Frigate dnevnikih (logs)." + } +} diff --git a/web/public/locales/sl/components/camera.json b/web/public/locales/sl/components/camera.json new file mode 100644 index 0000000..2ee987b --- /dev/null +++ b/web/public/locales/sl/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Skupine kamer", + "add": "Dodaj skupino kamer", + "edit": "Uredi skupino kamer", + "delete": { + "label": "Izbriši skupino kamer", + "confirm": { + "title": "Potrdi brisanje", + "desc": "Ali si prepričan, da želiš izbrisati skupino kamer {{name}}?" + } + }, + "camera": { + "setting": { + "desc": "Spremeni možnosti prenosa v živo za nadzorno ploščo te skupine. Te nastavitve so specifične za napravo/brskalnik.", + "streamMethod": { + "method": { + "smartStreaming": { + "desc": "Pametno pretakanje posodobi sliko kamere enkrat na minuto, ko ni zaznane dejavnosti, da prihrani pasovno širino in vire. Ko je zaznana dejavnost, se slika neopazno preklopi na prenos v živo.", + "label": "Pametno pretakanje (priporočeno)" + }, + "continuousStreaming": { + "desc": { + "warning": "Neprekinjeno pretakanje lahko povzroči visoko porabo pasovne širine in težave z zmogljivostjo. Uporabljaj previdno.", + "title": "Slika kamere bo vedno v živo, ko je vidna na nadzorni plošči, tudi če ni zaznane dejavnosti." + }, + "label": "Neprekinjeno pretakanje" + }, + "noStreaming": { + "desc": "Slike kamere se bodo posodobile le enkrat na minuto, prenos v živo pa se ne bo izvajal.", + "label": "Brez pretakanja" + } + }, + "label": "Način pretakanja", + "placeholder": "Izberi način pretakanja" + }, + "audio": { + "tips": { + "title": "Za ta tok mora kamera oddajati zvok, ki mora biti konfiguriran v go2rtc.", + "document": "Preberite dokumentacijo " + } + }, + "label": "Nastavitve pretakanja kamere", + "title": "Nastavitve pretakanja za {{cameraName}}", + "audioIsAvailable": "Zvok je na voljo za ta tok", + "audioIsUnavailable": "Zvok ni na voljo za ta tok", + "compatibilityMode": { + "label": "Združljivostni način", + "desc": "To možnost omogoči le, če so v prenosu v živo vidni barvni popački ali diagonalna črta na desni strani slike." + }, + "placeholder": "Izberi tok", + "stream": "Tok" + }, + "birdseye": "Ptičja perspektiva" + }, + "name": { + "label": "Ime", + "placeholder": "Vnesi ime…", + "errorMessage": { + "mustLeastCharacters": "Ime skupine mora imeti vsaj 2 znaka.", + "exists": "Skupina s tem imenom že obstaja.", + "nameMustNotPeriod": "Ime skupine ne sme vsebovati pike.", + "invalid": "Neveljavno ime skupine kamer." + } + }, + "cameras": { + "label": "Kamere", + "desc": "Izberi kamere za to skupino." + }, + "icon": "Ikona", + "success": "Skupina kamer ({{name}}) je bila shranjena." + }, + "debug": { + "options": { + "label": "Nastavitve", + "title": "Možnosti", + "showOptions": "Pokaži možnosti", + "hideOptions": "Skrij možnosti" + }, + "boundingBox": "Okvir zaznave", + "timestamp": "Časovni žig", + "zones": "Cone", + "mask": "Maska", + "motion": "Gibanje", + "regions": "Regije" + } +} diff --git a/web/public/locales/sl/components/dialog.json b/web/public/locales/sl/components/dialog.json new file mode 100644 index 0000000..fdae7b9 --- /dev/null +++ b/web/public/locales/sl/components/dialog.json @@ -0,0 +1,130 @@ +{ + "restart": { + "title": "Ali si prepričan, da želiš ponovno zagnati Frigate?", + "button": "Ponovni zagon", + "restarting": { + "title": "Frigate se ponovno zaganja", + "content": "Stran se bo osvežila čez {{countdown}} sekund.", + "button": "Prisili osvežitev zdaj" + }, + "description": "To bo za kratek čas ustavilo delovanje programa." + }, + "explore": { + "plus": { + "review": { + "question": { + "ask_full": "Je ta predmet {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Potrdi to oznako za Frigate Plus", + "ask_a": "Je ta predmet {{label}}?", + "ask_an": "Je ta predmet {{label}}?" + }, + "state": { + "submitted": "Oddano" + } + }, + "submitToPlus": { + "label": "Pošlji v Frigate+", + "desc": "Predmeti na lokacijah, ki se jim želiš izogniti, niso lažni zadetki. Če jih pošlješ kot lažne zadetke, boš zmedli model." + } + }, + "video": { + "viewInHistory": "Poglej v zgodovini" + } + }, + "export": { + "time": { + "lastHour_one": "Zadnja ura", + "lastHour_two": "", + "lastHour_few": "", + "lastHour_other": "Zadnjih {{count}} ur", + "fromTimeline": "Izberi s časovnice", + "custom": "Po meri", + "start": { + "title": "Čas začetka", + "label": "Izberi čas začetka" + }, + "end": { + "title": "Čas konca", + "label": "Izberi čas konca" + } + }, + "name": { + "placeholder": "Poimenuj izvoz" + }, + "select": "Izberi", + "export": "Izvozi", + "selectOrExport": "Izberi ali izvozi", + "toast": { + "success": "Izvoz se je uspešno začel. Datoteko si lahko ogledaš na strani z izvozi.", + "error": { + "failed": "Napaka pri izvozu: {{error}}", + "endTimeMustAfterStartTime": "Čas konca mora biti po času začetka", + "noVaildTimeSelected": "Ni izbranega veljavnega časovnega obdobja" + }, + "view": "Poglej" + }, + "fromTimeline": { + "saveExport": "Shrani izvoz", + "previewExport": "Predogled izvoza" + }, + "case": { + "label": "Primer", + "placeholder": "Izberi primer" + } + }, + "streaming": { + "label": "Pretok", + "restreaming": { + "disabled": "Ponovno pretakanje za to kamero ni omogočeno.", + "desc": { + "title": "Nastavi go2rtc za dodatne možnosti ogleda v živo in zvok za to kamero.", + "readTheDocumentation": "Preberi dokumentacijo" + } + }, + "showStats": { + "label": "Pokaži statistiko pretoka", + "desc": "Omogoči to možnost za prikaz statistike pretoka kot prekrivno plast na sliki kamere." + }, + "debugView": "Razhroščevalni pogled" + }, + "search": { + "saveSearch": { + "label": "Shrani iskanje", + "desc": "Vnesi ime za to shranjeno iskanje.", + "placeholder": "Vnesi ime iskanja", + "overwrite": "{{searchName}} že obstaja. Shranjevanje bo povozilo obstoječo vrednost.", + "success": "Iskanje ({{searchName}}) je bilo shranjeno.", + "button": { + "save": { + "label": "Shrani to iskanje" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "Potrdi brisanje", + "desc": { + "selected": "Ali si prepričan, da želiš izbrisati vse posnetke, povezane s tem elementom pregleda?

    Drži tipko Shift, da v prihodnje preskočiš to okno." + }, + "toast": { + "success": "Posnetki, povezani z izbranimi elementi pregleda, so bili uspešno izbrisani.", + "error": "Napaka pri brisanju: {{error}}" + } + }, + "button": { + "export": "Izvozi", + "markAsReviewed": "Označi kot pregledano", + "deleteNow": "Izbriši zdaj", + "markAsUnreviewed": "Označi kot nepregledano" + } + }, + "imagePicker": { + "selectImage": "Izberi sličico sledenega objekta", + "search": { + "placeholder": "Išči po oznaki ali podoznaki..." + }, + "noImages": "Za to kamero ni bilo najdenih sličic", + "unknownLabel": "Shranjena sprožilna slika" + } +} diff --git a/web/public/locales/sl/components/filter.json b/web/public/locales/sl/components/filter.json new file mode 100644 index 0000000..408928e --- /dev/null +++ b/web/public/locales/sl/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filter", + "labels": { + "label": "Oznake", + "all": { + "title": "Vse oznake", + "short": "Oznake" + }, + "count_one": "{{count}} oznaka", + "count_other": "{{count}} oznak" + }, + "dates": { + "selectPreset": "Izberi prednastavitev…", + "all": { + "title": "Vsi datumi", + "short": "Datumi" + } + }, + "more": "Več filtrov", + "explore": { + "settings": { + "defaultView": { + "summary": "Povzetek", + "title": "Privzeti pogled", + "desc": "Ko ni izbran noben filter, prikaži povzetek najnovejših sledenih objektov po oznaki ali pa prikaži nefiltrirano mrežo.", + "unfilteredGrid": "Nefiltrirana mreža" + }, + "title": "Nastavitve", + "gridColumns": { + "title": "Stolpci mreže", + "desc": "Izberi število stolpcev v mrežnem pogledu." + }, + "searchSource": { + "label": "Vir iskanja", + "desc": "Izberi, ali želiš iskati po sličicah ali opisih sledenih objektov.", + "options": { + "thumbnailImage": "Sličica", + "description": "Opis" + } + } + }, + "date": { + "selectDateBy": { + "label": "Izberi datum za filtriranje" + } + } + }, + "subLabels": { + "all": "Vse podoznake", + "label": "Podoznake" + }, + "sort": { + "relevance": "Ustreznost", + "dateAsc": "Datum (naraščajoče)", + "label": "Razvrsti", + "dateDesc": "Datum (padajoče)", + "scoreAsc": "Ocena objekta (naraščajoče)", + "scoreDesc": "Ocena objekta (padajoče)", + "speedAsc": "Ocenjena hitrost (naraščajoče)", + "speedDesc": "Ocenjena hitrost (padajoče)" + }, + "zones": { + "label": "Cone", + "all": { + "title": "Vse cone", + "short": "Cone" + } + }, + "timeRange": "Časovno obdobje", + "reset": { + "label": "Ponastavi filtre na privzete vrednosti" + }, + "logSettings": { + "disableLogStreaming": "Onemogoči sprotno osveževanje dnevnikov", + "allLogs": "Vsi dnevniki", + "label": "Filter ravni dnevnika", + "filterBySeverity": "Filtriraj dnevnike po resnosti", + "loading": { + "title": "Nalaganje", + "desc": "Ko je področje z dnevniki pomaknjeno do dna, se novi vnosi samodejno sproti dodajajo." + } + }, + "trackedObjectDelete": { + "title": "Potrdi brisanje", + "desc": "Z brisanjem teh {{objectLength}} sledenih objektov odstraniš posnetek, vse shranjene vložitve (embeddings) in vse povezane vnose v življenjskem ciklu objekta. Posnetki teh objektov v pogledu zgodovine NE bodo izbrisani.

    Ali si prepričan, da želiš nadaljevati?

    Drži tipko Shift, da v prihodnje preskočiš to okno.", + "toast": { + "success": "Sledeni objekti so bili uspešno izbrisani.", + "error": "Napaka pri brisanju sledenih objektov: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtriraj po maski cone" + }, + "classes": { + "label": "Razredi", + "all": { + "title": "Vsi razredi" + }, + "count_one": "{{count}} razred", + "count_other": "{{count}} razredi" + }, + "score": "Ocena", + "estimatedSpeed": "Ocenjena hitrost ({{unit}})", + "features": { + "label": "Funkcije", + "hasSnapshot": "Ima posnetek (snapshot)", + "hasVideoClip": "Ima video posnetek", + "submittedToFrigatePlus": { + "label": "Poslano v Frigate+", + "tips": "Najprej moraš filtrirati sledene objekte, ki imajo posnetek.

    Sledenih objektov brez posnetka ni mogoče poslati v Frigate+." + } + }, + "cameras": { + "label": "Filter kamer", + "all": { + "title": "Vse kamere", + "short": "Kamere" + } + }, + "review": { + "showReviewed": "Pokaži pregledano" + }, + "motion": { + "showMotionOnly": "Pokaži samo gibanje" + }, + "recognizedLicensePlates": { + "title": "Prepoznane registrske tablice", + "loadFailed": "Nalaganje prepoznanih registrskih tablic ni uspelo.", + "loading": "Nalagam prepoznane registrske tablice…", + "placeholder": "Tipkaj za iskanje tablic…", + "noLicensePlatesFound": "Ni najdenih registrskih tablic.", + "selectPlatesFromList": "Izberi eno ali več tablic s seznama.", + "selectAll": "Izberi vse", + "clearAll": "Počisti vse" + }, + "attributes": { + "label": "Atributi klasifikacije", + "all": "Vsi atributi" + } +} diff --git a/web/public/locales/sl/components/icons.json b/web/public/locales/sl/components/icons.json new file mode 100644 index 0000000..db9ab33 --- /dev/null +++ b/web/public/locales/sl/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Izberi ikono", + "search": { + "placeholder": "Iskanje ikone…" + } + } +} diff --git a/web/public/locales/sl/components/input.json b/web/public/locales/sl/components/input.json new file mode 100644 index 0000000..892df5f --- /dev/null +++ b/web/public/locales/sl/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Prenesi video", + "toast": { + "success": "Prenos videa za izbrani element se je začel." + } + } + } +} diff --git a/web/public/locales/sl/components/player.json b/web/public/locales/sl/components/player.json new file mode 100644 index 0000000..636c919 --- /dev/null +++ b/web/public/locales/sl/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Za ta čas ni bilo najdenih posnetkov", + "noPreviewFound": "Predogleda ni mogoče najti", + "noPreviewFoundFor": "Za kamero {{cameraName}} ni predogleda", + "submitFrigatePlus": { + "title": "Želiš poslati ta okvir v Frigate+?", + "submit": "Pošlji" + }, + "stats": { + "streamType": { + "title": "Vrsta toka:", + "short": "Vrsta" + }, + "bandwidth": { + "title": "Pasovna širina:", + "short": "Pasovna širina" + }, + "latency": { + "value": "{{seconds}} sekund", + "title": "Zakasnitev:", + "short": { + "value": "{{seconds}} s", + "title": "Zakasnitev" + } + }, + "totalFrames": "Skupno število slik:", + "droppedFrames": { + "title": "Izpuščene slike:", + "short": { + "title": "Izpuščeno", + "value": "{{droppedFrames}} slik" + } + }, + "decodedFrames": "Dekodirane slike:", + "droppedFrameRate": "Stopnja izpuščenih slik:" + }, + "livePlayerRequiredIOSVersion": "Za to vrsto prenosa v živo potrebuješ iOS 17.1 ali novejši.", + "streamOffline": { + "title": "Tok ni povezan", + "desc": "Pretok detect za kamero {{cameraName}} ne prejema slik, preveri dnevnike napak." + }, + "cameraDisabled": "Kamera je onemogočena", + "toast": { + "success": { + "submittedFrigatePlus": "Okvir je bil uspešno poslan v Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Napaka pri pošiljanju okvirja v Frigate+" + } + } +} diff --git a/web/public/locales/sl/config/cameras.json b/web/public/locales/sl/config/cameras.json new file mode 100644 index 0000000..f022c6f --- /dev/null +++ b/web/public/locales/sl/config/cameras.json @@ -0,0 +1,941 @@ +{ + "label": "Konfiguracija kamere", + "name": { + "label": "Ime kamere", + "description": "Ime kamere je obvezno" + }, + "friendly_name": { + "label": "Prijazno ime", + "description": "Prijazno ime kamere, ki se uporablja v vmesniku Frigate" + }, + "enabled": { + "label": "Omogočeno", + "description": "Omogočeno" + }, + "audio": { + "label": "Avdio dogodki", + "description": "Nastavitve za zaznavanje dogodkov na podlagi zvoka za to kamero.", + "enabled": { + "label": "Omogoči zaznavanje zvoka", + "description": "Omogoči ali onemogoči zaznavanje zvočnih dogodkov za to kamero." + }, + "max_not_heard": { + "label": "Časovna omejitev konca", + "description": "Število sekund brez nastavljenega tipa zvoka, preden se avdio dogodek zaključi." + }, + "min_volume": { + "label": "Najmanjša glasnost", + "description": "Najnižji prag glasnosti RMS za zagon zaznavanja; nižje vrednosti povečajo občutljivost (npr. 200 visoka, 500 srednja, 1000 nizka)." + }, + "listen": { + "label": "Tipi zvokov za poslušanje", + "description": "Seznam tipov avdio dogodkov za zaznavanje (npr. lajež, požarni alarm, krik, govor, vpitje)." + }, + "filters": { + "label": "Avdio filtri", + "description": "Nastavitve filtrov za posamezne tipe zvoka (npr. pragovi zaupanja) za zmanjšanje lažnih pozitivnih rezultatov." + }, + "enabled_in_config": { + "label": "Prvotno stanje zvoka", + "description": "Pove, ali je bilo zaznavanje zvoka prvotno omogočeno v statični konfiguracijski datoteki." + }, + "num_threads": { + "label": "Niti za zaznavanje", + "description": "Število niti za obdelavo zaznavanja zvoka." + } + }, + "audio_transcription": { + "label": "Transkripcija zvoka", + "description": "Nastavitve za transkripcijo zvoka v živo in govora, ki se uporablja za dogodke in podnapise v živo.", + "enabled": { + "label": "Omogoči transkripcijo", + "description": "Omogoči ali onemogoči ročno sproženo transkripcijo zvočnih dogodkov." + }, + "enabled_in_config": { + "label": "Prvotno stanje transkripcije" + }, + "live_enabled": { + "label": "Transkripcija v živo", + "description": "Omogoči sprotno transkripcijo zvoka ob prejemu." + } + }, + "birdseye": { + "label": "Birdseye (Ptičja perspektiva)", + "description": "Nastavitve za sestavljen pogled Birdseye, ki združi več virov kamer v eno postavitev.", + "enabled": { + "label": "Omogoči Birdseye", + "description": "Vklopi ali izklopi funkcijo Birdseye." + }, + "mode": { + "label": "Način sledenja", + "description": "Način vključitve kamer v Birdseye: 'objects' (objekti), 'motion' (gibanje) ali 'continuous' (neprekinjeno)." + }, + "order": { + "label": "Položaj", + "description": "Številčna vrednost, ki določa vrstni red kamere v postavitvi Birdseye." + } + }, + "detect": { + "label": "Zaznavanje objektov", + "description": "Nastavitve za vlogo zaznavanja, ki se uporablja za iskanje objektov in inicializacijo sledilnikov.", + "enabled": { + "label": "Zaznavanje omogočeno", + "description": "Omogoči ali onemogoči zaznavanje objektov za to kamero. Zaznavanje mora biti omogočeno, da sledenje objektom deluje." + }, + "height": { + "label": "Višina zaznavanja", + "description": "Višina (v pikslih) slik za tok zaznavanja; pusti prazno za uporabo izvorne ločljivosti." + }, + "width": { + "label": "Širina zaznavanja", + "description": "Širina (v pikslih) slik za tok zaznavanja; pusti prazno za uporabo izvorne ločljivosti." + }, + "fps": { + "label": "FPS zaznavanja", + "description": "Želeno število slik na sekundo za zaznavanje; nižje vrednosti zmanjšajo porabo procesorja (priporočeno je 5)." + }, + "min_initialized": { + "label": "Najmanj slik za inicializacijo", + "description": "Število zaporednih zaznav, potrebnih pred ustvarjanjem sledenega objekta. Povečaj za manj lažnih zaznav." + }, + "max_disappeared": { + "label": "Največ slik ob izginotju", + "description": "Število slik brez zaznave, preden se sledeni objekt šteje za izginulega." + }, + "stationary": { + "label": "Konfiguracija nepremičnih objektov", + "description": "Nastavitve za zaznavanje in upravljanje objektov, ki nekaj časa ostanejo na mestu.", + "interval": { + "label": "Interval nepremičnosti", + "description": "Kako pogosto (v slikah) naj se preveri prisotnost nepremičnega objekta." + }, + "threshold": { + "label": "Prag nepremičnosti", + "description": "Število slik brez spremembe položaja, potrebnih, da se objekt označi za nepremičnega." + }, + "max_frames": { + "label": "Največ slik", + "description": "Omejuje, kako dolgo se sledi nepremičnim objektom, preden se zavržejo.", + "default": { + "label": "Privzeto največ slik", + "description": "Privzeto največje število slik za sledenje nepremičnemu objektu." + }, + "objects": { + "label": "Največ slik za objekt", + "description": "Posebne omejitve za posamezne tipe objektov." + } + }, + "classifier": { + "label": "Omogoči vizualni klasifikator", + "description": "Uporabi vizualni klasifikator za potrditev nepremičnih objektov, tudi če se okvirji rahlo premikajo." + } + }, + "annotation_offset": { + "label": "Odmik anotacij", + "description": "Število milisekund za premik oznak zaznavanja, da se bolje ujemajo s posnetki; lahko je pozitivno ali negativno." + } + }, + "face_recognition": { + "label": "Prepoznava obrazov", + "description": "Nastavitve za zaznavanje in prepoznavanje obrazov za to kamero.", + "enabled": { + "label": "Omogoči prepoznavo obrazov", + "description": "Omogoči ali onemogoči prepoznavanje obrazov." + }, + "min_area": { + "label": "Najmanjša površina obraza", + "description": "Najmanjša površina (v pikslih) okvirja obraza, potrebna za poskus prepoznave." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "Nastavitve FFmpeg, vključno s potjo do binarne datoteke, argumenti, možnostmi strojnega pospeševanja in argumenti izhoda po vlogah.", + "path": { + "label": "Pot do FFmpeg", + "description": "Pot do binarne datoteke FFmpeg, ki naj se uporabi, ali vzdevek različice (\"5.0\" ali \"7.0\")." + }, + "global_args": { + "label": "Globalni argumenti FFmpeg", + "description": "Globalni argumenti, posredovani procesom FFmpeg." + }, + "hwaccel_args": { + "label": "Argumenti strojnega pospeševanja", + "description": "Argumenti za strojno pospeševanje FFmpeg. Priporočljive so prednastavitve glede na ponudnika." + }, + "input_args": { + "label": "Vhodni argumenti", + "description": "Vhodni argumenti, uporabljeni za vhodne tokove FFmpeg." + }, + "output_args": { + "label": "Izhodni argumenti", + "description": "Privzeti izhodni argumenti, uporabljeni za različne vloge FFmpeg, kot sta zaznavanje in snemanje.", + "detect": { + "label": "Izhodni argumenti za zaznavanje", + "description": "Privzeti izhodni argumenti za tokove z vlogo zaznavanja (detect)." + }, + "record": { + "label": "Izhodni argumenti za snemanje", + "description": "Privzeti izhodni argumenti za tokove z vlogo snemanja (record)." + } + }, + "retry_interval": { + "label": "Čas ponovnega poskusa FFmpeg", + "description": "Število sekund čakanja pred ponovnim poskusom povezave s tokom kamere po napaki. Privzeto je 10." + }, + "apple_compatibility": { + "label": "Združljivost z Apple napravami", + "description": "Omogoči označevanje HEVC za boljšo združljivost z Applovimi predvajalniki pri snemanju v H.265." + }, + "gpu": { + "label": "Indeks GPU", + "description": "Privzeti indeks grafične kartice (GPU), uporabljen za strojno pospeševanje, če je na voljo." + }, + "inputs": { + "label": "Vhodi kamere", + "description": "Seznam definicij vhodnih tokov (poti in vloge) za to kamero.", + "path": { + "label": "Vhodna pot", + "description": "URL ali pot do vhodnega toka kamere." + }, + "roles": { + "label": "Vloge vhoda", + "description": "Vloge za ta vhodni tok." + }, + "global_args": { + "label": "Globalni argumenti FFmpeg", + "description": "Globalni argumenti FFmpeg za ta vhodni tok." + }, + "hwaccel_args": { + "label": "Argumenti strojnega pospeševanja", + "description": "Argumenti strojnega pospeševanja za ta vhodni tok." + }, + "input_args": { + "label": "Vhodni argumenti", + "description": "Vhodni argumenti, specifični za ta tok." + } + } + }, + "live": { + "label": "Predvajanje v živo", + "description": "Nastavitve, ki jih uporablja spletni vmesnik za nadzor izbire toka v živo, ločljivosti in kakovosti.", + "streams": { + "label": "Imena tokov v živo", + "description": "Preslikava konfiguriranih imen tokov v imena restream/go2rtc, uporabljena za predvajanje v živo." + }, + "height": { + "label": "Višina v živo", + "description": "Višina (v pikslih) za upodabljanje jsmpeg toka v živo v spletnem vmesniku; mora biti <= višini toka za zaznavanje." + }, + "quality": { + "label": "Kakovost v živo", + "description": "Kakovost kodiranja za jsmpeg tok (1 najvišja, 31 najnižja)." + } + }, + "lpr": { + "label": "Prepoznava registrskih tablic (LPR)", + "description": "Nastavitve prepoznave registrskih tablic, vključno s pragi detekcije, formatiranjem in znanimi tablicami.", + "enabled": { + "label": "Omogoči LPR", + "description": "Omogoči ali onemogoči LPR na tej kameri." + }, + "expire_time": { + "label": "Sekunde do poteka", + "description": "Čas v sekundah, po katerem nevidna tablica poteče iz sledilnika (samo za namenske LPR kamere)." + }, + "min_area": { + "label": "Najmanjša površina tablice", + "description": "Najmanjša površina tablice (v pikslih) za poskus prepoznave." + }, + "enhancement": { + "label": "Stopnja izboljšave", + "description": "Stopnja izboljšave (0-10) slik tablic pred OCR; stopnje nad 5 so priporočljive le za nočne posnetke." + } + }, + "motion": { + "label": "Zaznavanje gibanja", + "description": "Privzete nastavitve zaznavanja gibanja za to kamero.", + "enabled": { + "label": "Omogoči zaznavanje gibanja", + "description": "Omogoči ali onemogoči zaznavanje gibanja za to kamero." + }, + "threshold": { + "label": "Prag gibanja", + "description": "Prag razlike v pikslih, ki ga uporablja detektor gibanja; višje vrednosti zmanjšajo občutljivost (razpon 1-255)." + }, + "lightning_threshold": { + "label": "Prag za strele", + "description": "Prag za zaznavanje in ignoriranje kratkih svetlobnih skokov (nižje je bolj občutljivo). To ne prepreči snemanja, le ustavi analizo dodatnih okvirjev ob blisku." + }, + "skip_motion_threshold": { + "label": "Prag za preskok gibanja", + "description": "Če se v enem okvirju spremeni večji delež slike od tega, detektor ne bo vrnil okvirjev gibanja in se bo takoj umeril. To zmanjša lažne pozitivne rezultate med nevihtami. Razpon 0.0 do 1.0." + }, + "improve_contrast": { + "label": "Izboljšaj kontrast", + "description": "Uporabi izboljšavo kontrasta na okvirjih pred analizo gibanja za boljšo detekcijo." + }, + "contour_area": { + "label": "Površina konture", + "description": "Najmanjša površina konture v pikslih, potrebna, da se gibanje upošteva." + }, + "delta_alpha": { + "label": "Delta alfa", + "description": "Faktor alfa mešanja, uporabljen pri razlikovanju okvirjev za izračun gibanja." + }, + "frame_alpha": { + "label": "Alfa okvirja", + "description": "Vrednost alfa, uporabljena pri mešanju okvirjev za predobdelavo gibanja." + }, + "frame_height": { + "label": "Višina okvirja", + "description": "Višina v pikslih, na katero se spremeni velikost okvirja pri izračunu gibanja." + }, + "mask": { + "label": "Koordinate maske", + "description": "Urejene koordinate x,y, ki določajo poligon maske gibanja za vključitev ali izključitev območij." + }, + "mqtt_off_delay": { + "label": "Zakasnitev izklopa MQTT", + "description": "Število sekund čakanja po zadnjem zaznanem gibanju pred objavo stanja 'off' na MQTT." + }, + "enabled_in_config": { + "label": "Prvotno stanje gibanja", + "description": "Pove, ali je bilo zaznavanje gibanja omogočeno v prvotni statični konfiguraciji." + }, + "raw_mask": { + "label": "Surova maska" + } + }, + "objects": { + "label": "Objekti", + "description": "Privzete nastavitve sledenja objektom, vključno s tem, katere oznake naj se sledijo, in filtri za posamezne objekte.", + "track": { + "label": "Objekti za sledenje", + "description": "Seznam oznak objektov za sledenje na tej kameri." + }, + "filters": { + "label": "Filtri objektov", + "description": "Filtri za zaznane objekte za zmanjšanje lažnih pozitivnih rezultatov (površina, razmerje, zaupanje).", + "min_area": { + "label": "Najmanjša površina objekta", + "description": "Najmanjša površina okvirja (v pikslih ali odstotkih), potrebna za to vrsto objekta." + }, + "max_area": { + "label": "Največja površina objekta", + "description": "Največja dovoljena površina okvirja za to vrsto objekta." + }, + "min_ratio": { + "label": "Najmanjše razmerje stranic", + "description": "Najmanjše razmerje širina/višina, potrebno za veljavnost okvirja." + }, + "max_ratio": { + "label": "Največje razmerje stranic", + "description": "Največje dovoljeno razmerje širina/višina." + }, + "threshold": { + "label": "Prag zaupanja", + "description": "Povprečni prag zaupanja detekcije, potreben, da se objekt šteje za pravega." + }, + "min_score": { + "label": "Najmanjše zaupanje", + "description": "Najmanjše zaupanje detekcije v enem okvirju, da se objekt upošteva." + }, + "mask": { + "label": "Maska filtra", + "description": "Koordinate poligona, ki določajo, kje znotraj okvirja velja ta filter." + }, + "raw_mask": { + "label": "Surova maska" + } + }, + "mask": { + "label": "Maska objekta", + "description": "Poligon maske, uporabljen za preprečevanje zaznavanja objektov na določenih območij." + }, + "raw_mask": { + "label": "Surova maska" + }, + "genai": { + "label": "GenAI konfiguracija objektov", + "description": "GenAI možnosti za opisovanje sledenih objektov in pošiljanje okvirjev za generiranje.", + "enabled": { + "label": "Omogoči GenAI", + "description": "Privzeto omogoči GenAI generiranje opisov za sledene objekte." + }, + "use_snapshot": { + "label": "Uporabi posnetke", + "description": "Za generiranje GenAI opisov uporabi posnetke (snapshots) namesto sličic (thumbnails)." + }, + "prompt": { + "label": "Navodilo za opis (Prompt)", + "description": "Privzeta predloga navodila, uporabljena pri generiranju opisov z GenAI." + }, + "object_prompts": { + "label": "Navodila za specifične objekte", + "description": "Navodila po meri za specifične oznake objektov." + }, + "objects": { + "label": "GenAI objekti", + "description": "Seznam oznak objektov, ki naj se privzeto pošiljajo GenAI-ju." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Območja, v katera mora objekt vstopiti, da je primeren za GenAI opis." + }, + "debug_save_thumbnails": { + "label": "Shrani sličice", + "description": "Shrani sličice, poslane GenAI-ju, za namene razhroščevanja in pregleda." + }, + "send_triggers": { + "label": "GenAI sprožilci", + "description": "Določa, kdaj naj se okvirji pošljejo GenAI-ju (ob koncu, po posodobitvah itd.).", + "tracked_object_end": { + "label": "Pošlji ob koncu", + "description": "Pošlji zahtevo GenAI-ju, ko se sledenje objektu konča." + }, + "after_significant_updates": { + "label": "Zgodnji GenAI sprožilec", + "description": "Pošlji zahtevo GenAI-ju po določenem številu pomembnih posodobitev sledenega objekta." + } + }, + "enabled_in_config": { + "label": "Prvotno GenAI stanje", + "description": "Pove, ali je bil GenAI omogočen v prvotni statični konfiguraciji." + } + } + }, + "record": { + "label": "Snemanje", + "description": "Nastavitve snemanja in hrambe za to kamero.", + "enabled": { + "label": "Omogoči snemanje", + "description": "Omogoči ali onemogoči snemanje za to kamero." + }, + "expire_interval": { + "label": "Interval čiščenja posnetkov", + "description": "Minute med cikli čiščenja, ki odstranijo potekle segmente snemanja." + }, + "continuous": { + "label": "Neprekinjena hramba", + "description": "Število dni hrambe posnetkov ne glede na objekte ali gibanje. Nastavi na 0, če želiš hraniti le opozorila in detekcije.", + "days": { + "label": "Dni hrambe", + "description": "Število dni za hrambo posnetkov." + } + }, + "motion": { + "label": "Hramba ob gibanju", + "description": "Število dni hrambe posnetkov, ki jih sproži gibanje, ne glede na objekte.", + "days": { + "label": "Dni hrambe", + "description": "Število dni za hrambo posnetkov." + } + }, + "detections": { + "label": "Hramba detekcij", + "description": "Nastavitve hrambe za dogodke zaznavanja, vključno s trajanjem pred in po zajemu.", + "pre_capture": { + "label": "Sekunde pred zajemom", + "description": "Število sekund pred dogodkom zaznavanja, ki se vključi v posnetek." + }, + "post_capture": { + "label": "Sekunde po zajemu", + "description": "Število sekund po dogodku zaznavanja, ki se vključi v posnetek." + }, + "retain": { + "label": "Hramba dogodkov", + "description": "Nastavitve hrambe za posnetke dogodkov zaznavanja.", + "days": { + "label": "Dni hrambe", + "description": "Število dni za hrambo posnetkov dogodkov zaznavanja." + }, + "mode": { + "label": "Način hrambe", + "description": "Način hrambe: all (vsi segmenti), motion (segmenti z gibanjem) ali active_objects (segmenti z aktivnimi objekti)." + } + } + }, + "alerts": { + "label": "Hramba opozoril", + "description": "Nastavitve hrambe za dogodke opozoril.", + "pre_capture": { + "label": "Sekunde pred zajemom", + "description": "Število sekund pred dogodkom opozorila, ki se vključi v posnetek." + }, + "post_capture": { + "label": "Sekunde po zajemu", + "description": "Število sekund po dogodku opozorila, ki se vključi v posnetek." + }, + "retain": { + "label": "Hramba dogodkov", + "description": "Nastavitve hrambe za posnetke dogodkov opozoril.", + "days": { + "label": "Dni hrambe", + "description": "Število dni za hrambo posnetkov dogodkov opozoril." + }, + "mode": { + "label": "Način hrambe", + "description": "Način hrambe posnetkov opozoril." + } + } + }, + "export": { + "label": "Konfiguracija izvoza", + "description": "Nastavitve za izvoz posnetkov, kot sta časovni zamik (timelapse) in strojno pospeševanje.", + "hwaccel_args": { + "label": "Argumenti strojnega pospeševanja za izvoz", + "description": "Argumenti strojnega pospeševanja za operacije izvoza in transkodiranja." + } + }, + "preview": { + "label": "Konfiguracija predogleda", + "description": "Nastavitve kakovosti predogledov snemanja v vmesniku.", + "quality": { + "label": "Kakovost predogleda", + "description": "Stopnja kakovosti predogleda (zelo nizka, nizka, srednja, visoka, zelo visoka)." + } + }, + "enabled_in_config": { + "label": "Prvotno stanje snemanja", + "description": "Pove, ali je bilo snemanje omogočeno v prvotni statični konfiguraciji." + } + }, + "review": { + "label": "Pregled", + "description": "Nastavitve, ki nadzorujejo opozorila, zaznave in GenAI povzetke pregledov za to kamero.", + "alerts": { + "label": "Konfiguracija opozoril", + "description": "Nastavitve za objekte, ki sprožijo opozorila, in njihovo hrambo.", + "enabled": { + "label": "Omogoči opozorila", + "description": "Omogoči ali onemogoči generiranje opozoril za to kamero." + }, + "labels": { + "label": "Oznake opozoril", + "description": "Seznam oznak objektov, ki štejejo kot opozorila (npr. avto, oseba)." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Območja, v katera mora objekt vstopiti, da postane opozorilo; pusti prazno za katerokoli območje." + }, + "enabled_in_config": { + "label": "Prvotno stanje opozoril", + "description": "Sledi, ali so bila opozorila prvotno omogočena." + }, + "cutoff_time": { + "label": "Čas zaključka opozorila", + "description": "Število sekund čakanja po končani aktivnosti, preden se opozorilo zaključi." + } + }, + "detections": { + "label": "Konfiguracija detekcij", + "description": "Nastavitve za ustvarjanje dogodkov zaznavanja (ki niso opozorila) in čas hrambe.", + "enabled": { + "label": "Omogoči detekcije", + "description": "Omogoči ali onemogoči dogodke zaznav za to kamero." + }, + "labels": { + "label": "Oznake detekcij", + "description": "Seznam oznak objektov, ki štejejo kot dogodki zaznavanja." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Območja, v katera mora objekt vstopiti, da postane detekcija." + }, + "cutoff_time": { + "label": "Čas zaključka detekcije", + "description": "Število sekund čakanja po končani aktivnosti, preden se detekcija zaključi." + }, + "enabled_in_config": { + "label": "Prvotno stanje detekcij", + "description": "Sledi, ali so bile detekcije prvotno omogočene." + } + }, + "genai": { + "label": "GenAI konfiguracija", + "description": "Nadzira uporabo generativne UI za izdelavo opisov in povzetkov postavk pregleda.", + "enabled": { + "label": "Omogoči GenAI opise", + "description": "Omogoči ali onemogoči GenAI opise in povzetke za postavke pregleda." + }, + "alerts": { + "label": "Omogoči GenAI za opozorila", + "description": "Uporabi GenAI za generiranje opisov opozoril." + }, + "detections": { + "label": "Omogoči GenAI za detekcije", + "description": "Uporabi GenAI za generiranje opisov detekcij." + }, + "image_source": { + "label": "Vir slik za pregled", + "description": "Vir slik za GenAI ('preview' ali 'recordings'); 'recordings' nudi višjo kakovost, a porabi več žetonov." + }, + "additional_concerns": { + "label": "Dodatni pomisleki", + "description": "Seznam dodatnih navodil ali opomb, ki naj jih GenAI upošteva pri ocenjevanju aktivnosti." + }, + "debug_save_thumbnails": { + "label": "Shrani sličice", + "description": "Shrani sličice, poslane ponudniku GenAI, za namene razhroščevanja." + }, + "enabled_in_config": { + "label": "Prvotno GenAI stanje", + "description": "Sledi, ali je bil GenAI pregled prvotno omogočen." + }, + "preferred_language": { + "label": "Prednostni jezik", + "description": "Jezik, ki se zahteva od ponudnika GenAI za generirane odgovore." + }, + "activity_context_prompt": { + "label": "Navodilo za kontekst aktivnosti", + "description": "Navodilo po meri, ki opisuje, kaj je in kaj ni sumljiva aktivnost, za boljšo orientacijo GenAI-ja." + } + } + }, + "semantic_search": { + "label": "Semantično iskanje", + "description": "Nastavitve za semantično iskanje, ki gradi in poizveduje po vložitvah objektov (embeddings) za iskanje podobnih elementov.", + "triggers": { + "label": "Sprožilci", + "description": "Dejanja in kriteriji ujemanja za sprožilce semantičnega iskanja na določeni kameri.", + "friendly_name": { + "label": "Prijazno ime", + "description": "Izbirno ime, ki se prikaže v vmesniku za ta sprožilec." + }, + "enabled": { + "label": "Omogoči ta sprožilec", + "description": "Vklopi ali izklopi ta sprožilec semantičnega iskanja." + }, + "type": { + "label": "Vrsta sprožilca", + "description": "Vrsta sprožilca: 'thumbnail' (ujemanje s sliko) ali 'description' (ujemanje z besedilom)." + }, + "data": { + "label": "Vsebina sprožilca", + "description": "Besedilna fraza ali ID sličice za primerjavo s sledenimi objekti." + }, + "threshold": { + "label": "Prag sprožilca", + "description": "Najmanjša ocena podobnosti (0-1), potrebna za aktivacijo tega sprožilca." + }, + "actions": { + "label": "Dejanja sprožilca", + "description": "Seznam dejanj ob ujemanju (obvestilo, pod-oznaka, atribut)." + } + } + }, + "snapshots": { + "label": "Posnetki (Snapshots)", + "description": "Nastavitve za shranjene JPEG posnetke sledenih objektov za to kamero.", + "enabled": { + "label": "Posnetki omogočeni", + "description": "Omogoči ali onemogoči shranjevanje posnetkov za to kamero." + }, + "clean_copy": { + "label": "Shrani čisto kopijo", + "description": "Poleg označenih shrani tudi čisto kopijo posnetkov brez anotacij." + }, + "timestamp": { + "label": "Prekrivna časovna značka", + "description": "Na shranjene posnetke dodaj časovno značko." + }, + "bounding_box": { + "label": "Prekrivni okvirji (Bounding box)", + "description": "Na shranjene posnetke nariši okvirje za sledene objekte." + }, + "crop": { + "label": "Obreži posnetek", + "description": "Shranjene posnetke obreži na okvir zaznanega objekta." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Območja, v katera mora objekt vstopiti, da se posnetek shrani." + }, + "height": { + "label": "Višina posnetka", + "description": "Višina (v pikslih) za shranjene posnetke; pusti prazno za izvorno velikost." + }, + "retain": { + "label": "Hramba posnetkov", + "description": "Nastavitve hrambe za posnetke, vključno s privzetimi dnevi in povoženji po objektih.", + "default": { + "label": "Privzeta hramba", + "description": "Privzeto število dni za hrambo posnetkov." + }, + "mode": { + "label": "Način hrambe", + "description": "Način hrambe posnetkov." + }, + "objects": { + "label": "Hramba po objektih", + "description": "Posebne nastavitve dni hrambe za posamezne vrste objektov." + } + }, + "quality": { + "label": "Kakovost JPEG", + "description": "Kakovost kodiranja JPEG za shranjene posnetke (0-100)." + } + }, + "timestamp_style": { + "label": "Slog časovne značke", + "description": "Možnosti oblikovanja časovnih žigov na posnetkih in slikah.", + "position": { + "label": "Položaj časovne značke", + "description": "Položaj časovne značke na sliki (tl/tr/bl/br - zgoraj levo/desno, spodaj levo/desno)." + }, + "format": { + "label": "Format časovne značke", + "description": "Niz za format datuma in časa (Python datetime koda)." + }, + "color": { + "label": "Barva časovne značke", + "description": "RGB vrednosti barve za besedilo časovne značke (vse vrednosti 0-255).", + "red": { + "label": "Rdeča", + "description": "Rdeča komponenta (0-255) barve." + }, + "green": { + "label": "Zelena", + "description": "Zelena komponenta (0-255) barve." + }, + "blue": { + "label": "Modra", + "description": "Modra komponenta (0-255) barve." + } + }, + "thickness": { + "label": "Debelina časovne značke", + "description": "Debelina črte besedila časovne značke." + }, + "effect": { + "label": "Učinek časovne značke", + "description": "Vizualni učinek za besedilo (brez, polno, senca)." + } + }, + "best_image_timeout": { + "label": "Časovna omejitev za najboljšo sliko", + "description": "Kako dolgo naj se čaka na sliko z najvišjo oceno zaupanja." + }, + "mqtt": { + "label": "MQTT", + "description": "Nastavitve objavljanja slik preko MQTT.", + "enabled": { + "label": "Pošlji sliko", + "description": "Omogoči objavljanje slik objektov na MQTT teme za to kamero." + }, + "timestamp": { + "label": "Dodaj časovni žig", + "description": "Na slike, objavljene preko MQTT, dodaj časovni žig." + }, + "bounding_box": { + "label": "Dodaj okvir zaznave", + "description": "Na slike, objavljene preko MQTT, nariši okvirje zaznave." + }, + "crop": { + "label": "Izreži sliko", + "description": "Izreži slike za MQTT na velikost okvirja zaznanega objekta." + }, + "height": { + "label": "Višina slike", + "description": "Višina (v pikslih) slik, objavljenih preko MQTT." + }, + "required_zones": { + "label": "Zahtevane cone", + "description": "Cone, v katere mora objekt vstopiti, da se MQTT slika objavi." + }, + "quality": { + "label": "Kakovost JPEG", + "description": "Kakovost JPEG za slike, objavljene preko MQTT (0-100)." + } + }, + "notifications": { + "label": "Obvestila", + "description": "Nastavitve za omogočanje in nadzor obvestil za to kamero.", + "enabled": { + "label": "Omogoči obvestila", + "description": "Omogoči ali onemogoči obvestila za to kamero." + }, + "email": { + "label": "E-pošta za obvestila", + "description": "E-poštni naslov, ki se uporablja za potisna obvestila ali ga zahtevajo določeni ponudniki obvestil." + }, + "cooldown": { + "label": "Obdobje mirovanja (Cooldown)", + "description": "Čas mirovanja (v sekundah) med obvestili, da preprečiš zasipanje prejemnikov s sporočili." + }, + "enabled_in_config": { + "label": "Prvotno stanje obvestil", + "description": "Pove, ali so bila obvestila omogočena v prvotni statični konfiguraciji." + } + }, + "onvif": { + "label": "ONVIF", + "description": "Nastavitve ONVIF povezave in PTZ samodejnega sledenja za to kamero.", + "host": { + "label": "ONVIF gostitelj", + "description": "Naslov gostitelja za storitev ONVIF za to kamero." + }, + "port": { + "label": "ONVIF vrata (port)", + "description": "Številka vrat za storitev ONVIF." + }, + "user": { + "label": "ONVIF uporabniško ime", + "description": "Uporabniško ime za ONVIF avtentikacijo." + }, + "password": { + "label": "ONVIF geslo", + "description": "Geslo za ONVIF avtentikacijo." + }, + "tls_insecure": { + "label": "Onemogoči TLS preverjanje", + "description": "Preskoči preverjanje TLS (nevarno; uporabljaj le v varnih omrežjih)." + }, + "autotracking": { + "label": "Samodejno sledenje", + "description": "Samodejno sledi premikajočim se objektom in jih drži v sredini okvirja s premiki PTZ kamere.", + "enabled": { + "label": "Omogoči samodejno sledenje", + "description": "Vklopi ali izklopi samodejno PTZ sledenje zaznanim objektom." + }, + "calibrate_on_startup": { + "label": "Umeri ob zagonu", + "description": "Izmeri hitrosti motorjev PTZ ob zagonu za boljšo natančnost sledenja." + }, + "zooming": { + "label": "Način povečave", + "description": "Nadzor povečave: onemogočeno, absolutno (najbolj združljivo) ali relativno." + }, + "zoom_factor": { + "label": "Faktor povečave", + "description": "Nadzor stopnje povečave na sledenih objektih (0.1 do 0.75)." + }, + "track": { + "label": "Sledeni objekti", + "description": "Seznam vrst objektov, ki sprožijo samodejno sledenje." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Objekti morajo vstopiti v eno od teh območij, preden se sledenje začne." + }, + "return_preset": { + "label": "Prednastavitev za vrnitev", + "description": "Ime ONVIF prednastavitve (preset), na katero se kamera vrne po končanem sledenju." + }, + "timeout": { + "label": "Čas do vrnitve", + "description": "Koliko sekund naj kamera čaka po izgubi objekta, preden se vrne v prvotni položaj." + }, + "movement_weights": { + "label": "Uteži premikanja", + "description": "Vrednosti umerjanja, ki se generirajo samodejno. Ne spreminjaj ročno." + }, + "enabled_in_config": { + "label": "Prvotno stanje sledenja", + "description": "Interno polje za sledenje stanja sledenja v konfiguraciji." + } + }, + "ignore_time_mismatch": { + "label": "Prezri časovno neskladje", + "description": "Prezri razlike v sinhronizaciji časa med kamero in strežnikom za ONVIF komunikacijo." + } + }, + "type": { + "label": "Vrsta kamere", + "description": "Vrsta kamere" + }, + "ui": { + "label": "Uporabniški vmesnik kamere", + "description": "Vrstni red prikaza in vidnost te kamere v vmesniku. Vrstni red vpliva na privzeto nadzorno ploščo. Za natančnejši nadzor uporabi skupine kamer.", + "order": { + "label": "Vrstni red v vmesniku", + "description": "Številčni vrstni red za razvrščanje kamere v vmesniku (nadzorna plošča in seznami); višje številke se pojavijo kasneje." + }, + "dashboard": { + "label": "Prikaži v vmesniku", + "description": "Preklopi vidnost kamere povsod v vmesniku Frigate. Če to onemogočiš, boš moral ročno urediti konfiguracijo, da bo kamera spet vidna." + } + }, + "webui_url": { + "label": "URL kamere", + "description": "URL za neposreden obisk spletnega vmesnika kamere s strani sistema." + }, + "zones": { + "label": "Cone", + "description": "Cone ti omogočajo določitev specifičnega območja slike, da lahko ugotoviš, ali je objekt znotraj določenega predela.", + "friendly_name": { + "label": "Ime cone", + "description": "Uporabniku prijazno ime cone, prikazano v vmesniku Frigate. Če ni nastavljeno, bo uporabljena formatirana različica imena cone." + }, + "enabled": { + "label": "Omogočeno", + "description": "Omogoči ali onemogoči to cono. Onemogočene cone se med delovanjem prezrejo." + }, + "enabled_in_config": { + "label": "Sledi prvotnemu stanju cone." + }, + "filters": { + "label": "Filtri cone", + "description": "Filtri, ki se uporabijo za objekte znotraj te cone. Uporabljajo se za zmanjšanje lažnih zadetkov ali omejitev objektov, ki štejejo kot prisotni v coni.", + "min_area": { + "label": "Najmanjša površina objekta", + "description": "Najmanjša površina okvirja (v pikslih ali odstotkih), potrebna za to vrsto objekta. Lahko so piksli (celo število) ali odstotek (decimalno število med 0.000001 in 0.99)." + }, + "max_area": { + "label": "Največja površina objekta", + "description": "Največja dovoljena površina okvirja (v pikslih ali odstotkih) za to vrsto objekta. Lahko so piksli (celo število) ali odstotek (decimalno število med 0.000001 in 0.99)." + }, + "min_ratio": { + "label": "Najmanjše razmerje stranic", + "description": "Najmanjše razmerje širina/višina, potrebno za ustreznost okvirja." + }, + "max_ratio": { + "label": "Največje razmerje stranic", + "description": "Največje dovoljeno razmerje širina/višina za ustreznost okvirja." + }, + "threshold": { + "label": "Prag zaupanja", + "description": "Povprečni prag zaupanja zaznavanja, potreben, da se objekt v coni šteje za resničen zadetek." + }, + "min_score": { + "label": "Najmanjše zaupanje", + "description": "Najmanjše zaupanje zaznave v posamezni sliki, potrebno za upoštevanje objekta." + }, + "mask": { + "label": "Maska filtra", + "description": "Koordinate poligona, ki določa, kje znotraj slike se ta filter uporabi." + }, + "raw_mask": { + "label": "Surova maska" + } + }, + "coordinates": { + "label": "Koordinate", + "description": "Koordinate poligona, ki določa območje cone. Lahko je z vejico ločen niz ali seznam nizov koordinat. Koordinate morajo biti relativne (0-1) ali absolutne (starejši sistemi)." + }, + "distances": { + "label": "Razdalje v realnem svetu", + "description": "Izbirne realne razdalje za vsako stran štirikotnika cone, ki se uporabljajo za izračun hitrosti ali razdalje. Če je nastavljeno, mora imeti natanko 4 vrednosti." + }, + "inertia": { + "label": "Slike vztrajnosti (Inertia frames)", + "description": "Število zaporednih slik, v katerih mora biti objekt zaznan v coni, preden se šteje za prisotnega. Pomaga filtrirati prehodne zaznave." + }, + "loitering_time": { + "label": "Sekunde postopanja (Loitering)", + "description": "Število sekund, ki jih mora objekt preživeti v coni, da se šteje za postopanje. Nastavi na 0, če želiš onemogočiti zaznavanje postopanja." + }, + "speed_threshold": { + "label": "Najmanjša hitrost", + "description": "Najmanjša hitrost (v realnih enotah, če so nastavljene razdalje), potrebna, da se objekt šteje za prisotnega v coni. Uporablja se za sprožilce con na podlagi hitrosti." + }, + "objects": { + "label": "Objekti sprožilci", + "description": "Seznam vrst objektov (iz labelmap), ki lahko sprožijo to cono. Če je prazno, se upoštevajo vsi objekti." + } + }, + "enabled_in_config": { + "label": "Prvotno stanje kamere", + "description": "Sledi prvotnemu stanju kamere v konfiguraciji." + } +} diff --git a/web/public/locales/sl/config/global.json b/web/public/locales/sl/config/global.json new file mode 100644 index 0000000..625dd28 --- /dev/null +++ b/web/public/locales/sl/config/global.json @@ -0,0 +1,2192 @@ +{ + "audio": { + "label": "Avdio dogodki", + "enabled": { + "label": "Omogoči zaznavanje zvoka", + "description": "Vklopi ali izklopi zaznavanje avdio dogodkov za vse kamere." + }, + "max_not_heard": { + "label": "Časovna omejitev konca", + "description": "Število sekund brez nastavljenega tipa zvoka, preden se avdio dogodek zaključi." + }, + "min_volume": { + "label": "Najmanjša glasnost", + "description": "Najnižji prag glasnosti RMS za zagon zaznavanja; nižje vrednosti povečajo občutljivost (npr. 200 visoka, 500 srednja, 1000 nizka)." + }, + "listen": { + "label": "Tipi zvokov za poslušanje", + "description": "Seznam tipov avdio dogodkov za zaznavanje (npr. lajež, požarni alarm, krik, govor, vpitje)." + }, + "filters": { + "label": "Avdio filtri", + "description": "Nastavitve filtrov za posamezne tipe zvoka (npr. pragovi zaupanja) za zmanjšanje lažnih pozitivnih rezultatov." + }, + "enabled_in_config": { + "label": "Prvotno stanje zvoka", + "description": "Pove, ali je bilo zaznavanje zvoka prvotno omogočeno v statični konfiguracijski datoteki." + }, + "num_threads": { + "label": "Niti za zaznavanje", + "description": "Število niti za obdelavo zaznavanja zvoka." + }, + "description": "Nastavitve za zaznavanje dogodkov na podlagi zvoka za vse kamere; lahko jih povoziš za vsako kamero posebej." + }, + "audio_transcription": { + "label": "Transkripcija zvoka", + "description": "Nastavitve za transkripcijo zvoka v živo in govora, ki se uporablja za dogodke in podnapise v živo.", + "live_enabled": { + "label": "Transkripcija v živo", + "description": "Omogoči sprotno transkripcijo zvoka ob prejemu." + }, + "enabled": { + "label": "Omogoči transkripcijo zvoka", + "description": "Vklopi ali izklopi samodejno transkripcijo zvoka za vse kamere." + }, + "language": { + "label": "Jezik transkripcije", + "description": "Koda jezika za transkripcijo/prevajanje (npr. 'sl' za slovenščino ali 'en' za angleščino)." + }, + "device": { + "label": "Naprava za transkripcijo", + "description": "Ključ naprave (CPU/GPU), na kateri naj teče model. Trenutno so podprte le NVIDIA CUDA grafične kartice." + }, + "model_size": { + "label": "Velikost modela", + "description": "Velikost modela za uporabo pri transkripciji zvočnih dogodkov brez povezave." + } + }, + "birdseye": { + "label": "Birdseye (Ptičja perspektiva)", + "description": "Nastavitve za sestavljen pogled Birdseye, ki združi več virov kamer v eno postavitev.", + "enabled": { + "label": "Omogoči Birdseye", + "description": "Vklopi ali izklopi funkcijo Birdseye." + }, + "mode": { + "label": "Način sledenja", + "description": "Način vključitve kamer v Birdseye: 'objects' (objekti), 'motion' (gibanje) ali 'continuous' (neprekinjeno)." + }, + "order": { + "label": "Položaj", + "description": "Številčna vrednost, ki določa vrstni red kamere v postavitvi Birdseye." + }, + "restream": { + "label": "Pretakanje RTSP", + "description": "Ponovno pretakaj izhod Birdseye kot RTSP vir; to bo ohranilo Birdseye neprekinjeno delujoč." + }, + "width": { + "label": "Širina", + "description": "Izhodna širina (v pikslih) sestavljenega okvira Birdseye." + }, + "height": { + "label": "Višina", + "description": "Izhodna višina (v pikslih) sestavljenega okvira Birdseye." + }, + "quality": { + "label": "Kakovost kodiranja", + "description": "Kakovost kodiranja za mpeg1 vir Birdseye (1 najvišja, 31 najnižja kakovost)." + }, + "inactivity_threshold": { + "label": "Prag neaktivnosti", + "description": "Število sekund neaktivnosti, po katerih se kamera preneha prikazovati v Birdseye." + }, + "layout": { + "label": "Postavitev", + "description": "Možnosti postavitve za kompozicijo Birdseye.", + "scaling_factor": { + "label": "Faktor povečave", + "description": "Faktor povečave za kalkulator postavitve (razpon od 1.0 do 5.0)." + }, + "max_cameras": { + "label": "Največ kamer", + "description": "Največje število kamer, prikazanih hkrati v Birdseye; prikazane bodo najnovejše kamere." + } + }, + "idle_heartbeat_fps": { + "label": "FPS v mirovanju", + "description": "Število slik na sekundo za ponovno pošiljanje zadnjega okvira Birdseye med mirovanjem; nastavi na 0 za onemogočitev." + } + }, + "detect": { + "label": "Zaznavanje objektov", + "description": "Nastavitve za vlogo zaznavanja, ki se uporablja za iskanje objektov in inicializacijo sledilnikov.", + "enabled": { + "label": "Zaznavanje omogočeno", + "description": "Vklopi ali izklopi zaznavanje objektov za vse kamere. Zaznavanje mora biti omogočeno, da deluje sledenje objektom." + }, + "height": { + "label": "Višina zaznavanja", + "description": "Višina (v pikslih) slik za tok zaznavanja; pusti prazno za uporabo izvorne ločljivosti." + }, + "width": { + "label": "Širina zaznavanja", + "description": "Širina (v pikslih) slik za tok zaznavanja; pusti prazno za uporabo izvorne ločljivosti." + }, + "fps": { + "label": "FPS zaznavanja", + "description": "Želeno število slik na sekundo za zaznavanje; nižje vrednosti zmanjšajo porabo procesorja (priporočeno je 5)." + }, + "min_initialized": { + "label": "Najmanj slik za inicializacijo", + "description": "Število zaporednih zaznav, potrebnih pred ustvarjanjem sledenega objekta. Povečaj za manj lažnih zaznav." + }, + "max_disappeared": { + "label": "Največ slik ob izginotju", + "description": "Število slik brez zaznave, preden se sledeni objekt šteje za izginulega." + }, + "stationary": { + "label": "Konfiguracija nepremičnih objektov", + "description": "Nastavitve za zaznavanje in upravljanje objektov, ki nekaj časa ostanejo na mestu.", + "interval": { + "label": "Interval nepremičnosti", + "description": "Kako pogosto (v slikah) naj se preveri prisotnost nepremičnega objekta." + }, + "threshold": { + "label": "Prag nepremičnosti", + "description": "Število slik brez spremembe položaja, potrebnih, da se objekt označi za nepremičnega." + }, + "max_frames": { + "label": "Največ slik", + "description": "Omejuje, kako dolgo se sledi nepremičnim objektom, preden se zavržejo.", + "default": { + "label": "Privzeto največ slik", + "description": "Privzeto največje število slik za sledenje nepremičnemu objektu." + }, + "objects": { + "label": "Največ slik za objekt", + "description": "Posebne omejitve za posamezne tipe objektov." + } + }, + "classifier": { + "label": "Omogoči vizualni klasifikator", + "description": "Uporabi vizualni klasifikator za potrditev nepremičnih objektov, tudi če se okvirji rahlo premikajo." + } + }, + "annotation_offset": { + "label": "Odmik anotacij", + "description": "Število milisekund za premik oznak zaznavanja, da se bolje ujemajo s posnetki; lahko je pozitivno ali negativno." + } + }, + "face_recognition": { + "label": "Prepoznava obrazov", + "enabled": { + "label": "Omogoči prepoznavo obrazov", + "description": "Vklopi ali izklopi prepoznavo obrazov na vseh kamerah." + }, + "min_area": { + "label": "Najmanjša površina obraza", + "description": "Najmanjša površina (v pikslih) okvirja obraza, potrebna za poskus prepoznave." + }, + "description": "Nastavitve za zaznavanje in prepoznavo obrazov za vse kamere.", + "model_size": { + "label": "Velikost modela", + "description": "Velikost modela za vdelave obrazov; večji modeli lahko zahtevajo GPU." + }, + "unknown_score": { + "label": "Prag za neznan obraz", + "description": "Prag razdalje, pod katerim se obraz šteje za potencialno ujemanje (višje = strožje)." + }, + "detection_threshold": { + "label": "Prag zaznavanja", + "description": "Najmanjše zaupanje, potrebno za veljavno detekcijo obraza." + }, + "recognition_threshold": { + "label": "Prag prepoznave", + "description": "Prag razdalje vdelave obraza za potrditev ujemanja dveh obrazov." + }, + "min_faces": { + "label": "Najmanjše število obrazov", + "description": "Najmanjše število prepoznav obraza, preden se osebi dodeli prepoznana pod-oznaka." + }, + "save_attempts": { + "label": "Shrani poskuse", + "description": "Število poskusov prepoznave obraza, ki se hranijo za vmesnik." + }, + "blur_confidence_filter": { + "label": "Filter zamegljenosti", + "description": "Prilagodi oceno zaupanja glede na zamegljenost slike, da se zmanjša število napačnih prepoznav pri slabši kakovosti." + }, + "device": { + "label": "Naprava", + "description": "Povoženje nastavitve za ciljanje specifične naprave." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "Nastavitve FFmpeg, vključno s potjo do binarne datoteke, argumenti, možnostmi strojnega pospeševanja in argumenti izhoda po vlogah.", + "path": { + "label": "Pot do FFmpeg", + "description": "Pot do binarne datoteke FFmpeg, ki naj se uporabi, ali vzdevek različice (\"5.0\" ali \"7.0\")." + }, + "global_args": { + "label": "Globalni argumenti FFmpeg", + "description": "Globalni argumenti, posredovani procesom FFmpeg." + }, + "hwaccel_args": { + "label": "Argumenti strojnega pospeševanja", + "description": "Argumenti za strojno pospeševanje FFmpeg. Priporočljive so prednastavitve glede na ponudnika." + }, + "input_args": { + "label": "Vhodni argumenti", + "description": "Vhodni argumenti, uporabljeni za vhodne tokove FFmpeg." + }, + "output_args": { + "label": "Izhodni argumenti", + "description": "Privzeti izhodni argumenti, uporabljeni za različne vloge FFmpeg, kot sta zaznavanje in snemanje.", + "detect": { + "label": "Izhodni argumenti za zaznavanje", + "description": "Privzeti izhodni argumenti za tokove z vlogo zaznavanja (detect)." + }, + "record": { + "label": "Izhodni argumenti za snemanje", + "description": "Privzeti izhodni argumenti za tokove z vlogo snemanja (record)." + } + }, + "retry_interval": { + "label": "Čas ponovnega poskusa FFmpeg", + "description": "Število sekund čakanja pred ponovnim poskusom povezave s tokom kamere po napaki. Privzeto je 10." + }, + "apple_compatibility": { + "label": "Združljivost z Apple napravami", + "description": "Omogoči označevanje HEVC za boljšo združljivost z Applovimi predvajalniki pri snemanju v H.265." + }, + "gpu": { + "label": "Indeks GPU", + "description": "Privzeti indeks grafične kartice (GPU), uporabljen za strojno pospeševanje, če je na voljo." + }, + "inputs": { + "label": "Vhodi kamere", + "description": "Seznam definicij vhodnih tokov (poti in vloge) za to kamero.", + "path": { + "label": "Vhodna pot", + "description": "URL ali pot do vhodnega toka kamere." + }, + "roles": { + "label": "Vloge vhoda", + "description": "Vloge za ta vhodni tok." + }, + "global_args": { + "label": "Globalni argumenti FFmpeg", + "description": "Globalni argumenti FFmpeg za ta vhodni tok." + }, + "hwaccel_args": { + "label": "Argumenti strojnega pospeševanja", + "description": "Argumenti strojnega pospeševanja za ta vhodni tok." + }, + "input_args": { + "label": "Vhodni argumenti", + "description": "Vhodni argumenti, specifični za ta tok." + } + } + }, + "live": { + "label": "Predvajanje v živo", + "streams": { + "label": "Imena tokov v živo", + "description": "Preslikava konfiguriranih imen tokov v imena restream/go2rtc, uporabljena za predvajanje v živo." + }, + "height": { + "label": "Višina v živo", + "description": "Višina (v pikslih) za upodabljanje jsmpeg toka v živo v spletnem vmesniku; mora biti <= višini toka za zaznavanje." + }, + "quality": { + "label": "Kakovost v živo", + "description": "Kakovost kodiranja za jsmpeg tok (1 najvišja, 31 najnižja)." + }, + "description": "Nastavitve, ki jih uporablja spletni vmesnik za nadzor ločljivosti in kakovosti toka v živo." + }, + "lpr": { + "label": "Prepoznava registrskih tablic (LPR)", + "description": "Nastavitve prepoznave registrskih tablic, vključno s pragi detekcije, formatiranjem in znanimi tablicami.", + "enabled": { + "label": "Omogoči LPR", + "description": "Vklopi ali izklopi prepoznavo registrskih tablic za vse kamere." + }, + "expire_time": { + "label": "Sekunde do poteka", + "description": "Čas v sekundah, po katerem nevidna tablica poteče iz sledilnika (samo za namenske LPR kamere)." + }, + "min_area": { + "label": "Najmanjša površina tablice", + "description": "Najmanjša površina tablice (v pikslih) za poskus prepoznave." + }, + "enhancement": { + "label": "Stopnja izboljšave", + "description": "Stopnja izboljšave (0-10) slik tablic pred OCR; stopnje nad 5 so priporočljive le za nočne posnetke." + }, + "model_size": { + "label": "Velikost modela", + "description": "Velikost modela za zaznavanje/prepoznavanje besedila. Večina uporabnikov naj uporablja 'small'." + }, + "detection_threshold": { + "label": "Prag zaznavanja", + "description": "Prag zaupanja za začetek izvajanja OCR na sumljivi tablici." + }, + "recognition_threshold": { + "label": "Prag prepoznave", + "description": "Prag zaupanja, potreben, da se besedilo tablice doda kot pod-oznaka." + }, + "min_plate_length": { + "label": "Najmanjša dolžina tablice", + "description": "Najmanjše število znakov, ki jih mora vsebovati tablica, da se šteje za veljavno." + }, + "format": { + "label": "Regex format tablice", + "description": "Izbirni regex za preverjanje, ali prepoznana tablica ustreza pričakovanemu formatu." + }, + "match_distance": { + "label": "Razdalja ujemanja", + "description": "Dovoljeno število napačnih znakov pri primerjanju zaznanih tablic z znanimi." + }, + "known_plates": { + "label": "Znane tablice", + "description": "Seznam tablic ali regexov za posebno sledenje ali opozarjanje." + }, + "debug_save_plates": { + "label": "Shrani tablice za razhroščevanje", + "description": "Shrani slike izrezov tablic za preverjanje delovanja LPR." + }, + "device": { + "label": "Naprava", + "description": "Povoženje nastavitve za ciljanje specifične naprave." + }, + "replace_rules": { + "label": "Pravila zamenjave", + "description": "Pravila za zamenjavo z regexi za normalizacijo nizov tablic pred primerjavo.", + "pattern": { + "label": "Regex vzorec" + }, + "replacement": { + "label": "Niz za zamenjavo" + } + } + }, + "motion": { + "label": "Zaznavanje gibanja", + "enabled": { + "label": "Omogoči zaznavanje gibanja", + "description": "Vklopi ali izklopi zaznavanje gibanja za vse kamere." + }, + "threshold": { + "label": "Prag gibanja", + "description": "Prag razlike v pikslih, ki ga uporablja detektor gibanja; višje vrednosti zmanjšajo občutljivost (razpon 1-255)." + }, + "lightning_threshold": { + "label": "Prag za strele", + "description": "Prag za zaznavanje in ignoriranje kratkih svetlobnih skokov (nižje je bolj občutljivo). To ne prepreči snemanja, le ustavi analizo dodatnih okvirjev ob blisku." + }, + "skip_motion_threshold": { + "label": "Prag za preskok gibanja", + "description": "Če se v enem okvirju spremeni večji delež slike od tega, detektor ne bo vrnil okvirjev gibanja in se bo takoj umeril. To zmanjša lažne pozitivne rezultate med nevihtami. Razpon 0.0 do 1.0." + }, + "improve_contrast": { + "label": "Izboljšaj kontrast", + "description": "Uporabi izboljšavo kontrasta na okvirjih pred analizo gibanja za boljšo detekcijo." + }, + "contour_area": { + "label": "Površina konture", + "description": "Najmanjša površina konture v pikslih, potrebna, da se gibanje upošteva." + }, + "delta_alpha": { + "label": "Delta alfa", + "description": "Faktor alfa mešanja, uporabljen pri razlikovanju okvirjev za izračun gibanja." + }, + "frame_alpha": { + "label": "Alfa okvirja", + "description": "Vrednost alfa, uporabljena pri mešanju okvirjev za predobdelavo gibanja." + }, + "frame_height": { + "label": "Višina okvirja", + "description": "Višina v pikslih, na katero se spremeni velikost okvirja pri izračunu gibanja." + }, + "mask": { + "label": "Koordinate maske", + "description": "Urejene koordinate x,y, ki določajo poligon maske gibanja za vključitev ali izključitev območij." + }, + "mqtt_off_delay": { + "label": "Zakasnitev izklopa MQTT", + "description": "Število sekund čakanja po zadnjem zaznanem gibanju pred objavo stanja 'off' na MQTT." + }, + "enabled_in_config": { + "label": "Prvotno stanje gibanja", + "description": "Pove, ali je bilo zaznavanje gibanja omogočeno v prvotni statični konfiguraciji." + }, + "raw_mask": { + "label": "Surova maska" + }, + "description": "Privzete nastavitve zaznavanja gibanja, uporabljene za kamere, razen če so povožene pri posamezni kameri." + }, + "objects": { + "label": "Objekti", + "description": "Privzete nastavitve sledenja objektom, vključno s tem, katere oznake naj se sledijo, in filtri za posamezne objekte.", + "track": { + "label": "Objekti za sledenje", + "description": "Seznam oznak objektov, ki naj se sledijo na vseh kamerah." + }, + "filters": { + "label": "Filtri objektov", + "description": "Filtri za zaznane objekte za zmanjšanje lažnih pozitivnih rezultatov (površina, razmerje, zaupanje).", + "min_area": { + "label": "Najmanjša površina objekta", + "description": "Najmanjša površina okvirja (v pikslih ali odstotkih), potrebna za to vrsto objekta." + }, + "max_area": { + "label": "Največja površina objekta", + "description": "Največja dovoljena površina okvirja za to vrsto objekta." + }, + "min_ratio": { + "label": "Najmanjše razmerje stranic", + "description": "Najmanjše razmerje širina/višina, potrebno za veljavnost okvirja." + }, + "max_ratio": { + "label": "Največje razmerje stranic", + "description": "Največje dovoljeno razmerje širina/višina." + }, + "threshold": { + "label": "Prag zaupanja", + "description": "Povprečni prag zaupanja detekcije, potreben, da se objekt šteje za pravega." + }, + "min_score": { + "label": "Najmanjše zaupanje", + "description": "Najmanjše zaupanje detekcije v enem okvirju, da se objekt upošteva." + }, + "mask": { + "label": "Maska filtra", + "description": "Koordinate poligona, ki določajo, kje znotraj okvirja velja ta filter." + }, + "raw_mask": { + "label": "Surova maska" + } + }, + "mask": { + "label": "Maska objekta", + "description": "Poligon maske, uporabljen za preprečevanje zaznavanja objektov na določenih območij." + }, + "raw_mask": { + "label": "Surova maska" + }, + "genai": { + "label": "GenAI konfiguracija objektov", + "description": "GenAI možnosti za opisovanje sledenih objektov in pošiljanje okvirjev za generiranje.", + "enabled": { + "label": "Omogoči GenAI", + "description": "Privzeto omogoči GenAI generiranje opisov za sledene objekte." + }, + "use_snapshot": { + "label": "Uporabi posnetke", + "description": "Za generiranje GenAI opisov uporabi posnetke (snapshots) namesto sličic (thumbnails)." + }, + "prompt": { + "label": "Navodilo za opis (Prompt)", + "description": "Privzeta predloga navodila, uporabljena pri generiranju opisov z GenAI." + }, + "object_prompts": { + "label": "Navodila za specifične objekte", + "description": "Navodila po meri za specifične oznake objektov." + }, + "objects": { + "label": "GenAI objekti", + "description": "Seznam oznak objektov, ki naj se privzeto pošiljajo GenAI-ju." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Območja, v katera mora objekt vstopiti, da je primeren za GenAI opis." + }, + "debug_save_thumbnails": { + "label": "Shrani sličice", + "description": "Shrani sličice, poslane GenAI-ju, za namene razhroščevanja in pregleda." + }, + "send_triggers": { + "label": "GenAI sprožilci", + "description": "Določa, kdaj naj se okvirji pošljejo GenAI-ju (ob koncu, po posodobitvah itd.).", + "tracked_object_end": { + "label": "Pošlji ob koncu", + "description": "Pošlji zahtevo GenAI-ju, ko se sledenje objektu konča." + }, + "after_significant_updates": { + "label": "Zgodnji GenAI sprožilec", + "description": "Pošlji zahtevo GenAI-ju po določenem številu pomembnih posodobitev sledenega objekta." + } + }, + "enabled_in_config": { + "label": "Prvotno GenAI stanje", + "description": "Pove, ali je bil GenAI omogočen v prvotni statični konfiguraciji." + } + } + }, + "record": { + "label": "Snemanje", + "enabled": { + "label": "Omogoči snemanje", + "description": "Vklopi ali izklopi snemanje za vse kamere." + }, + "expire_interval": { + "label": "Interval čiščenja posnetkov", + "description": "Minute med cikli čiščenja, ki odstranijo potekle segmente snemanja." + }, + "continuous": { + "label": "Neprekinjena hramba", + "description": "Število dni hrambe posnetkov ne glede na objekte ali gibanje. Nastavi na 0, če želiš hraniti le opozorila in detekcije.", + "days": { + "label": "Dni hrambe", + "description": "Število dni za hrambo posnetkov." + } + }, + "motion": { + "label": "Hramba ob gibanju", + "description": "Število dni hrambe posnetkov, ki jih sproži gibanje, ne glede na objekte.", + "days": { + "label": "Dni hrambe", + "description": "Število dni za hrambo posnetkov." + } + }, + "detections": { + "label": "Hramba detekcij", + "description": "Nastavitve hrambe za dogodke zaznavanja, vključno s trajanjem pred in po zajemu.", + "pre_capture": { + "label": "Sekunde pred zajemom", + "description": "Število sekund pred dogodkom zaznavanja, ki se vključi v posnetek." + }, + "post_capture": { + "label": "Sekunde po zajemu", + "description": "Število sekund po dogodku zaznavanja, ki se vključi v posnetek." + }, + "retain": { + "label": "Hramba dogodkov", + "description": "Nastavitve hrambe za posnetke dogodkov zaznavanja.", + "days": { + "label": "Dni hrambe", + "description": "Število dni za hrambo posnetkov dogodkov zaznavanja." + }, + "mode": { + "label": "Način hrambe", + "description": "Način hrambe: all (vsi segmenti), motion (segmenti z gibanjem) ali active_objects (segmenti z aktivnimi objekti)." + } + } + }, + "alerts": { + "label": "Hramba opozoril", + "description": "Nastavitve hrambe za dogodke opozoril.", + "pre_capture": { + "label": "Sekunde pred zajemom", + "description": "Število sekund pred dogodkom opozorila, ki se vključi v posnetek." + }, + "post_capture": { + "label": "Sekunde po zajemu", + "description": "Število sekund po dogodku opozorila, ki se vključi v posnetek." + }, + "retain": { + "label": "Hramba dogodkov", + "description": "Nastavitve hrambe za posnetke dogodkov opozoril.", + "days": { + "label": "Dni hrambe", + "description": "Število dni za hrambo posnetkov dogodkov opozoril." + }, + "mode": { + "label": "Način hrambe", + "description": "Način hrambe posnetkov opozoril." + } + } + }, + "export": { + "label": "Konfiguracija izvoza", + "description": "Nastavitve za izvoz posnetkov, kot sta časovni zamik (timelapse) in strojno pospeševanje.", + "hwaccel_args": { + "label": "Argumenti strojnega pospeševanja za izvoz", + "description": "Argumenti strojnega pospeševanja za operacije izvoza in transkodiranja." + } + }, + "preview": { + "label": "Konfiguracija predogleda", + "description": "Nastavitve kakovosti predogledov snemanja v vmesniku.", + "quality": { + "label": "Kakovost predogleda", + "description": "Stopnja kakovosti predogleda (zelo nizka, nizka, srednja, visoka, zelo visoka)." + } + }, + "enabled_in_config": { + "label": "Prvotno stanje snemanja", + "description": "Pove, ali je bilo snemanje omogočeno v prvotni statični konfiguraciji." + }, + "description": "Nastavitve snemanja in hrambe, razen če so povožene pri posamezni kameri." + }, + "review": { + "label": "Pregled", + "alerts": { + "label": "Konfiguracija opozoril", + "description": "Nastavitve za objekte, ki sprožijo opozorila, in njihovo hrambo.", + "enabled": { + "label": "Omogoči opozorila", + "description": "Vklopi ali izklopi generiranje opozoril za vse kamere." + }, + "labels": { + "label": "Oznake opozoril", + "description": "Seznam oznak objektov, ki štejejo kot opozorila (npr. avto, oseba)." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Območja, v katera mora objekt vstopiti, da postane opozorilo; pusti prazno za katerokoli območje." + }, + "enabled_in_config": { + "label": "Prvotno stanje opozoril", + "description": "Sledi, ali so bila opozorila prvotno omogočena." + }, + "cutoff_time": { + "label": "Čas zaključka opozorila", + "description": "Število sekund čakanja po končani aktivnosti, preden se opozorilo zaključi." + } + }, + "detections": { + "label": "Konfiguracija detekcij", + "description": "Nastavitve za ustvarjanje dogodkov zaznavanja (ki niso opozorila) in čas hrambe.", + "enabled": { + "label": "Omogoči detekcije", + "description": "Vklopi ali izklopi dogodke zaznavanja za vse kamere." + }, + "labels": { + "label": "Oznake detekcij", + "description": "Seznam oznak objektov, ki štejejo kot dogodki zaznavanja." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Območja, v katera mora objekt vstopiti, da postane detekcija." + }, + "cutoff_time": { + "label": "Čas zaključka detekcije", + "description": "Število sekund čakanja po končani aktivnosti, preden se detekcija zaključi." + }, + "enabled_in_config": { + "label": "Prvotno stanje detekcij", + "description": "Sledi, ali so bile detekcije prvotno omogočene." + } + }, + "genai": { + "label": "GenAI konfiguracija", + "description": "Nadzira uporabo generativne UI za izdelavo opisov in povzetkov postavk pregleda.", + "enabled": { + "label": "Omogoči GenAI opise", + "description": "Omogoči ali onemogoči GenAI opise in povzetke za postavke pregleda." + }, + "alerts": { + "label": "Omogoči GenAI za opozorila", + "description": "Uporabi GenAI za generiranje opisov opozoril." + }, + "detections": { + "label": "Omogoči GenAI za detekcije", + "description": "Uporabi GenAI za generiranje opisov detekcij." + }, + "image_source": { + "label": "Vir slik za pregled", + "description": "Vir slik za GenAI ('preview' ali 'recordings'); 'recordings' nudi višjo kakovost, a porabi več žetonov." + }, + "additional_concerns": { + "label": "Dodatni pomisleki", + "description": "Seznam dodatnih navodil ali opomb, ki naj jih GenAI upošteva pri ocenjevanju aktivnosti." + }, + "debug_save_thumbnails": { + "label": "Shrani sličice", + "description": "Shrani sličice, poslane ponudniku GenAI, za namene razhroščevanja." + }, + "enabled_in_config": { + "label": "Prvotno GenAI stanje", + "description": "Sledi, ali je bil GenAI pregled prvotno omogočen." + }, + "preferred_language": { + "label": "Prednostni jezik", + "description": "Jezik, ki se zahteva od ponudnika GenAI za generirane odgovore." + }, + "activity_context_prompt": { + "label": "Navodilo za kontekst aktivnosti", + "description": "Navodilo po meri, ki opisuje, kaj je in kaj ni sumljiva aktivnost, za boljšo orientacijo GenAI-ja." + } + }, + "description": "Nastavitve, ki nadzorujejo opozorila, detekcije in GenAI povzetke za spletni vmesnik." + }, + "semantic_search": { + "label": "Semantično iskanje", + "triggers": { + "label": "Sprožilci", + "description": "Dejanja in kriteriji ujemanja za sprožilce semantičnega iskanja na določeni kameri.", + "friendly_name": { + "label": "Prijazno ime", + "description": "Izbirno ime, ki se prikaže v vmesniku za ta sprožilec." + }, + "enabled": { + "label": "Omogoči ta sprožilec", + "description": "Vklopi ali izklopi ta sprožilec semantičnega iskanja." + }, + "type": { + "label": "Vrsta sprožilca", + "description": "Vrsta sprožilca: 'thumbnail' (ujemanje s sliko) ali 'description' (ujemanje z besedilom)." + }, + "data": { + "label": "Vsebina sprožilca", + "description": "Besedilna fraza ali ID sličice za primerjavo s sledenimi objekti." + }, + "threshold": { + "label": "Prag sprožilca", + "description": "Najmanjša ocena podobnosti (0-1), potrebna za aktivacijo tega sprožilca." + }, + "actions": { + "label": "Dejanja sprožilca", + "description": "Seznam dejanj ob ujemanju (obvestilo, pod-oznaka, atribut)." + } + }, + "description": "Nastavitve za semantično iskanje, ki gradi in poizveduje po vdelavah (embeddings) objektov za iskanje podobnih postavk.", + "enabled": { + "label": "Omogoči semantično iskanje", + "description": "Vklopi ali izklopi funkcijo semantičnega iskanja." + }, + "reindex": { + "label": "Ponovno indeksiranje ob zagonu", + "description": "Sproži popolno ponovno indeksiranje zgodovinskih sledenih objektov v bazo podatkov vdelav." + }, + "model": { + "label": "Model za semantično iskanje", + "description": "Model vdelav, ki se uporabi za semantično iskanje (npr. 'jinav1')." + }, + "model_size": { + "label": "Velikost modela", + "description": "Izberi velikost modela; 'small' deluje na procesorju (CPU), 'large' običajno zahteva grafično kartico (GPU)." + }, + "device": { + "label": "Naprava", + "description": "Povoženje nastavitve za ciljanje specifične naprave." + } + }, + "snapshots": { + "label": "Posnetki (Snapshots)", + "enabled": { + "label": "Posnetki omogočeni", + "description": "Vklopi ali izklopi shranjevanje posnetkov za vse kamere." + }, + "clean_copy": { + "label": "Shrani čisto kopijo", + "description": "Poleg označenih shrani tudi čisto kopijo posnetkov brez anotacij." + }, + "timestamp": { + "label": "Prekrivna časovna značka", + "description": "Na shranjene posnetke dodaj časovno značko." + }, + "bounding_box": { + "label": "Prekrivni okvirji (Bounding box)", + "description": "Na shranjene posnetke nariši okvirje za sledene objekte." + }, + "crop": { + "label": "Obreži posnetek", + "description": "Shranjene posnetke obreži na okvir zaznanega objekta." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Območja, v katera mora objekt vstopiti, da se posnetek shrani." + }, + "height": { + "label": "Višina posnetka", + "description": "Višina (v pikslih) za shranjene posnetke; pusti prazno za izvorno velikost." + }, + "retain": { + "label": "Hramba posnetkov", + "description": "Nastavitve hrambe za posnetke, vključno s privzetimi dnevi in povoženji po objektih.", + "default": { + "label": "Privzeta hramba", + "description": "Privzeto število dni za hrambo posnetkov." + }, + "mode": { + "label": "Način hrambe", + "description": "Način hrambe posnetkov." + }, + "objects": { + "label": "Hramba po objektih", + "description": "Posebne nastavitve dni hrambe za posamezne vrste objektov." + } + }, + "quality": { + "label": "Kakovost JPEG", + "description": "Kakovost kodiranja JPEG za shranjene posnetke (0-100)." + }, + "description": "Nastavitve za shranjene JPEG posnetke sledenih objektov; lahko jih povoziš pri posamezni kameri." + }, + "timestamp_style": { + "label": "Slog časovne značke", + "position": { + "label": "Položaj časovne značke", + "description": "Položaj časovne značke na sliki (tl/tr/bl/br - zgoraj levo/desno, spodaj levo/desno)." + }, + "format": { + "label": "Format časovne značke", + "description": "Niz za format datuma in časa (Python datetime koda)." + }, + "color": { + "label": "Barva časovne značke", + "description": "RGB vrednosti barve za besedilo časovne značke (vse vrednosti 0-255).", + "red": { + "label": "Rdeča", + "description": "Rdeča komponenta (0-255) barve." + }, + "green": { + "label": "Zelena", + "description": "Zelena komponenta (0-255) barve." + }, + "blue": { + "label": "Modra", + "description": "Modra komponenta (0-255) barve." + } + }, + "thickness": { + "label": "Debelina časovne značke", + "description": "Debelina črte besedila časovne značke." + }, + "effect": { + "label": "Učinek časovne značke", + "description": "Vizualni učinek za besedilo (brez, polno, senca)." + }, + "description": "Možnosti oblikovanja časovnih značk v viru, ki se uporabljajo v pogledu za razhroščevanje in na posnetkih." + }, + "mqtt": { + "label": "MQTT", + "description": "Nastavitve za povezovanje in objavljanje telemetrije, posnetkov zaslona in podrobnosti o dogodkih na posrednika (broker) MQTT.", + "enabled": { + "label": "Omogoči MQTT", + "description": "Omogoči ali onemogoči integracijo MQTT za stanja, dogodke in posnetke zaslona." + }, + "host": { + "label": "Gostitelj MQTT", + "description": "Ime gostitelja ali IP naslov posrednika MQTT." + }, + "port": { + "label": "Vrata MQTT (Port)", + "description": "Vrata posrednika MQTT (običajno 1883 za navaden MQTT)." + }, + "topic_prefix": { + "label": "Predpona teme (Topic prefix)", + "description": "Predpona teme MQTT za vse teme Frigate; mora biti edinstvena, če poganjaš več instanc." + }, + "client_id": { + "label": "ID odjemalca", + "description": "Identifikator odjemalca, uporabljen pri povezovanju z MQTT posrednikom; moral bi biti edinstven za vsako instanco." + }, + "stats_interval": { + "label": "Interval statistike", + "description": "Interval v sekundah za objavljanje statistike sistema in kamer na MQTT." + }, + "user": { + "label": "MQTT uporabniško ime", + "description": "Izbirno uporabniško ime za MQTT; lahko ga podaš prek okoljskih spremenljivk ali skrivnosti (secrets)." + }, + "password": { + "label": "MQTT geslo", + "description": "Izbirno geslo za MQTT; lahko ga podaš prek okoljskih spremenljivk ali skrivnosti (secrets)." + }, + "tls_ca_certs": { + "label": "Certifikati TLS CA", + "description": "Pot do CA certifikata za TLS povezave s posrednikom (za samopodpisane certifikate)." + }, + "tls_client_cert": { + "label": "Certifikat odjemalca", + "description": "Pot do certifikata odjemalca za TLS medsebojno avtentikacijo; ne nastavljaj uporabniškega imena/gesla, če uporabljaš certifikate odjemalca." + }, + "tls_client_key": { + "label": "Ključ odjemalca", + "description": "Pot do zasebnega ključa za certifikat odjemalca." + }, + "tls_insecure": { + "label": "TLS nezaščiteno", + "description": "Dovoli nevarne TLS povezave s preskokom preverjanja imena gostitelja (ni priporočljivo)." + }, + "qos": { + "label": "MQTT QoS", + "description": "Raven kakovosti storitve (Quality of Service) za objave/naročnine MQTT (0, 1 ali 2)." + } + }, + "notifications": { + "label": "Obvestila", + "enabled": { + "label": "Omogoči obvestila", + "description": "Omogoči ali onemogoči obvestila za vse kamere; lahko jih povoziš pri posamezni kameri." + }, + "email": { + "label": "E-pošta za obvestila", + "description": "E-poštni naslov, ki se uporablja za potisna obvestila ali ga zahtevajo določeni ponudniki obvestil." + }, + "cooldown": { + "label": "Obdobje mirovanja (Cooldown)", + "description": "Čas mirovanja (v sekundah) med obvestili, da preprečiš zasipanje prejemnikov s sporočili." + }, + "enabled_in_config": { + "label": "Prvotno stanje obvestil", + "description": "Pove, ali so bila obvestila omogočena v prvotni statični konfiguraciji." + }, + "description": "Nastavitve za omogočanje in nadzor obvestil za vse kamere; lahko jih povoziš pri posamezni kameri." + }, + "onvif": { + "label": "ONVIF", + "description": "Nastavitve ONVIF povezave in PTZ samodejnega sledenja za to kamero.", + "host": { + "label": "ONVIF gostitelj", + "description": "Naslov gostitelja za storitev ONVIF za to kamero." + }, + "port": { + "label": "ONVIF vrata (port)", + "description": "Številka vrat za storitev ONVIF." + }, + "user": { + "label": "ONVIF uporabniško ime", + "description": "Uporabniško ime za ONVIF avtentikacijo." + }, + "password": { + "label": "ONVIF geslo", + "description": "Geslo za ONVIF avtentikacijo." + }, + "tls_insecure": { + "label": "Onemogoči TLS preverjanje", + "description": "Preskoči preverjanje TLS (nevarno; uporabljaj le v varnih omrežjih)." + }, + "autotracking": { + "label": "Samodejno sledenje", + "description": "Samodejno sledi premikajočim se objektom in jih drži v sredini okvirja s premiki PTZ kamere.", + "enabled": { + "label": "Omogoči samodejno sledenje", + "description": "Vklopi ali izklopi samodejno PTZ sledenje zaznanim objektom." + }, + "calibrate_on_startup": { + "label": "Umeri ob zagonu", + "description": "Izmeri hitrosti motorjev PTZ ob zagonu za boljšo natančnost sledenja." + }, + "zooming": { + "label": "Način povečave", + "description": "Nadzor povečave: onemogočeno, absolutno (najbolj združljivo) ali relativno." + }, + "zoom_factor": { + "label": "Faktor povečave", + "description": "Nadzor stopnje povečave na sledenih objektih (0.1 do 0.75)." + }, + "track": { + "label": "Sledeni objekti", + "description": "Seznam vrst objektov, ki sprožijo samodejno sledenje." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Objekti morajo vstopiti v eno od teh območij, preden se sledenje začne." + }, + "return_preset": { + "label": "Prednastavitev za vrnitev", + "description": "Ime ONVIF prednastavitve (preset), na katero se kamera vrne po končanem sledenju." + }, + "timeout": { + "label": "Čas do vrnitve", + "description": "Koliko sekund naj kamera čaka po izgubi objekta, preden se vrne v prvotni položaj." + }, + "movement_weights": { + "label": "Uteži premikanja", + "description": "Vrednosti umerjanja, ki se generirajo samodejno. Ne spreminjaj ročno." + }, + "enabled_in_config": { + "label": "Prvotno stanje sledenja", + "description": "Interno polje za sledenje stanja sledenja v konfiguraciji." + } + }, + "ignore_time_mismatch": { + "label": "Prezri časovno neskladje", + "description": "Prezri razlike v sinhronizaciji časa med kamero in strežnikom za ONVIF komunikacijo." + } + }, + "version": { + "label": "Trenutna različica konfiguracije", + "description": "Številčna ali tekstovna različica aktivne konfiguracije, ki pomaga pri zaznavanju migracij ali sprememb formata." + }, + "safe_mode": { + "label": "Varni način", + "description": "Ko je omogočeno, se Frigate zažene v varnem načinu z omejenimi funkcijami za odpravljanje težav." + }, + "environment_vars": { + "label": "Okoljske spremenljivke", + "description": "Pari ključ/vrednost okoljskih spremenljivk, ki se nastavijo za proces Frigate v sistemu Home Assistant OS. Uporabniki, ki ne uporabljajo HAOS, morajo namesto tega uporabiti konfiguracijo okoljskih spremenljivk Docker." + }, + "logger": { + "label": "Beleženje (Logging)", + "description": "Nadzira privzeto podrobnost dnevnikov in omogoča povoženje ravni beleženja za posamezne komponente.", + "default": { + "label": "Raven beleženja", + "description": "Privzeta globalna podrobnost dnevnikov (debug, info, warning, error)." + }, + "logs": { + "label": "Raven beleženja po procesih", + "description": "Povoženje ravni beleženja za posamezne komponente, da povečaš ali zmanjšaš podrobnost za specifične module." + } + }, + "auth": { + "label": "Avtentikacija", + "description": "Nastavitve avtentikacije in sej, vključno z možnostmi piškotkov in omejevanjem hitrosti zahtev.", + "enabled": { + "label": "Omogoči avtentikacijo", + "description": "Omogoči izvorno avtentikacijo za uporabniški vmesnik Frigate." + }, + "reset_admin_password": { + "label": "Ponastavi geslo skrbnika", + "description": "Če je izbrano (true), se ob zagonu ponastavi geslo skrbnika, novo geslo pa se izpiše v dnevnikih (logs)." + }, + "cookie_name": { + "label": "Ime JWT piškotka", + "description": "Ime piškotka, ki se uporablja za shranjevanje žetona JWT za izvorno avtentikacijo." + }, + "cookie_secure": { + "label": "Varnostna zastavica piškotka (Secure)", + "description": "Nastavi zastavico 'secure' na avtentikacijskem piškotku; to bi moralo biti vklopljeno, ko uporabljaš TLS." + }, + "session_length": { + "label": "Dolžina seje", + "description": "Trajanje seje v sekundah za seje, ki temeljijo na JWT." + }, + "refresh_time": { + "label": "Okno za osvežitev seje", + "description": "Ko seji do poteka manjka toliko sekund, jo sistem samodejno osveži na polno dolžino." + }, + "failed_login_rate_limit": { + "label": "Omejitve neuspelih prijav", + "description": "Pravila za omejevanje hitrosti neuspelih poskusov prijave, da se zmanjša nevarnost napadov s silo (brute-force)." + }, + "trusted_proxies": { + "label": "Zaupanja vredni proksiji", + "description": "Seznam zaupanja vrednih IP naslovov proksi strežnikov, ki se uporabljajo pri določanju IP-ja odjemalca za omejevanje hitrosti." + }, + "hash_iterations": { + "label": "Iteracije zgostitve (Hash)", + "description": "Število iteracij PBKDF2-SHA256, ki se uporabijo pri zgoščevanju uporabniških gesel." + }, + "roles": { + "label": "Preslikava vlog", + "description": "Preslikaj vloge na sezname kamer. Prazen seznam podeli vlogi dostop do vseh kamer." + }, + "admin_first_time_login": { + "label": "Oznaka prve prijave skrbnika", + "description": "Ko je vklopljeno, lahko vmesnik na strani za prijavo prikaže povezavo do pomoči, ki uporabnike obvesti, kako se prijaviti po ponastavitvi skrbniškega gesla. " + } + }, + "database": { + "label": "Zbirka podatkov", + "description": "Nastavitve za zbirko podatkov SQLite, ki jo Frigate uporablja za shranjevanje sledenih objektov in metapodatkov posnetkov.", + "path": { + "label": "Pot do zbirke podatkov", + "description": "Pot v datotečnem sistemu, kjer bo shranjena SQLite datoteka zbirke podatkov Frigate." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "Nastavitve za integrirano storitev pretakanja go2rtc, ki se uporablja za posredovanje in pretvorbo prenosov v živo." + }, + "networking": { + "label": "Omrežje", + "description": "Nastavitve, povezane z omrežjem, kot je omogočanje IPv6 za dostopne točke Frigate.", + "ipv6": { + "label": "Konfiguracija IPv6", + "description": "Specifične nastavitve IPv6 za omrežne storitve Frigate.", + "enabled": { + "label": "Omogoči IPv6", + "description": "Omogoči podporo za IPv6 za storitve Frigate (API in vmesnik), kjer je to mogoče." + } + }, + "listen": { + "label": "Konfiguracija vrat za poslušanje", + "description": "Konfiguracija za notranja in zunanja vrata za poslušanje. To je za napredne uporabnike. V večini primerov je priporočljivo spremeniti sekcijo 'ports' v tvoji datoteki Docker Compose.", + "internal": { + "label": "Notranja vrata", + "description": "Notranja vrata za poslušanje za Frigate (privzeto 5000)." + }, + "external": { + "label": "Zunanja vrata", + "description": "Zunanja vrata za poslušanje za Frigate (privzeto 8971)." + } + } + }, + "proxy": { + "label": "Proksi", + "description": "Nastavitve za integracijo Frigate za povratnim proksijem (reverse proxy), ki posreduje glave z avtenticiranimi uporabniki.", + "header_map": { + "label": "Preslikava glav (Headers)", + "description": "Preslikaj dohodne glave proksija v polja za uporabnika in vlogo v Frigate za avtentikacijo prek proksija.", + "user": { + "label": "Glava uporabnika", + "description": "Glava, ki vsebuje avtenticirano uporabniško ime, ki ga posreduje nadrejeni proksi." + }, + "role": { + "label": "Glava vloge", + "description": "Glava, ki vsebuje vlogo ali skupine avtenticiranega uporabnika iz nadrejenega proksija." + }, + "role_map": { + "label": "Preslikava vlog", + "description": "Preslikaj vrednosti skupin iz nadrejenega sistema v vloge Frigate (npr. preslikaj skupine skrbnikov v vlogo 'admin')." + } + }, + "logout_url": { + "label": "URL za odjavo", + "description": "URL, na katerega naj bodo uporabniki preusmerjeni ob odjavi prek proksija." + }, + "auth_secret": { + "label": "Skrivnost proksija", + "description": "Izbirna skrivnost, ki se preveri glede na glavo 'X-Proxy-Secret' za potrditev zaupanja vrednih proksijev." + }, + "default_role": { + "label": "Privzeta vloga", + "description": "Privzeta vloga, dodeljena uporabnikom, avtenticiranim prek proksija, ko nobena preslikava vlog ne ustreza (admin ali viewer)." + }, + "separator": { + "label": "Ločilni znak", + "description": "Znak, ki se uporablja za ločevanje več vrednosti, podanih v glavah proksija." + } + }, + "telemetry": { + "label": "Telemetrija", + "description": "Sistemska telemetrija in možnosti statistike, vključno s spremljanjem grafičnega procesorja (GPU) in omrežne pasovne širine.", + "network_interfaces": { + "label": "Omrežni vmesniki", + "description": "Seznam predpon imen omrežnih vmesnikov, ki naj se spremljajo za statistiko pasovne širine." + }, + "stats": { + "label": "Sistemska statistika", + "description": "Možnosti za omogočanje/onemogočanje zbiranja različnih sistemskih in GPU statistik.", + "amd_gpu_stats": { + "label": "Statistika AMD GPU", + "description": "Omogoči zbiranje statistike za AMD grafične procesorje, če so prisotni." + }, + "intel_gpu_stats": { + "label": "Statistika Intel GPU", + "description": "Omogoči zbiranje statistike za Intel grafične procesorje, če so prisotni." + }, + "network_bandwidth": { + "label": "Omrežna pasovna širina", + "description": "Omogoči spremljanje omrežne pasovne širine na proces za ffmpeg procese kamer in detektorje (zahteva posebna dovoljenja/capabilities)." + }, + "intel_gpu_device": { + "label": "SR-IOV naprava", + "description": "Identifikator naprave, ki se uporablja pri obravnavi Intelovih GPU-jev kot SR-IOV za popravek GPU statistike." + } + }, + "version_check": { + "label": "Preverjanje različice", + "description": "Omogoči odhodno preverjanje, da ugotoviš, ali je na voljo novejša različica Frigate." + } + }, + "tls": { + "label": "TLS", + "description": "Nastavitve TLS za spletne dostopne točke Frigate (vrata 8971).", + "enabled": { + "label": "Omogoči TLS", + "description": "Omogoči TLS za spletni vmesnik in API Frigate na konfiguriranih vratih TLS." + } + }, + "ui": { + "label": "Uporabniški vmesnik (UI)", + "description": "Nastavitve uporabniškega vmesnika, kot so časovni pas, oblika zapisa časa/datuma in enote.", + "timezone": { + "label": "Časovni pas", + "description": "Izbirni časovni pas za prikaz v vmesniku (če ni nastavljeno, se uporabi lokalni čas brskalnika)." + }, + "time_format": { + "label": "Oblika zapisa časa", + "description": "Oblika časa v vmesniku (brskalnik, 12-urna ali 24-urna)." + }, + "date_style": { + "label": "Slog datuma", + "description": "Slog prikaza datuma v vmesniku (polno, dolgo, srednje, kratko)." + }, + "time_style": { + "label": "Slog časa", + "description": "Slog prikaza časa v vmesniku (polno, dolgo, srednje, kratko)." + }, + "unit_system": { + "label": "Sistem enot", + "description": "Sistem enot za prikaz (metrični ali imperialni), uporabljen v vmesniku in MQTT." + } + }, + "detectors": { + "label": "Strojna oprema detektorjev", + "description": "Konfiguracija za detektorje objektov (CPU, GPU, ONNX zaledja) in specifične nastavitve modelov detektorjev.", + "type": { + "label": "Vrsta detektorja", + "description": "Vrsta detektorja za zaznavanje objektov (npr. 'cpu', 'edgetpu', 'openvino')." + }, + "cpu": { + "label": "CPU", + "description": "Detektor CPU TFLite, ki poganja modele TensorFlow Lite na glavnem procesorju brez strojne pospešitve. Ni priporočljivo.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov, ki se uporabljajo za dodajanje metapodatkov (na primer 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas), ki jo nekateri detektorji uporabljajo za optimizacijo." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja, če jo izbrani detektor zahteva." + }, + "num_threads": { + "label": "Število niti za zaznavanje", + "description": "Število niti, ki se uporabljajo za sklepanje (inference) na procesorju (CPU)." + } + }, + "deepstack": { + "label": "DeepStack", + "description": "Detektor DeepStack/CodeProject.AI, ki pošilja slike v oddaljeni API DeepStack HTTP za sklepanje. Ni priporočljivo.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov (npr. 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas), uporabljen za optimizacijo." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja, če jo izbrani detektor zahteva." + }, + "api_url": { + "label": "URL API-ja DeepStack", + "description": "URL naslov API-ja DeepStack." + }, + "api_timeout": { + "label": "Časovna omejitev API-ja DeepStack (v sekundah)", + "description": "Najdaljši dovoljeni čas za zahtevo na API DeepStack." + }, + "api_key": { + "label": "Ključ API DeepStack (če je zahtevan)", + "description": "Izbirni ključ API za avtenticirane storitve DeepStack." + } + }, + "degirum": { + "label": "DeGirum", + "description": "Detektor DeGirum za poganjanje modelov prek oblaka DeGirum ali lokalnih storitev sklepanja.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas)." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja." + }, + "location": { + "label": "Lokacija sklepanja", + "description": "Lokacija pogona za sklepanje DeGirum (npr. '@cloud', '127.0.0.1')." + }, + "zoo": { + "label": "Zbirka modelov (Model Zoo)", + "description": "Pot ali URL do zbirke modelov DeGirum." + }, + "token": { + "label": "Žeton za DeGirum Cloud", + "description": "Žeton za dostop do oblaka DeGirum." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "Detektor EdgeTPU, ki poganja modele TensorFlow Lite, prevedene za Coral EdgeTPU, z uporabo delegata EdgeTPU.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov (npr. 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas), uporabljen za optimizacijo." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja, če jo izbrani detektor zahteva." + }, + "device": { + "label": "Vrsta naprave", + "description": "Naprava, ki naj se uporabi za sklepanje EdgeTPU (npr. 'usb', 'pci')." + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "Detektor Hailo-8/Hailo-8L, ki uporablja modele HEF in SDK HailoRT za sklepanje na strojni opremi Hailo.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas)." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja, če jo izbrani detektor zahteva." + }, + "device": { + "label": "Vrsta naprave", + "description": "Naprava, ki naj se uporabi za sklepanje Hailo (npr. 'PCIe', 'M.2')." + } + }, + "memryx": { + "label": "MemryX", + "description": "Detektor MemryX MX3, ki poganja prevedene modele DFP na pospeševalnikih MemryX.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas)." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja." + }, + "device": { + "label": "Pot do naprave", + "description": "Naprava, ki naj se uporabi za sklepanje MemryX (npr. 'PCIe')." + } + }, + "onnx": { + "label": "ONNX", + "description": "Detektor ONNX za poganjanje modelov ONNX; uporabil bo razpoložljiva zaledja za pospeševanje (CUDA/ROCm/OpenVINO), če so na voljo.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas)." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja." + }, + "device": { + "label": "Vrsta naprave", + "description": "Naprava, ki naj se uporabi za sklepanje ONNX (npr. 'AUTO', 'CPU', 'GPU')." + } + }, + "openvino": { + "label": "OpenVINO", + "description": "Detektor OpenVINO za procesorje AMD in Intel, grafične procesorje Intel in strojno opremo Intel VPU.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas)." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja." + }, + "device": { + "label": "Vrsta naprave", + "description": "Naprava, ki naj se uporabi za sklepanje OpenVINO (npr. 'CPU', 'GPU', 'NPU')." + } + }, + "rknn": { + "label": "RKNN", + "description": "Detektor RKNN za NPUs Rockchip; poganja prevedene modele RKNN na strojni opremi Rockchip.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas)." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja." + }, + "num_cores": { + "label": "Število jeder NPU", + "description": "Število jeder NPU, ki naj se uporabi (0 za samodejno)." + } + }, + "synaptics": { + "label": "Synaptics", + "description": "Detektor Synaptics NPU za modele v formatu .synap z uporabo SDK Synap na strojni opremi Synaptics.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas)." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja." + } + }, + "teflon_tfl": { + "label": "Teflon", + "description": "Detektor delegata Teflon za TFLite, ki uporablja knjižnico Mesa Teflon za pospeševanje sklepanja na podprtih grafičnih procesorjih.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov (npr. 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas), uporabljen za optimizacijo." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja, če jo izbrani detektor zahteva." + } + }, + "tensorrt": { + "label": "TensorRT", + "description": "Detektor TensorRT za naprave Nvidia Jetson, ki uporablja serializirane pogone TensorRT za pospešeno sklepanje.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke s seznami oznak (labelmap), ki številčne razrede preslika v tekstovne oznake detektorja." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov, ki se združijo v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za dodajanje metapodatkov." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model: 'rgb', 'bgr' ali 'yuv'." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja modela (na primer 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas)." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do datoteke z binarnim modelom detektorja." + }, + "device": { + "label": "Indeks grafične naprave (GPU)", + "description": "Indeks naprave GPU, ki naj se uporabi." + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "Detektor ZMQ IPC, ki preloži sklepanje na zunanji proces prek končne točke ZeroMQ IPC.", + "type": { + "label": "Vrsta" + }, + "model": { + "label": "Konfiguracija modela za specifičen detektor", + "description": "Možnosti konfiguracije modela za določen detektor (pot, vhodna velikost itd.).", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke labelmap." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja (nhwc ali nchw)." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor (rgb, bgr ali yuv)." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov (npr. float32)." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela (ssd, yolox, yolonas)." + } + }, + "model_path": { + "label": "Pot do modela za specifičen detektor", + "description": "Pot do binarne datoteke modela." + }, + "endpoint": { + "label": "Končna točka ZMQ IPC", + "description": "ZMQ končna točka za povezavo." + }, + "request_timeout_ms": { + "label": "Časovna omejitev zahteve ZMQ (v milisekundah)", + "description": "Časovna omejitev za zahteve ZMQ v milisekundah." + }, + "linger_ms": { + "label": "Zadrževanje vtičnice ZMQ (v milisekundah)", + "description": "Obdobje zadrževanja vtičnice v milisekundah." + } + } + }, + "model": { + "label": "Model za zaznavanje", + "description": "Nastavitve za konfiguracijo modela za zaznavanje objektov po meri in njegove vhodne oblike.", + "path": { + "label": "Pot do modela za zaznavanje objektov po meri", + "description": "Pot do datoteke modela po meri (ali plus:// za modele Frigate+)." + }, + "labelmap_path": { + "label": "Pot do labelmap datoteke za detektor po meri", + "description": "Pot do datoteke labelmap, ki številčne razrede preslika v tekstovne oznake." + }, + "width": { + "label": "Vhodna širina modela za zaznavanje", + "description": "Širina vhodnega tenzorja modela v pikslih." + }, + "height": { + "label": "Vhodna višina modela za zaznavanje", + "description": "Višina vhodnega tenzorja modela v pikslih." + }, + "labelmap": { + "label": "Prilagoditev labelmapa", + "description": "Povoženja ali ponovne preslikave vnosov v standardni labelmap." + }, + "attributes_map": { + "label": "Zemljevid oznak objektov in njihovih atributov", + "description": "Preslikava med oznakami objektov in oznakami atributov za metapodatke." + }, + "input_tensor": { + "label": "Oblika vhodnega tenzorja modela", + "description": "Format tenzorja, ki ga model pričakuje: 'nhwc' ali 'nchw'." + }, + "input_pixel_format": { + "label": "Barvni format slikovnih pik vhoda modela", + "description": "Barvni prostor, ki ga pričakuje model (rgb, bgr ali yuv)." + }, + "input_dtype": { + "label": "Tip podatkov vhoda modela (D Type)", + "description": "Tip podatkov vhodnega tenzorja (npr. 'float32')." + }, + "model_type": { + "label": "Vrsta modela za zaznavanje objektov", + "description": "Arhitektura modela detektorja (ssd, yolox, yolonas)." + } + }, + "genai": { + "label": "Konfiguracija generativne umetne inteligence (imenovani ponudniki)", + "description": "Nastavitve za integrirane ponudnike generativne UI, ki se uporabljajo za opise objektov in povzetke pregledov.", + "api_key": { + "label": "Ključ API", + "description": "Ključ API, ki ga zahtevajo nekateri ponudniki (lahko ga nastaviš tudi prek okoljskih spremenljivk)." + }, + "base_url": { + "label": "Osnovni URL", + "description": "Osnovni URL za lastno gostovane ali združljive ponudnike (na primer instanca Ollama)." + }, + "model": { + "label": "Model", + "description": "Model ponudnika, ki naj se uporabi za generiranje opisov ali povzetkov." + }, + "provider": { + "label": "Ponudnik", + "description": "Ponudnik GenAI (na primer: ollama, gemini, openai)." + }, + "roles": { + "label": "Vloge", + "description": "Vloge GenAI (orodja, vid, vdelave); en ponudnik na vlogo." + }, + "provider_options": { + "label": "Možnosti ponudnika", + "description": "Dodatne možnosti, specifične za ponudnika, ki se pošljejo GenAI odjemalcu." + }, + "runtime_options": { + "label": "Možnosti ob izvedbi", + "description": "Možnosti ob izvedbi, ki se pošljejo ponudniku za vsak klic sklepanja." + } + }, + "classification": { + "label": "Klasifikacija objektov", + "description": "Nastavitve za modele klasifikacije, ki se uporabljajo za izboljšanje oznak objektov ali klasifikacijo stanja.", + "bird": { + "label": "Konfiguracija klasifikacije ptic", + "description": "Nastavitve, specifične za modele za klasifikacijo ptic.", + "enabled": { + "label": "Klasifikacija ptic", + "description": "Vklopi ali izklopi klasifikacijo ptic." + }, + "threshold": { + "label": "Najmanjša ocena", + "description": "Najmanjša ocena klasifikacije, potrebna za sprejetje detekcije ptice." + } + }, + "custom": { + "label": "Modeli za klasifikacijo po meri", + "description": "Konfiguracija modelov po meri za zaznavanje objektov ali stanj.", + "enabled": { + "label": "Omogoči model", + "description": "Vklopi ali izklopi model za klasifikacijo po meri." + }, + "name": { + "label": "Ime modela", + "description": "Identifikator za uporabljen model po meri." + }, + "threshold": { + "label": "Prag ocene", + "description": "Prag ocene, uporabljen za spremembo stanja klasifikacije." + }, + "save_attempts": { + "label": "Shrani poskuse", + "description": "Koliko poskusov klasifikacije naj se shrani za prikaz v vmesniku." + }, + "object_config": { + "objects": { + "label": "Klasificiraj objekte", + "description": "Seznam vrst objektov, nad katerimi naj se izvaja klasifikacija." + }, + "classification_type": { + "label": "Vrsta klasifikacije", + "description": "Vrsta uporabljene klasifikacije: 'sub_label' (doda pod-oznako) ali druge podprte vrste." + } + }, + "state_config": { + "cameras": { + "label": "Kamere za klasifikacijo", + "description": "Izrez (crop) in nastavitve za posamezno kamero za klasifikacijo stanja.", + "crop": { + "label": "Izrez za klasifikacijo", + "description": "Koordinate izreza za izvajanje klasifikacije na tej kameri." + } + }, + "motion": { + "label": "Zženi ob gibanju", + "description": "Če je izbrano, zaženi klasifikacijo, ko je zaznano gibanje znotraj določenega izreza." + }, + "interval": { + "label": "Interval klasifikacije", + "description": "Interval (v sekundah) med občasnimi klasifikacijami stanja." + } + } + } + }, + "camera_groups": { + "label": "Skupine kamer", + "description": "Konfiguracija za poimenovane skupine kamer za boljšo organizacijo v vmesniku.", + "cameras": { + "label": "Seznam kamer", + "description": "Seznam imen kamer, vključenih v to skupino." + }, + "icon": { + "label": "Ikona skupine", + "description": "Ikona za predstavitev skupine kamer v vmesniku." + }, + "order": { + "label": "Vrstni red", + "description": "Numerični vrstni red za razvrščanje skupin; večje številke se prikažejo kasneje." + } + }, + "camera_mqtt": { + "label": "MQTT", + "description": "Nastavitve objavljanja slik preko MQTT.", + "enabled": { + "label": "Pošlji sliko", + "description": "Omogoči objavljanje posnetkov objektov na MQTT teme za to kamero." + }, + "timestamp": { + "label": "Dodaj časovno značko", + "description": "Dodaj časovno značko na slike, objavljene na MQTT." + }, + "bounding_box": { + "label": "Dodaj okvir", + "description": "Nariši okvirje objektov na slike, objavljene preko MQTT." + }, + "crop": { + "label": "Obreži sliko", + "description": "Obreži slike za MQTT na okvir zaznanega objekta." + }, + "height": { + "label": "Višina slike", + "description": "Višina (v pikslih) za spreminjanje velikosti slik za MQTT." + }, + "required_zones": { + "label": "Zahtevana območja", + "description": "Območja, v katera mora objekt vstopiti, da se slika objavi na MQTT." + }, + "quality": { + "label": "Kakovost JPEG", + "description": "Kakovost JPEG slik za MQTT (0-100)." + } + }, + "camera_ui": { + "label": "Uporabniški vmesnik kamere", + "description": "Vrstni red prikaza in vidljivost kamere v vmesniku.", + "order": { + "label": "UI vrstni red", + "description": "Številka za razvrščanje kamere v vmesniku; večje številke so nižje na seznamu." + }, + "dashboard": { + "label": "Prikaži v vmesniku", + "description": "Preklopi vidljivost kamere v Frigate vmesniku. Če to izklopiš, boš moral ročno urediti konfiguracijo, da jo spet vidiš." + } + } +} diff --git a/web/public/locales/sl/config/groups.json b/web/public/locales/sl/config/groups.json new file mode 100644 index 0000000..c97fba4 --- /dev/null +++ b/web/public/locales/sl/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "Globalno zaznavanje", + "sensitivity": "Globalna občutljivost" + }, + "cameras": { + "detection": "Zaznavanje", + "sensitivity": "Občutljivost" + } + }, + "timestamp_style": { + "global": { + "appearance": "Globalni videz" + }, + "cameras": { + "appearance": "Videz" + } + }, + "motion": { + "global": { + "sensitivity": "Globalna občutljivost", + "algorithm": "Globalni algoritem" + }, + "cameras": { + "sensitivity": "Občutljivost", + "algorithm": "Algoritem" + } + }, + "snapshots": { + "global": { + "display": "Globalni prikaz" + }, + "cameras": { + "display": "Prikaz" + } + }, + "detect": { + "global": { + "resolution": "Globalna ločljivost", + "tracking": "Globalno sledenje" + }, + "cameras": { + "resolution": "Ločljivost", + "tracking": "Sledenje" + } + }, + "objects": { + "global": { + "tracking": "Globalno sledenje", + "filtering": "Globalno filtriranje" + }, + "cameras": { + "tracking": "Sledenje", + "filtering": "Filtriranje" + } + }, + "record": { + "global": { + "retention": "Globalna hramba", + "events": "Globalni dogodki" + }, + "cameras": { + "retention": "Hramba", + "events": "Dogodki" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Argumenti FFmpeg za specifično kamero" + } + } +} diff --git a/web/public/locales/sl/config/validation.json b/web/public/locales/sl/config/validation.json new file mode 100644 index 0000000..75f5ad3 --- /dev/null +++ b/web/public/locales/sl/config/validation.json @@ -0,0 +1,32 @@ +{ + "minimum": "Vrednost mora biti vsaj {{limit}}", + "maximum": "Vrednost je lahko največ {{limit}}", + "exclusiveMinimum": "Vrednost mora biti večja od {{limit}}", + "exclusiveMaximum": "Vrednost mora biti manjša od {{limit}}", + "minLength": "Vsebovati mora vsaj {{limit}} znakov", + "maxLength": "Vsebovati mora največ {{limit}} znakov", + "minItems": "Vsebovati mora vsaj {{limit}} elementov", + "maxItems": "Vsebovati mora največ {{limit}} elementov", + "pattern": "Neveljaven format", + "required": "To polje je obvezno", + "type": "Neveljavna vrsta vrednosti", + "enum": "Izbrati moraš eno izmed dovoljenih vrednosti", + "const": "Vrednost se ne ujema s pričakovano konstanto", + "uniqueItems": "Vsi elementi morajo biti edinstveni", + "format": "Neveljaven format", + "additionalProperties": "Neznana lastnost ni dovoljena", + "oneOf": "Ujemati se mora z natanko eno od dovoljenih shem", + "anyOf": "Ujemati se mora z vsaj eno od dovoljenih shem", + "proxy": { + "header_map": { + "roleHeaderRequired": "Glava vloge (role header) je obvezna, ko so nastavljene preslikave vlog." + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "Vsaka vloga je lahko dodeljena le enemu vhodnemu toku.", + "detectRequired": "Vsaj enemu vhodnemu toku mora biti dodeljena vloga 'detect' (zaznavanje).", + "hwaccelDetectOnly": "Argumente za strojno pospeševanje lahko določiš samo za vhodni tok z vlogo 'detect'." + } + } +} diff --git a/web/public/locales/sl/objects.json b/web/public/locales/sl/objects.json new file mode 100644 index 0000000..d0ff728 --- /dev/null +++ b/web/public/locales/sl/objects.json @@ -0,0 +1,120 @@ +{ + "cat": "Mačka", + "sheep": "Ovca", + "bird": "Ptica", + "animal": "Žival", + "goat": "Koza", + "horse": "Konj", + "dog": "Pes", + "skis": "Smuči", + "surfboard": "Surfaška deska", + "laptop": "Prenosnik", + "tennis_racket": "Tenis lopar", + "deer": "Srna/Jelen", + "waste_bin": "Koš za smeti", + "skateboard": "Skejtbord", + "baseball_glove": "Baseball rokavica", + "snowboard": "Snežna deska", + "bottle": "Steklenica", + "squirrel": "Veverica", + "raccoon": "Rakovica", + "robot_lawnmower": "Robotska kosilnica", + "person": "Oseba", + "bicycle": "Kolo", + "car": "Avto", + "motorcycle": "Motor", + "airplane": "Letalo", + "bus": "Avtobus", + "train": "Vlak", + "boat": "Čoln", + "traffic_light": "Semafor", + "fire_hydrant": "Hidrant", + "street_sign": "Prometni znak", + "stop_sign": "Stop znak", + "parking_meter": "Parkirna ura", + "bench": "Klop", + "cow": "Krava", + "elephant": "Slon", + "bear": "Medved", + "zebra": "Zebra", + "giraffe": "Žirafa", + "hat": "Kapa/Klobuk", + "backpack": "Nahrbtnik", + "umbrella": "Dežnik", + "shoe": "Čevelj", + "eye_glasses": "Očala", + "handbag": "Torbica", + "tie": "Kravata", + "suitcase": "Kovček", + "frisbee": "Frisbi", + "sports_ball": "Športna žoga", + "kite": "Zmaj", + "baseball_bat": "Baseball kij", + "plate": "Tanjir", + "wine_glass": "Vinski kozarec", + "cup": "Skodelica", + "fork": "Vilica", + "knife": "Nož", + "spoon": "Žlica", + "bowl": "Skleda", + "banana": "Banana", + "apple": "Jabolko", + "sandwich": "Sendvič", + "orange": "Pomaranča", + "broccoli": "Brokoli", + "carrot": "Korenje", + "hot_dog": "Hot dog", + "pizza": "Pica", + "donut": "Krof", + "cake": "Torta", + "chair": "Stol", + "couch": "Kavč", + "potted_plant": "Rastlina v lončku", + "bed": "Postelja", + "mirror": "Ogledalo", + "dining_table": "Jedilna miza", + "window": "Okno", + "desk": "Pisalna miza", + "toilet": "Stranišče", + "door": "Vrata", + "tv": "TV", + "mouse": "Miška", + "remote": "Daljinec", + "keyboard": "Tipkovnica", + "cell_phone": "Telefon", + "microwave": "Mikrovalovka", + "oven": "Pečica", + "toaster": "Opekač", + "sink": "Umivalnik", + "refrigerator": "Hladilnik", + "blender": "Mešalnik", + "book": "Knjiga", + "clock": "Ura", + "vase": "Vaza", + "scissors": "Škarje", + "teddy_bear": "Medvedek", + "hair_dryer": "Fen", + "toothbrush": "Zobna ščetka", + "hair_brush": "Krtača za lase", + "vehicle": "Vozilo", + "bark": "Lubje", + "fox": "Lisica", + "rabbit": "Zajec", + "on_demand": "Na zahtevo", + "face": "Obraz", + "license_plate": "Registrska tablica", + "package": "Paket", + "bbq_grill": "Žar", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/web/public/locales/sl/views/chat.json b/web/public/locales/sl/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sl/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sl/views/classificationModel.json b/web/public/locales/sl/views/classificationModel.json new file mode 100644 index 0000000..514c3af --- /dev/null +++ b/web/public/locales/sl/views/classificationModel.json @@ -0,0 +1,203 @@ +{ + "description": { + "invalidName": "Neveljavno ime. Imena lahko vsebujejo le črke, številke, presledke, opuščaje, podčrtaje in vezaje." + }, + "categories": "Razredi", + "createCategory": { + "new": "Ustvari nov razred" + }, + "button": { + "renameCategory": "Preimenuj razred", + "deleteCategory": "Izbriši razred", + "deleteImages": "Izbriši slike", + "trainModel": "Nauči model", + "deleteClassificationAttempts": "Izbriši slike klasifikacij", + "addClassification": "Dodaj klasifikacijo", + "deleteModels": "Izbriši modele", + "editModel": "Uredi model" + }, + "toast": { + "success": { + "deletedCategory_one": "Razred izbrisan", + "deletedCategory_two": "", + "deletedCategory_few": "", + "deletedCategory_other": "", + "deletedImage_one": "Slike izbrisane", + "deletedImage_two": "", + "deletedImage_few": "", + "deletedImage_other": "", + "trainedModel": "Model uspešno naučen.", + "trainingModel": "Učenje modela se je uspešno začelo.", + "deletedModel_one": "Model uspešno izbrisan", + "deletedModel_two": "Uspešno izbrisana {{count}} modela", + "deletedModel_few": "Uspešno izbrisani {{count}} modeli", + "deletedModel_other": "Uspešno izbrisanih {{count}} modelov", + "categorizedImage": "Slika uspešno klasificirana", + "updatedModel": "Konfiguracija modela uspešno posodobljena", + "renamedCategory": "Razred uspešno preimenovan v {{name}}" + }, + "error": { + "deleteImageFailed": "Brisanje ni uspelo: {{errorMessage}}", + "deleteCategoryFailed": "Brisanje razreda ni uspelo: {{errorMessage}}", + "trainingFailed": "Učenje modela ni uspelo. Preveri Frigate dnevnike (logs) za podrobnosti.", + "deleteModelFailed": "Brisanje modela ni uspelo: {{errorMessage}}", + "categorizeFailed": "Klasifikacija slike ni uspela: {{errorMessage}}", + "trainingFailedToStart": "Začetek učenja modela ni uspel: {{errorMessage}}", + "updateModelFailed": "Posodobitev modela ni uspela: {{errorMessage}}", + "renameCategoryFailed": "Preimenovanje razreda ni uspelo: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Izbriši razred", + "desc": "Ali si prepričan, da želiš izbrisati razred {{name}}? To bo trajno izbrisalo vse povezane slike in zahtevalo ponovno učenje modela.", + "minClassesTitle": "Razreda ni mogoče izbrisati", + "minClassesDesc": "Klasifikacijski model mora imeti vsaj 2 razreda. Dodaj drug razred, preden izbrišeš tega." + }, + "deleteTrainImages": { + "title": "Izbriši slike za učenje", + "desc_one": "Ali si prepričan, da želiš izbrisati {{count}} sliko?", + "desc_two": "Ali si prepričan, da želiš izbrisati {{count}} sliki?", + "desc_few": "Ali si prepričan, da želiš izbrisati {{count}} slike?", + "desc_other": "Ali si prepričan, da želiš izbrisati {{count}} slik? Tega dejanja ni mogoče razveljaviti." + }, + "renameCategory": { + "title": "Preimenuj razred", + "desc": "Vnesi novo ime za {{name}}. Za uveljavitev spremembe imena boš moral ponovno naučiti model." + }, + "train": { + "title": "Zadnje klasifikacije", + "aria": "Izberi zadnje klasifikacije", + "titleShort": "Zadnje" + }, + "categorizeImageAs": "Klasificiraj sliko kot:", + "categorizeImage": "Klasificiraj sliko", + "noModels": { + "object": { + "title": "Ni modelov za klasifikacijo objektov", + "description": "Ustvari model po meri za klasifikacijo zaznanih objektov.", + "buttonText": "Ustvari model objekta" + }, + "state": { + "title": "Ni modelov za klasifikacijo stanja", + "description": "Ustvari model po meri za spremljanje in klasifikacijo sprememb stanja na določenih območjih kamere.", + "buttonText": "Ustvari model stanja" + } + }, + "documentTitle": "Modeli za klasifikacijo - Frigate", + "details": { + "scoreInfo": "Ocena predstavlja povprečno zaupanje klasifikacije vseh zaznav tega objekta.", + "none": "Brez", + "unknown": "Neznan" + }, + "tooltip": { + "trainingInProgress": "Učenje modela trenutno poteka", + "noNewImages": "Ni novih slik za učenje. Najprej klasificiraj več slik v naboru podatkov.", + "noChanges": "Od zadnjega učenja v naboru podatkov ni bilo sprememb.", + "modelNotReady": "Model ni pripravljen na učenje" + }, + "deleteModel": { + "title": "Izbriši klasifikacijski model", + "single": "Ali si prepričan, da želiš izbrisati {{name}}? To bo trajno izbrisalo vse povezane podatke, vključno s slikami in podatki za učenje. Tega dejanja ni mogoče razveljaviti.", + "desc_one": "Ali si prepričan, da želiš izbrisati {{count}} model?", + "desc_two": "Ali si prepričan, da želiš izbrisati {{count}} modela?", + "desc_few": "Ali si prepričan, da želiš izbrisati {{count}} modele?", + "desc_other": "Ali si prepričan, da želiš izbrisati {{count}} modelov? To bo trajno izbrisalo vse povezane podatke. Tega dejanja ni mogoče razveljaviti." + }, + "edit": { + "title": "Uredi klasifikacijski model", + "descriptionState": "Uredi razrede za ta model klasifikacije stanja. Spremembe bodo zahtevale ponovno učenje modela.", + "descriptionObject": "Uredi vrsto objekta in vrsto klasifikacije za ta model klasifikacije objektov.", + "stateClassesInfo": "Opomba: Spreminjanje razredov stanja zahteva ponovno učenje modela s posodobljenimi razredi." + }, + "deleteDatasetImages": { + "title": "Izbriši slike iz nabora podatkov", + "desc_one": "Ali si prepričan, da želiš izbrisati {{count}} sliko iz {{dataset}}?", + "desc_two": "Ali si prepričan, da želiš izbrisati {{count}} sliki iz {{dataset}}?", + "desc_few": "Ali si prepričan, da želiš izbrisati {{count}} slike iz {{dataset}}?", + "desc_other": "Ali si prepričan, da želiš izbrisati {{count}} slik iz {{dataset}}? Tega dejanja ni mogoče razveljaviti in zahtevalo bo ponovno učenje modela." + }, + "menu": { + "objects": "Objekti", + "states": "Stanja" + }, + "wizard": { + "title": "Ustvari novo klasifikacijo", + "steps": { + "nameAndDefine": "Poimenuj in določi", + "stateArea": "Območje stanja", + "chooseExamples": "Izberi primere" + }, + "step1": { + "description": "Modeli stanja spremljajo fiksna območja kamere za spremembe (npr. odprta/zaprta vrata). Modeli objektov dodajo klasifikacije zaznanim objektom (npr. znane živali, dostavljavci itd.).", + "name": "Ime", + "namePlaceholder": "Vnesi ime modela...", + "type": "Vrsta", + "typeState": "Stanje", + "typeObject": "Objekt", + "objectLabel": "Oznaka objekta", + "objectLabelPlaceholder": "Izberi vrsto objekta...", + "classificationType": "Vrsta klasifikacije", + "classificationTypeTip": "Spoznaj vrste klasifikacij", + "classificationTypeDesc": "Pod-oznake dodajo dodatno besedilo k oznaki objekta (npr. 'Oseba: dostavljavec'). Atributi so metapodatki, ki jih je mogoče iskati in se shranjujejo ločeno.", + "classificationSubLabel": "Pod-oznaka", + "classificationAttribute": "Atribut", + "classes": "Razredi", + "states": "Stanja", + "classesTip": "Spoznaj razrede", + "classesStateDesc": "Določi različna stanja, v katerih je lahko območje tvoje kamere. Na primer: 'odprto' in 'zaprto' za garažna vrata.", + "classesObjectDesc": "Določi različne kategorije, v katere naj se klasificirajo zaznani objekti. Na primer: 'dostavljavec', 'stanovalec', 'neznanec' za klasifikacijo oseb.", + "classPlaceholder": "Vnesi ime razreda...", + "errors": { + "nameRequired": "Ime modela je obvezno", + "nameLength": "Ime modela mora imeti 64 znakov ali manj", + "nameOnlyNumbers": "Ime modela ne sme vsebovati samo številk", + "classRequired": "Zahtevan je vsaj 1 razred", + "classesUnique": "Imena razredov morajo biti edinstvena", + "noneNotAllowed": "Razred 'none' ni dovoljen", + "stateRequiresTwoClasses": "Modeli stanja zahtevajo vsaj 2 razreda", + "objectLabelRequired": "Prosim, izberi oznako objekta", + "objectTypeRequired": "Prosim, izberi vrsto klasifikacije" + } + }, + "step2": { + "description": "Izberi kamere in določi območje spremljanja za vsako kamero. Model bo klasificiral stanje teh območij.", + "cameras": "Kamere", + "selectCamera": "Izberi kamero", + "noCameras": "Klikni +, da dodaš kamere", + "selectCameraPrompt": "Izberi kamero s seznama, da določiš njeno območje spremljanja" + }, + "step3": { + "selectImagesPrompt": "Izberi vse slike z: {{className}}", + "selectImagesDescription": "Klikni na slike, da jih izbereš. Klikni Nadaljuj, ko končaš s tem razredom.", + "allImagesRequired_one": "Prosim, klasificiraj vse slike. Preostala je še {{count}} slika.", + "allImagesRequired_two": "Prosim, klasificiraj vse slike. Preostali sta še {{count}} sliki.", + "allImagesRequired_few": "Prosim, klasificiraj vse slike. Preostale so še {{count}} slike.", + "allImagesRequired_other": "Prosim, klasificiraj vse slike. Preostalo je še {{count}} slik.", + "generating": { + "title": "Generiranje vzorčnih slik", + "description": "Frigate pridobiva reprezentativne slike iz tvojih posnetkov. To lahko traja trenutek..." + }, + "training": { + "title": "Učenje modela", + "description": "Tvoj model se uči v ozadju. To pogovorno okno lahko zapreš; model bo začel delovati takoj, ko bo učenje končano." + }, + "retryGenerate": "Poskusi ponovno generirati", + "noImages": "Vzorčne slike niso bile generirane", + "classifying": "Klasificiranje in učenje...", + "trainingStarted": "Učenje se je uspešno začelo", + "modelCreated": "Model uspešno ustvarjen. Uporabi pogled Zadnje klasifikacije, da dodaš slike za manjkajoča stanja, nato nauči model.", + "errors": { + "noCameras": "Ni nastavljenih kamer", + "noObjectLabel": "Oznaka objekta ni izbrana", + "generateFailed": "Generiranje primerov ni uspelo: {{error}}", + "generationFailed": "Generiranje ni uspelo. Prosim, poskusi ponovno.", + "classifyFailed": "Klasifikacija slik ni uspela: {{error}}" + }, + "generateSuccess": "Vzorčne slike so bile uspešno generirane", + "missingStatesWarning": { + "title": "Manjkajoči primeri stanj", + "description": "Za najboljše rezultate je priporočljivo izbrati primere za vsa stanja. Lahko nadaljuješ brez izbire vseh stanj, vendar model ne bo naučen, dokler vsa stanja ne bodo imela slik. Po nadaljevanju uporabi pogled Zadnje klasifikacije za klasifikacijo slik za manjkajoča stanja, nato nauči model." + } + } + } +} diff --git a/web/public/locales/sl/views/configEditor.json b/web/public/locales/sl/views/configEditor.json new file mode 100644 index 0000000..129a70e --- /dev/null +++ b/web/public/locales/sl/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Urejevalnik konfiguracije - Frigate", + "configEditor": "Urejevalnik konfiguracije", + "copyConfig": "Kopiraj konfiguracijo", + "saveAndRestart": "Shrani in ponovno zaženi", + "saveOnly": "Samo shrani", + "toast": { + "success": { + "copyToClipboard": "Konfiguracija je kopirana v odložišče." + }, + "error": { + "savingError": "Napaka pri shranjevanju konfiguracije" + } + }, + "confirm": "Želiš zapustiti brez shranjevanja?", + "safeConfigEditor": "Urejevalnik konfiguracije (Varni način)", + "safeModeDescription": "Frigate je v varnem načinu zaradi napake pri validaciji konfiguracije." +} diff --git a/web/public/locales/sl/views/events.json b/web/public/locales/sl/views/events.json new file mode 100644 index 0000000..cb39e1b --- /dev/null +++ b/web/public/locales/sl/views/events.json @@ -0,0 +1,88 @@ +{ + "detected": "zaznano", + "events": { + "noFoundForTimePeriod": "Za to obdobje ni najdenih dogodkov.", + "label": "Dogodki", + "aria": "Izberi dogodke" + }, + "allCameras": "Vse kamere", + "empty": { + "motion": "Ni najdenih podatkov o gibanju", + "alert": "Ni opozoril za pregled", + "detection": "Ni zaznav za pregled", + "recordingsDisabled": { + "title": "Snemanje mora biti omogočeno", + "description": "Postavke za pregled se lahko ustvarijo le za kamere, ki imajo vklopljeno snemanje." + } + }, + "recordings": { + "documentTitle": "Posnetki - Frigate" + }, + "camera": "Kamera", + "documentTitle": "Pregled - Frigate", + "alerts": "Opozorila", + "detections": "Zaznave", + "motion": { + "label": "Gibanje", + "only": "Samo gibanje" + }, + "timeline": "Časovnica", + "timeline.aria": "Izberi časovnico", + "calendarFilter": { + "last24Hours": "Zadnjih 24 ur" + }, + "markAsReviewed": "Označi kot pregledano", + "markTheseItemsAsReviewed": "Označi te postavke kot pregledane", + "newReviewItems": { + "label": "Prikaži nove postavke za pregled", + "button": "Nove postavke za pregled" + }, + "selected_one": "{{count}} izbran", + "selected_other": "{{count}} izbranih", + "zoomIn": "Povečaj", + "zoomOut": "Pomanjšaj", + "detail": { + "label": "Podrobnosti", + "noDataFound": "Ni podrobnih podatkov za pregled", + "aria": "Preklopi podrobni pogled", + "trackedObject_one": "{{count}} objekt", + "trackedObject_other": "{{count}} objektov", + "noObjectDetailData": "Podatki o podrobnostih objekta niso na voljo.", + "settings": "Nastavitve podrobnega pogleda", + "alwaysExpandActive": { + "title": "Vedno razširi aktivno", + "desc": "Vedno razširi podrobnosti objekta za trenutno aktivno postavko pregleda, ko so na voljo." + } + }, + "objectTrack": { + "trackedPoint": "Sledena točka", + "clickToSeek": "Klikni za skok na ta čas" + }, + "select_all": "Vse", + "normalActivity": "Običajno", + "needsReview": "Potrebno pregleda", + "securityConcern": "Varnostni pomislek", + "motionSearch": { + "menuItem": "Iskanje gibanja", + "openMenu": "Možnosti kamere" + }, + "motionPreviews": { + "menuItem": "Ogled predogledov gibanja", + "title": "Predogledi gibanja: {{camera}}", + "mobileSettingsTitle": "Nastavitve predogleda gibanja", + "mobileSettingsDesc": "Prilagodi hitrost predvajanja, zatemnitev in izberi datum za pregled posnetkov s samim gibanjem.", + "dim": "Zatemnitev", + "dimAria": "Prilagodi intenzivnost zatemnitve", + "dimDesc": "Povečaj zatemnitev za boljšo vidnost območja gibanja.", + "speed": "Hitrost", + "speedAria": "Izberi hitrost predvajanja predogleda", + "speedDesc": "Izberi, kako hitro naj se predvajajo predogledi.", + "back": "Nazaj", + "empty": "Predogledi niso na voljo", + "noPreview": "Predogled ni na voljo", + "seekAria": "Skoči na predvajalniku {{camera}} na čas {{time}}", + "filter": "Filter", + "filterDesc": "Izberi območja, da prikažeš le posnetke z gibanjem v teh regijah.", + "filterClear": "Počisti" + } +} diff --git a/web/public/locales/sl/views/explore.json b/web/public/locales/sl/views/explore.json new file mode 100644 index 0000000..0d9b727 --- /dev/null +++ b/web/public/locales/sl/views/explore.json @@ -0,0 +1,310 @@ +{ + "exploreIsUnavailable": { + "title": "Raziskovanje ni na voljo", + "downloadingModels": { + "setup": { + "visionModel": "Vidni model", + "visionModelFeatureExtractor": "Ekstraktor lastnosti vidnega modela", + "textModel": "Besedilni model", + "textTokenizer": "Besedilni razčlenjevalnik (tokenizer)" + }, + "context": "Frigate prenaša potrebne modele vdelav za podporo funkcije semantičnega iskanja. To lahko traja nekaj minut, odvisno od hitrosti tvoje omrežne povezave.", + "tips": { + "context": "Morda boš želel ponovno indeksirati vdelave sledenih objektov, ko bodo modeli preneseni.", + "documentation": "Preberi dokumentacijo" + }, + "error": "Prišlo je do napake. Preveri Frigate dnevnike (logs)." + }, + "embeddingsReindexing": { + "step": { + "descriptionsEmbedded": "Vdelani opisi: ", + "trackedObjectsProcessed": "Obdelani sledeni objekti: ", + "thumbnailsEmbedded": "Vdelane sličice: " + }, + "context": "Raziskovanje bo na voljo, ko se zaključi ponovno indeksiranje vdelav sledenih objektov.", + "startingUp": "Zagon…", + "estimatedTime": "Predviden preostali čas:", + "finishingShortly": "Kmalu bo končano" + } + }, + "documentTitle": "Raziskovanje - Frigate", + "generativeAI": "Generativna UI", + "exploreMore": "Razišči več objektov vrste {{label}}", + "details": { + "button": { + "regenerate": { + "label": "Ponovno generiraj opis sledenega objekta", + "title": "Ponovno generiraj" + }, + "findSimilar": "Najdi podobne" + }, + "camera": "Kamera", + "estimatedSpeed": "Ocenjena hitrost", + "description": { + "placeholder": "Opis sledenega objekta", + "label": "Opis", + "aiTips": "Frigate ne bo zahteval opisa od tvojega ponudnika generativne UI, dokler se življenjski cikel sledenega objekta ne zaključi." + }, + "recognizedLicensePlate": "Prepoznana registrska tablica", + "objects": "Objekti", + "zones": "Območja", + "timestamp": "Časovna značka", + "item": { + "button": { + "share": "Deli to postavko za pregled", + "viewInExplore": "Ogled v Raziskovanju" + }, + "tips": { + "hasMissingObjects": "Prilagodi svojo konfiguracijo, če želiš, da Frigate shranjuje sledene objekte za naslednje oznake: {{objects}}", + "mismatch_one": "{{count}} nedostopen objekt je bil zaznan in vključen v to postavko. Ti objekti niso izpolnili pogojev za opozorilo ali zaznavo ali pa so bili že izbrisani.", + "mismatch_two": "{{count}} nedostopna objekta sta bila zaznana in vključena v to postavko.", + "mismatch_few": "{{count}} nedostopni objekti so bili zaznani in vključeni v to postavko.", + "mismatch_other": "{{count}} nedostopnih objektov je bilo zaznanih in vključenih v to postavko." + }, + "toast": { + "success": { + "regenerate": "Zahtevan je nov opis od ponudnika {{provider}}. Odvisno od hitrosti tvojega ponudnika lahko generiranje traja nekaj časa.", + "updatedSublabel": "Pod-oznaka uspešno posodobljena.", + "updatedLPR": "Registrska tablica uspešno posodobljena.", + "audioTranscription": "Zahteva za transkripcijo zvoka je bila uspešna. Odvisno od hitrosti tvojega strežnika lahko transkripcija traja nekaj časa.", + "updatedAttributes": "Atributi uspešno posodobljeni." + }, + "error": { + "regenerate": "Priklic ponudnika {{provider}} za nov opis ni uspel: {{errorMessage}}", + "updatedSublabelFailed": "Posodobitev pod-oznake ni uspela: {{errorMessage}}", + "updatedLPRFailed": "Posodobitev registrske tablice ni uspela: {{errorMessage}}", + "audioTranscription": "Zahteva za transkripcijo zvoka ni uspela: {{errorMessage}}", + "updatedAttributesFailed": "Posodobitev atributov ni uspela: {{errorMessage}}" + } + }, + "title": "Podrobnosti postavke za pregled", + "desc": "Podrobnosti postavke za pregled" + }, + "label": "Oznaka", + "editSubLabel": { + "title": "Uredi pod-oznako", + "desc": "Vnesi novo pod-oznako za ta {{label}}", + "descNoLabel": "Vnesi novo pod-oznako za ta sledeni objekt" + }, + "editLPR": { + "title": "Uredi registrsko tablico", + "desc": "Vnesi novo vrednost registrske tablice za ta {{label}}", + "descNoLabel": "Vnesi novo vrednost registrske tablice za ta sledeni objekt" + }, + "snapshotScore": { + "label": "Ocena posnetka" + }, + "topScore": { + "label": "Najvišja ocena", + "info": "Najvišja ocena je najvišja mediana ocene za sledeni objekt, zato se lahko razlikuje od ocene na sličici rezultatov iskanja." + }, + "expandRegenerationMenu": "Razširi meni za ponovno generiranje", + "tips": { + "descriptionSaved": "Opis uspešno shranjen", + "saveDescriptionFailed": "Posodobitev opisa ni uspela: {{errorMessage}}" + }, + "editAttributes": { + "title": "Uredi atribute", + "desc": "Izberi atribute klasifikacije za ta {{label}}" + }, + "score": { + "label": "Ocena" + }, + "attributes": "Atributi klasifikacije", + "regenerateFromSnapshot": "Generiraj iz posnetka", + "regenerateFromThumbnails": "Generiraj iz sličic", + "title": { + "label": "Naslov" + } + }, + "itemMenu": { + "findSimilar": { + "aria": "Najdi podobne sledene objekte", + "label": "Najdi podobne" + }, + "submitToPlus": { + "label": "Pošlji v Frigate+", + "aria": "Pošlji v Frigate Plus" + }, + "viewInHistory": { + "label": "Ogled v zgodovini", + "aria": "Ogled v zgodovini" + }, + "deleteTrackedObject": { + "label": "Izbriši ta sledeni objekt" + }, + "viewObjectLifecycle": { + "aria": "Pokaži življenjski cikel predmeta", + "label": "Poglej življenjski cikel predmeta" + }, + "downloadVideo": { + "label": "Prenesi video", + "aria": "Prenesi video" + }, + "downloadSnapshot": { + "label": "Prenesi posnetek", + "aria": "Prenesi posnetek" + }, + "addTrigger": { + "label": "Dodaj sprožilec", + "aria": "Dodaj sprožilec za ta sledeni objekt" + }, + "audioTranscription": { + "label": "Transkribiraj", + "aria": "Zahtevaj transkripcijo zvoka" + }, + "downloadCleanSnapshot": { + "label": "Prenesi čisti posnetek", + "aria": "Prenesi čisti posnetek" + }, + "viewTrackingDetails": { + "label": "Ogled podrobnosti sledenja", + "aria": "Prikaži podrobnosti sledenja" + }, + "showObjectDetails": { + "label": "Prikaži pot objekta" + }, + "hideObjectDetails": { + "label": "Skrij pot objekta" + }, + "debugReplay": { + "label": "Ponovno predvajanje za razhroščevanje", + "aria": "Ogled tega sledenega objekta v pogledu za razhroščevanje" + } + }, + "dialog": { + "confirmDelete": { + "title": "Potrdi brisanje", + "desc": "Brisanje tega sledenega objekta bo odstranilo posnetek, vse shranjene vdelave in morebitne povezane vpise podrobnosti sledenja. Zabeleženi posnetki tega objekta v pogledu Zgodovina NE bodo izbrisani.

    Ali si prepričan, da želiš nadaljevati?" + } + }, + "trackedObjectDetails": "Podrobnosti sledenega objekta", + "type": { + "details": "podrobnosti", + "snapshot": "posnetek", + "video": "video", + "object_lifecycle": "življenjski cikel objekta", + "thumbnail": "sličica", + "tracking_details": "podrobnosti sledenja" + }, + "objectLifecycle": { + "title": "Življenjski Cikel Objekta", + "noImageFound": "Za ta čas ni bila najdena nobena slika.", + "createObjectMask": "Ustvarite Masko Objekta", + "adjustAnnotationSettings": "Prilagodi nastavitve opomb", + "scrollViewTips": "Pomaknite se, da si ogledate pomembne trenutke življenjskega cikla tega predmeta.", + "count": "{{first}} od {{second}}", + "trackedPoint": "Sledena točka", + "lifecycleItemDesc": { + "visible": "{{label}} zaznan", + "entered_zone": "{{label}} je vstopil/a v {{zones}}", + "active": "{{label}} je postal aktiven", + "stationary": "{{label}} je postal nepremičen", + "attribute": { + "faceOrLicense_plate": "{{attribute}} je bil zaznan za {{label}}", + "other": "{{label}} zaznan kot {{attribute}}" + }, + "gone": "{{label}} levo", + "heard": "{{label}} slišano", + "external": "{{label}} zaznan", + "header": { + "zones": "Cone", + "ratio": "Razmerje", + "area": "Območje" + } + }, + "annotationSettings": { + "title": "Nastavitve Anotacij", + "showAllZones": { + "title": "Prikaži Vse Cone", + "desc": "Vedno prikaži območja na okvirjih, kjer so predmeti vstopili v območje." + }, + "offset": { + "label": "Anotacijski Odmik", + "documentation": "Preberi dokumentacijo ", + "millisecondsToOffset": "Odmik zaznanih anotacij v milisekundah. Privzeto: 0", + "tips": "NASVET: Predstavljajte si posnetek dogodka, v katerem oseba hodi od leve proti desni. Če je okvir dogodka na časovnici preveč levo od osebe, je treba vrednost zmanjšati. Podobno je treba vrednost povečati, če oseba hodi od leve proti desni in je okvir preveč pred njo.", + "toast": { + "success": "Odmik anotacij za {{camera}} je bil shranjen v konfiguracijsko datoteko. Znova zaženite Frigate, da uveljavite spremembe." + } + } + }, + "carousel": { + "previous": "Prejšnji diapozitiv", + "next": "Naslednji diapozitiv" + }, + "autoTrackingTips": "Položaji okvirjev bodo za kamere s samodejnim sledenjem netočni." + }, + "noTrackedObjects": "Ni najdenih sledenih objektov", + "fetchingTrackedObjectsFailed": "Napaka pri pridobivanju sledenih objektov: {{errorMessage}}", + "searchResult": { + "tooltip": "Ujemanje {{type}} pri {{confidence}} %", + "deleteTrackedObject": { + "toast": { + "success": "Sledeni objekt uspešno izbrisan.", + "error": "Brisanje sledenega objekta ni uspelo: {{errorMessage}}" + } + }, + "previousTrackedObject": "Prejšnji sledeni objekt", + "nextTrackedObject": "Naslednji sledeni objekt" + }, + "trackingDetails": { + "title": "Podrobnosti sledenja", + "noImageFound": "Za to časovno značko ni bila najdena nobena slika.", + "createObjectMask": "Ustvari masko objekta", + "adjustAnnotationSettings": "Prilagodi nastavitve anotacij", + "scrollViewTips": "Klikni za ogled pomembnih trenutkov v življenjskem ciklu tega objekta.", + "autoTrackingTips": "Položaji uokvirjanja (bounding box) bodo nenatančni pri kamerah s samodejnim sledenjem.", + "count": "{{first}} od {{second}}", + "trackedPoint": "Sledena točka", + "lifecycleItemDesc": { + "visible": "{{label}} zaznan", + "entered_zone": "{{label}} je vstopil v {{zones}}", + "active": "{{label}} je postal aktiven", + "stationary": "{{label}} je postal nepremičen", + "attribute": { + "faceOrLicense_plate": "{{attribute}} zaznan za {{label}}", + "other": "{{label}} prepoznan kot {{attribute}}" + }, + "gone": "{{label}} je odšel", + "heard": "{{label}} slišan", + "external": "{{label}} zaznan", + "header": { + "zones": "Območja", + "ratio": "Razmerje", + "area": "Površina", + "score": "Ocena" + } + }, + "annotationSettings": { + "title": "Nastavitve anotacij", + "showAllZones": { + "title": "Prikaži vsa območja", + "desc": "Vedno prikaži območja na okvirjih, kjer so objekti vstopili v območje." + }, + "offset": { + "label": "Odmik anotacij", + "desc": "Ti podatki prihajajo iz vira za zaznavanje tvoje kamere, vendar so prekrivni na slikah iz vira za snemanje. Malo verjetno je, da sta tokova popolnoma usklajena. Zato se okvirji in posnetek ne bodo popolnoma ujemali. S to nastavitvijo lahko zamakneš anotacije naprej ali nazaj v času za boljšo poravnavo s posnetkom.", + "millisecondsToOffset": "Milisekunde odmika za anotacije zaznavanja. Privzeto: 0", + "tips": "Zmanjšaj vrednost, če video predvajanje prehiteva okvirje in točke poti, ter jo povečaj, če video predvajanje zaostaja za njimi. Vrednost je lahko negativna.", + "toast": { + "success": "Odmik anotacij za {{camera}} je shranjen v konfiguracijo." + } + } + }, + "carousel": { + "previous": "Prejšnja stran", + "next": "Naslednja stran" + } + }, + "trackedObjectsCount_one": "{{count}} sledeni objekt ", + "trackedObjectsCount_two": "{{count}} sledena objekta ", + "trackedObjectsCount_few": "{{count}} sledeni objekti ", + "trackedObjectsCount_other": "{{count}} sledenih objektov ", + "aiAnalysis": { + "title": "AI analiza" + }, + "concerns": { + "label": "Pomisleki" + } +} diff --git a/web/public/locales/sl/views/exports.json b/web/public/locales/sl/views/exports.json new file mode 100644 index 0000000..55083c4 --- /dev/null +++ b/web/public/locales/sl/views/exports.json @@ -0,0 +1,37 @@ +{ + "documentTitle": "Izvoz - Frigate", + "search": "Iskanje", + "noExports": "Ni najdenih izvozov", + "deleteExport": "Izbriši izvoz", + "deleteExport.desc": "Ali si prepričan, da želiš izbrisati {{exportName}}?", + "editExport": { + "title": "Preimenuj izvoz", + "desc": "Vnesi novo ime za ta izvoz.", + "saveExport": "Shrani izvoz" + }, + "toast": { + "error": { + "renameExportFailed": "Preimenovanje izvoza ni uspelo: {{errorMessage}}", + "assignCaseFailed": "Posodobitev dodelitve k primeru ni uspela: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Deli izvoz", + "editName": "Uredi ime", + "deleteExport": "Izbriši izvoz", + "downloadVideo": "Prenesi video", + "assignToCase": "Dodaj k primeru" + }, + "headings": { + "cases": "Primeri", + "uncategorizedExports": "Nekategorizirani izvozi" + }, + "caseDialog": { + "title": "Dodaj k primeru", + "description": "Izberi obstoječ primer ali ustvari novega.", + "selectLabel": "Primer", + "newCaseOption": "Ustvari nov primer", + "nameLabel": "Ime primera", + "descriptionLabel": "Opis" + } +} diff --git a/web/public/locales/sl/views/faceLibrary.json b/web/public/locales/sl/views/faceLibrary.json new file mode 100644 index 0000000..3421809 --- /dev/null +++ b/web/public/locales/sl/views/faceLibrary.json @@ -0,0 +1,107 @@ +{ + "description": { + "addFace": "Dodaj novo zbirko v knjižnico obrazov tako, da naložiš svojo prvo sliko.", + "placeholder": "Vnesi ime za to zbirko", + "invalidName": "Neveljavno ime. Imena lahko vsebujejo le črke, številke, presledke, opuščaje, podčrtaje in vezaje.", + "nameCannotContainHash": "Ime ne sme vsebovati znaka #." + }, + "details": { + "person": "Oseba", + "unknown": "Neznan", + "timestamp": "Časovna značka", + "subLabelScore": "Ocena Podoznake", + "scoreInfo": "Ocena je uteženo povprečje vseh ocen obraza, uteženo glede na velikost obraza na posamezni sliki.", + "face": "Podrobnosti Obraza", + "faceDesc": "Podrobnosti sledenega objekta, ki je ustvaril ta obraz" + }, + "uploadFaceImage": { + "title": "Naloži sliko obraza", + "desc": "Naloži sliko za iskanje obrazov in vključi za {{pageToggle}}" + }, + "deleteFaceAttempts": { + "desc_one": "Ali si prepričan, da želiš izbrisati {{count}} obraz? Tega dejanja ni mogoče razveljaviti.", + "desc_two": "Ali si prepričan, da želiš izbrisati {{count}} obraza? Tega dejanja ni mogoče razveljaviti.", + "desc_few": "Ali si prepričan, da želiš izbrisati {{count}} obraze? Tega dejanja ni mogoče razveljaviti.", + "desc_other": "Ali si prepričan, da želiš izbrisati {{count}} obrazov? Tega dejanja ni mogoče razveljaviti.", + "title": "Izbriši obraze" + }, + "toast": { + "success": { + "deletedFace_one": "Uspešno izbrisan {{count}} obraz.", + "deletedFace_two": "Uspešno izbrisana {{count}} obraza.", + "deletedFace_few": "Uspešno izbrisani {{count}} obrazi.", + "deletedFace_other": "Uspešno izbrisanih {{count}} obrazov.", + "deletedName_one": "{{count}} obraz je bil uspešno izbrisan.", + "deletedName_two": "{{count}} obraza sta bila uspešno izbrisana.", + "deletedName_few": "{{count}} obrazi so bili uspešno izbrisani.", + "deletedName_other": "{{count}} obrazov je bilo uspešno izbrisanih.", + "uploadedImage": "Slika uspešno naložena.", + "addFaceLibrary": "{{name}} je bil uspešno dodan v knjižnico obrazov!", + "renamedFace": "Obraz uspešno preimenovan v {{name}}", + "trainedFace": "Obraz uspešno naučen.", + "updatedFaceScore": "Ocena obraza uspešno posodobljena na {{name}} ({{score}})." + }, + "error": { + "uploadingImageFailed": "Nalaganje slike ni uspelo: {{errorMessage}}", + "addFaceLibraryFailed": "Nastavitev imena obraza ni uspela: {{errorMessage}}", + "deleteFaceFailed": "Brisanje ni uspelo: {{errorMessage}}", + "deleteNameFailed": "Brisanje imena ni uspelo: {{errorMessage}}", + "renameFaceFailed": "Preimenovanje obraza ni uspelo: {{errorMessage}}", + "trainFailed": "Učenje ni uspelo: {{errorMessage}}", + "updateFaceScoreFailed": "Posodobitev ocene obraza ni uspela: {{errorMessage}}" + } + }, + "documentTitle": "Knjižnica obrazov - Frigate", + "collections": "Zbirke", + "createFaceLibrary": { + "title": "Ustvari Zbirko", + "desc": "Ustvari novo zbirko", + "new": "Ustvari nov obraz", + "nextSteps": "Za dobre temelje:
  • Uporabi zavihek Zadnje prepoznave za izbiro in učenje slik za vsako zaznano osebo.
  • Za najboljše rezultate se osredotoči na slike od spredaj; izogibaj se slikam, ki zajamejo obraz pod kotom.
  • " + }, + "steps": { + "faceName": "Vnesi ime obraza", + "uploadFace": "Naloži sliko obraza", + "nextSteps": "Naslednji koraki", + "description": { + "uploadFace": "Naloži sliko osebe {{name}}, ki prikazuje njen obraz od spredaj. Slike ni treba obrezati samo na obraz." + } + }, + "train": { + "title": "Zadnje prepoznave", + "aria": "Izberi zadnje prepoznave", + "empty": "Ni zadnjih poskusov prepoznave obrazov", + "titleShort": "Zadnje" + }, + "selectItem": "Izberi {{item}}", + "selectFace": "Izberi Obraz", + "deleteFaceLibrary": { + "title": "Izbriši ime", + "desc": "Ali si prepričan, da želiš izbrisati zbirko {{name}}? To bo trajno izbrisalo vse povezane obraze." + }, + "renameFace": { + "title": "Preimenuj obraz", + "desc": "Vnesi novo ime za {{name}}" + }, + "button": { + "deleteFaceAttempts": "Izbriši obraze", + "addFace": "Dodaj obraz", + "renameFace": "Preimenuj obraz", + "deleteFace": "Izbriši obraz", + "uploadImage": "Naloži sliko", + "reprocessFace": "Ponovno obdelaj obraz" + }, + "imageEntry": { + "validation": { + "selectImage": "Prosim, izberi slikovno datoteko." + }, + "dropActive": "Spusti sliko tukaj…", + "dropInstructions": "Povleci in spusti ali prilepi sliko sem, ali klikni za izbiro", + "maxSize": "Največja velikost: {{size}} MB" + }, + "nofaces": "Ni razpoložljivih obrazov", + "pixels": "{{area}}px", + "readTheDocs": "Preberi dokumentacijo", + "trainFaceAs": "Nauči obraz kot:", + "trainFace": "Nauči obraz" +} diff --git a/web/public/locales/sl/views/live.json b/web/public/locales/sl/views/live.json new file mode 100644 index 0000000..de62319 --- /dev/null +++ b/web/public/locales/sl/views/live.json @@ -0,0 +1,199 @@ +{ + "documentTitle": "V živo - Frigate", + "documentTitle.withCamera": "{{camera}} - V živo - Frigate", + "lowBandwidthMode": "Način nizke pasovne širine", + "twoWayTalk": { + "enable": "Omogoči dvosmerni pogovor", + "disable": "Onemogoči dvosmerni pogovor" + }, + "ptz": { + "move": { + "clickMove": { + "disable": "Onemogoči premik s klikom", + "label": "Klikni v okvir, da usrediniš kamero", + "enable": "Omogoči premik s klikom" + }, + "left": { + "label": "Premakni PTZ kamero levo" + }, + "up": { + "label": "Premakni PTZ kamero gor" + }, + "down": { + "label": "Premakni PTZ kamero dol" + }, + "right": { + "label": "Premakni PTZ kamero desno" + } + }, + "zoom": { + "in": { + "label": "Povečaj (zoom in)" + }, + "out": { + "label": "Pomanjšaj (zoom out)" + } + }, + "focus": { + "in": { + "label": "Fokusiraj bližje" + }, + "out": { + "label": "Fokusiraj dlje" + } + }, + "frame": { + "center": { + "label": "Klikni v okvir, da usrediniš PTZ kamero" + } + }, + "presets": "Prednastavitve PTZ kamere" + }, + "cameraAudio": { + "enable": "Omogoči zvok kamere", + "disable": "Onemogoči zvok kamere" + }, + "camera": { + "enable": "Omogoči kamero", + "disable": "Onemogoči kamero" + }, + "muteCameras": { + "enable": "Utihni vse kamere", + "disable": "Vklopi zvok vseh kamer" + }, + "detect": { + "enable": "Omogoči zaznavanje", + "disable": "Onemogoči zaznavanje" + }, + "recording": { + "enable": "Omogoči snemanje", + "disable": "Onemogoči snemanje" + }, + "snapshots": { + "enable": "Omogoči posnetke", + "disable": "Onemogoči posnetke" + }, + "audioDetect": { + "enable": "Omogoči zaznavanje zvoka", + "disable": "Onemogoči zaznavanje zvoka" + }, + "transcription": { + "enable": "Omogoči transkripcijo zvoka v živo", + "disable": "Onemogoči transkripcijo zvoka v živo" + }, + "autotracking": { + "enable": "Omogoči samodejno sledenje", + "disable": "Onemogoči samodejno sledenje" + }, + "streamStats": { + "enable": "Prikaži statistiko pretoka", + "disable": "Skrij statistiko pretoka" + }, + "manualRecording": { + "title": "Na zahtevo", + "tips": "Prenesi takojšen posnetek ali začni ročni dogodek na podlagi nastavitev hrambe te kamere.", + "playInBackground": { + "label": "Predvajaj v ozadju", + "desc": "Omogoči to možnost za nadaljevanje pretakanja, ko je predvajalnik skrit." + }, + "showStats": { + "label": "Prikaži statistiko", + "desc": "Omogoči to možnost za prikaz statistike pretoka kot prekrivni element na viru kamere." + }, + "debugView": "Pogled za razhroščevanje", + "start": "Začni snemanje na zahtevo", + "started": "Ročno snemanje na zahtevo se je začelo.", + "failedToStart": "Začetek ročnega snemanja na zahtevo ni uspel.", + "recordDisabledTips": "Ker je snemanje v konfiguraciji za to kamero onemogočeno ali omejeno, bo shranjen le posnetek (snapshot).", + "end": "Končaj snemanje na zahtevo", + "ended": "Ročno snemanje na zahtevo se je končalo.", + "failedToEnd": "Končanje ročnega snemanja na zahtevo ni uspelo." + }, + "streamingSettings": "Nastavitve pretakanja", + "notifications": "Obvestila", + "audio": "Zvok", + "suspend": { + "forTime": "Prekini za: " + }, + "stream": { + "title": "Pretok", + "audio": { + "tips": { + "title": "Zvok mora priti iz tvoje kamere in biti nastavljen v go2rtc za ta pretok.", + "documentation": "Preberi Dokumentacijo " + }, + "available": "Zvok je na voljo za ta pretok", + "unavailable": "Zvok ni na voljo za ta pretok" + }, + "twoWayTalk": { + "tips": "Tvoja naprava mora podpirati to funkcijo, WebRTC pa mora biti nastavljen za dvosmerni pogovor.", + "tips.documentation": "Preberi dokumentacijo ", + "available": "Dvosmerni pogovor je na voljo za ta pretok", + "unavailable": "Dvosmerni pogovor ni na voljo za ta pretok" + }, + "lowBandwidth": { + "tips": "Pogled v živo je v načinu nizke pasovne širine zaradi medpomnjenja ali napak v pretoku.", + "resetStream": "Ponastavi pretok" + }, + "playInBackground": { + "label": "Predvajaj v ozadju", + "tips": "Omogoči to možnost za nadaljevanje pretakanja, ko je predvajalnik skrit." + }, + "debug": { + "picker": "Izbira pretoka v načinu razhroščevanja ni na voljo. Ta pogled vedno uporablja pretok z vlogo 'detect'." + } + }, + "cameraSettings": { + "title": "Nastavitve kamere {{camera}}", + "cameraEnabled": "Kamera omogočena", + "objectDetection": "Zaznavanje objektov", + "recording": "Snemanje", + "snapshots": "Posnetki", + "audioDetection": "Zaznavanje zvoka", + "transcription": "Transkripcija zvoka", + "autotracking": "Samodejno sledenje" + }, + "history": { + "label": "Prikaži zgodovinske posnetke" + }, + "effectiveRetainMode": { + "modes": { + "all": "Vse", + "motion": "Gibanje", + "active_objects": "Aktivni objekti" + }, + "notAllTips": "Vaša konfiguracija hranjenja posnetkov {{source}} je nastavljena na način : {{effectiveRetainMode}}, zato bo ta posnetek na zahtevo hranil samo segmente z {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Uredi postavitev", + "group": { + "label": "Uredi skupino kamer" + }, + "exitEdit": "Izhod iz urejanja" + }, + "snapshot": { + "takeSnapshot": "Prenesi takojšen posnetek", + "noVideoSource": "Vir videa za posnetek ni na voljo.", + "captureFailed": "Zajem posnetka ni uspel.", + "downloadStarted": "Prenos posnetka se je začel." + }, + "noCameras": { + "title": "Ni nastavljenih kamer", + "description": "Začni tako, da povežeš kamero s Frigate.", + "buttonText": "Dodaj kamero", + "restricted": { + "title": "Ni razpoložljivih kamer", + "description": "Nimaš dovoljenja za ogled kamer v tej skupini." + }, + "default": { + "title": "Ni nastavljenih kamer", + "description": "Začni tako, da povežeš kamero s Frigate.", + "buttonText": "Dodaj kamero" + }, + "group": { + "title": "V skupini ni kamer", + "description": "Ta skupina kamer nima dodeljenih ali omogočenih kamer.", + "buttonText": "Upravljaj skupine" + } + } +} diff --git a/web/public/locales/sl/views/motionSearch.json b/web/public/locales/sl/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sl/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sl/views/recording.json b/web/public/locales/sl/views/recording.json new file mode 100644 index 0000000..5b9f4c5 --- /dev/null +++ b/web/public/locales/sl/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filter", + "export": "Izvoz", + "calendar": "Koledar", + "filters": "Filtri", + "toast": { + "error": { + "noValidTimeSelected": "Izbrano ni nobeno veljavno časovno obdobje", + "endTimeMustAfterStartTime": "Čas konca mora biti po času začetka" + } + } +} diff --git a/web/public/locales/sl/views/replay.json b/web/public/locales/sl/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sl/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sl/views/search.json b/web/public/locales/sl/views/search.json new file mode 100644 index 0000000..a8fb14c --- /dev/null +++ b/web/public/locales/sl/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Iskanje", + "savedSearches": "Shranjena iskanja", + "searchFor": "Išči: {{inputValue}}", + "button": { + "clear": "Počisti iskanje", + "save": "Shrani iskanje", + "delete": "Izbriši shranjeno iskanje", + "filterInformation": "Informacije o filtrih", + "filterActive": "Aktivni filtri" + }, + "filter": { + "label": { + "cameras": "Kamere", + "labels": "Oznake", + "zones": "Območja", + "sub_labels": "Pod-oznake", + "search_type": "Vrsta iskanja", + "time_range": "Časovni razpon", + "before": "Pred", + "after": "Po", + "min_score": "Najnižja ocena", + "max_score": "Najvišja ocena", + "recognized_license_plate": "Prepoznana registrska tablica", + "has_clip": "Ima posnetek", + "max_speed": "Najvišja hitrost", + "min_speed": "Najnižja hitrost", + "has_snapshot": "Ima sliko (snapshot)", + "attributes": "Atributi" + }, + "searchType": { + "thumbnail": "Sličica", + "description": "Opis" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Datum 'Pred' mora biti poznejši od datuma 'Po'.", + "afterDatebeEarlierBefore": "Datum 'Po' mora biti zgodnejši od datuma 'Pred'.", + "minScoreMustBeLessOrEqualMaxScore": "Najnižja ocena mora biti manjša ali enaka najvišji oceni.", + "maxScoreMustBeGreaterOrEqualMinScore": "Najvišja ocena mora biti večja ali enaka najnižji oceni.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Najvišja hitrost mora biti večja ali enaka najnižji hitrosti.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Najnižja hitrost mora biti manjša ali enaka najvišji hitrosti." + } + }, + "tips": { + "title": "Kako uporabljati tekstovne filtre", + "desc": { + "text": "Filtri ti pomagajo zožiti rezultate iskanja. Takole jih uporabiš v vnosnem polju:", + "step1": "Vnesi ime ključa filtra, ki mu sledi dvopičje (npr. \"cameras:\").", + "step2": "Izberi vrednost iz predlogov ali vnesi svojo.", + "step3": "Uporabiš lahko več filtrov hkrati tako, da jih dodaš enega za drugim s presledkom vmes.", + "step4": "Datumski filtri (before: in after:) uporabljajo format {{DateFormat}}.", + "step5": "Filter časovnega razpona uporablja format {{exampleTime}}.", + "step6": "Filtre odstraniš s klikom na 'x' poleg njih.", + "exampleLabel": "Primer:" + } + }, + "header": { + "currentFilterType": "Vrednosti filtrov", + "noFilters": "Filtri", + "activeFilters": "Aktivni filtri" + } + }, + "trackedObjectId": "ID sledenega objekta", + "similaritySearch": { + "title": "Iskanje po podobnosti", + "active": "Iskanje po podobnosti je aktivno", + "clear": "Počisti iskanje po podobnosti" + }, + "placeholder": { + "search": "Išči…" + } +} diff --git a/web/public/locales/sl/views/settings.json b/web/public/locales/sl/views/settings.json new file mode 100644 index 0000000..2f695e0 --- /dev/null +++ b/web/public/locales/sl/views/settings.json @@ -0,0 +1,1554 @@ +{ + "documentTitle": { + "default": "Nastavitve - Frigate", + "authentication": "Nastavitve avtentikacije - Frigate", + "camera": "Nastavitve kamere - Frigate", + "notifications": "Nastavitve obvestil - Frigate", + "masksAndZones": "Urejevalnik mask in območij - Frigate", + "object": "Razhroščevanje - Frigate", + "general": "Nastavitve profila - Frigate", + "frigatePlus": "Nastavitve Frigate+ - Frigate", + "enrichments": "Nastavitve obogatenih podatkov - Frigate", + "motionTuner": "Prilagajanje zaznavanja gibanja - Frigate", + "cameraManagement": "Upravljanje kamer - Frigate", + "cameraReview": "Nastavitve pregleda kamer - Frigate", + "globalConfig": "Globalna konfiguracija - Frigate", + "cameraConfig": "Konfiguracija kamere - Frigate", + "maintenance": "Vzdrževanje - Frigate" + }, + "menu": { + "ui": "Uporabniški vmesnik", + "enrichments": "Obogatitve", + "cameras": "Konfiguracija kamer", + "masksAndZones": "Maske / Območja", + "debug": "Razhroščevanje", + "users": "Uporabniki", + "notifications": "Obvestila", + "frigateplus": "Frigate+", + "motionTuner": "Nastavljalnik gibanja", + "triggers": "Sprožilci", + "cameraManagement": "Upravljanje", + "cameraReview": "Pregled", + "roles": "Vloge", + "general": "Splošno", + "globalConfig": "Globalna konfiguracija", + "system": "Sistem", + "integrations": "Integracije", + "profileSettings": "Nastavitve profila", + "globalDetect": "Zaznavanje objektov", + "globalRecording": "Snemanje", + "globalSnapshots": "Posnetki (snapshots)", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Zaznavanje gibanja", + "globalObjects": "Objekti", + "globalReview": "Pregled", + "globalAudioEvents": "Zvočni dogodki", + "globalLivePlayback": "Predvajanje v živo", + "globalTimestampStyle": "Slog časovne značke", + "systemDatabase": "Podatkovna baza", + "systemTls": "TLS", + "systemAuthentication": "Avtentikacija", + "systemNetworking": "Omrežje", + "systemProxy": "Proxy", + "systemUi": "Uporabniški vmesnik", + "systemLogging": "Beleženje (logging)", + "systemEnvironmentVariables": "Okoljske spremenljivke", + "systemTelemetry": "Telemetrija", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "Strojna oprema detektorja", + "systemDetectionModel": "Model zaznavanja", + "systemMqtt": "MQTT", + "integrationSemanticSearch": "Semantično iskanje", + "integrationGenerativeAi": "Generativna UI", + "integrationFaceRecognition": "Prepoznava obrazov", + "integrationLpr": "Prepoznava registrskih tablic", + "integrationObjectClassification": "Klasifikacija objektov", + "integrationAudioTranscription": "Transkripcija zvoka", + "cameraDetect": "Zaznavanje objektov", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "Snemanje", + "cameraSnapshots": "Posnetki (snapshots)", + "cameraMotion": "Zaznavanje gibanja", + "cameraObjects": "Objekti", + "cameraConfigReview": "Pregled", + "cameraAudioEvents": "Zvočni dogodki", + "cameraAudioTranscription": "Transkripcija zvoka", + "cameraNotifications": "Obvestila", + "cameraLivePlayback": "Predvajanje v živo", + "cameraBirdseye": "Birdseye", + "cameraFaceRecognition": "Prepoznava obrazov", + "cameraLpr": "Prepoznava registrskih tablic", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "Uporabniški vmesnik kamere", + "cameraTimestampStyle": "Slog časovne značke", + "cameraMqtt": "Kamera MQTT", + "mediaSync": "Sinhronizacija medijev", + "regionGrid": "Mreža regij" + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} točka", + "point_two": "", + "point_few": "", + "point_other": "{{count}} točke", + "label": "Območja", + "documentTitle": "Uredi območje - Frigate", + "desc": { + "title": "Območja (Zones) ti omogočajo določitev specifičnih delov slike, da lahko ugotoviš, ali se objekt nahaja v določenem predelu.", + "documentation": "Dokumentacija" + }, + "add": "Dodaj območje", + "edit": "Uredi območje", + "clickDrawPolygon": "Klikni za risanje poligona na sliki.", + "name": { + "title": "Ime", + "inputPlaceHolder": "Vnesi ime…", + "tips": "Ime mora imeti vsaj 2 znaka, vsaj eno črko in ne sme biti enako imenu kamere ali drugega območja na tej kameri." + }, + "enabled": { + "title": "Omogočeno", + "description": "Ali je to območje aktivno v konfiguracijski datoteki. Če je onemogočeno, ga ni mogoče vklopiti preko MQTT. Onemogočena območja sistem med delovanjem prezre." + }, + "inertia": { + "title": "Vztrajnost (Inertia)", + "desc": "Določa, v koliko zaporednih sličicah mora biti objekt v območju, da se šteje, da je v njem. Privzeto: 3" + }, + "loiteringTime": { + "title": "Čas zadrževanja", + "desc": "Določa minimalni čas v sekundah, ki ga mora objekt preživeti v območju, da se to aktivira. Privzeto: 0" + }, + "objects": { + "title": "Objekti", + "desc": "Seznam objektov, ki veljajo za to območje." + }, + "allObjects": "Vsi objekti", + "speedEstimation": { + "title": "Ocena hitrosti", + "desc": "Omogoči ocenjevanje hitrosti za objekte v tem območju. Območje mora imeti natanko 4 točke.", + "lineADistance": "Razdalja linije A ({{unit}})", + "lineBDistance": "Razdalja linije B ({{unit}})", + "lineCDistance": "Razdalja linije C ({{unit}})", + "lineDDistance": "Razdalja linije D ({{unit}})" + }, + "speedThreshold": { + "title": "Prag hitrosti ({{unit}})", + "desc": "Določa minimalno hitrost, da se objekt upošteva v tem območju.", + "toast": { + "error": { + "pointLengthError": "Ocena hitrosti je bila onemogočena za to območje. Območja z oceno hitrosti morajo imeti natanko 4 točke.", + "loiteringTimeError": "Območij s časom zadrževanja nad 0 ne bi smeli uporabljati skupaj z oceno hitrosti." + } + } + }, + "toast": { + "success": "Območje ({{zoneName}}) je bilo shranjeno." + } + }, + "objectMasks": { + "point_one": "{{count}} točka", + "point_two": "", + "point_few": "", + "point_other": "{{count}} točke", + "label": "Maske objektov", + "documentTitle": "Uredi masko objekta - Frigate", + "desc": { + "title": "Maske za filtre objektov se uporabljajo za filtriranje lažnih pozitivnih rezultatov za določen tip objekta glede na lokacijo.", + "documentation": "Dokumentacija" + }, + "add": "Dodaj masko objekta", + "edit": "Uredi masko objekta", + "context": "Maske za filtre objektov se uporabljajo za filtriranje lažnih pozitivnih rezultatov za določen tip objekta glede na lokacijo.", + "clickDrawPolygon": "Klikni za risanje poligona na sliki.", + "name": { + "title": "Ime", + "description": "Opcijsko prijazno ime za to masko objekta.", + "placeholder": "Vnesi ime..." + }, + "objects": { + "title": "Objekti", + "desc": "Tip objekta, ki velja za to masko.", + "allObjectTypes": "Vsi tipi objektov" + }, + "toast": { + "success": { + "title": "{{polygonName}} je bila shranjena.", + "noName": "Maska objekta je bila shranjena." + } + } + }, + "motionMasks": { + "point_one": "{{count}} točka", + "point_two": "", + "point_few": "", + "point_other": "{{count}} točke", + "label": "Maska gibanja", + "documentTitle": "Uredi masko gibanja - Frigate", + "desc": { + "title": "Maske gibanja se uporabljajo za preprečevanje neželenih vrst gibanja, ki bi sprožile zaznavanje. Preveč maskiranja bo otežilo sledenje objektom.", + "documentation": "Dokumentacija" + }, + "add": "Nova maska gibanja", + "edit": "Uredi masko gibanja", + "defaultName": "Maska gibanja {{number}}", + "context": { + "title": "Maske gibanja preprečujejo, da bi npr. veje dreves ali časovne značke sprožile zaznavanje. Uporabljaj jih zelo varčno, saj preveč mask oteži sledenje objektom." + }, + "clickDrawPolygon": "Klikni za risanje poligona na sliki.", + "name": { + "title": "Ime", + "description": "Opcijsko prijazno ime za to masko gibanja.", + "placeholder": "Vnesi ime..." + }, + "polygonAreaTooLarge": { + "title": "Maska gibanja prekriva {{polygonArea}} % slike kamere. Velike maske niso priporočljive.", + "tips": "Maske gibanja ne preprečujejo zaznavanja objektov. Namesto tega uporabi zahtevano območje (zone)." + }, + "toast": { + "success": { + "title": "{{polygonName}} je bila shranjena.", + "noName": "Maska gibanja je bila shranjena." + } + } + }, + "filter": { + "all": "Vse maske in območja" + }, + "restart_required": "Potreben ponovni zagon (sprememba mask/območij)", + "disabledInConfig": "Element je onemogočen v konfiguracijski datoteki", + "toast": { + "success": { + "copyCoordinates": "Koordinate za {{polyName}} so kopirane v odložišče." + }, + "error": { + "copyCoordinatesFailed": "Koordinat ni bilo mogoče kopirati v odložišče." + } + }, + "motionMaskLabel": "Maska gibanja {{number}}", + "objectMaskLabel": "Maska objekta {{number}}", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Ime območja mora imeti vsaj 2 znaka.", + "mustNotBeSameWithCamera": "Ime območja ne sme biti enako imenu kamere.", + "alreadyExists": "Območje s tem imenom za to kamero že obstaja.", + "mustNotContainPeriod": "Ime območja ne sme vsebovati pik.", + "hasIllegalCharacter": "Ime območja vsebuje neveljavne znake.", + "mustHaveAtLeastOneLetter": "Ime območja mora vsebovati vsaj eno črko." + } + }, + "distance": { + "error": { + "text": "Razdalja mora biti večja ali enaka 0.1.", + "mustBeFilled": "Za oceno hitrosti morajo biti izpolnjena vsa polja za razdaljo." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Vztrajnost (inertia) mora biti večja od 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Čas zadrževanja mora biti večji ali enak 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Prag hitrosti mora biti večji ali enak 0.1." + } + }, + "polygonDrawing": { + "type": { + "zone": "območje", + "motion_mask": "maska gibanja", + "object_mask": "maska objekta" + }, + "removeLastPoint": "Odstrani zadnjo točko", + "reset": { + "label": "Počisti vse točke" + }, + "snapPoints": { + "true": "Pripni točke (Snap)", + "false": "Brez pripenjanja točk" + }, + "delete": { + "title": "Potrdi izbris", + "desc": "Ali si prepričan, da želiš izbrisati {{type}} {{name}}?", + "success": "{{name}} je bil izbrisan." + }, + "error": { + "mustBeFinished": "Risanje poligona mora biti zaključeno pred shranjevanjem." + } + } + }, + "masks": { + "enabled": { + "title": "Omogočeno", + "description": "Ali je ta maska omogočena v konfiguracijski datoteki. Če je onemogočena, je ni mogoče vklopiti preko MQTT." + } + } + }, + "dialog": { + "unsavedChanges": { + "title": "Imaš neshranjene spremembe.", + "desc": "Želiš shraniti spremembe, preden nadaljuješ?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Ni kamere" + }, + "general": { + "title": "Nastavitve profila", + "liveDashboard": { + "title": "Nadzorna plošča v živo", + "automaticLiveView": { + "label": "Samodejni pogled v živo", + "desc": "Samodejno preklopi na pogled v živo, ko je zaznana aktivnost. Če to onemogočiš, se bodo statične slike kamer na nadzorni plošči osvežile le enkrat na minuto." + }, + "playAlertVideos": { + "label": "Predvajaj videe opozoril", + "desc": "Privzeto se zadnja opozorila predvajajo kot kratki ponavljajoči se videi. Onemogoči to možnost, če želiš na tej napravi/v brskalniku videti le statično sliko zadnjih opozoril." + }, + "displayCameraNames": { + "label": "Vedno prikaži imena kamer", + "desc": "Vedno prikaži imena kamer v znački na nadzorni plošči z več kamerami." + }, + "liveFallbackTimeout": { + "label": "Časovna omejitev za preklop predvajalnika", + "desc": "Če visokokakovostni tok kamere ni na voljo, po toliko sekundah preklopi na način nizke pasovne širine. Privzeto: 3." + } + }, + "storedLayouts": { + "title": "Shranjene postavitve", + "desc": "Postavitev kamer v skupini lahko spreminjaš z vlečenjem ali spreminjanjem velikosti. Položaji so shranjeni v lokalni shrambi tvojega brskalnika.", + "clearAll": "Počisti vse postavitve" + }, + "cameraGroupStreaming": { + "title": "Nastavitve pretakanja skupin kamer", + "desc": "Nastavitve pretakanja za vsako skupino kamer so shranjene v lokalni shrambi tvojega brskalnika.", + "clearAll": "Počisti vse nastavitve pretakanja" + }, + "recordingsViewer": { + "title": "Pregledovalnik posnetkov", + "defaultPlaybackRate": { + "label": "Privzeta hitrost predvajanja", + "desc": "Privzeta hitrost predvajanja za posnetke." + } + }, + "calendar": { + "title": "Koledar", + "firstWeekday": { + "label": "Prvi dan v tednu", + "desc": "Dan, s katerim se začne teden na koledarju pregledov.", + "sunday": "Nedelja", + "monday": "Ponedeljek" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Postavitev za {{cameraName}} je počiščena", + "clearStreamingSettings": "Nastavitve pretakanja za vse skupine kamer so počiščene." + }, + "error": { + "clearStoredLayoutFailed": "Čiščenje shranjene postavitve ni uspelo: {{errorMessage}}", + "clearStreamingSettingsFailed": "Čiščenje nastavitev pretakanja ni uspelo: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Nastavitve obogatenih podatkov", + "unsavedChanges": "Neshranjene spremembe nastavitev obogatenih podatkov", + "birdClassification": { + "title": "Klasifikacija ptic", + "desc": "Klasifikacija ptic identificira znane ptice s pomočjo kvantiziranega modela Tensorflow. Ko je znana ptica prepoznana, se njeno običajno ime doda kot pod-oznaka. Ti podatki so vključeni v vmesniku, filtrih in obvestilih." + }, + "semanticSearch": { + "title": "Semantično iskanje", + "desc": "Semantično iskanje v Frigate ti omogoča iskanje sledenih objektov z uporabo same slike, tvojega opisa ali samodejno generiranega opisa.", + "readTheDocumentation": "Preberi Dokumentacijo", + "reindexNow": { + "label": "Ponovno indeksiraj zdaj", + "desc": "Ponovno indeksiranje bo regeneriralo vdelave (embeddings) za vse sledene objekte. Proces teče v ozadju, lahko močno obremeni procesor in traja nekaj časa, odvisno od števila objektov.", + "confirmTitle": "Potrdi ponovno indeksiranje", + "confirmDesc": "Ali si prepričan, da želiš ponovno indeksirati vse vdelave sledenih objektov? Proces bo tekel v ozadju, lahko močno obremeni procesor in traja dlje časa. Napredek lahko spremljaš na strani 'Razišči'.", + "confirmButton": "Ponovno indeksiraj", + "success": "Ponovno indeksiranje se je uspešno začelo.", + "alreadyInProgress": "Ponovno indeksiranje že poteka.", + "error": "Začetek ponovnega indeksiranja ni uspel: {{errorMessage}}" + }, + "modelSize": { + "label": "Velikost modela", + "desc": "Velikost modela, ki se uporablja za vdelave semantičnega iskanja.", + "small": { + "title": "majhen", + "desc": "Uporaba majhnega modela uporablja kvantizirano različico, ki zasede manj RAM-a in teče hitreje na procesorju, ob zanemarljivi razliki v kakovosti." + }, + "large": { + "title": "velik", + "desc": "Uporaba velikega modela uporablja celoten model Jina in bo samodejno tekla na GPU, če je ta na voljo." + } + } + }, + "faceRecognition": { + "title": "Prepoznava obrazov", + "desc": "Prepoznava obrazov omogoča dodeljevanje imen osebam; ko je obraz prepoznan, Frigate ime osebe doda kot pod-oznako. Ti podatki so vidni v vmesniku, filtrih in obvestilih.", + "readTheDocumentation": "Preberi Dokumentacijo", + "modelSize": { + "label": "Velikost modela", + "desc": "Velikost modela za prepoznavo obrazov.", + "small": { + "title": "majhen", + "desc": "Uporaba majhnega modela uporablja model FaceNet, ki učinkovito teče na večini procesorjev." + }, + "large": { + "title": "velik", + "desc": "Uporaba velikega modela uporablja model ArcFace in bo samodejno tekla na GPU, če je ta na voljo." + } + } + }, + "licensePlateRecognition": { + "title": "Prepoznava registrskih tablic", + "desc": "Frigate lahko prepozna registrske tablice na vozilih in samodejno doda prepoznane znake v polje 'recognized_license_plate' ali znano ime kot pod-oznako za objekte vrste avto. Tipičen primer uporabe je branje tablic na dovozu ali na ulici.", + "readTheDocumentation": "Preberi Dokumentacijo" + }, + "restart_required": "Potreben ponovni zagon (sprememba nastavitev obogatenih podatkov)", + "toast": { + "success": "Nastavitve obogatenih podatkov so shranjene. Ponovno zaženi Frigate, da uveljaviš spremembe.", + "error": "Shranjevanje sprememb konfiguracije ni uspelo: {{errorMessage}}" + } + }, + "camera": { + "title": "Nastavitve Kamere", + "streams": { + "title": "Pretoki" + }, + "object_descriptions": { + "title": "Opisi objektov z uporabo generativne UI", + "desc": "Začasno omogoči/onemogoči opise objektov z uporabo generativne UI za to kamero. Ko so onemogočeni, opisi, ki jih ustvari UI, ne bodo zahtevani za sledene objekte na tej kameri." + }, + "review": { + "title": "Pregled", + "desc": "Začasno omogoči/onemogoči opozorila in zaznavanja za to kamero, dokler se Frigate ne zažene znova. Ko je onemogočeno, ne bodo ustvarjeni novi elementi pregleda. ", + "alerts": "Opozorila ", + "detections": "Detekcije " + }, + "reviewClassification": { + "title": "Pregled Klasifikacij", + "readTheDocumentation": "Preberi Dokumentacijo", + "noDefinedZones": "Za to kamero ni določenih nobenih con.", + "objectAlertsTips": "Vsi objekti {{alertsLabels}} na {{cameraName}} bodo prikazani kot Opozorila.", + "unsavedChanges": "Neshranjene nastavitve Pregleda Klasifikacije za {{camera}}", + "selectAlertsZones": "Izberite cone za Opozorila", + "selectDetectionsZones": "Izberite cone za Zaznavanje", + "limitDetections": "Omejite zaznavanje na določene cone" + }, + "addCamera": "Dodaj Novo Kamero", + "editCamera": "Uredi Kamero:", + "selectCamera": "Izberi Kamero", + "backToSettings": "Nazaj na Nastavitve Kamere", + "cameraConfig": { + "add": "Dodaj Kamero", + "edit": "Uredi Kamero", + "description": "Konfigurirajte nastavitve kamere, vključno z pretočnimi vhodi in vlogami.", + "name": "Ime Kamere", + "nameRequired": "Ime kamere je obvezno", + "nameInvalid": "Ime kamere mora vsebovati samo črke, številke, podčrtaje ali vezaje", + "namePlaceholder": "npr. vhodna_vrata" + } + }, + "cameraWizard": { + "title": "Dodaj kamero", + "description": "Sledi spodnjim korakom, da dodaš novo kamero v svojo namestitev Frigate.", + "steps": { + "nameAndConnection": "Ime in povezava", + "streamConfiguration": "Konfiguracija pretoka", + "validationAndTesting": "Potrditev in testiranje", + "probeOrSnapshot": "Preverjanje ali slika" + }, + "save": { + "success": "Nova kamera {{cameraName}} je bila uspešno shranjena.", + "failure": "Napaka pri shranjevanju kamere {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Ločljivost", + "video": "Video", + "audio": "Zvok", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Prosim, navedi veljaven URL pretoka", + "testFailed": "Test pretoka ni uspel: {{error}}" + }, + "step1": { + "description": "Vnesi podrobnosti o kameri in izberi samodejno preverjanje ali ročno izbiro znamke.", + "cameraName": "Ime kamere", + "cameraNamePlaceholder": "npr. vhodna_vrata ali Zadnje dvorisce", + "host": "Gostitelj/IP naslov", + "port": "Vrata (Port)", + "username": "Uporabniško ime", + "usernamePlaceholder": "Opcijsko", + "password": "Geslo", + "passwordPlaceholder": "Opcijsko", + "selectTransport": "Izberi transportni protokol", + "cameraBrand": "Znamka kamere", + "selectBrand": "Izberi znamko kamere za predlogo URL-ja", + "customUrl": "URL pretoka po meri", + "brandInformation": "Informacije o znamki", + "brandUrlFormat": "Za kamere z RTSP URL formatom: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://uporabnik:geslo@gostitelj:vrata/pot", + "testConnection": "Preveri povezavo", + "testSuccess": "Test povezave uspešen!", + "testFailed": "Test povezave neuspešen. Prosim preveri vnos in poskusi še enkrat.", + "streamDetails": "Podrobnosti pretoka", + "testing": { + "probingMetadata": "Preiskovanje metapodatkov kamere...", + "fetchingSnapshot": "Pridobivanje posnetka kamere..." + }, + "warnings": { + "noSnapshot": "Ni mogoče pridobiti posnetka iz nastavljenega pretoka." + }, + "errors": { + "nameLength": "Ime kamere mora imeti 64 znakov ali manj", + "invalidCharacters": "Ime kamere vsebuje neveljavne znake", + "nameExists": "Ime kamere že obstaja", + "customUrlRtspRequired": "URL-ji po meri se morajo začeti z \"rtsp://\". Za pretoke, ki niso RTSP, je potrebna ročna konfiguracija.", + "brands": { + "reolink-rtsp": "RTSP za Reolink ni priporočen. \nV nastavitvah kamere omogočite HTTP in znova zaženite čarovnika." + }, + "brandOrCustomUrlRequired": "Izberi znamko kamere s podatki o gostitelju ali izberi 'Drugo' z URL-jem po meri", + "nameRequired": "Ime kamere je obvezno" + }, + "connectionSettings": "Nastavitve povezave", + "detectionMethod": "Metoda zaznavanja pretoka", + "onvifPort": "ONVIF vrata", + "probeMode": "Preveri kamero (Probe)", + "manualMode": "Ročna izbira", + "detectionMethodDescription": "Preveri kamero preko protokola ONVIF (če je podprt), da najdeš URL-je pretoka, ali ročno izberi znamko kamere za uporabo vnaprej določenih URL-jev. Za vnos lastnega RTSP URL-ja izberi ročno metodo in nato \"Drugo\".", + "onvifPortDescription": "Za kamere, ki podpirajo ONVIF, so to običajno vrata 80 ali 8080.", + "useDigestAuth": "Uporabi 'digest' avtentikacijo", + "useDigestAuthDescription": "Uporabi HTTP digest avtentikacijo za ONVIF. Nekatere kamere zahtevajo posebno ONVIF uporabniško ime/geslo namesto običajnega skrbniškega računa." + }, + "step2": { + "streamUrlPlaceholder": "rtsp://uporabniskoime:geslo@gostitelj:vrata/pot", + "url": "URL", + "resolution": "Resolucija", + "selectResolution": "Izberi resolucijo", + "quality": "Kvaliteta", + "selectQuality": "Izberi kvaliteto", + "roles": "Vloge", + "roleLabels": { + "detect": "Prepoznavanje objektov", + "record": "Snemanje", + "audio": "Zvok" + }, + "testStream": "Preveri povezavo", + "testSuccess": "Povezava je uspešno vzpostavljena!", + "testFailed": "Povezava ni uspela. Preveri vnose in poskusi znova.", + "testFailedTitle": "Test ni uspel", + "connected": "Povezano", + "notConnected": "Ni povezave", + "featuresTitle": "Funkcije", + "go2rtc": "Zmanjšaj povezave na kamero", + "detectRoleWarning": "Vsaj en pretok mora imeti vlogo »zaznavanje«, da lahko nadaljuješ.", + "rolesPopover": { + "title": "Vloge pretoka", + "detect": "Glavni vir za zaznavanje objektov.", + "record": "Shranjuje odseke video posnetka glede na nastavitve konfiguracije.", + "audio": "Vir za zaznavanje na podlagi zvoka." + }, + "featuresPopover": { + "title": "Značilnosti pretoka", + "description": "Uporabi ponovno pretakanje go2rtc, da zmanjšaš število povezav s kamero." + }, + "description": "Preveri razpoložljive pretoke kamere ali ročno nastavi parametre glede na izbrano metodo.", + "streamDetails": "Podrobnosti pretoka", + "probing": "Preverjanje kamere...", + "retry": "Poskusi znova", + "testing": { + "probingMetadata": "Preverjanje metapodatkov kamere...", + "fetchingSnapshot": "Pridobivanje slike s kamere..." + }, + "probeFailed": "Preverjanje kamere ni uspelo: {{error}}", + "probingDevice": "Preverjanje naprave...", + "probeSuccessful": "Preverjanje uspešno", + "probeError": "Napaka pri preverjanju", + "probeNoSuccess": "Preverjanje neuspešno", + "deviceInfo": "Podatki o napravi", + "manufacturer": "Proizvajalec", + "model": "Model", + "firmware": "Strojna programska oprema (Firmware)", + "profiles": "Profili", + "ptzSupport": "Podpora za PTZ", + "autotrackingSupport": "Podpora za samodejno sledenje", + "presets": "Prednastavitve", + "rtspCandidates": "Možni RTSP URL-ji", + "rtspCandidatesDescription": "Preverjanje je našlo naslednje RTSP URL-je. Testiraj povezavo za ogled metapodatkov pretoka.", + "noRtspCandidates": "Preverjanje ni našlo nobenega RTSP URL-ja. Morda so poverilnice napačne ali pa kamera ne podpira ONVIF. Pojdi nazaj in vnesi RTSP URL ročno.", + "candidateStreamTitle": "Kandidat {{number}}", + "useCandidate": "Uporabi", + "uriCopy": "Kopiraj", + "uriCopied": "URI kopiran v odložišče", + "testConnection": "Testiraj povezavo", + "toggleUriView": "Klikni za preklop polnega URI pogleda", + "errors": { + "hostRequired": "Naslov gostitelja/IP je obvezen" + } + }, + "step3": { + "description": "Nastavi vloge pretokov in dodaj morebitne dodatne pretoke za svojo kamero.", + "validationTitle": "Preverjanje pretoka", + "connectAllStreams": "Poveži vse pretoke", + "reconnectionSuccess": "Ponovna povezava uspešna.", + "reconnectionPartial": "Nekateri pretoki se niso ponovno povezali.", + "streamUnavailable": "Predogled pretoka ni na voljo", + "reload": "Ponovno naloži", + "connecting": "Povezujem...", + "streamTitle": "Pretok {{number}}", + "valid": "Veljaven", + "failed": "Spodletel", + "notTested": "Ni testiran", + "connectStream": "Poveži", + "connectingStream": "Povezujem", + "disconnectStream": "Prekini povezavo", + "estimatedBandwidth": "Predvidena pasovna širina", + "roles": "Vloge", + "none": "Noben", + "error": "Napaka", + "streamValidated": "Pretok {{number}} uspešno preverjen", + "streamValidationFailed": "Preverjanje pretoka {{number}} spodletelo", + "saveAndApply": "Shrani novo kamero", + "saveError": "Neveljavna konfiguracija. Prosimo preverite vaše nastavitve.", + "issues": { + "title": "Preverjanje pretoka", + "videoCodecGood": "Video kodek je {{codec}}.", + "audioCodecGood": "Audio kodek je {{codec}}.", + "resolutionHigh": "Resolucija {{resolution}} lahko povzroči povečano porabo virov." + }, + "streamsTitle": "Pretoki kamere", + "addStream": "Dodaj pretok", + "addAnotherStream": "Dodaj še en pretok", + "streamUrl": "URL pretoka", + "streamUrlPlaceholder": "rtsp://uporabnik:geslo@gostitelj:vrata/pot", + "selectStream": "Izberi pretok", + "searchCandidates": "Išči med kandidati...", + "noStreamFound": "Pretok ni bil najden", + "url": "URL", + "resolution": "Ločljivost", + "selectResolution": "Izberi ločljivost", + "quality": "Kakovost", + "selectQuality": "Izberi kakovost", + "roleLabels": { + "detect": "Zaznavanje objektov", + "record": "Snemanje", + "audio": "Zvok" + }, + "testStream": "Testiraj povezavo", + "testSuccess": "Test pretoka uspešen!", + "testFailed": "Test pretoka ni uspel", + "testFailedTitle": "Test ni uspel", + "connected": "Povezano", + "notConnected": "Ni povezave", + "featuresTitle": "Funkcije", + "go2rtc": "Zmanjšaj število povezav do kamere", + "detectRoleWarning": "Vsaj en pretok mora imeti vlogo \"detect\" za nadaljevanje.", + "rolesPopover": { + "title": "Vloge pretokov", + "detect": "Glavni vir za zaznavanje objektov.", + "record": "Shranjuje dele videa glede na nastavitve.", + "audio": "Vir za zaznavanje na podlagi zvoka." + }, + "featuresPopover": { + "title": "Funkcije pretoka", + "description": "Uporabi go2rtc za ponovno pretakanje (restreaming), da zmanjšaš obremenitev kamere." + } + }, + "step4": { + "connectStream": "Poveži", + "connectingStream": "Povezovanje", + "disconnectStream": "Prekini povezavo", + "estimatedBandwidth": "Ocenjena pasovna širina", + "roles": "Vloge", + "connectAllStreams": "Poveži vse pretoke", + "reconnectionSuccess": "Ponovna povezava uspešna.", + "reconnectionPartial": "Nekaterih pretokov ni bilo mogoče ponovno povezati.", + "streamUnavailable": "Predogled pretoka ni na voljo", + "reload": "Ponovno naloži", + "connecting": "Povezovanje...", + "streamTitle": "Pretok {{number}}", + "valid": "Veljaven", + "failed": "Spodletelo", + "notTested": "Ni testirano", + "description": "Končna potrditev in analiza pred shranjevanjem. Pred shranjevanjem poveži vsak pretok.", + "validationTitle": "Potrditev pretoka", + "ffmpegModule": "Uporabi način za združljivost pretoka", + "ffmpegModuleDescription": "Če se pretok po več poskusih ne naloži, poskusi omogočiti to možnost. Frigate bo uporabil modul ffmpeg z go2rtc, kar lahko izboljša združljivost z nekaterimi kamerami.", + "none": "Brez", + "error": "Napaka", + "streamValidated": "Pretok {{number}} je bil uspešno potrjen", + "streamValidationFailed": "Potrditev pretoka {{number}} ni uspela", + "saveAndApply": "Shrani novo kamero", + "saveError": "Neveljavna konfiguracija. Preveri svoje nastavitve.", + "issues": { + "title": "Potrditev pretoka", + "videoCodecGood": "Video kodek je {{codec}}.", + "audioCodecGood": "Zvočni kodek je {{codec}}.", + "resolutionHigh": "Ločljivost {{resolution}} lahko povzroči povečano porabo virov.", + "resolutionLow": "Ločljivost {{resolution}} je morda prenizka za zanesljivo zaznavanje majhnih objektov.", + "noAudioWarning": "Na tem pretoku ni zaznanega zvoka, posnetki bodo brez zvoka.", + "audioCodecRecordError": "Za zvok v posnetkih je potreben zvočni kodek AAC.", + "audioCodecRequired": "Zvočni pretok je potreben za podporo zaznavanja zvoka.", + "restreamingWarning": "Zmanjševanje povezav do kamere za snemanje lahko rahlo poveča porabo procesorja.", + "brands": { + "reolink-rtsp": "Reolink RTSP ni priporočljiv. Omogoči HTTP v nastavitvah kamere in ponovno zaženi čarovnika.", + "reolink-http": "Za Reolink HTTP pretoke je priporočljiva uporaba FFmpeg. Omogoči 'Način za združljivost pretoka' za ta pretok." + }, + "dahua": { + "substreamWarning": "Podpretok 1 je zaklenjen na nizko ločljivost. Veliko kamer Dahua / Amcrest podpira dodatne podpretoce, ki jih je treba omogočiti v nastavitvah kamere. Priporočljivo je, da preveriš in uporabiš te pretoke, če so na voljo." + }, + "hikvision": { + "substreamWarning": "Podpretok 1 je zaklenjen na nizko ločljivost. Veliko kamer Hikvision podpira dodatne podpretoce, ki jih je treba omogočiti v nastavitvah kamere. Priporočljivo je, da preveriš in uporabiš te pretoke, če so na voljo." + } + } + } + }, + "roles": { + "toast": { + "success": { + "userRolesUpdated_one": "{{count}} uporabnik, ki je imel to vlogo, je bil posodobljen na vlogo 'viewer', ki ima dostop do vseh kamer.", + "userRolesUpdated_two": "", + "userRolesUpdated_few": "", + "userRolesUpdated_other": "{{count}} uporabniki, ki so imeli to vlogo, so bili posodobljeni na vlogo 'viewer', ki ima dostop do vseh kamer.", + "createRole": "Vloga {{role}} je bila uspešno ustvarjena", + "updateCameras": "Kamere za vlogo {{role}} so bile posodobljene", + "deleteRole": "Vloga {{role}} je bila uspešno izbrisana" + }, + "error": { + "createRoleFailed": "Ustvarjanje vloge ni uspelo: {{errorMessage}}", + "updateCamerasFailed": "Posodabljanje kamer ni uspelo: {{errorMessage}}", + "deleteRoleFailed": "Brisanje vloge ni uspelo: {{errorMessage}}", + "userUpdateFailed": "Posodabljanje vlog uporabnikov ni uspelo: {{errorMessage}}" + } + }, + "management": { + "title": "Upravljanje vlog gledalcev", + "desc": "Upravljaj z vlogami gledalcev po meri in njihovimi dovoljenji za dostop do kamer." + }, + "addRole": "Dodaj vlogo", + "table": { + "role": "Vloga", + "cameras": "Kamere", + "actions": "Dejanja", + "noRoles": "Ni najdenih vlog po meri.", + "editCameras": "Uredi kamere", + "deleteRole": "Izbriši vlogo" + }, + "dialog": { + "createRole": { + "title": "Ustvari novo vlogo", + "desc": "Dodaj novo vlogo in določi dovoljenja za dostop do kamer." + }, + "editCameras": { + "title": "Uredi kamere vloge", + "desc": "Posodobi dostop do kamer za vlogo {{role}}." + }, + "deleteRole": { + "title": "Izbriši vlogo", + "desc": "Tega dejanja ni mogoče razveljaviti. Vloga bo trajno izbrisana, vsi uporabniki s to vlogo pa bodo premaknjeni v vlogo 'viewer', ki ima dostop do vseh kamer.", + "warn": "Ali si prepričan, da želiš izbrisati vlogo {{role}}?", + "deleting": "Brisanje..." + }, + "form": { + "role": { + "title": "Ime vloge", + "placeholder": "Vnesi ime vloge", + "desc": "Dovoljene so le črke, številke, pike in podčrtaji.", + "roleIsRequired": "Ime vloge je obvezno", + "roleOnlyInclude": "Ime vloge lahko vsebuje le črke, številke, . ali _", + "roleExists": "Vloga s tem imenom že obstaja." + }, + "cameras": { + "title": "Kamere", + "desc": "Izberi kamere, do katerih ima ta vloga dostop. Izbrana mora biti vsaj ena kamera.", + "required": "Izbrati moraš vsaj eno kamoero." + } + } + } + }, + "triggers": { + "toast": { + "error": { + "createTriggerFailed": "Ustvarjanje sprožilca ni uspelo: {{errorMessage}}", + "updateTriggerFailed": "Posodabljanje sprožilca ni uspelo: {{errorMessage}}", + "deleteTriggerFailed": "Brisanje sprožilca ni uspelo: {{errorMessage}}" + }, + "success": { + "deleteTrigger": "Sprožilec {{name}} je bil uspešno izbrisan.", + "updateTrigger": "Sprožilec {{name}} je bil uspešno posodobljen.", + "createTrigger": "Sprožilec {{name}} je bil uspešno ustvarjen." + } + }, + "wizard": { + "steps": { + "thresholdAndActions": "Prag in dejanja", + "configureData": "Nastavitev podatkov", + "nameAndType": "Ime in tip" + }, + "step3": { + "description": "Nastavi prag podobnosti in dejanja sprožilca." + }, + "step2": { + "description": "Nastavi vsebino, ki bo aktivirala to dejanje." + }, + "step1": { + "description": "Nastavi osnovne parametre sprožilca." + }, + "title": "Ustvari sprožilec" + }, + "dialog": { + "form": { + "actions": { + "error": { + "min": "Izbrano mora biti vsaj eno dejanje." + }, + "desc": "Frigate privzeto pošlje MQTT sporočilo za vse sprožilce. 'Podoznake' dodajo ime sprožilca k oznaki objekta. 'Atributi' pa so metapodatki, ki se shranijo ločeno.", + "title": "Dejanja" + }, + "threshold": { + "error": { + "max": "Prag je lahko največ 1", + "min": "Prag mora biti vsaj 0" + }, + "desc": "Nastavi prag podobnosti. Višji prag pomeni, da je za sprožitev potrebno natančnejše ujemaje.", + "title": "Prag" + }, + "content": { + "error": { + "required": "Vsebina je obvezna." + }, + "textDesc": "Vnesi besedilo, ki bo sprožilo dejanje ob zaznavi podobnega opisa objekta.", + "imageDesc": "Prikazanih je le zadnjih 100 sličic. Če ne najdeš želene, preveri starejše objekte v zavihku 'Razišči' (Explore) in nastavi sprožilec tam prek menija.", + "textPlaceholder": "Vnesi besedilo", + "imagePlaceholder": "Izberi sličico", + "title": "Vsebina" + }, + "type": { + "thumbnail": "Sproži, ko je zaznana podobna sličica sledenega objekta", + "title": "Tip", + "placeholder": "Izberi tip sprožilca", + "description": "Sproži, ko je zaznan podoben opis sledenega objekta" + }, + "name": { + "title": "Ime", + "placeholder": "Poimenuj sprožilec", + "description": "Vnesi unikatno ime ali opis za identifikacijo tega sprožilca", + "error": { + "minLength": "Polje mora vsebovati vsaj 2 znaka.", + "invalidCharacters": "Polje lahko vsebuje le črke, številke, podčrtaje in vezaje.", + "alreadyExists": "Sprožilec s tem imenom za to kamero že obstaja." + } + }, + "enabled": { + "description": "Omogoči ali onemogoči ta sprožilec" + } + }, + "createTrigger": { + "title": "Ustvari sprožilec", + "desc": "Ustvari sprožilec za kamero {{camera}}" + }, + "editTrigger": { + "title": "Uredi sprožilec", + "desc": "Uredi nastavitve sprožilca za kamero {{camera}}" + }, + "deleteTrigger": { + "title": "Izbriši sprožilec", + "desc": "Ali si prepričan, da želiš izbrisati sprožilec {{triggerName}}? Tega dejanja ni mogoče razveljaviti." + } + }, + "documentTitle": "Sprožilci", + "semanticSearch": { + "title": "Semantično iskanje je onemogočeno", + "desc": "Za uporabo sprožilcev mora biti omogočeno semantično iskanje." + }, + "management": { + "title": "Sprožilci (Triggers)", + "desc": "Upravljaj sprožilce za kamero {{camera}}. Uporabi tip 'sličica' za sprožitev na podlagi podobnosti s sličico sledenega objekta, ali tip 'opis' za sprožitev na podlagi podobnosti besedilu, ki ga določiš." + }, + "addTrigger": "Dodaj sprožilec", + "table": { + "name": "Ime", + "type": "Tip", + "content": "Vsebina", + "threshold": "Prag", + "actions": "Dejanja", + "noTriggers": "Za to kamero ni nastavljenih sprožilcev.", + "edit": "Uredi", + "deleteTrigger": "Izbriši sprožilec", + "lastTriggered": "Zadnjič sproženo" + }, + "type": { + "thumbnail": "Sličica", + "description": "Opis" + }, + "actions": { + "notification": "Pošlji obvestilo", + "sub_label": "Dodaj podoznako", + "attribute": "Dodaj atribut" + } + }, + "debug": { + "zones": { + "title": "Območja", + "desc": "Prikaži obrise vseh določenih območij" + }, + "title": "Razhroščevanje", + "detectorDesc": "Frigate uporablja tvoje detektorje ({{detectors}}) za zaznavanje objektov v video toku kamere.", + "desc": "Pogled za razhroščevanje prikazuje sledene objekte in njihovo statistiko v realnem času. Seznam objektov prikazuje povzetek zaznanih objektov s časovnim zamikom.", + "openCameraWebUI": "Odpri spletni vmesnik kamere {{camera}}", + "debugging": "Razhroščevanje", + "objectList": "Seznam objektov", + "noObjects": "Ni objektov", + "audio": { + "title": "Zvok", + "noAudioDetections": "Ni zaznav zvoka", + "score": "rezultat", + "currentRMS": "Trenutni RMS", + "currentdbFS": "Trenutni dbFS" + }, + "boundingBoxes": { + "title": "Okvirji (Bounding boxes)", + "desc": "Prikaži okvirje okoli sledenih objektov", + "colors": { + "label": "Barve okvirjev objektov", + "info": "
  • Ob zagonu se vsaki oznaki objekta dodeli svoja barva
  • Temno modra tanka črta pomeni, da objekt trenutno ni zaznan
  • Siva tanka črta pomeni, da je objekt zaznan kot mirujoč
  • Debela črta označuje objekt, ki mu sledi samodejno sledenje (če je omogočeno)
  • " + } + }, + "timestamp": { + "title": "Časovna značka", + "desc": "Prekrij sliko s časovno značko" + }, + "mask": { + "title": "Maske gibanja", + "desc": "Prikaži poligone mask gibanja" + }, + "motion": { + "title": "Okvirji gibanja", + "desc": "Prikaži okvirje okoli območij, kjer je zaznano gibanje", + "tips": "

    Okvirji gibanja


    Rdeči okvirji bodo prekrili dele slike, kjer se trenutno zaznava gibanje.

    " + }, + "regions": { + "title": "Regije", + "desc": "Prikaži okvir regije interesa, ki je poslana detektorju objektov", + "tips": "

    Okvirji regij


    Svetlo zeleni okvirji bodo prekrili območja interesa, ki so poslana v obdelavo detektorju objektov.

    " + }, + "paths": { + "title": "Poti", + "desc": "Prikaži pomembne točke poti sledenega objekta", + "tips": "

    Poti


    Črte in krogi prikazujejo pomembne točke, po katerih se je sledeni objekt premikal.

    " + }, + "objectShapeFilterDrawing": { + "title": "Risanje filtra oblike objekta", + "desc": "Nariši pravokotnik na sliki za prikaz podrobnosti o površini in razmerju", + "tips": "Omogoči to možnost, da na sliki narišeš pravokotnik in vidiš njegovo površino ter razmerje stranic. Te vrednosti lahko nato uporabiš za nastavitev filtrov oblike v konfiguraciji.", + "score": "Rezultat", + "ratio": "Razmerje", + "area": "Površina" + } + }, + "saveAllPreview": { + "title": "Spremembe za shranjevanje", + "triggerLabel": "Preglej čakajoče spremembe", + "empty": "Ni čakajočih sprememb.", + "scope": { + "label": "Področje", + "global": "Globalno", + "camera": "Kamera: {{cameraName}}" + }, + "field": { + "label": "Polje" + }, + "value": { + "label": "Nova vrednost", + "reset": "Ponastavi" + } + }, + "cameraManagement": { + "title": "Upravljanje kamer", + "addCamera": "Dodaj novo kamero", + "deleteCamera": "Izbriši kamero", + "deleteCameraDialog": { + "title": "Izbriši kamero", + "description": "Brisanje kamere bo trajno odstranilo vse posnetke, sledene objekte in konfiguracijo za to kamero. Morebitne go2rtc pretoke, povezane s to kamero, bo morda še vedno treba odstraniti ročno.", + "selectPlaceholder": "Izberi kamero...", + "confirmTitle": "Ali si prepričan?", + "confirmWarning": "Brisanja kamere {{cameraName}} ni mogoče razveljaviti.", + "deleteExports": "Izbriši tudi izvožene posnetke za to kamero", + "confirmButton": "Trajno izbriši", + "success": "Kamera {{cameraName}} je bila uspešno izbrisana", + "error": "Brisanje kamere {{cameraName}} ni uspelo" + }, + "editCamera": "Uredi kamero:", + "selectCamera": "Izberi kamero", + "backToSettings": "Nazaj na nastavitve kamere", + "streams": { + "title": "Omogoči / Onemogoči kamere", + "enableLabel": "Omogočene kamere", + "enableDesc": "Začasno onemogoči omogočeno kamero do ponovnega zagona Frigate. Onemogočanje popolnoma ustavi obdelavo pretokov te kamere. Zaznavanje, snemanje in razhroščevanje ne bodo na voljo.
    Opomba: To ne onemogoči go2rtc ponovnega pretakanja.", + "disableLabel": "Onemogočene kamere", + "disableDesc": "Omogoči kamero, ki trenutno ni vidna v vmesniku in je onemogočena v konfiguraciji. Po omogočanju je potreben ponovni zagon Frigate.", + "enableSuccess": "Kamera {{cameraName}} je omogočena v konfiguraciji. Ponovno zaženi Frigate za uveljavitev sprememb." + }, + "cameraConfig": { + "add": "Dodaj kamero", + "edit": "Uredi kamero", + "description": "Nastavi parametre kamere, vključno z vhodnimi pretoki in vlogami.", + "name": "Ime kamere", + "nameRequired": "Ime kamere je obvezno", + "nameLength": "Ime kamere mora biti krajše od 64 znakov.", + "namePlaceholder": "npr. vhodna_vrata ali Zadnje dvorisce", + "enabled": "Omogočeno", + "ffmpeg": { + "inputs": "Vhodni pretoki", + "path": "Pot pretoka", + "pathRequired": "Pot pretoka je obvezna", + "pathPlaceholder": "rtsp://...", + "roles": "Vloge", + "rolesRequired": "Vsaj jedna vloga je obvezna", + "rolesUnique": "Vsaka vloga (audio, detect, record) je lahko dodeljena le enemu pretoku", + "addInput": "Dodaj vhodni pretok", + "removeInput": "Odstrani vhodni pretok", + "inputsRequired": "Vsaj en vhodni pretok je obvezen" + }, + "go2rtcStreams": "go2rtc pretoki", + "streamUrls": "URL-ji pretoka", + "addUrl": "Dodaj URL", + "addGo2rtcStream": "Dodaj go2rtc pretok", + "toast": { + "success": "Kamera {{cameraName}} je bila uspešno shranjena" + } + } + }, + "cameraReview": { + "title": "Nastavitve pregleda kamere", + "object_descriptions": { + "title": "Opisi objektov z generativno UI", + "desc": "Začasno omogoči/onemogoči opise objektov z generativno UI za to kamero do ponovnega zagona. Ko je onemogočeno, se za sledene objekte na tej kameri ne bodo zahtevali AI opisi." + }, + "review_descriptions": { + "title": "Opisi pregledov z generativno UI", + "desc": "Začasno omogoči/onemogoči opise pregledov z generativno UI za to kamero do ponovnega zagona. Ko je onemogočeno, se za elemente pregleda na tej kameri ne bodo zahtevali AI opisi." + }, + "review": { + "title": "Pregled", + "desc": "Začasno omogoči/onemogoči opozorila in zaznave za to kamero do ponovnega zagona. Ko je onemogočeno, se novi elementi za pregled ne bodo generirali. ", + "alerts": "Opozorila (Alerts) ", + "detections": "Zaznave (Detections) " + }, + "reviewClassification": { + "title": "Klasifikacija pregledov", + "desc": "Frigate razvršča elemente pregleda na Opozorila (Alerts) in Zaznave (Detections). Privzeto vsi objekti oseba in avto štejejo kot Opozorila. Razvrščanje lahko izboljšaš z določitvijo zahtevanih območij zanje.", + "noDefinedZones": "Za to kamero ni določenih območij (zones).", + "objectAlertsTips": "Vsi objekti {{alertsLabels}} na kameri {{cameraName}} bodo prikazani kot Opozorila.", + "zoneObjectAlertsTips": "Vsi objekti {{alertsLabels}}, zaznani v območju {{zone}} na kameri {{cameraName}}, bodo prikazani kot Opozorila.", + "objectDetectionsTips": "Vsi objekti {{detectionsLabels}}, ki niso razvrščeni, bodo na kameri {{cameraName}} prikazani kot Zaznave, ne glede na območje.", + "zoneObjectDetectionsTips": { + "text": "Vsi objekti {{detectionsLabels}}, ki niso razvrščeni v območju {{zone}} na kameri {{cameraName}}, bodo prikazani kot Zaznave.", + "notSelectDetections": "Vsi objekti {{detectionsLabels}}, zaznani v območju {{zone}} na kameri {{cameraName}}, ki niso razvrščeni kot Opozorila, bodo prikazani kot Zaznave ne glede na območje.", + "regardlessOfZoneObjectDetectionsTips": "Vsi objekti {{detectionsLabels}}, ki niso razvrščeni, bodo na kameri {{cameraName}} prikazani kot Zaznave ne glede na območje." + }, + "unsavedChanges": "Neshranjene spremembe klasifikacije pregledov za {{camera}}", + "selectAlertsZones": "Izberi območja za Opozorila", + "selectDetectionsZones": "Izberi območja za Zaznave", + "limitDetections": "Omeji zaznave na določena območja", + "toast": { + "success": "Konfiguracija klasifikacije pregledov je shranjena. Ponovno zaženi Frigate za uveljavitev sprememb." + } + } + }, + "motionDetectionTuner": { + "title": "Nastavljalnik zaznavanja gibanja", + "unsavedChanges": "Neshranjene spremembe nastavitev gibanja ({{camera}})", + "desc": { + "title": "Frigate uporablja zaznavanje gibanja kot prvi korak, da ugotovi, ali se v kadru dogaja kaj, kar bi bilo vredno preveriti z zaznavanjem objektov.", + "documentation": "Preberi vodič za nastavljanje gibanja" + }, + "Threshold": { + "title": "Prag (Threshold)", + "desc": "Vrednost praga določa, kolikšna sprememba svetilnosti piksla je potrebna, da se šteje za gibanje. Privzeto: 30" + }, + "contourArea": { + "title": "Površina konture", + "desc": "Vrednost površine konture določa, katere skupine spremenjenih pikslov se štejejo za gibanje. Privzeto: 10" + }, + "improveContrast": { + "title": "Izboljšaj kontrast", + "desc": "Izboljša kontrast za temnejše prizore. Privzeto: VKLOPLJENO" + }, + "toast": { + "success": "Nastavitve gibanja so bile shranjene." + } + }, + "users": { + "title": "Uporabniki", + "management": { + "title": "Upravljanje uporabnikov", + "desc": "Upravljaj z uporabniškimi računi te namestitve Frigate." + }, + "addUser": "Dodaj uporabnika", + "updatePassword": "Ponastavi geslo", + "toast": { + "success": { + "createUser": "Uporabnik {{user}} je bil uspešno ustvarjen", + "deleteUser": "Uporabnik {{user}} je bil uspešno izbrisan", + "updatePassword": "Geslo je bilo uspešno posodobljeno.", + "roleUpdated": "Vloga za uporabnika {{user}} je bila posodobljena" + }, + "error": { + "setPasswordFailed": "Shranjevanje gesla ni uspelo: {{errorMessage}}", + "createUserFailed": "Ustvarjanje uporabnika ni uspelo: {{errorMessage}}", + "deleteUserFailed": "Brisanje uporabnika ni uspelo: {{errorMessage}}", + "roleUpdateFailed": "Posodobitev vloge ni uspela: {{errorMessage}}" + } + }, + "table": { + "username": "Uporabniško ime", + "actions": "Dejanja", + "role": "Vloga", + "noUsers": "Uporabnikov ni bilo mogoče najti.", + "changeRole": "Spremeni vlogo uporabnika", + "password": "Ponastavi geslo", + "deleteUser": "Izbriši uporabnika" + }, + "dialog": { + "form": { + "user": { + "title": "Uporabniško ime", + "desc": "Dovoljene so samo črke, številke, pike in podčrtaji.", + "placeholder": "Vnesi uporabniško ime" + }, + "password": { + "title": "Geslo", + "placeholder": "Vnesi geslo", + "show": "Prikaži geslo", + "hide": "Skrij geslo", + "confirm": { + "title": "Potrdi geslo", + "placeholder": "Ponovno vnesi geslo" + }, + "strength": { + "title": "Moč gesla: ", + "weak": "Šibko", + "medium": "Srednje", + "strong": "Močno", + "veryStrong": "Zelo močno" + }, + "requirements": { + "title": "Zahteve za geslo:", + "length": "Vsaj 12 znakov" + }, + "match": "Gesli se ujemata", + "notMatch": "Gesli se ne ujemata" + }, + "newPassword": { + "title": "Novo geslo", + "placeholder": "Vnesi novo geslo", + "confirm": { + "placeholder": "Ponovno vnesi novo geslo" + } + }, + "currentPassword": { + "title": "Trenutno geslo", + "placeholder": "Vnesi svoje trenutno geslo" + }, + "usernameIsRequired": "Uporabniško ime je obvezno", + "passwordIsRequired": "Geslo je obvezno" + }, + "createUser": { + "title": "Ustvari novega uporabnika", + "desc": "Dodaj nov uporabniški račun in določi vlogo za dostop do delov vmesnika Frigate.", + "usernameOnlyInclude": "Uporabniško ime lahko vsebuje le črke, številke, . ali _", + "confirmPassword": "Prosim, potrdi geslo" + }, + "deleteUser": { + "title": "Izbriši uporabnika", + "desc": "Tega dejanja ni mogoče razveljaviti. S tem boš trajno izbrisal uporabniški račun in vse povezane podatke.", + "warn": "Ali si prepričan, da želiš izbrisati uporabnika {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "Geslo ne more biti prazno", + "doNotMatch": "Gesli se ne ujemata", + "currentPasswordRequired": "Trenutno geslo je obvezno", + "incorrectCurrentPassword": "Trenutno geslo je napačno", + "passwordVerificationFailed": "Preverjanje gesla ni uspelo", + "updatePassword": "Posodobi geslo za {{username}}", + "setPassword": "Nastavi geslo", + "desc": "Ustvari močno geslo za zaščito tega računa.", + "multiDeviceWarning": "Vse ostale naprave, kjer si prijavljen, bodo zahtevale ponovno prijavo v roku {{refresh_time}}.", + "multiDeviceAdmin": "Takojšnjo ponovno avtentikacijo vseh uporabnikov lahko izsiliš tudi z rotacijo JWT ključa." + }, + "changeRole": { + "title": "Spremeni vlogo uporabnika", + "select": "Izberi vlogo", + "desc": "Posodobi dovoljenja za uporabnika {{username}}", + "roleInfo": { + "intro": "Izberi ustrezno vlogo za tega uporabnika:", + "admin": "Administrator", + "adminDesc": "Popoln dostop do vseh funkcij.", + "viewer": "Gledalec (Viewer)", + "viewerDesc": "Omejeno na nadzorne plošče v živo, preglede, raziskovanje in izvoze.", + "customDesc": "Vloga po meri s specifičnim dostopom do kamer." + } + } + } + }, + "notification": { + "title": "Obvestila", + "notificationSettings": { + "title": "Nastavitve obvestil", + "desc": "Frigate lahko pošilja potisna obvestila (push) neposredno na tvojo napravo, ko teče v brskalniku ali je nameščen kot PWA." + }, + "notificationUnavailable": { + "title": "Obvestila niso na voljo", + "desc": "Spletna potisna obvestila zahtevajo varno povezavo (https://…). To je omejitev brskalnika. Za uporabo obvestil dostopaj do Frigate prek varne povezave." + }, + "globalSettings": { + "title": "Globalne nastavitve", + "desc": "Začasno prekini obvestila za določene kamere na vseh registriranih napravah." + }, + "email": { + "title": "E-pošta", + "placeholder": "npr. primer@email.com", + "desc": "Veljaven e-poštni naslov je obvezen in bo uporabljen za obveščanje v primeru težav s potisno storitvijo." + }, + "cameras": { + "title": "Kamere", + "noCameras": "Ni razpoložljivih kamer", + "desc": "Izberi, za katere kamere želiš omogočiti obvestila." + }, + "deviceSpecific": "Nastavitve za to napravo", + "registerDevice": "Registriraj to napravo", + "unregisterDevice": "Odstrani to napravo", + "sendTestNotification": "Pošlji testno obvestilo", + "unsavedRegistrations": "Neshranjene registracije obvestil", + "unsavedChanges": "Neshranjene spremembe obvestil", + "active": "Obvestila so aktivna", + "suspended": "Obvestila odložena do {{time}}", + "suspendTime": { + "suspend": "Prekini", + "5minutes": "Prekini za 5 minut", + "10minutes": "Prekini za 10 minut", + "30minutes": "Prekini za 30 minut", + "1hour": "Prekini za 1 uro", + "12hours": "Prekini za 12 ur", + "24hours": "Prekini za 24 ur", + "untilRestart": "Prekini do ponovnega zagona" + }, + "cancelSuspension": "Prekliči prekinitev", + "toast": { + "success": { + "registered": "Registracija na obvestila je bila uspešna. Preden se lahko pošljejo kakršna koli obvestila (vključno s testnim), je potreben ponovni zagon Frigate.", + "settingSaved": "Nastavitve obvestil so shranjene." + }, + "error": { + "registerFailed": "Registracija obvestil ni uspela." + } + } + }, + "frigatePlus": { + "title": "Nastavitve Frigate+", + "description": "Frigate+ je naročniška storitev, ki omogoča dostop do dodatnih funkcij, vključno z modeli za zaznavanje objektov po meri, ki so naučeni na tvojih lastnih podatkih. Tukaj lahko upravljaš s svojimi Frigate+ modeli.", + "cardTitles": { + "api": "API", + "currentModel": "Trenutni model", + "otherModels": "Ostali modeli", + "configuration": "Konfiguracija" + }, + "apiKey": { + "title": "Frigate+ API ključ", + "validated": "API ključ je zaznan in potrjen", + "notValidated": "API ključ ni zaznan ali ni veljaven", + "desc": "API ključ omogoča integracijo s storitvijo Frigate+.", + "plusLink": "Preberi več o Frigate+" + }, + "snapshotConfig": { + "title": "Konfiguracija slik (Snapshots)", + "desc": "Pošiljanje v Frigate+ zahteva, da sta v tvoji konfiguraciji omogočena tako snapshots kot clean_copy.", + "cleanCopyWarning": "Nekatere kamere imajo omogočene slike, vendar onemogočeno možnost 'clean copy'. Da bi lahko pošiljal slike v Frigate+, moraš v nastavitvah omogočiti clean_copy.", + "table": { + "camera": "Kamera", + "snapshots": "Slike (Snapshots)", + "cleanCopySnapshots": "clean_copy slike" + } + }, + "modelInfo": { + "title": "Informacije o modelu", + "modelType": "Tip modela", + "trainDate": "Datum učenja", + "baseModel": "Osnovni model", + "plusModelType": { + "baseModel": "Osnovni model", + "userModel": "Prilagojen model (Fine-Tuned)" + }, + "supportedDetectors": "Podprti detektorji", + "cameras": "Kamere", + "loading": "Nalaganje informacij o modelu…", + "error": "Nalaganje informacij o modelu ni uspelo", + "availableModels": "Razpoložljivi modeli", + "loadingAvailableModels": "Nalaganje razpoložljivih modelov…", + "modelSelect": "Tukaj lahko izbereš svoje modele, ki so na voljo v Frigate+. Izbereš lahko le modele, ki so združljivi s tvojo trenutno konfiguracijo detektorja." + }, + "unsavedChanges": "Neshranjene spremembe Frigate+ nastavitev", + "restart_required": "Potreben ponovni zagon (sprememba Frigate+ modela)", + "toast": { + "success": "Nastavitve Frigate+ so bile shranjene. Ponovno zaženi Frigate za uveljavitev sprememb.", + "error": "Napaka pri shranjevanju konfiguracije: {{errorMessage}}" + } + }, + "detectionModel": { + "plusActive": { + "title": "Upravljanje modelov Frigate+", + "label": "Trenutni vir modela", + "description": "Ta instanca uporablja Frigate+ model. Model lahko izbereš ali spremeniš v nastavitvah Frigate+.", + "goToFrigatePlus": "Pojdi na nastavitve Frigate+", + "showModelForm": "Ročno nastavi model" + } + }, + "maintenance": { + "title": "Vzdrževanje", + "sync": { + "title": "Sinhronizacija medijev", + "desc": "Frigate redno čisti medije glede na tvoje nastavitve hrambe. Občasno se lahko pojavijo osirotele datoteke. Uporabi to funkcijo za ročno odstranitev datotek, na katere baza podatkov ne referencira več.", + "started": "Sinhronizacija medijev se je začela.", + "alreadyRunning": "Sinhronizacija že poteka.", + "error": "Zagon sinhronizacije ni uspel", + "currentStatus": "Stanje", + "jobId": "ID opravila", + "startTime": "Čas začetka", + "endTime": "Čas konca", + "statusLabel": "Status", + "results": "Rezultati", + "errorLabel": "Napaka", + "mediaTypes": "Tipi medijev", + "allMedia": "Vsi mediji", + "dryRun": "Preizkus (Dry Run)", + "dryRunEnabled": "Nobena datoteka ne bo izbrisana", + "dryRunDisabled": "Datoteke bodo izbrisane", + "force": "Prisili (Force)", + "forceDesc": "Prezri varnostni prag in dokončaj sinhronizacijo, tudi če bi bilo izbrisanih več kot 50 % datotek.", + "running": "Sinhronizacija poteka...", + "start": "Zaženi sinhronizacijo", + "inProgress": "Sinhronizacija poteka. Ta stran je trenutno onemogočena.", + "status": { + "queued": "V čakalni vrsti", + "running": "Poteka", + "completed": "Zaključeno", + "failed": "Spodletelo", + "notRunning": "Ne teče" + }, + "resultsFields": { + "filesChecked": "Preverjene datoteke", + "orphansFound": "Najdenih sirot", + "orphansDeleted": "Izbrisanih sirot", + "aborted": "Prekinjeno. Brisanje bi preseglo varnostni prag.", + "error": "Napaka", + "totals": "Skupaj" + }, + "event_snapshots": "Slike sledenih objektov", + "event_thumbnails": "Sličice sledenih objektov", + "review_thumbnails": "Sličice pregledov", + "previews": "Predogledi", + "exports": "Izvozi", + "recordings": "Posnetki" + }, + "regionGrid": { + "title": "Mreža regij (Region Grid)", + "desc": "Mreža regij je optimizacija, s katero se sistem nauči, kje se na posamezni kameri običajno pojavljajo objekti določenih velikosti. To omogoča učinkovitejše določanje regij zaznavanja. Mreža se sčasoma zgradi samodejno.", + "clear": "Počisti mrežo regij", + "clearConfirmTitle": "Počisti mrežo regij", + "clearConfirmDesc": "Čiščenje mreže regij ni priporočljivo, razen če si pred kratkim spremenil velikost modela detektorja ali fizično premaknil kamero in imaš težave s sledenjem. Mreža se bo samodejno zgradila nazaj. Za uveljavitev je potreben ponovni zagon Frigate.", + "clearSuccess": "Mreža regij je bila uspešno očiščena", + "clearError": "Čiščenje mreže regij ni uspelo", + "restartRequired": "Za uveljavitev sprememb mreže regij je potreben ponovni zagon" + } + }, + "configForm": { + "global": { + "title": "Globalne nastavitve", + "description": "Te nastavitve veljajo za vse kamere, razen če so preglase v nastavitvah posamezne kamere." + }, + "camera": { + "title": "Nastavitve kamere", + "description": "Te nastavitve veljajo le za to kamero in preglasijo globalne nastavitve." + }, + "advancedSettingsCount": "Napredne nastavitve ({{count}})", + "advancedCount": "Napredno ({{count}})", + "showAdvanced": "Prikaži napredne nastavitve", + "tabs": { + "sharedDefaults": "Skupne privzete vrednosti", + "system": "Sistem", + "integrations": "Integracije" + }, + "additionalProperties": { + "keyLabel": "Ključ", + "valueLabel": "Vrednost", + "keyPlaceholder": "Nov ključ", + "remove": "Odstrani" + }, + "timezone": { + "defaultOption": "Uporabi časovni pas brskalnika" + }, + "roleMap": { + "empty": "Ni dodelitev vlog", + "roleLabel": "Vloga", + "groupsLabel": "Skupine", + "addMapping": "Dodaj dodelitev vloge", + "remove": "Odstrani" + }, + "ffmpegArgs": { + "preset": "Prednastavitev", + "manual": "Ročni argumenti", + "inherit": "Podeduj iz nastavitev kamere", + "selectPreset": "Izberi prednastavitev", + "manualPlaceholder": "Vnesi FFmpeg argumente" + }, + "cameraInputs": { + "itemTitle": "Tok (Stream) {{index}}" + }, + "restartRequiredField": "Potreben ponovni zagon", + "restartRequiredFooter": "Konfiguracija spremenjena - potreben ponovni zagon", + "sections": { + "detect": "Zaznavanje", + "record": "Snemanje", + "snapshots": "Slike (Snapshots)", + "motion": "Gibanje", + "objects": "Objekti", + "review": "Pregled", + "audio": "Zvok", + "notifications": "Obvestila", + "live": "Pogled v živo", + "timestamp_style": "Časovne značke", + "mqtt": "MQTT", + "database": "Baza podatkov", + "telemetry": "Telemetrija", + "auth": "Avtentikacija", + "tls": "TLS", + "proxy": "Proxy", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg", + "detectors": "Detektorji", + "model": "Model", + "semantic_search": "Semantično iskanje", + "genai": "GenAI", + "face_recognition": "Prepoznava obrazov", + "lpr": "Prepoznava registrskih tablic", + "birdseye": "Birdseye" + }, + "detect": { + "title": "Nastavitve zaznavanja" + }, + "detectors": { + "title": "Nastavitve detektorjev", + "singleType": "Dovoljen je le en detektor tipa {{type}}.", + "keyRequired": "Ime detektorja je obvezno.", + "keyDuplicate": "Ime detektorja že obstaja.", + "noSchema": "Sheme detektorjev niso na voljo.", + "none": "Ni nastavljenih instanc detektorjev.", + "add": "Dodaj detektor" + }, + "record": { + "title": "Nastavitve snemanja" + }, + "snapshots": { + "title": "Nastavitve slik (Snapshots)" + }, + "motion": { + "title": "Nastavitve gibanja" + }, + "objects": { + "title": "Nastavitve objektov" + }, + "audioLabels": { + "summary": "Izbranih {{count}} oznak zvoka", + "empty": "Ni razpoložljivih oznak zvoka" + }, + "objectLabels": { + "summary": "Izbranih {{count}} tipov objektov", + "empty": "Ni razpoložljivih oznak objektov" + }, + "filters": { + "objectFieldLabel": "{{field}} za {{label}}" + }, + "zoneNames": { + "summary": "Izbrano: {{count}}", + "empty": "Ni razpoložljivih območij" + }, + "inputRoles": { + "summary": "Izbranih {{count}} vlog", + "empty": "Ni razpoložljivih vlog", + "options": { + "detect": "Zaznavanje (Detect)", + "record": "Snemanje (Record)", + "audio": "Zvok (Audio)" + } + }, + "review": { + "title": "Nastavitve pregleda" + }, + "audio": { + "title": "Nastavitve zvoka" + }, + "notifications": { + "title": "Nastavitve obvestil" + }, + "live": { + "title": "Nastavitve pogleda v živo" + }, + "timestamp_style": { + "title": "Nastavitve časovnih značk" + }, + "searchPlaceholder": "Išči..." + }, + "globalConfig": { + "title": "Globalna konfiguracija", + "description": "Nastavi globalne parametre, ki veljajo za vse kamere, razen če so preglasi.", + "toast": { + "success": "Globalne nastavitve so bile uspešno shranjene", + "error": "Shranjevanje globalnih nastavitev ni uspelo", + "validationError": "Validacija ni uspela" + } + }, + "cameraConfig": { + "title": "Konfiguracija kamere", + "description": "Nastavi parametre za posamezne kamere. Te nastavitve preglasijo globalne privzete vrednosti.", + "overriddenBadge": "Preglašeno", + "resetToGlobal": "Ponastavi na globalno", + "toast": { + "success": "Nastavitve kamere so bile uspešno shranjene", + "error": "Shranjevanje nastavitev kamere ni uspelo" + } + }, + "toast": { + "success": "Nastavitve so bile uspešno shranjene", + "applied": "Nastavitve so bile uspešno uveljavljene", + "successRestartRequired": "Nastavitve so shranjene. Ponovno zaženi Frigate, da uveljaviš spremembe.", + "error": "Shranjevanje nastavitev ni uspelo", + "validationError": "Validacija ni uspela: {{message}}", + "resetSuccess": "Ponastavljeno na globalne privzete vrednosti", + "resetError": "Ponastavitev nastavitev ni uspela", + "saveAllSuccess_one": "{{count}} sklop je bil uspešno shranjen.", + "saveAllSuccess_two": "", + "saveAllSuccess_few": "", + "saveAllSuccess_other": "Vseh {{count}} sklopov je bilo uspešno shranjenih.", + "saveAllPartial_one": "Shranjevanje uspelo za {{successCount}} od {{totalCount}} sklopa. {{failCount}} je spodletelo.", + "saveAllPartial_two": "", + "saveAllPartial_few": "", + "saveAllPartial_other": "Shranjevanje uspelo za {{successCount}} od {{totalCount}} sklopov. {{failCount}} je spodletelo.", + "saveAllFailure": "Shranjevanje vseh sklopov ni uspelo." + }, + "unsavedChanges": "Imaš neshranjene spremembe", + "confirmReset": "Potrdi ponastavitev", + "resetToDefaultDescription": "S tem boš vse nastavitve v tem sklopu ponastavil na njihove privzete vrednosti. Tega dejanja ni mogoče razveljaviti.", + "resetToGlobalDescription": "S tem boš nastavitve v tem sklopu ponastavil na globalne privzete vrednosti. Tega dejanja ni mogoče razveljaviti." +} diff --git a/web/public/locales/sl/views/system.json b/web/public/locales/sl/views/system.json new file mode 100644 index 0000000..66dbc78 --- /dev/null +++ b/web/public/locales/sl/views/system.json @@ -0,0 +1,253 @@ +{ + "documentTitle": { + "cameras": "Statistika kamer - Frigate", + "storage": "Statistika shrambe - Frigate", + "general": "Splošna statistika - Frigate", + "logs": { + "frigate": "Frigate dnevniki - Frigate", + "go2rtc": "Go2RTC dnevniki - Frigate", + "nginx": "Nginx dnevniki - Frigate", + "websocket": "Dnevniki sporočil - Frigate" + }, + "enrichments": "Statistika obogatenih podatkov - Frigate" + }, + "logs": { + "download": { + "label": "Prenesi dnevnike" + }, + "copy": { + "label": "Kopiraj v odložišče", + "success": "Dnevniki kopirani v odložišče", + "error": "Dnevnikov ni bilo mogoče kopirati" + }, + "type": { + "label": "Vrsta", + "timestamp": "Časovna značka", + "message": "Sporočilo", + "tag": "Oznaka" + }, + "tips": "Dnevniki se pretakajo s strežnika", + "toast": { + "error": { + "fetchingLogsFailed": "Napaka pri pridobivanju dnevnikov: {{errorMessage}}", + "whileStreamingLogs": "Napaka med pretakanjem dnevnikov: {{errorMessage}}" + } + }, + "websocket": { + "label": "Sporočila", + "pause": "Premor", + "resume": "Nadaljuj", + "clear": "Počisti", + "filter": { + "all": "Vse teme", + "topics": "Teme", + "events": "Dogodki", + "reviews": "Pregledi", + "classification": "Klasifikacija", + "face_recognition": "Prepoznava obrazov", + "lpr": "Prepoznava tablic", + "camera_activity": "Aktivnost kamere", + "system": "Sistem", + "camera": "Kamera", + "all_cameras": "Vse kamere", + "cameras_count_one": "{{count}} kamera", + "cameras_count_other": "{{count}} kamer" + }, + "empty": "Ni še ujetih sporočil", + "expanded": { + "payload": "Tovor (payload)" + }, + "count": "{{count}} sporočil" + } + }, + "storage": { + "recordings": { + "title": "Posnetki", + "tips": "Ta vrednost predstavlja skupno shrambo, ki jo zasedajo posnetki v podatkovni bazi Frigate. Frigate ne spremlja porabe shrambe za vse datoteke na tvojem disku.", + "earliestRecording": "Najstarejši razpoložljiv posnetek:" + }, + "title": "Shramba", + "overview": "Pregled", + "cameraStorage": { + "title": "Shramba kamer", + "camera": "Kamera", + "unusedStorageInformation": "Informacije o neporabljeni shrambi", + "storageUsed": "Shramba", + "percentageOfTotalUsed": "Odstotek skupnega", + "bandwidth": "Pasovna širina", + "unused": { + "title": "Neporabljeno", + "tips": "Ta vrednost morda ne predstavlja natančno prostega prostora, ki je na voljo programu Frigate, če imaš na disku poleg posnetkov Frigate shranjene še druge datoteke. Frigate ne spremlja porabe shrambe izven svojih posnetkov." + } + }, + "shm": { + "warning": "Trenutna velikost SHM ({{total}} MB) je premajhna. Povečaj jo na vsaj {{min_shm}} MB.", + "title": "Dodelitev SHM (deljenega pomnilnika)", + "frameLifetime": { + "title": "Življenjska doba okvirja", + "description": "Vsaka kamera ima {{frames}} rež za okvirje v deljenem pomnilniku. Pri najvišji hitrosti sličic kamere je vsak okvir na voljo približno {{lifetime}} s, preden se prepiše." + } + } + }, + "general": { + "hardwareInfo": { + "npuMemory": "NPU pomnilnik", + "title": "Strojna oprema", + "gpuUsage": "Uporaba GPU", + "gpuMemory": "GPU pomnilnik", + "gpuEncoder": "GPU kodirnik", + "gpuDecoder": "GPU dekodirnik", + "gpuInfo": { + "vainfoOutput": { + "title": "Izhod Vainfo", + "returnCode": "Vrnjena koda: {{code}}", + "processOutput": "Izhod procesa:", + "processError": "Napaka procesa:" + }, + "nvidiaSMIOutput": { + "title": "Izhod Nvidia SMI", + "name": "Ime: {{name}}", + "driver": "Gonilnik: {{driver}}", + "cudaComputerCapability": "CUDA Compute Capability: {{cuda_compute}}", + "vbios": "Informacije VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Zapri info o GPU" + }, + "copyInfo": { + "label": "Kopiraj info o GPU" + }, + "toast": { + "success": "Informacije o GPU kopirane v odložišče" + } + }, + "npuUsage": "Uporaba NPU", + "intelGpuWarning": { + "message": "Statistika GPU ni na voljo", + "description": "To je znana napaka v Intelovih orodjih za poročanje (intel_gpu_top), kjer orodje neha delovati in nenehno vrača 0 % porabo GPU, čeprav strojno pospeševanje in zaznavanje objektov pravilno delujeta na (i)GPU. To ni napaka programa Frigate. Za začasno rešitev in potrditev delovanja lahko ponovno zaženeš gostiteljski sistem. To ne vpliva na zmogljivost.", + "title": "Opozorilo za Intel GPU" + }, + "gpuTemperature": "Temperatura GPU", + "npuTemperature": "Temperatura NPU" + }, + "title": "Splošno", + "detector": { + "title": "Detektorji", + "inferenceSpeed": "Hitrost sklepanja detektorja", + "temperature": "Temperatura detektorja", + "cpuUsage": "Poraba procesorja detektorja", + "memoryUsage": "Poraba pomnilnika detektorja", + "cpuUsageInformation": "Procesor, uporabljen za pripravo vhodnih in izhodnih podatkov v/iz modelov za zaznavanje. Ta vrednost ne meri uporabe sklepanja (inference), tudi če uporabljaš GPU ali pospeševalnik." + }, + "otherProcesses": { + "title": "Drugi procesi", + "processMemoryUsage": "Poraba pomnilnika procesov", + "processCpuUsage": "Poraba procesorja procesov", + "series": { + "go2rtc": "go2rtc", + "recording": "snemanje", + "audio_detector": "detektor zvoka", + "review_segment": "segment pregleda", + "embeddings": "vdelave" + } + } + }, + "title": "Sistem", + "metrics": "Sistemske metrike", + "cameras": { + "title": "Kamere", + "overview": "Pregled", + "info": { + "aspectRatio": "razmerje stranic", + "cameraProbeInfo": "Informacije o viru kamere {{camera}}", + "streamDataFromFFPROBE": "Podatki o pretoku so pridobljeni s pomočjo ffprobe.", + "fetching": "Pridobivanje podatkov o kameri", + "stream": "Pretok {{idx}}", + "video": "Video:", + "codec": "Kodek:", + "resolution": "Ločljivost:", + "fps": "Sličic na sekundo (FPS):", + "unknown": "Neznano", + "audio": "Zvok:", + "error": "Napaka: {{error}}", + "tips": { + "title": "Informacije o viru kamere" + } + }, + "framesAndDetections": "Okvirji / Zaznave", + "label": { + "camera": "kamera", + "detect": "zaznavanje", + "skipped": "preskočeno", + "ffmpeg": "FFmpeg", + "capture": "zajem", + "overallFramesPerSecond": "skupno sličic na sekundo", + "overallDetectionsPerSecond": "skupno zaznav na sekundo", + "overallSkippedDetectionsPerSecond": "skupno preskočenih zaznav na sekundo", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} zajem", + "cameraDetect": "{{camName}} zaznavanje", + "cameraFramesPerSecond": "{{camName}} sličic na sekundo", + "cameraDetectionsPerSecond": "{{camName}} zaznav na sekundo", + "cameraSkippedDetectionsPerSecond": "{{camName}} preskočenih zaznav na sekundo" + }, + "toast": { + "success": { + "copyToClipboard": "Podatki o viru kopirani v odložišče." + }, + "error": { + "unableToProbeCamera": "Ni mogoče preveriti vira kamere: {{errorMessage}}" + } + }, + "connectionQuality": { + "title": "Kakovost povezave", + "excellent": "Odlično", + "fair": "Zadovoljivo", + "poor": "Slabo", + "unusable": "Neuporabno", + "fps": "FPS", + "expectedFps": "Pričakovan FPS", + "reconnectsLastHour": "Ponovne povezave (zadnja ura)", + "stallsLastHour": "Zastoji (zadnja ura)" + } + }, + "lastRefreshed": "Zadnja osvežitev: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} ima visoko porabo procesorja FFmpeg ({{ffmpegAvg}} %)", + "detectHighCpuUsage": "{{camera}} ima visoko porabo procesorja za zaznavanje ({{detectAvg}} %)", + "healthy": "Sistem deluje brezhibno", + "reindexingEmbeddings": "Ponovno indeksiranje vdelav ({{processed}} % končano)", + "cameraIsOffline": "Kamera {{camera}} je brez povezave", + "detectIsSlow": "Zaznavanje ({{detect}}) je počasno ({{speed}} ms)", + "detectIsVerySlow": "Zaznavanje ({{detect}}) je zelo počasno ({{speed}} ms)", + "shmTooLow": "Dodelitev /dev/shm ({{total}} MB) bi morala biti povečana na vsaj {{min}} MB.", + "debugReplayActive": "Seja ponovnega predvajanja za razhroščevanje je aktivna" + }, + "enrichments": { + "title": "Obogatitve", + "infPerSecond": "Sklepanj na sekundo", + "embeddings": { + "face_recognition": "Prepoznava obrazov", + "plate_recognition": "Prepoznava tablic", + "face_recognition_speed": "Hitrost prepoznave obrazov", + "plate_recognition_speed": "Hitrost prepoznave tablic", + "yolov9_plate_detection": "YOLOv9 zaznava tablic", + "image_embedding": "Vdelava slik", + "text_embedding": "Vdelava besedila", + "image_embedding_speed": "Hitrost vdelave slik", + "yolov9_plate_detection_speed": "YOLOv9 hitrost zaznave tablic", + "review_description": "Opis za pregled", + "review_description_speed": "Hitrost opisa za pregled", + "classification": "Klasifikacija {{name}}", + "classification_speed": "Hitrost klasifikacije {{name}}", + "classification_events_per_second": "Dogodki klasifikacije {{name}} na sekundo", + "face_embedding_speed": "Hitrost vdelave obrazov", + "text_embedding_speed": "Hitrost vdelave besedila", + "review_description_events_per_second": "Dogodki opisa za pregled", + "object_description": "Opis objekta", + "object_description_speed": "Hitrost opisa objekta", + "object_description_events_per_second": "Dogodki opisa objekta" + }, + "averageInf": "Povprečen čas sklepanja" + } +} diff --git a/web/public/locales/sq/audio.json b/web/public/locales/sq/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/audio.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/common.json b/web/public/locales/sq/common.json new file mode 100644 index 0000000..91c74b3 --- /dev/null +++ b/web/public/locales/sq/common.json @@ -0,0 +1,85 @@ +{ + "time": { + "never": "Kurrë", + "ago": "{{timeAgo}} më parë", + "today": "Sot", + "yesterday": "Dje", + "last7": "7 ditët e fundit", + "last14": "14 ditët e fundit", + "last30": "30 ditët e fundit", + "thisWeek": "Këtë javë", + "lastWeek": "Javën e kaluar", + "thisMonth": "Këtë muaj", + "lastMonth": "Muajin e kaluar", + "5minutes": "5 minuta", + "10minutes": "10 minuta", + "30minutes": "30 minuta", + "1hour": "1 orë", + "12hours": "12 orë", + "24hours": "24 orë", + "pm": "mbasdite", + "am": "paradite" + }, + "unit": { + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/orë", + "mbph": "MB/orë", + "gbph": "GB/orë" + } + }, + "label": { + "back": "Kthehu pas", + "hide": "Fsheh {{item}}", + "show": "Shfaq {{item}}", + "ID": "ID", + "none": "Asnjë", + "all": "Të gjitha", + "other": "Tjetër" + }, + "list": { + "two": "{{0}} dhe {{1}}", + "many": "{{items}}, dhe {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opsionale", + "internalID": "ID-ja e brendshme që Frigate përdor në konfigurim dhe në databazë" + }, + "button": { + "add": "Shto", + "apply": "Vendos", + "applying": "Duke vendosur…", + "reset": "Rivendos", + "undo": "Zhbëj", + "done": "Përfunduar", + "enabled": "Aktivizuar", + "enable": "Aktivizo", + "disabled": "Deaktivizuar", + "disable": "Deaktivizo", + "save": "Ruaj", + "saving": "Duke ruajtur…", + "cancel": "Anulo", + "close": "Mbyll", + "copy": "Kopjo", + "copiedToClipboard": "Kopjuar në clipboard", + "back": "Pas", + "history": "Historia", + "fullscreen": "Ekran i plotë", + "exitFullscreen": "Dil nga ekrani i plotë", + "pictureInPicture": "Fotografi në fotografi (PiP)", + "twoWayTalk": "Komunikim dyanësor", + "cameraAudio": "Zëri i kamerës", + "on": "Aktiv", + "off": "Joaktiv", + "edit": "Ndrysho", + "copyCoordinates": "Kopjo koordinatat", + "delete": "Fshij", + "yes": "Po", + "no": "Jo", + "download": "Shkarko", + "info": "Info" + } +} diff --git a/web/public/locales/sq/components/auth.json b/web/public/locales/sq/components/auth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/components/camera.json b/web/public/locales/sq/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/components/dialog.json b/web/public/locales/sq/components/dialog.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/components/filter.json b/web/public/locales/sq/components/filter.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/components/icons.json b/web/public/locales/sq/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/components/input.json b/web/public/locales/sq/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/components/input.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/components/player.json b/web/public/locales/sq/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/components/player.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/config/cameras.json b/web/public/locales/sq/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/config/global.json b/web/public/locales/sq/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/config/groups.json b/web/public/locales/sq/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/config/validation.json b/web/public/locales/sq/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/objects.json b/web/public/locales/sq/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/objects.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/chat.json b/web/public/locales/sq/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/classificationModel.json b/web/public/locales/sq/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/configEditor.json b/web/public/locales/sq/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/events.json b/web/public/locales/sq/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/events.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/explore.json b/web/public/locales/sq/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/exports.json b/web/public/locales/sq/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/faceLibrary.json b/web/public/locales/sq/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/live.json b/web/public/locales/sq/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/motionSearch.json b/web/public/locales/sq/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/recording.json b/web/public/locales/sq/views/recording.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/replay.json b/web/public/locales/sq/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/search.json b/web/public/locales/sq/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/settings.json b/web/public/locales/sq/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sq/views/system.json b/web/public/locales/sq/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sq/views/system.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sr/audio.json b/web/public/locales/sr/audio.json new file mode 100644 index 0000000..3d0bebf --- /dev/null +++ b/web/public/locales/sr/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Говор", + "scream": "Вриска", + "babbling": "Брбљање", + "bicycle": "Бицикл", + "yell": "Викање", + "car": "Аутомобил", + "bellow": "Рика", + "motorcycle": "Мотор", + "whoop": "Опа", + "whispering": "Шаптање", + "bus": "Аутобус", + "laughter": "Смех", + "train": "Воз", + "boat": "Брод", + "crying": "Плач", + "sigh": "Уздах", + "singing": "Певање", + "choir": "Хор", + "yodeling": "Јодловање", + "chant": "Појање", + "mantra": "Мантра", + "bird": "Птица", + "snicker": "Кикот", + "child_singing": "Дечје певање", + "cat": "Мачка", + "synthetic_singing": "Синтетичко певање", + "dog": "Пас", + "rapping": "Реповање", + "horse": "Коњ", + "humming": "Брундање", + "sheep": "Овца", + "groan": "Стењање", + "grunt": "Мрмљање", + "whistling": "Звиждање", + "breathing": "Дисање", + "wheeze": "Шиштање", + "snoring": "Хркање", + "gasp": "Задиханост", + "pant": "Дахтање", + "snort": "Шмркање", + "cough": "Кашаљ", + "throat_clearing": "Прочишћавање грла", + "sneeze": "Кијање", + "sniff": "Њушкање", + "run": "Трчање", + "shuffle": "Вучење ногама", + "footsteps": "Кораци", + "chewing": "Жвакање", + "biting": "Угриз", + "gargling": "Гргљање", + "stomach_rumble": "Крчање стомака", + "camera": "Камера", + "burping": "Подригивање", + "skateboard": "Скејтборд", + "hiccup": "Штуцање", + "fart": "Прдеж", + "hands": "Руке", + "finger_snapping": "Пуцање прстима", + "clapping": "Пљескање", + "heartbeat": "Откуцаји срца", + "cheering": "Навијање", + "applause": "Аплауз", + "chatter": "Жамор", + "crowd": "Гомила", + "children_playing": "Деца се играју", + "heart_murmur": "Шум на срцу", + "animal": "Животиња", + "pets": "Кућни љубимци", + "bark": "Лавеж", + "yip": "Цвиљење", + "howl": "Завијање", + "bow_wow": "Ав-ав", + "growling": "Режање", + "whimper_dog": "Цвиљење пса", + "purr": "Предење", + "meow": "Мјаукање", + "hiss": "Шиштање", + "caterwaul": "Мачје дерање", + "livestock": "Стока", + "clip_clop": "Топот копита", + "neigh": "Рзање", + "cattle": "Говеда", + "moo": "Мукање", + "cowbell": "Звоно за краве", + "pig": "Свиња", + "oink": "Рокћање", + "goat": "Коза", + "bleat": "Блејање", + "fowl": "Живина", + "chicken": "Кокошка", + "cluck": "Кокодакање", + "cock_a_doodle_doo": "Кукурикање", + "turkey": "Ћурка", + "gobble": "Глоглотање", + "duck": "Патка", + "quack": "Квакање", + "goose": "Гуска", + "honk": "Гакање", + "wild_animals": "Дивље животиње", + "roaring_cats": "Дивље мачке", + "roar": "Рика", + "chirp": "Цвркут", + "squawk": "Крештање", + "pigeon": "Голуб", + "coo": "Гукање", + "crow": "Врана", + "caw": "Грактање", + "owl": "Сова", + "hoot": "Хукање", + "flapping_wings": "Лепет крила", + "dogs": "Пси", + "rats": "Пацови", + "mouse": "Миш", + "patter": "Тапкање", + "insect": "Инсект", + "cricket": "Цврчак", + "mosquito": "Комарац", + "fly": "Мува", + "buzz": "Зујање", + "frog": "Жаба", + "croak": "Крекетање", + "snake": "Змија", + "rattle": "Звечка", + "whale_vocalization": "Вокализација кита", + "music": "Музика", + "musical_instrument": "Музички инструмент", + "plucked_string_instrument": "Жичани инструмент", + "guitar": "Гитара", + "electric_guitar": "Електрична гитара", + "bass_guitar": "Бас гитара", + "acoustic_guitar": "Акустична гитара", + "steel_guitar": "Челична гитара", + "tapping": "Тапкање", + "strum": "Брење", + "banjo": "Банџо", + "sitar": "Ситар", + "mandolin": "Мандолина", + "zither": "Цитра", + "ukulele": "Укулеле", + "keyboard": "Клавијатура", + "piano": "Клавир", + "electric_piano": "Електрични клавир", + "organ": "Оргуље", + "electronic_organ": "Електронске оргуље", + "hammond_organ": "Хамонд оргуље", + "synthesizer": "Синтисајзер", + "sampler": "Семплер", + "harpsichord": "Чембало", + "percussion": "Перкусије", + "drum_kit": "Бубањ сет", + "drum_machine": "Бубањ машина", + "drum": "Бубањ", + "snare_drum": "Мали бубањ", + "rimshot": "Ударац по обруку", + "drum_roll": "Бубањ рол", + "bass_drum": "Велики бубањ", + "timpani": "Тимпани", + "tabla": "Табла", + "cymbal": "Чинела", + "hi_hat": "Хај-хет", + "wood_block": "Дрвени блок", + "tambourine": "Даире", + "maraca": "Маракас", + "gong": "Гонг", + "tubular_bells": "Тубуларна звона", + "mallet_percussion": "Перкусије палицама", + "marimba": "Маримба", + "glockenspiel": "Глокеншпил", + "vibraphone": "Вибрафон", + "steelpan": "Стилпан", + "orchestra": "Оркестар", + "brass_instrument": "Лимени инструмент", + "french_horn": "Хорна", + "trumpet": "Труба", + "trombone": "Тромбон", + "bowed_string_instrument": "Гудачки инструмент", + "string_section": "Гудачка секција", + "violin": "Виолина", + "pizzicato": "Пицикато", + "cello": "Виолончело", + "double_bass": "Контрабас", + "wind_instrument": "Дувачки инструмент", + "flute": "Флаута", + "saxophone": "Саксофон", + "clarinet": "Кларинет", + "harp": "Харфа", + "bell": "Звоно", + "church_bell": "Црквено звоно", + "jingle_bell": "Прапорац", + "bicycle_bell": "Звоно бицикла", + "tuning_fork": "Виљушка за штимовање", + "chime": "Звоњава", + "wind_chime": "Ветрено звонце", + "harmonica": "Усна хармоника", + "accordion": "Хармоника", + "bagpipes": "Гајде", + "didgeridoo": "Диџериду", + "theremin": "Теремин", + "singing_bowl": "Тибетанска здела", + "scratching": "Скречовање", + "pop_music": "Поп музика", + "hip_hop_music": "Хип-хоп музика", + "beatboxing": "Битбоксовање", + "rock_music": "Рок музика", + "heavy_metal": "Хеви метал", + "punk_rock": "Панк рок", + "grunge": "Гранџ", + "progressive_rock": "Прогресивни рок", + "rock_and_roll": "Рок енд рол", + "psychedelic_rock": "Психоделични рок", + "rhythm_and_blues": "Ритам и блуз", + "soul_music": "Соул музика", + "reggae": "Реге", + "country": "Кантри", + "swing_music": "Свинг музика", + "bluegrass": "Блуграс", + "funk": "Фанк", + "folk_music": "Народна музика", + "middle_eastern_music": "Блискоисточна музика", + "jazz": "Џез", + "disco": "Диско", + "classical_music": "Класична музика", + "opera": "Опера", + "electronic_music": "Електронска музика", + "house_music": "Хаус музика", + "techno": "Техно", + "dubstep": "Дабстеп", + "drum_and_bass": "Драм енд бејс", + "electronica": "Електроника", + "electronic_dance_music": "Електронска плесна музика", + "ambient_music": "Амбијент музика", + "trance_music": "Транс музика", + "music_of_latin_america": "Латиноамеричка музика", + "salsa_music": "Салса музика", + "flamenco": "Фламенко", + "blues": "Блуз", + "music_for_children": "Музика за децу", + "new-age_music": "Њу ејџ музика", + "vocal_music": "Вокална музика", + "a_capella": "А капела", + "music_of_africa": "Афричка музика", + "afrobeat": "Афробит", + "christian_music": "Хришћанска музика", + "gospel_music": "Госпел музика", + "music_of_asia": "Азијска музика", + "carnatic_music": "Карнатска музика", + "music_of_bollywood": "Боливудска музика", + "ska": "Ска", + "traditional_music": "Традиционална музика", + "independent_music": "Независна музика", + "song": "Песма", + "background_music": "Позадинска музика", + "theme_music": "Тематска музика", + "jingle": "Џингл", + "soundtrack_music": "Музика филмског записа", + "lullaby": "Успаванка", + "video_game_music": "Музика видео игара", + "christmas_music": "Божићна музика", + "dance_music": "Плесна музика", + "wedding_music": "Свадбена музика", + "happy_music": "Весела музика", + "sad_music": "Тужна музика", + "tender_music": "Нежна музика", + "exciting_music": "Узбудљива музика", + "angry_music": "Бесна музика", + "scary_music": "Страшна музика", + "wind": "Ветар", + "rustling_leaves": "Шуштање лишћа", + "wind_noise": "Шум ветра", + "thunderstorm": "Олуја", + "thunder": "Гром", + "water": "Вода", + "rain": "Киша", + "raindrop": "Кап кише", + "rain_on_surface": "Киша на површини", + "stream": "Поток", + "waterfall": "Водопад", + "ocean": "Океан", + "waves": "Таласи", + "steam": "Пара", + "gurgling": "Клокотање", + "fire": "Ватра", + "crackle": "Пуцкетање", + "vehicle": "Возило", + "sailboat": "Једрилица", + "rowboat": "Чамац", + "motorboat": "Моторни чамац", + "ship": "Лађа", + "motor_vehicle": "Моторно возило", + "toot": "Труба", + "car_alarm": "Аларм аутомобила", + "power_windows": "Електрични прозори", + "skidding": "Проклизавање", + "tire_squeal": "Шкрипа гума", + "car_passing_by": "Пролазак аутомобила", + "race_car": "Тркачки аутомобил", + "truck": "Камион", + "air_brake": "Ваздушна кочница", + "air_horn": "Ваздушна сирена", + "reversing_beeps": "Звук вожње уназад", + "ice_cream_truck": "Камион за сладолед", + "emergency_vehicle": "Возило хитних служби", + "police_car": "Полицијски аутомобил", + "ambulance": "Амбулантно возило", + "fire_engine": "Ватрогасно возило", + "traffic_noise": "Шум саобраћаја", + "rail_transport": "Железнички саобраћај", + "train_whistle": "Звиждук воза", + "train_horn": "Сирена воза", + "railroad_car": "Железнички вагон", + "train_wheels_squealing": "Шкрипа точкова воза", + "subway": "Метро", + "aircraft": "Авион", + "aircraft_engine": "Мотор авиона", + "jet_engine": "Млазни мотор", + "propeller": "Пропелер", + "helicopter": "Хеликоптер", + "fixed-wing_aircraft": "Авион са фиксним крилима", + "engine": "Мотор", + "light_engine": "Лаки мотор", + "dental_drill's_drill": "Зубарска бушилица", + "lawn_mower": "Косилица", + "chainsaw": "Моторна тестера", + "medium_engine": "Средњи мотор", + "heavy_engine": "Тешки мотор", + "engine_knocking": "Лупање мотора", + "engine_starting": "Покретање мотора", + "idling": "Празан ход", + "accelerating": "Убрзавање", + "door": "Врата", + "doorbell": "Звоно на вратима", + "ding-dong": "Динг-донг", + "sliding_door": "Клизна врата", + "slam": "Лупање", + "knock": "Куцање", + "tap": "Тапкање", + "squeak": "Шкрипа", + "cupboard_open_or_close": "Отварање или затварање ормарића", + "drawer_open_or_close": "Отварање или затварање фиоке", + "dishes": "Судови", + "cutlery": "Прибор за јело", + "chopping": "Сецкање", + "frying": "Пржење", + "microwave_oven": "Микроталасна пећница", + "blender": "Блендер", + "water_tap": "Славина", + "sink": "Судопера", + "bathtub": "Када", + "hair_dryer": "Фен за косу", + "toilet_flush": "Пуштање воде", + "toothbrush": "Четкица за зубе", + "electric_toothbrush": "Електрична четкица за зубе", + "vacuum_cleaner": "Усисивач", + "zipper": "Рајсфершлус", + "keys_jangling": "Звецкање кључева", + "coin": "Новчић", + "scissors": "Маказе", + "electric_shaver": "Електрични бријач", + "shuffling_cards": "Мешање карата", + "typing": "Куцање", + "typewriter": "Писаћа машина", + "computer_keyboard": "Тастатура", + "writing": "Писање", + "alarm": "Аларм", + "telephone": "Телефон", + "telephone_bell_ringing": "Звоњава телефона", + "ringtone": "Мелодија звона", + "telephone_dialing": "Бирање броја", + "dial_tone": "Тон за бирање", + "busy_signal": "Заузет сигнал", + "alarm_clock": "Будилник", + "siren": "Сирена", + "civil_defense_siren": "Сирена цивилне заштите", + "buzzer": "Зујалица", + "smoke_detector": "Детектор дима", + "fire_alarm": "Противпожарни аларм", + "foghorn": "Сирена за маглу", + "whistle": "Звиждук", + "steam_whistle": "Парна пиштаљка", + "mechanisms": "Механизми", + "ratchet": "Чегртаљка", + "clock": "Сат", + "tick": "Откуцај", + "tick-tock": "Тик-так", + "gears": "Зупчаници", + "pulleys": "Кајише", + "sewing_machine": "Шиваћа машина", + "mechanical_fan": "Вентилатор", + "air_conditioning": "Клима уређај", + "cash_register": "Каса", + "printer": "Штампач", + "single-lens_reflex_camera": "Рефлексни фотоапарат", + "tools": "Алати", + "hammer": "Чекић", + "jackhammer": "Пнеуматска бушилица", + "sawing": "Тестерисање", + "filing": "Турпијање", + "sanding": "Брушење", + "power_tool": "Електрични алат", + "drill": "Бушилица", + "explosion": "Експлозија", + "gunshot": "Пуцањ", + "machine_gun": "Митраљез", + "fusillade": "Рафал", + "artillery_fire": "Артиљеријска паљба", + "cap_gun": "Капислара", + "fireworks": "Ватромет", + "firecracker": "Петарда", + "burst": "Прскање", + "eruption": "Ерупција", + "boom": "Бум", + "wood": "Дрво", + "chop": "Сечење", + "splinter": "Пуцање дрвета", + "crack": "Прасак", + "glass": "Стакло", + "chink": "Звецкање", + "shatter": "Разбијање", + "silence": "Тишина", + "sound_effect": "Звучни ефекат", + "environmental_noise": "Амбијентална бука", + "static": "Статички шум", + "white_noise": "Бели шум", + "pink_noise": "Розе шум", + "television": "Телевизија", + "radio": "Радио", + "field_recording": "Теренски снимак", + "sodeling": "Напевавање", + "chird": "Цвркут", + "change_ringing": "Звоњава на промену", + "shofar": "Шофар", + "liquid": "Течност", + "splash": "Пљусак", + "slosh": "Шљапкање", + "squish": "Гњечење", + "drip": "Капање", + "pour": "Сипање", + "trickle": "Цурење", + "gush": "Млаз", + "fill": "Пуњење", + "spray": "Прскање", + "pump": "Пумпа", + "stir": "Мешање", + "boiling": "Кључање", + "sonar": "Сонар", + "arrow": "Стрела", + "whoosh": "Фијук", + "thump": "Тупи ударац", + "thunk": "Тупо лупање", + "electronic_tuner": "Електронски штимер", + "effects_unit": "Јединица за ефекте", + "chorus_effect": "Хорус ефекат", + "basketball_bounce": "Одбијање лопте", + "bang": "Тресак", + "slap": "Шамар", + "whack": "Ударац", + "smash": "Лом", + "breaking": "Ломљење", + "bouncing": "Одскакање", + "whip": "Бич", + "flap": "Лепет", + "scratch": "Гребање", + "scrape": "Стругање", + "rub": "Трљање", + "roll": "Котрљање", + "crushing": "Мрвљење", + "crumpling": "Гужвање", + "tearing": "Цепање", + "beep": "Бип", + "ping": "Пинг", + "ding": "Динг", + "clang": "Звекет", + "squeal": "Цвиљење", + "creak": "Шкрипање", + "rustle": "Шуштање", + "whir": "Бруј", + "clatter": "Клопарање", + "sizzle": "Цврчање", + "clicking": "Клацкање", + "clickety_clack": "Клик-клак", + "rumble": "Тутњава", + "plop": "Пљус", + "hum": "Брујање", + "zing": "Зврндање", + "boing": "Бојнг", + "crunch": "Крцкање", + "sine_wave": "Синусни талас", + "harmonic": "Хармоник", + "chirp_tone": "Цвркут тон", + "pulse": "Пулс", + "inside": "Унутра", + "outside": "Напољу", + "reverberation": "Реверберација", + "echo": "Ехо", + "noise": "Бука", + "mains_hum": "Брујање мреже", + "distortion": "Дисторзија", + "sidetone": "Бочни тон", + "cacophony": "Какофонија", + "throbbing": "Пулсирање", + "vibration": "Вибрација" +} diff --git a/web/public/locales/sr/common.json b/web/public/locales/sr/common.json new file mode 100644 index 0000000..1408ef4 --- /dev/null +++ b/web/public/locales/sr/common.json @@ -0,0 +1,79 @@ +{ + "time": { + "untilForTime": "Do {{time}}", + "untilForRestart": "Dok se Frigate ponovo ne pokrene.", + "untilRestart": "Do ponovnog pokretanja", + "ago": "{{timeAgo}} pre", + "justNow": "Upravo sada", + "today": "Danas", + "yesterday": "Juče", + "last7": "Zadnjih 7 dana", + "last14": "Zadnjih 14 dana", + "last30": "Zadnjih 30 dana", + "thisWeek": "Ove nedelje", + "lastWeek": "Prošle nedelje", + "thisMonth": "Ovog meseca", + "lastMonth": "Prošlog meseca", + "5minutes": "5 minuta", + "10minutes": "10 minuta", + "30minutes": "30 minuta", + "1hour": "1 sat", + "12hours": "12 sati", + "24hours": "24 sata", + "pm": "pm", + "am": "am", + "yr": "{{time}}god", + "year_one": "{{time}} година", + "year_few": "{{time}} године", + "year_other": "{{time}} година", + "mo": "{{time}}mes", + "month_one": "{{time}} месец", + "month_few": "{{time}} месеца", + "month_other": "{{time}} месеци", + "d": "{{time}}d", + "day_one": "{{time}} дан", + "day_few": "{{time}} дана", + "day_other": "{{time}} дана", + "h": "{{time}}h", + "hour_one": "{{time}} сат", + "hour_few": "{{time}} сата", + "hour_other": "{{time}} сати", + "m": "{{time}}m", + "minute_one": "{{time}} минут", + "minute_few": "{{time}} минута", + "minute_other": "{{time}} минута", + "s": "{{time}}s", + "second_one": "{{time}} секунда", + "second_few": "{{time}} секунде", + "second_other": "{{time}} секунди", + "formattedTimestampHourMinute": { + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + } + }, + "readTheDocumentation": "Прочитајте документацију" +} diff --git a/web/public/locales/sr/components/auth.json b/web/public/locales/sr/components/auth.json new file mode 100644 index 0000000..177b293 --- /dev/null +++ b/web/public/locales/sr/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Korisničko ime", + "password": "Lozinka", + "login": "Uloguj se", + "errors": { + "usernameRequired": "Korisničko ime je obavezno", + "passwordRequired": "Lozinka je obavezna", + "rateLimit": "Prekoračeno ograničenje brzine. Pokušajte ponovo kasnije.", + "loginFailed": "Prijava nije uspela", + "unknownError": "Nepoznata greška. Proveri logove.", + "webUnknownError": "Nepoznata greška. Proveri logove u konzoli." + }, + "firstTimeLogin": "Пријављујете се по први пут? Креденцијали су одштампани у логовима Фригејта." + } +} diff --git a/web/public/locales/sr/components/camera.json b/web/public/locales/sr/components/camera.json new file mode 100644 index 0000000..b5e9c5f --- /dev/null +++ b/web/public/locales/sr/components/camera.json @@ -0,0 +1,86 @@ +{ + "group": { + "label": "Grupa kamera", + "add": "Dodajte grupu kamera", + "edit": "Uredite grupu kamera", + "delete": { + "label": "Izbrišite grupu kamera", + "confirm": { + "title": "Potvrdi Brisanje", + "desc": "Da li ste sigurni da želite da obrišete grupu kamera {{name}}?" + } + }, + "name": { + "label": "Ime", + "placeholder": "Unesite ime…", + "errorMessage": { + "mustLeastCharacters": "Naziv grupe kamera mora imati bar 2 karaktera.", + "exists": "Група камера са тим именом већ постоји.", + "nameMustNotPeriod": "Назив групе камера не сме да садржи запету.", + "invalid": "Назив групе камера није исправан." + } + }, + "cameras": { + "label": "Камере", + "desc": "Изаберите камере за ову групу." + }, + "icon": "Иконица", + "success": "Група камера ({{name}}) је сачувана.", + "camera": { + "birdseye": "Птичије око", + "setting": { + "label": "Подешавање стримовања камере", + "title": "{{cameraName}} подешавања стримовања", + "desc": "Промена опција за стримовање уживо за контролну таблу групе камера. Ова подешавања су везана за уређај/браузер.", + "audioIsAvailable": "Звук је доступан у овом стриму", + "audioIsUnavailable": "Звук није доступан за овај стрим", + "audio": { + "tips": { + "title": "Звук мора бити излаз из ваше камере и подешен у go2rtc за овај стрим." + } + }, + "stream": "Стрим", + "placeholder": "Изаберите стрим", + "streamMethod": { + "label": "Метод стриминга", + "placeholder": "Изаберите метод стриминга", + "method": { + "noStreaming": { + "label": "Нема стриминга", + "desc": "Слике камере ће бити ажуриране једном у минуту и неће се приказати стриминг уживо." + }, + "smartStreaming": { + "label": "Паметан стриминг (препоручено)", + "desc": "Паметан стриминг ће ажурирати слике камере једном у минуту, када нема детектоване активности, да би се уштедело на мрежном саобраћају и ресурсима. Када је детектована активност, слика ће аутоматски прећи на стриминг уживо." + }, + "continuousStreaming": { + "label": "Непрекидно стримовање", + "desc": { + "title": "Слика камере ће увек бити стрим уживо када је видљива на контролној табли, чак и када активност није детектована.", + "warning": "Непрекидно стримовање може проузроковати високу употребу мрежу и проблеме са перформансама. Користити опрезно." + } + } + } + }, + "compatibilityMode": { + "label": "Режим компатибилности", + "desc": "Омогућите ову опцију само ако репродукција стрима уживо на камери приказује обојене артифекте и има дијагоналну линију на десној страни слице." + } + } + } + }, + "debug": { + "options": { + "label": "Подешавања", + "title": "Опције", + "showOptions": "Приказ опција", + "hideOptions": "Скривање опција" + }, + "boundingBox": "Оквир", + "timestamp": "Временски тренутак", + "zones": "Зоне", + "mask": "Маска", + "motion": "Покрет", + "regions": "Региони" + } +} diff --git a/web/public/locales/sr/components/dialog.json b/web/public/locales/sr/components/dialog.json new file mode 100644 index 0000000..c4cd640 --- /dev/null +++ b/web/public/locales/sr/components/dialog.json @@ -0,0 +1,118 @@ +{ + "restart": { + "title": "Da li želite da restartujete Frigate?", + "button": "Ponovo pokreni", + "restarting": { + "title": "Frigate se ponovo pokreće", + "content": "Ova stranica će se ponovo učitati za {{countdown}} sekundi.", + "button": "Prisilno ponovno učitavanje" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Pošalji na Frigate+", + "desc": "Objekti na lokacijama koje želite da izbegnete nisu lažno pozitivni. Slanje lažno pozitivnih rezultata će zbuniti model." + }, + "review": { + "question": { + "ask_a": "Da li je ovaj objekat {{label}}?", + "label": "Потврдите ову ознаку за Фригејт+", + "ask_an": "Да ли је овај објекат {{label}}?", + "ask_full": "Да ли је овај објекат {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Послато" + } + } + }, + "video": { + "viewInHistory": "Преглед у историји" + } + }, + "export": { + "time": { + "fromTimeline": "Изаберите са временске линије", + "lastHour_one": "Последњи {{count}} сат", + "lastHour_few": "Последња {{count}} сата", + "lastHour_other": "Последњих {{count}} сати", + "custom": "Прилагођено", + "start": { + "title": "Почетно време", + "label": "Изаберите почетно време" + }, + "end": { + "title": "Коначно време", + "label": "Изаберите завршно време" + } + }, + "name": { + "placeholder": "Назив извоза" + }, + "select": "Избор", + "export": "Извоз", + "selectOrExport": "Избор или извоз", + "toast": { + "success": "Извоз је успешно започет. Прегледајте фајл на страници са извозима.", + "view": "Преглед", + "error": { + "failed": "Неуспешан почетак извоза: {{error}}", + "endTimeMustAfterStartTime": "Завршно време мора бити након почетног времена", + "noVaildTimeSelected": "Није изабран валидан временски распон" + } + }, + "fromTimeline": { + "saveExport": "Чување извоза", + "previewExport": "Преглед извоза" + } + }, + "streaming": { + "label": "Стрим", + "restreaming": { + "disabled": "Поновни стриминг није омогућен за ову камеру.", + "desc": { + "title": "Подесите go2rtc за додатне опције репродукције слике и звука уживо за ову камеру." + } + }, + "showStats": { + "label": "Приказ статистике стримовања", + "desc": "Омогућите ову опцију за приказ статистике стримовања као надслој на преноса са камере." + }, + "debugView": "Приказ за дебаговање" + }, + "search": { + "saveSearch": { + "label": "Сачувати претрагу", + "desc": "Обезбедите назив за ову сачувану претрагу.", + "placeholder": "Унесите име за вашу претрагу", + "overwrite": "{{searchName}} већ постоји. Чување ће преписати постојећу вредност.", + "success": "Претрага ({{searchName}}) је сачувана.", + "button": { + "save": { + "label": "Чување ове претраге" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "Потврдите брисање", + "desc": { + "selected": "Да ли сте сигурни да желите да обришете све видео снимке повезане са овом ставком?

    Држите притиснут Shift тастер да прескочите овај дијалог у будућности." + }, + "toast": { + "success": "Видео снимак повезан са изабраним ставкама за преглед успешно је обрисан.", + "error": "Неуспешно брисање: {{error}}" + } + }, + "button": { + "export": "Извоз", + "markAsReviewed": "Означити као прегледано", + "markAsUnreviewed": "Означити као непрегледано", + "deleteNow": "Обрисати сада" + } + }, + "imagePicker": { + "selectImage": "Избор сличице за праћени објекат" + } +} diff --git a/web/public/locales/sr/components/filter.json b/web/public/locales/sr/components/filter.json new file mode 100644 index 0000000..21c7991 --- /dev/null +++ b/web/public/locales/sr/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Filter", + "labels": { + "label": "Ознаке", + "all": { + "title": "Sve oznake", + "short": "Oznake" + }, + "count_one": "{{count}} Oznaka", + "count_other": "{{count}} Oznake" + }, + "zones": { + "label": "Zone", + "all": { + "title": "Sve zone", + "short": "Zone" + } + }, + "classes": { + "label": "Класе", + "all": { + "title": "Све класе" + }, + "count_one": "{{count}} класа", + "count_other": "{{count}} класа" + }, + "dates": { + "selectPreset": "Изаберите предефинисано…", + "all": { + "title": "Сви датуми", + "short": "Датуми" + } + }, + "more": "Још филтера", + "reset": { + "label": "Ресетовање филтера на подразумеване вредности" + }, + "timeRange": "Распон времена", + "subLabels": { + "label": "Под-ознаке", + "all": "Све под-ознаке" + }, + "attributes": { + "label": "Атрибути класификације", + "all": "Сви атрибути" + }, + "score": "Резултат", + "estimatedSpeed": "Процењена брзина ({{unit}})", + "features": { + "label": "Особине", + "hasSnapshot": "Постоји снимак", + "hasVideoClip": "Постоји видео клип", + "submittedToFrigatePlus": { + "label": "Послато у Frigate+", + "tips": "Прво морате филтрирати праћене објекте који имају снимак.

    Праћени објекти без снимка не могу бити послати у Frigate+." + } + }, + "sort": { + "label": "Сортирање", + "dateAsc": "Датум (растући)", + "dateDesc": "Дату (опадајући)", + "scoreAsc": "Резултат објекта (растући)", + "scoreDesc": "Резултат објекта (опадајући)", + "speedAsc": "Процењена брзина (растућа)", + "speedDesc": "Процењена брзина (опадајућа)", + "relevance": "Значај" + }, + "cameras": { + "label": "Филтери камера", + "all": { + "title": "Све камере", + "short": "Камере" + } + }, + "review": { + "showReviewed": "Прикажи прегледане" + }, + "motion": { + "showMotionOnly": "Прикажи само покрете" + }, + "explore": { + "settings": { + "title": "Подешавања", + "defaultView": { + "title": "Подразумевани приказ", + "desc": "Када нису изабрани филтери приказати сажето најскорије праћене објекте по ознакама или приказати нефилтрирану табелу.", + "summary": "Сумарно", + "unfilteredGrid": "Нефилтрирана табела" + }, + "gridColumns": { + "title": "Колоне табеле", + "desc": "Изаберите број колона табеле које ће се приказати." + }, + "searchSource": { + "label": "Извор претраге", + "desc": "Изаберите да ли да се претражују сличице или описи праћених објеката.", + "options": { + "thumbnailImage": "Сличица", + "description": "Опис" + } + } + }, + "date": { + "selectDateBy": { + "label": "Изаберите датум по ком ће се филтрирати" + } + } + }, + "logSettings": { + "label": "Ниво логовања филтера", + "filterBySeverity": "Филтрирање логова по озбиљности", + "loading": { + "title": "Учитавање", + "desc": "Када се лог панел скролује до дна, нови записи се аутоматски приказују чим су додати." + }, + "disableLogStreaming": "Искључивање ажурирања лога", + "allLogs": "Сви логови" + }, + "trackedObjectDelete": { + "title": "Потврдите брисање", + "desc": "Брисање ових {{objectLength}} праћених објеката уклања снимке, све сачуване ембединге, као и све повезанезаписе везане за животни циклус објекта. Снимци праћених објеката у Историји НЕЋЕ бити избрисани.

    Да ли сте сигурни да желите да наставите?

    Држите притиснут Shift тастер да прескочите овај дијалог у будућности.", + "toast": { + "success": "Праћени објекти су успешно обрисани.", + "error": "Брисање праћених објеката није успело: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Филтрирање по маскама зоне" + }, + "recognizedLicensePlates": { + "title": "Препознате регистарске таблице", + "loadFailed": "Учитавање препознатих регистарских таблица није успело.", + "loading": "Учитавање препознатих регистарских таблица…", + "placeholder": "Претражите регистарске таблице…", + "noLicensePlatesFound": "Регистарске таблице нису пронађене.", + "selectPlatesFromList": "Изаберите једне или више таблица са листе.", + "selectAll": "Изаберите све", + "clearAll": "Почистите све" + } +} diff --git a/web/public/locales/sr/components/icons.json b/web/public/locales/sr/components/icons.json new file mode 100644 index 0000000..4bc5937 --- /dev/null +++ b/web/public/locales/sr/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Izaberite ikonu", + "search": { + "placeholder": "Potraži ikonu…" + } + } +} diff --git a/web/public/locales/sr/components/input.json b/web/public/locales/sr/components/input.json new file mode 100644 index 0000000..b05c1f6 --- /dev/null +++ b/web/public/locales/sr/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Sačuvaj video", + "toast": { + "success": "Preuzimanje vašeg videa za recenziju je počelo." + } + } + } +} diff --git a/web/public/locales/sr/components/player.json b/web/public/locales/sr/components/player.json new file mode 100644 index 0000000..afe5402 --- /dev/null +++ b/web/public/locales/sr/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Nije pronađen nijedan snimak za ovo vreme", + "noPreviewFound": "Pregled nije pronađen", + "noPreviewFoundFor": "Nije pronađen pregled za {{cameraName}}", + "submitFrigatePlus": { + "title": "Pošaljite ovaj frejm na Frigate+?", + "submit": "Pošalji" + }, + "livePlayerRequiredIOSVersion": "Za ovaj tip prenosa uživo potreban je iOS 17.1 ili noviji.", + "streamOffline": { + "title": "Strim je oflajn", + "desc": "Ни један фрејм није добијен од {{cameraName}} detect, проверите лог грешака" + }, + "cameraDisabled": "Камера је онемогућена", + "stats": { + "streamType": { + "title": "Тип стрима:", + "short": "Тип" + }, + "bandwidth": { + "title": "Пропусни опсег:", + "short": "Ширина опсега" + }, + "latency": { + "title": "Кашњење:", + "value": "{{seconds}} секунди", + "short": { + "title": "Кашњење", + "value": "{{seconds}}s" + } + }, + "totalFrames": "Укупно фрејмова:", + "droppedFrames": { + "title": "Изгубљени фрејмови:", + "short": { + "title": "Испуштено", + "value": "{{droppedFrames}} фрејмова" + } + }, + "decodedFrames": "Декодовани фрејмови:", + "droppedFrameRate": "Однос испуштених фрејмова:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Фрејм је успешно послат у Фригејт+" + }, + "error": { + "submitFrigatePlusFailed": "Неуспешно слање фрејма у Фригејт+" + } + } +} diff --git a/web/public/locales/sr/config/cameras.json b/web/public/locales/sr/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sr/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sr/config/global.json b/web/public/locales/sr/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sr/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sr/config/groups.json b/web/public/locales/sr/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sr/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sr/config/validation.json b/web/public/locales/sr/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sr/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sr/objects.json b/web/public/locales/sr/objects.json new file mode 100644 index 0000000..cf99ee9 --- /dev/null +++ b/web/public/locales/sr/objects.json @@ -0,0 +1,67 @@ +{ + "person": "Osoba", + "bicycle": "Бицикл", + "car": "Аутомобил", + "motorcycle": "Мотор", + "airplane": "Avion", + "bus": "Аутобус", + "train": "Воз", + "boat": "Брод", + "traffic_light": "Семафор", + "fire_hydrant": "Хидрант", + "street_sign": "Улични знак", + "stop_sign": "Знак стоп", + "parking_meter": "Паркинг апарат", + "bench": "Клупа", + "bird": "Птица", + "cat": "Мачка", + "dog": "Пас", + "horse": "Коњ", + "sheep": "Овца", + "cow": "Крава", + "elephant": "Слон", + "bear": "Медвед", + "zebra": "Зебра", + "giraffe": "Жирафа", + "hat": "Капа", + "backpack": "Ранац", + "umbrella": "Кишобран", + "shoe": "Ципела", + "eye_glasses": "Наочаре", + "handbag": "Ручна торба", + "tie": "Кравата", + "suitcase": "Актовка", + "frisbee": "Фризби", + "skis": "Скије", + "snowboard": "Сноуборд", + "sports_ball": "Спортска лопта", + "baseball_bat": "Палица за бејзбол", + "baseball_glove": "Рукавица за бејзбол", + "kite": "Змај", + "skateboard": "Скејтборд", + "surfboard": "Даска за сурфовање", + "tennis_racket": "Тениски рекет", + "bottle": "Боца", + "plate": "Тањир", + "wine_glass": "Чаша за вино", + "cup": "Шоља", + "fork": "Виљушка", + "knife": "Нож", + "spoon": "Кашика", + "bowl": "Посуда", + "banana": "Банана", + "apple": "Јабука", + "animal": "Животиња", + "bark": "Лавеж", + "goat": "Коза", + "mouse": "Миш", + "keyboard": "Клавијатура", + "vehicle": "Возило", + "door": "Врата", + "blender": "Блендер", + "sink": "Судопера", + "hair_dryer": "Фен за косу", + "toothbrush": "Четкица за зубе", + "scissors": "Маказе", + "clock": "Сат" +} diff --git a/web/public/locales/sr/views/chat.json b/web/public/locales/sr/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sr/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sr/views/classificationModel.json b/web/public/locales/sr/views/classificationModel.json new file mode 100644 index 0000000..81650c5 --- /dev/null +++ b/web/public/locales/sr/views/classificationModel.json @@ -0,0 +1,94 @@ +{ + "documentTitle": "Класификациони модели - Фригејт", + "details": { + "scoreInfo": "Резултат представља просечно поверење у класификацију код свих откривања овог објекта.", + "none": "Ниједан", + "unknown": "Непознато" + }, + "button": { + "deleteClassificationAttempts": "Брисање класификационих слика", + "renameCategory": "Преименовање класе", + "deleteCategory": "Брисање класе", + "deleteImages": "Брисање слика", + "trainModel": "Модел за тренирање", + "addClassification": "Додавање класификације", + "deleteModels": "Брисање модела", + "editModel": "Уређивање модела" + }, + "tooltip": { + "trainingInProgress": "Модел се тренутно тренира", + "noNewImages": "Нема нових слика за тренирање. Класификујте више слика прво у датасету.", + "noChanges": "Нема измена у датасету од последњег тренирања.", + "modelNotReady": "Модел није спреман за тренирање" + }, + "toast": { + "success": { + "deletedCategory_one": "Обрисана класа", + "deletedCategory_few": "", + "deletedCategory_other": "", + "deletedImage_one": "Обрисане слике", + "deletedImage_few": "", + "deletedImage_other": "", + "deletedModel_one": "Успешно је обрисан {{count}} модел", + "deletedModel_few": "Успешно су обрисана {{count}} модела", + "deletedModel_other": "Успешно је обрисано {{count}} модела", + "categorizedImage": "Успешно класификована слика", + "trainedModel": "Успешно трениран модел.", + "trainingModel": "Тренирање модела је успешно започето.", + "updatedModel": "Успешно је ажурирана конфигурација модела", + "renamedCategory": "Класа је успешно преименована у {{name}}" + }, + "error": { + "deleteImageFailed": "Неуспешно брисање: {{errorMessage}}", + "deleteCategoryFailed": "Неуспешно брисање класе: {{errorMessage}}", + "deleteModelFailed": "Неуспешно брисање модела: {{errorMessage}}", + "categorizeFailed": "Неуспешна категоризација слике: {{errorMessage}}", + "trainingFailed": "Неуспешно тренирање модела. Проверите Фригејт логове за детаље.", + "trainingFailedToStart": "Неуспешан почетак тренирања модела: {{errorMessage}}", + "updateModelFailed": "Неуспешно ажурирање модела: {{errorMessage}}", + "renameCategoryFailed": "Неуспешно преименовање класе: {{errorMessage}}" + } + }, + "train": { + "titleShort": "Скорашње", + "title": "Скорашње класификације" + }, + "deleteCategory": { + "title": "Брисање класе", + "desc": "Да ли сте сигурни да желите да обришете класу {{name}}? Тиме ће трајно бити обрисане и све придружене слике и биће потребно поновно тренирање модела.", + "minClassesTitle": "Није могуће обрисати класу", + "minClassesDesc": "Класификациони модел мора имати најмање две класе. Додајте нову класу пре него што избришете ову." + }, + "deleteModel": { + "title": "Брисање класификационог модела", + "single": "Да ли сте сигурни да желите да обришете {{name}}? Ово ће трајно обрисати све повезане податке, укључујући слике и податке за тренирање. Ова акција се не може накнадно опозвати.", + "desc_one": "Да ли сте сигурни да желите да обришете {{count}} модел? Ово ће трајно обрисати све повезане податке, укључујући и слике и податке за тренирање. Ова акција не може бити опозвана накнадно.", + "desc_few": "Да ли сте сигурни да желите да обришете {{count}} модела? Ово ће трајно обрисати све повезане податке, укључујући и слике и податке за тренирање. Ова акција не може бити опозвана накнадно.", + "desc_other": "Да ли сте сигурни да желите да обришете {{count}} модела? Ово ће трајно обрисати све повезане податке, укључујући и слике и податке за тренирање. Ова акција не може бити опозвана накнадно." + }, + "edit": { + "title": "Уређивање класификационог модела", + "descriptionState": "Уређивање класа за класификациони модел овог стања. Измене ће захтевати поновно тренирање модела.", + "descriptionObject": "Уређивање типа објекта и типа касификације за овај објекат класификационог модела.", + "stateClassesInfo": "Напомена: Измена класа стања захтева поновно тренирање модела са ажурираним класама." + }, + "deleteDatasetImages": { + "title": "Брисање слика датасета", + "desc_one": "Да ли сте сигурни да желите да обришете {{count}} слику из {{dataset}}? Ова акција се не може накнадно опозвати и захтева поновно тренирање модела.", + "desc_few": "Да ли сте сигурни да желите да обришете {{count}} слике из {{dataset}}? Ова акција се не може накнадно опозвати и захтева поновно тренирање модела.", + "desc_other": "Да ли сте сигурни да желите да обришете {{count}} слика из {{dataset}}? Ова акција се не може накнадно опозвати и захтева поновно тренирање модела." + }, + "deleteTrainImages": { + "title": "Брисање слика за тренирање", + "desc_one": "Да ли сте сигурни да желите да избришете {{count}} слику? Ова акција не може бити накнадно опозвана.", + "desc_few": "Да ли сте сигурни да желите да избришете {{count}} слике? Ова акција не може бити накнадно опозвана.", + "desc_other": "Да ли сте сигурни да желите да избришете {{count}} слика? Ова акција не може бити накнадно опозвана." + }, + "renameCategory": { + "title": "Преименовање класе", + "desc": "Унесите ново име за {{name}}. Мораћете поново да тренирате модел да би промена имала ефекта." + }, + "description": { + "invalidName": "Неисправно име. Имена могу да садрже само слова, цифре, размаке, апострофе, доње црте и повлаке." + } +} diff --git a/web/public/locales/sr/views/configEditor.json b/web/public/locales/sr/views/configEditor.json new file mode 100644 index 0000000..18fba5a --- /dev/null +++ b/web/public/locales/sr/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor Konfiguracije - Frigate", + "configEditor": "Editor konfiguracije", + "copyConfig": "Kopiraj konfiguraciju", + "saveAndRestart": "Sačuvaj & Ponovo pokreni", + "saveOnly": "Samo sačuvaj", + "confirm": "Izađi bez čuvanja?", + "toast": { + "success": { + "copyToClipboard": "Konfiguracija je kopirana u clipboard." + }, + "error": { + "savingError": "Грешка при чувању конфигурације" + } + }, + "safeConfigEditor": "Уређивач конфигурације (безбедан режим)", + "safeModeDescription": "Фригејт је у безбедном режиму због грешке при провери исправности конфигурације." +} diff --git a/web/public/locales/sr/views/events.json b/web/public/locales/sr/views/events.json new file mode 100644 index 0000000..a07288b --- /dev/null +++ b/web/public/locales/sr/views/events.json @@ -0,0 +1,65 @@ +{ + "alerts": "Upozorenja", + "detections": "Detekcije", + "motion": { + "label": "Pokret", + "only": "Samo pokret" + }, + "allCameras": "Sve Kamere", + "empty": { + "alert": "Nema upozorenja za pregled", + "detection": "Nema detekcija za pregled", + "motion": "Nema podataka o pokretu", + "recordingsDisabled": { + "title": "Снимање мора бити омогућено", + "description": "Преглед ставки може бити креиран само за камеру када је омогућено снимање за њу." + } + }, + "timeline": "Временска линија", + "timeline.aria": "Изаберите временску линију", + "zoomIn": "Зумирање", + "zoomOut": "Одзумирање", + "events": { + "label": "Догађаји", + "aria": "Изаберите догађаје", + "noFoundForTimePeriod": "Нису пронађени догађају у овом периоду." + }, + "detail": { + "label": "Детаљ", + "noDataFound": "Нема детаљних података за преглед", + "aria": "Промена детаљности прегледа", + "trackedObject_one": "{{count}} објеката", + "trackedObject_other": "{{count}} објеката", + "noObjectDetailData": "Нема података о детаљима објекта.", + "settings": "Подешавање приказа детаља", + "alwaysExpandActive": { + "title": "Увек проширити активан", + "desc": "Увек прошири детаље кативног прегледа објекта, уколико су доступни." + } + }, + "objectTrack": { + "trackedPoint": "Праћена тачка", + "clickToSeek": "Кликните да претражите у ово време" + }, + "documentTitle": "Преглед - Фригејт", + "recordings": { + "documentTitle": "Снимци - Фригејт" + }, + "calendarFilter": { + "last24Hours": "Последња 24 часа" + }, + "markAsReviewed": "Означавање да је прегледано", + "markTheseItemsAsReviewed": "Означи ове ставке као прегледане", + "newReviewItems": { + "label": "Приказ нових ставки за прегледање", + "button": "Нове ставке за преглед" + }, + "selected_one": "{{count}} изабрано", + "selected_other": "{{count}} изабрано", + "select_all": "Све", + "camera": "Камера", + "detected": "детектовано", + "normalActivity": "Нормално", + "needsReview": "Потребан је преглед", + "securityConcern": "Безбедносно питање" +} diff --git a/web/public/locales/sr/views/explore.json b/web/public/locales/sr/views/explore.json new file mode 100644 index 0000000..c79ae1f --- /dev/null +++ b/web/public/locales/sr/views/explore.json @@ -0,0 +1,79 @@ +{ + "documentTitle": "Istraži - Frigate", + "generativeAI": "Generativni AI", + "exploreMore": "Istražite više {{label}} objekata", + "exploreIsUnavailable": { + "title": "Istraživanje je nedostupno", + "embeddingsReindexing": { + "context": "Istraživanje se može koristiti nakon što se završi reindeksiranje ugrađivanja praćenih objekata.", + "startingUp": "Pokretanje…", + "estimatedTime": "Procenjeno preostalo vreme:", + "finishingShortly": "Завршава се ускоро", + "step": { + "thumbnailsEmbedded": "Угњеждене сличице: ", + "descriptionsEmbedded": "Угњеждени описи: ", + "trackedObjectsProcessed": "Обрађени праћени објекти: " + } + }, + "downloadingModels": { + "context": "Фригејт преузима неопходне embedding моделе за подршку могућности Семантичке претраге. Ово може потрајати неколико минута, зависно од брзине Ваше мрежне везе.", + "setup": { + "visionModel": "Модел визије", + "visionModelFeatureExtractor": "Екстрактор особина модела визије", + "textModel": "Модел текста", + "textTokenizer": "Токенизатор текста" + }, + "tips": { + "context": "Можда ћете желети да реиндексујете ембединге праћених објеката када модели буду преузети." + }, + "error": "Дошло је до грешке. Проверите логове." + } + }, + "details": { + "timestamp": "Временски тренутак" + }, + "trackedObjectDetails": "Детаљи праћеног објекта", + "type": { + "details": "детаљи", + "snapshot": "снимак", + "thumbnail": "сличица", + "video": "видео", + "tracking_details": "детаљи праћења" + }, + "trackingDetails": { + "title": "Детаљи праћења", + "noImageFound": "Нема слике за овај временски тренутак.", + "createObjectMask": "Креирање маске објекта", + "adjustAnnotationSettings": "Прилагођавање подешавања анотације", + "scrollViewTips": "Кликните да видите значајне моменте у животном циклусу овог објекта.", + "autoTrackingTips": "Позиције оквира неће бити тачне за камере са аутоматским праћењем.", + "count": "{{first}} од {{second}}", + "trackedPoint": "Праћена тачка", + "lifecycleItemDesc": { + "visible": "{{label}} детектован", + "entered_zone": "{{label}} је ушао у {{zones}}", + "active": "{{label}} се активирао", + "stationary": "{{label}} је постао стационаран", + "attribute": { + "faceOrLicense_plate": "{{attribute}} детектован за {{label}}", + "other": "{{label}} је препознат као {{attribute}}" + }, + "gone": "{{label}} преостало", + "heard": "{{label}} се чуло", + "external": "{{label}} детектован", + "header": { + "zones": "Зоне", + "ratio": "Однос", + "area": "Подручје", + "score": "Резултат" + } + }, + "annotationSettings": { + "title": "Подешавања анотације", + "showAllZones": { + "title": "Приказ свих зона", + "desc": "Увек приказати зоне на фрејмовима у којима су објекти ушли у зону." + } + } + } +} diff --git a/web/public/locales/sr/views/exports.json b/web/public/locales/sr/views/exports.json new file mode 100644 index 0000000..2f5acb7 --- /dev/null +++ b/web/public/locales/sr/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Izvoz - Frigate", + "search": "Pretraga", + "noExports": "Nije pronađen nijedan izvoz", + "deleteExport": "Izbriši izvoz", + "deleteExport.desc": "Da li zaista želite obrisati {{exportName}}?", + "editExport": { + "title": "Preimenuj izvoz", + "desc": "Unesite novo ime za ovaj izvoz.", + "saveExport": "Sačuvaj izvoz" + }, + "tooltip": { + "shareExport": "Дељење експорта", + "downloadVideo": "Преузимање видео-снимка", + "editName": "Уређивање имена", + "deleteExport": "Брисање извоза" + }, + "toast": { + "error": { + "renameExportFailed": "Промена назива извоза није успела: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/sr/views/faceLibrary.json b/web/public/locales/sr/views/faceLibrary.json new file mode 100644 index 0000000..cb90686 --- /dev/null +++ b/web/public/locales/sr/views/faceLibrary.json @@ -0,0 +1,96 @@ +{ + "description": { + "addFace": "Додавање нове колекције у библиотеку лица отпремањем прве слике.", + "placeholder": "Unesite ime za ovu kolekciju", + "invalidName": "Неисправно име. Имена могу да садрже само слова, цифре, размаке, апострофе, доње црте и повлаке." + }, + "details": { + "person": "Osoba", + "subLabelScore": "Sub Label Skor", + "scoreInfo": "Rezultat podoznake je otežan rezultat za sve prepoznate pouzdanosti lica, tako da se može razlikovati od rezultata prikazanog na snimku.", + "face": "Detalji lica", + "faceDesc": "Detalji praćenog objekta koji je generisao ovo lice", + "timestamp": "Временски тренутак", + "unknown": "Непознато" + }, + "documentTitle": "Библиотека лица - Фригејт", + "uploadFaceImage": { + "title": "Отпремање слике лица", + "desc": "Орпремање слике за скенирање за лица и укључивање у {{pageToggle}}" + }, + "collections": "Колекције", + "createFaceLibrary": { + "new": "Креирање новог лика", + "nextSteps": "За изградњу снажне основе:
  • Користите картицу \"Скорашња препознавања\" да бисте изабрали и тренирали на сликама за сваку од откривених особа.
  • За најбоље резултате фокусирајте се на фронталне слике; избегавајте тренирање на сликама где су лица приказана под углом.
  • " + }, + "steps": { + "faceName": "Унесите назив лица", + "uploadFace": "Отпремање слике лица", + "nextSteps": "Следећи кораци", + "description": { + "uploadFace": "Отпремите слику {{name}} која приказује његово/њено лице спреда. Слика не мора да садржи само лице." + } + }, + "train": { + "title": "Скорашња препознавања", + "titleShort": "Скорашње", + "aria": "Изаберите скорашња препознавања", + "empty": "Нема скорашњих покушаја препознавања лица" + }, + "deleteFaceLibrary": { + "title": "Брисање имена", + "desc": "Да ли сте сигурни да желите да обришете колекцију {{name}}? То ће трајно обрисати и сва придружена лица." + }, + "deleteFaceAttempts": { + "title": "Обрисана лица", + "desc_one": "Да ли сте сигурни да желите да обришете {{count}} лице? Ова акција се не може опозвати накнадно.", + "desc_few": "Да ли сте сигурни да желите да обришете {{count}} лица? Ова акција се не може опозвати накнадно.", + "desc_other": "Да ли сте сигурни да желите да обришете {{count}} лица? Ова акција се не може опозвати накнадно." + }, + "renameFace": { + "title": "Преименовање лица", + "desc": "Унесите ново име за {{name}}" + }, + "button": { + "deleteFaceAttempts": "Брисање лица", + "addFace": "Додавање лица", + "renameFace": "Преименовање лица", + "deleteFace": "Брисање лица", + "uploadImage": "Отпремање слике", + "reprocessFace": "Поново обради лице" + }, + "imageEntry": { + "validation": { + "selectImage": "Изаберите фајл са сликом." + }, + "dropActive": "Превуците слику овде…", + "dropInstructions": "Превуците или пејстујте слику овде, или кликните за избор", + "maxSize": "Максимална величина: {{size}}MB" + }, + "nofaces": "Нема доступних лица", + "trainFaceAs": "Тренирање лица као:", + "trainFace": "Тренирање лица", + "toast": { + "success": { + "uploadedImage": "Слика је успешно отпремљена.", + "addFaceLibrary": "{{name}} је успешно додат у библиотеку лица!", + "deletedFace_one": "Успешно је обрисано {{count}} лице.", + "deletedFace_few": "Успешно је обрисано {{count}} лица.", + "deletedFace_other": "Успешно је обрисано {{count}} лица.", + "deletedName_one": "{{count}} лице је успешно обрисано.", + "deletedName_few": "{{count}} лица су успешно обрисана.", + "deletedName_other": "{{count}} лица је успешно обрисано.", + "renamedFace": "Лице је успешно преименовано у {{name}}", + "trainedFace": "Лице је успешно истренирано.", + "updatedFaceScore": "Успешно је ажуриран резултат лица за {{name}} ({{score}})." + }, + "error": { + "uploadingImageFailed": "Неуспешно отпремање слике: {{errorMessage}}", + "addFaceLibraryFailed": "Неуспешно постављање имена лица: {{errorMessage}}", + "deleteFaceFailed": "Неуспешно брисање: {{errorMessage}}", + "deleteNameFailed": "Неуспешно брисање имена: {{errorMessage}}", + "renameFaceFailed": "Неуспешна промена назива лица: {{errorMessage}}", + "trainFailed": "Неуспешно тренирање: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/sr/views/live.json b/web/public/locales/sr/views/live.json new file mode 100644 index 0000000..3364e7c --- /dev/null +++ b/web/public/locales/sr/views/live.json @@ -0,0 +1,113 @@ +{ + "documentTitle": "Uživo - Frigate", + "documentTitle.withCamera": "{{camera}} - Uživo - Frigate", + "lowBandwidthMode": "Režim niskog propusnog opsega", + "twoWayTalk": { + "enable": "Omogući dvosmerni razgovor", + "disable": "Onemogućite dvosmerni razgovor" + }, + "cameraAudio": { + "enable": "Omogući zvuk kamere", + "disable": "Onemogući zvuk kamere" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kliknite na sliku da bi centrirali kameru", + "enable": "Укључивање померања кликом", + "disable": "Онемогућавање померања кликом" + }, + "left": { + "label": "Померање PTZ камере у лево" + }, + "up": { + "label": "Окретање PTZ камере на горе" + }, + "down": { + "label": "Окретање PTZ камере на доле" + }, + "right": { + "label": "Окретање PTZ камере у десно" + } + }, + "zoom": { + "in": { + "label": "Зумирање PTZ камере" + }, + "out": { + "label": "Одзумирање PTZ камере" + } + }, + "focus": { + "in": { + "label": "Фокусирање PTZ камере" + }, + "out": { + "label": "Одфокусирање PTZ камере" + } + }, + "frame": { + "center": { + "label": "Кликните унутар фрејма да центрирате PTZ камеру" + } + }, + "presets": "Пресети PTZ камере" + }, + "camera": { + "enable": "Укључивање камере", + "disable": "Онемогућавање камере" + }, + "muteCameras": { + "enable": "Мутирање свих камера", + "disable": "Демутирање свих камера" + }, + "detect": { + "enable": "Укључивање детекције", + "disable": "Искључивање детекције" + }, + "recording": { + "enable": "Укључивање снимања", + "disable": "Искључивање снимања" + }, + "snapshots": { + "enable": "Укључивање снепшотова", + "disable": "Онемогућивање снепшотова" + }, + "snapshot": { + "takeSnapshot": "Преузимање тренутног снимка", + "noVideoSource": "Нема доступних извора за снепшот.", + "captureFailed": "Неуспешно прављење снепшота.", + "downloadStarted": "Преузимање снепшота је започето." + }, + "audioDetect": { + "enable": "Омогућавање детектовања звука", + "disable": "Онемогућавање детекције звука" + }, + "transcription": { + "enable": "Омогућавање транскрибовања звука уживо", + "disable": "Искључивање транскрибовања звука уживо" + }, + "autotracking": { + "enable": "Омогућавање аутоматског праћења", + "disable": "Онемогућити аутоматско праћење" + }, + "streamStats": { + "enable": "Приказ статистике стримовања", + "disable": "Скривање статистике стримовања" + }, + "manualRecording": { + "title": "На захтев", + "tips": "Преузимање тренутног снепшота или ручно покретање догађаја засновано на подешавањима задржавања снимања ове камере.", + "playInBackground": { + "label": "Пустити у позадини", + "desc": "Укључите ову опцију да наставите стримовање када је плејер скривен." + }, + "showStats": { + "label": "Приказ статистике" + }, + "debugView": "Приказ за дебаговање", + "start": "Почетак снимања на захтев", + "started": "Ручно снимање на захтев је започето.", + "failedToStart": "Неуспешно покретање ручног снимања на захтев." + } +} diff --git a/web/public/locales/sr/views/motionSearch.json b/web/public/locales/sr/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sr/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sr/views/recording.json b/web/public/locales/sr/views/recording.json new file mode 100644 index 0000000..2a12e9b --- /dev/null +++ b/web/public/locales/sr/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filter", + "export": "Izvezi", + "calendar": "Kalendar", + "filters": "Filteri", + "toast": { + "error": { + "noValidTimeSelected": "Nije izabran važeći vremenski opseg", + "endTimeMustAfterStartTime": "Vreme završetka mora biti posle vremena početka" + } + } +} diff --git a/web/public/locales/sr/views/replay.json b/web/public/locales/sr/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sr/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sr/views/search.json b/web/public/locales/sr/views/search.json new file mode 100644 index 0000000..0c65ef3 --- /dev/null +++ b/web/public/locales/sr/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Pretraga", + "savedSearches": "Sačuvane pretrage", + "searchFor": "Pretraži {{inputValue}}", + "button": { + "clear": "Obriši pretragu", + "save": "Чување претраге", + "delete": "Izbrišite sačuvanu pretragu", + "filterInformation": "Filtriraj informacije", + "filterActive": "Aktivni filteri" + }, + "trackedObjectId": "ID праћеног објекта", + "filter": { + "label": { + "cameras": "Камере", + "labels": "Ознаке", + "zones": "Зоне", + "sub_labels": "Под-ознаке", + "attributes": "Атрибути", + "search_type": "Тип претраге", + "time_range": "Временски распон", + "before": "Пре", + "after": "Након", + "min_score": "Најнижи резултат", + "max_score": "Најбољи резултат", + "min_speed": "Најнижа брзина", + "max_speed": "Највиша брзина", + "recognized_license_plate": "Препознате регистарске таблице", + "has_clip": "Има клип", + "has_snapshot": "Има снепшот" + }, + "searchType": { + "thumbnail": "Сличица", + "description": "Опис" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Датум 'пре' мора бити након датума 'после'.", + "afterDatebeEarlierBefore": "Датум 'после' мора бити пре датума 'пре'.", + "minScoreMustBeLessOrEqualMaxScore": "'min_score' мора бити нижи или један 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'max_score' мора бити већи или једнак од 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'min_speed' мора бити мања или једнака 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'max_speed' мора бити већа или једнака 'min_speed'." + } + }, + "tips": { + "title": "Како да се користе филтери за текст", + "desc": { + "text": "Филтери Вам помажу да сузите резултате претраге. Ево како да их користите у пољима за унос:", + "step1": "Откуцајте назив кључа филтера а затим две тачке (нпр. \"cameras:\").", + "step2": "Изаберите предложену или сопствену вредност.", + "step3": "Примените више филтера тако што ћете их додати један за другим са размаком између.", + "step4": "Филтери за датум (пре: и касније:) користе {{DateFormat}} формат.", + "step5": "Филтер за временски распон користи {{exampleTime}} формат.", + "step6": "Уклоните филтере кликом на 'x' поред њих.", + "exampleLabel": "Пример:" + } + }, + "header": { + "currentFilterType": "Филтрирање вредности", + "noFilters": "Филтери", + "activeFilters": "Активни филтери" + } + }, + "similaritySearch": { + "title": "Претрага сличности", + "active": "Претрага по сличности је активна", + "clear": "Почистити претрагу сличности" + }, + "placeholder": { + "search": "Претрага…" + } +} diff --git a/web/public/locales/sr/views/settings.json b/web/public/locales/sr/views/settings.json new file mode 100644 index 0000000..d3c55e6 --- /dev/null +++ b/web/public/locales/sr/views/settings.json @@ -0,0 +1,87 @@ +{ + "documentTitle": { + "default": "Podešavanja - Frigate", + "authentication": "Podešavanja autentifikacije - Fregate", + "camera": "Podešavanje kamera - Frigate", + "enrichments": "Podešavanja obogaćivanja - Frigate", + "masksAndZones": "Uređivač maski i zona - Frigate", + "motionTuner": "Tjuner pokreta - Frigate", + "general": "Подењавања UI - Фригејт", + "cameraManagement": "Управљање камерама - Фригејт", + "cameraReview": "Преглед подешавања камере - Фригејт", + "object": "Дебаговање - Фригејт", + "frigatePlus": "Подешавања за Фригејт+ - Фригејт", + "notifications": "Подешавања обавештавања - Фригејт" + }, + "menu": { + "ui": "UI", + "enrichments": "Обогаћивања", + "cameraManagement": "Управљање", + "cameraReview": "Преглед", + "masksAndZones": "Маске / Зоне", + "motionTuner": "Подешавач покрета", + "triggers": "Окидачи", + "debug": "Дебаговање", + "users": "Корисници", + "roles": "Улоге", + "notifications": "Нотификације", + "frigateplus": "Фригејт+" + }, + "dialog": { + "unsavedChanges": { + "title": "Имате несачуване измене.", + "desc": "Да ли желите да сачувате измене пре наставка?" + } + }, + "cameraSetting": { + "camera": "Камера", + "noCamera": "Нема камере" + }, + "general": { + "title": "UI подешавања", + "liveDashboard": { + "title": "Контролна табла уживо", + "automaticLiveView": { + "label": "Аутоматси преглед уживо", + "desc": "Аутоматско пребацивање на преглед камере уживо када је активност детектована. Онемогућавање ове опције доводи до тога да се слике камере на контролној тапли ажурирају једном у минуту." + }, + "playAlertVideos": { + "label": "Репродукуј видео за узбуну", + "desc": "Скорашњa упозорења се на контролној табли подразумевано репродукују као мали видеи који се понављају. Онемогућите ову опцију за приказивање само статичке слике скорашњих упозорења (на овом уређају/браузеру)." + }, + "displayCameraNames": { + "label": "Увек приказивати називе камера", + "desc": "Увек приказивати називе камера у чипу у репродукцији уживо више камера на контролној табли." + }, + "liveFallbackTimeout": { + "desc": "Када стрим високог квалитета није доступан за камеру, спусти се на режим споре мреже након оволико секунди. Подразумевано 3." + } + }, + "storedLayouts": { + "title": "Сачувани распореди", + "desc": "Распоред камера у групи може бити превлачен и може му се променити величина. Позиције су складиштене у локалном Веб браузеру.", + "clearAll": "Чишћење свих распореда" + }, + "cameraGroupStreaming": { + "title": "Подешавање стримовања за групу камера", + "desc": "Подешавања стримовања за сваку групу камера чувају се у локалном браузеру.", + "clearAll": "Чишћење свих подешавања стримовања" + }, + "recordingsViewer": { + "title": "Преглед снимака", + "defaultPlaybackRate": { + "label": "Подразумевана брзина репродукције", + "desc": "Подразумевана брзина репродукције за снимке." + } + }, + "calendar": { + "title": "Календар", + "firstWeekday": { + "label": "Први дан у недељи", + "desc": "Дан којим недеље у календару прегледа почињу.", + "sunday": "Недеља", + "monday": "Понедељак" + } + } + } +} diff --git a/web/public/locales/sr/views/system.json b/web/public/locales/sr/views/system.json new file mode 100644 index 0000000..436c9da --- /dev/null +++ b/web/public/locales/sr/views/system.json @@ -0,0 +1,92 @@ +{ + "documentTitle": { + "cameras": "Statusi kamera - Frigate", + "storage": "Statistika skladištenja - Frigate", + "general": "Општа статистика - Фригејт", + "enrichments": "Statistika obogaćivanja - Frigate", + "logs": { + "frigate": "Frigate logovi - Frigate", + "go2rtc": "Go2RTC dnevnici - Frigate", + "nginx": "Nginx logovi - Frigate" + } + }, + "title": "Sistem", + "metrics": "Системске метрике", + "logs": { + "download": { + "label": "Преузимање логова" + }, + "copy": { + "label": "Копирање", + "success": "Логови су копирани", + "error": "Копирање логова није успело" + }, + "type": { + "label": "Тип", + "timestamp": "Временски тренутак", + "tag": "Ознака", + "message": "Порука" + }, + "tips": "Логови стримују са сервера", + "toast": { + "error": { + "fetchingLogsFailed": "Грешка при преузимању логова: {{errorMessage}}", + "whileStreamingLogs": "Грешка код стримовања логова: {{errorMessage}}" + } + } + }, + "general": { + "title": "Опште", + "detector": { + "title": "Детектори", + "inferenceSpeed": "Детектор брзине закључивања", + "temperature": "Детектор температуре", + "cpuUsage": "Детектор употребе CPU-а", + "cpuUsageInformation": "CPU коришћен за припрему улаза и излаза података у/из модела детекције. Ова вредност не мери коришћење инференције, чак ни када користи GPU или акселератор.", + "memoryUsage": "Употреба меморије од стране детектора" + }, + "hardwareInfo": { + "title": "Информације о хардверу", + "gpuUsage": "Употреба GPU", + "gpuMemory": "GPU меморија", + "gpuEncoder": "GPU енкодер", + "gpuDecoder": "GPU декодер", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo Output", + "returnCode": "Повратни код: {{code}}", + "processOutput": "Обрада излаза:", + "processError": "Грешка у обради:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI излаз", + "name": "Назив: {{name}}", + "driver": "Драјвер: {{driver}}", + "cudaComputerCapability": "Способност CUDA рачунања: {{cuda_compute}}", + "vbios": "VBios Info: {{vbios}}" + }, + "closeInfo": { + "label": "Затварање GPU информација" + }, + "copyInfo": { + "label": "Копирање GPU ифнормација" + }, + "toast": { + "success": "Копиране су GPU информације у клипборд" + } + }, + "npuUsage": "Употреба NPU", + "npuMemory": "NPU меморија", + "intelGpuWarning": { + "title": "Упозорење за Intel GPU статистику", + "message": "GPU статистика није доступна", + "description": "Ово је познати баг у алатима за извештавање статистике код Intel GPU (intel_gpu_top) где се јавља пуцање и враћа 0% као GPU искоришћење, чак и у случајевима када хардверска акцелерација и детектовање објекта регуларно раде на (i)GPU. Ово није баг у Фригејту. Можете рестартовати хост да привремено поправите проблем и потврдите да GPU ради исправно. Ово не утиче на перформансе." + } + }, + "otherProcesses": { + "title": "Остали процеси", + "processCpuUsage": "Процесна употреба CPU", + "processMemoryUsage": "Процесна употреба меморије" + } + } +} diff --git a/web/public/locales/sv/audio.json b/web/public/locales/sv/audio.json new file mode 100644 index 0000000..2de942a --- /dev/null +++ b/web/public/locales/sv/audio.json @@ -0,0 +1,503 @@ +{ + "yell": "Skrik", + "bicycle": "Cykel", + "speech": "Tal", + "car": "Bil", + "bellow": "Vrål", + "motorcycle": "Motorcykel", + "whispering": "Viskning", + "bus": "Buss", + "babbling": "Babblande", + "whoop": "Skrika", + "camera": "Kamera", + "laughter": "Skratt", + "snicker": "Fnittra", + "crying": "Gråt", + "choir": "Kör", + "singing": "Sjunger", + "yodeling": "Joddling", + "chant": "Sång", + "mantra": "Mantra", + "synthetic_singing": "Syntetisk sång", + "rapping": "Rappar", + "groan": "Stöna", + "grunt": "Grymta", + "whistling": "Visslar", + "breathing": "Andas", + "snoring": "Snarkning", + "gasp": "Flämtning", + "pant": "Flämta", + "cough": "Hosta", + "throat_clearing": "Halsrensning", + "sneeze": "Nysa", + "run": "Spring", + "shuffle": "Blanda", + "footsteps": "Fotsteg", + "chewing": "Tugga", + "biting": "Biter", + "gargling": "Gurgling", + "stomach_rumble": "Magljud", + "burping": "Rapning", + "hiccup": "Hicka", + "fart": "Fis", + "hands": "Händer", + "finger_snapping": "Knäppning med fingrar", + "clapping": "Klappar", + "heartbeat": "Hjärtslag", + "heart_murmur": "Blåsljud i hjärtat", + "cheering": "Glädjande", + "applause": "Applåder", + "chatter": "Prat", + "crowd": "Folkmassa", + "animal": "Djur", + "yip": "Japp", + "howl": "Tjut", + "bow_wow": "Bow Wow", + "growling": "Morrande", + "whimper_dog": "Hund gnäll", + "cat": "Katt", + "meow": "Mjau", + "hiss": "Väsa", + "caterwaul": "Kattgräl", + "livestock": "Boskap", + "horse": "Häst", + "clip_clop": "Klipp Clop", + "neigh": "Gnägga", + "cattle": "Boskap", + "oink": "Oink", + "goat": "Get", + "bleat": "Bräka", + "fowl": "Fjäderfä", + "cluck": "Kluck", + "cock_a_doodle_doo": "kukilikuk", + "turkey": "kalkon", + "gobble": "Gobble", + "duck": "Anka", + "quack": "Quack", + "goose": "Gås", + "honk": "Tuta", + "wild_animals": "Vilda djur", + "roaring_cats": "Rytande katter", + "roar": "Rytande", + "bird": "Fågel", + "chirp": "Kvittra", + "squawk": "Skriande", + "pigeon": "Duva", + "caw": "Kraxa", + "owl": "Uggla", + "hoot": "Tuta", + "dogs": "Hundar", + "rats": "Råttor", + "mouse": "Älg", + "music": "Musik", + "sigh": "Suck", + "child_singing": "Barnsång", + "sheep": "Får", + "wheeze": "Väsande", + "dog": "Hund", + "sniff": "Sniffa", + "humming": "Hummar", + "pets": "Husdjur", + "coo": "Kuttra", + "snort": "Fnysa", + "children_playing": "Barn som leker", + "bark": "Skall", + "purr": "Spinna", + "moo": "Muu", + "cowbell": "Koskälla", + "pig": "Gris", + "chicken": "Kyckling", + "crow": "Kråka", + "frog": "Groda", + "patter": "Droppar", + "insect": "Insekt", + "cricket": "Syrsa", + "fly": "Fluga", + "buzz": "Surr", + "croak": "Kvack", + "rattle": "Skallra", + "musical_instrument": "Musikinstrument", + "plucked_string_instrument": "Stränginstrument", + "guitar": "Gitarr", + "electric_guitar": "Elektrisk Gitarr", + "bass_guitar": "Basgitarr", + "steel_guitar": "Stålgitarr", + "tapping": "Knackning", + "snake": "Orm", + "acoustic_guitar": "Aukustisk gitarr", + "mosquito": "Mygga", + "flapping_wings": "Vingslag", + "whale_vocalization": "Val-ljud", + "bass_drum": "Bastrumma", + "timpani": "Pukor", + "tabla": "Tabla", + "hi_hat": "Hi-Hat", + "wood_block": "Träblock", + "tambourine": "Tamburin", + "maraca": "Maracas", + "drum_roll": "Trumvirvel", + "rimshot": "Kantslag", + "snare_drum": "Virveltrumma", + "cymbal": "Cymbal", + "mandolin": "Mandolin", + "boat": "Båt", + "train": "Tåg", + "bowed_string_instrument": "stråkinstrument", + "banjo": "Banjo", + "sitar": "Sitar", + "clock": "Klocka", + "keyboard": "Tangentbord", + "vehicle": "Fordon", + "skateboard": "Skatebord", + "door": "Dörr", + "blender": "Blandare", + "sink": "Vask", + "hair_dryer": "Hårfön", + "toothbrush": "Tandborste", + "scissors": "Sax", + "strum": "Anslag", + "zither": "Citer", + "ukulele": "Ukulele", + "piano": "Piano", + "electric_piano": "Elpiano", + "organ": "Orgel", + "electronic_organ": "Elektronisk orgel", + "hammond_organ": "Hammondorgel", + "synthesizer": "Synthesizer", + "sampler": "Provtagare", + "harpsichord": "Cembalo", + "percussion": "Slagverk", + "drum_kit": "Trumset", + "drum_machine": "Trummaskin", + "drum": "Trumma", + "french_horn": "Franskt horn", + "trumpet": "Trumpet", + "flute": "Flöjt", + "gong": "Gonggong", + "tubular_bells": "Rörklockor", + "mallet_percussion": "Malletinstrument", + "marimba": "Marimba", + "glockenspiel": "Klockspel", + "vibraphone": "Vibrafon", + "steelpan": "Stålpanna", + "orchestra": "Orkester", + "brass_instrument": "Bleckblåsinstrument", + "trombone": "Trombon", + "string_section": "Stråkinstrument", + "violin": "Fiol", + "pizzicato": "Pizzicato", + "cello": "Cello", + "double_bass": "Kontrabas", + "wind_instrument": "Blåsinstrument", + "saxophone": "Saxofon", + "clarinet": "Klarinett", + "harp": "Harpa", + "bell": "Klocka", + "church_bell": "Kyrkklocka", + "jingle_bell": "Bjällerklang", + "bicycle_bell": "Cykelklocka", + "tuning_fork": "Stämgaffel", + "chime": "Klämta", + "wind_chime": "Vindspel", + "harmonica": "Munspel", + "accordion": "Dragspel", + "bagpipes": "Säckpipor", + "didgeridoo": "Didjeridu", + "theremin": "Teremin", + "singing_bowl": "Sjungande skål", + "scratching": "Repa", + "pop_music": "Popmusik", + "hip_hop_music": "Hiphopmusik", + "beatboxing": "Beatboxning", + "rock_music": "Rockmusik", + "heavy_metal": "Heavy Metal musik", + "punk_rock": "Punkrock", + "grunge": "Grunge", + "progressive_rock": "Progressiv rock", + "rock_and_roll": "Rock and roll", + "psychedelic_rock": "Psykedelisk rock", + "rhythm_and_blues": "Rytm och blues", + "soul_music": "Soulmusik", + "reggae": "Reggae", + "country": "Land", + "swing_music": "Swingmusik", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folkmusik", + "middle_eastern_music": "Mellanösternmusik", + "jazz": "Jazz", + "disco": "Disko", + "classical_music": "Klassisk musik", + "opera": "Opera", + "electronic_music": "Elektronisk musik", + "house_music": "Housemusik", + "techno": "Tekno", + "dubstep": "Dubstep", + "drum_and_bass": "Trumma och bas", + "electronica": "Elektronisk musik", + "electronic_dance_music": "Elektronisk dansmusik", + "ambient_music": "Ambientmusik", + "trance_music": "Trancemusik", + "music_of_latin_america": "Latinamerikansk musik", + "salsa_music": "Salsamusik", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Musik för barn", + "new-age_music": "New Age-musik", + "vocal_music": "Vokalmusik", + "a_capella": "A cappella", + "music_of_africa": "Afrikansk musik", + "afrobeat": "Afrobeat", + "christian_music": "Kristen musik", + "gospel_music": "Gospelmusik", + "music_of_asia": "Asiens musik", + "carnatic_music": "Karnatisk musik", + "music_of_bollywood": "Bollywoods musik", + "ska": "Ska", + "traditional_music": "Traditionell musik", + "independent_music": "Oberoende musik", + "song": "Låt", + "background_music": "Bakgrundsmusik", + "theme_music": "Temamusik", + "jingle": "Klingande", + "soundtrack_music": "Soundtrackmusik", + "lullaby": "Vaggvisa", + "video_game_music": "Videospelsmusik", + "christmas_music": "Julmusik", + "dance_music": "Dansmusik", + "wedding_music": "Bröllopsmusik", + "happy_music": "Glad musik", + "sad_music": "Sorglig musik", + "tender_music": "Öm musik", + "exciting_music": "Spännande musik", + "angry_music": "Arg musik", + "scary_music": "Skräckmusik", + "wind": "Vind", + "rustling_leaves": "Prasslande löv", + "wind_noise": "Vindbrus", + "thunderstorm": "Åskväder", + "thunder": "Åska", + "water": "Vatten", + "rain": "Regn", + "raindrop": "Regndroppe", + "rain_on_surface": "Regn på ytan", + "stream": "Strömma", + "waterfall": "Vattenfall", + "ocean": "Hav", + "waves": "Vågor", + "steam": "Ånga", + "gurgling": "Gurglande", + "fire": "Brand", + "crackle": "Spraka", + "sailboat": "Segelbåt", + "rowboat": "Roddbåt", + "motorboat": "Motorbåt", + "ship": "Fartyg", + "motor_vehicle": "Motorfordon", + "power_windows": "Elfönster", + "skidding": "Slirning", + "tire_squeal": "Däckskrik", + "toot": "Tuta", + "car_alarm": "Billarm", + "car_passing_by": "Bil som passerar", + "race_car": "Racerbil", + "truck": "Lastbil", + "air_brake": "Luftbroms", + "air_horn": "Lufthorn", + "reversing_beeps": "Backningljud", + "ice_cream_truck": "Glassbil", + "emergency_vehicle": "Akutbil", + "police_car": "Polisbil", + "ambulance": "Ambulans", + "fire_engine": "Brandbil", + "traffic_noise": "Trafikbuller", + "rail_transport": "Järnvägstransport", + "train_whistle": "Tågvissla", + "train_horn": "Tåghorn", + "railroad_car": "Järnvägsvagn", + "train_wheels_squealing": "Tåghjul skriker", + "subway": "Tunnelbana", + "aircraft": "Flygplan", + "aircraft_engine": "Flygmotor", + "jet_engine": "Jetmotor", + "propeller": "Propeller", + "helicopter": "Helikopter", + "fixed-wing_aircraft": "Flygplan med fasta vingar", + "engine": "Motor", + "light_engine": "Ljusmotor", + "lawn_mower": "Gräsklippare", + "chainsaw": "Motorsåg", + "doorbell": "Dörrklocka", + "electric_toothbrush": "Eltandborste", + "computer_keyboard": "Tangentbord", + "alarm": "Larm", + "telephone": "Telefon", + "ringtone": "Ringsignal", + "dial_tone": "Rington", + "busy_signal": "Upptagetsignal", + "alarm_clock": "Alarmklocka", + "smoke_detector": "Brandvarnare", + "fire_alarm": "Brandlarm", + "dental_drill's_drill": "Tandläkarborr", + "medium_engine": "Medelstor motor", + "heavy_engine": "Tung motor", + "engine_knocking": "Motorknackning", + "engine_starting": "Motor startar", + "idling": "Tomgång", + "accelerating": "Accelererar", + "ding-dong": "Ring-ring", + "sliding_door": "Skjutdörr", + "slam": "Smäll", + "knock": "Knack", + "tap": "Knacka", + "squeak": "Gnissla", + "cupboard_open_or_close": "Skåp öppnas eller stängs", + "drawer_open_or_close": "Låda öppnas eller stängs", + "dishes": "Tallrikar", + "cutlery": "Bestick", + "chopping": "Hackning", + "frying": "Steka", + "microwave_oven": "Mikrovågsugn", + "water_tap": "Vattenkran", + "bathtub": "Badkar", + "toilet_flush": "Toalettspolning", + "vacuum_cleaner": "Dammsugare", + "zipper": "Dragkedja", + "keys_jangling": "Nycklar som klirrar", + "coin": "Mynt", + "electric_shaver": "Elektrisk rakhyvel", + "shuffling_cards": "Blanda kort", + "typing": "Skrivar", + "typewriter": "Skrivmaskin", + "writing": "Skriva", + "telephone_bell_ringing": "Telefonen ringer", + "telephone_dialing": "Ljud för telefonuppringning", + "siren": "Siren", + "civil_defense_siren": "Civilförsvarssiren", + "buzzer": "Summer", + "foghorn": "Mistlur", + "whistle": "Vissla", + "steam_whistle": "Ångvissla", + "mechanisms": "Mekanismer", + "ratchet": "Spärrhake", + "tick": "Tick", + "tick-tock": "Tick Tack", + "gears": "Kugghjul", + "pulleys": "Remskivor", + "sewing_machine": "Symaskin", + "printer": "Skrivare", + "mechanical_fan": "Mekanisk fläkt", + "air_conditioning": "Luftkonditionering", + "cash_register": "Kassaapparat", + "single-lens_reflex_camera": "Enkellinsreflexkamera", + "tools": "Verktyg", + "hammer": "Hammare", + "jackhammer": "Tryckluftsborr", + "sawing": "Sågning", + "filing": "Filning", + "sanding": "Sandning", + "power_tool": "Elverktyg", + "drill": "Borra", + "explosion": "Explosion", + "gunshot": "Skottlossning", + "machine_gun": "Kulspruta", + "fusillade": "Fusillad", + "artillery_fire": "Artillerieeld", + "cap_gun": "Kapsylpistol", + "fireworks": "Fyrverkeri", + "firecracker": "Smällare", + "burst": "Brista", + "eruption": "Utbrott", + "boom": "Pang", + "wood": "Trä", + "chop": "Hugga", + "splinter": "Flisa", + "crack": "Spricka", + "glass": "Glas", + "chink": "Skaka", + "shatter": "Splittras", + "silence": "Tystnad", + "sound_effect": "Ljudeffekt", + "environmental_noise": "Miljöbuller", + "static": "Statisk", + "white_noise": "Vitt brus", + "pink_noise": "Rosa brus", + "television": "Tv", + "radio": "Radio", + "field_recording": "Fältinspelning", + "scream": "Skrika", + "sodeling": "Södling", + "chird": "Ackord", + "change_ringing": "Ljud från myntväxling", + "shofar": "Shofar", + "liquid": "Flytande", + "splash": "Stänk", + "slosh": "Plaska", + "squish": "Stryk", + "drip": "Dropp", + "pour": "Hälla", + "trickle": "Sippra", + "gush": "Välla", + "fill": "Fylla", + "spray": "Sprej", + "pump": "Pump", + "stir": "Rör", + "boiling": "Kokande", + "sonar": "Ekolod", + "arrow": "Pil", + "whoosh": "Svischande", + "thump": "Dunk", + "thunk": "Dunkande", + "electronic_tuner": "Elektronisk stämapparat", + "effects_unit": "Effektenhet", + "chorus_effect": "Chorus-effekt", + "basketball_bounce": "Basketbollstuds", + "bang": "Smäll", + "slap": "Slag", + "whack": "Slog", + "smash": "Smälla", + "breaking": "Brytning", + "bouncing": "Studsande", + "whip": "Piska", + "flap": "Flaxa", + "scratch": "Repa", + "scrape": "Skrapa", + "rub": "Gnugga", + "roll": "Rulla", + "crushing": "Krossa", + "crumpling": "Skrynkliga", + "tearing": "Rivning", + "beep": "Pip", + "ping": "Ping", + "ding": "Ding", + "clang": "Klang", + "squeal": "Skrika", + "creak": "Knarr", + "rustle": "Prassel", + "whir": "Surra", + "clatter": "Slammer", + "sizzle": "Fräsa vid matlagning", + "clicking": "Klickande", + "clickety_clack": "Klickigt klack", + "rumble": "Mullrande", + "plop": "Plopp", + "hum": "Brum", + "zing": "Vinande", + "boing": "Pling", + "crunch": "Knastrande", + "sine_wave": "Sinusvåg", + "harmonic": "Harmonisk", + "chirp_tone": "Kvittringston", + "pulse": "Puls", + "inside": "Inuti", + "outside": "Utanför", + "reverberation": "Eko", + "echo": "Eko", + "noise": "Buller", + "mains_hum": "Huvudbrum", + "distortion": "Distorsion", + "sidetone": "Sidoton", + "cacophony": "Kakofoni", + "throbbing": "Bultande", + "vibration": "Vibration" +} diff --git a/web/public/locales/sv/common.json b/web/public/locales/sv/common.json new file mode 100644 index 0000000..458cb28 --- /dev/null +++ b/web/public/locales/sv/common.json @@ -0,0 +1,324 @@ +{ + "time": { + "untilForTime": "Till {{time}}", + "untilForRestart": "Tills Frigate startar om.", + "untilRestart": "Tills omstart", + "ago": "{{timeAgo}} sedan", + "justNow": "Just nu", + "today": "Idag", + "yesterday": "Igår", + "last14": "Senaste 14 dagarna", + "thisMonth": "Denna månad", + "lastMonth": "Förra månaden", + "30minutes": "30 minuter", + "1hour": "1 timma", + "12hours": "12 timmar", + "pm": "pm", + "am": "am", + "yr": "{{time}}år", + "mo": "{{time}}må", + "month_one": "{{time}} månad", + "month_other": "{{time}} månader", + "d": "{{time}}d", + "last7": "Senaste 7 dagarna", + "5minutes": "5 minuter", + "last30": "Senaste 30 dagarna", + "thisWeek": "Denna vecka", + "lastWeek": "Förra veckan", + "10minutes": "10 minuter", + "24hours": "24 timmar", + "year_one": "{{time}} år", + "year_other": "{{time}} år", + "second_one": "{{time}} sekund", + "second_other": "{{time}} sekunder", + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyy" + }, + "h": "{{time}}t", + "hour_one": "{{time}} timme", + "hour_other": "{{time}} timmar", + "m": "{{time}}m", + "minute_one": "{{time}} minut", + "minute_other": "{{time}} minuter", + "s": "{{time}}s", + "formattedTimestamp": { + "12hour": "d MMM, 'kl.' h:mm:ss a", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "'kl.' h:mm a", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "day_one": "{{time}} dag", + "day_other": "{{time}} dagar", + "inProgress": "Pågår", + "invalidStartTime": "Ogiltig starttid", + "invalidEndTime": "Ogiltig sluttid", + "never": "Aldrig" + }, + "button": { + "save": "Spara", + "enabled": "Aktiverad", + "enable": "Aktivera", + "disabled": "Inaktiverad", + "pictureInPicture": "Bild-i-Bild", + "twoWayTalk": "Tvåvägskommunikation", + "edit": "Redigera", + "copyCoordinates": "Kopiera koordinater", + "suspended": "Pausad", + "play": "Spela", + "unselect": "Avmarkera", + "unsuspended": "Återuppta", + "deleteNow": "Radera nu", + "next": "Nästa", + "apply": "Verkställ", + "reset": "Återställ", + "done": "Klar", + "disable": "Inaktivera", + "saving": "Sparar…", + "cancel": "Avbryt", + "close": "Stäng", + "copy": "Kopiera", + "back": "Tillbaka", + "history": "Historia", + "fullscreen": "Fullskärm", + "exitFullscreen": "Lämna Fullskärm", + "cameraAudio": "Kameraljud", + "on": "PÅ", + "off": "AV", + "delete": "Radera", + "yes": "Ja", + "no": "Nej", + "download": "Ladda ner", + "info": "Info", + "export": "Exportera", + "continue": "Fortsätta", + "add": "Lägg till", + "applying": "Verkställer…", + "undo": "Ångra", + "copiedToClipboard": "Kopieras till urklipp", + "modified": "Modifiera", + "overridden": "Åsidosatt", + "resetToGlobal": "Återställ till Global", + "resetToDefault": "Återställ till standard", + "saveAll": "Spara alla", + "savingAll": "Sparar alla…", + "retry": "Försök igen", + "undoAll": "Ångra alla" + }, + "menu": { + "language": { + "yue": "粵語 (Kantonesiska)", + "it": "Italiano (Italienska)", + "fr": "Français (Franska)", + "nl": "Nederlands (Nederländska)", + "hi": "हिन्दी (Hindi)", + "pt": "Português (Portugisiska)", + "ru": "Русский (Ryska)", + "pl": "Polski (Polska)", + "el": "Ελληνικά (Grekiska)", + "sk": "Slovenčina (Slovenska)", + "tr": "Türkçe (Turkiska)", + "uk": "Українська (Ukrainska)", + "he": "עברית (Hebreiska)", + "ro": "Română (Romänska)", + "hu": "Magyar (Ungerska)", + "fi": "Suomi (Finska)", + "da": "Dansk (Danska)", + "ar": "العربية (Arabiska)", + "es": "Español (Spanska)", + "zhCN": "简体中文 (Kinesiska)", + "de": "Deutsch (Tyska)", + "ja": "日本語 (Japanska)", + "sv": "Svenska (Svenska)", + "cs": "Čeština (Tjeckiska)", + "nb": "Norsk Bokmål (Norsk Bokmål)", + "ko": "한국어 (Koreanska)", + "vi": "Tiếng Việt (Vietnamesiska)", + "fa": "فارسی (Persiska)", + "th": "ไทย (Thailändska)", + "withSystem": { + "label": "Använd systeminställningarna för språk" + }, + "en": "English (Engelska)", + "ptBR": "Português brasileiro (Brasiliansk Portugisiska)", + "ca": "Català (Katalanska)", + "sr": "Српски (Serbiska)", + "sl": "Slovenščina (Slovenska)", + "lt": "Lietuvių (Litauiska)", + "bg": "Български (Bulgariska)", + "gl": "Galego (Galiciska)", + "id": "Bahasa Indonesia (Indonesiska)", + "ur": "اردو (Urdu)", + "hr": "Hrvatski (kroatiska)", + "zhHant": "繁體中文 (Traditionell kinesisk)", + "bs": "Bosanski (Bosniska)" + }, + "darkMode": { + "withSystem": { + "label": "Använd systeminställningarna för ljust eller mörkt läge" + }, + "label": "Mörk Läge", + "light": "Ljus", + "dark": "Mörk" + }, + "theme": { + "label": "Tema", + "blue": "Blå", + "green": "Grön", + "nord": "North", + "default": "Standard", + "highcontrast": "Hög Kontrast", + "red": "Röd" + }, + "export": "Exportera", + "faceLibrary": "Ansiktsbibliotek", + "user": { + "title": "Användare", + "account": "Konto", + "current": "Nuvarande Användare: {{user}}", + "anonymous": "anonym", + "logout": "Logga ut", + "setPassword": "Sätt Lösenord" + }, + "systemMetrics": "Systemstatus", + "configuration": "Konfiguration", + "explore": "Utforska", + "live": { + "cameras": { + "count_one": "{{count}} Kamera", + "count_other": "{{count}} Kameror", + "title": "Kameror" + }, + "allCameras": "Alla kameror", + "title": "Live" + }, + "system": "System", + "systemLogs": "Systemlogg", + "settings": "Inställningar", + "help": "Hjälp", + "documentation": { + "title": "Dokumentation", + "label": "Frigate-dokumentation" + }, + "uiPlayground": "UI Testmiljö", + "restart": "Starta om Frigate", + "review": "Granska", + "languages": "Språk", + "configurationEditor": "Konfigurationsredigerare", + "withSystem": "System", + "appearance": "Utseende", + "classification": "Klassificering", + "profiles": "Profiler", + "actions": "Åtgärder", + "features": "Funktioner", + "chat": "Chatt" + }, + "pagination": { + "next": { + "title": "Nästa", + "label": "Gå till nästa sida" + }, + "previous": { + "title": "Föregående", + "label": "Gå till föregående sida" + }, + "more": "Flera sidor", + "label": "paginering" + }, + "accessDenied": { + "title": "Åtkomst Förbjuden", + "desc": "Du har inte rättigheter att visa den här sidan.", + "documentTitle": "Åtkomst Förbjuden - Frigate" + }, + "role": { + "admin": "Admin", + "viewer": "Tittare", + "desc": "Administratörer har fullständig åtkomst till alla funktioner i Frigates gränssnitt. Tittare kan endast visa kameror, granska objekt och se historiskt videomaterial.", + "title": "Roll" + }, + "notFound": { + "documentTitle": "Hittades Inte - Frigate", + "desc": "Sidan hittades inte", + "title": "404" + }, + "toast": { + "save": { + "title": "Spara", + "error": { + "title": "Misslyckades med att spara konfigurationsändringar: {{errorMessage}}", + "noMessage": "Misslyckades med att spara konfigurationsändringar" + }, + "success": "Konfigurationsändringarna har sparats." + }, + "copyUrlToClipboard": "Webbadressen har kopierats till urklipp." + }, + "label": { + "back": "Gå tillbaka", + "hide": "Dölj {{item}}", + "show": "Visa {{item}}", + "ID": "ID", + "none": "Ingen", + "all": "Alla", + "other": "Annat" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "fot", + "meters": "meter" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/timme", + "mbph": "MB/timme", + "gbph": "GB/timme" + } + }, + "selectItem": "Välj {{item}}", + "readTheDocumentation": "Läs dokumentationen", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} och {{1}}", + "many": "{{items}} och {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Valfritt", + "internalID": "Det interna ID som Frigate använder i konfigurationen och databasen" + }, + "no_items": "Inga artiklar", + "validation_errors": "Valideringsfel", + "credentialField": { + "savedPlaceholder": "Sparad — lämna tomt för att hålla dig aktuell" + } +} diff --git a/web/public/locales/sv/components/auth.json b/web/public/locales/sv/components/auth.json new file mode 100644 index 0000000..1fcf909 --- /dev/null +++ b/web/public/locales/sv/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Lösenord", + "user": "Användarnamn", + "login": "Logga in", + "errors": { + "usernameRequired": "Användarnamn är obligatoriskt", + "passwordRequired": "Lösenord är obligatoriskt", + "loginFailed": "Inloggning misslyckades", + "unknownError": "Okänt fel. Kontrollera loggarna.", + "webUnknownError": "Okänt fel. Kontrollera konsol loggarna.", + "rateLimit": "Överskriden anropsgräns. Försök igen senare." + }, + "firstTimeLogin": "Försöker du logga in för första gången? Inloggningsuppgifterna finns angivna i Frigate-loggarna." + } +} diff --git a/web/public/locales/sv/components/camera.json b/web/public/locales/sv/components/camera.json new file mode 100644 index 0000000..85996ef --- /dev/null +++ b/web/public/locales/sv/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "Kameragrupper", + "add": "Lägg till Kameragrupp", + "edit": "Ändra Kameragrupp", + "delete": { + "label": "Radera kameragrupp", + "confirm": { + "title": "Bekräfta borttagning", + "desc": "Är du säker på att du vill ta bort kameragruppen {{name}}?" + } + }, + "success": "Kameragruppen ({{name}}) har sparats.", + "name": { + "label": "Namn", + "placeholder": "Ange ett namn…", + "errorMessage": { + "mustLeastCharacters": "Gruppnamnet för kameror måste vara minst 2 tecken.", + "nameMustNotPeriod": "Kameragruppnamnet får inte innehålla en punkt.", + "invalid": "Ogiltigt kameragruppnamn.", + "exists": "Kameragruppnamnet finns redan." + } + }, + "icon": "Ikon", + "camera": { + "setting": { + "label": "inställningar för kameraströmning", + "title": "{{cameraName}} Streaminginställningar", + "desc": "Ändra alternativen för livestreaming för den här kameragruppens instrumentpanel. Dessa inställningar är enhets-/webbläsarspecifika.", + "audioIsAvailable": "Ljud är tillgängligt för denna kameraström", + "audioIsUnavailable": "Ljud är inte tillgängligt för den här kameraströmmen", + "audio": { + "tips": { + "title": "Ljud måste sändas från din kamera och konfigureras i go2rtc för den här strömmen.", + "document": "Läs dokumentationen. " + } + }, + "streamMethod": { + "label": "Strömningsmetod", + "method": { + "smartStreaming": { + "desc": "Smart streaming uppdaterar kamerabilden en gång per minut när ingen detekterbar aktivitet sker för att spara bandbredd och resurser. När aktivitet detekteras växlar bilden sömlöst till en livestream.", + "label": "Smart strömning (rekommenderas)" + }, + "continuousStreaming": { + "label": "Kontinuerlig strömning", + "desc": { + "title": "Kamerabilden kommer alltid att vara en liveström när den är synlig på instrumentpanelen, även om ingen aktivitet detekteras.", + "warning": "Kontinuerlig strömning kan orsaka hög bandbreddsanvändning och prestandaproblem. Använd med försiktighet." + } + }, + "noStreaming": { + "label": "Ingen strömning", + "desc": "Kamerabilderna uppdateras bara en gång per minut och ingen livestreaming kommer att ske." + } + }, + "placeholder": "Välj en strömningsmetod" + }, + "stream": "Strömma", + "placeholder": "Välj en ström", + "compatibilityMode": { + "label": "Kompatibilitetsläge", + "desc": "Aktivera endast det här alternativet om kamerans livestream visar färgartefakter och har en diagonal linje på höger sida av bilden." + } + }, + "birdseye": "Fågelöga" + }, + "cameras": { + "desc": "Välj kameror för denna guppen.", + "label": "Kameror" + }, + "showAll": "Visa alla kameragrupper", + "showLess": "Visa mindre", + "editGroups": "Redigera kameragrupper" + }, + "debug": { + "options": { + "showOptions": "Visa alternativ", + "label": "Inställningar", + "title": "Alternativ", + "hideOptions": "Dölj alternativ" + }, + "boundingBox": "Avgränsningsruta", + "timestamp": "Tidsstämpel", + "zones": "Zoner", + "mask": "Maskera", + "motion": "Rörelse", + "regions": "Regioner", + "paths": "Vägar" + } +} diff --git a/web/public/locales/sv/components/dialog.json b/web/public/locales/sv/components/dialog.json new file mode 100644 index 0000000..5fc3794 --- /dev/null +++ b/web/public/locales/sv/components/dialog.json @@ -0,0 +1,193 @@ +{ + "restart": { + "button": "Starta om", + "restarting": { + "title": "Frigate startar om", + "content": "Sidan uppdateras om {{countdown}} sekunder.", + "button": "Tvinga omladdning nu" + }, + "title": "Är du säker på att du vill starta om Frigate?", + "description": "Frigate stoppas tillfälligt under omstarten." + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Skicka till Frigate+", + "desc": "Objekt på platser du vill undvika är inte falska positiva resultat. Att skicka in dem som falska positiva resultat kommer att förvirra modellen." + }, + "review": { + "question": { + "ask_a": "Är detta objektet en/ett {{label}}?", + "ask_an": "Är detta objekt en/ett {{label}}?", + "ask_full": "Är detta objektet en/ett {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Bekräfta denna etikett för Frigate Plus" + }, + "state": { + "submitted": "Inskickad" + } + } + }, + "video": { + "viewInHistory": "Se i Historik" + } + }, + "export": { + "time": { + "fromTimeline": "Välj från tidslinjen", + "lastHour_one": "Sista timma", + "lastHour_other": "Sista {{count}} timmar", + "start": { + "title": "Start Tid", + "label": "Välj Start Tid" + }, + "end": { + "title": "Slut Tid", + "label": "Välj Sluttid" + }, + "custom": "Anpassad" + }, + "name": { + "placeholder": "Ge exporten ett namn" + }, + "select": "Välj", + "export": "Export", + "selectOrExport": "Välj eller exportera", + "toast": { + "success": "Exporten har startats. Visa filen på exportsidan.", + "error": { + "failed": "Misslyckades med att köa exporten: {{error}}", + "endTimeMustAfterStartTime": "Sluttiden måste vara efter starttiden", + "noVaildTimeSelected": "Inget giltigt tidsintervall valt" + }, + "view": "Visa", + "queued": "Exporten är i kö. Se förloppet på exportsidan.", + "batchSuccess_one": "Startade 1 export. Öppnar ärendet nu.", + "batchSuccess_other": "Startade {{count}} exporter. Öppnar ärendet nu.", + "batchPartial": "Startade {{successful}} av {{total}} exporter. Misslyckade kameror: {{failedCameras}}", + "batchFailed": "Misslyckades med att starta export av {{total}}. Kameror som inte fungerade: {{failedCameras}}", + "batchQueuedSuccess_one": "1 export har köats. Ärendet öppnas nu.", + "batchQueuedSuccess_other": "{{count}} exporter har köats. Öppnar ärendet nu.", + "batchQueuedPartial": "Köade {{successful}} av {{total}} exporter. Misslyckade kameror: {{failedCameras}}", + "batchQueueFailed": "Misslyckades med att köa {{total}} exporter. Felaktiga kameror: {{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "Spara export", + "previewExport": "Förhandsgranska export", + "queueingExport": "Export i kö...", + "useThisRange": "Använd detta intervall" + }, + "case": { + "newCaseOption": "Skapa nytt ärende", + "newCaseNamePlaceholder": "Nytt ärende namn", + "newCaseDescriptionPlaceholder": "Ärende beskrivning", + "label": "Ärende", + "nonAdminHelp": "Ett nytt ärende kommer att skapas för dessa exporter.", + "placeholder": "Välj ett ärende" + }, + "queueing": "Export i kö...", + "tabs": { + "export": "Enskild kamera", + "multiCamera": "Flerkameror" + }, + "multiCamera": { + "timeRange": "Tidsintervall", + "selectFromTimeline": "Välj från tidslinjen", + "cameraSelection": "Kameror", + "cameraSelectionHelp": "Kameror med spårade objekt inom detta tidsintervall är förvalda", + "checkingActivity": "Kontrollerar kameraaktivitet...", + "noCameras": "Inga kameror tillgängliga", + "detectionCount_one": "1 spårat objekt", + "detectionCount_other": "{{count}} spårade objekt", + "nameLabel": "Exportnamn", + "namePlaceholder": "Valfritt basnamn för dessa exporter", + "queueingButton": "Köar exporter...", + "exportButton_one": "Exportera 1 kamera", + "exportButton_other": "Exportera {{count}} kameror", + "selectAll": "Välj alla kameror", + "selectWithActivity": "Kameror med spårade objekt", + "selectGroup": "Välj grupp", + "noMatchingCameras": "Ingen kamera matchar din sökning" + }, + "multi": { + "title_one": "Exportera 1 recension", + "title_other": "Exportera {{count}} recensioner", + "description": "Exportera varje vald recension. Alla exporter grupperas under ett enda ärende.", + "descriptionNoCase": "Exportera varje vald recension.", + "caseNamePlaceholder": "Granska export - {{date}}", + "exportButton_one": "Exportera 1 recension", + "exportButton_other": "Exportera {{count}} recensioner", + "exportingButton": "Exporterar...", + "toast": { + "started_one": "Startade 1 export. Öppnar ärendet nu.", + "started_other": "Startade {{count}} exporter. Öppnar ärendet nu.", + "startedNoCase_one": "Startade 1 export.", + "startedNoCase_other": "Startade {{count}} exporter.", + "partial": "Startade {{successful}} av {{total}} exporter. Misslyckades: {{failedItems}}", + "failed": "Misslyckades med att starta exporten av {{total}}. Misslyckades: {{failedItems}}" + } + } + }, + "streaming": { + "label": "Videoström", + "restreaming": { + "disabled": "Omströmning är inte aktiverad för den här kameran.", + "desc": { + "title": "Konfigurera go2rtc för ytterligare livevisningsalternativ och ljud för den här kameran.", + "readTheDocumentation": "Läs dokumentationen" + } + }, + "showStats": { + "label": "Visa strömstatistik", + "desc": "Aktivera det här alternativet för att visa strömstatistik som ett överlägg över kameraflödet." + }, + "debugView": "Felsöknings vy" + }, + "search": { + "saveSearch": { + "overwrite": "{{searchName}} finns redan. Om du sparar skrivs det befintliga värdet över.", + "success": "Sökningen ({{searchName}}) har sparats.", + "button": { + "save": { + "label": "Spara den här sökningen" + } + }, + "label": "Spara Sökning", + "desc": "Ange ett namn för den här sparade sökningen.", + "placeholder": "Ange ett namn för din sökning" + } + }, + "recording": { + "confirmDelete": { + "title": "Bekräfta radering", + "desc": { + "selected": "Är du säker på att du vill radera all inspelad video som är kopplad till det här granskningsobjektet?

    Håll ner Shift-tangenten för att hoppa över den här dialogrutan i framtiden." + }, + "toast": { + "success": "Videoklipp som är kopplade till de valda granskningsobjekten har raderats.", + "error": "Misslyckades med att ta bort: {{error}}" + } + }, + "button": { + "export": "Exportera", + "markAsReviewed": "Markera som granskad", + "deleteNow": "Ta bort nu", + "markAsUnreviewed": "Markera som ogranskad" + }, + "shareTimestamp": { + "label": "Dela tidsstämpel", + "title": "Dela tidsstämpel", + "description": "Dela en tidsstämplad URL för spelarens nuvarande position eller välj en anpassad tidsstämpel. Observera att detta inte är en offentlig delnings-URL och endast är tillgänglig för användare med åtkomst till Frigate och den här kameran.", + "custom": "Anpassad tidsstämpel", + "button": "Dela tidsstämpel-URL", + "shareTitle": "Frigate granskning tidsstämpel: {{camera}}" + } + }, + "imagePicker": { + "selectImage": "Välj miniatyrbilden för ett spårat objekt", + "search": { + "placeholder": "Sök efter etikett eller underetikett..." + }, + "noImages": "Inga miniatyrbilder hittades för den här kameran", + "unknownLabel": "Sparad triggerbild" + } +} diff --git a/web/public/locales/sv/components/filter.json b/web/public/locales/sv/components/filter.json new file mode 100644 index 0000000..846b2f0 --- /dev/null +++ b/web/public/locales/sv/components/filter.json @@ -0,0 +1,141 @@ +{ + "labels": { + "all": { + "title": "Alla etiketter", + "short": "Etiketter" + }, + "label": "Etiketter", + "count": "{{count}} Etiketter", + "count_one": "{{count}} Etikett", + "count_other": "{{count}} etiketter" + }, + "filter": "Filtrera", + "zones": { + "label": "Zoner", + "all": { + "title": "Alla zoner", + "short": "Zoner" + } + }, + "features": { + "hasSnapshot": "Har ögonblicksbild", + "hasVideoClip": "Har ett video klipp", + "submittedToFrigatePlus": { + "label": "Skickat till Frigate+", + "tips": "Du måste först filtrera på spårade objekt som har en ögonblicksbild.

    Spårade objekt utan ögonblicksbild kan inte skickas till Frigate+." + }, + "label": "Detaljer" + }, + "sort": { + "dateAsc": "Datum (Stigande)", + "label": "Sortera", + "scoreAsc": "Objektpoäng (Stigande)", + "speedDesc": "Uppskattad Hastighet (Fallande)", + "relevance": "Relevans", + "dateDesc": "Datum (Fallande)", + "scoreDesc": "Objektpoäng (Fallande)", + "speedAsc": "Uppskattad Hastighet (Stigande)" + }, + "cameras": { + "all": { + "short": "Kameror", + "title": "Alla Kameror" + }, + "label": "Kamerafilter" + }, + "explore": { + "settings": { + "title": "Inställningar", + "defaultView": { + "title": "Standard Vy", + "summary": "Sammanfattning", + "desc": "När inga filter är valda, visa en översikt av de senaste spårade objekten per etikett eller visa ett ofiltrerat rutnät.", + "unfilteredGrid": "Ofiltrerat Rutnät" + }, + "searchSource": { + "options": { + "description": "Beskrivning", + "thumbnailImage": "Miniatyrbild" + }, + "label": "Sökkälla", + "desc": "Välj om du vill söka miniatyrbilderna eller beskrivningarna av de spårade objekten." + }, + "gridColumns": { + "desc": "Välj antal kolumner i rutnätsvy.", + "title": "Kolumner i Rutnät" + } + }, + "date": { + "selectDateBy": { + "label": "Välj datum att filtrera efter" + } + } + }, + "review": { + "showReviewed": "Visa Kontrollerade" + }, + "motion": { + "showMotionOnly": "Visa Endast Rörelse" + }, + "score": "Poäng", + "dates": { + "all": { + "short": "Datum", + "title": "Alla datum" + }, + "selectPreset": "Välj Förval…" + }, + "recognizedLicensePlates": { + "noLicensePlatesFound": "Inga registreringsplåtar hittade.", + "selectPlatesFromList": "Välj en eller flera registreringsplåtar från listan.", + "title": "Igenkända Registreringsskyltar", + "loadFailed": "Misslyckades med att ladda igenkända registreringsskyltar.", + "placeholder": "Skriv för att söka registreringsskyltar…", + "loading": "Laddar igenkända registreringsskyltar…", + "selectAll": "Välj alla", + "clearAll": "Rensa alla" + }, + "more": "Fler filter", + "reset": { + "label": "Nollställ filter" + }, + "subLabels": { + "label": "Under kategori", + "all": "Alla under kategorier" + }, + "estimatedSpeed": "Estimerad hastighet ({{unit}})", + "classes": { + "all": { + "title": "Alla Klasser" + }, + "count_one": "{{count}} Klass", + "count_other": "{{count}} Klasser", + "label": "Klasser" + }, + "timeRange": "Tidsspann", + "logSettings": { + "loading": { + "title": "Laddar", + "desc": "När loggvyn är rullad till slutet, strömmas automatiskt nya loggar till vyn." + }, + "filterBySeverity": "Filtrera logg på allvarlighetsgrad", + "disableLogStreaming": "Inaktivera strömning av logg", + "allLogs": "Alla loggar", + "label": "Filtrera loggnivå" + }, + "trackedObjectDelete": { + "title": "Bekräfta Borttagning", + "toast": { + "success": "Spårade objekt borttagna.", + "error": "Misslyckades med att ta bort spårade objekt: {{errorMessage}}" + }, + "desc": "Borttagning av dessa {{objectLength}} spårade objekt tar bort ögonblicksbild, sparade inbäddningar, och tillhörande livscykelposter. Inspelat material av dessa spårade objekt i Historievyn kommer INTE att tas bort.

    Vill du verkligen fortsätta?

    Håll ner Skift-tangenten för att hoppa över denna dialog i framtiden." + }, + "zoneMask": { + "filterBy": "Filtrera på zonmaskering" + }, + "attributes": { + "label": "Klassificeringsattribut", + "all": "Alla attribut" + } +} diff --git a/web/public/locales/sv/components/icons.json b/web/public/locales/sv/components/icons.json new file mode 100644 index 0000000..afdcfb7 --- /dev/null +++ b/web/public/locales/sv/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "Sök efter en ikon…" + }, + "selectIcon": "Välj en ikon" + } +} diff --git a/web/public/locales/sv/components/input.json b/web/public/locales/sv/components/input.json new file mode 100644 index 0000000..44c981f --- /dev/null +++ b/web/public/locales/sv/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Ladda ner video", + "toast": { + "success": "Nedladdning påbörjad för granskningsobjekt." + } + } + } +} diff --git a/web/public/locales/sv/components/player.json b/web/public/locales/sv/components/player.json new file mode 100644 index 0000000..6e0cd60 --- /dev/null +++ b/web/public/locales/sv/components/player.json @@ -0,0 +1,53 @@ +{ + "noPreviewFound": "Ingen förhandsvisning hittad", + "noRecordingsFoundForThisTime": "Inga inspelningar hittade för denna tid", + "noPreviewFoundFor": "Ingen förhandsvisning hittad för {{cameraName}}", + "submitFrigatePlus": { + "title": "Skicka denna bild till Frigate+?", + "submit": "Skicka", + "previewError": "Det gick inte att ladda förhandsgranskningen av ögonblicksbilden. Inspelningen är kanske inte tillgänglig just nu." + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 eller senare krävs för den här typen av livestream.", + "streamOffline": { + "title": "Ström ej tillgänglig", + "desc": "Inga bildrutor har tagits emot från {{cameraName}}-strömmen detect, kontrollera felloggarna" + }, + "stats": { + "streamType": { + "short": "Typ", + "title": "Strömtyp:" + }, + "bandwidth": { + "title": "Bandbredd:", + "short": "Bandbredd" + }, + "latency": { + "title": "Latens:", + "short": { + "title": "Latens", + "value": "{{seconds}} sek" + }, + "value": "{{seconds}} sekunder" + }, + "totalFrames": "Totala Bildrutor:", + "droppedFrames": { + "title": "Bortfallna bildrutor:", + "short": { + "title": "Bortfallen", + "value": "{{droppedFrames}} Bildrutor" + } + }, + "decodedFrames": "Avkodade bildrutor:", + "droppedFrameRate": "Frekvens för bortfallna bildrutor:" + }, + "cameraDisabled": "Kameran är inaktiverad", + "toast": { + "error": { + "submitFrigatePlusFailed": "Bildruta har skickats till Frigate+ med misslyckat resultat" + }, + "success": { + "submittedFrigatePlus": "Bildruta har skickats till Frigate+ med lyckat resultat" + } + }, + "cameraOff": "Kameran är avstängd" +} diff --git a/web/public/locales/sv/config/cameras.json b/web/public/locales/sv/config/cameras.json new file mode 100644 index 0000000..6b59c41 --- /dev/null +++ b/web/public/locales/sv/config/cameras.json @@ -0,0 +1,29 @@ +{ + "label": "KameraKonfiguration", + "name": { + "label": "Kameranamn", + "description": "Kameranamn krävs" + }, + "friendly_name": { + "label": "Visningsnamn", + "description": "Visningsnamn för kamera i Frigate UI" + }, + "enabled": { + "label": "Aktiverad", + "description": "Aktiverad" + }, + "audio": { + "label": "Ljuddetektering", + "description": "Inställningar för ljudbaserad händelsedetektering för denna kamera.", + "enabled": { + "label": "Aktivera ljuddetektering", + "description": "Aktivera eller avaktivera ljudbaserad detektering för denna kamera." + }, + "max_not_heard": { + "description": "Antal sekunder utan den konfigurerade ljudtypen innan en ljudbaserad händelse slutar." + }, + "min_volume": { + "label": "Minsta ljudvolym" + } + } +} diff --git a/web/public/locales/sv/config/global.json b/web/public/locales/sv/config/global.json new file mode 100644 index 0000000..ae266a8 --- /dev/null +++ b/web/public/locales/sv/config/global.json @@ -0,0 +1,17 @@ +{ + "version": { + "label": "Nuvarande konfigurations version" + }, + "audio": { + "label": "Ljuddetektering", + "enabled": { + "label": "Aktivera ljuddetektering" + }, + "max_not_heard": { + "description": "Antal sekunder utan den konfigurerade ljudtypen innan en ljudbaserad händelse slutar." + }, + "min_volume": { + "label": "Minsta ljudvolym" + } + } +} diff --git a/web/public/locales/sv/config/groups.json b/web/public/locales/sv/config/groups.json new file mode 100644 index 0000000..4a81abf --- /dev/null +++ b/web/public/locales/sv/config/groups.json @@ -0,0 +1,7 @@ +{ + "audio": { + "global": { + "sensitivity": "Global känslighet" + } + } +} diff --git a/web/public/locales/sv/config/validation.json b/web/public/locales/sv/config/validation.json new file mode 100644 index 0000000..23e4d27 --- /dev/null +++ b/web/public/locales/sv/config/validation.json @@ -0,0 +1,3 @@ +{ + "minimum": "Måste minst vara {{limit}}" +} diff --git a/web/public/locales/sv/objects.json b/web/public/locales/sv/objects.json new file mode 100644 index 0000000..1e2926f --- /dev/null +++ b/web/public/locales/sv/objects.json @@ -0,0 +1,120 @@ +{ + "car": "Bil", + "person": "Person", + "bicycle": "Cykel", + "motorcycle": "Motorcykel", + "airplane": "Flygplan", + "bus": "Buss", + "horse": "Häst", + "sheep": "Får", + "mouse": "Älg", + "bark": "Skall", + "goat": "Get", + "animal": "Djur", + "dog": "Hund", + "cat": "Katt", + "bird": "Fågel", + "train": "Tåg", + "traffic_light": "Trafiklyse", + "stop_sign": "Stoppskylt", + "cow": "Ko", + "elephant": "Elefant", + "bear": "Björn", + "hat": "Hatt", + "boat": "Båt", + "street_sign": "Gatuskylt", + "shoe": "Sko", + "giraffe": "Giraff", + "fire_hydrant": "Brandpost", + "zebra": "Zebra", + "backpack": "Ryggsäck", + "umbrella": "Paraply", + "bench": "Bänk", + "wine_glass": "vinglas", + "bottle": "Flaska", + "spoon": "Sked", + "orange": "Apelsin", + "carrot": "Morot", + "hot_dog": "Varmkorv", + "broccoli": "Broccoli", + "fork": "Gaffel", + "banana": "Banan", + "apple": "Äpple", + "knife": "Kniv", + "bowl": "Skål", + "cup": "Kopp", + "sandwich": "Smörgås", + "toaster": "Brödrost", + "vehicle": "Fordon", + "postnord": "PostNord", + "refrigerator": "Kylskåp", + "hair_dryer": "Hårfön", + "hair_brush": "Hårborste", + "amazon": "Amazon", + "clock": "Klocka", + "parking_meter": "Parkeringsmätare", + "eye_glasses": "Glasögon", + "handbag": "Handväska", + "tie": "Slips", + "suitcase": "Resväska", + "frisbee": "Frisbee", + "skis": "Skidor", + "snowboard": "Snowboard", + "sports_ball": "Boll", + "kite": "Drake", + "baseball_bat": "Basebollträ", + "baseball_glove": "Baseballhandske", + "skateboard": "Skatebord", + "surfboard": "Surfbräda", + "tennis_racket": "Tennisrack", + "pizza": "Pizza", + "donut": "Munk", + "cake": "Tårta", + "chair": "Stol", + "window": "Fönster", + "couch": "Soffa", + "potted_plant": "Krukväxt", + "bed": "Säng", + "mirror": "Spegel", + "dining_table": "Vardagsrumsbord", + "desk": "Skrivbord", + "toilet": "Toalett", + "tv": "TV", + "laptop": "Bärbar dator", + "remote": "Fjärrkontroll", + "keyboard": "Tangentbord", + "cell_phone": "Mobiltelefon", + "microwave": "Mikrovågsugn", + "sink": "Vask", + "vase": "Vas", + "scissors": "Sax", + "squirrel": "Ekorre", + "deer": "Rådjur", + "fox": "Räv", + "rabbit": "Kanin", + "raccoon": "Tvättbjörn", + "robot_lawnmower": "Robotgräsklippare", + "on_demand": "På begäran", + "face": "Ansikte", + "package": "Paket", + "bbq_grill": "Grill", + "usps": "USPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "gls": "GLS", + "dpd": "DPD", + "plate": "Tallrik", + "door": "Dörr", + "oven": "Ugn", + "blender": "Blandare", + "book": "Bok", + "waste_bin": "Soptunna", + "license_plate": "Nummerplåt", + "toothbrush": "Tandborste", + "ups": "UPS", + "teddy_bear": "Nallebjörn" +} diff --git a/web/public/locales/sv/views/chat.json b/web/public/locales/sv/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sv/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sv/views/classificationModel.json b/web/public/locales/sv/views/classificationModel.json new file mode 100644 index 0000000..5a66ca2 --- /dev/null +++ b/web/public/locales/sv/views/classificationModel.json @@ -0,0 +1,190 @@ +{ + "documentTitle": "Klassificeringsmodeller - Frigate", + "button": { + "deleteClassificationAttempts": "Ta bort klassificeringsbilder", + "renameCategory": "Byt namn på klass", + "deleteCategory": "Ta bort klass", + "deleteImages": "Ta bort bilder", + "trainModel": "Träna modellen", + "addClassification": "Lägg till klassificering", + "deleteModels": "Ta bort modeller", + "editModel": "Redigera modell" + }, + "toast": { + "success": { + "deletedCategory_one": "Tog bort {{count}} klass", + "deletedCategory_other": "Tog bort {{count}} klasser", + "deletedImage_one": "Tog bort {{count}} bild", + "deletedImage_other": "Tog bort {{count}} bilder", + "categorizedImage": "Lyckades klassificera bilden", + "trainedModel": "Modellen har tränats.", + "trainingModel": "Modellträning har startat.", + "deletedModel_one": "{{count}} modell har raderats", + "deletedModel_other": "{{count}} modeller har raderats", + "updatedModel": "Uppdaterade modellkonfiguration", + "renamedCategory": "Klassen har bytt namn till {{name}}" + }, + "error": { + "deleteImageFailed": "Misslyckades med att ta bort: {{errorMessage}}", + "deleteCategoryFailed": "Misslyckades med att ta bort klassen: {{errorMessage}}", + "categorizeFailed": "Misslyckades med att kategorisera bilden: {{errorMessage}}", + "trainingFailed": "Modellträningen misslyckades. Kontrollera Frigate-loggarna för mer information.", + "deleteModelFailed": "Misslyckades med att ta bort modellen: {{errorMessage}}", + "updateModelFailed": "Misslyckades med att uppdatera modell: {{errorMessage}}", + "trainingFailedToStart": "Misslyckades med att starta modellträning: {{errorMessage}}", + "renameCategoryFailed": "Misslyckades med att byta namn på klassen: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Ta bort klass", + "desc": "Är du säker på att du vill ta bort klassen {{name}}? Detta kommer att ta bort alla associerade bilder permanent och kräva att modellen tränas om.", + "minClassesTitle": "Kan inte ta bort klassen", + "minClassesDesc": "En klassificeringsmodell måste ha minst två klasser. Lägg till ytterligare en klass innan du tar bort den här." + }, + "deleteDatasetImages": { + "title": "Ta bort datamängdsbilder", + "desc_one": "Är du säker på att du vill ta bort {{count}} bild från {{dataset}}? Den här åtgärden kan inte ångras och kräver att modellen tränas om.", + "desc_other": "Är du säker på att du vill ta bort {{count}} bilder från {{dataset}}? Den här åtgärden kan inte ångras och kräver att modellen tränas om." + }, + "deleteTrainImages": { + "title": "Ta bort tränade bilder", + "desc_one": "Är du säker på att du vill ta bort {{count}} bild? Den här åtgärden kan inte ångras.", + "desc_other": "Är du säker på att du vill ta bort {{count}} bilder? Den här åtgärden kan inte ångras." + }, + "renameCategory": { + "title": "Byt namn på klass", + "desc": "Ange ett nytt namn för {{name}}. Du måste träna om modellen för att namnändringen ska träda i kraft." + }, + "description": { + "invalidName": "Ogiltigt namn. Namn får endast innehålla bokstäver, siffror, mellanslag, apostrofer, understreck och bindestreck." + }, + "train": { + "title": "Nyligen tillagd klassificeringar", + "aria": "Välj senaste klassificeringar", + "titleShort": "Ny" + }, + "categories": "Klasser", + "createCategory": { + "new": "Skapa ny klass" + }, + "categorizeImageAs": "Klassificera bilden som:", + "categorizeImage": "Klassificera bild", + "noModels": { + "object": { + "title": "Inga objektklassificeringsmodeller", + "description": "Skapa en anpassad modell för att klassificera detekterade objekt.", + "buttonText": "Skapa objektmodell" + }, + "state": { + "title": "Inga tillstånd klassificeringsmodeller", + "description": "Skapa en anpassad modell för att övervaka och klassificera tillståndsförändringar i specifika kameraområden.", + "buttonText": "Skapa en tillståndsmodell" + } + }, + "wizard": { + "title": "Skapa ny klassificering", + "steps": { + "nameAndDefine": "Namnge och definiera", + "stateArea": "Stat område", + "chooseExamples": "Välj exempel" + }, + "step1": { + "description": "Tillståndsmodeller övervakar fasta kameraområden för förändringar (t.ex. dörr öppen/stängd). Objektmodeller lägger till klassificeringar till detekterade objekt (t.ex. kända djur, leveranspersoner etc.).", + "name": "Namn", + "namePlaceholder": "Ange modellnamn...", + "type": "Typ", + "typeState": "Tillståndet", + "typeObject": "Objekt", + "objectLabel": "Objektetikett", + "objectLabelPlaceholder": "Välj objekttyp...", + "classificationType": "Klassificeringstyp", + "classificationTypeTip": "Lär dig mer om klassificeringstyper", + "classificationTypeDesc": "Underetiketter lägger till ytterligare text till objektetiketten (t.ex. 'Person: UPS'). Attribut är sökbara metadata som lagras separat i objektmetadata.", + "classificationSubLabel": "Underetikett", + "classificationAttribute": "Attribut", + "classes": "Klasser", + "states": "Tillstånd", + "classesTip": "Lär dig mer om klasser", + "classesStateDesc": "Definiera de olika tillstånd som ditt kameraområde kan vara i. Till exempel: \"öppen\" och \"stängd\" för en garageport.", + "classesObjectDesc": "Definiera de olika kategorierna som detekterade objekt ska klassificeras i. Till exempel: 'leveransperson', 'boende', 'främling' för personklassificering.", + "classPlaceholder": "Ange klassnamn...", + "errors": { + "nameRequired": "Modellnamn krävs", + "nameLength": "Modellnamnet måste vara högst 64 tecken långt", + "nameOnlyNumbers": "Modellnamnet får inte bara innehålla siffror", + "classRequired": "Minst 1 klass krävs", + "classesUnique": "Klassnamn måste vara unika", + "stateRequiresTwoClasses": "Tillståndsmodeller kräver minst två klasser", + "objectLabelRequired": "Välj en objektetikett", + "objectTypeRequired": "Vänligen välj en klassificeringstyp", + "noneNotAllowed": "Klassen 'none' är inte tillåten" + } + }, + "step2": { + "description": "Välj kameror och definiera området som ska övervakas för varje kamera. Modellen kommer att klassificera tillståndet för dessa områden.", + "cameras": "Kameror", + "selectCamera": "Välj kamera", + "noCameras": "Klicka på + för att lägga till kameror", + "selectCameraPrompt": "Välj en kamera från listan för att definiera dess övervakningsområde" + }, + "step3": { + "selectImagesPrompt": "Markera alla bilder med: {{className}}", + "selectImagesDescription": "Klicka på bilderna för att välja dem. Klicka på Fortsätt när du är klar med den här klass.", + "generating": { + "title": "Generera exempelbilder", + "description": "Frigate hämtar representativa bilder från dina inspelningar. Det kan ta en stund..." + }, + "training": { + "title": "Träningsmodell", + "description": "Din modell tränas i bakgrunden. Stäng den här dialogrutan så börjar modellen köras så snart träningen är klar." + }, + "retryGenerate": "Försök att generera igen", + "noImages": "Inga exempelbilder genererade", + "classifying": "Klassificering & Träning...", + "trainingStarted": "Träningen har börjat", + "errors": { + "noCameras": "Inga kameror konfigurerade", + "noObjectLabel": "Ingen objektetikett vald", + "generateFailed": "Misslyckades med att generera exempel: {{error}}", + "generationFailed": "Genereringen misslyckades. Försök igen.", + "classifyFailed": "Misslyckades med att klassificera bilder: {{error}}" + }, + "generateSuccess": "Exempelbilder har genererats", + "allImagesRequired_one": "Vänligen klassificera alla bilder. {{count}} bild återstår.", + "allImagesRequired_other": "Vänligen klassificera alla bilder. {{count}} bilder återstår.", + "modelCreated": "Modellen har skapats. Använd vyn Senaste klassificeringar för att lägga till bilder för saknade tillstånd och träna sedan modellen.", + "missingStatesWarning": { + "title": "Exempel på saknade tillstånd", + "description": "Det rekommenderas att välja exempel för alla tillstånd för bästa resultat. Du kan fortsätta utan att välja alla tillstånd, men modellen kommer inte att tränas förrän alla tillstånd har bilder. När du har fortsatt använder du vyn Senaste klassificeringar för att klassificera bilder för de saknade tillstånden och tränar sedan modellen." + } + } + }, + "deleteModel": { + "title": "Ta bort klassificeringsmodell", + "single": "Är du säker på att du vill ta bort {{name}}? Detta kommer att permanent ta bort all tillhörande data, inklusive bilder och träningsdata. Åtgärden kan inte ångras.", + "desc_one": "Är du säker på att du vill ta bort {{count}} modell? Detta kommer att permanent ta bort all tillhörande data, inklusive bilder och träningsdata. Åtgärden kan inte ångras.", + "desc_other": "Är du säker på att du vill ta bort {{count}} modeller? Detta kommer att permanent ta bort all tillhörande data, inklusive bilder och träningsdata. Åtgärden kan inte ångras." + }, + "menu": { + "objects": "Objekt", + "states": "Tillstånd" + }, + "details": { + "scoreInfo": "Poängen representerar den genomsnittliga klassificeringssäkerheten för alla upptäckter av detta objekt.", + "none": "Ingen", + "unknown": "Okänd" + }, + "edit": { + "title": "Redigera klassificeringsmodell", + "descriptionState": "Redigera klasserna för denna tillståndsklassificeringsmodell. Ändringar kräver omträning av modellen.", + "descriptionObject": "Redigera objekttyp och klassificeringstyp för denna objektklassificeringsmodell.", + "stateClassesInfo": "Observera: För att ändra tillståndsklasser måste modellen omtränas med de uppdaterade klasserna." + }, + "tooltip": { + "trainingInProgress": "Modellen tränar för närvarande", + "noNewImages": "Inga nya bilder att träna. Klassificera fler bilder i datasetet först.", + "noChanges": "Inga ändringar i datamängden sedan senaste träningen.", + "modelNotReady": "Modellen är inte redo för träning" + }, + "none": "Ingen" +} diff --git a/web/public/locales/sv/views/configEditor.json b/web/public/locales/sv/views/configEditor.json new file mode 100644 index 0000000..4e64a39 --- /dev/null +++ b/web/public/locales/sv/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "copyConfig": "Kopiera konfiguration", + "saveAndRestart": "Spara & Starta om", + "saveOnly": "Spara", + "toast": { + "success": { + "copyToClipboard": "Konfiguration kopierad till urklipp." + }, + "error": { + "savingError": "Problem att spara konfiguration" + } + }, + "documentTitle": "Ändra konfiguration - Frigate", + "configEditor": "Ändra konfiguration", + "confirm": "Avsluta utan att spara?", + "safeConfigEditor": "Konfigurationsredigeraren (felsäkert läge)", + "safeModeDescription": "Frigate är i felsäkert läge på grund av ett konfigurationsvalideringsfel." +} diff --git a/web/public/locales/sv/views/events.json b/web/public/locales/sv/views/events.json new file mode 100644 index 0000000..f849a43 --- /dev/null +++ b/web/public/locales/sv/views/events.json @@ -0,0 +1,67 @@ +{ + "detections": "Detektioner", + "alerts": "Varningar", + "motion": { + "label": "Rörelse", + "only": "Endast rörelse" + }, + "allCameras": "Alla kameror", + "empty": { + "alert": "Det finns inga varningar att granska", + "detection": "Det finns inga detekteringar att granska", + "motion": "Ingen rörelsedata hittad", + "recordingsDisabled": { + "title": "Inspelningar måste vara aktiverat", + "description": "Granskningsobjekt kan bara skapas för en kamera när inspelningar är aktiverat för den kameran." + } + }, + "documentTitle": "Granska - Frigate", + "timeline": "Tidslinje", + "events": { + "noFoundForTimePeriod": "Inga hädelser hittade för denna tidsperiod.", + "aria": "Välj händelse", + "label": "Händelse" + }, + "recordings": { + "documentTitle": "Inspelningar - Frigate" + }, + "newReviewItems": { + "label": "Visa nya objekt att granska", + "button": "Nya objekt att granska" + }, + "markAsReviewed": "Markera som granskad", + "calendarFilter": { + "last24Hours": "Senaste 24 timmarna" + }, + "timeline.aria": "Välj tidslinje", + "camera": "Kamera", + "markTheseItemsAsReviewed": "Markera dessa objekt som granskade", + "detected": "upptäckt", + "selected_one": "{{count}} valda", + "selected_other": "{{count}} valda", + "suspiciousActivity": "Misstänkt aktivitet", + "threateningActivity": "Hotande aktivitet", + "detail": { + "noDataFound": "Inga detaljerade data att granska", + "aria": "Växla detaljvy", + "trackedObject_one": "{{count}} objekt", + "trackedObject_other": "{{count}} objekt", + "noObjectDetailData": "Inga objektdetaljdata tillgängliga.", + "label": "Detalj", + "settings": "Detaljvy inställningar", + "alwaysExpandActive": { + "title": "Expandera alltid aktivt", + "desc": "Expandera alltid objektinformationen för det aktiva granskningsobjektet när den är tillgänglig." + } + }, + "objectTrack": { + "trackedPoint": "Spårad punkt", + "clickToSeek": "Klicka för att söka till den här tiden" + }, + "zoomIn": "Zooma in", + "zoomOut": "Zooma ut", + "normalActivity": "Normal", + "needsReview": "Behöver granskas", + "securityConcern": "Säkerhetsproblem", + "select_all": "Alla" +} diff --git a/web/public/locales/sv/views/explore.json b/web/public/locales/sv/views/explore.json new file mode 100644 index 0000000..37ce216 --- /dev/null +++ b/web/public/locales/sv/views/explore.json @@ -0,0 +1,303 @@ +{ + "generativeAI": "Generativ AI", + "documentTitle": "Utforska - Frigate", + "exploreIsUnavailable": { + "embeddingsReindexing": { + "startingUp": "Startar upp…", + "estimatedTime": "Beräknad återstående tid:", + "finishingShortly": "Snart klar", + "context": "Utforskaren kan användas när inbäddningarna för spårade objekt har indexerats om.", + "step": { + "thumbnailsEmbedded": "Inbäddade miniatyrbilder: ", + "descriptionsEmbedded": "Beskrivningar inbäddade: ", + "trackedObjectsProcessed": "Bearbetade spårade objekt: " + } + }, + "title": "Utforska är inte tillgänglig", + "downloadingModels": { + "setup": { + "textModel": "Text modell", + "visionModel": "Visionsmodell", + "visionModelFeatureExtractor": "Funktionsutdragare för visionsmodell", + "textTokenizer": "Texttokeniserare" + }, + "tips": { + "documentation": "Läs dokumentationen", + "context": "Du kanske vill omindexera inbäddningarna av dina spårade objekt när modellerna har laddats ner." + }, + "error": "Ett fel har inträffat. Kontrollera Frigate loggarna.", + "context": "Frigate laddar ner de nödvändiga inbäddningsmodellerna för att stödja den semantiska sökfunktionen. Detta kan ta flera minuter beroende på hastigheten på din nätverksanslutning." + } + }, + "details": { + "timestamp": "tidsstämpel", + "item": { + "title": "Granska objektinformation", + "desc": "Granska objektinformation", + "button": { + "share": "Dela den här recensionen", + "viewInExplore": "Visa i Utforska" + }, + "tips": { + "mismatch_one": "{{count}} otillgängligt objekt upptäcktes och inkluderades i detta granskningsobjekt. Dessa objekt kvalificerade sig antingen inte som en varning eller detektering, eller så har de redan rensats/raderats.", + "mismatch_other": "{{count}} otillgängliga objekt upptäcktes och inkluderades i detta granskningsobjekt. Dessa objekt kvalificerade sig antingen inte som en varning eller upptäckt, eller så har de redan rensats/raderats.", + "hasMissingObjects": "Justera din konfiguration om du vill att Frigate ska spara spårade objekt för följande etiketter: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "En ny beskrivning har begärts från {{provider}}. Beroende på din leverantörs hastighet kan det ta lite tid att generera den nya beskrivningen.", + "updatedSublabel": "Underetiketten har uppdaterats.", + "updatedLPR": "Nummerplåt har uppdaterats.", + "audioTranscription": "Ljudtranskription har begärts. Beroende på hastigheten på din Frigate-server kan transkriptionen ta lite tid att slutföra.", + "updatedAttributes": "Attributen har uppdaterats." + }, + "error": { + "regenerate": "Kunde inte ringa {{provider}} för en ny beskrivning: {{errorMessage}}", + "updatedSublabelFailed": "Misslyckades med att uppdatera underetiketten: {{errorMessage}}", + "audioTranscription": "Misslyckades med att begära ljudtranskription: {{errorMessage}}", + "updatedLPRFailed": "Misslyckades med att uppdatera nummerplåten: {{errorMessage}}", + "updatedAttributesFailed": "Misslyckades med att uppdatera attribut: {{errorMessage}}" + } + } + }, + "label": "Märka", + "editSubLabel": { + "title": "Redigera underetikett", + "desc": "Ange en ny underetikett för denna {{label}}", + "descNoLabel": "Ange en ny underetikett för det här spårade objektet" + }, + "editLPR": { + "title": "Redigera nummerplåt", + "desc": "Ange ett nytt registreringsnummer för detta {{label}}-objekt", + "descNoLabel": "Ange ett nytt registreringsnummer för det här spårade objektet" + }, + "snapshotScore": { + "label": "Ögonblicksbildspoäng" + }, + "topScore": { + "label": "Högsta poäng", + "info": "Topppoängen är den högsta medianpoängen för det spårade objektet, så denna kan skilja sig från poängen som visas på miniatyrbilden av sökresultatet." + }, + "score": { + "label": "Poäng" + }, + "recognizedLicensePlate": "Identifierad registreringsskylt", + "estimatedSpeed": "Uppskattad hastighet", + "objects": "Objekt", + "camera": "Kamera", + "zones": "Zoner", + "button": { + "findSimilar": "Hitta liknande", + "regenerate": { + "title": "Regenerera", + "label": "Återskapa beskrivningen av spårat objekt" + } + }, + "description": { + "label": "Beskrivning", + "placeholder": "Beskrivning av det spårade objektet", + "aiTips": "Frigate kommer inte att begära en beskrivning från din generativa AI-leverantör förrän det spårade objektets livscykel har avslutats." + }, + "expandRegenerationMenu": "Expandera regenereringsmenyn", + "regenerateFromSnapshot": "Återskapa från ögonblicksbild", + "regenerateFromThumbnails": "Återskapa från miniatyrbilder", + "tips": { + "descriptionSaved": "Beskrivningen har sparats", + "saveDescriptionFailed": "Misslyckades med att uppdatera beskrivningen: {{errorMessage}}" + }, + "editAttributes": { + "title": "Redigera attribut", + "desc": "Välj klassificeringsattribut för denna {{label}}" + }, + "attributes": "Klassificeringsattribut", + "title": { + "label": "Titel" + } + }, + "exploreMore": "Utforska fler {{label}} objekt", + "type": { + "details": "detaljer", + "video": "video", + "snapshot": "ögonblicksbild", + "object_lifecycle": "objektets livscykel", + "thumbnail": "miniatyrbild", + "tracking_details": "spårningsdetaljer" + }, + "trackedObjectDetails": "Detaljer om spårade objekt", + "objectLifecycle": { + "title": "Objektets livscykel", + "noImageFound": "Ingen bild hittades för denna tidsstämpel.", + "createObjectMask": "Skapa objektmask", + "adjustAnnotationSettings": "Justera annoteringsinställningar", + "scrollViewTips": "Scrolla för att se de viktiga ögonblicken i detta objekts livscykel.", + "autoTrackingTips": "Begränsningsrutornas positioner kommer att vara felaktiga för autospårningskameror.", + "count": "{{first}} av {{second}}", + "lifecycleItemDesc": { + "external": "{{label}} upptäckt", + "header": { + "zones": "Zoner", + "ratio": "Proportion", + "area": "Område" + }, + "visible": "{{label}} upptäckt", + "entered_zone": "{{label}} gick in i {{zones}}", + "active": "{{label}} blev aktiv", + "stationary": "{{label}} blev stationär", + "attribute": { + "faceOrLicense_plate": "{{attribute}} upptäckt för {{label}}", + "other": "{{label}} igenkänd som {{attribute}}" + }, + "gone": "{{label}} vänster", + "heard": "{{label}} hört" + }, + "annotationSettings": { + "title": "Annoteringsinställningar", + "showAllZones": { + "title": "Visa alla zoner", + "desc": "Visa alltid zoner på ramar där objekt har kommit in i en zon." + }, + "offset": { + "label": "Annoteringsförskjutning", + "desc": "Denna data kommer från din kameras detekteringsflöde men läggs ovanpå bilder från inspelningsflödet. Det är osannolikt att de två strömmarna är helt synkroniserade. Som ett resultat kommer avgränsningsramen och filmmaterialet inte att radas upp perfekt. Fältet annotation_offset kan dock användas för att justera detta.", + "documentation": "Läs dokumentationen ", + "millisecondsToOffset": "Millisekunder för att förskjuta detektera annoteringar med. Standard: 0", + "tips": "TIPS: Föreställ dig ett händelseklipp med en person som går från vänster till höger. Om tidslinjens avgränsningsram konsekvent är till vänster om personen bör värdet minskas. På samma sätt, om en person går från vänster till höger och avgränsningsramen konsekvent är framför personen bör värdet ökas.", + "toast": { + "success": "Annoterings förskjutningen för {{camera}} har sparats i konfigurationsfilen. Starta om Frigate för att tillämpa dina ändringar." + } + } + }, + "trackedPoint": "Spårad punkt", + "carousel": { + "previous": "Föregående bild", + "next": "Nästa bild" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "Ladda ner video", + "aria": "Ladda ner video" + }, + "downloadSnapshot": { + "label": "Ladda ner ögonblicksbild", + "aria": "Ladda ner ögonblicksbild" + }, + "viewObjectLifecycle": { + "label": "Visa objektets livscykel", + "aria": "Visa objektets livscykel" + }, + "findSimilar": { + "label": "Hitta liknande", + "aria": "Hitta liknande spårade objekt" + }, + "addTrigger": { + "label": "Lägg till utlösare", + "aria": "Lägg till en utlösare för det här spårade objektet" + }, + "audioTranscription": { + "label": "Transkribera", + "aria": "Begär ljudtranskribering" + }, + "submitToPlus": { + "label": "Skicka till Frigate+", + "aria": "Skicka till Frigate Plus" + }, + "viewInHistory": { + "label": "Visa i historik", + "aria": "Visa i historik" + }, + "deleteTrackedObject": { + "label": "Ta bort det här spårade objektet" + }, + "showObjectDetails": { + "label": "Visa objektets plats" + }, + "viewTrackingDetails": { + "label": "Visa spårningsinformation", + "aria": "Visa spårningsdetaljerna" + }, + "hideObjectDetails": { + "label": "Dölj objektsökväg" + }, + "downloadCleanSnapshot": { + "label": "Ladda ner ren ögonblicksbild", + "aria": "Ladda ner ren ögonblicksbild" + } + }, + "dialog": { + "confirmDelete": { + "title": "Bekräfta radering", + "desc": "Om du tar bort det här spårade objektet tas ögonblicksbilden, alla sparade inbäddningar och alla tillhörande spårningsdetaljer bort. Inspelade bilder av det här spårade objektet i historikvyn kommer INTE att raderas.

    Är du säker på att du vill fortsätta?" + } + }, + "noTrackedObjects": "Inga spårade objekt hittades", + "fetchingTrackedObjectsFailed": "Fel vid hämtning av spårade objekt: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} spårat objekt ", + "trackedObjectsCount_other": "{{count}} spårade objekt ", + "searchResult": { + "tooltip": "Matchade {{type}} vid {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "success": "Spårat objekt har raderats.", + "error": "Misslyckades med att ta bort spårat objekt: {{errorMessage}}" + } + }, + "previousTrackedObject": "Föregående spårade objekt", + "nextTrackedObject": "Nästa spårade objekt" + }, + "aiAnalysis": { + "title": "AI-analys" + }, + "concerns": { + "label": "Oro" + }, + "trackingDetails": { + "title": "Spårningsdetaljer", + "noImageFound": "Ingen bild hittades för denna tidsstämpel.", + "createObjectMask": "Skapa objektmask", + "adjustAnnotationSettings": "Justera annoteringsinställningar", + "scrollViewTips": "Klicka för att se de viktiga ögonblicken i detta objekts livscykel.", + "autoTrackingTips": "Begränsningsrutornas positioner kommer att vara felaktiga för autospårningskameror.", + "count": "{{first}} av {{second}}", + "trackedPoint": "Spårad punkt", + "lifecycleItemDesc": { + "visible": "{{label}} upptäckt", + "entered_zone": "{{label}} gick in i {{zones}}", + "active": "{{label}} blev aktiv", + "stationary": "{{label}} blev stationär", + "attribute": { + "faceOrLicense_plate": "{{attribute}} upptäckt för {{label}}", + "other": "{{label}} igenkänd som {{attribute}}" + }, + "gone": "{{label}} lämnade", + "heard": "{{label}} hördes", + "external": "{{label}} upptäckt", + "header": { + "zones": "Zoner", + "ratio": "Förhållandet", + "area": "Område", + "score": "Resultat" + } + }, + "annotationSettings": { + "title": "Annoteringsinställningar", + "showAllZones": { + "title": "Visa alla zoner", + "desc": "Visa alltid zoner på ramar där objekt har kommit in i en zon." + }, + "offset": { + "label": "Annoteringsförskjutning", + "desc": "Denna data kommer från din kameras detekteringsflöde men läggs ovanpå bilder från inspelningsflödet. Det är osannolikt att de två strömmarna är helt synkroniserade. Som ett resultat kommer avgränsningsramen och filmmaterialet inte att radas upp perfekt. Du kan använda den här inställningen för att förskjuta anteckningarna framåt eller bakåt i tiden för att bättre anpassa dem till det inspelade materialet.", + "millisecondsToOffset": "Millisekunder för att förskjuta detektera annoteringar med. Standard: 0", + "tips": "Sänk värdet om videouppspelningen sker före rutorna och banpunkterna, och öka värdet om videouppspelningen sker bakom dem. Detta värde kan vara negativt.", + "toast": { + "success": "Annoteringsförskjutningen för {{camera}} har sparats i konfigurationsfilen." + } + } + }, + "carousel": { + "previous": "Föregående bild", + "next": "Nästa bild" + } + } +} diff --git a/web/public/locales/sv/views/exports.json b/web/public/locales/sv/views/exports.json new file mode 100644 index 0000000..da2bc13 --- /dev/null +++ b/web/public/locales/sv/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "Sök", + "documentTitle": "Export - Frigate", + "noExports": "Inga exporter hittade", + "deleteExport": "Radera export", + "deleteExport.desc": "Är du säker att du vill radera {{exportName}}?", + "editExport": { + "desc": "Ange ett nytt namn för denna export.", + "title": "Byt namn på Export", + "saveExport": "Spara Export" + }, + "toast": { + "error": { + "renameExportFailed": "Misslyckades att byta namn på export: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Dela export", + "downloadVideo": "Ladda ner video", + "editName": "Redigera namn", + "deleteExport": "Ta bort export" + } +} diff --git a/web/public/locales/sv/views/faceLibrary.json b/web/public/locales/sv/views/faceLibrary.json new file mode 100644 index 0000000..24c80eb --- /dev/null +++ b/web/public/locales/sv/views/faceLibrary.json @@ -0,0 +1,102 @@ +{ + "details": { + "person": "Person", + "confidence": "Säkerhet", + "face": "Ansiktsdetaljer", + "timestamp": "tidsstämpel", + "faceDesc": "Detaljer om det spårade objektet som genererade detta ansikte", + "unknown": "Okänd", + "subLabelScore": "Underetikettpoäng", + "scoreInfo": "Poängen är ett viktat genomsnitt av alla ansiktspoäng, viktat efter ansiktets storlek i varje bild." + }, + "description": { + "placeholder": "Ange ett namn för denna samling", + "addFace": "Lägg till en ny samling i ansiktsbiblioteket genom att ladda upp din första bild.", + "invalidName": "Ogiltigt namn. Namn får endast innehålla bokstäver, siffror, mellanslag, apostrofer, understreck och bindestreck.", + "nameCannotContainHash": "Namn får inte innehålla #." + }, + "documentTitle": "Ansiktsbibliotek - Frigate", + "steps": { + "faceName": "Ange namn", + "uploadFace": "Ladda upp bild på ansikte", + "nextSteps": "Nästa steg", + "description": { + "uploadFace": "Ladda upp en bild på {{name}} som visar deras ansikte framifrån. Bilden behöver inte beskäras till bara deras ansikte." + } + }, + "createFaceLibrary": { + "title": "Skapa samling", + "desc": "Skapa ny samling", + "nextSteps": "För att bygga en stark grund:
  • Använd fliken Senaste Igenkänningar för att välja och träna bilder för varje detekterad person.
  • Fokusera på raka bilder för bästa resultat; undvik att träna bilder som fångar ansikten i en vinkel.
  • ", + "new": "Skapa nytt ansikte" + }, + "train": { + "title": "Senaste Igenkänningar", + "aria": "Välj senaste igenkänningar", + "empty": "Det finns inga ny försök till ansiktsigenkänning", + "titleShort": "Ny" + }, + "uploadFaceImage": { + "title": "Ladda upp ansiktsbild", + "desc": "Ladda upp en bild för att skanna efter ansikte och inkludera {{pageToggle}}" + }, + "selectItem": "Välj {{item}}", + "collections": "Samlingar", + "selectFace": "Välj ansikte", + "deleteFaceLibrary": { + "title": "Ta bort namn", + "desc": "Är du säker på att du vill ta bort samlingen {{name}}? Detta kommer att ta bort alla associerade ansikten permanent." + }, + "deleteFaceAttempts": { + "title": "Ta bort ansikten", + "desc_one": "Är du säker på att du vill ta bort {{count}} ansikte? Den här åtgärden kan inte ångras.", + "desc_other": "Är du säker på att du vill ta bort {{count}} ansikten? Den här åtgärden kan inte ångras." + }, + "imageEntry": { + "dropActive": "Släpp bilden här…", + "dropInstructions": "Dra och släpp eller klistra in en bild här, eller klicka för att välja", + "maxSize": "Maxstorlek: {{size}}MB", + "validation": { + "selectImage": "Välj en bildfil." + } + }, + "nofaces": "Inga ansikten tillgängliga", + "pixels": "{{area}}px", + "readTheDocs": "Läs dokumentationen", + "trainFaceAs": "Träna ansikte som:", + "trainFace": "Träna ansikte", + "toast": { + "success": { + "uploadedImage": "Bilden har laddats upp.", + "addFaceLibrary": "{{name}} har lagts till i ansiktsbiblioteket!", + "deletedFace_one": "{{count}} ansikte har raderats.", + "deletedFace_other": "{{count}} ansikten har raderats.", + "deletedName_one": "{{count}} ansikte har raderats.", + "deletedName_other": "{{count}} ansikten har raderats.", + "renamedFace": "Ansiktet har bytt namn till {{name}}", + "trainedFace": "Ansikte är tränant.", + "updatedFaceScore": "Ansiktspoängen för {{name}} har uppdaterats till {{score}}." + }, + "error": { + "uploadingImageFailed": "Misslyckades med att ladda upp bilden: {{errorMessage}}", + "addFaceLibraryFailed": "Misslyckades med att ange ansiktsnamn: {{errorMessage}}", + "deleteFaceFailed": "Misslyckades med att ta bort: {{errorMessage}}", + "deleteNameFailed": "Misslyckades med att ta bort namnet: {{errorMessage}}", + "renameFaceFailed": "Misslyckades med att byta namn på ansikte: {{errorMessage}}", + "trainFailed": "Misslyckades med att träna: {{errorMessage}}", + "updateFaceScoreFailed": "Misslyckades med att uppdatera ansiktspoäng: {{errorMessage}}" + } + }, + "renameFace": { + "title": "Byt namn på ansikte", + "desc": "Ange ett nytt namn för {{name}}" + }, + "button": { + "deleteFaceAttempts": "Ta bort ansikten", + "addFace": "Lägg till ansikte", + "renameFace": "Byt namn på ansikte", + "deleteFace": "Ta bort ansikte", + "uploadImage": "Ladda upp bild", + "reprocessFace": "Återbearbeta ansiktet" + } +} diff --git a/web/public/locales/sv/views/live.json b/web/public/locales/sv/views/live.json new file mode 100644 index 0000000..bd2bfbc --- /dev/null +++ b/web/public/locales/sv/views/live.json @@ -0,0 +1,199 @@ +{ + "documentTitle": "Live - Frigate", + "documentTitle.withCamera": "{{camera}} - Live - Frigate", + "twoWayTalk": { + "enable": "Aktivera tvåvägssamtal", + "disable": "Avaktivera tvåvägssamtal" + }, + "cameraAudio": { + "disable": "Inaktivera kameraljud", + "enable": "Aktivera kameraljud" + }, + "ptz": { + "zoom": { + "in": { + "label": "Zooma in PTZ-kameran" + }, + "out": { + "label": "Zooma ut PTZ-kameran" + } + }, + "move": { + "up": { + "label": "Flytta PTZ-kameran uppåt" + }, + "right": { + "label": "Flytta PTZ-kameran åt höger" + }, + "clickMove": { + "disable": "Inaktivera klick för att flytta", + "label": "Klicka i bilden för att centrera kameran", + "enable": "Aktivera klick för att flytta" + }, + "left": { + "label": "Flytta PTZ kamera till vänster" + }, + "down": { + "label": "Flytta PTZ kamera nedåt" + } + }, + "frame": { + "center": { + "label": "Klicka i bilden för att centrera PTZ kamera" + } + }, + "presets": "PTZ-kamerans förinställningar", + "focus": { + "in": { + "label": "Fokusera PTZ-kameran närmare" + }, + "out": { + "label": "Fokusera PTZ-kameran ut" + } + } + }, + "streamStats": { + "enable": "Visa videostatistik", + "disable": "Dölj videostatistik" + }, + "detect": { + "enable": "Aktivera detektering", + "disable": "Avaktivera detektering" + }, + "recording": { + "enable": "Aktivera inspelning", + "disable": "Avaktivera inspelning" + }, + "snapshots": { + "enable": "Aktivera ögonblicksbilder", + "disable": "Avaktivera ögonblicksbilder" + }, + "audioDetect": { + "enable": "Aktivera ljudaktivering", + "disable": "Avaktivera ljudaktivering" + }, + "autotracking": { + "enable": "Aktivera Autospårning", + "disable": "Avaktivera Autospårning" + }, + "notifications": "Notifikationer", + "audio": "Ljud", + "lowBandwidthMode": "Läge för låg bandbredd", + "manualRecording": { + "failedToEnd": "Misslyckades med att avsluta manuell vid behov-inspelning.", + "started": "Starta manuell inspelning vid behov.", + "title": "Vid behov", + "tips": "Ladda ner en omedelbar ögonblicksbild eller starta en manuell händelse baserat på kamerans inställningar för inspelningslagring.", + "playInBackground": { + "label": "Spela upp i bakgrunden", + "desc": "Strömma vidare när spelaren inte visas." + }, + "showStats": { + "label": "Visa Statistik", + "desc": "Visa statistik ovanpå kamerabilden." + }, + "debugView": "Felsökningsvy", + "start": "Starta inspelning vid behov", + "failedToStart": "Misslyckades med att starta manuell inspelning vid behov.", + "recordDisabledTips": "Eftersom inspelning är inaktiverad eller begränsad i konfigurationen för den här kameran kommer endast en ögonblicksbild att sparas.", + "end": "Avsluta vid behov-inspelning", + "ended": "Avslutade manuell vid behov-inspelning." + }, + "cameraSettings": { + "audioDetection": "Ljuddetektering", + "title": "Inställningar för {{camera}}", + "cameraEnabled": "Kamera Aktiverad", + "objectDetection": "Objektsdetektering", + "recording": "Inspelning", + "snapshots": "Ögonblicksbilder", + "autotracking": "Autospårning", + "transcription": "Ljudtranskription" + }, + "effectiveRetainMode": { + "modes": { + "active_objects": "Aktiva Objekt", + "all": "Allt", + "motion": "Rörelse" + }, + "notAllTips": "Din lagringskonfiguration för {{source}}-inspelning är inställd på läge: {{effectiveRetainMode}}, så den här inspelningen vid behov kommer endast att behålla segment av {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Redigera Layout", + "group": { + "label": "Redigera Kameragrupp" + }, + "exitEdit": "Lämna Redigering" + }, + "camera": { + "enable": "Aktivera Kamera", + "disable": "Inaktivera Kamera" + }, + "muteCameras": { + "enable": "Slå av ljudet på alla kameror", + "disable": "Slå på ljudet på alla kameror" + }, + "streamingSettings": "Inställningar för Streaming", + "suspend": { + "forTime": "Pausa för: " + }, + "stream": { + "title": "Ström", + "audio": { + "tips": { + "title": "Ljud måste skickas ut från din kamera och konfigureras i go2rtc för den här strömmen.", + "documentation": "Läs dokumentationen: " + }, + "available": "Ljud är tillgängligt för den här strömmen", + "unavailable": "Ljud är inte tillgängligt för den här strömmen" + }, + "twoWayTalk": { + "tips": "Din enhet måste stödja funktionen och WebRTC måste vara konfigurerat för tvåvägskommunikation.", + "tips.documentation": "Läs dokumentationen: ", + "available": "tvåvägskommunikation är tillgängligt för den här strömmen", + "unavailable": "Tvåvägskommunikation är inte tillgängligt för den här strömmen" + }, + "lowBandwidth": { + "tips": "Livevisningen är i lägebandbreddsläge på grund av buffring eller strömningsfel.", + "resetStream": "Återställ ström" + }, + "playInBackground": { + "label": "Spela i bakgrunden", + "tips": "Aktivera det här alternativet för att fortsätta strömma när spelaren är dold." + }, + "debug": { + "picker": "Strömval är inte tillgängligt i felsökningsläge. Felsökningsvyn använder alltid den ström som tilldelats detekteringsrollen." + } + }, + "history": { + "label": "Visa historiskt videomaterial" + }, + "transcription": { + "enable": "Aktivera live-ljudtranskription", + "disable": "Inaktivera live-ljudtranskription" + }, + "noCameras": { + "title": "Inga kameror konfigurerade", + "description": "Börja med att ansluta en kamera till Frigate.", + "buttonText": "Lägg till kamera", + "restricted": { + "title": "Inga kameror tillgängliga", + "description": "Du har inte behörighet att visa några kameror i den här gruppen." + }, + "default": { + "title": "Ingen kamera konfigurerad", + "description": "Börja med att ansluta en kamera till Frigate.", + "buttonText": "Lägg till kamera" + }, + "group": { + "title": "Inga kameror i gruppen", + "description": "Kameragruppen har inga tilldelade eller aktiverade kameror.", + "buttonText": "Hantera grupper" + } + }, + "snapshot": { + "takeSnapshot": "Ladda ner omedelbar ögonblicksbild", + "noVideoSource": "Ingen videokälla tillgänglig för ögonblicksbilden.", + "captureFailed": "Misslyckades med att ta en ögonblicksbild.", + "downloadStarted": "Nedladdning av ögonblicksbild har startat." + } +} diff --git a/web/public/locales/sv/views/motionSearch.json b/web/public/locales/sv/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sv/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sv/views/recording.json b/web/public/locales/sv/views/recording.json new file mode 100644 index 0000000..b4bfaf2 --- /dev/null +++ b/web/public/locales/sv/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Export", + "filter": "Filtrera", + "calendar": "Kalender", + "filters": "Filter", + "toast": { + "error": { + "noValidTimeSelected": "Inget giltigt tidsintervall valt", + "endTimeMustAfterStartTime": "Sluttid måste vara efter starttid" + } + } +} diff --git a/web/public/locales/sv/views/replay.json b/web/public/locales/sv/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/sv/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sv/views/search.json b/web/public/locales/sv/views/search.json new file mode 100644 index 0000000..c1a01d6 --- /dev/null +++ b/web/public/locales/sv/views/search.json @@ -0,0 +1,73 @@ +{ + "savedSearches": "Sparade Sökningar", + "searchFor": "Sök efter {{inputValue}}", + "search": "Sök", + "button": { + "clear": "Radera sökning", + "save": "Spara sökning", + "filterActive": "Aktiva filter", + "delete": "Ta bort sparad sökning", + "filterInformation": "Filterinformation" + }, + "filter": { + "tips": { + "desc": { + "step1": "Skriv ett filtreringsnyckelnamn följt av ett kolon (t.ex. \"kameror:\").", + "step2": "Välj ett värde utifrån alternativen eller skriv in ditt egna.", + "step3": "Använd flera filter genom att lägga till dem en efter en med ett mellanslag mellan dem.", + "step4": "Datumfilter (före: och efter:) använd {{DateFormat}} format.", + "step5": "Tidsintervaller använder {{exampleTime}} format.", + "exampleLabel": "Exempel:", + "step6": "Ta bort filter genom att klicka på \"x\" bredvid dem.", + "text": "Filter hjälper dig att begränsa dina sökresultat. Så här använder du dem i inmatningsfältet:" + }, + "title": "Hur du använder filter" + }, + "header": { + "noFilters": "Filter", + "activeFilters": "Aktiva filter", + "currentFilterType": "Filtervärden" + }, + "label": { + "after": "Efter", + "min_speed": "Minsta Hastighet", + "zones": "Zoner", + "min_score": "Minsta Poäng", + "cameras": "Kameror", + "sub_labels": "Underetiketter", + "search_type": "Söktyp", + "time_range": "Tidsintervall", + "before": "Före", + "max_speed": "Högsta Hastighet", + "recognized_license_plate": "Identifierad registreringsskylt", + "has_clip": "Har klipp", + "has_snapshot": "Har Ögonblicksbild", + "labels": "Etiketter", + "max_score": "Högsta Poäng", + "attributes": "Attribut" + }, + "searchType": { + "thumbnail": "Miniatyrbild", + "description": "Beskrivning" + }, + "toast": { + "error": { + "afterDatebeEarlierBefore": "Datumet \"efter\" måste vara tidigare än datumet \"före\".", + "minScoreMustBeLessOrEqualMaxScore": "\"min_score\" måste vara mindre än eller lika med \"max_score\".", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "\"max_speed\" måste vara större än eller lika med \"min_speed\".", + "beforeDateBeLaterAfter": "Datumet \"före\" måste vara senare än datumet \"efter\".", + "maxScoreMustBeGreaterOrEqualMinScore": "\"max_score\" måste vara större än eller lika med \"min_score\".", + "minSpeedMustBeLessOrEqualMaxSpeed": "\"min_speed\" måste vara mindre än eller lika med \"max_speed\"." + } + } + }, + "similaritySearch": { + "title": "Likhetssökning", + "active": "Likhetsökning aktiv", + "clear": "Rensa likhetsökning" + }, + "placeholder": { + "search": "Sök…" + }, + "trackedObjectId": "Spårad Objekts ID" +} diff --git a/web/public/locales/sv/views/settings.json b/web/public/locales/sv/views/settings.json new file mode 100644 index 0000000..e09673a --- /dev/null +++ b/web/public/locales/sv/views/settings.json @@ -0,0 +1,1244 @@ +{ + "documentTitle": { + "camera": "Kamerainställningar - Frigate", + "default": "Inställningar - Frigate", + "general": "Användargränssnitt Inställningar - Frigate", + "authentication": "Autentiseringsinställningar - Frigate", + "classification": "Klassificeringsinställningar - Frigate", + "masksAndZones": "Maskerings- och zonverktyg - Frigate", + "enrichments": "Förbättringsinställningar - Frigate", + "frigatePlus": "Frigate+ Inställningar - Frigate", + "notifications": "Notifikations Inställningar - Frigate", + "motionTuner": "Rörelse inställning - Frigate", + "object": "Felsöka - Frigate", + "cameraManagement": "Hantera kameror - Frigate", + "cameraReview": "Kameragranskningsinställningar - Frigate" + }, + "general": { + "title": "UI inställningar", + "liveDashboard": { + "automaticLiveView": { + "desc": "Växla automatiskt till kamerans livevy när aktivitet upptäcks. Om det här alternativet inaktiveras uppdateras statiska kamerabilder på livepanelen bara en gång per minut.", + "label": "Automatisk livevisning" + }, + "playAlertVideos": { + "desc": "Som standard spelas de senaste varningarna upp som små loopande videor på livepanelen. Inaktivera det här alternativet om du bara vill visa en statisk bild av de senaste varningarna på den här enheten/i den här webbläsaren.", + "label": "Spela upp Varnings videor" + }, + "title": "Livepanel", + "displayCameraNames": { + "label": "Visa alltid kameranamn", + "desc": "Visa alltid kameranamnen i ett chip i instrumentpanelen för livevisning med flera kameror." + }, + "liveFallbackTimeout": { + "label": "Reservtimeout för livespelare", + "desc": "När en kameras högkvalitativa liveström inte är tillgänglig, återgå till lågbandbreddsläge efter så här många sekunder. Standard: 3." + } + }, + "storedLayouts": { + "title": "Sparade Layouter", + "desc": "Layouten av kameror i en grupp kan dras för att ändra storlek. Positionerna lagras lokalt i din webbläsare.", + "clearAll": "Rensa Alla Layouter" + }, + "cameraGroupStreaming": { + "desc": "Streaming inställningar för varje kameragrupp lagras lokalt i din webbläsare.", + "clearAll": "Rensa Alla Streaming Inställningar", + "title": "Kamera Grupp Streaming Inställningar" + }, + "recordingsViewer": { + "title": "Inspelningsvisare", + "defaultPlaybackRate": { + "desc": "Standard uppspelningshastighet för inspelningar.", + "label": "Standard Uppspelningshastighet" + } + }, + "calendar": { + "firstWeekday": { + "sunday": "Söndag", + "monday": "Måndag", + "label": "Första Veckodag", + "desc": "Den dag då veckorna i översynskalendern börjar." + }, + "title": "Kalender" + }, + "toast": { + "success": { + "clearStoredLayout": "Rensa lagrad layout för {{cameraName}}", + "clearStreamingSettings": "Rensa streaminginställningar för samtliga kameragrupper." + }, + "error": { + "clearStoredLayoutFailed": "Misslyckades att rensa lagrad layout för: {{errorMessage}}", + "clearStreamingSettingsFailed": "Misslyckades med att rensa streaminginställningar: {{errorMessage}}" + } + } + }, + "cameraSetting": { + "noCamera": "Ingen Kamera", + "camera": "Kamera" + }, + "enrichments": { + "unsavedChanges": "Osparade Förbättringsinställningar", + "birdClassification": { + "title": "Fågel klassificering", + "desc": "Fågelklassificering identifierar kända fåglar med hjälp av en kvantiserad Tensorflow-modell. När en känd fågel känns igen läggs dess vanliga namn till som en underetikett. Denna information inkluderas i användargränssnittet, filter och i aviseringar." + }, + "title": "Förbättringsinställningar", + "semanticSearch": { + "title": "Semantisk sökning", + "desc": "Semantisk sökning i Frigate låter dig hitta spårade objekt i dina granskningsobjekt med hjälp av antingen själva bilden, en användardefinierad textbeskrivning eller en automatiskt genererad.", + "readTheDocumentation": "Läs dokumentationen", + "reindexNow": { + "label": "Omindexera nu", + "desc": "Omindexering kommer att generera inbäddningar för alla spårade objekt. Den här processen körs i bakgrunden och kan maximera din CPU och ta en hel del tid beroende på antalet spårade objekt du har.", + "confirmTitle": "Bekräfta omindexering", + "confirmDesc": "Är du säker på att du vill omindexera alla spårade objektinbäddningar? Den här processen körs i bakgrunden men den kan maximera din processor och ta en hel del tid. Du kan se förloppet på Utforska-sidan.", + "confirmButton": "Omindexera", + "success": "Omindexeringen har startat.", + "alreadyInProgress": "Omindexering pågår redan.", + "error": "Misslyckades med att starta omindexering: {{errorMessage}}" + }, + "modelSize": { + "label": "Modellstorlek", + "desc": "Storleken på modellen som används för semantiska sökinbäddningar.", + "small": { + "title": "små", + "desc": "Att använda small använder en kvantiserad version av modellen som använder mindre RAM och körs snabbare på CPU med en mycket försumbar skillnad i inbäddningskvalitet." + }, + "large": { + "title": "stor", + "desc": "Att använda large använder hela Jina-modellen och körs automatiskt på GPU:n om tillämpligt." + } + } + }, + "faceRecognition": { + "desc": "Ansiktsigenkänning gör att personer kan tilldelas namn och när deras ansikte känns igen kommer Frigate att tilldela personens namn som en underetikett. Denna information finns i användargränssnittet, filter och i aviseringar.", + "readTheDocumentation": "Läs dokumentationen", + "modelSize": { + "label": "Modellstorlek", + "desc": "Storleken på modellen som används för ansiktsigenkänning.", + "small": { + "title": "små", + "desc": "Att använda small använder en FaceNet-modell för ansiktsinbäddning som körs effektivt på de flesta processorer." + }, + "large": { + "title": "stor", + "desc": "Att använda large använder en ArcFace-modell för ansiktsinbäddning och körs automatiskt på GPU:n om tillämpligt." + } + }, + "title": "Ansiktsigenkänning" + }, + "licensePlateRecognition": { + "title": "Igenkänning av registreringsskyltar", + "desc": "Frigate kan känna igen nummerplåt på fordon och automatiskt lägga till de upptäckta tecknen i fältet recognized_license_plate eller ett känt namn som en underetikett till objekt av typen bil. Ett vanligt användningsfall kan vara att läsa nummerplåtor på bilar som kör in på en uppfart eller bilar som passerar på en gata.", + "readTheDocumentation": "Läs dokumentationen" + }, + "restart_required": "Omstart krävs (berikningsinställningar har ändrats)", + "toast": { + "success": "Inställningarna för berikning har sparats. Starta om Frigate för att tillämpa dina ändringar.", + "error": "Kunde inte spara konfigurationsändringarna: {{errorMessage}}" + } + }, + "menu": { + "ui": "Användargränssnitt", + "cameras": "Kamera Inställningar", + "masksAndZones": "Masker / Områden", + "users": "Användare", + "notifications": "Notifikationer", + "frigateplus": "Frigate+", + "enrichments": "Förbättringar", + "motionTuner": "Rörelsemottagare", + "debug": "Felsök", + "triggers": "Utlösare", + "roles": "Roller", + "cameraManagement": "Hantering", + "cameraReview": "Granska", + "profiles": "Profiler" + }, + "dialog": { + "unsavedChanges": { + "title": "Du har osparade ändringar.", + "desc": "Vill du spara dina ändringar innan du fortsätter?" + } + }, + "camera": { + "title": "Kamera inställningar", + "streams": { + "title": "Videoströmmar", + "desc": "Inaktivera tillfälligt en kamera tills Frigate startar om. Om du inaktiverar en kamera helt stoppas Frigates bearbetning av kamerans strömmar. Detektering, inspelning och felsökning kommer inte att vara tillgängliga.
    Obs! Detta inaktiverar inte go2rtc-återströmmar." + }, + "object_descriptions": { + "title": "Generativa AI-objektbeskrivningar", + "desc": "Aktivera/inaktivera tillfälligt generativa AI-objektbeskrivningar för den här kameran. När den är inaktiverad kommer AI-genererade beskrivningar inte att begäras för spårade objekt på den här kameran." + }, + "review_descriptions": { + "title": "Beskrivningar av generativa AI-granskningar", + "desc": "Aktivera/inaktivera tillfälligt genererade AI-granskningsbeskrivningar för den här kameran. När det är inaktiverat kommer AI-genererade beskrivningar inte att begäras för granskningsobjekt på den här kameran." + }, + "review": { + "title": "Recensera", + "desc": "Tillfälligt Aktivera/avaktivera varningar och detekteringar för den här kameran tills Frigate startar om. När den är avaktiverad genereras inga nya granskningsobjekt. ", + "alerts": "Aviseringar ", + "detections": "Detektioner " + }, + "reviewClassification": { + "title": "Granska klassificering", + "desc": "Frigate kategoriserar granskningsobjekt som varningar och detekteringar. Som standard betraktas alla person- och bil-objekt som varningar. Du kan förfina kategoriseringen av dina granskningsobjekt genom att konfigurera obligatoriska zoner för dem.", + "noDefinedZones": "Inga zoner är definierade för den här kameran.", + "objectAlertsTips": "Alla {{alertsLabels}}-objekt på {{cameraName}} kommer att visas som varningar.", + "zoneObjectAlertsTips": "Alla {{alertsLabels}} objekt som upptäcks i {{zone}} på {{cameraName}} kommer att visas som varningar.", + "objectDetectionsTips": "Alla {{detectionsLabels}}-objekt som inte kategoriseras på {{cameraName}} kommer att visas som detektioner oavsett vilken zon de befinner sig i.", + "zoneObjectDetectionsTips": { + "text": "Alla {{detectionsLabels}}-objekt som inte kategoriseras i {{zone}} på {{cameraName}} kommer att visas som detektioner.", + "notSelectDetections": "Alla {{detectionsLabels}} objekt som upptäckts i {{zone}} på {{cameraName}} och som inte kategoriserats som varningar kommer att visas som detekteringar oavsett vilken zon de befinner sig i.", + "regardlessOfZoneObjectDetectionsTips": "Alla {{detectionsLabels}}-objekt som inte kategoriseras på {{cameraName}} kommer att visas som detektioner oavsett vilken zon de befinner sig i." + }, + "unsavedChanges": "Osparade inställningar för granskningsklassificering för {{camera}}", + "selectAlertsZones": "Välj zoner för aviseringar", + "selectDetectionsZones": "Välj zoner för detektioner", + "limitDetections": "Begränsa detektioner till specifika zoner", + "toast": { + "success": "Konfigurationen för granskning av klassificering har sparats. Starta om Frigate för att tillämpa ändringarna." + } + }, + "addCamera": "Lägg till ny kamera", + "editCamera": "Redigera kamera:", + "selectCamera": "Välj en kamera", + "backToSettings": "Tillbaka till kamera inställningar", + "cameraConfig": { + "add": "Lägg till kamera", + "edit": "Redigera kamera", + "description": "Konfigurera kamerainställningar inklusive strömingångar och roller.", + "name": "Kamera namn", + "nameRequired": "Kamera namn krävs", + "nameInvalid": "Kamera namnet får endast innehålla bokstäver, siffror, understreck, eller bindestreck", + "namePlaceholder": "t.ex. fram_dörr", + "enabled": "Aktiverad", + "ffmpeg": { + "inputs": "Ingångsströmmar", + "path": "Strömväg", + "pathRequired": "Strömningsväg krävs", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "Minst en roll krävs", + "rolesUnique": "Varje roll (ljud, detektering, inspelning) kan bara tilldelas en ström", + "addInput": "Lägg till inmatningsström", + "removeInput": "Ta bort inmatningsström", + "inputsRequired": "Minst en indataström krävs" + }, + "toast": { + "success": "Kamera {{cameraName}} sparades" + }, + "nameLength": "Namnet på kameran måste vara kortare än 24 tecken." + } + }, + "masksAndZones": { + "filter": { + "all": "Alla masker och zoner" + }, + "restart_required": "Omstart krävs (masker/zoner har ändrats)", + "toast": { + "success": { + "copyCoordinates": "Kopierade koordinaterna för {{polyName}} till urklipp." + }, + "error": { + "copyCoordinatesFailed": "Kunde inte kopiera koordinaterna till urklipp." + } + }, + "motionMaskLabel": "Rörelsemask {{number}}", + "objectMaskLabel": "Objektmask {{number}}", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Zonnamnet måste vara minst 2 tecken långt.", + "mustNotBeSameWithCamera": "Zonnamnet får inte vara detsamma som kameranamnet.", + "alreadyExists": "En zon med detta namn finns redan för den här kameran.", + "mustNotContainPeriod": "Zonnamnet får inte innehålla punkter.", + "hasIllegalCharacter": "Zonnamnet innehåller ogiltiga tecken.", + "mustHaveAtLeastOneLetter": "Zonnamnet måste ha minst en bokstav." + } + }, + "distance": { + "error": { + "text": "Avståndet måste vara större än eller lika med 0,1.", + "mustBeFilled": "Alla avståndsfält måste fyllas i för att hastighetsuppskattning ska kunna användas." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Trögheten måste vara över 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Uppehållstiden måste vara större än eller lika med 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Gränsvärdet för hastigheten måste vara större eller lika med 0.1." + } + }, + "polygonDrawing": { + "removeLastPoint": "Ta bort senaste punkten", + "reset": { + "label": "Rensa alla punkter" + }, + "snapPoints": { + "true": "Fäst punkter", + "false": "Fäst inte punkter" + }, + "delete": { + "title": "Bekräfta borttagning", + "desc": "Är du säker på att du vill ta bort {{type}} {{name}}?", + "success": "{{name}} har raderats." + }, + "error": { + "mustBeFinished": "Polygonritningen måste vara klar innan du sparar." + }, + "type": { + "zone": "zon", + "motion_mask": "rörelsemask", + "object_mask": "objektmask" + } + } + }, + "zones": { + "label": "Zoner", + "documentTitle": "Redigera zon - Frigate", + "desc": { + "documentation": "Dokumentation", + "title": "Zoner låter dig definiera ett specifikt område av bilden så att du kan avgöra om ett objekt befinner sig inom ett visst område eller inte." + }, + "add": "Lägg till zon", + "edit": "Redigera zon", + "name": { + "title": "Namn", + "inputPlaceHolder": "Ange ett namn…", + "tips": "Namnet måste vara minst 2 tecken långt, måste innehålla minst en bokstav och får inte vara namnet på en kamera eller någon annan zon på den här kameran." + }, + "inertia": { + "title": "Momentum", + "desc": "Anger hur många bildrutor ett objekt måste finnas i en zon innan de räknas som en del av zonen. Standard: 3" + }, + "objects": { + "title": "Objekt", + "desc": "Lista över objekt som gäller för den här zonen." + }, + "allObjects": "Alla objekt", + "point_one": "{{count}} poäng", + "point_other": "{{count}} poäng", + "clickDrawPolygon": "Klicka för att rita en polygon på bilden.", + "loiteringTime": { + "title": "Tid någon hänger omkring", + "desc": "Ställer in en minsta tid i sekunder som objektet måste vara i zonen för att det ska aktiveras. Standard: 0" + }, + "speedEstimation": { + "title": "Hastighetsuppskattning", + "desc": "Aktivera hastighetsuppskattning för objekt i den här zonen. Zonen måste ha exakt fyra punkter.", + "lineADistance": "Avstånd till linje A ({{unit}})", + "lineBDistance": "Avstånd till linje B ({{unit}})", + "lineCDistance": "Avstånd till linje C ({{unit}})", + "lineDDistance": "Avstånd till linje D ({{unit}})" + }, + "speedThreshold": { + "title": "Hastighetsgräns ({{unit}})", + "desc": "Anger en lägsta hastighet för objekt som ska beaktas i denna zon.", + "toast": { + "error": { + "pointLengthError": "Hastighetsuppskattning har inaktiverats för den här zonen. Zoner med hastighetsuppskattning måste ha exakt 4 punkter.", + "loiteringTimeError": "Zoner med uppehållstider större än 0 bör inte användas vid hastighetsuppskattning." + } + } + }, + "toast": { + "success": "Zonen ({{zoneName}}) har sparats." + } + }, + "motionMasks": { + "label": "Rörelsemask", + "documentTitle": "Redigera rörelsemask - Frigate", + "desc": { + "title": "Rörelsemasker används för att förhindra att oönskade typer av rörelser utlöser detektering. Övermaskering gör det svårare att spåra objekt.", + "documentation": "Dokumentation" + }, + "add": "Ny rörelsemask", + "edit": "Redigera rörelsemask", + "context": { + "title": "Rörelsemasker används för att förhindra att oönskade typer av rörelser utlöser detektering (till exempel: trädgrenar, kameratidsstämplar). Rörelsemasker bör användas mycket sparsamt, övermaskering gör det svårare att spåra objekt." + }, + "point_one": "{{count}} poäng", + "point_other": "{{count}} poäng", + "clickDrawPolygon": "Klicka för att rita en polygon på bilden.", + "polygonAreaTooLarge": { + "title": "Rörelsemasken täcker {{polygonArea}}% av kamerabilden. Stora rörelsemasker rekommenderas inte.", + "tips": "Rörelsemasker förhindrar inte att objekt upptäcks. Du bör använda en obligatorisk zon istället." + }, + "toast": { + "success": { + "title": "{{polygonName}} har sparats.", + "noName": "Rörelsemasken har sparats." + } + } + }, + "objectMasks": { + "label": "Objektmasker", + "documentTitle": "Redigera objektmask - Frigate", + "point_one": "{{count}} poäng", + "point_other": "{{count}} poäng", + "desc": { + "title": "Objektfiltermasker används för att filtrera bort falska positiva resultat för en given objekttyp baserat på plats.", + "documentation": "Dokumentation" + }, + "add": "Lägg till objektmask", + "edit": "Redigera objektmask", + "context": "Objektfiltermasker används för att filtrera bort falska positiva resultat för en given objekttyp baserat på plats.", + "clickDrawPolygon": "Klicka för att rita en polygon på bilden.", + "objects": { + "title": "Objekt", + "desc": "Objekttypen som gäller för den här objektmasken.", + "allObjectTypes": "Alla objekttyper" + }, + "toast": { + "success": { + "title": "{{polygonName}} har sparats.", + "noName": "Objektmasken har sparats." + } + } + } + }, + "motionDetectionTuner": { + "title": "Rörelsedetekteringstuner", + "unsavedChanges": "Osparade ändringar i rörelsejusteraren ({{camera}})", + "desc": { + "title": "Frigate använder rörelsedetektering som en första kontroll för att se om det händer något i bilden som är värt att kontrollera med objektdetektering.", + "documentation": "Läs guiden för rörelsejustering" + }, + "Threshold": { + "title": "Tröskel", + "desc": "Tröskelvärdet anger hur mycket förändring i en pixels luminans som krävs för att betraktas som rörelse. Standard: 30" + }, + "contourArea": { + "title": "Konturområde", + "desc": "Konturareans värde används för att avgöra vilka grupper av ändrade pixlar som kvalificeras som rörelse. Standard: 10" + }, + "improveContrast": { + "title": "Förbättra kontrasten", + "desc": "Förbättra kontrasten för mörkare scener. Standard: PÅ" + }, + "toast": { + "success": "Rörelseinställningarna har sparats." + } + }, + "debug": { + "title": "Felsök", + "detectorDesc": "Frigate använder dina detektorer ({{detectors}}) för att upptäcka objekt i kamerans videoström.", + "desc": "Felsökningsvyn visar en realtidsvy av spårade objekt och deras statistik. Objektlistan visar en tidsfördröjd sammanfattning av upptäckta objekt.", + "openCameraWebUI": "Öppna {{camera}}s webbgränssnitt", + "debugging": "Felsökning", + "objectList": "Objektlista", + "noObjects": "Inga föremål", + "audio": { + "title": "Ljud", + "noAudioDetections": "Inga ljuddetekteringar", + "score": "betyg", + "currentRMS": "Nuvarande RMS", + "currentdbFS": "Nuvarande dbFS" + }, + "boundingBoxes": { + "title": "Avgränsande rutor", + "desc": "Visa avgränsningsrutor runt spårade objekt", + "colors": { + "label": "Färger för objektgränser", + "info": "
  • Vid uppstart tilldelas olika färger till varje objektetikett
  • En mörkblå tunn linje indikerar att objektet inte detekteras vid denna aktuella tidpunkt
  • En grå tunn linje indikerar att objektet detekteras som stillastående
  • En tjock linje indikerar att objektet är föremål för autospårning (när det är aktiverat)
  • " + } + }, + "timestamp": { + "title": "Tidsstämpel", + "desc": "Lägg en tidsstämpel över bilden" + }, + "zones": { + "title": "Zoner", + "desc": "Visa en översikt över alla definierade zoner" + }, + "mask": { + "title": "Rörelsemasker", + "desc": "Visa rörelsemaskpolygoner" + }, + "motion": { + "title": "Rörelseboxar", + "desc": "Visa rutor runt områden där rörelse detekteras", + "tips": "

    Rörelserutor


    Röda rutor kommer att läggas över områden i bilden där rörelse för närvarande detekteras

    " + }, + "regions": { + "title": "Regioner", + "desc": "Visa en ruta med det intresseområde som skickats till objektdetektorn", + "tips": "

    Regionsrutor


    Ljusgröna rutor kommer att läggas över intressanta områden i bilden som skickas till objektdetektorn.

    " + }, + "paths": { + "title": "Vägar", + "desc": "Visa viktiga punkter i det spårade objektets bana", + "tips": "

    Vägar


    Linjer och cirklar indikerar viktiga punkter som det spårade objektet har flyttat under sin livscykel.

    " + }, + "objectShapeFilterDrawing": { + "title": "Ritning av objektformfilter", + "desc": "Rita en rektangel på bilden för att visa detaljer om area och förhållande", + "tips": "Aktivera det här alternativet för att rita en rektangel på kamerabilden för att visa dess area och förhållande. Dessa värden kan sedan användas för att ställa in parametrar för objektformsfilter i din konfiguration.", + "score": "Betyg", + "ratio": "Förhållandet", + "area": "Område" + } + }, + "users": { + "title": "Användare", + "management": { + "title": "Användarhantering", + "desc": "Hantera användarkonton för denna Frigate-instans." + }, + "addUser": "Lägg till användare", + "updatePassword": "Återställ lösenord", + "toast": { + "success": { + "createUser": "Användaren {{user}} har skapats", + "deleteUser": "Användaren {{user}} har raderats", + "updatePassword": "Lösenordet har uppdaterats.", + "roleUpdated": "Rollen uppdaterades för {{user}}" + }, + "error": { + "setPasswordFailed": "Misslyckades med att spara lösenordet: {{errorMessage}}", + "createUserFailed": "Misslyckades med att skapa användare: {{errorMessage}}", + "deleteUserFailed": "Misslyckades med att ta bort användaren: {{errorMessage}}", + "roleUpdateFailed": "Misslyckades med att uppdatera rollen: {{errorMessage}}" + } + }, + "table": { + "username": "Användarnamn", + "actions": "Åtgärder", + "role": "Roll", + "noUsers": "Inga användare hittades.", + "changeRole": "Ändra användarroll", + "password": "Återställ Lösenord", + "deleteUser": "Ta bort användare" + }, + "dialog": { + "form": { + "user": { + "title": "Användarnamn", + "desc": "Endast bokstäver, siffror, punkter och understreck är tillåtna.", + "placeholder": "Ange användarnamn" + }, + "password": { + "title": "Lösenord", + "strength": { + "title": "Lösenordsstyrka: ", + "weak": "Svag", + "medium": "Mellanstark", + "strong": "Stark", + "veryStrong": "Mycket stark" + }, + "match": "Lösenorden matchar", + "notMatch": "Lösenorden matchar inte", + "placeholder": "Ange lösenord", + "confirm": { + "title": "Bekräfta lösenord", + "placeholder": "Bekräfta lösenord" + }, + "show": "Visa lösenord", + "hide": "Dölj lösenord", + "requirements": { + "title": "Lösenordskrav:", + "length": "Minst 12 tecken", + "uppercase": "Minst en stor bokstav", + "digit": "Minst en siffra", + "special": "Minst ett specialtecken (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nytt lösenord", + "placeholder": "Ange nytt lösenord", + "confirm": { + "placeholder": "Ange nytt lösenord igen" + } + }, + "usernameIsRequired": "Användarnamn krävs", + "passwordIsRequired": "Lösenord krävs", + "currentPassword": { + "title": "Nuvarande lösenord", + "placeholder": "Ange ditt nuvarande lösenord" + } + }, + "createUser": { + "title": "Skapa ny användare", + "desc": "Lägg till ett nytt användarkonto och ange en roll för åtkomst till områden i Frigate-gränssnittet.", + "usernameOnlyInclude": "Användarnamnet får endast innehålla bokstäver, siffror, . eller _", + "confirmPassword": "Vänligen bekräfta ditt lösenord" + }, + "deleteUser": { + "title": "Ta bort användare", + "desc": "Den här åtgärden kan inte ångras. Detta kommer att permanent radera användarkontot och all tillhörande data.", + "warn": "Är du säker på att du vill ta bort {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "Lösenordet får inte vara tomt", + "doNotMatch": "Lösenorden matchar inte", + "updatePassword": "Uppdatera lösenord för {{username}}", + "setPassword": "Ange lösenord", + "desc": "Skapa ett starkt lösenord för att säkra det här kontot.", + "currentPasswordRequired": "Nuvarande lösenord krävs", + "incorrectCurrentPassword": "Nuvarande lösenord är felaktigt", + "passwordVerificationFailed": "Misslyckades med att verifiera lösenordet", + "multiDeviceWarning": "Alla andra enheter där du är inloggad måste logga in igen inom {{refresh_time}}.", + "multiDeviceAdmin": "Du kan också tvinga alla användare att autentisera om sig omedelbart genom att rotera din JWT-hemlighet." + }, + "changeRole": { + "title": "Ändra användarroll", + "select": "Välj en roll", + "desc": "Uppdatera behörigheter för {{username}}", + "roleInfo": { + "intro": "Välj lämplig roll för den här användaren:", + "admin": "Administratör", + "adminDesc": "Full åtkomst till alla funktioner.", + "viewer": "Åskådare", + "viewerDesc": "Endast åtkomst till Live-paneler, Granskning, Utforska och Exporter.", + "customDesc": "Anpassad roll med specifik kameraåtkomst." + } + } + } + }, + "notification": { + "title": "Aviseringar", + "notificationSettings": { + "title": "Aviseringsinställningar", + "desc": "Frigate kan skicka push-notiser till din enhet när den körs i webbläsare eller installerad som PWA." + }, + "globalSettings": { + "title": "Övergripande inställningar", + "desc": "Stäng tillfälligt av aviseringar för specifika kameror på alla registrerade enheter." + }, + "email": { + "title": "E-post", + "placeholder": "t.ex. exempel@epost.se", + "desc": "En giltig e-postadress krävs och kommer att användas för att meddela dig om det uppstår problem med push-tjänsten." + }, + "cameras": { + "title": "Kameror", + "noCameras": "Inga kameror tillgängliga", + "desc": "Välj vilka kameror som notifikationer ska aktiveras för." + }, + "unregisterDevice": "Avregistrera enheten", + "sendTestNotification": "Skicka testnotis", + "active": "Aviseringar är aktiva", + "notificationUnavailable": { + "title": "Meddelanden otillgängliga", + "desc": "Webb push-meddelanden kräver en säker kontext (https://…). Detta är en begränsning i webbläsaren. Få säker åtkomst till Frigate för att använda meddelanden." + }, + "deviceSpecific": "Enhetsspecifika inställningar", + "registerDevice": "Registrera den här enheten", + "unsavedRegistrations": "Osparade aviseringsregistreringar", + "unsavedChanges": "Osparade ändringar till aviseringar", + "suspended": "Aviseringar avstängda {{time}}", + "suspendTime": { + "suspend": "Pausa", + "5minutes": "Pausa i 5 minuter", + "10minutes": "Pausa i 10 minuter", + "30minutes": "Pausa i 30 minuter", + "1hour": "Pausa i 1 timme", + "12hours": "Pausa i 12 timmar", + "24hours": "Pausa i 24 timmar", + "untilRestart": "Pausa tills omstart" + }, + "cancelSuspension": "Avbryt pausning", + "toast": { + "success": { + "registered": "Registreringen för aviseringar har lyckats. Omstart av Frigate krävs innan några aviseringar (inklusive en testavisering) kan skickas.", + "settingSaved": "Aviseringsinställningarna har sparats." + }, + "error": { + "registerFailed": "Det gick inte att spara aviseringsregistreringen." + } + } + }, + "roles": { + "addRole": "Lägg till roll", + "table": { + "role": "Roll", + "cameras": "Kameror", + "noRoles": "Inga anpassade roller hittades.", + "editCameras": "Redigera kameror", + "deleteRole": "Radera roll", + "actions": "Åtgärder" + }, + "toast": { + "success": { + "createRole": "Roll {{role}} skapad", + "updateCameras": "Kameror uppdaterade för roll {{role}}", + "deleteRole": "Roll {{role}} raderad", + "userRolesUpdated_one": "{{count}} användare som tilldelats den här rollen har uppdaterats till 'tittare', vilket har åtkomst till alla kameror.", + "userRolesUpdated_other": "{{count}} användare som tilldelats den här rollen har uppdaterats till 'tittare', vilket har åtkomst till alla kameror." + }, + "error": { + "createRoleFailed": "Misslyckades att skapa roll: {{errorMessage}}", + "updateCamerasFailed": "Misslyckades att uppdatera kameror: {{errorMessage}}", + "deleteRoleFailed": "Misslyckades att radera roll: {{errorMessage}}", + "userUpdateFailed": "Misslyckades att uppdatera användar-roller: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Skapa ny roll", + "desc": "Skapa en ny roll och ange behörigheter för kameraåtkomst." + }, + "deleteRole": { + "title": "Radera roll", + "deleting": "Raderar...", + "desc": "Den här åtgärden kan inte ångras. Detta kommer att ta bort rollen permanent och tilldela alla användare med rollen 'tittare', vilket ger tittaren åtkomst till alla kameror.", + "warn": "Är du säker på att du vill ta bort {{role}}?" + }, + "form": { + "role": { + "placeholder": "Ange rollens namn", + "desc": "Enbart bokstäver, siffror, punkter och understreck tillåtna.", + "roleIsRequired": "Rollens namn krävs", + "roleExists": "En roll med detta namn finns redan.", + "title": "Rollnamn", + "roleOnlyInclude": "Rollnamnet får endast innehålla bokstäver, siffror, . eller _" + }, + "cameras": { + "title": "Kameror", + "required": "Minst en kamera måste väljas.", + "desc": "Välj kameror som den här rollen har åtkomst till. Minst en kamera krävs." + } + }, + "editCameras": { + "title": "Redigera rollkameror", + "desc": "Uppdatera kameraåtkomst för rollen {{role}}." + } + }, + "management": { + "title": "Hantering av tittarroller", + "desc": "Hantera anpassade tittarroller och deras kameraåtkomstbehörigheter för den här Frigate-instansen." + } + }, + "frigatePlus": { + "title": "Frigate+ Inställningar", + "apiKey": { + "notValidated": "Frigate+ API-nyckeln upptäcktes inte eller validerades inte", + "desc": "Frigate+ API-nyckeln möjliggör integration med Frigate+-tjänsten.", + "plusLink": "Läs mer om Frigate+", + "title": "Frigate+ API-nyckel", + "validated": "Frigate+ API-nyckeln har upptäckts och validerats" + }, + "snapshotConfig": { + "title": "Ögonblicksbild konfiguration", + "desc": "För att skicka till Frigate+ måste ögonblicksbilder vara aktiverade i konfigurationen.", + "cleanCopyWarning": "Vissa kameror har ögonblicksbilder inaktiverade", + "table": { + "camera": "Kamera", + "snapshots": "Ögonblicksbilder", + "cleanCopySnapshots": "clean_copy Ögonblicksbilder" + } + }, + "modelInfo": { + "title": "Modellinformation", + "modelType": "Modelltyp", + "trainDate": "Träningsdatum", + "baseModel": "Basmodell", + "plusModelType": { + "baseModel": "Basmodell", + "userModel": "Finjusterad" + }, + "supportedDetectors": "Detektorer som stöds", + "cameras": "Kameror", + "loading": "Laddar modellinformation…", + "error": "Misslyckades med att ladda modellinformationen", + "availableModels": "Tillgängliga modeller", + "loadingAvailableModels": "Laddar tillgängliga modeller…", + "modelSelect": "Dina tillgängliga modeller på Frigate+ kan väljas här. Observera att endast modeller som är kompatibla med din nuvarande detektorkonfiguration kan väljas." + }, + "unsavedChanges": "Osparade ändringar av inställningar för Frigate+", + "restart_required": "Omstart krävs (Frigate+ modell ändrad)", + "toast": { + "success": "Inställningarna för Frigate+ har sparats. Starta om Frigate för att tillämpa ändringarna.", + "error": "Kunde inte spara konfigurationsändringarna: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Utlösare", + "management": { + "title": "Utlösare", + "desc": "Hantera utlösare för {{camera}}. Använd miniatyrtypen för att utlösa liknande miniatyrer som ditt valda spårade objekt och beskrivningstypen för att utlösa liknande beskrivningar av text du anger." + }, + "addTrigger": "Lägg till utlösare", + "table": { + "name": "Namn", + "type": "Typ", + "content": "Innehåll", + "threshold": "Tröskel", + "actions": "Åtgärder", + "noTriggers": "Inga utlösare konfigurerade för den här kameran.", + "edit": "Redigera", + "deleteTrigger": "Ta bort utlösare", + "lastTriggered": "Senast utlöst" + }, + "type": { + "thumbnail": "Miniatyrbild", + "description": "Beskrivning" + }, + "actions": { + "notification": "Skicka avisering", + "alert": "Markera som Varning", + "sub_label": "Lägg till underetikett", + "attribute": "Lägg till attribut" + }, + "dialog": { + "createTrigger": { + "title": "Skapa utlösare", + "desc": "Skapa en utlösare för kamera {{camera}}" + }, + "editTrigger": { + "title": "Redigera utlösare", + "desc": "Redigera inställningarna för utlösare på kameran {{camera}}" + }, + "deleteTrigger": { + "title": "Ta bort utlösare", + "desc": "Är du säker på att du vill ta bort utlösaren {{triggerName}}? Den här åtgärden kan inte ångras." + }, + "form": { + "name": { + "title": "Namn", + "placeholder": "Namnge denna utlösare", + "error": { + "minLength": "Fältet måste vara minst 2 tecken långt.", + "invalidCharacters": "Fältet får bara innehålla bokstäver, siffror, understreck och bindestreck.", + "alreadyExists": "En utlösare med detta namn finns redan för den här kameran." + }, + "description": "Ange ett unikt namn eller en unik beskrivning för att identifiera den här utlösaren" + }, + "enabled": { + "description": "Aktivera eller inaktivera den här utlösaren" + }, + "type": { + "title": "Typ", + "placeholder": "Välj utlösartyp", + "description": "Utlöses när en liknande beskrivning av spårat objekt detekteras", + "thumbnail": "Utlöses när en liknande miniatyrbild av ett spårat objekt upptäcks" + }, + "content": { + "title": "Innehåll", + "imagePlaceholder": "Välj en miniatyrbild", + "textPlaceholder": "Ange textinnehåll", + "imageDesc": "Endast de senaste 100 miniatyrerna visas. Om du inte hittar önskad miniatyr kan du granska tidigare objekt i Utforska och skapa en utlösare från menyn där.", + "textDesc": "Ange text för att utlösa den här åtgärden när en liknande beskrivning av spårat objekt upptäcks.", + "error": { + "required": "Innehåll krävs." + } + }, + "threshold": { + "title": "Tröskel", + "error": { + "min": "Tröskelvärdet måste vara minst 0", + "max": "Tröskelvärdet får vara högst 1" + }, + "desc": "Ställ in likhetströskeln för denna utlösare. En högre tröskel innebär att en bättre matchning krävs för att utlösaren ska aktiveras." + }, + "actions": { + "title": "Åtgärder", + "desc": "Som standard utlöser Frigate ett MQTT-meddelande för alla utlösare. Underetiketter lägger till utlösarnamnet till objektetiketten. Attribut är sökbara metadata som lagras separat i de spårade objektmetadata.", + "error": { + "min": "Minst en åtgärd måste väljas." + } + }, + "friendly_name": { + "title": "Vänligt namn", + "placeholder": "Namnge eller beskriv denna utlösare", + "description": "Ett valfritt vänligt namn eller en beskrivande text för denna utlösare." + } + } + }, + "toast": { + "success": { + "createTrigger": "Utlösaren {{name}} har skapats.", + "updateTrigger": "Utlösaren {{name}} har uppdaterats.", + "deleteTrigger": "Utlösaren {{name}} har raderats." + }, + "error": { + "createTriggerFailed": "Misslyckades med att skapa utlösaren: {{errorMessage}}", + "updateTriggerFailed": "Misslyckades med att uppdatera utlösaren: {{errorMessage}}", + "deleteTriggerFailed": "Misslyckades med att ta bort utlösaren: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Semantisk sökning är inaktiverad", + "desc": "Semantisk sökning måste vara aktiverad för att använda Utlösare." + }, + "wizard": { + "title": "Skapa utlösare", + "step1": { + "description": "Konfigurera grundinställningarna för din trigger." + }, + "step2": { + "description": "Ställ in innehållet som ska utlösa den här åtgärden." + }, + "step3": { + "description": "Konfigurera tröskelvärdet och åtgärderna för den här utlösaren." + }, + "steps": { + "nameAndType": "Namn och typ", + "configureData": "Konfigurera data", + "thresholdAndActions": "Tröskelvärde och åtgärder" + } + } + }, + "cameraWizard": { + "title": "Lägg till kamera", + "description": "Följ stegen nedan för att lägga till en ny kamera i din Frigate-installation.", + "steps": { + "nameAndConnection": "Namn och anslutning", + "streamConfiguration": "Strömkonfiguration", + "validationAndTesting": "Validering och testning", + "probeOrSnapshot": "Prob eller ögonblicksbild" + }, + "save": { + "success": "Ny kamera {{cameraName}} har sparats.", + "failure": "Fel vid sparning av {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Upplösning", + "video": "Video", + "audio": "Ljud", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Ange en giltig strömnings länk", + "testFailed": "Strömtest misslyckades: {{error}}" + }, + "step1": { + "description": "Ange dina kamerauppgifter och välj att undersöka kameran eller manuellt välja märke.", + "cameraName": "Kameranamn", + "cameraNamePlaceholder": "t.ex. ytterdörr eller Översikt över bakgård", + "host": "Värd-/IP-adress", + "port": "Portnummer", + "username": "Användarnamn", + "usernamePlaceholder": "Frivillig", + "password": "Lösenord", + "passwordPlaceholder": "Frivillig", + "selectTransport": "Välj transportprotokoll", + "cameraBrand": "Kameramärke", + "selectBrand": "Välj kameramärke för URL-mall", + "customUrl": "Anpassad ström länk", + "brandInformation": "Varumärkesinformation", + "brandUrlFormat": "För kameror med RTSP URL-formatet: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://användarnamn:passord@värd:port/text", + "testConnection": "Testa anslutning", + "testSuccess": "Anslutningstestet lyckades!", + "testFailed": "Anslutningstestet misslyckades. Kontrollera dina indata och försök igen.", + "streamDetails": "Streamdetaljer", + "warnings": { + "noSnapshot": "Det gick inte att hämta en ögonblicksbild från den konfigurerade strömmen." + }, + "errors": { + "brandOrCustomUrlRequired": "Välj antingen ett kameramärke med värd/IP eller välj \"Annat\" med en anpassad URL", + "nameRequired": "Kameranamn krävs", + "nameLength": "Kameranamnet måste vara högst 64 tecken långt", + "invalidCharacters": "Kameranamnet innehåller ogiltiga tecken", + "nameExists": "Kameranamnet finns redan", + "brands": { + "reolink-rtsp": "Reolink RTSP rekommenderas inte. Aktivera HTTP i kamerans firmwareinställningar och starta om guiden." + }, + "customUrlRtspRequired": "Anpassade webbadresser måste börja med \"rtsp://\". Manuell konfiguration krävs för kameraströmmar som inte använder RTSP." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Undersöker kamerans metadata...", + "fetchingSnapshot": "Hämtar kamerabild..." + }, + "connectionSettings": "Anslutningsinställningar", + "detectionMethod": "Strömdetekteringsmetod", + "onvifPort": "ONVIF-port", + "probeMode": "Undersök kameran", + "manualMode": "Manuellt val", + "detectionMethodDescription": "Undersök kameran med ONVIF (om det stöds) för att hitta kameraströms-URL:er, eller välj kameramärke manuellt för att använda fördefinierade URL:er. För att ange en anpassad RTSP-URL, välj den manuella metoden och välj \"Annat\".", + "onvifPortDescription": "För kameror som stöder ONVIF är detta vanligtvis 80 eller 8080.", + "useDigestAuth": "Använd digest-autentisering", + "useDigestAuthDescription": "Använd HTTP-sammanfattningsautentisering för ONVIF. Vissa kameror kan kräva ett dedikerat ONVIF-användarnamn/lösenord istället för standardadministratörsanvändaren." + }, + "step2": { + "description": "Undersök kameran efter tillgängliga strömmar eller konfigurera manuella inställningar baserat på din valda detekteringsmetod.", + "streamsTitle": "Kameraströmmar", + "addStream": "Lägg till ström", + "addAnotherStream": "Lägg till ytterligare en ström", + "streamTitle": "Ström {{number}}", + "streamUrl": "Ström URL", + "streamUrlPlaceholder": "rtsp://användarnamn:lösenord@värd:portnummer/plats", + "url": "URL", + "resolution": "Upplösning", + "selectResolution": "Välj upplösning", + "quality": "Kvalitet", + "selectQuality": "Välj kvalitet", + "roles": "Roller", + "roleLabels": { + "detect": "Objektdetektering", + "record": "Inspelning", + "audio": "Ljud" + }, + "testStream": "Testa anslutning", + "testSuccess": "Anslutningstestet lyckades!", + "testFailed": "Anslutningstestet misslyckades. Kontrollera dina indata och försök igen.", + "testFailedTitle": "Testet misslyckades", + "connected": "Ansluten", + "notConnected": "Inte ansluten", + "featuresTitle": "Funktioner", + "go2rtc": "Minska anslutningar till kameran", + "detectRoleWarning": "Minst en ström måste ha rollen \"upptäcka\" för att fortsätta.", + "rolesPopover": { + "title": "Ström-roller", + "detect": "Huvud video ström för objektdetektering.", + "record": "Sparar segment av videoflödet baserat på konfigurationsinställningar.", + "audio": "Flöde för ljudbaserad detektering." + }, + "featuresPopover": { + "title": "Strömfunktioner", + "description": "Använd go2rtc-omströmning för att minska anslutningar till din kamera." + }, + "streamDetails": "Streamdetaljer", + "probing": "Undersöker kameran...", + "retry": "Försöka igen", + "testing": { + "probingMetadata": "Undersöker kamerans metadata...", + "fetchingSnapshot": "Hämtar kamerabild..." + }, + "probeFailed": "Misslyckades med att undersöka kameran: {{error}}", + "probingDevice": "Undersöker enheten...", + "probeSuccessful": "Kontroll lyckades", + "probeError": "Kontroll fel", + "probeNoSuccess": "Kontroll misslyckades", + "deviceInfo": "Enhetsinformation", + "manufacturer": "Tillverkare", + "model": "Modell", + "firmware": "Inbyggd programvara", + "profiles": "Profiler", + "ptzSupport": "PTZ-stöd", + "autotrackingSupport": "Stöd för Autospårning", + "presets": "Förinställningar", + "rtspCandidates": "RTSP-kandidater", + "rtspCandidatesDescription": "Följande RTSP-URL:er hittades från kamera kontrollen. Testa anslutningen för att visa strömmetadata.", + "noRtspCandidates": "Inga RTSP-URL:er hittades från kameran. Dina inloggningsuppgifter kan vara felaktiga, eller så kanske kameran inte stöder ONVIF eller metoden som används för att hämta RTSP-URL:er. Gå tillbaka och ange RTSP-URL:en manuellt.", + "candidateStreamTitle": "Kandidat {{number}}", + "useCandidate": "Använda", + "uriCopy": "Kopiera", + "uriCopied": "URI kopierad till urklipp", + "testConnection": "Testa anslutning", + "toggleUriView": "Klicka för att växla mellan fullständig URI-vy", + "errors": { + "hostRequired": "Värd-/IP-adress krävs" + } + }, + "step3": { + "description": "Konfigurera strömningsroller och lägg till ytterligare strömmar för din kamera.", + "validationTitle": "Strömvalidering", + "connectAllStreams": "Anslut alla strömmar", + "reconnectionSuccess": "Återanslutningen lyckades.", + "reconnectionPartial": "Vissa strömmar kunde inte återanslutas.", + "streamUnavailable": "Förhandsgranskning av strömmen är inte tillgänglig", + "reload": "Ladda om", + "connecting": "Ansluter...", + "streamTitle": "Ström {{number}}", + "valid": "Giltig", + "failed": "Misslyckades", + "notTested": "Inte testad", + "connectStream": "Ansluta", + "connectingStream": "Ansluter", + "disconnectStream": "Koppla från", + "estimatedBandwidth": "Uppskattad bandbredd", + "roles": "Roller", + "none": "Ingen", + "error": "Fel", + "streamValidated": "Ström {{number}} har validerats", + "streamValidationFailed": "Validering av ström {{number}} misslyckades", + "saveAndApply": "Spara ny kamera", + "saveError": "Ogiltig konfiguration. Kontrollera dina inställningar.", + "issues": { + "title": "Strömvalidering", + "videoCodecGood": "Videokodeken är {{codec}}.", + "audioCodecGood": "Ljudkodeken är {{codec}}.", + "noAudioWarning": "Inget ljud upptäcktes för den här strömmen, inspelningarna kommer inte att ha något ljud.", + "audioCodecRecordError": "AAC-ljudkodeken krävs för att stödja ljud i inspelningar.", + "audioCodecRequired": "En ljudström krävs för att stödja ljuddetektering.", + "restreamingWarning": "Att minska anslutningarna till kameran för inspelningsströmmen kan öka CPU-användningen något.", + "dahua": { + "substreamWarning": "Delström 1 är låst till en låg upplösning. Många Dahua / Amcrest / EmpireTech kameror stöder ytterligare delströmmar som måste aktiveras i kamerans inställningar. Det rekommenderas att kontrollera och använda dessa strömmar om de är tillgängliga." + }, + "hikvision": { + "substreamWarning": "Delström 1 är låst till en låg upplösning. Många Hikvision kameror stöder ytterligare delströmmar som måste aktiveras i kamerans inställningar. Det rekommenderas att kontrollera och använda dessa strömmar om de är tillgängliga." + }, + "resolutionHigh": "En upplösning på {{resolution}} kan orsaka ökad resursanvändning.", + "resolutionLow": "En upplösning på {{resolution}} kan vara för låg för tillförlitlig detektering av små objekt." + }, + "ffmpegModule": "Använd läge för strömkompatibilitet", + "ffmpegModuleDescription": "Om strömmen inte läses in efter flera försök, prova att aktivera detta. När det är aktiverat kommer Frigate att använda ffmpeg-modulen med go2rtc. Detta kan ge bättre kompatibilitet med vissa kameraströmmar.", + "streamsTitle": "Kameraströmmar", + "addStream": "Lägg till ström", + "addAnotherStream": "Lägg till ytterligare en ström", + "streamUrl": "Ström-URL", + "streamUrlPlaceholder": "rtsp://användarnamn:lösenord@värd:portnummer/plats", + "selectStream": "Välj en ström", + "searchCandidates": "Sök kandidater...", + "noStreamFound": "Ingen ström hittades", + "url": "URL", + "resolution": "Upplösning", + "selectResolution": "Välj upplösning", + "quality": "Kvalitet", + "selectQuality": "Välj kvalitet", + "roleLabels": { + "detect": "Objektdetektering", + "record": "Inspelning", + "audio": "Ljud" + }, + "testStream": "Testa anslutning", + "testSuccess": "Streamtestet lyckades!", + "testFailed": "Strömtestet misslyckades", + "testFailedTitle": "Testet misslyckades", + "connected": "Ansluten", + "notConnected": "Inte ansluten", + "featuresTitle": "Funktioner", + "go2rtc": "Minska anslutningar till kameran", + "detectRoleWarning": "Minst en ström måste ha rollen \"upptäck\" för att fortsätta.", + "rolesPopover": { + "title": "Stream-roller", + "detect": "Huvud kamera flöde för objektdetektering.", + "record": "Sparar segment av videoflödet baserat på konfigurationsinställningar.", + "audio": "Flöde för ljudbaserad detektering." + }, + "featuresPopover": { + "title": "Streamfunktioner", + "description": "Använd go2rtc-omströmning för att minska anslutningar till din kamera." + } + }, + "step4": { + "description": "Slutgiltig validering och analys innan du sparar din nya kamera. Anslut varje ström innan du sparar.", + "validationTitle": "Ström validering", + "connectAllStreams": "Anslut alla strömmar", + "reconnectionSuccess": "Återanslutningen lyckades.", + "reconnectionPartial": "Vissa strömmar kunde inte återanslutas.", + "streamUnavailable": "Förhandsgranskning av strömmen är inte tillgänglig", + "reload": "Ladda om", + "connecting": "Ansluter...", + "streamTitle": "Ström {{number}}", + "valid": "Giltig", + "failed": "Misslyckades", + "notTested": "Inte testad", + "connectStream": "Ansluta", + "connectingStream": "Ansluter", + "disconnectStream": "Koppla från", + "estimatedBandwidth": "Uppskattad bandbredd", + "roles": "Roller", + "ffmpegModule": "Använd strömkompatibilitetsläge", + "ffmpegModuleDescription": "Om strömmen inte laddas efter flera försök, försök att aktivera detta. När det är aktiverat kommer Frigate att använda ffmpeg-modulen med go2rtc. Detta kan ge bättre kompatibilitet med vissa kameraströmmar.", + "none": "Ingen", + "error": "Fel", + "streamValidated": "Ström {{number}} har validerats", + "streamValidationFailed": "Validering av ström {{number}} misslyckades", + "saveAndApply": "Spara ny kamera", + "saveError": "Ogiltig konfiguration. Kontrollera dina inställningar.", + "issues": { + "title": "Ström validering", + "videoCodecGood": "Videokodeken är {{codec}}.", + "audioCodecGood": "Ljudkodeken är {{codec}}.", + "resolutionHigh": "En upplösning på {{resolution}} kan orsaka ökad resursanvändning.", + "resolutionLow": "En upplösning på {{resolution}} kan vara för låg för tillförlitlig detektering av små objekt.", + "noAudioWarning": "Inget ljud upptäcktes för den här strömmen, inspelningarna kommer inte att ha ljud.", + "audioCodecRecordError": "AAC-ljudkodeken krävs för att stödja ljud i inspelningar.", + "audioCodecRequired": "En ljudström krävs för att stödja ljuddetektering.", + "restreamingWarning": "Att minska anslutningarna till kameran för inspelningsströmmen kan öka CPU-användningen något.", + "brands": { + "reolink-rtsp": "Reolink RTSP rekommenderas inte. Aktivera HTTP i kamerans firmwareinställningar och starta om guiden.", + "reolink-http": "Reolink HTTP-strömmar bör använda FFmpeg för bättre kompatibilitet. Aktivera \"Använd strömkompatibilitetsläge\" för den här strömmen." + }, + "dahua": { + "substreamWarning": "Delström 1 är låst till en låg upplösning. Många Dahua/Amcrest/EmpireTech-kameror stöder ytterligare delströmmar som måste aktiveras i kamerans inställningar. Det rekommenderas att kontrollera och använda dessa strömmar om de är tillgängliga." + }, + "hikvision": { + "substreamWarning": "Delström 1 är låst till en låg upplösning. Många Hikvision-kameror stöder ytterligare delströmmar som måste aktiveras i kamerans inställningar. Det rekommenderas att kontrollera och använda dessa strömmar om de är tillgängliga." + } + } + } + }, + "cameraManagement": { + "title": "Hantera kameror", + "addCamera": "Lägg till ny kamera", + "editCamera": "Redigera kamera:", + "selectCamera": "Välj en kamera", + "backToSettings": "Tillbaka till kamerainställningar", + "streams": { + "title": "Aktivera/avaktivera kameror", + "desc": "Inaktivera tillfälligt en kamera tills Frigate startar om. Om du inaktiverar en kamera helt stoppas Frigates bearbetning av kamerans strömmar. Detektering, inspelning och felsökning kommer inte att vara tillgängliga.
    Obs! Detta inaktiverar inte go2rtc-återströmmar." + }, + "cameraConfig": { + "add": "Lägg till kamera", + "edit": "Redigera kamera", + "description": "Konfigurera kamerainställningar inklusive strömingångar och roller.", + "name": "Kameranamn", + "nameRequired": "Kameranamn krävs", + "nameLength": "Kameranamnet måste vara kortare än 64 tecken.", + "namePlaceholder": "t.ex. ytterdörr eller Översikt över bakgård", + "enabled": "Aktiverad", + "ffmpeg": { + "inputs": "Ingångsströmmar", + "path": "Strömväg", + "pathRequired": "Strömväg krävs", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "Minst en roll krävs", + "rolesUnique": "Varje roll (ljud, detektering, inspelning) kan bara tilldelas en ström", + "addInput": "Lägg till inmatningsström", + "removeInput": "Ta bort inmatningsström", + "inputsRequired": "Minst en indataström krävs" + }, + "go2rtcStreams": "go2rtc-strömmar", + "streamUrls": "Ström-URL:er", + "addUrl": "Lägg till URL", + "addGo2rtcStream": "Lägg till go2rtc-ström", + "toast": { + "success": "Kamera {{cameraName}} sparades" + } + } + }, + "cameraReview": { + "title": "Inställningar för kameragranskning", + "object_descriptions": { + "title": "Generativa AI-objektbeskrivningar", + "desc": "Aktivera/inaktivera generativa AI-objektbeskrivningar temporärt för den här kameran tills Frigate startas om. Vid inaktivering kommer AI-genererade beskrivningar inte att begäras för spårade objekt på den här kameran." + }, + "review_descriptions": { + "title": "Beskrivningar av generativa AI-granskningar", + "desc": "Aktivera/inaktivera generativa AI-granskningsbeskrivningar för den här kameran temporärt tills Frigate startas om. Vid inaktivering kommer AI-genererade beskrivningar inte att begäras för granskningsobjekt på den här kameran." + }, + "review": { + "title": "Granska", + "desc": "Tillfälligt aktivera/avaktivera varningar och detekteringar för den här kameran tills Frigate startar om. När den är avaktiverad genereras inga nya granskningsobjekt. ", + "alerts": "Aviseringar ", + "detections": "Detektioner " + }, + "reviewClassification": { + "title": "Granska klassificering", + "desc": "Frigate kategoriserar granskningsobjekt som Varningar och Detekteringar. Som standard betraktas alla person- och bil-objekt som Varningar. Du kan förfina kategoriseringen av dina granskningsobjekt genom att konfigurera obligatoriska zoner för dem.", + "noDefinedZones": "Inga zoner är definierade för den här kameran.", + "objectAlertsTips": "Alla {{alertsLabels}}-objekt på {{cameraName}} kommer att visas som Varningar.", + "zoneObjectAlertsTips": "Alla {{alertsLabels}} objekt som upptäcks i {{zone}} på {{cameraName}} kommer att visas som Varningar.", + "objectDetectionsTips": "Alla {{detectionsLabels}}-objekt som inte kategoriseras på {{cameraName}} visas som detekteringar oavsett vilken zon de befinner sig i.", + "zoneObjectDetectionsTips": { + "text": "Alla {{detectionsLabels}}-objekt som inte kategoriseras i {{zone}} på {{cameraName}} kommer att visas som Detektioner.", + "notSelectDetections": "Alla {{detectionsLabels}} objekt som upptäckts i {{zone}} på {{cameraName}} och som inte kategoriserats som Varningar kommer att visas som Detekteringar oavsett vilken zon de befinner sig i.", + "regardlessOfZoneObjectDetectionsTips": "Alla {{detectionsLabels}}-objekt som inte kategoriseras på {{cameraName}} kommer att visas som Detektioner oavsett vilken zon de befinner sig i." + }, + "unsavedChanges": "Osparade inställningar för granskningsklassificering för {{camera}}", + "selectAlertsZones": "Välj zoner för Varningar", + "selectDetectionsZones": "Välj zoner för Detektioner", + "limitDetections": "Begränsa detektioner till specifika zoner", + "toast": { + "success": "Konfigurationen för granskning av klassificering har sparats. Starta om Frigate för att tillämpa ändringarna." + } + } + } +} diff --git a/web/public/locales/sv/views/system.json b/web/public/locales/sv/views/system.json new file mode 100644 index 0000000..8a5320c --- /dev/null +++ b/web/public/locales/sv/views/system.json @@ -0,0 +1,208 @@ +{ + "documentTitle": { + "storage": "Lagringsstatistik - Frigate", + "general": "Allmän statistik - Frigate", + "cameras": "Kamerastatistik - Frigate", + "logs": { + "frigate": "Frigate-loggar - Frigate", + "go2rtc": "Go2RTC loggar - Frigate", + "nginx": "Nginx loggar - Frigate" + }, + "enrichments": "Förbättringsstatistik - Frigate" + }, + "logs": { + "copy": { + "label": "Kopiera till urklipp", + "success": "Kopierat loggarna till utklippstavlan", + "error": "Kunde inte kopiera loggarna till utklippstavlan" + }, + "download": { + "label": "Ladda ned logg" + }, + "type": { + "label": "Typ", + "timestamp": "Tidsstämpel", + "message": "Meddelande", + "tag": "Tagg" + }, + "tips": "Loggarna strömmas från Server", + "toast": { + "error": { + "fetchingLogsFailed": "Fel vid hämtning av loggar: {{errorMessage}}", + "whileStreamingLogs": "Fel vid uppspelning av loggar: {{errorMessage}}" + } + } + }, + "title": "System", + "metrics": "Systemdetaljer", + "general": { + "title": "Generellt", + "detector": { + "title": "Detektorer", + "inferenceSpeed": "Detektorns inferenstid", + "temperature": "Detektor temperatur", + "cpuUsage": "Detektorns CPU-användning", + "memoryUsage": "Detektorns minnesanvändning", + "cpuUsageInformation": "CPU som används för att förbereda in- och utdata till/från detekteringsmodeller. Detta värde mäter inte inferensanvändning, även om en GPU eller accelerator används." + }, + "hardwareInfo": { + "title": "Hårdvaruinformation", + "gpuUsage": "GPU-användning", + "gpuMemory": "GPU-minne", + "gpuEncoder": "GPU-kodare", + "gpuDecoder": "GPU-avkodare", + "gpuInfo": { + "nvidiaSMIOutput": { + "vbios": "VBios-information: {{vbios}}", + "title": "Nvidia SMI utdata", + "name": "Namn: {{name}}", + "driver": "Drivrutin: {{driver}}", + "cudaComputerCapability": "CUDA beräknings kapacitet: {{cuda_compute}}" + }, + "closeInfo": { + "label": "Stäng GPU-info" + }, + "copyInfo": { + "label": "Kopiera GPU-info" + }, + "toast": { + "success": "Kopierade GPU-info till urklipp" + }, + "vainfoOutput": { + "title": "Vainfo resultat", + "returnCode": "Returkod: {{code}}", + "processOutput": "Bearbeta utdata:", + "processError": "Processfel:" + } + }, + "npuUsage": "NPU-användning", + "npuMemory": "NPU-minne", + "intelGpuWarning": { + "title": "Intel GPU statistik varning", + "message": "GPU statistik otillgänglig", + "description": "Detta är en känd bugg i Intels GPU-statistikrapporteringsverktyg (intel_gpu_top) där den slutar fungera och upprepade gånger returnerar en GPU-användning på 0 %, även i fall där hårdvaruacceleration och objektdetektering körs korrekt på (i)GPU:n. Detta är inte en Frigate-bugg. Du kan starta om värden för att tillfälligt åtgärda problemet och bekräfta att GPU:n fungerar korrekt. Detta påverkar inte prestandan." + } + }, + "otherProcesses": { + "title": "Övriga processer", + "processCpuUsage": "Process CPU-användning", + "processMemoryUsage": "Processminnesanvändning", + "series": { + "go2rtc": "go2rtc", + "recording": "inspelning", + "review_segment": "granskningssegment", + "embeddings": "inbäddningar", + "audio_detector": "ljuddetektor" + } + } + }, + "storage": { + "cameraStorage": { + "storageUsed": "Lagring", + "percentageOfTotalUsed": "Procentandel av totalt", + "bandwidth": "Bandbredd", + "unused": { + "title": "Oanvänt", + "tips": "Det här värdet kanske inte korrekt representerar det lediga utrymmet tillgängligt för Frigate om du har andra filer lagrade på din hårddisk utöver Frigates inspelningar. Frigate spårar inte lagringsanvändning utanför sina egna inspelningar." + }, + "title": "Kamera lagring", + "camera": "Kamera", + "unusedStorageInformation": "Information om oanvänd lagring" + }, + "title": "Lagring", + "overview": "Översikt", + "recordings": { + "title": "Inspelningar", + "tips": "Detta värde representerar den totala lagringsmängden som används av inspelningarna i Frigates databas. Frigate spårar inte lagringsanvändningen för alla filer på din disk.", + "earliestRecording": "Tidigast tillgängliga inspelning:" + }, + "shm": { + "title": "SHM-allokering (delat minne)", + "warning": "Den nuvarande SHM-storleken på {{total}}MB är för liten. Öka den till minst {{min_shm}}MB." + } + }, + "cameras": { + "title": "Kameror", + "overview": "Översikt", + "info": { + "aspectRatio": "bildförhållande", + "cameraProbeInfo": "Kamerainformation för {{camera}}", + "streamDataFromFFPROBE": "Strömdata erhålls med ffprobe.", + "codec": "Codec:", + "resolution": "Upplösning:", + "fps": "FPS:", + "unknown": "Okänd", + "audio": "Ljud:", + "error": "Fel: {{error}}", + "tips": { + "title": "Kamera sond information" + }, + "fetching": "Hämtar kamera data", + "stream": "Ström {{idx}}", + "video": "Video:" + }, + "label": { + "detect": "detektera", + "camera": "kamera", + "skipped": "hoppade över", + "ffmpeg": "FFmpeg", + "capture": "spela in", + "overallFramesPerSecond": "totalt antal bilder per sekund", + "overallDetectionsPerSecond": "totala detektioner per sekund", + "overallSkippedDetectionsPerSecond": "totalt antal hoppade detekteringar per sekund", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} inspelning", + "cameraDetect": "{{camName}} upptäcka", + "cameraFramesPerSecond": "{{camName}} bildrutor per sekund", + "cameraDetectionsPerSecond": "{{camName}} detekteringar per sekund", + "cameraSkippedDetectionsPerSecond": "{{camName}} hoppade över detekteringar per sekund" + }, + "framesAndDetections": "Ramar / Detektioner", + "toast": { + "success": { + "copyToClipboard": "Kopierade probdata till urklipp." + }, + "error": { + "unableToProbeCamera": "Kunde inte undersöka kameran: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Senast uppdaterad: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} har hög FFmpeg CPU-användning ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} har hög CPU-användning vid detektering ({{detectAvg}}%)", + "healthy": "Systemet är hälsosamt", + "reindexingEmbeddings": "Omindexering av inbäddningar ({{processed}}% klar)", + "cameraIsOffline": "{{camera}} är frånkopplad", + "detectIsSlow": "{{detect}} är långsam ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} är väldigt långsam ({{speed}} ms)", + "shmTooLow": "/dev/shm allokeringen ({{total}} MB) bör ökas till minst {{min}} MB." + }, + "enrichments": { + "title": "Berikningar", + "infPerSecond": "Slutsatser per sekund", + "embeddings": { + "image_embedding": "Bildinbäddning", + "text_embedding": "Textinbäddning", + "face_recognition": "Ansiktsigenkänning", + "plate_recognition": "Nummerplåt igenkänning", + "image_embedding_speed": "Bildinbäddningshastighet", + "face_embedding_speed": "Hastighet för ansiktsinbäddning", + "face_recognition_speed": "Ansiktsigenkänningshastighet", + "plate_recognition_speed": "Hastighet för igenkänning av nummerplåtar", + "text_embedding_speed": "Textinbäddningshastighet", + "yolov9_plate_detection_speed": "Detekteringshastighet för YOLOv9-registreringsskyltar", + "yolov9_plate_detection": "YOLOv9 nummerplåt detektering", + "review_description": "Recensionsbeskrivning", + "review_description_speed": "Recensionsbeskrivning Hastighet", + "review_description_events_per_second": "Recensionsbeskrivning", + "object_description": "Objekt beskrivning", + "object_description_speed": "Objekt beskrivning hastighet", + "object_description_events_per_second": "Objekt beskrivning", + "classification_events_per_second": "{{name}} Klassificering Händelser per sekund", + "classification": "{{name}} Klassificering", + "classification_speed": "{{name}} Klassificeringshastighet" + }, + "averageInf": "Genomsnittlig inferenstid" + } +} diff --git a/web/public/locales/te/audio.json b/web/public/locales/te/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/audio.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/common.json b/web/public/locales/te/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/common.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/components/auth.json b/web/public/locales/te/components/auth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/components/camera.json b/web/public/locales/te/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/components/dialog.json b/web/public/locales/te/components/dialog.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/components/filter.json b/web/public/locales/te/components/filter.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/components/icons.json b/web/public/locales/te/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/components/input.json b/web/public/locales/te/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/components/input.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/components/player.json b/web/public/locales/te/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/components/player.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/config/cameras.json b/web/public/locales/te/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/config/global.json b/web/public/locales/te/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/config/groups.json b/web/public/locales/te/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/config/validation.json b/web/public/locales/te/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/objects.json b/web/public/locales/te/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/objects.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/chat.json b/web/public/locales/te/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/classificationModel.json b/web/public/locales/te/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/configEditor.json b/web/public/locales/te/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/events.json b/web/public/locales/te/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/events.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/explore.json b/web/public/locales/te/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/exports.json b/web/public/locales/te/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/faceLibrary.json b/web/public/locales/te/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/live.json b/web/public/locales/te/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/motionSearch.json b/web/public/locales/te/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/recording.json b/web/public/locales/te/views/recording.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/replay.json b/web/public/locales/te/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/search.json b/web/public/locales/te/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/settings.json b/web/public/locales/te/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/te/views/system.json b/web/public/locales/te/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/te/views/system.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/th/audio.json b/web/public/locales/th/audio.json new file mode 100644 index 0000000..d6da104 --- /dev/null +++ b/web/public/locales/th/audio.json @@ -0,0 +1,429 @@ +{ + "speech": "พูด", + "yell": "ตะโกน", + "babbling": "พูดไม่ชัด", + "bellow": "ตะโกนเสียงต่ำ", + "whoop": "โห่ร้อง", + "singing": "ร้องเพลง", + "choir": "คณะนักร้องประสานเสียง", + "yodeling": "การร้องเพลงแบบโยเดิล", + "chant": "สวดมนต์", + "mantra": "มนต์", + "child_singing": "เด็กกำลังร้องเพลง", + "throat_clearing": "กระแอม", + "sneeze": "จาม", + "sniff": "สูดจมูก", + "run": "[ยังไม่แปล: Run]", + "shuffle": "[ยังไม่แปล: Shuffle]", + "footsteps": "[ยังไม่แปล: Footsteps]", + "biting": "[ยังไม่แปล: Biting]", + "gargling": "[ยังไม่แปล: Gargling]", + "stomach_rumble": "[ยังไม่แปล: Stomach Rumble]", + "burping": "[ยังไม่แปล: Burping]", + "hiccup": "[ยังไม่แปล: Hiccup]", + "heart_murmur": "[ยังไม่แปล: Heart Murmur]", + "cheering": "[ยังไม่แปล: Cheering]", + "applause": "[ยังไม่แปล: Applause]", + "chatter": "[ยังไม่แปล: Chatter]", + "crowd": "[ยังไม่แปล: Crowd]", + "children_playing": "[ยังไม่แปล: Children Playing]", + "animal": "สัตว์", + "pets": "[ยังไม่แปล: Pets]", + "dog": "สุนัข", + "bark": "เสียงหอบ", + "yip": "[ยังไม่แปล: Yip]", + "howl": "[ยังไม่แปล: Howl]", + "bow_wow": "[ยังไม่แปล: Bow Wow]", + "growling": "[ยังไม่แปล: Growling]", + "whimper_dog": "[ยังไม่แปล: Dog Whimper]", + "chirp": "[ยังไม่แปล: Chirp]", + "squawk": "[ยังไม่แปล: Squawk]", + "pigeon": "[ยังไม่แปล: Pigeon]", + "coo": "[ยังไม่แปล: Coo]", + "dogs": "[ยังไม่แปล: Dogs]", + "rats": "[ยังไม่แปล: Rats]", + "mouse": "เมาส์", + "patter": "[ยังไม่แปล: Patter]", + "insect": "[ยังไม่แปล: Insect]", + "cricket": "[ยังไม่แปล: Cricket]", + "frog": "[ยังไม่แปล: Frog]", + "croak": "[ยังไม่แปล: Croak]", + "snake": "[ยังไม่แปล: Snake]", + "snare_drum": "[ยังไม่แปล: Snare Drum]", + "rimshot": "[ยังไม่แปล: Rimshot]", + "trumpet": "[ยังไม่แปล: Trumpet]", + "trombone": "[ยังไม่แปล: Trombone]", + "bowed_string_instrument": "[ยังไม่แปล: Bowed String Instrument]", + "tuning_fork": "[ยังไม่แปล: Tuning Fork]", + "chime": "[ยังไม่แปล: Chime]", + "wind_chime": "[ยังไม่แปล: Wind Chime]", + "harmonica": "[ยังไม่แปล: Harmonica]", + "accordion": "[ยังไม่แปล: Accordion]", + "bagpipes": "[ยังไม่แปล: Bagpipes]", + "rock_music": "[ยังไม่แปล: Rock Music]", + "heavy_metal": "[ยังไม่แปล: Heavy Metal]", + "punk_rock": "[ยังไม่แปล: Punk Rock]", + "psychedelic_rock": "[ยังไม่แปล: Psychedelic Rock]", + "rhythm_and_blues": "[ยังไม่แปล: Rhythm and Blues]", + "soul_music": "[ยังไม่แปล: Soul Music]", + "reggae": "[ยังไม่แปล: Reggae]", + "country": "[ยังไม่แปล: Country]", + "swing_music": "[ยังไม่แปล: Swing Music]", + "bluegrass": "[ยังไม่แปล: Bluegrass]", + "funk": "[ยังไม่แปล: Funk]", + "folk_music": "[ยังไม่แปล: Folk Music]", + "middle_eastern_music": "[ยังไม่แปล: Middle Eastern Music]", + "jazz": "[ยังไม่แปล: Jazz]", + "disco": "[ยังไม่แปล: Disco]", + "independent_music": "[ยังไม่แปล: Independent Music]", + "song": "[ยังไม่แปล: Song]", + "jet_engine": "[ยังไม่แปล: Jet Engine]", + "propeller": "[ยังไม่แปล: Propeller]", + "helicopter": "[ยังไม่แปล: Helicopter]", + "fixed-wing_aircraft": "[ยังไม่แปล: Fixed-Wing Aircraft]", + "bicycle": "จักรยาน", + "accelerating": "[ยังไม่แปล: Accelerating]", + "door": "ประตู", + "slam": "[ยังไม่แปล: Slam]", + "computer_keyboard": "[ยังไม่แปล: Computer Keyboard]", + "radio": "[ยังไม่แปล: Radio]", + "field_recording": "[ยังไม่แปล: Field Recording]", + "scream": "[ยังไม่แปล: Scream]", + "whispering": "กระซิบ", + "laughter": "เสียงหัวเราะ", + "snicker": "หัวเราะเยาะ", + "crying": "ร้องไห้", + "sigh": "ถอนหายใจ", + "synthetic_singing": "การร้องเพลงสังเคราะห์", + "rapping": "การแร็พ", + "humming": "ฮัมเพลง", + "groan": "ครวญคราง", + "grunt": "เสียงคราง", + "whistling": "ผิวปาก", + "breathing": "การหายใจ", + "wheeze": "หายใจเสียงวี้ด", + "snoring": "กรน", + "gasp": "หอบ", + "pant": "หายใจหอบ", + "snort": "เสียงสูดจมูก", + "cough": "ไอ", + "chewing": "[ยังไม่แปล: Chewing]", + "fart": "[ยังไม่แปล: Fart]", + "hands": "[ยังไม่แปล: Hands]", + "finger_snapping": "[ยังไม่แปล: Finger Snapping]", + "clapping": "[ยังไม่แปล: Clapping]", + "heartbeat": "[ยังไม่แปล: Heartbeat]", + "cat": "แมว", + "purr": "[ยังไม่แปล: Purr]", + "meow": "[ยังไม่แปล: Meow]", + "hiss": "[ยังไม่แปล: Hiss]", + "caterwaul": "[ยังไม่แปล: Caterwaul]", + "livestock": "[ยังไม่แปล: Livestock]", + "horse": "ม้า", + "clip_clop": "[ยังไม่แปล: Clip Clop]", + "neigh": "[ยังไม่แปล: Neigh]", + "cattle": "[ยังไม่แปล: Cattle]", + "moo": "[ยังไม่แปล: Moo]", + "cowbell": "[ยังไม่แปล: Cowbell]", + "pig": "[ยังไม่แปล: Pig]", + "oink": "[ยังไม่แปล: Oink]", + "goat": "แพะ", + "bleat": "[ยังไม่แปล: Bleat]", + "sheep": "แกะ", + "fowl": "[ยังไม่แปล: Fowl]", + "chicken": "[ยังไม่แปล: Chicken]", + "cluck": "[ยังไม่แปล: Cluck]", + "cock_a_doodle_doo": "[ยังไม่แปล: Cock-a-Doodle-Doo]", + "turkey": "[ยังไม่แปล: Turkey]", + "gobble": "[ยังไม่แปล: Gobble]", + "duck": "[ยังไม่แปล: Duck]", + "quack": "[ยังไม่แปล: Quack]", + "goose": "[ยังไม่แปล: Goose]", + "honk": "[ยังไม่แปล: Honk]", + "wild_animals": "[ยังไม่แปล: Wild Animals]", + "roaring_cats": "[ยังไม่แปล: Roaring Cats]", + "roar": "[ยังไม่แปล: Roar]", + "bird": "นก", + "crow": "[ยังไม่แปล: Crow]", + "caw": "[ยังไม่แปล: Caw]", + "owl": "[ยังไม่แปล: Owl]", + "hoot": "[ยังไม่แปล: Hoot]", + "flapping_wings": "[ยังไม่แปล: Flapping Wings]", + "mosquito": "[ยังไม่แปล: Mosquito]", + "fly": "[ยังไม่แปล: Fly]", + "buzz": "[ยังไม่แปล: Buzz]", + "rattle": "[ยังไม่แปล: Rattle]", + "whale_vocalization": "[ยังไม่แปล: Whale Vocalization]", + "music": "[ยังไม่แปล: Music]", + "musical_instrument": "[ยังไม่แปล: Musical Instrument]", + "plucked_string_instrument": "[ยังไม่แปล: Plucked String Instrument]", + "guitar": "[ยังไม่แปล: Guitar]", + "electric_guitar": "[ยังไม่แปล: Electric Guitar]", + "bass_guitar": "[ยังไม่แปล: Bass Guitar]", + "acoustic_guitar": "[ยังไม่แปล: Acoustic Guitar]", + "steel_guitar": "[ยังไม่แปล: Steel Guitar]", + "tapping": "[ยังไม่แปล: Tapping]", + "strum": "[ยังไม่แปล: Strum]", + "banjo": "[ยังไม่แปล: Banjo]", + "sitar": "[ยังไม่แปล: Sitar]", + "mandolin": "[ยังไม่แปล: Mandolin]", + "zither": "[ยังไม่แปล: Zither]", + "ukulele": "[ยังไม่แปล: Ukulele]", + "keyboard": "คีย์บอร์ด", + "piano": "[ยังไม่แปล: Piano]", + "electric_piano": "[ยังไม่แปล: Electric Piano]", + "organ": "[ยังไม่แปล: Organ]", + "electronic_organ": "[ยังไม่แปล: Electronic Organ]", + "hammond_organ": "[ยังไม่แปล: Hammond Organ]", + "synthesizer": "[ยังไม่แปล: Synthesizer]", + "sampler": "[ยังไม่แปล: Sampler]", + "harpsichord": "[ยังไม่แปล: Harpsichord]", + "percussion": "[ยังไม่แปล: Percussion]", + "drum_kit": "[ยังไม่แปล: Drum Kit]", + "drum_machine": "[ยังไม่แปล: Drum Machine]", + "drum": "[ยังไม่แปล: Drum]", + "drum_roll": "[ยังไม่แปล: Drum Roll]", + "bass_drum": "[ยังไม่แปล: Bass Drum]", + "timpani": "[ยังไม่แปล: Timpani]", + "tabla": "[ยังไม่แปล: Tabla]", + "cymbal": "[ยังไม่แปล: Cymbal]", + "hi_hat": "[ยังไม่แปล: Hi-Hat]", + "wood_block": "[ยังไม่แปล: Wood Block]", + "tambourine": "[ยังไม่แปล: Tambourine]", + "maraca": "[ยังไม่แปล: Maraca]", + "gong": "[ยังไม่แปล: Gong]", + "tubular_bells": "[ยังไม่แปล: Tubular Bells]", + "mallet_percussion": "[ยังไม่แปล: Mallet Percussion]", + "marimba": "[ยังไม่แปล: Marimba]", + "glockenspiel": "[ยังไม่แปล: Glockenspiel]", + "vibraphone": "[ยังไม่แปล: Vibraphone]", + "steelpan": "[ยังไม่แปล: Steelpan]", + "orchestra": "[ยังไม่แปล: Orchestra]", + "brass_instrument": "[ยังไม่แปล: Brass Instrument]", + "church_bell": "[ยังไม่แปล: Church Bell]", + "jingle_bell": "[ยังไม่แปล: Jingle Bell]", + "french_horn": "[ยังไม่แปล: French Horn]", + "string_section": "[ยังไม่แปล: String Section]", + "violin": "[ยังไม่แปล: Violin]", + "bicycle_bell": "[ยังไม่แปล: Bicycle Bell]", + "pizzicato": "[ยังไม่แปล: Pizzicato]", + "cello": "[ยังไม่แปล: Cello]", + "double_bass": "[ยังไม่แปล: Double Bass]", + "wind_instrument": "[ยังไม่แปล: Wind Instrument]", + "flute": "[ยังไม่แปล: Flute]", + "saxophone": "[ยังไม่แปล: Saxophone]", + "clarinet": "[ยังไม่แปล: Clarinet]", + "harp": "[ยังไม่แปล: Harp]", + "bell": "[ยังไม่แปล: Bell]", + "didgeridoo": "[ยังไม่แปล: Didgeridoo]", + "theremin": "[ยังไม่แปล: Theremin]", + "singing_bowl": "[ยังไม่แปล: Singing Bowl]", + "scratching": "[ยังไม่แปล: Scratching]", + "pop_music": "[ยังไม่แปล: Pop Music]", + "hip_hop_music": "[ยังไม่แปล: Hip-Hop Music]", + "beatboxing": "[ยังไม่แปล: Beatboxing]", + "grunge": "[ยังไม่แปล: Grunge]", + "progressive_rock": "[ยังไม่แปล: Progressive Rock]", + "rock_and_roll": "[ยังไม่แปล: Rock and Roll]", + "classical_music": "[ยังไม่แปล: Classical Music]", + "opera": "[ยังไม่แปล: Opera]", + "electronic_music": "[ยังไม่แปล: Electronic Music]", + "house_music": "[ยังไม่แปล: House Music]", + "drum_and_bass": "[ยังไม่แปล: Drum and Bass]", + "techno": "[ยังไม่แปล: Techno]", + "dubstep": "[ยังไม่แปล: Dubstep]", + "electronica": "[ยังไม่แปล: Electronica]", + "electronic_dance_music": "[ยังไม่แปล: Electronic Dance Music]", + "ambient_music": "[ยังไม่แปล: Ambient Music]", + "trance_music": "[ยังไม่แปล: Trance Music]", + "music_of_latin_america": "[ยังไม่แปล: Music of Latin America]", + "salsa_music": "[ยังไม่แปล: Salsa Music]", + "flamenco": "[ยังไม่แปล: Flamenco]", + "blues": "[ยังไม่แปล: Blues]", + "music_for_children": "[ยังไม่แปล: Music for Children]", + "new-age_music": "[ยังไม่แปล: New Age Music]", + "vocal_music": "[ยังไม่แปล: Vocal Music]", + "a_capella": "[ยังไม่แปล: A Capella]", + "music_of_africa": "[ยังไม่แปล: Music of Africa]", + "afrobeat": "[ยังไม่แปล: Afrobeat]", + "christian_music": "[ยังไม่แปล: Christian Music]", + "gospel_music": "[ยังไม่แปล: Gospel Music]", + "music_of_asia": "[ยังไม่แปล: Music of Asia]", + "carnatic_music": "[ยังไม่แปล: Carnatic Music]", + "music_of_bollywood": "[ยังไม่แปล: Music of Bollywood]", + "ska": "[ยังไม่แปล: Ska]", + "traditional_music": "[ยังไม่แปล: Traditional Music]", + "background_music": "[ยังไม่แปล: Background Music]", + "theme_music": "[ยังไม่แปล: Theme Music]", + "jingle": "[ยังไม่แปล: Jingle]", + "soundtrack_music": "[ยังไม่แปล: Soundtrack Music]", + "lullaby": "[ยังไม่แปล: Lullaby]", + "video_game_music": "[ยังไม่แปล: Video Game Music]", + "christmas_music": "[ยังไม่แปล: Christmas Music]", + "dance_music": "[ยังไม่แปล: Dance Music]", + "wedding_music": "[ยังไม่แปล: Wedding Music]", + "happy_music": "[ยังไม่แปล: Happy Music]", + "sad_music": "[ยังไม่แปล: Sad Music]", + "angry_music": "[ยังไม่แปล: Angry Music]", + "scary_music": "[ยังไม่แปล: Scary Music]", + "tender_music": "[ยังไม่แปล: Tender Music]", + "exciting_music": "[ยังไม่แปล: Exciting Music]", + "wind": "[ยังไม่แปล: Wind]", + "rustling_leaves": "[ยังไม่แปล: Rustling Leaves]", + "wind_noise": "[ยังไม่แปล: Wind Noise]", + "thunderstorm": "[ยังไม่แปล: Thunderstorm]", + "thunder": "[ยังไม่แปล: Thunder]", + "water": "[ยังไม่แปล: Water]", + "rain": "[ยังไม่แปล: Rain]", + "raindrop": "[ยังไม่แปล: Raindrop]", + "rain_on_surface": "[ยังไม่แปล: Rain on Surface]", + "stream": "[ยังไม่แปล: Stream]", + "ocean": "[ยังไม่แปล: Ocean]", + "waterfall": "[ยังไม่แปล: Waterfall]", + "waves": "[ยังไม่แปล: Waves]", + "steam": "[ยังไม่แปล: Steam]", + "gurgling": "[ยังไม่แปล: Gurgling]", + "fire": "[ยังไม่แปล: Fire]", + "crackle": "[ยังไม่แปล: Crackle]", + "vehicle": "ยานพาหนะ", + "boat": "เรือ", + "sailboat": "[ยังไม่แปล: Sailboat]", + "rowboat": "[ยังไม่แปล: Rowboat]", + "motorboat": "[ยังไม่แปล: Motorboat]", + "ship": "[ยังไม่แปล: Ship]", + "motor_vehicle": "[ยังไม่แปล: Motor Vehicle]", + "car": "รถยนต์", + "toot": "[ยังไม่แปล: Toot]", + "car_alarm": "[ยังไม่แปล: Car Alarm]", + "power_windows": "[ยังไม่แปล: Power Windows]", + "skidding": "[ยังไม่แปล: Skidding]", + "tire_squeal": "[ยังไม่แปล: Tire Squeal]", + "car_passing_by": "[ยังไม่แปล: Car Passing By]", + "race_car": "[ยังไม่แปล: Race Car]", + "truck": "[ยังไม่แปล: Truck]", + "air_brake": "[ยังไม่แปล: Air Brake]", + "air_horn": "[ยังไม่แปล: Air Horn]", + "reversing_beeps": "[ยังไม่แปล: Reversing Beeps]", + "ice_cream_truck": "[ยังไม่แปล: Ice Cream Truck]", + "bus": "รถประจำทาง", + "emergency_vehicle": "[ยังไม่แปล: Emergency Vehicle]", + "police_car": "[ยังไม่แปล: Police Car]", + "ambulance": "[ยังไม่แปล: Ambulance]", + "fire_engine": "[ยังไม่แปล: Fire Engine]", + "motorcycle": "มอเตอร์ไซค์", + "traffic_noise": "[ยังไม่แปล: Traffic Noise]", + "rail_transport": "[ยังไม่แปล: Rail Transport]", + "train": "รถไฟ", + "train_whistle": "[ยังไม่แปล: Train Whistle]", + "train_horn": "[ยังไม่แปล: Train Horn]", + "railroad_car": "[ยังไม่แปล: Railroad Car]", + "train_wheels_squealing": "[ยังไม่แปล: Train Wheels Squealing]", + "subway": "[ยังไม่แปล: Subway]", + "aircraft": "[ยังไม่แปล: Aircraft]", + "aircraft_engine": "[ยังไม่แปล: Aircraft Engine]", + "skateboard": "สเก็ตบอร์ด", + "engine": "[ยังไม่แปล: Engine]", + "light_engine": "[ยังไม่แปล: Light Engine]", + "dental_drill's_drill": "[ยังไม่แปล: Dental Drill]", + "lawn_mower": "[ยังไม่แปล: Lawn Mower]", + "chainsaw": "[ยังไม่แปล: Chainsaw]", + "medium_engine": "[ยังไม่แปล: Medium Engine]", + "heavy_engine": "[ยังไม่แปล: Heavy Engine]", + "engine_knocking": "[ยังไม่แปล: Engine Knocking]", + "engine_starting": "[ยังไม่แปล: Engine Starting]", + "idling": "[ยังไม่แปล: Idling]", + "doorbell": "[ยังไม่แปล: Doorbell]", + "ding-dong": "[ยังไม่แปล: Ding-Dong]", + "sliding_door": "[ยังไม่แปล: Sliding Door]", + "knock": "[ยังไม่แปล: Knock]", + "tap": "[ยังไม่แปล: Tap]", + "squeak": "[ยังไม่แปล: Squeak]", + "drawer_open_or_close": "[ยังไม่แปล: Drawer Open or Close]", + "cupboard_open_or_close": "[ยังไม่แปล: Cupboard Open or Close]", + "dishes": "[ยังไม่แปล: Dishes]", + "cutlery": "[ยังไม่แปล: Cutlery]", + "chopping": "[ยังไม่แปล: Chopping]", + "frying": "[ยังไม่แปล: Frying]", + "microwave_oven": "[ยังไม่แปล: Microwave Oven]", + "blender": "เครื่องปั่น", + "water_tap": "[ยังไม่แปล: Water Tap]", + "sink": "อ่างล้างจาน", + "bathtub": "[ยังไม่แปล: Bathtub]", + "hair_dryer": "ไดร์เป่าผม", + "toilet_flush": "[ยังไม่แปล: Toilet Flush]", + "toothbrush": "แปรงสีฟัน", + "electric_toothbrush": "[ยังไม่แปล: Electric Toothbrush]", + "vacuum_cleaner": "[ยังไม่แปล: Vacuum Cleaner]", + "zipper": "[ยังไม่แปล: Zipper]", + "keys_jangling": "[ยังไม่แปล: Keys Jangling]", + "coin": "[ยังไม่แปล: Coin]", + "scissors": "กรรไกร", + "electric_shaver": "[ยังไม่แปล: Electric Shaver]", + "shuffling_cards": "[ยังไม่แปล: Shuffling Cards]", + "typing": "[ยังไม่แปล: Typing]", + "typewriter": "[ยังไม่แปล: Typewriter]", + "writing": "[ยังไม่แปล: Writing]", + "alarm": "[ยังไม่แปล: Alarm]", + "ringtone": "[ยังไม่แปล: Ringtone]", + "telephone": "[ยังไม่แปล: Telephone]", + "telephone_bell_ringing": "[ยังไม่แปล: Telephone Bell Ringing]", + "telephone_dialing": "[ยังไม่แปล: Telephone Dialing]", + "dial_tone": "[ยังไม่แปล: Dial Tone]", + "busy_signal": "[ยังไม่แปล: Busy Signal]", + "alarm_clock": "[ยังไม่แปล: Alarm Clock]", + "siren": "[ยังไม่แปล: Siren]", + "civil_defense_siren": "[ยังไม่แปล: Civil Defense Siren]", + "buzzer": "[ยังไม่แปล: Buzzer]", + "smoke_detector": "[ยังไม่แปล: Smoke Detector]", + "fire_alarm": "[ยังไม่แปล: Fire Alarm]", + "foghorn": "[ยังไม่แปล: Foghorn]", + "whistle": "[ยังไม่แปล: Whistle]", + "steam_whistle": "[ยังไม่แปล: Steam Whistle]", + "mechanisms": "[ยังไม่แปล: Mechanisms]", + "ratchet": "[ยังไม่แปล: Ratchet]", + "clock": "นาฬิกา", + "tick": "[ยังไม่แปล: Tick]", + "tick-tock": "[ยังไม่แปล: Tick-Tock]", + "gears": "[ยังไม่แปล: Gears]", + "pulleys": "[ยังไม่แปล: Pulleys]", + "sewing_machine": "[ยังไม่แปล: Sewing Machine]", + "mechanical_fan": "[ยังไม่แปล: Mechanical Fan]", + "air_conditioning": "[ยังไม่แปล: Air Conditioning]", + "cash_register": "[ยังไม่แปล: Cash Register]", + "printer": "[ยังไม่แปล: Printer]", + "camera": "กล้อง", + "single-lens_reflex_camera": "[ยังไม่แปล: Single-Lens Reflex Camera]", + "tools": "[ยังไม่แปล: Tools]", + "hammer": "[ยังไม่แปล: Hammer]", + "sawing": "[ยังไม่แปล: Sawing]", + "jackhammer": "[ยังไม่แปล: Jackhammer]", + "filing": "[ยังไม่แปล: Filing]", + "sanding": "[ยังไม่แปล: Sanding]", + "power_tool": "[ยังไม่แปล: Power Tool]", + "drill": "[ยังไม่แปล: Drill]", + "explosion": "[ยังไม่แปล: Explosion]", + "gunshot": "[ยังไม่แปล: Gunshot]", + "machine_gun": "[ยังไม่แปล: Machine Gun]", + "fusillade": "[ยังไม่แปล: Fusillade]", + "artillery_fire": "[ยังไม่แปล: Artillery Fire]", + "cap_gun": "[ยังไม่แปล: Cap Gun]", + "fireworks": "[ยังไม่แปล: Fireworks]", + "firecracker": "[ยังไม่แปล: Firecracker]", + "burst": "[ยังไม่แปล: Burst]", + "eruption": "[ยังไม่แปล: Eruption]", + "boom": "[ยังไม่แปล: Boom]", + "wood": "[ยังไม่แปล: Wood]", + "chop": "[ยังไม่แปล: Chop]", + "splinter": "[ยังไม่แปล: Splinter]", + "crack": "[ยังไม่แปล: Crack]", + "glass": "[ยังไม่แปล: Glass]", + "chink": "[ยังไม่แปล: Chink]", + "shatter": "[ยังไม่แปล: Shatter]", + "silence": "[ยังไม่แปล: Silence]", + "sound_effect": "[ยังไม่แปล: Sound Effect]", + "environmental_noise": "[ยังไม่แปล: Environmental Noise]", + "static": "[ยังไม่แปล: Static]", + "white_noise": "[ยังไม่แปล: White Noise]", + "pink_noise": "[ยังไม่แปล: Pink Noise]", + "television": "[ยังไม่แปล: Television]" +} diff --git a/web/public/locales/th/common.json b/web/public/locales/th/common.json new file mode 100644 index 0000000..75cbef6 --- /dev/null +++ b/web/public/locales/th/common.json @@ -0,0 +1,252 @@ +{ + "time": { + "today": "วันนี้", + "justNow": "ตอนนี้", + "yesterday": "เมื่อวาน", + "lastWeek": "สัปดาห์ที่แล้ว", + "untilForRestart": "จนกว่า Frigate รีสตาร์ท.", + "untilRestart": "จนกว่า รีสตาร์ท", + "ago": "{{timeAgo}} ที่แล้ว", + "last7": "7 วันที่แล้ว", + "last14": "14 วันที่แล้ว", + "last30": "30 วันที่แล้ว", + "thisWeek": "สัปดาห์นี้", + "untilForTime": "จนกว่า {{time}}", + "thisMonth": "เดือนนี้", + "lastMonth": "เดือนที่แล้ว", + "5minutes": "5 นาที", + "30minutes": "30 นาที", + "12hours": "12 ชั่วโมง", + "10minutes": "10 นาที", + "1hour": "1 ชั่วโมง", + "24hours": "24 ชั่วโมง", + "pm": "หลังเที่ยง", + "year_other": "{{time}} ปี", + "month_other": "{{time}} เดือน", + "day_other": "{{time}} วัน", + "h": "{{time}} ชั่วโมง", + "hour_other": "{{time}} ชั่วโมง", + "m": "{{time}} นาที", + "s": "{{time}} วินาที", + "mo": "{{time}} เดือน", + "minute_other": "{{time}} นาที", + "am": "ก่อนเที่ยง", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "yr": "{{time}} ปี", + "d": "{{time}} วัน", + "second_other": "{{time}} วินาที", + "formattedTimestampMonthDayYear": { + "24hour": "MMM d, yyyy", + "12hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "h:mm:ss aaa" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestampMonthDay": "MMM d", + "never": "ไม่เคย" + }, + "label": { + "back": "ย้อนกลับ" + }, + "button": { + "apply": "ใช้งาน", + "reset": "รีเซ็ต", + "done": "เรียบร้อย", + "enabled": "เปิดใช้งานแล้ว", + "enable": "เปิดใช้งาน", + "disable": "ปิดใช้งาน", + "disabled": "ปิดใช้งานแล้ว", + "save": "บันทึก", + "saving": "กำลังบันทึก…", + "cancel": "ยกเลิก", + "close": "ปิด", + "copy": "คัดลอก", + "back": "กลับ", + "history": "ประวัติ", + "fullscreen": "เต็มจอ", + "exitFullscreen": "ออกเต็มจอ", + "pictureInPicture": "ภาพซ้อนภาพ", + "cameraAudio": "เสียงกล้อง", + "on": "เปิด", + "off": "ปิด", + "edit": "แก้ไข", + "delete": "ลบ", + "yes": "ใช่", + "no": "ไม่", + "download": "ดาวน์โหลด", + "info": "ข้อมูล", + "play": "เล่น", + "unselect": "ไม่ได้เลือก", + "export": "ส่งออก", + "deleteNow": "ลบตอนนี้", + "next": "ต่อไป", + "twoWayTalk": "พูดคุยสองทาง", + "copyCoordinates": "คัดลอกพิกัด", + "suspended": "ถูกระงับ", + "unsuspended": "ยกเลิกถูกระงับ" + }, + "menu": { + "restart": "รีสตาร์ท Frigate", + "user": { + "logout": "ออกจากระบบ", + "title": "ผู้ใช้", + "account": "บัญชี", + "current": "ผู้ใช้ปัจจุบัน: {{user}}", + "anonymous": "ไม่ระบุตัวตน", + "setPassword": "ตั้งรหัสผ่าน" + }, + "live": { + "cameras": { + "count_other": "{{count}} กล้อง", + "title": "กล้อง" + }, + "title": "สด", + "allCameras": "กล้องทั้งหมด" + }, + "configurationEditor": "ตัวแก้ไขการกำหนดค่า", + "export": "ส่งออก", + "system": "ระบบ", + "configuration": "การกำหนดค่า", + "systemLogs": "บันทึกระบบ", + "settings": "ตั้งค่า", + "languages": "ภาษา", + "language": { + "withSystem": { + "label": "ใช้ภาษาของระบบ" + }, + "en": "English (อังกฤษ)", + "zhCN": "简体中文 (ภาษาจีนตัวย่อ)", + "hi": "हिन्दी (ฮินดี)", + "fr": "Français (ฝรั่งเศส)", + "ar": "العربية (อาหรับ)", + "pt": "Português (โปรตุเกส)", + "ru": "Русский (รัสเซีย)", + "de": "Deutsch (เยอรมัน)", + "ja": "日本語 (ญี่ปุ่น)", + "tr": "Türkçe (ตุรกี)", + "it": "Italiano (อิตาเลียน)", + "nl": "Nederlands (ดัตช์)", + "sv": "Svenska (สวีเดน)", + "cs": "Čeština (เช็ก)", + "nb": "Norsk Bokmål (นอร์เวย์ บ็อกมอล)", + "ko": "한국어 (เกาหลี)", + "fa": "فارسی (เปอร์เซีย)", + "he": "עברית (ฮีบรู)", + "el": "Ελληνικά (กรีก)", + "ro": "Română (โรมาเนีย)", + "hu": "Magyar (ฮังการี)", + "fi": "Suomi (ฟินแลนด์)", + "da": "Dansk (เดนมาร์ก)", + "es": "Español (สเปน)", + "sk": "Slovenčina (สโลวัก)", + "uk": "Українська (ยูเครน)", + "vi": "Tiếng Việt (เวียดนาม)", + "yue": "粵語 (กวางตุ้ง)", + "pl": "Polski (ขัด)", + "th": "ไทย (ไทย)" + }, + "darkMode": { + "label": "โหมดมืด", + "light": "สว่าง", + "dark": "มืด", + "withSystem": { + "label": "ใช้ของระบบสำหรับโหมดสว่างหรือมืด" + } + }, + "withSystem": "ระบบ", + "theme": { + "label": "ธีม", + "blue": "น้ำเงิน", + "green": "เขียว", + "red": "แดง", + "highcontrast": "คอนทราสต์สูง", + "default": "เริ่มต้น", + "nord": "ฟ้า" + }, + "review": "รีวิว", + "explore": "สำรวจ", + "uiPlayground": "UI สนามเด็กเล่น", + "faceLibrary": "ที่เก็บหน้า", + "help": "ช่วยเหลือ", + "documentation": { + "title": "เอกสาร", + "label": "เอกสาร Frigate" + }, + "systemMetrics": "ตัวชี้วัดของระบบ", + "appearance": "หน้าตา" + }, + "role": { + "viewer": "ผู้ชม", + "title": "บทบาท", + "admin": "ผู้ดูแล", + "desc": "ผู้ดูแลสามารถเข้าถึงระบบได้ทั้งหมดใน UI Frigate. ผู้ชมสามารถทำได้แค่ ดูกล้อง, ดูรีวิว, และ ประวัติคลิปใน UI." + }, + "toast": { + "save": { + "error": { + "noMessage": "มีข้อผิดพลาดในการกำหนดค่า", + "title": "ผิดพลาดในการบันทึกการกำหนดค่า: {{errorMessage}}" + }, + "title": "บันทึก" + }, + "copyUrlToClipboard": "คัดลอก URL ใส่ คลิปบอร์ดแล้ว" + }, + "pagination": { + "previous": { + "title": "ก่อนหน้า", + "label": "ไปหน้าที่แล้ว" + }, + "next": { + "title": "ต่อไป", + "label": "ไปหน้าต่อไป" + }, + "more": "หน้าเพิ่มเติม", + "label": "แบ่งหน้า" + }, + "accessDenied": { + "documentTitle": "ไม่สามารถเข้าถึงได้ - Frigate", + "title": "ไม่สามารถเข้าถึงได้", + "desc": "คุณไม่มีสิทธิ์ในการเข้าถึงหน้านี้" + }, + "notFound": { + "documentTitle": "ไม่พบ - Frigate", + "desc": "ไม่พบหน้านี้", + "title": "๔๐๔" + }, + "selectItem": "เลือก {{item}}", + "unit": { + "speed": { + "mph": "ไมล์ต่อชั่วโมง", + "kph": "กิโลเมตรต่อชั่วโมง" + }, + "length": { + "feet": "ฟุต", + "meters": "เมตร" + } + }, + "readTheDocumentation": "อ่านเอกสาร" +} diff --git a/web/public/locales/th/components/auth.json b/web/public/locales/th/components/auth.json new file mode 100644 index 0000000..7a24ffe --- /dev/null +++ b/web/public/locales/th/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "user": "ชื่อผู้ใช้", + "errors": { + "webUnknownError": "ข้อผิดพลาดที่ไม่รู้จัก. ตรวจสอบที่ console logs.", + "rateLimit": "เกินขีดจำกัด. โปรดลองอีกครั้งในภายหลัง.", + "loginFailed": "ล็อกอินไม่สำเร็จ", + "unknownError": "ข้อผิดพลาดที่ไม่รู้จัก. ตรวจสอบที่ logs.", + "passwordRequired": "ต้องการรหัสผ่าน", + "usernameRequired": "ต้องการชื่อผู้ใช้" + }, + "login": "ล็อกอิน", + "password": "รหัสผ่าน" + } +} diff --git a/web/public/locales/th/components/camera.json b/web/public/locales/th/components/camera.json new file mode 100644 index 0000000..3be4d79 --- /dev/null +++ b/web/public/locales/th/components/camera.json @@ -0,0 +1,86 @@ +{ + "group": { + "camera": { + "setting": { + "title": "{{cameraName}} ตั้งค่าการสตรีมมิ่ง", + "audioIsAvailable": "เสียงสามารถใช้งานได้ในสตรีมนี้", + "audioIsUnavailable": "เสียงไม่สามารถใช้ได้สําหรับสตรีมนี้", + "audio": { + "tips": { + "title": "เสียงต้องส่งออกจากกล้องของคุณและกําหนดค่าบน go2rtc สําหรับสตรีมนี้.", + "document": "อ่านเอกสาร " + } + }, + "stream": "สตรีม", + "placeholder": "เลือกสตรีม", + "streamMethod": { + "label": "วิธีสตรีมมิ่ง", + "placeholder": "เลือกวิธีสตรีมมิ่ง", + "method": { + "noStreaming": { + "label": "ไม่มีการสตรีม", + "desc": "ภาพของกล้องจะอัปเดตนาทีต่อครั้งและไม่มีสตรีมสด." + }, + "smartStreaming": { + "label": "สมาร์ทสตรีม (แนะนํา)", + "desc": "สมาร์ทสตรีมจะอัปเดตกล้องของคุณทุกหนึ่งนาทีเมื่อไม่มีการตรวจพบกิจกรรมที่เกิดขึ้นเพื่อรักษาแบนด์วิดท์และทรัพยากร. เมื่อมีการตรวจพบกิจกรรม, ภาพจะถูกเปลี่ยนเป็นสตรีมสด." + }, + "continuousStreaming": { + "label": "สตรีมมิ่ง", + "desc": { + "title": "ภาพของกล้องจะแสดงตลอดเมื่ออยู่บนแดชบอร์ด, แม้ว่าไม่มีกิจกรรม.", + "warning": "สตรีมแบบต่อเนื่องอาจทําให้เกิดแบนด์วิดท์สูงและเกิดปัญหาประสิทธิภาพการทํางาน, โปรดใช้ความระมัดระวัง." + } + } + } + }, + "desc": "เปลี่ยนตัวเลือกสตรีมสดสําหรับแผงควบคุมของกลุ่มกล้องนี้. การตั้งค่าเหล่านั้นเฉพาะอุปกรณ์/เบราว์เซอร์", + "compatibilityMode": { + "label": "โหมด", + "desc": "เปิดใช้งานตัวเลือกนี้เฉพาะเมื่อถ้ากล้องของคุณถ่ายทอดสดเป็นการแสดงสีแปลกๆและมีเส้นด้านข้าง." + }, + "label": "การตั้งค่าสตรีมกล้อง" + } + }, + "label": "กลุ่มกล้อง", + "add": "เพิ่มกลุ่มกล้อง", + "edit": "แก้ไขกลุ่มกล้อง", + "delete": { + "label": "ลบกลุ่มกล้อง", + "confirm": { + "title": "ยืนยันการลบ", + "desc": "คุณแน่ใจหรือไม่ว่าต้องการลบกลุ่มกล้อง {{name}}?" + } + }, + "name": { + "label": "ชื่อ", + "placeholder": "ป้อนชื่อ…", + "errorMessage": { + "mustLeastCharacters": "ชื่อกลุ่มกล้องต้องมีอย่างน้อย 2 ตัวอักษร", + "exists": "ชื่อกลุ่มกล้องมีอยู่แล้ว", + "nameMustNotPeriod": "ชื่อกลุ่มกล้องต้องไม่มีจุด", + "invalid": "ชื่อกลุ่มกล้องไม่ถูกต้อง" + } + }, + "cameras": { + "label": "กล้อง", + "desc": "เลือกกล้องสำหรับกลุ่มนี้" + }, + "icon": "ไอคอน", + "success": "บันทึกกลุ่มกล้อง ({{name}}) เรียบร้อยแล้ว" + }, + "debug": { + "options": { + "label": "การตั้งค่า", + "title": "ตัวเลือก", + "showOptions": "แสดงตัวเลือก", + "hideOptions": "ซ่อนไฟล์ตัวเลือก" + }, + "boundingBox": "กรอบตรวจจับ", + "timestamp": "เวลาปัจจุบัน", + "zones": "โซน", + "mask": "หน้ากาก", + "motion": "การเคลื่อนไหว", + "regions": "พื้นที่" + } +} diff --git a/web/public/locales/th/components/dialog.json b/web/public/locales/th/components/dialog.json new file mode 100644 index 0000000..6e4d322 --- /dev/null +++ b/web/public/locales/th/components/dialog.json @@ -0,0 +1,106 @@ +{ + "search": { + "saveSearch": { + "label": "บันทึกค้นหา", + "success": "ค้นหา {{searchName}} ถูกบันทึกเรียบร้อยแล้ว", + "desc": "ระบุชื่อสำหรับการค้นหาบันทึกนี้", + "button": { + "save": { + "label": "บันทึกการค้นหานี้" + } + }, + "overwrite": "{{searchName}} มีอยู่แล้ว. การบันทึกจะทำการทับของเดิมลงไป.", + "placeholder": "ใส่ชื่อสําหรับการค้นหาของคุณ" + } + }, + "export": { + "fromTimeline": { + "saveExport": "บันทึกส่งออก", + "previewExport": "ตัวอย่างส่งออก" + }, + "toast": { + "error": { + "endTimeMustAfterStartTime": "เวลาสิ้นสุดต้องอยู่หลังเวลาเริ่มต้น", + "noVaildTimeSelected": "ไม่ได้เลือกช่วงเวลาที่ถูกต้อง", + "failed": "เริ่มต้นการส่งออกผิดพลาด: {{error}}" + }, + "success": "เริ่มต้นการส่งออก ดูไฟล์ในโฟลเดอร์ /exports." + }, + "time": { + "fromTimeline": "เลือกจากเวลา", + "lastHour_other": "ก่อนหน้านี้ {{count}} ชั่วโมง", + "custom": "กําหนดเอง", + "start": { + "title": "เวลาเริ่มต้น", + "label": "เลือกเวลาเริ่มต้น" + }, + "end": { + "title": "เวลาสิ้นสุด", + "label": "เลือกเวลาสิ้นสุด" + } + }, + "name": { + "placeholder": "ชื่อส่งออก" + }, + "select": "เลือก", + "export": "ส่งออก", + "selectOrExport": "เลือกหรือส่งออก" + }, + "restart": { + "restarting": { + "button": "โหลดหน้าใหม่ตอนนี้", + "title": "Frigate กำลังรีสตาร์ท", + "content": "หน้านี้จะถูกโหลดในอีก {{countdown}} วินาที." + }, + "title": "คุณแน่ใจหรือว่าต้องการรีสตาร์ท Frigate?", + "button": "รีสตาร์ท", + "description": "Frigate จะหยุดทำงานชั่วขณะในระหว่างรีสตาร์ท" + }, + "explore": { + "plus": { + "review": { + "question": { + "ask_full": "วัตถุนี้คือ {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "ยืนยันหมวดหมู่นี้สําหรับ Frigate+", + "ask_a": "วัตถุนี้คือ {{label}}?", + "ask_an": "วัตถุนี้คือ {{label}}?" + }, + "state": { + "submitted": "ส่งเรียบร้อย" + } + }, + "submitToPlus": { + "label": "ส่งไปยัง Frigate+" + } + }, + "video": { + "viewInHistory": "ดูประวัติ" + } + }, + "recording": { + "button": { + "deleteNow": "ลบตอนนี้", + "export": "ส่งออก", + "markAsReviewed": "ทำเครื่องหมายว่ารีวิวแล้ว" + }, + "confirmDelete": { + "title": "ยืนยันการลบ", + "toast": { + "error": "ลบไม่ได้: {{error}}" + } + } + }, + "streaming": { + "label": "สตรีม", + "restreaming": { + "disabled": "รีสตรีมไม่ได้เปิดใช้งานสําหรับกล้องนี้.", + "desc": { + "readTheDocumentation": "อ่านเอกสาร" + } + }, + "showStats": { + "label": "แสดงสถานะสตรีม", + "desc": "เปิดใช้งานตัวเลือกนี้เพื่อจะแสดงสถิติในกล้อง." + } + } +} diff --git a/web/public/locales/th/components/filter.json b/web/public/locales/th/components/filter.json new file mode 100644 index 0000000..ff7233d --- /dev/null +++ b/web/public/locales/th/components/filter.json @@ -0,0 +1,94 @@ +{ + "filter": "กรอง", + "zones": { + "label": "โซน", + "all": { + "title": "โซนทั้งหมด", + "short": "โซน" + } + }, + "dates": { + "selectPreset": "เลือกค่าที่ตั้งไว้…", + "all": { + "title": "วันที่ทั้งหมด", + "short": "วันที่" + } + }, + "more": "ตัวกรองเพิ่มเติม", + "reset": { + "label": "รีเซ็ตตัวกรองเป็นค่าเริ่มต้น" + }, + "timeRange": "ช่วงเวลา", + "score": "คะแนน", + "estimatedSpeed": "ความเร็วโดยประมาณ {{unit}}", + "features": { + "label": "คุณสมบัติ", + "submittedToFrigatePlus": { + "label": "ส่งไปยัง Frigate+", + "tips": "คุณต้องกรองครั้งแรกบนวัตถุที่มีภาพ.

    วัตถุที่ไม่มีภาพไม่สามารถส่งออกได้." + }, + "hasSnapshot": "มีภาพ", + "hasVideoClip": "มีวิดีโอ" + }, + "sort": { + "label": "เรียง", + "dateAsc": "วันที่ (จากน้อยไปมาก)", + "scoreDesc": "คะแนน วัตถุ (จากมากไปน้อย)", + "speedDesc": "ความเร็วโดยประมาณ (จากมากไปน้อย)", + "relevance": "สอดคล้อง", + "scoreAsc": "คะแนน วัตถุ (จากน้อยไปมาก)", + "speedAsc": "ความเร็วโดยประมาณ (จากน้อยไปมาก)", + "dateDesc": "วันที่ (จากมากไปน้อย)" + }, + "subLabels": { + "all": "หมวดหมู่ย่อยทั้งหมด", + "label": "หมวดหมู่ย่อย" + }, + "labels": { + "all": { + "title": "หมวดหมู่ทั้งหมด", + "short": "หมวดหมู่" + }, + "count_other": "{{count}} หมวดหมู่", + "count_one": "{{count}} หมวดหมู่", + "label": "ป้าย" + }, + "cameras": { + "all": { + "short": "กล้อง", + "title": "กล้องทั้งหมด" + }, + "label": "กรองกล้อง" + }, + "review": { + "showReviewed": "แสดงที่รีวิวแล้ว" + }, + "motion": { + "showMotionOnly": "แสดงเฉพาะการเคลื่อนไหวเท่านั้น" + }, + "explore": { + "settings": { + "defaultView": { + "summary": "สรุป", + "unfilteredGrid": "ตารางที่ไม่ได้กรอง", + "title": "มุมเริ่มต้น", + "desc": "เมื่อไม่มีตัวกรอง, แสดงสรุปวัตถุล่าสุดหรือแสดงตารางที่ไม่ได้กรอง." + }, + "gridColumns": { + "title": "ตารางคอลัมน์", + "desc": "เลือกจำนวนคอลัมน์ในตาราง." + }, + "searchSource": { + "label": "ค้นหาแหล่ง" + }, + "title": "การตั้งค่า" + } + }, + "classes": { + "label": "หมวดหมู่", + "all": { + "title": "คลาสทั้งหมด" + }, + "count_one": "{{count}} คลาส" + } +} diff --git a/web/public/locales/th/components/icons.json b/web/public/locales/th/components/icons.json new file mode 100644 index 0000000..e224c88 --- /dev/null +++ b/web/public/locales/th/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "เลือกไอคอน", + "search": { + "placeholder": "ค้นหาไอคอน" + } + } +} diff --git a/web/public/locales/th/components/input.json b/web/public/locales/th/components/input.json new file mode 100644 index 0000000..ad2edd2 --- /dev/null +++ b/web/public/locales/th/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "ดาวน์โหลดวิดีโอ", + "toast": { + "success": "วิดีโอรายการรีวิวของคุณเริ่มดาวน์โหลดแล้ว." + } + } + } +} diff --git a/web/public/locales/th/components/player.json b/web/public/locales/th/components/player.json new file mode 100644 index 0000000..03e5a1a --- /dev/null +++ b/web/public/locales/th/components/player.json @@ -0,0 +1,51 @@ +{ + "streamOffline": { + "desc": "ไม่ได้รับเฟรมบน {{cameraName}} สตรีม detect, ตรวจสอบ error logs", + "title": "สตรีมออฟไลน์" + }, + "noPreviewFound": "ไม่พบตัวอย่าง", + "submitFrigatePlus": { + "title": "ส่งเฟรมนี้ให้ Frigate+ ไหม?", + "submit": "ส่ง" + }, + "livePlayerRequiredIOSVersion": "ต้องใช้ iOS 17.1 ขึ้นไปสำหรับประเภทสตรีมสดนี้", + "cameraDisabled": "กล้องถูกปิดใช้งาน", + "stats": { + "streamType": { + "title": "ประเภทสตรีม:", + "short": "ประเภท" + }, + "latency": { + "title": "ความหน่วง:", + "value": "{{seconds}} วินาที", + "short": { + "title": "ความหน่วง", + "value": "{{seconds}} วิ" + } + }, + "totalFrames": "จำนวนเฟรมทั้งหมด:", + "droppedFrames": { + "short": { + "title": "หายไป", + "value": "{{droppedFrames}} เฟรม" + }, + "title": "เฟรมที่หาย:" + }, + "decodedFrames": "เฟรมถูกถอดรหัส:", + "droppedFrameRate": "อัตราเฟรมที่หายไป:", + "bandwidth": { + "title": "แบนด์วิธ:", + "short": "แบนด์วิธ" + } + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "ไม่สามารถส่งเฟรมไปยัง Frigate+ ได้" + }, + "success": { + "submittedFrigatePlus": "ส่งเฟรมให้ Frigate+ เรียบร้อยแล้ว" + } + }, + "noRecordingsFoundForThisTime": "ไม่เจอการบันทึกในช่วงเวลานี้", + "noPreviewFoundFor": "ไม่พบตัวอย่างสำหรับ {{cameraName}}" +} diff --git a/web/public/locales/th/config/cameras.json b/web/public/locales/th/config/cameras.json new file mode 100644 index 0000000..4a41919 --- /dev/null +++ b/web/public/locales/th/config/cameras.json @@ -0,0 +1,16 @@ +{ + "label": "ตั้งค่ากล้อง", + "name": { + "label": "ชื่อกล้อง" + }, + "friendly_name": { + "label": "ชื่อแบบจำง่าย" + }, + "enabled": { + "label": "ถูกเปิดอยู่", + "description": "ถูกเปิดอยู่" + }, + "audio": { + "label": "การตรวจจับเสียง" + } +} diff --git a/web/public/locales/th/config/global.json b/web/public/locales/th/config/global.json new file mode 100644 index 0000000..86743dc --- /dev/null +++ b/web/public/locales/th/config/global.json @@ -0,0 +1,16 @@ +{ + "version": { + "label": "การตั้งค่าปัจจุบัน" + }, + "environment_vars": { + "label": "สภาพแวดล้อมที่หลากหลาย" + }, + "audio": { + "label": "การตรวจจับเสียง" + }, + "auth": { + "enabled": { + "label": "เปิดใช้การยืนยันตัวตน" + } + } +} diff --git a/web/public/locales/th/config/groups.json b/web/public/locales/th/config/groups.json new file mode 100644 index 0000000..43ae3e5 --- /dev/null +++ b/web/public/locales/th/config/groups.json @@ -0,0 +1,18 @@ +{ + "audio": { + "cameras": { + "detection": "การตรวจจับ", + "sensitivity": "ความอ่อนไหว" + } + }, + "snapshots": { + "cameras": { + "display": "แสดงผล" + } + }, + "detect": { + "cameras": { + "resolution": "ความละเอียด" + } + } +} diff --git a/web/public/locales/th/config/validation.json b/web/public/locales/th/config/validation.json new file mode 100644 index 0000000..1e9b0d5 --- /dev/null +++ b/web/public/locales/th/config/validation.json @@ -0,0 +1,9 @@ +{ + "maximum": "มากที่สุดไม่เกิน {{limit}}", + "exclusiveMinimum": "ต้องเกินกว่า {{limit}}", + "exclusiveMaximum": "ต้องน้อยกว่า {{limit}}", + "minLength": "จำนวนอย่างน้อย {{limit}} อักขระ", + "maxLength": "ต้องไม่เกิน {{limit}} อักขระ", + "maxItems": "ต้องไม่เกิน {{limit}}", + "minimum": "ขั้นต่ำ {{limit}}" +} diff --git a/web/public/locales/th/objects.json b/web/public/locales/th/objects.json new file mode 100644 index 0000000..8d3130d --- /dev/null +++ b/web/public/locales/th/objects.json @@ -0,0 +1,118 @@ +{ + "traffic_light": "สัญญาณไฟจราจร", + "hair_dryer": "ไดร์เป่าผม", + "skateboard": "สเก็ตบอร์ด", + "fire_hydrant": "หัวดับเพลิง", + "bicycle": "จักรยาน", + "person": "คน", + "car": "รถยนต์", + "motorcycle": "มอเตอร์ไซค์", + "airplane": "เครื่องบิน", + "bus": "รถประจำทาง", + "train": "รถไฟ", + "cat": "แมว", + "horse": "ม้า", + "bird": "นก", + "boat": "เรือ", + "bench": "ม้านั่ง", + "dog": "สุนัข", + "parking_meter": "มิเตอร์จอดรถ", + "sheep": "แกะ", + "eye_glasses": "แว่นตา", + "sports_ball": "ลูกบอลกีฬา", + "baseball_bat": "ไม้เบสบอล", + "baseball_glove": "ถุงมือเบสบอล", + "tennis_racket": "ไม้เทนนิส", + "wine_glass": "แก้วไวน์", + "vehicle": "ยานพาหนะ", + "clock": "นาฬิกา", + "knife": "มีด", + "bowl": "ชาม", + "hot_dog": "ฮอทดอก", + "potted_plant": "ต้นไม้ในกระถาง", + "dining_table": "โต๊ะกินอาหาร", + "tv": "ทีวิ", + "teddy_bear": "ตุ๊กตาหมี", + "hair_brush": "แปรงหวีผม", + "squirrel": "กระรอก", + "deer": "กวาง", + "face": "ใบหน้า", + "robot_lawnmower": "หุ่นยนต์ตัดหญ้า", + "waste_bin": "ถังขยะ", + "license_plate": "ป้ายทะเบียนรถ", + "amazon": "อเมซอน", + "usps": "ไปรษณีย์สหรัฐ", + "stop_sign": "ป้ายหยุด", + "street_sign": "ป้ายถนน", + "cow": "วัว", + "elephant": "ช้าง", + "bear": "หมี", + "zebra": "ยีราฟ", + "giraffe": "จระเข้น้ำ", + "hat": "หมวก", + "backpack": "กระเป๋าเป้", + "umbrella": "ร่ม", + "shoe": "รองเท้า", + "handbag": "กระเป๋าถือ", + "mouse": "เมาส์", + "tie": "เนคไท", + "suitcase": "กระเป๋าเดินทาง", + "frisbee": "ดิสก์", + "skis": "สกี", + "snowboard": "สโนบอร์ด", + "kite": "ดอกกระดาษ", + "surfboard": "บอร์ดโต้คลื่น", + "bottle": "ขวด", + "plate": "จาน", + "cup": "ถ้วย", + "fork": "ส้อม", + "spoon": "ช้อน", + "banana": "กล้วย", + "apple": "แอปเปิล", + "sandwich": "แซนวิช", + "orange": "ส้ม", + "broccoli": "บรอกโคลี", + "carrot": "แครอท", + "pizza": "พิซซ่า", + "donut": "โดนัท", + "cake": "เค้ก", + "chair": "เก้าอี้", + "couch": "โซฟา", + "bed": "เตียง", + "mirror": "กระจก", + "window": "หน้าต่าง", + "desk": "โต๊ะทำงาน", + "toilet": "ห้องน้ำ", + "door": "ประตู", + "laptop": "แล็ปท็อป", + "remote": "รีโมท", + "oven": "เตาอบ", + "keyboard": "คีย์บอร์ด", + "microwave": "ไมโครเวฟ", + "toaster": "เครื่องทำขนมปัง", + "sink": "อ่างล้างจาน", + "refrigerator": "ตู้เย็น", + "blender": "เครื่องปั่น", + "book": "หนังสือ", + "vase": "แจกัน", + "scissors": "กรรไกร", + "toothbrush": "แปรงสีฟัน", + "animal": "สัตว์", + "bark": "เสียงหอบ", + "fox": "สุนัขจิ้งจอก", + "goat": "แพะ", + "rabbit": "กระต่าย", + "raccoon": "กระรอกน้ำ", + "package": "พัสดุ", + "purolator": "เพรูเลตอร์", + "bbq_grill": "เตาบาร์บีคิว", + "cell_phone": "โทรศัพท์มือถือ", + "ups": "UPS", + "dhl": "DHL", + "gls": "GLS", + "dpd": "DPD", + "postnord": "PostNord", + "nzpost": "NZPost", + "fedex": "FedEx", + "postnl": "PostNL" +} diff --git a/web/public/locales/th/views/chat.json b/web/public/locales/th/views/chat.json new file mode 100644 index 0000000..4390d39 --- /dev/null +++ b/web/public/locales/th/views/chat.json @@ -0,0 +1,10 @@ +{ + "documentTitle": "สนทนา - Frigate", + "subtitle": "AI ผู้ช่วยบริหารจัดการข้อมูลเชิงลึกสำหรับกล้องวงจรปิดของคุณ", + "placeholder": "เชิญถาม…", + "error": "เกิดข้อขัดข้อง โปรดลองอีกครั้ง", + "processing": "กำลังประมวลผล…", + "showTools": "แสดงเครื่องมือ ({{count}})", + "response": "ตอบกลับ", + "attachment_chip_remove": "เอาสิ่งที่แนบออก" +} diff --git a/web/public/locales/th/views/classificationModel.json b/web/public/locales/th/views/classificationModel.json new file mode 100644 index 0000000..81389c3 --- /dev/null +++ b/web/public/locales/th/views/classificationModel.json @@ -0,0 +1,14 @@ +{ + "documentTitle": "โมเดลการจำแนกประเภท- Frigate", + "details": { + "scoreInfo": "คะแนน (Score) คือค่าเฉลี่ยของความมั่นใจในการจำแนกประเภท (Classification Confidence) จากการตรวจจับวัตถุชิ้นนี้ในทุกๆ ครั้ง", + "none": "ไม่มี", + "unknown": "ไม่ทราบ" + }, + "description": { + "invalidName": "ชื่อไม่ถูกต้อง ชื่อสามารถประกอบได้ด้วยตัวอักษร, ตัวเลข, ช่องว่าง, เครื่องหมาย ( ' , _ , - ) เท่านั้น" + }, + "button": { + "deleteImages": "ลบภาพ" + } +} diff --git a/web/public/locales/th/views/configEditor.json b/web/public/locales/th/views/configEditor.json new file mode 100644 index 0000000..d85309d --- /dev/null +++ b/web/public/locales/th/views/configEditor.json @@ -0,0 +1,17 @@ +{ + "copyConfig": "คัดลอกการกำหนดค่า", + "saveOnly": "บันทึกเท่านั้น", + "confirm": "ออกโดยที่ไม่บันทึก?", + "toast": { + "error": { + "savingError": "เกิดข้อผิดพลาดในการบันทึกการกำหนดค่า" + }, + "success": { + "copyToClipboard": "คัดลอกการกำหนดค่าไปยังคลิปบอร์ดแล้ว." + } + }, + "saveAndRestart": "บันทึก และ รีสตาร์ท", + "documentTitle": "ตัวแก้ไขการกำหนดค่า - Frigate", + "configEditor": "ตัวแก้ไขการกำหนดค่า", + "safeConfigEditor": "ตัวแก้ไขการกำหนดค่า (โหมดปลอดภัย)" +} diff --git a/web/public/locales/th/views/events.json b/web/public/locales/th/views/events.json new file mode 100644 index 0000000..9279e9a --- /dev/null +++ b/web/public/locales/th/views/events.json @@ -0,0 +1,39 @@ +{ + "motion": { + "label": "เคลื่อนไหว", + "only": "เคลื่อนไหวเท่านั้น" + }, + "allCameras": "กล้องทั้งหมด", + "empty": { + "detection": "ไม่มีการเคลื่อนไหวให้รีวิว", + "motion": "ไม่เจอข้อมูลการเคลื่อนไหว", + "alert": "ไม่มีการแจ้งเตือนให้รีวิว" + }, + "events": { + "label": "กิจกรรม", + "aria": "เลือกกิจกรรม", + "noFoundForTimePeriod": "ไม่เจอกิจกรรมในช่วงเวลานี้" + }, + "recordings": { + "documentTitle": "การบันทึก - Frigate" + }, + "calendarFilter": { + "last24Hours": "24 ชั่วโมงล่าสุด" + }, + "markAsReviewed": "ทำเครื่องหมายว่ารีวิวแล้ว", + "newReviewItems": { + "button": "รายการใหม่ที่จะรีวิว", + "label": "ดูรายการรีวิวใหม่" + }, + "selected_other": "เลือก {{count}} แล้ว", + "camera": "กล้อง", + "detected": "ตรวจพบ", + "timeline": "ไทม์ไลน์", + "markTheseItemsAsReviewed": "ทำเครื่องหมายรายการเหล่านี้ว่าได้รับการรีวิวแล้ว", + "alerts": "การแจ้งเตือน", + "detections": "การตรวจจับ", + "selected_one": "เลือก {{count}} แล้ว", + "timeline.aria": "เลือกไทม์ไลน์", + "documentTitle": "รีวิว - Frigate", + "zoomIn": "ซูมเข้า" +} diff --git a/web/public/locales/th/views/explore.json b/web/public/locales/th/views/explore.json new file mode 100644 index 0000000..8c17c33 --- /dev/null +++ b/web/public/locales/th/views/explore.json @@ -0,0 +1,38 @@ +{ + "documentTitle": "สํารวจ - Frigate", + "generativeAI": "AI", + "exploreMore": "สํารวจวัตถุ {{label}} เพิ่มเติม", + "exploreIsUnavailable": { + "title": "สํารวจไม่มีให้ใช้งาน", + "embeddingsReindexing": { + "context": "สํารวจสามารถใช้หลังจากติดตามวัตถุเสร็จ.", + "startingUp": "เริ่มต้น…", + "estimatedTime": "ระยะเวลาโดยประมาณ:", + "finishingShortly": "เสร็จเร็วๆนี้", + "step": { + "thumbnailsEmbedded": "รูปภาพย่อที่ฝังไว้: " + } + }, + "downloadingModels": { + "tips": { + "documentation": "อ่านเอกสาร" + } + } + }, + "type": { + "details": "รายละเอียด", + "video": "วิดีโอ" + }, + "objectLifecycle": { + "noImageFound": "ไม่มีภาพสําหรับช่วงเวลานี้.", + "annotationSettings": { + "offset": { + "documentation": "อ่านเอกสาร " + } + } + }, + "trackedObjectsCount_other": "{{count}} วัตถุที่เจอ ", + "details": { + "timestamp": "เวลา" + } +} diff --git a/web/public/locales/th/views/exports.json b/web/public/locales/th/views/exports.json new file mode 100644 index 0000000..1c58da8 --- /dev/null +++ b/web/public/locales/th/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "ส่งออก - Frigate", + "search": "ค้นหา", + "noExports": "ไม่เจอการส่งออก", + "deleteExport": "ลบส่งออก", + "deleteExport.desc": "คุณแน่ใจหรอที่จะลบ {{exportName}}?", + "editExport": { + "title": "แก้ชื่อส่งออก", + "desc": "ใส่ชื่อใหม่สำหรับการส่งออกนี้", + "saveExport": "บันทึกการส่งออก" + }, + "toast": { + "error": { + "renameExportFailed": "ผิดพลาดในการแก้ไขชื่อการส่งออก: {{errorMessage}}" + } + }, + "headings": { + "cases": "กรณี" + }, + "tooltip": { + "editName": "เปลี่ยนชื่อ" + } +} diff --git a/web/public/locales/th/views/faceLibrary.json b/web/public/locales/th/views/faceLibrary.json new file mode 100644 index 0000000..4cbefcc --- /dev/null +++ b/web/public/locales/th/views/faceLibrary.json @@ -0,0 +1,60 @@ +{ + "details": { + "person": "คน", + "subLabelScore": "คะแนน Sub Label", + "unknown": "ไม่ทราบ", + "timestamp": "เวลา" + }, + "steps": { + "faceName": "ใส่ชื่อหน้า", + "uploadFace": "ใส่รูปหน้า", + "nextSteps": "ต่อไป", + "description": { + "uploadFace": "อัพโหลดภาพ {{name}} ที่แสดงให้เห็นใบหน้าของเขาจากมุมข้างหน้า. รูปภาพไม่จําเป็นต้องตัดให้เห็นเฉพาะใบหน้าของเขา." + } + }, + "selectFace": "เลือกหน้า", + "deleteFaceLibrary": { + "title": "ลบชื่อ", + "desc": "คุณแน่ใจหรือไม่ว่าต้องการลบคอลเลกชัน {{name}}? การลบนี้จะเป็นการลบอย่างถาวร." + }, + "deleteFaceAttempts": { + "title": "ลบหน้า" + }, + "renameFace": { + "title": "เปลี่ยนชื่อหน้า", + "desc": "ใส่ชื่อใหม่สำหรับ {{name}}" + }, + "button": { + "deleteFaceAttempts": "ลบหน้า", + "addFace": "เพิ่มหน้า", + "renameFace": "แก้ชื่อหน้า", + "deleteFace": "ลบหน้า", + "uploadImage": "อัปโหลดรูป", + "reprocessFace": "คำนวนหน้าใหม่" + }, + "imageEntry": { + "dropActive": "ลากรูปลงที่นี้", + "dropInstructions": "ลากและวางภาพที่นี่, หรือคลิกเลือก" + }, + "selectItem": "เลือก {{item}}", + "createFaceLibrary": { + "new": "สร้างหน้าใหม่", + "nextSteps": "สร้างรากฐานที่แข็งแรง:
  • ใช้แท็บฝึกเพื่อเลือกและฝึกบนภาพแต่ละบุคคล.
  • เน้นไปที่ภาพหน้าตรงเพื่อผลลัพธ์ที่ดีที่สุด; หลีกเลี่ยงภาพที่จับภาพใบหน้าแบบมุม
  • " + }, + "collections": "คอลเลกชัน", + "description": { + "addFace": "เพิ่มคอลเลกชันใหม่ไปยังคลังใบหน้า โดยการอัปโหลดรูปภาพแรก", + "placeholder": "ใส่ชื่อสําหรับคอลเลกชันนี้", + "invalidName": "ชื่อไม่ถูกต้อง ชื่อสามารถประกอบได้ด้วยตัวอักษร, ตัวเลข, ช่องว่าง, เครื่องหมาย ( ' , _ , - ) เท่านั้น", + "nameCannotContainHash": "ชื่อ ห้ามมีเครื่องหมาย #" + }, + "toast": { + "success": { + "addFaceLibrary": "{{name}} เพิ่มลงที่เก็บหน้าเรียบร้อยแล้ว!", + "deletedName_other": "{{count}} หน้าถูกลบไปเรียบร้อยแล้ว." + } + }, + "readTheDocs": "อ่านเอกสาร", + "documentTitle": "คลังข้อมูลใบหน้า - Frigate" +} diff --git a/web/public/locales/th/views/live.json b/web/public/locales/th/views/live.json new file mode 100644 index 0000000..3bdd408 --- /dev/null +++ b/web/public/locales/th/views/live.json @@ -0,0 +1,58 @@ +{ + "stream": { + "playInBackground": { + "label": "เล่นในพื้นหลัง" + }, + "audio": { + "tips": { + "documentation": "อ่านเอกสาร " + } + }, + "twoWayTalk": { + "tips.documentation": "อ่านเอกสาร " + } + }, + "documentTitle": { + "default": "ถ่ายทอดสด - Frigate" + }, + "lowBandwidthMode": "โหมดแบนด์วิดท์ต่ำ", + "twoWayTalk": { + "enable": "เปิดใช้งานการสนทนาสองทาง", + "disable": "ปิดใช้งานการสนทนาสองทาง" + }, + "cameraAudio": { + "enable": "เปิดเสียงกล้อง", + "disable": "ปิดเสียงกล้อง" + }, + "manualRecording": { + "playInBackground": { + "label": "เล่นในพื้นหลัง" + } + }, + "editLayout": { + "exitEdit": "ออกจากการแก้ไข" + }, + "effectiveRetainMode": { + "modes": { + "all": "ทั้งหมด" + } + }, + "documentTitle.withCamera": "{{camera}} - สด - Frigate", + "streamingSettings": "การตั้งค่าการสตรีม", + "camera": { + "disable": "ปิดกล้อง" + }, + "detect": { + "disable": "ปิดการตรวจจับ" + }, + "recording": { + "disable": "ปิดการบันทึก" + }, + "ptz": { + "move": { + "clickMove": { + "label": "คลิกที่ภาพ เพื่อเลือกตำแหน่งที่จะตั้งค่าให้เป็นศูนย์กลางภาพ" + } + } + } +} diff --git a/web/public/locales/th/views/motionSearch.json b/web/public/locales/th/views/motionSearch.json new file mode 100644 index 0000000..852fb09 --- /dev/null +++ b/web/public/locales/th/views/motionSearch.json @@ -0,0 +1,11 @@ +{ + "documentTitle": "ค้นหาการเคลื่อนไหว - Frigate", + "title": "ค้นหาการเคลื่อนไหว", + "description": "วาดเส้นกรอบกำหนดขอบเขตที่ต้องการ และระบุช่วงเวลาค้นหาการเคลื่อนไหวในบริเวณนั้น", + "startSearch": "เริ่มการค้นหา", + "searchStarted": "การค้นหาเริ่มแล้ว", + "searchCancelled": "เลิกค้นหาแล้ว", + "cancelSearch": "ยกเลิก", + "noChangesFound": "ไม่มีการเปลี่ยนแปลงในภาพบริเวณที่เลือก", + "jumpToTime": "ข้ามมาที่เวลานี้" +} diff --git a/web/public/locales/th/views/recording.json b/web/public/locales/th/views/recording.json new file mode 100644 index 0000000..3a27ea9 --- /dev/null +++ b/web/public/locales/th/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "ส่งออก", + "calendar": "ปฎิทิน", + "filter": "กรอง", + "filters": "ตัวกรอง", + "toast": { + "error": { + "noValidTimeSelected": "ไม่ได้เลือกช่วงเวลาที่ถูกต้อง", + "endTimeMustAfterStartTime": "เวลาสิ้นสุดต้องอยู่หลังเวลาเริ่มต้น" + } + } +} diff --git a/web/public/locales/th/views/replay.json b/web/public/locales/th/views/replay.json new file mode 100644 index 0000000..1c8cc9b --- /dev/null +++ b/web/public/locales/th/views/replay.json @@ -0,0 +1,13 @@ +{ + "websocket_messages": "ข้อความ", + "dialog": { + "camera": "ภาพจากกล้อง", + "timeRange": "ช่วงเวลา", + "preset": { + "1m": "1 นาทีสุดท้าย" + }, + "startButton": "เริ่มเล่นภาพย้อนหลัง", + "selectFromTimeline": "เลือก", + "startLabel": "เริ่ม" + } +} diff --git a/web/public/locales/th/views/search.json b/web/public/locales/th/views/search.json new file mode 100644 index 0000000..050d8aa --- /dev/null +++ b/web/public/locales/th/views/search.json @@ -0,0 +1,62 @@ +{ + "search": "ค้นหา", + "button": { + "save": "บันทึกการค้นหา", + "delete": "ลบการบันทึกค้นหา", + "clear": "ล้างการค้นหา", + "filterInformation": "ข้อมูลตัวกรอง", + "filterActive": "ตัวกรองที่ใช้งาน" + }, + "savedSearches": "บันทึกการค้นหา", + "searchFor": "ค้นหา {{inputValue}}", + "trackedObjectId": "ติดตามรหัส", + "filter": { + "label": { + "cameras": "กล้อง", + "labels": "หมวดหมู่", + "zones": "โซน", + "sub_labels": "หมวดหมู่ย่อย", + "search_type": "ค้นหาประเภท", + "time_range": "ช่วงเวลา", + "after": "หลัง", + "min_score": "คะแนนต่ำ", + "max_speed": "ความเร็วสูงสุด", + "has_clip": "มีคลิป", + "has_snapshot": "มีภาพ", + "before": "ก่อน", + "max_score": "คะแนนสูงสุด", + "min_speed": "ความเร็วต่ำ" + }, + "searchType": { + "description": "ลักษณะ" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "วันที่ของก่อนต้องอยู่หลังจากวันที่ของหลัง.", + "afterDatebeEarlierBefore": "วันที่ 'หลัง' จะต้องมาก่อนวันที่ 'ก่อน'.", + "minScoreMustBeLessOrEqualMaxScore": "'คะแนนต่ำ' ต้องน้อยกว่าหรือเท่ากับ 'คะแนนสูง'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'ความเร็วต่ำ' จะต้องน้อยกว่าหรือเท่ากับ 'ความเร็วสูงสุด'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'ความเร็วสูงสุด' จะต้องมากกว่าหรือเท่ากับ 'ความเร็วต่ำ'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'คะแนนสูง' ต้องมากกว่าหรือเท่ากับ 'คะแนนต่ำ'." + } + }, + "tips": { + "desc": { + "step1": "ประเภทตัวกรองต้องตามด้วยอัญประกาศ (เช่น \"cameras:\").", + "exampleLabel": "ตัวอย่าง:", + "step4": "วันที่กรอง (before: และ after:)ใช้รูปแบบ {{DateFormat}}.", + "step2": "เลือกค่าจากคําแนะนําหรือพิมพ์ด้วยคุณเอง." + }, + "title": "วิธีการใช้ตัวกรองข้อความ" + }, + "header": { + "activeFilters": "ตัวกรองที่ใช้งาน" + } + }, + "placeholder": { + "search": "ค้นหา…" + }, + "similaritySearch": { + "title": "ค้นหาที่คล้ายกัน" + } +} diff --git a/web/public/locales/th/views/settings.json b/web/public/locales/th/views/settings.json new file mode 100644 index 0000000..ebd2805 --- /dev/null +++ b/web/public/locales/th/views/settings.json @@ -0,0 +1,253 @@ +{ + "dialog": { + "unsavedChanges": { + "title": "คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก.", + "desc": "คุณต้องการบันทึกการเปลี่ยนแปลงของคุณก่อนดำเนินการต่อหรือไม่?" + } + }, + "users": { + "dialog": { + "form": { + "newPassword": { + "title": "รหัสผ่านใหม่", + "confirm": { + "placeholder": "ใส่รหัสผ่านใหม่อีกครั้ง" + }, + "placeholder": "ใส่รหัสผ่านใหม่" + }, + "password": { + "notMatch": "รหัสไม่ตรงกัน", + "title": "รหัสผ่าน", + "confirm": { + "title": "ยืนยันรหัสผ่าน", + "placeholder": "ยืนยันรหัสผ่าน" + }, + "strength": { + "title": "ความแข็งแรงของรหัส: ", + "weak": "แย่", + "strong": "ใช้ได้", + "veryStrong": "แข็งแรง", + "medium": "กลาง" + }, + "match": "รหัสตรงกัน", + "placeholder": "ใส่รหัสผ่าน" + }, + "user": { + "placeholder": "ใส่ชื่อผู้ใช้", + "title": "ผู้ใช้" + }, + "passwordIsRequired": "ต้องการรหัสผ่าน", + "usernameIsRequired": "ต้องการชื่อผู้ใช้" + }, + "changeRole": { + "roleInfo": { + "admin": "ผู้ดูแล" + } + } + }, + "toast": { + "success": { + "updatePassword": "อัปเดตรหัสผ่านเรียบร้อย", + "deleteUser": "ลบผู้ใช้ {{user}} เรียบร้อย", + "createUser": "สร้างผู้ใช้ {{user}} เรียบร้อย" + }, + "error": { + "setPasswordFailed": "ผิดพลาดในการบันทึกรหัสผ่าน: {{errorMessage}}", + "deleteUserFailed": "ผิดพลาดในการลบผู้ใช้: {{errorMessage}}", + "createUserFailed": "ผิดพลาดในการสร้างผู้ใช้: {{errorMessage}}" + } + }, + "table": { + "username": "ชื่อผู้ใช้", + "noUsers": "ไม่เจอผู้ใช้", + "password": "รหัสผ่าน", + "deleteUser": "ลบผู้ใช้", + "actions": "การดำเนินการ" + }, + "management": { + "title": "จัดการผู้ใช้", + "desc": "จัดการบัญชีของ Frigate นี้." + }, + "addUser": "แก้ไขผู้ใช้", + "title": "ผู้ใช้", + "updatePassword": "อัปเดตรหัสผ่าน" + }, + "notification": { + "suspendTime": { + "12hours": "ระงับ 12 ชั่วโมง", + "5minutes": "ระงับ 5 นาที", + "10minutes": "ระงับ 10 นาที", + "1hour": "ระงับ 1 ชั่วโมง", + "untilRestart": "ระงับจนกว่ารีสตาร์ท", + "suspend": "ระงับ", + "24hours": "ระงับ 24 ชั่วโมง", + "30minutes": "ระงับ 30 นาที" + }, + "cameras": { + "title": "กล้อง", + "noCameras": "ไม่มีกล้องให้ใช้งาน" + }, + "email": { + "title": "อีเมล" + }, + "notificationSettings": { + "documentation": "อ่านเอกสาร" + }, + "notificationUnavailable": { + "documentation": "อ่านเอกสาร" + } + }, + "documentTitle": { + "default": "ตั้งค่า - Frigate", + "authentication": "การตั้งค่าการตรวจสอบสิทธิ์ - Frigate", + "camera": "การตั้งค่ากล้อง - Frigate", + "classification": "การตั้งค่าการจำแนกประเภท - Frigate", + "masksAndZones": "ตัวแก้ไขแมสและโซน - Frigate", + "general": "การตั้งค่าทั่วไป - Frigate", + "frigatePlus": "การตั้งค่า Frigate+ - Frigate", + "notifications": "การตั้งค่าการแจ้งเตือน - Frigate", + "cameraManagement": "จัดการกล้อง - Frigate", + "enrichments": "การตั้งค่าของเพิ่มเติม - Frigate", + "motionTuner": "ปรับแต่งการเคลื่อนไหว - Frigate", + "object": "ดีบั๊ก - Frigate", + "cameraReview": "แสดงการตั้งค่าของกล้อง - Frigate" + }, + "menu": { + "notifications": "การแจ้งเตือน", + "frigateplus": "Frigate+", + "cameras": "ตั้งค่ากล้อง", + "users": "ผู้ใช้", + "classification": "การจําแนกประเภท", + "masksAndZones": "แมส / โซน", + "ui": "UI" + }, + "cameraSetting": { + "camera": "กล้อง", + "noCamera": "ไม่มีกล้อง" + }, + "general": { + "liveDashboard": { + "title": "แดชบอร์ดสด", + "automaticLiveView": { + "label": "การดูสดอัตโนมัติ", + "desc": "สลับไปที่มุมมองสดของกล้องโดยอัตโนมัติเมื่อตรวจพบกิจกรรม. การปิดใช้งานตัวเลือกนี้จะทำให้ภาพคงที่จากกล้องบนแดชบอร์ดสดอัปเดตเพียงครั้งเดียวต่อนาที." + }, + "playAlertVideos": { + "label": "เล่นวิดีโอการแจ้งเตือน.", + "desc": "ตามค่าเริ่มต้น, แจ้งเตือนล่าสุดบนแดชบอร์ดสด จะเล่นเป็นวิดีโอวนซ้ำขนาดเล็ก. ปิดใช้งานตัวเลือกนี้เพื่อแสดงเฉพาะภาพนิ่งของการแจ้งเตือนล่าสุดบนอุปกรณ์/เบราว์เซอร์นี้เท่านั้น." + } + }, + "storedLayouts": { + "title": "เลย์เอาท์ที่จัดเก็บไว้", + "desc": "คุณสามารถลากหรือปรับขนาดเลย์เอาท์ของกล้องในกลุ่มกล้องได้. ตำแหน่งต่างๆ จะถูกเก็บไว้ในหน่วยความจำภายในของเบราว์เซอร์ของคุณ.", + "clearAll": "ล้างเลย์เอาต์ทั้งหมด" + }, + "calendar": { + "title": "ปฎิทิน", + "firstWeekday": { + "label": "วันธรรมดาวันแรก", + "sunday": "วันอาทิตย์", + "monday": "วันจันทร์", + "desc": "วันที่เริ่มต้นสัปดาห์ของปฏิทินการรีวิว." + } + }, + "title": "การตั้งค่าทั่วไป", + "cameraGroupStreaming": { + "title": "การตั้งค่าสตรีมกล้องแบบกลุ่ม", + "desc": "การตั้งค่าสตรีมมิ่งสําหรับกล้องแต่ละกลุ่มเก็บไว้ในเบราว์เซอร์ของคุณ.", + "clearAll": "ล้างการตั้งค่าสตรีมทั้งหมด" + } + }, + "classification": { + "faceRecognition": { + "title": "การจดจำใบหน้า", + "modelSize": { + "small": { + "title": "เล็ก" + }, + "large": { + "title": "ใหญ่" + } + } + }, + "toast": { + "error": "ผิดพลาดในการบันทึกการกำหนดค่า: {{errorMessage}}" + }, + "semanticSearch": { + "modelSize": { + "small": { + "title": "เล็ก" + }, + "large": { + "title": "ใหญ่" + } + } + }, + "restart_required": "จำเป็นต้องรีสตาร์ท (การตั้งค่าการจำแนกมีการเปลี่ยนแปลง)" + }, + "camera": { + "review": { + "title": "รีวิว", + "alerts": "การแจ้งเตือน ", + "detections": "การเคลื่อนไหว " + }, + "reviewClassification": { + "readTheDocumentation": "อ่านเอกสาร" + } + }, + "masksAndZones": { + "zones": { + "name": { + "tips": "ชื่อต้องมีความยาวอย่างน้อย 2 อักขระ และต้องไม่ใช่ชื่อกล้องหรือโซนอื่น", + "title": "ชื่อ", + "inputPlaceHolder": "ใส่ชื่อ…" + }, + "add": "เพิ่มโซน", + "edit": "แก้โซน", + "point_other": "{{count}} จุด", + "speedEstimation": { + "docs": "อ่านเอกสาร" + } + }, + "motionMasks": { + "point_other": "{{count}} จุด", + "context": { + "documentation": "อ่านเอกสาร" + }, + "polygonAreaTooLarge": { + "documentation": "อ่านเอกสาร" + } + }, + "objectMasks": { + "point_other": "{{count}} จุด" + } + }, + "frigatePlus": { + "title": "การตั้งค่า Frigate+", + "modelInfo": { + "cameras": "กล้อง" + }, + "toast": { + "error": "ผิดพลาดในการบันทึกการกำหนดค่า: {{errorMessage}}" + }, + "snapshotConfig": { + "documentation": "อ่านเอกสาร" + } + }, + "debug": { + "objectShapeFilterDrawing": { + "document": "อ่านเอกสาร " + } + }, + "enrichments": { + "semanticSearch": { + "readTheDocumentation": "อ่านเอกสาร" + }, + "faceRecognition": { + "readTheDocumentation": "อ่านเอกสาร" + }, + "licensePlateRecognition": { + "readTheDocumentation": "อ่านเอกสาร" + } + } +} diff --git a/web/public/locales/th/views/system.json b/web/public/locales/th/views/system.json new file mode 100644 index 0000000..90e29a2 --- /dev/null +++ b/web/public/locales/th/views/system.json @@ -0,0 +1,80 @@ +{ + "general": { + "title": "ทั่วไป", + "hardwareInfo": { + "gpuInfo": { + "nvidiaSMIOutput": { + "driver": "ไดรเวอร์: {{driver}}", + "name": "ชื่อ: {{name}}" + }, + "copyInfo": { + "label": "คัดลอกข้อมูล GPU" + }, + "closeInfo": { + "label": "ปิดข้อมูล GPU" + }, + "toast": { + "success": "คัดลอกข้อมูล GPU ไปยังคลิปบอร์ดแล้ว" + } + }, + "gpuUsage": "การใช้งาน GPU", + "gpuMemory": "หน่วยความจํา GPU", + "title": "รายละเอียดของอุปกรณ์", + "gpuEncoder": "ใช้ GPU เข้ารหัส", + "gpuDecoder": "ใช้ GPU ถอดรหัส" + }, + "detector": { + "cpuUsage": "ตัวตรวจจับใช้งานหน่วยประมวลผลกลาง", + "title": "ตัวตรวจจับ", + "inferenceSpeed": "ความเร็วในการตรวจจับ", + "temperature": "อุณภูมิตัวตรวจจับ", + "memoryUsage": "ตัวตรวจจับใช้งานหน่วยความจำ" + } + }, + "enrichments": { + "embeddings": { + "face_recognition_speed": "ความเร็วในการจดจำใบหน้า", + "plate_recognition_speed": "ความเร็วในการจดจำป้าย", + "yolov9_plate_detection_speed": "ความเร็วในการตรวจจับป้าย ของ YOLOv9", + "face_recognition": "การจดจำใบหน้า", + "text_embedding_speed": "ความเร็วในการอ่านข้อความ", + "yolov9_plate_detection": "การตรวจจับป้าย ของ YOLOv9" + } + }, + "title": "ระบบ", + "storage": { + "cameraStorage": { + "unused": { + "title": "ไม่ได้ใช้" + }, + "storageUsed": "พื้นที่จัดเก็บ", + "title": "พื้นที่จัดเก็บกล้อง" + }, + "title": "พื้นที่จัดเก็บ" + }, + "stats": { + "cameraIsOffline": "{{camera}} ออฟไลน์", + "detectIsVerySlow": "{{detect}} ช้ามาก ({{speed}} มิลลิวินาที)" + }, + "documentTitle": { + "cameras": "ข้อมูลกล้อง - Frigate", + "storage": "สถิติคลังข้อมูล - Frigate", + "general": "สถิติทั่วไป - Frigate", + "enrichments": "สถิติเพิ่มเติม - Frigate", + "logs": { + "frigate": "Frigate Logs - Frigate", + "go2rtc": "Logs ของ Go2RTC - Frigate", + "nginx": "Logs ของ Nginx - Frigate", + "websocket": "ประวัติข้อความ - Frigate" + } + }, + "logs": { + "websocket": { + "pause": "พัก", + "clear": "ลบล้าง", + "filter": { + "all": "ทุกหัวข้อ" + } + } + } +} diff --git a/web/public/locales/tr/audio.json b/web/public/locales/tr/audio.json new file mode 100644 index 0000000..34a6f36 --- /dev/null +++ b/web/public/locales/tr/audio.json @@ -0,0 +1,503 @@ +{ + "mantra": "mantra", + "breathing": "nefes alma", + "snoring": "horlama", + "animal": "hayvan", + "dog": "köpek", + "laughter": "kahkaha", + "yell": "bağırma", + "cat": "kedi", + "whispering": "fısıldama", + "crying": "ağlama", + "bark": "havlama", + "speech": "Konuşma", + "bicycle": "bisiklet", + "horse": "at", + "goat": "keçi", + "mouse": "fare", + "keyboard": "klavye", + "vehicle": "araç", + "boat": "bot", + "car": "araba", + "bus": "otobüs", + "motorcycle": "motosiklet", + "skateboard": "kaykay", + "door": "kapı", + "blender": "mikser", + "sink": "lavabo", + "toothbrush": "diş fırçası", + "clock": "saat", + "scissors": "makas", + "bird": "kuş", + "sheep": "koyun", + "train": "tren", + "hair_dryer": "saç kurutma makinesi", + "babbling": "Agulama", + "snicker": "kıkırdama", + "sigh": "iç çekme", + "bellow": "haykırma", + "whoop": "nara", + "singing": "şarkı söyleme", + "choir": "koro", + "yodeling": "gırtlak naresi", + "grunt": "homurdanma", + "whistling": "ıslık", + "wheeze": "hırıltı", + "gasp": "kesik nefes", + "pant": "soluma", + "cough": "öksürük", + "throat_clearing": "boğaz temizleme", + "chatter": "gevezelik", + "crowd": "kalabalık", + "children_playing": "oynayan çocuklar", + "pets": "evcil hayvan", + "meow": "miyavlama", + "hiss": "tıslama", + "moo": "böğürme", + "cowbell": "inek çanı", + "fowl": "kümes hayvanı", + "chicken": "tavuk", + "cluck": "gıdaklama", + "quack": "vakvaklama", + "coo": "kumru sesi", + "crow": "karga", + "insect": "böcek", + "plucked_string_instrument": "çekmeli telli çalgı", + "guitar": "gitar", + "electric_guitar": "elektro gitar", + "strum": "pena vuruşu", + "organ": "org", + "electronic_organ": "elektronik org", + "hammond_organ": "hammond org", + "percussion": "vurmalı çalgı", + "drum_kit": "bateri", + "drum": "davul", + "gong": "gong", + "tubular_bells": "boru çanlar", + "mallet_percussion": "tokmaklı vurmalılar", + "brass_instrument": "bakır nefesli", + "flute": "flüt", + "church_bell": "kilise çanı", + "didgeridoo": "didgeridoo", + "theremin": "teremin", + "heavy_metal": "heavy metal", + "punk_rock": "punk rock", + "grunge": "grunge", + "reggae": "reggae", + "country": "country müzik", + "middle_eastern_music": "orta doğu müziği", + "jazz": "caz", + "trance_music": "trance müzik", + "music_of_latin_america": "latin amerika müziği", + "music_of_africa": "afrika müziği", + "afrobeat": "afrobeat", + "christian_music": "hristiyan müziği", + "gospel_music": "gospel", + "independent_music": "bağımsız müzik", + "wedding_music": "düğün müziği", + "happy_music": "mutlu müzik", + "scary_music": "korkutucu müzik", + "wind": "rüzgar", + "thunder": "gök gürültüsü", + "water": "su", + "rain": "yağmur", + "ocean": "okyanus", + "waves": "dalgalar", + "steam": "buhar", + "gurgling": "şırıltı", + "fire": "ateş", + "sailboat": "yelkenli", + "rowboat": "sandal", + "motorboat": "motorbot", + "toot": "korna sesi", + "race_car": "yarış arabası", + "ambulance": "ambulans", + "train_horn": "tren kornası", + "jet_engine": "jet motoru", + "accelerating": "hızlanma", + "doorbell": "kapı zili", + "ding-dong": "ding dong", + "cupboard_open_or_close": "dolap açma/kapama", + "drawer_open_or_close": "çekmece açma/kapama", + "dishes": "bulaşık", + "toilet_flush": "sifon çekme", + "zipper": "fermuar", + "shuffling_cards": "kart karıştırma", + "ringtone": "zil sesi", + "telephone_dialing": "numara çevirme", + "dial_tone": "çevir sesi", + "buzzer": "buzzer", + "mechanisms": "mekanizma", + "ratchet": "cırcır", + "tick": "tik", + "filing": "törpüleme", + "burst": "patlama", + "eruption": "püskürme", + "silence": "sessizlik", + "chant": "tezahürat", + "child_singing": "çocuk şarkısı", + "synthetic_singing": "sentetik şarkı", + "rapping": "rap", + "humming": "mırıldanma", + "groan": "inleme", + "snort": "burnundan soluma", + "sneeze": "hapşırma", + "sniff": "burun çekme", + "run": "koşma", + "shuffle": "ayak sürtme", + "footsteps": "adım sesleri", + "chewing": "çiğneme", + "biting": "ısırma", + "gargling": "gargara", + "stomach_rumble": "mide gurultusu", + "burping": "geğirme", + "hiccup": "hıçkırık", + "fart": "gaz çıkarma", + "hands": "el sesi", + "finger_snapping": "parmak şıklatma", + "clapping": "alkışlama", + "heartbeat": "kalp atışı", + "heart_murmur": "kalp üfürümü", + "cheering": "tezahürat", + "applause": "alkış", + "yip": "ince havlama", + "howl": "uluma", + "bow_wow": "havlama", + "growling": "hırlama", + "whimper_dog": "köpek sızlanması", + "purr": "mırlama", + "caterwaul": "kedi çığlığı", + "livestock": "çiftlik hayvanı", + "clip_clop": "nal sesi", + "neigh": "kişneme", + "cattle": "büyükbaş hayvan", + "pig": "domuz", + "oink": "domuz sesi", + "bleat": "meleme", + "cock_a_doodle_doo": "horoz ötüşü", + "turkey": "hindi", + "gobble": "hindi sesi", + "duck": "ördek", + "goose": "kaz", + "honk": "kaz sesi", + "wild_animals": "vahşi hayvan", + "roaring_cats": "kükreyen kedi", + "roar": "kükreme", + "chirp": "cıvıltı", + "squawk": "kuş çığlığı", + "pigeon": "güvercin", + "caw": "karga sesi", + "owl": "baykuş", + "hoot": "baykuş ötüşü", + "flapping_wings": "kanat çırpma", + "dogs": "köpekler", + "rats": "sıçanlar", + "patter": "tıkırtı", + "cricket": "cırcır böceği", + "mosquito": "sivrisinek", + "fly": "sinek", + "frog": "kurbağa", + "croak": "vraklama", + "snake": "yılan", + "rattle": "çıngırak", + "buzz": "vızıltı", + "whale_vocalization": "balina sesi", + "music": "müzik", + "musical_instrument": "müzik aleti", + "bass_guitar": "bas gitar", + "acoustic_guitar": "akustik gitar", + "steel_guitar": "steel gitar", + "tapping": "tıklatma", + "ukulele": "ukulele", + "banjo": "banjo", + "sitar": "sitar", + "mandolin": "mandolin", + "zither": "ziter", + "piano": "piyano", + "electric_piano": "elektro piyano", + "synthesizer": "sentezleyici", + "sampler": "örnekleyici", + "drum_machine": "davul makinesi", + "harpsichord": "klavsen", + "snare_drum": "trampet", + "rimshot": "rimşat", + "drum_roll": "davul geçişi", + "bass_drum": "bas davul", + "timpani": "timpani", + "maraca": "marakas", + "tabla": "tabla", + "cymbal": "zil", + "hi_hat": "hi-hat", + "wood_block": "tahta blok", + "tambourine": "tef", + "marimba": "marimba", + "glockenspiel": "glockenspiel", + "vibraphone": "vibrafon", + "steelpan": "steelpan", + "orchestra": "orkestra", + "french_horn": "korno", + "trumpet": "trompet", + "trombone": "trombon", + "bowed_string_instrument": "yaylı telli çalgı", + "string_section": "yaylılar", + "violin": "keman", + "pizzicato": "pizzicato", + "cello": "viyolonsel", + "double_bass": "kontrbas", + "wind_instrument": "nefesli çalgı", + "saxophone": "saksafon", + "clarinet": "klarnet", + "harp": "arp", + "bell": "çan", + "singing_bowl": "tibet çanağı", + "scratching": "tırmalama", + "pop_music": "pop müzik", + "bicycle_bell": "bisiklet zili", + "tuning_fork": "diyapazon", + "chime": "çan sesi", + "wind_chime": "rüzgar çanı", + "harmonica": "mızıka", + "accordion": "akordeon", + "bagpipes": "gayda", + "hip_hop_music": "hip hop müzik", + "beatboxing": "beatbox", + "rock_music": "rock müzik", + "progressive_rock": "progressive rock", + "rock_and_roll": "rock and roll", + "psychedelic_rock": "psychedelic rock", + "rhythm_and_blues": "rhythm and blues", + "soul_music": "soul müzik", + "swing_music": "swing müzik", + "bluegrass": "bluegrass", + "funk": "funk", + "folk_music": "halk müziği", + "disco": "disko", + "classical_music": "klasik müzik", + "opera": "opera", + "electronic_music": "elektronik müzik", + "house_music": "house müzik", + "techno": "tekno", + "dubstep": "dubstep", + "drum_and_bass": "drum and bass", + "electronica": "electronica", + "electronic_dance_music": "elektronik dans müziği", + "ambient_music": "ambient müzik", + "salsa_music": "salsa müziği", + "flamenco": "flamenko", + "blues": "blues", + "music_for_children": "çocuk müziği", + "new-age_music": "new age müzik", + "vocal_music": "vokal müzik", + "a_capella": "akapella", + "music_of_asia": "asya müziği", + "carnatic_music": "karnatik müzik", + "music_of_bollywood": "bollywood müziği", + "ska": "ska", + "traditional_music": "geleneksel müzik", + "song": "şarkı", + "background_music": "arka plan müziği", + "theme_music": "tema müziği", + "jingle": "jingle", + "soundtrack_music": "film müziği", + "lullaby": "ninni", + "video_game_music": "video oyunu müziği", + "christmas_music": "noel müziği", + "dance_music": "dans müziği", + "sad_music": "hüzünlü müzik", + "tender_music": "yumuşak müzik", + "exciting_music": "heyecanlı müzik", + "angry_music": "öfkeli müzik", + "rustling_leaves": "yaprak hışırtısı", + "wind_noise": "rüzgar gürültüsü", + "thunderstorm": "gök gürültülü fırtına", + "raindrop": "yağmur damlası", + "rain_on_surface": "yüzeye düşen yağmur", + "stream": "dere", + "waterfall": "şelale", + "crackle": "çıtırtı", + "ship": "gemi", + "motor_vehicle": "motorlu taşıt", + "car_alarm": "araç alarmı", + "power_windows": "otomatik cam", + "skidding": "kayma", + "tire_squeal": "lastik gıcırtısı", + "car_passing_by": "geçen araba", + "truck": "kamyon", + "air_brake": "havalı fren", + "air_horn": "havalı korna", + "reversing_beeps": "geri vites bip sesi", + "ice_cream_truck": "dondurma kamyonu", + "emergency_vehicle": "acil durum aracı", + "police_car": "polis arabası", + "fire_engine": "itfaiye aracı", + "traffic_noise": "trafik gürültüsü", + "rail_transport": "raylı ulaşım", + "train_whistle": "tren düdüğü", + "railroad_car": "vagon", + "fixed-wing_aircraft": "sabit kanatlı uçak", + "train_wheels_squealing": "tren tekeri gıcırtısı", + "subway": "metro", + "aircraft": "hava aracı", + "aircraft_engine": "uçak motoru", + "propeller": "pervane", + "helicopter": "helikopter", + "engine": "motor", + "light_engine": "hafif motor", + "dental_drill's_drill": "dişçi matkabı", + "lawn_mower": "çim biçme makinesi", + "chainsaw": "motorlu testere", + "medium_engine": "orta motor", + "heavy_engine": "ağır motor", + "engine_knocking": "motor vuruntusu", + "engine_starting": "motor çalıştırma", + "idling": "rölanti", + "sliding_door": "sürgülü kapı", + "slam": "kapı çarpma", + "knock": "kapı çalma", + "tap": "tıklatma", + "squeak": "gıcırtı", + "cutlery": "çatal bıçak", + "chopping": "doğrama", + "frying": "kızartma", + "microwave_oven": "mikrodalga fırın", + "water_tap": "musluk", + "bathtub": "küvet", + "electric_toothbrush": "elektrikli diş fırçası", + "vacuum_cleaner": "elektrik süpürgesi", + "keys_jangling": "anahtar şıngırtısı", + "coin": "madeni para", + "electric_shaver": "tıraş makinesi", + "typing": "klavyede yazma", + "typewriter": "daktilo", + "computer_keyboard": "bilgisayar klavyesi", + "writing": "yazma", + "alarm": "alarm", + "telephone": "telefon", + "telephone_bell_ringing": "telefon çalması", + "busy_signal": "meşgul sinyali", + "alarm_clock": "çalar saat", + "siren": "siren", + "civil_defense_siren": "sivil savunma sireni", + "smoke_detector": "duman dedektörü", + "fire_alarm": "yangın alarmı", + "foghorn": "sis düdüğü", + "whistle": "düdük", + "steam_whistle": "buhar düdüğü", + "tick-tock": "tik tak", + "gears": "dişliler", + "pulleys": "makaralar", + "sewing_machine": "dikiş makinesi", + "mechanical_fan": "mekanik vantilatör", + "air_conditioning": "klima", + "cash_register": "yazar kasa", + "printer": "yazıcı", + "camera": "kamera", + "single-lens_reflex_camera": "slr kamera", + "tools": "aletler", + "hammer": "çekiç", + "jackhammer": "hilti", + "sawing": "testere ile kesme", + "sanding": "zımparalama", + "power_tool": "elektrikli alet", + "drill": "matkap", + "explosion": "patlama", + "gunshot": "silah sesi", + "machine_gun": "makineli tüfek", + "fusillade": "yaylım ateşi", + "artillery_fire": "topçu ateşi", + "cap_gun": "mantar tabancası", + "fireworks": "havai fişek", + "firecracker": "torpil", + "boom": "gümbürtü", + "wood": "ahşap", + "chop": "kesme", + "splinter": "parçalanma", + "crack": "çatlama", + "glass": "cam", + "chink": "şıngırtı", + "shatter": "kırılma", + "sound_effect": "ses efekti", + "environmental_noise": "çevre gürültüsü", + "static": "parazit", + "white_noise": "beyaz gürültü", + "pink_noise": "pembe gürültü", + "television": "televizyon", + "radio": "radyo", + "field_recording": "alan kaydı", + "scream": "çığlık", + "jingle_bell": "küçük çan", + "sodeling": "Jodel (Yodeling)", + "chird": "Cıvıltı", + "change_ringing": "Sıralı Çan Çalma", + "shofar": "Şofar", + "liquid": "Sıvı", + "splash": "Su Sıçraması", + "slosh": "Çalkalanma", + "squish": "Vıcıklama (Islak Ezilme)", + "drip": "Damlama", + "pour": "Dökülme", + "trickle": "Şırıldama / İnce Akış", + "gush": "Fışkırma", + "fill": "Doldurma", + "spray": "Püskürtme / Sprey", + "pump": "Pompalama", + "stir": "Karıştırma", + "boiling": "Kaynama", + "sonar": "Sonar Sesi", + "arrow": "Ok Sesi", + "whoosh": "Hışırtı (Hızlı Geçiş Sesi)", + "thump": "Küt Sesi (Boğuk)", + "thunk": "Tok Ses", + "electronic_tuner": "Elektronik Akort Cihazı", + "effects_unit": "Efekt Ünitesi", + "chorus_effect": "Chorus (Koro) Efekti", + "basketball_bounce": "Basketbol Topu Sektirme", + "bang": "Gümleme / Patlama", + "slap": "Tokat / Şaplak", + "whack": "Sert Vuruş / Kütletme", + "smash": "Parçalanma", + "breaking": "Kırılma", + "bouncing": "Sekme / Zıplama", + "whip": "Kırbaç", + "flap": "Kanat Çırpma / Pırpır Etme", + "scratch": "Tırmalama / Cızırtı", + "scrape": "Kazıma / Sürtünme", + "rub": "Ovma / Sürtme", + "roll": "Yuvarlanma", + "crushing": "Ezilme (Kuru/Sert)", + "crumpling": "Buruşturma", + "tearing": "Yırtılma", + "beep": "Bip Sesi", + "ping": "Ping Sesi (Çınlama)", + "ding": "Ding (Zil Sesi)", + "clang": "Çangırtı (Metalik)", + "squeal": "Ciyaklama / Acı Gıcırtı", + "creak": "Gıcırdama (Tahta/Kapı)", + "rustle": "Hışırtı (Kağıt/Yaprak)", + "whir": "Vızıltı (Motor/Pervane)", + "clatter": "Takırtı", + "sizzle": "Cızırdayarak Kızarma", + "clicking": "Tıklama", + "clickety_clack": "Takır Tukur Sesi", + "rumble": "Gürleme / Gümbürtü", + "plop": "Lup Sesi (Suya düşme)", + "hum": "Uğultu / Mırıldanma", + "zing": "Vınlama", + "boing": "Boing (Yay Sesi)", + "crunch": "Kıtırdatma / Çıtırdatma", + "sine_wave": "Sinüs Dalgası", + "harmonic": "Harmonik", + "chirp_tone": "Cıvıltı Tonu (Sinyal)", + "pulse": "Darbe / Pulse", + "inside": "İç Mekan", + "outside": "Dış Mekan", + "reverberation": "Yankılanım (Reverb)", + "echo": "Yankı", + "noise": "Gürültü", + "mains_hum": "Şebeke Uğultusu (Elektrik)", + "distortion": "Bozulma / Distorsiyon", + "sidetone": "Yan Ton", + "cacophony": "Kakofoni (Ses Kargaşası)", + "throbbing": "Zonklama", + "vibration": "Titreşim" +} diff --git a/web/public/locales/tr/common.json b/web/public/locales/tr/common.json new file mode 100644 index 0000000..50ea2c6 --- /dev/null +++ b/web/public/locales/tr/common.json @@ -0,0 +1,334 @@ +{ + "time": { + "lastWeek": "Geçen Hafta", + "thisWeek": "Bu Hafta", + "lastMonth": "Geçen Ay", + "untilForRestart": "Frigate yeniden başlatılana kadar.", + "untilRestart": "Yeniden başlatmaya kadar", + "ago": "{{timeAgo}} önce", + "justNow": "Az önce", + "today": "Bugün", + "yesterday": "Dün", + "last7": "Son 7 gün", + "last30": "Son 30 gün", + "thisMonth": "Bu Ay", + "5minutes": "5 dakika", + "10minutes": "10 dakika", + "30minutes": "30 dakika", + "1hour": "1 saat", + "12hours": "12 saat", + "last14": "Son 14 gün", + "24hours": "24 saat", + "formattedTimestamp": { + "24hour": "d MMM, HH:mm:ss", + "12hour": "d MMM, h:mm:ss aaa" + }, + "formattedTimestamp2": { + "24hour": "d MMM HH:mm:ss", + "12hour": "dd/MM h:mm:ssa" + }, + "second_one": "{{time}} saniye", + "second_other": "{{time}} saniye", + "year_one": "{{time}} yıl", + "year_other": "{{time}} yıl", + "hour_one": "{{time}} saat", + "hour_other": "{{time}} saat", + "h": "{{time}} s", + "yr": "{{time}} yıl", + "mo": "{{time}} ay", + "untilForTime": "{{time}} saatine kadar", + "pm": "ÖS", + "am": "ÖÖ", + "d": "{{time}} gün", + "day_one": "{{time}} gün", + "day_other": "{{time}} gün", + "m": "{{time}}d", + "minute_one": "{{time}} dakika", + "minute_other": "{{time}} dakika", + "formattedTimestampWithYear": { + "12hour": "%-d %b %Y, %I:%M %p", + "24hour": "%-d %b %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%-d %b", + "formattedTimestampExcludeSeconds": { + "12hour": "%-d %b, %I:%M %p", + "24hour": "%-d %b, %H:%M" + }, + "s": "{{time}}sn", + "month_one": "{{time}} ay", + "month_other": "{{time}} ay", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "inProgress": "Devam ediyor", + "invalidStartTime": "Geçersiz başlangıç zamanı", + "invalidEndTime": "Geçersiz bitiş zamanı", + "never": "Asla" + }, + "button": { + "off": "KAPALI", + "next": "Sonraki", + "saving": "Kaydediliyor…", + "back": "Geri", + "unselect": "Seçimi kaldır", + "info": "Bilgi", + "enable": "Aç", + "disabled": "Kapalı", + "disable": "Kapat", + "history": "Geçmiş", + "cameraAudio": "Kamera Sesi", + "on": "AÇIK", + "suspended": "Askıya alınmış", + "unsuspended": "Askıdan çıkart", + "export": "Dışa aktar", + "download": "İndir", + "edit": "Düzenle", + "fullscreen": "Tam Ekran", + "deleteNow": "Şimdi Sil", + "apply": "Uygula", + "reset": "Sıfırla", + "done": "Bitti", + "enabled": "Açık", + "save": "Kaydet", + "exitFullscreen": "Tam Ekrandan Çık", + "pictureInPicture": "Pencere içinde pencere", + "copyCoordinates": "Koordinatları kopyala", + "yes": "Evet", + "play": "Oynat", + "no": "Hayır", + "copy": "Kopyala", + "cancel": "İptal", + "twoWayTalk": "Çift Yönlü Ses", + "close": "Kapat", + "delete": "Sil", + "continue": "Devam Et", + "add": "Ekle", + "applying": "Uygulanıyor…", + "undo": "Geri al", + "copiedToClipboard": "Panoya kopyaladı", + "modified": "Değiştirilmiş", + "overridden": "Üstüne yazılmış", + "resetToGlobal": "Genele sıfırla", + "resetToDefault": "Varsayılana sıfırla", + "saveAll": "Hepsini Kaydet", + "savingAll": "Hepsi Kaydediliyor…", + "undoAll": "Hepsini Geri Al", + "retry": "Yeniden dene" + }, + "menu": { + "systemLogs": "Sistem günlükleri", + "user": { + "anonymous": "anonim", + "account": "Hesap", + "current": "Mevcut kullanıcı: {{user}}", + "setPassword": "Parola Belirle", + "logout": "Oturumu Kapat", + "title": "Kullanıcı" + }, + "configuration": "Yapılandırma", + "languages": "Diller", + "language": { + "en": "İngilizce", + "zhCN": "简体中文 (Basitleştirilmiş Çince)", + "withSystem": { + "label": "Dil için sistem tercihini kullan" + }, + "hi": "हिन्दी (Hintçe)", + "fr": "Français (Fransızca)", + "pt": "Português (Portekizce)", + "de": "Deutsch (Almanca)", + "ja": "日本語 (Japonca)", + "tr": "Türkçe (Türkçe)", + "it": "Italiano (İtalyanca)", + "nl": "Nederlands (Felemenkçe)", + "sv": "Svenska (İsveççe)", + "cs": "Čeština (Çekçe)", + "nb": "Norsk Bokmål (Bokmål Norveç Dili)", + "ko": "한국어 (Korece)", + "vi": "Tiếng Việt (Vietnamca)", + "pl": "Polski (Lehçe)", + "uk": "Українська (Ukraynaca)", + "he": "עברית (İbranice)", + "el": "Ελληνικά (Yunanca)", + "ro": "Română (Rumence)", + "hu": "Magyar (Macarca)", + "fi": "Suomi (Fince)", + "da": "Dansk (Danimarka Dili)", + "sk": "Slovenčina (Slovakça)", + "fa": "فارسی (Farsça)", + "es": "Español (İspanyolca)", + "ar": "العربية (Arapça)", + "ru": "Русский (Rusça)", + "yue": "粵語 (Kantonca)", + "th": "ไทย (Tayca)", + "ca": "Català (Katalanca)", + "ptBR": "Português brasileiro (Brezilya Portekizcesi)", + "sr": "Српски (Sırpça)", + "sl": "Slovenščina (Slovence)", + "lt": "Lietuvių (Litvanyaca)", + "bg": "Български (Bulgarca)", + "gl": "Galego (Galiçyaca)", + "id": "Bahasa Indonesia (Endonezce)", + "ur": "اردو (Urduca)", + "hr": "Hrvatski (Hırvatça)", + "zhHant": "繁體中文 (Geleneksel Çince)", + "bs": "Bosanski (Boşnakça)" + }, + "withSystem": "Sistem", + "theme": { + "label": "Tema", + "blue": "Mavi", + "contrast": "Yüksek Karşıtlık", + "green": "Yeşil", + "red": "Kırmızı", + "default": "Varsayılan", + "nord": "Kuzey", + "highcontrast": "Yüksek Karşıtlık" + }, + "restart": "Frigate'i yeniden başlat", + "live": { + "title": "Canlı", + "allCameras": "Tüm Kameralar", + "cameras": { + "title": "Kameralar", + "count_one": "{{count}} Kamera", + "count_other": "{{count}} Kamera" + } + }, + "review": "İncele", + "explore": "Keşfet", + "system": "Sistem", + "documentation": { + "title": "Dökümantasyon", + "label": "Frigate dökümantasyonu" + }, + "settings": "Ayarlar", + "appearance": "Görünüm", + "darkMode": { + "label": "Karanlık Mod", + "withSystem": { + "label": "Karanlık tema için sistem tercihini kullan" + }, + "light": "Açık", + "dark": "Koyu" + }, + "export": "Dışa Aktar", + "configurationEditor": "Yapılandırma düzenleyicisi", + "help": "Yardım", + "faceLibrary": "Yüz Veritabanı", + "systemMetrics": "Sistem metrikleri", + "uiPlayground": "UI Deneme Alanı", + "classification": "Sınıflandırma", + "profiles": "Profiller", + "actions": "Eylemler", + "chat": "Sohbet", + "features": "Özellikler" + }, + "label": { + "back": "Geri", + "hide": "{{item}} öğesini gizle", + "show": "{{item}} öğesini göster", + "ID": "ID", + "none": "Hiçbiri", + "all": "Tümü", + "other": "Diğer" + }, + "notFound": { + "documentTitle": "Bulunamadı - Frigate", + "desc": "Sayfa bulunamadı", + "title": "404" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/s" + }, + "length": { + "feet": "feet", + "meters": "metre" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/saat", + "mbph": "MB/saat", + "gbph": "GB/saat" + } + }, + "pagination": { + "next": { + "title": "Sonraki", + "label": "Sonraki sayfaya git" + }, + "previous": { + "label": "Önceki sayfaya git", + "title": "Önceki" + }, + "label": "sayfalandırma", + "more": "Daha fazla" + }, + "accessDenied": { + "title": "Erişim Reddedildi", + "desc": "Bu sayfayı görüntüleme yetkiniz yok.", + "documentTitle": "Erişim Reddedildi - Frigate" + }, + "toast": { + "copyUrlToClipboard": "URL panoya kopyalandı.", + "save": { + "title": "Kaydet", + "error": { + "noMessage": "Yapılandırma değişiklikleri kaydedilemedi", + "title": "Yapılandırma değişiklikleri kaydedilemedi: {{errorMessage}}" + }, + "success": "Yapılandırma değişiklikleri kaydedildi." + } + }, + "selectItem": "{{item}} seçin", + "role": { + "title": "Rol", + "viewer": "Görüntüleyici", + "admin": "Yönetici", + "desc": "Yöneticiler Frigate arayüzündeki bütün özelliklere tam erişim sahibidir. Görüntüleyiciler ise yalnızca kameraları, eski görüntüleri ve inceleme öğelerini görüntülemekle sınırlıdır." + }, + "readTheDocumentation": "Dökümantasyonu oku", + "list": { + "two": "{{0}} ve {{1}}", + "many": "{{items}} ve {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "İsteğe bağlı", + "internalID": "Frigate’ın yapılandırma ve veritabanında kullandığı Dahili Kimlik" + }, + "information": { + "pixels": "{{area}}px" + }, + "no_items": "Öge bulunmuyor", + "validation_errors": "Doğrulama Hataları", + "credentialField": { + "savedPlaceholder": "Kaydedildi — mevcut haliyle kalması için boş bırakın" + } +} diff --git a/web/public/locales/tr/components/auth.json b/web/public/locales/tr/components/auth.json new file mode 100644 index 0000000..b66836b --- /dev/null +++ b/web/public/locales/tr/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Parola", + "login": "Oturum Aç", + "errors": { + "webUnknownError": "Bilinmeyen hata. Konsol günlüklerini kontrol edin.", + "usernameRequired": "Kullanıcı adı gereklidir", + "loginFailed": "Oturum açma başarısız", + "passwordRequired": "Parola gereklidir", + "rateLimit": "İstek sınırı aşıldı. Daha sonra tekrar deneyin.", + "unknownError": "Bilinmeyen hata. Günlükleri kontrol edin." + }, + "user": "Kullanıcı Adı", + "firstTimeLogin": "İlk kez giriş yapmayı mı deniyorsunuz? Giriş bilgileri Frigate günlüklerinde görüntülenir." + } +} diff --git a/web/public/locales/tr/components/camera.json b/web/public/locales/tr/components/camera.json new file mode 100644 index 0000000..2076c0b --- /dev/null +++ b/web/public/locales/tr/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "name": { + "placeholder": "Bir isim girin…", + "label": "İsim", + "errorMessage": { + "nameMustNotPeriod": "Kamera grubu ismi nokta içeremez.", + "invalid": "Geçersiz kamera grubu ismi.", + "mustLeastCharacters": "Kamera grubu ismi en az iki karakterden oluşmalıdır.", + "exists": "Bu isimle bir kamera grubu zaten var." + } + }, + "success": "{{name}} adlı kamera grubu kaydedildi.", + "camera": { + "setting": { + "title": "{{cameraName}} Yayın Ayarları", + "audio": { + "tips": { + "document": "Dökümantasyonu oku ", + "title": "Bu yayın için kameranızın yayın çıkışında ses olması ve go2rtc'de ayarlanmış olması gerekmektedir." + } + }, + "label": "Kamera Yayın Ayarları", + "desc": "Bu kamera grubunun kontrol paneli için canlı yayın seçeneklerini değiştirin. Bu ayarlar cihaz/tarayıcı özelindedir.", + "audioIsUnavailable": "Bu yayında ses yok", + "audioIsAvailable": "Bu yayında ses kullanılabilir", + "streamMethod": { + "label": "Yayın Yöntemi", + "method": { + "noStreaming": { + "label": "Yayın Yok", + "desc": "Kamera görüntüsü dakikda bir güncellenecektir ve canlı yayın yapılmayacaktır." + }, + "continuousStreaming": { + "desc": { + "title": "Kamera görüntüsü panelde görüldüğü sürece, kamerada aktivite olmasa bile, canlı yayın şeklinde olacaktır.", + "warning": "Sürekli yayın yüksek internet kullanımına ve performans sorunlarına yol açabilir. Dikkatli kullanın." + }, + "label": "Sürekli Yayın" + }, + "smartStreaming": { + "label": "Akıllı yayın (önerilir)", + "desc": "Akıllı yayın özelliği, internet ve diğer kaynaklardan tasarruf için aktivite yokken yayının yerine dakikada bir güncellenen sabit resim gösterir. Kamerada aktivite tespit edildiğinde görüntü sabit resimden canlı yayına geçer." + } + }, + "placeholder": "Bir yayın metodu seçin" + }, + "compatibilityMode": { + "label": "Uyumluluk modu", + "desc": "Bu özelliği sadece kamera akışında renkli mozaiklenme yahut resmin sağ tarafında çizgi görüyorsanız etkinleştirin." + }, + "placeholder": "Bir yayın seçin", + "stream": "Yayın" + }, + "birdseye": "Kuş Bakışı" + }, + "icon": "Simge", + "add": "Kamera Grubu Ekle", + "label": "Kamera Grupları", + "delete": { + "confirm": { + "desc": "{{name}} isimli kamera grubunu silmek istediğinizden emin misiniz?", + "title": "Silmeyi Onayla" + }, + "label": "Kamera Grubunu Sil" + }, + "edit": "Kamera Grubunu Düzenle", + "cameras": { + "desc": "Bu gruba dahil olacak kameraları seçin.", + "label": "Kameralar" + }, + "showAll": "Bütün Kamera Gruplarını Göster", + "showLess": "Daha Az", + "editGroups": "Kamera Gruplarını Düzenle" + }, + "debug": { + "options": { + "label": "Ayarlar", + "title": "Seçenekler", + "hideOptions": "Seçenekleri Gizle", + "showOptions": "Seçenekleri Göster" + }, + "boundingBox": "Çerçeve", + "timestamp": "Zaman Damgası", + "zones": "Alanlar", + "mask": "Maske", + "motion": "Hareket", + "regions": "Tespit Bölgeleri", + "paths": "Yollar" + } +} diff --git a/web/public/locales/tr/components/dialog.json b/web/public/locales/tr/components/dialog.json new file mode 100644 index 0000000..323fe5d --- /dev/null +++ b/web/public/locales/tr/components/dialog.json @@ -0,0 +1,206 @@ +{ + "restart": { + "title": "Frigate'i yeniden başlatmak istediğinize emin misiniz?", + "button": "Yeniden Başlat", + "restarting": { + "content": "Bu sayfa {{countdown}} saniye sonra yeniden yüklenecektir.", + "title": "Frigate Yeniden Başlatılıyor", + "button": "Şimdi Yeniden Yükle" + }, + "description": "Bu Frigate'i kısa süreliğine yeniden başlayana kadar durduracak." + }, + "explore": { + "plus": { + "review": { + "state": { + "submitted": "Gönderildi" + }, + "true": { + "true_one": "Bu bir {{label}}", + "true_other": "Bu bir {{label}}", + "label": "Frigate+ için bu etiketi onaylayın" + }, + "false": { + "label": "Bu etiketi Frigate+ için onaylamaktan vazgeç", + "false_one": "Bu bir {{label}} değil", + "false_other": "Bu bir {{label}} değil" + }, + "question": { + "ask_an": "Bu nesne bir {{label}} nesnesi mi?", + "label": "Bu etiketi Frigate+ için onaylayın", + "ask_a": "Bu nesne bir {{label}} mi?", + "ask_full": "Bu nesne bir {{untranslatedLabel}} ({{translatedLabel}}) nesnesi mi?" + } + }, + "submitToPlus": { + "label": "Frigate+'ya Gönder", + "desc": "Görülmesini istemediğiniz yerlerdeki nesneler yanlış pozitif değildir. Bunları yanlış pozitif olarak göndermek modeli yanıltacaktır." + } + }, + "video": { + "viewInHistory": "Geçmiş Görünümünde Görüntüle" + } + }, + "export": { + "time": { + "end": { + "label": "Bitiş Zamanını Seç", + "title": "Bitiş Zamanı" + }, + "lastHour_one": "Son 1 Saat", + "lastHour_other": "Son {{count}} Saat", + "start": { + "title": "Başlangıç Zamanı", + "label": "Başlangıç Zamanını Seç" + }, + "fromTimeline": "Zaman Şeridinde Seç", + "custom": "Özel" + }, + "select": "Seç", + "export": "Dışa Aktar", + "selectOrExport": "Seç veya Dışa Aktar", + "toast": { + "success": "Dışa aktarma başarıyla başlatıldı. Dosyayı dışa aktarmalar sayfasında görüntüleyebilirsiniz.", + "error": { + "failed": "Dışa aktarım başlatılamadı: {{error}}", + "endTimeMustAfterStartTime": "Bitiş zamanı başlangıç zamanından sonra olmalıdır", + "noVaildTimeSelected": "Geçerli bir zaman aralığı seçilmedi" + }, + "view": "Görüntüle", + "queued": "Dışa aktarımlar kuyruğa alındı. İlerlemeyi dışa aktarım sayfasından görebilirsiniz.", + "batchSuccess_one": "1 Adet dışa aktarım başlatıldı. Durum açılıyor.", + "batchSuccess_other": "{{count}} Adet dışa aktarım başlatıldı. Durum açılıyor.", + "batchPartial": "{{total}} üzerinden {{successful}} adet dışa aktarım başlatıldı. Başarısız: {{failedCameras}}", + "batchFailed": "{{total}} dışa aktarma işlemi başlatılamadı. Başarısız kameralar: {{failedCameras}}", + "batchQueuedSuccess_one": "1 dışa aktarım sıraya alındı.", + "batchQueuedSuccess_other": "{{count}} dışa aktarım sıraya alındı.", + "batchQueuedPartial": "{{total}} dışa aktarımdan {{successful}} tanesi sıraya alındı. Başarısız olan kameralar: {{failedCameras}}", + "batchQueueFailed": "{{total}} adet dışa aktarım sıraya alınamadı. Başarısız kameralar: {{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "Dışa Aktarımı Kaydet", + "previewExport": "Dışa Aktarımı Önizle", + "useThisRange": "Use This Range", + "queueingExport": "Dışa aktarma sıraya alınıyor..." + }, + "name": { + "placeholder": "Dışa Aktarımı Adlandırın" + }, + "case": { + "newCaseOption": "Yeni durum oluştur", + "newCaseNamePlaceholder": "Yeni durum ismi", + "newCaseDescriptionPlaceholder": "Durum açıklaması", + "label": "Durum", + "nonAdminHelp": "Bu dışa aktarımlar için yeni durumlar oluşturulacak.", + "placeholder": "Durum seç" + }, + "queueing": "Dışa aktarımlar kuyruğa alınıyor...", + "tabs": { + "export": "Tek Kamera", + "multiCamera": "Çoklu Kamera" + }, + "multiCamera": { + "timeRange": "Zaman Aralığı", + "selectFromTimeline": "Zaman çizelgesinden seç", + "cameraSelection": "Kameralar", + "cameraSelectionHelp": "Bu zaman aralığında ki obje takibi olan kameralar önceden seçildi", + "checkingActivity": "Kamera faliyeti kontrol ediliyor...", + "noCameras": "Kamera mevcut değil", + "detectionCount_one": "1 adet takip edilen obje", + "detectionCount_other": "{{count}} adet takip edilen obje", + "nameLabel": "Dışa aktarım ismi", + "namePlaceholder": "Dışa aktarım için opsiyonel temel isim", + "queueingButton": "Dışa aktarımlar kuyruğa alınıyor...", + "exportButton_one": "1 Adet Kamera Dışarı Aktarıldı", + "exportButton_other": "{{count}} Adet Kamera Dışarı Aktarıldı", + "searchOrSelectGroup": "Kamera grubu arayın veya seçin...", + "selectAll": "Tüm kameraları seç", + "clearSelection": "Seçimi kaldır", + "selectWithActivity": "Nesne takibi yapan kameralar", + "selectGroup": "Grup seç", + "noMatchingCameras": "Arama kriterlerinize uyan kamera bulunamadı", + "selectedCount": "{{selected}} / {{total}} seçilen" + }, + "multi": { + "title_one": "1 Adet Değerlendirme Dışarı Aktarıldı", + "title_other": "{{count}} Adet Değerlendirme Dışarı Aktarıldı", + "description": "Seçilmiş değerlendirmeleri tek tek dışa aktarın. Bütün dışa aktarımlar tek bir durum altında toplanacak.", + "descriptionNoCase": "Seçilmiş değerlendirmeleri tek tek dışa aktar.", + "caseNamePlaceholder": "Değerlendirme dışa aktarımı - {{date}}", + "exportButton_one": "1 Adet Değerlendirme'yi dışa akatar", + "exportButton_other": "{{count}} Adet Değerlendirme'yi dışa akatar", + "exportingButton": "Dışa aktarılıyor...", + "toast": { + "started_one": "1 Adet dışa aktarın başladı. Durum açılıyor.", + "started_other": "{{count}} Adet dışa aktarın başladı. Durum açılıyor.", + "startedNoCase_one": "1 Adet dışa aktarım başladı.", + "startedNoCase_other": "{{count}} Adet dışa aktarım başladı.", + "partial": "{{total}} üzerinden {{successful}} dışa aktarıldı. Başarısız: {{failedItems}}", + "failed": "{{total}} Adet dışa aktarım başarısız oldu. Başarısız: {{failedItems}}" + } + } + }, + "streaming": { + "label": "Akış", + "restreaming": { + "disabled": "Bu kamera için Yeniden Akış devre dışı.", + "desc": { + "readTheDocumentation": "Dökümantasyonu oku", + "title": "Bu kameradan ek canlı gösterim seçenekleri ve sesli yayın almak için go2rtc'yi yapılandırın." + } + }, + "showStats": { + "label": "Akış istatistiklerini göster", + "desc": "Kamera akışının üzerinde akış istastistiklerini görmek için bu seçeneği aktifleştirin." + }, + "debugView": "Hata Ayıklama Görünümü" + }, + "search": { + "saveSearch": { + "desc": "Kayıtlı aramaya bir isim verin.", + "placeholder": "Aramanız için bir isim girin", + "overwrite": "{{searchName}} zaten var. Bu isimle kaydetmek mevcut olanın üzerine yazacaktır.", + "label": "Aramayı Kaydet", + "button": { + "save": { + "label": "Bu aramayı kaydet" + } + }, + "success": "Arama ({{searchName}}) kaydedildi." + } + }, + "recording": { + "confirmDelete": { + "title": "Silmeyi Onayla", + "desc": { + "selected": "Bu inceleme öğesiyle ilişkili tüm kaydedilmiş videoları silmek istediğinizden emin misiniz?

    Gelecekte bu diyaloğu pas geçmek için Shift tuşuna basılı tutarak tıklatın." + }, + "toast": { + "success": "Seçili incele öğelerinin ilişkili olduğu video kayıtları başarıyla silinmiştir.", + "error": "Siliemedi: {{error}}" + } + }, + "button": { + "export": "Dışa Aktar", + "markAsReviewed": "İncelendi olarak işaretle", + "deleteNow": "Şimdi Sil", + "markAsUnreviewed": "Gözden geçirilmedi olarak işaretle" + }, + "shareTimestamp": { + "label": "Zaman Damgasını Paylaş", + "title": "Zaman Damgasını Paylaş", + "description": "Mevcut oynatıcı konumuna ait zaman damgalı bir URL paylaşın veya özel bir zaman damgası seçin. Bunun herkese açık bir paylaşım URL'si olmadığını ve yalnızca Frigate ile bu kameraya erişimi olan kullanıcılar tarafından görüntülenebileceğini unutmayın.", + "custom": "Özel Zaman Damgası", + "button": "Zaman Damgalı URL'yi Paylaş", + "shareTitle": "Frigate İnceleme Zaman Damgası: {{camera}}" + } + }, + "imagePicker": { + "selectImage": "Takip edilen nesnenin küçük resmini seçin", + "noImages": "Bu kamera için küçük resim bulunamadı", + "search": { + "placeholder": "Etiket/alt etiket kullanarak arama yapın..." + }, + "unknownLabel": "Kaydedilen Tetikleme Görseli" + } +} diff --git a/web/public/locales/tr/components/filter.json b/web/public/locales/tr/components/filter.json new file mode 100644 index 0000000..ef178d4 --- /dev/null +++ b/web/public/locales/tr/components/filter.json @@ -0,0 +1,141 @@ +{ + "labels": { + "label": "Etiketler", + "all": { + "title": "Bütün Etiketler", + "short": "Etiketler" + }, + "count": "{{count}} Etiket", + "count_one": "{{count}} Etiket", + "count_other": "{{count}} Etiket" + }, + "dates": { + "all": { + "title": "Tüm Tarihler", + "short": "Tarihler" + }, + "selectPreset": "Ön ayar seçin…" + }, + "sort": { + "label": "Sırala", + "dateAsc": "Tarih (Artan)", + "dateDesc": "Tarih (Azalan)", + "scoreAsc": "Nesne Skoru (Artan)", + "scoreDesc": "Nesne Skoru (Azalan)", + "speedAsc": "Tahmini Hız (Artan)", + "relevance": "Alaka", + "speedDesc": "Tahmini Hız (Azalan)" + }, + "filter": "Filtre", + "zones": { + "all": { + "short": "Alanlar", + "title": "Bütün Alanlar" + }, + "label": "Alanlar" + }, + "reset": { + "label": "Filtreleri varsayılanlara sıfırla" + }, + "features": { + "submittedToFrigatePlus": { + "tips": "Öncelikle izlenen nesneler içinde fotoğrafı olanlar için filtre uygulamalısınız.

    Fotoğrafı olmayan nesneler Frigate+’a gönderilemez.", + "label": "Frigate+'a Gönderildi" + }, + "hasVideoClip": "Video klibi var", + "hasSnapshot": "Fotoğrafı var", + "label": "Özellikler" + }, + "score": "Skor", + "estimatedSpeed": "Tahmini Hız ({{unit}})", + "timeRange": "Zaman Aralığı", + "subLabels": { + "all": "Tüm Alt Etiketler", + "label": "Alt Etiketler" + }, + "more": "Daha Fazla Filtre", + "cameras": { + "all": { + "short": "Kameralar", + "title": "Tüm Kameralar" + }, + "label": "Kameraları Filtrele" + }, + "review": { + "showReviewed": "İncelenenleri de Göster" + }, + "explore": { + "settings": { + "defaultView": { + "summary": "Özet", + "title": "Varsayılan Görünüm", + "unfilteredGrid": "Filtresiz Izgara", + "desc": "Filtre seçilmediğinde, her etiket için en son izlenen nesnelerin özeti ya da filtresiz ızgara görünümü gösterilir." + }, + "gridColumns": { + "title": "Izgara Sütun Sayısı", + "desc": "Izgara görünümüde gösterilecek sütun sayısı." + }, + "title": "Ayarlar", + "searchSource": { + "options": { + "thumbnailImage": "Küçük Resim", + "description": "Metin Açıklaması" + }, + "label": "Arama kaynağı", + "desc": "Aramanızda küçük resimleri mi yoksa açıklamaları mı kullanacağınızı seçin." + } + }, + "date": { + "selectDateBy": { + "label": "Filtrelemek için tarih seçin" + } + } + }, + "logSettings": { + "filterBySeverity": "Önceliğe göre günlükleri filtrele", + "disableLogStreaming": "Günlük akışını devre dışı bırak", + "allLogs": "Tüm günlükler", + "loading": { + "title": "Günlük Akışı", + "desc": "Günlükler sayfası en aşağıya kaydırıldığında yeni günlük satırları geldikçe aşağıya eklenir." + }, + "label": "Düzeye göre günlükleri filtrele" + }, + "trackedObjectDelete": { + "toast": { + "success": "Takip edilen nesneler başarıyla silindi.", + "error": "Takip edilen nesneler silinemedi: {{errorMessage}}" + }, + "title": "Silmeyi onayla", + "desc": "Bu {{objectLength}} adet izlenen nesneyi sildiğinizde ilgili tüm fotoğraflar, kaydedilmiş tüm gömüler ve ilişkili tüm nesne yaşam döngüsü kayıtları kaldırılır. Bu izlenen nesnelere ait Geçmiş görünümündeki kayıtlı görüntüler SİLİNMEYECEKTİR.

    Devam etmek istediğinize emin misiniz?

    Gelecekte bu diyaloğu pas geçmek için Shift tuşuna basılı tutarak tıklayın." + }, + "recognizedLicensePlates": { + "selectPlatesFromList": "Listeden bir veya birden fazla plaka seçin.", + "placeholder": "Plaka ara…", + "loading": "Tanınan plakalar yükleniyor…", + "title": "Tanınan Plakalar", + "noLicensePlatesFound": "Plaka bulunamadı.", + "loadFailed": "Tanınan plakalar yüklenemedi.", + "selectAll": "Tümünü seç", + "clearAll": "Tümünü temizle" + }, + "motion": { + "showMotionOnly": "Yalnızca Hareket Olanları Göster" + }, + "zoneMask": { + "filterBy": "Alana göre filtrele" + }, + "classes": { + "count_one": "{{count}} Sınıf", + "count_other": "{{count}} Sınıf", + "label": "Sınıflar", + "all": { + "title": "Tüm Sınıflar" + } + }, + "attributes": { + "label": "Sınıflandırma Özellikleri", + "all": "Tüm Özellikler" + } +} diff --git a/web/public/locales/tr/components/icons.json b/web/public/locales/tr/components/icons.json new file mode 100644 index 0000000..30c0e1d --- /dev/null +++ b/web/public/locales/tr/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "Bir simge arayın…" + }, + "selectIcon": "Bir simge belirleyin" + } +} diff --git a/web/public/locales/tr/components/input.json b/web/public/locales/tr/components/input.json new file mode 100644 index 0000000..bbaa987 --- /dev/null +++ b/web/public/locales/tr/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Videoyu İndir", + "toast": { + "success": "İncele öğesinin videosu indirilmeye başlandı." + } + } + } +} diff --git a/web/public/locales/tr/components/player.json b/web/public/locales/tr/components/player.json new file mode 100644 index 0000000..46ff41d --- /dev/null +++ b/web/public/locales/tr/components/player.json @@ -0,0 +1,53 @@ +{ + "streamOffline": { + "title": "Yayın Çevrimdışı", + "desc": "{{cameraName}} isimli kameranın tespit akışından hiç bir görüntü alınamadı, hata günlüklerini kontrol edin" + }, + "stats": { + "streamType": { + "title": "Yayın Türü:", + "short": "Tür" + }, + "latency": { + "value": "{{seconds}} saniye", + "short": { + "title": "Gecikme", + "value": "{{seconds}} sn" + }, + "title": "Gecikme:" + }, + "bandwidth": { + "title": "Bant genişliği:", + "short": "Bant genişliği" + }, + "decodedFrames": "Çözülen kareler:", + "droppedFrameRate": "Atlanan Kare Oranı:", + "totalFrames": "Toplam Kareler:", + "droppedFrames": { + "short": { + "title": "Atlanan", + "value": "{{droppedFrames}} kare" + }, + "title": "Atlanan Kareler:" + } + }, + "noPreviewFound": "Önizleme Bulunamadı", + "toast": { + "success": { + "submittedFrigatePlus": "Kare başarıyla Frigate+'a gönderildi" + }, + "error": { + "submitFrigatePlusFailed": "Kare Frigate+'a gönderilemedi" + } + }, + "noRecordingsFoundForThisTime": "Bu zaman aralığı için kayıt bulunamadı", + "cameraDisabled": "Kamera devre dışı bırakıldı", + "noPreviewFoundFor": "{{cameraName}} için Önizleme Bulunamadı", + "livePlayerRequiredIOSVersion": "Bu canlı yayın türü için iOS 17.1 veya daha yeni sürüm gereklidir.", + "submitFrigatePlus": { + "title": "Bu kare Frigate+'ya gönderilsin mi?", + "submit": "Gönder", + "previewError": "Önizleme şuan aktif edilemiyor. Kayıt şuan mevcut olmayabilir." + }, + "cameraOff": "Kamera kapalı" +} diff --git a/web/public/locales/tr/config/cameras.json b/web/public/locales/tr/config/cameras.json new file mode 100644 index 0000000..13595d4 --- /dev/null +++ b/web/public/locales/tr/config/cameras.json @@ -0,0 +1,66 @@ +{ + "name": { + "label": "Kamera adı", + "description": "Kamera adı gereklidir" + }, + "friendly_name": { + "label": "Kolay ad", + "description": "Frigate arayüzünde kullanılacak kolay ad" + }, + "enabled": { + "label": "Etkin", + "description": "Etkin" + }, + "audio": { + "label": "Ses olayları", + "description": "Bu kamera için ses tabanlı olay algılama ayarları.", + "enabled": { + "label": "Ses algılamayı etkinleştir", + "description": "Bu kamera için ses tabanlı olay algılamayı etkinleştirin veya devre dışı bırakın." + }, + "max_not_heard": { + "label": "Zaman aşımını sonlandır", + "description": "Ses tabanlı olayın sona ermesi için yapılandırılmış ses türünün algılanmadığı saniye süresi." + }, + "min_volume": { + "label": "Minimum ses seviyesi", + "description": "Ses algılamayı çalıştırmak için gereken minimum RMS ses seviyesi eşiği; daha düşük değerler hassasiyeti artırır (örneğin: 200 yüksek, 500 orta, 1000 düşük)." + }, + "listen": { + "label": "Dinlenecek ses türleri", + "description": "Algılanacak ses olayı türlerinin listesi (örneğin: bark (havlama), fire_alarm (yangın alarmı), speech (konuşma), yell (bağırma))." + }, + "filters": { + "label": "Ses filtreleri", + "description": "Yanlış alarmları azaltmak için kullanılan güven eşikleri gibi ses türüne özel filtre ayarları.", + "threshold": { + "label": "Minimum ses güven eşiği", + "description": "Ses olayının sayılması için gereken minimum güven eşiği." + } + }, + "enabled_in_config": { + "label": "Orijinal ses durumu", + "description": "Ses algılamanın statik yapılandırma dosyasında başlangıçta etkinleştirilip etkinleştirilmediğini belirtir." + } + }, + "label": "Kamera Konfigürasyonu", + "detect": { + "stationary": { + "threshold": { + "label": "Sabitlik eşiği", + "description": "Bir nesneyi sabit olarak işaretlemek için gereken, konum değişikliği olmayan kare sayısı." + }, + "max_frames": { + "label": "Maksimum kare sayısı", + "description": "Sabit nesnelerin, izlenmesi bırakılmadan önce ne kadar süre takip edileceğini sınırlar.", + "default": { + "label": "Varsayılan maksimum kare sayısı", + "description": "Sabit bir nesneyi durdurmadan önce takip etmek için varsayılan maksimum kare sayısı." + }, + "objects": { + "label": "Nesne maksimum kare sayısı" + } + } + } + } +} diff --git a/web/public/locales/tr/config/global.json b/web/public/locales/tr/config/global.json new file mode 100644 index 0000000..bcc1260 --- /dev/null +++ b/web/public/locales/tr/config/global.json @@ -0,0 +1,65 @@ +{ + "safe_mode": { + "label": "Güvenli mod", + "description": "Etkinleştirildiğinde, Firagate'i sorun girderme için kısıtlı özelliklere sahip güvenli modda başlat." + }, + "environment_vars": { + "label": "Ortam değişkenleri" + }, + "audio": { + "label": "Ses olayları", + "enabled": { + "label": "Ses algılamayı etkinleştir" + }, + "max_not_heard": { + "label": "Zaman aşımını sonlandır", + "description": "Ses tabanlı olayın sona ermesi için yapılandırılmış ses türünün algılanmadığı saniye süresi." + }, + "min_volume": { + "label": "Minimum ses seviyesi", + "description": "Ses algılamayı çalıştırmak için gereken minimum RMS ses seviyesi eşiği; daha düşük değerler hassasiyeti artırır (örneğin: 200 yüksek, 500 orta, 1000 düşük)." + }, + "listen": { + "label": "Dinlenecek ses türleri", + "description": "Algılanacak ses olayı türlerinin listesi (örneğin: bark (havlama), fire_alarm (yangın alarmı), speech (konuşma), yell (bağırma))." + }, + "filters": { + "label": "Ses filtreleri", + "description": "Yanlış alarmları azaltmak için kullanılan güven eşikleri gibi ses türüne özel filtre ayarları.", + "threshold": { + "label": "Minimum ses güven eşiği", + "description": "Ses olayının sayılması için gereken minimum güven eşiği." + } + }, + "enabled_in_config": { + "label": "Orijinal ses durumu", + "description": "Ses algılamanın statik yapılandırma dosyasında başlangıçta etkinleştirilip etkinleştirilmediğini belirtir." + } + }, + "version": { + "label": "Mevcut konfigürasyon versiyonu", + "description": "Taşıma veya biçimlendirme değişikliklerini tespit etmeye yardımcı olmak için etkin konfigürasyonun sayısal veya metin tabanlı sürümü." + }, + "logger": { + "label": "Kayıt" + }, + "detect": { + "stationary": { + "threshold": { + "label": "Sabitlik eşiği", + "description": "Bir nesneyi sabit olarak işaretlemek için gereken, konum değişikliği olmayan kare sayısı." + }, + "max_frames": { + "label": "Maksimum kare sayısı", + "description": "Sabit nesnelerin, izlenmesi bırakılmadan önce ne kadar süre takip edileceğini sınırlar.", + "default": { + "label": "Varsayılan maksimum kare sayısı", + "description": "Sabit bir nesneyi durdurmadan önce takip etmek için varsayılan maksimum kare sayısı." + }, + "objects": { + "label": "Nesne maksimum kare sayısı" + } + } + } + } +} diff --git a/web/public/locales/tr/config/groups.json b/web/public/locales/tr/config/groups.json new file mode 100644 index 0000000..c6e643d --- /dev/null +++ b/web/public/locales/tr/config/groups.json @@ -0,0 +1,71 @@ +{ + "audio": { + "global": { + "detection": "Genel Tespit", + "sensitivity": "Genel Hassasiyet" + }, + "cameras": { + "detection": "Tespit", + "sensitivity": "Hassasiyet" + } + }, + "timestamp_style": { + "global": { + "appearance": "Genel Görünüm" + }, + "cameras": { + "appearance": "Görünüm" + } + }, + "detect": { + "cameras": { + "resolution": "Çözünürlük", + "tracking": "Takip" + }, + "global": { + "resolution": "Genel Çözünürlük", + "tracking": "Genel Takip" + } + }, + "objects": { + "global": { + "tracking": "Genel Takip", + "filtering": "Genel Filtreleme" + }, + "cameras": { + "tracking": "Takip", + "filtering": "Filtreleme" + } + }, + "record": { + "global": { + "events": "Genel Etkinlikler" + }, + "cameras": { + "events": "Etkinlikler" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Kamera özel FFmpeg argümanları" + } + }, + "motion": { + "global": { + "sensitivity": "Genel Hassasiyet", + "algorithm": "Genel Algoritma" + }, + "cameras": { + "sensitivity": "Hassasiyet", + "algorithm": "Algoritma" + } + }, + "snapshots": { + "global": { + "display": "Genel Görüntü" + }, + "cameras": { + "display": "Görüntü" + } + } +} diff --git a/web/public/locales/tr/config/validation.json b/web/public/locales/tr/config/validation.json new file mode 100644 index 0000000..aaac6f5 --- /dev/null +++ b/web/public/locales/tr/config/validation.json @@ -0,0 +1,8 @@ +{ + "minimum": "En az {{limit}} olmalı", + "maximum": "En fazla {{limit}} olmalı", + "exclusiveMinimum": "{{limit}}’den büyük olmalı", + "exclusiveMaximum": "{{limit}}’den küçük olmalı", + "minLength": "En az {{limit}} karakter olmalı", + "maxLength": "En fazla {{limit}} karakter olmalı" +} diff --git a/web/public/locales/tr/objects.json b/web/public/locales/tr/objects.json new file mode 100644 index 0000000..fb07a09 --- /dev/null +++ b/web/public/locales/tr/objects.json @@ -0,0 +1,120 @@ +{ + "dog": "köpek", + "cat": "kedi", + "animal": "hayvan", + "bark": "havlama", + "blender": "mikser", + "mouse": "fare", + "door": "kapı", + "sink": "lavabo", + "boat": "bot", + "skateboard": "kaykay", + "sheep": "koyun", + "bicycle": "bisiklet", + "motorcycle": "motosiklet", + "bird": "kuş", + "car": "araba", + "bus": "otobüs", + "goat": "keçi", + "hair_dryer": "saç kurutma makinesi", + "clock": "saat", + "scissors": "makas", + "vehicle": "araç", + "keyboard": "klavye", + "toothbrush": "diş fırçası", + "horse": "at", + "train": "tren", + "handbag": "El Çantası", + "umbrella": "Şemsiye", + "shoe": "Ayakkabı", + "eye_glasses": "Gözlük", + "desk": "Masa", + "remote": "Uzaktan Kumanda", + "refrigerator": "Buzdolabı", + "book": "Kitap", + "hair_brush": "Saç Fırçası", + "squirrel": "Sincap", + "waste_bin": "Çöp Kutusu", + "face": "Yüz", + "license_plate": "Araç Plakası", + "an_post": "An Post", + "sandwich": "Sandviç", + "couch": "Koltuk", + "baseball_glove": "Beyzbol Eldiveni", + "donut": "Donut", + "bed": "Yatak", + "backpack": "Sırt Çantası", + "parking_meter": "Parkmetre", + "stop_sign": "Dur Tabelası", + "person": "İnsan", + "bear": "Ayı", + "hat": "Şapka", + "orange": "Portakal", + "dining_table": "Yemek Masası", + "traffic_light": "Trafik Lambası", + "giraffe": "Zürafa", + "fire_hydrant": "Yangın Musluğu", + "street_sign": "Sokak Tabelası", + "mirror": "Ayna", + "banana": "Muz", + "carrot": "Havuç", + "pizza": "Pizza", + "vase": "Vazo", + "nzpost": "NZPost", + "bench": "Bank", + "elephant": "Fil", + "spoon": "Kaşık", + "laptop": "Dizüstü Bilgisayar", + "frisbee": "Frizbi", + "skis": "Kayak", + "kite": "Uçurtma", + "bottle": "Şişe", + "cup": "Fincan", + "knife": "Bıçak", + "bowl": "Kase", + "package": "Paket", + "airplane": "Uçak", + "snowboard": "Kar Kayağı", + "usps": "USPS", + "ups": "UPS", + "cow": "İnek", + "zebra": "Zebra", + "suitcase": "Bavul", + "sports_ball": "Spor Topu", + "baseball_bat": "Beyzbol Sopası", + "surfboard": "Sörf Tahtası", + "plate": "Plaka", + "broccoli": "Brokoli", + "tv": "Televizyon", + "cell_phone": "Cep Telefonu", + "teddy_bear": "Ayıcık", + "deer": "Geyik", + "fox": "Tilki", + "purolator": "Purolator", + "fork": "Çatal", + "toilet": "Tuvalet", + "window": "Pencere", + "microwave": "Mikrodalga Fırın", + "hot_dog": "Sosisli Sandviç", + "wine_glass": "Şarap Bardağı", + "cake": "Kek", + "potted_plant": "Saksı Bitkisi", + "bbq_grill": "Izgara", + "tennis_racket": "Tenis Raketi", + "dhl": "DHL", + "raccoon": "Rakun", + "robot_lawnmower": "Robot Çim Biçme Makinesi", + "toaster": "Tost Makinesi", + "apple": "Elma", + "amazon": "Amazon", + "rabbit": "Tavşan", + "chair": "Sandalye", + "postnl": "PostNL", + "oven": "Fırın", + "fedex": "FedEx", + "on_demand": "İstenildiğinde", + "tie": "Kravat", + "dpd": "DPD", + "gls": "GLS", + "postnord": "PostNord" +} diff --git a/web/public/locales/tr/views/chat.json b/web/public/locales/tr/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/tr/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/tr/views/classificationModel.json b/web/public/locales/tr/views/classificationModel.json new file mode 100644 index 0000000..3a14e1f --- /dev/null +++ b/web/public/locales/tr/views/classificationModel.json @@ -0,0 +1,190 @@ +{ + "documentTitle": "Sınıflandırma Modelleri - Frigate", + "details": { + "scoreInfo": "Skor, modelin nesneyi tespit ettiği tüm durumlar için ortalama güven düzeyini gösterir.", + "none": "Hiçbiri", + "unknown": "Bilinmiyor" + }, + "button": { + "deleteClassificationAttempts": "Sınıflandırma Fotoğraflarını Sil", + "renameCategory": "Sınıfı Yeniden Adlandır", + "deleteCategory": "Sınıfı Sil", + "deleteImages": "Fotoğrafları Sil", + "trainModel": "Modeli Eğit", + "addClassification": "Sınıflandırma Ekle", + "deleteModels": "Modelleri Sil", + "editModel": "Modeli Düzenle" + }, + "toast": { + "success": { + "deletedCategory_one": "Silinmiş Sınıf", + "deletedCategory_other": "", + "deletedImage_one": "Silinmiş Fotoğraflar", + "deletedImage_other": "", + "deletedModel_one": "{{count}} model başarıyla silindi", + "deletedModel_other": "{{count}} model başarıyla silindi", + "categorizedImage": "Fotoğraf Başarıyla Sınıflandırıldı", + "trainedModel": "Model başarıyla eğitildi.", + "trainingModel": "Model eğitimi başarıyla başladı.", + "updatedModel": "Model yapılandırması başarıyla güncellendi", + "renamedCategory": "Sınıf başarıyla {{name}} olarak yeniden adlandırıldı" + }, + "error": { + "deleteImageFailed": "Silinemedi: {{errorMessage}}", + "deleteModelFailed": "Model silinemedi: {{errorMessage}}", + "categorizeFailed": "Görsel sınıflandırılamadı: {{errorMessage}}", + "trainingFailed": "Model eğitimi başarısız oldu. Ayrıntılar için Frigate günlüklerini kontrol edin.", + "deleteCategoryFailed": "Sınıf silinemedi: {{errorMessage}}", + "trainingFailedToStart": "Model eğitimi başlatılamadı: {{errorMessage}}", + "updateModelFailed": "Model güncellenemedi: {{errorMessage}}", + "renameCategoryFailed": "Sınıf yeniden adlandırılamadı: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Sınıfı Sil", + "desc": "{{name}} adlı sınıfı silmek istediğinizden emin misiniz? Bu işlem, sınıfa ait tüm görselleri kalıcı olarak silecek ve modelin yeniden eğitilmesini gerektirecektir.", + "minClassesTitle": "Sınıf Silinemiyor", + "minClassesDesc": "Bu sınıfı silmeden önce bir sınıflandırma modelinin en az 2 sınıfa sahip olması gerekir. Bu sınıfı silmeden önce başka bir sınıf ekleyin." + }, + "deleteModel": { + "title": "Sınıflandırma Modelini Sil", + "single": "{{name}} öğesini silmek istediğinizden emin misiniz? Bu işlem, görseller ve eğitim verileri dâhil olmak üzere tüm ilişkili verileri kalıcı olarak silecektir. Bu işlem geri alınamaz.", + "desc_one": "{{count}} modeli silmek istediğinizden emin misiniz? Bu işlem, görseller ve eğitim verileri dâhil olmak üzere tüm ilişkili verileri kalıcı olarak silecektir. Bu işlem geri alınamaz.", + "desc_other": "{{count}} modeli silmek istediğinizden emin misiniz? Bu işlem, görseller ve eğitim verileri dâhil olmak üzere tüm ilişkili verileri kalıcı olarak silecektir. Bu işlem geri alınamaz." + }, + "deleteDatasetImages": { + "title": "Eğitim verisi görsellerini sil", + "desc_one": "{{dataset}} veri kümesinden {{count}} görseli silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve modelin yeniden eğitilmesini gerektirir.", + "desc_other": "{{dataset}} veri kümesinden {{count}} görseli silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve modelin yeniden eğitilmesini gerektirir." + }, + "deleteTrainImages": { + "title": "Eğitim Görsellerini Sil", + "desc_one": "{{count}} görseli silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "desc_other": "{{count}} görseli silmek istediğinizden emin misiniz? Bu işlem geri alınamaz." + }, + "renameCategory": { + "title": "Sınıfı Yeniden Adlandır", + "desc": "{{name}} için yeni bir ad girin. Ad değişikliğinin geçerli olması için modeli yeniden eğitmeniz gerekecektir." + }, + "description": { + "invalidName": "Geçersiz isim. İsimler; yalnızca harf, rakam, boşluk, kesme işareti (’), alt çizgi(_) ve tire (-) içerebilir." + }, + "train": { + "title": "Son Sınıflandırmalar", + "titleShort": "Son", + "aria": "Son Sınıflandırmaları Seç" + }, + "categories": "Sınıflar", + "createCategory": { + "new": "Yeni Sınıf Oluştur" + }, + "categorizeImageAs": "Görseli Şu Şekilde Sınıflandır:", + "categorizeImage": "Görseli Sınıflandır", + "menu": { + "objects": "Nesneler", + "states": "Durumlar" + }, + "noModels": { + "object": { + "title": "Nesne sınıflandırma modeli mevcut değil", + "description": "Algılanan nesneleri sınıflandırmak için özel bir model oluşturun.", + "buttonText": "Nesne Modeli Oluştur" + }, + "state": { + "title": "Durum Sınıflandırma Modeli Yok", + "description": "Belirli kamera alanlarındaki durum değişimlerini izlemek ve sınıflandırmak için özel bir model oluşturun.", + "buttonText": "Durum Modeli Oluştur" + } + }, + "tooltip": { + "trainingInProgress": "Model şu anda eğitiliyor", + "noNewImages": "Eğitilecek yeni görsel bulunmuyor. Önce veri kümesinde daha fazla görseli sınıflandırın.", + "noChanges": "Son eğitimden bu yana veri kümesinde herhangi bir değişiklik yapılmadı.", + "modelNotReady": "Model eğitim için hazır değil" + }, + "edit": { + "title": "Sınıflandırma Modelini Düzenle", + "descriptionState": "Bu durum sınıflandırma modeli için sınıfları düzenleyin. Değişiklikler, modelin yeniden eğitilmesini gerektirecektir.", + "descriptionObject": "Bu nesne sınıflandırma modeli için nesne türünü ve sınıflandırma türünü düzenleyin.", + "stateClassesInfo": "Not: Durum sınıflarını değiştirmek, modelin güncellenmiş sınıflarla yeniden eğitilmesini gerektirir." + }, + "wizard": { + "title": "Yeni Sınıflandırma Oluştur", + "steps": { + "nameAndDefine": "İsim ver ve Tanımla", + "stateArea": "Durum Alanı", + "chooseExamples": "Örnekleri Seç" + }, + "step1": { + "description": "Durum modelleri, sabit kamera alanlarındaki değişiklikleri (ör. kapının açılması/kapanması) izler. Nesne modelleri ise algılanan nesnelere ek sınıflandırmalar ekler (ör. bilinen hayvanlar, kuryeler vb.).", + "name": "İsim", + "namePlaceholder": "Model ismi girin...", + "type": "Tür", + "typeState": "Durum", + "typeObject": "Nesne", + "objectLabel": "Nesne Etiketi", + "objectLabelPlaceholder": "Nesne türünü seçin...", + "classificationType": "Sınıflandırma Türü", + "classificationTypeTip": "Sınıflandırma türleri hakkında bilgi edinin", + "classificationTypeDesc": "Alt etiketler, nesne etiketine ek olarak saklanır (örneğin: “Person: UPS”). Öznitelikler(attributes) ise nesne meta verilerinde saklanan aranabilir meta verilerdir.", + "classificationSubLabel": "Alt Etiket", + "classificationAttribute": "Özellik", + "classes": "Sınıflar", + "states": "Durumlar", + "classesTip": "Sınıflar hakkında bilgi edinin", + "classesStateDesc": "Kamera alanınızın içinde bulunabileceği farklı durumları tanımlayın. Örneğin: bir garaj kapısı için ‘açık’ ve ‘kapalı’.", + "classesObjectDesc": "Algılanan nesneleri sınıflandırmak için farklı kategorileri tanımlayın. Örneğin: Bir kişi sınıflandırması için \"kurye\", \"bahçıvan\" veya \"yabancı\" olabilir.", + "classPlaceholder": "Sınıf ismi girin...", + "errors": { + "nameRequired": "Model ismi gereklidir", + "nameLength": "Model ismi en fazla 64 karakter olmalıdır", + "nameOnlyNumbers": "Model ismi yalnızca rakamlardan oluşamaz", + "classRequired": "En az 1 sınıf gereklidir", + "classesUnique": "Sınıf isimleri benzersiz olmalıdır", + "stateRequiresTwoClasses": "Durum modelleri en az 2 sınıf gerektirir", + "objectLabelRequired": "Lütfen bir nesne etiketi seçin", + "objectTypeRequired": "Lütfen bir sınıflandırma türü seçin", + "noneNotAllowed": "'none' sınıfına izin verilmiyor" + } + }, + "step2": { + "description": "İzlenecek alanı her kamera için seçin ve tanımlayın. Model bu alanların durumunu sınıflandıracaktır.", + "cameras": "Kameralar", + "selectCamera": "Kamera Seç", + "noCameras": "Kameraları eklemek için + simgesine tıklayın", + "selectCameraPrompt": "Listedeki bir kamerayı seçerek izlenecek alanı tanımlayın" + }, + "step3": { + "selectImagesPrompt": "{{className}} etiketli tüm görselleri seç", + "selectImagesDescription": "Görselleri seçmek için üzerlerine tıklayın. Bu sınıfla işiniz bittiğinde Devam Et’e tıklayın.", + "allImagesRequired_one": "Lütfen tüm görselleri sınıflandırın. Bir görsel kaldı.", + "allImagesRequired_other": "Lütfen tüm görselleri sınıflandırın. {{count}} görsel kaldı.", + "generating": { + "title": "Örnek Görseller Oluşturuluyor", + "description": "Frigate kayıtlarınızdan temsili görüntüler alınıyor. Bu işlem biraz zaman alabilir…" + }, + "training": { + "title": "Model Eğitiliyor", + "description": "Modeliniz arka planda eğitiliyor. Bu pencereyi kapatabilirsiniz; eğitim tamamlandığında model otomatik olarak çalışmaya başlayacaktır." + }, + "retryGenerate": "Oluşturmayı Yeniden Dene", + "noImages": "Örnek görsel oluşturulamadı", + "classifying": "Sınıflandırılıyor ve Eğitiliyor...", + "trainingStarted": "Eğitim başarıyla başlatıldı", + "modelCreated": "Model başarıyla oluşturuldu. Eksik durumlar için görseller eklemek üzere Son Sınıflandırmalar görünümünü kullanın ve ardından modeli eğitin.", + "errors": { + "noCameras": "Hiç kamera yapılandırılmadı", + "noObjectLabel": "Nesne etiketi seçilmedi", + "generateFailed": "Örnekler oluşturulamadı: {{error}}", + "generationFailed": "Oluşturma başarısız oldu. Lütfen tekrar deneyin.", + "classifyFailed": "Görseller sınıflandırılamadı: {{error}}" + }, + "generateSuccess": "Örnek görseller başarıyla oluşturuldu", + "missingStatesWarning": { + "title": "Eksik Durum Örnekleri", + "description": "En iyi sonuçlar için her bir durum için örnek görseller seçmeniz tavsiye edilir. Tüm durumlar için görsel seçmeden devam edebilirsiniz fakat tüm durumlar için görseller seçilmedikçe model eğitilemeyecektir. Son Sınıflandırmalar arayüzünü kullanarak görselleri sınıflandırmak üzere görüntüleyebilir, yeterince görsel seçildikten sonra da modeli eğitebilirsiniz." + } + } + }, + "none": "Yok" +} diff --git a/web/public/locales/tr/views/configEditor.json b/web/public/locales/tr/views/configEditor.json new file mode 100644 index 0000000..32ffdb2 --- /dev/null +++ b/web/public/locales/tr/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "saveOnly": "Sadece Kaydet", + "toast": { + "error": { + "savingError": "Yapılandırma kaydedilirken hata" + }, + "success": { + "copyToClipboard": "Yapılandırma panoya kopyalandı." + } + }, + "copyConfig": "Yapılandırmayı Kopyala", + "configEditor": "Yapılandırma Düzenleyicisi", + "documentTitle": "Yapılandırma Düzenleyicisi - Frigate", + "saveAndRestart": "Kaydet & Yeniden Başlat", + "confirm": "Kaydetmeden çıkılsın mı?", + "safeConfigEditor": "Yapılandırma Düzenleyicisi (Güvenli Mod)", + "safeModeDescription": "Frigate, yapılandırmanızdaki bir hata nedeniyle güvenli moda geçti." +} diff --git a/web/public/locales/tr/views/events.json b/web/public/locales/tr/views/events.json new file mode 100644 index 0000000..d0d3007 --- /dev/null +++ b/web/public/locales/tr/views/events.json @@ -0,0 +1,67 @@ +{ + "camera": "kamera", + "alerts": "Uyarılar", + "detections": "Tespitler", + "empty": { + "detection": "İncelenecek tespit öğesi yok", + "alert": "İncelenecek uyarı öğesi yok", + "motion": "Hareket verisi bulunamadı", + "recordingsDisabled": { + "title": "Kayıt özelliği etkinleştirilmelidir", + "description": "İnceleme öğeleri yalnızca bir kamera için kayıt özelliği etkinleştirildiğinde oluşturulabilir." + } + }, + "timeline": "Zaman şeridi", + "events": { + "aria": "Olayları seçin", + "noFoundForTimePeriod": "Seçili zaman aralığında olay bulunamadı.", + "label": "Olaylar" + }, + "recordings": { + "documentTitle": "Kayıtlar - Frigate" + }, + "calendarFilter": { + "last24Hours": "Son 24 Saat" + }, + "markAsReviewed": "İncelendi Olarak İşaretle", + "newReviewItems": { + "button": "Yeni İncelenecek Öğeler Var", + "label": "Yeni inceleme öğelerini göster" + }, + "documentTitle": "İncele - Frigate", + "motion": { + "label": "Hareket", + "only": "Yalnızca hareket" + }, + "timeline.aria": "Zaman şeridi seçin", + "markTheseItemsAsReviewed": "Bunları incelendi olarak işaretle", + "allCameras": "Tüm Kameralar", + "selected_one": "{{count}} seçildi", + "selected_other": "{{count}} seçildi", + "detected": "algılandı", + "suspiciousActivity": "Şüpheli Etkinlik", + "threateningActivity": "Tehlikeli Etkinlik", + "zoomIn": "Büyüt", + "zoomOut": "Küçült", + "detail": { + "label": "Detay", + "aria": "Ayrıntı görünümünü aç/kapat", + "trackedObject_one": "{{count}} nesne", + "trackedObject_other": "{{count}} nesne", + "noObjectDetailData": "Nesneye ait ayrıntılı veri bulunmuyor.", + "settings": "Ayrıntılı Görünüm Ayarları", + "alwaysExpandActive": { + "title": "Etkin olanı her zaman genişlet", + "desc": "Varsa, etkin inceleme öğesinin nesne ayrıntılarını daima göster." + }, + "noDataFound": "İncelenecek ayrıntılı veri bulunmuyor" + }, + "objectTrack": { + "trackedPoint": "Takip edilen nokta", + "clickToSeek": "Bu zamana atlamak için tıklayın" + }, + "normalActivity": "Normal", + "needsReview": "İnceleme Gerekiyor", + "securityConcern": "Güvenlik endişesi", + "select_all": "Tümü" +} diff --git a/web/public/locales/tr/views/explore.json b/web/public/locales/tr/views/explore.json new file mode 100644 index 0000000..206e6ac --- /dev/null +++ b/web/public/locales/tr/views/explore.json @@ -0,0 +1,303 @@ +{ + "documentTitle": "Keşfet - Frigate", + "details": { + "timestamp": "Zaman Damgası", + "item": { + "title": "Nesne Detaylarını İncele", + "desc": "Nesne detaylarıın incele", + "button": { + "share": "Bu incele öğesini paylaş", + "viewInExplore": "Keşfet'te Görüntüle" + }, + "tips": { + "hasMissingObjects": "Eğer Frigate'in {{objects}} etiketine sahip nesneleri kaydetmesini istiyorsanız yapılandırmanızı buna göre ayarlayın", + "mismatch_one": "Tespit edilmiş olan bir nesne bu İncele öğesine dahil edildi. Bu nesne Alarm veya Tespit olarak derecelendirilemedi veya çoktan silindi/temizlendi.", + "mismatch_other": "Tespit edilmiş olan {{count}} adet nesne bu İncele öğesine dahil edildi. Bu nesneler Alarm veya Tespit olarak derecelendirilemedi veya çoktan silindi/temizlendi." + }, + "toast": { + "success": { + "updatedSublabel": "Alt etiket başarıyla gücellendi.", + "regenerate": "Yeni bir açıklama {{provider}} sağlayıcısından talep edildi. Sağlayıcının hızına bağlı olarak yeni açıklamanın oluşturulması biraz zaman alabilir.", + "updatedLPR": "Plaka başarıyla güncellendi.", + "audioTranscription": "Ses dökümü başarıyla istendi. Frigate sunucun­uzun hızına bağlı olarak döküm işlemi tamamlanması biraz zaman alabilir.", + "updatedAttributes": "Özellikler başarıyla güncellendi." + }, + "error": { + "updatedSublabelFailed": "Alt etiket güncellenemedi: {{errorMessage}}", + "regenerate": "{{provider}} sağlayıcısından yeni açıklama talep edilemedi: {{errorMessage}}", + "updatedLPRFailed": "Plaka güncellenemedi: {{errorMessage}}", + "audioTranscription": "Ses çözümlemesi talep edilemedi: {{errorMessage}}", + "updatedAttributesFailed": "Özellikler güncellenemedi: {{errorMessage}}" + } + } + }, + "label": "Etiket", + "editSubLabel": { + "desc": "Bu {{label}} için yeni bir alt etiket girin", + "descNoLabel": "Bu takip edilmiş nesne için yeni bir alt etiket girin", + "title": "Alt etiketi gücelle" + }, + "estimatedSpeed": "Tahmini Hız", + "camera": "Kamera", + "zones": "Alanlar", + "description": { + "label": "Açıklama", + "aiTips": "Frigate, Nesne Geçmişi tamamlanana kadar Üretken Yapay Zeka sağlayıcısından bir resim açıklaması talep etmeyecektir.", + "placeholder": "Takip edilen nesnenin açıklaması" + }, + "expandRegenerationMenu": "Yeniden Üret menüsünü genişlet", + "regenerateFromSnapshot": "Fotoğraftan Üret", + "regenerateFromThumbnails": "Küçük Resimden Üret", + "tips": { + "descriptionSaved": "Açıklama başarıyla kaydedildi", + "saveDescriptionFailed": "Açıklama güncellenemedi: {{errorMessage}}" + }, + "button": { + "regenerate": { + "label": "Nesne açıklaması yeniden üretildi", + "title": "Yeniden Üret" + }, + "findSimilar": "Benzerini Bul" + }, + "topScore": { + "info": "Tepe skor, bir takip edilen nesne için en yüksek ortalama puandır ve arama sonucundaki küçük resimde gösterilen puandan farklı olabilir.", + "label": "Tepe Skor" + }, + "objects": "Nesneler", + "editLPR": { + "title": "Plakayı düzenle", + "desc": "Bu {{label}} için yeni bir plaka değeri girin", + "descNoLabel": "Bu nesne için yeni bir plaka değeri girin" + }, + "recognizedLicensePlate": "Tanınan Plaka", + "snapshotScore": { + "label": "Fotoğraf Skoru" + }, + "score": { + "label": "Skor" + }, + "editAttributes": { + "title": "Özellikleri düzenle", + "desc": "Bu {{label}} için sınıflandırma özelliklerini seçin" + }, + "attributes": "Sınıflandırma Özellikleri", + "title": { + "label": "Başlık" + } + }, + "generativeAI": "Üretken Yapay Zeka", + "exploreIsUnavailable": { + "title": "Keşfet şu anda kullanılamıyor", + "embeddingsReindexing": { + "startingUp": "Başlatılıyor…", + "estimatedTime": "Tahmini kalan süre:", + "step": { + "thumbnailsEmbedded": "Gömü eklenen küçük resimler: ", + "descriptionsEmbedded": "Gömü eklenen açıklamalar: ", + "trackedObjectsProcessed": "İşlenen takip edilen nesneler: " + }, + "finishingShortly": "Birazdan tamamlanacak", + "context": "Keşfet sayfası nesnelerin gömülerinin yeniden dizinlemesi tamamlandığında kullanılabilecektir." + }, + "downloadingModels": { + "setup": { + "visionModel": "Görüş modeli", + "visionModelFeatureExtractor": "Görüş modeli özellik çıkarmcısı", + "textModel": "Metin modeli", + "textTokenizer": "Metin tokenizeri" + }, + "error": "Bir hata oluştu. Frigate günlüklerini kontrol edin.", + "tips": { + "documentation": "Dökümantasyonu oku", + "context": "Model indirildikten sonra takip edilmiş nesnelerinizin gömüleri tekrar dizinlemeyi tercih edebilirsiniz." + }, + "context": "Frigate, Anlamsal Arama özelliği için gerekli olan gömü modellerini indiriyor. Ağ bağlantınızın hızına göre bu işlem bir kaç dakika sürebilir." + } + }, + "trackedObjectDetails": "Takip Edilen Nesne Detayları", + "type": { + "details": "detaylar", + "object_lifecycle": "nesne yaşam döngüsü", + "snapshot": "fotoğraf", + "video": "video", + "thumbnail": "küçük resim", + "tracking_details": "izleme ayrıntıları" + }, + "objectLifecycle": { + "title": "Nesne Yaşam Döngüsü", + "noImageFound": "Bu zaman damgası için bir resim bulunamadı.", + "createObjectMask": "Nesne Maskesi Oluştur", + "adjustAnnotationSettings": "Belirteç ayarları", + "lifecycleItemDesc": { + "visible": "{{label}} tespit edildi", + "entered_zone": "{{label}} {{zones}} alanına girdi", + "active": "{{label}} inaktif oldu", + "heard": "{{label}} duyuldu", + "external": "{{label}} tespit edildi", + "stationary": "{{label}} sabit durdu", + "attribute": { + "other": "{{label}} {{attribute}} olarak tespit edildi", + "faceOrLicense_plate": "{{label}} için {{attribute}} tespit edildi" + }, + "gone": "{{label}} ayrıldı", + "header": { + "zones": "Alanlar", + "ratio": "Oran", + "area": "Alan" + } + }, + "annotationSettings": { + "offset": { + "label": "Belirteç telafisi", + "documentation": "Dökümantasyonu oku ", + "millisecondsToOffset": "Belirteç gecikmesi. Varsayılan: 0", + "desc": "Tespit belirteç verisi kameranızın tespit yayınından gelir fakat kameranızın kayıt yayını üzerine çizilir. Bu iki yayın zaman zaman senkrondan kayar. Bunun sonucu olarak görüntü ve belirteç karelerinin zaman uyumu kayabilir. Bunu telafi etmek için annotation_offset alanı kullanılarak gecikmeyi ayarlanabilir.", + "tips": "İPUCU: Videoda bir kişinin soldan sağa doğru yürüdüğünü hayal edin. Eğer belirteç sürekli olarak kişinin solunda/arkasında ise bu değer daha küçük veya negatif olarak ayarlanmalıdır. Benzer şekilde, eğer kişi sağdan sola doğru yürürken belirteç karesi sürekli olarak kişinin önünde/sağında kalıyorsa bu değer daha büyük veya pozitif olarak ayarlanmalıdır.", + "toast": { + "success": "{{camera}} kamerası için belirteç telafi değeri yapılandırma dosyasına kaydedildi. Değişikliklerin uygulamak için Frigate'i yeniden başlatın." + } + }, + "title": "Belirteç Ayarları", + "showAllZones": { + "title": "Tüm Alanları Göster", + "desc": "Nesnelerin bir alana girdiği karelerde her zaman alanları göster." + } + }, + "carousel": { + "next": "Sonraki sayfa", + "previous": "Önceki sayfa" + }, + "scrollViewTips": "Bu nesnenin yaşam döngüsündeki önemli noktaları görmek için kaydırın.", + "autoTrackingTips": "Otomatik takip yapılan kameralarda gösterilen çerçeveler hatalı olacaktır.", + "count": "Toplam {{second}} kerede {{first}} kez", + "trackedPoint": "Takip Edilen Nokta" + }, + "itemMenu": { + "downloadVideo": { + "label": "Videoyu indir", + "aria": "Videoyu indir" + }, + "findSimilar": { + "aria": "Benzer takip edilen nesneleri bul", + "label": "Benzerini bul" + }, + "submitToPlus": { + "label": "Frigate+'a gönder", + "aria": "Frigate+'a gönder" + }, + "viewInHistory": { + "label": "Geçmiş görünümünde görüntüle", + "aria": "Geçmiş görünümünde görüntüle" + }, + "deleteTrackedObject": { + "label": "Bu takip edilen nesneyi sil" + }, + "viewObjectLifecycle": { + "aria": "Nesne yaşam döngüsünü göster", + "label": "Nesne yaşam döngüsünü göster" + }, + "downloadSnapshot": { + "aria": "Fotoğrafı indir", + "label": "Fotoğrafı indir" + }, + "addTrigger": { + "label": "Tetik ekle", + "aria": "Bu takip edilen nesne için bir tetik ekle" + }, + "audioTranscription": { + "label": "Çözümle", + "aria": "Ses çözümlemesi iste" + }, + "downloadCleanSnapshot": { + "label": "Temiz anlık görüntüyü indir", + "aria": "Temiz anlık görüntüyü indir" + }, + "viewTrackingDetails": { + "label": "Takip ayrıntılarını görüntüle", + "aria": "Takip ayrıntılarını göster" + }, + "showObjectDetails": { + "label": "Nesne yolunu göster" + }, + "hideObjectDetails": { + "label": "Nesne yolunu gizle" + } + }, + "noTrackedObjects": "Takip Edilen Nesne Bulunamadı", + "fetchingTrackedObjectsFailed": "Takip edilen nesneler getirilirken hata: {{errorMessage}}", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "error": "Takip edilen nesne silinemedi: {{errorMessage}}", + "success": "Takip edilen nesne başarıyla silindi." + } + }, + "tooltip": "Eşleşme: {{type}} (%{{confidence}})", + "previousTrackedObject": "Önceki izlenen nesne", + "nextTrackedObject": "Sonraki izlenen nesne" + }, + "dialog": { + "confirmDelete": { + "desc": "Bu takip edilen nesneyi silmek anlık görüntüyü, kaydedilmiş gömü verilerini ve ilişkili yaşam döngüsü kayıtlarını siler. Geçmiş görünümündeki bu izlenen nesneye ait kayıtlı video görüntüleri SİLİNMEYECEKTİR.

    Devam etmek istediğinizden emin misiniz?", + "title": "Silmeyi onayla" + } + }, + "trackedObjectsCount_one": "{{count}} adet takip edilen nesne ", + "trackedObjectsCount_other": "{{count}} adet takip edilen nesne ", + "exploreMore": "Daha fazla {{label}} nesnesini keşfet", + "aiAnalysis": { + "title": "Yapay Zeka Analizi" + }, + "trackingDetails": { + "title": "Takip Ayrıntıları", + "noImageFound": "Bu zaman damgasına ait bir görsel bulunamadı.", + "createObjectMask": "Nesne Maskesi Oluştur", + "adjustAnnotationSettings": "Etiketleme ayarlarını düzenle", + "scrollViewTips": "Bu nesnenin yaşam döngüsündeki önemli olayları görmek için tıklayın.", + "autoTrackingTips": "Otomatik takip yapan kameralar için sınır kutusu konumları doğru olmayabilir.", + "count": "{{second}}’den {{first}}", + "trackedPoint": "Takip edilen nokta", + "lifecycleItemDesc": { + "visible": "{{label}} tespit edildi", + "entered_zone": "{{label}}, {{zones}} bölgesine girdi", + "active": "{{label}} etkin hale geldi", + "stationary": "{{label}} sabit hale geldi", + "attribute": { + "faceOrLicense_plate": "{{label}} için {{attribute}} tespit edildi", + "other": "{{label}}, {{attribute}} olarak tanındı" + }, + "gone": "{{label}} ayrıldı", + "heard": "{{label}} duyuldu", + "external": "{{label}} tespit edildi", + "header": { + "zones": "Bölgeler", + "ratio": "Oran", + "area": "Alan", + "score": "Skor" + } + }, + "annotationSettings": { + "title": "Etiketleme Ayarları", + "showAllZones": { + "title": "Tüm Bölgeleri Göster", + "desc": "Herhangi bir bölgeye nesne girdiğinde, o karede bölgeleri her zaman göster." + }, + "offset": { + "label": "Etiket Kaydırma Değeri", + "desc": "Bu veriler kameranızın algılama akışından gelir ancak kayıt akışındaki görüntülerin üzerine bindirilir. İki akış tamamen eşzamanlı olmayabilir, bu durum da sınır kutusu ile görüntünün hizasını kaydırabilir. Bu ayarı kullanarak zaman senkronunu ileri veya geri kaydırarak kayıt akışını ve etiketlemeleri hizalayabilirsiniz.", + "millisecondsToOffset": "Algılama etiketlemelerinin kaydırılacağı milisaniye değeri. Varsayılan: 0", + "tips": "Videonun oynatımı kutulardan ve yol noktalarından öndeyse değeri düşürün; geride kalıyorsa değeri artırın. Bu değer negatif olabilir.", + "toast": { + "success": "{{camera}} için etiketleme zaman kaydırması yapılandırma dosyasına kaydedildi." + } + } + }, + "carousel": { + "previous": "Önceki slayt", + "next": "Sonraki slayt" + } + }, + "concerns": { + "label": "Endişeler" + } +} diff --git a/web/public/locales/tr/views/exports.json b/web/public/locales/tr/views/exports.json new file mode 100644 index 0000000..d6ee3eb --- /dev/null +++ b/web/public/locales/tr/views/exports.json @@ -0,0 +1,29 @@ +{ + "search": "Arama", + "documentTitle": "Dışa Aktar - Frigate", + "deleteExport": { + "label": "Dışa Aktarımı Sil" + }, + "deleteExport.desc": "{{exportName}} adlı dışa aktarımı silmek istediğinize emin misiniz?", + "editExport": { + "saveExport": "Dışa Aktarımı Kaydet", + "desc": "Bu dışa aktarım için yeni bir isim girin.", + "title": "Dışa Aktarımı Yeniden Adlandır" + }, + "toast": { + "error": { + "renameExportFailed": "Dışa aktarım adlandırılamadı: {{errorMessage}}" + } + }, + "noExports": "Dışa aktarım bulunamadı", + "tooltip": { + "shareExport": "Dışa aktarmayı paylaş", + "downloadVideo": "Videoyu İndir", + "editName": "İsmi Düzenle", + "deleteExport": "Dışa Aktarmayı Sil" + }, + "headings": { + "uncategorizedExports": "Kategorize Edilmemiş Dışa Aktarım", + "cases": "Durumlar" + } +} diff --git a/web/public/locales/tr/views/faceLibrary.json b/web/public/locales/tr/views/faceLibrary.json new file mode 100644 index 0000000..46663ac --- /dev/null +++ b/web/public/locales/tr/views/faceLibrary.json @@ -0,0 +1,102 @@ +{ + "selectItem": "{{item}} seçin", + "description": { + "placeholder": "Bu koleksiyona bir isim verin", + "addFace": "İlk görselinizi yükleyerek Yüz Kütüphanesi’ne yeni bir koleksiyon ekleyin.", + "invalidName": "Geçersiz isim. İsimler; yalnızca harf, rakam, boşluk, kesme işareti (’), alt çizgi(_) ve tire (-) içerebilir.", + "nameCannotContainHash": "İsim, # içeremez." + }, + "details": { + "person": "İnsan", + "faceDesc": "Yüz ve ilişkili nesneye ait detaylar", + "confidence": "Kesinlik", + "timestamp": "Zaman Damgası", + "face": "Yüz Detayları", + "scoreInfo": "Alt etiket skoru, tanınan tüm yüzlerin güvenilirlik değerlerinin ağırlıklı ortalamasından elde edilir, dolayısıyla fotoğraf üzerinde gösterilen skordan farklı olabilir.", + "subLabelScore": "Alt Etiket Puanı", + "unknown": "Bilinmiyor" + }, + "documentTitle": "Yüz Kütüphanesi - Frigate", + "uploadFaceImage": { + "title": "Yüz Resmi Yükle", + "desc": "Yüzleri taramak ve {{pageToggle}} için dahil etmek üzere bir resim yükleyin" + }, + "createFaceLibrary": { + "desc": "Yeni bir yüz koleksiyonu oluşturun", + "new": "Yeni Yüz Oluştur", + "title": "Koleksiyon Oluştur", + "nextSteps": "Sağlam bir temel oluşturmak için:
  • Her tespit edilen kişi için **Recent Recognitions (Son Tanımalar)** sekmesini kullanarak görüntüleri seçin ve eğitim gerçekleştirin.
  • En iyi sonuçlar için doğrudan önden çekilmiş yüz görüntülerine odaklanın; yüzlerin açılı göründüğü fotoğrafları eğitimde kullanmaktan kaçının.
  • " + }, + "train": { + "title": "Son Tanımalar", + "aria": "Son algılanan nesneleri seç", + "empty": "Yakın zamanda yüz tanıma denemesi olmadı", + "titleShort": "Son" + }, + "deleteFaceLibrary": { + "title": "İsmi Sil", + "desc": "{{name}} koleksiyonunu silmek istediğinizden emin misiniz? Bu işlem, ilişkili tüm yüzleri kalıcı olarak silecektir." + }, + "button": { + "deleteFaceAttempts": "Yüzleri Sil", + "addFace": "Yüz Ekle", + "reprocessFace": "Yüzü Yeniden İşle", + "uploadImage": "Resim Yükle", + "renameFace": "Yüzü Yeniden Adlandır", + "deleteFace": "Yüzü Sil" + }, + "imageEntry": { + "dropActive": "Resmi buraya bırakın…", + "maxSize": "Maksimum boyut: {{size}} MB", + "validation": { + "selectImage": "Lütfen bir resim dosyası seçin." + }, + "dropInstructions": "Bir görseli buraya sürükleyip bırakın, yapıştırın ya da seçmek için tıklayın" + }, + "trainFaceAs": "Yüzü şu olarak eğit:", + "toast": { + "success": { + "deletedFace_one": "{{count}} yüz başarıyla silindi.", + "deletedFace_other": "{{count}} yüz başarıyla silindi.", + "deletedName_one": "{{count}} yüz başarıyla silindi.", + "deletedName_other": "{{count}} yüz başarıyla silindi.", + "addFaceLibrary": "{{name}} başarıyla Yüz Kütüphanesi’ne eklendi!", + "trainedFace": "Yüz başarıyla eğitildi.", + "uploadedImage": "Resim başarıyla yüklendi.", + "updatedFaceScore": "Yüz tanıma skoru {{name}} ({{score}}) olarak başarıyla güncellendi.", + "renamedFace": "Yüz başarıyla {{name}} olarak adlandırıldı" + }, + "error": { + "uploadingImageFailed": "Resim yüklenemedi: {{errorMessage}}", + "addFaceLibraryFailed": "Yüz ismi ayarlanamadı: {{errorMessage}}", + "updateFaceScoreFailed": "Yüz skoru güncellenemedi: {{errorMessage}}", + "trainFailed": "Eğitme işlemi başarısız oldu: {{errorMessage}}", + "deleteFaceFailed": "Silme işlemi başarısız: {{errorMessage}}", + "deleteNameFailed": "İsim silinemedi: {{errorMessage}}", + "renameFaceFailed": "Yüz yeniden adlandırılamadı: {{errorMessage}}" + } + }, + "readTheDocs": "Dokümantasyonu oku", + "selectFace": "Yüz Seçin", + "trainFace": "Yüzü Eğit", + "steps": { + "faceName": "Yüze İsim Verin", + "uploadFace": "Yüz Resmi Yükle", + "nextSteps": "Sonraki Adımlar", + "description": { + "uploadFace": "{{name}}'in yüzünü önden gösteren bir fotoğraf yükleyin. Fotoğrafın sadece yüzünü gösterecek şekilde kırpılması gerekmez." + } + }, + "renameFace": { + "title": "Yüzü Yeniden Adlandır", + "desc": "{{name}} için yeni bir isim girin" + }, + "collections": "Koleksiyonlar", + "deleteFaceAttempts": { + "title": "Yüzleri Sil", + "desc_one": "Bir adet yüzü silmek istediğinize emin misiniz? Bu işlem geri alınamaz.", + "desc_other": "{{count}} adet yüzü silmek istediğinize emin misiniz? Bu işlem geri alınamaz." + }, + "nofaces": "Yüz bulunamadı", + "pixels": "{{area}}px" +} diff --git a/web/public/locales/tr/views/live.json b/web/public/locales/tr/views/live.json new file mode 100644 index 0000000..1c9af53 --- /dev/null +++ b/web/public/locales/tr/views/live.json @@ -0,0 +1,201 @@ +{ + "documentTitle": { + "default": "Canlı - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - Canlı - Frigate", + "muteCameras": { + "disable": "Tüm Kameraların Sesini Aç", + "enable": "Tüm Kameraları Sustur" + }, + "autotracking": { + "disable": "Otomatik Takibi Kapat", + "enable": "Otomatik Takibi Aç" + }, + "manualRecording": { + "start": "Talep Üzerine Kaydı Başlat", + "failedToEnd": "Manuel talep üzerine kayıt bitirilemedi.", + "recordDisabledTips": "Kamera konfigürasyonunda kayıtlar devre dışı bırakıldığı veya kısıtlandığı için yalnızca bir fotoğraf kaydedilcektir.", + "showStats": { + "desc": "Yayın istatistiklerini göstermek için bu seçeneği açın.", + "label": "İstatistikleri Göster" + }, + "started": "Manuel talep üzerine kayıt başlatıldı.", + "failedToStart": "Manuel talep üzerine kayıt başlatılamadı.", + "title": "İsteğe Bağlı", + "end": "Talep Üzerine Kaydı Bitir", + "debugView": "Hata Ayıklama Görünümü", + "ended": "Manuel talep üzerine kayıt bitirildi.", + "tips": "Bu kameranın kayıt saklama ayarlarına göre anlık bir görüntü indirin veya manuel bir olay başlatın.", + "playInBackground": { + "label": "Arka planda oynat", + "desc": "Yayını oynatıcı arkadayken de devam ettirmek için bu seçeneği açın." + } + }, + "stream": { + "audio": { + "tips": { + "documentation": "Dökümantasyonu oku ", + "title": "Bu yayın için kameranızın yayın çıkışında ses olması ve go2rtc'de ayarlanmış olması gerekmektedir." + }, + "unavailable": "Bu yayında ses yok", + "available": "Bu yayında ses var" + }, + "twoWayTalk": { + "tips": "Çift yönlü ses için cihazınızın ve kameranızın bu özelliği desteklemesi ve WebRTC'nin ayarlanmış olması gereklidir.", + "tips.documentation": "Dökümantasyonu oku ", + "available": "Bu yayında çift yönlü ses var", + "unavailable": "Bu yayında çift yönlü ses yok" + }, + "lowBandwidth": { + "tips": "Canlı görünüm, yayında donmalar veya yayın hataları sebebiyle düşük bant genişliği moduna geçti.", + "resetStream": "Yayını sıfırla" + }, + "playInBackground": { + "label": "Arka planda oynat", + "tips": "Yayını oynatıcı arkadayken de devam ettirmek için bu seçeneği açın." + }, + "title": "Yayın", + "debug": { + "picker": "Hata ayıklama modunda akış seçimi kullanılamaz. Hata ayıklama görünümü her zaman tespit(detect) rolüne atanmış akışı kullanır." + } + }, + "cameraSettings": { + "recording": "Kayıt", + "snapshots": "Fotoğraflar", + "title": "{{camera}} Ayarları", + "autotracking": "Otomatik Takip", + "cameraEnabled": "Kamera Açık", + "objectDetection": "Nesne Tespiti", + "audioDetection": "Ses Algılama", + "transcription": "Ses Çözümlemesi" + }, + "effectiveRetainMode": { + "modes": { + "active_objects": "Aktif nesneler", + "all": "Tümü", + "motion": "Hareket" + }, + "notAllTips": "İlgili {{source}} kaynağındaki kayıt saklama politikanız şu moda ayarlı: {{effectiveRetainMode}}. Dolayısıyla şu anda gerçekleştirdiğiniz manuel talep üzerine kayıtta yalnızca {{effectiveRetainModeName}} içeren bölümler yer alacaktır." + }, + "editLayout": { + "label": "Düzeni düzenle", + "group": { + "label": "Kamera Grubunu Düzenle" + }, + "exitEdit": "Düzenlemeden Çık" + }, + "cameraAudio": { + "enable": "Kamera sesini aç", + "disable": "Kamera sesini kapat" + }, + "ptz": { + "move": { + "clickMove": { + "enable": "Tıklamayla gezintiyi aç", + "disable": "Tıklamayla gezintiyi kapat", + "label": "Kamerayı ortalamak için görüntüye tıklatın" + }, + "down": { + "label": "PTZ kamerayı aşağı çevir" + }, + "right": { + "label": "PTZ kamerayı sağa çevir" + }, + "left": { + "label": "PTZ kameryı sağa çevir" + }, + "up": { + "label": "PTZ kamerayı yukarı çevir" + } + }, + "zoom": { + "out": { + "label": "PTZ kamerayı uzaklaştır" + }, + "in": { + "label": "PTZ kamerayı yakınlaştır" + } + }, + "presets": "PTZ kamera ön ayarları", + "frame": { + "center": { + "label": "PTZ kamerayı ortalamak için görüntüye tıklatın" + } + }, + "focus": { + "in": { + "label": "PTZ kamera odağını yakınlaştır" + }, + "out": { + "label": "PTZ kamera odağını uzaklaştır" + } + } + }, + "history": { + "label": "Geçmiş görüntüleri göster" + }, + "camera": { + "enable": "Kamerayı Aç", + "disable": "Kamerayı Kapat" + }, + "suspend": { + "forTime": "Askıya alınma süresi: " + }, + "twoWayTalk": { + "disable": "Çift yönli sesi kapat", + "enable": "Çift yönli sesi aç" + }, + "snapshots": { + "enable": "Resimleri Aç", + "disable": "Resimleri Kapat" + }, + "audioDetect": { + "enable": "Ses Tespitini Aç", + "disable": "Ses Tespitini Kapat" + }, + "streamStats": { + "disable": "Yayın İstatistiklerini Gizel", + "enable": "Yayın İstatistiklerini Göster" + }, + "lowBandwidthMode": "Düşük bant genişliği modu", + "streamingSettings": "Yayın ayarları", + "audio": "Ses", + "recording": { + "enable": "Kaydı Aç", + "disable": "Kaydı Kapat" + }, + "notifications": "Bildirimler", + "detect": { + "disable": "Tespiti Kapat", + "enable": "Tespiti Aç" + }, + "transcription": { + "enable": "Canlı Ses Çözümlemeyi Aç", + "disable": "Canlı Ses Çözümlemeyi Kapat" + }, + "snapshot": { + "takeSnapshot": "Anlık görüntüyü indir", + "noVideoSource": "Anlık görüntü için kullanılabilir bir video kaynağı bulunamadı.", + "captureFailed": "Anlık görüntü yakalanamadı.", + "downloadStarted": "Anlık görüntü indirme işlemi başlatıldı." + }, + "noCameras": { + "title": "Yapılandırılmış Kamera Yok", + "description": "Frigate’e bir kamera bağlayarak başlayın.", + "buttonText": "Kamera Ekle", + "restricted": { + "title": "Kullanılabilir Kamera Yok", + "description": "Bu gruptaki kameraları görüntüleme izniniz yok." + }, + "default": { + "title": "Hiçbir kamera yapılandırılmamış", + "description": "Öncelikle bir kamerayı Frigate'e bağlayarak başlayın.", + "buttonText": "Kamera Ekle" + }, + "group": { + "title": "Grupta Kamera Yok", + "description": "Bu kamera grubuna atanmış veya etkinleştirilmiş kamera bulunmamaktadır.", + "buttonText": "Grupları Yönet" + } + } +} diff --git a/web/public/locales/tr/views/motionSearch.json b/web/public/locales/tr/views/motionSearch.json new file mode 100644 index 0000000..2ed13c9 --- /dev/null +++ b/web/public/locales/tr/views/motionSearch.json @@ -0,0 +1,10 @@ +{ + "newSearch": "Yeni Arama", + "clearResults": "Sonuçları Temizle", + "dialog": { + "cameraLabel": "Kamera" + }, + "timeRange": { + "end": "Bitiş Zamanı" + } +} diff --git a/web/public/locales/tr/views/recording.json b/web/public/locales/tr/views/recording.json new file mode 100644 index 0000000..c113d36 --- /dev/null +++ b/web/public/locales/tr/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtre", + "export": "Dışa Aktar", + "filters": "Filtreler", + "toast": { + "error": { + "noValidTimeSelected": "Geçerli bir zaman aralığı seçilmedi", + "endTimeMustAfterStartTime": "Bitiş zamanı başlangıç zamanında sonra olmalıdır" + } + }, + "calendar": "Takvim" +} diff --git a/web/public/locales/tr/views/replay.json b/web/public/locales/tr/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/tr/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/tr/views/search.json b/web/public/locales/tr/views/search.json new file mode 100644 index 0000000..2de2edf --- /dev/null +++ b/web/public/locales/tr/views/search.json @@ -0,0 +1,75 @@ +{ + "button": { + "save": "Aramayı kaydet", + "filterActive": "Fitreler açık", + "filterInformation": "Filtrele", + "clear": "Aramayı temizle", + "delete": "Kayıtlı aramayı sil" + }, + "trackedObjectId": "Takip Edilen Nesne ID", + "filter": { + "label": { + "min_score": "Min. Skor", + "recognized_license_plate": "Tanınan Plaka", + "search_type": "Arama Türü", + "has_snapshot": "Fotoğrafı var", + "cameras": "Kameralar", + "max_score": "Maks. Skor", + "labels": "Etiketler", + "time_range": "Zaman Aralığı", + "before": "Önce", + "zones": "Alanlar", + "after": "Sonra", + "has_clip": "Klibi var", + "min_speed": "Min. Hız", + "sub_labels": "Alt Etiketler", + "max_speed": "Maks. Hız", + "attributes": "Özellikler" + }, + "searchType": { + "description": "Açıklama", + "thumbnail": "Küçük resim" + }, + "tips": { + "title": "Metin filtreleri nasıl kullanılır", + "desc": { + "text": "Filtreler arama sonuçlarınızı daraltmanıza yardımcı olur. Giriş alanındaki kullanımları şöyledir:", + "step": "
    • Bir filtre adı yazın ve iki nokta üst üste (:) ile bitirin (örn. \"kameralar:\").
    • Önerilerden bir değer seçin veya kendiniz yazın.
    • Birden fazla filtreyi aralarına boşluk koyarak art arda ekleyip kullanın.
    • Tarih filtreleri (before: ve after:) {{DateFormat}} biçimini kullanır.
    • Zaman aralığı filtresi {{exampleTime}} biçimini kullanır.
    • Filtreleri kaldırmak için yanlarındaki 'x'e tıklayın.
    ", + "example": "Örnek(anahtar kelimeler ingilizce olmalıdır): cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "exampleLabel": "Örnek:", + "step1": "Bir filtre anahtarını iki nokta üst üste ile beraber yazın (örn. belli kameraları seçmek için \"cameras:\").", + "step2": "Önerilen bir değer seçin veya kendiniz girin.", + "step3": "Birden fazla filtreyi aralarında boşluk bırakarak kullanabilirsiniz.", + "step4": "Tarih filtreleri (before: ve after:) {{DateFormat}} formatını kullanır.", + "step5": "Zaman aralığı filtreleri {{exampleTime}} formatını kullanır.", + "step6": "Filtreleri kaldırmak için yanlarındaki çarpıya basın." + } + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "'Önce' tarihi 'sonra' tarihinden sonra olmalıdır.", + "maxScoreMustBeGreaterOrEqualMinScore": "Maksimum skor, minimum skora eşit veya daha fazla olmalıdır.", + "minScoreMustBeLessOrEqualMaxScore": "Minimum skor, maksimum skora eşit veya daha az olmalıdır.", + "afterDatebeEarlierBefore": "'Sonra' tarihi 'Önce' tarihinden önce olmalıdır.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Maksimum hız, minimum hıza eşit veya daha fazla olmalıdır.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Minimum hız, maksimum hıza eşit veya daha az olmalıdır." + } + }, + "header": { + "noFilters": "Filtreler", + "activeFilters": "Aktif Filtreler", + "currentFilterType": "Değerleri Filtrele" + } + }, + "placeholder": { + "search": "Ara…" + }, + "similaritySearch": { + "active": "Benzerlik araması aktif", + "title": "Benzerlik Araması", + "clear": "Benzerlik aramasını temizle" + }, + "searchFor": "{{inputValue}} için Arat", + "search": "Arama", + "savedSearches": "Kayıtlı Aramalar" +} diff --git a/web/public/locales/tr/views/settings.json b/web/public/locales/tr/views/settings.json new file mode 100644 index 0000000..72cc56e --- /dev/null +++ b/web/public/locales/tr/views/settings.json @@ -0,0 +1,1218 @@ +{ + "documentTitle": { + "default": "Ayarlar - Frigate", + "classification": "Sınıflandırma Ayarları - Frigate", + "camera": "Kamera Ayarları - Frigate", + "masksAndZones": "Maske ve Alan Düzenleyici - Frigate", + "authentication": "Kimlik Doğrulama Ayarları - Frigate", + "motionTuner": "Hareket Algılama Ayarları - Frigate", + "frigatePlus": "Frigate+ Ayarları - Frigate", + "object": "Hata Ayıklama - Frigate", + "general": "Kullanıcı Arayüzü Ayarları – Frigate", + "notifications": "Bildirim Ayarları - Frigate", + "enrichments": "Zenginleştirme Ayarları - Frigate", + "cameraManagement": "Kameraları Yönet - Frigate", + "cameraReview": "Kamera İnceleme Ayarları - Frigate" + }, + "menu": { + "masksAndZones": "Maskeler / Alanlar", + "users": "Kullanıcılar", + "frigateplus": "Frigate+", + "ui": "Arayüz", + "notifications": "Bildirimler", + "motionTuner": "Hareket Algılama", + "classification": "Sınıflandırma", + "debug": "Hata Ayıklama", + "cameras": "Kamera Ayarları", + "enrichments": "Zenginleştirmeler", + "triggers": "Tetikler", + "cameraManagement": "Yönetim", + "cameraReview": "İncele", + "roles": "Roller", + "profiles": "Profiller" + }, + "general": { + "title": "Kullanıcı Arayüzü Ayarları", + "liveDashboard": { + "automaticLiveView": { + "label": "Otomatik Canlı Görünüm", + "desc": "Aktivite tespit edildiğinde otomatik olarak kameranın canlı akışına geç. Bu seçeneği devre dışı bırakmak canlı görüntü panelinde dakikada bir güncellenen sabit resim gösterilmesine sebep olur." + }, + "playAlertVideos": { + "label": "Alarm Videolarını Oynat", + "desc": "Varsayılan olarak canlı görüntü panelinde gösterilen son uyarılar ufak videolar olarak oynatılır. Bu tarayıcı/cihazda video yerine sabit resim göstermek için bu seçeneği kapatın." + }, + "title": "Canlı Görüntü Paneli", + "displayCameraNames": { + "label": "Kamera Adlarını Daima Göster", + "desc": "Çok kameralı canlı izleme panelinde, kamera adlarını her zaman bir etiket içinde göster." + }, + "liveFallbackTimeout": { + "label": "Canlı Oynatıcı Yedeğe Geçiş Zaman Aşımı", + "desc": "Bir kameranın yüksek kaliteli canlı akışı kullanılamadığında, belirtilen saniye kadar sonra düşük bant genişliği moduna geç. Varsayılan: 3." + } + }, + "storedLayouts": { + "desc": "Kamera grubundaki kameraların düzenini kameraları sürükleyerek ve büyüterek/küçülterek değiştirebilirsiniz. Düzen bilgisi tarayıcınızda depolanır.", + "clearAll": "Tüm Düzenleri Temizle", + "title": "Kayıtlı Düzenler" + }, + "cameraGroupStreaming": { + "title": "Kamera Grubu Yayın Ayarları", + "desc": "Kamera gruplarının ilgili yayın ayarları tarayıcınızda depolanır.", + "clearAll": "Tüm Yayın Ayarlarını Temizle" + }, + "recordingsViewer": { + "defaultPlaybackRate": { + "label": "Varsayılan Oynatma Hızı", + "desc": "Kayıt oynatılırken kullanılan varsayılan oynatma hızı." + }, + "title": "Kayıt Görüntüleyicisi" + }, + "calendar": { + "firstWeekday": { + "sunday": "Pazar", + "desc": "Arayüzdeki takvimde gösterilecek haftanın ilk günü.", + "label": "Haftanın ilk günü", + "monday": "Pazartesi" + }, + "title": "Takvim" + }, + "toast": { + "success": { + "clearStreamingSettings": "Tüm kamera grupları için yayın ayarları temizlendi.", + "clearStoredLayout": "{{cameraName}} için kayıtlı düzenler temizlendi" + }, + "error": { + "clearStreamingSettingsFailed": "Yayın ayarları temizlenemedi: {{errorMessage}}", + "clearStoredLayoutFailed": "Kayıtlı düzen temizlenemedi: {{errorMessage}}" + } + } + }, + "classification": { + "title": "Sınıflandırma Ayarları", + "semanticSearch": { + "title": "Anlamsal Arama", + "readTheDocumentation": "Dökümantasyonu Oku", + "reindexNow": { + "confirmButton": "Yeniden Dizinle", + "label": "Şimdi Yeniden Dizinle", + "desc": "Yeniden dizinleme bütün takip edilen nesneler için gömüleri tekrar oluşturur. Bu işlem arka planda çalışacak olsa da nesne sayısına göre işlemcinizi tamamen kullanabilir ve tamamlanması biraz zaman alabilir.", + "alreadyInProgress": "Yeniden dizinleme zaten devam ediyor.", + "confirmTitle": "Yenden Dizinlemeyi onayla", + "success": "Yeniden dizinleme başladı.", + "confirmDesc": "Takip edilen bütün objelerin gömülerini yeniden dizinlemek istediğinze emin misiniz? Bu işlem arka planda çalışacak fakat işlemcinizi tamamen kullanabilir ve tamamlanması biraz zaman alabilir. İlerlemeyi Keşfet sayfasından takip edebilirsiniz.", + "error": "Yeniden dizinlemeye başlanamadı: {{errorMessage}}" + }, + "modelSize": { + "label": "Model boyutu", + "large": { + "title": "büyük", + "desc": "Büyük modeli kullandığınızda tam boyutlu Jina modeli kullanılacaktır ve uygunsa otomatik olarak grafik işlemcisinde çalıştırılacaktır." + }, + "small": { + "desc": "Küçük modeli kullandığınızda modelin kuantize edilmiş bir sürümü kullanılır. Bu model daha az RAM kullanır, işlemcilerde daha hızlı çalışır ve gömü kalitesinde neredeyse hiç kalite farkı yoktur.", + "title": "küçük" + }, + "desc": "Anlamsal arama için kullanılan dil modelinin büyüklüğü." + }, + "desc": "Frigate'daki Anlamsal Arama özelliği inceleme öğelerinizde takip edilen nesneleri bizzat nesnenin resmi ile aratarak ya da otomatik olarak veya kullanıcı tarafından yazılmış bir metin açıklaması içinde aratarak bulmanıza imkan sağlar." + }, + "faceRecognition": { + "modelSize": { + "large": { + "desc": "Büyük modeli kullandığınızda tam boyutlu ArcFace yüz gömü modeli kullanılacaktır ve uygunsa otomatik olarak grafik işlemcisinde çalıştırılacaktır.", + "title": "büyük" + }, + "small": { + "title": "küçük", + "desc": "Küçük modeli kullandığınızda çoğu işlemcide verimli bir şekilde çalışan bir FaceNet yüz gömme modeli kullanılır." + }, + "label": "Model Boyutu", + "desc": "Yüz tanıma için kullanılan modelin boyutu." + }, + "title": "Yüz Tanıma", + "desc": "Yüz tanıma, tanınan insanlara isim vermenize olanak tanır ve bu yüzler tanındığında Frigate, kişinin adını alt etiket olarak ekler. Bu bilgi; kullanıcı arayüzü, filtreler ve bildirimlerde gösterilir.", + "readTheDocumentation": "Dökümantasyonu Oku" + }, + "toast": { + "error": "Yapılandırma değişiklikleri kaydedilemedi: {{errorMessage}}", + "success": "Sınıflandırma ayarları kaydedildi. Değişikliklerinizi uygulamak için Frigate'i yeniden başlatın." + }, + "licensePlateRecognition": { + "desc": "Frigate araç plakalarını tanıyabilir ve algılanan karakterleri otomatik olarak recognized_license_plate alanına veya belirli bir plaka için tanımladığınız bir takma ismi alt etiket olarak ilgili aracın tanımlanan nesnesine ekleyebilir. Bu sistem, garajınıza giren veya caddeden geçen araçların plakalarını okumak için kullanılabilir.", + "title": "Plaka Tanıma", + "readTheDocumentation": "Dökümantasyonu Oku" + }, + "birdClassification": { + "title": "Kuş Sınıflandırma", + "desc": "Kuş Sınıflandırma özelliği, bilinen kuş türlerini kuantize edilmiş bir Tensorflow modeli kullanarak teşhis etmenizi sağlar. Model bir kuş türünü teşhis ettiğinde, Frigate, bu türün adını alt etiket olarak ekler. Bu bilgi; kullanıcı arayüzü, filtreler ve bildirimlerde gösterilir." + }, + "restart_required": "Yeniden Başlatma Gerekli (Sınıflandırma ayarları değiştirildi)" + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Kamera Yok" + }, + "dialog": { + "unsavedChanges": { + "title": "Kaydedilmemiş değişiklikleriniz var.", + "desc": "Devam etmeden önce değişiklikleri kaydetmek ister misiniz?" + } + }, + "camera": { + "title": "Kamera Ayarları", + "review": { + "title": "İncele", + "alerts": "Alarmlar ", + "detections": "Tespitler ", + "desc": "Bu kamera için uyarıları ve algılamaları geçici olarak etkinleştirin/devre dışı bırakın. Frigate yeniden başlatıldığında yapılandırmanızdaki tercihlere geri dönülür. Devre dışı bırakıldığında, yeni inceleme öğeleri oluşturulamaz. " + }, + "reviewClassification": { + "readTheDocumentation": "Dökümantasyonu Oku", + "selectAlertsZones": "Alarmlar için alanları seçin", + "zoneObjectDetectionsTips": { + "regardlessOfZoneObjectDetectionsTips": "{{cameraName}} kamerasındaki kategorize edilmemiş bütün {{detectionsLabels}} nesneleri hangi alanda olduklarına bakılmaksızın Algılama olarak sınıflandırılacaktır.", + "notSelectDetections": "{{cameraName}} kamerasındaki {{zone}} alanı içinde algılanan ve Alarm olarak kategorize edilmeyen bütün {{detectionsLabels}} nesneleri hangi alanda olduklarına bakılmaksızın Algılama olarak sınıflandırılacaktır.", + "text": "{{cameraName}} kamerasındaki {{zone}} alanındaki kategorize edilmemiş bütün {{detectionsLabels}} nesneleri Algılama olarak sınıflandırılacaktır." + }, + "zoneObjectAlertsTips": "{{cameraName}} kamerasındaki {{zone}} alanında algılanan bütün {{alertsLabels}} nesneleri Alarm olarak sınıflandırılacaktır.", + "desc": "Frigate tespit edilen İnceleme öğelerini Alarmlar ve Tespitler olarak kategorize eder. Varsayılan olarak bütün kişi ve araba nesneleri Alarm olarak sınıflandırılır. İnceleme öğelerinizin sınıflandırmasını tespit sınırlandırma sayfasında gerekli alanlar seçerek sınırlandırabilirsiniz.", + "objectDetectionsTips": "{{cameraName}} kamerasında kategorize edilmemiş bütün {{detectionsLabels}} nesneleri, hangi alanda olduklarına bakılmaksızın Algılama olarak sınıflandırılacaktır.", + "selectDetectionsZones": "Algılamalar için alanları seçin", + "title": "Tespit Sınıflandırmaları", + "noDefinedZones": "Bu kamera için tanımlanmış alan yok.", + "objectAlertsTips": "{{cameraName}} kamerasındaki bütün {{alertsLabels}} nesneleri Alarm olarak sınıflandırılacaktır.", + "limitDetections": "Algılamaları belirli alanlara sınırla", + "toast": { + "success": "İnceleme Sınıflandırma ayarları kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın." + }, + "unsavedChanges": "{{camera}} için Kaydedilmemiş Sınıflandırma Ayarları" + }, + "streams": { + "desc": "Frigate yeniden başlataılana kadar bir kamerayı devre dışı bırakın. Bir kameranın devre dışı bırakılması, Frigate'in bu kamerayı işlemesini tamamen durdurur. Algılama, kayıt ve hata ayıklama özellikleri kullanılamaz.
    Not: Bu eylem, go2rtc'deki yeniden akışları devre dışı bırakmaz.", + "title": "Akışlar" + }, + "object_descriptions": { + "title": "Üretken AI Nesne Açıklamaları", + "desc": "Bu kamera için Üretken Yapay Zeka kullanarak nesne açıklamaları oluşturmayı geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, bu kamerada takip edilen nesneler için yapay zekadan nesne açıklamaları talep edilmeyecektir." + }, + "review_descriptions": { + "title": "Üretken AI İnceleme Öğesi Açıklamaları", + "desc": "Bu kamera için Üretken Yapay Zeka kullanarak inceleme öğelerinin açıklamalarını oluşturmayı geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, bu kameraya bağlı inceleme öğeleri için yapay zekadan açıklama metni talep edilmeyecektir." + }, + "addCamera": "Yeni Kamera Ekle", + "editCamera": "Kamerayı Düzenle:", + "selectCamera": "Kamera Seç", + "backToSettings": "Kamera Ayarlarına Dön", + "cameraConfig": { + "add": "Kamera Ekle", + "edit": "Kamerayı Düzenle", + "description": "Kameranızın ayarlarını, kameraların akışları ve roller de dahil olacak şekilde yapılandırın.", + "name": "Kamera İsmi", + "nameRequired": "Kamera adı gereklidir", + "nameInvalid": "Kamera adı yalnızca harf, rakam, alt çizgi veya tire içerebilir", + "namePlaceholder": "örn: onkapi", + "enabled": "Açık", + "ffmpeg": { + "inputs": "Kamera Girdi Akışları", + "path": "Akış Yolu", + "pathRequired": "Akış yolu gereklidir", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "En az bir rol gereklidir", + "rolesUnique": "Her rol (ses, tespit, kayıt) yalnızca bir adet yayına atanabilir. Her rol aynı akışı kullanabilir, lakin bir rol birden fazla akışa atanamaz.", + "addInput": "Girdi Akışı Ekle", + "removeInput": "Girdi Akışını Kaldır", + "inputsRequired": "En az bir girdi akışı gereklidir" + }, + "toast": { + "success": "Kamera {{cameraName}} başarıyla kaydedildi" + }, + "nameLength": "Kamera ismi 24 karakterden kısa olmalıdır." + } + }, + "masksAndZones": { + "filter": { + "all": "Bütün Maskeler ve Alanlar" + }, + "toast": { + "success": { + "copyCoordinates": "{{polyName}} için koordinatlar panoya kopyalandı." + }, + "error": { + "copyCoordinatesFailed": "Koordinatlar panoya kopyalanamadı." + } + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Alan adı en az 2 karakter olmalıdır.", + "hasIllegalCharacter": "Alan adı geçersiz karakterler içeriyor.", + "mustNotBeSameWithCamera": "Alan adı kamera adıyla aynı olmamalıdır.", + "alreadyExists": "Bu kamera için bu ada sahip bir alan zaten mevcut.", + "mustNotContainPeriod": "Alan adı nokta içermemelidir.", + "mustHaveAtLeastOneLetter": "Bölge ismi en az bir harf içermelidir." + } + }, + "distance": { + "error": { + "text": "Mesafe 0.1'den büyük veya eşit olmalıdır.", + "mustBeFilled": "Hız tahmini özelliğini kullanabilmek için bütün mesafe alanları doldurulmalıdır." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Oyalanma süresi 0'dan büyük veya eşit olmalıdır." + } + }, + "polygonDrawing": { + "snapPoints": { + "false": "Noktaları hizalama", + "true": "Noktaları hizala" + }, + "removeLastPoint": "Son noktayı kaldır", + "reset": { + "label": "Bütün noktaları temizle" + }, + "delete": { + "desc": "{{type}} {{name}}'i silmek istediğinizden emin misiniz?", + "title": "Silmeyi Onayla", + "success": "{{name}} silindi." + }, + "error": { + "mustBeFinished": "Kaydetmeden önce çokgen çizimi bitirilmelidir." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Eylemsizlik sıfırın üzerinde olmalıdır." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Hız eşiği 0.1'e eşit veya daha yüksek olmalıdır." + } + } + }, + "zones": { + "label": "Alanlar", + "documentTitle": "Alanı Düzenle - Frigate", + "desc": { + "documentation": "Dökümantasyon", + "title": "Alanlar, görüntüde belirli bir alanını tanımlamanıza olanak tanır. Böylece bir nesnenin belirli bir alanda olup olmadığını tespit edebilirsiniz." + }, + "add": "Alan Ekle", + "point_one": "{{count}} nokta", + "point_other": "{{count}} nokta", + "name": { + "inputPlaceHolder": "Bir isim girin…", + "title": "İsim", + "tips": "İsim 2 karakter veya daha uzun olmalı, en az bir harf içermeli ve bu kameradaki bir kamera ismi veya başka bir bölge ismiyle çakışmamalıdır." + }, + "inertia": { + "title": "Eylemsizlik", + "desc": "Bir nesnenin alanda kabul edilmesi için kaç kare boyunca alanda kalması gerektiğini belirtir. Varsayılan: 3" + }, + "objects": { + "title": "Nesneler", + "desc": "Bu alana uygulanan nesnelerin listesi." + }, + "speedEstimation": { + "title": "Hız Tahmini", + "desc": "Bu alandaki nesneler için hız tahminini etkinleştirin. Alan tam olarak 4 noktaya belirlenmiş olmalıdır.", + "docs": "Dökümantasyonu oku", + "lineADistance": "Çizgi A mesafesi ({{unit}})", + "lineBDistance": "Çizgi B mesafesi ({{unit}})", + "lineCDistance": "Çizgi C mesafesi ({{unit}})", + "lineDDistance": "Çizgi D mesafesi ({{unit}})" + }, + "clickDrawPolygon": "Görüntü üzerinde bir çokgen çizmek için tıklayın.", + "edit": "Alanı Düzenle", + "loiteringTime": { + "title": "Oyalanma Süresi", + "desc": "Nesnenin etkinleşmesi için alanda kalması gereken minimum süreyi saniye cinsinden ayarlar. Varsayılan: 0" + }, + "speedThreshold": { + "desc": "Nesnelerin bu alanda kabul edilmesi için minimum hızı belirtir.", + "toast": { + "error": { + "pointLengthError": "Bu alan için hız tahmini devre dışı bırakıldı. Hız tahmini olan alanlar tam olarak 4 noktaya sahip olmak zorundadır.", + "loiteringTimeError": "Oyalanma süreleri 0'dan büyük olan alanlar hız tahmini ile birlikte kullanılmamalıdır." + } + }, + "title": "Hız Alt Sınırı ({{unit}})" + }, + "toast": { + "success": "Bölge ({{zoneName}}) kaydedildi." + }, + "allObjects": "Bütün Nesneler" + }, + "motionMasks": { + "add": "Yeni Hareket Maskesi", + "context": { + "documentation": "Dökümantasyonu okuyun", + "title": "Hareket maskeleri, istenmeyen hareketli nesnelerin (örneğin: ağaç dalları, kamera yayınına gömülü tarih saat, vb.) algılamayı tetiklemesini önlemek için kullanılır. Hareket maskeleri çok dikkatli kullanılmalıdır, zira gereğinden fazla maskeleme nesnelerin tespitini zorlaştıracaktır." + }, + "point_one": "{{count}} nokta", + "point_other": "{{count}} nokta", + "clickDrawPolygon": "Görüntü üzerinde bir çokgen çizmek için tıklayın.", + "polygonAreaTooLarge": { + "title": "Bu hareket maskesi, kamera görüntüsünün %{{polygonArea}}'sını kaplıyor. Büyük hareket maskeleri kullanmanız önerilmez.", + "documentation": "Dökümantasyonu oku", + "tips": "Hareket maskeleri nesnelerin algılanmasını kesin olarak engellemez. Bunun yerine tespit sınıflandırma sayfasında gerekli alan kısıtlaması ayarlamalısınız." + }, + "toast": { + "success": { + "title": "{{polygonName}} kaydedildi.", + "noName": "Hareket Maskesi kaydedildi." + } + }, + "desc": { + "title": "Hareket maskeleri, istenmeyen hareketlerin algılamayı tetiklemesini önlemek için kullanılır. Gereğinden fazla maskeleme nesnelerin tespitini zorlaştıracaktır.", + "documentation": "Dökümantasyon" + }, + "label": "Hareket Maskesi", + "edit": "Hareket Maskesini Düzenle", + "documentTitle": "Hareket Maskesini Düzenle - Frigate" + }, + "objectMasks": { + "desc": { + "documentation": "Dökümantasyon", + "title": "Nesne filtresi maskeleri, belirli bir nesne türü için konum bazında yanlış pozitifleri filtrelemek için kullanılır." + }, + "point_one": "{{count}} nokta", + "point_other": "{{count}} nokta", + "context": "Nesne filtresi maskeleri, belirli bir nesne türü için konum bazında yanlış pozitifleri filtrelemek için kullanılır.", + "objects": { + "allObjectTypes": "Bütün nesne türleri", + "desc": "Bu nesne maskesinin uygulanacağı nesne türü.", + "title": "Nesneler" + }, + "add": "Nesne Maskesi Ekle", + "edit": "Nesne Maskesini Düzenle", + "toast": { + "success": { + "noName": "Nesne Maskesi kaydedildi.", + "title": "{{polygonName}} kaydedildi." + } + }, + "documentTitle": "Nesne Maskesini Düzenle - Frigate", + "label": "Nesne Maskeleri", + "clickDrawPolygon": "Görüntü üzerinde bir çokgen çizmek için tıklayın." + }, + "restart_required": "Yeniden Başlatma Gerekli (maskeler/alanlar değiştirildi)", + "motionMaskLabel": "Hareket Maskesi {{number}}", + "objectMaskLabel": "Nesne maskesi {{number}} ({{label}})" + }, + "motionDetectionTuner": { + "title": "Hareket Algılama Ayarlayıcı", + "Threshold": { + "desc": "Eşik değeri, bir pikselin parlaklığındaki ne kadar değişikliğin hareket olarak kabul edileceğini belirler. Varsayılan: 30", + "title": "Eşik" + }, + "contourArea": { + "desc": "Kontur alanı değeri, hangi değişen piksel gruplarının hareket olarak nitelendirileceğine karar vermek için kullanılır. Varsayılan: 10", + "title": "Kontur Alanı" + }, + "toast": { + "success": "Hareket ayarları kaydedildi." + }, + "desc": { + "title": "Frigate, kamera görüntüsünde nesne tespiti ile kontrol etmeye değer bir şey olup olmadığını görmek için ilk olarak hareket algılamayı kullanır.", + "documentation": "Hareket Algılama İnce Ayar Kılavuzunu Okuyun" + }, + "improveContrast": { + "desc": "Daha karanlık sahneler için kontrastı iyileştirin. Varsayılan: AÇIK", + "title": "Kontrastı İyileştir" + }, + "unsavedChanges": "Kaydedilmemiş Hareket Alıglama ayarları ({{camera}})" + }, + "debug": { + "title": "Hata Ayıklama", + "boundingBoxes": { + "colors": { + "label": "Nesne Çerçeve Renkleri", + "info": "
  • Başlangıçta, her nesne etiketi için farklı renkler atanacaktır
  • Koyu mavi ince bir çizgi, nesnenin şu anda algılanamadığını gösterir
  • Gri ince bir çizgi, nesnenin sabit olduğunun algılandığını gösterir
  • Kalın bir çizgi, nesnenin otomatik izlemeye (eğer açıksa) tabi olduğunu gösterir
  • " + }, + "title": "Çerçeveler", + "desc": "İzlenen nesnelerin etrafında çerçeve göster" + }, + "timestamp": { + "title": "Zaman Damgası", + "desc": "Görüntüye bir zaman damgası ekle" + }, + "motion": { + "title": "Hareket kutuları", + "desc": "Hareketin algılandığı alanların etrafında çerçeve göster", + "tips": "

    Hareket Kutuları


    Hareketin algılandığı alanlar kırmızı çerçeve ile gösterilecektir.

    " + }, + "regions": { + "title": "Tespit Bölgeleri", + "desc": "Nesne algılayıcıya gönderilen tespit alanlarını göster", + "tips": "

    Bölge Kutuları


    Nesne dedektörüne gönderilen tespit alanları görüntüde parlak yeşil renk çerçeve ile gösterilir.

    " + }, + "objectShapeFilterDrawing": { + "title": "Nesne Şekil Filtresi Çizimi", + "desc": "Alan ve oran ayrıntılarını görüntülemek için görüntü üzerinde bir dikdörtgen çizin", + "score": "Puan", + "ratio": "Oran", + "area": "Alan", + "document": "Dökümantasyonu oku ", + "tips": "Alanını ve oranını göstermek için kamera görüntüsü üzerinde bir dikdörtgen çizmek için bu seçeneği etkinleştirin. Bu değerler daha sonra yapılandırmanızda nesne şekil filtresi parametrelerini ayarlamak için kullanılabilir." + }, + "debugging": "Hata Ayıklama", + "detectorDesc": "Frigate, kamera video akışınızdaki nesneleri algılamak için algılayıcılar ({{detectors}}) kullanır.", + "noObjects": "Nesne Yok", + "mask": { + "title": "Hareket maskeleri", + "desc": "Tanımlanmış hareket maskelerinin sınırlarını göster" + }, + "zones": { + "title": "Alanlar", + "desc": "Tanımlanmış alanların sınırlarını göster" + }, + "objectList": "Nesne Listesi", + "desc": "Hata ayıklama görünümü, izlenen nesnelerin ve istatistiklerinin gerçek zamanlı bir görünümünü gösterir. Nesne listesi algılanan nesnelerin zaman gecikmeli bir özetini gösterir.", + "paths": { + "title": "Hareket İzi", + "desc": "Takip edilen nesnenin hareket izi üzerindeki önemli noktaları göster", + "tips": "

    Hareket İzi


    Çizgiler ve daireler, takip edilen nesnenin yaşam döngüsü boyunca hareket ettiği önemli noktaları gösterir.

    " + }, + "openCameraWebUI": "{{camera}}'nın Web Arayüzünü Aç", + "audio": { + "title": "Ses", + "noAudioDetections": "Ses tespiti yok", + "score": "skor", + "currentRMS": "Şu Anki RMS", + "currentdbFS": "Şu Anki dbFS" + } + }, + "users": { + "title": "Kullanıcılar", + "management": { + "title": "Kullanıcı Yönetimi", + "desc": "Bu Frigate kurulumundaki kullanıcı hesaplarını yönetin." + }, + "addUser": "Kullanıcı Ekle", + "toast": { + "success": { + "deleteUser": "{{user}} kullanıcısı başarıyla silindi", + "roleUpdated": "{{user}} için rol güncellendi", + "createUser": "{{user}} kullanıcısı başarıyla oluşturuldu", + "updatePassword": "Parola başarıyla güncellendi." + }, + "error": { + "setPasswordFailed": "Parola kaydedilemedi: {{errorMessage}}", + "createUserFailed": "Kullanıcı oluşturulamadı: {{errorMessage}}", + "deleteUserFailed": "Kullanıcı silinemedi: {{errorMessage}}", + "roleUpdateFailed": "Rol güncellenemedi: {{errorMessage}}" + } + }, + "table": { + "username": "Kullanıcı Adı", + "actions": "Eylemler", + "noUsers": "Kullanıcı bulunamadı.", + "changeRole": "Kullanıcı rolünü değiştir", + "deleteUser": "Kullanıcıyı sil", + "role": "Rol", + "password": "Parola Sıfırla" + }, + "dialog": { + "form": { + "user": { + "placeholder": "Kullanıcı adı girin", + "title": "Kullanıcı Adı", + "desc": "Yalnızca harfler, sayılar, noktalar ve alt çizgiler kullanabilirsiniz." + }, + "password": { + "title": "Parola", + "placeholder": "Parola girin", + "confirm": { + "title": "Parolayı Onayla", + "placeholder": "Parolayı Onayla" + }, + "strength": { + "title": "Parola kuvveti: ", + "weak": "Zayıf", + "medium": "Orta", + "strong": "Güçlü", + "veryStrong": "Çok Güçlü" + }, + "notMatch": "Parolalar eşleşmiyor", + "match": "Parolalar eşleşiyor", + "show": "Parolay⁸ göster", + "hide": "Parolayı gizle", + "requirements": { + "title": "Parola gereksinimleri:", + "length": "En az 8 karakter", + "uppercase": "En az bir büyük harf", + "digit": "En az bir rakam", + "special": "En az bir özel karakter (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "placeholder": "Yeni parola girin", + "confirm": { + "placeholder": "Yeni parolayı tekrar girin" + }, + "title": "Yeni Parola" + }, + "usernameIsRequired": "Kullanıcı adı gereklidir", + "passwordIsRequired": "Parola gereklidir", + "currentPassword": { + "title": "Mevcut Parola", + "placeholder": "Mevcut parolanızı girin" + } + }, + "createUser": { + "title": "Yeni Kullanıcı Oluştur", + "desc": "Yeni bir kullanıcı hesabı ekleyin ve Frigate'deki erişim düzeylerini sınırlandırmak için bir rol belirtin.", + "usernameOnlyInclude": "Kullanıcı adı yalnızca harfler, sayılar, nokta veya alt çizgi (_) içerebilir", + "confirmPassword": "Lütfen parolanızı onaylayın" + }, + "deleteUser": { + "title": "Kullanıcıyı Sil", + "warn": "{{username}}'i silmek istediğinizden emin misiniz?", + "desc": "Bu işlem geri alınamaz. Bu, kullanıcı hesabını kalıcı olarak silecek ve tüm ilişkili verileri kaldıracaktır." + }, + "passwordSetting": { + "updatePassword": "{{username}} için Parola Belirle", + "setPassword": "Parola Belirle", + "desc": "Bu hesabı güvenli hale getirmek güçlü bir parola belirleyin.", + "cannotBeEmpty": "Parola boş olamaz", + "doNotMatch": "Parolalar eşleşmiyor", + "currentPasswordRequired": "Mevcut parola gereklidir", + "incorrectCurrentPassword": "Mevcut parola yanlış", + "passwordVerificationFailed": "Parola doğrulanamadı", + "multiDeviceWarning": "Oturum açtığınız diğer tüm cihazların {{refresh_time}} süresi içinde yeniden oturum açması gerekecektir.", + "multiDeviceAdmin": "JWT gizli anahtarınızı yenileyerek tüm kullanıcıları derhal yeniden doğrulama yapmaya zorlayabilirsiniz." + }, + "changeRole": { + "title": "Kullanıcı Rolünü Değiştir", + "desc": "{{username}} için izinleri güncelle", + "roleInfo": { + "adminDesc": "Tüm özelliklere tam erişim.", + "intro": "Bu kullanıcı için bir rol seçin:", + "admin": "Yönetici", + "viewer": "Görüntüleyici", + "viewerDesc": "Yalnızca Canlı, İncele, Keşfet ve Dışa Aktar'a girebilir.", + "customDesc": "Belirli kamera erişimine sahip özel rol." + }, + "select": "Bir rol seçin" + } + }, + "updatePassword": "Parola Sıfırla" + }, + "notification": { + "title": "Bildirimler", + "notificationSettings": { + "title": "Bildirim Ayarları", + "documentation": "Dökümantasyonu Oku", + "desc": "Frigate, tarayıcıdan veya web uygulaması (PWA) olarak kullanıyor olmanız fark etmeksizin, tarayıcınızın bildirimler özelliği aracılığıyla bildirimler gönderebilir." + }, + "notificationUnavailable": { + "title": "Bildirimler Kullanılamıyor", + "documentation": "Dökümantasyonu Oku", + "desc": "Web push bildirimleri güvenli bir bağlam gerektirir (https://…). Bu, tarayıcı sınırlamasıdır. Bildirimleri kullanmak için Frigate'e güvenli bir şekilde (https) erişin." + }, + "globalSettings": { + "title": "Genel Ayarlar", + "desc": "Kayıtlı tüm cihazlarda belirli kameralar için bildirimleri geçici olarak askıya alın." + }, + "email": { + "title": "E-posta", + "desc": "Geçerli bir e-posta adresi gereklidir ve push hizmetiyle ilgili herhangi bir sorun olması durumunda sizi bilgilendirmek için kullanılacaktır.", + "placeholder": "örn. ornek@eposta.com" + }, + "cameras": { + "desc": "Bildirimlerin etkinleştirileceği kameraları seçin.", + "title": "Kameralar", + "noCameras": "Kullanılabilir kamera yok" + }, + "deviceSpecific": "Cihaza Özel Ayarlar", + "suspended": "Bildirimler askıya alındı {{time}}", + "suspendTime": { + "1hour": "1 saat süreyle askıya al", + "12hours": "12 saat süreyle askıya al", + "24hours": "24 saat süreyle askıya al", + "30minutes": "30 dakika süreyle askıya al", + "untilRestart": "Yeniden başlatılana kadar askıya al", + "10minutes": "10 dakika süreyle askıya al", + "5minutes": "5 dakika süreyle askıya al", + "suspend": "Askıya Al" + }, + "toast": { + "success": { + "registered": "Bildirimlere başarıyla kaydolundu. Herhangi bir bildirimin (test bildirimi dahil) gönderilebilmesi için Frigate'in yeniden başlatılması gereklidir.", + "settingSaved": "Bildirim ayarları kaydedildi." + }, + "error": { + "registerFailed": "Bildirimlere kaydolunurken hata oluştu." + } + }, + "registerDevice": "Bu Cihazı Kaydet", + "sendTestNotification": "Bir test bildirimi gönder", + "cancelSuspension": "Askıya Almayı İptal Et", + "unregisterDevice": "Bu Cihazın Kaydını Sil", + "active": "Bildirimler Aktif", + "unsavedChanges": "Kaydedilmemiş bildirim ayar değişiklikleri", + "unsavedRegistrations": "Kaydedilmemiş bildirim ayar değişiklikleri" + }, + "frigatePlus": { + "title": "Frigate+ Ayarları", + "apiKey": { + "title": "Frigate+ API Anahtarı", + "validated": "Frigate+ API anahtarınız doğrulandı", + "plusLink": "Frigate+ hakkında daha fazla bilgi edinin", + "notValidated": "Frigate+ API anahtarı bulunamadı veya doğrulanamadı", + "desc": "Frigate+ API anahtarı, Frigate+ hizmetiyle entegrasyonu sağlar." + }, + "snapshotConfig": { + "title": "Fotoğraf Yapılandırması", + "documentation": "Dökümantasyonu oku", + "table": { + "camera": "Kamera", + "snapshots": "Fotoğraflar", + "cleanCopySnapshots": "clean_copy Fotoğraflar" + }, + "desc": "Frigate+'a göndermek için yapılandırmanızda hem fotoğrafların hem de clean_copy fotoğraflarının etkinleştirilmesi gerekir.", + "cleanCopyWarning": "Bazı kameralarda fotoğraflar etkin ancak temiz kopya özelliği devre dışı. Bu kameralardan Frigate+'a görüntü gönderebilmek için fotoğraf(snapshots) yapılandırmanızda clean_copy'yi etkinleştirmeniz gerekiyor." + }, + "modelInfo": { + "error": "Model bilgileri yüklenemedi", + "loadingAvailableModels": "Kullanılabilir modeller yükleniyor…", + "loading": "Model bilgileri yükleniyor…", + "modelType": "Model Türü", + "dimensions": "Boyutlar", + "availableModels": "Kullanılabilir Modeller", + "modelSelect": "Frigate+'daki kullanılabilir modelleriniz buradan seçilebilir. Yalnızca mevcut algılayıcı yapılandırmanızla uyumlu modellerin seçilebileceğini unutmayın.", + "baseModel": "Temel Model", + "title": "Model Bilgileri", + "trainDate": "Eğitim Tarihi", + "supportedDetectors": "Desteklenen Algılayıcılar", + "cameras": "Kameralar", + "plusModelType": { + "userModel": "İnce Ayarlı", + "baseModel": "Baz Model" + } + }, + "toast": { + "success": "Frigate+ ayarları kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın.", + "error": "Yapılandırma değişiklikleri kaydedilemedi: {{errorMessage}}" + }, + "restart_required": "Yeniden Başlatma Gerekli (Frigate+ modeli değiştirildi)", + "unsavedChanges": "Kaydedilmemiş Frigate+ ayar değişiklikleri" + }, + "enrichments": { + "birdClassification": { + "title": "Kuş Sınıflandırma", + "desc": "Kuş sınıflandırma, kuantize edilmiş bir Tensorflow modeli aracılığıyla bilinen kuş türlerini tespit eder. Bilinen bir kuş türü tespit edildiğinde, bu türün bilinen adı bir alt etiket olarak ilgili öğeye iliştirilir. Bu bilgi; arayüzde ve bildirimlerde gösterilir ve filtre olarak kullanılabilir." + }, + "unsavedChanges": "Değişitirilen zenginleştirme ayarları kaydedilmedi", + "semanticSearch": { + "reindexNow": { + "desc": "Yeniden dizinleme işlemi, bütün takip edilen nesneler için gömüleri tekrar üretecektir. Bu işlem arka planda gerçekleşir, fakat nesne sayısına bağlı olarak çok işlemci kullanabilir veya tamamlanması makul bir süre alabilir.", + "label": "Şimdi Yeniden Dizinle", + "confirmTitle": "Yeniden Dizinlemeyi Onayla", + "confirmDesc": "Bütün takip edilen nesnelerin gömülerini yeniden dizinlemek istediğinize emin misiniz? Bu işlem arka planda gerçekleşecektir fakat nesne sayısına bağlı olarak çok işlemci kullanabilir ve biraz süre alabilir. İlerlemesini Keşfet sayfasından görüntüleyebilirsiniz.", + "confirmButton": "Yeniden Dizinle", + "success": "Yeniden dizinleme başarıyla başlatıldı.", + "alreadyInProgress": "Yeniden dizinleme zaten sürüyor.", + "error": "Yeniden dizinleme başlatılamadı: {{errorMessage}}" + }, + "title": "Anlamsal Arama", + "desc": "Anlamsal arama, takip edilen nesneleri; ya görselin kendisini kullanarak ya da görsel öğelere ait açıklamalar üzerinden (kullanıcı tarafından yazılmış ya da otomatik oluşturulmuş) metinle arama yaparak bulmanıza olanak tanır.", + "readTheDocumentation": "Dökümantasyonu Oku", + "modelSize": { + "label": "Model Boyutu", + "desc": "Anlamsal aramadaki gömüler için kullanılan modelin büyüklüğü.", + "small": { + "title": "küçük", + "desc": "Modelin küçük sürümü, gömme kalitesinde fark edilmesi zor bir değişikliğe karşılık daha az RAM kullanan ve CPU’da daha hızlı çalışan kuantize bir model kullanır." + }, + "large": { + "title": "büyük", + "desc": "Modelin büyük sürümü tam boyutlu Jina modelini kullanır ve uygunsa GPU'da çalışacaktır." + } + } + }, + "title": "Zenginleştirme Ayarları", + "faceRecognition": { + "title": "Yüz Tanıma", + "desc": "Yüz tanıma, tespit edilen insanların yüzleri tanındığında onlara isim atamanıza olanak sağlar ve bu isim bilgisi alt etiket olarak iliştirilir. Bu bilgi; arayüzde ve bildirimlerde gösterilir ve filtre olarak kullanılabilir.", + "readTheDocumentation": "Dökümantasyonu Oku", + "modelSize": { + "label": "Model Boyutu", + "desc": "Yüz tanıma için kullanılan modelin büyüklüğü.", + "small": { + "title": "küçük", + "desc": "Modelin küçük sürümü olarak çoğu işlemcide verimli çalışabilen bir FaceNet yüz gömü modeli kullanılır." + }, + "large": { + "title": "büyük", + "desc": "Modelin büyük sürümü olarak ArcFace yüz gömü modeli kullanılır ve uygunsa GPU'da çalışır." + } + } + }, + "licensePlateRecognition": { + "title": "Plaka Tanıma", + "desc": "Frigate tespit edilen araçların tescil plakalarındaki karakterleri okuyabilir ve ilgili araba türündeki nesnenin recognized_license_plate bölümüne, varsa bilindik bir adını da sub_label bölümüne ekleyebilir. Bu özelliği bir yoldan geçen yahut evin önüne park eden araçların plakalarını okumak için kullanabilirsiniz.", + "readTheDocumentation": "Dökümantasyonu Oku" + }, + "restart_required": "Yeniden başlatma gerekli (zenginleştirme ayarları değişti)", + "toast": { + "success": "Zenginleştirme ayarları kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın.", + "error": "Yapılandırma değişiklikleri kaydedilemedi: {{errorMessage}}" + } + }, + "triggers": { + "dialog": { + "form": { + "name": { + "error": { + "invalidCharacters": "Girdi yalnızca harf, rakam, alt çizgi ve tire içerebilir.", + "minLength": "Girdi en az 2 karakter uzunluğunda olmalıdır.", + "alreadyExists": "Bu kamerada aynı isimle bir tetik zaten mevcut." + }, + "title": "İsim", + "placeholder": "Bu tetikleyiciye isim verin", + "description": "Bu tetikleyiciyi tanımlamak için benzersiz bir isim veya açıklama girin" + }, + "enabled": { + "description": "Bu tetiği açın veya kapatın" + }, + "type": { + "title": "Tetik Türü", + "placeholder": "Tetik türünü seçin", + "description": "Benzer izlenen nesne açıklaması algılandığında tetiklenir", + "thumbnail": "Benzer izlenen nesne küçük resmi algılandığında tetiklenir" + }, + "content": { + "title": "İçerik", + "imagePlaceholder": "Bir küçük resim seçin", + "textPlaceholder": "Metin içeriği girin", + "imageDesc": "Yalnızca en son 100 küçük resim görüntülenir. İstediğiniz küçük resmi bulamazsanız, lütfen Keşfet bölümündeki önceki nesneleri inceleyin ve oradaki menüden bir tetikleyici ayarlayın.", + "textDesc": "Benzer bir takip edilen nesne açıklaması algılandığında bu eylemi tetiklemek için metin girin.", + "error": { + "required": "İçerik gereklidir." + } + }, + "threshold": { + "title": "Tetik Eşiği", + "error": { + "min": "Tetik eşiği 0 ile 1 arasında olmalıdır", + "max": "Tetik eşiği 0 ile 1 arasında olmalıdır" + }, + "desc": "Bu tetikleyici için benzerlik eşiğini ayarlayın. Daha yüksek bir eşik, tetiği tetiklemek için daha yakın bir eşleşme gerektiği anlamına gelir." + }, + "actions": { + "title": "Eylemler", + "desc": "Varsayılan olarak, Frigate tüm tetikleyici isimlerini bir MQTT mesajı olarak gönderir. Alt etiketler, tetikleyici ismini nesne etiketine ekler. Nitelikler, izlenen nesne meta verilerinde ayrı olarak depolanan aranabilir meta verilerdir.", + "error": { + "min": "En az bir eylem seçilmelidir." + } + } + }, + "createTrigger": { + "title": "Tetik Oluştur", + "desc": "{{camera}} kamerası için tetik oluşturun" + }, + "editTrigger": { + "title": "Tetiği Düzenle", + "desc": "{{camera}} kamerasındaki tetiğin ayarlarını düzenleyin" + }, + "deleteTrigger": { + "title": "Tetiği Sil", + "desc": "{{triggerName}} isimli tetiği silmek istediğinizden emin misiniz? Bu işlem geri alınamaz." + } + }, + "documentTitle": "Tetikler", + "management": { + "title": "Tetikleyiciler", + "desc": "{{camera}} için tetikleri yönetin. Seçtiğiniz takip edilen nesneye benzer küçük resimlerde tetiklemek için küçük resmi kullanın veya belirlediğiniz metne benzer açıklamalar çıkması durumunda tetiklemek için ise açıklama seçeneğini kullanın." + }, + "addTrigger": "Tetik Ekle", + "table": { + "name": "İsim", + "type": "Tetik Türü", + "content": "İçerik", + "threshold": "Tetik Eşiği", + "actions": "Eylemler", + "noTriggers": "Bu kamera için hiç bir tetik ayarlanmadı.", + "edit": "Düzenle", + "deleteTrigger": "Tetiği Sil", + "lastTriggered": "En son tetikleme" + }, + "type": { + "thumbnail": "Küçük Resim", + "description": "Açıklama" + }, + "actions": { + "alert": "Alarm Olarak İşaretle", + "notification": "Bildirim Gönder", + "sub_label": "Alt Etiket Ekle", + "attribute": "Özellik Ekle" + }, + "toast": { + "success": { + "createTrigger": "Tetik {{name}} başarıyla oluşturuldu.", + "updateTrigger": "Tetik {{name}} başarıyla güncellendi.", + "deleteTrigger": "Tetik {{name}} başarıyla silindi." + }, + "error": { + "createTriggerFailed": "Tetik oluşturulamadı: {{errorMessage}}", + "updateTriggerFailed": "Tetik güncellenemedi: {{errorMessage}}", + "deleteTriggerFailed": "Tetik silinemedi: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Anlamsal Arama devre dışı bırakıldı", + "desc": "Tetikleyicileri kullanmak için Anlamsal Arama'nın etkinleştirilmesi gerekir." + }, + "wizard": { + "title": "Tetikleyici Oluştur", + "step1": { + "description": "Tetikleyiciniz için temel ayarları yapılandırın." + }, + "step2": { + "description": "Bu eylemi tetikleyecek içeriği ayarlayın." + }, + "step3": { + "description": "Bu tetikleyici için eşik değerini ve eylemleri yapılandırın." + }, + "steps": { + "nameAndType": "İsim ve Tür", + "configureData": "Verileri Yapılandır", + "thresholdAndActions": "Eşik ve Eylemler" + } + } + }, + "cameraWizard": { + "title": "Kamera Ekle", + "description": "Aşağıdaki adımları izleyerek Frigate kurulumunuza yeni bir kamera ekleyin.", + "steps": { + "nameAndConnection": "İsim & Bağlantı", + "probeOrSnapshot": "Probe veya Anlık Görüntü", + "streamConfiguration": "Akış Yapılandırması", + "validationAndTesting": "Doğrulama ve Test" + }, + "save": { + "success": "Yeni kamera {{cameraName}} başarıyla kaydedildi.", + "failure": "{{cameraName}} kaydedilirken hata oluştu." + }, + "testResultLabels": { + "resolution": "Çözünürlük", + "video": "Video", + "audio": "Ses", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Lütfen geçerli bir akış URL'si sağlayın", + "testFailed": "Akış testi başarısız oldu: {{error}}" + }, + "step1": { + "description": "Kamera bilgilerinizi girin ve kamerayı taramayı (probe) ya da markayı manuel olarak seçmeyi tercih edin.", + "cameraName": "Kamera İsmi", + "cameraNamePlaceholder": "örn. onkapi, veya Arka Bahçe Genel Görünümü", + "host": "Ana makine adı veya IP Adresi", + "port": "Port", + "username": "Kullanıcı adı", + "usernamePlaceholder": "İsteğe bağlı", + "password": "Parola", + "passwordPlaceholder": "İsteğe bağlı", + "selectTransport": "İletişim protokolünü seçin", + "cameraBrand": "Kamera Markası", + "selectBrand": "URL şablonu için kamera markasını seçin", + "customUrl": "Özel Akış URL’si", + "brandInformation": "Marka Bilgileri", + "brandUrlFormat": "RTSP URL formatı şu şekilde olan kameralar için: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://kullanıcıadı:parola@host:port/path", + "connectionSettings": "Bağlantı Ayarları", + "detectionMethod": "Akış Algılama Yöntemi", + "onvifPort": "ONVIF Portu", + "probeMode": "Kamerayı tara", + "manualMode": "Manuel seçim", + "detectionMethodDescription": "Kamera akış URL’lerini bulmak için kamerayı ONVIF ile tarayın (destekleniyorsa) veya ön tanımlı URL’leri kullanmak için kamera markasını manuel olarak seçin. Özel bir RTSP URL’si girmek için manuel yöntemi seçin ve “Diğer”i işaretleyin.", + "onvifPortDescription": "ONVIF'i destekleyen kameralarda bu genellikle 80 veya 8080'dir.", + "useDigestAuth": "Digest kimlik doğrulamasını kullan", + "errors": { + "nameRequired": "Kamera ismi gereklidir", + "nameLength": "Kamera ismi en fazla 64 karakter olmalıdır", + "invalidCharacters": "Kamera ismi geçersiz karakterler içeriyor", + "nameExists": "Kamera ismi zaten mevcut", + "customUrlRtspRequired": "Özel URL'ler \"rtsp://\" ile başlamalıdır. RTSP olmayan kamera akışları için manuel yapılandırma gereklidir.", + "brandOrCustomUrlRequired": "Bir kamera markası seçip host/IP adresi girin ya da özel bir URL kullanmak için ‘Diğer’ seçeneğini tercih edin" + }, + "useDigestAuthDescription": "ONVIF için HTTP digest kimlik doğrulamasını kullanın. Bazı kameralar, standart yönetici kullanıcısı yerine özel bir ONVIF kullanıcı adı/parola kullanılmasını gerektirebilir." + }, + "step2": { + "description": "Mevcut akışları bulmak için kamerayı tarayın veya seçtiğiniz algılama yöntemine göre manuel ayarları yapılandırın.", + "testSuccess": "Bağlantı testi başarılı!", + "testFailed": "Bağlantı testi başarısız oldu. Lütfen bilgileri kontrol edip tekrar deneyin.", + "testFailedTitle": "Test Başarısız", + "streamDetails": "Akış Ayrıntıları", + "probing": "Kamera taranıyor...", + "retry": "Yeniden dene", + "testing": { + "probingMetadata": "Kamera meta verileri inceleniyor...", + "fetchingSnapshot": "Kamera anlık görüntüsü alınıyor..." + }, + "probeFailed": "Kamerayı tarama başarısız oldu: {{error}}", + "probingDevice": "Cihaz taranıyor…", + "probeSuccessful": "Tarama başarılı", + "probeError": "Tarama hatası", + "probeNoSuccess": "Tarama başarısız", + "deviceInfo": "Cihaz Bilgileri", + "manufacturer": "Üretici", + "model": "Modeli", + "firmware": "Donanım yazılımı", + "profiles": "Profiller", + "ptzSupport": "PTZ Desteği", + "autotrackingSupport": "Otomatik Takip Desteği", + "presets": "Ön ayarlar", + "rtspCandidates": "RTSP Yayınları", + "rtspCandidatesDescription": "Kamera taramasından aşağıdaki RTSP URL'leri bulundu. Akış meta verilerini görüntülemek için bağlantıyı test edin.", + "noRtspCandidates": "Kameradan RTSP URL'si bulunamadı. Kimlik bilgileriniz yanlış olabilir veya kamera ONVIF'i veya RTSP URL'lerini almak için kullanılan yöntemi desteklemiyor olabilir. Geri dönün ve RTSP URL'sini manuel olarak girin.", + "candidateStreamTitle": "Yayın {{number}}", + "useCandidate": "Kullan", + "uriCopy": "Kopyala", + "uriCopied": "URI panoya kopyalandı", + "testConnection": "Bağlantıyı Test Et", + "toggleUriView": "Tam URI görünümünü değiştirmek için tıklayın", + "connected": "Bağlandı", + "notConnected": "Bağlı Değil", + "errors": { + "hostRequired": "Host/IP adresi gereklidir" + } + }, + "step3": { + "description": "Akış rollerini yapılandırın ve kameranız için ek akışlar ekleyin.", + "streamsTitle": "Kamera Akışları", + "addStream": "Akış Ekle", + "addAnotherStream": "Başka Bir Akış Ekle", + "streamTitle": "Akış {{number}}", + "streamUrl": "Akış URL'si", + "streamUrlPlaceholder": "rtsp://kullanıcıadı:parola@host:port/path", + "selectStream": "Bir akış seçin", + "searchCandidates": "Yayınları arayın...", + "noStreamFound": "Akış bulunamadı", + "url": "URL", + "resolution": "Çözünürlük", + "selectResolution": "Çözünürlüğü seçin", + "quality": "Kalite", + "selectQuality": "Kaliteyi seçin", + "roles": "Roller", + "roleLabels": { + "detect": "Nesne Algılama", + "record": "Kayıt", + "audio": "Ses" + }, + "testStream": "Bağlantıyı Test Et", + "testSuccess": "Yayın testi başarılı!", + "testFailed": "Yayın testi başarısız oldu", + "testFailedTitle": "Test Başarısız", + "connected": "Bağlı", + "notConnected": "Bağlı Değil", + "featuresTitle": "Özellikler", + "go2rtc": "Kameraya olan bağlantıları azaltın", + "detectRoleWarning": "Devam edebilmek için en az bir akışın algılama (detect) rolüne sahip olması gerekir.", + "rolesPopover": { + "title": "Akış Rolleri", + "detect": "Nesne algılama için ana besleme.", + "record": "Yapılandırma ayarlarına göre video akışının bölümlerini kaydeder.", + "audio": "Ses tabanlı algılama için besleme." + }, + "featuresPopover": { + "title": "Yayın Özellikleri", + "description": "Kameranıza olan bağlantıları azaltmak için go2rtc yeniden akışını kullanın." + } + }, + "step4": { + "disconnectStream": "Bağlantıyı kes", + "estimatedBandwidth": "Tahmini Bant Genişliği", + "roles": "Roller", + "ffmpegModule": "Yayın uyumluluk modunu kullan", + "ffmpegModuleDescription": "Yayın birkaç denemeden sonra yüklenmezse, bunu etkinleştirmeyi deneyin. Etkinleştirildiğinde, Frigate go2rtc ile ffmpeg modülünü kullanacaktır. Bu, bazı kamera yayınları ile daha iyi uyumluluk sağlayabilir.", + "none": "Hiçbiri", + "error": "Hata", + "description": "Yeni kameranızı kaydetmeden önce son doğrulama ve analiz. Kaydetmeden önce her akışı bağlayın.", + "validationTitle": "Akış Doğrulaması", + "connectAllStreams": "Tüm Akışlara Bağlan", + "reconnectionSuccess": "Yeniden bağlantı başarılı.", + "reconnectionPartial": "Bazı Akışlara yeniden bağlanılamadı.", + "streamUnavailable": "Akış önizlemesi kullanılamıyor", + "reload": "Yeniden yükle", + "connecting": "Bağlanıyor...", + "streamTitle": "Akış {{number}}", + "valid": "Geçerli", + "failed": "Başarısız", + "notTested": "Test edilmedi", + "connectStream": "Bağlan", + "connectingStream": "Bağlanıyor", + "streamValidated": "{{number}} nolu akış başarıyla doğrulandı", + "streamValidationFailed": "{{number}} nolu akış doğrulanamadı", + "saveAndApply": "Yeni Kamerayı Kaydet", + "saveError": "Geçersiz yapılandırma. Lütfen ayarlarınızı kontrol edin.", + "issues": { + "title": "Akış Doğrulaması", + "videoCodecGood": "Video kodeği {{codec}}.", + "audioCodecGood": "Ses kodeği {{codec}}.", + "resolutionHigh": "{{resolution}} çözünürlüğü kaynak kullanımının artmasına neden olabilir.", + "resolutionLow": "{{resolution}} çözünürlüğü, küçük nesnelerin güvenilir bir şekilde algılanması için çok düşük olabilir.", + "noAudioWarning": "Bu yayın için ses algılanmadı, kayıtlarda ses bulunmayacak.", + "audioCodecRecordError": "Kayıtlarda sesi desteklemek için AAC ses kodeği gereklidir.", + "audioCodecRequired": "Ses algılamayı desteklemek için bir ses akışı gereklidir.", + "restreamingWarning": "Kayıt akışı için kameraya olan bağlantıları azaltmak CPU kullanımını bir miktar artırabilir.", + "brands": { + "reolink-rtsp": "Reolink RTSP önerilmez. Kameranın ayarlarında HTTP'yi etkinleştirin ve sihirbazı baştan başlatın.", + "reolink-http": "Reolink HTTP akışları daha iyi uyumluluk için FFmpeg kullanmalıdır. Bu akış için 'Akış uyumluluk modunu kullan' seçeneğini etkinleştirin." + }, + "dahua": { + "substreamWarning": "Alt akış 1 düşük çözünürlüğe kilitlenmiştir. Birçok Dahua / Amcrest / EmpireTech kamera, kamera ayarlarında etkinleştirilmesi gereken ek alt akışları destekler. Mevcutsa, bu akışları kontrol edip kullanmanız önerilir." + }, + "hikvision": { + "substreamWarning": "Alt akış 1 düşük çözünürlüğe kilitlendi. Birçok Hikvision kamera, kamera ayarlarında etkinleştirilmesi gereken ek alt akışları destekler. Mevcutsa, bu akışları kontrol edip kullanmanız önerilir." + } + } + } + }, + "cameraManagement": { + "title": "Kameraları Yönet", + "addCamera": "Yeni Kamera Ekle", + "editCamera": "Kamerayı Düzenle:", + "selectCamera": "Bir Kamera Seçin", + "backToSettings": "Kamera Ayarlarına Dön", + "streams": { + "title": "Kameraları Etkinleştir / Devre Dışı Bırak", + "desc": "Frigate yeniden başlatılana kadar bir kamerayı geçici olarak devre dışı bırakın. Bir kamerayı devre dışı bırakmak, Frigate'in bu kameranın akışlarını işlemesini tamamen durdurur. Algılama, kayıt ve hata ayıklama kullanılamaz.
    Not: Bu, go2rtc yeniden akışlarını devre dışı bırakmaz." + }, + "cameraConfig": { + "add": "Kamera Ekle", + "edit": "Kamerayı Düzenle", + "description": "Yayınlar ve roller dahil olmak üzere kamera ayarlarını yapılandırın.", + "name": "Kamera İsmi", + "nameRequired": "Kamera ismi gereklidir", + "nameLength": "Kamera ismi 64 karakterden az olmalıdır.", + "namePlaceholder": "örneğin, ön_kapı veya Arka Bahçe Genel Bakışı", + "enabled": "Etkin", + "ffmpeg": { + "inputs": "Giriş Akışları", + "path": "Akış Yolu", + "pathRequired": "Akış yolu gereklidir", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "En az bir rol gereklidir", + "rolesUnique": "Her rol (ses, algılama, kayıt) yalnızca bir akışa atanabilir", + "addInput": "Akış Ekle", + "removeInput": "Akış Kaldır", + "inputsRequired": "En az bir akış gereklidir" + }, + "go2rtcStreams": "go2rtc Akışları", + "streamUrls": "Akış URL'leri", + "addUrl": "URL ekle", + "addGo2rtcStream": "go2rtc Akışı Ekle", + "toast": { + "success": "Kamera {{cameraName}} başarıyla kaydedildi" + } + } + }, + "cameraReview": { + "title": "Kamera İnceleme Ayarları", + "object_descriptions": { + "title": "Üretken Yapay Zeka Nesne Açıklamaları", + "desc": "Frigate yeniden başlatılana kadar bu kamera için Üretken Yapay Zeka nesne açıklamalarını geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, bu kameradaki izlenen nesneler için yapay zeka tarafından oluşturulan açıklamalar istenmeyecektir." + }, + "review_descriptions": { + "title": "Üretken Yapay Zeka İnceleme Açıklamaları", + "desc": "Bu kamera için yapay zekadan incele öğelerini açıklama taleplerini geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, bu kameradaki inceleme öğeleri için yapay zekadan açıklama istenmeyecektir." + }, + "review": { + "title": "İncele", + "desc": "Frigate yeniden başlatılana kadar bu kamera için uyarıları ve algılamaları geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, yeni inceleme öğeleri oluşturulmaz. ", + "alerts": "Uyarılar ", + "detections": "Tespitler " + }, + "reviewClassification": { + "title": "Sınıflandırmayı İncele", + "desc": "Frigate, inceleme öğelerini Uyarılar ve Algılamalar olarak kategorilere ayırır. Varsayılan olarak, tüm kişi ve araba nesneleri Uyarı olarak kabul edilir. İnceleme öğelerinizin kategorilendirmesini, bunlar için gerekli bölgeleri yapılandırarak iyileştirebilirsiniz.", + "noDefinedZones": "Bu kamera için herhangi bir bölge tanımlanmamıştır.", + "objectAlertsTips": "{{cameraName}} üzerindeki tüm {{alertsLabels}} nesneleri Uyarı olarak gösterilecektir.", + "zoneObjectAlertsTips": "{{cameraName}} üzerinde, {{zone}} bölgesinde tespit edilen tüm {{alertsLabels}} nesneleri Uyarı olarak gösterilecektir.", + "objectDetectionsTips": "{{cameraName}} üzerinde kategorize edilmemiş tüm {{detectionsLabels}} nesneleri, bölgeden bağımsız olarak Tespit olarak gösterilecektir.", + "zoneObjectDetectionsTips": { + "text": "{{cameraName}} üzerindeki {{zone}} bölgesinde kategorize edilmemiş tüm {{detectionsLabels}} nesneleri, Tespit olarak gösterilecektir.", + "notSelectDetections": "{{cameraName}} üzerinde {{zone}} bölgesinde tespit edilen ve Uyarı olarak kategorize edilmemiş tüm {{detectionsLabels}} nesneleri, bölgeden bağımsız olarak Tespitler olarak gösterilecektir.", + "regardlessOfZoneObjectDetectionsTips": "{{cameraName}} üzerinde kategorize edilmemiş tüm {{detectionsLabels}} nesneleri, bulundukları bölgeden bağımsız olarak Tespit olarak gösterilecektir." + }, + "unsavedChanges": "{{camera}} için Kaydedilmemiş İnceleme Sınıflandırması ayarları", + "selectAlertsZones": "Uyarılar için bölgeleri seçin", + "selectDetectionsZones": "Tespitler için bölgeleri seçin", + "limitDetections": "Tespitleri belirli bölgelerle sınırlayın", + "toast": { + "success": "Sınıflandırma yapılandırması kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın." + } + } + }, + "roles": { + "management": { + "title": "İzleyici Rol Yönetimi", + "desc": "Bu Frigate örneği için özel görüntüleyici rollerini ve kamera erişim izinlerini yönetin." + }, + "addRole": "Rol Ekle", + "table": { + "role": "Rol", + "cameras": "Kameralar", + "actions": "Eylemler", + "noRoles": "Özel rol bulunamadı.", + "editCameras": "Kameraları Düzenle", + "deleteRole": "Rolü Sil" + }, + "toast": { + "success": { + "createRole": "{{role}} rolü başarıyla oluşturuldu", + "updateCameras": "{{role}} rolü için kameralar güncellendi", + "deleteRole": "{{role}} rolü başarıyla silindi", + "userRolesUpdated_one": "Bu role atanan {{count}} kullanıcı, tüm kameralara erişimi olan 'görüntüleyici' olarak güncellendi.", + "userRolesUpdated_other": "Bu role atanan {{count}} kullanıcı, tüm kameralara erişimi olan 'görüntüleyici' olarak güncellendi." + }, + "error": { + "createRoleFailed": "Rol oluşturulamadı: {{errorMessage}}", + "updateCamerasFailed": "Kameralar güncellenemedi: {{errorMessage}}", + "deleteRoleFailed": "Rol silinemedi: {{errorMessage}}", + "userUpdateFailed": "Kullanıcı rolleri güncellenemedi: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Yeni Rol Oluştur", + "desc": "Yeni bir rol ekleyin ve kamera erişim izinlerini belirtin." + }, + "editCameras": { + "title": "Rol Kameralarını Düzenle", + "desc": "{{role}} rolü için kamera erişimini güncelleyin." + }, + "deleteRole": { + "title": "Rolü Sil", + "desc": "Bu işlem geri alınamaz. Bu işlem, rolü kalıcı olarak siler ve bu role sahip tüm kullanıcılara 'izleyici' rolü atar; bu da izleyiciye tüm kameralara erişim sağlar.", + "warn": "{{role}} rolünü silmek istediğinizden emin misiniz?", + "deleting": "Siliniyor..." + }, + "form": { + "role": { + "title": "Rol İsmi", + "placeholder": "Rol ismini girin", + "desc": "Sadece harf, rakam, nokta ve alt çizgi kullanılabilir.", + "roleIsRequired": "Rol ismi gereklidir", + "roleOnlyInclude": "Rol ismi yalnızca harf, sayı veya alt çizgi (_) içerebilir", + "roleExists": "Bu isimde bir rol zaten mevcut." + }, + "cameras": { + "title": "Kameralar", + "desc": "Bu rolün erişebileceği kameraları seçin. En az bir kamera gereklidir.", + "required": "En az bir kamera seçilmelidir." + } + } + } + } +} diff --git a/web/public/locales/tr/views/system.json b/web/public/locales/tr/views/system.json new file mode 100644 index 0000000..8ce1835 --- /dev/null +++ b/web/public/locales/tr/views/system.json @@ -0,0 +1,212 @@ +{ + "documentTitle": { + "logs": { + "frigate": "Frigate Günlükleri - Frigate", + "go2rtc": "Go2RTC Günlükleri - Frigate", + "nginx": "Nginx Günlükleri - Frigate" + }, + "general": "Genel İstatistikler - Frigate", + "storage": "Depolama İstatistikleri - Frigate", + "cameras": "Kamera İstatistikleri - Frigate", + "enrichments": "Zenginleştirme İstatistikleri - Frigate" + }, + "metrics": "Sistem metrikleri", + "general": { + "hardwareInfo": { + "gpuDecoder": "GPU Kod Çözücü", + "gpuInfo": { + "nvidiaSMIOutput": { + "name": "İsim: {{name}}", + "driver": "Sürücü: {{driver}}", + "cudaComputerCapability": "CUDA Hesaplama Yeteneği: {{cuda_compute}}", + "vbios": "VBios Bilgisi: {{vbios}}", + "title": "Nvidia SMI Çıktısı" + }, + "toast": { + "success": "GPU bilgisi panoya kopyalandı" + }, + "vainfoOutput": { + "processError": "İşlem Hatası:", + "returnCode": "Dönüt Kodu: {{code}}", + "processOutput": "İşlem Çıktısı:", + "title": "Vainfo çıktısı" + }, + "closeInfo": { + "label": "GPU bilgisini kapat" + }, + "copyInfo": { + "label": "GPU bilgisini kopyala" + } + }, + "gpuUsage": "GPU Kullanımı", + "gpuMemory": "GPU Belleği", + "gpuEncoder": "GPU Kodlayıcı", + "title": "Donanım Bilgisi", + "npuUsage": "NPU Kullanımı", + "npuMemory": "NPU Bellek Kullanımı", + "intelGpuWarning": { + "title": "Intel GPU İstatistik Uyarısı", + "message": "GPU istatistikleri kullanılamıyor", + "description": "Bu durum, donanımsal hızlandırma ve nesne tespiti (i)GPU üzerinde sorunsuz çalışıyor olsa bile, Intel’in GPU istatistik raporlama aracındaki (intel_gpu_top) bilinen bir hatadan ötürü GPU kullanımının %0 olarak bildirilmesinden kaynaklanmakta olup, Frigate hatası değildir. Sorunu geçici olarak düzeltmek ve (i)GPU’nun doğru çalıştığını doğrulamak için ana makineyi yeniden başlatabilirsiniz. Bu durum performansı etkilememektedir." + } + }, + "otherProcesses": { + "title": "Diğer İşlemler", + "processCpuUsage": "İşlem CPU Kullanımı", + "processMemoryUsage": "İşlem Bellek Kullanımı", + "series": { + "go2rtc": "go2rtc", + "recording": "kayıt", + "embeddings": "gömüler", + "audio_detector": "ses detektörü", + "review_segment": "inceleme bölümü" + } + }, + "detector": { + "title": "Algılayıcılar", + "inferenceSpeed": "Algılayıcı Çıkarım Hızı", + "memoryUsage": "Algılayıcı Bellek Kullanımı", + "cpuUsage": "Algılayıcı İşlemci Kullanımı", + "temperature": "Algılayıcı Sıcaklığı", + "cpuUsageInformation": "Tespit modellerine giriş ve çıkış verilerini hazırlarken kullanılan işlemci yoğunluğu. Bu değer, grafik işlemci veya benzeri bir hızlandırıcı kullanılsa bile çıkarım yükünü ölçmek için kullanılmamalıdır." + }, + "title": "Genel" + }, + "storage": { + "title": "Depolama", + "overview": "Genel", + "recordings": { + "title": "Kayıtlar", + "earliestRecording": "Mevcut en erken kayıt:", + "tips": "Burada gösterilen değer, Frigate’in veritabanına göre kayıtların diskinizde kullandığı toplam alanı ifade eder. Frigate, diskinizdeki tüm dosyaların alan kullanımını takip etmez." + }, + "cameraStorage": { + "title": "Kamera Depolaması", + "camera": "Kamera", + "unused": { + "tips": "Eğer diskinizde Frigate'in kayıtları dışında dosyalar varsa bu değer diskinizdeki boş alanı doğru olarak göstermeyebilir. Frigate kendi kayıtları dışındaki dosyaların disk kullanımını takip etmez.", + "title": "Kullanılmayan" + }, + "percentageOfTotalUsed": "Toplam Yüzde", + "storageUsed": "Depolama", + "bandwidth": "Saatlik Veri Kullanımı", + "unusedStorageInformation": "Kullanılmayan Depolama Bilgisi" + }, + "shm": { + "warning": "Şu anki {{total}}MB'lik SHM boyutu yetersiz. Bu boyutu en az {{min_shm}}MB'a çıkartın.", + "title": "Ayrılan SHM (paylaşımlı bellek)" + } + }, + "cameras": { + "info": { + "streamDataFromFFPROBE": "Yayın bilgisi ffprobe ile edinilmiştir.", + "video": "Video:", + "codec": "Kodlama:", + "fps": "Kare Hızı:", + "resolution": "Çözünürlük:", + "unknown": "Bilinmeyen", + "stream": "Yayın {{idx}}", + "tips": { + "title": "Kamera Detayları" + }, + "fetching": "Kamera Bilgileri Alınıyor", + "cameraProbeInfo": "{{camera}} Kamera Detayları", + "error": "Hata: {{error}}", + "audio": "Ses:", + "aspectRatio": "en boy oranı" + }, + "framesAndDetections": "Kare / Tespit", + "label": { + "camera": "kamera", + "detect": "tespit", + "ffmpeg": "FFmpeg", + "capture": "kayıt", + "skipped": "atlanan", + "overallDetectionsPerSecond": "toplam tespit/sn", + "overallFramesPerSecond": "toplam kare/sn", + "cameraFramesPerSecond": "{{camName}} kare/sn", + "overallSkippedDetectionsPerSecond": "toplam atlanan tespit/sn", + "cameraCapture": "{{camName}} kayıt", + "cameraDetect": "{{camName}} tespit", + "cameraDetectionsPerSecond": "{{camName}} tespit/sn", + "cameraSkippedDetectionsPerSecond": "{{camName}} atlanan tespit/sn", + "cameraFfmpeg": "{{camName}} FFmpeg" + }, + "toast": { + "success": { + "copyToClipboard": "Detaylar panoya kopyalandı." + }, + "error": { + "unableToProbeCamera": "Kamera detayları alınamadı: {{errorMessage}}" + } + }, + "title": "Kameralar", + "overview": "Genel", + "connectionQuality": { + "expectedFps": "Beklenen FPS", + "fps": "FPS" + } + }, + "lastRefreshed": "Son güncelleme: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} FFmpeg'te yüksek miktarda CPU kullanıyor (%{{ffmpegAvg}})", + "reindexingEmbeddings": "Gömüler yeniden dizinleniyor (%{{processed}} tamamlandı)", + "detectHighCpuUsage": "{{camera}} tespitte yüksek miktarda CPU kullanıyor (%{{detectAvg}})", + "healthy": "Sistem sağlıklı", + "detectIsVerySlow": "{{detect}} çok yavaş çalışıyor ({{speed}} ms)", + "cameraIsOffline": "{{camera}} çevrimdışı", + "detectIsSlow": "{{detect}} yavaş çalışıyor ({{speed}} ms)", + "shmTooLow": "Ayrılan /dev/shm belleği (şu anda {{total}} MB), en az {{min}} MB'a çıkartılmalıdır." + }, + "enrichments": { + "embeddings": { + "image_embedding_speed": "Resim Gömü Hızı", + "text_embedding_speed": "Metin Gömü Hızı", + "plate_recognition_speed": "Plaka Tanıma Hızı", + "face_embedding_speed": "Yüz Gömü Hızı", + "image_embedding": "Resim Gömüleme", + "text_embedding": "Metin Gömülüeme", + "face_recognition": "Yüz Tanıma", + "plate_recognition": "Plaka Tanıma", + "face_recognition_speed": "Yüz Tanıma Hızı", + "yolov9_plate_detection_speed": "YOLOv9 Plaka Tanıma Hızı", + "yolov9_plate_detection": "YOLOv9 Plaka Tanıma", + "review_description": "İnceleme Açıklaması", + "review_description_speed": "İnceleme Açıklama Hızı", + "review_description_events_per_second": "İnceleme Açıklaması", + "object_description": "Nesne Açıklaması", + "object_description_speed": "Nesne Açıklama Hızı", + "object_description_events_per_second": "Nesne Açıklaması", + "classification": "{{name}} Sınıflandırması", + "classification_speed": "{{name}} Sınıflandırma Hızı", + "classification_events_per_second": "{{name}} Saniyede Sınıflandırma Olayları" + }, + "infPerSecond": "Saniye Başına Çıkarım", + "title": "Zenginleştirmeler", + "averageInf": "Ortalama Çıkarım Süresi" + }, + "logs": { + "download": { + "label": "Günlükleri İndir" + }, + "type": { + "message": "Mesaj", + "tag": "Etiket", + "timestamp": "Zaman Damgası", + "label": "Tür" + }, + "copy": { + "error": "Günlükler panoya kopyalanamadı", + "label": "Panoya Kopyala", + "success": "Günlükler panoya kopyalandı" + }, + "tips": "Günlükler sunucudan yansıtılıyor", + "toast": { + "error": { + "whileStreamingLogs": "Günlükler yansıtılırken hata: {{errorMessage}}", + "fetchingLogsFailed": "Günlükler alınırken hata: {{errorMessage}}" + } + } + }, + "title": "Sistem" +} diff --git a/web/public/locales/uk/audio.json b/web/public/locales/uk/audio.json new file mode 100644 index 0000000..4aaf054 --- /dev/null +++ b/web/public/locales/uk/audio.json @@ -0,0 +1,503 @@ +{ + "child_singing": "Дитячий спів", + "breathing": "Дихання", + "cough": "Кашель", + "throat_clearing": "Прозорий очищення", + "mantra": "Мантра", + "synthetic_singing": "Синтетичний спів", + "whimper_dog": "Собаче скиглення", + "cat": "Кіт", + "cowbell": "Коров'ячий здвіночок", + "whispering": "Шепіт", + "run": "Біг", + "choir": "Хор", + "chewing": "Жування", + "pets": "Домашні улюбленці", + "dog": "Собака", + "bark": "Лай", + "meow": "Котяче нявчання", + "horse": "Кінь", + "moo": "Мичання", + "goat": "Коза", + "sheep": "Вівця", + "chicken": "Курка", + "speech": "Розмова", + "idling": "Холостий хід", + "railroad_car": "Залізничний вагон", + "alarm": "Сигналізація", + "fire_alarm": "Пожежна сигналізація", + "flute": "Флейта", + "musical_instrument": "Музичний інструмент", + "buzz": "Дзижчання", + "fly": "Муха", + "vocal_music": "Вокальна музика", + "motorcycle": "Мотоцикл", + "rustling_leaves": "Шелест листя", + "crackle": "Потріскування", + "thunder": "Грім", + "rock_and_roll": "Рок-н-рол", + "theme_music": "Тематична музика", + "exciting_music": "Енергійна музика", + "water": "Вода", + "violin": "Скрипка", + "tubular_bells": "Трубчасті дзвони", + "christmas_music": "Різдвяна музика", + "house_music": "Хауз", + "fire": "Вогонь", + "tapping": "Постукування", + "scratching": "Скретчінг", + "drum_kit": "Ударна установка", + "engine": "Двигун", + "light_engine": "Легкий двигун", + "swing_music": "Свінг", + "opera": "Опера", + "electronic_dance_music": "Електронна танцювальна музика", + "dance_music": "Танцювальна музика", + "thunderstorm": "Гроза", + "waves": "Хвилі", + "trombone": "Тромбон", + "music_of_asia": "Азіатська музика", + "tools": "Iнструменти", + "wind_chime": "Музика вітру", + "singing_bowl": "Співоча чаша", + "boat": "Човен", + "sailboat": "Вітрильник", + "rowboat": "Весловий човен", + "power_windows": "Електросклопідйомники", + "cutlery": "Столові прибори", + "mechanical_fan": "Механічний вентилятор", + "traffic_noise": "Дорожній шум", + "aircraft_engine": "Двигун повітряного судна", + "dental_drill's_drill": "Стоматологічна бормашина", + "door": "Двері", + "accelerating": "Прискорення", + "siren": "Сирена", + "typewriter": "Друкарська машинка", + "computer_keyboard": "Комп'ютерна клавіатура", + "smoke_detector": "Датчик диму", + "hammer": "Молот", + "gunshot": "Постріл", + "machine_gun": "Автомат", + "fireworks": "Феєрверки", + "firecracker": "Петарда", + "heartbeat": "Серцебиття", + "heart_murmur": "Серцевий шум", + "footsteps": "Кроки", + "burping": "Відрижка", + "hiccup": "Ікання", + "fart": "Пукання", + "finger_snapping": "Клацати пальцями", + "applause": "Оплески", + "chatter": "Балаканина", + "animal": "Тварина", + "yip": "Гавкання", + "babbling": "Балаканина", + "yell": "Крик", + "bow_wow": "Гав гав", + "growling": "Ревіння", + "purr": "Муркотіти", + "hiss": "Шипіння", + "clip_clop": "Цокання", + "neigh": "іржання", + "oink": "Рохкання", + "bleat": "Мекання", + "cluck": "Кудкудакання", + "cock_a_doodle_doo": "Кукурікання", + "honk": "Гелготання", + "roar": "Гуркіт", + "bird": "Птах", + "chirp": "Цвірінькання", + "pigeon": "Голуб", + "coo": "Воркування", + "crow": "Ворона", + "caw": "Каркання", + "owl": "Сова", + "hoot": "Ухання", + "flapping_wings": "Ляскання крил", + "dogs": "Собаки", + "rats": "Щури", + "mouse": "Миш", + "patter": "Шерех", + "insect": "Комара", + "cricket": "Цвіркун", + "mosquito": "Комар", + "frog": "Жаба", + "croak": "Квакання", + "snake": "Змія", + "rattle": "Тріск", + "whale_vocalization": "Співання кита", + "music": "Музика", + "guitar": "Гітара", + "electric_guitar": "Електрогітара", + "bass_guitar": "Бас-гітара", + "acoustic_guitar": "Акустична гітара", + "strum": "Звук струн", + "keyboard": "Клавіатура", + "piano": "Піаніно", + "electric_piano": "Електропіаніно", + "organ": "Орган", + "electronic_organ": "Електроорган", + "synthesizer": "Синтезатор", + "percussion": "Ударні інструменти", + "drum": "Барабан", + "snare_drum": "Малий барабан", + "drum_roll": "Барабанний дріб", + "bass_drum": "Бас-барабан", + "tambourine": "Бубон", + "gong": "Гонг", + "glockenspiel": "Дзвіночки", + "orchestra": "Оркестр", + "double_bass": "Контрабас", + "wind_instrument": "Духовий інструмент", + "saxophone": "Саксофон", + "clarinet": "Кларнет", + "harp": "Арфа", + "bell": "Дзвін", + "church_bell": "Церковний дзвін", + "jingle_bell": "Бубонець", + "bicycle_bell": "Велосипедний дзвінок", + "tuning_fork": "Камертон", + "chime": "Дзвіночок", + "harmonica": "Губна гармоніка", + "accordion": "Акордеон", + "bagpipes": "Волинка", + "theremin": "Терменвокс", + "pop_music": "Поп-музика", + "hip_hop_music": "Хіп-хоп музика", + "beatboxing": "Бітбоксинг", + "rock_music": "Рок-музика", + "punk_rock": "Панк-рок", + "psychedelic_rock": "Психоделічний рок", + "rhythm_and_blues": "Ритм-н-блюз", + "country": "Кантрі", + "funk": "Фанк", + "folk_music": "Фолк-музика", + "jazz": "Джаз", + "disco": "Диско", + "classical_music": "Класична музика", + "electronic_music": "Електронна музика", + "techno": "Техно", + "dubstep": "Дабстеп", + "drum_and_bass": "Драм-н-бейс", + "electronica": "Електроніка", + "ambient_music": "Ембієнт", + "trance_music": "Транс музика", + "music_of_latin_america": "Латиноамериканська музика", + "flamenco": "Фламенко", + "blues": "Блюз", + "music_for_children": "Дитяча музика", + "a_capella": "А капела", + "music_of_africa": "Африканська музика", + "afrobeat": "Афробіт", + "christian_music": "Християнська музика", + "gospel_music": "Госпел", + "carnatic_music": "Карнатична музика", + "ska": "Ска-музика", + "traditional_music": "Традиційна музика", + "independent_music": "Iнді музика", + "song": "Пісня", + "background_music": "Фонова музика", + "jingle": "Джингл", + "soundtrack_music": "Саундтрек", + "lullaby": "Колискова", + "video_game_music": "Музика з відеоігор", + "wedding_music": "Весільна музика", + "happy_music": "Весела музика", + "sad_music": "Сумна музика", + "tender_music": "Ніжна музика", + "angry_music": "Сердита музика", + "scary_music": "Моторошна музика", + "wind": "Вітер", + "wind_noise": "Шум вітру", + "rain": "Дощ", + "raindrop": "Краплі дощу", + "rain_on_surface": "Дощ на поверхні", + "stream": "Потік", + "waterfall": "Водоспад", + "ocean": "Океан", + "steam": "Пар", + "gurgling": "Дзюрчання", + "vehicle": "Транспорт", + "motorboat": "Моторний човен", + "ship": "Корабель", + "motor_vehicle": "Моторний транспорт", + "car": "Автомобіль", + "toot": "Гудок", + "car_alarm": "Автосигналізація", + "skidding": "Занос", + "tire_squeal": "Вереск шін", + "car_passing_by": "Проїжджаюча машина", + "race_car": "Гоночний автомобіль", + "truck": "Вантажівка", + "air_brake": "Пневматичне гальмо", + "air_horn": "Пневматичні гудок", + "reversing_beeps": "Сигнал заднього ходу", + "ice_cream_truck": "Вантажівки з морозивом", + "bus": "Автобус", + "emergency_vehicle": "Транспорт екстрених служб", + "police_car": "Поліцейський автомобіль", + "ambulance": "Швидка допомога", + "fire_engine": "Пожежна машина", + "rail_transport": "Рейковий транспорт", + "train": "Поїзд", + "train_whistle": "Свист поїзда", + "train_horn": "Гудок поїзда", + "train_wheels_squealing": "Вереск колес поїзда", + "subway": "Метро", + "aircraft": "Повітряне судно", + "jet_engine": "Реактивний двигун", + "propeller": "Пропелер", + "helicopter": "Вертоліт", + "fixed-wing_aircraft": "Літак з нерухомим крилом", + "bicycle": "Велосипед", + "skateboard": "Скейтборд", + "lawn_mower": "Газонокосарка", + "chainsaw": "Ланцюгова пила", + "medium_engine": "Середні двигун", + "heavy_engine": "Важкий двигун", + "engine_knocking": "Детонація у двигуні", + "engine_starting": "Запуск двигуна", + "doorbell": "Дверний дзвінок", + "ding-dong": "Дін-дон", + "sliding_door": "Розсувні двері", + "slam": "Бавовна", + "knock": "Стукіт", + "tap": "Невеличкий стук", + "squeak": "Скрип", + "cupboard_open_or_close": "Відкриття або закриття шафи", + "drawer_open_or_close": "Відкриття або закриття коробки", + "dishes": "Тарілки", + "chopping": "Нарізування", + "frying": "Смаження", + "microwave_oven": "Мікрохвильова піч", + "blender": "Блендер", + "water_tap": "Водопровідний кран", + "sink": "Раковина", + "bathtub": "Ванна", + "hair_dryer": "Фен", + "toilet_flush": "Злив унітазу", + "toothbrush": "Зубна щітка", + "electric_toothbrush": "Електрична зубна щітка", + "vacuum_cleaner": "Пилосос", + "zipper": "Блискавки на одязі", + "keys_jangling": "Брязкання ключів", + "coin": "Монета", + "scissors": "Ножиці", + "electric_shaver": "Електробритва", + "shuffling_cards": "Тасуванні карт", + "typing": "Друкування", + "writing": "Написання", + "telephone": "Телефон", + "telephone_bell_ringing": "Телефонний дзвінок", + "ringtone": "Рінгтон", + "telephone_dialing": "Набір телефонного номеру", + "dial_tone": "Телефонний гудок", + "busy_signal": "Сигнал зайнято", + "alarm_clock": "Будильник", + "civil_defense_siren": "Сирени громадянської оборони", + "buzzer": "Зумер", + "foghorn": "Туманний горн", + "whistle": "Свисток", + "steam_whistle": "Парової свисток", + "mechanisms": "Механізми", + "ratchet": "Тріскачка", + "clock": "Годинник", + "tick": "Тік", + "tick-tock": "Тік-так", + "gears": "Шестерні", + "pulleys": "Шківи", + "sewing_machine": "Швейна машинка", + "air_conditioning": "Кондиціонер", + "cash_register": "Каса", + "printer": "Принтер", + "camera": "Камера", + "single-lens_reflex_camera": "Дзеркальна камера", + "jackhammer": "Відбійний молоток", + "sawing": "Розпилювання", + "filing": "Звучання напилку", + "sanding": "Шліфування", + "power_tool": "Електроінструмент", + "drill": "Дриль", + "explosion": "Вибух", + "fusillade": "Збройна черга", + "artillery_fire": "Артилерійський вогонь", + "cap_gun": "Iграшковий пістолет", + "burst": "Черга пострілів", + "eruption": "Виверження", + "boom": "Бум", + "wood": "Деревина", + "chop": "Рубання", + "splinter": "Тріска", + "crack": "Тріщина", + "glass": "Скло", + "chink": "Дзенькіт", + "shatter": "Розбиття", + "silence": "Тиша", + "sound_effect": "Звуковий ефект", + "environmental_noise": "Шум навколишнього середовища", + "static": "Статичний шум", + "white_noise": "Білий шум", + "pink_noise": "Рожевий шум", + "television": "Телебачення", + "radio": "Радіо", + "field_recording": "Польова запись", + "scream": "Крик", + "laughter": "Сміх", + "bellow": "Рев", + "singing": "Спів", + "whoop": "Вигук", + "snicker": "хіхікання", + "crying": "Плач", + "sigh": "Зітхання", + "yodeling": "Співати йодлем", + "chant": "Скандування", + "grunt": "Гарчання", + "wheeze": "Хрипіти", + "gasp": "Aхнути", + "snort": "Пирхання", + "sniff": "Понюхати", + "shuffle": "Перетасувати", + "biting": "Кусання", + "gargling": "Полоскання", + "stomach_rumble": "Шлунок бурчати", + "hands": "Руки", + "clapping": "Плескання", + "cheering": "Аплодувати", + "crowd": "Натовп", + "children_playing": "Діти граються", + "howl": "Виття", + "rapping": "Стукiт", + "humming": "Гудіння", + "caterwaul": "Нявкання", + "livestock": "Тваринництво", + "cattle": "Велика рогата худоба", + "pig": "Свиня", + "fowl": "Птиця", + "turkey": "Iндичка", + "gobble": "Гелґотіння", + "duck": "Качка", + "quack": "Крякання", + "goose": "Гусак", + "wild_animals": "Дикі тварини", + "roaring_cats": "Ревіння котів", + "squawk": "Пташиний крик", + "plucked_string_instrument": "Щипковий струнний інструмент", + "steel_guitar": "Слайд-гітара", + "banjo": "Банджо", + "sitar": "Ситара", + "mandolin": "Мандоліна", + "zither": "Цитра", + "ukulele": "Укулеле", + "hammond_organ": "Орган Хаммонда", + "sampler": "Семплер", + "harpsichord": "Клавесин", + "drum_machine": "Драм-машина", + "rimshot": "Удар по ободу", + "timpani": "Тимпані", + "tabla": "Табла", + "cymbal": "Тарілка", + "hi_hat": "Хай-хет", + "wood_block": "Дерев'яний брусок", + "maraca": "Маракас", + "mallet_percussion": "Малет-перкусія", + "marimba": "Маримба", + "vibraphone": "Вібрафон", + "steelpan": "Стілпен", + "brass_instrument": "Мідний духовий інструмент", + "french_horn": "Валторна", + "trumpet": "Труба", + "bowed_string_instrument": "Струнно-смичкови інструмент", + "string_section": "Струнна секція", + "pizzicato": "Піцикато", + "cello": "Віолончель", + "didgeridoo": "Діджеріду", + "heavy_metal": "Хеві-метал", + "grunge": "Гранж", + "progressive_rock": "Прогресивний рок", + "soul_music": "Соул", + "reggae": "Реггі", + "bluegrass": "Блюграс", + "middle_eastern_music": "Близькосхідна музика", + "salsa_music": "Музика сальси", + "new-age_music": "Музика нью-ейдж", + "music_of_bollywood": "Музика Боллівуду", + "groan": "стогнати", + "whistling": "Свист", + "snoring": "Хропіння", + "pant": "Задихатися", + "sneeze": "Чхати", + "sodeling": "Соделінг", + "chird": "Дитина", + "change_ringing": "Змінити дзвінок", + "shofar": "Шофар", + "liquid": "Рідина", + "splash": "Сплеск", + "slosh": "Сльоз", + "squish": "Хлюпати", + "drip": "Крапельне", + "pour": "Для", + "trickle": "Струмінь", + "gush": "Гуш", + "fill": "Заповнити", + "spray": "Спрей", + "pump": "Насос", + "stir": "Перемішати", + "boiling": "Кипіння", + "sonar": "Сонар", + "arrow": "Стрілка", + "whoosh": "Свисти", + "thump": "Тупіт", + "thunk": "Тюнк", + "electronic_tuner": "Електронний тюнер", + "effects_unit": "Блок ефектів", + "chorus_effect": "Ефект хорусу", + "basketball_bounce": "Відскок баскетбольного м'яча", + "bang": "Вибухи", + "slap": "Ляпас", + "whack": "Вдарити", + "smash": "Розгром", + "breaking": "Розбиттям", + "bouncing": "Підстрибування", + "whip": "Батіг", + "flap": "Клаптик", + "scratch": "Подряпина", + "scrape": "Скрейп", + "rub": "Розтирання", + "roll": "Рулон", + "crushing": "Дроблення", + "crumpling": "Зминання", + "tearing": "Розривання", + "beep": "Звуковий сигнал", + "ping": "Пінг", + "ding": "Дін", + "clang": "Брязкіт", + "squeal": "Вереск", + "creak": "Скрипи", + "rustle": "Шелест", + "whir": "Гудінням", + "clatter": "Брязкіти", + "sizzle": "Шипінням", + "clicking": "Клацання", + "clickety_clack": "Клацання-Клак", + "rumble": "Гуркіті", + "plop": "Плюх", + "hum": "Гум", + "zing": "Зінг", + "boing": "Боїнг", + "crunch": "Хрускіт", + "sine_wave": "Синусоїда", + "harmonic": "Гармоніка", + "chirp_tone": "Чирп-тон", + "pulse": "Пульс", + "inside": "Всередині", + "outside": "Зовні", + "reverberation": "Реверберація", + "echo": "Відлуння", + "noise": "Шум", + "mains_hum": "Гуміння рук", + "distortion": "Спотворення", + "sidetone": "Побічний тон", + "cacophony": "Какофонія", + "throbbing": "Пульсуючий", + "vibration": "Вібрація" +} diff --git a/web/public/locales/uk/common.json b/web/public/locales/uk/common.json new file mode 100644 index 0000000..21002ee --- /dev/null +++ b/web/public/locales/uk/common.json @@ -0,0 +1,332 @@ +{ + "time": { + "year_one": "{{time}}рік", + "year_few": "{{time}}роки", + "year_many": "{{time}}років", + "month_one": "{{time}}місяць", + "month_few": "{{time}}місяця", + "month_many": "{{time}} місяців", + "justNow": "Зараз", + "today": "Сьогодні", + "last7": "Останні 7 днів", + "last14": "Останній 14 днів", + "last30": "Останній 30 днів", + "thisWeek": "Цей тиждень", + "lastWeek": "Останній тиждень", + "thisMonth": "У цьому місяці", + "lastMonth": "Останній місяць", + "5minutes": "5 хвилин", + "10minutes": "10 хвилин", + "30minutes": "30 хвилин", + "1hour": "1 година", + "12hours": "12 годин", + "24hours": "24 години", + "pm": "вечора", + "am": "ранку", + "yr": "{{time}} рік", + "hour_one": "{{time}} година", + "hour_few": "{{time}} години", + "hour_many": "{{time}} годин", + "minute_one": "{{time}} хвилина", + "minute_few": "{{time}} хвилини", + "minute_many": "{{time}} хвилин", + "day_one": "{{time}} день", + "day_few": "{{time}} дні", + "day_many": "{{time}} днів", + "second_one": "{{time}}секунда", + "second_few": "{{time}}секунди", + "second_many": "{{time}}секунд", + "ago": "{{timeAgo}} тому", + "yesterday": "Учора", + "mo": "{{time}}місяць", + "d": "{{time}}д", + "h": "{{time}}г", + "m": "{{time}}хв", + "s": "{{time}}сек", + "untilForTime": "До {{time}}", + "untilForRestart": "Доки Frigate не перезавантажиться.", + "untilRestart": "До перезавантаження", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestamp2": { + "12hour": "dd.MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampMonthDayYear": { + "24hour": "MMM d, yyyy", + "12hour": "MMM d, yyyy" + }, + "inProgress": "У процесі", + "invalidStartTime": "Недійсний час початку", + "invalidEndTime": "Недійсний час завершення", + "never": "Ніколи" + }, + "button": { + "exitFullscreen": "Вийти з повноекранного режиму", + "on": "ВКЛ", + "yes": "Так", + "copy": "Копіювати", + "close": "Закрити", + "saving": "Збереження…", + "history": "Історія", + "cancel": "Відмінити", + "fullscreen": "Повноекранний режим", + "back": "Назад", + "pictureInPicture": "Картинка в картинці", + "cameraAudio": "Аудіо камери", + "copyCoordinates": "Копіювати координати", + "edit": "Редагувати", + "no": "Ні", + "twoWayTalk": "Двосторонній зв'язок", + "off": "ВИКЛ", + "delete": "Видалити", + "apply": "Застосувати", + "reset": "Скинути", + "done": "Готово", + "enabled": "Увімкнено", + "enable": "Увімкнути", + "disabled": "Вимкнено", + "disable": "Вимкнути", + "save": "Зберегти", + "download": "Завантажити", + "info": "Інфо", + "suspended": "Призупинено", + "play": "Грати", + "unselect": "Прибрати виділення", + "export": "Експортувати", + "deleteNow": "Видалити негайно", + "next": "Наступне", + "unsuspended": "Відновити дію", + "continue": "Продовжити", + "add": "Додати", + "applying": "Застосовую…", + "undo": "Скасувати", + "copiedToClipboard": "Скопійовано в буфер обміну", + "modified": "Змінено", + "overridden": "Перевизначено", + "resetToGlobal": "Скинути до Глобальних", + "resetToDefault": "Скинути до По замовчуванню", + "saveAll": "Зберігти все", + "savingAll": "Зберігаю все…", + "undoAll": "Відмінити все", + "retry": "Спробувати ще" + }, + "menu": { + "language": { + "da": "Датська", + "uk": "Українська", + "ro": "Румунська", + "es": "Іспанська", + "zhCN": "Спрощена китайська", + "hi": "Хінді", + "fr": "Французька", + "ar": "Арабська", + "pt": "Португальська", + "de": "Німецька", + "ja": "Японська", + "tr": "Турецька", + "ru": "російська", + "it": "Італійська", + "nl": "Голландська", + "sv": "Швецька", + "cs": "Чешська", + "nb": "Норвежский букмол", + "ko": "Корейська", + "vi": "В'єтнамська", + "fa": "Персидська", + "pl": "Польська", + "he": "Иврит", + "el": "Грецька", + "hu": "Венгерська", + "fi": "Фінська", + "sk": "Словацька", + "withSystem": { + "label": "Використовувати системну мову" + }, + "en": "Англійська", + "yue": "粵語 (Кантонська)", + "th": "ไทย (Тайська)", + "ca": "Català (Каталанська)", + "ptBR": "Português brasileiro (Бразильська португальська)", + "sr": "Српски (Сербська)", + "sl": "Slovenščina (Словенська)", + "lt": "Lietuvių (Литовська)", + "bg": "Български (Болгарська)", + "gl": "Galego (Галісійська)", + "id": "Bahasa Indonesia (Індонезійська)", + "ur": "اردو (Урду)", + "hr": "Hrvatski (Хорватська)", + "bs": "Bosanski (Боснійська)", + "zhHant": "繁體中文 (Традиційна китайська)" + }, + "system": "Система", + "systemMetrics": "Системна метріка", + "configuration": "Конфігурація", + "systemLogs": "Системні логи", + "settings": "Налаштування", + "configurationEditor": "Редактор конфігурації", + "languages": "Мови", + "theme": { + "nord": "Північ", + "red": "Червоний", + "contrast": "Висока контрастність", + "default": "Типовий", + "label": "Тема", + "blue": "Синій", + "green": "Зелений", + "highcontrast": "Висока контрастність" + }, + "help": "Допомогти", + "documentation": { + "title": "Документація", + "label": "Frigate документація" + }, + "restart": "Перезапустити Frigate", + "live": { + "title": "Пряма трансляція", + "allCameras": "Всi камери", + "cameras": { + "title": "Камери", + "count_one": "{{count}} Камера", + "count_few": "{{count}} Камери", + "count_many": "{{count}} Камер" + } + }, + "review": "Перегляд", + "explore": "Вивчити", + "export": "Експорт", + "uiPlayground": "UI iгровий майданчик", + "faceLibrary": "Бібліотека обличчя", + "user": { + "title": "Користувач", + "account": "Акаунт", + "current": "Поточний користувач: {{user}}", + "anonymous": "анонімний", + "logout": "Вихід", + "setPassword": "Встановити пароль" + }, + "darkMode": { + "label": "Темний режим", + "light": "Світло", + "dark": "Темний", + "withSystem": { + "label": "Використовуйте налаштування системи для світлого або темного режиму" + } + }, + "appearance": "Зовнішність", + "withSystem": "Система", + "classification": "Класифікація", + "profiles": "Профілі", + "actions": "Дії", + "features": "Особливості", + "chat": "Чат" + }, + "unit": { + "speed": { + "mph": "миль/г", + "kph": "км/г" + }, + "length": { + "feet": "ноги", + "meters": "метрів" + }, + "data": { + "kbps": "кБ/с", + "mbps": "МБ/с", + "gbps": "ГБ/с", + "kbph": "кБ/годину", + "mbph": "МБ/годину", + "gbph": "ГБ/годину" + } + }, + "label": { + "back": "Повернутись", + "hide": "Приховати {{item}}", + "show": "Показати {{item}}", + "ID": "ID", + "none": "Жоден", + "all": "Усі", + "other": "Інше" + }, + "toast": { + "save": { + "title": "Зберегти", + "error": { + "title": "Не вдалося зберегти зміни конфігурації: {{errorMessage}}", + "noMessage": "Не вдалося зберегти зміни налаштування" + }, + "success": "Зміни до налаштувань збережені вдало." + }, + "copyUrlToClipboard": "Скопійовано URL до буфера обміну." + }, + "role": { + "title": "Роль", + "desc": "Адміністратори мають повний доступ до всіх функцій інтерфейсу Fregate. Глядачі обмежуються переглядом камер, оглядовими елементами та історичними кадрами в інтерфейсі користувача.", + "admin": "Адміністратор", + "viewer": "Глядач" + }, + "pagination": { + "previous": { + "title": "Попередній", + "label": "Повернутись до попередньої сторінки" + }, + "next": { + "title": "Наступний", + "label": "Перехід до наступної сторінки" + }, + "more": "Більше сторінок", + "label": "Пагінація" + }, + "accessDenied": { + "documentTitle": "Доступ заборонений - Frigate", + "title": "Доступ заборонений", + "desc": "У вас немає дозволу на перегляд цієї сторінки." + }, + "notFound": { + "documentTitle": "Не знайдено - Frigate", + "desc": "Сторінка не знайдена", + "title": "404" + }, + "selectItem": "Вибрати {{item}}", + "readTheDocumentation": "Прочитати документацію", + "information": { + "pixels": "{{area}}пикс" + }, + "list": { + "two": "{{0}} і {{1}}", + "many": "{{items}}, і {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Необов'язково", + "internalID": "Внутрішній ідентифікатор, який Frigate використовує в конфігурації та базі даних" + }, + "no_items": "Нема елементів", + "validation_errors": "Помилки при перевірці", + "credentialField": { + "savedPlaceholder": "Збережено - залиште порожнім щоб зберегти поточні" + } +} diff --git a/web/public/locales/uk/components/auth.json b/web/public/locales/uk/components/auth.json new file mode 100644 index 0000000..4c9f7e2 --- /dev/null +++ b/web/public/locales/uk/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "errors": { + "webUnknownError": "Невідома помилка. Перевірте журнали консолi.", + "usernameRequired": "Ви повинні ввести ім'я користувача", + "passwordRequired": "Необхідно ввести пароль", + "rateLimit": "Перевищення кількості спроб. Спробуйте пізніше.", + "loginFailed": "Спроба входу зазнала невдачі", + "unknownError": "Невідома помилка. Перевірте журнали." + }, + "user": "Iм'я користувача", + "password": "Пароль", + "login": "Логiн", + "firstTimeLogin": "Намагаєтеся вперше увійти? Облікові дані надруковані в журналах Frigate." + } +} diff --git a/web/public/locales/uk/components/camera.json b/web/public/locales/uk/components/camera.json new file mode 100644 index 0000000..c8ccbca --- /dev/null +++ b/web/public/locales/uk/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "name": { + "placeholder": "Введіть назву…", + "errorMessage": { + "exists": "Назва групи камер вже існує.", + "mustLeastCharacters": "Назва групи камер має містити щонайменше 2 символи.", + "nameMustNotPeriod": "Назва групи камер не повинна містити крапку.", + "invalid": "Недійсна назва групи камер." + }, + "label": "Ім'я" + }, + "camera": { + "setting": { + "streamMethod": { + "method": { + "noStreaming": { + "label": "Без потокового передавання", + "desc": "Зображення з камер оновлюватимуться лише раз на хвилину, і пряма трансляція не відбуватиметься." + }, + "continuousStreaming": { + "desc": { + "warning": "Безперервна потокова передача може призвести до високого використання пропускної здатності та проблем із продуктивністю. Використовуйте обережно.", + "title": "Зображення з камери завжди транслюватиметься в реальному часі, коли воно відображається на панелі приладів, навіть якщо жодної активності не виявлено." + }, + "label": "Безперервна потокова передача" + }, + "smartStreaming": { + "label": "Розумне потокове передавання (рекомендовано)", + "desc": "Інтелектуальна потокова передача оновлює зображення з камери раз на хвилину за відсутності видимої активності, щоб заощадити смугу пропускання і ресурси. При виявленні активності зображення плавно перемикається на пряму трансляцію." + } + }, + "label": "Метод потокової передачі", + "placeholder": "Виберіть спосіб потокової передачі" + }, + "audioIsUnavailable": "Аудіо недоступне для цієї трансляції", + "title": "{{cameraName}} Налаштування потокової передачі", + "audio": { + "tips": { + "document": "Прочитайте документацію ", + "title": "Аудіо має виводитися з вашої камери та бути налаштованим у go2rtc для цього потоку." + } + }, + "label": "Налаштування потокової передачі з камери", + "desc": "Змініть параметри прямої трансляції для панелі керування цієї групи камер. Ці налаштування залежать від пристрою/браузера.", + "audioIsAvailable": "Для цього потоку доступне аудіо", + "compatibilityMode": { + "label": "Режим сумісності", + "desc": "Увімкніть цю опцію, лише якщо пряма трансляція вашої камери відображає кольорові артефакти та має діагональну лінію з правого боку зображення." + }, + "stream": "Потік", + "placeholder": "Виберіть потік" + }, + "birdseye": "Бердсай" + }, + "edit": "Редагувати групу камер", + "delete": { + "label": "Видалити групу камер", + "confirm": { + "title": "Підтвердити видалення", + "desc": "Ви впевнені, що хочете видалити групу камер? {{name}}?" + } + }, + "success": "Групу камер ({{name}}) збережено.", + "label": "Групи камер", + "add": "Додати групу камер", + "cameras": { + "label": "Камери", + "desc": "Виберіть камери для цієї групи." + }, + "icon": "Значок", + "showAll": "Відобразити всі групи камер", + "showLess": "Показати менше", + "editGroups": "Редагувати групи камер" + }, + "debug": { + "zones": "Зони", + "mask": "Маска", + "motion": "Рух", + "regions": "Регiони", + "options": { + "label": "Налаштування", + "title": "Опції", + "showOptions": "Показати параметри", + "hideOptions": "Приховати параметри" + }, + "boundingBox": "Обмежувальна рамка", + "timestamp": "Позначка часу", + "paths": "Шляхи" + } +} diff --git a/web/public/locales/uk/components/dialog.json b/web/public/locales/uk/components/dialog.json new file mode 100644 index 0000000..f1bab4a --- /dev/null +++ b/web/public/locales/uk/components/dialog.json @@ -0,0 +1,198 @@ +{ + "explore": { + "plus": { + "review": { + "question": { + "ask_a": "Чи є цей об'єкт {{label}}?", + "ask_full": "Чи є цей об'єкт {{untranslatedLabel}}{{translatedLabel}}?", + "label": "Підтвердіть цей ярлик для Frigate Plus", + "ask_an": "Це об'єкт – {{label}}?" + }, + "state": { + "submitted": "Поданi" + } + }, + "submitToPlus": { + "label": "Надіслати да Frigate+", + "desc": "Об'єкти в місцях, які ви хочете уникнути, не є помилковими спрацьовуваннями. Подання їх як помилкових спрацьовувань заплутає модель." + } + }, + "video": { + "viewInHistory": "Перегляд у історії" + } + }, + "streaming": { + "label": "Потік", + "restreaming": { + "desc": { + "readTheDocumentation": "Прочитати документацію", + "title": "Налаштуйте go2rtc для додаткових параметрів перегляду в реальному часі та аудіо для цієї камери." + }, + "disabled": "Перезавантаження не ввімкнено для цієї камери." + }, + "showStats": { + "label": "Показати статистику потоку", + "desc": "Позначте цей пункт, щоб показувати статистику потоку як накладання на канал камери." + }, + "debugView": "Режим зневаджування" + }, + "search": { + "saveSearch": { + "label": "Зберегти пошук", + "button": { + "save": { + "label": "Зберегти цей пошук" + } + }, + "desc": "Вкажіть назву для цього збереженого пошуку.", + "placeholder": "Введіть назву для пошуку", + "overwrite": "{{searchName}} вже існує. Збереження перезапише існуюче значення.", + "success": "Пошук ({{searchName}}) збережено." + } + }, + "export": { + "toast": { + "error": { + "failed": "Не вдалося додати до черги експорт: {{error}}", + "endTimeMustAfterStartTime": "Час закінчення повинен бути після часу початку", + "noVaildTimeSelected": "Не вибрано допустимий діапазон часу" + }, + "success": "Експорт успішно розпочато. Перегляньте файл на сторінці експорту.", + "view": "Переглянути", + "queued": "Експорт додано до черги. Дивіться прогрес на сторінці експортів.", + "batchSuccess_one": "Розпочинаю {{count}} експорт. Відкриваю справу зараз.", + "batchSuccess_few": "Розпочинаю {{count}} експортів. Відкриваю справу зараз.", + "batchSuccess_many": "Розпочинаю {{count}} експортів. Відкриваю справу зараз.", + "batchPartial": "Розпочато {{successful}} з {{total}} експортів. Невдалі камери: {{failedCameras}}", + "batchFailed": "Не зміг розпочати {{total}} експортів. Невдалі камери: {{failedCameras}}", + "batchQueuedSuccess_one": "Додав до черги {{count}} експорт. Відкриваю справу зараз.", + "batchQueuedSuccess_few": "Додав до черги {{count}} експортів. Відкриваю справу зараз.", + "batchQueuedSuccess_many": "Додав до черги {{count}} експортів. Відкриваю справу зараз.", + "batchQueuedPartial": "Додав до черги {{successful}} з {{total}} експортів. Невдалі камери: {{failedCameras}}", + "batchQueueFailed": "Не зміг додати до черги {{total}} експортів. Невдалі камери: {{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "Зберегти експорт", + "previewExport": "Попередній перегляд експорту", + "queueingExport": "Додаю до черги експорт...", + "useThisRange": "Використовуй цей діапазон" + }, + "time": { + "fromTimeline": "Вибір шкали часу", + "custom": "Користувацький", + "start": { + "title": "Час початку", + "label": "Виберіть час початку" + }, + "end": { + "title": "Час закінчення", + "label": "Вибрати час закінчення" + }, + "lastHour_one": "Остання {{count}} година", + "lastHour_few": "Останні {{count}} години", + "lastHour_many": "Останні {{count}} годин" + }, + "name": { + "placeholder": "Введіть назву для експорту" + }, + "select": "Вибрати", + "export": "Експорт", + "selectOrExport": "Выбiр або експорт", + "case": { + "newCaseOption": "Створити новий випадок", + "newCaseNamePlaceholder": "Ім'я для нового випадку", + "newCaseDescriptionPlaceholder": "Опис випадку", + "label": "Випадок", + "nonAdminHelp": "Новий випадок буде створено для цих експортів.", + "placeholder": "Оберіть випадок" + }, + "queueing": "Додаю експорт у чергу...", + "tabs": { + "export": "Одна камера", + "multiCamera": "Мульті-камера" + }, + "multiCamera": { + "timeRange": "Діапазон часу", + "selectFromTimeline": "Оберіть з шкали часу", + "cameraSelection": "Камери", + "cameraSelectionHelp": "Камери з відстежуваними об'єктами у цьому часовому діапазоні є попередньо обраними", + "checkingActivity": "Перевіряю активність камери...", + "noCameras": "Доступні камери відсутні", + "detectionCount_one": "{{count}} відстежувана камера", + "detectionCount_few": "{{count}} відстежувані камери", + "detectionCount_many": "{{count}} відстежуваних камер", + "nameLabel": "Ім'я для експорту", + "namePlaceholder": "Необов'язкове базове ім'я для цих експортів", + "queueingButton": "Додаю експорти у чергу...", + "exportButton_one": "Експорт {{count}} камери", + "exportButton_few": "Експорт {{count}} камер", + "exportButton_many": "Експорт {{count}} камер" + }, + "multi": { + "title_one": "Експорт {{count}} рецензії", + "title_few": "Експорт {{count}} рецензій", + "title_many": "Експорт {{count}} рецензій", + "description": "Експорт всіх обраних рецензій. Всі експорти будуть згруповані під спільною справою.", + "descriptionNoCase": "Експорт всіх обраних рецензій.", + "caseNamePlaceholder": "Переглянути експорт - {{date}}", + "exportButton_one": "Переглянути {{count}} рецензію", + "exportButton_few": "Переглянути {{count}} рецензії", + "exportButton_many": "Переглянути {{count}} рецензій", + "exportingButton": "Експортую...", + "toast": { + "started_one": "Розпочинаю {{count}} експорт. Відкриваю справу зараз.", + "started_few": "Розпочинаю {{count}} експортів. Відкриваю справу зараз.", + "started_many": "Розпочинаю {{count}} експортів. Відкриваю справу зараз.", + "startedNoCase_one": "Розпочинаю {{count}} експорт.", + "startedNoCase_few": "Розпочинаю {{count}} експортів.", + "startedNoCase_many": "Розпочинаю {{count}} експортів.", + "partial": "Розпочато {{successful}} з {{total}} експортів. Невдало: {{failedItems}}", + "failed": "Не зміг розпочати {{total}} експортів. Невдало: {{failedItems}}" + } + } + }, + "recording": { + "button": { + "export": "Експорт", + "markAsReviewed": "Позначити як переглянуте", + "deleteNow": "Вилучити зараз", + "markAsUnreviewed": "Позначити як непереглянуте" + }, + "confirmDelete": { + "title": "Підтвердити вилучення", + "desc": { + "selected": "Ви впевнені, що хочете видалити все записане відео, пов'язане з цим пунктом огляду?

    Утримуйте клавішу Shift, щоб обійти це діалогове вікно в майбутньому." + }, + "toast": { + "error": "Не вдалося видалити: {{error}}", + "success": "Відеозаписи, пов’язані з вибраними елементами огляду, успішно видалено." + } + }, + "shareTimestamp": { + "label": "Поділитися позначкою часу", + "title": "Поділитися позначкою часу", + "description": "Поділитися URL з позначкою часу поточної позиції плеєру або оберіть довільну позначку часу. Зверніть увагу, що це не публічна URL і вона доступна тільки для користувачів з доступом до Frigate та цієї камери.", + "custom": "Довільна позначка часу", + "button": "Поділитися URL з позначкою часу", + "shareTitle": "Позначка часу для Frigate рецензії: {{camera}}" + } + }, + "restart": { + "title": "Ви впевнені, що хочете перезапустити Frigate?", + "button": "Перезавантажувати", + "restarting": { + "title": "Frigate перезапускається", + "content": "Цю сторінку буде перезавантажено за {{countdown}} секунд.", + "button": "Примусово перезавантажити" + }, + "description": "Це призупинить роботу Frigate на час перезавантаження." + }, + "imagePicker": { + "selectImage": "Вибір мініатюри відстежуваного об'єкта", + "search": { + "placeholder": "Пошук за міткою або підміткою..." + }, + "noImages": "Для цієї камери не знайдено мініатюр", + "unknownLabel": "Збережене зображення тригера" + } +} diff --git a/web/public/locales/uk/components/filter.json b/web/public/locales/uk/components/filter.json new file mode 100644 index 0000000..a99867c --- /dev/null +++ b/web/public/locales/uk/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "Фiльтр", + "explore": { + "settings": { + "defaultView": { + "desc": "Якщо фільтри не вибрано, відобразити резюме останніх відстежуваних об'єктів за міткою, або показати нефільтровану сітку.", + "title": "Вид за замовчуванням", + "summary": "Підсумок", + "unfilteredGrid": "Нефільтровані сiтка" + }, + "searchSource": { + "desc": "Виберіть, чи слід шукати мініатюри або описи відстежуваних об'єктів.", + "label": "Пошук джерела", + "options": { + "thumbnailImage": "Зображення мініатюри", + "description": "Опис" + } + }, + "title": "Налаштування", + "gridColumns": { + "title": "Колонки сітки", + "desc": "Виберіть кількість стовпчиків у вигляді сітки." + } + }, + "date": { + "selectDateBy": { + "label": "Виберіть дату для фільтрування" + } + } + }, + "labels": { + "count_one": "{{count}} Етикетка", + "all": { + "title": "Всi етикетки", + "short": "Етикетки" + }, + "label": "Етикетки", + "count_other": "{{count}} Етикетки" + }, + "cameras": { + "all": { + "short": "Камери", + "title": "Всi камери" + }, + "label": "Фільтр камери" + }, + "timeRange": "Часовий діапазон", + "features": { + "hasSnapshot": "Має своє уявлення", + "hasVideoClip": "Має відеокліп", + "label": "Особливості", + "submittedToFrigatePlus": { + "label": "Подані до Frigate+", + "tips": "Спочатку потрібно відфільтрувати об'єкти, які відслідковуються.

    Відстежувані об'єкти без знімка не можуть бути передані Frigate+." + } + }, + "sort": { + "dateAsc": "Дата (за зростанням)", + "dateDesc": "Дата (по спадаючій)", + "label": "Сортування", + "scoreAsc": "Оцiнка об'єкту (За зростанням)", + "scoreDesc": "Оцiнка об'єкту (За спаданням)", + "speedAsc": "Розрахункова швидкість (За зростанням)", + "speedDesc": "Розрахункова швидкість (За спаданням)", + "relevance": "Актуальність" + }, + "trackedObjectDelete": { + "desc": "Видалення цих {{objectLength}} відстежуваних об'єктів видаляє знімок, будь-які збережені вкладення та пов'язані з ними записи життєвого циклу об'єкта. Записані кадри цих відстежуваних об'єктів у перегляді історії НЕ будуть видалені.

    Ви впевнені, що хочете продовжити?

    Утримуйте клавішу Shift, щоб обійти це діалогове вікно в майбутньому.", + "title": "Підтвердіть видалення", + "toast": { + "success": "Відстежені об'єкти успішно видалені.", + "error": "Не вдалося видалити відстежувані об'єкти: {{errorMessage}}" + } + }, + "zones": { + "label": "Зони", + "all": { + "title": "Всi зони", + "short": "Зони" + } + }, + "dates": { + "all": { + "title": "Всi дати", + "short": "Дати" + }, + "selectPreset": "Виберіть пресет…" + }, + "more": "Кілька фільтрів", + "reset": { + "label": "Відновити типові значення фільтрів" + }, + "subLabels": { + "label": "Суб-мiтка", + "all": "Всi суб-мiтки" + }, + "score": "Рахунок", + "estimatedSpeed": "Розрахункова швидкість ({{unit}})", + "review": { + "showReviewed": "Показувати переглянуті" + }, + "motion": { + "showMotionOnly": "Показати тiльки рух" + }, + "logSettings": { + "label": "Фільтр рівня журналу", + "filterBySeverity": "Фільтрувати журнали за ступенем тяжкості", + "loading": { + "title": "Завантаження", + "desc": "Коли панель журналу прокручується внизу, нові журнали автоматично транслюються після додавання." + }, + "disableLogStreaming": "Вимикати журнал стрімінгу", + "allLogs": "Всi журнали" + }, + "zoneMask": { + "filterBy": "Фільтрувати за маскою зони" + }, + "recognizedLicensePlates": { + "title": "Розпізнано номерні знаки", + "loadFailed": "Не вдалося завантажити розпізнані номерні знаки.", + "loading": "Завантаження визнаних номерів…", + "placeholder": "Введіть для пошуку номерні знаки…", + "noLicensePlatesFound": "Номерних знаків не знайдено.", + "selectPlatesFromList": "Виберіть одну або кілька пластин зі списку.", + "selectAll": "Вибрати все", + "clearAll": "Очистити все" + }, + "classes": { + "label": "Заняття", + "all": { + "title": "Усі класи" + }, + "count_one": "Клас {{count}}", + "count_other": "{{count}} Класи" + }, + "attributes": { + "label": "Атрибути класифікації", + "all": "Усі атрибути" + } +} diff --git a/web/public/locales/uk/components/icons.json b/web/public/locales/uk/components/icons.json new file mode 100644 index 0000000..bb5ab6e --- /dev/null +++ b/web/public/locales/uk/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Оберіть іконку", + "search": { + "placeholder": "Пошук значка…" + } + } +} diff --git a/web/public/locales/uk/components/input.json b/web/public/locales/uk/components/input.json new file mode 100644 index 0000000..e7b818d --- /dev/null +++ b/web/public/locales/uk/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Завантажити відео", + "toast": { + "success": "Розпочата завантаження відео." + } + } + } +} diff --git a/web/public/locales/uk/components/player.json b/web/public/locales/uk/components/player.json new file mode 100644 index 0000000..7577dff --- /dev/null +++ b/web/public/locales/uk/components/player.json @@ -0,0 +1,52 @@ +{ + "noRecordingsFoundForThisTime": "Не знайдено жодного запису", + "noPreviewFoundFor": "Попередній перегляд для {{cameraName}} не знайдені", + "livePlayerRequiredIOSVersion": "Для цього типу потокового передавання потрібна iOS 17.1 або пізніша версія.", + "stats": { + "droppedFrames": { + "short": { + "title": "Пропущене", + "value": "{{droppedFrames}} кадрiв" + }, + "title": "Пропущене кадрiв:" + }, + "droppedFrameRate": "Частота пропущенив кадрiв:", + "streamType": { + "short": "Тип", + "title": "Тип потоку:" + }, + "bandwidth": { + "title": "Пропускна здатність:", + "short": "Пропускна здатність" + }, + "latency": { + "title": "Затримка:", + "short": { + "title": "Затримка", + "value": "{{seconds}} сек" + }, + "value": "{{seconds}} секунд" + }, + "totalFrames": "Всього кадрiв:", + "decodedFrames": "Декодовані кадри:" + }, + "noPreviewFound": "Попередній перегляд не знайдені", + "submitFrigatePlus": { + "title": "Відправити це зображення Frigate+?", + "submit": "Посилати" + }, + "streamOffline": { + "title": "Струм офлайн", + "desc": "Потік detect камери {{cameraName}} не отримував ніяких кадрів, перевірте журнал помилок" + }, + "cameraDisabled": "Камера вимкнена", + "toast": { + "success": { + "submittedFrigatePlus": "Зображення успішно завантажено до Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Не вдалося надіслати фрейм Frigate+" + } + }, + "cameraOff": "Камера вимкнена" +} diff --git a/web/public/locales/uk/config/cameras.json b/web/public/locales/uk/config/cameras.json new file mode 100644 index 0000000..4de1a14 --- /dev/null +++ b/web/public/locales/uk/config/cameras.json @@ -0,0 +1,72 @@ +{ + "name": { + "label": "Назва камери", + "description": "Потрібно вказати назву камери" + }, + "label": "Конфігурація камери", + "friendly_name": { + "label": "Зрозуміле ім'я", + "description": "Зручна назва камери, що використовується в інтерфейсі Frigate" + }, + "enabled": { + "label": "Увімкнено", + "description": "Увімкнено" + }, + "audio": { + "label": "Виявлення звуку", + "enabled": { + "label": "Увімкнути виявлення звуку", + "description": "Увімкнути або вимкнути виявлення звукових подій для цієї камери." + }, + "max_not_heard": { + "description": "Кількість секунд без налаштованого типу звуку перед тим, як звукова подія закінчиться.", + "label": "Кінець очікування" + }, + "min_volume": { + "label": "Мінімальний рівень звуку", + "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)." + }, + "num_threads": { + "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)." + }, + "listen": { + "description": "Список звукових подій для виявлення (наприклад, bark, fire_alarm, speech, yell).", + "label": "Типи звукових подій" + }, + "filters": { + "label": "Фільтри звуку" + }, + "description": "Налаштування звукових подій для цієї камери." + }, + "audio_transcription": { + "label": "Транскрипція аудіо", + "enabled": { + "label": "Увімкнути транскрипцію" + }, + "live_enabled": { + "description": "Увімкнути потокову транскрипцію аудіо в реальному часі коли вона надходить." + } + }, + "birdseye": { + "label": "Режим Birdseye", + "description": "Налаштування для складеного елементу перегляду режиму Birdseye який відображає потоки з декількох камер на єдиному макеті.", + "enabled": { + "label": "Увімкнути Birdseye", + "description": "Увімкнути або вимкнути функцію перегляду у режимі Birdseye." + }, + "mode": { + "label": "Режим відстежування", + "description": "Режими для додавання камер в Birdseye: 'objects' (об'єкти), 'motion' (рух) або 'continuous' (безперервний)." + }, + "order": { + "label": "Розміщення", + "description": "Номер позиції, що визначає розміщення камери на макеті Birdseye." + } + }, + "detect": { + "label": "Виявлення об'єктів", + "enabled": { + "label": "Увімкнути виявлення об'єктів" + } + } +} diff --git a/web/public/locales/uk/config/global.json b/web/public/locales/uk/config/global.json new file mode 100644 index 0000000..6618f81 --- /dev/null +++ b/web/public/locales/uk/config/global.json @@ -0,0 +1,54 @@ +{ + "audio": { + "label": "Виявлення звуку", + "enabled": { + "label": "Увімкнути виявлення звуку" + }, + "max_not_heard": { + "description": "Кількість секунд без налаштованого типу звуку перед тим, як звукова подія закінчиться.", + "label": "Кінець очікування" + }, + "min_volume": { + "label": "Мінімальний рівень звуку", + "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)." + }, + "num_threads": { + "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)." + }, + "listen": { + "description": "Список звукових подій для виявлення (наприклад, bark, fire_alarm, speech, yell).", + "label": "Типи звукових подій" + }, + "filters": { + "label": "Фільтри звуку" + } + }, + "audio_transcription": { + "label": "Транскрипція аудіо", + "live_enabled": { + "description": "Увімкнути потокову транскрипцію аудіо в реальному часі коли вона надходить." + } + }, + "birdseye": { + "label": "Режим Birdseye", + "description": "Налаштування для складеного елементу перегляду режиму Birdseye який відображає потоки з декількох камер на єдиному макеті.", + "enabled": { + "label": "Увімкнути Birdseye", + "description": "Увімкнути або вимкнути функцію перегляду у режимі Birdseye." + }, + "mode": { + "label": "Режим відстежування", + "description": "Режими для додавання камер в Birdseye: 'objects' (об'єкти), 'motion' (рух) або 'continuous' (безперервний)." + }, + "order": { + "label": "Розміщення", + "description": "Номер позиції, що визначає розміщення камери на макеті Birdseye." + } + }, + "detect": { + "label": "Виявлення об'єктів", + "enabled": { + "label": "Увімкнути виявлення об'єктів" + } + } +} diff --git a/web/public/locales/uk/config/groups.json b/web/public/locales/uk/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uk/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uk/config/validation.json b/web/public/locales/uk/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uk/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uk/objects.json b/web/public/locales/uk/objects.json new file mode 100644 index 0000000..881ac79 --- /dev/null +++ b/web/public/locales/uk/objects.json @@ -0,0 +1,120 @@ +{ + "dog": "Собака", + "cat": "Кіт", + "horse": "Кінь", + "sheep": "Вівця", + "bark": "Лай", + "goat": "Коза", + "mouse": "Миш", + "skateboard": "Скейтборд", + "bird": "Птах", + "boat": "Човен", + "car": "Автомобіль", + "bus": "Автобус", + "motorcycle": "Мотоцикл", + "train": "Поїзд", + "bicycle": "Велосипед", + "keyboard": "Клавіатура", + "door": "Двері", + "sink": "Раковина", + "animal": "Тварина", + "vehicle": "Транспорт", + "blender": "Блендер", + "hair_dryer": "Фен", + "toothbrush": "Зубна щітка", + "scissors": "Ножиці", + "clock": "Годинник", + "toilet": "Вбиральня", + "spoon": "Ложка", + "tennis_racket": "Тенісна ракетка", + "potted_plant": "Кімнатна рослина", + "bbq_grill": "Гриль та барбекю", + "person": "Людина", + "airplane": "Літак", + "traffic_light": "Світлофор", + "fire_hydrant": "Пожежний гідрант", + "parking_meter": "Паркоматів", + "bench": "Лавка", + "cow": "Корова", + "elephant": "Слон", + "bear": "Ведмідь", + "zebra": "Зебра", + "giraffe": "Жираф", + "hat": "Шапка", + "backpack": "Рюкзак", + "street_sign": "Дорожній знак", + "stop_sign": "Знак зупинки", + "umbrella": "Парасолька", + "shoe": "Взуття", + "eye_glasses": "Окуляри", + "handbag": "Гаманець", + "tie": "Краватка", + "suitcase": "Валiза", + "frisbee": "Фрiсбi", + "skis": "Лижи", + "snowboard": "Сноуборд", + "sports_ball": "Спортивні м'яч", + "kite": "Повітряний змій", + "baseball_bat": "Бейсбольна біта", + "baseball_glove": "Рукавичка ловця", + "surfboard": "Дошка для серфінгу", + "bottle": "Пляшка", + "plate": "Тарiлка", + "wine_glass": "Винний келих", + "cup": "Чашка", + "fork": "Виделка", + "knife": "Нiж", + "bowl": "Миска", + "banana": "Банан", + "apple": "Яблуко", + "sandwich": "Сендвіч", + "orange": "Апельсин", + "broccoli": "Броколі", + "carrot": "Морква", + "hot_dog": "Хот-дог", + "pizza": "Пiца", + "donut": "Пампушка", + "cake": "Торт", + "chair": "Стілець", + "couch": "Диван", + "bed": "Лiжко", + "mirror": "Люстерко", + "dining_table": "Обідній стіл", + "window": "Вiкно", + "desk": "Стiл", + "tv": "Телевізор", + "laptop": "Лептоп", + "remote": "Пульт дистанційного керування", + "cell_phone": "Мобільний телефон", + "microwave": "Мікрохвильовка", + "oven": "Пiч", + "toaster": "Тостер", + "refrigerator": "Холодильник", + "book": "Книжка", + "vase": "Ваза", + "teddy_bear": "Плюшевий ведмедик", + "hair_brush": "Гребінець", + "squirrel": "Білка", + "deer": "Олень", + "fox": "Лисиця", + "rabbit": "Кролик", + "raccoon": "Єнот", + "robot_lawnmower": "Роботизована газонокосарка", + "waste_bin": "Відро для сміття", + "on_demand": "На вимогу", + "face": "Обличчя", + "license_plate": "Номерний знак", + "package": "Посилка", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "Fed Ex", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator'", + "postnl": "PostNL'", + "nzpost": "NZPost'", + "postnord": "PostNord'", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/web/public/locales/uk/views/chat.json b/web/public/locales/uk/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uk/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uk/views/classificationModel.json b/web/public/locales/uk/views/classificationModel.json new file mode 100644 index 0000000..82739b6 --- /dev/null +++ b/web/public/locales/uk/views/classificationModel.json @@ -0,0 +1,197 @@ +{ + "documentTitle": "Моделі класифікації - Frigate", + "button": { + "deleteClassificationAttempts": "Видалити зображення класифікації", + "renameCategory": "Перейменувати клас", + "deleteCategory": "Видалити клас", + "deleteImages": "Видалити зображення", + "trainModel": "Модель поїзда", + "addClassification": "Додати класифікацію", + "deleteModels": "Видалити моделі", + "editModel": "Редагувати модель" + }, + "toast": { + "success": { + "deletedCategory_one": "Видалено {{count}} клас", + "deletedCategory_few": "Видалено {{count}} класи", + "deletedCategory_many": "Видалено {{count}} класів", + "deletedImage_one": "Видалено {{count}} зображення", + "deletedImage_few": "Видалено {{count}} зображення", + "deletedImage_many": "Видалено {{count}} зображень", + "categorizedImage": "Зображення успішно класифіковано", + "trainedModel": "Успішно навчена модель.", + "trainingModel": "Успішно розпочато навчання моделі.", + "deletedModel_one": "Успішно видалено модель {{count}}", + "deletedModel_few": "Успішно видалено моделей {{count}}", + "deletedModel_many": "Успішно видалено моделі {{count}}", + "updatedModel": "Конфігурацію моделі успішно оновлено", + "renamedCategory": "Клас успішно перейменовано на {{name}}" + }, + "error": { + "deleteImageFailed": "Не вдалося видалити: {{errorMessage}}", + "deleteCategoryFailed": "Не вдалося видалити клас: {{errorMessage}}", + "categorizeFailed": "Не вдалося класифікувати зображення: {{errorMessage}}", + "trainingFailed": "Навчання моделі не вдалося. Перегляньте журнали Frigate для отримання детальної інформації.", + "deleteModelFailed": "Не вдалося видалити модель: {{errorMessage}}", + "updateModelFailed": "Не вдалося оновити модель: {{errorMessage}}", + "renameCategoryFailed": "Не вдалося перейменувати клас: {{errorMessage}}", + "trainingFailedToStart": "Не вдалося розпочати навчання моделі: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Видалити клас", + "desc": "Ви впевнені, що хочете видалити клас {{name}}? Це назавжди видалить усі пов'язані зображення та вимагатиме повторного навчання моделі.", + "minClassesTitle": "Не вдається видалити клас", + "minClassesDesc": "Модель класифікації повинна мати щонайменше 2 класи. Додайте ще один клас, перш ніж видаляти цей." + }, + "deleteDatasetImages": { + "title": "Видалити зображення набору даних", + "desc_one": "Ви впевнені, що хочете видалити {{count}} зображень з {{dataset}}? Цю дію неможливо скасувати, вона вимагатиме повторного навчання моделі.", + "desc_few": "Ви впевнені, що хочете видалити {{count}} зображенні з {{dataset}}? Цю дію неможливо скасувати, вона вимагатиме повторного навчання моделі.", + "desc_many": "Ви впевнені, що хочете видалити {{count}} зображенні з {{dataset}}? Цю дію неможливо скасувати, вона вимагатиме повторного навчання моделі." + }, + "deleteTrainImages": { + "title": "Видалити зображення поїздів", + "desc_one": "Ви впевнені, що хочете видалити {{count}} зображень? Цю дію не можна скасувати.", + "desc_few": "Ви впевнені, що хочете видалити {{count}} зображенні? Цю дію не можна скасувати.", + "desc_many": "Ви впевнені, що хочете видалити {{count}} зображенні? Цю дію не можна скасувати." + }, + "renameCategory": { + "title": "Перейменувати клас", + "desc": "Введіть нову назву для {{name}}. Вам потрібно буде перенавчити модель, щоб зміна назви набула чинності." + }, + "description": { + "invalidName": "Недійсне ім'я. Ім'я може містити лише літери, цифри, пробіли, апострофи, символи підкреслення та дефіси." + }, + "train": { + "title": "Нещодавні класифікації", + "titleShort": "Нещодавні", + "aria": "Виберіть останні класифікації" + }, + "categories": "Заняття", + "createCategory": { + "new": "Створити новий клас" + }, + "categorizeImageAs": "Класифікувати зображення як:", + "categorizeImage": "Класифікувати зображення", + "noModels": { + "object": { + "title": "Без моделей класифікації об'єктів", + "description": "Створіть власну модель для класифікації виявлених об'єктів.", + "buttonText": "Створення об'єктної моделі" + }, + "state": { + "title": "Без моделей класифікації штатів", + "description": "Створіть власну модель для моніторингу та класифікації змін стану в певних областях камери.", + "buttonText": "Створити модель стану" + } + }, + "wizard": { + "title": "Створити нову класифікацію", + "steps": { + "nameAndDefine": "Назва та визначення", + "stateArea": "Площа штату", + "chooseExamples": "Виберіть приклади" + }, + "step1": { + "description": "Моделі станів відстежують зміни в зонах дії фіксованих камер (наприклад, відкриття/закриття дверей). Моделі об'єктів додають класифікації до виявлених об'єктів (наприклад, відомі тварини, кур'єри тощо).", + "name": "Ім'я", + "namePlaceholder": "Введіть назву моделі...", + "type": "Тип", + "typeState": "Штат", + "typeObject": "Об'єкт", + "objectLabel": "Мітка об'єкта", + "objectLabelPlaceholder": "Виберіть тип об'єкта...", + "classificationType": "Тип класифікації", + "classificationTypeTip": "Дізнайтеся про типи класифікації", + "classificationTypeDesc": "Підмітки додають додатковий текст до мітки об’єкта (наприклад, «Особа: UPS»). Атрибути – це метадані для пошуку, що зберігаються окремо в метаданих об’єкта.", + "classificationSubLabel": "Підмітка", + "classificationAttribute": "Атрибут", + "classes": "Заняття", + "classesTip": "Дізнайтеся про заняття", + "classesStateDesc": "Визначте різні стани, в яких може перебувати зона вашої камери. Наприклад: «відкрито» та «закрито» для гаражних воріт.", + "classesObjectDesc": "Визначте різні категорії для класифікації виявлених об'єктів. Наприклад: «доставник», «мешканець», «незнайомець» для класифікації осіб.", + "classPlaceholder": "Введіть назву класу...", + "errors": { + "nameRequired": "Назва моделі обов'язкова", + "nameLength": "Назва моделі має містити не більше 64 символів", + "nameOnlyNumbers": "Назва моделі не може містити лише цифри", + "classRequired": "Потрібно хоча б 1 заняття", + "classesUnique": "Назви класів мають бути унікальними", + "stateRequiresTwoClasses": "Моделі станів вимагають щонайменше 2 класів", + "objectLabelRequired": "Будь ласка, виберіть мітку об'єкта", + "objectTypeRequired": "Будь ласка, виберіть тип класифікації", + "noneNotAllowed": "Клас «none» не дозволено" + }, + "states": "Штати" + }, + "step2": { + "description": "Виберіть камери та визначте область для моніторингу для кожної камери. Модель класифікуватиме стан цих областей.", + "cameras": "Камери", + "selectCamera": "Виберіть Камеру", + "noCameras": "Натисніть +, щоб додати камери", + "selectCameraPrompt": "Виберіть камеру зі списку, щоб визначити її зону спостереження" + }, + "step3": { + "selectImagesPrompt": "Виберіть усі зображення з: {{className}}", + "selectImagesDescription": "Натисніть на зображення, щоб вибрати їх. Натисніть «Продовжити», коли закінчите з цим уроком.", + "generating": { + "title": "Створення зразків зображень", + "description": "Фрегат отримує типові зображення з ваших записів. Це може зайняти деякий час..." + }, + "training": { + "title": "Модель навчання", + "description": "Ваша модель навчається у фоновому режимі. Закрийте це діалогове вікно, і ваша модель почне працювати, щойно навчання буде завершено." + }, + "retryGenerate": "Генерація повторних спроб", + "noImages": "Немає згенерованих зразків зображень", + "classifying": "Класифікація та навчання...", + "trainingStarted": "Навчання розпочалося успішно", + "errors": { + "noCameras": "Немає налаштованих камер", + "noObjectLabel": "Мітку об'єкта не вибрано", + "generateFailed": "Не вдалося створити приклади: {{error}}", + "generationFailed": "Помилка генерації. Будь ласка, спробуйте ще раз.", + "classifyFailed": "Не вдалося класифікувати зображення: {{error}}" + }, + "generateSuccess": "Зразки зображень успішно створено", + "allImagesRequired_one": "Будь ласка, класифікуйте всі зображення. Залишилося {{count}} зображення.", + "allImagesRequired_few": "Будь ласка, класифікуйте всі зображення. Залишилося зображень: {{count}}.", + "allImagesRequired_many": "Будь ласка, класифікуйте всі зображення. Залишилося зображень: {{count}}.", + "modelCreated": "Модель успішно створено. Використовуйте режим перегляду «Нещодавні класифікації», щоб додати зображення для відсутніх станів, а потім навчіть модель.", + "missingStatesWarning": { + "title": "Приклади відсутніх станів", + "description": "Для найкращих результатів рекомендується вибрати приклади для всіх станів. Ви можете продовжити, не вибираючи всі стани, але модель не буде навчена, доки всі стани не матимуть зображень. Після продовження скористайтеся поданням «Нещодавні класифікації», щоб класифікувати зображення для відсутніх станів, а потім навчіть модель." + } + } + }, + "deleteModel": { + "title": "Видалити модель класифікації", + "single": "Ви впевнені, що хочете видалити {{name}}? Це назавжди видалить усі пов’язані дані, включаючи зображення та дані навчання. Цю дію не можна скасувати.", + "desc_one": "Ви впевнені, що хочете видалити {{count}} модель? Це назавжди видалить усі пов’язані дані, включаючи зображення та навчальні дані. Цю дію не можна скасувати.", + "desc_few": "Ви впевнені, що хочете видалити {{count}} моделей? Це назавжди видалить усі пов’язані дані, включаючи зображення та навчальні дані. Цю дію не можна скасувати.", + "desc_many": "Ви впевнені, що хочете видалити {{count}} моделі? Це назавжди видалить усі пов’язані дані, включаючи зображення та навчальні дані. Цю дію не можна скасувати." + }, + "menu": { + "objects": "Об'єкти", + "states": "Стани" + }, + "details": { + "scoreInfo": "Оцінка представляє середню достовірність класифікації для всіх виявлень цього об'єкта.", + "none": "Жоден", + "unknown": "Невідомо" + }, + "edit": { + "title": "Редагувати модель класифікації", + "descriptionState": "Відредагуйте класи для цієї моделі класифікації штатів. Зміни вимагатимуть перенавчання моделі.", + "descriptionObject": "Відредагуйте тип об'єкта та тип класифікації для цієї моделі класифікації об'єктів.", + "stateClassesInfo": "Примітка: Зміна класів станів вимагає перенавчання моделі з використанням оновлених класів." + }, + "tooltip": { + "trainingInProgress": "Модель зараз тренується", + "noNewImages": "Немає нових зображень для навчання. Спочатку класифікуйте більше зображень у наборі даних.", + "modelNotReady": "Модель не готова до навчання", + "noChanges": "З моменту останнього навчання в наборі даних не було змін." + }, + "none": "Жоден" +} diff --git a/web/public/locales/uk/views/configEditor.json b/web/public/locales/uk/views/configEditor.json new file mode 100644 index 0000000..0e3ef13 --- /dev/null +++ b/web/public/locales/uk/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "saveAndRestart": "Зберегти та перезавантажити", + "toast": { + "error": { + "savingError": "Помилка збереження конфігурації" + }, + "success": { + "copyToClipboard": "Налаштування було скопійовано до буфера обміну даними." + } + }, + "documentTitle": "Редактор конфігурації - Frigate", + "copyConfig": "Скопіювати конфігурацію", + "saveOnly": "Тільки зберегти", + "configEditor": "Налаштування редактора", + "confirm": "Вийти без збереження?", + "safeConfigEditor": "Редактор конфігурації (безпечний режим)", + "safeModeDescription": "Фрегат перебуває в безпечному режимі через помилку перевірки конфігурації." +} diff --git a/web/public/locales/uk/views/events.json b/web/public/locales/uk/views/events.json new file mode 100644 index 0000000..5b3c204 --- /dev/null +++ b/web/public/locales/uk/views/events.json @@ -0,0 +1,67 @@ +{ + "camera": "Камера", + "selected_one": "{{count}} відібраний", + "selected_other": "{{count}} відібраний", + "alerts": "Оповіщення", + "detections": "Виявлень", + "motion": { + "label": "Рух", + "only": "Тiльки рух" + }, + "allCameras": "Всi камери", + "empty": { + "alert": "Немає попереджень для перегляду", + "detection": "Немає ніяких ознак", + "motion": "Даних про рух не знайдено", + "recordingsDisabled": { + "title": "Записи мають бути ввімкнені", + "description": "Елементи рецензування можна створювати для камери, лише якщо для цієї камери ввімкнено запис." + } + }, + "timeline": "Хронологія", + "timeline.aria": "Вибрати хронiку", + "events": { + "label": "Події", + "aria": "Выбрати події", + "noFoundForTimePeriod": "За цей період подій не знайдено." + }, + "documentTitle": "Перегляд подiя - Frigate", + "recordings": { + "documentTitle": "Записи - Frigate" + }, + "calendarFilter": { + "last24Hours": "Останні 24 години" + }, + "markAsReviewed": "Прибрати позначку про необхідність огляду", + "markTheseItemsAsReviewed": "Позначити ці елементи як переглянуті", + "newReviewItems": { + "label": "Переглянути нові елементи огляду", + "button": "Нові матеріали для перегляду" + }, + "detected": "виявлено", + "suspiciousActivity": "Підозріла активність", + "threateningActivity": "Загрозлива діяльність", + "detail": { + "noDataFound": "Немає детальних даних для перегляду", + "aria": "Перемикання детального перегляду", + "trackedObject_one": "{{count}} об'єкт", + "trackedObject_other": "{{count}} об'єкти", + "noObjectDetailData": "Детальні дані про об'єкт недоступні.", + "label": "Деталь", + "settings": "Налаштування детального перегляду", + "alwaysExpandActive": { + "title": "Завжди розгортати активне", + "desc": "Завжди розгортайте деталі об'єкта активного елемента огляду, якщо вони доступні." + } + }, + "objectTrack": { + "trackedPoint": "Відстежувана Точка", + "clickToSeek": "Натисніть, щоб перейти до цього часу" + }, + "zoomIn": "Збільшити масштаб", + "zoomOut": "Зменшити масштаб", + "normalActivity": "Звичайний", + "needsReview": "Потребує перегляду", + "securityConcern": "Проблема безпеки", + "select_all": "Усі" +} diff --git a/web/public/locales/uk/views/explore.json b/web/public/locales/uk/views/explore.json new file mode 100644 index 0000000..0c7863e --- /dev/null +++ b/web/public/locales/uk/views/explore.json @@ -0,0 +1,305 @@ +{ + "exploreIsUnavailable": { + "downloadingModels": { + "tips": { + "documentation": "Прочитати документацію", + "context": "Можливо, вам варто переіндексувати вбудовування відстежуваних об'єктів після завантаження моделей." + }, + "setup": { + "visionModel": "Модель зору", + "visionModelFeatureExtractor": "Екстрактор ознак моделі зору", + "textModel": "Текстова модель", + "textTokenizer": "Токенізатор тексту" + }, + "error": "Сталася помилка. Перевірте журнали Frigate.", + "context": "Frigate завантажує необхідні моделі вбудовування для підтримки функції семантичного пошуку. Це може тривати кілька хвилин залежно від швидкості вашого мережевого з’єднання." + }, + "title": "Огляд недоступний", + "embeddingsReindexing": { + "context": "Функцію «Дослідити» можна використовувати після завершення переіндексації вбудовування відстежуваних об’єктів.", + "startingUp": "Запуск…", + "estimatedTime": "Орієнтовний час, що залишився:", + "finishingShortly": "Закінчується незабаром", + "step": { + "thumbnailsEmbedded": "Вбудовані мініатюри: ", + "descriptionsEmbedded": "Вбудовані описи: ", + "trackedObjectsProcessed": "Оброблено відстежуваних об'єктів: " + } + } + }, + "documentTitle": "Пошук подія - Frigate", + "searchResult": { + "tooltip": "Збігається з {{type}} на рівні {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "error": "Не вдалося видалити відстежуваний об'єкт: {{errorMessage}}", + "success": "Відстежуваний об'єкт успішно видалено." + } + }, + "previousTrackedObject": "Попередній відстежуваний об'єкт", + "nextTrackedObject": "Наступний відстежуваний об'єкт" + }, + "trackedObjectsCount_one": "{{count}} відстежуваний об'єкт ", + "trackedObjectsCount_few": "{{count}} відстежувані об'єкти ", + "trackedObjectsCount_many": "{{count}} відстежувані об'єктів ", + "objectLifecycle": { + "title": "Життєвий цикл об'єкта", + "createObjectMask": "Створити маску об'єкта", + "annotationSettings": { + "title": "Налаштування анотацій", + "offset": { + "desc": "Ці дані надходять із сигналу виявлення вашої камери, але накладаються на зображення із сигналу запису. Малоймовірно, що два потоки ідеально синхронізовані. В результаті, обмежувальна рамка та відеоматеріал не будуть ідеально вирівняні. Однак, для налаштування цього можна скористатися полем annotation_offset.", + "tips": "ПОРАДА: Уявіть, що є кліп події, в якому людина йде зліва направо. Якщо рамка часової шкали події постійно знаходиться ліворуч від людини, то значення слід зменшити. Аналогічно, якщо людина йде зліва направо, а обмежувальна рамка постійно знаходиться попереду неї, тоді значення слід збільшити.", + "label": "Зсув анотації", + "documentation": "Прочитайте документацію ", + "millisecondsToOffset": "Мілісекунди для зміщення виявлених анотацій. За замовчуванням: 0", + "toast": { + "success": "Зміщення анотації для {{camera}} збережено у файлі конфігурації. Перезапустіть Frigate, щоб застосувати зміни." + } + }, + "showAllZones": { + "title": "Показати всі зони", + "desc": "Завжди показувати зони на кадрах, де об'єкти увійшли в зону." + } + }, + "scrollViewTips": "Прокрутіть, щоб переглянути важливі моменти життєвого циклу цього об'єкта.", + "lifecycleItemDesc": { + "attribute": { + "other": "{{label}} визнаний як {{attribute}}", + "faceOrLicense_plate": "{{attribute}} виявлено для {{label}}" + }, + "header": { + "zones": "Зони", + "ratio": "Співвідношення", + "area": "Площа" + }, + "visible": "{{label}} виявлено", + "entered_zone": "{{label}} увійшов {{zones}}", + "active": "{{label}} став активним", + "stationary": "{{label}} став нерухомим", + "gone": "{{label}} ліворуч", + "heard": "{{label}} чув", + "external": "{{label}} виявлено" + }, + "noImageFound": "Для цієї позначки часу не знайдено зображення.", + "adjustAnnotationSettings": "Налаштування параметрів анотацій", + "autoTrackingTips": "Положення обмежувальних рамок будуть неточними для камер з автоматичним відстеженням.", + "carousel": { + "previous": "Попередній слайд", + "next": "Наступний слайд" + }, + "count": "{{first}} з {{second}}", + "trackedPoint": "Відстежувана точка" + }, + "details": { + "label": "Мітка", + "editLPR": { + "title": "Редагувати номерний знак", + "desc": "Введіть нове значення номерного знака для цього {{label}}", + "descNoLabel": "Введіть нове значення номерного знака для цього відстежуваного об'єкта" + }, + "item": { + "toast": { + "success": { + "updatedLPR": "Номерний знак успішно оновлено.", + "updatedSublabel": "Підмітку успішно оновлено.", + "regenerate": "Новий опис було запрошено від {{provider}}. Залежно від швидкості вашого провайдера, його перегенерація може зайняти деякий час.", + "audioTranscription": "Запит на аудіотранскрипцію успішно надіслано. Залежно від швидкості вашого сервера Frigate, транскрипція може тривати деякий час.", + "updatedAttributes": "Атрибути успішно оновлено." + }, + "error": { + "regenerate": "Не вдалося звернутися до {{provider}} для отримання нового опису: {{errorMessage}}", + "updatedSublabelFailed": "Не вдалося оновити підмітку: {{errorMessage}}", + "updatedLPRFailed": "Не вдалося оновити номерний знак: {{errorMessage}}", + "audioTranscription": "Не вдалося надіслати запит на транскрипцію аудіо: {{errorMessage}}", + "updatedAttributesFailed": "Не вдалося оновити атрибути: {{errorMessage}}" + } + }, + "button": { + "share": "Поділитися цим оглядом", + "viewInExplore": "Переглянути в розділі «Огляд»" + }, + "tips": { + "hasMissingObjects": "Змініть конфігурацію, якщо хочете, щоб Frigate зберігав відстежувані об'єкти для таких міток: {{objects}}", + "mismatch_one": "{{count}} Виявлено та включено до цього елемента огляду недоступний об’єкт. Ці об’єкти або не кваліфікувалися як сповіщення чи виявлення, або вже були очищені/видалені.", + "mismatch_few": "{{count}} Було виявлено та включено до цього елемента огляду недоступні об’єкти. Ці об’єкти або не кваліфікувалися як сповіщення чи виявлення, або вже були очищені/видалені.", + "mismatch_many": "{{count}} Були виявлені та включені до цього елементи огляду недоступні об’єкти. Ці об’єкти або не кваліфікувалися як сповіщення чи виявлення, або вже були очищені/видалені." + }, + "title": "Огляд деталей товару", + "desc": "Переглянути деталі товару" + }, + "editSubLabel": { + "title": "Редагувати підмітку", + "desc": "Введіть нову підмітку для цього {{label}}", + "descNoLabel": "Введіть нову підмітку для цього відстежуваного об'єкта" + }, + "snapshotScore": { + "label": "Оцінка моментального результату" + }, + "topScore": { + "label": "Найкращий результат", + "info": "Найвищий бал – це найвищий середній бал для відстежуваного об’єкта, тому він може відрізнятися від балу, що відображається на мініатюрі результатів пошуку." + }, + "timestamp": "Позначка часу", + "recognizedLicensePlate": "Розпізнаний номерний знак", + "zones": "Зони", + "description": { + "aiTips": "Фрегат не запитуватиме опис у вашого постачальника генеративного штучного інтелекту, доки не завершиться життєвий цикл відстежуваного об'єкта.", + "placeholder": "Опис відстежуваного об'єкта", + "label": "Опис" + }, + "regenerateFromThumbnails": "Згенерувати з мініатюр", + "tips": { + "descriptionSaved": "Опис успішно збережено", + "saveDescriptionFailed": "Не вдалося оновити опис: {{errorMessage}}" + }, + "objects": "Об'єкти", + "estimatedSpeed": "Орієнтовна швидкість", + "camera": "Камера", + "button": { + "findSimilar": "Знайти схожі", + "regenerate": { + "title": "Регенерувати", + "label": "Згенерувати опис відстежуваного об'єкта повторно" + } + }, + "expandRegenerationMenu": "Розгорнути меню регенерації", + "regenerateFromSnapshot": "Відновити зі знімка", + "score": { + "label": "Оцінка" + }, + "editAttributes": { + "title": "Редагувати атрибути", + "desc": "Виберіть атрибути класифікації для цього {{label}}" + }, + "attributes": "Атрибути класифікації", + "title": { + "label": "Назва" + } + }, + "dialog": { + "confirmDelete": { + "title": "Підтвердити видалення", + "desc": "Видалення цього відстежуваного об'єкта призведе до видалення знімка, усіх збережених вбудованих даних та усіх пов'язаних записів деталей відстеження. Записані кадри цього відстежуваного об'єкта в режимі перегляду історії НЕ будуть видалені.

    Ви впевнені, що хочете продовжити?" + } + }, + "itemMenu": { + "findSimilar": { + "label": "Знайти схожі", + "aria": "Знайти схожі відстежувані об'єкти" + }, + "viewInHistory": { + "label": "Переглянути в історії", + "aria": "Переглянути в історії" + }, + "downloadVideo": { + "aria": "Завантажити Відео", + "label": "Завантажити Відео" + }, + "submitToPlus": { + "aria": "Надіслати до Frigate Plus", + "label": "Надіслати до Frigate+" + }, + "downloadSnapshot": { + "label": "Завантажити знімок", + "aria": "Завантажити знімок" + }, + "viewObjectLifecycle": { + "label": "Переглянути життєвий цикл об'єкта", + "aria": "Показати життєвий цикл об'єкта" + }, + "deleteTrackedObject": { + "label": "Видалити цей відстежуваний об'єкт" + }, + "addTrigger": { + "label": "Додати тригер", + "aria": "Додати тригер для цього відстежуваного об'єкта" + }, + "audioTranscription": { + "label": "Транскрибувати", + "aria": "Запит на аудіотранскрипцію" + }, + "viewTrackingDetails": { + "label": "Переглянути деталі відстеження", + "aria": "Показати деталі відстеження" + }, + "showObjectDetails": { + "label": "Показати шлях до об'єкта" + }, + "hideObjectDetails": { + "label": "Приховати шлях до об'єкта" + }, + "downloadCleanSnapshot": { + "label": "Завантажити чистий знімок", + "aria": "Завантажити чистий знімок" + } + }, + "noTrackedObjects": "Відстежуваних об'єктів не знайдено", + "fetchingTrackedObjectsFailed": "Помилка отримання відстежуваних об'єктів: {{errorMessage}}", + "generativeAI": "Генеративний ШІ", + "trackedObjectDetails": "Деталі відстежуваного об'єкта", + "type": { + "details": "деталі", + "snapshot": "знімок", + "video": "відео", + "object_lifecycle": "життєвий цикл об'єкта", + "thumbnail": "мініатюра", + "tracking_details": "деталі відстеження" + }, + "exploreMore": "Дослідіть більше об'єктів {{label}}", + "aiAnalysis": { + "title": "Аналіз ШІ" + }, + "concerns": { + "label": "Проблеми" + }, + "trackingDetails": { + "title": "Деталі відстеження", + "noImageFound": "Для цієї позначки часу не знайдено зображення.", + "createObjectMask": "Створити маску об'єкта", + "adjustAnnotationSettings": "Налаштування параметрів анотацій", + "scrollViewTips": "Натисніть, щоб переглянути важливі моменти життєвого циклу цього об'єкта.", + "autoTrackingTips": "Положення обмежувальних рамок будуть неточними для камер з автоматичним відстеженням.", + "count": "{{first}} з {{second}}", + "trackedPoint": "Відстежувана точка", + "lifecycleItemDesc": { + "visible": "Виявлено {{label}}", + "entered_zone": "{{label}} увійшов до {{zones}}", + "active": "{{label}} став активним", + "stationary": "{{label}} став нерухомим", + "attribute": { + "faceOrLicense_plate": "Виявлено атрибут {{attribute}} для {{label}}", + "other": "{{label}} розпізнано як {{attribute}}" + }, + "gone": "{{label}} залишилося", + "heard": "{{label}} почув(ла)", + "external": "Виявлено {{label}}", + "header": { + "zones": "Зони", + "ratio": "Співвідношення", + "area": "Площа", + "score": "Рахунок" + } + }, + "annotationSettings": { + "title": "Налаштування анотацій", + "showAllZones": { + "title": "Показати всі зони", + "desc": "Завжди показувати зони на кадрах, де об'єкти увійшли в зону." + }, + "offset": { + "label": "Зсув анотації", + "desc": "Ці дані надходять із каналу виявлення вашої камери, але накладаються на зображення з каналу запису. Малоймовірно, що ці два потоки будуть ідеально синхронізовані. Як результат, обмежувальна рамка та відеоматеріал не будуть ідеально збігатися. Ви можете використовувати це налаштування, щоб змістити анотації вперед або назад у часі, щоб краще узгодити їх із записаним відеоматеріалом.", + "millisecondsToOffset": "Мілісекунди для зміщення виявлених анотацій. За замовчуванням: 0", + "tips": "Зменште значення, якщо відтворення відео відбувається попереду блоків та точок шляху, і збільште значення, якщо відтворення відео відбувається позаду них. Це значення може бути від’ємним.", + "toast": { + "success": "Зміщення анотації для {{camera}} було збережено у файлі конфігурації." + } + } + }, + "carousel": { + "previous": "Попередній слайд", + "next": "Наступний слайд" + } + } +} diff --git a/web/public/locales/uk/views/exports.json b/web/public/locales/uk/views/exports.json new file mode 100644 index 0000000..6b4108f --- /dev/null +++ b/web/public/locales/uk/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Експорта - Frigate", + "search": "Пошук", + "noExports": "Не знайдено експортованих файлів", + "deleteExport": "Видалити експортування", + "deleteExport.desc": "Ви справді бажаєте вилучити {{exportName}}?", + "editExport": { + "title": "Перейменувати експорт", + "desc": "Введіть нову назву для цього експорту.", + "saveExport": "Зберегти експорт" + }, + "toast": { + "error": { + "renameExportFailed": "Не вдалося перейменувати експорт: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Поділитися експортом", + "downloadVideo": "Завантажити відео", + "editName": "Редагувати ім'я", + "deleteExport": "Видалити експорт" + } +} diff --git a/web/public/locales/uk/views/faceLibrary.json b/web/public/locales/uk/views/faceLibrary.json new file mode 100644 index 0000000..1170e3e --- /dev/null +++ b/web/public/locales/uk/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "selectItem": "Вибрати {{item}}", + "documentTitle": "Бібліотека обличчя - Frigate", + "readTheDocs": "Прочитати документацію", + "deleteFaceLibrary": { + "desc": "Ви впевнені, що хочете видалити колекцію {{name}}? Це назавжди видалить усі пов’язані з нею обличчя.", + "title": "Видалити ім'я" + }, + "toast": { + "error": { + "renameFaceFailed": "Не вдалося перейменувати обличчя: {{errorMessage}}", + "updateFaceScoreFailed": "Не вдалося оновити оцінку обличчя: {{errorMessage}}", + "deleteFaceFailed": "Не вдалося видалити: {{errorMessage}}", + "uploadingImageFailed": "Не вдалося завантажити зображення: {{errorMessage}}", + "addFaceLibraryFailed": "Не вдалося встановити ім'я обличчя: {{errorMessage}}", + "deleteNameFailed": "Не вдалося видалити ім'я: {{errorMessage}}", + "trainFailed": "Не вдалося тренуватися: {{errorMessage}}" + }, + "success": { + "updatedFaceScore": "Оцінку обличчя успішно оновлено до {{name}} ({{score}}).", + "deletedName_one": "{{count}} Обличчя успішно видалено.", + "deletedName_few": "{{count}} Обличчі успішно видалено.", + "deletedName_many": "{{count}} Облич. успішно видалено.", + "uploadedImage": "Зображення успішно завантажено.", + "addFaceLibrary": "{{name}} успішно додано до Бібліотеки облич!", + "renamedFace": "Обличчя успішно перейменовано на {{name}}", + "trainedFace": "Успішно натреноване обличчя.", + "deletedFace_one": "Успішно видалено {{count}} обличчя.", + "deletedFace_few": "Успішно видалено {{count}} обличчі.", + "deletedFace_many": "Успішно видалено {{count}} облич." + } + }, + "details": { + "scoreInfo": "Оцінка підмітки – це зважена оцінка для всіх розпізнаних ознак достовірності обличчя, тому вона може відрізнятися від оцінки, показаної на знімку.", + "subLabelScore": "Оцінка підмітки", + "person": "Людина", + "face": "Деталі обличчя", + "faceDesc": "Деталі відстежуваного об'єкта, який створив це обличчя", + "timestamp": "Позначка часу", + "unknown": "Невідомо" + }, + "steps": { + "uploadFace": "Завантажити зображення обличчя", + "nextSteps": "Наступні кроки", + "faceName": "Введіть ім'я обличчя", + "description": { + "uploadFace": "Завантажте зображення обличчя {{name}}, на якому його/її обличчя зображено спереду. Зображення не потрібно обрізати, щоб воно мало лише обличчя." + } + }, + "selectFace": "Виберіть обличчя", + "renameFace": { + "title": "Перейменувати обличчя", + "desc": "Введіть нову назву для {{name}}" + }, + "button": { + "deleteFaceAttempts": "Видалити обличчі", + "renameFace": "Перейменувати обличчя", + "uploadImage": "Завантажити зображення", + "deleteFace": "Видалити обличчя", + "reprocessFace": "Переобробка обличчя", + "addFace": "Додати обличчя" + }, + "imageEntry": { + "maxSize": "Максимальний розмір: {{size}} МБ", + "validation": { + "selectImage": "Будь ласка, виберіть файл зображення." + }, + "dropActive": "Скинь зображення сюди…", + "dropInstructions": "Перетягніть або вставте зображення сюди, або клацніть, щоб вибрати" + }, + "trainFaceAs": "Тренуйте обличчя як:", + "trainFace": "Обличчя поїзда", + "description": { + "addFace": "Додайте нову колекцію до Бібліотеки облич, завантаживши своє перше зображення.", + "placeholder": "Введіть назву для цієї колекції", + "invalidName": "Недійсне ім'я. Ім'я може містити лише літери, цифри, пробіли, апострофи, символи підкреслення та дефіси." + }, + "uploadFaceImage": { + "title": "Завантажити зображення обличчя", + "desc": "Завантажте зображення для сканування облич та додайте його для {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Створити колекцію", + "desc": "Створити нову колекцію", + "new": "Створити нове обличчя", + "nextSteps": "Щоб створити міцну основу:
  • Використовуйте вкладку «Недавні розпізнавання», щоб вибрати та навчити систему розпізнавати зображення для кожної виявленої особи.
  • Для досягнення найкращих результатів зосередьтеся на прямих зображеннях; уникайте навчання зображень, на яких обличчя зняті під кутом.
  • " + }, + "train": { + "title": "Нещодавні визнання", + "aria": "Виберіть нещодавні визнання", + "empty": "Немає останніх спроб розпізнавання обличчя", + "titleShort": "Нещодавні" + }, + "collections": "Колекції", + "deleteFaceAttempts": { + "title": "Видалити обличчі", + "desc_one": "Ви впевнені, що хочете видалити {{count}} обличчя? Цю дію неможливо скасувати.", + "desc_few": "Ви впевнені, що хочете видалити {{count}} обличчі? Цю дію неможливо скасувати.", + "desc_many": "Ви впевнені, що хочете видалити {{count}} облич? Цю дію неможливо скасувати." + }, + "nofaces": "Немає облич", + "pixels": "{{area}}пикс" +} diff --git a/web/public/locales/uk/views/live.json b/web/public/locales/uk/views/live.json new file mode 100644 index 0000000..f5ef34f --- /dev/null +++ b/web/public/locales/uk/views/live.json @@ -0,0 +1,199 @@ +{ + "manualRecording": { + "started": "Почав ручний запис на вимогу.", + "showStats": { + "desc": "Позначте цей пункт, щоб показувати статистику потоку як накладання на канал камери.", + "label": "Показати статистику" + }, + "failedToEnd": "Не вдалося завершити запис вручну на вимогу.", + "playInBackground": { + "label": "Грати у фоновому режимі", + "desc": "Увімкніть цей параметр, щоб продовжувати потокове передавання, коли програвач приховано." + }, + "tips": "Завантажте миттєвий знімок або запустіть ручну подію на основі налаштувань збереження запису цієї камери.", + "title": "На-вимогу", + "debugView": "Режим зневаджування", + "start": "Почати запис за запитом", + "failedToStart": "Не вдалося запустити ручний запис на вимогу.", + "end": "Завершення запису на вимогу", + "ended": "Запис на вимогу припинився.", + "recordDisabledTips": "Оскільки запис вимкнено або обмежено в конфігурації цієї камери, буде збережено лише знімок." + }, + "snapshots": { + "enable": "Увімкнути знімки", + "disable": "Вимкнути знімки" + }, + "stream": { + "twoWayTalk": { + "tips": "Ваш пристрій повинен підтримувати функцію, а WebRTC повинен бути налаштований для двосторонньої розмови.", + "tips.documentation": "Прочитати документацію ", + "available": "Двостороння розмова доступна для цього потоку", + "unavailable": "Двостороння розмова недоступна для цього потоку" + }, + "playInBackground": { + "tips": "Увімкніть цей параметр, щоб продовжувати потокове передавання, коли програвач приховано.", + "label": "Грати у фоновому режимі" + }, + "title": "Потiк", + "audio": { + "tips": { + "documentation": "Прочитати документацію ", + "title": "Звук повинен бути виведений з камери і налаштований в go2rtc для цього потоку." + }, + "available": "Звук доступний для цього потоку", + "unavailable": "Аудіо недоступне для цього потоку" + }, + "lowBandwidth": { + "resetStream": "Скинути потік", + "tips": "Режим перегляду в реальному часі перемикається в економічний режим через помилки буферизації або потоку." + }, + "debug": { + "picker": "Вибір потоку недоступний у режимі налагодження. У режимі налагодження завжди використовується потік, якому призначено роль виявлення." + } + }, + "muteCameras": { + "disable": "Увімкнути звук на всі камери", + "enable": "Вимкнути всі камери" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Клацніть у кадрі, щоб відцентрувати камеру", + "enable": "Увімкнути клацання для переміщення", + "disable": "Вимкнути клацання для переміщення" + }, + "up": { + "label": "Перемістити PTZ камеру вгору" + }, + "left": { + "label": "Переміщення камери PTZ вліво" + }, + "down": { + "label": "Переміщення PTZ камери вниз" + }, + "right": { + "label": "Переміщення PTZ камери вправо" + } + }, + "zoom": { + "in": { + "label": "Наближати PTZ камеру" + }, + "out": { + "label": "Зменшити PTZ камеру" + } + }, + "presets": "Попередни установки PTZ камери", + "frame": { + "center": { + "label": "Клацніть у кадрі, щоб відцентрувати камеру PTZ" + } + }, + "focus": { + "in": { + "label": "Фокус PTZ-камери" + }, + "out": { + "label": "Вихід PTZ-камери для фокусування" + } + } + }, + "editLayout": { + "exitEdit": "Вийти з редагування", + "label": "Редагувати макет", + "group": { + "label": "Редагувати групу камер" + } + }, + "documentTitle": "Пряма трансляція - Frigate", + "documentTitle.withCamera": "{{camera}} - Пряма трансляція - Frigate", + "lowBandwidthMode": "Економічний режим", + "twoWayTalk": { + "enable": "Увімкнути двосторонню розмову", + "disable": "Вимкнути двосторонню розмову" + }, + "cameraAudio": { + "enable": "Увімкнути звук камери", + "disable": "Вимкнути звук камери" + }, + "camera": { + "enable": "Увімкнути камеру", + "disable": "Вимкнути камеру" + }, + "detect": { + "enable": "Увімкнути виявлення", + "disable": "Вимкнути виявлення" + }, + "recording": { + "enable": "Увімкнути запис", + "disable": "Вимкнути запис" + }, + "audioDetect": { + "enable": "Увімкнути виявлення звуку", + "disable": "Вимкнути виявлення звуку" + }, + "autotracking": { + "disable": "Вимкнути автотрекінг", + "enable": "Увімкнути автотрекінг" + }, + "streamStats": { + "enable": "Показати статистику потоку", + "disable": "Сховати статистику потоку" + }, + "streamingSettings": "Параметри потокового передавання", + "notifications": "Повідомлення", + "audio": "Аудіо", + "suspend": { + "forTime": "Призупинити до: " + }, + "cameraSettings": { + "title": "{{camera}} Налаштування", + "cameraEnabled": "Камера включена", + "objectDetection": "Виявлення об'єктів", + "recording": "Записування", + "snapshots": "Знімки", + "audioDetection": "Виявлення звуку", + "autotracking": "Автотрекiнг", + "transcription": "Аудіотранскрипція" + }, + "history": { + "label": "Показати історичні кадри" + }, + "effectiveRetainMode": { + "modes": { + "all": "Всi", + "motion": "Рух", + "active_objects": "Активні об'єкти" + }, + "notAllTips": "Ваш {{source}} конфігурацію збереження записів встановлено на режим: {{effectiveRetainMode}}, тому цей запис на вимогу збереже лише сегменти з {{effectiveRetainModeName}}." + }, + "transcription": { + "enable": "Увімкнути транскрипцію аудіо в реальному часі", + "disable": "Вимкнути транскрипцію аудіо в реальному часі" + }, + "noCameras": { + "title": "Немає налаштованих камер", + "description": "Почніть з підключення камери до Frigate.", + "buttonText": "Додати камеру", + "restricted": { + "title": "Немає Доступних Камер", + "description": "У вас немає дозволу на перегляд будь-яких камер у цій групі." + }, + "default": { + "title": "Немає Налаштованих Камер", + "description": "Почніть з підключення камери до Frigate.", + "buttonText": "Додати Камеру" + }, + "group": { + "title": "Немає камер у групі", + "description": "Цій групі камер не призначено або не ввімкнено камер.", + "buttonText": "Керування групами" + } + }, + "snapshot": { + "takeSnapshot": "Завантажити миттєвий знімок", + "noVideoSource": "Немає доступного джерела відео для знімка.", + "captureFailed": "Не вдалося зробити знімок.", + "downloadStarted": "Розпочато завантаження знімка." + } +} diff --git a/web/public/locales/uk/views/motionSearch.json b/web/public/locales/uk/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uk/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uk/views/recording.json b/web/public/locales/uk/views/recording.json new file mode 100644 index 0000000..a1ae10e --- /dev/null +++ b/web/public/locales/uk/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Експорт", + "toast": { + "error": { + "noValidTimeSelected": "Вибраний діапазон часу не є коректним", + "endTimeMustAfterStartTime": "Час закінчення повинен бути пізніше часу початку" + } + }, + "calendar": "Календар", + "filter": "Фiльтр", + "filters": "Фiльтри" +} diff --git a/web/public/locales/uk/views/replay.json b/web/public/locales/uk/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uk/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uk/views/search.json b/web/public/locales/uk/views/search.json new file mode 100644 index 0000000..052b4c4 --- /dev/null +++ b/web/public/locales/uk/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "Пошук", + "filter": { + "tips": { + "desc": { + "step1": "Введіть назву ключа фільтра, а потім двокрапку (наприклад, «камери:»).", + "step2": "Виберіть значення з пропозицій або введіть своє.", + "step3": "Використовуйте кілька фільтрів, додаючи їх один за одним з проміжком між ними.", + "step5": "Фільтр діапазону часу використовує формат {{exampleTime}}.", + "step6": "Видалити фільтри, натиснувши 'x' поруч з ними.", + "exampleLabel": "Наприклад:", + "step4": "Фільтри дат (до: і після:) використовують формат {{DateFormat}}.", + "text": "Фільтри допомагають звузити результати пошуку. Ось як використовувати їх у полі вводу:" + }, + "title": "Як використовувати текстові фільтри" + }, + "header": { + "currentFilterType": "Фільтрувати значення", + "activeFilters": "Активнi фiльтри", + "noFilters": "Фiлтри" + }, + "label": { + "zones": "Зони", + "sub_labels": "Суб-меткi", + "search_type": "Тип пошуку", + "cameras": "Камери", + "labels": "Етикеткi", + "time_range": "Часовий діапазон", + "before": "Перед", + "after": "Пiсля", + "min_score": "Мінімальний бал", + "max_score": "Найвищий бал", + "min_speed": "Мінімальна швидкість", + "max_speed": "Максимальна швидкість", + "recognized_license_plate": "Розпізнаний номерний знак", + "has_clip": "Має клiп", + "has_snapshot": "Має знiмок", + "attributes": "Атрибути" + }, + "searchType": { + "thumbnail": "Мініатюра", + "description": "Опис" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Дата 'до' повинна бути пізнішою, ніж дата 'після'.", + "afterDatebeEarlierBefore": "Дата 'після' повинна бути раніше, ніж дата 'до'.", + "minScoreMustBeLessOrEqualMaxScore": "Значення 'min_score' має бути меншим або дорівнювати 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Значення 'max_score' має бути більшим або дорівнювати 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Значення 'min_speed' має бути меншим або дорівнювати 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Значення 'max_speed' має бути більшим або дорівнювати 'min_speed'." + } + } + }, + "similaritySearch": { + "title": "Пошук подібності", + "active": "Активнi пошук подібності", + "clear": "Очистити пошук подібності" + }, + "placeholder": { + "search": "Пошук…" + }, + "button": { + "save": "Зберігати пошук", + "delete": "Видалити збережений пошук", + "filterInformation": "Фільтрувати інформацію", + "clear": "Очистити пошук", + "filterActive": "Активни фільтри" + }, + "savedSearches": "Збережені пошуки", + "searchFor": "Пошук да {{inputValue}}", + "trackedObjectId": "Iдентифікатори об'єктів" +} diff --git a/web/public/locales/uk/views/settings.json b/web/public/locales/uk/views/settings.json new file mode 100644 index 0000000..a7816ac --- /dev/null +++ b/web/public/locales/uk/views/settings.json @@ -0,0 +1,1314 @@ +{ + "notification": { + "notificationSettings": { + "documentation": "Прочитати документацію", + "desc": "Frigate може надсилати push-сповіщення на ваш пристрій, коли він працює у браузері або встановлений як PWA.", + "title": "Налаштування сповіщень" + }, + "notificationUnavailable": { + "desc": "Веб-повідомлення вимагають безпечного контексту (https://…). Це обмеження браузера. Безпечний доступ до фрегатів для використання сповіщень.", + "documentation": "Прочитати документацію", + "title": "Сповіщення недоступні" + }, + "globalSettings": { + "desc": "Тимчасово призупинити сповіщення для певних камер на всіх зареєстрованих пристроях.", + "title": "Глобальні налаштування" + }, + "email": { + "title": "Електронна пошта", + "placeholder": "наприклад example@email.com", + "desc": "Потрібна дійсна електронна адреса, яка буде використана для сповіщення вас про будь-які проблеми з push-сервісом." + }, + "cameras": { + "title": "Камери", + "noCameras": "Немає доступних камер", + "desc": "Виберіть, для яких камер увімкнути сповіщення." + }, + "suspendTime": { + "5minutes": "Призупинити на 5 хвилин", + "30minutes": "Призупинити на 30 хвилин", + "1hour": "Призупинити на 1 годину", + "12hours": "Призупинити на 12 годин", + "24hours": "Призупинити на 24 години", + "untilRestart": "Призупинити до перезапуску", + "10minutes": "Призупинити на 10 хвилин", + "suspend": "Призупинити" + }, + "toast": { + "success": { + "registered": "Успішно зареєстровано для отримання сповіщень. Перед надсиланням будь-яких сповіщень (включно з тестовим сповіщенням) потрібен перезапуск Frigate.", + "settingSaved": "Налаштування сповіщень збережено." + }, + "error": { + "registerFailed": "Не вдалося зберегти реєстрацію сповіщення." + } + }, + "title": "Сповіщення", + "suspended": "Сповіщення призупинено {{time}}", + "deviceSpecific": "Налаштування, специфічні для пристрою", + "registerDevice": "Зареєструйте цей пристрій", + "unregisterDevice": "Скасувати реєстрацію цього пристрою", + "sendTestNotification": "Надіслати тестове сповіщення", + "active": "Активні сповіщення", + "cancelSuspension": "Скасувати призупинення", + "unsavedRegistrations": "Незбережені реєстрації сповіщень", + "unsavedChanges": "Незбережені зміни сповіщень" + }, + "camera": { + "streams": { + "title": "Потоки", + "desc": "Тимчасово вимкніть камеру до перезавантаження Frigate. Вимкнення камери повністю зупиняє обробку потоків цієї камери Frigate. Виявлення, запис і налагодження будуть недоступні.
    Примітка. Це не вимикає повторні потоки go2rtc." + }, + "reviewClassification": { + "readTheDocumentation": "Прочитати документацію", + "objectDetectionsTips": "Усі об’єкти {{detectionsLabels}}, які не класифіковані на {{cameraName}}, будуть відображатися як виявлені, незалежно від того, в якій зоні вони знаходяться.", + "zoneObjectDetectionsTips": { + "regardlessOfZoneObjectDetectionsTips": "Усі об’єкти {{detectionsLabels}}, які не класифіковані на {{cameraName}}, будуть відображатися як виявлені, незалежно від того, в якій зоні вони знаходяться.", + "text": "Усі об’єкти {{detectionsLabels}}, що не належать до категорії {{zone}} на {{cameraName}}, будуть відображатися як Виявлення.", + "notSelectDetections": "Усі об’єкти {{detectionsLabels}}, виявлені в {{zone}} на {{cameraName}}, які не віднесені до категорії «Сповіщення», будуть відображатися як Виявлення незалежно від того, в якій зоні вони знаходяться." + }, + "objectAlertsTips": "Усі об’єкти {{alertsLabels}} на {{cameraName}} будуть відображатися як сповіщення.", + "toast": { + "success": "Конфігурацію класифікації перегляду збережено. Перезапустіть Frigate, щоб застосувати зміни." + }, + "title": "Класифікація оглядів", + "desc": "Frigate класифікує елементи огляду як сповіщення та виявлення. За замовчуванням усі об’єкти люди та автомобілі вважаються сповіщеннями. Ви можете уточнити категоризацію елементів огляду, налаштувавши для них обов'язкові зони.", + "noDefinedZones": "Для цієї камери не визначено жодної зони.", + "selectDetectionsZones": "Виберіть зони для виявлення", + "limitDetections": "Обмеження виявлення певними зонами", + "zoneObjectAlertsTips": "Усі об’єкти {{alertsLabels}}, виявлені в {{zone}} на {{cameraName}}, будуть відображатися як сповіщення.", + "selectAlertsZones": "Виберіть зони для сповіщень", + "unsavedChanges": "Незбережені налаштування класифікації відгуків для {{camera}}" + }, + "review": { + "alerts": "Сповіщення ", + "detections": "Виявлення ", + "title": "Огляд", + "desc": "Тимчасово ввімкнути/вимкнути сповіщення та виявлення для цієї камери до перезавантаження Frigate. Якщо вимкнено, нові елементи огляду не створюватимуться. " + }, + "title": "Налаштування камери", + "object_descriptions": { + "title": "Генеративні описи об'єктів штучного інтелекту", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи об'єктів ШІ для цієї камери. Якщо вимкнено, згенеровані ШІ описи не запитуватимуться для об'єктів, що відстежуються на цій камері." + }, + "review_descriptions": { + "title": "Описи генеративного ШІ-огляду", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи огляду за допомогою штучного інтелекту для цієї камери. Якщо вимкнено, для елементів огляду на цій камері не запитуватимуться згенеровані штучним інтелектом описи." + }, + "addCamera": "Додати нову камеру", + "editCamera": "Редагувати камеру:", + "selectCamera": "Виберіть камеру", + "backToSettings": "Назад до налаштувань камери", + "cameraConfig": { + "add": "Додати камеру", + "edit": "Редагувати камеру", + "description": "Налаштуйте параметри камери, включаючи потокові входи та ролі.", + "name": "Назва камери", + "nameRequired": "Потрібно вказати назву камери", + "nameInvalid": "Назва камери повинна містити лише літери, цифри, символи підкреслення або дефіси", + "namePlaceholder": "наприклад, вхідні_двері", + "enabled": "Увімкнено", + "ffmpeg": { + "inputs": "Вхідні потоки", + "path": "Шлях потоку", + "pathRequired": "Шлях потоку обов'язковий", + "pathPlaceholder": "'rtsp://...", + "roles": "Ролі", + "rolesRequired": "Потрібна хоча б одна роль", + "rolesUnique": "Кожна роль (аудіо, виявлення, запис) може бути призначена лише одному потоку", + "addInput": "Додати вхідний потік", + "removeInput": "Вилучити вхідний потік", + "inputsRequired": "Потрібен принаймні один вхідний потік" + }, + "toast": { + "success": "Камеру {{cameraName}} успішно збережено" + }, + "nameLength": "Назва камери має містити менше 24 символів." + } + }, + "masksAndZones": { + "motionMasks": { + "polygonAreaTooLarge": { + "documentation": "Прочитати документацію", + "tips": "Маски руху не запобігають виявленню об'єктів. Натомість слід використовувати обов'язкову зону.", + "title": "Маска руху покриває {{polygonArea}}% кадру камери. Великі маски руху не рекомендуються." + }, + "context": { + "documentation": "Прочитати документацію", + "title": "Маски руху використовуються для запобігання виявлення небажаних типів руху (наприклад: гілки дерева, часові мітки камери). Слід використовувати маски руху дуже економно, надмірне маскування ускладнить відстеження об'єктів." + }, + "clickDrawPolygon": "Клацніть, щоб намалювати багатокутник на зображенні.", + "add": "Нова маска руху", + "edit": "Редагувати маску руху", + "toast": { + "success": { + "title": "{{polygonName}} збережено.", + "noName": "Маску руху збережено." + } + }, + "label": "Маска руху", + "documentTitle": "Редагувати маску руху – Фрегат", + "desc": { + "title": "Маски руху використовуються для запобігання спрацьовуванню виявлення небажаних типів руху. Надмірне маскування ускладнить відстеження об'єктів.", + "documentation": "Документація" + }, + "point_one": "{{count}} бал", + "point_few": "{{count}} бали", + "point_many": "{{count}} балів" + }, + "zones": { + "label": "Зони", + "name": { + "inputPlaceHolder": "Введіть назву…", + "title": "Ім'я", + "tips": "Назва має містити щонайменше 2 символи, принаймні одну літеру та не повинна бути назвою камери чи іншої зони на цій камері." + }, + "desc": { + "title": "Зони дозволяють визначити певну область кадру, щоб ви могли визначити, чи знаходиться об'єкт у певній області.", + "documentation": "Документація" + }, + "allObjects": "Усі об'єкти", + "speedEstimation": { + "title": "Оцінка швидкості", + "desc": "Увімкнути оцінку швидкості для об'єктів у цій зоні. Зона повинна мати рівно 4 точки.", + "docs": "Прочитайте документацію", + "lineADistance": "Відстань лінії A ({{unit}})", + "lineBDistance": "Відстань лінії B ({{unit}})", + "lineCDistance": "Відстань лінії C ({{unit}})", + "lineDDistance": "Відстань лінії D ({{unit}})" + }, + "speedThreshold": { + "title": "Поріг швидкості ({{unit}})", + "desc": "Визначає мінімальну швидкість для об'єктів, які слід враховувати в цій зоні.", + "toast": { + "error": { + "pointLengthError": "Оцінку швидкості для цієї зони вимкнено. Зони з оцінкою швидкості повинні мати рівно 4 бали.", + "loiteringTimeError": "Зони з часом байдикування більше 0 не слід використовувати з оцінкою швидкості." + } + } + }, + "documentTitle": "Зона редагування – Фрегат", + "add": "Додати зону", + "edit": "Редагувати зону", + "point_one": "{{count}} бал", + "point_few": "{{count}} бали", + "point_many": "{{count}} балів", + "clickDrawPolygon": "Клацніть, щоб намалювати багатокутник на зображенні.", + "inertia": { + "title": "Інерція", + "desc": "Визначає, скільки кадрів об'єкт має перебувати в зоні, перш ніж його буде враховано в ній. За замовчуванням: 3" + }, + "loiteringTime": { + "title": "Час тиняння", + "desc": "Встановлює мінімальний час у секундах, протягом якого об'єкт має перебувати в зоні для активації. За замовчуванням: 0" + }, + "objects": { + "title": "Об'єкти", + "desc": "Список об'єктів, що належать до цієї зони." + }, + "toast": { + "success": "Зону ({{zoneName}}) збережено." + } + }, + "objectMasks": { + "desc": { + "title": "Маски фільтрів об'єктів використовуються для фільтрації хибнопозитивних результатів для заданого типу об'єкта на основі його розташування.", + "documentation": "Документація" + }, + "documentTitle": "Редагувати маску об'єкта - Фрегат", + "add": "Додати маску об'єкта", + "edit": "Редагувати маску об'єкта", + "context": "Маски фільтрів об'єктів використовуються для фільтрації хибнопозитивних результатів для заданого типу об'єкта на основі його розташування.", + "point_one": "{{count}} бал", + "point_few": "{{count}} бали", + "point_many": "{{count}} балів", + "clickDrawPolygon": "Клацніть, щоб намалювати багатокутник на зображенні.", + "objects": { + "title": "Об'єкти", + "desc": "Тип об'єкта, що застосовується до цієї маски об'єкта.", + "allObjectTypes": "Усі типи об'єктів" + }, + "toast": { + "success": { + "title": "{{polygonName}} збережено.", + "noName": "Маску об'єкта збережено." + } + }, + "label": "Маски об'єктів" + }, + "restart_required": "Потрібно перезавантажити (маски/зони змінено)", + "toast": { + "success": { + "copyCoordinates": "Координати для {{polyName}} скопійовано в буфер обміну." + }, + "error": { + "copyCoordinatesFailed": "Не вдалося скопіювати координати в буфер обміну." + } + }, + "form": { + "zoneName": { + "error": { + "alreadyExists": "Для цієї камери вже існує зона з такою назвою.", + "mustNotContainPeriod": "Назва зони не повинна містити крапок.", + "mustNotBeSameWithCamera": "Назва зони не повинна збігатися з назвою камери.", + "mustBeAtLeastTwoCharacters": "Назва зони має містити щонайменше 2 символи.", + "hasIllegalCharacter": "Назва зони містить недопустимі символи.", + "mustHaveAtLeastOneLetter": "Назва зони повинна містити щонайменше одну літеру." + } + }, + "polygonDrawing": { + "delete": { + "desc": "Ви впевнені, що хочете видалити {{type}} {{name}}?", + "title": "Підтвердити видалення", + "success": "{{name}} було видалено." + }, + "removeLastPoint": "Видалити останню точку", + "reset": { + "label": "Очистити всі бали" + }, + "snapPoints": { + "true": "Точки прив'язки", + "false": "Не зачіпайте точки" + }, + "error": { + "mustBeFinished": "Малювання полігону має бути завершене перед збереженням." + } + }, + "distance": { + "error": { + "text": "Відстань має бути більшою або рівною 0,1.", + "mustBeFilled": "Для використання оцінки швидкості необхідно заповнити всі поля відстані." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Інерція повинна бути вище 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Час байдикування має бути більшим або рівним 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Поріг швидкості має бути більшим або рівним 0,1." + } + } + }, + "filter": { + "all": "Усі маски та зони" + }, + "motionMaskLabel": "Маска руху {{number}}", + "objectMaskLabel": "Маска об'єкта {{number}}" + }, + "debug": { + "zones": { + "title": "Зони", + "desc": "Показати контур будь-яких визначених зон" + }, + "objectShapeFilterDrawing": { + "document": "Прочитати документацію ", + "tips": "Увімкніть цю опцію, щоб намалювати прямокутник на зображенні з камери, щоб показати його площу та співвідношення. Ці значення потім можна використовувати для встановлення параметрів фільтра форми об'єкта у вашій конфігурації.", + "title": "Фільтр форми об'єкта, малюнок", + "desc": "Намалюйте прямокутник на зображенні, щоб переглянути деталі площі та співвідношення", + "score": "Рахунок", + "area": "Площа", + "ratio": "Співвідношення" + }, + "regions": { + "tips": "

    Регіональні рамки


    Яскраво-зелені рамки будуть накладені на області інтересу в кадрі, які надсилаються на детектор об'єктів.

    ", + "desc": "Показати рамку області інтересу, що надсилається на детектор об'єктів", + "title": "Регіони" + }, + "boundingBoxes": { + "colors": { + "info": "
  • Під час запуску кожній мітці об’єкта будуть призначені різні кольори
  • Темно-синя тонка лінія вказує на те, що об’єкт не виявлено в цей момент часу
  • Тонка сіра лінія вказує на те, що об’єкт виявлено як нерухомий
  • Товста лінія вказує на те, що об’єкт є об’єктом автоматичного відстеження (якщо його ввімкнено)
  • ", + "label": "Кольори рамки обмежування об'єкта" + }, + "title": "Обмежувальні рамки", + "desc": "Показувати обмежувальні рамки навколо відстежуваних об'єктів" + }, + "title": "Налагодження", + "detectorDesc": "Фрегат використовує ваші детектори ({{detectors}}) для виявлення об'єктів у відеопотоці вашої камери.", + "desc": "У режимі налагодження відображається відстежувані об’єкти та їхня статистика в режимі реального часу. Список об’єктів показує зведену інформацію про виявлені об’єкти із затримкою в часі.", + "debugging": "Налагоджуємо", + "timestamp": { + "title": "Позначка часу", + "desc": "Накладання позначки часу на зображення" + }, + "mask": { + "title": "Маски руху", + "desc": "Показати полігони маски руху" + }, + "motion": { + "title": "Коробки руху", + "desc": "Показувати рамки навколо областей, де виявлено рух", + "tips": "

    Поля руху


    Червоні поля будуть накладені на області кадру, де наразі виявляється рух

    " + }, + "objectList": "Список об'єктів", + "noObjects": "Без об'єктів", + "paths": { + "title": "Шляхи", + "desc": "Показувати важливі точки шляху відстежуваного об'єкта", + "tips": "

    Шляхи


    Лінії та кола позначатимуть важливі точки, які відстежуваний об'єкт переміщував протягом свого життєвого циклу.

    " + }, + "audio": { + "title": "Аудіо", + "noAudioDetections": "Немає виявлення звуку", + "score": "рахунок", + "currentRMS": "Поточне середньоквадратичне значення", + "currentdbFS": "Поточний dbFS" + }, + "openCameraWebUI": "Відкрийте веб-інтерфейс {{camera}}" + }, + "classification": { + "licensePlateRecognition": { + "readTheDocumentation": "Прочитати документацію", + "title": "Розпізнавання номерних знаків", + "desc": "Фрегат може розпізнавати номерні знаки транспортних засобів та автоматично додавати виявлені символи до поля recognized_license_plate або відоме ім'я як sub_label до об'єктів типу автомобіль. Поширеним випадком використання може бути зчитування номерних знаків автомобілів, що заїжджають на під'їзну доріжку, або автомобілів, що проїжджають повз вулицю." + }, + "faceRecognition": { + "readTheDocumentation": "Прочитати документацію", + "title": "Розпізнавання обличчя", + "desc": "Розпізнавання обличчя дозволяє присвоювати людям імена, і коли їхнє обличчя розпізнається, Frigate призначає ім'я людини як підмітку. Ця інформація міститься в інтерфейсі користувача, фільтрах, а також у сповіщеннях.", + "modelSize": { + "label": "Розмір моделі", + "desc": "Розмір моделі, що використовується для розпізнавання обличчя.", + "small": { + "title": "маленький", + "desc": "Використання small використовує модель вбудовування облич FaceNet, яка ефективно працює на більшості процесорів." + }, + "large": { + "title": "великий", + "desc": "Використання параметра large використовує модель вбудовування облич ArcFace та автоматично запускатиметься на графічному процесорі, якщо це можливо." + } + } + }, + "semanticSearch": { + "reindexNow": { + "alreadyInProgress": "Переіндексація вже триває.", + "error": "Не вдалося розпочати переіндексацію: {{errorMessage}}", + "confirmDesc": "Ви впевнені, що хочете переіндексувати всі вбудовані відстежувані об'єкти? Цей процес працюватиме у фоновому режимі, але може максимально навантажити ваш процесор і зайняти чимало часу. Ви можете спостерігати за прогресом на Ex.", + "confirmButton": "Переіндексувати", + "desc": "Переіндексація призведе до повторного створення вбудованих елементів для всіх відстежуваних об'єктів. Цей процес працює у фоновому режимі та може максимально навантажити ваш процесор і зайняти достатню кількість часу залежно від кількості відстежуваних об'єктів.", + "success": "Переіндексацію успішно розпочато.", + "label": "Переіндексувати зараз", + "confirmTitle": "Підтвердити переіндексацію" + }, + "title": "Семантичний пошук", + "desc": "Семантичний пошук у Frigate дозволяє знаходити відстежувані об'єкти у ваших оглядах, використовуючи або саме зображення, або текстовий опис, визначений користувачем, або автоматично згенерований.", + "readTheDocumentation": "Прочитайте документацію", + "modelSize": { + "label": "Розмір моделі", + "desc": "Розмір моделі, що використовується для вбудовування семантичного пошуку.", + "small": { + "title": "маленький", + "desc": "Використання малих працівників квантована версія моделі, яка використовує менше оперативної пам'яті та працює швидше на процесорі з дуже незначною різницею в якості вбудовування." + }, + "large": { + "title": "великий", + "desc": "Використання large використовує повну модель Jina та автоматично запускатиметься на графічному процесорі, якщо це можливо." + } + } + }, + "restart_required": "Потрібно перезавантажити (налаштування класифікації змінено)", + "toast": { + "success": "Налаштування класифікації збережено. Перезапустіть Frigate, щоб застосувати зміни.", + "error": "Не вдалося зберегти зміни конфігурації: {{errorMessage}}" + }, + "title": "Налаштування класифікації", + "birdClassification": { + "title": "Класифікація птахів", + "desc": "Класифікація птахів ідентифікує відомих птахів за допомогою квантованої моделі Tensorflow. Коли відомого птаха розпізнають, його загальна назва буде додана як sub_label. Ця інформація міститься в інтерфейсі користувача, фільтрах, а також у сповіщеннях." + }, + "unsavedChanges": "Незбережені зміни налаштувань класифікації" + }, + "frigatePlus": { + "modelInfo": { + "loading": "Завантаження інформації про модель…", + "loadingAvailableModels": "Завантаження доступних моделей…", + "plusModelType": { + "baseModel": "Базова модель", + "userModel": "Точно налаштований" + }, + "supportedDetectors": "Підтримувані детектори", + "error": "Не вдалося завантажити інформацію про модель", + "availableModels": "Доступні моделі", + "trainDate": "Дата тренування", + "baseModel": "Базова модель", + "modelSelect": "Тут можна вибрати доступні моделі на Frigate+. Зверніть увагу, що можна вибрати лише моделі, сумісні з вашою поточною конфігурацією детектора.", + "title": "Інформація про модель", + "modelType": "Тип моделі", + "cameras": "Камери" + }, + "snapshotConfig": { + "title": "Конфігурація знімків", + "desc": "Для надсилання до Frigate+ потрібно ввімкнути як знімки, так і знімки clean_copy у вашій конфігурації.", + "documentation": "Прочитайте документацію", + "table": { + "camera": "Камера", + "snapshots": "Знімки", + "cleanCopySnapshots": "clean_copy Знімки" + }, + "cleanCopyWarning": "На деяких камерах знімки екрана вимкнені" + }, + "apiKey": { + "desc": "Ключ API Frigate+ забезпечує інтеграцію з сервісом Frigate+.", + "notValidated": "Ключ API Frigate+ не виявлено або не перевірено", + "title": "Ключ API Фрегат+", + "validated": "Ключ API Frigate+ виявлено та перевірено", + "plusLink": "Дізнайтеся більше про Фрегат+" + }, + "restart_required": "Потрібно перезавантаження (модель Frigate+ змінена)", + "toast": { + "success": "Налаштування Frigate+ збережено. Перезапустіть Frigate, щоб застосувати зміни.", + "error": "Не вдалося зберегти зміни конфігурації: {{errorMessage}}" + }, + "title": "Налаштування Фрегат+", + "unsavedChanges": "Незбережені зміни налаштувань Frigate+" + }, + "general": { + "calendar": { + "title": "Календар", + "firstWeekday": { + "label": "Перший день тижня", + "desc": "День, з якого починаються тижні календаря оглядів.", + "sunday": "Неділя", + "monday": "Понеділок" + } + }, + "title": "Налаштування інтерфейсу користувача", + "liveDashboard": { + "title": "Панель керування в прямому ефірі", + "automaticLiveView": { + "label": "Автоматичний перегляд у реальному часі", + "desc": "Автоматично перемикатися на режим реального часу з камери, коли виявляється активність. Якщо вимкнути цю опцію, статичні зображення з камери на панелі керування реальним часом оновлюватимуться лише раз на хвилину." + }, + "playAlertVideos": { + "label": "Відтворити відео зі сповіщеннями", + "desc": "За замовчуванням останні сповіщення на панелі керування Live відтворюються як невеликі відеозаписи, що циклічно відтворюються. Вимкніть цю опцію, щоб відображати лише статичне зображення останніх сповіщень на цьому пристрої/у браузері." + }, + "displayCameraNames": { + "label": "Завжди показувати назви камер", + "desc": "Завжди відображати назви камер у чіпі на панелі керування режимом живого перегляду з кількох камер." + }, + "liveFallbackTimeout": { + "label": "Час очікування резервного програвача в реальному часі", + "desc": "Коли високоякісна пряма трансляція з камери недоступна, повернутися до режиму низької пропускної здатності через певну кількість секунд. За замовчуванням: 3." + } + }, + "storedLayouts": { + "title": "Збережені макети", + "clearAll": "Очистити всі макети", + "desc": "Розташування камер у групі камер можна перетягувати/змінювати розмір. Позиції зберігаються в локальному сховищі вашого браузера." + }, + "cameraGroupStreaming": { + "title": "Налаштування потокової передачі групи камер", + "desc": "Налаштування потокової передачі для кожної групи камер зберігаються в локальному сховищі вашого браузера.", + "clearAll": "Очистити всі налаштування потокового передавання" + }, + "recordingsViewer": { + "title": "Переглядач записів", + "defaultPlaybackRate": { + "label": "Стандартна швидкість відтворення", + "desc": "Швидкість відтворення записів за замовчуванням." + } + }, + "toast": { + "success": { + "clearStoredLayout": "Очищено збережений макет для {{cameraName}}", + "clearStreamingSettings": "Очищено налаштування потокової передачі для всіх груп камер." + }, + "error": { + "clearStoredLayoutFailed": "Не вдалося очистити збережений макет: {{errorMessage}}", + "clearStreamingSettingsFailed": "Не вдалося очистити налаштування потокового передавання: {{errorMessage}}" + } + } + }, + "motionDetectionTuner": { + "improveContrast": { + "title": "Покращення контрастності", + "desc": "Покращення контрастності для темніших сцен. За замовчуванням: УВІМК" + }, + "desc": { + "documentation": "Прочитайте посібник з налаштування руху", + "title": "Фрегат використовує виявлення руху як першочергову перевірку, щоб побачити, чи відбувається щось у кадрі, що варто перевірити за допомогою виявлення об'єктів." + }, + "title": "Тюнер виявлення руху", + "Threshold": { + "title": "Поріг", + "desc": "Порогове значення визначає, наскільки велика зміна яскравості пікселя має вважатися рухом. За замовчуванням: 30" + }, + "toast": { + "success": "Налаштування руху збережено." + }, + "contourArea": { + "title": "Площа контуру", + "desc": "Значення площі контуру використовується для визначення того, які групи змінених пікселів кваліфікуються як рух. За замовчуванням: 10" + }, + "unsavedChanges": "Незбережені зміни в налаштуванні руху ({{camera}})" + }, + "documentTitle": { + "object": "Налагодження – Фрегат", + "notifications": "Налаштування сповіщень – Фрегат", + "default": "Налаштування - Фрегат", + "authentication": "Налаштування автентифікації – Фрегат", + "camera": "Налаштування камери – Фрегат", + "classification": "Налаштування класифікації – Фрегат", + "masksAndZones": "Редактор масок та зон – Фрегат", + "motionTuner": "Тюнер руху - Фрегат", + "general": "Основна Статус – Frigate", + "frigatePlus": "Налаштування Frigate+ – Frigate", + "enrichments": "Налаштуваннях збагачення – Frigate", + "cameraManagement": "Керування камерами - Frigate", + "cameraReview": "Налаштування перегляду камери - Frigate" + }, + "menu": { + "ui": "Інтерфейс користувача", + "classification": "Класифікація", + "cameras": "Налаштування камери", + "users": "Користувачі", + "masksAndZones": "Маски / Зони", + "motionTuner": "Тюнер руху", + "debug": "Налагодження", + "notifications": "Сповіщення", + "frigateplus": "Frigate+", + "enrichments": "Збагаченням", + "triggers": "Тригери", + "roles": "Ролі", + "cameraManagement": "Управління", + "cameraReview": "Огляду", + "profiles": "Профілі" + }, + "dialog": { + "unsavedChanges": { + "desc": "Ви хочете зберегти зміни, перш ніж продовжити?", + "title": "У вас є незбережені зміни." + } + }, + "cameraSetting": { + "camera": "Камера", + "noCamera": "Без камери" + }, + "users": { + "management": { + "title": "Керування користувачами", + "desc": "Керувати обліковими записами користувачів цього екземпляра Frigate." + }, + "addUser": "Додати користувача", + "updatePassword": "Скинути пароль", + "toast": { + "success": { + "deleteUser": "Користувач {{user}} успішно видалений", + "roleUpdated": "Роль оновлено для {{user}}", + "updatePassword": "Пароль успішно оновлено.", + "createUser": "Користувач {{user}} успішно створено" + }, + "error": { + "setPasswordFailed": "Не вдалося зберегти пароль: {{errorMessage}}", + "createUserFailed": "Не вдалося створити користувача: {{errorMessage}}", + "deleteUserFailed": "Не вдалося видалити користувача: {{errorMessage}}", + "roleUpdateFailed": "Не вдалося оновити роль: {{errorMessage}}" + } + }, + "table": { + "password": "Скинути пароль", + "deleteUser": "Видалити користувача", + "username": "Ім'я користувача", + "actions": "Дії", + "noUsers": "Користувачів не знайдено.", + "role": "Роль", + "changeRole": "Змінити роль користувача" + }, + "dialog": { + "form": { + "user": { + "title": "Ім'я користувача", + "desc": "Дозволено використовувати лише літери, цифри, крапки та символи підкреслення.", + "placeholder": "Введіть ім'я користувача" + }, + "password": { + "strength": { + "medium": "Середній", + "strong": "Сильний", + "veryStrong": "Дуже сильний", + "title": "Надійність пароля: ", + "weak": "Слабкий" + }, + "match": "Паролі збігаються", + "title": "Пароль", + "notMatch": "Паролі не збігається", + "placeholder": "Введіть пароль", + "confirm": { + "title": "Підтвердьте пароль", + "placeholder": "Підтвердьте пароль" + }, + "show": "Показати пароль", + "hide": "Приховати пароль", + "requirements": { + "title": "Вимоги до пароля:", + "length": "Принаймні 12 символів", + "uppercase": "Принаймні одна велика літера", + "digit": "Принаймні одна цифра", + "special": "Принаймні один спеціальний символ (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "confirm": { + "placeholder": "Введіть новий пароль ще раз" + }, + "title": "Новий пароль", + "placeholder": "Введіть новий пароль" + }, + "usernameIsRequired": "Потрібне ім'я користувача", + "passwordIsRequired": "Потрібен пароль", + "currentPassword": { + "title": "Поточний пароль", + "placeholder": "Введіть свій поточний пароль" + } + }, + "changeRole": { + "roleInfo": { + "admin": "Адміністратор", + "intro": "Виберіть відповідну роль для цього користувача:", + "adminDesc": "Повний доступ до всіх функцій.", + "viewer": "Глядач", + "viewerDesc": "Обмежено лише активними інформаційними панелями, функціями «Огляд», «Дослідження» та «Експорт».", + "customDesc": "Особлива роль з доступом до певної камери." + }, + "title": "Змінити роль користувача", + "desc": "Оновити дозволи для {{username}}", + "select": "Виберіть роль" + }, + "createUser": { + "title": "Створити нового користувача", + "desc": "Додайте новий обліковий запис користувача та вкажіть роль для доступу до областей інтерфейсу Frigate.", + "usernameOnlyInclude": "Ім'я користувача може містити лише літери, цифри, . або _", + "confirmPassword": "Будь ласка, підтвердіть свій пароль" + }, + "deleteUser": { + "title": "Видалити користувача", + "desc": "Цю дію не можна скасувати. Це призведе до остаточного видалення облікового запису користувача та всіх пов’язаних з ним даних.", + "warn": "Ви впевнені, що хочете видалити {{username}}?" + }, + "passwordSetting": { + "updatePassword": "Оновити пароль для {{username}}", + "setPassword": "Встановити пароль", + "desc": "Створіть надійний пароль для захисту цього облікового запису.", + "cannotBeEmpty": "Пароль не може бути порожнім", + "doNotMatch": "Паролі не збігаються", + "currentPasswordRequired": "Потрібно ввести поточний пароль", + "incorrectCurrentPassword": "Поточний пароль неправильний", + "passwordVerificationFailed": "Не вдалося перевірити пароль", + "multiDeviceWarning": "На будь-яких інших пристроях, на яких ви ввійшли в систему, потрібно буде повторно ввійти протягом {{refresh_time}}.", + "multiDeviceAdmin": "Ви також можете змусити всіх користувачів негайно повторно автентифікуватися, змінивши свій JWT-секрет." + } + }, + "title": "Користувачі" + }, + "enrichments": { + "unsavedChanges": "Зміни в налаштуваннях незбережених збагачень", + "semanticSearch": { + "title": "Семантичний пошук", + "desc": "Семантичний пошук у Frigate дозволяє знаходити відстежувані об'єкти в елементах огляду, використовуючи або саме зображення, або текстовий опис, визначений користувачем, або автоматично згенерований опис.", + "readTheDocumentation": "Прочитайте документації", + "reindexNow": { + "label": "Переіндексувати зараз", + "confirmTitle": "Підтвердити переіндексацію", + "error": "Не вдалося розпочати переіндексацію: {{errorMessage}}", + "confirmDesc": "Ви впевнені, що хочете переіндексувати всі вбудовані відстежувані об'єкти? Цей процес працюватиме у фоновому режимі, але може максимально навантажити ваш процесор і зайняти чимало часу. Ви можете спостерігати за прогресом на Ex.", + "confirmButton": "Переіндексувати", + "alreadyInProgress": "Переіндексація вже триває.", + "success": "Переіндексацію успішно розпочато.", + "desc": "Переіндексація відновить вбудовування для всіх відстежуваних об'єктів. Цей процес працює у фоновому режимі та може максимально навантажувати ваш процесор і займати достатню кількість часу залежно від кількості відстежуваних об'єктів." + }, + "modelSize": { + "label": "Розмір моделі", + "desc": "Розмір моделі, що використовується для вбудовування семантичного пошуку.", + "small": { + "title": "маленький", + "desc": "Використання small застосовує квантовану версію моделі, яка використовує менше оперативної пам'яті та працює швидше на процесорі з дуже незначною різницею в якості вбудовування." + }, + "large": { + "title": "великий", + "desc": "Використання large використовує повну модель Jina та автоматично запускатиметься на графічному процесорі, якщо це можливо." + } + } + }, + "faceRecognition": { + "title": "Розпізнавання обличчя", + "readTheDocumentation": "Прочитайте документація", + "modelSize": { + "label": "Розмір моделі", + "desc": "Розмір моделі, що використовується для розпізнавання обличчя.", + "small": { + "title": "маленький", + "desc": "Використання small використовує модель вбудовування облич FaceNet, яка ефективно працює на більшості процесорів." + }, + "large": { + "title": "великий", + "desc": "Використання параметра large використовує модель вбудовування облич ArcFace та автоматично запускатиметься на графічному процесорі, якщо це можливо." + } + }, + "desc": "Розпізнавання облич дозволяє присвоювати людям імена, і коли обличчя розпізнається, Frigate додає ім'я людини в якості підмітки. Ця інформація відображається в інтерфейсі, фільтрах, а також у сповіщеннях." + }, + "licensePlateRecognition": { + "title": "Розпізнавання номерних знаків", + "readTheDocumentation": "Прочитайте документації", + "desc": "Frigate може розпізнавати номерні знаки на автомобілях і автоматично додавати виявлені символи до поля розпізнаний_номер_автомобіля або відоме ім'я як підмітку до об'єктів, що мають тип \"автомобіль\". Поширеним випадком використання може бути зчитування номерних знаків автомобілів, що заїжджають на під'їзд, або автомобілів, що проїжджають вулицею." + }, + "restart_required": "Потрібно перезавантажити (налаштування збагачення змінено)", + "toast": { + "success": "Налаштування збагачення збережено. Перезапустіть Frigate, щоб застосувати зміни.", + "error": "Не вдалося зберегти зміни конфігурації: {{errorMessage}}" + }, + "birdClassification": { + "desc": "Класифікація птахів ідентифікує відомих птахів за допомогою квантованої моделі тензорного потоку. Коли відомого птаха розпізнано, його загальну назву буде додано як підмітку. Ця інформація відображається в інтерфейсі, фільтрах, а також у сповіщеннях.", + "title": "Класифікація птахів" + }, + "title": "Налаштуваннях Збагаченням" + }, + "triggers": { + "documentTitle": "Тригери", + "management": { + "title": "Тригери", + "desc": "Керуйте тригерами для {{camera}}. Використовуйте тип мініатюри для спрацьовування на схожих мініатюрах до вибраного об’єкта відстеження, а тип опису – для спрацьовування на схожих описах до вказаного вами тексту." + }, + "addTrigger": "Додати Тригер", + "table": { + "name": "Ім'я", + "type": "Тип", + "content": "Зміст", + "threshold": "Поріг", + "actions": "Дії", + "noTriggers": "Для цієї камери не налаштовано жодних тригерів.", + "edit": "Редагувати", + "deleteTrigger": "Видалити тригер", + "lastTriggered": "Остання активація" + }, + "type": { + "thumbnail": "Мініатюра", + "description": "Опис" + }, + "actions": { + "alert": "Позначити як сповіщення", + "notification": "Надіслати сповіщення", + "sub_label": "Додати підмітку", + "attribute": "Додати атрибут" + }, + "dialog": { + "createTrigger": { + "title": "Створити тригер", + "desc": "Створіть тригер для камери {{camera}}" + }, + "editTrigger": { + "title": "Редагувати тригер", + "desc": "Редагувати налаштування для тригера на камері {{camera}}" + }, + "deleteTrigger": { + "title": "Видалити тригер", + "desc": "Ви впевнені, що хочете видалити тригер {{triggerName}}? Цю дію не можна скасувати." + }, + "form": { + "name": { + "title": "Ім'я", + "placeholder": "Назвіть цей тригер", + "error": { + "minLength": "Поле має містити щонайменше 2 символи.", + "invalidCharacters": "Поле може містити лише літери, цифри, символи підкреслення та дефіси.", + "alreadyExists": "Тригер із такою назвою вже існує для цієї камери." + }, + "description": "Введіть унікальну назву або опис, щоб ідентифікувати цей тригер" + }, + "enabled": { + "description": "Увімкнути або вимкнути цей тригер" + }, + "type": { + "title": "Тип", + "placeholder": "Виберіть тип тригера", + "description": "Спрацьовує, коли виявляється схожий опис відстежуваного об'єкта", + "thumbnail": "Спрацьовує, коли виявляється мініатюра схожого відстежуваного об'єкта" + }, + "content": { + "title": "Зміст", + "imagePlaceholder": "Виберіть мініатюру", + "textPlaceholder": "Введіть текстовий вміст", + "imageDesc": "Відображаються лише 100 останніх мініатюр. Якщо ви не можете знайти потрібну мініатюру, перегляньте попередні об’єкти в розділі «Огляд» і налаштуйте тригер у меню.", + "textDesc": "Введіть текст, щоб запустити цю дію, коли буде виявлено схожий опис відстежуваного об’єкта.", + "error": { + "required": "Контент обов'язковий." + } + }, + "threshold": { + "title": "Поріг", + "error": { + "min": "Поріг має бути щонайменше 0", + "max": "Поріг має бути не більше 1" + }, + "desc": "Встановіть поріг подібності для цього тригера. Вищий поріг означає, що для спрацьовування тригера потрібна ближча відповідність." + }, + "actions": { + "title": "Дії", + "desc": "За замовчуванням Frigate надсилає повідомлення MQTT для всіх тригерів. Підмітки додають назву тригера до мітки об'єкта. Атрибути – це метадані, які можна шукати, що зберігаються окремо в метаданих відстежуваного об'єкта.", + "error": { + "min": "Потрібно вибрати принаймні одну дію." + } + }, + "friendly_name": { + "title": "Зрозуміле ім'я", + "placeholder": "Назвіть або опишіть цей тригер", + "description": "Зрозуміла назва або описовий текст (необов'язково) для цього тригера." + } + } + }, + "toast": { + "success": { + "createTrigger": "Тригер {{name}} успішно створено.", + "updateTrigger": "Тригер {{name}} успішно оновлено.", + "deleteTrigger": "Тригер {{name}} успішно видалено." + }, + "error": { + "createTriggerFailed": "Не вдалося створити тригер: {{errorMessage}}", + "updateTriggerFailed": "Не вдалося оновити тригер: {{errorMessage}}", + "deleteTriggerFailed": "Не вдалося видалити тригер: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Семантичний пошук вимкнено", + "desc": "Для використання тригерів необхідно ввімкнути семантичний пошук." + }, + "wizard": { + "title": "Створити тригер", + "step1": { + "description": "Налаштуйте основні параметри для вашого тригера." + }, + "step2": { + "description": "Налаштуйте контент, який запускатиме цю дію." + }, + "step3": { + "description": "Налаштуйте поріг та дії для цього тригера." + }, + "steps": { + "nameAndType": "Ім'я та тип", + "configureData": "Налаштувати дані", + "thresholdAndActions": "Поріг та дії" + } + } + }, + "roles": { + "addRole": "Додати роль", + "table": { + "role": "Роль", + "cameras": "Камери", + "actions": "Дії", + "noRoles": "Не знайдено користувацьких ролей.", + "editCameras": "Редагувати камери", + "deleteRole": "Видалити роль" + }, + "toast": { + "success": { + "createRole": "Роль {{role}} успішно створена", + "updateCameras": "Камери оновлено для ролі {{role}}", + "deleteRole": "Роль {{role}} успішно видалено", + "userRolesUpdated_one": "{{count}} користувача, призначену цій ролі, оновлено до «глядача», який має доступ до всіх камер.", + "userRolesUpdated_few": "{{count}} Користувачі, яким призначено цю роль, оновлено до ролі «глядача», що має доступ до всіх камер.", + "userRolesUpdated_many": "{{count}} Користувачів, яким призначено цю роль, оновлено до ролі «глядача», що має доступ до всіх камер." + }, + "error": { + "createRoleFailed": "Не вдалося створити роль: {{errorMessage}}", + "updateCamerasFailed": "Не вдалося оновити камери: {{errorMessage}}", + "deleteRoleFailed": "Не вдалося видалити роль: {{errorMessage}}", + "userUpdateFailed": "Не вдалося оновити ролі користувачів: {{errorMessage}}" + } + }, + "management": { + "title": "Керування ролями глядача", + "desc": "Керуйте ролями глядачів та їхніми дозволами на доступ до камери для цього екземпляра Frigate." + }, + "dialog": { + "createRole": { + "title": "Створити нову роль", + "desc": "Додайте нову роль і вкажіть дозволи доступу до камери." + }, + "editCameras": { + "title": "Редагувати рольові камери", + "desc": "Оновіть доступ до камери для цієї ролі {{role}}." + }, + "deleteRole": { + "title": "Видалити роль", + "desc": "Цю дію не можна скасувати. Це призведе до остаточного видалення ролі та призначення всім користувачам із цією роллю ролі «глядач», що надасть глядачеві доступ до всіх камер.", + "warn": "Ви впевнені, що хочете видалити {{role}}?", + "deleting": "Видалення..." + }, + "form": { + "role": { + "title": "Назва ролі", + "placeholder": "Введіть назву ролі", + "desc": "Дозволено використовувати лише літери, цифри, крапки та символи підкреслення.", + "roleIsRequired": "Потрібно вказати назву ролі", + "roleOnlyInclude": "Назва ролі може містити лише літери, цифри, символи *.* або *.*", + "roleExists": "Роль із такою назвою вже існує." + }, + "cameras": { + "title": "Камери", + "desc": "Виберіть камери, до яких ця роль має доступ. Потрібна принаймні одна камера.", + "required": "Потрібно вибрати принаймні одну камеру." + } + } + } + }, + "cameraWizard": { + "title": "Додати камеру", + "description": "Виконайте наведені нижче кроки, щоб додати нову камеру до вашої установки Frigate.", + "steps": { + "nameAndConnection": "Ім'я та з'єднання", + "streamConfiguration": "Конфігурація потоку", + "validationAndTesting": "Валідація та тестування", + "probeOrSnapshot": "Зонд або знімок" + }, + "save": { + "success": "Нову камеру успішно збережено {{cameraName}}.", + "failure": "Помилка збереження {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Роздільна здатність", + "video": "Відео", + "audio": "Аудіо", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Будь ласка, надайте дійсну URL-адресу потоку", + "testFailed": "Тест потоку не вдався: {{error}}" + }, + "step1": { + "description": "Введіть дані вашої камери та виберіть тестування камери або виберіть бренд вручну.", + "cameraName": "Назва камери", + "cameraNamePlaceholder": "наприклад, передні_двері або огляд заднього двору", + "host": "Хост/IP-адреса", + "port": "Порт", + "username": "Ім'я користувача", + "usernamePlaceholder": "Необов'язково", + "password": "Пароль", + "passwordPlaceholder": "Необов'язково", + "selectTransport": "Виберіть транспортний протокол", + "cameraBrand": "Бренд камери", + "selectBrand": "Виберіть марку камери для шаблону URL-адреси", + "customUrl": "URL-адреса користувацького потоку", + "brandInformation": "Інформація про бренд", + "brandUrlFormat": "Для камер з форматом RTSP URL, як: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "testConnection": "Тестове з'єднання", + "testSuccess": "Тестування з'єднання успішне!", + "testFailed": "Перевірка з’єднання не вдалася. Перевірте введені дані та повторіть спробу.", + "streamDetails": "Деталі трансляції", + "warnings": { + "noSnapshot": "Не вдалося отримати знімок із налаштованого потоку." + }, + "errors": { + "brandOrCustomUrlRequired": "Виберіть або марку камери з хостом/IP-адресою, або виберіть «Інше» з власною URL-адресою", + "nameRequired": "Потрібно вказати назву камери", + "nameLength": "Назва камери має містити не більше 64 символів", + "invalidCharacters": "Назва камери містить недійсні символи", + "nameExists": "Назва камери вже існує", + "brands": { + "reolink-rtsp": "Не рекомендується використовувати Reolink RTSP. Увімкніть HTTP у налаштуваннях прошивки камери та перезапустіть майстер." + }, + "customUrlRtspRequired": "Користувацькі URL-адреси мають починатися з \"rtsp://\". Для потоків з камер, що не підтримують RTSP, потрібне ручне налаштування." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Зондування метаданих камери...", + "fetchingSnapshot": "Отримання знімка камери..." + }, + "connectionSettings": "Налаштування підключення", + "detectionMethod": "Метод виявлення потоку", + "onvifPort": "Порт ONVIF", + "probeMode": "Зонд-камера", + "manualMode": "Ручний вибір", + "detectionMethodDescription": "Перевірте камеру з ONVIF (якщо підтримується) для пошуку URL-адресів потоку камери або вручну виберіть бренд камери, щоб використовувати попередньо визначені URL. Щоб введіти налаштований URL-адрес RTSP, виберіть ручний метод і вибрати \"Інший\".", + "onvifPortDescription": "Для камер, що підтримують ONVIF, це зазвичай 80 або 8080.", + "useDigestAuth": "Використовувати дайджест-автентифікацію", + "useDigestAuthDescription": "Використовуйте автентифікацію HTTP-дайджест для ONVIF. Деякі камери можуть вимагати спеціальне ім’я користувача/пароль ONVIF замість стандартного користувача-адміністратора." + }, + "step2": { + "description": "Перевірте камеру на наявність доступних потоків або налаштуйте ручні параметри на основі вибраного методу виявлення.", + "streamsTitle": "Потоки з камери", + "addStream": "Додати потік", + "addAnotherStream": "Додати ще один потік", + "streamTitle": "Потік {{number}}", + "streamUrl": "URL-адреса потоку", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "url": "URL", + "resolution": "Роздільна здатність", + "selectResolution": "Виберіть роздільну здатність", + "quality": "Якість", + "selectQuality": "Виберіть якість", + "roles": "Ролі", + "roleLabels": { + "detect": "Виявлення об'єктів", + "record": "Запис", + "audio": "Аудіо" + }, + "testStream": "Тестове з'єднання", + "testSuccess": "Тестування з'єднання успішне!", + "testFailed": "Перевірка з’єднання не вдалася. Перевірте введені дані та повторіть спробу.", + "testFailedTitle": "Тест не вдався", + "connected": "Підключено", + "notConnected": "Не підключено", + "featuresTitle": "Особливості", + "go2rtc": "Зменште кількість підключень до камери", + "detectRoleWarning": "Для продовження принаймні один потік повинен мати роль \"виявлення\".", + "rolesPopover": { + "title": "Ролі потоку", + "detect": "Основний канал для виявлення об'єктів.", + "record": "Зберігає сегменти відеоканалу на основі налаштувань конфігурації.", + "audio": "Стрічка даних для виявлення на основі аудіо." + }, + "featuresPopover": { + "title": "Функції потоку", + "description": "Використовуйте ретрансляцію go2rtc, щоб зменшити кількість підключень до вашої камери." + }, + "streamDetails": "Деталі трансляції", + "probing": "Зондуюча камера...", + "retry": "Повторити спробу", + "testing": { + "probingMetadata": "Зондування метаданих камери...", + "fetchingSnapshot": "Отримання знімка камери..." + }, + "probeFailed": "Не вдалося дослідити камеру: {{error}}", + "probingDevice": "Зондуючий пристрій...", + "probeSuccessful": "Зонд успішно", + "probeError": "Помилка зонда", + "probeNoSuccess": "Зондування Невдало", + "deviceInfo": "Інформація про пристрій", + "manufacturer": "Виробник", + "model": "Модель", + "firmware": "Прошивка", + "profiles": "Профілі", + "ptzSupport": "Підтримка PTZ-камер", + "autotrackingSupport": "Підтримка автоматичного відстеження", + "presets": "Пресети", + "rtspCandidates": "Кандидати RTSP", + "rtspCandidatesDescription": "З камери було знайдено такі URL-адреси RTSP. Перевірте з’єднання, щоб переглянути метадані потоку.", + "noRtspCandidates": "Не знайдено URL-адрес RTSP з камери. Ваші облікові дані можуть бути неправильними, або камера може не підтримувати ONVIF чи метод, який використовується для отримання URL-адрес RTSP. Поверніться та введіть URL-адресу RTSP вручну.", + "candidateStreamTitle": "Кандидат {{number}}", + "useCandidate": "Використання", + "uriCopy": "Копіювати", + "uriCopied": "URI скопійовано в буфер обміну", + "testConnection": "Тестове з'єднання", + "toggleUriView": "Натисніть, щоб перемкнути повний вигляд URI", + "errors": { + "hostRequired": "Потрібно вказати хост/IP-адресу" + } + }, + "step3": { + "description": "Налаштуйте ролі потоків та додайте додаткові потоки для вашої камери.", + "validationTitle": "Перевірка потоку", + "connectAllStreams": "Підключити всі потоки", + "reconnectionSuccess": "Повторне підключення успішне.", + "reconnectionPartial": "Не вдалося відновити підключення до деяких потоків.", + "streamUnavailable": "Попередній перегляд трансляції недоступний", + "reload": "Перезавантажити", + "connecting": "Підключення...", + "streamTitle": "Потік {{number}}", + "valid": "Дійсний", + "failed": "Не вдалося", + "notTested": "Не тестувалося", + "connectStream": "Підключитися", + "connectingStream": "Підключення", + "disconnectStream": "Відключитися", + "estimatedBandwidth": "Орієнтовна пропускна здатність", + "roles": "Ролі", + "none": "Жоден", + "error": "Помилка", + "streamValidated": "Потік {{number}} успішно перевірено", + "streamValidationFailed": "Не вдалося перевірити потік {{number}}", + "saveAndApply": "Зберегти нову камеру", + "saveError": "Недійсна конфігурація. Перевірте свої налаштування.", + "issues": { + "title": "Перевірка потоку", + "videoCodecGood": "Відеокодек: {{codec}}.", + "audioCodecGood": "Аудіокодек: {{codec}}.", + "noAudioWarning": "Для цього потоку не виявлено аудіо, записи не матимуть аудіо.", + "audioCodecRecordError": "Для підтримки аудіо в записах потрібен аудіокодек AAC.", + "audioCodecRequired": "Для підтримки виявлення звуку потрібен аудіопотік.", + "restreamingWarning": "Зменшення кількості підключень до камери для потоку запису може дещо збільшити використання процесора.", + "dahua": { + "substreamWarning": "Підпотік 1 заблокований на низькій роздільній здатності. Багато камер Dahua / Amcrest / EmpireTech підтримують додаткові підпотоки, які потрібно ввімкнути в налаштуваннях камери. Рекомендується перевірити та використовувати ці потоки, якщо вони доступні." + }, + "hikvision": { + "substreamWarning": "Підпотік 1 заблокований на низькій роздільній здатності. Багато камер Hikvision підтримують додаткові підпотоки, які потрібно ввімкнути в налаштуваннях камери. Рекомендується перевірити та використовувати ці потоки, якщо вони доступні." + }, + "resolutionHigh": "Роздільна здатність {{resolution}} може призвести до збільшення використання ресурсів.", + "resolutionLow": "Роздільна здатність {{resolution}} може бути занадто низькою для надійного виявлення малих об'єктів." + }, + "ffmpegModule": "Використовувати режим сумісності з потоками", + "ffmpegModuleDescription": "Якщо потік не завантажується після кількох спроб, спробуйте ввімкнути цю функцію. Коли вона ввімкнена, Frigate використовуватиме модуль ffmpeg з go2rtc. Це може забезпечити кращу сумісність з деякими потоками камер.", + "streamsTitle": "Трансляції з камери", + "addStream": "Додати потік", + "addAnotherStream": "Додати ще один потік", + "streamUrl": "URL-адреса потоку", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "selectStream": "Виберіть потік", + "searchCandidates": "Пошук кандидатів...", + "noStreamFound": "Потік не знайдено", + "url": "URL", + "resolution": "Роздільна здатність", + "selectResolution": "Виберіть роздільну здатність", + "quality": "Якість", + "selectQuality": "Виберіть якість", + "roleLabels": { + "detect": "Виявлення об'єктів", + "record": "Запис", + "audio": "Аудіо" + }, + "testStream": "Тестове з'єднання", + "testSuccess": "Тестування трансляції успішне!", + "testFailed": "Тест потоку не вдався", + "testFailedTitle": "Тест не вдався", + "connected": "Підключено", + "notConnected": "Не підключено", + "featuresTitle": "Особливості", + "go2rtc": "Зменште кількість підключень до камери", + "detectRoleWarning": "Для продовження принаймні один потік повинен мати роль \"виявлення\".", + "rolesPopover": { + "title": "Ролі потоку", + "detect": "Основний канал для виявлення об'єктів.", + "record": "Зберігає сегменти відеоканалу на основі налаштувань конфігурації.", + "audio": "Стрічка даних для виявлення на основі аудіо." + }, + "featuresPopover": { + "title": "Функції потоку", + "description": "Використовуйте ретрансляцію go2rtc, щоб зменшити кількість підключень до вашої камери." + } + }, + "step4": { + "description": "Фінальна перевірка та аналіз перед збереженням нової камери. Підключіть кожен потік перед збереженням.", + "validationTitle": "Перевірка потоку", + "connectAllStreams": "Підключити всі потоки", + "reconnectionSuccess": "Повторне підключення успішне.", + "reconnectionPartial": "Не вдалося відновити підключення до деяких потоків.", + "streamUnavailable": "Попередній перегляд трансляції недоступний", + "reload": "Перезавантажити", + "connecting": "Підключення...", + "streamTitle": "Потік {{number}}", + "valid": "Дійсний", + "failed": "Не вдалося", + "notTested": "Не тестувалося", + "connectStream": "Підключитися", + "connectingStream": "Підключення", + "disconnectStream": "Відключитися", + "estimatedBandwidth": "Орієнтовна пропускна здатність", + "roles": "Ролі", + "ffmpegModule": "Використовувати режим сумісності з потоками", + "ffmpegModuleDescription": "Якщо потік не завантажується після кількох спроб, спробуйте ввімкнути цю функцію. Коли вона ввімкнена, Frigate використовуватиме модуль ffmpeg з go2rtc. Це може забезпечити кращу сумісність з деякими потоками камер.", + "none": "Жоден", + "error": "Помилка", + "streamValidated": "Потік {{number}} успішно перевірено", + "streamValidationFailed": "Не вдалося перевірити потік {{number}}", + "saveAndApply": "Зберегти нову камеру", + "saveError": "Недійсна конфігурація. Перевірте свої налаштування.", + "issues": { + "title": "Перевірка потоку", + "videoCodecGood": "Відеокодек є {{codec}}.", + "audioCodecGood": "Аудіокодек є {{codec}}.", + "resolutionHigh": "Роздільна здатність {{resolution}} може призвести до збільшення використання ресурсів.", + "resolutionLow": "Роздільна здатність {{resolution}} може бути занадто низькою для надійного виявлення малих об'єктів.", + "noAudioWarning": "Для цього потоку не виявлено аудіо, записи не матимуть аудіо.", + "audioCodecRecordError": "Для підтримки аудіо в записах потрібен аудіокодек AAC.", + "audioCodecRequired": "Для підтримки виявлення звуку потрібен аудіопотік.", + "restreamingWarning": "Зменшення кількості підключень до камери для потоку запису може дещо збільшити використання процесора.", + "brands": { + "reolink-rtsp": "Не рекомендується використовувати Reolink RTSP. Увімкніть HTTP у налаштуваннях прошивки камери та перезапустіть майстер.", + "reolink-http": "Для кращої сумісності HTTP-потоки Reolink повинні використовувати FFmpeg. Увімкніть для цього потоку опцію «Використовувати режим сумісності потоків»." + }, + "dahua": { + "substreamWarning": "Підпотік 1 заперечений до низького розділу. Багато камери Dahua / Amcrest / EmpireTech підтримують додаткові підтоки, які потрібно включити в налаштуваннях камери. Рекомендується перевірити та використовувати ці потоки, якщо вони доступні." + }, + "hikvision": { + "substreamWarning": "Підпотік 1 заперечений до низького розділу. Багато камер Hikvision підтримують додаткові підтоки, які повинні бути включені в налаштуваннях камери. Рекомендується перевірити та використовувати ці потоки, якщо вони доступні." + } + } + } + }, + "cameraManagement": { + "title": "Керування камерами", + "addCamera": "Додати нову камеру", + "editCamera": "Редагувати камеру:", + "selectCamera": "Виберіть камеру", + "backToSettings": "Назад до налаштувань камери", + "streams": { + "title": "Увімкнути/вимкнути камери", + "desc": "Тимчасово вимкніть камеру до перезапуску Frigate. Вимкнення камери повністю зупиняє обробку потоків цієї камери в Frigate. Функції виявлення, запису та налагодження будуть недоступні.
    Примітка: це не вимикає ретрансляції " + }, + "cameraConfig": { + "add": "Додати камеру", + "edit": "Редагувати камеру", + "description": "Налаштуйте параметри камери, включаючи потокові входи та ролі.", + "name": "Назва камери", + "nameRequired": "Потрібно вказати назву камери", + "nameLength": "Назва камери має містити менше 64 символів.", + "namePlaceholder": "наприклад, передні_двері або огляд заднього двору", + "enabled": "Увімкнено", + "ffmpeg": { + "inputs": "Вхідні потоки", + "path": "Шлях потоку", + "pathRequired": "Шлях потоку обов'язковий", + "pathPlaceholder": "rtsp://...", + "roles": "Ролі", + "rolesRequired": "Потрібна хоча б одна роль", + "rolesUnique": "Кожна роль (аудіо, виявлення, запис) може бути призначена лише одному потоку", + "addInput": "Додати вхідний потік", + "removeInput": "Вилучити вхідний потік", + "inputsRequired": "Потрібен принаймні один вхідний потік" + }, + "go2rtcStreams": "go2rtc Стріми", + "streamUrls": "URL-адреси потоків", + "addUrl": "Додати URL-адресу", + "addGo2rtcStream": "Додати потік go2rtc", + "toast": { + "success": "Камеру {{cameraName}} успішно збережено" + } + } + }, + "cameraReview": { + "title": "Налаштування перегляду камери", + "object_descriptions": { + "title": "Генеративні описи об'єктів штучного інтелекту", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи об'єктів ШІ для цієї камери до перезавантаження Frigate. Якщо вимкнено, згенеровані ШІ описи не запитуватимуться для об'єктів, що відстежуються на цій камері." + }, + "review_descriptions": { + "title": "Описи генеративного ШІ-огляду", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи огляду за допомогою штучного інтелекту для цієї камери до перезавантаження Frigate. Якщо вимкнено, згенеровані штучним інтелектом описи не запитуватимуться для елементів огляду на цій камері." + }, + "review": { + "title": "Огляду", + "desc": "Тимчасово ввімкнути/вимкнути сповіщення та виявлення для цієї камери до перезавантаження Frigate. Якщо вимкнено, нові елементи огляду не створюватимуться. ", + "alerts": "Сповіщення ", + "detections": "Виявлення " + }, + "reviewClassification": { + "title": "Класифікація оглядів", + "desc": "Frigate класифікує об'єкти перевірки як сповіщення та виявлення. За замовчуванням усі об'єкти людина та автомобіль вважаються сповіщеннями. Ви можете уточнити класифікацію об'єктів перевірки, налаштувавши для них необхідні зони.", + "noDefinedZones": "Для цієї камери не визначено жодної зони.", + "objectAlertsTips": "Усі об’єкти {{alertsLabels}} на {{cameraName}} будуть відображатися як сповіщення.", + "zoneObjectAlertsTips": "Усі об’єкти {{alertsLabels}}, виявлені в {{zone}} на {{cameraName}}, будуть відображатися як сповіщення.", + "objectDetectionsTips": "Усі об’єкти {{detectionsLabels}}, які не класифіковані на {{cameraName}}, будуть відображатися як виявлені, незалежно від того, в якій зоні вони знаходяться.", + "zoneObjectDetectionsTips": { + "text": "Усі об’єкти {{detectionsLabels}}, що не належать до категорії {{zone}} на {{cameraName}}, будуть відображатися як Виявлення.", + "notSelectDetections": "Усі об’єкти {{detectionsLabels}}, виявлені в {{zone}} на {{cameraName}}, які не віднесені до категорії «Сповіщення», будуть відображатися як Виявлення незалежно від того, в якій зоні вони знаходяться.", + "regardlessOfZoneObjectDetectionsTips": "Усі об’єкти {{detectionsLabels}}, які не класифіковані на {{cameraName}}, будуть відображатися як виявлені, незалежно від того, в якій зоні вони знаходяться." + }, + "unsavedChanges": "Незбережені налаштування класифікації рецензій для {{camera}}", + "selectAlertsZones": "Виберіть зони для сповіщень", + "selectDetectionsZones": "Виберіть зони для виявлення", + "limitDetections": "Обмеження виявлення певними зонами", + "toast": { + "success": "Конфігурацію класифікації перегляду збережено. Перезапустіть Frigate, щоб застосувати зміни." + } + } + } +} diff --git a/web/public/locales/uk/views/system.json b/web/public/locales/uk/views/system.json new file mode 100644 index 0000000..4d8412f --- /dev/null +++ b/web/public/locales/uk/views/system.json @@ -0,0 +1,232 @@ +{ + "cameras": { + "label": { + "ffmpeg": "FFmpeg'", + "cameraSkippedDetectionsPerSecond": "{{camName}} пропущених виявлень на секунду", + "cameraDetect": "{{camName}} виявлення", + "cameraFfmpeg": "{{camName}} FFmpeg'", + "overallDetectionsPerSecond": "загальна кiлькiсть виявлень за секунду", + "cameraDetectionsPerSecond": "{{camName}} виявлень на секунду", + "overallFramesPerSecond": "загальна кiлкiсть кадрiв на секунду", + "overallSkippedDetectionsPerSecond": "загальна кiлкiсть пропущених виявлень за секунду", + "cameraCapture": "{{camName}} захоплення", + "cameraFramesPerSecond": "{{camName}} кадрiв на секунду", + "skipped": "пропущено", + "capture": "захоплення", + "camera": "камера", + "detect": "виявити" + }, + "toast": { + "success": { + "copyToClipboard": "Тестові дані копіюються в буфер обміну." + }, + "error": { + "unableToProbeCamera": "Не вдалося дослідити камеру: {{errorMessage}}" + } + }, + "title": "Камери", + "info": { + "fetching": "Отримання даних з камери", + "cameraProbeInfo": "{{camera}} Інформація про зонд камери", + "streamDataFromFFPROBE": "Дані потоку отримуються за допомогою ffprobe.", + "stream": "Потік {{idx}}", + "video": "Відео:", + "codec": "Кодек:", + "resolution": "Роздільна здатність:", + "fps": "FPS:", + "unknown": "Невідомо", + "audio": "Аудіо:", + "error": "Помилка: {{error}}", + "tips": { + "title": "Інформація про зонд камери" + }, + "aspectRatio": "співвідношення сторін", + "keyframes": { + "observedDuration": "Тривалість спостереження:" + } + }, + "overview": "Огляд", + "framesAndDetections": "Кадри / Виявлення" + }, + "enrichments": { + "embeddings": { + "plate_recognition": "Розпiзнавання номерiв", + "image_embedding_speed": "Швидкість вбудовування зображень", + "face_embedding_speed": "Швидкість вбудовування облич", + "face_recognition_speed": "Швидкість розпізнавання обличчя", + "plate_recognition_speed": "Швидкість розпізнавання номерних знаків", + "text_embedding_speed": "Швидкість вбудовування тексту", + "image_embedding": "Вбудовування зображень", + "text_embedding": "Вбудовування тексту", + "face_recognition": "Розпізнавання обличчя", + "yolov9_plate_detection_speed": "Швидкість виявлення номерних знаків YOLOv9", + "yolov9_plate_detection": "Виявлення пластин YOLOv9", + "review_description": "Опис огляду", + "review_description_speed": "Огляд Опис Швидкість", + "review_description_events_per_second": "Опис огляду", + "object_description": "Опис об'єкта", + "object_description_speed": "Опис об'єкта Швидкість", + "object_description_events_per_second": "Опис об'єкта", + "classification": "Класифікація {{name}}", + "classification_speed": "Швидкість класифікації {{name}}", + "classification_events_per_second": "{{name}} Подій класифікації за секунду" + }, + "title": "Збагачення", + "infPerSecond": "Висновки за секунду", + "averageInf": "Середній час висновування" + }, + "general": { + "title": "Загальна", + "hardwareInfo": { + "npuUsage": "Використання нейронного процесора", + "npuMemory": "Пам'ять NPU", + "title": "Інформація про обладнання", + "gpuUsage": "Використання графічного процесора", + "gpuMemory": "Пам'ять графічного процесора", + "gpuEncoder": "GPU-кодер", + "gpuDecoder": "Декодер графічного процесора", + "gpuInfo": { + "vainfoOutput": { + "title": "Вихід Vainfo", + "returnCode": "Код повернення: {{code}}", + "processOutput": "Вихід процесу:", + "processError": "Помилка процесу:" + }, + "nvidiaSMIOutput": { + "title": "Вихід Nvidia SMI", + "name": "Ім'я: {{name}}", + "driver": "Водій: {{driver}}", + "cudaComputerCapability": "Можливості обчислень CUDA: {{cuda_compute}}", + "vbios": "Інформація про VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Закрити інформацію про графічний процесор" + }, + "copyInfo": { + "label": "Копіювати інформацію про графічний процесор" + }, + "toast": { + "success": "Інформацію про графічний процесор скопійовано в буфер обміну" + } + }, + "intelGpuWarning": { + "title": "Попередження щодо статистики графічного процесора Intel", + "message": "Статистика графічного процесора недоступна", + "description": "Це відома помилка в інструментах звітності статистики графічного процесора Intel (intel_gpu_top), яка неодноразово повертає використання графічного процесора на рівні 0%, навіть у випадках, коли апаратне прискорення та виявлення об'єктів працюють належним чином на (i)GPU. Це не помилка Frigate. Ви можете перезавантажити хост, щоб тимчасово виправити проблему та переконатися, що графічний процесор працює правильно. Це не впливає на продуктивність." + } + }, + "otherProcesses": { + "processMemoryUsage": "Використання пам'яті процесу", + "processCpuUsage": "Використання процесора процесу", + "title": "Інші процеси", + "series": { + "go2rtc": "go2rtc", + "recording": "запис", + "review_segment": "сегмент огляду", + "embeddings": "вбудовування", + "audio_detector": "аудіодетектор" + } + }, + "detector": { + "temperature": "Температура детектора", + "title": "Детектори", + "inferenceSpeed": "Швидкість виведення детектора", + "cpuUsage": "Використання процесора детектора", + "memoryUsage": "Використання пам'яті детектора", + "cpuUsageInformation": "Процесор, що використовується для підготовки вхідних та вихідних даних до/з моделей виявлення. Це значення не вимірює використання логічного висновку, навіть якщо використовується графічний процесор або прискорювач." + } + }, + "storage": { + "cameraStorage": { + "unused": { + "tips": "Це значення може неточно відображати обсяг вільного простору, доступного для Frigate, якщо на вашому диску зберігаються інші файли, окрім записів Frigate. Frigate не відстежує використання пам’яті поза ним.", + "title": "Невикористаний" + }, + "title": "Зберігання камери", + "storageUsed": "Зберігання", + "camera": "Камера", + "unusedStorageInformation": "Інформація про невикористане сховище", + "percentageOfTotalUsed": "Відсоток від загальної кількості", + "bandwidth": "Пропускна здатність" + }, + "overview": "Огляд", + "recordings": { + "title": "Записи", + "tips": "Це значення відображає загальний обсяг пам’яті, що використовується записами в базі даних Frigate. Frigate не відстежує використання пам’яті для всіх файлів на вашому диску.", + "earliestRecording": "Найдавніший доступний запис:" + }, + "title": "Зберігання", + "shm": { + "title": "Розподіл спільної пам'яті (SHM)", + "warning": "Поточний розмір SHM, що становить {{total}} МБ, замалий. Збільште його принаймні до {{min_shm}} МБ.", + "readTheDocumentation": "Прочитайте документацію" + } + }, + "lastRefreshed": "Останнє оновлення: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} має високе використання процесора FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} має високе використання процесора для виявлення ({{detectAvg}}%)", + "healthy": "Система справна", + "reindexingEmbeddings": "Переіндексація вбудовування (виконано {{processed}}%)", + "cameraIsOffline": "{{camera}} не в мережі", + "detectIsSlow": "{{detect}} повільний ({{speed}} мс)", + "detectIsVerySlow": "{{detect}} дуже повільний ({{speed}} мс)", + "shmTooLow": "Розмір /dev/shm ({{total}} МБ) слід збільшити щонайменше до {{min}} МБ." + }, + "documentTitle": { + "cameras": "Статистика камер - Фрегат", + "storage": "Статистика сховища - Фрегат", + "general": "Основна Статус – Frigate", + "enrichments": "Статистика збагачені - Фрегат", + "logs": { + "frigate": "Фрегатні журнали - Фрегат", + "go2rtc": "Журнали Go2RTC - Фрегат", + "nginx": "Журнали Nginx - Фрегат", + "websocket": "Журнал повідомлень - Frigate" + } + }, + "title": "Система", + "metrics": "Системні показники", + "logs": { + "download": { + "label": "Завантажити журнали" + }, + "copy": { + "label": "Копіювати в буфер обміну", + "success": "Скопійовано журнали в буфер обміну", + "error": "Не вдалося скопіювати журнали в буфер обміну" + }, + "type": { + "label": "Тип", + "timestamp": "Позначка часу", + "tag": "Тег", + "message": "Повідомлення" + }, + "tips": "Журнали передаються потоком із сервера", + "toast": { + "error": { + "fetchingLogsFailed": "Помилка отримання журналів: {{errorMessage}}", + "whileStreamingLogs": "Помилка під час потокової передачі журналів: {{errorMessage}}" + } + }, + "websocket": { + "label": "Повідомлення", + "pause": "Павза", + "resume": "Продовжити", + "clear": "Очистити", + "filter": { + "all": "Всі теми (topics)", + "topics": "Теми (topics)", + "events": "Події", + "reviews": "Перевірки", + "classification": "Класифікація", + "face_recognition": "Розпізнавання обличчя", + "lpr": "Розпізнавання номерних знаків (LPR)", + "camera_activity": "Активність камери", + "system": "Система", + "camera": "Камера", + "all_cameras": "Всі камери" + } + } + } +} diff --git a/web/public/locales/ur/audio.json b/web/public/locales/ur/audio.json new file mode 100644 index 0000000..6d7904e --- /dev/null +++ b/web/public/locales/ur/audio.json @@ -0,0 +1,53 @@ +{ + "wheeze": "گھرگھراہٹ", + "snoring": "خراٹے", + "cough": "کھانسی", + "singing": "گانا", + "throat_clearing": "گلا صاف کرنا", + "sneeze": "چھینک", + "sniff": "سونگھنا", + "run": "دوڑو", + "applause": "تالیاں", + "chatter": "چہچہانا", + "crowd": "بھیڑ", + "animal": "جانور", + "children_playing": "بچے کھیل رہے ہیں", + "howl": "چیخنا", + "growling": "کراہنے والا", + "dog": "کتا", + "cat": "بلی", + "pets": "پالتو جانور", + "whimper_dog": "کتے کی آواز", + "bark": "بھونکنا", + "meow": "میانو", + "speech": "تقریر", + "babbling": "بڑبڑانا", + "yell": "چیخنا", + "bellow": "دَہاڑ", + "whoop": "اففف", + "whispering": "سرگوشی", + "laughter": "ہنسی", + "snicker": "منہ دبا کر ہنسنا", + "car": "گاڑی", + "bus": "بس", + "motorcycle": "موٹر سائیکل", + "train": "ٹرین", + "bicycle": "سائیکل", + "crying": "رونا", + "sigh": "آہیں", + "choir": "کوئر", + "yodeling": "یوڈیلنگ", + "chant": "نعرہ لگانا", + "mantra": "منتر", + "child_singing": "چائلڈ گانا", + "synthetic_singing": "مصنوعی گانا", + "rapping": "ریپنگ", + "humming": "گنگنانا", + "groan": "کراہنا", + "grunt": "گرنٹ", + "whistling": "سیٹی بجانا", + "breathing": "سانس لینا", + "gasp": "ہانپنا", + "pant": "ہانپنا", + "snort": "خراٹے" +} diff --git a/web/public/locales/ur/common.json b/web/public/locales/ur/common.json new file mode 100644 index 0000000..37ff068 --- /dev/null +++ b/web/public/locales/ur/common.json @@ -0,0 +1,39 @@ +{ + "time": { + "untilForTime": "{{time}} تک", + "untilForRestart": "فریگیٹ کے دوبارہ شروع ہونے تک۔", + "untilRestart": "دوبارہ شروع ہونے تک", + "ago": "{{timeAgo}} پہلے", + "justNow": "ابھی ابھی", + "today": "آج", + "yesterday": "کل", + "last7": "پچھلے 7 دن", + "last14": "آخری 14 دن", + "last30": "آخری 30 دن", + "thisWeek": "اس ہفتے", + "lastWeek": "گزشتہ ہفتے", + "thisMonth": "اس مہینے", + "lastMonth": "پچھلے مہینے", + "5minutes": "5 منٹ", + "10minutes": "10 منٹ", + "30minutes": "30 منٹ", + "1hour": "1 گھنٹہ", + "12hours": "12 گھنٹے", + "24hours": "24 گھنٹے", + "pm": "pm", + "am": "am", + "yr": "{{time}}سال", + "mo": "{{time}} مہینہ", + "d": "{{time}} دن", + "h": "{{time}} گھنٹہ", + "year_one": "{{time}} سال", + "year_other": "{{time}} سال", + "day_one": "{{time}} دن", + "day_other": "{{time}} دن", + "month_one": "{{time}} مہینہ", + "month_other": "{{time}} مہینے", + "hour_one": "{{time}} گھنٹہ", + "hour_other": "{{time}} گھنٹے" + }, + "readTheDocumentation": "دستاویز پڑھیں" +} diff --git a/web/public/locales/ur/components/auth.json b/web/public/locales/ur/components/auth.json new file mode 100644 index 0000000..ab19b2e --- /dev/null +++ b/web/public/locales/ur/components/auth.json @@ -0,0 +1,14 @@ +{ + "form": { + "user": "اکاؤنٹ کا نام", + "password": "پاسورڈ", + "login": "لاگ ان", + "errors": { + "usernameRequired": "صارف نام درکار ہے", + "passwordRequired": "پاس ورڈ درکار ہے", + "rateLimit": "شرح کی حد سے تجاوز کر گیا۔ بعد میں دوبارہ کوشش کریں۔", + "loginFailed": "لاگ ان ناکام ہو گیا", + "unknownError": "نامعلوم خرابی۔ لاگز چیک کریں۔" + } + } +} diff --git a/web/public/locales/ur/components/camera.json b/web/public/locales/ur/components/camera.json new file mode 100644 index 0000000..900341e --- /dev/null +++ b/web/public/locales/ur/components/camera.json @@ -0,0 +1,17 @@ +{ + "group": { + "delete": { + "confirm": { + "desc": "کیا آپ واقعی کیمرہ گروپ {{name}} کو حذف کرنا چاہتے ہیں؟", + "title": "حذف کی تصدیق کریں" + }, + "label": "کیمرہ گروپ کو حذف کریں" + }, + "label": "کیمرہ گروپس", + "add": "کیمرہ گروپ شامل کریں", + "edit": "کیمرہ گروپ میں ترمیم کریں", + "name": { + "label": "نام" + } + } +} diff --git a/web/public/locales/ur/components/dialog.json b/web/public/locales/ur/components/dialog.json new file mode 100644 index 0000000..6367caf --- /dev/null +++ b/web/public/locales/ur/components/dialog.json @@ -0,0 +1,24 @@ +{ + "restart": { + "title": "کیا آپ واقعی Frigate کو دوبارہ شروع کرنا چاہتے ہیں؟", + "button": "دوبارہ شروع کریں", + "restarting": { + "title": "فریگیٹ دوبارہ شروع ہو رہا ہے", + "content": "یہ صفحہ {{countdown}} سیکنڈ میں دوبارہ لوڈ ہو جائے گا۔", + "button": "ابھی دوبارہ لوڈ کرنے پر مجبور کریں" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "فریگیٹ+ کو جمع کروائیں", + "desc": "ایسے مقامات میں موجود آبجیکٹس جن سے آپ بچنا چاہتے ہیں، جھوٹے مثبت نہیں ہوتے۔ انہیں جھوٹے مثبت کے طور پر جمع کروانے سے ماڈل کنفیوز ہو جائے گا۔" + }, + "review": { + "question": { + "label": "فریگیٹ پلس کے لیے اس لیبل کی تصدیق کریں" + } + } + } + } +} diff --git a/web/public/locales/ur/components/filter.json b/web/public/locales/ur/components/filter.json new file mode 100644 index 0000000..45cd366 --- /dev/null +++ b/web/public/locales/ur/components/filter.json @@ -0,0 +1,18 @@ +{ + "filter": "فلٹر", + "labels": { + "label": "لیبلز", + "all": { + "title": "تمام لیبلز", + "short": "لیبلز" + }, + "count_one": "{{count}} لیبل", + "count_other": "{{count}} لیبلز" + }, + "zones": { + "label": "زونز", + "all": { + "title": "تمام زونز" + } + } +} diff --git a/web/public/locales/ur/components/icons.json b/web/public/locales/ur/components/icons.json new file mode 100644 index 0000000..8c2d816 --- /dev/null +++ b/web/public/locales/ur/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "آئیکن منتخب کریں", + "search": { + "placeholder": "آئیکن تلاش کریں…" + } + } +} diff --git a/web/public/locales/ur/components/input.json b/web/public/locales/ur/components/input.json new file mode 100644 index 0000000..8b71c9b --- /dev/null +++ b/web/public/locales/ur/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "ویڈیو ڈاؤن لوڈ کریں", + "toast": { + "success": "آپ کی جائزے کی ویڈیو ڈاؤن لوڈ ہونا شروع ہو گئی ہے۔" + } + } + } +} diff --git a/web/public/locales/ur/components/player.json b/web/public/locales/ur/components/player.json new file mode 100644 index 0000000..b66f970 --- /dev/null +++ b/web/public/locales/ur/components/player.json @@ -0,0 +1,13 @@ +{ + "noRecordingsFoundForThisTime": "اس وقت کے لیے کوئی ریکارڈنگ نہیں ملی", + "noPreviewFound": "کوئی پیش نظارہ نہیں ملا", + "noPreviewFoundFor": "{{cameraName}} کے لیے کوئی پیش نظارہ نہیں ملا", + "submitFrigatePlus": { + "title": "اس فریم کو فریگیٹ+ میں جمع کرائیں؟", + "submit": "جمع کروائیں" + }, + "livePlayerRequiredIOSVersion": "اس لائیو اسٹریم کی قسم کے لیے iOS 17.1 یا اس سے جدید ورژن درکار ہے۔", + "streamOffline": { + "title": "آف لائن اسٹریم" + } +} diff --git a/web/public/locales/ur/config/cameras.json b/web/public/locales/ur/config/cameras.json new file mode 100644 index 0000000..23c240d --- /dev/null +++ b/web/public/locales/ur/config/cameras.json @@ -0,0 +1,3 @@ +{ + "label": "کیمرے کی ترتیب" +} diff --git a/web/public/locales/ur/config/global.json b/web/public/locales/ur/config/global.json new file mode 100644 index 0000000..805c0d3 --- /dev/null +++ b/web/public/locales/ur/config/global.json @@ -0,0 +1,5 @@ +{ + "version": { + "label": "موجودہ کنفیگریشن ورژن" + } +} diff --git a/web/public/locales/ur/config/groups.json b/web/public/locales/ur/config/groups.json new file mode 100644 index 0000000..13c20a3 --- /dev/null +++ b/web/public/locales/ur/config/groups.json @@ -0,0 +1,7 @@ +{ + "audio": { + "global": { + "detection": "عالمی کھوج" + } + } +} diff --git a/web/public/locales/ur/config/validation.json b/web/public/locales/ur/config/validation.json new file mode 100644 index 0000000..b871b9f --- /dev/null +++ b/web/public/locales/ur/config/validation.json @@ -0,0 +1,3 @@ +{ + "minimum": "کم از کم {{limit}} ہونا چاہیے" +} diff --git a/web/public/locales/ur/objects.json b/web/public/locales/ur/objects.json new file mode 100644 index 0000000..88a44f1 --- /dev/null +++ b/web/public/locales/ur/objects.json @@ -0,0 +1,9 @@ +{ + "person": "شخص", + "bicycle": "سائیکل", + "car": "گاڑی", + "motorcycle": "موٹر سائیکل", + "airplane": "ہوائی جہاز", + "bus": "بس", + "train": "ٹرین" +} diff --git a/web/public/locales/ur/views/chat.json b/web/public/locales/ur/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ur/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ur/views/classificationModel.json b/web/public/locales/ur/views/classificationModel.json new file mode 100644 index 0000000..7893f7d --- /dev/null +++ b/web/public/locales/ur/views/classificationModel.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "درجہ بندی کے ماڈلز - فریگیٹ" +} diff --git a/web/public/locales/ur/views/configEditor.json b/web/public/locales/ur/views/configEditor.json new file mode 100644 index 0000000..e32955e --- /dev/null +++ b/web/public/locales/ur/views/configEditor.json @@ -0,0 +1,16 @@ +{ + "toast": { + "error": { + "savingError": "کنفیگریشن محفوظ کرنے میں خرابی" + }, + "success": { + "copyToClipboard": "کنفیگ کلپ بورڈ پر کاپی ہو گیا۔" + } + }, + "documentTitle": "کنفیگریشن ایڈیٹر - Frigate", + "configEditor": "کنفیگریشن ایڈیٹر", + "copyConfig": "کنفیگریشن نقل کریں", + "saveAndRestart": "محفوظ کریں اور دوبارہ شروع کریں", + "saveOnly": "صرف محفوظ کریں", + "confirm": "محفوظ کیے بغیر باہر نکلیں؟" +} diff --git a/web/public/locales/ur/views/events.json b/web/public/locales/ur/views/events.json new file mode 100644 index 0000000..517d7dd --- /dev/null +++ b/web/public/locales/ur/views/events.json @@ -0,0 +1,14 @@ +{ + "alerts": "انتباہات", + "detections": "کھوج", + "motion": { + "label": "حرکت", + "only": "صرف حرکت" + }, + "allCameras": "تمام کیمرے", + "empty": { + "alert": "جائزہ لینے کے لیے کوئی انتباہات نہیں ہیں", + "detection": "جائزہ لینے کے لیے کوئی ڈیٹیکشن موجود نہیں ہے", + "motion": "کوئی موشن ڈیٹا نہیں ملا" + } +} diff --git a/web/public/locales/ur/views/explore.json b/web/public/locales/ur/views/explore.json new file mode 100644 index 0000000..2657e80 --- /dev/null +++ b/web/public/locales/ur/views/explore.json @@ -0,0 +1,13 @@ +{ + "documentTitle": "جائزہ لیں - فریگیٹ", + "generativeAI": "جنریٹو اے آئی", + "exploreIsUnavailable": { + "title": "جائزہ لینا دستیاب نہیں ہے", + "embeddingsReindexing": { + "context": "جب ٹریک کیے گئے آبجیکٹ ایمبیڈنگز کی دوبارہ انڈیکسنگ مکمل ہو جائےتو \"جائزہ لیں\" استعمال کیا جا سکتا ہے۔", + "startingUp": "شروع ہو رہا ہے…", + "estimatedTime": "متوقع باقی وقت:" + } + }, + "exploreMore": "مزید {{label}} اشیاء کو دریافت کریں" +} diff --git a/web/public/locales/ur/views/exports.json b/web/public/locales/ur/views/exports.json new file mode 100644 index 0000000..395ee16 --- /dev/null +++ b/web/public/locales/ur/views/exports.json @@ -0,0 +1,11 @@ +{ + "documentTitle": "برآمد - فریگیٹ", + "search": "تلاش", + "noExports": "کوئی برآمدات نہیں ملے", + "deleteExport": "برآمد کو حذف کریں", + "deleteExport.desc": "کیا آپ واقعی {{exportName}} کو حذف کرنا چاہتے ہیں؟", + "editExport": { + "title": "برآمد کا نام تبدیل کریں", + "desc": "اس برآمد کے لیے ایک نیا نام درج کریں۔" + } +} diff --git a/web/public/locales/ur/views/faceLibrary.json b/web/public/locales/ur/views/faceLibrary.json new file mode 100644 index 0000000..9185231 --- /dev/null +++ b/web/public/locales/ur/views/faceLibrary.json @@ -0,0 +1,13 @@ +{ + "description": { + "addFace": "اپنی پہلی تصویر اپ لوڈ کرکے فیس لائبریری میں ایک نیا کلیکشن شامل کریں۔", + "placeholder": "اس مجموعہ کے لیے ایک نام درج کریں", + "invalidName": "غلط نام۔ ناموں میں صرف حروف، اعداد، فاصلے، اپوسٹروف، انڈر اسکور، اور ہائفن شامل ہو سکتے ہیں۔" + }, + "details": { + "face": "چہرے کی تفصیلات", + "person": "شخص", + "subLabelScore": "سب لیبل سکور", + "scoreInfo": "سب لیبل سکور تمام تسلیم شدہ چہرے کے اعتماد کے لیے وزنی سکور ہے، اس لیے یہ سنیپ شاٹ پر دکھائے گئے سکور سے مختلف ہو سکتا ہے۔" + } +} diff --git a/web/public/locales/ur/views/live.json b/web/public/locales/ur/views/live.json new file mode 100644 index 0000000..cace611 --- /dev/null +++ b/web/public/locales/ur/views/live.json @@ -0,0 +1,13 @@ +{ + "documentTitle": "لائیو - فریگیٹ", + "documentTitle.withCamera": "{{camera}} -براہِ راست - فریگیٹ", + "lowBandwidthMode": "کم بینڈوتھ موڈ", + "twoWayTalk": { + "enable": "دو طرفہ گفتگو کو فعال کریں", + "disable": "دو طرفہ گفتگو کو غیر فعال کریں" + }, + "cameraAudio": { + "enable": "کیمرہ آڈیو فعال کریں", + "disable": "کیمرہ آڈیو کو غیر فعال کریں" + } +} diff --git a/web/public/locales/ur/views/motionSearch.json b/web/public/locales/ur/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ur/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ur/views/recording.json b/web/public/locales/ur/views/recording.json new file mode 100644 index 0000000..5ec3474 --- /dev/null +++ b/web/public/locales/ur/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "فلٹر", + "export": "برآمد", + "calendar": "کیلنڈر", + "filters": "فلٹرز", + "toast": { + "error": { + "noValidTimeSelected": "درست وقت کی حد منتخب نہیں کی گئی", + "endTimeMustAfterStartTime": "اختتامی وقت آغاز کے وقت کے بعد ہونا چاہیے" + } + } +} diff --git a/web/public/locales/ur/views/replay.json b/web/public/locales/ur/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/ur/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ur/views/search.json b/web/public/locales/ur/views/search.json new file mode 100644 index 0000000..5e73fa4 --- /dev/null +++ b/web/public/locales/ur/views/search.json @@ -0,0 +1,11 @@ +{ + "search": "تلاش", + "savedSearches": "محفوظ شدہ تلاشیں", + "searchFor": "{{inputValue}} تلاش کریں", + "button": { + "clear": "تلاش صاف کریں", + "save": "تلاش کو محفوظ کریں", + "delete": "محفوظ کردہ تلاش کو حذف کریں", + "filterInformation": "معلومات کو فلٹر کریں" + } +} diff --git a/web/public/locales/ur/views/settings.json b/web/public/locales/ur/views/settings.json new file mode 100644 index 0000000..d7172ab --- /dev/null +++ b/web/public/locales/ur/views/settings.json @@ -0,0 +1,11 @@ +{ + "documentTitle": { + "default": "ترتیبات - فریگیٹ", + "authentication": "تصدیق کی ترتیبات - فریگیٹ", + "camera": "کیمرے کی ترتیبات - فریگیٹ", + "masksAndZones": "ماسک اور زون ایڈیٹر - فریگیٹ", + "motionTuner": "موشن ٹونر - فریگیٹ", + "object": "ڈی بگ - فریگیٹ", + "enrichments": "افزودگی کی ترتیبات - فریگیٹ" + } +} diff --git a/web/public/locales/ur/views/system.json b/web/public/locales/ur/views/system.json new file mode 100644 index 0000000..3cc08e1 --- /dev/null +++ b/web/public/locales/ur/views/system.json @@ -0,0 +1,13 @@ +{ + "documentTitle": { + "cameras": "کیمروں کے اعدادوشمار - فریگیٹ", + "storage": "اسٹوریج کے اعدادوشمار - فریگیٹ", + "general": "عمومی اعدادوشمار - فریگیٹ", + "enrichments": "افزودگی کے اعدادوشمار - فریگیٹ", + "logs": { + "frigate": "فریگیٹ لاگز - فریگیٹ", + "go2rtc": "Go2RTC لاگز - فریگیٹ", + "nginx": "Nginx لاگز - فریگیٹ" + } + } +} diff --git a/web/public/locales/uz/audio.json b/web/public/locales/uz/audio.json new file mode 100644 index 0000000..436bf91 --- /dev/null +++ b/web/public/locales/uz/audio.json @@ -0,0 +1,4 @@ +{ + "speech": "Nutq", + "laughter": "Kulgi" +} diff --git a/web/public/locales/uz/common.json b/web/public/locales/uz/common.json new file mode 100644 index 0000000..2601b3f --- /dev/null +++ b/web/public/locales/uz/common.json @@ -0,0 +1,5 @@ +{ + "time": { + "untilForTime": "{{time}} vaqtgacha" + } +} diff --git a/web/public/locales/uz/components/auth.json b/web/public/locales/uz/components/auth.json new file mode 100644 index 0000000..ee2f331 --- /dev/null +++ b/web/public/locales/uz/components/auth.json @@ -0,0 +1,5 @@ +{ + "form": { + "user": "Foydalanuvchi nomi" + } +} diff --git a/web/public/locales/uz/components/camera.json b/web/public/locales/uz/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/components/dialog.json b/web/public/locales/uz/components/dialog.json new file mode 100644 index 0000000..680c3b0 --- /dev/null +++ b/web/public/locales/uz/components/dialog.json @@ -0,0 +1,5 @@ +{ + "restart": { + "title": "Frigate dasturini qayta ishga tushirishga aminmisiz?" + } +} diff --git a/web/public/locales/uz/components/filter.json b/web/public/locales/uz/components/filter.json new file mode 100644 index 0000000..33d5b02 --- /dev/null +++ b/web/public/locales/uz/components/filter.json @@ -0,0 +1,3 @@ +{ + "filter": "Filtr" +} diff --git a/web/public/locales/uz/components/icons.json b/web/public/locales/uz/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/components/input.json b/web/public/locales/uz/components/input.json new file mode 100644 index 0000000..ed23647 --- /dev/null +++ b/web/public/locales/uz/components/input.json @@ -0,0 +1,7 @@ +{ + "button": { + "downloadVideo": { + "label": "Videoni yuklab olish" + } + } +} diff --git a/web/public/locales/uz/components/player.json b/web/public/locales/uz/components/player.json new file mode 100644 index 0000000..1e126a8 --- /dev/null +++ b/web/public/locales/uz/components/player.json @@ -0,0 +1,3 @@ +{ + "noRecordingsFoundForThisTime": "Ushbu vaqt uchun hech qanday qayd mavjud emas" +} diff --git a/web/public/locales/uz/config/cameras.json b/web/public/locales/uz/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/config/global.json b/web/public/locales/uz/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/config/groups.json b/web/public/locales/uz/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/config/validation.json b/web/public/locales/uz/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/objects.json b/web/public/locales/uz/objects.json new file mode 100644 index 0000000..3a4a299 --- /dev/null +++ b/web/public/locales/uz/objects.json @@ -0,0 +1,3 @@ +{ + "person": "Shaxs" +} diff --git a/web/public/locales/uz/views/chat.json b/web/public/locales/uz/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/classificationModel.json b/web/public/locales/uz/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/configEditor.json b/web/public/locales/uz/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/events.json b/web/public/locales/uz/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/events.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/explore.json b/web/public/locales/uz/views/explore.json new file mode 100644 index 0000000..f04d684 --- /dev/null +++ b/web/public/locales/uz/views/explore.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "Frigate dasturi bilan tanishish" +} diff --git a/web/public/locales/uz/views/exports.json b/web/public/locales/uz/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/faceLibrary.json b/web/public/locales/uz/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/live.json b/web/public/locales/uz/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/motionSearch.json b/web/public/locales/uz/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/recording.json b/web/public/locales/uz/views/recording.json new file mode 100644 index 0000000..33d5b02 --- /dev/null +++ b/web/public/locales/uz/views/recording.json @@ -0,0 +1,3 @@ +{ + "filter": "Filtr" +} diff --git a/web/public/locales/uz/views/replay.json b/web/public/locales/uz/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/search.json b/web/public/locales/uz/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/settings.json b/web/public/locales/uz/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/uz/views/system.json b/web/public/locales/uz/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/uz/views/system.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/vi/audio.json b/web/public/locales/vi/audio.json new file mode 100644 index 0000000..9581108 --- /dev/null +++ b/web/public/locales/vi/audio.json @@ -0,0 +1,429 @@ +{ + "babbling": "Nói líu lo", + "bellow": "Gầm rú", + "whoop": "Hò reo", + "whispering": "Thì thầm", + "laughter": "Tiếng cười", + "snicker": "Cười khúc khích", + "sigh": "Thở dài", + "singing": "Hát", + "choir": "Dàn hợp xướng", + "yodeling": "Hát ngân nga", + "chant": "Hát đồng ca", + "mantra": "Thần chú", + "synthetic_singing": "Giọng hát tổng hợp", + "rapping": "Rap", + "humming": "Ngân nga", + "groan": "Rên rỉ", + "grunt": "Gằn giọng", + "whistling": "Huýt sáo", + "breathing": "Hít thở", + "wheeze": "Thở khò khè", + "gasp": "Thở hổn hển", + "pant": "Thở gấp", + "snort": "Khịt mũi", + "cough": "Ho", + "throat_clearing": "Hắng giọng", + "sneeze": "Hắt hơi", + "sniff": "Hít mũi", + "crying": "Khóc", + "yell": "La hét", + "snoring": "Ngáy", + "speech": "Giọng nói", + "child_singing": "Trẻ con hát", + "run": "Chạy", + "shuffle": "Kéo lê chân", + "footsteps": "Tiếng bước chân", + "chewing": "Nhai", + "biting": "Cắn", + "gargling": "Súc miệng", + "stomach_rumble": "Bụng sôi", + "burping": "Ợ", + "hiccup": "Nấc cụt", + "fart": "Đánh rắm", + "artillery_fire": "Tiếng pháo kích", + "cap_gun": "Tiếng súng giấy", + "fireworks": "Tiếng pháo hoa", + "firecracker": "Tiếng pháo nổ", + "burst": "Tiếng nổ bung", + "eruption": "Tiếng phun trào", + "boom": "Tiếng bùm", + "wood": "Tiếng gỗ", + "chop": "Tiếng chặt", + "splinter": "Tiếng gỗ vỡ", + "crack": "Tiếng nứt", + "glass": "Tiếng thủy tinh", + "chink": "Tiếng leng keng", + "shatter": "Tiếng vỡ vụn", + "silence": "Sự im lặng", + "sound_effect": "Hiệu ứng âm thanh", + "environmental_noise": "Tiếng ồn môi trường", + "static": "Tiếng nhiễu", + "white_noise": "Tiếng trắng", + "pink_noise": "Tiếng hồng", + "television": "Tiếng tivi", + "hands": "Tay", + "finger_snapping": "Búng tay", + "clapping": "Vỗ tay", + "heartbeat": "Nhịp tim", + "heart_murmur": "Tiếng thổi tim", + "cheering": "Cổ vũ", + "applause": "Tràng pháo tay", + "chatter": "Nói chuyện rì rầm", + "crowd": "Đám đông", + "children_playing": "Trẻ con chơi", + "animal": "Động vật", + "pets": "Thú cưng", + "dog": "Chó", + "bark": "Sủa", + "yip": "Sủa nhỏ", + "howl": "Hú", + "bow_wow": "Gâu gâu", + "growling": "Gầm gừ", + "whimper_dog": "Rên rỉ (chó)", + "livestock": "Gia súc", + "cat": "Mèo", + "purr": "Rù rì", + "meow": "Meo meo", + "hiss": "Phì phì", + "caterwaul": "Tiếng mèo gào", + "horse": "Ngựa", + "clip_clop": "Lộc cộc", + "neigh": "Hí (ngựa)", + "cattle": "Bò", + "moo": "Bò rống", + "cowbell": "Chuông bò", + "pig": "Heo", + "oink": "Ụt ịt", + "goat": "Dê", + "bleat": "Kêu be be", + "sheep": "Cừu", + "fowl": "Gia cầm", + "chicken": "Gà", + "cluck": "Cục tác", + "cock_a_doodle_doo": "Gáy (gà trống)", + "turkey": "Gà tây", + "gobble": "Gù gù (gà tây)", + "duck": "Vịt", + "quack": "Quạc quạc", + "goose": "Ngỗng", + "roar": "Gầm rú", + "bird": "Chim", + "chirp": "Hót líu lo", + "squawk": "Kêu the thé", + "honk": "Kêu vang (ngỗng)", + "wild_animals": "Động vật hoang dã", + "roaring_cats": "Mèo lớn gầm", + "pigeon": "Bồ câu", + "coo": "Cục cu", + "crow": "Quạ", + "caw": "Kêu quạ quạ", + "owl": "Cú mèo", + "hoot": "Kêu tu hú", + "flapping_wings": "Vỗ cánh", + "dogs": "Nhiều con chó", + "rats": "Chuột cống", + "mouse": "Chuột nhắt", + "patter": "Lách cách (bước chân nhỏ)", + "insect": "Côn trùng", + "cricket": "Dế", + "mosquito": "Muỗi", + "fly": "Ruồi", + "buzz": "Vo ve", + "frog": "Ếch", + "croak": "Ếch kêu", + "snake": "Rắn", + "rattle": "Lắc lư / lách cách", + "whale_vocalization": "Tiếng cá voi", + "music": "Âm nhạc", + "musical_instrument": "Nhạc cụ", + "plucked_string_instrument": "Nhạc cụ dây gảy", + "guitar": "Đàn guitar", + "electric_guitar": "Đàn guitar điện", + "bass_guitar": "Đàn guitar bass", + "acoustic_guitar": "Đàn guitar acoustic", + "steel_guitar": "Đàn steel guitar", + "tapping": "Kỹ thuật tapping", + "strum": "Gảy đàn", + "banjo": "Đàn banjo", + "sitar": "Đàn sitar", + "mandolin": "Đàn mandolin", + "zither": "Đàn tranh", + "ukulele": "Đàn ukulele", + "keyboard": "Bàn phím nhạc", + "piano": "Đàn piano", + "electric_piano": "đàn piano điện", + "organ": "Đàn organ", + "electronic_organ": "Đàn organ điện tử", + "hammond_organ": "Đàn organ Hammond", + "synthesizer": "Bộ tổng hợp âm", + "sampler": "Thiết bị lấy mẫu âm thanh", + "harpsichord": "Đàn harpsichord", + "percussion": "Bộ gõ", + "drum_kit": "Bộ trống", + "drum_machine": "Máy trống", + "drum": "Tiếng trống", + "snare_drum": "Trống snare", + "rimshot": "Gõ vành trống", + "drum_roll": "Cuộn trống", + "bass_drum": "Trống bass", + "timpani": "Trống timpani", + "tabla": "Trống tabla", + "cymbal": "Tiếng chũm chọe", + "hi_hat": "Tiếng hi-hat", + "wood_block": "Khối gỗ gõ", + "tambourine": "Trống lắc", + "maraca": "Tiếng lắc maraca", + "gong": "Tiếng chiêng", + "tubular_bells": "Chuông ống", + "mallet_percussion": "Nhạc cụ gõ bằng dùi", + "marimba": "Đàn marimba", + "glockenspiel": "Chuông gõ glockenspiel", + "vibraphone": "Đàn vibraphone", + "steelpan": "Trống thép", + "orchestra": "Dàn nhạc giao hưởng", + "brass_instrument": "Nhạc cụ đồng", + "french_horn": "Kèn Pháp", + "trumpet": "Kèn trumpet", + "trombone": "Kèn trombone", + "bowed_string_instrument": "Nhạc cụ dây kéo", + "string_section": "Dàn dây", + "violin": "Đàn violin", + "pizzicato": "Gảy dây pizzicato", + "cello": "Đàn cello", + "double_bass": "Đàn contrabass", + "wind_instrument": "Nhạc cụ hơi", + "flute": "Tiếng sáo", + "saxophone": "Kèn saxophone", + "clarinet": "Kèn clarinet", + "harp": "Đàn harp", + "bell": "Chuông", + "church_bell": "Chuông nhà thờ", + "jingle_bell": "Chuông leng keng", + "bicycle_bell": "Chuông xe đạp", + "tuning_fork": "Âm thoa", + "chime": "Tiếng chuỗi chuông", + "wind_chime": "Tiếng chuông gió", + "harmonica": "Tiếng kèn harmonica", + "accordion": "Tiếng đàn accordion", + "bagpipes": "Tiếng kèn túi", + "didgeridoo": "Tiếng kèn didgeridoo", + "theremin": "Tiếng nhạc cụ theremin", + "singing_bowl": "Tiếng chuông xoay Tây Tạng", + "scratching": "Scratch nhạc (xoay đĩa)", + "pop_music": "Nhạc pop", + "hip_hop_music": "Nhạc hip hop", + "beatboxing": "Beatbox", + "rock_music": "Nhạc rock", + "heavy_metal": "Nhạc heavy metal", + "punk_rock": "Nhạc punk rock", + "grunge": "Nhạc grunge", + "progressive_rock": "Nhạc rock tiến bộ", + "rock_and_roll": "Nhạc rock and roll", + "psychedelic_rock": "Nhạc rock ảo giác", + "rhythm_and_blues": "Nhạc R&B", + "soul_music": "Nhạc soul", + "reggae": "Nhạc reggae", + "country": "Nhạc đồng quê", + "swing_music": "Nhạc swing", + "bluegrass": "Nhạc bluegrass", + "funk": "Nhạc funk", + "folk_music": "Nhạc dân gian", + "middle_eastern_music": "Nhạc Trung Đông", + "jazz": "Nhạc jazz", + "disco": "Nhạc disco", + "classical_music": "Nhạc cổ điển", + "opera": "Nhạc opera", + "electronic_music": "Nhạc điện tử", + "house_music": "Nhạc house", + "techno": "Nhạc techno", + "dubstep": "Nhạc dubstep", + "drum_and_bass": "Nhạc trống và bass", + "electronica": "Nhạc electronica", + "electronic_dance_music": "Nhạc nhảy điện tử", + "ambient_music": "Nhạc nền", + "trance_music": "Nhạc trance", + "music_of_latin_america": "Nhạc Mỹ Latinh", + "salsa_music": "Nhạc salsa", + "flamenco": "Nhạc flamenco", + "blues": "Nhạc blues", + "music_for_children": "Nhạc thiếu nhi", + "new-age_music": "Nhạc thời đại mới", + "vocal_music": "Nhạc thanh nhạc", + "a_capella": "Nhạc a cappella", + "music_of_africa": "Nhạc châu Phi", + "afrobeat": "Nhạc afrobeat", + "christian_music": "Nhạc Cơ Đốc", + "gospel_music": "Nhạc phúc âm", + "music_of_asia": "Nhạc châu Á", + "carnatic_music": "Nhạc Carnatic", + "music_of_bollywood": "Nhạc Bollywood", + "ska": "Nhạc ska", + "traditional_music": "Nhạc truyền thống", + "independent_music": "Nhạc indie", + "song": "Bài hát", + "background_music": "Nhạc nền", + "theme_music": "Nhạc chủ đề", + "jingle": "Nhạc quảng cáo", + "soundtrack_music": "Nhạc phim", + "lullaby": "Tiếng ru", + "video_game_music": "Nhạc trò chơi", + "christmas_music": "Nhạc Giáng Sinh", + "dance_music": "Nhạc khiêu vũ", + "wedding_music": "Nhạc đám cưới", + "happy_music": "Nhạc vui", + "sad_music": "Nhạc buồn", + "tender_music": "Nhạc nhẹ nhàng", + "exciting_music": "Nhạc sôi động", + "angry_music": "Nhạc tức giận", + "scary_music": "Nhạc rùng rợn", + "wind": "Tiếng gió", + "rustling_leaves": "Tiếng lá xào xạc", + "wind_noise": "Tiếng gió rít", + "thunderstorm": "Tiếng giông bão", + "water": "Tiếng nước", + "thunder": "Tiếng sấm", + "rain": "Tiếng mưa", + "raindrop": "Tiếng giọt mưa", + "rain_on_surface": "Tiếng mưa rơi", + "stream": "Tiếng suối", + "waterfall": "Tiếng thác nước", + "ocean": "Tiếng biển", + "waves": "Tiếng sóng", + "steam": "Tiếng hơi nước", + "gurgling": "Tiếng róc rách", + "fire": "Tiếng lửa", + "crackle": "Tiếng tí tách", + "vehicle": "Phương tiện", + "boat": "Thuyền", + "sailboat": "Tiếng thuyền buồm", + "rowboat": "Tiếng chèo thuyền", + "motorboat": "Tiếng xuồng máy", + "ship": "Tiếng tàu", + "motor_vehicle": "Tiếng xe cơ giới", + "car": "Xe ô tô", + "toot": "Tiếng bấm còi", + "car_alarm": "Tiếng báo động ô tô", + "power_windows": "Tiếng cửa kính xe", + "skidding": "Tiếng trượt bánh", + "tire_squeal": "Tiếng lốp rít", + "car_passing_by": "Tiếng xe chạy qua", + "race_car": "Tiếng xe đua", + "truck": "Tiếng xe tải", + "ice_cream_truck": "Tiếng xe kem", + "air_brake": "Tiếng phanh hơi", + "air_horn": "Tiếng còi hơi", + "reversing_beeps": "Tiếng kêu lùi xe", + "bus": "Xe buýt", + "emergency_vehicle": "Tiếng xe khẩn cấp", + "police_car": "Tiếng xe cảnh sát", + "ambulance": "Tiếng xe cứu thương", + "fire_engine": "Tiếng xe cứu hỏa", + "motorcycle": "Xe máy", + "traffic_noise": "Tiếng giao thông", + "rail_transport": "Tiếng đường sắt", + "train_horn": "Tiếng còi tàu hỏa", + "railroad_car": "Tiếng toa tàu", + "train": "Tàu hỏa", + "train_whistle": "Tiếng còi tàu", + "train_wheels_squealing": "Tiếng bánh tàu rít", + "subway": "Tiếng tàu điện ngầm", + "aircraft": "Tiếng máy bay", + "aircraft_engine": "Tiếng động cơ máy bay", + "jet_engine": "Tiếng động cơ phản lực", + "propeller": "Tiếng cánh quạt", + "helicopter": "Tiếng trực thăng", + "fixed-wing_aircraft": "Tiếng máy bay cánh cố định", + "bicycle": "Xe đạp", + "skateboard": "Ván trượt", + "engine": "Tiếng động cơ", + "light_engine": "Tiếng động cơ nhẹ", + "dental_drill's_drill": "Tiếng khoan nha khoa", + "lawn_mower": "Tiếng máy cắt cỏ", + "chainsaw": "Tiếng cưa máy", + "medium_engine": "Tiếng động cơ vừa", + "heavy_engine": "Tiếng động cơ nặng", + "engine_knocking": "Tiếng gõ máy", + "engine_starting": "Tiếng khởi động động cơ", + "ding-dong": "Tiếng ding-dong", + "idling": "Tiếng nổ không tải", + "accelerating": "Tiếng tăng tốc", + "door": "Cửa", + "doorbell": "Tiếng chuông cửa", + "sliding_door": "Tiếng cửa trượt", + "slam": "Tiếng đóng sầm", + "knock": "Tiếng gõ cửa", + "tap": "Tiếng gõ nhẹ", + "squeak": "Tiếng kêu cót két", + "cupboard_open_or_close": "Tiếng mở/đóng tủ", + "drawer_open_or_close": "Tiếng mở/đóng ngăn kéo", + "dishes": "Tiếng bát đĩa", + "cutlery": "Tiếng dao nĩa", + "chopping": "Tiếng băm chặt", + "frying": "Tiếng chiên xào", + "microwave_oven": "Tiếng lò vi sóng", + "blender": "Máy xay", + "water_tap": "Tiếng vòi nước", + "sink": "Bồn rửa", + "bathtub": "Tiếng bồn tắm", + "coin": "Tiếng đồng xu", + "hair_dryer": "Máy sấy tóc", + "toilet_flush": "Tiếng xả nước", + "toothbrush": "Bàn chải", + "electric_toothbrush": "Tiếng bàn chải điện", + "vacuum_cleaner": "Tiếng máy hút bụi", + "zipper": "Tiếng dây kéo", + "keys_jangling": "Tiếng chìa khóa leng keng", + "scissors": "Kéo cắt", + "electric_shaver": "Tiếng máy cạo râu", + "shuffling_cards": "Tiếng xào bài", + "typing": "Tiếng gõ phím", + "typewriter": "Tiếng máy đánh chữ", + "computer_keyboard": "Tiếng bàn phím", + "writing": "Tiếng viết", + "alarm": "Tiếng báo động", + "telephone": "Tiếng điện thoại", + "telephone_bell_ringing": "Tiếng chuông điện thoại", + "ringtone": "Tiếng nhạc chuông", + "telephone_dialing": "Tiếng quay số", + "dial_tone": "Tiếng âm quay số", + "busy_signal": "Tiếng tín hiệu bận", + "alarm_clock": "Tiếng đồng hồ báo thức", + "siren": "Tiếng còi báo động", + "civil_defense_siren": "Tiếng còi phòng không", + "buzzer": "Tiếng chuông báo", + "smoke_detector": "Tiếng báo khói", + "fire_alarm": "Tiếng báo cháy", + "foghorn": "Tiếng còi sương", + "whistle": "Tiếng còi", + "steam_whistle": "Tiếng còi hơi", + "mechanisms": "Tiếng cơ khí", + "ratchet": "Tiếng cơ cấu bánh cóc", + "clock": "Đồng hồ", + "tick": "Tiếng tích", + "tick-tock": "Tiếng tích tắc", + "gears": "Tiếng bánh răng", + "pulleys": "Tiếng ròng rọc", + "sewing_machine": "Tiếng máy may", + "camera": "Tiếng máy ảnh", + "single-lens_reflex_camera": "Máy ảnh phản xạ ống kính đơn", + "mechanical_fan": "Tiếng quạt máy", + "air_conditioning": "Tiếng máy lạnh", + "cash_register": "Tiếng máy tính tiền", + "printer": "Tiếng máy in", + "tools": "Tiếng dụng cụ", + "hammer": "Tiếng búa", + "jackhammer": "Tiếng khoan bê tông", + "sawing": "Tiếng cưa", + "filing": "Tiếng giũa", + "sanding": "Tiếng chà nhám", + "power_tool": "Tiếng dụng cụ điện", + "drill": "Tiếng máy khoan", + "explosion": "Tiếng nổ", + "gunshot": "Tiếng súng", + "machine_gun": "Tiếng súng máy", + "fusillade": "Tiếng loạt súng", + "radio": "Tiếng radio", + "field_recording": "Ghi âm hiện trường", + "scream": "Tiếng hét" +} diff --git a/web/public/locales/vi/common.json b/web/public/locales/vi/common.json new file mode 100644 index 0000000..c2cc093 --- /dev/null +++ b/web/public/locales/vi/common.json @@ -0,0 +1,301 @@ +{ + "time": { + "untilRestart": "Đến khi khởi động lại", + "untilForTime": "Cho đến khi {{time}}", + "untilForRestart": "Cho đến khi Frigate khởi động lại.", + "ago": "{{timeAgo}} trước", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "year_other": "{{time}} năm", + "month_other": "{{time}} tháng", + "day_other": "{{time}} ngày", + "hour_other": "{{time}} giờ", + "minute_other": "{{time}} phút", + "second_other": "{{time}} giây", + "justNow": "Vừa xong", + "today": "Hôm nay", + "yesterday": "Hôm qua", + "last7": "7 ngày qua", + "last14": "14 ngày qua", + "last30": "30 ngày qua", + "thisWeek": "Tuần này", + "lastWeek": "Tuần trước", + "thisMonth": "Tháng này", + "lastMonth": "Tháng trước", + "5minutes": "5 phút", + "10minutes": "10 phút", + "30minutes": "30 phút", + "1hour": "1 giờ", + "12hours": "12 giờ", + "24hours": "24 giờ", + "pm": "pm", + "am": "am", + "mo": "{{time}} tháng", + "d": "{{time}} ngày", + "m": "{{time}} phút", + "s": "{{time}} giây", + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampExcludeSeconds": { + "12hour": "thời gian 12 giờ (không giây)", + "24hour": "thời gian 24 giờ (không giây)" + }, + "formattedTimestampWithYear": { + "12hour": "thời gian 12 giờ kèm năm", + "24hour": "thời gian 24 giờ kèm năm" + }, + "formattedTimestampOnlyMonthAndDay": "chỉ tháng và ngày", + "yr": "{{time}} năm", + "h": "{{time}} giờ", + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-s" + }, + "inProgress": "Đang tiến hành", + "invalidStartTime": "Thời gian bắt đầu không hợp lệ", + "invalidEndTime": "Thời gian kết thúc không hợp lệ", + "never": "Không bao giờ" + }, + "menu": { + "systemLogs": "Nhật ký hệ thống", + "user": { + "account": "Tài khoản", + "anonymous": "Ẩn danh", + "logout": "Đăng xuất", + "setPassword": "Đặt mật khẩu", + "current": "Người dùng hiện tại: {{user}}", + "title": "Người dùng" + }, + "language": { + "en": "English (Tiếng Anh)", + "es": "Español (Tiếng Tây Ban Nha)", + "zhCN": "简体中文 (Tiếng Trung Giản Thể)", + "ar": "العربية (Tiếng Ả Rập)", + "hi": "हिन्दी (Tiếng Hindi)", + "fr": "Français (Tiếng Pháp)", + "pt": "Português (Tiếng Bồ Đào Nha)", + "ru": "Русский (Tiếng Nga)", + "de": "Deutsch (Tiếng Đức)", + "ja": "日本語 (Tiếng Nhật)", + "tr": "Türkçe (Tiếng Thổ Nhĩ Kỳ)", + "it": "Italiano (Tiếng Ý)", + "nl": "Nederlands (Tiếng Hà Lan)", + "sv": "Svenska (Tiếng Thụy Điển)", + "cs": "Čeština (Tiếng Séc)", + "nb": "Norsk Bokmål (Tiếng Na Uy)", + "ko": "한국어 (Tiếng Hàn)", + "pl": "Polski (Tiếng Ba Lan)", + "vi": "Tiếng Việt (Tiếng Việt)", + "fa": "فارسی (Tiếng Ba Tư)", + "uk": "Українська (Tiếng Ukraina)", + "he": "עברית (Tiếng Do Thái)", + "el": "Ελληνικά (Tiếng Hy Lạp)", + "ro": "Română (Tiếng Romania)", + "hu": "Magyar (Tiếng Hungary)", + "fi": "Suomi (Tiếng Phần Lan)", + "da": "Dansk (Tiếng Đan Mạch)", + "sk": "Slovenčina (Tiếng Slovakia)", + "withSystem": { + "label": "Theo hệ thống" + }, + "yue": "粵語 (Tiếng Quảng Đông)", + "ca": "Català (Tiếng Catalan)", + "th": "ไทย (Tiếng Thái)", + "ptBR": "Português brasileiro (Tiếng Bồ Đào Nha Brazil)", + "sr": "Српски (Tiếng Serbian)", + "sl": "Slovenščina (Tiếng Slovenian)", + "lt": "Lietuvių (Tiếng Lithuanian)", + "bg": "Български (Tiếng Bulgarian)", + "gl": "Galego (Tiếng Galician)", + "id": "Bahasa Indonesia (Tiếng Indonesian)", + "ur": "اردو (Tiếng Urdu)" + }, + "system": "Hệ thống", + "systemMetrics": "Thông số hệ thống", + "configuration": "Cấu hình", + "settings": "Cài đặt", + "configurationEditor": "Trình chỉnh sửa cấu hình", + "languages": "Ngôn ngữ", + "appearance": "Giao diện", + "darkMode": { + "label": "Chế độ tối", + "light": "Sáng", + "dark": "Tối", + "withSystem": { + "label": "Theo hệ thống" + } + }, + "withSystem": "Hệ thống", + "theme": { + "label": "Giao diện", + "red": "Đỏ", + "contrast": "tương phản", + "blue": "Xanh dương", + "green": "Xanh lá", + "nord": "Nord", + "default": "Mặc định", + "highcontrast": "Độ tương phản cao" + }, + "help": "Trợ giúp", + "documentation": { + "title": "Tài liệu", + "label": "Hướng dẫn" + }, + "restart": "Khởi động lại", + "live": { + "title": "Trực tiếp", + "allCameras": "Tất cả Camera", + "cameras": { + "title": "Camera", + "count_other": "{{count}} Camera" + } + }, + "review": "Xem lại", + "explore": "Khám phá", + "export": "Xuất", + "uiPlayground": "UI Playground", + "faceLibrary": "Thư viện khuôn mặt", + "classification": "Phân loại" + }, + "unit": { + "speed": { + "mph": "mph (dặm/giờ)", + "kph": "km/h (kilômét/giờ)" + }, + "length": { + "meters": "mét (m)", + "feet": "feet (ft)" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/giờ", + "mbph": "MB/giờ", + "gbph": "GB/giờ" + } + }, + "label": { + "back": "Quay lại", + "hide": "Ẩn {{item}}", + "show": "Hiển thị {{item}}", + "ID": "ID", + "none": "Không có", + "all": "Tất cả" + }, + "button": { + "apply": "Áp dụng", + "reset": "Đặt lại", + "done": "Xong", + "enabled": "Đã bật", + "enable": "Bật", + "disabled": "Đã tắt", + "disable": "Tắt", + "save": "Lưu", + "cancel": "Hủy", + "close": "Đóng", + "copy": "Sao chép", + "back": "Quay lại", + "history": "Lịch sử", + "fullscreen": "Toàn màn hình", + "on": "Bật", + "exitFullscreen": "Thoát toàn màn hình", + "pictureInPicture": "Hình trong hình", + "twoWayTalk": "Đàm thoại hai chiều", + "cameraAudio": "Âm thanh Camera", + "off": "Tắt", + "edit": "Chỉnh sửa", + "copyCoordinates": "Sao chép tọa độ", + "delete": "Xóa", + "yes": "Có", + "no": "Không", + "download": "Tải xuống", + "info": "Thông tin", + "suspended": "Đã tạm dừng", + "unsuspended": "Khôi phục", + "play": "Phát", + "unselect": "Bỏ chọn", + "export": "Xuất", + "deleteNow": "Xóa ngay", + "next": "Tiếp theo", + "saving": "Đang lưu…", + "continue": "Tiếp tục" + }, + "toast": { + "copyUrlToClipboard": "Đã sao chép liên kết.", + "save": { + "title": "Lưu thành công", + "error": { + "noMessage": "Không thể lưu thay đổi cấu hình", + "title": "Lỗi khi lưu thay đổi cấu hình: {{errorMessage}}" + } + } + }, + "role": { + "title": "Vai trò", + "admin": "Quản trị viên", + "viewer": "Người xem", + "desc": "Quản trị viên có toàn quyền truy cập tất cả các tính năng trong giao diện Frigate. Người xem chỉ được phép xem camera, mục đã ghi lại và các đoạn video lịch sử trong giao diện." + }, + "pagination": { + "label": "Trang", + "previous": { + "title": "Trước đó", + "label": "Trước" + }, + "next": { + "title": "Kế tiếp", + "label": "Tiếp" + }, + "more": "Xem thêm" + }, + "accessDenied": { + "documentTitle": "Từ chối truy cập", + "title": "Truy cập bị từ chối", + "desc": "Bạn không có quyền truy cập vào trang này." + }, + "notFound": { + "documentTitle": "Không tìm thấy", + "title": "Không tìm thấy", + "desc": "Trang bạn đang tìm không tồn tại" + }, + "selectItem": "Chọn mục {{item}}", + "readTheDocumentation": "Đọc tài liệu", + "list": { + "two": "{{0}} và {{1}}", + "many": "{{items}}, và {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Không bắt buộc", + "internalID": "Internal ID Frigate sử dụng trong cấu hình và cơ sở dữ liệu" + }, + "information": { + "pixels": "{{area}}px" + } +} diff --git a/web/public/locales/vi/components/auth.json b/web/public/locales/vi/components/auth.json new file mode 100644 index 0000000..bc664d5 --- /dev/null +++ b/web/public/locales/vi/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Tên người dùng", + "password": "Mật khẩu", + "login": "Đăng nhập", + "errors": { + "usernameRequired": "Tên người dùng là bắt buộc", + "passwordRequired": "Mật khẩu là bắt buộc", + "rateLimit": "Đã vượt quá giới hạn tốc độ. Hãy thử lại sau.", + "loginFailed": "Đăng nhập không thành công", + "unknownError": "Lỗi không xác định. Kiểm tra nhật ký.", + "webUnknownError": "Lỗi không xác định. Kiểm tra nhật ký bảng điều khiển." + }, + "firstTimeLogin": "Lần đầu đăng nhập? Thông tin đăng nhập được in trong nhật ký (log) của Frigate." + } +} diff --git a/web/public/locales/vi/components/camera.json b/web/public/locales/vi/components/camera.json new file mode 100644 index 0000000..e67824e --- /dev/null +++ b/web/public/locales/vi/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "delete": { + "label": "Xóa nhóm Camera", + "confirm": { + "title": "Xác nhận xóa", + "desc": "Bạn có chắc chắn muốn xóa nhóm camera {{name}} không?" + } + }, + "label": "Các nhóm Camera", + "add": "Thêm nhóm Camera", + "camera": { + "setting": { + "stream": "Trực tiếp", + "audio": { + "tips": { + "title": "Âm thanh phải được xuất từ camera của bạn và được định cấu hình trong go2rtc cho luồng này.", + "document": "Đọc tài liệu " + } + }, + "desc": "Thay đổi các tùy chọn truyền phát trực tiếp cho bảng điều khiển của nhóm camera này.Các cài đặt này dành riêng cho thiết bị/trình duyệt..", + "streamMethod": { + "method": { + "noStreaming": { + "desc": "Hình ảnh camera sẽ chỉ cập nhật mỗi phút một lần và không có truyền phát nào xảy ra.", + "label": "Không truyền phát" + }, + "continuousStreaming": { + "desc": { + "title": "Hình ảnh camera sẽ luôn là luồng trực tiếp khi hiển thị trên bảng điều khiển, ngay cả khi không có hoạt động nào được phát hiện.", + "warning": "Truyền phát liên tục có thể gây ra sử dụng băng thông cao và các vấn đề về hiệu suất. Sử dụng một cách thận trọng." + }, + "label": "Truyền phát liên tục" + }, + "smartStreaming": { + "label": "Truyền phát Thông minh (khuyến nghị)", + "desc": "Truyền phát thông minh sẽ cập nhật hình ảnh camera của bạn mỗi phút một lần khi không có hoạt động nào được phát hiện để tiết kiệm băng thông và tài nguyên. Khi phát hiện hoạt động, hình ảnh sẽ chuyển đổi liền mạch sang luồng trực tiếp." + } + }, + "placeholder": "Chọn phương thức truyền phát", + "label": "Phương thức truyền phát" + }, + "placeholder": "Chọn phát trực tiếp", + "compatibilityMode": { + "label": "Chế độ tương thích", + "desc": "Chỉ bật tùy chọn này nếu luồng trực tiếp của camera của bạn hiển thị các hiện vật màu và có một đường chéo ở phía bên phải của hình ảnh." + }, + "title": "Cài đặt trực tiếp {{cameraName}}", + "audioIsAvailable": "Âm thanh có sẵn cho luồng này", + "audioIsUnavailable": "Âm thanh không có sẵn cho luồng này", + "label": "Cài đặt trực tiếp Camera" + }, + "birdseye": "Toàn cảnh" + }, + "name": { + "label": "Tên", + "errorMessage": { + "mustLeastCharacters": "Tên nhóm Camera phải có ít nhất 2 ký tự.", + "nameMustNotPeriod": "Tên nhóm camera không được chứa dấu chấm.", + "exists": "Tên nhóm Camera đã tồn tại.", + "invalid": "Tên nhóm camera không hợp lệ." + }, + "placeholder": "Nhập tên…" + }, + "icon": "Biểu tượng", + "success": "Nhóm camera ({{name}}) đã được lưu.", + "cameras": { + "desc": "Chọn camera cho nhóm này.", + "label": "Camera" + }, + "edit": "Sửa nhóm Camera" + }, + "debug": { + "boundingBox": "Hộp giới hạn", + "options": { + "hideOptions": "Ẩn tùy chọn", + "label": "Cài đặt", + "title": "Tùy chọn", + "showOptions": "Hiện thị tùy chọn" + }, + "timestamp": "Dấu thời gian", + "zones": "Khu vực", + "mask": "Mặt nạ", + "motion": "Chuyển động", + "regions": "Vùng" + } +} diff --git a/web/public/locales/vi/components/dialog.json b/web/public/locales/vi/components/dialog.json new file mode 100644 index 0000000..2eae9fb --- /dev/null +++ b/web/public/locales/vi/components/dialog.json @@ -0,0 +1,123 @@ +{ + "restart": { + "title": "Bạn có chắc chắn muốn khởi động lại Frigate không?", + "button": "Khởi động lại", + "restarting": { + "title": "Đang khởi động lại Frigate", + "content": "Trang này sẽ tải lại sau {{countdown}} giây.", + "button": "Tải lại ngay" + }, + "description": "Thao tác này sẽ tạm dừng hoạt động của Frigate trong thời gian ngắn để khởi động lại." + }, + "explore": { + "plus": { + "review": { + "question": { + "ask_a": "Đối tượng này có phải là một {{label}} không?", + "ask_an": "Đối tượng này có phải là một {{label}} không?", + "label": "Xác nhận nhãn này cho Frigate Plus", + "ask_full": "Đối tượng này có phải là một {{untranslatedLabel}} ({{translatedLabel}}) không?" + }, + "state": { + "submitted": "Đã gửi" + } + }, + "submitToPlus": { + "label": "Gửi lên Frigate+", + "desc": "Đối tượng xuất hiện ở các khu vực bạn muốn tránh không được xem là phát hiện sai. Gửi chúng lên dưới dạng phát hiện sai có thể làm mô hình bị nhầm lẫn." + } + }, + "video": { + "viewInHistory": "Xem lại trong Lịch sử" + } + }, + "export": { + "time": { + "fromTimeline": "Chọn từ Dòng thời gian", + "custom": "Tuỳ chọn", + "start": { + "title": "Thời gian bắt đầu", + "label": "Chọn thời gian bắt đầu" + }, + "end": { + "title": "Thời gian kết thúc", + "label": "Chọn thời gian kết thúc" + }, + "lastHour_other": "{{count}} giờ trước" + }, + "name": { + "placeholder": "Đặt tên cho bản xuất" + }, + "select": "Chọn", + "export": "Xuất", + "selectOrExport": "Chọn hay xuất", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Thời gian kết thúc phải sau thời gian bắt đầu", + "noVaildTimeSelected": "Chưa chọn khoảng thời gian hợp lệ", + "failed": "Không thể bắt đầu xuất: {{error}}" + }, + "success": "Đã bắt đầu xuất dữ liệu thành công. Xem tệp trên trang xuất dữ liệu.", + "view": "Xem" + }, + "fromTimeline": { + "saveExport": "Lưu bản xuất", + "previewExport": "Xem trước bản xuất" + } + }, + "streaming": { + "debugView": "Chế độ xem Gỡ lỗi", + "label": "Luồng", + "restreaming": { + "disabled": "Tính năng phát lại luồng không được bật cho camera này.", + "desc": { + "title": "Thiết lập go2rtc để có thêm tùy chọn xem trực tiếp và âm thanh cho camera này.", + "readTheDocumentation": "Đọc tài liệu" + } + }, + "showStats": { + "label": "Hiển thị số liệu thống kê luồng", + "desc": "Bật tùy chọn này để hiển thị số liệu thống kê luồng dưới dạng lớp phủ trên nguồn cấp dữ liệu camera." + } + }, + "recording": { + "confirmDelete": { + "title": "Xác nhận xóa", + "toast": { + "success": "Đoạn video liên quan đến các mục đánh giá đã chọn đã được xóa thành công.", + "error": "Không thể xóa: {{error}}" + }, + "desc": { + "selected": "Bạn có chắc chắn muốn xóa tất cả video đã ghi liên quan đến mục đánh giá này không?

    Giữ phím Shift để bỏ qua hộp thoại này trong tương lai." + } + }, + "button": { + "deleteNow": "Xóa ngay", + "export": "Xuất", + "markAsReviewed": "Đánh dấu là đã xem xét", + "markAsUnreviewed": "Đánh dấu là chưa xem xét" + } + }, + "search": { + "saveSearch": { + "success": "Tìm kiếm ({{searchName}}) đã được lưu.", + "button": { + "save": { + "label": "Lưu tìm kiếm này" + } + }, + "label": "Lưu tìm kiếm", + "desc": "Cung cấp tên cho tìm kiếm đã lưu này.", + "placeholder": "Nhập tên cho tìm kiếm của bạn", + "overwrite": "{{searchName}} đã tồn tại. Lưu sẽ ghi đè lên giá trị hiện có." + } + }, + "imagePicker": { + "selectImage": "Chọn hình thu nhỏ của đối tượng cần theo dõi", + "search": { + "placeholder": "Tìm theo nhãn hoặc nhãn phụ..." + }, + "noImages": "Không tìm thấy hình thu nhỏ cho camera này", + "unknownLabel": "Ảnh kích hoạt đã lưu" + } +} diff --git a/web/public/locales/vi/components/filter.json b/web/public/locales/vi/components/filter.json new file mode 100644 index 0000000..3678ba1 --- /dev/null +++ b/web/public/locales/vi/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Lọc", + "labels": { + "label": "Nhãn", + "all": { + "title": "Tất cả các nhãn", + "short": "Nhãn" + }, + "count_one": "{{count}} nhãn", + "count_other": "{{count}} nhãn" + }, + "features": { + "submittedToFrigatePlus": { + "tips": "Trước tiên, bạn phải lọc các đối tượng được theo dõi có ảnh chụp nhanh.

    Không thể gửi các đối tượng được theo dõi không có ảnh chụp nhanh tới Frigate+.", + "label": "Đã gửi tới Frigate+" + }, + "label": "Tính năng", + "hasSnapshot": "Có ảnh chụp nhanh", + "hasVideoClip": "Có video clip" + }, + "sort": { + "scoreDesc": "Điểm Đối tượng (Giảm dần)", + "speedAsc": "Tốc độ Ước tính (Tăng dần)", + "relevance": "Mức độ liên quan", + "label": "Sắp xếp", + "dateAsc": "Ngày (Tăng dần)", + "dateDesc": "Ngày (Giảm dần)", + "scoreAsc": "Điểm Đối tượng (Tăng dần)", + "speedDesc": "Tốc độ Ước tính (Giảm dần)" + }, + "cameras": { + "label": "Bộ lọc Camera", + "all": { + "title": "Tất cả Camera", + "short": "Camera" + } + }, + "review": { + "showReviewed": "Hiển thị đã xem xét" + }, + "motion": { + "showMotionOnly": "Chỉ Hiển thị Chuyển động" + }, + "explore": { + "settings": { + "title": "Cài đặt", + "defaultView": { + "title": "Chế độ xem Mặc định", + "desc": "Khi không có bộ lọc nào được chọn, hiển thị bản tóm tắt các đối tượng được theo dõi gần đây nhất cho mỗi nhãn, hoặc hiển thị lưới không được lọc.", + "summary": "Tóm tắt", + "unfilteredGrid": "Lưới không được lọc" + }, + "gridColumns": { + "title": "Cột Lưới", + "desc": "Chọn số lượng cột trong chế độ xem lưới." + }, + "searchSource": { + "label": "Nguồn Tìm kiếm", + "desc": "Chọn tìm kiếm hình thu nhỏ hay mô tả của các đối tượng được theo dõi của bạn.", + "options": { + "thumbnailImage": "Hình thu nhỏ", + "description": "Mô tả" + } + } + }, + "date": { + "selectDateBy": { + "label": "Chọn một ngày để lọc theo" + } + } + }, + "logSettings": { + "allLogs": "Tất cả nhật ký", + "label": "Lọc cấp độ nhật ký", + "filterBySeverity": "Lọc nhật ký theo mức độ nghiêm trọng", + "loading": { + "title": "Đang tải", + "desc": "Khi bảng nhật ký được cuộn xuống dưới cùng, nhật ký mới sẽ tự động truyền trực tuyến khi chúng được thêm vào." + }, + "disableLogStreaming": "Tắt truyền phát nhật ký" + }, + "trackedObjectDelete": { + "title": "Xác nhận Xóa", + "toast": { + "success": "Đã xóa thành công các đối tượng được theo dõi.", + "error": "Không thể xóa các đối tượng được theo dõi: {{errorMessage}}" + }, + "desc": "Việc xóa {{objectLength}} đối tượng được theo dõi này sẽ xóa ảnh chụp nhanh, mọi nội dung nhúng đã lưu và mọi mục nhập vòng đời đối tượng liên quan. Đoạn ghi hình đã ghi của các đối tượng được theo dõi này trong chế độ xem Lịch sử sẽ KHÔNG bị xóa.

    Bạn có chắc chắn muốn tiếp tục không?

    Giữ phím Shift để bỏ qua hộp thoại này trong tương lai." + }, + "recognizedLicensePlates": { + "selectPlatesFromList": "Chọn một hoặc nhiều biển số từ danh sách.", + "title": "Biển số xe được Nhận dạng", + "loadFailed": "Không thể tải biển số xe được nhận dạng.", + "loading": "Đang tải biển số xe được nhận dạng…", + "placeholder": "Nhập để tìm kiếm biển số xe…", + "noLicensePlatesFound": "Không tìm thấy biển số xe nào.", + "selectAll": "Chọn tất cả", + "clearAll": "Xóa tất cả" + }, + "more": "Thêm Bộ lọc", + "reset": { + "label": "Đặt lại bộ lọc về giá trị mặc định" + }, + "timeRange": "Phạm vi Thời gian", + "subLabels": { + "label": "Nhãn phụ", + "all": "Tất cả Nhãn phụ" + }, + "score": "Điểm", + "estimatedSpeed": "Tốc độ Ước tính ({{unit}})", + "dates": { + "selectPreset": "Chọn thiết lập sẵn…", + "all": { + "title": "Tất cả Ngày", + "short": "Ngày" + } + }, + "zoneMask": { + "filterBy": "Lọc theo mặt nạ khu vực" + }, + "zones": { + "label": "Khu vực", + "all": { + "title": "Tất cả Khu vực", + "short": "Khu vực" + } + }, + "classes": { + "label": "Các nhãn nhận diện", + "all": { + "title": "Tất cả nhãn nhận diện" + }, + "count_one": "{{count}} Nhãn nhận diện", + "count_other": "{{count}} Các nhãn nhận diện" + } +} diff --git a/web/public/locales/vi/components/icons.json b/web/public/locales/vi/components/icons.json new file mode 100644 index 0000000..666736e --- /dev/null +++ b/web/public/locales/vi/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Chọn một biểu tượng", + "search": { + "placeholder": "Tìm kiếm một biểu tượng…" + } + } +} diff --git a/web/public/locales/vi/components/input.json b/web/public/locales/vi/components/input.json new file mode 100644 index 0000000..c12a614 --- /dev/null +++ b/web/public/locales/vi/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Tải xuống video", + "toast": { + "success": "Video trong mục xem lại của bạn đã bắt đầu tải xuống." + } + } + } +} diff --git a/web/public/locales/vi/components/player.json b/web/public/locales/vi/components/player.json new file mode 100644 index 0000000..3ce29ac --- /dev/null +++ b/web/public/locales/vi/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Không tìm thấy bản ghi nào cho thời điểm này", + "noPreviewFound": "Không tìm thấy bản xem trước", + "noPreviewFoundFor": "Không tìm thấy bản xem trước cho {{cameraName}}", + "stats": { + "droppedFrameRate": "Tỷ lệ khung hình bị rớt:", + "droppedFrames": { + "short": { + "title": "Bị rớt", + "value": "{{droppedFrames}} khung hình" + }, + "title": "Khung hình bị rớt:" + }, + "decodedFrames": "Khung hình đã giải mã:", + "latency": { + "short": { + "value": "{{seconds}} giây", + "title": "Độ trễ" + }, + "title": "Độ trễ:", + "value": "{{seconds}} giây" + }, + "totalFrames": "Tổng số khung hình:", + "streamType": { + "title": "Loại luồng:", + "short": "Loại" + }, + "bandwidth": { + "title": "Băng thông:", + "short": "Băng thông" + } + }, + "toast": { + "success": { + "submittedFrigatePlus": "Đã gửi thành công khung hình tới Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Không gửi được khung hình tới Frigate+" + } + }, + "submitFrigatePlus": { + "title": "Gửi khung hình này tới Frigate+?", + "submit": "Gửi" + }, + "livePlayerRequiredIOSVersion": "Yêu cầu iOS 17.1 trở lên cho loại luồng trực tiếp này.", + "streamOffline": { + "title": "Luồng ngoại tuyến", + "desc": "Không nhận được khung hình nào trên luồng detect của {{cameraName}}, hãy kiểm tra nhật ký lỗi" + }, + "cameraDisabled": "Camera đã bị tắt" +} diff --git a/web/public/locales/vi/config/cameras.json b/web/public/locales/vi/config/cameras.json new file mode 100644 index 0000000..5ffe9a2 --- /dev/null +++ b/web/public/locales/vi/config/cameras.json @@ -0,0 +1,11 @@ +{ + "label": "CameraConfig", + "name": { + "label": "Tên máy ảnh", + "description": "Tên camera là bắt buộc" + }, + "friendly_name": { + "label": "Tên thân thiện", + "description": "Tên hiển thị thân thiện với camera được sử dụng trong giao diện người dùng Frigate" + } +} diff --git a/web/public/locales/vi/config/global.json b/web/public/locales/vi/config/global.json new file mode 100644 index 0000000..71a2e65 --- /dev/null +++ b/web/public/locales/vi/config/global.json @@ -0,0 +1,13 @@ +{ + "version": { + "label": "Phiên bản cấu hình hiện tại", + "description": "Phiên bản dạng số hoặc chuỗi của cấu hình hiện tại giúp phát hiện các thay đổi về định dạng hoặc quá trình di chuyển dữ liệu." + }, + "safe_mode": { + "label": "Chế độ an toàn", + "description": "Khi được kích hoạt, hãy khởi động Frigate ở chế độ an toàn với các tính năng bị hạn chế để khắc phục sự cố." + }, + "environment_vars": { + "label": "Biến môi trường" + } +} diff --git a/web/public/locales/vi/config/groups.json b/web/public/locales/vi/config/groups.json new file mode 100644 index 0000000..8265713 --- /dev/null +++ b/web/public/locales/vi/config/groups.json @@ -0,0 +1,12 @@ +{ + "audio": { + "global": { + "detection": "Phát hiện toàn cục", + "sensitivity": "Độ nhạy toàn cục" + }, + "cameras": { + "detection": "Phát hiện", + "sensitivity": "Độ nhạy" + } + } +} diff --git a/web/public/locales/vi/config/validation.json b/web/public/locales/vi/config/validation.json new file mode 100644 index 0000000..e8f9598 --- /dev/null +++ b/web/public/locales/vi/config/validation.json @@ -0,0 +1,6 @@ +{ + "minimum": "Phải có giá trị tối thiểu là {{limit}}", + "maximum": "Tối đa phải là {{limit}}", + "exclusiveMinimum": "Phải lớn hơn {{limit}}", + "exclusiveMaximum": "Phải nhỏ hơn {{limit}}" +} diff --git a/web/public/locales/vi/objects.json b/web/public/locales/vi/objects.json new file mode 100644 index 0000000..d62b4b3 --- /dev/null +++ b/web/public/locales/vi/objects.json @@ -0,0 +1,120 @@ +{ + "mouse": "Chuột nhắt", + "keyboard": "Bàn phím nhạc", + "blender": "Máy xay", + "sink": "Bồn rửa", + "animal": "Động vật", + "dog": "Chó", + "bark": "Sủa", + "cat": "Mèo", + "horse": "Ngựa", + "goat": "Dê", + "sheep": "Cừu", + "bird": "Chim", + "vehicle": "Phương tiện", + "boat": "Thuyền", + "car": "Xe ô tô", + "bus": "Xe buýt", + "motorcycle": "Xe máy", + "train": "Tàu hỏa", + "bicycle": "Xe đạp", + "skateboard": "Ván trượt", + "door": "Cửa", + "hair_dryer": "Máy sấy tóc", + "toothbrush": "Bàn chải", + "scissors": "Kéo cắt", + "clock": "Đồng hồ", + "person": "Người", + "airplane": "Máy bay", + "zebra": "Ngựa vằn", + "tennis_racket": "Vợt tennis", + "plate": "Đĩa", + "wine_glass": "Ly rượu vang", + "cup": "Cốc", + "fork": "Nĩa", + "knife": "Dao", + "spoon": "Thìa", + "bowl": "Bát", + "banana": "Chuối", + "apple": "Táo", + "sandwich": "Bánh mì kẹp", + "orange": "Cam", + "broccoli": "Bông cải xanh", + "carrot": "Cà rốt", + "hot_dog": "Xúc xích", + "pizza": "Pizza", + "donut": "Bánh rán", + "chair": "Ghế", + "couch": "Ghế sofa", + "potted_plant": "Cây trồng trong chậu", + "bed": "Giường", + "mirror": "Gương", + "window": "Cửa sổ", + "desk": "Bàn làm việc", + "toilet": "Nhà vệ sinh", + "tv": "Ti vi", + "microwave": "Lò vi sóng", + "oven": "Lò nướng", + "toaster": "Máy nướng bánh mì", + "refrigerator": "Tủ lạnh", + "book": "Sách", + "face": "Mặt", + "license_plate": "Biển số xe", + "package": "Gói hàng", + "bbq_grill": "Vỉ nướng BBQ", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZ Post", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD", + "traffic_light": "Đèn giao thông", + "fire_hydrant": "Trụ cứu hỏa", + "street_sign": "Biển báo đường phố", + "stop_sign": "Biển báo dừng", + "parking_meter": "Đồng hồ đỗ xe", + "bench": "Ghế dài", + "cow": "Bò", + "elephant": "Voi", + "bear": "Gấu", + "giraffe": "Hươu cao cổ", + "hat": "Mũ", + "backpack": "Ba lô", + "umbrella": "Ô", + "shoe": "Giày", + "snowboard": "Ván trượt tuyết", + "eye_glasses": "Kính mắt", + "handbag": "Túi xách", + "tie": "Cà vạt", + "suitcase": "Va li", + "frisbee": "Đĩa ném", + "skis": "Ván trượt tuyết", + "sports_ball": "Bóng thể thao", + "kite": "Diều", + "baseball_bat": "Gậy bóng chày", + "baseball_glove": "Găng tay bóng chày", + "surfboard": "Ván lướt sóng", + "bottle": "Chai", + "cake": "Bánh ngọt", + "dining_table": "Bàn ăn", + "laptop": "Máy tính xách tay", + "remote": "Điều khiển từ xa", + "cell_phone": "Điện thoại di động", + "vase": "Bình hoa", + "teddy_bear": "Gấu bông", + "hair_brush": "Lược chải tóc", + "squirrel": "Sóc", + "deer": "Hươu", + "fox": "Cáo", + "rabbit": "Thỏ", + "raccoon": "Gấu mèo", + "robot_lawnmower": "Máy cắt cỏ robot", + "waste_bin": "Thùng rác", + "on_demand": "Theo yêu cầu" +} diff --git a/web/public/locales/vi/views/chat.json b/web/public/locales/vi/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/vi/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/vi/views/classificationModel.json b/web/public/locales/vi/views/classificationModel.json new file mode 100644 index 0000000..666a72f --- /dev/null +++ b/web/public/locales/vi/views/classificationModel.json @@ -0,0 +1,61 @@ +{ + "documentTitle": "شمار بندی کے ماڈل", + "button": { + "deleteClassificationAttempts": "Xóa Hình Ảnh Phân Loại", + "renameCategory": "Đổi Tên Lớp", + "deleteCategory": "Xoá Lớp", + "deleteImages": "Xoá Hình Ảnh", + "trainModel": "Huấn Luyện Mô Hình", + "addClassification": "Thêm Phân Loại", + "deleteModels": "Xoá Mô Hình", + "editModel": "Chỉnh sửa mô hình" + }, + "toast": { + "success": { + "deletedCategory_other": "Lớp Đã Bị Xoá", + "deletedImage_other": "Hình ảnh đã bị xóa", + "deletedModel_other": "Đã xóa thành công {{count}} mô hình", + "categorizedImage": "Phân Loại Hình Ảnh Thành Công", + "trainedModel": "Đã huấn luyện mô hình thành công.", + "trainingModel": "Đã bắt đầu huấn luyện mô hình thành công.", + "updatedModel": "Đã cập nhật cấu hình mô hình thành công", + "renamedCategory": "Đã đổi tên lớp thành công thành {{name}}" + }, + "error": { + "deleteImageFailed": "Xóa không thành công: {{errorMessage}}", + "deleteCategoryFailed": "Xóa lớp không thành công: {{errorMessage}}", + "deleteModelFailed": "Xóa mô hình không thành công: {{errorMessage}}", + "categorizeFailed": "Phân loại hình ảnh không thành công: {{errorMessage}}", + "trainingFailed": "Huấn luyện mô hình thất bại. Vui lòng kiểm tra nhật ký của Frigate để biết chi tiết.", + "trainingFailedToStart": "Khởi động huấn luyện mô hình không thành công: {{errorMessage}}", + "updateModelFailed": "Cập nhật mô hình không thành công: {{errorMessage}}", + "renameCategoryFailed": "Không đổi tên được lớp: {{errorMessage}}" + } + }, + "details": { + "scoreInfo": "Điểm số cho biết mức độ tự tin trung bình mà hệ thống xác định được cho tất cả các lần phát hiện đối tượng này.", + "none": "Không có", + "unknown": "Không rõ" + }, + "tooltip": { + "trainingInProgress": "Mô hình hiện đang được huấn luyện", + "noNewImages": "Không có hình ảnh mới để đào tạo. Trước tiên, hãy phân loại nhiều hình ảnh hơn trong tập dữ liệu.", + "noChanges": "Không có thay đổi nào đối với tập dữ liệu kể từ lần đào tạo cuối cùng.", + "modelNotReady": "Mô hình chưa sẵn sàng để huấn luyện" + }, + "deleteCategory": { + "title": "Xóa lớp", + "desc": "Bạn có chắc chắn muốn xóa lớp {{name}} không? Điều này sẽ xóa vĩnh viễn tất cả các hình ảnh liên quan và yêu cầu đào tạo lại mô hình.", + "minClassesTitle": "Không thể xóa lớp", + "minClassesDesc": "Một mô hình phân loại phải có ít nhất 2 lớp. Thêm một lớp khác trước khi xóa lớp này." + }, + "deleteModel": { + "title": "Xóa mô hình phân loại", + "single": "Bạn có chắc chắn muốn xóa {{name}} không? Thao tác này sẽ xóa vĩnh viễn tất cả dữ liệu liên quan bao gồm hình ảnh và dữ liệu đào tạo. Không thể hoàn tác hành động này.", + "desc_other": "Bạn có chắc chắn muốn xóa mô hình {{count}} không? Thao tác này sẽ xóa vĩnh viễn tất cả dữ liệu liên quan bao gồm hình ảnh và dữ liệu đào tạo. Không thể hoàn tác hành động này." + }, + "edit": { + "title": "Chỉnh sửa mô hình phân loại", + "descriptionState": "Chỉnh sửa các lớp cho mô hình phân loại trạng thái này. Những thay đổi sẽ yêu cầu đào tạo lại mô hình." + } +} diff --git a/web/public/locales/vi/views/configEditor.json b/web/public/locales/vi/views/configEditor.json new file mode 100644 index 0000000..a2ffce4 --- /dev/null +++ b/web/public/locales/vi/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "copyConfig": "Sao chép cấu hình", + "saveAndRestart": "Lưu & Khởi động lại", + "saveOnly": "Chỉ lưu", + "confirm": "Thoát mà không lưu?", + "toast": { + "error": { + "savingError": "Lỗi khi lưu cấu hình" + }, + "success": { + "copyToClipboard": "Đã sao chép cấu hình vào bộ nhớ tạm." + } + }, + "configEditor": "Trình chỉnh sửa cấu hình", + "documentTitle": "Trình chỉnh sửa - Frigate", + "safeConfigEditor": "Chỉnh sửa cấu hình (Chế độ an toàn)", + "safeModeDescription": "Frigate đang ở chế độ an toàn do lỗi kiểm tra cấu hình." +} diff --git a/web/public/locales/vi/views/events.json b/web/public/locales/vi/views/events.json new file mode 100644 index 0000000..94b2bc7 --- /dev/null +++ b/web/public/locales/vi/views/events.json @@ -0,0 +1,62 @@ +{ + "camera": "Tiếng máy ảnh", + "alerts": "Cảnh báo", + "detections": "Phát hiện", + "motion": { + "only": "Chỉ chuyển động", + "label": "Chuyển động" + }, + "allCameras": "Tất cả Camera", + "detected": "Đã phát hiện", + "recordings": { + "documentTitle": "Bản ghi - Frigate" + }, + "events": { + "aria": "Chọn sự kiện", + "label": "Sự kiện", + "noFoundForTimePeriod": "Không tìm thấy sự kiện nào trong khoảng thời gian này." + }, + "timeline.aria": "Chọn dòng thời gian", + "selected_one": "{{count}} đã chọn", + "selected_other": "{{count}} đã chọn", + "empty": { + "alert": "Không có cảnh báo nào để xem xét", + "detection": "Không có phát hiện nào để xem xét", + "motion": "Không tìm thấy dữ liệu chuyển động" + }, + "timeline": "Dòng thời gian", + "documentTitle": "Xem lại - Frigate", + "calendarFilter": { + "last24Hours": "24 giờ qua" + }, + "newReviewItems": { + "label": "Xem các mục mới cần xem xét", + "button": "Các mục mới cần xem xét" + }, + "markAsReviewed": "Đánh dấu là đã xem xét", + "markTheseItemsAsReviewed": "Đánh dấu các mục này là đã xem xét", + "suspiciousActivity": "Hoạt động đáng ngờ", + "threateningActivity": "Hoạt động đe dọa", + "zoomIn": "Phóng To", + "zoomOut": "Thu nhỏ", + "detail": { + "label": "Chi tiết", + "noDataFound": "Không có dữ liệu chi tiết để xem xét", + "aria": "Chuyển đổi chế độ xem chi tiết", + "trackedObject_one": "{{count}} đối tượng", + "trackedObject_other": "{{count}} đối tượng", + "noObjectDetailData": "Không có dữ liệu chi tiết đối tượng nào khả dụng.", + "settings": "Cài đặt chế độ xem chi tiết", + "alwaysExpandActive": { + "title": "Luôn mở rộng mục đang hoạt động", + "desc": "Luôn mở rộng chi tiết đối tượng của mục đánh giá đang hoạt động khi có sẵn." + } + }, + "objectTrack": { + "trackedPoint": "Điểm theo dõi", + "clickToSeek": "Nhấn để tua đến thời điểm này" + }, + "normalActivity": "Bình thường", + "needsReview": "Cần xem xét", + "securityConcern": "Mối lo ngại về an ninh" +} diff --git a/web/public/locales/vi/views/explore.json b/web/public/locales/vi/views/explore.json new file mode 100644 index 0000000..7110009 --- /dev/null +++ b/web/public/locales/vi/views/explore.json @@ -0,0 +1,291 @@ +{ + "objectLifecycle": { + "lifecycleItemDesc": { + "header": { + "zones": "Vùng", + "ratio": "Tỷ lệ", + "area": "Diện tích" + }, + "gone": "{{label}} đã rời đi", + "heard": "{{label}} đã nghe thấy", + "external": "{{label}} được phát hiện", + "visible": "{{label}} được phát hiện", + "entered_zone": "{{label}} đã đi vào {{zones}}", + "active": "{{label}} trở nên hoạt động", + "stationary": "{{label}} trở nên đứng yên", + "attribute": { + "faceOrLicense_plate": "Đã phát hiện {{attribute}} cho {{label}}", + "other": "{{label}} được nhận dạng là {{attribute}}" + } + }, + "carousel": { + "previous": "Trang trước", + "next": "Trang tiếp theo" + }, + "annotationSettings": { + "showAllZones": { + "desc": "Luôn hiển thị các vùng trên các khung hình mà đối tượng đã đi vào.", + "title": "Hiển thị tất cả các Vùng" + }, + "offset": { + "millisecondsToOffset": "Số mili giây để lệch các chú thích phát hiện. Mặc định: 0", + "documentation": "Đọc tài liệu ", + "desc": "Dữ liệu này đến từ luồng phát hiện của camera nằm ở trên hình ảnh từ luồng ghi hình. Có khả năng hai luồng này không đồng bộ hoàn hảo. Do đó, hộp giới hạn và đoạn ghi hình sẽ không khớp nhau một cách hoàn hảo. Tuy nhiên, trường annotation_offset có thể được sử dụng để điều chỉnh điều này.", + "label": "Độ lệch Chú thích", + "tips": "MẸO: Hãy tưởng tượng có một clip sự kiện với một người đi từ trái sang phải. Nếu hộp giới hạn trên dòng thời gian sự kiện luôn ở bên trái của người đó thì giá trị nên được giảm xuống. Tương tự, nếu một người đi từ trái sang phải và hộp giới hạn luôn ở phía trước người đó thì giá trị nên được tăng lên.", + "toast": { + "success": "Độ lệch chú thích cho {{camera}} đã được lưu vào tệp cấu hình. Khởi động lại Frigate để áp dụng các thay đổi của bạn." + } + }, + "title": "Cài đặt Chú thích" + }, + "count": "{{first}} trên {{second}}", + "title": "Vòng đời Đối tượng", + "noImageFound": "Không tìm thấy hình ảnh cho dấu thời gian này.", + "createObjectMask": "Tạo Mặt nạ Đối tượng", + "adjustAnnotationSettings": "Điều chỉnh cài đặt chú thích", + "trackedPoint": "Điểm được theo dõi", + "scrollViewTips": "Cuộn để xem những khoảnh khắc quan trọng trong vòng đời của đối tượng này.", + "autoTrackingTips": "Vị trí hộp giới hạn sẽ không chính xác đối với camera quay tự động theo dõi." + }, + "details": { + "item": { + "title": "Chi tiết Mục Xem lại", + "desc": "Chi tiết mục xem lại", + "button": { + "share": "Chia sẻ mục xem lại này", + "viewInExplore": "Xem trong Khám phá" + }, + "toast": { + "error": { + "updatedSublabelFailed": "Không thể cập nhật nhãn phụ: {{errorMessage}}", + "updatedLPRFailed": "Không thể cập nhật biển số xe: {{errorMessage}}", + "regenerate": "Không thể gọi {{provider}} để lấy mô tả mới: {{errorMessage}}", + "audioTranscription": "Không thể yêu cầu phiên âm: {{errorMessage}}" + }, + "success": { + "regenerate": "Một mô tả mới đã được yêu cầu từ {{provider}}. Tùy thuộc vào tốc độ của nhà cung cấp của bạn, mô tả mới có thể mất một chút thời gian để tạo lại.", + "updatedLPR": "Cập nhật biển số xe thành công.", + "updatedSublabel": "Cập nhật nhãn phụ thành công.", + "audioTranscription": "Đã yêu cầu chuyển đổi âm thanh thành văn bản thành công. Tùy vào tốc độ của máy chủ Frigate, quá trình chuyển đổi có thể mất một khoảng thời gian để hoàn tất." + } + }, + "tips": { + "mismatch_other": "{{count}} đối tượng không khả dụng đã được phát hiện và bao gồm trong mục xem lại này. Những đối tượng đó hoặc không đủ điều kiện là một cảnh báo hoặc phát hiện hoặc đã được dọn dẹp/xóa.", + "hasMissingObjects": "Điều chỉnh cấu hình của bạn nếu bạn muốn Frigate lưu các đối tượng được theo dõi cho các nhãn sau: {{objects}}" + } + }, + "expandRegenerationMenu": "Mở rộng menu tạo lại", + "label": "Nhãn", + "editSubLabel": { + "title": "Chỉnh sửa nhãn phụ", + "desc": "Nhập một nhãn phụ mới cho {{label}} này", + "descNoLabel": "Nhập một nhãn phụ mới cho đối tượng được theo dõi này" + }, + "snapshotScore": { + "label": "Điểm Ảnh chụp nhanh" + }, + "topScore": { + "info": "Điểm cao nhất là điểm trung vị cao nhất cho đối tượng được theo dõi, vì vậy điểm này có thể khác với điểm được hiển thị trên ảnh thu nhỏ của kết quả tìm kiếm.", + "label": "Điểm Cao nhất" + }, + "estimatedSpeed": "Tốc độ ước tính", + "objects": "Đối tượng", + "timestamp": "Dấu thời gian", + "button": { + "findSimilar": "Tìm đối tượng tương tự", + "regenerate": { + "title": "Tạo lại", + "label": "Tạo lại mô tả đối tượng được theo dõi" + } + }, + "description": { + "label": "Mô tả", + "placeholder": "Mô tả của đối tượng được theo dõi", + "aiTips": "Frigate sẽ không yêu cầu mô tả từ nhà cung cấp AI tạo sinh của bạn cho đến khi vòng đời của đối tượng được theo dõi kết thúc." + }, + "tips": { + "descriptionSaved": "Lưu mô tả thành công", + "saveDescriptionFailed": "Không thể cập nhật mô tả: {{errorMessage}}" + }, + "camera": "Camera", + "recognizedLicensePlate": "Biển số xe được nhận dạng", + "regenerateFromSnapshot": "Tạo lại từ Ảnh chụp nhanh", + "regenerateFromThumbnails": "Tạo lại từ Ảnh thu nhỏ", + "zones": "Vùng", + "editLPR": { + "title": "Chỉnh sửa biển số xe", + "desc": "Nhập một giá trị biển số xe mới cho {{label}} này", + "descNoLabel": "Nhập một giá trị biển số xe mới cho đối tượng được theo dõi này" + }, + "score": { + "label": "Điểm tin cậy" + } + }, + "itemMenu": { + "viewObjectLifecycle": { + "label": "Xem vòng đời đối tượng", + "aria": "Hiển thị vòng đời đối tượng" + }, + "downloadSnapshot": { + "label": "Tải xuống ảnh chụp nhanh", + "aria": "Tải xuống ảnh chụp nhanh" + }, + "downloadVideo": { + "aria": "Tải xuống video", + "label": "Tải xuống video" + }, + "findSimilar": { + "label": "Tìm đối tượng tương tự", + "aria": "Tìm các đối tượng được theo dõi tương tự" + }, + "submitToPlus": { + "aria": "Gửi đến Frigate Plus", + "label": "Gửi đến Frigate+" + }, + "viewInHistory": { + "label": "Xem trong Lịch sử", + "aria": "Xem trong Lịch sử" + }, + "deleteTrackedObject": { + "label": "Xóa đối tượng được theo dõi này" + }, + "addTrigger": { + "label": "Thêm sự kiện kích hoạt", + "aria": "Thêm trình kích hoạt cho đối tượng được theo dõi này" + }, + "audioTranscription": { + "label": "Phiên âm", + "aria": "Yêu cầu phiên âm" + }, + "downloadCleanSnapshot": { + "label": "Tải xuống ảnh chụp nhanh", + "aria": "Tải xuống ảnh chụp nhanh" + }, + "viewTrackingDetails": { + "label": "Xem chi tiết theo dõi", + "aria": "Xem chi tiết theo dõi" + }, + "showObjectDetails": { + "label": "Hiển thị đường dẫn đối tượng" + }, + "hideObjectDetails": { + "label": "Ẩn đường dẫn đối tượng" + } + }, + "exploreIsUnavailable": { + "embeddingsReindexing": { + "step": { + "descriptionsEmbedded": "Mô tả đã được nhúng: ", + "trackedObjectsProcessed": "Đối tượng được theo dõi đã xử lý: ", + "thumbnailsEmbedded": "Ảnh thu nhỏ đã được nhúng: " + }, + "finishingShortly": "Sẽ hoàn thành trong giây lát", + "context": "Tính năng Khám phá có thể được sử dụng sau khi quá trình tái lập chỉ mục dữ liệu nhúng (embeddings) của đối tượng được theo dõi hoàn tất.", + "startingUp": "Đang khởi động…", + "estimatedTime": "Thời gian ước tính còn lại:" + }, + "downloadingModels": { + "context": "Frigate đang tải xuống các mô hình dữ liệu nhúng cần thiết để hỗ trợ tính năng Tìm kiếm theo Ngữ nghĩa. Quá trình này có thể mất vài phút tùy thuộc vào tốc độ kết nối mạng của bạn.", + "setup": { + "visionModel": "Mô hình thị giác", + "visionModelFeatureExtractor": "Trình trích xuất đặc trưng mô hình thị giác", + "textModel": "Mô hình văn bản", + "textTokenizer": "Trình mã hóa văn bản" + }, + "tips": { + "context": "Bạn có thể muốn tái lập chỉ mục dữ liệu nhúng của các đối tượng được theo dõi sau khi các mô hình được tải xuống.", + "documentation": "Đọc tài liệu" + }, + "error": "Đã xảy ra lỗi. Vui lòng kiểm tra nhật ký của Frigate." + }, + "title": "Tính năng Khám phá không khả dụng" + }, + "dialog": { + "confirmDelete": { + "desc": "Việc xóa đối tượng được theo dõi này sẽ xóa ảnh chụp nhanh, mọi phần nhúng đã lưu và mọi mục nhập chi tiết theo dõi được liên kết. Đoạn phim đã ghi của đối tượng được theo dõi này trong chế độ xem Lịch sử sẽ KHÔNG bị xóa.

    Bạn có chắc chắn muốn tiếp tục không?", + "title": "Xác nhận Xóa" + } + }, + "noTrackedObjects": "Không tìm thấy Đối tượng được theo dõi nào", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Đã xóa đối tượng được theo dõi thành công.", + "error": "Không thể xóa đối tượng được theo dõi: {{errorMessage}}" + } + }, + "tooltip": "Khớp {{type}} ở mức {{confidence}}%", + "previousTrackedObject": "Đối tượng được theo dõi trước đó", + "nextTrackedObject": "Đối tượng được theo dõi tiếp theo" + }, + "exploreMore": "Khám phá thêm các đối tượng {{label}}", + "trackedObjectDetails": "Chi tiết Đối tượng được theo dõi", + "type": { + "details": "chi tiết", + "snapshot": "ảnh chụp nhanh", + "video": "video", + "object_lifecycle": "vòng đời đối tượng", + "thumbnail": "Ảnh thu nhỏ", + "tracking_details": "chi tiết theo dõi" + }, + "fetchingTrackedObjectsFailed": "Lỗi khi tìm nạp các đối tượng được theo dõi: {{errorMessage}}", + "documentTitle": "Khám phá - Frigate", + "generativeAI": "AI Tạo sinh", + "trackedObjectsCount_other": "{{count}} đối tượng được theo dõi ", + "aiAnalysis": { + "title": "Phân tích bằng AI" + }, + "concerns": { + "label": "Mối lo ngại" + }, + "trackingDetails": { + "title": "Chi tiết theo dõi", + "noImageFound": "Không tìm thấy hình ảnh cho mốc thời gian này.", + "createObjectMask": "Tạo mặt nạ đối tượng", + "adjustAnnotationSettings": "Điều chỉnh cài đặt chú thích", + "scrollViewTips": "Nhấn để xem những khoảnh khắc quan trọng trong vòng đời của đối tượng này.", + "autoTrackingTips": "Vị trí khung bao sẽ không chính xác đối với các camera tự động theo dõi (autotracking).", + "count": "{{first}} của {{second}}", + "trackedPoint": "Điểm theo dõi", + "lifecycleItemDesc": { + "visible": "Đã phát hiện được {{label}}", + "entered_zone": "{{label}} đã vào {{zones}}", + "active": "{{label}} đã hoạt động", + "stationary": "{{label}} đã đứng yên", + "attribute": { + "faceOrLicense_plate": "Đã phát hiện {{attribute}} đối với {{label}}", + "other": "{{label}} được nhận diện là {{attribute}}" + }, + "gone": "{{label}} đã rời đi", + "heard": "Đã nghe thấy {{label}}", + "external": "{{label}} đã được nhận diện", + "header": { + "zones": "Vùng", + "ratio": "Tỷ lệ", + "area": "Khu vực", + "score": "Điểm" + } + }, + "annotationSettings": { + "title": "Cài đặt chú thích", + "showAllZones": { + "title": "Hiện tất cả các vùng", + "desc": "Luôn hiển thị các vùng trên khung hình khi có đối tượng đi vào vùng đó." + }, + "offset": { + "label": "Độ lệch chú thích", + "desc": "Dữ liệu này lấy từ luồng phát hiện (detect feed) của camera bạn, nhưng được hiển thị chồng lên hình ảnh từ luồng ghi hình (record feed). Hai luồng này thường không đồng bộ hoàn hảo với nhau. Do đó, khung bao (bounding box) và đoạn video có thể không khớp chính xác. Bạn có thể sử dụng cài đặt này để điều chỉnh thời gian hiển thị chú thích (annotation) lùi hoặc tiến để đồng bộ tốt hơn với video đã ghi.", + "millisecondsToOffset": "Số mili giây để điều chỉnh thời gian hiển thị chú thích phát hiện. Mặc định: 0", + "tips": "Giảm giá trị nếu quá trình phát lại video ở phía trước các hộp và điểm đường dẫn, đồng thời tăng giá trị nếu quá trình phát lại video ở phía sau chúng. Giá trị này có thể âm.", + "toast": { + "success": "Độ lệch chú thích cho {{camera}} đã được lưu vào tệp cấu hình." + } + } + }, + "carousel": { + "previous": "Trang trình bày trước", + "next": "Trang trình bày tiếp theo" + } + } +} diff --git a/web/public/locales/vi/views/exports.json b/web/public/locales/vi/views/exports.json new file mode 100644 index 0000000..21b5065 --- /dev/null +++ b/web/public/locales/vi/views/exports.json @@ -0,0 +1,27 @@ +{ + "search": "Tìm kiếm", + "documentTitle": "Xuất tệp - Frigate", + "noExports": "Không tìm thấy tệp xuất nào", + "deleteExport": "Xóa tệp xuất", + "deleteExport.desc": "Bạn có chắc chắn muốn xóa {{exportName}} không?", + "editExport": { + "title": "Đổi tên tệp xuất", + "desc": "Nhập tên mới cho tệp xuất này.", + "saveExport": "Lưu tệp xuất" + }, + "toast": { + "error": { + "renameExportFailed": "Đổi tên tệp xuất thất bại: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Chia sẻ bản xuất", + "downloadVideo": "Tải video", + "editName": "Chỉnh sửa tên", + "deleteExport": "Xóa bản xuất" + }, + "headings": { + "cases": "Các trường hợp", + "uncategorizedExports": "Xuất chưa được phân loại" + } +} diff --git a/web/public/locales/vi/views/faceLibrary.json b/web/public/locales/vi/views/faceLibrary.json new file mode 100644 index 0000000..6301b6c --- /dev/null +++ b/web/public/locales/vi/views/faceLibrary.json @@ -0,0 +1,97 @@ +{ + "selectItem": "Chọn mục {{item}}", + "description": { + "addFace": "Thêm một bộ sưu tập mới vào Thư viện Khuôn Mặt bằng cách tải lên hình ảnh đầu tiên của bạn.", + "invalidName": "Tên không hợp lệ. Tên chỉ được phép chứa chữ cái, số, khoảng trắng, dấu nháy đơn, dấu gạch dưới và dấu gạch ngang.", + "placeholder": "Nhập tên cho bộ sưu tập này", + "nameCannotContainHash": "Tên không được chứa ký tự #." + }, + "details": { + "person": "Người", + "unknown": "Không rõ", + "subLabelScore": "Điểm nhãn phụ", + "scoreInfo": "Điểm nhãn phụ là điểm số có trọng số cho tất cả các độ tin cậy của khuôn mặt được nhận dạng, vì vậy điểm này có thể khác với điểm hiển thị trên ảnh chụp nhanh.", + "timestamp": "Dấu thời gian", + "face": "Chi tiết khuôn mặt", + "faceDesc": "Chi tiết của đối tượng được theo dõi đã tạo ra khuôn mặt này" + }, + "renameFace": { + "title": "Đổi tên khuôn mặt", + "desc": "Nhập tên mới cho {{name}}" + }, + "button": { + "renameFace": "Đổi tên khuôn mặt", + "deleteFaceAttempts": "Xóa khuôn mặt", + "addFace": "Thêm khuôn mặt", + "deleteFace": "Xóa khuôn mặt", + "uploadImage": "Tải lên hình ảnh", + "reprocessFace": "Xử lý lại khuôn mặt" + }, + "imageEntry": { + "dropActive": "Thả hình ảnh vào đây…", + "validation": { + "selectImage": "Vui lòng chọn một tệp hình ảnh." + }, + "dropInstructions": "Kéo và thả hình ảnh vào đây, hoặc nhấp để chọn", + "maxSize": "Kích thước tối đa: {{size}}MB" + }, + "toast": { + "success": { + "uploadedImage": "Tải lên hình ảnh thành công.", + "trainedFace": "Huấn luyện khuôn mặt thành công.", + "updatedFaceScore": "Đã cập nhật thành công điểm khuôn mặt thành {{name}} ({{score}}).", + "addFaceLibrary": "{{name}} đã được thêm thành công vào Thư viện Khuôn mặt!", + "deletedFace_other": "Đã xóa thành công {{count}} khuôn mặt.", + "deletedName_other": "{{count}} khuôn mặt đã được xóa thành công.", + "renamedFace": "Đổi tên khuôn mặt thành {{name}} thành công" + }, + "error": { + "uploadingImageFailed": "Tải lên hình ảnh thất bại: {{errorMessage}}", + "addFaceLibraryFailed": "Đặt tên khuôn mặt thất bại: {{errorMessage}}", + "renameFaceFailed": "Đổi tên khuôn mặt thất bại: {{errorMessage}}", + "deleteFaceFailed": "Xóa thất bại: {{errorMessage}}", + "deleteNameFailed": "Xóa tên thất bại: {{errorMessage}}", + "trainFailed": "Huấn luyện thất bại: {{errorMessage}}", + "updateFaceScoreFailed": "Cập nhật điểm khuôn mặt thất bại: {{errorMessage}}" + } + }, + "collections": "Bộ sưu tập", + "steps": { + "description": { + "uploadFace": "Tải lên một hình ảnh của {{name}} cho thấy khuôn mặt của họ từ góc nhìn trực diện. Hình ảnh không cần phải được cắt chỉ lấy khuôn mặt." + }, + "faceName": "Nhập tên khuôn mặt", + "uploadFace": "Tải lên hình ảnh khuôn mặt", + "nextSteps": "Các bước tiếp theo" + }, + "deleteFaceLibrary": { + "title": "Xóa tên", + "desc": "Bạn có chắc chắn muốn xóa bộ sưu tập {{name}} không? Thao tác này sẽ xóa vĩnh viễn tất cả các khuôn mặt liên quan." + }, + "deleteFaceAttempts": { + "title": "Xóa khuôn mặt", + "desc_other": "Bạn có chắc chắn muốn xóa {{count}} khuôn mặt không? Hành động này không thể hoàn tác." + }, + "readTheDocs": "Đọc tài liệu", + "trainFaceAs": "Huấn luyện khuôn mặt với tên:", + "trainFace": "Huấn luyện khuôn mặt", + "nofaces": "Không có khuôn mặt nào", + "createFaceLibrary": { + "nextSteps": "Để xây dựng một nền tảng vững chắc:
  • Sử dụng tab Nhận dạng gần đây để chọn và huấn luyện trên hình ảnh cho mỗi người được phát hiện.
  • Tập trung vào hình ảnh chụp thẳng để có kết quả tốt nhất; tránh huấn luyện các hình ảnh chụp khuôn mặt ở một góc.
  • ", + "title": "Tạo bộ sưu tập", + "desc": "Tạo một bộ sưu tập mới", + "new": "Tạo khuôn mặt mới" + }, + "train": { + "title": "Nhận dạng gần đây", + "empty": "Không có nỗ lực nhận dạng khuôn mặt nào gần đây", + "aria": "Chọn các nhận dạng gần đây" + }, + "selectFace": "Chọn khuôn mặt", + "pixels": "{{area}}px", + "documentTitle": "Thư viện Khuôn mặt - Frigate", + "uploadFaceImage": { + "title": "Tải lên hình ảnh khuôn mặt", + "desc": "Tải lên một hình ảnh để quét khuôn mặt và bao gồm cho {{pageToggle}}" + } +} diff --git a/web/public/locales/vi/views/live.json b/web/public/locales/vi/views/live.json new file mode 100644 index 0000000..c238a34 --- /dev/null +++ b/web/public/locales/vi/views/live.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Trực tiếp - Frigate", + "documentTitle.withCamera": "{{camera}} - Trực tiếp - Frigate", + "lowBandwidthMode": "Chế độ băng thông thấp", + "twoWayTalk": { + "enable": "Bật đàm thoại hai chiều", + "disable": "Tắt đàm thoại hai chiều" + }, + "stream": { + "lowBandwidth": { + "tips": "Chế độ xem trực tiếp đang ở chế độ băng thông thấp do bộ đệm hoặc lỗi luồng.", + "resetStream": "Đặt lại luồng" + }, + "playInBackground": { + "label": "Phát trong nền", + "tips": "\"Bật tùy chọn này để tiếp tục phát khi trình phát bị ẩn." + }, + "title": "Luồng", + "audio": { + "tips": { + "title": "Âm thanh phải được phát ra từ camera của bạn và cấu hình trong go2rtc cho luồng này.", + "documentation": "Đọc tài liệu " + }, + "available": "Âm thanh khả dụng cho luồng này", + "unavailable": "Âm thanh không khả dụng cho luồng này" + }, + "twoWayTalk": { + "tips": "hiết bị của bạn phải hỗ trợ tính năng này và WebRTC phải được cấu hình để đàm thoại hai chiều.", + "tips.documentation": "Đọc tài liệu ", + "available": "Đàm thoại hai chiều khả dụng cho luồng này", + "unavailable": "Đàm thoại hai chiều không khả dụng cho luồng này" + }, + "debug": { + "picker": "Việc chọn luồng phát không khả dụng trong chế độ gỡ lỗi. Chế độ xem gỡ lỗi luôn sử dụng luồng được gán vai trò phát hiện (detect)." + } + }, + "editLayout": { + "group": { + "label": "Chỉnh sửa nhóm Camera" + }, + "label": "Chỉnh sửa bố cục", + "exitEdit": "Thoát chế độ chỉnh sửa" + }, + "ptz": { + "zoom": { + "out": { + "label": "Thu nhỏ camera PTZ" + }, + "in": { + "label": "Phóng to camera PTZ" + } + }, + "frame": { + "center": { + "label": "Nhấp vào khung hình để căn giữa camera PTZ" + } + }, + "move": { + "clickMove": { + "label": "Nhấp vào khung hình để căn giữa camera", + "enable": "Bật nhấp để di chuyển", + "disable": "Tắt nhấp để di chuyển" + }, + "left": { + "label": "Di chuyển camera PTZ sang trái" + }, + "up": { + "label": "Di chuyển camera PTZ lên" + }, + "down": { + "label": "Di chuyển camera PTZ xuống" + }, + "right": { + "label": "Di chuyển camera PTZ sang phải" + } + }, + "presets": "Các thiết lập sẵn cho camera PTZ", + "focus": { + "in": { + "label": "Lấy nét gần (camera PTZ)" + }, + "out": { + "label": "Lấy nét xa (camera PTZ)" + } + } + }, + "manualRecording": { + "playInBackground": { + "label": "Phát trong nền", + "desc": "Bật tùy chọn này để tiếp tục phát khi trình phát bị ẩn." + }, + "end": "Kết thúc ghi hình theo yêu cầu", + "failedToStart": "Không thể bắt đầu ghi hình theo yêu cầu.", + "started": "Đã bắt đầu ghi hình theo yêu cầu.", + "ended": "Đã kết thúc ghi hình theo yêu cầu.", + "title": "Theo yêu cầu", + "tips": "Tải xuống ảnh chụp nhanh tức thì hoặc bắt đầu sự kiện thủ công dựa trên cài đặt lưu giữ bản ghi của máy ảnh này.", + "showStats": { + "label": "Hiện thống kê", + "desc": "Bật tùy chọn này để hiển thị thống kê luồng trên khung hình." + }, + "debugView": "Chế độ gỡ lỗi", + "start": "Bắt đầu ghi hình theo yêu cầu", + "recordDisabledTips": "Vì ghi hình đã bị vô hiệu hóa hoặc bị giới hạn trong cấu hình của camera này, chỉ ảnh chụp sẽ được lưu.", + "failedToEnd": "Không thể kết thúc ghi hình theo yêu cầu." + }, + "cameraAudio": { + "enable": "Bật âm thanh Camera", + "disable": "Tắt âm thanh Camera" + }, + "camera": { + "enable": "Bật camera", + "disable": "Tắt camera" + }, + "muteCameras": { + "enable": "Tắt tiếng tất cả Camera", + "disable": "Bật tiếng tất cả Camera" + }, + "detect": { + "enable": "Bật phát hiện", + "disable": "Tắt phát hiện" + }, + "recording": { + "enable": "Bật ghi hình", + "disable": "Tắt ghi hình" + }, + "snapshots": { + "enable": "Bật ảnh chụp", + "disable": "Tắt ảnh chụp" + }, + "audioDetect": { + "enable": "Bật phát hiện âm thanh", + "disable": "Tắt phát hiện âm thanh" + }, + "autotracking": { + "enable": "Bật tự động theo dõi", + "disable": "Tắt tự động theo dõi" + }, + "streamStats": { + "enable": "Hiện thống kê luồng", + "disable": "Ẩn thống kê luồng" + }, + "streamingSettings": "Cài đặt truyền phát", + "notifications": "Thông báo", + "audio": "Âm thanh", + "suspend": { + "forTime": "Tạm dừng trong: " + }, + "cameraSettings": { + "title": "Cài đặt {{camera}}", + "cameraEnabled": "Camera đã bật", + "objectDetection": "Phát hiện đối tượng", + "recording": "Ghi hình", + "snapshots": "Ảnh chụp", + "audioDetection": "Phát hiện âm thanh", + "autotracking": "Tự động theo dõi", + "transcription": "Phiên âm" + }, + "history": { + "label": "Hiện cảnh quay lịch sử" + }, + "effectiveRetainMode": { + "modes": { + "all": "Tất cả", + "motion": "Chuyển động", + "active_objects": "Đối tượng hoạt động" + }, + "notAllTips": "Cấu hình giữ lại ghi hình {{source}} của bạn được đặt là mode: {{effectiveRetainMode}}, vì vậy lần ghi hình theo yêu cầu này chỉ giữ lại các đoạn có {{effectiveRetainModeName}}." + }, + "transcription": { + "enable": "Bật phiên âm trực tiếp", + "disable": "Tắt phiên âm trực tiếp" + }, + "snapshot": { + "takeSnapshot": "Tải xuống ảnh chụp nhanh ngay lập tức", + "noVideoSource": "Không có nguồn video để chụp ảnh nhanh.", + "captureFailed": "Chụp ảnh nhanh không thành công.", + "downloadStarted": "Bắt đầu tải xuống ảnh chụp nhanh." + }, + "noCameras": { + "title": "Không có camera nào được cấu hình", + "description": "Bắt đầu bằng cách kết nối một camera với Frigate.", + "buttonText": "Thêm Camera", + "restricted": { + "title": "Không có Camera nào khả dụng", + "description": "Bạn không có quyền xem bất kỳ camera nào trong nhóm này." + } + } +} diff --git a/web/public/locales/vi/views/motionSearch.json b/web/public/locales/vi/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/vi/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/vi/views/recording.json b/web/public/locales/vi/views/recording.json new file mode 100644 index 0000000..de52f8b --- /dev/null +++ b/web/public/locales/vi/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Lọc", + "export": "Xuất", + "calendar": "Lịch", + "filters": "Bộ lọc", + "toast": { + "error": { + "noValidTimeSelected": "Thời gian chọn không hợp lệ", + "endTimeMustAfterStartTime": "Thời gian kết thúc phải sau thời gian bắt đầu" + } + } +} diff --git a/web/public/locales/vi/views/replay.json b/web/public/locales/vi/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/vi/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/vi/views/search.json b/web/public/locales/vi/views/search.json new file mode 100644 index 0000000..d95cd17 --- /dev/null +++ b/web/public/locales/vi/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Tìm kiếm", + "savedSearches": "Tìm kiếm đã lưu", + "searchFor": "Tìm kiếm {{inputValue}}", + "button": { + "clear": "Xóa tìm kiếm", + "save": "Lưu tìm kiếm", + "delete": "Xóa tìm kiếm đã lưu", + "filterInformation": "Thông tin bộ lọc", + "filterActive": "Bộ lọc đang hoạt động" + }, + "filter": { + "tips": { + "desc": { + "step2": "Chọn một giá trị từ các gợi ý hoặc tự nhập.", + "step1": "Nhập tên khóa bộ lọc theo sau là dấu hai chấm (ví dụ: \"cameras:\").", + "step3": "Sử dụng nhiều bộ lọc bằng cách thêm chúng nối tiếp nhau, cách nhau bằng dấu cách.", + "step5": "Bộ lọc phạm vi thời gian sử dụng định dạng {{exampleTime}}.", + "exampleLabel": "Ví dụ:", + "step6": "Xóa bộ lọc bằng cách nhấp vào dấu 'x' bên cạnh chúng..", + "step4": "Bộ lọc ngày (before: và after:) sử dụng định dạng {{DateFormat}}.", + "text": "Bộ lọc giúp bạn thu hẹp kết quả tìm kiếm. Dưới đây là cách sử dụng chúng trong trường nhập liệu:" + }, + "title": "Cách sử dụng bộ lọc văn bản" + }, + "header": { + "activeFilters": "Bộ lọc đang hoạt động", + "currentFilterType": "Giá trị bộ lọc", + "noFilters": "Bộ lọc" + }, + "label": { + "has_clip": "Có clip", + "min_score": "Điểm tối thiểu", + "has_snapshot": "Có ảnh chụp nhanh", + "max_score": "Điểm tối đa", + "search_type": "Loại tìm kiếm", + "after": "Sau", + "cameras": "Camera", + "labels": "Nhãn", + "zones": "Khu vực", + "sub_labels": "Nhãn phụ", + "time_range": "Phạm vi thời gian", + "before": "Trước", + "min_speed": "Tốc độ tối thiểu", + "max_speed": "Tốc độ tối đa", + "recognized_license_plate": "Biển số xe được nhận dạng" + }, + "toast": { + "error": { + "afterDatebeEarlierBefore": "Ngày 'Sau' phải trước ngày 'Trước'.", + "beforeDateBeLaterAfter": "Ngày 'Trước' phải sau ngày 'Sau'.", + "minScoreMustBeLessOrEqualMaxScore": "'Điểm tối thiểu' phải nhỏ hơn hoặc bằng 'Điểm tối đa'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'Điểm tối đa' phải lớn hơn hoặc bằng 'Điểm tối thiểu.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'Tốc độ tối thiểu' phải nhỏ hơn hoặc bằng 'Tốc độ tối đa'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'Tốc độ tối đa' phải lớn hơn hoặc bằng 'Tốc độ tối thiểu'." + } + }, + "searchType": { + "thumbnail": "Hình thu nhỏ", + "description": "Mô tả" + } + }, + "similaritySearch": { + "title": "Tìm kiếm tương đồng", + "clear": "Xóa tìm kiếm tương đồng", + "active": "Tìm kiếm tương đồng đang hoạt động" + }, + "placeholder": { + "search": "Tìm kiếm…" + }, + "trackedObjectId": "ID đối tượng được theo dõi" +} diff --git a/web/public/locales/vi/views/settings.json b/web/public/locales/vi/views/settings.json new file mode 100644 index 0000000..90a6c19 --- /dev/null +++ b/web/public/locales/vi/views/settings.json @@ -0,0 +1,776 @@ +{ + "documentTitle": { + "default": "Cài đặt - Frigate", + "authentication": "Cài đặt Xác thực - Frigate", + "camera": "Cài đặt Camera - Frigate", + "enrichments": "Cài đặt Làm giàu Dữ liệu - Frigate", + "notifications": "Cài đặt Thông báo - Frigate", + "masksAndZones": "Trình chỉnh sửa Mặt nạ và Vùng - Frigate", + "object": "Gỡ lỗi - Frigate", + "general": "Cài đặt giao diện – Frigate", + "frigatePlus": "Cài đặt Frigate+ - Frigate", + "motionTuner": "Bộ tinh chỉnh Chuyển động - Frigate", + "cameraManagement": "Quản Lý Camera - Frigate", + "cameraReview": "Cài Đặt Xem Lại Camera - Frigate" + }, + "notification": { + "toast": { + "error": { + "registerFailed": "Không thể lưu đăng ký thông báo." + }, + "success": { + "settingSaved": "Cài đặt thông báo đã được lưu.", + "registered": "Đã đăng ký nhận thông báo thành công. Cần khởi động lại Frigate trước khi có thể gửi bất kỳ thông báo nào (kể cả thông báo thử)." + } + }, + "unsavedChanges": "Các thay đổi Thông báo chưa được lưu", + "registerDevice": "Đăng ký Thiết bị này", + "unregisterDevice": "Hủy đăng ký Thiết bị này", + "unsavedRegistrations": "Các đăng ký Thông báo chưa được lưu", + "suspended": "Thông báo bị đình chỉ {{time}}", + "cancelSuspension": "Hủy tạm dừng", + "email": { + "desc": "Một email hợp lệ là bắt buộc và sẽ được sử dụng để thông báo cho bạn nếu có bất kỳ vấn đề nào với dịch vụ đẩy.", + "placeholder": "ví dụ: example@email.com", + "title": "Email" + }, + "cameras": { + "noCameras": "Không có camera nào", + "title": "Camera", + "desc": "Chọn camera để bật thông báo." + }, + "deviceSpecific": "Cài đặt dành riêng cho thiết bị", + "sendTestNotification": "Gửi một thông báo thử", + "active": "Thông báo đang hoạt động", + "suspendTime": { + "24hours": "Tạm dừng trong 24 giờ", + "untilRestart": "Tạm dừng cho đến khi khởi động lại", + "30minutes": "Tạm dừng trong 30 phút", + "1hour": "Tạm dừng trong 1 giờ", + "suspend": "Tạm dừng", + "12hours": "Tạm dừng trong 12 giờ", + "5minutes": "Tạm dừng trong 5 phút", + "10minutes": "Tạm dừng trong 10 phút" + }, + "notificationSettings": { + "desc": "Frigate có thể gửi thông báo đẩy tự nhiên đến thiết bị của bạn khi nó đang chạy trong trình duyệt hoặc được cài đặt dưới dạng PWA.", + "title": "Cài đặt Thông báo", + "documentation": "Đọc tài liệu" + }, + "notificationUnavailable": { + "desc": "Thông báo đẩy web yêu cầu một ngữ cảnh an toàn (https://…). Đây là một hạn chế của trình duyệt. Truy cập Frigate một cách an toàn để sử dụng thông báo.", + "title": "Thông báo không khả dụng", + "documentation": "Đọc tài liệu" + }, + "globalSettings": { + "desc": "Tạm thời đình chỉ thông báo cho các camera cụ thể trên tất cả các thiết bị đã đăng ký.", + "title": "Cài đặt Chung" + }, + "title": "Thông báo" + }, + "frigatePlus": { + "title": "Cài đặt Frigate+", + "apiKey": { + "title": "Khóa API Frigate+", + "validated": "Khóa API Frigate+ đã được phát hiện và xác thực", + "notValidated": "Khóa API Frigate+ không được phát hiện hoặc chưa được xác thực", + "desc": "Khóa API Frigate+ cho phép tích hợp với dịch vụ Frigate+.", + "plusLink": "Đọc thêm về Frigate+" + }, + "snapshotConfig": { + "table": { + "camera": "Máy quay", + "cleanCopySnapshots": "Ảnh chụp nhanh clean_copy", + "snapshots": "Ảnh chụp nhanh" + }, + "desc": "Việc gửi đến Frigate+ yêu cầu cả ảnh chụp nhanh và ảnh chụp nhanh clean_copy phải được bật trong cấu hình của bạn.", + "cleanCopyWarning": "Một số camera đã bật ảnh chụp nhanh nhưng đã tắt bản sao sạch. Bạn cần bật clean_copy trong cấu hình ảnh chụp nhanh của mình để có thể gửi hình ảnh từ các camera này đến Frigate+.", + "title": "Cấu hình Ảnh chụp nhanh", + "documentation": "Đọc tài liệu" + }, + "modelInfo": { + "error": "Không thể tải thông tin mô hình", + "plusModelType": { + "userModel": "Đã tinh chỉnh", + "baseModel": "Mô hình Cơ sở" + }, + "supportedDetectors": "Các bộ phát hiện được hỗ trợ", + "title": "Thông tin Mô hình", + "baseModel": "Mô hình Cơ sở", + "availableModels": "Các mô hình có sẵn", + "loadingAvailableModels": "Đang tải các mô hình có sẵn…", + "modelSelect": "Các mô hình có sẵn của bạn trên Frigate+ có thể được chọn ở đây. Lưu ý rằng chỉ những mô hình tương thích với cấu hình bộ phát hiện hiện tại của bạn mới có thể được chọn.", + "cameras": "Camera", + "loading": "Đang tải thông tin mô hình…", + "modelType": "Loại Mô hình", + "trainDate": "Ngày Huấn luyện" + }, + "unsavedChanges": "Các thay đổi cài đặt Frigate+ chưa được lưu", + "toast": { + "success": "Cài đặt Frigate+ đã được lưu. Khởi động lại Frigate để áp dụng các thay đổi.", + "error": "Không thể lưu các thay đổi cấu hình: {{errorMessage}}" + }, + "restart_required": "Yêu cầu khởi động lại (mô hình Frigate+ đã thay đổi)" + }, + "camera": { + "title": "Cài đặt Camera", + "review": { + "alerts": "Cảnh báo ", + "detections": "Phát hiện ", + "title": "Xem lại", + "desc": "Tạm thời bật/tắt cảnh báo và phát hiện cho camera này cho đến khi Frigate khởi động lại. Khi bị vô hiệu hóa, sẽ không có mục xem lại mới nào được tạo ra. " + }, + "reviewClassification": { + "title": "Phân loại mục Xem lại", + "unsavedChanges": "Các thay đổi cài đặt Phân loại mục Xem lại chưa được lưu cho {{camera}}", + "readTheDocumentation": "Đọc tài liệu", + "objectDetectionsTips": "Tất cả các đối tượng {{detectionsLabels}} không được phân loại trên {{cameraName}} sẽ được hiển thị dưới dạng Phát hiện bất kể chúng ở trong vùng nào.", + "desc": "Frigate phân loại các mục xem lại thành Cảnh báo và Phát hiện. Theo mặc định, tất cả các đối tượng ngườiô tô được coi là Cảnh báo. Bạn có thể tinh chỉnh việc phân loại các mục xem lại của mình bằng cách định cấu hình các vùng bắt buộc cho chúng.", + "zoneObjectDetectionsTips": { + "text": "Tất cả các đối tượng {{detectionsLabels}} không được phân loại trong vùng {{zone}} trên {{cameraName}} sẽ được hiển thị dưới dạng Phát hiện.", + "notSelectDetections": "Tất cả các đối tượng {{detectionsLabels}} được phát hiện trong vùng {{zone}} trên {{cameraName}} không được phân loại là Cảnh báo sẽ được hiển thị dưới dạng Phát hiện bất kể chúng ở trong vùng nào.", + "regardlessOfZoneObjectDetectionsTips": "Tất cả các đối tượng {{detectionsLabels}} không được phân loại trên {{cameraName}} sẽ được hiển thị dưới dạng Phát hiện bất kể chúng ở trong vùng nào." + }, + "toast": { + "success": "Cấu hình Phân loại mục Xem lại đã được lưu. Khởi động lại Frigate để áp dụng các thay đổi." + }, + "limitDetections": "Giới hạn phát hiện trong các vùng cụ thể", + "selectDetectionsZones": "Chọn vùng cho Phát hiện", + "zoneObjectAlertsTips": "Tất cả các đối tượng {{alertsLabels}} được phát hiện trong vùng {{zone}} trên {{cameraName}} sẽ được hiển thị dưới dạng Cảnh báo.", + "noDefinedZones": "Không có vùng nào được xác định cho camera này.", + "objectAlertsTips": "Tất cả các đối tượng {{alertsLabels}} trên {{cameraName}} sẽ được hiển thị dưới dạng Cảnh báo.", + "selectAlertsZones": "Chọn vùng cho Cảnh báo" + }, + "streams": { + "title": "Luồng phát", + "desc": "Tạm thời vô hiệu hóa một camera cho đến khi Frigate khởi động lại. Vô hiệu hóa một camera sẽ dừng hoàn toàn quá trình xử lý các luồng của camera này của Frigate. Việc phát hiện, ghi hình và gỡ lỗi sẽ không khả dụng.
    Lưu ý: Điều này không vô hiệu hóa các luồng phát lại của go2rtc." + }, + "object_descriptions": { + "title": "Mô tả đối tượng bằng AI tạo sinh", + "desc": "Tạm thời bật/tắt mô tả đối tượng bằng AI tạo sinh cho camera này. Khi tắt, mô tả do AI tạo sinh sẽ không được yêu cầu cho các đối tượng được theo dõi trên camera này." + }, + "review_descriptions": { + "title": "Mô tả đánh giá bằng AI tạo sinh", + "desc": "Tạm thời bật/tắt mô tả xem lại bằng AI tạo sinh cho camera này. Khi tắt, mô tả do AI tạo sinh sẽ không được yêu cầu cho các mục xem lại trên camera này." + }, + "addCamera": "Thêm Camera mới", + "editCamera": "Chỉnh sửa Camera:", + "selectCamera": "Chọn Camera", + "backToSettings": "Quay lại cài đặt Camera", + "cameraConfig": { + "add": "Thêm Camera", + "edit": "Chỉnh sửa Camera", + "description": "Cấu hình Camera, bao gồm luồng đầu vào và vai trò.", + "name": "Tên Camera", + "nameRequired": "Yêu cầu nhập tên Camera", + "nameInvalid": "Tên Camera chỉ được chứa chữ cái, số, dấu gạch dưới hoặc dấu gạch ngang", + "namePlaceholder": "Ví dụ: front_door", + "enabled": "Bật", + "ffmpeg": { + "inputs": "Luồng đầu vào", + "path": "Đường dẫn luồng", + "pathRequired": "Yêu cầu nhập đường dẫn luồng", + "pathPlaceholder": "rtsp://...", + "roles": "Vai trò", + "rolesRequired": "Cần ít nhất một vai trò", + "rolesUnique": "Mỗi vai trò (âm thanh, phát hiện, ghi hình) chỉ có thể được gán cho một luồng duy nhất", + "addInput": "Thêm luồng đầu vào", + "removeInput": "Xóa luồng đầu vào", + "inputsRequired": "Cần ít nhất một luồng đầu vào" + }, + "toast": { + "success": "Camera {{cameraName}} đã được lưu thành công" + }, + "nameLength": "Tên của camera phải dưới 24 ký tự." + } + }, + "masksAndZones": { + "form": { + "zoneName": { + "error": { + "mustNotBeSameWithCamera": "Tên vùng không được trùng với tên camera.", + "mustBeAtLeastTwoCharacters": "Tên vùng phải có ít nhất 2 ký tự.", + "mustNotContainPeriod": "Tên vùng không được chứa dấu chấm.", + "alreadyExists": "Một vùng với tên này đã tồn tại cho camera này.", + "hasIllegalCharacter": "Tên vùng chứa các ký tự không hợp lệ." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Quán tính phải lớn hơn 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Thời gian lảng vảng phải lớn hơn hoặc bằng 0." + } + }, + "polygonDrawing": { + "delete": { + "desc": "Bạn có chắc muốn xóa {{type}} {{name}} không?", + "title": "Xác nhận Xóa", + "success": "{{name}} đã được xóa." + }, + "removeLastPoint": "Xóa điểm cuối", + "snapPoints": { + "true": "Bắt dính điểm", + "false": "Không bắt dính điểm" + }, + "reset": { + "label": "Xóa tất cả các điểm" + }, + "error": { + "mustBeFinished": "Việc vẽ đa giác phải được hoàn thành trước khi lưu." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Ngưỡng tốc độ phải lớn hơn hoặc bằng 0.1." + } + }, + "distance": { + "error": { + "text": "Khoảng cách phải lớn hơn hoặc bằng 0.1.", + "mustBeFilled": "Tất cả các trường khoảng cách phải được điền để sử dụng ước tính tốc độ." + } + } + }, + "objectMasks": { + "add": "Thêm Mặt nạ đối tượng", + "edit": "Chỉnh sửa Mặt nạ đối tượng", + "context": "Mặt nạ lọc đối tượng được sử dụng để lọc ra các kết quả dương tính giả cho một loại đối tượng nhất định dựa trên vị trí.", + "objects": { + "allObjectTypes": "Tất cả các loại đối tượng", + "title": "Đối tượng", + "desc": "Loại đối tượng áp dụng cho mặt nạ đối tượng này." + }, + "documentTitle": "Chỉnh sửa Mặt nạ đối tượng - Frigate", + "desc": { + "title": "Mặt nạ lọc đối tượng được sử dụng để lọc ra các kết quả dương tính giả cho một loại đối tượng nhất định dựa trên vị trí.", + "documentation": "Tài liệu" + }, + "point_other": "{{count}} điểm", + "toast": { + "success": { + "noName": "Mặt nạ đối tượng đã được lưu.", + "title": "{{polygonName}} đã được lưu." + } + }, + "label": "Mặt nạ đối tượng", + "clickDrawPolygon": "Nhấp để vẽ một đa giác trên hình ảnh." + }, + "zones": { + "speedEstimation": { + "docs": "Đọc tài liệu", + "desc": "Bật ước tính tốc độ cho các đối tượng trong vùng này. Vùng phải có chính xác 4 điểm.", + "title": "Ước tính Tốc độ", + "lineADistance": "Khoảng cách đường A ({{unit}})", + "lineBDistance": "Khoảng cách đường B ({{unit}})", + "lineCDistance": "Khoảng cách đường C ({{unit}})", + "lineDDistance": "Khoảng cách đường D ({{unit}})" + }, + "desc": { + "documentation": "Tài liệu", + "title": "Các vùng cho phép bạn xác định một khu vực cụ thể của khung hình để bạn có thể xác định xem một đối tượng có ở trong một khu vực cụ thể hay không." + }, + "add": "Thêm Vùng", + "allObjects": "Tất cả Đối tượng", + "speedThreshold": { + "toast": { + "error": { + "loiteringTimeError": "Các vùng có thời gian lảng vảng lớn hơn 0 không nên được sử dụng với ước tính tốc độ.", + "pointLengthError": "Ước tính tốc độ đã bị vô hiệu hóa cho vùng này. Các vùng có ước tính tốc độ phải có chính xác 4 điểm." + } + }, + "desc": "Chỉ định tốc độ tối thiểu để các đối tượng được coi là ở trong vùng này.", + "title": "Ngưỡng Tốc độ ({{unit}})" + }, + "loiteringTime": { + "title": "Thời gian lảng vảng", + "desc": "Đặt một khoảng thời gian tối thiểu tính bằng giây mà đối tượng phải ở trong vùng để kích hoạt nó. Mặc định: 0" + }, + "point_other": "{{count}} điểm", + "clickDrawPolygon": "Nhấp để vẽ một đa giác trên hình ảnh.", + "objects": { + "title": "Đối tượng", + "desc": "Danh sách các đối tượng áp dụng cho vùng này." + }, + "toast": { + "success": "Vùng ({{zoneName}}) đã được lưu." + }, + "name": { + "inputPlaceHolder": "Nhập tên…", + "tips": "Tên phải có ít nhất 2 ký tự, phải có ít nhất một chữ cái và không được là tên của camera hoặc vùng khác trên camera này.", + "title": "Tên" + }, + "edit": "Chỉnh sửa Vùng", + "label": "Vùng", + "documentTitle": "Chỉnh sửa Vùng - Frigate", + "inertia": { + "title": "Quán tính", + "desc": "Chỉ định số khung hình mà một đối tượng phải ở trong một vùng trước khi được coi là ở trong vùng. Mặc định: 3" + } + }, + "motionMasks": { + "documentTitle": "Chỉnh sửa Mặt nạ chuyển động - Frigate", + "desc": { + "documentation": "Tài liệu", + "title": "Mặt nạ chuyển động được sử dụng để ngăn các loại chuyển động không mong muốn kích hoạt phát hiện. Việc che quá nhiều sẽ khiến việc theo dõi đối tượng trở nên khó khăn hơn." + }, + "point_other": "{{count}} điểm", + "polygonAreaTooLarge": { + "documentation": "Đọc tài liệu", + "tips": "Mặt nạ chuyển động không ngăn chặn việc phát hiện đối tượng. Bạn nên sử dụng một vùng bắt buộc thay thế.", + "title": "Mặt nạ chuyển động đang che {{polygonArea}}% khung hình của camera. Không khuyến khích sử dụng mặt nạ chuyển động lớn." + }, + "add": "Mặt nạ chuyển động mới", + "edit": "Chỉnh sửa Mặt nạ chuyển động", + "context": { + "documentation": "Đọc tài liệu", + "title": "Mặt nạ chuyển động được sử dụng để ngăn chặn các loại chuyển động không mong muốn kích hoạt việc phát hiện (ví dụ: cành cây, dấu thời gian của camera). Mặt nạ chuyển động nên được sử dụng rất hạn chế, việc che quá nhiều sẽ làm cho việc theo dõi đối tượng trở nên khó khăn hơn." + }, + "label": "Mặt nạ chuyển động", + "clickDrawPolygon": "Nhấp để vẽ một đa giác trên hình ảnh.", + "toast": { + "success": { + "title": "{{polygonName}} đã được lưu.", + "noName": "Mặt nạ chuyển động đã được lưu." + } + } + }, + "toast": { + "success": { + "copyCoordinates": "Đã sao chép tọa độ của {{polyName}} vào clipboard." + }, + "error": { + "copyCoordinatesFailed": "Không thể sao chép tọa độ vào clipboard." + } + }, + "restart_required": "Yêu cầu khởi động lại (mặt nạ/vùng đã thay đổi)", + "motionMaskLabel": "Mặt nạ chuyển động {{number}}", + "objectMaskLabel": "Mặt nạ đối tượng {{number}}", + "filter": { + "all": "Tất cả Mặt nạ và Vùng" + } + }, + "motionDetectionTuner": { + "unsavedChanges": "Các thay đổi Tinh chỉnh Chuyển động chưa được lưu ({{camera}})", + "Threshold": { + "title": "Ngưỡng", + "desc": "Giá trị ngưỡng quy định mức độ thay đổi độ sáng của một pixel cần thiết để được coi là chuyển động. Mặc định: 30" + }, + "contourArea": { + "title": "Diện tích đường viền", + "desc": "Giá trị diện tích đường viền được sử dụng để quyết định nhóm pixel nào đã thay đổi đủ điều kiện là chuyển động. Mặc định: 10" + }, + "title": "Bộ tinh chỉnh Phát hiện Chuyển động", + "desc": { + "title": "Frigate sử dụng phát hiện chuyển động như một bước kiểm tra đầu tiên để xem có điều gì đang xảy ra trong khung hình đáng để kiểm tra bằng phát hiện đối tượng hay không.", + "documentation": "Đọc Hướng dẫn Tinh chỉnh Chuyển động" + }, + "improveContrast": { + "desc": "Cải thiện độ tương phản cho các cảnh tối hơn. Mặc định: BẬT", + "title": "Cải thiện độ tương phản" + }, + "toast": { + "success": "Cài đặt chuyển động đã được lưu." + } + }, + "debug": { + "objectShapeFilterDrawing": { + "title": "Vẽ bộ lọc hình dạng đối tượng", + "desc": "Vẽ một hình chữ nhật trên hình ảnh để xem chi tiết diện tích và tỷ lệ", + "document": "Đọc tài liệu ", + "score": "Điểm", + "tips": "Bật tùy chọn này để vẽ một hình chữ nhật trên hình ảnh của camera để hiển thị diện tích và tỷ lệ của nó. Các giá trị này sau đó có thể được sử dụng để đặt các tham số bộ lọc hình dạng đối tượng trong cấu hình của bạn.", + "ratio": "Tỷ lệ", + "area": "Diện tích" + }, + "detectorDesc": "Frigate sử dụng các bộ phát hiện của bạn ({{detectors}}) để phát hiện các đối tượng trong luồng video của camera.", + "boundingBoxes": { + "colors": { + "label": "Màu sắc Hộp giới hạn Đối tượng", + "info": "
  • Khi khởi động, các màu khác nhau sẽ được gán cho mỗi nhãn đối tượng
  • Một đường mỏng màu xanh đậm cho biết đối tượng không được phát hiện tại thời điểm hiện tại
  • Một đường mỏng màu xám cho biết đối tượng được phát hiện là đang đứng yên
  • Một đường dày cho biết đối tượng là chủ thể của việc theo dõi tự động (khi được bật)
  • " + }, + "desc": "Hiển thị các hộp giới hạn xung quanh các đối tượng được theo dõi", + "title": "Hộp giới hạn" + }, + "desc": "Chế độ xem gỡ lỗi hiển thị chế độ xem thời gian thực của các đối tượng được theo dõi và các thống kê của chúng. Danh sách đối tượng hiển thị một bản tóm tắt có độ trễ về các đối tượng được phát hiện.", + "debugging": "Gỡ lỗi", + "timestamp": { + "desc": "Chồng một dấu thời gian lên hình ảnh", + "title": "Dấu thời gian" + }, + "regions": { + "title": "Khu vực", + "tips": "

    Hộp khu vực


    Các hộp màu xanh lá cây sáng sẽ được chồng lên các khu vực quan tâm trong khung hình đang được gửi đến bộ phát hiện đối tượng.

    ", + "desc": "Hiển thị một hộp của khu vực quan tâm được gửi đến bộ phát hiện đối tượng" + }, + "zones": { + "title": "Vùng", + "desc": "Hiển thị đường viền của bất kỳ vùng nào đã được xác định" + }, + "mask": { + "title": "Mặt nạ chuyển động", + "desc": "Hiển thị các đa giác mặt nạ chuyển động" + }, + "title": "Gỡ lỗi", + "objectList": "Danh sách đối tượng", + "noObjects": "Không có đối tượng", + "motion": { + "desc": "Hiển thị các hộp xung quanh các khu vực phát hiện có chuyển động", + "tips": "

    Hộp chuyển động


    Các hộp màu đỏ sẽ được chồng lên các khu vực của khung hình nơi chuyển động đang được phát hiện

    ", + "title": "Hộp chuyển động" + }, + "paths": { + "title": "Đường dẫn", + "desc": "Hiển thị các điểm quan trọng trên đường đi của đối tượng được theo dõi", + "tips": "

    Đường đi


    Đường thẳng và vòng tròn sẽ hiển thị các điểm quan trọng mà đối tượng được theo dõi đã di chuyển trong suốt quá trình theo dõi.

    " + }, + "openCameraWebUI": "Đang mở giao diện Web của {{camera}}", + "audio": { + "title": "Âm thanh", + "noAudioDetections": "Không phát hiện âm thanh", + "score": "điểm", + "currentRMS": "RMS hiện tại", + "currentdbFS": "dbFS hiện tại" + } + }, + "users": { + "title": "Người dùng", + "management": { + "title": "Quản lý Người dùng", + "desc": "Quản lý các tài khoản người dùng của phiên bản Frigate này." + }, + "table": { + "noUsers": "Không tìm thấy người dùng nào.", + "username": "Tên người dùng", + "actions": "Hành động", + "role": "Vai trò", + "changeRole": "Thay đổi vai trò người dùng", + "password": "Mật khẩu", + "deleteUser": "Xóa người dùng" + }, + "dialog": { + "form": { + "password": { + "strength": { + "strong": "Mạnh", + "title": "Độ mạnh mật khẩu: ", + "medium": "Trung bình", + "veryStrong": "Rất mạnh", + "weak": "Yếu" + }, + "title": "Mật khẩu", + "placeholder": "Nhập mật khẩu", + "confirm": { + "title": "Xác nhận Mật khẩu", + "placeholder": "Xác nhận Mật khẩu" + }, + "notMatch": "Mật khẩu không trùng khớp", + "match": "Mật khẩu trùng khớp" + }, + "newPassword": { + "placeholder": "Nhập mật khẩu mới", + "confirm": { + "placeholder": "Nhập lại mật khẩu mới" + }, + "title": "Mật khẩu mới" + }, + "usernameIsRequired": "Tên người dùng là bắt buộc", + "user": { + "title": "Tên người dùng", + "desc": "Chỉ cho phép chữ cái, số, dấu chấm và dấu gạch dưới.", + "placeholder": "Nhập tên người dùng" + }, + "passwordIsRequired": "Mật khẩu là bắt buộc" + }, + "createUser": { + "desc": "Thêm một tài khoản người dùng mới và chỉ định một vai trò để truy cập vào các khu vực của giao diện người dùng Frigate.", + "usernameOnlyInclude": "Tên người dùng chỉ có thể bao gồm chữ cái, số, . hoặc _", + "title": "Tạo Người dùng Mới", + "confirmPassword": "Vui lòng xác nhận mật khẩu của bạn" + }, + "deleteUser": { + "desc": "Hành động này không thể được hoàn tác. Điều này sẽ xóa vĩnh viễn tài khoản người dùng và xóa tất cả dữ liệu liên quan.", + "warn": "Bạn có chắc muốn xóa {{username}} không?", + "title": "Xóa Người dùng" + }, + "passwordSetting": { + "setPassword": "Đặt Mật khẩu", + "updatePassword": "Cập nhật Mật khẩu cho {{username}}", + "cannotBeEmpty": "Mật khẩu không được để trống", + "desc": "Tạo một mật khẩu mạnh để bảo mật tài khoản này.", + "doNotMatch": "Mật khẩu không khớp" + }, + "changeRole": { + "title": "Thay đổi Vai trò Người dùng", + "roleInfo": { + "intro": "Chọn vai trò thích hợp cho người dùng này:", + "admin": "Quản trị viên", + "adminDesc": "Toàn quyền truy cập vào tất cả các tính năng.", + "viewer": "Người xem", + "viewerDesc": "Chỉ giới hạn ở các bảng điều khiển Trực tiếp, Xem lại, Khám phá và Xuất file." + }, + "select": "Chọn một vai trò", + "desc": "Cập nhật quyền cho {{username}}" + } + }, + "addUser": "Thêm Người dùng", + "updatePassword": "Cập nhật Mật khẩu", + "toast": { + "error": { + "setPasswordFailed": "Không thể lưu mật khẩu: {{errorMessage}}", + "createUserFailed": "Không thể tạo người dùng: {{errorMessage}}", + "deleteUserFailed": "Không thể xóa người dùng: {{errorMessage}}", + "roleUpdateFailed": "Không thể cập nhật vai trò: {{errorMessage}}" + }, + "success": { + "roleUpdated": "Vai trò đã được cập nhật cho {{user}}", + "createUser": "Người dùng {{user}} đã được tạo thành công", + "deleteUser": "Người dùng {{user}} đã được xóa thành công", + "updatePassword": "Mật khẩu đã được cập nhật thành công." + } + } + }, + "general": { + "calendar": { + "firstWeekday": { + "sunday": "Chủ nhật", + "monday": "Thứ hai", + "label": "Ngày đầu tuần", + "desc": "Ngày bắt đầu của các tuần trong lịch xem lại." + }, + "title": "Lịch" + }, + "storedLayouts": { + "desc": "Bố cục của các camera trong một nhóm camera có thể được kéo/thay đổi kích thước. Các vị trí được lưu trữ trong bộ nhớ cục bộ của trình duyệt của bạn.", + "title": "Bố cục đã lưu", + "clearAll": "Xóa tất cả Bố cục" + }, + "cameraGroupStreaming": { + "title": "Cài đặt Phát luồng Nhóm Camera", + "desc": "Cài đặt phát luồng cho mỗi nhóm camera được lưu trữ trong bộ nhớ cục bộ của trình duyệt của bạn.", + "clearAll": "Xóa tất cả Cài đặt Phát luồng" + }, + "toast": { + "success": { + "clearStoredLayout": "Đã xóa bố cục đã lưu cho {{cameraName}}", + "clearStreamingSettings": "Đã xóa cài đặt phát luồng cho tất cả các nhóm camera." + }, + "error": { + "clearStoredLayoutFailed": "Không thể xóa bố cục đã lưu: {{errorMessage}}", + "clearStreamingSettingsFailed": "Không thể xóa cài đặt phát luồng: {{errorMessage}}" + } + }, + "liveDashboard": { + "automaticLiveView": { + "desc": "Tự động chuyển sang chế độ xem trực tiếp của camera khi phát hiện hoạt động. Việc tắt tùy chọn này sẽ khiến hình ảnh tĩnh của camera trên bảng điều khiển Trực tiếp chỉ cập nhật mỗi phút một lần.", + "label": "Chế độ xem trực tiếp tự động" + }, + "playAlertVideos": { + "desc": "Theo mặc định, các cảnh báo gần đây trên bảng điều khiển Trực tiếp sẽ phát dưới dạng các video lặp lại nhỏ. Tắt tùy chọn này để chỉ hiển thị hình ảnh tĩnh của các cảnh báo gần đây trên thiết bị/trình duyệt này.", + "label": "Phát video cảnh báo" + }, + "title": "Bảng điều khiển trực tiếp", + "displayCameraNames": { + "label": "Luôn hiển thị tên camera", + "desc": "Luôn hiển thị tên camera trong một con chip trong bảng điều khiển xem trực tiếp nhiều camera." + }, + "liveFallbackTimeout": { + "label": "Hết thời gian chờ dự phòng của người chơi trực tiếp", + "desc": "Khi luồng trực tiếp chất lượng cao của camera không khả dụng, tự động chuyển sang chế độ băng thông thấp sau số giây này. Mặc định: 3." + } + }, + "recordingsViewer": { + "defaultPlaybackRate": { + "label": "Tốc độ phát mặc định", + "desc": "Tốc độ phát mặc định cho việc xem lại các bản ghi." + }, + "title": "Trình xem Bản ghi" + }, + "title": "Cài đặt giao diện" + }, + "dialog": { + "unsavedChanges": { + "desc": "Bạn có muốn lưu các thay đổi trước khi tiếp tục không?", + "title": "Bạn có các thay đổi chưa được lưu." + } + }, + "enrichments": { + "title": "Cài đặt Làm giàu Dữ liệu", + "unsavedChanges": "Các thay đổi cài đặt Làm giàu Dữ liệu chưa được lưu", + "birdClassification": { + "title": "Phân loại Chim", + "desc": "Phân loại chim xác định các loài chim đã biết bằng mô hình Tensorflow lượng tử hóa. Khi một loài chim đã biết được nhận dạng, tên thông thường của nó sẽ được thêm vào dưới dạng nhãn phụ (sub_label). Thông tin này được bao gồm trong giao diện người dùng, bộ lọc, cũng như trong các thông báo." + }, + "semanticSearch": { + "title": "Tìm kiếm theo Ngữ nghĩa", + "desc": "Tìm kiếm theo Ngữ nghĩa trong Frigate cho phép bạn tìm các đối tượng được theo dõi trong các mục xem lại của mình bằng cách sử dụng chính hình ảnh, mô tả văn bản do người dùng xác định hoặc mô tả được tạo tự động.", + "reindexNow": { + "desc": "Việc tái lập chỉ mục sẽ tạo lại các nhúng (embeddings) cho tất cả các đối tượng được theo dõi. Quá trình này chạy ở chế độ nền và có thể làm CPU của bạn hoạt động tối đa và mất một khoảng thời gian đáng kể tùy thuộc vào số lượng đối tượng bạn đã theo dõi.", + "confirmTitle": "Xác nhận Tái lập chỉ mục", + "confirmDesc": "Bạn có chắc muốn tái lập chỉ mục cho tất cả các nhúng của đối tượng được theo dõi không? Quá trình này sẽ chạy ở chế độ nền nhưng có thể làm CPU của bạn hoạt động tối đa và mất một khoảng thời gian đáng kể. Bạn có thể theo dõi tiến trình trên trang Khám phá.", + "success": "Đã bắt đầu tái lập chỉ mục thành công.", + "alreadyInProgress": "Quá trình tái lập chỉ mục đang được tiến hành.", + "error": "Không thể bắt đầu tái lập chỉ mục: {{errorMessage}}", + "label": "Tái lập chỉ mục ngay", + "confirmButton": "Tái lập chỉ mục" + }, + "modelSize": { + "label": "Kích thước Mô hình", + "desc": "Kích thước của mô hình được sử dụng cho các nhúng tìm kiếm theo ngữ nghĩa.", + "small": { + "title": "nhỏ", + "desc": "Sử dụng mô hình nhỏ sẽ dùng phiên bản lượng tử hóa của mô hình, tiêu thụ ít RAM hơn và chạy nhanh hơn trên CPU với sự khác biệt không đáng kể về chất lượng nhúng." + }, + "large": { + "title": "lớn", + "desc": "Sử dụng mô hình lớn sẽ dùng mô hình Jina đầy đủ và sẽ tự động chạy trên GPU nếu có." + } + }, + "readTheDocumentation": "Đọc tài liệu" + }, + "faceRecognition": { + "title": "Nhận dạng Khuôn mặt", + "desc": "Nhận dạng khuôn mặt cho phép gán tên cho người và khi khuôn mặt của họ được nhận dạng, Frigate sẽ gán tên của người đó làm nhãn phụ. Thông tin này được bao gồm trong giao diện người dùng, bộ lọc, cũng như trong các thông báo.", + "readTheDocumentation": "Đọc tài liệu", + "modelSize": { + "label": "Kích thước Mô hình", + "desc": "Kích thước của mô hình được sử dụng để nhận dạng khuôn mặt.", + "small": { + "title": "nhỏ", + "desc": "Sử dụng mô hình nhỏ sẽ dùng mô hình nhúng khuôn mặt FaceNet, chạy hiệu quả trên hầu hết các CPU." + }, + "large": { + "title": "lớn", + "desc": "Sử dụng mô hình lớn sẽ dùng mô hình nhúng khuôn mặt ArcFace và sẽ tự động chạy trên GPU nếu có." + } + } + }, + "licensePlateRecognition": { + "title": "Nhận dạng Biển số xe", + "desc": "Frigate có thể nhận dạng biển số xe trên các phương tiện và tự động thêm các ký tự được phát hiện vào trường recognized_license_plate hoặc một tên đã biết làm nhãn phụ cho các đối tượng thuộc loại ô tô. Một trường hợp sử dụng phổ biến có thể là đọc biển số xe ô tô đi vào đường lái xe hoặc ô tô đi ngang qua trên đường phố.", + "readTheDocumentation": "Đọc tài liệu" + }, + "restart_required": "Yêu cầu khởi động lại (cài đặt Làm giàu Dữ liệu đã thay đổi)", + "toast": { + "success": "Cài đặt Làm giàu Dữ liệu đã được lưu. Khởi động lại Frigate để áp dụng các thay đổi của bạn.", + "error": "Không thể lưu các thay đổi cấu hình: {{errorMessage}}" + } + }, + "menu": { + "frigateplus": "Frigate+", + "ui": "Giao diện người dùng", + "masksAndZones": "Mặt nạ / Vùng", + "debug": "Gỡ lỗi", + "users": "Người dùng", + "notifications": "Thông báo", + "motionTuner": "Tinh chỉnh Chuyển động", + "cameras": "Cài đặt Camera", + "enrichments": "Làm giàu Dữ liệu", + "triggers": "Sự kiện kích hoạt", + "cameraManagement": "Quản lý", + "cameraReview": "Đánh giá", + "roles": "Vai trò" + }, + "cameraSetting": { + "camera": "Máy quay", + "noCamera": "Không có Camera" + }, + "triggers": { + "documentTitle": "Sự kiện kích hoạt", + "management": { + "title": "Sự kiện kích hoạt", + "desc": "Quản lý sự kiện kích hoạt cho {{camera}}. Sử dụng kiểu \"ảnh xem trước\" để kích hoạt dựa trên ảnh xem trước tương tự cho đối tượng cần theo dõi đã chọn, và kiểu \"mô tả\" để kích hoạt dựa trên những mô tả tương tự cho đoạn văn bản bạn đã chỉ định." + }, + "addTrigger": "Thêm sự kiện kích hoạt", + "table": { + "content": "Nội dung", + "threshold": "Ngưỡng", + "actions": "Hành động", + "noTriggers": "Không có sự kiện kích hoạt được cài đặt cho máy quay này.", + "type": "Kiểu", + "name": "Tên", + "deleteTrigger": "Xóa sự kiện kích hoạt", + "lastTriggered": "Lần kích hoạt gần nhất", + "edit": "Chỉnh sửa" + }, + "type": { + "description": "Mô tả", + "thumbnail": "Ảnh xem trước" + }, + "dialog": { + "form": { + "enabled": { + "description": "Kích hoạt hoặc vô hiệu hóa sự kiện kích hoạt này" + }, + "actions": { + "title": "Các hành động", + "desc": "Theo mặc định, Frigate kích hoạt thông báo MQTT cho tất cả các trình kích hoạt. Nhãn phụ thêm tên kích hoạt vào nhãn đối tượng. Thuộc tính là siêu dữ liệu có thể tìm kiếm được lưu trữ riêng biệt trong siêu dữ liệu đối tượng được theo dõi.", + "error": { + "min": "Phải chọn ít nhất một hành động." + } + }, + "name": { + "title": "Tên", + "placeholder": "Tên sự kiện kích hoạt", + "error": { + "minLength": "Trường phải dài ít nhất 2 ký tự.", + "invalidCharacters": "Trường chỉ có thể chứa các chữ cái, số, dấu gạch dưới và dấu gạch nối.", + "alreadyExists": "Một sự kiện kích hoạt trùng tên đã tồn tại cho máy quay này." + } + }, + "type": { + "title": "Kiểu", + "placeholder": "Chọn kiểu cho sự kiện kích hoạt" + }, + "content": { + "title": "Nội dung", + "imagePlaceholder": "Chọn một hình ảnh", + "textPlaceholder": "Nhập nội dung văn bản", + "imageDesc": "Chỉ 100 hình thu nhỏ gần đây nhất được hiển thị. Nếu bạn không thể tìm thấy hình thu nhỏ mong muốn, vui lòng xem lại các đối tượng trước đó trong Khám phá và thiết lập trình kích hoạt từ menu ở đó.", + "textDesc": "Nhập vẵn bản để kích hoạt hành động này khi một đối tượng theo dõi với mô tả tương tự được phát hiện.", + "error": { + "required": "Nội dung bắt buộc." + } + }, + "threshold": { + "title": "Ngưỡng", + "error": { + "min": "Ngưỡng phải ít nhất bằng 0", + "max": "Ngưỡng lớn nhất phải bé hơn 1" + } + } + }, + "createTrigger": { + "title": "Tạo sự kiện kích hoạt", + "desc": "Tạo sự kiện kích hoạt cho máy quay {{camera}}" + }, + "editTrigger": { + "title": "Chỉnh sửa Sự kiện kích hoạt", + "desc": "Chỉnh sửa cài đặt cho sự kiện kích hoạt trên máy quay {{camera}}" + }, + "deleteTrigger": { + "title": "Xóa Sự kiện kích hoạt", + "desc": "Bạn có chắc chắn muốn xóa sự kịn kích hoạt {{triggerName}}? Thao tác này không thể khôi phục được." + } + }, + "toast": { + "success": { + "createTrigger": "Sự kiện kích hoạt {{name}} đã được tạo thành công.", + "updateTrigger": "Sự kiện kích hoạt {{name}} đã được cập nhật thành công.", + "deleteTrigger": "Sự kiện kích hoạt {{name}} đã được xóa thành công." + }, + "error": { + "createTriggerFailed": "Tạo sự kiện kích hoạt thất bại: {{errorMessage}}", + "updateTriggerFailed": "Cập nhật sự kiện kích hoạt thất bại: {{errorMessage}}", + "deleteTriggerFailed": "Xóa sự kiện kích hoạt thất bại: {{errorMessage}}" + } + }, + "actions": { + "alert": "Gắn nhãn Cảnh báo", + "notification": "Gửi thông báo" + } + } +} diff --git a/web/public/locales/vi/views/system.json b/web/public/locales/vi/views/system.json new file mode 100644 index 0000000..bdaffe7 --- /dev/null +++ b/web/public/locales/vi/views/system.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "storage": "Thống kê lưu trữ - Frigate", + "general": "Thống kê Chung - Frigate", + "enrichments": "Thống kê Làm giàu Dữ liệu - Frigate", + "logs": { + "frigate": "Nhật ký Frigate - Frigate", + "go2rtc": "Nhật ký Go2RTC - Frigate", + "nginx": "Nhật ký Nginx - Frigate" + }, + "cameras": "Thống kê Camera - Frigate" + }, + "general": { + "hardwareInfo": { + "npuUsage": "Mức sử dụng NPU", + "npuMemory": "Bộ nhớ NPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Đầu ra Vainfo", + "returnCode": "Mã trả về: {{code}}", + "processOutput": "Đầu ra Tiến trình:", + "processError": "Lỗi Tiến trình:" + }, + "nvidiaSMIOutput": { + "title": "Đầu ra Nvidia SMI", + "name": "Tên: {{name}}", + "driver": "Trình điều khiển: {{driver}}", + "cudaComputerCapability": "Khả năng Tính toán CUDA: {{cuda_compute}}", + "vbios": "Thông tin VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Đóng thông tin GPU" + }, + "copyInfo": { + "label": "Sao chép thông tin GPU" + }, + "toast": { + "success": "Đã sao chép thông tin GPU vào clipboard" + } + }, + "title": "Thông tin Phần cứng", + "gpuUsage": "Mức sử dụng GPU", + "gpuMemory": "Bộ nhớ GPU", + "gpuEncoder": "Bộ mã hóa GPU", + "gpuDecoder": "Bộ giải mã GPU", + "intelGpuWarning": { + "title": "Cảnh báo thống kê GPU Intel", + "message": "Không có số liệu thống kê GPU", + "description": "Đây là lỗi đã biết trong công cụ báo cáo thống kê GPU của Intel (intel_gpu_top), khi nó bị trục trặc và liên tục trả về mức sử dụng GPU là 0%, dù thực tế phần cứng tăng tốc và nhận diện đối tượng đang hoạt động đúng trên (i)GPU. Đây không phải lỗi của Frigate. Bạn có thể khởi động lại máy chủ để tạm thời khắc phục và xác nhận GPU vẫn hoạt động bình thường. Điều này không ảnh hưởng đến hiệu suất." + } + }, + "otherProcesses": { + "processCpuUsage": "Mức sử dụng CPU của Tiến trình", + "processMemoryUsage": "Mức sử dụng Bộ nhớ của Tiến trình", + "title": "Các Tiến trình Khác" + }, + "detector": { + "temperature": "Nhiệt độ Bộ phát hiện", + "memoryUsage": "Mức sử dụng Bộ nhớ của Bộ phát hiện", + "title": "Bộ phát hiện", + "inferenceSpeed": "Tốc độ Suy luận của Bộ phát hiện", + "cpuUsage": "Mức sử dụng CPU của Bộ phát hiện", + "cpuUsageInformation": "Dùng CPU để chuẩn bị đầu vào và ngõ ra dữ liệu dùng cho mẫu nhận dạng. Giá trị này không đo lường mức sử dụng suy luận, ngay cả khi sử dụng GPU hoặc bộ tăng tốc." + }, + "title": "Chung" + }, + "storage": { + "overview": "Tổng quan", + "cameraStorage": { + "title": "Lưu trữ Camera", + "camera": "Camera", + "unusedStorageInformation": "Thông tin Lưu trữ Chưa sử dụng", + "storageUsed": "Lưu trữ", + "percentageOfTotalUsed": "Tổng phần trăm", + "bandwidth": "Băng thông", + "unused": { + "title": "Chưa sử dụng", + "tips": "Giá trị này có thể không phản ánh chính xác dung lượng trống có sẵn cho Frigate nếu bạn có các tệp khác được lưu trữ trên ổ đĩa ngoài các bản ghi của Frigate. Frigate không theo dõi việc sử dụng dung lượng lưu trữ bên ngoài các bản ghi của nó." + } + }, + "title": "Lưu trữ", + "recordings": { + "title": "Bản ghi", + "tips": "Giá trị này thể hiện tổng dung lượng lưu trữ được sử dụng bởi các bản ghi trong cơ sở dữ liệu của Frigate. Frigate không theo dõi việc sử dụng dung lượng lưu trữ cho tất cả các tệp trên đĩa của bạn.", + "earliestRecording": "Bản ghi sớm nhất hiện có:" + }, + "shm": { + "title": "Sắp xếp bộ nhớ được chia sẻ (SHM)", + "warning": "Bộ nhớ chia sẻ hiện tại quá thấp {{total}}MB. Tăng lên tối thiểu là {{min_shm}}MB." + } + }, + "cameras": { + "label": { + "detect": "phát hiện", + "skipped": "bỏ qua", + "ffmpeg": "FFmpeg", + "capture": "ghi hình", + "overallDetectionsPerSecond": "tổng số phát hiện mỗi giây", + "cameraFramesPerSecond": "{{camName}} khung hình mỗi giây", + "cameraDetectionsPerSecond": "{{camName}} phát hiện mỗi giây", + "overallFramesPerSecond": "tổng số khung hình mỗi giây", + "camera": "camera", + "overallSkippedDetectionsPerSecond": "tổng số phát hiện bị bỏ qua mỗi giây", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} ghi hình", + "cameraDetect": "{{camName}} phát hiện", + "cameraSkippedDetectionsPerSecond": "{{camName}} phát hiện bị bỏ qua mỗi giây" + }, + "toast": { + "success": { + "copyToClipboard": "Đã sao chép dữ liệu thăm dò vào clipboard." + }, + "error": { + "unableToProbeCamera": "Không thể thăm dò camera: {{errorMessage}}" + } + }, + "info": { + "stream": "Luồng {{idx}}", + "streamDataFromFFPROBE": "Dữ liệu luồng được lấy bằng ffprobe.", + "video": "Video:", + "fetching": "Đang tìm nạp Dữ liệu Camera", + "codec": "Codec:", + "unknown": "Không xác định", + "audio": "Âm thanh:", + "error": "Lỗi: {{error}}", + "tips": { + "title": "Thông tin Thăm dò Camera" + }, + "resolution": "Độ phân giải:", + "fps": "FPS:", + "cameraProbeInfo": "Thông tin Thăm dò Camera {{camera}}", + "aspectRatio": "tỉ lệ khung hình" + }, + "overview": "Tổng quan", + "framesAndDetections": "Khung hình / Phát hiện", + "title": "Camera" + }, + "lastRefreshed": "Làm mới lần cuối: ", + "stats": { + "detectIsSlow": "{{detect}} đang chậm ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} đang rất chậm ({{speed}} ms)", + "cameraIsOffline": "{{camera}} đang ngoại tuyến", + "ffmpegHighCpuUsage": "{{camera}} có mức sử dụng CPU FFmpeg cao ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} có mức sử dụng CPU phát hiện cao ({{detectAvg}}%)", + "healthy": "Hệ thống đang hoạt động tốt", + "reindexingEmbeddings": "Đang lập chỉ mục lại các embedding (hoàn thành {{processed}}%)", + "shmTooLow": "/dev/shm ({{total}} MB) cần được tăng lên tối thiểu {{min}} MB." + }, + "enrichments": { + "embeddings": { + "image_embedding": "Embedding Hình ảnh", + "text_embedding_speed": "Tốc độ Embedding Văn bản", + "face_embedding_speed": "Tốc độ Embedding Khuôn mặt", + "text_embedding": "Embedding Văn bản", + "face_recognition": "Nhận dạng Khuôn mặt", + "plate_recognition": "Nhận dạng Biển số", + "image_embedding_speed": "Tốc độ Embedding Hình ảnh", + "face_recognition_speed": "Tốc độ Nhận dạng Khuôn mặt", + "plate_recognition_speed": "Tốc độ Nhận dạng Biển số", + "yolov9_plate_detection_speed": "Tốc độ Phát hiện Biển số YOLOv9", + "yolov9_plate_detection": "Phát hiện Biển số YOLOv9", + "review_description": "Đánh giá mô tả", + "review_description_speed": "Đánh giá Mô tả Tốc độ", + "review_description_events_per_second": "Đánh giá mô tả", + "object_description": "Mô tả đối tượng", + "object_description_speed": "Đối tượng Mô tả Tốc độ", + "object_description_events_per_second": "Mô tả đối tượng" + }, + "title": "Làm giàu Dữ liệu", + "infPerSecond": "Suy luận Mỗi Giây", + "averageInf": "Thời gian suy luận trung bình" + }, + "title": "Hệ thống", + "metrics": "Số liệu hệ thống", + "logs": { + "download": { + "label": "Tải xuống Nhật ký" + }, + "copy": { + "label": "Sao chép vào Clipboard", + "success": "Đã sao chép nhật ký vào clipboard", + "error": "Không thể sao chép nhật ký vào clipboard" + }, + "type": { + "label": "Loại", + "timestamp": "Dấu thời gian", + "tag": "Thẻ", + "message": "Thông báo" + }, + "tips": "Nhật ký đang được truyền trực tiếp từ máy chủ", + "toast": { + "error": { + "fetchingLogsFailed": "Lỗi khi tìm nạp nhật ký: {{errorMessage}}", + "whileStreamingLogs": "Lỗi trong khi truyền trực tiếp nhật ký: {{errorMessage}}" + } + } + } +} diff --git a/web/public/locales/yue-Hant/audio.json b/web/public/locales/yue-Hant/audio.json new file mode 100644 index 0000000..c25ece5 --- /dev/null +++ b/web/public/locales/yue-Hant/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "講話", + "babbling": "牙牙學語", + "yell": "大嗌", + "bellow": "咆哮", + "whoop": "歡呼聲", + "whispering": "細細聲", + "laughter": "笑聲", + "sigh": "歎氣聲", + "crying": "喊聲", + "yodeling": "山歌", + "choir": "合唱", + "snicker": "偷笑聲", + "mantra": "咒語", + "singing": "歌聲", + "chant": "唸經", + "breathing": "呼吸聲", + "child_singing": "兒童歌聲", + "rapping": "饒舌", + "humming": "哼歌", + "whistling": "口哨聲", + "synthetic_singing": "人造歌聲", + "groan": "呻吟聲", + "grunt": "哼聲", + "snort": "哼哼聲", + "throat_clearing": "清喉嚨", + "wheeze": "氣喘聲", + "snoring": "鼻鼾聲", + "gasp": "喘氣", + "pant": "急促喘氣", + "cough": "咳嗽", + "sneeze": "打乞嚏", + "shuffle": "拖步行", + "sniff": "嗅嗅聲", + "footsteps": "腳步聲", + "chewing": "咀嚼聲", + "biting": "咬嘢聲", + "gargling": "漱口聲", + "run": "跑步", + "stomach_rumble": "肚餓聲", + "burping": "打嗝聲", + "clapping": "掌聲", + "children_playing": "兒童玩耍聲", + "applause": "掌聲", + "heartbeat": "心跳", + "growling": "狗咆哮聲", + "bow_wow": "狗汪汪聲", + "caterwaul": "貓嚎叫", + "howl": "狗慘叫聲", + "livestock": "牲畜", + "clip_clop": "馬蹄聲", + "cattle": "牛", + "horse": "馬", + "heart_murmur": "心臟雜音", + "fart": "放屁", + "hands": "手", + "cheering": "歡呼聲", + "dog": "狗", + "bark": "樹皮", + "yip": "狗尖叫聲", + "chatter": "嘈雜聲", + "purr": "貓呼嚕聲", + "whimper_dog": "狗嗚咽聲", + "hiccup": "打嗝聲", + "finger_snapping": "彈手指聲", + "crowd": "人群聲", + "animal": "動物", + "pets": "寵物", + "cat": "貓", + "meow": "貓喵喵聲", + "hiss": "貓嘶嘶聲", + "neigh": "馬嘶聲", + "cowbell": "牛鈴", + "moo": "牛哞哞聲", + "gobble": "火雞叫聲", + "turkey": "火雞", + "chicken": "雞", + "cluck": "雞咯咯聲", + "fowl": "家禽", + "sheep": "羊", + "duck": "鴨子", + "goat": "山羊", + "pig": "豬", + "oink": "豬哼聲", + "bleat": "咩咩聲", + "cock_a_doodle_doo": "公雞叫聲", + "honk": "鵝叫聲", + "quack": "鴨叫聲", + "goose": "鵝", + "wild_animals": "野生動物", + "crow": "烏鴉", + "coo": "白鴿咕咕聲", + "pigeon": "白鴿", + "roaring_cats": "貓咆哮聲", + "roar": "咆哮聲", + "bird": "鳥", + "chirp": "鳥啾啾聲", + "squawk": "鳥嘎嘎聲", + "caw": "烏鴉呱呱聲", + "mouse": "滑鼠", + "owl": "貓頭鷹", + "rats": "大老鼠", + "hoot": "貓頭鷹咕咕聲", + "patter": "老鼠腳步聲", + "flapping_wings": "拍打翅膀聲", + "dogs": "狗", + "snake": "蛇", + "insect": "昆蟲", + "whale_vocalization": "鯨魚叫聲", + "rattle": "蛇叫聲", + "fly": "蒼蠅", + "croak": "青蛙呱呱聲", + "mosquito": "蚊", + "music": "音樂", + "frog": "青蛙", + "cricket": "蟋蟀", + "buzz": "嗡嗡聲", + "musical_instrument": "樂器", + "steel_guitar": "鋼弦結他", + "tapping": "拍擊", + "guitar": "結他", + "strum": "撥弦聲", + "electric_guitar": "電結他", + "plucked_string_instrument": "撥弦樂器", + "bass_guitar": "低音結他", + "banjo": "班祖琴", + "acoustic_guitar": "原聲結他", + "piano": "鋼琴", + "synthesizer": "合成器", + "keyboard": "鍵盤", + "organ": "風琴", + "sitar": "錫塔琴", + "mandolin": "曼陀鈴", + "zither": "齊特琴", + "ukulele": "烏克麗麗", + "sampler": "採樣器", + "hammond_organ": "哈蒙德風琴", + "electric_piano": "電子鋼琴", + "electronic_organ": "電子風琴", + "rimshot": "鼓邊敲擊", + "bass_drum": "低音鼓", + "drum_kit": "鼓套", + "drum_machine": "鼓機", + "drum": "鼓", + "snare_drum": "小鼓", + "timpani": "定音鼓", + "drum_roll": "鼓聲", + "harpsichord": "大鍵琴", + "percussion": "打擊樂器", + "trumpet": "小號", + "cymbal": "銅鈸", + "french_horn": "法國號", + "string_section": "弦樂組", + "saxophone": "色士風", + "marimba": "馬林巴琴", + "mallet_percussion": "鎚擊樂器", + "maraca": "沙槌", + "harp": "豎琴", + "orchestra": "管弦樂團", + "violin": "小提琴", + "gong": "鑼", + "flute": "長笛", + "bowed_string_instrument": "弓弦樂器", + "vibraphone": "顫音琴", + "tabla": "塔布拉鼓", + "trombone": "長號", + "tambourine": "鈴鼓", + "double_bass": "低音提琴", + "brass_instrument": "銅管樂器", + "cello": "大提琴", + "clarinet": "單簧管", + "pizzicato": "撥奏", + "hi_hat": "高帽鈸", + "wood_block": "木魚", + "tubular_bells": "管鐘", + "glockenspiel": "鐘琴", + "steelpan": "鋼鼓", + "wind_instrument": "管樂器", + "bell": "鐘", + "bicycle_bell": "單車鈴", + "wind_chime": "風鈴", + "church_bell": "教堂鐘聲", + "chime": "鈴聲", + "tuning_fork": "音叉", + "jingle_bell": "鈴鐺", + "accordion": "手風琴", + "bagpipes": "風笛", + "didgeridoo": "迪吉里杜管", + "theremin": "特雷門琴", + "singing_bowl": "頌缽", + "scratching": "抓碟聲", + "pop_music": "流行音樂", + "hip_hop_music": "嘻哈音樂", + "harmonica": "口琴", + "beatboxing": "人聲節奏", + "country": "鄉村音樂", + "water": "水", + "scary_music": "恐怖音樂", + "music_of_asia": "亞洲音樂", + "dance_music": "舞曲", + "video_game_music": "電子遊戲音樂", + "thunderstorm": "雷雨", + "bluegrass": "藍草音樂", + "train_wheels_squealing": "火車車輪聲", + "techno": "電子舞曲", + "new-age_music": "新世紀音樂", + "background_music": "背景音樂", + "theme_music": "主題音樂", + "jingle": "鈴聲", + "bus": "巴士", + "emergency_vehicle": "緊急車輛", + "aircraft": "飛機", + "ice_cream_truck": "雪糕車", + "dubstep": "杜步音樂", + "aircraft_engine": "飛機引擎聲", + "funk": "放克音樂", + "lullaby": "搖籃曲", + "rain": "雨", + "happy_music": "快樂音樂", + "music_of_latin_america": "拉丁美洲音樂", + "soundtrack_music": "配樂", + "rock_music": "搖滾樂", + "rock_and_roll": "搖滾樂", + "psychedelic_rock": "迷幻搖滾", + "rhythm_and_blues": "節奏藍調", + "soul_music": "靈魂音樂", + "reggae": "雷鬼音樂", + "folk_music": "民謠音樂", + "middle_eastern_music": "中東音樂", + "jazz": "爵士樂", + "disco": "迪斯可音樂", + "classical_music": "古典音樂", + "opera": "歌劇", + "electronic_music": "電子音樂", + "house_music": "浩室音樂", + "electronic_dance_music": "電子舞曲", + "ambient_music": "環境音樂", + "trance_music": "迷幻音樂", + "salsa_music": "薩爾薩音樂", + "flamenco": "佛朗明哥", + "blues": "藍調音樂", + "music_for_children": "兒童音樂", + "vocal_music": "聲樂", + "a_capella": "無伴奏合唱", + "music_of_africa": "非洲音樂", + "afrobeat": "非洲節拍", + "christian_music": "基督教音樂", + "gospel_music": "福音音樂", + "music_of_bollywood": "寶萊塢音樂", + "ska": "斯卡音樂", + "traditional_music": "傳統音樂", + "independent_music": "獨立音樂", + "song": "歌曲", + "raindrop": "雨點聲", + "rain_on_surface": "雨打地面聲", + "stream": "小溪", + "waterfall": "瀑布", + "ocean": "海洋", + "waves": "波浪", + "steam": "蒸氣", + "gurgling": "咕嚕聲", + "fire": "火", + "crackle": "劈啪聲", + "vehicle": "車輛", + "boat": "船", + "sailboat": "帆船", + "rowboat": "划艇", + "motorboat": "機動船", + "ship": "船", + "motor_vehicle": "機動車", + "car": "車", + "toot": "汽車響咹聲", + "car_alarm": "汽車防盜器", + "power_windows": "電動車窗", + "skidding": "車胎打滑聲", + "tire_squeal": "車胎尖叫聲", + "car_passing_by": "車駛過聲", + "race_car": "賽車", + "truck": "貨車", + "air_brake": "煞車聲", + "air_horn": "空氣喇叭", + "police_car": "警車", + "ambulance": "救護車", + "fire_engine": "消防車", + "motorcycle": "電單車", + "rail_transport": "鐵路運輸", + "train_whistle": "火車汽笛聲", + "train_horn": "火車喇叭聲", + "railroad_car": "火車車廂", + "propeller": "螺旋槳", + "helicopter": "直升機", + "fixed-wing_aircraft": "固定翼飛機", + "bicycle": "單車", + "skateboard": "滑板", + "engine": "引擎", + "light_engine": "小型引擎", + "dental_drill's_drill": "牙科鑽機", + "lawn_mower": "剪草機", + "chainsaw": "電鋸", + "medium_engine": "中型引擎", + "heavy_engine": "大型引擎", + "engine_knocking": "引擎敲擊聲", + "engine_starting": "引擎啟動聲", + "idling": "引擎空轉聲", + "accelerating": "加速聲", + "door": "門", + "doorbell": "門鈴", + "ding-dong": "叮咚聲", + "sliding_door": "趟門", + "knock": "敲門", + "tap": "輕敲門", + "squeak": "吱吱聲", + "cupboard_open_or_close": "櫃門開關聲", + "drawer_open_or_close": "抽屜開關聲", + "dishes": "餐具聲", + "cutlery": "刀叉", + "chopping": "切菜聲", + "frying": "炒煮", + "microwave_oven": "微波爐", + "water_tap": "水龍頭", + "electric_toothbrush": "電動牙刷", + "vacuum_cleaner": "吸塵機", + "zipper": "拉鍊", + "keys_jangling": "鎖匙碰撞聲", + "coin": "硬幣", + "scissors": "剪刀", + "electric_shaver": "電鬚刨", + "shuffling_cards": "洗牌", + "typing": "打字", + "computer_keyboard": "電腦鍵盤", + "telephone": "電話", + "telephone_bell_ringing": "電話鈴聲", + "siren": "警報聲", + "steam_whistle": "蒸氣汽笛", + "mechanisms": "機械聲", + "ratchet": "棘輪聲", + "clock": "時鐘", + "tick": "滴答聲", + "tick-tock": "滴答滴答聲", + "sewing_machine": "衣車", + "mechanical_fan": "機械風扇", + "printer": "印表機", + "camera": "鏡頭", + "single-lens_reflex_camera": "單反相機", + "tools": "工具", + "hammer": "鎚仔", + "sawing": "鋸", + "filing": "銼", + "sanding": "打磨", + "power_tool": "電動工具", + "drill": "鑽", + "explosion": "爆炸", + "fusillade": "連環射擊", + "artillery_fire": "火砲", + "cap_gun": "玩具槍", + "fireworks": "煙花", + "firecracker": "炮仗", + "burst": "爆裂", + "eruption": "爆發", + "wood": "木頭", + "chop": "劈木聲", + "splinter": "木刺聲", + "crack": "裂聲", + "shatter": "破碎聲", + "silence": "寂靜", + "sound_effect": "音效", + "white_noise": "白噪音", + "pink_noise": "粉紅噪音", + "television": "電視", + "radio": "收音機", + "field_recording": "現場錄音", + "angry_music": "憤怒音樂", + "sad_music": "悲傷音樂", + "wind": "風", + "wind_noise": "風聲", + "drum_and_bass": "鼓和貝斯", + "wedding_music": "婚禮音樂", + "progressive_rock": "前衛搖滾", + "grunge": "垃圾搖滾", + "punk_rock": "朋克搖滾", + "christmas_music": "聖誕音樂", + "subway": "地鐵", + "thunder": "雷聲", + "carnatic_music": "卡納蒂克音樂", + "traffic_noise": "交通噪音", + "train": "火車", + "slam": "砰門聲", + "tender_music": "溫柔音樂", + "reversing_beeps": "倒車提示聲", + "heavy_metal": "重金屬音樂", + "jet_engine": "噴射機引擎聲", + "rustling_leaves": "樹葉沙沙聲", + "electronica": "電子樂", + "swing_music": "搖擺音樂", + "exciting_music": "刺激音樂", + "ringtone": "鈴聲", + "pulleys": "滑輪", + "jackhammer": "風鑽", + "writing": "寫作", + "toilet_flush": "沖廁", + "whistle": "哨子聲", + "gunshot": "槍聲", + "alarm_clock": "鬧鐘", + "dial_tone": "電話按號聲", + "boom": "轟隆", + "typewriter": "打字機", + "blender": "攪拌機", + "toothbrush": "牙刷", + "cash_register": "收銀機", + "civil_defense_siren": "民防警報", + "machine_gun": "機關槍", + "sink": "洗手盆", + "fire_alarm": "火警鐘", + "bathtub": "浴缸", + "busy_signal": "線路繁忙聲", + "smoke_detector": "煙霧偵測器", + "hair_dryer": "吹風機", + "alarm": "警報", + "gears": "齒輪", + "telephone_dialing": "電話撥號聲", + "foghorn": "霧號", + "buzzer": "蜂鳴器聲", + "air_conditioning": "冷氣機", + "glass": "玻璃", + "chink": "碰撞聲", + "environmental_noise": "環境噪音", + "static": "靜電聲", + "scream": "尖叫聲", + "sodeling": "約德爾唱法", + "chird": "鳥鳴聲", + "change_ringing": "變化鐘聲", + "shofar": "羊角號聲", + "liquid": "液體聲", + "splash": "潑水聲", + "slosh": "晃水聲", + "squish": "擠壓濕聲", + "drip": "滴水聲", + "pour": "倒水聲", + "trickle": "細流聲", + "gush": "湧出聲", + "fill": "注滿聲", + "spray": "噴灑聲", + "pump": "抽水聲", + "stir": "攪拌聲", + "boiling": "沸騰聲", + "sonar": "聲納聲", + "arrow": "箭飛聲", + "whoosh": "呼嘯聲", + "thump": "悶撞聲", + "thunk": "咚一聲", + "electronic_tuner": "電子調音器聲", + "effects_unit": "效果器聲", + "chorus_effect": "合唱效果", + "basketball_bounce": "籃球彈地聲", + "bang": "砰聲", + "slap": "拍打聲", + "whack": "重擊聲", + "smash": "粉碎聲", + "breaking": "破裂聲", + "bouncing": "彈跳聲", + "whip": "鞭甩聲", + "flap": "拍翼聲", + "scratch": "抓刮聲", + "scrape": "刮擦聲", + "rub": "摩擦聲", + "roll": "滾動聲", + "crushing": "壓碎聲", + "crumpling": "揉皺聲", + "tearing": "撕裂聲", + "beep": "嗶聲", + "ping": "乒聲", + "ding": "叮聲", + "clang": "鏗鏘聲", + "squeal": "尖叫聲", + "creak": "吱吱聲", + "rustle": "沙沙聲", + "whir": "嗡轉聲", + "clatter": "叮噹雜響", + "sizzle": "滋滋聲", + "clicking": "喀嗒聲", + "clickety_clack": "喀嚓喀嚓聲", + "rumble": "隆隆聲", + "plop": "撲通聲", + "hum": "嗡聲", + "zing": "嗖聲", + "boing": "彈簧彈聲", + "crunch": "咔嚓碎裂聲", + "sine_wave": "正弦波", + "harmonic": "諧波", + "chirp_tone": "啁啾音", + "pulse": "脈衝聲", + "inside": "室內聲", + "outside": "室外聲", + "reverberation": "混響", + "echo": "回聲", + "noise": "噪音", + "mains_hum": "電源嗡聲", + "distortion": "失真", + "sidetone": "側音", + "cacophony": "嘈雜聲", + "throbbing": "搏動聲", + "vibration": "振動聲" +} diff --git a/web/public/locales/yue-Hant/common.json b/web/public/locales/yue-Hant/common.json new file mode 100644 index 0000000..5a0f449 --- /dev/null +++ b/web/public/locales/yue-Hant/common.json @@ -0,0 +1,307 @@ +{ + "time": { + "untilForTime": "直到 {{time}}", + "untilRestart": "直到重新啟動", + "yesterday": "昨日", + "last7": "過去7日", + "last14": "過去14日", + "last30": "過去30日", + "thisWeek": "今個星期", + "lastMonth": "上個月", + "10minutes": "10分鐘", + "12hours": "12 小時", + "24hours": "24 小時", + "am": "上午", + "year_other": "{{time}}年", + "mo": "{{time}}月", + "m": "{{time}}分鐘", + "minute_other": "{{time}}分鐘", + "formattedTimestamp": { + "12hour": "M月d日 ah:mm:ss", + "24hour": "M月d日 HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "a h:mm", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "ah:mm:ss", + "24hour": "HH:mm:ss" + }, + "formattedTimestampFilename": { + "24hour": "yy年MM月dd日 HH時mm分ss秒", + "12hour": "yy年MM月dd日 ah時mm分ss秒" + }, + "s": "{{time}}秒", + "formattedTimestamp2": { + "12hour": "MM月dd日 ah:mm:ss", + "24hour": "MM月dd日 HH:mm:ss" + }, + "thisMonth": "今個月", + "pm": "下午", + "formattedTimestampMonthDayHourMinute": { + "24hour": "M月d日 HH:mm", + "12hour": "M月d日 ah:mm" + }, + "justNow": "剛剛", + "day_other": "{{time}}日", + "hour_other": "{{time}}小時", + "30minutes": "30分鐘", + "5minutes": "5分鐘", + "yr": "{{time}}年", + "today": "今日", + "month_other": "{{time}}月", + "second_other": "{{time}}秒", + "untilForRestart": "直到 Frigate 重新啟動。", + "ago": "{{timeAgo}} 前", + "d": "{{time}}日", + "lastWeek": "上個星期", + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "yyyy年M月d日 ah:mm", + "24hour": "yyyy年M月d日 HH:mm" + }, + "1hour": "1 小時", + "h": "{{time}}小時", + "formattedTimestampMonthDay": "M月d日", + "formattedTimestampMonthDayYear": { + "24hour": "yy年MM月dd日", + "12hour": "yy年MM月dd日" + }, + "never": "從不", + "inProgress": "進行中", + "invalidStartTime": "開始時間無效", + "invalidEndTime": "結束時間無效" + }, + "unit": { + "speed": { + "mph": "英里/小時", + "kph": "公里/小時" + }, + "length": { + "feet": "呎", + "meters": "米" + }, + "data": { + "kbps": "kB/秒", + "mbps": "MB/秒", + "gbps": "GB/秒", + "kbph": "kB/小時", + "mbph": "MB/小時", + "gbph": "GB/小時" + } + }, + "label": { + "back": "返回", + "hide": "隱藏 {{item}}", + "show": "顯示 {{item}}", + "ID": "編號", + "none": "無", + "all": "全部", + "other": "其他" + }, + "button": { + "apply": "套用", + "reset": "重置", + "done": "完成", + "enabled": "已啟用", + "enable": "啟用", + "disabled": "已停用", + "disable": "停用", + "save": "儲存", + "saving": "儲存中…", + "cancel": "取消", + "close": "關閉", + "copy": "複製", + "back": "返回", + "history": "歷史記錄", + "fullscreen": "全螢幕", + "exitFullscreen": "離開全螢幕", + "pictureInPicture": "畫中畫", + "twoWayTalk": "雙向通話", + "cameraAudio": "鏡頭音訊", + "suspended": "已暫停", + "export": "匯出", + "deleteNow": "立即刪除", + "next": "下一步", + "play": "播放", + "no": "否", + "copyCoordinates": "複製座標", + "delete": "刪除", + "off": "關閉", + "edit": "編輯", + "on": "開啟", + "yes": "是", + "info": "資訊", + "download": "下載", + "unsuspended": "取消暫停", + "unselect": "取消選取", + "continue": "繼續", + "add": "新增", + "undo": "復原", + "copiedToClipboard": "已複製到剪貼簿", + "modified": "已修改", + "overridden": "已覆寫", + "resetToGlobal": "重設為全域設定", + "resetToDefault": "重設為預設值", + "saveAll": "全部儲存", + "savingAll": "正在儲存全部…", + "undoAll": "全部復原", + "applying": "套用中…" + }, + "menu": { + "system": "系統", + "systemMetrics": "系統指標", + "configuration": "設定", + "systemLogs": "系統日誌", + "settings": "設定", + "configurationEditor": "設定編輯器", + "languages": "語言", + "language": { + "en": "English (英文)", + "es": "Español (西班牙文)", + "zhCN": "简体中文 (簡體中文)", + "hi": "हिन्दी (印地文)", + "fr": "Français (法文)", + "de": "Deutsch (德文)", + "ja": "日本語 (日文)", + "it": "Italiano (意大利文)", + "tr": "Türkçe (土耳其文)", + "nl": "Nederlands (荷蘭文)", + "cs": "Čeština (捷克文)", + "nb": "Norsk Bokmål (挪威文)", + "ko": "한국어 (韓文)", + "vi": "Tiếng Việt (越南文)", + "fa": "فارسی (波斯文)", + "pl": "Polski (波蘭文)", + "el": "Ελληνικά (希臘文)", + "ro": "Română (羅馬尼亞文)", + "hu": "Magyar (匈牙利文)", + "fi": "Suomi (芬蘭文)", + "da": "Dansk (丹麥文)", + "sk": "Slovenčina (斯洛伐克文)", + "withSystem": { + "label": "使用系統語言設定" + }, + "ru": "Русский (俄文)", + "sv": "Svenska (瑞典文)", + "ar": "العربية (阿拉伯文)", + "pt": "Português (葡萄牙文)", + "uk": "Українська (烏克蘭文)", + "he": "עברית (希伯來文)", + "yue": "粵語 (廣東話)", + "th": "ไทย (泰文)", + "ca": "Català (加泰羅尼亞語)", + "ptBR": "Português brasileiro (巴西葡萄牙文)", + "sr": "Српски (塞爾維亞文)", + "sl": "Slovenščina (斯洛文尼亞文)", + "lt": "Lietuvių (立陶宛文)", + "bg": "Български (保加利亞文)", + "gl": "Galego (加利西亞文)", + "id": "Bahasa Indonesia (印尼文)", + "ur": "اردو (烏爾都文)", + "hr": "Hrvatski (克羅地亞語)" + }, + "appearance": "外觀", + "darkMode": { + "label": "深色模式", + "light": "淺色", + "dark": "深色", + "withSystem": { + "label": "使用系統模式設定" + } + }, + "withSystem": "系統", + "theme": { + "label": "主題", + "blue": "藍色", + "green": "綠色", + "nord": "北歐風", + "red": "紅色", + "default": "預設", + "contrast": "高對比", + "highcontrast": "高對比度" + }, + "documentation": { + "title": "文件", + "label": "Frigate 文件" + }, + "restart": "重新啟動 Frigate", + "live": { + "title": "即時", + "allCameras": "所有鏡頭", + "cameras": { + "title": "鏡頭", + "count_other": "{{count}} 個鏡頭" + } + }, + "review": "審查", + "explore": "瀏覽", + "export": "匯出", + "uiPlayground": "UI 測試場", + "faceLibrary": "臉部資料庫", + "user": { + "title": "使用者", + "logout": "登出", + "account": "帳戶", + "current": "當前使用者:{{user}}", + "anonymous": "匿名", + "setPassword": "設定密碼" + }, + "help": "幫助", + "classification": "分類", + "actions": "行動", + "chat": "聊天" + }, + "role": { + "admin": "管理員", + "viewer": "檢視者", + "desc": "管理員擁有 Frigate UI 全功能存取權限。檢視者只能查看鏡頭、審查項目和歷史影像。", + "title": "角色" + }, + "pagination": { + "label": "分頁", + "previous": { + "title": "上一頁", + "label": "前往上一頁" + }, + "next": { + "title": "下一頁", + "label": "前往下一頁" + }, + "more": "更多頁數" + }, + "accessDenied": { + "title": "拒絕存取", + "documentTitle": "拒絕存取 - Frigate", + "desc": "你無權查看此頁面。" + }, + "selectItem": "選擇 {{item}}", + "toast": { + "save": { + "error": { + "noMessage": "儲存設定變更失敗", + "title": "儲存設定變更失敗:{{errorMessage}}" + }, + "title": "儲存" + }, + "copyUrlToClipboard": "已複製 URL 到剪貼簿。" + }, + "notFound": { + "documentTitle": "找不到頁面 - Frigate", + "desc": "找不到頁面", + "title": "404" + }, + "readTheDocumentation": "閱讀文件", + "information": { + "pixels": "{{area}}像素" + }, + "list": { + "two": "{{0}} 和 {{1}}", + "many": "{{items}}, 和 {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "選填", + "internalID": "Frigate 在設定及資料庫中使用的內部編號" + } +} diff --git a/web/public/locales/yue-Hant/components/auth.json b/web/public/locales/yue-Hant/components/auth.json new file mode 100644 index 0000000..630bc06 --- /dev/null +++ b/web/public/locales/yue-Hant/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "errors": { + "webUnknownError": "未知錯誤。請檢查控制台日誌。", + "rateLimit": "超過速率限制。請稍後再試。", + "usernameRequired": "必須填寫用戶名", + "passwordRequired": "必須填寫密碼", + "loginFailed": "登入失敗", + "unknownError": "未知錯誤。請檢查日誌。" + }, + "user": "用戶名", + "password": "密碼", + "login": "登入", + "firstTimeLogin": "首次登入?登入憑證已列印於 Frigate 日誌中。" + } +} diff --git a/web/public/locales/yue-Hant/components/camera.json b/web/public/locales/yue-Hant/components/camera.json new file mode 100644 index 0000000..ecfa463 --- /dev/null +++ b/web/public/locales/yue-Hant/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "camera": { + "setting": { + "audio": { + "tips": { + "title": "此串流必須從你的鏡頭輸出音訊並在 go2rtc 中設定。", + "document": "閱讀文件 " + } + }, + "streamMethod": { + "method": { + "continuousStreaming": { + "desc": { + "warning": "持續串流可能導致高頻寬使用及效能問題,請小心使用。", + "title": "即使沒有偵測到活動,只要在控制台上可見,鏡頭影像也會一直保持即時串流。" + }, + "label": "持續串流" + }, + "smartStreaming": { + "label": "智能串流(建議)", + "desc": "當沒有偵測到活動時,智能串流會每分鐘更新一次鏡頭影像以節省頻寬和資源。當偵測到活動時,影像會無縫切換到即時串流。" + }, + "noStreaming": { + "label": "不串流", + "desc": "鏡頭影像每分鐘只會更新一次,不會進行即時串流。" + } + }, + "label": "串流方式", + "placeholder": "選擇串流方式" + }, + "compatibilityMode": { + "label": "相容模式", + "desc": "只有當你的鏡頭串流出現色彩異常及右側有斜線時,才啟用此選項。" + }, + "label": "鏡頭串流設定", + "title": "{{cameraName}} 串流設定", + "desc": "更改此鏡頭群組控制台的即時串流選項。這些設定是裝置/瀏覽器專屬的。", + "audioIsAvailable": "此串流有提供音訊", + "audioIsUnavailable": "此串流沒有音訊", + "placeholder": "選擇串流來源", + "stream": "串流" + }, + "birdseye": "鳥瞰" + }, + "delete": { + "confirm": { + "title": "確認刪除", + "desc": "你確定要刪除鏡頭群組 {{name}} 嗎?" + }, + "label": "刪除鏡頭群組" + }, + "name": { + "errorMessage": { + "exists": "鏡頭群組名稱已存在。", + "invalid": "鏡頭群組名稱無效。", + "mustLeastCharacters": "鏡頭群組名稱必須至少包含兩個字元。", + "nameMustNotPeriod": "鏡頭群組名稱不能包含句號。" + }, + "placeholder": "請輸入名稱…", + "label": "名稱" + }, + "icon": "圖標", + "cameras": { + "desc": "為此群組選擇鏡頭。", + "label": "鏡頭" + }, + "label": "鏡頭群組", + "add": "新增鏡頭群組", + "edit": "編輯鏡頭群組", + "success": "鏡頭群組({{name}})已儲存。" + }, + "debug": { + "options": { + "label": "設定", + "title": "選項", + "showOptions": "顯示選項", + "hideOptions": "隱藏選項" + }, + "mask": "遮罩", + "boundingBox": "框選區", + "motion": "移動", + "regions": "大區域", + "timestamp": "時間戳記", + "zones": "區域" + } +} diff --git a/web/public/locales/yue-Hant/components/dialog.json b/web/public/locales/yue-Hant/components/dialog.json new file mode 100644 index 0000000..908f2c1 --- /dev/null +++ b/web/public/locales/yue-Hant/components/dialog.json @@ -0,0 +1,127 @@ +{ + "restart": { + "title": "你確定要重新啟動 Frigate 嗎?", + "button": "重新啟動", + "restarting": { + "title": "Frigate 正在重新啟動", + "content": "此頁面將在 {{countdown}} 秒後重新載入。", + "button": "立即強制重新載入" + }, + "description": "重新啟動期間將會短暫停止 Frigate。" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "提交到 Frigate+", + "desc": "位於你想避免的區域的物件並不是誤判。提交這些作為誤判會令模型混淆。" + }, + "review": { + "question": { + "label": "確認此標籤給 Frigate Plus", + "ask_a": "此物件是 {{label}} 嗎?", + "ask_an": "此物件是 {{label}} 嗎?", + "ask_full": "此物件是 {{untranslatedLabel}}({{translatedLabel}})嗎?" + }, + "state": { + "submitted": "已提交" + } + } + }, + "video": { + "viewInHistory": "在歷史記錄中查看" + } + }, + "export": { + "time": { + "fromTimeline": "從時間線選取", + "lastHour_other": "最后{{count}}小時", + "end": { + "label": "選擇結束時間", + "title": "結束時間" + }, + "custom": "自訂", + "start": { + "title": "開始時間", + "label": "選擇開始時間" + } + }, + "name": { + "placeholder": "為匯出命名" + }, + "select": "選取", + "export": "匯出", + "selectOrExport": "選取或匯出", + "toast": { + "error": { + "failed": "無法開始匯出:{{error}}", + "noVaildTimeSelected": "沒有選取有效的時間範圍", + "endTimeMustAfterStartTime": "結束時間必須在開始時間之後" + }, + "success": "成功開始匯出。請到匯出頁面看檔案。", + "view": "檢視" + }, + "fromTimeline": { + "saveExport": "儲存匯出", + "previewExport": "預覽匯出" + }, + "case": { + "label": "案例", + "placeholder": "選擇案例" + } + }, + "streaming": { + "label": "串流", + "restreaming": { + "disabled": "此鏡頭未啟用重串流。", + "desc": { + "title": "設定 go2rtc 以啟用此鏡頭的更多即時預覽選項及音訊。", + "readTheDocumentation": "閱讀文件" + } + }, + "showStats": { + "desc": "啟用此選項可在鏡頭畫面上顯示串流統計資料。", + "label": "顯示串流統計資料" + }, + "debugView": "除錯檢視" + }, + "search": { + "saveSearch": { + "label": "儲存搜尋", + "desc": "請為這個已儲存的搜尋輸入名稱。", + "placeholder": "請輸入搜尋名稱", + "overwrite": "{{searchName}} 已存在。儲存將會覆蓋現有資料。", + "button": { + "save": { + "label": "儲存此搜尋" + } + }, + "success": "搜尋({{searchName}})已儲存。" + } + }, + "recording": { + "confirmDelete": { + "title": "確認刪除", + "desc": { + "selected": "你確定要刪除與此審查項目相關的所有錄影嗎?

    按住 Shift 鍵可略過未來此對話框。" + }, + "toast": { + "success": "已成功刪除所選審查項目相關的影片片段。", + "error": "刪除失敗:{{error}}" + } + }, + "button": { + "export": "匯出", + "markAsReviewed": "標記為已審查", + "deleteNow": "立即刪除", + "markAsUnreviewed": "標記為未審查" + } + }, + "imagePicker": { + "selectImage": "選取追蹤物件縮圖", + "search": { + "placeholder": "以標籤或子標籤搜尋..." + }, + "noImages": "未找到此鏡頭的縮圖", + "unknownLabel": "已儲存的觸發影像" + } +} diff --git a/web/public/locales/yue-Hant/components/filter.json b/web/public/locales/yue-Hant/components/filter.json new file mode 100644 index 0000000..014b794 --- /dev/null +++ b/web/public/locales/yue-Hant/components/filter.json @@ -0,0 +1,140 @@ +{ + "reset": { + "label": "重設篩選條件為預設值" + }, + "subLabels": { + "all": "所有子標籤", + "label": "子標籤" + }, + "score": "分數", + "features": { + "label": "特徵", + "hasSnapshot": "有快照", + "hasVideoClip": "有影片片段", + "submittedToFrigatePlus": { + "label": "已提交到 Frigate+", + "tips": "你必須先篩選出有快照的追蹤物件。

    沒有快照的追蹤物件無法提交到 Frigate+。" + } + }, + "sort": { + "label": "排序", + "dateAsc": "日期(由舊到新)", + "dateDesc": "日期(由新到舊)", + "scoreAsc": "物件分數(由細到大)", + "scoreDesc": "物件分數(由大到細)", + "speedAsc": "預計速度(由慢到快)", + "speedDesc": "預計速度(由快到慢)", + "relevance": "相關性" + }, + "cameras": { + "label": "鏡頭篩選", + "all": { + "title": "所有鏡頭", + "short": "鏡頭" + } + }, + "review": { + "showReviewed": "顯示已審查" + }, + "motion": { + "showMotionOnly": "只顯示有移動" + }, + "explore": { + "settings": { + "title": "設定", + "defaultView": { + "summary": "摘要", + "unfilteredGrid": "未篩選網格", + "desc": "當未選取篩選條件時,顯示每個標籤最近追蹤物件的摘要,或顯示未篩選的網格。", + "title": "預設檢視" + }, + "gridColumns": { + "title": "網格欄數", + "desc": "選擇網格檢視中的欄數。" + }, + "searchSource": { + "label": "搜尋來源", + "desc": "選擇搜尋追蹤物件的縮圖還是描述。", + "options": { + "thumbnailImage": "縮圖", + "description": "描述" + } + } + }, + "date": { + "selectDateBy": { + "label": "選擇日期進行篩選" + } + } + }, + "logSettings": { + "filterBySeverity": "依嚴重程度篩選日誌", + "loading": { + "desc": "當日誌窗格捲動到底部時,新日誌將自動串流顯示。", + "title": "載入中" + }, + "label": "篩選日誌等級", + "allLogs": "所有日誌", + "disableLogStreaming": "停用日誌串流" + }, + "trackedObjectDelete": { + "title": "確認刪除", + "toast": { + "success": "成功刪除追蹤物件。", + "error": "刪除追蹤物件失敗:{{errorMessage}}" + }, + "desc": "刪除這 {{objectLength}} 個追蹤物件將會移除快照、儲存的嵌入資料,以及相關的物件生命週期記錄。歷史檢視中的錄影檔案不會被刪除。

    你確定要繼續嗎?

    按住 Shift 鍵可略過未來此對話框。" + }, + "recognizedLicensePlates": { + "loading": "載入已識別車牌中…", + "noLicensePlatesFound": "找不到車牌。", + "selectPlatesFromList": "從列表中選取一個或多個車牌。", + "placeholder": "輸入以搜尋車牌…", + "title": "已識別車牌", + "loadFailed": "載入已識別車牌失敗。", + "selectAll": "全部選取", + "clearAll": "全部清除" + }, + "estimatedSpeed": "預計速度({{unit}})", + "labels": { + "label": "標籤", + "count_one": "{{count}} 個標籤", + "all": { + "title": "所有標籤", + "short": "標籤" + }, + "count_other": "{{count}} 個標籤" + }, + "zoneMask": { + "filterBy": "按區域遮罩篩選" + }, + "zones": { + "label": "區域", + "all": { + "short": "區域", + "title": "所有區域" + } + }, + "filter": "篩選", + "dates": { + "all": { + "title": "所有日期", + "short": "日期" + }, + "selectPreset": "選擇預設設定…" + }, + "more": "更多篩選條件", + "timeRange": "時間範圍", + "classes": { + "label": "分類", + "all": { + "title": "所有分類" + }, + "count_one": "{{count}} 個分類", + "count_other": "{{count}} 個分類" + }, + "attributes": { + "label": "分類屬性", + "all": "全部屬性" + } +} diff --git a/web/public/locales/yue-Hant/components/icons.json b/web/public/locales/yue-Hant/components/icons.json new file mode 100644 index 0000000..467858b --- /dev/null +++ b/web/public/locales/yue-Hant/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "選擇圖示", + "search": { + "placeholder": "搜尋圖示…" + } + } +} diff --git a/web/public/locales/yue-Hant/components/input.json b/web/public/locales/yue-Hant/components/input.json new file mode 100644 index 0000000..ed7eee7 --- /dev/null +++ b/web/public/locales/yue-Hant/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "下載影片", + "toast": { + "success": "你的審查項目影片已開始下載。" + } + } + } +} diff --git a/web/public/locales/yue-Hant/components/player.json b/web/public/locales/yue-Hant/components/player.json new file mode 100644 index 0000000..4fe43d2 --- /dev/null +++ b/web/public/locales/yue-Hant/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "此時間段內沒有錄影", + "noPreviewFound": "找不到預覽", + "submitFrigatePlus": { + "submit": "提交", + "title": "提交此畫面至 Frigate+?" + }, + "streamOffline": { + "desc": "{{cameraName}} 的 detect 串流未接收到任何畫面,請檢查錯誤日誌", + "title": "串流已離線" + }, + "cameraDisabled": "鏡頭已停用", + "stats": { + "bandwidth": { + "short": "頻寬", + "title": "頻寬:" + }, + "latency": { + "value": "{{seconds}} 秒", + "short": { + "value": "{{seconds}} 秒", + "title": "延遲" + }, + "title": "延遲:" + }, + "totalFrames": "總畫面數:", + "droppedFrames": { + "short": { + "title": "已丟棄", + "value": "{{droppedFrames}} 個畫面" + }, + "title": "丟棄畫面數:" + }, + "decodedFrames": "解碼畫面數:", + "droppedFrameRate": "畫面丟棄率:", + "streamType": { + "title": "串流類型:", + "short": "類型" + } + }, + "toast": { + "success": { + "submittedFrigatePlus": "成功提交畫面至 Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "提交畫面至 Frigate+ 失敗" + } + }, + "noPreviewFoundFor": "找不到 {{cameraName}} 的預覽", + "livePlayerRequiredIOSVersion": "此串流類型需要 iOS 17.1 或以上版本。" +} diff --git a/web/public/locales/yue-Hant/config/cameras.json b/web/public/locales/yue-Hant/config/cameras.json new file mode 100644 index 0000000..ea83d05 --- /dev/null +++ b/web/public/locales/yue-Hant/config/cameras.json @@ -0,0 +1,605 @@ +{ + "zones": { + "label": "區域" + }, + "label": "鏡頭設定", + "name": { + "label": "鏡頭名稱", + "description": "必須填寫鏡頭名稱" + }, + "friendly_name": { + "label": "顯示名稱", + "description": "在 Frigate 介面顯示的鏡頭名稱" + }, + "enabled": { + "label": "已啟用", + "description": "已啟用" + }, + "audio": { + "label": "聲音事件", + "description": "此鏡頭用於聲音事件偵測的設定。", + "enabled": { + "label": "啟用聲音偵測", + "description": "啟用或停用此鏡頭的聲音事件偵測。" + }, + "max_not_heard": { + "label": "結束逾時", + "description": "當指定聲音類型消失多少秒後,聲音事件會結束。" + }, + "min_volume": { + "label": "最低音量", + "description": "執行聲音偵測所需的最低 RMS 音量閾值;數值越低靈敏度越高(例如:200 高、500 中、1000 低)。" + }, + "listen": { + "label": "監聽聲音類型", + "description": "要偵測的聲音事件類型清單(例如:狗吠、火警、尖叫、說話、大叫)。" + }, + "filters": { + "label": "聲音過濾器", + "description": "針對每種聲音類型的過濾設定,例如信心值門檻,用來減少誤判。" + }, + "enabled_in_config": { + "label": "原始聲音偵測狀態", + "description": "表示在原始靜態設定檔中是否已啟用聲音偵測。" + }, + "num_threads": { + "label": "偵測執行緒數量", + "description": "用於聲音偵測處理的執行緒數量。" + } + }, + "audio_transcription": { + "label": "聲音轉錄", + "description": "用於事件及即時字幕的語音與即時聲音轉錄設定。", + "enabled": { + "label": "啟用語音轉錄", + "description": "啟用或停用手動觸發的聲音事件轉錄。" + }, + "enabled_in_config": { + "label": "原始轉錄狀態" + }, + "live_enabled": { + "label": "即時轉錄", + "description": "在接收聲音時啟用串流即時轉錄。" + } + }, + "birdseye": { + "label": "Birdseye", + "description": "Birdseye 合成畫面的設定,可將多個鏡頭畫面合併成單一佈局。", + "enabled": { + "label": "啟用 Birdseye", + "description": "啟用或停用 Birdseye 功能。" + }, + "mode": { + "label": "追蹤模式", + "description": "鏡頭在 Birdseye 中的顯示模式:objects(物件)、motion(動作)、continuous(持續)。" + }, + "order": { + "label": "位置", + "description": "控制鏡頭在 Birdseye 佈局中排序的數值位置。" + } + }, + "detect": { + "label": "物件偵測", + "description": "用於執行物件偵測及初始化追蹤器的 detect 角色設定。", + "enabled": { + "label": "啟用偵測", + "description": "啟用或停用此鏡頭的物件偵測。必須啟用偵測才能進行物件追蹤。" + }, + "height": { + "label": "偵測高度", + "description": "偵測串流所使用影像幀的高度(像素);留空會使用原始解析度。" + }, + "width": { + "label": "偵測闊度", + "description": "偵測串流所使用影像幀的闊度(像素);留空會使用原始解析度。" + }, + "fps": { + "label": "偵測 FPS", + "description": "每秒執行偵測的幀數;數值越低 CPU 使用量越少(建議值為 5,只有在追蹤非常快速移動物件時才提高,最多 10)。" + }, + "min_initialized": { + "label": "最少初始化幀數", + "description": "建立追蹤物件前所需的連續偵測幀數。提高數值可減少誤初始化。預設值為 fps 的一半。" + }, + "max_disappeared": { + "label": "最大消失幀數", + "description": "當連續多少幀沒有偵測到物件後,該追蹤物件會被視為消失。" + }, + "stationary": { + "label": "靜止物件設定", + "description": "用於偵測及管理長時間保持靜止的物件。", + "interval": { + "label": "靜止檢查間隔", + "description": "每隔多少幀執行一次偵測檢查以確認物件是否靜止。" + }, + "threshold": { + "label": "靜止判定幀數", + "description": "當物件位置在多少幀內沒有變化時,會被標記為靜止。" + }, + "max_frames": { + "label": "最大幀數", + "description": "限制靜止物件被追蹤的最長幀數,之後會被移除。", + "default": { + "label": "預設最大幀數", + "description": "預設追蹤靜止物件的最大幀數。" + }, + "objects": { + "label": "物件最大幀數", + "description": "針對不同物件設定追蹤靜止物件的最大幀數。" + } + }, + "classifier": { + "label": "啟用視覺分類器", + "description": "使用視覺分類器在邊界框抖動時仍能偵測真正靜止的物件。" + } + }, + "annotation_offset": { + "label": "標註時間偏移", + "description": "用於調整偵測標註的毫秒偏移,使時間線上的框與錄影更準確對齊,可為正或負值。" + } + }, + "face_recognition": { + "label": "人臉識別", + "description": "此鏡頭的人臉偵測與識別設定。", + "enabled": { + "label": "啟用人臉識別", + "description": "啟用或停用人臉識別。" + }, + "min_area": { + "label": "最小人臉面積", + "description": "嘗試識別前所需的人臉偵測框最小面積(像素)。" + } + }, + "ffmpeg": { + "label": "FFmpeg 設定", + "description": "FFmpeg 設定,包括程式路徑、參數、硬件加速選項及不同角色的輸出參數。", + "path": { + "label": "FFmpeg 路徑", + "description": "FFmpeg 可執行檔路徑或版本別名(例如 \"5.0\" 或 \"7.0\")。" + }, + "global_args": { + "label": "FFmpeg 全域參數", + "description": "傳遞給 FFmpeg 程序的全域參數。" + }, + "hwaccel_args": { + "label": "硬件加速參數", + "description": "FFmpeg 硬件加速參數,建議使用對應供應商的預設設定。" + }, + "input_args": { + "label": "輸入參數", + "description": "套用於 FFmpeg 輸入串流的參數。" + }, + "output_args": { + "label": "輸出參數", + "description": "不同 FFmpeg 角色(例如 detect、record)使用的預設輸出參數。", + "detect": { + "label": "偵測輸出參數", + "description": "detect 角色串流的預設輸出參數。" + }, + "record": { + "label": "錄影輸出參數", + "description": "record 角色串流的預設輸出參數。" + } + }, + "retry_interval": { + "label": "FFmpeg 重試時間", + "description": "當鏡頭串流失敗後,等待多少秒再嘗試重新連線。預設為 10。" + }, + "apple_compatibility": { + "label": "Apple 相容模式", + "description": "在錄製 H.265 時啟用 HEVC 標記,以改善 Apple 播放器相容性。" + }, + "gpu": { + "label": "GPU 編號", + "description": "若可用,硬件加速所使用的預設 GPU 編號。" + }, + "inputs": { + "label": "鏡頭輸入", + "description": "此鏡頭的輸入串流定義清單(路徑及角色)。", + "path": { + "label": "輸入路徑", + "description": "鏡頭輸入串流 URL 或路徑。" + }, + "roles": { + "label": "輸入角色", + "description": "此輸入串流的角色。" + }, + "global_args": { + "label": "FFmpeg 全域參數", + "description": "此輸入串流的 FFmpeg 全域參數。" + }, + "hwaccel_args": { + "label": "硬件加速參數", + "description": "此輸入串流的硬件加速參數。" + }, + "input_args": { + "label": "輸入參數", + "description": "此串流專用的輸入參數。" + } + } + }, + "live": { + "label": "即時播放", + "description": "Web UI 用來控制即時串流選擇、解析度及品質的設定。", + "streams": { + "label": "即時串流名稱", + "description": "將設定的串流名稱對應到 restream / go2rtc 名稱以供即時播放。" + }, + "height": { + "label": "即時畫面高度", + "description": "在 Web UI 顯示 jsmpeg 即時串流的高度(像素);必須小於或等於偵測串流高度。" + }, + "quality": { + "label": "即時畫面品質", + "description": "jsmpeg 串流的編碼品質(1 最高,31 最低)。" + } + }, + "lpr": { + "label": "車牌識別", + "description": "車牌識別設定,包括偵測閾值、格式化及已知車牌。", + "enabled": { + "label": "啟用車牌識別", + "description": "啟用或停用此鏡頭的車牌識別。" + }, + "expire_time": { + "label": "過期秒數", + "description": "當車牌在指定秒數內沒有再次出現時會從追蹤器中過期(只適用於專用 LPR 鏡頭)。" + }, + "min_area": { + "label": "最小車牌面積", + "description": "嘗試識別所需的最小車牌面積(像素)。" + }, + "enhancement": { + "label": "增強等級", + "description": "在 OCR 前對車牌裁剪圖像套用的增強等級(0-10);數值越高不一定效果更好,5 以上通常只對夜間車牌有效,需小心使用。" + } + }, + "motion": { + "label": "移動偵測", + "description": "此鏡頭的預設移動偵測設定。", + "enabled": { + "label": "啟用移動偵測", + "description": "啟用或停用此鏡頭的移動偵測。" + }, + "threshold": { + "label": "移動閾值", + "description": "移動偵測使用的像素差異閾值;數值越高靈敏度越低(範圍 1-255)。" + }, + "lightning_threshold": { + "label": "閃光閾值", + "description": "用於偵測並忽略短暫光線變化的閾值(數值越低越敏感,範圍 0.3 至 1.0)。這不會完全阻止移動偵測;當超過閾值時偵測器只會停止分析額外影像幀。" + }, + "skip_motion_threshold": { + "label": "跳過移動閾值", + "description": "如果單一影像幀有超過此比例的畫面改變,偵測器會不回傳移動框並立即重新校準。這可在閃電、暴風雨等情況節省 CPU 並減少誤判,但可能會錯過真正事件,例如 PTZ 鏡頭自動追蹤物件。這是在丟失少量錄影資料與需要檢查一些短片之間的取捨。" + }, + "improve_contrast": { + "label": "改善對比", + "description": "在進行移動分析前改善影像對比以協助偵測。" + }, + "contour_area": { + "label": "輪廓面積", + "description": "移動輪廓被計算所需的最小像素面積。" + }, + "delta_alpha": { + "label": "Delta alpha 值", + "description": "用於影像差分計算移動時的 alpha 混合係數。" + }, + "frame_alpha": { + "label": "影像幀 alpha 值", + "description": "用於移動預處理時影像幀混合的 alpha 值。" + }, + "frame_height": { + "label": "影像幀高度", + "description": "在計算移動時縮放影像幀的高度(像素)。" + }, + "mask": { + "label": "遮罩座標", + "description": "定義移動遮罩多邊形的 x,y 座標順序,用於包含或排除特定區域。" + }, + "mqtt_off_delay": { + "label": "MQTT 關閉延遲", + "description": "最後一次移動後等待多少秒才發佈 MQTT 的「off」狀態。" + }, + "enabled_in_config": { + "label": "原始移動偵測狀態", + "description": "表示在原始靜態設定中是否已啟用移動偵測。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "objects": { + "label": "物件", + "description": "物件追蹤的預設設定,包括要追蹤的標籤及每個物件的過濾器。", + "track": { + "label": "要追蹤的物件", + "description": "此鏡頭要追蹤的物件標籤清單。" + }, + "filters": { + "label": "物件過濾器", + "description": "套用於偵測物件的過濾器以減少誤判(面積、比例、信心值)。", + "min_area": { + "label": "最小物件面積", + "description": "此物件類型所需的最小偵測框面積(像素或百分比)。可以是像素(整數)或百分比(介於 0.000001 和 0.99 之間的浮點數)。" + }, + "max_area": { + "label": "最大物件面積", + "description": "此物件類型允許的最大偵測框面積(像素或百分比)。可以是像素(整數)或百分比(介於 0.000001 和 0.99 之間的浮點數)。" + }, + "min_ratio": { + "label": "最小長寬比", + "description": "偵測框符合條件所需的最小寬高比。" + }, + "max_ratio": { + "label": "最大長寬比", + "description": "偵測框允許的最大寬高比。" + }, + "threshold": { + "label": "信心值閾值", + "description": "物件被視為真正偵測結果所需的平均信心值閾值。" + }, + "min_score": { + "label": "最低信心值", + "description": "物件被計算所需的單一影像幀最低信心值。" + }, + "mask": { + "label": "過濾遮罩", + "description": "定義此過濾器在畫面中生效位置的多邊形座標。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "mask": { + "label": "物件遮罩", + "description": "用於防止在指定區域進行物件偵測的遮罩多邊形。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "genai": { + "label": "GenAI 物件設定", + "description": "用於描述被追蹤物件並傳送影像幀作生成用途的 GenAI 設定。", + "enabled": { + "label": "啟用 GenAI", + "description": "預設為被追蹤物件啟用 GenAI 描述生成功能。" + }, + "use_snapshot": { + "label": "使用快照", + "description": "使用物件快照而不是縮圖來生成 GenAI 描述。" + }, + "prompt": { + "label": "描述提示詞", + "description": "使用 GenAI 生成描述時使用的預設提示模板。" + }, + "object_prompts": { + "label": "物件提示詞", + "description": "為特定物件標籤自訂 GenAI 輸出的提示詞。" + }, + "objects": { + "label": "GenAI 物件", + "description": "預設會傳送到 GenAI 的物件標籤清單。" + }, + "required_zones": { + "label": "必要區域", + "description": "物件必須進入的區域,才會生成 GenAI 描述。" + }, + "debug_save_thumbnails": { + "label": "儲存縮圖", + "description": "儲存傳送到 GenAI 的縮圖以供除錯及檢視。" + }, + "send_triggers": { + "label": "GenAI 觸發條件", + "description": "定義何時將影像幀傳送到 GenAI(例如結束時、更新後等)。", + "tracked_object_end": { + "label": "結束時傳送", + "description": "當追蹤物件結束時向 GenAI 發送請求。" + }, + "after_significant_updates": { + "label": "提前 GenAI 觸發", + "description": "當追蹤物件出現指定數量的重要更新後向 GenAI 發送請求。" + } + }, + "enabled_in_config": { + "label": "原始 GenAI 狀態", + "description": "表示在原始靜態設定中是否啟用了 GenAI。" + } + } + }, + "record": { + "label": "錄影", + "description": "此鏡頭的錄影及保存設定。", + "enabled": { + "label": "啟用錄影", + "description": "啟用或停用此鏡頭錄影。" + }, + "expire_interval": { + "label": "錄影清理間隔", + "description": "清理過期錄影片段的間隔時間(分鐘)。" + }, + "continuous": { + "label": "持續錄影保存", + "description": "無論是否有物件或移動都保留錄影的天數。如果只想保留警報和偵測記錄,請設定為 0。", + "days": { + "label": "保存日數", + "description": "錄影保存日數。" + } + }, + "motion": { + "label": "移動錄影保存", + "description": "由動作觸發的錄影保存日數(不論是否有追蹤物件)。如果只想保留警報和偵測記錄,請設定為 0。", + "days": { + "label": "保存日數", + "description": "錄影保存日數。" + } + }, + "detections": { + "label": "偵測錄影保存", + "description": "偵測事件錄影的保存設定,包括事件前後錄影時間。", + "pre_capture": { + "label": "事件前錄影秒數", + "description": "在偵測事件前包含於錄影中的秒數。" + }, + "post_capture": { + "label": "事件後錄影秒數", + "description": "在偵測事件後包含於錄影中的秒數。" + }, + "retain": { + "label": "事件保存", + "description": "偵測事件錄影的保存設定。", + "days": { + "label": "保存日數", + "description": "偵測事件錄影保存日數。" + }, + "mode": { + "label": "保存模式", + "description": "保存模式:all(保存所有片段)、motion(保存有移動的片段)、active_objects(保存有移動物件的片段)。" + } + } + }, + "alerts": { + "label": "警報錄影保存", + "description": "警報事件錄影保存設定,包括事件前後錄影時間。", + "pre_capture": { + "label": "事件前錄影秒數", + "description": "在偵測事件前包含於錄影中的秒數。" + }, + "post_capture": { + "label": "事件後錄影秒數", + "description": "在偵測事件後包含於錄影中的秒數。" + }, + "retain": { + "label": "事件保存", + "description": "偵測事件錄影保存設定。", + "days": { + "label": "保存日數", + "description": "偵測事件錄影保存日數。" + }, + "mode": { + "label": "保存模式", + "description": "保存模式:all(保存所有片段)、motion(保存有移動的片段)、active_objects(保存有移動物件的片段)。" + } + } + }, + "export": { + "label": "匯出設定", + "description": "匯出錄影(例如縮時影片)時使用的設定,包括硬件加速。", + "hwaccel_args": { + "label": "匯出硬件加速參數", + "description": "用於匯出或轉碼操作的硬件加速參數。" + } + }, + "preview": { + "label": "預覽設定", + "description": "控制 UI 中錄影預覽品質的設定。", + "quality": { + "label": "預覽品質", + "description": "預覽品質等級(very_low、low、medium、high、very_high)。" + } + }, + "enabled_in_config": { + "label": "原始錄影狀態", + "description": "表示在原始靜態設定中是否已啟用錄影。" + } + }, + "review": { + "label": "審查", + "description": "控制警報、偵測及 GenAI 檢視摘要的設定,供 UI 與儲存使用。", + "alerts": { + "label": "警報設定", + "description": "哪些追蹤物件會產生警報以及警報保存方式的設定。", + "enabled": { + "label": "啟用警報", + "description": "啟用或停用此鏡頭的警報產生。" + }, + "labels": { + "label": "警報標籤", + "description": "符合警報條件的物件標籤清單(例如 car、person)。" + }, + "required_zones": { + "label": "必要區域", + "description": "物件必須進入的區域才會被視為警報;留空表示任何區域都可以。" + }, + "enabled_in_config": { + "label": "原始警報狀態", + "description": "追蹤原始靜態設定中是否啟用了警報。" + }, + "cutoff_time": { + "label": "警報截止時間", + "description": "在沒有觸發警報活動後等待多少秒才結束警報。" + } + }, + "detections": { + "label": "偵測設定", + "description": "建立偵測事件(非警報)及其保存時間的設定。", + "enabled": { + "label": "啟用偵測事件", + "description": "啟用或停用此鏡頭的偵測事件。" + }, + "labels": { + "label": "偵測標籤", + "description": "符合偵測事件條件的物件標籤清單。" + }, + "required_zones": { + "label": "必要區域", + "description": "物件必須進入的區域才會被視為偵測事件;留空表示任何區域。" + }, + "cutoff_time": { + "label": "偵測截止時間", + "description": "在沒有觸發偵測活動後等待多少秒才結束偵測事件。" + }, + "enabled_in_config": { + "label": "原始偵測狀態", + "description": "追蹤原始靜態設定中是否啟用了偵測事件。" + } + }, + "genai": { + "label": "GenAI 設定", + "enabled_in_config": { + "label": "原始 GenAI 狀態" + }, + "preferred_language": { + "label": "偏好語言", + "description": "向 GenAI 服務請求生成回應時使用的偏好語言。" + }, + "activity_context_prompt": { + "label": "活動情境提示", + "description": "用於描述哪些行為屬於或不屬於可疑活動的自訂提示詞,以提供 GenAI 摘要情境。" + } + } + }, + "semantic_search": { + "label": "語意搜尋", + "description": "語意搜尋設定,用於建立及查詢物件嵌入向量以找出相似項目。", + "triggers": { + "label": "觸發器", + "friendly_name": { + "label": "顯示名稱" + } + } + }, + "snapshots": { + "label": "快照", + "description": "此鏡頭保存追蹤物件 JPEG 快照的設定。", + "enabled": { + "label": "啟用快照", + "description": "啟用或停用此鏡頭保存快照。" + }, + "clean_copy": { + "label": "保存原始副本", + "description": "啟用或停用此鏡頭保存快照。" + }, + "timestamp": { + "label": "時間戳疊加", + "description": "在保存的快照上顯示時間戳。" + }, + "bounding_box": { + "label": "偵測框疊加", + "description": "在保存的快照上顯示追蹤物件的邊界框。" + }, + "crop": { + "label": "裁剪快照" + } + } +} diff --git a/web/public/locales/yue-Hant/config/global.json b/web/public/locales/yue-Hant/config/global.json new file mode 100644 index 0000000..ce6b726 --- /dev/null +++ b/web/public/locales/yue-Hant/config/global.json @@ -0,0 +1,564 @@ +{ + "audio": { + "label": "聲音事件", + "enabled": { + "label": "啟用聲音偵測" + }, + "max_not_heard": { + "label": "結束逾時", + "description": "當指定聲音類型消失多少秒後,聲音事件會結束。" + }, + "min_volume": { + "label": "最低音量", + "description": "執行聲音偵測所需的最低 RMS 音量閾值;數值越低靈敏度越高(例如:200 高、500 中、1000 低)。" + }, + "listen": { + "label": "監聽聲音類型", + "description": "要偵測的聲音事件類型清單(例如:狗吠、火警、尖叫、說話、大叫)。" + }, + "filters": { + "label": "聲音過濾器", + "description": "針對每種聲音類型的過濾設定,例如信心值門檻,用來減少誤判。" + }, + "enabled_in_config": { + "label": "原始聲音偵測狀態", + "description": "表示在原始靜態設定檔中是否已啟用聲音偵測。" + }, + "num_threads": { + "label": "偵測執行緒數量", + "description": "用於聲音偵測處理的執行緒數量。" + } + }, + "audio_transcription": { + "label": "聲音轉錄", + "description": "用於事件及即時字幕的語音與即時聲音轉錄設定。", + "live_enabled": { + "label": "即時轉錄", + "description": "在接收聲音時啟用串流即時轉錄。" + } + }, + "birdseye": { + "label": "Birdseye", + "description": "Birdseye 合成畫面的設定,可將多個鏡頭畫面合併成單一佈局。", + "enabled": { + "label": "啟用 Birdseye", + "description": "啟用或停用 Birdseye 功能。" + }, + "mode": { + "label": "追蹤模式", + "description": "鏡頭在 Birdseye 中的顯示模式:objects(物件)、motion(動作)、continuous(持續)。" + }, + "order": { + "label": "位置", + "description": "控制鏡頭在 Birdseye 佈局中排序的數值位置。" + } + }, + "detect": { + "label": "物件偵測", + "description": "用於執行物件偵測及初始化追蹤器的 detect 角色設定。", + "enabled": { + "label": "啟用偵測" + }, + "height": { + "label": "偵測高度", + "description": "偵測串流所使用影像幀的高度(像素);留空會使用原始解析度。" + }, + "width": { + "label": "偵測闊度", + "description": "偵測串流所使用影像幀的闊度(像素);留空會使用原始解析度。" + }, + "fps": { + "label": "偵測 FPS", + "description": "每秒執行偵測的幀數;數值越低 CPU 使用量越少(建議值為 5,只有在追蹤非常快速移動物件時才提高,最多 10)。" + }, + "min_initialized": { + "label": "最少初始化幀數", + "description": "建立追蹤物件前所需的連續偵測幀數。提高數值可減少誤初始化。預設值為 fps 的一半。" + }, + "max_disappeared": { + "label": "最大消失幀數", + "description": "當連續多少幀沒有偵測到物件後,該追蹤物件會被視為消失。" + }, + "stationary": { + "label": "靜止物件設定", + "description": "用於偵測及管理長時間保持靜止的物件。", + "interval": { + "label": "靜止檢查間隔", + "description": "每隔多少幀執行一次偵測檢查以確認物件是否靜止。" + }, + "threshold": { + "label": "靜止判定幀數", + "description": "當物件位置在多少幀內沒有變化時,會被標記為靜止。" + }, + "max_frames": { + "label": "最大幀數", + "description": "限制靜止物件被追蹤的最長幀數,之後會被移除。", + "default": { + "label": "預設最大幀數", + "description": "預設追蹤靜止物件的最大幀數。" + }, + "objects": { + "label": "物件最大幀數", + "description": "針對不同物件設定追蹤靜止物件的最大幀數。" + } + }, + "classifier": { + "label": "啟用視覺分類器", + "description": "使用視覺分類器在邊界框抖動時仍能偵測真正靜止的物件。" + } + }, + "annotation_offset": { + "label": "標註時間偏移", + "description": "用於調整偵測標註的毫秒偏移,使時間線上的框與錄影更準確對齊,可為正或負值。" + } + }, + "face_recognition": { + "label": "人臉識別", + "enabled": { + "label": "啟用人臉識別" + }, + "min_area": { + "label": "最小人臉面積", + "description": "嘗試識別前所需的人臉偵測框最小面積(像素)。" + } + }, + "ffmpeg": { + "label": "FFmpeg 設定", + "description": "FFmpeg 設定,包括程式路徑、參數、硬件加速選項及不同角色的輸出參數。", + "path": { + "label": "FFmpeg 路徑", + "description": "FFmpeg 可執行檔路徑或版本別名(例如 \"5.0\" 或 \"7.0\")。" + }, + "global_args": { + "label": "FFmpeg 全域參數", + "description": "傳遞給 FFmpeg 程序的全域參數。" + }, + "hwaccel_args": { + "label": "硬件加速參數", + "description": "FFmpeg 硬件加速參數,建議使用對應供應商的預設設定。" + }, + "input_args": { + "label": "輸入參數", + "description": "套用於 FFmpeg 輸入串流的參數。" + }, + "output_args": { + "label": "輸出參數", + "description": "不同 FFmpeg 角色(例如 detect、record)使用的預設輸出參數。", + "detect": { + "label": "偵測輸出參數", + "description": "detect 角色串流的預設輸出參數。" + }, + "record": { + "label": "錄影輸出參數", + "description": "record 角色串流的預設輸出參數。" + } + }, + "retry_interval": { + "label": "FFmpeg 重試時間", + "description": "當鏡頭串流失敗後,等待多少秒再嘗試重新連線。預設為 10。" + }, + "apple_compatibility": { + "label": "Apple 相容模式", + "description": "在錄製 H.265 時啟用 HEVC 標記,以改善 Apple 播放器相容性。" + }, + "gpu": { + "label": "GPU 編號", + "description": "若可用,硬件加速所使用的預設 GPU 編號。" + }, + "inputs": { + "label": "鏡頭輸入", + "description": "此鏡頭的輸入串流定義清單(路徑及角色)。", + "path": { + "label": "輸入路徑", + "description": "鏡頭輸入串流 URL 或路徑。" + }, + "roles": { + "label": "輸入角色", + "description": "此輸入串流的角色。" + }, + "global_args": { + "label": "FFmpeg 全域參數", + "description": "此輸入串流的 FFmpeg 全域參數。" + }, + "hwaccel_args": { + "label": "硬件加速參數", + "description": "此輸入串流的硬件加速參數。" + }, + "input_args": { + "label": "輸入參數", + "description": "此串流專用的輸入參數。" + } + } + }, + "live": { + "label": "即時播放", + "streams": { + "label": "即時串流名稱", + "description": "將設定的串流名稱對應到 restream / go2rtc 名稱以供即時播放。" + }, + "height": { + "label": "即時畫面高度", + "description": "在 Web UI 顯示 jsmpeg 即時串流的高度(像素);必須小於或等於偵測串流高度。" + }, + "quality": { + "label": "即時畫面品質", + "description": "jsmpeg 串流的編碼品質(1 最高,31 最低)。" + } + }, + "lpr": { + "label": "車牌識別", + "description": "車牌識別設定,包括偵測閾值、格式化及已知車牌。", + "enabled": { + "label": "啟用車牌識別" + }, + "expire_time": { + "label": "過期秒數", + "description": "當車牌在指定秒數內沒有再次出現時會從追蹤器中過期(只適用於專用 LPR 鏡頭)。" + }, + "min_area": { + "label": "最小車牌面積", + "description": "嘗試識別所需的最小車牌面積(像素)。" + }, + "enhancement": { + "label": "增強等級", + "description": "在 OCR 前對車牌裁剪圖像套用的增強等級(0-10);數值越高不一定效果更好,5 以上通常只對夜間車牌有效,需小心使用。" + } + }, + "motion": { + "label": "移動偵測", + "enabled": { + "label": "啟用移動偵測" + }, + "threshold": { + "label": "移動閾值", + "description": "移動偵測使用的像素差異閾值;數值越高靈敏度越低(範圍 1-255)。" + }, + "lightning_threshold": { + "label": "閃光閾值", + "description": "用於偵測並忽略短暫光線變化的閾值(數值越低越敏感,範圍 0.3 至 1.0)。這不會完全阻止移動偵測;當超過閾值時偵測器只會停止分析額外影像幀。" + }, + "skip_motion_threshold": { + "label": "跳過移動閾值", + "description": "如果單一影像幀有超過此比例的畫面改變,偵測器會不回傳移動框並立即重新校準。這可在閃電、暴風雨等情況節省 CPU 並減少誤判,但可能會錯過真正事件,例如 PTZ 鏡頭自動追蹤物件。這是在丟失少量錄影資料與需要檢查一些短片之間的取捨。" + }, + "improve_contrast": { + "label": "改善對比", + "description": "在進行移動分析前改善影像對比以協助偵測。" + }, + "contour_area": { + "label": "輪廓面積", + "description": "移動輪廓被計算所需的最小像素面積。" + }, + "delta_alpha": { + "label": "Delta alpha 值", + "description": "用於影像差分計算移動時的 alpha 混合係數。" + }, + "frame_alpha": { + "label": "影像幀 alpha 值", + "description": "用於移動預處理時影像幀混合的 alpha 值。" + }, + "frame_height": { + "label": "影像幀高度", + "description": "在計算移動時縮放影像幀的高度(像素)。" + }, + "mask": { + "label": "遮罩座標", + "description": "定義移動遮罩多邊形的 x,y 座標順序,用於包含或排除特定區域。" + }, + "mqtt_off_delay": { + "label": "MQTT 關閉延遲", + "description": "最後一次移動後等待多少秒才發佈 MQTT 的「off」狀態。" + }, + "enabled_in_config": { + "label": "原始移動偵測狀態", + "description": "表示在原始靜態設定中是否已啟用移動偵測。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "objects": { + "label": "物件", + "description": "物件追蹤的預設設定,包括要追蹤的標籤及每個物件的過濾器。", + "track": { + "label": "要追蹤的物件" + }, + "filters": { + "label": "物件過濾器", + "description": "套用於偵測物件的過濾器以減少誤判(面積、比例、信心值)。", + "min_area": { + "label": "最小物件面積", + "description": "此物件類型所需的最小偵測框面積(像素或百分比)。可以是像素(整數)或百分比(介於 0.000001 和 0.99 之間的浮點數)。" + }, + "max_area": { + "label": "最大物件面積", + "description": "此物件類型允許的最大偵測框面積(像素或百分比)。可以是像素(整數)或百分比(介於 0.000001 和 0.99 之間的浮點數)。" + }, + "min_ratio": { + "label": "最小長寬比", + "description": "偵測框符合條件所需的最小寬高比。" + }, + "max_ratio": { + "label": "最大長寬比", + "description": "偵測框允許的最大寬高比。" + }, + "threshold": { + "label": "信心值閾值", + "description": "物件被視為真正偵測結果所需的平均信心值閾值。" + }, + "min_score": { + "label": "最低信心值", + "description": "物件被計算所需的單一影像幀最低信心值。" + }, + "mask": { + "label": "過濾遮罩", + "description": "定義此過濾器在畫面中生效位置的多邊形座標。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "mask": { + "label": "物件遮罩", + "description": "用於防止在指定區域進行物件偵測的遮罩多邊形。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "genai": { + "label": "GenAI 物件設定", + "description": "用於描述被追蹤物件並傳送影像幀作生成用途的 GenAI 設定。", + "enabled": { + "label": "啟用 GenAI", + "description": "預設為被追蹤物件啟用 GenAI 描述生成功能。" + }, + "use_snapshot": { + "label": "使用快照", + "description": "使用物件快照而不是縮圖來生成 GenAI 描述。" + }, + "prompt": { + "label": "描述提示詞", + "description": "使用 GenAI 生成描述時使用的預設提示模板。" + }, + "object_prompts": { + "label": "物件提示詞", + "description": "為特定物件標籤自訂 GenAI 輸出的提示詞。" + }, + "objects": { + "label": "GenAI 物件", + "description": "預設會傳送到 GenAI 的物件標籤清單。" + }, + "required_zones": { + "label": "必要區域", + "description": "物件必須進入的區域,才會生成 GenAI 描述。" + }, + "debug_save_thumbnails": { + "label": "儲存縮圖", + "description": "儲存傳送到 GenAI 的縮圖以供除錯及檢視。" + }, + "send_triggers": { + "label": "GenAI 觸發條件", + "description": "定義何時將影像幀傳送到 GenAI(例如結束時、更新後等)。", + "tracked_object_end": { + "label": "結束時傳送", + "description": "當追蹤物件結束時向 GenAI 發送請求。" + }, + "after_significant_updates": { + "label": "提前 GenAI 觸發", + "description": "當追蹤物件出現指定數量的重要更新後向 GenAI 發送請求。" + } + }, + "enabled_in_config": { + "label": "原始 GenAI 狀態", + "description": "表示在原始靜態設定中是否啟用了 GenAI。" + } + } + }, + "record": { + "label": "錄影", + "enabled": { + "label": "啟用錄影" + }, + "expire_interval": { + "label": "錄影清理間隔", + "description": "清理過期錄影片段的間隔時間(分鐘)。" + }, + "continuous": { + "label": "持續錄影保存", + "description": "無論是否有物件或移動都保留錄影的天數。如果只想保留警報和偵測記錄,請設定為 0。", + "days": { + "label": "保存日數", + "description": "錄影保存日數。" + } + }, + "motion": { + "label": "移動錄影保存", + "description": "由動作觸發的錄影保存日數(不論是否有追蹤物件)。如果只想保留警報和偵測記錄,請設定為 0。", + "days": { + "label": "保存日數", + "description": "錄影保存日數。" + } + }, + "detections": { + "label": "偵測錄影保存", + "description": "偵測事件錄影的保存設定,包括事件前後錄影時間。", + "pre_capture": { + "label": "事件前錄影秒數", + "description": "在偵測事件前包含於錄影中的秒數。" + }, + "post_capture": { + "label": "事件後錄影秒數", + "description": "在偵測事件後包含於錄影中的秒數。" + }, + "retain": { + "label": "事件保存", + "description": "偵測事件錄影的保存設定。", + "days": { + "label": "保存日數", + "description": "偵測事件錄影保存日數。" + }, + "mode": { + "label": "保存模式", + "description": "保存模式:all(保存所有片段)、motion(保存有移動的片段)、active_objects(保存有移動物件的片段)。" + } + } + }, + "alerts": { + "label": "警報錄影保存", + "description": "警報事件錄影保存設定,包括事件前後錄影時間。", + "pre_capture": { + "label": "事件前錄影秒數", + "description": "在偵測事件前包含於錄影中的秒數。" + }, + "post_capture": { + "label": "事件後錄影秒數", + "description": "在偵測事件後包含於錄影中的秒數。" + }, + "retain": { + "label": "事件保存", + "description": "偵測事件錄影保存設定。", + "days": { + "label": "保存日數", + "description": "偵測事件錄影保存日數。" + }, + "mode": { + "label": "保存模式", + "description": "保存模式:all(保存所有片段)、motion(保存有移動的片段)、active_objects(保存有移動物件的片段)。" + } + } + }, + "export": { + "label": "匯出設定", + "description": "匯出錄影(例如縮時影片)時使用的設定,包括硬件加速。", + "hwaccel_args": { + "label": "匯出硬件加速參數", + "description": "用於匯出或轉碼操作的硬件加速參數。" + } + }, + "preview": { + "label": "預覽設定", + "description": "控制 UI 中錄影預覽品質的設定。", + "quality": { + "label": "預覽品質", + "description": "預覽品質等級(very_low、low、medium、high、very_high)。" + } + }, + "enabled_in_config": { + "label": "原始錄影狀態", + "description": "表示在原始靜態設定中是否已啟用錄影。" + } + }, + "review": { + "label": "審查", + "alerts": { + "label": "警報設定", + "description": "哪些追蹤物件會產生警報以及警報保存方式的設定。", + "enabled": { + "label": "啟用警報" + }, + "labels": { + "label": "警報標籤", + "description": "符合警報條件的物件標籤清單(例如 car、person)。" + }, + "required_zones": { + "label": "必要區域", + "description": "物件必須進入的區域才會被視為警報;留空表示任何區域都可以。" + }, + "enabled_in_config": { + "label": "原始警報狀態", + "description": "追蹤原始靜態設定中是否啟用了警報。" + }, + "cutoff_time": { + "label": "警報截止時間", + "description": "在沒有觸發警報活動後等待多少秒才結束警報。" + } + }, + "detections": { + "label": "偵測設定", + "description": "建立偵測事件(非警報)及其保存時間的設定。", + "enabled": { + "label": "啟用偵測事件" + }, + "labels": { + "label": "偵測標籤", + "description": "符合偵測事件條件的物件標籤清單。" + }, + "required_zones": { + "label": "必要區域", + "description": "物件必須進入的區域才會被視為偵測事件;留空表示任何區域。" + }, + "cutoff_time": { + "label": "偵測截止時間", + "description": "在沒有觸發偵測活動後等待多少秒才結束偵測事件。" + }, + "enabled_in_config": { + "label": "原始偵測狀態", + "description": "追蹤原始靜態設定中是否啟用了偵測事件。" + } + }, + "genai": { + "label": "GenAI 設定", + "enabled_in_config": { + "label": "原始 GenAI 狀態" + }, + "preferred_language": { + "label": "偏好語言", + "description": "向 GenAI 服務請求生成回應時使用的偏好語言。" + }, + "activity_context_prompt": { + "label": "活動情境提示", + "description": "用於描述哪些行為屬於或不屬於可疑活動的自訂提示詞,以提供 GenAI 摘要情境。" + } + } + }, + "semantic_search": { + "label": "語意搜尋", + "triggers": { + "label": "觸發器", + "friendly_name": { + "label": "顯示名稱" + } + } + }, + "snapshots": { + "label": "快照", + "enabled": { + "label": "啟用快照" + }, + "clean_copy": { + "label": "保存原始副本", + "description": "啟用或停用此鏡頭保存快照。" + }, + "timestamp": { + "label": "時間戳疊加", + "description": "在保存的快照上顯示時間戳。" + }, + "bounding_box": { + "label": "偵測框疊加", + "description": "在保存的快照上顯示追蹤物件的邊界框。" + }, + "crop": { + "label": "裁剪快照" + } + } +} diff --git a/web/public/locales/yue-Hant/config/groups.json b/web/public/locales/yue-Hant/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/yue-Hant/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/yue-Hant/config/validation.json b/web/public/locales/yue-Hant/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/yue-Hant/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/yue-Hant/objects.json b/web/public/locales/yue-Hant/objects.json new file mode 100644 index 0000000..b0838d7 --- /dev/null +++ b/web/public/locales/yue-Hant/objects.json @@ -0,0 +1,120 @@ +{ + "vehicle": "車輛", + "car": "車", + "boat": "船", + "bus": "巴士", + "motorcycle": "電單車", + "train": "火車", + "bicycle": "單車", + "skateboard": "滑板", + "door": "門", + "blender": "攪拌機", + "sink": "洗手盆", + "scissors": "剪刀", + "clock": "時鐘", + "toothbrush": "牙刷", + "hair_dryer": "吹風機", + "person": "人", + "airplane": "飛機", + "traffic_light": "紅綠燈", + "fire_hydrant": "消防栓", + "street_sign": "街道標誌", + "stop_sign": "停車標誌", + "parking_meter": "咪錶", + "bench": "長凳", + "bird": "鳥", + "cat": "貓", + "sheep": "羊", + "cow": "牛", + "elephant": "大象", + "bear": "熊", + "zebra": "斑馬", + "giraffe": "長頸鹿", + "backpack": "背囊", + "tie": "領呔", + "suitcase": "行李箱", + "frisbee": "飛碟", + "skis": "滑雪板", + "snowboard": "單板滑雪板", + "sports_ball": "運動球", + "kite": "風箏", + "baseball_bat": "棒球棍", + "baseball_glove": "棒球手套", + "surfboard": "衝浪板", + "tennis_racket": "網球拍", + "bottle": "樽", + "plate": "碟", + "wine_glass": "酒杯", + "cup": "杯", + "fork": "叉", + "bowl": "碗", + "banana": "香蕉", + "apple": "蘋果", + "sandwich": "三文治", + "orange": "橙", + "carrot": "紅蘿蔔", + "hot_dog": "熱狗", + "pizza": "薄餅", + "donut": "甜甜圈", + "cake": "蛋糕", + "chair": "凳", + "couch": "梳化", + "laptop": "手提電腦", + "mouse": "滑鼠", + "remote": "遙控器", + "keyboard": "鍵盤", + "cell_phone": "手機", + "microwave": "微波爐", + "oven": "焗爐", + "toaster": "多士爐", + "refrigerator": "雪櫃", + "book": "書", + "vase": "花瓶", + "teddy_bear": "泰迪熊", + "hair_brush": "梳", + "squirrel": "松鼠", + "deer": "鹿", + "animal": "動物", + "bark": "樹皮", + "fox": "狐狸", + "goat": "山羊", + "rabbit": "兔", + "raccoon": "浣熊", + "robot_lawnmower": "自動剪草機", + "waste_bin": "垃圾桶", + "license_plate": "車牌", + "bbq_grill": "燒烤爐", + "amazon": "亞馬遜", + "usps": "美國郵政", + "ups": "UPS", + "postnl": "荷蘭郵政", + "nzpost": "新西蘭郵政", + "postnord": "北歐郵政", + "gls": "GLS", + "dpd": "DPD", + "broccoli": "西蘭花", + "umbrella": "雨傘", + "eye_glasses": "眼鏡", + "dog": "狗", + "desk": "書枱", + "tv": "電視", + "horse": "馬", + "mirror": "鏡", + "spoon": "匙羹", + "hat": "帽", + "shoe": "鞋", + "potted_plant": "盆栽植物", + "fedex": "聯邦快遞", + "handbag": "手袋", + "dining_table": "飯枱", + "an_post": "愛爾蘭郵政", + "knife": "刀", + "window": "窗", + "bed": "床", + "toilet": "廁所", + "purolator": "Purolator", + "on_demand": "按需要提供", + "face": "人臉", + "package": "包裹", + "dhl": "DHL" +} diff --git a/web/public/locales/yue-Hant/views/chat.json b/web/public/locales/yue-Hant/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/yue-Hant/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/yue-Hant/views/classificationModel.json b/web/public/locales/yue-Hant/views/classificationModel.json new file mode 100644 index 0000000..7bfda4a --- /dev/null +++ b/web/public/locales/yue-Hant/views/classificationModel.json @@ -0,0 +1,182 @@ +{ + "documentTitle": "分類模型 - Frigate", + "details": { + "unknown": "未知", + "scoreInfo": "分數代表此物件所有偵測結果的平均分類信心度。", + "none": "無" + }, + "train": { + "titleShort": "最近", + "title": "最近分類", + "aria": "選擇最近分類" + }, + "button": { + "deleteClassificationAttempts": "刪除分類影像", + "renameCategory": "重新命名類別", + "deleteCategory": "刪除類別", + "deleteImages": "刪除影像", + "trainModel": "訓練模型", + "addClassification": "新增分類", + "deleteModels": "刪除模型", + "editModel": "編輯模型" + }, + "tooltip": { + "trainingInProgress": "模型正在訓練中", + "noNewImages": "沒有新影像可訓練,請先分類更多資料集影像。", + "noChanges": "自上次訓練後資料集沒有變更。", + "modelNotReady": "模型尚未準備好訓練" + }, + "toast": { + "success": { + "deletedCategory_other": "已刪除類別", + "deletedImage_other": "已刪除影像", + "deletedModel_other": "已成功刪除 {{count}} 個模型", + "categorizedImage": "影像分類成功", + "trainedModel": "模型訓練成功。", + "trainingModel": "已成功開始模型訓練。", + "updatedModel": "已成功更新模型設定", + "renamedCategory": "已成功將類別重新命名為 {{name}}" + }, + "error": { + "deleteImageFailed": "刪除失敗:{{errorMessage}}", + "deleteCategoryFailed": "刪除類別失敗:{{errorMessage}}", + "deleteModelFailed": "刪除模型失敗:{{errorMessage}}", + "categorizeFailed": "影像分類失敗:{{errorMessage}}", + "trainingFailed": "模型訓練失敗,請查看 Frigate 日誌。", + "trainingFailedToStart": "啟動模型訓練失敗:{{errorMessage}}", + "updateModelFailed": "更新模型失敗:{{errorMessage}}", + "renameCategoryFailed": "重新命名類別失敗:{{errorMessage}}" + } + }, + "deleteCategory": { + "title": "刪除類別", + "desc": "確定要刪除類別 {{name}}?這將永久刪除所有相關影像,並需要重新訓練模型。", + "minClassesTitle": "無法刪除類別", + "minClassesDesc": "分類模型至少需要 2 個類別,請先新增類別。" + }, + "deleteModel": { + "title": "刪除分類模型", + "single": "確定要刪除 {{name}}?所有資料將永久刪除且無法復原。", + "desc_other": "確定要刪除 {{count}} 個模型?所有資料將永久刪除且無法復原。" + }, + "edit": { + "title": "編輯分類模型", + "descriptionState": "編輯此狀態分類模型的類別,變更後需重新訓練。", + "descriptionObject": "編輯此物件分類模型的物件類型與分類方式。", + "stateClassesInfo": "更改狀態類別需重新訓練模型。" + }, + "deleteDatasetImages": { + "title": "刪除資料集影像", + "desc_other": "確定要刪除 {{dataset}} 中的 {{count}} 張影像?此操作不可復原並需重新訓練。" + }, + "deleteTrainImages": { + "title": "刪除訓練影像", + "desc_other": "確定要刪除 {{count}} 張影像?此操作不可復原。" + }, + "renameCategory": { + "title": "重新命名類別", + "desc": "為 {{name}} 輸入新名稱,需重新訓練模型才會生效。" + }, + "description": { + "invalidName": "名稱無效,只可包含字母、數字、空格、撇號、底線及連字號。" + }, + "categories": "類別", + "createCategory": { + "new": "建立新類別" + }, + "categorizeImageAs": "將影像分類為:", + "categorizeImage": "分類影像", + "menu": { + "objects": "物件", + "states": "狀態" + }, + "noModels": { + "object": { + "title": "沒有物件分類模型", + "description": "建立自訂模型以分類偵測到的物件。", + "buttonText": "建立物件模型" + }, + "state": { + "title": "沒有狀態分類模型", + "description": "建立自訂模型監測指定區域狀態。", + "buttonText": "建立狀態模型" + } + }, + "wizard": { + "title": "建立新分類", + "steps": { + "nameAndDefine": "名稱與定義", + "stateArea": "狀態區域", + "chooseExamples": "選擇範例" + }, + "step1": { + "description": "狀態模型監測固定區域變化(例如,開門/關門)。物件模型為偵測物件加入分類(例如,已知的動物、送貨員等)。", + "name": "名稱", + "namePlaceholder": "輸入模型名稱…", + "type": "類型", + "typeState": "狀態", + "typeObject": "物件", + "objectLabel": "物件標籤", + "objectLabelPlaceholder": "選擇物件類型…", + "classificationType": "分類類型", + "classificationTypeTip": "了解分類類型", + "classificationTypeDesc": "子標籤為物件增加附加文字(例如,「人員:UPS」)。屬性是可搜尋的元數據,單獨儲存在物件元資料中。", + "classificationSubLabel": "子標籤", + "classificationAttribute": "屬性", + "classes": "類別", + "states": "狀態", + "classesTip": "了解類別", + "classesStateDesc": "定義區域可能狀態。例如:車房門的「開」和「關」狀態。", + "classesObjectDesc": "定義不同類別將偵測到物件去分類。例如:人分類嘅「送貨員」、「居民」、「陌生人」。", + "classPlaceholder": "輸入類別名稱…", + "errors": { + "nameRequired": "必須輸入模型名稱", + "nameLength": "名稱不可超過 64 字元", + "nameOnlyNumbers": "名稱不可只有數字", + "classRequired": "至少需要 1 個類別", + "classesUnique": "類別名稱必須唯一", + "noneNotAllowed": "不可使用「none」", + "stateRequiresTwoClasses": "狀態模型至少需 2 類", + "objectLabelRequired": "請選擇物件標籤", + "objectTypeRequired": "請選擇分類類型" + } + }, + "step2": { + "description": "選擇鏡頭並設定監測區域。模型將對這些區域的狀態進行分類。", + "cameras": "鏡頭", + "selectCamera": "選擇鏡頭", + "noCameras": "按 + 新增鏡頭", + "selectCameraPrompt": "從清單選擇鏡頭以設定區域" + }, + "step3": { + "selectImagesPrompt": "選取所有 {{className}} 影像", + "selectImagesDescription": "點擊影像選取,完成後按繼續。", + "allImagesRequired_other": "請完成所有分類,尚餘 {{count}} 張影像。", + "generating": { + "title": "正在產生範例影像", + "description": "Frigate 正在擷取代表性影像,請稍候…" + }, + "training": { + "title": "正在訓練模型", + "description": "模型正在背景訓練,完成後會自動運行。" + }, + "retryGenerate": "重新產生", + "noImages": "未產生範例影像", + "classifying": "分類及訓練中…", + "trainingStarted": "已成功開始訓練", + "modelCreated": "模型建立成功,請新增影像後再訓練。", + "errors": { + "noCameras": "未設定鏡頭", + "noObjectLabel": "未選擇物件標籤", + "generateFailed": "產生範例失敗:{{error}}", + "generationFailed": "產生失敗,請重試。", + "classifyFailed": "影像分類失敗:{{error}}" + }, + "generateSuccess": "已成功產生範例影像", + "missingStatesWarning": { + "title": "缺少狀態範例", + "description": "建議為所有狀態選取範例以獲得最佳效果。未齊全前模型不會訓練。繼續操作後,使用「最近分類」對缺失狀態的影像進行分類,然後訓練模型。" + } + } + } +} diff --git a/web/public/locales/yue-Hant/views/configEditor.json b/web/public/locales/yue-Hant/views/configEditor.json new file mode 100644 index 0000000..5bf9d8a --- /dev/null +++ b/web/public/locales/yue-Hant/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "設定編輯器 - Frigate", + "configEditor": "設定編輯器", + "copyConfig": "複製設定", + "saveAndRestart": "儲存並重新啟動", + "saveOnly": "只儲存", + "toast": { + "success": { + "copyToClipboard": "設定已複製到剪貼簿。" + }, + "error": { + "savingError": "儲存設定時出錯" + } + }, + "confirm": "是否不儲存就離開?", + "safeConfigEditor": "設定編輯器 (安全模式)", + "safeModeDescription": "Frigate 因配置驗證錯誤而進入安全模式。" +} diff --git a/web/public/locales/yue-Hant/views/events.json b/web/public/locales/yue-Hant/views/events.json new file mode 100644 index 0000000..ba50bc9 --- /dev/null +++ b/web/public/locales/yue-Hant/views/events.json @@ -0,0 +1,67 @@ +{ + "camera": "鏡頭", + "alerts": "警報", + "empty": { + "alert": "沒有警報需要審查", + "detection": "沒有偵測到的項目需要審查", + "motion": "找不到移動數據", + "recordingsDisabled": { + "title": "必須啟用錄影", + "description": "只有在該鏡頭啟用錄影時,才可為該鏡頭建立審查項目。" + } + }, + "timeline": "時間線", + "events": { + "label": "事件", + "noFoundForTimePeriod": "此時段內沒有找到事件。", + "aria": "選擇事件" + }, + "recordings": { + "documentTitle": "錄影 - Frigate" + }, + "calendarFilter": { + "last24Hours": "過去24小時" + }, + "markAsReviewed": "標記為已審查", + "markTheseItemsAsReviewed": "將這些項目標記為已審查", + "newReviewItems": { + "label": "查看新的審查項目", + "button": "有新的審查項目" + }, + "selected_one": "已選擇 {{count}} 項", + "selected_other": "已選擇 {{count}} 項", + "allCameras": "所有鏡頭", + "documentTitle": "審查 - Frigate", + "motion": { + "only": "只顯示移動", + "label": "移動" + }, + "detections": "偵測", + "timeline.aria": "選擇時間線", + "detected": "已偵測", + "suspiciousActivity": "可疑行為", + "threateningActivity": "威脅行為", + "zoomIn": "放大", + "zoomOut": "縮小", + "detail": { + "label": "詳情", + "noDataFound": "沒有可審查的詳情資料", + "aria": "切換詳情檢視", + "trackedObject_one": "{{count}} 個物件", + "trackedObject_other": "{{count}} 個物件", + "noObjectDetailData": "沒有可用的物件詳情資料。", + "settings": "詳情檢視設定", + "alwaysExpandActive": { + "title": "總是展開目前項目", + "desc": "如有資料,總是展開目前審查項目的物件詳情。" + } + }, + "objectTrack": { + "trackedPoint": "追蹤點", + "clickToSeek": "點擊以跳轉至此時間" + }, + "select_all": "全部", + "normalActivity": "正常", + "needsReview": "需要審查", + "securityConcern": "安全疑慮" +} diff --git a/web/public/locales/yue-Hant/views/explore.json b/web/public/locales/yue-Hant/views/explore.json new file mode 100644 index 0000000..b6c780c --- /dev/null +++ b/web/public/locales/yue-Hant/views/explore.json @@ -0,0 +1,301 @@ +{ + "documentTitle": "瀏覽 - Frigate", + "generativeAI": "生成式人工智能", + "exploreIsUnavailable": { + "title": "無法使用瀏覽功能", + "embeddingsReindexing": { + "startingUp": "啟動中…", + "estimatedTime": "預計剩餘時間:", + "finishingShortly": "即將完成", + "step": { + "thumbnailsEmbedded": "已嵌入縮圖: ", + "descriptionsEmbedded": "已嵌入描述: ", + "trackedObjectsProcessed": "已處理的追蹤物件: " + }, + "context": "完成重新索引追蹤物件的嵌入後即可使用瀏覽功能。" + }, + "downloadingModels": { + "tips": { + "context": "下載完成後,你可能需要重新索引追蹤物件的嵌入。", + "documentation": "閱讀文件" + }, + "error": "發生錯誤。請檢查 Frigate 日誌。", + "context": "Frigate 正在下載必要的嵌入模型以支援語意搜尋功能。這可能需要幾分鐘,視乎你的網絡速度。", + "setup": { + "textTokenizer": "文字分詞器", + "textModel": "文字模型", + "visionModelFeatureExtractor": "視覺模型特徵提取器", + "visionModel": "視覺模型" + } + } + }, + "trackedObjectDetails": "追蹤物件詳情", + "type": { + "details": "詳情", + "snapshot": "快照", + "video": "影片", + "object_lifecycle": "物件生命周期", + "thumbnail": "縮圖", + "tracking_details": "追蹤詳情" + }, + "objectLifecycle": { + "title": "物件生命周期", + "noImageFound": "此時間點找不到圖像。", + "createObjectMask": "建立物件遮罩", + "lifecycleItemDesc": { + "active": "{{label}} 變為活躍", + "stationary": "{{label}} 變為靜止", + "attribute": { + "faceOrLicense_plate": "偵測到 {{label}} 的 {{attribute}}", + "other": "{{label}} 被識別為 {{attribute}}" + }, + "header": { + "zones": "區域", + "ratio": "比例", + "area": "區域範圍" + }, + "heard": "聽到 {{label}}", + "entered_zone": "{{label}} 進入了 {{zones}}", + "gone": "{{label}} 離開了", + "visible": "偵測到 {{label}}", + "external": "偵測到 {{label}}" + }, + "annotationSettings": { + "title": "註解設定", + "showAllZones": { + "title": "顯示所有區域", + "desc": "在物件進入區域的畫面上總是顯示區域。" + }, + "offset": { + "tips": "提示:試想像有一段事件片段,當中有人由左行到右。如果事件時間線上的方框一直偏向人物的左邊,則應該減少數值。相反,如果有人由左行到右,而方框一直走在人物前面,則應該增加數值。", + "desc": "此資料來自鏡頭的偵測串流,但覆蓋在錄影串流的畫面上。兩個串流通常無法完全同步。因此邊界框和影片可能無法完全對齊。不過可以使用 annotation_offset 欄位來調整。", + "label": "註解偏移量", + "documentation": "閱讀文件 ", + "millisecondsToOffset": "偵測註解的偏移毫秒數。預設:0", + "toast": { + "success": "{{camera}} 的註解偏移量已儲存到設定檔。請重新啟動 Frigate 以套用更改。" + } + } + }, + "carousel": { + "previous": "上一張", + "next": "下一張" + }, + "adjustAnnotationSettings": "調整註解設定", + "scrollViewTips": "滾動以查看此物件生命周期中的重要時刻。", + "autoTrackingTips": "自動追蹤鏡頭的邊界框位置可能不準確。", + "count": "第 {{first}} 個,共 {{second}} 個", + "trackedPoint": "追蹤點" + }, + "details": { + "item": { + "title": "審查項目詳情", + "desc": "審查項目詳情", + "button": { + "share": "分享此審查項目", + "viewInExplore": "在瀏覽中查看" + }, + "tips": { + "mismatch_other": "偵測到 {{count}} 個不可用的物件並包含在此審查項目中。這些物件可能未符合警報或偵測標準,或已被清除/刪除。", + "hasMissingObjects": "如果你想讓 Frigate 保存下列標籤的追蹤物件,請調整設定:{{objects}}" + }, + "toast": { + "success": { + "updatedSublabel": "成功更新子標籤。", + "updatedLPR": "成功更新車牌號碼。", + "regenerate": "已從 {{provider}} 請求新的描述。根據提供者的速度,生成新的描述可能需要一些時間。", + "audioTranscription": "成功請求音訊轉錄。視乎你的 Frigate 伺服器速度,轉錄可能需要一些時間完成。", + "updatedAttributes": "已成功更新屬性。" + }, + "error": { + "regenerate": "呼叫 {{provider}} 以獲取新描述失敗:{{errorMessage}}", + "updatedSublabelFailed": "更新子標籤失敗:{{errorMessage}}", + "updatedLPRFailed": "更新車牌號碼失敗:{{errorMessage}}", + "audioTranscription": "請求音訊轉錄失敗:{{errorMessage}}", + "updatedAttributesFailed": "更新屬性失敗:{{errorMessage}}" + } + } + }, + "label": "標籤", + "recognizedLicensePlate": "已識別車牌", + "estimatedSpeed": "預計速度", + "objects": "物件", + "camera": "鏡頭", + "zones": "區域", + "timestamp": "時間戳記", + "tips": { + "descriptionSaved": "成功保存描述", + "saveDescriptionFailed": "更新描述失敗:{{errorMessage}}" + }, + "regenerateFromSnapshot": "從快照重新生成", + "button": { + "regenerate": { + "label": "重新生成追蹤物件描述", + "title": "重新生成" + }, + "findSimilar": "尋找相似項目" + }, + "description": { + "label": "描述", + "placeholder": "追蹤物件的描述", + "aiTips": "Frigate 會等到追蹤物件生命周期結束後,才向你的生成式 AI 提供者請求描述。" + }, + "editLPR": { + "descNoLabel": "為此追蹤物件輸入新的車牌號碼", + "title": "編輯車牌號碼", + "desc": "為此 {{label}} 輸入新的車牌號碼" + }, + "topScore": { + "label": "最高分數", + "info": "最高分數是追蹤物件的最高中位分數,因此可能與搜尋結果縮圖上顯示的分數不同。" + }, + "editSubLabel": { + "desc": "為此 {{label}} 輸入新的子標籤", + "title": "編輯子標籤", + "descNoLabel": "為此追蹤物件輸入新的子標籤" + }, + "snapshotScore": { + "label": "快照分數" + }, + "expandRegenerationMenu": "展開重新生成選單", + "regenerateFromThumbnails": "從縮圖重新生成", + "score": { + "label": "分數" + }, + "editAttributes": { + "title": "編輯屬性", + "desc": "為此 {{label}} 選擇分類屬性" + }, + "attributes": "分類屬性", + "title": { + "label": "標題" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "下載影片", + "aria": "下載影片" + }, + "downloadSnapshot": { + "label": "下載快照", + "aria": "下載快照" + }, + "viewObjectLifecycle": { + "label": "查看物件生命周期", + "aria": "顯示物件生命周期" + }, + "findSimilar": { + "label": "尋找相似項目", + "aria": "尋找相似追蹤物件" + }, + "submitToPlus": { + "label": "提交到 Frigate+", + "aria": "提交到 Frigate Plus" + }, + "viewInHistory": { + "label": "在歷史記錄中查看", + "aria": "在歷史記錄中查看" + }, + "deleteTrackedObject": { + "label": "刪除此追蹤物件" + }, + "addTrigger": { + "label": "新增觸發器", + "aria": "為此追蹤物件新增觸發器" + }, + "audioTranscription": { + "label": "轉錄音訊", + "aria": "請求音訊轉錄" + }, + "downloadCleanSnapshot": { + "label": "下載乾淨快照", + "aria": "下載乾淨快照" + }, + "viewTrackingDetails": { + "label": "檢視追蹤詳情", + "aria": "顯示追蹤詳情" + }, + "showObjectDetails": { + "label": "顯示物件路徑" + }, + "hideObjectDetails": { + "label": "隱藏物件路徑" + } + }, + "dialog": { + "confirmDelete": { + "title": "確認刪除", + "desc": "刪除此追蹤物件會移除快照、所有已保存的嵌入,以及相關的追蹤詳情記錄。歷史記錄中的錄影不會被刪除。

    你確定要繼續嗎?" + } + }, + "noTrackedObjects": "找不到追蹤物件", + "fetchingTrackedObjectsFailed": "取得追蹤物件時出錯:{{errorMessage}}", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "追蹤物件已成功刪除。", + "error": "刪除追蹤物件失敗:{{errorMessage}}" + } + }, + "tooltip": "已配對{{type}}({{confidence}}% 信心", + "previousTrackedObject": "上一個追蹤物件", + "nextTrackedObject": "下一個追蹤物件" + }, + "trackedObjectsCount_other": "{{count}} 個追蹤物件 ", + "exploreMore": "瀏覽更多{{label}}物件", + "aiAnalysis": { + "title": "AI 分析" + }, + "concerns": { + "label": "關注" + }, + "trackingDetails": { + "title": "追蹤詳情", + "noImageFound": "找不到此時間點的影像。", + "createObjectMask": "建立物件遮罩", + "adjustAnnotationSettings": "調整標註設定", + "scrollViewTips": "點擊以查看此物件生命週期中的重要時刻。", + "autoTrackingTips": "對於自動追蹤鏡頭,邊界框位置可能不準確。", + "count": "第 {{first}} 個,共 {{second}} 個", + "trackedPoint": "追蹤點", + "lifecycleItemDesc": { + "visible": "偵測到 {{label}}", + "entered_zone": "{{label}} 進入 {{zones}}", + "active": "{{label}} 變為活動中", + "stationary": "{{label}} 變為靜止", + "attribute": { + "faceOrLicense_plate": "偵測到 {{label}} 的 {{attribute}}", + "other": "{{label}} 被識別為 {{attribute}}" + }, + "gone": "{{label}} 離開", + "heard": "偵測到 {{label}} 聲音", + "external": "偵測到 {{label}}", + "header": { + "zones": "區域", + "ratio": "比例", + "area": "面積", + "score": "分數" + } + }, + "annotationSettings": { + "title": "標註設定", + "showAllZones": { + "title": "顯示所有區域", + "desc": "當物件進入區域時,始終在畫面上顯示該區域。" + }, + "offset": { + "label": "標註偏移", + "desc": "此資料來自鏡頭的偵測串流,但會疊加在錄影串流的影像上。兩個串流不太可能完全同步,因此邊界框與影片畫面未必完全對齊。你可使用此設定將標註在時間上向前或向後偏移,以更好地對齊錄影畫面。", + "millisecondsToOffset": "偵測標註的偏移毫秒數。預設:0", + "tips": "如果影片播放比邊界框與路徑點快,請降低數值;如果影片播放較慢,請提高數值。此數值可以為負。", + "toast": { + "success": "{{camera}} 的標註偏移已儲存到設定檔。" + } + } + }, + "carousel": { + "previous": "上一張", + "next": "下一張" + } + } +} diff --git a/web/public/locales/yue-Hant/views/exports.json b/web/public/locales/yue-Hant/views/exports.json new file mode 100644 index 0000000..a8c14b5 --- /dev/null +++ b/web/public/locales/yue-Hant/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "匯出 - Frigate", + "search": "搜尋", + "noExports": "未找到匯出項目", + "deleteExport": "刪除匯出", + "editExport": { + "title": "重新命名匯出", + "desc": "請輸入新的匯出名稱。", + "saveExport": "儲存匯出" + }, + "toast": { + "error": { + "renameExportFailed": "重新命名匯出失敗:{{errorMessage}}" + } + }, + "deleteExport.desc": "你確定要刪除 {{exportName}} 嗎?", + "tooltip": { + "shareExport": "分享匯出", + "downloadVideo": "下載影片", + "editName": "編輯名稱", + "deleteExport": "刪除匯出" + } +} diff --git a/web/public/locales/yue-Hant/views/faceLibrary.json b/web/public/locales/yue-Hant/views/faceLibrary.json new file mode 100644 index 0000000..01441bd --- /dev/null +++ b/web/public/locales/yue-Hant/views/faceLibrary.json @@ -0,0 +1,99 @@ +{ + "selectItem": "選擇 {{item}}", + "details": { + "timestamp": "時間戳記", + "person": "人", + "confidence": "信心指數", + "face": "人臉詳細資料", + "faceDesc": "產生這個人臉的追蹤物件的詳細資料", + "scoreInfo": "子標籤分數是所有已識別人臉的信心值加權分數,因此可能與快照上顯示的分數不同。", + "subLabelScore": "子標籤分數", + "unknown": "未知" + }, + "description": { + "addFace": "上傳您的第一張圖片,即可在人臉庫中新增新的集合。", + "placeholder": "請輸入此集合的名稱", + "invalidName": "名稱無效,只可包含字母、數字、空格、撇號、底線及連字號。", + "nameCannotContainHash": "名稱不可包含 #。" + }, + "documentTitle": "人臉庫 - Frigate", + "uploadFaceImage": { + "title": "上傳人臉圖片", + "desc": "上傳圖片以掃描人臉並納入 {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "建立集合", + "desc": "建立新集合", + "new": "建立新的人臉", + "nextSteps": "建立穩固基礎:
  • 使用最近識別分頁,為每位偵測到的人物選擇並訓練圖片。
  • 以正面照片為主,避免用側面或傾斜角度的人臉作訓練。
  • " + }, + "steps": { + "faceName": "請輸入人臉名稱", + "uploadFace": "上傳人臉圖片", + "nextSteps": "下一步", + "description": { + "uploadFace": "請上載一張{{name}}面向鏡頭的相片。相片不需要裁剪至只顯示人臉。" + } + }, + "train": { + "title": "最近識別", + "aria": "選擇最近識別", + "empty": "最近沒有人臉識別嘗試", + "titleShort": "最近" + }, + "selectFace": "選擇人臉", + "deleteFaceLibrary": { + "title": "刪除名稱", + "desc": "你確定要刪除集合 {{name}} 嗎?這將永久刪除所有相關的人臉資料。" + }, + "renameFace": { + "title": "重新命名人臉", + "desc": "請輸入 {{name}} 的新名稱" + }, + "button": { + "uploadImage": "上傳圖片", + "reprocessFace": "重新處理人臉", + "deleteFace": "刪除人臉", + "addFace": "新增人臉", + "deleteFaceAttempts": "刪除人臉", + "renameFace": "重新命名人臉" + }, + "imageEntry": { + "validation": { + "selectImage": "請選擇一個圖片檔案。" + }, + "dropActive": "將圖片拖到這裡…", + "dropInstructions": "拖放圖片或貼上到此處,或點擊選取", + "maxSize": "最大檔案大小:{{size}}MB" + }, + "readTheDocs": "閱讀文件", + "trainFaceAs": "將人臉訓練為:", + "trainFace": "訓練人臉", + "toast": { + "success": { + "uploadedImage": "成功上傳圖片。", + "renamedFace": "成功將人臉重新命名為 {{name}}", + "trainedFace": "成功訓練人臉。", + "updatedFaceScore": "已成功更新 {{name}} 的人臉分數({{score}})。", + "deletedFace_other": "成功刪除 {{count}} 個人臉。", + "addFaceLibrary": "{{name}} 已成功加入人臉庫!", + "deletedName_other": "成功刪除 {{count}} 個人臉。" + }, + "error": { + "uploadingImageFailed": "上傳圖片失敗:{{errorMessage}}", + "addFaceLibraryFailed": "設定人臉名稱失敗:{{errorMessage}}", + "deleteFaceFailed": "刪除失敗:{{errorMessage}}", + "deleteNameFailed": "刪除名稱失敗:{{errorMessage}}", + "renameFaceFailed": "重新命名人臉失敗:{{errorMessage}}", + "trainFailed": "訓練失敗:{{errorMessage}}", + "updateFaceScoreFailed": "更新人臉分數失敗:{{errorMessage}}" + } + }, + "collections": "集合", + "deleteFaceAttempts": { + "desc_other": "你確定要刪除{{count}}個人臉嗎?這個動作無法還原。", + "title": "刪除人臉" + }, + "nofaces": "沒有可用人臉", + "pixels": "{{area}} 像素" +} diff --git a/web/public/locales/yue-Hant/views/live.json b/web/public/locales/yue-Hant/views/live.json new file mode 100644 index 0000000..6ebd69f --- /dev/null +++ b/web/public/locales/yue-Hant/views/live.json @@ -0,0 +1,199 @@ +{ + "documentTitle": "即時畫面 - Frigate", + "cameraAudio": { + "disable": "停用鏡頭音訊", + "enable": "啟用鏡頭音訊" + }, + "ptz": { + "move": { + "clickMove": { + "label": "點擊畫面以置中鏡頭", + "enable": "啟用點擊移動", + "disable": "停用點擊移動" + }, + "up": { + "label": "移動 PTZ 鏡頭向上" + }, + "right": { + "label": "移動 PTZ 鏡頭向右" + }, + "left": { + "label": "移動 PTZ 鏡頭向左" + }, + "down": { + "label": "移動 PTZ 鏡頭向下" + } + }, + "frame": { + "center": { + "label": "點擊畫面以置中 PTZ 鏡頭" + } + }, + "presets": "PTZ 鏡頭預設位置", + "zoom": { + "in": { + "label": "放大 PTZ 鏡頭" + }, + "out": { + "label": "縮小 PTZ 鏡頭" + } + }, + "focus": { + "in": { + "label": "PTZ 鏡頭拉近焦距" + }, + "out": { + "label": "PTZ 鏡頭拉遠焦距" + } + } + }, + "twoWayTalk": { + "enable": "啟用雙向通話", + "disable": "停用雙向通話" + }, + "lowBandwidthMode": "低頻寬模式", + "documentTitle.withCamera": "{{camera}} - 即時畫面 - Frigate", + "recording": { + "disable": "停用錄影", + "enable": "啟用錄影" + }, + "snapshots": { + "enable": "啟用快照", + "disable": "停用快照" + }, + "audioDetect": { + "enable": "啟用音訊偵測", + "disable": "停用音訊偵測" + }, + "autotracking": { + "enable": "啟用自動追蹤", + "disable": "停用自動追蹤" + }, + "streamStats": { + "enable": "顯示串流統計資料", + "disable": "隱藏串流統計資料" + }, + "manualRecording": { + "title": "按需", + "tips": "根據此鏡頭的錄影保留設定手動啟動事件。", + "debugView": "除錯視圖", + "start": "開始按需錄影", + "showStats": { + "label": "顯示統計資料", + "desc": "啟用此選項可在鏡頭畫面上疊加串流統計資料。" + }, + "playInBackground": { + "desc": "啟用此選項可在播放器隱藏時繼續串流播放。", + "label": "背景播放" + }, + "started": "已開始手動按需錄影。", + "end": "結束按需錄影", + "ended": "已結束手動按需錄影。", + "failedToEnd": "無法結束手動按需錄影。", + "failedToStart": "無法開始手動按需錄影。", + "recordDisabledTips": "由於此鏡頭的設定已停用或限制錄影,因此只會儲存快照。" + }, + "camera": { + "enable": "啟用鏡頭", + "disable": "停用鏡頭" + }, + "muteCameras": { + "enable": "所有鏡頭靜音", + "disable": "所有鏡頭取消靜音" + }, + "detect": { + "disable": "停用偵測", + "enable": "啟用偵測" + }, + "streamingSettings": "串流設定", + "notifications": "通知", + "audio": "音訊", + "suspend": { + "forTime": "暫停時間: " + }, + "stream": { + "title": "串流", + "audio": { + "tips": { + "documentation": "閱讀文件 ", + "title": "音訊必須從你的鏡頭輸出,並在 go2rtc 中正確設定此串流。" + }, + "available": "此串流支援音訊", + "unavailable": "此串流不支援音訊" + }, + "twoWayTalk": { + "tips.documentation": "閱讀文件 ", + "available": "此串流支援雙向通話", + "unavailable": "此串流不支援雙向通話", + "tips": "你的裝置必須支援此功能,且需設定 WebRTC 才能使用雙向通話。" + }, + "lowBandwidth": { + "tips": "因緩衝或串流錯誤,即時畫面已切換至低頻寬模式。", + "resetStream": "重置串流" + }, + "playInBackground": { + "tips": "啟用此選項可在播放器隱藏時繼續串流播放。", + "label": "背景播放" + }, + "debug": { + "picker": "除錯模式下無法選擇串流。除錯視圖永遠使用已分配偵測角色的串流。" + } + }, + "cameraSettings": { + "cameraEnabled": "鏡頭已啟用", + "objectDetection": "物件偵測", + "recording": "錄影", + "snapshots": "快照", + "autotracking": "自動追蹤", + "audioDetection": "音訊偵測", + "title": "{{camera}} 設定", + "transcription": "音訊轉錄" + }, + "history": { + "label": "顯示歷史影像" + }, + "effectiveRetainMode": { + "modes": { + "all": "全部", + "motion": "移動", + "active_objects": "活躍物件" + }, + "notAllTips": "你的 {{source}} 錄影保留設定為 mode: {{effectiveRetainMode}},因此此按需錄影只會保留{{effectiveRetainModeName}}的片段。" + }, + "editLayout": { + "label": "編輯版面配置", + "group": { + "label": "編輯鏡頭群組" + }, + "exitEdit": "結束編輯" + }, + "transcription": { + "enable": "啟用即時音訊轉錄", + "disable": "停用即時音訊轉錄" + }, + "noCameras": { + "title": "未設定任何鏡頭", + "description": "請先將鏡頭連接到 Frigate 以開始使用。", + "buttonText": "新增鏡頭", + "restricted": { + "title": "沒有可用鏡頭", + "description": "你沒有權限檢視此群組中的任何鏡頭。" + }, + "default": { + "title": "未設定任何鏡頭", + "description": "請先將鏡頭連接到 Frigate 以開始使用。", + "buttonText": "新增鏡頭" + }, + "group": { + "title": "群組中沒有鏡頭", + "description": "此鏡頭群組沒有已指派或已啟用的鏡頭。", + "buttonText": "管理群組" + } + }, + "snapshot": { + "takeSnapshot": "下載即時快照", + "noVideoSource": "無可用影片來源以擷取快照。", + "captureFailed": "擷取快照失敗。", + "downloadStarted": "已開始下載快照。" + } +} diff --git a/web/public/locales/yue-Hant/views/motionSearch.json b/web/public/locales/yue-Hant/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/yue-Hant/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/yue-Hant/views/recording.json b/web/public/locales/yue-Hant/views/recording.json new file mode 100644 index 0000000..34473d2 --- /dev/null +++ b/web/public/locales/yue-Hant/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "篩選", + "export": "匯出", + "calendar": "日曆", + "filters": "篩選條件", + "toast": { + "error": { + "noValidTimeSelected": "未選擇有效的時間範圍", + "endTimeMustAfterStartTime": "結束時間必須在開始時間之後" + } + } +} diff --git a/web/public/locales/yue-Hant/views/replay.json b/web/public/locales/yue-Hant/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/yue-Hant/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/yue-Hant/views/search.json b/web/public/locales/yue-Hant/views/search.json new file mode 100644 index 0000000..ffc353e --- /dev/null +++ b/web/public/locales/yue-Hant/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "搜尋", + "savedSearches": "已儲存的搜尋", + "searchFor": "搜尋 {{inputValue}}", + "button": { + "clear": "清除搜尋", + "save": "儲存搜尋", + "delete": "刪除已儲存的搜尋", + "filterInformation": "篩選資料", + "filterActive": "篩選中" + }, + "trackedObjectId": "追蹤物件編號", + "filter": { + "label": { + "labels": "標籤", + "zones": "區域", + "search_type": "搜尋類型", + "time_range": "時間範圍", + "after": "之後", + "recognized_license_plate": "已辨識車牌", + "has_clip": "有片段", + "has_snapshot": "有快照", + "min_score": "最低分數", + "before": "之前", + "max_score": "最高分數", + "max_speed": "最高速度", + "min_speed": "最低速度", + "cameras": "鏡頭", + "sub_labels": "子標籤", + "attributes": "屬性" + }, + "searchType": { + "thumbnail": "縮圖", + "description": "描述" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "「結束」日期必須遲於「開始」日期。", + "afterDatebeEarlierBefore": "「開始」日期必須早於「結束」日期。", + "minScoreMustBeLessOrEqualMaxScore": "「最低分數」必須少於或等於「最高分數」。", + "maxScoreMustBeGreaterOrEqualMinScore": "「最高分數」必須多於或等於「最低分數」。", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "「最高速度」必須多於或等於「最低速度」。", + "minSpeedMustBeLessOrEqualMaxSpeed": "「最低速度」必須少於或等於「最高速度」。" + } + }, + "tips": { + "title": "如何使用文字篩選", + "desc": { + "step1": "輸入篩選鍵名後加上冒號(例如:\"cameras:\")。", + "step2": "從建議中選擇一個值,或者自行輸入。", + "step3": "可以用空格隔開,連續使用多個篩選條件。", + "step4": "日期篩選(before: 同 after:)要用 {{DateFormat}} 格式。", + "step5": "時間範圍篩選要用 {{exampleTime}} 格式。", + "step6": "點擊旁邊的「x」就可以移除篩選條件。", + "text": "篩選可以幫你縮窄搜尋結果。以下係使用方法:", + "exampleLabel": "例子:" + } + }, + "header": { + "activeFilters": "啟用中的篩選條件", + "noFilters": "篩選條件", + "currentFilterType": "篩選數值" + } + }, + "similaritySearch": { + "title": "相似搜尋", + "active": "正在進行相似搜尋", + "clear": "清除相似搜尋" + }, + "placeholder": { + "search": "搜尋…" + } +} diff --git a/web/public/locales/yue-Hant/views/settings.json b/web/public/locales/yue-Hant/views/settings.json new file mode 100644 index 0000000..36907ee --- /dev/null +++ b/web/public/locales/yue-Hant/views/settings.json @@ -0,0 +1,1298 @@ +{ + "documentTitle": { + "default": "設定 - Frigate", + "authentication": "認證設定 - Frigate", + "camera": "鏡頭設定 - Frigate", + "classification": "進階功能設定 - Frigate", + "masksAndZones": "遮罩與區域編輯器 - Frigate", + "motionTuner": "移動調校器 - Frigate", + "object": "除錯 - Frigate", + "general": "介面設定 - Frigate", + "frigatePlus": "Frigate+ 設定 - Frigate", + "notifications": "通知設定 - Frigate", + "enrichments": "進階功能設定 - Frigate", + "cameraManagement": "管理鏡頭 - Frigate", + "cameraReview": "鏡頭檢視設定 - Frigate" + }, + "menu": { + "ui": "介面", + "classification": "進階功能", + "cameras": "鏡頭設定", + "masksAndZones": "遮罩/區域", + "motionTuner": "移動調校器", + "debug": "除錯", + "users": "用戶", + "notifications": "通知", + "frigateplus": "Frigate+", + "enrichments": "進階功能", + "triggers": "觸發器", + "roles": "角色", + "cameraManagement": "管理", + "cameraReview": "審查" + }, + "dialog": { + "unsavedChanges": { + "title": "你有未儲存的更改。", + "desc": "你想在繼續前儲存更改嗎?" + } + }, + "cameraSetting": { + "camera": "鏡頭", + "noCamera": "沒有鏡頭" + }, + "general": { + "title": "介面設定", + "liveDashboard": { + "playAlertVideos": { + "label": "播放警報影片", + "desc": "預設情況下,即時儀表板上的最近警報會以小型循環影片形式播放。停用此選項後,只會在此裝置/瀏覽器上顯示警報的靜態圖片。" + }, + "automaticLiveView": { + "label": "自動即時檢視", + "desc": "當偵測到活動時,自動切換到該鏡頭的即時畫面。若停用此選項,即時儀表板上的鏡頭靜態畫面將每分鐘只更新一次。" + }, + "title": "即時儀表板", + "displayCameraNames": { + "label": "一直顯示鏡頭名稱", + "desc": "在多鏡頭即時畫面儀表板中以標籤顯示鏡頭名稱。" + }, + "liveFallbackTimeout": { + "label": "即時播放器備援逾時", + "desc": "當高畫質即時串流不可用時,於指定秒數後切換至低頻寬模式。預設:3。" + } + }, + "storedLayouts": { + "title": "儲存的版面配置", + "clearAll": "清除所有版面配置", + "desc": "鏡頭群組內的鏡頭佈局可以拖動或調整大小。位置會儲存在你瀏覽器的本機儲存空間內。" + }, + "cameraGroupStreaming": { + "title": "鏡頭群組串流設定", + "clearAll": "清除所有串流設定", + "desc": "每個鏡頭群組的串流設定會儲存在你瀏覽器的本機儲存空間內。" + }, + "recordingsViewer": { + "defaultPlaybackRate": { + "desc": "錄影播放的預設播放速度。", + "label": "預設播放速度" + }, + "title": "錄影瀏覽器" + }, + "calendar": { + "title": "日曆", + "firstWeekday": { + "label": "每星期的第一天", + "sunday": "星期日", + "monday": "星期一", + "desc": "審查日曆中每星期開始的日子。" + } + }, + "toast": { + "success": { + "clearStoredLayout": "已清除 {{cameraName}} 的儲存版面配置", + "clearStreamingSettings": "已清除所有鏡頭群組的串流設定。" + }, + "error": { + "clearStoredLayoutFailed": "清除儲存版面配置失敗:{{errorMessage}}", + "clearStreamingSettingsFailed": "清除串流設定失敗:{{errorMessage}}" + } + } + }, + "classification": { + "birdClassification": { + "desc": "鳥類分類會使用量化 Tensorflow 模型識別已知鳥類。當辨識到已知鳥類時,牠的常見名稱會加到子標籤上。此資訊會顯示在介面、篩選器及通知中。", + "title": "鳥類分類" + }, + "semanticSearch": { + "title": "語意搜尋", + "desc": "Frigate 的語意搜尋功能讓你可以利用影像本身、自訂文字描述,或自動產生的描述,在審查項目中尋找已追蹤的物件。", + "readTheDocumentation": "閱讀文件", + "reindexNow": { + "label": "立即重建索引", + "confirmTitle": "確認重建索引", + "confirmButton": "重建索引", + "success": "重建索引已成功開始。", + "alreadyInProgress": "重建索引已在進行中。", + "error": "啟動重建索引失敗:{{errorMessage}}", + "confirmDesc": "你確定要重建索引所有已追蹤物件的嵌入向量嗎?這個過程會在背景運行,但可能會用盡你的 CPU,而且需要一定時間。你可以在「瀏覽」頁面查看進度。", + "desc": "重建索引會為所有已追蹤物件重新生成嵌入向量。這個過程會在背景運行,可能會用盡你的 CPU,所需時間取決於已追蹤物件的數量。" + }, + "modelSize": { + "label": "模型大小", + "desc": "用於語意搜尋的模型大小。", + "small": { + "title": "小型", + "desc": "使用小型模型會採用量化版本,較少佔用 RAM,在 CPU 上運行更快,而嵌入品質的差異非常細微。" + }, + "large": { + "title": "大型", + "desc": "使用大型模型會採用完整的 Jina 模型,並在適用情況下自動於 GPU 上運行。" + } + } + }, + "faceRecognition": { + "modelSize": { + "small": { + "title": "小型", + "desc": "使用小型模型會採用 FaceNet 臉部嵌入模型,在大多數 CPU 上能有效運行。" + }, + "large": { + "title": "大型", + "desc": "使用大型模型會採用 ArcFace 臉部嵌入模型,並在適用情況下自動於 GPU 上運行。" + }, + "label": "模型大小", + "desc": "用於人臉識別的模型大小。" + }, + "readTheDocumentation": "閱讀文件", + "title": "人臉識別", + "desc": "人臉識別功能允許為人物分配名字,當辨識到他們的臉孔時,Frigate 會將名字加到子標籤上。此資訊會顯示於介面、篩選器及通知中。" + }, + "licensePlateRecognition": { + "title": "車牌識別", + "readTheDocumentation": "閱讀文件", + "desc": "Frigate 可以識別車輛上的車牌,自動將偵測到的字元加到已辨識車牌欄位,或將已知名稱加到屬於車輛類型的物件的子標籤上。常見用途包括讀取駛入車道或在街道上駛過的車輛的車牌。" + }, + "restart_required": "需要重新啟動(分類設定已變更)", + "toast": { + "error": "儲存設定變更失敗:{{errorMessage}}", + "success": "分類設定已儲存。請重新啟動 Frigate 以套用你的更改。" + }, + "title": "進階功能設定", + "unsavedChanges": "進階功能設定更改尚未儲存" + }, + "camera": { + "title": "鏡頭設定", + "streams": { + "title": "串流", + "desc": "暫時停用鏡頭直到Frigate重新啟動。停用鏡頭會完全停止 Frigate 處理此鏡頭的串流。將會無法使用偵測、錄影和除錯功能。
    注意:這不會停用 go2rtc 的轉播功能。" + }, + "review": { + "title": "審查", + "desc": "暫時啟用或停用此鏡頭的警報和偵測直到Frigate重新啟動。停用後,將不會產生新的審查項目。 ", + "alerts": "警報 ", + "detections": "偵測 " + }, + "reviewClassification": { + "readTheDocumentation": "閱讀文件", + "noDefinedZones": "此鏡頭未定義任何區域。", + "zoneObjectAlertsTips": "在{{cameraName}}的{{zone}}區域偵測到的所有{{alertsLabels}}物件將會顯示為警報。", + "objectDetectionsTips": "無論位於哪個區域,在{{cameraName}}上所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。", + "objectAlertsTips": "在{{cameraName}}上所有{{alertsLabels}}物件將會顯示為警報。", + "zoneObjectDetectionsTips": { + "text": "在{{cameraName}}的{{zone}}區域內所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。", + "regardlessOfZoneObjectDetectionsTips": "無論位於哪個區域,在{{cameraName}}上所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。", + "notSelectDetections": "無論位於哪個區域,在{{cameraName}}的{{zone}}區域偵測到、但未分類為警報的{{detectionsLabels}}物件將會顯示為偵測結果。" + }, + "selectDetectionsZones": "選擇偵測的區域", + "limitDetections": "限制偵測至特定區域", + "title": "審查分類", + "desc": "Frigate會將審查項目分類為「警報」同「偵測」。預設情況下,所有 的物件都會被視為警報。你可以透過設定所需區域,細分審查項目分類。", + "selectAlertsZones": "選擇警報的區域", + "toast": { + "success": "審查分類設定已儲存。請重新啟動Frigate以套用更改。" + }, + "unsavedChanges": "{{camera}}的審查分類設定尚未儲存" + }, + "object_descriptions": { + "title": "生成式 AI 物件描述", + "desc": "暫時啟用或停用此鏡頭生成式 AI 物件描述。停用時,不會為此鏡頭的追蹤物件請求 AI 描述。" + }, + "review_descriptions": { + "title": "生成式 AI 審查描述", + "desc": "暫時啟用或停用此鏡頭生成式 AI 審查描述。停用時,不會為此鏡頭的審查項目請求 AI 描述。" + }, + "addCamera": "新增鏡頭", + "editCamera": "編輯鏡頭:", + "selectCamera": "選擇鏡頭", + "backToSettings": "返回鏡頭設定", + "cameraConfig": { + "add": "新增鏡頭", + "edit": "編輯鏡頭", + "description": "設定鏡頭,包括串流輸入同角色分配。", + "name": "鏡頭名稱", + "nameRequired": "必須填寫鏡頭名稱", + "nameLength": "鏡頭名稱不得多於 24 個字元。", + "namePlaceholder": "例如:front_door", + "enabled": "已啟用", + "ffmpeg": { + "inputs": "輸入串流", + "path": "串流路徑", + "pathRequired": "必須填寫串流路徑", + "pathPlaceholder": "rtsp://...", + "roles": "角色", + "rolesRequired": "至少需要分配一個角色", + "rolesUnique": "每個角色(音訊、偵測、錄影)只可分配到一個串流", + "addInput": "新增輸入串流", + "removeInput": "移除輸入串流", + "inputsRequired": "至少需要一個輸入串流" + }, + "toast": { + "success": "鏡頭 {{cameraName}} 已成功儲存" + } + } + }, + "masksAndZones": { + "toast": { + "error": { + "copyCoordinatesFailed": "無法將座標複製到剪貼簿。" + }, + "success": { + "copyCoordinates": "已將{{polyName}}的座標複製到剪貼簿。" + } + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "區域名稱必須至少有2個字元。", + "mustNotBeSameWithCamera": "區域名稱不得與鏡頭名稱相同。", + "alreadyExists": "此鏡頭已存在相同名稱的區域。", + "mustNotContainPeriod": "區域名稱不可包含句號。", + "hasIllegalCharacter": "區域名稱包含非法字元。", + "mustHaveAtLeastOneLetter": "區域名稱至少需包含一個字母。" + } + }, + "distance": { + "error": { + "mustBeFilled": "必須填寫所有距離欄位,才能使用速度估算功能。", + "text": "距離必須大於或等於0.1。" + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "慣性值必須大於0。" + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "逗留時間必須大於或等於0。" + } + }, + "polygonDrawing": { + "removeLastPoint": "移除最後一個點", + "delete": { + "title": "確認刪除", + "desc": "確定要刪除{{type}} {{name}}嗎?", + "success": "已刪除{{name}}。" + }, + "error": { + "mustBeFinished": "必須完成多邊形繪製後才能儲存。" + }, + "snapPoints": { + "false": "不對齊點", + "true": "對齊點" + }, + "reset": { + "label": "清除所有點" + }, + "type": { + "zone": "區域", + "motion_mask": "移動遮罩", + "object_mask": "物件遮罩" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "速度閾值必須大於或等於 0.1。" + } + } + }, + "zones": { + "label": "區域", + "add": "新增區域", + "edit": "編輯區域", + "point_other": "{{count}}個點", + "clickDrawPolygon": "在圖片上點擊以繪製多邊形。", + "name": { + "title": "名稱", + "inputPlaceHolder": "請輸入名稱…", + "tips": "這鏡頭名稱必須至少有2個字元,至少需包含一個字母,且不可與鏡頭或其他區域同名。" + }, + "inertia": { + "title": "慣性", + "desc": "指定物件需在區域內停留多少個畫格,才會被視為進入該區域。預設:3" + }, + "loiteringTime": { + "desc": "設定物件必須於區域內停留的最少秒數,以觸發動作。預設:0", + "title": "逗留時間" + }, + "objects": { + "title": "物件", + "desc": "此區域適用的物件列表。" + }, + "allObjects": "所有物件", + "speedEstimation": { + "title": "速度估算", + "desc": "啟用此區域內物件的速度估算。區域必須正好有4個點。", + "docs": "閱讀文件", + "lineCDistance": "C 線距離 ({{unit}})", + "lineBDistance": "B 線距離 ({{unit}})", + "lineADistance": "A 線距離 ({{unit}})", + "lineDDistance": "D 線距離 ({{unit}})" + }, + "speedThreshold": { + "title": "速度門檻 ({{unit}})", + "desc": "指定物件於此區域被視為有效時所需的最小速度。", + "toast": { + "error": { + "loiteringTimeError": "設有逗留時間大於0的區域,不應同時使用速度估算功能。", + "pointLengthError": "此區域已停用速度估算功能。啟用速度估算的區域必須正好有4個點。" + } + } + }, + "toast": { + "success": "區域({{zoneName}})已儲存。" + }, + "desc": { + "title": "區域可讓你定義畫面中的特定範圍,以判斷物件是否進入該範圍。", + "documentation": "文件" + }, + "documentTitle": "編輯區域 - Frigate" + }, + "motionMasks": { + "label": "移動遮罩", + "documentTitle": "編輯移動遮罩 - Frigate", + "desc": { + "title": "移動遮罩可防止不需要的移動觸發偵測。遮罩過多會令物件追蹤變得困難。", + "documentation": "文件" + }, + "edit": "編輯移動遮罩", + "point_other": "{{count}}個點", + "clickDrawPolygon": "在圖片上點擊以繪製多邊形。", + "polygonAreaTooLarge": { + "title": "移動遮罩覆蓋了鏡頭畫面{{polygonArea}}%。建議不要使用過大的遮罩。", + "tips": "移動遮罩無法阻止物件被偵測,應使用必要區域來限制範圍。", + "documentation": "閱讀文件" + }, + "context": { + "title": "移動遮罩用於防止某些不需要的移動(如樹枝晃動、鏡頭時間戳記)觸發偵測。移動遮罩應該非常謹慎使用,過度遮罩會令物件追蹤更加困難。", + "documentation": "閱讀文件" + }, + "add": "新增移動遮罩", + "toast": { + "success": { + "title": "{{polygonName}}已儲存。", + "noName": "移動遮罩已儲存。" + } + } + }, + "objectMasks": { + "label": "物件遮罩", + "desc": { + "documentation": "文件", + "title": "物件過濾遮罩根據位置,過濾指定物件類型的誤判偵測。" + }, + "add": "新增物件遮罩", + "context": "物件過濾遮罩根據位置,過濾指定物件類型的誤判偵測。", + "point_other": "{{count}}個點", + "clickDrawPolygon": "在圖片上點擊以繪製多邊形。", + "objects": { + "title": "物件", + "desc": "此物件遮罩適用的物件類型。", + "allObjectTypes": "所有物件類型" + }, + "toast": { + "success": { + "title": "{{polygonName}}已儲存。", + "noName": "物件遮罩已儲存。" + } + }, + "documentTitle": "編輯物件遮罩 - Frigate", + "edit": "編輯物件遮罩" + }, + "filter": { + "all": "所有遮罩與區域" + }, + "restart_required": "需要重新啟動(遮罩/區域已變更)", + "motionMaskLabel": "移動遮罩 {{number}}", + "objectMaskLabel": "物件遮罩 {{number}}({{label}}" + }, + "motionDetectionTuner": { + "title": "移動偵測調校器", + "desc": { + "title": "Frigate首先利用移動偵測作初步篩選,以判斷畫面中是否出現值得進行物件偵測的情況。", + "documentation": "閱讀移動調整指南" + }, + "Threshold": { + "title": "門檻", + "desc": "門檻值決定像素亮度變化多少才會被視為移動。預設:30" + }, + "contourArea": { + "title": "輪廓面積", + "desc": "輪廓面積值用來決定哪些變化像素群符合移動標準。預設:10" + }, + "improveContrast": { + "title": "改善對比度", + "desc": "改善黑暗場景的對比度。預設:開啟" + }, + "toast": { + "success": "移動設定已儲存。" + }, + "unsavedChanges": "{{camera}}的移動調校設定尚未儲存" + }, + "debug": { + "title": "除錯", + "objectList": "物件列表", + "noObjects": "沒有物件", + "boundingBoxes": { + "title": "邊框框線", + "colors": { + "info": "
  • 啟動時,系統會為每個物件標籤指派不同顏色
  • 深藍色幼線代表目前沒有偵測到該物件
  • 灰色幼線代表該物件被偵測為靜止狀態
  • 粗線代表該物件正被自動追蹤(若已啟用)
  • ", + "label": "物件邊框顏色" + }, + "desc": "顯示追蹤物件周圍的邊框" + }, + "zones": { + "title": "區域", + "desc": "顯示所有已定義區域的輪廓" + }, + "motion": { + "title": "移動方框", + "desc": "顯示偵測到移動的區域方框", + "tips": "

    移動方框

    畫面中偵測到移動的地方將會顯示亮紅色方框

    " + }, + "regions": { + "desc": "顯示屬於物件偵測器感興趣範圍的方框", + "title": "偵測區", + "tips": "

    偵測區方框

    畫面中屬於物件偵測器感興趣的地方將會顯示亮綠色方框,並且進行分析

    " + }, + "desc": "除錯畫面會即時顯示追蹤到的物件及統計資料。物件列表則顯示偵測到物件的延遲總結。", + "timestamp": { + "title": "時間戳記", + "desc": "在圖片上疊加時間戳記" + }, + "detectorDesc": "Frigate 使用你的偵測器({{detectors}})來偵測鏡頭影片串流中的物件。", + "debugging": "除錯中", + "mask": { + "title": "移動遮罩", + "desc": "顯示移動遮罩多邊形" + }, + "objectShapeFilterDrawing": { + "title": "物件形狀篩選繪圖", + "document": "閱讀文件 ", + "score": "分數", + "area": "面積", + "ratio": "比例", + "desc": "在圖片上畫矩形以查看面積與比例詳情", + "tips": "啟用此選項後,會於鏡頭畫面上繪製矩形,以顯示其面積及比例。這些數值可用於設定物件形狀過濾參數。" + }, + "openCameraWebUI": "打開 {{camera}} 的網頁介面", + "audio": { + "title": "音訊", + "noAudioDetections": "未偵測到音訊", + "score": "分數", + "currentRMS": "目前 RMS", + "currentdbFS": "目前 dbFS" + }, + "paths": { + "title": "軌跡", + "desc": "顯示追蹤物件軌跡上的重要點", + "tips": "

    軌跡


    線條同圓圈會標示追蹤物件整個生命周期中移動過的重要點。

    " + } + }, + "users": { + "management": { + "desc": "管理此Frigate個體的用戶帳戶。", + "title": "用戶管理" + }, + "addUser": "新增用戶", + "updatePassword": "重設密碼", + "toast": { + "success": { + "createUser": "成功建立用戶{{user}}", + "deleteUser": "成功刪除用戶{{user}}", + "roleUpdated": "成功更新{{user}}的角色", + "updatePassword": "成功更新密碼。" + }, + "error": { + "createUserFailed": "建立用戶失敗:{{errorMessage}}", + "roleUpdateFailed": "更新角色失敗:{{errorMessage}}", + "setPasswordFailed": "儲存密碼失敗:{{errorMessage}}", + "deleteUserFailed": "刪除用戶失敗:{{errorMessage}}" + } + }, + "table": { + "username": "用戶名稱", + "role": "角色", + "noUsers": "找不到用戶。", + "changeRole": "更改用戶角色", + "password": "重設密碼", + "deleteUser": "刪除用戶", + "actions": "操作" + }, + "dialog": { + "form": { + "user": { + "title": "用戶名稱", + "desc": "只允許使用字母、數字、句號及底線。", + "placeholder": "輸入用戶名稱" + }, + "password": { + "title": "密碼", + "placeholder": "輸入密碼", + "confirm": { + "placeholder": "確認密碼", + "title": "確認密碼" + }, + "strength": { + "title": "密碼強度: ", + "weak": "弱", + "medium": "中等", + "strong": "強", + "veryStrong": "非常強" + }, + "match": "密碼相符", + "notMatch": "密碼不相符", + "show": "顯示密碼", + "hide": "隱藏密碼", + "requirements": { + "title": "密碼要求:", + "length": "最少 12 個字元" + } + }, + "newPassword": { + "confirm": { + "placeholder": "重新輸入新密碼" + }, + "title": "新密碼", + "placeholder": "輸入新密碼" + }, + "usernameIsRequired": "必須輸入用戶名稱", + "passwordIsRequired": "必須填寫密碼", + "currentPassword": { + "title": "目前密碼", + "placeholder": "輸入目前密碼" + } + }, + "createUser": { + "title": "建立新用戶", + "desc": "新增用戶帳戶,並指定可存取Frigate介面各區域的角色。", + "usernameOnlyInclude": "用戶名稱只可包含字母、數字、句號或底線", + "confirmPassword": "請確認你的密碼" + }, + "deleteUser": { + "title": "刪除用戶", + "desc": "此操作無法還原,將會永久刪除用戶帳戶及所有相關資料。", + "warn": "確定要刪除{{username}}嗎?" + }, + "changeRole": { + "title": "更改用戶角色", + "desc": "更新{{username}}的權限", + "roleInfo": { + "intro": "為此用戶選擇合適的角色:", + "adminDesc": "可使用所有功能。", + "viewer": "觀看者", + "viewerDesc": "只限使用即時儀表板、審查、瀏覽及匯出功能。", + "admin": "管理員", + "customDesc": "自訂角色,具特定鏡頭存取權限。" + }, + "select": "選擇角色" + }, + "passwordSetting": { + "setPassword": "設定密碼", + "updatePassword": "更新{{username}}的密碼", + "desc": "建立強密碼以保障此帳戶安全。", + "cannotBeEmpty": "密碼不能留空", + "doNotMatch": "密碼不相符", + "currentPasswordRequired": "必須輸入目前密碼", + "incorrectCurrentPassword": "目前密碼不正確", + "passwordVerificationFailed": "驗證密碼失敗", + "multiDeviceWarning": "其他已登入裝置需於 {{refresh_time}} 內重新登入。", + "multiDeviceAdmin": "亦可更換 JWT 密鑰以強制所有使用者重新驗證。" + } + }, + "title": "用戶" + }, + "notification": { + "title": "通知", + "notificationSettings": { + "title": "通知設定", + "desc": "Frigate可原生向你的裝置推送通知,無論在瀏覽器中運行或安裝為PWA。", + "documentation": "閱讀文件" + }, + "notificationUnavailable": { + "title": "無法使用通知功能", + "desc": "網頁推送通知需在安全環境下運作(https://…),這是瀏覽器的限制。請透過安全連線存取Frigate以使用通知功能。", + "documentation": "閱讀文件" + }, + "globalSettings": { + "title": "全域設定", + "desc": "暫停所有已註冊裝置上特定鏡頭的通知。" + }, + "email": { + "placeholder": "例如:example@email.com", + "desc": "需要提供有效的電郵地址,若推送服務出現問題,將會透過此地址通知你。", + "title": "電郵地址" + }, + "cameras": { + "title": "鏡頭", + "noCameras": "沒有可用鏡頭", + "desc": "選擇啟用通知功能的鏡頭。" + }, + "deviceSpecific": "裝置專屬設定", + "registerDevice": "登記此裝置", + "unregisterDevice": "取消登記此裝置", + "sendTestNotification": "發送測試通知", + "active": "通知功能已啟用", + "suspended": "通知功能已暫停{{time}}", + "suspendTime": { + "10minutes": "暫停10分鐘", + "12hours": "暫停12小時", + "30minutes": "暫停30分鐘", + "24hours": "暫停24小時", + "5minutes": "暫停5分鐘", + "1hour": "暫停1小時", + "untilRestart": "暫停至重新啟動", + "suspend": "暫停" + }, + "toast": { + "error": { + "registerFailed": "儲存通知登記失敗。" + }, + "success": { + "registered": "成功登記通知功能。必須重新啟動Frigate後,才能發送任何通知(包括測試通知)。", + "settingSaved": "通知設定已儲存。" + } + }, + "cancelSuspension": "取消暫停", + "unsavedRegistrations": "通知註冊尚未儲存", + "unsavedChanges": "通知設定更改尚未儲存" + }, + "frigatePlus": { + "title": "Frigate+設定", + "apiKey": { + "title": "Frigate+ API金鑰", + "validated": "已偵測並驗證Frigate+ API金鑰", + "notValidated": "未偵測到Frigate+ API金鑰或驗證失敗", + "desc": "Frigate+ API金鑰可啟用與Frigate+服務的整合功能。", + "plusLink": "了解更多Frigate+資料" + }, + "snapshotConfig": { + "title": "快照設定", + "documentation": "閱讀文件", + "table": { + "camera": "鏡頭", + "snapshots": "快照", + "cleanCopySnapshots": "clean_copy 快照" + }, + "cleanCopyWarning": "部分鏡頭已啟用快照,但未啟用乾淨副本。你必須於快照設定中啟用clean_copy,才能從這些鏡頭提交影像至Frigate+。", + "desc": "提交至Frigate+需要在設定中同時啟用快照及clean_copy快照功能。" + }, + "modelInfo": { + "title": "模型資料", + "modelType": "模型類型", + "trainDate": "訓練日期", + "baseModel": "基礎模型", + "plusModelType": { + "baseModel": "基礎模型", + "userModel": "微調" + }, + "supportedDetectors": "支援的偵測器", + "cameras": "鏡頭", + "availableModels": "可用模型", + "loadingAvailableModels": "正在載入可用模型…", + "modelSelect": "可於此選擇你在Frigate+上的可用模型。請注意,只能選擇與當前偵測器設定相容的模型。", + "loading": "正在載入模型資料…", + "error": "載入模型資料失敗" + }, + "toast": { + "error": "儲存設定變更失敗:{{errorMessage}}", + "success": "Frigate+設定已儲存。請重新啟動Frigate以套用更改。" + }, + "restart_required": "需要重新啟動(已更改Frigate+模型)", + "unsavedChanges": "Frigate+ 設定更改尚未儲存" + }, + "enrichments": { + "faceRecognition": { + "modelSize": { + "large": { + "title": "大型", + "desc": "使用大型模型會採用 ArcFace 臉部嵌入模型,並在適用情況下自動於 GPU 上運行。" + }, + "desc": "用於人臉識別的模型大小。", + "label": "模型大小", + "small": { + "title": "小型", + "desc": "使用小型模型會採用 FaceNet 臉部嵌入模型,在大多數 CPU 上能有效運行。" + } + }, + "desc": "人臉識別功能允許為人物分配名字,當辨識到他們的臉孔時,Frigate 會將名字加到子標籤上。此資訊會顯示於介面、篩選器及通知中。", + "title": "人臉識別", + "readTheDocumentation": "閱讀文件" + }, + "birdClassification": { + "title": "鳥類分類", + "desc": "鳥類分類會使用量化 Tensorflow 模型識別已知鳥類。當辨識到已知鳥類時,牠的常見名稱會加到子標籤上。此資訊會顯示在介面、篩選器及通知中。" + }, + "semanticSearch": { + "desc": "Frigate 的語意搜尋功能讓你可以利用影像本身、自訂文字描述,或自動產生的描述,在審查項目中尋找已追蹤的物件。", + "reindexNow": { + "confirmDesc": "你確定要重建索引所有已追蹤物件的嵌入向量嗎?這個過程會在背景運行,但可能會用盡你的 CPU,而且需要一定時間。你可以在「瀏覽」頁面查看進度。", + "label": "立即重建索引", + "desc": "重建索引會為所有已追蹤物件重新生成嵌入向量。這個過程會在背景運行,可能會用盡你的 CPU,所需時間取決於已追蹤物件的數量。", + "confirmTitle": "確認重建索引", + "confirmButton": "重建索引", + "success": "重建索引已成功開始。", + "alreadyInProgress": "重建索引已在進行中。", + "error": "啟動重建索引失敗:{{errorMessage}}" + }, + "title": "語意搜尋", + "readTheDocumentation": "閱讀文件", + "modelSize": { + "label": "模型大小", + "desc": "用於語意搜尋的模型大小。", + "small": { + "title": "小型", + "desc": "使用小型模型會採用量化版本,較少佔用 RAM,在 CPU 上運行更快,而嵌入品質的差異非常細微。" + }, + "large": { + "title": "大型", + "desc": "使用大型模型會採用完整的 Jina 模型,並在適用情況下自動於 GPU 上運行。" + } + } + }, + "licensePlateRecognition": { + "title": "車牌識別", + "desc": "Frigate 可以識別車輛上的車牌,自動將偵測到的字元加到已辨識車牌欄位,或將已知名稱加到屬於車輛類型的物件的子標籤上。常見用途包括讀取駛入車道或在街道上駛過的車輛的車牌。", + "readTheDocumentation": "閱讀文件" + }, + "title": "進階功能設定", + "unsavedChanges": "未儲存進階功能設定變更", + "restart_required": "需要重新啟動(進階功能設定已變更)", + "toast": { + "success": "進階功能設定已儲存。請重新啟動 Frigate 以套用你的更改。", + "error": "儲存設定變更失敗:{{errorMessage}}" + } + }, + "roles": { + "management": { + "title": "觀察者角色管理", + "desc": "管理自訂觀察者角色及其對此 Frigate 實例的鏡頭存取權限。" + }, + "addRole": "新增角色", + "table": { + "role": "角色", + "cameras": "鏡頭", + "actions": "操作", + "noRoles": "未找到自訂角色。", + "editCameras": "編輯鏡頭", + "deleteRole": "刪除角色" + }, + "toast": { + "success": { + "createRole": "角色 {{role}} 已成功建立", + "updateCameras": "角色 {{role}} 的鏡頭已更新", + "deleteRole": "角色 {{role}} 已成功刪除", + "userRolesUpdated_other": "{{count}} 位使用者被更新為「觀察者」角色,將可存取所有鏡頭。" + }, + "error": { + "createRoleFailed": "建立角色失敗:{{errorMessage}}", + "updateCamerasFailed": "更新鏡頭失敗:{{errorMessage}}", + "deleteRoleFailed": "刪除角色失敗:{{errorMessage}}", + "userUpdateFailed": "更新使用者角色失敗:{{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "建立新角色", + "desc": "新增角色,並指定鏡頭存取權限。" + }, + "editCameras": { + "title": "編輯角色鏡頭", + "desc": "更新角色 {{role}} 的鏡頭存取權限。" + }, + "deleteRole": { + "title": "刪除角色", + "desc": "此操作無法復原。將永久刪除該角色,並將使用此角色的所有使用者改為「觀察者」角色,可存取所有鏡頭。", + "warn": "你確定要刪除 {{role}} 嗎?", + "deleting": "正在刪除…" + }, + "form": { + "role": { + "title": "角色名稱", + "placeholder": "輸入角色名稱", + "desc": "只允許字母、數字、句號或底線。", + "roleIsRequired": "必須填寫角色名稱", + "roleOnlyInclude": "角色名稱只可包含字母、數字、句號或底線", + "roleExists": "已有相同名稱的角色存在。" + }, + "cameras": { + "title": "鏡頭", + "desc": "選擇此角色可存取的鏡頭。至少需要選擇一個鏡頭。", + "required": "至少需要選擇一個鏡頭。" + } + } + } + }, + "triggers": { + "documentTitle": "觸發器", + "semanticSearch": { + "title": "語意搜尋已停用", + "desc": "必須啟用語意搜尋才能使用觸發器。" + }, + "management": { + "title": "觸發器", + "desc": "管理 {{camera}} 的觸發器。使用縮圖類型可對與所選追蹤物件相似的縮圖觸發,使用描述類型可對與你指定文字描述相似的事件觸發。" + }, + "addTrigger": "新增觸發器", + "table": { + "name": "名稱", + "type": "類型", + "content": "內容", + "threshold": "閾值", + "actions": "操作", + "noTriggers": "此鏡頭尚未設定任何觸發器。", + "edit": "編輯", + "deleteTrigger": "刪除觸發器", + "lastTriggered": "上次觸發" + }, + "type": { + "thumbnail": "縮圖", + "description": "描述" + }, + "actions": { + "alert": "標記為警報", + "notification": "發送通知", + "sub_label": "新增子標籤", + "attribute": "新增屬性" + }, + "dialog": { + "createTrigger": { + "title": "建立觸發器", + "desc": "為鏡頭 {{camera}} 建立觸發器" + }, + "editTrigger": { + "title": "編輯觸發器", + "desc": "編輯鏡頭 {{camera}} 的觸發器設定" + }, + "deleteTrigger": { + "title": "刪除觸發器", + "desc": "你確定要刪除觸發器 {{triggerName}} 嗎?此操作無法復原。" + }, + "form": { + "name": { + "title": "名稱", + "placeholder": "為觸發器命名", + "error": { + "minLength": "欄位至少需 2 個字元。", + "invalidCharacters": "欄位只可包含字母、數字、底線及連字符。", + "alreadyExists": "此鏡頭已有相同名稱的觸發器。" + }, + "description": "輸入唯一名稱或描述以識別此觸發器" + }, + "enabled": { + "description": "啟用或停用此觸發器" + }, + "type": { + "title": "類型", + "placeholder": "選擇觸發器類型", + "description": "偵測到相似物件描述時觸發", + "thumbnail": "偵測到相似縮圖時觸發" + }, + "friendly_name": { + "title": "顯示名稱", + "placeholder": "為此觸發器命名或描述", + "description": "此觸發器的可選顯示名稱或描述文字。" + }, + "content": { + "title": "內容", + "imagePlaceholder": "選擇縮圖", + "textPlaceholder": "輸入文字內容", + "imageDesc": "只顯示最近100張縮圖。如果你找不到所需的縮圖,請在「瀏覽」中查看先前的物件,並從選單中設定觸發器。", + "textDesc": "輸入文字,當偵測到相似追蹤物件描述時觸發此動作。", + "error": { + "required": "必須提供內容。" + } + }, + "threshold": { + "title": "閾值", + "error": { + "min": "閾值至少為 0", + "max": "閾值最多為 1" + }, + "desc": "為觸發器設定相似度門檻,越高越嚴格。" + }, + "actions": { + "title": "操作", + "desc": "預設情況下,Frigate 會對所有觸發器發送 MQTT 訊息。子標籤會將觸發器名稱加入到物件標籤中。屬性是可搜尋的元數據,單獨儲存在被追蹤對象的元數據中。", + "error": { + "min": "至少需要選擇一個操作。" + } + } + } + }, + "toast": { + "success": { + "createTrigger": "觸發器 {{name}} 已成功建立。", + "updateTrigger": "觸發器 {{name}} 已成功更新。", + "deleteTrigger": "觸發器 {{name}} 已成功刪除。" + }, + "error": { + "createTriggerFailed": "建立觸發器失敗:{{errorMessage}}", + "updateTriggerFailed": "更新觸發器失敗:{{errorMessage}}", + "deleteTriggerFailed": "刪除觸發器失敗:{{errorMessage}}" + } + }, + "wizard": { + "title": "建立觸發器", + "step1": { + "description": "設定觸發器基本參數。" + }, + "step2": { + "description": "設定觸發內容。" + }, + "step3": { + "description": "設定觸發器門檻與動作。" + }, + "steps": { + "nameAndType": "名稱與類型", + "configureData": "設定資料", + "thresholdAndActions": "門檻與動作" + } + } + }, + "cameraWizard": { + "title": "新增鏡頭", + "description": "請依照以下步驟,將新鏡頭加入 Frigate。", + "steps": { + "nameAndConnection": "名稱與連線", + "streamConfiguration": "串流設定", + "validationAndTesting": "驗證與測試", + "probeOrSnapshot": "探測或快照" + }, + "save": { + "success": "已成功儲存新鏡頭 {{cameraName}}。", + "failure": "儲存 {{cameraName}} 時發生錯誤。" + }, + "testResultLabels": { + "resolution": "解析度", + "video": "影像", + "audio": "音訊", + "fps": "每秒影格數" + }, + "commonErrors": { + "noUrl": "請輸入有效的串流網址", + "testFailed": "串流測試失敗:{{error}}" + }, + "step1": { + "description": "輸入鏡頭詳細資料並選擇探測鏡頭或手動選擇品牌。", + "cameraName": "鏡頭名稱", + "cameraNamePlaceholder": "例如:front_door 或 back_yard_overview", + "host": "主機名稱/IP 位址", + "port": "連接埠", + "username": "用戶名稱", + "usernamePlaceholder": "可選", + "password": "密碼", + "passwordPlaceholder": "選擇傳輸協定", + "selectTransport": "選擇傳輸協定", + "cameraBrand": "鏡頭品牌", + "selectBrand": "選擇鏡頭品牌以套用 URL 模板", + "customUrl": "自訂串流網址", + "brandInformation": "品牌資訊", + "brandUrlFormat": "適用於 RTSP 網址格式如下的鏡頭:{{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "testConnection": "測試連線", + "testSuccess": "連線測試成功!", + "testFailed": "連線測試失敗,請檢查輸入內容後再試一次。", + "streamDetails": "串流詳情", + "warnings": { + "noSnapshot": "無法從設定的串流中擷取快照。" + }, + "errors": { + "brandOrCustomUrlRequired": "請選擇包含主機/IP 的鏡頭品牌,或選擇「其他」並輸入自訂網址", + "nameRequired": "必須輸入鏡頭名稱", + "nameLength": "鏡頭名稱長度不得超過 64 個字元", + "invalidCharacters": "鏡頭名稱包含無效字元", + "nameExists": "鏡頭名稱已存在", + "brands": { + "reolink-rtsp": "不建議使用 Reolink RTSP。建議在鏡頭設定中啟用 HTTP,並重新啟動鏡頭設定精靈。" + }, + "customUrlRtspRequired": "自訂 URL 必須以「rtsp://」開頭。非 RTSP 串流需手動設定。" + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "connectionSettings": "連線設定", + "detectionMethod": "串流偵測方式", + "onvifPort": "ONVIF 連接埠", + "probeMode": "探測鏡頭", + "manualMode": "手動選擇", + "detectionMethodDescription": "使用 ONVIF(如支援)探測鏡頭以取得串流 URL,或手動選擇鏡頭品牌以使用預設 URL。若要輸入自訂 RTSP URL,請選擇手動方式並選「其他」。", + "onvifPortDescription": "支援 ONVIF 的鏡頭通常為 80 或 8080。", + "useDigestAuth": "使用摘要驗證", + "useDigestAuthDescription": "對 ONVIF 使用 HTTP 摘要驗證。部分鏡頭可能需要專用的 ONVIF 帳號密碼。" + }, + "step2": { + "description": "根據你所選擇的偵測方法,探測鏡頭是否有用串流,或者設定手動設定。", + "streamsTitle": "鏡頭串流", + "addStream": "新增串流", + "addAnotherStream": "新增另一個串流", + "streamTitle": "串流 {{number}}", + "streamUrl": "串流網址", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "url": "網址", + "resolution": "解析度", + "selectResolution": "選擇解析度", + "quality": "畫質", + "selectQuality": "選擇畫質", + "roles": "角色", + "roleLabels": { + "detect": "物件偵測", + "record": "錄影", + "audio": "音訊" + }, + "testStream": "測試連線", + "testSuccess": "連線測試成功!", + "testFailed": "連線測試失敗。請檢查你的輸入並重試。", + "testFailedTitle": "測試失敗", + "connected": "已連線", + "notConnected": "未連線", + "featuresTitle": "功能", + "go2rtc": "減少與鏡頭的連線數", + "detectRoleWarning": "至少需有一個串流設定為「偵測」角色才能繼續。", + "rolesPopover": { + "title": "串流角色", + "detect": "用於物件偵測的主要影像來源。", + "record": "根據設定儲存影片片段。", + "audio": "用於音訊偵測的來源。" + }, + "featuresPopover": { + "title": "串流功能", + "description": "使用 go2rtc 轉串流以減少與鏡頭的直接連線。" + }, + "streamDetails": "串流詳情", + "probing": "正在探測鏡頭…", + "retry": "重試", + "testing": { + "probingMetadata": "正在探測鏡頭中繼資料…", + "fetchingSnapshot": "正在取得鏡頭快照…" + }, + "probeFailed": "探測鏡頭失敗:{{error}}", + "probingDevice": "正在探測裝置…", + "probeSuccessful": "探測成功", + "probeError": "探測錯誤", + "probeNoSuccess": "探測失敗", + "deviceInfo": "裝置資訊", + "manufacturer": "製造商", + "model": "型號", + "firmware": "韌體", + "profiles": "設定檔", + "ptzSupport": "支援 PTZ", + "autotrackingSupport": "支援自動追蹤", + "presets": "預設位置", + "rtspCandidates": "RTSP 候選", + "rtspCandidatesDescription": "已從鏡頭探測到以下 RTSP URL。測試連線以查看串流中繼資料。", + "noRtspCandidates": "未從鏡頭找到 RTSP URL,可能憑證錯誤或不支援 ONVIF,請手動輸入。", + "candidateStreamTitle": "候選 {{number}}", + "useCandidate": "使用", + "uriCopy": "複製", + "uriCopied": "URI 已複製到剪貼簿", + "testConnection": "測試連線", + "toggleUriView": "點擊切換完整 URI 顯示", + "errors": { + "hostRequired": "必須輸入主機或 IP 位址" + } + }, + "step3": { + "description": "設定串流角色,並為鏡頭新增其他串流。", + "validationTitle": "串流驗證", + "connectAllStreams": "連線所有串流", + "reconnectionSuccess": "重新連線成功。", + "reconnectionPartial": "部分串流重新連線失敗。", + "streamUnavailable": "無法預覽串流", + "reload": "重新載入", + "connecting": "正在連線...", + "streamTitle": "串流 {{number}}", + "valid": "有效", + "failed": "失敗", + "notTested": "未測試", + "connectStream": "連線", + "connectingStream": "連線中", + "disconnectStream": "中斷連線", + "estimatedBandwidth": "預計頻寬", + "roles": "角色", + "none": "無", + "error": "錯誤", + "streamValidated": "串流 {{number}} 驗證成功", + "streamValidationFailed": "串流 {{number}} 驗證失敗", + "saveAndApply": "儲存新鏡頭", + "saveError": "設定無效,請檢查你的設定。", + "issues": { + "title": "串流驗證", + "videoCodecGood": "影片編碼格式為 {{codec}}。", + "audioCodecGood": "音訊編碼格式為 {{codec}}。", + "noAudioWarning": "此串流未偵測到音訊,錄影將不會有聲音。", + "audioCodecRecordError": "錄影要支援音訊,必須使用 AAC 編碼。", + "audioCodecRequired": "要支援音訊偵測,必須有音訊串流。", + "restreamingWarning": "若減少錄影串流與鏡頭的連線,CPU 使用率可能會略微增加。", + "dahua": { + "substreamWarning": "子串流 1 被鎖定為低解析度。許多 Dahua / Amcrest / EmpireTech 鏡頭支援額外子串流,需要在鏡頭設定中啟用。建議如有可用,檢查並使用這些子串流。" + }, + "hikvision": { + "substreamWarning": "子串流 1 被鎖定為低解析度。許多 Hikvision 鏡頭支援額外子串流,需要在鏡頭設定中啟用。建議如有可用,檢查並使用這些子串流。" + } + }, + "streamsTitle": "鏡頭串流", + "addStream": "新增串流", + "addAnotherStream": "新增另一個串流", + "streamUrl": "串流 URL", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "selectStream": "選擇串流", + "searchCandidates": "搜尋候選…", + "noStreamFound": "找不到串流", + "url": "URL", + "resolution": "解析度", + "selectResolution": "選擇解析度", + "quality": "畫質", + "selectQuality": "選擇畫質", + "roleLabels": { + "detect": "物件偵測", + "record": "錄影", + "audio": "音訊" + }, + "testStream": "測試連線", + "testSuccess": "串流測試成功!", + "testFailed": "串流測試失敗", + "testFailedTitle": "測試失敗", + "connected": "已連線", + "notConnected": "未連線", + "featuresTitle": "功能", + "go2rtc": "減少連線至鏡頭", + "detectRoleWarning": "至少一個串流需設定為「detect」角色。", + "rolesPopover": { + "title": "串流角色", + "detect": "物件偵測主要來源。", + "record": "依設定儲存影片片段。", + "audio": "音訊偵測來源。" + }, + "featuresPopover": { + "title": "串流功能", + "description": "使用 go2rtc 轉串流以減少鏡頭連線。" + } + }, + "step4": { + "description": "儲存鏡頭前進行最終驗證與分析,請先連接所有串流。", + "validationTitle": "串流驗證", + "connectAllStreams": "連接所有串流", + "reconnectionSuccess": "重新連線成功。", + "reconnectionPartial": "部分串流重新連線失敗。", + "streamUnavailable": "無法預覽串流", + "reload": "重新載入", + "connecting": "連線中…", + "streamTitle": "串流 {{number}}", + "valid": "有效", + "failed": "失敗", + "notTested": "未測試", + "connectStream": "連線", + "connectingStream": "連線中", + "disconnectStream": "中斷連線", + "estimatedBandwidth": "預計頻寬", + "roles": "角色", + "ffmpegModule": "使用串流相容模式", + "ffmpegModuleDescription": "若多次嘗試仍無法載入,建議啟用。啟用後,Frigate 將使用 ffmpeg 模組和 go2rtc。這可能會提高與某些鏡頭串流相容性。", + "none": "無", + "error": "錯誤", + "streamValidated": "串流 {{number}} 驗證成功", + "streamValidationFailed": "串流 {{number}} 驗證失敗", + "saveAndApply": "儲存新鏡頭", + "saveError": "設定無效,請檢查。", + "issues": { + "title": "串流驗證", + "videoCodecGood": "影片編碼為 {{codec}}。", + "audioCodecGood": "音訊編碼為 {{codec}}。", + "resolutionHigh": "此解析度{{resolution}} 可能增加資源使用。", + "resolutionLow": "此解析度{{resolution}}可能過低,不利小物件偵測。", + "noAudioWarning": "未偵測到音訊,錄影將沒有聲音。", + "audioCodecRecordError": "錄影需 AAC 音訊編碼。", + "audioCodecRequired": "音訊偵測需音訊串流。", + "restreamingWarning": "減少錄影串流連線可能略增 CPU 使用。", + "brands": { + "reolink-rtsp": "不建議使用 Reolink RTSP,請啟用 HTTP 並重新啟動精靈。", + "reolink-http": "Reolink HTTP 串流建議使用 FFmpeg,請啟用相容模式。" + }, + "dahua": { + "substreamWarning": "子串流 1 解析度過低。許多Dahua / Amcrest / EmpireTech鏡頭支援額外的子串流,需要在鏡頭的設定中啟用。建議於鏡頭設定啟用更多子串流。" + }, + "hikvision": { + "substreamWarning": "子串流 1 解析度過低。許多Hikvision鏡頭支援額外的子串流,需要在鏡頭的設定中啟用。建議於鏡頭設定啟用更多子串流。" + } + } + } + }, + "cameraManagement": { + "title": "管理鏡頭", + "addCamera": "新增鏡頭", + "editCamera": "編輯鏡頭:", + "selectCamera": "選擇鏡頭", + "backToSettings": "返回鏡頭設定", + "streams": { + "title": "啟用/停用鏡頭", + "desc": "暫時停用鏡頭,直到 Frigate 重新啟動。停用鏡頭會完全停止 Frigate 對該鏡頭串流的處理。偵測、錄影及除錯功能將無法使用。
    注意:這不會停用 go2rtc 轉串流。" + }, + "cameraConfig": { + "add": "新增鏡頭", + "edit": "編輯鏡頭", + "description": "設定鏡頭,包括串流輸入與角色。", + "name": "鏡頭名稱", + "nameRequired": "必須輸入鏡頭名稱", + "nameLength": "鏡頭名稱長度不得超過 64 個字元。", + "namePlaceholder": "例如:front_door 或 back_yard_overview", + "enabled": "已啟用", + "ffmpeg": { + "inputs": "輸入串流", + "path": "串流路徑", + "pathRequired": "必須提供串流路徑", + "pathPlaceholder": "rtsp://...", + "roles": "角色", + "rolesRequired": "至少需要一個角色", + "rolesUnique": "每個角色(音訊 / 偵測 / 錄影)只能分配給一個串流", + "addInput": "新增輸入串流", + "removeInput": "移除輸入串流", + "inputsRequired": "至少需要一個輸入串流" + }, + "go2rtcStreams": "go2rtc 串流", + "streamUrls": "串流網址", + "addUrl": "新增網址", + "addGo2rtcStream": "新增 go2rtc 串流", + "toast": { + "success": "鏡頭 {{cameraName}} 已成功儲存" + } + } + }, + "cameraReview": { + "title": "鏡頭檢視設定", + "object_descriptions": { + "title": "生成式 AI 物件描述", + "desc": "暫時啟用/停用此鏡頭的生成式 AI 物件描述直到Frigate重新啟動。停用時,系統不會為此鏡頭的追蹤物件生成 AI 描述。" + }, + "review_descriptions": { + "title": "生成式 AI 審查描述", + "desc": "暫時啟用/停用此鏡頭的生成式 AI 審查描述直到Frigate重新啟動。停用時,系統不會為此鏡頭的審查項目生成 AI 描述。" + }, + "review": { + "title": "審查", + "desc": "暫時啟用/停用此鏡頭的警報與偵測,直到 Frigate 重啟。停用時,不會產生新的審查項目。 ", + "alerts": "警報 ", + "detections": "偵測 " + }, + "reviewClassification": { + "title": "審查分類", + "desc": "Frigate 將審查項目分類為警報與偵測。預設情況下,所有 personcar 物件會視為警報。你可以透過設定對應區域來精確分類審查項目。", + "noDefinedZones": "此鏡頭未定義任何區域。", + "objectAlertsTips": "在{{cameraName}}上所有{{alertsLabels}}物件將會顯示為警報。", + "zoneObjectAlertsTips": "在{{cameraName}}的{{zone}}區域偵測到的所有{{alertsLabels}}物件將會顯示為警報。", + "objectDetectionsTips": "無論位於哪個區域,在{{cameraName}}上所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。", + "zoneObjectDetectionsTips": { + "text": "在{{cameraName}}的{{zone}}區域內所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。", + "notSelectDetections": "無論位於哪個區域,在{{cameraName}}的{{zone}}區域偵測到、但未分類為警報的{{detectionsLabels}}物件將會顯示為偵測結果。", + "regardlessOfZoneObjectDetectionsTips": "無論位於哪個區域,在{{cameraName}}上所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。" + }, + "unsavedChanges": "{{camera}}的審查分類設定尚未儲存", + "selectAlertsZones": "選擇警報的區域", + "selectDetectionsZones": "選擇偵測的區域", + "limitDetections": "限制偵測至特定區域", + "toast": { + "success": "審查分類設定已儲存。請重新啟動Frigate以套用更改。" + } + } + } +} diff --git a/web/public/locales/yue-Hant/views/system.json b/web/public/locales/yue-Hant/views/system.json new file mode 100644 index 0000000..8f578d5 --- /dev/null +++ b/web/public/locales/yue-Hant/views/system.json @@ -0,0 +1,221 @@ +{ + "documentTitle": { + "cameras": "鏡頭統計 - Frigate", + "storage": "儲存裝置統計 - Frigate", + "general": "一般統計 - Frigate", + "enrichments": "進階功能統計 - Frigate", + "logs": { + "frigate": "Frigate 日誌 - Frigate", + "nginx": "Nginx 日誌 - Frigate", + "go2rtc": "Go2RTC 日誌 - Frigate" + } + }, + "title": "系統", + "metrics": "系統指標", + "logs": { + "download": { + "label": "下載日誌" + }, + "type": { + "timestamp": "時間戳記", + "tag": "標籤", + "message": "訊息", + "label": "類型" + }, + "tips": "正在從伺服器串流日誌", + "toast": { + "error": { + "fetchingLogsFailed": "擷取日誌時出錯:{{errorMessage}}", + "whileStreamingLogs": "串流日誌時出錯:{{errorMessage}}" + } + }, + "copy": { + "error": "無法將日誌複製到剪貼簿", + "label": "複製到剪貼簿", + "success": "已將日誌複製到剪貼簿" + } + }, + "general": { + "detector": { + "inferenceSpeed": "偵測器推理速度", + "memoryUsage": "偵測器記憶體使用量", + "title": "偵測器", + "cpuUsage": "偵測器 CPU 使用率", + "temperature": "偵測器溫度", + "cpuUsageInformation": "CPU 用於準備偵測模型的輸入同輸出數據。此數值不計算推理運算,即使使用 GPU 或加速器也是一樣。" + }, + "hardwareInfo": { + "gpuUsage": "GPU 使用率", + "gpuInfo": { + "vainfoOutput": { + "processOutput": "程序輸出:", + "processError": "程序錯誤:", + "title": "Vainfo 輸出", + "returnCode": "返回代碼:{{code}}" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI 輸出", + "vbios": "VBios 資訊:{{vbios}}", + "cudaComputerCapability": "CUDA 計算能力:{{cuda_compute}}", + "name": "名稱:{{name}}", + "driver": "驅動程式:{{driver}}" + }, + "closeInfo": { + "label": "關閉 GPU 資訊" + }, + "toast": { + "success": "已將 GPU 資訊複製到剪貼簿" + }, + "copyInfo": { + "label": "複製 GPU 資訊" + } + }, + "title": "硬件資訊", + "npuUsage": "NPU 使用率", + "gpuMemory": "GPU 記憶體", + "gpuEncoder": "GPU 編碼器", + "gpuDecoder": "GPU 解碼器", + "npuMemory": "NPU 記憶體", + "intelGpuWarning": { + "title": "Intel GPU 狀態警告", + "message": "GPU 狀態不可用", + "description": "這是 Intel GPU 統計工具已知問題,可能顯示 0% 使用率,但不影響效能。可重新啟動主機暫時修復。" + }, + "gpuTemperature": "GPU 溫度", + "npuTemperature": "NPU 溫度" + }, + "otherProcesses": { + "title": "其他程序", + "processCpuUsage": "程序 CPU 使用率", + "processMemoryUsage": "程序記憶體使用量", + "series": { + "go2rtc": "go2rtc", + "recording": "錄影", + "review_segment": "檢視片段", + "embeddings": "嵌入向量", + "audio_detector": "音訊偵測器" + } + }, + "title": "一般" + }, + "storage": { + "title": "儲存裝置", + "overview": "概覽", + "recordings": { + "title": "錄影檔案", + "earliestRecording": "最早可用錄影檔案:", + "tips": "此數值代表 Frigate 資料庫中錄影檔案的總儲存使用量。Frigate 不會追蹤磁碟上所有檔案的儲存使用量。" + }, + "cameraStorage": { + "camera": "鏡頭", + "unusedStorageInformation": "未使用儲存資訊", + "storageUsed": "已使用儲存", + "bandwidth": "每小時使用量", + "unused": { + "tips": "若您的磁碟中存有其他檔案,該數值可能無法準確反映 Frigate 可用的空間。Frigate 只追蹤其錄影檔案的儲存使用量。", + "title": "未使用" + }, + "title": "鏡頭儲存", + "percentageOfTotalUsed": "佔總量百分比" + }, + "shm": { + "title": "SHM(共享記憶體) 分配", + "warning": "目前 SHM 大小 {{total}}MB 太小,請增加至至少 {{min_shm}}MB。" + } + }, + "cameras": { + "info": { + "streamDataFromFFPROBE": "串流資料是透過 ffprobe 取得。", + "fetching": "正在取得鏡頭資料", + "video": "影片:", + "codec": "編碼器:", + "resolution": "解像度:", + "tips": { + "title": "鏡頭詳細資訊" + }, + "stream": "串流 {{idx}}", + "audio": "音訊:", + "fps": "每秒影格數 (FPS):", + "unknown": "未知", + "error": "錯誤:{{error}}", + "cameraProbeInfo": "{{camera}} 鏡頭詳細資訊", + "aspectRatio": "長寬比" + }, + "framesAndDetections": "畫面 / 偵測", + "label": { + "camera": "鏡頭", + "detect": "偵測", + "skipped": "略過", + "ffmpeg": "FFmpeg", + "capture": "讀取影像", + "overallFramesPerSecond": "整體每秒畫面數", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraDetect": "{{camName}} 偵測", + "cameraFramesPerSecond": "{{camName}} 每秒畫面數", + "cameraDetectionsPerSecond": "{{camName}} 每秒偵測次數", + "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒略過偵測次數", + "overallSkippedDetectionsPerSecond": "整體每秒略過偵測次數", + "cameraCapture": "{{camName}} 讀取影像", + "overallDetectionsPerSecond": "整體每秒偵測次數" + }, + "title": "鏡頭", + "overview": "概覽", + "toast": { + "success": { + "copyToClipboard": "已將鏡頭資料複製到剪貼簿。" + }, + "error": { + "unableToProbeCamera": "無法取得鏡頭資料:{{errorMessage}}" + } + }, + "connectionQuality": { + "title": "連線品質", + "excellent": "極佳", + "fair": "一般", + "poor": "差", + "unusable": "無法使用", + "fps": "每秒幀數", + "expectedFps": "預期每秒幀數", + "reconnectsLastHour": "重新連線次數(過去一小時)", + "stallsLastHour": "卡頓次數(過去一小時)" + } + }, + "lastRefreshed": "最後更新: ", + "stats": { + "detectIsSlow": "{{detect}} 偵測速度慢 ({{speed}} 毫秒)", + "detectIsVerySlow": "{{detect}} 偵測速度非常慢 ({{speed}} 毫秒)", + "cameraIsOffline": "{{camera}} 已離線", + "detectHighCpuUsage": "{{camera}} 的偵測 CPU 使用率過高 ({{detectAvg}}%)", + "healthy": "系統運作正常", + "ffmpegHighCpuUsage": "{{camera}} 的 FFmpeg CPU 使用率過高 ({{ffmpegAvg}}%)", + "reindexingEmbeddings": "重新索引嵌入資料 (已完成 {{processed}}%)", + "shmTooLow": "/dev/shm 分配({{total}} MB)太小,請增加至至少 {{min}} MB。" + }, + "enrichments": { + "title": "進階功能", + "infPerSecond": "每秒推理次數", + "embeddings": { + "image_embedding": "圖片嵌入", + "face_embedding_speed": "人臉嵌入速度", + "face_recognition_speed": "人臉辨識速度", + "plate_recognition_speed": "車牌辨識速度", + "face_recognition": "人臉辨識", + "text_embedding": "文字嵌入", + "yolov9_plate_detection": "YOLOv9 車牌偵測", + "text_embedding_speed": "文字嵌入速度", + "yolov9_plate_detection_speed": "YOLOv9 車牌偵測速度", + "plate_recognition": "車牌辨識", + "image_embedding_speed": "圖片嵌入速度", + "review_description": "審查描述", + "review_description_speed": "審查描述速度", + "review_description_events_per_second": "審查描述", + "object_description": "物件描述", + "object_description_speed": "物件描述速度", + "object_description_events_per_second": "物件描述", + "classification": "{{name}} 分類", + "classification_speed": "{{name}} 分類速度", + "classification_events_per_second": "{{name}} 每秒分類事件數" + }, + "averageInf": "平均推論時間" + } +} diff --git a/web/public/locales/zh-CN/audio.json b/web/public/locales/zh-CN/audio.json new file mode 100644 index 0000000..848418f --- /dev/null +++ b/web/public/locales/zh-CN/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "谈话", + "babbling": "喋喋不休", + "yell": "大喊", + "bellow": "吼叫", + "whoop": "欢呼", + "whispering": "耳语", + "laughter": "笑声", + "snicker": "窃笑", + "crying": "哭泣", + "sigh": "叹息", + "singing": "唱歌", + "choir": "合唱", + "yodeling": "山歌", + "chant": "吟唱", + "mantra": "咒语", + "child_singing": "儿童歌唱", + "synthetic_singing": "合成歌声", + "rapping": "说唱", + "humming": "哼唱", + "groan": "呻吟", + "grunt": "咕哝", + "whistling": "口哨", + "breathing": "呼吸", + "wheeze": "喘息", + "snoring": "打鼾", + "gasp": "倒抽气", + "pant": "喘气", + "snort": "哼声", + "cough": "咳嗽", + "throat_clearing": "清嗓子", + "sneeze": "打喷嚏", + "sniff": "抽鼻子", + "run": "跑步", + "shuffle": "拖步", + "footsteps": "脚步声", + "chewing": "咀嚼", + "biting": "咬", + "gargling": "漱口", + "stomach_rumble": "肚子咕噜", + "burping": "打嗝", + "hiccup": "打嗝", + "fart": "放屁", + "hands": "手", + "finger_snapping": "打响指", + "clapping": "鼓掌", + "heartbeat": "心跳", + "heart_murmur": "心脏杂音", + "cheering": "欢呼", + "applause": "掌声", + "chatter": "闲聊", + "crowd": "人群", + "children_playing": "儿童玩耍", + "animal": "动物", + "pets": "宠物", + "dog": "狗", + "bark": "狗叫", + "yip": "吠叫", + "howl": "嚎叫", + "bow_wow": "汪汪", + "growling": "咆哮", + "whimper_dog": "狗呜咽", + "cat": "猫", + "purr": "咕噜", + "meow": "喵喵", + "hiss": "嘶嘶声", + "caterwaul": "猫叫春", + "livestock": "牲畜", + "horse": "马", + "clip_clop": "蹄声", + "neigh": "嘶鸣", + "cattle": "牛", + "moo": "哞哞", + "cowbell": "牛铃", + "pig": "猪", + "oink": "哼哼", + "goat": "山羊", + "bleat": "咩咩", + "sheep": "绵羊", + "fowl": "家禽", + "chicken": "鸡", + "cluck": "咯咯", + "cock_a_doodle_doo": "喔喔", + "turkey": "火鸡", + "gobble": "咯咯", + "duck": "鸭子", + "quack": "嘎嘎", + "goose": "鹅", + "honk": "鸣笛/鹅叫声", + "wild_animals": "野生动物", + "roaring_cats": "吼叫的猫科动物", + "roar": "吼叫", + "bird": "鸟", + "chirp": "啾啾", + "squawk": "啼叫", + "pigeon": "鸽子", + "coo": "咕咕", + "crow": "乌鸦", + "caw": "呱呱", + "owl": "猫头鹰", + "hoot": "呜呜", + "flapping_wings": "翅膀拍打", + "dogs": "狗群", + "rats": "老鼠", + "mouse": "鼠标", + "patter": "啪嗒声", + "insect": "昆虫", + "cricket": "蟋蟀", + "mosquito": "蚊子", + "fly": "苍蝇", + "buzz": "嗡嗡", + "frog": "青蛙", + "croak": "呱呱", + "snake": "蛇", + "rattle": "响尾", + "whale_vocalization": "鲸鱼叫声", + "music": "音乐", + "musical_instrument": "乐器", + "plucked_string_instrument": "弹拨乐器", + "guitar": "吉他", + "electric_guitar": "电吉他", + "bass_guitar": "贝斯", + "acoustic_guitar": "原声吉他", + "steel_guitar": "钢弦吉他", + "tapping": "敲击", + "strum": "扫弦", + "banjo": "班卓琴", + "sitar": "西塔琴", + "mandolin": "曼陀林", + "zither": "古筝", + "ukulele": "尤克里里", + "keyboard": "键盘", + "piano": "钢琴", + "electric_piano": "电钢琴", + "organ": "风琴", + "electronic_organ": "电子琴", + "hammond_organ": "哈蒙德风琴", + "synthesizer": "合成器", + "sampler": "采样器", + "harpsichord": "大键琴", + "percussion": "打击乐器", + "drum_kit": "架子鼓", + "drum_machine": "鼓机", + "drum": "鼓", + "snare_drum": "军鼓", + "rimshot": "鼓边击", + "drum_roll": "滚鼓", + "bass_drum": "大鼓", + "timpani": "定音鼓", + "tabla": "塔布拉鼓", + "cymbal": "钹", + "hi_hat": "踩镲", + "wood_block": "木鱼", + "tambourine": "铃鼓", + "maraca": "沙锤", + "gong": "锣", + "tubular_bells": "管钟", + "mallet_percussion": "槌击打击乐器", + "marimba": "马林巴", + "glockenspiel": "钟琴", + "vibraphone": "颤音琴", + "steelpan": "钢鼓", + "orchestra": "管弦乐队", + "brass_instrument": "铜管乐器", + "french_horn": "圆号", + "trumpet": "小号", + "trombone": "长号", + "bowed_string_instrument": "弓弦乐器", + "string_section": "弦乐组", + "violin": "小提琴", + "pizzicato": "拨弦", + "cello": "大提琴", + "double_bass": "低音提琴", + "wind_instrument": "管乐器", + "flute": "长笛", + "saxophone": "萨克斯", + "clarinet": "单簧管", + "harp": "竖琴", + "bell": "铃", + "church_bell": "教堂钟", + "jingle_bell": "铃铛", + "bicycle_bell": "自行车铃", + "tuning_fork": "音叉", + "chime": "风铃", + "wind_chime": "风铃", + "harmonica": "口琴", + "accordion": "手风琴", + "bagpipes": "风笛", + "didgeridoo": "迪吉里杜管", + "theremin": "特雷门琴", + "singing_bowl": "颂钵", + "scratching": "刮擦声", + "pop_music": "流行音乐", + "hip_hop_music": "嘻哈音乐", + "beatboxing": "人声节拍", + "rock_music": "摇滚音乐", + "heavy_metal": "重金属", + "punk_rock": "朋克摇滚", + "grunge": "垃圾摇滚", + "progressive_rock": "前卫摇滚", + "rock_and_roll": "摇滚乐", + "psychedelic_rock": "迷幻摇滚", + "rhythm_and_blues": "节奏布鲁斯", + "soul_music": "灵魂乐", + "reggae": "雷鬼", + "country": "乡村音乐", + "swing_music": "摇摆乐", + "bluegrass": "蓝草音乐", + "funk": "放克", + "folk_music": "民谣", + "middle_eastern_music": "中东音乐", + "jazz": "爵士乐", + "disco": "迪斯科", + "classical_music": "古典音乐", + "opera": "歌剧", + "electronic_music": "电子音乐", + "house_music": "浩室音乐", + "techno": "科技舞曲", + "dubstep": "回响贝斯", + "drum_and_bass": "鼓打贝斯", + "electronica": "电子乐", + "electronic_dance_music": "电子舞曲", + "ambient_music": "环境音乐", + "trance_music": "迷幻舞曲", + "music_of_latin_america": "拉丁美洲音乐", + "salsa_music": "萨尔萨", + "flamenco": "弗拉门戈", + "blues": "蓝调", + "music_for_children": "儿童音乐", + "new-age_music": "新世纪音乐", + "vocal_music": "声乐", + "a_capella": "无伴奏合唱", + "music_of_africa": "非洲音乐", + "afrobeat": "非洲节拍", + "christian_music": "基督教音乐", + "gospel_music": "福音音乐", + "music_of_asia": "亚洲音乐", + "carnatic_music": "卡纳提克音乐", + "music_of_bollywood": "宝莱坞音乐", + "ska": "斯卡", + "traditional_music": "传统音乐", + "independent_music": "独立音乐", + "song": "歌曲", + "background_music": "背景音乐", + "theme_music": "主题音乐", + "jingle": "广告歌", + "soundtrack_music": "配乐", + "lullaby": "摇篮曲", + "video_game_music": "电子游戏音乐", + "christmas_music": "圣诞音乐", + "dance_music": "舞曲", + "wedding_music": "婚礼音乐", + "happy_music": "欢快音乐", + "sad_music": "悲伤音乐", + "tender_music": "温柔音乐", + "exciting_music": "激动音乐", + "angry_music": "愤怒音乐", + "scary_music": "恐怖音乐", + "wind": "风", + "rustling_leaves": "树叶沙沙声", + "wind_noise": "风声", + "thunderstorm": "雷暴", + "thunder": "雷声", + "water": "水", + "rain": "雨", + "raindrop": "雨滴", + "rain_on_surface": "雨打表面", + "stream": "溪流", + "waterfall": "瀑布", + "ocean": "海洋", + "waves": "波浪", + "steam": "蒸汽", + "gurgling": "汩汩声", + "fire": "火", + "crackle": "噼啪声", + "vehicle": "车辆", + "boat": "船", + "sailboat": "帆船", + "rowboat": "划艇", + "motorboat": "摩托艇", + "ship": "轮船", + "motor_vehicle": "机动车", + "car": "汽车", + "toot": "鸣笛", + "car_alarm": "汽车警报", + "power_windows": "电动车窗", + "skidding": "轮胎打滑", + "tire_squeal": "轮胎尖叫", + "car_passing_by": "汽车驶过", + "race_car": "赛车", + "truck": "卡车", + "air_brake": "气闸", + "air_horn": "气笛", + "reversing_beeps": "倒车提示音", + "ice_cream_truck": "冰淇淋车", + "bus": "公交车", + "emergency_vehicle": "应急车辆", + "police_car": "警车", + "ambulance": "救护车", + "fire_engine": "消防车", + "motorcycle": "摩托车", + "traffic_noise": "交通噪音", + "rail_transport": "铁路运输", + "train": "火车", + "train_whistle": "火车汽笛", + "train_horn": "火车鸣笛", + "railroad_car": "铁路车厢", + "train_wheels_squealing": "火车轮子尖叫", + "subway": "地铁", + "aircraft": "飞行器", + "aircraft_engine": "飞机引擎", + "jet_engine": "喷气引擎", + "propeller": "螺旋桨", + "helicopter": "直升机", + "fixed-wing_aircraft": "固定翼飞机", + "bicycle": "自行车", + "skateboard": "滑板", + "engine": "引擎", + "light_engine": "轻型引擎", + "dental_drill's_drill": "牙科钻", + "lawn_mower": "割草机", + "chainsaw": "电锯", + "medium_engine": "中型引擎", + "heavy_engine": "重型引擎", + "engine_knocking": "引擎敲击", + "engine_starting": "引擎启动", + "idling": "怠速", + "accelerating": "加速", + "door": "门", + "doorbell": "门铃", + "ding-dong": "叮咚", + "sliding_door": "滑动门", + "slam": "猛关", + "knock": "敲门", + "tap": "轻敲", + "squeak": "吱吱声", + "cupboard_open_or_close": "橱柜开关", + "drawer_open_or_close": "抽屉开关", + "dishes": "餐具", + "cutlery": "刀叉", + "chopping": "切菜", + "frying": "煎炸", + "microwave_oven": "微波炉", + "blender": "搅拌机", + "water_tap": "水龙头", + "sink": "水槽", + "bathtub": "浴缸", + "hair_dryer": "吹风机", + "toilet_flush": "马桶冲水", + "toothbrush": "牙刷", + "electric_toothbrush": "电动牙刷", + "vacuum_cleaner": "吸尘器", + "zipper": "拉链", + "keys_jangling": "钥匙叮当", + "coin": "硬币", + "scissors": "剪刀", + "electric_shaver": "电动剃须刀", + "shuffling_cards": "洗牌", + "typing": "打字", + "typewriter": "打字机", + "computer_keyboard": "电脑键盘", + "writing": "书写", + "alarm": "警报", + "telephone": "电话", + "telephone_bell_ringing": "电话铃声", + "ringtone": "手机铃声", + "telephone_dialing": "电话拨号", + "dial_tone": "拨号音", + "busy_signal": "忙音", + "alarm_clock": "闹钟", + "siren": "警笛", + "civil_defense_siren": "防空警报", + "buzzer": "蜂鸣器", + "smoke_detector": "烟雾检测器", + "fire_alarm": "火灾警报器", + "foghorn": "雾笛", + "whistle": "哨子", + "steam_whistle": "蒸汽汽笛", + "mechanisms": "机械装置", + "ratchet": "棘轮", + "clock": "时钟", + "tick": "滴答", + "tick-tock": "滴答滴答", + "gears": "齿轮", + "pulleys": "滑轮", + "sewing_machine": "缝纫机", + "mechanical_fan": "机械风扇", + "air_conditioning": "空调", + "cash_register": "收银机", + "printer": "打印机", + "camera": "相机", + "single-lens_reflex_camera": "单反相机", + "tools": "工具", + "hammer": "锤子", + "jackhammer": "风镐", + "sawing": "锯", + "filing": "锉", + "sanding": "砂磨", + "power_tool": "电动工具", + "drill": "电钻", + "explosion": "爆炸", + "gunshot": "枪声", + "machine_gun": "机关枪", + "fusillade": "齐射", + "artillery_fire": "炮火", + "cap_gun": "玩具枪", + "fireworks": "烟花", + "firecracker": "鞭炮", + "burst": "爆裂", + "eruption": "爆发", + "boom": "轰隆", + "wood": "木头", + "chop": "砍", + "splinter": "碎裂", + "crack": "破裂", + "glass": "玻璃", + "chink": "叮当", + "shatter": "粉碎", + "silence": "寂静", + "sound_effect": "音效", + "environmental_noise": "环境噪音", + "static": "静电噪音", + "white_noise": "白噪音", + "pink_noise": "粉红噪音", + "television": "电视", + "radio": "收音机", + "field_recording": "实地录音", + "scream": "尖叫", + "sodeling": "索德铃", + "chird": "啾鸣", + "change_ringing": "变奏钟声", + "shofar": "羊角号", + "liquid": "液体", + "splash": "液体飞溅", + "slosh": "液体晃动", + "squish": "挤压", + "drip": "水滴声", + "pour": "倒水声", + "trickle": "细流水声", + "gush": "液体喷涌", + "fill": "注水声", + "spray": "喷洒", + "pump": "泵送", + "stir": "搅拌声", + "boiling": "沸腾声", + "sonar": "声呐声", + "arrow": "箭矢声", + "whoosh": "呼啸声", + "thump": "砰击声", + "thunk": "沉闷声", + "electronic_tuner": "电子调音器", + "effects_unit": "效果器", + "chorus_effect": "合唱效果", + "basketball_bounce": "篮球反弹声", + "bang": "砰声", + "slap": "拍击声", + "whack": "重击声", + "smash": "猛击声", + "breaking": "破碎声", + "bouncing": "弹跳声", + "whip": "鞭打声", + "flap": "扑动声", + "scratch": "刮擦声", + "scrape": "刮擦声", + "rub": "摩擦声", + "roll": "滚动声", + "crushing": "压碎声", + "crumpling": "揉皱声", + "tearing": "撕裂声", + "beep": "哔声", + "ping": "嘀声", + "ding": "叮声", + "clang": "铛声", + "squeal": "尖锐声", + "creak": "嘎吱声", + "rustle": "沙沙声", + "whir": "嗡声", + "clatter": "哐啷声", + "sizzle": "滋滋声", + "clicking": "点击声", + "clickety_clack": "咔嗒声", + "rumble": "隆隆声", + "plop": "扑通声", + "hum": "嗡鸣声", + "zing": "嗖声", + "boing": "嘣声", + "crunch": "咔嚓声", + "sine_wave": "正弦波声", + "harmonic": "谐波声", + "chirp_tone": "啾声", + "pulse": "脉冲", + "inside": "室内声", + "outside": "室外声", + "reverberation": "混响", + "echo": "回声", + "noise": "噪声", + "mains_hum": "电流嗡声", + "distortion": "失真声", + "sidetone": "旁音", + "cacophony": "刺耳噪声", + "throbbing": "脉动声", + "vibration": "振动声" +} diff --git a/web/public/locales/zh-CN/common.json b/web/public/locales/zh-CN/common.json new file mode 100644 index 0000000..ebb58ce --- /dev/null +++ b/web/public/locales/zh-CN/common.json @@ -0,0 +1,327 @@ +{ + "time": { + "untilForTime": "直到 {{time}}", + "untilForRestart": "直到 Frigate 重启。", + "untilRestart": "直到重启", + "ago": "{{timeAgo}} 前", + "justNow": "刚才", + "today": "今天", + "yesterday": "昨天", + "last7": "最后 7 天", + "last14": "最后 14 天", + "last30": "最后 30 天", + "thisWeek": "本周", + "lastWeek": "上个周", + "thisMonth": "本月", + "lastMonth": "上个月", + "5minutes": "5 分钟", + "10minutes": "10 分钟", + "30minutes": "30 分钟", + "1hour": "1 小时", + "12hours": "12 小时", + "24hours": "24 小时", + "pm": "下午", + "am": "上午", + "yr": "{{time}}年", + "year_other": "{{time}} 年", + "mo": "{{time}}月", + "month_other": "{{time}} 个月", + "d": "{{time}}天", + "day_other": "{{time}} 天", + "h": "{{time}}小时", + "hour_other": "{{time}} 小时", + "m": "{{time}}分钟", + "minute_other": "{{time}} 分钟", + "s": "{{time}}秒", + "second_other": "{{time}} 秒", + "formattedTimestamp": { + "12hour": "M月d日 ah:mm:ss", + "24hour": "M月d日 HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM月dd日 ah:mm:ss", + "24hour": "MM月dd日 HH:mm:ss" + }, + "formattedTimestampExcludeSeconds": { + "12hour": "%m月%-d日 %I:%M %p", + "24hour": "%m月%-d日 %H:%M" + }, + "formattedTimestampWithYear": { + "12hour": "%Y年%m月%-d日 %I:%M:%S %p", + "24hour": "%Y年%m月%-d日 %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%m月%-d日", + "formattedTimestampHourMinute": { + "12hour": "a h:mm", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "ah:mm:ss", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "M月d日 ah:mm", + "24hour": "M月d日 HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "24hour": "yyyy年M月d日 HH:mm", + "12hour": "yyyy年M月d日 ah:mm" + }, + "formattedTimestampMonthDay": "M月d日", + "formattedTimestampFilename": { + "12hour": "yy年MM月dd日 ah时mm分ss秒", + "24hour": "yy年MM月dd日 HH时mm分ss秒" + }, + "formattedTimestampMonthDayYear": { + "12hour": "yy年MM月dd日", + "24hour": "yy年MM月dd日" + }, + "inProgress": "进行中", + "invalidStartTime": "无效的开始时间", + "invalidEndTime": "无效的结束时间", + "never": "从不" + }, + "unit": { + "speed": { + "mph": "英里/小时", + "kph": "公里/小时" + }, + "length": { + "feet": "英尺", + "meters": "米" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/每小时", + "mbph": "MB/每小时", + "gbph": "GB/每小时" + } + }, + "label": { + "back": "返回", + "hide": "隐藏 {{item}}", + "show": "显示 {{item}}", + "ID": "ID", + "none": "无", + "all": "所有", + "other": "其他" + }, + "pagination": { + "label": "分页", + "previous": { + "title": "上一页", + "label": "转到上一页" + }, + "next": { + "title": "下一页", + "label": "转到下一页" + }, + "more": "更多页面" + }, + "button": { + "apply": "应用", + "reset": "重置", + "done": "完成", + "enabled": "启用", + "enable": "启用", + "disabled": "禁用", + "disable": "禁用", + "save": "保存", + "saving": "保存中…", + "cancel": "取消", + "close": "关闭", + "copy": "复制", + "back": "返回", + "history": "历史", + "fullscreen": "全屏", + "exitFullscreen": "退出全屏", + "pictureInPicture": "画中画", + "on": "开", + "off": "关", + "edit": "编辑", + "copyCoordinates": "复制坐标", + "delete": "删除", + "yes": "是", + "no": "否", + "download": "下载", + "info": "信息", + "suspended": "已暂停", + "unsuspended": "取消暂停", + "play": "播放", + "unselect": "取消选择", + "export": "导出", + "deleteNow": "立即删除", + "next": "下一个", + "cameraAudio": "摄像头音频", + "twoWayTalk": "双向对话", + "continue": "继续", + "add": "添加", + "applying": "应用中…", + "undo": "撤销", + "copiedToClipboard": "已复制到剪贴板", + "modified": "已修改", + "overridden": "已覆盖", + "resetToGlobal": "重置为全局", + "resetToDefault": "重置为默认", + "saveAll": "保存全部", + "savingAll": "保存全部中…", + "undoAll": "撤销全部", + "retry": "重试" + }, + "menu": { + "system": "系统", + "systemMetrics": "系统信息", + "configuration": "配置", + "systemLogs": "系统日志", + "settings": "设置", + "configurationEditor": "配置编辑器", + "languages": "Languages / 语言", + "language": { + "en": "英语 (English)", + "zhCN": "简体中文", + "withSystem": { + "label": "使用系统的语言设置" + }, + "hi": "印地语 (हिन्दी)", + "es": "西班牙语 (Español)", + "fr": "法语 (Français)", + "ar": "阿拉伯语 (العربية)", + "pt": "葡萄牙语 (Português)", + "de": "德语 (Deutsch)", + "ja": "日语 (日本語)", + "tr": "土耳其语 (Türkçe)", + "it": "意大利语 (Italiano)", + "nl": "荷兰语 (Nederlands)", + "sv": "瑞典语 (Svenska)", + "nb": "挪威博克马尔语 (Norsk Bokmål)", + "ko": "韩语 (한국어)", + "vi": "越南语 (Tiếng Việt)", + "fa": "波斯语 (فارسی)", + "pl": "波兰语 (Polski)", + "uk": "乌克兰语 (Українська)", + "he": "希伯来语 (עברית)", + "el": "希腊语 (Ελληνικά)", + "ro": "罗马尼亚语 (Română)", + "hu": "匈牙利语 (Magyar)", + "fi": "芬兰语 (Suomi)", + "da": "丹麦语 (Dansk)", + "sk": "斯洛伐克语 (Slovenčina)", + "ru": "俄语 (Русский)", + "cs": "捷克语 (Čeština)", + "yue": "粤语 (粵語)", + "th": "泰语 (ไทย)", + "ca": "加泰罗尼亚语 (Català)", + "ptBR": "巴西葡萄牙语 (Português brasileiro)", + "sr": "塞尔维亚语 (Српски)", + "sl": "斯洛文尼亚语 (Slovenščina)", + "lt": "立陶宛语 (Lietuvių)", + "bg": "保加利亚语 (Български)", + "gl": "加利西亚语 (Galego)", + "id": "印度尼西亚语 (Bahasa Indonesia)", + "ur": "乌尔都语 (اردو)", + "hr": "克罗地亚语 (Hrvatski)", + "bs": "波斯尼亚语(Bosanski)", + "zhHant": "繁体中文 (Traditional Chinese)" + }, + "appearance": "外观", + "darkMode": { + "label": "深色模式", + "light": "浅色", + "dark": "深色", + "withSystem": { + "label": "使用系统深色模式设置" + } + }, + "withSystem": "跟随系统", + "theme": { + "label": "主题", + "blue": "蓝色", + "green": "绿色", + "nord": "Nord", + "red": "红色", + "contrast": "高对比度", + "default": "默认", + "highcontrast": "高对比" + }, + "help": "帮助", + "documentation": { + "title": "文档", + "label": "Frigate 的官方文档" + }, + "live": { + "title": "实时监控", + "allCameras": "所有摄像头", + "cameras": { + "title": "摄像头", + "count_other": "{{count}} 个摄像头" + } + }, + "review": "核查", + "explore": "浏览", + "export": "导出", + "uiPlayground": "UI 演示", + "faceLibrary": "人脸管理", + "user": { + "account": "账号", + "current": "当前用户:{{user}}", + "anonymous": "匿名", + "logout": "登出", + "setPassword": "设置密码", + "title": "用户" + }, + "restart": "重启 Frigate", + "classification": "目标分类", + "actions": "操作", + "chat": "聊天", + "profiles": "配置模板", + "features": "功能" + }, + "toast": { + "copyUrlToClipboard": "已复制链接到剪贴板。", + "save": { + "title": "保存", + "error": { + "title": "保存配置信息失败: {{errorMessage}}", + "noMessage": "保存配置信息失败" + }, + "success": "成功保存配置文件。" + } + }, + "role": { + "title": "权限组", + "admin": "管理员", + "viewer": "成员", + "desc": "管理员可以完全访问 Frigate 界面上所有功能。成员则仅能查看摄像头、核查项和历史录像。" + }, + "accessDenied": { + "documentTitle": "没有权限 - Frigate", + "title": "没有权限", + "desc": "您没有权限查看此页面。" + }, + "notFound": { + "documentTitle": "没有找到页面 - Frigate", + "title": "404", + "desc": "页面未找到" + }, + "selectItem": "选择 {{item}}", + "readTheDocumentation": "阅读文档", + "information": { + "pixels": "{{area}} 像素" + }, + "list": { + "two": "{{0}} 和 {{1}}", + "many": "{{items}} 以及 {{last}}", + "separatorWithSpace": "、 " + }, + "field": { + "optional": "可选", + "internalID": "Frigate 在配置与数据库中使用的内部 ID" + }, + "no_items": "没有项目", + "validation_errors": "验证错误", + "credentialField": { + "savedPlaceholder": "已保存 — 留空则保留当前设置" + } +} diff --git a/web/public/locales/zh-CN/components/auth.json b/web/public/locales/zh-CN/components/auth.json new file mode 100644 index 0000000..dbfc349 --- /dev/null +++ b/web/public/locales/zh-CN/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "用户名", + "password": "密码", + "login": "登录", + "errors": { + "usernameRequired": "用户名不能为空", + "passwordRequired": "密码不能为空", + "rateLimit": "超出请求限制,请稍后再试。", + "loginFailed": "登录失败", + "unknownError": "未知错误,请检查日志。", + "webUnknownError": "未知错误,请检查控制台日志。" + }, + "firstTimeLogin": "首次尝试登录?请从 Frigate 日志中查找生成的登录密码等信息。" + } +} diff --git a/web/public/locales/zh-CN/components/camera.json b/web/public/locales/zh-CN/components/camera.json new file mode 100644 index 0000000..1e45e40 --- /dev/null +++ b/web/public/locales/zh-CN/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "摄像头组", + "add": "添加摄像头组", + "edit": "编辑摄像头组", + "delete": { + "label": "删除摄像头组", + "confirm": { + "title": "确认删除", + "desc": "你确定要删除摄像头组 {{name}} 吗?" + } + }, + "name": { + "label": "名称", + "placeholder": "请输入名称…", + "errorMessage": { + "mustLeastCharacters": "摄像头组的名称必须至少有 2 个字符。", + "exists": "摄像头组名称已存在。", + "nameMustNotPeriod": "摄像头组名称不能包含英文句号(.)。", + "invalid": "无效的摄像头组名称。" + } + }, + "cameras": { + "label": "摄像头", + "desc": "选择添加至该组的摄像头。" + }, + "icon": "图标", + "success": "摄像头组({{name}})保存成功。", + "camera": { + "setting": { + "label": "摄像头视频流设置", + "title": "{{cameraName}} 视频流设置", + "desc": "更改此摄像头组仪表板的实时视频流选项。这些设置特定于设备/浏览器。", + "audioIsAvailable": "此视频流支持音频", + "audioIsUnavailable": "此视频流不支持音频传输", + "audio": { + "tips": { + "title": "音频必须从您的摄像头输出并在 go2rtc 中配置此流。", + "document": "阅读文档 " + } + }, + "streamMethod": { + "label": "视频流方法", + "method": { + "noStreaming": { + "label": "无视频流", + "desc": "摄像头图像每分钟仅更新一次,不会进行实时视频流播放。" + }, + "smartStreaming": { + "label": "智能视频流(推荐)", + "desc": "智能视频流在没有检测到活动时,每分钟更新一次摄像头图像,以节省带宽和资源。当检测到活动时,图像会无缝切换到实时视频流。" + }, + "continuousStreaming": { + "label": "持续视频流", + "desc": { + "title": "当摄像头画面在仪表板上可见时,始终为实时视频流,即使未检测到活动。", + "warning": "持续视频流可能会导致高带宽使用和性能问题,请谨慎使用。" + } + } + }, + "placeholder": "选择视频流传输方式" + }, + "compatibilityMode": { + "label": "兼容模式", + "desc": "仅在摄像头的实时视频流显示颜色伪影,并且图像右侧有一条对角线时启用此选项。" + }, + "stream": "视频流", + "placeholder": "选择视频流" + }, + "birdseye": "鸟瞰图" + }, + "showAll": "显示所有的摄像头组摄像头摄像头", + "showLess": "显示更少", + "editGroups": "编辑摄像头组" + }, + "debug": { + "options": { + "label": "设置", + "title": "选项", + "showOptions": "显示选项", + "hideOptions": "隐藏选项" + }, + "boundingBox": "边界框", + "timestamp": "时间戳", + "zones": "区域", + "mask": "遮罩", + "motion": "画面变动", + "regions": "区域", + "paths": "行动轨迹" + } +} diff --git a/web/public/locales/zh-CN/components/dialog.json b/web/public/locales/zh-CN/components/dialog.json new file mode 100644 index 0000000..35e45b7 --- /dev/null +++ b/web/public/locales/zh-CN/components/dialog.json @@ -0,0 +1,198 @@ +{ + "restart": { + "title": "你确定要重启 Frigate?", + "button": "重启", + "restarting": { + "title": "Frigate 正在重启", + "content": "该页面将会在 {{countdown}} 秒后自动刷新。", + "button": "强制刷新" + }, + "description": "Frigate 在重启期间将短暂停止运行。" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "提交至 Frigate+", + "desc": "你不希望检测指定地点中的目标或物体不应被视为误报。若将其作为误报提交,可能会导致 AI 模型容易混淆相关目标或物体的识别。" + }, + "review": { + "true": { + "label": "为 Frigate Plus 确认此标签", + "true_other": "这是 {{label}}" + }, + "false": { + "label": "不为 Frigate Plus 确认此标签", + "false_other": "这不是 {{label}}" + }, + "state": { + "submitted": "已提交" + }, + "question": { + "label": "为 Frigate Plus 确认此标签", + "ask_a": "这个目标/物体是 {{label}} 吗?", + "ask_an": "这个目标/物体是 {{label}} 吗?", + "ask_full": "这个目标/物体是 {{untranslatedLabel}} ({{translatedLabel}}) 吗?" + }, + "toast": { + "error": "提交至 Frigate+ 失败,请检查您的网络连接并重试。" + } + } + }, + "video": { + "viewInHistory": "在历史中查看" + } + }, + "export": { + "time": { + "fromTimeline": "从时间线选择", + "lastHour_other": "最后 {{count}} 小时", + "custom": "自定义", + "start": { + "title": "开始时间", + "label": "选择开始时间" + }, + "end": { + "title": "结束时间", + "label": "选择结束时间" + } + }, + "name": { + "placeholder": "导出项目的名字" + }, + "select": "选择", + "export": "导出", + "selectOrExport": "选择或导出", + "toast": { + "success": "导出成功。进入 导出 页面查看文件。", + "error": { + "failed": "未能加入导出队列:{{error}}", + "endTimeMustAfterStartTime": "结束时间必须在开始时间之后", + "noVaildTimeSelected": "未选择有效的时间范围" + }, + "view": "查看", + "queued": "导出已加入队列。请在导出页面查看进度。", + "batchSuccess_other": "已开始 {{count}} 个导出,正在打开合集。", + "batchPartial": "已开始 {{total}} 个导出中的 {{successful}} 个。失败的摄像头:{{failedCameras}}", + "batchFailed": "启动导出失败(共 {{total}} 个)。失败的摄像头:{{failedCameras}}", + "batchQueuedSuccess_other": "已排队 {{count}} 个导出,正在打开合集。", + "batchQueuedPartial": "已将 {{total}} 个导出中的 {{successful}} 个加入队列。失败的摄像头:{{failedCameras}}", + "batchQueueFailed": "未能将 {{total}} 个导出加入队列。失败的摄像头:{{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "保存导出", + "previewExport": "预览导出", + "queueingExport": "正在加入导出队列…", + "useThisRange": "使用此范围" + }, + "case": { + "label": "合集", + "placeholder": "选择合集", + "newCaseOption": "创建新合集", + "newCaseNamePlaceholder": "新合集名称", + "newCaseDescriptionPlaceholder": "合集描述", + "nonAdminHelp": "将为这些导出文件创建一个新的合集。" + }, + "queueing": "正在加入导出队列…", + "tabs": { + "export": "单个摄像头", + "multiCamera": "多个摄像头" + }, + "multiCamera": { + "timeRange": "时间范围", + "selectFromTimeline": "从时间线选择", + "cameraSelection": "摄像头", + "cameraSelectionHelp": "在此时间范围内具有追踪目标的摄像头会被预先选中", + "checkingActivity": "正在检查摄像头活动…", + "noCameras": "没有可用的摄像头", + "detectionCount_other": "{{count}} 个追踪目标", + "nameLabel": "导出名称", + "namePlaceholder": "这些导出文件的可选基础名称", + "queueingButton": "正在加入导出队列…", + "exportButton_other": "导出 {{count}} 个摄像头", + "searchOrSelectGroup": "搜索或选择摄像头组…", + "selectAll": "选择所有摄像头", + "clearSelection": "清空选择", + "selectWithActivity": "包含追踪目标的摄像头", + "selectGroup": "选择组", + "noMatchingCameras": "没有匹配搜索的摄像头", + "selectedCount": "{{selected}} / {{total}} 个" + }, + "multi": { + "title_other": "导出 {{count}} 个核查", + "description": "导出每个选定的核查项。所有导出文件将归入同一个合集。", + "descriptionNoCase": "导出每个选定的核查项。", + "caseNamePlaceholder": "核查导出 - {{date}}", + "exportButton_other": "导出 {{count}} 个核查", + "exportingButton": "导出中…", + "toast": { + "started_other": "已开始 {{count}} 个导出。正在打开合集。", + "startedNoCase_other": "已开始 {{count}} 个导出。", + "partial": "已启动 {{total}} 个导出,其中 {{successful}} 个成功。失败项:{{failedItems}}", + "failed": "启动导出失败(共 {{total}} 个)。失败项:{{failedItems}}" + } + } + }, + "streaming": { + "label": "视频流", + "restreaming": { + "disabled": "此摄像头未启用视频流转发功能。", + "desc": { + "title": "为此摄像头设置 go2rtc,以获取额外的实时预览选项和音频支持。", + "readTheDocumentation": "阅读文档" + } + }, + "showStats": { + "label": "显示视频流统计信息", + "desc": "启用后将在摄像头画面上叠加显示视频流统计信息。" + }, + "debugView": "调试界面" + }, + "search": { + "saveSearch": { + "label": "保存搜索", + "desc": "请为此已保存的搜索提供一个名称。", + "placeholder": "请输入搜索名称", + "overwrite": "{{searchName}} 已存在。保存将覆盖现有值。", + "success": "搜索 ({{searchName}}) 已保存。", + "button": { + "save": { + "label": "保存此搜索" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "确认删除", + "desc": { + "selected": "你确定要删除与此核查项相关的所有录制视频吗?

    提示:按住 Shift 键点击删除可跳过此对话框。" + }, + "toast": { + "success": "已删除与所选核查项关联的视频片段。", + "error": "删除失败:{{error}}" + } + }, + "button": { + "export": "导出", + "markAsReviewed": "标记为已核查", + "deleteNow": "立即删除", + "markAsUnreviewed": "标记为未核查" + }, + "shareTimestamp": { + "label": "分享该时间片段", + "title": "分享该时间片段", + "description": "分享带当前录制的播放时间的地址,或选择自定义时间。请注意,这不是公开的分享链接,只有具有 Frigate 和此摄像头访问权限的用户才能访问。", + "custom": "自定义时间", + "button": "分享时间片段地址", + "shareTitle": "Frigate 核查时间:{{camera}}" + } + }, + "imagePicker": { + "selectImage": "选择追踪目标的缩略图", + "search": { + "placeholder": "通过标签或子标签搜索……" + }, + "noImages": "未在此摄像头找到缩略图", + "unknownLabel": "已保存触发的图片" + } +} diff --git a/web/public/locales/zh-CN/components/filter.json b/web/public/locales/zh-CN/components/filter.json new file mode 100644 index 0000000..9bf90d2 --- /dev/null +++ b/web/public/locales/zh-CN/components/filter.json @@ -0,0 +1,141 @@ +{ + "filter": "过滤器", + "labels": { + "label": "标签", + "all": { + "title": "所有标签", + "short": "标签" + }, + "count": "{{count}} 个标签", + "count_other": "{{count}} 个标签", + "count_one": "{{count}} 个标签" + }, + "zones": { + "all": { + "title": "所有区域", + "short": "区域" + }, + "label": "区域" + }, + "dates": { + "all": { + "title": "所有日期", + "short": "日期" + }, + "selectPreset": "选择预定时间…" + }, + "more": "更多筛选项", + "reset": { + "label": "重置筛选器为默认值" + }, + "timeRange": "时间范围", + "subLabels": { + "label": "子标签", + "all": "所有子标签" + }, + "score": "分值", + "estimatedSpeed": "预计速度({{unit}})", + "features": { + "label": "特性", + "hasSnapshot": "包含快照", + "hasVideoClip": "包含视频片段", + "submittedToFrigatePlus": { + "label": "提交至 Frigate+", + "tips": "你必须要先筛选有快照的追踪目标。

    没有快照的追踪目标无法提交至 Frigate+。" + } + }, + "sort": { + "label": "排序", + "dateAsc": "日期 (正序)", + "dateDesc": "日期 (倒序)", + "scoreAsc": "目标分值 (正序)", + "scoreDesc": "目标分值 (倒序)", + "speedAsc": "预计速度 (正序)", + "speedDesc": "预计速度 (倒序)", + "relevance": "关联性" + }, + "cameras": { + "label": "摄像头筛选", + "all": { + "title": "所有摄像头", + "short": "摄像头" + } + }, + "review": { + "showReviewed": "显示已核查的项目" + }, + "motion": { + "showMotionOnly": "仅显示画面变动" + }, + "explore": { + "settings": { + "title": "设置", + "defaultView": { + "title": "默认视图", + "desc": "当未选择任何筛选条件时,将显示每个标签下最近追踪目标的汇总信息,或者显示未筛选的网格视图。", + "summary": "摘要", + "unfilteredGrid": "未过滤网格" + }, + "gridColumns": { + "title": "网格列数", + "desc": "选择网格视图中的列数。" + }, + "searchSource": { + "label": "搜索源", + "desc": "选择是搜索缩略图还是追踪目标的描述。", + "options": { + "thumbnailImage": "缩略图", + "description": "描述" + } + } + }, + "date": { + "selectDateBy": { + "label": "选择日期进行筛选" + } + } + }, + "logSettings": { + "label": "日志级别筛选", + "filterBySeverity": "按严重程度筛选日志", + "loading": { + "title": "加载中", + "desc": "当日志面板滚动到底部时,新的日志会自动流式加载。" + }, + "disableLogStreaming": "禁用日志流式加载", + "allLogs": "所有日志" + }, + "trackedObjectDelete": { + "title": "确认删除", + "desc": "删除这 {{objectLength}} 个已追踪目标将移除它们的快照、所有已保存的嵌入向量数据以及任何相关的目标全周期条目,但在 历史 页面中这些追踪目标的录制视频片段将不会被删除。

    您确定要继续吗?

    以后按住 Shift 键进行删除可跳过此提醒。", + "toast": { + "success": "删除追踪目标成功。", + "error": "删除追踪目标失败:{{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "按区域遮罩筛选" + }, + "recognizedLicensePlates": { + "title": "识别的车牌", + "loadFailed": "加载识别的车牌失败。", + "loading": "正在加载识别的车牌…", + "placeholder": "输入以搜索车牌…", + "noLicensePlatesFound": "未找到车牌。", + "selectPlatesFromList": "从列表中选择一个或多个车牌。", + "selectAll": "选择所有", + "clearAll": "清除所有" + }, + "classes": { + "label": "分类", + "all": { + "title": "所有分类" + }, + "count_one": "{{count}} 个分类", + "count_other": "{{count}} 个分类" + }, + "attributes": { + "label": "分类属性", + "all": "所有属性" + } +} diff --git a/web/public/locales/zh-CN/components/icons.json b/web/public/locales/zh-CN/components/icons.json new file mode 100644 index 0000000..20f2012 --- /dev/null +++ b/web/public/locales/zh-CN/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "选择图标", + "search": { + "placeholder": "搜索图标…" + } + } +} diff --git a/web/public/locales/zh-CN/components/input.json b/web/public/locales/zh-CN/components/input.json new file mode 100644 index 0000000..add854a --- /dev/null +++ b/web/public/locales/zh-CN/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "下载视频", + "toast": { + "success": "你的核查视频已开始下载。" + } + } + } +} diff --git a/web/public/locales/zh-CN/components/player.json b/web/public/locales/zh-CN/components/player.json new file mode 100644 index 0000000..70956cc --- /dev/null +++ b/web/public/locales/zh-CN/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "找不到此次录制", + "noPreviewFound": "没有找到预览", + "noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览", + "submitFrigatePlus": { + "title": "提交此帧到 Frigate+?", + "submit": "提交", + "previewError": "无法加载快照预览。该录制当前可能不可用。" + }, + "livePlayerRequiredIOSVersion": "此直播流类型需要 iOS 17.1 或更高版本。", + "streamOffline": { + "title": "视频流离线", + "desc": "未在 {{cameraName}} 的 detect 流上接收到任何帧,请检查错误日志" + }, + "cameraDisabled": "摄像头已禁用", + "stats": { + "streamType": { + "title": "流类型:", + "short": "类型" + }, + "bandwidth": { + "title": "带宽:", + "short": "带宽" + }, + "latency": { + "title": "延迟:", + "value": "{{seconds}} 秒", + "short": { + "title": "延迟", + "value": "{{seconds}} 秒" + } + }, + "totalFrames": "总帧数:", + "droppedFrames": { + "title": "丢帧数:", + "short": { + "title": "丢帧", + "value": "{{droppedFrames}} 帧" + } + }, + "decodedFrames": "解码帧数:", + "droppedFrameRate": "丢帧率:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "已成功提交帧到 Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "提交帧到 Frigate+ 失败" + } + }, + "cameraOff": "摄像头已关闭" +} diff --git a/web/public/locales/zh-CN/config/cameras.json b/web/public/locales/zh-CN/config/cameras.json new file mode 100644 index 0000000..93d82d6 --- /dev/null +++ b/web/public/locales/zh-CN/config/cameras.json @@ -0,0 +1,964 @@ +{ + "label": "摄像头配置", + "name": { + "label": "摄像头名称", + "description": "必须填写摄像头名称" + }, + "friendly_name": { + "label": "别名", + "description": "摄像头别名将用于展示在页面中" + }, + "enabled": { + "label": "开启", + "description": "开启" + }, + "audio": { + "label": "音频检测", + "description": "此摄像头的音频事件检测设置。", + "enabled": { + "label": "开启音频检测", + "description": "开启或禁用此摄像头的音频事件检测。" + }, + "max_not_heard": { + "label": "结束超时", + "description": "在结束音频事件之前,未检测到配置的音频类型的秒数。" + }, + "num_threads": { + "label": "检测线程", + "description": "用于音频检测处理的线程数量。" + }, + "min_volume": { + "label": "最小音量", + "description": "运行音频检测所需的最小 RMS 音量阈值;数值越低灵敏度越高(例如 200 高灵敏度,500 中等,1000 低灵敏度)。" + }, + "listen": { + "label": "监听类型", + "description": "要检测的音频事件类型列表(例如:bark、fire_alarm、speech、yell)。" + }, + "filters": { + "label": "音频过滤器", + "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。", + "threshold": { + "label": "最低音频置信度", + "description": "设置音频事件所需的最低置信度阈值。" + } + }, + "enabled_in_config": { + "label": "原始音频状态", + "description": "指示原始静态配置文件中是否开启了音频检测。" + } + }, + "audio_transcription": { + "label": "音频转录", + "description": "用于事件和实时字幕的实时和语音音频转录设置。", + "enabled": { + "label": "开启转录", + "description": "开启或关闭手动触发的音频事件转写。" + }, + "enabled_in_config": { + "label": "原始转写状态" + }, + "live_enabled": { + "label": "实时监控转写", + "description": "在接收到音频时开启实时监控持续转写。" + } + }, + "birdseye": { + "label": "鸟瞰图", + "description": "将多路摄像头画面合并为统一布局的鸟瞰合成视图设置。", + "enabled": { + "label": "开启鸟瞰图", + "description": "开启或关闭鸟瞰图功能。" + }, + "mode": { + "label": "追踪模式", + "description": "在鸟瞰视图中包含摄像头的模式有:“基于目标”、“基于画面变动”或“连续”。" + }, + "order": { + "label": "排序位置", + "description": "用于控制摄像头在鸟瞰视图布局中排序位置的数值。" + } + }, + "detect": { + "label": "目标检测", + "description": "用于运行目标检测、初始化追踪器的检测模块设置。", + "enabled": { + "label": "开启目标检测", + "description": "开启或关闭该摄像头的目标检测。" + }, + "height": { + "label": "检测画面高度", + "description": "用于配置检测流的画面高度(像素);留空则使用原始视频流分辨率。" + }, + "width": { + "label": "检测画面宽度", + "description": "用于配置检测流的画面宽度(像素);留空则使用原始视频流分辨率。" + }, + "fps": { + "label": "检测帧率", + "description": "检测时希望使用的帧率;数值越低,CPU 占用越小(推荐值为 5,仅在追踪极高速运动的目标时才设置更高数值,最高不建议超过 10)。" + }, + "min_initialized": { + "label": "最小初始化帧数", + "description": "创建追踪目标前,需要连续检测到目标的次数。数值越大,错误触发的追踪越少。默认值为帧率除以 2。" + }, + "max_disappeared": { + "label": "最大消失帧数", + "description": "追踪目标在连续多少帧未被检测到时,将被判定为已消失。" + }, + "stationary": { + "label": "静止目标配置", + "description": "用于检测和管理长时间静止目标的相关设置。", + "interval": { + "label": "静止间隔", + "description": "设置每隔多少帧执行一次检测,用于确认目标是否处于静止状态。" + }, + "threshold": { + "label": "静止阈值", + "description": "目标需要连续多少帧位置不变,才会被标记为静止状态。" + }, + "max_frames": { + "label": "最大帧数", + "description": "限制静止目标最大追踪时长(以帧数为单位),超过将会停止追踪。", + "default": { + "label": "默认最大帧数", + "description": "停止追踪前,用于追踪静止目标的默认最大帧数。" + }, + "objects": { + "label": "目标最大帧数", + "description": "可对不同类型目标分别设置静止追踪的最大帧数(覆盖全局设置)。" + } + }, + "classifier": { + "label": "开启视觉分类器", + "description": "使用视觉分类器,即使检测框有轻微抖动,也能准确判断物体是否为静止。" + } + }, + "annotation_offset": { + "label": "标记偏移量", + "description": "检测标记的时间偏移量(毫秒),用于让时间轴上的检测框与录像画面更精准对齐;可设置为正数或负数。" + } + }, + "face_recognition": { + "label": "人脸识别", + "description": "该摄像头的人脸检测与识别设置。", + "enabled": { + "label": "开启人脸识别", + "description": "开启或关闭人脸识别。" + }, + "min_area": { + "label": "最小人脸区域", + "description": "需要尝试进行人脸识别的人脸检测框最小大小(像素)。" + } + }, + "ffmpeg": { + "label": "视频流(FFmpeg)", + "description": "摄像头视频流和 FFmpeg 编解码相关设置,包含可执行文件路径、命令行参数、硬件加速,以及按不同功能划分的输出参数。", + "path": { + "label": "FFmpeg 路径", + "description": "要使用的 FFmpeg 可执行文件路径,或版本别名(如 \"7.0\" 或 \"8.0\")。" + }, + "global_args": { + "label": "FFmpeg 全局参数", + "description": "传递给 FFmpeg 进程的全局参数。" + }, + "hwaccel_args": { + "label": "硬件加速参数", + "description": "用于 FFmpeg 的硬件加速参数。建议使用对应硬件厂商的预设配置。" + }, + "input_args": { + "label": "输入参数", + "description": "应用于 FFmpeg 输入视频流的输入参数。" + }, + "output_args": { + "label": "输出参数", + "description": "用于不同 FFmpeg 功能(如检测、录制)的默认输出参数。", + "detect": { + "label": "检测输出参数", + "description": "检测功能视频流的默认输出参数。" + }, + "record": { + "label": "录制输出参数", + "description": "录制功能视频流的默认输出参数。" + } + }, + "retry_interval": { + "label": "FFmpeg 重试时间", + "description": "摄像头视频流异常断开后,重新连接前的等待时间。默认为 10 秒。" + }, + "apple_compatibility": { + "label": "Apple 兼容性", + "description": "录制 H.265 视频时启用 HEVC 标记,以提升对 Apple 设备播放的兼容性。" + }, + "gpu": { + "label": "GPU 索引", + "description": "在启用硬件加速时,默认使用的 GPU 索引。" + }, + "inputs": { + "label": "摄像头输入视频流", + "description": "该摄像头的所有输入流配置列表(包含路径和功能)。", + "path": { + "label": "输入路径", + "description": "摄像头输入视频流的地址或路径。" + }, + "roles": { + "label": "输入流功能", + "description": "定义该视频流的功能。" + }, + "global_args": { + "label": "FFmpeg 全局参数", + "description": "该输入视频流使用的 FFmpeg 全局通用参数。" + }, + "hwaccel_args": { + "label": "硬件加速参数", + "description": "该输入视频流的硬件加速参数。" + }, + "input_args": { + "label": "输入参数", + "description": "该视频流特定的输入参数。" + } + } + }, + "mqtt": { + "label": "MQTT", + "description": "MQTT 图像发布设置。", + "enabled": { + "label": "发送图像", + "description": "为此摄像头启用向 MQTT 主题发布目标图像快照。" + }, + "timestamp": { + "label": "添加时间戳", + "description": "在发布到 MQTT 的图像上叠加时间戳。" + }, + "bounding_box": { + "label": "添加边界框", + "description": "在通过 MQTT 发布的图像上绘制边界框。" + }, + "crop": { + "label": "裁剪图像", + "description": "将发布到 MQTT 的图像裁剪到检测到的目标边界框。" + }, + "height": { + "label": "图像高度", + "description": "通过 MQTT 发布的图像的调整高度(像素)。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入才能发布 MQTT 图像的区域。" + }, + "quality": { + "label": "JPEG 质量", + "description": "发布到 MQTT 的图像的 JPEG 质量(0-100)。" + } + }, + "notifications": { + "label": "通知", + "enabled": { + "label": "开启通知", + "description": "为此摄像头启用或禁用通知。" + }, + "email": { + "label": "通知邮箱", + "description": "用于推送通知或某些通知提供商要求的邮箱地址。" + }, + "cooldown": { + "label": "冷却时间", + "description": "通知之间的冷却时间(秒),以避免向收件人发送垃圾信息。" + }, + "enabled_in_config": { + "label": "原始通知状态", + "description": "指示原始静态配置中是否启用了通知。" + }, + "description": "为此摄像头启用和控制通知的设置。" + }, + "live": { + "label": "实时监控观看", + "streams": { + "label": "实时监控流名称", + "description": "配置的流名称到用于实时监控播放的 restream/go2rtc 名称的映射。" + }, + "height": { + "label": "实时监控高度", + "description": "在网页页面中渲染 jsmpeg 实时监控流的高度(像素);必须小于等于检测流高度。" + }, + "quality": { + "label": "实时监控质量", + "description": "jsmpeg 流的编码质量(1 最高,31 最低)。" + }, + "description": "用于控制实时流选择、分辨率和质量的网页页面设置。" + }, + "motion": { + "label": "画面变动检测", + "enabled": { + "label": "开启画面变动检测", + "description": "开启或关闭此摄像头的画面变动检测。" + }, + "threshold": { + "label": "画面变动阈值", + "description": "画面变动检测器使用的像素差异阈值;数值越高灵敏度越低(范围 1-255)。" + }, + "lightning_threshold": { + "label": "闪电阈值", + "description": "用于检测和忽略短暂闪电闪烁的阈值(数值越低越敏感,范围 0.3 到 1.0)。这不会完全阻止画面变动检测;只是当超过阈值时检测器会停止分析额外的帧。在此类事件期间仍会创建基于画面变动的录像。" + }, + "skip_motion_threshold": { + "label": "跳过画面变动阈值", + "description": "如果单帧中画面变化超过此比例,检测器将判定为无画面变动并立即重新校准。这可以节省 CPU 并减少闪电、风暴等情况下的误报,但也可能会错过真正的事件,如 PTZ 摄像头自动追踪目标。你需要权衡取舍:是否牺牲少量录制片段,换取更少无效视频与更低的误检。保持为空即可关闭该功能。" + }, + "improve_contrast": { + "label": "改善对比度", + "description": "在画面变动分析之前对帧应用对比度改善以帮助检测。" + }, + "contour_area": { + "label": "轮廓区域", + "description": "画面变动轮廓被计入所需的最小轮廓区域(像素)。" + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "用于画面变动计算的帧差异中使用的 alpha 混合因子。" + }, + "frame_alpha": { + "label": "画面 alpha 通道", + "description": "画面变动预处理时混合画面所使用的 alpha 值。" + }, + "frame_height": { + "label": "画面高度", + "description": "计算画面变动时缩放画面的高度(像素)。" + }, + "mask": { + "label": "遮罩坐标", + "description": "定义用于包含/排除区域的画面变动遮罩多边形的有序 x,y 坐标。" + }, + "mqtt_off_delay": { + "label": "MQTT 关闭延迟", + "description": "在发布 MQTT 'off' 状态之前,最后一次画面变动后等待的秒数。" + }, + "enabled_in_config": { + "label": "原始画面变动状态", + "description": "指示原始静态配置中是否启用了画面变动检测。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "description": "此摄像头的默认画面变动检测设置。" + }, + "objects": { + "label": "目标", + "description": "目标追踪默认设置,包括要追踪的标签和按目标的过滤器。", + "track": { + "label": "要追踪的目标", + "description": "此摄像头要追踪的目标标签列表。" + }, + "filters": { + "label": "目标过滤器", + "description": "应用于检测到的目标以减少误报的过滤器(区域、比例、置信度)。", + "min_area": { + "label": "最小目标区域", + "description": "此目标类型所需的最小边界框区域(像素或百分比)。可以是像素(整数)或百分比(0.000001 到 0.99 之间的浮点数)。" + }, + "max_area": { + "label": "最大目标区域", + "description": "此目标类型允许的最大边界框区域(像素或百分比)。可以是像素(整数)或百分比(0.000001 到 0.99 之间的浮点数)。" + }, + "min_ratio": { + "label": "最小纵横比", + "description": "边界框所需的最小宽高比。" + }, + "max_ratio": { + "label": "最大纵横比", + "description": "边界框允许的最大宽高比。" + }, + "threshold": { + "label": "置信度阈值", + "description": "目标被视为真正阳性所需的平均检测置信度阈值。" + }, + "min_score": { + "label": "最小置信度", + "description": "目标被计入所需的最小单帧检测置信度。" + }, + "mask": { + "label": "过滤器遮罩", + "description": "定义此过滤器在帧内应用位置的多边形坐标。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "mask": { + "label": "目标遮罩", + "description": "用于防止在指定区域进行目标检测的遮罩多边形。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "genai": { + "label": "生成式 AI 目标配置", + "description": "用于发送画面给生成式 AI 进行生成和描述追踪目标的选项。", + "enabled": { + "label": "开启生成式 AI", + "description": "默认开启生成式 AI 生成追踪目标的描述。" + }, + "use_snapshot": { + "label": "使用快照", + "description": "使用目标快照而不是缩略图给生成式 AI 进行描述生成。" + }, + "prompt": { + "label": "字幕提示", + "description": "使用生成式 AI 生成描述时使用的默认提示模板。" + }, + "object_prompts": { + "label": "目标提示", + "description": "按目标设置提示词,让生成式 AI 对不同标签的输出进行定制。" + }, + "objects": { + "label": "生成式 AI 目标", + "description": "默认发送给生成式 AI 的目标标签列表。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入这些区域,才会触发生成式 AI 描述生成。" + }, + "debug_save_thumbnails": { + "label": "保存缩略图", + "description": "保存发送给生成式 AI 的缩略图用于调试和核查。" + }, + "send_triggers": { + "label": "生成式 AI 触发器", + "description": "定义画面帧应在何时发送给生成式 AI(如检测结束时、更新后等)。", + "tracked_object_end": { + "label": "结束时发送", + "description": "目标追踪结束时向生成式 AI 发送请求。" + }, + "after_significant_updates": { + "label": "生成式 AI 提前触发", + "description": "在追踪目标发生指定次数的重要变化后,向生成式 AI 发送请求。" + } + }, + "enabled_in_config": { + "label": "原配置生成式 AI 状态", + "description": "表示在原始静态配置中是否已启用生成式 AI。" + } + } + }, + "record": { + "label": "录像", + "enabled": { + "label": "开启录像", + "description": "开启或关闭此摄像头的录像。" + }, + "expire_interval": { + "label": "录像清理间隔", + "description": "清理过期录像片段的间隔分钟数。" + }, + "continuous": { + "label": "持续保留", + "description": "无论是否有追踪目标或动作,保留录像的天数。如果只想保留警报和检测的录像,请设置为 0。", + "days": { + "label": "保留天数", + "description": "保留录像的天数。" + } + }, + "motion": { + "label": "动作保留", + "description": "无论是否有追踪目标,由动作触发的录像保留天数。如果只想保留警报和检测的录像,请设置为 0。", + "days": { + "label": "保留天数", + "description": "保留录像的天数。" + } + }, + "detections": { + "label": "检测保留", + "description": "检测事件的录像保留设置,包括前后捕获时长。", + "pre_capture": { + "label": "前捕获秒数", + "description": "检测事件之前包含在录像中的秒数。" + }, + "post_capture": { + "label": "后捕获秒数", + "description": "检测事件之后包含在录像中的秒数。" + }, + "retain": { + "label": "事件保留", + "description": "检测事件录像的保留设置。", + "days": { + "label": "保留天数", + "description": "保留检测事件录像的天数。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(保存所有片段)、motion(保存有动作的片段)或 active_objects(保存有活动目标的片段)。" + } + } + }, + "alerts": { + "label": "警报保留", + "description": "警报事件的录像保留设置,包括前后捕获时长。", + "pre_capture": { + "label": "前捕获秒数", + "description": "检测事件之前包含在录像中的秒数。" + }, + "post_capture": { + "label": "后捕获秒数", + "description": "检测事件之后包含在录像中的秒数。" + }, + "retain": { + "label": "事件保留", + "description": "检测事件录像的保留设置。", + "days": { + "label": "保留天数", + "description": "保留检测事件录像的天数。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(保存所有片段)、motion(保存有动作的片段)或 active_objects(保存有活动目标的片段)。" + } + } + }, + "export": { + "label": "导出配置", + "description": "导出录像时使用的设置,如延时摄影和硬件加速。", + "hwaccel_args": { + "label": "导出硬件加速参数", + "description": "用于导出/转码操作的硬件加速参数。" + }, + "max_concurrent": { + "label": "最大并发导出数", + "description": "同时可处理的最大导出任务数量。" + }, + "chapters": { + "label": "导出的录制中嵌入元数据" + } + }, + "preview": { + "label": "预览配置", + "description": "控制界面中显示的录像预览质量的设置。", + "quality": { + "label": "预览质量", + "description": "预览质量级别(very_low、low、medium、high、very_high)。" + } + }, + "enabled_in_config": { + "label": "原始录像状态", + "description": "指示原始静态配置中是否启用了录像。" + }, + "description": "此摄像头的录像和保留设置。" + }, + "review": { + "label": "核查", + "alerts": { + "label": "警报配置", + "description": "哪些追踪目标生成警报以及如何保留警报的设置。", + "enabled": { + "label": "开启警报", + "description": "开启或关闭此摄像头的警报生成。" + }, + "labels": { + "label": "警报标签", + "description": "符合警报条件的目标标签列表(例如:car、person)。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入才能被视为警报的区域;留空则允许任何区域。" + }, + "enabled_in_config": { + "label": "原始警报状态", + "description": "追踪原始静态配置中是否启用了警报。" + }, + "cutoff_time": { + "label": "警报截止时间", + "description": "在没有引起警报的活动后等待多少秒后截止警报。" + } + }, + "detections": { + "label": "检测配置", + "description": "用于设置哪些追踪目标会生成检测记录(非警报类),以及检测记录的保留方式。", + "enabled": { + "label": "开启检测", + "description": "开启或关闭此摄像头的检测事件。" + }, + "labels": { + "label": "检测标签", + "description": "符合检测事件条件的目标标签列表。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入才能被视为检测的区域;留空则允许任何区域。" + }, + "cutoff_time": { + "label": "检测截止时间", + "description": "在没有引起检测的活动后等待多少秒后截止检测。" + }, + "enabled_in_config": { + "label": "原始检测状态", + "description": "追踪原始静态配置中是否启用了检测。" + } + }, + "genai": { + "label": "生成式 AI 配置", + "description": "控制使用生成式 AI 为核查项生成描述和摘要。", + "enabled": { + "label": "开启生成式 AI 描述", + "description": "为核查项开启或关闭使用生成式 AI 生成描述和摘要。" + }, + "alerts": { + "label": "为警报开启生成式 AI", + "description": "使用生成式 AI 为警报项生成描述。" + }, + "detections": { + "label": "为检测开启生成式 AI", + "description": "使用生成式 AI 为检测项生成描述。" + }, + "image_source": { + "label": "核查图像来源", + "description": "发送给生成式 AI 的画面来源(“预览” 或 “录制”);“录制”将使用更高质量的画面帧,但会消耗更多的 token。" + }, + "additional_concerns": { + "label": "额外关注事项", + "description": "生成式 AI 在分析此摄像头的监控行为时,需要额外注意的事项或说明列表。" + }, + "debug_save_thumbnails": { + "label": "保存缩略图", + "description": "保存发送给生成式 AI 提供商的缩略图用于调试和核查。" + }, + "enabled_in_config": { + "label": "原配置生成式 AI 状态", + "description": "记录在静态配置中最初是否已启用生成式 AI 核查功能。" + }, + "preferred_language": { + "label": "首选语言", + "description": "向生成式 AI 提供商请求生成响应的首选语言。" + }, + "activity_context_prompt": { + "label": "活动上下文提示", + "description": "自定义提示词,用于说明可疑行为与非可疑行为的界定,为生成式 AI 生成摘要提供上下文依据。" + } + }, + "description": "控制此摄像头的警报、检测和生成式 AI 核查总结的设置,这些设置会被界面与存储功能使用。" + }, + "snapshots": { + "label": "快照", + "enabled": { + "label": "开启快照", + "description": "开启或关闭此摄像头的快照保存。" + }, + "clean_copy": { + "label": "保存干净副本", + "description": "除了带注释的快照外,还保存一份不带注释的干净快照副本。" + }, + "timestamp": { + "label": "时间戳叠加", + "description": "在 API 生成的快照上叠加时间戳。" + }, + "bounding_box": { + "label": "边界框叠加", + "description": "在 API 生成的快照上绘制追踪目标的边界框。" + }, + "crop": { + "label": "裁剪快照", + "description": "在 API 生成的快照裁剪到检测到的目标边界框。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入才能保存快照的区域。" + }, + "height": { + "label": "快照高度", + "description": "将 API 生成的快照调整到的目标高度(像素);留空则保持原始大小。" + }, + "retain": { + "label": "快照保留", + "description": "快照的保留设置,包括默认天数和按目标覆盖。", + "default": { + "label": "默认保留", + "description": "保留快照的默认天数。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(保存所有片段)、motion(保存有动作的片段)或 active_objects(保存有活动目标的片段)。" + }, + "objects": { + "label": "目标保留", + "description": "按目标覆盖的快照保留天数。" + } + }, + "quality": { + "label": "快照质量", + "description": "保存快照的编码质量(0-100)。" + }, + "description": "此摄像头的追踪目标 API 快照设置。" + }, + "timestamp_style": { + "label": "时间戳样式", + "position": { + "label": "时间戳位置", + "description": "时间戳在图像上的位置(tl/tr/bl/br)。" + }, + "format": { + "label": "时间戳格式", + "description": "用于时间戳的日期时间格式字符串(Python 日期时间格式代码)。" + }, + "color": { + "label": "时间戳颜色", + "description": "时间戳文本的 RGB 颜色值(所有值 0-255)。", + "red": { + "label": "红色", + "description": "时间戳颜色的红色分量(0-255)。" + }, + "green": { + "label": "绿色", + "description": "时间戳颜色的绿色分量(0-255)。" + }, + "blue": { + "label": "蓝色", + "description": "时间戳颜色的蓝色分量(0-255)。" + } + }, + "thickness": { + "label": "时间戳粗细", + "description": "时间戳文本的线条粗细。" + }, + "effect": { + "label": "时间戳效果", + "description": "时间戳文本的视觉效果(none、solid、shadow)。" + }, + "description": "快照与调试视图的时间戳样式设置。" + }, + "semantic_search": { + "label": "语义搜索", + "triggers": { + "label": "触发器", + "description": "摄像头特定语义搜索触发器的操作和匹配条件。", + "friendly_name": { + "label": "友好名称", + "description": "可选友好名称,用于在界面上为触发器显示此名称。" + }, + "enabled": { + "label": "开启此触发器", + "description": "启用或禁用此语义搜索触发器。" + }, + "type": { + "label": "触发器类型", + "description": "触发器类型:'thumbnail'(与图像匹配)或 'description'(与文本匹配)。" + }, + "data": { + "label": "触发器内容", + "description": "要与追踪目标匹配的文本短语或缩略图 ID。" + }, + "threshold": { + "label": "触发器阈值", + "description": "激活此触发器所需的最小相似度分数(0-1)。" + }, + "actions": { + "label": "触发器操作", + "description": "触发器匹配时要执行的操作列表(通知、sub_label、属性)。" + } + }, + "description": "语义搜索设置,用于构建和查询目标嵌入以查找相似项目。" + }, + "lpr": { + "label": "车牌识别", + "description": "车牌识别设置,包括检测阈值、格式化和已知车牌。", + "enabled": { + "label": "开启车牌识别", + "description": "在此摄像头上启用或禁用车牌识别。" + }, + "min_area": { + "label": "最小车牌区域", + "description": "尝试识别所需的最小车牌区域(像素)。" + }, + "enhancement": { + "label": "增强级别", + "description": "在 OCR 之前应用于车牌裁剪的增强级别(0-10);较高的值可能不总是改善结果,5 以上的级别可能仅适用于夜间车牌,应谨慎使用。" + }, + "expire_time": { + "label": "过期秒数", + "description": "未见到的车牌从追踪器中过期的时间(秒)(仅适用于专用 LPR 摄像头)。" + } + }, + "onvif": { + "label": "ONVIF", + "description": "此摄像头的 ONVIF 连接和 PTZ 自动追踪设置。", + "host": { + "label": "ONVIF 主机", + "description": "此摄像头 ONVIF 服务的主机(和可选协议)。" + }, + "port": { + "label": "ONVIF 端口", + "description": "ONVIF 服务的端口号。" + }, + "user": { + "label": "ONVIF 用户名", + "description": "ONVIF 身份验证的用户名;某些设备需要管理员用户才能使用 ONVIF。" + }, + "password": { + "label": "ONVIF 密码", + "description": "ONVIF 身份验证的密码。" + }, + "tls_insecure": { + "label": "禁用 TLS 验证", + "description": "跳过 TLS 验证并禁用 ONVIF 的摘要认证(不安全;仅用于安全网络)。" + }, + "autotracking": { + "label": "自动追踪", + "description": "使用 PTZ 摄像头移动自动追踪移动目标并使其保持在画面中心。", + "enabled": { + "label": "开启自动追踪", + "description": "启用或禁用检测目标的自动 PTZ 摄像头追踪。" + }, + "calibrate_on_startup": { + "label": "启动时校准", + "description": "在启动时测量 PTZ 电机速度以提高追踪精度。Frigate 将在校准后用 movement_weights 更新配置。" + }, + "zooming": { + "label": "变焦模式", + "description": "控制变焦行为:disabled(仅平移/倾斜)、absolute(最兼容)或 relative(同时平移/倾斜/变焦)。" + }, + "zoom_factor": { + "label": "变焦因子", + "description": "控制追踪目标的变焦级别。数值越低保持更多场景可见;数值越高放大更近但可能丢失追踪。数值范围 0.1 到 0.75。" + }, + "track": { + "label": "追踪目标", + "description": "应触发自动追踪的目标类型列表。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入这些区域之一才能开始自动追踪。" + }, + "return_preset": { + "label": "返回预设", + "description": "追踪结束后返回的摄像头固件中配置的 ONVIF 预设名称。" + }, + "timeout": { + "label": "返回超时", + "description": "失去追踪后等待多少秒后将摄像头返回到预设位置。" + }, + "movement_weights": { + "label": "移动权重", + "description": "由摄像头校准自动生成的校准值。请勿手动修改。" + }, + "enabled_in_config": { + "label": "原始自动追踪状态", + "description": "用于追踪配置中是否启用自动追踪的内部字段。" + } + }, + "ignore_time_mismatch": { + "label": "忽略时间不匹配", + "description": "忽略 ONVIF 通信中摄像头和 Frigate 服务器之间的时间同步差异。" + }, + "profile": { + "label": "ONVIF 配置文件", + "description": "用于 PTZ 控制的指定 ONVIF 媒体配置,将通过 Token 或名称匹配。如果未手动指定,将自动选择第一个包含有效 PTZ 配置的媒体配置。" + } + }, + "ui": { + "label": "摄像头页面", + "description": "此摄像头在页面中的显示顺序和可见性。显示顺序仅影响默认仪表板。如需更精细的控制,请使用“摄像头组”。", + "order": { + "label": "UI 顺序", + "description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。" + }, + "dashboard": { + "label": "在页面中显示", + "description": "切换此摄像头在 Frigate 页面的所有位置是否可见。禁用此项将需要手动编辑配置才能在页面中再次查看此摄像头。" + }, + "review": { + "label": "在核查中显示", + "description": "切换该摄像头是否在核查页面可见(包含核查页、摄像头筛选栏、画面变动核查与历史视图)。" + } + }, + "best_image_timeout": { + "label": "最佳图像超时", + "description": "等待具有最高置信度分数的图像的时间。" + }, + "type": { + "label": "摄像头类型", + "description": "摄像头类型" + }, + "webui_url": { + "label": "摄像头 URL", + "description": "从系统页面直接访问摄像头管理后台的 URL" + }, + "zones": { + "label": "区域", + "description": "区域允许您定义帧的特定区域,以便确定目标是否在特定区域内。", + "friendly_name": { + "label": "区域名称", + "description": "区域的友好名称,显示在 Frigate 页面中。如果未设置,将使用区域名称的格式化版本。" + }, + "enabled": { + "label": "开启", + "description": "开启或关闭此区域。禁用的区域在运行时将被忽略。" + }, + "enabled_in_config": { + "label": "保持区域原始状态的跟踪。" + }, + "filters": { + "label": "区域过滤器", + "description": "应用于此区域内目标的过滤器。用于减少误报或限制哪些目标被认为存在于区域内。", + "min_area": { + "label": "最小目标区域", + "description": "此目标类型所需的最小边界框区域(像素或百分比)。可以是像素(整数)或百分比(0.000001 到 0.99 之间的浮点数)。" + }, + "max_area": { + "label": "最大目标区域", + "description": "此目标类型允许的最大边界框区域(像素或百分比)。可以是像素(整数)或百分比(0.000001 到 0.99 之间的浮点数)。" + }, + "min_ratio": { + "label": "最小纵横比", + "description": "边界框所需的最小宽高比。" + }, + "max_ratio": { + "label": "最大纵横比", + "description": "边界框允许的最大宽高比。" + }, + "threshold": { + "label": "置信度阈值", + "description": "目标被视为真正阳性所需的平均检测置信度阈值。" + }, + "min_score": { + "label": "最小置信度", + "description": "目标被计入所需的最小单帧检测置信度。" + }, + "mask": { + "label": "过滤器遮罩", + "description": "定义此过滤器在帧内应用位置的多边形坐标。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "coordinates": { + "label": "坐标", + "description": "定义区域区域的多边形坐标。可以是逗号分隔的字符串或坐标字符串列表。坐标应该是相对的(0-1)或绝对的(传统)。" + }, + "distances": { + "label": "真实世界距离", + "description": "区域四边形每边的可选真实世界距离,用于速度或距离计算。如果设置,必须恰好有 4 个值。" + }, + "inertia": { + "label": "惯性帧数", + "description": "目标必须在区域内被连续检测多少帧才能被认为存在。有助于过滤掉短暂检测。" + }, + "loitering_time": { + "label": "徘徊秒数", + "description": "目标必须在区域内停留多少秒才能被视为徘徊。设置为 0 可禁用徘徊检测。" + }, + "speed_threshold": { + "label": "最小速度", + "description": "目标被认为存在于区域所需的最小速度(如果设置了距离,则为真实世界单位)。用于基于速度的区域触发器。" + }, + "objects": { + "label": "触发目标", + "description": "可以触发此区域的目标类型列表(来自标签映射)。可以是字符串或字符串列表。如果为空,则考虑所有目标。" + } + }, + "enabled_in_config": { + "label": "原始摄像头状态", + "description": "保持摄像头的原始状态跟踪。" + }, + "profiles": { + "label": "配置模板", + "description": "可在运行时切换指定命名的配置模板,支持局部覆盖参数。" + } +} diff --git a/web/public/locales/zh-CN/config/global.json b/web/public/locales/zh-CN/config/global.json new file mode 100644 index 0000000..96f45f9 --- /dev/null +++ b/web/public/locales/zh-CN/config/global.json @@ -0,0 +1,2313 @@ +{ + "version": { + "label": "当前配置版本", + "description": "用于标识当前生效配置的版本号(数字或字符串均可),帮助识别配置迁移或格式是否发生变更。" + }, + "safe_mode": { + "label": "安全模式", + "description": "开启后,Frigate 将以安全模式启动,将会关闭部分功能,以便排查问题。" + }, + "environment_vars": { + "label": "环境变量", + "description": "用于在 Home Assistant OS 中为 Frigate 进程设置的环境变量。非 HAOS 用户不能使用该配置项,而必须使用 Docker 的环境变量配置。" + }, + "logger": { + "label": "日志", + "description": "控制默认日志详细程度,以及各组件的日志级别覆盖。", + "default": { + "label": "日志等级", + "description": "默认全局日志详细程度(调试、信息、警告、错误)。" + }, + "logs": { + "label": "单进程日志级别", + "description": "按组件覆盖日志级别配置,用于提高或降低特定模块的日志详细程度。" + } + }, + "audio": { + "label": "音频检测", + "enabled": { + "label": "开启音频检测", + "description": "为所有摄像头启用或禁用音频事件检测;可按摄像头覆盖。" + }, + "max_not_heard": { + "label": "结束超时", + "description": "在结束音频事件之前,未检测到配置的音频类型的秒数。" + }, + "num_threads": { + "label": "检测线程", + "description": "用于音频检测处理的线程数量。" + }, + "description": "所有摄像头的基于音频的事件检测设置;可按摄像头覆盖。", + "min_volume": { + "label": "最小音量", + "description": "运行音频检测所需的最小 RMS 音量阈值;数值越低灵敏度越高(例如 200 高灵敏度,500 中等,1000 低灵敏度)。" + }, + "listen": { + "label": "监听类型", + "description": "要检测的音频事件类型列表(例如:bark、fire_alarm、speech、yell)。" + }, + "filters": { + "label": "音频过滤器", + "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。", + "threshold": { + "label": "最低音频置信度", + "description": "设置音频事件所需的最低置信度阈值。" + } + }, + "enabled_in_config": { + "label": "原始音频状态", + "description": "指示原始静态配置文件中是否开启了音频检测。" + } + }, + "auth": { + "cookie_secure": { + "label": "安全 Cookie 标志", + "description": "在身份验证 Cookie 上设置安全标志;使用 TLS 时应启用此选项。" + }, + "label": "身份验证", + "description": "身份验证和会话相关设置,包括 Cookie 和速率限制选项。", + "enabled": { + "label": "开启身份验证", + "description": "为 Frigate 页面开启原生身份验证。" + }, + "reset_admin_password": { + "label": "重置管理员密码", + "description": "开启后,启动时将重置管理员用户密码,并在日志中打印新密码。" + }, + "cookie_name": { + "label": "JWT Cookie 名称", + "description": "用于存储原生身份验证 JWT 令牌的 Cookie 名称。" + }, + "session_length": { + "label": "会话时长", + "description": "基于 JWT 的会话持续时间(秒)。" + }, + "refresh_time": { + "label": "会话刷新窗口", + "description": "当会话距离过期时间在此秒数范围内时,将会话刷新回完整时长。" + }, + "failed_login_rate_limit": { + "label": "登录失败限制", + "description": "用于限制登录失败尝试次数的规则,以减少暴力破解攻击。" + }, + "trusted_proxies": { + "label": "受信任的代理", + "description": "用于确定客户端 IP 以进行速率限制的受信任代理 IP 列表。" + }, + "hash_iterations": { + "label": "哈希迭代次数", + "description": "对用户密码进行哈希处理时使用的 PBKDF2-SHA256 迭代次数。" + }, + "roles": { + "label": "权限组映射", + "description": "将权限组映射到摄像头列表。空列表表示该权限组可以访问所有摄像头。" + }, + "admin_first_time_login": { + "label": "管理员首次登录标志", + "description": "启用后,UI 可能会在登录页面显示帮助链接,告知用户如何在管理员密码重置后登录。 " + } + }, + "audio_transcription": { + "label": "音频转录", + "description": "用于事件和实时字幕的实时和语音音频转录设置。", + "live_enabled": { + "label": "实时监控转写", + "description": "在接收到音频时开启实时监控持续转写。" + }, + "enabled": { + "label": "开启音频转录", + "description": "为所有摄像头启用或禁用自动音频转录;可按摄像头覆盖。" + }, + "language": { + "label": "转录语言", + "description": "用于转录/翻译的语言代码(例如 'en' 表示英语)。请参阅 https://whisper-api.com/docs/languages/ 了解支持的语言代码。" + }, + "device": { + "label": "转录设备", + "description": "运行转录模型的设备密钥(CPU/GPU)。目前仅支持 NVIDIA CUDA GPU 进行转录。" + }, + "model_size": { + "label": "模型大小", + "description": "用于离线音频事件转录的模型大小。" + } + }, + "birdseye": { + "label": "鸟瞰图", + "description": "将多路摄像头画面合并为统一布局的鸟瞰合成视图设置。", + "enabled": { + "label": "开启鸟瞰图", + "description": "开启或关闭鸟瞰图功能。" + }, + "mode": { + "label": "追踪模式", + "description": "在鸟瞰视图中包含摄像头的模式有:“基于目标”、“基于画面变动”或“连续”。" + }, + "order": { + "label": "排序位置", + "description": "用于控制摄像头在鸟瞰视图布局中排序位置的数值。" + }, + "restream": { + "label": "转发 RTSP", + "description": "将鸟瞰图输出作为 RTSP 流重新转发;启用此功能将使鸟瞰图持续运行。" + }, + "width": { + "label": "宽度", + "description": "合成的鸟瞰帧的输出宽度(像素)。" + }, + "height": { + "label": "高度", + "description": "合成的鸟瞰帧的输出高度(像素)。" + }, + "quality": { + "label": "编码质量", + "description": "鸟瞰图 mpeg1 流的编码质量(1 最高质量,31 最低)。" + }, + "inactivity_threshold": { + "label": "非活动阈值", + "description": "摄像头停止在鸟瞰图中显示的非活动秒数。" + }, + "layout": { + "label": "布局", + "description": "鸟瞰图合成的布局选项。", + "scaling_factor": { + "label": "缩放因子", + "description": "布局计算器使用的缩放因子(范围 1.0 到 5.0)。" + }, + "max_cameras": { + "label": "最大摄像头数", + "description": "鸟瞰图中同时显示的最大摄像头数量;显示最近的摄像头。" + } + }, + "idle_heartbeat_fps": { + "label": "空闲心跳 FPS", + "description": "空闲时重新发送最后一个合成鸟瞰帧的每秒帧数;设为 0 则禁用。" + } + }, + "detect": { + "label": "目标检测", + "description": "用于运行目标检测、初始化追踪器的检测模块设置。", + "enabled": { + "label": "开启目标检测", + "description": "为所有摄像头启用或禁用目标检测,可按摄像头覆盖。" + }, + "height": { + "label": "检测画面高度", + "description": "用于配置检测流的画面高度(像素);留空则使用原始视频流分辨率。" + }, + "width": { + "label": "检测画面宽度", + "description": "用于配置检测流的画面宽度(像素);留空则使用原始视频流分辨率。" + }, + "fps": { + "label": "检测帧率", + "description": "检测时希望使用的帧率;数值越低,CPU 占用越小(推荐值为 5,仅在追踪极高速运动的目标时才设置更高数值,最高不建议超过 10)。" + }, + "min_initialized": { + "label": "最小初始化帧数", + "description": "创建追踪目标前,需要连续检测到目标的次数。数值越大,错误触发的追踪越少。默认值为帧率除以 2。" + }, + "max_disappeared": { + "label": "最大消失帧数", + "description": "追踪目标在连续多少帧未被检测到时,将被判定为已消失。" + }, + "stationary": { + "label": "静止目标配置", + "description": "用于检测和管理长时间静止目标的相关设置。", + "interval": { + "label": "静止间隔", + "description": "设置每隔多少帧执行一次检测,用于确认目标是否处于静止状态。" + }, + "threshold": { + "label": "静止阈值", + "description": "目标需要连续多少帧位置不变,才会被标记为静止状态。" + }, + "max_frames": { + "label": "最大帧数", + "description": "限制静止目标最大追踪时长(以帧数为单位),超过将会停止追踪。", + "default": { + "label": "默认最大帧数", + "description": "停止追踪前,用于追踪静止目标的默认最大帧数。" + }, + "objects": { + "label": "目标最大帧数", + "description": "可对不同类型目标分别设置静止追踪的最大帧数(覆盖全局设置)。" + } + }, + "classifier": { + "label": "开启视觉分类器", + "description": "使用视觉分类器,即使检测框有轻微抖动,也能准确判断物体是否为静止。" + } + }, + "annotation_offset": { + "label": "标记偏移量", + "description": "检测标记的时间偏移量(毫秒),用于让时间轴上的检测框与录像画面更精准对齐;可设置为正数或负数。" + } + }, + "face_recognition": { + "label": "人脸识别", + "enabled": { + "label": "开启人脸识别", + "description": "为所有摄像头启用或禁用人脸识别;可按摄像头覆盖。" + }, + "min_area": { + "label": "最小人脸区域", + "description": "需要尝试进行人脸识别的人脸检测框最小大小(像素)。" + }, + "description": "所有摄像头的人脸检测和识别设置;可按摄像头覆盖。", + "model_size": { + "label": "模型大小", + "description": "用于人脸嵌入的模型大小(小型/大型);较大的可能需要 GPU。" + }, + "unknown_score": { + "label": "未知分数阈值", + "description": "低于此距离阈值的人脸被视为潜在匹配(数值越高越严格)。" + }, + "detection_threshold": { + "label": "检测阈值", + "description": "将人脸检测视为有效所需的最小检测置信度。" + }, + "recognition_threshold": { + "label": "识别阈值", + "description": "将两张人脸视为匹配的人脸嵌入距离阈值。" + }, + "min_faces": { + "label": "最小人脸数", + "description": "在将识别的子标签应用于人员之前所需的最小人脸识别次数。" + }, + "save_attempts": { + "label": "保存尝试", + "description": "为最近识别 UI 保留的人脸识别尝试次数。" + }, + "blur_confidence_filter": { + "label": "模糊置信度过滤器", + "description": "根据图像模糊程度调整置信度分数,以减少低质量人脸的误报。" + }, + "device": { + "label": "设备", + "description": "这是一个覆盖选项,用于指定特定设备。请参阅 https://onnxruntime.ai/docs/execution-providers/ 了解更多信息" + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg 编解码相关设置,包含可执行文件路径、命令行参数、硬件加速选项,以及按不同功能划分的输出参数。", + "path": { + "label": "FFmpeg 路径", + "description": "要使用的 FFmpeg 可执行文件路径,或版本别名(如 \"7.0\" 或 \"8.0\")。" + }, + "global_args": { + "label": "FFmpeg 全局参数", + "description": "传递给 FFmpeg 进程的全局参数。" + }, + "hwaccel_args": { + "label": "硬件加速参数", + "description": "用于 FFmpeg 的硬件加速参数。建议使用对应硬件厂商的预设配置。" + }, + "input_args": { + "label": "输入参数", + "description": "应用于 FFmpeg 输入视频流的输入参数。" + }, + "output_args": { + "label": "输出参数", + "description": "用于不同 FFmpeg 功能(如检测、录制)的默认输出参数。", + "detect": { + "label": "检测输出参数", + "description": "检测功能视频流的默认输出参数。" + }, + "record": { + "label": "录制输出参数", + "description": "录制功能视频流的默认输出参数。" + } + }, + "retry_interval": { + "label": "FFmpeg 重试时间", + "description": "摄像头视频流异常断开后,重新连接前的等待时间。默认为 10 秒。" + }, + "apple_compatibility": { + "label": "Apple 兼容性", + "description": "录制 H.265 视频时启用 HEVC 标记,以提升对 Apple 设备播放的兼容性。" + }, + "gpu": { + "label": "GPU 索引", + "description": "在启用硬件加速时,默认使用的 GPU 索引。" + }, + "inputs": { + "label": "摄像头输入视频流", + "description": "该摄像头的所有输入流配置列表(包含路径和功能)。", + "path": { + "label": "输入路径", + "description": "摄像头输入视频流的地址或路径。" + }, + "roles": { + "label": "输入流功能", + "description": "定义该视频流的功能。" + }, + "global_args": { + "label": "FFmpeg 全局参数", + "description": "该输入视频流使用的 FFmpeg 全局通用参数。" + }, + "hwaccel_args": { + "label": "硬件加速参数", + "description": "该输入视频流的硬件加速参数。" + }, + "input_args": { + "label": "输入参数", + "description": "该视频流特定的输入参数。" + } + } + }, + "database": { + "label": "数据库", + "description": "Frigate 用于存储追踪目标和录像元数据的 SQLite 数据库设置。", + "path": { + "label": "数据库路径", + "description": "Frigate SQLite 数据库文件的存储路径。" + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "集成的 go2rtc 转发服务设置,用于实时监控流转发和转码。" + }, + "mqtt": { + "label": "MQTT", + "description": "连接到 MQTT 代理并发布遥测数据、快照和事件详情的设置。", + "enabled": { + "label": "开启 MQTT", + "description": "启用或禁用 MQTT 集成,用于状态、事件和快照。" + }, + "host": { + "label": "MQTT 主机", + "description": "MQTT 代理的主机名或 IP 地址。" + }, + "port": { + "label": "MQTT 端口", + "description": "MQTT 代理的端口(普通 MQTT 通常为 1883)。" + }, + "topic_prefix": { + "label": "主题前缀", + "description": "所有 Frigate 主题的 MQTT 主题前缀;如果运行多个实例,必须唯一。" + }, + "client_id": { + "label": "客户端 ID", + "description": "连接到 MQTT 代理时使用的客户端标识符;每个实例应该唯一。" + }, + "stats_interval": { + "label": "统计信息间隔", + "description": "向 MQTT 发布系统和摄像头统计信息的时间间隔(秒)。" + }, + "user": { + "label": "MQTT 用户名", + "description": "可选的 MQTT 用户名;可以通过环境变量或密钥提供。" + }, + "password": { + "label": "MQTT 密码", + "description": "可选的 MQTT 密码;可以通过环境变量或密钥提供。" + }, + "tls_ca_certs": { + "label": "TLS CA 证书", + "description": "用于 TLS 连接到代理的 CA 证书路径(用于自签名证书)。" + }, + "tls_client_cert": { + "label": "客户端证书", + "description": "TLS 双向认证的客户端证书路径;使用客户端证书时不要设置用户名/密码。" + }, + "tls_client_key": { + "label": "客户端密钥", + "description": "客户端证书的私钥路径。" + }, + "tls_insecure": { + "label": "TLS 不安全连接", + "description": "通过跳过主机名验证允许不安全的 TLS 连接(不推荐)。" + }, + "qos": { + "label": "MQTT QoS", + "description": "MQTT 发布/订阅的服务质量级别(0、1 或 2)。" + } + }, + "notifications": { + "label": "通知", + "description": "为所有摄像头启用和控制通知的设置;可按摄像头覆盖。", + "enabled": { + "label": "开启通知", + "description": "为所有摄像头启用或禁用通知;可按摄像头覆盖。" + }, + "email": { + "label": "通知邮箱", + "description": "用于推送通知或某些通知提供商要求的邮箱地址。" + }, + "cooldown": { + "label": "冷却时间", + "description": "通知之间的冷却时间(秒),以避免向收件人发送垃圾信息。" + }, + "enabled_in_config": { + "label": "原始通知状态", + "description": "指示原始静态配置中是否启用了通知。" + } + }, + "networking": { + "label": "网络", + "description": "网络相关设置,如 Frigate 端点的 IPv6 启用。", + "ipv6": { + "label": "IPv6 配置", + "description": "Frigate 网络服务的 IPv6 特定设置。", + "enabled": { + "label": "开启 IPv6", + "description": "在适用的情况下为 Frigate 服务(API 和 UI)启用 IPv6 支持。" + } + }, + "listen": { + "label": "监听端口配置", + "description": "内部和外部监听端口的配置。此选项适用于高级用户。对于大多数用例,建议在 Docker compose 文件的 ports 部分进行更改。", + "internal": { + "label": "内部端口", + "description": "Frigate 的内部监听端口(默认 5000)。" + }, + "external": { + "label": "外部端口", + "description": "Frigate 的外部监听端口(默认 8971)。" + } + } + }, + "proxy": { + "label": "代理", + "description": "用于将 Frigate 集成到传递已认证用户头的反向代理后面的设置。", + "header_map": { + "label": "请求头映射", + "description": "将传入的代理请求头映射到 Frigate 用户和权限组字段,用于基于代理的身份验证。", + "user": { + "label": "用户请求头", + "description": "包含上游代理提供的已认证用户名的请求头。" + }, + "role": { + "label": "权限组请求头", + "description": "包含来自上游代理的已认证用户权限组或用户组的请求头。" + }, + "role_map": { + "label": "权限组映射", + "description": "将上游组值映射到 Frigate 权限组(例如将管理员组映射到管理员权限组)。" + } + }, + "logout_url": { + "label": "登出 URL", + "description": "通过代理登出时重定向用户的 URL。" + }, + "auth_secret": { + "label": "代理密钥", + "description": "与 X-Proxy-Secret 请求头进行比对的可选密钥,用于验证受信任的代理。" + }, + "default_role": { + "label": "默认权限组", + "description": "当没有权限组映射适用时分配给代理认证用户的默认权限组。" + }, + "separator": { + "label": "分隔符", + "description": "用于分割代理请求头中多个值的字符。" + } + }, + "telemetry": { + "label": "遥测", + "description": "系统遥测和统计选项,包括 GPU 和网络带宽监控。", + "network_interfaces": { + "label": "网络接口", + "description": "要监控带宽统计信息的网络接口名称前缀列表。" + }, + "stats": { + "label": "系统统计", + "description": "用于启用/禁用各种系统和 GPU 统计信息收集的选项。", + "amd_gpu_stats": { + "label": "AMD GPU 统计", + "description": "如果存在 AMD GPU,则启用 AMD GPU 统计信息收集。" + }, + "intel_gpu_stats": { + "label": "Intel GPU 统计", + "description": "如果存在 Intel GPU,则启用 Intel GPU 统计信息收集。" + }, + "network_bandwidth": { + "label": "网络带宽", + "description": "为摄像头 ffmpeg 进程和检测器启用按进程网络带宽监控(需要权限)。" + }, + "intel_gpu_device": { + "label": "Intel GPU 设备", + "description": "当系统存在多个 Intel 显卡时,用于将显卡运行数据绑定到指定设备的 PCI 总线地址或 DRM 设备路径(示例:/dev/dri/card1)。" + } + }, + "version_check": { + "label": "版本检查", + "description": "启用出站检查以检测是否有更新版本的 Frigate 可用。" + } + }, + "tls": { + "label": "TLS", + "description": "Frigate Web 端点(端口 8971)的 TLS 设置。", + "enabled": { + "label": "开启 TLS", + "description": "为 Frigate 的网页页面和 API 的端口开启 TLS 加密。" + } + }, + "ui": { + "label": "用户界面", + "description": "用户界面偏好设置,如时区、时间/日期格式和单位。", + "timezone": { + "label": "时区", + "description": "可选时区,用于整个界面展示时间(如果未设置,则默认为浏览器本地时间的时区)。" + }, + "time_format": { + "label": "时间格式", + "description": "页面中将使用的时间格式(浏览器、12小时制 或 24小时制)。" + }, + "date_style": { + "label": "日期样式", + "description": "页面中将使用的日期样式(完整、长、中等、短)。" + }, + "time_style": { + "label": "时间样式", + "description": "页面中将使用的时间样式(完整、长、中等、短)。" + }, + "unit_system": { + "label": "单位系统", + "description": "UI 和 MQTT 中使用的显示单位系统(公制或英制)。" + } + }, + "detectors": { + "label": "检测器硬件", + "description": "目标检测器(CPU、GPU、ONNX 后端)的配置以及任何检测器特定的模型设置。", + "type": { + "label": "类型", + "description": "用于目标检测的检测器类型(例如 'cpu'、'edgetpu'、'openvino')。" + }, + "cpu": { + "label": "CPU", + "description": "在主机 CPU 上运行 TensorFlow Lite 模型的 CPU TFLite 检测器,无硬件加速。不推荐使用。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "num_threads": { + "label": "检测线程数", + "description": "用于基于 CPU 的推理的线程数。" + } + }, + "deepstack": { + "label": "DeepStack", + "description": "将图像发送到远程 DeepStack HTTP API 进行推理的 DeepStack/CodeProject.AI 检测器。不推荐使用。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "api_url": { + "label": "DeepStack API URL", + "description": "DeepStack API 的 URL。" + }, + "api_timeout": { + "label": "DeepStack API 超时时间(秒)", + "description": "DeepStack API 请求允许的最长时间。" + }, + "api_key": { + "label": "DeepStack API 密钥(如需要)", + "description": "用于认证 DeepStack 服务的可选 API 密钥。" + } + }, + "degirum": { + "label": "DeGirum", + "description": "通过 DeGirum 云或本地推理服务运行模型的 DeGirum 检测器。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchg'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "location": { + "label": "推理位置", + "description": "DeGirum 推理引擎的位置(例如 '@cloud'、'127.0.0.1')。" + }, + "zoo": { + "label": "模型库", + "description": "DeGirum 模型库的路径或 URL。" + }, + "token": { + "label": "DeGirum 云令牌", + "description": "用于 DeGirum 云访问的令牌。" + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "使用 EdgeTPU 委托运行为 Coral EdgeTPU 编译的 TensorFlow Lite 模型的 EdgeTPU 检测器。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "device": { + "label": "设备类型", + "description": "用于 EdgeTPU 推理的设备(例如 'usb'、'pci')。" + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "使用 HEF 模型和 HailoRT SDK 在 Hailo 硬件上进行推理的 Hailo-8/Hailo-8L 检测器。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "device": { + "label": "设备类型", + "description": "用于 Hailo 推理的设备(例如 'PCIe'、'M.2')。" + } + }, + "memryx": { + "label": "MemryX", + "description": "在 MemryX 加速器上运行编译的 DFP 模型的 MemryX MX3 检测器。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "device": { + "label": "设备路径", + "description": "用于 MemryX 推理的设备(例如 'PCIe')。" + } + }, + "onnx": { + "label": "ONNX", + "description": "运行 ONNX 模型的 ONNX 检测器;当可用时将使用可用的加速后端(CUDA/ROCm/OpenVINO)。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "device": { + "label": "设备类型", + "description": "用于 ONNX 推理的设备(例如 'AUTO'、'CPU'、'GPU')。" + } + }, + "openvino": { + "label": "OpenVINO", + "description": "适用于 AMD 和 Intel CPU、Intel GPU 和 Intel VPU 硬件的 OpenVINO 检测器。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "device": { + "label": "设备类型", + "description": "用于 OpenVINO 推理的设备(例如 'CPU'、'GPU'、'NPU')。" + } + }, + "rknn": { + "label": "RKNN", + "description": "用于 Rockchip NPU 的 RKNN 检测器;在 Rockchip 硬件上运行编译的 RKNN 模型。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "num_cores": { + "label": "使用的 NPU 核心数。", + "description": "要使用的 NPU 核心数(0 表示自动)。" + } + }, + "synaptics": { + "label": "Synaptics", + "description": "使用 Synap SDK 在 Synaptics 硬件上运行 .synap 格式模型的 Synaptics NPU 检测器。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + } + }, + "teflon_tfl": { + "label": "Teflon", + "description": "使用 Mesa Teflon 委托库在支持的 GPU 上加速推理的 TFLite Teflon 委托检测器。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + } + }, + "tensorrt": { + "label": "TensorRT", + "description": "使用序列化的 TensorRT 引擎进行加速推理的 Nvidia Jetson 设备 TensorRT 检测器。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "device": { + "label": "GPU 设备索引", + "description": "要使用的 GPU 设备索引。" + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "通过 ZeroMQ IPC 端点将推理卸载到外部进程的 ZMQ IPC 检测器。", + "type": { + "label": "类型" + }, + "model": { + "label": "检测器特定模型配置", + "description": "检测器特定的模型配置选项(路径、输入大小等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器特定模型路径", + "description": "如果所选检测器需要,则为检测器模型二进制文件的路径。" + }, + "endpoint": { + "label": "ZMQ IPC 端点", + "description": "要连接的 ZMQ 端点。" + }, + "request_timeout_ms": { + "label": "ZMQ 请求超时(毫秒)", + "description": "ZMQ 请求的超时时间(毫秒)。" + }, + "linger_ms": { + "label": "ZMQ 套接字逗留时间(毫秒)", + "description": "套接字逗留时间(毫秒)。" + } + }, + "axengine": { + "label": "爱芯元智 NPU", + "description": "AXERA AX650N/AX8850N NPU 检测器,通过 AXEngine 运行库加载并执行编译后的 .axmodel 模型文件。", + "type": { + "label": "类型" + } + }, + "model": { + "label": "检测器特定的模型配置", + "description": "检测器特定的模型配置选项(路径、输入尺寸等)。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或使用 plus:// 指定 Frigate+ 模型)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射(labelmap)", + "description": "检测器标签映射文件(labelmap)路径,用于将数字类别映射为文字标签。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量(input tensor)的宽度(以像素为单位)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量(input tensor)的高度(以像素为单位)。" + }, + "labelmap": { + "label": "标签映射(labelmap)自定义", + "description": "合并到标准标签映射表中的覆盖 / 重映射规则。" + }, + "attributes_map": { + "label": "目标标签到其属性标签的映射", + "description": "用于绑定元数据的目标标签 → 属性标签映射关系(例如:'car'→ ['license_plate'] 为将车牌属性绑定到车辆上)。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式(Tensor format):'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素颜色空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "检测器专用模型路径", + "description": "所选检测器需要时,需填写其模型文件的路径。" + } + }, + "model": { + "label": "检测模型", + "description": "用于配置自定义目标检测模型及其输入形状的设置。", + "path": { + "label": "自定义目标检测模型路径", + "description": "自定义检测模型文件的路径(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自定义目标检测器的标签映射", + "description": "将数字类别映射到检测器字符串标签的标签映射文件路径。" + }, + "width": { + "label": "目标检测模型输入宽度", + "description": "模型输入张量的宽度(像素)。" + }, + "height": { + "label": "目标检测模型输入高度", + "description": "模型输入张量的高度(像素)。" + }, + "labelmap": { + "label": "标签映射自定义", + "description": "要合并到标准标签映射中的覆盖或重映射条目。" + }, + "attributes_map": { + "label": "目标标签到属性标签的映射", + "description": "从目标标签到属性标签的映射,用于附加元数据(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型输入张量形状", + "description": "模型期望的张量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型输入像素颜色格式", + "description": "模型期望的像素色彩空间:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型输入数据类型", + "description": "模型输入张量的数据类型(例如 'float32')。" + }, + "model_type": { + "label": "目标检测模型类型", + "description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。" + } + }, + "genai": { + "label": "生成式 AI 配置", + "description": "用于生成目标描述和核查摘要的集成生成式 AI 提供商设置。", + "api_key": { + "label": "API 密钥", + "description": "某些提供商要求的 API 密钥(也可以通过环境变量设置)。" + }, + "base_url": { + "label": "基础 URL", + "description": "自托管或兼容提供商的基础 URL(例如 Ollama 实例)。" + }, + "model": { + "label": "模型", + "description": "用于生成描述或摘要的提供商模型。" + }, + "provider": { + "label": "提供商", + "description": "要使用的生成式 AI 提供商(例如:ollama、gemini、openai 等。国产大模型厂商可使用 openai 接口)。" + }, + "roles": { + "label": "功能", + "description": "生成式 AI 功能(对话、描述、嵌入);每个功能单独一个提供商。" + }, + "provider_options": { + "label": "提供商选项", + "description": "要传递给生成式 AI 客户端的、与服务提供商相关的额外配置项。" + }, + "runtime_options": { + "label": "运行时选项", + "description": "每次推理调用时传递给提供商的运行时选项。" + } + }, + "live": { + "label": "实时监控观看", + "description": "用于控制 JSMPEG 实时流分辨率与画质的设置。此设置不影响使用 go2rtc 进行实时预览的摄像头。", + "streams": { + "label": "实时监控流名称", + "description": "配置的流名称到用于实时监控播放的 restream/go2rtc 名称的映射。" + }, + "height": { + "label": "实时监控高度", + "description": "在网页页面中渲染 jsmpeg 实时监控流的高度(像素);必须小于等于检测流高度。" + }, + "quality": { + "label": "实时监控质量", + "description": "jsmpeg 流的编码质量(1 最高,31 最低)。" + } + }, + "motion": { + "label": "画面变动检测", + "description": "应用于摄像头的默认动作检测设置,除非按摄像头覆盖。", + "enabled": { + "label": "开启画面变动检测", + "description": "为所有摄像头启用或禁用动作检测;可按摄像头覆盖。" + }, + "threshold": { + "label": "画面变动阈值", + "description": "画面变动检测器使用的像素差异阈值;数值越高灵敏度越低(范围 1-255)。" + }, + "lightning_threshold": { + "label": "闪电阈值", + "description": "用于检测和忽略短暂闪电闪烁的阈值(数值越低越敏感,范围 0.3 到 1.0)。这不会完全阻止画面变动检测;只是当超过阈值时检测器会停止分析额外的帧。在此类事件期间仍会创建基于画面变动的录像。" + }, + "skip_motion_threshold": { + "label": "跳过画面变动阈值", + "description": "如果单帧中画面变化超过此比例,检测器将判定为无画面变动并立即重新校准。这可以节省 CPU 并减少闪电、风暴等情况下的误报,但也可能会错过真正的事件,如 PTZ 摄像头自动追踪目标。你需要权衡取舍:是否牺牲少量录制片段,换取更少无效视频与更低的误检。保持为空即可关闭该功能。" + }, + "improve_contrast": { + "label": "改善对比度", + "description": "在画面变动分析之前对帧应用对比度改善以帮助检测。" + }, + "contour_area": { + "label": "轮廓区域", + "description": "画面变动轮廓被计入所需的最小轮廓区域(像素)。" + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "用于画面变动计算的帧差异中使用的 alpha 混合因子。" + }, + "frame_alpha": { + "label": "画面 alpha 通道", + "description": "画面变动预处理时混合画面所使用的 alpha 值。" + }, + "frame_height": { + "label": "画面高度", + "description": "计算画面变动时缩放画面的高度(像素)。" + }, + "mask": { + "label": "遮罩坐标", + "description": "定义用于包含/排除区域的画面变动遮罩多边形的有序 x,y 坐标。" + }, + "mqtt_off_delay": { + "label": "MQTT 关闭延迟", + "description": "在发布 MQTT 'off' 状态之前,最后一次画面变动后等待的秒数。" + }, + "enabled_in_config": { + "label": "原始画面变动状态", + "description": "指示原始静态配置中是否启用了画面变动检测。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "objects": { + "label": "目标", + "description": "目标追踪默认设置,包括要追踪的标签和按目标的过滤器。", + "track": { + "label": "要追踪的目标", + "description": "所有摄像头要追踪的目标标签列表;可按摄像头覆盖。" + }, + "filters": { + "label": "目标过滤器", + "description": "应用于检测到的目标以减少误报的过滤器(区域、比例、置信度)。", + "min_area": { + "label": "最小目标区域", + "description": "此目标类型所需的最小边界框区域(像素或百分比)。可以是像素(整数)或百分比(0.000001 到 0.99 之间的浮点数)。" + }, + "max_area": { + "label": "最大目标区域", + "description": "此目标类型允许的最大边界框区域(像素或百分比)。可以是像素(整数)或百分比(0.000001 到 0.99 之间的浮点数)。" + }, + "min_ratio": { + "label": "最小纵横比", + "description": "边界框所需的最小宽高比。" + }, + "max_ratio": { + "label": "最大纵横比", + "description": "边界框允许的最大宽高比。" + }, + "threshold": { + "label": "置信度阈值", + "description": "目标被视为真正阳性所需的平均检测置信度阈值。" + }, + "min_score": { + "label": "最小置信度", + "description": "目标被计入所需的最小单帧检测置信度。" + }, + "mask": { + "label": "过滤器遮罩", + "description": "定义此过滤器在帧内应用位置的多边形坐标。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "mask": { + "label": "目标遮罩", + "description": "用于防止在指定区域进行目标检测的遮罩多边形。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "genai": { + "label": "生成式 AI 目标配置", + "description": "用于发送画面给生成式 AI 进行生成和描述追踪目标的选项。", + "enabled": { + "label": "开启生成式 AI", + "description": "默认开启生成式 AI 生成追踪目标的描述。" + }, + "use_snapshot": { + "label": "使用快照", + "description": "使用目标快照而不是缩略图给生成式 AI 进行描述生成。" + }, + "prompt": { + "label": "字幕提示", + "description": "使用生成式 AI 生成描述时使用的默认提示模板。" + }, + "object_prompts": { + "label": "目标提示", + "description": "按目标设置提示词,让生成式 AI 对不同标签的输出进行定制。" + }, + "objects": { + "label": "生成式 AI 目标", + "description": "默认发送给生成式 AI 的目标标签列表。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入这些区域,才会触发生成式 AI 描述生成。" + }, + "debug_save_thumbnails": { + "label": "保存缩略图", + "description": "保存发送给生成式 AI 的缩略图用于调试和核查。" + }, + "send_triggers": { + "label": "生成式 AI 触发器", + "description": "定义画面帧应在何时发送给生成式 AI(如检测结束时、更新后等)。", + "tracked_object_end": { + "label": "结束时发送", + "description": "目标追踪结束时向生成式 AI 发送请求。" + }, + "after_significant_updates": { + "label": "生成式 AI 提前触发", + "description": "在追踪目标发生指定次数的重要变化后,向生成式 AI 发送请求。" + } + }, + "enabled_in_config": { + "label": "原配置生成式 AI 状态", + "description": "表示在原始静态配置中是否已启用生成式 AI。" + } + }, + "filters_attribute": { + "label": "属性筛选", + "description": "对检测到的属性进行筛选,以此减少误报(包含面积、比例、置信度)。", + "min_area": { + "label": "最小属性区域", + "description": "该属性所需的最小边框框面积,支持像素值(整数)或百分比数值(范围 0.000001 至 0.99)。" + }, + "max_area": { + "label": "最大属性区域", + "description": "该属性所需的最大边框框面积,支持像素值(整数)或百分比数值(范围 0.000001 至 0.99)。" + }, + "min_ratio": { + "label": "最小纵横比", + "description": "边界框所需的最小宽高比。" + }, + "max_ratio": { + "label": "最大纵横比", + "description": "边界框允许的最大宽高比。" + }, + "threshold": { + "label": "置信度阈值", + "description": "判定该属性为有效目标所需的平均检测置信度阈值。" + }, + "min_score": { + "label": "最小置信度", + "description": "将该属性关联至所属父目标所需的单帧最低检测置信度。" + }, + "mask": { + "label": "过滤器遮罩", + "description": "定义此过滤器在帧内应用位置的多边形坐标。" + }, + "raw_mask": { + "label": "原始遮罩" + } + } + }, + "record": { + "label": "录像", + "description": "应用于摄像头的录像和保留设置,除非按摄像头覆盖。", + "enabled": { + "label": "开启录像", + "description": "为所有摄像头启用或禁用录像;可按摄像头覆盖。" + }, + "expire_interval": { + "label": "录像清理间隔", + "description": "清理过期录像片段的间隔分钟数。" + }, + "continuous": { + "label": "持续保留", + "description": "无论是否有追踪目标或动作,保留录像的天数。如果只想保留警报和检测的录像,请设置为 0。", + "days": { + "label": "保留天数", + "description": "保留录像的天数。" + } + }, + "motion": { + "label": "动作保留", + "description": "无论是否有追踪目标,由动作触发的录像保留天数。如果只想保留警报和检测的录像,请设置为 0。", + "days": { + "label": "保留天数", + "description": "保留录像的天数。" + } + }, + "detections": { + "label": "检测保留", + "description": "检测事件的录像保留设置,包括前后捕获时长。", + "pre_capture": { + "label": "前捕获秒数", + "description": "检测事件之前包含在录像中的秒数。" + }, + "post_capture": { + "label": "后捕获秒数", + "description": "检测事件之后包含在录像中的秒数。" + }, + "retain": { + "label": "事件保留", + "description": "检测事件录像的保留设置。", + "days": { + "label": "保留天数", + "description": "保留检测事件录像的天数。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(保存所有片段)、motion(保存有动作的片段)或 active_objects(保存有活动目标的片段)。" + } + } + }, + "alerts": { + "label": "警报保留", + "description": "警报事件的录像保留设置,包括前后捕获时长。", + "pre_capture": { + "label": "前捕获秒数", + "description": "检测事件之前包含在录像中的秒数。" + }, + "post_capture": { + "label": "后捕获秒数", + "description": "检测事件之后包含在录像中的秒数。" + }, + "retain": { + "label": "事件保留", + "description": "检测事件录像的保留设置。", + "days": { + "label": "保留天数", + "description": "保留检测事件录像的天数。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(保存所有片段)、motion(保存有动作的片段)或 active_objects(保存有活动目标的片段)。" + } + } + }, + "export": { + "label": "导出配置", + "description": "导出录像时使用的设置,如延时摄影和硬件加速。", + "hwaccel_args": { + "label": "导出硬件加速参数", + "description": "用于导出/转码操作的硬件加速参数。" + }, + "max_concurrent": { + "label": "最大并发导出数", + "description": "同时可处理的最大导出任务数量。" + }, + "chapters": { + "label": "导出的录制中嵌入元数据" + } + }, + "preview": { + "label": "预览配置", + "description": "控制界面中显示的录像预览质量的设置。", + "quality": { + "label": "预览质量", + "description": "预览质量级别(very_low、low、medium、high、very_high)。" + } + }, + "enabled_in_config": { + "label": "原始录像状态", + "description": "指示原始静态配置中是否启用了录像。" + } + }, + "review": { + "label": "核查", + "description": "控制界面与存储所使用的警报、检测和生成式 AI 核查总结的相关设置。", + "alerts": { + "label": "警报配置", + "description": "哪些追踪目标生成警报以及如何保留警报的设置。", + "enabled": { + "label": "开启警报", + "description": "为所有摄像头启用或禁用警报生成;可按摄像头覆盖。" + }, + "labels": { + "label": "警报标签", + "description": "符合警报条件的目标标签列表(例如:car、person)。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入才能被视为警报的区域;留空则允许任何区域。" + }, + "enabled_in_config": { + "label": "原始警报状态", + "description": "追踪原始静态配置中是否启用了警报。" + }, + "cutoff_time": { + "label": "警报截止时间", + "description": "在没有引起警报的活动后等待多少秒后截止警报。" + } + }, + "detections": { + "label": "检测配置", + "description": "用于设置哪些追踪目标会生成检测记录(非警报类),以及检测记录的保留方式。", + "enabled": { + "label": "开启检测", + "description": "为所有摄像头启用或禁用检测事件;可按摄像头覆盖。" + }, + "labels": { + "label": "检测标签", + "description": "符合检测事件条件的目标标签列表。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入才能被视为检测的区域;留空则允许任何区域。" + }, + "cutoff_time": { + "label": "检测截止时间", + "description": "在没有引起检测的活动后等待多少秒后截止检测。" + }, + "enabled_in_config": { + "label": "原始检测状态", + "description": "追踪原始静态配置中是否启用了检测。" + } + }, + "genai": { + "label": "生成式 AI 配置", + "description": "控制使用生成式 AI 为核查项生成描述和摘要。", + "enabled": { + "label": "开启生成式 AI 描述", + "description": "为核查项开启或关闭使用生成式 AI 生成描述和摘要。" + }, + "alerts": { + "label": "为警报开启生成式 AI", + "description": "使用生成式 AI 为警报项生成描述。" + }, + "detections": { + "label": "为检测开启生成式 AI", + "description": "使用生成式 AI 为检测项生成描述。" + }, + "image_source": { + "label": "核查图像来源", + "description": "发送给生成式 AI 的画面来源(“预览” 或 “录制”);“录制”将使用更高质量的画面帧,但会消耗更多的 token。" + }, + "additional_concerns": { + "label": "额外关注事项", + "description": "生成式 AI 在分析此摄像头的监控行为时,需要额外注意的事项或说明列表。" + }, + "debug_save_thumbnails": { + "label": "保存缩略图", + "description": "保存发送给生成式 AI 提供商的缩略图用于调试和核查。" + }, + "enabled_in_config": { + "label": "原配置生成式 AI 状态", + "description": "记录在静态配置中最初是否已启用生成式 AI 核查功能。" + }, + "preferred_language": { + "label": "首选语言", + "description": "向生成式 AI 提供商请求生成响应的首选语言。" + }, + "activity_context_prompt": { + "label": "活动上下文提示", + "description": "自定义提示词,用于说明可疑行为与非可疑行为的界定,为生成式 AI 生成摘要提供上下文依据。" + } + } + }, + "snapshots": { + "label": "快照", + "description": "所有摄像头的追踪目标 API 快照设置;可摄像头单独配置覆盖全局配置。", + "enabled": { + "label": "开启快照", + "description": "为所有摄像头启用或禁用保存快照;可按摄像头覆盖。" + }, + "clean_copy": { + "label": "保存干净副本", + "description": "除了带注释的快照外,还保存一份不带注释的干净快照副本。" + }, + "timestamp": { + "label": "时间戳叠加", + "description": "在 API 生成的快照上叠加时间戳。" + }, + "bounding_box": { + "label": "边界框叠加", + "description": "在 API 生成的快照上绘制追踪目标的边界框。" + }, + "crop": { + "label": "裁剪快照", + "description": "在 API 生成的快照裁剪到检测到的目标边界框。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入才能保存快照的区域。" + }, + "height": { + "label": "快照高度", + "description": "将 API 生成的快照调整到的目标高度(像素);留空则保持原始大小。" + }, + "retain": { + "label": "快照保留", + "description": "快照的保留设置,包括默认天数和按目标覆盖。", + "default": { + "label": "默认保留", + "description": "保留快照的默认天数。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(保存所有片段)、motion(保存有动作的片段)或 active_objects(保存有活动目标的片段)。" + }, + "objects": { + "label": "目标保留", + "description": "按目标覆盖的快照保留天数。" + } + }, + "quality": { + "label": "快照质量", + "description": "保存快照的编码质量(0-100)。" + } + }, + "timestamp_style": { + "label": "时间戳样式", + "description": "应用于调试视图和快照的帧内时间戳样式选项。", + "position": { + "label": "时间戳位置", + "description": "时间戳在图像上的位置(tl/tr/bl/br)。" + }, + "format": { + "label": "时间戳格式", + "description": "用于时间戳的日期时间格式字符串(Python 日期时间格式代码)。" + }, + "color": { + "label": "时间戳颜色", + "description": "时间戳文本的 RGB 颜色值(所有值 0-255)。", + "red": { + "label": "红色", + "description": "时间戳颜色的红色分量(0-255)。" + }, + "green": { + "label": "绿色", + "description": "时间戳颜色的绿色分量(0-255)。" + }, + "blue": { + "label": "蓝色", + "description": "时间戳颜色的蓝色分量(0-255)。" + } + }, + "thickness": { + "label": "时间戳粗细", + "description": "时间戳文本的线条粗细。" + }, + "effect": { + "label": "时间戳效果", + "description": "时间戳文本的视觉效果(none、solid、shadow)。" + } + }, + "classification": { + "label": "目标分类", + "description": "用于优化目标标签或状态分类的分类模型设置。", + "bird": { + "label": "鸟类分类配置", + "description": "鸟类分类模型特定的设置。", + "enabled": { + "label": "鸟类分类", + "description": "启用或禁用鸟类分类。" + }, + "threshold": { + "label": "最小分数", + "description": "接受鸟类分类所需的最小分类分数。" + } + }, + "custom": { + "label": "自定义分类模型", + "description": "用于目标或状态检测的自定义分类模型配置。", + "enabled": { + "label": "开启模型", + "description": "启用或禁用自定义分类模型。" + }, + "name": { + "label": "模型名称", + "description": "要使用的自定义分类模型的标识符。" + }, + "threshold": { + "label": "分数阈值", + "description": "用于更改分类状态的分数阈值。" + }, + "save_attempts": { + "label": "保存尝试", + "description": "为最近分类 UI 保存多少次分类尝试。" + }, + "object_config": { + "objects": { + "label": "分类目标", + "description": "要运行目标分类的目标类型列表。" + }, + "classification_type": { + "label": "分类类型", + "description": "应用的分类类型:'sub_label'(添加 sub_label)或其他支持的类型。" + } + }, + "state_config": { + "cameras": { + "label": "分类摄像头", + "description": "用于运行状态分类的按摄像头裁剪和设置。", + "crop": { + "label": "分类裁剪", + "description": "用于在此摄像头上运行分类的裁剪坐标。" + } + }, + "motion": { + "label": "动作时运行", + "description": "启用后,当在指定裁剪区域内检测到动作时运行分类。" + }, + "interval": { + "label": "分类间隔", + "description": "状态分类的定期分类运行间隔(秒)。" + } + } + } + }, + "semantic_search": { + "label": "语义搜索", + "description": "用于构建和查询目标嵌入以查找相似项的语义搜索设置。", + "enabled": { + "label": "开启语义搜索", + "description": "启用或禁用语义搜索功能。" + }, + "reindex": { + "label": "启动时重建索引", + "description": "触发将历史追踪目标完全重新索引到嵌入数据库。" + }, + "model": { + "label": "语义搜索模型或生成式 AI 服务名称", + "description": "用于语义搜索的嵌入模型(例如 'jinav1'),或具有嵌入功能(embeddings)的生成式 AI 服务名称。" + }, + "model_size": { + "label": "模型大小", + "description": "选择模型大小;“小型”模型一般在 CPU 上运行,而“大型”模型通常需要 GPU。" + }, + "device": { + "label": "设备", + "description": "这是一个覆盖选项,用于指定特定设备。请参阅 https://onnxruntime.ai/docs/execution-providers/ 了解更多信息" + }, + "triggers": { + "label": "触发器", + "description": "摄像头特定语义搜索触发器的操作和匹配条件。", + "friendly_name": { + "label": "友好名称", + "description": "可选友好名称,用于在界面上为触发器显示此名称。" + }, + "enabled": { + "label": "开启此触发器", + "description": "启用或禁用此语义搜索触发器。" + }, + "type": { + "label": "触发器类型", + "description": "触发器类型:'thumbnail'(与图像匹配)或 'description'(与文本匹配)。" + }, + "data": { + "label": "触发器内容", + "description": "要与追踪目标匹配的文本短语或缩略图 ID。" + }, + "threshold": { + "label": "触发器阈值", + "description": "激活此触发器所需的最小相似度分数(0-1)。" + }, + "actions": { + "label": "触发器操作", + "description": "触发器匹配时要执行的操作列表(通知、sub_label、属性)。" + } + } + }, + "lpr": { + "label": "车牌识别", + "description": "车牌识别设置,包括检测阈值、格式化和已知车牌。", + "enabled": { + "label": "开启车牌识别", + "description": "为所有摄像头启用或禁用车牌识别;可按摄像头覆盖。" + }, + "model_size": { + "label": "模型大小", + "description": "用于文本检测/识别的模型大小,大多数用户应使用“小型”模型,而且只有“小型”模型支持中文车牌。" + }, + "detection_threshold": { + "label": "检测阈值", + "description": "开始对疑似车牌运行 OCR 的检测置信度阈值。" + }, + "min_area": { + "label": "最小车牌区域", + "description": "尝试识别所需的最小车牌区域(像素)。" + }, + "recognition_threshold": { + "label": "识别阈值", + "description": "识别的车牌文本作为子标签附加所需的置信度阈值。" + }, + "min_plate_length": { + "label": "最小车牌长度", + "description": "识别的车牌被视为有效所需的最小字符数。" + }, + "format": { + "label": "车牌格式正则", + "description": "用于验证识别的车牌字符串是否符合预期格式的可选正则表达式。" + }, + "match_distance": { + "label": "匹配距离", + "description": "将检测到的车牌与已知车牌比较时允许的字符不匹配数。" + }, + "known_plates": { + "label": "已知车牌", + "description": "要特别追踪或报警的车牌或正则表达式列表。" + }, + "enhancement": { + "label": "增强级别", + "description": "在 OCR 之前应用于车牌裁剪的增强级别(0-10);较高的值可能不总是改善结果,5 以上的级别可能仅适用于夜间车牌,应谨慎使用。" + }, + "debug_save_plates": { + "label": "保存调试车牌", + "description": "保存车牌裁剪图像用于调试 LPR 性能。" + }, + "device": { + "label": "设备", + "description": "这是一个覆盖选项,用于指定特定设备。请参阅 https://onnxruntime.ai/docs/execution-providers/ 了解更多信息" + }, + "replace_rules": { + "label": "替换规则", + "description": "用于在匹配之前规范化检测到的车牌字符串的正则替换规则。", + "pattern": { + "label": "正则模式" + }, + "replacement": { + "label": "替换字符串" + } + }, + "expire_time": { + "label": "过期秒数", + "description": "未见到的车牌从追踪器中过期的时间(秒)(仅适用于专用 LPR 摄像头)。" + } + }, + "camera_groups": { + "label": "摄像头分组", + "description": "用于在页面中组织摄像头的命名摄像头分组配置。", + "cameras": { + "label": "摄像头列表", + "description": "此分组中包含的摄像头名称数组。" + }, + "icon": { + "label": "分组图标", + "description": "在页面中代表摄像头分组的图标。" + }, + "order": { + "label": "排序顺序", + "description": "用于在页面中对摄像头分组进行排序的数字顺序;数值越大越靠后。" + } + }, + "camera_mqtt": { + "label": "MQTT", + "description": "MQTT 图像发布设置。", + "enabled": { + "label": "发送图像", + "description": "为此摄像头启用将目标快照图像发布到 MQTT 主题。" + }, + "timestamp": { + "label": "添加时间戳", + "description": "在发布到 MQTT 的图像上叠加时间戳。" + }, + "bounding_box": { + "label": "添加边界框", + "description": "在通过 MQTT 发布的图像上绘制边界框。" + }, + "crop": { + "label": "裁剪图像", + "description": "将发布到 MQTT 的图像裁剪到检测到的目标边界框。" + }, + "height": { + "label": "图像高度", + "description": "通过 MQTT 发布的图像调整到的目标高度(像素)。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入才能发布 MQTT 图像的区域。" + }, + "quality": { + "label": "JPEG 质量", + "description": "发布到 MQTT 的图像的 JPEG 质量(0-100)。" + } + }, + "camera_ui": { + "label": "摄像头页面", + "description": "此摄像头在页面中的显示顺序和可见性。显示顺序仅影响默认仪表板。如需更精细的控制,请使用“摄像头组”。", + "order": { + "label": "UI 顺序", + "description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。" + }, + "dashboard": { + "label": "在页面中显示", + "description": "切换此摄像头在 Frigate 页面中是否可见。禁用后需要手动编辑配置才能再次在页面中查看此摄像头。" + }, + "review": { + "label": "在核查中显示", + "description": "切换该摄像头是否在核查页面可见(包含核查页、摄像头筛选栏、画面变动核查与历史视图)。" + } + }, + "onvif": { + "label": "ONVIF", + "description": "此摄像头的 ONVIF 连接和 PTZ 自动追踪设置。", + "host": { + "label": "ONVIF 主机", + "description": "此摄像头 ONVIF 服务的主机(和可选协议)。" + }, + "port": { + "label": "ONVIF 端口", + "description": "ONVIF 服务的端口号。" + }, + "user": { + "label": "ONVIF 用户名", + "description": "ONVIF 身份验证的用户名;某些设备需要管理员用户才能使用 ONVIF。" + }, + "password": { + "label": "ONVIF 密码", + "description": "ONVIF 身份验证的密码。" + }, + "tls_insecure": { + "label": "禁用 TLS 验证", + "description": "跳过 TLS 验证并禁用 ONVIF 的摘要认证(不安全;仅用于安全网络)。" + }, + "autotracking": { + "label": "自动追踪", + "description": "使用 PTZ 摄像头移动自动追踪移动目标并使其保持在画面中心。", + "enabled": { + "label": "开启自动追踪", + "description": "启用或禁用检测目标的自动 PTZ 摄像头追踪。" + }, + "calibrate_on_startup": { + "label": "启动时校准", + "description": "在启动时测量 PTZ 电机速度以提高追踪精度。Frigate 将在校准后用 movement_weights 更新配置。" + }, + "zooming": { + "label": "变焦模式", + "description": "控制变焦行为:disabled(仅平移/倾斜)、absolute(最兼容)或 relative(同时平移/倾斜/变焦)。" + }, + "zoom_factor": { + "label": "变焦因子", + "description": "控制追踪目标的变焦级别。数值越低保持更多场景可见;数值越高放大更近但可能丢失追踪。数值范围 0.1 到 0.75。" + }, + "track": { + "label": "追踪目标", + "description": "应触发自动追踪的目标类型列表。" + }, + "required_zones": { + "label": "必需区域", + "description": "目标必须进入这些区域之一才能开始自动追踪。" + }, + "return_preset": { + "label": "返回预设", + "description": "追踪结束后返回的摄像头固件中配置的 ONVIF 预设名称。" + }, + "timeout": { + "label": "返回超时", + "description": "失去追踪后等待多少秒后将摄像头返回到预设位置。" + }, + "movement_weights": { + "label": "移动权重", + "description": "由摄像头校准自动生成的校准值。请勿手动修改。" + }, + "enabled_in_config": { + "label": "原始自动追踪状态", + "description": "用于追踪配置中是否启用自动追踪的内部字段。" + } + }, + "ignore_time_mismatch": { + "label": "忽略时间不匹配", + "description": "忽略 ONVIF 通信中摄像头和 Frigate 服务器之间的时间同步差异。" + }, + "profile": { + "label": "ONVIF 配置文件", + "description": "用于 PTZ 控制的指定 ONVIF 媒体配置,将通过 Token 或名称匹配。如果未手动指定,将自动选择第一个包含有效 PTZ 配置的媒体配置。" + } + }, + "profiles": { + "label": "配置模板", + "description": "带有别名的命名配置模板定义。摄像头配置模板必须引用此处定义的名称。", + "friendly_name": { + "label": "别名", + "description": "在界面中显示的此配置模板名称,可以使用中文。" + } + }, + "active_profile": { + "label": "激活配置模板", + "description": "当前激活的配置模板名称。仅在运行时使用,不会写入 YAML 配置文件中。" + } +} diff --git a/web/public/locales/zh-CN/config/groups.json b/web/public/locales/zh-CN/config/groups.json new file mode 100644 index 0000000..3311c12 --- /dev/null +++ b/web/public/locales/zh-CN/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "全局检测", + "sensitivity": "全局灵敏度" + }, + "cameras": { + "detection": "检测", + "sensitivity": "灵敏度" + } + }, + "timestamp_style": { + "global": { + "appearance": "全局外观" + }, + "cameras": { + "appearance": "外观" + } + }, + "motion": { + "global": { + "sensitivity": "全局灵敏度", + "algorithm": "全局算法" + }, + "cameras": { + "sensitivity": "灵敏度", + "algorithm": "算法" + } + }, + "snapshots": { + "global": { + "display": "全局显示" + }, + "cameras": { + "display": "显示" + } + }, + "record": { + "global": { + "retention": "全局保留", + "events": "全局事件" + }, + "cameras": { + "retention": "保留", + "events": "事件" + } + }, + "detect": { + "global": { + "resolution": "全局分辨率", + "tracking": "全局追踪" + }, + "cameras": { + "resolution": "分辨率", + "tracking": "追踪" + } + }, + "objects": { + "global": { + "tracking": "全局追踪", + "filtering": "全局筛选" + }, + "cameras": { + "tracking": "追踪", + "filtering": "筛选" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "摄像头特定的 FFmpeg 参数" + } + } +} diff --git a/web/public/locales/zh-CN/config/validation.json b/web/public/locales/zh-CN/config/validation.json new file mode 100644 index 0000000..ba275a8 --- /dev/null +++ b/web/public/locales/zh-CN/config/validation.json @@ -0,0 +1,35 @@ +{ + "minimum": "必须至少为 {{limit}}", + "maximum": "最大值不能超过 {{limit}}", + "exclusiveMinimum": "必须大于 {{limit}}", + "exclusiveMaximum": "必须小于 {{limit}}", + "minLength": "长度至少为 {{limit}} 个字符", + "maxLength": "长度最多为 {{limit}} 个字符", + "minItems": "至少包含 {{limit}} 项", + "maxItems": "最多包含 {{limit}} 项", + "pattern": "格式无效", + "required": "此字段为必填项", + "type": "值类型无效", + "ffmpeg": { + "inputs": { + "detectRequired": "必须至少有一个输入流分配为“检测”功能。", + "rolesUnique": "每个功能只能分配给一个输入流。", + "hwaccelDetectOnly": "只有分配了检测功能的输入流才能定义硬件加速参数。" + } + }, + "enum": "必须是允许的值之一", + "const": "值与预期的常量不匹配", + "uniqueItems": "所有项必须唯一", + "format": "格式无效", + "additionalProperties": "不允许未知属性", + "oneOf": "必须完全匹配一个允许的模式", + "anyOf": "必须至少匹配一个允许的模式", + "proxy": { + "header_map": { + "roleHeaderRequired": "配置权限组映射时需要的 role 请求头。" + } + }, + "detect": { + "dimensionMustBeEven": "必须是偶数。" + } +} diff --git a/web/public/locales/zh-CN/objects.json b/web/public/locales/zh-CN/objects.json new file mode 100644 index 0000000..59058f3 --- /dev/null +++ b/web/public/locales/zh-CN/objects.json @@ -0,0 +1,130 @@ +{ + "person": "人", + "bicycle": "自行车", + "car": "汽车", + "motorcycle": "摩托车", + "airplane": "飞机", + "bus": "公交车", + "train": "火车", + "boat": "船", + "traffic_light": "交通灯", + "fire_hydrant": "消防栓", + "street_sign": "路标", + "stop_sign": "停车标志", + "parking_meter": "停车计时器", + "bench": "长椅", + "bird": "鸟", + "cat": "猫", + "dog": "狗", + "horse": "马", + "sheep": "绵羊", + "cow": "牛", + "elephant": "大象", + "bear": "熊", + "zebra": "斑马", + "giraffe": "长颈鹿", + "hat": "帽子", + "backpack": "背包", + "umbrella": "雨伞", + "shoe": "鞋子", + "eye_glasses": "眼镜", + "handbag": "手提包", + "tie": "领带", + "suitcase": "手提箱", + "frisbee": "飞盘", + "skis": "滑雪板", + "snowboard": "滑雪板", + "sports_ball": "运动球", + "kite": "风筝", + "baseball_bat": "棒球棒", + "baseball_glove": "棒球手套", + "skateboard": "滑板", + "surfboard": "冲浪板", + "tennis_racket": "网球拍", + "bottle": "瓶子", + "plate": "盘子", + "wine_glass": "酒杯", + "cup": "杯子", + "fork": "叉子", + "knife": "刀", + "spoon": "勺子", + "bowl": "碗", + "banana": "香蕉", + "apple": "苹果", + "sandwich": "三明治", + "orange": "橙子", + "broccoli": "西兰花", + "carrot": "胡萝卜", + "hot_dog": "热狗", + "pizza": "披萨", + "donut": "甜甜圈", + "cake": "蛋糕", + "chair": "椅子", + "couch": "沙发", + "potted_plant": "盆栽植物", + "bed": "床", + "mirror": "镜子", + "dining_table": "餐桌", + "window": "窗户", + "desk": "桌子", + "toilet": "厕所", + "door": "门", + "tv": "电视", + "laptop": "笔记本电脑", + "mouse": "鼠标", + "remote": "遥控器", + "keyboard": "键盘", + "cell_phone": "手机", + "microwave": "微波炉", + "oven": "烤箱", + "toaster": "烤面包机", + "sink": "水槽", + "refrigerator": "冰箱", + "blender": "搅拌机", + "book": "书", + "clock": "时钟", + "vase": "花瓶", + "scissors": "剪刀", + "teddy_bear": "泰迪熊", + "hair_dryer": "吹风机", + "toothbrush": "牙刷", + "hair_brush": "发刷", + "vehicle": "车辆", + "squirrel": "松鼠", + "deer": "鹿", + "animal": "动物", + "bark": "狗叫", + "fox": "狐狸", + "goat": "山羊", + "rabbit": "兔子", + "raccoon": "浣熊", + "robot_lawnmower": "自动割草机", + "waste_bin": "垃圾桶", + "on_demand": "手动", + "face": "人脸", + "license_plate": "车牌", + "package": "包裹", + "bbq_grill": "烧烤架", + "amazon": "亚马逊", + "usps": "美国邮政", + "ups": "UPS", + "fedex": "联邦快递", + "dhl": "DHL", + "an_post": "爱尔兰邮政", + "purolator": "普罗莱特", + "postnl": "荷兰邮政", + "nzpost": "新西兰邮政", + "postnord": "北欧邮政", + "gls": "GLS", + "dpd": "DPD", + "canada_post": "加拿大邮政", + "royal_mail": "英国皇家邮政", + "school_bus": "校车", + "skunk": "臭鼬", + "kangaroo": "袋鼠", + "baby": "婴儿", + "baby_stroller": "婴儿车", + "rickshaw": "三轮车", + "Rodent": "啮齿动物", + "rodent": "鼠类动物" +} diff --git a/web/public/locales/zh-CN/views/chat.json b/web/public/locales/zh-CN/views/chat.json new file mode 100644 index 0000000..dee1346 --- /dev/null +++ b/web/public/locales/zh-CN/views/chat.json @@ -0,0 +1,72 @@ +{ + "documentTitle": "聊天 - Frigate", + "title": "Frigate 聊天", + "subtitle": "你的摄像头管理与智能分析 AI 助手", + "placeholder": "尝试问我任何事儿…", + "error": "出现错误,请稍后重试。", + "processing": "进行中…", + "toolsUsed": "使用:{{tools}}", + "showTools": "显示工具({{count}})", + "hideTools": "隐藏工具", + "call": "调用", + "result": "结果", + "arguments": "参数:", + "response": "响应:", + "attachment_chip_label": "在 {{camera}} 的 {{label}}", + "attachment_chip_remove": "移除附件", + "open_in_explore": "从浏览中打开", + "attach_event_aria": "关联事件 {{eventId}}", + "attachment_picker_paste_label": "或粘贴事件 ID", + "attachment_picker_attach": "关联", + "attachment_picker_placeholder": "关联一个事件", + "quick_reply_find_similar": "查找相似抓拍事件", + "quick_reply_tell_me_more": "了解更多详情", + "quick_reply_when_else": "还在哪些时段出现过?", + "quick_reply_find_similar_text": "查找与此相似的抓拍记录。", + "quick_reply_tell_me_more_text": "了解此条更多详情。", + "starting_requests": { + "show_recent_events": "查看近期事件", + "show_camera_status": "显示摄像头状态", + "recap": "我不在的时候发生了什么?", + "watch_camera": "监控摄像头活动" + }, + "quick_reply_when_else_text": "还在哪些时间出现过?", + "anchor": "来源", + "similarity_score": "相似度", + "no_similar_objects_found": "未找到相似目标。", + "semantic_search_required": "必须启用语义搜索才能查找相似目标。", + "send": "发送", + "suggested_requests": "尝试问问:", + "starting_requests_prompts": { + "show_recent_events": "显示最近一小时的事件", + "show_camera_status": "我的摄像头当前状态如何?", + "recap": "我不在的时候发生了什么事?", + "watch_camera": "监控前门,有人出现就通知我" + }, + "new_chat": "新对话", + "settings": { + "title": "对话设置", + "show_stats": { + "title": "显示统计数据", + "desc": "显示对话回复的生成速率和上下文大小。", + "while_generating": "生成过程中", + "always": "始终" + }, + "auto_scroll": { + "title": "自动滚动", + "desc": "自动滚动到最新消息。" + } + }, + "stats": { + "context": "{{tokens}} 词元(tokens)", + "tokens_per_second": "{{rate}} 词元/秒" + }, + "reasoning": { + "active": "思考中…", + "show": "显示推理过程", + "hide": "隐藏推理过程" + }, + "thinking": { + "toggle": "切换思考" + } +} diff --git a/web/public/locales/zh-CN/views/classificationModel.json b/web/public/locales/zh-CN/views/classificationModel.json new file mode 100644 index 0000000..ea10683 --- /dev/null +++ b/web/public/locales/zh-CN/views/classificationModel.json @@ -0,0 +1,192 @@ +{ + "documentTitle": "分类模型 - Frigate", + "button": { + "deleteClassificationAttempts": "删除分类图片", + "renameCategory": "重命名类别", + "deleteCategory": "删除类别", + "deleteImages": "删除图片", + "trainModel": "训练模型", + "addClassification": "添加分类", + "deleteModels": "删除模型", + "editModel": "编辑模型" + }, + "toast": { + "success": { + "deletedCategory_other": "删除 {{count}} 个类别", + "deletedImage_other": "删除 {{count}} 张图片", + "categorizedImage": "成功分类图片", + "trainedModel": "训练模型成功。", + "trainingModel": "已开始训练模型。", + "deletedModel_other": "已删除 {{count}} 个模型", + "updatedModel": "已更新模型配置", + "renamedCategory": "成功修改类别名称为 {{name}}", + "reclassifiedImage": "成功重新分类图片" + }, + "error": { + "deleteImageFailed": "删除失败:{{errorMessage}}", + "deleteCategoryFailed": "删除类别失败:{{errorMessage}}", + "categorizeFailed": "图片分类失败:{{errorMessage}}", + "trainingFailed": "训练模型失败,请查看 Frigate 日志获取详情。", + "deleteModelFailed": "删除模型失败:{{errorMessage}}", + "updateModelFailed": "更新模型失败:{{errorMessage}}", + "trainingFailedToStart": "开始训练模型失败:{{errorMessage}}", + "renameCategoryFailed": "修改类别名称失败:{{errorMessage}}", + "reclassifyFailed": "重新分类图片失败:{{errorMessage}}" + } + }, + "deleteCategory": { + "title": "删除类别", + "desc": "确定要删除类别 {{name}} 吗?此操作将永久删除所有关联的图片,并需要重新训练模型。", + "minClassesTitle": "无法删除此类别", + "minClassesDesc": "分类模型必须至少有2个类别。你需要先添加一个新的类别,然后再删除当前这个类别。" + }, + "deleteDatasetImages": { + "title": "删除图片数据集", + "desc": "确定要从 {{dataset}} 中删除 {{count}} 张图片吗?此操作无法撤销,并将需要重新训练模型。" + }, + "deleteTrainImages": { + "title": "删除训练的图片", + "desc": "确定要删除 {{count}} 张图片吗?此操作无法撤销。" + }, + "renameCategory": { + "title": "重命名类别", + "desc": "请输入 {{name}} 的新名称。名称变更后需要重新训练模型。" + }, + "description": { + "invalidName": "名称无效。名称只能包含字母、数字、空格、撇号、下划线和连字符。" + }, + "train": { + "title": "最近分类记录", + "aria": "选择最近分类记录", + "titleShort": "近期" + }, + "categories": "类别", + "createCategory": { + "new": "创建新类别" + }, + "categorizeImageAs": "图片分类为:", + "categorizeImage": "图片分类", + "noModels": { + "object": { + "title": "未创建目标/物体分类模型", + "description": "创建自定义模型以分类检测到的目标。", + "buttonText": "创建目标/物体模型" + }, + "state": { + "title": "尚未创建状态分类模型", + "description": "创建自定义模型以监控并分类摄像头特定区域的状态变化。", + "buttonText": "创建状态模型" + } + }, + "wizard": { + "title": "创建新分类", + "steps": { + "nameAndDefine": "名称与定义", + "stateArea": "状态区域", + "chooseExamples": "选择范例" + }, + "step1": { + "description": "状态模型用于监控摄像头固定区域的状态变化(例如门是否开启或关闭)。目标/物体模型用于为检测到的目标添加分类标签(例如区分宠物、快递员等)。", + "name": "名称", + "namePlaceholder": "请输入模型名称……", + "type": "类型", + "typeState": "状态", + "typeObject": "目标/物体", + "objectLabel": "目标/物体标签", + "objectLabelPlaceholder": "请选择目标类型……", + "classificationType": "分类方式", + "classificationTypeTip": "了解分类方式", + "classificationTypeDesc": "子标签会为目标标签添加附加文本(例如:“人员:美团”)。属性是可搜索的元数据,独立存储在目标的元信息中。", + "classificationSubLabel": "子标签", + "classificationAttribute": "属性", + "classes": "类别", + "classesTip": "了解类别", + "classesStateDesc": "定义摄像头区域内可能出现的不同状态。例如:车库门的“开启”和“关闭”。", + "classesObjectDesc": "定义用于分类检测目标的不同类别。例如:人员分类中的“快递员”、“居民”、“陌生人”。", + "classPlaceholder": "请输入分类名称……", + "errors": { + "nameRequired": "模型名称为必填项", + "nameLength": "模型名称长度不能超过 64 个字符", + "nameOnlyNumbers": "模型名称不能仅包含数字", + "classRequired": "至少需要一个类别", + "classesUnique": "类别名称必须唯一", + "stateRequiresTwoClasses": "状态模型至少需要两个类别", + "objectLabelRequired": "请选择一个目标标签", + "objectTypeRequired": "请选择一个目标标签", + "noneNotAllowed": "不能创建“none”(无标签)类别" + }, + "states": "状态" + }, + "step2": { + "description": "选择摄像头,并为摄像头定义要监控的区域。模型将对这些区域的状态进行分类。", + "cameras": "摄像头", + "selectCamera": "选择摄像头", + "noCameras": "点击 + 符号添加摄像头", + "selectCameraPrompt": "从列表中选择一个摄像头以定义其检测区域" + }, + "step3": { + "selectImagesPrompt": "选择所有属于 {{className}} 的图片", + "selectImagesDescription": "点击图像进行选择,完成该类别后点击“继续”。", + "generating": { + "title": "正在生成样本图片", + "description": "Frigate 正在从录像中提取代表性图片。这可能需要一些时间……" + }, + "training": { + "title": "正在训练模型", + "description": "系统正在后台训练模型。你可以关闭此对话框,训练完成后模型将自动开始运行。" + }, + "retryGenerate": "重新生成", + "noImages": "未生成样本图像", + "classifying": "正在分类与训练……", + "trainingStarted": "已开始模型训练", + "errors": { + "noCameras": "未配置摄像头", + "noObjectLabel": "未选择目标标签", + "generateFailed": "示例生成失败:{{error}}", + "generationFailed": "生成失败,请重试。", + "classifyFailed": "图片分类失败:{{error}}" + }, + "generateSuccess": "样本图片生成成功", + "allImagesRequired_other": "请对所有图片进行分类。还有 {{count}} 张图片需要分类。", + "modelCreated": "模型创建成功。请在“最近分类”页面为缺失的状态添加图片,然后训练模型。", + "missingStatesWarning": { + "title": "缺失分类示例", + "description": "并非所有类别都有示例。可尝试生成新示例以查找缺失的类别,或继续该步骤,之后通过 “最近分类” 页面添加图片。" + }, + "refreshExamples": "生成新示例", + "refreshConfirm": { + "title": "需要生成新示例?", + "description": "此操作将生成一组新的图片,并清除所有选择内容(包括之前的所有类别)。你需要为所有类别重新选择示例。" + } + } + }, + "deleteModel": { + "title": "删除分类模型", + "single": "你确定要删除 {{name}} 吗?此操作将永久删除所有相关数据,包括图片和训练数据,且无法撤销。", + "desc": "你确定要删除 {{count}} 个模型吗?此操作将永久删除所有相关数据,包括图片和训练数据,且无法撤销。" + }, + "menu": { + "objects": "目标", + "states": "状态" + }, + "details": { + "scoreInfo": "得分表示该目标所有检测结果的平均分类置信度。", + "none": "无分类", + "unknown": "未知" + }, + "edit": { + "title": "编辑分类模型", + "descriptionState": "编辑此状态分类模型的类别;更改后需要重新训练模型。", + "descriptionObject": "编辑此目标分类模型的目标类型和分类类型。", + "stateClassesInfo": "注意:更改状态类别后需使用更新后的类别重新训练模型。" + }, + "tooltip": { + "trainingInProgress": "模型正在训练中", + "noNewImages": "没有新的图片可用于训练。请先对数据集中的更多图片进行分类。", + "noChanges": "自上次训练以来,数据集未作任何更改。", + "modelNotReady": "模型尚未准备好进行训练" + }, + "none": "无标签", + "reclassifyImageAs": "重新分类图片为:", + "reclassifyImage": "重新分类图片" +} diff --git a/web/public/locales/zh-CN/views/configEditor.json b/web/public/locales/zh-CN/views/configEditor.json new file mode 100644 index 0000000..a4ca5c5 --- /dev/null +++ b/web/public/locales/zh-CN/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "配置编辑器 - Frigate", + "configEditor": "配置编辑器", + "copyConfig": "复制配置", + "saveAndRestart": "保存并重启", + "saveOnly": "只保存", + "toast": { + "success": { + "copyToClipboard": "配置已复制到剪贴板。" + }, + "error": { + "savingError": "保存配置时出错" + } + }, + "confirm": "是否退出并不保存?", + "safeConfigEditor": "配置编辑器(安全模式)", + "safeModeDescription": "由于验证配置出现错误,Frigate目前为安全模式。" +} diff --git a/web/public/locales/zh-CN/views/events.json b/web/public/locales/zh-CN/views/events.json new file mode 100644 index 0000000..6035051 --- /dev/null +++ b/web/public/locales/zh-CN/views/events.json @@ -0,0 +1,95 @@ +{ + "alerts": "警报", + "detections": "检测", + "motion": { + "label": "画面变动", + "only": "仅变动画面" + }, + "allCameras": "所有摄像头", + "empty": { + "alert": "还没有“警报”类核查项", + "detection": "还没有“检测”类核查项", + "motion": "还没有画面变动类数据", + "recordingsDisabled": { + "title": "必须要开启录制功能", + "description": "必须要摄像头开启录制功能时,才可为其创建回放项目。" + } + }, + "timeline": { + "label": "时间线" + }, + "timeline.aria": "选择时间线", + "events": { + "label": "事件", + "aria": "选择事件", + "noFoundForTimePeriod": "未找到该时间段的事件。" + }, + "documentTitle": "核查 - Frigate", + "recordings": { + "documentTitle": "回放 - Frigate", + "invalidSharedLink": "由于解析错误,无法打开带时间戳的录制链接。", + "invalidSharedCamera": "由于摄像头未知或未获授权,无法打开带时间戳的录制链接。" + }, + "calendarFilter": { + "last24Hours": "过去24小时" + }, + "markAsReviewed": "标记为已核查", + "markTheseItemsAsReviewed": "将这些项目标记为已核查", + "newReviewItems": { + "label": "查看新的核查项目", + "button": "核查新项目" + }, + "camera": "摄像头", + "selected": "已选择 {{count}} 个", + "selected_one": "已选择 {{count}} 个", + "selected_other": "已选择 {{count}} 个", + "detected": "已检测", + "suspiciousActivity": "可疑活动", + "threateningActivity": "风险类活动", + "detail": { + "noDataFound": "没有可供核查的详细数据", + "aria": "切换详细视图", + "trackedObject_one": "{{count}}个目标或物体", + "trackedObject_other": "{{count}}个目标或物体", + "noObjectDetailData": "没有目标详细信息。", + "label": "详细信息", + "settings": "详细视图设置", + "alwaysExpandActive": { + "title": "始终展开当前项", + "desc": "在可用情况下,将始终展开当前核查项的目标详细信息。" + } + }, + "objectTrack": { + "trackedPoint": "追踪点", + "clickToSeek": "点击从该时间进行寻找" + }, + "zoomIn": "放大", + "zoomOut": "缩小", + "normalActivity": "正常", + "needsReview": "需要核查", + "securityConcern": "安全隐患", + "select_all": "所有", + "motionSearch": { + "menuItem": "画面变动搜索", + "openMenu": "摄像头选项" + }, + "motionPreviews": { + "menuItem": "查看画面变动预览", + "title": "画面变动预览:{{camera}}", + "mobileSettingsTitle": "画面变动预览设置", + "mobileSettingsDesc": "调整播放速度和变暗程度,并选择日期以仅查看画面变动的片段。", + "dim": "变暗", + "dimAria": "调整变暗强度", + "dimDesc": "增加变暗程度可以提高画面变动区域的可见性。", + "speed": "速度", + "speedAria": "选择预览播放速度", + "speedDesc": "选择预览片段的播放速度。", + "back": "返回", + "empty": "没有可用的预览", + "noPreview": "预览不可用", + "seekAria": "将 {{camera}} 播放器定位到 {{time}}", + "filter": "筛选", + "filterDesc": "选择区域以仅显示在这些区域中有画面变动的片段。", + "filterClear": "清除" + } +} diff --git a/web/public/locales/zh-CN/views/explore.json b/web/public/locales/zh-CN/views/explore.json new file mode 100644 index 0000000..b5860d1 --- /dev/null +++ b/web/public/locales/zh-CN/views/explore.json @@ -0,0 +1,315 @@ +{ + "documentTitle": "浏览 - Frigate", + "generativeAI": "生成式 AI", + "exploreIsUnavailable": { + "title": "浏览功能不可用", + "embeddingsReindexing": { + "context": "完成追踪目标嵌入重新索引后,才可以使用 浏览 功能。", + "startingUp": "启动中…", + "estimatedTime": "预计剩余时间:", + "finishingShortly": "即将完成", + "step": { + "thumbnailsEmbedded": "缩略图嵌入:", + "descriptionsEmbedded": "描述嵌入:", + "trackedObjectsProcessed": "追踪目标已处理: " + } + }, + "downloadingModels": { + "context": "Frigate正在下载支持语义搜索功能所需的嵌入模型。根据网络连接速度,这可能需要几分钟。", + "setup": { + "visionModel": "视觉模型", + "visionModelFeatureExtractor": "视觉模型特征提取器", + "textModel": "文本模型", + "textTokenizer": "文本分词器" + }, + "tips": { + "context": "模型下载完成后,您可能需要重新索引追踪目标的嵌入。", + "documentation": "阅读文档" + }, + "error": "发生错误。请检查Frigate日志。" + } + }, + "trackedObjectDetails": "目标追踪详情", + "type": { + "details": "详情", + "snapshot": "快照", + "video": "视频", + "object_lifecycle": "目标全周期", + "thumbnail": "缩略图", + "tracking_details": "追踪详情" + }, + "objectLifecycle": { + "title": "目标全周期", + "noImageFound": "未找到此时间戳的图像。", + "createObjectMask": "创建目标/物体遮罩", + "adjustAnnotationSettings": "调整标注设置", + "scrollViewTips": "滚动查看此目标全周期的关键节点。", + "autoTrackingTips": "自动跟踪摄像头的边界框位置可能不准确。", + "lifecycleItemDesc": { + "visible": "检测到 {{label}}", + "entered_zone": "{{label}} 进入 {{zones}}", + "active": "{{label}} 变为活动状态", + "stationary": "{{label}} 变为静止状态", + "attribute": { + "faceOrLicense_plate": "检测到 {{label}} 的 {{attribute}}", + "other": "{{label}} 识别为 {{attribute}}" + }, + "gone": "{{label}} 离开", + "heard": "听到 {{label}}", + "external": "检测到 {{label}}", + "header": { + "ratio": "得分", + "zones": "区域", + "area": "坐标区域" + } + }, + "annotationSettings": { + "title": "标注设置", + "showAllZones": { + "title": "显示所有区域", + "desc": "始终在目标进入区域的帧上显示区域标记。" + }, + "offset": { + "label": "标注偏移", + "desc": "这些数据来自摄像头的检测源,但是叠加在录制源的图像上。这两个流不太可能完全同步。因此,边界框和录像不会完全对齐。但是,可以使用 annotation_offset 字段来调整这个问题。", + "documentation": "阅读文档 ", + "millisecondsToOffset": "检测标注的偏移毫秒数。默认值:0", + "tips": "提示:假设有一个人从左向右走的事件片段。如果事件时间线上的边界框始终在人的左侧,则应该减小该值。同样,如果一个人从左向右走,而边界框始终在人的前面,则应该增加该值。", + "toast": { + "success": "{{camera}} 的标注偏移量已成功保存至配置文件。请重启Frigate生效。" + } + } + }, + "carousel": { + "previous": "上一张", + "next": "下一张" + }, + "count": "第 {{first}} 个,共 {{second}} 个", + "trackedPoint": "追踪点" + }, + "details": { + "item": { + "title": "回放项目详情", + "desc": "核查项详情", + "button": { + "share": "分享该核查项", + "viewInExplore": "在 浏览 中查看" + }, + "tips": { + "mismatch_other": "检测到 {{count}} 个不可用的目标,并已包含在此核查项中。这些目标可能未达到警报或检测标准,或者已被清理/删除。", + "hasMissingObjects": "如果希望 Frigate 保存 {{objects}} 标签的追踪目标,请调整您的配置" + }, + "toast": { + "success": { + "regenerate": "已向 {{provider}} 请求新的描述。根据提供商的速度,生成新描述可能需要一些时间。", + "updatedSublabel": "成功更新子标签。", + "updatedLPR": "成功更新车牌。", + "audioTranscription": "成功请求音频转录。根据你运行 Frigate 的服务器速度,转录可能需要一些时间才能完成。", + "updatedAttributes": "更新属性成功。" + }, + "error": { + "regenerate": "调用 {{provider}} 生成新描述失败:{{errorMessage}}", + "updatedSublabelFailed": "更新子标签失败:{{errorMessage}}", + "updatedLPRFailed": "更新车牌失败:{{errorMessage}}", + "audioTranscription": "请求音频转录失败:{{errorMessage}}", + "updatedAttributesFailed": "更新属性失败:{{errorMessage}}" + } + } + }, + "label": "标签", + "editSubLabel": { + "title": "编辑子标签", + "desc": "为 {{label}} 输入新的子标签", + "descNoLabel": "为该追踪目标输入新的子标签" + }, + "topScore": { + "label": "最高得分", + "info": "最高分是追踪目标的中位分数最高值,因此可能与搜索结果缩略图中显示的分数有所不同。" + }, + "estimatedSpeed": "预计速度", + "objects": "目标/物体", + "camera": "摄像头", + "zones": "区域", + "timestamp": "时间戳", + "button": { + "findSimilar": "查找相似项", + "regenerate": { + "title": "重新生成", + "label": "重新生成追踪目标的描述" + } + }, + "description": { + "label": "描述", + "placeholder": "追踪目标的描述", + "aiTips": "在追踪目标的目标全周期结束之前,Frigate 不会向您的生成式 AI 提供商请求描述。" + }, + "expandRegenerationMenu": "展开重新生成菜单", + "regenerateFromSnapshot": "从快照重新生成", + "regenerateFromThumbnails": "从缩略图重新生成", + "tips": { + "descriptionSaved": "已保存描述", + "saveDescriptionFailed": "更新描述失败:{{errorMessage}}" + }, + "editLPR": { + "desc": "为 {{label}} 输入新的车牌值", + "descNoLabel": "为检测到的目标输入新的车牌值", + "title": "编辑车牌" + }, + "recognizedLicensePlate": "识别的车牌", + "snapshotScore": { + "label": "快照得分" + }, + "score": { + "label": "分值" + }, + "editAttributes": { + "title": "编辑属性", + "desc": "为 {{label}} 选择分类属性" + }, + "attributes": "分类属性", + "title": { + "label": "标题" + }, + "scoreInfo": "分数信息" + }, + "itemMenu": { + "downloadVideo": { + "label": "下载视频", + "aria": "下载视频" + }, + "downloadSnapshot": { + "label": "下载快照", + "aria": "下载快照" + }, + "viewObjectLifecycle": { + "label": "查看目标全周期", + "aria": "显示目标的全周期" + }, + "findSimilar": { + "label": "查找相似项", + "aria": "查看相似的目标/物体" + }, + "submitToPlus": { + "label": "提交至 Frigate+", + "aria": "提交至 Frigate Plus" + }, + "viewInHistory": { + "label": "在历史记录中查看", + "aria": "在历史记录中查看" + }, + "deleteTrackedObject": { + "label": "删除此追踪目标" + }, + "addTrigger": { + "label": "添加触发器", + "aria": "为该追踪目标添加触发器" + }, + "audioTranscription": { + "label": "转录", + "aria": "请求音频转录" + }, + "showObjectDetails": { + "label": "显示目标轨迹" + }, + "hideObjectDetails": { + "label": "隐藏目标轨迹" + }, + "viewTrackingDetails": { + "label": "查看追踪详情", + "aria": "显示追踪详情" + }, + "downloadCleanSnapshot": { + "label": "下载干净快照", + "aria": "下载干净快照" + }, + "debugReplay": { + "label": "调试回放", + "aria": "在调试回放视图中查看此被追踪对象" + }, + "more": { + "aria": "更多" + } + }, + "dialog": { + "confirmDelete": { + "title": "确认删除", + "desc": "删除此追踪目标后,将移除快照、所有已保存的嵌入向量数据以及任何相关的目标追踪详情条目,但在 历史 页面中追踪目标的录制视频片段不会被删除。

    你确定要继续删除该追踪目标吗?" + }, + "toast": { + "error": "删除该追踪目标时出错:{{errorMessage}}" + } + }, + "noTrackedObjects": "未找到追踪目标", + "fetchingTrackedObjectsFailed": "获取追踪目标失败:{{errorMessage}}", + "trackedObjectsCount_other": "{{count}} 个追踪目标 ", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "删除追踪目标成功。", + "error": "删除追踪目标失败:{{errorMessage}}" + } + }, + "tooltip": "与 {{type}} 匹配度为 {{confidence}}%", + "previousTrackedObject": "上一个追踪目标", + "nextTrackedObject": "下一个追踪目标" + }, + "exploreMore": "浏览更多的 {{label}}", + "aiAnalysis": { + "title": "AI分析" + }, + "concerns": { + "label": "风险等级" + }, + "trackingDetails": { + "title": "追踪细节", + "noImageFound": "在该时间内没找到图片。", + "createObjectMask": "创建目标遮罩", + "adjustAnnotationSettings": "调整注释设置", + "scrollViewTips": "点击以查看该目标全周期中的关键时刻。", + "autoTrackingTips": "自动追踪摄像头的边框定位可能不准确。", + "count": "{{first}} / {{second}}", + "trackedPoint": "追踪点", + "lifecycleItemDesc": { + "visible": "已检测到 {{label}}", + "entered_zone": "{{label}} 进入 {{zones}}", + "active": "{{label}} 正在活动", + "stationary": "{{label}} 变为静止", + "attribute": { + "faceOrLicense_plate": "检测到 {{label}} 的 {{attribute}} 属性", + "other": "{{label}} 被识别为 {{attribute}}" + }, + "gone": "{{label}} 离开", + "heard": "听到 {{label}}", + "external": "已检测到 {{label}}", + "header": { + "zones": "区", + "ratio": "比例", + "area": "大小", + "score": "分数", + "computedScore": "计算得分", + "topScore": "最高得分", + "toggleAdvancedScores": "切换高级分数" + } + }, + "annotationSettings": { + "title": "标记设置", + "showAllZones": { + "title": "显示所有区", + "desc": "在目标进入区域的帧中始终显示区域框。" + }, + "offset": { + "label": "标记偏移量", + "desc": "此数据来自摄像头的检测视频流,但叠加在录制视频流的画面上。两个视频流可能不会完全同步,因此边框与画面可能无法完全对齐。可以使用此设置将标记在时间轴上向前或向后偏移,以更好地与录制画面对齐。", + "millisecondsToOffset": "用于偏移检测标记的毫秒数。 默认值:0", + "tips": "提示:假设有一段人从左向右走的事件录制,如果事件时间轴中的边框始终在人的左侧(即后方),则应该减小偏移值;反之,如果边框始终领先于人物,则应增大偏移值。", + "toast": { + "success": "{{camera}} 的标记偏移量已保存。" + } + } + }, + "carousel": { + "previous": "上一张图", + "next": "下一张图" + } + } +} diff --git a/web/public/locales/zh-CN/views/exports.json b/web/public/locales/zh-CN/views/exports.json new file mode 100644 index 0000000..6eaed05 --- /dev/null +++ b/web/public/locales/zh-CN/views/exports.json @@ -0,0 +1,128 @@ +{ + "documentTitle": "导出 - Frigate", + "search": "搜索", + "noExports": "没有找到导出的项目", + "deleteExport": { + "label": "删除导出" + }, + "deleteExport.desc": "你确定要删除 {{exportName}} 吗?", + "editExport": { + "title": "重命名导出", + "desc": "为此导出项目输入新名称。", + "saveExport": "保存导出" + }, + "toast": { + "error": { + "renameExportFailed": "重命名导出失败:{{errorMessage}}", + "assignCaseFailed": "更新合集分配失败:{{errorMessage}}", + "caseSaveFailed": "保存合集失败:{{errorMessage}}", + "caseDeleteFailed": "删除合集失败:{{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "分享导出", + "downloadVideo": "下载视频", + "editName": "编辑名称", + "deleteExport": "删除导出", + "assignToCase": "加入合集", + "removeFromCase": "从合集中移除" + }, + "headings": { + "uncategorizedExports": "未分类导出项", + "cases": "合集" + }, + "caseDialog": { + "nameLabel": "合集名称", + "title": "加入合集", + "description": "选择现有合集或创建新合集。", + "selectLabel": "合集", + "newCaseOption": "创建新合集", + "descriptionLabel": "描述" + }, + "toolbar": { + "newCase": "新合集", + "addExport": "新导出", + "editCase": "编辑合集", + "deleteCase": "删除合集" + }, + "deleteCase": { + "label": "删除合集", + "desc": "你确定要删除 {{caseName}} 吗?", + "descKeepExports": "导出文件将继续保留为未分类导出。", + "descDeleteExports": "此合集中的所有导出项都将被永久删除。", + "deleteExports": "同时删除导出文件" + }, + "caseCard": { + "emptyCase": "暂无导出文件" + }, + "jobCard": { + "defaultName": "{{camera}} 导出", + "queued": "队列中", + "running": "运行中", + "preparing": "准备中", + "copying": "复制中", + "encoding": "编码中", + "encodingRetry": "重试编码中", + "finalizing": "正在完成" + }, + "caseView": { + "noDescription": "没有描述", + "createdAt": "已创建 {{value}}", + "exportCount_one": "1 个导出", + "exportCount_other": "{{count}} 个导出", + "cameraCount_one": "1 个摄像头", + "cameraCount_other": "{{count}} 个摄像头", + "showMore": "显示更多", + "showLess": "显示更少", + "emptyTitle": "该合集为空", + "emptyDescription": "将现有未分类的导出添加进来,以便整理该条目。", + "emptyDescriptionNoExports": "目前没有可添加的未分类导出项。" + }, + "caseEditor": { + "createTitle": "创建合集", + "editTitle": "编辑合集", + "namePlaceholder": "合集名称", + "descriptionPlaceholder": "为该合集添加备注或相关说明" + }, + "addExportDialog": { + "title": "将导出添加到 {{caseName}}", + "searchPlaceholder": "搜索未分类的导出项", + "empty": "未找到匹配的未分类导出。", + "addButton_one": "添加 1 个导出", + "addButton_other": "添加 {{count}} 个导出", + "adding": "添加中…" + }, + "selected_one": "已选择 {{count}} 个", + "selected_other": "已选择 {{count}} 个", + "bulkActions": { + "addToCase": "添加至合集", + "moveToCase": "移动至合集", + "removeFromCase": "从合集中移除", + "delete": "删除", + "deleteNow": "立即删除" + }, + "bulkDelete": { + "title": "删除导出", + "desc_one": "你确定要删除 {{count}} 个导出吗?", + "desc_other": "确定要删除 {{count}} 个导出吗?" + }, + "bulkRemoveFromCase": { + "title": "从合集中移除", + "desc_one": "你确定要从该合集中移除这 {{count}} 个导出吗?", + "desc_other": "你确定要从该合集中移除这 {{count}} 个导出吗?", + "descKeepExports": "导出将被移至未分类。", + "descDeleteExports": "导出将被永久删除。", + "deleteExports": "选择删除导出" + }, + "bulkToast": { + "success": { + "delete": "已删除导出", + "reassign": "已更新合集分配", + "remove": "已从合集中移除导出" + }, + "error": { + "deleteFailed": "删除导出失败:{{errorMessage}}", + "reassignFailed": "更新合集分配失败:{{errorMessage}}" + } + } +} diff --git a/web/public/locales/zh-CN/views/faceLibrary.json b/web/public/locales/zh-CN/views/faceLibrary.json new file mode 100644 index 0000000..59aedc9 --- /dev/null +++ b/web/public/locales/zh-CN/views/faceLibrary.json @@ -0,0 +1,107 @@ +{ + "description": { + "addFace": "我们将引导你如何向人脸库中添加新的合集。", + "placeholder": "请输入此合集的名称", + "invalidName": "名称无效。名称只能包含字母、数字、空格、撇号、下划线和连字符。", + "nameCannotContainHash": "名称中不允许包含“#”符号。" + }, + "details": { + "person": "人", + "confidence": "置信度", + "face": "人脸详情", + "faceDesc": "生成此人脸特征的追踪目标详细信息", + "timestamp": "时间戳", + "subLabelScore": "子标签得分", + "scoreInfo": "子标签分数是基于所有识别到的人脸置信度的加权评分,因此可能与快照中显示的分数有所不同。", + "unknown": "未知" + }, + "documentTitle": "人脸库 - Frigate", + "uploadFaceImage": { + "title": "上传人脸图片", + "desc": "上传图片以扫描人脸并包含在{{pageToggle}}中" + }, + "createFaceLibrary": { + "title": "创建特征库", + "desc": "创建一个新的特征库", + "new": "新建人脸", + "nextSteps": "建议按以下步骤建立可靠的数据集:
  • 使用近期识别记录选项卡为每个检测到的人员选择并训练图像
  • 优先使用正脸图像以获得最佳效果,尽可能避免使用侧脸图像进行训练
  • " + }, + "train": { + "title": "近期识别记录", + "aria": "选择近期识别记录", + "empty": "近期未检测到人脸识别操作", + "titleShort": "近期", + "emptyNoLibrary": { + "title": "更新人脸", + "description": "你必须向库中添加至少一张人脸,人脸识别功能才能正常工作。" + } + }, + "selectItem": "选择 {{item}}", + "selectFace": "选择人脸", + "deleteFaceLibrary": { + "title": "删除名称", + "desc": "确定要删除数据集 {{name}} 吗?此操作将永久删除所有关联的人脸特征数据。" + }, + "button": { + "deleteFaceAttempts": "删除人脸", + "addFace": "添加人脸", + "uploadImage": "上传图片", + "reprocessFace": "重新处理人脸", + "renameFace": "重命名人脸", + "deleteFace": "删除人脸" + }, + "imageEntry": { + "validation": { + "selectImage": "请选择图片文件。" + }, + "dropActive": "拖动图片文件到这里…", + "dropInstructions": "拖动或粘贴图片文件到此处,也可以点击选择文件", + "maxSize": "最大文件大小:{{size}}MB" + }, + "readTheDocs": "阅读文档", + "trainFaceAs": "将人脸特征训练为:", + "trainFace": "训练人脸特征", + "toast": { + "success": { + "uploadedImage": "图片上传成功。", + "addFaceLibrary": "{{name}} 已成功添加至人脸库!", + "deletedFace_other": "成功删除 {{count}} 个 人脸特征。", + "deletedName_other": "成功删除 {{count}} 个 人脸特征。", + "trainedFace": "人脸特征训练成功。", + "updatedFaceScore": "更新 {{name}} 人脸特征评分({{score}})成功。", + "renamedFace": "成功重命名人脸为{{name}}", + "reclassifiedFace": "重新分类人脸成功。" + }, + "error": { + "uploadingImageFailed": "图片上传失败:{{errorMessage}}", + "addFaceLibraryFailed": "人脸命名失败:{{errorMessage}}", + "deleteFaceFailed": "删除失败:{{errorMessage}}", + "deleteNameFailed": "数据集删除失败:{{errorMessage}}", + "trainFailed": "训练失败:{{errorMessage}}", + "updateFaceScoreFailed": "更新人脸评分失败:{{errorMessage}}", + "renameFaceFailed": "重命名人脸失败:{{errorMessage}}", + "reclassifyFailed": "重新分类人脸失败:{{errorMessage}}" + } + }, + "steps": { + "faceName": "输入人脸姓名", + "uploadFace": "上传人脸照片", + "nextSteps": "下一步", + "description": { + "uploadFace": "上传一张{{name}}的正面人脸照片。图片无需裁剪为仅显示面部。" + } + }, + "renameFace": { + "desc": "为 {{name}} 输入新的名称", + "title": "重命名人脸" + }, + "collections": "合集", + "deleteFaceAttempts": { + "desc_other": "你确定要删除 {{count}} 张人脸数据吗?此操作不可撤销。", + "title": "删除人脸" + }, + "pixels": "{{area}} 像素", + "nofaces": "没有可用的人脸", + "reclassifyFaceAs": "将人脸重新分类为:", + "reclassifyFace": "重新分类人脸" +} diff --git a/web/public/locales/zh-CN/views/live.json b/web/public/locales/zh-CN/views/live.json new file mode 100644 index 0000000..de900ba --- /dev/null +++ b/web/public/locales/zh-CN/views/live.json @@ -0,0 +1,206 @@ +{ + "documentTitle": { + "default": "实时监控 - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - 实时监控 - Frigate", + "lowBandwidthMode": "低带宽模式", + "twoWayTalk": { + "enable": "开启实时对话", + "disable": "关闭实时通话" + }, + "cameraAudio": { + "enable": "开启音频输出", + "disable": "关闭音频输出" + }, + "ptz": { + "move": { + "clickMove": { + "label": "点击画面以使摄像头居中", + "enable": "开启点击移动", + "disable": "禁用点击移动", + "enableWithZoom": "开启点击移动 / 拖动缩放功能" + }, + "left": { + "label": "PTZ摄像头向左移动" + }, + "up": { + "label": "PTZ摄像头向上移动" + }, + "down": { + "label": "PTZ摄像头向下移动" + }, + "right": { + "label": "PTZ摄像头向右移动" + } + }, + "zoom": { + "in": { + "label": "PTZ摄像头放大" + }, + "out": { + "label": "PTZ摄像头缩小" + } + }, + "frame": { + "center": { + "label": "点击将PTZ摄像头画面居中" + } + }, + "presets": "PTZ摄像头预设", + "focus": { + "in": { + "label": "PTZ摄像头聚焦" + }, + "out": { + "label": "PTZ摄像头拉远" + } + } + }, + "camera": { + "enable": "开启摄像头", + "disable": "关闭摄像头", + "turnOn": "开启摄像头", + "turnOff": "关闭摄像头" + }, + "muteCameras": { + "enable": "屏蔽所有摄像头", + "disable": "取消屏蔽所有摄像头" + }, + "detect": { + "enable": "开启检测", + "disable": "关闭检测" + }, + "recording": { + "enable": "开启录制", + "disable": "关闭录制", + "disabledInConfig": "必须先在该摄像头的设置中开启录制功能。" + }, + "snapshots": { + "enable": "开启快照", + "disable": "关闭快照" + }, + "audioDetect": { + "enable": "开启音频检测", + "disable": "关闭音频检测" + }, + "autotracking": { + "enable": "启用自动追踪", + "disable": "关闭自动追踪" + }, + "streamStats": { + "enable": "显示视频流统计信息", + "disable": "隐藏视频流统计信息" + }, + "manualRecording": { + "title": "按需录制", + "tips": "根据此摄像头的录像存储设置,可以下载即时快照或手动触发事件记录。", + "playInBackground": { + "label": "后台播放", + "desc": "启用此选项可在播放器隐藏时继续视频流播放。" + }, + "showStats": { + "label": "显示统计信息", + "desc": "启用此选项可在摄像头画面上叠加显示视频流统计信息。" + }, + "debugView": "调试视图", + "start": "开始手动按需录制", + "started": "已启用手动按需录制。", + "failedToStart": "启动手动录制失败。", + "recordDisabledTips": "由于此摄像头的配置中禁用了录制或对其进行了限制,将只会保存快照。", + "end": "停止手动按需录制", + "ended": "已完成手动按需录制。", + "failedToEnd": "停止手动录制失败。" + }, + "streamingSettings": "视频流设置", + "notifications": "通知", + "audio": "音频", + "suspend": { + "forTime": "暂停时长: " + }, + "stream": { + "title": "视频流", + "audio": { + "tips": { + "title": "必须要摄像头支持音频,以及需要 go2rtc 支持并配置。", + "documentation": "阅读文档 " + }, + "available": "此视频流支持音频", + "unavailable": "此视频流不支持音频传输" + }, + "twoWayTalk": { + "tips": "您的设备必须支持此功能,并且必须配置 WebRTC 以支持双向对讲。", + "tips.documentation": "阅读文档 ", + "available": "此视频流支持双向对讲", + "unavailable": "此视频流不支持双向对讲" + }, + "lowBandwidth": { + "tips": "由于缓冲或视频流错误,实时视图处于低带宽模式。", + "resetStream": "重置视频流" + }, + "playInBackground": { + "label": "后台播放", + "tips": "启用此选项可在播放器隐藏时继续视频流播放。" + }, + "debug": { + "picker": "调试模式下无法切换视频流。调试将始终使用检测(detect)功能的视频流。" + } + }, + "cameraSettings": { + "title": "{{camera}} 设置", + "cameraEnabled": "摄像头已启用", + "objectDetection": "目标检测", + "recording": "录制", + "snapshots": "快照", + "audioDetection": "音频检测", + "autotracking": "自动追踪", + "transcription": "音频转录", + "camera": "摄像头" + }, + "history": { + "label": "显示历史录像" + }, + "effectiveRetainMode": { + "modes": { + "all": "全部", + "motion": "画面变动", + "active_objects": "活动目标" + }, + "notAllTips": "您的 {{source}} 录制保留配置设置为 mode: {{effectiveRetainMode}},因此此按需录制将仅保留包含 {{effectiveRetainModeName}} 的片段。" + }, + "editLayout": { + "label": "编辑布局", + "group": { + "label": "编辑摄像头分组" + }, + "exitEdit": "退出编辑" + }, + "transcription": { + "enable": "启用实时音频转录", + "disable": "关闭实时音频转录" + }, + "noCameras": { + "title": "未设置摄像头", + "description": "准备开始连接摄像头至 Frigate 。", + "buttonText": "添加摄像头", + "restricted": { + "title": "无可用摄像头", + "description": "你没有权限查看此分组中的任何摄像头。" + }, + "default": { + "title": "没有配置摄像头", + "description": "现在就将摄像头接入到 Frigate 吧。", + "buttonText": "添加摄像头" + }, + "group": { + "title": "摄像头组目前为空", + "description": "该摄像头组未分配或启动了摄像头。", + "buttonText": "管理摄像头组" + } + }, + "snapshot": { + "takeSnapshot": "下载即时快照", + "noVideoSource": "当前无可用于快照的视频源。", + "captureFailed": "捕获快照失败。", + "downloadStarted": "快照下载已开始。" + } +} diff --git a/web/public/locales/zh-CN/views/motionSearch.json b/web/public/locales/zh-CN/views/motionSearch.json new file mode 100644 index 0000000..11dafbe --- /dev/null +++ b/web/public/locales/zh-CN/views/motionSearch.json @@ -0,0 +1,78 @@ +{ + "documentTitle": "变动搜索 - Frigate", + "title": "画面变动搜索", + "description": "绘制一个多边形以划定感兴趣区域,并指定时间范围,检索该区域内的异动变化。", + "selectCamera": "画面变动搜索正在加载中", + "startSearch": "开始搜索", + "searchStarted": "搜索已开始", + "searchCancelled": "搜索已取消", + "cancelSearch": "取消", + "searching": "搜索进行中。", + "searchComplete": "搜索完成", + "noResultsYet": "在所选区域内执行搜索,查找异常变化", + "noChangesFound": "所选区域未检测到像素变化", + "changesFound_other": "检测到 {{count}} 处画面变化", + "framesProcessed": "已处理 {{count}} 帧画面", + "jumpToTime": "跳转到该时间", + "results": "结果", + "showSegmentHeatmap": "热力图", + "newSearch": "新的搜索", + "clearResults": "清除结果", + "clearROI": "清除多边形选区", + "polygonControls": { + "points_other": "{{count}} 个点位", + "undo": "撤销上一个点位", + "reset": "重置多边形", + "drawMode": "绘制", + "moveMode": "移动" + }, + "motionHeatmapLabel": "画面变动热力图", + "dialog": { + "title": "画面变动搜索", + "cameraLabel": "摄像头", + "previewAlt": "{{camera}} 摄像头实时预览" + }, + "timeRange": { + "title": "搜索范围", + "start": "开始时间", + "end": "结束时间" + }, + "settings": { + "title": "搜索设置", + "parallelMode": "并行模式", + "parallelModeDesc": "同时扫描多个录制片段(速度更快,将使用更多解码资源)", + "threshold": "灵敏度阈值", + "thresholdDesc": "数值越低,可检测到越小的变化(取值范围 1-255)", + "minArea": "最小变化区域", + "minAreaDesc": "单个移动区域的最小尺寸,占目标区域的百分比", + "frameSkip": "帧跳过", + "frameSkipDesc": "每隔 N 帧进行一次处理。将该值设置为摄像头的帧率,即可实现每秒处理一帧画面(例如:5 帧 / 秒的摄像头设为 5,30 帧 / 秒的摄像头设为 30)。数值越高处理速度越快,但有可能遗漏短时移动侦测事件。", + "maxResults": "最大结果数", + "maxResultsDesc": "匹配到设定条数的录像事件后,就自动停止检索" + }, + "errors": { + "noCamera": "请选择摄像头", + "noROI": "请绘制感兴趣的区域", + "noTimeRange": "请选择时间范围", + "invalidTimeRange": "结束时间必须在开始时间之后", + "searchFailed": "搜索失败:{{message}}", + "polygonTooSmall": "多边形至少需要 3 个顶点", + "unknown": "未知错误" + }, + "changePercentage": "{{percentage}}% 已变化", + "metrics": { + "title": "搜索指标", + "segmentsScanned": "已扫描片段数", + "segmentsProcessed": "已处理", + "segmentsSkippedInactive": "已跳过(无活动)", + "segmentsSkippedHeatmap": "已跳过(不在感兴趣区域)", + "fallbackFullRange": "备用全范围扫描", + "framesDecoded": "画面已解码", + "wallTime": "搜索时间", + "segmentErrors": "片段异常", + "seconds": "{{seconds}} 秒", + "minutesSeconds": "{{minutes}}分 {{seconds}}秒", + "scanSummary": "{{segments}} 分段 · {{time}}" + }, + "scanning": "扫描中 {{time}}" +} diff --git a/web/public/locales/zh-CN/views/recording.json b/web/public/locales/zh-CN/views/recording.json new file mode 100644 index 0000000..77443e1 --- /dev/null +++ b/web/public/locales/zh-CN/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "导出", + "calendar": "日历", + "filter": "过滤器", + "filters": "筛选条件", + "toast": { + "error": { + "noValidTimeSelected": "未选择有效的时间范围", + "endTimeMustAfterStartTime": "结束时间必须晚于开始时间" + } + } +} diff --git a/web/public/locales/zh-CN/views/replay.json b/web/public/locales/zh-CN/views/replay.json new file mode 100644 index 0000000..00634af --- /dev/null +++ b/web/public/locales/zh-CN/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "调试回放", + "description": "回放摄像头录像以供调试。目标列表会延时展示已检测目标的汇总信息,消息标签页则实时展示回放录像对应的 Frigate 内部日志信息流。", + "websocket_messages": "消息", + "dialog": { + "title": "开始调试回放", + "description": "创建临时回放摄像头,循环播放历史录制视频,用于调试目标检测与追踪相关问题。临时回放的摄像头将沿用原摄像头的检测配置。请选择一个时间范围开始。", + "camera": "原摄像头", + "timeRange": "时间范围", + "preset": { + "1m": "最后 1 分钟", + "5m": "最后 5 分钟", + "timeline": "从时间线", + "custom": "自定义" + }, + "startButton": "开始回放", + "selectFromTimeline": "选择", + "starting": "开始回放…", + "startLabel": "开始", + "endLabel": "结束", + "toast": { + "error": "调试回放启动失败:{{error}}", + "alreadyActive": "已有回放会话正在运行", + "stopError": "调试回放停止失败:{{error}}", + "goToReplay": "进入回放" + } + }, + "page": { + "noSession": "没有正在进行的调试回放会话", + "noSessionDesc": "从历史回放页面启动调试回放:点击工具栏中的操作按钮,选择调试回放即可。", + "goToRecordings": "查看历史记录", + "preparingClip": "正在准备片段…", + "preparingClipDesc": "Frigate 正在拼接所选时间范围的录像片段。时间跨度较大时,该过程可能需要一分钟左右。", + "startingCamera": "开始调试回放中…", + "startError": { + "title": "调试回放启动失败", + "back": "返回历史记录" + }, + "sourceCamera": "源摄像头", + "replayCamera": "回放摄像头", + "initializingReplay": "初始化调试回放中…", + "stoppingReplay": "正在停止调试回放…", + "stopReplay": "停止回放", + "confirmStop": { + "title": "要停止调试回放吗?", + "description": "这将终止会话并清除所有临时数据。是否确定?", + "confirm": "停止回放", + "cancel": "取消" + }, + "activity": "活动", + "objects": "目标列表", + "audioDetections": "音频检测", + "noActivity": "未检测到活动", + "activeTracking": "活动追踪中", + "noActiveTracking": "没有活动追踪", + "configuration": "配置", + "configurationDesc": "微调调试回放摄像头的移动侦测与目标追踪参数。本次调整不会保存到你的 Frigate 配置文件中。" + } +} diff --git a/web/public/locales/zh-CN/views/search.json b/web/public/locales/zh-CN/views/search.json new file mode 100644 index 0000000..ffb7708 --- /dev/null +++ b/web/public/locales/zh-CN/views/search.json @@ -0,0 +1,75 @@ +{ + "search": "搜索", + "savedSearches": "已保存的搜索", + "searchFor": "搜索 {{inputValue}}", + "button": { + "clear": "清除搜索", + "save": "保存搜索", + "delete": "删除已保存的搜索", + "filterInformation": "筛选信息", + "filterActive": "筛选器已激活" + }, + "trackedObjectId": "追踪目标 ID", + "filter": { + "label": { + "cameras": "摄像头", + "labels": "标签", + "zones": "区域", + "sub_labels": "子标签", + "search_type": "搜索类型", + "time_range": "时间范围", + "before": "之前", + "after": "之后", + "min_score": "最低分数", + "max_score": "最高分数", + "min_speed": "最低速度", + "max_speed": "最高速度", + "recognized_license_plate": "识别的车牌", + "has_clip": "包含片段", + "has_snapshot": "包含快照", + "attributes": "属性" + }, + "searchType": { + "thumbnail": "缩略图", + "description": "描述" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "结束日期必须晚于开始日期。", + "afterDatebeEarlierBefore": "开始日期必须早于结束日期。", + "minScoreMustBeLessOrEqualMaxScore": "最低分数必须小于或等于最高分数。", + "maxScoreMustBeGreaterOrEqualMinScore": "最高分数必须大于或等于最低分数。", + "minSpeedMustBeLessOrEqualMaxSpeed": "最低速度必须小于或等于最高速度。", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "最高速度必须大于或等于最低速度。" + } + }, + "tips": { + "title": "如何使用文本筛选器", + "desc": { + "text": "筛选器可帮助您缩小搜索范围。注意,Jina v1 不支持中文搜索。以下是在输入字段中使用筛选器的方法:", + "step": "
    • 输入筛选器名称后跟一个冒号(例如:“cameras:”)。
    • 从建议中选择一个值或输入您自己的值。
    • 使用多个筛选器时,可以在它们之间用空格分隔。
    • 日期筛选器(before: 和 after:)使用 {{DateFormat}} 格式。
    • 时间范围筛选器使用 {{exampleTime}} 格式。
    • 点击筛选器旁边的“x”即可移除筛选条件。
    ", + "example": "示例:cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM", + "step2": "选择给出的建议值或自行输入;", + "step3": "多个过滤器之间用空格分隔;", + "step5": "时间范围过滤器使用 {{exampleTime}} 格式;", + "step6": "点击过滤器旁的'x'可移除该过滤选项。", + "exampleLabel": "范例:", + "step1": "输入过滤键名后接英文冒号(例如 \"cameras:\" );", + "step4": "日期过滤器(before: 和 after:)使用 {{DateFormat}} 格式;" + } + }, + "header": { + "currentFilterType": "筛选值", + "noFilters": "筛选条件", + "activeFilters": "激活的筛选项" + } + }, + "similaritySearch": { + "title": "相似搜索", + "active": "相似搜索已激活", + "clear": "清除相似搜索" + }, + "placeholder": { + "search": "搜索…" + } +} diff --git a/web/public/locales/zh-CN/views/settings.json b/web/public/locales/zh-CN/views/settings.json new file mode 100644 index 0000000..9a6dbc5 --- /dev/null +++ b/web/public/locales/zh-CN/views/settings.json @@ -0,0 +1,2236 @@ +{ + "documentTitle": { + "default": "设置 - Frigate", + "authentication": "身份验证设置 - Frigate", + "camera": "摄像头设置 - Frigate", + "classification": "分类设置 - Frigate", + "masksAndZones": "遮罩和区域编辑器 - Frigate", + "motionTuner": "画面变动调整 - Frigate", + "object": "调试 - Frigate", + "general": "界面设置 - Frigate", + "frigatePlus": "Frigate+ 设置 - Frigate", + "notifications": "通知设置 - Frigate", + "enrichments": "增强功能设置 - Frigate", + "cameraManagement": "管理摄像头 - Frigate", + "cameraReview": "摄像头核查设置 - Frigate", + "globalConfig": "全局配置 - Frigate", + "cameraConfig": "摄像头配置 - Frigate", + "maintenance": "维护 - Frigate", + "profiles": "配置模板 - Frigate", + "detectorsAndModel": "检测器和模型 - Frigate" + }, + "menu": { + "ui": "界面设置", + "classification": "分类设置", + "cameras": "摄像头设置", + "masksAndZones": "遮罩/ 区域", + "motionTuner": "画面变动调整", + "debug": "调试", + "users": "用户", + "notifications": "通知", + "frigateplus": "Frigate+", + "enrichments": "增强功能", + "triggers": "触发器", + "roles": "权限组", + "cameraManagement": "摄像头管理", + "cameraReview": "核查", + "globalDetect": "目标检测", + "general": "常规", + "globalConfig": "全局配置", + "system": "系统", + "integrations": "集成", + "profileSettings": "配置文件设置", + "globalRecording": "录制", + "globalSnapshots": "快照", + "globalFfmpeg": "FFmpeg", + "globalMotion": "画面变动检测", + "globalObjects": "目标", + "globalReview": "核查", + "globalAudioEvents": "音频检测", + "globalLivePlayback": "实时监控观看", + "globalTimestampStyle": "时间戳样式", + "systemDatabase": "数据库", + "systemTls": "TLS加密链接", + "systemAuthentication": "验证", + "systemNetworking": "网络", + "systemProxy": "反向代理", + "systemUi": "界面", + "systemLogging": "日志", + "systemEnvironmentVariables": "环境变量", + "systemTelemetry": "遥测", + "systemBirdseye": "鸟瞰图", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "检测器硬件", + "systemDetectionModel": "检测模型", + "systemMqtt": "MQTT", + "integrationSemanticSearch": "语义搜索", + "integrationGenerativeAi": "生成式 AI", + "integrationFaceRecognition": "人脸识别", + "integrationLpr": "车牌识别", + "integrationObjectClassification": "目标分类", + "integrationAudioTranscription": "音频转录", + "cameraDetect": "目标检测", + "cameraFfmpeg": "视频流(FFmpeg)", + "cameraRecording": "录制", + "cameraSnapshots": "快照", + "cameraMotion": "画面变动检测", + "cameraObjects": "目标", + "cameraConfigReview": "核查", + "cameraAudioEvents": "音频检测", + "cameraAudioTranscription": "音频转录", + "cameraNotifications": "通知", + "cameraLivePlayback": "实时监控观看", + "cameraBirdseye": "鸟瞰图", + "cameraFaceRecognition": "人脸识别", + "cameraLpr": "车牌识别", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "摄像头页面", + "cameraTimestampStyle": "时间戳样式", + "cameraMqtt": "摄像头 MQTT", + "mediaSync": "媒体同步", + "regionGrid": "区域网格", + "uiSettings": "界面设置", + "profiles": "配置模板", + "systemGo2rtcStreams": "go2rtc 视频流", + "maintenance": "维护", + "systemDetectorsAndModel": "检测器和模型" + }, + "dialog": { + "unsavedChanges": { + "title": "你有未保存的更改。", + "desc": "是否要在继续之前保存更改?" + } + }, + "cameraSetting": { + "camera": "摄像头", + "noCamera": "没有摄像头" + }, + "general": { + "title": "界面设置", + "liveDashboard": { + "title": "实时监控面板", + "automaticLiveView": { + "label": "自动实时预览", + "desc": "检测到画面活动时将自动切换至该摄像头实时画面。禁用此选项会导致实时监控页面的摄像头图像每分钟只更新一次。" + }, + "playAlertVideos": { + "label": "播放警报视频", + "desc": "默认情况下,实时监控页面上的最新警报会以一小段循环视频的形式进行播放。禁用此选项将仅显示浏览器本地缓存的静态图片。" + }, + "displayCameraNames": { + "label": "始终显示摄像头名称", + "desc": "在有多摄像头情况下的实时监控页面,将始终显示摄像头名称标签。" + }, + "liveFallbackTimeout": { + "label": "实时监控播放器回退超时", + "desc": "当摄像头的高清实时监控流不可用时,将在此时间后回退到低带宽模式。默认值:3秒。" + } + }, + "storedLayouts": { + "title": "存储监控面板布局", + "desc": "可以在监控面板调整或拖动摄像头的布局。这些设置将保存在浏览器的本地存储中。", + "clearAll": "清除所有布局" + }, + "cameraGroupStreaming": { + "title": "摄像头组视频流设置", + "desc": "每个摄像头组的视频流设置将保存在浏览器的本地存储中。", + "clearAll": "清除所有视频流设置" + }, + "recordingsViewer": { + "title": "回放查看", + "defaultPlaybackRate": { + "label": "默认播放速率", + "desc": "调整播放录像时默认的速率。" + } + }, + "calendar": { + "title": "日历", + "firstWeekday": { + "label": "每周第一天", + "desc": "设置每周第一天是星期几。", + "sunday": "星期天", + "monday": "星期一" + } + }, + "toast": { + "success": { + "clearStoredLayout": "已清除 {{cameraName}} 的存储布局", + "clearStreamingSettings": "已清除所有摄像头组的视频流设置。" + }, + "error": { + "clearStoredLayoutFailed": "清除存储布局失败:{{errorMessage}}", + "clearStreamingSettingsFailed": "清除视频流设置失败:{{errorMessage}}" + } + } + }, + "classification": { + "title": "分类设置", + "semanticSearch": { + "title": "语义搜索", + "desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。", + "readTheDocumentation": "阅读文档(英文)", + "reindexNow": { + "label": "立即重建索引", + "desc": "重建索引将为所有跟踪对象重新生成特征向量。该过程将在后台运行,可能会使CPU满载,所需时间取决于跟踪对象的数量。", + "confirmTitle": "确认重建索引", + "confirmDesc": "确定要为所有跟踪对象重建特征向量索引吗?此过程将在后台运行,但可能会导致CPU满载并耗费较长时间。您可以在 浏览 页面查看进度。", + "confirmButton": "重建索引", + "success": "重建索引已成功启动。", + "alreadyInProgress": "重建索引已在执行中。", + "error": "启动重建索引失败:{{errorMessage}}" + }, + "modelSize": { + "label": "模型大小", + "desc": "用于语义搜索的语言模型大小。", + "small": { + "title": "小", + "desc": "使用 模型。该模型将使用较少的内存,在CPU上也能较快的运行。质量较好。" + }, + "large": { + "title": "大", + "desc": "使用 模型。该模型采用了完整的Jina模型,并在适用的情况下使用GPU。" + } + } + }, + "faceRecognition": { + "title": "人脸识别", + "desc": "人脸识别功能允许为人物分配名称,当识别到他们的面孔时,Frigate 会将人物的名字作为子标签进行分配。这些信息会显示在界面、过滤器以及通知中。", + "readTheDocumentation": "阅读文档(英文)", + "modelSize": { + "label": "模型大小", + "desc": "用于人脸识别的模型尺寸。", + "small": { + "title": "小", + "desc": "使用模型将采用FaceNet人脸特征提取模型,可在大多数CPU上高效运行。" + }, + "large": { + "title": "大", + "desc": "使用模型将采用ArcFace人脸特征提取模型,若条件允许将自动使用GPU运行。" + } + } + }, + "licensePlateRecognition": { + "title": "车牌识别", + "desc": "Frigate 可以识别车辆的车牌,并自动将检测到的字符添加到 recognized_license_plate 字段中,或将已知名称作为子标签添加到汽车类型的对象中。常见的使用场景可能是读取驶入车道的汽车车牌或经过街道的汽车车牌。", + "readTheDocumentation": "阅读文档(英文)" + }, + "toast": { + "success": "分类设置已保存,请重启 Frigate 以应用更改。", + "error": "保存配置更改失败:{{errorMessage}}" + }, + "birdClassification": { + "title": "鸟类识别分类", + "desc": "鸟类识别分类采用量化TensorFlow模型识别已知鸟类。当识别到已知鸟类时,其通用名称将作为子标签(sub_label)添加。该信息将显示在用户界面、过滤器及通知中。" + }, + "restart_required": "需要重启(分类设置已修改)", + "unsavedChanges": "分类设置未保存" + }, + "camera": { + "title": "摄像头设置", + "streams": { + "title": "视频流", + "desc": "暂时禁用摄像头,除非重启Frigate否则将保持禁用。禁用摄像头将完全停止 Frigate 对该摄像头视频流的处理。检测、录制和调试功能都将不可用。
    注意:该选项不会禁用 go2rtc 转播。" + }, + "review": { + "title": "核查", + "desc": "启用/禁用摄像头的警报和检测。禁用后,除非重启Frigate,否则不会生成新的核查项。 ", + "alerts": "警报 ", + "detections": "检测 " + }, + "reviewClassification": { + "title": "核查分级", + "desc": "Frigate 将核查项分为“警报”和“检测”。默认情况下,所有的 汽车 对象都将视为警报。你可以通过修改配置文件配置区域来细分。", + "readTheDocumentation": "阅读文档", + "noDefinedZones": "该摄像头没有设置区域。", + "objectAlertsTips": "所有 {{alertsLabels}} 对象在 {{cameraName}} 下都将显示为警报。", + "zoneObjectAlertsTips": "所有 {{alertsLabels}} 类的目标或物体在 {{cameraName}} 下的 {{zone}} 区内都将显示为警报。", + "objectDetectionsTips": "所有未在 {{cameraName}} 归类的 {{detectionsLabels}} 目标或物体,无论它位于哪个区,都将显示为检测。", + "zoneObjectDetectionsTips": { + "text": "所有未在 {{cameraName}} 上归类为 {{detectionsLabels}} 的目标或物体在 {{zone}} 区内都将显示为检测。", + "notSelectDetections": "所有在 {{cameraName}} 下的 {{zone}} 区内检测到的 {{detectionsLabels}} 目标或物体,如果它未归类为警报,无论它位于哪个区,都将显示为检测。", + "regardlessOfZoneObjectDetectionsTips": "所有未在 {{cameraName}} 归类的 {{detectionsLabels}} 目标或物体,无论它位于哪个区域,都将显示为检测。" + }, + "selectAlertsZones": "选择警报区", + "selectDetectionsZones": "选择检测区域", + "limitDetections": "限制仅在特定区域内进行检测", + "toast": { + "success": "核查分级配置已保存。请重启 Frigate 以应用更改。" + }, + "unsavedChanges": "{{camera}} 的核查分类设置未保存" + }, + "object_descriptions": { + "title": "生成式AI对象描述", + "desc": "临时启用/禁用此摄像头的生成式AI对象描述功能。禁用后,系统将不再请求该摄像头追踪对象的AI生成描述。" + }, + "review_descriptions": { + "title": "生成式AI核查描述", + "desc": "临时启用/禁用本摄像头的生成式AI核查描述功能。禁用后,系统将不再为该摄像头的核查项目请求AI生成的描述内容。" + }, + "addCamera": "添加新摄像头", + "editCamera": "编辑摄像头:", + "selectCamera": "选择摄像头", + "backToSettings": "返回摄像头设置", + "cameraConfig": { + "add": "添加摄像头", + "edit": "编辑摄像头", + "description": "配置摄像头设置,包括视频流输入和视频流功能选择。", + "name": "摄像头名称", + "nameRequired": "摄像头名称为必填项", + "nameInvalid": "摄像头名称只能包含字母、数字、下划线或连字符", + "namePlaceholder": "比如:front_door", + "enabled": "开启", + "ffmpeg": { + "inputs": "视频流输入", + "path": "视频流路径", + "pathRequired": "视频流路径为必填项", + "pathPlaceholder": "rtsp://...", + "roles": "功能", + "rolesRequired": "至少需要指定一个功能", + "rolesUnique": "每个功能(音频、检测、录制)只能用于一个视频流,不能够重复分配到多个视频流", + "addInput": "添加视频流输入", + "removeInput": "移除视频流输入", + "inputsRequired": "至少需要一个视频流" + }, + "toast": { + "success": "摄像头 {{cameraName}} 保存已保存" + }, + "nameLength": "摄像头名称必须少于24个字符。" + } + }, + "masksAndZones": { + "filter": { + "all": "所有遮罩和区域" + }, + "toast": { + "success": { + "copyCoordinates": "已复制 {{polyName}} 的坐标到剪贴板。" + }, + "error": { + "copyCoordinatesFailed": "无法复制坐标到剪贴板。" + } + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "区域名称必须至少包含 2 个字符。", + "mustNotBeSameWithCamera": "区域名称不能与摄像头名称相同。", + "alreadyExists": "该摄像头已有相同的区域名称。", + "mustNotContainPeriod": "区域名称不能包含句点。", + "hasIllegalCharacter": "区域名称包含非法字符。", + "mustHaveAtLeastOneLetter": "区域名称必须至少包含一个字母。" + } + }, + "distance": { + "error": { + "text": "距离必须大于或等于 0.1。", + "mustBeFilled": "所有距离字段必须填写才能使用速度估算。" + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "惯性必须大于 0。" + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "徘徊时间必须大于或等于 0。" + } + }, + "polygonDrawing": { + "removeLastPoint": "删除最后一个点", + "reset": { + "label": "清除所有点" + }, + "snapPoints": { + "true": "启用点对齐", + "false": "禁用点对齐" + }, + "delete": { + "title": "确认删除", + "desc": "你确定要删除{{type}} “{{name}}” 吗?", + "success": "{{name}} 已被删除。" + }, + "error": { + "mustBeFinished": "多边形绘制必须完成闭合后才能保存。" + }, + "type": { + "zone": "区域", + "motion_mask": "画面变动遮罩", + "object_mask": "目标遮罩" + }, + "revertOverride": { + "title": "恢复为默认配置", + "desc": "这将移除针对 {{type}} {{name}} 的配置覆盖,并恢复为基础配置。" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "速度阈值必须大于或等于0.1。" + } + }, + "id": { + "error": { + "mustNotBeEmpty": "ID 不能为空。", + "alreadyExists": "此摄像头已存在使用该 ID 的遮罩。" + } + }, + "name": { + "error": { + "mustNotBeEmpty": "名称不能为空。" + } + } + }, + "zones": { + "label": "区域", + "documentTitle": "编辑区域 - Frigate", + "desc": { + "title": "该功能允许你定义特定区域,以便你可以确定特定目标或物体是否在该区域内。", + "documentation": "文档" + }, + "add": "添加区域", + "edit": "编辑区域", + "point_other": "{{count}} 点", + "clickDrawPolygon": "在图像上点击添加点绘制多边形区域。", + "name": { + "title": "区域名称", + "inputPlaceHolder": "请输入名称…", + "tips": "名称至少包含两个字符,且不能和摄像头名或该摄像头下的其他区域同名。" + }, + "inertia": { + "title": "惯性", + "desc": "识别指定目标前该目标必须在这个区域内出现了多少帧。默认值:3" + }, + "loiteringTime": { + "title": "停留时间", + "desc": "设置目标必须在区域中至少要活动多少时间(单位为秒)。默认值:0" + }, + "objects": { + "title": "目标/物体", + "desc": "将在此区域应用的目标/物体类别列表。" + }, + "allObjects": "所有目标/物体", + "speedEstimation": { + "title": "速度估算", + "desc": "启用此区域内物体的速度估算。该区域必须恰好包含 4 个点。", + "docs": "阅读文档", + "lineBDistance": "B线距离({{unit}})", + "lineCDistance": "C线距离({{unit}})", + "lineDDistance": "D线距离({{unit}})", + "lineADistance": "A线距离({{unit}})" + }, + "speedThreshold": { + "title": "速度阈值 ({{unit}})", + "desc": "指定物体在此区域内被视为有效的最低速度。", + "toast": { + "error": { + "pointLengthError": "此区域的速度估算已禁用。启用速度估算的区域必须恰好包含 4 个点。", + "loiteringTimeError": "徘徊时间大于 0 的区域不应与速度估算一起使用。" + } + } + }, + "toast": { + "success": "区域 ({{zoneName}}) 已保存。" + }, + "enabled": { + "title": "开启", + "description": "指示该区域在配置文件中是否处于激活并启用的状态。若被停用,则无法通过 MQTT 启用。禁用的区域在运行时会被忽略。" + } + }, + "motionMasks": { + "label": "画面变动遮罩", + "documentTitle": "编辑画面变动遮罩 - Frigate", + "desc": { + "title": "画面变动遮罩用于防止触发不必要的画面变动检测。过度的设置遮罩将使目标更加难以被追踪。", + "documentation": "文档" + }, + "add": "添加画面变动遮罩", + "edit": "编辑画面变动遮罩", + "context": { + "title": "画面变动遮罩用于防止不需要的画面变动触发检测(例如:容易被风吹动的树枝、摄像头画面上显示的时间等)。画面变动遮罩应谨慎使用,过度的遮罩会导致追踪目标变得更加困难。", + "documentation": "阅读文档" + }, + "point_other": "{{count}} 点", + "clickDrawPolygon": "在图像上点击添加点绘制多边形区域。", + "polygonAreaTooLarge": { + "title": "画面变动遮罩的大小达到了摄像头画面的{{polygonArea}}%。不建议设置太大的画面变动遮罩。", + "tips": "画面变动遮罩并不会使该区域无法检测到指定目标/物体,如有需要,你应该使用 区域 来限制检测的目标/物体类型。", + "documentation": "阅读文档" + }, + "toast": { + "success": { + "title": "{{polygonName}} 已保存。", + "noName": "画面变动遮罩已保存。" + } + }, + "defaultName": "画面变动遮罩 {{number}}", + "name": { + "title": "名称", + "description": "为该画面变动遮罩设置别名(可选)。", + "placeholder": "输入名称…" + } + }, + "objectMasks": { + "label": "目标遮罩", + "documentTitle": "编辑目标遮罩 - Frigate", + "desc": { + "title": "目标过滤器用于防止特定位置出现对某个目标/物体的误报。", + "documentation": "文档" + }, + "add": "添加目标遮罩", + "edit": "编辑目标遮罩", + "context": "目标过滤器用于防止特定位置的指定目标会误报。", + "point_other": "{{count}} 点", + "clickDrawPolygon": "在图像上点击添加点绘制多边形区域。", + "objects": { + "title": "目标/物体", + "desc": "将应用于此目标遮罩的目标或物体类型。", + "allObjectTypes": "所有目标或物体类型" + }, + "toast": { + "success": { + "title": "{{polygonName}} 已保存。", + "noName": "目标遮罩已保存。" + } + }, + "name": { + "title": "名称", + "description": "为该目标遮罩设置别名(可选)。", + "placeholder": "输入名称…" + } + }, + "restart_required": "需要重启(遮罩与区域已修改)", + "motionMaskLabel": "画面变动遮罩 {{number}}", + "objectMaskLabel": "目标/物体遮罩 {{number}}", + "disabledInConfig": "该项目已在配置文件中被禁用", + "masks": { + "enabled": { + "title": "开启", + "description": "指示该遮罩在配置文件中是否处于激活并启用的状态。若被禁用,则无法通过 MQTT 启用。禁用的遮罩在运行时会被忽略。" + } + }, + "profileBase": "(基础)", + "profileOverride": "(覆盖)", + "addDisabledProfile": "先添加到基础配置中,然后在配置模板中进行覆盖" + }, + "motionDetectionTuner": { + "title": "画面变动检测调整", + "desc": { + "title": "Frigate 将首先使用画面变动检测来确认每一帧画面中是否有变动的区域,然后再对该区域使用目标检测。", + "documentation": "阅读有关画面变动检测的文档" + }, + "Threshold": { + "title": "阈值", + "desc": "阈值决定像素亮度变化达到多少时会被认为是画面变动。默认值:30" + }, + "contourArea": { + "title": "轮廓面积", + "desc": "轮廓面积值用于判断产生了多大的变化区域可被认定为画面变动。默认值:10" + }, + "improveContrast": { + "title": "提高对比度", + "desc": "提高较暗场景的对比度。默认值:启用" + }, + "toast": { + "success": "画面变动设置已保存。" + }, + "unsavedChanges": "{{camera}} 的画面变动调整设置未保存" + }, + "debug": { + "title": "调试", + "detectorDesc": "Frigate 将使用检测器({{detectors}})来检测摄像头视频流中的目标或物体。", + "desc": "调试界面将实时显示被追踪的目标以及统计信息,目标列表将显示检测到的目标和延迟显示的概览。", + "debugging": "调试选项", + "objectList": "目标列表", + "noObjects": "没有目标", + "boundingBoxes": { + "title": "边界框", + "desc": "将在被追踪的目标周围显示边界框", + "colors": { + "label": "目标边界框颜色定义", + "info": "
  • 启用后,将会为每个目标的标签分配不同的颜色
  • 深蓝色细线代表该目标或物体在当前时间点未被检测到
  • 灰色细线代表检测到的目标或物体静止不动
  • 粗线表示在启动自动追踪时,该目标为自动追踪的主体
  • " + } + }, + "timestamp": { + "title": "时间戳", + "desc": "在图像上显示时间戳" + }, + "zones": { + "title": "区域", + "desc": "显示已定义的区域图层" + }, + "mask": { + "title": "画面变动遮罩", + "desc": "显示画面变动遮罩图层" + }, + "motion": { + "title": "画面变动区域框", + "desc": "在检测到画面变动的区域显示区域框", + "tips": "

    画面变动区域框


    将在当前检测到画面变动的区域内显示红色区域框。

    " + }, + "regions": { + "title": "范围", + "desc": "显示发送给目标检测器感兴趣的区域框", + "tips": "

    范围框


    将在帧中发送到目标检测器的感兴趣范围上叠加绿色框。

    " + }, + "objectShapeFilterDrawing": { + "title": "允许绘制“目标形状过滤器”", + "desc": "在图像上绘制矩形,以查看区域和比例详细信息", + "tips": "启用此选项,能够在摄像头画面上绘制矩形,将显示其区域和比例。你可以通过使用这些值在配置中设置目标形状过滤器的参数。", + "document": "阅读文档 ", + "score": "分数", + "ratio": "比例", + "area": "区域" + }, + "paths": { + "title": "行动轨迹", + "desc": "显示被追踪目标的行动轨迹关键点", + "tips": "

    行动轨迹

    将使用线条来标示被追踪目标在其活动周期内移动的关键位置点。

    " + }, + "audio": { + "title": "音频", + "noAudioDetections": "未检测到音频事件", + "score": "分值", + "currentRMS": "当前均方根值(RMS)", + "currentdbFS": "当前满量程相对分贝值(dbFS)" + }, + "openCameraWebUI": "打开 {{camera}} 的管理页面" + }, + "users": { + "title": "用户", + "management": { + "title": "用户管理", + "desc": "管理此 Frigate 实例的用户账户。" + }, + "addUser": "添加用户", + "updatePassword": "修改密码", + "toast": { + "success": { + "createUser": "用户 {{user}} 创建成功", + "deleteUser": "用户 {{user}} 删除成功", + "updatePassword": "已成功修改密码。", + "roleUpdated": "已更新 {{user}} 的权限组" + }, + "error": { + "setPasswordFailed": "保存密码出现错误:{{errorMessage}}", + "createUserFailed": "创建用户失败:{{errorMessage}}", + "deleteUserFailed": "删除用户失败:{{errorMessage}}", + "roleUpdateFailed": "更新权限组失败:{{errorMessage}}" + } + }, + "table": { + "username": "用户名", + "actions": "操作", + "role": "权限组", + "noUsers": "未找到用户。", + "changeRole": "更改用户权限组", + "password": "修改密码", + "deleteUser": "删除用户" + }, + "dialog": { + "form": { + "user": { + "title": "用户名", + "desc": "仅允许使用字母、数字、句点和下划线。", + "placeholder": "请输入用户名" + }, + "password": { + "title": "密码", + "placeholder": "请输入密码", + "confirm": { + "title": "确认密码", + "placeholder": "请再次输入密码" + }, + "strength": { + "title": "密码强度: ", + "weak": "弱", + "medium": "中等", + "strong": "强", + "veryStrong": "非常强" + }, + "match": "密码匹配", + "notMatch": "密码不匹配", + "show": "显示密码", + "hide": "隐藏密码", + "requirements": { + "title": "密码要求:", + "length": "至少需要 12 位字符", + "uppercase": "至少一个大写字母", + "digit": "至少一位数字", + "special": "至少一个特殊符号 (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "新密码", + "placeholder": "请输入新密码", + "confirm": { + "placeholder": "请再次输入新密码" + } + }, + "usernameIsRequired": "用户名为必填项", + "passwordIsRequired": "必须输入密码", + "currentPassword": { + "title": "当前密码", + "placeholder": "请输入当前密码" + } + }, + "createUser": { + "title": "创建新用户", + "desc": "创建一个新用户账户,并指定一个权限组以控制访问 Frigate 页面的权限。", + "usernameOnlyInclude": "用户名只能包含字母、数字和 _", + "confirmPassword": "请确认你的密码" + }, + "deleteUser": { + "title": "删除该用户", + "desc": "此操作无法撤销。这将永久删除用户账户并移除所有相关数据。", + "warn": "你确定要删除 {{username}} 吗?" + }, + "passwordSetting": { + "updatePassword": "更新 {{username}} 的密码", + "setPassword": "设置密码", + "desc": "创建一个强密码来保护此账户。", + "doNotMatch": "两次输入密码不匹配", + "cannotBeEmpty": "密码不能为空", + "currentPasswordRequired": "当前密码为必填", + "incorrectCurrentPassword": "当前密码错误", + "passwordVerificationFailed": "验证密码失败", + "multiDeviceWarning": "其他已登录的设备将需要在 {{refresh_time}} 内重新登录。", + "multiDeviceAdmin": "你也可以通过轮换你的 JWT 密钥,强制所有用户立即重新登录验证。" + }, + "changeRole": { + "title": "更改用户权限组", + "desc": "更新 {{username}} 的权限", + "roleInfo": { + "admin": "管理员", + "viewer": "成员", + "viewerDesc": "仅能够查看实时监控面板、核查、浏览和导出功能。", + "adminDesc": "完全功能与访问权限。", + "intro": "为该用户选择一个合适的权限组:", + "customDesc": "自定义特定摄像头的访问规则。" + }, + "select": "选择权限组" + } + } + }, + "notification": { + "title": "通知", + "notificationSettings": { + "title": "通知设置", + "desc": "Frigate 在浏览器中运行或作为 PWA 安装时,可以原生向您的设备发送推送通知。", + "documentation": "阅读文档" + }, + "globalSettings": { + "title": "全局设置", + "desc": "临时暂停所有已注册设备上特定摄像头的通知。" + }, + "notificationUnavailable": { + "title": "通知功能不可用", + "desc": "网页推送通知需要安全连接(https://…)。这是浏览器的限制。请通过安全方式访问 Frigate 以使用通知功能。", + "documentation": "阅读文档", + "descPwa": "在 iOS 设备上,只有将 Frigate 安装到主屏幕后,才能使用网页推送通知。请打开分享菜单,选择添加到主屏幕,然后从新生成的图标打开 Frigate,即可注册此设备以接收通知。" + }, + "email": { + "title": "电子邮箱", + "placeholder": "例如:example@email.com", + "desc": "需要输入有效的电子邮件,在推送服务出现问题时,将使用此电子邮件进行通知。" + }, + "cameras": { + "title": "摄像头", + "noCameras": "没有可用的摄像头", + "desc": "选择要启用通知的摄像头。" + }, + "deviceSpecific": "设备专用设置", + "registerDevice": "注册该设备", + "unregisterDevice": "取消注册该设备", + "sendTestNotification": "发送测试通知", + "active": "通知已启用", + "suspended": "通知已暂停 {{time}}", + "suspendTime": { + "5minutes": "暂停 5 分钟", + "10minutes": "暂停 10 分钟", + "30minutes": "暂停 30 分钟", + "1hour": "暂停 1 小时", + "12hours": "暂停 12 小时", + "24hours": "暂停 24 小时", + "untilRestart": "暂停直到重启", + "suspend": "暂停" + }, + "cancelSuspension": "取消暂停", + "toast": { + "success": { + "registered": "已成功注册通知。需要重启 Frigate 才能发送任何通知(包括测试通知)。", + "settingSaved": "通知设置已保存。" + }, + "error": { + "registerFailed": "通知注册失败。" + } + }, + "unsavedRegistrations": "未保存通知注册", + "unsavedChanges": "未保存通知设置更改" + }, + "frigatePlus": { + "title": "Frigate+ 设置", + "apiKey": { + "title": "Frigate+ API 密钥", + "validated": "Frigate+ API 密钥已检测并验证通过", + "notValidated": "未检测到 Frigate+ API 密钥或验证未通过", + "desc": "Frigate+ API 密钥用于启用与 Frigate+ 服务的集成。", + "plusLink": "了解更多关于 Frigate+" + }, + "snapshotConfig": { + "title": "快照配置", + "desc": "提交到 Frigate+ 需要同时在配置中开启快照功能。", + "documentation": "阅读文档", + "cleanCopyWarning": "部分摄像头未开启快照功能", + "table": { + "camera": "摄像头", + "snapshots": "快照", + "cleanCopySnapshots": "clean_copy 快照" + } + }, + "modelInfo": { + "title": "模型信息", + "modelType": "模型类型", + "trainDate": "训练日期", + "baseModel": "基础模型", + "supportedDetectors": "支持的检测器", + "dimensions": "大小", + "cameras": "摄像头", + "loading": "正在加载模型信息…", + "error": "加载模型信息失败", + "availableModels": "可用 Frigate+ 模型", + "loadingAvailableModels": "正在加载可用模型…", + "modelSelect": "您可以在Frigate+上选择可用的模型。请注意,只能选择与当前检测器配置兼容的模型。", + "plusModelType": { + "baseModel": "基础模型", + "userModel": "定向调优" + }, + "noModelLoaded": "当前未加载任何Frigate+模型。", + "selectModel": "选择一个模型", + "noModelsAvailable": "无可用模型", + "filter": { + "ariaLabel": "按类型筛选模型", + "baseModels": "基础模型", + "fineTunedModels": "微调过的模型" + } + }, + "toast": { + "success": "Frigate+ 设置已保存。请重启 Frigate 以应用更改。", + "error": "配置更改保存失败:{{errorMessage}}" + }, + "restart_required": "需要重启(Frigate+模型已修改)", + "unsavedChanges": "未保存Frigate+变更设置", + "description": "Frigate+ 是一项订阅服务,可为你的 Frigate 实例提供额外的功能和能力,包括使用基于你自己的数据训练的自定义目标检测模型。你可以在此管理 Frigate+ 的模型设置。", + "cardTitles": { + "api": "API", + "currentModel": "当前模型", + "otherModels": "其他模型", + "configuration": "配置" + }, + "changeInDetectorsAndModel": "改变模型" + }, + "enrichments": { + "title": "增强功能设置", + "birdClassification": { + "desc": "鸟类分类通过量化的 TensorFlow 模型识别已知鸟类。当识别到已知鸟类时,其通用名称将作为子标签(sub_label)添加。此信息包含在用户界面、筛选器以及通知中。", + "title": "鸟类分类" + }, + "semanticSearch": { + "reindexNow": { + "desc": "重建索引将为所有追踪的目标重新生成特征向量信息。该过程将在后台进行,期间可能会使 CPU 满载,所需时间取决于追踪目标的数量。", + "label": "立即重建索引", + "confirmTitle": "确认重建索引", + "confirmDesc": "确定要为所有追踪目标重建特征向量索引信息吗?此过程将在后台进行,但可能会导致CPU满载并耗费较长时间。您可以在 浏览 页面查看进度。", + "confirmButton": "重建索引", + "success": "重建索引已成功启动。", + "alreadyInProgress": "重建索引已在执行中。", + "error": "启动重建索引失败:{{errorMessage}}" + }, + "modelSize": { + "label": "模型大小", + "desc": "用于语义搜索的语言模型大小。", + "small": { + "title": "小", + "desc": "将使用 模型。该模型使用的内存较少,在 CPU 上也能较快的运行,质量较好。" + }, + "large": { + "title": "大", + "desc": "将使用 模型。该选项使用了完整的 Jina 模型,条件允许的情况下将自动使用 GPU 运行。" + } + }, + "title": "语义搜索", + "desc": "Frigate 中的语义搜索功能将能够让你通过图片、用户自定义的文本描述,或自动生成的文本描述等方式在核查项目中查找目标/物体。", + "readTheDocumentation": "阅读文档" + }, + "licensePlateRecognition": { + "desc": "Frigate 可以识别车辆的车牌,并自动将检测到的字符添加到 识别的车牌(recognized_license_plate)字段中,或将已知车牌对应的名称作为子标签添加到该车辆目标中。该功能常用于识别驶入车道的车辆车牌或经过街道的车辆车牌。", + "title": "车牌识别", + "readTheDocumentation": "阅读文档" + }, + "faceRecognition": { + "desc": "人脸识别功能允许为人物分配名称,当识别到他们的面孔时,Frigate 会将人物的名字作为子标签进行分配。这些信息会显示在界面、过滤器以及通知中。", + "title": "人脸识别", + "readTheDocumentation": "阅读文档", + "modelSize": { + "label": "模型大小", + "desc": "用于人脸识别的模型大小。", + "small": { + "title": "小", + "desc": "将使用模型。该选项采用 FaceNet 人脸特征提取模型,可在大多数 CPU 上高效运行。" + }, + "large": { + "title": "大", + "desc": "将使用模型。该选项使用 ArcFace 人脸特征提取模型,条件允许的情况下将自动使用 GPU 运行。" + } + } + }, + "toast": { + "success": "增强功能设置已保存。请重启 Frigate 以应用更改。", + "error": "配置更改保存失败:{{errorMessage}}" + }, + "unsavedChanges": "增强功能设置未保存", + "restart_required": "需要重启(增强功能设置已保存)" + }, + "triggers": { + "documentTitle": "触发器", + "management": { + "title": "触发器", + "desc": "管理 {{camera}} 的触发器。你可以选择“缩略图”类型,将通过与追踪目标相似的缩略图来触发;也可以通过“描述”类型,与你描述的文本相似来触发(中文描述需要使用 jina v2模型,对配置要求更高)。" + }, + "addTrigger": "添加触发器", + "table": { + "name": "名称", + "type": "类型", + "content": "触发内容", + "threshold": "阈值", + "actions": "动作", + "noTriggers": "此摄像头未配置任何触发器。", + "edit": "编辑", + "deleteTrigger": "删除触发器", + "lastTriggered": "最后一个触发项" + }, + "type": { + "thumbnail": "缩略图", + "description": "描述" + }, + "actions": { + "alert": "标记为警报", + "notification": "发送通知", + "sub_label": "添加子标签", + "attribute": "添加属性" + }, + "dialog": { + "createTrigger": { + "title": "创建触发器", + "desc": "为摄像头 {{camera}} 创建触发器" + }, + "editTrigger": { + "title": "编辑触发器", + "desc": "编辑摄像头 {{camera}} 的触发器设置" + }, + "deleteTrigger": { + "title": "删除触发器", + "desc": "你确定要删除触发器 {{triggerName}} 吗?此操作不可撤销。" + }, + "form": { + "name": { + "title": "名称", + "placeholder": "触发器名称", + "error": { + "minLength": "该字段至少需要两个字符。", + "invalidCharacters": "该字段只能包含字母、数字、下划线和连字符。", + "alreadyExists": "此摄像头已存在同名触发器。" + }, + "description": "请输入用于识别此触发器的唯一名称或描述" + }, + "enabled": { + "description": "开启/关闭此触发器" + }, + "type": { + "title": "类型", + "placeholder": "选择触发类型", + "description": "当检测到相似的追踪目标描述时触发", + "thumbnail": "当检测到相似的追踪目标缩略图时触发" + }, + "content": { + "title": "内容", + "imagePlaceholder": "选择图片", + "textPlaceholder": "输入文字内容", + "imageDesc": "仅显示最近的 100 张缩略图。如果找不到需要的图片,请前往“浏览”页面查看更早的目标,并从菜单中设置触发器。", + "textDesc": "输入文本,当检测到相似的追踪目标描述时触发此操作。", + "error": { + "required": "内容为必填项。" + } + }, + "threshold": { + "title": "阈值", + "error": { + "min": "阈值必须大于 0", + "max": "阈值必须小于 1" + }, + "desc": "设置此触发器的相似度阈值。阈值越高,触发所需的匹配就越精确。" + }, + "actions": { + "title": "动作", + "desc": "默认情况下,Frigate 会为所有触发器发送 MQTT 消息。子标签会将触发器名称添加到目标标签中。属性是可搜索的元数据,独立存储在追踪目标的元数据中。", + "error": { + "min": "必须至少选择一项动作。" + } + }, + "friendly_name": { + "title": "友好名称", + "placeholder": "为此触发器命名或添加描述", + "description": "(可选)为触发器添加友好名称或描述。" + } + } + }, + "toast": { + "success": { + "createTrigger": "触发器 {{name}} 创建成功。", + "updateTrigger": "触发器 {{name}} 更新成功。", + "deleteTrigger": "触发器 {{name}} 已删除。" + }, + "error": { + "createTriggerFailed": "创建触发器失败:{{errorMessage}}", + "updateTriggerFailed": "更新触发器失败:{{errorMessage}}", + "deleteTriggerFailed": "删除触发器失败:{{errorMessage}}" + } + }, + "semanticSearch": { + "title": "语义搜索已关闭", + "desc": "必须启用语义搜索功能才能使用触发器。" + }, + "wizard": { + "title": "创建触发器", + "step1": { + "description": "配置触发器的基础设置。" + }, + "step2": { + "description": "设置触发此操作的内容。" + }, + "step3": { + "description": "配置此触发器的相似度阈值与执行动作。" + }, + "steps": { + "nameAndType": "名称与类型", + "configureData": "配置数据", + "thresholdAndActions": "阈值与动作" + } + } + }, + "roles": { + "management": { + "title": "成员权限组管理", + "desc": "管理此 Frigate 实例的自定义权限组及其摄像头访问权限。" + }, + "addRole": "添加权限组", + "table": { + "role": "权限组", + "cameras": "摄像头", + "actions": "操作", + "noRoles": "没有找到自定义权限组。", + "editCameras": "编辑摄像头", + "deleteRole": "删除权限组" + }, + "toast": { + "success": { + "createRole": "权限组 {{role}} 创建成功", + "updateCameras": "已更新摄像头至 {{role}} 权限组", + "deleteRole": "已删除 {{role}} 权限组", + "userRolesUpdated_other": "已将分配到此权限组的 {{count}} 位用户更新为 “成员”,该权限组可访问所有摄像头。" + }, + "error": { + "createRoleFailed": "创建权限组失败:{{errorMessage}}", + "updateCamerasFailed": "更新摄像头失败:{{errorMessage}}", + "deleteRoleFailed": "删除权限组失败:{{errorMessage}}", + "userUpdateFailed": "更新用户权限组失败:{{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "创建新权限组", + "desc": "添加新权限组并分配摄像头访问权限。" + }, + "editCameras": { + "title": "编辑权限组的摄像头", + "desc": "为权限组 {{role}} 更新摄像头访问权限。" + }, + "deleteRole": { + "title": "删除权限组", + "desc": "此操作无法撤销。这将永久删除该权限组,并将所有拥有此权限组的用户分配到 “成员” (view)权限组,该权限组将赋予用户查看所有摄像头的权限。", + "warn": "你确定要删除权限组 {{role}} 吗?", + "deleting": "删除中…" + }, + "form": { + "role": { + "title": "权限组名称", + "placeholder": "输入权限组名称", + "desc": "仅允许使用字母、数字、句点和下划线。", + "roleIsRequired": "必须输入权限组名称", + "roleOnlyInclude": "权限组名称仅支持字母、数字、英文句号和下划线", + "roleExists": "该权限组名称已存在。" + }, + "cameras": { + "title": "摄像头", + "desc": "请选择该权限组能够访问的摄像头。至少需要选择一个摄像头。", + "required": "至少要选择一个摄像头。" + } + } + } + }, + "cameraWizard": { + "title": "添加摄像头", + "description": "请按照以下步骤添加摄像头至 Frigate 中。", + "steps": { + "nameAndConnection": "名称与连接", + "streamConfiguration": "视频流配置", + "validationAndTesting": "验证与测试", + "probeOrSnapshot": "探测或快照" + }, + "save": { + "success": "已保存新摄像头 {{cameraName}}。", + "failure": "保存摄像头 {{cameraName}} 遇到了错误。" + }, + "testResultLabels": { + "resolution": "分辨率", + "video": "视频", + "audio": "音频", + "fps": "帧率" + }, + "commonErrors": { + "noUrl": "请提供正确的视频流地址", + "testFailed": "视频流测试失败:{{error}}" + }, + "step1": { + "description": "请输入你的摄像头信息,并选择是自动探测摄像头信息还是手动指定品牌。", + "cameraName": "摄像头名称", + "cameraNamePlaceholder": "例如:大门,后院等", + "host": "主机/IP地址", + "port": "端口号", + "username": "用户名", + "usernamePlaceholder": "可选", + "password": "密码", + "passwordPlaceholder": "可选", + "selectTransport": "选择传输协议", + "cameraBrand": "摄像头品牌", + "selectBrand": "选择摄像头品牌用于生成URL地址模板", + "customUrl": "自定义视频流地址", + "brandInformation": "品牌信息", + "brandUrlFormat": "对于采用RTSP URL格式的摄像头,其格式为:{{exampleUrl}}", + "customUrlPlaceholder": "rtsp://用户名:密码@主机或IP地址:端口/路径", + "testConnection": "测试连接", + "testSuccess": "连接测试通过!", + "testFailed": "连接测试失败。请检查输入是否正确并重试。", + "streamDetails": "视频流信息", + "warnings": { + "noSnapshot": "无法从配置的视频流中获取快照。" + }, + "errors": { + "brandOrCustomUrlRequired": "请选择摄像头品牌并配置主机/ IP 地址,或选择“其他”后手动配置视频流地址", + "nameRequired": "摄像头名称为必填项", + "nameLength": "摄像头名称要少于64个字符", + "invalidCharacters": "摄像头名称内有不允许使用的字符", + "nameExists": "该摄像头名称已存在", + "brands": { + "reolink-rtsp": "不建议使用萤石 RTSP 协议。建议在摄像头设置中启用 HTTP 协议,并重新运行摄像头添加向导。" + }, + "customUrlRtspRequired": "自定义 URL 必须以“rtsp://”或“rtsps://”开头;对于非 RTSP 协议的摄像头流,需手动添加至配置文件。" + }, + "docs": { + "reolink": "https://docs.frigate-cn.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "正在获取摄像头基本数据……", + "fetchingSnapshot": "正在获取摄像头快照……" + }, + "connectionSettings": "连接设置", + "detectionMethod": "视频流检测方法", + "onvifPort": "ONVIF 端口", + "probeMode": "探测摄像头", + "manualMode": "手动选择", + "detectionMethodDescription": "如果摄像头支持 ONVIF 协议,将使用该协议探测摄像头,以自动获取摄像头视频流地址;若不支持,也可手动选择摄像头品牌来使用预设地址。如需输入自定义RTSP地址,请选择“手动选择”并选择“其他”选项。", + "onvifPortDescription": "对于支持ONVIF协议的摄像头,该端口通常为80或8080。", + "useDigestAuth": "使用摘要认证", + "useDigestAuthDescription": "为 ONVIF 协议启用 HTTP 摘要认证。部分摄像头可能需要专用的 ONVIF 用户名/密码,而非默认的 admin 账户。" + }, + "step2": { + "description": "将根据你选择的检测方式,将会自动查找摄像头可用流配置,或进行手动配置。", + "streamsTitle": "摄像头视频流", + "addStream": "添加视频流", + "addAnotherStream": "添加另一个视频流", + "streamTitle": "{{number}} 号视频流", + "streamUrl": "视频流地址", + "streamUrlPlaceholder": "rtsp://用户名:密码@主机或IP:端口/路径", + "url": "URL地址", + "resolution": "分辨率", + "selectResolution": "选择分辨率", + "quality": "质量", + "selectQuality": "选择质量", + "roles": "功能", + "roleLabels": { + "detect": "目标/物体检测", + "record": "录制", + "audio": "音频" + }, + "testStream": "测试连接", + "testSuccess": "视频流测试成功!", + "testFailed": "连接测试失败,请检查您的输入后重试。", + "testFailedTitle": "测试失败", + "connected": "已连接", + "notConnected": "未连接", + "featuresTitle": "特殊功能", + "go2rtc": "减少摄像头连接数", + "detectRoleWarning": "至少需要一个视频流分配\"detect\"功能才能继续。", + "rolesPopover": { + "title": "视频流功能", + "detect": "目标/物体的主数据流。", + "record": "根据配置设置保存视频流的片段。", + "audio": "用于音频的检测的输入流。" + }, + "featuresPopover": { + "title": "视频流特殊功能", + "description": "将使用go2rtc的转流功能来减少摄像头连接数。" + }, + "streamDetails": "视频流详情", + "probing": "正在检测摄像头中……", + "retry": "重试", + "testing": { + "probingMetadata": "正在查询摄像头参数……", + "fetchingSnapshot": "正在获取摄像头快照……" + }, + "probeFailed": "检测摄像头失败:{{error}}", + "probingDevice": "寻找设备中……", + "probeSuccessful": "检测成功", + "probeError": "检测遇到错误", + "probeNoSuccess": "检测未成功", + "deviceInfo": "设备信息", + "manufacturer": "制造商", + "model": "型号", + "firmware": "固件", + "profiles": "配置文件", + "ptzSupport": "支持 PTZ", + "autotrackingSupport": "支持自动追踪", + "presets": "预设配置", + "rtspCandidates": "RTSP候选地址", + "rtspCandidatesDescription": "通过摄像头自动检测发现了以下RTSP地址。测试连接以查看视频流参数。", + "noRtspCandidates": "未从摄像头检测到任何 RTSP 地址。可能是你的账号密码错误,或者摄像头不支持 ONVIF 协议,亦或是当前采用的 RTSP 地址获取方式无效。请返回上一步,尝试手动输入RTSP地址。", + "candidateStreamTitle": "候选{{number}}", + "useCandidate": "使用", + "uriCopy": "复制", + "uriCopied": "地址已复制到剪贴板", + "testConnection": "测试连接", + "toggleUriView": "点击切换完整 URI 显示", + "errors": { + "hostRequired": "主机/IP地址为必填" + } + }, + "step3": { + "description": "为你的摄像头配置视频流功能并添加额外视频流。", + "validationTitle": "视频流验证", + "connectAllStreams": "连接所有视频流", + "reconnectionSuccess": "重连成功。", + "reconnectionPartial": "有些视频流重连失败了。", + "streamUnavailable": "视频流预览不可用", + "reload": "重新加载", + "connecting": "连接中……", + "streamTitle": "{{number}} 号视频流", + "valid": "通过", + "failed": "失败", + "notTested": "未测试", + "connectStream": "连接", + "connectingStream": "连接中", + "disconnectStream": "断开连接", + "estimatedBandwidth": "预计带宽", + "roles": "功能", + "none": "无", + "error": "错误", + "streamValidated": "{{number}} 号视频流验证通过", + "streamValidationFailed": "{{number}} 号视频流验证失败", + "saveAndApply": "保存新摄像头", + "saveError": "配置无效,请检查你的设置。", + "issues": { + "title": "视频流验证", + "videoCodecGood": "视频编码为 {{codec}}。", + "audioCodecGood": "音频编码为 {{codec}}。", + "noAudioWarning": "未检测到此视频流包含音频,录制将不会有声音。", + "audioCodecRecordError": "录制音频需要支持AAC音频编码器。", + "audioCodecRequired": "需要带音频的流才能开启声音检测。", + "restreamingWarning": "为录制流开启减少与摄像头的连接数可能会导致 CPU 使用率略有提升。", + "dahua": { + "substreamWarning": "子码流1被锁定为低分辨率。多数大华的摄像头支持额外的子码流,但需要在摄像头设置中手动开启。如果可以,建议检查并使用这些子码流。" + }, + "hikvision": { + "substreamWarning": "子码流1被锁定为低分辨率。多数海康威视的摄像头支持额外的子码流,但需要在摄像头设置中手动开启。如果可以,建议检查并使用这些子码流。" + }, + "resolutionHigh": "使用 {{resolution}} 分辨率可能会导致占用更多的系统资源。", + "resolutionLow": "使用 {{resolution}} 分辨率可能过低,难以检测较小的物体。" + }, + "ffmpegModule": "使用视频流兼容模式", + "ffmpegModuleDescription": "如果多次尝试后视频流仍无法加载,可以尝试启用此功能。启用后,Frigate 将使用集成 go2rtc 的 ffmpeg 模块,这可能会提高与某些摄像头视频流的兼容性。", + "streamsTitle": "摄像头视频流", + "addStream": "添加视频流", + "addAnotherStream": "添加其他视频流", + "streamUrl": "视频流地址", + "streamUrlPlaceholder": "rtsp://用户名:密码@主机:端口/路径", + "selectStream": "选择一个视频流", + "searchCandidates": "搜索候选项……", + "noStreamFound": "没有找到视频流", + "url": "URL地址", + "resolution": "分辨率", + "selectResolution": "选择分辨率", + "quality": "质量", + "selectQuality": "选择质量", + "roleLabels": { + "detect": "目标检测", + "record": "录制", + "audio": "音频检测" + }, + "testStream": "测试连接", + "testSuccess": "视频流测试成功!", + "testFailed": "视频流测试失败", + "testFailedTitle": "测试失败", + "connected": "已连接", + "notConnected": "未连接", + "featuresTitle": "功能特性", + "go2rtc": "减少与摄像头的连接数", + "detectRoleWarning": "必须得有一个视频流设置了“检测”功能才能继续操作。", + "rolesPopover": { + "title": "视频流功能", + "detect": "用于目标检测的主码流。", + "record": "根据配置设置保存视频流片段。", + "audio": "用于音频检测的音视频流。" + }, + "featuresPopover": { + "title": "视频流功能特性", + "description": "使用 go2rtc 中继转流功能,减少与摄像头的网络连接数,提升效率。" + } + }, + "step4": { + "description": "将进行保存新摄像头配置前的最终验证与分析,请在保存前确保所有视频流均已连接。", + "validationTitle": "视频流验证", + "connectAllStreams": "连接所有视频流", + "reconnectionSuccess": "重新连接成功。", + "reconnectionPartial": "部分视频流重新连接失败。", + "streamUnavailable": "视频流预览不可用", + "reload": "重新加载", + "connecting": "连接中……", + "streamTitle": "视频流 {{number}}", + "valid": "通过", + "failed": "失败", + "notTested": "未测试", + "connectStream": "连接", + "connectingStream": "连接中", + "disconnectStream": "断开连接", + "estimatedBandwidth": "预估带宽", + "roles": "功能", + "ffmpegModule": "使用视频流兼容模式", + "ffmpegModuleDescription": "若多次尝试后仍无法加载视频流,可尝试启用此功能。启用后,Frigate 将通过 go2rtc 调用 ffmpeg 模块。这可能会提升与部分摄像头视频流的兼容性。", + "none": "无", + "error": "错误", + "streamValidated": "视频流 {{number}} 验证成功", + "streamValidationFailed": "视频流 {{number}} 验证失败", + "saveAndApply": "保存新摄像头", + "saveError": "配置无效,请检查您的设置。", + "issues": { + "title": "视频流验证", + "videoCodecGood": "视频编解码器为 {{codec}}。", + "audioCodecGood": "音频编解码器为 {{codec}}。", + "resolutionHigh": "使用 {{resolution}} 分辨率可能导致资源使用率增加。", + "resolutionLow": "{{resolution}} 分辨率可能过低,难以可靠检测小型目标或物体。", + "noAudioWarning": "检测到该视频流无音频信号,录制视频将没有声音。", + "audioCodecRecordError": "录制功能需要 AAC 音频编解码器以实现音频支持。", + "audioCodecRequired": "要实现音频检测功能,必须要有音频流。", + "restreamingWarning": "为录制流开启“减少与摄像头的连接数”可能会略微增加 CPU 使用率。", + "brands": { + "reolink-rtsp": "不建议使用 Reolink 的 RTSP 协议。请在摄像头后台设置中启用 HTTP协议,并重新启动向导。", + "reolink-http": "Reolink HTTP 视频流应该使用 FFmpeg 以获得更好的兼容性,为此视频流启用“使用流兼容模式”。" + }, + "dahua": { + "substreamWarning": "子码流1当前被锁定为低分辨率。多数大华、安讯士、EmpireTech品牌的摄像头都支持额外的子码流,这些子码流需要在摄像头设置中手动启用。如果你的设备支持,建议你检查并使用这些高分辨率子码流。" + }, + "hikvision": { + "substreamWarning": "子码流1当前被锁定为低分辨率。多数海康威视的摄像头都支持额外的子码流,这些子码流需要在摄像头设置中手动启用。如果你的设备支持,建议你检查并使用这些高分辨率子码流。" + }, + "resolutionUnknown": "无法检测此视频流的分辨率。你需要在设置或配置文件中手动指定检测分辨率。" + } + } + }, + "cameraManagement": { + "title": "管理摄像头", + "addCamera": "添加新摄像头", + "editCamera": "编辑摄像头:", + "selectCamera": "选择摄像头", + "backToSettings": "返回摄像头设置", + "streams": { + "title": "摄像头状态和详细信息", + "desc": "将临时禁用摄像头,直到 Frigate 重启。禁用摄像头将完全停止 Frigate 对该摄像头视频流的处理,届时检测、录制及调试功能均不可用。
    注意:go2rtc 的转流服务不受影响。", + "enableLabel": "开启摄像头", + "enableDesc": "暂时禁用已开启的摄像头,直到 Frigate 重启。禁用摄像头会完全停止 Frigate 对该摄像头视频流的处理。检测、录像和调试功能将不可用。
    注意:这不会禁用 go2rtc 的转推流。

    拖动滑块以重新排序摄像头,使其在用户界面中按顺序显示。启用的摄像头的顺序将在整个用户界面中反映,包括实时监控仪表板和摄像头选择下拉菜单。", + "disableLabel": "关闭摄像头", + "disableDesc": "开启在当前在界面中不可见且在配置中被禁用的摄像头。启用后需要重启 Frigate 才能生效。", + "enableSuccess": "已启用 {{cameraName}}。请重启 Frigate 以应用。", + "friendlyName": { + "edit": "修改摄像头显示名称", + "title": "修改显示名称", + "description": "设置该摄像机在 Frigate 用户界面中显示的名称。若留空,则使用摄像机 ID。", + "rename": "重命名" + }, + "reorderHandle": "拖动以重新排序", + "saving": "保存中…", + "saved": "已保存", + "details": { + "edit": "编辑摄像头细节", + "title": "编辑摄像头细节", + "description": "更新此摄像头在 Frigate 页面中使用的显示名称、外部设置 URL 地址和是否可见。", + "friendlyNameLabel": "显示名称", + "friendlyNameHelp": "在 Frigate 页面中显示此摄像头的友好名称。留空以使用摄像头 ID。", + "webuiUrlLabel": "摄像头管理后台 URL", + "webuiUrlHelp": "从调试页面中直接访问摄像头管理网页界面。留空以禁用链接。", + "webuiUrlInvalid": "必须是有效的 URL(例如:https://example.com)。", + "dashboardLabel": "在实时监控面板上显示", + "dashboardHelp": "在实时监控面板显示该摄像头。", + "reviewLabel": "在核查中显示", + "reviewHelp": "在核查页面展示该摄像头,包括在筛选列表、画面变动核查以及历史视图。" + }, + "label": "摄像头状态", + "description": "设置各摄像头运行状态

    开启:正常处理视频流
    关闭:临时暂停处理,重启后状态不保留
    停用:停止处理并保存配置,重新启用需重启程序

    备注:停用操作不会影响 go2rtc 转流功能

    拖动控件调整摄像头界面显示顺序,排序效果同步应用于实时面板及摄像头选择下拉栏。", + "disabledSubheading": "配置中禁用", + "status": { + "on": "开", + "off": "关", + "disabled": "关闭" + }, + "disableSuccess": "已停用 {{cameraName}} 并保存至配置。" + }, + "cameraConfig": { + "add": "添加摄像头", + "edit": "编辑摄像头", + "description": "配置摄像头设置,包括视频流输入和功能选择。", + "name": "摄像头名称", + "nameRequired": "摄像头名称为必填项", + "nameLength": "摄像头名称必须少于64个字符。", + "namePlaceholder": "例如:大门、后院等", + "enabled": "开启", + "ffmpeg": { + "inputs": "视频流输入", + "path": "视频流地址", + "pathRequired": "视频流地址为必填项", + "pathPlaceholder": "rtsp://...", + "roles": "功能", + "rolesRequired": "至少选择一个功能", + "rolesUnique": "每个功能(音频audio、检测detect、录制record)只能分配给一个视频流", + "addInput": "添加输入视频流", + "removeInput": "移除输入视频流", + "inputsRequired": "至少需要一个输入视频流" + }, + "go2rtcStreams": "go2rtc 视频流", + "streamUrls": "视频流地址", + "addUrl": "添加地址", + "addGo2rtcStream": "添加 go2rtc 视频流", + "toast": { + "success": "摄像头 {{cameraName}} 已保存" + } + }, + "deleteCamera": "删除摄像头", + "deleteCameraDialog": { + "title": "删除摄像头", + "description": "删除摄像头将永久移除该摄像头的所有录像、跟踪目标以及配置。任何与该摄像头关联的 go2rtc 流可能仍需手动删除。", + "selectPlaceholder": "选择摄像头…", + "confirmTitle": "你确定吗?", + "confirmWarning": "删除 {{cameraName}} 后将无法撤销。", + "deleteExports": "同时删除该摄像头导出的视频", + "confirmButton": "永久删除", + "success": "摄像头 {{cameraName}} 删除完成", + "error": "删除摄像头 {{cameraName}} 失败" + }, + "profiles": { + "title": "配置模板的摄像头覆盖项", + "selectLabel": "选择配置模板", + "description": "配置启用配置文件时开启或关闭的摄像头。设置为“继承”的摄像头会沿用它原本的默认状态。", + "inherit": "继承", + "enabled": "开启", + "disabled": "关闭", + "on": "开", + "off": "关" + }, + "cameraType": { + "title": "摄像头类型", + "label": "摄像头类型", + "description": "为每路摄像头设置类型。专用车牌识别(LPR)摄像头为单用途设备,配备高倍光学变焦,可抓拍远处车辆的车牌。绝大多数摄像头应选用“通用”类型;只有专为车牌识别部署、且画面聚焦对准车牌的摄像头,才需选择“专用车牌识别”。", + "normal": "通用", + "dedicatedLpr": "车牌识别专用", + "saveSuccess": "已更新 {{cameraName}} 的摄像头类型,请重启 Frigate 以使更改生效。" + }, + "description": "添加、编辑和删除摄像头,控制每个摄像头的状态,并配置每个配置文件和摄像头类型的覆盖设置。要配置视频流、检测、画面变动和其他特定于摄像头的设置,请在“摄像头配置”下选择相关功能。", + "clone": { + "sectionTitle": "复制设置", + "sectionDescription": "将摄像头的配置复制到另一台摄像头或新摄像头上。", + "button": "复制设置", + "title": "复制摄像头设置", + "description": "将摄像头的配置复制到其他一个或多个摄像头,或者一个新摄像头上。但摄像头标识(包括名称、别名、Web UI 网址、显示顺序)不会被复制。", + "source": { + "label": "源摄像头", + "placeholder": "选择源摄像头", + "required": "选择源摄像头" + }, + "target": { + "legend": "目标", + "newRadio": "新摄像头", + "newNameLabel": "摄像头名称", + "newNamePlaceholder": "例如:后门", + "newNameRequired": "摄像头名称为必填项", + "newNameInvalid": "摄像头名称无效", + "newNameCollision": "已存在同名摄像头", + "newStreamsForced": "在创建新摄像头时,始终会复制视频流配置。", + "existingCamerasRadio": "现有摄像头", + "allCameras": "所有摄像头", + "existingPlaceholder": "至少选择一个摄像头", + "existingDisabled": "没有其他摄像头可供复制" + }, + "categories": { + "legend": "要复制的设置", + "description": "选择要从源摄像头复制哪些设置。", + "selectAll": "选择所有", + "selectNone": "取消全选", + "resetDefaults": "恢复默认设置", + "general": "常规", + "spatial": "空间设置", + "streams": "视频流", + "spatialWarningTitle": "分辨率不一致", + "spatialWarning": "源摄像头 {{srcCamera}} 的检测分辨率 ({{srcWidth}}×{{srcHeight}}) 与以下摄像头存在差异:{{cameras}}。在这些摄像头上,多边形(检测区域)可能无法准确对齐。这些默认选项当前处于关闭状态;如果启用,将按原样进行复制。", + "restartHint": "需要重启", + "items": { + "record": "录制", + "snapshots": "快照", + "review": "核查", + "motion": "画面变动检测", + "objects": "目标", + "audio": "音频检测", + "audio_transcription": "音频转录", + "notifications": "通知", + "birdseye": "鸟瞰图", + "mqtt": "MQTT", + "timestamp_style": "时间戳样式", + "onvif": "ONVIF", + "lpr": "车牌识别", + "face_recognition": "人脸识别", + "semantic_search": "语义搜索", + "genai": "生成式 AI", + "type": "摄像头类型(通用或车牌识别专用)", + "profiles": "配置模板", + "detect": "检测维度", + "zones": "区域", + "motion_mask": "画面变动遮罩", + "object_masks": "目标遮罩", + "ffmpeg_live": "视频流地址和功能" + } + }, + "footer": { + "changeCount_other": "将应用 {{count}} 项更改", + "restartNeeded": "部分更改需要重启才能生效。", + "liveOnly": "所有更改将立即生效,无需重启。", + "submit": "复制", + "submitting": "复制中…" + }, + "toast": { + "success": "设置已复制到 {{cameraName}}", + "successWithRestart": "设置已复制到 {{cameraName}}。请重启 Frigate 以应用所有更改。", + "successMulti_other": "设置已复制到 {{count}} 个摄像头", + "successMultiWithRestart_other": "设置已复制到 {{count}} 个摄像头。请重启 Frigate 以应用所有更改。", + "partialFailure": "{{successCount}} 个部分已应用;'{{failedSection}}' 失败:{{errorMessage}}", + "partialFailureMulti": "已复制到 {{successCount}} 个摄像头;{{failed}} 个失败:{{errorMessage}}", + "newCameraPartialFailure": "摄像头 {{cameraName}} 已创建,但部分设置复制失败:{{errorMessage}}", + "sourceMissing": "源摄像头已不存在", + "submitError": "复制摄像头失败:{{errorMessage}}" + } + } + }, + "cameraReview": { + "title": "摄像头核查设置", + "object_descriptions": { + "title": "生成式AI目标描述", + "desc": "临时启用或禁用此摄像头的 生成式AI目标描述 功能,直到 Frigate 重启。禁用后,系统将不再请求该摄像头追踪目标和物体的AI生成描述。" + }, + "review_descriptions": { + "title": "生成式 AI 核查总结", + "desc": "临时开关该摄像头的 生成式 AI 核查总结 功能,直到 Frigate 重启。禁用后,系统将不再请求 AI 生成该摄像头核查项目的总结。" + }, + "review": { + "title": "核查", + "desc": "临时开关该摄像头的警报与检测项生成功能,直到 Frigate 重启后恢复。禁用期间,系统将不再生成新的核查项目。 ", + "alerts": "警报 ", + "detections": "检测 " + }, + "reviewClassification": { + "title": "核查分类", + "desc": "Frigate 将核查项的严重程度分为“警报”和“检测”两个等级。默认情况下,所有的汽车 目标都将视为警报。你可以通过修改配置文件配置区域来细分。", + "noDefinedZones": "此摄像头未设置任何监控区。", + "objectAlertsTips": "所有 {{alertsLabels}} 类目标或物体在 {{cameraName}} 下都将视为警报。", + "zoneObjectAlertsTips": "所有 {{alertsLabels}} 类目标或物体在 {{cameraName}} 下的 {{zone}} 区域内都将视为警报。", + "objectDetectionsTips": "所有在摄像头 {{cameraName}} 上,检测到的 {{detectionsLabels}} 目标或物体,无论它位于哪个区,都将显示为检测。", + "zoneObjectDetectionsTips": { + "text": "所有在摄像头 {{cameraName}} 下的 {{zone}} 区域内检测到未分类的 {{detectionsLabels}} 目标或物体,都将显示为检测。", + "notSelectDetections": "所有在摄像头 {{cameraName}}下的 {{zone}} 区域内检测到的 {{detectionsLabels}} 目标或物体,如果它未归类为警报,无论它位于哪个区,都将显示为检测。", + "regardlessOfZoneObjectDetectionsTips": "在摄像头 {{cameraName}} 上,所有未分类的 {{detectionsLabels}} 检测目标或物体,无论出现在哪个区域,都将显示为检测。" + }, + "unsavedChanges": "摄像头 {{camera}} 的核查分类设置尚未保存", + "selectAlertsZones": "选择警报区", + "selectDetectionsZones": "选择检测区", + "limitDetections": "限制仅在特定区域内进行检测", + "toast": { + "success": "核查分类设置已保存,重启后生效。" + } + } + }, + "saveAllPreview": { + "title": "未保存的更改", + "triggerLabel": "查看待处理的更改", + "empty": "没有待处理的更改。", + "scope": { + "label": "作用范围", + "global": "全局", + "camera": "摄像头:{{cameraName}}" + }, + "field": { + "label": "字段" + }, + "value": { + "label": "新值", + "reset": "重置" + }, + "profile": { + "label": "配置" + } + }, + "detectionModel": { + "plusActive": { + "title": "Frigate+ 模型管理", + "label": "当前模型来源", + "description": "此实例正在运行 Frigate+ 模型。请在 Frigate+ 设置中选择或更改您的模型。", + "goToFrigatePlus": "前往 Frigate+ 设置", + "showModelForm": "手动配置模型" + } + }, + "configForm": { + "sections": { + "semantic_search": "语义搜索", + "model": "模型", + "detect": "检测", + "record": "录制", + "snapshots": "快照", + "motion": "画面变动", + "objects": "目标", + "review": "核查", + "audio": "音频", + "notifications": "通知", + "live": "实时查看", + "timestamp_style": "时间戳", + "mqtt": "MQTT", + "database": "数据库", + "telemetry": "遥测", + "auth": "身份验证", + "tls": "TLS", + "proxy": "代理", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg 编解码", + "detectors": "检测器", + "genai": "生成式 AI", + "face_recognition": "人脸识别", + "lpr": "车牌识别", + "birdseye": "鸟瞰图", + "masksAndZones": "遮罩 / 区域" + }, + "global": { + "title": "全局设置", + "description": "这些设置适用于所有摄像头,除非在摄像头特定设置中被覆盖。" + }, + "camera": { + "title": "摄像头设置", + "description": "这些设置仅适用于此摄像头,并会覆盖全局设置。", + "noCameras": "没有可用的摄像头" + }, + "advancedSettingsCount": "高级设置 ({{count}})", + "advancedCount": "高级选项 ({{count}})", + "additionalProperties": { + "keyLabel": "键", + "valueLabel": "值", + "keyPlaceholder": "新键名", + "remove": "移除" + }, + "roleMap": { + "empty": "未配置权限组映射", + "addMapping": "添加角色映射", + "roleLabel": "角色", + "groupsLabel": "用户组", + "remove": "移除" + }, + "ffmpegArgs": { + "preset": "预设", + "manual": "手动参数", + "inherit": "继承摄像头设置", + "selectPreset": "选择预设", + "manualPlaceholder": "输入 FFmpeg 参数", + "none": "无", + "useGlobalSetting": "继承全局设置", + "presetLabels": { + "preset-rpi-64-h264": "树莓派(H.264)", + "preset-rpi-64-h265": "树莓派(H.265)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "瑞芯微 RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG(通用)", + "preset-http-mjpeg-generic": "HTTP MJPEG(通用)", + "preset-http-reolink": "HTTP - Reolink 摄像头", + "preset-rtmp-generic": "RTMP(通用)", + "preset-rtsp-generic": "RTSP(通用)", + "preset-rtsp-restream": "RTSP - 从 go2rtc 转流", + "preset-rtsp-restream-low-latency": "RTSP - 从 go2rtc 转流(低延迟)", + "preset-rtsp-udp": "RTSP - UDP协议", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "录制(通用,无音频)", + "preset-record-generic-audio-copy": "录制(通用,不转码音频)", + "preset-record-generic-audio-aac": "录制(通用并将音频转码为 AAC)", + "preset-record-mjpeg": "录制 - MJPEG 流摄像头", + "preset-record-jpeg": "录制 - JPEG 流摄像头", + "preset-record-ubiquiti": "录制 - 优必飞摄像头" + } + }, + "cameraInputs": { + "itemTitle": "视频流 {{index}}", + "sourceMode": { + "restream": "转流传输(go2rtc)", + "manual": "手动输入路径", + "go2rtcStreamPlaceholder": "选择 go2rtc 视频流", + "noGo2rtcStreams": "没有配置 go2rtc 视频流", + "go2rtcStreamSearch": "搜索视频流…", + "go2rtcStreamLabel": "go2rtc 视频流", + "availableStreams": "可用的视频流", + "noMatchingStreams": "没有匹配的视频流" + } + }, + "restartRequiredField": "需要重启", + "restartRequiredFooter": "配置已更改 - 需要重启", + "detect": { + "title": "检测设置" + }, + "detectors": { + "title": "检测器设置", + "singleType": "只允许一个 {{type}} 检测器。", + "keyRequired": "检测器名称为必填项。", + "keyDuplicate": "检测器名称已存在。", + "noSchema": "没有可用的检测器架构。", + "none": "未配置检测器实例。", + "add": "添加检测器", + "addCustomKey": "添加自定义键(Key)" + }, + "record": { + "title": "录制设置" + }, + "snapshots": { + "title": "快照设置" + }, + "motion": { + "title": "画面变动设置" + }, + "objects": { + "title": "目标设置" + }, + "audioLabels": { + "summary": "已选择 {{count}} 个音频标签", + "empty": "无可用音频标签" + }, + "objectLabels": { + "summary": "已选择 {{count}} 个目标类型", + "empty": "无可用目标标签" + }, + "filters": { + "objectFieldLabel": "{{label}} 的 {{field}}" + }, + "inputRoles": { + "summary": "已选择 {{count}} 个功能", + "empty": "无可用功能", + "options": { + "detect": "检测", + "record": "录制", + "audio": "音频" + } + }, + "review": { + "title": "核查设置" + }, + "audio": { + "title": "音频设置" + }, + "notifications": { + "title": "通知设置" + }, + "live": { + "title": "实时查看设置" + }, + "showAdvanced": "显示高级设置", + "tabs": { + "sharedDefaults": "共享默认值", + "system": "系统", + "integrations": "集成" + }, + "timezone": { + "defaultOption": "使用浏览器时区" + }, + "zoneNames": { + "summary": "已选择 {{count}} 个", + "empty": "没有可用的区域" + }, + "timestamp_style": { + "title": "时间戳设置" + }, + "searchPlaceholder": "搜索…", + "genaiRoles": { + "options": { + "embeddings": "嵌入(Embedding)", + "vision": "视觉(Vision)", + "tools": "工具(Tools)", + "descriptions": "描述生成", + "chat": "聊天对话" + } + }, + "semanticSearchModel": { + "placeholder": "选择模型…", + "builtIn": "内置模型", + "genaiProviders": "生成式 AI 服务" + }, + "reviewLabels": { + "summary": "已选择 {{count}} 个标签", + "empty": "暂无可用标签" + }, + "addCustomLabel": "添加自定义标签…", + "genaiModel": { + "placeholder": "选择或输入模型…", + "search": "搜索或输入模型…", + "noModels": "暂无模型", + "available": "可用模型", + "useCustom": "使用 “{{value}}”", + "refresh": "刷新模型列表", + "probeFailed": "无法获取模型列表", + "fetchedModels": "成功获取模型列表" + }, + "knownPlates": { + "namePlaceholder": "例如:老婆的车", + "platePlaceholder": "车牌号或正则表达式" + }, + "semanticSearchModelSize": { + "notApplicable": "不适用于生成式 AI 提供者" + }, + "liveStreams": { + "streamNameLabel": "视频流名称", + "streamNamePlaceholder": "例如:高清流", + "go2rtcStreamLabel": "go2rtc 视频流", + "go2rtcStreamPlaceholder": "选择 go2rtc 视频流", + "go2rtcStreamSearch": "搜索或输入视频流名称…", + "noGo2rtcStreams": "没有 go2rtc 视频流配置", + "availableStreams": "可用的视频流", + "useCustom": "使用“{{value}}”", + "addStream": "添加视频流" + }, + "ptzPresets": { + "placeholder": "选择或输入预设…", + "search": "搜索或输入预设…", + "noPresets": "没有可用的预设", + "available": "摄像头预设", + "useCustom": "使用 “{{value}}”" + }, + "defaultRole": { + "admin": "管理员", + "viewer": "成员" + } + }, + "cameraConfig": { + "title": "摄像头配置", + "description": "配置单个摄像头的设置。这些设置会覆盖全局默认值。", + "overriddenBadge": "已覆盖", + "resetToGlobal": "重置为全局设置", + "toast": { + "success": "摄像头设置保存成功", + "error": "保存摄像头设置失败" + } + }, + "maintenance": { + "title": "维护", + "sync": { + "title": "媒体同步", + "desc": "Frigate 会根据您的保留配置定期清理媒体文件。出现少量孤立文件是正常现象。使用此功能可以删除磁盘上不再被数据库引用的孤立媒体文件。", + "started": "媒体同步已启动。", + "alreadyRunning": "同步任务已在运行中", + "error": "启动同步失败", + "currentStatus": "状态", + "jobId": "任务 ID", + "startTime": "开始时间", + "endTime": "结束时间", + "statusLabel": "状态", + "results": "结果", + "errorLabel": "错误", + "mediaTypes": "媒体类型", + "allMedia": "所有媒体", + "dryRun": "试运行", + "dryRunEnabled": "不会删除任何文件", + "dryRunDisabled": "将删除文件", + "force": "强制执行", + "forceDesc": "绕过安全阈值,即使删除超过 50% 的文件也完成同步。", + "running": "同步运行中…", + "start": "开始同步", + "inProgress": "同步正在进行中。此页面已禁用。", + "status": { + "queued": "已排队", + "running": "运行中", + "completed": "已完成", + "failed": "失败", + "notRunning": "未运行" + }, + "resultsFields": { + "filesChecked": "已检查文件", + "orphansFound": "发现孤立文件", + "orphansDeleted": "已删除孤立文件", + "aborted": "已中止。删除操作将超过安全阈值。", + "error": "错误", + "totals": "总计" + }, + "event_snapshots": "追踪目标快照", + "event_thumbnails": "追踪目标缩略图", + "review_thumbnails": "核查缩略图", + "previews": "预览", + "exports": "导出", + "recordings": "录像", + "verbose": "详细模式", + "verboseDesc": "将所有孤立文件的完整清单写入硬盘以供核查。" + }, + "regionGrid": { + "title": "区域网格", + "desc": "区域网格是一种优化功能,它会学习不同大小的目标通常出现在每个摄像头视野中的位置。Frigate 利用这些数据来高效地确定检测区域的大小。该网格会根据追踪目标数据自动构建。", + "clear": "清除区域网格", + "clearConfirmTitle": "清除区域网格", + "clearConfirmDesc": "除非你最近更改了检测器模型大小或摄像头的物理位置,并且遇到了目标追踪问题,否则不建议清除区域网格。网格会随着目标的追踪自动重建。更改需要重启 Frigate 才能生效。", + "clearSuccess": "区域网格清除成功", + "clearError": "清除区域网格失败", + "restartRequired": "需要重启以使区域网格更改生效" + } + }, + "globalConfig": { + "title": "全局配置", + "description": "配置适用于所有摄像头的全局设置,除非被单独覆盖。", + "toast": { + "success": "全局设置保存成功", + "error": "保存全局设置失败", + "validationError": "验证失败" + } + }, + "toast": { + "success": "设置保存成功", + "applied": "设置应用成功", + "successRestartRequired": "设置保存成功。请重启 Frigate 以应用更改。", + "error": "保存设置失败", + "validationError": "验证失败:{{message}}", + "resetSuccess": "已重置为全局默认值", + "resetError": "重置设置失败", + "saveAllSuccess_other": "所有 {{count}} 个部分保存成功。", + "saveAllPartial_other": "已保存 {{successCount}} / {{totalCount}} 个部分。{{failCount}} 个失败。", + "saveAllFailure": "保存所有部分失败。", + "saveAllSuccessRestartRequired_other": "成功保存 {{count}} 个部分。重启 Frigate 以生效。" + }, + "unsavedChanges": "您有未保存的更改", + "confirmReset": "确认重置", + "resetToDefaultDescription": "这将把此部分的所有设置重置为默认值。此操作无法撤销。", + "resetToGlobalDescription": "这将把此部分的设置重置为全局默认值。此操作无法撤销。", + "button": { + "overriddenGlobal": "已覆盖全局通用配置", + "overriddenGlobalTooltip": "当前摄像头配置,将优先覆盖全局通用设置", + "overriddenBaseConfigTooltip": "当前 {{profile}} 配置模板会覆盖本节所有设置", + "overriddenBaseConfig": "已覆盖默认配置", + "overriddenInCameras": { + "label_other": "已在 {{count}} 个摄像头中单独配置", + "tooltip_other": "{{count}} 个摄像头在此项中存在单独配置,点击查看详情。", + "heading_other": "此全局设置项下有 {{count}} 个摄像头存在自定义单独配置。", + "othersField_other": "其余 {{count}} 个", + "profilePrefix": "{{profile}} 配置方案:{{fields}}" + }, + "overriddenGlobalHeading_other": "该摄像头已覆盖全局配置中的 {{count}} 项设置:", + "overriddenGlobalNoDeltas": "该摄像头已覆盖全局配置,但所有配置项数值均无差异。", + "overriddenBaseConfigHeading_other": "{{profile}} 配置模板已覆盖基础配置中的 {{count}} 项设置:", + "overriddenBaseConfigNoDeltas": "{{profile}} 配置模板已覆盖该板块,但各项参数与基础配置完全一致无差异。" + }, + "profiles": { + "title": "配置模板", + "activeProfile": "激活配置模板", + "noActiveProfile": "无激活的配置模板", + "active": "激活", + "activated": "配置模板 {{profile}} 已激活", + "activateFailed": "配置模板设置失败", + "deactivated": "配置模板已停用", + "noProfiles": "未定义任何配置模板。", + "noOverrides": "无覆盖项", + "cameraCount_other": "{{count}} 个摄像头", + "baseConfig": "基础配置", + "addProfile": "添加配置模板", + "newProfile": "新配置模板", + "profileNamePlaceholder": "例如:布防、外出、夜间模式", + "friendlyNameLabel": "配置模板名称", + "profileIdLabel": "配置模板 ID", + "profileIdDescription": "用于配置和自动化的内部标识符", + "nameInvalid": "仅允许使用小写字母、数字和下划线", + "nameDuplicate": "已存在同名配置模板", + "columnCamera": "摄像头", + "columnOverrides": "配置文件覆盖", + "error": { + "mustBeAtLeastTwoCharacters": "至少需要 2 个字符", + "mustNotContainPeriod": "不得包含英文句号(\".\")", + "alreadyExists": "已存在使用此 ID 的配置文件" + }, + "renameProfile": "重命名配置文件", + "renameSuccess": "已将配置文件重命名为 “{{profile}}”", + "deleteProfile": "删除配置文件", + "deleteProfileConfirm": "确定要为所有摄像头删除配置文件“{{profile}}”吗?该步骤无法撤销。", + "deleteSuccess": "配置文件“{{profile}}”已删除", + "createSuccess": "配置文件“{{profile}}”已创建", + "removeOverride": "移除配置文件覆盖", + "deleteSection": "删除节点覆盖", + "deleteSectionConfirm": "是否要移除摄像机 {{camera}} 上针对配置文件 {{profile}} 的 {{section}} 覆盖设置?", + "deleteSectionSuccess": "已移除 {{profile}} 的 {{section}} 覆盖设置", + "enableSwitch": "开启配置模板", + "enabledDescription": "配置文件功能已启用。请在下方创建新的配置文件,进入摄像头配置页面进行修改并保存,修改即可生效。", + "disabledDescription": "配置文件功能可以让你创建一组带名称的摄像头自定义参数(比如布防、离家、夜间模式),并随时切换启用。" + }, + "timestampPosition": { + "tl": "左上角", + "tr": "右上角", + "bl": "左下角", + "br": "右下角" + }, + "go2rtcStreams": { + "title": "go2rtc 视频流", + "description": "管理用于摄像头转流的 go2rtc 流配置。每个视频流包含一个名称以及一个或多个源地址 URL。", + "addStream": "添加视频流", + "addStreamDesc": "为新的视频流输入一个名称,该名称将用于在摄像头配置中引用该视频流。", + "addUrl": "添加 URL 地址", + "streamName": "视频流名称", + "streamNamePlaceholder": "例如:front_door,此处只能使用英文", + "streamUrlPlaceholder": "例如:rtsp://user:pass@192.168.1.100/stream", + "deleteStream": "删除视频流", + "deleteStreamConfirm": "确定要删除视频流 “{{streamName}}” 吗?引用该视频流的摄像头可能会停止工作。", + "noStreams": "未配置任何 go2rtc 流。请添加一个视频流以开始使用。", + "validation": { + "nameRequired": "视频流名称为必填", + "nameDuplicate": "已存在同名的视频流", + "nameInvalid": "视频流名称只能使用字母、数字、下划线和连字符", + "urlRequired": "至少需要填写一个 URL 地址" + }, + "renameStream": "重命名视频流", + "renameStreamDesc": "为此视频流输入新名称。重命名视频流可能会导致通过名称引用它的摄像头或其他流无法正常工作。", + "newStreamName": "新视频流名称", + "ffmpeg": { + "useFfmpegModule": "使用兼容模式(ffmpeg)", + "video": "视频", + "audio": "音频", + "hardware": "硬件加速", + "videoCopy": "直接复制", + "videoH264": "转码为 H.264", + "videoH265": "转码为 H.265", + "videoExclude": "排除", + "audioCopy": "直接复制", + "audioAac": "转码为 AAC", + "audioOpus": "转码为 Opus", + "audioPcmu": "转码为 PCM μ-law", + "audioPcma": "转码为 PCM A-law", + "audioPcm": "转码为 PCM", + "audioMp3": "转码为 MP3", + "audioExclude": "排除", + "hardwareNone": "无硬件加速", + "hardwareAuto": "自动模式(推荐)", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "添加视频编码器", + "addAudioCodec": "添加音频编码器", + "removeCodec": "移除编码器" + }, + "streamNumber": "视频流 {{index}}", + "sourceNumber": "源 {{index}}" + }, + "onvif": { + "profileAuto": "自动", + "profileLoading": "正在加载配置文件…", + "autotracking": { + "zooming": { + "disabled": "关闭", + "absolute": "绝对", + "relative": "相对" + } + } + }, + "configMessages": { + "review": { + "recordDisabled": "录制已禁用,不会生成核查记录项。", + "detectDisabled": "目标检测已禁用。核查记录需要依靠检测到的目标来对警报和检测事件进行分类。", + "allNonAlertDetections": "所有非警报类活动都将被记录为检测事件。", + "genaiImageSourceRecordingsRecordDisabled": "图像源虽然设置为“录制”,但录制功能已关闭。Frigate 将自动降级使用预览图片。" + }, + "lpr": { + "vehicleNotTracked": "车牌识别需要先开启对 “汽车” 或 “摩托车” 的目标追踪。请在该摄像头的检测目标中添加“汽车”或“摩托车”。", + "globalDisabled": "要让该摄像头的车牌识别功能正常使用,必须先开启车牌识别增强功能。", + "modelSizeLarge": "大型模型针对多行格式车牌做了优化。小型模型的性能优于大型模型,而且只有小型模型才能支持中文车牌。除非你所在地区使用多行车牌格式,否则建议使用小型模型。" + }, + "audio": { + "noAudioRole": "暂无任何流已开启音频(audio)功能(role)。必须在视频流上启用音频功能,音频检测才能正常工作。" + }, + "audioTranscription": { + "audioDetectionDisabled": "该摄像头未开启音频检测功能。音频转录需要先开启音频检测。" + }, + "detect": { + "fpsGreaterThanFive": "不建议设置检测帧率高于 5,数值设置过高可能引发性能问题,且不会带来任何增益。", + "disabled": "目标检测已禁用。快照、回放条目以及人脸识别、车牌识别、生成式 AI 等增强功能都将无法使用。", + "resolutionShouldBeMultipleOfFour": "为了获得最佳效果,检测的宽度和高度应该是4的倍数。其他偶数值可能会在检测流中产生视觉伪影或轻微失真。", + "aspectRatioMismatch": "你输入的宽度和高度与当前检测分辨率的不匹配,这可能会导致图像被拉伸或变形。", + "maxFramesSet": "设置最大帧数将会覆盖默认行为,并且禁用静态物体追踪。只有很少的情况下需要这个功能,请谨慎使用。", + "squareResolution": "方形的检测分辨率比较少见。检测宽度和高度应该与你摄像头的画面比例一致(例如 16:9),而不是跟目标检测模型的输入尺寸一致。画面比例不匹配可能会拉伸图像,从而降低检测准确率。", + "resolutionHigh": "这个检测分辨率高于推荐值,可能会增加资源占用,但不一定提升检测准确率。对于大多数摄像头,建议使用 1080p 或更低的检测分辨率。", + "globalResolutionMultipleCameras": "在配置了多个摄像头同时还设置了全局检测分辨率。除非所有摄像头都使用相同的分辨率和画面比例,否则应该为每个摄像头单独设置检测宽度和高度,使其匹配各自摄像头的原生画面比例。" + }, + "faceRecognition": { + "globalDisabled": "必须开启人脸识别增强功能,此摄像头的人脸识别相关功能才能正常使用。", + "personNotTracked": "人脸识别需要检测到 “人”(person) 后才能工作。请在该摄像头的检测目标设置中添加“人”。", + "modelSizeLarge": "大型模型需要 GPU 或 NPU 才能运行正常。仅使用 CPU 的设备请选用小型模型。" + }, + "record": { + "noRecordRole": "暂无任何视频流已配置录制功能,录制功能将无法正常工作。" + }, + "birdseye": { + "objectsModeDetectDisabled": "鸟瞰图已设置为 “目标” 模式,但此摄像头未开启目标检测。该摄像头将不会显示在鸟瞰画面中。" + }, + "snapshots": { + "detectDisabled": "目标检测已禁用。快照是根据追踪到的目标生成的,因此将不会创建快照。" + }, + "detectors": { + "mixedTypes": "所有检测器必须为同一类型。若要更换为其他类型,请先移除现有的检测器。", + "mixedTypesSuggestion": "所有检测器必须使用相同类型。请移除现有检测器,或选择 {{type}}。" + }, + "objects": { + "genaiNoDescriptionsProvider": "必须配置具备“描述”功能的生成式 AI 服务商,才能自动生成事件描述。" + }, + "semanticSearch": { + "jinav2SmallModelSize": "Jina V2 的大型模型版本内存占用与推理开销较高,建议搭配独立显卡使用大型模型。", + "modelSizeIgnoredForProvider": "模型大小仅适用于内置的 Jina 模型。当使用生成式 AI 作为嵌入提供者时,此值将被忽略。" + }, + "onvif": { + "autotrackingNoZones": "自动追踪至少需要一个区域。请先在“遮罩 / 区域”中为此摄像头定义一个区域,然后在下方将其设置为必需区域。" + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "不建议手动硬件加速参数。除非存在特定需求,否则选择与你的硬件匹配的预设。" + }, + "model": { + "optimizedFor320": "Frigate 针对了 320x320 的模型进行优化,这是大多数设置的最佳选择。640x640 模型速度较慢,仅在特定场景下有帮助。", + "inputDimensionsNotDetectResolution": "模型输入的宽度和高度是目标检测模型的输入尺寸,而不是你摄像头的检测分辨率。它们应该与你正在使用的模型的尺寸相匹配,通常是一个方形尺寸,如320x320或640x640。" + } + }, + "birdseye": { + "trackingMode": { + "objects": "基于目标", + "motion": "基于画面变动", + "continuous": "连续" + }, + "cameraOrder": { + "label": "摄像头排序", + "description": "拖动摄像头以在鸟瞰布局中设置它们的顺序。", + "reorderHandle": "拖动以重新排序", + "saving": "保存中…", + "saved": "已保存" + } + }, + "snapshot": { + "retainMode": { + "all": "所有", + "motion": "画面变动", + "active_objects": "活动目标" + } + }, + "ui": { + "timeFormat": { + "browser": "基于浏览器", + "12hour": "12 小时制", + "24hour": "24 小时制" + }, + "TimeOrDateStyle": { + "full": "完整", + "long": "长", + "medium": "中等", + "short": "段" + }, + "unitSystem": { + "metric": "公制单位", + "imperial": "英制单位" + } + }, + "review": { + "imageSource": { + "recordings": "录制文件", + "previews": "预览" + } + }, + "logger": { + "logLevel": { + "debug": "调试", + "info": "信息", + "warning": "警告", + "error": "错误", + "critical": "关键" + } + }, + "modelSize": { + "small": "小型", + "large": "大型" + }, + "retainMode": { + "all": "全部", + "motion": "画面变动", + "active_objects": "活动目标" + }, + "previewQuality": { + "very_high": "非常高", + "high": "高", + "medium": "中等", + "low": "低", + "very_low": "非常低" + }, + "detectorsAndModel": { + "title": "检测器和模型", + "description": "配置用于运行目标检测的检测器后端及对应模型,配置将统一保存,确保检测器与模型保持匹配一致。", + "cardTitles": { + "detector": "检测器硬件", + "model": "检测器模型" + }, + "tabs": { + "plus": "Frigate+", + "custom": "自定义模型" + }, + "mismatch": { + "warning": "当前 Frigate+ 模型“{{model}}”需搭配 {{required}} 检测器使用。请在下方选择兼容的模型,或切换为自定义模型后再保存。" + }, + "plusModel": { + "requiresDetector": "需要检测器:{{detector}}", + "noModelSelected": "选择 Frigate+ 模型" + }, + "toast": { + "saveSuccess": "检测器与模型设置已保存,请重启 Frigate 以生效配置。", + "saveError": "保存检测器及模型设置失败" + }, + "unsavedChanges": "检测器与模型配置存在未保存修改", + "restartRequired": "需要重启(检测器 或 模型 的设置已变更)" + }, + "menuDot": { + "overrideGlobal": "这一部分覆盖了全局配置", + "overrideProfile": "本节被 {{profile}} 配置文件覆盖", + "unsaved": "这一部分有未保存的更改" + } +} diff --git a/web/public/locales/zh-CN/views/system.json b/web/public/locales/zh-CN/views/system.json new file mode 100644 index 0000000..c14f062 --- /dev/null +++ b/web/public/locales/zh-CN/views/system.json @@ -0,0 +1,275 @@ +{ + "documentTitle": { + "cameras": "摄像头统计 - Frigate", + "storage": "存储统计 - Frigate", + "general": "常规统计 - Frigate", + "enrichments": "增强功能统计 - Frigate", + "logs": { + "frigate": "Frigate 日志 - Frigate", + "go2rtc": "Go2RTC 日志 - Frigate", + "nginx": "Nginx 日志 - Frigate", + "websocket": "消息日志 - Frigate" + } + }, + "title": "系统", + "metrics": "系统指标", + "logs": { + "download": { + "label": "下载日志" + }, + "copy": { + "label": "复制到剪贴板", + "success": "已复制日志到剪贴板", + "error": "无法复制日志到剪贴板" + }, + "type": { + "label": "类型", + "timestamp": "时间戳", + "tag": "标签", + "message": "消息" + }, + "tips": "日志正在从服务器流式传输", + "toast": { + "error": { + "fetchingLogsFailed": "获取日志出错:{{errorMessage}}", + "whileStreamingLogs": "流式传输日志时出错:{{errorMessage}}" + } + }, + "websocket": { + "label": "消息", + "pause": "暂停", + "filter": { + "lpr": "车牌识别", + "all": "全部主题", + "topics": "主题", + "events": "事件", + "reviews": "核查", + "classification": "分类", + "face_recognition": "人脸识别", + "camera_activity": "摄像头活动", + "system": "系统", + "camera": "摄像头", + "all_cameras": "所有摄像头", + "cameras_count_one": "{{count}} 个摄像头", + "cameras_count_other": "{{count}} 个摄像头" + }, + "resume": "继续", + "clear": "清除", + "empty": "未捕获到消息", + "count": "{{count}} 条消息", + "expanded": { + "payload": "Payload" + }, + "count_one": "{{count}} 条消息", + "count_other": "{{count}} 条消息" + } + }, + "general": { + "title": "常规", + "detector": { + "title": "检测器", + "inferenceSpeed": "检测器推理速度", + "cpuUsage": "检测器 CPU 使用率", + "memoryUsage": "检测器内存使用率", + "temperature": "检测器温度", + "cpuUsageInformation": "此处的 CPU 使用率,只统计在给检测模型准备输入数据和处理输出数据时用到的 CPU。它不统计模型推理本身的资源占用,即使推理是在 GPU 或其他检测器上进行的。" + }, + "hardwareInfo": { + "title": "硬件信息", + "gpuUsage": "GPU 使用率", + "gpuMemory": "GPU 显存", + "gpuEncoder": "GPU 编码", + "gpuDecoder": "GPU 解码", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo 输出", + "returnCode": "返回代码:{{code}}", + "processOutput": "进程输出:", + "processError": "进程错误:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI 输出", + "name": "名称:{{name}}", + "driver": "驱动:{{driver}}", + "cudaComputerCapability": "CUDA计算能力:{{cuda_compute}}", + "vbios": "VBios信息:{{vbios}}" + }, + "closeInfo": { + "label": "关闭 GPU 信息" + }, + "copyInfo": { + "label": "复制 GPU 信息" + }, + "toast": { + "success": "已复制 GPU 信息到剪贴板" + } + }, + "npuMemory": "NPU内存", + "npuUsage": "NPU 使用率", + "intelGpuWarning": { + "title": "Intel GPU 处于警告状态", + "message": "GPU 状态不可用", + "description": "这是 Intel 的 GPU 状态报告工具(intel_gpu_top)的已知问题:该工具会失效并反复返回 GPU 使用率为 0%,即使在硬件加速和目标检测已在 (i)GPU 上正常运行的情况下也是如此,这并不是 Frigate 的 bug。你可以通过重启主机来临时修复该问题,并确认 GPU 正常工作。该问题并不会影响性能。" + }, + "gpuTemperature": "GPU 温度", + "npuTemperature": "NPU 温度", + "gpuCompute": "GPU 计算 / 编码" + }, + "otherProcesses": { + "title": "其他进程", + "processCpuUsage": "主进程 CPU 使用率", + "processMemoryUsage": "主进程内存使用率", + "series": { + "go2rtc": "go2rtc", + "recording": "录制", + "review_segment": "核查片段", + "embeddings": "增强功能", + "audio_detector": "音频检测" + } + } + }, + "storage": { + "title": "存储", + "overview": "概览", + "recordings": { + "title": "录制内容", + "tips": "该值表示 Frigate 数据库中录制内容所使用的总存储空间。Frigate 不会追踪磁盘上所有文件的存储使用情况。", + "earliestRecording": "最早的可用录制:" + }, + "cameraStorage": { + "title": "摄像头存储", + "camera": "摄像头", + "unusedStorageInformation": "未使用存储信息", + "storageUsed": "存储使用", + "percentageOfTotalUsed": "总使用率", + "bandwidth": "带宽", + "unused": { + "title": "未使用", + "tips": "如果您的驱动器上存储了除 Frigate 录制内容之外的其他文件,该值可能无法准确反映 Frigate 可用的剩余空间。Frigate 不会追踪录制内容以外的存储使用情况。" + } + }, + "shm": { + "title": "共享内存(SHM)分配", + "warning": "当前共享内存(SHM)容量过小( {{total}}MB),请将其至少增加到 {{min_shm}}MB。", + "frameLifetime": { + "title": "帧保留时间", + "description": "每个摄像头在共享内存中拥有 {{frames}} 个帧槽位。在最快摄像头的帧率下,每一帧在被覆盖前大约可保留 {{lifetime}} 秒。" + } + } + }, + "cameras": { + "title": "摄像头", + "overview": "概览", + "info": { + "cameraProbeInfo": "摄像头 {{camera}} 的信息", + "streamDataFromFFPROBE": "流数据信息通过ffprobe获取。", + "fetching": "正在获取摄像头数据", + "stream": "视频流{{idx}}", + "video": "视频:", + "codec": "编解码器:", + "resolution": "分辨率:", + "fps": "帧率:", + "unknown": "未知", + "audio": "音频:", + "error": "错误:{{error}}", + "tips": { + "title": "摄像头信息" + }, + "aspectRatio": "宽高比", + "keyframes": { + "title": "关键帧分析", + "analyzing": "正在分析关键帧... 剩余 {{seconds}} 秒", + "stillAnalyzing": "仍在分析关键帧...", + "recordStream": "录制视频流:", + "keyframeCount": "观察到的关键帧:", + "observedDuration": "观测持续时间:", + "gap": "关键帧间隔(最小值 / 平均值 / 最大值):", + "segmentLength": "录制片段长度:", + "ok": "每 {{seconds}} 秒取一帧,适用于录制和回放。", + "warning": "稀疏或不均匀的关键帧(最长间隔约{{seconds}}秒),可能是使用了智能编码器(例如H.264+/H.265+),不建议开启该功能。", + "error": "关键帧间隔(~{{seconds}}秒)超过了录制片段长度({{segmentTime}}秒)。某些片段可能没有关键帧,这会导致播放中断。请尝试禁用摄像头的智能或 + 编解码器或缩短其关键帧间隔。", + "unknown": "无法确定关键帧间隔。", + "recordDisabled": "此摄像头的录制功能已禁用。" + } + }, + "framesAndDetections": "帧数/检测次数", + "label": { + "camera": "摄像头", + "detect": "检测", + "skipped": "跳过", + "ffmpeg": "FFmpeg编码器", + "capture": "捕获", + "overallFramesPerSecond": "每秒总帧数", + "overallDetectionsPerSecond": "每秒总检测数", + "overallSkippedDetectionsPerSecond": "每秒跳过检测数", + "cameraCapture": "{{camName}} 捕获", + "cameraDetect": "{{camName}} 检测", + "cameraDetectionsPerSecond": "{{camName}} 每秒检测数", + "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒跳过检测数", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraFramesPerSecond": "{{camName}} 每秒帧数", + "cameraGpu": "{{camName}} GPU" + }, + "toast": { + "success": { + "copyToClipboard": "已复制检测数据到剪贴板。" + }, + "error": { + "unableToProbeCamera": "无法检测到摄像头:{{errorMessage}}" + } + }, + "connectionQuality": { + "title": "连接质量", + "excellent": "优秀", + "fair": "一般", + "poor": "较差", + "unusable": "不可用", + "fps": "帧率", + "expectedFps": "预期帧率", + "reconnectsLastHour": "最近一小时重连次数", + "stallsLastHour": "最近一小时卡顿次数" + }, + "noCameras": { + "title": "没有找到摄像头" + } + }, + "lastRefreshed": "最后刷新时间: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} 的 FFmpeg CPU 使用率较高({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} 的 检测器 CPU 使用率较高({{detectAvg}}%)", + "healthy": "系统运行正常", + "reindexingEmbeddings": "正在重新索引嵌入(已完成 {{processed}}%)", + "detectIsSlow": "{{detect}} 运行缓慢({{speed}}毫秒)", + "detectIsVerySlow": "{{detect}} 运行非常缓慢({{speed}}毫秒)", + "cameraIsOffline": "{{camera}} 已离线", + "shmTooLow": "/dev/shm 的分配空间过低(当前 {{total}} MB),应至少增加到 {{min}} MB。", + "debugReplayActive": "调试回放会话正在进行" + }, + "enrichments": { + "title": "增强功能", + "infPerSecond": "每秒推理次数", + "embeddings": { + "image_embedding_speed": "图像特征提取速度", + "face_embedding_speed": "人脸特征提取速度", + "plate_recognition_speed": "车牌识别速度", + "text_embedding_speed": "文本编码速度", + "face_recognition_speed": "人脸识别速度", + "image_embedding": "图像特征提取", + "text_embedding": "文字编码", + "face_recognition": "人脸特征提取", + "plate_recognition": "车牌识别", + "yolov9_plate_detection_speed": "YOLOv9 车牌检测速度", + "yolov9_plate_detection": "YOLOv9 车牌检测", + "review_description": "核查总结", + "review_description_speed": "核查总结速度", + "review_description_events_per_second": "核查总结", + "object_description": "目标描述", + "object_description_speed": "目标描述速度", + "object_description_events_per_second": "目标描述", + "classification": "分类 {{name}}", + "classification_speed": "{{name}} 的分类速度", + "classification_events_per_second": "{{name}} 的每秒分类速度" + }, + "averageInf": "平均推理时间" + } +} diff --git a/web/public/locales/zh-Hant/audio.json b/web/public/locales/zh-Hant/audio.json new file mode 100644 index 0000000..f5dd289 --- /dev/null +++ b/web/public/locales/zh-Hant/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "說話聲", + "babbling": "牙牙學語", + "bicycle": "腳踏車", + "yell": "大叫", + "car": "車", + "bellow": "吼叫", + "boat": "船", + "crying": "哭聲", + "sigh": "嘆氣", + "singing": "歌聲", + "choir": "合唱", + "yodeling": "山歌", + "chant": "誦經", + "mantra": "咒語", + "camera": "鏡頭", + "motorcycle": "摩托車", + "bus": "巴士", + "train": "火車", + "bird": "鳥", + "cat": "貓", + "dog": "狗", + "horse": "馬", + "sheep": "羊", + "skateboard": "滑板", + "door": "門", + "mouse": "滑鼠", + "keyboard": "鍵盤", + "sink": "水槽", + "blender": "果汁機", + "clock": "時鐘", + "scissors": "剪刀", + "hair_dryer": "吹風機", + "toothbrush": "牙刷", + "vehicle": "車輛", + "animal": "動物", + "bark": "樹皮", + "goat": "山羊", + "whoop": "大叫", + "whispering": "講話", + "laughter": "笑聲", + "snicker": "竊笑", + "child_singing": "小孩歌聲", + "synthetic_singing": "合成音樂聲", + "rapping": "饒舌聲", + "humming": "哼歌聲", + "groan": "呻吟聲", + "grunt": "咕噥聲", + "whistling": "口哨聲", + "breathing": "呼吸聲", + "wheeze": "喘息聲", + "snoring": "打呼聲", + "gasp": "倒抽一口氣", + "pant": "喘氣聲", + "snort": "鼻息聲", + "cough": "咳嗽聲", + "throat_clearing": "清喉嚨聲", + "sneeze": "打噴嚏聲", + "sniff": "嗅聞聲", + "run": "跑步聲", + "shuffle": "拖著腳走路聲", + "footsteps": "腳步聲", + "chewing": "咀嚼聲", + "biting": "咬", + "gargling": "漱口", + "stomach_rumble": "腸胃蠕動", + "burping": "打嗝", + "hiccup": "打噎", + "fart": "放屁", + "hands": "手", + "finger_snapping": "彈手指聲", + "clapping": "拍手", + "heartbeat": "心跳聲", + "heart_murmur": "心臟雜音", + "cheering": "歡呼聲", + "applause": "掌聲", + "chatter": "嘈雜聲", + "crowd": "人群聲", + "children_playing": "兒童嬉鬧聲", + "pets": "寵物", + "yip": "吠叫", + "howl": "嚎叫", + "bow_wow": "汪汪", + "growling": "咆哮", + "whimper_dog": "狗嗚咽", + "purr": "咕嚕", + "meow": "喵喵", + "hiss": "嘶嘶聲", + "caterwaul": "貓叫春", + "livestock": "牲畜", + "clip_clop": "蹄聲", + "neigh": "嘶鳴", + "cattle": "牛", + "moo": "哞哞", + "cowbell": "牛鈴", + "pig": "豬", + "oink": "哼哼", + "bleat": "咩咩", + "fowl": "家禽", + "chicken": "雞", + "cluck": "咯咯", + "cock_a_doodle_doo": "喔喔", + "turkey": "火雞", + "gobble": "咯咯", + "duck": "鴨子", + "quack": "嘎嘎", + "goose": "鵝", + "honk": "鳴笛/鵝叫聲", + "wild_animals": "野生動物", + "roaring_cats": "吼叫的貓科動物", + "roar": "吼叫", + "chirp": "啾啾", + "squawk": "啼叫", + "pigeon": "鴿子", + "coo": "咕咕", + "crow": "烏鴉", + "caw": "呱呱", + "owl": "貓頭鷹", + "hoot": "嗚嗚", + "flapping_wings": "翅膀拍打", + "dogs": "狗群", + "rats": "老鼠", + "patter": "啪嗒聲", + "insect": "昆蟲", + "cricket": "蟋蟀", + "mosquito": "蚊子", + "fly": "蒼蠅", + "buzz": "嗡嗡", + "frog": "青蛙", + "croak": "呱呱", + "snake": "蛇", + "rattle": "響尾", + "whale_vocalization": "鯨魚叫聲", + "music": "音樂", + "musical_instrument": "樂器", + "plucked_string_instrument": "彈撥樂器", + "guitar": "吉他", + "electric_guitar": "電吉他", + "bass_guitar": "貝斯", + "acoustic_guitar": "原聲吉他", + "steel_guitar": "鋼弦吉他", + "tapping": "敲擊", + "strum": "掃弦", + "banjo": "班卓琴", + "sitar": "西塔琴", + "mandolin": "曼陀林", + "zither": "古箏", + "ukulele": "尤克里裡", + "piano": "鋼琴", + "electric_piano": "電鋼琴", + "organ": "風琴", + "electronic_organ": "電子琴", + "hammond_organ": "哈蒙德風琴", + "synthesizer": "合成器", + "sampler": "取樣器", + "harpsichord": "大鍵琴", + "percussion": "打擊樂器", + "drum_kit": "架子鼓", + "drum_machine": "鼓機", + "drum": "鼓", + "snare_drum": "軍鼓", + "rimshot": "鼓邊擊", + "drum_roll": "滾鼓", + "bass_drum": "大鼓", + "timpani": "定音鼓", + "tabla": "塔布拉鼓", + "cymbal": "鈸", + "hi_hat": "踩鑔", + "wood_block": "木魚", + "tambourine": "鈴鼓", + "maraca": "沙錘", + "gong": "鑼", + "tubular_bells": "管鍾", + "mallet_percussion": "槌擊打擊樂器", + "marimba": "馬林巴", + "glockenspiel": "鐘琴", + "vibraphone": "顫音琴", + "steelpan": "鋼鼓", + "orchestra": "管絃樂隊", + "brass_instrument": "銅管樂器", + "french_horn": "圓號", + "trumpet": "小號", + "trombone": "長號", + "bowed_string_instrument": "弓弦樂器", + "string_section": "絃樂組", + "violin": "小提琴", + "pizzicato": "撥絃", + "cello": "大提琴", + "double_bass": "低音提琴", + "wind_instrument": "管樂器", + "flute": "長笛", + "saxophone": "薩克斯", + "clarinet": "單簧管", + "harp": "豎琴", + "bell": "鈴", + "church_bell": "教堂鍾", + "jingle_bell": "鈴鐺", + "bicycle_bell": "腳踏車鈴", + "tuning_fork": "音叉", + "chime": "風鈴", + "wind_chime": "風鈴", + "harmonica": "口琴", + "accordion": "手風琴", + "bagpipes": "風笛", + "didgeridoo": "迪吉里杜管", + "theremin": "特雷門琴", + "singing_bowl": "頌缽", + "scratching": "刮擦聲", + "pop_music": "流行音樂", + "hip_hop_music": "嘻哈音樂", + "beatboxing": "人聲節拍", + "rock_music": "搖滾音樂", + "heavy_metal": "重金屬", + "punk_rock": "朋克搖滾", + "grunge": "垃圾搖滾", + "progressive_rock": "前衛搖滾", + "rock_and_roll": "搖滾樂", + "psychedelic_rock": "迷幻搖滾", + "rhythm_and_blues": "節奏布魯斯", + "soul_music": "靈魂樂", + "reggae": "雷鬼", + "country": "鄉村音樂", + "swing_music": "搖擺樂", + "bluegrass": "藍草音樂", + "funk": "放克", + "folk_music": "民謠", + "middle_eastern_music": "中東音樂", + "jazz": "爵士樂", + "disco": "迪斯科", + "classical_music": "古典音樂", + "opera": "歌劇", + "electronic_music": "電子音樂", + "house_music": "浩室音樂", + "techno": "科技舞曲", + "dubstep": "迴響貝斯", + "drum_and_bass": "鼓打貝斯", + "electronica": "電子樂", + "electronic_dance_music": "電子舞曲", + "ambient_music": "環境音樂", + "trance_music": "迷幻舞曲", + "music_of_latin_america": "拉丁美洲音樂", + "salsa_music": "薩爾薩", + "flamenco": "弗拉門戈", + "blues": "藍調", + "music_for_children": "兒童音樂", + "new-age_music": "新世紀音樂", + "vocal_music": "聲樂", + "a_capella": "無伴奏合唱", + "music_of_africa": "非洲音樂", + "afrobeat": "非洲節拍", + "christian_music": "基督教音樂", + "gospel_music": "福音音樂", + "music_of_asia": "亞洲音樂", + "carnatic_music": "卡納提克音樂", + "music_of_bollywood": "寶萊塢音樂", + "ska": "斯卡", + "traditional_music": "傳統音樂", + "independent_music": "獨立音樂", + "song": "歌曲", + "background_music": "背景音樂", + "theme_music": "主題音樂", + "jingle": "廣告歌", + "soundtrack_music": "配樂", + "lullaby": "搖籃曲", + "video_game_music": "電子遊戲音樂", + "christmas_music": "聖誕音樂", + "dance_music": "舞曲", + "wedding_music": "婚禮音樂", + "happy_music": "歡快音樂", + "sad_music": "悲傷音樂", + "tender_music": "溫柔音樂", + "exciting_music": "激動音樂", + "angry_music": "憤怒音樂", + "scary_music": "恐怖音樂", + "wind": "風", + "rustling_leaves": "樹葉沙沙聲", + "wind_noise": "風聲", + "thunderstorm": "雷暴", + "thunder": "雷聲", + "water": "水", + "rain": "雨", + "raindrop": "雨滴", + "rain_on_surface": "雨打表面", + "stream": "溪流", + "waterfall": "瀑布", + "ocean": "海洋", + "waves": "波浪", + "steam": "蒸汽", + "gurgling": "汩汩聲", + "fire": "火", + "crackle": "噼啪聲", + "sailboat": "帆船", + "rowboat": "划艇", + "motorboat": "摩托艇", + "ship": "輪船", + "motor_vehicle": "機動車", + "toot": "鳴笛", + "car_alarm": "汽車警報", + "power_windows": "電動車窗", + "skidding": "輪胎打滑", + "tire_squeal": "輪胎尖叫", + "car_passing_by": "汽車駛過", + "race_car": "賽車", + "truck": "卡車", + "air_brake": "氣閘", + "air_horn": "氣笛", + "reversing_beeps": "倒車提示音", + "ice_cream_truck": "冰淇淋車", + "emergency_vehicle": "應急車輛", + "police_car": "警車", + "ambulance": "救護車", + "fire_engine": "消防車", + "traffic_noise": "交通噪音", + "rail_transport": "鐵路運輸", + "train_whistle": "火車汽笛", + "train_horn": "火車鳴笛", + "railroad_car": "鐵路車廂", + "train_wheels_squealing": "火車輪子尖叫", + "subway": "地鐵", + "aircraft": "飛行器", + "aircraft_engine": "飛機引擎", + "jet_engine": "噴氣引擎", + "propeller": "螺旋槳", + "helicopter": "直升機", + "fixed-wing_aircraft": "固定翼飛機", + "engine": "引擎", + "light_engine": "輕型引擎", + "dental_drill's_drill": "牙科鑽", + "lawn_mower": "割草機", + "chainsaw": "電鋸", + "medium_engine": "中型引擎", + "heavy_engine": "重型引擎", + "engine_knocking": "引擎敲擊", + "engine_starting": "引擎啟動", + "idling": "怠速", + "accelerating": "加速", + "doorbell": "門鈴", + "ding-dong": "叮咚", + "sliding_door": "滑動門", + "slam": "猛關", + "knock": "敲門", + "tap": "輕敲", + "squeak": "吱吱聲", + "cupboard_open_or_close": "櫥櫃開關", + "drawer_open_or_close": "抽屜開關", + "dishes": "餐具", + "cutlery": "刀叉", + "chopping": "切菜", + "frying": "煎炸", + "microwave_oven": "微波爐", + "water_tap": "水龍頭", + "bathtub": "浴缸", + "toilet_flush": "馬桶沖水", + "electric_toothbrush": "電動牙刷", + "vacuum_cleaner": "吸塵器", + "zipper": "拉鍊", + "keys_jangling": "鑰匙叮噹", + "coin": "硬幣", + "electric_shaver": "電動剃鬚刀", + "shuffling_cards": "洗牌", + "typing": "打字", + "typewriter": "打字機", + "computer_keyboard": "電腦鍵盤", + "writing": "書寫", + "alarm": "警報", + "telephone": "電話", + "telephone_bell_ringing": "電話鈴聲", + "ringtone": "手機鈴聲", + "telephone_dialing": "電話撥號", + "dial_tone": "撥號音", + "busy_signal": "忙音", + "alarm_clock": "鬧鐘", + "siren": "警笛", + "civil_defense_siren": "防空警報", + "buzzer": "蜂鳴器", + "smoke_detector": "煙霧檢測器", + "fire_alarm": "火災警報器", + "foghorn": "霧笛", + "whistle": "哨子", + "steam_whistle": "蒸汽汽笛", + "mechanisms": "機械裝置", + "ratchet": "棘輪", + "tick": "滴答", + "tick-tock": "滴答滴答", + "gears": "齒輪", + "pulleys": "滑輪", + "sewing_machine": "縫紉機", + "mechanical_fan": "機械風扇", + "air_conditioning": "空調", + "cash_register": "收銀機", + "printer": "印表機", + "single-lens_reflex_camera": "單反相機", + "tools": "工具", + "hammer": "錘子", + "jackhammer": "風鎬", + "sawing": "鋸", + "filing": "銼", + "sanding": "砂磨", + "power_tool": "電動工具", + "drill": "電鑽", + "explosion": "爆炸", + "gunshot": "槍聲", + "machine_gun": "機關槍", + "fusillade": "齊射", + "artillery_fire": "炮火", + "cap_gun": "玩具槍", + "fireworks": "煙花", + "firecracker": "鞭炮", + "burst": "爆裂", + "eruption": "爆發", + "boom": "轟隆", + "wood": "木頭", + "chop": "砍", + "splinter": "碎裂", + "crack": "破裂", + "glass": "玻璃", + "chink": "叮噹", + "shatter": "粉碎", + "silence": "寂靜", + "sound_effect": "音效", + "environmental_noise": "環境噪音", + "static": "靜電噪音", + "white_noise": "白噪音", + "pink_noise": "粉紅噪音", + "television": "電視", + "radio": "收音機", + "field_recording": "實地錄音", + "scream": "尖叫", + "sodeling": "索德鈴", + "chird": "啾鳴", + "change_ringing": "變奏鐘聲", + "shofar": "羊角號", + "liquid": "液體", + "splash": "液體飛濺", + "slosh": "液體晃動", + "squish": "擠壓", + "drip": "水滴聲", + "pour": "倒水聲", + "trickle": "細流水聲", + "gush": "液體噴湧", + "fill": "注水聲", + "spray": "噴灑", + "pump": "泵送", + "stir": "攪拌聲", + "boiling": "沸騰聲", + "sonar": "聲吶聲", + "arrow": "箭矢聲", + "whoosh": "呼嘯聲", + "thump": "砰擊聲", + "thunk": "沉悶聲", + "electronic_tuner": "電子調音器", + "effects_unit": "效果器", + "chorus_effect": "合唱效果", + "basketball_bounce": "籃球反彈聲", + "bang": "砰聲", + "slap": "拍擊聲", + "whack": "重擊聲", + "smash": "猛擊聲", + "breaking": "破碎聲", + "bouncing": "彈跳聲", + "whip": "鞭打聲", + "flap": "撲動聲", + "scratch": "刮擦聲", + "scrape": "刮擦聲", + "rub": "摩擦聲", + "roll": "捲動聲", + "crushing": "壓碎聲", + "crumpling": "揉皺聲", + "tearing": "撕裂聲", + "beep": "嗶聲", + "ping": "嘀聲", + "ding": "叮聲", + "clang": "鐺聲", + "squeal": "尖銳聲", + "creak": "嘎吱聲", + "rustle": "沙沙聲", + "whir": "嗡聲", + "clatter": "哐啷聲", + "sizzle": "滋滋聲", + "clicking": "點選聲", + "clickety_clack": "咔嗒聲", + "rumble": "隆隆聲", + "plop": "撲通聲", + "hum": "嗡鳴聲", + "zing": "嗖聲", + "boing": "嘣聲", + "crunch": "咔嚓聲", + "sine_wave": "正弦波聲", + "harmonic": "諧波聲", + "chirp_tone": "啾聲", + "pulse": "脈衝", + "inside": "室內聲", + "outside": "室外聲", + "reverberation": "混響", + "echo": "回聲", + "noise": "噪聲", + "mains_hum": "電流嗡聲", + "distortion": "失真聲", + "sidetone": "旁音", + "cacophony": "刺耳噪聲", + "throbbing": "脈動聲", + "vibration": "振動聲" +} diff --git a/web/public/locales/zh-Hant/common.json b/web/public/locales/zh-Hant/common.json new file mode 100644 index 0000000..08ae3d6 --- /dev/null +++ b/web/public/locales/zh-Hant/common.json @@ -0,0 +1,317 @@ +{ + "time": { + "untilForTime": "直到 {{time}}", + "untilForRestart": "直到 Frigate 重新啟動。", + "untilRestart": "直到重新啟動", + "ago": "{{timeAgo}} 前", + "last14": "過去 14 天", + "last30": "過去 30 天", + "thisWeek": "這週", + "lastWeek": "上週", + "thisMonth": "這個月", + "lastMonth": "上個月", + "justNow": "剛剛", + "today": "今天", + "yesterday": "昨天", + "last7": "過去 7 天", + "5minutes": "5 分鐘", + "10minutes": "10 分鐘", + "30minutes": "30 分鐘", + "1hour": "1 小時", + "12hours": "12 小時", + "24hours": "24 小時", + "pm": "下午", + "am": "上午", + "yr": "{{time}} 年", + "year_other": "{{time}} 年", + "mo": "{{time}} 月", + "month_other": "{{time}} 月", + "d": "{{time}} 日", + "day_other": "{{time}}天", + "h": "{{time}}時", + "hour_other": "{{time}}小時", + "m": "{{time}}分", + "minute_other": "{{time}}分鐘", + "s": "{{time}}秒", + "second_other": "{{time}}秒鐘", + "formattedTimestamp": { + "12hour": "M 月 d 日 ah:mm:ss", + "24hour": "M 月 d 日 HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "a h:mm", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "a h:mm:ss", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "M 月 d 日 ah:mm", + "24hour": "M 月 d 日 HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "yy 年 MM 月 dd 日", + "24hour": "yy 年 MM 月 dd 日" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "yyyy 年 M 月 d 日 ah:mm", + "24hour": "yyyy 年 M 月 d 日 HH:mm" + }, + "formattedTimestampMonthDay": "M 月 d 日", + "formattedTimestampFilename": { + "12hour": "yy年MM月dd日 h時mm分ss秒", + "24hour": "yy年MM月dd日 HH時mm分ss秒" + }, + "inProgress": "處理中", + "invalidStartTime": "無效的起始時間", + "invalidEndTime": "無效的結束時間", + "never": "永不" + }, + "unit": { + "speed": { + "mph": "英里/小時", + "kph": "公里/小時" + }, + "length": { + "feet": "英尺", + "meters": "公尺" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/小時", + "mbph": "MB/小時", + "gbph": "GB/小時" + } + }, + "label": { + "back": "返回", + "hide": "隱藏{{item}}", + "show": "顯示{{item}}", + "ID": "ID", + "none": "無", + "all": "全部", + "other": "其他" + }, + "button": { + "apply": "套用", + "reset": "重置", + "done": "完成", + "enabled": "已啟用", + "enable": "啟用", + "disabled": "已停用", + "disable": "停用", + "save": "儲存", + "saving": "儲存中…", + "cancel": "取消", + "close": "關閉", + "copy": "複製", + "back": "返回", + "history": "歷史紀錄", + "fullscreen": "全螢幕", + "exitFullscreen": "退出全螢幕", + "pictureInPicture": "子母畫面", + "twoWayTalk": "雙向通話", + "cameraAudio": "鏡頭音訊", + "on": "開", + "off": "關", + "edit": "編輯", + "copyCoordinates": "複製座標", + "delete": "刪除", + "yes": "是", + "no": "否", + "download": "下載", + "info": "資訊", + "suspended": "已暫停", + "unsuspended": "取消暫停", + "play": "播放", + "unselect": "取消選取", + "export": "匯出", + "deleteNow": "立即刪除", + "next": "繼續", + "continue": "繼續", + "add": "新增", + "applying": "應用中…", + "undo": "撤銷", + "copiedToClipboard": "已複製到剪貼簿", + "modified": "已修改", + "overridden": "已覆蓋", + "resetToGlobal": "重設為全域性", + "resetToDefault": "重設為預設", + "saveAll": "儲存全部", + "savingAll": "儲存全部中…", + "undoAll": "撤銷全部", + "retry": "重試" + }, + "menu": { + "system": "系統", + "systemMetrics": "系統訊息", + "configuration": "設定", + "systemLogs": "系統日誌", + "settings": "設定", + "configurationEditor": "設定編輯器", + "languages": "語言", + "language": { + "en": "English (英文)", + "es": "Español (西班牙文)", + "zhCN": "简体中文 (簡體中文)", + "hi": "हिन्दी (印地文)", + "fr": "Français (法文)", + "ar": "العربية (阿拉伯文)", + "pt": "Português (葡萄牙文)", + "ru": "Русский (俄文)", + "de": "Deutsch (德文)", + "ja": "日本語 (日文)", + "tr": "Türkçe (土耳其文)", + "it": "Italiano (義大利文)", + "nl": "Nederlands (荷蘭文)", + "sv": "Svenska (瑞典文)", + "cs": "Čeština (捷克文)", + "nb": "Norsk Bokmål (挪威文)", + "ko": "한국어 (韓文)", + "vi": "Tiếng Việt (越南文)", + "fa": "فارسی (波斯文)", + "pl": "Polski (波蘭文)", + "uk": "Українська (烏克蘭文)", + "he": "עברית (希伯來文)", + "el": "Ελληνικά (希臘文)", + "ro": "Română (羅馬尼亞文)", + "hu": "Magyar (匈牙利文)", + "fi": "Suomi (芬蘭文)", + "da": "Dansk (丹麥文)", + "sk": "Slovenčina (斯洛伐克文)", + "yue": "粵語 (粵語)", + "th": "ไทย (泰文)", + "ca": "Català (加泰隆尼亞文)", + "withSystem": { + "label": "使用系統語言設定" + }, + "ptBR": "Português brasileiro (巴西葡萄牙文)", + "sr": "Српски (塞爾維亞文)", + "sl": "Slovenščina (斯洛文尼亞文)", + "lt": "Lietuvių (立陶宛文)", + "bg": "Български (保加利亞文)", + "gl": "Galego (加利西亞文)", + "id": "Bahasa Indonesia (印尼文)", + "ur": "اردو (烏爾都文)", + "hr": "Hrvatski(克羅地亞語)", + "bs": "Bosanski (波士尼亞語)", + "zhHant": "繁體中文 (繁體中文)" + }, + "appearance": "外觀", + "darkMode": { + "label": "深色模式", + "light": "淺色", + "dark": "深色", + "withSystem": { + "label": "使用系統外觀模式設定" + } + }, + "withSystem": "系統", + "theme": { + "label": "主題", + "blue": "藍色", + "green": "綠色", + "nord": "北歐風", + "red": "紅色", + "highcontrast": "高對比", + "default": "預設" + }, + "help": "幫助", + "documentation": { + "title": "文件", + "label": "Frigate 文件" + }, + "restart": "重新啟動 Frigate", + "live": { + "title": "即時影像", + "allCameras": "所有鏡頭", + "cameras": { + "title": "鏡頭", + "count_other": "{{count}} 個鏡頭" + } + }, + "review": "審核", + "explore": "瀏覽", + "export": "匯出", + "uiPlayground": "UI 測試區", + "faceLibrary": "人臉資料庫", + "user": { + "title": "使用者", + "account": "帳號", + "current": "當前使用者:{{user}}", + "anonymous": "匿名", + "logout": "登出", + "setPassword": "設定密碼" + }, + "classification": "標籤分類", + "profiles": "設定檔", + "actions": "操作", + "features": "功能", + "chat": "聊天" + }, + "toast": { + "copyUrlToClipboard": "已複製連結至剪貼簿。", + "save": { + "title": "保存", + "error": { + "title": "保存設定變更失敗:{{errorMessage}}", + "noMessage": "保存設定變更失敗" + }, + "success": "成功儲存設定檔。" + } + }, + "role": { + "title": "角色", + "admin": "管理員", + "viewer": "檢視者", + "desc": "管理員可以存取 Frigate UI 的全部功能。檢視者僅能查看鏡頭影像、審核物件及歷史紀錄。" + }, + "pagination": { + "label": "分頁", + "previous": { + "title": "上一頁", + "label": "前往上一頁" + }, + "next": { + "title": "下一頁", + "label": "前往下一頁" + }, + "more": "更多頁面" + }, + "accessDenied": { + "documentTitle": "拒絕存取 - Frigate", + "title": "拒絕存取", + "desc": "你沒有瀏覽此頁面的權限。" + }, + "notFound": { + "documentTitle": "找不到頁面 - Frigate", + "title": "404", + "desc": "找不到頁面" + }, + "selectItem": "選擇 {{item}}", + "readTheDocumentation": "閱讀文件", + "list": { + "two": "{{0}}和{{1}}", + "many": "{{items}}和{{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "可選的", + "internalID": "在Frigate 設定檔和資料庫使用的內部ID" + }, + "information": { + "pixels": "{{area}} 像素" + }, + "no_items": "沒有項目", + "validation_errors": "驗證錯誤", + "credentialField": { + "savedPlaceholder": "已儲存 — 留空以保留當前設定" + } +} diff --git a/web/public/locales/zh-Hant/components/auth.json b/web/public/locales/zh-Hant/components/auth.json new file mode 100644 index 0000000..fbc70c4 --- /dev/null +++ b/web/public/locales/zh-Hant/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "使用者名稱", + "password": "密碼", + "login": "登入", + "errors": { + "usernameRequired": "使用者名稱不可為空", + "webUnknownError": "未知的錯誤,請檢查控制台日誌。", + "passwordRequired": "密碼不可為空", + "rateLimit": "超過次數限制,請稍後再試。", + "loginFailed": "登入失敗", + "unknownError": "未知錯誤,請檢查日誌。" + }, + "firstTimeLogin": "首次嘗試登入嗎?請從 Frigate 的日誌中查找產生的登入密碼等相關資訊。" + } +} diff --git a/web/public/locales/zh-Hant/components/camera.json b/web/public/locales/zh-Hant/components/camera.json new file mode 100644 index 0000000..f56923d --- /dev/null +++ b/web/public/locales/zh-Hant/components/camera.json @@ -0,0 +1,91 @@ +{ + "group": { + "label": "鏡頭群組", + "add": "新增鏡頭群組", + "edit": "編輯鏡頭群組", + "delete": { + "label": "刪除鏡頭群組", + "confirm": { + "title": "確認刪除", + "desc": "你確定要刪除鏡頭群組 {{name}} 嗎?" + } + }, + "name": { + "errorMessage": { + "mustLeastCharacters": "鏡頭群組名稱需至少包含兩個字元。", + "exists": "鏡頭群組名稱已存在。", + "nameMustNotPeriod": "鏡頭群組名稱不可包含點(.)。", + "invalid": "無效的鏡頭群組名稱。" + }, + "label": "名稱", + "placeholder": "請輸入名稱…" + }, + "cameras": { + "label": "鏡頭", + "desc": "選擇欲添加至群組的鏡頭。" + }, + "icon": "圖標", + "success": "鏡頭群組({{name}})已儲存。", + "camera": { + "setting": { + "label": "鏡頭串流設定", + "title": "{{cameraName}} 串流設定", + "desc": "更改此鏡頭群組控制台的串流選項。這些設定是裝置/瀏覽器專屬的。", + "audioIsAvailable": "此串流有提供音訊", + "audioIsUnavailable": "此串流不提供音訊", + "audio": { + "tips": { + "title": "此串流必須從你的鏡頭輸出音訊並在 go2rtc 中設定。", + "document": "請參照文件 " + } + }, + "stream": "串流", + "placeholder": "選擇串流", + "streamMethod": { + "label": "串流方式", + "placeholder": "選擇串流方式", + "method": { + "noStreaming": { + "label": "沒有串流", + "desc": "鏡頭影像每分鐘只會更新一次,並且不會進行串流。" + }, + "smartStreaming": { + "label": "智慧串流(建議)", + "desc": "智慧串流在沒有偵測到活動時會每分鐘只更新一次鏡頭影像以節約頻寬及資源。並在偵測到活動時切換為即時串流。" + }, + "continuousStreaming": { + "label": "持續串流", + "desc": { + "title": "即使為偵測到任何活動,鏡頭影像在處於畫面中時會保持即時串流。", + "warning": "持續串流可能會佔用較高的頻寬並且可能會對效能造成影響,請謹慎使用。" + } + } + } + }, + "compatibilityMode": { + "label": "相容模式", + "desc": "只有在鏡頭的串流影像中出現色彩異常及右側有斜線時才啟用此選項。" + } + }, + "birdseye": "鳥瞰" + }, + "showAll": "顯示所有鏡頭群組", + "showLess": "顯示更少", + "editGroups": "編輯鏡頭群組" + }, + "debug": { + "options": { + "label": "設定", + "title": "選項", + "showOptions": "顯示選項", + "hideOptions": "隱藏選項" + }, + "boundingBox": "定界框", + "timestamp": "時間戳", + "zones": "區域", + "mask": "遮罩", + "motion": "移動", + "regions": "區塊", + "paths": "行動軌跡" + } +} diff --git a/web/public/locales/zh-Hant/components/dialog.json b/web/public/locales/zh-Hant/components/dialog.json new file mode 100644 index 0000000..4d29471 --- /dev/null +++ b/web/public/locales/zh-Hant/components/dialog.json @@ -0,0 +1,190 @@ +{ + "restart": { + "title": "你確定要重新啟動 Frigate 嗎?", + "button": "重新啟動", + "restarting": { + "title": "Frigate 正在重新啟動", + "content": "此頁面將在 {{countdown}} 秒後重新載入。", + "button": "立即重新載入" + }, + "description": "Frigate 在重啟期間將短暫停止執行。" + }, + "explore": { + "plus": { + "review": { + "question": { + "label": "確認此 Frigate Plus 標籤", + "ask_a": "此物件是 {{label}} 嗎?", + "ask_an": "此物件是 {{label}} 嗎?", + "ask_full": "此物件是 {{untranslatedLabel}} ({{translatedLabel}}) 嗎?" + }, + "state": { + "submitted": "已提交" + }, + "toast": { + "error": "無法提交至 Frigate+,請檢查您的網路連線並再試一次。" + } + }, + "submitToPlus": { + "label": "提交到 Frigate+", + "desc": "在你欲遮蓋範圍內的物件並不是誤判。將他們標示為誤判並提交會使模型混淆。" + } + }, + "video": { + "viewInHistory": "在歷史記錄中查看" + } + }, + "export": { + "time": { + "fromTimeline": "從時間線選擇", + "lastHour_other": "過去 {{count}} 個小時", + "custom": "自定義", + "start": { + "title": "開始時間", + "label": "選擇開始時間" + }, + "end": { + "title": "結束時間", + "label": "選擇結束時間" + } + }, + "name": { + "placeholder": "替匯出資料命名" + }, + "select": "選擇", + "export": "匯出", + "selectOrExport": "選擇或匯出", + "toast": { + "success": "成功開始匯出。至 /exports 頁查看匯出資料。", + "error": { + "failed": "匯出失敗:{{error}}", + "endTimeMustAfterStartTime": "結束時間必須要在開始時間之後", + "noVaildTimeSelected": "沒有選取有效的時間範圍" + }, + "view": "查看", + "queued": "匯出已加入佇列。請在匯出頁面檢視進度。", + "batchSuccess_other": "已開始 {{count}} 個匯出,正在開啟案件。", + "batchPartial": "已開始 {{total}} 個匯出中的 {{successful}} 個。失敗的攝影機:{{failedCameras}}", + "batchFailed": "啟動匯出失敗(共 {{total}} 個)。失敗的攝影機:{{failedCameras}}", + "batchQueuedSuccess_other": "已排隊 {{count}} 個匯出,正在開啟案件。", + "batchQueuedPartial": "已將 {{total}} 個匯出中的 {{successful}} 個加入佇列。失敗的攝影機:{{failedCameras}}", + "batchQueueFailed": "未能將 {{total}} 個匯出加入佇列。失敗的攝影機:{{failedCameras}}" + }, + "fromTimeline": { + "saveExport": "保存匯出資料", + "previewExport": "預覽匯出資料", + "queueingExport": "正在加入匯出佇列…", + "useThisRange": "使用此範圍" + }, + "case": { + "newCaseOption": "建立新案件", + "newCaseNamePlaceholder": "新案件名稱", + "newCaseDescriptionPlaceholder": "案件描述", + "label": "案件", + "nonAdminHelp": "將為這些匯出檔案建立一個新的案件。", + "placeholder": "選擇案件" + }, + "queueing": "正在加入匯出佇列…", + "tabs": { + "export": "單個攝影機", + "multiCamera": "多個攝影機" + }, + "multiCamera": { + "timeRange": "時間範圍", + "selectFromTimeline": "從時間線選擇", + "cameraSelection": "攝影機", + "cameraSelectionHelp": "在此時間範圍內具有追蹤目標的攝影機會被預先選中", + "checkingActivity": "正在檢查攝影機活動…", + "noCameras": "沒有可用的攝影機", + "detectionCount_other": "{{count}} 個追蹤目標", + "nameLabel": "匯出名稱", + "namePlaceholder": "這些匯出檔案的可選基礎名稱", + "queueingButton": "正在加入匯出佇列…", + "exportButton_other": "匯出 {{count}} 個攝影機", + "searchOrSelectGroup": "搜尋或選擇一個攝影機群組…", + "selectAll": "選擇所有攝影機", + "clearSelection": "清除選擇", + "selectWithActivity": "有追蹤物件的攝影機", + "selectGroup": "選擇群組", + "noMatchingCameras": "找不到符合您搜尋條件的攝影機", + "selectedCount": "已選取 {{selected}} / {{total}}" + }, + "multi": { + "title_other": "匯出 {{count}} 個審閱", + "description": "匯出每個選定的審閱項。所有匯出檔案將歸入同一個案件。", + "descriptionNoCase": "匯出每個選定的審閱項。", + "caseNamePlaceholder": "審閱匯出 - {{date}}", + "exportButton_other": "匯出 {{count}} 個審閱", + "exportingButton": "匯出中…", + "toast": { + "started_other": "已開始 {{count}} 個匯出。正在開啟案件。", + "startedNoCase_other": "已開始 {{count}} 個匯出。", + "partial": "已啟動 {{total}} 個匯出,其中 {{successful}} 個成功。失敗項:{{failedItems}}", + "failed": "啟動匯出失敗(共 {{total}} 個)。失敗項:{{failedItems}}" + } + } + }, + "streaming": { + "label": "串流", + "restreaming": { + "disabled": "此鏡頭並未啟用串流重導向。", + "desc": { + "title": "設定 go2rtc 以啟用更多此鏡頭的預覽選項及音訊。", + "readTheDocumentation": "閱讀文件" + } + }, + "showStats": { + "label": "顯示串流統計資料", + "desc": "啟用此選項以在鏡頭畫面上顯示串流統計資料。" + }, + "debugView": "除錯檢視" + }, + "search": { + "saveSearch": { + "label": "儲存搜尋", + "desc": "替此保存的搜尋命名。", + "placeholder": "請輸入搜尋的名稱", + "overwrite": "{{searchName}} 已存在。保存將會覆蓋現有資料。", + "success": "搜尋 {{searchName}} 已保存。", + "button": { + "save": { + "label": "保存此次搜尋" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "確認刪除", + "desc": { + "selected": "您確定要刪除所有與此審查項目相關的錄影檔案嗎?

    按住 Shift 鍵可在未來跳過此確認步驟。" + }, + "toast": { + "success": "已成功刪除與選擇的審核物件有關的影片片段。", + "error": "刪除失敗:{{error}}" + } + }, + "button": { + "export": "匯出", + "markAsReviewed": "標記為已審核", + "deleteNow": "立即刪除", + "markAsUnreviewed": "標記為未審核" + }, + "shareTimestamp": { + "label": "分享該時間片段", + "title": "分享該時間片段", + "description": "分享帶當前錄製播放時間的網址,或選擇自訂時間。請注意這不是公開的分享連結,只有具備 Frigate 及此攝影機存取權限的使用者才能存取。", + "custom": "自訂時間", + "button": "分享時間片段網址", + "shareTitle": "Frigate 審閱時間:{{camera}}" + } + }, + "imagePicker": { + "selectImage": "選取追蹤物件預覽圖", + "unknownLabel": "已儲存觸發圖片", + "search": { + "placeholder": "以標籤或子標籤搜尋..." + }, + "noImages": "未找到此攝影機的縮圖" + } +} diff --git a/web/public/locales/zh-Hant/components/filter.json b/web/public/locales/zh-Hant/components/filter.json new file mode 100644 index 0000000..1b3cbe0 --- /dev/null +++ b/web/public/locales/zh-Hant/components/filter.json @@ -0,0 +1,140 @@ +{ + "filter": "過濾", + "labels": { + "label": "標籤", + "all": { + "title": "所有標籤", + "short": "標籤" + }, + "count_one": "{{count}} 個標籤", + "count_other": "{{count}} 個標籤" + }, + "zones": { + "all": { + "title": "所有區域", + "short": "區域" + }, + "label": "區域" + }, + "dates": { + "selectPreset": "選擇預設設定…", + "all": { + "title": "所有日期", + "short": "日期" + } + }, + "more": "更多過濾條件", + "reset": { + "label": "重設過濾條件為預設值" + }, + "timeRange": "時間範圍", + "subLabels": { + "label": "子標籤", + "all": "所有子標籤" + }, + "score": "分數", + "estimatedSpeed": "估計速度({{unit}})", + "features": { + "label": "特徵", + "hasSnapshot": "包含截圖", + "hasVideoClip": "包含影片片段", + "submittedToFrigatePlus": { + "label": "已提交至 Frigate+", + "tips": "你必須先過濾出包含截圖的追蹤物件。

    無法提交不包含截圖的追蹤物件至 Frigate+。" + } + }, + "sort": { + "label": "排序", + "dateAsc": "日期(由舊到新)", + "dateDesc": "日期(由新到舊)", + "scoreAsc": "物件分數(由小到大)", + "scoreDesc": "物件分數(由大到小)", + "speedAsc": "估計速度(由慢到快)", + "speedDesc": "估計速度(由快到慢)", + "relevance": "相關性" + }, + "cameras": { + "label": "鏡頭過濾", + "all": { + "title": "所有鏡頭", + "short": "鏡頭" + } + }, + "review": { + "showReviewed": "顯示已審核的內容" + }, + "motion": { + "showMotionOnly": "僅顯示有移動的內容" + }, + "explore": { + "settings": { + "title": "設定", + "defaultView": { + "title": "預設檢視", + "desc": "當未選擇過濾條件時,顯示每個標籤最近追蹤物件的摘要,或顯示未過濾的網格。", + "summary": "摘要", + "unfilteredGrid": "未過濾的網格" + }, + "gridColumns": { + "title": "網格欄位數", + "desc": "選擇檢視網格的欄位數量。" + }, + "searchSource": { + "label": "搜尋來源", + "desc": "選擇搜尋追蹤物件的截圖或是說明。", + "options": { + "thumbnailImage": "截圖", + "description": "說明" + } + } + }, + "date": { + "selectDateBy": { + "label": "選擇要過濾的日期" + } + } + }, + "logSettings": { + "label": "過濾日誌等級", + "filterBySeverity": "依嚴重程度過濾日誌", + "loading": { + "title": "讀取中", + "desc": "當日誌頁面捲動到底部時,新日誌將自動更新顯示。" + }, + "disableLogStreaming": "停用日誌串流", + "allLogs": "所有日誌" + }, + "trackedObjectDelete": { + "title": "確認刪除", + "desc": "刪除這 {{objectLength}} 個追蹤物件將會移除快照、已儲存的嵌入向量資料(embeddings),以及相關的物件生命週期紀錄。歷史檢視中的錄影檔案 不會 被刪除。

    確定要繼續嗎?

    按住 Shift 鍵可在未來跳過此對話框。", + "toast": { + "success": "成功刪除追蹤物件。", + "error": "刪除追蹤物件失敗:{{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "按區域遮罩過濾" + }, + "recognizedLicensePlates": { + "title": "已辨識車牌", + "loadFailed": "讀取已辨識車牌失敗。", + "loading": "讀取已辨識車牌中…", + "placeholder": "輸入以搜尋車牌…", + "noLicensePlatesFound": "未找到車牌。", + "selectPlatesFromList": "從列表中選擇一個或多個車牌。", + "selectAll": "全選", + "clearAll": "全部清除" + }, + "classes": { + "label": "類別", + "all": { + "title": "所有類別" + }, + "count_one": "{{count}} 個類別", + "count_other": "{{count}} 個類別" + }, + "attributes": { + "label": "分類屬性", + "all": "所有屬性" + } +} diff --git a/web/public/locales/zh-Hant/components/icons.json b/web/public/locales/zh-Hant/components/icons.json new file mode 100644 index 0000000..467858b --- /dev/null +++ b/web/public/locales/zh-Hant/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "選擇圖示", + "search": { + "placeholder": "搜尋圖示…" + } + } +} diff --git a/web/public/locales/zh-Hant/components/input.json b/web/public/locales/zh-Hant/components/input.json new file mode 100644 index 0000000..ed7eee7 --- /dev/null +++ b/web/public/locales/zh-Hant/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "下載影片", + "toast": { + "success": "你的審查項目影片已開始下載。" + } + } + } +} diff --git a/web/public/locales/zh-Hant/components/player.json b/web/public/locales/zh-Hant/components/player.json new file mode 100644 index 0000000..7818615 --- /dev/null +++ b/web/public/locales/zh-Hant/components/player.json @@ -0,0 +1,53 @@ +{ + "noRecordingsFoundForThisTime": "此時間段內沒有錄影", + "noPreviewFound": "找不到預覽", + "noPreviewFoundFor": "找不到 {{cameraName}} 的預覽", + "submitFrigatePlus": { + "title": "提交此畫面至 Frigate+?", + "submit": "提交", + "previewError": "無法載入快照預覽。該錄製當前可能不可用。" + }, + "streamOffline": { + "desc": "{{cameraName}} 的 detect 串流未接收到任何畫面,請檢查錯誤日誌", + "title": "串流離線" + }, + "cameraDisabled": "鏡頭已關閉", + "stats": { + "streamType": { + "title": "串流類型:", + "short": "類型" + }, + "bandwidth": { + "title": "頻寬:", + "short": "頻寬" + }, + "latency": { + "title": "延遲:", + "value": "{{seconds}} 秒", + "short": { + "title": "延遲", + "value": "{{seconds}} 秒" + } + }, + "totalFrames": "總幀數:", + "droppedFrames": { + "title": "掉幀數:", + "short": { + "title": "掉幀", + "value": "{{droppedFrames}} 幀" + } + }, + "decodedFrames": "已解碼幀數:", + "droppedFrameRate": "掉幀率:" + }, + "livePlayerRequiredIOSVersion": "此串流類型需要 IOS 17.1 或以上版本。", + "toast": { + "success": { + "submittedFrigatePlus": "成功提交畫面至 Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "提交畫面至 Frigate+ 失敗" + } + }, + "cameraOff": "攝影機關機" +} diff --git a/web/public/locales/zh-Hant/config/cameras.json b/web/public/locales/zh-Hant/config/cameras.json new file mode 100644 index 0000000..b9c9244 --- /dev/null +++ b/web/public/locales/zh-Hant/config/cameras.json @@ -0,0 +1,957 @@ +{ + "name": { + "description": "必須填寫攝影機名稱", + "label": "攝影機名稱" + }, + "label": "攝影機設定", + "friendly_name": { + "label": "顯示名稱", + "description": "攝影機在 Frigate 介面顯示的名稱" + }, + "enabled": { + "label": "已啟用", + "description": "已啟用" + }, + "audio": { + "label": "音訊事件", + "description": "此攝影機的音訊事件偵測設定。", + "enabled": { + "label": "啟用音訊偵測", + "description": "啟用或停用此攝影機的音訊事件偵測。" + }, + "max_not_heard": { + "label": "結束逾時", + "description": "在未偵測到已設定音訊類型的情況下,經過多少秒後視為音訊事件結束。" + }, + "min_volume": { + "label": "最小音量", + "description": "執行音訊偵測所需的最小 RMS 音量門檻;數值越低,敏感度越高(例如:200 高、500 中、1000 低)。" + }, + "listen": { + "label": "監聽的音訊類型", + "description": "要偵測的音訊事件類型清單(例如:狗吠、火警、尖叫、說話、大叫)。" + }, + "filters": { + "label": "音訊過濾器", + "description": "按音訊型別的過濾器設定,如用於減少誤報的置信度閾值。", + "threshold": { + "label": "最低音訊置信度", + "description": "音訊事件被計入的最低置信度閾值。" + } + }, + "enabled_in_config": { + "label": "原始音訊狀態", + "description": "指示原始靜態設定檔中是否開啟了音訊偵測。" + }, + "num_threads": { + "label": "偵測執行緒", + "description": "用於音訊偵測處理的執行緒數量。" + } + }, + "mqtt": { + "label": "MQTT", + "description": "MQTT 影像釋出設定。", + "enabled": { + "label": "傳送影像", + "description": "為此攝影機啟用向 MQTT 主題釋出目標影像快照。" + }, + "timestamp": { + "label": "新增時間戳", + "description": "在釋出到 MQTT 的影像上疊加時間戳。" + }, + "bounding_box": { + "label": "新增邊界框", + "description": "在透過 MQTT 釋出的影像上繪製邊界框。" + }, + "crop": { + "label": "裁剪影像", + "description": "將釋出到 MQTT 的影像裁剪到偵測到的目標邊界框。" + }, + "height": { + "label": "影像高度", + "description": "透過 MQTT 釋出的影像的調整高度(像素)。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能釋出 MQTT 影像的區域。" + }, + "quality": { + "label": "JPEG 品質", + "description": "釋出到 MQTT 的影像的 JPEG 品質(0-100)。" + } + }, + "notifications": { + "label": "通知", + "enabled": { + "label": "開啟通知", + "description": "為此攝影機啟用或停用通知。" + }, + "email": { + "label": "通知郵箱", + "description": "用於推送通知或某些通知提供商要求的郵箱地址。" + }, + "cooldown": { + "label": "冷卻時間", + "description": "通知之間的冷卻時間(秒),以避免向收件人傳送垃圾資訊。" + }, + "enabled_in_config": { + "label": "原始通知狀態", + "description": "指示原始靜態配置中是否啟用了通知。" + }, + "description": "為此攝影機啟用和控制通知的設定。" + }, + "birdseye": { + "label": "鳥瞰圖", + "description": "將多路攝影機畫面合併為統一佈局的鳥瞰合成檢視設定。", + "enabled": { + "label": "開啟鳥瞰圖", + "description": "開啟或關閉鳥瞰圖功能。" + }, + "mode": { + "label": "追蹤模式", + "description": "在鳥瞰檢視中包含攝影機的模式:'objects'(目標)、'motion'(動作)或 'continuous'(持續)。" + }, + "order": { + "label": "排序位置", + "description": "用於控制攝影機在鳥瞰檢視佈局中排序位置的數值。" + } + }, + "detect": { + "label": "目標偵測", + "description": "用於執行目標偵測、初始化追蹤器的偵測模組設定。", + "enabled": { + "label": "開啟目標偵測", + "description": "開啟或關閉該攝影機的目標偵測。" + }, + "height": { + "label": "偵測畫面高度", + "description": "用於配置偵測流的畫面高度(像素);留空則使用原始影片流解析度。" + }, + "width": { + "label": "偵測畫面寬度", + "description": "用於配置偵測流的畫面寬度(像素);留空則使用原始影片流解析度。" + }, + "fps": { + "label": "偵測幀率", + "description": "偵測時希望使用的幀率;數值越低,CPU 佔用越小(推薦值為 5,僅在追蹤極高速運動的目標時才設定更高數值,最高不建議超過 10)。" + }, + "min_initialized": { + "label": "最小初始化幀數", + "description": "建立追蹤目標前,需要連續偵測到目標的次數。數值越大,錯誤觸發的追蹤越少。預設值為幀率除以 2。" + }, + "max_disappeared": { + "label": "最大消失幀數", + "description": "追蹤目標在連續多少幀未被偵測到時,將被判定為已消失。" + }, + "stationary": { + "label": "靜止目標配置", + "description": "用於偵測和管理長時間靜止目標的相關設定。", + "interval": { + "label": "靜止間隔", + "description": "設定每隔多少幀執行一次偵測,用於確認目標是否處於靜止狀態。" + }, + "threshold": { + "label": "靜止閾值", + "description": "目標需要連續多少幀位置不變,才會被標記為靜止狀態。" + }, + "max_frames": { + "label": "最大幀數", + "description": "限制靜止目標最大追蹤時長(以幀數為單位),超過將會停止追蹤。", + "default": { + "label": "預設最大幀數", + "description": "停止追蹤前,用於追蹤靜止目標的預設最大幀數。" + }, + "objects": { + "label": "目標最大幀數", + "description": "可對不同型別目標分別設定靜止追蹤的最大幀數(覆蓋全域性設定)。" + } + }, + "classifier": { + "label": "開啟視覺分類器", + "description": "使用視覺分類器,即使偵測框有輕微抖動,也能準確判斷物體是否為靜止。" + } + }, + "annotation_offset": { + "label": "標記偏移量", + "description": "偵測標記的時間偏移量(毫秒),用於讓時間軸上的偵測框與錄影畫面更精準對齊;可設定為正數或負數。" + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg 編解碼相關設定,包含可執行檔案路徑、命令列引數、硬體加速選項,以及按不同功能劃分的輸出引數。", + "path": { + "label": "FFmpeg 路徑", + "description": "要使用的 FFmpeg 可執行檔案路徑,或版本別名(如 \"5.0\" 或 \"7.0\")。" + }, + "global_args": { + "label": "FFmpeg 全域性引數", + "description": "傳遞給 FFmpeg 程序的全域性引數。" + }, + "hwaccel_args": { + "label": "硬體加速引數", + "description": "用於 FFmpeg 的硬體加速引數。建議使用對應硬體廠商的預設配置。" + }, + "input_args": { + "label": "輸入引數", + "description": "應用於 FFmpeg 輸入影片流的輸入引數。" + }, + "output_args": { + "label": "輸出引數", + "description": "用於不同 FFmpeg 功能(如偵測、錄製)的預設輸出引數。", + "detect": { + "label": "偵測輸出引數", + "description": "偵測功能影片流的預設輸出引數。" + }, + "record": { + "label": "錄製輸出引數", + "description": "錄製功能影片流的預設輸出引數。" + } + }, + "retry_interval": { + "label": "FFmpeg 重試時間", + "description": "攝影機影片流異常斷開後,重新連線前的等待時間。預設為 10 秒。" + }, + "apple_compatibility": { + "label": "Apple 相容性", + "description": "錄製 H.265 影片時啟用 HEVC 標記,以提升對 Apple 裝置播放的相容性。" + }, + "gpu": { + "label": "GPU 索引", + "description": "在啟用硬體加速時,預設使用的 GPU 索引。" + }, + "inputs": { + "label": "攝影機輸入影片流", + "description": "該攝影機的所有輸入流配置清單(包含路徑和功能)。", + "path": { + "label": "輸入路徑", + "description": "攝影機輸入影片流的地址或路徑。" + }, + "roles": { + "label": "輸入流功能", + "description": "定義該影片流的功能。" + }, + "global_args": { + "label": "FFmpeg 全域性引數", + "description": "該輸入影片流使用的 FFmpeg 全域性通用引數。" + }, + "hwaccel_args": { + "label": "硬體加速引數", + "description": "該輸入影片流的硬體加速引數。" + }, + "input_args": { + "label": "輸入引數", + "description": "該影片流特定的輸入引數。" + } + } + }, + "live": { + "label": "即時監控觀看", + "streams": { + "label": "即時監控流名稱", + "description": "配置的流名稱到用於即時監控播放的 restream/go2rtc 名稱的對映。" + }, + "height": { + "label": "即時監控高度", + "description": "在網頁頁面中渲染 jsmpeg 即時監控流的高度(像素);必須小於等於偵測流高度。" + }, + "quality": { + "label": "即時監控品質", + "description": "jsmpeg 流的編碼品質(1 最高,31 最低)。" + }, + "description": "用於控制即時流選擇、解析度和品質的網頁頁面設定。" + }, + "motion": { + "label": "畫面變動偵測", + "enabled": { + "label": "開啟畫面變動偵測", + "description": "開啟或關閉此攝影機的畫面變動偵測。" + }, + "threshold": { + "label": "畫面變動閾值", + "description": "畫面變動偵測器使用的像素差異閾值;數值越高靈敏度越低(範圍 1-255)。" + }, + "lightning_threshold": { + "label": "閃電閾值", + "description": "用於偵測和忽略短暫閃電閃爍的閾值(數值越低越敏感,範圍 0.3 到 1.0)。這不會完全阻止畫面變動偵測;只是當超過閾值時偵測器會停止分析額外的幀。在此類事件期間仍會建立基於畫面變動的錄影。" + }, + "skip_motion_threshold": { + "label": "跳過畫面變動閾值", + "description": "如果單幀中畫面變化超過此比例,偵測器將判定為無畫面變動並立即重新校準。這可以節省 CPU 並減少閃電、風暴等情況下的誤報,但也可能會錯過真正的事件,如 PTZ 攝影機自動追蹤目標。你需要權衡取捨:是否犧牲少量錄製片段,換取更少無效影片與更低的誤檢。保持為空即可關閉該功能。" + }, + "improve_contrast": { + "label": "改善對比度", + "description": "在畫面變動分析之前對幀應用對比度改善以幫助偵測。" + }, + "contour_area": { + "label": "輪廓區域", + "description": "畫面變動輪廓被計入所需的最小輪廓區域(像素)。" + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "用於畫面變動計算的幀差異中使用的 alpha 混合因子。" + }, + "frame_alpha": { + "label": "畫面 alpha 通道", + "description": "畫面變動預處理時混合畫面所使用的 alpha 值。" + }, + "frame_height": { + "label": "畫面高度", + "description": "計算畫面變動時縮放畫面的高度(像素)。" + }, + "mask": { + "label": "遮罩座標", + "description": "定義用於包含/排除區域的畫面變動遮罩多邊形的有序 x,y 座標。" + }, + "mqtt_off_delay": { + "label": "MQTT 關閉延遲", + "description": "在釋出 MQTT 'off' 狀態之前,最後一次畫面變動後等待的秒數。" + }, + "enabled_in_config": { + "label": "原始畫面變動狀態", + "description": "指示原始靜態配置中是否啟用了畫面變動偵測。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "description": "此攝影機的預設畫面變動偵測設定。" + }, + "objects": { + "label": "目標", + "description": "目標追蹤預設設定,包括要追蹤的標籤和按目標的過濾器。", + "track": { + "label": "要追蹤的目標", + "description": "此攝影機要追蹤的目標標籤清單。" + }, + "filters": { + "label": "目標過濾器", + "description": "應用於偵測到的目標以減少誤報的過濾器(區域、比例、置信度)。", + "min_area": { + "label": "最小目標區域", + "description": "此目標型別所需的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "max_area": { + "label": "最大目標區域", + "description": "此目標型別允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "min_ratio": { + "label": "最小縱橫比", + "description": "邊界框所需的最小寬高比。" + }, + "max_ratio": { + "label": "最大縱橫比", + "description": "邊界框允許的最大寬高比。" + }, + "threshold": { + "label": "置信度閾值", + "description": "目標被視為真正陽性所需的平均偵測置信度閾值。" + }, + "min_score": { + "label": "最小置信度", + "description": "目標被計入所需的最小單幀偵測置信度。" + }, + "mask": { + "label": "過濾器遮罩", + "description": "定義此過濾器在幀內應用位置的多邊形座標。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "mask": { + "label": "目標遮罩", + "description": "用於防止在指定區域進行目標偵測的遮罩多邊形。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "genai": { + "label": "生成式 AI 目標配置", + "description": "用於傳送畫面給生成式 AI 進行生成和描述追蹤目標的選項。", + "enabled": { + "label": "開啟生成式 AI", + "description": "預設開啟生成式 AI 生成追蹤目標的描述。" + }, + "use_snapshot": { + "label": "使用快照", + "description": "使用目標快照而不是縮圖給生成式 AI 進行描述生成。" + }, + "prompt": { + "label": "字幕提示", + "description": "使用生成式 AI 生成描述時使用的預設提示模板。" + }, + "object_prompts": { + "label": "目標提示", + "description": "按目標設定提示詞,讓生成式 AI 對不同標籤的輸出進行定製。" + }, + "objects": { + "label": "生成式 AI 目標", + "description": "預設傳送給生成式 AI 的目標標籤清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入這些區域,才會觸發生成式 AI 描述生成。" + }, + "debug_save_thumbnails": { + "label": "儲存縮圖", + "description": "儲存傳送給生成式 AI 的縮圖用於除錯和審閱。" + }, + "send_triggers": { + "label": "生成式 AI 觸發器", + "description": "定義畫面幀應在何時傳送給生成式 AI(如偵測結束時、更新後等)。", + "tracked_object_end": { + "label": "結束時傳送", + "description": "目標追蹤結束時向生成式 AI 傳送請求。" + }, + "after_significant_updates": { + "label": "生成式 AI 提前觸發", + "description": "在追蹤目標發生指定次數的重要變化後,向生成式 AI 傳送請求。" + } + }, + "enabled_in_config": { + "label": "原配置生成式 AI 狀態", + "description": "表示在原始靜態配置中是否已啟用生成式 AI。" + } + } + }, + "record": { + "label": "錄影", + "enabled": { + "label": "開啟錄影", + "description": "開啟或關閉此攝影機的錄影。" + }, + "expire_interval": { + "label": "錄影清理間隔", + "description": "清理過期錄影片段的間隔分鐘數。" + }, + "continuous": { + "label": "持續保留", + "description": "無論是否有追蹤目標或動作,保留錄影的天數。如果只想保留警報和偵測的錄影,請設定為 0。", + "days": { + "label": "保留天數", + "description": "保留錄影的天數。" + } + }, + "motion": { + "label": "動作保留", + "description": "無論是否有追蹤目標,由動作觸發的錄影保留天數。如果只想保留警報和偵測的錄影,請設定為 0。", + "days": { + "label": "保留天數", + "description": "保留錄影的天數。" + } + }, + "detections": { + "label": "偵測保留", + "description": "偵測事件的錄影保留設定,包括前後捕獲時長。", + "pre_capture": { + "label": "前捕獲秒數", + "description": "偵測事件之前包含在錄影中的秒數。" + }, + "post_capture": { + "label": "後捕獲秒數", + "description": "偵測事件之後包含在錄影中的秒數。" + }, + "retain": { + "label": "事件保留", + "description": "偵測事件錄影的保留設定。", + "days": { + "label": "保留天數", + "description": "保留偵測事件錄影的天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + } + } + }, + "alerts": { + "label": "警報保留", + "description": "警報事件的錄影保留設定,包括前後捕獲時長。", + "pre_capture": { + "label": "前捕獲秒數", + "description": "偵測事件之前包含在錄影中的秒數。" + }, + "post_capture": { + "label": "後捕獲秒數", + "description": "偵測事件之後包含在錄影中的秒數。" + }, + "retain": { + "label": "事件保留", + "description": "偵測事件錄影的保留設定。", + "days": { + "label": "保留天數", + "description": "保留偵測事件錄影的天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + } + } + }, + "export": { + "label": "匯出配置", + "description": "匯出錄影時使用的設定,如延時攝影和硬體加速。", + "hwaccel_args": { + "label": "匯出硬體加速引數", + "description": "用於匯出/轉碼操作的硬體加速引數。" + }, + "max_concurrent": { + "label": "最大併發匯出數", + "description": "同時可處理的最大匯出任務數量。" + } + }, + "preview": { + "label": "預覽配置", + "description": "控制介面中顯示的錄影預覽品質的設定。", + "quality": { + "label": "預覽品質", + "description": "預覽品質級別(very_low、low、medium、high、very_high)。" + } + }, + "enabled_in_config": { + "label": "原始錄影狀態", + "description": "指示原始靜態配置中是否啟用了錄影。" + }, + "description": "此攝影機的錄影和保留設定。" + }, + "review": { + "label": "審閱", + "alerts": { + "label": "警報配置", + "description": "哪些追蹤目標生成警報以及如何保留警報的設定。", + "enabled": { + "label": "開啟警報", + "description": "開啟或關閉此攝影機的警報生成。" + }, + "labels": { + "label": "警報標籤", + "description": "符合警報條件的目標標籤清單(例如:car、person)。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能被視為警報的區域;留空則允許任何區域。" + }, + "enabled_in_config": { + "label": "原始警報狀態", + "description": "追蹤原始靜態配置中是否啟用了警報。" + }, + "cutoff_time": { + "label": "警報截止時間", + "description": "在沒有引起警報的活動後等待多少秒後截止警報。" + } + }, + "detections": { + "label": "偵測配置", + "description": "用於設定哪些追蹤目標會生成偵測記錄(非警報類),以及偵測記錄的保留方式。", + "enabled": { + "label": "開啟偵測", + "description": "開啟或關閉此攝影機的偵測事件。" + }, + "labels": { + "label": "偵測標籤", + "description": "符合偵測事件條件的目標標籤清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能被視為偵測的區域;留空則允許任何區域。" + }, + "cutoff_time": { + "label": "偵測截止時間", + "description": "在沒有引起偵測的活動後等待多少秒後截止偵測。" + }, + "enabled_in_config": { + "label": "原始偵測狀態", + "description": "追蹤原始靜態配置中是否啟用了偵測。" + } + }, + "genai": { + "label": "生成式 AI 配置", + "description": "控制使用生成式 AI 為審閱項生成描述和摘要。", + "enabled": { + "label": "開啟生成式 AI 描述", + "description": "為審閱項開啟或關閉使用生成式 AI 生成描述和摘要。" + }, + "alerts": { + "label": "為警報開啟生成式 AI", + "description": "使用生成式 AI 為警報項生成描述。" + }, + "detections": { + "label": "為偵測開啟生成式 AI", + "description": "使用生成式 AI 為偵測項生成描述。" + }, + "image_source": { + "label": "審閱影像來源", + "description": "傳送給生成式 AI 的畫面來源('preview' 或 'recordings');'recordings' 使用更高品質的畫面幀,但會消耗更多的 token。" + }, + "additional_concerns": { + "label": "額外關注事項", + "description": "生成式 AI 在分析此攝影機的監控行為時,需要額外注意的事項或說明清單。" + }, + "debug_save_thumbnails": { + "label": "儲存縮圖", + "description": "儲存傳送給生成式 AI 提供商的縮圖用於除錯和審閱。" + }, + "enabled_in_config": { + "label": "原配置生成式 AI 狀態", + "description": "記錄在靜態配置中最初是否已啟用生成式 AI 審閱功能。" + }, + "preferred_language": { + "label": "首選語言", + "description": "向生成式 AI 提供商請求生成回應的首選語言。" + }, + "activity_context_prompt": { + "label": "活動上下文提示", + "description": "自訂提示詞,用於說明可疑行為與非可疑行為的界定,為生成式 AI 生成摘要提供上下文依據。" + } + }, + "description": "控制此攝影機的警報、偵測和生成式 AI 審閱總結的設定,這些設定會被介面與儲存功能使用。" + }, + "snapshots": { + "label": "快照", + "enabled": { + "label": "開啟快照", + "description": "開啟或關閉此攝影機的快照儲存。" + }, + "timestamp": { + "label": "時間戳疊加", + "description": "在 API 生成的快照上疊加時間戳。" + }, + "bounding_box": { + "label": "邊界框疊加", + "description": "在 API 生成的快照上繪製追蹤目標的邊界框。" + }, + "crop": { + "label": "裁剪快照", + "description": "在 API 生成的快照裁剪到偵測到的目標邊界框。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能儲存快照的區域。" + }, + "height": { + "label": "快照高度", + "description": "將 API 生成的快照調整到的目標高度(像素);留空則保持原始大小。" + }, + "retain": { + "label": "快照保留", + "description": "快照的保留設定,包括預設天數和按目標覆蓋。", + "default": { + "label": "預設保留", + "description": "保留快照的預設天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + }, + "objects": { + "label": "目標保留", + "description": "按目標覆蓋的快照保留天數。" + } + }, + "quality": { + "label": "快照品質", + "description": "儲存快照的編碼品質(0-100)。" + }, + "description": "此攝影機的追蹤目標 API 快照設定。" + }, + "timestamp_style": { + "label": "時間戳樣式", + "position": { + "label": "時間戳位置", + "description": "時間戳在影像上的位置(tl/tr/bl/br)。" + }, + "format": { + "label": "時間戳格式", + "description": "用於時間戳的日期時間格式字串(Python 日期時間格式程式碼)。" + }, + "color": { + "label": "時間戳顏色", + "description": "時間戳文字的 RGB 顏色值(所有值 0-255)。", + "red": { + "label": "紅色", + "description": "時間戳顏色的紅色分量(0-255)。" + }, + "green": { + "label": "綠色", + "description": "時間戳顏色的綠色分量(0-255)。" + }, + "blue": { + "label": "藍色", + "description": "時間戳顏色的藍色分量(0-255)。" + } + }, + "thickness": { + "label": "時間戳粗細", + "description": "時間戳文字的線條粗細。" + }, + "effect": { + "label": "時間戳效果", + "description": "時間戳文字的視覺效果(none、solid、shadow)。" + }, + "description": "應用於錄影和快照的即時監控流中時間戳的樣式選項。" + }, + "audio_transcription": { + "label": "音訊轉錄", + "description": "用於事件和即時字幕的即時和語音音訊轉錄設定。", + "live_enabled": { + "label": "即時監控轉寫", + "description": "在接收到音訊時開啟即時監控持續轉寫。" + }, + "enabled": { + "label": "啟用語音轉文字", + "description": "開啟或關閉手動觸發的音訊事件轉寫。" + }, + "enabled_in_config": { + "label": "原始轉寫狀態" + } + }, + "semantic_search": { + "label": "語意搜尋", + "triggers": { + "label": "觸發器", + "description": "攝影機特定語意搜尋觸發器的操作和匹配條件。", + "friendly_name": { + "label": "友好名稱", + "description": "在 UI 中為此觸發器顯示的可選友好名稱。" + }, + "enabled": { + "label": "開啟此觸發器", + "description": "啟用或停用此語意搜尋觸發器。" + }, + "type": { + "label": "觸發器型別", + "description": "觸發器型別:'thumbnail'(與影像匹配)或 'description'(與文字匹配)。" + }, + "data": { + "label": "觸發器內容", + "description": "要與追蹤目標匹配的文字短語或縮圖 ID。" + }, + "threshold": { + "label": "觸發器閾值", + "description": "啟用此觸發器所需的最小相似度分數(0-1)。" + }, + "actions": { + "label": "觸發器操作", + "description": "觸發器匹配時要執行的操作清單(通知、sub_label、屬性)。" + } + }, + "description": "語意搜尋設定,用於構建和查詢目標嵌入以查詢相似項目。" + }, + "face_recognition": { + "label": "人臉辨識", + "enabled": { + "label": "開啟人臉辨識", + "description": "開啟或關閉人臉辨識。" + }, + "min_area": { + "label": "最小人臉區域", + "description": "需要嘗試進行人臉辨識的人臉偵測框最小大小(像素)。" + }, + "description": "該攝影機的人臉偵測與辨識設定。" + }, + "lpr": { + "label": "車牌辨識", + "description": "車牌辨識設定,包括偵測閾值、格式化和已知車牌。", + "enabled": { + "label": "開啟車牌辨識", + "description": "在此攝影機上啟用或停用車牌辨識。" + }, + "min_area": { + "label": "最小車牌區域", + "description": "嘗試辨識所需的最小車牌區域(像素)。" + }, + "enhancement": { + "label": "增強級別", + "description": "在 OCR 之前應用於車牌裁剪的增強級別(0-10);較高的值可能不總是改善結果,5 以上的級別可能僅適用於夜間車牌,應謹慎使用。" + }, + "expire_time": { + "label": "過期秒數", + "description": "未見到的車牌從追蹤器中過期的時間(秒)(僅適用於專用 LPR 攝影機)。" + } + }, + "profiles": { + "label": "設定檔", + "description": "可在執行時切換指定命名的設定檔,支援區域性覆蓋引數。" + }, + "onvif": { + "label": "ONVIF", + "description": "此攝影機的 ONVIF 連線和 PTZ 自動追蹤設定。", + "host": { + "label": "ONVIF 主機", + "description": "此攝影機 ONVIF 服務的主機(和可選協議)。" + }, + "port": { + "label": "ONVIF 埠", + "description": "ONVIF 服務的埠號。" + }, + "user": { + "label": "ONVIF 使用者名稱", + "description": "ONVIF 身份驗證的使用者名稱;某些裝置需要管理員使用者才能使用 ONVIF。" + }, + "password": { + "label": "ONVIF 密碼", + "description": "ONVIF 身份驗證的密碼。" + }, + "tls_insecure": { + "label": "停用 TLS 驗證", + "description": "跳過 TLS 驗證並停用 ONVIF 的摘要認證(不安全;僅用於安全網路)。" + }, + "profile": { + "label": "ONVIF 設定檔", + "description": "用於 PTZ 控制的指定 ONVIF 媒體配置,將透過 Token 或名稱匹配。如果未手動指定,將自動選擇第一個包含有效 PTZ 配置的媒體配置。" + }, + "autotracking": { + "label": "自動追蹤", + "description": "使用 PTZ 攝影機移動自動追蹤移動目標並使其保持在畫面中心。", + "enabled": { + "label": "開啟自動追蹤", + "description": "啟用或停用偵測目標的自動 PTZ 攝影機追蹤。" + }, + "calibrate_on_startup": { + "label": "啟動時校準", + "description": "在啟動時測量 PTZ 電機速度以提高追蹤精度。Frigate 將在校準後用 movement_weights 更新配置。" + }, + "zooming": { + "label": "變焦模式", + "description": "控制變焦行為:disabled(僅平移/傾斜)、absolute(最相容)或 relative(同時平移/傾斜/變焦)。" + }, + "zoom_factor": { + "label": "變焦因子", + "description": "控制追蹤目標的變焦級別。數值越低保持更多場景可見;數值越高放大更近但可能丟失追蹤。數值範圍 0.1 到 0.75。" + }, + "track": { + "label": "追蹤目標", + "description": "應觸發自動追蹤的目標型別清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入這些區域之一才能開始自動追蹤。" + }, + "return_preset": { + "label": "返回預設", + "description": "追蹤結束後返回的攝影機韌體中配置的 ONVIF 預設名稱。" + }, + "timeout": { + "label": "返回超時", + "description": "失去追蹤後等待多少秒後將攝影機返回到預設位置。" + }, + "movement_weights": { + "label": "移動權重", + "description": "由攝影機校準自動生成的校準值。請勿手動修改。" + }, + "enabled_in_config": { + "label": "原始自動追蹤狀態", + "description": "用於追蹤配置中是否啟用自動追蹤的內部欄位。" + } + }, + "ignore_time_mismatch": { + "label": "忽略時間不匹配", + "description": "忽略 ONVIF 通訊中攝影機和 Frigate 伺服器之間的時間同步差異。" + } + }, + "best_image_timeout": { + "label": "最佳影像超時", + "description": "等待具有最高置信度分數的影像的時間。" + }, + "type": { + "label": "攝影機型別", + "description": "攝影機型別" + }, + "ui": { + "label": "攝影機介面", + "description": "此攝影機在頁面中的顯示順序和可見性。顯示順序僅影響預設儀表板。如需更精細的控制,請使用“攝影機組”。", + "order": { + "label": "UI 順序", + "description": "用於在頁面中排序攝影機的順序(只會影響預設儀表板和清單);數值越大則在越後面。" + }, + "dashboard": { + "label": "在 UI 中顯示", + "description": "切換此攝影機在 Frigate 頁面的所有位置是否可見。停用此項將需要手動編輯配置才能在頁面中再次檢視此攝影機。" + }, + "review": { + "label": "在Review中顯示", + "description": "切換此攝影機在Rewiew中是否可見 (包含Review 頁面、該攝影機的過濾器、移動檢視、歷史畫面)。" + } + }, + "webui_url": { + "label": "攝影機 URL", + "description": "從系統頁面直接存取攝影機管理後臺的 URL" + }, + "zones": { + "label": "區域", + "description": "區域允許您定義幀的特定區域,以便確定目標是否在特定區域內。", + "friendly_name": { + "label": "區域名稱", + "description": "區域的友好名稱,顯示在 Frigate UI 中。如果未設定,將使用區域名稱的格式化版本。" + }, + "enabled": { + "label": "開啟", + "description": "開啟或關閉此區域。停用的區域在執行時將被忽略。" + }, + "enabled_in_config": { + "label": "保持區域原始狀態的跟蹤。" + }, + "filters": { + "label": "區域過濾器", + "description": "應用於此區域內目標的過濾器。用於減少誤報或限制哪些目標被認為存在於區域內。", + "min_area": { + "label": "最小目標區域", + "description": "此目標型別所需的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "max_area": { + "label": "最大目標區域", + "description": "此目標型別允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "min_ratio": { + "label": "最小縱橫比", + "description": "邊界框所需的最小寬高比。" + }, + "max_ratio": { + "label": "最大縱橫比", + "description": "邊界框允許的最大寬高比。" + }, + "threshold": { + "label": "置信度閾值", + "description": "目標被視為真正陽性所需的平均偵測置信度閾值。" + }, + "min_score": { + "label": "最小置信度", + "description": "目標被計入所需的最小單幀偵測置信度。" + }, + "mask": { + "label": "過濾器遮罩", + "description": "定義此過濾器在幀內應用位置的多邊形座標。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "coordinates": { + "label": "座標", + "description": "定義區域區域的多邊形座標。可以是逗號分隔的字串或座標字串清單。座標應該是相對的(0-1)或絕對的(傳統)。" + }, + "distances": { + "label": "真實世界距離", + "description": "區域四邊形每邊的可選真實世界距離,用於速度或距離計算。如果設定,必須恰好有 4 個值。" + }, + "inertia": { + "label": "慣性幀數", + "description": "目標必須在區域內被連續偵測多少幀才能被認為存在。有助於過濾掉短暫偵測。" + }, + "loitering_time": { + "label": "徘徊秒數", + "description": "目標必須在區域內停留多少秒才能被視為徘徊。設定為 0 可停用徘徊偵測。" + }, + "speed_threshold": { + "label": "最小速度", + "description": "目標被認為存在於區域所需的最小速度(如果設定了距離,則為真實世界單位)。用於基於速度的區域觸發器。" + }, + "objects": { + "label": "觸發目標", + "description": "可以觸發此區域的目標型別清單(來自標籤對映)。可以是字串或字串清單。如果為空,則考慮所有目標。" + } + }, + "enabled_in_config": { + "label": "原始攝影機狀態", + "description": "保持攝影機的原始狀態跟蹤。" + } +} diff --git a/web/public/locales/zh-Hant/config/global.json b/web/public/locales/zh-Hant/config/global.json new file mode 100644 index 0000000..a71d8c6 --- /dev/null +++ b/web/public/locales/zh-Hant/config/global.json @@ -0,0 +1,1639 @@ +{ + "audio": { + "label": "音訊事件", + "enabled": { + "label": "啟用音訊偵測", + "description": "為所有攝影機啟用或停用音訊事件偵測;可按攝影機覆蓋。" + }, + "max_not_heard": { + "label": "結束逾時", + "description": "在未偵測到已設定音訊類型的情況下,經過多少秒後視為音訊事件結束。" + }, + "min_volume": { + "label": "最小音量", + "description": "執行音訊偵測所需的最小 RMS 音量門檻;數值越低,敏感度越高(例如:200 高、500 中、1000 低)。" + }, + "listen": { + "label": "監聽的音訊類型", + "description": "要偵測的音訊事件類型清單(例如:狗吠、火警、尖叫、說話、大叫)。" + }, + "description": "所有攝影機的基於音訊的事件偵測設定;可按攝影機覆蓋。", + "filters": { + "label": "音訊過濾器", + "description": "按音訊型別的過濾器設定,如用於減少誤報的置信度閾值。", + "threshold": { + "label": "最低音訊置信度", + "description": "音訊事件被計入的最低置信度閾值。" + } + }, + "enabled_in_config": { + "label": "原始音訊狀態", + "description": "指示原始靜態設定檔中是否開啟了音訊偵測。" + }, + "num_threads": { + "label": "偵測執行緒", + "description": "用於音訊偵測處理的執行緒數量。" + } + }, + "version": { + "label": "當前配置版本", + "description": "用於標識當前生效配置的版本號(數字或字串均可),幫助辨識配置遷移或格式是否發生變更。" + }, + "safe_mode": { + "label": "安全模式", + "description": "開啟後,Frigate 將以安全模式啟動,將會關閉部分功能,以便排查問題。" + }, + "environment_vars": { + "label": "環境變數", + "description": "用於在 Home Assistant OS 中為 Frigate 程序設定的環境變數。非 HAOS 使用者不能使用該配置項,而必須使用 Docker 的環境變數配置。" + }, + "logger": { + "label": "日誌", + "description": "控制預設日誌詳細程度,以及各元件的日誌級別覆蓋。", + "default": { + "label": "日誌等級", + "description": "預設全域性日誌詳細程度(除錯、資訊、警告、錯誤)。" + }, + "logs": { + "label": "單程序日誌級別", + "description": "按元件覆蓋日誌級別配置,用於提高或降低特定模組的日誌詳細程度。" + } + }, + "auth": { + "label": "身份驗證", + "description": "身份驗證和工作階段相關設定,包括 Cookie 和速率限制選項。", + "enabled": { + "label": "開啟身份驗證", + "description": "為 Frigate 頁面開啟原生身份驗證。" + }, + "reset_admin_password": { + "label": "重設管理員密碼", + "description": "開啟後,啟動時將重設管理員使用者密碼,並在日誌中列印新密碼。" + }, + "cookie_name": { + "label": "JWT Cookie 名稱", + "description": "用於儲存原生身份驗證 JWT 令牌的 Cookie 名稱。" + }, + "cookie_secure": { + "label": "安全 Cookie 標誌", + "description": "在身份驗證 Cookie 上設定安全標誌;使用 TLS 時應啟用此選項。" + }, + "session_length": { + "label": "工作階段時長", + "description": "基於 JWT 的工作階段持續時間(秒)。" + }, + "refresh_time": { + "label": "工作階段重新整理視窗", + "description": "當工作階段距離過期時間在此秒數範圍內時,將工作階段重新整理回完整時長。" + }, + "failed_login_rate_limit": { + "label": "登入失敗限制", + "description": "用於限制登入失敗嘗試次數的規則,以減少暴力破解攻擊。" + }, + "trusted_proxies": { + "label": "受信任的代理", + "description": "用於確定客戶端 IP 以進行速率限制的受信任代理 IP 清單。" + }, + "hash_iterations": { + "label": "雜湊迭代次數", + "description": "對使用者密碼進行雜湊處理時使用的 PBKDF2-SHA256 迭代次數。" + }, + "roles": { + "label": "權限組對映", + "description": "將權限組對映到攝影機清單。空清單表示該權限組可以存取所有攝影機。" + }, + "admin_first_time_login": { + "label": "管理員首次登入標誌", + "description": "啟用後,UI 可能會在登入頁面顯示幫助連結,告知使用者如何在管理員密碼重設後登入。 " + } + }, + "database": { + "label": "資料庫", + "description": "Frigate 用於儲存追蹤目標和錄影元資料的 SQLite 資料庫設定。", + "path": { + "label": "資料庫路徑", + "description": "Frigate SQLite 資料庫檔案的儲存路徑。" + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "整合的 go2rtc 轉發服務設定,用於即時監控流轉發和轉碼。" + }, + "mqtt": { + "label": "MQTT", + "description": "連線到 MQTT 代理併發布遙測資料、快照和事件詳情的設定。", + "enabled": { + "label": "開啟 MQTT", + "description": "啟用或停用 MQTT 整合,用於狀態、事件和快照。" + }, + "host": { + "label": "MQTT 主機", + "description": "MQTT 代理的主機名或 IP 地址。" + }, + "port": { + "label": "MQTT 埠", + "description": "MQTT 代理的埠(普通 MQTT 通常為 1883)。" + }, + "topic_prefix": { + "label": "主題字首", + "description": "所有 Frigate 主題的 MQTT 主題字首;如果執行多個例項,必須唯一。" + }, + "client_id": { + "label": "客戶端 ID", + "description": "連線到 MQTT 代理時使用的客戶端辨識符號;每個例項應該唯一。" + }, + "stats_interval": { + "label": "統計資訊間隔", + "description": "向 MQTT 釋出系統和攝影機統計資訊的時間間隔(秒)。" + }, + "user": { + "label": "MQTT 使用者名稱", + "description": "可選的 MQTT 使用者名稱;可以透過環境變數或金鑰提供。" + }, + "password": { + "label": "MQTT 密碼", + "description": "可選的 MQTT 密碼;可以透過環境變數或金鑰提供。" + }, + "tls_ca_certs": { + "label": "TLS CA 證書", + "description": "用於 TLS 連線到代理的 CA 證書路徑(用於自簽名證書)。" + }, + "tls_client_cert": { + "label": "客戶端證書", + "description": "TLS 雙向認證的客戶端證書路徑;使用客戶端證書時不要設定使用者名稱/密碼。" + }, + "tls_client_key": { + "label": "客戶端金鑰", + "description": "客戶端證書的私鑰路徑。" + }, + "tls_insecure": { + "label": "TLS 不安全連線", + "description": "透過跳過主機名驗證允許不安全的 TLS 連線(不推薦)。" + }, + "qos": { + "label": "MQTT QoS", + "description": "MQTT 釋出/訂閱的服務品質級別(0、1 或 2)。" + } + }, + "notifications": { + "label": "通知", + "description": "為所有攝影機啟用和控制通知的設定;可按攝影機覆蓋。", + "enabled": { + "label": "開啟通知", + "description": "為所有攝影機啟用或停用通知;可按攝影機覆蓋。" + }, + "email": { + "label": "通知郵箱", + "description": "用於推送通知或某些通知提供商要求的郵箱地址。" + }, + "cooldown": { + "label": "冷卻時間", + "description": "通知之間的冷卻時間(秒),以避免向收件人傳送垃圾資訊。" + }, + "enabled_in_config": { + "label": "原始通知狀態", + "description": "指示原始靜態配置中是否啟用了通知。" + } + }, + "networking": { + "label": "網路", + "description": "網路相關設定,如 Frigate 端點的 IPv6 啟用。", + "ipv6": { + "label": "IPv6 配置", + "description": "Frigate 網路服務的 IPv6 特定設定。", + "enabled": { + "label": "開啟 IPv6", + "description": "在適用的情況下為 Frigate 服務(API 和 UI)啟用 IPv6 支援。" + } + }, + "listen": { + "label": "監聽埠配置", + "description": "內部和外部監聽埠的配置。此選項適用於高階使用者。對於大多數用例,建議在 Docker compose 檔案的 ports 部分進行更改。", + "internal": { + "label": "內部埠", + "description": "Frigate 的內部監聽埠(預設 5000)。" + }, + "external": { + "label": "外部埠", + "description": "Frigate 的外部監聽埠(預設 8971)。" + } + } + }, + "proxy": { + "label": "代理", + "description": "用於將 Frigate 整合到傳遞已認證使用者頭的反向代理後面的設定。", + "header_map": { + "label": "請求頭對映", + "description": "將傳入的代理請求頭對映到 Frigate 使用者和權限組欄位,用於基於代理的身份驗證。", + "user": { + "label": "使用者請求頭", + "description": "包含上游代理提供的已認證使用者名稱的請求頭。" + }, + "role": { + "label": "權限組請求頭", + "description": "包含來自上游代理的已認證使用者權限組或使用者組的請求頭。" + }, + "role_map": { + "label": "權限組對映", + "description": "將上游組值對映到 Frigate 權限組(例如將管理員組對映到管理員權限組)。" + } + }, + "logout_url": { + "label": "登出 URL", + "description": "透過代理登出時重定向使用者的 URL。" + }, + "auth_secret": { + "label": "代理金鑰", + "description": "與 X-Proxy-Secret 請求頭進行比對的可選金鑰,用於驗證受信任的代理。" + }, + "default_role": { + "label": "預設權限組", + "description": "當沒有權限組對映適用時分配給代理認證使用者的預設權限組(admin 或 viewer)。" + }, + "separator": { + "label": "分隔符", + "description": "用於分割代理請求頭中多個值的字元。" + } + }, + "telemetry": { + "label": "遙測", + "description": "系統遙測和統計選項,包括 GPU 和網路頻寬監控。", + "network_interfaces": { + "label": "網路介面", + "description": "要監控頻寬統計資訊的網路介面名稱字首清單。" + }, + "stats": { + "label": "系統統計", + "description": "用於啟用/停用各種系統和 GPU 統計資訊收集的選項。", + "amd_gpu_stats": { + "label": "AMD GPU 統計", + "description": "如果存在 AMD GPU,則啟用 AMD GPU 統計資訊收集。" + }, + "intel_gpu_stats": { + "label": "Intel GPU 統計", + "description": "如果存在 Intel GPU,則啟用 Intel GPU 統計資訊收集。" + }, + "network_bandwidth": { + "label": "網路頻寬", + "description": "為攝影機 ffmpeg 程序和偵測器啟用按程序網路頻寬監控(需要權限)。" + }, + "intel_gpu_device": { + "label": "Intel GPU 裝置", + "description": "當系統存在多個 Intel 顯示卡時,用於將顯示卡執行資料繫結到指定裝置的 PCI 匯流排地址或 DRM 裝置路徑(示例:/dev/dri/card1)。" + } + }, + "version_check": { + "label": "版本檢查", + "description": "啟用出站檢查以偵測是否有更新版本的 Frigate 可用。" + } + }, + "tls": { + "label": "TLS", + "description": "Frigate Web 端點(埠 8971)的 TLS 設定。", + "enabled": { + "label": "開啟 TLS", + "description": "為 Frigate 的網頁頁面和 API 的埠開啟 TLS 加密。" + } + }, + "ui": { + "label": "使用者介面", + "description": "使用者介面偏好設定,如時區、時間/日期格式和單位。", + "timezone": { + "label": "時區", + "description": "UI 中顯示的可選時區(如果未設定,則預設為瀏覽器本地時間)。" + }, + "time_format": { + "label": "時間格式", + "description": "UI 中使用的時間格式(browser、12hour 或 24hour)。" + }, + "date_style": { + "label": "日期樣式", + "description": "UI 中使用的日期樣式(full、long、medium、short)。" + }, + "time_style": { + "label": "時間樣式", + "description": "UI 中使用的時間樣式(full、long、medium、short)。" + }, + "unit_system": { + "label": "單位系統", + "description": "UI 和 MQTT 中使用的顯示單位系統(公制或英制)。" + } + }, + "detectors": { + "label": "偵測器硬體", + "description": "目標偵測器(CPU、GPU、ONNX 後端)的配置以及任何偵測器特定的模型設定。", + "type": { + "label": "型別" + }, + "model": { + "label": "偵測器特定的模型配置", + "description": "偵測器特定的模型配置選項(路徑、輸入尺寸等)。", + "path": { + "label": "自訂目標偵測模型路徑", + "description": "自訂偵測模型檔案的路徑(或使用 plus:// 指定 Frigate+ 模型)。" + }, + "labelmap_path": { + "label": "自訂目標偵測器的標籤對映(labelmap)", + "description": "偵測器標籤對映檔案(labelmap)路徑,用於將數字類別對映為文字標籤。" + }, + "width": { + "label": "目標偵測模型輸入寬度", + "description": "模型輸入張量(input tensor)的寬度(以像素為單位)。" + }, + "height": { + "label": "目標偵測模型輸入高度", + "description": "模型輸入張量(input tensor)的高度(以像素為單位)。" + }, + "labelmap": { + "label": "標籤對映(labelmap)自訂", + "description": "合併到標準標籤對映表中的覆蓋 / 重對映規則。" + }, + "attributes_map": { + "label": "目標標籤到其屬性標籤的對映", + "description": "用於繫結元資料的目標標籤 → 屬性標籤對映關係(例如:'car'→ ['license_plate'] 為將車牌屬性繫結到車輛上)。" + }, + "input_tensor": { + "label": "模型輸入張量形狀", + "description": "模型期望的張量格式(Tensor format):'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型輸入像素顏色格式", + "description": "模型期望的像素顏色空間:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型輸入資料型別", + "description": "模型輸入張量的資料型別(例如 'float32')。" + }, + "model_type": { + "label": "目標偵測模型型別", + "description": "某些偵測器用於最佳化的偵測器模型架構型別(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "偵測器專用模型路徑", + "description": "所選偵測器需要時,需填寫其模型檔案的路徑。" + }, + "axengine": { + "label": "愛芯元智 NPU", + "description": "AXERA AX650N/AX8850N NPU 偵測器,透過 AXEngine 執行庫載入並執行編譯後的 .axmodel 模型檔案。" + }, + "cpu": { + "label": "CPU", + "description": "在主機 CPU 上執行 TensorFlow Lite 模型的 CPU TFLite 偵測器,無硬體加速。不推薦使用。", + "num_threads": { + "label": "偵測執行緒數", + "description": "用於基於 CPU 的推理的執行緒數。" + } + }, + "deepstack": { + "label": "DeepStack", + "description": "將影像傳送到遠端 DeepStack HTTP API 進行推理的 DeepStack/CodeProject.AI 偵測器。不推薦使用。", + "api_url": { + "label": "DeepStack API 網址", + "description": "DeepStack API 的 URL。" + }, + "api_timeout": { + "label": "DeepStack API 超時時間(秒)", + "description": "DeepStack API 請求允許的最長時間。" + }, + "api_key": { + "label": "DeepStack API 金鑰(如需要)", + "description": "用於認證 DeepStack 服務的可選 API 金鑰。" + } + }, + "degirum": { + "label": "DeGirum", + "description": "透過 DeGirum 雲或本地推理服務執行模型的 DeGirum 偵測器。", + "location": { + "label": "推理位置", + "description": "DeGirum 推理引擎的位置(例如 '@cloud'、'127.0.0.1')。" + }, + "zoo": { + "label": "模型庫", + "description": "DeGirum 模型庫的路徑或 URL。" + }, + "token": { + "label": "DeGirum 雲令牌", + "description": "用於 DeGirum 雲存取的令牌。" + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "使用 EdgeTPU 委託執行為 Coral EdgeTPU 編譯的 TensorFlow Lite 模型的 EdgeTPU 偵測器。", + "device": { + "label": "裝置型別", + "description": "用於 EdgeTPU 推理的裝置(例如 'usb'、'pci')。" + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "使用 HEF 模型和 HailoRT SDK 在 Hailo 硬體上進行推理的 Hailo-8/Hailo-8L 偵測器。", + "device": { + "label": "裝置型別", + "description": "用於 Hailo 推理的裝置(例如 'PCIe'、'M.2')。" + } + }, + "memryx": { + "label": "MemryX", + "description": "在 MemryX 加速器上執行編譯的 DFP 模型的 MemryX MX3 偵測器。", + "device": { + "label": "裝置路徑", + "description": "用於 MemryX 推理的裝置(例如 'PCIe')。" + } + }, + "onnx": { + "label": "ONNX", + "description": "執行 ONNX 模型的 ONNX 偵測器;當可用時將使用可用的加速後端(CUDA/ROCm/OpenVINO)。", + "device": { + "label": "裝置型別", + "description": "用於 ONNX 推理的裝置(例如 'AUTO'、'CPU'、'GPU')。" + } + }, + "openvino": { + "label": "OpenVINO", + "description": "適用於 AMD 和 Intel CPU、Intel GPU 和 Intel VPU 硬體的 OpenVINO 偵測器。", + "device": { + "label": "裝置型別", + "description": "用於 OpenVINO 推理的裝置(例如 'CPU'、'GPU'、'NPU')。" + } + }, + "rknn": { + "label": "RKNN", + "description": "用於 Rockchip NPU 的 RKNN 偵測器;在 Rockchip 硬體上執行編譯的 RKNN 模型。", + "num_cores": { + "label": "使用的 NPU 核心數。", + "description": "要使用的 NPU 核心數(0 表示自動)。" + } + }, + "synaptics": { + "label": "Synaptics", + "description": "使用 Synap SDK 在 Synaptics 硬體上執行 .synap 格式模型的 Synaptics NPU 偵測器。" + }, + "teflon_tfl": { + "label": "Teflon", + "description": "使用 Mesa Teflon 委託庫在支援的 GPU 上加速推理的 TFLite Teflon 委託偵測器。" + }, + "tensorrt": { + "label": "TensorRT", + "description": "使用序列化的 TensorRT 引擎進行加速推理的 Nvidia Jetson 裝置 TensorRT 偵測器。", + "device": { + "label": "GPU 裝置索引", + "description": "要使用的 GPU 裝置索引。" + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "透過 ZeroMQ IPC 端點將推理解除安裝到外部程序的 ZMQ IPC 偵測器。", + "endpoint": { + "label": "ZMQ IPC 端點", + "description": "要連線的 ZMQ 端點。" + }, + "request_timeout_ms": { + "label": "ZMQ 請求超時(毫秒)", + "description": "ZMQ 請求的超時時間(毫秒)。" + }, + "linger_ms": { + "label": "ZMQ 套接字逗留時間(毫秒)", + "description": "套接字逗留時間(毫秒)。" + } + } + }, + "model": { + "label": "偵測模型", + "description": "用於配置自訂目標偵測模型及其輸入形狀的設定。", + "path": { + "label": "自訂目標偵測模型路徑", + "description": "自訂偵測模型檔案的路徑(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自訂目標偵測器的標籤對映", + "description": "將數字類別對映到偵測器字串標籤的標籤對映檔案路徑。" + }, + "width": { + "label": "目標偵測模型輸入寬度", + "description": "模型輸入張量的寬度(像素)。" + }, + "height": { + "label": "目標偵測模型輸入高度", + "description": "模型輸入張量的高度(像素)。" + }, + "labelmap": { + "label": "標籤對映自訂", + "description": "要合併到標準標籤對映中的覆蓋或重對映條目。" + }, + "attributes_map": { + "label": "目標標籤到屬性標籤的對映", + "description": "從目標標籤到屬性標籤的對映,用於附加元資料(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型輸入張量形狀", + "description": "模型期望的張量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型輸入像素顏色格式", + "description": "模型期望的像素色彩空間:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型輸入資料型別", + "description": "模型輸入張量的資料型別(例如 'float32')。" + }, + "model_type": { + "label": "目標偵測模型型別", + "description": "某些偵測器用於最佳化的偵測器模型架構型別(ssd、yolox、yolonas)。" + } + }, + "genai": { + "label": "生成式 AI 配置", + "description": "用於生成目標描述和審閱摘要的整合生成式 AI 提供商設定。", + "api_key": { + "label": "API 金鑰", + "description": "某些提供商要求的 API 金鑰(也可以透過環境變數設定)。" + }, + "base_url": { + "label": "基礎 URL", + "description": "自託管或相容提供商的基礎 URL(例如 Ollama 例項)。" + }, + "model": { + "label": "模型", + "description": "用於生成描述或摘要的提供商模型。" + }, + "provider": { + "label": "提供商", + "description": "要使用的生成式 AI 提供商(例如:ollama、gemini、openai 等。國產大模型廠商可使用 openai 介面)。" + }, + "roles": { + "label": "功能", + "description": "生成式 AI 功能(對話、描述、嵌入);每個功能單獨一個提供商。" + }, + "provider_options": { + "label": "提供商選項", + "description": "要傳遞給生成式 AI 客戶端的、與服務提供商相關的額外配置項。" + }, + "runtime_options": { + "label": "執行時選項", + "description": "每次推理呼叫時傳遞給提供商的執行時選項。" + } + }, + "birdseye": { + "label": "鳥瞰圖", + "description": "將多路攝影機畫面合併為統一佈局的鳥瞰合成檢視設定。", + "enabled": { + "label": "開啟鳥瞰圖", + "description": "開啟或關閉鳥瞰圖功能。" + }, + "mode": { + "label": "追蹤模式", + "description": "在鳥瞰檢視中包含攝影機的模式:'objects'(目標)、'motion'(動作)或 'continuous'(持續)。" + }, + "restream": { + "label": "轉發 RTSP", + "description": "將鳥瞰圖輸出作為 RTSP 流重新轉發;啟用此功能將使鳥瞰圖持續執行。" + }, + "width": { + "label": "寬度", + "description": "合成的鳥瞰幀的輸出寬度(像素)。" + }, + "height": { + "label": "高度", + "description": "合成的鳥瞰幀的輸出高度(像素)。" + }, + "quality": { + "label": "編碼品質", + "description": "鳥瞰圖 mpeg1 流的編碼品質(1 最高品質,31 最低)。" + }, + "inactivity_threshold": { + "label": "非活動閾值", + "description": "攝影機停止在鳥瞰圖中顯示的非活動秒數。" + }, + "layout": { + "label": "佈局", + "description": "鳥瞰圖合成的佈局選項。", + "scaling_factor": { + "label": "縮放因子", + "description": "佈局計算器使用的縮放因子(範圍 1.0 到 5.0)。" + }, + "max_cameras": { + "label": "最大攝影機數", + "description": "鳥瞰圖中同時顯示的最大攝影機數量;顯示最近的攝影機。" + } + }, + "idle_heartbeat_fps": { + "label": "空閒心跳 FPS", + "description": "空閒時重新發送最後一個合成鳥瞰幀的每秒幀數;設為 0 則停用。" + }, + "order": { + "label": "排序位置", + "description": "用於控制攝影機在鳥瞰檢視佈局中排序位置的數值。" + } + }, + "detect": { + "label": "目標偵測", + "description": "用於執行目標偵測、初始化追蹤器的偵測模組設定。", + "enabled": { + "label": "開啟目標偵測", + "description": "為所有攝影機啟用或停用目標偵測,可按攝影機覆蓋。" + }, + "height": { + "label": "偵測畫面高度", + "description": "用於配置偵測流的畫面高度(像素);留空則使用原始影片流解析度。" + }, + "width": { + "label": "偵測畫面寬度", + "description": "用於配置偵測流的畫面寬度(像素);留空則使用原始影片流解析度。" + }, + "fps": { + "label": "偵測幀率", + "description": "偵測時希望使用的幀率;數值越低,CPU 佔用越小(推薦值為 5,僅在追蹤極高速運動的目標時才設定更高數值,最高不建議超過 10)。" + }, + "min_initialized": { + "label": "最小初始化幀數", + "description": "建立追蹤目標前,需要連續偵測到目標的次數。數值越大,錯誤觸發的追蹤越少。預設值為幀率除以 2。" + }, + "max_disappeared": { + "label": "最大消失幀數", + "description": "追蹤目標在連續多少幀未被偵測到時,將被判定為已消失。" + }, + "stationary": { + "label": "靜止目標配置", + "description": "用於偵測和管理長時間靜止目標的相關設定。", + "interval": { + "label": "靜止間隔", + "description": "設定每隔多少幀執行一次偵測,用於確認目標是否處於靜止狀態。" + }, + "threshold": { + "label": "靜止閾值", + "description": "目標需要連續多少幀位置不變,才會被標記為靜止狀態。" + }, + "max_frames": { + "label": "最大幀數", + "description": "限制靜止目標最大追蹤時長(以幀數為單位),超過將會停止追蹤。", + "default": { + "label": "預設最大幀數", + "description": "停止追蹤前,用於追蹤靜止目標的預設最大幀數。" + }, + "objects": { + "label": "目標最大幀數", + "description": "可對不同型別目標分別設定靜止追蹤的最大幀數(覆蓋全域性設定)。" + } + }, + "classifier": { + "label": "開啟視覺分類器", + "description": "使用視覺分類器,即使偵測框有輕微抖動,也能準確判斷物體是否為靜止。" + } + }, + "annotation_offset": { + "label": "標記偏移量", + "description": "偵測標記的時間偏移量(毫秒),用於讓時間軸上的偵測框與錄影畫面更精準對齊;可設定為正數或負數。" + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg 編解碼相關設定,包含可執行檔案路徑、命令列引數、硬體加速選項,以及按不同功能劃分的輸出引數。", + "path": { + "label": "FFmpeg 路徑", + "description": "要使用的 FFmpeg 可執行檔案路徑,或版本別名(如 \"5.0\" 或 \"7.0\")。" + }, + "global_args": { + "label": "FFmpeg 全域性引數", + "description": "傳遞給 FFmpeg 程序的全域性引數。" + }, + "hwaccel_args": { + "label": "硬體加速引數", + "description": "用於 FFmpeg 的硬體加速引數。建議使用對應硬體廠商的預設配置。" + }, + "input_args": { + "label": "輸入引數", + "description": "應用於 FFmpeg 輸入影片流的輸入引數。" + }, + "output_args": { + "label": "輸出引數", + "description": "用於不同 FFmpeg 功能(如偵測、錄製)的預設輸出引數。", + "detect": { + "label": "偵測輸出引數", + "description": "偵測功能影片流的預設輸出引數。" + }, + "record": { + "label": "錄製輸出引數", + "description": "錄製功能影片流的預設輸出引數。" + } + }, + "retry_interval": { + "label": "FFmpeg 重試時間", + "description": "攝影機影片流異常斷開後,重新連線前的等待時間。預設為 10 秒。" + }, + "apple_compatibility": { + "label": "Apple 相容性", + "description": "錄製 H.265 影片時啟用 HEVC 標記,以提升對 Apple 裝置播放的相容性。" + }, + "gpu": { + "label": "GPU 索引", + "description": "在啟用硬體加速時,預設使用的 GPU 索引。" + }, + "inputs": { + "label": "攝影機輸入影片流", + "description": "該攝影機的所有輸入流配置清單(包含路徑和功能)。", + "path": { + "label": "輸入路徑", + "description": "攝影機輸入影片流的地址或路徑。" + }, + "roles": { + "label": "輸入流功能", + "description": "定義該影片流的功能。" + }, + "global_args": { + "label": "FFmpeg 全域性引數", + "description": "該輸入影片流使用的 FFmpeg 全域性通用引數。" + }, + "hwaccel_args": { + "label": "硬體加速引數", + "description": "該輸入影片流的硬體加速引數。" + }, + "input_args": { + "label": "輸入引數", + "description": "該影片流特定的輸入引數。" + } + } + }, + "live": { + "label": "即時監控觀看", + "description": "用於控制 jsmpeg 即時串流解析度與畫質的設定。此設定不影響使用 go2rtc 進行即時預覽的重新串流攝影機。", + "streams": { + "label": "即時監控流名稱", + "description": "配置的流名稱到用於即時監控播放的 restream/go2rtc 名稱的對映。" + }, + "height": { + "label": "即時監控高度", + "description": "在網頁頁面中渲染 jsmpeg 即時監控流的高度(像素);必須小於等於偵測流高度。" + }, + "quality": { + "label": "即時監控品質", + "description": "jsmpeg 流的編碼品質(1 最高,31 最低)。" + } + }, + "motion": { + "label": "畫面變動偵測", + "description": "應用於攝影機的預設動作偵測設定,除非按攝影機覆蓋。", + "enabled": { + "label": "開啟畫面變動偵測", + "description": "為所有攝影機啟用或停用動作偵測;可按攝影機覆蓋。" + }, + "threshold": { + "label": "畫面變動閾值", + "description": "畫面變動偵測器使用的像素差異閾值;數值越高靈敏度越低(範圍 1-255)。" + }, + "lightning_threshold": { + "label": "閃電閾值", + "description": "用於偵測和忽略短暫閃電閃爍的閾值(數值越低越敏感,範圍 0.3 到 1.0)。這不會完全阻止畫面變動偵測;只是當超過閾值時偵測器會停止分析額外的幀。在此類事件期間仍會建立基於畫面變動的錄影。" + }, + "skip_motion_threshold": { + "label": "跳過畫面變動閾值", + "description": "如果單幀中畫面變化超過此比例,偵測器將判定為無畫面變動並立即重新校準。這可以節省 CPU 並減少閃電、風暴等情況下的誤報,但也可能會錯過真正的事件,如 PTZ 攝影機自動追蹤目標。你需要權衡取捨:是否犧牲少量錄製片段,換取更少無效影片與更低的誤檢。保持為空即可關閉該功能。" + }, + "improve_contrast": { + "label": "改善對比度", + "description": "在畫面變動分析之前對幀應用對比度改善以幫助偵測。" + }, + "contour_area": { + "label": "輪廓區域", + "description": "畫面變動輪廓被計入所需的最小輪廓區域(像素)。" + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "用於畫面變動計算的幀差異中使用的 alpha 混合因子。" + }, + "frame_alpha": { + "label": "畫面 alpha 通道", + "description": "畫面變動預處理時混合畫面所使用的 alpha 值。" + }, + "frame_height": { + "label": "畫面高度", + "description": "計算畫面變動時縮放畫面的高度(像素)。" + }, + "mask": { + "label": "遮罩座標", + "description": "定義用於包含/排除區域的畫面變動遮罩多邊形的有序 x,y 座標。" + }, + "mqtt_off_delay": { + "label": "MQTT 關閉延遲", + "description": "在釋出 MQTT 'off' 狀態之前,最後一次畫面變動後等待的秒數。" + }, + "enabled_in_config": { + "label": "原始畫面變動狀態", + "description": "指示原始靜態配置中是否啟用了畫面變動偵測。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "objects": { + "label": "目標", + "description": "目標追蹤預設設定,包括要追蹤的標籤和按目標的過濾器。", + "track": { + "label": "要追蹤的目標", + "description": "所有攝影機要追蹤的目標標籤清單;可按攝影機覆蓋。" + }, + "filters": { + "label": "目標過濾器", + "description": "應用於偵測到的目標以減少誤報的過濾器(區域、比例、置信度)。", + "min_area": { + "label": "最小目標區域", + "description": "此目標型別所需的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "max_area": { + "label": "最大目標區域", + "description": "此目標型別允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "min_ratio": { + "label": "最小縱橫比", + "description": "邊界框所需的最小寬高比。" + }, + "max_ratio": { + "label": "最大縱橫比", + "description": "邊界框允許的最大寬高比。" + }, + "threshold": { + "label": "置信度閾值", + "description": "目標被視為真正陽性所需的平均偵測置信度閾值。" + }, + "min_score": { + "label": "最小置信度", + "description": "目標被計入所需的最小單幀偵測置信度。" + }, + "mask": { + "label": "過濾器遮罩", + "description": "定義此過濾器在幀內應用位置的多邊形座標。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "mask": { + "label": "目標遮罩", + "description": "用於防止在指定區域進行目標偵測的遮罩多邊形。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "genai": { + "label": "生成式 AI 目標配置", + "description": "用於傳送畫面給生成式 AI 進行生成和描述追蹤目標的選項。", + "enabled": { + "label": "開啟生成式 AI", + "description": "預設開啟生成式 AI 生成追蹤目標的描述。" + }, + "use_snapshot": { + "label": "使用快照", + "description": "使用目標快照而不是縮圖給生成式 AI 進行描述生成。" + }, + "prompt": { + "label": "字幕提示", + "description": "使用生成式 AI 生成描述時使用的預設提示模板。" + }, + "object_prompts": { + "label": "目標提示", + "description": "按目標設定提示詞,讓生成式 AI 對不同標籤的輸出進行定製。" + }, + "objects": { + "label": "生成式 AI 目標", + "description": "預設傳送給生成式 AI 的目標標籤清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入這些區域,才會觸發生成式 AI 描述生成。" + }, + "debug_save_thumbnails": { + "label": "儲存縮圖", + "description": "儲存傳送給生成式 AI 的縮圖用於除錯和審閱。" + }, + "send_triggers": { + "label": "生成式 AI 觸發器", + "description": "定義畫面幀應在何時傳送給生成式 AI(如偵測結束時、更新後等)。", + "tracked_object_end": { + "label": "結束時傳送", + "description": "目標追蹤結束時向生成式 AI 傳送請求。" + }, + "after_significant_updates": { + "label": "生成式 AI 提前觸發", + "description": "在追蹤目標發生指定次數的重要變化後,向生成式 AI 傳送請求。" + } + }, + "enabled_in_config": { + "label": "原配置生成式 AI 狀態", + "description": "表示在原始靜態配置中是否已啟用生成式 AI。" + } + }, + "filters_attribute": { + "label": "屬性篩選器", + "description": "篩選器用來篩選屬性,以此降低誤報(區域、比例、置信度)。", + "min_area": { + "label": "最小屬性區域", + "description": "此屬性允許的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "max_area": { + "label": "最大屬性區域", + "description": "此屬性允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "min_ratio": { + "label": "最小長寬比", + "description": "邊界框所需的最小長寬比。" + }, + "max_ratio": { + "label": "最大長寬比", + "description": "邊界框允許的最大長寬比。" + }, + "threshold": { + "label": "置信度門檻", + "description": "判定屬性有效所需的平均偵測置信度門檻。" + }, + "min_score": { + "label": "最小置信度", + "description": "此屬性與其父物件關聯所需的最小單幀畫面的偵測置信度。" + }, + "mask": { + "label": "篩選遮罩", + "description": "定義此篩選器在畫面內應用位置的多邊形座標。" + }, + "raw_mask": { + "label": "原始遮罩" + } + } + }, + "record": { + "label": "錄影", + "description": "應用於攝影機的錄影和保留設定,除非按攝影機覆蓋。", + "enabled": { + "label": "開啟錄影", + "description": "為所有攝影機啟用或停用錄影;可按攝影機覆蓋。" + }, + "expire_interval": { + "label": "錄影清理間隔", + "description": "清理過期錄影片段的間隔分鐘數。" + }, + "continuous": { + "label": "持續保留", + "description": "無論是否有追蹤目標或動作,保留錄影的天數。如果只想保留警報和偵測的錄影,請設定為 0。", + "days": { + "label": "保留天數", + "description": "保留錄影的天數。" + } + }, + "motion": { + "label": "動作保留", + "description": "無論是否有追蹤目標,由動作觸發的錄影保留天數。如果只想保留警報和偵測的錄影,請設定為 0。", + "days": { + "label": "保留天數", + "description": "保留錄影的天數。" + } + }, + "detections": { + "label": "偵測保留", + "description": "偵測事件的錄影保留設定,包括前後捕獲時長。", + "pre_capture": { + "label": "前捕獲秒數", + "description": "偵測事件之前包含在錄影中的秒數。" + }, + "post_capture": { + "label": "後捕獲秒數", + "description": "偵測事件之後包含在錄影中的秒數。" + }, + "retain": { + "label": "事件保留", + "description": "偵測事件錄影的保留設定。", + "days": { + "label": "保留天數", + "description": "保留偵測事件錄影的天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + } + } + }, + "alerts": { + "label": "警報保留", + "description": "警報事件的錄影保留設定,包括前後捕獲時長。", + "pre_capture": { + "label": "前捕獲秒數", + "description": "偵測事件之前包含在錄影中的秒數。" + }, + "post_capture": { + "label": "後捕獲秒數", + "description": "偵測事件之後包含在錄影中的秒數。" + }, + "retain": { + "label": "事件保留", + "description": "偵測事件錄影的保留設定。", + "days": { + "label": "保留天數", + "description": "保留偵測事件錄影的天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + } + } + }, + "export": { + "label": "匯出配置", + "description": "匯出錄影時使用的設定,如延時攝影和硬體加速。", + "hwaccel_args": { + "label": "匯出硬體加速引數", + "description": "用於匯出/轉碼操作的硬體加速引數。" + }, + "max_concurrent": { + "label": "最大併發匯出數", + "description": "同時可處理的最大匯出任務數量。" + } + }, + "preview": { + "label": "預覽配置", + "description": "控制介面中顯示的錄影預覽品質的設定。", + "quality": { + "label": "預覽品質", + "description": "預覽品質級別(very_low、low、medium、high、very_high)。" + } + }, + "enabled_in_config": { + "label": "原始錄影狀態", + "description": "指示原始靜態配置中是否啟用了錄影。" + } + }, + "review": { + "label": "審閱", + "description": "控制 UI 和儲存使用的警報、偵測和 GenAI 審閱摘要的設定。", + "alerts": { + "label": "警報配置", + "description": "哪些追蹤目標生成警報以及如何保留警報的設定。", + "enabled": { + "label": "開啟警報", + "description": "為所有攝影機啟用或停用警報生成;可按攝影機覆蓋。" + }, + "labels": { + "label": "警報標籤", + "description": "符合警報條件的目標標籤清單(例如:car、person)。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能被視為警報的區域;留空則允許任何區域。" + }, + "enabled_in_config": { + "label": "原始警報狀態", + "description": "追蹤原始靜態配置中是否啟用了警報。" + }, + "cutoff_time": { + "label": "警報截止時間", + "description": "在沒有引起警報的活動後等待多少秒後截止警報。" + } + }, + "detections": { + "label": "偵測配置", + "description": "用於設定哪些追蹤目標會生成偵測記錄(非警報類),以及偵測記錄的保留方式。", + "enabled": { + "label": "開啟偵測", + "description": "為所有攝影機啟用或停用偵測事件;可按攝影機覆蓋。" + }, + "labels": { + "label": "偵測標籤", + "description": "符合偵測事件條件的目標標籤清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能被視為偵測的區域;留空則允許任何區域。" + }, + "cutoff_time": { + "label": "偵測截止時間", + "description": "在沒有引起偵測的活動後等待多少秒後截止偵測。" + }, + "enabled_in_config": { + "label": "原始偵測狀態", + "description": "追蹤原始靜態配置中是否啟用了偵測。" + } + }, + "genai": { + "label": "生成式 AI 配置", + "description": "控制使用生成式 AI 為審閱項生成描述和摘要。", + "enabled": { + "label": "開啟生成式 AI 描述", + "description": "為審閱項開啟或關閉使用生成式 AI 生成描述和摘要。" + }, + "alerts": { + "label": "為警報開啟生成式 AI", + "description": "使用生成式 AI 為警報項生成描述。" + }, + "detections": { + "label": "為偵測開啟生成式 AI", + "description": "使用生成式 AI 為偵測項生成描述。" + }, + "image_source": { + "label": "審閱影像來源", + "description": "傳送給生成式 AI 的畫面來源('preview' 或 'recordings');'recordings' 使用更高品質的畫面幀,但會消耗更多的 token。" + }, + "additional_concerns": { + "label": "額外關注事項", + "description": "生成式 AI 在分析此攝影機的監控行為時,需要額外注意的事項或說明清單。" + }, + "debug_save_thumbnails": { + "label": "儲存縮圖", + "description": "儲存傳送給生成式 AI 提供商的縮圖用於除錯和審閱。" + }, + "enabled_in_config": { + "label": "原配置生成式 AI 狀態", + "description": "記錄在靜態配置中最初是否已啟用生成式 AI 審閱功能。" + }, + "preferred_language": { + "label": "首選語言", + "description": "向生成式 AI 提供商請求生成回應的首選語言。" + }, + "activity_context_prompt": { + "label": "活動上下文提示", + "description": "自訂提示詞,用於說明可疑行為與非可疑行為的界定,為生成式 AI 生成摘要提供上下文依據。" + } + } + }, + "snapshots": { + "label": "快照", + "description": "所有攝影機的追蹤目標 API 快照設定;可攝影機單獨配置覆蓋全域性配置。", + "enabled": { + "label": "開啟快照", + "description": "為所有攝影機啟用或停用儲存快照;可按攝影機覆蓋。" + }, + "timestamp": { + "label": "時間戳疊加", + "description": "在 API 生成的快照上疊加時間戳。" + }, + "bounding_box": { + "label": "邊界框疊加", + "description": "在 API 生成的快照上繪製追蹤目標的邊界框。" + }, + "crop": { + "label": "裁剪快照", + "description": "在 API 生成的快照裁剪到偵測到的目標邊界框。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能儲存快照的區域。" + }, + "height": { + "label": "快照高度", + "description": "將 API 生成的快照調整到的目標高度(像素);留空則保持原始大小。" + }, + "retain": { + "label": "快照保留", + "description": "快照的保留設定,包括預設天數和按目標覆蓋。", + "default": { + "label": "預設保留", + "description": "保留快照的預設天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + }, + "objects": { + "label": "目標保留", + "description": "按目標覆蓋的快照保留天數。" + } + }, + "quality": { + "label": "快照品質", + "description": "儲存快照的編碼品質(0-100)。" + } + }, + "timestamp_style": { + "label": "時間戳樣式", + "description": "應用於除錯檢視和快照的幀內時間戳樣式選項。", + "position": { + "label": "時間戳位置", + "description": "時間戳在影像上的位置(tl/tr/bl/br)。" + }, + "format": { + "label": "時間戳格式", + "description": "用於時間戳的日期時間格式字串(Python 日期時間格式程式碼)。" + }, + "color": { + "label": "時間戳顏色", + "description": "時間戳文字的 RGB 顏色值(所有值 0-255)。", + "red": { + "label": "紅色", + "description": "時間戳顏色的紅色分量(0-255)。" + }, + "green": { + "label": "綠色", + "description": "時間戳顏色的綠色分量(0-255)。" + }, + "blue": { + "label": "藍色", + "description": "時間戳顏色的藍色分量(0-255)。" + } + }, + "thickness": { + "label": "時間戳粗細", + "description": "時間戳文字的線條粗細。" + }, + "effect": { + "label": "時間戳效果", + "description": "時間戳文字的視覺效果(none、solid、shadow)。" + } + }, + "audio_transcription": { + "label": "音訊轉錄", + "description": "用於事件和即時字幕的即時和語音音訊轉錄設定。", + "enabled": { + "label": "啟用語音轉文字", + "description": "為所有攝影機啟用或停用自動音訊轉錄;可按攝影機覆蓋。" + }, + "language": { + "label": "轉錄語言", + "description": "用於轉錄/翻譯的語言程式碼(例如 'en' 表示英語)。請參閱 https://whisper-api.com/docs/languages/ 瞭解支援的語言程式碼。" + }, + "device": { + "label": "轉錄裝置", + "description": "執行轉錄模型的裝置金鑰(CPU/GPU)。目前僅支援 NVIDIA CUDA GPU 進行轉錄。" + }, + "model_size": { + "label": "模型大小", + "description": "用於離線音訊事件轉錄的模型大小。" + }, + "live_enabled": { + "label": "即時監控轉寫", + "description": "在接收到音訊時開啟即時監控持續轉寫。" + } + }, + "classification": { + "label": "目標分類", + "description": "用於最佳化目標標籤或狀態分類的分類模型設定。", + "bird": { + "label": "鳥類分類配置", + "description": "鳥類分類模型特定的設定。", + "enabled": { + "label": "鳥類分類", + "description": "啟用或停用鳥類分類。" + }, + "threshold": { + "label": "最小分數", + "description": "接受鳥類分類所需的最小分類分數。" + } + }, + "custom": { + "label": "自訂分類模型", + "description": "用於目標或狀態偵測的自訂分類模型配置。", + "enabled": { + "label": "開啟模型", + "description": "啟用或停用自訂分類模型。" + }, + "name": { + "label": "模型名稱", + "description": "要使用的自訂分類模型的辨識符號。" + }, + "threshold": { + "label": "分數閾值", + "description": "用於更改分類狀態的分數閾值。" + }, + "save_attempts": { + "label": "儲存嘗試", + "description": "為最近分類 UI 儲存多少次分類嘗試。" + }, + "object_config": { + "objects": { + "label": "分類目標", + "description": "要執行目標分類的目標型別清單。" + }, + "classification_type": { + "label": "分類型別", + "description": "應用的分類型別:'sub_label'(新增 sub_label)或其他支援的型別。" + } + }, + "state_config": { + "cameras": { + "label": "分類攝影機", + "description": "用於執行狀態分類的按攝影機裁剪和設定。", + "crop": { + "label": "分類裁剪", + "description": "用於在此攝影機上執行分類的裁剪座標。" + } + }, + "motion": { + "label": "動作時執行", + "description": "啟用後,當在指定裁剪區域內偵測到動作時執行分類。" + }, + "interval": { + "label": "分類間隔", + "description": "狀態分類的定期分類執行間隔(秒)。" + } + } + } + }, + "semantic_search": { + "label": "語意搜尋", + "description": "用於構建和查詢目標嵌入以查詢相似項的語意搜尋設定。", + "enabled": { + "label": "開啟語意搜尋", + "description": "啟用或停用語意搜尋功能。" + }, + "reindex": { + "label": "啟動時重建索引", + "description": "觸發將歷史追蹤目標完全重新索引到嵌入資料庫。" + }, + "model": { + "label": "語意搜尋模型或生成式 AI 服務名稱", + "description": "用於語意搜尋的嵌入模型(例如 'jinav1'),或具有嵌入功能(embeddings)的生成式 AI 服務名稱。" + }, + "model_size": { + "label": "模型大小", + "description": "選擇模型大小;'small' 在 CPU 上執行,'large' 通常需要 GPU。" + }, + "device": { + "label": "裝置", + "description": "這是一個覆蓋選項,用於指定特定裝置。請參閱 https://onnxruntime.ai/docs/execution-providers/ 瞭解更多資訊" + }, + "triggers": { + "label": "觸發器", + "description": "攝影機特定語意搜尋觸發器的操作和匹配條件。", + "friendly_name": { + "label": "友好名稱", + "description": "在 UI 中為此觸發器顯示的可選友好名稱。" + }, + "enabled": { + "label": "開啟此觸發器", + "description": "啟用或停用此語意搜尋觸發器。" + }, + "type": { + "label": "觸發器型別", + "description": "觸發器型別:'thumbnail'(與影像匹配)或 'description'(與文字匹配)。" + }, + "data": { + "label": "觸發器內容", + "description": "要與追蹤目標匹配的文字短語或縮圖 ID。" + }, + "threshold": { + "label": "觸發器閾值", + "description": "啟用此觸發器所需的最小相似度分數(0-1)。" + }, + "actions": { + "label": "觸發器操作", + "description": "觸發器匹配時要執行的操作清單(通知、sub_label、屬性)。" + } + } + }, + "face_recognition": { + "label": "人臉辨識", + "description": "所有攝影機的人臉偵測和辨識設定;可按攝影機覆蓋。", + "enabled": { + "label": "開啟人臉辨識", + "description": "為所有攝影機啟用或停用人臉辨識;可按攝影機覆蓋。" + }, + "model_size": { + "label": "模型大小", + "description": "用於人臉嵌入的模型大小(small/large);較大的可能需要 GPU。" + }, + "unknown_score": { + "label": "未知分數閾值", + "description": "低於此距離閾值的人臉被視為潛在匹配(數值越高越嚴格)。" + }, + "detection_threshold": { + "label": "偵測閾值", + "description": "將人臉偵測視為有效所需的最小偵測置信度。" + }, + "recognition_threshold": { + "label": "辨識閾值", + "description": "將兩張人臉視為匹配的人臉嵌入距離閾值。" + }, + "min_area": { + "label": "最小人臉區域", + "description": "需要嘗試進行人臉辨識的人臉偵測框最小大小(像素)。" + }, + "min_faces": { + "label": "最小人臉數", + "description": "在將辨識的子標籤應用於人員之前所需的最小人臉辨識次數。" + }, + "save_attempts": { + "label": "儲存嘗試", + "description": "為最近辨識 UI 保留的人臉辨識嘗試次數。" + }, + "blur_confidence_filter": { + "label": "模糊置信度過濾器", + "description": "根據影像模糊程度調整置信度分數,以減少低品質人臉的誤報。" + }, + "device": { + "label": "裝置", + "description": "這是一個覆蓋選項,用於指定特定裝置。請參閱 https://onnxruntime.ai/docs/execution-providers/ 瞭解更多資訊" + } + }, + "lpr": { + "label": "車牌辨識", + "description": "車牌辨識設定,包括偵測閾值、格式化和已知車牌。", + "enabled": { + "label": "開啟車牌辨識", + "description": "為所有攝影機啟用或停用車牌辨識;可按攝影機覆蓋。" + }, + "model_size": { + "label": "模型大小", + "description": "用於文字偵測/辨識的模型大小,大多數使用者應使用 'small',只有'small'模型支援中文。" + }, + "detection_threshold": { + "label": "偵測閾值", + "description": "開始對疑似車牌執行 OCR 的偵測置信度閾值。" + }, + "min_area": { + "label": "最小車牌區域", + "description": "嘗試辨識所需的最小車牌區域(像素)。" + }, + "recognition_threshold": { + "label": "辨識閾值", + "description": "辨識的車牌文字作為子標籤附加所需的置信度閾值。" + }, + "min_plate_length": { + "label": "最小車牌長度", + "description": "辨識的車牌被視為有效所需的最小字元數。" + }, + "format": { + "label": "車牌格式正則", + "description": "用於驗證辨識的車牌字串是否符合預期格式的可選正則表示式。" + }, + "match_distance": { + "label": "匹配距離", + "description": "將偵測到的車牌與已知車牌比較時允許的字元不匹配數。" + }, + "known_plates": { + "label": "已知車牌", + "description": "要特別追蹤或報警的車牌或正則表示式清單。" + }, + "enhancement": { + "label": "增強級別", + "description": "在 OCR 之前應用於車牌裁剪的增強級別(0-10);較高的值可能不總是改善結果,5 以上的級別可能僅適用於夜間車牌,應謹慎使用。" + }, + "debug_save_plates": { + "label": "儲存除錯車牌", + "description": "儲存車牌裁剪影像用於除錯 LPR 效能。" + }, + "device": { + "label": "裝置", + "description": "這是一個覆蓋選項,用於指定特定裝置。請參閱 https://onnxruntime.ai/docs/execution-providers/ 瞭解更多資訊" + }, + "replace_rules": { + "label": "替換規則", + "description": "用於在匹配之前規範化偵測到的車牌字串的正則替換規則。", + "pattern": { + "label": "正則模式" + }, + "replacement": { + "label": "替換字串" + } + }, + "expire_time": { + "label": "過期秒數", + "description": "未見到的車牌從追蹤器中過期的時間(秒)(僅適用於專用 LPR 攝影機)。" + } + }, + "camera_groups": { + "label": "攝影機分組", + "description": "用於在頁面中組織攝影機的命名攝影機分組配置。", + "cameras": { + "label": "攝影機清單", + "description": "此分組中包含的攝影機名稱陣列。" + }, + "icon": { + "label": "分組圖示", + "description": "在頁面中代表攝影機分組的圖示。" + }, + "order": { + "label": "排序順序", + "description": "用於在頁面中對攝影機分組進行排序的數字順序;數值越大越靠後。" + } + }, + "profiles": { + "label": "設定檔", + "description": "帶有別名的命名設定檔定義。攝影機設定檔必須引用此處定義的名稱。", + "friendly_name": { + "label": "別名", + "description": "在介面中顯示的此設定檔名稱,可以使用中文。" + } + }, + "active_profile": { + "label": "啟用設定檔", + "description": "當前啟用的設定檔名稱。僅在執行時使用,不會寫入 YAML 設定檔中。" + }, + "camera_mqtt": { + "label": "MQTT", + "description": "MQTT 影像釋出設定。", + "enabled": { + "label": "傳送影像", + "description": "為此攝影機啟用將目標快照影像釋出到 MQTT 主題。" + }, + "timestamp": { + "label": "新增時間戳", + "description": "在釋出到 MQTT 的影像上疊加時間戳。" + }, + "bounding_box": { + "label": "新增邊界框", + "description": "在透過 MQTT 釋出的影像上繪製邊界框。" + }, + "crop": { + "label": "裁剪影像", + "description": "將釋出到 MQTT 的影像裁剪到偵測到的目標邊界框。" + }, + "height": { + "label": "影像高度", + "description": "透過 MQTT 釋出的影像調整到的目標高度(像素)。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能釋出 MQTT 影像的區域。" + }, + "quality": { + "label": "JPEG 品質", + "description": "釋出到 MQTT 的影像的 JPEG 品質(0-100)。" + } + }, + "camera_ui": { + "label": "攝影機頁面", + "description": "此攝影機在頁面中的顯示順序和可見性。顯示順序僅影響預設儀表板。如需更精細的控制,請使用“攝影機組”。", + "order": { + "label": "UI 順序", + "description": "用於在頁面中排序攝影機的順序(只會影響預設儀表板和清單);數值越大則在越後面。" + }, + "dashboard": { + "label": "在 UI 中顯示", + "description": "切換此攝影機在 Frigate 頁面中是否可見。停用後需要手動編輯配置才能再次在頁面中檢視此攝影機。" + }, + "review": { + "label": "在審查中顯示", + "description": "切換此攝影機在審查中是否顯示(包含審查頁面及其攝影機篩選器、動態審查和歷史檢視)。" + } + }, + "onvif": { + "label": "ONVIF", + "description": "此攝影機的 ONVIF 連線和 PTZ 自動追蹤設定。", + "host": { + "label": "ONVIF 主機", + "description": "此攝影機 ONVIF 服務的主機(和可選協議)。" + }, + "port": { + "label": "ONVIF 埠", + "description": "ONVIF 服務的埠號。" + }, + "user": { + "label": "ONVIF 使用者名稱", + "description": "ONVIF 身份驗證的使用者名稱;某些裝置需要管理員使用者才能使用 ONVIF。" + }, + "password": { + "label": "ONVIF 密碼", + "description": "ONVIF 身份驗證的密碼。" + }, + "tls_insecure": { + "label": "停用 TLS 驗證", + "description": "跳過 TLS 驗證並停用 ONVIF 的摘要認證(不安全;僅用於安全網路)。" + }, + "profile": { + "label": "ONVIF 設定檔", + "description": "用於 PTZ 控制的指定 ONVIF 媒體配置,將透過 Token 或名稱匹配。如果未手動指定,將自動選擇第一個包含有效 PTZ 配置的媒體配置。" + }, + "autotracking": { + "label": "自動追蹤", + "description": "使用 PTZ 攝影機移動自動追蹤移動目標並使其保持在畫面中心。", + "enabled": { + "label": "開啟自動追蹤", + "description": "啟用或停用偵測目標的自動 PTZ 攝影機追蹤。" + }, + "calibrate_on_startup": { + "label": "啟動時校準", + "description": "在啟動時測量 PTZ 電機速度以提高追蹤精度。Frigate 將在校準後用 movement_weights 更新配置。" + }, + "zooming": { + "label": "變焦模式", + "description": "控制變焦行為:disabled(僅平移/傾斜)、absolute(最相容)或 relative(同時平移/傾斜/變焦)。" + }, + "zoom_factor": { + "label": "變焦因子", + "description": "控制追蹤目標的變焦級別。數值越低保持更多場景可見;數值越高放大更近但可能丟失追蹤。數值範圍 0.1 到 0.75。" + }, + "track": { + "label": "追蹤目標", + "description": "應觸發自動追蹤的目標型別清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入這些區域之一才能開始自動追蹤。" + }, + "return_preset": { + "label": "返回預設", + "description": "追蹤結束後返回的攝影機韌體中配置的 ONVIF 預設名稱。" + }, + "timeout": { + "label": "返回超時", + "description": "失去追蹤後等待多少秒後將攝影機返回到預設位置。" + }, + "movement_weights": { + "label": "移動權重", + "description": "由攝影機校準自動生成的校準值。請勿手動修改。" + }, + "enabled_in_config": { + "label": "原始自動追蹤狀態", + "description": "用於追蹤配置中是否啟用自動追蹤的內部欄位。" + } + }, + "ignore_time_mismatch": { + "label": "忽略時間不匹配", + "description": "忽略 ONVIF 通訊中攝影機和 Frigate 伺服器之間的時間同步差異。" + } + } +} diff --git a/web/public/locales/zh-Hant/config/groups.json b/web/public/locales/zh-Hant/config/groups.json new file mode 100644 index 0000000..5180463 --- /dev/null +++ b/web/public/locales/zh-Hant/config/groups.json @@ -0,0 +1,73 @@ +{ + "audio": { + "global": { + "detection": "全域性偵測", + "sensitivity": "全域性靈敏度" + }, + "cameras": { + "detection": "偵測", + "sensitivity": "靈敏度" + } + }, + "timestamp_style": { + "global": { + "appearance": "全域性外觀" + }, + "cameras": { + "appearance": "外觀" + } + }, + "motion": { + "global": { + "sensitivity": "全域性靈敏度", + "algorithm": "全域性演算法" + }, + "cameras": { + "sensitivity": "靈敏度", + "algorithm": "演算法" + } + }, + "snapshots": { + "global": { + "display": "全域性顯示" + }, + "cameras": { + "display": "顯示" + } + }, + "detect": { + "global": { + "resolution": "全域性解析度", + "tracking": "全域性追蹤" + }, + "cameras": { + "resolution": "解析度", + "tracking": "追蹤" + } + }, + "objects": { + "global": { + "tracking": "全域性追蹤", + "filtering": "全域性篩選" + }, + "cameras": { + "tracking": "追蹤", + "filtering": "篩選" + } + }, + "record": { + "global": { + "retention": "全域性保留", + "events": "全域性事件" + }, + "cameras": { + "retention": "保留", + "events": "事件" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "攝影機特定的 FFmpeg 引數" + } + } +} diff --git a/web/public/locales/zh-Hant/config/validation.json b/web/public/locales/zh-Hant/config/validation.json new file mode 100644 index 0000000..c75f681 --- /dev/null +++ b/web/public/locales/zh-Hant/config/validation.json @@ -0,0 +1,35 @@ +{ + "minimum": "必須至少為 {{limit}}", + "maximum": "最大值不能超過 {{limit}}", + "exclusiveMinimum": "必須大於 {{limit}}", + "exclusiveMaximum": "必須小於 {{limit}}", + "minLength": "長度至少為 {{limit}} 個字元", + "maxLength": "長度最多為 {{limit}} 個字元", + "minItems": "至少包含 {{limit}} 項", + "maxItems": "最多包含 {{limit}} 項", + "pattern": "格式無效", + "required": "此欄位為必填項", + "type": "值型別無效", + "enum": "必須是允許的值之一", + "const": "值與預期的常量不匹配", + "uniqueItems": "所有項必須唯一", + "format": "格式無效", + "additionalProperties": "不允許未知屬性", + "oneOf": "必須完全匹配一個允許的模式", + "anyOf": "必須至少匹配一個允許的模式", + "proxy": { + "header_map": { + "roleHeaderRequired": "設定角色對應時必須要有 role 標頭。" + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "每個角色只能分配給一個輸入串流。", + "detectRequired": "必須至少有一個輸入串流分配為 'detect' 角色。", + "hwaccelDetectOnly": "只有分配了 detect 角色的輸入串流才能定義硬體加速引數。" + } + }, + "detect": { + "dimensionMustBeEven": "必須是偶數。" + } +} diff --git a/web/public/locales/zh-Hant/objects.json b/web/public/locales/zh-Hant/objects.json new file mode 100644 index 0000000..2dc4ad5 --- /dev/null +++ b/web/public/locales/zh-Hant/objects.json @@ -0,0 +1,129 @@ +{ + "person": "人", + "bicycle": "腳踏車", + "car": "車", + "boat": "船", + "traffic_light": "紅綠燈", + "fire_hydrant": "消防栓", + "street_sign": "道路標示", + "stop_sign": "停止標示", + "parking_meter": "停車柱", + "bench": "長椅", + "motorcycle": "摩托車", + "airplane": "飛機", + "bus": "巴士", + "train": "火車", + "bird": "鳥", + "cat": "貓", + "dog": "狗", + "horse": "馬", + "sheep": "羊", + "cow": "牛", + "elephant": "大象", + "bear": "熊", + "zebra": "斑馬", + "giraffe": "長頸鹿", + "hat": "帽子", + "backpack": "背包", + "umbrella": "雨傘", + "shoe": "鞋子", + "eye_glasses": "眼睛", + "handbag": "手提包", + "tie": "領帶", + "suitcase": "行李箱", + "frisbee": "飛盤", + "skis": "滑雪板", + "snowboard": "單板滑雪板", + "sports_ball": "運動球", + "kite": "風箏", + "baseball_bat": "棒球棍", + "baseball_glove": "棒球手套", + "skateboard": "滑板", + "surfboard": "衝浪板", + "tennis_racket": "網球拍", + "bottle": "瓶子", + "plate": "盤子", + "wine_glass": "酒杯", + "cup": "杯子", + "fork": "叉子", + "knife": "刀子", + "spoon": "湯匙", + "bowl": "碗", + "banana": "香蕉", + "apple": "蘋果", + "sandwich": "三明治", + "orange": "橘子", + "broccoli": "花椰菜", + "carrot": "紅蘿蔔", + "hot_dog": "熱狗", + "pizza": "披薩", + "donut": "甜甜圈", + "cake": "蛋糕", + "chair": "椅子", + "couch": "沙發", + "potted_plant": "盆栽植物", + "bed": "床", + "mirror": "鏡子", + "dining_table": "餐桌", + "window": "窗戶", + "desk": "桌子", + "toilet": "廁所", + "door": "門", + "tv": "電視", + "laptop": "筆電", + "mouse": "滑鼠", + "remote": "遠端", + "keyboard": "鍵盤", + "cell_phone": "手機", + "microwave": "微波爐", + "oven": "烤箱", + "toaster": "烤麵包機", + "sink": "水槽", + "refrigerator": "冰箱", + "blender": "果汁機", + "book": "書", + "clock": "時鐘", + "vase": "花瓶", + "scissors": "剪刀", + "teddy_bear": "泰迪熊", + "hair_dryer": "吹風機", + "toothbrush": "牙刷", + "hair_brush": "梳子", + "vehicle": "車輛", + "squirrel": "松鼠", + "deer": "鹿", + "animal": "動物", + "bark": "樹皮", + "fox": "狐狸", + "goat": "山羊", + "rabbit": "兔子", + "raccoon": "浣熊", + "robot_lawnmower": "自動割草機", + "waste_bin": "垃圾桶", + "on_demand": "隨選服務", + "face": "臉部", + "license_plate": "車牌", + "package": "包裹", + "bbq_grill": "烤肉架", + "amazon": "亞馬遜(Amazon)", + "usps": "美國郵政(USPS)", + "ups": "UPS", + "fedex": "聯邦快遞(FedEx)", + "dhl": "DHL", + "an_post": "愛爾蘭郵政(An Post)", + "purolator": "加拿大普洛特快遞", + "postnl": "荷蘭郵政(PostNL)", + "nzpost": "紐西蘭郵政(NZ Post)", + "postnord": "北歐郵政(PostNord)", + "gls": "GLS 快遞", + "dpd": "DPD 快遞", + "canada_post": "加拿大郵政", + "royal_mail": "英國皇家郵政", + "school_bus": "校車", + "skunk": "臭鼬", + "kangaroo": "袋鼠", + "baby": "嬰兒", + "baby_stroller": "嬰兒推車", + "rickshaw": "人力車", + "rodent": "齧齒動物" +} diff --git a/web/public/locales/zh-Hant/views/chat.json b/web/public/locales/zh-Hant/views/chat.json new file mode 100644 index 0000000..af68b79 --- /dev/null +++ b/web/public/locales/zh-Hant/views/chat.json @@ -0,0 +1,72 @@ +{ + "documentTitle": "聊天 - Frigate", + "title": "Frigate 聊天", + "subtitle": "你的攝影機管理與智慧分析 AI 助手", + "placeholder": "嘗試問我任何事…", + "error": "出現錯誤,請稍後重試。", + "processing": "進行中…", + "toolsUsed": "使用:{{tools}}", + "showTools": "顯示工具({{count}})", + "hideTools": "隱藏工具", + "call": "呼叫", + "result": "結果", + "arguments": "引數:", + "response": "回應:", + "attachment_chip_label": "在 {{camera}} 的 {{label}}", + "attachment_chip_remove": "移除附件", + "open_in_explore": "從瀏覽中開啟", + "attach_event_aria": "關聯事件 {{eventId}}", + "attachment_picker_paste_label": "或貼上事件 ID", + "attachment_picker_attach": "關聯", + "attachment_picker_placeholder": "關聯一個事件", + "quick_reply_find_similar": "查詢相似抓拍事件", + "quick_reply_tell_me_more": "瞭解更多詳情", + "quick_reply_when_else": "還在哪些時段出現過?", + "quick_reply_find_similar_text": "查詢與此相似的抓拍記錄。", + "quick_reply_tell_me_more_text": "瞭解此條更多詳情。", + "quick_reply_when_else_text": "還在哪些時間出現過?", + "anchor": "來源", + "similarity_score": "相似度", + "no_similar_objects_found": "未找到相似目標。", + "semantic_search_required": "必須啟用語意搜尋才能查詢相似目標。", + "send": "傳送", + "suggested_requests": "嘗試問問:", + "starting_requests": { + "show_recent_events": "檢視近期事件", + "show_camera_status": "顯示攝影機狀態", + "recap": "我不在的時候發生了什麼?", + "watch_camera": "監控攝影機活動" + }, + "starting_requests_prompts": { + "show_recent_events": "顯示最近一小時的事件", + "show_camera_status": "我的攝影機當前狀態如何?", + "recap": "我不在的時候發生了什麼事?", + "watch_camera": "監控前門,有人出現就通知我" + }, + "new_chat": "新對話", + "settings": { + "title": "對話設定", + "show_stats": { + "title": "顯示統計", + "desc": "顯示對話回應的產生速度與上下文大小。", + "while_generating": "產生時", + "always": "一律顯示" + }, + "auto_scroll": { + "title": "自動捲動", + "desc": "隨新訊息到來自動跟進。" + } + }, + "stats": { + "context": "{{tokens}} 個 token", + "tokens_per_second": "{{rate}} tokens/秒" + }, + "reasoning": { + "active": "推論中…", + "show": "顯示推論過程", + "hide": "隱藏推論過程" + }, + "thinking": { + "toggle": "切換顯示推論" + } +} diff --git a/web/public/locales/zh-Hant/views/classificationModel.json b/web/public/locales/zh-Hant/views/classificationModel.json new file mode 100644 index 0000000..c716e89 --- /dev/null +++ b/web/public/locales/zh-Hant/views/classificationModel.json @@ -0,0 +1,191 @@ +{ + "toast": { + "success": { + "deletedImage_other": "已刪除 {{count}} 張圖片", + "deletedModel_other": "已成功刪除 {{count}} 個模型", + "deletedCategory_other": "已刪除 {{count}} 個類別", + "categorizedImage": "成功分類圖片", + "trainedModel": "訓練模型成功。", + "trainingModel": "已開始模型訓練。", + "updatedModel": "已更新模型配置", + "renamedCategory": "成功修改分類名稱為{{name}}", + "reclassifiedImage": "成功重新分類圖片" + }, + "error": { + "deleteImageFailed": "刪除失敗:{{errorMessage}}", + "deleteCategoryFailed": "刪除分類標籤失敗: {{errorMessage}}", + "deleteModelFailed": "刪除模型失敗: {{errorMessage}}", + "categorizeFailed": "圖片分類失敗: {{errorMessage}}", + "trainingFailed": "模型訓練失敗。請至Frigate 日誌查看詳情。", + "trainingFailedToStart": "模型訓練啟動失敗: {{errorMessage}}", + "updateModelFailed": "模型更新失敗: {{errorMessage}}", + "renameCategoryFailed": "類別重新命名失敗: {{errorMessage}}", + "reclassifyFailed": "重新分類圖片失敗:{{errorMessage}}" + } + }, + "documentTitle": "分類模型", + "details": { + "scoreInfo": "分數表示該目標所有偵測結果的平均分類置信度。", + "none": "沒有", + "unknown": "未知" + }, + "button": { + "deleteClassificationAttempts": "刪除分類圖片", + "renameCategory": "重新命名分類", + "deleteCategory": "刪除分類", + "deleteImages": "刪除圖片", + "trainModel": "訓練模型", + "addClassification": "添加分類", + "deleteModels": "刪除模型", + "editModel": "編輯模型" + }, + "tooltip": { + "trainingInProgress": "模型正在訓練中", + "noNewImages": "沒有新的圖片可用於訓練。請先對數據集中的更多圖片進行分類。", + "noChanges": "自上次訓練以來,數據集未作任何更改。", + "modelNotReady": "模型尚未準備好進行訓練" + }, + "deleteCategory": { + "title": "刪除類別", + "desc": "你確定要刪除類別{{name}}嗎? 這將刪除所有有關的圖片並需要重新訓練模型。", + "minClassesTitle": "無法刪除此類別", + "minClassesDesc": "分類模型必須至少擁有2個類別,新增一個新的類別已刪除這個。" + }, + "deleteModel": { + "title": "刪除分類模型", + "single": "你確定要刪除{{name}}嗎? 這將永久刪除包含圖片和訓練資料在內的所有相關資料。這個操作無法被復原。", + "desc_other": "你確定要刪除{{count}}個模型? 這將永久刪除包含圖片和訓練資料在內的所有相關資料。這個操作無法被復原。" + }, + "edit": { + "title": "編輯分類模型", + "descriptionState": "編輯這個狀態分類模型的類別,變更將需要重新訓練模型。", + "descriptionObject": "編輯這個物件分類模型的物件種類與分類種類。", + "stateClassesInfo": "注意: 變更狀態類別後需要以更新後的類別重新訓練模型。" + }, + "deleteDatasetImages": { + "title": "刪除圖片資料集合", + "desc_other": "你確定要從{{dataset}}中刪除{{count}}個圖片嗎? 這個操作將無法被復原且將需要重新訓練模型。" + }, + "deleteTrainImages": { + "title": "刪除訓練圖片", + "desc_other": "你確定要刪除{{count}}個圖片? 這個操作無法被復原。" + }, + "renameCategory": { + "title": "重新命名類別", + "desc": "輸入 {{name}} 的新名稱。您需要在名稱變更後重新訓練模型以套用變更。" + }, + "description": { + "invalidName": "無效的名稱。名稱只能包涵英數字、空格、撇(')、底線(_)及連字號(-)。" + }, + "train": { + "title": "最近的分類紀錄", + "titleShort": "最近", + "aria": "選取最近的分類紀錄" + }, + "categories": "類別", + "createCategory": { + "new": "建立新的類別" + }, + "wizard": { + "step1": { + "objectLabel": "物件標籤", + "objectLabelPlaceholder": "請選擇物件類型...", + "classificationType": "分類類型", + "classificationTypeTip": "學習更多有關分類類型", + "description": "狀態模型監視固定攝像頭區域的變化(例如:開關門)。物件模型為檢測到的物件(例如:已知動物、送貨員等等)添加分類。", + "name": "名稱", + "namePlaceholder": "請輸入模型名稱...", + "type": "類別", + "typeState": "狀態", + "typeObject": "物件", + "classificationTypeDesc": "子標籤會為目標標籤新增附加文字(例如:“人員:美團”)。屬性是可搜尋的元資料,獨立儲存在目標的元資訊中。", + "classificationSubLabel": "子標籤", + "classificationAttribute": "屬性", + "classes": "類別", + "states": "狀態", + "classesTip": "瞭解類別", + "classesStateDesc": "定義攝影機區域內可能出現的不同狀態。例如:車庫門的“開啟”和“關閉”。", + "classesObjectDesc": "定義用於分類偵測目標的不同類別。例如:人員分類中的“快遞員”、“居民”、“陌生人”。", + "classPlaceholder": "請輸入分類名稱……", + "errors": { + "nameRequired": "模型名稱為必填項", + "nameLength": "模型名稱長度不能超過 64 個字元", + "nameOnlyNumbers": "模型名稱不能僅包含數字", + "classRequired": "至少需要一個類別", + "classesUnique": "類別名稱必須唯一", + "noneNotAllowed": "不能建立“none”(無標籤)類別", + "stateRequiresTwoClasses": "狀態模型至少需要兩個類別", + "objectLabelRequired": "請選擇一個目標標籤", + "objectTypeRequired": "請選擇一個目標標籤" + } + }, + "steps": { + "chooseExamples": "選擇範本", + "nameAndDefine": "名稱與定義", + "stateArea": "狀態區域" + }, + "title": "建立新分類", + "step2": { + "description": "選擇攝影機,併為攝影機定義要監控的區域。模型將對這些區域的狀態進行分類。", + "cameras": "攝影機", + "selectCamera": "選擇攝影機", + "noCameras": "點選 + 符號新增攝影機", + "selectCameraPrompt": "從清單中選擇一個攝影機以定義其偵測區域" + }, + "step3": { + "selectImagesPrompt": "選擇所有屬於 {{className}} 的圖片", + "selectImagesDescription": "點選影像進行選擇,完成該類別後點選“繼續”。", + "allImagesRequired_other": "請對所有圖片進行分類。還有 {{count}} 張圖片需要分類。", + "generating": { + "title": "正在生成樣本圖片", + "description": "Frigate 正在從錄影中提取代表性圖片。這可能需要一些時間……" + }, + "training": { + "title": "正在訓練模型", + "description": "系統正在後臺訓練模型。你可以關閉此對話方塊,訓練完成後模型將自動開始執行。" + }, + "retryGenerate": "重新生成", + "noImages": "未生成樣本影像", + "classifying": "正在分類與訓練……", + "trainingStarted": "已開始模型訓練", + "modelCreated": "模型建立成功。請在“最近分類”頁面為缺失的狀態新增圖片,然後訓練模型。", + "errors": { + "noCameras": "未配置攝影機", + "noObjectLabel": "未選擇目標標籤", + "generateFailed": "示例生成失敗:{{error}}", + "generationFailed": "生成失敗,請重試。", + "classifyFailed": "圖片分類失敗:{{error}}" + }, + "generateSuccess": "樣本圖片生成成功", + "refreshExamples": "生成新示例", + "refreshConfirm": { + "title": "需要生成新示例?", + "description": "此操作將生成一組新的圖片,並清除所有選擇內容(包括之前的所有類別)。你需要為所有類別重新選擇示例。" + }, + "missingStatesWarning": { + "title": "缺失分類示例", + "description": "並非所有類別都有示例。可嘗試生成新示例以查詢缺失的類別,或繼續該步驟,之後透過 “最近分類” 頁面新增圖片。" + } + } + }, + "menu": { + "states": "狀態", + "objects": "目標" + }, + "noModels": { + "object": { + "title": "沒有物件檢測模型", + "description": "建立自訂模型以對偵測到的物件進行分類。", + "buttonText": "建立物件模型" + }, + "state": { + "description": "建立自訂模型,用於監控和分類特定攝影機區域的狀態變化。", + "title": "尚未建立狀態分類模型", + "buttonText": "建立狀態模型" + } + }, + "categorizeImageAs": "圖片分類為:", + "categorizeImage": "圖片分類", + "reclassifyImageAs": "重新分類圖片為:", + "reclassifyImage": "重新分類圖片" +} diff --git a/web/public/locales/zh-Hant/views/configEditor.json b/web/public/locales/zh-Hant/views/configEditor.json new file mode 100644 index 0000000..f1943ed --- /dev/null +++ b/web/public/locales/zh-Hant/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "設定編輯器 - Frigate", + "configEditor": "設定編輯器", + "copyConfig": "複製設定", + "saveAndRestart": "保存並重新啟動", + "toast": { + "error": { + "savingError": "保存設定時出錯" + }, + "success": { + "copyToClipboard": "已複製設定製剪貼簿。" + } + }, + "saveOnly": "僅保存", + "confirm": "是否不保存就離開?", + "safeConfigEditor": "設定編輯器(安全模式)", + "safeModeDescription": "由於設定驗證有誤,Frigate 進入安全模式。" +} diff --git a/web/public/locales/zh-Hant/views/events.json b/web/public/locales/zh-Hant/views/events.json new file mode 100644 index 0000000..bbe3f4b --- /dev/null +++ b/web/public/locales/zh-Hant/views/events.json @@ -0,0 +1,94 @@ +{ + "alerts": "警報", + "detections": "偵測", + "motion": { + "label": "移動", + "only": "只顯示移動" + }, + "empty": { + "motion": "未找到移動資料", + "alert": "沒有警告需要審核", + "detection": "沒有偵測到的內容需要審核", + "recordingsDisabled": { + "title": "必須啟用錄製功能", + "description": "僅當該攝影機啟用錄製功能時,才能為該攝影機建立審查項目。" + } + }, + "timeline": { + "label": "時間線" + }, + "timeline.aria": "選擇時間線", + "events": { + "label": "事件", + "aria": "選擇事件", + "noFoundForTimePeriod": "此時間段內沒有找到事件。" + }, + "documentTitle": "審核 - Frigate", + "allCameras": "所有鏡頭", + "recordings": { + "documentTitle": "錄影 - Frigate", + "invalidSharedLink": "由於解析錯誤,無法開啟帶時間戳的錄製連結。", + "invalidSharedCamera": "由於攝影機未知或未獲授權,無法開啟帶時間戳的錄製連結。" + }, + "calendarFilter": { + "last24Hours": "過去 24 小時" + }, + "markAsReviewed": "標示為已審核", + "markTheseItemsAsReviewed": "將這些內容標記為已審核", + "newReviewItems": { + "label": "查看新的審核項目", + "button": "有新的審核項目" + }, + "selected_one": "已選擇 {{count}} 個", + "selected_other": "已選擇 {{count}} 個", + "camera": "鏡頭", + "detected": "已偵測", + "suspiciousActivity": "可疑的活動", + "threateningActivity": "有威脅性的活動", + "zoomIn": "放大", + "zoomOut": "縮小", + "detail": { + "label": "詳細資訊", + "noDataFound": "沒有可供檢視的詳細資訊", + "aria": "開關詳細資訊視圖", + "trackedObject_one": "{{count}} 個物件", + "trackedObject_other": "{{count}} 個物件", + "noObjectDetailData": "沒有可用物件細節。", + "settings": "細節視圖設定", + "alwaysExpandActive": { + "title": "總是展開", + "desc": "在可用時總是展開當前物件的詳細資訊。" + } + }, + "objectTrack": { + "trackedPoint": "追蹤點", + "clickToSeek": "點擊從此時間點尋找" + }, + "normalActivity": "正常", + "needsReview": "待審核", + "securityConcern": "安全隱憂", + "select_all": "全選", + "motionSearch": { + "menuItem": "畫面變動搜尋", + "openMenu": "攝影機選項" + }, + "motionPreviews": { + "menuItem": "檢視畫面變動預覽", + "title": "畫面變動預覽:{{camera}}", + "mobileSettingsTitle": "畫面變動預覽設定", + "mobileSettingsDesc": "調整播放速度和變暗程度,並選擇日期以僅檢視畫面變動的片段。", + "dim": "變暗", + "dimAria": "調整變暗強度", + "dimDesc": "增加變暗程度可以提高畫面變動區域的可見性。", + "speed": "速度", + "speedAria": "選擇預覽播放速度", + "speedDesc": "選擇預覽片段的播放速度。", + "back": "返回", + "empty": "沒有可用的預覽", + "noPreview": "預覽不可用", + "seekAria": "將 {{camera}} 播放器定位到 {{time}}", + "filter": "篩選", + "filterDesc": "選擇區域以僅顯示在這些區域中有畫面變動的片段。", + "filterClear": "清除" + } +} diff --git a/web/public/locales/zh-Hant/views/explore.json b/web/public/locales/zh-Hant/views/explore.json new file mode 100644 index 0000000..6fa6680 --- /dev/null +++ b/web/public/locales/zh-Hant/views/explore.json @@ -0,0 +1,315 @@ +{ + "documentTitle": "瀏覽 - Frigate", + "generativeAI": "生成式 AI", + "exploreMore": "瀏覽更多 {{label}} 物件", + "exploreIsUnavailable": { + "title": "無法使用瀏覽功能", + "embeddingsReindexing": { + "finishingShortly": "即將完成", + "step": { + "thumbnailsEmbedded": "已嵌入縮圖: ", + "descriptionsEmbedded": "已嵌入說明: ", + "trackedObjectsProcessed": "已處理的追蹤物件: " + }, + "context": "在重新建立完追蹤物件的嵌入索引後可以使用瀏覽功能。", + "startingUp": "啟動中…", + "estimatedTime": "預計剩餘時間:" + }, + "downloadingModels": { + "context": "Frigate 正在下載所需的嵌入式模型以支援語意搜尋功能。根據你的網路連接速度,這可能會需要幾分鐘。", + "setup": { + "visionModel": "視覺模型", + "visionModelFeatureExtractor": "視覺模型特徵提取器", + "textModel": "文字模型", + "textTokenizer": "文字分詞器" + }, + "tips": { + "context": "在模型下載完成後,你可能會需要重新建立追蹤物件的特徵索引。", + "documentation": "閱讀文件" + }, + "error": "發生錯誤。請檢查 Frigate 日誌。" + } + }, + "details": { + "timestamp": "時間戳", + "item": { + "title": "審核項目詳情", + "desc": "審核項目詳情", + "button": { + "share": "分享此審核項目", + "viewInExplore": "在瀏覽中查看" + }, + "tips": { + "mismatch_other": "在此審核項目中偵測到 {{count}} 個不可用物件。這些物件可能不符合警示或偵測標準,或者已被清除/刪除。", + "hasMissingObjects": "如果你想要 Frigate 儲存以下標籤的追蹤物件:{{objects}},請調整設定" + }, + "toast": { + "success": { + "regenerate": "已從 {{provider}} 請求新的說明。根據提供者的速度,生成新的說明可能會需要一段時間。", + "updatedSublabel": "成功更新子標籤。", + "updatedLPR": "成功更新車牌。", + "updatedAttributes": "已成功更新屬性。", + "audioTranscription": "已成功送出音訊轉錄請求。轉錄完成所需時間會依您的 Frigate 伺服器速度而定,可能需要一段時間。" + }, + "error": { + "regenerate": "請求 {{provider}} 生成新的說明失敗:{{errorMessage}}", + "updatedSublabelFailed": "更新子標籤失敗:{{errorMessage}}", + "updatedLPRFailed": "更新車牌失敗:{{errorMessage}}", + "updatedAttributesFailed": "更新屬性失敗:{{errorMessage}}", + "audioTranscription": "請求音訊轉錄失敗:{{errorMessage}}" + } + } + }, + "label": "標籤", + "editSubLabel": { + "title": "編輯子標籤", + "desc": "輸入 {{label}} 的新子標籤", + "descNoLabel": "輸入此追蹤物件的新子標籤" + }, + "editLPR": { + "title": "編輯車牌", + "desc": "輸入此 {{label}} 的新車牌號碼", + "descNoLabel": "輸入此追蹤物件的新車牌號碼" + }, + "snapshotScore": { + "label": "截圖分數" + }, + "topScore": { + "label": "最高分數", + "info": "最高分數是追蹤物件的最高中位數,因此可能會與搜尋結果的截圖顯示的分數有所不同。" + }, + "recognizedLicensePlate": "已辨識車牌", + "estimatedSpeed": "估計速度", + "objects": "物件", + "camera": "鏡頭", + "zones": "區域", + "button": { + "findSimilar": "尋找相似項目", + "regenerate": { + "title": "重新生成", + "label": "重新生成追蹤物件的說明" + } + }, + "description": { + "label": "說明", + "placeholder": "追蹤物件的說明", + "aiTips": "在追蹤物件的生命週期結束前,Frigate 不會向你設定的生成式 AI 提供者請求說明。" + }, + "expandRegenerationMenu": "展開重新生成選單", + "regenerateFromSnapshot": "從截圖重新生成", + "regenerateFromThumbnails": "從縮圖重新生成", + "tips": { + "descriptionSaved": "成功保存說明", + "saveDescriptionFailed": "更新說明失敗:{{errorMessage}}" + }, + "editAttributes": { + "title": "編輯屬性", + "desc": "為此 {{label}} 選擇分類屬性" + }, + "score": { + "label": "分數" + }, + "attributes": "分類屬性", + "title": { + "label": "標題" + }, + "scoreInfo": "分數資訊" + }, + "trackedObjectDetails": "追蹤物件詳情", + "type": { + "details": "詳情", + "snapshot": "截圖", + "video": "影片", + "object_lifecycle": "物件生命週期", + "thumbnail": "預覽圖", + "tracking_details": "追蹤詳情" + }, + "objectLifecycle": { + "title": "物件生命週期", + "noImageFound": "此時間點找不到圖片。", + "createObjectMask": "建立物件遮罩", + "adjustAnnotationSettings": "調整標注設定", + "scrollViewTips": "滾動以查看此物件生命週期中的重要時刻。", + "autoTrackingTips": "自動追蹤鏡頭的定界框位置可能不準確。", + "count": "第 {{first}} 個,共 {{second}} 個", + "trackedPoint": "追蹤點", + "lifecycleItemDesc": { + "visible": "偵測到 {{label}}", + "entered_zone": "{{label}} 進入了 {{zones}}", + "active": "{{label}} 開始活動", + "stationary": "{{label}} 停止活動", + "attribute": { + "faceOrLicense_plate": "偵測到 {{label}} 的 {{attribute}}", + "other": "{{label}} 被辨識為 {{attribute}}" + }, + "gone": "{{label}} 離開了", + "heard": "聽到 {{label}}", + "external": "偵測到 {{label}}", + "header": { + "zones": "區域", + "ratio": "比例", + "area": "範圍" + } + }, + "annotationSettings": { + "title": "標注設定", + "showAllZones": { + "title": "顯示所有區域", + "desc": "總是在畫面上顯示有物件進入的區域。" + }, + "offset": { + "label": "標注偏移量", + "desc": "此資料是來自鏡頭的偵測串流,但被覆蓋在錄影串流上。通常兩個串流沒辦法完美的同步,因此,影片片段中的定界框可能無法完全對齊。不過,這可以透過 annotation_offset 進行調整。", + "documentation": "閱讀文件 ", + "millisecondsToOffset": "偵測註解偏移的毫秒數。預設:0", + "tips": "提示:試想在一個片段中有個人從畫面左邊走到右邊。如果事件時間線上的定界框一直出現在人物的左邊,則應該減少數值。在同樣的畫面中,如果定界框持續出現在人的前方,則應該增加數值。", + "toast": { + "success": "{{camera}} 的標注偏移量已保存到設定檔。重新啟動 Frigate 以套用更改。" + } + } + }, + "carousel": { + "previous": "上一張", + "next": "下一張" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "下載影片", + "aria": "下載影片" + }, + "downloadSnapshot": { + "label": "下載截圖", + "aria": "下載截圖" + }, + "viewObjectLifecycle": { + "label": "查看物件生命週期", + "aria": "顯示物件生命週期" + }, + "findSimilar": { + "label": "尋找相似項目", + "aria": "尋找相似的追蹤物件" + }, + "submitToPlus": { + "label": "提交到 Frigate+", + "aria": "提交到 Frigate Plus" + }, + "viewInHistory": { + "label": "於歷史記錄中查看", + "aria": "於歷史記錄中查看" + }, + "deleteTrackedObject": { + "label": "刪除此追蹤物件" + }, + "hideObjectDetails": { + "label": "隱藏物件路徑" + }, + "showObjectDetails": { + "label": "顯示物件路徑" + }, + "addTrigger": { + "label": "新增觸發器", + "aria": "為此追蹤物件新增觸發器" + }, + "audioTranscription": { + "label": "轉錄", + "aria": "請求音訊轉錄" + }, + "downloadCleanSnapshot": { + "label": "下載乾淨的快照", + "aria": "下載乾淨的快照" + }, + "viewTrackingDetails": { + "label": "檢視追蹤詳細資訊", + "aria": "顯示追蹤詳細資訊" + }, + "debugReplay": { + "label": "除錯回放", + "aria": "在除錯回放檢視中檢視此被追蹤物件" + }, + "more": { + "aria": "更多" + } + }, + "dialog": { + "confirmDelete": { + "title": "確認刪除", + "desc": "刪除此追蹤物件將會移除快照、已儲存的嵌入向量資料(embeddings),以及相關的追蹤詳情紀錄。歷史檢視中的錄影檔案 不會 被刪除。

    確定要刪除嗎?" + }, + "toast": { + "error": "刪除該追蹤目標時出錯:{{errorMessage}}" + } + }, + "noTrackedObjects": "找不到追蹤物件", + "fetchingTrackedObjectsFailed": "取得追蹤物件時錯誤:{{errorMessage}}", + "trackedObjectsCount_other": "{{count}} 個追蹤物件 ", + "searchResult": { + "tooltip": "與 {{type}} 相似的程度為 {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "success": "成功刪除蹤物件。", + "error": "刪除追蹤物件失敗:{{errorMessage}}" + } + }, + "previousTrackedObject": "上一個追蹤物件", + "nextTrackedObject": "下一個追蹤物件" + }, + "trackingDetails": { + "title": "追蹤詳情", + "noImageFound": "沒有找到在此時間點的圖片。", + "createObjectMask": "建立物件遮罩", + "adjustAnnotationSettings": "調整標記設定", + "scrollViewTips": "點擊查看物件周期的關鍵時間。", + "autoTrackingTips": "自動追蹤鏡頭的邊框位置可能不準確。", + "count": "{{second}}之{{first}}", + "trackedPoint": "追蹤點", + "lifecycleItemDesc": { + "visible": "偵測到 {{label}}", + "entered_zone": "{{label}} 已進入 {{zones}}", + "active": "{{label}} 正在活動", + "stationary": "{{label}} 變為靜止", + "attribute": { + "faceOrLicense_plate": "偵測到{{label}} {{attribute}}", + "other": "{{label}} 被識別為 {{attribute}}" + }, + "gone": "{{label}} 已離開", + "heard": "聽到了 {{label}}", + "external": "偵測到 {{label}}", + "header": { + "zones": "區域", + "ratio": "比例", + "score": "分數", + "area": "面積", + "computedScore": "計算得分", + "topScore": "最高得分", + "toggleAdvancedScores": "切換高階分數" + } + }, + "annotationSettings": { + "title": "標記設定", + "showAllZones": { + "title": "顯示所有區域", + "desc": "總是在物件進入區域時在畫面上顯示區域範圍。" + }, + "offset": { + "label": "標記偏移量", + "desc": "這個資料來自您的鏡頭的偵測串流源,但是被疊加在錄影串流源的畫面上,兩個串流源不太可能完美的同步,因此邊框與畫面無法完美的對齊。您可以用這項設定調整標記在時間上前後偏移的補償量來更好的將其與錄影畫面對齊。", + "millisecondsToOffset": "偵測標記偏移補償的毫秒數。預設值: 0", + "tips": "如果影片播放進度超前於方框和路徑點,則降低該值;如果影片播放進度落後於方框和路徑點,則增加該數值。該值可以為負數。", + "toast": { + "success": "{{camera}} 的標記偏移補償量已儲存至設定檔。" + } + } + }, + "carousel": { + "previous": "上一張投影片", + "next": "下一張投影片" + } + }, + "aiAnalysis": { + "title": "AI 分析" + }, + "concerns": { + "label": "風險等級" + } +} diff --git a/web/public/locales/zh-Hant/views/exports.json b/web/public/locales/zh-Hant/views/exports.json new file mode 100644 index 0000000..0b376bc --- /dev/null +++ b/web/public/locales/zh-Hant/views/exports.json @@ -0,0 +1,128 @@ +{ + "search": "搜尋", + "documentTitle": "匯出 - Frigate", + "noExports": "找不到匯出內容", + "deleteExport": { + "label": "刪除匯出" + }, + "editExport": { + "saveExport": "儲存匯出內容", + "title": "重新命名匯出內容", + "desc": "請輸入此匯出內容的新名稱。" + }, + "toast": { + "error": { + "renameExportFailed": "重新命名匯出內容失敗:{{errorMessage}}", + "assignCaseFailed": "更新案件分配失敗:{{errorMessage}}", + "caseSaveFailed": "儲存案件失敗:{{errorMessage}}", + "caseDeleteFailed": "刪除案件失敗:{{errorMessage}}" + } + }, + "deleteExport.desc": "你確定要刪除 {{exportName}} 嗎?", + "tooltip": { + "shareExport": "分享匯出", + "downloadVideo": "下載影片", + "editName": "編輯名稱", + "deleteExport": "刪除匯出", + "assignToCase": "加入案件", + "removeFromCase": "從案件中移除" + }, + "headings": { + "cases": "案件", + "uncategorizedExports": "未分類匯出項" + }, + "toolbar": { + "newCase": "新案件", + "addExport": "新匯出", + "editCase": "編輯案件", + "deleteCase": "刪除案件" + }, + "deleteCase": { + "label": "刪除案件", + "desc": "你確定要刪除 {{caseName}} 嗎?", + "descKeepExports": "匯出檔案將繼續保留為未分類匯出。", + "descDeleteExports": "此案件中的所有匯出項都將被永久刪除。", + "deleteExports": "同時刪除匯出檔案" + }, + "caseDialog": { + "title": "加入案件", + "description": "選擇現有案件或建立新案件。", + "selectLabel": "案件", + "newCaseOption": "建立新案件", + "nameLabel": "案件名稱", + "descriptionLabel": "描述" + }, + "caseCard": { + "emptyCase": "暫無匯出檔案" + }, + "jobCard": { + "defaultName": "{{camera}} 匯出", + "queued": "佇列中", + "running": "執行中", + "preparing": "準備中", + "copying": "複製中", + "encoding": "編碼中", + "encodingRetry": "重試編碼中", + "finalizing": "正在完成" + }, + "caseView": { + "noDescription": "沒有描述", + "createdAt": "已建立 {{value}}", + "exportCount_one": "1 個匯出", + "exportCount_other": "{{count}} 個匯出", + "cameraCount_one": "1 個攝影機", + "cameraCount_other": "{{count}} 個攝影機", + "showMore": "顯示更多", + "showLess": "顯示更少", + "emptyTitle": "該案件為空", + "emptyDescription": "將現有未分類的匯出新增進來,以便整理該條目。", + "emptyDescriptionNoExports": "目前沒有可新增的未分類匯出項。" + }, + "caseEditor": { + "createTitle": "建立案件", + "editTitle": "編輯案件", + "namePlaceholder": "案件名稱", + "descriptionPlaceholder": "為該案件新增備註或相關說明" + }, + "addExportDialog": { + "title": "將匯出新增到 {{caseName}}", + "searchPlaceholder": "搜尋未分類的匯出項", + "empty": "未找到匹配的未分類匯出。", + "addButton_one": "新增 1 個匯出", + "addButton_other": "新增 {{count}} 個匯出", + "adding": "新增中…" + }, + "selected_one": "已選擇 {{count}} 個", + "selected_other": "已選擇 {{count}} 個", + "bulkActions": { + "addToCase": "新增至案件", + "moveToCase": "移動至案件", + "removeFromCase": "從案件中移除", + "delete": "刪除", + "deleteNow": "立即刪除" + }, + "bulkDelete": { + "title": "刪除匯出", + "desc_one": "你確定要刪除 {{count}} 個匯出嗎?", + "desc_other": "確定要刪除 {{count}} 個匯出嗎?" + }, + "bulkRemoveFromCase": { + "title": "從案件中移除", + "desc_one": "你確定要從該案件中移除這 {{count}} 個匯出嗎?", + "desc_other": "你確定要從該案件中移除這 {{count}} 個匯出嗎?", + "descKeepExports": "匯出將被移至未分類。", + "descDeleteExports": "匯出將被永久刪除。", + "deleteExports": "選擇刪除匯出" + }, + "bulkToast": { + "success": { + "delete": "已刪除匯出", + "reassign": "已更新案件分配", + "remove": "已從案件中移除匯出" + }, + "error": { + "deleteFailed": "刪除匯出失敗:{{errorMessage}}", + "reassignFailed": "更新案件分配失敗:{{errorMessage}}" + } + } +} diff --git a/web/public/locales/zh-Hant/views/faceLibrary.json b/web/public/locales/zh-Hant/views/faceLibrary.json new file mode 100644 index 0000000..496e163 --- /dev/null +++ b/web/public/locales/zh-Hant/views/faceLibrary.json @@ -0,0 +1,106 @@ +{ + "description": { + "addFace": "上傳您的第一張照片至臉部資料庫以新增一個新的集合。", + "placeholder": "輸入此集合的名稱", + "invalidName": "無效的名稱。名稱只能包涵英數字、空格、撇(')、底線(_)及連字號(-)。", + "nameCannotContainHash": "名稱中不允許包含“#”符號。" + }, + "details": { + "person": "人", + "timestamp": "時間戳", + "unknown": "未知", + "subLabelScore": "子標籤分數", + "scoreInfo": "子標籤分數是所有已辨識的人臉信心值的加權平均,因此可能與截圖上顯示的分數不同。", + "face": "人臉詳細資料", + "faceDesc": "組成此人臉的追蹤物件的詳細資料" + }, + "documentTitle": "人臉資料庫 - Frigate", + "uploadFaceImage": { + "title": "上傳人臉圖片", + "desc": "上傳圖片以掃描人臉並將其加入 {{pageToggle}}" + }, + "collections": "集合", + "selectItem": "選擇 {{item}}", + "createFaceLibrary": { + "title": "建立集合", + "desc": "建立新集合", + "new": "建立新人臉", + "nextSteps": "為了建立可靠的模型基底:
  • 在最近的識別紀錄分頁中選擇並針對每個偵測到人的圖片進行訓練。
  • 請優先使用正臉照以獲得最佳效果,請盡量避免使用從側面或有傾斜角度的人臉
  • " + }, + "steps": { + "faceName": "輸入人臉名稱", + "uploadFace": "上傳人臉圖片", + "nextSteps": "下一步", + "description": { + "uploadFace": "上傳一張 {{name}} 的正臉圖片。圖片不需要裁剪到只剩下臉部。" + } + }, + "train": { + "title": "最近的識別紀錄", + "aria": "選擇最近的識別紀錄", + "empty": "最近沒有辨識人臉的操作", + "titleShort": "最近", + "emptyNoLibrary": { + "title": "上傳一張人臉", + "description": "您必須先在資料庫中加入至少一張人臉,才能使用人臉辨識功能。" + } + }, + "selectFace": "選擇人臉", + "deleteFaceLibrary": { + "title": "刪除名稱", + "desc": "你確定要刪除 {{name}} 集合嗎?這會刪除所有有關的人臉資料。" + }, + "deleteFaceAttempts": { + "title": "刪除人臉", + "desc_other": "你確定要刪除 {{count}} 個人臉嗎?這個步驟無法復原。" + }, + "renameFace": { + "title": "重新命名人臉", + "desc": "輸入 {{name}} 的新名稱" + }, + "button": { + "deleteFaceAttempts": "刪除人臉", + "addFace": "新增人臉", + "renameFace": "重新命名人臉", + "uploadImage": "上傳圖片", + "reprocessFace": "重新處理人臉", + "deleteFace": "刪除人臉" + }, + "imageEntry": { + "validation": { + "selectImage": "請選擇一個圖片檔。" + }, + "dropActive": "將圖片拖到這裡…", + "dropInstructions": "拖放或貼上圖片至此處,或點擊以選取", + "maxSize": "最大檔案大小:{{size}}MB" + }, + "nofaces": "沒有可用的人臉", + "pixels": "{{area}}px", + "readTheDocs": "閱讀文件", + "trainFaceAs": "將人臉訓練為:", + "trainFace": "訓練人臉", + "toast": { + "success": { + "uploadedImage": "成功上傳圖片。", + "addFaceLibrary": "已成功將 {{name}} 加入至人臉資料庫!", + "deletedFace_other": "成功刪除 {{count}} 個人臉。", + "deletedName_other": "{{count}} 個人臉已成功刪除。", + "renamedFace": "成功將人臉重新命名為 {{name}}", + "trainedFace": "成功訓練人臉。", + "updatedFaceScore": "成功更新人臉分數{{name}}({{score}})。", + "reclassifiedFace": "重新分類人臉成功。" + }, + "error": { + "uploadingImageFailed": "上傳圖片失敗:{{errorMessage}}", + "addFaceLibraryFailed": "設定人臉名稱失敗:{{errorMessage}}", + "deleteFaceFailed": "刪除失敗:{{errorMessage}}", + "deleteNameFailed": "刪除名稱失敗:{{errorMessage}}", + "renameFaceFailed": "重新命名人臉失敗:{{errorMessage}}", + "trainFailed": "訓練失敗:{{errorMessage}}", + "updateFaceScoreFailed": "更新人臉分數失敗:{{errorMessage}}", + "reclassifyFailed": "重新分類人臉失敗:{{errorMessage}}" + } + }, + "reclassifyFaceAs": "將人臉重新分類為:", + "reclassifyFace": "重新分類人臉" +} diff --git a/web/public/locales/zh-Hant/views/live.json b/web/public/locales/zh-Hant/views/live.json new file mode 100644 index 0000000..cb4ed93 --- /dev/null +++ b/web/public/locales/zh-Hant/views/live.json @@ -0,0 +1,206 @@ +{ + "documentTitle": { + "default": "即時監控 - Frigate" + }, + "documentTitle.withCamera": "{{camera}} - 即時畫面 - Frigate", + "lowBandwidthMode": "低流量模式", + "twoWayTalk": { + "enable": "啟用雙向通話", + "disable": "停用雙向通話" + }, + "ptz": { + "move": { + "clickMove": { + "label": "點擊畫面以置中鏡頭", + "enable": "啟用點擊移動", + "disable": "停用點擊移動", + "enableWithZoom": "開啟點選移動 / 拖動縮放功能" + }, + "left": { + "label": "向左移動 PTZ 鏡頭" + }, + "up": { + "label": "向上移動 PTZ 鏡頭" + }, + "down": { + "label": "向下移動 PTZ 鏡頭" + }, + "right": { + "label": "向右移動 PTZ 鏡頭" + } + }, + "zoom": { + "in": { + "label": "放大 PTZ 鏡頭" + }, + "out": { + "label": "縮小 PTZ 鏡頭" + } + }, + "frame": { + "center": { + "label": "點擊畫面以置中 PTZ 鏡頭" + } + }, + "presets": "PTZ 鏡頭預設", + "focus": { + "in": { + "label": "聚焦 PTZ 鏡頭" + }, + "out": { + "label": "離焦 PTZ 鏡頭" + } + } + }, + "cameraAudio": { + "enable": "啟用鏡頭音訊", + "disable": "停用鏡頭音訊" + }, + "camera": { + "enable": "啟用鏡頭", + "disable": "停用鏡頭", + "turnOn": "開啟攝影機", + "turnOff": "關閉攝影機" + }, + "muteCameras": { + "enable": "所有鏡頭靜音", + "disable": "所有鏡頭取消靜音" + }, + "detect": { + "enable": "啟用偵測", + "disable": "停用偵測" + }, + "recording": { + "enable": "啟用錄影", + "disable": "停用錄影", + "disabledInConfig": "必須先在該攝影機的設定中開啟錄製功能。" + }, + "snapshots": { + "enable": "啟用截圖", + "disable": "停用截圖" + }, + "audioDetect": { + "enable": "啟用音訊偵測", + "disable": "停用音訊偵測" + }, + "autotracking": { + "enable": "啟用自動追蹤", + "disable": "停用自動追蹤" + }, + "streamStats": { + "enable": "顯示串流統計資料", + "disable": "隱藏串流統計資料" + }, + "manualRecording": { + "title": "應需", + "tips": "根據此鏡頭的錄影保留設定,下載快照或手動啟動事件。", + "playInBackground": { + "label": "背景播放", + "desc": "啟用此選項以在播放器被隱藏時繼續播放串流。" + }, + "showStats": { + "label": "顯示統計資料", + "desc": "啟用此選項以在鏡頭畫面上顯示串流的統計資料。" + }, + "debugView": "除錯畫面", + "start": "開始應需錄影", + "started": "開始手動應需錄影。", + "failedToStart": "手動開始應需錄影失敗。", + "recordDisabledTips": "因為此鏡頭的錄影功能被停用或限制,因此僅會保存截圖。", + "end": "結束應需錄影", + "ended": "已結束手動應需錄影。", + "failedToEnd": "結束手動應需錄影失敗。" + }, + "streamingSettings": "串流設定", + "notifications": "通知", + "audio": "音訊", + "suspend": { + "forTime": "暫停: " + }, + "stream": { + "title": "串流", + "audio": { + "tips": { + "title": "此串流的音訊必須要從鏡頭輸出並且在 go2rtc 中被設定。", + "documentation": "閱讀文件 " + }, + "available": "此串流支援音訊", + "unavailable": "此串流不支援音訊" + }, + "twoWayTalk": { + "tips": "你的裝置被需支援此功能,並且需設定 WebRTC 以使用雙向通話。", + "tips.documentation": "閱讀文件 ", + "available": "此串流支援雙向通話", + "unavailable": "此串流不支援雙向通話" + }, + "lowBandwidth": { + "tips": "因為緩衝區或串流錯誤,即時畫面已切換至低流量模式。", + "resetStream": "重設串流" + }, + "playInBackground": { + "label": "背景播放", + "tips": "啟用此選項以在播放器被隱藏時繼續播放串流。" + }, + "debug": { + "picker": "除錯模式下無法切換影片流。除錯將始終使用偵測(detect)功能的影片流。" + } + }, + "cameraSettings": { + "title": "{{camera}} 設定", + "cameraEnabled": "鏡頭已啟用", + "objectDetection": "物件偵測", + "recording": "錄影", + "snapshots": "截圖", + "audioDetection": "音訊偵測", + "autotracking": "自動追蹤", + "transcription": "音訊轉錄", + "camera": "攝影機" + }, + "history": { + "label": "顯示歷史影像" + }, + "effectiveRetainMode": { + "modes": { + "all": "全部", + "motion": "移動", + "active_objects": "活躍物件" + }, + "notAllTips": "你的 {{source}} 錄影保留設定為 {{effectiveRetainMode}} 模式,因此此應需錄影僅會保留 {{effectiveRetainModeName}} 片段。" + }, + "editLayout": { + "label": "編輯版面配置", + "group": { + "label": "編輯鏡頭群組" + }, + "exitEdit": "結束編輯" + }, + "transcription": { + "enable": "啟用即時語音轉錄", + "disable": "停用即時語音轉錄" + }, + "snapshot": { + "takeSnapshot": "下載即時快照", + "noVideoSource": "沒有可用的影片資源以擷取快照。", + "captureFailed": "快照擷取失敗。", + "downloadStarted": "已開始下載快照。" + }, + "noCameras": { + "title": "未設定攝影機", + "description": "準備開始連線攝影機至 Frigate 。", + "buttonText": "新增攝影機", + "restricted": { + "title": "無可用攝影機", + "description": "你沒有權限檢視此分組中的任何攝影機。" + }, + "default": { + "title": "沒有配置攝影機", + "description": "現在就將攝影機接入到 Frigate 吧。", + "buttonText": "新增攝影機" + }, + "group": { + "title": "攝影機組目前為空", + "description": "該攝影機組未分配或啟動了攝影機。", + "buttonText": "管理攝影機組" + } + } +} diff --git a/web/public/locales/zh-Hant/views/motionSearch.json b/web/public/locales/zh-Hant/views/motionSearch.json new file mode 100644 index 0000000..7ea301c --- /dev/null +++ b/web/public/locales/zh-Hant/views/motionSearch.json @@ -0,0 +1,78 @@ +{ + "documentTitle": "變動搜尋 - Frigate", + "title": "畫面變動搜尋", + "description": "繪製一個多邊形以劃定感興趣區域,並指定時間範圍,檢索該區域內的動態變化。", + "selectCamera": "畫面變動搜尋正在載入中", + "startSearch": "開始搜尋", + "searchStarted": "搜尋已開始", + "searchCancelled": "搜尋已取消", + "cancelSearch": "取消", + "searching": "搜尋進行中。", + "searchComplete": "搜尋完成", + "noResultsYet": "在所選區域內執行搜尋,查詢異常變化", + "noChangesFound": "所選區域未偵測到像素變化", + "changesFound_other": "偵測到 {{count}} 處畫面變化", + "framesProcessed": "已處理 {{count}} 幀畫面", + "jumpToTime": "跳轉到該時間", + "results": "結果", + "showSegmentHeatmap": "熱力圖", + "newSearch": "新的搜尋", + "clearResults": "清除結果", + "clearROI": "清除多邊形選區", + "polygonControls": { + "points_other": "{{count}} 個點位", + "undo": "撤銷上一個點位", + "reset": "重設多邊形", + "drawMode": "繪製", + "moveMode": "移動" + }, + "motionHeatmapLabel": "畫面變動熱力圖", + "dialog": { + "title": "畫面變動搜尋", + "cameraLabel": "攝影機", + "previewAlt": "{{camera}} 攝影機即時預覽" + }, + "timeRange": { + "title": "搜尋範圍", + "start": "開始時間", + "end": "結束時間" + }, + "settings": { + "title": "搜尋設定", + "parallelMode": "並行模式", + "parallelModeDesc": "同時掃描多個錄影片段(速度更快,但會消耗更多解碼資源)", + "threshold": "靈敏度閾值", + "thresholdDesc": "數值越低,可偵測到越小的變化(取值範圍 1-255)", + "minArea": "最小變化區域", + "minAreaDesc": "最小感興趣區域變化佔比,達到該比例才會判定為有效變動", + "frameSkip": "幀跳過", + "frameSkipDesc": "每隔 N 幀進行一次處理。將該值設定為攝影機的幀率,即可實現每秒處理一幀畫面(例如:5 幀 / 秒的攝影機設為 5,30 幀 / 秒的攝影機設為 30)。數值越高處理速度越快,但有可能遺漏短時移動偵測事件。", + "maxResults": "最大結果數", + "maxResultsDesc": "匹配到設定條數的錄影事件後,就自動停止檢索" + }, + "errors": { + "noCamera": "請選擇攝影機", + "noROI": "請繪製感興趣的區域", + "noTimeRange": "請選擇時間範圍", + "invalidTimeRange": "結束時間必須在開始時間之後", + "searchFailed": "搜尋失敗:{{message}}", + "polygonTooSmall": "多邊形至少需要 3 個頂點", + "unknown": "未知錯誤" + }, + "changePercentage": "{{percentage}}% 已變化", + "metrics": { + "title": "搜尋指標", + "segmentsScanned": "已掃描片段數", + "segmentsProcessed": "已處理", + "segmentsSkippedInactive": "已跳過(無活動)", + "segmentsSkippedHeatmap": "已跳過(不在感興趣區域)", + "fallbackFullRange": "備用全範圍掃描", + "framesDecoded": "畫面已解碼", + "wallTime": "搜尋時間", + "segmentErrors": "片段異常", + "seconds": "{{seconds}} 秒", + "minutesSeconds": "{{minutes}}分 {{seconds}}秒", + "scanSummary": "{{segments}} 個片段 · {{time}}" + }, + "scanning": "掃描中 {{time}}" +} diff --git a/web/public/locales/zh-Hant/views/recording.json b/web/public/locales/zh-Hant/views/recording.json new file mode 100644 index 0000000..1b10c05 --- /dev/null +++ b/web/public/locales/zh-Hant/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "過濾", + "export": "匯出", + "calendar": "日曆", + "filters": "過濾條件", + "toast": { + "error": { + "noValidTimeSelected": "未選擇有效的時間範圍", + "endTimeMustAfterStartTime": "結束時間需晚於開始時間" + } + } +} diff --git a/web/public/locales/zh-Hant/views/replay.json b/web/public/locales/zh-Hant/views/replay.json new file mode 100644 index 0000000..afe2cee --- /dev/null +++ b/web/public/locales/zh-Hant/views/replay.json @@ -0,0 +1,59 @@ +{ + "title": "除錯回放", + "description": "回放攝影機錄影以供除錯。目標清單會延時展示已偵測目標的彙總資訊,訊息分頁則即時展示回放錄影對應的 Frigate 內部日誌資訊流。", + "websocket_messages": "訊息", + "dialog": { + "title": "開始除錯回放", + "description": "建立臨時回放攝影機,迴圈播放歷史錄製影片,用於除錯目標偵測與追蹤相關問題。臨時回放的攝影機將沿用原攝影機的偵測配置。請選擇一個時間範圍開始。", + "camera": "原攝影機", + "timeRange": "時間範圍", + "preset": { + "1m": "最後 1 分鐘", + "5m": "最後 5 分鐘", + "timeline": "從時間線", + "custom": "自訂" + }, + "startButton": "開始回放", + "selectFromTimeline": "選擇", + "starting": "開始回放…", + "startLabel": "開始", + "endLabel": "結束", + "toast": { + "error": "除錯回放啟動失敗:{{error}}", + "alreadyActive": "已有回放工作階段正在執行", + "stopError": "除錯回放停止失敗:{{error}}", + "goToReplay": "進入回放" + } + }, + "page": { + "noSession": "沒有正在進行的除錯回放工作階段", + "noSessionDesc": "從歷史回放頁面啟動除錯回放:點選工具列中的操作按鈕,選擇除錯回放即可。", + "goToRecordings": "檢視歷史記錄", + "preparingClip": "正在準備片段…", + "preparingClipDesc": "Frigate 正在拼接所選時間範圍的錄影片段。時間跨度較大時,該過程可能需要一分鐘左右。", + "startingCamera": "開始除錯回放中…", + "startError": { + "title": "除錯回放啟動失敗", + "back": "返回歷史記錄" + }, + "sourceCamera": "源攝影機", + "replayCamera": "回放攝影機", + "initializingReplay": "初始化除錯回放中…", + "stoppingReplay": "正在停止除錯回放…", + "stopReplay": "停止回放", + "confirmStop": { + "title": "要停止除錯回放嗎?", + "description": "這將終止工作階段並清除所有臨時資料。是否確定?", + "confirm": "停止回放", + "cancel": "取消" + }, + "activity": "活動", + "objects": "目標清單", + "audioDetections": "音訊偵測", + "noActivity": "未偵測到活動", + "activeTracking": "活動追蹤中", + "noActiveTracking": "沒有活動追蹤", + "configuration": "配置", + "configurationDesc": "微調除錯回放攝影機的移動偵測與目標追蹤引數。本次調整不會儲存到你的 Frigate 設定檔中。" + } +} diff --git a/web/public/locales/zh-Hant/views/search.json b/web/public/locales/zh-Hant/views/search.json new file mode 100644 index 0000000..7fe475e --- /dev/null +++ b/web/public/locales/zh-Hant/views/search.json @@ -0,0 +1,73 @@ +{ + "search": "搜尋", + "savedSearches": "已儲存的搜尋", + "searchFor": "搜尋 {{inputValue}}", + "button": { + "clear": "清空搜尋", + "filterActive": "過濾中", + "save": "儲存搜尋", + "delete": "刪除保存的搜尋", + "filterInformation": "過濾資訊" + }, + "trackedObjectId": "追蹤物件編號", + "filter": { + "label": { + "cameras": "鏡頭", + "labels": "標籤", + "zones": "區域", + "sub_labels": "子標籤", + "search_type": "搜尋類型", + "before": "結束時間", + "after": "開始時間", + "min_score": "最低分數", + "max_score": "最高分數", + "min_speed": "最低速度", + "max_speed": "最高速度", + "recognized_license_plate": "已辨識的車牌", + "has_clip": "包含片段", + "has_snapshot": "包含截圖", + "time_range": "時間範圍", + "attributes": "屬性" + }, + "searchType": { + "thumbnail": "截圖", + "description": "說明" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "「結束」日期必須要在「開始」日期之後。", + "afterDatebeEarlierBefore": "「開始」日期需要在「結束」日期之前。", + "minScoreMustBeLessOrEqualMaxScore": "「最低分數」必須小於或等於「最高分數」。", + "maxScoreMustBeGreaterOrEqualMinScore": "「最高分數」必須要大於或等於「最低分數」。", + "minSpeedMustBeLessOrEqualMaxSpeed": "「最低速度」必須要小於或等於「最高速度」。", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "「最高速度」必須要大於或等於「最低速度」。" + } + }, + "tips": { + "title": "如何使用文字過濾", + "desc": { + "text": "過濾功能可以幫助你縮小搜尋範圍。以下是使用方法:", + "step1": "輸入過濾標的名稱後加上冒號(例如:\"cameras:\")。", + "step2": "從建議中選擇一個值,或者自行輸入。", + "step3": "若有多個過濾條件可以使用空格隔開。", + "step4": "過濾日期(before: 以及 after:)需使用 {{DateFormat}} 格式。", + "step5": "過濾時間範圍時需使用 {{exampleTime}} 格式。", + "step6": "點擊旁邊的「x」可以移除對應的過濾條件。", + "exampleLabel": "範例:" + } + }, + "header": { + "currentFilterType": "過濾內容", + "noFilters": "過濾條件", + "activeFilters": "已套用的過濾條件" + } + }, + "similaritySearch": { + "title": "相似搜尋", + "active": "已啟用相似搜尋", + "clear": "清空相似搜尋" + }, + "placeholder": { + "search": "搜尋…" + } +} diff --git a/web/public/locales/zh-Hant/views/settings.json b/web/public/locales/zh-Hant/views/settings.json new file mode 100644 index 0000000..2d25590 --- /dev/null +++ b/web/public/locales/zh-Hant/views/settings.json @@ -0,0 +1,1968 @@ +{ + "documentTitle": { + "default": "設定 - Frigate", + "authentication": "認證設定 - Frigate", + "camera": "鏡頭設定 - Frigate", + "enrichments": "進階功能設定 - Frigate", + "general": "使用者介面設定 - Frigate", + "frigatePlus": "Frigate+ 設定 - Frigate", + "notifications": "通知設定 - Frigate", + "masksAndZones": "遮罩與區域編輯器 - Frigate", + "motionTuner": "移動偵測調教器 - Frigate", + "object": "除錯 - Frigate", + "cameraManagement": "管理鏡頭 - Frigate", + "cameraReview": "相機預覽設置 - Frigate", + "globalConfig": "全域性配置 - Frigate", + "cameraConfig": "攝影機配置 - Frigate", + "detectorsAndModel": "偵測器與模型 - Frigate", + "maintenance": "維護 - Frigate", + "profiles": "設定檔 - Frigate" + }, + "menu": { + "ui": "使用者介面", + "enrichments": "進階功能", + "cameras": "鏡頭設定", + "masksAndZones": "遮罩 / 區域", + "motionTuner": "移動偵測調教器", + "debug": "除錯", + "users": "使用者", + "notifications": "通知", + "frigateplus": "Frigate+", + "triggers": "觸發", + "cameraManagement": "管理", + "cameraReview": "預覽", + "roles": "角色", + "general": "常規", + "globalConfig": "全域性配置", + "system": "系統", + "integrations": "整合", + "uiSettings": "介面設定", + "profiles": "設定檔", + "globalDetect": "目標偵測", + "globalRecording": "錄製", + "globalSnapshots": "快照", + "globalFfmpeg": "FFmpeg", + "globalMotion": "畫面變動偵測", + "globalObjects": "目標", + "globalReview": "審閱", + "globalAudioEvents": "音訊偵測", + "globalLivePlayback": "即時監控觀看", + "globalTimestampStyle": "時間戳樣式", + "systemDatabase": "資料庫", + "systemTls": "TLS加密連結", + "systemAuthentication": "驗證", + "systemNetworking": "網路", + "systemProxy": "代理", + "systemUi": "使用者介面", + "systemLogging": "日誌", + "systemEnvironmentVariables": "環境變數", + "systemTelemetry": "遙測", + "systemBirdseye": "鳥瞰圖", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "偵測器與模型", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "go2rtc 影片流", + "integrationSemanticSearch": "語意搜尋", + "integrationGenerativeAi": "生成式 AI", + "integrationFaceRecognition": "人臉辨識", + "integrationLpr": "車牌辨識", + "integrationObjectClassification": "目標分類", + "integrationAudioTranscription": "音訊轉錄", + "cameraDetect": "目標偵測", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "錄製", + "cameraSnapshots": "快照", + "cameraMotion": "畫面變動偵測", + "cameraObjects": "目標", + "cameraConfigReview": "審閱", + "cameraAudioEvents": "音訊偵測", + "cameraAudioTranscription": "音訊轉錄", + "cameraNotifications": "通知", + "cameraLivePlayback": "即時監控觀看", + "cameraBirdseye": "鳥瞰圖", + "cameraFaceRecognition": "人臉辨識", + "cameraLpr": "車牌辨識", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "攝影機頁面", + "cameraTimestampStyle": "時間戳樣式", + "cameraMqtt": "攝影機 MQTT", + "maintenance": "維護", + "mediaSync": "媒體同步", + "regionGrid": "區域網格" + }, + "dialog": { + "unsavedChanges": { + "title": "你有未保存的變更。", + "desc": "再繼續之前,你想先儲存你的變更嗎?" + } + }, + "cameraSetting": { + "camera": "鏡頭", + "noCamera": "沒有鏡頭" + }, + "general": { + "title": "使用者介面設定", + "liveDashboard": { + "title": "即時監控面板", + "automaticLiveView": { + "label": "自動即時檢視", + "desc": "在偵測到移動時自動切換至即時影像。停用此設定將使得在即時監控面板上的靜態畫面每分鐘更新一次。" + }, + "playAlertVideos": { + "label": "播放警報影片", + "desc": "最近的警報影片預設會在即時監控面板中連續循環播放。取消這個選項,可以只顯示靜態的最近警報擷圖(僅套用於該裝置/瀏覽器)。" + }, + "displayCameraNames": { + "label": "總是顯示鏡頭名稱", + "desc": "總是在多鏡頭直播頁面顯示鏡頭的名稱標籤。" + }, + "liveFallbackTimeout": { + "label": "直播播放器回退逾時", + "desc": "當高畫質串流直播無法使用時,在此秒數後會回退成低流量模式。預設值: 3。" + } + }, + "storedLayouts": { + "title": "儲存的排版", + "desc": "在鏡頭群組內的鏡頭排版可以拖拉或縮放調整。這個排版設定儲存於目前瀏覽器的本機儲存空間。", + "clearAll": "清除所有排版" + }, + "cameraGroupStreaming": { + "title": "鏡頭群組串流播放設定", + "desc": "每個鏡頭群組的串流播放設定都儲存在目前瀏覽器的本機儲存空間。", + "clearAll": "清除所有串流播放設定" + }, + "recordingsViewer": { + "title": "錄影檢視器", + "defaultPlaybackRate": { + "label": "預設播放速度", + "desc": "錄影回放的預設播放速度。" + } + }, + "calendar": { + "title": "月曆", + "firstWeekday": { + "label": "第一個工作天", + "desc": "在檢視月曆中,每個禮拜從禮拜幾開始。", + "sunday": "禮拜天", + "monday": "禮拜一" + } + }, + "toast": { + "success": { + "clearStoredLayout": "清除 {{cameraName}} 儲存的排版", + "clearStreamingSettings": "清除所有鏡頭群組的串流播放設定。" + }, + "error": { + "clearStoredLayoutFailed": "清除儲存的排版設定失敗: {{errorMessage}}", + "clearStreamingSettingsFailed": "清除串流播放設定失敗: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "進階功能設定", + "unsavedChanges": "尚未儲存的強化設定變更", + "semanticSearch": { + "modelSize": { + "label": "模型大小", + "small": { + "title": "小", + "desc": "使用 small 將採用量化版本的模型。該模型佔用的記憶體(RAM)較少,在 CPU 上的執行速度也較快,且在嵌入向量品質上的差異微乎其微。" + }, + "desc": "用於語意搜尋的語言模型大小。", + "large": { + "title": "大", + "desc": "使用 large 將採用完整的 Jina 模型,並在支援的情況下自動於 GPU 上執行。" + } + }, + "title": "語意搜尋", + "desc": "Frigate 中的語意搜尋功能可讓您使用圖像本身、使用者定義的文字描述或自動產生的描述,在審核專案中尋找追蹤物件。", + "reindexNow": { + "label": "立即重新索引", + "desc": "重新索引會為所有追蹤物件重新產生嵌入向量。此過程在背景運行,可能會佔用大量 CPU 資源,並且耗時較長,具體取決於追蹤物件的數量。", + "confirmTitle": "確認重建索引", + "confirmDesc": "確定要為所有追蹤目標重建特徵向量索引資訊嗎?此過程將在後臺進行,但可能會導致CPU滿載並耗費較長時間。您可以在 瀏覽 頁面檢視進度。", + "confirmButton": "重建索引", + "success": "重建索引已成功啟動。", + "alreadyInProgress": "重建索引已在執行中。", + "error": "啟動重建索引失敗:{{errorMessage}}" + } + }, + "faceRecognition": { + "title": "人臉識別", + "desc": "人臉辨識功能允許為人物分配名稱,當辨識到他們的面孔時,Frigate 會將人物的名字作為子標籤進行分配。這些資訊會顯示在介面、過濾器以及通知中。", + "modelSize": { + "label": "模型大小", + "desc": "用於人臉辨識的模型大小。", + "small": { + "title": "小", + "desc": "使用 模型將採用 FaceNet 臉部嵌入模型,可在大多數 CPU 上高效執行。" + }, + "large": { + "title": "大", + "desc": "使用 模型將採用 ArcFace 臉部嵌入模型,並在適用情況下自動於 GPU 上執行。" + } + } + }, + "birdClassification": { + "title": "鳥類分類", + "desc": "鳥類分類功能使用量化的 TensorFlow 模型識別已知鳥類。識別出已知鳥類後,其通用名稱將作為子標籤添加。此資訊會顯示在使用者介面、篩選器以及通知中。" + }, + "licensePlateRecognition": { + "title": "車牌辨識", + "desc": "Frigate 可以辨識車輛的車牌,並自動將偵測到的字元新增到 辨識的車牌(recognized_license_plate)欄位中,或將已知車牌對應的名稱作為子標籤新增到該車輛目標中。該功能常用於辨識駛入車道的車輛車牌或經過街道的車輛車牌。" + }, + "restart_required": "需要重啟(增強功能設定已儲存)", + "toast": { + "success": "增強功能設定已儲存。請重啟 Frigate 以應用更改。", + "error": "配置更改儲存失敗:{{errorMessage}}" + } + }, + "cameraWizard": { + "title": "新增相機", + "testResultLabels": { + "resolution": "解析度", + "video": "影像", + "audio": "語音", + "fps": "幀率" + }, + "commonErrors": { + "testFailed": "串流測試失敗: {{error}}", + "noUrl": "請提供正確的影片流地址" + }, + "step1": { + "description": "輸入相機詳細資訊並選擇自動偵測或手動選擇相機品牌。", + "cameraName": "相機名稱", + "cameraNamePlaceholder": "例: 前門 / 後院", + "host": "主機/IP 位置", + "port": "埠", + "username": "用戶名稱", + "usernamePlaceholder": "選填", + "password": "密碼", + "passwordPlaceholder": "選填", + "selectTransport": "選擇協議", + "cameraBrand": "相機品牌", + "selectBrand": "選擇攝影機品牌用於生成URL地址模板", + "customUrl": "自訂影片流地址", + "brandInformation": "品牌資訊", + "brandUrlFormat": "對於採用RTSP URL格式的攝影機,其格式為:{{exampleUrl}}", + "customUrlPlaceholder": "rtsp://使用者名稱:密碼@主機或ip位址:通訊埠/路徑", + "connectionSettings": "連線設定", + "detectionMethod": "影片流偵測方法", + "onvifPort": "ONVIF 埠", + "probeMode": "探測攝影機", + "manualMode": "手動選擇", + "detectionMethodDescription": "如果攝影機支援 ONVIF 協議,將使用該協議探測攝影機,以自動獲取攝影機影片流地址;若不支援,也可手動選擇攝影機品牌來使用預設地址。如需輸入自訂RTSP地址,請選擇“手動選擇”並選擇“其他”選項。", + "onvifPortDescription": "對於支援ONVIF協議的攝影機,該埠通常為80或8080。", + "useDigestAuth": "使用摘要認證", + "useDigestAuthDescription": "為 ONVIF 協議啟用 HTTP 摘要認證。部分攝影機可能需要專用的 ONVIF 使用者名稱/密碼,而非預設的 admin 帳戶。", + "errors": { + "brandOrCustomUrlRequired": "請選擇攝影機品牌並配置主機/ IP 地址,或選擇“其他”後手動配置影片流地址", + "nameRequired": "攝影機名稱為必填項", + "nameLength": "攝影機名稱要少於64個字元", + "invalidCharacters": "攝影機名稱內有不允許使用的字元", + "nameExists": "該攝影機名稱已存在", + "customUrlRtspRequired": "自訂 URL 必須以“rtsp://”開頭;對於非 RTSP 協議的攝影機流,需手動新增至設定檔。" + } + }, + "description": "請按照以下步驟新增攝影機至 Frigate 中。", + "steps": { + "nameAndConnection": "名稱與連線", + "probeOrSnapshot": "探測或快照", + "streamConfiguration": "影片流配置", + "validationAndTesting": "驗證與測試" + }, + "save": { + "success": "已儲存新攝影機 {{cameraName}}。", + "failure": "儲存攝影機 {{cameraName}} 遇到了錯誤。" + }, + "step2": { + "description": "將根據你選擇的偵測方式,將會自動查詢攝影機可用流配置,或進行手動配置。", + "testSuccess": "影片流測試成功!", + "testFailed": "連線測試失敗,請檢查您的輸入後重試。", + "testFailedTitle": "測試失敗", + "streamDetails": "影片流詳情", + "probing": "正在偵測攝影機中……", + "retry": "重試", + "testing": { + "probingMetadata": "正在查詢攝影機引數……", + "fetchingSnapshot": "正在獲取攝影機快照……" + }, + "probeFailed": "偵測攝影機失敗:{{error}}", + "probingDevice": "尋找裝置中……", + "probeSuccessful": "偵測成功", + "probeError": "偵測遇到錯誤", + "probeNoSuccess": "偵測未成功", + "deviceInfo": "裝置資訊", + "manufacturer": "製造商", + "model": "型號", + "firmware": "韌體", + "profiles": "設定檔", + "ptzSupport": "支援 PTZ", + "autotrackingSupport": "支援自動追蹤", + "presets": "預設配置", + "rtspCandidates": "RTSP候選地址", + "rtspCandidatesDescription": "透過攝影機自動偵測發現了以下RTSP地址。測試連線以檢視影片流引數。", + "noRtspCandidates": "未從攝影機偵測到任何 RTSP 地址。可能是你的帳號密碼錯誤,或者攝影機不支援 ONVIF 協議,亦或是當前採用的 RTSP 地址獲取方式無效。請返回上一步,嘗試手動輸入RTSP地址。", + "candidateStreamTitle": "候選{{number}}", + "useCandidate": "使用", + "uriCopy": "複製", + "uriCopied": "地址已複製到剪貼簿", + "testConnection": "測試連線", + "toggleUriView": "點選切換完整 URI 顯示", + "connected": "已連線", + "notConnected": "未連線", + "errors": { + "hostRequired": "主機/IP地址為必填" + } + }, + "step3": { + "description": "為你的攝影機配置影片流功能並新增額外影片流。", + "streamsTitle": "攝影機影片流", + "addStream": "新增影片流", + "addAnotherStream": "新增其他影片流", + "streamTitle": "{{number}} 號影片流", + "streamUrl": "影片流地址", + "streamUrlPlaceholder": "rtsp://使用者名稱:密碼@主機:埠/路徑", + "selectStream": "選擇一個影片流", + "searchCandidates": "搜尋候選項……", + "noStreamFound": "沒有找到影片流", + "url": "URL地址", + "resolution": "解析度", + "selectResolution": "選擇解析度", + "quality": "品質", + "selectQuality": "選擇品質", + "roles": "功能", + "roleLabels": { + "detect": "目標偵測", + "record": "錄製", + "audio": "音訊偵測" + }, + "testStream": "測試連線", + "testSuccess": "影片流測試成功!", + "testFailed": "影片流測試失敗", + "testFailedTitle": "測試失敗", + "connected": "已連線", + "notConnected": "未連線", + "featuresTitle": "功能特性", + "go2rtc": "減少與攝影機的連線數", + "detectRoleWarning": "必須得有一個影片流設定了“偵測”功能才能繼續操作。", + "rolesPopover": { + "title": "影片流功能", + "detect": "用於目標偵測的主碼流。", + "record": "根據配置設定儲存影片流片段。", + "audio": "用於音訊偵測的音影片流。" + }, + "featuresPopover": { + "title": "影片流功能特性", + "description": "使用 go2rtc 中繼轉流功能,減少與攝影機的網路連線數,提升效率。" + } + }, + "step4": { + "description": "將進行儲存新攝影機配置前的最終驗證與分析,請在儲存前確保所有影片流均已連線。", + "validationTitle": "影片流驗證", + "connectAllStreams": "連線所有影片流", + "reconnectionSuccess": "重新連線成功。", + "reconnectionPartial": "部分影片流重新連線失敗。", + "streamUnavailable": "影片流預覽不可用", + "reload": "重新載入", + "connecting": "連線中……", + "streamTitle": "影片流 {{number}}", + "valid": "透過", + "failed": "失敗", + "notTested": "未測試", + "connectStream": "連線", + "connectingStream": "連線中", + "disconnectStream": "斷開連線", + "estimatedBandwidth": "預估頻寬", + "roles": "功能", + "ffmpegModule": "使用影片流相容模式", + "ffmpegModuleDescription": "若多次嘗試後仍無法載入影片流,可嘗試啟用此功能。啟用後,Frigate 將透過 go2rtc 呼叫 ffmpeg 模組。這可能會提升與部分攝影機影片流的相容性。", + "none": "無", + "error": "錯誤", + "streamValidated": "影片流 {{number}} 驗證成功", + "streamValidationFailed": "影片流 {{number}} 驗證失敗", + "saveAndApply": "儲存新攝影機", + "saveError": "配置無效,請檢查您的設定。", + "issues": { + "title": "影片流驗證", + "videoCodecGood": "影片編解碼器為 {{codec}}。", + "audioCodecGood": "音訊編解碼器為 {{codec}}。", + "resolutionHigh": "使用 {{resolution}} 解析度可能導致資源使用率增加。", + "resolutionLow": "{{resolution}} 解析度可能過低,難以可靠偵測小型目標或物體。", + "resolutionUnknown": "無法偵測此影片流的解析度。你需要在設定或設定檔中手動指定偵測解析度。", + "noAudioWarning": "偵測到該影片流無音訊訊號,錄製影片將沒有聲音。", + "audioCodecRecordError": "錄製功能需要 AAC 音訊編解碼器以實現音訊支援。", + "audioCodecRequired": "要實現音訊偵測功能,必須要有音訊流。", + "restreamingWarning": "為錄製流開啟“減少與攝影機的連線數”可能會略微增加 CPU 使用率。", + "brands": { + "reolink-rtsp": "不建議使用 Reolink 的 RTSP 協議。請在攝影機後臺設定中啟用 HTTP協議,並重新啟動向導。", + "reolink-http": "Reolink HTTP 影片流應該使用 FFmpeg 以獲得更好的相容性,為此影片流啟用“使用流相容模式”。" + }, + "dahua": { + "substreamWarning": "子碼流1當前被鎖定為低解析度。多數大華、安訊士、EmpireTech品牌的攝影機都支援額外的子碼流,這些子碼流需要在攝影機設定中手動啟用。如果你的裝置支援,建議你檢查並使用這些高解析度子碼流。" + }, + "hikvision": { + "substreamWarning": "子碼流1當前被鎖定為低解析度。多數海康威視的攝影機都支援額外的子碼流,這些子碼流需要在攝影機設定中手動啟用。如果你的裝置支援,建議你檢查並使用這些高解析度子碼流。" + } + } + } + }, + "triggers": { + "toast": { + "error": { + "deleteTriggerFailed": "刪除觸發器失敗:{{errorMessage}}", + "updateTriggerFailed": "更新觸發器失敗:{{errorMessage}}", + "createTriggerFailed": "建立觸發器失敗:{{errorMessage}}" + }, + "success": { + "createTrigger": "觸發器 {{name}} 建立成功。", + "updateTrigger": "觸發器 {{name}} 更新成功。", + "deleteTrigger": "觸發器 {{name}} 已刪除。" + } + }, + "documentTitle": "觸發器", + "semanticSearch": { + "title": "語意搜尋已關閉", + "desc": "必須啟用語意搜尋功能才能使用觸發器。" + }, + "management": { + "title": "觸發器", + "desc": "管理 {{camera}} 的觸發器。你可以選擇“縮圖”型別,將透過與追蹤目標相似的縮圖來觸發;也可以透過“描述”型別,與你描述的文字相似來觸發(中文描述需要使用 jina v2模型,對配置要求更高)。" + }, + "addTrigger": "新增觸發器", + "table": { + "name": "名稱", + "type": "型別", + "content": "觸發內容", + "threshold": "閾值", + "actions": "動作", + "noTriggers": "此攝影機未配置任何觸發器。", + "edit": "編輯", + "deleteTrigger": "刪除觸發器", + "lastTriggered": "最後一個觸發項" + }, + "type": { + "thumbnail": "縮圖", + "description": "描述" + }, + "actions": { + "notification": "傳送通知", + "sub_label": "新增子標籤", + "attribute": "新增屬性" + }, + "dialog": { + "createTrigger": { + "title": "建立觸發器", + "desc": "為攝影機 {{camera}} 建立觸發器" + }, + "editTrigger": { + "title": "編輯觸發器", + "desc": "編輯攝影機 {{camera}} 的觸發器設定" + }, + "deleteTrigger": { + "title": "刪除觸發器", + "desc": "你確定要刪除觸發器 {{triggerName}} 嗎?此操作不可撤銷。" + }, + "form": { + "name": { + "title": "名稱", + "placeholder": "觸發器名稱", + "description": "請輸入用於辨識此觸發器的唯一名稱或描述", + "error": { + "minLength": "該欄位至少需要兩個字元。", + "invalidCharacters": "該欄位只能包含字母、數字、下劃線和連字元。", + "alreadyExists": "此攝影機已存在同名觸發器。" + } + }, + "enabled": { + "description": "開啟/關閉此觸發器" + }, + "type": { + "title": "型別", + "placeholder": "選擇觸發型別", + "description": "當偵測到相似的追蹤目標描述時觸發", + "thumbnail": "當偵測到相似的追蹤目標縮圖時觸發" + }, + "content": { + "title": "內容", + "imagePlaceholder": "選擇圖片", + "textPlaceholder": "輸入文字內容", + "imageDesc": "僅顯示最近的 100 張縮圖。如果找不到需要的圖片,請前往“瀏覽”頁面檢視更早的目標,並從選單中設定觸發器。", + "textDesc": "輸入文字,當偵測到相似的追蹤目標描述時觸發此操作。", + "error": { + "required": "內容為必填項。" + } + }, + "threshold": { + "title": "閾值", + "desc": "設定此觸發器的相似度閾值。閾值越高,觸發所需的匹配就越精確。", + "error": { + "min": "閾值必須大於 0", + "max": "閾值必須小於 1" + } + }, + "actions": { + "title": "動作", + "desc": "預設情況下,Frigate 會為所有觸發器傳送 MQTT 訊息。子標籤會將觸發器名稱新增到目標標籤中。屬性是可搜尋的元資料,獨立儲存在追蹤目標的元資料中。", + "error": { + "min": "必須至少選擇一項動作。" + } + } + } + }, + "wizard": { + "title": "建立觸發器", + "step1": { + "description": "配置觸發器的基礎設定。" + }, + "step2": { + "description": "設定觸發此操作的內容。" + }, + "step3": { + "description": "配置此觸發器的相似度閾值與執行動作。" + }, + "steps": { + "nameAndType": "名稱與型別", + "configureData": "配置資料", + "thresholdAndActions": "閾值與動作" + } + } + }, + "button": { + "overriddenGlobal": "已覆蓋全域性通用配置", + "overriddenGlobalTooltip": "當前攝影機配置,將優先覆蓋全域性通用設定", + "overriddenGlobalHeading_other": "此攝影機覆蓋了全域性設定中的 {{count}} 個欄位:", + "overriddenGlobalNoDeltas": "此攝影機覆蓋了全域性設定,但所有欄位值都相同。", + "overriddenBaseConfig": "已覆蓋預設配置", + "overriddenBaseConfigTooltip": "當前 {{profile}} 設定檔會覆蓋本節所有設定", + "overriddenBaseConfigHeading_other": "{{profile}} 設定檔覆蓋了基礎設定中的 {{count}} 個欄位:", + "overriddenBaseConfigNoDeltas": "{{profile}} 設定檔覆蓋了此區段,但所有欄位值與基礎設定相同。", + "overriddenInCameras": { + "label_other": "已在 {{count}} 個攝影機中單獨配置", + "tooltip_other": "{{count}} 個攝影機在此項中存在單獨配置,點選檢視詳情。", + "heading_other": "此全域性設定項下有 {{count}} 個攝影機存在自訂單獨配置。", + "othersField_other": "其餘 {{count}} 個", + "profilePrefix": "{{profile}} 配置方案:{{fields}}" + } + }, + "saveAllPreview": { + "title": "未儲存的更改", + "triggerLabel": "檢視待處理的更改", + "empty": "沒有待處理的更改。", + "scope": { + "label": "作用範圍", + "global": "全域性", + "camera": "攝影機:{{cameraName}}" + }, + "profile": { + "label": "配置" + }, + "field": { + "label": "欄位" + }, + "value": { + "label": "新值", + "reset": "重設" + } + }, + "cameraManagement": { + "title": "管理攝影機", + "description": "新增、編輯和刪除攝影機,控制哪些攝影機已啟用,並設定按設定檔與攝影機類型的覆蓋。若要設定串流、偵測、動作及其他攝影機特定設定,請在「攝影機設定」下選擇對應的區段。", + "addCamera": "新增新攝影機", + "deleteCamera": "刪除攝影機", + "deleteCameraDialog": { + "title": "刪除攝影機", + "description": "刪除攝影機將永久移除該攝影機的所有錄影、跟蹤目標以及配置。任何與該攝影機關聯的 go2rtc 流可能仍需手動刪除。", + "selectPlaceholder": "選擇攝影機…", + "confirmTitle": "你確定嗎?", + "confirmWarning": "刪除 {{cameraName}} 後將無法撤銷。", + "deleteExports": "同時刪除該攝影機匯出的影片", + "confirmButton": "永久刪除", + "success": "攝影機 {{cameraName}} 刪除完成", + "error": "刪除攝影機 {{cameraName}} 失敗" + }, + "editCamera": "編輯攝影機:", + "selectCamera": "選擇攝影機", + "backToSettings": "返回攝影機設定", + "streams": { + "title": "開啟或關閉攝影機", + "enableLabel": "開啟攝影機", + "enableDesc": "暫時停用已開啟的攝影機,直到 Frigate 重啟。停用攝影機會完全停止 Frigate 對該攝影機影片流的處理。偵測、錄影和除錯功能將不可用。
    注意:這不會停用 go2rtc 的轉推流。", + "disableLabel": "關閉攝影機", + "disableDesc": "開啟在當前在介面中不可見且在配置中被停用的攝影機。啟用後需要重啟 Frigate 才能生效。", + "enableSuccess": "已在配置中啟用 {{cameraName}}。請重啟 Frigate 以應用更改。", + "friendlyName": { + "edit": "修改攝影機顯示名稱", + "title": "修改顯示名稱", + "description": "設定該攝像機在 Frigate 使用者介面中顯示的名稱。若留空,則使用攝像機 ID。", + "rename": "重新命名" + }, + "label": "攝影機狀態", + "description": "設定各攝影機的運作狀態。

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

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

    拖曳控制鍵可調整現用攝影機在整個 UI 中顯示的順序,包括即時儀表板和攝影機選擇下拉選單。", + "disabledSubheading": "在設定中禁用", + "status": { + "on": "開啟", + "off": "關閉", + "disabled": "已停用" + }, + "disableSuccess": "已停用 {{cameraName}} 並儲存至設定。", + "reorderHandle": "拖曳以重新排序", + "saving": "儲存中…", + "saved": "已儲存", + "details": { + "edit": "編輯攝影機詳細資料", + "title": "編輯攝影機詳細資料", + "description": "更新此攝影機在整個 Frigate UI 中使用的顯示名稱、外部 URL 和可見性。", + "friendlyNameLabel": "顯示名稱", + "friendlyNameHelp": "在整個 Frigate UI 中顯示此攝影機的顯示名稱。留空則會使用攝影機 ID。", + "webuiUrlLabel": "攝影機網頁介面網址", + "webuiUrlHelp": "可從「偵錯」檢視中直接造訪攝影機網頁介面的網址。留空以停用此連結。", + "webuiUrlInvalid": "必須是有效的網址(例如:https://example.com)。", + "dashboardLabel": "在即時儀表板上顯示", + "dashboardHelp": "在即時儀表板上顯示此攝影機。", + "reviewLabel": "在審查中顯示", + "reviewHelp": "在「審查」中顯示此攝影機,包括攝影機篩選器、移動審查以及歷史檢視。" + } + }, + "cameraConfig": { + "add": "新增攝影機", + "edit": "編輯攝影機", + "description": "配置攝影機設定,包括影片流輸入和功能選擇。", + "name": "攝影機名稱", + "nameRequired": "攝影機名稱為必填項", + "nameLength": "攝影機名稱必須少於64個字元。", + "namePlaceholder": "例如:大門、後院等", + "enabled": "開啟", + "ffmpeg": { + "inputs": "影片流輸入", + "path": "影片流地址", + "pathRequired": "影片流地址為必填項", + "pathPlaceholder": "rtsp://...", + "roles": "功能", + "rolesRequired": "至少選擇一個功能", + "rolesUnique": "每個功能(音訊audio、偵測detect、錄製record)只能分配給一個影片流", + "addInput": "新增輸入影片流", + "removeInput": "移除輸入影片流", + "inputsRequired": "至少需要一個輸入影片流" + }, + "go2rtcStreams": "go2rtc 影片流", + "streamUrls": "影片流地址", + "addUrl": "新增地址", + "addGo2rtcStream": "新增 go2rtc 影片流", + "toast": { + "success": "攝影機 {{cameraName}} 已儲存" + } + }, + "profiles": { + "title": "設定檔的攝影機覆蓋項", + "selectLabel": "選擇設定檔", + "description": "配置在啟用某個設定檔時,哪些攝影機應被開啟或關閉。設定為“繼承”的攝影機會沿用它原本的啟用/停用狀態。", + "inherit": "繼承", + "enabled": "開啟", + "disabled": "關閉", + "on": "開啟", + "off": "關閉" + }, + "cameraType": { + "title": "攝影機型別", + "label": "攝影機型別", + "description": "為每路攝影機設定型別。專用車牌辨識(LPR)攝影機為單用途裝置,配備高倍光學變焦,可抓拍遠處車輛的車牌。絕大多數攝影機應選用通用型別;只有專為車牌辨識部署、且畫面聚焦對準車牌的攝影機,才需選擇專用 LPR 型別。", + "normal": "通用", + "dedicatedLpr": "車牌辨識專用", + "saveSuccess": "已更新 {{cameraName}} 的攝影機型別,請重啟 Frigate 以使更改生效。" + }, + "clone": { + "sectionTitle": "複製設定", + "sectionDescription": "將攝影機的設定複製到另一台攝影機或新攝影機。", + "button": "複製設定", + "title": "複製攝影機設定", + "description": "將攝影機的設定複製到一或多台其他攝影機,或是新攝影機。識別資料(名稱、顯示名稱、網頁介面網址、顯示順序)將不會被複製。", + "source": { + "label": "來源攝影機", + "placeholder": "選擇一個來源攝影機", + "required": "選擇一個來源攝影機" + }, + "target": { + "legend": "目標", + "newRadio": "新攝影機", + "newNameLabel": "攝影機名稱", + "newNamePlaceholder": "例如:back_door 或 Back Door", + "newNameRequired": "必須填寫攝影機名稱", + "newNameInvalid": "無效的攝影機名稱", + "newNameCollision": "已存在相同名稱的攝影機", + "newStreamsForced": "建立新攝影機時,一律會複製串流設定。", + "existingCamerasRadio": "現有攝影機", + "allCameras": "所有攝影機", + "existingPlaceholder": "請至少選擇一台攝影機", + "existingDisabled": "沒有其他攝影機可供複製" + }, + "categories": { + "legend": "要複製的設定", + "description": "選擇要從來源攝影機複製哪些設定。", + "selectAll": "全選", + "selectNone": "取消全選", + "resetDefaults": "重設為預設值", + "general": "一般", + "spatial": "空間設定", + "streams": "串流", + "spatialWarningTitle": "解析度不符", + "spatialWarning": "來源攝影機 {{srcCamera}} 的偵測解析度 ({{srcWidth}}×{{srcHeight}}) 與以下攝影機不符:{{cameras}}。這些攝影機上的多邊形(偵測區域)可能無法對齊。此為預設關閉;啟用即可按原樣複製。", + "restartHint": "需要重新啟動", + "items": { + "record": "錄影", + "snapshots": "快照", + "review": "審核", + "motion": "移動偵測", + "objects": "物件", + "audio": "聲音偵測", + "audio_transcription": "語音轉文字", + "notifications": "通知", + "birdseye": "鳥瞰圖", + "mqtt": "MQTT", + "timestamp_style": "時間戳記樣式", + "onvif": "ONVIF", + "lpr": "車牌辨識", + "face_recognition": "人臉辨識", + "semantic_search": "語意搜尋", + "genai": "生成式 AI", + "type": "攝影機類型(一般 / 車牌識別專用)", + "profiles": "設定檔", + "detect": "偵測解析度", + "zones": "區域", + "motion_mask": "移動偵測遮罩", + "object_masks": "物件遮罩", + "ffmpeg_live": "串流網址與用途" + } + }, + "footer": { + "changeCount_other": "將套用 {{count}} 項變更", + "restartNeeded": "部分變更需要重新啟動。", + "liveOnly": "所有變更將即時生效,無需重新啟動。", + "submit": "複製", + "submitting": "複製中…" + }, + "toast": { + "success": "設定已複製到 {{cameraName}}", + "successWithRestart": "設定已複製到 {{cameraName}}。請重新啟動 Frigate 以套用所有變更。", + "successMulti_other": "設定已複製到 {{count}} 台攝影機", + "successMultiWithRestart_other": "設定已複製到 {{count}} 台攝影機。請重新啟動 Frigate 以套用所有變更。", + "partialFailure": "已套用 {{successCount}} 個區段;「{{failedSection}}」失敗:{{errorMessage}}", + "partialFailureMulti": "設定已複製到 {{successCount}} 台攝影機;{{failed}} 失敗:{{errorMessage}}", + "newCameraPartialFailure": "攝影機 {{cameraName}} 已建立,但部分設定複製失敗:{{errorMessage}}", + "sourceMissing": "來源攝影機已不存在", + "submitError": "無法複製攝影機:{{errorMessage}}" + } + } + }, + "cameraReview": { + "title": "攝影機審閱設定", + "object_descriptions": { + "title": "生成式AI目標描述", + "desc": "臨時啟用或停用此攝影機的 生成式AI目標描述 功能,直到 Frigate 重啟。停用後,系統將不再請求該攝影機追蹤目標和物體的AI生成描述。" + }, + "review_descriptions": { + "title": "生成式 AI 審閱總結", + "desc": "臨時開關該攝影機的 生成式 AI 審閱總結 功能,直到 Frigate 重啟。停用後,系統將不再請求 AI 生成該攝影機審閱項目的總結。" + }, + "review": { + "title": "審閱", + "desc": "臨時開關該攝影機的警報與偵測項生成功能,直到 Frigate 重啟後恢復。停用期間,系統將不再生成新的審閱項目。 ", + "alerts": "警報 ", + "detections": "偵測 " + }, + "reviewClassification": { + "title": "審閱分類", + "desc": "Frigate 將審查項目分類為警報與偵測。預設情況下,所有 汽車 物件都會被視為警報。您可以透過為其設定所需的區域,來進一步精確分類您的審查項目。", + "noDefinedZones": "此攝影機未設定任何監控區。", + "objectAlertsTips": "所有 {{alertsLabels}} 類目標或物體在 {{cameraName}} 下都將視為警報。", + "zoneObjectAlertsTips": "所有 {{alertsLabels}} 類目標或物體在 {{cameraName}} 下的 {{zone}} 區域內都將視為警報。", + "objectDetectionsTips": "所有在攝影機 {{cameraName}} 上,偵測到的 {{detectionsLabels}} 目標或物體,無論它位於哪個區,都將顯示為偵測。", + "zoneObjectDetectionsTips": { + "text": "所有在攝影機 {{cameraName}} 下的 {{zone}} 區域內偵測到未分類的 {{detectionsLabels}} 目標或物體,都將顯示為偵測。", + "notSelectDetections": "所有在攝影機 {{cameraName}}下的 {{zone}} 區域內偵測到的 {{detectionsLabels}} 目標或物體,如果它未歸類為警報,無論它位於哪個區,都將顯示為偵測。", + "regardlessOfZoneObjectDetectionsTips": "在攝影機 {{cameraName}} 上,所有未分類的 {{detectionsLabels}} 偵測目標或物體,無論出現在哪個區域,都將顯示為偵測。" + }, + "unsavedChanges": "攝影機 {{camera}} 的審閱分類設定尚未儲存", + "selectAlertsZones": "選擇警報區", + "selectDetectionsZones": "選擇偵測區", + "limitDetections": "限制僅在特定區域內進行偵測", + "toast": { + "success": "審閱分類設定已儲存,重啟後生效。" + } + } + }, + "masksAndZones": { + "filter": { + "all": "所有遮罩和區域" + }, + "restart_required": "需要重啟(遮罩與區域已修改)", + "disabledInConfig": "該項目已在設定檔中被停用", + "addDisabledProfile": "先新增到基礎配置中,然後在設定檔中進行覆蓋", + "profileBase": "(基礎)", + "profileOverride": "(覆蓋)", + "toast": { + "success": { + "copyCoordinates": "已複製 {{polyName}} 的座標到剪貼簿。" + }, + "error": { + "copyCoordinatesFailed": "無法複製座標到剪貼簿。" + } + }, + "motionMaskLabel": "畫面變動遮罩 {{number}}", + "objectMaskLabel": "目標/物體遮罩 {{number}}", + "form": { + "id": { + "error": { + "mustNotBeEmpty": "ID 不能為空。", + "alreadyExists": "此攝影機已存在使用該 ID 的遮罩。" + } + }, + "name": { + "error": { + "mustNotBeEmpty": "名稱不能為空。" + } + }, + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "區域名稱必須至少包含 2 個字元。", + "mustNotBeSameWithCamera": "區域名稱不能與攝影機名稱相同。", + "alreadyExists": "該攝影機已有相同的區域名稱。", + "mustNotContainPeriod": "區域名稱不能包含句點。", + "hasIllegalCharacter": "區域名稱包含非法字元。", + "mustHaveAtLeastOneLetter": "區域名稱必須至少包含一個字母。" + } + }, + "distance": { + "error": { + "text": "距離必須大於或等於 0.1。", + "mustBeFilled": "所有距離欄位必須填寫才能使用速度估算。" + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "慣性必須大於 0。" + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "徘徊時間必須大於或等於 0。" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "速度閾值必須大於或等於0.1。" + } + }, + "polygonDrawing": { + "type": { + "zone": "區域", + "motion_mask": "畫面變動遮罩", + "object_mask": "目標遮罩" + }, + "removeLastPoint": "刪除最後一個點", + "reset": { + "label": "清除所有點" + }, + "snapPoints": { + "true": "啟用點對齊", + "false": "停用點對齊" + }, + "delete": { + "title": "確認刪除", + "desc": "你確定要刪除 {{type}} {{name}} 嗎?", + "success": "{{name}} 已被刪除。" + }, + "revertOverride": { + "title": "恢復為預設配置", + "desc": "這將移除針對 {{type}} {{name}} 的配置覆蓋,並恢復為基礎配置。" + }, + "error": { + "mustBeFinished": "多邊形繪製必須完成閉合後才能儲存。" + } + } + }, + "zones": { + "label": "區域", + "documentTitle": "編輯區域 - Frigate", + "desc": { + "title": "該功能允許你定義特定區域,以便你可以確定特定目標或物體是否在該區域內。", + "documentation": "文件" + }, + "add": "新增區域", + "edit": "編輯區域", + "point_other": "{{count}} 點", + "clickDrawPolygon": "在影像上點選新增點繪製多邊形區域。", + "name": { + "title": "區域名稱", + "inputPlaceHolder": "請輸入名稱…", + "tips": "名稱至少包含兩個字元,且不能和攝影機名或該攝影機下的其他區域同名。" + }, + "enabled": { + "title": "開啟", + "description": "指示該區域在設定檔中是否處於啟用並啟用的狀態。若被停用,則無法透過 MQTT 啟用。停用的區域在執行時會被忽略。" + }, + "inertia": { + "title": "慣性", + "desc": "辨識指定目標前該目標必須在這個區域內出現了多少幀。預設值:3" + }, + "loiteringTime": { + "title": "停留時間", + "desc": "設定目標必須在區域中至少要活動多少時間(單位為秒)。預設值:0" + }, + "objects": { + "title": "目標/物體", + "desc": "將在此區域應用的目標/物體類別清單。" + }, + "allObjects": "所有目標/物體", + "speedEstimation": { + "title": "速度估算", + "desc": "啟用此區域內物體的速度估算。該區域必須恰好包含 4 個點。", + "lineADistance": "A線距離({{unit}})", + "lineBDistance": "B線距離({{unit}})", + "lineCDistance": "C線距離({{unit}})", + "lineDDistance": "D線距離({{unit}})" + }, + "speedThreshold": { + "title": "速度閾值 ({{unit}})", + "desc": "指定物體在此區域內被視為有效的最低速度。", + "toast": { + "error": { + "pointLengthError": "此區域的速度估算已停用。啟用速度估算的區域必須恰好包含 4 個點。", + "loiteringTimeError": "徘徊時間大於 0 的區域不應與速度估算一起使用。" + } + } + }, + "toast": { + "success": "區域 ({{zoneName}}) 已儲存。" + } + }, + "motionMasks": { + "label": "畫面變動遮罩", + "documentTitle": "編輯畫面變動遮罩 - Frigate", + "desc": { + "title": "畫面變動遮罩用於防止觸發不必要的畫面變動偵測。過度的設定遮罩將使目標更加難以被追蹤。", + "documentation": "文件" + }, + "add": "新增畫面變動遮罩", + "edit": "編輯畫面變動遮罩", + "defaultName": "畫面變動遮罩 {{number}}", + "context": { + "title": "移動遮罩用於防止不需要的移動觸發偵測(例如:樹枝搖晃、攝影機時間戳記等)。移動遮罩應 非常謹慎地使用,過度的遮罩會導致物件追蹤變得更加困難。" + }, + "point_other": "{{count}} 點", + "clickDrawPolygon": "在影像上點選新增點繪製多邊形區域。", + "name": { + "title": "名稱", + "description": "為該畫面變動遮罩設定別名(可選)。", + "placeholder": "輸入名稱…" + }, + "polygonAreaTooLarge": { + "title": "畫面變動遮罩的大小達到了攝影機畫面的{{polygonArea}}%。不建議設定太大的畫面變動遮罩。", + "tips": "畫面變動遮罩並不會使該區域無法偵測到指定目標/物體,如有需要,你應該使用 區域 來限制偵測的目標/物體型別。" + }, + "toast": { + "success": { + "title": "{{polygonName}} 已儲存。", + "noName": "畫面變動遮罩已儲存。" + } + } + }, + "objectMasks": { + "label": "目標遮罩", + "documentTitle": "編輯目標遮罩 - Frigate", + "desc": { + "title": "目標過濾器用於防止特定位置出現對某個目標/物體的誤報。", + "documentation": "文件" + }, + "add": "新增目標遮罩", + "edit": "編輯目標遮罩", + "context": "目標過濾器用於防止特定位置的指定目標會誤報。", + "point_other": "{{count}} 點", + "clickDrawPolygon": "在影像上點選新增點繪製多邊形區域。", + "name": { + "title": "名稱", + "description": "為該目標遮罩設定別名(可選)。", + "placeholder": "輸入名稱…" + }, + "objects": { + "title": "目標/物體", + "desc": "將應用於此目標遮罩的目標或物體型別。", + "allObjectTypes": "所有目標或物體型別" + }, + "toast": { + "success": { + "title": "{{polygonName}} 已儲存。", + "noName": "目標遮罩已儲存。" + } + } + }, + "masks": { + "enabled": { + "title": "開啟", + "description": "指示該遮罩在設定檔中是否處於啟用並啟用的狀態。若被停用,則無法透過 MQTT 啟用。停用的遮罩在執行時會被忽略。" + } + } + }, + "motionDetectionTuner": { + "title": "畫面變動偵測調整", + "unsavedChanges": "{{camera}} 的畫面變動調整設定未儲存", + "desc": { + "title": "Frigate 將首先使用畫面變動偵測來確認每一幀畫面中是否有變動的區域,然後再對該區域使用目標偵測。", + "documentation": "閱讀有關畫面變動偵測的文件" + }, + "Threshold": { + "title": "閾值", + "desc": "閾值決定像素亮度變化達到多少時會被認為是畫面變動。預設值:30" + }, + "contourArea": { + "title": "輪廓面積", + "desc": "輪廓面積值用於判斷產生了多大的變化區域可被認定為畫面變動。預設值:10" + }, + "improveContrast": { + "title": "提高對比度", + "desc": "提高較暗場景的對比度。預設值:啟用" + }, + "toast": { + "success": "畫面變動設定已儲存。" + } + }, + "debug": { + "title": "除錯", + "detectorDesc": "Frigate 將使用偵測器({{detectors}})來偵測攝影機影片流中的目標或物體。", + "desc": "除錯介面將即時顯示被追蹤的目標以及統計資訊,目標清單將顯示偵測到的目標和延遲顯示的概覽。", + "openCameraWebUI": "開啟 {{camera}} 的管理頁面", + "debugging": "除錯選項", + "objectList": "目標清單", + "noObjects": "沒有目標", + "audio": { + "title": "音訊", + "noAudioDetections": "未偵測到音訊事件", + "score": "分值", + "currentRMS": "當前均方根值(RMS)", + "currentdbFS": "當前滿量程相對分貝值(dbFS)" + }, + "boundingBoxes": { + "title": "邊界框", + "desc": "將在被追蹤的目標周圍顯示邊界框", + "colors": { + "label": "目標邊界框顏色定義", + "info": "
  • 啟動時,系統會為每個物件標籤指派不同的顏色
  • 深藍色細線代表該物件目前未被偵測到
  • 灰色細線代表該物件被偵測為靜止狀態
  • 粗線代表該物件正被自動追蹤(若已啟用)
  • " + } + }, + "timestamp": { + "title": "時間戳", + "desc": "在影像上顯示時間戳" + }, + "zones": { + "title": "區域", + "desc": "顯示已定義的區域圖層" + }, + "mask": { + "title": "畫面變動遮罩", + "desc": "顯示畫面變動遮罩圖層" + }, + "motion": { + "title": "畫面變動區域框", + "desc": "在偵測到畫面變動的區域顯示區域框", + "tips": "

    畫面變動區域框


    將在當前偵測到畫面變動的區域內顯示紅色區域框。

    " + }, + "regions": { + "title": "範圍", + "desc": "顯示傳送給目標偵測器感興趣的區域框", + "tips": "

    範圍框


    將在幀中傳送到目標偵測器的感興趣範圍上疊加綠色框。

    " + }, + "paths": { + "title": "行動軌跡", + "desc": "顯示被追蹤目標的行動軌跡關鍵點", + "tips": "

    行動軌跡

    將使用線條來標示被追蹤目標在其活動週期內移動的關鍵位置點。

    " + }, + "objectShapeFilterDrawing": { + "title": "允許繪製“目標形狀過濾器”", + "desc": "在影像上繪製矩形,以檢視區域和比例詳細資訊", + "tips": "啟用此選項,能夠在攝影機畫面上繪製矩形,將顯示其區域和比例。你可以透過使用這些值在配置中設定目標形狀過濾器的引數。", + "score": "分數", + "ratio": "比例", + "area": "區域" + } + }, + "timestampPosition": { + "tl": "左上角", + "tr": "右上角", + "bl": "左下角", + "br": "右下角" + }, + "users": { + "title": "使用者", + "management": { + "title": "使用者管理", + "desc": "管理此 Frigate 例項的使用者帳戶。" + }, + "addUser": "新增使用者", + "updatePassword": "修改密碼", + "toast": { + "success": { + "createUser": "使用者 {{user}} 建立成功", + "deleteUser": "使用者 {{user}} 刪除成功", + "updatePassword": "已成功修改密碼。", + "roleUpdated": "已更新 {{user}} 的權限組" + }, + "error": { + "setPasswordFailed": "儲存密碼出現錯誤:{{errorMessage}}", + "createUserFailed": "建立使用者失敗:{{errorMessage}}", + "deleteUserFailed": "刪除使用者失敗:{{errorMessage}}", + "roleUpdateFailed": "更新權限組失敗:{{errorMessage}}" + } + }, + "table": { + "username": "使用者名稱", + "actions": "操作", + "role": "權限組", + "noUsers": "未找到使用者。", + "changeRole": "更改使用者權限組", + "password": "修改密碼", + "deleteUser": "刪除使用者" + }, + "dialog": { + "form": { + "user": { + "title": "使用者名稱", + "desc": "僅允許使用字母、數字、句點和下劃線。", + "placeholder": "請輸入使用者名稱" + }, + "password": { + "title": "密碼", + "placeholder": "請輸入密碼", + "show": "顯示密碼", + "hide": "隱藏密碼", + "confirm": { + "title": "確認密碼", + "placeholder": "請再次輸入密碼" + }, + "strength": { + "title": "密碼強度: ", + "weak": "弱", + "medium": "中等", + "strong": "強", + "veryStrong": "非常強" + }, + "requirements": { + "title": "密碼要求:", + "length": "至少需要 12 位字元" + }, + "match": "密碼匹配", + "notMatch": "密碼不匹配" + }, + "newPassword": { + "title": "新密碼", + "placeholder": "請輸入新密碼", + "confirm": { + "placeholder": "請再次輸入新密碼" + } + }, + "currentPassword": { + "title": "當前密碼", + "placeholder": "請輸入當前密碼" + }, + "usernameIsRequired": "使用者名稱為必填項", + "passwordIsRequired": "必須輸入密碼" + }, + "createUser": { + "title": "建立新使用者", + "desc": "建立一個新使用者帳戶,並指定一個權限組以控制存取 Frigate 頁面的權限。", + "usernameOnlyInclude": "使用者名稱只能包含字母、數字和 _", + "confirmPassword": "請確認你的密碼" + }, + "deleteUser": { + "title": "刪除該使用者", + "desc": "此操作無法撤銷。這將永久刪除使用者帳戶並移除所有相關資料。", + "warn": "你確定要刪除 {{username}} 嗎?" + }, + "passwordSetting": { + "cannotBeEmpty": "密碼不能為空", + "doNotMatch": "兩次輸入密碼不匹配", + "currentPasswordRequired": "當前密碼為必填", + "incorrectCurrentPassword": "當前密碼錯誤", + "passwordVerificationFailed": "驗證密碼失敗", + "updatePassword": "更新 {{username}} 的密碼", + "setPassword": "設定密碼", + "desc": "建立一個強密碼來保護此帳戶。", + "multiDeviceWarning": "其他已登入的裝置將需要在 {{refresh_time}} 內重新登入。", + "multiDeviceAdmin": "你也可以透過輪換你的 JWT 金鑰,強制所有使用者立即重新登入驗證。" + }, + "changeRole": { + "title": "更改使用者權限組", + "select": "選擇權限組", + "desc": "更新 {{username}} 的權限", + "roleInfo": { + "intro": "為該使用者選擇一個合適的權限組:", + "admin": "管理員", + "adminDesc": "完全功能與存取權限。", + "viewer": "成員", + "viewerDesc": "僅能夠檢視即時監控面板、審閱、瀏覽和匯出功能。", + "customDesc": "自訂特定攝影機的存取規則。" + } + } + } + }, + "roles": { + "management": { + "title": "成員權限組管理", + "desc": "管理此 Frigate 例項的自訂權限組及其攝影機存取權限。" + }, + "addRole": "新增權限組", + "table": { + "role": "權限組", + "cameras": "攝影機", + "actions": "操作", + "noRoles": "沒有找到自訂權限組。", + "editCameras": "編輯攝影機", + "deleteRole": "刪除權限組" + }, + "toast": { + "success": { + "createRole": "權限組 {{role}} 建立成功", + "updateCameras": "已更新攝影機至 {{role}} 權限組", + "deleteRole": "已刪除 {{role}} 權限組", + "userRolesUpdated_other": "已將分配到此權限組的 {{count}} 位使用者更新為 “成員”,該權限組可存取所有攝影機。" + }, + "error": { + "createRoleFailed": "建立權限組失敗:{{errorMessage}}", + "updateCamerasFailed": "更新攝影機失敗:{{errorMessage}}", + "deleteRoleFailed": "刪除權限組失敗:{{errorMessage}}", + "userUpdateFailed": "更新使用者權限組失敗:{{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "建立新權限組", + "desc": "新增新權限組並分配攝影機存取權限。" + }, + "editCameras": { + "title": "編輯權限組的攝影機", + "desc": "為權限組 {{role}} 更新攝影機存取權限。" + }, + "deleteRole": { + "title": "刪除權限組", + "desc": "此操作無法撤銷。這將永久刪除該權限組,並將所有擁有此權限組的使用者分配到 “成員” (view)權限組,該權限組將賦予使用者檢視所有攝影機的權限。", + "warn": "你確定要刪除權限組 {{role}} 嗎?", + "deleting": "刪除中…" + }, + "form": { + "role": { + "title": "權限組名稱", + "placeholder": "輸入權限組名稱", + "desc": "僅允許使用字母、數字、句點和下劃線。", + "roleIsRequired": "必須輸入權限組名稱", + "roleOnlyInclude": "權限組名稱僅支援字母、數字、英文句號和下劃線", + "roleExists": "該權限組名稱已存在。" + }, + "cameras": { + "title": "攝影機", + "desc": "請選擇該權限組能夠存取的攝影機。至少需要選擇一個攝影機。", + "required": "至少要選擇一個攝影機。" + } + } + } + }, + "notification": { + "title": "通知", + "notificationSettings": { + "title": "通知設定", + "desc": "Frigate 在瀏覽器中執行或作為 PWA 安裝時,可以原生向您的裝置傳送推送通知。" + }, + "notificationUnavailable": { + "title": "通知功能不可用", + "desc": "網頁推送通知需要安全連線(https://…)。這是瀏覽器的限制。請透過安全方式存取 Frigate 以使用通知功能。", + "descPwa": "在 iOS 上,只有將 Frigate 加到主畫面後,才能使用網頁推播通知。請開啟 分享 選單,選擇 加入主畫面,然後從新圖示開啟 Frigate,即可為此裝置註冊通知功能。" + }, + "globalSettings": { + "title": "全域性設定", + "desc": "臨時暫停所有已註冊裝置上特定攝影機的通知。" + }, + "email": { + "title": "電子郵箱", + "placeholder": "例如:example@email.com", + "desc": "需要輸入有效的電子郵件,在推送服務出現問題時,將使用此電子郵件進行通知。" + }, + "cameras": { + "title": "攝影機", + "noCameras": "沒有可用的攝影機", + "desc": "選擇要啟用通知的攝影機。" + }, + "deviceSpecific": "裝置專用設定", + "registerDevice": "註冊該裝置", + "unregisterDevice": "取消註冊該裝置", + "sendTestNotification": "傳送測試通知", + "unsavedRegistrations": "未儲存通知註冊", + "unsavedChanges": "未儲存通知設定更改", + "active": "通知已啟用", + "suspended": "通知已暫停 {{time}}", + "suspendTime": { + "suspend": "暫停", + "5minutes": "暫停 5 分鐘", + "10minutes": "暫停 10 分鐘", + "30minutes": "暫停 30 分鐘", + "1hour": "暫停 1 小時", + "12hours": "暫停 12 小時", + "24hours": "暫停 24 小時", + "untilRestart": "暫停直到重啟" + }, + "cancelSuspension": "取消暫停", + "toast": { + "success": { + "registered": "已成功註冊通知。需要重啟 Frigate 才能傳送任何通知(包括測試通知)。", + "settingSaved": "通知設定已儲存。" + }, + "error": { + "registerFailed": "通知註冊失敗。" + } + } + }, + "frigatePlus": { + "title": "Frigate+ 設定", + "description": "Frigate+ 是一項訂閱服務,可為你的 Frigate 例項提供額外的功能和能力,包括使用基於你自己的資料訓練的自訂目標偵測模型。你可以在此管理 Frigate+ 的模型設定。", + "cardTitles": { + "api": "API", + "currentModel": "當前模型", + "otherModels": "其他模型", + "configuration": "配置" + }, + "apiKey": { + "title": "Frigate+ API 金鑰", + "validated": "Frigate+ API 金鑰已偵測並驗證透過", + "notValidated": "未偵測到 Frigate+ API 金鑰或驗證未透過", + "desc": "Frigate+ API 金鑰用於啟用與 Frigate+ 服務的整合。", + "plusLink": "瞭解更多關於 Frigate+" + }, + "snapshotConfig": { + "title": "快照配置", + "desc": "提交到 Frigate+ 需要同時在配置中開啟快照功能。", + "cleanCopyWarning": "部分攝影機未開啟快照功能", + "table": { + "camera": "攝影機", + "snapshots": "快照" + } + }, + "modelInfo": { + "title": "模型資訊", + "modelType": "模型型別", + "trainDate": "訓練日期", + "baseModel": "基礎模型", + "plusModelType": { + "baseModel": "基礎模型", + "userModel": "定向調優" + }, + "supportedDetectors": "支援的偵測器", + "cameras": "攝影機", + "loading": "正在載入模型資訊…", + "error": "載入模型資訊失敗", + "noModelLoaded": "目前未載入 Frigate+ 模型。", + "availableModels": "可用模型", + "loadingAvailableModels": "正在載入可用模型…", + "selectModel": "選擇模型", + "noModelsAvailable": "無可用模型", + "filter": { + "ariaLabel": "依類型篩選模型", + "baseModels": "基礎模型", + "fineTunedModels": "微調模型" + }, + "modelSelect": "您可以在Frigate+上選擇可用的模型。請注意,只能選擇與當前偵測器配置相容的模型。" + }, + "changeInDetectorsAndModel": "變更模型", + "unsavedChanges": "未儲存Frigate+變更設定", + "restart_required": "需要重啟(Frigate+模型已修改)", + "toast": { + "success": "Frigate+ 設定已儲存。請重啟 Frigate 以應用更改。", + "error": "配置更改儲存失敗:{{errorMessage}}" + } + }, + "detectorsAndModel": { + "title": "偵測器與模型", + "description": "設定執行物件偵測的偵測器後端及其使用的模型。變更會一起儲存以確保偵測器與模型保持同步。", + "cardTitles": { + "detector": "偵測器硬體", + "model": "偵測模型" + }, + "tabs": { + "plus": "Frigate+", + "custom": "自訂模型" + }, + "mismatch": { + "warning": "目前的 Frigate+ 模型「{{model}}」需要 {{required}} 偵測器。請在下方選擇相容的模型,或在儲存前切換到「自訂模型」。" + }, + "plusModel": { + "requiresDetector": "需要:{{detector}}", + "noModelSelected": "選擇 Frigate+ 模型" + }, + "toast": { + "saveSuccess": "偵測器與模型設定已儲存。請重新啟動 Frigate 以套用變更。", + "saveError": "儲存偵測器與模型設定失敗" + }, + "unsavedChanges": "偵測器與模型有未儲存的變更", + "restartRequired": "需要重新啟動(偵測器或模型已變更)" + }, + "maintenance": { + "title": "維護", + "sync": { + "title": "媒體同步", + "desc": "Frigate 會根據您的保留配置定期清理媒體檔案。出現少量孤立檔案是正常現象。使用此功能可以刪除磁碟上不再被資料庫引用的孤立媒體檔案。", + "started": "媒體同步已啟動。", + "alreadyRunning": "同步任務已在執行中", + "error": "啟動同步失敗", + "currentStatus": "狀態", + "jobId": "任務 ID", + "startTime": "開始時間", + "endTime": "結束時間", + "statusLabel": "狀態", + "results": "結果", + "errorLabel": "錯誤", + "mediaTypes": "媒體型別", + "allMedia": "所有媒體", + "dryRun": "試執行", + "dryRunEnabled": "不會刪除任何檔案", + "dryRunDisabled": "將刪除檔案", + "force": "強制執行", + "forceDesc": "繞過安全閾值,即使刪除超過 50% 的檔案也完成同步。", + "verbose": "詳細模式", + "verboseDesc": "將所有孤立檔案的完整清單寫入硬碟以供審閱。", + "running": "同步執行中…", + "start": "開始同步", + "inProgress": "同步正在進行中。此頁面已停用。", + "status": { + "queued": "已排隊", + "running": "執行中", + "completed": "已完成", + "failed": "失敗", + "notRunning": "未執行" + }, + "resultsFields": { + "filesChecked": "已檢查檔案", + "orphansFound": "發現孤立檔案", + "orphansDeleted": "已刪除孤立檔案", + "aborted": "已中止。刪除操作將超過安全閾值。", + "error": "錯誤", + "totals": "總計" + }, + "event_snapshots": "追蹤目標快照", + "event_thumbnails": "追蹤目標縮圖", + "review_thumbnails": "審閱縮圖", + "previews": "預覽", + "exports": "匯出", + "recordings": "錄影" + }, + "regionGrid": { + "title": "區域網格", + "desc": "區域網格是一種最佳化功能,它會學習不同大小的目標通常出現在每個攝影機視野中的位置。Frigate 利用這些資料來高效地確定偵測區域的大小。該網格會根據追蹤目標資料自動構建。", + "clear": "清除區域網格", + "clearConfirmTitle": "清除區域網格", + "clearConfirmDesc": "除非你最近更改了偵測器模型大小或攝影機的物理位置,並且遇到了目標追蹤問題,否則不建議清除區域網格。網格會隨著目標的追蹤自動重建。更改需要重啟 Frigate 才能生效。", + "clearSuccess": "區域網格清除成功", + "clearError": "清除區域網格失敗", + "restartRequired": "需要重啟以使區域網格更改生效" + } + }, + "configForm": { + "global": { + "title": "全域性設定", + "description": "這些設定適用於所有攝影機,除非在攝影機特定設定中被覆蓋。" + }, + "camera": { + "title": "攝影機設定", + "description": "這些設定僅適用於此攝影機,並會覆蓋全域性設定。", + "noCameras": "沒有可用的攝影機" + }, + "advancedSettingsCount": "高階設定 ({{count}})", + "advancedCount": "高階選項 ({{count}})", + "showAdvanced": "顯示高階設定", + "tabs": { + "sharedDefaults": "共享預設值", + "system": "系統", + "integrations": "整合" + }, + "additionalProperties": { + "keyLabel": "鍵", + "valueLabel": "值", + "keyPlaceholder": "新鍵名", + "remove": "移除" + }, + "knownPlates": { + "namePlaceholder": "例如:老婆的車", + "platePlaceholder": "車牌號或正則表示式" + }, + "timezone": { + "defaultOption": "使用瀏覽器時區" + }, + "roleMap": { + "empty": "未配置權限組對映", + "roleLabel": "角色", + "groupsLabel": "使用者組", + "addMapping": "新增角色對映", + "remove": "移除" + }, + "ffmpegArgs": { + "preset": "預設", + "manual": "手動引數", + "inherit": "繼承攝影機設定", + "none": "無", + "useGlobalSetting": "繼承全域性設定", + "selectPreset": "選擇預設", + "manualPlaceholder": "輸入 FFmpeg 引數", + "presetLabels": { + "preset-rpi-64-h264": "樹莓派(H.264)", + "preset-rpi-64-h265": "樹莓派(H.265)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "瑞芯微 RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG(通用)", + "preset-http-mjpeg-generic": "HTTP MJPEG(通用)", + "preset-http-reolink": "HTTP - Reolink 攝影機", + "preset-rtmp-generic": "RTMP(通用)", + "preset-rtsp-generic": "RTSP(通用)", + "preset-rtsp-restream": "RTSP - 從 go2rtc 轉流", + "preset-rtsp-restream-low-latency": "RTSP - 從 go2rtc 轉流(低延遲)", + "preset-rtsp-udp": "RTSP - UDP協議", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "錄製(通用,無音訊)", + "preset-record-generic-audio-copy": "錄製(通用,不轉碼音訊)", + "preset-record-generic-audio-aac": "錄製(通用並將音訊轉碼為 AAC)", + "preset-record-mjpeg": "錄製 - MJPEG 流攝影機", + "preset-record-jpeg": "錄製 - JPEG 流攝影機", + "preset-record-ubiquiti": "錄製 - 優必飛攝影機" + } + }, + "cameraInputs": { + "itemTitle": "影片流 {{index}}", + "sourceMode": { + "restream": "重新串流 (go2rtc)", + "manual": "手動輸入路徑", + "go2rtcStreamLabel": "go2rtc 串流", + "go2rtcStreamPlaceholder": "選擇一個 go2rtc 串流", + "noGo2rtcStreams": "未設定 go2rtc 串流", + "go2rtcStreamSearch": "搜尋串流...", + "availableStreams": "可用串流", + "noMatchingStreams": "沒有符合的串流" + } + }, + "restartRequiredField": "需要重啟", + "restartRequiredFooter": "配置已更改 - 需要重啟", + "sections": { + "detect": "偵測", + "record": "錄製", + "snapshots": "快照", + "motion": "畫面變動", + "objects": "目標", + "review": "審閱", + "audio": "音訊", + "notifications": "通知", + "live": "即時檢視", + "timestamp_style": "時間戳", + "mqtt": "MQTT", + "database": "資料庫", + "telemetry": "遙測", + "auth": "身份驗證", + "tls": "TLS", + "proxy": "代理", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg 編解碼", + "detectors": "偵測器", + "model": "模型", + "semantic_search": "語意搜尋", + "genai": "生成式 AI", + "face_recognition": "人臉辨識", + "lpr": "車牌辨識", + "birdseye": "鳥瞰圖", + "masksAndZones": "遮罩 / 區域" + }, + "detect": { + "title": "偵測設定" + }, + "detectors": { + "title": "偵測器設定", + "singleType": "只允許一個 {{type}} 偵測器。", + "keyRequired": "偵測器名稱為必填項。", + "keyDuplicate": "偵測器名稱已存在。", + "noSchema": "沒有可用的偵測器架構。", + "none": "未配置偵測器例項。", + "add": "新增偵測器", + "addCustomKey": "新增自訂鍵(Key)" + }, + "record": { + "title": "錄製設定" + }, + "snapshots": { + "title": "快照設定" + }, + "motion": { + "title": "畫面變動設定" + }, + "objects": { + "title": "目標設定" + }, + "audioLabels": { + "summary": "已選擇 {{count}} 個音訊標籤", + "empty": "無可用音訊標籤" + }, + "objectLabels": { + "summary": "已選擇 {{count}} 個目標型別", + "empty": "無可用目標標籤" + }, + "reviewLabels": { + "summary": "已選擇 {{count}} 個標籤", + "empty": "暫無可用標籤" + }, + "filters": { + "objectFieldLabel": "{{label}} 的 {{field}}" + }, + "zoneNames": { + "summary": "已選擇 {{count}} 個", + "empty": "沒有可用的區域" + }, + "inputRoles": { + "summary": "已選擇 {{count}} 個功能", + "empty": "無可用功能", + "options": { + "detect": "偵測", + "record": "錄製", + "audio": "音訊" + } + }, + "genaiRoles": { + "options": { + "embeddings": "嵌入(Embedding)", + "descriptions": "描述", + "chat": "對話" + } + }, + "semanticSearchModel": { + "placeholder": "選擇模型…", + "builtIn": "內建模型", + "genaiProviders": "生成式 AI 服務" + }, + "review": { + "title": "審閱設定" + }, + "audio": { + "title": "音訊設定" + }, + "notifications": { + "title": "通知設定" + }, + "live": { + "title": "即時檢視設定" + }, + "timestamp_style": { + "title": "時間戳設定" + }, + "searchPlaceholder": "搜尋…", + "addCustomLabel": "新增自訂標籤…", + "genaiModel": { + "placeholder": "選擇模型…", + "search": "搜尋模型…", + "noModels": "暫無模型", + "available": "可用模型", + "useCustom": "使用 \"{{value}}\"", + "refresh": "重新整理模型清單", + "probeFailed": "無法取得模型清單", + "fetchedModels": "已成功取得模型清單" + }, + "liveStreams": { + "streamNameLabel": "串流名稱", + "streamNamePlaceholder": "例如:主高畫質串流", + "go2rtcStreamLabel": "go2rtc 串流", + "go2rtcStreamPlaceholder": "選擇一個 go2rtc 串流", + "go2rtcStreamSearch": "搜尋或輸入串流名稱…", + "noGo2rtcStreams": "未設定 go2rtc 串流", + "availableStreams": "可用串流", + "useCustom": "使用 \"{{value}}\"", + "addStream": "新增串流" + }, + "semanticSearchModelSize": { + "notApplicable": "不適用於生成式 AI 服務" + }, + "ptzPresets": { + "placeholder": "選擇或輸入預設...", + "search": "選擇或輸入預設...", + "noPresets": "沒有可用的預設", + "available": "攝影機預設", + "useCustom": "使用 \"{{value}}\"" + }, + "defaultRole": { + "admin": "管理員", + "viewer": "檢視者" + } + }, + "globalConfig": { + "title": "全域性配置", + "description": "配置適用於所有攝影機的全域性設定,除非被單獨覆蓋。", + "toast": { + "success": "全域性設定儲存成功", + "error": "儲存全域性設定失敗", + "validationError": "驗證失敗" + } + }, + "cameraConfig": { + "title": "攝影機配置", + "description": "配置單個攝影機的設定。這些設定會覆蓋全域性預設值。", + "overriddenBadge": "已覆蓋", + "resetToGlobal": "重設為全域性設定", + "toast": { + "success": "攝影機設定儲存成功", + "error": "儲存攝影機設定失敗" + } + }, + "toast": { + "success": "設定儲存成功", + "applied": "設定應用成功", + "successRestartRequired": "設定儲存成功。請重啟 Frigate 以應用更改。", + "error": "儲存設定失敗", + "validationError": "驗證失敗:{{message}}", + "resetSuccess": "已重設為全域性預設值", + "resetError": "重設設定失敗", + "saveAllSuccess_other": "所有 {{count}} 個部分儲存成功。", + "saveAllPartial_other": "已儲存 {{successCount}} / {{totalCount}} 個部分。{{failCount}} 個失敗。", + "saveAllFailure": "儲存所有部分失敗。", + "saveAllSuccessRestartRequired_other": "已成功儲存 {{count}} 個區塊。請重新啟動 Frigate 以套用變更。" + }, + "profiles": { + "title": "設定檔", + "activeProfile": "啟用設定檔", + "noActiveProfile": "無啟用的設定檔", + "active": "啟用", + "activated": "設定檔 {{profile}} 已啟用", + "activateFailed": "設定檔設定失敗", + "deactivated": "設定檔已停用", + "noProfiles": "未定義任何設定檔。", + "noOverrides": "無覆蓋項", + "cameraCount_other": "{{count}} 個攝影機", + "columnCamera": "攝影機", + "columnOverrides": "設定檔覆蓋", + "baseConfig": "基礎配置", + "addProfile": "新增設定檔", + "newProfile": "新設定檔", + "profileNamePlaceholder": "例如:佈防、外出、夜間模式", + "friendlyNameLabel": "設定檔名稱", + "profileIdLabel": "設定檔 ID", + "profileIdDescription": "用於配置和自動化的內部辨識符號", + "nameInvalid": "僅允許使用小寫字母、數字和下劃線", + "nameDuplicate": "已存在同名設定檔", + "error": { + "mustBeAtLeastTwoCharacters": "至少需要 2 個字元", + "mustNotContainPeriod": "不得包含英文句號(\".\")", + "alreadyExists": "已存在使用此 ID 的設定檔" + }, + "renameProfile": "重新命名設定檔", + "renameSuccess": "已將設定檔重新命名為 “{{profile}}”", + "deleteProfile": "刪除設定檔", + "deleteProfileConfirm": "確定要為所有攝影機刪除設定檔“{{profile}}”嗎?該步驟無法撤銷。", + "deleteSuccess": "設定檔“{{profile}}”已刪除", + "createSuccess": "設定檔“{{profile}}”已建立", + "removeOverride": "移除設定檔覆蓋", + "deleteSection": "刪除節點覆蓋", + "deleteSectionConfirm": "是否要移除攝像機 {{camera}} 上針對設定檔 {{profile}} 的 {{section}} 覆蓋設定?", + "deleteSectionSuccess": "已移除 {{profile}} 的 {{section}} 覆蓋設定", + "enableSwitch": "開啟設定檔", + "enabledDescription": "設定檔功能已啟用。請在下方建立新的設定檔,進入攝影機配置頁面進行修改並儲存,修改即可生效。", + "disabledDescription": "設定檔功能可以讓你建立一組帶名稱的攝影機自訂引數(比如佈防、離家、夜間模式),並隨時切換啟用。" + }, + "unsavedChanges": "您有未儲存的更改", + "confirmReset": "確認重設", + "resetToDefaultDescription": "這將把此部分的所有設定重設為預設值。此操作無法撤銷。", + "resetToGlobalDescription": "這將把此部分的設定重設為全域性預設值。此操作無法撤銷。", + "go2rtcStreams": { + "title": "go2rtc 影片流", + "description": "管理用於攝影機轉流的 go2rtc 流配置。每個影片流包含一個名稱以及一個或多個源地址 URL。", + "addStream": "新增影片流", + "addStreamDesc": "為新的影片流輸入一個名稱,該名稱將用於在攝影機配置中引用該影片流。", + "addUrl": "新增 URL 地址", + "streamName": "影片流名稱", + "streamNamePlaceholder": "例如:front_door,此處只能使用英文", + "streamUrlPlaceholder": "例如:rtsp://user:pass@192.168.1.100/stream", + "deleteStream": "刪除影片流", + "deleteStreamConfirm": "確定要刪除影片流 “{{streamName}}” 嗎?引用該影片流的攝影機可能會停止工作。", + "noStreams": "未配置任何 go2rtc 流。請新增一個影片流以開始使用。", + "validation": { + "nameRequired": "影片流名稱為必填", + "nameDuplicate": "已存在同名的影片流", + "nameInvalid": "影片流名稱只能使用字母、數字、下劃線和連字元", + "urlRequired": "至少需要填寫一個 URL 地址" + }, + "renameStream": "重新命名影片流", + "renameStreamDesc": "為此影片流輸入新名稱。重新命名影片流可能會導致透過名稱引用它的攝影機或其他流無法正常工作。", + "newStreamName": "新影片流名稱", + "ffmpeg": { + "useFfmpegModule": "使用相容模式(ffmpeg)", + "video": "影片", + "audio": "音訊", + "hardware": "硬體加速", + "videoCopy": "直接複製", + "videoH264": "轉碼為 H.264", + "videoH265": "轉碼為 H.265", + "videoExclude": "排除", + "audioCopy": "直接複製", + "audioAac": "轉碼為 AAC", + "audioOpus": "轉碼為 Opus", + "audioPcmu": "轉碼為 PCM μ-law", + "audioPcma": "轉碼為 PCM A-law", + "audioPcm": "轉碼為 PCM", + "audioMp3": "轉碼為 MP3", + "audioExclude": "排除", + "hardwareNone": "無硬體加速", + "hardwareAuto": "自動選擇硬體加速", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "新增視訊編碼器", + "addAudioCodec": "新增音訊編碼器", + "removeCodec": "移除編碼器" + }, + "sourceNumber": "來源 {{index}}" + }, + "birdseye": { + "trackingMode": { + "objects": "目標", + "motion": "動作", + "continuous": "持續" + }, + "cameraOrder": { + "label": "攝影機排序", + "description": "拖曳攝影機以設定它們在鳥瞰佈局中的順序。", + "reorderHandle": "拖曳以重新排序", + "saving": "儲存中…", + "saved": "已儲存" + } + }, + "retainMode": { + "all": "全部", + "motion": "動作", + "active_objects": "活動目標" + }, + "previewQuality": { + "very_high": "極高", + "high": "高", + "medium": "中", + "low": "低", + "very_low": "極低" + }, + "ui": { + "timeFormat": { + "browser": "瀏覽器", + "12hour": "12 小時", + "24hour": "24 小時" + }, + "TimeOrDateStyle": { + "full": "完整", + "long": "長", + "medium": "中", + "short": "短" + }, + "unitSystem": { + "metric": "公制", + "imperial": "英制" + } + }, + "review": { + "imageSource": { + "recordings": "錄影", + "previews": "預覽" + } + }, + "logger": { + "logLevel": { + "debug": "Debug", + "info": "Info", + "warning": "警告", + "error": "Error", + "critical": "嚴重" + } + }, + "onvif": { + "profileAuto": "自動", + "profileLoading": "正在載入設定檔…", + "autotracking": { + "zooming": { + "disabled": "停用", + "absolute": "絕對", + "relative": "相對" + } + } + }, + "modelSize": { + "small": "小", + "large": "大" + }, + "configMessages": { + "review": { + "recordDisabled": "錄製已停用,不會生成審閱記錄項。", + "detectDisabled": "目標偵測已停用。審閱記錄需要依靠偵測到的目標來對警報和偵測事件進行分類。", + "allNonAlertDetections": "所有非警報類活動都將被記錄為偵測事件。", + "genaiImageSourceRecordingsRecordDisabled": "影像源雖然設定為“錄製”,但錄製功能已關閉。Frigate 將自動降級使用預覽圖片。" + }, + "audio": { + "noAudioRole": "暫無任何流已開啟音訊(audio)功能(role)。必須在影片流上啟用音訊功能,音訊偵測才能正常工作。" + }, + "audioTranscription": { + "audioDetectionDisabled": "該攝影機未開啟音訊偵測功能。音訊轉錄需要先開啟音訊偵測。" + }, + "detect": { + "fpsGreaterThanFive": "不建議設定偵測幀率高於 5,數值設定過高可能引發效能問題,且不會帶來任何增益。", + "disabled": "目標偵測已停用。快照、回放條目以及人臉辨識、車牌辨識、生成式 AI 等增強功能都將無法使用。", + "resolutionShouldBeMultipleOfFour": "為了獲得最佳效果,偵測的寬度與高度應為 4 的倍數。其他偶數值可能會在偵測串流中產生畫面殘影或輕微失真。", + "aspectRatioMismatch": "您輸入的寬度與高度與目前的偵測解析度長寬比不符。這可能會導致影像被拉伸或失真。", + "maxFramesSet": "設定最大影格數將會覆蓋預設行為,並停用靜態物件追蹤。極少有情況需要使用此功能,請謹慎使用。", + "squareResolution": "正方形的偵測解析度並不常見。偵測的寬度與高度應符合攝影機的長寬比(例如 16:9),而不是物件偵測模型的尺寸。不相符的長寬比可能會拉伸影像並降低偵測準確率度。", + "resolutionHigh": "此偵測解析度高於建議值,可能會增加資源消耗,但不會提高偵測準確度。對於大多數攝影機,建議使用 1080p 或更低的偵測解析度。", + "globalResolutionMultipleCameras": "在設定多台攝影機的情況下設定了全域偵測解析度。除非所有攝影機都具有相同的解析度和長寬比,否則應針對每台攝影機單獨定義偵測寬度與高度,以符合各台攝影機的原生長寬比。" + }, + "objects": { + "genaiNoDescriptionsProvider": "必須配置具備“描述”功能的生成式 AI 服務商,才能自動生成事件描述。" + }, + "faceRecognition": { + "globalDisabled": "必須開啟人臉辨識增強功能,此攝影機的人臉辨識相關功能才能正常使用。", + "personNotTracked": "人臉辨識需要偵測到 “人”(person) 後才能工作。請在該攝影機的偵測目標設定中新增“人”。", + "modelSizeLarge": "大型模型需要 GPU 或 NPU 才能執行正常。僅使用 CPU 的裝置請選用小型模型。" + }, + "lpr": { + "globalDisabled": "要讓該攝影機的車牌辨識功能正常使用,必須先開啟車牌辨識增強功能。", + "vehicleNotTracked": "車牌辨識需要先開啟對 “汽車” 或 “摩托車” 的目標追蹤。請在該攝影機的偵測目標中新增“汽車”或“摩托車”。", + "modelSizeLarge": "大型模型針對多行格式車牌做了最佳化。小型模型的效能優於大型模型,而且只有小型模型才能支援中文車牌。除非你所在地區使用多行車牌格式,否則建議使用小型模型。" + }, + "record": { + "noRecordRole": "暫無任何影片流已配置錄製功能,錄製功能將無法正常工作。" + }, + "birdseye": { + "objectsModeDetectDisabled": "鳥瞰圖已設定為 “目標” 模式,但此攝影機未開啟目標偵測。該攝影機將不會顯示在鳥瞰畫面中。" + }, + "snapshots": { + "detectDisabled": "目標偵測已停用。快照是根據追蹤到的目標生成的,因此將不會建立快照。" + }, + "detectors": { + "mixedTypes": "所有偵測器必須為同一型別。若要更換為其他型別,請先移除現有的偵測器。", + "mixedTypesSuggestion": "所有偵測器必須使用相同型別。請移除現有偵測器,或選擇 {{type}}。" + }, + "semanticSearch": { + "jinav2SmallModelSize": "Jina V2 的大型模型版本記憶體佔用與推理開銷較高,建議搭配獨立顯示卡使用大型模型。" + }, + "model": { + "optimizedFor320": "Frigate 已針對 320x320 模型進行最佳化,這是大多數環境下的最佳選擇。640x640 模型速度較慢,且僅在特定情境下有所幫助。", + "inputDimensionsNotDetectResolution": "模型輸入的寬度與高度是物件偵測模型的輸入尺寸,而不是攝影機的偵測解析度。它們應該與您使用的模型尺寸相符 — 通常是像 320x320 或 640x640 這樣的正方形尺寸。" + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "不建議手動設定硬體加速參數。除非有特殊需求,否則請選擇符合您硬體的預設組態。" + }, + "onvif": { + "autotrackingNoZones": "自動追蹤至少需要一個區域。請先在「遮罩 / 區域」中為此攝影機定義一個區域,然後在下方將其設定為必要區域。" + } + }, + "menuDot": { + "overrideGlobal": "此區段會覆蓋全域設定", + "overrideProfile": "此區段已被 {{profile}} 設定檔覆蓋", + "unsaved": "此區段有未儲存的變更" + } +} diff --git a/web/public/locales/zh-Hant/views/system.json b/web/public/locales/zh-Hant/views/system.json new file mode 100644 index 0000000..df8c0a7 --- /dev/null +++ b/web/public/locales/zh-Hant/views/system.json @@ -0,0 +1,274 @@ +{ + "documentTitle": { + "cameras": "鏡頭統計 - Frigate", + "storage": "儲存裝置統計 - Frigate", + "general": "一般統計 - Frigate", + "enrichments": "進階功能統計 - Frigate", + "logs": { + "frigate": "Frigate 日誌 - Frigate", + "go2rtc": "Go2RTC 日誌 - Frigate", + "nginx": "Nginx 日誌 - Frigate", + "websocket": "訊息日誌 - Frigate" + } + }, + "title": "系統", + "metrics": "系統指標", + "logs": { + "download": { + "label": "下載日誌" + }, + "copy": { + "label": "複製到剪貼簿", + "success": "已將日誌複製到剪貼簿", + "error": "無法將日誌複製到剪貼簿" + }, + "type": { + "label": "類型", + "timestamp": "時間戳", + "tag": "標籤", + "message": "訊息" + }, + "tips": "正在從伺服器串流日誌", + "toast": { + "error": { + "fetchingLogsFailed": "擷取日誌時出錯:{{errorMessage}}", + "whileStreamingLogs": "串流日誌時出錯:{{errorMessage}}" + } + }, + "websocket": { + "label": "訊息", + "pause": "暫停", + "resume": "繼續", + "clear": "清除", + "filter": { + "all": "全部主題", + "topics": "主題", + "events": "事件", + "reviews": "審閱", + "classification": "分類", + "face_recognition": "人臉辨識", + "lpr": "車牌辨識", + "camera_activity": "攝影機活動", + "system": "系統", + "camera": "攝影機", + "all_cameras": "所有攝影機", + "cameras_count_one": "{{count}} 個攝影機", + "cameras_count_other": "{{count}} 個攝影機" + }, + "empty": "未捕獲到訊息", + "count_one": "{{count}} 則訊息", + "count_other": "{{count}} 則訊息", + "expanded": { + "payload": "Payload" + } + } + }, + "general": { + "title": "一般", + "detector": { + "title": "偵測器", + "inferenceSpeed": "偵測器推理速度", + "temperature": "偵測器溫度", + "cpuUsage": "偵測器 CPU 使用率", + "memoryUsage": "偵測器記憶體使用量", + "cpuUsageInformation": "用於準備輸入和輸出數據至/從偵測模型的CPU。此值不衡量推論使用量,即使使用GPU或加速器。" + }, + "hardwareInfo": { + "title": "硬體資訊", + "gpuUsage": "GPU 使用率", + "gpuMemory": "GPU 記憶體", + "gpuEncoder": "GPU 編碼器", + "gpuDecoder": "GPU 解碼器", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo 輸出", + "returnCode": "返回代碼:{{code}}", + "processOutput": "行程輸出:", + "processError": "行程錯誤:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI 輸出", + "name": "名稱:{{name}}", + "driver": "驅動程式:{{driver}}", + "cudaComputerCapability": "CUDA 計算能力:{{cuda_compute}}", + "vbios": "VBios 資訊:{{vbios}}" + }, + "closeInfo": { + "label": "關閉 GPU 資訊" + }, + "copyInfo": { + "label": "複製 GPU 訊息" + }, + "toast": { + "success": "已複製 GPU 訊息至剪貼簿" + } + }, + "npuUsage": "NPU 使用率", + "npuMemory": "NPU 記憶體", + "intelGpuWarning": { + "title": "Intel GPU 狀態警告", + "message": "GPU 狀態資訊不可用", + "description": "這是一個在Intel GPU 狀態回報工具 (intel_gpu_top) 中已知的 Bug,該工具會故障並重複的回報 GPU占用率為 0%,甚至在硬體加速與物件偵測在 (i)GPU上正確運作時也是如此。這不是 Frigate 的 Bug。您可以透過重新啟動主機來暫時修復此問題以確認 GPU 運作正常。這不會影響效能。" + }, + "gpuCompute": "GPU 計算 / 編碼", + "gpuTemperature": "GPU 溫度", + "npuTemperature": "NPU 溫度" + }, + "otherProcesses": { + "title": "其他行程", + "processCpuUsage": "行程 CPU 使用率", + "processMemoryUsage": "行程記憶體使用量", + "series": { + "recording": "记录", + "review_segment": "评论部分", + "embeddings": "嵌入", + "audio_detector": "音訊偵測器", + "go2rtc": "go2rtc" + } + } + }, + "storage": { + "title": "儲存裝置", + "overview": "總覽", + "recordings": { + "title": "錄影檔案", + "tips": "此數值僅代表 Frigate 資料庫中錄影資料的儲存空間用量。Frigate 不會追蹤硬碟上所有檔案的使用量。", + "earliestRecording": "最早的錄影檔案:" + }, + "cameraStorage": { + "title": "鏡頭儲存", + "camera": "鏡頭", + "unusedStorageInformation": "未使用的儲存空間資訊", + "storageUsed": "已使用的儲存空間", + "percentageOfTotalUsed": "佔總量百分比", + "bandwidth": "頻寬", + "unused": { + "title": "未使用", + "tips": "在磁碟中有除了 Frigate 錄影內容以外的檔案時,此數值可能無法正確反應可用的空間。Frigate 不會追蹤錄影資料以外的檔案的儲存空間用量。" + } + }, + "shm": { + "title": "SHM(共享記憶體)配置", + "warning": "目前的 SHM 大小為 {{total}}MB,過小。請將其增加至至少 {{min_shm}}MB。", + "frameLifetime": { + "title": "幀保留時間", + "description": "每個攝影機在共享記憶體中擁有 {{frames}} 個幀槽位。在最快攝影機的幀率下,每一幀在被覆蓋前大約可保留 {{lifetime}} 秒。" + } + } + }, + "cameras": { + "title": "鏡頭", + "overview": "總覽", + "info": { + "aspectRatio": "長寬比", + "cameraProbeInfo": "{{camera}} 的詳細資訊", + "streamDataFromFFPROBE": "串流資料是透過 ffprobe 取得。", + "fetching": "正在讀取鏡頭資訊", + "stream": "串流 {{idx}}", + "video": "影片:", + "codec": "編解碼器:", + "resolution": "解析度:", + "fps": "幀率:", + "unknown": "未知", + "audio": "音訊:", + "error": "錯誤:{{error}}", + "tips": { + "title": "鏡頭詳細資訊" + }, + "keyframes": { + "title": "關鍵幀分析", + "analyzing": "正在分析關鍵幀... 剩餘 {{seconds}} 秒", + "stillAnalyzing": "仍在分析關鍵幀...", + "recordStream": "錄影串流:", + "keyframeCount": "偵測到的關鍵幀:", + "observedDuration": "觀測持續時間:", + "gap": "關鍵幀間隔 (最小 / 平均 / 最大):", + "segmentLength": "錄影片段長度:", + "ok": "每約 {{seconds}} 秒一個關鍵幀,適合錄影與回放。", + "warning": "關鍵幀稀疏或不固定(最長間隔約 {{seconds}} 秒),可能使用了智慧編碼(H.264+/H.265+),不建議這樣設定。", + "error": "關鍵幀間隔(~{{seconds}} 秒)超過了錄影片段長度({{segmentTime}} 秒)。某些片段可能沒有關鍵幀,這會導致回放中斷。請停用攝影機上的智慧/+編碼,或縮短其關鍵幀間隔。", + "unknown": "無法確定關鍵幀間隔。", + "recordDisabled": "此攝影機已停用錄影功能。" + } + }, + "framesAndDetections": "幀數 / 偵測數", + "label": { + "camera": "鏡頭", + "detect": "偵測", + "skipped": "跳過", + "ffmpeg": "FFmpeg", + "capture": "抓取", + "overallFramesPerSecond": "總體幀率", + "overallDetectionsPerSecond": "總體每秒偵測幀數", + "overallSkippedDetectionsPerSecond": "總體每秒跳過偵測幀率", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} 抓取", + "cameraDetect": "{{camName}} 偵測", + "cameraFramesPerSecond": "{{camName}} 幀率", + "cameraDetectionsPerSecond": "{{camName}} 每秒偵測幀率", + "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒跳過偵測幀率", + "cameraGpu": "{{camName}} GPU" + }, + "toast": { + "success": { + "copyToClipboard": "已複製檢測資料至剪貼簿。" + }, + "error": { + "unableToProbeCamera": "無法檢測鏡頭:{{errorMessage}}" + } + }, + "noCameras": { + "title": "沒有找到攝影機" + }, + "connectionQuality": { + "title": "連線品質", + "excellent": "優秀", + "fair": "一般", + "poor": "較差", + "unusable": "不可用", + "fps": "幀率", + "expectedFps": "預期幀率", + "reconnectsLastHour": "最近一小時重連次數", + "stallsLastHour": "最近一小時卡頓次數" + } + }, + "lastRefreshed": "最後更新: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} 的 FFmpeg CPU 使用率較高 ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} 的偵測 CPU 使用率較高 ({{detectAvg}}%)", + "healthy": "系統運作正常", + "reindexingEmbeddings": "正在重新替嵌入資料建立索引(已完成 {{processed}}%)", + "cameraIsOffline": "{{camera}} 已離線", + "detectIsSlow": "{{detect}} 偵測速度較慢({{speed}} 毫秒)", + "detectIsVerySlow": "{{detect}} 偵測速度緩慢({{speed}} 毫秒)", + "shmTooLow": "/dev/shm 配置({{total}} MB)應增加至至少{{min}} MB。", + "debugReplayActive": "除錯回放工作階段正在進行" + }, + "enrichments": { + "title": "進階功能", + "infPerSecond": "每秒推理次數", + "embeddings": { + "image_embedding": "圖片特徵提取", + "text_embedding": "文字提取", + "face_recognition": "人臉辨識", + "plate_recognition": "車牌辨識", + "image_embedding_speed": "圖片特徵提取速度", + "face_embedding_speed": "人臉特徵提取速度", + "face_recognition_speed": "人臉辨識速度", + "plate_recognition_speed": "車牌辨識速度", + "text_embedding_speed": "文字提取速度", + "yolov9_plate_detection_speed": "YOLOv9 車牌偵測速度", + "yolov9_plate_detection": "YOLOv9 車牌辨識", + "review_description": "審查說明", + "review_description_speed": "審查描述速度", + "review_description_events_per_second": "審查說明", + "object_description": "物件說明", + "object_description_speed": "物件說明速度", + "object_description_events_per_second": "物件說明", + "classification": "{{name}} 分類", + "classification_speed": "{{name}}分類速度", + "classification_events_per_second": "{{name}} 分類每秒事件數" + }, + "averageInf": "平均推論時間" + } +} diff --git a/web/public/locales/zun/audio.json b/web/public/locales/zun/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/audio.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/common.json b/web/public/locales/zun/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/common.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/components/auth.json b/web/public/locales/zun/components/auth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/components/camera.json b/web/public/locales/zun/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/components/dialog.json b/web/public/locales/zun/components/dialog.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/components/filter.json b/web/public/locales/zun/components/filter.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/components/icons.json b/web/public/locales/zun/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/components/input.json b/web/public/locales/zun/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/components/input.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/components/player.json b/web/public/locales/zun/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/components/player.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/config/cameras.json b/web/public/locales/zun/config/cameras.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/config/cameras.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/config/global.json b/web/public/locales/zun/config/global.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/config/global.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/config/groups.json b/web/public/locales/zun/config/groups.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/config/groups.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/config/validation.json b/web/public/locales/zun/config/validation.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/config/validation.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/objects.json b/web/public/locales/zun/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/objects.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/chat.json b/web/public/locales/zun/views/chat.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/chat.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/classificationModel.json b/web/public/locales/zun/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/configEditor.json b/web/public/locales/zun/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/events.json b/web/public/locales/zun/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/events.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/explore.json b/web/public/locales/zun/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/exports.json b/web/public/locales/zun/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/faceLibrary.json b/web/public/locales/zun/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/live.json b/web/public/locales/zun/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/motionSearch.json b/web/public/locales/zun/views/motionSearch.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/motionSearch.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/recording.json b/web/public/locales/zun/views/recording.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/replay.json b/web/public/locales/zun/views/replay.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/replay.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/search.json b/web/public/locales/zun/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/settings.json b/web/public/locales/zun/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/zun/views/system.json b/web/public/locales/zun/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/web/public/locales/zun/views/system.json @@ -0,0 +1 @@ +{} diff --git a/web/public/notifications-worker.js b/web/public/notifications-worker.js new file mode 100644 index 0000000..b42a32a --- /dev/null +++ b/web/public/notifications-worker.js @@ -0,0 +1,96 @@ +// Notifications Worker + +self.addEventListener("push", function (event) { + // @ts-expect-error we know this exists + if (event.data) { + // @ts-expect-error we know this exists + const data = event.data.json(); + + let actions = []; + + switch (data.type ?? "unknown") { + case "alert": + actions = [ + { + action: "markReviewed", + title: "Mark as Reviewed", + }, + ]; + break; + } + + const notificationOptions = { + body: data.message, + icon: "/images/maskable-icon.png", + image: data.image, + badge: "/images/maskable-badge.png", + tag: data.id, + data: { id: data.id, link: data.direct_url }, + actions, + }; + + // iOS Safari does not auto-coalesce notifications by tag (WebKit bug #258922). + // On iOS 18.3+ close() works, so we manually close duplicates before showing. + // On other platforms, tag-based replacement works natively — skip the extra work. + const isIOS = + /iPad|iPhone|iPod/.test(navigator.userAgent) && !self.MSStream; + + const show = () => + // @ts-expect-error we know this exists + self.registration.showNotification(data.title, notificationOptions); + + // event.waitUntil is required on iOS Safari — without it, the browser + // may consider this a "silent push" and revoke the subscription after 3 occurrences. + event.waitUntil( + isIOS + ? // @ts-expect-error we know this exists + self.registration + .getNotifications({ tag: data.id }) + .then((existing) => { + for (const n of existing) { + n.close(); + } + }) + .then(show) + : show(), // eslint-disable-line comma-dangle + ); + } else { + // pass + // This push event has no data + } +}); + +self.addEventListener("notificationclick", (event) => { + // @ts-expect-error we know this exists + if (event.notification) { + // @ts-expect-error we know this exists + event.notification.close(); + + switch (event.action ?? "default") { + case "markReviewed": + if (event.notification.data) { + event.waitUntil( + fetch("/api/reviews/viewed", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": 1, + }, + body: JSON.stringify({ ids: [event.notification.data.id] }), + }), // eslint-disable-line comma-dangle + ); + } + break; + default: + // @ts-expect-error we know this exists + if (event.notification.data) { + const url = event.notification.data.link; + // eslint-disable-next-line no-undef + if (clients.openWindow) { + // eslint-disable-next-line no-undef + event.waitUntil(clients.openWindow(url)); + } + } + } + } +}); diff --git a/web/public/robots.txt b/web/public/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/web/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/web/site.webmanifest b/web/site.webmanifest new file mode 100644 index 0000000..7040ce5 --- /dev/null +++ b/web/site.webmanifest @@ -0,0 +1,34 @@ +{ + "name": "Frigate", + "short_name": "Frigate", + "start_url": "/BASE_PATH/", + "icons": [ + { + "src": "/BASE_PATH/images/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/BASE_PATH/images/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/BASE_PATH/images/maskable-icon.png", + "sizes": "180x180", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/BASE_PATH/images/maskable-badge.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..550bc36 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,134 @@ +import Providers from "@/context/providers"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Wrapper from "@/components/Wrapper"; +import Sidebar from "@/components/navigation/Sidebar"; + +import { isDesktop, isMobile } from "react-device-detect"; +import Statusbar from "./components/Statusbar"; +import Bottombar from "./components/navigation/Bottombar"; +import { Suspense, lazy } from "react"; +import { Redirect } from "./components/navigation/Redirect"; +import { cn } from "./lib/utils"; +import { isPWA } from "./utils/isPWA"; +import ProtectedRoute from "@/components/auth/ProtectedRoute"; +import useSWR from "swr"; +import { FrigateConfig } from "./types/frigateConfig"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { isRedirectingToLogin } from "@/api/auth-redirect"; + +const Live = lazy(() => import("@/pages/Live")); +const Events = lazy(() => import("@/pages/Events")); +const Explore = lazy(() => import("@/pages/Explore")); +const Exports = lazy(() => import("@/pages/Exports")); +const ConfigEditor = lazy(() => import("@/pages/ConfigEditor")); +const System = lazy(() => import("@/pages/System")); +const Settings = lazy(() => import("@/pages/Settings")); +const UIPlayground = lazy(() => import("@/pages/UIPlayground")); +const FaceLibrary = lazy(() => import("@/pages/FaceLibrary")); +const Classification = lazy(() => import("@/pages/ClassificationModel")); +const Chat = lazy(() => import("@/pages/Chat")); +const Logs = lazy(() => import("@/pages/Logs")); +const AccessDenied = lazy(() => import("@/pages/AccessDenied")); +const Replay = lazy(() => import("@/pages/Replay")); + +function App() { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + return ( + + + + {config?.safe_mode ? : } + + + + ); +} + +function DefaultAppView() { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + // Compute required roles for main routes, ensuring we have config first + // to prevent race condition where custom roles are temporarily unavailable + const mainRouteRoles = config?.auth?.roles + ? Object.keys(config.auth.roles) + : undefined; + + // Show loading indicator during redirect to prevent React from attempting to render + // lazy components, which would cause error #426 (suspension during synchronous navigation) + if (isRedirectingToLogin()) { + return ( +
    + +
    + ); + } + + return ( +
    + {isDesktop && } + {isDesktop && } + {isMobile && } +
    + + } + > + + }> + } /> + } /> + } /> + } /> + } /> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } />{" "} + } />{" "} + + } /> + } /> + + +
    +
    + ); +} + +function SafeAppView() { + return ( +
    +
    + + + +
    +
    + ); +} + +export default App; diff --git a/web/src/api/WsProvider.tsx b/web/src/api/WsProvider.tsx new file mode 100644 index 0000000..eb0d763 --- /dev/null +++ b/web/src/api/WsProvider.tsx @@ -0,0 +1,103 @@ +import { baseUrl } from "./baseUrl"; +import { ReactNode, useCallback, useEffect, useRef } from "react"; +import { WsSendContext } from "./wsContext"; +import type { Update } from "./wsContext"; +import { + invalidateCameraActivityCache, + processWsMessage, + resetWsStore, +} from "./ws"; + +export function WsProvider({ children }: { children: ReactNode }) { + const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`; + const wsRef = useRef(null); + const reconnectTimer = useRef | null>(null); + const reconnectAttempt = useRef(0); + const unmounted = useRef(false); + const pendingSends = useRef>(new Map()); + + const sendJsonMessage = useCallback((msg: unknown) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify(msg)); + } else if (msg && typeof msg === "object" && "topic" in msg) { + // Sends issued before the socket reaches OPEN (or during a reconnect + // window) are buffered here and flushed in onopen + pendingSends.current.set(String((msg as { topic: unknown }).topic), msg); + } + }, []); + + useEffect(() => { + unmounted.current = false; + const queue = pendingSends.current; + + function connect() { + if (unmounted.current) return; + + const ws = new WebSocket(wsUrl); + wsRef.current = ws; + + ws.onopen = () => { + reconnectAttempt.current = 0; + // events may have been missed while disconnected — the snapshot + // requested below must fully apply even if byte-identical + invalidateCameraActivityCache(); + ws.send( + JSON.stringify({ topic: "onConnect", message: "", retain: false }), + ); + for (const queued of queue.values()) { + ws.send(JSON.stringify(queued)); + } + queue.clear(); + }; + + ws.onmessage = (event: MessageEvent) => { + processWsMessage(event.data as string); + }; + + ws.onclose = () => { + if (unmounted.current) return; + const delay = Math.min(1000 * 2 ** reconnectAttempt.current, 30000); + reconnectAttempt.current++; + reconnectTimer.current = setTimeout(connect, delay); + }; + + ws.onerror = () => { + ws.close(); + }; + } + + connect(); + + return () => { + unmounted.current = true; + if (reconnectTimer.current) { + clearTimeout(reconnectTimer.current); + } + const ws = wsRef.current; + if (ws) { + ws.onopen = null; + ws.onmessage = null; + ws.onclose = null; + ws.onerror = null; + ws.close(); + } + queue.clear(); + resetWsStore(); + }; + }, [wsUrl]); + + const send = useCallback( + (message: Update) => { + sendJsonMessage({ + topic: message.topic, + payload: message.payload, + retain: message.retain, + }); + }, + [sendJsonMessage], + ); + + return ( + {children} + ); +} diff --git a/web/src/api/auth-redirect.ts b/web/src/api/auth-redirect.ts new file mode 100644 index 0000000..f19e2b8 --- /dev/null +++ b/web/src/api/auth-redirect.ts @@ -0,0 +1,12 @@ +// Module-level flag to prevent multiple simultaneous redirects +// (eg, when multiple SWR queries fail with 401 at once, or when +// both ApiProvider and ProtectedRoute try to redirect) +let _isRedirectingToLogin = false; + +export function isRedirectingToLogin(): boolean { + return _isRedirectingToLogin; +} + +export function setRedirectingToLogin(value: boolean): void { + _isRedirectingToLogin = value; +} diff --git a/web/src/api/baseUrl.ts b/web/src/api/baseUrl.ts new file mode 100644 index 0000000..fb7faa6 --- /dev/null +++ b/web/src/api/baseUrl.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + baseUrl?: string; + } +} + +export const baseUrl = `${window.location.protocol}//${window.location.host}${window.baseUrl || "/"}`; diff --git a/web/src/api/index.tsx b/web/src/api/index.tsx new file mode 100644 index 0000000..41cb7d2 --- /dev/null +++ b/web/src/api/index.tsx @@ -0,0 +1,60 @@ +import { baseUrl } from "./baseUrl"; +import { SWRConfig } from "swr"; +import { WsProvider } from "./WsProvider"; +import axios from "axios"; +import { ReactNode } from "react"; +import { isRedirectingToLogin, setRedirectingToLogin } from "./auth-redirect"; + +axios.defaults.baseURL = `${baseUrl}api/`; + +type ApiProviderType = { + children?: ReactNode; + options?: Record; +}; + +export function ApiProvider({ children, options }: ApiProviderType) { + axios.defaults.headers.common = { + "X-CSRF-TOKEN": 1, + "X-CACHE-BYPASS": 1, + }; + + return ( + { + const [path, params] = Array.isArray(key) ? key : [key, undefined]; + return axios.get(path, { params }).then((res) => res.data); + }, + onError: (error, _key) => { + if ( + error.response && + [401, 302, 307].includes(error.response.status) + ) { + // redirect to the login page if not already there + const loginPage = error.response.headers.get("location") ?? "login"; + if (window.location.href !== loginPage && !isRedirectingToLogin()) { + setRedirectingToLogin(true); + window.location.href = loginPage; + } + } + }, + ...options, + }} + > + {children} + + ); +} + +type WsWithConfigType = { + children: ReactNode; +}; + +function WsWithConfig({ children }: WsWithConfigType) { + return {children}; +} + +// eslint-disable-next-line react-refresh/only-export-components +export function useApiHost() { + return baseUrl; +} diff --git a/web/src/api/ws.ts b/web/src/api/ws.ts new file mode 100644 index 0000000..0a91a88 --- /dev/null +++ b/web/src/api/ws.ts @@ -0,0 +1,886 @@ +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useSyncExternalStore, +} from "react"; +import { + EmbeddingsReindexProgressType, + FrigateCameraState, + FrigateEvent, + FrigateReview, + ModelState, + ToggleableSetting, + TrackedObjectUpdateReturnType, + TriggerStatus, + FrigateAudioDetections, + Job, +} from "@/types/ws"; +import { FrigateStats } from "@/types/stats"; +import { isEqual } from "lodash"; +import { WsSendContext } from "./wsContext"; +import type { Update, WsSend } from "./wsContext"; + +export type { Update }; + +export type WsFeedMessage = { + topic: string; + payload: unknown; + timestamp: number; + id: string; +}; + +type WsState = { + [topic: string]: unknown; +}; + +// External store for WebSocket state using useSyncExternalStore +type Listener = () => void; + +const wsState: WsState = {}; +const wsTopicListeners = new Map>(); + +// Reset all module-level state. Called on WsProvider unmount to prevent +// stale data from leaking across mount/unmount cycles (e.g. HMR, logout) +export function resetWsStore() { + for (const key of Object.keys(wsState)) { + delete wsState[key]; + } + wsTopicListeners.clear(); + lastCameraActivityPayload = null; + wsMessageSubscribers.clear(); + wsMessageIdCounter = 0; +} + +// Parse and apply a raw WS message synchronously. +// Called directly from WsProvider's onmessage handler. +export function processWsMessage(raw: string) { + const data: Update = JSON.parse(raw); + if (!data) return; + + const { topic, payload } = data; + + if (topic === "camera_activity") { + applyCameraActivity(payload as string); + } else { + applyTopicUpdate(topic, payload); + } + + if (wsMessageSubscribers.size > 0) { + wsMessageSubscribers.forEach((cb) => + cb({ + topic, + payload, + timestamp: Date.now(), + id: String(wsMessageIdCounter++), + }), + ); + } +} + +function applyTopicUpdate(topic: string, newVal: unknown) { + const oldVal = wsState[topic]; + // camera_activity snapshots always re-notify: consumers reconcile local + // state that may have diverged from an unchanged snapshot + const isActivitySnapshot = topic.startsWith("camera_activity/"); + // Fast path: === for primitives ("ON"/"OFF", numbers). + // Fall back to isEqual for objects/arrays. + const unchanged = + oldVal === newVal || + (typeof newVal === "object" && newVal !== null && isEqual(oldVal, newVal)); + if (unchanged && !isActivitySnapshot) return; + + wsState[topic] = newVal; + // Snapshot the Set — a listener may trigger unmount that modifies it. + const listeners = wsTopicListeners.get(topic); + if (listeners) { + for (const l of Array.from(listeners)) l(); + } +} + +// Subscriptions + +export function subscribeWsTopic( + topic: string, + listener: Listener, +): () => void { + let set = wsTopicListeners.get(topic); + if (!set) { + set = new Set(); + wsTopicListeners.set(topic, set); + } + set.add(listener); + return () => { + set!.delete(listener); + if (set!.size === 0) wsTopicListeners.delete(topic); + }; +} + +export function getWsTopicValue(topic: string): unknown { + return wsState[topic]; +} + +// Feed message subscribers +const wsMessageSubscribers = new Set<(msg: WsFeedMessage) => void>(); +let wsMessageIdCounter = 0; + +// Camera activity expansion +// +// Cache the last raw camera_activity JSON string so we can skip JSON.parse +// and the entire expansion when nothing has changed. This avoids creating +// fresh objects (which defeat Object.is and force expensive isEqual deep +// traversals) on every flush — critical with many cameras. +let lastCameraActivityPayload: string | null = null; + +// Make the next camera_activity snapshot fully apply even when byte-identical +// to the previous one — local state may have diverged while no messages flowed +export function invalidateCameraActivityCache() { + lastCameraActivityPayload = null; +} + +// Collapse same-task resync requests (one hook per camera card) into a +// single onConnect round-trip +let resyncScheduled = false; +function requestCameraActivityResync(sendOnConnect: () => void) { + if (resyncScheduled) return; + resyncScheduled = true; + queueMicrotask(() => { + resyncScheduled = false; + invalidateCameraActivityCache(); + sendOnConnect(); + }); +} + +function applyCameraActivity(payload: string) { + // Fast path: if the raw JSON string is identical, nothing changed. + if (payload === lastCameraActivityPayload) return; + lastCameraActivityPayload = payload; + + let activity: { [key: string]: Partial }; + + try { + activity = JSON.parse(payload); + } catch { + return; + } + + if (Object.keys(activity).length === 0) return; + + for (const [name, state] of Object.entries(activity)) { + applyTopicUpdate(`camera_activity/${name}`, state); + + const cameraConfig = state?.config; + if (!cameraConfig) continue; + + const { + record, + detect, + enabled, + snapshots, + audio, + audio_transcription, + notifications, + notifications_suspended, + autotracking, + alerts, + detections, + object_descriptions, + review_descriptions, + } = cameraConfig; + + applyTopicUpdate(`${name}/recordings/state`, record ? "ON" : "OFF"); + applyTopicUpdate(`${name}/enabled/state`, enabled ? "ON" : "OFF"); + applyTopicUpdate(`${name}/detect/state`, detect ? "ON" : "OFF"); + applyTopicUpdate(`${name}/snapshots/state`, snapshots ? "ON" : "OFF"); + applyTopicUpdate(`${name}/audio/state`, audio ? "ON" : "OFF"); + applyTopicUpdate( + `${name}/audio_transcription/state`, + audio_transcription ? "ON" : "OFF", + ); + applyTopicUpdate( + `${name}/notifications/state`, + notifications ? "ON" : "OFF", + ); + applyTopicUpdate( + `${name}/notifications/suspended`, + notifications_suspended || 0, + ); + applyTopicUpdate( + `${name}/ptz_autotracker/state`, + autotracking ? "ON" : "OFF", + ); + applyTopicUpdate(`${name}/review_alerts/state`, alerts ? "ON" : "OFF"); + applyTopicUpdate( + `${name}/review_detections/state`, + detections ? "ON" : "OFF", + ); + applyTopicUpdate( + `${name}/object_descriptions/state`, + object_descriptions ? "ON" : "OFF", + ); + applyTopicUpdate( + `${name}/review_descriptions/state`, + review_descriptions ? "ON" : "OFF", + ); + } +} + +// Hooks +export function useWsUpdate(): WsSend { + const send = useContext(WsSendContext); + if (!send) { + throw new Error("useWsUpdate must be used within WsProvider"); + } + return send; +} + +// Subscribe to a single WS topic with proper bail-out. +// Only re-renders when the topic's value changes (Object.is comparison). +// Uses useSyncExternalStore — zero useEffect, so no PassiveMask flags +// propagate through the fiber tree. +export function useWs(watchTopic: string, publishTopic: string) { + const payload = useSyncExternalStore( + useCallback( + (listener: Listener) => subscribeWsTopic(watchTopic, listener), + [watchTopic], + ), + useCallback(() => wsState[watchTopic], [watchTopic]), + ); + + const sendJsonMessage = useWsUpdate(); + + const value = { payload: payload ?? null }; + + const send = useCallback( + (payload: unknown, retain = false) => { + sendJsonMessage({ + topic: publishTopic || watchTopic, + payload, + retain, + }); + }, + [sendJsonMessage, watchTopic, publishTopic], + ); + + return { value, send }; +} + +// Convenience hooks + +export function useEnabledState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/enabled/state`, `${camera}/enabled/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useDetectState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/detect/state`, `${camera}/detect/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useRecordingsState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/recordings/state`, `${camera}/recordings/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useSnapshotsState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/snapshots/state`, `${camera}/snapshots/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useAudioState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/audio/state`, `${camera}/audio/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useAudioTranscriptionState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/audio_transcription/state`, + `${camera}/audio_transcription/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useAutotrackingState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/ptz_autotracker/state`, `${camera}/ptz_autotracker/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useAlertsState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/review_alerts/state`, `${camera}/review_alerts/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useDetectionsState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/review_detections/state`, + `${camera}/review_detections/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useObjectDescriptionState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/object_descriptions/state`, + `${camera}/object_descriptions/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useReviewDescriptionState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/review_descriptions/state`, + `${camera}/review_descriptions/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useMotionMaskState( + camera: string, + maskName: string, +): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/motion_mask/${maskName}/state`, + `${camera}/motion_mask/${maskName}/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useObjectMaskState( + camera: string, + maskName: string, +): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/object_mask/${maskName}/state`, + `${camera}/object_mask/${maskName}/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useZoneState( + camera: string, + zoneName: string, +): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/zone/${zoneName}/state`, + `${camera}/zone/${zoneName}/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function usePtzCommand(camera: string): { + payload: string; + send: (payload: string, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/ptz`, `${camera}/ptz`); + return { payload: payload as string, send }; +} + +export function useRestart(): { + payload: string; + send: (payload: string, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs("restart", "restart"); + return { payload: payload as string, send }; +} + +export function useFrigateEvents(): { payload: FrigateEvent } { + const { + value: { payload }, + } = useWs("events", ""); + const parsed = useMemo( + () => (payload ? JSON.parse(payload as string) : undefined), + [payload], + ); + return { payload: parsed }; +} + +export function useAudioDetections(): { payload: FrigateAudioDetections } { + const { + value: { payload }, + } = useWs("audio_detections", ""); + const parsed = useMemo( + () => (payload ? JSON.parse(payload as string) : undefined), + [payload], + ); + return { payload: parsed }; +} + +export function useFrigateReviews(): FrigateReview { + const { + value: { payload }, + } = useWs("reviews", ""); + return useMemo( + () => (payload ? JSON.parse(payload as string) : undefined), + [payload], + ); +} + +export function useFrigateStats(): FrigateStats { + const { + value: { payload }, + } = useWs("stats", ""); + return useMemo( + () => (payload ? JSON.parse(payload as string) : undefined), + [payload], + ); +} + +export function useInitialCameraState( + camera: string, + revalidateOnFocus: boolean, +): { + payload: FrigateCameraState; +} { + const { + value: { payload }, + send: sendCommand, + } = useWs(`camera_activity/${camera}`, "onConnect"); + + // camera_activity sub-topic payload is already parsed by expandCameraActivity + const data = payload as FrigateCameraState | undefined; + + // the cached snapshot is only written on onConnect and can be stale by the + // time this hook mounts — re-request on mount and when the user tabs back in + useEffect(() => { + if (!revalidateOnFocus) return; + + requestCameraActivityResync(() => sendCommand("onConnect")); + + const listener = () => { + if (document.visibilityState === "visible") { + requestCameraActivityResync(() => sendCommand("onConnect")); + } + }; + addEventListener("visibilitychange", listener); + + return () => { + removeEventListener("visibilitychange", listener); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data as FrigateCameraState }; +} + +export function useModelState( + model: string, + revalidateOnFocus: boolean = true, +): { payload: ModelState } { + const { + value: { payload }, + send: sendCommand, + } = useWs("model_state", "modelState"); + + const data = useMemo( + () => (payload ? JSON.parse(payload as string) : undefined), + [payload], + ); + + useEffect(() => { + let listener = undefined; + if (revalidateOnFocus) { + sendCommand("modelState"); + listener = () => { + if (document.visibilityState == "visible") { + sendCommand("modelState"); + } + }; + addEventListener("visibilitychange", listener); + } + + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data ? data[model] : undefined }; +} + +export function useEmbeddingsReindexProgress( + revalidateOnFocus: boolean = true, +): { + payload: EmbeddingsReindexProgressType; +} { + const { + value: { payload }, + send: sendCommand, + } = useWs("embeddings_reindex_progress", "embeddingsReindexProgress"); + + const data = useMemo( + () => (payload ? JSON.parse(payload as string) : undefined), + [payload], + ); + + useEffect(() => { + let listener = undefined; + if (revalidateOnFocus) { + sendCommand("embeddingsReindexProgress"); + listener = () => { + if (document.visibilityState == "visible") { + sendCommand("embeddingsReindexProgress"); + } + }; + addEventListener("visibilitychange", listener); + } + + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data }; +} + +export function useAudioTranscriptionProcessState( + revalidateOnFocus: boolean = true, +): { payload: string } { + const { + value: { payload }, + send: sendCommand, + } = useWs("audio_transcription_state", "audioTranscriptionState"); + + const data = useMemo( + () => (payload ? (JSON.parse(payload as string) as string) : "idle"), + [payload], + ); + + useEffect(() => { + let listener = undefined; + if (revalidateOnFocus) { + sendCommand("audioTranscriptionState"); + listener = () => { + if (document.visibilityState == "visible") { + sendCommand("audioTranscriptionState"); + } + }; + addEventListener("visibilitychange", listener); + } + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data || "idle" }; +} + +export function useBirdseyeLayout(revalidateOnFocus: boolean = true): { + payload: string; +} { + const { + value: { payload }, + send: sendCommand, + } = useWs("birdseye_layout", "birdseyeLayout"); + + const data = useMemo( + () => (payload ? JSON.parse(payload as string) : undefined), + [payload], + ); + + useEffect(() => { + let listener = undefined; + if (revalidateOnFocus) { + sendCommand("birdseyeLayout"); + listener = () => { + if (document.visibilityState == "visible") { + sendCommand("birdseyeLayout"); + } + }; + addEventListener("visibilitychange", listener); + } + + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data }; +} + +export function useMotionActivity(camera: string): { payload: string } { + const { + value: { payload }, + } = useWs(`${camera}/motion`, ""); + return { payload: payload as string }; +} + +export function useAudioActivity(camera: string): { payload: number } { + const { + value: { payload }, + } = useWs(`${camera}/audio/rms`, ""); + return { payload: payload as number }; +} + +export function useAudioLiveTranscription(camera: string): { + payload: string; +} { + const { + value: { payload }, + } = useWs(`${camera}/audio/transcription`, ""); + return { payload: payload as string }; +} + +export function useMotionThreshold(camera: string): { + payload: string; + send: (payload: number, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/motion_threshold/state`, + `${camera}/motion_threshold/set`, + ); + return { payload: payload as string, send }; +} + +export function useMotionContourArea(camera: string): { + payload: string; + send: (payload: number, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/motion_contour_area/state`, + `${camera}/motion_contour_area/set`, + ); + return { payload: payload as string, send }; +} + +export function useImproveContrast(camera: string): { + payload: ToggleableSetting; + send: (payload: string, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/improve_contrast/state`, + `${camera}/improve_contrast/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useTrackedObjectUpdate(): { + payload: TrackedObjectUpdateReturnType; +} { + const { + value: { payload }, + } = useWs("tracked_object_update", ""); + const parsed = useMemo( + () => + payload + ? JSON.parse(payload as string) + : { type: "", id: "", camera: "" }, + [payload], + ); + return { payload: parsed }; +} + +export function useNotifications(camera: string): { + payload: ToggleableSetting; + send: (payload: string, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/notifications/state`, `${camera}/notifications/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useNotificationSuspend(camera: string): { + payload: string; + send: (payload: number, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/notifications/suspended`, + `${camera}/notifications/suspend`, + ); + return { payload: payload as string, send }; +} + +export function useNotificationTest(): { + payload: string; + send: (payload: string, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs("notification_test", "notification_test"); + return { payload: payload as string, send }; +} + +export function useTriggers(): { payload: TriggerStatus } { + const { + value: { payload }, + } = useWs("triggers", ""); + const parsed = useMemo( + () => + payload + ? JSON.parse(payload as string) + : { name: "", camera: "", event_id: "", type: "", score: 0 }, + [payload], + ); + return { payload: parsed }; +} + +export function useJobStatus( + jobType: string, + revalidateOnFocus: boolean = true, +): { payload: Job | null } { + const { + value: { payload }, + send: sendCommand, + } = useWs("job_state", "jobState"); + + const jobData = useMemo( + () => (payload && typeof payload === "string" ? JSON.parse(payload) : {}), + [payload], + ); + const currentJob = jobData[jobType] || null; + + useEffect(() => { + let listener: (() => void) | undefined; + if (revalidateOnFocus) { + sendCommand("jobState"); + listener = () => { + if (document.visibilityState === "visible") { + sendCommand("jobState"); + } + }; + addEventListener("visibilitychange", listener); + } + + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: currentJob as Job | null }; +} + +export function useWsMessageSubscribe(callback: (msg: WsFeedMessage) => void) { + const callbackRef = useRef(callback); + callbackRef.current = callback; + + useEffect(() => { + const handler = (msg: WsFeedMessage) => callbackRef.current(msg); + wsMessageSubscribers.add(handler); + return () => { + wsMessageSubscribers.delete(handler); + }; + }, []); +} diff --git a/web/src/api/wsContext.ts b/web/src/api/wsContext.ts new file mode 100644 index 0000000..15fee8f --- /dev/null +++ b/web/src/api/wsContext.ts @@ -0,0 +1,11 @@ +import { createContext } from "react"; + +export type Update = { + topic: string; + payload: unknown; + retain: boolean; +}; + +export type WsSend = (update: Update) => void; + +export const WsSendContext = createContext(null); diff --git a/web/src/components/Logo.tsx b/web/src/components/Logo.tsx new file mode 100644 index 0000000..ca2897a --- /dev/null +++ b/web/src/components/Logo.tsx @@ -0,0 +1,12 @@ +import { cn } from "@/lib/utils"; + +type LogoProps = { + className?: string; +}; +export default function Logo({ className }: LogoProps) { + return ( + + + + ); +} diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx new file mode 100644 index 0000000..4e7c405 --- /dev/null +++ b/web/src/components/Statusbar.tsx @@ -0,0 +1,226 @@ +import { useEmbeddingsReindexProgress } from "@/api/ws"; +import { + StatusBarMessagesContext, + StatusMessage, +} from "@/context/statusbar-provider"; +import useStats, { useAutoFrigateStats } from "@/hooks/use-stats"; +import { cn } from "@/lib/utils"; +import type { ProfilesApiResponse } from "@/types/profile"; +import { getProfileColor } from "@/utils/profileColors"; +import { useIsAdmin } from "@/hooks/use-is-admin"; +import { useContext, useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; + +import { FaCheck } from "react-icons/fa"; +import { IoIosWarning } from "react-icons/io"; +import { MdCircle } from "react-icons/md"; +import { Link } from "react-router-dom"; + +export default function Statusbar() { + const { t } = useTranslation(["views/system"]); + const isAdmin = useIsAdmin(); + + const { messages, addMessage, clearMessages } = useContext( + StatusBarMessagesContext, + )!; + + const stats = useAutoFrigateStats(); + + const cpuPercent = useMemo(() => { + const systemCpu = stats?.cpu_usages["frigate.full_system"]?.cpu; + + if (!systemCpu || systemCpu == "0.0") { + return null; + } + + return parseInt(systemCpu); + }, [stats]); + + const { potentialProblems } = useStats(stats); + + useEffect(() => { + clearMessages("stats"); + potentialProblems.forEach((problem) => { + addMessage( + "stats", + problem.text, + problem.color, + undefined, + problem.relevantLink, + ); + }); + }, [potentialProblems, addMessage, clearMessages]); + + const { data: profilesData } = useSWR("profiles"); + + const activeProfile = useMemo(() => { + if (!profilesData?.active_profile || !profilesData.profiles) return null; + const info = profilesData.profiles.find( + (p) => p.name === profilesData.active_profile, + ); + const allNames = profilesData.profiles.map((p) => p.name).sort(); + return { + name: profilesData.active_profile, + friendlyName: info?.friendly_name ?? profilesData.active_profile, + color: getProfileColor(profilesData.active_profile, allNames), + }; + }, [profilesData]); + + const { payload: reindexState } = useEmbeddingsReindexProgress(); + + useEffect(() => { + if (reindexState) { + if (reindexState.status == "indexing") { + clearMessages("embeddings-reindex"); + addMessage( + "embeddings-reindex", + t("stats.reindexingEmbeddings", { + processed: Math.floor( + (reindexState.processed_objects / reindexState.total_objects) * + 100, + ), + }), + ); + } + if (reindexState.status === "completed") { + clearMessages("embeddings-reindex"); + } + } + }, [reindexState, addMessage, clearMessages, t]); + + return ( +
    +
    + {cpuPercent && ( + +
    + + CPU {cpuPercent}% +
    + + )} + {Object.entries(stats?.gpu_usages || {}).map(([name, stats]) => { + if (name == "error-gpu") { + return; + } + + let gpuTitle; + switch (name) { + case "amd-vaapi": + gpuTitle = "AMD GPU"; + break; + case "intel-gpu": + gpuTitle = "Intel GPU"; + break; + case "rockchip": + gpuTitle = "Rockchip GPU"; + break; + default: + gpuTitle = name; + break; + } + + const gpu = parseInt(stats.gpu); + + if (isNaN(gpu)) { + return; + } + + return ( + + {" "} +
    + + {gpuTitle} {gpu}% +
    + + ); + })} + {activeProfile && + (isAdmin ? ( + +
    + + + {activeProfile.friendlyName} + +
    + + ) : ( +
    + + + {activeProfile.friendlyName} + +
    + ))} +
    +
    + {Object.entries(messages).length === 0 ? ( +
    + + {t("stats.healthy")} +
    + ) : ( + Object.entries(messages).map(([key, messageArray]) => ( +
    + {messageArray.map(({ text, color, link }: StatusMessage) => { + const message = ( +
    + + {text} +
    + ); + + if (link) { + return ( + + {message} + + ); + } else { + return message; + } + })} +
    + )) + )} +
    +
    + ); +} diff --git a/web/src/components/Wrapper.tsx b/web/src/components/Wrapper.tsx new file mode 100644 index 0000000..4b1d389 --- /dev/null +++ b/web/src/components/Wrapper.tsx @@ -0,0 +1,11 @@ +import { ReactNode } from "react"; + +type TWrapperProps = { + children: ReactNode; +}; + +const Wrapper = ({ children }: TWrapperProps) => { + return
    {children}
    ; +}; + +export default Wrapper; diff --git a/web/src/components/audio/AudioLevelGraph.tsx b/web/src/components/audio/AudioLevelGraph.tsx new file mode 100644 index 0000000..74c3ce0 --- /dev/null +++ b/web/src/components/audio/AudioLevelGraph.tsx @@ -0,0 +1,166 @@ +import { useEffect, useMemo, useState, useCallback } from "react"; +import { MdCircle } from "react-icons/md"; +import Chart from "react-apexcharts"; +import { useTheme } from "@/context/theme-provider"; +import { useWs } from "@/api/ws"; +import { useDateLocale } from "@/hooks/use-date-locale"; +import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useTranslation } from "react-i18next"; +import { useTimeFormat } from "@/hooks/use-date-utils"; + +const GRAPH_COLORS = ["#3b82f6", "#ef4444"]; // RMS, dBFS + +interface AudioLevelGraphProps { + cameraName: string; +} + +export function AudioLevelGraph({ cameraName }: AudioLevelGraphProps) { + const [audioData, setAudioData] = useState< + { timestamp: number; rms: number; dBFS: number }[] + >([]); + const [maxDataPoints] = useState(50); + + // config for time formatting + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const locale = useDateLocale(); + const { t } = useTranslation(["common"]); + + const { + value: { payload: audioRms }, + } = useWs(`${cameraName}/audio/rms`, ""); + const { + value: { payload: audioDBFS }, + } = useWs(`${cameraName}/audio/dBFS`, ""); + + useEffect(() => { + if (typeof audioRms === "number") { + const now = Date.now(); + setAudioData((prev) => { + const next = [ + ...prev, + { + timestamp: now, + rms: audioRms, + dBFS: typeof audioDBFS === "number" ? audioDBFS : 0, + }, + ]; + return next.slice(-maxDataPoints); + }); + } + }, [audioRms, audioDBFS, maxDataPoints]); + + const series = useMemo( + () => [ + { + name: "RMS", + data: audioData.map((p) => ({ x: p.timestamp, y: p.rms })), + }, + { + name: "dBFS", + data: audioData.map((p) => ({ x: p.timestamp, y: p.dBFS })), + }, + ], + [audioData], + ); + + const lastValues = useMemo(() => { + if (!audioData.length) return undefined; + const last = audioData[audioData.length - 1]; + return [last.rms, last.dBFS]; + }, [audioData]); + + const timeFormat = useTimeFormat(config); + const formatString = useMemo( + () => + t(`time.formattedTimestampHourMinuteSecond.${timeFormat}`, { + ns: "common", + }), + [t, timeFormat], + ); + + const formatTime = useCallback( + (val: unknown) => { + const seconds = Math.round(Number(val) / 1000); + return formatUnixTimestampToDateTime(seconds, { + timezone: config?.ui.timezone, + date_format: formatString, + locale, + }); + }, + [config?.ui.timezone, formatString, locale], + ); + + const { theme, systemTheme } = useTheme(); + + const options = useMemo(() => { + return { + chart: { + id: `${cameraName}-audio`, + selection: { enabled: false }, + toolbar: { show: false }, + zoom: { enabled: false }, + animations: { enabled: false }, + }, + colors: GRAPH_COLORS, + grid: { + show: true, + borderColor: "#374151", + strokeDashArray: 3, + xaxis: { lines: { show: true } }, + yaxis: { lines: { show: true } }, + }, + legend: { show: false }, + dataLabels: { enabled: false }, + stroke: { width: 1 }, + markers: { size: 0 }, + tooltip: { + theme: systemTheme || theme, + x: { formatter: (val: number) => formatTime(val) }, + y: { formatter: (v: number) => v.toFixed(1) }, + }, + xaxis: { + type: "datetime", + labels: { + rotate: 0, + formatter: formatTime, + style: { colors: "#6B6B6B", fontSize: "10px" }, + }, + axisBorder: { show: false }, + axisTicks: { show: false }, + }, + yaxis: { + show: true, + labels: { + formatter: (val: number) => Math.round(val).toString(), + style: { colors: "#6B6B6B", fontSize: "10px" }, + }, + }, + } as ApexCharts.ApexOptions; + }, [cameraName, theme, systemTheme, formatTime]); + + return ( +
    + {lastValues && ( +
    + {["RMS", "dBFS"].map((label, idx) => ( +
    + +
    {label}
    +
    + {lastValues[idx].toFixed(1)} +
    +
    + ))} +
    + )} + +
    + ); +} diff --git a/web/src/components/auth/AuthForm.tsx b/web/src/components/auth/AuthForm.tsx new file mode 100644 index 0000000..e621d12 --- /dev/null +++ b/web/src/components/auth/AuthForm.tsx @@ -0,0 +1,170 @@ +"use client"; + +import * as React from "react"; + +import { baseUrl } from "../../api/baseUrl"; +import { cn } from "@/lib/utils"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import axios, { AxiosError } from "axios"; +import { Toaster } from "@/components/ui/sonner"; +import { toast } from "sonner"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { AuthContext } from "@/context/auth-context"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { LuExternalLink } from "react-icons/lu"; +import { useDocDomain } from "@/hooks/use-doc-domain"; +import { Card, CardContent } from "@/components/ui/card"; + +interface UserAuthFormProps extends React.HTMLAttributes {} + +export function UserAuthForm({ className, ...props }: UserAuthFormProps) { + const { t } = useTranslation(["components/auth", "common"]); + const { getLocaleDocUrl } = useDocDomain(); + const [isLoading, setIsLoading] = React.useState(false); + const { login } = React.useContext(AuthContext); + + // need to use local fetcher because useSWR default fetcher is not set up in this context + const fetcher = (path: string) => axios.get(path).then((res) => res.data); + const { data } = useSWR("/auth/first_time_login", fetcher); + const showFirstTimeLink = data?.admin_first_time_login === true; + + const formSchema = z.object({ + user: z.string().min(1, t("form.errors.usernameRequired")), + password: z.string().min(1, t("form.errors.passwordRequired")), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { user: "", password: "" }, + }); + + const onSubmit = async (values: z.infer) => { + setIsLoading(true); + try { + await axios.post( + "/login", + { + user: values.user, + password: values.password, + }, + { + headers: { "X-CSRF-TOKEN": 1 }, + }, + ); + const profileRes = await axios.get("/profile", { withCredentials: true }); + login({ + username: profileRes.data.username, + role: profileRes.data.role || "viewer", + }); + window.location.href = baseUrl; + } catch (error) { + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + if (err.response?.status === 429) { + toast.error(t("form.errors.rateLimit"), { + position: "top-center", + }); + } else if (err.response?.status === 401) { + toast.error(t("form.errors.loginFailed"), { + position: "top-center", + }); + } else { + toast.error(t("form.errors.unknownError"), { + position: "top-center", + }); + } + } else { + toast.error(t("form.errors.webUnknownError"), { + position: "top-center", + }); + } + + setIsLoading(false); + } + }; + + return ( +
    +
    + + ( + + {t("form.user")} + + + + + )} + /> + ( + + {t("form.password")} + + + + + )} + /> +
    + +
    + + + {showFirstTimeLink && ( + + +

    + {t("form.firstTimeLogin")} +

    + + {t("readTheDocumentation", { ns: "common" })} + + +
    +
    + )} + +
    + ); +} diff --git a/web/src/components/auth/ProtectedRoute.tsx b/web/src/components/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..c10a94a --- /dev/null +++ b/web/src/components/auth/ProtectedRoute.tsx @@ -0,0 +1,76 @@ +import { useContext, useEffect } from "react"; +import { Navigate, Outlet } from "react-router-dom"; +import { AuthContext } from "@/context/auth-context"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { + isRedirectingToLogin, + setRedirectingToLogin, +} from "@/api/auth-redirect"; +import { baseUrl } from "@/api/baseUrl"; + +export default function ProtectedRoute({ + requiredRoles, +}: { + requiredRoles?: string[]; +}) { + const { auth } = useContext(AuthContext); + + // Redirect to login page when not authenticated + // don't use because we need a full page load to reset state + useEffect(() => { + if ( + !auth.isLoading && + auth.isAuthenticated && + !auth.user && + !isRedirectingToLogin() + ) { + setRedirectingToLogin(true); + window.location.href = `${baseUrl}login`; + } + }, [auth.isLoading, auth.isAuthenticated, auth.user]); + + // Show loading indicator during redirect to prevent React from attempting to render + // lazy components, which would cause error #426 (suspension during synchronous navigation) + if (isRedirectingToLogin()) { + return ( + + ); + } + + // Wait for config to provide required roles + if (!requiredRoles) { + return ( + + ); + } + + if (auth.isLoading) { + return ( + + ); + } + + // Unauthenticated mode + if (!auth.isAuthenticated) { + return ; + } + + // Authenticated mode (external port): require login + if (!auth.user) { + return ( + + ); + } + + // If role is null (shouldn’t happen if isAuthenticated, but type safety), fallback + // though isAuthenticated should catch this + if (auth.user.role === null) { + return ; + } + + if (!requiredRoles.includes(auth.user.role)) { + return ; + } + + return ; +} diff --git a/web/src/components/button/BlurredIconButton.tsx b/web/src/components/button/BlurredIconButton.tsx new file mode 100644 index 0000000..90ffa36 --- /dev/null +++ b/web/src/components/button/BlurredIconButton.tsx @@ -0,0 +1,28 @@ +import React, { forwardRef } from "react"; +import { cn } from "@/lib/utils"; + +type BlurredIconButtonProps = React.HTMLAttributes; + +const BlurredIconButton = forwardRef( + ({ className = "", children, ...rest }, ref) => { + return ( +
    +
    +
    + {children} +
    +
    + ); + }, +); + +BlurredIconButton.displayName = "BlurredIconButton"; + +export default BlurredIconButton; diff --git a/web/src/components/button/DownloadVideoButton.tsx b/web/src/components/button/DownloadVideoButton.tsx new file mode 100644 index 0000000..93a8e1d --- /dev/null +++ b/web/src/components/button/DownloadVideoButton.tsx @@ -0,0 +1,63 @@ +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { FaDownload } from "react-icons/fa"; +import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useDateLocale } from "@/hooks/use-date-locale"; +import { useTimeFormat } from "@/hooks/use-date-utils"; +import { useMemo } from "react"; + +type DownloadVideoButtonProps = { + source: string; + camera: string; + startTime: number; + className?: string; +}; + +export function DownloadVideoButton({ + source, + camera, + startTime, + className, +}: DownloadVideoButtonProps) { + const { t } = useTranslation(["components/input"]); + const { data: config } = useSWR("config"); + const locale = useDateLocale(); + + const timeFormat = useTimeFormat(config); + const format = useMemo(() => { + return t(`time.formattedTimestampFilename.${timeFormat}`, { ns: "common" }); + }, [t, timeFormat]); + + const formattedDate = formatUnixTimestampToDateTime(startTime, { + date_format: format, + locale, + }); + const filename = `${camera}_${formattedDate}.mp4`; + + const handleDownloadStart = () => { + toast.success(t("button.downloadVideo.toast.success"), { + position: "top-center", + }); + }; + + return ( +
    + +
    + ); +} diff --git a/web/src/components/camera/AutoUpdatingCameraImage.tsx b/web/src/components/camera/AutoUpdatingCameraImage.tsx new file mode 100644 index 0000000..95d90d9 --- /dev/null +++ b/web/src/components/camera/AutoUpdatingCameraImage.tsx @@ -0,0 +1,103 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import CameraImage from "./CameraImage"; + +type AutoUpdatingCameraImageProps = { + camera: string; + searchParams?: URLSearchParams; + showFps?: boolean; + className?: string; + cameraClasses?: string; + reloadInterval?: number; + periodicCache?: boolean; +}; + +const MIN_LOAD_TIMEOUT_MS = 200; + +export default function AutoUpdatingCameraImage({ + camera, + searchParams = undefined, + showFps = true, + className, + cameraClasses, + reloadInterval = MIN_LOAD_TIMEOUT_MS, + periodicCache = false, +}: AutoUpdatingCameraImageProps) { + const [key, setKey] = useState(Date.now()); + const [fps, setFps] = useState("0"); + const timeoutRef = useRef(null); + + useEffect(() => { + if (reloadInterval == -1) { + return; + } + + setKey(Date.now()); + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reloadInterval]); + + const handleLoad = useCallback(() => { + setIsCached(true); + + if (reloadInterval == -1) { + return; + } + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + const loadTime = Date.now() - key; + + if (showFps) { + setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1)); + } + + timeoutRef.current = setTimeout( + () => { + setKey(Date.now()); + }, + loadTime > reloadInterval ? 1 : reloadInterval, + ); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [key, setFps]); + + // periodic cache to reduce loading indicator + + const [isCached, setIsCached] = useState(false); + + const cacheKey = useMemo(() => { + let baseParam = ""; + + if (periodicCache && !isCached) { + const date = new Date(key); + date.setMinutes(date.getMinutes() - (date.getMinutes() % 10), 0, 0); + + baseParam = `store=1&cache=${date.getTime() / 1000}`; + } else { + baseParam = `cache=${key}`; + } + + return `${baseParam}${searchParams ? `&${searchParams}` : ""}`; + }, [isCached, periodicCache, key, searchParams]); + + return ( +
    + + {showFps ? Displaying at {fps}fps : null} +
    + ); +} diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx new file mode 100644 index 0000000..f0c0599 --- /dev/null +++ b/web/src/components/camera/CameraImage.tsx @@ -0,0 +1,111 @@ +import { useApiHost } from "@/api"; +import { useEffect, useMemo, useRef, useState } from "react"; +import useSWR from "swr"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { useResizeObserver } from "@/hooks/resize-observer"; +import { isDesktop } from "react-device-detect"; +import { cn } from "@/lib/utils"; +import { useEnabledState } from "@/api/ws"; + +type CameraImageProps = { + className?: string; + camera: string; + onload?: () => void; + searchParams?: string; +}; + +export default function CameraImage({ + className, + camera, + onload, + searchParams = "", +}: CameraImageProps) { + const { data: config } = useSWR("config"); + const apiHost = useApiHost(); + const [imageLoaded, setImageLoaded] = useState(false); + const containerRef = useRef(null); + const imgRef = useRef(null); + + const cameraConfig = config?.cameras?.[camera]; + const { name } = cameraConfig ?? { name: camera }; + const { payload: enabledState } = useEnabledState(camera); + const enabled = enabledState ? enabledState === "ON" : true; + + const [{ width: containerWidth, height: containerHeight }] = + useResizeObserver(containerRef); + + const requestHeight = useMemo(() => { + if (!cameraConfig || containerHeight == 0) { + return 360; + } + + return Math.min( + cameraConfig.detect.height, + Math.round(containerHeight * (isDesktop ? 1.1 : 1.25)), + ); + }, [cameraConfig, containerHeight]); + + const [isPortraitImage, setIsPortraitImage] = useState(false); + + useEffect(() => { + setImageLoaded(false); + setIsPortraitImage(false); + }, [camera]); + + useEffect(() => { + if (!config || !imgRef.current) { + return; + } + + const newSrc = `${apiHost}api/${name}/latest.webp?height=${requestHeight}${ + searchParams ? `&${searchParams}` : "" + }`; + + if (imgRef.current.src !== newSrc) { + imgRef.current.src = newSrc; + } + }, [apiHost, name, searchParams, requestHeight, config, camera]); + + const handleImageLoad = () => { + if (imgRef.current && containerWidth && containerHeight) { + const { naturalWidth, naturalHeight } = imgRef.current; + setIsPortraitImage( + naturalWidth / naturalHeight < containerWidth / containerHeight, + ); + } + + setImageLoaded(true); + + if (onload) { + onload(); + } + }; + + return ( +
    + {enabled ? ( + + ) : ( +
    + )} + {!imageLoaded && enabled ? ( +
    + +
    + ) : null} +
    + ); +} diff --git a/web/src/components/camera/ConnectionQualityIndicator.tsx b/web/src/components/camera/ConnectionQualityIndicator.tsx new file mode 100644 index 0000000..3ea3c4f --- /dev/null +++ b/web/src/components/camera/ConnectionQualityIndicator.tsx @@ -0,0 +1,76 @@ +import { useTranslation } from "react-i18next"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; + +type ConnectionQualityIndicatorProps = { + quality: "excellent" | "fair" | "poor" | "unusable"; + expectedFps: number; + reconnects: number; + stalls: number; +}; + +export function ConnectionQualityIndicator({ + quality, + expectedFps, + reconnects, + stalls, +}: ConnectionQualityIndicatorProps) { + const { t } = useTranslation(["views/system"]); + + const getColorClass = (quality: string): string => { + switch (quality) { + case "excellent": + return "bg-success"; + case "fair": + return "bg-yellow-500"; + case "poor": + return "bg-orange-500"; + case "unusable": + return "bg-destructive"; + default: + return "bg-gray-500"; + } + }; + + const qualityLabel = t(`cameras.connectionQuality.${quality}`); + + return ( + + +
    + + +
    +
    + {t("cameras.connectionQuality.title")} +
    +
    +
    {qualityLabel}
    +
    +
    + {t("cameras.connectionQuality.expectedFps")}:{" "} + {expectedFps.toFixed(1)} {t("cameras.connectionQuality.fps")} +
    +
    + {t("cameras.connectionQuality.reconnectsLastHour")}:{" "} + {reconnects} +
    +
    + {t("cameras.connectionQuality.stallsLastHour")}: {stalls} +
    +
    +
    +
    +
    + + ); +} diff --git a/web/src/components/camera/DebugCameraImage.tsx b/web/src/components/camera/DebugCameraImage.tsx new file mode 100644 index 0000000..924eb86 --- /dev/null +++ b/web/src/components/camera/DebugCameraImage.tsx @@ -0,0 +1,173 @@ +import { Switch } from "../ui/switch"; +import { Label } from "../ui/label"; +import { CameraConfig } from "@/types/frigateConfig"; +import { Button } from "../ui/button"; +import { LuSettings } from "react-icons/lu"; +import { useCallback, useMemo, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; +import { useUserPersistence } from "@/hooks/use-user-persistence"; +import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage"; +import { useTranslation } from "react-i18next"; + +type Options = { [key: string]: boolean }; + +const emptyObject = Object.freeze({}); + +type DebugCameraImageProps = { + className?: string; + cameraConfig: CameraConfig; +}; + +export default function DebugCameraImage({ + className, + cameraConfig, +}: DebugCameraImageProps) { + const { t } = useTranslation(["components/camera"]); + const [showSettings, setShowSettings] = useState(false); + const [options, setOptions] = useUserPersistence( + `${cameraConfig?.name}-feed`, + emptyObject, + ); + const handleSetOption = useCallback( + (id: string, value: boolean) => { + const newOptions = { ...options, [id]: value }; + setOptions(newOptions); + }, + [options, setOptions], + ); + const searchParams = useMemo( + () => + new URLSearchParams( + Object.keys(options || {}).reduce((memo, key) => { + //@ts-expect-error we know this is correct + memo.push([key, options[key] === true ? "1" : "0"]); + return memo; + }, []), + ), + [options], + ); + const handleToggleSettings = useCallback(() => { + setShowSettings(!showSettings); + }, [showSettings]); + + return ( +
    + + + {showSettings ? ( + + + {t("debug.options.title")} + + + + + + ) : null} +
    + ); +} + +type DebugSettingsProps = { + handleSetOption: (id: string, value: boolean) => void; + options: Options; +}; + +function DebugSettings({ handleSetOption, options }: DebugSettingsProps) { + const { t } = useTranslation(["components/camera"]); + return ( +
    +
    + { + handleSetOption("bbox", isChecked); + }} + /> + +
    +
    + { + handleSetOption("timestamp", isChecked); + }} + /> + +
    +
    + { + handleSetOption("zones", isChecked); + }} + /> + +
    +
    + { + handleSetOption("mask", isChecked); + }} + /> + +
    +
    + { + handleSetOption("motion", isChecked); + }} + /> + +
    +
    + { + handleSetOption("regions", isChecked); + }} + /> + +
    +
    + { + handleSetOption("paths", isChecked); + }} + /> + +
    +
    + ); +} diff --git a/web/src/components/camera/FriendlyNameLabel.tsx b/web/src/components/camera/FriendlyNameLabel.tsx new file mode 100644 index 0000000..ca09788 --- /dev/null +++ b/web/src/components/camera/FriendlyNameLabel.tsx @@ -0,0 +1,44 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; +import { CameraConfig } from "@/types/frigateConfig"; +import { useZoneFriendlyName } from "@/hooks/use-zone-friendly-name"; + +interface CameraNameLabelProps + extends React.ComponentPropsWithoutRef { + camera?: string | CameraConfig; +} + +interface ZoneNameLabelProps + extends React.ComponentPropsWithoutRef { + zone: string; + camera?: string; +} + +const CameraNameLabel = React.forwardRef< + React.ElementRef, + CameraNameLabelProps +>(({ className, camera, ...props }, ref) => { + const displayName = useCameraFriendlyName(camera); + return ( + + {displayName} + + ); +}); +CameraNameLabel.displayName = LabelPrimitive.Root.displayName; + +const ZoneNameLabel = React.forwardRef< + React.ElementRef, + ZoneNameLabelProps +>(({ className, zone, camera, ...props }, ref) => { + const displayName = useZoneFriendlyName(zone, camera); + return ( + + {displayName} + + ); +}); +ZoneNameLabel.displayName = LabelPrimitive.Root.displayName; + +export { CameraNameLabel, ZoneNameLabel }; diff --git a/web/src/components/camera/ResizingCameraImage.tsx b/web/src/components/camera/ResizingCameraImage.tsx new file mode 100644 index 0000000..fbb5767 --- /dev/null +++ b/web/src/components/camera/ResizingCameraImage.tsx @@ -0,0 +1,123 @@ +import { useApiHost } from "@/api"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import useSWR from "swr"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { useResizeObserver } from "@/hooks/resize-observer"; +import { cn } from "@/lib/utils"; + +type CameraImageProps = { + className?: string; + camera: string; + onload?: (event: Event) => void; + searchParams?: string; + stretch?: boolean; // stretch to fit width + fitAspect?: number; // shrink to fit height +}; + +export default function CameraImage({ + className, + camera, + onload, + searchParams = "", + stretch = false, + fitAspect, +}: CameraImageProps) { + const { data: config } = useSWR("config"); + const apiHost = useApiHost(); + const [hasLoaded, setHasLoaded] = useState(false); + const containerRef = useRef(null); + const canvasRef = useRef(null); + const [{ width: containerWidth, height: containerHeight }] = + useResizeObserver(containerRef); + + // Add scrollbar width (when visible) to the available observer width to eliminate screen juddering. + // https://github.com/blakeblackshear/frigate/issues/1657 + let scrollBarWidth = 0; + if (window.innerWidth && document.body.offsetWidth) { + scrollBarWidth = window.innerWidth - document.body.offsetWidth; + } + const availableWidth = scrollBarWidth + ? containerWidth + scrollBarWidth + : containerWidth; + + const { name } = config ? config.cameras[camera] : ""; + const enabled = config ? config.cameras[camera].enabled : "True"; + const { width, height } = config + ? config.cameras[camera].detect + : { width: 1, height: 1 }; + const aspectRatio = width / height; + + const scaledHeight = useMemo(() => { + const scaledHeight = + aspectRatio < (fitAspect ?? 0) + ? Math.floor(containerHeight) + : Math.floor(availableWidth / aspectRatio); + const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height); + + if (finalHeight > 0) { + return finalHeight; + } + + return 100; + }, [ + availableWidth, + aspectRatio, + containerHeight, + fitAspect, + height, + stretch, + ]); + const scaledWidth = useMemo( + () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), + [scaledHeight, aspectRatio, scrollBarWidth], + ); + + const img = useMemo(() => new Image(), []); + img.onload = useCallback( + (event: Event) => { + setHasLoaded(true); + if (canvasRef.current) { + const ctx = canvasRef.current.getContext("2d"); + ctx?.drawImage(img, 0, 0, scaledWidth, scaledHeight); + } + onload && onload(event); + }, + [img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef], + ); + + useEffect(() => { + if (!config || scaledHeight === 0 || !canvasRef.current) { + return; + } + img.src = `${apiHost}api/${name}/latest.webp?height=${scaledHeight}${ + searchParams ? `&${searchParams}` : "" + }`; + }, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]); + + return ( +
    + {enabled ? ( + + ) : ( +
    Camera is disabled.
    + )} + {!hasLoaded && enabled ? ( +
    + +
    + ) : null} +
    + ); +} diff --git a/web/src/components/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx new file mode 100644 index 0000000..1b7b11f --- /dev/null +++ b/web/src/components/card/AnimatedEventCard.tsx @@ -0,0 +1,276 @@ +import TimeAgo from "../dynamic/TimeAgo"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { REVIEW_PADDING, ReviewSegment } from "@/types/review"; +import { useNavigate } from "react-router-dom"; +import { RecordingStartingPoint } from "@/types/record"; +import axios from "axios"; +import { isCurrentHour } from "@/utils/dateUtil"; +import { useCameraPreviews } from "@/hooks/use-camera-previews"; +import { baseUrl } from "@/api/baseUrl"; +import { VideoPreview } from "../preview/ScrubbablePreview"; +import { useApiHost } from "@/api"; +import { isSafari } from "react-device-detect"; +import { useUserPersistence } from "@/hooks/use-user-persistence"; +import { Skeleton } from "../ui/skeleton"; +import { Button } from "../ui/button"; +import { FaCircleCheck } from "react-icons/fa6"; +import { FaExclamationTriangle } from "react-icons/fa"; +import { MdOutlinePersonSearch } from "react-icons/md"; +import { ThreatLevel } from "@/types/review"; +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { formatList } from "@/utils/stringUtil"; + +type AnimatedEventCardProps = { + event: ReviewSegment; + selectedGroup?: string; + updateEvents: () => void; +}; +export function AnimatedEventCard({ + event, + selectedGroup, + updateEvents, +}: AnimatedEventCardProps) { + const { t } = useTranslation(["views/events"]); + const { data: config } = useSWR("config"); + const apiHost = useApiHost(); + + const currentHour = useMemo(() => isCurrentHour(event.start_time), [event]); + + const initialTimeRange = useMemo(() => { + return { + after: Math.round(event.start_time), + before: Math.round(event.end_time || event.start_time + 20), + }; + }, [event]); + + // preview + + const previews = useCameraPreviews(initialTimeRange, { + camera: event.camera, + fetchPreviews: !currentHour, + }); + + const getEventType = useCallback( + (text: string) => { + if (event.data.sub_labels?.includes(text)) return "manual"; + if (event.data.audio.includes(text)) return "audio"; + return "object"; + }, + [event], + ); + + const tooltipText = useMemo(() => { + if (event?.data?.metadata?.title) { + return event.data.metadata.title; + } + + return ( + `${formatList( + [ + ...new Set([ + ...(event.data.objects || []), + ...(event.data.sub_labels || []), + ...(event.data.audio || []), + ]), + ] + .filter((item) => item !== undefined && !item.includes("-verified")) + .map((text) => getTranslatedLabel(text, getEventType(text))) + .sort(), + )} ` + t("detected") + ); + }, [event, getEventType, t]); + + // visibility + + const [windowVisible, setWindowVisible] = useState(true); + const visibilityListener = useCallback(() => { + setWindowVisible(document.visibilityState == "visible"); + }, []); + + useEffect(() => { + addEventListener("visibilitychange", visibilityListener); + + return () => { + removeEventListener("visibilitychange", visibilityListener); + }; + }, [visibilityListener]); + + const [isLoaded, setIsLoaded] = useState(false); + + // interaction + + const navigate = useNavigate(); + const onOpenReview = useCallback(() => { + const url = + selectedGroup && selectedGroup != "default" + ? `review?group=${selectedGroup}` + : "review"; + navigate(url, { + state: { + severity: event.severity, + recording: { + camera: event.camera, + startTime: event.start_time - REVIEW_PADDING, + severity: event.severity, + } as RecordingStartingPoint, + }, + }); + axios.post(`reviews/viewed`, { ids: [event.id] }); + }, [navigate, selectedGroup, event]); + + // image behavior + + const [alertVideos, _, alertVideosLoaded] = useUserPersistence( + "alertVideos", + true, + ); + + const threatLevel = useMemo( + () => (event.data.metadata?.potential_threat_level ?? 0) as ThreatLevel, + [event], + ); + + const aspectRatio = useMemo(() => { + if ( + !config || + !alertVideos || + !Object.keys(config.cameras).includes(event.camera) + ) { + return 16 / 9; + } + + const detect = config.cameras[event.camera].detect; + return detect.width / detect.height; + }, [alertVideos, config, event]); + + return ( + + +
    + + + + + {t("markAsReviewed")} + + {previews != undefined && alertVideosLoaded && ( +
    { + if (e.button === 1) { + window + .open(`${baseUrl}review?id=${event.id}`, "_blank") + ?.focus(); + } + }} + > + {!alertVideos ? ( + setIsLoaded(true)} + /> + ) : ( + <> + {previews.length ? ( + {}} + setIgnoreClick={() => {}} + isPlayingBack={() => {}} + onTimeUpdate={() => { + if (!isLoaded) { + setIsLoaded(true); + } + }} + windowVisible={windowVisible} + /> + ) : ( + + )} + + )} +
    + )} + {isLoaded && ( +
    +
    + +
    +
    + )} + {!isLoaded && ( + + )} +
    +
    + {tooltipText} +
    + ); +} diff --git a/web/src/components/card/ClassificationCard.tsx b/web/src/components/card/ClassificationCard.tsx new file mode 100644 index 0000000..4fed7e2 --- /dev/null +++ b/web/src/components/card/ClassificationCard.tsx @@ -0,0 +1,457 @@ +import { baseUrl } from "@/api/baseUrl"; +import useContextMenu from "@/hooks/use-contextmenu"; +import { cn } from "@/lib/utils"; +import { + ClassificationItemData, + ClassificationThreshold, + ClassifiedEvent, +} from "@/types/classification"; +import { forwardRef, useEffect, useMemo, useRef, useState } from "react"; +import { isDesktop, isIOS, isMobile, isMobileOnly } from "react-device-detect"; +import { useTranslation } from "react-i18next"; +import TimeAgo from "../dynamic/TimeAgo"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { LuSearch, LuInfo } from "react-icons/lu"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; +import { useNavigate } from "react-router-dom"; +import { HiSquare2Stack } from "react-icons/hi2"; +import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../ui/dialog"; +import { + MobilePage, + MobilePageContent, + MobilePageDescription, + MobilePageHeader, + MobilePageTitle, + MobilePageTrigger, +} from "../mobile/MobilePage"; + +type ClassificationCardProps = { + className?: string; + imgClassName?: string; + data: ClassificationItemData; + threshold?: ClassificationThreshold; + selected: boolean; + clickable: boolean; + i18nLibrary: string; + showArea?: boolean; + count?: number; + onClick: (data: ClassificationItemData, meta: boolean) => void; + children?: React.ReactNode; +}; +export const ClassificationCard = forwardRef< + HTMLDivElement, + ClassificationCardProps +>(function ClassificationCard( + { + className, + imgClassName, + data, + threshold, + selected, + clickable, + i18nLibrary, + showArea = true, + count, + onClick, + children, + }, + ref, +) { + const { t } = useTranslation([i18nLibrary]); + const [imageLoaded, setImageLoaded] = useState(false); + + const scoreStatus = useMemo(() => { + if (!data.score || !threshold) { + return "unknown"; + } + + if (data.score >= threshold.recognition) { + return "match"; + } else if (data.score >= threshold.unknown) { + return "potential"; + } else { + return "unknown"; + } + }, [data, threshold]); + + // interaction + + const imgRef = useRef(null); + + useContextMenu(imgRef, () => { + onClick(data, true); + }); + + const imageArea = useMemo(() => { + if (!showArea || imgRef.current == null || !imageLoaded) { + return undefined; + } + + return imgRef.current.naturalWidth * imgRef.current.naturalHeight; + }, [showArea, imageLoaded]); + + return ( +
    { + const isMeta = e.metaKey || e.ctrlKey; + if (isMeta) { + e.stopPropagation(); + } + onClick(data, isMeta); + }} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + onClick(data, true); + }} + > + setImageLoaded(true)} + src={`${baseUrl}${data.filepath}`} + /> + + {count && ( +
    +
    {count}
    {" "} + +
    + )} + {!count && imageArea != undefined && ( +
    + {t("information.pixels", { ns: "common", area: imageArea })} +
    + )} +
    +
    +
    +
    + {data.name.toLowerCase() == "unknown" + ? t("details.unknown") + : data.name.toLowerCase() == "none" + ? t("details.none") + : data.name} +
    + {data.score != undefined && ( +
    + {Math.round(data.score * 100)}% +
    + )} +
    +
    + {children} +
    +
    +
    + ); +}); + +type GroupedClassificationCardProps = { + group: ClassificationItemData[]; + classifiedEvent?: ClassifiedEvent; + threshold?: ClassificationThreshold; + selectedItems: string[]; + i18nLibrary: string; + objectType: string; + noClassificationLabel?: string; + onClick: (data: ClassificationItemData | undefined) => void; + children?: (data: ClassificationItemData) => React.ReactNode; +}; +export function GroupedClassificationCard({ + group, + classifiedEvent, + threshold, + selectedItems, + i18nLibrary, + noClassificationLabel = "details.none", + onClick, + children, +}: GroupedClassificationCardProps) { + const navigate = useNavigate(); + const { t } = useTranslation(["views/explore", i18nLibrary]); + const [detailOpen, setDetailOpen] = useState(false); + + // If the component unmounts while the detail overlay is open, we need to + // pop the history state that was pushed by useHistoryBack, otherwise it + // leaves a stale entry that breaks back navigation. + const detailOpenRef = useRef(detailOpen); + useEffect(() => { + detailOpenRef.current = detailOpen; + }, [detailOpen]); + + useEffect(() => { + return () => { + // Only pop the state if we are still sitting on the overlayOpen history entry. + // This prevents the unmount from undoing cross-page routing if the unmount + // was caused by navigating away to a different view. + if (detailOpenRef.current && window.history.state?.overlayOpen) { + window.history.back(); + } + }; + }, []); + + // data + + const bestItem = useMemo(() => { + let best: undefined | ClassificationItemData = undefined; + + group.forEach((item) => { + if (item?.name != undefined && item.name != "none") { + if ( + best?.score == undefined || + (item.score && best.score < item.score) + ) { + best = item; + } + } + }); + + if (!best) { + best = group.at(-1)!; + } + + const bestTyped: ClassificationItemData = best; + return { + ...bestTyped, + name: + classifiedEvent?.label && classifiedEvent.label !== "none" + ? classifiedEvent.label + : classifiedEvent + ? t(noClassificationLabel) + : bestTyped.name, + score: classifiedEvent?.score, + }; + }, [group, classifiedEvent, noClassificationLabel, t]); + + const bestScoreStatus = useMemo(() => { + if (!bestItem?.score || !threshold) { + return "unknown"; + } + + if (bestItem.score >= threshold.recognition) { + return "match"; + } else if (bestItem.score >= threshold.unknown) { + return "potential"; + } else { + return "unknown"; + } + }, [bestItem, threshold]); + + const time = useMemo(() => { + const item = group[0]; + + if (!item?.timestamp) { + return undefined; + } + + return item.timestamp * 1000; + }, [group]); + + if (!bestItem) { + return null; + } + + const Overlay = isDesktop ? Dialog : MobilePage; + const Trigger = isDesktop ? DialogTrigger : MobilePageTrigger; + const Content = isDesktop ? DialogContent : MobilePageContent; + const Header = isDesktop ? DialogHeader : MobilePageHeader; + const ContentTitle = isDesktop ? DialogTitle : MobilePageTitle; + const ContentDescription = isDesktop + ? DialogDescription + : MobilePageDescription; + + return ( + <> + { + if (meta || selectedItems.length > 0) { + onClick(undefined); + } else { + setDetailOpen(true); + } + }} + /> + { + if (!open) { + setDetailOpen(false); + } + }} + > + + e.preventDefault()} + > + <> +
    +
    + + {classifiedEvent?.label && classifiedEvent.label !== "none" + ? classifiedEvent.label + : t(noClassificationLabel, { ns: i18nLibrary })} + {classifiedEvent?.label && + classifiedEvent.label !== "none" && + classifiedEvent.score !== undefined && ( +
    +
    {`${Math.round((classifiedEvent.score || 0) * 100)}%`}
    + + + + + + {t("details.scoreInfo", { ns: i18nLibrary })} + + +
    + )} +
    + + {time && ( + + )} + +
    + {classifiedEvent && ( +
    + + +
    { + navigate(`/explore?event_id=${classifiedEvent.id}`); + }} + > + +
    +
    + + + {t("details.item.button.viewInExplore", { + ns: "views/explore", + })} + + +
    +
    + )} +
    +
    + {group.map((data: ClassificationItemData) => ( +
    + {}} + > + {children?.(data)} + +
    + ))} +
    + +
    +
    + + ); +} diff --git a/web/src/components/card/EmptyCard.tsx b/web/src/components/card/EmptyCard.tsx new file mode 100644 index 0000000..5495a6e --- /dev/null +++ b/web/src/components/card/EmptyCard.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { Button } from "../ui/button"; +import Heading from "../ui/heading"; +import { Link } from "react-router-dom"; +import { cn } from "@/lib/utils"; + +type EmptyCardProps = { + className?: string; + icon: React.ReactNode; + title: string; + titleHeading?: boolean; + description?: string; + buttonText?: string; + link?: string; + onClick?: () => void; +}; +export function EmptyCard({ + className, + icon, + title, + titleHeading = true, + description, + buttonText, + link, + onClick, +}: EmptyCardProps) { + let TitleComponent; + + if (titleHeading) { + TitleComponent = {title}; + } else { + TitleComponent =
    {title}
    ; + } + + return ( +
    + {icon} + {TitleComponent} + {description && ( +
    + {description} +
    + )} + {buttonText?.length && + (onClick ? ( + + ) : ( + + ))} +
    + ); +} diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx new file mode 100644 index 0000000..958135d --- /dev/null +++ b/web/src/components/card/ExportCard.tsx @@ -0,0 +1,528 @@ +import ActivityIndicator from "../indicators/activity-indicator"; +import { Button } from "../ui/button"; +import { Progress } from "../ui/progress"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { isMobile } from "react-device-detect"; +import { FiMoreVertical } from "react-icons/fi"; +import { Skeleton } from "../ui/skeleton"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogTitle, +} from "../ui/dialog"; +import { Input } from "../ui/input"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; +import { DeleteClipType, Export, ExportCase, ExportJob } from "@/types/export"; +import { baseUrl } from "@/api/baseUrl"; +import { cn } from "@/lib/utils"; +import { shareOrCopy } from "@/utils/browserUtil"; +import { useTranslation } from "react-i18next"; +import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay"; +import BlurredIconButton from "../button/BlurredIconButton"; +import { useIsAdmin } from "@/hooks/use-is-admin"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { FaFolder, FaVideo } from "react-icons/fa"; +import { HiSquare2Stack } from "react-icons/hi2"; +import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; +import useContextMenu from "@/hooks/use-contextmenu"; +import axios from "axios"; +import { toast } from "sonner"; +import { useNavigate } from "react-router-dom"; + +type CaseCardProps = { + className: string; + exportCase: ExportCase; + exports: Export[]; + onSelect: () => void; +}; +export function CaseCard({ + className, + exportCase, + exports, + onSelect, +}: CaseCardProps) { + const { t } = useTranslation(["views/exports"]); + const firstExport = useMemo( + () => exports.find((exp) => exp.thumb_path && exp.thumb_path.length > 0), + [exports], + ); + const cameraCount = useMemo( + () => new Set(exports.map((exp) => exp.camera)).size, + [exports], + ); + + return ( +
    onSelect()} + > + {firstExport && ( + + )} + {!firstExport && ( +
    + )} +
    +
    +
    + +
    {exports.length}
    +
    +
    + +
    {cameraCount}
    +
    +
    +
    +
    + +
    {exportCase.name}
    +
    + {exports.length === 0 && ( +
    + {t("caseCard.emptyCase")} +
    + )} +
    +
    + ); +} + +type ExportCardProps = { + className: string; + exportedRecording: Export; + isSelected?: boolean; + selectionMode?: boolean; + onSelect: (selected: Export) => void; + onContextSelect?: (selected: Export) => void; + onRename: (original: string, update: string) => void; + onDelete: ({ file, exportName }: DeleteClipType) => void; + onAssignToCase?: (selected: Export) => void; + onRemoveFromCase?: (selected: Export) => void; +}; +export function ExportCard({ + className, + exportedRecording, + isSelected, + selectionMode, + onSelect, + onContextSelect, + onRename, + onDelete, + onAssignToCase, + onRemoveFromCase, +}: ExportCardProps) { + const { t } = useTranslation(["views/exports", "views/replay"]); + const navigate = useNavigate(); + const isAdmin = useIsAdmin(); + const [loading, setLoading] = useState( + exportedRecording.thumb_path.length > 0, + ); + const [isStartingReplay, setIsStartingReplay] = useState(false); + + const handleDebugReplay = useCallback(() => { + setIsStartingReplay(true); + + axios + .post("debug_replay/start_from_export", { + export_id: exportedRecording.id, + }) + .then((response) => { + if (response.status === 202 || response.status === 200) { + navigate("/replay"); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + + if (error.response?.status === 409) { + toast.error(t("dialog.toast.alreadyActive", { ns: "views/replay" }), { + position: "top-center", + closeButton: true, + dismissible: false, + action: ( + + + + ), + }); + } else { + toast.error( + t("dialog.toast.error", { + ns: "views/replay", + error: errorMessage, + }), + { position: "top-center" }, + ); + } + }) + .finally(() => { + setIsStartingReplay(false); + }); + }, [exportedRecording.id, navigate, t]); + + // Resync the skeleton state whenever the backing export changes. The + // list keys by id now, so in practice the component remounts instead + // of receiving new props — but this keeps the card honest if a parent + // ever reuses the instance across different exports. + useEffect(() => { + setLoading(exportedRecording.thumb_path.length > 0); + }, [exportedRecording.thumb_path]); + + // selection + + const cardRef = useRef(null); + useContextMenu(cardRef, () => { + if (!exportedRecording.in_progress && onContextSelect) { + onContextSelect(exportedRecording); + } + }); + + // editing name + + const [editName, setEditName] = useState<{ + original: string; + update?: string; + }>(); + + const submitRename = useCallback(() => { + if (editName == undefined) { + return; + } + + onRename(exportedRecording.id, editName.update ?? ""); + setEditName(undefined); + }, [editName, exportedRecording, onRename, setEditName]); + + useKeyboardListener( + editName != undefined ? ["Enter"] : [], + (key, modifiers) => { + if ( + key == "Enter" && + modifiers.down && + !modifiers.repeat && + editName && + (editName.update?.length ?? 0) > 0 + ) { + submitRename(); + return true; + } + + return false; + }, + ); + + return ( + <> + { + if (!open) { + setEditName(undefined); + } + }} + > + { + if (isMobile) { + e.preventDefault(); + } + }} + > + {t("editExport.title")} + {t("editExport.desc")} + {editName && ( + <> + + setEditName({ + original: editName.original ?? "", + update: e.target.value, + }) + } + /> + + + + + )} + + + +
    { + if (!exportedRecording.in_progress) { + if ((selectionMode || e.ctrlKey || e.metaKey) && onContextSelect) { + onContextSelect(exportedRecording); + } else { + onSelect(exportedRecording); + } + } + }} + > + {exportedRecording.in_progress ? ( + + ) : ( + <> + {exportedRecording.thumb_path.length > 0 ? ( + setLoading(false)} + /> + ) : ( +
    + )} + + )} + {!exportedRecording.in_progress && !selectionMode && ( +
    + + + e.stopPropagation()} + > + + + + + { + e.stopPropagation(); + shareOrCopy( + `${baseUrl}export?id=${exportedRecording.id}`, + exportedRecording.name.replaceAll("_", " "), + ); + }} + > + {t("tooltip.shareExport")} + + + e.stopPropagation()} + > + {t("tooltip.downloadVideo")} + + + {isAdmin && ( + { + e.stopPropagation(); + handleDebugReplay(); + }} + > + {isStartingReplay + ? t("dialog.starting", { ns: "views/replay" }) + : t("title", { ns: "views/replay" })} + + )} + {isAdmin && onAssignToCase && ( + { + e.stopPropagation(); + onAssignToCase(exportedRecording); + }} + > + {t("tooltip.assignToCase")} + + )} + {isAdmin && onRemoveFromCase && ( + { + e.stopPropagation(); + onRemoveFromCase(exportedRecording); + }} + > + {t("tooltip.removeFromCase")} + + )} + {isAdmin && ( + { + e.stopPropagation(); + setEditName({ + original: exportedRecording.name, + update: undefined, + }); + }} + > + {t("tooltip.editName")} + + )} + {isAdmin && ( + { + e.stopPropagation(); + onDelete({ + file: exportedRecording.id, + exportName: exportedRecording.name, + }); + }} + > + {t("tooltip.deleteExport")} + + )} + + +
    + )} + {loading && ( + + )} + +
    +
    +
    + {exportedRecording.name.replaceAll("_", " ")} +
    +
    +
    + + ); +} + +type ActiveExportJobCardProps = { + className?: string; + job: ExportJob; +}; + +export function ActiveExportJobCard({ + className = "", + job, +}: ActiveExportJobCardProps) { + const { t } = useTranslation(["views/exports", "common"]); + const cameraName = useCameraFriendlyName(job.camera); + const displayName = useMemo(() => { + if (job.name && job.name.length > 0) { + return job.name.replaceAll("_", " "); + } + + return t("jobCard.defaultName", { + camera: cameraName, + }); + }, [cameraName, job.name, t]); + + const step = job.current_step + ? job.current_step + : job.status === "queued" + ? "queued" + : "preparing"; + const percent = Math.round(job.progress_percent ?? 0); + + const stepLabel = useMemo(() => { + switch (step) { + case "queued": + return t("jobCard.queued"); + case "preparing": + return t("jobCard.preparing"); + case "copying": + return t("jobCard.copying"); + case "encoding": + return t("jobCard.encoding"); + case "encoding_retry": + return t("jobCard.encodingRetry"); + case "finalizing": + return t("jobCard.finalizing"); + default: + return t("jobCard.running"); + } + }, [step, t]); + + const hasDeterminateProgress = + step === "copying" || step === "encoding" || step === "encoding_retry"; + + return ( +
    +
    +
    + {stepLabel} + {hasDeterminateProgress && ` · ${percent}%`} +
    + {step === "queued" ? ( + + ) : hasDeterminateProgress ? ( + + ) : ( +
    +
    +
    + )} +
    {displayName}
    +
    +
    + ); +} diff --git a/web/src/components/card/ReviewCard.tsx b/web/src/components/card/ReviewCard.tsx new file mode 100644 index 0000000..19b4714 --- /dev/null +++ b/web/src/components/card/ReviewCard.tsx @@ -0,0 +1,399 @@ +import { baseUrl } from "@/api/baseUrl"; +import { useFormattedTimestamp, use24HourTime } from "@/hooks/use-date-utils"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { REVIEW_PADDING, ReviewSegment } from "@/types/review"; +import { getIconForLabel } from "@/utils/iconUtil"; +import { isDesktop, isIOS, isSafari } from "react-device-detect"; +import useSWR from "swr"; +import TimeAgo from "../dynamic/TimeAgo"; +import { useCallback, useRef, useState } from "react"; +import useImageLoaded from "@/hooks/use-image-loaded"; +import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator"; +import { FaCompactDisc } from "react-icons/fa"; +import { FaCircleCheck } from "react-icons/fa6"; +import { HiTrash } from "react-icons/hi"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "../ui/context-menu"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; +import { Drawer, DrawerContent } from "../ui/drawer"; +import axios from "axios"; +import { toast } from "sonner"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { Button, buttonVariants } from "../ui/button"; +import { Trans, useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; +import { LuCircle } from "react-icons/lu"; +import { MdAutoAwesome } from "react-icons/md"; +import { GenAISummaryDialog } from "../overlay/chip/GenAISummaryChip"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { formatList } from "@/utils/stringUtil"; + +type ReviewCardProps = { + event: ReviewSegment; + activeReviewItem?: ReviewSegment; + onClick?: () => void; +}; +export default function ReviewCard({ + event, + activeReviewItem, + onClick, +}: ReviewCardProps) { + const { t } = useTranslation(["components/dialog"]); + const { data: config } = useSWR("config"); + const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); + const is24Hour = use24HourTime(config); + const formattedDate = useFormattedTimestamp( + event.start_time, + is24Hour + ? t("time.formattedTimestampHourMinute.24hour", { ns: "common" }) + : t("time.formattedTimestampHourMinute.12hour", { ns: "common" }), + config?.ui.timezone, + ); + + const [optionsOpen, setOptionsOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const bypassDialogRef = useRef(false); + + const onMarkAsReviewed = useCallback(async () => { + await axios.post(`reviews/viewed`, { ids: [event.id] }); + event.has_been_reviewed = true; + setOptionsOpen(false); + }, [event]); + + const onExport = useCallback(async () => { + const endTime = event.end_time + ? event.end_time + REVIEW_PADDING + : Date.now() / 1000; + + const genAiTitle = event.data.metadata?.title?.trim(); + + axios + .post( + `export/${event.camera}/start/${event.start_time - REVIEW_PADDING}/end/${endTime}`, + { + playback: "realtime", + ...(genAiTitle ? { name: genAiTitle } : {}), + }, + ) + .then((response) => { + if (response.status < 300) { + toast.success(t("export.toast.success"), { + position: "top-center", + action: ( + + + + ), + }); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || error.message || "Unknown error"; + toast.error(t("export.toast.error.failed", { error: errorMessage }), { + position: "top-center", + }); + }); + setOptionsOpen(false); + }, [event, t]); + + const onDelete = useCallback(async () => { + await axios.post(`reviews/delete`, { ids: [event.id] }); + event.id = ""; + setOptionsOpen(false); + }, [event]); + + useKeyboardListener(["Shift"], (_, modifiers) => { + bypassDialogRef.current = modifiers.shift; + return false; + }); + + const handleDelete = useCallback(() => { + if (bypassDialogRef.current) { + onDelete(); + } else { + setDeleteDialogOpen(true); + } + }, [bypassDialogRef, onDelete]); + + const getEventType = (text: string) => { + if (event.data.sub_labels?.includes(text)) return "manual"; + if (event.data.audio.includes(text)) return "audio"; + return "object"; + }; + + const content = ( +
    { + e.preventDefault(); + setOptionsOpen(true); + } + } + > + + { + onImgLoad(); + }} + /> +
    + + +
    + +
    + {event.data.objects.map((object, idx) => ( +
    + {getIconForLabel(object, "object", "size-3 text-white")} +
    + ))} + {event.data.audio.map((audio, idx) => ( +
    + {getIconForLabel(audio, "audio", "size-3 text-white")} +
    + ))} +
    +
    {formattedDate}
    +
    +
    + + {formatList( + [ + ...new Set([ + ...(event.data.objects || []), + ...(event.data.sub_labels || []), + ...(event.data.audio || []), + ]), + ] + .filter( + (item) => item !== undefined && !item.includes("-verified"), + ) + .map((text) => getTranslatedLabel(text, getEventType(text))) + .sort(), + )} + +
    + +
    + {event.data.metadata?.title && ( + +
    + + + {event.data.metadata.title} + +
    +
    + )} +
    + ); + + if (event.id == "") { + return; + } + + if (isDesktop) { + return ( + <> + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + + {t("recording.confirmDelete.title")} + + + + + recording.confirmDelete.title + + + + setOptionsOpen(false)}> + {t("button.cancel", { ns: "common" })} + + + {t("button.delete", { ns: "common" })} + + + + + + {content} + + +
    + +
    + {t("recording.button.export")} +
    +
    +
    + {!event.has_been_reviewed && ( + +
    + +
    + {t("recording.button.markAsReviewed")} +
    +
    +
    + )} + +
    + +
    + {bypassDialogRef.current + ? t("recording.button.deleteNow") + : t("button.delete", { ns: "common" })} +
    +
    +
    +
    +
    + + ); + } + + return ( + <> + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + + {t("recording.confirmDelete.title")} + + + + + recording.confirmDelete.desc.selected + + + + setOptionsOpen(false)}> + {t("button.cancel", { ns: "common" })} + + + {t("button.delete", { ns: "common" })} + + + + + + {content} + +
    + +
    {t("recording.button.export")}
    +
    + {!event.has_been_reviewed && ( +
    + +
    + {t("recording.button.markAsReviewed")} +
    +
    + )} +
    + +
    + {bypassDialogRef.current + ? t("recording.button.deleteNow") + : t("button.delete", { ns: "common" })} +
    +
    +
    +
    + + ); +} diff --git a/web/src/components/card/SearchThumbnail.tsx b/web/src/components/card/SearchThumbnail.tsx new file mode 100644 index 0000000..66f58f4 --- /dev/null +++ b/web/src/components/card/SearchThumbnail.tsx @@ -0,0 +1,172 @@ +import { useCallback, useMemo } from "react"; +import { useApiHost } from "@/api"; +import { getIconForLabel } from "@/utils/iconUtil"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { isIOS, isSafari } from "react-device-detect"; +import Chip from "@/components/indicators/Chip"; +import useImageLoaded from "@/hooks/use-image-loaded"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator"; +import { SearchResult } from "@/types/search"; +import { cn } from "@/lib/utils"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; +import useContextMenu from "@/hooks/use-contextmenu"; +import { getTranslatedLabel } from "@/utils/i18n"; + +type SearchThumbnailProps = { + searchResult: SearchResult; + onClick: (searchResult: SearchResult, ctrl: boolean, detail: boolean) => void; +}; + +export default function SearchThumbnail({ + searchResult, + onClick, +}: SearchThumbnailProps) { + const apiHost = useApiHost(); + const { data: config } = useSWR("config"); + const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); + + // interactions + + useContextMenu(imgRef, () => { + onClick(searchResult, true, false); + }); + + const handleOnClick = useCallback( + (e: React.MouseEvent) => { + if (e.metaKey) { + e.stopPropagation(); + onClick(searchResult, true, false); + } + }, + [searchResult, onClick], + ); + + const hasRecognizedPlate = useMemo( + () => (searchResult.data.recognized_license_plate?.length || 0) > 0, + [searchResult], + ); + + const objectLabel = useMemo(() => { + if (!config) { + return searchResult.label; + } + + if (!searchResult.sub_label) { + return `${searchResult.label}${hasRecognizedPlate ? "-plate" : ""}`; + } + + if ( + config.model.attributes_map[searchResult.label]?.includes( + searchResult.sub_label, + ) + ) { + return searchResult.sub_label; + } + + return `${searchResult.label}-verified`; + }, [config, hasRecognizedPlate, searchResult]); + + const objectDetail = useMemo(() => { + if (!config) { + return undefined; + } + + if (!searchResult.sub_label) { + if (hasRecognizedPlate) { + return `(${searchResult.data.recognized_license_plate})`; + } + + return undefined; + } + + if ( + config.model.attributes_map[searchResult.label]?.includes( + searchResult.sub_label, + ) + ) { + return ""; + } + + return `(${searchResult.sub_label})`; + }, [config, hasRecognizedPlate, searchResult]); + + return ( +
    onClick(searchResult, false, true)} + > + +
    + { + onImgLoad(); + }} + /> + +
    + +
    + +
    + onClick(searchResult, false, true)} + > + {getIconForLabel( + objectLabel, + searchResult.data.type, + "size-3 text-white", + )} + {Math.floor( + (searchResult.data.score ?? + searchResult.data.top_score ?? + searchResult.top_score) * 100, + )} + % {objectDetail} + +
    +
    +
    + + + {[searchResult.sub_label ?? objectLabel] + .filter( + (item) => item !== undefined && !item.includes("-verified"), + ) + .map((text) => + getTranslatedLabel(text, searchResult.data.type), + ) + .sort() + .join(", ") + .replaceAll("-verified", "")} + + +
    +
    +
    +
    +
    +
    + ); +} diff --git a/web/src/components/card/SearchThumbnailFooter.tsx b/web/src/components/card/SearchThumbnailFooter.tsx new file mode 100644 index 0000000..1087a53 --- /dev/null +++ b/web/src/components/card/SearchThumbnailFooter.tsx @@ -0,0 +1,69 @@ +import TimeAgo from "../dynamic/TimeAgo"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useFormattedTimestamp, use24HourTime } from "@/hooks/use-date-utils"; +import { SearchResult } from "@/types/search"; +import ActivityIndicator from "../indicators/activity-indicator"; +import SearchResultActions from "../menu/SearchResultActions"; +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; + +type SearchThumbnailProps = { + searchResult: SearchResult; + columns: number; + findSimilar: () => void; + refreshResults: () => void; + showTrackingDetails: () => void; + addTrigger: () => void; +}; + +export default function SearchThumbnailFooter({ + searchResult, + columns, + findSimilar, + refreshResults, + showTrackingDetails, + addTrigger, +}: SearchThumbnailProps) { + const { t } = useTranslation(["views/search"]); + const { data: config } = useSWR("config"); + + // date + const is24Hour = use24HourTime(config); + const formattedDate = useFormattedTimestamp( + searchResult.start_time, + is24Hour + ? t("time.formattedTimestampMonthDayHourMinute.24hour", { ns: "common" }) + : t("time.formattedTimestampMonthDayHourMinute.12hour", { ns: "common" }), + config?.ui.timezone, + ); + + return ( +
    4 && "items-start sm:flex-col lg:flex-row lg:items-center", + )} + > +
    + {searchResult.end_time ? ( + + ) : ( +
    + +
    + )} + {formattedDate} +
    +
    + +
    +
    + ); +} diff --git a/web/src/components/card/SettingsGroupCard.tsx b/web/src/components/card/SettingsGroupCard.tsx new file mode 100644 index 0000000..819ec21 --- /dev/null +++ b/web/src/components/card/SettingsGroupCard.tsx @@ -0,0 +1,56 @@ +import { ReactNode } from "react"; +import { Label } from "../ui/label"; + +export const SPLIT_ROW_CLASS_NAME = + "space-y-2 md:grid md:grid-cols-[minmax(14rem,24rem)_minmax(0,1fr)] md:items-start md:gap-x-6 md:space-y-0"; +export const DESCRIPTION_CLASS_NAME = "text-sm text-muted-foreground"; +export const CONTROL_COLUMN_CLASS_NAME = "w-full md:max-w-2xl"; + +type SettingsGroupCardProps = { + title: string | ReactNode; + children: ReactNode; +}; + +export function SettingsGroupCard({ title, children }: SettingsGroupCardProps) { + return ( +
    +
    + {title} +
    + {children} +
    + ); +} + +type SplitCardRowProps = { + label: ReactNode; + description?: ReactNode; + content: ReactNode; +}; + +export function SplitCardRow({ + label, + description, + content, +}: SplitCardRowProps) { + return ( +
    +
    + + {description && ( +
    + {description} +
    + )} +
    +
    + {content} + {description && ( +
    + {description} +
    + )} +
    +
    + ); +} diff --git a/web/src/components/chat/ChatAttachmentChip.tsx b/web/src/components/chat/ChatAttachmentChip.tsx new file mode 100644 index 0000000..ef4a114 --- /dev/null +++ b/web/src/components/chat/ChatAttachmentChip.tsx @@ -0,0 +1,112 @@ +import { useApiHost } from "@/api"; +import { baseUrl } from "@/api/baseUrl"; +import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { LuX, LuExternalLink } from "react-icons/lu"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { cn } from "@/lib/utils"; +import { getTranslatedLabel } from "@/utils/i18n"; + +type ChatAttachmentChipProps = { + eventId: string; + mode: "composer" | "bubble"; + onRemove?: () => void; +}; + +/** + * Small horizontal chip rendering an event as an "attachment": a thumbnail, + * a friendly label like "Person on driveway", an optional remove X (composer + * mode), and an external-link icon that opens the event in Explore. + */ +export function ChatAttachmentChip({ + eventId, + mode, + onRemove, +}: ChatAttachmentChipProps) { + const apiHost = useApiHost(); + const { t } = useTranslation(["views/chat"]); + + const { data: eventData } = useSWR<{ label: string; camera: string }[]>( + `event_ids?ids=${eventId}`, + ); + const evt = eventData?.[0]; + const cameraName = useCameraFriendlyName(evt?.camera); + const displayLabel = evt + ? t("attachment_chip_label", { + label: getTranslatedLabel(evt.label), + camera: cameraName, + }) + : eventId; + + return ( +
    +
    + { + (e.currentTarget as HTMLImageElement).style.visibility = "hidden"; + }} + /> +
    + {evt ? ( + + {displayLabel} + + ) : ( + + )} + + + e.stopPropagation()} + aria-label={t("open_in_explore")} + > + + + + {t("open_in_explore")} + + {mode === "composer" && onRemove && ( + + )} +
    + ); +} diff --git a/web/src/components/chat/ChatComposer.tsx b/web/src/components/chat/ChatComposer.tsx new file mode 100644 index 0000000..5ccfe93 --- /dev/null +++ b/web/src/components/chat/ChatComposer.tsx @@ -0,0 +1,147 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { FaArrowUpLong, FaStop } from "react-icons/fa6"; +import { LuBrain } from "react-icons/lu"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { ChatAttachmentChip } from "@/components/chat/ChatAttachmentChip"; +import { ChatQuickReplies } from "@/components/chat/ChatQuickReplies"; +import { ChatPaperclipButton } from "@/components/chat/ChatPaperclipButton"; + +type ChatComposerProps = { + input: string; + setInput: (value: string) => void; + sendMessage: (textOverride?: string) => void; + placeholder: string; + + supportsThinking: boolean; + thinkingEnabled: boolean; + setThinkingEnabled: (value: boolean | undefined) => void; + + isLoading?: boolean; + onStop?: () => void; + + attachedEventId?: string | null; + onClearAttachment?: () => void; + onAttach?: (eventId: string) => void; + recentEventIds?: string[]; + + large?: boolean; +}; + +export function ChatComposer({ + input, + setInput, + sendMessage, + placeholder, + supportsThinking, + thinkingEnabled, + setThinkingEnabled, + isLoading = false, + onStop, + attachedEventId, + onClearAttachment, + onAttach, + recentEventIds, + large = false, +}: ChatComposerProps) { + const { t } = useTranslation(["views/chat"]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + const showPaperclip = !!onAttach; + const showStop = isLoading && !!onStop; + + return ( +
    + {attachedEventId && onClearAttachment && ( +
    + +
    + )} + {attachedEventId && ( + sendMessage(text)} + disabled={isLoading} + /> + )} +
    + {showPaperclip && ( + + )} + {supportsThinking && ( + + + + + + {t("thinking.toggle")} + + + )} + setInput(e.target.value)} + onKeyDown={handleKeyDown} + aria-busy={isLoading} + /> + {showStop ? ( + + ) : ( + + )} +
    +
    + ); +} diff --git a/web/src/components/chat/ChatEventThumbnailsRow.tsx b/web/src/components/chat/ChatEventThumbnailsRow.tsx new file mode 100644 index 0000000..47b8d46 --- /dev/null +++ b/web/src/components/chat/ChatEventThumbnailsRow.tsx @@ -0,0 +1,100 @@ +import { useApiHost } from "@/api"; +import { baseUrl } from "@/api/baseUrl"; +import { useTranslation } from "react-i18next"; +import { LuExternalLink } from "react-icons/lu"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +type ChatEvent = { id: string; score?: number }; + +type ChatEventThumbnailsRowProps = { + events: ChatEvent[]; + anchor?: { id: string } | null; + onAttach?: (eventId: string) => void; +}; + +/** + * Horizontal scroll row of event thumbnail images for chat. + * Optionally renders an anchor thumbnail with a "reference" badge above the + * results, and per-event similarity scores when provided. + * Clicking a thumbnail calls onAttach; a small external-link overlay opens + * the event in Explore. + * Renders nothing when there is nothing to show. + */ +export function ChatEventThumbnailsRow({ + events, + anchor = null, + onAttach, +}: ChatEventThumbnailsRowProps) { + const apiHost = useApiHost(); + const { t } = useTranslation(["views/chat"]); + + if (events.length === 0 && !anchor) return null; + + const renderThumb = (event: ChatEvent, isAnchor = false) => ( +
    + + + + e.stopPropagation()} + className="absolute right-1 top-1 flex size-6 items-center justify-center rounded bg-black/60 text-white hover:bg-black/80" + aria-label={t("open_in_explore")} + > + + + + {t("open_in_explore")} + + {isAnchor && ( + <> +
    + ); + + return ( +
    + {anchor && ( +
    +
    {renderThumb(anchor, true)}
    +
    + )} + {events.length > 0 && ( +
    +
    + {events.map((event) => renderThumb(event))} +
    +
    + )} +
    + ); +} diff --git a/web/src/components/chat/ChatMessage.tsx b/web/src/components/chat/ChatMessage.tsx new file mode 100644 index 0000000..0a5c027 --- /dev/null +++ b/web/src/components/chat/ChatMessage.tsx @@ -0,0 +1,299 @@ +import { useState, useEffect, useRef, useCallback } from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { useTranslation } from "react-i18next"; +import copy from "copy-to-clipboard"; +import { toast } from "sonner"; +import { FaCopy, FaPencilAlt } from "react-icons/fa"; +import { FaArrowUpLong } from "react-icons/fa6"; +import { LuCheck } from "react-icons/lu"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { ChatAttachmentChip } from "@/components/chat/ChatAttachmentChip"; +import { parseAttachedEvent } from "@/utils/chatUtil"; +import type { ChatStats, ShowStatsMode } from "@/types/chat"; + +type MessageBubbleProps = { + role: "user" | "assistant"; + content: string; + messageIndex?: number; + onEditSubmit?: (messageIndex: number, newContent: string) => void; + isComplete?: boolean; + stats?: ChatStats; + showStats?: ShowStatsMode; +}; + +function formatTokens(n: number | undefined): string | null { + if (n === undefined) return null; + if (n >= 1000) return `${(n / 1000).toFixed(1)}k`; + return String(n); +} + +function formatRate(rate: number | undefined): string | null { + if (rate === undefined || rate <= 0) return null; + return rate >= 10 ? rate.toFixed(0) : rate.toFixed(1); +} + +export function MessageBubble({ + role, + content, + messageIndex = 0, + onEditSubmit, + isComplete = true, + stats, + showStats = "while_generating", +}: MessageBubbleProps) { + const { t } = useTranslation(["views/chat", "common"]); + const isUser = role === "user"; + const [isEditing, setIsEditing] = useState(false); + const [draftContent, setDraftContent] = useState(content); + const editInputRef = useRef(null); + + useEffect(() => { + setDraftContent(content); + }, [content]); + + useEffect(() => { + if (isEditing) { + editInputRef.current?.focus(); + editInputRef.current?.setSelectionRange( + editInputRef.current.value.length, + editInputRef.current.value.length, + ); + } + }, [isEditing]); + + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(() => { + const text = content?.trim() || ""; + if (!text) return; + if (copy(text)) { + setCopied(true); + toast.success(t("button.copiedToClipboard", { ns: "common" })); + setTimeout(() => setCopied(false), 2000); + } + }, [content, t]); + + const handleEditClick = () => { + setDraftContent(content); + setIsEditing(true); + }; + + const handleEditSubmit = () => { + const trimmed = draftContent.trim(); + if (!trimmed || onEditSubmit == null) return; + onEditSubmit(messageIndex, trimmed); + setIsEditing(false); + }; + + const handleEditCancel = () => { + setDraftContent(content); + setIsEditing(false); + }; + + const handleEditKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleEditSubmit(); + } + if (e.key === "Escape") { + handleEditCancel(); + } + }; + + if (isUser && isEditing) { + return ( +
    +